diff --git a/.gitattributes b/.gitattributes index ebc35c6745458..7bc18040742d7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,9 @@ crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf +crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py text eol=crlf +crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap text eol=crlf + crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf @@ -12,9 +15,16 @@ crates/ruff_python_parser/resources/invalid/re_lexing/line_continuation_windows_ crates/ruff_python_parser/resources/invalid/re_lex_logical_token_windows_eol.py text eol=crlf crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text eol=cr +crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py text eol=cr +crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py text eol=lf + +crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py text eol=cr +crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf + crates/ruff_python_parser/resources/inline linguist-generated=true ruff.schema.json -diff linguist-generated=true text=auto eol=lf +ty.schema.json -diff linguist-generated=true text=auto eol=lf crates/ruff_python_ast/src/generated.rs -diff linguist-generated=true text=auto eol=lf crates/ruff_python_formatter/src/generated.rs -diff linguist-generated=true text=auto eol=lf *.md.snap linguist-language=Markdown diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4c58d30794721..43e454d164ad2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,11 +14,11 @@ # flake8-pyi /crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood -# Script for fuzzing the parser/red-knot etc. +# Script for fuzzing the parser/ty etc. /python/py-fuzzer/ @AlexWaygood -# red-knot -/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager +# ty +/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager /crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager -/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager -/crates/red_knot_python_semantic @carljm @AlexWaygood @sharkdp @dcreager +/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager +/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7c58301908321..5d56b28ace965 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: true contact_links: + - name: Report an issue with ty + url: https://github.com/astral-sh/ty/issues/new/choose + about: Please report issues for our type checker ty in the ty repository. - name: Documentation url: https://docs.astral.sh/ruff about: Please consult the documentation before creating an issue. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5b6455590f091..3bc48f3cc6747 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,9 @@ diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index f86b551af83eb..f96c7ead99477 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -6,5 +6,8 @@ self-hosted-runner: labels: - depot-ubuntu-latest-8 - depot-ubuntu-22.04-16 + - depot-ubuntu-22.04-32 + - depot-windows-2022-16 - github-windows-2025-x86_64-8 - github-windows-2025-x86_64-16 + - codspeed-macro diff --git a/.github/mypy-primer-ty.toml b/.github/mypy-primer-ty.toml new file mode 100644 index 0000000000000..9317364fe263f --- /dev/null +++ b/.github/mypy-primer-ty.toml @@ -0,0 +1,8 @@ +#:schema ../ty.schema.json +# Configuration overrides for the mypy primer run + +# Enable off-by-default rules. +[rules] +possibly-unresolved-reference = "warn" +unused-ignore-comment = "warn" +division-by-zero = "warn" diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 02c09a6ad5101..1e1266ecb760c 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -43,13 +43,13 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build sdist" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: command: sdist args: --out dist @@ -72,14 +72,14 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels - x86_64" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: x86_64 args: --release --locked --out dist @@ -114,14 +114,14 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: arm64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels - aarch64" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: aarch64 args: --release --locked --out dist @@ -170,14 +170,14 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: ${{ matrix.platform.arch }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} args: --release --locked --out dist @@ -223,14 +223,14 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.target }} manylinux: auto @@ -298,19 +298,19 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --release --locked --out dist - - uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2 + - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 if: ${{ matrix.platform.arch != 'ppc64' && matrix.platform.arch != 'ppc64le'}} name: Test wheel with: @@ -363,21 +363,21 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.target }} manylinux: musllinux_1_2 args: --release --locked --out dist - name: "Test wheel" if: matrix.target == 'x86_64-unknown-linux-musl' - uses: addnab/docker-run-action@v3 + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 with: image: alpine:latest options: -v ${{ github.workspace }}:/io -w /io @@ -429,19 +429,19 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: target: ${{ matrix.platform.target }} manylinux: musllinux_1_2 args: --release --locked --out dist docker-options: ${{ matrix.platform.maturin_docker_options }} - - uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2 + - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: Test wheel with: arch: ${{ matrix.platform.arch }} diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 212d12425f035..7e2c1c5c2f016 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -38,9 +38,9 @@ jobs: submodules: recursive persist-credentials: false - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -63,7 +63,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.RUFF_BASE_IMG }} # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name @@ -79,7 +79,7 @@ jobs: # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ - name: Build and push by digest id: build - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: ${{ matrix.platform }} @@ -113,17 +113,17 @@ jobs: if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} steps: - name: Download digests - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: /tmp/digests pattern: digests-* merge-multiple: true - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.RUFF_BASE_IMG }} # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version @@ -131,7 +131,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -167,9 +167,9 @@ jobs: - debian:bookworm-slim,bookworm-slim,debian-slim - buildpack-deps:bookworm,bookworm,debian steps: - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -219,7 +219,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 # ghcr.io prefers index level annotations env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index @@ -231,7 +231,7 @@ jobs: ${{ env.TAG_PATTERNS }} - name: Build and push - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -256,17 +256,17 @@ jobs: if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} steps: - name: Download digests - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: /tmp/digests pattern: digests-* merge-multiple: true - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index with: @@ -276,7 +276,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ca4d12bb455d..56b8f2f8f8069 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,8 +36,8 @@ jobs: code: ${{ steps.check_code.outputs.changed }} # Flag that is raised when any code that affects the fuzzer is changed fuzz: ${{ steps.check_fuzzer.outputs.changed }} - # Flag that is set to "true" when code related to red-knot changes. - red_knot: ${{ steps.check_red_knot.outputs.changed }} + # Flag that is set to "true" when code related to ty changes. + ty: ${{ steps.check_ty.outputs.changed }} # Flag that is set to "true" when code related to the playground changes. playground: ${{ steps.check_playground.outputs.changed }} @@ -84,7 +84,7 @@ jobs: if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \ ':Cargo.lock' \ ':crates/**' \ - ':!crates/red_knot*/**' \ + ':!crates/ty*/**' \ ':!crates/ruff_python_formatter/**' \ ':!crates/ruff_formatter/**' \ ':!crates/ruff_dev/**' \ @@ -145,7 +145,7 @@ jobs: run: | if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \ ':!**/*.md' \ - ':crates/red_knot_python_semantic/resources/mdtest/**/*.md' \ + ':crates/ty_python_semantic/resources/mdtest/**/*.md' \ ':!docs/**' \ ':!assets/**' \ ':.github/workflows/ci.yaml' \ @@ -168,15 +168,15 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Check if the red-knot code changed - id: check_red_knot + - name: Check if the ty code changed + id: check_ty env: MERGE_BASE: ${{ steps.merge_base.outputs.sha }} run: | if git diff --quiet "${MERGE_BASE}...HEAD" -- \ ':Cargo.toml' \ ':Cargo.lock' \ - ':crates/red_knot*/**' \ + ':crates/ty*/**' \ ':crates/ruff_db/**' \ ':crates/ruff_annotate_snippets/**' \ ':crates/ruff_python_ast/**' \ @@ -184,6 +184,7 @@ jobs: ':crates/ruff_python_trivia/**' \ ':crates/ruff_source_file/**' \ ':crates/ruff_text_size/**' \ + ':crates/ruff_benchmark/**' \ ':.github/workflows/ci.yaml' \ ; then echo "changed=false" >> "$GITHUB_OUTPUT" @@ -213,7 +214,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: | rustup component add clippy @@ -221,7 +222,7 @@ jobs: - name: "Clippy" run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - name: "Clippy (wasm)" - run: cargo clippy -p ruff_wasm -p red_knot_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings + run: cargo clippy -p ruff_wasm -p ty_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings cargo-test-linux: name: "cargo test (linux)" @@ -233,27 +234,27 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-insta - - name: Red-knot mdtests (GitHub annotations) - if: ${{ needs.determine_changes.outputs.red_knot == 'true' }} + - name: ty mdtests (GitHub annotations) + if: ${{ needs.determine_changes.outputs.ty == 'true' }} env: NO_COLOR: 1 MDTEST_GITHUB_ANNOTATIONS_FORMAT: 1 # Ignore errors if this step fails; we want to continue to later steps in the workflow anyway. # This step is just to get nice GitHub annotations on the PR diff in the files-changed tab. - run: cargo test -p red_knot_python_semantic --test mdtest || true + run: cargo test -p ty_python_semantic --test mdtest || true - name: "Run tests" shell: bash env: @@ -268,7 +269,7 @@ jobs: # sync, not just public items. Eventually we should do this for all # crates; for now add crates here as they are warning-clean to prevent # regression. - - run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items + - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items env: # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). RUSTDOCFLAGS: "-D warnings" @@ -276,6 +277,10 @@ jobs: with: name: ruff path: target/debug/ruff + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ty + path: target/debug/ty cargo-test-linux-release: name: "cargo test (linux, release)" @@ -287,17 +292,17 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-insta - name: "Run tests" @@ -308,7 +313,7 @@ jobs: cargo-test-windows: name: "cargo test (windows)" - runs-on: github-windows-2025-x86_64-16 + runs-on: depot-windows-2022-16 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 @@ -316,11 +321,11 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-nextest - name: "Run tests" @@ -343,10 +348,10 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: "npm" @@ -358,9 +363,9 @@ jobs: run: | cd crates/ruff_wasm wasm-pack test --node - - name: "Test red_knot_wasm" + - name: "Test ty_wasm" run: | - cd crates/red_knot_wasm + cd crates/ty_wasm wasm-pack test --node cargo-build-release: @@ -372,11 +377,11 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1 - name: "Build" run: cargo build --release --locked @@ -395,19 +400,19 @@ jobs: with: file: "Cargo.toml" field: "workspace.package.rust-version" - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" env: MSRV: ${{ steps.msrv.outputs.value }} run: rustup default "${MSRV}" - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-insta - name: "Run tests" @@ -427,13 +432,13 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: "fuzz -> target" - name: "Install Rust toolchain" run: rustup show - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@main + uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1 with: tool: cargo-fuzz@0.11.2 - name: "Install cargo-fuzz" @@ -455,8 +460,8 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download Ruff binary to test id: download-cached-binary with: @@ -489,7 +494,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -500,12 +505,10 @@ jobs: # Verify that adding a plugin or rule produces clean code. - run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes - run: cargo check - - run: cargo fmt --all --check - run: | ./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST ./scripts/add_rule.py --name FirstRule --prefix TST --code 001 --linter test - run: cargo check - - run: cargo fmt --all --check ecosystem: name: "ecosystem" @@ -521,11 +524,11 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download comparison Ruff binary id: ruff-target with: @@ -632,6 +635,53 @@ jobs: name: ecosystem-result path: ecosystem-result + fuzz-ty: + name: "Fuzz for new ty panics" + runs-on: depot-ubuntu-22.04-16 + needs: + - cargo-test-linux + - determine_changes + # Only runs on pull requests, since that is the only we way we can find the base version for comparison. + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.ty == 'true' }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + name: Download new ty binary + id: ty-new + with: + name: ty + path: target/debug + - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 + name: Download baseline ty binary + with: + name: ty + branch: ${{ github.event.pull_request.base.ref }} + workflow: "ci.yaml" + check_artifacts: true + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + - name: Fuzz + env: + FORCE_COLOR: 1 + NEW_TY: ${{ steps.ty-new.outputs.download-path }} + run: | + # Make executable, since artifact download doesn't preserve this + chmod +x "${PWD}/ty" "${NEW_TY}/ty" + + ( + uvx \ + --python="${PYTHON_VERSION}" \ + --from=./python/py-fuzzer \ + fuzz \ + --test-executable="${NEW_TY}/ty" \ + --baseline-executable="${PWD}/ty" \ + --only-new-bugs \ + --bin=ty \ + 0-500 + ) + cargo-shear: name: "cargo shear" runs-on: ubuntu-latest @@ -641,7 +691,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@main + - uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear @@ -654,15 +704,15 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" - uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3 with: args: --out dist - name: "Test wheel" @@ -681,7 +731,11 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 - name: "Cache pre-commit" uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: @@ -708,10 +762,10 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 @@ -720,7 +774,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: Install uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "Install Insiders dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: uv pip install -r docs/requirements-insiders.txt --system @@ -750,7 +804,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Rust toolchain" run: rustup show - name: "Run checks" @@ -769,7 +823,7 @@ jobs: - determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} steps: - - uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 + - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -779,12 +833,12 @@ jobs: persist-credentials: false repository: "astral-sh/ruff-lsp" - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: # installation fails on 3.13 and newer python-version: "3.12" - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download development ruff binary id: ruff-target with: @@ -820,8 +874,8 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "npm" @@ -840,7 +894,7 @@ jobs: run: npm run fmt:check working-directory: playground - benchmarks: + benchmarks-instrumented: runs-on: ubuntu-24.04 needs: determine_changes if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} @@ -851,18 +905,52 @@ jobs: with: persist-credentials: false - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + + - name: "Install Rust toolchain" + run: rustup show + + - name: "Install codspeed" + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 + with: + tool: cargo-codspeed + + - name: "Build benchmarks" + run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark + + - name: "Run benchmarks" + uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0 + with: + run: cargo codspeed run + token: ${{ secrets.CODSPEED_TOKEN }} + + benchmarks-walltime: + runs-on: codspeed-macro + needs: determine_changes + if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }} + timeout-minutes: 20 + env: + TY_LOG: ruff_benchmark=debug + steps: + - name: "Checkout Branch" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7 with: tool: cargo-codspeed - name: "Build benchmarks" - run: cargo codspeed build --features codspeed -p ruff_benchmark + run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark - name: "Run benchmarks" uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index a32ff088d9a54..9b311a99828c1 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -34,12 +34,12 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - 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 diff --git a/.github/workflows/daily_property_tests.yaml b/.github/workflows/daily_property_tests.yaml deleted file mode 100644 index a4afc6d80b63d..0000000000000 --- a/.github/workflows/daily_property_tests.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Daily property test run - -on: - workflow_dispatch: - schedule: - - cron: "0 12 * * *" - pull_request: - paths: - - ".github/workflows/daily_property_tests.yaml" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - CARGO_INCREMENTAL: 0 - CARGO_NET_RETRY: 10 - CARGO_TERM_COLOR: always - RUSTUP_MAX_RETRIES: 10 - FORCE_COLOR: 1 - -jobs: - property_tests: - name: Property tests - runs-on: ubuntu-latest - timeout-minutes: 20 - # Don't run the cron job on forks: - if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - name: "Install Rust toolchain" - run: rustup show - - name: "Install mold" - uses: rui314/setup-mold@v1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - - 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. - run: cargo build --locked --release --package red_knot_python_semantic --tests - - name: Run property tests - shell: bash - run: | - export QUICKCHECK_TESTS=100000 - for _ in {1..5}; do - cargo test --locked --release --package red_knot_python_semantic -- --ignored list::property_tests - cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable - done - - create-issue-on-failure: - name: Create an issue if the daily property test run surfaced any bugs - runs-on: ubuntu-latest - needs: property_tests - if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }} - permissions: - issues: write - steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.issues.create({ - owner: "astral-sh", - repo: "ruff", - title: `Daily property test run failed on ${new Date().toDateString()}`, - body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - labels: ["bug", "red-knot", "testing"], - }) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 2ca8f2d50ee33..2d3b2003114ca 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -5,12 +5,14 @@ permissions: {} on: pull_request: paths: - - "crates/red_knot*/**" + - "crates/ty*/**" - "crates/ruff_db" - "crates/ruff_python_ast" - "crates/ruff_python_parser" - ".github/workflows/mypy_primer.yaml" - ".github/workflows/mypy_primer_comment.yaml" + - "Cargo.lock" + - "!**.md" concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} @@ -21,11 +23,12 @@ env: CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: 1 jobs: mypy_primer: name: Run mypy_primer - runs-on: depot-ubuntu-22.04-16 + runs-on: depot-ubuntu-22.04-32 timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -35,50 +38,24 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: workspaces: "ruff" + - name: Install Rust toolchain run: rustup show - - name: Install mypy_primer - run: | - uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5" - - name: Run mypy_primer shell: bash + env: + PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt + DIFF_FILE: mypy_primer.diff run: | cd ruff - - echo "new commit" - git rev-list --format=%s --max-count=1 "$GITHUB_SHA" - - MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")" - git checkout -b base_commit "$MERGE_BASE" - echo "base commit" - git rev-list --format=%s --max-count=1 base_commit - - cd .. - - # Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs - uvx mypy_primer \ - --repo ruff \ - --type-checker knot \ - --old base_commit \ - --new "$GITHUB_SHA" \ - --project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy|werkzeug|bidict|async-utils|python-chess|dacite|python-htmlgen|paroxython|porcupine|psycopg)$' \ - --output concise \ - --debug > mypy_primer.diff || [ $? -eq 1 ] - - # Output diff with ANSI color codes - cat mypy_primer.diff - - # Remove ANSI color codes before uploading - sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff - - echo ${{ github.event.number }} > pr-number + scripts/mypy_primer.sh + echo ${{ github.event.number }} > ../pr-number - name: Upload diff uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -91,3 +68,41 @@ jobs: with: name: pr-number path: pr-number + + memory_usage: + name: Run memory statistics + runs-on: depot-ubuntu-22.04-32 + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: ruff + fetch-depth: 0 + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + + - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + with: + workspaces: "ruff" + + - name: Install Rust toolchain + run: rustup show + + - name: Run mypy_primer + shell: bash + env: + TY_MAX_PARALLELISM: 1 # for deterministic memory numbers + TY_MEMORY_REPORT: mypy_primer + PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/memory.txt + DIFF_FILE: mypy_primer_memory.diff + run: | + cd ruff + scripts/mypy_primer.sh + + - name: Upload diff + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: mypy_primer_memory_diff + path: mypy_primer_memory.diff diff --git a/.github/workflows/mypy_primer_comment.yaml b/.github/workflows/mypy_primer_comment.yaml index 593a38e79f009..895956e7667a6 100644 --- a/.github/workflows/mypy_primer_comment.yaml +++ b/.github/workflows/mypy_primer_comment.yaml @@ -45,15 +45,28 @@ jobs: if_no_artifact_found: ignore allow_forks: true + - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 + name: "Download mypy_primer memory results" + id: download-mypy_primer_memory_diff + if: steps.pr-number.outputs.pr-number + with: + name: mypy_primer_memory_diff + workflow: mypy_primer.yaml + pr: ${{ steps.pr-number.outputs.pr-number }} + path: pr/mypy_primer_memory_diff + workflow_conclusion: completed + if_no_artifact_found: ignore + allow_forks: true + - name: Generate comment content id: generate-comment - if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true' + if: ${{ steps.download-mypy_primer_diff.outputs.found_artifact == 'true' && steps.download-mypy_primer_memory_diff.outputs.found_artifact == 'true' }} run: | # Guard against malicious mypy_primer results that symlink to a secret # file on this runner - if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]] + if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]] || [[ -L pr/mypy_primer_memory_diff/mypy_primer_memory.diff ]] then - echo "Error: mypy_primer.diff cannot be a symlink" + echo "Error: mypy_primer.diff and mypy_primer_memory.diff cannot be a symlink" exit 1 fi @@ -74,12 +87,24 @@ jobs: echo 'No ecosystem changes detected ✅' >> comment.txt fi + if [ -s "pr/mypy_primer_memory_diff/mypy_primer_memory.diff" ]; then + echo '
' >> comment.txt + echo 'Memory usage changes were detected when running on open source projects' >> comment.txt + echo '' >> comment.txt + echo '```diff' >> comment.txt + cat pr/mypy_primer_memory_diff/mypy_primer_memory.diff >> comment.txt + echo '```' >> comment.txt + echo '
' >> comment.txt + else + echo 'No memory usage changes detected ✅' >> comment.txt + fi + echo 'comment<> "$GITHUB_OUTPUT" cat comment.txt >> "$GITHUB_OUTPUT" echo 'EOF' >> "$GITHUB_OUTPUT" - name: Find existing comment - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 if: steps.generate-comment.outcome == 'success' id: find-comment with: diff --git a/.github/workflows/pr-comment.yaml b/.github/workflows/pr-comment.yaml index e0fad2358a6db..0ef00644c1d6e 100644 --- a/.github/workflows/pr-comment.yaml +++ b/.github/workflows/pr-comment.yaml @@ -70,7 +70,7 @@ jobs: echo 'EOF' >> "$GITHUB_OUTPUT" - name: Find existing comment - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 if: steps.generate-comment.outcome == 'success' id: find-comment with: diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index f0a2520fef4a6..eaa0303296595 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -28,7 +28,7 @@ jobs: ref: ${{ inputs.ref }} persist-credentials: true - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.12 @@ -68,7 +68,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Install Insiders dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} diff --git a/.github/workflows/publish-knot-playground.yml b/.github/workflows/publish-knot-playground.yml deleted file mode 100644 index 071c81f907d16..0000000000000 --- a/.github/workflows/publish-knot-playground.yml +++ /dev/null @@ -1,58 +0,0 @@ -# Publish the Red Knot playground. -name: "[Knot Playground] Release" - -permissions: {} - -on: - push: - branches: [main] - paths: - - "crates/red_knot*/**" - - "crates/ruff_db/**" - - "crates/ruff_python_ast/**" - - "crates/ruff_python_parser/**" - - "playground/**" - - ".github/workflows/publish-knot-playground.yml" - -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true - -env: - CARGO_INCREMENTAL: 0 - CARGO_NET_RETRY: 10 - CARGO_TERM_COLOR: always - RUSTUP_MAX_RETRIES: 10 - -jobs: - publish: - runs-on: ubuntu-latest - env: - CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - name: "Install Rust toolchain" - run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 - with: - node-version: 22 - - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 - - name: "Install Node dependencies" - run: npm ci - working-directory: playground - - name: "Run TypeScript checks" - run: npm run check - working-directory: playground - - name: "Build Knot playground" - run: npm run build --workspace knot-playground - working-directory: playground - - name: "Deploy to Cloudflare Pages" - if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 - with: - apiToken: ${{ secrets.CF_API_TOKEN }} - accountId: ${{ secrets.CF_ACCOUNT_ID }} - # `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production - command: pages deploy playground/knot/dist --project-name=knot-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA} diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index 63d85b7fa083b..e7bf15dde27de 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -29,7 +29,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "npm" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 822a4086e6ade..b8b9238ae6a19 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -22,8 +22,8 @@ jobs: id-token: write steps: - name: "Install uv" - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: wheels-* path: wheels diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml new file mode 100644 index 0000000000000..b5dc37dc475f1 --- /dev/null +++ b/.github/workflows/publish-ty-playground.yml @@ -0,0 +1,58 @@ +# Publish the ty playground. +name: "[ty Playground] Release" + +permissions: {} + +on: + push: + branches: [main] + paths: + - "crates/ty*/**" + - "crates/ruff_db/**" + - "crates/ruff_python_ast/**" + - "crates/ruff_python_parser/**" + - "playground/**" + - ".github/workflows/publish-ty-playground.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +env: + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + CARGO_TERM_COLOR: always + RUSTUP_MAX_RETRIES: 10 + +jobs: + publish: + runs-on: ubuntu-latest + env: + CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: "Install Rust toolchain" + run: rustup target add wasm32-unknown-unknown + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 + - name: "Install Node dependencies" + run: npm ci + working-directory: playground + - name: "Run TypeScript checks" + run: npm run check + working-directory: playground + - name: "Build ty playground" + run: npm run build --workspace ty-playground + working-directory: playground + - name: "Deploy to Cloudflare Pages" + if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + # `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production + command: pages deploy playground/ty/dist --project-name=ty-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA} diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 309e2490dc2db..81193b9352d27 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -45,7 +45,7 @@ jobs: jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json mv /tmp/package.json crates/ruff_wasm/pkg - run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c12aa00fc1161..88048a0bccd04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,7 @@ permissions: # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: + pull_request: workflow_dispatch: inputs: tag: @@ -60,7 +61,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f with: persist-credentials: false submodules: recursive @@ -68,9 +69,9 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.5-prerelease.1/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: cargo-dist-cache path: ~/.cargo/bin/dist @@ -86,7 +87,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@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -123,19 +124,19 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 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@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -153,7 +154,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: artifacts-build-global path: | @@ -174,19 +175,19 @@ jobs: outputs: val: ${{ steps.host.outputs.manifest }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 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@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -200,7 +201,7 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: # Overwrite the previous copy name: artifacts-dist-manifest @@ -250,13 +251,13 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f with: persist-credentials: false submodules: recursive # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: artifacts diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index a3128023f8a07..c384d8868aada 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -39,13 +39,13 @@ jobs: - name: Sync typeshed id: sync run: | - rm -rf ruff/crates/red_knot_vendored/vendor/typeshed - mkdir ruff/crates/red_knot_vendored/vendor/typeshed - cp typeshed/README.md ruff/crates/red_knot_vendored/vendor/typeshed - cp typeshed/LICENSE ruff/crates/red_knot_vendored/vendor/typeshed - cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib - rm -rf ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/@tests - git -C typeshed rev-parse HEAD > ruff/crates/red_knot_vendored/vendor/typeshed/source_commit.txt + rm -rf ruff/crates/ty_vendored/vendor/typeshed + mkdir ruff/crates/ty_vendored/vendor/typeshed + cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed + cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed + cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib + rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests + git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt - name: Commit the changes id: commit if: ${{ steps.sync.outcome == 'success' }} @@ -60,7 +60,7 @@ jobs: cd ruff git push --force origin typeshedbot/sync-typeshed gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr - gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal" + gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty" create-issue-on-failure: name: Create an issue if the typeshed sync failed @@ -79,5 +79,5 @@ jobs: repo: "ruff", title: `Automated typeshed sync failed on ${new Date().toDateString()}`, body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - labels: ["bug", "red-knot"], + labels: ["bug", "ty"], }) diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml new file mode 100644 index 0000000000000..cfefcf6dc52f5 --- /dev/null +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -0,0 +1,138 @@ +name: ty ecosystem-analyzer + +permissions: {} + +on: + pull_request: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + CARGO_TERM_COLOR: always + RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: 1 + REF_NAME: ${{ github.ref_name }} + CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} + +jobs: + ty-ecosystem-analyzer: + name: Compute diagnostic diff + runs-on: depot-ubuntu-22.04-32 + timeout-minutes: 20 + if: contains(github.event.label.name, 'ecosystem-analyzer') + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: ruff + fetch-depth: 0 + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + with: + workspaces: "ruff" + + - name: Install Rust toolchain + run: rustup show + + - name: Compute diagnostic diff + shell: bash + run: | + cd ruff + + echo "Enabling configuration overloads (see .github/mypy-primer-ty.toml)" + mkdir -p ~/.config/ty + cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml + + echo "new commit" + git checkout -b new_commit "$GITHUB_SHA" + git rev-list --format=%s --max-count=1 new_commit + cp crates/ty_python_semantic/resources/primer/good.txt projects_new.txt + + echo "old commit (merge base)" + MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")" + git checkout -b old_commit "$MERGE_BASE" + git rev-list --format=%s --max-count=1 old_commit + cp crates/ty_python_semantic/resources/primer/good.txt projects_old.txt + + cd .. + + uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda" + + ecosystem-analyzer \ + --repository ruff \ + diff \ + --projects-old ruff/projects_old.txt \ + --projects-new ruff/projects_new.txt \ + --old old_commit \ + --new new_commit \ + --output-old diagnostics-old.json \ + --output-new diagnostics-new.json + + mkdir dist + + ecosystem-analyzer \ + generate-diff \ + diagnostics-old.json \ + diagnostics-new.json \ + --old-name "main (merge base)" \ + --new-name "$REF_NAME" \ + --output-html dist/diff.html + + ecosystem-analyzer \ + generate-diff-statistics \ + diagnostics-old.json \ + diagnostics-new.json \ + --old-name "main (merge base)" \ + --new-name "$REF_NAME" \ + --output diff-statistics.md + + echo '## `ecosystem-analyzer` results' > comment.md + echo >> comment.md + cat diff-statistics.md >> comment.md + + cat diff-statistics.md >> "$GITHUB_STEP_SUMMARY" + + echo ${{ github.event.number }} > pr-number + + - name: "Deploy to Cloudflare Pages" + if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} + id: deploy + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 + with: + apiToken: ${{ secrets.CF_API_TOKEN }} + accountId: ${{ secrets.CF_ACCOUNT_ID }} + command: pages deploy dist --project-name=ty-ecosystem --branch ${{ github.head_ref }} --commit-hash ${GITHUB_SHA} + + - name: "Append deployment URL" + if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }} + run: | + echo >> comment.md + echo "**[Full report with detailed diff]($DEPLOYMENT_URL/diff)**" >> comment.md + + - name: Upload comment + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: comment.md + path: comment.md + + - name: Upload pr-number + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: pr-number + path: pr-number + + - name: Upload diagnostics diff + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: diff.html + path: dist/diff.html diff --git a/.github/workflows/ty-ecosystem-analyzer_comment.yaml b/.github/workflows/ty-ecosystem-analyzer_comment.yaml new file mode 100644 index 0000000000000..f237f45e1ed6a --- /dev/null +++ b/.github/workflows/ty-ecosystem-analyzer_comment.yaml @@ -0,0 +1,85 @@ +name: PR comment (ty ecosystem-analyzer) + +on: # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: [ty ecosystem-analyzer] + types: [completed] + workflow_dispatch: + inputs: + workflow_run_id: + description: The ty ecosystem-analyzer workflow that triggers the workflow run + required: true + +jobs: + comment: + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + steps: + - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 + name: Download PR number + with: + name: pr-number + run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }} + if_no_artifact_found: ignore + allow_forks: true + + - name: Parse pull request number + id: pr-number + run: | + if [[ -f pr-number ]] + then + echo "pr-number=$(> "$GITHUB_OUTPUT" + fi + + - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 + name: "Download comment.md" + id: download-comment + if: steps.pr-number.outputs.pr-number + with: + name: comment.md + workflow: ty-ecosystem-analyzer.yaml + pr: ${{ steps.pr-number.outputs.pr-number }} + path: pr/comment + workflow_conclusion: completed + if_no_artifact_found: ignore + allow_forks: true + + - name: Generate comment content + id: generate-comment + if: ${{ steps.download-comment.outputs.found_artifact == 'true' }} + run: | + # Guard against malicious ty ecosystem-analyzer results that symlink to a secret + # file on this runner + if [[ -L pr/comment/comment.md ]] + then + echo "Error: comment.md cannot be a symlink" + exit 1 + fi + + # Note: this identifier is used to find the comment to update on subsequent runs + echo '' > comment.md + echo >> comment.md + cat pr/comment/comment.md >> comment.md + + echo 'comment<> "$GITHUB_OUTPUT" + cat comment.md >> "$GITHUB_OUTPUT" + echo 'EOF' >> "$GITHUB_OUTPUT" + + - name: Find existing comment + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + if: steps.generate-comment.outcome == 'success' + id: find-comment + with: + issue-number: ${{ steps.pr-number.outputs.pr-number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Create or update comment + if: steps.find-comment.outcome == 'success' + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ steps.pr-number.outputs.pr-number }} + body-path: comment.md + edit-mode: replace diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 383dcea02fbd6..a4c61d31d462d 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -10,6 +10,7 @@ rules: ignore: - build-docker.yml - publish-playground.yml + - ty-ecosystem-analyzer.yaml excessive-permissions: # it's hard to test what the impact of removing these ignores would be # without actually running the release workflow... diff --git a/.markdownlint.yaml b/.markdownlint.yaml index ca458564d8667..a361d0ab68947 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -29,3 +29,7 @@ MD024: # # Ref: https://github.com/astral-sh/ruff/pull/15011#issuecomment-2544790854 MD046: false + +# Link text should be descriptive +# Disallows link text like *here* which is annoying. +MD059: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6192e40e56f5e..1abbe47ce3785 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,10 @@ fail_fast: false exclude: | (?x)^( .github/workflows/release.yml| - crates/red_knot_vendored/vendor/.*| - crates/red_knot_project/resources/.*| + crates/ty_vendored/vendor/.*| + crates/ty_project/resources/.*| + crates/ty_python_semantic/resources/corpus/.*| + crates/ty/docs/(configuration|rules|cli|environment).md| crates/ruff_benchmark/resources/.*| crates/ruff_linter/resources/.*| crates/ruff_linter/src/rules/.*/snapshots/.*| @@ -42,7 +44,7 @@ repos: )$ - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.44.0 + rev: v0.45.0 hooks: - id: markdownlint-fix exclude: | @@ -65,7 +67,7 @@ repos: - black==25.1.0 - repo: https://github.com/crate-ci/typos - rev: v1.31.1 + rev: v1.34.0 hooks: - id: typos @@ -79,7 +81,7 @@ repos: pass_filenames: false # This makes it a lot faster - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.12.2 hooks: - id: ruff-format - id: ruff @@ -89,7 +91,7 @@ repos: # Prettier - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.5.3 + rev: v3.6.2 hooks: - id: prettier types: [yaml] @@ -97,12 +99,12 @@ repos: # zizmor detects security vulnerabilities in GitHub Actions workflows. # Additional configuration for the tool is found in `.github/zizmor.yml` - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.5.2 + rev: v1.11.0 hooks: - id: zizmor - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.0 + rev: 0.33.2 hooks: - id: check-github-workflows diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 5124ef6a13841..7dc0583db580d 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,39 @@ # Breaking Changes +## 0.12.0 + +- **Detection of more syntax errors** + + Ruff now detects version-related syntax errors, such as the use of the `match` + statement on Python versions before 3.10, and syntax errors emitted by + CPython's compiler, such as irrefutable `match` patterns before the final + `case` arm. + +- **New default Python version handling for syntax errors** + + Ruff will default to the _latest_ supported Python version (3.13) when + checking for the version-related syntax errors mentioned above to prevent + false positives in projects without a Python version configured. The default + in all other cases, like applying lint rules, is unchanged and remains at the + minimum supported Python version (3.9). + +- **Updated f-string formatting** + + Ruff now formats multi-line f-strings with format specifiers to avoid adding a + line break after the format specifier. This addresses a change to the Python + grammar in version 3.13.4 that made such a line break a syntax error. + +- **`rust-toolchain.toml` is no longer included in source distributions** + + The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's + minimum supported Rust version (MSRV) for development and building release + artifacts. However, when present in source distributions, it would also cause + downstream package maintainers to pull in the same Rust toolchain, even if + their available toolchain was MSRV-compatible. + +- **[`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/) + (`S320`) has been removed** + ## 0.11.0 This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`. diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e57f3f6e056..87db909184365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3828 +1,354 @@ # Changelog -## 0.11.6 +## 0.12.3 ### Preview features -- Avoid adding whitespace to the end of a docstring after an escaped quote ([#17216](https://github.com/astral-sh/ruff/pull/17216)) -- \[`airflow`\] Extract `AIR311` from `AIR301` rules (`AIR301`, `AIR311`) ([#17310](https://github.com/astral-sh/ruff/pull/17310), [#17422](https://github.com/astral-sh/ruff/pull/17422)) +- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063)) +- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213)) +- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922)) ### Bug fixes -- Raise syntax error when `\` is at end of file ([#17409](https://github.com/astral-sh/ruff/pull/17409)) - -## 0.11.5 - -### Preview features - -- \[`airflow`\] Add missing `AIR302` attribute check ([#17115](https://github.com/astral-sh/ruff/pull/17115)) -- \[`airflow`\] Expand module path check to individual symbols (`AIR302`) ([#17278](https://github.com/astral-sh/ruff/pull/17278)) -- \[`airflow`\] Extract `AIR312` from `AIR302` rules (`AIR302`, `AIR312`) ([#17152](https://github.com/astral-sh/ruff/pull/17152)) -- \[`airflow`\] Update oudated `AIR301`, `AIR302` rules ([#17123](https://github.com/astral-sh/ruff/pull/17123)) -- [syntax-errors] Async comprehension in sync comprehension ([#17177](https://github.com/astral-sh/ruff/pull/17177)) -- [syntax-errors] Check annotations in annotated assignments ([#17283](https://github.com/astral-sh/ruff/pull/17283)) -- [syntax-errors] Extend annotation checks to `await` ([#17282](https://github.com/astral-sh/ruff/pull/17282)) - -### Bug fixes - -- \[`flake8-pie`\] Avoid false positive for multiple assignment with `auto()` (`PIE796`) ([#17274](https://github.com/astral-sh/ruff/pull/17274)) - -### Rule changes - -- \[`ruff`\] Fix `RUF100` to detect unused file-level `noqa` directives with specific codes (#17042) ([#17061](https://github.com/astral-sh/ruff/pull/17061)) -- \[`flake8-pytest-style`\] Avoid false positive for legacy form of `pytest.raises` (`PT011`) ([#17231](https://github.com/astral-sh/ruff/pull/17231)) +- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433)) +- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220)) +- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150)) +- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131)) ### Documentation -- Fix formatting of "See Style Guide" link ([#17272](https://github.com/astral-sh/ruff/pull/17272)) - -## 0.11.4 - -### Preview features - -- \[`ruff`\] Implement `invalid-rule-code` as `RUF102` ([#17138](https://github.com/astral-sh/ruff/pull/17138)) -- [syntax-errors] Detect duplicate keys in `match` mapping patterns ([#17129](https://github.com/astral-sh/ruff/pull/17129)) -- [syntax-errors] Detect duplicate attributes in `match` class patterns ([#17186](https://github.com/astral-sh/ruff/pull/17186)) -- [syntax-errors] Detect invalid syntax in annotations ([#17101](https://github.com/astral-sh/ruff/pull/17101)) - -### Bug fixes - -- [syntax-errors] Fix multiple assignment error for class fields in `match` patterns ([#17184](https://github.com/astral-sh/ruff/pull/17184)) -- Don't skip visiting non-tuple slice in `typing.Annotated` subscripts ([#17201](https://github.com/astral-sh/ruff/pull/17201)) +- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103)) +- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111)) +- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151)) +- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189)) +- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191)) +- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190)) +- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218)) +- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127)) +- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949)) +- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241)) -## 0.11.3 +## 0.12.2 ### Preview features -- \[`airflow`\] Add more autofixes for `AIR302` ([#16876](https://github.com/astral-sh/ruff/pull/16876), [#16977](https://github.com/astral-sh/ruff/pull/16977), [#16976](https://github.com/astral-sh/ruff/pull/16976), [#16965](https://github.com/astral-sh/ruff/pull/16965)) -- \[`airflow`\] Move `AIR301` to `AIR002` ([#16978](https://github.com/astral-sh/ruff/pull/16978)) -- \[`airflow`\] Move `AIR302` to `AIR301` and `AIR303` to `AIR302` ([#17151](https://github.com/astral-sh/ruff/pull/17151)) -- \[`flake8-bandit`\] Mark `str` and `list[str]` literals as trusted input (`S603`) ([#17136](https://github.com/astral-sh/ruff/pull/17136)) -- \[`ruff`\] Support slices in `RUF005` ([#17078](https://github.com/astral-sh/ruff/pull/17078)) -- [syntax-errors] Start detecting compile-time syntax errors ([#16106](https://github.com/astral-sh/ruff/pull/16106)) -- [syntax-errors] Duplicate type parameter names ([#16858](https://github.com/astral-sh/ruff/pull/16858)) -- [syntax-errors] Irrefutable `case` pattern before final case ([#16905](https://github.com/astral-sh/ruff/pull/16905)) -- [syntax-errors] Multiple assignments in `case` pattern ([#16957](https://github.com/astral-sh/ruff/pull/16957)) -- [syntax-errors] Single starred assignment target ([#17024](https://github.com/astral-sh/ruff/pull/17024)) -- [syntax-errors] Starred expressions in `return`, `yield`, and `for` ([#17134](https://github.com/astral-sh/ruff/pull/17134)) -- [syntax-errors] Store to or delete `__debug__` ([#16984](https://github.com/astral-sh/ruff/pull/16984)) - -### Bug fixes - -- Error instead of `panic!` when running Ruff from a deleted directory (#16903) ([#17054](https://github.com/astral-sh/ruff/pull/17054)) -- [syntax-errors] Fix false positive for parenthesized tuple index ([#16948](https://github.com/astral-sh/ruff/pull/16948)) - -### CLI - -- Check `pyproject.toml` correctly when it is passed via stdin ([#16971](https://github.com/astral-sh/ruff/pull/16971)) - -### Configuration +- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572)) +- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683)) + +### Bug fixes + +- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616)) +- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042)) +- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930)) +- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678)) +- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058)) +- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021)) +- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899)) + +### Rule changes -- \[`flake8-import-conventions`\] Add import `numpy.typing as npt` to default `flake8-import-conventions.aliases` ([#17133](https://github.com/astral-sh/ruff/pull/17133)) +- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967)) +- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963)) +- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682)) ### Documentation -- \[`refurb`\] Document why `UserDict`, `UserList`, and `UserString` are preferred over `dict`, `list`, and `str` (`FURB189`) ([#16927](https://github.com/astral-sh/ruff/pull/16927)) +- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733)) +- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095)) +- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988)) +- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989)) +- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974)) +- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993)) +- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002)) +- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975)) +- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977)) +- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978)) +- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990)) +- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017)) +- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049)) +- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054)) +- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051)) +- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055)) +- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056)) +- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061)) +- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097)) +- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101)) +- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080)) +- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079)) +- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104)) +- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105)) +- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106)) +- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113)) +- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109)) +- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110)) +- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098)) +- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970)) -## 0.11.2 +## 0.12.1 ### Preview features -- [syntax-errors] Fix false-positive syntax errors emitted for annotations on variadic parameters before Python 3.11 ([#16878](https://github.com/astral-sh/ruff/pull/16878)) - -## 0.11.1 - -### Preview features - -- \[`airflow`\] Add `chain`, `chain_linear` and `cross_downstream` for `AIR302` ([#16647](https://github.com/astral-sh/ruff/pull/16647)) -- [syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors ([#16581](https://github.com/astral-sh/ruff/pull/16581)) -- [syntax-errors] PEP 701 f-strings before Python 3.12 ([#16543](https://github.com/astral-sh/ruff/pull/16543)) -- [syntax-errors] Parenthesized context managers before Python 3.9 ([#16523](https://github.com/astral-sh/ruff/pull/16523)) -- [syntax-errors] Star annotations before Python 3.11 ([#16545](https://github.com/astral-sh/ruff/pull/16545)) -- [syntax-errors] Star expression in index before Python 3.11 ([#16544](https://github.com/astral-sh/ruff/pull/16544)) -- [syntax-errors] Unparenthesized assignment expressions in sets and indexes ([#16404](https://github.com/astral-sh/ruff/pull/16404)) +- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867)) +- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763)) +- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547)) +- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400)) +- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862)) +- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888)) ### Bug fixes -- Server: Allow `FixAll` action in presence of version-specific syntax errors ([#16848](https://github.com/astral-sh/ruff/pull/16848)) -- \[`flake8-bandit`\] Allow raw strings in `suspicious-mark-safe-usage` (`S308`) #16702 ([#16770](https://github.com/astral-sh/ruff/pull/16770)) -- \[`refurb`\] Avoid panicking `unwrap` in `verbose-decimal-constructor` (`FURB157`) ([#16777](https://github.com/astral-sh/ruff/pull/16777)) -- \[`refurb`\] Fix starred expressions fix (`FURB161`) ([#16550](https://github.com/astral-sh/ruff/pull/16550)) -- Fix `--statistics` reporting for unsafe fixes ([#16756](https://github.com/astral-sh/ruff/pull/16756)) - -### Rule changes - -- \[`flake8-executables`\] Allow `uv run` in shebang line for `shebang-missing-python` (`EXE003`) ([#16849](https://github.com/astral-sh/ruff/pull/16849),[#16855](https://github.com/astral-sh/ruff/pull/16855)) - -### CLI +- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801)) +- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805)) +- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737)) +- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779)) +- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792)) +- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782)) +- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788)) +- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793)) +- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794)) +- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213)) +- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637)) +- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803)) +- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866)) +- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885)) +- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857)) +- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832)) +- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856)) +- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925)) +- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756)) +- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858)) +- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690)) +- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892)) + +### Rule changes + +- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882)) +- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780)) +- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767)) +- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753)) +- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755)) +- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768)) +- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710)) +- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903)) +- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839)) +- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936)) +- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630)) +- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836)) +- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633)) +- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541)) +- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233)) +- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735)) + +### Server + +- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910)) + +### Documentation + +- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842)) +- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718)) +- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786)) +- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743)) +- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829)) +- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884)) +- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666)) +- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870)) -- Add `--exit-non-zero-on-format` ([#16009](https://github.com/astral-sh/ruff/pull/16009)) - -### Documentation - -- Update Ruff tutorial to avoid non-existent fix in `__init__.py` ([#16818](https://github.com/astral-sh/ruff/pull/16818)) -- \[`flake8-gettext`\] Swap `format-` and `printf-in-get-text-func-call` examples (`INT002`, `INT003`) ([#16769](https://github.com/astral-sh/ruff/pull/16769)) - -## 0.11.0 - -This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`. - -### Breaking changes - -- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319)) - - In previous versions of Ruff, you could specify your Python version with: - - - The `target-version` option in a `ruff.toml` file or the `[tool.ruff]` section of a pyproject.toml file. - - The `project.requires-python` field in a `pyproject.toml` file with a `[tool.ruff]` section. - - These options worked well in most cases, and are still recommended for fine control of the Python version. However, because of the way Ruff discovers config files, `pyproject.toml` files without a `[tool.ruff]` section would be ignored, including the `requires-python` setting. Ruff would then use the default Python version (3.9 as of this writing) instead, which is surprising when you've attempted to request another version. - - In v0.10, config discovery has been updated to address this issue: - - - If Ruff finds a `ruff.toml` file without a `target-version`, it will check - for a `pyproject.toml` file in the same directory and respect its - `requires-python` version, even if it does not contain a `[tool.ruff]` - section. - - If Ruff finds a user-level configuration, the `requires-python` field of the closest `pyproject.toml` in a parent directory will take precedence. - - If there is no config file (`ruff.toml`or `pyproject.toml` with a - `[tool.ruff]` section) in the directory of the file being checked, Ruff will - search for the closest `pyproject.toml` in the parent directories and use its - `requires-python` setting. - -### Stabilization - -The following behaviors have been stabilized: - -- [`blanket-noqa`](https://docs.astral.sh/ruff/rules/blanket-noqa/) (`PGH004`): Also detect blanked file-level noqa comments (and not just line level comments). - -### Preview features +### Other changes -- [syntax-errors] Tuple unpacking in `for` statement iterator clause before Python 3.9 ([#16558](https://github.com/astral-sh/ruff/pull/16558)) +- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708)) +- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840)) +- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838)) -## 0.10.0 +## 0.12.0 -Check out the [blog post](https://astral.sh/blog/ruff-v0.10.0) for a migration guide and overview of the changes! +Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration +guide and overview of the changes! ### Breaking changes -See also, the "Remapped rules" section which may result in disabled rules. +- **Detection of more syntax errors** -- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319)) + Ruff now detects version-related syntax errors, such as the use of the `match` + statement on Python versions before 3.10, and syntax errors emitted by + CPython's compiler, such as irrefutable `match` patterns before the final + `case` arm. - Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0. - You can find a description of this change in the 0.11.0 section. +- **New default Python version handling for syntax errors** -- **Updated `TYPE_CHECKING` behavior** ([#16669](https://github.com/astral-sh/ruff/pull/16669)) + Ruff will default to the *latest* supported Python version (3.13) when + checking for the version-related syntax errors mentioned above to prevent + false positives in projects without a Python version configured. The default + in all other cases, like applying lint rules, is unchanged and remains at the + minimum supported Python version (3.9). - Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead. +- **Updated f-string formatting** -- **More robust noqa parsing** ([#16483](https://github.com/astral-sh/ruff/pull/16483)) + Ruff now formats multi-line f-strings with format specifiers to avoid adding a + line break after the format specifier. This addresses a change to the Python + grammar in version 3.13.4 that made such a line break a syntax error. - The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [*Error suppression*](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification. +- **`rust-toolchain.toml` is no longer included in source distributions** -- **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#14005](https://github.com/astral-sh/ruff/pull/14005)) + The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's + minimum supported Rust version (MSRV) for development and building release + artifacts. However, when present in source distributions, it would also cause + downstream package maintainers to pull in the same Rust toolchain, even if + their available toolchain was MSRV-compatible. - This change fixes a bug in the formatter where it introduced unnecessary parentheses around with statements with a single context manager and a trailing comment. This change may result in a change in formatting for some users. +### Removed Rules -- **Bump alpine default tag to 3.21 for derived Docker images** ([#16456](https://github.com/astral-sh/ruff/pull/16456)) +The following rules have been removed: - Alpine 3.21 was released in Dec 2024 and is used in the official Alpine-based Python images. Now the ruff:alpine image will use 3.21 instead of 3.20 and ruff:alpine3.20 will no longer be updated. +- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/) + (`S320`) ### Deprecated Rules The following rules have been deprecated: -- [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance/) (`UP038`) -- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/) (`S320`) - -### Remapped rules - -The following rules have been remapped to new rule codes: - -- \[`unsafe-markup-use`\]: `RUF035` to `S704` +- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/) ### Stabilization The following rules have been stabilized and are no longer in preview: -- [`batched-without-explicit-strict`](https://docs.astral.sh/ruff/rules/batched-without-explicit-strict) (`B911`) -- [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable) (`C420`) -- [`datetime-min-max`](https://docs.astral.sh/ruff/rules/datetime-min-max) (`DTZ901`) -- [`fast-api-unused-path-parameter`](https://docs.astral.sh/ruff/rules/fast-api-unused-path-parameter) (`FAST003`) -- [`root-logger-call`](https://docs.astral.sh/ruff/rules/root-logger-call) (`LOG015`) -- [`len-test`](https://docs.astral.sh/ruff/rules/len-test) (`PLC1802`) -- [`shallow-copy-environ`](https://docs.astral.sh/ruff/rules/shallow-copy-environ) (`PLW1507`) -- [`os-listdir`](https://docs.astral.sh/ruff/rules/os-listdir) (`PTH208`) -- [`invalid-pathlib-with-suffix`](https://docs.astral.sh/ruff/rules/invalid-pathlib-with-suffix) (`PTH210`) -- [`invalid-assert-message-literal-argument`](https://docs.astral.sh/ruff/rules/invalid-assert-message-literal-argument) (`RUF040`) -- [`unnecessary-nested-literal`](https://docs.astral.sh/ruff/rules/unnecessary-nested-literal) (`RUF041`) -- [`unnecessary-cast-to-int`](https://docs.astral.sh/ruff/rules/unnecessary-cast-to-int) (`RUF046`) -- [`map-int-version-parsing`](https://docs.astral.sh/ruff/rules/map-int-version-parsing) (`RUF048`) -- [`if-key-in-dict-del`](https://docs.astral.sh/ruff/rules/if-key-in-dict-del) (`RUF051`) -- [`unsafe-markup-use`](https://docs.astral.sh/ruff/rules/unsafe-markup-use) (`S704`). This rule has also been renamed from `RUF035`. -- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string) (`SIM905`) -- [`runtime-cast-value`](https://docs.astral.sh/ruff/rules/runtime-cast-value) (`TC006`) -- [`unquoted-type-alias`](https://docs.astral.sh/ruff/rules/unquoted-type-alias) (`TC007`) -- [`non-pep646-unpack`](https://docs.astral.sh/ruff/rules/non-pep646-unpack) (`UP044`) +- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`) +- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`) +- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`) +- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`) +- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`) +- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`) +- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`) +- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`) +- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`) +- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`) +- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`) +- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`) +- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`) +- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`) +- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`) +- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`) +- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`) +- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`) +- [`non-pep604-annotation-optional`] (`UP045`) +- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`) +- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`) +- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`) The following behaviors have been stabilized: -- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) [`invalid-first-argument-name-for-class-method`](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/) (`N804`): `__new__` methods are now no longer flagged by `invalid-first-argument-name-for-class-method` (`N804`) but instead by `bad-staticmethod-argument` (`PLW0211`) -- [`bad-str-strip-call`](https://docs.astral.sh/ruff/rules/bad-str-strip-call/) (`PLE1310`): The rule now applies to objects which are known to have type `str` or `bytes`. -- [`custom-type-var-for-self`](https://docs.astral.sh/ruff/rules/custom-type-var-for-self/) (`PYI019`): More accurate detection of custom `TypeVars` replaceable by `Self`. The range of the diagnostic is now the full function header rather than just the return annotation. -- [`invalid-argument-name`](https://docs.astral.sh/ruff/rules/invalid-argument-name/) (`N803`): Ignore argument names of functions decorated with `typing.override` -- [`invalid-envvar-default`](https://docs.astral.sh/ruff/rules/invalid-envvar-default/) (`PLW1508`): Detect default value arguments to `os.environ.get` with invalid type. -- [`pytest-raises-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-raises-with-multiple-statements/) (`PT012`) [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements/) (`PT031`): Allow `for` statements with an empty body in `pytest.raises` and `pytest.warns` `with` statements. -- [`redundant-open-modes`](https://docs.astral.sh/ruff/rules/redundant-open-modes/) (`UP015`): The diagnostic range is now the range of the redundant mode argument where it previously was the range of the entire open call. You may have to replace your `noqa` comments when suppressing `UP015`. -- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`): Changes the default value of `lint.flake8-builtins.strict-checking` from `true` to `false`. -- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`): Now also recognizes `type(expr) is type(None)` comparisons where `expr` isn't a name expression. - -The following fixes or improvements to fixes have been stabilized: - -- [`repeated-equality-comparison`](https://docs.astral.sh/ruff/rules/repeated-equality-comparison/) (`PLR1714`) ([#16685](https://github.com/astral-sh/ruff/pull/16685)) -- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) ([#16684](https://github.com/astral-sh/ruff/pull/16684)) -- [`unused-private-type-var`](https://docs.astral.sh/ruff/rules/unused-private-type-var/) (`PYI018`) ([#16682](https://github.com/astral-sh/ruff/pull/16682)) - -### Server - -- Remove logging output for `ruff.printDebugInformation` ([#16617](https://github.com/astral-sh/ruff/pull/16617)) - -### Configuration - -- \[`flake8-builtins`\] Deprecate the `builtins-` prefixed options in favor of the unprefixed options (e.g. `builtins-allowed-modules` is now deprecated in favor of `allowed-modules`) ([#16092](https://github.com/astral-sh/ruff/pull/16092)) - -### Bug fixes - -- [flake8-bandit] Fix mixed-case hash algorithm names (S324) ([#16552](https://github.com/astral-sh/ruff/pull/16552)) - -### CLI - -- [ruff] Fix `last_tag`/`commits_since_last_tag` for `version` command ([#16686](https://github.com/astral-sh/ruff/pull/16686)) - -## 0.9.10 - -### Preview features - -- \[`ruff`\] Add new rule `RUF059`: Unused unpacked assignment ([#16449](https://github.com/astral-sh/ruff/pull/16449)) -- \[`syntax-errors`\] Detect assignment expressions before Python 3.8 ([#16383](https://github.com/astral-sh/ruff/pull/16383)) -- \[`syntax-errors`\] Named expressions in decorators before Python 3.9 ([#16386](https://github.com/astral-sh/ruff/pull/16386)) -- \[`syntax-errors`\] Parenthesized keyword argument names after Python 3.8 ([#16482](https://github.com/astral-sh/ruff/pull/16482)) -- \[`syntax-errors`\] Positional-only parameters before Python 3.8 ([#16481](https://github.com/astral-sh/ruff/pull/16481)) -- \[`syntax-errors`\] Tuple unpacking in `return` and `yield` before Python 3.8 ([#16485](https://github.com/astral-sh/ruff/pull/16485)) -- \[`syntax-errors`\] Type parameter defaults before Python 3.13 ([#16447](https://github.com/astral-sh/ruff/pull/16447)) -- \[`syntax-errors`\] Type parameter lists before Python 3.12 ([#16479](https://github.com/astral-sh/ruff/pull/16479)) -- \[`syntax-errors`\] `except*` before Python 3.11 ([#16446](https://github.com/astral-sh/ruff/pull/16446)) -- \[`syntax-errors`\] `type` statements before Python 3.12 ([#16478](https://github.com/astral-sh/ruff/pull/16478)) - -### Bug fixes - -- Escape template filenames in glob patterns in configuration ([#16407](https://github.com/astral-sh/ruff/pull/16407)) -- \[`flake8-simplify`\] Exempt unittest context methods for `SIM115` rule ([#16439](https://github.com/astral-sh/ruff/pull/16439)) -- Formatter: Fix syntax error location in notebooks ([#16499](https://github.com/astral-sh/ruff/pull/16499)) -- \[`pyupgrade`\] Do not offer fix when at least one target is `global`/`nonlocal` (`UP028`) ([#16451](https://github.com/astral-sh/ruff/pull/16451)) -- \[`flake8-builtins`\] Ignore variables matching module attribute names (`A001`) ([#16454](https://github.com/astral-sh/ruff/pull/16454)) -- \[`pylint`\] Convert `code` keyword argument to a positional argument in fix for (`PLR1722`) ([#16424](https://github.com/astral-sh/ruff/pull/16424)) - -### CLI - -- Move rule code from `description` to `check_name` in GitLab output serializer ([#16437](https://github.com/astral-sh/ruff/pull/16437)) - -### Documentation - -- \[`pydocstyle`\] Clarify that `D417` only checks docstrings with an arguments section ([#16494](https://github.com/astral-sh/ruff/pull/16494)) - -## 0.9.9 - -### Preview features - -- Fix caching of unsupported-syntax errors ([#16425](https://github.com/astral-sh/ruff/pull/16425)) - -### Bug fixes - -- Only show unsupported-syntax errors in editors when preview mode is enabled ([#16429](https://github.com/astral-sh/ruff/pull/16429)) - -## 0.9.8 - -### Preview features - -- Start detecting version-related syntax errors in the parser ([#16090](https://github.com/astral-sh/ruff/pull/16090)) - -### Rule changes - -- \[`pylint`\] Mark fix unsafe (`PLW1507`) ([#16343](https://github.com/astral-sh/ruff/pull/16343)) -- \[`pylint`\] Catch `case np.nan`/`case math.nan` in `match` statements (`PLW0177`) ([#16378](https://github.com/astral-sh/ruff/pull/16378)) -- \[`ruff`\] Add more Pydantic models variants to the list of default copy semantics (`RUF012`) ([#16291](https://github.com/astral-sh/ruff/pull/16291)) - -### Server - -- Avoid indexing the project if `configurationPreference` is `editorOnly` ([#16381](https://github.com/astral-sh/ruff/pull/16381)) -- Avoid unnecessary info at non-trace server log level ([#16389](https://github.com/astral-sh/ruff/pull/16389)) -- Expand `ruff.configuration` to allow inline config ([#16296](https://github.com/astral-sh/ruff/pull/16296)) -- Notify users for invalid client settings ([#16361](https://github.com/astral-sh/ruff/pull/16361)) - -### Configuration - -- Add `per-file-target-version` option ([#16257](https://github.com/astral-sh/ruff/pull/16257)) - -### Bug fixes - -- \[`refurb`\] Do not consider docstring(s) (`FURB156`) ([#16391](https://github.com/astral-sh/ruff/pull/16391)) -- \[`flake8-self`\] Ignore attribute accesses on instance-like variables (`SLF001`) ([#16149](https://github.com/astral-sh/ruff/pull/16149)) -- \[`pylint`\] Fix false positives, add missing methods, and support positional-only parameters (`PLE0302`) ([#16263](https://github.com/astral-sh/ruff/pull/16263)) -- \[`flake8-pyi`\] Mark `PYI030` fix unsafe when comments are deleted ([#16322](https://github.com/astral-sh/ruff/pull/16322)) - -### Documentation - -- Fix example for `S611` ([#16316](https://github.com/astral-sh/ruff/pull/16316)) -- Normalize inconsistent markdown headings in docstrings ([#16364](https://github.com/astral-sh/ruff/pull/16364)) -- Document MSRV policy ([#16384](https://github.com/astral-sh/ruff/pull/16384)) - -## 0.9.7 - -### Preview features - -- Consider `__new__` methods as special function type for enforcing class method or static method rules ([#13305](https://github.com/astral-sh/ruff/pull/13305)) -- \[`airflow`\] Improve the internal logic to differentiate deprecated symbols (`AIR303`) ([#16013](https://github.com/astral-sh/ruff/pull/16013)) -- \[`refurb`\] Manual timezone monkeypatching (`FURB162`) ([#16113](https://github.com/astral-sh/ruff/pull/16113)) -- \[`ruff`\] Implicit class variable in dataclass (`RUF045`) ([#14349](https://github.com/astral-sh/ruff/pull/14349)) -- \[`ruff`\] Skip singleton starred expressions for `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#16083](https://github.com/astral-sh/ruff/pull/16083)) -- \[`refurb`\] Check for subclasses includes subscript expressions (`FURB189`) ([#16155](https://github.com/astral-sh/ruff/pull/16155)) - -### Rule changes - -- \[`flake8-debugger`\] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#16191](https://github.com/astral-sh/ruff/pull/16191)) -- \[`pycodestyle`\] Exempt `site.addsitedir(...)` calls (`E402`) ([#16251](https://github.com/astral-sh/ruff/pull/16251)) - -### Formatter - -- Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values ([#16187](https://github.com/astral-sh/ruff/pull/16187)) - -### Server - -- Fix handling of requests received after shutdown message ([#16262](https://github.com/astral-sh/ruff/pull/16262)) -- Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code actions for a notebook cell ([#16154](https://github.com/astral-sh/ruff/pull/16154)) -- Include document specific debug info for `ruff.printDebugInformation` ([#16215](https://github.com/astral-sh/ruff/pull/16215)) -- Update server to return the debug info as string with `ruff.printDebugInformation` ([#16214](https://github.com/astral-sh/ruff/pull/16214)) - -### CLI - -- Warn on invalid `noqa` even when there are no diagnostics ([#16178](https://github.com/astral-sh/ruff/pull/16178)) -- Better error messages while loading configuration `extend`s ([#15658](https://github.com/astral-sh/ruff/pull/15658)) - -### Bug fixes - -- \[`flake8-comprehensions`\] Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110)) -- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141)) -- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011)) -- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080)) -- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132)) -- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219)) -- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091)) -- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919)) -- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237)) -- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122)) - -### Documentation - -- Add FAQ entry for `source.*` code actions in Notebook ([#16212](https://github.com/astral-sh/ruff/pull/16212)) -- Add `SECURITY.md` ([#16224](https://github.com/astral-sh/ruff/pull/16224)) - -## 0.9.6 - -### Preview features - -- \[`airflow`\] Add `external_task.{ExternalTaskMarker, ExternalTaskSensor}` for `AIR302` ([#16014](https://github.com/astral-sh/ruff/pull/16014)) -- \[`flake8-builtins`\] Make strict module name comparison optional (`A005`) ([#15951](https://github.com/astral-sh/ruff/pull/15951)) -- \[`flake8-pyi`\] Extend fix to Python \<= 3.9 for `redundant-none-literal` (`PYI061`) ([#16044](https://github.com/astral-sh/ruff/pull/16044)) -- \[`pylint`\] Also report when the object isn't a literal (`PLE1310`) ([#15985](https://github.com/astral-sh/ruff/pull/15985)) -- \[`ruff`\] Implement `indented-form-feed` (`RUF054`) ([#16049](https://github.com/astral-sh/ruff/pull/16049)) -- \[`ruff`\] Skip type definitions for `missing-f-string-syntax` (`RUF027`) ([#16054](https://github.com/astral-sh/ruff/pull/16054)) - -### Rule changes - -- \[`flake8-annotations`\] Correct syntax for `typing.Union` in suggested return type fixes for `ANN20x` rules ([#16025](https://github.com/astral-sh/ruff/pull/16025)) -- \[`flake8-builtins`\] Match upstream module name comparison (`A005`) ([#16006](https://github.com/astral-sh/ruff/pull/16006)) -- \[`flake8-comprehensions`\] Detect overshadowed `list`/`set`/`dict`, ignore variadics and named expressions (`C417`) ([#15955](https://github.com/astral-sh/ruff/pull/15955)) -- \[`flake8-pie`\] Remove following comma correctly when the unpacked dictionary is empty (`PIE800`) ([#16008](https://github.com/astral-sh/ruff/pull/16008)) -- \[`flake8-simplify`\] Only trigger `SIM401` on known dictionaries ([#15995](https://github.com/astral-sh/ruff/pull/15995)) -- \[`pylint`\] Do not report calls when object type and argument type mismatch, remove custom escape handling logic (`PLE1310`) ([#15984](https://github.com/astral-sh/ruff/pull/15984)) -- \[`pyupgrade`\] Comments within parenthesized value ranges should not affect applicability (`UP040`) ([#16027](https://github.com/astral-sh/ruff/pull/16027)) -- \[`pyupgrade`\] Don't introduce invalid syntax when upgrading old-style type aliases with parenthesized multiline values (`UP040`) ([#16026](https://github.com/astral-sh/ruff/pull/16026)) -- \[`pyupgrade`\] Ensure we do not rename two type parameters to the same name (`UP049`) ([#16038](https://github.com/astral-sh/ruff/pull/16038)) -- \[`pyupgrade`\] \[`ruff`\] Don't apply renamings if the new name is shadowed in a scope of one of the references to the binding (`UP049`, `RUF052`) ([#16032](https://github.com/astral-sh/ruff/pull/16032)) -- \[`ruff`\] Update `RUF009` to behave similar to `B008` and ignore attributes with immutable types ([#16048](https://github.com/astral-sh/ruff/pull/16048)) - -### Server - -- Root exclusions in the server to project root ([#16043](https://github.com/astral-sh/ruff/pull/16043)) - -### Bug fixes - -- \[`flake8-datetime`\] Ignore `.replace()` calls while looking for `.astimezone` ([#16050](https://github.com/astral-sh/ruff/pull/16050)) -- \[`flake8-type-checking`\] Avoid `TC004` false positive where the runtime definition is provided by `__getattr__` ([#16052](https://github.com/astral-sh/ruff/pull/16052)) - -### Documentation - -- Improve `ruff-lsp` migration document ([#16072](https://github.com/astral-sh/ruff/pull/16072)) -- Undeprecate `ruff.nativeServer` ([#16039](https://github.com/astral-sh/ruff/pull/16039)) - -## 0.9.5 - -### Preview features - -- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719)) -- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837)) -- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799)) -- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853)) -- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821)) -- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889)) -- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854)) -- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762)) -- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872)) -- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862)) -- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905)) -- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832)) -- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841)) -- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922)) -- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999)) -- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888)) - -### Rule changes - -- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818)) -- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838)) -- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885)) -- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954)) -- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840)) -- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921)) -- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980)) - -### Configuration - -- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918)) -- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992)) - -### Bug fixes - -- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877)) -- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929)) -- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851)) -- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988)) -- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829)) -- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930)) -- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953)) -- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779)) - -### Documentation - -- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850)) -- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882)) -- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946)) -- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909)) -- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844)) -- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928)) -- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956)) -- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982)) -- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883)) - -## 0.9.4 +- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in + addition to list literals and variables. +- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe. +- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify + expressions to use `or` instead of an `if` expression, where possible. +- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well + as inline comments. +- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings, + as well as lists and tuples of literal strings, as trusted input. +- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that + include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to + plain `bool` annotations. +- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules. + `UP007` now applies only to `typing.Union`, while + [`non-pep604-annotation-optional`] (`UP045`) checks for use of + `typing.Optional`. `UP045` has also been stabilized in this release, but you + may need to update existing `include`, `ignore`, or `noqa` settings to + accommodate this change. ### Preview features -- \[`airflow`\] Extend airflow context parameter check for `BaseOperator.execute` (`AIR302`) ([#15713](https://github.com/astral-sh/ruff/pull/15713)) -- \[`airflow`\] Update `AIR302` to check for deprecated context keys ([#15144](https://github.com/astral-sh/ruff/pull/15144)) -- \[`flake8-bandit`\] Permit suspicious imports within stub files (`S4`) ([#15822](https://github.com/astral-sh/ruff/pull/15822)) -- \[`pylint`\] Do not trigger `PLR6201` on empty collections ([#15732](https://github.com/astral-sh/ruff/pull/15732)) -- \[`refurb`\] Do not emit diagnostic when loop variables are used outside loop body (`FURB122`) ([#15757](https://github.com/astral-sh/ruff/pull/15757)) -- \[`ruff`\] Add support for more `re` patterns (`RUF055`) ([#15764](https://github.com/astral-sh/ruff/pull/15764)) -- \[`ruff`\] Check for shadowed `map` before suggesting fix (`RUF058`) ([#15790](https://github.com/astral-sh/ruff/pull/15790)) -- \[`ruff`\] Do not emit diagnostic when all arguments to `zip()` are variadic (`RUF058`) ([#15744](https://github.com/astral-sh/ruff/pull/15744)) -- \[`ruff`\] Parenthesize fix when argument spans multiple lines for `unnecessary-round` (`RUF057`) ([#15703](https://github.com/astral-sh/ruff/pull/15703)) - -### Rule changes - -- Preserve quote style in generated code ([#15726](https://github.com/astral-sh/ruff/pull/15726), [#15778](https://github.com/astral-sh/ruff/pull/15778), [#15794](https://github.com/astral-sh/ruff/pull/15794)) -- \[`flake8-bugbear`\] Exempt `NewType` calls where the original type is immutable (`B008`) ([#15765](https://github.com/astral-sh/ruff/pull/15765)) -- \[`pylint`\] Honor banned top-level imports by `TID253` in `PLC0415`. ([#15628](https://github.com/astral-sh/ruff/pull/15628)) -- \[`pyupgrade`\] Ignore `is_typeddict` and `TypedDict` for `deprecated-import` (`UP035`) ([#15800](https://github.com/astral-sh/ruff/pull/15800)) - -### CLI - -- Fix formatter warning message for `flake8-quotes` option ([#15788](https://github.com/astral-sh/ruff/pull/15788)) -- Implement tab autocomplete for `ruff config` ([#15603](https://github.com/astral-sh/ruff/pull/15603)) +- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368)) +- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664)) ### Bug fixes -- \[`flake8-comprehensions`\] Do not emit `unnecessary-map` diagnostic when lambda has different arity (`C417`) ([#15802](https://github.com/astral-sh/ruff/pull/15802)) -- \[`flake8-comprehensions`\] Parenthesize `sorted` when needed for `unnecessary-call-around-sorted` (`C413`) ([#15825](https://github.com/astral-sh/ruff/pull/15825)) -- \[`pyupgrade`\] Handle end-of-line comments for `quoted-annotation` (`UP037`) ([#15824](https://github.com/astral-sh/ruff/pull/15824)) - -### Documentation - -- Add missing config docstrings ([#15803](https://github.com/astral-sh/ruff/pull/15803)) -- Add references to `trio.run_process` and `anyio.run_process` ([#15761](https://github.com/astral-sh/ruff/pull/15761)) -- Use `uv init --lib` in tutorial ([#15718](https://github.com/astral-sh/ruff/pull/15718)) - -## 0.9.3 - -### Preview features - -- \[`airflow`\] Argument `fail_stop` in DAG has been renamed as `fail_fast` (`AIR302`) ([#15633](https://github.com/astral-sh/ruff/pull/15633)) -- \[`airflow`\] Extend `AIR303` with more symbols ([#15611](https://github.com/astral-sh/ruff/pull/15611)) -- \[`flake8-bandit`\] Report all references to suspicious functions (`S3`) ([#15541](https://github.com/astral-sh/ruff/pull/15541)) -- \[`flake8-pytest-style`\] Do not emit diagnostics for empty `for` loops (`PT012`, `PT031`) ([#15542](https://github.com/astral-sh/ruff/pull/15542)) -- \[`flake8-simplify`\] Avoid double negations (`SIM103`) ([#15562](https://github.com/astral-sh/ruff/pull/15562)) -- \[`pyflakes`\] Fix infinite loop with unused local import in `__init__.py` (`F401`) ([#15517](https://github.com/astral-sh/ruff/pull/15517)) -- \[`pylint`\] Do not report methods with only one `EM101`-compatible `raise` (`PLR6301`) ([#15507](https://github.com/astral-sh/ruff/pull/15507)) -- \[`pylint`\] Implement `redefined-slots-in-subclass` (`W0244`) ([#9640](https://github.com/astral-sh/ruff/pull/9640)) -- \[`pyupgrade`\] Add rules to use PEP 695 generics in classes and functions (`UP046`, `UP047`) ([#15565](https://github.com/astral-sh/ruff/pull/15565), [#15659](https://github.com/astral-sh/ruff/pull/15659)) -- \[`refurb`\] Implement `for-loop-writes` (`FURB122`) ([#10630](https://github.com/astral-sh/ruff/pull/10630)) -- \[`ruff`\] Implement `needless-else` clause (`RUF047`) ([#15051](https://github.com/astral-sh/ruff/pull/15051)) -- \[`ruff`\] Implement `starmap-zip` (`RUF058`) ([#15483](https://github.com/astral-sh/ruff/pull/15483)) +- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706)) +- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542)) +- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673)) +- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704)) +- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611)) +- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428)) +- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472)) +- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479)) +- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688)) +- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592)) +- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614)) +- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598)) +- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631)) +- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509)) +- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665)) ### Rule changes -- \[`flake8-bugbear`\] Do not raise error if keyword argument is present and target-python version is less or equals than 3.9 (`B903`) ([#15549](https://github.com/astral-sh/ruff/pull/15549)) -- \[`flake8-comprehensions`\] strip parentheses around generators in `unnecessary-generator-set` (`C401`) ([#15553](https://github.com/astral-sh/ruff/pull/15553)) -- \[`flake8-pytest-style`\] Rewrite references to `.exception` (`PT027`) ([#15680](https://github.com/astral-sh/ruff/pull/15680)) -- \[`flake8-simplify`\] Mark fixes as unsafe (`SIM201`, `SIM202`) ([#15626](https://github.com/astral-sh/ruff/pull/15626)) -- \[`flake8-type-checking`\] Fix some safe fixes being labeled unsafe (`TC006`,`TC008`) ([#15638](https://github.com/astral-sh/ruff/pull/15638)) -- \[`isort`\] Omit trailing whitespace in `unsorted-imports` (`I001`) ([#15518](https://github.com/astral-sh/ruff/pull/15518)) -- \[`pydoclint`\] Allow ignoring one line docstrings for `DOC` rules ([#13302](https://github.com/astral-sh/ruff/pull/13302)) -- \[`pyflakes`\] Apply redefinition fixes by source code order (`F811`) ([#15575](https://github.com/astral-sh/ruff/pull/15575)) -- \[`pyflakes`\] Avoid removing too many imports in `redefined-while-unused` (`F811`) ([#15585](https://github.com/astral-sh/ruff/pull/15585)) -- \[`pyflakes`\] Group redefinition fixes by source statement (`F811`) ([#15574](https://github.com/astral-sh/ruff/pull/15574)) -- \[`pylint`\] Include name of base class in message for `redefined-slots-in-subclass` (`W0244`) ([#15559](https://github.com/astral-sh/ruff/pull/15559)) -- \[`ruff`\] Update fix for `RUF055` to use `var == value` ([#15605](https://github.com/astral-sh/ruff/pull/15605)) - -### Formatter - -- Fix bracket spacing for single-element tuples in f-string expressions ([#15537](https://github.com/astral-sh/ruff/pull/15537)) -- Fix unstable f-string formatting for expressions containing a trailing comma ([#15545](https://github.com/astral-sh/ruff/pull/15545)) - -### Performance - -- Avoid quadratic membership check in import fixes ([#15576](https://github.com/astral-sh/ruff/pull/15576)) +- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408)) +- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532)) +- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311)) +- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545)) +- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478)) +- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645)) ### Server -- Allow `unsafe-fixes` settings for code actions ([#15666](https://github.com/astral-sh/ruff/pull/15666)) - -### Bug fixes - -- \[`flake8-bandit`\] Add missing single-line/dotall regex flag (`S608`) ([#15654](https://github.com/astral-sh/ruff/pull/15654)) -- \[`flake8-import-conventions`\] Fix infinite loop between `ICN001` and `I002` (`ICN001`) ([#15480](https://github.com/astral-sh/ruff/pull/15480)) -- \[`flake8-simplify`\] Do not emit diagnostics for expressions inside string type annotations (`SIM222`, `SIM223`) ([#15405](https://github.com/astral-sh/ruff/pull/15405)) -- \[`pyflakes`\] Treat arguments passed to the `default=` parameter of `TypeVar` as type expressions (`F821`) ([#15679](https://github.com/astral-sh/ruff/pull/15679)) -- \[`pyupgrade`\] Avoid syntax error when the iterable is a non-parenthesized tuple (`UP028`) ([#15543](https://github.com/astral-sh/ruff/pull/15543)) -- \[`ruff`\] Exempt `NewType` calls where the original type is immutable (`RUF009`) ([#15588](https://github.com/astral-sh/ruff/pull/15588)) -- Preserve raw string prefix and escapes in all codegen fixes ([#15694](https://github.com/astral-sh/ruff/pull/15694)) +- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627)) ### Documentation -- Generate documentation redirects for lowercase rule codes ([#15564](https://github.com/astral-sh/ruff/pull/15564)) -- `TRY300`: Add some extra notes on not catching exceptions you didn't expect ([#15036](https://github.com/astral-sh/ruff/pull/15036)) - -## 0.9.2 - -### Preview features - -- \[`airflow`\] Fix typo "security_managr" to "security_manager" (`AIR303`) ([#15463](https://github.com/astral-sh/ruff/pull/15463)) -- \[`airflow`\] extend and fix AIR302 rules ([#15525](https://github.com/astral-sh/ruff/pull/15525)) -- \[`fastapi`\] Handle parameters with `Depends` correctly (`FAST003`) ([#15364](https://github.com/astral-sh/ruff/pull/15364)) -- \[`flake8-pytest-style`\] Implement pytest.warns diagnostics (`PT029`, `PT030`, `PT031`) ([#15444](https://github.com/astral-sh/ruff/pull/15444)) -- \[`flake8-pytest-style`\] Test function parameters with default arguments (`PT028`) ([#15449](https://github.com/astral-sh/ruff/pull/15449)) -- \[`flake8-type-checking`\] Avoid false positives for `|` in `TC008` ([#15201](https://github.com/astral-sh/ruff/pull/15201)) - -### Rule changes - -- \[`flake8-todos`\] Allow VSCode GitHub PR extension style links in `missing-todo-link` (`TD003`) ([#15519](https://github.com/astral-sh/ruff/pull/15519)) -- \[`pyflakes`\] Show syntax error message for `F722` ([#15523](https://github.com/astral-sh/ruff/pull/15523)) - -### Formatter - -- Fix curly bracket spacing around f-string expressions containing curly braces ([#15471](https://github.com/astral-sh/ruff/pull/15471)) -- Fix joining of f-strings with different quotes when using quote style `Preserve` ([#15524](https://github.com/astral-sh/ruff/pull/15524)) - -### Server - -- Avoid indexing the same workspace multiple times ([#15495](https://github.com/astral-sh/ruff/pull/15495)) -- Display context for `ruff.configuration` errors ([#15452](https://github.com/astral-sh/ruff/pull/15452)) - -### Configuration - -- Remove `flatten` to improve deserialization error messages ([#15414](https://github.com/astral-sh/ruff/pull/15414)) - -### Bug fixes - -- Parse triple-quoted string annotations as if parenthesized ([#15387](https://github.com/astral-sh/ruff/pull/15387)) -- \[`fastapi`\] Update `Annotated` fixes (`FAST002`) ([#15462](https://github.com/astral-sh/ruff/pull/15462)) -- \[`flake8-bandit`\] Check for `builtins` instead of `builtin` (`S102`, `PTH123`) ([#15443](https://github.com/astral-sh/ruff/pull/15443)) -- \[`flake8-pathlib`\] Fix `--select` for `os-path-dirname` (`PTH120`) ([#15446](https://github.com/astral-sh/ruff/pull/15446)) -- \[`ruff`\] Fix false positive on global keyword (`RUF052`) ([#15235](https://github.com/astral-sh/ruff/pull/15235)) - -## 0.9.1 - -### Preview features - -- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308)) -- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376)) - -### Rule changes - -- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389)) - -### Formatter - -- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378)) - -### Server - -- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398)) - -### Bug fixes - -- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394)) -- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337)) - -## 0.9.0 - -Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes! - -### Breaking changes - -Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes. - -This release doesn’t remove or remap any existing stable rules. - -### Stabilization - -The following rules have been stabilized and are no longer in preview: - -- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`). - This rule has also been renamed: previously, it was called `builtin-module-shadowing`. -- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`) -- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`) -- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`) -- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`) -- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`) -- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`) - -The following behaviors have been stabilized: - -- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments. -- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements. -- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/). -- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files. - -The following fixes or improvements to fixes have been stabilized: +- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709)) +- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491)) +- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613)) +- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615)) -- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`) -- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`) +## 0.11.x -### Formatter +See [changelogs/0.11.x](./changelogs/0.11.x.md) -This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes: +## 0.10.x -- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594)) -- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860)) -- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766)) -- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539)) -- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457)) -- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123)) -- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457)) -- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513)) -- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933)) -- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381)) -- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282)) -- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276)) -- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523)) +See [changelogs/0.10.x](./changelogs/0.10.x.md) -### Preview features - -- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601)) -- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180)) -- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310)) -- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313)) -- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299)) -- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258)) - -### Rule changes - -- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350)) -- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298)) - -### Server - -- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel) - which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didn’t contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232)) -- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373)) - -### CLI - -- Improve the error message for `--config key=value` when the `key` is for a table and it’s a simple `value` - -### Bug fixes - -- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330)) -- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365)) -- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356)) -- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311)) -- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309)) -- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273)) -- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318)) -- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277)) - -## 0.8.6 - -### Preview features +## 0.9.x -- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126)) -- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104)) -- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234)) +See [changelogs/0.9.x](./changelogs/0.9.x.md) -### Rule changes - -- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175)) -- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215)) - -### CLI - -- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237)) - -### Bug fixes - -- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230)) -- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250)) - -## 0.8.5 +## 0.8.x -### Preview features - -- \[`airflow`\] Extend names moved from core to provider (`AIR303`) ([#15145](https://github.com/astral-sh/ruff/pull/15145), [#15159](https://github.com/astral-sh/ruff/pull/15159), [#15196](https://github.com/astral-sh/ruff/pull/15196), [#15216](https://github.com/astral-sh/ruff/pull/15216)) -- \[`airflow`\] Extend rule to check class attributes, methods, arguments (`AIR302`) ([#15054](https://github.com/astral-sh/ruff/pull/15054), [#15083](https://github.com/astral-sh/ruff/pull/15083)) -- \[`fastapi`\] Update `FAST002` to check keyword-only arguments ([#15119](https://github.com/astral-sh/ruff/pull/15119)) -- \[`flake8-type-checking`\] Disable `TC006` and `TC007` in stub files ([#15179](https://github.com/astral-sh/ruff/pull/15179)) -- \[`pylint`\] Detect nested methods correctly (`PLW1641`) ([#15032](https://github.com/astral-sh/ruff/pull/15032)) -- \[`ruff`\] Detect more strict-integer expressions (`RUF046`) ([#14833](https://github.com/astral-sh/ruff/pull/14833)) -- \[`ruff`\] Implement `falsy-dict-get-fallback` (`RUF056`) ([#15160](https://github.com/astral-sh/ruff/pull/15160)) -- \[`ruff`\] Implement `unnecessary-round` (`RUF057`) ([#14828](https://github.com/astral-sh/ruff/pull/14828)) - -### Rule changes +See [changelogs/0.8.x](./changelogs/0.8.x.md) -- Visit PEP 764 inline `TypedDict` keys as non-type-expressions ([#15073](https://github.com/astral-sh/ruff/pull/15073)) -- \[`flake8-comprehensions`\] Skip `C416` if comprehension contains unpacking ([#14909](https://github.com/astral-sh/ruff/pull/14909)) -- \[`flake8-pie`\] Allow `cast(SomeType, ...)` (`PIE796`) ([#15141](https://github.com/astral-sh/ruff/pull/15141)) -- \[`flake8-simplify`\] More precise inference for dictionaries (`SIM300`) ([#15164](https://github.com/astral-sh/ruff/pull/15164)) -- \[`flake8-use-pathlib`\] Catch redundant joins in `PTH201` and avoid syntax errors ([#15177](https://github.com/astral-sh/ruff/pull/15177)) -- \[`pycodestyle`\] Preserve original value format (`E731`) ([#15097](https://github.com/astral-sh/ruff/pull/15097)) -- \[`pydocstyle`\] Split on first whitespace character (`D403`) ([#15082](https://github.com/astral-sh/ruff/pull/15082)) -- \[`pyupgrade`\] Add all PEP-585 names to `UP006` rule ([#5454](https://github.com/astral-sh/ruff/pull/5454)) +## 0.7.x -### Configuration +See [changelogs/0.7.x](./changelogs/0.7.x.md) -- \[`flake8-type-checking`\] Improve flexibility of `runtime-evaluated-decorators` ([#15204](https://github.com/astral-sh/ruff/pull/15204)) -- \[`pydocstyle`\] Add setting to ignore missing documentation for `*args` and `**kwargs` parameters (`D417`) ([#15210](https://github.com/astral-sh/ruff/pull/15210)) -- \[`ruff`\] Add an allowlist for `unsafe-markup-use` (`RUF035`) ([#15076](https://github.com/astral-sh/ruff/pull/15076)) +## 0.6.x -### Bug fixes +See [changelogs/0.6.x](./changelogs/0.6.x.md) -- Fix type subscript on older python versions ([#15090](https://github.com/astral-sh/ruff/pull/15090)) -- Use `TypeChecker` for detecting `fastapi` routes ([#15093](https://github.com/astral-sh/ruff/pull/15093)) -- \[`pycodestyle`\] Avoid false positives and negatives related to type parameter default syntax (`E225`, `E251`) ([#15214](https://github.com/astral-sh/ruff/pull/15214)) +## 0.5.x -### Documentation +See [changelogs/0.5.x](./changelogs/0.5.x.md) -- Fix incorrect doc in `shebang-not-executable` (`EXE001`) and add git+windows solution to executable bit ([#15208](https://github.com/astral-sh/ruff/pull/15208)) -- Rename rules currently not conforming to naming convention ([#15102](https://github.com/astral-sh/ruff/pull/15102)) +## 0.4.x -## 0.8.4 +See [changelogs/0.4.x](./changelogs/0.4.x.md) -### Preview features +## 0.3.x -- \[`airflow`\] Extend `AIR302` with additional functions and classes ([#15015](https://github.com/astral-sh/ruff/pull/15015)) -- \[`airflow`\] Implement `moved-to-provider-in-3` for modules that has been moved to Airflow providers (`AIR303`) ([#14764](https://github.com/astral-sh/ruff/pull/14764)) -- \[`flake8-use-pathlib`\] Extend check for invalid path suffix to include the case `"."` (`PTH210`) ([#14902](https://github.com/astral-sh/ruff/pull/14902)) -- \[`perflint`\] Fix panic in `PERF401` when list variable is after the `for` loop ([#14971](https://github.com/astral-sh/ruff/pull/14971)) -- \[`perflint`\] Simplify finding the loop target in `PERF401` ([#15025](https://github.com/astral-sh/ruff/pull/15025)) -- \[`pylint`\] Preserve original value format (`PLR6104`) ([#14978](https://github.com/astral-sh/ruff/pull/14978)) -- \[`ruff`\] Avoid false positives for `RUF027` for typing context bindings ([#15037](https://github.com/astral-sh/ruff/pull/15037)) -- \[`ruff`\] Check for ambiguous pattern passed to `pytest.raises()` (`RUF043`) ([#14966](https://github.com/astral-sh/ruff/pull/14966)) +See [changelogs/0.3.x](./changelogs/0.3.x.md) -### Rule changes +## 0.2.x -- \[`flake8-bandit`\] Check `S105` for annotated assignment ([#15059](https://github.com/astral-sh/ruff/pull/15059)) -- \[`flake8-pyi`\] More autofixes for `redundant-none-literal` (`PYI061`) ([#14872](https://github.com/astral-sh/ruff/pull/14872)) -- \[`pydocstyle`\] Skip leading whitespace for `D403` ([#14963](https://github.com/astral-sh/ruff/pull/14963)) -- \[`ruff`\] Skip `SQLModel` base classes for `mutable-class-default` (`RUF012`) ([#14949](https://github.com/astral-sh/ruff/pull/14949)) +See [changelogs/0.2.x](./changelogs/0.2.x.md) -### Bug +## 0.1.x -- \[`perflint`\] Parenthesize walrus expressions in autofix for `manual-list-comprehension` (`PERF401`) ([#15050](https://github.com/astral-sh/ruff/pull/15050)) - -### Server - -- Check diagnostic refresh support from client capability which enables dynamic configuration for various editors ([#15014](https://github.com/astral-sh/ruff/pull/15014)) - -## 0.8.3 - -### Preview features - -- Fix fstring formatting removing overlong implicit concatenated string in expression part ([#14811](https://github.com/astral-sh/ruff/pull/14811)) -- \[`airflow`\] Add fix to remove deprecated keyword arguments (`AIR302`) ([#14887](https://github.com/astral-sh/ruff/pull/14887)) -- \[`airflow`\]: Extend rule to include deprecated names for Airflow 3.0 (`AIR302`) ([#14765](https://github.com/astral-sh/ruff/pull/14765) and [#14804](https://github.com/astral-sh/ruff/pull/14804)) -- \[`flake8-bugbear`\] Improve error messages for `except*` (`B025`, `B029`, `B030`, `B904`) ([#14815](https://github.com/astral-sh/ruff/pull/14815)) -- \[`flake8-bugbear`\] `itertools.batched()` without explicit `strict` (`B911`) ([#14408](https://github.com/astral-sh/ruff/pull/14408)) -- \[`flake8-use-pathlib`\] Dotless suffix passed to `Path.with_suffix()` (`PTH210`) ([#14779](https://github.com/astral-sh/ruff/pull/14779)) -- \[`pylint`\] Include parentheses and multiple comparators in check for `boolean-chained-comparison` (`PLR1716`) ([#14781](https://github.com/astral-sh/ruff/pull/14781)) -- \[`ruff`\] Do not simplify `round()` calls (`RUF046`) ([#14832](https://github.com/astral-sh/ruff/pull/14832)) -- \[`ruff`\] Don't emit `used-dummy-variable` on function parameters (`RUF052`) ([#14818](https://github.com/astral-sh/ruff/pull/14818)) -- \[`ruff`\] Implement `if-key-in-dict-del` (`RUF051`) ([#14553](https://github.com/astral-sh/ruff/pull/14553)) -- \[`ruff`\] Mark autofix for `RUF052` as always unsafe ([#14824](https://github.com/astral-sh/ruff/pull/14824)) -- \[`ruff`\] Teach autofix for `used-dummy-variable` about TypeVars etc. (`RUF052`) ([#14819](https://github.com/astral-sh/ruff/pull/14819)) - -### Rule changes - -- \[`flake8-bugbear`\] Offer unsafe autofix for `no-explicit-stacklevel` (`B028`) ([#14829](https://github.com/astral-sh/ruff/pull/14829)) -- \[`flake8-pyi`\] Skip all type definitions in `string-or-bytes-too-long` (`PYI053`) ([#14797](https://github.com/astral-sh/ruff/pull/14797)) -- \[`pyupgrade`\] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (`UP009`) ([#14728](https://github.com/astral-sh/ruff/pull/14728)) -- \[`pyupgrade`\] Mark fixes for `convert-typed-dict-functional-to-class` and `convert-named-tuple-functional-to-class` as unsafe if they will remove comments (`UP013`, `UP014`) ([#14842](https://github.com/astral-sh/ruff/pull/14842)) - -### Bug fixes - -- Raise syntax error for mixing `except` and `except*` ([#14895](https://github.com/astral-sh/ruff/pull/14895)) -- \[`flake8-bugbear`\] Fix `B028` to allow `stacklevel` to be explicitly assigned as a positional argument ([#14868](https://github.com/astral-sh/ruff/pull/14868)) -- \[`flake8-bugbear`\] Skip `B028` if `warnings.warn` is called with `*args` or `**kwargs` ([#14870](https://github.com/astral-sh/ruff/pull/14870)) -- \[`flake8-comprehensions`\] Skip iterables with named expressions in `unnecessary-map` (`C417`) ([#14827](https://github.com/astral-sh/ruff/pull/14827)) -- \[`flake8-pyi`\] Also remove `self` and `cls`'s annotation (`PYI034`) ([#14801](https://github.com/astral-sh/ruff/pull/14801)) -- \[`flake8-pytest-style`\] Fix `pytest-parametrize-names-wrong-type` (`PT006`) to edit both `argnames` and `argvalues` if both of them are single-element tuples/lists ([#14699](https://github.com/astral-sh/ruff/pull/14699)) -- \[`perflint`\] Improve autofix for `PERF401` ([#14369](https://github.com/astral-sh/ruff/pull/14369)) -- \[`pylint`\] Fix `PLW1508` false positive for default string created via a mult operation ([#14841](https://github.com/astral-sh/ruff/pull/14841)) - -## 0.8.2 - -### Preview features - -- \[`airflow`\] Avoid deprecated values (`AIR302`) ([#14582](https://github.com/astral-sh/ruff/pull/14582)) -- \[`airflow`\] Extend removed names for `AIR302` ([#14734](https://github.com/astral-sh/ruff/pull/14734)) -- \[`ruff`\] Extend `unnecessary-regular-expression` to non-literal strings (`RUF055`) ([#14679](https://github.com/astral-sh/ruff/pull/14679)) -- \[`ruff`\] Implement `used-dummy-variable` (`RUF052`) ([#14611](https://github.com/astral-sh/ruff/pull/14611)) -- \[`ruff`\] Implement `unnecessary-cast-to-int` (`RUF046`) ([#14697](https://github.com/astral-sh/ruff/pull/14697)) - -### Rule changes - -- \[`airflow`\] Check `AIR001` from builtin or providers `operators` module ([#14631](https://github.com/astral-sh/ruff/pull/14631)) -- \[`flake8-pytest-style`\] Remove `@` in `pytest.mark.parametrize` rule messages ([#14770](https://github.com/astral-sh/ruff/pull/14770)) -- \[`pandas-vet`\] Skip rules if the `panda` module hasn't been seen ([#14671](https://github.com/astral-sh/ruff/pull/14671)) -- \[`pylint`\] Fix false negatives for `ascii` and `sorted` in `len-as-condition` (`PLC1802`) ([#14692](https://github.com/astral-sh/ruff/pull/14692)) -- \[`refurb`\] Guard `hashlib` imports and mark `hashlib-digest-hex` fix as safe (`FURB181`) ([#14694](https://github.com/astral-sh/ruff/pull/14694)) - -### Configuration - -- \[`flake8-import-conventions`\] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias` (`ICN001`) ([#14745](https://github.com/astral-sh/ruff/pull/14745)) - -### Bug fixes - -- Revert: [pyflakes] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) (#14615) ([#14726](https://github.com/astral-sh/ruff/pull/14726)) -- \[`pep8-naming`\] Avoid false positive for `class Bar(type(foo))` (`N804`) ([#14683](https://github.com/astral-sh/ruff/pull/14683)) -- \[`pycodestyle`\] Handle f-strings properly for `invalid-escape-sequence` (`W605`) ([#14748](https://github.com/astral-sh/ruff/pull/14748)) -- \[`pylint`\] Ignore `@overload` in `PLR0904` ([#14730](https://github.com/astral-sh/ruff/pull/14730)) -- \[`refurb`\] Handle non-finite decimals in `verbose-decimal-constructor` (`FURB157`) ([#14596](https://github.com/astral-sh/ruff/pull/14596)) -- \[`ruff`\] Avoid emitting `assignment-in-assert` when all references to the assigned variable are themselves inside `assert`s (`RUF018`) ([#14661](https://github.com/astral-sh/ruff/pull/14661)) - -### Documentation - -- Improve docs for `flake8-use-pathlib` rules ([#14741](https://github.com/astral-sh/ruff/pull/14741)) -- Improve error messages and docs for `flake8-comprehensions` rules ([#14729](https://github.com/astral-sh/ruff/pull/14729)) -- \[`flake8-type-checking`\] Expands `TC006` docs to better explain itself ([#14749](https://github.com/astral-sh/ruff/pull/14749)) - -## 0.8.1 - -### Preview features - -- Formatter: Avoid invalid syntax for format-spec with quotes for all Python versions ([#14625](https://github.com/astral-sh/ruff/pull/14625)) -- Formatter: Consider quotes inside format-specs when choosing the quotes for an f-string ([#14493](https://github.com/astral-sh/ruff/pull/14493)) -- Formatter: Do not consider f-strings with escaped newlines as multiline ([#14624](https://github.com/astral-sh/ruff/pull/14624)) -- Formatter: Fix f-string formatting in assignment statement ([#14454](https://github.com/astral-sh/ruff/pull/14454)) -- Formatter: Fix unnecessary space around power operator (`**`) in overlong f-string expressions ([#14489](https://github.com/astral-sh/ruff/pull/14489)) -- \[`airflow`\] Avoid implicit `schedule` argument to `DAG` and `@dag` (`AIR301`) ([#14581](https://github.com/astral-sh/ruff/pull/14581)) -- \[`flake8-builtins`\] Exempt private built-in modules (`A005`) ([#14505](https://github.com/astral-sh/ruff/pull/14505)) -- \[`flake8-pytest-style`\] Fix `pytest.mark.parametrize` rules to check calls instead of decorators ([#14515](https://github.com/astral-sh/ruff/pull/14515)) -- \[`flake8-type-checking`\] Implement `runtime-cast-value` (`TC006`) ([#14511](https://github.com/astral-sh/ruff/pull/14511)) -- \[`flake8-type-checking`\] Implement `unquoted-type-alias` (`TC007`) and `quoted-type-alias` (`TC008`) ([#12927](https://github.com/astral-sh/ruff/pull/12927)) -- \[`flake8-use-pathlib`\] Recommend `Path.iterdir()` over `os.listdir()` (`PTH208`) ([#14509](https://github.com/astral-sh/ruff/pull/14509)) -- \[`pylint`\] Extend `invalid-envvar-default` to detect `os.environ.get` (`PLW1508`) ([#14512](https://github.com/astral-sh/ruff/pull/14512)) -- \[`pylint`\] Implement `len-test` (`PLC1802`) ([#14309](https://github.com/astral-sh/ruff/pull/14309)) -- \[`refurb`\] Fix bug where methods defined using lambdas were flagged by `FURB118` ([#14639](https://github.com/astral-sh/ruff/pull/14639)) -- \[`ruff`\] Auto-add `r` prefix when string has no backslashes for `unraw-re-pattern` (`RUF039`) ([#14536](https://github.com/astral-sh/ruff/pull/14536)) -- \[`ruff`\] Implement `invalid-assert-message-literal-argument` (`RUF040`) ([#14488](https://github.com/astral-sh/ruff/pull/14488)) -- \[`ruff`\] Implement `unnecessary-nested-literal` (`RUF041`) ([#14323](https://github.com/astral-sh/ruff/pull/14323)) -- \[`ruff`\] Implement `unnecessary-regular-expression` (`RUF055`) ([#14659](https://github.com/astral-sh/ruff/pull/14659)) - -### Rule changes - -- Ignore more rules for stub files ([#14541](https://github.com/astral-sh/ruff/pull/14541)) -- \[`pep8-naming`\] Eliminate false positives for single-letter names (`N811`, `N814`) ([#14584](https://github.com/astral-sh/ruff/pull/14584)) -- \[`pyflakes`\] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) ([#14615](https://github.com/astral-sh/ruff/pull/14615)) -- \[`ruff`\] Detect redirected-noqa in file-level comments (`RUF101`) ([#14635](https://github.com/astral-sh/ruff/pull/14635)) -- \[`ruff`\] Mark fixes for `unsorted-dunder-all` and `unsorted-dunder-slots` as unsafe when there are complex comments in the sequence (`RUF022`, `RUF023`) ([#14560](https://github.com/astral-sh/ruff/pull/14560)) - -### Bug fixes - -- Avoid fixing code to `None | None` for `redundant-none-literal` (`PYI061`) and `never-union` (`RUF020`) ([#14583](https://github.com/astral-sh/ruff/pull/14583), [#14589](https://github.com/astral-sh/ruff/pull/14589)) -- \[`flake8-bugbear`\] Fix `mutable-contextvar-default` to resolve annotated function calls properly (`B039`) ([#14532](https://github.com/astral-sh/ruff/pull/14532)) -- \[`flake8-pyi`, `ruff`\] Fix traversal of nested literals and unions (`PYI016`, `PYI051`, `PYI055`, `PYI062`, `RUF041`) ([#14641](https://github.com/astral-sh/ruff/pull/14641)) -- \[`flake8-pyi`\] Avoid rewriting invalid type expressions in `unnecessary-type-union` (`PYI055`) ([#14660](https://github.com/astral-sh/ruff/pull/14660)) -- \[`flake8-type-checking`\] Avoid syntax errors and type checking problem for quoted annotations autofix (`TC003`, `TC006`) ([#14634](https://github.com/astral-sh/ruff/pull/14634)) -- \[`pylint`\] Do not wrap function calls in parentheses in the fix for unnecessary-dunder-call (`PLC2801`) ([#14601](https://github.com/astral-sh/ruff/pull/14601)) -- \[`ruff`\] Handle `attrs`'s `auto_attribs` correctly (`RUF009`) ([#14520](https://github.com/astral-sh/ruff/pull/14520)) - -## 0.8.0 - -Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes! - -### Breaking changes - -See also, the "Remapped rules" section which may result in disabled rules. - -- **Default to Python 3.9** - - Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896)) - -- **Changed location of `pydoclint` diagnostics** - - [`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case. - - If you've opted into these preview rules but have them suppressed using - [`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in - some places, this change may mean that you need to move the `noqa` suppression - comments. Most users should be unaffected by this change. - -- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer** - - Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order). - - This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected. - -- **Changes to the line width calculation** - - Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)). - -### Removed Rules - -The following deprecated rules have been removed: - -- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`) -- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`) -- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`) -- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`) -- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`) -- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`) - -### Remapped rules - -The following rules have been remapped to new rule codes: - -- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC` - -### Stabilization - -The following rules have been stabilized and are no longer in preview: - -- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`) -- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`) -- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`) -- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`) -- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`) -- [`pep484-style-positional-only-parameter`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-parameter/) (`PYI063`) -- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`) -- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`) -- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`) -- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`) -- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`) -- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`) -- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`) - -The following behaviors have been stabilized: - -- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names. -- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available - -The following fixes have been stabilized: - -- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`) - -### Preview features - -- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394)) -- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393)) -- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316)) -- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273)) -- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373)) -- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319)) -- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446)) -- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474)) -- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372)) - -### Rule changes - -- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/). -- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402)) -- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371)) -- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424)) - -### Configuration - -- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435)) -- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477)) - -## 0.7.4 - -### Preview features - -- \[`flake8-datetimez`\] Detect usages of `datetime.max`/`datetime.min` (`DTZ901`) ([#14288](https://github.com/astral-sh/ruff/pull/14288)) -- \[`flake8-logging`\] Implement `root-logger-calls` (`LOG015`) ([#14302](https://github.com/astral-sh/ruff/pull/14302)) -- \[`flake8-no-pep420`\] Detect empty implicit namespace packages (`INP001`) ([#14236](https://github.com/astral-sh/ruff/pull/14236)) -- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI019`) ([#14238](https://github.com/astral-sh/ruff/pull/14238)) -- \[`perflint`\] Implement quick-fix for `manual-list-comprehension` (`PERF401`) ([#13919](https://github.com/astral-sh/ruff/pull/13919)) -- \[`pylint`\] Implement `shallow-copy-environ` (`W1507`) ([#14241](https://github.com/astral-sh/ruff/pull/14241)) -- \[`ruff`\] Implement `none-not-at-end-of-union` (`RUF036`) ([#14314](https://github.com/astral-sh/ruff/pull/14314)) -- \[`ruff`\] Implementation `unsafe-markup-call` from `flake8-markupsafe` plugin (`RUF035`) ([#14224](https://github.com/astral-sh/ruff/pull/14224)) -- \[`ruff`\] Report problems for `attrs` dataclasses (`RUF008`, `RUF009`) ([#14327](https://github.com/astral-sh/ruff/pull/14327)) - -### Rule changes - -- \[`flake8-boolean-trap`\] Exclude dunder methods that define operators (`FBT001`) ([#14203](https://github.com/astral-sh/ruff/pull/14203)) -- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI034`) ([#14217](https://github.com/astral-sh/ruff/pull/14217)) -- \[`flake8-pyi`\] Always autofix `duplicate-union-members` (`PYI016`) ([#14270](https://github.com/astral-sh/ruff/pull/14270)) -- \[`flake8-pyi`\] Improve autofix for nested and mixed type unions for `unnecessary-type-union` (`PYI055`) ([#14272](https://github.com/astral-sh/ruff/pull/14272)) -- \[`flake8-pyi`\] Mark fix as unsafe when type annotation contains comments for `duplicate-literal-member` (`PYI062`) ([#14268](https://github.com/astral-sh/ruff/pull/14268)) - -### Server - -- Use the current working directory to resolve settings from `ruff.configuration` ([#14352](https://github.com/astral-sh/ruff/pull/14352)) - -### Bug fixes - -- Avoid conflicts between `PLC014` (`useless-import-alias`) and `I002` (`missing-required-import`) by considering `lint.isort.required-imports` for `PLC014` ([#14287](https://github.com/astral-sh/ruff/pull/14287)) -- \[`flake8-type-checking`\] Skip quoting annotation if it becomes invalid syntax (`TCH001`) -- \[`flake8-pyi`\] Avoid using `typing.Self` in stub files pre-Python 3.11 (`PYI034`) ([#14230](https://github.com/astral-sh/ruff/pull/14230)) -- \[`flake8-pytest-style`\] Flag `pytest.raises` call with keyword argument `expected_exception` (`PT011`) ([#14298](https://github.com/astral-sh/ruff/pull/14298)) -- \[`flake8-simplify`\] Infer "unknown" truthiness for literal iterables whose items are all unpacks (`SIM222`) ([#14263](https://github.com/astral-sh/ruff/pull/14263)) -- \[`flake8-type-checking`\] Fix false positives for `typing.Annotated` (`TCH001`) ([#14311](https://github.com/astral-sh/ruff/pull/14311)) -- \[`pylint`\] Allow `await` at the top-level scope of a notebook (`PLE1142`) ([#14225](https://github.com/astral-sh/ruff/pull/14225)) -- \[`pylint`\] Fix miscellaneous issues in `await-outside-async` detection (`PLE1142`) ([#14218](https://github.com/astral-sh/ruff/pull/14218)) -- \[`pyupgrade`\] Avoid applying PEP 646 rewrites in invalid contexts (`UP044`) ([#14234](https://github.com/astral-sh/ruff/pull/14234)) -- \[`pyupgrade`\] Detect permutations in redundant open modes (`UP015`) ([#14255](https://github.com/astral-sh/ruff/pull/14255)) -- \[`refurb`\] Avoid triggering `hardcoded-string-charset` for reordered sets (`FURB156`) ([#14233](https://github.com/astral-sh/ruff/pull/14233)) -- \[`refurb`\] Further special cases added to `verbose-decimal-constructor` (`FURB157`) ([#14216](https://github.com/astral-sh/ruff/pull/14216)) -- \[`refurb`\] Use `UserString` instead of non-existent `UserStr` (`FURB189`) ([#14209](https://github.com/astral-sh/ruff/pull/14209)) -- \[`ruff`\] Avoid treating lowercase letters as `# noqa` codes (`RUF100`) ([#14229](https://github.com/astral-sh/ruff/pull/14229)) -- \[`ruff`\] Do not report when `Optional` has no type arguments (`RUF013`) ([#14181](https://github.com/astral-sh/ruff/pull/14181)) - -### Documentation - -- Add "Notebook behavior" section for `F704`, `PLE1142` ([#14266](https://github.com/astral-sh/ruff/pull/14266)) -- Document comment policy around fix safety ([#14300](https://github.com/astral-sh/ruff/pull/14300)) - -## 0.7.3 - -### Preview features - -- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928)) -- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059)) -- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008)) -- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105)) -- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068)) - -### Rule changes - -- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064)) -- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094)) -- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150)) -- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188)) -- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065)) -- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061)) - -### Bug fixes - -- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809)) -- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069)) -- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058)) -- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144)) -- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097)) -- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066)) -- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063)) -- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146)) -- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053)) -- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098)) - -### Documentation - -- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971)) -- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040)) - -## 0.7.2 - -### Preview features - -- Fix formatting of single with-item with trailing comment ([#14005](https://github.com/astral-sh/ruff/pull/14005)) -- \[`pyupgrade`\] Add PEP 646 `Unpack` conversion to `*` with fix (`UP044`) ([#13988](https://github.com/astral-sh/ruff/pull/13988)) - -### Rule changes - -- Regenerate `known_stdlibs.rs` with stdlibs 2024.10.25 ([#13963](https://github.com/astral-sh/ruff/pull/13963)) -- \[`flake8-no-pep420`\] Skip namespace package enforcement for PEP 723 scripts (`INP001`) ([#13974](https://github.com/astral-sh/ruff/pull/13974)) - -### Server - -- Fix server panic when undoing an edit ([#14010](https://github.com/astral-sh/ruff/pull/14010)) - -### Bug fixes - -- Fix issues in discovering ruff in pip build environments ([#13881](https://github.com/astral-sh/ruff/pull/13881)) -- \[`flake8-type-checking`\] Fix false positive for `singledispatchmethod` (`TCH003`) ([#13941](https://github.com/astral-sh/ruff/pull/13941)) -- \[`flake8-type-checking`\] Treat return type of `singledispatch` as runtime-required (`TCH003`) ([#13957](https://github.com/astral-sh/ruff/pull/13957)) - -### Documentation - -- \[`flake8-simplify`\] Include caveats of enabling `if-else-block-instead-of-if-exp` (`SIM108`) ([#14019](https://github.com/astral-sh/ruff/pull/14019)) - -## 0.7.1 - -### Preview features - -- Fix `E221` and `E222` to flag missing or extra whitespace around `==` operator ([#13890](https://github.com/astral-sh/ruff/pull/13890)) -- Formatter: Alternate quotes for strings inside f-strings in preview ([#13860](https://github.com/astral-sh/ruff/pull/13860)) -- Formatter: Join implicit concatenated strings when they fit on a line ([#13663](https://github.com/astral-sh/ruff/pull/13663)) -- \[`pylint`\] Restrict `iteration-over-set` to only work on sets of literals (`PLC0208`) ([#13731](https://github.com/astral-sh/ruff/pull/13731)) - -### Rule changes - -- \[`flake8-type-checking`\] Support auto-quoting when annotations contain quotes ([#11811](https://github.com/astral-sh/ruff/pull/11811)) - -### Server - -- Avoid indexing the workspace for single-file mode ([#13770](https://github.com/astral-sh/ruff/pull/13770)) - -### Bug fixes - -- Make `ARG002` compatible with `EM101` when raising `NotImplementedError` ([#13714](https://github.com/astral-sh/ruff/pull/13714)) - -### Other changes - -- Introduce more Docker tags for Ruff (similar to uv) ([#13274](https://github.com/astral-sh/ruff/pull/13274)) - -## 0.7.0 - -Check out the [blog post](https://astral.sh/blog/ruff-v0.7.0) for a migration guide and overview of the changes! - -### Breaking changes - -- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments - ([#12838](https://github.com/astral-sh/ruff/pull/12838), [#13292](https://github.com/astral-sh/ruff/pull/13292)). - This was a change that we attempted to make in Ruff v0.6.0, but only partially made due to an error on our part. - See the [blog post](https://astral.sh/blog/ruff-v0.7.0) for more details. -- The `useless-try-except` rule (in our `tryceratops` category) has been recoded from `TRY302` to - `TRY203` ([#13502](https://github.com/astral-sh/ruff/pull/13502)). This ensures Ruff's code is consistent with - the same rule in the [`tryceratops`](https://github.com/guilatrova/tryceratops) linter. -- The `lint.allow-unused-imports` setting has been removed ([#13677](https://github.com/astral-sh/ruff/pull/13677)). Use - [`lint.pyflakes.allow-unused-imports`](https://docs.astral.sh/ruff/settings/#lint_pyflakes_allowed-unused-imports) - instead. - -### Formatter preview style - -- Normalize implicit concatenated f-string quotes per part ([#13539](https://github.com/astral-sh/ruff/pull/13539)) - -### Preview linter features - -- \[`refurb`\] implement `hardcoded-string-charset` (FURB156) ([#13530](https://github.com/astral-sh/ruff/pull/13530)) -- \[`refurb`\] Count codepoints not bytes for `slice-to-remove-prefix-or-suffix (FURB188)` ([#13631](https://github.com/astral-sh/ruff/pull/13631)) - -### Rule changes - -- \[`pylint`\] Mark `PLE1141` fix as unsafe ([#13629](https://github.com/astral-sh/ruff/pull/13629)) -- \[`flake8-async`\] Consider async generators to be "checkpoints" for `cancel-scope-no-checkpoint` (`ASYNC100`) ([#13639](https://github.com/astral-sh/ruff/pull/13639)) -- \[`flake8-bugbear`\] Do not suggest setting parameter `strict=` to `False` in `B905` diagnostic message ([#13656](https://github.com/astral-sh/ruff/pull/13656)) -- \[`flake8-todos`\] Only flag the word "TODO", not words starting with "todo" (`TD006`) ([#13640](https://github.com/astral-sh/ruff/pull/13640)) -- \[`pycodestyle`\] Fix whitespace-related false positives and false negatives inside type-parameter lists (`E231`, `E251`) ([#13704](https://github.com/astral-sh/ruff/pull/13704)) -- \[`flake8-simplify`\] Stabilize preview behavior for `SIM115` so that the rule can detect files - being opened from a wider range of standard-library functions ([#12959](https://github.com/astral-sh/ruff/pull/12959)). - -### CLI - -- Add explanation of fixable in `--statistics` command ([#13774](https://github.com/astral-sh/ruff/pull/13774)) - -### Bug fixes - -- \[`pyflakes`\] Allow `ipytest` cell magic (`F401`) ([#13745](https://github.com/astral-sh/ruff/pull/13745)) -- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor ([#13616](https://github.com/astral-sh/ruff/pull/13616)) -- \[`flake8-bandit`\] Detect patterns from multi line SQL statements (`S608`) ([#13574](https://github.com/astral-sh/ruff/pull/13574)) -- \[`flake8-pyi`\] - Fix dropped expressions in `PYI030` autofix ([#13727](https://github.com/astral-sh/ruff/pull/13727)) - -## 0.6.9 - -### Preview features - -- Fix codeblock dynamic line length calculation for indented docstring examples ([#13523](https://github.com/astral-sh/ruff/pull/13523)) -- \[`refurb`\] Mark `FURB118` fix as unsafe ([#13613](https://github.com/astral-sh/ruff/pull/13613)) - -### Rule changes - -- \[`pydocstyle`\] Don't raise `D208` when last line is non-empty ([#13372](https://github.com/astral-sh/ruff/pull/13372)) -- \[`pylint`\] Preserve trivia (i.e. comments) in `PLR5501` autofix ([#13573](https://github.com/astral-sh/ruff/pull/13573)) - -### Configuration - -- \[`pyflakes`\] Add `allow-unused-imports` setting for `unused-import` rule (`F401`) ([#13601](https://github.com/astral-sh/ruff/pull/13601)) - -### Bug fixes - -- Support ruff discovery in pip build environments ([#13591](https://github.com/astral-sh/ruff/pull/13591)) -- \[`flake8-bugbear`\] Avoid short circuiting `B017` for multiple context managers ([#13609](https://github.com/astral-sh/ruff/pull/13609)) -- \[`pylint`\] Do not offer an invalid fix for `PLR1716` when the comparisons contain parenthesis ([#13527](https://github.com/astral-sh/ruff/pull/13527)) -- \[`pyupgrade`\] Fix `UP043` to apply to `collections.abc.Generator` and `collections.abc.AsyncGenerator` ([#13611](https://github.com/astral-sh/ruff/pull/13611)) -- \[`refurb`\] Fix handling of slices in tuples for `FURB118`, e.g., `x[:, 1]` ([#13518](https://github.com/astral-sh/ruff/pull/13518)) - -### Documentation - -- Update GitHub Action link to `astral-sh/ruff-action` ([#13551](https://github.com/astral-sh/ruff/pull/13551)) - -## 0.6.8 - -### Preview features - -- Remove unnecessary parentheses around `match case` clauses ([#13510](https://github.com/astral-sh/ruff/pull/13510)) -- Parenthesize overlong `if` guards in `match..case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513)) -- Detect basic wildcard imports in `ruff analyze graph` ([#13486](https://github.com/astral-sh/ruff/pull/13486)) -- \[`pylint`\] Implement `boolean-chained-comparison` (`R1716`) ([#13435](https://github.com/astral-sh/ruff/pull/13435)) - -### Rule changes - -- \[`lake8-simplify`\] Detect `SIM910` when using variadic keyword arguments, i.e., `**kwargs` ([#13503](https://github.com/astral-sh/ruff/pull/13503)) -- \[`pyupgrade`\] Avoid false negatives with non-reference shadowed bindings of loop variables (`UP028`) ([#13504](https://github.com/astral-sh/ruff/pull/13504)) - -### Bug fixes - -- Detect tuples bound to variadic positional arguments i.e. `*args` ([#13512](https://github.com/astral-sh/ruff/pull/13512)) -- Exit gracefully on broken pipe errors ([#13485](https://github.com/astral-sh/ruff/pull/13485)) -- Avoid panic when analyze graph hits broken pipe ([#13484](https://github.com/astral-sh/ruff/pull/13484)) - -### Performance - -- Reuse `BTreeSets` in module resolver ([#13440](https://github.com/astral-sh/ruff/pull/13440)) -- Skip traversal for non-compound statements ([#13441](https://github.com/astral-sh/ruff/pull/13441)) - -## 0.6.7 - -### Preview features - -- Add Python version support to ruff analyze CLI ([#13426](https://github.com/astral-sh/ruff/pull/13426)) -- Add `exclude` support to `ruff analyze` ([#13425](https://github.com/astral-sh/ruff/pull/13425)) -- Fix parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381)) - -### Rule changes - -- \[`pycodestyle`\] Fix: Don't autofix if the first line ends in a question mark? (D400) ([#13399](https://github.com/astral-sh/ruff/pull/13399)) - -### Bug fixes - -- Respect `lint.exclude` in ruff check `--add-noqa` ([#13427](https://github.com/astral-sh/ruff/pull/13427)) - -### Performance - -- Avoid tracking module resolver files in Salsa ([#13437](https://github.com/astral-sh/ruff/pull/13437)) -- Use `forget` for module resolver database ([#13438](https://github.com/astral-sh/ruff/pull/13438)) - -## 0.6.6 - -### Preview features - -- \[`refurb`\] Skip `slice-to-remove-prefix-or-suffix` (`FURB188`) when non-trivial slice steps are present ([#13405](https://github.com/astral-sh/ruff/pull/13405)) -- Add a subcommand to generate dependency graphs ([#13402](https://github.com/astral-sh/ruff/pull/13402)) - -### Formatter - -- Fix placement of inline parameter comments ([#13379](https://github.com/astral-sh/ruff/pull/13379)) - -### Server - -- Fix off-by one error in the `LineIndex::offset` calculation ([#13407](https://github.com/astral-sh/ruff/pull/13407)) - -### Bug fixes - -- \[`fastapi`\] Respect FastAPI aliases in route definitions ([#13394](https://github.com/astral-sh/ruff/pull/13394)) -- \[`pydocstyle`\] Respect word boundaries when detecting function signature in docs ([#13388](https://github.com/astral-sh/ruff/pull/13388)) - -### Documentation - -- Add backlinks to rule overview linter ([#13368](https://github.com/astral-sh/ruff/pull/13368)) -- Fix documentation for editor vim plugin ALE ([#13348](https://github.com/astral-sh/ruff/pull/13348)) -- Fix rendering of `FURB188` docs ([#13406](https://github.com/astral-sh/ruff/pull/13406)) - -## 0.6.5 - -### Preview features - -- \[`pydoclint`\] Ignore `DOC201` when function name is "**new**" ([#13300](https://github.com/astral-sh/ruff/pull/13300)) -- \[`refurb`\] Implement `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#13256](https://github.com/astral-sh/ruff/pull/13256)) - -### Rule changes - -- \[`eradicate`\] Ignore script-comments with multiple end-tags (`ERA001`) ([#13283](https://github.com/astral-sh/ruff/pull/13283)) -- \[`pyflakes`\] Improve error message for `UndefinedName` when a builtin was added in a newer version than specified in Ruff config (`F821`) ([#13293](https://github.com/astral-sh/ruff/pull/13293)) - -### Server - -- Add support for extensionless Python files for server ([#13326](https://github.com/astral-sh/ruff/pull/13326)) -- Fix configuration inheritance for configurations specified in the LSP settings ([#13285](https://github.com/astral-sh/ruff/pull/13285)) - -### Bug fixes - -- \[`ruff`\] Handle unary operators in `decimal-from-float-literal` (`RUF032`) ([#13275](https://github.com/astral-sh/ruff/pull/13275)) - -### CLI - -- Only include rules with diagnostics in SARIF metadata ([#13268](https://github.com/astral-sh/ruff/pull/13268)) - -### Playground - -- Add "Copy as pyproject.toml/ruff.toml" and "Paste from TOML" ([#13328](https://github.com/astral-sh/ruff/pull/13328)) -- Fix errors not shown for restored snippet on page load ([#13262](https://github.com/astral-sh/ruff/pull/13262)) - -## 0.6.4 - -### Preview features - -- \[`flake8-builtins`\] Use dynamic builtins list based on Python version ([#13172](https://github.com/astral-sh/ruff/pull/13172)) -- \[`pydoclint`\] Permit yielding `None` in `DOC402` and `DOC403` ([#13148](https://github.com/astral-sh/ruff/pull/13148)) -- \[`pylint`\] Update diagnostic message for `PLW3201` ([#13194](https://github.com/astral-sh/ruff/pull/13194)) -- \[`ruff`\] Implement `post-init-default` (`RUF033`) ([#13192](https://github.com/astral-sh/ruff/pull/13192)) -- \[`ruff`\] Implement useless if-else (`RUF034`) ([#13218](https://github.com/astral-sh/ruff/pull/13218)) - -### Rule changes - -- \[`flake8-pyi`\] Respect `pep8_naming.classmethod-decorators` settings when determining if a method is a classmethod in `custom-type-var-return-type` (`PYI019`) ([#13162](https://github.com/astral-sh/ruff/pull/13162)) -- \[`flake8-pyi`\] Teach various rules that annotations might be stringized ([#12951](https://github.com/astral-sh/ruff/pull/12951)) -- \[`pylint`\] Avoid `no-self-use` for `attrs`-style validators ([#13166](https://github.com/astral-sh/ruff/pull/13166)) -- \[`pylint`\] Recurse into subscript subexpressions when searching for list/dict lookups (`PLR1733`, `PLR1736`) ([#13186](https://github.com/astral-sh/ruff/pull/13186)) -- \[`pyupgrade`\] Detect `aiofiles.open` calls in `UP015` ([#13173](https://github.com/astral-sh/ruff/pull/13173)) -- \[`pyupgrade`\] Mark `sys.version_info[0] < 3` and similar comparisons as outdated (`UP036`) ([#13175](https://github.com/astral-sh/ruff/pull/13175)) - -### CLI - -- Enrich messages of SARIF results ([#13180](https://github.com/astral-sh/ruff/pull/13180)) -- Handle singular case for incompatible rules warning in `ruff format` output ([#13212](https://github.com/astral-sh/ruff/pull/13212)) - -### Bug fixes - -- \[`pydocstyle`\] Improve heuristics for detecting Google-style docstrings ([#13142](https://github.com/astral-sh/ruff/pull/13142)) -- \[`refurb`\] Treat `sep` arguments with effects as unsafe removals (`FURB105`) ([#13165](https://github.com/astral-sh/ruff/pull/13165)) - -## 0.6.3 - -### Preview features - -- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with `dbm.sqlite3` (`SIM115`) ([#13104](https://github.com/astral-sh/ruff/pull/13104)) -- \[`pycodestyle`\] Disable `E741` in stub files (`.pyi`) ([#13119](https://github.com/astral-sh/ruff/pull/13119)) -- \[`pydoclint`\] Avoid `DOC201` on explicit returns in functions that only return `None` ([#13064](https://github.com/astral-sh/ruff/pull/13064)) - -### Rule changes - -- \[`flake8-async`\] Disable check for `asyncio` before Python 3.11 (`ASYNC109`) ([#13023](https://github.com/astral-sh/ruff/pull/13023)) - -### Bug fixes - -- \[`FastAPI`\] Avoid introducing invalid syntax in fix for `fast-api-non-annotated-dependency` (`FAST002`) ([#13133](https://github.com/astral-sh/ruff/pull/13133)) -- \[`flake8-implicit-str-concat`\] Normalize octals before merging concatenated strings in `single-line-implicit-string-concatenation` (`ISC001`) ([#13118](https://github.com/astral-sh/ruff/pull/13118)) -- \[`flake8-pytest-style`\] Improve help message for `pytest-incorrect-mark-parentheses-style` (`PT023`) ([#13092](https://github.com/astral-sh/ruff/pull/13092)) -- \[`pylint`\] Avoid autofix for calls that aren't `min` or `max` as starred expression (`PLW3301`) ([#13089](https://github.com/astral-sh/ruff/pull/13089)) -- \[`ruff`\] Add `datetime.time`, `datetime.tzinfo`, and `datetime.timezone` as immutable function calls (`RUF009`) ([#13109](https://github.com/astral-sh/ruff/pull/13109)) -- \[`ruff`\] Extend comment deletion for `RUF100` to include trailing text from `noqa` directives while preserving any following comments on the same line, if any ([#13105](https://github.com/astral-sh/ruff/pull/13105)) -- Fix dark theme on initial page load for the Ruff playground ([#13077](https://github.com/astral-sh/ruff/pull/13077)) - -## 0.6.2 - -### Preview features - -- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with other standard-library IO modules (`SIM115`) ([#12959](https://github.com/astral-sh/ruff/pull/12959)) -- \[`ruff`\] Avoid `unused-async` for functions with FastAPI route decorator (`RUF029`) ([#12938](https://github.com/astral-sh/ruff/pull/12938)) -- \[`ruff`\] Ignore `fstring-missing-syntax` (`RUF027`) for `fastAPI` paths ([#12939](https://github.com/astral-sh/ruff/pull/12939)) -- \[`ruff`\] Implement check for Decimal called with a float literal (RUF032) ([#12909](https://github.com/astral-sh/ruff/pull/12909)) - -### Rule changes - -- \[`flake8-bugbear`\] Update diagnostic message when expression is at the end of function (`B015`) ([#12944](https://github.com/astral-sh/ruff/pull/12944)) -- \[`flake8-pyi`\] Skip type annotations in `string-or-bytes-too-long` (`PYI053`) ([#13002](https://github.com/astral-sh/ruff/pull/13002)) -- \[`flake8-type-checking`\] Always recognise relative imports as first-party ([#12994](https://github.com/astral-sh/ruff/pull/12994)) -- \[`flake8-unused-arguments`\] Ignore unused arguments on stub functions (`ARG001`) ([#12966](https://github.com/astral-sh/ruff/pull/12966)) -- \[`pylint`\] Ignore augmented assignment for `self-cls-assignment` (`PLW0642`) ([#12957](https://github.com/astral-sh/ruff/pull/12957)) - -### Server - -- Show full context in error log messages ([#13029](https://github.com/astral-sh/ruff/pull/13029)) - -### Bug fixes - -- \[`pep8-naming`\] Don't flag `from` imports following conventional import names (`N817`) ([#12946](https://github.com/astral-sh/ruff/pull/12946)) -- \[`pylint`\] - Allow `__new__` methods to have `cls` as their first argument even if decorated with `@staticmethod` for `bad-staticmethod-argument` (`PLW0211`) ([#12958](https://github.com/astral-sh/ruff/pull/12958)) - -### Documentation - -- Add `hyperfine` installation instructions; update `hyperfine` code samples ([#13034](https://github.com/astral-sh/ruff/pull/13034)) -- Expand note to use Ruff with other language server in Kate ([#12806](https://github.com/astral-sh/ruff/pull/12806)) -- Update example for `PT001` as per the new default behavior ([#13019](https://github.com/astral-sh/ruff/pull/13019)) -- \[`perflint`\] Improve docs for `try-except-in-loop` (`PERF203`) ([#12947](https://github.com/astral-sh/ruff/pull/12947)) -- \[`pydocstyle`\] Add reference to `lint.pydocstyle.ignore-decorators` setting to rule docs ([#12996](https://github.com/astral-sh/ruff/pull/12996)) - -## 0.6.1 - -This is a hotfix release to address an issue with `ruff-pre-commit`. In v0.6, -Ruff changed its behavior to lint and format Jupyter notebooks by default; -however, due to an oversight, these files were still excluded by default if -Ruff was run via pre-commit, leading to inconsistent behavior. -This has [now been fixed](https://github.com/astral-sh/ruff-pre-commit/pull/96). - -### Preview features - -- \[`fastapi`\] Implement `fast-api-unused-path-parameter` (`FAST003`) ([#12638](https://github.com/astral-sh/ruff/pull/12638)) - -### Rule changes - -- \[`pylint`\] Rename `too-many-positional` to `too-many-positional-arguments` (`R0917`) ([#12905](https://github.com/astral-sh/ruff/pull/12905)) - -### Server - -- Fix crash when applying "fix-all" code-action to notebook cells ([#12929](https://github.com/astral-sh/ruff/pull/12929)) - -### Other changes - -- \[`flake8-naming`\]: Respect import conventions (`N817`) ([#12922](https://github.com/astral-sh/ruff/pull/12922)) - -## 0.6.0 - -Check out the [blog post](https://astral.sh/blog/ruff-v0.6.0) for a migration guide and overview of the changes! - -### Breaking changes - -See also, the "Remapped rules" section which may result in disabled rules. - -- Lint and format Jupyter Notebook by default ([#12878](https://github.com/astral-sh/ruff/pull/12878)). -- Detect imports in `src` layouts by default for `isort` rules ([#12848](https://github.com/astral-sh/ruff/pull/12848)) -- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments ([#12838](https://github.com/astral-sh/ruff/pull/12838)). - -### Deprecations - -The following rules are now deprecated: - -- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`) -- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`) -- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`) - -### Remapped rules - -The following rules have been remapped to new rule codes: - -- [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable/): `RUF025` to `C420` - -### Stabilization - -The following rules have been stabilized and are no longer in preview: - -- [`singledispatch-method`](https://docs.astral.sh/ruff/rules/singledispatch-method/) (`PLE1519`) -- [`singledispatchmethod-function`](https://docs.astral.sh/ruff/rules/singledispatchmethod-function/) (`PLE1520`) -- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) -- [`if-stmt-min-max`](https://docs.astral.sh/ruff/rules/if-stmt-min-max/) (`PLR1730`) -- [`invalid-bytes-return-type`](https://docs.astral.sh/ruff/rules/invalid-bytes-return-type/) (`PLE0308`) -- [`invalid-hash-return-type`](https://docs.astral.sh/ruff/rules/invalid-hash-return-type/) (`PLE0309`) -- [`invalid-index-return-type`](https://docs.astral.sh/ruff/rules/invalid-index-return-type/) (`PLE0305`) -- [`invalid-length-return-type`](https://docs.astral.sh/ruff/rules/invalid-length-return-type/) (`PLEE303`) -- [`self-or-cls-assignment`](https://docs.astral.sh/ruff/rules/self-or-cls-assignment/) (`PLW0642`) -- [`byte-string-usage`](https://docs.astral.sh/ruff/rules/byte-string-usage/) (`PYI057`) -- [`duplicate-literal-member`](https://docs.astral.sh/ruff/rules/duplicate-literal-member/) (`PYI062`) -- [`redirected-noqa`](https://docs.astral.sh/ruff/rules/redirected-noqa/) (`RUF101`) - -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 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: - -- [`superfluous-else-return`](https://docs.astral.sh/ruff/rules/superfluous-else-return/) (`RET505`) -- [`superfluous-else-raise`](https://docs.astral.sh/ruff/rules/superfluous-else-raise/) (`RET506`) -- [`superfluous-else-continue`](https://docs.astral.sh/ruff/rules/superfluous-else-continue/) (`RET507`) -- [`superfluous-else-break`](https://docs.astral.sh/ruff/rules/superfluous-else-break/) (`RET508`) - -### Preview features - -- \[`flake8-simplify`\] Further simplify to binary in preview for (`SIM108`) ([#12796](https://github.com/astral-sh/ruff/pull/12796)) -- \[`pyupgrade`\] Show violations without auto-fix (`UP031`) ([#11229](https://github.com/astral-sh/ruff/pull/11229)) - -### Rule changes - -- \[`flake8-import-conventions`\] Add `xml.etree.ElementTree` to default conventions ([#12455](https://github.com/astral-sh/ruff/pull/12455)) -- \[`flake8-pytest-style`\] Add a space after comma in CSV output (`PT006`) ([#12853](https://github.com/astral-sh/ruff/pull/12853)) - -### Server - -- Show a message for incorrect settings ([#12781](https://github.com/astral-sh/ruff/pull/12781)) - -### Bug fixes - -- \[`flake8-async`\] Do not lint yield in context manager (`ASYNC100`) ([#12896](https://github.com/astral-sh/ruff/pull/12896)) -- \[`flake8-comprehensions`\] Do not lint `async for` comprehensions (`C419`) ([#12895](https://github.com/astral-sh/ruff/pull/12895)) -- \[`flake8-return`\] Only add return `None` at end of a function (`RET503`) ([#11074](https://github.com/astral-sh/ruff/pull/11074)) -- \[`flake8-type-checking`\] Avoid treating `dataclasses.KW_ONLY` as typing-only (`TCH003`) ([#12863](https://github.com/astral-sh/ruff/pull/12863)) -- \[`pep8-naming`\] Treat `type(Protocol)` et al as metaclass base (`N805`) ([#12770](https://github.com/astral-sh/ruff/pull/12770)) -- \[`pydoclint`\] Don't enforce returns and yields in abstract methods (`DOC201`, `DOC202`) ([#12771](https://github.com/astral-sh/ruff/pull/12771)) -- \[`ruff`\] Skip tuples with slice expressions in (`RUF031`) ([#12768](https://github.com/astral-sh/ruff/pull/12768)) -- \[`ruff`\] Ignore unparenthesized tuples in subscripts when the subscript is a type annotation or type alias (`RUF031`) ([#12762](https://github.com/astral-sh/ruff/pull/12762)) -- \[`ruff`\] Ignore template strings passed to logging and `builtins._()` calls (`RUF027`) ([#12889](https://github.com/astral-sh/ruff/pull/12889)) -- \[`ruff`\] Do not remove parens for tuples with starred expressions in Python \<=3.10 (`RUF031`) ([#12784](https://github.com/astral-sh/ruff/pull/12784)) -- Evaluate default parameter values for a function in that function's enclosing scope ([#12852](https://github.com/astral-sh/ruff/pull/12852)) - -### Other changes - -- Respect VS Code cell metadata when detecting the language of Jupyter Notebook cells ([#12864](https://github.com/astral-sh/ruff/pull/12864)) -- Respect `kernelspec` notebook metadata when detecting the preferred language for a Jupyter Notebook ([#12875](https://github.com/astral-sh/ruff/pull/12875)) - -## 0.5.7 - -### Preview features - -- \[`flake8-comprehensions`\] Account for list and set comprehensions in `unnecessary-literal-within-tuple-call` (`C409`) ([#12657](https://github.com/astral-sh/ruff/pull/12657)) -- \[`flake8-pyi`\] Add autofix for `future-annotations-in-stub` (`PYI044`) ([#12676](https://github.com/astral-sh/ruff/pull/12676)) -- \[`flake8-return`\] Avoid syntax error when auto-fixing `RET505` with mixed indentation (space and tabs) ([#12740](https://github.com/astral-sh/ruff/pull/12740)) -- \[`pydoclint`\] Add `docstring-missing-yields` (`DOC402`) and `docstring-extraneous-yields` (`DOC403`) ([#12538](https://github.com/astral-sh/ruff/pull/12538)) -- \[`pydoclint`\] Avoid `DOC201` if docstring begins with "Return", "Returns", "Yield", or "Yields" ([#12675](https://github.com/astral-sh/ruff/pull/12675)) -- \[`pydoclint`\] Deduplicate collected exceptions after traversing function bodies (`DOC501`) ([#12642](https://github.com/astral-sh/ruff/pull/12642)) -- \[`pydoclint`\] Ignore `DOC` errors for stub functions ([#12651](https://github.com/astral-sh/ruff/pull/12651)) -- \[`pydoclint`\] Teach rules to understand reraised exceptions as being explicitly raised (`DOC501`, `DOC502`) ([#12639](https://github.com/astral-sh/ruff/pull/12639)) -- \[`ruff`\] Implement `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#12480](https://github.com/astral-sh/ruff/pull/12480)) -- \[`ruff`\] Mark `RUF023` fix as unsafe if `__slots__` is not a set and the binding is used elsewhere ([#12692](https://github.com/astral-sh/ruff/pull/12692)) - -### Rule changes - -- \[`refurb`\] Add autofix for `implicit-cwd` (`FURB177`) ([#12708](https://github.com/astral-sh/ruff/pull/12708)) -- \[`ruff`\] Add autofix for `zip-instead-of-pairwise` (`RUF007`) ([#12663](https://github.com/astral-sh/ruff/pull/12663)) -- \[`tryceratops`\] Add `BaseException` to `raise-vanilla-class` rule (`TRY002`) ([#12620](https://github.com/astral-sh/ruff/pull/12620)) - -### Server - -- Ignore non-file workspace URL; Ruff will display a warning notification in this case ([#12725](https://github.com/astral-sh/ruff/pull/12725)) - -### CLI - -- Fix cache invalidation for nested `pyproject.toml` files ([#12727](https://github.com/astral-sh/ruff/pull/12727)) - -### Bug fixes - -- \[`flake8-async`\] Fix false positives with multiple `async with` items (`ASYNC100`) ([#12643](https://github.com/astral-sh/ruff/pull/12643)) -- \[`flake8-bandit`\] Avoid false-positives for list concatenations in SQL construction (`S608`) ([#12720](https://github.com/astral-sh/ruff/pull/12720)) -- \[`flake8-bugbear`\] Treat `return` as equivalent to `break` (`B909`) ([#12646](https://github.com/astral-sh/ruff/pull/12646)) -- \[`flake8-comprehensions`\] Set comprehensions not a violation for `sum` in `unnecessary-comprehension-in-call` (`C419`) ([#12691](https://github.com/astral-sh/ruff/pull/12691)) -- \[`flake8-simplify`\] Parenthesize conditions based on precedence when merging if arms (`SIM114`) ([#12737](https://github.com/astral-sh/ruff/pull/12737)) -- \[`pydoclint`\] Try both 'Raises' section styles when convention is unspecified (`DOC501`) ([#12649](https://github.com/astral-sh/ruff/pull/12649)) - -## 0.5.6 - -Ruff 0.5.6 automatically enables linting and formatting of notebooks in *preview mode*. -You can opt-out of this behavior by adding `*.ipynb` to the `extend-exclude` setting. - -```toml -[tool.ruff] -extend-exclude = ["*.ipynb"] -``` - -### Preview features - -- Enable notebooks by default in preview mode ([#12621](https://github.com/astral-sh/ruff/pull/12621)) -- \[`flake8-builtins`\] Implement import, lambda, and module shadowing ([#12546](https://github.com/astral-sh/ruff/pull/12546)) -- \[`pydoclint`\] Add `docstring-missing-returns` (`DOC201`) and `docstring-extraneous-returns` (`DOC202`) ([#12485](https://github.com/astral-sh/ruff/pull/12485)) - -### Rule changes - -- \[`flake8-return`\] Exempt cached properties and other property-like decorators from explicit return rule (`RET501`) ([#12563](https://github.com/astral-sh/ruff/pull/12563)) - -### Server - -- Make server panic hook more error resilient ([#12610](https://github.com/astral-sh/ruff/pull/12610)) -- Use `$/logTrace` for server trace logs in Zed and VS Code ([#12564](https://github.com/astral-sh/ruff/pull/12564)) -- Keep track of deleted cells for reorder change request ([#12575](https://github.com/astral-sh/ruff/pull/12575)) - -### Configuration - -- \[`flake8-implicit-str-concat`\] Always allow explicit multi-line concatenations when implicit concatenations are banned ([#12532](https://github.com/astral-sh/ruff/pull/12532)) - -### Bug fixes - -- \[`flake8-async`\] Avoid flagging `asyncio.timeout`s as unused when the context manager includes `asyncio.TaskGroup` ([#12605](https://github.com/astral-sh/ruff/pull/12605)) -- \[`flake8-slots`\] Avoid recommending `__slots__` for classes that inherit from more than `namedtuple` ([#12531](https://github.com/astral-sh/ruff/pull/12531)) -- \[`isort`\] Avoid marking required imports as unused ([#12537](https://github.com/astral-sh/ruff/pull/12537)) -- \[`isort`\] Preserve trailing inline comments on import-from statements ([#12498](https://github.com/astral-sh/ruff/pull/12498)) -- \[`pycodestyle`\] Add newlines before comments (`E305`) ([#12606](https://github.com/astral-sh/ruff/pull/12606)) -- \[`pycodestyle`\] Don't attach comments with mismatched indents ([#12604](https://github.com/astral-sh/ruff/pull/12604)) -- \[`pyflakes`\] Fix preview-mode bugs in `F401` when attempting to autofix unused first-party submodule imports in an `__init__.py` file ([#12569](https://github.com/astral-sh/ruff/pull/12569)) -- \[`pylint`\] Respect start index in `unnecessary-list-index-lookup` ([#12603](https://github.com/astral-sh/ruff/pull/12603)) -- \[`pyupgrade`\] Avoid recommending no-argument super in `slots=True` dataclasses ([#12530](https://github.com/astral-sh/ruff/pull/12530)) -- \[`pyupgrade`\] Use colon rather than dot formatting for integer-only types ([#12534](https://github.com/astral-sh/ruff/pull/12534)) -- Fix NFKC normalization bug when removing unused imports ([#12571](https://github.com/astral-sh/ruff/pull/12571)) - -### Other changes - -- Consider more stdlib decorators to be property-like ([#12583](https://github.com/astral-sh/ruff/pull/12583)) -- Improve handling of metaclasses in various linter rules ([#12579](https://github.com/astral-sh/ruff/pull/12579)) -- Improve consistency between linter rules in determining whether a function is property ([#12581](https://github.com/astral-sh/ruff/pull/12581)) - -## 0.5.5 - -### Preview features - -- \[`fastapi`\] Implement `fastapi-redundant-response-model` (`FAST001`) and `fastapi-non-annotated-dependency`(`FAST002`) ([#11579](https://github.com/astral-sh/ruff/pull/11579)) -- \[`pydoclint`\] Implement `docstring-missing-exception` (`DOC501`) and `docstring-extraneous-exception` (`DOC502`) ([#11471](https://github.com/astral-sh/ruff/pull/11471)) - -### Rule changes - -- \[`numpy`\] Fix NumPy 2.0 rule for `np.alltrue` and `np.sometrue` ([#12473](https://github.com/astral-sh/ruff/pull/12473)) -- \[`numpy`\] Ignore `NPY201` inside `except` blocks for compatibility with older numpy versions ([#12490](https://github.com/astral-sh/ruff/pull/12490)) -- \[`pep8-naming`\] Avoid applying `ignore-names` to `self` and `cls` function names (`N804`, `N805`) ([#12497](https://github.com/astral-sh/ruff/pull/12497)) - -### Formatter - -- Fix incorrect placement of leading function comment with type params ([#12447](https://github.com/astral-sh/ruff/pull/12447)) - -### Server - -- Do not bail code action resolution when a quick fix is requested ([#12462](https://github.com/astral-sh/ruff/pull/12462)) - -### Bug fixes - -- Fix `Ord` implementation of `cmp_fix` ([#12471](https://github.com/astral-sh/ruff/pull/12471)) -- Raise syntax error for unparenthesized generator expression in multi-argument call ([#12445](https://github.com/astral-sh/ruff/pull/12445)) -- \[`pydoclint`\] Fix panic in `DOC501` reported in [#12428](https://github.com/astral-sh/ruff/pull/12428) ([#12435](https://github.com/astral-sh/ruff/pull/12435)) -- \[`flake8-bugbear`\] Allow singleton tuples with starred expressions in `B013` ([#12484](https://github.com/astral-sh/ruff/pull/12484)) - -### Documentation - -- Add Eglot setup guide for Emacs editor ([#12426](https://github.com/astral-sh/ruff/pull/12426)) -- Add note about the breaking change in `nvim-lspconfig` ([#12507](https://github.com/astral-sh/ruff/pull/12507)) -- Add note to include notebook files for native server ([#12449](https://github.com/astral-sh/ruff/pull/12449)) -- Add setup docs for Zed editor ([#12501](https://github.com/astral-sh/ruff/pull/12501)) - -## 0.5.4 - -### Rule changes - -- \[`ruff`\] Rename `RUF007` to `zip-instead-of-pairwise` ([#12399](https://github.com/astral-sh/ruff/pull/12399)) - -### Bug fixes - -- \[`flake8-builtins`\] Avoid shadowing diagnostics for `@override` methods ([#12415](https://github.com/astral-sh/ruff/pull/12415)) -- \[`flake8-comprehensions`\] Insert parentheses for multi-argument generators ([#12422](https://github.com/astral-sh/ruff/pull/12422)) -- \[`pydocstyle`\] Handle escaped docstrings within docstring (`D301`) ([#12192](https://github.com/astral-sh/ruff/pull/12192)) - -### Documentation - -- Fix GitHub link to Neovim setup ([#12410](https://github.com/astral-sh/ruff/pull/12410)) -- Fix `output-format` default in settings reference ([#12409](https://github.com/astral-sh/ruff/pull/12409)) - -## 0.5.3 - -**Ruff 0.5.3 marks the stable release of the Ruff language server and introduces revamped -[documentation](https://docs.astral.sh/ruff/editors), including [setup guides for your editor of -choice](https://docs.astral.sh/ruff/editors/setup) and [the language server -itself](https://docs.astral.sh/ruff/editors/settings)**. - -### Preview features - -- Formatter: Insert empty line between suite and alternative branch after function/class definition ([#12294](https://github.com/astral-sh/ruff/pull/12294)) -- \[`pyupgrade`\] Implement `unnecessary-default-type-args` (`UP043`) ([#12371](https://github.com/astral-sh/ruff/pull/12371)) - -### Rule changes - -- \[`flake8-bugbear`\] Detect enumerate iterations in `loop-iterator-mutation` (`B909`) ([#12366](https://github.com/astral-sh/ruff/pull/12366)) -- \[`flake8-bugbear`\] Remove `discard`, `remove`, and `pop` allowance for `loop-iterator-mutation` (`B909`) ([#12365](https://github.com/astral-sh/ruff/pull/12365)) -- \[`pylint`\] Allow `repeated-equality-comparison` for mixed operations (`PLR1714`) ([#12369](https://github.com/astral-sh/ruff/pull/12369)) -- \[`pylint`\] Ignore `self` and `cls` when counting arguments (`PLR0913`) ([#12367](https://github.com/astral-sh/ruff/pull/12367)) -- \[`pylint`\] Use UTF-8 as default encoding in `unspecified-encoding` fix (`PLW1514`) ([#12370](https://github.com/astral-sh/ruff/pull/12370)) - -### Server - -- Build settings index in parallel for the native server ([#12299](https://github.com/astral-sh/ruff/pull/12299)) -- Use fallback settings when indexing the project ([#12362](https://github.com/astral-sh/ruff/pull/12362)) -- Consider `--preview` flag for `server` subcommand for the linter and formatter ([#12208](https://github.com/astral-sh/ruff/pull/12208)) - -### Bug fixes - -- \[`flake8-comprehensions`\] Allow additional arguments for `sum` and `max` comprehensions (`C419`) ([#12364](https://github.com/astral-sh/ruff/pull/12364)) -- \[`pylint`\] Avoid dropping extra boolean operations in `repeated-equality-comparison` (`PLR1714`) ([#12368](https://github.com/astral-sh/ruff/pull/12368)) -- \[`pylint`\] Consider expression before statement when determining binding kind (`PLR1704`) ([#12346](https://github.com/astral-sh/ruff/pull/12346)) - -### Documentation - -- Add docs for Ruff language server ([#12344](https://github.com/astral-sh/ruff/pull/12344)) -- Migrate to standalone docs repo ([#12341](https://github.com/astral-sh/ruff/pull/12341)) -- Update versioning policy for editor integration ([#12375](https://github.com/astral-sh/ruff/pull/12375)) - -### Other changes - -- Publish Wasm API to npm ([#12317](https://github.com/astral-sh/ruff/pull/12317)) - -## 0.5.2 - -### Preview features - -- Use `space` separator before parenthesized expressions in comprehensions with leading comments ([#12282](https://github.com/astral-sh/ruff/pull/12282)) -- \[`flake8-async`\] Update `ASYNC100` to include `anyio` and `asyncio` ([#12221](https://github.com/astral-sh/ruff/pull/12221)) -- \[`flake8-async`\] Update `ASYNC109` to include `anyio` and `asyncio` ([#12236](https://github.com/astral-sh/ruff/pull/12236)) -- \[`flake8-async`\] Update `ASYNC110` to include `anyio` and `asyncio` ([#12261](https://github.com/astral-sh/ruff/pull/12261)) -- \[`flake8-async`\] Update `ASYNC115` to include `anyio` and `asyncio` ([#12262](https://github.com/astral-sh/ruff/pull/12262)) -- \[`flake8-async`\] Update `ASYNC116` to include `anyio` and `asyncio` ([#12266](https://github.com/astral-sh/ruff/pull/12266)) - -### Rule changes - -- \[`flake8-return`\] Exempt properties from explicit return rule (`RET501`) ([#12243](https://github.com/astral-sh/ruff/pull/12243)) -- \[`numpy`\] Add `np.NAN`-to-`np.nan` diagnostic ([#12292](https://github.com/astral-sh/ruff/pull/12292)) -- \[`refurb`\] Make `list-reverse-copy` an unsafe fix ([#12303](https://github.com/astral-sh/ruff/pull/12303)) - -### Server - -- Consider `include` and `extend-include` settings in native server ([#12252](https://github.com/astral-sh/ruff/pull/12252)) -- Include nested configurations in settings reloading ([#12253](https://github.com/astral-sh/ruff/pull/12253)) - -### CLI - -- Omit code frames for fixes with empty ranges ([#12304](https://github.com/astral-sh/ruff/pull/12304)) -- Warn about formatter incompatibility for `D203` ([#12238](https://github.com/astral-sh/ruff/pull/12238)) - -### Bug fixes - -- Make cache-write failures non-fatal on Windows ([#12302](https://github.com/astral-sh/ruff/pull/12302)) -- Treat `not` operations as boolean tests ([#12301](https://github.com/astral-sh/ruff/pull/12301)) -- \[`flake8-bandit`\] Avoid `S310` violations for HTTP-safe f-strings ([#12305](https://github.com/astral-sh/ruff/pull/12305)) -- \[`flake8-bandit`\] Support explicit string concatenations in S310 HTTP detection ([#12315](https://github.com/astral-sh/ruff/pull/12315)) -- \[`flake8-bandit`\] fix S113 false positive for httpx without `timeout` argument ([#12213](https://github.com/astral-sh/ruff/pull/12213)) -- \[`pycodestyle`\] Remove "non-obvious" allowance for E721 ([#12300](https://github.com/astral-sh/ruff/pull/12300)) -- \[`pyflakes`\] Consider `with` blocks as single-item branches for redefinition analysis ([#12311](https://github.com/astral-sh/ruff/pull/12311)) -- \[`refurb`\] Restrict forwarding for `newline` argument in `open()` calls to Python versions >= 3.10 ([#12244](https://github.com/astral-sh/ruff/pull/12244)) - -### Documentation - -- Update help and documentation to reflect `--output-format full` default ([#12248](https://github.com/astral-sh/ruff/pull/12248)) - -### Performance - -- Use more threads when discovering Python files ([#12258](https://github.com/astral-sh/ruff/pull/12258)) - -## 0.5.1 - -### Preview features - -- \[`flake8-bugbear`\] Implement mutable-contextvar-default (B039) ([#12113](https://github.com/astral-sh/ruff/pull/12113)) -- \[`pycodestyle`\] Whitespace after decorator (`E204`) ([#12140](https://github.com/astral-sh/ruff/pull/12140)) -- \[`pytest`\] Reverse `PT001` and `PT0023` defaults ([#12106](https://github.com/astral-sh/ruff/pull/12106)) - -### Rule changes - -- Enable token-based rules on source with syntax errors ([#11950](https://github.com/astral-sh/ruff/pull/11950)) -- \[`flake8-bandit`\] Detect `httpx` for `S113` ([#12174](https://github.com/astral-sh/ruff/pull/12174)) -- \[`numpy`\] Update `NPY201` to include exception deprecations ([#12065](https://github.com/astral-sh/ruff/pull/12065)) -- \[`pylint`\] Generate autofix for `duplicate-bases` (`PLE0241`) ([#12105](https://github.com/astral-sh/ruff/pull/12105)) - -### Server - -- Avoid syntax error notification for source code actions ([#12148](https://github.com/astral-sh/ruff/pull/12148)) -- Consider the content of the new cells during notebook sync ([#12203](https://github.com/astral-sh/ruff/pull/12203)) -- Fix replacement edit range computation ([#12171](https://github.com/astral-sh/ruff/pull/12171)) - -### Bug fixes - -- Disable auto-fix when source has syntax errors ([#12134](https://github.com/astral-sh/ruff/pull/12134)) -- Fix cache key collisions for paths with separators ([#12159](https://github.com/astral-sh/ruff/pull/12159)) -- Make `requires-python` inference robust to `==` ([#12091](https://github.com/astral-sh/ruff/pull/12091)) -- Use char-wise width instead of `str`-width ([#12135](https://github.com/astral-sh/ruff/pull/12135)) -- \[`pycodestyle`\] Avoid `E275` if keyword followed by comma ([#12136](https://github.com/astral-sh/ruff/pull/12136)) -- \[`pycodestyle`\] Avoid `E275` if keyword is followed by a semicolon ([#12095](https://github.com/astral-sh/ruff/pull/12095)) -- \[`pylint`\] Skip [dummy variables](https://docs.astral.sh/ruff/settings/#lint_dummy-variable-rgx) for `PLR1704` ([#12190](https://github.com/astral-sh/ruff/pull/12190)) - -### Performance - -- Remove allocation in `parse_identifier` ([#12103](https://github.com/astral-sh/ruff/pull/12103)) -- Use `CompactString` for `Identifier` AST node ([#12101](https://github.com/astral-sh/ruff/pull/12101)) - -## 0.5.0 - -Check out the [blog post](https://astral.sh/blog/ruff-v0.5.0) for a migration guide and overview of the changes! - -### Breaking changes - -See also, the "Remapped rules" section which may result in disabled rules. - -- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms) -- Selecting `ALL` now excludes deprecated rules -- The released archives now include an extra level of nesting, which can be removed with `--strip-components=1` when untarring. -- The release artifact's file name no longer includes the version tag. This enables users to install via `/latest` URLs on GitHub. -- The diagnostic ranges for some `flake8-bandit` rules were modified ([#10667](https://github.com/astral-sh/ruff/pull/10667)). - -### Deprecations - -The following rules are now deprecated: - -- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`): Syntax errors are now always shown - -### Remapped rules - -The following rules have been remapped to new rule codes: - -- [`blocking-http-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-in-async-function/): `ASYNC100` to `ASYNC210` -- [`open-sleep-or-subprocess-in-async-function`](https://docs.astral.sh/ruff/rules/open-sleep-or-subprocess-in-async-function/): `ASYNC101` split into `ASYNC220`, `ASYNC221`, `ASYNC230`, and `ASYNC251` -- [`blocking-os-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-os-call-in-async-function/): `ASYNC102` has been merged into `ASYNC220` and `ASYNC221` -- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await/): `TRIO100` to `ASYNC100` -- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call/): `TRIO105` to `ASYNC105` -- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout/): `TRIO109` to `ASYNC109` -- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep/): `TRIO110` to `ASYNC110` -- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call/): `TRIO115` to `ASYNC115` -- [`repeated-isinstance-calls`](https://docs.astral.sh/ruff/rules/repeated-isinstance-calls/): `PLR1701` to `SIM101` - -### Stabilization - -The following rules have been stabilized and are no longer in preview: - -- [`mutable-fromkeys-value`](https://docs.astral.sh/ruff/rules/mutable-fromkeys-value/) (`RUF024`) -- [`default-factory-kwarg`](https://docs.astral.sh/ruff/rules/default-factory-kwarg/) (`RUF026`) -- [`django-extra`](https://docs.astral.sh/ruff/rules/django-extra/) (`S610`) -- [`manual-dict-comprehension`](https://docs.astral.sh/ruff/rules/manual-dict-comprehension/) (`PERF403`) -- [`print-empty-string`](https://docs.astral.sh/ruff/rules/print-empty-string/) (`FURB105`) -- [`readlines-in-for`](https://docs.astral.sh/ruff/rules/readlines-in-for/) (`FURB129`) -- [`if-expr-min-max`](https://docs.astral.sh/ruff/rules/if-expr-min-max/) (`FURB136`) -- [`bit-count`](https://docs.astral.sh/ruff/rules/bit-count/) (`FURB161`) -- [`redundant-log-base`](https://docs.astral.sh/ruff/rules/redundant-log-base/) (`FURB163`) -- [`regex-flag-alias`](https://docs.astral.sh/ruff/rules/regex-flag-alias/) (`FURB167`) -- [`isinstance-type-none`](https://docs.astral.sh/ruff/rules/isinstance-type-none/) (`FURB168`) -- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`) -- [`implicit-cwd`](https://docs.astral.sh/ruff/rules/implicit-cwd/) (`FURB177`) -- [`hashlib-digest-hex`](https://docs.astral.sh/ruff/rules/hashlib-digest-hex/) (`FURB181`) -- [`list-reverse-copy`](https://docs.astral.sh/ruff/rules/list-reverse-copy/) (`FURB187`) -- [`bad-open-mode`](https://docs.astral.sh/ruff/rules/bad-open-mode/) (`PLW1501`) -- [`empty-comment`](https://docs.astral.sh/ruff/rules/empty-comment/) (`PLR2044`) -- [`global-at-module-level`](https://docs.astral.sh/ruff/rules/global-at-module-level/) (`PLW0604`) -- [`misplaced-bare-raise`](https://docs.astral.sh/ruff/rules/misplaced-bare-raise/) (`PLE0744`) -- [`non-ascii-import-name`](https://docs.astral.sh/ruff/rules/non-ascii-import-name/) (`PLC2403`) -- [`non-ascii-name`](https://docs.astral.sh/ruff/rules/non-ascii-name/) (`PLC2401`) -- [`nonlocal-and-global`](https://docs.astral.sh/ruff/rules/nonlocal-and-global/) (`PLE0115`) -- [`potential-index-error`](https://docs.astral.sh/ruff/rules/potential-index-error/) (`PLE0643`) -- [`redeclared-assigned-name`](https://docs.astral.sh/ruff/rules/redeclared-assigned-name/) (`PLW0128`) -- [`redefined-argument-from-local`](https://docs.astral.sh/ruff/rules/redefined-argument-from-local/) (`PLR1704`) -- [`repeated-keyword-argument`](https://docs.astral.sh/ruff/rules/repeated-keyword-argument/) (`PLE1132`) -- [`super-without-brackets`](https://docs.astral.sh/ruff/rules/super-without-brackets/) (`PLW0245`) -- [`unnecessary-list-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-list-index-lookup/) (`PLR1736`) -- [`useless-exception-statement`](https://docs.astral.sh/ruff/rules/useless-exception-statement/) (`PLW0133`) -- [`useless-with-lock`](https://docs.astral.sh/ruff/rules/useless-with-lock/) (`PLW2101`) - -The following behaviors have been stabilized: - -- [`is-literal`](https://docs.astral.sh/ruff/rules/is-literal/) (`F632`) now warns for identity checks against list, set or dictionary literals -- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) now detects `if` expressions with implicit `else` branches -- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) now allows `os.environ` modifications between import statements -- [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison/) (`E721`) now allows idioms such as `type(x) is int` -- [`yoda-condition`](https://docs.astral.sh/ruff/rules/yoda-conditions/) (`SIM300`) now flags a wider range of expressions - -### Removals - -The following deprecated settings have been removed: - -- `output-format=text`; use `output-format=concise` or `output-format=full` -- `tab-size`; use `indent-width` - -The following deprecated CLI options have been removed: - -- `--show-source`; use `--output-format=full` -- `--no-show-source`; use `--output-format=concise` - -The following deprecated CLI commands have been removed: - -- `ruff `; use `ruff check ` -- `ruff --clean`; use `ruff clean` -- `ruff --generate-shell-completion`; use `ruff generate-shell-completion` - -### Preview features - -- \[`ruff`\] Add `assert-with-print-message` rule ([#11981](https://github.com/astral-sh/ruff/pull/11981)) - -### CLI - -- Use rule name rather than message in `--statistics` ([#11697](https://github.com/astral-sh/ruff/pull/11697)) -- Use the output format `full` by default ([#12010](https://github.com/astral-sh/ruff/pull/12010)) -- Don't log syntax errors to the console ([#11902](https://github.com/astral-sh/ruff/pull/11902)) - -### Rule changes - -- \[`ruff`\] Fix false positives if `gettext` is imported using an alias (`RUF027`) ([#12025](https://github.com/astral-sh/ruff/pull/12025)) -- \[`numpy`\] Update `trapz` and `in1d` deprecation (`NPY201`) ([#11948](https://github.com/astral-sh/ruff/pull/11948)) -- \[`flake8-bandit`\] Modify diagnostic ranges for shell-related rules ([#10667](https://github.com/astral-sh/ruff/pull/10667)) - -### Server - -- Closing an untitled, unsaved notebook document no longer throws an error ([#11942](https://github.com/astral-sh/ruff/pull/11942)) -- Support the usage of tildes and environment variables in `logFile` ([#11945](https://github.com/astral-sh/ruff/pull/11945)) -- Add option to configure whether to show syntax errors ([#12059](https://github.com/astral-sh/ruff/pull/12059)) - -### Bug fixes - -- \[`pycodestyle`\] Avoid `E203` for f-string debug expression ([#12024](https://github.com/astral-sh/ruff/pull/12024)) -- \[`pep8-naming`\] Match import-name ignores against both name and alias (`N812`, `N817`) ([#12033](https://github.com/astral-sh/ruff/pull/12033)) -- \[`pyflakes`\] Detect assignments that shadow definitions (`F811`) ([#11961](https://github.com/astral-sh/ruff/pull/11961)) - -### Parser - -- Emit a syntax error for an empty type parameter list ([#12030](https://github.com/astral-sh/ruff/pull/12030)) -- Avoid consuming the newline for unterminated strings ([#12067](https://github.com/astral-sh/ruff/pull/12067)) -- Do not include the newline in the unterminated string range ([#12017](https://github.com/astral-sh/ruff/pull/12017)) -- Use the correct range to highlight line continuation errors ([#12016](https://github.com/astral-sh/ruff/pull/12016)) -- Consider 2-character EOL before line continuations ([#12035](https://github.com/astral-sh/ruff/pull/12035)) -- Consider line continuation character for re-lexing ([#12008](https://github.com/astral-sh/ruff/pull/12008)) - -### Other changes - -- Upgrade the Unicode table used for measuring the line-length ([#11194](https://github.com/astral-sh/ruff/pull/11194)) -- Remove the deprecation error message for the nursery selector ([#10172](https://github.com/astral-sh/ruff/pull/10172)) - -## 0.4.10 - -### Parser - -- Implement re-lexing logic for better error recovery ([#11845](https://github.com/astral-sh/ruff/pull/11845)) - -### Rule changes - -- \[`flake8-copyright`\] Update `CPY001` to check the first 4096 bytes instead of 1024 ([#11927](https://github.com/astral-sh/ruff/pull/11927)) -- \[`pycodestyle`\] Update `E999` to show all syntax errors instead of just the first one ([#11900](https://github.com/astral-sh/ruff/pull/11900)) - -### Server - -- Add tracing setup guide to Helix documentation ([#11883](https://github.com/astral-sh/ruff/pull/11883)) -- Add tracing setup guide to Neovim documentation ([#11884](https://github.com/astral-sh/ruff/pull/11884)) -- Defer notebook cell deletion to avoid an error message ([#11864](https://github.com/astral-sh/ruff/pull/11864)) - -### Security - -- Guard against malicious ecosystem comment artifacts ([#11879](https://github.com/astral-sh/ruff/pull/11879)) - -## 0.4.9 - -### Preview features - -- \[`pylint`\] Implement `consider-dict-items` (`C0206`) ([#11688](https://github.com/astral-sh/ruff/pull/11688)) -- \[`refurb`\] Implement `repeated-global` (`FURB154`) ([#11187](https://github.com/astral-sh/ruff/pull/11187)) - -### Rule changes - -- \[`pycodestyle`\] Adapt fix for `E203` to work identical to `ruff format` ([#10999](https://github.com/astral-sh/ruff/pull/10999)) - -### Formatter - -- Fix formatter instability for lines only consisting of zero-width characters ([#11748](https://github.com/astral-sh/ruff/pull/11748)) - -### Server - -- Add supported commands in server capabilities ([#11850](https://github.com/astral-sh/ruff/pull/11850)) -- Use real file path when available in `ruff server` ([#11800](https://github.com/astral-sh/ruff/pull/11800)) -- Improve error message when a command is run on an unavailable document ([#11823](https://github.com/astral-sh/ruff/pull/11823)) -- Introduce the `ruff.printDebugInformation` command ([#11831](https://github.com/astral-sh/ruff/pull/11831)) -- Tracing system now respects log level and trace level, with options to log to a file ([#11747](https://github.com/astral-sh/ruff/pull/11747)) - -### CLI - -- Handle non-printable characters in diff view ([#11687](https://github.com/astral-sh/ruff/pull/11687)) - -### Bug fixes - -- \[`refurb`\] Avoid suggesting starmap when arguments are used outside call (`FURB140`) ([#11830](https://github.com/astral-sh/ruff/pull/11830)) -- \[`flake8-bugbear`\] Avoid panic in `B909` when checking large loop blocks ([#11772](https://github.com/astral-sh/ruff/pull/11772)) -- \[`refurb`\] Fix misbehavior of `operator.itemgetter` when getter param is a tuple (`FURB118`) ([#11774](https://github.com/astral-sh/ruff/pull/11774)) - -## 0.4.8 - -### Performance - -- Linter performance has been improved by around 10% on some microbenchmarks by refactoring the lexer and parser to maintain synchronicity between them ([#11457](https://github.com/astral-sh/ruff/pull/11457)) - -### Preview features - -- \[`flake8-bugbear`\] Implement `return-in-generator` (`B901`) ([#11644](https://github.com/astral-sh/ruff/pull/11644)) -- \[`flake8-pyi`\] Implement `pep484-style-positional-only-parameter` (`PYI063`) ([#11699](https://github.com/astral-sh/ruff/pull/11699)) -- \[`pygrep_hooks`\] Check blanket ignores via file-level pragmas (`PGH004`) ([#11540](https://github.com/astral-sh/ruff/pull/11540)) - -### Rule changes - -- \[`pyupgrade`\] Update `UP035` for Python 3.13 and the latest version of `typing_extensions` ([#11693](https://github.com/astral-sh/ruff/pull/11693)) -- \[`numpy`\] Update `NPY001` rule for NumPy 2.0 ([#11735](https://github.com/astral-sh/ruff/pull/11735)) - -### Server - -- Formatting a document with syntax problems no longer spams a visible error popup ([#11745](https://github.com/astral-sh/ruff/pull/11745)) - -### CLI - -- Add RDJson support for `--output-format` flag ([#11682](https://github.com/astral-sh/ruff/pull/11682)) - -### Bug fixes - -- \[`pyupgrade`\] Write empty string in lieu of panic when fixing `UP032` ([#11696](https://github.com/astral-sh/ruff/pull/11696)) -- \[`flake8-simplify`\] Simplify double negatives in `SIM103` ([#11684](https://github.com/astral-sh/ruff/pull/11684)) -- Ensure the expression generator adds a newline before `type` statements ([#11720](https://github.com/astral-sh/ruff/pull/11720)) -- Respect per-file ignores for blanket and redirected noqa rules ([#11728](https://github.com/astral-sh/ruff/pull/11728)) - -## 0.4.7 - -### Preview features - -- \[`flake8-pyi`\] Implement `PYI064` ([#11325](https://github.com/astral-sh/ruff/pull/11325)) -- \[`flake8-pyi`\] Implement `PYI066` ([#11541](https://github.com/astral-sh/ruff/pull/11541)) -- \[`flake8-pyi`\] Implement `PYI057` ([#11486](https://github.com/astral-sh/ruff/pull/11486)) -- \[`pyflakes`\] Enable `F822` in `__init__.py` files by default ([#11370](https://github.com/astral-sh/ruff/pull/11370)) - -### Formatter - -- Fix incorrect placement of trailing stub function comments ([#11632](https://github.com/astral-sh/ruff/pull/11632)) - -### Server - -- Respect file exclusions in `ruff server` ([#11590](https://github.com/astral-sh/ruff/pull/11590)) -- Add support for documents not exist on disk ([#11588](https://github.com/astral-sh/ruff/pull/11588)) -- Add Vim and Kate setup guide for `ruff server` ([#11615](https://github.com/astral-sh/ruff/pull/11615)) - -### Bug fixes - -- Avoid removing newlines between docstring headers and rST blocks ([#11609](https://github.com/astral-sh/ruff/pull/11609)) -- Infer indentation with imports when logical indent is absent ([#11608](https://github.com/astral-sh/ruff/pull/11608)) -- Use char index rather than position for indent slice ([#11645](https://github.com/astral-sh/ruff/pull/11645)) -- \[`flake8-comprehension`\] Strip parentheses around generators in `C400` ([#11607](https://github.com/astral-sh/ruff/pull/11607)) -- Mark `repeated-isinstance-calls` as unsafe on Python 3.10 and later ([#11622](https://github.com/astral-sh/ruff/pull/11622)) - -## 0.4.6 - -### Breaking changes - -- Use project-relative paths when calculating GitLab fingerprints ([#11532](https://github.com/astral-sh/ruff/pull/11532)) -- Bump minimum supported Windows version to Windows 10 ([#11613](https://github.com/astral-sh/ruff/pull/11613)) - -### Preview features - -- \[`flake8-async`\] Sleep with >24 hour interval should usually sleep forever (`ASYNC116`) ([#11498](https://github.com/astral-sh/ruff/pull/11498)) - -### Rule changes - -- \[`numpy`\] Add missing functions to NumPy 2.0 migration rule ([#11528](https://github.com/astral-sh/ruff/pull/11528)) -- \[`mccabe`\] Consider irrefutable pattern similar to `if .. else` for `C901` ([#11565](https://github.com/astral-sh/ruff/pull/11565)) -- Consider `match`-`case` statements for `C901`, `PLR0912`, and `PLR0915` ([#11521](https://github.com/astral-sh/ruff/pull/11521)) -- Remove empty strings when converting to f-string (`UP032`) ([#11524](https://github.com/astral-sh/ruff/pull/11524)) -- \[`flake8-bandit`\] `request-without-timeout` should warn for `requests.request` ([#11548](https://github.com/astral-sh/ruff/pull/11548)) -- \[`flake8-self`\] Ignore sunder accesses in `flake8-self` rules ([#11546](https://github.com/astral-sh/ruff/pull/11546)) -- \[`pyupgrade`\] Lint for `TypeAliasType` usages (`UP040`) ([#11530](https://github.com/astral-sh/ruff/pull/11530)) - -### Server - -- Respect excludes in `ruff server` configuration discovery ([#11551](https://github.com/astral-sh/ruff/pull/11551)) -- Use default settings if initialization options is empty or not provided ([#11566](https://github.com/astral-sh/ruff/pull/11566)) -- `ruff server` correctly treats `.pyi` files as stub files ([#11535](https://github.com/astral-sh/ruff/pull/11535)) -- `ruff server` searches for configuration in parent directories ([#11537](https://github.com/astral-sh/ruff/pull/11537)) -- `ruff server`: An empty code action filter no longer returns notebook source actions ([#11526](https://github.com/astral-sh/ruff/pull/11526)) - -### Bug fixes - -- \[`flake8-logging-format`\] Fix autofix title in `logging-warn` (`G010`) ([#11514](https://github.com/astral-sh/ruff/pull/11514)) -- \[`refurb`\] Avoid recommending `operator.itemgetter` with dependence on lambda arguments ([#11574](https://github.com/astral-sh/ruff/pull/11574)) -- \[`flake8-simplify`\] Avoid recommending context manager in `__enter__` implementations ([#11575](https://github.com/astral-sh/ruff/pull/11575)) -- Create intermediary directories for `--output-file` ([#11550](https://github.com/astral-sh/ruff/pull/11550)) -- Propagate reads on global variables ([#11584](https://github.com/astral-sh/ruff/pull/11584)) -- Treat all `singledispatch` arguments as runtime-required ([#11523](https://github.com/astral-sh/ruff/pull/11523)) - -## 0.4.5 - -### Ruff's language server is now in Beta - -`v0.4.5` marks the official Beta release of `ruff server`, an integrated language server built into Ruff. -`ruff server` supports the same feature set as `ruff-lsp`, powering linting, formatting, and -code fixes in Ruff's editor integrations -- but with superior performance and -no installation required. We'd love your feedback! - -You can enable `ruff server` in the [VS Code extension](https://github.com/astral-sh/ruff-vscode?tab=readme-ov-file#enabling-the-rust-based-language-server) today. - -To read more about this exciting milestone, check out our [blog post](https://astral.sh/blog/ruff-v0.4.5)! - -### Rule changes - -- \[`flake8-future-annotations`\] Reword `future-rewritable-type-annotation` (`FA100`) message ([#11381](https://github.com/astral-sh/ruff/pull/11381)) -- \[`isort`\] Expanded the set of standard-library modules to include `_string`, etc. ([#11374](https://github.com/astral-sh/ruff/pull/11374)) -- \[`pycodestyle`\] Consider soft keywords for `E27` rules ([#11446](https://github.com/astral-sh/ruff/pull/11446)) -- \[`pyflakes`\] Recommend adding unused import bindings to `__all__` ([#11314](https://github.com/astral-sh/ruff/pull/11314)) -- \[`pyflakes`\] Update documentation and deprecate `ignore_init_module_imports` ([#11436](https://github.com/astral-sh/ruff/pull/11436)) -- \[`pyupgrade`\] Mark quotes as unnecessary for non-evaluated annotations ([#11485](https://github.com/astral-sh/ruff/pull/11485)) - -### Formatter - -- Avoid multiline quotes warning with `quote-style = preserve` ([#11490](https://github.com/astral-sh/ruff/pull/11490)) - -### Server - -- Support Jupyter Notebook files ([#11206](https://github.com/astral-sh/ruff/pull/11206)) -- Support `noqa` comment code actions ([#11276](https://github.com/astral-sh/ruff/pull/11276)) -- Fix automatic configuration reloading ([#11492](https://github.com/astral-sh/ruff/pull/11492)) -- Fix several issues with configuration in Neovim and Helix ([#11497](https://github.com/astral-sh/ruff/pull/11497)) - -### CLI - -- Add `--output-format` as a CLI option for `ruff config` ([#11438](https://github.com/astral-sh/ruff/pull/11438)) - -### Bug fixes - -- Avoid `PLE0237` for property with setter ([#11377](https://github.com/astral-sh/ruff/pull/11377)) -- Avoid `TCH005` for `if` stmt with `elif`/`else` block ([#11376](https://github.com/astral-sh/ruff/pull/11376)) -- Avoid flagging `__future__` annotations as required for non-evaluated type annotations ([#11414](https://github.com/astral-sh/ruff/pull/11414)) -- Check for ruff executable in 'bin' directory as installed by 'pip install --target'. ([#11450](https://github.com/astral-sh/ruff/pull/11450)) -- Sort edits prior to deduplicating in quotation fix ([#11452](https://github.com/astral-sh/ruff/pull/11452)) -- Treat escaped newline as valid sequence ([#11465](https://github.com/astral-sh/ruff/pull/11465)) -- \[`flake8-pie`\] Preserve parentheses in `unnecessary-dict-kwargs` ([#11372](https://github.com/astral-sh/ruff/pull/11372)) -- \[`pylint`\] Ignore `__slots__` with dynamic values ([#11488](https://github.com/astral-sh/ruff/pull/11488)) -- \[`pylint`\] Remove `try` body from branch counting ([#11487](https://github.com/astral-sh/ruff/pull/11487)) -- \[`refurb`\] Respect operator precedence in `FURB110` ([#11464](https://github.com/astral-sh/ruff/pull/11464)) - -### Documentation - -- Add `--preview` to the README ([#11395](https://github.com/astral-sh/ruff/pull/11395)) -- Add Python 3.13 to list of allowed Python versions ([#11411](https://github.com/astral-sh/ruff/pull/11411)) -- Simplify Neovim setup documentation ([#11489](https://github.com/astral-sh/ruff/pull/11489)) -- Update CONTRIBUTING.md to reflect the new parser ([#11434](https://github.com/astral-sh/ruff/pull/11434)) -- Update server documentation with new migration guide ([#11499](https://github.com/astral-sh/ruff/pull/11499)) -- \[`pycodestyle`\] Clarify motivation for `E713` and `E714` ([#11483](https://github.com/astral-sh/ruff/pull/11483)) -- \[`pyflakes`\] Update docs to describe WAI behavior (F541) ([#11362](https://github.com/astral-sh/ruff/pull/11362)) -- \[`pylint`\] Clearly indicate what is counted as a branch ([#11423](https://github.com/astral-sh/ruff/pull/11423)) - -## 0.4.4 - -### Preview features - -- \[`pycodestyle`\] Ignore end-of-line comments when determining blank line rules ([#11342](https://github.com/astral-sh/ruff/pull/11342)) -- \[`pylint`\] Detect `pathlib.Path.open` calls in `unspecified-encoding` (`PLW1514`) ([#11288](https://github.com/astral-sh/ruff/pull/11288)) -- \[`flake8-pyi`\] Implement `PYI059` (`generic-not-last-base-class`) ([#11233](https://github.com/astral-sh/ruff/pull/11233)) -- \[`flake8-pyi`\] Implement `PYI062` (`duplicate-literal-member`) ([#11269](https://github.com/astral-sh/ruff/pull/11269)) - -### Rule changes - -- \[`flake8-boolean-trap`\] Allow passing booleans as positional-only arguments in code such as `set(True)` ([#11287](https://github.com/astral-sh/ruff/pull/11287)) -- \[`flake8-bugbear`\] Ignore enum classes in `cached-instance-method` (`B019`) ([#11312](https://github.com/astral-sh/ruff/pull/11312)) - -### Server - -- Expand tildes when resolving Ruff server configuration file ([#11283](https://github.com/astral-sh/ruff/pull/11283)) -- Fix `ruff server` hanging after Neovim closes ([#11291](https://github.com/astral-sh/ruff/pull/11291)) -- Editor settings are used by default if no file-based configuration exists ([#11266](https://github.com/astral-sh/ruff/pull/11266)) - -### Bug fixes - -- \[`pylint`\] Consider `with` statements for `too-many-branches` (`PLR0912`) ([#11321](https://github.com/astral-sh/ruff/pull/11321)) -- \[`flake8-blind-except`, `tryceratops`\] Respect logged and re-raised expressions in nested statements (`BLE001`, `TRY201`) ([#11301](https://github.com/astral-sh/ruff/pull/11301)) -- Recognise assignments such as `__all__ = builtins.list(["foo", "bar"])` as valid `__all__` definitions ([#11335](https://github.com/astral-sh/ruff/pull/11335)) - -## 0.4.3 - -### Enhancements - -- Add support for PEP 696 syntax ([#11120](https://github.com/astral-sh/ruff/pull/11120)) - -### Preview features - -- \[`refurb`\] Use function range for `reimplemented-operator` diagnostics ([#11271](https://github.com/astral-sh/ruff/pull/11271)) -- \[`refurb`\] Ignore methods in `reimplemented-operator` (`FURB118`) ([#11270](https://github.com/astral-sh/ruff/pull/11270)) -- \[`refurb`\] Implement `fstring-number-format` (`FURB116`) ([#10921](https://github.com/astral-sh/ruff/pull/10921)) -- \[`ruff`\] Implement `redirected-noqa` (`RUF101`) ([#11052](https://github.com/astral-sh/ruff/pull/11052)) -- \[`pyflakes`\] Distinguish between first-party and third-party imports for fix suggestions ([#11168](https://github.com/astral-sh/ruff/pull/11168)) - -### Rule changes - -- \[`flake8-bugbear`\] Ignore non-abstract class attributes when enforcing `B024` ([#11210](https://github.com/astral-sh/ruff/pull/11210)) -- \[`flake8-logging`\] Include inline instantiations when detecting loggers ([#11154](https://github.com/astral-sh/ruff/pull/11154)) -- \[`pylint`\] Also emit `PLR0206` for properties with variadic parameters ([#11200](https://github.com/astral-sh/ruff/pull/11200)) -- \[`ruff`\] Detect duplicate codes as part of `unused-noqa` (`RUF100`) ([#10850](https://github.com/astral-sh/ruff/pull/10850)) - -### Formatter - -- Avoid multiline expression if format specifier is present ([#11123](https://github.com/astral-sh/ruff/pull/11123)) - -### LSP - -- Write `ruff server` setup guide for Helix ([#11183](https://github.com/astral-sh/ruff/pull/11183)) -- `ruff server` no longer hangs after shutdown ([#11222](https://github.com/astral-sh/ruff/pull/11222)) -- `ruff server` reads from a configuration TOML file in the user configuration directory if no local configuration exists ([#11225](https://github.com/astral-sh/ruff/pull/11225)) -- `ruff server` respects `per-file-ignores` configuration ([#11224](https://github.com/astral-sh/ruff/pull/11224)) -- `ruff server`: Support a custom TOML configuration file ([#11140](https://github.com/astral-sh/ruff/pull/11140)) -- `ruff server`: Support setting to prioritize project configuration over editor configuration ([#11086](https://github.com/astral-sh/ruff/pull/11086)) - -### Bug fixes - -- Avoid debug assertion around NFKC renames ([#11249](https://github.com/astral-sh/ruff/pull/11249)) -- \[`pyflakes`\] Prioritize `redefined-while-unused` over `unused-import` ([#11173](https://github.com/astral-sh/ruff/pull/11173)) -- \[`ruff`\] Respect `async` expressions in comprehension bodies ([#11219](https://github.com/astral-sh/ruff/pull/11219)) -- \[`pygrep_hooks`\] Fix `blanket-noqa` panic when last line has noqa with no newline (`PGH004`) ([#11108](https://github.com/astral-sh/ruff/pull/11108)) -- \[`perflint`\] Ignore list-copy recommendations for async `for` loops ([#11250](https://github.com/astral-sh/ruff/pull/11250)) -- \[`pyflakes`\] Improve `invalid-print-syntax` documentation ([#11171](https://github.com/astral-sh/ruff/pull/11171)) - -### Performance - -- Avoid allocations for isort module names ([#11251](https://github.com/astral-sh/ruff/pull/11251)) -- Build a separate ARM wheel for macOS ([#11149](https://github.com/astral-sh/ruff/pull/11149)) - -### Windows - -- Increase the minimum requirement to Windows 10. - -## 0.4.2 - -### Rule changes - -- \[`flake8-pyi`\] Allow for overloaded `__exit__` and `__aexit__` definitions (`PYI036`) ([#11057](https://github.com/astral-sh/ruff/pull/11057)) -- \[`pyupgrade`\] Catch usages of `"%s" % var` and provide an unsafe fix (`UP031`) ([#11019](https://github.com/astral-sh/ruff/pull/11019)) -- \[`refurb`\] Implement new rule that suggests min/max over `sorted()` (`FURB192`) ([#10868](https://github.com/astral-sh/ruff/pull/10868)) - -### Server - -- Fix an issue with missing diagnostics for Neovim and Helix ([#11092](https://github.com/astral-sh/ruff/pull/11092)) -- Implement hover documentation for `noqa` codes ([#11096](https://github.com/astral-sh/ruff/pull/11096)) -- Introduce common Ruff configuration options with new server settings ([#11062](https://github.com/astral-sh/ruff/pull/11062)) - -### Bug fixes - -- Use `macos-12` for building release wheels to enable macOS 11 compatibility ([#11146](https://github.com/astral-sh/ruff/pull/11146)) -- \[`flake8-blind-expect`\] Allow raise from in `BLE001` ([#11131](https://github.com/astral-sh/ruff/pull/11131)) -- \[`flake8-pyi`\] Allow simple assignments to `None` in enum class scopes (`PYI026`) ([#11128](https://github.com/astral-sh/ruff/pull/11128)) -- \[`flake8-simplify`\] Avoid raising `SIM911` for non-`zip` attribute calls ([#11126](https://github.com/astral-sh/ruff/pull/11126)) -- \[`refurb`\] Avoid `operator.itemgetter` suggestion for single-item tuple ([#11095](https://github.com/astral-sh/ruff/pull/11095)) -- \[`ruff`\] Respect per-file-ignores for `RUF100` with no other diagnostics ([#11058](https://github.com/astral-sh/ruff/pull/11058)) -- \[`ruff`\] Fix async comprehension false positive (`RUF029`) ([#11070](https://github.com/astral-sh/ruff/pull/11070)) - -### Documentation - -- \[`flake8-bugbear`\] Document explicitly disabling strict zip (`B905`) ([#11040](https://github.com/astral-sh/ruff/pull/11040)) -- \[`flake8-type-checking`\] Mention `lint.typing-modules` in `TCH001`, `TCH002`, and `TCH003` ([#11144](https://github.com/astral-sh/ruff/pull/11144)) -- \[`isort`\] Improve documentation around custom `isort` sections ([#11050](https://github.com/astral-sh/ruff/pull/11050)) -- \[`pylint`\] Fix documentation oversight for `invalid-X-returns` ([#11094](https://github.com/astral-sh/ruff/pull/11094)) - -### Performance - -- Use `matchit` to resolve per-file settings ([#11111](https://github.com/astral-sh/ruff/pull/11111)) - -## 0.4.1 - -### Preview features - -- \[`pylint`\] Implement `invalid-hash-returned` (`PLE0309`) ([#10961](https://github.com/astral-sh/ruff/pull/10961)) -- \[`pylint`\] Implement `invalid-index-returned` (`PLE0305`) ([#10962](https://github.com/astral-sh/ruff/pull/10962)) - -### Bug fixes - -- \[`pylint`\] Allow `NoReturn`-like functions for `__str__`, `__len__`, etc. (`PLE0307`) ([#11017](https://github.com/astral-sh/ruff/pull/11017)) -- Parser: Use empty range when there's "gap" in token source ([#11032](https://github.com/astral-sh/ruff/pull/11032)) -- \[`ruff`\] Ignore stub functions in `unused-async` (`RUF029`) ([#11026](https://github.com/astral-sh/ruff/pull/11026)) -- Parser: Expect indented case block instead of match stmt ([#11033](https://github.com/astral-sh/ruff/pull/11033)) - -## 0.4.0 - -### A new, hand-written parser - -Ruff's new parser is **>2x faster**, which translates to a **20-40% speedup** for all linting and formatting invocations. -There's a lot to say about this exciting change, so check out the [blog post](https://astral.sh/blog/ruff-v0.4.0) for more details! - -See [#10036](https://github.com/astral-sh/ruff/pull/10036) for implementation details. - -### A new language server in Rust - -With this release, we also want to highlight our new language server. `ruff server` is a Rust-powered language -server that comes built-in with Ruff. It can be used with any editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP). -It uses a multi-threaded, lock-free architecture inspired by `rust-analyzer` and it will open the door for a lot -of exciting features. It’s also faster than our previous [Python-based language server](https://github.com/astral-sh/ruff-lsp) --- but you probably guessed that already. - -`ruff server` is only in alpha, but it has a lot of features that you can try out today: - -- Lints Python files automatically and shows quick-fixes when available -- Formats Python files, with support for range formatting -- Comes with commands for quickly performing actions: `ruff.applyAutofix`, `ruff.applyFormat`, and `ruff.applyOrganizeImports` -- Supports `source.fixAll` and `source.organizeImports` source actions -- Automatically reloads your project configuration when you change it - -To setup `ruff server` with your editor, refer to the [README.md](https://github.com/astral-sh/ruff/blob/main/crates/ruff_server/README.md). - -### Preview features - -- \[`pycodestyle`\] Do not trigger `E3` rules on `def`s following a function/method with a dummy body ([#10704](https://github.com/astral-sh/ruff/pull/10704)) -- \[`pylint`\] Implement `invalid-bytes-returned` (`E0308`) ([#10959](https://github.com/astral-sh/ruff/pull/10959)) -- \[`pylint`\] Implement `invalid-length-returned` (`E0303`) ([#10963](https://github.com/astral-sh/ruff/pull/10963)) -- \[`pylint`\] Implement `self-cls-assignment` (`W0642`) ([#9267](https://github.com/astral-sh/ruff/pull/9267)) -- \[`pylint`\] Omit stubs from `invalid-bool` and `invalid-str-return-type` ([#11008](https://github.com/astral-sh/ruff/pull/11008)) -- \[`ruff`\] New rule `unused-async` (`RUF029`) to detect unneeded `async` keywords on functions ([#9966](https://github.com/astral-sh/ruff/pull/9966)) - -### Rule changes - -- \[`flake8-bandit`\] Allow `urllib.request.urlopen` calls with static `Request` argument (`S310`) ([#10964](https://github.com/astral-sh/ruff/pull/10964)) -- \[`flake8-bugbear`\] Treat `raise NotImplemented`-only bodies as stub functions (`B006`) ([#10990](https://github.com/astral-sh/ruff/pull/10990)) -- \[`flake8-slots`\] Respect same-file `Enum` subclasses (`SLOT000`) ([#11006](https://github.com/astral-sh/ruff/pull/11006)) -- \[`pylint`\] Support inverted comparisons (`PLR1730`) ([#10920](https://github.com/astral-sh/ruff/pull/10920)) - -### Linter - -- Improve handling of builtin symbols in linter rules ([#10919](https://github.com/astral-sh/ruff/pull/10919)) -- Improve display of rules in `--show-settings` ([#11003](https://github.com/astral-sh/ruff/pull/11003)) -- Improve inference capabilities of the `BuiltinTypeChecker` ([#10976](https://github.com/astral-sh/ruff/pull/10976)) -- Resolve classes and functions relative to script name ([#10965](https://github.com/astral-sh/ruff/pull/10965)) -- Improve performance of `RuleTable::any_enabled` ([#10971](https://github.com/astral-sh/ruff/pull/10971)) - -### Server - -*This section is devoted to updates for our new language server, written in Rust.* - -- Enable ruff-specific source actions ([#10916](https://github.com/astral-sh/ruff/pull/10916)) -- Refreshes diagnostics for open files when file configuration is changed ([#10988](https://github.com/astral-sh/ruff/pull/10988)) -- Important errors are now shown as popups ([#10951](https://github.com/astral-sh/ruff/pull/10951)) -- Introduce settings for directly configuring the linter and formatter ([#10984](https://github.com/astral-sh/ruff/pull/10984)) -- Resolve configuration for each document individually ([#10950](https://github.com/astral-sh/ruff/pull/10950)) -- Write a setup guide for Neovim ([#10987](https://github.com/astral-sh/ruff/pull/10987)) - -### Configuration - -- Add `RUFF_OUTPUT_FILE` environment variable support ([#10992](https://github.com/astral-sh/ruff/pull/10992)) - -### Bug fixes - -- Avoid `non-augmented-assignment` for reversed, non-commutative operators (`PLR6104`) ([#10909](https://github.com/astral-sh/ruff/pull/10909)) -- Limit commutative non-augmented-assignments to primitive data types (`PLR6104`) ([#10912](https://github.com/astral-sh/ruff/pull/10912)) -- Respect `per-file-ignores` for `RUF100` on blanket `# noqa` ([#10908](https://github.com/astral-sh/ruff/pull/10908)) -- Consider `if` expression for parenthesized with items parsing ([#11010](https://github.com/astral-sh/ruff/pull/11010)) -- Consider binary expr for parenthesized with items parsing ([#11012](https://github.com/astral-sh/ruff/pull/11012)) -- Reset `FOR_TARGET` context for all kinds of parentheses ([#11009](https://github.com/astral-sh/ruff/pull/11009)) - -## 0.3.7 - -### Preview features - -- \[`flake8-bugbear`\] Implement `loop-iterator-mutation` (`B909`) ([#9578](https://github.com/astral-sh/ruff/pull/9578)) -- \[`pylint`\] Implement rule to prefer augmented assignment (`PLR6104`) ([#9932](https://github.com/astral-sh/ruff/pull/9932)) - -### Bug fixes - -- Avoid TOCTOU errors in cache initialization ([#10884](https://github.com/astral-sh/ruff/pull/10884)) -- \[`pylint`\] Recode `nan-comparison` rule to `W0177` ([#10894](https://github.com/astral-sh/ruff/pull/10894)) -- \[`pylint`\] Reverse min-max logic in `if-stmt-min-max` ([#10890](https://github.com/astral-sh/ruff/pull/10890)) - -## 0.3.6 - -### Preview features - -- \[`pylint`\] Implement `bad-staticmethod-argument` (`PLW0211`) ([#10781](https://github.com/astral-sh/ruff/pull/10781)) -- \[`pylint`\] Implement `if-stmt-min-max` (`PLR1730`, `PLR1731`) ([#10002](https://github.com/astral-sh/ruff/pull/10002)) -- \[`pyupgrade`\] Replace `str,Enum` multiple inheritance with `StrEnum` `UP042` ([#10713](https://github.com/astral-sh/ruff/pull/10713)) -- \[`refurb`\] Implement `if-expr-instead-of-or-operator` (`FURB110`) ([#10687](https://github.com/astral-sh/ruff/pull/10687)) -- \[`refurb`\] Implement `int-on-sliced-str` (`FURB166`) ([#10650](https://github.com/astral-sh/ruff/pull/10650)) -- \[`refurb`\] Implement `write-whole-file` (`FURB103`) ([#10802](https://github.com/astral-sh/ruff/pull/10802)) -- \[`refurb`\] Support `itemgetter` in `reimplemented-operator` (`FURB118`) ([#10526](https://github.com/astral-sh/ruff/pull/10526)) -- \[`flake8_comprehensions`\] Add `sum`/`min`/`max` to unnecessary comprehension check (`C419`) ([#10759](https://github.com/astral-sh/ruff/pull/10759)) - -### Rule changes - -- \[`pydocstyle`\] Require capitalizing docstrings where the first sentence is a single word (`D403`) ([#10776](https://github.com/astral-sh/ruff/pull/10776)) -- \[`pycodestyle`\] Ignore annotated lambdas in class scopes (`E731`) ([#10720](https://github.com/astral-sh/ruff/pull/10720)) -- \[`flake8-pyi`\] Various improvements to PYI034 ([#10807](https://github.com/astral-sh/ruff/pull/10807)) -- \[`flake8-slots`\] Flag subclasses of call-based `typing.NamedTuple`s as well as subclasses of `collections.namedtuple()` (`SLOT002`) ([#10808](https://github.com/astral-sh/ruff/pull/10808)) -- \[`pyflakes`\] Allow forward references in class bases in stub files (`F821`) ([#10779](https://github.com/astral-sh/ruff/pull/10779)) -- \[`pygrep-hooks`\] Improve `blanket-noqa` error message (`PGH004`) ([#10851](https://github.com/astral-sh/ruff/pull/10851)) - -### CLI - -- Support `FORCE_COLOR` env var ([#10839](https://github.com/astral-sh/ruff/pull/10839)) - -### Configuration - -- Support negated patterns in `[extend-]per-file-ignores` ([#10852](https://github.com/astral-sh/ruff/pull/10852)) - -### Bug fixes - -- \[`flake8-import-conventions`\] Accept non-aliased (but correct) import in `unconventional-import-alias` (`ICN001`) ([#10729](https://github.com/astral-sh/ruff/pull/10729)) -- \[`flake8-quotes`\] Add semantic model flag when inside f-string replacement field ([#10766](https://github.com/astral-sh/ruff/pull/10766)) -- \[`pep8-naming`\] Recursively resolve `TypeDicts` for N815 violations ([#10719](https://github.com/astral-sh/ruff/pull/10719)) -- \[`flake8-quotes`\] Respect `Q00*` ignores in `flake8-quotes` rules ([#10728](https://github.com/astral-sh/ruff/pull/10728)) -- \[`flake8-simplify`\] Show negated condition in `needless-bool` diagnostics (`SIM103`) ([#10854](https://github.com/astral-sh/ruff/pull/10854)) -- \[`ruff`\] Use within-scope shadowed bindings in `asyncio-dangling-task` (`RUF006`) ([#10793](https://github.com/astral-sh/ruff/pull/10793)) -- \[`flake8-pytest-style`\] Fix single-tuple conversion in `pytest-parametrize-values-wrong-type` (`PT007`) ([#10862](https://github.com/astral-sh/ruff/pull/10862)) -- \[`flake8-return`\] Ignore assignments to annotated variables in `unnecessary-assign` (`RET504`) ([#10741](https://github.com/astral-sh/ruff/pull/10741)) -- \[`refurb`\] Do not allow any keyword arguments for `read-whole-file` in `rb` mode (`FURB101`) ([#10803](https://github.com/astral-sh/ruff/pull/10803)) -- \[`pylint`\] Don't recommend decorating staticmethods with `@singledispatch` (`PLE1519`, `PLE1520`) ([#10637](https://github.com/astral-sh/ruff/pull/10637)) -- \[`pydocstyle`\] Use section name range for all section-related docstring diagnostics ([#10740](https://github.com/astral-sh/ruff/pull/10740)) -- Respect `# noqa` directives on `__all__` openers ([#10798](https://github.com/astral-sh/ruff/pull/10798)) - -## 0.3.5 - -### Preview features - -- \[`pylint`\] Implement `modified-iterating-set` (`E4703`) ([#10473](https://github.com/astral-sh/ruff/pull/10473)) -- \[`refurb`\] Implement `for-loop-set-mutations` (`FURB142`) ([#10583](https://github.com/astral-sh/ruff/pull/10583)) -- \[`refurb`\] Implement `unnecessary-from-float` (`FURB164`) ([#10647](https://github.com/astral-sh/ruff/pull/10647)) -- \[`refurb`\] Implement `verbose-decimal-constructor` (`FURB157`) ([#10533](https://github.com/astral-sh/ruff/pull/10533)) - -### Rule changes - -- \[`flake8-comprehensions`\] Handled special case for `C401` which also matches `C416` ([#10596](https://github.com/astral-sh/ruff/pull/10596)) -- \[`flake8-pyi`\] Mark `unaliased-collections-abc-set-import` fix as "safe" for more cases in stub files (`PYI025`) ([#10547](https://github.com/astral-sh/ruff/pull/10547)) -- \[`numpy`\] Add `row_stack` to NumPy 2.0 migration rule ([#10646](https://github.com/astral-sh/ruff/pull/10646)) -- \[`pycodestyle`\] Allow cell magics before an import (`E402`) ([#10545](https://github.com/astral-sh/ruff/pull/10545)) -- \[`pycodestyle`\] Avoid blank line rules for the first logical line in cell ([#10291](https://github.com/astral-sh/ruff/pull/10291)) - -### Configuration - -- Respected nested namespace packages ([#10541](https://github.com/astral-sh/ruff/pull/10541)) -- \[`flake8-boolean-trap`\] Add setting for user defined allowed boolean trap ([#10531](https://github.com/astral-sh/ruff/pull/10531)) - -### Bug fixes - -- Correctly handle references in `__all__` definitions when renaming symbols in autofixes ([#10527](https://github.com/astral-sh/ruff/pull/10527)) -- Track ranges of names inside `__all__` definitions ([#10525](https://github.com/astral-sh/ruff/pull/10525)) -- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`) ([#10539](https://github.com/astral-sh/ruff/pull/10539)) -- \[`flake8-copyright`\] Accept commas in default copyright pattern ([#9498](https://github.com/astral-sh/ruff/pull/9498)) -- \[`flake8-datetimez`\] Allow f-strings with `%z` for `DTZ007` ([#10651](https://github.com/astral-sh/ruff/pull/10651)) -- \[`flake8-pytest-style`\] Fix `PT014` autofix for last item in list ([#10532](https://github.com/astral-sh/ruff/pull/10532)) -- \[`flake8-quotes`\] Ignore `Q000`, `Q001` when string is inside forward ref ([#10585](https://github.com/astral-sh/ruff/pull/10585)) -- \[`isort`\] Always place non-relative imports after relative imports ([#10669](https://github.com/astral-sh/ruff/pull/10669)) -- \[`isort`\] Respect Unicode characters in import sorting ([#10529](https://github.com/astral-sh/ruff/pull/10529)) -- \[`pyflakes`\] Fix F821 false negatives when `from __future__ import annotations` is active (attempt 2) ([#10524](https://github.com/astral-sh/ruff/pull/10524)) -- \[`pyflakes`\] Make `unnecessary-lambda` an always-unsafe fix ([#10668](https://github.com/astral-sh/ruff/pull/10668)) -- \[`pylint`\] Fixed false-positive on the rule `PLW1641` (`eq-without-hash`) ([#10566](https://github.com/astral-sh/ruff/pull/10566)) -- \[`ruff`\] Fix panic in unused `# noqa` removal with multi-byte space (`RUF100`) ([#10682](https://github.com/astral-sh/ruff/pull/10682)) - -### Documentation - -- Add PR title format to `CONTRIBUTING.md` ([#10665](https://github.com/astral-sh/ruff/pull/10665)) -- Fix list markup to include blank lines required ([#10591](https://github.com/astral-sh/ruff/pull/10591)) -- Put `flake8-logging` next to the other flake8 plugins in registry ([#10587](https://github.com/astral-sh/ruff/pull/10587)) -- \[`flake8-bandit`\] Update warning message for rule `S305` to address insecure block cipher mode use ([#10602](https://github.com/astral-sh/ruff/pull/10602)) -- \[`flake8-bugbear`\] Document use of anonymous assignment in `useless-expression` ([#10551](https://github.com/astral-sh/ruff/pull/10551)) -- \[`flake8-datetimez`\] Clarify error messages and docs for `DTZ` rules ([#10621](https://github.com/astral-sh/ruff/pull/10621)) -- \[`pycodestyle`\] Use same before vs. after numbers for `space-around-operator` ([#10640](https://github.com/astral-sh/ruff/pull/10640)) -- \[`ruff`\] Change `quadratic-list-summation` docs to use `iadd` consistently ([#10666](https://github.com/astral-sh/ruff/pull/10666)) - -## 0.3.4 - -### Preview features - -- \[`flake8-simplify`\] Detect implicit `else` cases in `needless-bool` (`SIM103`) ([#10414](https://github.com/astral-sh/ruff/pull/10414)) -- \[`pylint`\] Implement `nan-comparison` (`PLW0117`) ([#10401](https://github.com/astral-sh/ruff/pull/10401)) -- \[`pylint`\] Implement `nonlocal-and-global` (`E115`) ([#10407](https://github.com/astral-sh/ruff/pull/10407)) -- \[`pylint`\] Implement `singledispatchmethod-function` (`PLE5120`) ([#10428](https://github.com/astral-sh/ruff/pull/10428)) -- \[`refurb`\] Implement `list-reverse-copy` (`FURB187`) ([#10212](https://github.com/astral-sh/ruff/pull/10212)) - -### Rule changes - -- \[`flake8-pytest-style`\] Add automatic fix for `pytest-parametrize-values-wrong-type` (`PT007`) ([#10461](https://github.com/astral-sh/ruff/pull/10461)) -- \[`pycodestyle`\] Allow SPDX license headers to exceed the line length (`E501`) ([#10481](https://github.com/astral-sh/ruff/pull/10481)) - -### Formatter - -- Fix unstable formatting for trailing subscript end-of-line comment ([#10492](https://github.com/astral-sh/ruff/pull/10492)) - -### Bug fixes - -- Avoid code comment detection in PEP 723 script tags ([#10464](https://github.com/astral-sh/ruff/pull/10464)) -- Avoid incorrect tuple transformation in single-element case (`C409`) ([#10491](https://github.com/astral-sh/ruff/pull/10491)) -- Bug fix: Prevent fully defined links [`name`](link) from being reformatted ([#10442](https://github.com/astral-sh/ruff/pull/10442)) -- Consider raw source code for `W605` ([#10480](https://github.com/astral-sh/ruff/pull/10480)) -- Docs: Link inline settings when not part of options section ([#10499](https://github.com/astral-sh/ruff/pull/10499)) -- Don't treat annotations as redefinitions in `.pyi` files ([#10512](https://github.com/astral-sh/ruff/pull/10512)) -- Fix `E231` bug: Inconsistent catch compared to pycodestyle, such as when dict nested in list ([#10469](https://github.com/astral-sh/ruff/pull/10469)) -- Fix pylint upstream categories not showing in docs ([#10441](https://github.com/astral-sh/ruff/pull/10441)) -- Add missing `Options` references to blank line docs ([#10498](https://github.com/astral-sh/ruff/pull/10498)) -- 'Revert "F821: Fix false negatives in .py files when `from __future__ import annotations` is active (#10362)"' ([#10513](https://github.com/astral-sh/ruff/pull/10513)) -- Apply NFKC normalization to unicode identifiers in the lexer ([#10412](https://github.com/astral-sh/ruff/pull/10412)) -- Avoid failures due to non-deterministic binding ordering ([#10478](https://github.com/astral-sh/ruff/pull/10478)) -- \[`flake8-bugbear`\] Allow tuples of exceptions (`B030`) ([#10437](https://github.com/astral-sh/ruff/pull/10437)) -- \[`flake8-quotes`\] Avoid syntax errors due to invalid quotes (`Q000, Q002`) ([#10199](https://github.com/astral-sh/ruff/pull/10199)) - -## 0.3.3 - -### Preview features - -- \[`flake8-bandit`\]: Implement `S610` rule ([#10316](https://github.com/astral-sh/ruff/pull/10316)) -- \[`pycodestyle`\] Implement `blank-line-at-end-of-file` (`W391`) ([#10243](https://github.com/astral-sh/ruff/pull/10243)) -- \[`pycodestyle`\] Implement `redundant-backslash` (`E502`) ([#10292](https://github.com/astral-sh/ruff/pull/10292)) -- \[`pylint`\] - implement `redeclared-assigned-name` (`W0128`) ([#9268](https://github.com/astral-sh/ruff/pull/9268)) - -### Rule changes - -- \[`flake8_comprehensions`\] Handled special case for `C400` which also matches `C416` ([#10419](https://github.com/astral-sh/ruff/pull/10419)) -- \[`flake8-bandit`\] Implement upstream updates for `S311`, `S324` and `S605` ([#10313](https://github.com/astral-sh/ruff/pull/10313)) -- \[`pyflakes`\] Remove `F401` fix for `__init__` imports by default and allow opt-in to unsafe fix ([#10365](https://github.com/astral-sh/ruff/pull/10365)) -- \[`pylint`\] Implement `invalid-bool-return-type` (`E304`) ([#10377](https://github.com/astral-sh/ruff/pull/10377)) -- \[`pylint`\] Include builtin warnings in useless-exception-statement (`PLW0133`) ([#10394](https://github.com/astral-sh/ruff/pull/10394)) - -### CLI - -- Add message on success to `ruff check` ([#8631](https://github.com/astral-sh/ruff/pull/8631)) - -### Bug fixes - -- \[`PIE970`\] Allow trailing ellipsis in `typing.TYPE_CHECKING` ([#10413](https://github.com/astral-sh/ruff/pull/10413)) -- Avoid `TRIO115` if the argument is a variable ([#10376](https://github.com/astral-sh/ruff/pull/10376)) -- \[`F811`\] Avoid removing shadowed imports that point to different symbols ([#10387](https://github.com/astral-sh/ruff/pull/10387)) -- Fix `F821` and `F822` false positives in `.pyi` files ([#10341](https://github.com/astral-sh/ruff/pull/10341)) -- Fix `F821` false negatives in `.py` files when `from __future__ import annotations` is active ([#10362](https://github.com/astral-sh/ruff/pull/10362)) -- Fix case where `Indexer` fails to identify continuation preceded by newline #10351 ([#10354](https://github.com/astral-sh/ruff/pull/10354)) -- Sort hash maps in `Settings` display ([#10370](https://github.com/astral-sh/ruff/pull/10370)) -- Track conditional deletions in the semantic model ([#10415](https://github.com/astral-sh/ruff/pull/10415)) -- \[`C413`\] Wrap expressions in parentheses when negating ([#10346](https://github.com/astral-sh/ruff/pull/10346)) -- \[`pycodestyle`\] Do not ignore lines before the first logical line in blank lines rules. ([#10382](https://github.com/astral-sh/ruff/pull/10382)) -- \[`pycodestyle`\] Do not trigger `E225` and `E275` when the next token is a ')' ([#10315](https://github.com/astral-sh/ruff/pull/10315)) -- \[`pylint`\] Avoid false-positive slot non-assignment for `__dict__` (`PLE0237`) ([#10348](https://github.com/astral-sh/ruff/pull/10348)) -- Gate f-string struct size test for Rustc < 1.76 ([#10371](https://github.com/astral-sh/ruff/pull/10371)) - -### Documentation - -- Use `ruff.toml` format in README ([#10393](https://github.com/astral-sh/ruff/pull/10393)) -- \[`RUF008`\] Make it clearer that a mutable default in a dataclass is only valid if it is typed as a ClassVar ([#10395](https://github.com/astral-sh/ruff/pull/10395)) -- \[`pylint`\] Extend docs and test in `invalid-str-return-type` (`E307`) ([#10400](https://github.com/astral-sh/ruff/pull/10400)) -- Remove `.` from `check` and `format` commands ([#10217](https://github.com/astral-sh/ruff/pull/10217)) - -## 0.3.2 - -### Preview features - -- Improve single-`with` item formatting for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276)) - -### Rule changes - -- \[`pyupgrade`\] Allow fixes for f-string rule regardless of line length (`UP032`) ([#10263](https://github.com/astral-sh/ruff/pull/10263)) -- \[`pycodestyle`\] Include actual conditions in E712 diagnostics ([#10254](https://github.com/astral-sh/ruff/pull/10254)) - -### Bug fixes - -- Fix trailing kwargs end of line comment after slash ([#10297](https://github.com/astral-sh/ruff/pull/10297)) -- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274)) -- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265)) -- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280)) -- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283)) -- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285)) - -## 0.3.1 - -### Preview features - -- \[`pycodestyle`\] Fix E301 not triggering on decorated methods. ([#10117](https://github.com/astral-sh/ruff/pull/10117)) -- \[`pycodestyle`\] Respect `isort` settings in blank line rules (`E3*`) ([#10096](https://github.com/astral-sh/ruff/pull/10096)) -- \[`pycodestyle`\] Make blank lines in typing stub files optional (`E3*`) ([#10098](https://github.com/astral-sh/ruff/pull/10098)) -- \[`pylint`\] Implement `singledispatch-method` (`E1519`) ([#10140](https://github.com/astral-sh/ruff/pull/10140)) -- \[`pylint`\] Implement `useless-exception-statement` (`W0133`) ([#10176](https://github.com/astral-sh/ruff/pull/10176)) - -### Rule changes - -- \[`flake8-debugger`\] Check for use of `debugpy` and `ptvsd` debug modules (#10177) ([#10194](https://github.com/astral-sh/ruff/pull/10194)) -- \[`pyupgrade`\] Generate diagnostic for all valid f-string conversions regardless of line length (`UP032`) ([#10238](https://github.com/astral-sh/ruff/pull/10238)) -- \[`pep8_naming`\] Add fixes for `N804` and `N805` ([#10215](https://github.com/astral-sh/ruff/pull/10215)) - -### CLI - -- Colorize the output of `ruff format --diff` ([#10110](https://github.com/astral-sh/ruff/pull/10110)) -- Make `--config` and `--isolated` global flags ([#10150](https://github.com/astral-sh/ruff/pull/10150)) -- Correctly expand tildes and environment variables in paths passed to `--config` ([#10219](https://github.com/astral-sh/ruff/pull/10219)) - -### Configuration - -- Accept a PEP 440 version specifier for `required-version` ([#10216](https://github.com/astral-sh/ruff/pull/10216)) -- Implement isort's `default-section` setting ([#10149](https://github.com/astral-sh/ruff/pull/10149)) - -### Bug fixes - -- Remove trailing space from `CapWords` message ([#10220](https://github.com/astral-sh/ruff/pull/10220)) -- Respect external codes in file-level exemptions ([#10203](https://github.com/astral-sh/ruff/pull/10203)) -- \[`flake8-raise`\] Avoid false-positives for parens-on-raise with `future.exception()` (`RSE102`) ([#10206](https://github.com/astral-sh/ruff/pull/10206)) -- \[`pylint`\] Add fix for unary expressions in `PLC2801` ([#9587](https://github.com/astral-sh/ruff/pull/9587)) -- \[`ruff`\] Fix RUF028 not allowing `# fmt: skip` on match cases ([#10178](https://github.com/astral-sh/ruff/pull/10178)) - -## 0.3.0 - -This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to -detect invalid formatter suppression comments. - -### Preview features - -- \[`flake8-bandit`\] Remove suspicious-lxml-import (`S410`) ([#10154](https://github.com/astral-sh/ruff/pull/10154)) -- \[`pycodestyle`\] Allow `os.environ` modifications between imports (`E402`) ([#10066](https://github.com/astral-sh/ruff/pull/10066)) -- \[`pycodestyle`\] Don't warn about a single whitespace character before a comma in a tuple (`E203`) ([#10094](https://github.com/astral-sh/ruff/pull/10094)) - -### Rule changes - -- \[`eradicate`\] Detect commented out `case` statements (`ERA001`) ([#10055](https://github.com/astral-sh/ruff/pull/10055)) -- \[`eradicate`\] Detect single-line code for `try:`, `except:`, etc. (`ERA001`) ([#10057](https://github.com/astral-sh/ruff/pull/10057)) -- \[`flake8-boolean-trap`\] Allow boolean positionals in `__post_init__` ([#10027](https://github.com/astral-sh/ruff/pull/10027)) -- \[`flake8-copyright`\] Allow © in copyright notices ([#10065](https://github.com/astral-sh/ruff/pull/10065)) -- \[`isort`\]: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971)) -- \[`pylint`\] New Rule `dict-iter-missing-items` (`PLE1141`) ([#9845](https://github.com/astral-sh/ruff/pull/9845)) -- \[`pylint`\] Ignore `sys.version` and `sys.platform` (`PLR1714`) ([#10054](https://github.com/astral-sh/ruff/pull/10054)) -- \[`pyupgrade`\] Detect literals with unary operators (`UP018`) ([#10060](https://github.com/astral-sh/ruff/pull/10060)) -- \[`ruff`\] Expand rule for `list(iterable).pop(0)` idiom (`RUF015`) ([#10148](https://github.com/astral-sh/ruff/pull/10148)) - -### Formatter - -This release introduces the Ruff 2024.2 style, stabilizing the following changes: - -- Prefer splitting the assignment's value over the target or type annotation ([#8943](https://github.com/astral-sh/ruff/pull/8943)) -- Remove blank lines before class docstrings ([#9154](https://github.com/astral-sh/ruff/pull/9154)) -- Wrap multiple context managers in `with` parentheses when targeting Python 3.9 or newer ([#9222](https://github.com/astral-sh/ruff/pull/9222)) -- Add a blank line after nested classes with a dummy body (`...`) in typing stub files ([#9155](https://github.com/astral-sh/ruff/pull/9155)) -- Reduce vertical spacing for classes and functions with a dummy (`...`) body ([#7440](https://github.com/astral-sh/ruff/issues/7440), [#9240](https://github.com/astral-sh/ruff/pull/9240)) -- Add a blank line after the module docstring ([#8283](https://github.com/astral-sh/ruff/pull/8283)) -- Parenthesize long type hints in assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210)) -- Preserve indent for single multiline-string call-expressions ([#9673](https://github.com/astral-sh/ruff/pull/9637)) -- Normalize hex escape and unicode escape sequences ([#9280](https://github.com/astral-sh/ruff/pull/9280)) -- Format module docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725)) - -### CLI - -- Explicitly disallow `extend` as part of a `--config` flag ([#10135](https://github.com/astral-sh/ruff/pull/10135)) -- Remove `build` from the default exclusion list ([#10093](https://github.com/astral-sh/ruff/pull/10093)) -- Deprecate `ruff `, `ruff --explain`, `ruff --clean`, and `ruff --generate-shell-completion` in favor of `ruff check `, `ruff rule`, `ruff clean`, and `ruff generate-shell-completion` ([#10169](https://github.com/astral-sh/ruff/pull/10169)) -- Remove the deprecated CLI option `--format` from `ruff rule` and `ruff linter` ([#10170](https://github.com/astral-sh/ruff/pull/10170)) - -### Bug fixes - -- \[`flake8-bugbear`\] Avoid adding default initializers to stubs (`B006`) ([#10152](https://github.com/astral-sh/ruff/pull/10152)) -- \[`flake8-type-checking`\] Respect runtime-required decorators for function signatures ([#10091](https://github.com/astral-sh/ruff/pull/10091)) -- \[`pycodestyle`\] Mark fixes overlapping with a multiline string as unsafe (`W293`) ([#10049](https://github.com/astral-sh/ruff/pull/10049)) -- \[`pydocstyle`\] Trim whitespace when removing blank lines after section (`D413`) ([#10162](https://github.com/astral-sh/ruff/pull/10162)) -- \[`pylint`\] Delete entire statement, including semicolons (`PLR0203`) ([#10074](https://github.com/astral-sh/ruff/pull/10074)) -- \[`ruff`\] Avoid f-string false positives in `gettext` calls (`RUF027`) ([#10118](https://github.com/astral-sh/ruff/pull/10118)) -- Fix `ruff` crashing on PowerPC systems because of too small page size ([#10080](https://github.com/astral-sh/ruff/pull/10080)) - -### Performance - -- Add cold attribute to less likely printer queue branches in the formatter ([#10121](https://github.com/astral-sh/ruff/pull/10121)) -- Skip unnecessary string normalization in the formatter ([#10116](https://github.com/astral-sh/ruff/pull/10116)) - -### Documentation - -- Remove "Beta" Label from formatter documentation ([#10144](https://github.com/astral-sh/ruff/pull/10144)) -- `line-length` option: fix link to `pycodestyle.max-line-length` ([#10136](https://github.com/astral-sh/ruff/pull/10136)) - -## 0.2.2 - -Highlights include: - -- Initial support formatting f-strings (in `--preview`). -- Support for overriding arbitrary configuration options via the CLI through an expanded `--config` argument (e.g., `--config "lint.isort.combine-as-imports=false"`). -- Significant performance improvements in Ruff's lexer, parser, and lint rules. - -### Preview features - -- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642)) -- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266)) -- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880)) - -### Rule changes - -- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793)) -- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862)) -- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887)) -- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976)) -- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952)) - -### Formatter - -- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915)) -- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957)) -- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922)) - -### CLI - -- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599)) - -### Bug fixes - -- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866)) -- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905)) -- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969)) -- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981)) -- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953)) -- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996)) -- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859)) -- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955)) -- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907)) -- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894)) -- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878)) -- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964)) -- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956)) -- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865)) -- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990)) -- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849)) - -### Performance - -- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888)) -- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853)) -- Reduce `Result` size by using `Box` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885)) -- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900)) -- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867)) -- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884)) - -## 0.2.1 - -This release includes support for range formatting (i.e., the ability to format specific lines -within a source file). - -### Preview features - -- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728)) -- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725)) - -### Formatter - -- Add `--range` option to `ruff format` ([#9733](https://github.com/astral-sh/ruff/pull/9733)) -- Don't trim last empty line in docstrings ([#9813](https://github.com/astral-sh/ruff/pull/9813)) - -### Bug fixes - -- Skip empty lines when determining base indentation ([#9795](https://github.com/astral-sh/ruff/pull/9795)) -- Drop `__get__` and `__set__` from `unnecessary-dunder-call` ([#9791](https://github.com/astral-sh/ruff/pull/9791)) -- Respect generic `Protocol` in ellipsis removal ([#9841](https://github.com/astral-sh/ruff/pull/9841)) -- Revert "Use publicly available Apple Silicon runners (#9726)" ([#9834](https://github.com/astral-sh/ruff/pull/9834)) - -### Performance - -- Skip LibCST parsing for standard dedent adjustments ([#9769](https://github.com/astral-sh/ruff/pull/9769)) -- Remove CST-based fixer for `C408` ([#9822](https://github.com/astral-sh/ruff/pull/9822)) -- Add our own ignored-names abstractions ([#9802](https://github.com/astral-sh/ruff/pull/9802)) -- Remove CST-based fixers for `C400`, `C401`, `C410`, and `C418` ([#9819](https://github.com/astral-sh/ruff/pull/9819)) -- Use `AhoCorasick` to speed up quote match ([#9773](https://github.com/astral-sh/ruff/pull/9773)) -- Remove CST-based fixers for `C405` and `C409` ([#9821](https://github.com/astral-sh/ruff/pull/9821)) -- Add fast-path for comment detection ([#9808](https://github.com/astral-sh/ruff/pull/9808)) -- Invert order of checks in `zero-sleep-call` ([#9766](https://github.com/astral-sh/ruff/pull/9766)) -- Short-circuit typing matches based on imports ([#9800](https://github.com/astral-sh/ruff/pull/9800)) -- Run dunder method rule on methods directly ([#9815](https://github.com/astral-sh/ruff/pull/9815)) -- Track top-level module imports in the semantic model ([#9775](https://github.com/astral-sh/ruff/pull/9775)) -- Slight speed-up for lowercase and uppercase identifier checks ([#9798](https://github.com/astral-sh/ruff/pull/9798)) -- Remove LibCST-based fixer for `C403` ([#9818](https://github.com/astral-sh/ruff/pull/9818)) - -### Documentation - -- Update `max-pos-args` example to `max-positional-args` ([#9797](https://github.com/astral-sh/ruff/pull/9797)) -- Fixed example code in `weak_cryptographic_key.rs` ([#9774](https://github.com/astral-sh/ruff/pull/9774)) -- Fix references to deprecated `ANN` rules in changelog ([#9771](https://github.com/astral-sh/ruff/pull/9771)) -- Fix default for `max-positional-args` ([#9838](https://github.com/astral-sh/ruff/pull/9838)) - -## 0.2.0 - -### Breaking changes - -- The `NURSERY` selector cannot be used anymore -- Legacy selection of nursery rules by exact codes is no longer allowed without preview enabled - -See also, the "Remapped rules" section which may result in disabled rules. - -### Deprecations - -The following rules are now deprecated: - -- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`) -- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`) - -The following command line options are now deprecated: - -- `--show-source`; use `--output-format full` instead -- `--no-show-source`; use `--output-format concise` instead -- `--output-format text`; use `full` or `concise` instead - -The following settings have moved and the previous name is deprecated: - -- `ruff.allowed-confusables` → [`ruff.lint.allowed-confusables`](https://docs.astral.sh//ruff/settings/#lint_allowed-confusables) -- `ruff.dummy-variable-rgx` → [`ruff.lint.dummy-variable-rgx`](https://docs.astral.sh//ruff/settings/#lint_dummy-variable-rgx) -- `ruff.explicit-preview-rules` → [`ruff.lint.explicit-preview-rules`](https://docs.astral.sh//ruff/settings/#lint_explicit-preview-rules) -- `ruff.extend-fixable` → [`ruff.lint.extend-fixable`](https://docs.astral.sh//ruff/settings/#lint_extend-fixable) -- `ruff.extend-ignore` → [`ruff.lint.extend-ignore`](https://docs.astral.sh//ruff/settings/#lint_extend-ignore) -- `ruff.extend-per-file-ignores` → [`ruff.lint.extend-per-file-ignores`](https://docs.astral.sh//ruff/settings/#lint_extend-per-file-ignores) -- `ruff.extend-safe-fixes` → [`ruff.lint.extend-safe-fixes`](https://docs.astral.sh//ruff/settings/#lint_extend-safe-fixes) -- `ruff.extend-select` → [`ruff.lint.extend-select`](https://docs.astral.sh//ruff/settings/#lint_extend-select) -- `ruff.extend-unfixable` → [`ruff.lint.extend-unfixable`](https://docs.astral.sh//ruff/settings/#lint_extend-unfixable) -- `ruff.extend-unsafe-fixes` → [`ruff.lint.extend-unsafe-fixes`](https://docs.astral.sh//ruff/settings/#lint_extend-unsafe-fixes) -- `ruff.external` → [`ruff.lint.external`](https://docs.astral.sh//ruff/settings/#lint_external) -- `ruff.fixable` → [`ruff.lint.fixable`](https://docs.astral.sh//ruff/settings/#lint_fixable) -- `ruff.flake8-annotations` → [`ruff.lint.flake8-annotations`](https://docs.astral.sh//ruff/settings/#lint_flake8-annotations) -- `ruff.flake8-bandit` → [`ruff.lint.flake8-bandit`](https://docs.astral.sh//ruff/settings/#lint_flake8-bandit) -- `ruff.flake8-bugbear` → [`ruff.lint.flake8-bugbear`](https://docs.astral.sh//ruff/settings/#lint_flake8-bugbear) -- `ruff.flake8-builtins` → [`ruff.lint.flake8-builtins`](https://docs.astral.sh//ruff/settings/#lint_flake8-builtins) -- `ruff.flake8-comprehensions` → [`ruff.lint.flake8-comprehensions`](https://docs.astral.sh//ruff/settings/#lint_flake8-comprehensions) -- `ruff.flake8-copyright` → [`ruff.lint.flake8-copyright`](https://docs.astral.sh//ruff/settings/#lint_flake8-copyright) -- `ruff.flake8-errmsg` → [`ruff.lint.flake8-errmsg`](https://docs.astral.sh//ruff/settings/#lint_flake8-errmsg) -- `ruff.flake8-gettext` → [`ruff.lint.flake8-gettext`](https://docs.astral.sh//ruff/settings/#lint_flake8-gettext) -- `ruff.flake8-implicit-str-concat` → [`ruff.lint.flake8-implicit-str-concat`](https://docs.astral.sh//ruff/settings/#lint_flake8-implicit-str-concat) -- `ruff.flake8-import-conventions` → [`ruff.lint.flake8-import-conventions`](https://docs.astral.sh//ruff/settings/#lint_flake8-import-conventions) -- `ruff.flake8-pytest-style` → [`ruff.lint.flake8-pytest-style`](https://docs.astral.sh//ruff/settings/#lint_flake8-pytest-style) -- `ruff.flake8-quotes` → [`ruff.lint.flake8-quotes`](https://docs.astral.sh//ruff/settings/#lint_flake8-quotes) -- `ruff.flake8-self` → [`ruff.lint.flake8-self`](https://docs.astral.sh//ruff/settings/#lint_flake8-self) -- `ruff.flake8-tidy-imports` → [`ruff.lint.flake8-tidy-imports`](https://docs.astral.sh//ruff/settings/#lint_flake8-tidy-imports) -- `ruff.flake8-type-checking` → [`ruff.lint.flake8-type-checking`](https://docs.astral.sh//ruff/settings/#lint_flake8-type-checking) -- `ruff.flake8-unused-arguments` → [`ruff.lint.flake8-unused-arguments`](https://docs.astral.sh//ruff/settings/#lint_flake8-unused-arguments) -- `ruff.ignore` → [`ruff.lint.ignore`](https://docs.astral.sh//ruff/settings/#lint_ignore) -- `ruff.ignore-init-module-imports` → [`ruff.lint.ignore-init-module-imports`](https://docs.astral.sh//ruff/settings/#lint_ignore-init-module-imports) -- `ruff.isort` → [`ruff.lint.isort`](https://docs.astral.sh//ruff/settings/#lint_isort) -- `ruff.logger-objects` → [`ruff.lint.logger-objects`](https://docs.astral.sh//ruff/settings/#lint_logger-objects) -- `ruff.mccabe` → [`ruff.lint.mccabe`](https://docs.astral.sh//ruff/settings/#lint_mccabe) -- `ruff.pep8-naming` → [`ruff.lint.pep8-naming`](https://docs.astral.sh//ruff/settings/#lint_pep8-naming) -- `ruff.per-file-ignores` → [`ruff.lint.per-file-ignores`](https://docs.astral.sh//ruff/settings/#lint_per-file-ignores) -- `ruff.pycodestyle` → [`ruff.lint.pycodestyle`](https://docs.astral.sh//ruff/settings/#lint_pycodestyle) -- `ruff.pydocstyle` → [`ruff.lint.pydocstyle`](https://docs.astral.sh//ruff/settings/#lint_pydocstyle) -- `ruff.pyflakes` → [`ruff.lint.pyflakes`](https://docs.astral.sh//ruff/settings/#lint_pyflakes) -- `ruff.pylint` → [`ruff.lint.pylint`](https://docs.astral.sh//ruff/settings/#lint_pylint) -- `ruff.pyupgrade` → [`ruff.lint.pyupgrade`](https://docs.astral.sh//ruff/settings/#lint_pyupgrade) -- `ruff.select` → [`ruff.lint.select`](https://docs.astral.sh//ruff/settings/#lint_select) -- `ruff.task-tags` → [`ruff.lint.task-tags`](https://docs.astral.sh//ruff/settings/#lint_task-tags) -- `ruff.typing-modules` → [`ruff.lint.typing-modules`](https://docs.astral.sh//ruff/settings/#lint_typing-modules) -- `ruff.unfixable` → [`ruff.lint.unfixable`](https://docs.astral.sh//ruff/settings/#lint_unfixable) - -### Remapped rules - -The following rules have been remapped to new codes: - -- [`raise-without-from-inside-except`](https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/): `TRY200` to `B904` -- [`suspicious-eval-usage`](https://docs.astral.sh/ruff/rules/suspicious-eval-usage/): `PGH001` to `S307` -- [`logging-warn`](https://docs.astral.sh/ruff/rules/logging-warn/): `PGH002` to `G010` -- [`static-key-dict-comprehension`](https://docs.astral.sh/ruff/rules/static-key-dict-comprehension): `RUF011` to `B035` -- [`runtime-string-union`](https://docs.astral.sh/ruff/rules/runtime-string-union): `TCH006` to `TCH010` - -### Stabilizations - -The following rules have been stabilized and are no longer in preview: - -- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await) (`TRIO100`) -- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call) (`TRIO105`) -- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout) (`TRIO109`) -- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep) (`TRIO110`) -- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call) (`TRIO115`) -- [`unnecessary-escaped-quote`](https://docs.astral.sh/ruff/rules/unnecessary-escaped-quote) (`Q004`) -- [`enumerate-for-loop`](https://docs.astral.sh/ruff/rules/enumerate-for-loop) (`SIM113`) -- [`zip-dict-keys-and-values`](https://docs.astral.sh/ruff/rules/zip-dict-keys-and-values) (`SIM911`) -- [`timeout-error-alias`](https://docs.astral.sh/ruff/rules/timeout-error-alias) (`UP041`) -- [`flask-debug-true`](https://docs.astral.sh/ruff/rules/flask-debug-true) (`S201`) -- [`tarfile-unsafe-members`](https://docs.astral.sh/ruff/rules/tarfile-unsafe-members) (`S202`) -- [`ssl-insecure-version`](https://docs.astral.sh/ruff/rules/ssl-insecure-version) (`S502`) -- [`ssl-with-bad-defaults`](https://docs.astral.sh/ruff/rules/ssl-with-bad-defaults) (`S503`) -- [`ssl-with-no-version`](https://docs.astral.sh/ruff/rules/ssl-with-no-version) (`S504`) -- [`weak-cryptographic-key`](https://docs.astral.sh/ruff/rules/weak-cryptographic-key) (`S505`) -- [`ssh-no-host-key-verification`](https://docs.astral.sh/ruff/rules/ssh-no-host-key-verification) (`S507`) -- [`django-raw-sql`](https://docs.astral.sh/ruff/rules/django-raw-sql) (`S611`) -- [`mako-templates`](https://docs.astral.sh/ruff/rules/mako-templates) (`S702`) -- [`generator-return-from-iter-method`](https://docs.astral.sh/ruff/rules/generator-return-from-iter-method) (`PYI058`) -- [`runtime-string-union`](https://docs.astral.sh/ruff/rules/runtime-string-union) (`TCH006`) -- [`numpy2-deprecation`](https://docs.astral.sh/ruff/rules/numpy2-deprecation) (`NPY201`) -- [`quadratic-list-summation`](https://docs.astral.sh/ruff/rules/quadratic-list-summation) (`RUF017`) -- [`assignment-in-assert`](https://docs.astral.sh/ruff/rules/assignment-in-assert) (`RUF018`) -- [`unnecessary-key-check`](https://docs.astral.sh/ruff/rules/unnecessary-key-check) (`RUF019`) -- [`never-union`](https://docs.astral.sh/ruff/rules/never-union) (`RUF020`) -- [`direct-logger-instantiation`](https://docs.astral.sh/ruff/rules/direct-logger-instantiation) (`LOG001`) -- [`invalid-get-logger-argument`](https://docs.astral.sh/ruff/rules/invalid-get-logger-argument) (`LOG002`) -- [`exception-without-exc-info`](https://docs.astral.sh/ruff/rules/exception-without-exc-info) (`LOG007`) -- [`undocumented-warn`](https://docs.astral.sh/ruff/rules/undocumented-warn) (`LOG009`) - -Fixes for the following rules have been stabilized and are now available without preview: - -- [`triple-single-quotes`](https://docs.astral.sh/ruff/rules/triple-single-quotes) (`D300`) -- [`non-pep604-annotation`](https://docs.astral.sh/ruff/rules/non-pep604-annotation) (`UP007`) -- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default) (`SIM910`) -- [`in-dict-keys`](https://docs.astral.sh/ruff/rules/in-dict-keys) (`SIM118`) -- [`collapsible-else-if`](https://docs.astral.sh/ruff/rules/collapsible-else-if) (`PLR5501`) -- [`if-with-same-arms`](https://docs.astral.sh/ruff/rules/if-with-same-arms) (`SIM114`) -- [`useless-else-on-loop`](https://docs.astral.sh/ruff/rules/useless-else-on-loop) (`PLW0120`) -- [`unnecessary-literal-union`](https://docs.astral.sh/ruff/rules/unnecessary-literal-union) (`PYI030`) -- [`unnecessary-spread`](https://docs.astral.sh/ruff/rules/unnecessary-spread) (`PIE800`) -- [`error-instead-of-exception`](https://docs.astral.sh/ruff/rules/error-instead-of-exception) (`TRY400`) -- [`redefined-while-unused`](https://docs.astral.sh/ruff/rules/redefined-while-unused) (`F811`) -- [`duplicate-value`](https://docs.astral.sh/ruff/rules/duplicate-value) (`B033`) -- [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) -- [`non-pep585-annotation`](https://docs.astral.sh/ruff/rules/non-pep585-annotation) (`UP006`) - -Fixes for the following rules have been promoted from unsafe to safe: - -- [`unaliased-collections-abc-set-import`](https://docs.astral.sh/ruff/rules/unaliased-collections-abc-set-import) (`PYI025`) - -The following behaviors have been stabilized: - -- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) allows `sys.path` modifications between imports -- [`reimplemented-container-builtin`](https://docs.astral.sh/ruff/rules/reimplemented-container-builtin/) (`PIE807`) includes lambdas that can be replaced with `dict` -- [`unnecessary-placeholder`](https://docs.astral.sh/ruff/rules/unnecessary-placeholder/) (`PIE790`) applies to unnecessary ellipses (`...`) -- [`if-else-block-instead-of-dict-get`](https://docs.astral.sh/ruff/rules/if-else-block-instead-of-dict-get/) (`SIM401`) applies to `if-else` expressions - -### Preview features - -- \[`refurb`\] Implement `metaclass_abcmeta` (`FURB180`) ([#9658](https://github.com/astral-sh/ruff/pull/9658)) -- Implement `blank_line_after_nested_stub_class` preview style ([#9155](https://github.com/astral-sh/ruff/pull/9155)) -- The preview rule [`and-or-ternary`](https://docs.astral.sh/ruff/rules/and-or-ternary) (`PLR1706`) was removed - -### Bug fixes - -- \[`flake8-async`\] Take `pathlib.Path` into account when analyzing async functions ([#9703](https://github.com/astral-sh/ruff/pull/9703)) -- \[`flake8-return`\] - fix indentation syntax error (`RET505`) ([#9705](https://github.com/astral-sh/ruff/pull/9705)) -- Detect multi-statement lines in else removal ([#9748](https://github.com/astral-sh/ruff/pull/9748)) -- `RUF022`, `RUF023`: never add two trailing commas to the end of a sequence ([#9698](https://github.com/astral-sh/ruff/pull/9698)) -- `RUF023`: Don't sort `__match_args__`, only `__slots__` ([#9724](https://github.com/astral-sh/ruff/pull/9724)) -- \[`flake8-simplify`\] - Fix syntax error in autofix (`SIM114`) ([#9704](https://github.com/astral-sh/ruff/pull/9704)) -- \[`pylint`\] Show verbatim constant in `magic-value-comparison` (`PLR2004`) ([#9694](https://github.com/astral-sh/ruff/pull/9694)) -- Removing trailing whitespace inside multiline strings is unsafe ([#9744](https://github.com/astral-sh/ruff/pull/9744)) -- Support `IfExp` with dual string arms in `invalid-envvar-default` ([#9734](https://github.com/astral-sh/ruff/pull/9734)) -- \[`pylint`\] Add `__mro_entries__` to known dunder methods (`PLW3201`) ([#9706](https://github.com/astral-sh/ruff/pull/9706)) - -### Documentation - -- Removed rules are now retained in the documentation ([#9691](https://github.com/astral-sh/ruff/pull/9691)) -- Deprecated rules are now indicated in the documentation ([#9689](https://github.com/astral-sh/ruff/pull/9689)) - -## 0.1.15 - -### Preview features - -- Error when `NURSERY` selector is used with `--preview` ([#9682](https://github.com/astral-sh/ruff/pull/9682)) -- Preserve indentation around multiline strings in formatter ([#9637](https://github.com/astral-sh/ruff/pull/9637)) -- \[`flake8-return`\] Add fixes for all rules (`RET505`, `RET506`, `RET507`, `RET508`) ([#9595](https://github.com/astral-sh/ruff/pull/9595)) -- \[`flake8-simplify`\] Add fix for `if-with-same-arms` (`SIM114`) ([#9591](https://github.com/astral-sh/ruff/pull/9591)) -- \[`pycodestyle`\] Add fix for `multiple-imports-on-one-line` (`E401`) ([#9518](https://github.com/astral-sh/ruff/pull/9518)) -- \[`pylint`\] Add fix for `collapsible-else-if` (`PLR5501`) ([#9594](https://github.com/astral-sh/ruff/pull/9594)) -- \[`pylint`\] Add fix for `useless-else-on-loop` (`PLW0120`) ([#9590](https://github.com/astral-sh/ruff/pull/9590)) -- \[`pylint`\] Implement `assigning-non-slot` (`E0237`) ([#9623](https://github.com/astral-sh/ruff/pull/9623)) -- \[`pylint`\] Implement `potential-index-error` (`PLE0643`) ([#9545](https://github.com/astral-sh/ruff/pull/9545)) -- \[`pylint`\] Implement `too-many-nested-blocks` (`PLR1702`) ([#9172](https://github.com/astral-sh/ruff/pull/9172)) -- \[`ruff`\] Add rule to sort `__slots__` and `__match_args__` ([#9564](https://github.com/astral-sh/ruff/pull/9564)) -- \[`ruff`\] Detect unnecessary `dict` comprehensions for iterables (`RUF025`) ([#9613](https://github.com/astral-sh/ruff/pull/9613)) -- \[`ruff`\] Guard against use of `default_factory` as a keyword argument (`RUF026`) ([#9651](https://github.com/astral-sh/ruff/pull/9651)) -- \[`ruff`\] Implement `mutable-fromkeys-value` (`RUF024`) ([#9597](https://github.com/astral-sh/ruff/pull/9597)) - -### CLI - -- Enable auto-wrapping of `--help` output ([#9633](https://github.com/astral-sh/ruff/pull/9633)) - -### Bug fixes - -- Avoid rendering display-only rules as fixable ([#9649](https://github.com/astral-sh/ruff/pull/9649)) -- Detect automagic-like assignments in notebooks ([#9653](https://github.com/astral-sh/ruff/pull/9653)) -- Generate custom JSON schema for dynamic setting ([#9632](https://github.com/astral-sh/ruff/pull/9632)) -- \[`flake8-no-pep420`\] Include global `--config` when determining namespace packages ([#9603](https://github.com/astral-sh/ruff/pull/9603)) -- \[`flake8-pie`\] Omit bound tuples passed to `.startswith` or `.endswith` ([#9661](https://github.com/astral-sh/ruff/pull/9661)) -- \[`flake8-return`\] Avoid panic when fixing inlined else blocks ([#9657](https://github.com/astral-sh/ruff/pull/9657)) -- \[`flake8-return`\] Consider exception suppression in unnecessary assignment ([#9673](https://github.com/astral-sh/ruff/pull/9673)) -- \[`flake8-return`\] Take `NoReturn` annotation into account when analyzing implicit returns ([#9636](https://github.com/astral-sh/ruff/pull/9636)) -- \[`flake8-simplify`\] Support inverted returns in `needless-bool` (`SIM103`) ([#9619](https://github.com/astral-sh/ruff/pull/9619)) -- \[`flake8-type-checking`\] Add Pydantic's `BaseConfig` to default-copy list ([#9650](https://github.com/astral-sh/ruff/pull/9650)) -- \[`flake8-type-checking`\] Avoid marking `InitVar` as a typing-only annotation ([#9688](https://github.com/astral-sh/ruff/pull/9688)) -- \[`pycodestyle`\] Allow `dtype` comparisons in `type-comparison` ([#9676](https://github.com/astral-sh/ruff/pull/9676)) -- \[`pydocstyle`\] Re-implement `last-line-after-section` (`D413`) ([#9654](https://github.com/astral-sh/ruff/pull/9654)) - -### Documentation - -- \[`flake8-pytest-style`\] Add fix safety documentation for `duplicate-parameterize-test-cases` ([#9678](https://github.com/astral-sh/ruff/pull/9678)) -- \[`pylint`\] Document `literal-membership` fix safety conditions ([#9677](https://github.com/astral-sh/ruff/pull/9677)) -- \[`isort`\] Fix reference to `isort` rule code ([#9598](https://github.com/astral-sh/ruff/pull/9598)) - -## 0.1.14 - -### Preview features - -- \[`flake8-bugbear`\] Add fix for `duplicate-value` (`B033`) ([#9510](https://github.com/astral-sh/ruff/pull/9510)) -- \[`flake8-simplify`\] Implement `enumerate-for-loop` (`SIM113`) ([#7777](https://github.com/astral-sh/ruff/pull/7777)) -- \[`pygrep_hooks`\] Add fix for `deprecated-log-warn` (`PGH002`) ([#9519](https://github.com/astral-sh/ruff/pull/9519)) -- \[`pylint`\] Implement `import-private-name` (`C2701`) ([#5920](https://github.com/astral-sh/ruff/pull/5920)) -- \[`refurb`\] Implement `regex-flag-alias` with fix (`FURB167`) ([#9516](https://github.com/astral-sh/ruff/pull/9516)) -- \[`ruff`\] Add rule and fix to sort contents of `__all__` (`RUF022`) ([#9474](https://github.com/astral-sh/ruff/pull/9474)) -- \[`tryceratops`\] Add fix for `error-instead-of-exception` (`TRY400`) ([#9520](https://github.com/astral-sh/ruff/pull/9520)) - -### Rule changes - -- \[`flake8-pyi`\] Fix `PYI047` false negatives on PEP-695 type aliases ([#9566](https://github.com/astral-sh/ruff/pull/9566)) -- \[`flake8-pyi`\] Fix `PYI049` false negatives on call-based `TypedDict`s ([#9567](https://github.com/astral-sh/ruff/pull/9567)) -- \[`pylint`\] Exclude `self` and `cls` when counting method arguments (`PLR0917`) ([#9563](https://github.com/astral-sh/ruff/pull/9563)) - -### CLI - -- `--show-settings` displays active settings in a far more readable format ([#9464](https://github.com/astral-sh/ruff/pull/9464)) -- Add `--extension` support to the formatter ([#9483](https://github.com/astral-sh/ruff/pull/9483)) - -### Configuration - -- Ignore preview status for fixable and unfixable selectors ([#9538](https://github.com/astral-sh/ruff/pull/9538)) -- \[`pycodestyle`\] Use the configured tab size when expanding indents ([#9506](https://github.com/astral-sh/ruff/pull/9506)) - -### Bug fixes - -- Recursively visit deferred AST nodes ([#9541](https://github.com/astral-sh/ruff/pull/9541)) -- Visit deferred lambdas before type definitions ([#9540](https://github.com/astral-sh/ruff/pull/9540)) -- \[`flake8-simplify`\] Avoid some more `enumerate-for-loop` false positives (`SIM113`) ([#9515](https://github.com/astral-sh/ruff/pull/9515)) -- \[`pandas-vet`\] Limit inplace diagnostics to methods that accept inplace ([#9495](https://github.com/astral-sh/ruff/pull/9495)) -- \[`pylint`\] Add the `__prepare__` method to the list of recognized dunder method ([#9529](https://github.com/astral-sh/ruff/pull/9529)) -- \[`pylint`\] Ignore unnecessary dunder calls within dunder definitions ([#9496](https://github.com/astral-sh/ruff/pull/9496)) -- \[`refurb`\] Avoid bailing when `reimplemented-operator` is called on function (`FURB118`) ([#9556](https://github.com/astral-sh/ruff/pull/9556)) -- \[`ruff`\] Avoid treating named expressions as static keys (`RUF011`) ([#9494](https://github.com/astral-sh/ruff/pull/9494)) - -### Documentation - -- Add instructions on using `noqa` with isort rules ([#9555](https://github.com/astral-sh/ruff/pull/9555)) -- Documentation update for URL giving 'page not found' ([#9565](https://github.com/astral-sh/ruff/pull/9565)) -- Fix admonition in dark mode ([#9502](https://github.com/astral-sh/ruff/pull/9502)) -- Update contributing docs to use `cargo bench -p ruff_benchmark` ([#9535](https://github.com/astral-sh/ruff/pull/9535)) -- Update emacs integration section to include `emacs-ruff-format` ([#9403](https://github.com/astral-sh/ruff/pull/9403)) -- \[`flake8-blind-except`\] Document exceptions to `blind-except` rule ([#9580](https://github.com/astral-sh/ruff/pull/9580)) - -## 0.1.13 - -### Bug fixes - -- Include base pyproject when initializing cache settings ([#9480](https://github.com/astral-sh/ruff/pull/9480)) -- \[`flake8-simplify`\] Account for possibly-empty f-string values in truthiness logic ([#9484](https://github.com/astral-sh/ruff/pull/9484)) -- \[`pylint`\] Add the missing period in `unnecessary-dunder-call` ([#9485](https://github.com/astral-sh/ruff/pull/9485)) -- \[`pylint`\] Fix `__aenter__` message in `unnecessary-dunder-call` ([#9492](https://github.com/astral-sh/ruff/pull/9492)) - -## 0.1.12 - -### Preview features - -- Formatter: Hug multiline-strings in preview style ([#9243](https://github.com/astral-sh/ruff/pull/9243)) -- \[`flake8-bandit`\] Add `ssl-with-no-version` (`S504`) ([#9384](https://github.com/astral-sh/ruff/pull/9384)) -- \[`flake8-bandit`\] Implement `ssl-insecure-version` (`S502`) ([#9390](https://github.com/astral-sh/ruff/pull/9390)) -- \[`flake8-bandit`\] Implement `ssl-with-bad-defaults` (`S503`) ([#9391](https://github.com/astral-sh/ruff/pull/9391)) -- \[`flake8-bandit`\] Implement suspicious import rules (`S4XX`) ([#8831](https://github.com/astral-sh/ruff/pull/8831)) -- \[`flake8-simplify`\] Implement `zip-dict-keys-and-values` (`SIM911`) ([#9460](https://github.com/astral-sh/ruff/pull/9460)) -- \[`pyflakes`\] Add a fix for `redefined-while-unused` (`F811`) ([#9419](https://github.com/astral-sh/ruff/pull/9419)) -- \[`pylint`\] Implement `unnecessary-dunder-call` (`C2801`) ([#9166](https://github.com/astral-sh/ruff/pull/9166)) -- \[`ruff`\] Add `parenthesize-chained-operators` (`RUF021`) to enforce parentheses in `a or b and c` ([#9440](https://github.com/astral-sh/ruff/pull/9440)) - -### Rule changes - -- \[`flake8-boolean-trap`\] Allow Boolean positional arguments in setters ([#9429](https://github.com/astral-sh/ruff/pull/9429)) -- \[`flake8-builtins`\] Restrict `builtin-attribute-shadowing` (`A003`) to actual shadowed references ([#9462](https://github.com/astral-sh/ruff/pull/9462)) -- \[`flake8-pyi`\] Add fix for `generator-return-from-iter-method` (`PYI058`) ([#9355](https://github.com/astral-sh/ruff/pull/9355)) -- \[`pyflakes`\] Don't flag `redefined-while-unused` (`F811`) in `if` branches ([#9418](https://github.com/astral-sh/ruff/pull/9418)) -- \[`pyupgrade`\] Add some additional Python 3.12 typing members to `deprecated-import` ([#9445](https://github.com/astral-sh/ruff/pull/9445)) -- \[`ruff`\] Add fix for `parenthesize-chained-operators` (`RUF021`) ([#9449](https://github.com/astral-sh/ruff/pull/9449)) -- \[`ruff`\] Include subscripts and attributes in static key rule (`RUF011`) ([#9416](https://github.com/astral-sh/ruff/pull/9416)) -- \[`ruff`\] Support variable keys in static dictionary key rule (`RUF011`) ([#9411](https://github.com/astral-sh/ruff/pull/9411)) - -### Formatter - -- Generate deterministic IDs when formatting notebooks ([#9359](https://github.com/astral-sh/ruff/pull/9359)) -- Allow `# fmt: skip` with interspersed same-line comments ([#9395](https://github.com/astral-sh/ruff/pull/9395)) -- Parenthesize breaking named expressions in match guards ([#9396](https://github.com/astral-sh/ruff/pull/9396)) - -### Bug fixes - -- Add cell indexes to all diagnostics ([#9387](https://github.com/astral-sh/ruff/pull/9387)) -- Avoid infinite loop in constant vs. `None` comparisons ([#9376](https://github.com/astral-sh/ruff/pull/9376)) -- Handle raises with implicit alternate branches ([#9377](https://github.com/astral-sh/ruff/pull/9377)) -- Ignore trailing quotes for unclosed l-brace errors ([#9388](https://github.com/astral-sh/ruff/pull/9388)) -- Respect multi-segment submodule imports when resolving qualified names ([#9382](https://github.com/astral-sh/ruff/pull/9382)) -- Use `DisplayParseError` for stdin parser errors ([#9409](https://github.com/astral-sh/ruff/pull/9409)) -- Use `comment_ranges` for isort directive extraction ([#9414](https://github.com/astral-sh/ruff/pull/9414)) -- Use transformed source code for diagnostic locations ([#9408](https://github.com/astral-sh/ruff/pull/9408)) -- \[`flake8-pyi`\] Exclude `warnings.deprecated` and `typing_extensions.deprecated` arguments ([#9423](https://github.com/astral-sh/ruff/pull/9423)) -- \[`flake8-pyi`\] Fix false negative for `unused-private-protocol` (`PYI046`) with unused generic protocols ([#9405](https://github.com/astral-sh/ruff/pull/9405)) -- \[`pydocstyle`\] Disambiguate argument descriptors from section headers ([#9427](https://github.com/astral-sh/ruff/pull/9427)) -- \[`pylint`\] Homogenize `PLR0914` message to match other `PLR09XX` rules ([#9399](https://github.com/astral-sh/ruff/pull/9399)) -- \[`ruff`\] Allow `Hashable = None` in type annotations (`RUF013`) ([#9442](https://github.com/astral-sh/ruff/pull/9442)) - -### Documentation - -- Fix admonition hyperlink colouring ([#9385](https://github.com/astral-sh/ruff/pull/9385)) -- Add missing preview link ([#9386](https://github.com/astral-sh/ruff/pull/9386)) - -## 0.1.11 - -### Preview features - -- \[`pylint`\] Implement `super-without-brackets` (`W0245`) ([#9257](https://github.com/astral-sh/ruff/pull/9257)) - -### Bug fixes - -- Check path string properly in `python -m ruff` invocations ([#9367](https://github.com/astral-sh/ruff/pull/9367)) - -### Documentation - -- Tweak `relative-imports` message ([#9365](https://github.com/astral-sh/ruff/pull/9365)) -- Add fix safety note for `yield-in-for-loop` ([#9364](https://github.com/astral-sh/ruff/pull/9364)) - -## 0.1.10 - -### Preview features - -- Improve `dummy_implementations` preview style formatting ([#9240](https://github.com/astral-sh/ruff/pull/9240)) -- Normalise Hex and unicode escape sequences in strings ([#9280](https://github.com/astral-sh/ruff/pull/9280)) -- Parenthesize long type annotations in annotated assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210)) -- Parenthesize multi-context managers in `with` statements ([#9222](https://github.com/astral-sh/ruff/pull/9222)) -- \[`flake8-pyi`\] Implement `generator-return-from-iter-method` (`PYI058`) ([#9313](https://github.com/astral-sh/ruff/pull/9313)) -- \[`pylint`\] Implement `empty-comment` (`PLR2044`) ([#9174](https://github.com/astral-sh/ruff/pull/9174)) -- \[`refurb`\] Implement `bit-count` (`FURB161`) ([#9265](https://github.com/astral-sh/ruff/pull/9265)) -- \[`ruff`\] Add `never-union` rule to detect redundant `typing.NoReturn` and `typing.Never` ([#9217](https://github.com/astral-sh/ruff/pull/9217)) - -### CLI - -- Add paths to TOML parse errors ([#9358](https://github.com/astral-sh/ruff/pull/9358)) -- Add row and column numbers to formatter parse errors ([#9321](https://github.com/astral-sh/ruff/pull/9321)) -- Improve responsiveness when invoked via Python ([#9315](https://github.com/astral-sh/ruff/pull/9315)) -- Short rule messages should not end with a period ([#9345](https://github.com/astral-sh/ruff/pull/9345)) - -### Configuration - -- Respect runtime-required decorators on functions ([#9317](https://github.com/astral-sh/ruff/pull/9317)) - -### Bug fixes - -- Avoid `asyncio-dangling-task` for nonlocal and global bindings ([#9263](https://github.com/astral-sh/ruff/pull/9263)) -- Escape trailing placeholders in rule documentation ([#9301](https://github.com/astral-sh/ruff/pull/9301)) -- Fix continuation detection following multi-line strings ([#9332](https://github.com/astral-sh/ruff/pull/9332)) -- Fix scoping for generators in named expressions in classes ([#9248](https://github.com/astral-sh/ruff/pull/9248)) -- Port from obsolete wsl crate to is-wsl ([#9356](https://github.com/astral-sh/ruff/pull/9356)) -- Remove special pre-visit for module docstrings ([#9261](https://github.com/astral-sh/ruff/pull/9261)) -- Respect `__str__` definitions from super classes ([#9338](https://github.com/astral-sh/ruff/pull/9338)) -- Respect `unused-noqa` via `per-file-ignores` ([#9300](https://github.com/astral-sh/ruff/pull/9300)) -- Respect attribute chains when resolving builtin call paths ([#9309](https://github.com/astral-sh/ruff/pull/9309)) -- Treat all `typing_extensions` members as typing aliases ([#9335](https://github.com/astral-sh/ruff/pull/9335)) -- Use `Display` for formatter parse errors ([#9316](https://github.com/astral-sh/ruff/pull/9316)) -- Wrap subscripted dicts in parens for f-string conversion ([#9238](https://github.com/astral-sh/ruff/pull/9238)) -- \[`flake8-annotations`\] Avoid adding return types to stub methods ([#9277](https://github.com/astral-sh/ruff/pull/9277)) -- \[`flake8-annotations`\] Respect mixed `return` and `raise` cases in return-type analysis ([#9310](https://github.com/astral-sh/ruff/pull/9310)) -- \[`flake8-bandit`\] Don't report violations when `SafeLoader` is imported from `yaml.loader` (`S506`) ([#9299](https://github.com/astral-sh/ruff/pull/9299)) -- \[`pylint`\] Avoid panic when comment is preceded by Unicode ([#9331](https://github.com/astral-sh/ruff/pull/9331)) -- \[`pylint`\] Change `PLR0917` error message to match other `PLR09XX` messages ([#9308](https://github.com/astral-sh/ruff/pull/9308)) -- \[`refurb`\] Avoid false positives for `math-constant` (`FURB152`) ([#9290](https://github.com/astral-sh/ruff/pull/9290)) - -### Documentation - -- Expand target name for better rule documentation ([#9302](https://github.com/astral-sh/ruff/pull/9302)) -- Fix typos found by codespell ([#9346](https://github.com/astral-sh/ruff/pull/9346)) -- \[`perflint`\] Document `PERF102` fix un-safety ([#9351](https://github.com/astral-sh/ruff/pull/9351)) -- \[`pyupgrade`\] Document `UP007` fix un-safety ([#9306](https://github.com/astral-sh/ruff/pull/9306)) - -## 0.1.9 - -### Breaking changes - -- Add site-packages to default exclusions ([#9188](https://github.com/astral-sh/ruff/pull/9188)) - -### Preview features - -- Fix: Avoid parenthesizing subscript targets and values ([#9209](https://github.com/astral-sh/ruff/pull/9209)) -- \[`pylint`\] Implement `too-many-locals` (`PLR0914`) ([#9163](https://github.com/astral-sh/ruff/pull/9163)) -- Implement `reimplemented_operator` (FURB118) ([#9171](https://github.com/astral-sh/ruff/pull/9171)) -- Add a rule to detect string members in runtime-evaluated unions ([#9143](https://github.com/astral-sh/ruff/pull/9143)) -- Implement `no_blank_line_before_class_docstring` preview style ([#9154](https://github.com/astral-sh/ruff/pull/9154)) - -### Rule changes - -- `CONSTANT_CASE` variables are improperly flagged for yoda violation (`SIM300`) ([#9164](https://github.com/astral-sh/ruff/pull/9164)) -- \[`flake8-pyi`\] Cover ParamSpecs and TypeVarTuples (`PYI018`) ([#9198](https://github.com/astral-sh/ruff/pull/9198)) -- \[`flake8-bugbear`\] Add fix for `zip-without-explicit-strict` (`B905`) ([#9176](https://github.com/astral-sh/ruff/pull/9176)) -- Add fix to automatically remove `print` and `pprint` statements (`T201`, `T203`) ([#9208](https://github.com/astral-sh/ruff/pull/9208)) -- Prefer `Never` to `NoReturn` in auto-typing in Python >= 3.11 (`ANN201`) ([#9213](https://github.com/astral-sh/ruff/pull/9213)) - -### Formatter - -- `can_omit_optional_parentheses`: Exit early for unparenthesized expressions ([#9125](https://github.com/astral-sh/ruff/pull/9125)) -- Fix `dynamic` mode with doctests so that it doesn't exceed configured line width ([#9129](https://github.com/astral-sh/ruff/pull/9129)) -- Fix `can_omit_optional_parentheses` for expressions with a right most fstring ([#9124](https://github.com/astral-sh/ruff/pull/9124)) -- Add `target_version` to formatter options ([#9220](https://github.com/astral-sh/ruff/pull/9220)) - -### CLI - -- Update `ruff format --check` to display message for already formatted files ([#9153](https://github.com/astral-sh/ruff/pull/9153)) - -### Bug fixes - -- Reverse order of arguments for `operator.contains` ([#9192](https://github.com/astral-sh/ruff/pull/9192)) -- Iterate over lambdas in deferred type annotations ([#9175](https://github.com/astral-sh/ruff/pull/9175)) -- Fix panic in `D208` with multibyte indent ([#9147](https://github.com/astral-sh/ruff/pull/9147)) -- Add support for `NoReturn` in auto-return-typing ([#9206](https://github.com/astral-sh/ruff/pull/9206)) -- Allow removal of `typing` from `exempt-modules` ([#9214](https://github.com/astral-sh/ruff/pull/9214)) -- Avoid `mutable-class-default` violations for Pydantic subclasses ([#9187](https://github.com/astral-sh/ruff/pull/9187)) -- Fix dropped union expressions for piped non-types in `PYI055` autofix ([#9161](https://github.com/astral-sh/ruff/pull/9161)) -- Enable annotation quoting for multi-line expressions ([#9142](https://github.com/astral-sh/ruff/pull/9142)) -- Deduplicate edits when quoting annotations ([#9140](https://github.com/astral-sh/ruff/pull/9140)) -- Prevent invalid utf8 indexing in cell magic detection ([#9146](https://github.com/astral-sh/ruff/pull/9146)) -- Avoid nested quotations in auto-quoting fix ([#9168](https://github.com/astral-sh/ruff/pull/9168)) -- Add base-class inheritance detection to flake8-django rules ([#9151](https://github.com/astral-sh/ruff/pull/9151)) -- Avoid `asyncio-dangling-task` violations on shadowed bindings ([#9215](https://github.com/astral-sh/ruff/pull/9215)) - -### Documentation - -- Fix blog post URL in changelog ([#9119](https://github.com/astral-sh/ruff/pull/9119)) -- Add error suppression hint for multi-line strings ([#9205](https://github.com/astral-sh/ruff/pull/9205)) -- Fix typo in SemanticModel.parent_expression docstring ([#9167](https://github.com/astral-sh/ruff/pull/9167)) -- Document link between import sorting and formatter ([#9117](https://github.com/astral-sh/ruff/pull/9117)) - -## 0.1.8 - -This release includes opt-in support for formatting Python snippets within -docstrings via the `docstring-code-format` setting. -[Check out the blog post](https://astral.sh/blog/ruff-v0.1.8) for more details! - -### Preview features - -- Add `"preserve"` quote-style to mimic Black's skip-string-normalization ([#8822](https://github.com/astral-sh/ruff/pull/8822)) -- Implement `prefer_splitting_right_hand_side_of_assignments` preview style ([#8943](https://github.com/astral-sh/ruff/pull/8943)) -- \[`pycodestyle`\] Add fix for `unexpected-spaces-around-keyword-parameter-equals` ([#9072](https://github.com/astral-sh/ruff/pull/9072)) -- \[`pycodestyle`\] Add fix for comment-related whitespace rules ([#9075](https://github.com/astral-sh/ruff/pull/9075)) -- \[`pycodestyle`\] Allow `sys.path` modifications between imports ([#9047](https://github.com/astral-sh/ruff/pull/9047)) -- \[`refurb`\] Implement `hashlib-digest-hex` (`FURB181`) ([#9077](https://github.com/astral-sh/ruff/pull/9077)) - -### Rule changes - -- Allow `flake8-type-checking` rules to automatically quote runtime-evaluated references ([#6001](https://github.com/astral-sh/ruff/pull/6001)) -- Allow transparent cell magics in Jupyter Notebooks ([#8911](https://github.com/astral-sh/ruff/pull/8911)) -- \[`flake8-annotations`\] Avoid `ANN2xx` fixes for abstract methods with empty bodies ([#9034](https://github.com/astral-sh/ruff/pull/9034)) -- \[`flake8-self`\] Ignore underscore references in type annotations ([#9036](https://github.com/astral-sh/ruff/pull/9036)) -- \[`pep8-naming`\] Allow class names when `apps.get_model` is a non-string ([#9065](https://github.com/astral-sh/ruff/pull/9065)) -- \[`pycodestyle`\] Allow `matplotlib.use` calls to intersperse imports ([#9094](https://github.com/astral-sh/ruff/pull/9094)) -- \[`pyflakes`\] Support fixing unused assignments in tuples by renaming variables (`F841`) ([#9107](https://github.com/astral-sh/ruff/pull/9107)) -- \[`pylint`\] Add fix for `subprocess-run-without-check` (`PLW1510`) ([#6708](https://github.com/astral-sh/ruff/pull/6708)) - -### Formatter - -- Add `docstring-code-format` knob to enable docstring snippet formatting ([#8854](https://github.com/astral-sh/ruff/pull/8854)) -- Use double quotes for all docstrings, including single-quoted docstrings ([#9020](https://github.com/astral-sh/ruff/pull/9020)) -- Implement "dynamic" line width mode for docstring code formatting ([#9098](https://github.com/astral-sh/ruff/pull/9098)) -- Support reformatting Markdown code blocks ([#9030](https://github.com/astral-sh/ruff/pull/9030)) -- add support for formatting reStructuredText code snippets ([#9003](https://github.com/astral-sh/ruff/pull/9003)) -- Avoid trailing comma for single-argument with positional separator ([#9076](https://github.com/astral-sh/ruff/pull/9076)) -- Fix handling of trailing target comment ([#9051](https://github.com/astral-sh/ruff/pull/9051)) - -### CLI - -- Hide unsafe fix suggestions when explicitly disabled ([#9095](https://github.com/astral-sh/ruff/pull/9095)) -- Add SARIF support to `--output-format` ([#9078](https://github.com/astral-sh/ruff/pull/9078)) - -### Bug fixes - -- Apply unnecessary index rule prior to enumerate rewrite ([#9012](https://github.com/astral-sh/ruff/pull/9012)) -- \[`flake8-err-msg`\] Allow `EM` fixes even if `msg` variable is defined ([#9059](https://github.com/astral-sh/ruff/pull/9059)) -- \[`flake8-pie`\] Prevent keyword arguments duplication ([#8450](https://github.com/astral-sh/ruff/pull/8450)) -- \[`flake8-pie`\] Respect trailing comma in `unnecessary-dict-kwargs` (`PIE804`) ([#9015](https://github.com/astral-sh/ruff/pull/9015)) -- \[`flake8-raise`\] Avoid removing parentheses on ctypes.WinError ([#9027](https://github.com/astral-sh/ruff/pull/9027)) -- \[`isort`\] Avoid invalid combination of `force-sort-within-types` and `lines-between-types` ([#9041](https://github.com/astral-sh/ruff/pull/9041)) -- \[`isort`\] Ensure that from-style imports are always ordered first in `__future__` ([#9039](https://github.com/astral-sh/ruff/pull/9039)) -- \[`pycodestyle`\] Allow tab indentation before keyword ([#9099](https://github.com/astral-sh/ruff/pull/9099)) -- \[`pylint`\] Ignore `@overrides` and `@overloads` for `too-many-positional` ([#9000](https://github.com/astral-sh/ruff/pull/9000)) -- \[`pyupgrade`\] Enable `printf-string-formatting` fix with comments on right-hand side ([#9037](https://github.com/astral-sh/ruff/pull/9037)) -- \[`refurb`\] Make `math-constant` (`FURB152`) rule more targeted ([#9054](https://github.com/astral-sh/ruff/pull/9054)) -- \[`refurb`\] Support floating-point base in `redundant-log-base` (`FURB163`) ([#9100](https://github.com/astral-sh/ruff/pull/9100)) -- \[`ruff`\] Detect `unused-asyncio-dangling-task` (`RUF006`) on unused assignments ([#9060](https://github.com/astral-sh/ruff/pull/9060)) - -## 0.1.7 - -### Preview features - -- Implement multiline dictionary and list hugging for preview style ([#8293](https://github.com/astral-sh/ruff/pull/8293)) -- Implement the `fix_power_op_line_length` preview style ([#8947](https://github.com/astral-sh/ruff/pull/8947)) -- Use Python version to determine typing rewrite safety ([#8919](https://github.com/astral-sh/ruff/pull/8919)) -- \[`flake8-annotations`\] Enable auto-return-type involving `Optional` and `Union` annotations ([#8885](https://github.com/astral-sh/ruff/pull/8885)) -- \[`flake8-bandit`\] Implement `django-raw-sql` (`S611`) ([#8651](https://github.com/astral-sh/ruff/pull/8651)) -- \[`flake8-bandit`\] Implement `tarfile-unsafe-members` (`S202`) ([#8829](https://github.com/astral-sh/ruff/pull/8829)) -- \[`flake8-pyi`\] Implement fix for `unnecessary-literal-union` (`PYI030`) ([#7934](https://github.com/astral-sh/ruff/pull/7934)) -- \[`flake8-simplify`\] Extend `dict-get-with-none-default` (`SIM910`) to non-literals ([#8762](https://github.com/astral-sh/ruff/pull/8762)) -- \[`pylint`\] - add `unnecessary-list-index-lookup` (`PLR1736`) + autofix ([#7999](https://github.com/astral-sh/ruff/pull/7999)) -- \[`pylint`\] - implement R0202 and R0203 with autofixes ([#8335](https://github.com/astral-sh/ruff/pull/8335)) -- \[`pylint`\] Implement `repeated-keyword` (`PLe1132`) ([#8706](https://github.com/astral-sh/ruff/pull/8706)) -- \[`pylint`\] Implement `too-many-positional` (`PLR0917`) ([#8995](https://github.com/astral-sh/ruff/pull/8995)) -- \[`pylint`\] Implement `unnecessary-dict-index-lookup` (`PLR1733`) ([#8036](https://github.com/astral-sh/ruff/pull/8036)) -- \[`refurb`\] Implement `redundant-log-base` (`FURB163`) ([#8842](https://github.com/astral-sh/ruff/pull/8842)) - -### Rule changes - -- \[`flake8-boolean-trap`\] Allow booleans in `@override` methods ([#8882](https://github.com/astral-sh/ruff/pull/8882)) -- \[`flake8-bugbear`\] Avoid `B015`,`B018` for last expression in a cell ([#8815](https://github.com/astral-sh/ruff/pull/8815)) -- \[`flake8-pie`\] Allow ellipses for enum values in stub files ([#8825](https://github.com/astral-sh/ruff/pull/8825)) -- \[`flake8-pyi`\] Check PEP 695 type aliases for `snake-case-type-alias` and `t-suffixed-type-alias` ([#8966](https://github.com/astral-sh/ruff/pull/8966)) -- \[`flake8-pyi`\] Check for kwarg and vararg `NoReturn` type annotations ([#8948](https://github.com/astral-sh/ruff/pull/8948)) -- \[`flake8-simplify`\] Omit select context managers from `SIM117` ([#8801](https://github.com/astral-sh/ruff/pull/8801)) -- \[`pep8-naming`\] Allow Django model loads in `non-lowercase-variable-in-function` (`N806`) ([#8917](https://github.com/astral-sh/ruff/pull/8917)) -- \[`pycodestyle`\] Avoid `E703` for last expression in a cell ([#8821](https://github.com/astral-sh/ruff/pull/8821)) -- \[`pycodestyle`\] Update `E402` to work at cell level for notebooks ([#8872](https://github.com/astral-sh/ruff/pull/8872)) -- \[`pydocstyle`\] Avoid `D100` for Jupyter Notebooks ([#8816](https://github.com/astral-sh/ruff/pull/8816)) -- \[`pylint`\] Implement fix for `unspecified-encoding` (`PLW1514`) ([#8928](https://github.com/astral-sh/ruff/pull/8928)) - -### Formatter - -- Avoid unstable formatting in ellipsis-only body with trailing comment ([#8984](https://github.com/astral-sh/ruff/pull/8984)) -- Inline trailing comments for type alias similar to assignments ([#8941](https://github.com/astral-sh/ruff/pull/8941)) -- Insert trailing comma when function breaks with single argument ([#8921](https://github.com/astral-sh/ruff/pull/8921)) - -### CLI - -- Update `ruff check` and `ruff format` to default to the current directory ([#8791](https://github.com/astral-sh/ruff/pull/8791)) -- Stop at the first resolved parent configuration ([#8864](https://github.com/astral-sh/ruff/pull/8864)) - -### Configuration - -- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998)) -- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812)) -- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663)) -- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841)) - -### Bug fixes - -- Add support for `@functools.singledispatch` ([#8934](https://github.com/astral-sh/ruff/pull/8934)) -- Avoid off-by-one error in stripping noqa following multi-byte char ([#8979](https://github.com/astral-sh/ruff/pull/8979)) -- Avoid off-by-one error in with-item named expressions ([#8915](https://github.com/astral-sh/ruff/pull/8915)) -- Avoid syntax error via invalid ur string prefix ([#8971](https://github.com/astral-sh/ruff/pull/8971)) -- Avoid underflow in `get_model` matching ([#8965](https://github.com/astral-sh/ruff/pull/8965)) -- Avoid unnecessary index diagnostics when value is modified ([#8970](https://github.com/astral-sh/ruff/pull/8970)) -- Convert over-indentation rule to use number of characters ([#8983](https://github.com/astral-sh/ruff/pull/8983)) -- Detect implicit returns in auto-return-types ([#8952](https://github.com/astral-sh/ruff/pull/8952)) -- Fix start >= end error in over-indentation ([#8982](https://github.com/astral-sh/ruff/pull/8982)) -- Ignore `@overload` and `@override` methods for too-many-arguments checks ([#8954](https://github.com/astral-sh/ruff/pull/8954)) -- Lexer start of line is false only for `Mode::Expression` ([#8880](https://github.com/astral-sh/ruff/pull/8880)) -- Mark `pydantic_settings.BaseSettings` as having default copy semantics ([#8793](https://github.com/astral-sh/ruff/pull/8793)) -- Respect dictionary unpacking in `NamedTuple` assignments ([#8810](https://github.com/astral-sh/ruff/pull/8810)) -- Respect local subclasses in `flake8-type-checking` ([#8768](https://github.com/astral-sh/ruff/pull/8768)) -- Support type alias statements in simple statement positions ([#8916](https://github.com/astral-sh/ruff/pull/8916)) -- \[`flake8-annotations`\] Avoid filtering out un-representable types in return annotation ([#8881](https://github.com/astral-sh/ruff/pull/8881)) -- \[`flake8-pie`\] Retain extra ellipses in protocols and abstract methods ([#8769](https://github.com/astral-sh/ruff/pull/8769)) -- \[`flake8-pyi`\] Respect local enum subclasses in `simple-defaults` (`PYI052`) ([#8767](https://github.com/astral-sh/ruff/pull/8767)) -- \[`flake8-trio`\] Use correct range for `TRIO115` fix ([#8933](https://github.com/astral-sh/ruff/pull/8933)) -- \[`flake8-trio`\] Use full arguments range for zero-sleep-call ([#8936](https://github.com/astral-sh/ruff/pull/8936)) -- \[`isort`\] fix: mark `__main__` as first-party import ([#8805](https://github.com/astral-sh/ruff/pull/8805)) -- \[`pep8-naming`\] Avoid `N806` errors for type alias statements ([#8785](https://github.com/astral-sh/ruff/pull/8785)) -- \[`perflint`\] Avoid `PERF101` if there's an append in loop body ([#8809](https://github.com/astral-sh/ruff/pull/8809)) -- \[`pycodestyle`\] Allow space-before-colon after end-of-slice ([#8838](https://github.com/astral-sh/ruff/pull/8838)) -- \[`pydocstyle`\] Avoid non-character breaks in `over-indentation` (`D208`) ([#8866](https://github.com/astral-sh/ruff/pull/8866)) -- \[`pydocstyle`\] Ignore underlines when determining docstring logical lines ([#8929](https://github.com/astral-sh/ruff/pull/8929)) -- \[`pylint`\] Extend `self-assigning-variable` to multi-target assignments ([#8839](https://github.com/astral-sh/ruff/pull/8839)) -- \[`tryceratops`\] Avoid repeated triggers in nested `tryceratops` diagnostics ([#8772](https://github.com/astral-sh/ruff/pull/8772)) - -### Documentation - -- Add advice for fixing RUF008 when mutability is not desired ([#8853](https://github.com/astral-sh/ruff/pull/8853)) -- Added the command to run ruff using pkgx to the installation.md ([#8955](https://github.com/astral-sh/ruff/pull/8955)) -- Document fix safety for flake8-comprehensions and some pyupgrade rules ([#8918](https://github.com/astral-sh/ruff/pull/8918)) -- Fix doc formatting for zero-sleep-call ([#8937](https://github.com/astral-sh/ruff/pull/8937)) -- Remove duplicate imports from os-stat documentation ([#8930](https://github.com/astral-sh/ruff/pull/8930)) -- Replace generated reference to MkDocs ([#8806](https://github.com/astral-sh/ruff/pull/8806)) -- Update Arch Linux package URL in installation.md ([#8802](https://github.com/astral-sh/ruff/pull/8802)) -- \[`flake8-pyi`\] Fix error in `t-suffixed-type-alias` (`PYI043`) example ([#8963](https://github.com/astral-sh/ruff/pull/8963)) -- \[`flake8-pyi`\] Improve motivation for `custom-type-var-return-type` (`PYI019`) ([#8766](https://github.com/astral-sh/ruff/pull/8766)) - -## 0.1.6 - -### Preview features - -- \[`flake8-boolean-trap`\] Extend `boolean-type-hint-positional-argument` (`FBT001`) to include booleans in unions ([#7501](https://github.com/astral-sh/ruff/pull/7501)) -- \[`flake8-pie`\] Extend `reimplemented-list-builtin` (`PIE807`) to `dict` reimplementations ([#8608](https://github.com/astral-sh/ruff/pull/8608)) -- \[`flake8-pie`\] Extend `unnecessary-pass` (`PIE790`) to include ellipses (`...`) ([#8641](https://github.com/astral-sh/ruff/pull/8641)) -- \[`flake8-pie`\] Implement fix for `unnecessary-spread` (`PIE800`) ([#8668](https://github.com/astral-sh/ruff/pull/8668)) -- \[`flake8-quotes`\] Implement `unnecessary-escaped-quote` (`Q004`) ([#8630](https://github.com/astral-sh/ruff/pull/8630)) -- \[`pycodestyle`\] Implement fix for `multiple-spaces-after-keyword` (`E271`) and `multiple-spaces-before-keyword` (`E272`) ([#8622](https://github.com/astral-sh/ruff/pull/8622)) -- \[`pycodestyle`\] Implement fix for `multiple-spaces-after-operator` (`E222`) and `multiple-spaces-before-operator` (`E221`) ([#8623](https://github.com/astral-sh/ruff/pull/8623)) -- \[`pyflakes`\] Extend `is-literal` (`F632`) to include comparisons against mutable initializers ([#8607](https://github.com/astral-sh/ruff/pull/8607)) -- \[`pylint`\] Implement `redefined-argument-from-local` (`PLR1704`) ([#8159](https://github.com/astral-sh/ruff/pull/8159)) -- \[`pylint`\] Implement fix for `unnecessary-lambda` (`PLW0108`) ([#8621](https://github.com/astral-sh/ruff/pull/8621)) -- \[`refurb`\] Implement `if-expr-min-max` (`FURB136`) ([#8664](https://github.com/astral-sh/ruff/pull/8664)) -- \[`refurb`\] Implement `math-constant` (`FURB152`) ([#8727](https://github.com/astral-sh/ruff/pull/8727)) - -### Rule changes - -- \[`flake8-annotations`\] Add autotyping-like return type inference for annotation rules ([#8643](https://github.com/astral-sh/ruff/pull/8643)) -- \[`flake8-future-annotations`\] Implement fix for `future-required-type-annotation` (`FA102`) ([#8711](https://github.com/astral-sh/ruff/pull/8711)) -- \[`flake8-implicit-namespace-package`\] Avoid missing namespace violations in scripts with shebangs ([#8710](https://github.com/astral-sh/ruff/pull/8710)) -- \[`pydocstyle`\] Update `over-indentation` (`D208`) to preserve indentation offsets when fixing overindented lines ([#8699](https://github.com/astral-sh/ruff/pull/8699)) -- \[`pyupgrade`\] Refine `timeout-error-alias` (`UP041`) to remove false positives ([#8587](https://github.com/astral-sh/ruff/pull/8587)) - -### Formatter - -- Fix instability in `await` formatting with fluent style ([#8676](https://github.com/astral-sh/ruff/pull/8676)) -- Compare formatted and unformatted ASTs during formatter tests ([#8624](https://github.com/astral-sh/ruff/pull/8624)) -- Preserve trailing semicolon for Notebooks ([#8590](https://github.com/astral-sh/ruff/pull/8590)) - -### CLI - -- Improve debug printing for resolving origin of config settings ([#8729](https://github.com/astral-sh/ruff/pull/8729)) -- Write unchanged, excluded files to stdout when read via stdin ([#8596](https://github.com/astral-sh/ruff/pull/8596)) - -### Configuration - -- \[`isort`\] Support disabling sections with `no-sections = true` ([#8657](https://github.com/astral-sh/ruff/pull/8657)) -- \[`pep8-naming`\] Support local and dynamic class- and static-method decorators ([#8592](https://github.com/astral-sh/ruff/pull/8592)) -- \[`pydocstyle`\] Allow overriding pydocstyle convention rules ([#8586](https://github.com/astral-sh/ruff/pull/8586)) - -### Bug fixes - -- Avoid syntax error via importing `trio.lowlevel` ([#8730](https://github.com/astral-sh/ruff/pull/8730)) -- Omit unrolled augmented assignments in `PIE794` ([#8634](https://github.com/astral-sh/ruff/pull/8634)) -- Slice source code instead of generating it for `EM` fixes ([#7746](https://github.com/astral-sh/ruff/pull/7746)) -- Allow whitespace around colon in slices for `whitespace-before-punctuation` (`E203`) ([#8654](https://github.com/astral-sh/ruff/pull/8654)) -- Use function range for `no-self-use` ([#8637](https://github.com/astral-sh/ruff/pull/8637)) -- F-strings doesn't contain bytes literal for `PLW0129` ([#8675](https://github.com/astral-sh/ruff/pull/8675)) -- Improve detection of `TYPE_CHECKING` blocks imported from `typing_extensions` or `_typeshed` ([#8429](https://github.com/astral-sh/ruff/pull/8429)) -- Treat display as a builtin in IPython ([#8707](https://github.com/astral-sh/ruff/pull/8707)) -- Avoid `FURB113` autofix if comments are present ([#8494](https://github.com/astral-sh/ruff/pull/8494)) -- Consider the new f-string tokens for `flake8-commas` ([#8582](https://github.com/astral-sh/ruff/pull/8582)) -- Remove erroneous bad-dunder-name reference ([#8742](https://github.com/astral-sh/ruff/pull/8742)) -- Avoid recommending Self usages in metaclasses ([#8639](https://github.com/astral-sh/ruff/pull/8639)) -- Detect runtime-evaluated base classes defined in the current file ([#8572](https://github.com/astral-sh/ruff/pull/8572)) -- Avoid inserting trailing commas within f-strings ([#8574](https://github.com/astral-sh/ruff/pull/8574)) -- Remove incorrect deprecation label for stdout and stderr ([#8743](https://github.com/astral-sh/ruff/pull/8743)) -- Fix unnecessary parentheses in UP007 fix ([#8610](https://github.com/astral-sh/ruff/pull/8610)) -- Remove repeated and erroneous scoped settings headers in docs ([#8670](https://github.com/astral-sh/ruff/pull/8670)) -- Trim trailing empty strings when converting to f-strings ([#8712](https://github.com/astral-sh/ruff/pull/8712)) -- Fix ordering for `force-sort-within-sections` ([#8665](https://github.com/astral-sh/ruff/pull/8665)) -- Run unicode prefix rule over tokens ([#8709](https://github.com/astral-sh/ruff/pull/8709)) -- Update UP032 to unescape curly braces in literal parts of converted strings ([#8697](https://github.com/astral-sh/ruff/pull/8697)) -- List all ipython builtins ([#8719](https://github.com/astral-sh/ruff/pull/8719)) - -### Documentation - -- Document conventions in the FAQ ([#8638](https://github.com/astral-sh/ruff/pull/8638)) -- Redirect from rule codes to rule pages in docs ([#8636](https://github.com/astral-sh/ruff/pull/8636)) -- Fix permalink to convention setting ([#8575](https://github.com/astral-sh/ruff/pull/8575)) - -## 0.1.5 - -### Preview features - -- \[`flake8-bandit`\] Implement `mako-templates` (`S702`) ([#8533](https://github.com/astral-sh/ruff/pull/8533)) -- \[`flake8-trio`\] Implement `TRIO105` ([#8490](https://github.com/astral-sh/ruff/pull/8490)) -- \[`flake8-trio`\] Implement `TRIO109` ([#8534](https://github.com/astral-sh/ruff/pull/8534)) -- \[`flake8-trio`\] Implement `TRIO110` ([#8537](https://github.com/astral-sh/ruff/pull/8537)) -- \[`flake8-trio`\] Implement `TRIO115` ([#8486](https://github.com/astral-sh/ruff/pull/8486)) -- \[`refurb`\] Implement `type-none-comparison` (`FURB169`) ([#8487](https://github.com/astral-sh/ruff/pull/8487)) -- Flag all comparisons against builtin types in `E721` ([#8491](https://github.com/astral-sh/ruff/pull/8491)) -- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525)) - -### Formatter - -- Fix multiline lambda expression statement formatting ([#8466](https://github.com/astral-sh/ruff/pull/8466)) - -### CLI - -- Add hidden `--extension` to override inference of source type from file extension ([#8373](https://github.com/astral-sh/ruff/pull/8373)) - -### Configuration - -- Account for selector specificity when merging `extend_unsafe_fixes` and `override extend_safe_fixes` ([#8444](https://github.com/astral-sh/ruff/pull/8444)) -- Add support for disabling cache with `RUFF_NO_CACHE` environment variable ([#8538](https://github.com/astral-sh/ruff/pull/8538)) - -### Bug fixes - -- \[`E721`\] Flag comparisons to `memoryview` ([#8485](https://github.com/astral-sh/ruff/pull/8485)) -- Allow collapsed-ellipsis bodies in other statements ([#8499](https://github.com/astral-sh/ruff/pull/8499)) -- Avoid `D301` autofix for `u` prefixed strings ([#8495](https://github.com/astral-sh/ruff/pull/8495)) -- Only flag `flake8-trio` rules when `trio` import is present ([#8550](https://github.com/astral-sh/ruff/pull/8550)) -- Reject more syntactically invalid Python programs ([#8524](https://github.com/astral-sh/ruff/pull/8524)) -- Avoid raising `TRIO115` violations for `trio.sleep(...)` calls with non-number values ([#8532](https://github.com/astral-sh/ruff/pull/8532)) -- Fix `F841` false negative on assignment to multiple variables ([#8489](https://github.com/astral-sh/ruff/pull/8489)) - -### Documentation - -- Fix link to isort `known-first-party` ([#8562](https://github.com/astral-sh/ruff/pull/8562)) -- Add notes on fix safety to a few rules ([#8500](https://github.com/astral-sh/ruff/pull/8500)) -- Add missing toml config tabs ([#8512](https://github.com/astral-sh/ruff/pull/8512)) -- Add instructions for configuration of Emacs ([#8488](https://github.com/astral-sh/ruff/pull/8488)) -- Improve detail link contrast in dark mode ([#8548](https://github.com/astral-sh/ruff/pull/8548)) -- Fix typo in example ([#8506](https://github.com/astral-sh/ruff/pull/8506)) -- Added tabs for configuration files in the documentation ([#8480](https://github.com/astral-sh/ruff/pull/8480)) -- Recommend `project.requires-python` over `target-version` ([#8513](https://github.com/astral-sh/ruff/pull/8513)) -- Add singleton escape hatch to `B008` documentation ([#8501](https://github.com/astral-sh/ruff/pull/8501)) -- Fix tab configuration docs ([#8502](https://github.com/astral-sh/ruff/pull/8502)) - -## 0.1.4 - -### Preview features - -- \[`flake8-trio`\] Implement `timeout-without-await` (`TRIO001`) ([#8439](https://github.com/astral-sh/ruff/pull/8439)) -- \[`numpy`\] Implement NumPy 2.0 migration rule (`NPY200`) ([#7702](https://github.com/astral-sh/ruff/pull/7702)) -- \[`pylint`\] Implement `bad-open-mode` (`W1501`) ([#8294](https://github.com/astral-sh/ruff/pull/8294)) -- \[`pylint`\] Implement `import-outside-toplevel` (`C0415`) rule ([#5180](https://github.com/astral-sh/ruff/pull/5180)) -- \[`pylint`\] Implement `useless-with-lock` (`W2101`) ([#8321](https://github.com/astral-sh/ruff/pull/8321)) -- \[`pyupgrade`\] Implement `timeout-error-alias` (`UP041`) ([#8476](https://github.com/astral-sh/ruff/pull/8476)) -- \[`refurb`\] Implement `isinstance-type-none` (`FURB168`) ([#8308](https://github.com/astral-sh/ruff/pull/8308)) -- Detect confusable Unicode-to-Unicode units in `RUF001`, `RUF002`, and `RUF003` ([#4430](https://github.com/astral-sh/ruff/pull/4430)) -- Add newline after module docstrings in preview style ([#8283](https://github.com/astral-sh/ruff/pull/8283)) - -### Formatter - -- Add a note on line-too-long to the formatter docs ([#8314](https://github.com/astral-sh/ruff/pull/8314)) -- Preserve trailing statement semicolons when using `fmt: skip` ([#8273](https://github.com/astral-sh/ruff/pull/8273)) -- Preserve trailing semicolons when using `fmt: off` ([#8275](https://github.com/astral-sh/ruff/pull/8275)) -- Avoid duplicating linter-formatter compatibility warnings ([#8292](https://github.com/astral-sh/ruff/pull/8292)) -- Avoid inserting a newline after function docstrings ([#8375](https://github.com/astral-sh/ruff/pull/8375)) -- Insert newline between docstring and following own line comment ([#8216](https://github.com/astral-sh/ruff/pull/8216)) -- Split tuples in return positions by comma first ([#8280](https://github.com/astral-sh/ruff/pull/8280)) -- Avoid treating byte strings as docstrings ([#8350](https://github.com/astral-sh/ruff/pull/8350)) -- Add `--line-length` option to `format` command ([#8363](https://github.com/astral-sh/ruff/pull/8363)) -- Avoid parenthesizing unsplittable because of comments ([#8431](https://github.com/astral-sh/ruff/pull/8431)) - -### CLI - -- Add `--output-format` to `ruff rule` and `ruff linter` ([#8203](https://github.com/astral-sh/ruff/pull/8203)) - -### Bug fixes - -- Respect `--force-exclude` in `lint.exclude` and `format.exclude` ([#8393](https://github.com/astral-sh/ruff/pull/8393)) -- Respect `--extend-per-file-ignores` on the CLI ([#8329](https://github.com/astral-sh/ruff/pull/8329)) -- Extend `bad-dunder-method-name` to permit `__index__` ([#8300](https://github.com/astral-sh/ruff/pull/8300)) -- Fix panic with 8 in octal escape ([#8356](https://github.com/astral-sh/ruff/pull/8356)) -- Avoid raising `D300` when both triple quote styles are present ([#8462](https://github.com/astral-sh/ruff/pull/8462)) -- Consider unterminated f-strings in `FStringRanges` ([#8154](https://github.com/astral-sh/ruff/pull/8154)) -- Avoid including literal `shell=True` for truthy, non-`True` diagnostics ([#8359](https://github.com/astral-sh/ruff/pull/8359)) -- Avoid triggering single-element test for starred expressions ([#8433](https://github.com/astral-sh/ruff/pull/8433)) -- Detect and ignore Jupyter automagics ([#8398](https://github.com/astral-sh/ruff/pull/8398)) -- Fix invalid E231 error with f-strings ([#8369](https://github.com/astral-sh/ruff/pull/8369)) -- Avoid triggering `NamedTuple` rewrite with starred annotation ([#8434](https://github.com/astral-sh/ruff/pull/8434)) -- Avoid un-setting bracket flag in logical lines ([#8380](https://github.com/astral-sh/ruff/pull/8380)) -- Place 'r' prefix before 'f' for raw format strings ([#8464](https://github.com/astral-sh/ruff/pull/8464)) -- Remove trailing periods from NumPy 2.0 code actions ([#8475](https://github.com/astral-sh/ruff/pull/8475)) -- Fix bug where `PLE1307` was raised when formatting `%c` with characters ([#8407](https://github.com/astral-sh/ruff/pull/8407)) -- Remove unicode flag from comparable ([#8440](https://github.com/astral-sh/ruff/pull/8440)) -- Improve B015 message ([#8295](https://github.com/astral-sh/ruff/pull/8295)) -- Use `fixedOverflowWidgets` for playground popover ([#8458](https://github.com/astral-sh/ruff/pull/8458)) -- Mark `byte_bounds` as a non-backwards-compatible NumPy 2.0 change ([#8474](https://github.com/astral-sh/ruff/pull/8474)) - -### Internals - -- Add a dedicated cache directory per Ruff version ([#8333](https://github.com/astral-sh/ruff/pull/8333)) -- Allow selective caching for `--fix` and `--diff` ([#8316](https://github.com/astral-sh/ruff/pull/8316)) -- Improve performance of comment parsing ([#8193](https://github.com/astral-sh/ruff/pull/8193)) -- Improve performance of string parsing ([#8227](https://github.com/astral-sh/ruff/pull/8227)) -- Use a dedicated sort key for isort import sorting ([#7963](https://github.com/astral-sh/ruff/pull/7963)) - -## 0.1.3 - -This release includes a variety of improvements to the Ruff formatter, removing several known and -unintentional deviations from Black. - -### Formatter - -- Avoid space around pow for `None`, `True` and `False` ([#8189](https://github.com/astral-sh/ruff/pull/8189)) -- Avoid sorting all paths in the format command ([#8181](https://github.com/astral-sh/ruff/pull/8181)) -- Insert necessary blank line between class and leading comments ([#8224](https://github.com/astral-sh/ruff/pull/8224)) -- Avoid introducing new parentheses in annotated assignments ([#8233](https://github.com/astral-sh/ruff/pull/8233)) -- Refine the warnings about incompatible linter options ([#8196](https://github.com/astral-sh/ruff/pull/8196)) -- Add test and basic implementation for formatter preview mode ([#8044](https://github.com/astral-sh/ruff/pull/8044)) -- Refine warning about incompatible `isort` settings ([#8192](https://github.com/astral-sh/ruff/pull/8192)) -- Only omit optional parentheses for starting or ending with parentheses ([#8238](https://github.com/astral-sh/ruff/pull/8238)) -- Use source type to determine parser mode for formatting ([#8205](https://github.com/astral-sh/ruff/pull/8205)) -- Don't warn about magic trailing comma when `isort.force-single-line` is true ([#8244](https://github.com/astral-sh/ruff/pull/8244)) -- Use `SourceKind::diff` for formatter ([#8240](https://github.com/astral-sh/ruff/pull/8240)) -- Fix `fmt:off` with trailing child comment ([#8234](https://github.com/astral-sh/ruff/pull/8234)) -- Formatter parentheses support for `IpyEscapeCommand` ([#8207](https://github.com/astral-sh/ruff/pull/8207)) - -### Linter - -- \[`pylint`\] Add buffer methods to `bad-dunder-method-name` (`PLW3201`) exclusions ([#8190](https://github.com/astral-sh/ruff/pull/8190)) -- Match rule prefixes from `external` codes setting in `unused-noqa` ([#8177](https://github.com/astral-sh/ruff/pull/8177)) -- Use `line-length` setting for isort in lieu of `pycodestyle.max-line-length` ([#8235](https://github.com/astral-sh/ruff/pull/8235)) -- Update fix for `unnecessary-paren-on-raise-exception` to unsafe for unknown types ([#8231](https://github.com/astral-sh/ruff/pull/8231)) -- Correct quick fix message for `W605` ([#8255](https://github.com/astral-sh/ruff/pull/8255)) - -### Documentation - -- Fix typo in max-doc-length documentation ([#8201](https://github.com/astral-sh/ruff/pull/8201)) -- Improve documentation around linter-formatter conflicts ([#8257](https://github.com/astral-sh/ruff/pull/8257)) -- Fix link to error suppression documentation in `unused-noqa` ([#8172](https://github.com/astral-sh/ruff/pull/8172)) -- Add `external` option to `unused-noqa` documentation ([#8171](https://github.com/astral-sh/ruff/pull/8171)) -- Add title attribute to icons ([#8060](https://github.com/astral-sh/ruff/pull/8060)) -- Clarify unsafe case in RSE102 ([#8256](https://github.com/astral-sh/ruff/pull/8256)) -- Fix skipping formatting examples ([#8210](https://github.com/astral-sh/ruff/pull/8210)) -- docs: fix name of `magic-trailing-comma` option in README ([#8200](https://github.com/astral-sh/ruff/pull/8200)) -- Add note about scope of rule changing in versioning policy ([#8169](https://github.com/astral-sh/ruff/pull/8169)) -- Document: Fix default lint rules ([#8218](https://github.com/astral-sh/ruff/pull/8218)) -- Fix a wrong setting in configuration.md ([#8186](https://github.com/astral-sh/ruff/pull/8186)) -- Fix misspelled TOML headers in the tutorial ([#8209](https://github.com/astral-sh/ruff/pull/8209)) - -## 0.1.2 - -This release includes the Beta version of the Ruff formatter — an extremely fast, Black-compatible Python formatter. -Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blog/the-ruff-formatter) and [read the docs](https://docs.astral.sh/ruff/formatter/). - -### Preview features - -- \[`pylint`\] Implement `non-ascii-module-import` (`C2403`) ([#8056](https://github.com/astral-sh/ruff/pull/8056)) -- \[`pylint`\] implement `non-ascii-name` (`C2401`) ([#8038](https://github.com/astral-sh/ruff/pull/8038)) -- \[`pylint`\] Implement unnecessary-lambda (W0108) ([#7953](https://github.com/astral-sh/ruff/pull/7953)) -- \[`refurb`\] Implement `read-whole-file` (`FURB101`) ([#7682](https://github.com/astral-sh/ruff/pull/7682)) -- Add fix for `E223`, `E224`, and `E242` ([#8143](https://github.com/astral-sh/ruff/pull/8143)) -- Add fix for `E225`, `E226`, `E227`, and `E228` ([#8136](https://github.com/astral-sh/ruff/pull/8136)) -- Add fix for `E252` ([#8142](https://github.com/astral-sh/ruff/pull/8142)) -- Add fix for `E261` ([#8114](https://github.com/astral-sh/ruff/pull/8114)) -- Add fix for `E273` and `E274` ([#8144](https://github.com/astral-sh/ruff/pull/8144)) -- Add fix for `E275` ([#8133](https://github.com/astral-sh/ruff/pull/8133)) -- Update `SIM401` to catch ternary operations ([#7415](https://github.com/astral-sh/ruff/pull/7415)) -- Update `E721` to allow `is` and `is` not for direct type comparisons ([#7905](https://github.com/astral-sh/ruff/pull/7905)) - -### Rule changes - -- Add `backports.strenum` to `deprecated-imports` ([#8113](https://github.com/astral-sh/ruff/pull/8113)) -- Update `SIM112` to ignore `https_proxy`, `http_proxy`, and `no_proxy` ([#8140](https://github.com/astral-sh/ruff/pull/8140)) -- Update fix for `literal-membership` (`PLR6201`) to be unsafe ([#8097](https://github.com/astral-sh/ruff/pull/8097)) -- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108)) - -### Formatter - -- Change `line-ending` default to `auto` ([#8057](https://github.com/astral-sh/ruff/pull/8057)) -- Respect parenthesized generators in `has_own_parentheses` ([#8100](https://github.com/astral-sh/ruff/pull/8100)) -- Add caching to formatter ([#8089](https://github.com/astral-sh/ruff/pull/8089)) -- Remove `--line-length` option from `format` command ([#8131](https://github.com/astral-sh/ruff/pull/8131)) -- Add formatter to `line-length` documentation ([#8150](https://github.com/astral-sh/ruff/pull/8150)) -- Warn about incompatible formatter options ([#8088](https://github.com/astral-sh/ruff/pull/8088)) -- Fix range of unparenthesized tuple subject in match statement ([#8101](https://github.com/astral-sh/ruff/pull/8101)) -- Remove experimental formatter warning ([#8148](https://github.com/astral-sh/ruff/pull/8148)) -- Don't move type param opening parenthesis comment ([#8163](https://github.com/astral-sh/ruff/pull/8163)) -- Update versions in format benchmark script ([#8110](https://github.com/astral-sh/ruff/pull/8110)) -- Avoid loading files for cached format results ([#8134](https://github.com/astral-sh/ruff/pull/8134)) - -### CLI - -- Show the `ruff format` command in help menus ([#8167](https://github.com/astral-sh/ruff/pull/8167)) -- Add `ruff version` command with long version display ([#8034](https://github.com/astral-sh/ruff/pull/8034)) - -### Configuration - -- New `pycodestyle.max-line-length` option ([#8039](https://github.com/astral-sh/ruff/pull/8039)) - -### Bug fixes - -- Detect `sys.version_info` slices in `outdated-version-block` ([#8112](https://github.com/astral-sh/ruff/pull/8112)) -- Avoid if-else simplification for `TYPE_CHECKING` blocks ([#8072](https://github.com/astral-sh/ruff/pull/8072)) -- Avoid false-positive print separator diagnostic with starred argument ([#8079](https://github.com/astral-sh/ruff/pull/8079)) - -### Documentation - -- Fix message for `too-many-arguments` lint ([#8092](https://github.com/astral-sh/ruff/pull/8092)) -- Fix `extend-unsafe-fixes` and `extend-safe-fixes` example ([#8139](https://github.com/astral-sh/ruff/pull/8139)) -- Add links to `flake8-import-conventions` options ([#8115](https://github.com/astral-sh/ruff/pull/8115)) -- Rework the documentation to incorporate the Ruff formatter ([#7732](https://github.com/astral-sh/ruff/pull/7732)) -- Fix `Options` JSON schema description ([#8081](https://github.com/astral-sh/ruff/pull/8081)) -- Fix typo (`pytext` -> `pytest`) ([#8117](https://github.com/astral-sh/ruff/pull/8117)) -- Improve `magic-value-comparison` example in docs ([#8111](https://github.com/astral-sh/ruff/pull/8111)) - -## 0.1.1 - -### Rule changes - -- Add unsafe fix for `escape-sequence-in-docstring` (`D301`) ([#7970](https://github.com/astral-sh/ruff/pull/7970)) - -### Configuration - -- Respect `#(deprecated)` attribute in configuration options ([#8035](https://github.com/astral-sh/ruff/pull/8035)) -- Add `[format|lint].exclude` options ([#8000](https://github.com/astral-sh/ruff/pull/8000)) -- Respect `tab-size` setting in formatter ([#8006](https://github.com/astral-sh/ruff/pull/8006)) -- Add `lint.preview` ([#8002](https://github.com/astral-sh/ruff/pull/8002)) - -### Preview features - -- \[`pylint`\] Implement `literal-membership` (`PLR6201`) ([#7973](https://github.com/astral-sh/ruff/pull/7973)) -- \[`pylint`\] Implement `too-many-boolean-expressions` (`PLR0916`) ([#7975](https://github.com/astral-sh/ruff/pull/7975)) -- \[`pylint`\] Implement `misplaced-bare-raise` (`E0704`) ([#7961](https://github.com/astral-sh/ruff/pull/7961)) -- \[`pylint`\] Implement `global-at-module-level` (`W0604`) ([#8058](https://github.com/astral-sh/ruff/pull/8058)) -- \[`pylint`\] Implement `unspecified-encoding` (`PLW1514`) ([#7939](https://github.com/astral-sh/ruff/pull/7939)) -- Add fix for `triple-single-quotes` (`D300`) ([#7967](https://github.com/astral-sh/ruff/pull/7967)) - -### Formatter - -- New code style badge for `ruff format` ([#7878](https://github.com/astral-sh/ruff/pull/7878)) -- Fix comments outside expression parentheses ([#7873](https://github.com/astral-sh/ruff/pull/7873)) -- Add `--target-version` to `ruff format` ([#8055](https://github.com/astral-sh/ruff/pull/8055)) -- Skip over parentheses when detecting `in` keyword ([#8054](https://github.com/astral-sh/ruff/pull/8054)) -- Add `--diff` option to `ruff format` ([#7937](https://github.com/astral-sh/ruff/pull/7937)) -- Insert newline after nested function or class statements ([#7946](https://github.com/astral-sh/ruff/pull/7946)) -- Use `pass` over ellipsis in non-function/class contexts ([#8049](https://github.com/astral-sh/ruff/pull/8049)) - -### Bug fixes - -- Lazily evaluate all PEP 695 type alias values ([#8033](https://github.com/astral-sh/ruff/pull/8033)) -- Avoid failed assertion when showing fixes from stdin ([#8029](https://github.com/astral-sh/ruff/pull/8029)) -- Avoid flagging HTTP and HTTPS literals in urllib-open ([#8046](https://github.com/astral-sh/ruff/pull/8046)) -- Avoid flagging `bad-dunder-method-name` for `_` ([#8015](https://github.com/astral-sh/ruff/pull/8015)) -- Remove Python 2-only methods from `URLOpen` audit ([#8047](https://github.com/astral-sh/ruff/pull/8047)) -- Use set bracket replacement for `iteration-over-set` to preserve whitespace and comments ([#8001](https://github.com/astral-sh/ruff/pull/8001)) - -### Documentation - -- Update tutorial to match revised Ruff defaults ([#8066](https://github.com/astral-sh/ruff/pull/8066)) -- Update rule `B005` docs ([#8028](https://github.com/astral-sh/ruff/pull/8028)) -- Update GitHub actions example in docs to use `--output-format` ([#8014](https://github.com/astral-sh/ruff/pull/8014)) -- Document `lint.preview` and `format.preview` ([#8032](https://github.com/astral-sh/ruff/pull/8032)) -- Clarify that new rules should be added to `RuleGroup::Preview`. ([#7989](https://github.com/astral-sh/ruff/pull/7989)) - -## 0.1.0 - -This is the first release which uses the `CHANGELOG` file. See [GitHub Releases](https://github.com/astral-sh/ruff/releases) for prior changelog entries. - -Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/). - -### Breaking changes - -- Unsafe fixes are no longer displayed or applied without opt-in ([#7769](https://github.com/astral-sh/ruff/pull/7769)) -- Drop formatting specific rules from the default set ([#7900](https://github.com/astral-sh/ruff/pull/7900)) -- The deprecated `format` setting has been removed ([#7984](https://github.com/astral-sh/ruff/pull/7984)) - - The `format` setting cannot be used to configure the output format, use `output-format` instead - - The `RUFF_FORMAT` environment variable is ignored, use `RUFF_OUTPUT_FORMAT` instead - - The `--format` option has been removed from `ruff check`, use `--output-format` instead - -### Rule changes - -- Extend `reimplemented-starmap` (`FURB140`) to catch calls with a single and starred argument ([#7768](https://github.com/astral-sh/ruff/pull/7768)) -- Improve cases covered by `RUF015` ([#7848](https://github.com/astral-sh/ruff/pull/7848)) -- Update `SIM15` to allow `open` followed by `close` ([#7916](https://github.com/astral-sh/ruff/pull/7916)) -- Respect `msgspec.Struct` default-copy semantics in `RUF012` ([#7786](https://github.com/astral-sh/ruff/pull/7786)) -- Add `sqlalchemy` methods to \`flake8-boolean-trap\`\` exclusion list ([#7874](https://github.com/astral-sh/ruff/pull/7874)) -- Add fix for `PLR1714` ([#7910](https://github.com/astral-sh/ruff/pull/7910)) -- Add fix for `PIE804` ([#7884](https://github.com/astral-sh/ruff/pull/7884)) -- Add fix for `PLC0208` ([#7887](https://github.com/astral-sh/ruff/pull/7887)) -- Add fix for `PYI055` ([#7886](https://github.com/astral-sh/ruff/pull/7886)) -- Update `non-pep695-type-alias` to require `--unsafe-fixes` outside of stub files ([#7836](https://github.com/astral-sh/ruff/pull/7836)) -- Improve fix message for `UP018` ([#7913](https://github.com/astral-sh/ruff/pull/7913)) -- Update `PLW3201` to support `Enum` [sunder names](https://docs.python.org/3/library/enum.html#supported-sunder-names) ([#7987](https://github.com/astral-sh/ruff/pull/7987)) - -### Preview features - -- Only show warnings for empty preview selectors when enabling rules ([#7842](https://github.com/astral-sh/ruff/pull/7842)) -- Add `unnecessary-key-check` to simplify `key in dct and dct[key]` to `dct.get(key)` ([#7895](https://github.com/astral-sh/ruff/pull/7895)) -- Add `assignment-in-assert` to prevent walrus expressions in assert statements ([#7856](https://github.com/astral-sh/ruff/pull/7856)) -- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815)) -- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811)) - -*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).* - -### Configuration - -- Add `unsafe-fixes` setting ([#7769](https://github.com/astral-sh/ruff/pull/7769)) -- Add `extend-safe-fixes` and `extend-unsafe-fixes` for promoting and demoting fixes ([#7841](https://github.com/astral-sh/ruff/pull/7841)) - -### CLI - -- Added `--unsafe-fixes` option for opt-in to display and apply unsafe fixes ([#7769](https://github.com/astral-sh/ruff/pull/7769)) -- Fix use of deprecated `--format` option in warning ([#7837](https://github.com/astral-sh/ruff/pull/7837)) -- Show changed files when running under `--check` ([#7788](https://github.com/astral-sh/ruff/pull/7788)) -- Write summary messages to stderr when fixing via stdin instead of omitting them ([#7838](https://github.com/astral-sh/ruff/pull/7838)) -- Update fix summary message in `check --diff` to include unsafe fix hints ([#7790](https://github.com/astral-sh/ruff/pull/7790)) -- Add notebook `cell` field to JSON output format ([#7664](https://github.com/astral-sh/ruff/pull/7664)) -- Rename applicability levels to `Safe`, `Unsafe`, and `Display` ([#7843](https://github.com/astral-sh/ruff/pull/7843)) - -### Bug fixes - -- Fix bug where f-strings were allowed in match pattern literal ([#7857](https://github.com/astral-sh/ruff/pull/7857)) -- Fix `SIM110` with a yield in the condition ([#7801](https://github.com/astral-sh/ruff/pull/7801)) -- Preserve trailing comments in `C414` fixes ([#7775](https://github.com/astral-sh/ruff/pull/7775)) -- Check sequence type before triggering `unnecessary-enumerate` `len` suggestion ([#7781](https://github.com/astral-sh/ruff/pull/7781)) -- Use correct start location for class/function clause header ([#7802](https://github.com/astral-sh/ruff/pull/7802)) -- Fix incorrect fixes for `SIM101` ([#7798](https://github.com/astral-sh/ruff/pull/7798)) -- Format comment before parameter default correctly ([#7870](https://github.com/astral-sh/ruff/pull/7870)) -- Fix `E251` false positive inside f-strings ([#7894](https://github.com/astral-sh/ruff/pull/7894)) -- Allow bindings to be created and referenced within annotations ([#7885](https://github.com/astral-sh/ruff/pull/7885)) -- Show per-cell diffs when analyzing notebooks over `stdin` ([#7789](https://github.com/astral-sh/ruff/pull/7789)) -- Avoid curly brace escape in f-string format spec ([#7780](https://github.com/astral-sh/ruff/pull/7780)) -- Fix lexing single-quoted f-string with multi-line format spec ([#7787](https://github.com/astral-sh/ruff/pull/7787)) -- Consider nursery rules to be in-preview for `ruff rule` ([#7812](https://github.com/astral-sh/ruff/pull/7812)) -- Report precise location for invalid conversion flag ([#7809](https://github.com/astral-sh/ruff/pull/7809)) -- Visit pattern match guard as a boolean test ([#7911](https://github.com/astral-sh/ruff/pull/7911)) -- Respect `--unfixable` in `ISC` rules ([#7917](https://github.com/astral-sh/ruff/pull/7917)) -- Fix edge case with `PIE804` ([#7922](https://github.com/astral-sh/ruff/pull/7922)) -- Show custom message in `PTH118` for `Path.joinpath` with starred arguments ([#7852](https://github.com/astral-sh/ruff/pull/7852)) -- Fix false negative in `outdated-version-block` when using greater than comparisons ([#7920](https://github.com/astral-sh/ruff/pull/7920)) -- Avoid converting f-strings within Django `gettext` calls ([#7898](https://github.com/astral-sh/ruff/pull/7898)) -- Fix false positive in `PLR6301` ([#7933](https://github.com/astral-sh/ruff/pull/7933)) -- Treat type aliases as typing-only expressions e.g. resolves false positive in `TCH004` ([#7968](https://github.com/astral-sh/ruff/pull/7968)) -- Resolve `cache-dir` relative to project root ([#7962](https://github.com/astral-sh/ruff/pull/7962)) -- Respect subscripted base classes in type-checking rules e.g. resolves false positive in `TCH003` ([#7954](https://github.com/astral-sh/ruff/pull/7954)) -- Fix JSON schema limit for `line-length` ([#7883](https://github.com/astral-sh/ruff/pull/7883)) -- Fix commented-out `coalesce` keyword ([#7876](https://github.com/astral-sh/ruff/pull/7876)) - -### Documentation +See [changelogs/0.1.x](./changelogs/0.1.x.md) -- Document `reimplemented-starmap` performance effects ([#7846](https://github.com/astral-sh/ruff/pull/7846)) -- Default to following the system dark/light mode ([#7888](https://github.com/astral-sh/ruff/pull/7888)) -- Add documentation for fixes ([#7901](https://github.com/astral-sh/ruff/pull/7901)) -- Fix typo in docs of `PLR6301` ([#7831](https://github.com/astral-sh/ruff/pull/7831)) -- Update `UP038` docs to note that it results in slower code ([#7872](https://github.com/astral-sh/ruff/pull/7872)) -- crlf -> cr-lf ([#7766](https://github.com/astral-sh/ruff/pull/7766)) -- Add an example of an unsafe fix ([#7924](https://github.com/astral-sh/ruff/pull/7924)) -- Fix documented examples for `unnecessary-subscript-reversal` ([#7774](https://github.com/astral-sh/ruff/pull/7774)) -- Correct error in tuple example in ruff formatter docs ([#7822](https://github.com/astral-sh/ruff/pull/7822)) -- Add versioning policy to documentation ([#7923](https://github.com/astral-sh/ruff/pull/7923)) -- Fix invalid code in `FURB177` example ([#7832](https://github.com/astral-sh/ruff/pull/7832)) - -### Formatter - -- Less scary `ruff format` message ([#7867](https://github.com/astral-sh/ruff/pull/7867)) -- Remove spaces from import statements ([#7859](https://github.com/astral-sh/ruff/pull/7859)) -- Formatter quoting for f-strings with triple quotes ([#7826](https://github.com/astral-sh/ruff/pull/7826)) -- Update `ruff_python_formatter` generate.py comment ([#7850](https://github.com/astral-sh/ruff/pull/7850)) -- Document one-call chaining deviation ([#7767](https://github.com/astral-sh/ruff/pull/7767)) -- Allow f-string modifications in line-shrinking cases ([#7818](https://github.com/astral-sh/ruff/pull/7818)) -- Add trailing comment deviation to README ([#7827](https://github.com/astral-sh/ruff/pull/7827)) -- Add trailing zero between dot and exponential ([#7956](https://github.com/astral-sh/ruff/pull/7956)) -- Force parentheses for power operations in unary expressions ([#7955](https://github.com/astral-sh/ruff/pull/7955)) - -### Playground - -- Fix playground `Quick Fix` action ([#7824](https://github.com/astral-sh/ruff/pull/7824)) +[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument +[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation +[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp +[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional +[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union +[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for +[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true +[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 350f7422a5dea..5c8537b757c2e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -71,8 +71,7 @@ representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -. +reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15f961e7d841a..ca0605ab90ffe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,11 @@ Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff. +> [!NOTE] +> +> This guide is for Ruff. If you're looking to contribute to ty, please see [the ty contributing +> guide](https://github.com/astral-sh/ruff/blob/main/crates/ty/CONTRIBUTING.md). + ## The Basics Ruff welcomes contributions in the form of pull requests. @@ -310,6 +315,17 @@ even patch releases may contain [non-backwards-compatible changes](https://semve - Often labels will be missing from pull requests they will need to be manually organized into the proper section - Changes should be edited to be user-facing descriptions, avoiding internal details + Additionally, for minor releases: + + - Move the existing contents of `CHANGELOG.md` to `changelogs/0.MINOR.x.md`, + where `MINOR` is the previous minor release (e.g. `11` when preparing + the 0.12.0 release) + - Reverse the entries to put the oldest version first (`0.MINOR.0` instead + of `0.MINOR.LATEST` as in the main changelog) + - Use the + [`reverse-changelog.py`](https://github.com/astral-sh/uv/blob/main/scripts/reverse-changelog.py) + script from the uv repo to do this automatically + 1. Highlight any breaking changes in `BREAKING_CHANGES.md` 1. Run `cargo check`. This should update the lock file with new versions. @@ -366,6 +382,15 @@ uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ru See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/python/ruff-ecosystem) for more details. +## Upgrading Rust + +1. Change the `channel` in `./rust-toolchain.toml` to the new Rust version (``) +1. Change the `rust-version` in the `./Cargo.toml` to ` - 2` (e.g. 1.84 if the latest is 1.86) +1. Run `cargo clippy --fix --allow-dirty --allow-staged` to fix new clippy warnings +1. Create and merge the PR +1. Bump the Rust version in Ruff's conda forge recipe. See [this PR](https://github.com/conda-forge/ruff-feedstock/pull/266) for an example. +1. Enjoy the new Rust version! + ## Benchmarking and Profiling We have several ways of benchmarking and profiling Ruff: @@ -397,7 +422,7 @@ cargo install hyperfine To benchmark the release build: ```shell -cargo build --release && hyperfine --warmup 10 \ +cargo build --release --bin ruff && hyperfine --warmup 10 \ "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e" \ "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ -e" @@ -596,8 +621,7 @@ Then convert the recorded profile perf script -F +pid > /tmp/test.perf ``` -You can now view the converted file with [firefox profiler](https://profiler.firefox.com/), with a -more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling) +You can now view the converted file with [firefox profiler](https://profiler.firefox.com/). To learn more about Firefox profiler, read the [Firefox profiler profiling-guide](https://profiler.firefox.com/docs/#/./guide-perf-profiling). An alternative is to convert the perf data to `flamegraph.svg` using [flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`): diff --git a/Cargo.lock b/Cargo.lock index 5016ae2e2b487..b581ddcf50f8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -51,14 +51,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ "anstyle", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-lossy" @@ -112,7 +112,7 @@ dependencies = [ "anstyle", "anstyle-lossy", "html-escape", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -132,6 +132,21 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "argfile" version = "0.2.1" @@ -150,9 +165,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_fs" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" +checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" dependencies = [ "anstyle", "doc-comment", @@ -163,6 +178,36 @@ dependencies = [ "tempfile", ] +[[package]] +name = "attribute-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0053e96dd3bec5b4879c23a138d6ef26f2cb936c9cdc96274ac2b9ed44b5bb54" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463b53ad0fd5b460af4b1915fe045ff4d946d025fb6c4dc3337752eaa980f71b" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -184,6 +229,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -192,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -207,12 +272,9 @@ dependencies = [ [[package]] name = "boxcar" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6740c6e2fc6360fa57c35214c7493826aee95993926092606f27c983b40837be" -dependencies = [ - "loom", -] +checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa" [[package]] name = "bstr" @@ -248,9 +310,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] @@ -272,9 +334,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.16" +version = "1.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" dependencies = [ "jobserver", "libc", @@ -295,9 +357,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -334,9 +396,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -344,9 +406,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -357,9 +419,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.46" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98" +checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1" dependencies = [ "clap", ] @@ -387,14 +449,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -405,11 +467,11 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clearscreen" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4" +checksum = "85a8ab73a1c02b0c15597b22e09c7dc36e63b2f601f9d1e83ac0c3decd38b1ae" dependencies = [ - "nix", + "nix 0.29.0", "terminfo", "thiserror 2.0.12", "which", @@ -418,22 +480,27 @@ dependencies = [ [[package]] name = "codspeed" -version = "2.9.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e744216bfa9add3b1f2505587cbbb837923232ed10963609f4a6e3cbd99c3e" +checksum = "922018102595f6668cdd09c03f4bff2d951ce2318c6dca4fe11bdcb24b65b2bf" dependencies = [ + "anyhow", + "bincode 1.3.3", "colored 2.2.0", + "glob", "libc", + "nix 0.29.0", "serde", "serde_json", + "statrs", "uuid", ] [[package]] name = "codspeed-criterion-compat" -version = "2.9.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5926ca63222a35b9a2299adcaafecf596efe20a9a2048e4a81cb2fc3463b4a8" +checksum = "24d8ad82d2383cb74995f58993cbdd2914aed57b2f91f46580310dd81dc3d05a" dependencies = [ "codspeed", "codspeed-criterion-compat-walltime", @@ -442,9 +509,9 @@ dependencies = [ [[package]] name = "codspeed-criterion-compat-walltime" -version = "2.9.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbae4da05076cbc673e242400ac8f4353bdb686e48020edc6e36a5c36ae0878e" +checksum = "61badaa6c452d192a29f8387147888f0ab358553597c3fe9bf8a162ef7c2fa64" dependencies = [ "anes", "cast", @@ -465,6 +532,52 @@ dependencies = [ "walkdir", ] +[[package]] +name = "codspeed-divan-compat" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acf1d6fe367c2ff5ff136ca723f678490c3691d59d7f2b83d5e53b7b25ac91e" +dependencies = [ + "codspeed", + "codspeed-divan-compat-macros", + "codspeed-divan-compat-walltime", +] + +[[package]] +name = "codspeed-divan-compat-macros" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfa2013d7bee54a497d0e1410751d5de690fd67a3e9eb728ca049b6a3d16d0b" +dependencies = [ + "divan-macros", + "itertools 0.14.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "codspeed-divan-compat-walltime" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e513100fb0e7ba02fb3824546ecd2abfb8f334262f0972225b463aad07f99ff0" +dependencies = [ + "cfg-if", + "clap", + "codspeed", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + [[package]] name = "colorchoice" version = "1.0.3" @@ -478,7 +591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -487,21 +600,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", + "windows-sys 0.52.0", ] [[package]] @@ -519,6 +618,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "console" version = "0.15.11" @@ -528,10 +633,22 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.1", + "windows-sys 0.60.2", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -563,11 +680,6 @@ name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" -dependencies = [ - "dashmap 5.5.3", - "once_cell", - "rustc-hash 1.1.0", -] [[package]] name = "cpufeatures" @@ -589,23 +701,20 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools 0.10.5", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -636,9 +745,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -695,19 +804,19 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.6" +version = "3.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" dependencies = [ - "nix", + "nix 0.30.1", "windows-sys 0.59.0", ] [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -715,36 +824,37 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "dashmap" -version = "5.5.3" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", + "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -752,17 +862,14 @@ dependencies = [ ] [[package]] -name = "dashmap" -version = "6.1.0" +name = "derive-where" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b" dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -805,28 +912,28 @@ dependencies = [ "glob", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -837,7 +944,18 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", +] + +[[package]] +name = "divan-macros" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -876,35 +994,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - [[package]] name = "env_home" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -913,12 +1008,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -929,9 +1024,9 @@ checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" [[package]] name = "escargot" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a3ac187a16b5382fef8c69fd1bad123c67b7cf3932240a2d43dcdd32cded88" +checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211" dependencies = [ "log", "once_cell", @@ -979,9 +1074,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1026,19 +1121,6 @@ dependencies = [ "libc", ] -[[package]] -name = "generator" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" -dependencies = [ - "cfg-if", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1049,6 +1131,29 @@ dependencies = [ "version_check", ] +[[package]] +name = "get-size-derive2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aac2af9f9a6a50e31b1e541d05b7925add83d3982c2793193fe9d4ee584323c" +dependencies = [ + "attribute-derive", + "quote", + "syn", +] + +[[package]] +name = "get-size2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a0312efd19e1c45922dfcc2d6806d3ffc4bca261f89f31fcc4f63f438d885" +dependencies = [ + "compact_str", + "get-size-derive2", + "hashbrown 0.15.4", + "smallvec", +] + [[package]] name = "getopts" version = "0.2.21" @@ -1060,9 +1165,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -1071,9 +1176,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", @@ -1108,16 +1213,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "ignore", "walkdir", ] [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -1131,9 +1236,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -1146,7 +1251,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] @@ -1163,9 +1268,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "home" @@ -1187,16 +1292,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -1210,21 +1316,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1233,31 +1340,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1265,67 +1352,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1345,9 +1419,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1375,7 +1449,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] @@ -1390,25 +1464,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "serde", ] [[package]] name = "indicatif" -version = "0.17.11" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ - "console", - "number_prefix", + "console 0.16.0", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.1", + "unit-prefix", "vt100", "web-time", ] @@ -1425,7 +1499,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -1441,17 +1515,15 @@ dependencies = [ [[package]] name = "insta" -version = "1.42.2" +version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ - "console", + "console 0.15.11", "globset", - "linked-hash-map", "once_cell", "pest", "pest_derive", - "pin-project", "regex", "ron", "serde", @@ -1470,6 +1542,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -1488,7 +1575,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1497,9 +1584,9 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi 0.5.1", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1553,9 +1640,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.4" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1568,13 +1655,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.4" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -1594,18 +1681,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] [[package]] name = "jod-thread" -version = "0.1.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" +checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" [[package]] name = "js-sys" @@ -1619,9 +1707,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -1645,15 +1733,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libcst" -version = "1.7.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9e315e3f679e61b9095ffd5e509de78b8a4ea3bba9d772f6fb243209f808d4" +checksum = "ae28ddc5b90c3e3146a21d051ca095cbc8d932ad8714cf65ddf71a9abb35684c" dependencies = [ "annotate-snippets", "libcst_derive", @@ -1661,24 +1749,24 @@ dependencies = [ "paste", "peg", "regex", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "libcst_derive" -version = "1.7.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa96ed35d0dccc67cf7ba49350cb86de3dcb1d072a7ab28f99117f19d874953" +checksum = "dc2de5c2f62bcf8a4f7290b1854388b262c4b68f1db1a3ee3ef6d4c1319b00a3" dependencies = [ "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "libmimalloc-sys" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" dependencies = [ "cc", "libc", @@ -1690,7 +1778,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -1707,35 +1795,23 @@ dependencies = [ "threadpool", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1747,19 +1823,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lsp-server" version = "0.7.8" @@ -1785,6 +1848,38 @@ dependencies = [ "url", ] +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1808,15 +1903,24 @@ checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "mimalloc" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" dependencies = [ "libmimalloc-sys", ] @@ -1839,9 +1943,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -1879,7 +1983,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -1903,12 +2019,11 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "notify" -version = "8.0.0" +version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" +checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97" dependencies = [ - "bitflags 2.9.0", - "filetime", + "bitflags 2.9.1", "fsevent-sys", "inotify", "kqueue", @@ -1917,7 +2032,7 @@ dependencies = [ "mio", "notify-types", "walkdir", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1936,15 +2051,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1964,17 +2070,11 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" @@ -1990,11 +2090,12 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordermap" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d31b8b7a99f71bdff4235faf9ce9eada0ad3562c8fbeb7d607d9f41a6ec569d" +checksum = "6d6bff06e4a5dc6416bead102d3e63c480dd852ffbb278bf8cfeb4966b329609" dependencies = [ "indexmap", + "serde", ] [[package]] @@ -2022,6 +2123,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -2116,7 +2227,7 @@ checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" dependencies = [ "once_cell", "serde", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "unscanny", "version-ranges", ] @@ -2133,11 +2244,11 @@ dependencies = [ "once_cell", "pep440_rs", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "smallvec", "thiserror 1.0.69", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "url", "urlencoding", "version-ranges", @@ -2151,9 +2262,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", "thiserror 2.0.12", @@ -2162,9 +2273,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -2172,22 +2283,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", @@ -2232,26 +2343,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2279,6 +2370,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2325,26 +2425,46 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "pyproject-toml" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643af57c3f36ba90a8b53e972727d8092f7408a9ebfbaf4c3d2c17b07c58d835" +checksum = "7b0f6160dc48298b9260d9b958ad1d7f96f6cd0b9df200b22329204e09334663" dependencies = [ "indexmap", "pep440_rs", "pep508_rs", "serde", - "thiserror 1.0.69", + "thiserror 2.0.12", "toml", ] @@ -2365,9 +2485,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] @@ -2383,13 +2503,13 @@ dependencies = [ [[package]] name = "quickcheck_macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -2401,6 +2521,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "r-efi" version = "5.2.0" @@ -2420,13 +2562,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy", ] [[package]] @@ -2449,271 +2590,62 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.2", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "red_knot" -version = "0.0.0" -dependencies = [ - "anyhow", - "argfile", - "clap", - "colored 3.0.0", - "countme", - "crossbeam", - "ctrlc", - "filetime", - "insta", - "insta-cmd", - "jiff", - "rayon", - "red_knot_project", - "red_knot_python_semantic", - "red_knot_server", - "regex", - "ruff_db", - "ruff_python_ast", - "ruff_python_trivia", - "salsa", - "tempfile", - "toml", - "tracing", - "tracing-flame", - "tracing-subscriber", - "tracing-tree", - "wild", -] - -[[package]] -name = "red_knot_ide" -version = "0.0.0" -dependencies = [ - "insta", - "red_knot_python_semantic", - "red_knot_vendored", - "ruff_db", - "ruff_python_ast", - "ruff_python_parser", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "smallvec", - "tracing", -] - -[[package]] -name = "red_knot_project" -version = "0.0.0" -dependencies = [ - "anyhow", - "crossbeam", - "glob", - "insta", - "notify", - "pep440_rs", - "rayon", - "red_knot_ide", - "red_knot_python_semantic", - "red_knot_vendored", - "ruff_cache", - "ruff_db", - "ruff_macros", - "ruff_python_ast", - "ruff_python_formatter", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "schemars", - "serde", - "thiserror 2.0.12", - "toml", - "tracing", -] - -[[package]] -name = "red_knot_python_semantic" -version = "0.0.0" -dependencies = [ - "anyhow", - "bitflags 2.9.0", - "camino", - "compact_str 0.9.0", - "countme", - "dir-test", - "drop_bomb", - "hashbrown 0.15.2", - "indexmap", - "insta", - "itertools 0.14.0", - "memchr", - "ordermap", - "quickcheck", - "quickcheck_macros", - "red_knot_test", - "red_knot_vendored", - "ruff_db", - "ruff_index", - "ruff_macros", - "ruff_python_ast", - "ruff_python_literal", - "ruff_python_parser", - "ruff_python_stdlib", - "ruff_python_trivia", - "ruff_source_file", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "schemars", - "serde", - "smallvec", - "static_assertions", - "strum", - "strum_macros", - "tempfile", - "test-case", - "thiserror 2.0.12", - "tracing", -] - -[[package]] -name = "red_knot_server" -version = "0.0.0" -dependencies = [ - "anyhow", - "crossbeam", - "jod-thread", - "libc", - "lsp-server", - "lsp-types", - "red_knot_ide", - "red_knot_project", - "red_knot_python_semantic", - "ruff_db", - "ruff_notebook", - "ruff_source_file", - "ruff_text_size", - "rustc-hash 2.1.1", - "serde", - "serde_json", - "shellexpand", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "red_knot_test" -version = "0.0.0" -dependencies = [ - "anyhow", - "camino", - "colored 3.0.0", - "insta", - "memchr", - "red_knot_python_semantic", - "red_knot_vendored", - "regex", - "ruff_db", - "ruff_index", - "ruff_notebook", - "ruff_python_ast", - "ruff_python_trivia", - "ruff_source_file", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "serde", - "smallvec", - "tempfile", - "thiserror 2.0.12", - "toml", +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] -name = "red_knot_vendored" -version = "0.0.0" +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "path-slash", - "ruff_db", - "walkdir", - "zip", + "getrandom 0.3.3", ] [[package]] -name = "red_knot_wasm" -version = "0.0.0" +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ - "console_error_panic_hook", - "console_log", - "getrandom 0.3.2", - "js-sys", - "log", - "red_knot_ide", - "red_knot_project", - "red_knot_python_semantic", - "ruff_db", - "ruff_notebook", - "ruff_python_formatter", - "ruff_source_file", - "ruff_text_size", - "serde-wasm-bindgen", - "wasm-bindgen", - "wasm-bindgen-test", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", ] [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -2748,6 +2680,12 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2773,18 +2711,19 @@ dependencies = [ [[package]] name = "ruff" -version = "0.11.6" +version = "0.12.3" dependencies = [ "anyhow", "argfile", "assert_fs", - "bincode", - "bitflags 2.9.0", + "bincode 2.0.1", + "bitflags 2.9.1", "cachedir", "clap", "clap_complete_command", "clearscreen", "colored 3.0.0", + "dunce", "filetime", "globwalk", "ignore", @@ -2807,6 +2746,7 @@ dependencies = [ "ruff_linter", "ruff_macros", "ruff_notebook", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_formatter", "ruff_python_parser", @@ -2814,7 +2754,7 @@ dependencies = [ "ruff_source_file", "ruff_text_size", "ruff_workspace", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "shellexpand", @@ -2841,26 +2781,31 @@ dependencies = [ "snapbox", "toml", "tryfn", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] name = "ruff_benchmark" version = "0.0.0" dependencies = [ + "anyhow", "codspeed-criterion-compat", + "codspeed-divan-compat", "criterion", "mimalloc", "rayon", - "red_knot_project", "ruff_db", "ruff_linter", "ruff_python_ast", "ruff_python_formatter", "ruff_python_parser", "ruff_python_trivia", - "rustc-hash 2.1.1", + "rustc-hash", + "serde", + "serde_json", "tikv-jemallocator", + "tracing", + "ty_project", ] [[package]] @@ -2880,12 +2825,15 @@ dependencies = [ name = "ruff_db" version = "0.0.0" dependencies = [ + "anstyle", + "arc-swap", "camino", "countme", - "dashmap 6.1.0", + "dashmap", "dunce", "etcetera", "filetime", + "get-size2", "glob", "ignore", "insta", @@ -2893,13 +2841,14 @@ dependencies = [ "path-slash", "ruff_annotate_snippets", "ruff_cache", + "ruff_diagnostics", "ruff_notebook", "ruff_python_ast", "ruff_python_parser", "ruff_python_trivia", "ruff_source_file", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "salsa", "schemars", "serde", @@ -2907,7 +2856,7 @@ dependencies = [ "thiserror 2.0.12", "tracing", "tracing-subscriber", - "tracing-tree", + "ty_static", "web-time", "zip", ] @@ -2924,15 +2873,15 @@ dependencies = [ "indoc", "itertools 0.14.0", "libcst", + "markdown", "pretty_assertions", "rayon", - "red_knot_project", "regex", "ruff", - "ruff_diagnostics", "ruff_formatter", "ruff_linter", "ruff_notebook", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_codegen", "ruff_python_formatter", @@ -2949,15 +2898,18 @@ dependencies = [ "tracing", "tracing-indicatif", "tracing-subscriber", + "ty", + "ty_project", + "ty_static", + "url", ] [[package]] name = "ruff_diagnostics" version = "0.0.0" dependencies = [ - "anyhow", + "get-size2", "is-macro", - "log", "ruff_text_size", "serde", ] @@ -2970,12 +2922,12 @@ dependencies = [ "ruff_cache", "ruff_macros", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "schemars", "serde", "static_assertions", "tracing", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -2984,7 +2936,6 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "red_knot_python_semantic", "ruff_cache", "ruff_db", "ruff_linter", @@ -2994,6 +2945,7 @@ dependencies = [ "salsa", "schemars", "serde", + "ty_python_semantic", "zip", ] @@ -3001,6 +2953,7 @@ dependencies = [ name = "ruff_index" version = "0.0.0" dependencies = [ + "get-size2", "ruff_macros", "salsa", "static_assertions", @@ -3008,16 +2961,17 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.11.6" +version = "0.12.3" dependencies = [ "aho-corasick", "anyhow", - "bitflags 2.9.0", + "bitflags 2.9.1", "clap", "colored 3.0.0", "fern", "glob", "globset", + "hashbrown 0.15.4", "imperative", "insta", "is-macro", @@ -3036,6 +2990,7 @@ dependencies = [ "regex", "ruff_annotate_snippets", "ruff_cache", + "ruff_db", "ruff_diagnostics", "ruff_macros", "ruff_notebook", @@ -3049,7 +3004,7 @@ dependencies = [ "ruff_python_trivia", "ruff_source_file", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "schemars", "serde", "serde_json", @@ -3057,12 +3012,13 @@ dependencies = [ "smallvec", "strum", "strum_macros", + "tempfile", "test-case", "thiserror 2.0.12", "toml", "typed-arena", "unicode-normalization", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "unicode_names2", "url", ] @@ -3071,11 +3027,12 @@ dependencies = [ name = "ruff_macros" version = "0.0.0" dependencies = [ + "heck", "itertools 0.14.0", "proc-macro2", "quote", "ruff_python_trivia", - "syn 2.0.100", + "syn", ] [[package]] @@ -3084,7 +3041,7 @@ version = "0.0.0" dependencies = [ "anyhow", "itertools 0.14.0", - "rand 0.9.0", + "rand 0.9.1", "ruff_diagnostics", "ruff_source_file", "ruff_text_size", @@ -3096,13 +3053,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "ruff_options_metadata" +version = "0.0.0" +dependencies = [ + "serde", +] + [[package]] name = "ruff_python_ast" version = "0.0.0" dependencies = [ "aho-corasick", - "bitflags 2.9.0", - "compact_str 0.9.0", + "bitflags 2.9.1", + "compact_str", + "get-size2", "is-macro", "itertools 0.14.0", "memchr", @@ -3111,10 +3076,11 @@ dependencies = [ "ruff_python_trivia", "ruff_source_file", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "salsa", "schemars", "serde", + "thiserror 2.0.12", ] [[package]] @@ -3160,7 +3126,7 @@ dependencies = [ "ruff_python_trivia", "ruff_source_file", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "salsa", "schemars", "serde", @@ -3187,7 +3153,7 @@ dependencies = [ name = "ruff_python_literal" version = "0.0.0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "itertools 0.14.0", "ruff_python_ast", "unic-ucd-category", @@ -3198,9 +3164,10 @@ name = "ruff_python_parser" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.9.0", + "bitflags 2.9.1", "bstr", - "compact_str 0.9.0", + "compact_str", + "get-size2", "insta", "memchr", "ruff_annotate_snippets", @@ -3208,7 +3175,7 @@ dependencies = [ "ruff_python_trivia", "ruff_source_file", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "static_assertions", @@ -3218,21 +3185,11 @@ dependencies = [ "walkdir", ] -[[package]] -name = "ruff_python_resolver" -version = "0.0.0" -dependencies = [ - "env_logger", - "insta", - "log", - "tempfile", -] - [[package]] name = "ruff_python_semantic" version = "0.0.0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "insta", "is-macro", "ruff_cache", @@ -3242,7 +3199,7 @@ dependencies = [ "ruff_python_parser", "ruff_python_stdlib", "ruff_text_size", - "rustc-hash 2.1.1", + "rustc-hash", "schemars", "serde", "smallvec", @@ -3253,7 +3210,7 @@ dependencies = [ name = "ruff_python_stdlib" version = "0.0.0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "unicode-ident", ] @@ -3290,6 +3247,7 @@ dependencies = [ "lsp-server", "lsp-types", "regex", + "ruff_db", "ruff_diagnostics", "ruff_formatter", "ruff_linter", @@ -3302,7 +3260,7 @@ dependencies = [ "ruff_source_file", "ruff_text_size", "ruff_workspace", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "shellexpand", @@ -3317,6 +3275,7 @@ dependencies = [ name = "ruff_source_file" version = "0.0.0" dependencies = [ + "get-size2", "memchr", "ruff_text_size", "serde", @@ -3326,6 +3285,7 @@ dependencies = [ name = "ruff_text_size" version = "0.0.0" dependencies = [ + "get-size2", "schemars", "serde", "serde_test", @@ -3334,11 +3294,11 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.11.6" +version = "0.12.3" dependencies = [ "console_error_panic_hook", "console_log", - "getrandom 0.3.2", + "getrandom 0.3.3", "js-sys", "log", "ruff_formatter", @@ -3354,6 +3314,7 @@ dependencies = [ "ruff_workspace", "serde", "serde-wasm-bindgen", + "uuid", "wasm-bindgen", "wasm-bindgen-test", ] @@ -3382,12 +3343,13 @@ dependencies = [ "ruff_graph", "ruff_linter", "ruff_macros", + "ruff_options_metadata", "ruff_python_ast", "ruff_python_formatter", "ruff_python_semantic", "ruff_python_stdlib", "ruff_source_file", - "rustc-hash 2.1.1", + "rustc-hash", "schemars", "serde", "shellexpand", @@ -3406,12 +3368,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3419,29 +3375,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rustix" -version = "0.38.44" +name = "rustc-stable-hash" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] +checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08" [[package]] name = "rustix" -version = "1.0.2" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.9.3", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3458,20 +3407,22 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" -version = "0.19.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26" +version = "0.23.0" +source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" dependencies = [ "boxcar", - "compact_str 0.8.1", + "compact_str", "crossbeam-queue", - "dashmap 6.1.0", - "hashbrown 0.15.2", + "crossbeam-utils", + "hashbrown 0.15.4", "hashlink", "indexmap", + "intrusive-collections", + "papaya", "parking_lot", "portable-atomic", "rayon", - "rustc-hash 2.1.1", + "rustc-hash", "salsa-macro-rules", "salsa-macros", "smallvec", @@ -3481,18 +3432,17 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.19.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26" +version = "0.23.0" +source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" [[package]] name = "salsa-macros" -version = "0.19.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26" +version = "0.23.0" +source = "git+https://github.com/salsa-rs/salsa?rev=fc00eba89e5dcaa5edba51c41aa5f309b5cb126b#fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" dependencies = [ - "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn", "synstructure", ] @@ -3526,15 +3476,9 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.100", + "syn", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -3547,6 +3491,16 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "seize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b8d813387d566f627f3ea1b914c068aac94c40ae27ec43f5f33bde65abefe7" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "serde" version = "1.0.219" @@ -3575,7 +3529,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -3586,7 +3540,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -3609,14 +3563,14 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -3632,9 +3586,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "serde", "serde_derive", @@ -3643,21 +3597,21 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3675,9 +3629,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "dirs", ] @@ -3702,9 +3656,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snapbox" @@ -3747,6 +3701,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "strip-ansi-escapes" version = "0.2.1" @@ -3781,25 +3745,14 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.100", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] name = "syn" -version = "2.0.100" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -3808,26 +3761,26 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.2", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.52.0", ] [[package]] @@ -3845,7 +3798,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.2", + "rustix", "windows-sys 0.59.0", ] @@ -3885,7 +3838,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -3896,7 +3849,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", "test-case-core", ] @@ -3932,7 +3885,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -3943,7 +3896,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -3987,9 +3940,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -4022,9 +3975,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -4034,26 +3987,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.41" @@ -4074,93 +4034,323 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] -name = "tracing-flame" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bae117ee14789185e129aaee5d93750abe67fdc5a9a62650452bfe4e122a3a9" +name = "tracing-flame" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bae117ee14789185e129aaee5d93750abe67fdc5a9a62650452bfe4e122a3a9" +dependencies = [ + "lazy_static", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-indicatif" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200" +dependencies = [ + "indicatif", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "chrono", + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tryfn" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe242ee9e646acec9ab73a5c540e8543ed1b107f0ce42be831e0775d423c396" +dependencies = [ + "ignore", + "libtest-mimic", + "snapbox", +] + +[[package]] +name = "ty" +version = "0.0.0" +dependencies = [ + "anyhow", + "argfile", + "clap", + "clap_complete_command", + "colored 3.0.0", + "crossbeam", + "ctrlc", + "dunce", + "filetime", + "indicatif", + "insta", + "insta-cmd", + "jiff", + "rayon", + "regex", + "ruff_db", + "ruff_python_ast", + "ruff_python_trivia", + "salsa", + "tempfile", + "toml", + "tracing", + "tracing-flame", + "tracing-subscriber", + "ty_project", + "ty_python_semantic", + "ty_server", + "ty_static", + "wild", +] + +[[package]] +name = "ty_ide" +version = "0.0.0" +dependencies = [ + "bitflags 2.9.1", + "insta", + "regex", + "ruff_db", + "ruff_python_ast", + "ruff_python_parser", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", + "salsa", + "smallvec", + "tracing", + "ty_python_semantic", + "ty_vendored", +] + +[[package]] +name = "ty_project" +version = "0.0.0" +dependencies = [ + "anyhow", + "camino", + "colored 3.0.0", + "crossbeam", + "get-size2", + "globset", + "insta", + "notify", + "ordermap", + "pep440_rs", + "rayon", + "regex", + "regex-automata 0.4.9", + "ruff_cache", + "ruff_db", + "ruff_macros", + "ruff_options_metadata", + "ruff_python_ast", + "ruff_python_formatter", + "ruff_text_size", + "rustc-hash", + "salsa", + "schemars", + "serde", + "thiserror 2.0.12", + "toml", + "tracing", + "ty_ide", + "ty_python_semantic", + "ty_vendored", +] + +[[package]] +name = "ty_python_semantic" +version = "0.0.0" dependencies = [ - "lazy_static", + "anyhow", + "bitflags 2.9.1", + "camino", + "colored 3.0.0", + "compact_str", + "dir-test", + "drop_bomb", + "get-size2", + "glob", + "hashbrown 0.15.4", + "indexmap", + "insta", + "itertools 0.14.0", + "memchr", + "ordermap", + "quickcheck", + "quickcheck_macros", + "ruff_annotate_snippets", + "ruff_db", + "ruff_index", + "ruff_macros", + "ruff_python_ast", + "ruff_python_literal", + "ruff_python_parser", + "ruff_python_stdlib", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", + "salsa", + "schemars", + "serde", + "smallvec", + "static_assertions", + "strum", + "strum_macros", + "tempfile", + "test-case", + "thiserror 2.0.12", "tracing", - "tracing-subscriber", + "ty_python_semantic", + "ty_static", + "ty_test", + "ty_vendored", ] [[package]] -name = "tracing-indicatif" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1" +name = "ty_server" +version = "0.0.0" dependencies = [ - "indicatif", + "anyhow", + "crossbeam", + "jod-thread", + "libc", + "lsp-server", + "lsp-types", + "ruff_db", + "ruff_notebook", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", + "salsa", + "serde", + "serde_json", + "shellexpand", "tracing", - "tracing-core", "tracing-subscriber", + "ty_ide", + "ty_project", + "ty_python_semantic", + "ty_vendored", ] [[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +name = "ty_static" +version = "0.0.1" dependencies = [ - "log", - "once_cell", - "tracing-core", + "ruff_macros", ] [[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +name = "ty_test" +version = "0.0.0" dependencies = [ - "chrono", - "matchers", - "nu-ansi-term 0.46.0", - "once_cell", + "anyhow", + "bitflags 2.9.1", + "camino", + "colored 3.0.0", + "insta", + "memchr", "regex", - "sharded-slab", + "ruff_db", + "ruff_index", + "ruff_notebook", + "ruff_python_ast", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", + "rustc-stable-hash", + "salsa", + "serde", "smallvec", - "thread_local", + "tempfile", + "thiserror 2.0.12", + "toml", "tracing", - "tracing-core", - "tracing-log", + "ty_python_semantic", + "ty_static", + "ty_vendored", ] [[package]] -name = "tracing-tree" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f459ca79f1b0d5f71c54ddfde6debfc59c8b6eeb46808ae492077f739dc7b49c" +name = "ty_vendored" +version = "0.0.0" dependencies = [ - "nu-ansi-term 0.50.1", - "tracing-core", - "tracing-log", - "tracing-subscriber", + "path-slash", + "ruff_db", + "static_assertions", + "walkdir", + "zip", ] [[package]] -name = "tryfn" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe242ee9e646acec9ab73a5c540e8543ed1b107f0ce42be831e0775d423c396" +name = "ty_wasm" +version = "0.0.0" dependencies = [ - "ignore", - "libtest-mimic", - "snapbox", + "console_error_panic_hook", + "console_log", + "getrandom 0.3.3", + "js-sys", + "log", + "ruff_db", + "ruff_notebook", + "ruff_python_formatter", + "ruff_source_file", + "ruff_text_size", + "serde-wasm-bindgen", + "ty_ide", + "ty_project", + "ty_python_semantic", + "uuid", + "wasm-bindgen", + "wasm-bindgen-test", ] [[package]] @@ -4223,6 +4413,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -4246,9 +4442,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode_names2" @@ -4272,12 +4468,24 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + [[package]] name = "unscanny" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" @@ -4296,12 +4504,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8-width" version = "0.1.7" @@ -4322,26 +4524,26 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "js-sys", - "rand 0.9.0", + "rand 0.9.1", "uuid-macro-internal", "wasm-bindgen", ] [[package]] name = "uuid-macro-internal" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862" +checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -4365,6 +4567,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "vt100" version = "0.15.2" @@ -4463,7 +4671,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn", "wasm-bindgen-shared", ] @@ -4498,7 +4706,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4533,7 +4741,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -4558,13 +4766,12 @@ dependencies = [ [[package]] name = "which" -version = "7.0.2" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", - "rustix 0.38.44", + "rustix", "winsafe", ] @@ -4599,7 +4806,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4608,92 +4815,63 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" -version = "0.58.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.52.6", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] @@ -4715,18 +4893,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets 0.53.2", ] [[package]] @@ -4738,7 +4910,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -4746,10 +4918,20 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-targets" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] [[package]] name = "windows_aarch64_gnullvm" @@ -4758,10 +4940,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" @@ -4770,10 +4952,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_aarch64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" @@ -4781,6 +4963,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -4788,10 +4976,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_i686_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" @@ -4800,10 +4988,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" @@ -4812,10 +5000,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "windows_x86_64_gnu" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" @@ -4824,10 +5012,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" @@ -4835,11 +5023,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -4856,20 +5050,14 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yansi" @@ -4879,9 +5067,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4891,34 +5079,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -4938,15 +5126,26 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", "synstructure", ] +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4955,13 +5154,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn", ] [[package]] @@ -4998,9 +5197,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 933ae24c5faad..4be307b42a064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,9 @@ members = ["crates/*"] resolver = "2" [workspace.package] -edition = "2021" -rust-version = "1.84" +# Please update rustfmt.toml when bumping the Rust edition +edition = "2024" +rust-version = "1.86" homepage = "https://docs.astral.sh/ruff" documentation = "https://docs.astral.sh/ruff" repository = "https://github.com/astral-sh/ruff" @@ -23,6 +24,7 @@ ruff_index = { path = "crates/ruff_index" } ruff_linter = { path = "crates/ruff_linter" } ruff_macros = { path = "crates/ruff_macros" } ruff_notebook = { path = "crates/ruff_notebook" } +ruff_options_metadata = { path = "crates/ruff_options_metadata" } ruff_python_ast = { path = "crates/ruff_python_ast" } ruff_python_codegen = { path = "crates/ruff_python_codegen" } ruff_python_formatter = { path = "crates/ruff_python_formatter" } @@ -35,22 +37,25 @@ ruff_python_trivia = { path = "crates/ruff_python_trivia" } ruff_server = { path = "crates/ruff_server" } ruff_source_file = { path = "crates/ruff_source_file" } ruff_text_size = { path = "crates/ruff_text_size" } -red_knot_vendored = { path = "crates/red_knot_vendored" } ruff_workspace = { path = "crates/ruff_workspace" } -red_knot_ide = { path = "crates/red_knot_ide" } -red_knot_project = { path = "crates/red_knot_project", default-features = false } -red_knot_python_semantic = { path = "crates/red_knot_python_semantic" } -red_knot_server = { path = "crates/red_knot_server" } -red_knot_test = { path = "crates/red_knot_test" } +ty = { path = "crates/ty" } +ty_ide = { path = "crates/ty_ide" } +ty_project = { path = "crates/ty_project", default-features = false } +ty_python_semantic = { path = "crates/ty_python_semantic" } +ty_server = { path = "crates/ty_server" } +ty_static = { path = "crates/ty_static" } +ty_test = { path = "crates/ty_test" } +ty_vendored = { path = "crates/ty_vendored" } aho-corasick = { version = "1.1.3" } anstream = { version = "0.6.18" } anstyle = { version = "1.0.10" } anyhow = { version = "1.0.80" } +arc-swap = { version = "1.7.1" } assert_fs = { version = "1.1.0" } argfile = { version = "0.2.0" } -bincode = { version = "1.3.3" } +bincode = { version = "2.0.0" } bitflags = { version = "2.5.0" } bstr = { version = "1.9.1" } cachedir = { version = "0.3.1" } @@ -58,23 +63,29 @@ camino = { version = "1.1.7" } clap = { version = "4.5.3", features = ["derive"] } clap_complete_command = { version = "0.6.0" } clearscreen = { version = "4.0.0" } -codspeed-criterion-compat = { version = "2.6.0", default-features = false } +divan = { package = "codspeed-divan-compat", version = "3.0.2" } +codspeed-criterion-compat = { version = "3.0.2", default-features = false } colored = { version = "3.0.0" } console_error_panic_hook = { version = "0.1.7" } console_log = { version = "1.0.0" } countme = { version = "3.0.1" } compact_str = "0.9.0" -criterion = { version = "0.5.1", default-features = false } +criterion = { version = "0.6.0", default-features = false } crossbeam = { version = "0.8.4" } dashmap = { version = "6.0.1" } dir-test = { version = "0.4.0" } dunce = { version = "1.0.5" } drop_bomb = { version = "0.1.5" } -env_logger = { version = "0.11.0" } etcetera = { version = "0.10.0" } fern = { version = "0.7.0" } filetime = { version = "0.2.23" } getrandom = { version = "0.3.1" } +get-size2 = { version = "0.5.0", features = [ + "derive", + "smallvec", + "hashbrown", + "compact-str", +] } glob = { version = "0.3.1" } globset = { version = "0.4.14" } globwalk = { version = "0.9.1" } @@ -83,11 +94,12 @@ hashbrown = { version = "0.15.0", default-features = false, features = [ "equivalent", "inline-more", ] } +heck = "0.5.0" ignore = { version = "0.4.22" } imara-diff = { version = "0.1.5" } imperative = { version = "1.0.4" } indexmap = { version = "2.6.0" } -indicatif = { version = "0.17.8" } +indicatif = { version = "0.18.0" } indoc = { version = "2.0.4" } insta = { version = "1.35.1" } insta-cmd = { version = "0.6.0" } @@ -96,7 +108,7 @@ is-wsl = { version = "0.4.0" } itertools = { version = "0.14.0" } jiff = { version = "0.2.0" } js-sys = { version = "0.3.69" } -jod-thread = { version = "0.1.2" } +jod-thread = { version = "1.0.0" } libc = { version = "0.2.153" } libcst = { version = "1.1.0", default-features = false } log = { version = "0.4.17" } @@ -122,9 +134,11 @@ quote = { version = "1.0.23" } rand = { version = "0.9.0" } rayon = { version = "1.10.0" } regex = { version = "1.10.2" } +regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } +rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" } +salsa = { git = "https://github.com/salsa-rs/salsa", rev = "fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" } schemars = { version = "0.8.16" } seahash = { version = "4.1.0" } serde = { version = "1.0.197", features = ["derive"] } @@ -154,13 +168,14 @@ tikv-jemallocator = { version = "0.6.0" } toml = { version = "0.8.11" } tracing = { version = "0.1.40" } tracing-flame = { version = "0.2.0" } -tracing-indicatif = { version = "0.3.6" } +tracing-indicatif = { version = "0.3.11" } tracing-log = { version = "0.2.0" } tracing-subscriber = { version = "0.3.18", default-features = false, features = [ "env-filter", "fmt", + "ansi", + "smallvec", ] } -tracing-tree = { version = "0.4.0" } tryfn = { version = "0.2.1" } typed-arena = { version = "2.0.2" } unic-ucd-category = { version = "0.9" } @@ -169,12 +184,7 @@ unicode-width = { version = "0.2.0" } unicode_names2 = { version = "1.2.2" } unicode-normalization = { version = "0.1.23" } url = { version = "2.5.0" } -uuid = { version = "1.6.1", features = [ - "v4", - "fast-rng", - "macro-diagnostics", - "js", -] } +uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] } walkdir = { version = "2.3.2" } wasm-bindgen = { version = "0.2.92" } wasm-bindgen-test = { version = "0.3.42" } @@ -182,7 +192,7 @@ wild = { version = "2" } zip = { version = "0.6.6", default-features = false } [workspace.metadata.cargo-shear] -ignored = ["getrandom"] +ignored = ["getrandom", "ruff_options_metadata", "uuid"] [workspace.lints.rust] @@ -209,10 +219,12 @@ must_use_candidate = "allow" similar_names = "allow" single_match_else = "allow" too_many_lines = "allow" -needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block. +needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block. +unnecessary_debug_formatting = "allow" # too many instances, the display also doesn't quote the path which is often desired in logs where we use them the most often. # Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250 needless_raw_string_hashes = "allow" # Disallowed restriction lints +ignore_without_reason = "allow" # Too many exsisting instances, and there's no auto fix. print_stdout = "warn" print_stderr = "warn" dbg_macro = "warn" @@ -254,6 +266,9 @@ opt-level = 3 [profile.dev.package.similar] opt-level = 3 +[profile.dev.package.salsa] +opt-level = 3 + # Reduce complexity of a parser function that would trigger a locals limit in a wasm tool. # https://github.com/bytecodealliance/wasm-tools/blob/b5c3d98e40590512a3b12470ef358d5c7b983b15/crates/wasmparser/src/limits.rs#L29 [profile.dev.package.ruff_python_parser] @@ -268,73 +283,3 @@ debug = 1 # The profile that 'cargo dist' will build with. [profile.dist] inherits = "release" - -# Config for 'dist' -[workspace.metadata.dist] -# The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.4-prerelease.1" -# CI backends to support -ci = "github" -# The installers to generate for each app -installers = ["shell", "powershell"] -# The archive format to use for windows builds (defaults .zip) -windows-archive = ".zip" -# The archive format to use for non-windows builds (defaults .tar.xz) -unix-archive = ".tar.gz" -# Target platforms to build apps for (Rust target-triple syntax) -targets = [ - "aarch64-apple-darwin", - "aarch64-pc-windows-msvc", - "aarch64-unknown-linux-gnu", - "aarch64-unknown-linux-musl", - "arm-unknown-linux-musleabihf", - "armv7-unknown-linux-gnueabihf", - "armv7-unknown-linux-musleabihf", - "i686-pc-windows-msvc", - "i686-unknown-linux-gnu", - "i686-unknown-linux-musl", - "powerpc64-unknown-linux-gnu", - "powerpc64le-unknown-linux-gnu", - "s390x-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-pc-windows-msvc", - "x86_64-unknown-linux-gnu", - "x86_64-unknown-linux-musl", -] -# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) -auto-includes = false -# Whether dist should create a Github Release or use an existing draft -create-release = true -# Which actions to run on pull requests -pr-run-mode = "skip" -# Whether CI should trigger releases with dispatches instead of tag pushes -dispatch-releases = true -# Which phase dist should use to create the GitHub release -github-release = "announce" -# Whether CI should include auto-generated code to build local artifacts -build-local-artifacts = false -# Local artifacts jobs to run in CI -local-artifacts-jobs = ["./build-binaries", "./build-docker"] -# Publish jobs to run in CI -publish-jobs = ["./publish-pypi", "./publish-wasm"] -# Post-announce jobs to run in CI -post-announce-jobs = [ - "./notify-dependents", - "./publish-docs", - "./publish-playground", -] -# Custom permissions for GitHub Jobs -github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } } -# Whether to install an updater program -install-updater = false -# Path that installers should place binaries in -install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] - -[workspace.metadata.dist.github-custom-runners] -global = "depot-ubuntu-latest-4" - -[workspace.metadata.dist.github-action-commits] -"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4 -"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2 -"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1 -"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 diff --git a/README.md b/README.md index 3d82736d8148d..52dde3c03d55d 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,7 @@ An extremely fast Python linter and code formatter, written in Rust. - 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports) - 📏 Over [800 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations of popular Flake8 plugins, like flake8-bugbear -- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/integrations/) for - [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup) +- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/editors) for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup) - 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#config-file-discovery) Ruff aims to be orders of magnitude faster than alternative tools while integrating more @@ -149,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.11.6/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.11.6/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.12.3/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.12.3/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -183,10 +182,10 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.12.3 hooks: # Run the linter. - - id: ruff + - id: ruff-check args: [ --fix ] # Run the formatter. - id: ruff-format @@ -255,7 +254,7 @@ indent-width = 4 target-version = "py39" [lint] -# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. select = ["E4", "E7", "E9", "F"] ignore = [] @@ -424,12 +423,14 @@ Ruff is used by a number of major open-source projects and companies, including: - [Albumentations](https://github.com/albumentations-team/albumentations) - Amazon ([AWS SAM](https://github.com/aws/serverless-application-model)) +- [Anki](https://apps.ankiweb.net/) - Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python)) - [Apache Airflow](https://github.com/apache/airflow) - AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core)) - [Babel](https://github.com/python-babel/babel) - Benchling ([Refac](https://github.com/benchling/refac)) - [Bokeh](https://github.com/bokeh/bokeh) +- Capital One ([datacompy](https://github.com/capitalone/datacompy)) - CrowdCent ([NumerBlox](https://github.com/crowdcent/numerblox)) - [Cryptography (PyCA)](https://github.com/pyca/cryptography) - CERN ([Indico](https://getindico.io/)) @@ -506,6 +507,7 @@ Ruff is used by a number of major open-source projects and companies, including: - [Streamlit](https://github.com/streamlit/streamlit) - [The Algorithms](https://github.com/TheAlgorithms/Python) - [Vega-Altair](https://github.com/altair-viz/altair) +- [Weblate](https://weblate.org/) - WordPress ([Openverse](https://github.com/WordPress/openverse)) - [ZenML](https://github.com/zenml-io/zenml) - [Zulip](https://github.com/zulip/zulip) diff --git a/_typos.toml b/_typos.toml index 2f38f52eea318..24406f10bba16 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,9 +1,13 @@ [files] # https://github.com/crate-ci/typos/issues/868 extend-exclude = [ - "crates/red_knot_vendored/vendor/**/*", + "crates/ty_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*", + # Completion tests tend to have a lot of incomplete + # words naturally. It's annoying to have to make all + # of them actually words. So just ignore typos here. + "crates/ty_ide/src/completion.rs", ] [default.extend-words] diff --git a/changelogs/0.1.x.md b/changelogs/0.1.x.md new file mode 100644 index 0000000000000..5599942bcd17e --- /dev/null +++ b/changelogs/0.1.x.md @@ -0,0 +1,885 @@ +# Changelog 0.1.x + +## 0.1.0 + +This is the first release which uses the `CHANGELOG` file. See [GitHub Releases](https://github.com/astral-sh/ruff/releases) for prior changelog entries. + +Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/). + +### Breaking changes + +- Unsafe fixes are no longer displayed or applied without opt-in ([#7769](https://github.com/astral-sh/ruff/pull/7769)) +- Drop formatting specific rules from the default set ([#7900](https://github.com/astral-sh/ruff/pull/7900)) +- The deprecated `format` setting has been removed ([#7984](https://github.com/astral-sh/ruff/pull/7984)) + - The `format` setting cannot be used to configure the output format, use `output-format` instead + - The `RUFF_FORMAT` environment variable is ignored, use `RUFF_OUTPUT_FORMAT` instead + - The `--format` option has been removed from `ruff check`, use `--output-format` instead + +### Rule changes + +- Extend `reimplemented-starmap` (`FURB140`) to catch calls with a single and starred argument ([#7768](https://github.com/astral-sh/ruff/pull/7768)) +- Improve cases covered by `RUF015` ([#7848](https://github.com/astral-sh/ruff/pull/7848)) +- Update `SIM15` to allow `open` followed by `close` ([#7916](https://github.com/astral-sh/ruff/pull/7916)) +- Respect `msgspec.Struct` default-copy semantics in `RUF012` ([#7786](https://github.com/astral-sh/ruff/pull/7786)) +- Add `sqlalchemy` methods to \`flake8-boolean-trap\`\` exclusion list ([#7874](https://github.com/astral-sh/ruff/pull/7874)) +- Add fix for `PLR1714` ([#7910](https://github.com/astral-sh/ruff/pull/7910)) +- Add fix for `PIE804` ([#7884](https://github.com/astral-sh/ruff/pull/7884)) +- Add fix for `PLC0208` ([#7887](https://github.com/astral-sh/ruff/pull/7887)) +- Add fix for `PYI055` ([#7886](https://github.com/astral-sh/ruff/pull/7886)) +- Update `non-pep695-type-alias` to require `--unsafe-fixes` outside of stub files ([#7836](https://github.com/astral-sh/ruff/pull/7836)) +- Improve fix message for `UP018` ([#7913](https://github.com/astral-sh/ruff/pull/7913)) +- Update `PLW3201` to support `Enum` [sunder names](https://docs.python.org/3/library/enum.html#supported-sunder-names) ([#7987](https://github.com/astral-sh/ruff/pull/7987)) + +### Preview features + +- Only show warnings for empty preview selectors when enabling rules ([#7842](https://github.com/astral-sh/ruff/pull/7842)) +- Add `unnecessary-key-check` to simplify `key in dct and dct[key]` to `dct.get(key)` ([#7895](https://github.com/astral-sh/ruff/pull/7895)) +- Add `assignment-in-assert` to prevent walrus expressions in assert statements ([#7856](https://github.com/astral-sh/ruff/pull/7856)) +- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815)) +- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811)) + +_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._ + +### Configuration + +- Add `unsafe-fixes` setting ([#7769](https://github.com/astral-sh/ruff/pull/7769)) +- Add `extend-safe-fixes` and `extend-unsafe-fixes` for promoting and demoting fixes ([#7841](https://github.com/astral-sh/ruff/pull/7841)) + +### CLI + +- Added `--unsafe-fixes` option for opt-in to display and apply unsafe fixes ([#7769](https://github.com/astral-sh/ruff/pull/7769)) +- Fix use of deprecated `--format` option in warning ([#7837](https://github.com/astral-sh/ruff/pull/7837)) +- Show changed files when running under `--check` ([#7788](https://github.com/astral-sh/ruff/pull/7788)) +- Write summary messages to stderr when fixing via stdin instead of omitting them ([#7838](https://github.com/astral-sh/ruff/pull/7838)) +- Update fix summary message in `check --diff` to include unsafe fix hints ([#7790](https://github.com/astral-sh/ruff/pull/7790)) +- Add notebook `cell` field to JSON output format ([#7664](https://github.com/astral-sh/ruff/pull/7664)) +- Rename applicability levels to `Safe`, `Unsafe`, and `Display` ([#7843](https://github.com/astral-sh/ruff/pull/7843)) + +### Bug fixes + +- Fix bug where f-strings were allowed in match pattern literal ([#7857](https://github.com/astral-sh/ruff/pull/7857)) +- Fix `SIM110` with a yield in the condition ([#7801](https://github.com/astral-sh/ruff/pull/7801)) +- Preserve trailing comments in `C414` fixes ([#7775](https://github.com/astral-sh/ruff/pull/7775)) +- Check sequence type before triggering `unnecessary-enumerate` `len` suggestion ([#7781](https://github.com/astral-sh/ruff/pull/7781)) +- Use correct start location for class/function clause header ([#7802](https://github.com/astral-sh/ruff/pull/7802)) +- Fix incorrect fixes for `SIM101` ([#7798](https://github.com/astral-sh/ruff/pull/7798)) +- Format comment before parameter default correctly ([#7870](https://github.com/astral-sh/ruff/pull/7870)) +- Fix `E251` false positive inside f-strings ([#7894](https://github.com/astral-sh/ruff/pull/7894)) +- Allow bindings to be created and referenced within annotations ([#7885](https://github.com/astral-sh/ruff/pull/7885)) +- Show per-cell diffs when analyzing notebooks over `stdin` ([#7789](https://github.com/astral-sh/ruff/pull/7789)) +- Avoid curly brace escape in f-string format spec ([#7780](https://github.com/astral-sh/ruff/pull/7780)) +- Fix lexing single-quoted f-string with multi-line format spec ([#7787](https://github.com/astral-sh/ruff/pull/7787)) +- Consider nursery rules to be in-preview for `ruff rule` ([#7812](https://github.com/astral-sh/ruff/pull/7812)) +- Report precise location for invalid conversion flag ([#7809](https://github.com/astral-sh/ruff/pull/7809)) +- Visit pattern match guard as a boolean test ([#7911](https://github.com/astral-sh/ruff/pull/7911)) +- Respect `--unfixable` in `ISC` rules ([#7917](https://github.com/astral-sh/ruff/pull/7917)) +- Fix edge case with `PIE804` ([#7922](https://github.com/astral-sh/ruff/pull/7922)) +- Show custom message in `PTH118` for `Path.joinpath` with starred arguments ([#7852](https://github.com/astral-sh/ruff/pull/7852)) +- Fix false negative in `outdated-version-block` when using greater than comparisons ([#7920](https://github.com/astral-sh/ruff/pull/7920)) +- Avoid converting f-strings within Django `gettext` calls ([#7898](https://github.com/astral-sh/ruff/pull/7898)) +- Fix false positive in `PLR6301` ([#7933](https://github.com/astral-sh/ruff/pull/7933)) +- Treat type aliases as typing-only expressions e.g. resolves false positive in `TCH004` ([#7968](https://github.com/astral-sh/ruff/pull/7968)) +- Resolve `cache-dir` relative to project root ([#7962](https://github.com/astral-sh/ruff/pull/7962)) +- Respect subscripted base classes in type-checking rules e.g. resolves false positive in `TCH003` ([#7954](https://github.com/astral-sh/ruff/pull/7954)) +- Fix JSON schema limit for `line-length` ([#7883](https://github.com/astral-sh/ruff/pull/7883)) +- Fix commented-out `coalesce` keyword ([#7876](https://github.com/astral-sh/ruff/pull/7876)) + +### Documentation + +- Document `reimplemented-starmap` performance effects ([#7846](https://github.com/astral-sh/ruff/pull/7846)) +- Default to following the system dark/light mode ([#7888](https://github.com/astral-sh/ruff/pull/7888)) +- Add documentation for fixes ([#7901](https://github.com/astral-sh/ruff/pull/7901)) +- Fix typo in docs of `PLR6301` ([#7831](https://github.com/astral-sh/ruff/pull/7831)) +- Update `UP038` docs to note that it results in slower code ([#7872](https://github.com/astral-sh/ruff/pull/7872)) +- crlf -> cr-lf ([#7766](https://github.com/astral-sh/ruff/pull/7766)) +- Add an example of an unsafe fix ([#7924](https://github.com/astral-sh/ruff/pull/7924)) +- Fix documented examples for `unnecessary-subscript-reversal` ([#7774](https://github.com/astral-sh/ruff/pull/7774)) +- Correct error in tuple example in ruff formatter docs ([#7822](https://github.com/astral-sh/ruff/pull/7822)) +- Add versioning policy to documentation ([#7923](https://github.com/astral-sh/ruff/pull/7923)) +- Fix invalid code in `FURB177` example ([#7832](https://github.com/astral-sh/ruff/pull/7832)) + +### Formatter + +- Less scary `ruff format` message ([#7867](https://github.com/astral-sh/ruff/pull/7867)) +- Remove spaces from import statements ([#7859](https://github.com/astral-sh/ruff/pull/7859)) +- Formatter quoting for f-strings with triple quotes ([#7826](https://github.com/astral-sh/ruff/pull/7826)) +- Update `ruff_python_formatter` generate.py comment ([#7850](https://github.com/astral-sh/ruff/pull/7850)) +- Document one-call chaining deviation ([#7767](https://github.com/astral-sh/ruff/pull/7767)) +- Allow f-string modifications in line-shrinking cases ([#7818](https://github.com/astral-sh/ruff/pull/7818)) +- Add trailing comment deviation to README ([#7827](https://github.com/astral-sh/ruff/pull/7827)) +- Add trailing zero between dot and exponential ([#7956](https://github.com/astral-sh/ruff/pull/7956)) +- Force parentheses for power operations in unary expressions ([#7955](https://github.com/astral-sh/ruff/pull/7955)) + +### Playground + +- Fix playground `Quick Fix` action ([#7824](https://github.com/astral-sh/ruff/pull/7824)) + +## 0.1.1 + +### Rule changes + +- Add unsafe fix for `escape-sequence-in-docstring` (`D301`) ([#7970](https://github.com/astral-sh/ruff/pull/7970)) + +### Configuration + +- Respect `#(deprecated)` attribute in configuration options ([#8035](https://github.com/astral-sh/ruff/pull/8035)) +- Add `[format|lint].exclude` options ([#8000](https://github.com/astral-sh/ruff/pull/8000)) +- Respect `tab-size` setting in formatter ([#8006](https://github.com/astral-sh/ruff/pull/8006)) +- Add `lint.preview` ([#8002](https://github.com/astral-sh/ruff/pull/8002)) + +### Preview features + +- \[`pylint`\] Implement `literal-membership` (`PLR6201`) ([#7973](https://github.com/astral-sh/ruff/pull/7973)) +- \[`pylint`\] Implement `too-many-boolean-expressions` (`PLR0916`) ([#7975](https://github.com/astral-sh/ruff/pull/7975)) +- \[`pylint`\] Implement `misplaced-bare-raise` (`E0704`) ([#7961](https://github.com/astral-sh/ruff/pull/7961)) +- \[`pylint`\] Implement `global-at-module-level` (`W0604`) ([#8058](https://github.com/astral-sh/ruff/pull/8058)) +- \[`pylint`\] Implement `unspecified-encoding` (`PLW1514`) ([#7939](https://github.com/astral-sh/ruff/pull/7939)) +- Add fix for `triple-single-quotes` (`D300`) ([#7967](https://github.com/astral-sh/ruff/pull/7967)) + +### Formatter + +- New code style badge for `ruff format` ([#7878](https://github.com/astral-sh/ruff/pull/7878)) +- Fix comments outside expression parentheses ([#7873](https://github.com/astral-sh/ruff/pull/7873)) +- Add `--target-version` to `ruff format` ([#8055](https://github.com/astral-sh/ruff/pull/8055)) +- Skip over parentheses when detecting `in` keyword ([#8054](https://github.com/astral-sh/ruff/pull/8054)) +- Add `--diff` option to `ruff format` ([#7937](https://github.com/astral-sh/ruff/pull/7937)) +- Insert newline after nested function or class statements ([#7946](https://github.com/astral-sh/ruff/pull/7946)) +- Use `pass` over ellipsis in non-function/class contexts ([#8049](https://github.com/astral-sh/ruff/pull/8049)) + +### Bug fixes + +- Lazily evaluate all PEP 695 type alias values ([#8033](https://github.com/astral-sh/ruff/pull/8033)) +- Avoid failed assertion when showing fixes from stdin ([#8029](https://github.com/astral-sh/ruff/pull/8029)) +- Avoid flagging HTTP and HTTPS literals in urllib-open ([#8046](https://github.com/astral-sh/ruff/pull/8046)) +- Avoid flagging `bad-dunder-method-name` for `_` ([#8015](https://github.com/astral-sh/ruff/pull/8015)) +- Remove Python 2-only methods from `URLOpen` audit ([#8047](https://github.com/astral-sh/ruff/pull/8047)) +- Use set bracket replacement for `iteration-over-set` to preserve whitespace and comments ([#8001](https://github.com/astral-sh/ruff/pull/8001)) + +### Documentation + +- Update tutorial to match revised Ruff defaults ([#8066](https://github.com/astral-sh/ruff/pull/8066)) +- Update rule `B005` docs ([#8028](https://github.com/astral-sh/ruff/pull/8028)) +- Update GitHub actions example in docs to use `--output-format` ([#8014](https://github.com/astral-sh/ruff/pull/8014)) +- Document `lint.preview` and `format.preview` ([#8032](https://github.com/astral-sh/ruff/pull/8032)) +- Clarify that new rules should be added to `RuleGroup::Preview`. ([#7989](https://github.com/astral-sh/ruff/pull/7989)) + +## 0.1.2 + +This release includes the Beta version of the Ruff formatter — an extremely fast, Black-compatible Python formatter. +Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blog/the-ruff-formatter) and [read the docs](https://docs.astral.sh/ruff/formatter/). + +### Preview features + +- \[`pylint`\] Implement `non-ascii-module-import` (`C2403`) ([#8056](https://github.com/astral-sh/ruff/pull/8056)) +- \[`pylint`\] implement `non-ascii-name` (`C2401`) ([#8038](https://github.com/astral-sh/ruff/pull/8038)) +- \[`pylint`\] Implement unnecessary-lambda (W0108) ([#7953](https://github.com/astral-sh/ruff/pull/7953)) +- \[`refurb`\] Implement `read-whole-file` (`FURB101`) ([#7682](https://github.com/astral-sh/ruff/pull/7682)) +- Add fix for `E223`, `E224`, and `E242` ([#8143](https://github.com/astral-sh/ruff/pull/8143)) +- Add fix for `E225`, `E226`, `E227`, and `E228` ([#8136](https://github.com/astral-sh/ruff/pull/8136)) +- Add fix for `E252` ([#8142](https://github.com/astral-sh/ruff/pull/8142)) +- Add fix for `E261` ([#8114](https://github.com/astral-sh/ruff/pull/8114)) +- Add fix for `E273` and `E274` ([#8144](https://github.com/astral-sh/ruff/pull/8144)) +- Add fix for `E275` ([#8133](https://github.com/astral-sh/ruff/pull/8133)) +- Update `SIM401` to catch ternary operations ([#7415](https://github.com/astral-sh/ruff/pull/7415)) +- Update `E721` to allow `is` and `is` not for direct type comparisons ([#7905](https://github.com/astral-sh/ruff/pull/7905)) + +### Rule changes + +- Add `backports.strenum` to `deprecated-imports` ([#8113](https://github.com/astral-sh/ruff/pull/8113)) +- Update `SIM112` to ignore `https_proxy`, `http_proxy`, and `no_proxy` ([#8140](https://github.com/astral-sh/ruff/pull/8140)) +- Update fix for `literal-membership` (`PLR6201`) to be unsafe ([#8097](https://github.com/astral-sh/ruff/pull/8097)) +- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108)) + +### Formatter + +- Change `line-ending` default to `auto` ([#8057](https://github.com/astral-sh/ruff/pull/8057)) +- Respect parenthesized generators in `has_own_parentheses` ([#8100](https://github.com/astral-sh/ruff/pull/8100)) +- Add caching to formatter ([#8089](https://github.com/astral-sh/ruff/pull/8089)) +- Remove `--line-length` option from `format` command ([#8131](https://github.com/astral-sh/ruff/pull/8131)) +- Add formatter to `line-length` documentation ([#8150](https://github.com/astral-sh/ruff/pull/8150)) +- Warn about incompatible formatter options ([#8088](https://github.com/astral-sh/ruff/pull/8088)) +- Fix range of unparenthesized tuple subject in match statement ([#8101](https://github.com/astral-sh/ruff/pull/8101)) +- Remove experimental formatter warning ([#8148](https://github.com/astral-sh/ruff/pull/8148)) +- Don't move type param opening parenthesis comment ([#8163](https://github.com/astral-sh/ruff/pull/8163)) +- Update versions in format benchmark script ([#8110](https://github.com/astral-sh/ruff/pull/8110)) +- Avoid loading files for cached format results ([#8134](https://github.com/astral-sh/ruff/pull/8134)) + +### CLI + +- Show the `ruff format` command in help menus ([#8167](https://github.com/astral-sh/ruff/pull/8167)) +- Add `ruff version` command with long version display ([#8034](https://github.com/astral-sh/ruff/pull/8034)) + +### Configuration + +- New `pycodestyle.max-line-length` option ([#8039](https://github.com/astral-sh/ruff/pull/8039)) + +### Bug fixes + +- Detect `sys.version_info` slices in `outdated-version-block` ([#8112](https://github.com/astral-sh/ruff/pull/8112)) +- Avoid if-else simplification for `TYPE_CHECKING` blocks ([#8072](https://github.com/astral-sh/ruff/pull/8072)) +- Avoid false-positive print separator diagnostic with starred argument ([#8079](https://github.com/astral-sh/ruff/pull/8079)) + +### Documentation + +- Fix message for `too-many-arguments` lint ([#8092](https://github.com/astral-sh/ruff/pull/8092)) +- Fix `extend-unsafe-fixes` and `extend-safe-fixes` example ([#8139](https://github.com/astral-sh/ruff/pull/8139)) +- Add links to `flake8-import-conventions` options ([#8115](https://github.com/astral-sh/ruff/pull/8115)) +- Rework the documentation to incorporate the Ruff formatter ([#7732](https://github.com/astral-sh/ruff/pull/7732)) +- Fix `Options` JSON schema description ([#8081](https://github.com/astral-sh/ruff/pull/8081)) +- Fix typo (`pytext` -> `pytest`) ([#8117](https://github.com/astral-sh/ruff/pull/8117)) +- Improve `magic-value-comparison` example in docs ([#8111](https://github.com/astral-sh/ruff/pull/8111)) + +## 0.1.3 + +This release includes a variety of improvements to the Ruff formatter, removing several known and +unintentional deviations from Black. + +### Formatter + +- Avoid space around pow for `None`, `True` and `False` ([#8189](https://github.com/astral-sh/ruff/pull/8189)) +- Avoid sorting all paths in the format command ([#8181](https://github.com/astral-sh/ruff/pull/8181)) +- Insert necessary blank line between class and leading comments ([#8224](https://github.com/astral-sh/ruff/pull/8224)) +- Avoid introducing new parentheses in annotated assignments ([#8233](https://github.com/astral-sh/ruff/pull/8233)) +- Refine the warnings about incompatible linter options ([#8196](https://github.com/astral-sh/ruff/pull/8196)) +- Add test and basic implementation for formatter preview mode ([#8044](https://github.com/astral-sh/ruff/pull/8044)) +- Refine warning about incompatible `isort` settings ([#8192](https://github.com/astral-sh/ruff/pull/8192)) +- Only omit optional parentheses for starting or ending with parentheses ([#8238](https://github.com/astral-sh/ruff/pull/8238)) +- Use source type to determine parser mode for formatting ([#8205](https://github.com/astral-sh/ruff/pull/8205)) +- Don't warn about magic trailing comma when `isort.force-single-line` is true ([#8244](https://github.com/astral-sh/ruff/pull/8244)) +- Use `SourceKind::diff` for formatter ([#8240](https://github.com/astral-sh/ruff/pull/8240)) +- Fix `fmt:off` with trailing child comment ([#8234](https://github.com/astral-sh/ruff/pull/8234)) +- Formatter parentheses support for `IpyEscapeCommand` ([#8207](https://github.com/astral-sh/ruff/pull/8207)) + +### Linter + +- \[`pylint`\] Add buffer methods to `bad-dunder-method-name` (`PLW3201`) exclusions ([#8190](https://github.com/astral-sh/ruff/pull/8190)) +- Match rule prefixes from `external` codes setting in `unused-noqa` ([#8177](https://github.com/astral-sh/ruff/pull/8177)) +- Use `line-length` setting for isort in lieu of `pycodestyle.max-line-length` ([#8235](https://github.com/astral-sh/ruff/pull/8235)) +- Update fix for `unnecessary-paren-on-raise-exception` to unsafe for unknown types ([#8231](https://github.com/astral-sh/ruff/pull/8231)) +- Correct quick fix message for `W605` ([#8255](https://github.com/astral-sh/ruff/pull/8255)) + +### Documentation + +- Fix typo in max-doc-length documentation ([#8201](https://github.com/astral-sh/ruff/pull/8201)) +- Improve documentation around linter-formatter conflicts ([#8257](https://github.com/astral-sh/ruff/pull/8257)) +- Fix link to error suppression documentation in `unused-noqa` ([#8172](https://github.com/astral-sh/ruff/pull/8172)) +- Add `external` option to `unused-noqa` documentation ([#8171](https://github.com/astral-sh/ruff/pull/8171)) +- Add title attribute to icons ([#8060](https://github.com/astral-sh/ruff/pull/8060)) +- Clarify unsafe case in RSE102 ([#8256](https://github.com/astral-sh/ruff/pull/8256)) +- Fix skipping formatting examples ([#8210](https://github.com/astral-sh/ruff/pull/8210)) +- docs: fix name of `magic-trailing-comma` option in README ([#8200](https://github.com/astral-sh/ruff/pull/8200)) +- Add note about scope of rule changing in versioning policy ([#8169](https://github.com/astral-sh/ruff/pull/8169)) +- Document: Fix default lint rules ([#8218](https://github.com/astral-sh/ruff/pull/8218)) +- Fix a wrong setting in configuration.md ([#8186](https://github.com/astral-sh/ruff/pull/8186)) +- Fix misspelled TOML headers in the tutorial ([#8209](https://github.com/astral-sh/ruff/pull/8209)) + +## 0.1.4 + +### Preview features + +- \[`flake8-trio`\] Implement `timeout-without-await` (`TRIO001`) ([#8439](https://github.com/astral-sh/ruff/pull/8439)) +- \[`numpy`\] Implement NumPy 2.0 migration rule (`NPY200`) ([#7702](https://github.com/astral-sh/ruff/pull/7702)) +- \[`pylint`\] Implement `bad-open-mode` (`W1501`) ([#8294](https://github.com/astral-sh/ruff/pull/8294)) +- \[`pylint`\] Implement `import-outside-toplevel` (`C0415`) rule ([#5180](https://github.com/astral-sh/ruff/pull/5180)) +- \[`pylint`\] Implement `useless-with-lock` (`W2101`) ([#8321](https://github.com/astral-sh/ruff/pull/8321)) +- \[`pyupgrade`\] Implement `timeout-error-alias` (`UP041`) ([#8476](https://github.com/astral-sh/ruff/pull/8476)) +- \[`refurb`\] Implement `isinstance-type-none` (`FURB168`) ([#8308](https://github.com/astral-sh/ruff/pull/8308)) +- Detect confusable Unicode-to-Unicode units in `RUF001`, `RUF002`, and `RUF003` ([#4430](https://github.com/astral-sh/ruff/pull/4430)) +- Add newline after module docstrings in preview style ([#8283](https://github.com/astral-sh/ruff/pull/8283)) + +### Formatter + +- Add a note on line-too-long to the formatter docs ([#8314](https://github.com/astral-sh/ruff/pull/8314)) +- Preserve trailing statement semicolons when using `fmt: skip` ([#8273](https://github.com/astral-sh/ruff/pull/8273)) +- Preserve trailing semicolons when using `fmt: off` ([#8275](https://github.com/astral-sh/ruff/pull/8275)) +- Avoid duplicating linter-formatter compatibility warnings ([#8292](https://github.com/astral-sh/ruff/pull/8292)) +- Avoid inserting a newline after function docstrings ([#8375](https://github.com/astral-sh/ruff/pull/8375)) +- Insert newline between docstring and following own line comment ([#8216](https://github.com/astral-sh/ruff/pull/8216)) +- Split tuples in return positions by comma first ([#8280](https://github.com/astral-sh/ruff/pull/8280)) +- Avoid treating byte strings as docstrings ([#8350](https://github.com/astral-sh/ruff/pull/8350)) +- Add `--line-length` option to `format` command ([#8363](https://github.com/astral-sh/ruff/pull/8363)) +- Avoid parenthesizing unsplittable because of comments ([#8431](https://github.com/astral-sh/ruff/pull/8431)) + +### CLI + +- Add `--output-format` to `ruff rule` and `ruff linter` ([#8203](https://github.com/astral-sh/ruff/pull/8203)) + +### Bug fixes + +- Respect `--force-exclude` in `lint.exclude` and `format.exclude` ([#8393](https://github.com/astral-sh/ruff/pull/8393)) +- Respect `--extend-per-file-ignores` on the CLI ([#8329](https://github.com/astral-sh/ruff/pull/8329)) +- Extend `bad-dunder-method-name` to permit `__index__` ([#8300](https://github.com/astral-sh/ruff/pull/8300)) +- Fix panic with 8 in octal escape ([#8356](https://github.com/astral-sh/ruff/pull/8356)) +- Avoid raising `D300` when both triple quote styles are present ([#8462](https://github.com/astral-sh/ruff/pull/8462)) +- Consider unterminated f-strings in `FStringRanges` ([#8154](https://github.com/astral-sh/ruff/pull/8154)) +- Avoid including literal `shell=True` for truthy, non-`True` diagnostics ([#8359](https://github.com/astral-sh/ruff/pull/8359)) +- Avoid triggering single-element test for starred expressions ([#8433](https://github.com/astral-sh/ruff/pull/8433)) +- Detect and ignore Jupyter automagics ([#8398](https://github.com/astral-sh/ruff/pull/8398)) +- Fix invalid E231 error with f-strings ([#8369](https://github.com/astral-sh/ruff/pull/8369)) +- Avoid triggering `NamedTuple` rewrite with starred annotation ([#8434](https://github.com/astral-sh/ruff/pull/8434)) +- Avoid un-setting bracket flag in logical lines ([#8380](https://github.com/astral-sh/ruff/pull/8380)) +- Place 'r' prefix before 'f' for raw format strings ([#8464](https://github.com/astral-sh/ruff/pull/8464)) +- Remove trailing periods from NumPy 2.0 code actions ([#8475](https://github.com/astral-sh/ruff/pull/8475)) +- Fix bug where `PLE1307` was raised when formatting `%c` with characters ([#8407](https://github.com/astral-sh/ruff/pull/8407)) +- Remove unicode flag from comparable ([#8440](https://github.com/astral-sh/ruff/pull/8440)) +- Improve B015 message ([#8295](https://github.com/astral-sh/ruff/pull/8295)) +- Use `fixedOverflowWidgets` for playground popover ([#8458](https://github.com/astral-sh/ruff/pull/8458)) +- Mark `byte_bounds` as a non-backwards-compatible NumPy 2.0 change ([#8474](https://github.com/astral-sh/ruff/pull/8474)) + +### Internals + +- Add a dedicated cache directory per Ruff version ([#8333](https://github.com/astral-sh/ruff/pull/8333)) +- Allow selective caching for `--fix` and `--diff` ([#8316](https://github.com/astral-sh/ruff/pull/8316)) +- Improve performance of comment parsing ([#8193](https://github.com/astral-sh/ruff/pull/8193)) +- Improve performance of string parsing ([#8227](https://github.com/astral-sh/ruff/pull/8227)) +- Use a dedicated sort key for isort import sorting ([#7963](https://github.com/astral-sh/ruff/pull/7963)) + +## 0.1.5 + +### Preview features + +- \[`flake8-bandit`\] Implement `mako-templates` (`S702`) ([#8533](https://github.com/astral-sh/ruff/pull/8533)) +- \[`flake8-trio`\] Implement `TRIO105` ([#8490](https://github.com/astral-sh/ruff/pull/8490)) +- \[`flake8-trio`\] Implement `TRIO109` ([#8534](https://github.com/astral-sh/ruff/pull/8534)) +- \[`flake8-trio`\] Implement `TRIO110` ([#8537](https://github.com/astral-sh/ruff/pull/8537)) +- \[`flake8-trio`\] Implement `TRIO115` ([#8486](https://github.com/astral-sh/ruff/pull/8486)) +- \[`refurb`\] Implement `type-none-comparison` (`FURB169`) ([#8487](https://github.com/astral-sh/ruff/pull/8487)) +- Flag all comparisons against builtin types in `E721` ([#8491](https://github.com/astral-sh/ruff/pull/8491)) +- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525)) + +### Formatter + +- Fix multiline lambda expression statement formatting ([#8466](https://github.com/astral-sh/ruff/pull/8466)) + +### CLI + +- Add hidden `--extension` to override inference of source type from file extension ([#8373](https://github.com/astral-sh/ruff/pull/8373)) + +### Configuration + +- Account for selector specificity when merging `extend_unsafe_fixes` and `override extend_safe_fixes` ([#8444](https://github.com/astral-sh/ruff/pull/8444)) +- Add support for disabling cache with `RUFF_NO_CACHE` environment variable ([#8538](https://github.com/astral-sh/ruff/pull/8538)) + +### Bug fixes + +- \[`E721`\] Flag comparisons to `memoryview` ([#8485](https://github.com/astral-sh/ruff/pull/8485)) +- Allow collapsed-ellipsis bodies in other statements ([#8499](https://github.com/astral-sh/ruff/pull/8499)) +- Avoid `D301` autofix for `u` prefixed strings ([#8495](https://github.com/astral-sh/ruff/pull/8495)) +- Only flag `flake8-trio` rules when `trio` import is present ([#8550](https://github.com/astral-sh/ruff/pull/8550)) +- Reject more syntactically invalid Python programs ([#8524](https://github.com/astral-sh/ruff/pull/8524)) +- Avoid raising `TRIO115` violations for `trio.sleep(...)` calls with non-number values ([#8532](https://github.com/astral-sh/ruff/pull/8532)) +- Fix `F841` false negative on assignment to multiple variables ([#8489](https://github.com/astral-sh/ruff/pull/8489)) + +### Documentation + +- Fix link to isort `known-first-party` ([#8562](https://github.com/astral-sh/ruff/pull/8562)) +- Add notes on fix safety to a few rules ([#8500](https://github.com/astral-sh/ruff/pull/8500)) +- Add missing toml config tabs ([#8512](https://github.com/astral-sh/ruff/pull/8512)) +- Add instructions for configuration of Emacs ([#8488](https://github.com/astral-sh/ruff/pull/8488)) +- Improve detail link contrast in dark mode ([#8548](https://github.com/astral-sh/ruff/pull/8548)) +- Fix typo in example ([#8506](https://github.com/astral-sh/ruff/pull/8506)) +- Added tabs for configuration files in the documentation ([#8480](https://github.com/astral-sh/ruff/pull/8480)) +- Recommend `project.requires-python` over `target-version` ([#8513](https://github.com/astral-sh/ruff/pull/8513)) +- Add singleton escape hatch to `B008` documentation ([#8501](https://github.com/astral-sh/ruff/pull/8501)) +- Fix tab configuration docs ([#8502](https://github.com/astral-sh/ruff/pull/8502)) + +## 0.1.6 + +### Preview features + +- \[`flake8-boolean-trap`\] Extend `boolean-type-hint-positional-argument` (`FBT001`) to include booleans in unions ([#7501](https://github.com/astral-sh/ruff/pull/7501)) +- \[`flake8-pie`\] Extend `reimplemented-list-builtin` (`PIE807`) to `dict` reimplementations ([#8608](https://github.com/astral-sh/ruff/pull/8608)) +- \[`flake8-pie`\] Extend `unnecessary-pass` (`PIE790`) to include ellipses (`...`) ([#8641](https://github.com/astral-sh/ruff/pull/8641)) +- \[`flake8-pie`\] Implement fix for `unnecessary-spread` (`PIE800`) ([#8668](https://github.com/astral-sh/ruff/pull/8668)) +- \[`flake8-quotes`\] Implement `unnecessary-escaped-quote` (`Q004`) ([#8630](https://github.com/astral-sh/ruff/pull/8630)) +- \[`pycodestyle`\] Implement fix for `multiple-spaces-after-keyword` (`E271`) and `multiple-spaces-before-keyword` (`E272`) ([#8622](https://github.com/astral-sh/ruff/pull/8622)) +- \[`pycodestyle`\] Implement fix for `multiple-spaces-after-operator` (`E222`) and `multiple-spaces-before-operator` (`E221`) ([#8623](https://github.com/astral-sh/ruff/pull/8623)) +- \[`pyflakes`\] Extend `is-literal` (`F632`) to include comparisons against mutable initializers ([#8607](https://github.com/astral-sh/ruff/pull/8607)) +- \[`pylint`\] Implement `redefined-argument-from-local` (`PLR1704`) ([#8159](https://github.com/astral-sh/ruff/pull/8159)) +- \[`pylint`\] Implement fix for `unnecessary-lambda` (`PLW0108`) ([#8621](https://github.com/astral-sh/ruff/pull/8621)) +- \[`refurb`\] Implement `if-expr-min-max` (`FURB136`) ([#8664](https://github.com/astral-sh/ruff/pull/8664)) +- \[`refurb`\] Implement `math-constant` (`FURB152`) ([#8727](https://github.com/astral-sh/ruff/pull/8727)) + +### Rule changes + +- \[`flake8-annotations`\] Add autotyping-like return type inference for annotation rules ([#8643](https://github.com/astral-sh/ruff/pull/8643)) +- \[`flake8-future-annotations`\] Implement fix for `future-required-type-annotation` (`FA102`) ([#8711](https://github.com/astral-sh/ruff/pull/8711)) +- \[`flake8-implicit-namespace-package`\] Avoid missing namespace violations in scripts with shebangs ([#8710](https://github.com/astral-sh/ruff/pull/8710)) +- \[`pydocstyle`\] Update `over-indentation` (`D208`) to preserve indentation offsets when fixing overindented lines ([#8699](https://github.com/astral-sh/ruff/pull/8699)) +- \[`pyupgrade`\] Refine `timeout-error-alias` (`UP041`) to remove false positives ([#8587](https://github.com/astral-sh/ruff/pull/8587)) + +### Formatter + +- Fix instability in `await` formatting with fluent style ([#8676](https://github.com/astral-sh/ruff/pull/8676)) +- Compare formatted and unformatted ASTs during formatter tests ([#8624](https://github.com/astral-sh/ruff/pull/8624)) +- Preserve trailing semicolon for Notebooks ([#8590](https://github.com/astral-sh/ruff/pull/8590)) + +### CLI + +- Improve debug printing for resolving origin of config settings ([#8729](https://github.com/astral-sh/ruff/pull/8729)) +- Write unchanged, excluded files to stdout when read via stdin ([#8596](https://github.com/astral-sh/ruff/pull/8596)) + +### Configuration + +- \[`isort`\] Support disabling sections with `no-sections = true` ([#8657](https://github.com/astral-sh/ruff/pull/8657)) +- \[`pep8-naming`\] Support local and dynamic class- and static-method decorators ([#8592](https://github.com/astral-sh/ruff/pull/8592)) +- \[`pydocstyle`\] Allow overriding pydocstyle convention rules ([#8586](https://github.com/astral-sh/ruff/pull/8586)) + +### Bug fixes + +- Avoid syntax error via importing `trio.lowlevel` ([#8730](https://github.com/astral-sh/ruff/pull/8730)) +- Omit unrolled augmented assignments in `PIE794` ([#8634](https://github.com/astral-sh/ruff/pull/8634)) +- Slice source code instead of generating it for `EM` fixes ([#7746](https://github.com/astral-sh/ruff/pull/7746)) +- Allow whitespace around colon in slices for `whitespace-before-punctuation` (`E203`) ([#8654](https://github.com/astral-sh/ruff/pull/8654)) +- Use function range for `no-self-use` ([#8637](https://github.com/astral-sh/ruff/pull/8637)) +- F-strings doesn't contain bytes literal for `PLW0129` ([#8675](https://github.com/astral-sh/ruff/pull/8675)) +- Improve detection of `TYPE_CHECKING` blocks imported from `typing_extensions` or `_typeshed` ([#8429](https://github.com/astral-sh/ruff/pull/8429)) +- Treat display as a builtin in IPython ([#8707](https://github.com/astral-sh/ruff/pull/8707)) +- Avoid `FURB113` autofix if comments are present ([#8494](https://github.com/astral-sh/ruff/pull/8494)) +- Consider the new f-string tokens for `flake8-commas` ([#8582](https://github.com/astral-sh/ruff/pull/8582)) +- Remove erroneous bad-dunder-name reference ([#8742](https://github.com/astral-sh/ruff/pull/8742)) +- Avoid recommending Self usages in metaclasses ([#8639](https://github.com/astral-sh/ruff/pull/8639)) +- Detect runtime-evaluated base classes defined in the current file ([#8572](https://github.com/astral-sh/ruff/pull/8572)) +- Avoid inserting trailing commas within f-strings ([#8574](https://github.com/astral-sh/ruff/pull/8574)) +- Remove incorrect deprecation label for stdout and stderr ([#8743](https://github.com/astral-sh/ruff/pull/8743)) +- Fix unnecessary parentheses in UP007 fix ([#8610](https://github.com/astral-sh/ruff/pull/8610)) +- Remove repeated and erroneous scoped settings headers in docs ([#8670](https://github.com/astral-sh/ruff/pull/8670)) +- Trim trailing empty strings when converting to f-strings ([#8712](https://github.com/astral-sh/ruff/pull/8712)) +- Fix ordering for `force-sort-within-sections` ([#8665](https://github.com/astral-sh/ruff/pull/8665)) +- Run unicode prefix rule over tokens ([#8709](https://github.com/astral-sh/ruff/pull/8709)) +- Update UP032 to unescape curly braces in literal parts of converted strings ([#8697](https://github.com/astral-sh/ruff/pull/8697)) +- List all ipython builtins ([#8719](https://github.com/astral-sh/ruff/pull/8719)) + +### Documentation + +- Document conventions in the FAQ ([#8638](https://github.com/astral-sh/ruff/pull/8638)) +- Redirect from rule codes to rule pages in docs ([#8636](https://github.com/astral-sh/ruff/pull/8636)) +- Fix permalink to convention setting ([#8575](https://github.com/astral-sh/ruff/pull/8575)) + +## 0.1.7 + +### Preview features + +- Implement multiline dictionary and list hugging for preview style ([#8293](https://github.com/astral-sh/ruff/pull/8293)) +- Implement the `fix_power_op_line_length` preview style ([#8947](https://github.com/astral-sh/ruff/pull/8947)) +- Use Python version to determine typing rewrite safety ([#8919](https://github.com/astral-sh/ruff/pull/8919)) +- \[`flake8-annotations`\] Enable auto-return-type involving `Optional` and `Union` annotations ([#8885](https://github.com/astral-sh/ruff/pull/8885)) +- \[`flake8-bandit`\] Implement `django-raw-sql` (`S611`) ([#8651](https://github.com/astral-sh/ruff/pull/8651)) +- \[`flake8-bandit`\] Implement `tarfile-unsafe-members` (`S202`) ([#8829](https://github.com/astral-sh/ruff/pull/8829)) +- \[`flake8-pyi`\] Implement fix for `unnecessary-literal-union` (`PYI030`) ([#7934](https://github.com/astral-sh/ruff/pull/7934)) +- \[`flake8-simplify`\] Extend `dict-get-with-none-default` (`SIM910`) to non-literals ([#8762](https://github.com/astral-sh/ruff/pull/8762)) +- \[`pylint`\] - add `unnecessary-list-index-lookup` (`PLR1736`) + autofix ([#7999](https://github.com/astral-sh/ruff/pull/7999)) +- \[`pylint`\] - implement R0202 and R0203 with autofixes ([#8335](https://github.com/astral-sh/ruff/pull/8335)) +- \[`pylint`\] Implement `repeated-keyword` (`PLe1132`) ([#8706](https://github.com/astral-sh/ruff/pull/8706)) +- \[`pylint`\] Implement `too-many-positional` (`PLR0917`) ([#8995](https://github.com/astral-sh/ruff/pull/8995)) +- \[`pylint`\] Implement `unnecessary-dict-index-lookup` (`PLR1733`) ([#8036](https://github.com/astral-sh/ruff/pull/8036)) +- \[`refurb`\] Implement `redundant-log-base` (`FURB163`) ([#8842](https://github.com/astral-sh/ruff/pull/8842)) + +### Rule changes + +- \[`flake8-boolean-trap`\] Allow booleans in `@override` methods ([#8882](https://github.com/astral-sh/ruff/pull/8882)) +- \[`flake8-bugbear`\] Avoid `B015`,`B018` for last expression in a cell ([#8815](https://github.com/astral-sh/ruff/pull/8815)) +- \[`flake8-pie`\] Allow ellipses for enum values in stub files ([#8825](https://github.com/astral-sh/ruff/pull/8825)) +- \[`flake8-pyi`\] Check PEP 695 type aliases for `snake-case-type-alias` and `t-suffixed-type-alias` ([#8966](https://github.com/astral-sh/ruff/pull/8966)) +- \[`flake8-pyi`\] Check for kwarg and vararg `NoReturn` type annotations ([#8948](https://github.com/astral-sh/ruff/pull/8948)) +- \[`flake8-simplify`\] Omit select context managers from `SIM117` ([#8801](https://github.com/astral-sh/ruff/pull/8801)) +- \[`pep8-naming`\] Allow Django model loads in `non-lowercase-variable-in-function` (`N806`) ([#8917](https://github.com/astral-sh/ruff/pull/8917)) +- \[`pycodestyle`\] Avoid `E703` for last expression in a cell ([#8821](https://github.com/astral-sh/ruff/pull/8821)) +- \[`pycodestyle`\] Update `E402` to work at cell level for notebooks ([#8872](https://github.com/astral-sh/ruff/pull/8872)) +- \[`pydocstyle`\] Avoid `D100` for Jupyter Notebooks ([#8816](https://github.com/astral-sh/ruff/pull/8816)) +- \[`pylint`\] Implement fix for `unspecified-encoding` (`PLW1514`) ([#8928](https://github.com/astral-sh/ruff/pull/8928)) + +### Formatter + +- Avoid unstable formatting in ellipsis-only body with trailing comment ([#8984](https://github.com/astral-sh/ruff/pull/8984)) +- Inline trailing comments for type alias similar to assignments ([#8941](https://github.com/astral-sh/ruff/pull/8941)) +- Insert trailing comma when function breaks with single argument ([#8921](https://github.com/astral-sh/ruff/pull/8921)) + +### CLI + +- Update `ruff check` and `ruff format` to default to the current directory ([#8791](https://github.com/astral-sh/ruff/pull/8791)) +- Stop at the first resolved parent configuration ([#8864](https://github.com/astral-sh/ruff/pull/8864)) + +### Configuration + +- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998)) +- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812)) +- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663)) +- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841)) + +### Bug fixes + +- Add support for `@functools.singledispatch` ([#8934](https://github.com/astral-sh/ruff/pull/8934)) +- Avoid off-by-one error in stripping noqa following multi-byte char ([#8979](https://github.com/astral-sh/ruff/pull/8979)) +- Avoid off-by-one error in with-item named expressions ([#8915](https://github.com/astral-sh/ruff/pull/8915)) +- Avoid syntax error via invalid ur string prefix ([#8971](https://github.com/astral-sh/ruff/pull/8971)) +- Avoid underflow in `get_model` matching ([#8965](https://github.com/astral-sh/ruff/pull/8965)) +- Avoid unnecessary index diagnostics when value is modified ([#8970](https://github.com/astral-sh/ruff/pull/8970)) +- Convert over-indentation rule to use number of characters ([#8983](https://github.com/astral-sh/ruff/pull/8983)) +- Detect implicit returns in auto-return-types ([#8952](https://github.com/astral-sh/ruff/pull/8952)) +- Fix start >= end error in over-indentation ([#8982](https://github.com/astral-sh/ruff/pull/8982)) +- Ignore `@overload` and `@override` methods for too-many-arguments checks ([#8954](https://github.com/astral-sh/ruff/pull/8954)) +- Lexer start of line is false only for `Mode::Expression` ([#8880](https://github.com/astral-sh/ruff/pull/8880)) +- Mark `pydantic_settings.BaseSettings` as having default copy semantics ([#8793](https://github.com/astral-sh/ruff/pull/8793)) +- Respect dictionary unpacking in `NamedTuple` assignments ([#8810](https://github.com/astral-sh/ruff/pull/8810)) +- Respect local subclasses in `flake8-type-checking` ([#8768](https://github.com/astral-sh/ruff/pull/8768)) +- Support type alias statements in simple statement positions ([#8916](https://github.com/astral-sh/ruff/pull/8916)) +- \[`flake8-annotations`\] Avoid filtering out un-representable types in return annotation ([#8881](https://github.com/astral-sh/ruff/pull/8881)) +- \[`flake8-pie`\] Retain extra ellipses in protocols and abstract methods ([#8769](https://github.com/astral-sh/ruff/pull/8769)) +- \[`flake8-pyi`\] Respect local enum subclasses in `simple-defaults` (`PYI052`) ([#8767](https://github.com/astral-sh/ruff/pull/8767)) +- \[`flake8-trio`\] Use correct range for `TRIO115` fix ([#8933](https://github.com/astral-sh/ruff/pull/8933)) +- \[`flake8-trio`\] Use full arguments range for zero-sleep-call ([#8936](https://github.com/astral-sh/ruff/pull/8936)) +- \[`isort`\] fix: mark `__main__` as first-party import ([#8805](https://github.com/astral-sh/ruff/pull/8805)) +- \[`pep8-naming`\] Avoid `N806` errors for type alias statements ([#8785](https://github.com/astral-sh/ruff/pull/8785)) +- \[`perflint`\] Avoid `PERF101` if there's an append in loop body ([#8809](https://github.com/astral-sh/ruff/pull/8809)) +- \[`pycodestyle`\] Allow space-before-colon after end-of-slice ([#8838](https://github.com/astral-sh/ruff/pull/8838)) +- \[`pydocstyle`\] Avoid non-character breaks in `over-indentation` (`D208`) ([#8866](https://github.com/astral-sh/ruff/pull/8866)) +- \[`pydocstyle`\] Ignore underlines when determining docstring logical lines ([#8929](https://github.com/astral-sh/ruff/pull/8929)) +- \[`pylint`\] Extend `self-assigning-variable` to multi-target assignments ([#8839](https://github.com/astral-sh/ruff/pull/8839)) +- \[`tryceratops`\] Avoid repeated triggers in nested `tryceratops` diagnostics ([#8772](https://github.com/astral-sh/ruff/pull/8772)) + +### Documentation + +- Add advice for fixing RUF008 when mutability is not desired ([#8853](https://github.com/astral-sh/ruff/pull/8853)) +- Added the command to run ruff using pkgx to the installation.md ([#8955](https://github.com/astral-sh/ruff/pull/8955)) +- Document fix safety for flake8-comprehensions and some pyupgrade rules ([#8918](https://github.com/astral-sh/ruff/pull/8918)) +- Fix doc formatting for zero-sleep-call ([#8937](https://github.com/astral-sh/ruff/pull/8937)) +- Remove duplicate imports from os-stat documentation ([#8930](https://github.com/astral-sh/ruff/pull/8930)) +- Replace generated reference to MkDocs ([#8806](https://github.com/astral-sh/ruff/pull/8806)) +- Update Arch Linux package URL in installation.md ([#8802](https://github.com/astral-sh/ruff/pull/8802)) +- \[`flake8-pyi`\] Fix error in `t-suffixed-type-alias` (`PYI043`) example ([#8963](https://github.com/astral-sh/ruff/pull/8963)) +- \[`flake8-pyi`\] Improve motivation for `custom-type-var-return-type` (`PYI019`) ([#8766](https://github.com/astral-sh/ruff/pull/8766)) + +## 0.1.8 + +This release includes opt-in support for formatting Python snippets within +docstrings via the `docstring-code-format` setting. +[Check out the blog post](https://astral.sh/blog/ruff-v0.1.8) for more details! + +### Preview features + +- Add `"preserve"` quote-style to mimic Black's skip-string-normalization ([#8822](https://github.com/astral-sh/ruff/pull/8822)) +- Implement `prefer_splitting_right_hand_side_of_assignments` preview style ([#8943](https://github.com/astral-sh/ruff/pull/8943)) +- \[`pycodestyle`\] Add fix for `unexpected-spaces-around-keyword-parameter-equals` ([#9072](https://github.com/astral-sh/ruff/pull/9072)) +- \[`pycodestyle`\] Add fix for comment-related whitespace rules ([#9075](https://github.com/astral-sh/ruff/pull/9075)) +- \[`pycodestyle`\] Allow `sys.path` modifications between imports ([#9047](https://github.com/astral-sh/ruff/pull/9047)) +- \[`refurb`\] Implement `hashlib-digest-hex` (`FURB181`) ([#9077](https://github.com/astral-sh/ruff/pull/9077)) + +### Rule changes + +- Allow `flake8-type-checking` rules to automatically quote runtime-evaluated references ([#6001](https://github.com/astral-sh/ruff/pull/6001)) +- Allow transparent cell magics in Jupyter Notebooks ([#8911](https://github.com/astral-sh/ruff/pull/8911)) +- \[`flake8-annotations`\] Avoid `ANN2xx` fixes for abstract methods with empty bodies ([#9034](https://github.com/astral-sh/ruff/pull/9034)) +- \[`flake8-self`\] Ignore underscore references in type annotations ([#9036](https://github.com/astral-sh/ruff/pull/9036)) +- \[`pep8-naming`\] Allow class names when `apps.get_model` is a non-string ([#9065](https://github.com/astral-sh/ruff/pull/9065)) +- \[`pycodestyle`\] Allow `matplotlib.use` calls to intersperse imports ([#9094](https://github.com/astral-sh/ruff/pull/9094)) +- \[`pyflakes`\] Support fixing unused assignments in tuples by renaming variables (`F841`) ([#9107](https://github.com/astral-sh/ruff/pull/9107)) +- \[`pylint`\] Add fix for `subprocess-run-without-check` (`PLW1510`) ([#6708](https://github.com/astral-sh/ruff/pull/6708)) + +### Formatter + +- Add `docstring-code-format` knob to enable docstring snippet formatting ([#8854](https://github.com/astral-sh/ruff/pull/8854)) +- Use double quotes for all docstrings, including single-quoted docstrings ([#9020](https://github.com/astral-sh/ruff/pull/9020)) +- Implement "dynamic" line width mode for docstring code formatting ([#9098](https://github.com/astral-sh/ruff/pull/9098)) +- Support reformatting Markdown code blocks ([#9030](https://github.com/astral-sh/ruff/pull/9030)) +- add support for formatting reStructuredText code snippets ([#9003](https://github.com/astral-sh/ruff/pull/9003)) +- Avoid trailing comma for single-argument with positional separator ([#9076](https://github.com/astral-sh/ruff/pull/9076)) +- Fix handling of trailing target comment ([#9051](https://github.com/astral-sh/ruff/pull/9051)) + +### CLI + +- Hide unsafe fix suggestions when explicitly disabled ([#9095](https://github.com/astral-sh/ruff/pull/9095)) +- Add SARIF support to `--output-format` ([#9078](https://github.com/astral-sh/ruff/pull/9078)) + +### Bug fixes + +- Apply unnecessary index rule prior to enumerate rewrite ([#9012](https://github.com/astral-sh/ruff/pull/9012)) +- \[`flake8-err-msg`\] Allow `EM` fixes even if `msg` variable is defined ([#9059](https://github.com/astral-sh/ruff/pull/9059)) +- \[`flake8-pie`\] Prevent keyword arguments duplication ([#8450](https://github.com/astral-sh/ruff/pull/8450)) +- \[`flake8-pie`\] Respect trailing comma in `unnecessary-dict-kwargs` (`PIE804`) ([#9015](https://github.com/astral-sh/ruff/pull/9015)) +- \[`flake8-raise`\] Avoid removing parentheses on ctypes.WinError ([#9027](https://github.com/astral-sh/ruff/pull/9027)) +- \[`isort`\] Avoid invalid combination of `force-sort-within-types` and `lines-between-types` ([#9041](https://github.com/astral-sh/ruff/pull/9041)) +- \[`isort`\] Ensure that from-style imports are always ordered first in `__future__` ([#9039](https://github.com/astral-sh/ruff/pull/9039)) +- \[`pycodestyle`\] Allow tab indentation before keyword ([#9099](https://github.com/astral-sh/ruff/pull/9099)) +- \[`pylint`\] Ignore `@overrides` and `@overloads` for `too-many-positional` ([#9000](https://github.com/astral-sh/ruff/pull/9000)) +- \[`pyupgrade`\] Enable `printf-string-formatting` fix with comments on right-hand side ([#9037](https://github.com/astral-sh/ruff/pull/9037)) +- \[`refurb`\] Make `math-constant` (`FURB152`) rule more targeted ([#9054](https://github.com/astral-sh/ruff/pull/9054)) +- \[`refurb`\] Support floating-point base in `redundant-log-base` (`FURB163`) ([#9100](https://github.com/astral-sh/ruff/pull/9100)) +- \[`ruff`\] Detect `unused-asyncio-dangling-task` (`RUF006`) on unused assignments ([#9060](https://github.com/astral-sh/ruff/pull/9060)) + +## 0.1.9 + +### Breaking changes + +- Add site-packages to default exclusions ([#9188](https://github.com/astral-sh/ruff/pull/9188)) + +### Preview features + +- Fix: Avoid parenthesizing subscript targets and values ([#9209](https://github.com/astral-sh/ruff/pull/9209)) +- \[`pylint`\] Implement `too-many-locals` (`PLR0914`) ([#9163](https://github.com/astral-sh/ruff/pull/9163)) +- Implement `reimplemented_operator` (FURB118) ([#9171](https://github.com/astral-sh/ruff/pull/9171)) +- Add a rule to detect string members in runtime-evaluated unions ([#9143](https://github.com/astral-sh/ruff/pull/9143)) +- Implement `no_blank_line_before_class_docstring` preview style ([#9154](https://github.com/astral-sh/ruff/pull/9154)) + +### Rule changes + +- `CONSTANT_CASE` variables are improperly flagged for yoda violation (`SIM300`) ([#9164](https://github.com/astral-sh/ruff/pull/9164)) +- \[`flake8-pyi`\] Cover ParamSpecs and TypeVarTuples (`PYI018`) ([#9198](https://github.com/astral-sh/ruff/pull/9198)) +- \[`flake8-bugbear`\] Add fix for `zip-without-explicit-strict` (`B905`) ([#9176](https://github.com/astral-sh/ruff/pull/9176)) +- Add fix to automatically remove `print` and `pprint` statements (`T201`, `T203`) ([#9208](https://github.com/astral-sh/ruff/pull/9208)) +- Prefer `Never` to `NoReturn` in auto-typing in Python >= 3.11 (`ANN201`) ([#9213](https://github.com/astral-sh/ruff/pull/9213)) + +### Formatter + +- `can_omit_optional_parentheses`: Exit early for unparenthesized expressions ([#9125](https://github.com/astral-sh/ruff/pull/9125)) +- Fix `dynamic` mode with doctests so that it doesn't exceed configured line width ([#9129](https://github.com/astral-sh/ruff/pull/9129)) +- Fix `can_omit_optional_parentheses` for expressions with a right most fstring ([#9124](https://github.com/astral-sh/ruff/pull/9124)) +- Add `target_version` to formatter options ([#9220](https://github.com/astral-sh/ruff/pull/9220)) + +### CLI + +- Update `ruff format --check` to display message for already formatted files ([#9153](https://github.com/astral-sh/ruff/pull/9153)) + +### Bug fixes + +- Reverse order of arguments for `operator.contains` ([#9192](https://github.com/astral-sh/ruff/pull/9192)) +- Iterate over lambdas in deferred type annotations ([#9175](https://github.com/astral-sh/ruff/pull/9175)) +- Fix panic in `D208` with multibyte indent ([#9147](https://github.com/astral-sh/ruff/pull/9147)) +- Add support for `NoReturn` in auto-return-typing ([#9206](https://github.com/astral-sh/ruff/pull/9206)) +- Allow removal of `typing` from `exempt-modules` ([#9214](https://github.com/astral-sh/ruff/pull/9214)) +- Avoid `mutable-class-default` violations for Pydantic subclasses ([#9187](https://github.com/astral-sh/ruff/pull/9187)) +- Fix dropped union expressions for piped non-types in `PYI055` autofix ([#9161](https://github.com/astral-sh/ruff/pull/9161)) +- Enable annotation quoting for multi-line expressions ([#9142](https://github.com/astral-sh/ruff/pull/9142)) +- Deduplicate edits when quoting annotations ([#9140](https://github.com/astral-sh/ruff/pull/9140)) +- Prevent invalid utf8 indexing in cell magic detection ([#9146](https://github.com/astral-sh/ruff/pull/9146)) +- Avoid nested quotations in auto-quoting fix ([#9168](https://github.com/astral-sh/ruff/pull/9168)) +- Add base-class inheritance detection to flake8-django rules ([#9151](https://github.com/astral-sh/ruff/pull/9151)) +- Avoid `asyncio-dangling-task` violations on shadowed bindings ([#9215](https://github.com/astral-sh/ruff/pull/9215)) + +### Documentation + +- Fix blog post URL in changelog ([#9119](https://github.com/astral-sh/ruff/pull/9119)) +- Add error suppression hint for multi-line strings ([#9205](https://github.com/astral-sh/ruff/pull/9205)) +- Fix typo in SemanticModel.parent_expression docstring ([#9167](https://github.com/astral-sh/ruff/pull/9167)) +- Document link between import sorting and formatter ([#9117](https://github.com/astral-sh/ruff/pull/9117)) + +## 0.1.10 + +### Preview features + +- Improve `dummy_implementations` preview style formatting ([#9240](https://github.com/astral-sh/ruff/pull/9240)) +- Normalise Hex and unicode escape sequences in strings ([#9280](https://github.com/astral-sh/ruff/pull/9280)) +- Parenthesize long type annotations in annotated assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210)) +- Parenthesize multi-context managers in `with` statements ([#9222](https://github.com/astral-sh/ruff/pull/9222)) +- \[`flake8-pyi`\] Implement `generator-return-from-iter-method` (`PYI058`) ([#9313](https://github.com/astral-sh/ruff/pull/9313)) +- \[`pylint`\] Implement `empty-comment` (`PLR2044`) ([#9174](https://github.com/astral-sh/ruff/pull/9174)) +- \[`refurb`\] Implement `bit-count` (`FURB161`) ([#9265](https://github.com/astral-sh/ruff/pull/9265)) +- \[`ruff`\] Add `never-union` rule to detect redundant `typing.NoReturn` and `typing.Never` ([#9217](https://github.com/astral-sh/ruff/pull/9217)) + +### CLI + +- Add paths to TOML parse errors ([#9358](https://github.com/astral-sh/ruff/pull/9358)) +- Add row and column numbers to formatter parse errors ([#9321](https://github.com/astral-sh/ruff/pull/9321)) +- Improve responsiveness when invoked via Python ([#9315](https://github.com/astral-sh/ruff/pull/9315)) +- Short rule messages should not end with a period ([#9345](https://github.com/astral-sh/ruff/pull/9345)) + +### Configuration + +- Respect runtime-required decorators on functions ([#9317](https://github.com/astral-sh/ruff/pull/9317)) + +### Bug fixes + +- Avoid `asyncio-dangling-task` for nonlocal and global bindings ([#9263](https://github.com/astral-sh/ruff/pull/9263)) +- Escape trailing placeholders in rule documentation ([#9301](https://github.com/astral-sh/ruff/pull/9301)) +- Fix continuation detection following multi-line strings ([#9332](https://github.com/astral-sh/ruff/pull/9332)) +- Fix scoping for generators in named expressions in classes ([#9248](https://github.com/astral-sh/ruff/pull/9248)) +- Port from obsolete wsl crate to is-wsl ([#9356](https://github.com/astral-sh/ruff/pull/9356)) +- Remove special pre-visit for module docstrings ([#9261](https://github.com/astral-sh/ruff/pull/9261)) +- Respect `__str__` definitions from super classes ([#9338](https://github.com/astral-sh/ruff/pull/9338)) +- Respect `unused-noqa` via `per-file-ignores` ([#9300](https://github.com/astral-sh/ruff/pull/9300)) +- Respect attribute chains when resolving builtin call paths ([#9309](https://github.com/astral-sh/ruff/pull/9309)) +- Treat all `typing_extensions` members as typing aliases ([#9335](https://github.com/astral-sh/ruff/pull/9335)) +- Use `Display` for formatter parse errors ([#9316](https://github.com/astral-sh/ruff/pull/9316)) +- Wrap subscripted dicts in parens for f-string conversion ([#9238](https://github.com/astral-sh/ruff/pull/9238)) +- \[`flake8-annotations`\] Avoid adding return types to stub methods ([#9277](https://github.com/astral-sh/ruff/pull/9277)) +- \[`flake8-annotations`\] Respect mixed `return` and `raise` cases in return-type analysis ([#9310](https://github.com/astral-sh/ruff/pull/9310)) +- \[`flake8-bandit`\] Don't report violations when `SafeLoader` is imported from `yaml.loader` (`S506`) ([#9299](https://github.com/astral-sh/ruff/pull/9299)) +- \[`pylint`\] Avoid panic when comment is preceded by Unicode ([#9331](https://github.com/astral-sh/ruff/pull/9331)) +- \[`pylint`\] Change `PLR0917` error message to match other `PLR09XX` messages ([#9308](https://github.com/astral-sh/ruff/pull/9308)) +- \[`refurb`\] Avoid false positives for `math-constant` (`FURB152`) ([#9290](https://github.com/astral-sh/ruff/pull/9290)) + +### Documentation + +- Expand target name for better rule documentation ([#9302](https://github.com/astral-sh/ruff/pull/9302)) +- Fix typos found by codespell ([#9346](https://github.com/astral-sh/ruff/pull/9346)) +- \[`perflint`\] Document `PERF102` fix un-safety ([#9351](https://github.com/astral-sh/ruff/pull/9351)) +- \[`pyupgrade`\] Document `UP007` fix un-safety ([#9306](https://github.com/astral-sh/ruff/pull/9306)) + +## 0.1.11 + +### Preview features + +- \[`pylint`\] Implement `super-without-brackets` (`W0245`) ([#9257](https://github.com/astral-sh/ruff/pull/9257)) + +### Bug fixes + +- Check path string properly in `python -m ruff` invocations ([#9367](https://github.com/astral-sh/ruff/pull/9367)) + +### Documentation + +- Tweak `relative-imports` message ([#9365](https://github.com/astral-sh/ruff/pull/9365)) +- Add fix safety note for `yield-in-for-loop` ([#9364](https://github.com/astral-sh/ruff/pull/9364)) + +## 0.1.12 + +### Preview features + +- Formatter: Hug multiline-strings in preview style ([#9243](https://github.com/astral-sh/ruff/pull/9243)) +- \[`flake8-bandit`\] Add `ssl-with-no-version` (`S504`) ([#9384](https://github.com/astral-sh/ruff/pull/9384)) +- \[`flake8-bandit`\] Implement `ssl-insecure-version` (`S502`) ([#9390](https://github.com/astral-sh/ruff/pull/9390)) +- \[`flake8-bandit`\] Implement `ssl-with-bad-defaults` (`S503`) ([#9391](https://github.com/astral-sh/ruff/pull/9391)) +- \[`flake8-bandit`\] Implement suspicious import rules (`S4XX`) ([#8831](https://github.com/astral-sh/ruff/pull/8831)) +- \[`flake8-simplify`\] Implement `zip-dict-keys-and-values` (`SIM911`) ([#9460](https://github.com/astral-sh/ruff/pull/9460)) +- \[`pyflakes`\] Add a fix for `redefined-while-unused` (`F811`) ([#9419](https://github.com/astral-sh/ruff/pull/9419)) +- \[`pylint`\] Implement `unnecessary-dunder-call` (`C2801`) ([#9166](https://github.com/astral-sh/ruff/pull/9166)) +- \[`ruff`\] Add `parenthesize-chained-operators` (`RUF021`) to enforce parentheses in `a or b and c` ([#9440](https://github.com/astral-sh/ruff/pull/9440)) + +### Rule changes + +- \[`flake8-boolean-trap`\] Allow Boolean positional arguments in setters ([#9429](https://github.com/astral-sh/ruff/pull/9429)) +- \[`flake8-builtins`\] Restrict `builtin-attribute-shadowing` (`A003`) to actual shadowed references ([#9462](https://github.com/astral-sh/ruff/pull/9462)) +- \[`flake8-pyi`\] Add fix for `generator-return-from-iter-method` (`PYI058`) ([#9355](https://github.com/astral-sh/ruff/pull/9355)) +- \[`pyflakes`\] Don't flag `redefined-while-unused` (`F811`) in `if` branches ([#9418](https://github.com/astral-sh/ruff/pull/9418)) +- \[`pyupgrade`\] Add some additional Python 3.12 typing members to `deprecated-import` ([#9445](https://github.com/astral-sh/ruff/pull/9445)) +- \[`ruff`\] Add fix for `parenthesize-chained-operators` (`RUF021`) ([#9449](https://github.com/astral-sh/ruff/pull/9449)) +- \[`ruff`\] Include subscripts and attributes in static key rule (`RUF011`) ([#9416](https://github.com/astral-sh/ruff/pull/9416)) +- \[`ruff`\] Support variable keys in static dictionary key rule (`RUF011`) ([#9411](https://github.com/astral-sh/ruff/pull/9411)) + +### Formatter + +- Generate deterministic IDs when formatting notebooks ([#9359](https://github.com/astral-sh/ruff/pull/9359)) +- Allow `# fmt: skip` with interspersed same-line comments ([#9395](https://github.com/astral-sh/ruff/pull/9395)) +- Parenthesize breaking named expressions in match guards ([#9396](https://github.com/astral-sh/ruff/pull/9396)) + +### Bug fixes + +- Add cell indexes to all diagnostics ([#9387](https://github.com/astral-sh/ruff/pull/9387)) +- Avoid infinite loop in constant vs. `None` comparisons ([#9376](https://github.com/astral-sh/ruff/pull/9376)) +- Handle raises with implicit alternate branches ([#9377](https://github.com/astral-sh/ruff/pull/9377)) +- Ignore trailing quotes for unclosed l-brace errors ([#9388](https://github.com/astral-sh/ruff/pull/9388)) +- Respect multi-segment submodule imports when resolving qualified names ([#9382](https://github.com/astral-sh/ruff/pull/9382)) +- Use `DisplayParseError` for stdin parser errors ([#9409](https://github.com/astral-sh/ruff/pull/9409)) +- Use `comment_ranges` for isort directive extraction ([#9414](https://github.com/astral-sh/ruff/pull/9414)) +- Use transformed source code for diagnostic locations ([#9408](https://github.com/astral-sh/ruff/pull/9408)) +- \[`flake8-pyi`\] Exclude `warnings.deprecated` and `typing_extensions.deprecated` arguments ([#9423](https://github.com/astral-sh/ruff/pull/9423)) +- \[`flake8-pyi`\] Fix false negative for `unused-private-protocol` (`PYI046`) with unused generic protocols ([#9405](https://github.com/astral-sh/ruff/pull/9405)) +- \[`pydocstyle`\] Disambiguate argument descriptors from section headers ([#9427](https://github.com/astral-sh/ruff/pull/9427)) +- \[`pylint`\] Homogenize `PLR0914` message to match other `PLR09XX` rules ([#9399](https://github.com/astral-sh/ruff/pull/9399)) +- \[`ruff`\] Allow `Hashable = None` in type annotations (`RUF013`) ([#9442](https://github.com/astral-sh/ruff/pull/9442)) + +### Documentation + +- Fix admonition hyperlink colouring ([#9385](https://github.com/astral-sh/ruff/pull/9385)) +- Add missing preview link ([#9386](https://github.com/astral-sh/ruff/pull/9386)) + +## 0.1.13 + +### Bug fixes + +- Include base pyproject when initializing cache settings ([#9480](https://github.com/astral-sh/ruff/pull/9480)) +- \[`flake8-simplify`\] Account for possibly-empty f-string values in truthiness logic ([#9484](https://github.com/astral-sh/ruff/pull/9484)) +- \[`pylint`\] Add the missing period in `unnecessary-dunder-call` ([#9485](https://github.com/astral-sh/ruff/pull/9485)) +- \[`pylint`\] Fix `__aenter__` message in `unnecessary-dunder-call` ([#9492](https://github.com/astral-sh/ruff/pull/9492)) + +## 0.1.14 + +### Preview features + +- \[`flake8-bugbear`\] Add fix for `duplicate-value` (`B033`) ([#9510](https://github.com/astral-sh/ruff/pull/9510)) +- \[`flake8-simplify`\] Implement `enumerate-for-loop` (`SIM113`) ([#7777](https://github.com/astral-sh/ruff/pull/7777)) +- \[`pygrep_hooks`\] Add fix for `deprecated-log-warn` (`PGH002`) ([#9519](https://github.com/astral-sh/ruff/pull/9519)) +- \[`pylint`\] Implement `import-private-name` (`C2701`) ([#5920](https://github.com/astral-sh/ruff/pull/5920)) +- \[`refurb`\] Implement `regex-flag-alias` with fix (`FURB167`) ([#9516](https://github.com/astral-sh/ruff/pull/9516)) +- \[`ruff`\] Add rule and fix to sort contents of `__all__` (`RUF022`) ([#9474](https://github.com/astral-sh/ruff/pull/9474)) +- \[`tryceratops`\] Add fix for `error-instead-of-exception` (`TRY400`) ([#9520](https://github.com/astral-sh/ruff/pull/9520)) + +### Rule changes + +- \[`flake8-pyi`\] Fix `PYI047` false negatives on PEP-695 type aliases ([#9566](https://github.com/astral-sh/ruff/pull/9566)) +- \[`flake8-pyi`\] Fix `PYI049` false negatives on call-based `TypedDict`s ([#9567](https://github.com/astral-sh/ruff/pull/9567)) +- \[`pylint`\] Exclude `self` and `cls` when counting method arguments (`PLR0917`) ([#9563](https://github.com/astral-sh/ruff/pull/9563)) + +### CLI + +- `--show-settings` displays active settings in a far more readable format ([#9464](https://github.com/astral-sh/ruff/pull/9464)) +- Add `--extension` support to the formatter ([#9483](https://github.com/astral-sh/ruff/pull/9483)) + +### Configuration + +- Ignore preview status for fixable and unfixable selectors ([#9538](https://github.com/astral-sh/ruff/pull/9538)) +- \[`pycodestyle`\] Use the configured tab size when expanding indents ([#9506](https://github.com/astral-sh/ruff/pull/9506)) + +### Bug fixes + +- Recursively visit deferred AST nodes ([#9541](https://github.com/astral-sh/ruff/pull/9541)) +- Visit deferred lambdas before type definitions ([#9540](https://github.com/astral-sh/ruff/pull/9540)) +- \[`flake8-simplify`\] Avoid some more `enumerate-for-loop` false positives (`SIM113`) ([#9515](https://github.com/astral-sh/ruff/pull/9515)) +- \[`pandas-vet`\] Limit inplace diagnostics to methods that accept inplace ([#9495](https://github.com/astral-sh/ruff/pull/9495)) +- \[`pylint`\] Add the `__prepare__` method to the list of recognized dunder method ([#9529](https://github.com/astral-sh/ruff/pull/9529)) +- \[`pylint`\] Ignore unnecessary dunder calls within dunder definitions ([#9496](https://github.com/astral-sh/ruff/pull/9496)) +- \[`refurb`\] Avoid bailing when `reimplemented-operator` is called on function (`FURB118`) ([#9556](https://github.com/astral-sh/ruff/pull/9556)) +- \[`ruff`\] Avoid treating named expressions as static keys (`RUF011`) ([#9494](https://github.com/astral-sh/ruff/pull/9494)) + +### Documentation + +- Add instructions on using `noqa` with isort rules ([#9555](https://github.com/astral-sh/ruff/pull/9555)) +- Documentation update for URL giving 'page not found' ([#9565](https://github.com/astral-sh/ruff/pull/9565)) +- Fix admonition in dark mode ([#9502](https://github.com/astral-sh/ruff/pull/9502)) +- Update contributing docs to use `cargo bench -p ruff_benchmark` ([#9535](https://github.com/astral-sh/ruff/pull/9535)) +- Update emacs integration section to include `emacs-ruff-format` ([#9403](https://github.com/astral-sh/ruff/pull/9403)) +- \[`flake8-blind-except`\] Document exceptions to `blind-except` rule ([#9580](https://github.com/astral-sh/ruff/pull/9580)) + +## 0.1.15 + +### Preview features + +- Error when `NURSERY` selector is used with `--preview` ([#9682](https://github.com/astral-sh/ruff/pull/9682)) +- Preserve indentation around multiline strings in formatter ([#9637](https://github.com/astral-sh/ruff/pull/9637)) +- \[`flake8-return`\] Add fixes for all rules (`RET505`, `RET506`, `RET507`, `RET508`) ([#9595](https://github.com/astral-sh/ruff/pull/9595)) +- \[`flake8-simplify`\] Add fix for `if-with-same-arms` (`SIM114`) ([#9591](https://github.com/astral-sh/ruff/pull/9591)) +- \[`pycodestyle`\] Add fix for `multiple-imports-on-one-line` (`E401`) ([#9518](https://github.com/astral-sh/ruff/pull/9518)) +- \[`pylint`\] Add fix for `collapsible-else-if` (`PLR5501`) ([#9594](https://github.com/astral-sh/ruff/pull/9594)) +- \[`pylint`\] Add fix for `useless-else-on-loop` (`PLW0120`) ([#9590](https://github.com/astral-sh/ruff/pull/9590)) +- \[`pylint`\] Implement `assigning-non-slot` (`E0237`) ([#9623](https://github.com/astral-sh/ruff/pull/9623)) +- \[`pylint`\] Implement `potential-index-error` (`PLE0643`) ([#9545](https://github.com/astral-sh/ruff/pull/9545)) +- \[`pylint`\] Implement `too-many-nested-blocks` (`PLR1702`) ([#9172](https://github.com/astral-sh/ruff/pull/9172)) +- \[`ruff`\] Add rule to sort `__slots__` and `__match_args__` ([#9564](https://github.com/astral-sh/ruff/pull/9564)) +- \[`ruff`\] Detect unnecessary `dict` comprehensions for iterables (`RUF025`) ([#9613](https://github.com/astral-sh/ruff/pull/9613)) +- \[`ruff`\] Guard against use of `default_factory` as a keyword argument (`RUF026`) ([#9651](https://github.com/astral-sh/ruff/pull/9651)) +- \[`ruff`\] Implement `mutable-fromkeys-value` (`RUF024`) ([#9597](https://github.com/astral-sh/ruff/pull/9597)) + +### CLI + +- Enable auto-wrapping of `--help` output ([#9633](https://github.com/astral-sh/ruff/pull/9633)) + +### Bug fixes + +- Avoid rendering display-only rules as fixable ([#9649](https://github.com/astral-sh/ruff/pull/9649)) +- Detect automagic-like assignments in notebooks ([#9653](https://github.com/astral-sh/ruff/pull/9653)) +- Generate custom JSON schema for dynamic setting ([#9632](https://github.com/astral-sh/ruff/pull/9632)) +- \[`flake8-no-pep420`\] Include global `--config` when determining namespace packages ([#9603](https://github.com/astral-sh/ruff/pull/9603)) +- \[`flake8-pie`\] Omit bound tuples passed to `.startswith` or `.endswith` ([#9661](https://github.com/astral-sh/ruff/pull/9661)) +- \[`flake8-return`\] Avoid panic when fixing inlined else blocks ([#9657](https://github.com/astral-sh/ruff/pull/9657)) +- \[`flake8-return`\] Consider exception suppression in unnecessary assignment ([#9673](https://github.com/astral-sh/ruff/pull/9673)) +- \[`flake8-return`\] Take `NoReturn` annotation into account when analyzing implicit returns ([#9636](https://github.com/astral-sh/ruff/pull/9636)) +- \[`flake8-simplify`\] Support inverted returns in `needless-bool` (`SIM103`) ([#9619](https://github.com/astral-sh/ruff/pull/9619)) +- \[`flake8-type-checking`\] Add Pydantic's `BaseConfig` to default-copy list ([#9650](https://github.com/astral-sh/ruff/pull/9650)) +- \[`flake8-type-checking`\] Avoid marking `InitVar` as a typing-only annotation ([#9688](https://github.com/astral-sh/ruff/pull/9688)) +- \[`pycodestyle`\] Allow `dtype` comparisons in `type-comparison` ([#9676](https://github.com/astral-sh/ruff/pull/9676)) +- \[`pydocstyle`\] Re-implement `last-line-after-section` (`D413`) ([#9654](https://github.com/astral-sh/ruff/pull/9654)) + +### Documentation + +- \[`flake8-pytest-style`\] Add fix safety documentation for `duplicate-parameterize-test-cases` ([#9678](https://github.com/astral-sh/ruff/pull/9678)) +- \[`pylint`\] Document `literal-membership` fix safety conditions ([#9677](https://github.com/astral-sh/ruff/pull/9677)) +- \[`isort`\] Fix reference to `isort` rule code ([#9598](https://github.com/astral-sh/ruff/pull/9598)) diff --git a/changelogs/0.10.x.md b/changelogs/0.10.x.md new file mode 100644 index 0000000000000..b0ace96f344e0 --- /dev/null +++ b/changelogs/0.10.x.md @@ -0,0 +1,101 @@ +# Changelog 0.10.x + +## 0.10.0 + +Check out the [blog post](https://astral.sh/blog/ruff-v0.10.0) for a migration guide and overview of the changes! + +### Breaking changes + +See also, the "Remapped rules" section which may result in disabled rules. + +- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319)) + + Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0. + You can find a description of this change in the 0.11.0 section. + +- **Updated `TYPE_CHECKING` behavior** ([#16669](https://github.com/astral-sh/ruff/pull/16669)) + + Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead. + +- **More robust noqa parsing** ([#16483](https://github.com/astral-sh/ruff/pull/16483)) + + The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [_Error suppression_](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification. + +- **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#14005](https://github.com/astral-sh/ruff/pull/14005)) + + This change fixes a bug in the formatter where it introduced unnecessary parentheses around with statements with a single context manager and a trailing comment. This change may result in a change in formatting for some users. + +- **Bump alpine default tag to 3.21 for derived Docker images** ([#16456](https://github.com/astral-sh/ruff/pull/16456)) + + Alpine 3.21 was released in Dec 2024 and is used in the official Alpine-based Python images. Now the ruff:alpine image will use 3.21 instead of 3.20 and ruff:alpine3.20 will no longer be updated. + +### Deprecated Rules + +The following rules have been deprecated: + +- [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance/) (`UP038`) +- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/) (`S320`) + +### Remapped rules + +The following rules have been remapped to new rule codes: + +- \[`unsafe-markup-use`\]: `RUF035` to `S704` + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`batched-without-explicit-strict`](https://docs.astral.sh/ruff/rules/batched-without-explicit-strict) (`B911`) +- [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable) (`C420`) +- [`datetime-min-max`](https://docs.astral.sh/ruff/rules/datetime-min-max) (`DTZ901`) +- [`fast-api-unused-path-parameter`](https://docs.astral.sh/ruff/rules/fast-api-unused-path-parameter) (`FAST003`) +- [`root-logger-call`](https://docs.astral.sh/ruff/rules/root-logger-call) (`LOG015`) +- [`len-test`](https://docs.astral.sh/ruff/rules/len-test) (`PLC1802`) +- [`shallow-copy-environ`](https://docs.astral.sh/ruff/rules/shallow-copy-environ) (`PLW1507`) +- [`os-listdir`](https://docs.astral.sh/ruff/rules/os-listdir) (`PTH208`) +- [`invalid-pathlib-with-suffix`](https://docs.astral.sh/ruff/rules/invalid-pathlib-with-suffix) (`PTH210`) +- [`invalid-assert-message-literal-argument`](https://docs.astral.sh/ruff/rules/invalid-assert-message-literal-argument) (`RUF040`) +- [`unnecessary-nested-literal`](https://docs.astral.sh/ruff/rules/unnecessary-nested-literal) (`RUF041`) +- [`unnecessary-cast-to-int`](https://docs.astral.sh/ruff/rules/unnecessary-cast-to-int) (`RUF046`) +- [`map-int-version-parsing`](https://docs.astral.sh/ruff/rules/map-int-version-parsing) (`RUF048`) +- [`if-key-in-dict-del`](https://docs.astral.sh/ruff/rules/if-key-in-dict-del) (`RUF051`) +- [`unsafe-markup-use`](https://docs.astral.sh/ruff/rules/unsafe-markup-use) (`S704`). This rule has also been renamed from `RUF035`. +- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string) (`SIM905`) +- [`runtime-cast-value`](https://docs.astral.sh/ruff/rules/runtime-cast-value) (`TC006`) +- [`unquoted-type-alias`](https://docs.astral.sh/ruff/rules/unquoted-type-alias) (`TC007`) +- [`non-pep646-unpack`](https://docs.astral.sh/ruff/rules/non-pep646-unpack) (`UP044`) + +The following behaviors have been stabilized: + +- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) [`invalid-first-argument-name-for-class-method`](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/) (`N804`): `__new__` methods are now no longer flagged by `invalid-first-argument-name-for-class-method` (`N804`) but instead by `bad-staticmethod-argument` (`PLW0211`) +- [`bad-str-strip-call`](https://docs.astral.sh/ruff/rules/bad-str-strip-call/) (`PLE1310`): The rule now applies to objects which are known to have type `str` or `bytes`. +- [`custom-type-var-for-self`](https://docs.astral.sh/ruff/rules/custom-type-var-for-self/) (`PYI019`): More accurate detection of custom `TypeVars` replaceable by `Self`. The range of the diagnostic is now the full function header rather than just the return annotation. +- [`invalid-argument-name`](https://docs.astral.sh/ruff/rules/invalid-argument-name/) (`N803`): Ignore argument names of functions decorated with `typing.override` +- [`invalid-envvar-default`](https://docs.astral.sh/ruff/rules/invalid-envvar-default/) (`PLW1508`): Detect default value arguments to `os.environ.get` with invalid type. +- [`pytest-raises-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-raises-with-multiple-statements/) (`PT012`) [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements/) (`PT031`): Allow `for` statements with an empty body in `pytest.raises` and `pytest.warns` `with` statements. +- [`redundant-open-modes`](https://docs.astral.sh/ruff/rules/redundant-open-modes/) (`UP015`): The diagnostic range is now the range of the redundant mode argument where it previously was the range of the entire open call. You may have to replace your `noqa` comments when suppressing `UP015`. +- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`): Changes the default value of `lint.flake8-builtins.strict-checking` from `true` to `false`. +- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`): Now also recognizes `type(expr) is type(None)` comparisons where `expr` isn't a name expression. + +The following fixes or improvements to fixes have been stabilized: + +- [`repeated-equality-comparison`](https://docs.astral.sh/ruff/rules/repeated-equality-comparison/) (`PLR1714`) ([#16685](https://github.com/astral-sh/ruff/pull/16685)) +- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) ([#16684](https://github.com/astral-sh/ruff/pull/16684)) +- [`unused-private-type-var`](https://docs.astral.sh/ruff/rules/unused-private-type-var/) (`PYI018`) ([#16682](https://github.com/astral-sh/ruff/pull/16682)) + +### Server + +- Remove logging output for `ruff.printDebugInformation` ([#16617](https://github.com/astral-sh/ruff/pull/16617)) + +### Configuration + +- \[`flake8-builtins`\] Deprecate the `builtins-` prefixed options in favor of the unprefixed options (e.g. `builtins-allowed-modules` is now deprecated in favor of `allowed-modules`) ([#16092](https://github.com/astral-sh/ruff/pull/16092)) + +### Bug fixes + +- [flake8-bandit] Fix mixed-case hash algorithm names (S324) ([#16552](https://github.com/astral-sh/ruff/pull/16552)) + +### CLI + +- [ruff] Fix `last_tag`/`commits_since_last_tag` for `version` command ([#16686](https://github.com/astral-sh/ruff/pull/16686)) diff --git a/changelogs/0.11.x.md b/changelogs/0.11.x.md new file mode 100644 index 0000000000000..53bb0560f6533 --- /dev/null +++ b/changelogs/0.11.x.md @@ -0,0 +1,373 @@ +# Changelog 0.11.x + +## 0.11.0 + +This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`. + +### Breaking changes + +- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319)) + + In previous versions of Ruff, you could specify your Python version with: + + - The `target-version` option in a `ruff.toml` file or the `[tool.ruff]` section of a pyproject.toml file. + - The `project.requires-python` field in a `pyproject.toml` file with a `[tool.ruff]` section. + + These options worked well in most cases, and are still recommended for fine control of the Python version. However, because of the way Ruff discovers config files, `pyproject.toml` files without a `[tool.ruff]` section would be ignored, including the `requires-python` setting. Ruff would then use the default Python version (3.9 as of this writing) instead, which is surprising when you've attempted to request another version. + + In v0.10, config discovery has been updated to address this issue: + + - If Ruff finds a `ruff.toml` file without a `target-version`, it will check + for a `pyproject.toml` file in the same directory and respect its + `requires-python` version, even if it does not contain a `[tool.ruff]` + section. + - If Ruff finds a user-level configuration, the `requires-python` field of the closest `pyproject.toml` in a parent directory will take precedence. + - If there is no config file (`ruff.toml`or `pyproject.toml` with a + `[tool.ruff]` section) in the directory of the file being checked, Ruff will + search for the closest `pyproject.toml` in the parent directories and use its + `requires-python` setting. + +### Stabilization + +The following behaviors have been stabilized: + +- [`blanket-noqa`](https://docs.astral.sh/ruff/rules/blanket-noqa/) (`PGH004`): Also detect blanked file-level noqa comments (and not just line level comments). + +### Preview features + +- [syntax-errors] Tuple unpacking in `for` statement iterator clause before Python 3.9 ([#16558](https://github.com/astral-sh/ruff/pull/16558)) + +## 0.11.1 + +### Preview features + +- \[`airflow`\] Add `chain`, `chain_linear` and `cross_downstream` for `AIR302` ([#16647](https://github.com/astral-sh/ruff/pull/16647)) +- [syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors ([#16581](https://github.com/astral-sh/ruff/pull/16581)) +- [syntax-errors] PEP 701 f-strings before Python 3.12 ([#16543](https://github.com/astral-sh/ruff/pull/16543)) +- [syntax-errors] Parenthesized context managers before Python 3.9 ([#16523](https://github.com/astral-sh/ruff/pull/16523)) +- [syntax-errors] Star annotations before Python 3.11 ([#16545](https://github.com/astral-sh/ruff/pull/16545)) +- [syntax-errors] Star expression in index before Python 3.11 ([#16544](https://github.com/astral-sh/ruff/pull/16544)) +- [syntax-errors] Unparenthesized assignment expressions in sets and indexes ([#16404](https://github.com/astral-sh/ruff/pull/16404)) + +### Bug fixes + +- Server: Allow `FixAll` action in presence of version-specific syntax errors ([#16848](https://github.com/astral-sh/ruff/pull/16848)) +- \[`flake8-bandit`\] Allow raw strings in `suspicious-mark-safe-usage` (`S308`) #16702 ([#16770](https://github.com/astral-sh/ruff/pull/16770)) +- \[`refurb`\] Avoid panicking `unwrap` in `verbose-decimal-constructor` (`FURB157`) ([#16777](https://github.com/astral-sh/ruff/pull/16777)) +- \[`refurb`\] Fix starred expressions fix (`FURB161`) ([#16550](https://github.com/astral-sh/ruff/pull/16550)) +- Fix `--statistics` reporting for unsafe fixes ([#16756](https://github.com/astral-sh/ruff/pull/16756)) + +### Rule changes + +- \[`flake8-executables`\] Allow `uv run` in shebang line for `shebang-missing-python` (`EXE003`) ([#16849](https://github.com/astral-sh/ruff/pull/16849),[#16855](https://github.com/astral-sh/ruff/pull/16855)) + +### CLI + +- Add `--exit-non-zero-on-format` ([#16009](https://github.com/astral-sh/ruff/pull/16009)) + +### Documentation + +- Update Ruff tutorial to avoid non-existent fix in `__init__.py` ([#16818](https://github.com/astral-sh/ruff/pull/16818)) +- \[`flake8-gettext`\] Swap `format-` and `printf-in-get-text-func-call` examples (`INT002`, `INT003`) ([#16769](https://github.com/astral-sh/ruff/pull/16769)) + +## 0.11.2 + +### Preview features + +- [syntax-errors] Fix false-positive syntax errors emitted for annotations on variadic parameters before Python 3.11 ([#16878](https://github.com/astral-sh/ruff/pull/16878)) + +## 0.11.3 + +### Preview features + +- \[`airflow`\] Add more autofixes for `AIR302` ([#16876](https://github.com/astral-sh/ruff/pull/16876), [#16977](https://github.com/astral-sh/ruff/pull/16977), [#16976](https://github.com/astral-sh/ruff/pull/16976), [#16965](https://github.com/astral-sh/ruff/pull/16965)) +- \[`airflow`\] Move `AIR301` to `AIR002` ([#16978](https://github.com/astral-sh/ruff/pull/16978)) +- \[`airflow`\] Move `AIR302` to `AIR301` and `AIR303` to `AIR302` ([#17151](https://github.com/astral-sh/ruff/pull/17151)) +- \[`flake8-bandit`\] Mark `str` and `list[str]` literals as trusted input (`S603`) ([#17136](https://github.com/astral-sh/ruff/pull/17136)) +- \[`ruff`\] Support slices in `RUF005` ([#17078](https://github.com/astral-sh/ruff/pull/17078)) +- [syntax-errors] Start detecting compile-time syntax errors ([#16106](https://github.com/astral-sh/ruff/pull/16106)) +- [syntax-errors] Duplicate type parameter names ([#16858](https://github.com/astral-sh/ruff/pull/16858)) +- [syntax-errors] Irrefutable `case` pattern before final case ([#16905](https://github.com/astral-sh/ruff/pull/16905)) +- [syntax-errors] Multiple assignments in `case` pattern ([#16957](https://github.com/astral-sh/ruff/pull/16957)) +- [syntax-errors] Single starred assignment target ([#17024](https://github.com/astral-sh/ruff/pull/17024)) +- [syntax-errors] Starred expressions in `return`, `yield`, and `for` ([#17134](https://github.com/astral-sh/ruff/pull/17134)) +- [syntax-errors] Store to or delete `__debug__` ([#16984](https://github.com/astral-sh/ruff/pull/16984)) + +### Bug fixes + +- Error instead of `panic!` when running Ruff from a deleted directory (#16903) ([#17054](https://github.com/astral-sh/ruff/pull/17054)) +- [syntax-errors] Fix false positive for parenthesized tuple index ([#16948](https://github.com/astral-sh/ruff/pull/16948)) + +### CLI + +- Check `pyproject.toml` correctly when it is passed via stdin ([#16971](https://github.com/astral-sh/ruff/pull/16971)) + +### Configuration + +- \[`flake8-import-conventions`\] Add import `numpy.typing as npt` to default `flake8-import-conventions.aliases` ([#17133](https://github.com/astral-sh/ruff/pull/17133)) + +### Documentation + +- \[`refurb`\] Document why `UserDict`, `UserList`, and `UserString` are preferred over `dict`, `list`, and `str` (`FURB189`) ([#16927](https://github.com/astral-sh/ruff/pull/16927)) + +## 0.11.4 + +### Preview features + +- \[`ruff`\] Implement `invalid-rule-code` as `RUF102` ([#17138](https://github.com/astral-sh/ruff/pull/17138)) +- [syntax-errors] Detect duplicate keys in `match` mapping patterns ([#17129](https://github.com/astral-sh/ruff/pull/17129)) +- [syntax-errors] Detect duplicate attributes in `match` class patterns ([#17186](https://github.com/astral-sh/ruff/pull/17186)) +- [syntax-errors] Detect invalid syntax in annotations ([#17101](https://github.com/astral-sh/ruff/pull/17101)) + +### Bug fixes + +- [syntax-errors] Fix multiple assignment error for class fields in `match` patterns ([#17184](https://github.com/astral-sh/ruff/pull/17184)) +- Don't skip visiting non-tuple slice in `typing.Annotated` subscripts ([#17201](https://github.com/astral-sh/ruff/pull/17201)) + +## 0.11.5 + +### Preview features + +- \[`airflow`\] Add missing `AIR302` attribute check ([#17115](https://github.com/astral-sh/ruff/pull/17115)) +- \[`airflow`\] Expand module path check to individual symbols (`AIR302`) ([#17278](https://github.com/astral-sh/ruff/pull/17278)) +- \[`airflow`\] Extract `AIR312` from `AIR302` rules (`AIR302`, `AIR312`) ([#17152](https://github.com/astral-sh/ruff/pull/17152)) +- \[`airflow`\] Update outdated `AIR301`, `AIR302` rules ([#17123](https://github.com/astral-sh/ruff/pull/17123)) +- [syntax-errors] Async comprehension in sync comprehension ([#17177](https://github.com/astral-sh/ruff/pull/17177)) +- [syntax-errors] Check annotations in annotated assignments ([#17283](https://github.com/astral-sh/ruff/pull/17283)) +- [syntax-errors] Extend annotation checks to `await` ([#17282](https://github.com/astral-sh/ruff/pull/17282)) + +### Bug fixes + +- \[`flake8-pie`\] Avoid false positive for multiple assignment with `auto()` (`PIE796`) ([#17274](https://github.com/astral-sh/ruff/pull/17274)) + +### Rule changes + +- \[`ruff`\] Fix `RUF100` to detect unused file-level `noqa` directives with specific codes (#17042) ([#17061](https://github.com/astral-sh/ruff/pull/17061)) +- \[`flake8-pytest-style`\] Avoid false positive for legacy form of `pytest.raises` (`PT011`) ([#17231](https://github.com/astral-sh/ruff/pull/17231)) + +### Documentation + +- Fix formatting of "See Style Guide" link ([#17272](https://github.com/astral-sh/ruff/pull/17272)) + +## 0.11.6 + +### Preview features + +- Avoid adding whitespace to the end of a docstring after an escaped quote ([#17216](https://github.com/astral-sh/ruff/pull/17216)) +- \[`airflow`\] Extract `AIR311` from `AIR301` rules (`AIR301`, `AIR311`) ([#17310](https://github.com/astral-sh/ruff/pull/17310), [#17422](https://github.com/astral-sh/ruff/pull/17422)) + +### Bug fixes + +- Raise syntax error when `\` is at end of file ([#17409](https://github.com/astral-sh/ruff/pull/17409)) + +## 0.11.7 + +### Preview features + +- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR301`) ([#17355](https://github.com/astral-sh/ruff/pull/17355)) +- \[`perflint`\] Implement fix for `manual-dict-comprehension` (`PERF403`) ([#16719](https://github.com/astral-sh/ruff/pull/16719)) +- [syntax-errors] Make duplicate parameter names a semantic error ([#17131](https://github.com/astral-sh/ruff/pull/17131)) + +### Bug fixes + +- \[`airflow`\] Fix typos in provider package names (`AIR302`, `AIR312`) ([#17574](https://github.com/astral-sh/ruff/pull/17574)) +- \[`flake8-type-checking`\] Visit keyword arguments in checks involving `typing.cast`/`typing.NewType` arguments ([#17538](https://github.com/astral-sh/ruff/pull/17538)) +- \[`pyupgrade`\] Preserve parenthesis when fixing native literals containing newlines (`UP018`) ([#17220](https://github.com/astral-sh/ruff/pull/17220)) +- \[`refurb`\] Mark the `FURB161` fix unsafe except for integers and booleans ([#17240](https://github.com/astral-sh/ruff/pull/17240)) + +### Rule changes + +- \[`perflint`\] Allow list function calls to be replaced with a comprehension (`PERF401`) ([#17519](https://github.com/astral-sh/ruff/pull/17519)) +- \[`pycodestyle`\] Auto-fix redundant boolean comparison (`E712`) ([#17090](https://github.com/astral-sh/ruff/pull/17090)) +- \[`pylint`\] make fix unsafe if delete comments (`PLR1730`) ([#17459](https://github.com/astral-sh/ruff/pull/17459)) + +### Documentation + +- Add fix safety sections to docs for several rules ([#17410](https://github.com/astral-sh/ruff/pull/17410),[#17440](https://github.com/astral-sh/ruff/pull/17440),[#17441](https://github.com/astral-sh/ruff/pull/17441),[#17443](https://github.com/astral-sh/ruff/pull/17443),[#17444](https://github.com/astral-sh/ruff/pull/17444)) + +## 0.11.8 + +### Preview features + +- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR302`, `AIR311`) ([#17553](https://github.com/astral-sh/ruff/pull/17553), [#17570](https://github.com/astral-sh/ruff/pull/17570), [#17571](https://github.com/astral-sh/ruff/pull/17571)) +- \[`airflow`\] Extend `AIR301` rule ([#17598](https://github.com/astral-sh/ruff/pull/17598)) +- \[`airflow`\] Update existing `AIR302` rules with better suggestions ([#17542](https://github.com/astral-sh/ruff/pull/17542)) +- \[`refurb`\] Mark fix as safe for `readlines-in-for` (`FURB129`) ([#17644](https://github.com/astral-sh/ruff/pull/17644)) +- [syntax-errors] `nonlocal` declaration at module level ([#17559](https://github.com/astral-sh/ruff/pull/17559)) +- [syntax-errors] Detect single starred expression assignment `x = *y` ([#17624](https://github.com/astral-sh/ruff/pull/17624)) + +### Bug fixes + +- \[`flake8-pyi`\] Ensure `Literal[None,] | Literal[None,]` is not autofixed to `None | None` (`PYI061`) ([#17659](https://github.com/astral-sh/ruff/pull/17659)) +- \[`flake8-use-pathlib`\] Avoid suggesting `Path.iterdir()` for `os.listdir` with file descriptor (`PTH208`) ([#17715](https://github.com/astral-sh/ruff/pull/17715)) +- \[`flake8-use-pathlib`\] Fix `PTH104` false positive when `rename` is passed a file descriptor ([#17712](https://github.com/astral-sh/ruff/pull/17712)) +- \[`flake8-use-pathlib`\] Fix `PTH116` false positive when `stat` is passed a file descriptor ([#17709](https://github.com/astral-sh/ruff/pull/17709)) +- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor from a function call ([#17705](https://github.com/astral-sh/ruff/pull/17705)) +- \[`pycodestyle`\] Fix duplicated diagnostic in `E712` ([#17651](https://github.com/astral-sh/ruff/pull/17651)) +- \[`pylint`\] Detect `global` declarations in module scope (`PLE0118`) ([#17411](https://github.com/astral-sh/ruff/pull/17411)) +- [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific ([#17460](https://github.com/astral-sh/ruff/pull/17460)) + +### Configuration + +- Add option to disable `typing_extensions` imports ([#17611](https://github.com/astral-sh/ruff/pull/17611)) + +### Documentation + +- Fix example syntax for the `lint.pydocstyle.ignore-var-parameters` option ([#17740](https://github.com/astral-sh/ruff/pull/17740)) +- Add fix safety sections (`ASYNC116`, `FLY002`, `D200`, `RUF005`, `RUF017`, `RUF027`, `RUF028`, `RUF057`) ([#17497](https://github.com/astral-sh/ruff/pull/17497), [#17496](https://github.com/astral-sh/ruff/pull/17496), [#17502](https://github.com/astral-sh/ruff/pull/17502), [#17484](https://github.com/astral-sh/ruff/pull/17484), [#17480](https://github.com/astral-sh/ruff/pull/17480), [#17485](https://github.com/astral-sh/ruff/pull/17485), [#17722](https://github.com/astral-sh/ruff/pull/17722), [#17483](https://github.com/astral-sh/ruff/pull/17483)) + +### Other changes + +- Add Python 3.14 to configuration options ([#17647](https://github.com/astral-sh/ruff/pull/17647)) +- Make syntax error for unparenthesized except tuples version specific to before 3.14 ([#17660](https://github.com/astral-sh/ruff/pull/17660)) + +## 0.11.9 + +### Preview features + +- Default to latest supported Python version for version-related syntax errors ([#17529](https://github.com/astral-sh/ruff/pull/17529)) +- Implement deferred annotations for Python 3.14 ([#17658](https://github.com/astral-sh/ruff/pull/17658)) +- \[`airflow`\] Fix `SQLTableCheckOperator` typo (`AIR302`) ([#17946](https://github.com/astral-sh/ruff/pull/17946)) +- \[`airflow`\] Remove `airflow.utils.dag_parsing_context.get_parsing_context` (`AIR301`) ([#17852](https://github.com/astral-sh/ruff/pull/17852)) +- \[`airflow`\] Skip attribute check in try catch block (`AIR301`) ([#17790](https://github.com/astral-sh/ruff/pull/17790)) +- \[`flake8-bandit`\] Mark tuples of string literals as trusted input in `S603` ([#17801](https://github.com/astral-sh/ruff/pull/17801)) +- \[`isort`\] Check full module path against project root(s) when categorizing first-party imports ([#16565](https://github.com/astral-sh/ruff/pull/16565)) +- \[`ruff`\] Add new rule `in-empty-collection` (`RUF060`) ([#16480](https://github.com/astral-sh/ruff/pull/16480)) + +### Bug fixes + +- Fix missing `combine` call for `lint.typing-extensions` setting ([#17823](https://github.com/astral-sh/ruff/pull/17823)) +- \[`flake8-async`\] Fix module name in `ASYNC110`, `ASYNC115`, and `ASYNC116` fixes ([#17774](https://github.com/astral-sh/ruff/pull/17774)) +- \[`pyupgrade`\] Add spaces between tokens as necessary to avoid syntax errors in `UP018` autofix ([#17648](https://github.com/astral-sh/ruff/pull/17648)) +- \[`refurb`\] Fix false positive for float and complex numbers in `FURB116` ([#17661](https://github.com/astral-sh/ruff/pull/17661)) +- [parser] Flag single unparenthesized generator expr with trailing comma in arguments. ([#17893](https://github.com/astral-sh/ruff/pull/17893)) + +### Documentation + +- Add instructions on how to upgrade to a newer Rust version ([#17928](https://github.com/astral-sh/ruff/pull/17928)) +- Update code of conduct email address ([#17875](https://github.com/astral-sh/ruff/pull/17875)) +- Add fix safety sections to `PLC2801`, `PLR1722`, and `RUF013` ([#17825](https://github.com/astral-sh/ruff/pull/17825), [#17826](https://github.com/astral-sh/ruff/pull/17826), [#17759](https://github.com/astral-sh/ruff/pull/17759)) +- Add link to `check-typed-exception` from `S110` and `S112` ([#17786](https://github.com/astral-sh/ruff/pull/17786)) + +### Other changes + +- Allow passing a virtual environment to `ruff analyze graph` ([#17743](https://github.com/astral-sh/ruff/pull/17743)) + +## 0.11.10 + +### Preview features + +- \[`ruff`\] Implement a recursive check for `RUF060` ([#17976](https://github.com/astral-sh/ruff/pull/17976)) +- \[`airflow`\] Enable autofixes for `AIR301` and `AIR311` ([#17941](https://github.com/astral-sh/ruff/pull/17941)) +- \[`airflow`\] Apply try catch guard to all `AIR3` rules ([#17887](https://github.com/astral-sh/ruff/pull/17887)) +- \[`airflow`\] Extend `AIR311` rules ([#17913](https://github.com/astral-sh/ruff/pull/17913)) + +### Bug fixes + +- \[`flake8-bugbear`\] Ignore `B028` if `skip_file_prefixes` is present ([#18047](https://github.com/astral-sh/ruff/pull/18047)) +- \[`flake8-pie`\] Mark autofix for `PIE804` as unsafe if the dictionary contains comments ([#18046](https://github.com/astral-sh/ruff/pull/18046)) +- \[`flake8-simplify`\] Correct behavior for `str.split`/`rsplit` with `maxsplit=0` (`SIM905`) ([#18075](https://github.com/astral-sh/ruff/pull/18075)) +- \[`flake8-simplify`\] Fix `SIM905` autofix for `rsplit` creating a reversed list literal ([#18045](https://github.com/astral-sh/ruff/pull/18045)) +- \[`flake8-use-pathlib`\] Suppress diagnostics for all `os.*` functions that have the `dir_fd` parameter (`PTH`) ([#17968](https://github.com/astral-sh/ruff/pull/17968)) +- \[`refurb`\] Mark autofix as safe only for number literals (`FURB116`) ([#17692](https://github.com/astral-sh/ruff/pull/17692)) + +### Rule changes + +- \[`flake8-bandit`\] Skip `S608` for expressionless f-strings ([#17999](https://github.com/astral-sh/ruff/pull/17999)) +- \[`flake8-pytest-style`\] Don't recommend `usefixtures` for `parametrize` values (`PT019`) ([#17650](https://github.com/astral-sh/ruff/pull/17650)) +- \[`pyupgrade`\] Add `resource.error` as deprecated alias of `OSError` (`UP024`) ([#17933](https://github.com/astral-sh/ruff/pull/17933)) + +### CLI + +- Disable jemalloc on Android ([#18033](https://github.com/astral-sh/ruff/pull/18033)) + +### Documentation + +- Update Neovim setup docs ([#18108](https://github.com/astral-sh/ruff/pull/18108)) +- \[`flake8-simplify`\] Add fix safety section (`SIM103`) ([#18086](https://github.com/astral-sh/ruff/pull/18086)) +- \[`flake8-simplify`\] Add fix safety section (`SIM112`) ([#18099](https://github.com/astral-sh/ruff/pull/18099)) +- \[`pylint`\] Add fix safety section (`PLC0414`) ([#17802](https://github.com/astral-sh/ruff/pull/17802)) +- \[`pylint`\] Add fix safety section (`PLE4703`) ([#17824](https://github.com/astral-sh/ruff/pull/17824)) +- \[`pylint`\] Add fix safety section (`PLW1514`) ([#17932](https://github.com/astral-sh/ruff/pull/17932)) +- \[`pylint`\] Add fix safety section (`PLW3301`) ([#17878](https://github.com/astral-sh/ruff/pull/17878)) +- \[`ruff`\] Add fix safety section (`RUF007`) ([#17755](https://github.com/astral-sh/ruff/pull/17755)) +- \[`ruff`\] Add fix safety section (`RUF033`) ([#17760](https://github.com/astral-sh/ruff/pull/17760)) + +## 0.11.11 + +### Preview features + +- \[`airflow`\] Add autofixes for `AIR302` and `AIR312` ([#17942](https://github.com/astral-sh/ruff/pull/17942)) +- \[`airflow`\] Move rules from `AIR312` to `AIR302` ([#17940](https://github.com/astral-sh/ruff/pull/17940)) +- \[`airflow`\] Update `AIR301` and `AIR311` with the latest Airflow implementations ([#17985](https://github.com/astral-sh/ruff/pull/17985)) +- \[`flake8-simplify`\] Enable fix in preview mode (`SIM117`) ([#18208](https://github.com/astral-sh/ruff/pull/18208)) + +### Bug fixes + +- Fix inconsistent formatting of match-case on `[]` and `_` ([#18147](https://github.com/astral-sh/ruff/pull/18147)) +- \[`pylint`\] Fix `PLW1514` not recognizing the `encoding` positional argument of `codecs.open` ([#18109](https://github.com/astral-sh/ruff/pull/18109)) + +### CLI + +- Add full option name in formatter warning ([#18217](https://github.com/astral-sh/ruff/pull/18217)) + +### Documentation + +- Fix rendering of admonition in docs ([#18163](https://github.com/astral-sh/ruff/pull/18163)) +- \[`flake8-print`\] Improve print/pprint docs for `T201` and `T203` ([#18130](https://github.com/astral-sh/ruff/pull/18130)) +- \[`flake8-simplify`\] Add fix safety section (`SIM110`,`SIM210`) ([#18114](https://github.com/astral-sh/ruff/pull/18114),[#18100](https://github.com/astral-sh/ruff/pull/18100)) +- \[`pylint`\] Fix docs example that produced different output (`PLW0603`) ([#18216](https://github.com/astral-sh/ruff/pull/18216)) + +## 0.11.12 + +### Preview features + +- \[`airflow`\] Revise fix titles (`AIR3`) ([#18215](https://github.com/astral-sh/ruff/pull/18215)) +- \[`pylint`\] Implement `missing-maxsplit-arg` (`PLC0207`) ([#17454](https://github.com/astral-sh/ruff/pull/17454)) +- \[`pyupgrade`\] New rule `UP050` (`useless-class-metaclass-type`) ([#18334](https://github.com/astral-sh/ruff/pull/18334)) +- \[`flake8-use-pathlib`\] Replace `os.symlink` with `Path.symlink_to` (`PTH211`) ([#18337](https://github.com/astral-sh/ruff/pull/18337)) + +### Bug fixes + +- \[`flake8-bugbear`\] Ignore `__debug__` attribute in `B010` ([#18357](https://github.com/astral-sh/ruff/pull/18357)) +- \[`flake8-async`\] Fix `anyio.sleep` argument name (`ASYNC115`, `ASYNC116`) ([#18262](https://github.com/astral-sh/ruff/pull/18262)) +- \[`refurb`\] Fix `FURB129` autofix generating invalid syntax ([#18235](https://github.com/astral-sh/ruff/pull/18235)) + +### Rule changes + +- \[`flake8-implicit-str-concat`\] Add autofix for `ISC003` ([#18256](https://github.com/astral-sh/ruff/pull/18256)) +- \[`pycodestyle`\] Improve the diagnostic message for `E712` ([#18328](https://github.com/astral-sh/ruff/pull/18328)) +- \[`flake8-2020`\] Fix diagnostic message for `!=` comparisons (`YTT201`) ([#18293](https://github.com/astral-sh/ruff/pull/18293)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP010`) ([#18291](https://github.com/astral-sh/ruff/pull/18291)) + +### Documentation + +- Simplify rules table to improve readability ([#18297](https://github.com/astral-sh/ruff/pull/18297)) +- Update editor integrations link in README ([#17977](https://github.com/astral-sh/ruff/pull/17977)) +- \[`flake8-bugbear`\] Add fix safety section (`B006`) ([#17652](https://github.com/astral-sh/ruff/pull/17652)) + +## 0.11.13 + +### Preview features + +- \[`airflow`\] Add unsafe fix for module moved cases (`AIR301`,`AIR311`,`AIR312`,`AIR302`) ([#18367](https://github.com/astral-sh/ruff/pull/18367),[#18366](https://github.com/astral-sh/ruff/pull/18366),[#18363](https://github.com/astral-sh/ruff/pull/18363),[#18093](https://github.com/astral-sh/ruff/pull/18093)) +- \[`refurb`\] Add coverage of `set` and `frozenset` calls (`FURB171`) ([#18035](https://github.com/astral-sh/ruff/pull/18035)) +- \[`refurb`\] Mark `FURB180` fix unsafe when class has bases ([#18149](https://github.com/astral-sh/ruff/pull/18149)) + +### Bug fixes + +- \[`perflint`\] Fix missing parentheses for lambda and ternary conditions (`PERF401`, `PERF403`) ([#18412](https://github.com/astral-sh/ruff/pull/18412)) +- \[`pyupgrade`\] Apply `UP035` only on py313+ for `get_type_hints()` ([#18476](https://github.com/astral-sh/ruff/pull/18476)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP004`,`UP050`) ([#18393](https://github.com/astral-sh/ruff/pull/18393), [#18390](https://github.com/astral-sh/ruff/pull/18390)) + +### Rule changes + +- \[`fastapi`\] Avoid false positive for class dependencies (`FAST003`) ([#18271](https://github.com/astral-sh/ruff/pull/18271)) + +### Documentation + +- Update editor setup docs for Neovim and Vim ([#18324](https://github.com/astral-sh/ruff/pull/18324)) + +### Other changes + +- Support Python 3.14 template strings (t-strings) in formatter and parser ([#17851](https://github.com/astral-sh/ruff/pull/17851)) diff --git a/changelogs/0.2.x.md b/changelogs/0.2.x.md new file mode 100644 index 0000000000000..95832973f13e7 --- /dev/null +++ b/changelogs/0.2.x.md @@ -0,0 +1,271 @@ +# Changelog 0.2.x + +## 0.2.0 + +### Breaking changes + +- The `NURSERY` selector cannot be used anymore +- Legacy selection of nursery rules by exact codes is no longer allowed without preview enabled + +See also, the "Remapped rules" section which may result in disabled rules. + +### Deprecations + +The following rules are now deprecated: + +- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`) +- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`) + +The following command line options are now deprecated: + +- `--show-source`; use `--output-format full` instead +- `--no-show-source`; use `--output-format concise` instead +- `--output-format text`; use `full` or `concise` instead + +The following settings have moved and the previous name is deprecated: + +- `ruff.allowed-confusables` → [`ruff.lint.allowed-confusables`](https://docs.astral.sh//ruff/settings/#lint_allowed-confusables) +- `ruff.dummy-variable-rgx` → [`ruff.lint.dummy-variable-rgx`](https://docs.astral.sh//ruff/settings/#lint_dummy-variable-rgx) +- `ruff.explicit-preview-rules` → [`ruff.lint.explicit-preview-rules`](https://docs.astral.sh//ruff/settings/#lint_explicit-preview-rules) +- `ruff.extend-fixable` → [`ruff.lint.extend-fixable`](https://docs.astral.sh//ruff/settings/#lint_extend-fixable) +- `ruff.extend-ignore` → [`ruff.lint.extend-ignore`](https://docs.astral.sh//ruff/settings/#lint_extend-ignore) +- `ruff.extend-per-file-ignores` → [`ruff.lint.extend-per-file-ignores`](https://docs.astral.sh//ruff/settings/#lint_extend-per-file-ignores) +- `ruff.extend-safe-fixes` → [`ruff.lint.extend-safe-fixes`](https://docs.astral.sh//ruff/settings/#lint_extend-safe-fixes) +- `ruff.extend-select` → [`ruff.lint.extend-select`](https://docs.astral.sh//ruff/settings/#lint_extend-select) +- `ruff.extend-unfixable` → [`ruff.lint.extend-unfixable`](https://docs.astral.sh//ruff/settings/#lint_extend-unfixable) +- `ruff.extend-unsafe-fixes` → [`ruff.lint.extend-unsafe-fixes`](https://docs.astral.sh//ruff/settings/#lint_extend-unsafe-fixes) +- `ruff.external` → [`ruff.lint.external`](https://docs.astral.sh//ruff/settings/#lint_external) +- `ruff.fixable` → [`ruff.lint.fixable`](https://docs.astral.sh//ruff/settings/#lint_fixable) +- `ruff.flake8-annotations` → [`ruff.lint.flake8-annotations`](https://docs.astral.sh//ruff/settings/#lint_flake8-annotations) +- `ruff.flake8-bandit` → [`ruff.lint.flake8-bandit`](https://docs.astral.sh//ruff/settings/#lint_flake8-bandit) +- `ruff.flake8-bugbear` → [`ruff.lint.flake8-bugbear`](https://docs.astral.sh//ruff/settings/#lint_flake8-bugbear) +- `ruff.flake8-builtins` → [`ruff.lint.flake8-builtins`](https://docs.astral.sh//ruff/settings/#lint_flake8-builtins) +- `ruff.flake8-comprehensions` → [`ruff.lint.flake8-comprehensions`](https://docs.astral.sh//ruff/settings/#lint_flake8-comprehensions) +- `ruff.flake8-copyright` → [`ruff.lint.flake8-copyright`](https://docs.astral.sh//ruff/settings/#lint_flake8-copyright) +- `ruff.flake8-errmsg` → [`ruff.lint.flake8-errmsg`](https://docs.astral.sh//ruff/settings/#lint_flake8-errmsg) +- `ruff.flake8-gettext` → [`ruff.lint.flake8-gettext`](https://docs.astral.sh//ruff/settings/#lint_flake8-gettext) +- `ruff.flake8-implicit-str-concat` → [`ruff.lint.flake8-implicit-str-concat`](https://docs.astral.sh//ruff/settings/#lint_flake8-implicit-str-concat) +- `ruff.flake8-import-conventions` → [`ruff.lint.flake8-import-conventions`](https://docs.astral.sh//ruff/settings/#lint_flake8-import-conventions) +- `ruff.flake8-pytest-style` → [`ruff.lint.flake8-pytest-style`](https://docs.astral.sh//ruff/settings/#lint_flake8-pytest-style) +- `ruff.flake8-quotes` → [`ruff.lint.flake8-quotes`](https://docs.astral.sh//ruff/settings/#lint_flake8-quotes) +- `ruff.flake8-self` → [`ruff.lint.flake8-self`](https://docs.astral.sh//ruff/settings/#lint_flake8-self) +- `ruff.flake8-tidy-imports` → [`ruff.lint.flake8-tidy-imports`](https://docs.astral.sh//ruff/settings/#lint_flake8-tidy-imports) +- `ruff.flake8-type-checking` → [`ruff.lint.flake8-type-checking`](https://docs.astral.sh//ruff/settings/#lint_flake8-type-checking) +- `ruff.flake8-unused-arguments` → [`ruff.lint.flake8-unused-arguments`](https://docs.astral.sh//ruff/settings/#lint_flake8-unused-arguments) +- `ruff.ignore` → [`ruff.lint.ignore`](https://docs.astral.sh//ruff/settings/#lint_ignore) +- `ruff.ignore-init-module-imports` → [`ruff.lint.ignore-init-module-imports`](https://docs.astral.sh//ruff/settings/#lint_ignore-init-module-imports) +- `ruff.isort` → [`ruff.lint.isort`](https://docs.astral.sh//ruff/settings/#lint_isort) +- `ruff.logger-objects` → [`ruff.lint.logger-objects`](https://docs.astral.sh//ruff/settings/#lint_logger-objects) +- `ruff.mccabe` → [`ruff.lint.mccabe`](https://docs.astral.sh//ruff/settings/#lint_mccabe) +- `ruff.pep8-naming` → [`ruff.lint.pep8-naming`](https://docs.astral.sh//ruff/settings/#lint_pep8-naming) +- `ruff.per-file-ignores` → [`ruff.lint.per-file-ignores`](https://docs.astral.sh//ruff/settings/#lint_per-file-ignores) +- `ruff.pycodestyle` → [`ruff.lint.pycodestyle`](https://docs.astral.sh//ruff/settings/#lint_pycodestyle) +- `ruff.pydocstyle` → [`ruff.lint.pydocstyle`](https://docs.astral.sh//ruff/settings/#lint_pydocstyle) +- `ruff.pyflakes` → [`ruff.lint.pyflakes`](https://docs.astral.sh//ruff/settings/#lint_pyflakes) +- `ruff.pylint` → [`ruff.lint.pylint`](https://docs.astral.sh//ruff/settings/#lint_pylint) +- `ruff.pyupgrade` → [`ruff.lint.pyupgrade`](https://docs.astral.sh//ruff/settings/#lint_pyupgrade) +- `ruff.select` → [`ruff.lint.select`](https://docs.astral.sh//ruff/settings/#lint_select) +- `ruff.task-tags` → [`ruff.lint.task-tags`](https://docs.astral.sh//ruff/settings/#lint_task-tags) +- `ruff.typing-modules` → [`ruff.lint.typing-modules`](https://docs.astral.sh//ruff/settings/#lint_typing-modules) +- `ruff.unfixable` → [`ruff.lint.unfixable`](https://docs.astral.sh//ruff/settings/#lint_unfixable) + +### Remapped rules + +The following rules have been remapped to new codes: + +- [`raise-without-from-inside-except`](https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/): `TRY200` to `B904` +- [`suspicious-eval-usage`](https://docs.astral.sh/ruff/rules/suspicious-eval-usage/): `PGH001` to `S307` +- [`logging-warn`](https://docs.astral.sh/ruff/rules/logging-warn/): `PGH002` to `G010` +- [`static-key-dict-comprehension`](https://docs.astral.sh/ruff/rules/static-key-dict-comprehension): `RUF011` to `B035` +- [`runtime-string-union`](https://docs.astral.sh/ruff/rules/runtime-string-union): `TCH006` to `TCH010` + +### Stabilizations + +The following rules have been stabilized and are no longer in preview: + +- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await) (`TRIO100`) +- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call) (`TRIO105`) +- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout) (`TRIO109`) +- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep) (`TRIO110`) +- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call) (`TRIO115`) +- [`unnecessary-escaped-quote`](https://docs.astral.sh/ruff/rules/unnecessary-escaped-quote) (`Q004`) +- [`enumerate-for-loop`](https://docs.astral.sh/ruff/rules/enumerate-for-loop) (`SIM113`) +- [`zip-dict-keys-and-values`](https://docs.astral.sh/ruff/rules/zip-dict-keys-and-values) (`SIM911`) +- [`timeout-error-alias`](https://docs.astral.sh/ruff/rules/timeout-error-alias) (`UP041`) +- [`flask-debug-true`](https://docs.astral.sh/ruff/rules/flask-debug-true) (`S201`) +- [`tarfile-unsafe-members`](https://docs.astral.sh/ruff/rules/tarfile-unsafe-members) (`S202`) +- [`ssl-insecure-version`](https://docs.astral.sh/ruff/rules/ssl-insecure-version) (`S502`) +- [`ssl-with-bad-defaults`](https://docs.astral.sh/ruff/rules/ssl-with-bad-defaults) (`S503`) +- [`ssl-with-no-version`](https://docs.astral.sh/ruff/rules/ssl-with-no-version) (`S504`) +- [`weak-cryptographic-key`](https://docs.astral.sh/ruff/rules/weak-cryptographic-key) (`S505`) +- [`ssh-no-host-key-verification`](https://docs.astral.sh/ruff/rules/ssh-no-host-key-verification) (`S507`) +- [`django-raw-sql`](https://docs.astral.sh/ruff/rules/django-raw-sql) (`S611`) +- [`mako-templates`](https://docs.astral.sh/ruff/rules/mako-templates) (`S702`) +- [`generator-return-from-iter-method`](https://docs.astral.sh/ruff/rules/generator-return-from-iter-method) (`PYI058`) +- [`runtime-string-union`](https://docs.astral.sh/ruff/rules/runtime-string-union) (`TCH006`) +- [`numpy2-deprecation`](https://docs.astral.sh/ruff/rules/numpy2-deprecation) (`NPY201`) +- [`quadratic-list-summation`](https://docs.astral.sh/ruff/rules/quadratic-list-summation) (`RUF017`) +- [`assignment-in-assert`](https://docs.astral.sh/ruff/rules/assignment-in-assert) (`RUF018`) +- [`unnecessary-key-check`](https://docs.astral.sh/ruff/rules/unnecessary-key-check) (`RUF019`) +- [`never-union`](https://docs.astral.sh/ruff/rules/never-union) (`RUF020`) +- [`direct-logger-instantiation`](https://docs.astral.sh/ruff/rules/direct-logger-instantiation) (`LOG001`) +- [`invalid-get-logger-argument`](https://docs.astral.sh/ruff/rules/invalid-get-logger-argument) (`LOG002`) +- [`exception-without-exc-info`](https://docs.astral.sh/ruff/rules/exception-without-exc-info) (`LOG007`) +- [`undocumented-warn`](https://docs.astral.sh/ruff/rules/undocumented-warn) (`LOG009`) + +Fixes for the following rules have been stabilized and are now available without preview: + +- [`triple-single-quotes`](https://docs.astral.sh/ruff/rules/triple-single-quotes) (`D300`) +- [`non-pep604-annotation`](https://docs.astral.sh/ruff/rules/non-pep604-annotation) (`UP007`) +- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default) (`SIM910`) +- [`in-dict-keys`](https://docs.astral.sh/ruff/rules/in-dict-keys) (`SIM118`) +- [`collapsible-else-if`](https://docs.astral.sh/ruff/rules/collapsible-else-if) (`PLR5501`) +- [`if-with-same-arms`](https://docs.astral.sh/ruff/rules/if-with-same-arms) (`SIM114`) +- [`useless-else-on-loop`](https://docs.astral.sh/ruff/rules/useless-else-on-loop) (`PLW0120`) +- [`unnecessary-literal-union`](https://docs.astral.sh/ruff/rules/unnecessary-literal-union) (`PYI030`) +- [`unnecessary-spread`](https://docs.astral.sh/ruff/rules/unnecessary-spread) (`PIE800`) +- [`error-instead-of-exception`](https://docs.astral.sh/ruff/rules/error-instead-of-exception) (`TRY400`) +- [`redefined-while-unused`](https://docs.astral.sh/ruff/rules/redefined-while-unused) (`F811`) +- [`duplicate-value`](https://docs.astral.sh/ruff/rules/duplicate-value) (`B033`) +- [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) +- [`non-pep585-annotation`](https://docs.astral.sh/ruff/rules/non-pep585-annotation) (`UP006`) + +Fixes for the following rules have been promoted from unsafe to safe: + +- [`unaliased-collections-abc-set-import`](https://docs.astral.sh/ruff/rules/unaliased-collections-abc-set-import) (`PYI025`) + +The following behaviors have been stabilized: + +- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) allows `sys.path` modifications between imports +- [`reimplemented-container-builtin`](https://docs.astral.sh/ruff/rules/reimplemented-container-builtin/) (`PIE807`) includes lambdas that can be replaced with `dict` +- [`unnecessary-placeholder`](https://docs.astral.sh/ruff/rules/unnecessary-placeholder/) (`PIE790`) applies to unnecessary ellipses (`...`) +- [`if-else-block-instead-of-dict-get`](https://docs.astral.sh/ruff/rules/if-else-block-instead-of-dict-get/) (`SIM401`) applies to `if-else` expressions + +### Preview features + +- \[`refurb`\] Implement `metaclass_abcmeta` (`FURB180`) ([#9658](https://github.com/astral-sh/ruff/pull/9658)) +- Implement `blank_line_after_nested_stub_class` preview style ([#9155](https://github.com/astral-sh/ruff/pull/9155)) +- The preview rule [`and-or-ternary`](https://docs.astral.sh/ruff/rules/and-or-ternary) (`PLR1706`) was removed + +### Bug fixes + +- \[`flake8-async`\] Take `pathlib.Path` into account when analyzing async functions ([#9703](https://github.com/astral-sh/ruff/pull/9703)) +- \[`flake8-return`\] - fix indentation syntax error (`RET505`) ([#9705](https://github.com/astral-sh/ruff/pull/9705)) +- Detect multi-statement lines in else removal ([#9748](https://github.com/astral-sh/ruff/pull/9748)) +- `RUF022`, `RUF023`: never add two trailing commas to the end of a sequence ([#9698](https://github.com/astral-sh/ruff/pull/9698)) +- `RUF023`: Don't sort `__match_args__`, only `__slots__` ([#9724](https://github.com/astral-sh/ruff/pull/9724)) +- \[`flake8-simplify`\] - Fix syntax error in autofix (`SIM114`) ([#9704](https://github.com/astral-sh/ruff/pull/9704)) +- \[`pylint`\] Show verbatim constant in `magic-value-comparison` (`PLR2004`) ([#9694](https://github.com/astral-sh/ruff/pull/9694)) +- Removing trailing whitespace inside multiline strings is unsafe ([#9744](https://github.com/astral-sh/ruff/pull/9744)) +- Support `IfExp` with dual string arms in `invalid-envvar-default` ([#9734](https://github.com/astral-sh/ruff/pull/9734)) +- \[`pylint`\] Add `__mro_entries__` to known dunder methods (`PLW3201`) ([#9706](https://github.com/astral-sh/ruff/pull/9706)) + +### Documentation + +- Removed rules are now retained in the documentation ([#9691](https://github.com/astral-sh/ruff/pull/9691)) +- Deprecated rules are now indicated in the documentation ([#9689](https://github.com/astral-sh/ruff/pull/9689)) + +## 0.2.1 + +This release includes support for range formatting (i.e., the ability to format specific lines +within a source file). + +### Preview features + +- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728)) +- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725)) + +### Formatter + +- Add `--range` option to `ruff format` ([#9733](https://github.com/astral-sh/ruff/pull/9733)) +- Don't trim last empty line in docstrings ([#9813](https://github.com/astral-sh/ruff/pull/9813)) + +### Bug fixes + +- Skip empty lines when determining base indentation ([#9795](https://github.com/astral-sh/ruff/pull/9795)) +- Drop `__get__` and `__set__` from `unnecessary-dunder-call` ([#9791](https://github.com/astral-sh/ruff/pull/9791)) +- Respect generic `Protocol` in ellipsis removal ([#9841](https://github.com/astral-sh/ruff/pull/9841)) +- Revert "Use publicly available Apple Silicon runners (#9726)" ([#9834](https://github.com/astral-sh/ruff/pull/9834)) + +### Performance + +- Skip LibCST parsing for standard dedent adjustments ([#9769](https://github.com/astral-sh/ruff/pull/9769)) +- Remove CST-based fixer for `C408` ([#9822](https://github.com/astral-sh/ruff/pull/9822)) +- Add our own ignored-names abstractions ([#9802](https://github.com/astral-sh/ruff/pull/9802)) +- Remove CST-based fixers for `C400`, `C401`, `C410`, and `C418` ([#9819](https://github.com/astral-sh/ruff/pull/9819)) +- Use `AhoCorasick` to speed up quote match ([#9773](https://github.com/astral-sh/ruff/pull/9773)) +- Remove CST-based fixers for `C405` and `C409` ([#9821](https://github.com/astral-sh/ruff/pull/9821)) +- Add fast-path for comment detection ([#9808](https://github.com/astral-sh/ruff/pull/9808)) +- Invert order of checks in `zero-sleep-call` ([#9766](https://github.com/astral-sh/ruff/pull/9766)) +- Short-circuit typing matches based on imports ([#9800](https://github.com/astral-sh/ruff/pull/9800)) +- Run dunder method rule on methods directly ([#9815](https://github.com/astral-sh/ruff/pull/9815)) +- Track top-level module imports in the semantic model ([#9775](https://github.com/astral-sh/ruff/pull/9775)) +- Slight speed-up for lowercase and uppercase identifier checks ([#9798](https://github.com/astral-sh/ruff/pull/9798)) +- Remove LibCST-based fixer for `C403` ([#9818](https://github.com/astral-sh/ruff/pull/9818)) + +### Documentation + +- Update `max-pos-args` example to `max-positional-args` ([#9797](https://github.com/astral-sh/ruff/pull/9797)) +- Fixed example code in `weak_cryptographic_key.rs` ([#9774](https://github.com/astral-sh/ruff/pull/9774)) +- Fix references to deprecated `ANN` rules in changelog ([#9771](https://github.com/astral-sh/ruff/pull/9771)) +- Fix default for `max-positional-args` ([#9838](https://github.com/astral-sh/ruff/pull/9838)) + +## 0.2.2 + +Highlights include: + +- Initial support formatting f-strings (in `--preview`). +- Support for overriding arbitrary configuration options via the CLI through an expanded `--config` argument (e.g., `--config "lint.isort.combine-as-imports=false"`). +- Significant performance improvements in Ruff's lexer, parser, and lint rules. + +### Preview features + +- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642)) +- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266)) +- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880)) + +### Rule changes + +- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793)) +- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862)) +- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887)) +- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976)) +- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952)) + +### Formatter + +- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915)) +- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957)) +- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922)) + +### CLI + +- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599)) + +### Bug fixes + +- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866)) +- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905)) +- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969)) +- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981)) +- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953)) +- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996)) +- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859)) +- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955)) +- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907)) +- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894)) +- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878)) +- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964)) +- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956)) +- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865)) +- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990)) +- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849)) + +### Performance + +- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888)) +- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853)) +- Reduce `Result` size by using `Box` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885)) +- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900)) +- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867)) +- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884)) diff --git a/changelogs/0.3.x.md b/changelogs/0.3.x.md new file mode 100644 index 0000000000000..490eb311ed011 --- /dev/null +++ b/changelogs/0.3.x.md @@ -0,0 +1,308 @@ +# Changelog 0.3.x + +## 0.3.0 + +This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to +detect invalid formatter suppression comments. + +### Preview features + +- \[`flake8-bandit`\] Remove suspicious-lxml-import (`S410`) ([#10154](https://github.com/astral-sh/ruff/pull/10154)) +- \[`pycodestyle`\] Allow `os.environ` modifications between imports (`E402`) ([#10066](https://github.com/astral-sh/ruff/pull/10066)) +- \[`pycodestyle`\] Don't warn about a single whitespace character before a comma in a tuple (`E203`) ([#10094](https://github.com/astral-sh/ruff/pull/10094)) + +### Rule changes + +- \[`eradicate`\] Detect commented out `case` statements (`ERA001`) ([#10055](https://github.com/astral-sh/ruff/pull/10055)) +- \[`eradicate`\] Detect single-line code for `try:`, `except:`, etc. (`ERA001`) ([#10057](https://github.com/astral-sh/ruff/pull/10057)) +- \[`flake8-boolean-trap`\] Allow boolean positionals in `__post_init__` ([#10027](https://github.com/astral-sh/ruff/pull/10027)) +- \[`flake8-copyright`\] Allow © in copyright notices ([#10065](https://github.com/astral-sh/ruff/pull/10065)) +- \[`isort`\]: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971)) +- \[`pylint`\] New Rule `dict-iter-missing-items` (`PLE1141`) ([#9845](https://github.com/astral-sh/ruff/pull/9845)) +- \[`pylint`\] Ignore `sys.version` and `sys.platform` (`PLR1714`) ([#10054](https://github.com/astral-sh/ruff/pull/10054)) +- \[`pyupgrade`\] Detect literals with unary operators (`UP018`) ([#10060](https://github.com/astral-sh/ruff/pull/10060)) +- \[`ruff`\] Expand rule for `list(iterable).pop(0)` idiom (`RUF015`) ([#10148](https://github.com/astral-sh/ruff/pull/10148)) + +### Formatter + +This release introduces the Ruff 2024.2 style, stabilizing the following changes: + +- Prefer splitting the assignment's value over the target or type annotation ([#8943](https://github.com/astral-sh/ruff/pull/8943)) +- Remove blank lines before class docstrings ([#9154](https://github.com/astral-sh/ruff/pull/9154)) +- Wrap multiple context managers in `with` parentheses when targeting Python 3.9 or newer ([#9222](https://github.com/astral-sh/ruff/pull/9222)) +- Add a blank line after nested classes with a dummy body (`...`) in typing stub files ([#9155](https://github.com/astral-sh/ruff/pull/9155)) +- Reduce vertical spacing for classes and functions with a dummy (`...`) body ([#7440](https://github.com/astral-sh/ruff/issues/7440), [#9240](https://github.com/astral-sh/ruff/pull/9240)) +- Add a blank line after the module docstring ([#8283](https://github.com/astral-sh/ruff/pull/8283)) +- Parenthesize long type hints in assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210)) +- Preserve indent for single multiline-string call-expressions ([#9673](https://github.com/astral-sh/ruff/pull/9637)) +- Normalize hex escape and unicode escape sequences ([#9280](https://github.com/astral-sh/ruff/pull/9280)) +- Format module docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725)) + +### CLI + +- Explicitly disallow `extend` as part of a `--config` flag ([#10135](https://github.com/astral-sh/ruff/pull/10135)) +- Remove `build` from the default exclusion list ([#10093](https://github.com/astral-sh/ruff/pull/10093)) +- Deprecate `ruff `, `ruff --explain`, `ruff --clean`, and `ruff --generate-shell-completion` in favor of `ruff check `, `ruff rule`, `ruff clean`, and `ruff generate-shell-completion` ([#10169](https://github.com/astral-sh/ruff/pull/10169)) +- Remove the deprecated CLI option `--format` from `ruff rule` and `ruff linter` ([#10170](https://github.com/astral-sh/ruff/pull/10170)) + +### Bug fixes + +- \[`flake8-bugbear`\] Avoid adding default initializers to stubs (`B006`) ([#10152](https://github.com/astral-sh/ruff/pull/10152)) +- \[`flake8-type-checking`\] Respect runtime-required decorators for function signatures ([#10091](https://github.com/astral-sh/ruff/pull/10091)) +- \[`pycodestyle`\] Mark fixes overlapping with a multiline string as unsafe (`W293`) ([#10049](https://github.com/astral-sh/ruff/pull/10049)) +- \[`pydocstyle`\] Trim whitespace when removing blank lines after section (`D413`) ([#10162](https://github.com/astral-sh/ruff/pull/10162)) +- \[`pylint`\] Delete entire statement, including semicolons (`PLR0203`) ([#10074](https://github.com/astral-sh/ruff/pull/10074)) +- \[`ruff`\] Avoid f-string false positives in `gettext` calls (`RUF027`) ([#10118](https://github.com/astral-sh/ruff/pull/10118)) +- Fix `ruff` crashing on PowerPC systems because of too small page size ([#10080](https://github.com/astral-sh/ruff/pull/10080)) + +### Performance + +- Add cold attribute to less likely printer queue branches in the formatter ([#10121](https://github.com/astral-sh/ruff/pull/10121)) +- Skip unnecessary string normalization in the formatter ([#10116](https://github.com/astral-sh/ruff/pull/10116)) + +### Documentation + +- Remove "Beta" Label from formatter documentation ([#10144](https://github.com/astral-sh/ruff/pull/10144)) +- `line-length` option: fix link to `pycodestyle.max-line-length` ([#10136](https://github.com/astral-sh/ruff/pull/10136)) + +## 0.3.1 + +### Preview features + +- \[`pycodestyle`\] Fix E301 not triggering on decorated methods. ([#10117](https://github.com/astral-sh/ruff/pull/10117)) +- \[`pycodestyle`\] Respect `isort` settings in blank line rules (`E3*`) ([#10096](https://github.com/astral-sh/ruff/pull/10096)) +- \[`pycodestyle`\] Make blank lines in typing stub files optional (`E3*`) ([#10098](https://github.com/astral-sh/ruff/pull/10098)) +- \[`pylint`\] Implement `singledispatch-method` (`E1519`) ([#10140](https://github.com/astral-sh/ruff/pull/10140)) +- \[`pylint`\] Implement `useless-exception-statement` (`W0133`) ([#10176](https://github.com/astral-sh/ruff/pull/10176)) + +### Rule changes + +- \[`flake8-debugger`\] Check for use of `debugpy` and `ptvsd` debug modules (#10177) ([#10194](https://github.com/astral-sh/ruff/pull/10194)) +- \[`pyupgrade`\] Generate diagnostic for all valid f-string conversions regardless of line length (`UP032`) ([#10238](https://github.com/astral-sh/ruff/pull/10238)) +- \[`pep8_naming`\] Add fixes for `N804` and `N805` ([#10215](https://github.com/astral-sh/ruff/pull/10215)) + +### CLI + +- Colorize the output of `ruff format --diff` ([#10110](https://github.com/astral-sh/ruff/pull/10110)) +- Make `--config` and `--isolated` global flags ([#10150](https://github.com/astral-sh/ruff/pull/10150)) +- Correctly expand tildes and environment variables in paths passed to `--config` ([#10219](https://github.com/astral-sh/ruff/pull/10219)) + +### Configuration + +- Accept a PEP 440 version specifier for `required-version` ([#10216](https://github.com/astral-sh/ruff/pull/10216)) +- Implement isort's `default-section` setting ([#10149](https://github.com/astral-sh/ruff/pull/10149)) + +### Bug fixes + +- Remove trailing space from `CapWords` message ([#10220](https://github.com/astral-sh/ruff/pull/10220)) +- Respect external codes in file-level exemptions ([#10203](https://github.com/astral-sh/ruff/pull/10203)) +- \[`flake8-raise`\] Avoid false-positives for parens-on-raise with `future.exception()` (`RSE102`) ([#10206](https://github.com/astral-sh/ruff/pull/10206)) +- \[`pylint`\] Add fix for unary expressions in `PLC2801` ([#9587](https://github.com/astral-sh/ruff/pull/9587)) +- \[`ruff`\] Fix RUF028 not allowing `# fmt: skip` on match cases ([#10178](https://github.com/astral-sh/ruff/pull/10178)) + +## 0.3.2 + +### Preview features + +- Improve single-`with` item formatting for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276)) + +### Rule changes + +- \[`pyupgrade`\] Allow fixes for f-string rule regardless of line length (`UP032`) ([#10263](https://github.com/astral-sh/ruff/pull/10263)) +- \[`pycodestyle`\] Include actual conditions in E712 diagnostics ([#10254](https://github.com/astral-sh/ruff/pull/10254)) + +### Bug fixes + +- Fix trailing kwargs end of line comment after slash ([#10297](https://github.com/astral-sh/ruff/pull/10297)) +- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274)) +- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265)) +- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280)) +- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283)) +- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285)) + +## 0.3.3 + +### Preview features + +- \[`flake8-bandit`\]: Implement `S610` rule ([#10316](https://github.com/astral-sh/ruff/pull/10316)) +- \[`pycodestyle`\] Implement `blank-line-at-end-of-file` (`W391`) ([#10243](https://github.com/astral-sh/ruff/pull/10243)) +- \[`pycodestyle`\] Implement `redundant-backslash` (`E502`) ([#10292](https://github.com/astral-sh/ruff/pull/10292)) +- \[`pylint`\] - implement `redeclared-assigned-name` (`W0128`) ([#9268](https://github.com/astral-sh/ruff/pull/9268)) + +### Rule changes + +- \[`flake8_comprehensions`\] Handled special case for `C400` which also matches `C416` ([#10419](https://github.com/astral-sh/ruff/pull/10419)) +- \[`flake8-bandit`\] Implement upstream updates for `S311`, `S324` and `S605` ([#10313](https://github.com/astral-sh/ruff/pull/10313)) +- \[`pyflakes`\] Remove `F401` fix for `__init__` imports by default and allow opt-in to unsafe fix ([#10365](https://github.com/astral-sh/ruff/pull/10365)) +- \[`pylint`\] Implement `invalid-bool-return-type` (`E304`) ([#10377](https://github.com/astral-sh/ruff/pull/10377)) +- \[`pylint`\] Include builtin warnings in useless-exception-statement (`PLW0133`) ([#10394](https://github.com/astral-sh/ruff/pull/10394)) + +### CLI + +- Add message on success to `ruff check` ([#8631](https://github.com/astral-sh/ruff/pull/8631)) + +### Bug fixes + +- \[`PIE970`\] Allow trailing ellipsis in `typing.TYPE_CHECKING` ([#10413](https://github.com/astral-sh/ruff/pull/10413)) +- Avoid `TRIO115` if the argument is a variable ([#10376](https://github.com/astral-sh/ruff/pull/10376)) +- \[`F811`\] Avoid removing shadowed imports that point to different symbols ([#10387](https://github.com/astral-sh/ruff/pull/10387)) +- Fix `F821` and `F822` false positives in `.pyi` files ([#10341](https://github.com/astral-sh/ruff/pull/10341)) +- Fix `F821` false negatives in `.py` files when `from __future__ import annotations` is active ([#10362](https://github.com/astral-sh/ruff/pull/10362)) +- Fix case where `Indexer` fails to identify continuation preceded by newline #10351 ([#10354](https://github.com/astral-sh/ruff/pull/10354)) +- Sort hash maps in `Settings` display ([#10370](https://github.com/astral-sh/ruff/pull/10370)) +- Track conditional deletions in the semantic model ([#10415](https://github.com/astral-sh/ruff/pull/10415)) +- \[`C413`\] Wrap expressions in parentheses when negating ([#10346](https://github.com/astral-sh/ruff/pull/10346)) +- \[`pycodestyle`\] Do not ignore lines before the first logical line in blank lines rules. ([#10382](https://github.com/astral-sh/ruff/pull/10382)) +- \[`pycodestyle`\] Do not trigger `E225` and `E275` when the next token is a ')' ([#10315](https://github.com/astral-sh/ruff/pull/10315)) +- \[`pylint`\] Avoid false-positive slot non-assignment for `__dict__` (`PLE0237`) ([#10348](https://github.com/astral-sh/ruff/pull/10348)) +- Gate f-string struct size test for Rustc < 1.76 ([#10371](https://github.com/astral-sh/ruff/pull/10371)) + +### Documentation + +- Use `ruff.toml` format in README ([#10393](https://github.com/astral-sh/ruff/pull/10393)) +- \[`RUF008`\] Make it clearer that a mutable default in a dataclass is only valid if it is typed as a ClassVar ([#10395](https://github.com/astral-sh/ruff/pull/10395)) +- \[`pylint`\] Extend docs and test in `invalid-str-return-type` (`E307`) ([#10400](https://github.com/astral-sh/ruff/pull/10400)) +- Remove `.` from `check` and `format` commands ([#10217](https://github.com/astral-sh/ruff/pull/10217)) + +## 0.3.4 + +### Preview features + +- \[`flake8-simplify`\] Detect implicit `else` cases in `needless-bool` (`SIM103`) ([#10414](https://github.com/astral-sh/ruff/pull/10414)) +- \[`pylint`\] Implement `nan-comparison` (`PLW0117`) ([#10401](https://github.com/astral-sh/ruff/pull/10401)) +- \[`pylint`\] Implement `nonlocal-and-global` (`E115`) ([#10407](https://github.com/astral-sh/ruff/pull/10407)) +- \[`pylint`\] Implement `singledispatchmethod-function` (`PLE5120`) ([#10428](https://github.com/astral-sh/ruff/pull/10428)) +- \[`refurb`\] Implement `list-reverse-copy` (`FURB187`) ([#10212](https://github.com/astral-sh/ruff/pull/10212)) + +### Rule changes + +- \[`flake8-pytest-style`\] Add automatic fix for `pytest-parametrize-values-wrong-type` (`PT007`) ([#10461](https://github.com/astral-sh/ruff/pull/10461)) +- \[`pycodestyle`\] Allow SPDX license headers to exceed the line length (`E501`) ([#10481](https://github.com/astral-sh/ruff/pull/10481)) + +### Formatter + +- Fix unstable formatting for trailing subscript end-of-line comment ([#10492](https://github.com/astral-sh/ruff/pull/10492)) + +### Bug fixes + +- Avoid code comment detection in PEP 723 script tags ([#10464](https://github.com/astral-sh/ruff/pull/10464)) +- Avoid incorrect tuple transformation in single-element case (`C409`) ([#10491](https://github.com/astral-sh/ruff/pull/10491)) +- Bug fix: Prevent fully defined links [`name`](link) from being reformatted ([#10442](https://github.com/astral-sh/ruff/pull/10442)) +- Consider raw source code for `W605` ([#10480](https://github.com/astral-sh/ruff/pull/10480)) +- Docs: Link inline settings when not part of options section ([#10499](https://github.com/astral-sh/ruff/pull/10499)) +- Don't treat annotations as redefinitions in `.pyi` files ([#10512](https://github.com/astral-sh/ruff/pull/10512)) +- Fix `E231` bug: Inconsistent catch compared to pycodestyle, such as when dict nested in list ([#10469](https://github.com/astral-sh/ruff/pull/10469)) +- Fix pylint upstream categories not showing in docs ([#10441](https://github.com/astral-sh/ruff/pull/10441)) +- Add missing `Options` references to blank line docs ([#10498](https://github.com/astral-sh/ruff/pull/10498)) +- 'Revert "F821: Fix false negatives in .py files when `from __future__ import annotations` is active (#10362)"' ([#10513](https://github.com/astral-sh/ruff/pull/10513)) +- Apply NFKC normalization to unicode identifiers in the lexer ([#10412](https://github.com/astral-sh/ruff/pull/10412)) +- Avoid failures due to non-deterministic binding ordering ([#10478](https://github.com/astral-sh/ruff/pull/10478)) +- \[`flake8-bugbear`\] Allow tuples of exceptions (`B030`) ([#10437](https://github.com/astral-sh/ruff/pull/10437)) +- \[`flake8-quotes`\] Avoid syntax errors due to invalid quotes (`Q000, Q002`) ([#10199](https://github.com/astral-sh/ruff/pull/10199)) + +## 0.3.5 + +### Preview features + +- \[`pylint`\] Implement `modified-iterating-set` (`E4703`) ([#10473](https://github.com/astral-sh/ruff/pull/10473)) +- \[`refurb`\] Implement `for-loop-set-mutations` (`FURB142`) ([#10583](https://github.com/astral-sh/ruff/pull/10583)) +- \[`refurb`\] Implement `unnecessary-from-float` (`FURB164`) ([#10647](https://github.com/astral-sh/ruff/pull/10647)) +- \[`refurb`\] Implement `verbose-decimal-constructor` (`FURB157`) ([#10533](https://github.com/astral-sh/ruff/pull/10533)) + +### Rule changes + +- \[`flake8-comprehensions`\] Handled special case for `C401` which also matches `C416` ([#10596](https://github.com/astral-sh/ruff/pull/10596)) +- \[`flake8-pyi`\] Mark `unaliased-collections-abc-set-import` fix as "safe" for more cases in stub files (`PYI025`) ([#10547](https://github.com/astral-sh/ruff/pull/10547)) +- \[`numpy`\] Add `row_stack` to NumPy 2.0 migration rule ([#10646](https://github.com/astral-sh/ruff/pull/10646)) +- \[`pycodestyle`\] Allow cell magics before an import (`E402`) ([#10545](https://github.com/astral-sh/ruff/pull/10545)) +- \[`pycodestyle`\] Avoid blank line rules for the first logical line in cell ([#10291](https://github.com/astral-sh/ruff/pull/10291)) + +### Configuration + +- Respected nested namespace packages ([#10541](https://github.com/astral-sh/ruff/pull/10541)) +- \[`flake8-boolean-trap`\] Add setting for user defined allowed boolean trap ([#10531](https://github.com/astral-sh/ruff/pull/10531)) + +### Bug fixes + +- Correctly handle references in `__all__` definitions when renaming symbols in autofixes ([#10527](https://github.com/astral-sh/ruff/pull/10527)) +- Track ranges of names inside `__all__` definitions ([#10525](https://github.com/astral-sh/ruff/pull/10525)) +- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`) ([#10539](https://github.com/astral-sh/ruff/pull/10539)) +- \[`flake8-copyright`\] Accept commas in default copyright pattern ([#9498](https://github.com/astral-sh/ruff/pull/9498)) +- \[`flake8-datetimez`\] Allow f-strings with `%z` for `DTZ007` ([#10651](https://github.com/astral-sh/ruff/pull/10651)) +- \[`flake8-pytest-style`\] Fix `PT014` autofix for last item in list ([#10532](https://github.com/astral-sh/ruff/pull/10532)) +- \[`flake8-quotes`\] Ignore `Q000`, `Q001` when string is inside forward ref ([#10585](https://github.com/astral-sh/ruff/pull/10585)) +- \[`isort`\] Always place non-relative imports after relative imports ([#10669](https://github.com/astral-sh/ruff/pull/10669)) +- \[`isort`\] Respect Unicode characters in import sorting ([#10529](https://github.com/astral-sh/ruff/pull/10529)) +- \[`pyflakes`\] Fix F821 false negatives when `from __future__ import annotations` is active (attempt 2) ([#10524](https://github.com/astral-sh/ruff/pull/10524)) +- \[`pyflakes`\] Make `unnecessary-lambda` an always-unsafe fix ([#10668](https://github.com/astral-sh/ruff/pull/10668)) +- \[`pylint`\] Fixed false-positive on the rule `PLW1641` (`eq-without-hash`) ([#10566](https://github.com/astral-sh/ruff/pull/10566)) +- \[`ruff`\] Fix panic in unused `# noqa` removal with multi-byte space (`RUF100`) ([#10682](https://github.com/astral-sh/ruff/pull/10682)) + +### Documentation + +- Add PR title format to `CONTRIBUTING.md` ([#10665](https://github.com/astral-sh/ruff/pull/10665)) +- Fix list markup to include blank lines required ([#10591](https://github.com/astral-sh/ruff/pull/10591)) +- Put `flake8-logging` next to the other flake8 plugins in registry ([#10587](https://github.com/astral-sh/ruff/pull/10587)) +- \[`flake8-bandit`\] Update warning message for rule `S305` to address insecure block cipher mode use ([#10602](https://github.com/astral-sh/ruff/pull/10602)) +- \[`flake8-bugbear`\] Document use of anonymous assignment in `useless-expression` ([#10551](https://github.com/astral-sh/ruff/pull/10551)) +- \[`flake8-datetimez`\] Clarify error messages and docs for `DTZ` rules ([#10621](https://github.com/astral-sh/ruff/pull/10621)) +- \[`pycodestyle`\] Use same before vs. after numbers for `space-around-operator` ([#10640](https://github.com/astral-sh/ruff/pull/10640)) +- \[`ruff`\] Change `quadratic-list-summation` docs to use `iadd` consistently ([#10666](https://github.com/astral-sh/ruff/pull/10666)) + +## 0.3.6 + +### Preview features + +- \[`pylint`\] Implement `bad-staticmethod-argument` (`PLW0211`) ([#10781](https://github.com/astral-sh/ruff/pull/10781)) +- \[`pylint`\] Implement `if-stmt-min-max` (`PLR1730`, `PLR1731`) ([#10002](https://github.com/astral-sh/ruff/pull/10002)) +- \[`pyupgrade`\] Replace `str,Enum` multiple inheritance with `StrEnum` `UP042` ([#10713](https://github.com/astral-sh/ruff/pull/10713)) +- \[`refurb`\] Implement `if-expr-instead-of-or-operator` (`FURB110`) ([#10687](https://github.com/astral-sh/ruff/pull/10687)) +- \[`refurb`\] Implement `int-on-sliced-str` (`FURB166`) ([#10650](https://github.com/astral-sh/ruff/pull/10650)) +- \[`refurb`\] Implement `write-whole-file` (`FURB103`) ([#10802](https://github.com/astral-sh/ruff/pull/10802)) +- \[`refurb`\] Support `itemgetter` in `reimplemented-operator` (`FURB118`) ([#10526](https://github.com/astral-sh/ruff/pull/10526)) +- \[`flake8_comprehensions`\] Add `sum`/`min`/`max` to unnecessary comprehension check (`C419`) ([#10759](https://github.com/astral-sh/ruff/pull/10759)) + +### Rule changes + +- \[`pydocstyle`\] Require capitalizing docstrings where the first sentence is a single word (`D403`) ([#10776](https://github.com/astral-sh/ruff/pull/10776)) +- \[`pycodestyle`\] Ignore annotated lambdas in class scopes (`E731`) ([#10720](https://github.com/astral-sh/ruff/pull/10720)) +- \[`flake8-pyi`\] Various improvements to PYI034 ([#10807](https://github.com/astral-sh/ruff/pull/10807)) +- \[`flake8-slots`\] Flag subclasses of call-based `typing.NamedTuple`s as well as subclasses of `collections.namedtuple()` (`SLOT002`) ([#10808](https://github.com/astral-sh/ruff/pull/10808)) +- \[`pyflakes`\] Allow forward references in class bases in stub files (`F821`) ([#10779](https://github.com/astral-sh/ruff/pull/10779)) +- \[`pygrep-hooks`\] Improve `blanket-noqa` error message (`PGH004`) ([#10851](https://github.com/astral-sh/ruff/pull/10851)) + +### CLI + +- Support `FORCE_COLOR` env var ([#10839](https://github.com/astral-sh/ruff/pull/10839)) + +### Configuration + +- Support negated patterns in `[extend-]per-file-ignores` ([#10852](https://github.com/astral-sh/ruff/pull/10852)) + +### Bug fixes + +- \[`flake8-import-conventions`\] Accept non-aliased (but correct) import in `unconventional-import-alias` (`ICN001`) ([#10729](https://github.com/astral-sh/ruff/pull/10729)) +- \[`flake8-quotes`\] Add semantic model flag when inside f-string replacement field ([#10766](https://github.com/astral-sh/ruff/pull/10766)) +- \[`pep8-naming`\] Recursively resolve `TypeDicts` for N815 violations ([#10719](https://github.com/astral-sh/ruff/pull/10719)) +- \[`flake8-quotes`\] Respect `Q00*` ignores in `flake8-quotes` rules ([#10728](https://github.com/astral-sh/ruff/pull/10728)) +- \[`flake8-simplify`\] Show negated condition in `needless-bool` diagnostics (`SIM103`) ([#10854](https://github.com/astral-sh/ruff/pull/10854)) +- \[`ruff`\] Use within-scope shadowed bindings in `asyncio-dangling-task` (`RUF006`) ([#10793](https://github.com/astral-sh/ruff/pull/10793)) +- \[`flake8-pytest-style`\] Fix single-tuple conversion in `pytest-parametrize-values-wrong-type` (`PT007`) ([#10862](https://github.com/astral-sh/ruff/pull/10862)) +- \[`flake8-return`\] Ignore assignments to annotated variables in `unnecessary-assign` (`RET504`) ([#10741](https://github.com/astral-sh/ruff/pull/10741)) +- \[`refurb`\] Do not allow any keyword arguments for `read-whole-file` in `rb` mode (`FURB101`) ([#10803](https://github.com/astral-sh/ruff/pull/10803)) +- \[`pylint`\] Don't recommend decorating staticmethods with `@singledispatch` (`PLE1519`, `PLE1520`) ([#10637](https://github.com/astral-sh/ruff/pull/10637)) +- \[`pydocstyle`\] Use section name range for all section-related docstring diagnostics ([#10740](https://github.com/astral-sh/ruff/pull/10740)) +- Respect `# noqa` directives on `__all__` openers ([#10798](https://github.com/astral-sh/ruff/pull/10798)) + +## 0.3.7 + +### Preview features + +- \[`flake8-bugbear`\] Implement `loop-iterator-mutation` (`B909`) ([#9578](https://github.com/astral-sh/ruff/pull/9578)) +- \[`pylint`\] Implement rule to prefer augmented assignment (`PLR6104`) ([#9932](https://github.com/astral-sh/ruff/pull/9932)) + +### Bug fixes + +- Avoid TOCTOU errors in cache initialization ([#10884](https://github.com/astral-sh/ruff/pull/10884)) +- \[`pylint`\] Recode `nan-comparison` rule to `W0177` ([#10894](https://github.com/astral-sh/ruff/pull/10894)) +- \[`pylint`\] Reverse min-max logic in `if-stmt-min-max` ([#10890](https://github.com/astral-sh/ruff/pull/10890)) diff --git a/changelogs/0.4.x.md b/changelogs/0.4.x.md new file mode 100644 index 0000000000000..c658f8f4baf27 --- /dev/null +++ b/changelogs/0.4.x.md @@ -0,0 +1,415 @@ +# Changelog 0.4.x + +## 0.4.0 + +### A new, hand-written parser + +Ruff's new parser is **>2x faster**, which translates to a **20-40% speedup** for all linting and formatting invocations. +There's a lot to say about this exciting change, so check out the [blog post](https://astral.sh/blog/ruff-v0.4.0) for more details! + +See [#10036](https://github.com/astral-sh/ruff/pull/10036) for implementation details. + +### A new language server in Rust + +With this release, we also want to highlight our new language server. `ruff server` is a Rust-powered language +server that comes built-in with Ruff. It can be used with any editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP). +It uses a multi-threaded, lock-free architecture inspired by `rust-analyzer` and it will open the door for a lot +of exciting features. It’s also faster than our previous [Python-based language server](https://github.com/astral-sh/ruff-lsp) +-- but you probably guessed that already. + +`ruff server` is only in alpha, but it has a lot of features that you can try out today: + +- Lints Python files automatically and shows quick-fixes when available +- Formats Python files, with support for range formatting +- Comes with commands for quickly performing actions: `ruff.applyAutofix`, `ruff.applyFormat`, and `ruff.applyOrganizeImports` +- Supports `source.fixAll` and `source.organizeImports` source actions +- Automatically reloads your project configuration when you change it + +To setup `ruff server` with your editor, refer to the [README.md](https://github.com/astral-sh/ruff/blob/main/crates/ruff_server/README.md). + +### Preview features + +- \[`pycodestyle`\] Do not trigger `E3` rules on `def`s following a function/method with a dummy body ([#10704](https://github.com/astral-sh/ruff/pull/10704)) +- \[`pylint`\] Implement `invalid-bytes-returned` (`E0308`) ([#10959](https://github.com/astral-sh/ruff/pull/10959)) +- \[`pylint`\] Implement `invalid-length-returned` (`E0303`) ([#10963](https://github.com/astral-sh/ruff/pull/10963)) +- \[`pylint`\] Implement `self-cls-assignment` (`W0642`) ([#9267](https://github.com/astral-sh/ruff/pull/9267)) +- \[`pylint`\] Omit stubs from `invalid-bool` and `invalid-str-return-type` ([#11008](https://github.com/astral-sh/ruff/pull/11008)) +- \[`ruff`\] New rule `unused-async` (`RUF029`) to detect unneeded `async` keywords on functions ([#9966](https://github.com/astral-sh/ruff/pull/9966)) + +### Rule changes + +- \[`flake8-bandit`\] Allow `urllib.request.urlopen` calls with static `Request` argument (`S310`) ([#10964](https://github.com/astral-sh/ruff/pull/10964)) +- \[`flake8-bugbear`\] Treat `raise NotImplemented`-only bodies as stub functions (`B006`) ([#10990](https://github.com/astral-sh/ruff/pull/10990)) +- \[`flake8-slots`\] Respect same-file `Enum` subclasses (`SLOT000`) ([#11006](https://github.com/astral-sh/ruff/pull/11006)) +- \[`pylint`\] Support inverted comparisons (`PLR1730`) ([#10920](https://github.com/astral-sh/ruff/pull/10920)) + +### Linter + +- Improve handling of builtin symbols in linter rules ([#10919](https://github.com/astral-sh/ruff/pull/10919)) +- Improve display of rules in `--show-settings` ([#11003](https://github.com/astral-sh/ruff/pull/11003)) +- Improve inference capabilities of the `BuiltinTypeChecker` ([#10976](https://github.com/astral-sh/ruff/pull/10976)) +- Resolve classes and functions relative to script name ([#10965](https://github.com/astral-sh/ruff/pull/10965)) +- Improve performance of `RuleTable::any_enabled` ([#10971](https://github.com/astral-sh/ruff/pull/10971)) + +### Server + +_This section is devoted to updates for our new language server, written in Rust._ + +- Enable ruff-specific source actions ([#10916](https://github.com/astral-sh/ruff/pull/10916)) +- Refreshes diagnostics for open files when file configuration is changed ([#10988](https://github.com/astral-sh/ruff/pull/10988)) +- Important errors are now shown as popups ([#10951](https://github.com/astral-sh/ruff/pull/10951)) +- Introduce settings for directly configuring the linter and formatter ([#10984](https://github.com/astral-sh/ruff/pull/10984)) +- Resolve configuration for each document individually ([#10950](https://github.com/astral-sh/ruff/pull/10950)) +- Write a setup guide for Neovim ([#10987](https://github.com/astral-sh/ruff/pull/10987)) + +### Configuration + +- Add `RUFF_OUTPUT_FILE` environment variable support ([#10992](https://github.com/astral-sh/ruff/pull/10992)) + +### Bug fixes + +- Avoid `non-augmented-assignment` for reversed, non-commutative operators (`PLR6104`) ([#10909](https://github.com/astral-sh/ruff/pull/10909)) +- Limit commutative non-augmented-assignments to primitive data types (`PLR6104`) ([#10912](https://github.com/astral-sh/ruff/pull/10912)) +- Respect `per-file-ignores` for `RUF100` on blanket `# noqa` ([#10908](https://github.com/astral-sh/ruff/pull/10908)) +- Consider `if` expression for parenthesized with items parsing ([#11010](https://github.com/astral-sh/ruff/pull/11010)) +- Consider binary expr for parenthesized with items parsing ([#11012](https://github.com/astral-sh/ruff/pull/11012)) +- Reset `FOR_TARGET` context for all kinds of parentheses ([#11009](https://github.com/astral-sh/ruff/pull/11009)) + +## 0.4.1 + +### Preview features + +- \[`pylint`\] Implement `invalid-hash-returned` (`PLE0309`) ([#10961](https://github.com/astral-sh/ruff/pull/10961)) +- \[`pylint`\] Implement `invalid-index-returned` (`PLE0305`) ([#10962](https://github.com/astral-sh/ruff/pull/10962)) + +### Bug fixes + +- \[`pylint`\] Allow `NoReturn`-like functions for `__str__`, `__len__`, etc. (`PLE0307`) ([#11017](https://github.com/astral-sh/ruff/pull/11017)) +- Parser: Use empty range when there's "gap" in token source ([#11032](https://github.com/astral-sh/ruff/pull/11032)) +- \[`ruff`\] Ignore stub functions in `unused-async` (`RUF029`) ([#11026](https://github.com/astral-sh/ruff/pull/11026)) +- Parser: Expect indented case block instead of match stmt ([#11033](https://github.com/astral-sh/ruff/pull/11033)) + +## 0.4.2 + +### Rule changes + +- \[`flake8-pyi`\] Allow for overloaded `__exit__` and `__aexit__` definitions (`PYI036`) ([#11057](https://github.com/astral-sh/ruff/pull/11057)) +- \[`pyupgrade`\] Catch usages of `"%s" % var` and provide an unsafe fix (`UP031`) ([#11019](https://github.com/astral-sh/ruff/pull/11019)) +- \[`refurb`\] Implement new rule that suggests min/max over `sorted()` (`FURB192`) ([#10868](https://github.com/astral-sh/ruff/pull/10868)) + +### Server + +- Fix an issue with missing diagnostics for Neovim and Helix ([#11092](https://github.com/astral-sh/ruff/pull/11092)) +- Implement hover documentation for `noqa` codes ([#11096](https://github.com/astral-sh/ruff/pull/11096)) +- Introduce common Ruff configuration options with new server settings ([#11062](https://github.com/astral-sh/ruff/pull/11062)) + +### Bug fixes + +- Use `macos-12` for building release wheels to enable macOS 11 compatibility ([#11146](https://github.com/astral-sh/ruff/pull/11146)) +- \[`flake8-blind-expect`\] Allow raise from in `BLE001` ([#11131](https://github.com/astral-sh/ruff/pull/11131)) +- \[`flake8-pyi`\] Allow simple assignments to `None` in enum class scopes (`PYI026`) ([#11128](https://github.com/astral-sh/ruff/pull/11128)) +- \[`flake8-simplify`\] Avoid raising `SIM911` for non-`zip` attribute calls ([#11126](https://github.com/astral-sh/ruff/pull/11126)) +- \[`refurb`\] Avoid `operator.itemgetter` suggestion for single-item tuple ([#11095](https://github.com/astral-sh/ruff/pull/11095)) +- \[`ruff`\] Respect per-file-ignores for `RUF100` with no other diagnostics ([#11058](https://github.com/astral-sh/ruff/pull/11058)) +- \[`ruff`\] Fix async comprehension false positive (`RUF029`) ([#11070](https://github.com/astral-sh/ruff/pull/11070)) + +### Documentation + +- \[`flake8-bugbear`\] Document explicitly disabling strict zip (`B905`) ([#11040](https://github.com/astral-sh/ruff/pull/11040)) +- \[`flake8-type-checking`\] Mention `lint.typing-modules` in `TCH001`, `TCH002`, and `TCH003` ([#11144](https://github.com/astral-sh/ruff/pull/11144)) +- \[`isort`\] Improve documentation around custom `isort` sections ([#11050](https://github.com/astral-sh/ruff/pull/11050)) +- \[`pylint`\] Fix documentation oversight for `invalid-X-returns` ([#11094](https://github.com/astral-sh/ruff/pull/11094)) + +### Performance + +- Use `matchit` to resolve per-file settings ([#11111](https://github.com/astral-sh/ruff/pull/11111)) + +## 0.4.3 + +### Enhancements + +- Add support for PEP 696 syntax ([#11120](https://github.com/astral-sh/ruff/pull/11120)) + +### Preview features + +- \[`refurb`\] Use function range for `reimplemented-operator` diagnostics ([#11271](https://github.com/astral-sh/ruff/pull/11271)) +- \[`refurb`\] Ignore methods in `reimplemented-operator` (`FURB118`) ([#11270](https://github.com/astral-sh/ruff/pull/11270)) +- \[`refurb`\] Implement `fstring-number-format` (`FURB116`) ([#10921](https://github.com/astral-sh/ruff/pull/10921)) +- \[`ruff`\] Implement `redirected-noqa` (`RUF101`) ([#11052](https://github.com/astral-sh/ruff/pull/11052)) +- \[`pyflakes`\] Distinguish between first-party and third-party imports for fix suggestions ([#11168](https://github.com/astral-sh/ruff/pull/11168)) + +### Rule changes + +- \[`flake8-bugbear`\] Ignore non-abstract class attributes when enforcing `B024` ([#11210](https://github.com/astral-sh/ruff/pull/11210)) +- \[`flake8-logging`\] Include inline instantiations when detecting loggers ([#11154](https://github.com/astral-sh/ruff/pull/11154)) +- \[`pylint`\] Also emit `PLR0206` for properties with variadic parameters ([#11200](https://github.com/astral-sh/ruff/pull/11200)) +- \[`ruff`\] Detect duplicate codes as part of `unused-noqa` (`RUF100`) ([#10850](https://github.com/astral-sh/ruff/pull/10850)) + +### Formatter + +- Avoid multiline expression if format specifier is present ([#11123](https://github.com/astral-sh/ruff/pull/11123)) + +### LSP + +- Write `ruff server` setup guide for Helix ([#11183](https://github.com/astral-sh/ruff/pull/11183)) +- `ruff server` no longer hangs after shutdown ([#11222](https://github.com/astral-sh/ruff/pull/11222)) +- `ruff server` reads from a configuration TOML file in the user configuration directory if no local configuration exists ([#11225](https://github.com/astral-sh/ruff/pull/11225)) +- `ruff server` respects `per-file-ignores` configuration ([#11224](https://github.com/astral-sh/ruff/pull/11224)) +- `ruff server`: Support a custom TOML configuration file ([#11140](https://github.com/astral-sh/ruff/pull/11140)) +- `ruff server`: Support setting to prioritize project configuration over editor configuration ([#11086](https://github.com/astral-sh/ruff/pull/11086)) + +### Bug fixes + +- Avoid debug assertion around NFKC renames ([#11249](https://github.com/astral-sh/ruff/pull/11249)) +- \[`pyflakes`\] Prioritize `redefined-while-unused` over `unused-import` ([#11173](https://github.com/astral-sh/ruff/pull/11173)) +- \[`ruff`\] Respect `async` expressions in comprehension bodies ([#11219](https://github.com/astral-sh/ruff/pull/11219)) +- \[`pygrep_hooks`\] Fix `blanket-noqa` panic when last line has noqa with no newline (`PGH004`) ([#11108](https://github.com/astral-sh/ruff/pull/11108)) +- \[`perflint`\] Ignore list-copy recommendations for async `for` loops ([#11250](https://github.com/astral-sh/ruff/pull/11250)) +- \[`pyflakes`\] Improve `invalid-print-syntax` documentation ([#11171](https://github.com/astral-sh/ruff/pull/11171)) + +### Performance + +- Avoid allocations for isort module names ([#11251](https://github.com/astral-sh/ruff/pull/11251)) +- Build a separate ARM wheel for macOS ([#11149](https://github.com/astral-sh/ruff/pull/11149)) + +### Windows + +- Increase the minimum requirement to Windows 10. + +## 0.4.4 + +### Preview features + +- \[`pycodestyle`\] Ignore end-of-line comments when determining blank line rules ([#11342](https://github.com/astral-sh/ruff/pull/11342)) +- \[`pylint`\] Detect `pathlib.Path.open` calls in `unspecified-encoding` (`PLW1514`) ([#11288](https://github.com/astral-sh/ruff/pull/11288)) +- \[`flake8-pyi`\] Implement `PYI059` (`generic-not-last-base-class`) ([#11233](https://github.com/astral-sh/ruff/pull/11233)) +- \[`flake8-pyi`\] Implement `PYI062` (`duplicate-literal-member`) ([#11269](https://github.com/astral-sh/ruff/pull/11269)) + +### Rule changes + +- \[`flake8-boolean-trap`\] Allow passing booleans as positional-only arguments in code such as `set(True)` ([#11287](https://github.com/astral-sh/ruff/pull/11287)) +- \[`flake8-bugbear`\] Ignore enum classes in `cached-instance-method` (`B019`) ([#11312](https://github.com/astral-sh/ruff/pull/11312)) + +### Server + +- Expand tildes when resolving Ruff server configuration file ([#11283](https://github.com/astral-sh/ruff/pull/11283)) +- Fix `ruff server` hanging after Neovim closes ([#11291](https://github.com/astral-sh/ruff/pull/11291)) +- Editor settings are used by default if no file-based configuration exists ([#11266](https://github.com/astral-sh/ruff/pull/11266)) + +### Bug fixes + +- \[`pylint`\] Consider `with` statements for `too-many-branches` (`PLR0912`) ([#11321](https://github.com/astral-sh/ruff/pull/11321)) +- \[`flake8-blind-except`, `tryceratops`\] Respect logged and re-raised expressions in nested statements (`BLE001`, `TRY201`) ([#11301](https://github.com/astral-sh/ruff/pull/11301)) +- Recognise assignments such as `__all__ = builtins.list(["foo", "bar"])` as valid `__all__` definitions ([#11335](https://github.com/astral-sh/ruff/pull/11335)) + +## 0.4.5 + +### Ruff's language server is now in Beta + +`v0.4.5` marks the official Beta release of `ruff server`, an integrated language server built into Ruff. +`ruff server` supports the same feature set as `ruff-lsp`, powering linting, formatting, and +code fixes in Ruff's editor integrations -- but with superior performance and +no installation required. We'd love your feedback! + +You can enable `ruff server` in the [VS Code extension](https://github.com/astral-sh/ruff-vscode?tab=readme-ov-file#enabling-the-rust-based-language-server) today. + +To read more about this exciting milestone, check out our [blog post](https://astral.sh/blog/ruff-v0.4.5)! + +### Rule changes + +- \[`flake8-future-annotations`\] Reword `future-rewritable-type-annotation` (`FA100`) message ([#11381](https://github.com/astral-sh/ruff/pull/11381)) +- \[`isort`\] Expanded the set of standard-library modules to include `_string`, etc. ([#11374](https://github.com/astral-sh/ruff/pull/11374)) +- \[`pycodestyle`\] Consider soft keywords for `E27` rules ([#11446](https://github.com/astral-sh/ruff/pull/11446)) +- \[`pyflakes`\] Recommend adding unused import bindings to `__all__` ([#11314](https://github.com/astral-sh/ruff/pull/11314)) +- \[`pyflakes`\] Update documentation and deprecate `ignore_init_module_imports` ([#11436](https://github.com/astral-sh/ruff/pull/11436)) +- \[`pyupgrade`\] Mark quotes as unnecessary for non-evaluated annotations ([#11485](https://github.com/astral-sh/ruff/pull/11485)) + +### Formatter + +- Avoid multiline quotes warning with `quote-style = preserve` ([#11490](https://github.com/astral-sh/ruff/pull/11490)) + +### Server + +- Support Jupyter Notebook files ([#11206](https://github.com/astral-sh/ruff/pull/11206)) +- Support `noqa` comment code actions ([#11276](https://github.com/astral-sh/ruff/pull/11276)) +- Fix automatic configuration reloading ([#11492](https://github.com/astral-sh/ruff/pull/11492)) +- Fix several issues with configuration in Neovim and Helix ([#11497](https://github.com/astral-sh/ruff/pull/11497)) + +### CLI + +- Add `--output-format` as a CLI option for `ruff config` ([#11438](https://github.com/astral-sh/ruff/pull/11438)) + +### Bug fixes + +- Avoid `PLE0237` for property with setter ([#11377](https://github.com/astral-sh/ruff/pull/11377)) +- Avoid `TCH005` for `if` stmt with `elif`/`else` block ([#11376](https://github.com/astral-sh/ruff/pull/11376)) +- Avoid flagging `__future__` annotations as required for non-evaluated type annotations ([#11414](https://github.com/astral-sh/ruff/pull/11414)) +- Check for ruff executable in 'bin' directory as installed by 'pip install --target'. ([#11450](https://github.com/astral-sh/ruff/pull/11450)) +- Sort edits prior to deduplicating in quotation fix ([#11452](https://github.com/astral-sh/ruff/pull/11452)) +- Treat escaped newline as valid sequence ([#11465](https://github.com/astral-sh/ruff/pull/11465)) +- \[`flake8-pie`\] Preserve parentheses in `unnecessary-dict-kwargs` ([#11372](https://github.com/astral-sh/ruff/pull/11372)) +- \[`pylint`\] Ignore `__slots__` with dynamic values ([#11488](https://github.com/astral-sh/ruff/pull/11488)) +- \[`pylint`\] Remove `try` body from branch counting ([#11487](https://github.com/astral-sh/ruff/pull/11487)) +- \[`refurb`\] Respect operator precedence in `FURB110` ([#11464](https://github.com/astral-sh/ruff/pull/11464)) + +### Documentation + +- Add `--preview` to the README ([#11395](https://github.com/astral-sh/ruff/pull/11395)) +- Add Python 3.13 to list of allowed Python versions ([#11411](https://github.com/astral-sh/ruff/pull/11411)) +- Simplify Neovim setup documentation ([#11489](https://github.com/astral-sh/ruff/pull/11489)) +- Update CONTRIBUTING.md to reflect the new parser ([#11434](https://github.com/astral-sh/ruff/pull/11434)) +- Update server documentation with new migration guide ([#11499](https://github.com/astral-sh/ruff/pull/11499)) +- \[`pycodestyle`\] Clarify motivation for `E713` and `E714` ([#11483](https://github.com/astral-sh/ruff/pull/11483)) +- \[`pyflakes`\] Update docs to describe WAI behavior (F541) ([#11362](https://github.com/astral-sh/ruff/pull/11362)) +- \[`pylint`\] Clearly indicate what is counted as a branch ([#11423](https://github.com/astral-sh/ruff/pull/11423)) + +## 0.4.6 + +### Breaking changes + +- Use project-relative paths when calculating GitLab fingerprints ([#11532](https://github.com/astral-sh/ruff/pull/11532)) +- Bump minimum supported Windows version to Windows 10 ([#11613](https://github.com/astral-sh/ruff/pull/11613)) + +### Preview features + +- \[`flake8-async`\] Sleep with >24 hour interval should usually sleep forever (`ASYNC116`) ([#11498](https://github.com/astral-sh/ruff/pull/11498)) + +### Rule changes + +- \[`numpy`\] Add missing functions to NumPy 2.0 migration rule ([#11528](https://github.com/astral-sh/ruff/pull/11528)) +- \[`mccabe`\] Consider irrefutable pattern similar to `if .. else` for `C901` ([#11565](https://github.com/astral-sh/ruff/pull/11565)) +- Consider `match`-`case` statements for `C901`, `PLR0912`, and `PLR0915` ([#11521](https://github.com/astral-sh/ruff/pull/11521)) +- Remove empty strings when converting to f-string (`UP032`) ([#11524](https://github.com/astral-sh/ruff/pull/11524)) +- \[`flake8-bandit`\] `request-without-timeout` should warn for `requests.request` ([#11548](https://github.com/astral-sh/ruff/pull/11548)) +- \[`flake8-self`\] Ignore sunder accesses in `flake8-self` rules ([#11546](https://github.com/astral-sh/ruff/pull/11546)) +- \[`pyupgrade`\] Lint for `TypeAliasType` usages (`UP040`) ([#11530](https://github.com/astral-sh/ruff/pull/11530)) + +### Server + +- Respect excludes in `ruff server` configuration discovery ([#11551](https://github.com/astral-sh/ruff/pull/11551)) +- Use default settings if initialization options is empty or not provided ([#11566](https://github.com/astral-sh/ruff/pull/11566)) +- `ruff server` correctly treats `.pyi` files as stub files ([#11535](https://github.com/astral-sh/ruff/pull/11535)) +- `ruff server` searches for configuration in parent directories ([#11537](https://github.com/astral-sh/ruff/pull/11537)) +- `ruff server`: An empty code action filter no longer returns notebook source actions ([#11526](https://github.com/astral-sh/ruff/pull/11526)) + +### Bug fixes + +- \[`flake8-logging-format`\] Fix autofix title in `logging-warn` (`G010`) ([#11514](https://github.com/astral-sh/ruff/pull/11514)) +- \[`refurb`\] Avoid recommending `operator.itemgetter` with dependence on lambda arguments ([#11574](https://github.com/astral-sh/ruff/pull/11574)) +- \[`flake8-simplify`\] Avoid recommending context manager in `__enter__` implementations ([#11575](https://github.com/astral-sh/ruff/pull/11575)) +- Create intermediary directories for `--output-file` ([#11550](https://github.com/astral-sh/ruff/pull/11550)) +- Propagate reads on global variables ([#11584](https://github.com/astral-sh/ruff/pull/11584)) +- Treat all `singledispatch` arguments as runtime-required ([#11523](https://github.com/astral-sh/ruff/pull/11523)) + +## 0.4.7 + +### Preview features + +- \[`flake8-pyi`\] Implement `PYI064` ([#11325](https://github.com/astral-sh/ruff/pull/11325)) +- \[`flake8-pyi`\] Implement `PYI066` ([#11541](https://github.com/astral-sh/ruff/pull/11541)) +- \[`flake8-pyi`\] Implement `PYI057` ([#11486](https://github.com/astral-sh/ruff/pull/11486)) +- \[`pyflakes`\] Enable `F822` in `__init__.py` files by default ([#11370](https://github.com/astral-sh/ruff/pull/11370)) + +### Formatter + +- Fix incorrect placement of trailing stub function comments ([#11632](https://github.com/astral-sh/ruff/pull/11632)) + +### Server + +- Respect file exclusions in `ruff server` ([#11590](https://github.com/astral-sh/ruff/pull/11590)) +- Add support for documents not exist on disk ([#11588](https://github.com/astral-sh/ruff/pull/11588)) +- Add Vim and Kate setup guide for `ruff server` ([#11615](https://github.com/astral-sh/ruff/pull/11615)) + +### Bug fixes + +- Avoid removing newlines between docstring headers and rST blocks ([#11609](https://github.com/astral-sh/ruff/pull/11609)) +- Infer indentation with imports when logical indent is absent ([#11608](https://github.com/astral-sh/ruff/pull/11608)) +- Use char index rather than position for indent slice ([#11645](https://github.com/astral-sh/ruff/pull/11645)) +- \[`flake8-comprehension`\] Strip parentheses around generators in `C400` ([#11607](https://github.com/astral-sh/ruff/pull/11607)) +- Mark `repeated-isinstance-calls` as unsafe on Python 3.10 and later ([#11622](https://github.com/astral-sh/ruff/pull/11622)) + +## 0.4.8 + +### Performance + +- Linter performance has been improved by around 10% on some microbenchmarks by refactoring the lexer and parser to maintain synchronicity between them ([#11457](https://github.com/astral-sh/ruff/pull/11457)) + +### Preview features + +- \[`flake8-bugbear`\] Implement `return-in-generator` (`B901`) ([#11644](https://github.com/astral-sh/ruff/pull/11644)) +- \[`flake8-pyi`\] Implement `pep484-style-positional-only-parameter` (`PYI063`) ([#11699](https://github.com/astral-sh/ruff/pull/11699)) +- \[`pygrep_hooks`\] Check blanket ignores via file-level pragmas (`PGH004`) ([#11540](https://github.com/astral-sh/ruff/pull/11540)) + +### Rule changes + +- \[`pyupgrade`\] Update `UP035` for Python 3.13 and the latest version of `typing_extensions` ([#11693](https://github.com/astral-sh/ruff/pull/11693)) +- \[`numpy`\] Update `NPY001` rule for NumPy 2.0 ([#11735](https://github.com/astral-sh/ruff/pull/11735)) + +### Server + +- Formatting a document with syntax problems no longer spams a visible error popup ([#11745](https://github.com/astral-sh/ruff/pull/11745)) + +### CLI + +- Add RDJson support for `--output-format` flag ([#11682](https://github.com/astral-sh/ruff/pull/11682)) + +### Bug fixes + +- \[`pyupgrade`\] Write empty string in lieu of panic when fixing `UP032` ([#11696](https://github.com/astral-sh/ruff/pull/11696)) +- \[`flake8-simplify`\] Simplify double negatives in `SIM103` ([#11684](https://github.com/astral-sh/ruff/pull/11684)) +- Ensure the expression generator adds a newline before `type` statements ([#11720](https://github.com/astral-sh/ruff/pull/11720)) +- Respect per-file ignores for blanket and redirected noqa rules ([#11728](https://github.com/astral-sh/ruff/pull/11728)) + +## 0.4.9 + +### Preview features + +- \[`pylint`\] Implement `consider-dict-items` (`C0206`) ([#11688](https://github.com/astral-sh/ruff/pull/11688)) +- \[`refurb`\] Implement `repeated-global` (`FURB154`) ([#11187](https://github.com/astral-sh/ruff/pull/11187)) + +### Rule changes + +- \[`pycodestyle`\] Adapt fix for `E203` to work identical to `ruff format` ([#10999](https://github.com/astral-sh/ruff/pull/10999)) + +### Formatter + +- Fix formatter instability for lines only consisting of zero-width characters ([#11748](https://github.com/astral-sh/ruff/pull/11748)) + +### Server + +- Add supported commands in server capabilities ([#11850](https://github.com/astral-sh/ruff/pull/11850)) +- Use real file path when available in `ruff server` ([#11800](https://github.com/astral-sh/ruff/pull/11800)) +- Improve error message when a command is run on an unavailable document ([#11823](https://github.com/astral-sh/ruff/pull/11823)) +- Introduce the `ruff.printDebugInformation` command ([#11831](https://github.com/astral-sh/ruff/pull/11831)) +- Tracing system now respects log level and trace level, with options to log to a file ([#11747](https://github.com/astral-sh/ruff/pull/11747)) + +### CLI + +- Handle non-printable characters in diff view ([#11687](https://github.com/astral-sh/ruff/pull/11687)) + +### Bug fixes + +- \[`refurb`\] Avoid suggesting starmap when arguments are used outside call (`FURB140`) ([#11830](https://github.com/astral-sh/ruff/pull/11830)) +- \[`flake8-bugbear`\] Avoid panic in `B909` when checking large loop blocks ([#11772](https://github.com/astral-sh/ruff/pull/11772)) +- \[`refurb`\] Fix misbehavior of `operator.itemgetter` when getter param is a tuple (`FURB118`) ([#11774](https://github.com/astral-sh/ruff/pull/11774)) + +## 0.4.10 + +### Parser + +- Implement re-lexing logic for better error recovery ([#11845](https://github.com/astral-sh/ruff/pull/11845)) + +### Rule changes + +- \[`flake8-copyright`\] Update `CPY001` to check the first 4096 bytes instead of 1024 ([#11927](https://github.com/astral-sh/ruff/pull/11927)) +- \[`pycodestyle`\] Update `E999` to show all syntax errors instead of just the first one ([#11900](https://github.com/astral-sh/ruff/pull/11900)) + +### Server + +- Add tracing setup guide to Helix documentation ([#11883](https://github.com/astral-sh/ruff/pull/11883)) +- Add tracing setup guide to Neovim documentation ([#11884](https://github.com/astral-sh/ruff/pull/11884)) +- Defer notebook cell deletion to avoid an error message ([#11864](https://github.com/astral-sh/ruff/pull/11864)) + +### Security + +- Guard against malicious ecosystem comment artifacts ([#11879](https://github.com/astral-sh/ruff/pull/11879)) diff --git a/changelogs/0.5.x.md b/changelogs/0.5.x.md new file mode 100644 index 0000000000000..c0c5f5226df14 --- /dev/null +++ b/changelogs/0.5.x.md @@ -0,0 +1,402 @@ +# Changelog 0.5.x + +## 0.5.0 + +Check out the [blog post](https://astral.sh/blog/ruff-v0.5.0) for a migration guide and overview of the changes! + +### Breaking changes + +See also, the "Remapped rules" section which may result in disabled rules. + +- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms) +- Selecting `ALL` now excludes deprecated rules +- The released archives now include an extra level of nesting, which can be removed with `--strip-components=1` when untarring. +- The release artifact's file name no longer includes the version tag. This enables users to install via `/latest` URLs on GitHub. +- The diagnostic ranges for some `flake8-bandit` rules were modified ([#10667](https://github.com/astral-sh/ruff/pull/10667)). + +### Deprecations + +The following rules are now deprecated: + +- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`): Syntax errors are now always shown + +### Remapped rules + +The following rules have been remapped to new rule codes: + +- [`blocking-http-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-in-async-function/): `ASYNC100` to `ASYNC210` +- [`open-sleep-or-subprocess-in-async-function`](https://docs.astral.sh/ruff/rules/open-sleep-or-subprocess-in-async-function/): `ASYNC101` split into `ASYNC220`, `ASYNC221`, `ASYNC230`, and `ASYNC251` +- [`blocking-os-call-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-os-call-in-async-function/): `ASYNC102` has been merged into `ASYNC220` and `ASYNC221` +- [`trio-timeout-without-await`](https://docs.astral.sh/ruff/rules/trio-timeout-without-await/): `TRIO100` to `ASYNC100` +- [`trio-sync-call`](https://docs.astral.sh/ruff/rules/trio-sync-call/): `TRIO105` to `ASYNC105` +- [`trio-async-function-with-timeout`](https://docs.astral.sh/ruff/rules/trio-async-function-with-timeout/): `TRIO109` to `ASYNC109` +- [`trio-unneeded-sleep`](https://docs.astral.sh/ruff/rules/trio-unneeded-sleep/): `TRIO110` to `ASYNC110` +- [`trio-zero-sleep-call`](https://docs.astral.sh/ruff/rules/trio-zero-sleep-call/): `TRIO115` to `ASYNC115` +- [`repeated-isinstance-calls`](https://docs.astral.sh/ruff/rules/repeated-isinstance-calls/): `PLR1701` to `SIM101` + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`mutable-fromkeys-value`](https://docs.astral.sh/ruff/rules/mutable-fromkeys-value/) (`RUF024`) +- [`default-factory-kwarg`](https://docs.astral.sh/ruff/rules/default-factory-kwarg/) (`RUF026`) +- [`django-extra`](https://docs.astral.sh/ruff/rules/django-extra/) (`S610`) +- [`manual-dict-comprehension`](https://docs.astral.sh/ruff/rules/manual-dict-comprehension/) (`PERF403`) +- [`print-empty-string`](https://docs.astral.sh/ruff/rules/print-empty-string/) (`FURB105`) +- [`readlines-in-for`](https://docs.astral.sh/ruff/rules/readlines-in-for/) (`FURB129`) +- [`if-expr-min-max`](https://docs.astral.sh/ruff/rules/if-expr-min-max/) (`FURB136`) +- [`bit-count`](https://docs.astral.sh/ruff/rules/bit-count/) (`FURB161`) +- [`redundant-log-base`](https://docs.astral.sh/ruff/rules/redundant-log-base/) (`FURB163`) +- [`regex-flag-alias`](https://docs.astral.sh/ruff/rules/regex-flag-alias/) (`FURB167`) +- [`isinstance-type-none`](https://docs.astral.sh/ruff/rules/isinstance-type-none/) (`FURB168`) +- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`) +- [`implicit-cwd`](https://docs.astral.sh/ruff/rules/implicit-cwd/) (`FURB177`) +- [`hashlib-digest-hex`](https://docs.astral.sh/ruff/rules/hashlib-digest-hex/) (`FURB181`) +- [`list-reverse-copy`](https://docs.astral.sh/ruff/rules/list-reverse-copy/) (`FURB187`) +- [`bad-open-mode`](https://docs.astral.sh/ruff/rules/bad-open-mode/) (`PLW1501`) +- [`empty-comment`](https://docs.astral.sh/ruff/rules/empty-comment/) (`PLR2044`) +- [`global-at-module-level`](https://docs.astral.sh/ruff/rules/global-at-module-level/) (`PLW0604`) +- [`misplaced-bare-raise`](https://docs.astral.sh/ruff/rules/misplaced-bare-raise/) (`PLE0744`) +- [`non-ascii-import-name`](https://docs.astral.sh/ruff/rules/non-ascii-import-name/) (`PLC2403`) +- [`non-ascii-name`](https://docs.astral.sh/ruff/rules/non-ascii-name/) (`PLC2401`) +- [`nonlocal-and-global`](https://docs.astral.sh/ruff/rules/nonlocal-and-global/) (`PLE0115`) +- [`potential-index-error`](https://docs.astral.sh/ruff/rules/potential-index-error/) (`PLE0643`) +- [`redeclared-assigned-name`](https://docs.astral.sh/ruff/rules/redeclared-assigned-name/) (`PLW0128`) +- [`redefined-argument-from-local`](https://docs.astral.sh/ruff/rules/redefined-argument-from-local/) (`PLR1704`) +- [`repeated-keyword-argument`](https://docs.astral.sh/ruff/rules/repeated-keyword-argument/) (`PLE1132`) +- [`super-without-brackets`](https://docs.astral.sh/ruff/rules/super-without-brackets/) (`PLW0245`) +- [`unnecessary-list-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-list-index-lookup/) (`PLR1736`) +- [`useless-exception-statement`](https://docs.astral.sh/ruff/rules/useless-exception-statement/) (`PLW0133`) +- [`useless-with-lock`](https://docs.astral.sh/ruff/rules/useless-with-lock/) (`PLW2101`) + +The following behaviors have been stabilized: + +- [`is-literal`](https://docs.astral.sh/ruff/rules/is-literal/) (`F632`) now warns for identity checks against list, set or dictionary literals +- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) now detects `if` expressions with implicit `else` branches +- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`) now allows `os.environ` modifications between import statements +- [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison/) (`E721`) now allows idioms such as `type(x) is int` +- [`yoda-condition`](https://docs.astral.sh/ruff/rules/yoda-conditions/) (`SIM300`) now flags a wider range of expressions + +### Removals + +The following deprecated settings have been removed: + +- `output-format=text`; use `output-format=concise` or `output-format=full` +- `tab-size`; use `indent-width` + +The following deprecated CLI options have been removed: + +- `--show-source`; use `--output-format=full` +- `--no-show-source`; use `--output-format=concise` + +The following deprecated CLI commands have been removed: + +- `ruff `; use `ruff check ` +- `ruff --clean`; use `ruff clean` +- `ruff --generate-shell-completion`; use `ruff generate-shell-completion` + +### Preview features + +- \[`ruff`\] Add `assert-with-print-message` rule ([#11981](https://github.com/astral-sh/ruff/pull/11981)) + +### CLI + +- Use rule name rather than message in `--statistics` ([#11697](https://github.com/astral-sh/ruff/pull/11697)) +- Use the output format `full` by default ([#12010](https://github.com/astral-sh/ruff/pull/12010)) +- Don't log syntax errors to the console ([#11902](https://github.com/astral-sh/ruff/pull/11902)) + +### Rule changes + +- \[`ruff`\] Fix false positives if `gettext` is imported using an alias (`RUF027`) ([#12025](https://github.com/astral-sh/ruff/pull/12025)) +- \[`numpy`\] Update `trapz` and `in1d` deprecation (`NPY201`) ([#11948](https://github.com/astral-sh/ruff/pull/11948)) +- \[`flake8-bandit`\] Modify diagnostic ranges for shell-related rules ([#10667](https://github.com/astral-sh/ruff/pull/10667)) + +### Server + +- Closing an untitled, unsaved notebook document no longer throws an error ([#11942](https://github.com/astral-sh/ruff/pull/11942)) +- Support the usage of tildes and environment variables in `logFile` ([#11945](https://github.com/astral-sh/ruff/pull/11945)) +- Add option to configure whether to show syntax errors ([#12059](https://github.com/astral-sh/ruff/pull/12059)) + +### Bug fixes + +- \[`pycodestyle`\] Avoid `E203` for f-string debug expression ([#12024](https://github.com/astral-sh/ruff/pull/12024)) +- \[`pep8-naming`\] Match import-name ignores against both name and alias (`N812`, `N817`) ([#12033](https://github.com/astral-sh/ruff/pull/12033)) +- \[`pyflakes`\] Detect assignments that shadow definitions (`F811`) ([#11961](https://github.com/astral-sh/ruff/pull/11961)) + +### Parser + +- Emit a syntax error for an empty type parameter list ([#12030](https://github.com/astral-sh/ruff/pull/12030)) +- Avoid consuming the newline for unterminated strings ([#12067](https://github.com/astral-sh/ruff/pull/12067)) +- Do not include the newline in the unterminated string range ([#12017](https://github.com/astral-sh/ruff/pull/12017)) +- Use the correct range to highlight line continuation errors ([#12016](https://github.com/astral-sh/ruff/pull/12016)) +- Consider 2-character EOL before line continuations ([#12035](https://github.com/astral-sh/ruff/pull/12035)) +- Consider line continuation character for re-lexing ([#12008](https://github.com/astral-sh/ruff/pull/12008)) + +### Other changes + +- Upgrade the Unicode table used for measuring the line-length ([#11194](https://github.com/astral-sh/ruff/pull/11194)) +- Remove the deprecation error message for the nursery selector ([#10172](https://github.com/astral-sh/ruff/pull/10172)) + +## 0.5.1 + +### Preview features + +- \[`flake8-bugbear`\] Implement mutable-contextvar-default (B039) ([#12113](https://github.com/astral-sh/ruff/pull/12113)) +- \[`pycodestyle`\] Whitespace after decorator (`E204`) ([#12140](https://github.com/astral-sh/ruff/pull/12140)) +- \[`pytest`\] Reverse `PT001` and `PT0023` defaults ([#12106](https://github.com/astral-sh/ruff/pull/12106)) + +### Rule changes + +- Enable token-based rules on source with syntax errors ([#11950](https://github.com/astral-sh/ruff/pull/11950)) +- \[`flake8-bandit`\] Detect `httpx` for `S113` ([#12174](https://github.com/astral-sh/ruff/pull/12174)) +- \[`numpy`\] Update `NPY201` to include exception deprecations ([#12065](https://github.com/astral-sh/ruff/pull/12065)) +- \[`pylint`\] Generate autofix for `duplicate-bases` (`PLE0241`) ([#12105](https://github.com/astral-sh/ruff/pull/12105)) + +### Server + +- Avoid syntax error notification for source code actions ([#12148](https://github.com/astral-sh/ruff/pull/12148)) +- Consider the content of the new cells during notebook sync ([#12203](https://github.com/astral-sh/ruff/pull/12203)) +- Fix replacement edit range computation ([#12171](https://github.com/astral-sh/ruff/pull/12171)) + +### Bug fixes + +- Disable auto-fix when source has syntax errors ([#12134](https://github.com/astral-sh/ruff/pull/12134)) +- Fix cache key collisions for paths with separators ([#12159](https://github.com/astral-sh/ruff/pull/12159)) +- Make `requires-python` inference robust to `==` ([#12091](https://github.com/astral-sh/ruff/pull/12091)) +- Use char-wise width instead of `str`-width ([#12135](https://github.com/astral-sh/ruff/pull/12135)) +- \[`pycodestyle`\] Avoid `E275` if keyword followed by comma ([#12136](https://github.com/astral-sh/ruff/pull/12136)) +- \[`pycodestyle`\] Avoid `E275` if keyword is followed by a semicolon ([#12095](https://github.com/astral-sh/ruff/pull/12095)) +- \[`pylint`\] Skip [dummy variables](https://docs.astral.sh/ruff/settings/#lint_dummy-variable-rgx) for `PLR1704` ([#12190](https://github.com/astral-sh/ruff/pull/12190)) + +### Performance + +- Remove allocation in `parse_identifier` ([#12103](https://github.com/astral-sh/ruff/pull/12103)) +- Use `CompactString` for `Identifier` AST node ([#12101](https://github.com/astral-sh/ruff/pull/12101)) + +## 0.5.2 + +### Preview features + +- Use `space` separator before parenthesized expressions in comprehensions with leading comments ([#12282](https://github.com/astral-sh/ruff/pull/12282)) +- \[`flake8-async`\] Update `ASYNC100` to include `anyio` and `asyncio` ([#12221](https://github.com/astral-sh/ruff/pull/12221)) +- \[`flake8-async`\] Update `ASYNC109` to include `anyio` and `asyncio` ([#12236](https://github.com/astral-sh/ruff/pull/12236)) +- \[`flake8-async`\] Update `ASYNC110` to include `anyio` and `asyncio` ([#12261](https://github.com/astral-sh/ruff/pull/12261)) +- \[`flake8-async`\] Update `ASYNC115` to include `anyio` and `asyncio` ([#12262](https://github.com/astral-sh/ruff/pull/12262)) +- \[`flake8-async`\] Update `ASYNC116` to include `anyio` and `asyncio` ([#12266](https://github.com/astral-sh/ruff/pull/12266)) + +### Rule changes + +- \[`flake8-return`\] Exempt properties from explicit return rule (`RET501`) ([#12243](https://github.com/astral-sh/ruff/pull/12243)) +- \[`numpy`\] Add `np.NAN`-to-`np.nan` diagnostic ([#12292](https://github.com/astral-sh/ruff/pull/12292)) +- \[`refurb`\] Make `list-reverse-copy` an unsafe fix ([#12303](https://github.com/astral-sh/ruff/pull/12303)) + +### Server + +- Consider `include` and `extend-include` settings in native server ([#12252](https://github.com/astral-sh/ruff/pull/12252)) +- Include nested configurations in settings reloading ([#12253](https://github.com/astral-sh/ruff/pull/12253)) + +### CLI + +- Omit code frames for fixes with empty ranges ([#12304](https://github.com/astral-sh/ruff/pull/12304)) +- Warn about formatter incompatibility for `D203` ([#12238](https://github.com/astral-sh/ruff/pull/12238)) + +### Bug fixes + +- Make cache-write failures non-fatal on Windows ([#12302](https://github.com/astral-sh/ruff/pull/12302)) +- Treat `not` operations as boolean tests ([#12301](https://github.com/astral-sh/ruff/pull/12301)) +- \[`flake8-bandit`\] Avoid `S310` violations for HTTP-safe f-strings ([#12305](https://github.com/astral-sh/ruff/pull/12305)) +- \[`flake8-bandit`\] Support explicit string concatenations in S310 HTTP detection ([#12315](https://github.com/astral-sh/ruff/pull/12315)) +- \[`flake8-bandit`\] fix S113 false positive for httpx without `timeout` argument ([#12213](https://github.com/astral-sh/ruff/pull/12213)) +- \[`pycodestyle`\] Remove "non-obvious" allowance for E721 ([#12300](https://github.com/astral-sh/ruff/pull/12300)) +- \[`pyflakes`\] Consider `with` blocks as single-item branches for redefinition analysis ([#12311](https://github.com/astral-sh/ruff/pull/12311)) +- \[`refurb`\] Restrict forwarding for `newline` argument in `open()` calls to Python versions >= 3.10 ([#12244](https://github.com/astral-sh/ruff/pull/12244)) + +### Documentation + +- Update help and documentation to reflect `--output-format full` default ([#12248](https://github.com/astral-sh/ruff/pull/12248)) + +### Performance + +- Use more threads when discovering Python files ([#12258](https://github.com/astral-sh/ruff/pull/12258)) + +## 0.5.3 + +**Ruff 0.5.3 marks the stable release of the Ruff language server and introduces revamped +[documentation](https://docs.astral.sh/ruff/editors), including [setup guides for your editor of +choice](https://docs.astral.sh/ruff/editors/setup) and [the language server +itself](https://docs.astral.sh/ruff/editors/settings)**. + +### Preview features + +- Formatter: Insert empty line between suite and alternative branch after function/class definition ([#12294](https://github.com/astral-sh/ruff/pull/12294)) +- \[`pyupgrade`\] Implement `unnecessary-default-type-args` (`UP043`) ([#12371](https://github.com/astral-sh/ruff/pull/12371)) + +### Rule changes + +- \[`flake8-bugbear`\] Detect enumerate iterations in `loop-iterator-mutation` (`B909`) ([#12366](https://github.com/astral-sh/ruff/pull/12366)) +- \[`flake8-bugbear`\] Remove `discard`, `remove`, and `pop` allowance for `loop-iterator-mutation` (`B909`) ([#12365](https://github.com/astral-sh/ruff/pull/12365)) +- \[`pylint`\] Allow `repeated-equality-comparison` for mixed operations (`PLR1714`) ([#12369](https://github.com/astral-sh/ruff/pull/12369)) +- \[`pylint`\] Ignore `self` and `cls` when counting arguments (`PLR0913`) ([#12367](https://github.com/astral-sh/ruff/pull/12367)) +- \[`pylint`\] Use UTF-8 as default encoding in `unspecified-encoding` fix (`PLW1514`) ([#12370](https://github.com/astral-sh/ruff/pull/12370)) + +### Server + +- Build settings index in parallel for the native server ([#12299](https://github.com/astral-sh/ruff/pull/12299)) +- Use fallback settings when indexing the project ([#12362](https://github.com/astral-sh/ruff/pull/12362)) +- Consider `--preview` flag for `server` subcommand for the linter and formatter ([#12208](https://github.com/astral-sh/ruff/pull/12208)) + +### Bug fixes + +- \[`flake8-comprehensions`\] Allow additional arguments for `sum` and `max` comprehensions (`C419`) ([#12364](https://github.com/astral-sh/ruff/pull/12364)) +- \[`pylint`\] Avoid dropping extra boolean operations in `repeated-equality-comparison` (`PLR1714`) ([#12368](https://github.com/astral-sh/ruff/pull/12368)) +- \[`pylint`\] Consider expression before statement when determining binding kind (`PLR1704`) ([#12346](https://github.com/astral-sh/ruff/pull/12346)) + +### Documentation + +- Add docs for Ruff language server ([#12344](https://github.com/astral-sh/ruff/pull/12344)) +- Migrate to standalone docs repo ([#12341](https://github.com/astral-sh/ruff/pull/12341)) +- Update versioning policy for editor integration ([#12375](https://github.com/astral-sh/ruff/pull/12375)) + +### Other changes + +- Publish Wasm API to npm ([#12317](https://github.com/astral-sh/ruff/pull/12317)) + +## 0.5.4 + +### Rule changes + +- \[`ruff`\] Rename `RUF007` to `zip-instead-of-pairwise` ([#12399](https://github.com/astral-sh/ruff/pull/12399)) + +### Bug fixes + +- \[`flake8-builtins`\] Avoid shadowing diagnostics for `@override` methods ([#12415](https://github.com/astral-sh/ruff/pull/12415)) +- \[`flake8-comprehensions`\] Insert parentheses for multi-argument generators ([#12422](https://github.com/astral-sh/ruff/pull/12422)) +- \[`pydocstyle`\] Handle escaped docstrings within docstring (`D301`) ([#12192](https://github.com/astral-sh/ruff/pull/12192)) + +### Documentation + +- Fix GitHub link to Neovim setup ([#12410](https://github.com/astral-sh/ruff/pull/12410)) +- Fix `output-format` default in settings reference ([#12409](https://github.com/astral-sh/ruff/pull/12409)) + +## 0.5.5 + +### Preview features + +- \[`fastapi`\] Implement `fastapi-redundant-response-model` (`FAST001`) and `fastapi-non-annotated-dependency`(`FAST002`) ([#11579](https://github.com/astral-sh/ruff/pull/11579)) +- \[`pydoclint`\] Implement `docstring-missing-exception` (`DOC501`) and `docstring-extraneous-exception` (`DOC502`) ([#11471](https://github.com/astral-sh/ruff/pull/11471)) + +### Rule changes + +- \[`numpy`\] Fix NumPy 2.0 rule for `np.alltrue` and `np.sometrue` ([#12473](https://github.com/astral-sh/ruff/pull/12473)) +- \[`numpy`\] Ignore `NPY201` inside `except` blocks for compatibility with older numpy versions ([#12490](https://github.com/astral-sh/ruff/pull/12490)) +- \[`pep8-naming`\] Avoid applying `ignore-names` to `self` and `cls` function names (`N804`, `N805`) ([#12497](https://github.com/astral-sh/ruff/pull/12497)) + +### Formatter + +- Fix incorrect placement of leading function comment with type params ([#12447](https://github.com/astral-sh/ruff/pull/12447)) + +### Server + +- Do not bail code action resolution when a quick fix is requested ([#12462](https://github.com/astral-sh/ruff/pull/12462)) + +### Bug fixes + +- Fix `Ord` implementation of `cmp_fix` ([#12471](https://github.com/astral-sh/ruff/pull/12471)) +- Raise syntax error for unparenthesized generator expression in multi-argument call ([#12445](https://github.com/astral-sh/ruff/pull/12445)) +- \[`pydoclint`\] Fix panic in `DOC501` reported in [#12428](https://github.com/astral-sh/ruff/pull/12428) ([#12435](https://github.com/astral-sh/ruff/pull/12435)) +- \[`flake8-bugbear`\] Allow singleton tuples with starred expressions in `B013` ([#12484](https://github.com/astral-sh/ruff/pull/12484)) + +### Documentation + +- Add Eglot setup guide for Emacs editor ([#12426](https://github.com/astral-sh/ruff/pull/12426)) +- Add note about the breaking change in `nvim-lspconfig` ([#12507](https://github.com/astral-sh/ruff/pull/12507)) +- Add note to include notebook files for native server ([#12449](https://github.com/astral-sh/ruff/pull/12449)) +- Add setup docs for Zed editor ([#12501](https://github.com/astral-sh/ruff/pull/12501)) + +## 0.5.6 + +Ruff 0.5.6 automatically enables linting and formatting of notebooks in _preview mode_. +You can opt-out of this behavior by adding `*.ipynb` to the `extend-exclude` setting. + +```toml +[tool.ruff] +extend-exclude = ["*.ipynb"] +``` + +### Preview features + +- Enable notebooks by default in preview mode ([#12621](https://github.com/astral-sh/ruff/pull/12621)) +- \[`flake8-builtins`\] Implement import, lambda, and module shadowing ([#12546](https://github.com/astral-sh/ruff/pull/12546)) +- \[`pydoclint`\] Add `docstring-missing-returns` (`DOC201`) and `docstring-extraneous-returns` (`DOC202`) ([#12485](https://github.com/astral-sh/ruff/pull/12485)) + +### Rule changes + +- \[`flake8-return`\] Exempt cached properties and other property-like decorators from explicit return rule (`RET501`) ([#12563](https://github.com/astral-sh/ruff/pull/12563)) + +### Server + +- Make server panic hook more error resilient ([#12610](https://github.com/astral-sh/ruff/pull/12610)) +- Use `$/logTrace` for server trace logs in Zed and VS Code ([#12564](https://github.com/astral-sh/ruff/pull/12564)) +- Keep track of deleted cells for reorder change request ([#12575](https://github.com/astral-sh/ruff/pull/12575)) + +### Configuration + +- \[`flake8-implicit-str-concat`\] Always allow explicit multi-line concatenations when implicit concatenations are banned ([#12532](https://github.com/astral-sh/ruff/pull/12532)) + +### Bug fixes + +- \[`flake8-async`\] Avoid flagging `asyncio.timeout`s as unused when the context manager includes `asyncio.TaskGroup` ([#12605](https://github.com/astral-sh/ruff/pull/12605)) +- \[`flake8-slots`\] Avoid recommending `__slots__` for classes that inherit from more than `namedtuple` ([#12531](https://github.com/astral-sh/ruff/pull/12531)) +- \[`isort`\] Avoid marking required imports as unused ([#12537](https://github.com/astral-sh/ruff/pull/12537)) +- \[`isort`\] Preserve trailing inline comments on import-from statements ([#12498](https://github.com/astral-sh/ruff/pull/12498)) +- \[`pycodestyle`\] Add newlines before comments (`E305`) ([#12606](https://github.com/astral-sh/ruff/pull/12606)) +- \[`pycodestyle`\] Don't attach comments with mismatched indents ([#12604](https://github.com/astral-sh/ruff/pull/12604)) +- \[`pyflakes`\] Fix preview-mode bugs in `F401` when attempting to autofix unused first-party submodule imports in an `__init__.py` file ([#12569](https://github.com/astral-sh/ruff/pull/12569)) +- \[`pylint`\] Respect start index in `unnecessary-list-index-lookup` ([#12603](https://github.com/astral-sh/ruff/pull/12603)) +- \[`pyupgrade`\] Avoid recommending no-argument super in `slots=True` dataclasses ([#12530](https://github.com/astral-sh/ruff/pull/12530)) +- \[`pyupgrade`\] Use colon rather than dot formatting for integer-only types ([#12534](https://github.com/astral-sh/ruff/pull/12534)) +- Fix NFKC normalization bug when removing unused imports ([#12571](https://github.com/astral-sh/ruff/pull/12571)) + +### Other changes + +- Consider more stdlib decorators to be property-like ([#12583](https://github.com/astral-sh/ruff/pull/12583)) +- Improve handling of metaclasses in various linter rules ([#12579](https://github.com/astral-sh/ruff/pull/12579)) +- Improve consistency between linter rules in determining whether a function is property ([#12581](https://github.com/astral-sh/ruff/pull/12581)) + +## 0.5.7 + +### Preview features + +- \[`flake8-comprehensions`\] Account for list and set comprehensions in `unnecessary-literal-within-tuple-call` (`C409`) ([#12657](https://github.com/astral-sh/ruff/pull/12657)) +- \[`flake8-pyi`\] Add autofix for `future-annotations-in-stub` (`PYI044`) ([#12676](https://github.com/astral-sh/ruff/pull/12676)) +- \[`flake8-return`\] Avoid syntax error when auto-fixing `RET505` with mixed indentation (space and tabs) ([#12740](https://github.com/astral-sh/ruff/pull/12740)) +- \[`pydoclint`\] Add `docstring-missing-yields` (`DOC402`) and `docstring-extraneous-yields` (`DOC403`) ([#12538](https://github.com/astral-sh/ruff/pull/12538)) +- \[`pydoclint`\] Avoid `DOC201` if docstring begins with "Return", "Returns", "Yield", or "Yields" ([#12675](https://github.com/astral-sh/ruff/pull/12675)) +- \[`pydoclint`\] Deduplicate collected exceptions after traversing function bodies (`DOC501`) ([#12642](https://github.com/astral-sh/ruff/pull/12642)) +- \[`pydoclint`\] Ignore `DOC` errors for stub functions ([#12651](https://github.com/astral-sh/ruff/pull/12651)) +- \[`pydoclint`\] Teach rules to understand reraised exceptions as being explicitly raised (`DOC501`, `DOC502`) ([#12639](https://github.com/astral-sh/ruff/pull/12639)) +- \[`ruff`\] Implement `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#12480](https://github.com/astral-sh/ruff/pull/12480)) +- \[`ruff`\] Mark `RUF023` fix as unsafe if `__slots__` is not a set and the binding is used elsewhere ([#12692](https://github.com/astral-sh/ruff/pull/12692)) + +### Rule changes + +- \[`refurb`\] Add autofix for `implicit-cwd` (`FURB177`) ([#12708](https://github.com/astral-sh/ruff/pull/12708)) +- \[`ruff`\] Add autofix for `zip-instead-of-pairwise` (`RUF007`) ([#12663](https://github.com/astral-sh/ruff/pull/12663)) +- \[`tryceratops`\] Add `BaseException` to `raise-vanilla-class` rule (`TRY002`) ([#12620](https://github.com/astral-sh/ruff/pull/12620)) + +### Server + +- Ignore non-file workspace URL; Ruff will display a warning notification in this case ([#12725](https://github.com/astral-sh/ruff/pull/12725)) + +### CLI + +- Fix cache invalidation for nested `pyproject.toml` files ([#12727](https://github.com/astral-sh/ruff/pull/12727)) + +### Bug fixes + +- \[`flake8-async`\] Fix false positives with multiple `async with` items (`ASYNC100`) ([#12643](https://github.com/astral-sh/ruff/pull/12643)) +- \[`flake8-bandit`\] Avoid false-positives for list concatenations in SQL construction (`S608`) ([#12720](https://github.com/astral-sh/ruff/pull/12720)) +- \[`flake8-bugbear`\] Treat `return` as equivalent to `break` (`B909`) ([#12646](https://github.com/astral-sh/ruff/pull/12646)) +- \[`flake8-comprehensions`\] Set comprehensions not a violation for `sum` in `unnecessary-comprehension-in-call` (`C419`) ([#12691](https://github.com/astral-sh/ruff/pull/12691)) +- \[`flake8-simplify`\] Parenthesize conditions based on precedence when merging if arms (`SIM114`) ([#12737](https://github.com/astral-sh/ruff/pull/12737)) +- \[`pydoclint`\] Try both 'Raises' section styles when convention is unspecified (`DOC501`) ([#12649](https://github.com/astral-sh/ruff/pull/12649)) diff --git a/changelogs/0.6.x.md b/changelogs/0.6.x.md new file mode 100644 index 0000000000000..febdfa35398c5 --- /dev/null +++ b/changelogs/0.6.x.md @@ -0,0 +1,331 @@ +# Changelog 0.6.x + +## 0.6.0 + +Check out the [blog post](https://astral.sh/blog/ruff-v0.6.0) for a migration guide and overview of the changes! + +### Breaking changes + +See also, the "Remapped rules" section which may result in disabled rules. + +- Lint and format Jupyter Notebook by default ([#12878](https://github.com/astral-sh/ruff/pull/12878)). +- Detect imports in `src` layouts by default for `isort` rules ([#12848](https://github.com/astral-sh/ruff/pull/12848)) +- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments ([#12838](https://github.com/astral-sh/ruff/pull/12838)). + +### Deprecations + +The following rules are now deprecated: + +- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`) +- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`) +- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`) + +### Remapped rules + +The following rules have been remapped to new rule codes: + +- [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable/): `RUF025` to `C420` + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`singledispatch-method`](https://docs.astral.sh/ruff/rules/singledispatch-method/) (`PLE1519`) +- [`singledispatchmethod-function`](https://docs.astral.sh/ruff/rules/singledispatchmethod-function/) (`PLE1520`) +- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) +- [`if-stmt-min-max`](https://docs.astral.sh/ruff/rules/if-stmt-min-max/) (`PLR1730`) +- [`invalid-bytes-return-type`](https://docs.astral.sh/ruff/rules/invalid-bytes-return-type/) (`PLE0308`) +- [`invalid-hash-return-type`](https://docs.astral.sh/ruff/rules/invalid-hash-return-type/) (`PLE0309`) +- [`invalid-index-return-type`](https://docs.astral.sh/ruff/rules/invalid-index-return-type/) (`PLE0305`) +- [`invalid-length-return-type`](https://docs.astral.sh/ruff/rules/invalid-length-return-type/) (`PLEE303`) +- [`self-or-cls-assignment`](https://docs.astral.sh/ruff/rules/self-or-cls-assignment/) (`PLW0642`) +- [`byte-string-usage`](https://docs.astral.sh/ruff/rules/byte-string-usage/) (`PYI057`) +- [`duplicate-literal-member`](https://docs.astral.sh/ruff/rules/duplicate-literal-member/) (`PYI062`) +- [`redirected-noqa`](https://docs.astral.sh/ruff/rules/redirected-noqa/) (`RUF101`) + +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 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: + +- [`superfluous-else-return`](https://docs.astral.sh/ruff/rules/superfluous-else-return/) (`RET505`) +- [`superfluous-else-raise`](https://docs.astral.sh/ruff/rules/superfluous-else-raise/) (`RET506`) +- [`superfluous-else-continue`](https://docs.astral.sh/ruff/rules/superfluous-else-continue/) (`RET507`) +- [`superfluous-else-break`](https://docs.astral.sh/ruff/rules/superfluous-else-break/) (`RET508`) + +### Preview features + +- \[`flake8-simplify`\] Further simplify to binary in preview for (`SIM108`) ([#12796](https://github.com/astral-sh/ruff/pull/12796)) +- \[`pyupgrade`\] Show violations without auto-fix (`UP031`) ([#11229](https://github.com/astral-sh/ruff/pull/11229)) + +### Rule changes + +- \[`flake8-import-conventions`\] Add `xml.etree.ElementTree` to default conventions ([#12455](https://github.com/astral-sh/ruff/pull/12455)) +- \[`flake8-pytest-style`\] Add a space after comma in CSV output (`PT006`) ([#12853](https://github.com/astral-sh/ruff/pull/12853)) + +### Server + +- Show a message for incorrect settings ([#12781](https://github.com/astral-sh/ruff/pull/12781)) + +### Bug fixes + +- \[`flake8-async`\] Do not lint yield in context manager (`ASYNC100`) ([#12896](https://github.com/astral-sh/ruff/pull/12896)) +- \[`flake8-comprehensions`\] Do not lint `async for` comprehensions (`C419`) ([#12895](https://github.com/astral-sh/ruff/pull/12895)) +- \[`flake8-return`\] Only add return `None` at end of a function (`RET503`) ([#11074](https://github.com/astral-sh/ruff/pull/11074)) +- \[`flake8-type-checking`\] Avoid treating `dataclasses.KW_ONLY` as typing-only (`TCH003`) ([#12863](https://github.com/astral-sh/ruff/pull/12863)) +- \[`pep8-naming`\] Treat `type(Protocol)` et al as metaclass base (`N805`) ([#12770](https://github.com/astral-sh/ruff/pull/12770)) +- \[`pydoclint`\] Don't enforce returns and yields in abstract methods (`DOC201`, `DOC202`) ([#12771](https://github.com/astral-sh/ruff/pull/12771)) +- \[`ruff`\] Skip tuples with slice expressions in (`RUF031`) ([#12768](https://github.com/astral-sh/ruff/pull/12768)) +- \[`ruff`\] Ignore unparenthesized tuples in subscripts when the subscript is a type annotation or type alias (`RUF031`) ([#12762](https://github.com/astral-sh/ruff/pull/12762)) +- \[`ruff`\] Ignore template strings passed to logging and `builtins._()` calls (`RUF027`) ([#12889](https://github.com/astral-sh/ruff/pull/12889)) +- \[`ruff`\] Do not remove parens for tuples with starred expressions in Python \<=3.10 (`RUF031`) ([#12784](https://github.com/astral-sh/ruff/pull/12784)) +- Evaluate default parameter values for a function in that function's enclosing scope ([#12852](https://github.com/astral-sh/ruff/pull/12852)) + +### Other changes + +- Respect VS Code cell metadata when detecting the language of Jupyter Notebook cells ([#12864](https://github.com/astral-sh/ruff/pull/12864)) +- Respect `kernelspec` notebook metadata when detecting the preferred language for a Jupyter Notebook ([#12875](https://github.com/astral-sh/ruff/pull/12875)) + +## 0.6.1 + +This is a hotfix release to address an issue with `ruff-pre-commit`. In v0.6, +Ruff changed its behavior to lint and format Jupyter notebooks by default; +however, due to an oversight, these files were still excluded by default if +Ruff was run via pre-commit, leading to inconsistent behavior. +This has [now been fixed](https://github.com/astral-sh/ruff-pre-commit/pull/96). + +### Preview features + +- \[`fastapi`\] Implement `fast-api-unused-path-parameter` (`FAST003`) ([#12638](https://github.com/astral-sh/ruff/pull/12638)) + +### Rule changes + +- \[`pylint`\] Rename `too-many-positional` to `too-many-positional-arguments` (`R0917`) ([#12905](https://github.com/astral-sh/ruff/pull/12905)) + +### Server + +- Fix crash when applying "fix-all" code-action to notebook cells ([#12929](https://github.com/astral-sh/ruff/pull/12929)) + +### Other changes + +- \[`flake8-naming`\]: Respect import conventions (`N817`) ([#12922](https://github.com/astral-sh/ruff/pull/12922)) + +## 0.6.2 + +### Preview features + +- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with other standard-library IO modules (`SIM115`) ([#12959](https://github.com/astral-sh/ruff/pull/12959)) +- \[`ruff`\] Avoid `unused-async` for functions with FastAPI route decorator (`RUF029`) ([#12938](https://github.com/astral-sh/ruff/pull/12938)) +- \[`ruff`\] Ignore `fstring-missing-syntax` (`RUF027`) for `fastAPI` paths ([#12939](https://github.com/astral-sh/ruff/pull/12939)) +- \[`ruff`\] Implement check for Decimal called with a float literal (RUF032) ([#12909](https://github.com/astral-sh/ruff/pull/12909)) + +### Rule changes + +- \[`flake8-bugbear`\] Update diagnostic message when expression is at the end of function (`B015`) ([#12944](https://github.com/astral-sh/ruff/pull/12944)) +- \[`flake8-pyi`\] Skip type annotations in `string-or-bytes-too-long` (`PYI053`) ([#13002](https://github.com/astral-sh/ruff/pull/13002)) +- \[`flake8-type-checking`\] Always recognise relative imports as first-party ([#12994](https://github.com/astral-sh/ruff/pull/12994)) +- \[`flake8-unused-arguments`\] Ignore unused arguments on stub functions (`ARG001`) ([#12966](https://github.com/astral-sh/ruff/pull/12966)) +- \[`pylint`\] Ignore augmented assignment for `self-cls-assignment` (`PLW0642`) ([#12957](https://github.com/astral-sh/ruff/pull/12957)) + +### Server + +- Show full context in error log messages ([#13029](https://github.com/astral-sh/ruff/pull/13029)) + +### Bug fixes + +- \[`pep8-naming`\] Don't flag `from` imports following conventional import names (`N817`) ([#12946](https://github.com/astral-sh/ruff/pull/12946)) +- \[`pylint`\] - Allow `__new__` methods to have `cls` as their first argument even if decorated with `@staticmethod` for `bad-staticmethod-argument` (`PLW0211`) ([#12958](https://github.com/astral-sh/ruff/pull/12958)) + +### Documentation + +- Add `hyperfine` installation instructions; update `hyperfine` code samples ([#13034](https://github.com/astral-sh/ruff/pull/13034)) +- Expand note to use Ruff with other language server in Kate ([#12806](https://github.com/astral-sh/ruff/pull/12806)) +- Update example for `PT001` as per the new default behavior ([#13019](https://github.com/astral-sh/ruff/pull/13019)) +- \[`perflint`\] Improve docs for `try-except-in-loop` (`PERF203`) ([#12947](https://github.com/astral-sh/ruff/pull/12947)) +- \[`pydocstyle`\] Add reference to `lint.pydocstyle.ignore-decorators` setting to rule docs ([#12996](https://github.com/astral-sh/ruff/pull/12996)) + +## 0.6.3 + +### Preview features + +- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with `dbm.sqlite3` (`SIM115`) ([#13104](https://github.com/astral-sh/ruff/pull/13104)) +- \[`pycodestyle`\] Disable `E741` in stub files (`.pyi`) ([#13119](https://github.com/astral-sh/ruff/pull/13119)) +- \[`pydoclint`\] Avoid `DOC201` on explicit returns in functions that only return `None` ([#13064](https://github.com/astral-sh/ruff/pull/13064)) + +### Rule changes + +- \[`flake8-async`\] Disable check for `asyncio` before Python 3.11 (`ASYNC109`) ([#13023](https://github.com/astral-sh/ruff/pull/13023)) + +### Bug fixes + +- \[`FastAPI`\] Avoid introducing invalid syntax in fix for `fast-api-non-annotated-dependency` (`FAST002`) ([#13133](https://github.com/astral-sh/ruff/pull/13133)) +- \[`flake8-implicit-str-concat`\] Normalize octals before merging concatenated strings in `single-line-implicit-string-concatenation` (`ISC001`) ([#13118](https://github.com/astral-sh/ruff/pull/13118)) +- \[`flake8-pytest-style`\] Improve help message for `pytest-incorrect-mark-parentheses-style` (`PT023`) ([#13092](https://github.com/astral-sh/ruff/pull/13092)) +- \[`pylint`\] Avoid autofix for calls that aren't `min` or `max` as starred expression (`PLW3301`) ([#13089](https://github.com/astral-sh/ruff/pull/13089)) +- \[`ruff`\] Add `datetime.time`, `datetime.tzinfo`, and `datetime.timezone` as immutable function calls (`RUF009`) ([#13109](https://github.com/astral-sh/ruff/pull/13109)) +- \[`ruff`\] Extend comment deletion for `RUF100` to include trailing text from `noqa` directives while preserving any following comments on the same line, if any ([#13105](https://github.com/astral-sh/ruff/pull/13105)) +- Fix dark theme on initial page load for the Ruff playground ([#13077](https://github.com/astral-sh/ruff/pull/13077)) + +## 0.6.4 + +### Preview features + +- \[`flake8-builtins`\] Use dynamic builtins list based on Python version ([#13172](https://github.com/astral-sh/ruff/pull/13172)) +- \[`pydoclint`\] Permit yielding `None` in `DOC402` and `DOC403` ([#13148](https://github.com/astral-sh/ruff/pull/13148)) +- \[`pylint`\] Update diagnostic message for `PLW3201` ([#13194](https://github.com/astral-sh/ruff/pull/13194)) +- \[`ruff`\] Implement `post-init-default` (`RUF033`) ([#13192](https://github.com/astral-sh/ruff/pull/13192)) +- \[`ruff`\] Implement useless if-else (`RUF034`) ([#13218](https://github.com/astral-sh/ruff/pull/13218)) + +### Rule changes + +- \[`flake8-pyi`\] Respect `pep8_naming.classmethod-decorators` settings when determining if a method is a classmethod in `custom-type-var-return-type` (`PYI019`) ([#13162](https://github.com/astral-sh/ruff/pull/13162)) +- \[`flake8-pyi`\] Teach various rules that annotations might be stringized ([#12951](https://github.com/astral-sh/ruff/pull/12951)) +- \[`pylint`\] Avoid `no-self-use` for `attrs`-style validators ([#13166](https://github.com/astral-sh/ruff/pull/13166)) +- \[`pylint`\] Recurse into subscript subexpressions when searching for list/dict lookups (`PLR1733`, `PLR1736`) ([#13186](https://github.com/astral-sh/ruff/pull/13186)) +- \[`pyupgrade`\] Detect `aiofiles.open` calls in `UP015` ([#13173](https://github.com/astral-sh/ruff/pull/13173)) +- \[`pyupgrade`\] Mark `sys.version_info[0] < 3` and similar comparisons as outdated (`UP036`) ([#13175](https://github.com/astral-sh/ruff/pull/13175)) + +### CLI + +- Enrich messages of SARIF results ([#13180](https://github.com/astral-sh/ruff/pull/13180)) +- Handle singular case for incompatible rules warning in `ruff format` output ([#13212](https://github.com/astral-sh/ruff/pull/13212)) + +### Bug fixes + +- \[`pydocstyle`\] Improve heuristics for detecting Google-style docstrings ([#13142](https://github.com/astral-sh/ruff/pull/13142)) +- \[`refurb`\] Treat `sep` arguments with effects as unsafe removals (`FURB105`) ([#13165](https://github.com/astral-sh/ruff/pull/13165)) + +## 0.6.5 + +### Preview features + +- \[`pydoclint`\] Ignore `DOC201` when function name is "**new**" ([#13300](https://github.com/astral-sh/ruff/pull/13300)) +- \[`refurb`\] Implement `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#13256](https://github.com/astral-sh/ruff/pull/13256)) + +### Rule changes + +- \[`eradicate`\] Ignore script-comments with multiple end-tags (`ERA001`) ([#13283](https://github.com/astral-sh/ruff/pull/13283)) +- \[`pyflakes`\] Improve error message for `UndefinedName` when a builtin was added in a newer version than specified in Ruff config (`F821`) ([#13293](https://github.com/astral-sh/ruff/pull/13293)) + +### Server + +- Add support for extensionless Python files for server ([#13326](https://github.com/astral-sh/ruff/pull/13326)) +- Fix configuration inheritance for configurations specified in the LSP settings ([#13285](https://github.com/astral-sh/ruff/pull/13285)) + +### Bug fixes + +- \[`ruff`\] Handle unary operators in `decimal-from-float-literal` (`RUF032`) ([#13275](https://github.com/astral-sh/ruff/pull/13275)) + +### CLI + +- Only include rules with diagnostics in SARIF metadata ([#13268](https://github.com/astral-sh/ruff/pull/13268)) + +### Playground + +- Add "Copy as pyproject.toml/ruff.toml" and "Paste from TOML" ([#13328](https://github.com/astral-sh/ruff/pull/13328)) +- Fix errors not shown for restored snippet on page load ([#13262](https://github.com/astral-sh/ruff/pull/13262)) + +## 0.6.6 + +### Preview features + +- \[`refurb`\] Skip `slice-to-remove-prefix-or-suffix` (`FURB188`) when non-trivial slice steps are present ([#13405](https://github.com/astral-sh/ruff/pull/13405)) +- Add a subcommand to generate dependency graphs ([#13402](https://github.com/astral-sh/ruff/pull/13402)) + +### Formatter + +- Fix placement of inline parameter comments ([#13379](https://github.com/astral-sh/ruff/pull/13379)) + +### Server + +- Fix off-by one error in the `LineIndex::offset` calculation ([#13407](https://github.com/astral-sh/ruff/pull/13407)) + +### Bug fixes + +- \[`fastapi`\] Respect FastAPI aliases in route definitions ([#13394](https://github.com/astral-sh/ruff/pull/13394)) +- \[`pydocstyle`\] Respect word boundaries when detecting function signature in docs ([#13388](https://github.com/astral-sh/ruff/pull/13388)) + +### Documentation + +- Add backlinks to rule overview linter ([#13368](https://github.com/astral-sh/ruff/pull/13368)) +- Fix documentation for editor vim plugin ALE ([#13348](https://github.com/astral-sh/ruff/pull/13348)) +- Fix rendering of `FURB188` docs ([#13406](https://github.com/astral-sh/ruff/pull/13406)) + +## 0.6.7 + +### Preview features + +- Add Python version support to ruff analyze CLI ([#13426](https://github.com/astral-sh/ruff/pull/13426)) +- Add `exclude` support to `ruff analyze` ([#13425](https://github.com/astral-sh/ruff/pull/13425)) +- Fix parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381)) + +### Rule changes + +- \[`pycodestyle`\] Fix: Don't autofix if the first line ends in a question mark? (D400) ([#13399](https://github.com/astral-sh/ruff/pull/13399)) + +### Bug fixes + +- Respect `lint.exclude` in ruff check `--add-noqa` ([#13427](https://github.com/astral-sh/ruff/pull/13427)) + +### Performance + +- Avoid tracking module resolver files in Salsa ([#13437](https://github.com/astral-sh/ruff/pull/13437)) +- Use `forget` for module resolver database ([#13438](https://github.com/astral-sh/ruff/pull/13438)) + +## 0.6.8 + +### Preview features + +- Remove unnecessary parentheses around `match case` clauses ([#13510](https://github.com/astral-sh/ruff/pull/13510)) +- Parenthesize overlong `if` guards in `match..case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513)) +- Detect basic wildcard imports in `ruff analyze graph` ([#13486](https://github.com/astral-sh/ruff/pull/13486)) +- \[`pylint`\] Implement `boolean-chained-comparison` (`R1716`) ([#13435](https://github.com/astral-sh/ruff/pull/13435)) + +### Rule changes + +- \[`lake8-simplify`\] Detect `SIM910` when using variadic keyword arguments, i.e., `**kwargs` ([#13503](https://github.com/astral-sh/ruff/pull/13503)) +- \[`pyupgrade`\] Avoid false negatives with non-reference shadowed bindings of loop variables (`UP028`) ([#13504](https://github.com/astral-sh/ruff/pull/13504)) + +### Bug fixes + +- Detect tuples bound to variadic positional arguments i.e. `*args` ([#13512](https://github.com/astral-sh/ruff/pull/13512)) +- Exit gracefully on broken pipe errors ([#13485](https://github.com/astral-sh/ruff/pull/13485)) +- Avoid panic when analyze graph hits broken pipe ([#13484](https://github.com/astral-sh/ruff/pull/13484)) + +### Performance + +- Reuse `BTreeSets` in module resolver ([#13440](https://github.com/astral-sh/ruff/pull/13440)) +- Skip traversal for non-compound statements ([#13441](https://github.com/astral-sh/ruff/pull/13441)) + +## 0.6.9 + +### Preview features + +- Fix codeblock dynamic line length calculation for indented docstring examples ([#13523](https://github.com/astral-sh/ruff/pull/13523)) +- \[`refurb`\] Mark `FURB118` fix as unsafe ([#13613](https://github.com/astral-sh/ruff/pull/13613)) + +### Rule changes + +- \[`pydocstyle`\] Don't raise `D208` when last line is non-empty ([#13372](https://github.com/astral-sh/ruff/pull/13372)) +- \[`pylint`\] Preserve trivia (i.e. comments) in `PLR5501` autofix ([#13573](https://github.com/astral-sh/ruff/pull/13573)) + +### Configuration + +- \[`pyflakes`\] Add `allow-unused-imports` setting for `unused-import` rule (`F401`) ([#13601](https://github.com/astral-sh/ruff/pull/13601)) + +### Bug fixes + +- Support ruff discovery in pip build environments ([#13591](https://github.com/astral-sh/ruff/pull/13591)) +- \[`flake8-bugbear`\] Avoid short circuiting `B017` for multiple context managers ([#13609](https://github.com/astral-sh/ruff/pull/13609)) +- \[`pylint`\] Do not offer an invalid fix for `PLR1716` when the comparisons contain parenthesis ([#13527](https://github.com/astral-sh/ruff/pull/13527)) +- \[`pyupgrade`\] Fix `UP043` to apply to `collections.abc.Generator` and `collections.abc.AsyncGenerator` ([#13611](https://github.com/astral-sh/ruff/pull/13611)) +- \[`refurb`\] Fix handling of slices in tuples for `FURB118`, e.g., `x[:, 1]` ([#13518](https://github.com/astral-sh/ruff/pull/13518)) + +### Documentation + +- Update GitHub Action link to `astral-sh/ruff-action` ([#13551](https://github.com/astral-sh/ruff/pull/13551)) diff --git a/changelogs/0.7.x.md b/changelogs/0.7.x.md new file mode 100644 index 0000000000000..883e785cba171 --- /dev/null +++ b/changelogs/0.7.x.md @@ -0,0 +1,185 @@ +# Changelog 0.7.x + +## 0.7.0 + +Check out the [blog post](https://astral.sh/blog/ruff-v0.7.0) for a migration guide and overview of the changes! + +### Breaking changes + +- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments + ([#12838](https://github.com/astral-sh/ruff/pull/12838), [#13292](https://github.com/astral-sh/ruff/pull/13292)). + This was a change that we attempted to make in Ruff v0.6.0, but only partially made due to an error on our part. + See the [blog post](https://astral.sh/blog/ruff-v0.7.0) for more details. +- The `useless-try-except` rule (in our `tryceratops` category) has been recoded from `TRY302` to + `TRY203` ([#13502](https://github.com/astral-sh/ruff/pull/13502)). This ensures Ruff's code is consistent with + the same rule in the [`tryceratops`](https://github.com/guilatrova/tryceratops) linter. +- The `lint.allow-unused-imports` setting has been removed ([#13677](https://github.com/astral-sh/ruff/pull/13677)). Use + [`lint.pyflakes.allow-unused-imports`](https://docs.astral.sh/ruff/settings/#lint_pyflakes_allowed-unused-imports) + instead. + +### Formatter preview style + +- Normalize implicit concatenated f-string quotes per part ([#13539](https://github.com/astral-sh/ruff/pull/13539)) + +### Preview linter features + +- \[`refurb`\] implement `hardcoded-string-charset` (FURB156) ([#13530](https://github.com/astral-sh/ruff/pull/13530)) +- \[`refurb`\] Count codepoints not bytes for `slice-to-remove-prefix-or-suffix (FURB188)` ([#13631](https://github.com/astral-sh/ruff/pull/13631)) + +### Rule changes + +- \[`pylint`\] Mark `PLE1141` fix as unsafe ([#13629](https://github.com/astral-sh/ruff/pull/13629)) +- \[`flake8-async`\] Consider async generators to be "checkpoints" for `cancel-scope-no-checkpoint` (`ASYNC100`) ([#13639](https://github.com/astral-sh/ruff/pull/13639)) +- \[`flake8-bugbear`\] Do not suggest setting parameter `strict=` to `False` in `B905` diagnostic message ([#13656](https://github.com/astral-sh/ruff/pull/13656)) +- \[`flake8-todos`\] Only flag the word "TODO", not words starting with "todo" (`TD006`) ([#13640](https://github.com/astral-sh/ruff/pull/13640)) +- \[`pycodestyle`\] Fix whitespace-related false positives and false negatives inside type-parameter lists (`E231`, `E251`) ([#13704](https://github.com/astral-sh/ruff/pull/13704)) +- \[`flake8-simplify`\] Stabilize preview behavior for `SIM115` so that the rule can detect files + being opened from a wider range of standard-library functions ([#12959](https://github.com/astral-sh/ruff/pull/12959)). + +### CLI + +- Add explanation of fixable in `--statistics` command ([#13774](https://github.com/astral-sh/ruff/pull/13774)) + +### Bug fixes + +- \[`pyflakes`\] Allow `ipytest` cell magic (`F401`) ([#13745](https://github.com/astral-sh/ruff/pull/13745)) +- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor ([#13616](https://github.com/astral-sh/ruff/pull/13616)) +- \[`flake8-bandit`\] Detect patterns from multi line SQL statements (`S608`) ([#13574](https://github.com/astral-sh/ruff/pull/13574)) +- \[`flake8-pyi`\] - Fix dropped expressions in `PYI030` autofix ([#13727](https://github.com/astral-sh/ruff/pull/13727)) + +## 0.7.1 + +### Preview features + +- Fix `E221` and `E222` to flag missing or extra whitespace around `==` operator ([#13890](https://github.com/astral-sh/ruff/pull/13890)) +- Formatter: Alternate quotes for strings inside f-strings in preview ([#13860](https://github.com/astral-sh/ruff/pull/13860)) +- Formatter: Join implicit concatenated strings when they fit on a line ([#13663](https://github.com/astral-sh/ruff/pull/13663)) +- \[`pylint`\] Restrict `iteration-over-set` to only work on sets of literals (`PLC0208`) ([#13731](https://github.com/astral-sh/ruff/pull/13731)) + +### Rule changes + +- \[`flake8-type-checking`\] Support auto-quoting when annotations contain quotes ([#11811](https://github.com/astral-sh/ruff/pull/11811)) + +### Server + +- Avoid indexing the workspace for single-file mode ([#13770](https://github.com/astral-sh/ruff/pull/13770)) + +### Bug fixes + +- Make `ARG002` compatible with `EM101` when raising `NotImplementedError` ([#13714](https://github.com/astral-sh/ruff/pull/13714)) + +### Other changes + +- Introduce more Docker tags for Ruff (similar to uv) ([#13274](https://github.com/astral-sh/ruff/pull/13274)) + +## 0.7.2 + +### Preview features + +- Fix formatting of single with-item with trailing comment ([#14005](https://github.com/astral-sh/ruff/pull/14005)) +- \[`pyupgrade`\] Add PEP 646 `Unpack` conversion to `*` with fix (`UP044`) ([#13988](https://github.com/astral-sh/ruff/pull/13988)) + +### Rule changes + +- Regenerate `known_stdlibs.rs` with stdlibs 2024.10.25 ([#13963](https://github.com/astral-sh/ruff/pull/13963)) +- \[`flake8-no-pep420`\] Skip namespace package enforcement for PEP 723 scripts (`INP001`) ([#13974](https://github.com/astral-sh/ruff/pull/13974)) + +### Server + +- Fix server panic when undoing an edit ([#14010](https://github.com/astral-sh/ruff/pull/14010)) + +### Bug fixes + +- Fix issues in discovering ruff in pip build environments ([#13881](https://github.com/astral-sh/ruff/pull/13881)) +- \[`flake8-type-checking`\] Fix false positive for `singledispatchmethod` (`TCH003`) ([#13941](https://github.com/astral-sh/ruff/pull/13941)) +- \[`flake8-type-checking`\] Treat return type of `singledispatch` as runtime-required (`TCH003`) ([#13957](https://github.com/astral-sh/ruff/pull/13957)) + +### Documentation + +- \[`flake8-simplify`\] Include caveats of enabling `if-else-block-instead-of-if-exp` (`SIM108`) ([#14019](https://github.com/astral-sh/ruff/pull/14019)) + +## 0.7.3 + +### Preview features + +- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928)) +- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059)) +- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008)) +- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105)) +- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068)) + +### Rule changes + +- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064)) +- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094)) +- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150)) +- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to always provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188)) +- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065)) +- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061)) + +### Bug fixes + +- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809)) +- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069)) +- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058)) +- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144)) +- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097)) +- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066)) +- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063)) +- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146)) +- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053)) +- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098)) + +### Documentation + +- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971)) +- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040)) + +## 0.7.4 + +### Preview features + +- \[`flake8-datetimez`\] Detect usages of `datetime.max`/`datetime.min` (`DTZ901`) ([#14288](https://github.com/astral-sh/ruff/pull/14288)) +- \[`flake8-logging`\] Implement `root-logger-calls` (`LOG015`) ([#14302](https://github.com/astral-sh/ruff/pull/14302)) +- \[`flake8-no-pep420`\] Detect empty implicit namespace packages (`INP001`) ([#14236](https://github.com/astral-sh/ruff/pull/14236)) +- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI019`) ([#14238](https://github.com/astral-sh/ruff/pull/14238)) +- \[`perflint`\] Implement quick-fix for `manual-list-comprehension` (`PERF401`) ([#13919](https://github.com/astral-sh/ruff/pull/13919)) +- \[`pylint`\] Implement `shallow-copy-environ` (`W1507`) ([#14241](https://github.com/astral-sh/ruff/pull/14241)) +- \[`ruff`\] Implement `none-not-at-end-of-union` (`RUF036`) ([#14314](https://github.com/astral-sh/ruff/pull/14314)) +- \[`ruff`\] Implementation `unsafe-markup-call` from `flake8-markupsafe` plugin (`RUF035`) ([#14224](https://github.com/astral-sh/ruff/pull/14224)) +- \[`ruff`\] Report problems for `attrs` dataclasses (`RUF008`, `RUF009`) ([#14327](https://github.com/astral-sh/ruff/pull/14327)) + +### Rule changes + +- \[`flake8-boolean-trap`\] Exclude dunder methods that define operators (`FBT001`) ([#14203](https://github.com/astral-sh/ruff/pull/14203)) +- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI034`) ([#14217](https://github.com/astral-sh/ruff/pull/14217)) +- \[`flake8-pyi`\] Always autofix `duplicate-union-members` (`PYI016`) ([#14270](https://github.com/astral-sh/ruff/pull/14270)) +- \[`flake8-pyi`\] Improve autofix for nested and mixed type unions for `unnecessary-type-union` (`PYI055`) ([#14272](https://github.com/astral-sh/ruff/pull/14272)) +- \[`flake8-pyi`\] Mark fix as unsafe when type annotation contains comments for `duplicate-literal-member` (`PYI062`) ([#14268](https://github.com/astral-sh/ruff/pull/14268)) + +### Server + +- Use the current working directory to resolve settings from `ruff.configuration` ([#14352](https://github.com/astral-sh/ruff/pull/14352)) + +### Bug fixes + +- Avoid conflicts between `PLC014` (`useless-import-alias`) and `I002` (`missing-required-import`) by considering `lint.isort.required-imports` for `PLC014` ([#14287](https://github.com/astral-sh/ruff/pull/14287)) +- \[`flake8-type-checking`\] Skip quoting annotation if it becomes invalid syntax (`TCH001`) +- \[`flake8-pyi`\] Avoid using `typing.Self` in stub files pre-Python 3.11 (`PYI034`) ([#14230](https://github.com/astral-sh/ruff/pull/14230)) +- \[`flake8-pytest-style`\] Flag `pytest.raises` call with keyword argument `expected_exception` (`PT011`) ([#14298](https://github.com/astral-sh/ruff/pull/14298)) +- \[`flake8-simplify`\] Infer "unknown" truthiness for literal iterables whose items are all unpacks (`SIM222`) ([#14263](https://github.com/astral-sh/ruff/pull/14263)) +- \[`flake8-type-checking`\] Fix false positives for `typing.Annotated` (`TCH001`) ([#14311](https://github.com/astral-sh/ruff/pull/14311)) +- \[`pylint`\] Allow `await` at the top-level scope of a notebook (`PLE1142`) ([#14225](https://github.com/astral-sh/ruff/pull/14225)) +- \[`pylint`\] Fix miscellaneous issues in `await-outside-async` detection (`PLE1142`) ([#14218](https://github.com/astral-sh/ruff/pull/14218)) +- \[`pyupgrade`\] Avoid applying PEP 646 rewrites in invalid contexts (`UP044`) ([#14234](https://github.com/astral-sh/ruff/pull/14234)) +- \[`pyupgrade`\] Detect permutations in redundant open modes (`UP015`) ([#14255](https://github.com/astral-sh/ruff/pull/14255)) +- \[`refurb`\] Avoid triggering `hardcoded-string-charset` for reordered sets (`FURB156`) ([#14233](https://github.com/astral-sh/ruff/pull/14233)) +- \[`refurb`\] Further special cases added to `verbose-decimal-constructor` (`FURB157`) ([#14216](https://github.com/astral-sh/ruff/pull/14216)) +- \[`refurb`\] Use `UserString` instead of non-existent `UserStr` (`FURB189`) ([#14209](https://github.com/astral-sh/ruff/pull/14209)) +- \[`ruff`\] Avoid treating lowercase letters as `# noqa` codes (`RUF100`) ([#14229](https://github.com/astral-sh/ruff/pull/14229)) +- \[`ruff`\] Do not report when `Optional` has no type arguments (`RUF013`) ([#14181](https://github.com/astral-sh/ruff/pull/14181)) + +### Documentation + +- Add "Notebook behavior" section for `F704`, `PLE1142` ([#14266](https://github.com/astral-sh/ruff/pull/14266)) +- Document comment policy around fix safety ([#14300](https://github.com/astral-sh/ruff/pull/14300)) diff --git a/changelogs/0.8.x.md b/changelogs/0.8.x.md new file mode 100644 index 0000000000000..e82a2ebbae077 --- /dev/null +++ b/changelogs/0.8.x.md @@ -0,0 +1,304 @@ +# Changelog 0.8.x + +## 0.8.0 + +Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes! + +### Breaking changes + +See also, the "Remapped rules" section which may result in disabled rules. + +- **Default to Python 3.9** + + Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896)) + +- **Changed location of `pydoclint` diagnostics** + + [`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case. + + If you've opted into these preview rules but have them suppressed using + [`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in + some places, this change may mean that you need to move the `noqa` suppression + comments. Most users should be unaffected by this change. + +- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer** + + Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order). + + This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected. + +- **Changes to the line width calculation** + + Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)). + +### Removed Rules + +The following deprecated rules have been removed: + +- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`) +- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`) +- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`) +- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`) +- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`) +- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`) + +### Remapped rules + +The following rules have been remapped to new rule codes: + +- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC` + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`) +- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`) +- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`) +- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`) +- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`) +- [`pep484-style-positional-only-parameter`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-parameter/) (`PYI063`) +- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`) +- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`) +- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`) +- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`) +- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`) +- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`) +- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`) + +The following behaviors have been stabilized: + +- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names. +- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available + +The following fixes have been stabilized: + +- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`) + +### Preview features + +- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394)) +- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393)) +- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316)) +- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273)) +- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373)) +- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319)) +- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446)) +- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474)) +- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372)) + +### Rule changes + +- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/). +- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402)) +- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371)) +- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424)) + +### Configuration + +- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435)) +- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477)) + +## 0.8.1 + +### Preview features + +- Formatter: Avoid invalid syntax for format-spec with quotes for all Python versions ([#14625](https://github.com/astral-sh/ruff/pull/14625)) +- Formatter: Consider quotes inside format-specs when choosing the quotes for an f-string ([#14493](https://github.com/astral-sh/ruff/pull/14493)) +- Formatter: Do not consider f-strings with escaped newlines as multiline ([#14624](https://github.com/astral-sh/ruff/pull/14624)) +- Formatter: Fix f-string formatting in assignment statement ([#14454](https://github.com/astral-sh/ruff/pull/14454)) +- Formatter: Fix unnecessary space around power operator (`**`) in overlong f-string expressions ([#14489](https://github.com/astral-sh/ruff/pull/14489)) +- \[`airflow`\] Avoid implicit `schedule` argument to `DAG` and `@dag` (`AIR301`) ([#14581](https://github.com/astral-sh/ruff/pull/14581)) +- \[`flake8-builtins`\] Exempt private built-in modules (`A005`) ([#14505](https://github.com/astral-sh/ruff/pull/14505)) +- \[`flake8-pytest-style`\] Fix `pytest.mark.parametrize` rules to check calls instead of decorators ([#14515](https://github.com/astral-sh/ruff/pull/14515)) +- \[`flake8-type-checking`\] Implement `runtime-cast-value` (`TC006`) ([#14511](https://github.com/astral-sh/ruff/pull/14511)) +- \[`flake8-type-checking`\] Implement `unquoted-type-alias` (`TC007`) and `quoted-type-alias` (`TC008`) ([#12927](https://github.com/astral-sh/ruff/pull/12927)) +- \[`flake8-use-pathlib`\] Recommend `Path.iterdir()` over `os.listdir()` (`PTH208`) ([#14509](https://github.com/astral-sh/ruff/pull/14509)) +- \[`pylint`\] Extend `invalid-envvar-default` to detect `os.environ.get` (`PLW1508`) ([#14512](https://github.com/astral-sh/ruff/pull/14512)) +- \[`pylint`\] Implement `len-test` (`PLC1802`) ([#14309](https://github.com/astral-sh/ruff/pull/14309)) +- \[`refurb`\] Fix bug where methods defined using lambdas were flagged by `FURB118` ([#14639](https://github.com/astral-sh/ruff/pull/14639)) +- \[`ruff`\] Auto-add `r` prefix when string has no backslashes for `unraw-re-pattern` (`RUF039`) ([#14536](https://github.com/astral-sh/ruff/pull/14536)) +- \[`ruff`\] Implement `invalid-assert-message-literal-argument` (`RUF040`) ([#14488](https://github.com/astral-sh/ruff/pull/14488)) +- \[`ruff`\] Implement `unnecessary-nested-literal` (`RUF041`) ([#14323](https://github.com/astral-sh/ruff/pull/14323)) +- \[`ruff`\] Implement `unnecessary-regular-expression` (`RUF055`) ([#14659](https://github.com/astral-sh/ruff/pull/14659)) + +### Rule changes + +- Ignore more rules for stub files ([#14541](https://github.com/astral-sh/ruff/pull/14541)) +- \[`pep8-naming`\] Eliminate false positives for single-letter names (`N811`, `N814`) ([#14584](https://github.com/astral-sh/ruff/pull/14584)) +- \[`pyflakes`\] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) ([#14615](https://github.com/astral-sh/ruff/pull/14615)) +- \[`ruff`\] Detect redirected-noqa in file-level comments (`RUF101`) ([#14635](https://github.com/astral-sh/ruff/pull/14635)) +- \[`ruff`\] Mark fixes for `unsorted-dunder-all` and `unsorted-dunder-slots` as unsafe when there are complex comments in the sequence (`RUF022`, `RUF023`) ([#14560](https://github.com/astral-sh/ruff/pull/14560)) + +### Bug fixes + +- Avoid fixing code to `None | None` for `redundant-none-literal` (`PYI061`) and `never-union` (`RUF020`) ([#14583](https://github.com/astral-sh/ruff/pull/14583), [#14589](https://github.com/astral-sh/ruff/pull/14589)) +- \[`flake8-bugbear`\] Fix `mutable-contextvar-default` to resolve annotated function calls properly (`B039`) ([#14532](https://github.com/astral-sh/ruff/pull/14532)) +- \[`flake8-pyi`, `ruff`\] Fix traversal of nested literals and unions (`PYI016`, `PYI051`, `PYI055`, `PYI062`, `RUF041`) ([#14641](https://github.com/astral-sh/ruff/pull/14641)) +- \[`flake8-pyi`\] Avoid rewriting invalid type expressions in `unnecessary-type-union` (`PYI055`) ([#14660](https://github.com/astral-sh/ruff/pull/14660)) +- \[`flake8-type-checking`\] Avoid syntax errors and type checking problem for quoted annotations autofix (`TC003`, `TC006`) ([#14634](https://github.com/astral-sh/ruff/pull/14634)) +- \[`pylint`\] Do not wrap function calls in parentheses in the fix for unnecessary-dunder-call (`PLC2801`) ([#14601](https://github.com/astral-sh/ruff/pull/14601)) +- \[`ruff`\] Handle `attrs`'s `auto_attribs` correctly (`RUF009`) ([#14520](https://github.com/astral-sh/ruff/pull/14520)) + +## 0.8.2 + +### Preview features + +- \[`airflow`\] Avoid deprecated values (`AIR302`) ([#14582](https://github.com/astral-sh/ruff/pull/14582)) +- \[`airflow`\] Extend removed names for `AIR302` ([#14734](https://github.com/astral-sh/ruff/pull/14734)) +- \[`ruff`\] Extend `unnecessary-regular-expression` to non-literal strings (`RUF055`) ([#14679](https://github.com/astral-sh/ruff/pull/14679)) +- \[`ruff`\] Implement `used-dummy-variable` (`RUF052`) ([#14611](https://github.com/astral-sh/ruff/pull/14611)) +- \[`ruff`\] Implement `unnecessary-cast-to-int` (`RUF046`) ([#14697](https://github.com/astral-sh/ruff/pull/14697)) + +### Rule changes + +- \[`airflow`\] Check `AIR001` from builtin or providers `operators` module ([#14631](https://github.com/astral-sh/ruff/pull/14631)) +- \[`flake8-pytest-style`\] Remove `@` in `pytest.mark.parametrize` rule messages ([#14770](https://github.com/astral-sh/ruff/pull/14770)) +- \[`pandas-vet`\] Skip rules if the `panda` module hasn't been seen ([#14671](https://github.com/astral-sh/ruff/pull/14671)) +- \[`pylint`\] Fix false negatives for `ascii` and `sorted` in `len-as-condition` (`PLC1802`) ([#14692](https://github.com/astral-sh/ruff/pull/14692)) +- \[`refurb`\] Guard `hashlib` imports and mark `hashlib-digest-hex` fix as safe (`FURB181`) ([#14694](https://github.com/astral-sh/ruff/pull/14694)) + +### Configuration + +- \[`flake8-import-conventions`\] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias` (`ICN001`) ([#14745](https://github.com/astral-sh/ruff/pull/14745)) + +### Bug fixes + +- Revert: [pyflakes] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) (#14615) ([#14726](https://github.com/astral-sh/ruff/pull/14726)) +- \[`pep8-naming`\] Avoid false positive for `class Bar(type(foo))` (`N804`) ([#14683](https://github.com/astral-sh/ruff/pull/14683)) +- \[`pycodestyle`\] Handle f-strings properly for `invalid-escape-sequence` (`W605`) ([#14748](https://github.com/astral-sh/ruff/pull/14748)) +- \[`pylint`\] Ignore `@overload` in `PLR0904` ([#14730](https://github.com/astral-sh/ruff/pull/14730)) +- \[`refurb`\] Handle non-finite decimals in `verbose-decimal-constructor` (`FURB157`) ([#14596](https://github.com/astral-sh/ruff/pull/14596)) +- \[`ruff`\] Avoid emitting `assignment-in-assert` when all references to the assigned variable are themselves inside `assert`s (`RUF018`) ([#14661](https://github.com/astral-sh/ruff/pull/14661)) + +### Documentation + +- Improve docs for `flake8-use-pathlib` rules ([#14741](https://github.com/astral-sh/ruff/pull/14741)) +- Improve error messages and docs for `flake8-comprehensions` rules ([#14729](https://github.com/astral-sh/ruff/pull/14729)) +- \[`flake8-type-checking`\] Expands `TC006` docs to better explain itself ([#14749](https://github.com/astral-sh/ruff/pull/14749)) + +## 0.8.3 + +### Preview features + +- Fix fstring formatting removing overlong implicit concatenated string in expression part ([#14811](https://github.com/astral-sh/ruff/pull/14811)) +- \[`airflow`\] Add fix to remove deprecated keyword arguments (`AIR302`) ([#14887](https://github.com/astral-sh/ruff/pull/14887)) +- \[`airflow`\]: Extend rule to include deprecated names for Airflow 3.0 (`AIR302`) ([#14765](https://github.com/astral-sh/ruff/pull/14765) and [#14804](https://github.com/astral-sh/ruff/pull/14804)) +- \[`flake8-bugbear`\] Improve error messages for `except*` (`B025`, `B029`, `B030`, `B904`) ([#14815](https://github.com/astral-sh/ruff/pull/14815)) +- \[`flake8-bugbear`\] `itertools.batched()` without explicit `strict` (`B911`) ([#14408](https://github.com/astral-sh/ruff/pull/14408)) +- \[`flake8-use-pathlib`\] Dotless suffix passed to `Path.with_suffix()` (`PTH210`) ([#14779](https://github.com/astral-sh/ruff/pull/14779)) +- \[`pylint`\] Include parentheses and multiple comparators in check for `boolean-chained-comparison` (`PLR1716`) ([#14781](https://github.com/astral-sh/ruff/pull/14781)) +- \[`ruff`\] Do not simplify `round()` calls (`RUF046`) ([#14832](https://github.com/astral-sh/ruff/pull/14832)) +- \[`ruff`\] Don't emit `used-dummy-variable` on function parameters (`RUF052`) ([#14818](https://github.com/astral-sh/ruff/pull/14818)) +- \[`ruff`\] Implement `if-key-in-dict-del` (`RUF051`) ([#14553](https://github.com/astral-sh/ruff/pull/14553)) +- \[`ruff`\] Mark autofix for `RUF052` as always unsafe ([#14824](https://github.com/astral-sh/ruff/pull/14824)) +- \[`ruff`\] Teach autofix for `used-dummy-variable` about TypeVars etc. (`RUF052`) ([#14819](https://github.com/astral-sh/ruff/pull/14819)) + +### Rule changes + +- \[`flake8-bugbear`\] Offer unsafe autofix for `no-explicit-stacklevel` (`B028`) ([#14829](https://github.com/astral-sh/ruff/pull/14829)) +- \[`flake8-pyi`\] Skip all type definitions in `string-or-bytes-too-long` (`PYI053`) ([#14797](https://github.com/astral-sh/ruff/pull/14797)) +- \[`pyupgrade`\] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (`UP009`) ([#14728](https://github.com/astral-sh/ruff/pull/14728)) +- \[`pyupgrade`\] Mark fixes for `convert-typed-dict-functional-to-class` and `convert-named-tuple-functional-to-class` as unsafe if they will remove comments (`UP013`, `UP014`) ([#14842](https://github.com/astral-sh/ruff/pull/14842)) + +### Bug fixes + +- Raise syntax error for mixing `except` and `except*` ([#14895](https://github.com/astral-sh/ruff/pull/14895)) +- \[`flake8-bugbear`\] Fix `B028` to allow `stacklevel` to be explicitly assigned as a positional argument ([#14868](https://github.com/astral-sh/ruff/pull/14868)) +- \[`flake8-bugbear`\] Skip `B028` if `warnings.warn` is called with `*args` or `**kwargs` ([#14870](https://github.com/astral-sh/ruff/pull/14870)) +- \[`flake8-comprehensions`\] Skip iterables with named expressions in `unnecessary-map` (`C417`) ([#14827](https://github.com/astral-sh/ruff/pull/14827)) +- \[`flake8-pyi`\] Also remove `self` and `cls`'s annotation (`PYI034`) ([#14801](https://github.com/astral-sh/ruff/pull/14801)) +- \[`flake8-pytest-style`\] Fix `pytest-parametrize-names-wrong-type` (`PT006`) to edit both `argnames` and `argvalues` if both of them are single-element tuples/lists ([#14699](https://github.com/astral-sh/ruff/pull/14699)) +- \[`perflint`\] Improve autofix for `PERF401` ([#14369](https://github.com/astral-sh/ruff/pull/14369)) +- \[`pylint`\] Fix `PLW1508` false positive for default string created via a mult operation ([#14841](https://github.com/astral-sh/ruff/pull/14841)) + +## 0.8.4 + +### Preview features + +- \[`airflow`\] Extend `AIR302` with additional functions and classes ([#15015](https://github.com/astral-sh/ruff/pull/15015)) +- \[`airflow`\] Implement `moved-to-provider-in-3` for modules that has been moved to Airflow providers (`AIR303`) ([#14764](https://github.com/astral-sh/ruff/pull/14764)) +- \[`flake8-use-pathlib`\] Extend check for invalid path suffix to include the case `"."` (`PTH210`) ([#14902](https://github.com/astral-sh/ruff/pull/14902)) +- \[`perflint`\] Fix panic in `PERF401` when list variable is after the `for` loop ([#14971](https://github.com/astral-sh/ruff/pull/14971)) +- \[`perflint`\] Simplify finding the loop target in `PERF401` ([#15025](https://github.com/astral-sh/ruff/pull/15025)) +- \[`pylint`\] Preserve original value format (`PLR6104`) ([#14978](https://github.com/astral-sh/ruff/pull/14978)) +- \[`ruff`\] Avoid false positives for `RUF027` for typing context bindings ([#15037](https://github.com/astral-sh/ruff/pull/15037)) +- \[`ruff`\] Check for ambiguous pattern passed to `pytest.raises()` (`RUF043`) ([#14966](https://github.com/astral-sh/ruff/pull/14966)) + +### Rule changes + +- \[`flake8-bandit`\] Check `S105` for annotated assignment ([#15059](https://github.com/astral-sh/ruff/pull/15059)) +- \[`flake8-pyi`\] More autofixes for `redundant-none-literal` (`PYI061`) ([#14872](https://github.com/astral-sh/ruff/pull/14872)) +- \[`pydocstyle`\] Skip leading whitespace for `D403` ([#14963](https://github.com/astral-sh/ruff/pull/14963)) +- \[`ruff`\] Skip `SQLModel` base classes for `mutable-class-default` (`RUF012`) ([#14949](https://github.com/astral-sh/ruff/pull/14949)) + +### Bug + +- \[`perflint`\] Parenthesize walrus expressions in autofix for `manual-list-comprehension` (`PERF401`) ([#15050](https://github.com/astral-sh/ruff/pull/15050)) + +### Server + +- Check diagnostic refresh support from client capability which enables dynamic configuration for various editors ([#15014](https://github.com/astral-sh/ruff/pull/15014)) + +## 0.8.5 + +### Preview features + +- \[`airflow`\] Extend names moved from core to provider (`AIR303`) ([#15145](https://github.com/astral-sh/ruff/pull/15145), [#15159](https://github.com/astral-sh/ruff/pull/15159), [#15196](https://github.com/astral-sh/ruff/pull/15196), [#15216](https://github.com/astral-sh/ruff/pull/15216)) +- \[`airflow`\] Extend rule to check class attributes, methods, arguments (`AIR302`) ([#15054](https://github.com/astral-sh/ruff/pull/15054), [#15083](https://github.com/astral-sh/ruff/pull/15083)) +- \[`fastapi`\] Update `FAST002` to check keyword-only arguments ([#15119](https://github.com/astral-sh/ruff/pull/15119)) +- \[`flake8-type-checking`\] Disable `TC006` and `TC007` in stub files ([#15179](https://github.com/astral-sh/ruff/pull/15179)) +- \[`pylint`\] Detect nested methods correctly (`PLW1641`) ([#15032](https://github.com/astral-sh/ruff/pull/15032)) +- \[`ruff`\] Detect more strict-integer expressions (`RUF046`) ([#14833](https://github.com/astral-sh/ruff/pull/14833)) +- \[`ruff`\] Implement `falsy-dict-get-fallback` (`RUF056`) ([#15160](https://github.com/astral-sh/ruff/pull/15160)) +- \[`ruff`\] Implement `unnecessary-round` (`RUF057`) ([#14828](https://github.com/astral-sh/ruff/pull/14828)) + +### Rule changes + +- Visit PEP 764 inline `TypedDict` keys as non-type-expressions ([#15073](https://github.com/astral-sh/ruff/pull/15073)) +- \[`flake8-comprehensions`\] Skip `C416` if comprehension contains unpacking ([#14909](https://github.com/astral-sh/ruff/pull/14909)) +- \[`flake8-pie`\] Allow `cast(SomeType, ...)` (`PIE796`) ([#15141](https://github.com/astral-sh/ruff/pull/15141)) +- \[`flake8-simplify`\] More precise inference for dictionaries (`SIM300`) ([#15164](https://github.com/astral-sh/ruff/pull/15164)) +- \[`flake8-use-pathlib`\] Catch redundant joins in `PTH201` and avoid syntax errors ([#15177](https://github.com/astral-sh/ruff/pull/15177)) +- \[`pycodestyle`\] Preserve original value format (`E731`) ([#15097](https://github.com/astral-sh/ruff/pull/15097)) +- \[`pydocstyle`\] Split on first whitespace character (`D403`) ([#15082](https://github.com/astral-sh/ruff/pull/15082)) +- \[`pyupgrade`\] Add all PEP-585 names to `UP006` rule ([#5454](https://github.com/astral-sh/ruff/pull/5454)) + +### Configuration + +- \[`flake8-type-checking`\] Improve flexibility of `runtime-evaluated-decorators` ([#15204](https://github.com/astral-sh/ruff/pull/15204)) +- \[`pydocstyle`\] Add setting to ignore missing documentation for `*args` and `**kwargs` parameters (`D417`) ([#15210](https://github.com/astral-sh/ruff/pull/15210)) +- \[`ruff`\] Add an allowlist for `unsafe-markup-use` (`RUF035`) ([#15076](https://github.com/astral-sh/ruff/pull/15076)) + +### Bug fixes + +- Fix type subscript on older python versions ([#15090](https://github.com/astral-sh/ruff/pull/15090)) +- Use `TypeChecker` for detecting `fastapi` routes ([#15093](https://github.com/astral-sh/ruff/pull/15093)) +- \[`pycodestyle`\] Avoid false positives and negatives related to type parameter default syntax (`E225`, `E251`) ([#15214](https://github.com/astral-sh/ruff/pull/15214)) + +### Documentation + +- Fix incorrect doc in `shebang-not-executable` (`EXE001`) and add git+windows solution to executable bit ([#15208](https://github.com/astral-sh/ruff/pull/15208)) +- Rename rules currently not conforming to naming convention ([#15102](https://github.com/astral-sh/ruff/pull/15102)) + +## 0.8.6 + +### Preview features + +- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126)) +- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104)) +- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234)) + +### Rule changes + +- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175)) +- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215)) + +### CLI + +- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237)) + +### Bug fixes + +- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230)) +- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250)) diff --git a/changelogs/0.9.x.md b/changelogs/0.9.x.md new file mode 100644 index 0000000000000..8e4ea330afdb5 --- /dev/null +++ b/changelogs/0.9.x.md @@ -0,0 +1,475 @@ +# Changelog 0.9.x + +## 0.9.0 + +Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes! + +### Breaking changes + +Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes. + +This release doesn’t remove or remap any existing stable rules. + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`). + This rule has also been renamed: previously, it was called `builtin-module-shadowing`. +- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`) +- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`) +- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`) +- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`) +- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`) +- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`) + +The following behaviors have been stabilized: + +- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments. +- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements. +- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/). +- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files. + +The following fixes or improvements to fixes have been stabilized: + +- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`) +- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`) + +### Formatter + +This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes: + +- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594)) +- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860)) +- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766)) +- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539)) +- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457)) +- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123)) +- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457)) +- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513)) +- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933)) +- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381)) +- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282)) +- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276)) +- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523)) + +### Preview features + +- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601)) +- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180)) +- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310)) +- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313)) +- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299)) +- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258)) + +### Rule changes + +- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350)) +- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298)) + +### Server + +- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel) + which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didn’t contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232)) +- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373)) + +### CLI + +- Improve the error message for `--config key=value` when the `key` is for a table and it’s a simple `value` + +### Bug fixes + +- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330)) +- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365)) +- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356)) +- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311)) +- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309)) +- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273)) +- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318)) +- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277)) + +## 0.9.1 + +### Preview features + +- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308)) +- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376)) + +### Rule changes + +- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389)) + +### Formatter + +- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378)) + +### Server + +- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398)) + +### Bug fixes + +- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394)) +- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337)) + +## 0.9.2 + +### Preview features + +- \[`airflow`\] Fix typo "security_managr" to "security_manager" (`AIR303`) ([#15463](https://github.com/astral-sh/ruff/pull/15463)) +- \[`airflow`\] extend and fix AIR302 rules ([#15525](https://github.com/astral-sh/ruff/pull/15525)) +- \[`fastapi`\] Handle parameters with `Depends` correctly (`FAST003`) ([#15364](https://github.com/astral-sh/ruff/pull/15364)) +- \[`flake8-pytest-style`\] Implement pytest.warns diagnostics (`PT029`, `PT030`, `PT031`) ([#15444](https://github.com/astral-sh/ruff/pull/15444)) +- \[`flake8-pytest-style`\] Test function parameters with default arguments (`PT028`) ([#15449](https://github.com/astral-sh/ruff/pull/15449)) +- \[`flake8-type-checking`\] Avoid false positives for `|` in `TC008` ([#15201](https://github.com/astral-sh/ruff/pull/15201)) + +### Rule changes + +- \[`flake8-todos`\] Allow VSCode GitHub PR extension style links in `missing-todo-link` (`TD003`) ([#15519](https://github.com/astral-sh/ruff/pull/15519)) +- \[`pyflakes`\] Show syntax error message for `F722` ([#15523](https://github.com/astral-sh/ruff/pull/15523)) + +### Formatter + +- Fix curly bracket spacing around f-string expressions containing curly braces ([#15471](https://github.com/astral-sh/ruff/pull/15471)) +- Fix joining of f-strings with different quotes when using quote style `Preserve` ([#15524](https://github.com/astral-sh/ruff/pull/15524)) + +### Server + +- Avoid indexing the same workspace multiple times ([#15495](https://github.com/astral-sh/ruff/pull/15495)) +- Display context for `ruff.configuration` errors ([#15452](https://github.com/astral-sh/ruff/pull/15452)) + +### Configuration + +- Remove `flatten` to improve deserialization error messages ([#15414](https://github.com/astral-sh/ruff/pull/15414)) + +### Bug fixes + +- Parse triple-quoted string annotations as if parenthesized ([#15387](https://github.com/astral-sh/ruff/pull/15387)) +- \[`fastapi`\] Update `Annotated` fixes (`FAST002`) ([#15462](https://github.com/astral-sh/ruff/pull/15462)) +- \[`flake8-bandit`\] Check for `builtins` instead of `builtin` (`S102`, `PTH123`) ([#15443](https://github.com/astral-sh/ruff/pull/15443)) +- \[`flake8-pathlib`\] Fix `--select` for `os-path-dirname` (`PTH120`) ([#15446](https://github.com/astral-sh/ruff/pull/15446)) +- \[`ruff`\] Fix false positive on global keyword (`RUF052`) ([#15235](https://github.com/astral-sh/ruff/pull/15235)) + +## 0.9.3 + +### Preview features + +- \[`airflow`\] Argument `fail_stop` in DAG has been renamed as `fail_fast` (`AIR302`) ([#15633](https://github.com/astral-sh/ruff/pull/15633)) +- \[`airflow`\] Extend `AIR303` with more symbols ([#15611](https://github.com/astral-sh/ruff/pull/15611)) +- \[`flake8-bandit`\] Report all references to suspicious functions (`S3`) ([#15541](https://github.com/astral-sh/ruff/pull/15541)) +- \[`flake8-pytest-style`\] Do not emit diagnostics for empty `for` loops (`PT012`, `PT031`) ([#15542](https://github.com/astral-sh/ruff/pull/15542)) +- \[`flake8-simplify`\] Avoid double negations (`SIM103`) ([#15562](https://github.com/astral-sh/ruff/pull/15562)) +- \[`pyflakes`\] Fix infinite loop with unused local import in `__init__.py` (`F401`) ([#15517](https://github.com/astral-sh/ruff/pull/15517)) +- \[`pylint`\] Do not report methods with only one `EM101`-compatible `raise` (`PLR6301`) ([#15507](https://github.com/astral-sh/ruff/pull/15507)) +- \[`pylint`\] Implement `redefined-slots-in-subclass` (`W0244`) ([#9640](https://github.com/astral-sh/ruff/pull/9640)) +- \[`pyupgrade`\] Add rules to use PEP 695 generics in classes and functions (`UP046`, `UP047`) ([#15565](https://github.com/astral-sh/ruff/pull/15565), [#15659](https://github.com/astral-sh/ruff/pull/15659)) +- \[`refurb`\] Implement `for-loop-writes` (`FURB122`) ([#10630](https://github.com/astral-sh/ruff/pull/10630)) +- \[`ruff`\] Implement `needless-else` clause (`RUF047`) ([#15051](https://github.com/astral-sh/ruff/pull/15051)) +- \[`ruff`\] Implement `starmap-zip` (`RUF058`) ([#15483](https://github.com/astral-sh/ruff/pull/15483)) + +### Rule changes + +- \[`flake8-bugbear`\] Do not raise error if keyword argument is present and target-python version is less or equals than 3.9 (`B903`) ([#15549](https://github.com/astral-sh/ruff/pull/15549)) +- \[`flake8-comprehensions`\] strip parentheses around generators in `unnecessary-generator-set` (`C401`) ([#15553](https://github.com/astral-sh/ruff/pull/15553)) +- \[`flake8-pytest-style`\] Rewrite references to `.exception` (`PT027`) ([#15680](https://github.com/astral-sh/ruff/pull/15680)) +- \[`flake8-simplify`\] Mark fixes as unsafe (`SIM201`, `SIM202`) ([#15626](https://github.com/astral-sh/ruff/pull/15626)) +- \[`flake8-type-checking`\] Fix some safe fixes being labeled unsafe (`TC006`,`TC008`) ([#15638](https://github.com/astral-sh/ruff/pull/15638)) +- \[`isort`\] Omit trailing whitespace in `unsorted-imports` (`I001`) ([#15518](https://github.com/astral-sh/ruff/pull/15518)) +- \[`pydoclint`\] Allow ignoring one line docstrings for `DOC` rules ([#13302](https://github.com/astral-sh/ruff/pull/13302)) +- \[`pyflakes`\] Apply redefinition fixes by source code order (`F811`) ([#15575](https://github.com/astral-sh/ruff/pull/15575)) +- \[`pyflakes`\] Avoid removing too many imports in `redefined-while-unused` (`F811`) ([#15585](https://github.com/astral-sh/ruff/pull/15585)) +- \[`pyflakes`\] Group redefinition fixes by source statement (`F811`) ([#15574](https://github.com/astral-sh/ruff/pull/15574)) +- \[`pylint`\] Include name of base class in message for `redefined-slots-in-subclass` (`W0244`) ([#15559](https://github.com/astral-sh/ruff/pull/15559)) +- \[`ruff`\] Update fix for `RUF055` to use `var == value` ([#15605](https://github.com/astral-sh/ruff/pull/15605)) + +### Formatter + +- Fix bracket spacing for single-element tuples in f-string expressions ([#15537](https://github.com/astral-sh/ruff/pull/15537)) +- Fix unstable f-string formatting for expressions containing a trailing comma ([#15545](https://github.com/astral-sh/ruff/pull/15545)) + +### Performance + +- Avoid quadratic membership check in import fixes ([#15576](https://github.com/astral-sh/ruff/pull/15576)) + +### Server + +- Allow `unsafe-fixes` settings for code actions ([#15666](https://github.com/astral-sh/ruff/pull/15666)) + +### Bug fixes + +- \[`flake8-bandit`\] Add missing single-line/dotall regex flag (`S608`) ([#15654](https://github.com/astral-sh/ruff/pull/15654)) +- \[`flake8-import-conventions`\] Fix infinite loop between `ICN001` and `I002` (`ICN001`) ([#15480](https://github.com/astral-sh/ruff/pull/15480)) +- \[`flake8-simplify`\] Do not emit diagnostics for expressions inside string type annotations (`SIM222`, `SIM223`) ([#15405](https://github.com/astral-sh/ruff/pull/15405)) +- \[`pyflakes`\] Treat arguments passed to the `default=` parameter of `TypeVar` as type expressions (`F821`) ([#15679](https://github.com/astral-sh/ruff/pull/15679)) +- \[`pyupgrade`\] Avoid syntax error when the iterable is a non-parenthesized tuple (`UP028`) ([#15543](https://github.com/astral-sh/ruff/pull/15543)) +- \[`ruff`\] Exempt `NewType` calls where the original type is immutable (`RUF009`) ([#15588](https://github.com/astral-sh/ruff/pull/15588)) +- Preserve raw string prefix and escapes in all codegen fixes ([#15694](https://github.com/astral-sh/ruff/pull/15694)) + +### Documentation + +- Generate documentation redirects for lowercase rule codes ([#15564](https://github.com/astral-sh/ruff/pull/15564)) +- `TRY300`: Add some extra notes on not catching exceptions you didn't expect ([#15036](https://github.com/astral-sh/ruff/pull/15036)) + +## 0.9.4 + +### Preview features + +- \[`airflow`\] Extend airflow context parameter check for `BaseOperator.execute` (`AIR302`) ([#15713](https://github.com/astral-sh/ruff/pull/15713)) +- \[`airflow`\] Update `AIR302` to check for deprecated context keys ([#15144](https://github.com/astral-sh/ruff/pull/15144)) +- \[`flake8-bandit`\] Permit suspicious imports within stub files (`S4`) ([#15822](https://github.com/astral-sh/ruff/pull/15822)) +- \[`pylint`\] Do not trigger `PLR6201` on empty collections ([#15732](https://github.com/astral-sh/ruff/pull/15732)) +- \[`refurb`\] Do not emit diagnostic when loop variables are used outside loop body (`FURB122`) ([#15757](https://github.com/astral-sh/ruff/pull/15757)) +- \[`ruff`\] Add support for more `re` patterns (`RUF055`) ([#15764](https://github.com/astral-sh/ruff/pull/15764)) +- \[`ruff`\] Check for shadowed `map` before suggesting fix (`RUF058`) ([#15790](https://github.com/astral-sh/ruff/pull/15790)) +- \[`ruff`\] Do not emit diagnostic when all arguments to `zip()` are variadic (`RUF058`) ([#15744](https://github.com/astral-sh/ruff/pull/15744)) +- \[`ruff`\] Parenthesize fix when argument spans multiple lines for `unnecessary-round` (`RUF057`) ([#15703](https://github.com/astral-sh/ruff/pull/15703)) + +### Rule changes + +- Preserve quote style in generated code ([#15726](https://github.com/astral-sh/ruff/pull/15726), [#15778](https://github.com/astral-sh/ruff/pull/15778), [#15794](https://github.com/astral-sh/ruff/pull/15794)) +- \[`flake8-bugbear`\] Exempt `NewType` calls where the original type is immutable (`B008`) ([#15765](https://github.com/astral-sh/ruff/pull/15765)) +- \[`pylint`\] Honor banned top-level imports by `TID253` in `PLC0415`. ([#15628](https://github.com/astral-sh/ruff/pull/15628)) +- \[`pyupgrade`\] Ignore `is_typeddict` and `TypedDict` for `deprecated-import` (`UP035`) ([#15800](https://github.com/astral-sh/ruff/pull/15800)) + +### CLI + +- Fix formatter warning message for `flake8-quotes` option ([#15788](https://github.com/astral-sh/ruff/pull/15788)) +- Implement tab autocomplete for `ruff config` ([#15603](https://github.com/astral-sh/ruff/pull/15603)) + +### Bug fixes + +- \[`flake8-comprehensions`\] Do not emit `unnecessary-map` diagnostic when lambda has different arity (`C417`) ([#15802](https://github.com/astral-sh/ruff/pull/15802)) +- \[`flake8-comprehensions`\] Parenthesize `sorted` when needed for `unnecessary-call-around-sorted` (`C413`) ([#15825](https://github.com/astral-sh/ruff/pull/15825)) +- \[`pyupgrade`\] Handle end-of-line comments for `quoted-annotation` (`UP037`) ([#15824](https://github.com/astral-sh/ruff/pull/15824)) + +### Documentation + +- Add missing config docstrings ([#15803](https://github.com/astral-sh/ruff/pull/15803)) +- Add references to `trio.run_process` and `anyio.run_process` ([#15761](https://github.com/astral-sh/ruff/pull/15761)) +- Use `uv init --lib` in tutorial ([#15718](https://github.com/astral-sh/ruff/pull/15718)) + +## 0.9.5 + +### Preview features + +- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719)) +- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837)) +- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799)) +- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853)) +- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821)) +- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889)) +- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854)) +- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762)) +- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872)) +- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862)) +- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905)) +- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832)) +- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841)) +- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922)) +- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999)) +- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888)) + +### Rule changes + +- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818)) +- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838)) +- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885)) +- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954)) +- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840)) +- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921)) +- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980)) + +### Configuration + +- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918)) +- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992)) + +### Bug fixes + +- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877)) +- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929)) +- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851)) +- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988)) +- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829)) +- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930)) +- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953)) +- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779)) + +### Documentation + +- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850)) +- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882)) +- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946)) +- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909)) +- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844)) +- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928)) +- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956)) +- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982)) +- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883)) + +## 0.9.6 + +### Preview features + +- \[`airflow`\] Add `external_task.{ExternalTaskMarker, ExternalTaskSensor}` for `AIR302` ([#16014](https://github.com/astral-sh/ruff/pull/16014)) +- \[`flake8-builtins`\] Make strict module name comparison optional (`A005`) ([#15951](https://github.com/astral-sh/ruff/pull/15951)) +- \[`flake8-pyi`\] Extend fix to Python \<= 3.9 for `redundant-none-literal` (`PYI061`) ([#16044](https://github.com/astral-sh/ruff/pull/16044)) +- \[`pylint`\] Also report when the object isn't a literal (`PLE1310`) ([#15985](https://github.com/astral-sh/ruff/pull/15985)) +- \[`ruff`\] Implement `indented-form-feed` (`RUF054`) ([#16049](https://github.com/astral-sh/ruff/pull/16049)) +- \[`ruff`\] Skip type definitions for `missing-f-string-syntax` (`RUF027`) ([#16054](https://github.com/astral-sh/ruff/pull/16054)) + +### Rule changes + +- \[`flake8-annotations`\] Correct syntax for `typing.Union` in suggested return type fixes for `ANN20x` rules ([#16025](https://github.com/astral-sh/ruff/pull/16025)) +- \[`flake8-builtins`\] Match upstream module name comparison (`A005`) ([#16006](https://github.com/astral-sh/ruff/pull/16006)) +- \[`flake8-comprehensions`\] Detect overshadowed `list`/`set`/`dict`, ignore variadics and named expressions (`C417`) ([#15955](https://github.com/astral-sh/ruff/pull/15955)) +- \[`flake8-pie`\] Remove following comma correctly when the unpacked dictionary is empty (`PIE800`) ([#16008](https://github.com/astral-sh/ruff/pull/16008)) +- \[`flake8-simplify`\] Only trigger `SIM401` on known dictionaries ([#15995](https://github.com/astral-sh/ruff/pull/15995)) +- \[`pylint`\] Do not report calls when object type and argument type mismatch, remove custom escape handling logic (`PLE1310`) ([#15984](https://github.com/astral-sh/ruff/pull/15984)) +- \[`pyupgrade`\] Comments within parenthesized value ranges should not affect applicability (`UP040`) ([#16027](https://github.com/astral-sh/ruff/pull/16027)) +- \[`pyupgrade`\] Don't introduce invalid syntax when upgrading old-style type aliases with parenthesized multiline values (`UP040`) ([#16026](https://github.com/astral-sh/ruff/pull/16026)) +- \[`pyupgrade`\] Ensure we do not rename two type parameters to the same name (`UP049`) ([#16038](https://github.com/astral-sh/ruff/pull/16038)) +- \[`pyupgrade`\] \[`ruff`\] Don't apply renamings if the new name is shadowed in a scope of one of the references to the binding (`UP049`, `RUF052`) ([#16032](https://github.com/astral-sh/ruff/pull/16032)) +- \[`ruff`\] Update `RUF009` to behave similar to `B008` and ignore attributes with immutable types ([#16048](https://github.com/astral-sh/ruff/pull/16048)) + +### Server + +- Root exclusions in the server to project root ([#16043](https://github.com/astral-sh/ruff/pull/16043)) + +### Bug fixes + +- \[`flake8-datetime`\] Ignore `.replace()` calls while looking for `.astimezone` ([#16050](https://github.com/astral-sh/ruff/pull/16050)) +- \[`flake8-type-checking`\] Avoid `TC004` false positive where the runtime definition is provided by `__getattr__` ([#16052](https://github.com/astral-sh/ruff/pull/16052)) + +### Documentation + +- Improve `ruff-lsp` migration document ([#16072](https://github.com/astral-sh/ruff/pull/16072)) +- Undeprecate `ruff.nativeServer` ([#16039](https://github.com/astral-sh/ruff/pull/16039)) + +## 0.9.7 + +### Preview features + +- Consider `__new__` methods as special function type for enforcing class method or static method rules ([#13305](https://github.com/astral-sh/ruff/pull/13305)) +- \[`airflow`\] Improve the internal logic to differentiate deprecated symbols (`AIR303`) ([#16013](https://github.com/astral-sh/ruff/pull/16013)) +- \[`refurb`\] Manual timezone monkeypatching (`FURB162`) ([#16113](https://github.com/astral-sh/ruff/pull/16113)) +- \[`ruff`\] Implicit class variable in dataclass (`RUF045`) ([#14349](https://github.com/astral-sh/ruff/pull/14349)) +- \[`ruff`\] Skip singleton starred expressions for `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#16083](https://github.com/astral-sh/ruff/pull/16083)) +- \[`refurb`\] Check for subclasses includes subscript expressions (`FURB189`) ([#16155](https://github.com/astral-sh/ruff/pull/16155)) + +### Rule changes + +- \[`flake8-debugger`\] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#16191](https://github.com/astral-sh/ruff/pull/16191)) +- \[`pycodestyle`\] Exempt `site.addsitedir(...)` calls (`E402`) ([#16251](https://github.com/astral-sh/ruff/pull/16251)) + +### Formatter + +- Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values ([#16187](https://github.com/astral-sh/ruff/pull/16187)) + +### Server + +- Fix handling of requests received after shutdown message ([#16262](https://github.com/astral-sh/ruff/pull/16262)) +- Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code actions for a notebook cell ([#16154](https://github.com/astral-sh/ruff/pull/16154)) +- Include document specific debug info for `ruff.printDebugInformation` ([#16215](https://github.com/astral-sh/ruff/pull/16215)) +- Update server to return the debug info as string with `ruff.printDebugInformation` ([#16214](https://github.com/astral-sh/ruff/pull/16214)) + +### CLI + +- Warn on invalid `noqa` even when there are no diagnostics ([#16178](https://github.com/astral-sh/ruff/pull/16178)) +- Better error messages while loading configuration `extend`s ([#15658](https://github.com/astral-sh/ruff/pull/15658)) + +### Bug fixes + +- \[`flake8-comprehensions`\] Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110)) +- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141)) +- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011)) +- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080)) +- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132)) +- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219)) +- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091)) +- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919)) +- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237)) +- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122)) + +### Documentation + +- Add FAQ entry for `source.*` code actions in Notebook ([#16212](https://github.com/astral-sh/ruff/pull/16212)) +- Add `SECURITY.md` ([#16224](https://github.com/astral-sh/ruff/pull/16224)) + +## 0.9.8 + +### Preview features + +- Start detecting version-related syntax errors in the parser ([#16090](https://github.com/astral-sh/ruff/pull/16090)) + +### Rule changes + +- \[`pylint`\] Mark fix unsafe (`PLW1507`) ([#16343](https://github.com/astral-sh/ruff/pull/16343)) +- \[`pylint`\] Catch `case np.nan`/`case math.nan` in `match` statements (`PLW0177`) ([#16378](https://github.com/astral-sh/ruff/pull/16378)) +- \[`ruff`\] Add more Pydantic models variants to the list of default copy semantics (`RUF012`) ([#16291](https://github.com/astral-sh/ruff/pull/16291)) + +### Server + +- Avoid indexing the project if `configurationPreference` is `editorOnly` ([#16381](https://github.com/astral-sh/ruff/pull/16381)) +- Avoid unnecessary info at non-trace server log level ([#16389](https://github.com/astral-sh/ruff/pull/16389)) +- Expand `ruff.configuration` to allow inline config ([#16296](https://github.com/astral-sh/ruff/pull/16296)) +- Notify users for invalid client settings ([#16361](https://github.com/astral-sh/ruff/pull/16361)) + +### Configuration + +- Add `per-file-target-version` option ([#16257](https://github.com/astral-sh/ruff/pull/16257)) + +### Bug fixes + +- \[`refurb`\] Do not consider docstring(s) (`FURB156`) ([#16391](https://github.com/astral-sh/ruff/pull/16391)) +- \[`flake8-self`\] Ignore attribute accesses on instance-like variables (`SLF001`) ([#16149](https://github.com/astral-sh/ruff/pull/16149)) +- \[`pylint`\] Fix false positives, add missing methods, and support positional-only parameters (`PLE0302`) ([#16263](https://github.com/astral-sh/ruff/pull/16263)) +- \[`flake8-pyi`\] Mark `PYI030` fix unsafe when comments are deleted ([#16322](https://github.com/astral-sh/ruff/pull/16322)) + +### Documentation + +- Fix example for `S611` ([#16316](https://github.com/astral-sh/ruff/pull/16316)) +- Normalize inconsistent markdown headings in docstrings ([#16364](https://github.com/astral-sh/ruff/pull/16364)) +- Document MSRV policy ([#16384](https://github.com/astral-sh/ruff/pull/16384)) + +## 0.9.9 + +### Preview features + +- Fix caching of unsupported-syntax errors ([#16425](https://github.com/astral-sh/ruff/pull/16425)) + +### Bug fixes + +- Only show unsupported-syntax errors in editors when preview mode is enabled ([#16429](https://github.com/astral-sh/ruff/pull/16429)) + +## 0.9.10 + +### Preview features + +- \[`ruff`\] Add new rule `RUF059`: Unused unpacked assignment ([#16449](https://github.com/astral-sh/ruff/pull/16449)) +- \[`syntax-errors`\] Detect assignment expressions before Python 3.8 ([#16383](https://github.com/astral-sh/ruff/pull/16383)) +- \[`syntax-errors`\] Named expressions in decorators before Python 3.9 ([#16386](https://github.com/astral-sh/ruff/pull/16386)) +- \[`syntax-errors`\] Parenthesized keyword argument names after Python 3.8 ([#16482](https://github.com/astral-sh/ruff/pull/16482)) +- \[`syntax-errors`\] Positional-only parameters before Python 3.8 ([#16481](https://github.com/astral-sh/ruff/pull/16481)) +- \[`syntax-errors`\] Tuple unpacking in `return` and `yield` before Python 3.8 ([#16485](https://github.com/astral-sh/ruff/pull/16485)) +- \[`syntax-errors`\] Type parameter defaults before Python 3.13 ([#16447](https://github.com/astral-sh/ruff/pull/16447)) +- \[`syntax-errors`\] Type parameter lists before Python 3.12 ([#16479](https://github.com/astral-sh/ruff/pull/16479)) +- \[`syntax-errors`\] `except*` before Python 3.11 ([#16446](https://github.com/astral-sh/ruff/pull/16446)) +- \[`syntax-errors`\] `type` statements before Python 3.12 ([#16478](https://github.com/astral-sh/ruff/pull/16478)) + +### Bug fixes + +- Escape template filenames in glob patterns in configuration ([#16407](https://github.com/astral-sh/ruff/pull/16407)) +- \[`flake8-simplify`\] Exempt unittest context methods for `SIM115` rule ([#16439](https://github.com/astral-sh/ruff/pull/16439)) +- Formatter: Fix syntax error location in notebooks ([#16499](https://github.com/astral-sh/ruff/pull/16499)) +- \[`pyupgrade`\] Do not offer fix when at least one target is `global`/`nonlocal` (`UP028`) ([#16451](https://github.com/astral-sh/ruff/pull/16451)) +- \[`flake8-builtins`\] Ignore variables matching module attribute names (`A001`) ([#16454](https://github.com/astral-sh/ruff/pull/16454)) +- \[`pylint`\] Convert `code` keyword argument to a positional argument in fix for (`PLR1722`) ([#16424](https://github.com/astral-sh/ruff/pull/16424)) + +### CLI + +- Move rule code from `description` to `check_name` in GitLab output serializer ([#16437](https://github.com/astral-sh/ruff/pull/16437)) + +### Documentation + +- \[`pydocstyle`\] Clarify that `D417` only checks docstrings with an arguments section ([#16494](https://github.com/astral-sh/ruff/pull/16494)) diff --git a/clippy.toml b/clippy.toml index 4ea6eccde06e5..539d63305be5a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,6 +1,7 @@ doc-valid-idents = [ "..", "CodeQL", + "CPython", "FastAPI", "IPython", "LangChain", @@ -14,7 +15,7 @@ doc-valid-idents = [ "SNMPv1", "SNMPv2", "SNMPv3", - "PyFlakes" + "PyFlakes", ] ignore-interior-mutability = [ diff --git a/crates/red_knot/Cargo.toml b/crates/red_knot/Cargo.toml deleted file mode 100644 index 474a08f5b30c3..0000000000000 --- a/crates/red_knot/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "red_knot" -version = "0.0.0" -edition.workspace = true -rust-version.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true -authors.workspace = true -license.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -red_knot_python_semantic = { workspace = true } -red_knot_project = { workspace = true, features = ["zstd"] } -red_knot_server = { workspace = true } -ruff_db = { workspace = true, features = ["os", "cache"] } -ruff_python_ast = { workspace = true } - -anyhow = { workspace = true } -argfile = { workspace = true } -clap = { workspace = true, features = ["wrap_help"] } -colored = { workspace = true } -countme = { workspace = true, features = ["enable"] } -crossbeam = { workspace = true } -ctrlc = { version = "3.4.4" } -jiff = { workspace = true } -rayon = { workspace = true } -salsa = { workspace = true } -tracing = { workspace = true, features = ["release_max_level_debug"] } -tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } -tracing-flame = { workspace = true } -tracing-tree = { workspace = true } -wild = { workspace = true } - -[dev-dependencies] -ruff_db = { workspace = true, features = ["testing"] } -ruff_python_trivia = { workspace = true } - -insta = { workspace = true, features = ["filters"] } -insta-cmd = { workspace = true } -filetime = { workspace = true } -regex = { workspace = true } -tempfile = { workspace = true } -toml = { workspace = true } - -[lints] -workspace = true diff --git a/crates/red_knot/README.md b/crates/red_knot/README.md deleted file mode 100644 index de53ab22c9670..0000000000000 --- a/crates/red_knot/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Red Knot - -Red Knot is an extremely fast type checker. -Currently, it is a work-in-progress and not ready for user testing. - -Red Knot is designed to prioritize good type inference, even in unannotated code, -and aims to avoid false positives. - -While Red Knot will produce similar results to mypy and pyright on many codebases, -100% compatibility with these tools is a non-goal. -On some codebases, Red Knot's design decisions lead to different outcomes -than you would get from running one of these more established tools. - -## Contributing - -Core type checking tests are written as Markdown code blocks. -They can be found in [`red_knot_python_semantic/resources/mdtest`][resources-mdtest]. -See [`red_knot_test/README.md`][mdtest-readme] for more information -on the test framework itself. - -The list of open issues can be found [here][open-issues]. - -[mdtest-readme]: ../red_knot_test/README.md -[open-issues]: https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Ared-knot -[resources-mdtest]: ../red_knot_python_semantic/resources/mdtest diff --git a/crates/red_knot/build.rs b/crates/red_knot/build.rs deleted file mode 100644 index 3cd5edd64633c..0000000000000 --- a/crates/red_knot/build.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{ - fs, - path::{Path, PathBuf}, - process::Command, -}; - -fn main() { - // The workspace root directory is not available without walking up the tree - // https://github.com/rust-lang/cargo/issues/3946 - let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .join("..") - .join(".."); - - commit_info(&workspace_root); - - #[allow(clippy::disallowed_methods)] - let target = std::env::var("TARGET").unwrap(); - println!("cargo::rustc-env=RUST_HOST_TARGET={target}"); -} - -fn commit_info(workspace_root: &Path) { - // If not in a git repository, do not attempt to retrieve commit information - let git_dir = workspace_root.join(".git"); - if !git_dir.exists() { - return; - } - - if let Some(git_head_path) = git_head(&git_dir) { - println!("cargo:rerun-if-changed={}", git_head_path.display()); - - let git_head_contents = fs::read_to_string(git_head_path); - if let Ok(git_head_contents) = git_head_contents { - // The contents are either a commit or a reference in the following formats - // - "" when the head is detached - // - "ref " when working on a branch - // If a commit, checking if the HEAD file has changed is sufficient - // If a ref, we need to add the head file for that ref to rebuild on commit - let mut git_ref_parts = git_head_contents.split_whitespace(); - git_ref_parts.next(); - if let Some(git_ref) = git_ref_parts.next() { - let git_ref_path = git_dir.join(git_ref); - println!("cargo:rerun-if-changed={}", git_ref_path.display()); - } - } - } - - let output = match Command::new("git") - .arg("log") - .arg("-1") - .arg("--date=short") - .arg("--abbrev=9") - .arg("--format=%H %h %cd %(describe)") - .output() - { - Ok(output) if output.status.success() => output, - _ => return, - }; - let stdout = String::from_utf8(output.stdout).unwrap(); - let mut parts = stdout.split_whitespace(); - let mut next = || parts.next().unwrap(); - let _commit_hash = next(); - println!("cargo::rustc-env=RED_KNOT_COMMIT_SHORT_HASH={}", next()); - println!("cargo::rustc-env=RED_KNOT_COMMIT_DATE={}", next()); - - // Describe can fail for some commits - // https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem - if let Some(describe) = parts.next() { - let mut describe_parts = describe.split('-'); - let _last_tag = describe_parts.next().unwrap(); - - // If this is the tagged commit, this component will be missing - println!( - "cargo::rustc-env=RED_KNOT_LAST_TAG_DISTANCE={}", - describe_parts.next().unwrap_or("0") - ); - } -} - -fn git_head(git_dir: &Path) -> Option { - // The typical case is a standard git repository. - let git_head_path = git_dir.join("HEAD"); - if git_head_path.exists() { - return Some(git_head_path); - } - if !git_dir.is_file() { - return None; - } - // If `.git/HEAD` doesn't exist and `.git` is actually a file, - // then let's try to attempt to read it as a worktree. If it's - // a worktree, then its contents will look like this, e.g.: - // - // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 - // - // And the HEAD file we want to watch will be at: - // - // /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD - let contents = fs::read_to_string(git_dir).ok()?; - let (label, worktree_path) = contents.split_once(':')?; - if label != "gitdir" { - return None; - } - let worktree_path = worktree_path.trim(); - Some(PathBuf::from(worktree_path)) -} diff --git a/crates/red_knot/docs/mypy_primer.md b/crates/red_knot/docs/mypy_primer.md deleted file mode 100644 index d3898a2a358c8..0000000000000 --- a/crates/red_knot/docs/mypy_primer.md +++ /dev/null @@ -1,61 +0,0 @@ -# Running `mypy_primer` - -## Basics - -For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run: - -```sh -uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h -``` - -Alternatively, you can install the forked version of `mypy_primer` using: - -```sh -uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" -``` - -and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]). - -## Showing the diagnostics diff between two Git revisions - -To show the diagnostics diff between two Git revisions (e.g. your feature branch and `main`), run: - -```sh -mypy_primer \ - --type-checker knot \ - --old origin/main \ - --new my/feature \ - --debug \ - --output concise \ - --project-selector '/black$' -``` - -This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the -diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`. - -You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration -option to work correctly. - -## Avoiding recompilation - -If you want to run `mypy_primer` repeatedly, e.g. for different projects, but for the same combination of `--old` and `--new`, you -can use set the `MYPY_PRIMER_NO_REBUILD` environment variable to avoid recompilation of Red Knot: - -```sh -MYPY_PRIMER_NO_REBUILD=1 mypy_primer … -``` - -## Running from a local copy of the repository - -If you are working on a local branch, you can use `mypy_primer`'s `--repo` option to specify the path to your local copy of the `ruff` repository. -This allows `mypy_primer` to check out local branches: - -```sh -mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch … -``` - -Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly. - -[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support -[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py -[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables diff --git a/crates/red_knot/docs/tracing.md b/crates/red_knot/docs/tracing.md deleted file mode 100644 index 360d3bc87fb33..0000000000000 --- a/crates/red_knot/docs/tracing.md +++ /dev/null @@ -1,128 +0,0 @@ -# Tracing - -Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a particular thing. -Note, tracing messages with severity `debug` or greater are user-facing. They should be phrased accordingly. -Tracing spans are only shown when using `-vvv`. - -## Verbosity levels - -The CLI supports different verbosity levels. - -- default: Only show errors and warnings. -- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you which packages are fresh. -- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change. -- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating. - -## Better logging with `RED_KNOT_LOG` and `RAYON_NUM_THREADS` - -By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown. -The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one -or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). - -The `RAYON_NUM_THREADS` environment variable, meanwhile, can be used to control the level of concurrency red-knot uses. -By default, red-knot will attempt to parallelize its work so that multiple files are checked simultaneously, -but this can result in a confused logging output where messages from different threads are intertwined. -To switch off concurrency entirely and have more readable logs, use `RAYON_NUM_THREADS=1`. - -### Examples - -#### Show all debug messages - -Shows debug messages from all crates. - -```bash -RED_KNOT_LOG=debug -``` - -#### Show salsa query execution messages - -Show the salsa `execute: my_query` messages in addition to all red knot messages. - -```bash -RED_KNOT_LOG=ruff=trace,red_knot=trace,salsa=info -``` - -#### Show typing traces - -Only show traces for the `red_knot_python_semantic::types` module. - -```bash -RED_KNOT_LOG="red_knot_python_semantic::types" -``` - -Note: Ensure that you use `-vvv` to see tracing spans. - -#### Show messages for a single file - -Shows all messages that are inside of a span for a specific file. - -```bash -RED_KNOT_LOG=red_knot[{file=/home/micha/astral/test/x.py}]=trace -``` - -**Note**: Tracing still shows all spans because tracing can't know at the time of entering the span -whether one if its children has the file `x.py`. - -**Note**: Salsa currently logs the entire memoized values. In our case, the source text and parsed AST. -This very quickly leads to extremely long outputs. - -## Tracing and Salsa - -Be mindful about using `tracing` in Salsa queries, especially when using `warn` or `error` because it isn't guaranteed -that the query will execute after restoring from a persistent cache. In which case the user won't see the message. - -For example, don't use `tracing` to show the user a message when generating a lint violation failed -because the message would only be shown when linting the file the first time, but not on subsequent analysis -runs or when restoring from a persistent cache. This can be confusing for users because they -don't understand why a specific lint violation isn't raised. Instead, change your -query to return the failure as part of the query's result or use a Salsa accumulator. - -## Tracing in tests - -You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in tests. - -```rust -use ruff_db::testing::setup_logging; - -#[test] -fn test() { - let _logging = setup_logging(); - - tracing::info!("This message will be printed to stderr"); -} -``` - -Note: Most test runners capture stderr and only show its output when a test fails. - -Note also that `setup_logging` only sets up logging for the current thread because [`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be -called **once**. - -## Release builds - -`trace!` events are removed in release builds. - -## Profiling - -Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`. - -```bash -RED_KNOT_LOG_PROFILE=1 red_knot -- --current-directory=../test -vvv -``` - -You can convert the textual representation into a visual one using `inferno`. - -```shell -cargo install inferno -``` - -```shell -# flamegraph -cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg - -# flamechart -cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg -``` - -![Example flamegraph](./tracing-flamegraph.png) - -See [`tracing-flame`](https://crates.io/crates/tracing-flame) for more details. diff --git a/crates/red_knot/src/args.rs b/crates/red_knot/src/args.rs deleted file mode 100644 index 233a081445a42..0000000000000 --- a/crates/red_knot/src/args.rs +++ /dev/null @@ -1,280 +0,0 @@ -use crate::logging::Verbosity; -use crate::python_version::PythonVersion; -use clap::{ArgAction, ArgMatches, Error, Parser}; -use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions}; -use red_knot_project::metadata::value::{RangedValue, RelativePathBuf}; -use red_knot_python_semantic::lint; -use ruff_db::system::SystemPathBuf; - -#[derive(Debug, Parser)] -#[command( - author, - name = "red-knot", - about = "An extremely fast Python type checker." -)] -#[command(version)] -pub(crate) struct Args { - #[command(subcommand)] - pub(crate) command: Command, -} - -#[derive(Debug, clap::Subcommand)] -pub(crate) enum Command { - /// Check a project for type errors. - Check(CheckCommand), - - /// Start the language server - Server, - - /// Display Red Knot's version - Version, -} - -#[derive(Debug, Parser)] -pub(crate) struct CheckCommand { - /// List of files or directories to check. - #[clap( - help = "List of files or directories to check [default: the project root]", - value_name = "PATH" - )] - pub paths: Vec, - - /// Run the command within the given project directory. - /// - /// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory, - /// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set. - /// - /// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory. - #[arg(long, value_name = "PROJECT")] - pub(crate) project: Option, - - /// Path to the Python installation from which Red Knot resolves type information and third-party dependencies. - /// - /// If not specified, Red Knot will look at the `VIRTUAL_ENV` environment variable. - /// - /// Red Knot will search in the path's `site-packages` directories for type information and - /// third-party imports. - /// - /// This option is commonly used to specify the path to a virtual environment. - #[arg(long, value_name = "PATH")] - pub(crate) python: Option, - - /// Custom directory to use for stdlib typeshed stubs. - #[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")] - pub(crate) typeshed: Option, - - /// Additional path to use as a module-resolution source (can be passed multiple times). - #[arg(long, value_name = "PATH")] - pub(crate) extra_search_path: Option>, - - /// Python version to assume when resolving types. - #[arg(long, value_name = "VERSION", alias = "target-version")] - pub(crate) python_version: Option, - - /// Target platform to assume when resolving types. - /// - /// This is used to specialize the type of `sys.platform` and will affect the visibility - /// of platform-specific functions and attributes. If the value is set to `all`, no - /// assumptions are made about the target platform. If unspecified, the current system's - /// platform will be used. - #[arg(long, value_name = "PLATFORM", alias = "platform")] - pub(crate) python_platform: Option, - - #[clap(flatten)] - pub(crate) verbosity: Verbosity, - - #[clap(flatten)] - pub(crate) rules: RulesArg, - - /// The format to use for printing diagnostic messages. - #[arg(long)] - pub(crate) output_format: Option, - - /// Control when colored output is used. - #[arg(long, value_name = "WHEN")] - pub(crate) color: Option, - - /// Use exit code 1 if there are any warning-level diagnostics. - #[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)] - pub(crate) error_on_warning: Option, - - /// Always use exit code 0, even when there are error-level diagnostics. - #[arg(long)] - pub(crate) exit_zero: bool, - - /// Watch files for changes and recheck files related to the changed files. - #[arg(long, short = 'W')] - pub(crate) watch: bool, -} - -impl CheckCommand { - pub(crate) fn into_options(self) -> Options { - let rules = if self.rules.is_empty() { - None - } else { - Some( - self.rules - .into_iter() - .map(|(rule, level)| (RangedValue::cli(rule), RangedValue::cli(level))) - .collect(), - ) - }; - - Options { - environment: Some(EnvironmentOptions { - python_version: self - .python_version - .map(|version| RangedValue::cli(version.into())), - python_platform: self - .python_platform - .map(|platform| RangedValue::cli(platform.into())), - python: self.python.map(RelativePathBuf::cli), - typeshed: self.typeshed.map(RelativePathBuf::cli), - extra_paths: self.extra_search_path.map(|extra_search_paths| { - extra_search_paths - .into_iter() - .map(RelativePathBuf::cli) - .collect() - }), - }), - terminal: Some(TerminalOptions { - output_format: self - .output_format - .map(|output_format| RangedValue::cli(output_format.into())), - error_on_warning: self.error_on_warning, - }), - rules, - ..Default::default() - } - } -} - -/// A list of rules to enable or disable with a given severity. -/// -/// This type is used to parse the `--error`, `--warn`, and `--ignore` arguments -/// while preserving the order in which they were specified (arguments last override previous severities). -#[derive(Debug)] -pub(crate) struct RulesArg(Vec<(String, lint::Level)>); - -impl RulesArg { - fn is_empty(&self) -> bool { - self.0.is_empty() - } - - fn into_iter(self) -> impl Iterator { - self.0.into_iter() - } -} - -impl clap::FromArgMatches for RulesArg { - fn from_arg_matches(matches: &ArgMatches) -> Result { - let mut rules = Vec::new(); - - for (level, arg_id) in [ - (lint::Level::Ignore, "ignore"), - (lint::Level::Warn, "warn"), - (lint::Level::Error, "error"), - ] { - let indices = matches.indices_of(arg_id).into_iter().flatten(); - let levels = matches.get_many::(arg_id).into_iter().flatten(); - rules.extend( - indices - .zip(levels) - .map(|(index, rule)| (index, rule, level)), - ); - } - - // Sort by their index so that values specified later override earlier ones. - rules.sort_by_key(|(index, _, _)| *index); - - Ok(Self( - rules - .into_iter() - .map(|(_, rule, level)| (rule.to_owned(), level)) - .collect(), - )) - } - - fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> { - self.0 = Self::from_arg_matches(matches)?.0; - Ok(()) - } -} - -impl clap::Args for RulesArg { - fn augment_args(cmd: clap::Command) -> clap::Command { - const HELP_HEADING: &str = "Enabling / disabling rules"; - - cmd.arg( - clap::Arg::new("error") - .long("error") - .action(ArgAction::Append) - .help("Treat the given rule as having severity 'error'. Can be specified multiple times.") - .value_name("RULE") - .help_heading(HELP_HEADING), - ) - .arg( - clap::Arg::new("warn") - .long("warn") - .action(ArgAction::Append) - .help("Treat the given rule as having severity 'warn'. Can be specified multiple times.") - .value_name("RULE") - .help_heading(HELP_HEADING), - ) - .arg( - clap::Arg::new("ignore") - .long("ignore") - .action(ArgAction::Append) - .help("Disables the rule. Can be specified multiple times.") - .value_name("RULE") - .help_heading(HELP_HEADING), - ) - } - - fn augment_args_for_update(cmd: clap::Command) -> clap::Command { - Self::augment_args(cmd) - } -} - -/// The diagnostic output format. -#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)] -pub enum OutputFormat { - /// Print diagnostics verbosely, with context and helpful hints. - /// - /// Diagnostic messages may include additional context and - /// annotations on the input to help understand the message. - #[default] - #[value(name = "full")] - Full, - /// Print diagnostics concisely, one per line. - /// - /// This will guarantee that each diagnostic is printed on - /// a single line. Only the most important or primary aspects - /// of the diagnostic are included. Contextual information is - /// dropped. - #[value(name = "concise")] - Concise, -} - -impl From for ruff_db::diagnostic::DiagnosticFormat { - fn from(format: OutputFormat) -> ruff_db::diagnostic::DiagnosticFormat { - match format { - OutputFormat::Full => Self::Full, - OutputFormat::Concise => Self::Concise, - } - } -} - -/// Control when colored output is used. -#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)] -pub(crate) enum TerminalColor { - /// Display colors if the output goes to an interactive terminal. - #[default] - Auto, - - /// Always display colors. - Always, - - /// Never display colors. - Never, -} diff --git a/crates/red_knot/src/logging.rs b/crates/red_knot/src/logging.rs deleted file mode 100644 index c55878c60c952..0000000000000 --- a/crates/red_knot/src/logging.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Sets up logging for Red Knot - -use anyhow::Context; -use colored::Colorize; -use std::fmt; -use std::fs::File; -use std::io::BufWriter; -use tracing::{Event, Subscriber}; -use tracing_subscriber::filter::LevelFilter; -use tracing_subscriber::fmt::format::Writer; -use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; -use tracing_subscriber::registry::LookupSpan; -use tracing_subscriber::EnvFilter; - -/// Logging flags to `#[command(flatten)]` into your CLI -#[derive(clap::Args, Debug, Clone, Default)] -#[command(about = None, long_about = None)] -pub(crate) struct Verbosity { - #[arg( - long, - short = 'v', - help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)", - action = clap::ArgAction::Count, - global = true, - )] - verbose: u8, -} - -impl Verbosity { - /// Returns the verbosity level based on the number of `-v` flags. - /// - /// Returns `None` if the user did not specify any verbosity flags. - pub(crate) fn level(&self) -> VerbosityLevel { - match self.verbose { - 0 => VerbosityLevel::Default, - 1 => VerbosityLevel::Verbose, - 2 => VerbosityLevel::ExtraVerbose, - _ => VerbosityLevel::Trace, - } - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(crate) enum VerbosityLevel { - /// Default output level. Only shows Ruff and Red Knot events up to the [`WARN`](tracing::Level::WARN). - Default, - - /// Enables verbose output. Emits Ruff and Red Knot events up to the [`INFO`](tracing::Level::INFO). - /// Corresponds to `-v`. - Verbose, - - /// Enables a more verbose tracing format and emits Ruff and Red Knot events up to [`DEBUG`](tracing::Level::DEBUG). - /// Corresponds to `-vv` - ExtraVerbose, - - /// Enables all tracing events and uses a tree-like output format. Corresponds to `-vvv`. - Trace, -} - -impl VerbosityLevel { - const fn level_filter(self) -> LevelFilter { - match self { - VerbosityLevel::Default => LevelFilter::WARN, - VerbosityLevel::Verbose => LevelFilter::INFO, - VerbosityLevel::ExtraVerbose => LevelFilter::DEBUG, - VerbosityLevel::Trace => LevelFilter::TRACE, - } - } - - pub(crate) const fn is_trace(self) -> bool { - matches!(self, VerbosityLevel::Trace) - } - - pub(crate) const fn is_extra_verbose(self) -> bool { - matches!(self, VerbosityLevel::ExtraVerbose) - } -} - -pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result { - use tracing_subscriber::prelude::*; - - // The `RED_KNOT_LOG` environment variable overrides the default log level. - let filter = if let Ok(log_env_variable) = std::env::var("RED_KNOT_LOG") { - EnvFilter::builder() - .parse(log_env_variable) - .context("Failed to parse directives specified in RED_KNOT_LOG environment variable.")? - } else { - match level { - VerbosityLevel::Default => { - // Show warning traces - EnvFilter::default().add_directive(LevelFilter::WARN.into()) - } - level => { - let level_filter = level.level_filter(); - - // Show info|debug|trace events, but allow `RED_KNOT_LOG` to override - let filter = EnvFilter::default().add_directive( - format!("red_knot={level_filter}") - .parse() - .expect("Hardcoded directive to be valid"), - ); - - filter.add_directive( - format!("ruff={level_filter}") - .parse() - .expect("Hardcoded directive to be valid"), - ) - } - } - }; - - let (profiling_layer, guard) = setup_profile(); - - let registry = tracing_subscriber::registry() - .with(filter) - .with(profiling_layer); - - if level.is_trace() { - let subscriber = registry.with( - tracing_tree::HierarchicalLayer::default() - .with_indent_lines(true) - .with_indent_amount(2) - .with_bracketed_fields(true) - .with_thread_ids(true) - .with_targets(true) - .with_writer(std::io::stderr) - .with_timer(tracing_tree::time::Uptime::default()), - ); - - subscriber.init(); - } else { - let subscriber = registry.with( - tracing_subscriber::fmt::layer() - .event_format(RedKnotFormat { - display_level: true, - display_timestamp: level.is_extra_verbose(), - show_spans: false, - }) - .with_writer(std::io::stderr), - ); - - subscriber.init(); - } - - Ok(TracingGuard { - _flame_guard: guard, - }) -} - -#[allow(clippy::type_complexity)] -fn setup_profile() -> ( - Option>>, - Option>>, -) -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - if let Ok("1" | "true") = std::env::var("RED_KNOT_LOG_PROFILE").as_deref() { - let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded") - .expect("Flame layer to be created"); - (Some(layer), Some(guard)) - } else { - (None, None) - } -} - -pub(crate) struct TracingGuard { - _flame_guard: Option>>, -} - -struct RedKnotFormat { - display_timestamp: bool, - display_level: bool, - show_spans: bool, -} - -/// See -impl FormatEvent for RedKnotFormat -where - S: Subscriber + for<'a> LookupSpan<'a>, - N: for<'a> FormatFields<'a> + 'static, -{ - fn format_event( - &self, - ctx: &FmtContext<'_, S, N>, - mut writer: Writer<'_>, - event: &Event<'_>, - ) -> fmt::Result { - let meta = event.metadata(); - let ansi = writer.has_ansi_escapes(); - - if self.display_timestamp { - let timestamp = jiff::Zoned::now() - .strftime("%Y-%m-%d %H:%M:%S.%f") - .to_string(); - if ansi { - write!(writer, "{} ", timestamp.dimmed())?; - } else { - write!( - writer, - "{} ", - jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S.%f") - )?; - } - } - - if self.display_level { - let level = meta.level(); - // Same colors as tracing - if ansi { - let formatted_level = level.to_string(); - match *level { - tracing::Level::TRACE => { - write!(writer, "{} ", formatted_level.purple().bold())?; - } - tracing::Level::DEBUG => write!(writer, "{} ", formatted_level.blue().bold())?, - tracing::Level::INFO => write!(writer, "{} ", formatted_level.green().bold())?, - tracing::Level::WARN => write!(writer, "{} ", formatted_level.yellow().bold())?, - tracing::Level::ERROR => write!(writer, "{} ", level.to_string().red().bold())?, - } - } else { - write!(writer, "{level} ")?; - } - } - - if self.show_spans { - let span = event.parent(); - let mut seen = false; - - let span = span - .and_then(|id| ctx.span(id)) - .or_else(|| ctx.lookup_current()); - - let scope = span.into_iter().flat_map(|span| span.scope().from_root()); - - for span in scope { - seen = true; - if ansi { - write!(writer, "{}:", span.metadata().name().bold())?; - } else { - write!(writer, "{}:", span.metadata().name())?; - } - } - - if seen { - writer.write_char(' ')?; - } - } - - ctx.field_format().format_fields(writer.by_ref(), event)?; - - writeln!(writer) - } -} diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs deleted file mode 100644 index 021a777611c33..0000000000000 --- a/crates/red_knot/src/main.rs +++ /dev/null @@ -1,385 +0,0 @@ -use std::io::{self, stdout, BufWriter, Write}; -use std::process::{ExitCode, Termination}; - -use anyhow::Result; -use std::sync::Mutex; - -use crate::args::{Args, CheckCommand, Command, TerminalColor}; -use crate::logging::setup_tracing; -use anyhow::{anyhow, Context}; -use clap::Parser; -use colored::Colorize; -use crossbeam::channel as crossbeam_channel; -use red_knot_project::metadata::options::Options; -use red_knot_project::watch::ProjectWatcher; -use red_knot_project::{watch, Db}; -use red_knot_project::{ProjectDatabase, ProjectMetadata}; -use red_knot_server::run_server; -use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity}; -use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; -use salsa::plumbing::ZalsaDatabase; - -mod args; -mod logging; -mod python_version; -mod version; - -#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)] -pub fn main() -> ExitStatus { - run().unwrap_or_else(|error| { - use std::io::Write; - - // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. - let mut stderr = std::io::stderr().lock(); - - // This communicates that this isn't a linter error but Red Knot itself hard-errored for - // some reason (e.g. failed to resolve the configuration) - writeln!(stderr, "{}", "Red Knot failed".red().bold()).ok(); - // Currently we generally only see one error, but e.g. with io errors when resolving - // the configuration it is help to chain errors ("resolving configuration failed" -> - // "failed to read file: subdir/pyproject.toml") - for cause in error.chain() { - // Exit "gracefully" on broken pipe errors. - // - // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 - if let Some(ioerr) = cause.downcast_ref::() { - if ioerr.kind() == io::ErrorKind::BrokenPipe { - return ExitStatus::Success; - } - } - - writeln!(stderr, " {} {cause}", "Cause:".bold()).ok(); - } - - ExitStatus::Error - }) -} - -fn run() -> anyhow::Result { - let args = wild::args_os(); - let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX) - .context("Failed to read CLI arguments from file")?; - let args = Args::parse_from(args); - - match args.command { - Command::Server => run_server().map(|()| ExitStatus::Success), - Command::Check(check_args) => run_check(check_args), - Command::Version => version().map(|()| ExitStatus::Success), - } -} - -pub(crate) fn version() -> Result<()> { - let mut stdout = BufWriter::new(io::stdout().lock()); - let version_info = crate::version::version(); - writeln!(stdout, "red knot {}", &version_info)?; - Ok(()) -} - -fn run_check(args: CheckCommand) -> anyhow::Result { - set_colored_override(args.color); - - let verbosity = args.verbosity.level(); - countme::enable(verbosity.is_trace()); - let _guard = setup_tracing(verbosity)?; - - tracing::debug!("Version: {}", version::version()); - - // The base path to which all CLI arguments are relative to. - let cwd = { - let cwd = std::env::current_dir().context("Failed to get the current working directory")?; - SystemPathBuf::from_path_buf(cwd) - .map_err(|path| { - anyhow!( - "The current working directory `{}` contains non-Unicode characters. Red Knot only supports Unicode paths.", - path.display() - ) - })? - }; - - let project_path = args - .project - .as_ref() - .map(|project| { - if project.as_std_path().is_dir() { - Ok(SystemPath::absolute(project, &cwd)) - } else { - Err(anyhow!( - "Provided project path `{project}` is not a directory" - )) - } - }) - .transpose()? - .unwrap_or_else(|| cwd.clone()); - - let check_paths: Vec<_> = args - .paths - .iter() - .map(|path| SystemPath::absolute(path, &cwd)) - .collect(); - - let system = OsSystem::new(cwd); - let watch = args.watch; - let exit_zero = args.exit_zero; - - let cli_options = args.into_options(); - let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?; - project_metadata.apply_cli_options(cli_options.clone()); - project_metadata.apply_configuration_files(&system)?; - - let mut db = ProjectDatabase::new(project_metadata, system)?; - - if !check_paths.is_empty() { - db.project().set_included_paths(&mut db, check_paths); - } - - let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options); - - // Listen to Ctrl+C and abort the watch mode. - let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token)); - ctrlc::set_handler(move || { - let mut lock = main_loop_cancellation_token.lock().unwrap(); - - if let Some(token) = lock.take() { - token.stop(); - } - })?; - - let exit_status = if watch { - main_loop.watch(&mut db)? - } else { - main_loop.run(&mut db)? - }; - - tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all()); - - std::mem::forget(db); - - if exit_zero { - Ok(ExitStatus::Success) - } else { - Ok(exit_status) - } -} - -#[derive(Copy, Clone)] -pub enum ExitStatus { - /// Checking was successful and there were no errors. - Success = 0, - - /// Checking was successful but there were errors. - Failure = 1, - - /// Checking failed. - Error = 2, -} - -impl Termination for ExitStatus { - fn report(self) -> ExitCode { - ExitCode::from(self as u8) - } -} - -struct MainLoop { - /// Sender that can be used to send messages to the main loop. - sender: crossbeam_channel::Sender, - - /// Receiver for the messages sent **to** the main loop. - receiver: crossbeam_channel::Receiver, - - /// The file system watcher, if running in watch mode. - watcher: Option, - - cli_options: Options, -} - -impl MainLoop { - fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) { - let (sender, receiver) = crossbeam_channel::bounded(10); - - ( - Self { - sender: sender.clone(), - receiver, - watcher: None, - cli_options, - }, - MainLoopCancellationToken { sender }, - ) - } - - fn watch(mut self, db: &mut ProjectDatabase) -> Result { - tracing::debug!("Starting watch mode"); - let sender = self.sender.clone(); - let watcher = watch::directory_watcher(move |event| { - sender.send(MainLoopMessage::ApplyChanges(event)).unwrap(); - })?; - - self.watcher = Some(ProjectWatcher::new(watcher, db)); - - self.run(db)?; - - Ok(ExitStatus::Success) - } - - fn run(mut self, db: &mut ProjectDatabase) -> Result { - self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); - - let result = self.main_loop(db); - - tracing::debug!("Exiting main loop"); - - result - } - - fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result { - // Schedule the first check. - tracing::debug!("Starting main loop"); - - let mut revision = 0u64; - - while let Ok(message) = self.receiver.recv() { - match message { - MainLoopMessage::CheckWorkspace => { - let db = db.clone(); - let sender = self.sender.clone(); - - // Spawn a new task that checks the project. This needs to be done in a separate thread - // to prevent blocking the main loop here. - rayon::spawn(move || { - if let Ok(result) = db.check() { - // Send the result back to the main loop for printing. - sender - .send(MainLoopMessage::CheckCompleted { result, revision }) - .unwrap(); - } - }); - } - - MainLoopMessage::CheckCompleted { - result, - revision: check_revision, - } => { - let terminal_settings = db.project().settings(db).terminal(); - let display_config = DisplayDiagnosticConfig::default() - .format(terminal_settings.output_format) - .color(colored::control::SHOULD_COLORIZE.should_colorize()); - - let min_error_severity = if terminal_settings.error_on_warning { - Severity::Warning - } else { - Severity::Error - }; - - if check_revision == revision { - if db.project().files(db).is_empty() { - tracing::warn!("No python files found under the given path(s)"); - } - - let mut stdout = stdout().lock(); - - if result.is_empty() { - writeln!(stdout, "All checks passed!")?; - - if self.watcher.is_none() { - return Ok(ExitStatus::Success); - } - } else { - let mut failed = false; - let diagnostics_count = result.len(); - - for diagnostic in result { - write!(stdout, "{}", diagnostic.display(db, &display_config))?; - - failed |= diagnostic.severity() >= min_error_severity; - } - - writeln!( - stdout, - "Found {} diagnostic{}", - diagnostics_count, - if diagnostics_count > 1 { "s" } else { "" } - )?; - - if self.watcher.is_none() { - return Ok(if failed { - ExitStatus::Failure - } else { - ExitStatus::Success - }); - } - } - } else { - tracing::debug!( - "Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}" - ); - } - - tracing::trace!("Counts after last check:\n{}", countme::get_all()); - } - - MainLoopMessage::ApplyChanges(changes) => { - revision += 1; - // Automatically cancels any pending queries and waits for them to complete. - db.apply_changes(changes, Some(&self.cli_options)); - if let Some(watcher) = self.watcher.as_mut() { - watcher.update(db); - } - self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); - } - MainLoopMessage::Exit => { - // Cancel any pending queries and wait for them to complete. - // TODO: Don't use Salsa internal APIs - // [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries) - let _ = db.zalsa_mut(); - return Ok(ExitStatus::Success); - } - } - - tracing::debug!("Waiting for next main loop message."); - } - - Ok(ExitStatus::Success) - } -} - -#[derive(Debug)] -struct MainLoopCancellationToken { - sender: crossbeam_channel::Sender, -} - -impl MainLoopCancellationToken { - fn stop(self) { - self.sender.send(MainLoopMessage::Exit).unwrap(); - } -} - -/// Message sent from the orchestrator to the main loop. -#[derive(Debug)] -enum MainLoopMessage { - CheckWorkspace, - CheckCompleted { - /// The diagnostics that were found during the check. - result: Vec, - revision: u64, - }, - ApplyChanges(Vec), - Exit, -} - -fn set_colored_override(color: Option) { - let Some(color) = color else { - return; - }; - - match color { - TerminalColor::Auto => { - colored::control::unset_override(); - } - TerminalColor::Always => { - colored::control::set_override(true); - } - TerminalColor::Never => { - colored::control::set_override(false); - } - } -} diff --git a/crates/red_knot/src/version.rs b/crates/red_knot/src/version.rs deleted file mode 100644 index 35ccf05f650ba..0000000000000 --- a/crates/red_knot/src/version.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Code for representing Red Knot's release version number. -use std::fmt; - -/// Information about the git repository where Red Knot was built from. -pub(crate) struct CommitInfo { - short_commit_hash: String, - commit_date: String, - commits_since_last_tag: u32, -} - -/// Red Knot's version. -pub(crate) struct VersionInfo { - /// Red Knot's version, such as "0.5.1" - version: String, - /// Information about the git commit we may have been built from. - /// - /// `None` if not built from a git repo or if retrieval failed. - commit_info: Option, -} - -impl fmt::Display for VersionInfo { - /// Formatted version information: `[+] ( )` - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.version)?; - - if let Some(ref ci) = self.commit_info { - if ci.commits_since_last_tag > 0 { - write!(f, "+{}", ci.commits_since_last_tag)?; - } - write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?; - } - - Ok(()) - } -} - -/// Returns information about Red Knot's version. -pub(crate) fn version() -> VersionInfo { - // Environment variables are only read at compile-time - macro_rules! option_env_str { - ($name:expr) => { - option_env!($name).map(|s| s.to_string()) - }; - } - - // This version is pulled from Cargo.toml and set by Cargo - let version = option_env_str!("CARGO_PKG_VERSION").unwrap(); - - // Commit info is pulled from git and set by `build.rs` - let commit_info = - option_env_str!("RED_KNOT_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo { - short_commit_hash, - commit_date: option_env_str!("RED_KNOT_COMMIT_DATE").unwrap(), - commits_since_last_tag: option_env_str!("RED_KNOT_LAST_TAG_DISTANCE") - .as_deref() - .map_or(0, |value| value.parse::().unwrap_or(0)), - }); - - VersionInfo { - version, - commit_info, - } -} - -#[cfg(test)] -mod tests { - use insta::assert_snapshot; - - use super::{CommitInfo, VersionInfo}; - - #[test] - fn version_formatting() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: None, - }; - assert_snapshot!(version, @"0.0.0"); - } - - #[test] - fn version_formatting_with_commit_info() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 0, - }), - }; - assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); - } - - #[test] - fn version_formatting_with_commits_since_last_tag() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 24, - }), - }; - assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); - } -} diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs deleted file mode 100644 index 3f2dd4a98ac93..0000000000000 --- a/crates/red_knot/tests/cli.rs +++ /dev/null @@ -1,1168 +0,0 @@ -use anyhow::Context; -use insta::internals::SettingsBindDropGuard; -use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; -use std::path::{Path, PathBuf}; -use std::process::Command; -use tempfile::TempDir; - -/// Specifying an option on the CLI should take precedence over the same setting in the -/// project's configuration. Here, this is tested for the Python version. -#[test] -fn config_override_python_version() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.knot.environment] - python-version = "3.11" - "#, - ), - ( - "test.py", - r#" - import sys - - # Access `sys.last_exc` that was only added in Python 3.12 - print(sys.last_exc) - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:unresolved-attribute - --> /test.py:5:7 - | - 4 | # Access `sys.last_exc` that was only added in Python 3.12 - 5 | print(sys.last_exc) - | ^^^^^^^^^^^^ Type `` has no attribute `last_exc` - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - - Ok(()) -} - -/// Same as above, but for the Python platform. -#[test] -fn config_override_python_platform() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.knot.environment] - python-platform = "linux" - "#, - ), - ( - "test.py", - r#" - import sys - from typing_extensions import reveal_type - - reveal_type(sys.platform) - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: true - exit_code: 0 - ----- stdout ----- - info: revealed-type: Revealed type - --> /test.py:5:1 - | - 3 | from typing_extensions import reveal_type - 4 | - 5 | reveal_type(sys.platform) - | ^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal["linux"]` - | - - Found 1 diagnostic - - ----- stderr ----- - "#); - - assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r" - success: true - exit_code: 0 - ----- stdout ----- - info: revealed-type: Revealed type - --> /test.py:5:1 - | - 3 | from typing_extensions import reveal_type - 4 | - 5 | reveal_type(sys.platform) - | ^^^^^^^^^^^^^^^^^^^^^^^^^ `LiteralString` - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -/// Paths specified on the CLI are relative to the current working directory and not the project root. -/// -/// We test this by adding an extra search path from the CLI to the libs directory when -/// running the CLI from the child directory (using relative paths). -/// -/// Project layout: -/// ``` -/// - libs -/// |- utils.py -/// - child -/// | - test.py -/// - pyproject.toml -/// ``` -/// -/// And the command is run in the `child` directory. -#[test] -fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.knot.environment] - python-version = "3.11" - "#, - ), - ( - "libs/utils.py", - r#" - def add(a: int, b: int) -> int: - return a + b - "#, - ), - ( - "child/test.py", - r#" - from utils import add - - stat = add(10, 15) - "#, - ), - ])?; - - // Make sure that the CLI fails when the `libs` directory is not in the search path. - assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:unresolved-import - --> /child/test.py:2:6 - | - 2 | from utils import add - | ^^^^^ Cannot resolve import `utils` - 3 | - 4 | stat = add(10, 15) - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - - Ok(()) -} - -/// Paths specified in a configuration file are relative to the project root. -/// -/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run -/// the CLI from a subdirectory. -/// -/// Project layout: -/// ``` -/// - libs -/// |- utils.py -/// - child -/// | - test.py -/// - pyproject.toml -/// ``` -#[test] -fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.knot.environment] - python-version = "3.11" - extra-paths = ["libs"] - "#, - ), - ( - "libs/utils.py", - r#" - def add(a: int, b: int) -> int: - return a + b - "#, - ), - ( - "child/test.py", - r#" - from utils import add - - stat = add(10, 15) - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - - Ok(()) -} - -/// The rule severity can be changed in the configuration file -#[test] -fn configuration_rule_severity() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - y = 4 / 0 - - for a in range(0, y): - x = a - - print(x) # possibly-unresolved-reference - "#, - )?; - - // Assert that there's a possibly unresolved reference diagnostic - // and that division-by-zero has a severity of error by default. - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:division-by-zero - --> /test.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 3 | - 4 | for a in range(0, y): - | - - warning: lint:possibly-unresolved-reference - --> /test.py:7:7 - | - 5 | x = a - 6 | - 7 | print(x) # possibly-unresolved-reference - | ^ Name `x` used when possibly not defined - | - - Found 2 diagnostics - - ----- stderr ----- - "); - - case.write_file( - "pyproject.toml", - r#" - [tool.knot.rules] - division-by-zero = "warn" # demote to warn - possibly-unresolved-reference = "ignore" - "#, - )?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: lint:division-by-zero - --> /test.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 3 | - 4 | for a in range(0, y): - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` -#[test] -fn cli_rule_severity() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - import does_not_exit - - y = 4 / 0 - - for a in range(0, y): - x = a - - print(x) # possibly-unresolved-reference - "#, - )?; - - // Assert that there's a possibly unresolved reference diagnostic - // and that division-by-zero has a severity of error by default. - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:unresolved-import - --> /test.py:2:8 - | - 2 | import does_not_exit - | ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit` - 3 | - 4 | y = 4 / 0 - | - - error: lint:division-by-zero - --> /test.py:4:5 - | - 2 | import does_not_exit - 3 | - 4 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 5 | - 6 | for a in range(0, y): - | - - warning: lint:possibly-unresolved-reference - --> /test.py:9:7 - | - 7 | x = a - 8 | - 9 | print(x) # possibly-unresolved-reference - | ^ Name `x` used when possibly not defined - | - - Found 3 diagnostics - - ----- stderr ----- - "); - - assert_cmd_snapshot!( - case - .command() - .arg("--ignore") - .arg("possibly-unresolved-reference") - .arg("--warn") - .arg("division-by-zero") - .arg("--warn") - .arg("unresolved-import"), - @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: lint:unresolved-import - --> /test.py:2:8 - | - 2 | import does_not_exit - | ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit` - 3 | - 4 | y = 4 / 0 - | - - warning: lint:division-by-zero - --> /test.py:4:5 - | - 2 | import does_not_exit - 3 | - 4 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 5 | - 6 | for a in range(0, y): - | - - Found 2 diagnostics - - ----- stderr ----- - " - ); - - Ok(()) -} - -/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and -/// values specified last override previous severities. -#[test] -fn cli_rule_severity_precedence() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - y = 4 / 0 - - for a in range(0, y): - x = a - - print(x) # possibly-unresolved-reference - "#, - )?; - - // Assert that there's a possibly unresolved reference diagnostic - // and that division-by-zero has a severity of error by default. - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:division-by-zero - --> /test.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 3 | - 4 | for a in range(0, y): - | - - warning: lint:possibly-unresolved-reference - --> /test.py:7:7 - | - 5 | x = a - 6 | - 7 | print(x) # possibly-unresolved-reference - | ^ Name `x` used when possibly not defined - | - - Found 2 diagnostics - - ----- stderr ----- - "); - - assert_cmd_snapshot!( - case - .command() - .arg("--error") - .arg("possibly-unresolved-reference") - .arg("--warn") - .arg("division-by-zero") - // Override the error severity with warning - .arg("--ignore") - .arg("possibly-unresolved-reference"), - @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: lint:division-by-zero - --> /test.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 3 | - 4 | for a in range(0, y): - | - - Found 1 diagnostic - - ----- stderr ----- - " - ); - - Ok(()) -} - -/// Red Knot warns about unknown rules specified in a configuration file -#[test] -fn configuration_unknown_rules() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.knot.rules] - division-by-zer = "warn" # incorrect rule name - "#, - ), - ("test.py", "print(10)"), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: true - exit_code: 0 - ----- stdout ----- - warning: unknown-rule - --> /pyproject.toml:3:1 - | - 2 | [tool.knot.rules] - 3 | division-by-zer = "warn" # incorrect rule name - | ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer` - | - - Found 1 diagnostic - - ----- stderr ----- - "#); - - Ok(()) -} - -/// Red Knot warns about unknown rules specified in a CLI argument -#[test] -fn cli_unknown_rules() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", "print(10)")?; - - assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: unknown-rule: Unknown lint rule `division-by-zer` - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_only_warnings() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: lint:unresolved-reference - --> /test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_only_info() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - from typing_extensions import reveal_type - reveal_type(1) - "#, - )?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - info: revealed-type: Revealed type - --> /test.py:3:1 - | - 2 | from typing_extensions import reveal_type - 3 | reveal_type(1) - | ^^^^^^^^^^^^^^ `Literal[1]` - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - from typing_extensions import reveal_type - reveal_type(1) - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" - success: true - exit_code: 0 - ----- stdout ----- - info: revealed-type: Revealed type - --> /test.py:3:1 - | - 2 | from typing_extensions import reveal_type - 3 | reveal_type(1) - | ^^^^^^^^^^^^^^ `Literal[1]` - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; - - assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning: lint:unresolved-reference - --> /test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ("test.py", r"print(x) # [unresolved-reference]"), - ( - "knot.toml", - r#" - [terminal] - error-on-warning = true - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning: lint:unresolved-reference - --> /test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "#, - )?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning: lint:unresolved-reference - --> /test.py:2:7 - | - 2 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined - 3 | print(4[1]) # [non-subscriptable] - | - - error: lint:non-subscriptable - --> /test.py:3:7 - | - 2 | print(x) # [unresolved-reference] - 3 | print(4[1]) # [non-subscriptable] - | ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method - | - - Found 2 diagnostics - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r###" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "###, - )?; - - assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning: lint:unresolved-reference - --> /test.py:2:7 - | - 2 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined - 3 | print(4[1]) # [non-subscriptable] - | - - error: lint:non-subscriptable - --> /test.py:3:7 - | - 2 | print(x) # [unresolved-reference] - 3 | print(4[1]) # [non-subscriptable] - | ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method - | - - Found 2 diagnostics - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: lint:unresolved-reference - --> /test.py:2:7 - | - 2 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined - 3 | print(4[1]) # [non-subscriptable] - | - - error: lint:non-subscriptable - --> /test.py:3:7 - | - 2 | print(x) # [unresolved-reference] - 3 | print(4[1]) # [non-subscriptable] - | ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method - | - - Found 2 diagnostics - - ----- stderr ----- - "); - - Ok(()) -} - -#[test] -fn user_configuration() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "project/knot.toml", - r#" - [rules] - division-by-zero = "warn" - "#, - ), - ( - "project/main.py", - r#" - y = 4 / 0 - - for a in range(0, y): - x = a - - print(x) - "#, - ), - ])?; - - let config_directory = case.root().join("home/.config"); - let config_env_var = if cfg!(windows) { - "APPDATA" - } else { - "XDG_CONFIG_HOME" - }; - - assert_cmd_snapshot!( - case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), - @r" - success: true - exit_code: 0 - ----- stdout ----- - warning: lint:division-by-zero - --> /project/main.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 3 | - 4 | for a in range(0, y): - | - - warning: lint:possibly-unresolved-reference - --> /project/main.py:7:7 - | - 5 | x = a - 6 | - 7 | print(x) - | ^ Name `x` used when possibly not defined - | - - Found 2 diagnostics - - ----- stderr ----- - " - ); - - // The user-level configuration promotes `possibly-unresolved-reference` to an error. - // Changing the level for `division-by-zero` has no effect, because the project-level configuration - // has higher precedence. - case.write_file( - config_directory.join("knot/knot.toml"), - r#" - [rules] - division-by-zero = "error" - possibly-unresolved-reference = "error" - "#, - )?; - - assert_cmd_snapshot!( - case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), - @r" - success: false - exit_code: 1 - ----- stdout ----- - warning: lint:division-by-zero - --> /project/main.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - 3 | - 4 | for a in range(0, y): - | - - error: lint:possibly-unresolved-reference - --> /project/main.py:7:7 - | - 5 | x = a - 6 | - 7 | print(x) - | ^ Name `x` used when possibly not defined - | - - Found 2 diagnostics - - ----- stderr ----- - " - ); - - Ok(()) -} - -#[test] -fn check_specific_paths() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "project/main.py", - r#" - y = 4 / 0 # error: division-by-zero - "#, - ), - ( - "project/tests/test_main.py", - r#" - import does_not_exist # error: unresolved-import - "#, - ), - ( - "project/other.py", - r#" - from main2 import z # error: unresolved-import - - print(z) - "#, - ), - ])?; - - assert_cmd_snapshot!( - case.command(), - @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:unresolved-import - --> /project/tests/test_main.py:2:8 - | - 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` - | - - error: lint:division-by-zero - --> /project/main.py:2:5 - | - 2 | y = 4 / 0 # error: division-by-zero - | ^^^^^ Cannot divide object of type `Literal[4]` by zero - | - - error: lint:unresolved-import - --> /project/other.py:2:6 - | - 2 | from main2 import z # error: unresolved-import - | ^^^^^ Cannot resolve import `main2` - 3 | - 4 | print(z) - | - - Found 3 diagnostics - - ----- stderr ----- - " - ); - - // Now check only the `tests` and `other.py` files. - // We should no longer see any diagnostics related to `main.py`. - assert_cmd_snapshot!( - case.command().arg("project/tests").arg("project/other.py"), - @r" - success: false - exit_code: 1 - ----- stdout ----- - error: lint:unresolved-import - --> /project/tests/test_main.py:2:8 - | - 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` - | - - error: lint:unresolved-import - --> /project/other.py:2:6 - | - 2 | from main2 import z # error: unresolved-import - | ^^^^^ Cannot resolve import `main2` - 3 | - 4 | print(z) - | - - Found 2 diagnostics - - ----- stderr ----- - " - ); - - Ok(()) -} - -#[test] -fn check_non_existing_path() -> anyhow::Result<()> { - let case = TestCase::with_files([])?; - - let mut settings = insta::Settings::clone_current(); - settings.add_filter( - ®ex::escape("The system cannot find the path specified. (os error 3)"), - "No such file or directory (os error 2)", - ); - let _s = settings.bind_to_scope(); - - assert_cmd_snapshot!( - case.command().arg("project/main.py").arg("project/tests"), - @r" - success: false - exit_code: 1 - ----- stdout ----- - error: io: `/project/main.py`: No such file or directory (os error 2) - - error: io: `/project/tests`: No such file or directory (os error 2) - - Found 2 diagnostics - - ----- stderr ----- - WARN No python files found under the given path(s) - " - ); - - Ok(()) -} - -#[test] -fn concise_diagnostics() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[lint:unresolved-reference] /test.py:2:7: Name `x` used when not defined - error[lint:non-subscriptable] /test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - Found 2 diagnostics - - ----- stderr ----- - "); - - Ok(()) -} - -/// This tests the diagnostic format for revealed type. -/// -/// This test was introduced because changes were made to -/// how the revealed type diagnostic was constructed and -/// formatted in "verbose" mode. But it required extra -/// logic to ensure the concise version didn't regress on -/// information content. So this test was introduced to -/// capture that. -#[test] -fn concise_revealed_type() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - from typing_extensions import reveal_type - - x = "hello" - reveal_type(x) - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type] /test.py:5:1: Revealed type: `Literal["hello"]` - Found 1 diagnostic - - ----- stderr ----- - "#); - - Ok(()) -} - -struct TestCase { - _temp_dir: TempDir, - _settings_scope: SettingsBindDropGuard, - project_dir: PathBuf, -} - -impl TestCase { - fn new() -> anyhow::Result { - let temp_dir = TempDir::new()?; - - // Canonicalize the tempdir path because macos uses symlinks for tempdirs - // and that doesn't play well with our snapshot filtering. - let project_dir = temp_dir - .path() - .canonicalize() - .context("Failed to canonicalize project path")?; - - let mut settings = insta::Settings::clone_current(); - settings.add_filter(&tempdir_filter(&project_dir), "/"); - settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); - - let settings_scope = settings.bind_to_scope(); - - Ok(Self { - project_dir, - _temp_dir: temp_dir, - _settings_scope: settings_scope, - }) - } - - fn with_files<'a>(files: impl IntoIterator) -> anyhow::Result { - let case = Self::new()?; - case.write_files(files)?; - Ok(case) - } - - fn with_file(path: impl AsRef, content: &str) -> anyhow::Result { - let case = Self::new()?; - case.write_file(path, content)?; - Ok(case) - } - - fn write_files<'a>( - &self, - files: impl IntoIterator, - ) -> anyhow::Result<()> { - for (path, content) in files { - self.write_file(path, content)?; - } - - Ok(()) - } - - fn write_file(&self, path: impl AsRef, content: &str) -> anyhow::Result<()> { - let path = path.as_ref(); - let path = self.project_dir.join(path); - - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create directory `{}`", parent.display()))?; - } - std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content)) - .with_context(|| format!("Failed to write file `{path}`", path = path.display()))?; - - Ok(()) - } - - fn root(&self) -> &Path { - &self.project_dir - } - - fn command(&self) -> Command { - let mut command = Command::new(get_cargo_bin("red_knot")); - command.current_dir(&self.project_dir).arg("check"); - command - } -} - -fn tempdir_filter(path: &Path) -> String { - format!(r"{}\\?/?", regex::escape(path.to_str().unwrap())) -} diff --git a/crates/red_knot_ide/Cargo.toml b/crates/red_knot_ide/Cargo.toml deleted file mode 100644 index 5ac625385299d..0000000000000 --- a/crates/red_knot_ide/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "red_knot_ide" -version = "0.0.0" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[dependencies] -ruff_db = { workspace = true } -ruff_python_ast = { workspace = true } -ruff_python_parser = { workspace = true } -ruff_text_size = { workspace = true } -red_knot_python_semantic = { workspace = true } - -rustc-hash = { workspace = true } -salsa = { workspace = true } -smallvec = { workspace = true } -tracing = { workspace = true } - -[dev-dependencies] -red_knot_vendored = { workspace = true } - -insta = { workspace = true, features = ["filters"] } - -[lints] -workspace = true diff --git a/crates/red_knot_ide/src/db.rs b/crates/red_knot_ide/src/db.rs deleted file mode 100644 index ba49ee864f73e..0000000000000 --- a/crates/red_knot_ide/src/db.rs +++ /dev/null @@ -1,134 +0,0 @@ -use red_knot_python_semantic::Db as SemanticDb; -use ruff_db::{Db as SourceDb, Upcast}; - -#[salsa::db] -pub trait Db: SemanticDb + Upcast + Upcast {} - -#[cfg(test)] -pub(crate) mod tests { - use std::sync::Arc; - - use super::Db; - use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; - use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb}; - use ruff_db::files::{File, Files}; - use ruff_db::system::{DbWithTestSystem, System, TestSystem}; - use ruff_db::vendored::VendoredFileSystem; - use ruff_db::{Db as SourceDb, Upcast}; - - #[salsa::db] - #[derive(Clone)] - pub(crate) struct TestDb { - storage: salsa::Storage, - files: Files, - system: TestSystem, - vendored: VendoredFileSystem, - events: Arc>>, - rule_selection: Arc, - } - - #[allow(dead_code)] - impl TestDb { - pub(crate) fn new() -> Self { - Self { - storage: salsa::Storage::default(), - system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), - events: Arc::default(), - files: Files::default(), - rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), - } - } - - /// Takes the salsa events. - /// - /// ## Panics - /// If there are any pending salsa snapshots. - pub(crate) fn take_salsa_events(&mut self) -> Vec { - let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots"); - - let events = inner.get_mut().unwrap(); - std::mem::take(&mut *events) - } - - /// Clears the salsa events. - /// - /// ## Panics - /// If there are any pending salsa snapshots. - pub(crate) fn clear_salsa_events(&mut self) { - self.take_salsa_events(); - } - } - - impl DbWithTestSystem for TestDb { - fn test_system(&self) -> &TestSystem { - &self.system - } - - fn test_system_mut(&mut self) -> &mut TestSystem { - &mut self.system - } - } - - #[salsa::db] - impl SourceDb for TestDb { - fn vendored(&self) -> &VendoredFileSystem { - &self.vendored - } - - fn system(&self) -> &dyn System { - &self.system - } - - fn files(&self) -> &Files { - &self.files - } - } - - impl Upcast for TestDb { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } - } - - impl Upcast for TestDb { - fn upcast(&self) -> &(dyn SemanticDb + 'static) { - self - } - - fn upcast_mut(&mut self) -> &mut dyn SemanticDb { - self - } - } - - #[salsa::db] - impl SemanticDb for TestDb { - fn is_file_open(&self, file: File) -> bool { - !file.path(self).is_vendored_path() - } - - fn rule_selection(&self) -> Arc { - self.rule_selection.clone() - } - - fn lint_registry(&self) -> &LintRegistry { - default_lint_registry() - } - } - - #[salsa::db] - impl Db for TestDb {} - - #[salsa::db] - impl salsa::Database for TestDb { - fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) { - let event = event(); - tracing::trace!("event: {event:?}"); - let mut events = self.events.lock().unwrap(); - events.push(event); - } - } -} diff --git a/crates/red_knot_ide/src/find_node.rs b/crates/red_knot_ide/src/find_node.rs deleted file mode 100644 index c3ea78d3d6810..0000000000000 --- a/crates/red_knot_ide/src/find_node.rs +++ /dev/null @@ -1,106 +0,0 @@ -use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal}; -use ruff_python_ast::AnyNodeRef; -use ruff_text_size::{Ranged, TextRange}; -use std::fmt; -use std::fmt::Formatter; - -/// Returns the node with a minimal range that fully contains `range`. -/// -/// If `range` is empty and falls within a parser *synthesized* node generated during error recovery, -/// then the first node with the given range is returned. -/// -/// ## Panics -/// Panics if `range` is not contained within `root`. -pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode { - struct Visitor<'a> { - range: TextRange, - found: bool, - ancestors: Vec>, - } - - impl<'a> SourceOrderVisitor<'a> for Visitor<'a> { - fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal { - // If the node fully contains the range, than it is a possible match but traverse into its children - // to see if there's a node with a narrower range. - if !self.found && node.range().contains_range(self.range) { - self.ancestors.push(node); - TraversalSignal::Traverse - } else { - TraversalSignal::Skip - } - } - - fn leave_node(&mut self, node: AnyNodeRef<'a>) { - if !self.found && self.ancestors.last() == Some(&node) { - self.found = true; - } - } - } - - assert!( - root.range().contains_range(range), - "Range is not contained within root" - ); - - let mut visitor = Visitor { - range, - found: false, - ancestors: Vec::new(), - }; - - root.visit_source_order(&mut visitor); - - let minimal = visitor.ancestors.pop().unwrap_or(root); - CoveringNode { - node: minimal, - ancestors: visitor.ancestors, - } -} - -/// The node with a minimal range that fully contains the search range. -pub(crate) struct CoveringNode<'a> { - /// The node with a minimal range that fully contains the search range. - node: AnyNodeRef<'a>, - - /// The node's ancestor (the spine up to the root). - ancestors: Vec>, -} - -impl<'a> CoveringNode<'a> { - pub(crate) fn node(&self) -> AnyNodeRef<'a> { - self.node - } - - /// Returns the node's parent. - pub(crate) fn parent(&self) -> Option> { - self.ancestors.last().copied() - } - - /// Finds the minimal node that fully covers the range and fulfills the given predicate. - pub(crate) fn find(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result { - if f(self.node) { - return Ok(self); - } - - match self.ancestors.iter().rposition(|node| f(*node)) { - Some(index) => { - let node = self.ancestors[index]; - self.ancestors.truncate(index); - - Ok(Self { - node, - ancestors: self.ancestors, - }) - } - None => Err(self), - } - } -} - -impl fmt::Debug for CoveringNode<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_tuple("NodeWithAncestors") - .field(&self.node) - .finish() - } -} diff --git a/crates/red_knot_ide/src/hover.rs b/crates/red_knot_ide/src/hover.rs deleted file mode 100644 index dfe3f780e1683..0000000000000 --- a/crates/red_knot_ide/src/hover.rs +++ /dev/null @@ -1,602 +0,0 @@ -use crate::goto::{find_goto_target, GotoTarget}; -use crate::{Db, MarkupKind, RangedValue}; -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::SemanticModel; -use ruff_db::files::{File, FileRange}; -use ruff_db::parsed::parsed_module; -use ruff_text_size::{Ranged, TextSize}; -use std::fmt; -use std::fmt::Formatter; - -pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option> { - let parsed = parsed_module(db.upcast(), file); - let goto_target = find_goto_target(parsed, offset)?; - - if let GotoTarget::Expression(expr) = goto_target { - if expr.is_literal_expr() { - return None; - } - } - - let model = SemanticModel::new(db.upcast(), file); - let ty = goto_target.inferred_type(&model)?; - - tracing::debug!( - "Inferred type of covering node is {}", - ty.display(db.upcast()) - ); - - // TODO: Add documentation of the symbol (not the type's definition). - // TODO: Render the symbol's signature instead of just its type. - let contents = vec![HoverContent::Type(ty)]; - - Some(RangedValue { - range: FileRange::new(file, goto_target.range()), - value: Hover { contents }, - }) -} - -pub struct Hover<'db> { - contents: Vec>, -} - -impl<'db> Hover<'db> { - /// Renders the hover to a string using the specified markup kind. - pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> { - DisplayHover { - db, - hover: self, - kind, - } - } - - fn iter(&self) -> std::slice::Iter<'_, HoverContent<'db>> { - self.contents.iter() - } -} - -impl<'db> IntoIterator for Hover<'db> { - type Item = HoverContent<'db>; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.contents.into_iter() - } -} - -impl<'a, 'db> IntoIterator for &'a Hover<'db> { - type Item = &'a HoverContent<'db>; - type IntoIter = std::slice::Iter<'a, HoverContent<'db>>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -pub struct DisplayHover<'a> { - db: &'a dyn Db, - hover: &'a Hover<'a>, - kind: MarkupKind, -} - -impl fmt::Display for DisplayHover<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut first = true; - for content in &self.hover.contents { - if !first { - self.kind.horizontal_line().fmt(f)?; - } - - content.display(self.db, self.kind).fmt(f)?; - first = false; - } - - Ok(()) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum HoverContent<'db> { - Type(Type<'db>), -} - -impl<'db> HoverContent<'db> { - fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> { - DisplayHoverContent { - db, - content: self, - kind, - } - } -} - -pub(crate) struct DisplayHoverContent<'a, 'db> { - db: &'db dyn Db, - content: &'a HoverContent<'db>, - kind: MarkupKind, -} - -impl fmt::Display for DisplayHoverContent<'_, '_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.content { - HoverContent::Type(ty) => self - .kind - .fenced_code_block(ty.display(self.db.upcast()), "text") - .fmt(f), - } - } -} - -#[cfg(test)] -mod tests { - use crate::tests::{cursor_test, CursorTest}; - use crate::{hover, MarkupKind}; - use insta::assert_snapshot; - use ruff_db::diagnostic::{ - Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName, - Severity, Span, - }; - use ruff_text_size::{Ranged, TextRange}; - - #[test] - fn hover_basic() { - let test = cursor_test( - r#" - a = 10 - - a - "#, - ); - - assert_snapshot!(test.hover(), @r" - Literal[10] - --------------------------------------------- - ```text - Literal[10] - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:4:9 - | - 2 | a = 10 - 3 | - 4 | a - | ^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_member() { - let test = cursor_test( - r#" - class Foo: - a: int = 10 - - def __init__(a: int, b: str): - self.a = a - self.b: str = b - - foo = Foo() - foo.a - "#, - ); - - assert_snapshot!(test.hover(), @r" - int - --------------------------------------------- - ```text - int - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:10:9 - | - 9 | foo = Foo() - 10 | foo.a - | ^^^^- - | | | - | | Cursor offset - | source - | - "); - } - - #[test] - fn hover_function_typed_variable() { - let test = cursor_test( - r#" - def foo(a, b): ... - - foo - "#, - ); - - assert_snapshot!(test.hover(), @r###" - def foo(a, b) -> Unknown - --------------------------------------------- - ```text - def foo(a, b) -> Unknown - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:4:13 - | - 2 | def foo(a, b): ... - 3 | - 4 | foo - | ^^^- Cursor offset - | | - | source - | - "###); - } - - #[test] - fn hover_binary_expression() { - let test = cursor_test( - r#" - def foo(a: int, b: int, c: int): - a + b == c - "#, - ); - - assert_snapshot!(test.hover(), @r" - bool - --------------------------------------------- - ```text - bool - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:3:17 - | - 2 | def foo(a: int, b: int, c: int): - 3 | a + b == c - | ^^^^^^^^-^ - | | | - | | Cursor offset - | source - | - "); - } - - #[test] - fn hover_keyword_parameter() { - let test = cursor_test( - r#" - def test(a: int): ... - - test(a= 123) - "#, - ); - - // TODO: This should reveal `int` because the user hovers over the parameter and not the value. - assert_snapshot!(test.hover(), @r" - Literal[123] - --------------------------------------------- - ```text - Literal[123] - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:4:18 - | - 2 | def test(a: int): ... - 3 | - 4 | test(a= 123) - | ^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_union() { - let test = cursor_test( - r#" - - def foo(a, b): ... - - def bar(a, b): ... - - if random.choice([True, False]): - a = foo - else: - a = bar - - a - "#, - ); - - assert_snapshot!(test.hover(), @r###" - (def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown) - --------------------------------------------- - ```text - (def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown) - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:12:13 - | - 10 | a = bar - 11 | - 12 | a - | ^- Cursor offset - | | - | source - | - "###); - } - - #[test] - fn hover_module() { - let mut test = cursor_test( - r#" - import lib - - lib - "#, - ); - - test.write_file("lib.py", "a = 10").unwrap(); - - assert_snapshot!(test.hover(), @r" - - --------------------------------------------- - ```text - - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:4:13 - | - 2 | import lib - 3 | - 4 | lib - | ^^- - | | | - | | Cursor offset - | source - | - "); - } - - #[test] - fn hover_type_of_expression_with_type_var_type() { - let test = cursor_test( - r#" - type Alias[T: int = bool] = list[T] - "#, - ); - - assert_snapshot!(test.hover(), @r" - T - --------------------------------------------- - ```text - T - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:2:46 - | - 2 | type Alias[T: int = bool] = list[T] - | ^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_type_of_expression_with_type_param_spec() { - let test = cursor_test( - r#" - type Alias[**P = [int, str]] = Callable[P, int] - "#, - ); - - assert_snapshot!(test.hover(), @r" - @Todo - --------------------------------------------- - ```text - @Todo - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:2:53 - | - 2 | type Alias[**P = [int, str]] = Callable[P, int] - | ^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_type_of_expression_with_type_var_tuple() { - let test = cursor_test( - r#" - type Alias[*Ts = ()] = tuple[*Ts] - "#, - ); - - assert_snapshot!(test.hover(), @r" - @Todo - --------------------------------------------- - ```text - @Todo - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:2:43 - | - 2 | type Alias[*Ts = ()] = tuple[*Ts] - | ^^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_class_member_declaration() { - let test = cursor_test( - r#" - class Foo: - a: int - "#, - ); - - // TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122 - assert_snapshot!(test.hover(), @r" - Never - --------------------------------------------- - ```text - Never - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:3:13 - | - 2 | class Foo: - 3 | a: int - | ^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_type_narrowing() { - let test = cursor_test( - r#" - def foo(a: str | None, b): - if a is not None: - print(a) - "#, - ); - - assert_snapshot!(test.hover(), @r" - str - --------------------------------------------- - ```text - str - ``` - --------------------------------------------- - info: lint:hover: Hovered content is - --> /main.py:4:27 - | - 2 | def foo(a: str | None, b): - 3 | if a is not None: - 4 | print(a) - | ^- Cursor offset - | | - | source - | - "); - } - - #[test] - fn hover_whitespace() { - let test = cursor_test( - r#" - class C: - - foo: str = 'bar' - "#, - ); - - assert_snapshot!(test.hover(), @"Hover provided no content"); - } - - #[test] - fn hover_literal_int() { - let test = cursor_test( - r#" - print( - 0 + 1 - ) - "#, - ); - - assert_snapshot!(test.hover(), @"Hover provided no content"); - } - - #[test] - fn hover_literal_ellipsis() { - let test = cursor_test( - r#" - print( - ... - ) - "#, - ); - - assert_snapshot!(test.hover(), @"Hover provided no content"); - } - - #[test] - fn hover_docstring() { - let test = cursor_test( - r#" - def f(): - """Lorem ipsum dolor sit amet.""" - "#, - ); - - assert_snapshot!(test.hover(), @"Hover provided no content"); - } - - impl CursorTest { - fn hover(&self) -> String { - use std::fmt::Write; - - let Some(hover) = hover(&self.db, self.file, self.cursor_offset) else { - return "Hover provided no content".to_string(); - }; - - let source = hover.range; - - let mut buf = String::new(); - - write!( - &mut buf, - "{plaintext}{line}{markdown}{line}", - plaintext = hover.display(&self.db, MarkupKind::PlainText), - line = MarkupKind::PlainText.horizontal_line(), - markdown = hover.display(&self.db, MarkupKind::Markdown), - ) - .unwrap(); - - let config = DisplayDiagnosticConfig::default() - .color(false) - .format(DiagnosticFormat::Full); - - let mut diagnostic = Diagnostic::new( - DiagnosticId::Lint(LintName::of("hover")), - Severity::Info, - "Hovered content is", - ); - diagnostic.annotate( - Annotation::primary(Span::from(source.file()).with_range(source.range())) - .message("source"), - ); - diagnostic.annotate( - Annotation::secondary( - Span::from(source.file()).with_range(TextRange::empty(self.cursor_offset)), - ) - .message("Cursor offset"), - ); - - write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap(); - - buf - } - } -} diff --git a/crates/red_knot_ide/src/inlay_hints.rs b/crates/red_knot_ide/src/inlay_hints.rs deleted file mode 100644 index b282b0edfc1dc..0000000000000 --- a/crates/red_knot_ide/src/inlay_hints.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::Db; -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::{HasType, SemanticModel}; -use ruff_db::files::File; -use ruff_db::parsed::parsed_module; -use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; -use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; -use ruff_text_size::{Ranged, TextRange, TextSize}; -use std::fmt; -use std::fmt::Formatter; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct InlayHint<'db> { - pub position: TextSize, - pub content: InlayHintContent<'db>, -} - -impl<'db> InlayHint<'db> { - pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> { - self.content.display(db) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum InlayHintContent<'db> { - Type(Type<'db>), - ReturnType(Type<'db>), -} - -impl<'db> InlayHintContent<'db> { - pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> { - DisplayInlayHint { db, hint: self } - } -} - -pub struct DisplayInlayHint<'a, 'db> { - db: &'db dyn Db, - hint: &'a InlayHintContent<'db>, -} - -impl fmt::Display for DisplayInlayHint<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self.hint { - InlayHintContent::Type(ty) => { - write!(f, ": {}", ty.display(self.db.upcast())) - } - InlayHintContent::ReturnType(ty) => { - write!(f, " -> {}", ty.display(self.db.upcast())) - } - } - } -} - -pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec> { - let mut visitor = InlayHintVisitor::new(db, file, range); - - let ast = parsed_module(db.upcast(), file); - - visitor.visit_body(ast.suite()); - - visitor.hints -} - -struct InlayHintVisitor<'db> { - model: SemanticModel<'db>, - hints: Vec>, - in_assignment: bool, - range: TextRange, -} - -impl<'db> InlayHintVisitor<'db> { - fn new(db: &'db dyn Db, file: File, range: TextRange) -> Self { - Self { - model: SemanticModel::new(db.upcast(), file), - hints: Vec::new(), - in_assignment: false, - range, - } - } - - fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) { - self.hints.push(InlayHint { - position, - content: InlayHintContent::Type(ty), - }); - } -} - -impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> { - fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal { - if self.range.intersect(node.range()).is_some() { - TraversalSignal::Traverse - } else { - TraversalSignal::Skip - } - } - - fn visit_stmt(&mut self, stmt: &Stmt) { - let node = AnyNodeRef::from(stmt); - - if !self.enter_node(node).is_traverse() { - return; - } - - match stmt { - Stmt::Assign(assign) => { - self.in_assignment = true; - for target in &assign.targets { - self.visit_expr(target); - } - self.in_assignment = false; - - return; - } - // TODO - Stmt::FunctionDef(_) => {} - Stmt::For(_) => {} - Stmt::Expr(_) => { - // Don't traverse into expression statements because we don't show any hints. - return; - } - _ => {} - } - - source_order::walk_stmt(self, stmt); - } - - fn visit_expr(&mut self, expr: &'_ Expr) { - if !self.in_assignment { - return; - } - - match expr { - Expr::Name(name) => { - if name.ctx.is_store() { - let ty = expr.inferred_type(&self.model); - self.add_type_hint(expr.range().end(), ty); - } - } - _ => { - source_order::walk_expr(self, expr); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use insta::assert_snapshot; - use ruff_db::{ - files::{system_path_to_file, File}, - source::source_text, - }; - use ruff_text_size::TextSize; - - use crate::db::tests::TestDb; - - use red_knot_python_semantic::{ - Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, - }; - use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; - use ruff_python_ast::PythonVersion; - - pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest { - const START: &str = ""; - const END: &str = ""; - - let mut db = TestDb::new(); - - let start = source.find(START); - let end = source - .find(END) - .map(|x| if start.is_some() { x - START.len() } else { x }) - .unwrap_or(source.len()); - - let range = TextRange::new( - TextSize::try_from(start.unwrap_or_default()).unwrap(), - TextSize::try_from(end).unwrap(), - ); - - let source = source.replace(START, ""); - let source = source.replace(END, ""); - - db.write_file("main.py", source) - .expect("write to memory file system to be successful"); - - let file = system_path_to_file(&db, "main.py").expect("newly written file to existing"); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::latest(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![SystemPathBuf::from("/")], - custom_typeshed: None, - python_path: PythonPath::KnownSitePackages(vec![]), - }, - }, - ) - .expect("Default settings to be valid"); - - InlayHintTest { db, file, range } - } - - pub(super) struct InlayHintTest { - pub(super) db: TestDb, - pub(super) file: File, - pub(super) range: TextRange, - } - - impl InlayHintTest { - fn inlay_hints(&self) -> String { - let hints = inlay_hints(&self.db, self.file, self.range); - - let mut buf = source_text(&self.db, self.file).as_str().to_string(); - - let mut offset = 0; - - for hint in hints { - let end_position = (hint.position.to_u32() as usize) + offset; - let hint_str = format!("[{}]", hint.display(&self.db)); - buf.insert_str(end_position, &hint_str); - offset += hint_str.len(); - } - - buf - } - } - - #[test] - fn test_assign_statement() { - let test = inlay_hint_test("x = 1"); - - assert_snapshot!(test.inlay_hints(), @r" - x[: Literal[1]] = 1 - "); - } - - #[test] - fn test_tuple_assignment() { - let test = inlay_hint_test("x, y = (1, 'abc')"); - - assert_snapshot!(test.inlay_hints(), @r#" - x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc') - "#); - } - - #[test] - fn test_nested_tuple_assignment() { - let test = inlay_hint_test("x, (y, z) = (1, ('abc', 2))"); - - assert_snapshot!(test.inlay_hints(), @r#" - x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2)) - "#); - } - - #[test] - fn test_assign_statement_with_type_annotation() { - let test = inlay_hint_test("x: int = 1"); - - assert_snapshot!(test.inlay_hints(), @r" - x: int = 1 - "); - } - - #[test] - fn test_assign_statement_out_of_range() { - let test = inlay_hint_test("x = 1\ny = 2"); - - assert_snapshot!(test.inlay_hints(), @r" - x[: Literal[1]] = 1 - y = 2 - "); - } -} diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs deleted file mode 100644 index 48f1145894e30..0000000000000 --- a/crates/red_knot_ide/src/lib.rs +++ /dev/null @@ -1,296 +0,0 @@ -mod db; -mod find_node; -mod goto; -mod hover; -mod inlay_hints; -mod markup; - -pub use db::Db; -pub use goto::goto_type_definition; -pub use hover::hover; -pub use inlay_hints::inlay_hints; -pub use markup::MarkupKind; - -use rustc_hash::FxHashSet; -use std::ops::{Deref, DerefMut}; - -use red_knot_python_semantic::types::{Type, TypeDefinition}; -use ruff_db::files::{File, FileRange}; -use ruff_text_size::{Ranged, TextRange}; - -/// Information associated with a text range. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct RangedValue { - pub range: FileRange, - pub value: T, -} - -impl RangedValue { - pub fn file_range(&self) -> FileRange { - self.range - } -} - -impl Deref for RangedValue { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.value - } -} - -impl DerefMut for RangedValue { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.value - } -} - -impl IntoIterator for RangedValue -where - T: IntoIterator, -{ - type Item = T::Item; - type IntoIter = T::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.value.into_iter() - } -} - -/// Target to which the editor can navigate to. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct NavigationTarget { - file: File, - - /// The range that should be focused when navigating to the target. - /// - /// This is typically not the full range of the node. For example, it's the range of the class's name in a class definition. - /// - /// The `focus_range` must be fully covered by `full_range`. - focus_range: TextRange, - - /// The range covering the entire target. - full_range: TextRange, -} - -impl NavigationTarget { - pub fn file(&self) -> File { - self.file - } - - pub fn focus_range(&self) -> TextRange { - self.focus_range - } - - pub fn full_range(&self) -> TextRange { - self.full_range - } -} - -#[derive(Debug, Clone)] -pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>); - -impl NavigationTargets { - fn single(target: NavigationTarget) -> Self { - Self(smallvec::smallvec![target]) - } - - fn empty() -> Self { - Self(smallvec::SmallVec::new()) - } - - fn unique(targets: impl IntoIterator) -> Self { - let unique: FxHashSet<_> = targets.into_iter().collect(); - if unique.is_empty() { - Self::empty() - } else { - let mut targets = unique.into_iter().collect::>(); - targets.sort_by_key(|target| (target.file, target.focus_range.start())); - Self(targets.into()) - } - } - - fn iter(&self) -> std::slice::Iter<'_, NavigationTarget> { - self.0.iter() - } - - #[cfg(test)] - fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl IntoIterator for NavigationTargets { - type Item = NavigationTarget; - type IntoIter = smallvec::IntoIter<[NavigationTarget; 1]>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a> IntoIterator for &'a NavigationTargets { - type Item = &'a NavigationTarget; - type IntoIter = std::slice::Iter<'a, NavigationTarget>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl FromIterator for NavigationTargets { - fn from_iter>(iter: T) -> Self { - Self::unique(iter) - } -} - -pub trait HasNavigationTargets { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets; -} - -impl HasNavigationTargets for Type<'_> { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { - match self { - Type::Union(union) => union - .iter(db.upcast()) - .flat_map(|target| target.navigation_targets(db)) - .collect(), - - Type::Intersection(intersection) => { - // Only consider the positive elements because the negative elements are mainly from narrowing constraints. - let mut targets = intersection - .iter_positive(db.upcast()) - .filter(|ty| !ty.is_unknown()); - - let Some(first) = targets.next() else { - return NavigationTargets::empty(); - }; - - match targets.next() { - Some(_) => { - // If there are multiple types in the intersection, we can't navigate to a single one - // because the type is the intersection of all those types. - NavigationTargets::empty() - } - None => first.navigation_targets(db), - } - } - - ty => ty - .definition(db.upcast()) - .map(|definition| definition.navigation_targets(db)) - .unwrap_or_else(NavigationTargets::empty), - } - } -} - -impl HasNavigationTargets for TypeDefinition<'_> { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { - let full_range = self.full_range(db.upcast()); - NavigationTargets::single(NavigationTarget { - file: full_range.file(), - focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(), - full_range: full_range.range(), - }) - } -} - -#[cfg(test)] -mod tests { - use crate::db::tests::TestDb; - use insta::internals::SettingsBindDropGuard; - use red_knot_python_semantic::{ - Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, - }; - use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig}; - use ruff_db::files::{system_path_to_file, File}; - use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; - use ruff_python_ast::PythonVersion; - use ruff_text_size::TextSize; - - pub(super) fn cursor_test(source: &str) -> CursorTest { - let mut db = TestDb::new(); - let cursor_offset = source.find("").expect( - "`source`` should contain a `` marker, indicating the position of the cursor.", - ); - - let mut content = source[..cursor_offset].to_string(); - content.push_str(&source[cursor_offset + "".len()..]); - - db.write_file("main.py", &content) - .expect("write to memory file system to be successful"); - - let file = system_path_to_file(&db, "main.py").expect("newly written file to existing"); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::latest(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![SystemPathBuf::from("/")], - custom_typeshed: None, - python_path: PythonPath::KnownSitePackages(vec![]), - }, - }, - ) - .expect("Default settings to be valid"); - - let mut insta_settings = insta::Settings::clone_current(); - insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); - // Filter out TODO types because they are different between debug and release builds. - insta_settings.add_filter(r"@Todo\(.+\)", "@Todo"); - - let insta_settings_guard = insta_settings.bind_to_scope(); - - CursorTest { - db, - cursor_offset: TextSize::try_from(cursor_offset) - .expect("source to be smaller than 4GB"), - file, - _insta_settings_guard: insta_settings_guard, - } - } - - pub(super) struct CursorTest { - pub(super) db: TestDb, - pub(super) cursor_offset: TextSize, - pub(super) file: File, - _insta_settings_guard: SettingsBindDropGuard, - } - - impl CursorTest { - pub(super) fn write_file( - &mut self, - path: impl AsRef, - content: &str, - ) -> std::io::Result<()> { - self.db.write_file(path, content) - } - - pub(super) fn render_diagnostics(&self, diagnostics: I) -> String - where - I: IntoIterator, - D: IntoDiagnostic, - { - use std::fmt::Write; - - let mut buf = String::new(); - - let config = DisplayDiagnosticConfig::default() - .color(false) - .format(DiagnosticFormat::Full); - for diagnostic in diagnostics { - let diag = diagnostic.into_diagnostic(); - write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); - } - - buf - } - } - - pub(super) trait IntoDiagnostic { - fn into_diagnostic(self) -> Diagnostic; - } -} diff --git a/crates/red_knot_project/Cargo.toml b/crates/red_knot_project/Cargo.toml deleted file mode 100644 index f1659c9f33dbd..0000000000000 --- a/crates/red_knot_project/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "red_knot_project" -version = "0.0.0" -edition.workspace = true -rust-version.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true -authors.workspace = true -license.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -ruff_cache = { workspace = true } -ruff_db = { workspace = true, features = ["cache", "serde"] } -ruff_macros = { workspace = true } -ruff_python_ast = { workspace = true, features = ["serde"] } -ruff_python_formatter = { workspace = true, optional = true } -ruff_text_size = { workspace = true } -red_knot_ide = { workspace = true } -red_knot_python_semantic = { workspace = true, features = ["serde"] } -red_knot_vendored = { workspace = true } - -anyhow = { workspace = true } -crossbeam = { workspace = true } -glob = { workspace = true } -notify = { workspace = true } -pep440_rs = { workspace = true, features = ["version-ranges"] } -rayon = { workspace = true } -rustc-hash = { workspace = true } -salsa = { workspace = true } -schemars = { workspace = true, optional = true } -serde = { workspace = true } -thiserror = { workspace = true } -toml = { workspace = true } -tracing = { workspace = true } - -[dev-dependencies] -ruff_db = { workspace = true, features = ["testing"] } -glob = { workspace = true } -insta = { workspace = true, features = ["redactions", "ron"] } - -[features] -default = ["zstd"] -deflate = ["red_knot_vendored/deflate"] -schemars = [ - "dep:schemars", - "ruff_db/schemars", - "red_knot_python_semantic/schemars", -] -zstd = ["red_knot_vendored/zstd"] -format = ["ruff_python_formatter"] - -[lints] -workspace = true diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_invalid_target.py b/crates/red_knot_project/resources/test/corpus/04_assign_invalid_target.py deleted file mode 120000 index 8d551b9f78d09..0000000000000 --- a/crates/red_knot_project/resources/test/corpus/04_assign_invalid_target.py +++ /dev/null @@ -1 +0,0 @@ -../../../../ruff_python_parser/resources/invalid/statements/invalid_assignment_targets.py \ No newline at end of file diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_named_expr_invalid_target.py b/crates/red_knot_project/resources/test/corpus/04_assign_named_expr_invalid_target.py deleted file mode 120000 index c751f9658700a..0000000000000 --- a/crates/red_knot_project/resources/test/corpus/04_assign_named_expr_invalid_target.py +++ /dev/null @@ -1 +0,0 @@ -../../../../ruff_python_parser/resources/invalid/expressions/named/invalid_target.py \ No newline at end of file diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign_invalid_target.py b/crates/red_knot_project/resources/test/corpus/04_aug_assign_invalid_target.py deleted file mode 120000 index f3ebd5de4abc3..0000000000000 --- a/crates/red_knot_project/resources/test/corpus/04_aug_assign_invalid_target.py +++ /dev/null @@ -1 +0,0 @@ -../../../../ruff_python_parser/resources/invalid/statements/invalid_augmented_assignment_target.py \ No newline at end of file diff --git a/crates/red_knot_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb b/crates/red_knot_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb deleted file mode 120000 index 82df885aaa183..0000000000000 --- a/crates/red_knot_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb +++ /dev/null @@ -1 +0,0 @@ -../../../../ruff_notebook/resources/test/fixtures/jupyter/unused_variable.ipynb \ No newline at end of file diff --git a/crates/red_knot_project/resources/test/corpus/89_type_alias_invalid_bound.py b/crates/red_knot_project/resources/test/corpus/89_type_alias_invalid_bound.py deleted file mode 120000 index 5dfcadebb0d17..0000000000000 --- a/crates/red_knot_project/resources/test/corpus/89_type_alias_invalid_bound.py +++ /dev/null @@ -1 +0,0 @@ -../../../../ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py \ No newline at end of file diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_invalid_target.py b/crates/red_knot_project/resources/test/corpus/98_ann_assign_invalid_target.py deleted file mode 120000 index ebc265cfa2500..0000000000000 --- a/crates/red_knot_project/resources/test/corpus/98_ann_assign_invalid_target.py +++ /dev/null @@ -1 +0,0 @@ -../../../../ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_target.py \ No newline at end of file diff --git a/crates/red_knot_project/src/db.rs b/crates/red_knot_project/src/db.rs deleted file mode 100644 index e1d19983e00ce..0000000000000 --- a/crates/red_knot_project/src/db.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::panic::RefUnwindSafe; -use std::sync::Arc; - -use crate::DEFAULT_LINT_REGISTRY; -use crate::{Project, ProjectMetadata}; -use red_knot_ide::Db as IdeDb; -use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; -use red_knot_python_semantic::{Db as SemanticDb, Program}; -use ruff_db::diagnostic::Diagnostic; -use ruff_db::files::{File, Files}; -use ruff_db::system::System; -use ruff_db::vendored::VendoredFileSystem; -use ruff_db::{Db as SourceDb, Upcast}; -use salsa::plumbing::ZalsaDatabase; -use salsa::{Cancelled, Event}; - -mod changes; - -#[salsa::db] -pub trait Db: SemanticDb + Upcast { - fn project(&self) -> Project; -} - -#[salsa::db] -#[derive(Clone)] -pub struct ProjectDatabase { - project: Option, - storage: salsa::Storage, - files: Files, - system: Arc, -} - -impl ProjectDatabase { - pub fn new(project_metadata: ProjectMetadata, system: S) -> anyhow::Result - where - S: System + 'static + Send + Sync + RefUnwindSafe, - { - let mut db = Self { - project: None, - storage: salsa::Storage::default(), - files: Files::default(), - system: Arc::new(system), - }; - - // TODO: Use the `program_settings` to compute the key for the database's persistent - // cache and load the cache if it exists. - // we may want to have a dedicated method for this? - - // Initialize the `Program` singleton - let program_settings = project_metadata.to_program_settings(db.system()); - Program::from_settings(&db, program_settings)?; - - db.project = Some(Project::from_metadata(&db, project_metadata)); - - Ok(db) - } - - /// Checks all open files in the project and its dependencies. - pub fn check(&self) -> Result, Cancelled> { - self.with_db(|db| db.project().check(db)) - } - - #[tracing::instrument(level = "debug", skip(self))] - pub fn check_file(&self, file: File) -> Result, Cancelled> { - self.with_db(|db| self.project().check_file(db, file)) - } - - /// Returns a mutable reference to the system. - /// - /// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock. - pub fn system_mut(&mut self) -> &mut dyn System { - // TODO: Use a more official method to cancel other queries. - // https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries - let _ = self.zalsa_mut(); - - Arc::get_mut(&mut self.system).unwrap() - } - - pub(crate) fn with_db(&self, f: F) -> Result - where - F: FnOnce(&ProjectDatabase) -> T + std::panic::UnwindSafe, - { - Cancelled::catch(|| f(self)) - } -} - -impl Upcast for ProjectDatabase { - fn upcast(&self) -> &(dyn SemanticDb + 'static) { - self - } - - fn upcast_mut(&mut self) -> &mut (dyn SemanticDb + 'static) { - self - } -} - -impl Upcast for ProjectDatabase { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } -} - -impl Upcast for ProjectDatabase { - fn upcast(&self) -> &(dyn IdeDb + 'static) { - self - } - - fn upcast_mut(&mut self) -> &mut (dyn IdeDb + 'static) { - self - } -} - -#[salsa::db] -impl IdeDb for ProjectDatabase {} - -#[salsa::db] -impl SemanticDb for ProjectDatabase { - fn is_file_open(&self, file: File) -> bool { - let Some(project) = &self.project else { - return false; - }; - - project.is_file_open(self, file) - } - - fn rule_selection(&self) -> Arc { - self.project().rules(self) - } - - fn lint_registry(&self) -> &LintRegistry { - &DEFAULT_LINT_REGISTRY - } -} - -#[salsa::db] -impl SourceDb for ProjectDatabase { - fn vendored(&self) -> &VendoredFileSystem { - red_knot_vendored::file_system() - } - - fn system(&self) -> &dyn System { - &*self.system - } - - fn files(&self) -> &Files { - &self.files - } -} - -#[salsa::db] -impl salsa::Database for ProjectDatabase { - fn salsa_event(&self, event: &dyn Fn() -> Event) { - if !tracing::enabled!(tracing::Level::TRACE) { - return; - } - - let event = event(); - if matches!(event.kind, salsa::EventKind::WillCheckCancellation) { - return; - } - - tracing::trace!("Salsa event: {event:?}"); - } -} - -#[salsa::db] -impl Db for ProjectDatabase { - fn project(&self) -> Project { - self.project.unwrap() - } -} - -#[cfg(feature = "format")] -mod format { - use crate::ProjectDatabase; - use ruff_db::files::File; - use ruff_db::Upcast; - use ruff_python_formatter::{Db as FormatDb, PyFormatOptions}; - - #[salsa::db] - impl FormatDb for ProjectDatabase { - fn format_options(&self, file: File) -> PyFormatOptions { - let source_ty = file.source_type(self); - PyFormatOptions::from_source_type(source_ty) - } - } - - impl Upcast for ProjectDatabase { - fn upcast(&self) -> &(dyn FormatDb + 'static) { - self - } - - fn upcast_mut(&mut self) -> &mut (dyn FormatDb + 'static) { - self - } - } -} - -#[cfg(test)] -pub(crate) mod tests { - use std::sync::Arc; - - use salsa::Event; - - use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; - use red_knot_python_semantic::Db as SemanticDb; - use ruff_db::files::Files; - use ruff_db::system::{DbWithTestSystem, System, TestSystem}; - use ruff_db::vendored::VendoredFileSystem; - use ruff_db::{Db as SourceDb, Upcast}; - - use crate::db::Db; - use crate::DEFAULT_LINT_REGISTRY; - use crate::{Project, ProjectMetadata}; - - #[salsa::db] - #[derive(Clone)] - pub(crate) struct TestDb { - storage: salsa::Storage, - events: Arc>>, - files: Files, - system: TestSystem, - vendored: VendoredFileSystem, - project: Option, - } - - impl TestDb { - pub(crate) fn new(project: ProjectMetadata) -> Self { - let mut db = Self { - storage: salsa::Storage::default(), - system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), - files: Files::default(), - events: Arc::default(), - project: None, - }; - - let project = Project::from_metadata(&db, project); - db.project = Some(project); - db - } - } - - impl TestDb { - /// Takes the salsa events. - /// - /// ## Panics - /// If there are any pending salsa snapshots. - pub(crate) fn take_salsa_events(&mut self) -> Vec { - let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots"); - - let events = inner.get_mut().unwrap(); - std::mem::take(&mut *events) - } - } - - impl DbWithTestSystem for TestDb { - fn test_system(&self) -> &TestSystem { - &self.system - } - - fn test_system_mut(&mut self) -> &mut TestSystem { - &mut self.system - } - } - - #[salsa::db] - impl SourceDb for TestDb { - fn vendored(&self) -> &VendoredFileSystem { - &self.vendored - } - - fn system(&self) -> &dyn System { - &self.system - } - - fn files(&self) -> &Files { - &self.files - } - } - - impl Upcast for TestDb { - fn upcast(&self) -> &(dyn SemanticDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SemanticDb + 'static) { - self - } - } - - impl Upcast for TestDb { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } - } - - #[salsa::db] - impl red_knot_python_semantic::Db for TestDb { - fn is_file_open(&self, file: ruff_db::files::File) -> bool { - !file.path(self).is_vendored_path() - } - - fn rule_selection(&self) -> Arc { - self.project().rules(self) - } - - fn lint_registry(&self) -> &LintRegistry { - &DEFAULT_LINT_REGISTRY - } - } - - #[salsa::db] - impl Db for TestDb { - fn project(&self) -> Project { - self.project.unwrap() - } - } - - #[salsa::db] - impl salsa::Database for TestDb { - fn salsa_event(&self, event: &dyn Fn() -> Event) { - let mut events = self.events.lock().unwrap(); - events.push(event()); - } - } -} diff --git a/crates/red_knot_project/src/db/changes.rs b/crates/red_knot_project/src/db/changes.rs deleted file mode 100644 index 4ba90af10fce6..0000000000000 --- a/crates/red_knot_project/src/db/changes.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::db::{Db, ProjectDatabase}; -use crate::metadata::options::Options; -use crate::watch::{ChangeEvent, CreatedKind, DeletedKind}; -use crate::{Project, ProjectMetadata}; -use std::collections::BTreeSet; - -use crate::walk::ProjectFilesWalker; -use red_knot_python_semantic::Program; -use ruff_db::files::{File, Files}; -use ruff_db::system::SystemPath; -use ruff_db::Db as _; -use rustc_hash::FxHashSet; - -impl ProjectDatabase { - #[tracing::instrument(level = "debug", skip(self, changes, cli_options))] - pub fn apply_changes(&mut self, changes: Vec, cli_options: Option<&Options>) { - let mut project = self.project(); - let project_root = project.root(self).to_path_buf(); - let program = Program::get(self); - let custom_stdlib_versions_path = program - .custom_stdlib_search_path(self) - .map(|path| path.join("VERSIONS")); - - // Are there structural changes to the project - let mut project_changed = false; - // Changes to a custom stdlib path's VERSIONS - let mut custom_stdlib_change = false; - // Paths that were added - let mut added_paths = FxHashSet::default(); - - // Deduplicate the `sync` calls. Many file watchers emit multiple events for the same path. - let mut synced_files = FxHashSet::default(); - let mut sync_recursively = BTreeSet::default(); - - let mut sync_path = |db: &mut ProjectDatabase, path: &SystemPath| { - if synced_files.insert(path.to_path_buf()) { - File::sync_path(db, path); - } - }; - - for change in changes { - tracing::trace!("Handle change: {:?}", change); - - if let Some(path) = change.system_path() { - if matches!( - path.file_name(), - Some(".gitignore" | ".ignore" | "knot.toml" | "pyproject.toml") - ) { - // Changes to ignore files or settings can change the project structure or add/remove files. - project_changed = true; - - continue; - } - - if Some(path) == custom_stdlib_versions_path.as_deref() { - custom_stdlib_change = true; - } - } - - match change { - ChangeEvent::Changed { path, kind: _ } | ChangeEvent::Opened(path) => { - sync_path(self, &path); - } - - ChangeEvent::Created { kind, path } => { - match kind { - CreatedKind::File => sync_path(self, &path), - CreatedKind::Directory | CreatedKind::Any => { - sync_recursively.insert(path.clone()); - } - } - - // Unlike other files, it's not only important to update the status of existing - // and known `File`s (`sync_recursively`), it's also important to discover new files - // that were added in the project's root (or any of the paths included for checking). - // - // This is important because `Project::check` iterates over all included files. - // The code below walks the `added_paths` and adds all files that - // should be included in the project. We can skip this check for - // paths that aren't part of the project or shouldn't be included - // when checking the project. - if project.is_path_included(self, &path) { - if self.system().is_file(&path) { - // Add the parent directory because `walkdir` always visits explicitly passed files - // even if they match an exclude filter. - added_paths.insert(path.parent().unwrap().to_path_buf()); - } else { - added_paths.insert(path); - } - } - } - - ChangeEvent::Deleted { kind, path } => { - let is_file = match kind { - DeletedKind::File => true, - DeletedKind::Directory => { - // file watchers emit an event for every deleted file. No need to scan the entire dir. - continue; - } - DeletedKind::Any => self - .files - .try_system(self, &path) - .is_some_and(|file| file.exists(self)), - }; - - if is_file { - sync_path(self, &path); - - if let Some(file) = self.files().try_system(self, &path) { - project.remove_file(self, file); - } - } else { - sync_recursively.insert(path.clone()); - - if custom_stdlib_versions_path - .as_ref() - .is_some_and(|versions_path| versions_path.starts_with(&path)) - { - custom_stdlib_change = true; - } - - if project.is_path_included(self, &path) || path == project_root { - // TODO: Shouldn't it be enough to simply traverse the project files and remove all - // that start with the given path? - tracing::debug!( - "Reload project because of a path that could have been a directory." - ); - - // Perform a full-reload in case the deleted directory contained the pyproject.toml. - // We may want to make this more clever in the future, to e.g. iterate over the - // indexed files and remove the once that start with the same path, unless - // the deleted path is the project configuration. - project_changed = true; - } - } - } - - ChangeEvent::CreatedVirtual(path) | ChangeEvent::ChangedVirtual(path) => { - File::sync_virtual_path(self, &path); - } - - ChangeEvent::DeletedVirtual(path) => { - if let Some(virtual_file) = self.files().try_virtual_file(&path) { - virtual_file.close(self); - } - } - - ChangeEvent::Rescan => { - project_changed = true; - Files::sync_all(self); - sync_recursively.clear(); - break; - } - } - } - - let sync_recursively = sync_recursively.into_iter(); - let mut last = None; - - for path in sync_recursively { - // Avoid re-syncing paths that are sub-paths of each other. - if let Some(last) = &last { - if path.starts_with(last) { - continue; - } - } - - Files::sync_recursively(self, &path); - last = Some(path); - } - - if project_changed { - match ProjectMetadata::discover(&project_root, self.system()) { - Ok(mut metadata) => { - if let Some(cli_options) = cli_options { - metadata.apply_cli_options(cli_options.clone()); - } - - if let Err(error) = metadata.apply_configuration_files(self.system()) { - tracing::error!( - "Failed to apply configuration files, continuing without applying them: {error}" - ); - } - - let program_settings = metadata.to_program_settings(self.system()); - - let program = Program::get(self); - if let Err(error) = program.update_from_settings(self, program_settings) { - tracing::error!("Failed to update the program settings, keeping the old program settings: {error}"); - } - - if metadata.root() == project.root(self) { - tracing::debug!("Reloading project after structural change"); - project.reload(self, metadata); - } else { - tracing::debug!("Replace project after structural change"); - project = Project::from_metadata(self, metadata); - self.project = Some(project); - } - } - Err(error) => { - tracing::error!( - "Failed to load project, keeping old project configuration: {error}" - ); - } - } - - return; - } else if custom_stdlib_change { - let search_paths = project - .metadata(self) - .to_program_settings(self.system()) - .search_paths; - - if let Err(error) = program.update_search_paths(self, &search_paths) { - tracing::error!("Failed to set the new search paths: {error}"); - } - } - - let diagnostics = if let Some(walker) = ProjectFilesWalker::incremental(self, added_paths) { - // Use directory walking to discover newly added files. - let (files, diagnostics) = walker.collect_vec(self); - - for file in files { - project.add_file(self, file); - } - - diagnostics - } else { - Vec::new() - }; - - // Note: We simply replace all IO related diagnostics here. This isn't ideal, because - // it removes IO errors that may still be relevant. However, tracking IO errors correctly - // across revisions doesn't feel essential, considering that they're rare. However, we could - // implement a `BTreeMap` or similar and only prune the diagnostics from paths that we've - // re-scanned (or that were removed etc). - project.replace_index_diagnostics(self, diagnostics); - } -} diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs deleted file mode 100644 index a0a7c531d70d8..0000000000000 --- a/crates/red_knot_project/src/lib.rs +++ /dev/null @@ -1,569 +0,0 @@ -#![allow(clippy::ref_option)] - -use crate::metadata::options::OptionDiagnostic; -use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; -pub use db::{Db, ProjectDatabase}; -use files::{Index, Indexed, IndexedFiles}; -use metadata::settings::Settings; -pub use metadata::{ProjectDiscoveryError, ProjectMetadata}; -use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection}; -use red_knot_python_semantic::register_lints; -use red_knot_python_semantic::types::check_types; -use ruff_db::diagnostic::{ - create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span, -}; -use ruff_db::files::File; -use ruff_db::parsed::parsed_module; -use ruff_db::source::{source_text, SourceTextError}; -use ruff_db::system::{SystemPath, SystemPathBuf}; -use rustc_hash::FxHashSet; -use salsa::Durability; -use salsa::Setter; -use std::sync::Arc; -use thiserror::Error; - -pub mod combine; - -mod db; -mod files; -pub mod metadata; -mod walk; -pub mod watch; - -pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock = - std::sync::LazyLock::new(default_lints_registry); - -pub fn default_lints_registry() -> LintRegistry { - let mut builder = LintRegistryBuilder::default(); - register_lints(&mut builder); - builder.build() -} - -/// The project as a Salsa ingredient. -/// -/// ## How is a project different from a program? -/// There are two (related) motivations: -/// -/// 1. Program is defined in `ruff_db` and it can't reference the settings types for the linter and formatter -/// without introducing a cyclic dependency. The project is defined in a higher level crate -/// where it can reference these setting types. -/// 2. Running `ruff check` with different target versions results in different programs (settings) but -/// it remains the same project. That's why program is a narrowed view of the project only -/// holding on to the most fundamental settings required for checking. -#[salsa::input] -pub struct Project { - /// The files that are open in the project. - /// - /// Setting the open files to a non-`None` value changes `check` to only check the - /// open files rather than all files in the project. - #[return_ref] - #[default] - open_fileset: Option>>, - - /// The first-party files of this project. - #[default] - #[return_ref] - file_set: IndexedFiles, - - /// The metadata describing the project, including the unresolved options. - #[return_ref] - pub metadata: ProjectMetadata, - - /// The resolved project settings. - #[return_ref] - pub settings: Settings, - - /// The paths that should be included when checking this project. - /// - /// The default (when this list is empty) is to include all files in the project root - /// (that satisfy the configured include and exclude patterns). - /// However, it's sometimes desired to only check a subset of the project, e.g. to see - /// the diagnostics for a single file or a folder. - /// - /// This list gets initialized by the paths passed to `knot check ` - /// - /// ## How is this different from `open_files`? - /// - /// The `included_paths` is closely related to `open_files`. The only difference is that - /// `open_files` is already a resolved set of files whereas `included_paths` is only a list of paths - /// that are resolved to files by indexing them. The other difference is that - /// new files added to any directory in `included_paths` will be indexed and added to the project - /// whereas `open_files` needs to be updated manually (e.g. by the IDE). - /// - /// In short, `open_files` is cheaper in contexts where the set of files is known, like - /// in an IDE when the user only wants to check the open tabs. This could be modeled - /// with `included_paths` too but it would require an explicit walk dir step that's simply unnecessary. - #[default] - #[return_ref] - included_paths_list: Vec, - - /// Diagnostics that were generated when resolving the project settings. - #[return_ref] - settings_diagnostics: Vec, -} - -#[salsa::tracked] -impl Project { - pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self { - let (settings, settings_diagnostics) = metadata.options().to_settings(db); - - Project::builder(metadata, settings, settings_diagnostics) - .durability(Durability::MEDIUM) - .open_fileset_durability(Durability::LOW) - .file_set_durability(Durability::LOW) - .new(db) - } - - pub fn root(self, db: &dyn Db) -> &SystemPath { - self.metadata(db).root() - } - - pub fn name(self, db: &dyn Db) -> &str { - self.metadata(db).name() - } - - /// Returns the resolved linter rules for the project. - /// - /// This is a salsa query to prevent re-computing queries if other, unrelated - /// settings change. For example, we don't want that changing the terminal settings - /// invalidates any type checking queries. - #[salsa::tracked] - pub fn rules(self, db: &dyn Db) -> Arc { - self.settings(db).to_rules() - } - - /// Returns `true` if `path` is both part of the project and included (see `included_paths_list`). - /// - /// Unlike [Self::files], this method does not respect `.gitignore` files. It only checks - /// the project's include and exclude settings as well as the paths that were passed to `knot check `. - /// This means, that this method is an over-approximation of `Self::files` and may return `true` for paths - /// that won't be included when checking the project because they're ignored in a `.gitignore` file. - pub fn is_path_included(self, db: &dyn Db, path: &SystemPath) -> bool { - ProjectFilesFilter::from_project(db, self).is_included(path) - } - - pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) { - tracing::debug!("Reloading project"); - assert_eq!(self.root(db), metadata.root()); - - if &metadata != self.metadata(db) { - let (settings, settings_diagnostics) = metadata.options().to_settings(db); - - if self.settings(db) != &settings { - self.set_settings(db).to(settings); - } - - if self.settings_diagnostics(db) != &settings_diagnostics { - self.set_settings_diagnostics(db).to(settings_diagnostics); - } - - self.set_metadata(db).to(metadata); - } - - self.reload_files(db); - } - - /// Checks all open files in the project and its dependencies. - pub(crate) fn check(self, db: &ProjectDatabase) -> Vec { - let project_span = tracing::debug_span!("Project::check"); - let _span = project_span.enter(); - - tracing::debug!("Checking project '{name}'", name = self.name(db)); - - let mut diagnostics: Vec = Vec::new(); - diagnostics.extend( - self.settings_diagnostics(db) - .iter() - .map(OptionDiagnostic::to_diagnostic), - ); - - let files = ProjectFiles::new(db, self); - - diagnostics.extend( - files - .diagnostics() - .iter() - .map(IOErrorDiagnostic::to_diagnostic), - ); - - let result = Arc::new(std::sync::Mutex::new(diagnostics)); - let inner_result = Arc::clone(&result); - - let db = db.clone(); - let project_span = project_span.clone(); - - rayon::scope(move |scope| { - for file in &files { - let result = inner_result.clone(); - let db = db.clone(); - let project_span = project_span.clone(); - - scope.spawn(move |_| { - let check_file_span = - tracing::debug_span!(parent: &project_span, "check_file", ?file); - let _entered = check_file_span.entered(); - - let file_diagnostics = check_file_impl(&db, file); - result.lock().unwrap().extend(file_diagnostics); - }); - } - }); - - Arc::into_inner(result).unwrap().into_inner().unwrap() - } - - pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec { - let mut file_diagnostics: Vec<_> = self - .settings_diagnostics(db) - .iter() - .map(OptionDiagnostic::to_diagnostic) - .collect(); - - let check_diagnostics = check_file_impl(db, file); - file_diagnostics.extend(check_diagnostics); - - file_diagnostics - } - - /// Opens a file in the project. - /// - /// This changes the behavior of `check` to only check the open files rather than all files in the project. - pub fn open_file(self, db: &mut dyn Db, file: File) { - tracing::debug!("Opening file `{}`", file.path(db)); - - let mut open_files = self.take_open_files(db); - open_files.insert(file); - self.set_open_files(db, open_files); - } - - /// Closes a file in the project. - pub fn close_file(self, db: &mut dyn Db, file: File) -> bool { - tracing::debug!("Closing file `{}`", file.path(db)); - - let mut open_files = self.take_open_files(db); - let removed = open_files.remove(&file); - - if removed { - self.set_open_files(db, open_files); - } - - removed - } - - pub fn set_included_paths(self, db: &mut dyn Db, paths: Vec) { - tracing::debug!("Setting included paths: {paths}", paths = paths.len()); - - self.set_included_paths_list(db).to(paths); - self.reload_files(db); - } - - /// Returns the paths that should be checked. - /// - /// The default is to check the entire project in which case this method returns - /// the project root. However, users can specify to only check specific sub-folders or - /// even files of a project by using `knot check `. In that case, this method - /// returns the provided absolute paths. - /// - /// Note: The CLI doesn't prohibit users from specifying paths outside the project root. - /// This can be useful to check arbitrary files, but it isn't something we recommend. - /// We should try to support this use case but it's okay if there are some limitations around it. - fn included_paths_or_root(self, db: &dyn Db) -> &[SystemPathBuf] { - match &**self.included_paths_list(db) { - [] => std::slice::from_ref(&self.metadata(db).root), - paths => paths, - } - } - - /// Returns the open files in the project or `None` if the entire project should be checked. - pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet> { - self.open_fileset(db).as_deref() - } - - /// Sets the open files in the project. - /// - /// This changes the behavior of `check` to only check the open files rather than all files in the project. - #[tracing::instrument(level = "debug", skip(self, db))] - pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet) { - tracing::debug!("Set open project files (count: {})", open_files.len()); - - self.set_open_fileset(db).to(Some(Arc::new(open_files))); - } - - /// This takes the open files from the project and returns them. - /// - /// This changes the behavior of `check` to check all files in the project instead of just the open files. - fn take_open_files(self, db: &mut dyn Db) -> FxHashSet { - tracing::debug!("Take open project files"); - - // Salsa will cancel any pending queries and remove its own reference to `open_files` - // so that the reference counter to `open_files` now drops to 1. - let open_files = self.set_open_fileset(db).to(None); - - if let Some(open_files) = open_files { - Arc::try_unwrap(open_files).unwrap() - } else { - FxHashSet::default() - } - } - - /// Returns `true` if the file is open in the project. - /// - /// A file is considered open when: - /// * explicitly set as an open file using [`open_file`](Self::open_file) - /// * It has a [`SystemPath`] and belongs to a package's `src` files - /// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath) - pub fn is_file_open(self, db: &dyn Db, file: File) -> bool { - if let Some(open_files) = self.open_files(db) { - open_files.contains(&file) - } else if file.path(db).is_system_path() { - self.contains_file(db, file) - } else { - file.path(db).is_system_virtual_path() - } - } - - /// Returns `true` if `file` is a first-party file part of this package. - pub fn contains_file(self, db: &dyn Db, file: File) -> bool { - self.files(db).contains(&file) - } - - #[tracing::instrument(level = "debug", skip(self, db))] - pub fn remove_file(self, db: &mut dyn Db, file: File) { - tracing::debug!( - "Removing file `{}` from project `{}`", - file.path(db), - self.name(db) - ); - - let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { - return; - }; - - index.remove(file); - } - - pub fn add_file(self, db: &mut dyn Db, file: File) { - tracing::debug!( - "Adding file `{}` to project `{}`", - file.path(db), - self.name(db) - ); - - let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { - return; - }; - - index.insert(file); - } - - /// Replaces the diagnostics from indexing the project files with `diagnostics`. - /// - /// This is a no-op if the project files haven't been indexed yet. - pub fn replace_index_diagnostics(self, db: &mut dyn Db, diagnostics: Vec) { - let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { - return; - }; - - index.set_diagnostics(diagnostics); - } - - /// Returns the files belonging to this project. - pub fn files(self, db: &dyn Db) -> Indexed<'_> { - let files = self.file_set(db); - - let indexed = match files.get() { - Index::Lazy(vacant) => { - let _entered = - tracing::debug_span!("Project::index_files", project = %self.name(db)) - .entered(); - - let walker = ProjectFilesWalker::new(db); - let (files, diagnostics) = walker.collect_set(db); - - tracing::info!("Indexed {} file(s)", files.len()); - vacant.set(files, diagnostics) - } - Index::Indexed(indexed) => indexed, - }; - - indexed - } - - pub fn reload_files(self, db: &mut dyn Db) { - tracing::debug!("Reloading files for project `{}`", self.name(db)); - - if !self.file_set(db).is_lazy() { - // Force a re-index of the files in the next revision. - self.set_file_set(db).to(IndexedFiles::lazy()); - } - } -} - -fn check_file_impl(db: &dyn Db, file: File) -> Vec { - let mut diagnostics: Vec = Vec::new(); - - // Abort checking if there are IO errors. - let source = source_text(db.upcast(), file); - - if let Some(read_error) = source.read_error() { - diagnostics.push( - IOErrorDiagnostic { - file: Some(file), - error: read_error.clone().into(), - } - .to_diagnostic(), - ); - return diagnostics; - } - - let parsed = parsed_module(db.upcast(), file); - diagnostics.extend( - parsed - .errors() - .iter() - .map(|error| create_parse_diagnostic(file, error)), - ); - - diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned()); - - diagnostics.sort_unstable_by_key(|diagnostic| { - diagnostic - .primary_span() - .and_then(|span| span.range()) - .unwrap_or_default() - .start() - }); - - diagnostics -} - -#[derive(Debug)] -enum ProjectFiles<'a> { - OpenFiles(&'a FxHashSet), - Indexed(files::Indexed<'a>), -} - -impl<'a> ProjectFiles<'a> { - fn new(db: &'a dyn Db, project: Project) -> Self { - if let Some(open_files) = project.open_files(db) { - ProjectFiles::OpenFiles(open_files) - } else { - ProjectFiles::Indexed(project.files(db)) - } - } - - fn diagnostics(&self) -> &[IOErrorDiagnostic] { - match self { - ProjectFiles::OpenFiles(_) => &[], - ProjectFiles::Indexed(indexed) => indexed.diagnostics(), - } - } -} - -impl<'a> IntoIterator for &'a ProjectFiles<'a> { - type Item = File; - type IntoIter = ProjectFilesIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - match self { - ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()), - ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed { - files: indexed.into_iter(), - }, - } - } -} - -enum ProjectFilesIter<'db> { - OpenFiles(std::collections::hash_set::Iter<'db, File>), - Indexed { files: files::IndexedIter<'db> }, -} - -impl Iterator for ProjectFilesIter<'_> { - type Item = File; - - fn next(&mut self) -> Option { - match self { - ProjectFilesIter::OpenFiles(files) => files.next().copied(), - ProjectFilesIter::Indexed { files } => files.next(), - } - } -} - -#[derive(Debug, Clone)] -pub struct IOErrorDiagnostic { - file: Option, - error: IOErrorKind, -} - -impl IOErrorDiagnostic { - fn to_diagnostic(&self) -> Diagnostic { - let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error); - if let Some(file) = self.file { - diag.annotate(Annotation::primary(Span::from(file))); - } - diag - } -} - -#[derive(Error, Debug, Clone)] -enum IOErrorKind { - #[error(transparent)] - Walk(#[from] walk::WalkError), - - #[error(transparent)] - SourceText(#[from] SourceTextError), -} - -#[cfg(test)] -mod tests { - use crate::db::tests::TestDb; - use crate::{check_file_impl, ProjectMetadata}; - use red_knot_python_semantic::types::check_types; - use ruff_db::files::system_path_to_file; - use ruff_db::source::source_text; - use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf}; - use ruff_db::testing::assert_function_query_was_not_run; - use ruff_python_ast::name::Name; - - #[test] - fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { - let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/")); - let mut db = TestDb::new(project); - let path = SystemPath::new("test.py"); - - db.write_file(path, "x = 10")?; - let file = system_path_to_file(&db, path).unwrap(); - - // Now the file gets deleted before we had a chance to read its source text. - db.memory_file_system().remove_file(path)?; - file.sync(&mut db); - - assert_eq!(source_text(&db, file).as_str(), ""); - assert_eq!( - check_file_impl(&db, file) - .into_iter() - .map(|diagnostic| diagnostic.primary_message().to_string()) - .collect::>(), - vec!["Failed to read file: No such file or directory".to_string()] - ); - - let events = db.take_salsa_events(); - assert_function_query_was_not_run(&db, check_types, file, &events); - - // The user now creates a new file with an empty text. The source text - // content returned by `source_text` remains unchanged, but the diagnostics should get updated. - db.write_file(path, "").unwrap(); - - assert_eq!(source_text(&db, file).as_str(), ""); - assert_eq!( - check_file_impl(&db, file) - .into_iter() - .map(|diagnostic| diagnostic.primary_message().to_string()) - .collect::>(), - vec![] as Vec - ); - - Ok(()) - } -} diff --git a/crates/red_knot_project/src/metadata/configuration_file.rs b/crates/red_knot_project/src/metadata/configuration_file.rs deleted file mode 100644 index 03db373e36568..0000000000000 --- a/crates/red_knot_project/src/metadata/configuration_file.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::sync::Arc; - -use ruff_db::system::{System, SystemPath, SystemPathBuf}; -use thiserror::Error; - -use crate::metadata::value::ValueSource; - -use super::options::{KnotTomlError, Options}; - -/// A `knot.toml` configuration file with the options it contains. -pub(crate) struct ConfigurationFile { - path: SystemPathBuf, - options: Options, -} - -impl ConfigurationFile { - /// Loads the user-level configuration file if it exists. - /// - /// Returns `None` if the file does not exist or if the concept of user-level configurations - /// doesn't exist on `system`. - pub(crate) fn user(system: &dyn System) -> Result, ConfigurationFileError> { - let Some(configuration_directory) = system.user_config_directory() else { - return Ok(None); - }; - - let knot_toml_path = configuration_directory.join("knot").join("knot.toml"); - - tracing::debug!( - "Searching for a user-level configuration at `{path}`", - path = &knot_toml_path - ); - - let Ok(knot_toml_str) = system.read_to_string(&knot_toml_path) else { - return Ok(None); - }; - - match Options::from_toml_str( - &knot_toml_str, - ValueSource::File(Arc::new(knot_toml_path.clone())), - ) { - Ok(options) => Ok(Some(Self { - path: knot_toml_path, - options, - })), - Err(error) => Err(ConfigurationFileError::InvalidKnotToml { - source: Box::new(error), - path: knot_toml_path, - }), - } - } - - /// Returns the path to the configuration file. - pub(crate) fn path(&self) -> &SystemPath { - &self.path - } - - pub(crate) fn into_options(self) -> Options { - self.options - } -} - -#[derive(Debug, Error)] -pub enum ConfigurationFileError { - #[error("{path} is not a valid `knot.toml`: {source}")] - InvalidKnotToml { - source: Box, - path: SystemPathBuf, - }, -} diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs deleted file mode 100644 index d035f4cf7be5d..0000000000000 --- a/crates/red_knot_project/src/metadata/options.rs +++ /dev/null @@ -1,419 +0,0 @@ -use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard}; -use crate::Db; -use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; -use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings}; -use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span}; -use ruff_db::files::system_path_to_file; -use ruff_db::system::{System, SystemPath}; -use ruff_macros::Combine; -use ruff_python_ast::PythonVersion; -use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use thiserror::Error; - -use super::settings::{Settings, TerminalSettings}; - -/// The options for the project. -#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Options { - /// Configures the type checking environment. - #[serde(skip_serializing_if = "Option::is_none")] - pub environment: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub src: Option, - - /// Configures the enabled lints and their severity. - #[serde(skip_serializing_if = "Option::is_none")] - pub rules: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub terminal: Option, -} - -impl Options { - pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result { - 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 - where - D: serde::Deserializer<'de>, - { - let _guard = ValueSourceGuard::new(source, false); - Self::deserialize(deserializer) - } - - pub(crate) fn to_program_settings( - &self, - project_root: &SystemPath, - system: &dyn System, - ) -> ProgramSettings { - let python_version = self - .environment - .as_ref() - .and_then(|env| env.python_version.as_deref().copied()) - .unwrap_or_default(); - let python_platform = self - .environment - .as_ref() - .and_then(|env| env.python_platform.as_deref().cloned()) - .unwrap_or_else(|| { - let default = PythonPlatform::default(); - tracing::info!( - "Defaulting to default python version for this platform: '{default}'", - ); - default - }); - ProgramSettings { - python_version, - python_platform, - search_paths: self.to_search_path_settings(project_root, system), - } - } - - fn to_search_path_settings( - &self, - project_root: &SystemPath, - system: &dyn System, - ) -> SearchPathSettings { - let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref()) - { - vec![src_root.absolute(project_root, system)] - } else { - let src = project_root.join("src"); - - // Default to `src` and the project root if `src` exists and the root hasn't been specified. - if system.is_directory(&src) { - vec![project_root.to_path_buf(), src] - } else { - vec![project_root.to_path_buf()] - } - }; - - let (extra_paths, python, typeshed) = self - .environment - .as_ref() - .map(|env| { - ( - env.extra_paths.clone(), - env.python.clone(), - env.typeshed.clone(), - ) - }) - .unwrap_or_default(); - - SearchPathSettings { - extra_paths: extra_paths - .unwrap_or_default() - .into_iter() - .map(|path| path.absolute(project_root, system)) - .collect(), - src_roots, - custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)), - python_path: python - .map(|python_path| { - PythonPath::from_cli_flag(python_path.absolute(project_root, system)) - }) - .or_else(|| { - std::env::var("VIRTUAL_ENV") - .ok() - .map(PythonPath::from_virtual_env_var) - }) - .unwrap_or_else(|| PythonPath::Discover(project_root.to_path_buf())), - } - } - - #[must_use] - pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec) { - let (rules, diagnostics) = self.to_rule_selection(db); - - let mut settings = Settings::new(rules); - - if let Some(terminal) = self.terminal.as_ref() { - settings.set_terminal(TerminalSettings { - output_format: terminal - .output_format - .as_deref() - .copied() - .unwrap_or_default(), - error_on_warning: terminal.error_on_warning.unwrap_or_default(), - }); - } - - (settings, diagnostics) - } - - #[must_use] - fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec) { - let registry = db.lint_registry(); - let mut diagnostics = Vec::new(); - - // Initialize the selection with the defaults - let mut selection = RuleSelection::from_registry(registry); - - let rules = self - .rules - .as_ref() - .into_iter() - .flat_map(|rules| rules.inner.iter()); - - for (rule_name, level) in rules { - let source = rule_name.source(); - match registry.get(rule_name) { - Ok(lint) => { - let lint_source = match source { - ValueSource::File(_) => LintSource::File, - ValueSource::Cli => LintSource::Cli, - }; - if let Ok(severity) = Severity::try_from(**level) { - selection.enable(lint, severity, lint_source); - } else { - selection.disable(lint); - } - } - Err(error) => { - // `system_path_to_file` can return `Err` if the file was deleted since the configuration - // was read. This should be rare and it should be okay to default to not showing a configuration - // file in that case. - let file = source - .file() - .and_then(|path| system_path_to_file(db.upcast(), path).ok()); - - // TODO: Add a note if the value was configured on the CLI - let diagnostic = match error { - GetLintError::Unknown(_) => OptionDiagnostic::new( - DiagnosticId::UnknownRule, - format!("Unknown lint rule `{rule_name}`"), - Severity::Warning, - ), - GetLintError::PrefixedWithCategory { suggestion, .. } => { - OptionDiagnostic::new( - DiagnosticId::UnknownRule, - format!( - "Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?" - ), - Severity::Warning, - ) - } - - GetLintError::Removed(_) => OptionDiagnostic::new( - DiagnosticId::UnknownRule, - format!("Unknown lint rule `{rule_name}`"), - Severity::Warning, - ), - }; - - let span = file.map(Span::from).map(|span| { - if let Some(range) = rule_name.range() { - span.with_range(range) - } else { - span - } - }); - diagnostics.push(diagnostic.with_span(span)); - } - } - } - - (selection, diagnostics) - } -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct EnvironmentOptions { - /// Specifies the version of Python that will be used to analyze the source code. - /// The version should be specified as a string in the format `M.m` where `M` is the major version - /// and `m` is the minor (e.g. "3.0" or "3.6"). - /// If a version is provided, knot will generate errors if the source code makes use of language features - /// that are not supported in that version. - /// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version. - #[serde(skip_serializing_if = "Option::is_none")] - pub python_version: Option>, - - /// Specifies the target platform that will be used to analyze the source code. - /// If specified, Red Knot will tailor its use of type stub files, - /// which conditionalize type definitions based on the platform. - /// - /// If no platform is specified, knot will use the current platform: - /// - `win32` for Windows - /// - `darwin` for macOS - /// - `android` for Android - /// - `ios` for iOS - /// - `linux` for everything else - #[serde(skip_serializing_if = "Option::is_none")] - pub python_platform: Option>, - - /// List of user-provided paths that should take first priority in the module resolution. - /// Examples in other type checkers are mypy's MYPYPATH environment variable, - /// or pyright's stubPath configuration setting. - #[serde(skip_serializing_if = "Option::is_none")] - pub extra_paths: Option>, - - /// Optional path to a "typeshed" directory on disk for us to use for standard-library types. - /// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, - /// bundled as a zip file in the binary - #[serde(skip_serializing_if = "Option::is_none")] - pub typeshed: Option, - - /// Path to the Python installation from which Red Knot resolves type information and third-party dependencies. - /// - /// Red Knot will search in the path's `site-packages` directories for type information and - /// third-party imports. - /// - /// This option is commonly used to specify the path to a virtual environment. - #[serde(skip_serializing_if = "Option::is_none")] - pub python: Option, -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct SrcOptions { - /// The root of the project, used for finding first-party modules. - #[serde(skip_serializing_if = "Option::is_none")] - pub root: Option, -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", transparent)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Rules { - #[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))] - inner: FxHashMap, RangedValue>, -} - -impl FromIterator<(RangedValue, RangedValue)> for Rules { - fn from_iter, RangedValue)>>( - iter: T, - ) -> Self { - Self { - inner: iter.into_iter().collect(), - } - } -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TerminalOptions { - /// The format to use for printing diagnostic messages. - /// - /// Defaults to `full`. - #[serde(skip_serializing_if = "Option::is_none")] - pub output_format: Option>, - /// Use exit code 1 if there are any warning-level diagnostics. - /// - /// Defaults to `false`. - pub error_on_warning: Option, -} - -#[cfg(feature = "schemars")] -mod schema { - use crate::DEFAULT_LINT_REGISTRY; - use red_knot_python_semantic::lint::Level; - use schemars::gen::SchemaGenerator; - use schemars::schema::{ - InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation, - }; - use schemars::JsonSchema; - - pub(super) struct Rules; - - impl JsonSchema for Rules { - fn schema_name() -> String { - "Rules".to_string() - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let registry = &*DEFAULT_LINT_REGISTRY; - - let level_schema = gen.subschema_for::(); - - let properties: schemars::Map = registry - .lints() - .iter() - .map(|lint| { - ( - lint.name().to_string(), - Schema::Object(SchemaObject { - metadata: Some(Box::new(Metadata { - title: Some(lint.summary().to_string()), - description: Some(lint.documentation()), - deprecated: lint.status.is_deprecated(), - default: Some(lint.default_level.to_string().into()), - ..Metadata::default() - })), - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(vec![level_schema.clone()]), - ..Default::default() - })), - ..Default::default() - }), - ) - }) - .collect(); - - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(ObjectValidation { - properties, - // Allow unknown rules: Red Knot will warn about them. - // It gives a better experience when using an older Red Knot version because - // the schema will not deny rules that have been removed in newer versions. - additional_properties: Some(Box::new(level_schema)), - ..ObjectValidation::default() - })), - - ..Default::default() - }) - } - } -} - -#[derive(Error, Debug)] -pub enum KnotTomlError { - #[error(transparent)] - TomlSyntax(#[from] toml::de::Error), -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct OptionDiagnostic { - id: DiagnosticId, - message: String, - severity: Severity, - span: Option, -} - -impl OptionDiagnostic { - pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self { - Self { - id, - message, - severity, - span: None, - } - } - - #[must_use] - fn with_span(self, span: Option) -> Self { - OptionDiagnostic { span, ..self } - } - - pub(crate) fn to_diagnostic(&self) -> Diagnostic { - if let Some(ref span) = self.span { - let mut diag = Diagnostic::new(self.id, self.severity, ""); - diag.annotate(Annotation::primary(span.clone()).message(&self.message)); - diag - } else { - Diagnostic::new(self.id, self.severity, &self.message) - } - } -} diff --git a/crates/red_knot_project/src/metadata/settings.rs b/crates/red_knot_project/src/metadata/settings.rs deleted file mode 100644 index e16a72d4a030e..0000000000000 --- a/crates/red_knot_project/src/metadata/settings.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::sync::Arc; - -use red_knot_python_semantic::lint::RuleSelection; -use ruff_db::diagnostic::DiagnosticFormat; - -/// The resolved [`super::Options`] for the project. -/// -/// Unlike [`super::Options`], the struct has default values filled in and -/// uses representations that are optimized for reads (instead of preserving the source representation). -/// It's also not required that this structure precisely resembles the TOML schema, although -/// it's encouraged to use a similar structure. -/// -/// It's worth considering to adding a salsa query for specific settings to -/// limit the blast radius when only some settings change. For example, -/// changing the terminal settings shouldn't invalidate any core type-checking queries. -/// This can be achieved by adding a salsa query for the type checking specific settings. -/// -/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Settings { - rules: Arc, - - terminal: TerminalSettings, -} - -impl Settings { - pub fn new(rules: RuleSelection) -> Self { - Self { - rules: Arc::new(rules), - terminal: TerminalSettings::default(), - } - } - - pub fn rules(&self) -> &RuleSelection { - &self.rules - } - - pub fn to_rules(&self) -> Arc { - self.rules.clone() - } - - pub fn terminal(&self) -> &TerminalSettings { - &self.terminal - } - - pub fn set_terminal(&mut self, terminal: TerminalSettings) { - self.terminal = terminal; - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct TerminalSettings { - pub output_format: DiagnosticFormat, - pub error_on_warning: bool, -} diff --git a/crates/red_knot_project/src/walk.rs b/crates/red_knot_project/src/walk.rs deleted file mode 100644 index 531c823a28afe..0000000000000 --- a/crates/red_knot_project/src/walk.rs +++ /dev/null @@ -1,256 +0,0 @@ -use crate::{Db, IOErrorDiagnostic, IOErrorKind, Project}; -use ruff_db::files::{system_path_to_file, File}; -use ruff_db::system::walk_directory::{ErrorKind, WalkDirectoryBuilder, WalkState}; -use ruff_db::system::{FileType, SystemPath, SystemPathBuf}; -use ruff_python_ast::PySourceType; -use rustc_hash::{FxBuildHasher, FxHashSet}; -use std::path::PathBuf; -use thiserror::Error; - -/// Filter that decides which files are included in the project. -/// -/// In the future, this will hold a reference to the `include` and `exclude` pattern. -/// -/// This struct mainly exists because `dyn Db` isn't `Send` or `Sync`, making it impossible -/// to access fields from within the walker. -#[derive(Default, Debug)] -pub(crate) struct ProjectFilesFilter<'a> { - /// The same as [`Project::included_paths_or_root`]. - included_paths: &'a [SystemPathBuf], - - /// The filter skips checking if the path is in `included_paths` if set to `true`. - /// - /// Skipping this check is useful when the walker only walks over `included_paths`. - skip_included_paths: bool, -} - -impl<'a> ProjectFilesFilter<'a> { - pub(crate) fn from_project(db: &'a dyn Db, project: Project) -> Self { - Self { - included_paths: project.included_paths_or_root(db), - skip_included_paths: false, - } - } - - /// Returns `true` if a file is part of the project and included in the paths to check. - /// - /// A file is included in the checked files if it is a sub path of the project's root - /// (when no CLI path arguments are specified) or if it is a sub path of any path provided on the CLI (`knot check `) AND: - /// - /// * It matches a positive `include` pattern and isn't excluded by a later negative `include` pattern. - /// * It doesn't match a positive `exclude` pattern or is re-included by a later negative `exclude` pattern. - /// - /// ## Note - /// - /// This method may return `true` for files that don't end up being included when walking the - /// project tree because it doesn't consider `.gitignore` and other ignore files when deciding - /// if a file's included. - pub(crate) fn is_included(&self, path: &SystemPath) -> bool { - #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] - enum CheckPathMatch { - /// The path is a partial match of the checked path (it's a sub path) - Partial, - - /// The path matches a check path exactly. - Full, - } - - let m = if self.skip_included_paths { - Some(CheckPathMatch::Partial) - } else { - self.included_paths - .iter() - .filter_map(|included_path| { - if let Ok(relative_path) = path.strip_prefix(included_path) { - // Exact matches are always included - if relative_path.as_str().is_empty() { - Some(CheckPathMatch::Full) - } else { - Some(CheckPathMatch::Partial) - } - } else { - None - } - }) - .max() - }; - - match m { - None => false, - Some(CheckPathMatch::Partial) => { - // TODO: For partial matches, only include the file if it is included by the project's include/exclude settings. - true - } - Some(CheckPathMatch::Full) => true, - } - } -} - -pub(crate) struct ProjectFilesWalker<'a> { - walker: WalkDirectoryBuilder, - - filter: ProjectFilesFilter<'a>, -} - -impl<'a> ProjectFilesWalker<'a> { - pub(crate) fn new(db: &'a dyn Db) -> Self { - let project = db.project(); - - let mut filter = ProjectFilesFilter::from_project(db, project); - // It's unnecessary to filter on included paths because it only iterates over those to start with. - filter.skip_included_paths = true; - - Self::from_paths(db, project.included_paths_or_root(db), filter) - .expect("included_paths_or_root to never return an empty iterator") - } - - /// Creates a walker for indexing the project files incrementally. - /// - /// The main difference to a full project walk is that `paths` may contain paths - /// that aren't part of the included files. - pub(crate) fn incremental

(db: &'a dyn Db, paths: impl IntoIterator) -> Option - where - P: AsRef, - { - let project = db.project(); - - let filter = ProjectFilesFilter::from_project(db, project); - - Self::from_paths(db, paths, filter) - } - - fn from_paths

( - db: &'a dyn Db, - paths: impl IntoIterator, - filter: ProjectFilesFilter<'a>, - ) -> Option - where - P: AsRef, - { - let mut paths = paths.into_iter(); - - let mut walker = db.system().walk_directory(paths.next()?.as_ref()); - - for path in paths { - walker = walker.add(path); - } - - Some(Self { walker, filter }) - } - - /// Walks the project paths and collects the paths of all files that - /// are included in the project. - pub(crate) fn walk_paths(self) -> (Vec, Vec) { - let paths = std::sync::Mutex::new(Vec::new()); - let diagnostics = std::sync::Mutex::new(Vec::new()); - - self.walker.run(|| { - Box::new(|entry| { - match entry { - Ok(entry) => { - if !self.filter.is_included(entry.path()) { - tracing::debug!("Ignoring not-included path: {}", entry.path()); - return WalkState::Skip; - } - - // Skip over any non python files to avoid creating too many entries in `Files`. - match entry.file_type() { - FileType::File => { - if entry - .path() - .extension() - .and_then(PySourceType::try_from_extension) - .is_some() - { - let mut paths = paths.lock().unwrap(); - paths.push(entry.into_path()); - } - } - FileType::Directory | FileType::Symlink => {} - } - } - Err(error) => match error.kind() { - ErrorKind::Loop { .. } => { - unreachable!("Loops shouldn't be possible without following symlinks.") - } - ErrorKind::Io { path, err } => { - let mut diagnostics = diagnostics.lock().unwrap(); - let error = if let Some(path) = path { - WalkError::IOPathError { - path: path.clone(), - error: err.to_string(), - } - } else { - WalkError::IOError { - error: err.to_string(), - } - }; - - diagnostics.push(IOErrorDiagnostic { - file: None, - error: IOErrorKind::Walk(error), - }); - } - ErrorKind::NonUtf8Path { path } => { - diagnostics.lock().unwrap().push(IOErrorDiagnostic { - file: None, - error: IOErrorKind::Walk(WalkError::NonUtf8Path { - path: path.clone(), - }), - }); - } - }, - } - - WalkState::Continue - }) - }); - - ( - paths.into_inner().unwrap(), - diagnostics.into_inner().unwrap(), - ) - } - - pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec, Vec) { - let (paths, diagnostics) = self.walk_paths(); - - ( - paths - .into_iter() - .filter_map(move |path| { - // If this returns `None`, then the file was deleted between the `walk_directory` call and now. - // We can ignore this. - system_path_to_file(db.upcast(), &path).ok() - }) - .collect(), - diagnostics, - ) - } - - pub(crate) fn collect_set(self, db: &dyn Db) -> (FxHashSet, Vec) { - let (paths, diagnostics) = self.walk_paths(); - - let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher); - - for path in paths { - if let Ok(file) = system_path_to_file(db.upcast(), &path) { - files.insert(file); - } - } - - (files, diagnostics) - } -} - -#[derive(Error, Debug, Clone)] -pub(crate) enum WalkError { - #[error("`{path}`: {error}")] - IOPathError { path: SystemPathBuf, error: String }, - - #[error("Failed to walk project directory: {error}")] - IOError { error: String }, - - #[error("`{path}` is not a valid UTF-8 path")] - NonUtf8Path { path: PathBuf }, -} diff --git a/crates/red_knot_project/tests/check.rs b/crates/red_knot_project/tests/check.rs deleted file mode 100644 index 091908cf0c54d..0000000000000 --- a/crates/red_knot_project/tests/check.rs +++ /dev/null @@ -1,282 +0,0 @@ -use anyhow::{anyhow, Context}; -use red_knot_project::{ProjectDatabase, ProjectMetadata}; -use red_knot_python_semantic::{HasType, SemanticModel}; -use ruff_db::files::{system_path_to_file, File}; -use ruff_db::parsed::parsed_module; -use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem}; -use ruff_python_ast::visitor::source_order; -use ruff_python_ast::visitor::source_order::SourceOrderVisitor; -use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt}; - -fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result { - let project = ProjectMetadata::discover(project_root, &system)?; - ProjectDatabase::new(project, system) -} - -fn get_cargo_workspace_root() -> anyhow::Result { - Ok(SystemPathBuf::from(String::from_utf8( - std::process::Command::new("cargo") - .args(["locate-project", "--workspace", "--message-format", "plain"]) - .output()? - .stdout, - )?) - .parent() - .unwrap() - .to_owned()) -} - -/// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`]) -#[test] -fn corpus_no_panic() -> anyhow::Result<()> { - let crate_root = String::from(env!("CARGO_MANIFEST_DIR")); - run_corpus_tests(&format!("{crate_root}/resources/test/corpus/**/*.py")) -} - -#[test] -fn parser_no_panic() -> anyhow::Result<()> { - let workspace_root = get_cargo_workspace_root()?; - run_corpus_tests(&format!( - "{workspace_root}/crates/ruff_python_parser/resources/**/*.py" - )) -} - -#[test] -fn linter_af_no_panic() -> anyhow::Result<()> { - let workspace_root = get_cargo_workspace_root()?; - run_corpus_tests(&format!( - "{workspace_root}/crates/ruff_linter/resources/test/fixtures/[a-f]*/**/*.py" - )) -} - -#[test] -fn linter_gz_no_panic() -> anyhow::Result<()> { - let workspace_root = get_cargo_workspace_root()?; - run_corpus_tests(&format!( - "{workspace_root}/crates/ruff_linter/resources/test/fixtures/[g-z]*/**/*.py" - )) -} - -#[test] -#[ignore = "Enable running once there are fewer failures"] -fn linter_stubs_no_panic() -> anyhow::Result<()> { - let workspace_root = get_cargo_workspace_root()?; - run_corpus_tests(&format!( - "{workspace_root}/crates/ruff_linter/resources/test/fixtures/**/*.pyi" - )) -} - -#[test] -#[ignore = "Enable running over typeshed stubs once there are fewer failures"] -fn typeshed_no_panic() -> anyhow::Result<()> { - let workspace_root = get_cargo_workspace_root()?; - run_corpus_tests(&format!( - "{workspace_root}/crates/red_knot_vendored/vendor/typeshed/**/*.pyi" - )) -} - -#[allow(clippy::print_stdout)] -fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { - let root = SystemPathBuf::from("/src"); - - let system = TestSystem::default(); - let memory_fs = system.memory_file_system(); - memory_fs.create_directory_all(root.as_ref())?; - - let mut db = setup_db(&root, system.clone())?; - - let workspace_root = get_cargo_workspace_root()?; - let workspace_root = workspace_root.to_string(); - - let corpus = glob::glob(pattern).context("Failed to compile pattern")?; - - for path in corpus { - let path = path.context("Failed to glob path")?; - let path = SystemPathBuf::from_path_buf(path).map_err(|path| { - anyhow!( - "Failed to convert path '{path}' to system path", - path = path.display() - ) - })?; - - let relative_path = path.strip_prefix(&workspace_root)?; - - let (py_expected_to_fail, pyi_expected_to_fail) = KNOWN_FAILURES - .iter() - .find_map(|(path, py_fail, pyi_fail)| { - if *path == relative_path.as_str().replace('\\', "/") { - Some((*py_fail, *pyi_fail)) - } else { - None - } - }) - .unwrap_or((false, false)); - - let source = path.as_path(); - let source_filename = source.file_name().unwrap(); - - let code = std::fs::read_to_string(source)?; - - let mut check_with_file_name = |path: &SystemPath| { - memory_fs.write_file_all(path, &code).unwrap(); - File::sync_path(&mut db, path); - - // this test is only asserting that we can pull every expression type without a panic - // (and some non-expressions that clearly define a single type) - let file = system_path_to_file(&db, path).unwrap(); - - let result = std::panic::catch_unwind(|| pull_types(&db, file)); - - let expected_to_fail = if path.extension().map(|e| e == "pyi").unwrap_or(false) { - pyi_expected_to_fail - } else { - py_expected_to_fail - }; - if let Err(err) = result { - if !expected_to_fail { - println!("Check failed for {relative_path:?}. Consider fixing it or adding it to KNOWN_FAILURES"); - std::panic::resume_unwind(err); - } - } else { - assert!(!expected_to_fail, "Expected to panic, but did not. Consider removing this path from KNOWN_FAILURES"); - } - - memory_fs.remove_file(path).unwrap(); - file.sync(&mut db); - }; - - if source.extension() == Some("pyi") { - println!("checking {relative_path}"); - let pyi_dest = root.join(source_filename); - check_with_file_name(&pyi_dest); - } else { - println!("checking {relative_path}"); - let py_dest = root.join(source_filename); - check_with_file_name(&py_dest); - - let pyi_dest = root.join(format!("{source_filename}i")); - println!("re-checking as stub file: {pyi_dest}"); - check_with_file_name(&pyi_dest); - } - } - - Ok(()) -} - -fn pull_types(db: &ProjectDatabase, file: File) { - let mut visitor = PullTypesVisitor::new(db, file); - - let ast = parsed_module(db, file); - - visitor.visit_body(ast.suite()); -} - -struct PullTypesVisitor<'db> { - model: SemanticModel<'db>, -} - -impl<'db> PullTypesVisitor<'db> { - fn new(db: &'db ProjectDatabase, file: File) -> Self { - Self { - model: SemanticModel::new(db, file), - } - } - - fn visit_target(&mut self, target: &Expr) { - match target { - Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { - for element in elts { - self.visit_target(element); - } - } - _ => self.visit_expr(target), - } - } -} - -impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> { - fn visit_stmt(&mut self, stmt: &Stmt) { - match stmt { - Stmt::FunctionDef(function) => { - let _ty = function.inferred_type(&self.model); - } - Stmt::ClassDef(class) => { - let _ty = class.inferred_type(&self.model); - } - Stmt::Assign(assign) => { - for target in &assign.targets { - self.visit_target(target); - } - self.visit_expr(&assign.value); - return; - } - Stmt::For(for_stmt) => { - self.visit_target(&for_stmt.target); - self.visit_expr(&for_stmt.iter); - self.visit_body(&for_stmt.body); - self.visit_body(&for_stmt.orelse); - return; - } - Stmt::With(with_stmt) => { - for item in &with_stmt.items { - if let Some(target) = &item.optional_vars { - self.visit_target(target); - } - self.visit_expr(&item.context_expr); - } - - self.visit_body(&with_stmt.body); - return; - } - Stmt::AnnAssign(_) - | Stmt::Return(_) - | Stmt::Delete(_) - | Stmt::AugAssign(_) - | Stmt::TypeAlias(_) - | Stmt::While(_) - | Stmt::If(_) - | Stmt::Match(_) - | Stmt::Raise(_) - | Stmt::Try(_) - | Stmt::Assert(_) - | Stmt::Import(_) - | Stmt::ImportFrom(_) - | Stmt::Global(_) - | Stmt::Nonlocal(_) - | Stmt::Expr(_) - | Stmt::Pass(_) - | Stmt::Break(_) - | Stmt::Continue(_) - | Stmt::IpyEscapeCommand(_) => {} - } - - source_order::walk_stmt(self, stmt); - } - - fn visit_expr(&mut self, expr: &Expr) { - let _ty = expr.inferred_type(&self.model); - - source_order::walk_expr(self, expr); - } - - fn visit_parameter(&mut self, parameter: &Parameter) { - let _ty = parameter.inferred_type(&self.model); - - source_order::walk_parameter(self, parameter); - } - - fn visit_parameter_with_default(&mut self, parameter_with_default: &ParameterWithDefault) { - let _ty = parameter_with_default.inferred_type(&self.model); - - source_order::walk_parameter_with_default(self, parameter_with_default); - } - - fn visit_alias(&mut self, alias: &Alias) { - let _ty = alias.inferred_type(&self.model); - - source_order::walk_alias(self, alias); - } -} - -/// Whether or not the .py/.pyi version of this file is expected to fail -#[rustfmt::skip] -const KNOWN_FAILURES: &[(&str, bool, bool)] = &[]; diff --git a/crates/red_knot_python_semantic/Cargo.toml b/crates/red_knot_python_semantic/Cargo.toml deleted file mode 100644 index c1ef36cb14629..0000000000000 --- a/crates/red_knot_python_semantic/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "red_knot_python_semantic" -version = "0.0.0" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[dependencies] -ruff_db = { workspace = true } -ruff_index = { workspace = true, features = ["salsa"] } -ruff_macros = { workspace = true } -ruff_python_ast = { workspace = true, features = ["salsa"] } -ruff_python_parser = { workspace = true } -ruff_python_stdlib = { workspace = true } -ruff_source_file = { workspace = true } -ruff_text_size = { workspace = true } -ruff_python_literal = { workspace = true } -ruff_python_trivia = { workspace = true } - -anyhow = { workspace = true } -bitflags = { workspace = true } -camino = { workspace = true } -compact_str = { workspace = true } -countme = { workspace = true } -drop_bomb = { workspace = true } -indexmap = { workspace = true } -itertools = { workspace = true } -ordermap = { workspace = true } -salsa = { workspace = true, features = ["compact_str"] } -thiserror = { workspace = true } -tracing = { workspace = true } -rustc-hash = { workspace = true } -hashbrown = { workspace = true } -schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } -smallvec = { workspace = true } -static_assertions = { workspace = true } -test-case = { workspace = true } -memchr = { workspace = true } -strum = { workspace = true} -strum_macros = { workspace = true} - -[dev-dependencies] -ruff_db = { workspace = true, features = ["testing", "os"] } -ruff_python_parser = { workspace = true } -red_knot_test = { workspace = true } -red_knot_vendored = { workspace = true } - -anyhow = { workspace = true } -dir-test = { workspace = true } -insta = { workspace = true } -tempfile = { workspace = true } -quickcheck = { version = "1.0.3", default-features = false } -quickcheck_macros = { version = "1.0.0" } - -[features] -serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"] - -[lints] -workspace = true diff --git a/crates/red_knot_python_semantic/resources/README.md b/crates/red_knot_python_semantic/resources/README.md deleted file mode 100644 index 0a04772b9af87..0000000000000 --- a/crates/red_knot_python_semantic/resources/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking; -executed by the `tests/mdtest.rs` integration test. - -See `crates/red_knot_test/README.md` for documentation of this test format. diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md deleted file mode 100644 index b5b43a8412bac..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ /dev/null @@ -1,83 +0,0 @@ -# Any - -## Annotation - -`typing.Any` is a way to name the Any type. - -```py -from typing import Any - -x: Any = 1 -x = "foo" - -def f(): - reveal_type(x) # revealed: Any -``` - -## Aliased to a different name - -If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type. - -```py -from typing import Any as RenamedAny - -x: RenamedAny = 1 -x = "foo" - -def f(): - reveal_type(x) # revealed: Any -``` - -## Shadowed class - -If you define your own class named `Any`, using that in a type expression refers to your class, and -isn't a spelling of the Any type. - -```py -class Any: ... - -x: Any - -def f(): - reveal_type(x) # revealed: Any - -# This verifies that we're not accidentally seeing typing.Any, since str is assignable -# to that but not to our locally defined class. -y: Any = "not an Any" # error: [invalid-assignment] -``` - -## Subclass - -The spec allows you to define subclasses of `Any`. - -TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The -assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment -to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore -assignable to `int`. - -```py -from typing import Any - -class Subclass(Any): ... - -reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]] - -x: Subclass = 1 # error: [invalid-assignment] -# TODO: no diagnostic -y: int = Subclass() # error: [invalid-assignment] - -def _(s: Subclass): - reveal_type(s) # revealed: Subclass -``` - -## Invalid - -`Any` cannot be parameterized: - -```py -from typing import Any - -# error: [invalid-type-form] "Type `typing.Any` expected no type parameter" -def f(x: Any[int]): - reveal_type(x) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md deleted file mode 100644 index 06388bb72f2da..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md +++ /dev/null @@ -1,302 +0,0 @@ -# Callable - -References: - -- - -Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see: -). 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 - -The `Callable` special form requires _exactly_ two arguments where the first argument is either a -parameter type list, parameter specification, `typing.Concatenate`, or `...` and the second argument -is the return type. Here, we explore various invalid forms. - -### Empty - -A bare `Callable` without any type arguments: - -```py -from typing import Callable - -def _(c: Callable): - reveal_type(c) # revealed: (...) -> Unknown -``` - -### Invalid parameter type argument - -When it's not a list: - -```py -from typing import Callable - -# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -def _(c: Callable[int, str]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -Or, when it's a literal type: - -```py -# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" -def _(c: Callable[42, str]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -Or, when one of the parameter type is invalid in the list: - -```py -# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" -# error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" -def _(c: Callable[[int, 42, str, False], None]): - # revealed: (int, Unknown, str, Unknown, /) -> None - reveal_type(c) -``` - -### Missing return type - -Using a parameter list: - -```py -from typing import Callable - -# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" -def _(c: Callable[[int, str]]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -Or, an ellipsis: - -```py -# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" -def _(c: Callable[...]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -Or something else that's invalid in a type expression generally: - -```py -# fmt: off - -def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" - {1, 2} # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" - ] - ): - reveal_type(c) # revealed: (...) -> Unknown -``` - -### More than two arguments - -We can't reliably infer the callable type if there are more then 2 arguments because we don't know -which argument corresponds to either the parameters or the return type. - -```py -from typing import Callable - -# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" -def _(c: Callable[[int], str, str]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -### List as the second argument - -```py -from typing import Callable - -# fmt: off - -def _(c: Callable[ - int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" - [str] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" - ] - ): - reveal_type(c) # revealed: (...) -> Unknown -``` - -### List as both arguments - -```py -from typing import Callable - -# error: [invalid-type-form] "List literals are not allowed in this context in a type expression" -def _(c: Callable[[int], [str]]): - reveal_type(c) # revealed: (int, /) -> Unknown -``` - -### Three list arguments - -```py -from typing import Callable - -# fmt: off - - -def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" - [int], - [str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" - [bytes] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" - ] - ): - reveal_type(c) # revealed: (...) -> Unknown -``` - -## Simple - -A simple `Callable` with multiple parameters and a return type: - -```py -from typing import Callable - -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: - -```py -from typing import Callable - -def _(c: Callable[[Callable[[int], str]], int]): - reveal_type(c) # revealed: ((int, /) -> str, /) -> int -``` - -And, as the return type: - -```py -def _(c: Callable[[int, str], Callable[[int], int]]): - reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int -``` - -## Gradual form - -The `Callable` special form supports the use of `...` in place of the list of parameter types. This -is a [gradual form] indicating that the type is consistent with any input signature: - -```py -from typing import Callable - -def gradual_form(c: Callable[..., str]): - reveal_type(c) # revealed: (...) -> str -``` - -## Using `typing.Concatenate` - -Using `Concatenate` as the first argument to `Callable`: - -```py -from typing_extensions import Callable, Concatenate - -def _(c: Callable[Concatenate[int, str, ...], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int -``` - -And, as one of the parameter types: - -```py -def _(c: Callable[[Concatenate[int, str, ...], int], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int -``` - -## Using `typing.ParamSpec` - -Using a `ParamSpec` in a `Callable` annotation: - -```py -from typing_extensions import Callable - -# TODO: Not an error; remove once `ParamSpec` is supported -# error: [invalid-type-form] -def _[**P1](c: Callable[P1, int]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -And, using the legacy syntax: - -```py -from typing_extensions import ParamSpec - -P2 = ParamSpec("P2") - -# TODO: Not an error; remove once `ParamSpec` is supported -# error: [invalid-type-form] -def _(c: Callable[P2, int]): - reveal_type(c) # revealed: (...) -> Unknown -``` - -## Using `typing.Unpack` - -Using the unpack operator (`*`): - -```py -from typing_extensions import Callable, TypeVarTuple - -Ts = TypeVarTuple("Ts") - -def _(c: Callable[[int, *Ts], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int -``` - -And, using the legacy syntax using `Unpack`: - -```py -from typing_extensions import Unpack - -def _(c: Callable[[int, Unpack[Ts]], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int -``` - -## Member lookup - -```py -from typing import Callable - -def _(c: Callable[[int], int]): - reveal_type(c.__init__) # revealed: def __init__(self) -> None - reveal_type(c.__class__) # revealed: type - - # TODO: The member lookup for `Callable` uses `object` which does not have a `__call__` - # attribute. We could special case `__call__` in this context. Refer to - # https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 for more details. - # error: [unresolved-attribute] "Type `(int, /) -> int` has no attribute `__call__`" - reveal_type(c.__call__) # revealed: Unknown -``` - -[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md deleted file mode 100644 index a5154b450a64b..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md +++ /dev/null @@ -1,114 +0,0 @@ -# Tests for invalid types in type expressions - -## Invalid types are rejected - -Many types are illegal in the context of a type expression: - -```py -import typing -from knot_extensions import AlwaysTruthy, AlwaysFalsy -from typing_extensions import Literal, Never - -class A: ... - -def _( - a: type[int], - b: AlwaysTruthy, - c: AlwaysFalsy, - d: Literal[True], - e: Literal["bar"], - f: Literal[b"foo"], - g: tuple[int, str], - h: Never, - i: int, - j: A, -): - def foo(): ... - def invalid( - a_: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression" - b_: b, # error: [invalid-type-form] - c_: c, # error: [invalid-type-form] - d_: d, # error: [invalid-type-form] - e_: e, # error: [invalid-type-form] - f_: f, # error: [invalid-type-form] - g_: g, # error: [invalid-type-form] - h_: h, # error: [invalid-type-form] - i_: typing, # error: [invalid-type-form] - j_: foo, # error: [invalid-type-form] - k_: i, # error: [invalid-type-form] "Variable of type `int` is not allowed in a type expression" - l_: j, # error: [invalid-type-form] "Variable of type `A` is not allowed in a type expression" - ): - reveal_type(a_) # revealed: Unknown - reveal_type(b_) # revealed: Unknown - reveal_type(c_) # revealed: Unknown - reveal_type(d_) # revealed: Unknown - reveal_type(e_) # revealed: Unknown - reveal_type(f_) # revealed: Unknown - reveal_type(g_) # revealed: Unknown - reveal_type(h_) # revealed: Unknown - reveal_type(i_) # revealed: Unknown - reveal_type(j_) # revealed: Unknown -``` - -## Invalid AST nodes - -```py -def bar() -> None: - return None - -def _( - a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" - b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions" - c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions" - d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" - e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" - f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" - g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" - h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions" - i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions" - j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions" - k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions" - l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions" - m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" - n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions" - o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions" - p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions" - q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions" - r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions" -): - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown - reveal_type(d) # revealed: Unknown - reveal_type(e) # revealed: int | Unknown - reveal_type(f) # revealed: Unknown - reveal_type(g) # revealed: Unknown - reveal_type(h) # revealed: Unknown - reveal_type(i) # revealed: Unknown - reveal_type(j) # revealed: Unknown - reveal_type(k) # revealed: Unknown - reveal_type(p) # revealed: Unknown - reveal_type(q) # revealed: int | Unknown - reveal_type(r) # revealed: @Todo(generics) -``` - -## Invalid Collection based AST nodes - -```py -def _( - a: {1: 2}, # error: [invalid-type-form] "Dict literals are not allowed in type expressions" - b: {1, 2}, # error: [invalid-type-form] "Set literals are not allowed in type expressions" - c: {k: v for k, v in [(1, 2)]}, # error: [invalid-type-form] "Dict comprehensions are not allowed in type expressions" - d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions" - e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions" - f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions" - g: [int, str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" -): - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown - reveal_type(d) # revealed: Unknown - reveal_type(e) # revealed: Unknown - reveal_type(f) # revealed: Unknown - reveal_type(g) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md deleted file mode 100644 index a71a89bcc13db..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md +++ /dev/null @@ -1,18 +0,0 @@ -# Starred expression annotations - -Type annotations for `*args` can be starred expressions themselves: - -```py -from typing_extensions import TypeVarTuple - -Ts = TypeVarTuple("Ts") - -def append_int(*args: *Ts) -> tuple[*Ts, int]: - # TODO: tuple[*Ts] - reveal_type(args) # revealed: tuple - - return (*args, 1) - -# TODO should be tuple[Literal[True], Literal["a"], int] -reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md deleted file mode 100644 index d41d1276d6ac7..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ /dev/null @@ -1,124 +0,0 @@ -# Typing-module aliases to other stdlib classes - -The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but -still need to be supported by a type checker. - -## Correspondence - -All of the following symbols can be mapped one-to-one with the actual type: - -```py -import typing - -def f( - list_bare: typing.List, - list_parametrized: typing.List[int], - dict_bare: typing.Dict, - dict_parametrized: typing.Dict[int, str], - set_bare: typing.Set, - set_parametrized: typing.Set[int], - frozen_set_bare: typing.FrozenSet, - frozen_set_parametrized: typing.FrozenSet[str], - chain_map_bare: typing.ChainMap, - chain_map_parametrized: typing.ChainMap[int], - counter_bare: typing.Counter, - counter_parametrized: typing.Counter[int], - default_dict_bare: typing.DefaultDict, - default_dict_parametrized: typing.DefaultDict[str, int], - deque_bare: typing.Deque, - deque_parametrized: typing.Deque[str], - ordered_dict_bare: typing.OrderedDict, - ordered_dict_parametrized: typing.OrderedDict[int, str], -): - reveal_type(list_bare) # revealed: list - reveal_type(list_parametrized) # revealed: list - - reveal_type(dict_bare) # revealed: dict - reveal_type(dict_parametrized) # revealed: dict - - reveal_type(set_bare) # revealed: set - reveal_type(set_parametrized) # revealed: set - - reveal_type(frozen_set_bare) # revealed: frozenset - reveal_type(frozen_set_parametrized) # revealed: frozenset - - reveal_type(chain_map_bare) # revealed: ChainMap - reveal_type(chain_map_parametrized) # revealed: ChainMap - - reveal_type(counter_bare) # revealed: Counter - reveal_type(counter_parametrized) # revealed: Counter - - reveal_type(default_dict_bare) # revealed: defaultdict - reveal_type(default_dict_parametrized) # revealed: defaultdict - - reveal_type(deque_bare) # revealed: deque - reveal_type(deque_parametrized) # revealed: deque - - reveal_type(ordered_dict_bare) # revealed: OrderedDict - reveal_type(ordered_dict_parametrized) # revealed: OrderedDict -``` - -## Inheritance - -The aliases can be inherited from. Some of these are still partially or wholly TODOs. - -```py -import typing - -#################### -### Built-ins - -class ListSubclass(typing.List): ... - -# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] -reveal_type(ListSubclass.__mro__) - -class DictSubclass(typing.Dict): ... - -# TODO: should have `Generic`, should not have `Unknown` -# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]] -reveal_type(DictSubclass.__mro__) - -class SetSubclass(typing.Set): ... - -# 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): ... - -# TODO: should have `Generic`, should not have `Unknown` -# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]] -reveal_type(FrozenSetSubclass.__mro__) - -#################### -### `collections` - -class ChainMapSubclass(typing.ChainMap): ... - -# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]] -reveal_type(ChainMapSubclass.__mro__) - -class CounterSubclass(typing.Counter): ... - -# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]] -reveal_type(CounterSubclass.__mro__) - -class DefaultDictSubclass(typing.DefaultDict): ... - -# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]] -reveal_type(DefaultDictSubclass.__mro__) - -class DequeSubclass(typing.Deque): ... - -# 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): ... - -# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]] -reveal_type(OrderedDictSubclass.__mro__) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md deleted file mode 100644 index f44c000855ea6..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md +++ /dev/null @@ -1,212 +0,0 @@ -# String annotations - -## Simple - -```py -def f(v: "int"): - reveal_type(v) # revealed: int -``` - -## Nested - -```py -def f(v: "'int'"): - reveal_type(v) # revealed: int -``` - -## Type expression - -```py -def f1(v: "int | str", w: "tuple[int, str]"): - reveal_type(v) # revealed: int | str - reveal_type(w) # revealed: tuple[int, str] -``` - -## Partial - -```py -def f(v: tuple[int, "str"]): - reveal_type(v) # revealed: tuple[int, str] -``` - -## Deferred - -```py -def f(v: "Foo"): - reveal_type(v) # revealed: Foo - -class Foo: ... -``` - -## Deferred (undefined) - -```py -# error: [unresolved-reference] -def f(v: "Foo"): - reveal_type(v) # revealed: Unknown -``` - -## Partial deferred - -```py -def f(v: int | "Foo"): - reveal_type(v) # revealed: int | Foo - -class Foo: ... -``` - -## `typing.Literal` - -```py -from typing import Literal - -def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'): - reveal_type(v) # revealed: Literal["Foo", "Bar"] - reveal_type(w) # revealed: Literal["Foo", "Bar"] - -class Foo: ... -``` - -## Various string kinds - -```py -def f1( - # error: [raw-string-type-annotation] "Type expressions cannot use raw string literal" - a: r"int", - # error: [fstring-type-annotation] "Type expressions cannot use f-strings" - b: f"int", - # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal" - c: b"int", - d: "int", - # error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals" - e: "in" "t", - # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters" - f: "\N{LATIN SMALL LETTER I}nt", - # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters" - g: "\x69nt", - h: """int""", - # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal" - i: "b'int'", -): - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown - reveal_type(d) # revealed: int - reveal_type(e) # revealed: Unknown - reveal_type(f) # revealed: Unknown - reveal_type(g) # revealed: Unknown - reveal_type(h) # revealed: int - reveal_type(i) # revealed: Unknown -``` - -## Various string kinds in `typing.Literal` - -```py -from typing import Literal - -def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]): - reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"] -``` - -## Class variables - -```py -MyType = int - -class Aliases: - MyType = str - - forward: "MyType" = "value" - not_forward: MyType = "value" - -reveal_type(Aliases.forward) # revealed: str -reveal_type(Aliases.not_forward) # revealed: str -``` - -## Annotated assignment - -```py -a: "int" = 1 -b: "'int'" = 1 -c: "Foo" -# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`" -d: "Foo" = 1 - -class Foo: ... - -c = Foo() - -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[1] -reveal_type(c) # revealed: Foo -reveal_type(d) # revealed: Foo -``` - -## Parameter - -TODO: Add tests once parameter inference is supported - -## Invalid expressions - -The expressions in these string annotations aren't valid expressions in this context but we -shouldn't panic. - -```py -a: "1 or 2" -b: "(x := 1)" -c: "1 + 2" -d: "lambda x: x" -e: "x if True else y" -f: "{'a': 1, 'b': 2}" -g: "{1, 2}" -h: "[i for i in range(5)]" -i: "{i for i in range(5)}" -j: "{i: i for i in range(5)}" -k: "(i for i in range(5))" -l: "await 1" -# error: [invalid-syntax-in-forward-annotation] -m: "yield 1" -# error: [invalid-syntax-in-forward-annotation] -n: "yield from 1" -o: "1 < 2" -p: "call()" -r: "[1, 2]" -s: "(1, 2)" -``` - -## Multi line annotation - -Quoted type annotations should be parsed as if surrounded by parentheses. - -```py -def valid( - a1: """( - int | - str - ) - """, - a2: """ - int | - str - """, -): - reveal_type(a1) # revealed: int | str - reveal_type(a2) # revealed: int | str - -def invalid( - # error: [invalid-syntax-in-forward-annotation] - a1: """ - int | -str) -""", - # error: [invalid-syntax-in-forward-annotation] - a2: """ - int) | -str -""", - # error: [invalid-syntax-in-forward-annotation] - a3: """ - (int)) """, -): - pass -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md deleted file mode 100644 index 175ada12360fa..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ /dev/null @@ -1,94 +0,0 @@ -# Unsupported special forms - -## Not yet supported - -Several special forms are unsupported by red-knot currently. However, we also don't emit -false-positive errors if you use one in an annotation: - -```py -from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar - -P = ParamSpec("P") -Ts = TypeVarTuple("Ts") -R_co = TypeVar("R_co", covariant=True) - -Alias: TypeAlias = int - -def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: - # TODO: should understand the annotation - reveal_type(args) # revealed: tuple - - reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`) - -def g() -> TypeGuard[int]: ... -def h() -> TypeIs[int]: ... -def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: - # TODO: should understand the annotation - reveal_type(args) # revealed: tuple - - # TODO: should understand the annotation - reveal_type(kwargs) # revealed: dict - - return callback(42, *args, **kwargs) - -class Foo: - def method(self, x: Self): - reveal_type(x) # revealed: @Todo(Support for `typing.Self`) -``` - -## Type expressions - -One thing that is supported is error messages for using special forms in type expressions. - -```py -from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec - -def _( - a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression" - b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression" - c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression" - d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression" - e: ParamSpec, -) -> None: - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown - reveal_type(d) # revealed: Unknown - - def foo(a_: e) -> None: - reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`) -``` - -## Inheritance - -You can't inherit from most of these. `typing.Callable` is an exception. - -```py -from typing import Callable -from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate - -class A(Self): ... # error: [invalid-base] -class B(Unpack): ... # error: [invalid-base] -class C(TypeGuard): ... # error: [invalid-base] -class D(TypeIs): ... # error: [invalid-base] -class E(Concatenate): ... # error: [invalid-base] -class F(Callable): ... - -reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]] -``` - -## Subscriptability - -Some of these are not subscriptable: - -```py -from typing_extensions import Self, TypeAlias - -X: TypeAlias[T] = int # error: [invalid-type-form] - -class Foo[T]: - # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" - # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" - def method(self: Self[int]) -> Self[int]: - reveal_type(self) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md deleted file mode 100644 index 15882300c114c..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ /dev/null @@ -1,1881 +0,0 @@ -# Attributes - -Tests for attribute access on various kinds of types. - -## Class and instance variables - -### Pure instance variables - -#### Variable only declared/bound in `__init__` - -Variables only declared and/or bound in `__init__` are pure instance variables. They cannot be -accessed on the class itself. - -```py -class C: - def __init__(self, param: int | None, flag: bool = False) -> None: - value = 1 if flag else "a" - self.inferred_from_value = value - self.inferred_from_other_attribute = self.inferred_from_value - self.inferred_from_param = param - self.declared_only: bytes - self.declared_and_bound: bool = True - if flag: - self.possibly_undeclared_unbound: str = "possibly set in __init__" - -c_instance = C(1) - -reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] - -# TODO: Same here. This should be `Unknown | Literal[1, "a"]` -reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown - -# There is no special handling of attributes that are (directly) assigned to a declared parameter, -# which means we union with `Unknown` here, since the attribute itself is not declared. This is -# something that we might want to change in the future. -# -# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion. -reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None - -reveal_type(c_instance.declared_only) # revealed: bytes - -reveal_type(c_instance.declared_and_bound) # revealed: bool - -# error: [possibly-unbound-attribute] -reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str - -# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`. -c_instance.inferred_from_value = "value set on instance" - -# This assignment is also fine: -c_instance.declared_and_bound = False - -# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `declared_and_bound` of type `bool`" -c_instance.declared_and_bound = "incompatible" - -# mypy shows no error here, but pyright raises "reportAttributeAccessIssue" -# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself." -reveal_type(C.inferred_from_value) # revealed: Unknown - -# mypy shows no error here, but pyright raises "reportAttributeAccessIssue" -# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`" -C.inferred_from_value = "overwritten on class" - -# This assignment is fine: -c_instance.declared_and_bound = False - -# TODO: After this assignment to the attribute within this scope, we may eventually want to narrow -# the `bool` type (see above) for this instance variable to `Literal[False]` here. This is unsound -# in general (we don't know what else happened to `c_instance` between the assignment and the use -# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably -# be `Literal[False]`. -reveal_type(c_instance.declared_and_bound) # revealed: bool -``` - -#### Variable declared in class body and possibly bound in `__init__` - -The same rule applies even if the variable is *declared* (not bound!) in the class body: it is still -a pure instance variable. - -```py -class C: - declared_and_bound: str | None - - def __init__(self) -> None: - self.declared_and_bound = "value set in __init__" - -c_instance = C() - -reveal_type(c_instance.declared_and_bound) # revealed: str | None - -# Note that both mypy and pyright show no error in this case! So we may reconsider this in -# the future, if it turns out to produce too many false positives. We currently emit: -# error: [unresolved-attribute] "Attribute `declared_and_bound` can only be accessed on instances, not on the class object `Literal[C]` itself." -reveal_type(C.declared_and_bound) # revealed: Unknown - -# Same as above. Mypy and pyright do not show an error here. -# error: [invalid-attribute-access] "Cannot assign to instance attribute `declared_and_bound` from the class object `Literal[C]`" -C.declared_and_bound = "overwritten on class" - -# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`" -c_instance.declared_and_bound = 1 -``` - -#### Variable declared in class body and not bound anywhere - -If a variable is declared in the class body but not bound anywhere, we still consider it a pure -instance variable and allow access to it via instances. - -```py -class C: - only_declared: str - -c_instance = C() - -reveal_type(c_instance.only_declared) # revealed: str - -# Mypy and pyright do not show an error here. We treat this as a pure instance variable. -# error: [unresolved-attribute] "Attribute `only_declared` can only be accessed on instances, not on the class object `Literal[C]` itself." -reveal_type(C.only_declared) # revealed: Unknown - -# error: [invalid-attribute-access] "Cannot assign to instance attribute `only_declared` from the class object `Literal[C]`" -C.only_declared = "overwritten on class" -``` - -#### Mixed declarations/bindings in class body and `__init__` - -```py -class C: - only_declared_in_body: str | None - declared_in_body_and_init: str | None - - declared_in_body_defined_in_init: str | None - - bound_in_body_declared_in_init = "a" - - bound_in_body_and_init = None - - def __init__(self, flag) -> None: - self.only_declared_in_init: str | None - self.declared_in_body_and_init: str | None = None - - self.declared_in_body_defined_in_init = "a" - - self.bound_in_body_declared_in_init: str | None - - if flag: - self.bound_in_body_and_init = "a" - -c_instance = C(True) - -reveal_type(c_instance.only_declared_in_body) # revealed: str | None -reveal_type(c_instance.only_declared_in_init) # revealed: str | None -reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None - -reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None - -# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API, -# which is planned in https://github.com/astral-sh/ruff/issues/14297 -reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None - -reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"] -``` - -#### Variable defined in non-`__init__` method - -We also recognize pure instance variables if they are defined in a method that is not `__init__`. - -```py -class C: - def __init__(self, param: int | None, flag: bool = False) -> None: - self.initialize(param, flag) - - def initialize(self, param: int | None, flag: bool) -> None: - value = 1 if flag else "a" - self.inferred_from_value = value - self.inferred_from_other_attribute = self.inferred_from_value - self.inferred_from_param = param - self.declared_only: bytes - self.declared_and_bound: bool = True - -c_instance = C(1) - -reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] - -# TODO: Should be `Unknown | Literal[1, "a"]` -reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown - -reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None - -reveal_type(c_instance.declared_only) # revealed: bytes - -reveal_type(c_instance.declared_and_bound) # revealed: bool - -# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself." -reveal_type(C.inferred_from_value) # revealed: Unknown - -# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`" -C.inferred_from_value = "overwritten on class" -``` - -#### Variable defined in multiple methods - -If we see multiple un-annotated assignments to a single attribute (`self.x` below), we build the -union of all inferred types (and `Unknown`). If we see multiple conflicting declarations of the same -attribute, that should be an error. - -```py -def get_int() -> int: - return 0 - -def get_str() -> str: - return "a" - -class C: - z: int - - def __init__(self) -> None: - self.x = get_int() - self.y: int = 1 - - def other_method(self): - self.x = get_str() - - # TODO: this redeclaration should be an error - self.y: str = "a" - - # TODO: this redeclaration should be an error - self.z: str = "a" - -c_instance = C() - -reveal_type(c_instance.x) # revealed: Unknown | int | str -reveal_type(c_instance.y) # revealed: int -reveal_type(c_instance.z) # revealed: int -``` - -#### Attributes defined in multi-target assignments - -```py -class C: - def __init__(self) -> None: - self.a = self.b = 1 - -c_instance = C() - -reveal_type(c_instance.a) # revealed: Unknown | Literal[1] -reveal_type(c_instance.b) # revealed: Unknown | Literal[1] -``` - -#### Augmented assignments - -```py -class Weird: - def __iadd__(self, other: None) -> str: - return "a" - -class C: - def __init__(self) -> None: - self.w = Weird() - self.w += None - -# TODO: Mypy and pyright do not support this, but it would be great if we could -# infer `Unknown | str` or at least `Unknown | Weird | str` here. -reveal_type(C().w) # revealed: Unknown | Weird -``` - -#### Attributes defined in tuple unpackings - -```py -def returns_tuple() -> tuple[int, str]: - return (1, "a") - -class C: - a1, b1 = (1, "a") - c1, d1 = returns_tuple() - - def __init__(self) -> None: - self.a2, self.b2 = (1, "a") - self.c2, self.d2 = returns_tuple() - -c_instance = C() - -reveal_type(c_instance.a1) # revealed: Unknown | Literal[1] -reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"] -reveal_type(c_instance.c1) # revealed: Unknown | int -reveal_type(c_instance.d1) # revealed: Unknown | str - -reveal_type(c_instance.a2) # revealed: Unknown | Literal[1] - -reveal_type(c_instance.b2) # revealed: Unknown | Literal["a"] - -reveal_type(c_instance.c2) # revealed: Unknown | int -reveal_type(c_instance.d2) # revealed: Unknown | str -``` - -#### Starred assignments - -```py -class C: - def __init__(self) -> None: - self.a, *self.b = (1, 2, 3) - -c_instance = C() -reveal_type(c_instance.a) # revealed: Unknown | Literal[1] -reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking) -``` - -#### Attributes defined in for-loop (unpacking) - -```py -class IntIterator: - def __next__(self) -> int: - return 1 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -class TupleIterator: - def __next__(self) -> tuple[int, str]: - return (1, "a") - -class TupleIterable: - def __iter__(self) -> TupleIterator: - return TupleIterator() - -class NonIterable: ... - -class C: - def __init__(self): - for self.x in IntIterable(): - pass - - for _, self.y in TupleIterable(): - pass - - # TODO: We should emit a diagnostic here - for self.z in NonIterable(): - pass - -# Iterable might be empty -# error: [possibly-unbound-attribute] -reveal_type(C().x) # revealed: Unknown | int -# error: [possibly-unbound-attribute] -reveal_type(C().y) # revealed: Unknown | str -``` - -#### Attributes defined in `with` statements - -```py -class ContextManager: - def __enter__(self) -> int | None: - return 1 - - def __exit__(self, exc_type, exc_value, traceback) -> None: - pass - -class C: - def __init__(self) -> None: - with ContextManager() as self.x: - pass - -c_instance = C() - -reveal_type(c_instance.x) # revealed: Unknown | int | None -``` - -#### Attributes defined in `with` statements, but with unpacking - -```py -class ContextManager: - def __enter__(self) -> tuple[int | None, int]: - return 1, 2 - - def __exit__(self, exc_type, exc_value, traceback) -> None: - pass - -class C: - def __init__(self) -> None: - with ContextManager() as (self.x, self.y): - pass - -c_instance = C() - -reveal_type(c_instance.x) # revealed: Unknown | int | None -reveal_type(c_instance.y) # revealed: Unknown | int -``` - -#### Attributes defined in comprehensions - -```py -class IntIterator: - def __next__(self) -> int: - return 1 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -class C: - def __init__(self) -> None: - [... for self.a in IntIterable()] - -c_instance = C() - -# TODO: Should be `Unknown | int` -# error: [unresolved-attribute] -reveal_type(c_instance.a) # revealed: Unknown -``` - -#### Conditionally declared / bound attributes - -Attributes are possibly unbound if they, or the method to which they are added are conditionally -declared / bound. - -```py -def flag() -> bool: - return True - -class C: - def f(self) -> None: - if flag(): - self.a1: str | None = "a" - self.b1 = 1 - if flag(): - def f(self) -> None: - self.a2: str | None = "a" - self.b2 = 1 - -c_instance = C() - -# error: [possibly-unbound-attribute] -reveal_type(c_instance.a1) # revealed: str | None -# error: [possibly-unbound-attribute] -reveal_type(c_instance.a2) # revealed: str | None -# error: [possibly-unbound-attribute] -reveal_type(c_instance.b1) # revealed: Unknown | Literal[1] -# error: [possibly-unbound-attribute] -reveal_type(c_instance.b2) # revealed: Unknown | Literal[1] -``` - -#### Methods that does not use `self` as a first parameter - -```py -class C: - # This might trigger a stylistic lint like `invalid-first-argument-name-for-method`, but - # it should be supported in general: - def __init__(this) -> None: - this.declared_and_bound: str | None = "a" - -reveal_type(C().declared_and_bound) # revealed: str | None -``` - -#### Aliased `self` parameter - -```py -class C: - def __init__(self) -> None: - this = self - this.declared_and_bound: str | None = "a" - -# This would ideally be `str | None`, but mypy/pyright don't support this either, -# so `Unknown` + a diagnostic is also fine. -# error: [unresolved-attribute] -reveal_type(C().declared_and_bound) # revealed: Unknown -``` - -#### Static methods do not influence implicitly defined attributes - -```py -class Other: - x: int - -class C: - @staticmethod - def f(other: Other) -> None: - other.x = 1 - -# error: [unresolved-attribute] -reveal_type(C.x) # revealed: Unknown - -# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown` -reveal_type(C().x) # revealed: Unknown | Literal[1] - -# This also works if `staticmethod` is aliased: - -my_staticmethod = staticmethod - -class D: - @my_staticmethod - def f(other: Other) -> None: - other.x = 1 - -# error: [unresolved-attribute] -reveal_type(D.x) # revealed: Unknown - -# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown` -reveal_type(D().x) # revealed: Unknown | Literal[1] -``` - -If `staticmethod` is something else, that should not influence the behavior: - -```py -def staticmethod(f): - return f - -class C: - @staticmethod - def f(self) -> None: - self.x = 1 - -reveal_type(C().x) # revealed: Unknown | Literal[1] -``` - -And if `staticmethod` is fully qualified, that should also be recognized: - -```py -import builtins - -class Other: - x: int - -class C: - @builtins.staticmethod - def f(other: Other) -> None: - other.x = 1 - -# error: [unresolved-attribute] -reveal_type(C.x) # revealed: Unknown - -# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown` -reveal_type(C().x) # revealed: Unknown | Literal[1] -``` - -#### Attributes defined in statically-known-to-be-false branches - -```py -class C: - def __init__(self) -> None: - # We use a "significantly complex" condition here (instead of just `False`) - # for a proper comparison with mypy and pyright, which distinguish between - # conditions that can be resolved from a simple pattern matching and those - # that need proper type inference. - if (2 + 3) < 4: - self.x: str = "a" - -# error: [unresolved-attribute] -reveal_type(C().x) # revealed: Unknown -``` - -```py -class C: - def __init__(self, cond: bool) -> None: - if True: - self.a = 1 - else: - self.a = "a" - - if False: - self.b = 2 - - if cond: - return - - self.c = 3 - - self.d = 4 - self.d = 5 - - def set_c(self, c: str) -> None: - self.c = c - if False: - def set_e(self, e: str) -> None: - self.e = e - -reveal_type(C(True).a) # revealed: Unknown | Literal[1] -# error: [unresolved-attribute] -reveal_type(C(True).b) # revealed: Unknown -reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str -# TODO: this attribute is possibly unbound -reveal_type(C(True).d) # revealed: Unknown | Literal[5] -# error: [unresolved-attribute] -reveal_type(C(True).e) # revealed: Unknown -``` - -#### Attributes considered always bound - -```py -class C: - def __init__(self, cond: bool): - self.x = 1 - if cond: - raise ValueError("Something went wrong") - - # We consider this attribute is always bound. - # This is because, it is not possible to access a partially-initialized object by normal means. - self.y = 2 - -reveal_type(C(False).x) # revealed: Unknown | Literal[1] -reveal_type(C(False).y) # revealed: Unknown | Literal[2] - -class C: - def __init__(self, b: bytes) -> None: - self.b = b - - try: - s = b.decode() - except UnicodeDecodeError: - raise ValueError("Invalid UTF-8 sequence") - - self.s = s - -reveal_type(C(b"abc").b) # revealed: Unknown | bytes -reveal_type(C(b"abc").s) # revealed: Unknown | str - -class C: - def __init__(self, iter) -> None: - self.x = 1 - - for _ in iter: - pass - - # The for-loop may not stop, - # but we consider the subsequent attributes to be definitely-bound. - self.y = 2 - -reveal_type(C([]).x) # revealed: Unknown | Literal[1] -reveal_type(C([]).y) # revealed: Unknown | Literal[2] -``` - -#### Diagnostics are reported for the right-hand side of attribute assignments - -```py -class C: - def __init__(self) -> None: - # error: [too-many-positional-arguments] - # error: [invalid-argument-type] - self.x: int = len(1, 2, 3) -``` - -### Pure class variables (`ClassVar`) - -#### Annotated with `ClassVar` type qualifier - -Class variables annotated with the [`typing.ClassVar`] type qualifier are pure class variables. They -cannot be overwritten on instances, but they can be accessed on instances. - -For more details, see the [typing spec on `ClassVar`]. - -```py -from typing import ClassVar - -class C: - pure_class_variable1: ClassVar[str] = "value in class body" - pure_class_variable2: ClassVar = 1 - - def method(self): - # TODO: this should be an error - self.pure_class_variable1 = "value set through instance" - -reveal_type(C.pure_class_variable1) # revealed: str - -# TODO: Should be `Unknown | Literal[1]`. -reveal_type(C.pure_class_variable2) # revealed: Unknown - -c_instance = C() - -# It is okay to access a pure class variable on an instance. -reveal_type(c_instance.pure_class_variable1) # revealed: str - -# TODO: Should be `Unknown | Literal[1]`. -reveal_type(c_instance.pure_class_variable2) # revealed: Unknown - -# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`" -c_instance.pure_class_variable1 = "value set on instance" - -C.pure_class_variable1 = "overwritten on class" - -# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_class_variable1` of type `str`" -C.pure_class_variable1 = 1 - -class Subclass(C): - pure_class_variable1: ClassVar[str] = "overwritten on subclass" - -reveal_type(Subclass.pure_class_variable1) # revealed: str -``` - -#### Variable only mentioned in a class method - -We also consider a class variable to be a pure class variable if it is only mentioned in a class -method. - -```py -class C: - @classmethod - def class_method(cls): - cls.pure_class_variable = "value set in class method" - -# for a more realistic example, let's actually call the method -C.class_method() - -# TODO: We currently plan to support this and show no error here. -# mypy shows an error here, pyright does not. -# error: [unresolved-attribute] -reveal_type(C.pure_class_variable) # revealed: Unknown - -# TODO: should be no error when descriptor protocol is supported -# and the assignment is properly attributed to the class method. -# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `Literal[C]`" -C.pure_class_variable = "overwritten on class" - -# TODO: should be `Unknown | Literal["value set in class method"]` or -# Literal["overwritten on class"]`, once/if we support local narrowing. -# error: [unresolved-attribute] -reveal_type(C.pure_class_variable) # revealed: Unknown - -c_instance = C() -reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"] - -# TODO: should raise an error. -c_instance.pure_class_variable = "value set on instance" -``` - -### Instance variables with class-level default values - -These are instance attributes, but the fact that we can see that they have a binding (not a -declaration) in the class body means that reading the value from the class directly is also -permitted. This is the only difference for these attributes as opposed to "pure" instance -attributes. - -#### Basic - -```py -class C: - variable_with_class_default1: str = "value in class body" - variable_with_class_default2 = 1 - - def instance_method(self): - self.variable_with_class_default1 = "value set in instance method" - -reveal_type(C.variable_with_class_default1) # revealed: str - -reveal_type(C.variable_with_class_default2) # revealed: Unknown | Literal[1] - -c_instance = C() - -reveal_type(c_instance.variable_with_class_default1) # revealed: str -reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Literal[1] - -c_instance.variable_with_class_default1 = "value set on instance" - -reveal_type(C.variable_with_class_default1) # revealed: str - -# TODO: Could be Literal["value set on instance"], or still `str` if we choose not to -# narrow the type. -reveal_type(c_instance.variable_with_class_default1) # revealed: str - -C.variable_with_class_default1 = "overwritten on class" - -# TODO: Could be `Literal["overwritten on class"]`, or still `str` if we choose not to -# narrow the type. -reveal_type(C.variable_with_class_default1) # revealed: str - -# TODO: should still be `Literal["value set on instance"]`, or `str`. -reveal_type(c_instance.variable_with_class_default1) # revealed: str -``` - -### Inheritance of class/instance attributes - -#### Instance variable defined in a base class - -```py -class Base: - declared_in_body: int | None = 1 - - base_class_attribute_1: str | None - base_class_attribute_2: str | None - base_class_attribute_3: str | None - - def __init__(self) -> None: - self.defined_in_init: str | None = "value in base" - -class Intermediate(Base): - # Redeclaring base class attributes with the *same *type is fine: - base_class_attribute_1: str | None = None - - # 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 - # of same name in class "Base". Variable is mutable so its type is invariant" - # - # We should introduce a diagnostic for this. Whether or not that should be - # enabled by default can still be discussed. - # - # TODO: This should be an error - base_class_attribute_2: str - - # Redeclaring attributes with a *wider type* directly violates LSP. - # - # In this case, both mypy and pyright report an error. - # - # TODO: This should be an error - base_class_attribute_3: str | int | None - -class Derived(Intermediate): ... - -reveal_type(Derived.declared_in_body) # revealed: int | None - -reveal_type(Derived().declared_in_body) # revealed: int | None - -reveal_type(Derived().defined_in_init) # revealed: str | None -``` - -## Accessing attributes on class objects - -When accessing attributes on class objects, they are always looked up on the type of the class -object first, i.e. on the metaclass: - -```py -from typing import Literal - -class Meta1: - attr: Literal["metaclass value"] = "metaclass value" - -class C1(metaclass=Meta1): ... - -reveal_type(C1.attr) # revealed: Literal["metaclass value"] -``` - -However, the metaclass attribute only takes precedence over a class-level attribute if it is a data -descriptor. If it is a non-data descriptor or a normal attribute, the class-level attribute is used -instead (see the [descriptor protocol tests] for data/non-data descriptor attributes): - -```py -class Meta2: - attr: str = "metaclass value" - -class C2(metaclass=Meta2): - attr: Literal["class value"] = "class value" - -reveal_type(C2.attr) # revealed: Literal["class value"] -``` - -If the class-level attribute is only partially defined, we union the metaclass attribute with the -class-level attribute: - -```py -def _(flag: bool): - class Meta3: - attr1 = "metaclass value" - attr2: Literal["metaclass value"] = "metaclass value" - - class C3(metaclass=Meta3): - if flag: - attr1 = "class value" - # TODO: Neither mypy nor pyright show an error here, but we could consider emitting a conflicting-declaration diagnostic here. - attr2: Literal["class value"] = "class value" - - reveal_type(C3.attr1) # revealed: Unknown | Literal["metaclass value", "class value"] - reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"] -``` - -If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute` -diagnostic: - -```py -def _(flag: bool): - class Meta4: - if flag: - attr1: str = "metaclass value" - - class C4(metaclass=Meta4): ... - # error: [possibly-unbound-attribute] - reveal_type(C4.attr1) # revealed: str -``` - -Finally, if both the metaclass attribute and the class-level attribute are only partially defined, -we union them and emit a `possibly-unbound-attribute` diagnostic: - -```py -def _(flag1: bool, flag2: bool): - class Meta5: - if flag1: - attr1 = "metaclass value" - - class C5(metaclass=Meta5): - if flag2: - attr1 = "class value" - - # error: [possibly-unbound-attribute] - reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"] -``` - -## Unions of attributes - -If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we -infer those union types accordingly: - -```py -def _(flag: bool): - if flag: - class C1: - x = 1 - y: int = 1 - - else: - class C1: - x = 2 - y: int | str = "b" - - reveal_type(C1.x) # revealed: Unknown | Literal[1, 2] - reveal_type(C1.y) # revealed: int | str - - C1.y = 100 - # error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `Literal[C1, C1]`" - C1.y = "problematic" - - class C2: - if flag: - x = 3 - y: int = 3 - else: - x = 4 - y: int | str = "d" - - reveal_type(C2.x) # revealed: Unknown | Literal[3, 4] - reveal_type(C2.y) # revealed: int | str - - C2.y = 100 - # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`" - C2.y = None - # TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment` - C2.y = "problematic" - - if flag: - class Meta3(type): - x = 5 - y: int = 5 - - else: - class Meta3(type): - x = 6 - y: int | str = "f" - - class C3(metaclass=Meta3): ... - reveal_type(C3.x) # revealed: Unknown | Literal[5, 6] - reveal_type(C3.y) # revealed: int | str - - C3.y = 100 - # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`" - C3.y = None - # TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment` - C3.y = "problematic" - - class Meta4(type): - if flag: - x = 7 - y: int = 7 - else: - x = 8 - y: int | str = "h" - - class C4(metaclass=Meta4): ... - reveal_type(C4.x) # revealed: Unknown | Literal[7, 8] - reveal_type(C4.y) # revealed: int | str - - C4.y = 100 - # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`" - C4.y = None - # TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment` - C4.y = "problematic" -``` - -## Unions with possibly unbound paths - -### Definite boundness within a class - -In this example, the `x` attribute is not defined in the `C2` element of the union: - -```py -def _(flag1: bool, flag2: bool): - class C1: - x = 1 - - class C2: ... - - class C3: - x = 3 - - C = C1 if flag1 else C2 if flag2 else C3 - - # error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound" - reveal_type(C.x) # revealed: Unknown | Literal[1, 3] - - # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `Literal[C1, C2, C3]`" - C.x = 100 - - # error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound" - reveal_type(C().x) # revealed: Unknown | Literal[1, 3] - - # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`" - C().x = 100 -``` - -### Possibly-unbound within a class - -We raise the same diagnostic if the attribute is possibly-unbound in at least one element of the -union: - -```py -def _(flag: bool, flag1: bool, flag2: bool): - class C1: - x = 1 - - class C2: - if flag: - x = 2 - - class C3: - x = 3 - - C = C1 if flag1 else C2 if flag2 else C3 - - # error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound" - reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3] - - # error: [possibly-unbound-attribute] - C.x = 100 - - # Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually, - # see the "Possibly unbound/undeclared instance attribute" section below. - # error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound" - reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3] - - # error: [possibly-unbound-attribute] - C().x = 100 -``` - -### Possibly-unbound within gradual types - -```py -from typing import Any - -def _(flag: bool): - class Base: - x: Any - - class Derived(Base): - if flag: - # Redeclaring `x` with a more static type is okay in terms of LSP. - x: int - - reveal_type(Derived().x) # revealed: int | Any - - Derived().x = 1 - Derived().x = "a" -``` - -### Attribute possibly unbound on a subclass but not on a superclass - -```py -def _(flag: bool): - class Foo: - x = 1 - - class Bar(Foo): - if flag: - x = 2 - - reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1] - Bar.x = 3 - - reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1] - Bar().x = 3 -``` - -### Attribute possibly unbound on a subclass and on a superclass - -```py -def _(flag: bool): - class Foo: - if flag: - x = 1 - - class Bar(Foo): - if flag: - x = 2 - - # error: [possibly-unbound-attribute] - reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1] - - # error: [possibly-unbound-attribute] - Bar.x = 3 - - # error: [possibly-unbound-attribute] - reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1] - - # error: [possibly-unbound-attribute] - Bar().x = 3 -``` - -### Possibly unbound/undeclared instance attribute - -#### Possibly unbound and undeclared - -```py -def _(flag: bool): - class Foo: - if flag: - x: int - - def __init(self): - if flag: - self.x = 1 - - # error: [possibly-unbound-attribute] - reveal_type(Foo().x) # revealed: int | Unknown - - # error: [possibly-unbound-attribute] - Foo().x = 1 -``` - -#### Possibly unbound - -```py -def _(flag: bool): - class Foo: - def __init(self): - if flag: - self.x = 1 - self.y = "a" - else: - self.y = "b" - - # error: [possibly-unbound-attribute] - reveal_type(Foo().x) # revealed: Unknown | Literal[1] - - # error: [possibly-unbound-attribute] - Foo().x = 2 - - reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"] - Foo().y = "c" -``` - -### Unions with all paths unbound - -If the symbol is unbound in all elements of the union, we detect that: - -```py -def _(flag: bool): - class C1: ... - class C2: ... - C = C1 if flag else C2 - - # error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`" - reveal_type(C.x) # revealed: Unknown - - # TODO: This should ideally be a `unresolved-attribute` error. We need better union - # handling in `validate_attribute_assignment` for this. - # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type `Literal[C1, C2]`" - C.x = 1 -``` - -## Inherited class attributes - -### Basic - -```py -class A: - X = "foo" - -class B(A): ... -class C(B): ... - -reveal_type(C.X) # revealed: Unknown | Literal["foo"] - -C.X = "bar" -``` - -### Multiple inheritance - -```py -class O: ... - -class F(O): - X = 56 - -class E(O): - X = 42 - -class D(O): ... -class C(D, F): ... -class B(E, D): ... -class A(B, C): ... - -# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]] -reveal_type(A.__mro__) - -# `E` is earlier in the MRO than `F`, so we should use the type of `E.X` -reveal_type(A.X) # revealed: Unknown | Literal[42] - -A.X = 100 -``` - -## Intersections of attributes - -### Attribute only available on one element - -```py -from knot_extensions import Intersection - -class A: - x: int = 1 - -class B: ... - -def _(a_and_b: Intersection[A, B]): - reveal_type(a_and_b.x) # revealed: int - - a_and_b.x = 2 - -# Same for class objects -def _(a_and_b: Intersection[type[A], type[B]]): - reveal_type(a_and_b.x) # revealed: int - - a_and_b.x = 2 -``` - -### Attribute available on both elements - -```py -from knot_extensions import Intersection - -class P: ... -class Q: ... -class R(P, Q): ... - -class A: - x: P = P() - -class B: - x: Q = Q() - -def _(a_and_b: Intersection[A, B]): - reveal_type(a_and_b.x) # revealed: P & Q - a_and_b.x = R() - -# Same for class objects -def _(a_and_b: Intersection[type[A], type[B]]): - reveal_type(a_and_b.x) # revealed: P & Q - a_and_b.x = R() -``` - -### Possible unboundness - -```py -from knot_extensions import Intersection - -class P: ... -class Q: ... -class R(P, Q): ... - -def _(flag: bool): - class A1: - if flag: - x: P = P() - - class B1: ... - - def inner1(a_and_b: Intersection[A1, B1]): - # error: [possibly-unbound-attribute] - reveal_type(a_and_b.x) # revealed: P - - # error: [possibly-unbound-attribute] - a_and_b.x = R() - # Same for class objects - def inner1_class(a_and_b: Intersection[type[A1], type[B1]]): - # error: [possibly-unbound-attribute] - reveal_type(a_and_b.x) # revealed: P - - # error: [possibly-unbound-attribute] - a_and_b.x = R() - - class A2: - if flag: - x: P = P() - - class B1: - x: Q = Q() - - def inner2(a_and_b: Intersection[A2, B1]): - reveal_type(a_and_b.x) # revealed: P & Q - - # TODO: this should not be an error, we need better intersection - # handling in `validate_attribute_assignment` for this - # error: [possibly-unbound-attribute] - a_and_b.x = R() - # Same for class objects - def inner2_class(a_and_b: Intersection[type[A2], type[B1]]): - reveal_type(a_and_b.x) # revealed: P & Q - - class A3: - if flag: - x: P = P() - - class B3: - if flag: - x: Q = Q() - - def inner3(a_and_b: Intersection[A3, B3]): - # error: [possibly-unbound-attribute] - reveal_type(a_and_b.x) # revealed: P & Q - - # error: [possibly-unbound-attribute] - a_and_b.x = R() - # Same for class objects - def inner3_class(a_and_b: Intersection[type[A3], type[B3]]): - # error: [possibly-unbound-attribute] - reveal_type(a_and_b.x) # revealed: P & Q - - # error: [possibly-unbound-attribute] - a_and_b.x = R() - - class A4: ... - class B4: ... - - def inner4(a_and_b: Intersection[A4, B4]): - # error: [unresolved-attribute] - reveal_type(a_and_b.x) # revealed: Unknown - - # error: [invalid-assignment] - a_and_b.x = R() - # Same for class objects - def inner4_class(a_and_b: Intersection[type[A4], type[B4]]): - # error: [unresolved-attribute] - reveal_type(a_and_b.x) # revealed: Unknown - - # error: [invalid-assignment] - a_and_b.x = R() -``` - -### Intersection of implicit instance attributes - -```py -from knot_extensions import Intersection - -class P: ... -class Q: ... - -class A: - def __init__(self): - self.x: P = P() - -class B: - def __init__(self): - self.x: Q = Q() - -def _(a_and_b: Intersection[A, B]): - reveal_type(a_and_b.x) # revealed: P & Q -``` - -## Attribute access on `Any` - -The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows -from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on -`object` -- but if the attribute *does* exist on `object`, the type of the attribute is -` & Any`. - -```py -from typing import Any - -class Foo(Any): ... - -reveal_type(Foo.bar) # revealed: Any -reveal_type(Foo.__repr__) # revealed: (def __repr__(self) -> str) & Any -``` - -Similar principles apply if `Any` appears in the middle of an inheritance hierarchy: - -```py -from typing import ClassVar, Literal - -class A: - x: ClassVar[Literal[1]] = 1 - -class B(Any): ... -class C(B, A): ... - -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A], Literal[object]] -reveal_type(C.x) # revealed: Literal[1] & Any -``` - -## Classes with custom `__getattr__` methods - -### Basic - -If a type provides a custom `__getattr__` method, we use the return type of that method as the type -for unknown attributes. Consider the following `CustomGetAttr` class: - -```py -from typing import Literal - -def flag() -> bool: - return True - -class GetAttrReturnType: ... - -class CustomGetAttr: - class_attr: int = 1 - - if flag(): - possibly_unbound: bytes = b"a" - - def __init__(self) -> None: - self.instance_attr: str = "a" - - def __getattr__(self, name: str) -> GetAttrReturnType: - return GetAttrReturnType() -``` - -We can access arbitrary attributes on instances of this class, and the type of the attribute will be -`GetAttrReturnType`: - -```py -c = CustomGetAttr() - -reveal_type(c.whatever) # revealed: GetAttrReturnType -``` - -If an attribute is defined on the class, it takes precedence over the `__getattr__` method: - -```py -reveal_type(c.class_attr) # revealed: int -``` - -If the class attribute is possibly unbound, we union the type of the attribute with the fallback -type of the `__getattr__` method: - -```py -reveal_type(c.possibly_unbound) # revealed: bytes | GetAttrReturnType -``` - -Instance attributes also take precedence over the `__getattr__` method: - -```py -# Note: we could attempt to union with the fallback type of `__getattr__` here, as we currently do not -# attempt to determine if instance attributes are always bound or not. Neither mypy nor pyright do this, -# so it's not a priority. -reveal_type(c.instance_attr) # revealed: str -``` - -### Type of the `name` parameter - -If the `name` parameter of the `__getattr__` method is annotated with a (union of) literal type(s), -we only consider the attribute access to be valid if the accessed attribute is one of them: - -```py -from typing import Literal - -class Date: - def __getattr__(self, name: Literal["day", "month", "year"]) -> int: - return 0 - -date = Date() - -reveal_type(date.day) # revealed: int -reveal_type(date.month) # revealed: int -reveal_type(date.year) # revealed: int - -# error: [unresolved-attribute] "Type `Date` has no attribute `century`" -reveal_type(date.century) # revealed: Unknown -``` - -### `argparse.Namespace` - -A standard library example of a class with a custom `__getattr__` method is `argparse.Namespace`: - -```py -import argparse - -def _(ns: argparse.Namespace): - reveal_type(ns.whatever) # revealed: Any -``` - -## Classes with custom `__setattr__` methods - -### Basic - -If a type provides a custom `__setattr__` method, we use the parameter type of that method as the -type to validate attribute assignments. Consider the following `CustomSetAttr` class: - -```py -class CustomSetAttr: - def __setattr__(self, name: str, value: int) -> None: - pass -``` - -We can set arbitrary attributes on instances of this class: - -```py -c = CustomSetAttr() - -c.whatever = 42 -``` - -### Type of the `name` parameter - -If the `name` parameter of the `__setattr__` method is annotated with a (union of) literal type(s), -we only consider the attribute assignment to be valid if the assigned attribute is one of them: - -```py -from typing import Literal - -class Date: - def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: - pass - -date = Date() -date.day = 8 -date.month = 4 -date.year = 2025 - -# error: [unresolved-attribute] "Can not assign object of `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method." -date.tz = "UTC" -``` - -### `argparse.Namespace` - -A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`: - -```py -import argparse - -def _(ns: argparse.Namespace): - ns.whatever = 42 -``` - -## Objects of all types have a `__class__` method - -The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as -`type(x)`. - -```py -import typing_extensions - -reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType] -reveal_type(type(typing_extensions)) # revealed: Literal[ModuleType] - -a = 42 -reveal_type(a.__class__) # revealed: Literal[int] -reveal_type(type(a)) # revealed: Literal[int] - -b = "42" -reveal_type(b.__class__) # revealed: Literal[str] - -c = b"42" -reveal_type(c.__class__) # revealed: Literal[bytes] - -d = True -reveal_type(d.__class__) # revealed: Literal[bool] - -e = (42, 42) -reveal_type(e.__class__) # revealed: Literal[tuple] - -def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): - reveal_type(a.__class__) # revealed: type[int] - reveal_type(type(a)) # revealed: type[int] - - reveal_type(b.__class__) # revealed: Literal[str] - reveal_type(type(b)) # revealed: Literal[str] - - reveal_type(c.__class__) # revealed: type[int] | type[str] - reveal_type(type(c)) # revealed: type[int] | type[str] - - # `type[type]`, a.k.a., either the class `type` or some subclass of `type`. - # It would be incorrect to infer `Literal[type]` here, - # as `c` could be some subclass of `str` with a custom metaclass. - # All we know is that the metaclass must be a (non-strict) subclass of `type`. - reveal_type(d.__class__) # revealed: type[type] - -reveal_type(f.__class__) # revealed: Literal[FunctionType] - -class Foo: ... - -reveal_type(Foo.__class__) # revealed: Literal[type] -``` - -## Module attributes - -### Basic - -`mod.py`: - -```py -global_symbol: str = "a" -``` - -```py -import mod - -reveal_type(mod.global_symbol) # revealed: str -mod.global_symbol = "b" - -# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`" -mod.global_symbol = 1 - -# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`" -(_, mod.global_symbol) = (..., 1) - -# TODO: this should be an error, but we do not understand list unpackings yet. -[_, mod.global_symbol] = [1, 2] - -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -# error: [invalid-assignment] "Object of type `int` is not assignable to attribute `global_symbol` of type `str`" -for mod.global_symbol in IntIterable(): - pass -``` - -### Nested module attributes - -`outer/__init__.py`: - -```py -``` - -`outer/nested/__init__.py`: - -```py -``` - -`outer/nested/inner.py`: - -```py -class Outer: - class Nested: - class Inner: - attr: int = 1 -``` - -```py -import outer.nested.inner - -reveal_type(outer.nested.inner.Outer.Nested.Inner.attr) # revealed: int - -# error: [invalid-assignment] -outer.nested.inner.Outer.Nested.Inner.attr = "a" -``` - -## Literal types - -### Function-literal attributes - -Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all -functions are instances of that class: - -```py -def f(): ... - -reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None -reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None -``` - -Some attributes are special-cased, however: - -```py -reveal_type(f.__get__) # revealed: -reveal_type(f.__call__) # revealed: -``` - -### Int-literal attributes - -Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal -integers are instances of that class: - -```py -reveal_type((2).bit_length) # revealed: bound method Literal[2].bit_length() -> int -reveal_type((2).denominator) # revealed: Literal[1] -``` - -Some attributes are special-cased, however: - -```py -reveal_type((2).numerator) # revealed: Literal[2] -reveal_type((2).real) # revealed: Literal[2] -``` - -### Bool-literal attributes - -Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal -bools are instances of that class: - -```py -# revealed: bound method Literal[True].__and__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function) -reveal_type(True.__and__) -# revealed: bound method Literal[False].__or__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function) -reveal_type(False.__or__) -``` - -Some attributes are special-cased, however: - -```py -reveal_type(True.numerator) # revealed: Literal[1] -reveal_type(False.real) # revealed: Literal[0] -``` - -### Bytes-literal attributes - -All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: - -```py -reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(generics), /) -> bytes -# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool -reveal_type(b"foo".endswith) -``` - -## Instance attribute edge cases - -### Assignment to attribute that does not correspond to the instance - -```py -class Other: - x: int = 1 - -class C: - def __init__(self, other: Other) -> None: - other.x = 1 - -def f(c: C): - # error: [unresolved-attribute] - reveal_type(c.x) # revealed: Unknown -``` - -### Nested classes - -```py -class Outer: - def __init__(self): - self.x: int = 1 - - class Middle: - # has no 'x' attribute - - class Inner: - def __init__(self): - self.x: str = "a" - -reveal_type(Outer().x) # revealed: int - -# error: [unresolved-attribute] -Outer.Middle().x - -reveal_type(Outer.Middle.Inner().x) # revealed: str -``` - -### Shadowing of `self` - -```py -class Other: - x: int = 1 - -class C: - def __init__(self) -> None: - # Redeclaration of self. `self` does not refer to the instance anymore. - self: Other = Other() - self.x: int = 1 - -# TODO: this should be an error -C().x -``` - -### Assignment to `self` after nested function - -```py -class Other: - x: str = "a" - -class C: - def __init__(self) -> None: - def nested_function(self: Other): - self.x = "b" - self.x: int = 1 - -reveal_type(C().x) # revealed: int -``` - -### Assignment to `self` from nested function - -```py -class C: - def __init__(self) -> None: - def set_attribute(value: str): - self.x: str = value - set_attribute("a") - -# TODO: ideally, this would be `str`. Mypy supports this, pyright does not. -# error: [unresolved-attribute] -reveal_type(C().x) # revealed: Unknown -``` - -### Accessing attributes on `Never` - -Arbitrary attributes can be accessed on `Never` without emitting any errors: - -```py -from typing_extensions import Never - -def f(never: Never): - reveal_type(never.arbitrary_attribute) # revealed: Never - - # Assigning `Never` to an attribute on `Never` is also allowed: - never.another_attribute = never -``` - -### Builtin types attributes - -This test can probably be removed eventually, but we currently include it because we do not yet -understand generic bases and protocols, and we want to make sure that we can still use builtin types -in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more -information. - -```py -class C: - a_int: int = 1 - a_str: str = "a" - a_bytes: bytes = b"a" - a_bool: bool = True - a_float: float = 1.0 - a_complex: complex = 1 + 1j - a_tuple: tuple[int] = (1,) - a_range: range = range(1) - a_slice: slice = slice(1) - a_type: type = int - a_none: None = None - -reveal_type(C.a_int) # revealed: int -reveal_type(C.a_str) # revealed: str -reveal_type(C.a_bytes) # revealed: bytes -reveal_type(C.a_bool) # revealed: bool -reveal_type(C.a_float) # revealed: int | float -reveal_type(C.a_complex) # revealed: int | float | complex -reveal_type(C.a_tuple) # revealed: tuple[int] -reveal_type(C.a_range) # revealed: range -reveal_type(C.a_slice) # revealed: slice -reveal_type(C.a_type) # revealed: type -reveal_type(C.a_none) # revealed: None -``` - -## Enum classes - -Enums are not supported yet; attribute access on an enum class is inferred as `Todo`. - -```py -import enum - -reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes) - -class Foo(enum.Enum): - BAR = 1 - -reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes) -reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes) -reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes) -``` - -## References - -Some of the tests in the *Class and instance variables* section draw inspiration from -[pyright's documentation] on this topic. - -[descriptor protocol tests]: descriptor_protocol.md -[pyright's documentation]: https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables -[typing spec on `classvar`]: https://typing.python.org/en/latest/spec/class-compat.html#classvar -[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md b/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md deleted file mode 100644 index 1e62ce8920688..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md +++ /dev/null @@ -1,93 +0,0 @@ -## Binary operations on booleans - -## Basic Arithmetic - -We try to be precise and all operations except for division will result in Literal type. - -```py -a = True -b = False - -reveal_type(a + a) # revealed: Literal[2] -reveal_type(a + b) # revealed: Literal[1] -reveal_type(b + a) # revealed: Literal[1] -reveal_type(b + b) # revealed: Literal[0] - -reveal_type(a - a) # revealed: Literal[0] -reveal_type(a - b) # revealed: Literal[1] -reveal_type(b - a) # revealed: Literal[-1] -reveal_type(b - b) # revealed: Literal[0] - -reveal_type(a * a) # revealed: Literal[1] -reveal_type(a * b) # revealed: Literal[0] -reveal_type(b * a) # revealed: Literal[0] -reveal_type(b * b) # revealed: Literal[0] - -reveal_type(a % a) # revealed: Literal[0] -reveal_type(b % a) # revealed: Literal[0] - -reveal_type(a // a) # revealed: Literal[1] -reveal_type(b // a) # revealed: Literal[0] - -reveal_type(a**a) # revealed: Literal[1] -reveal_type(a**b) # revealed: Literal[1] -reveal_type(b**a) # revealed: Literal[0] -reveal_type(b**b) # revealed: Literal[1] - -# Division -reveal_type(a / a) # revealed: float -reveal_type(b / a) # revealed: float -b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero" -a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" - -# bitwise OR -reveal_type(a | a) # revealed: Literal[True] -reveal_type(a | b) # revealed: Literal[True] -reveal_type(b | a) # revealed: Literal[True] -reveal_type(b | b) # revealed: Literal[False] -``` - -## Arithmetic with a variable - -```py -def _(a: bool): - def lhs_is_int(x: int): - reveal_type(x + a) # revealed: int - reveal_type(x - a) # revealed: int - reveal_type(x * a) # revealed: int - reveal_type(x // a) # revealed: int - reveal_type(x / a) # revealed: int | float - reveal_type(x % a) # revealed: int - - def rhs_is_int(x: int): - reveal_type(a + x) # revealed: int - reveal_type(a - x) # revealed: int - reveal_type(a * x) # revealed: int - reveal_type(a // x) # revealed: int - reveal_type(a / x) # revealed: int | float - reveal_type(a % x) # revealed: int - - def lhs_is_bool(x: bool): - reveal_type(x + a) # revealed: int - reveal_type(x - a) # revealed: int - reveal_type(x * a) # revealed: int - reveal_type(x // a) # revealed: int - reveal_type(x / a) # revealed: int | float - reveal_type(x % a) # revealed: int - - def rhs_is_bool(x: bool): - reveal_type(a + x) # revealed: int - reveal_type(a - x) # revealed: int - reveal_type(a * x) # revealed: int - reveal_type(a // x) # revealed: int - reveal_type(a / x) # revealed: int | float - reveal_type(a % x) # revealed: int - - def both_are_bool(x: bool, y: bool): - reveal_type(x + y) # revealed: int - reveal_type(x - y) # revealed: int - reveal_type(x * y) # revealed: int - reveal_type(x // y) # revealed: int - reveal_type(x / y) # revealed: int | float - reveal_type(x % y) # revealed: int -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md b/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md deleted file mode 100644 index 464b2d3e7db92..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md +++ /dev/null @@ -1,27 +0,0 @@ -# Binary operations on classes - -## Union of two classes - -Unioning two classes via the `|` operator is only available in Python 3.10 and later. - -```toml -[environment] -python-version = "3.10" -``` - -```py -class A: ... -class B: ... - -reveal_type(A | B) # revealed: UnionType -``` - -## Union of two classes (prior to 3.10) - -```py -class A: ... -class B: ... - -# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`" -reveal_type(A | B) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md b/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md deleted file mode 100644 index 8b0b6741e5c2a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md +++ /dev/null @@ -1,379 +0,0 @@ -# Custom binary operations - -## Class instances - -```py -from typing import Literal - -class Yes: - def __add__(self, other) -> Literal["+"]: - return "+" - - def __sub__(self, other) -> Literal["-"]: - return "-" - - def __mul__(self, other) -> Literal["*"]: - return "*" - - def __matmul__(self, other) -> Literal["@"]: - return "@" - - def __truediv__(self, other) -> Literal["/"]: - return "/" - - def __mod__(self, other) -> Literal["%"]: - return "%" - - def __pow__(self, other) -> Literal["**"]: - return "**" - - def __lshift__(self, other) -> Literal["<<"]: - return "<<" - - def __rshift__(self, other) -> Literal[">>"]: - return ">>" - - def __or__(self, other) -> Literal["|"]: - return "|" - - def __xor__(self, other) -> Literal["^"]: - return "^" - - def __and__(self, other) -> Literal["&"]: - return "&" - - def __floordiv__(self, other) -> Literal["//"]: - return "//" - -class Sub(Yes): ... -class No: ... - -# Yes implements all of the dunder methods. -reveal_type(Yes() + Yes()) # revealed: Literal["+"] -reveal_type(Yes() - Yes()) # revealed: Literal["-"] -reveal_type(Yes() * Yes()) # revealed: Literal["*"] -reveal_type(Yes() @ Yes()) # revealed: Literal["@"] -reveal_type(Yes() / Yes()) # revealed: Literal["/"] -reveal_type(Yes() % Yes()) # revealed: Literal["%"] -reveal_type(Yes() ** Yes()) # revealed: Literal["**"] -reveal_type(Yes() << Yes()) # revealed: Literal["<<"] -reveal_type(Yes() >> Yes()) # revealed: Literal[">>"] -reveal_type(Yes() | Yes()) # revealed: Literal["|"] -reveal_type(Yes() ^ Yes()) # revealed: Literal["^"] -reveal_type(Yes() & Yes()) # revealed: Literal["&"] -reveal_type(Yes() // Yes()) # revealed: Literal["//"] - -# Sub inherits Yes's implementation of the dunder methods. -reveal_type(Sub() + Sub()) # revealed: Literal["+"] -reveal_type(Sub() - Sub()) # revealed: Literal["-"] -reveal_type(Sub() * Sub()) # revealed: Literal["*"] -reveal_type(Sub() @ Sub()) # revealed: Literal["@"] -reveal_type(Sub() / Sub()) # revealed: Literal["/"] -reveal_type(Sub() % Sub()) # revealed: Literal["%"] -reveal_type(Sub() ** Sub()) # revealed: Literal["**"] -reveal_type(Sub() << Sub()) # revealed: Literal["<<"] -reveal_type(Sub() >> Sub()) # revealed: Literal[">>"] -reveal_type(Sub() | Sub()) # revealed: Literal["|"] -reveal_type(Sub() ^ Sub()) # revealed: Literal["^"] -reveal_type(Sub() & Sub()) # revealed: Literal["&"] -reveal_type(Sub() // Sub()) # revealed: Literal["//"] - -# No does not implement any of the dunder methods. -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`" -reveal_type(No() + No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`" -reveal_type(No() - No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`" -reveal_type(No() * No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`" -reveal_type(No() @ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`" -reveal_type(No() / No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`" -reveal_type(No() % No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`" -reveal_type(No() ** No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`" -reveal_type(No() << No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`" -reveal_type(No() >> No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`" -reveal_type(No() | No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`" -reveal_type(No() ^ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`" -reveal_type(No() & No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`" -reveal_type(No() // No()) # revealed: Unknown - -# Yes does not implement any of the reflected dunder methods. -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() + Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() - Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() * Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() @ Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() / Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() % Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() ** Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() << Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() >> Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() | Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() ^ Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() & Yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" -reveal_type(No() // Yes()) # revealed: Unknown -``` - -## Subclass reflections override superclass dunders - -```py -from typing import Literal - -class Yes: - def __add__(self, other) -> Literal["+"]: - return "+" - - def __sub__(self, other) -> Literal["-"]: - return "-" - - def __mul__(self, other) -> Literal["*"]: - return "*" - - def __matmul__(self, other) -> Literal["@"]: - return "@" - - def __truediv__(self, other) -> Literal["/"]: - return "/" - - def __mod__(self, other) -> Literal["%"]: - return "%" - - def __pow__(self, other) -> Literal["**"]: - return "**" - - def __lshift__(self, other) -> Literal["<<"]: - return "<<" - - def __rshift__(self, other) -> Literal[">>"]: - return ">>" - - def __or__(self, other) -> Literal["|"]: - return "|" - - def __xor__(self, other) -> Literal["^"]: - return "^" - - def __and__(self, other) -> Literal["&"]: - return "&" - - def __floordiv__(self, other) -> Literal["//"]: - return "//" - -class Sub(Yes): - def __radd__(self, other) -> Literal["r+"]: - return "r+" - - def __rsub__(self, other) -> Literal["r-"]: - return "r-" - - def __rmul__(self, other) -> Literal["r*"]: - return "r*" - - def __rmatmul__(self, other) -> Literal["r@"]: - return "r@" - - def __rtruediv__(self, other) -> Literal["r/"]: - return "r/" - - def __rmod__(self, other) -> Literal["r%"]: - return "r%" - - def __rpow__(self, other) -> Literal["r**"]: - return "r**" - - def __rlshift__(self, other) -> Literal["r<<"]: - return "r<<" - - def __rrshift__(self, other) -> Literal["r>>"]: - return "r>>" - - def __ror__(self, other) -> Literal["r|"]: - return "r|" - - def __rxor__(self, other) -> Literal["r^"]: - return "r^" - - def __rand__(self, other) -> Literal["r&"]: - return "r&" - - def __rfloordiv__(self, other) -> Literal["r//"]: - return "r//" - -class No: - def __radd__(self, other) -> Literal["r+"]: - return "r+" - - def __rsub__(self, other) -> Literal["r-"]: - return "r-" - - def __rmul__(self, other) -> Literal["r*"]: - return "r*" - - def __rmatmul__(self, other) -> Literal["r@"]: - return "r@" - - def __rtruediv__(self, other) -> Literal["r/"]: - return "r/" - - def __rmod__(self, other) -> Literal["r%"]: - return "r%" - - def __rpow__(self, other) -> Literal["r**"]: - return "r**" - - def __rlshift__(self, other) -> Literal["r<<"]: - return "r<<" - - def __rrshift__(self, other) -> Literal["r>>"]: - return "r>>" - - def __ror__(self, other) -> Literal["r|"]: - return "r|" - - def __rxor__(self, other) -> Literal["r^"]: - return "r^" - - def __rand__(self, other) -> Literal["r&"]: - return "r&" - - def __rfloordiv__(self, other) -> Literal["r//"]: - return "r//" - -# Subclass reflected dunder methods take precedence over the superclass's regular dunders. -reveal_type(Yes() + Sub()) # revealed: Literal["r+"] -reveal_type(Yes() - Sub()) # revealed: Literal["r-"] -reveal_type(Yes() * Sub()) # revealed: Literal["r*"] -reveal_type(Yes() @ Sub()) # revealed: Literal["r@"] -reveal_type(Yes() / Sub()) # revealed: Literal["r/"] -reveal_type(Yes() % Sub()) # revealed: Literal["r%"] -reveal_type(Yes() ** Sub()) # revealed: Literal["r**"] -reveal_type(Yes() << Sub()) # revealed: Literal["r<<"] -reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"] -reveal_type(Yes() | Sub()) # revealed: Literal["r|"] -reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"] -reveal_type(Yes() & Sub()) # revealed: Literal["r&"] -reveal_type(Yes() // Sub()) # revealed: Literal["r//"] - -# But for an unrelated class, the superclass regular dunders are used. -reveal_type(Yes() + No()) # revealed: Literal["+"] -reveal_type(Yes() - No()) # revealed: Literal["-"] -reveal_type(Yes() * No()) # revealed: Literal["*"] -reveal_type(Yes() @ No()) # revealed: Literal["@"] -reveal_type(Yes() / No()) # revealed: Literal["/"] -reveal_type(Yes() % No()) # revealed: Literal["%"] -reveal_type(Yes() ** No()) # revealed: Literal["**"] -reveal_type(Yes() << No()) # revealed: Literal["<<"] -reveal_type(Yes() >> No()) # revealed: Literal[">>"] -reveal_type(Yes() | No()) # revealed: Literal["|"] -reveal_type(Yes() ^ No()) # revealed: Literal["^"] -reveal_type(Yes() & No()) # revealed: Literal["&"] -reveal_type(Yes() // No()) # revealed: Literal["//"] -``` - -## Classes - -Dunder methods defined in a class are available to instances of that class, but not to the class -itself. (For these operators to work on the class itself, they would have to be defined on the -class's type, i.e. `type`.) - -```py -from typing import Literal - -class Yes: - def __add__(self, other) -> Literal["+"]: - return "+" - -class Sub(Yes): ... -class No: ... - -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`" -reveal_type(Yes + Yes) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`" -reveal_type(Sub + Sub) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`" -reveal_type(No + No) # revealed: Unknown -``` - -## Subclass - -```py -from typing import Literal - -class Yes: - def __add__(self, other) -> Literal["+"]: - return "+" - -class Sub(Yes): ... -class No: ... - -def yes() -> type[Yes]: - return Yes - -def sub() -> type[Sub]: - return Sub - -def no() -> type[No]: - return No - -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`" -reveal_type(yes() + yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`" -reveal_type(sub() + sub()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`" -reveal_type(no() + no()) # revealed: Unknown -``` - -## Function literals - -```py -def f(): - pass - -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f + f) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f - f) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f * f) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f @ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f / f) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f % f) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f**f) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f << f) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f >> f) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f | f) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f ^ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f & f) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" -reveal_type(f // f) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md deleted file mode 100644 index a4172c821dce9..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ /dev/null @@ -1,110 +0,0 @@ -# Binary operations on integers - -## Basic Arithmetic - -```py -reveal_type(2 + 1) # revealed: Literal[3] -reveal_type(3 - 4) # revealed: Literal[-1] -reveal_type(3 * -1) # revealed: Literal[-3] -reveal_type(-3 // 3) # revealed: Literal[-1] -reveal_type(-3 / 3) # revealed: float -reveal_type(5 % 3) # revealed: Literal[2] - -# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`" -reveal_type(2 + "f") # revealed: Unknown - -def lhs(x: int): - reveal_type(x + 1) # revealed: int - reveal_type(x - 4) # revealed: int - reveal_type(x * -1) # revealed: int - reveal_type(x // 3) # revealed: int - reveal_type(x / 3) # revealed: int | float - reveal_type(x % 3) # revealed: int - -def rhs(x: int): - reveal_type(2 + x) # revealed: int - reveal_type(3 - x) # revealed: int - reveal_type(3 * x) # revealed: int - reveal_type(-3 // x) # revealed: int - reveal_type(-3 / x) # revealed: int | float - reveal_type(5 % x) # revealed: int - -def both(x: int): - reveal_type(x + x) # revealed: int - reveal_type(x - x) # revealed: int - reveal_type(x * x) # revealed: int - reveal_type(x // x) # revealed: int - reveal_type(x / x) # revealed: int | float - reveal_type(x % x) # revealed: int -``` - -## Power - -For power if the result fits in the int literal type it will be a Literal type. Otherwise the -outcome is int. - -```py -largest_u32 = 4_294_967_295 -reveal_type(2**2) # revealed: Literal[4] -reveal_type(1 ** (largest_u32 + 1)) # revealed: int -reveal_type(2**largest_u32) # revealed: int - -def variable(x: int): - reveal_type(x**2) # revealed: @Todo(return type of overloaded function) - reveal_type(2**x) # revealed: @Todo(return type of overloaded function) - reveal_type(x**x) # revealed: @Todo(return type of overloaded function) -``` - -If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but -the second argument is >=0, an `int` is still returned: - -```py -reveal_type(1**0) # revealed: Literal[1] -reveal_type(0**1) # revealed: Literal[0] -reveal_type(0**0) # revealed: Literal[1] -reveal_type((-1) ** 2) # revealed: Literal[1] -reveal_type(2 ** (-1)) # revealed: float -reveal_type((-1) ** (-1)) # revealed: float -``` - -## Division by Zero - -This error is really outside the current Python type system, because e.g. `int.__truediv__` and -friends are not annotated to indicate that it's an error, and we don't even have a facility to -permit such an annotation. So arguably divide-by-zero should be a lint error rather than a type -checker error. But we choose to go ahead and error in the cases that are very likely to be an error: -dividing something typed as `int` or `float` by something known to be `Literal[0]`. - -This isn't _definitely_ an error, because the object typed as `int` or `float` could be an instance -of a custom subclass which overrides division behavior to handle zero without error. But if this -unusual case occurs, the error can be avoided by explicitly typing the dividend as that safe custom -subclass; we only emit the error if the LHS type is exactly `int` or `float`, not if its a subclass. - -```py -a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero" -reveal_type(a) # revealed: float - -b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero" -reveal_type(b) # revealed: int - -c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero" -reveal_type(c) # revealed: int - -# error: "Cannot divide object of type `int` by zero" -reveal_type(int() / 0) # revealed: int | float - -# error: "Cannot divide object of type `Literal[1]` by zero" -reveal_type(1 / False) # revealed: float -# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" -True / False -# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" -bool(1) / False - -# error: "Cannot divide object of type `float` by zero" -reveal_type(1.0 / 0) # revealed: int | float - -class MyInt(int): ... - -# No error for a subclass of int -reveal_type(MyInt(3) / 0) # revealed: int | float -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/tuples.md b/crates/red_knot_python_semantic/resources/mdtest/binary/tuples.md deleted file mode 100644 index 49fb5b7333a2b..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/tuples.md +++ /dev/null @@ -1,22 +0,0 @@ -# Binary operations on tuples - -## Concatenation for heterogeneous tuples - -```py -reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]] -reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]] -reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]] -reveal_type(() + ()) # revealed: tuple[()] - -def _(x: tuple[int, str], y: tuple[None, tuple[int]]): - reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]] - reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str] -``` - -## Concatenation for homogeneous tuples - -```py -def _(x: tuple[int, ...], y: tuple[str, ...]): - reveal_type(x + y) # revealed: @Todo(full tuple[...] support) - reveal_type(x + (1, 2)) # revealed: @Todo(full tuple[...] support) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md deleted file mode 100644 index a7b9935ee67ed..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md +++ /dev/null @@ -1,101 +0,0 @@ -# Calling builtins - -## `bool` with incorrect arguments - -```py -class NotBool: - __bool__ = None - -# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2" -bool(1, 2) - -# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly. -bool(NotBool()) -``` - -## Calls to `type()` - -A single-argument call to `type()` returns an object that has the argument's meta-type. (This is -tested more extensively in `crates/red_knot_python_semantic/resources/mdtest/attributes.md`, -alongside the tests for the `__class__` attribute.) - -```py -reveal_type(type(1)) # revealed: Literal[int] -``` - -But a three-argument call to type creates a dynamic instance of the `type` class: - -```py -class Base: ... - -reveal_type(type("Foo", (), {})) # revealed: type - -reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type -``` - -Other numbers of arguments are invalid - -```py -# error: [no-matching-overload] "No overload of class `type` matches arguments" -type("Foo", ()) - -# error: [no-matching-overload] "No overload of class `type` matches arguments" -type("Foo", (), {}, weird_other_arg=42) -``` - -The following calls are also invalid, due to incorrect argument types: - -```py -class Base: ... - -# error: [no-matching-overload] "No overload of class `type` matches arguments" -type(b"Foo", (), {}) - -# error: [no-matching-overload] "No overload of class `type` matches arguments" -type("Foo", Base, {}) - -# TODO: this should be an error -type("Foo", (1, 2), {}) - -# TODO: this should be an error -type("Foo", (Base,), {b"attr": 1}) -``` - -## Calls to `str()` - -### Valid calls - -```py -str() -str("") -str(b"") -str(1) -str(object=1) - -str(b"M\xc3\xbcsli", "utf-8") -str(b"M\xc3\xbcsli", "utf-8", "replace") - -str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16") -str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore") - -str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8") -str(bytearray(), "utf-8") - -str(encoding="utf-8", object=b"M\xc3\xbcsli") -str(b"", errors="replace") -str(encoding="utf-8") -str(errors="replace") -``` - -### Invalid calls - -```py -str(1, 2) # error: [no-matching-overload] -str(o=1) # error: [no-matching-overload] - -# First argument is not a bytes-like object: -str("Müsli", "utf-8") # error: [no-matching-overload] - -# Second argument is not a valid encoding: -str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md deleted file mode 100644 index 56a49fc13f97c..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md +++ /dev/null @@ -1,325 +0,0 @@ -# Constructor - -When classes are instantiated, Python calls the meta-class `__call__` method, which can either be -customized by the user or `type.__call__` is used. - -The latter calls the `__new__` method of the class, which is responsible for creating the instance -and then calls the `__init__` method on the resulting instance to initialize it with the same -arguments. - -Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then -called as an implicit static, rather than bound method with `cls` passed as the first argument. -`__init__` has no special handling, it is fetched as bound method and is called just like any other -dunder method. - -`type.__call__` does other things too, but this is not yet handled by us. - -Since every class has `object` in it's MRO, the default implementations are `object.__new__` and -`object.__init__`. They have some special behavior, namely: - -- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`) - \- no arguments are accepted and `TypeError` is raised if any are passed. -- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments! - -As of today there are a number of behaviors that we do not support: - -- `__new__` is assumed to return an instance of the class on which it is called -- User defined `__call__` on metaclass is ignored - -## Creating an instance of the `object` class itself - -Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods -as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves -differently depending on whether `__new__` is defined or not), we have to test the behavior of -`object` itself. - -```py -reveal_type(object()) # revealed: object - -# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1" -reveal_type(object(1)) # revealed: object -``` - -## No init or new - -```py -class Foo: ... - -reveal_type(Foo()) # revealed: Foo - -# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1" -reveal_type(Foo(1)) # revealed: Foo -``` - -## `__new__` present on the class itself - -```py -class Foo: - def __new__(cls, x: int) -> "Foo": - return object.__new__(cls) - -reveal_type(Foo(1)) # revealed: Foo - -# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" -reveal_type(Foo()) # revealed: Foo -# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" -reveal_type(Foo(1, 2)) # revealed: Foo -``` - -## `__new__` present on a superclass - -If the `__new__` method is defined on a superclass, we can still infer the signature of the -constructor from it. - -```py -from typing_extensions import Self - -class Base: - def __new__(cls, x: int) -> Self: ... - -class Foo(Base): ... - -reveal_type(Foo(1)) # revealed: Foo - -# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" -reveal_type(Foo()) # revealed: Foo -# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" -reveal_type(Foo(1, 2)) # revealed: Foo -``` - -## Conditional `__new__` - -```py -def _(flag: bool) -> None: - class Foo: - if flag: - def __new__(cls, x: int): ... - else: - def __new__(cls, x: int, y: int = 1): ... - - reveal_type(Foo(1)) # revealed: Foo - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`" - reveal_type(Foo("1")) # revealed: Foo - # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" - reveal_type(Foo()) # revealed: Foo - # error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" - reveal_type(Foo(1, 2)) # revealed: Foo -``` - -## A descriptor in place of `__new__` - -```py -class SomeCallable: - def __call__(self, cls, x: int) -> "Foo": - obj = object.__new__(cls) - obj.x = x - return obj - -class Descriptor: - def __get__(self, instance, owner) -> SomeCallable: - return SomeCallable() - -class Foo: - __new__: Descriptor = Descriptor() - -reveal_type(Foo(1)) # revealed: Foo -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" -reveal_type(Foo()) # revealed: Foo -``` - -## A callable instance in place of `__new__` - -### Bound - -```py -class Callable: - def __call__(self, cls, x: int) -> "Foo": - return object.__new__(cls) - -class Foo: - __new__ = Callable() - -reveal_type(Foo(1)) # revealed: Foo -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" -reveal_type(Foo()) # revealed: Foo -``` - -### Possibly Unbound - -```py -def _(flag: bool) -> None: - class Callable: - if flag: - def __call__(self, cls, x: int) -> "Foo": - return object.__new__(cls) - - class Foo: - __new__ = Callable() - - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" - reveal_type(Foo(1)) # revealed: Foo - # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" - # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" - reveal_type(Foo()) # revealed: Foo -``` - -## `__init__` present on the class itself - -If the class has an `__init__` method, we can infer the signature of the constructor from it. - -```py -class Foo: - def __init__(self, x: int): ... - -reveal_type(Foo(1)) # revealed: Foo - -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" -reveal_type(Foo()) # revealed: Foo -# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" -reveal_type(Foo(1, 2)) # revealed: Foo -``` - -## `__init__` present on a superclass - -If the `__init__` method is defined on a superclass, we can still infer the signature of the -constructor from it. - -```py -class Base: - def __init__(self, x: int): ... - -class Foo(Base): ... - -reveal_type(Foo(1)) # revealed: Foo - -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" -reveal_type(Foo()) # revealed: Foo -# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" -reveal_type(Foo(1, 2)) # revealed: Foo -``` - -## Conditional `__init__` - -```py -def _(flag: bool) -> None: - class Foo: - if flag: - def __init__(self, x: int): ... - else: - def __init__(self, x: int, y: int = 1): ... - - reveal_type(Foo(1)) # revealed: Foo - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`" - reveal_type(Foo("1")) # revealed: Foo - # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" - reveal_type(Foo()) # revealed: Foo - # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" - reveal_type(Foo(1, 2)) # revealed: Foo -``` - -## A descriptor in place of `__init__` - -```py -class SomeCallable: - # TODO: at runtime `__init__` is checked to return `None` and - # a `TypeError` is raised if it doesn't. However, apparently - # this is not true when the descriptor is used as `__init__`. - # However, we may still want to check this. - def __call__(self, x: int) -> str: - return "a" - -class Descriptor: - def __get__(self, instance, owner) -> SomeCallable: - return SomeCallable() - -class Foo: - __init__: Descriptor = Descriptor() - -reveal_type(Foo(1)) # revealed: Foo -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" -reveal_type(Foo()) # revealed: Foo -``` - -## A callable instance in place of `__init__` - -### Bound - -```py -class Callable: - def __call__(self, x: int) -> None: - pass - -class Foo: - __init__ = Callable() - -reveal_type(Foo(1)) # revealed: Foo -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" -reveal_type(Foo()) # revealed: Foo -``` - -### Possibly Unbound - -```py -def _(flag: bool) -> None: - class Callable: - if flag: - def __call__(self, x: int) -> None: - pass - - class Foo: - __init__ = Callable() - - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" - reveal_type(Foo(1)) # revealed: Foo - # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" - # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" - reveal_type(Foo()) # revealed: Foo -``` - -## `__new__` and `__init__` both present - -### Identical signatures - -A common case is to have `__new__` and `__init__` with identical signatures (except for the first -argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect. - -At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are -incorrect. However, we decided that it is better to report errors for both methods, since after -fixing the `__new__` method, the user may forget to fix the `__init__` method. - -```py -class Foo: - def __new__(cls, x: int) -> "Foo": - return object.__new__(cls) - - def __init__(self, x: int): ... - -# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" -reveal_type(Foo()) # revealed: Foo - -reveal_type(Foo(1)) # revealed: Foo -``` - -### Compatible signatures - -But they can also be compatible, but not identical. We should correctly report errors only for the -mthod that would fail. - -```py -class Foo: - def __new__(cls, *args, **kwargs): - return object.__new__(cls) - - def __init__(self, x: int) -> None: - self.x = x - -# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" -reveal_type(Foo()) # revealed: Foo -reveal_type(Foo(1)) # revealed: Foo - -# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" -reveal_type(Foo(1, 2)) # revealed: Foo -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md deleted file mode 100644 index 10db8aaa80a32..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ /dev/null @@ -1,330 +0,0 @@ -# Call expression - -## Simple - -```py -def get_int() -> int: - return 42 - -reveal_type(get_int()) # revealed: int -``` - -## Async - -```py -async def get_int_async() -> int: - return 42 - -# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]` -reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType) -``` - -## Generic - -```py -def get_int[T]() -> int: - return 42 - -reveal_type(get_int()) # revealed: int -``` - -## Decorated - -```py -from typing import Callable - -def foo() -> int: - return 42 - -def decorator(func) -> Callable[[], int]: - return foo - -@decorator -def bar() -> str: - return "bar" - -reveal_type(bar()) # revealed: int -``` - -## Invalid callable - -```py -nonsense = 123 -x = nonsense() # error: "Object of type `Literal[123]` is not callable" -``` - -## Potentially unbound function - -```py -def _(flag: bool): - if flag: - def foo() -> int: - return 42 - # error: [possibly-unresolved-reference] - reveal_type(foo()) # revealed: int -``` - -## Wrong argument type - -### Positional argument, positional-or-keyword parameter - -```py -def f(x: int) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -reveal_type(f("foo")) # revealed: int -``` - -### Positional argument, positional-only parameter - -```py -def f(x: int, /) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -reveal_type(f("foo")) # revealed: int -``` - -### Positional argument, variadic parameter - -```py -def f(*args: int) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -reveal_type(f("foo")) # revealed: int -``` - -### Keyword argument, positional-or-keyword parameter - -```py -def f(x: int) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -reveal_type(f(x="foo")) # revealed: int -``` - -### Keyword argument, keyword-only parameter - -```py -def f(*, x: int) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -reveal_type(f(x="foo")) # revealed: int -``` - -### Keyword argument, keywords parameter - -```py -def f(**kwargs: int) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -reveal_type(f(x="foo")) # revealed: int -``` - -### Correctly match keyword out-of-order - -```py -def f(x: int = 1, y: str = "foo") -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[2]`" -# error: 20 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["bar"]`" -reveal_type(f(y=2, x="bar")) # revealed: int -``` - -## Too many positional arguments - -### One too many - -```py -def f() -> int: - return 1 - -# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1" -reveal_type(f("foo")) # revealed: int -``` - -### Two too many - -```py -def f() -> int: - return 1 - -# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2" -reveal_type(f("foo", "bar")) # revealed: int -``` - -### No too-many-positional if variadic is taken - -```py -def f(*args: int) -> int: - return 1 - -reveal_type(f(1, 2, 3)) # revealed: int -``` - -### Multiple keyword arguments map to keyword variadic parameter - -```py -def f(**kwargs: int) -> int: - return 1 - -reveal_type(f(foo=1, bar=2)) # revealed: int -``` - -## Missing arguments - -### No defaults or variadic - -```py -def f(x: int) -> int: - return 1 - -# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" -reveal_type(f()) # revealed: int -``` - -### With default - -```py -def f(x: int, y: str = "foo") -> int: - return 1 - -# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" -reveal_type(f()) # revealed: int -``` - -### Defaulted argument is not required - -```py -def f(x: int = 1) -> int: - return 1 - -reveal_type(f()) # revealed: int -``` - -### With variadic - -```py -def f(x: int, *y: str) -> int: - return 1 - -# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" -reveal_type(f()) # revealed: int -``` - -### Variadic argument is not required - -```py -def f(*args: int) -> int: - return 1 - -reveal_type(f()) # revealed: int -``` - -### Keywords argument is not required - -```py -def f(**kwargs: int) -> int: - return 1 - -reveal_type(f()) # revealed: int -``` - -### Multiple - -```py -def f(x: int, y: int) -> int: - return 1 - -# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`" -reveal_type(f()) # revealed: int -``` - -## Unknown argument - -```py -def f(x: int) -> int: - return 1 - -# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`" -reveal_type(f(x=1, y=2)) # revealed: int -``` - -## Parameter already assigned - -```py -def f(x: int) -> int: - return 1 - -# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" -reveal_type(f(1, x=2)) # revealed: int -``` - -## Special functions - -Some functions require special handling in type inference. Here, we make sure that we still emit -proper diagnostics in case of missing or superfluous arguments. - -### `reveal_type` - -```py -from typing_extensions import reveal_type - -# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`" -reveal_type() - -# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2" -reveal_type(1, 2) -``` - -### `static_assert` - -```py -from knot_extensions import static_assert - -# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`" -static_assert() - -# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3" -static_assert(True, 2, 3) -``` - -### `len` - -```py -# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`" -len() - -# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2" -len([], 1) -``` - -### Type API predicates - -```py -from knot_extensions import is_subtype_of, is_fully_static - -# error: [missing-argument] -is_subtype_of() - -# error: [missing-argument] -is_subtype_of(int) - -# error: [too-many-positional-arguments] -is_subtype_of(int, int, int) - -# error: [too-many-positional-arguments] -is_subtype_of(int, int, int, int) - -# error: [missing-argument] -is_fully_static() - -# error: [too-many-positional-arguments] -is_fully_static(int, int) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/invalid_syntax.md b/crates/red_knot_python_semantic/resources/mdtest/call/invalid_syntax.md deleted file mode 100644 index 81b933d7f517a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/call/invalid_syntax.md +++ /dev/null @@ -1,44 +0,0 @@ -# Invalid signatures - -## Multiple arguments with the same name - -We always map a keyword argument to the first parameter of that name. - -```py -# error: [invalid-syntax] "Duplicate parameter "x"" -def f(x: int, x: str) -> int: - return 1 - -# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" -# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" -reveal_type(f(1, x=2)) # revealed: int -``` - -## Positional after non-positional - -When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them -to the valid order: - -```py -# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter" -def f(**kw: int, x: str) -> int: - return 1 - -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`" -reveal_type(f(1)) # revealed: int -``` - -## Non-defaulted after defaulted - -We emit a syntax diagnostic for this, but it doesn't cause any problems for binding. - -```py -# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default" -def f(x: int = 1, y: str) -> int: - return 1 - -reveal_type(f(y="foo")) # revealed: int -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" -# error: [missing-argument] "No argument provided for required parameter `y` of function `f`" -reveal_type(f("foo")) # revealed: int -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md deleted file mode 100644 index 0d3e94cd929bc..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ /dev/null @@ -1,428 +0,0 @@ -# Methods - -## Background: Functions as descriptors - -> Note: See also this related section in the descriptor guide: [Functions and methods]. - -Say we have a simple class `C` with a function definition `f` inside its body: - -```py -class C: - def f(self, x: int) -> str: - return "a" -``` - -Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance -(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors -because they implement a `__get__` method. This is crucial in making sure that method calls work as -expected. In general, the signature of the `__get__` method in the descriptor protocol is -`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The -passed value for the `instance` argument depends on whether the attribute is accessed from the class -object (in which case it is `None`), or from an instance (in which case it is the instance of type -`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize: - -- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)` -- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)` - -Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the -function attribute. The way the special `__get__` method *on functions* works is as follows. In the -former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In -the latter case, it returns a *bound method* object: - -```py -from inspect import getattr_static - -reveal_type(getattr_static(C, "f")) # revealed: def f(self, x: int) -> str - -reveal_type(getattr_static(C, "f").__get__) # revealed: - -reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f(self, x: int) -> str -reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method C.f(x: int) -> str -``` - -In conclusion, this is why we see the following two types when accessing the `f` attribute on the -class object `C` and on an instance `C()`: - -```py -reveal_type(C.f) # revealed: def f(self, x: int) -> str -reveal_type(C().f) # revealed: bound method C.f(x: int) -> str -``` - -A bound method is a callable object that contains a reference to the `instance` that it was called -on (can be inspected via `__self__`), and the function object that it refers to (can be inspected -via `__func__`): - -```py -bound_method = C().f - -reveal_type(bound_method.__self__) # revealed: C -reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str -``` - -When we call the bound method, the `instance` is implicitly passed as the first argument (`self`): - -```py -reveal_type(C().f(1)) # revealed: str -reveal_type(bound_method(1)) # revealed: str -``` - -When we call the function object itself, we need to pass the `instance` explicitly: - -```py -C.f(1) # error: [missing-argument] - -reveal_type(C.f(C(), 1)) # revealed: str -``` - -When we access methods from derived classes, they will be bound to instances of the derived class: - -```py -class D(C): - pass - -reveal_type(D().f) # revealed: bound method D.f(x: int) -> str -``` - -If we access an attribute on a bound method object itself, it will defer to `types.MethodType`: - -```py -reveal_type(bound_method.__hash__) # revealed: bound method MethodType.__hash__() -> int -``` - -If an attribute is not available on the bound method object, it will be looked up on the underlying -function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound -methods, even though it is not available on `types.MethodType`: - -```py -reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None -``` - -## Basic method calls on class objects and instances - -```py -class Base: - def method_on_base(self, x: int | None) -> str: - return "a" - -class Derived(Base): - def method_on_derived(self, x: bytes) -> tuple[int, str]: - return (1, "a") - -reveal_type(Base().method_on_base(1)) # revealed: str -reveal_type(Base.method_on_base(Base(), 1)) # revealed: str - -Base().method_on_base("incorrect") # error: [invalid-argument-type] -Base().method_on_base() # error: [missing-argument] -Base().method_on_base(1, 2) # error: [too-many-positional-arguments] - -reveal_type(Derived().method_on_base(1)) # revealed: str -reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str] -reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str -reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str] -``` - -## Method calls on literals - -### Boolean literals - -```py -reveal_type(True.bit_length()) # revealed: int -reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]] -``` - -### Integer literals - -```py -reveal_type((42).bit_length()) # revealed: int -``` - -### String literals - -```py -reveal_type("abcde".find("abc")) # revealed: int -reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes - -"abcde".find(123) # error: [invalid-argument-type] -``` - -### Bytes literals - -```py -reveal_type(b"abcde".startswith(b"abc")) # revealed: bool -``` - -## Method calls on `LiteralString` - -```py -from typing_extensions import LiteralString - -def f(s: LiteralString) -> None: - reveal_type(s.find("a")) # revealed: int -``` - -## Method calls on `tuple` - -```py -def f(t: tuple[int, str]) -> None: - reveal_type(t.index("a")) # revealed: int -``` - -## Method calls on unions - -```py -from typing import Any - -class A: - def f(self) -> int: - return 1 - -class B: - def f(self) -> str: - return "a" - -def f(a_or_b: A | B, any_or_a: Any | A): - reveal_type(a_or_b.f) # revealed: (bound method A.f() -> int) | (bound method B.f() -> str) - reveal_type(a_or_b.f()) # revealed: int | str - - reveal_type(any_or_a.f) # revealed: Any | (bound method A.f() -> int) - reveal_type(any_or_a.f()) # revealed: Any | int -``` - -## Method calls on `KnownInstance` types - -```toml -[environment] -python-version = "3.12" -``` - -```py -type IntOrStr = int | str - -reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm -``` - -## Error cases: Calling `__get__` for methods - -The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed: - -```py -from types import FunctionType, MethodType -from typing import overload - -@overload -def __get__(self, instance: None, owner: type, /) -> FunctionType: ... -@overload -def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... -``` - -Here, we test that this signature is enforced correctly: - -```py -from inspect import getattr_static - -class C: - def f(self, x: int) -> str: - return "a" - -method_wrapper = getattr_static(C, "f").__get__ - -reveal_type(method_wrapper) # revealed: - -# All of these are fine: -method_wrapper(C(), C) -method_wrapper(C()) -method_wrapper(C(), None) -method_wrapper(None, C) - -# Passing `None` without an `owner` argument is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" -method_wrapper(None) - -# Passing something that is not assignable to `type` as the `owner` argument is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" -method_wrapper(None, 1) - -# Passing `None` as the `owner` argument when `instance` is `None` is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" -method_wrapper(None, None) - -# Calling `__get__` without any arguments is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" -method_wrapper() - -# Calling `__get__` with too many positional arguments is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" -method_wrapper(C(), C, "one too many") -``` - -## Fallback to metaclass - -When a method is accessed on a class object, it is looked up on the metaclass if it is not found on -the class itself. This also creates a bound method that is bound to the class object itself: - -```py -from __future__ import annotations - -class Meta(type): - def f(cls, arg: int) -> str: - return "a" - -class C(metaclass=Meta): - pass - -reveal_type(C.f) # revealed: bound method Literal[C].f(arg: int) -> str -reveal_type(C.f(1)) # revealed: str -``` - -The method `f` can not be accessed from an instance of the class: - -```py -# error: [unresolved-attribute] "Type `C` has no attribute `f`" -C().f -``` - -A metaclass function can be shadowed by a method on the class: - -```py -from typing import Any, Literal - -class D(metaclass=Meta): - def f(arg: int) -> Literal["a"]: - return "a" - -reveal_type(D.f(1)) # revealed: Literal["a"] -``` - -If the class method is possibly unbound, we union the return types: - -```py -def flag() -> bool: - return True - -class E(metaclass=Meta): - if flag(): - def f(arg: int) -> Any: - return "a" - -reveal_type(E.f(1)) # revealed: str | Any -``` - -## `@classmethod` - -### Basic - -When a `@classmethod` attribute is accessed, it returns a bound method object, even when accessed on -the class object itself: - -```py -from __future__ import annotations - -class C: - @classmethod - def f(cls: type[C], x: int) -> str: - return "a" - -reveal_type(C.f) # revealed: bound method Literal[C].f(x: int) -> str -reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str -``` - -The `cls` method argument is then implicitly passed as the first argument when calling the method: - -```py -reveal_type(C.f(1)) # revealed: str -reveal_type(C().f(1)) # revealed: str -``` - -When the class method is called incorrectly, we detect it: - -```py -C.f("incorrect") # error: [invalid-argument-type] -C.f() # error: [missing-argument] -C.f(1, 2) # error: [too-many-positional-arguments] -``` - -If the `cls` parameter is wrongly annotated, we emit an error at the call site: - -```py -class D: - @classmethod - def f(cls: D): - # This function is wrongly annotated, it should be `type[D]` instead of `D` - pass - -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `D`, found `Literal[D]`" -D.f() -``` - -When a class method is accessed on a derived class, it is bound to that derived class: - -```py -class Derived(C): - pass - -reveal_type(Derived.f) # revealed: bound method Literal[Derived].f(x: int) -> str -reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str - -reveal_type(Derived.f(1)) # revealed: str -reveal_type(Derived().f(1)) # revealed: str -``` - -### Accessing the classmethod as a static member - -Accessing a `@classmethod`-decorated function at runtime returns a `classmethod` object. We -currently don't model this explicitly: - -```py -from inspect import getattr_static - -class C: - @classmethod - def f(cls): ... - -reveal_type(getattr_static(C, "f")) # revealed: def f(cls) -> Unknown -reveal_type(getattr_static(C, "f").__get__) # revealed: -``` - -But we correctly model how the `classmethod` descriptor works: - -```py -reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method Literal[C].f() -> Unknown -reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method Literal[C].f() -> Unknown -reveal_type(getattr_static(C, "f").__get__(C())) # revealed: bound method type[C].f() -> Unknown -``` - -The `owner` argument takes precedence over the `instance` argument: - -```py -reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method Literal[C].f() -> Unknown -``` - -### Classmethods mixed with other decorators - -When a `@classmethod` is additionally decorated with another decorator, it is still treated as a -class method: - -```py -from __future__ import annotations - -def does_nothing[T](f: T) -> T: - return f - -class C: - @classmethod - @does_nothing - def f1(cls: type[C], x: int) -> str: - return "a" - - @does_nothing - @classmethod - def f2(cls: type[C], x: int) -> str: - return "a" - -reveal_type(C.f1(1)) # revealed: str -reveal_type(C().f1(1)) # revealed: str -reveal_type(C.f2(1)) # revealed: str -reveal_type(C().f2(1)) # revealed: str -``` - -[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md deleted file mode 100644 index b3615496c1fe9..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ /dev/null @@ -1,203 +0,0 @@ -# Unions in calls - -## Union of return types - -```py -def _(flag: bool): - if flag: - def f() -> int: - return 1 - else: - def f() -> str: - return "foo" - reveal_type(f()) # revealed: int | str -``` - -## Calling with an unknown union - -```py -from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`" - -def coinflip() -> bool: - return True - -if coinflip(): - def f() -> int: - return 1 - -reveal_type(f()) # revealed: Unknown | int -``` - -## Non-callable elements in a union - -Calling a union with a non-callable element should emit a diagnostic. - -```py -def _(flag: bool): - if flag: - f = 1 - else: - def f() -> int: - return 1 - x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable" - reveal_type(x) # revealed: Unknown | int -``` - -## Multiple non-callable elements in a union - -Calling a union with multiple non-callable elements should mention all of them in the diagnostic. - -```py -def _(flag: bool, flag2: bool): - if flag: - f = 1 - elif flag2: - f = "foo" - else: - def f() -> int: - return 1 - # TODO we should mention all non-callable elements of the union - # error: [call-non-callable] "Object of type `Literal[1]` is not callable" - # revealed: Unknown | int - reveal_type(f()) -``` - -## All non-callable union elements - -Calling a union with no callable elements can emit a simpler diagnostic. - -```py -def _(flag: bool): - if flag: - f = 1 - else: - f = "foo" - - x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable" - reveal_type(x) # revealed: Unknown -``` - -## Mismatching signatures - -Calling a union where the arguments don't match the signature of all variants. - -```py -def f1(a: int) -> int: - return a - -def f2(a: str) -> str: - return a - -def _(flag: bool): - if flag: - f = f1 - else: - f = f2 - - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[3]`" - x = f(3) - reveal_type(x) # revealed: int | str -``` - -## Any non-callable variant - -```py -def f1(a: int): ... -def _(flag: bool): - if flag: - f = f1 - else: - f = "This is a string literal" - - # error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable" - x = f(3) - reveal_type(x) # revealed: Unknown -``` - -## Union of binding errors - -```py -def f1(): ... -def f2(): ... -def _(flag: bool): - if flag: - f = f1 - else: - f = f2 - - # TODO: we should show all errors from the union, not arbitrarily pick one union element - # error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1" - x = f(3) - reveal_type(x) # revealed: Unknown -``` - -## One not-callable, one wrong argument - -```py -class C: ... - -def f1(): ... -def _(flag: bool): - if flag: - f = f1 - else: - f = C() - - # TODO: we should either show all union errors here, or prioritize the not-callable error - # error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1" - x = f(3) - reveal_type(x) # revealed: Unknown -``` - -## Union including a special-cased function - -```py -def _(flag: bool): - if flag: - f = str - else: - f = repr - reveal_type(str("string")) # revealed: Literal["string"] - 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] -``` - -## Size limit on unions of literals - -Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`, -`bytes`, `str`). - -```py -from typing import Literal - -def _(literals_2: Literal[0, 1], b: bool, flag: bool): - literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3] - literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15] - literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63] - literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127] - - # Going beyond the MAX_UNION_LITERALS limit (currently 200): - literals_256 = 16 * literals_16 + literals_16 - reveal_type(literals_256) # revealed: int - - # Going beyond the limit when another type is already part of the union - bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127] - literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255] - - # Now union the two: - reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/class/super.md b/crates/red_knot_python_semantic/resources/mdtest/class/super.md deleted file mode 100644 index 2fa7d0d767ecf..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/class/super.md +++ /dev/null @@ -1,400 +0,0 @@ -# Super - -Python defines the terms *bound super object* and *unbound super object*. - -An **unbound super object** is created when `super` is called with only one argument. (e.g. -`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is -rarely used in practice. - -A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the -implicit form `super()`, where both the pivot class and the owner are inferred. This is the most -common usage. - -## Basic Usage - -### Explicit Super Object - -`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the -specified pivot class. - -```py -class A: - def a(self): ... - aa: int = 1 - -class B(A): - def b(self): ... - bb: int = 2 - -class C(B): - def c(self): ... - cc: int = 3 - -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]] - -super(C, C()).a -super(C, C()).b -# error: [unresolved-attribute] "Type `` has no attribute `c`" -super(C, C()).c - -super(B, C()).a -# error: [unresolved-attribute] "Type `` has no attribute `b`" -super(B, C()).b -# error: [unresolved-attribute] "Type `` has no attribute `c`" -super(B, C()).c - -# error: [unresolved-attribute] "Type `` has no attribute `a`" -super(A, C()).a -# error: [unresolved-attribute] "Type `` has no attribute `b`" -super(A, C()).b -# error: [unresolved-attribute] "Type `` has no attribute `c`" -super(A, C()).c - -reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown -reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown -reveal_type(super(C, C()).aa) # revealed: int -reveal_type(super(C, C()).bb) # revealed: int -``` - -### Implicit Super Object - -The implicit form `super()` is same as `super(__class__, )`. The `__class__` refers -to the class that contains the function where `super()` is used. The first argument refers to the -current method’s first parameter (typically `self` or `cls`). - -```py -from __future__ import annotations - -class A: - def __init__(self, a: int): ... - @classmethod - def f(cls): ... - -class B(A): - def __init__(self, a: int): - # TODO: Once `Self` is supported, this should be `` - reveal_type(super()) # revealed: - super().__init__(a) - - @classmethod - def f(cls): - # TODO: Once `Self` is supported, this should be `` - reveal_type(super()) # revealed: - super().f() - -super(B, B(42)).__init__(42) -super(B, B).f() -``` - -### Unbound Super Object - -Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as -a plain `super` instance and does not support name lookup via the MRO. - -```py -class A: - a: int = 42 - -class B(A): ... - -reveal_type(super(B)) # revealed: super - -# error: [unresolved-attribute] "Type `super` has no attribute `a`" -super(B).a -``` - -## Attribute Assignment - -`super()` objects do not allow attribute assignment — even if the attribute is resolved -successfully. - -```py -class A: - a: int = 3 - -class B(A): ... - -reveal_type(super(B, B()).a) # revealed: int -# error: [invalid-assignment] "Cannot assign to attribute `a` on type ``" -super(B, B()).a = 3 -# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`" -super(B).a = 5 -``` - -## Dynamic Types - -If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a -member, it should effectively behave like a dynamic type. - -```py -class A: - a: int = 1 - -def f(x): - reveal_type(x) # revealed: Unknown - - reveal_type(super(x, x)) # revealed: - reveal_type(super(A, x)) # revealed: - reveal_type(super(x, A())) # revealed: - - reveal_type(super(x, x).a) # revealed: Unknown - reveal_type(super(A, x).a) # revealed: Unknown - reveal_type(super(x, A()).a) # revealed: Unknown -``` - -## Implicit `super()` in Complex Structure - -```py -from __future__ import annotations - -class A: - def test(self): - reveal_type(super()) # revealed: - - class B: - def test(self): - reveal_type(super()) # revealed: - - class C(A.B): - def test(self): - reveal_type(super()) # revealed: - - def inner(t: C): - reveal_type(super()) # revealed: - lambda x: reveal_type(super()) # revealed: -``` - -## Built-ins and Literals - -```py -reveal_type(super(bool, True)) # revealed: -reveal_type(super(bool, bool())) # revealed: -reveal_type(super(int, bool())) # revealed: -reveal_type(super(int, 3)) # revealed: -reveal_type(super(str, "")) # revealed: -``` - -## Descriptor Behavior with Super - -Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can -differ depending on whether the second argument to `super` is a class or an instance. - -```py -class A: - def a1(self): ... - @classmethod - def a2(cls): ... - -class B(A): ... - -# A.__dict__["a1"].__get__(B(), B) -reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown -# A.__dict__["a2"].__get__(B(), B) -reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown - -# A.__dict__["a1"].__get__(None, B) -reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown -# A.__dict__["a2"].__get__(None, B) -reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown -``` - -## Union of Supers - -When the owner is a union type, `super()` is built separately for each branch, and the resulting -super objects are combined into a union. - -```py -class A: ... - -class B: - b: int = 42 - -class C(A, B): ... -class D(B, A): ... - -def f(x: C | D): - reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]] - reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]] - - s = super(A, x) - reveal_type(s) # revealed: | - - # error: [possibly-unbound-attribute] "Attribute `b` on type ` | ` is possibly unbound" - s.b - -def f(flag: bool): - x = str() if flag else str("hello") - reveal_type(x) # revealed: Literal["", "hello"] - reveal_type(super(str, x)) # revealed: - -def f(x: int | str): - # error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call" - super(int, x) -``` - -Even when `super()` is constructed separately for each branch of a union, it should behave correctly -in all cases. - -```py -def f(flag: bool): - if flag: - class A: - x = 1 - y: int = 1 - - a: str = "hello" - - class B(A): ... - s = super(B, B()) - else: - class C: - x = 2 - y: int | str = "test" - - class D(C): ... - s = super(D, D()) - - reveal_type(s) # revealed: | - - reveal_type(s.x) # revealed: Unknown | Literal[1, 2] - reveal_type(s.y) # revealed: int | str - - # error: [possibly-unbound-attribute] "Attribute `a` on type ` | ` is possibly unbound" - reveal_type(s.a) # revealed: str -``` - -## Supers with Generic Classes - -```py -from knot_extensions import TypeOf, static_assert, is_subtype_of - -class A[T]: - def f(self, a: T) -> T: - return a - -class B[T](A[T]): - def f(self, b: T) -> T: - return super().f(b) -``` - -## Invalid Usages - -### Unresolvable `super()` Calls - -If an appropriate class and argument cannot be found, a runtime error will occur. - -```py -from __future__ import annotations - -# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" -reveal_type(super()) # revealed: Unknown - -def f(): - # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" - super() - -# No first argument in its scope -class A: - # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" - s = super() - - def f(self): - def g(): - # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" - super() - # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" - lambda: super() - - # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" - (super() for _ in range(10)) - - @staticmethod - def h(): - # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" - super() -``` - -### Failing Condition Checks - -`super()` requires its first argument to be a valid class, and its second argument to be either an -instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at -runtime. - -```py -def f(x: int): - # error: [invalid-super-argument] "`int` is not a valid class" - super(x, x) - - type IntAlias = int - # error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class" - super(IntAlias, 0) - -# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call" -# revealed: Unknown -reveal_type(super(int, str())) - -# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call" -# revealed: Unknown -reveal_type(super(int, str)) - -class A: ... -class B(A): ... - -# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call" -# revealed: Unknown -reveal_type(super(B, A())) - -# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call" -# revealed: Unknown -reveal_type(super(B, object())) - -# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call" -# revealed: Unknown -reveal_type(super(B, A)) - -# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call" -# revealed: Unknown -reveal_type(super(B, object)) - -super(object, object()).__class__ -``` - -### Instance Member Access via `super` - -Accessing instance members through `super()` is not allowed. - -```py -from __future__ import annotations - -class A: - def __init__(self, a: int): - self.a = a - -class B(A): - def __init__(self, a: int): - super().__init__(a) - # TODO: Once `Self` is supported, this should raise `unresolved-attribute` error - super().a - -# error: [unresolved-attribute] "Type `` has no attribute `a`" -super(B, B(42)).a -``` - -### Dunder Method Resolution - -Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super -object itself. In other words, `super` should not be treated as if it inherits attributes of the -`owner`. - -```py -class A: - def __getitem__(self, key: int) -> int: - return 42 - -class B(A): ... - -reveal_type(A()[0]) # revealed: int -reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int -# error: [non-subscriptable] "Cannot subscript object of type `` with no `__getitem__` method" -super(B, B())[0] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md deleted file mode 100644 index bf956e84132c7..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md +++ /dev/null @@ -1,27 +0,0 @@ -# Comparison: Integers - -## Integer literals - -```py -reveal_type(1 == 1 == True) # revealed: Literal[True] -reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False] -reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True] -reveal_type(1 < 1) # revealed: Literal[False] -reveal_type(1 > 1) # revealed: Literal[False] -reveal_type(1 is 1) # revealed: bool -reveal_type(1 is not 1) # revealed: bool -reveal_type(1 is 2) # revealed: Literal[False] -reveal_type(1 is not 7) # revealed: Literal[True] -# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`" -reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True] -``` - -## Integer instance - -```py -# TODO: implement lookup of `__eq__` on typeshed `int` stub. -def _(a: int, b: int): - reveal_type(1 == a) # revealed: bool - reveal_type(9 < a) # revealed: bool - reveal_type(a < b) # revealed: bool -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md deleted file mode 100644 index f1750dde1f479..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md +++ /dev/null @@ -1,150 +0,0 @@ -# Comparison: Intersections - -## Positive contributions - -If we have an intersection type `A & B` and we get a definitive true/false answer for one of the -types, we can infer that the result for the intersection type is also true/false: - -```py -from typing import Literal - -class Base: - def __gt__(self, other) -> bool: - return False - -class Child1(Base): - def __eq__(self, other) -> Literal[True]: - return True - -class Child2(Base): ... - -def _(x: Base): - c1 = Child1() - - # Create an intersection type through narrowing: - if isinstance(x, Child1): - if isinstance(x, Child2): - reveal_type(x) # revealed: Child1 & Child2 - - reveal_type(x == 1) # revealed: Literal[True] - - # Other comparison operators fall back to the base type: - reveal_type(x > 1) # revealed: bool - reveal_type(x is c1) # revealed: bool -``` - -## Negative contributions - -Negative contributions to the intersection type only allow simplifications in a few special cases -(equality and identity comparisons). - -### Equality comparisons - -#### Literal strings - -```py -x = "x" * 1_000_000_000 -y = "y" * 1_000_000_000 -reveal_type(x) # revealed: LiteralString - -if x != "abc": - reveal_type(x) # revealed: LiteralString & ~Literal["abc"] - - # TODO: This should be `Literal[False]` - reveal_type(x == "abc") # revealed: bool - # TODO: This should be `Literal[False]` - reveal_type("abc" == x) # revealed: bool - reveal_type(x == "something else") # revealed: bool - reveal_type("something else" == x) # revealed: bool - - # TODO: This should be `Literal[True]` - reveal_type(x != "abc") # revealed: bool - # TODO: This should be `Literal[True]` - reveal_type("abc" != x) # revealed: bool - reveal_type(x != "something else") # revealed: bool - reveal_type("something else" != x) # revealed: bool - - reveal_type(x == y) # revealed: bool - reveal_type(y == x) # revealed: bool - reveal_type(x != y) # revealed: bool - reveal_type(y != x) # revealed: bool - - reveal_type(x >= "abc") # revealed: bool - reveal_type("abc" >= x) # revealed: bool - - reveal_type(x in "abc") # revealed: bool - reveal_type("abc" in x) # revealed: bool -``` - -#### Integers - -```py -def _(x: int): - if x != 1: - reveal_type(x) # revealed: int & ~Literal[1] - - reveal_type(x != 1) # revealed: bool - reveal_type(x != 2) # revealed: bool - - reveal_type(x == 1) # revealed: bool - reveal_type(x == 2) # revealed: bool -``` - -### Identity comparisons - -```py -class A: ... - -def _(o: object): - a = A() - n = None - - if o is not None: - reveal_type(o) # revealed: ~None - reveal_type(o is n) # revealed: Literal[False] - reveal_type(o is not n) # revealed: Literal[True] -``` - -## Diagnostics - -### Unsupported operators for positive contributions - -Raise an error if any of the positive contributions to the intersection type are unsupported for the -given operator: - -```py -class Container: - def __contains__(self, x) -> bool: - return False - -class NonContainer: ... - -def _(x: object): - if isinstance(x, Container): - if isinstance(x, NonContainer): - reveal_type(x) # revealed: Container & NonContainer - - # error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`" - reveal_type(2 in x) # revealed: bool -``` - -### Unsupported operators for negative contributions - -Do *not* raise an error if any of the negative contributions to the intersection type are -unsupported for the given operator: - -```py -class Container: - def __contains__(self, x) -> bool: - return False - -class NonContainer: ... - -def _(x: object): - if isinstance(x, Container): - if not isinstance(x, NonContainer): - reveal_type(x) # revealed: Container & ~NonContainer - - # No error here! - reveal_type(2 in x) # revealed: bool -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md deleted file mode 100644 index 8047fc078d09e..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md +++ /dev/null @@ -1,150 +0,0 @@ -# Comprehensions - -## Basic comprehensions - -```py -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -# revealed: int -[reveal_type(x) for x in IntIterable()] - -class IteratorOfIterables: - def __next__(self) -> IntIterable: - return IntIterable() - -class IterableOfIterables: - def __iter__(self) -> IteratorOfIterables: - return IteratorOfIterables() - -# revealed: tuple[int, IntIterable] -[reveal_type((x, y)) for y in IterableOfIterables() for x in y] - -# revealed: int -{reveal_type(x): 0 for x in IntIterable()} - -# revealed: int -{0: reveal_type(x) for x in IntIterable()} -``` - -## Nested comprehension - -```py -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -# revealed: tuple[int, int] -[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()] -``` - -## Comprehension referencing outer comprehension - -```py -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -class IteratorOfIterables: - def __next__(self) -> IntIterable: - return IntIterable() - -class IterableOfIterables: - def __iter__(self) -> IteratorOfIterables: - return IteratorOfIterables() - -# revealed: tuple[int, IntIterable] -[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()] -``` - -## Comprehension with unbound iterable - -Iterating over an unbound iterable yields `Unknown`: - -```py -# error: [unresolved-reference] "Name `x` used when not defined" -# revealed: Unknown -[reveal_type(z) for z in x] - -class IntIterator: - def __next__(self) -> int: - return 42 - -class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - -# error: [not-iterable] "Object of type `int` is not iterable" -# revealed: tuple[int, Unknown] -[reveal_type((x, z)) for x in IntIterable() for z in x] -``` - -## Starred expressions - -Starred expressions must be iterable - -```py -class NotIterable: ... - -class Iterator: - def __next__(self) -> int: - return 42 - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -# This is fine: -x = [*Iterable()] - -# error: [not-iterable] "Object of type `NotIterable` is not iterable" -y = [*NotIterable()] -``` - -## Async comprehensions - -### Basic - -```py -class AsyncIterator: - async def __anext__(self) -> int: - return 42 - -class AsyncIterable: - def __aiter__(self) -> AsyncIterator: - return AsyncIterator() - -# revealed: @Todo(async iterables/iterators) -[reveal_type(x) async for x in AsyncIterable()] -``` - -### Invalid async comprehension - -This tests that we understand that `async` comprehensions do *not* work according to the synchronous -iteration protocol - -```py -class Iterator: - def __next__(self) -> int: - return 42 - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -# revealed: @Todo(async iterables/iterators) -[reveal_type(x) async for x in Iterable()] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md deleted file mode 100644 index 5037484212eb6..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md +++ /dev/null @@ -1,297 +0,0 @@ -# Pattern matching - -## With wildcard - -```py -def _(target: int): - match target: - case 1: - y = 2 - case _: - y = 3 - - reveal_type(y) # revealed: Literal[2, 3] -``` - -## Without wildcard - -```py -def _(target: int): - match target: - case 1: - y = 2 - case 2: - y = 3 - - # revealed: Literal[2, 3] - # error: [possibly-unresolved-reference] - reveal_type(y) -``` - -## Basic match - -```py -def _(target: int): - y = 1 - y = 2 - - match target: - case 1: - y = 3 - case 2: - y = 4 - - reveal_type(y) # revealed: Literal[2, 3, 4] -``` - -## Value match - -A value pattern matches based on equality: the first `case` branch here will be taken if `subject` -is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a -custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider -the possibility that the `case` branch might be taken even though the type `C` is disjoint from the -type `Literal[2]`. - -This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than -`Literal[1]`: - -```py -from typing import final - -@final -class C: - pass - -def _(subject: C): - y = 1 - match subject: - case 2: - y = 3 - reveal_type(y) # revealed: Literal[1, 3] -``` - -## Class match - -A `case` branch with a class pattern is taken if the subject is an instance of the given class, and -all subpatterns in the class pattern match. - -```py -from typing import final - -class Foo: - pass - -class FooSub(Foo): - pass - -class Bar: - pass - -@final -class Baz: - pass - -def _(target: FooSub): - y = 1 - - match target: - case Baz(): - y = 2 - case Foo(): - y = 3 - case Bar(): - y = 4 - - reveal_type(y) # revealed: Literal[3] - -def _(target: FooSub): - y = 1 - - match target: - case Baz(): - y = 2 - case Bar(): - y = 3 - case Foo(): - y = 4 - - reveal_type(y) # revealed: Literal[3, 4] - -def _(target: FooSub | str): - y = 1 - - match target: - case Baz(): - y = 2 - case Foo(): - y = 3 - case Bar(): - y = 4 - - reveal_type(y) # revealed: Literal[1, 3, 4] -``` - -## Singleton match - -Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks. - -```py -from typing import Literal - -def _(target: Literal[True, False]): - y = 1 - - match target: - case True: - y = 2 - case False: - y = 3 - case None: - y = 4 - - # TODO: with exhaustiveness checking, this should be Literal[2, 3] - reveal_type(y) # revealed: Literal[1, 2, 3] - -def _(target: bool): - y = 1 - - match target: - case True: - y = 2 - case False: - y = 3 - case None: - y = 4 - - # TODO: with exhaustiveness checking, this should be Literal[2, 3] - reveal_type(y) # revealed: Literal[1, 2, 3] - -def _(target: None): - y = 1 - - match target: - case True: - y = 2 - case False: - y = 3 - case None: - y = 4 - - reveal_type(y) # revealed: Literal[4] - -def _(target: None | Literal[True]): - y = 1 - - match target: - case True: - y = 2 - case False: - y = 3 - case None: - y = 4 - - # TODO: with exhaustiveness checking, this should be Literal[2, 4] - reveal_type(y) # revealed: Literal[1, 2, 4] - -# bool is an int subclass -def _(target: int): - y = 1 - - match target: - case True: - y = 2 - case False: - y = 3 - case None: - y = 4 - - reveal_type(y) # revealed: Literal[1, 2, 3] - -def _(target: str): - y = 1 - - match target: - case True: - y = 2 - case False: - y = 3 - case None: - y = 4 - - reveal_type(y) # revealed: Literal[1] -``` - -## Or match - -A `|` pattern matches if any of the subpatterns match. - -```py -from typing import Literal, final - -def _(target: Literal["foo", "baz"]): - y = 1 - - match target: - case "foo" | "bar": - y = 2 - case "baz": - y = 3 - - # TODO: with exhaustiveness, this should be Literal[2, 3] - reveal_type(y) # revealed: Literal[1, 2, 3] - -def _(target: None): - y = 1 - - match target: - case None | 3: - y = 2 - case "foo" | 4 | True: - y = 3 - - reveal_type(y) # revealed: Literal[2] - -@final -class Baz: - pass - -def _(target: int | None | float): - y = 1 - - match target: - case None | 3: - y = 2 - case Baz(): - y = 3 - - reveal_type(y) # revealed: Literal[1, 2] - -def _(target: None | str): - y = 1 - - match target: - case Baz() | True | False: - y = 2 - case int(): - y = 3 - - reveal_type(y) # revealed: Literal[1, 3] -``` - -## Guard with object that implements `__bool__` incorrectly - -```py -class NotBoolable: - __bool__: int = 3 - -def _(target: int, flag: NotBoolable): - y = 1 - match target: - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" - case 1 if flag: - y = 2 - case 2: - y = 3 - - reveal_type(y) # revealed: Literal[1, 2, 3] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md deleted file mode 100644 index 000c60031758f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md +++ /dev/null @@ -1,724 +0,0 @@ -# Dataclasses - -## Basic - -Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`, -`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass` -decorator. By default, only the three mentioned methods are generated. - -```py -from dataclasses import dataclass - -@dataclass -class Person: - name: str - age: int | None = None - -alice1 = Person("Alice", 30) -alice2 = Person(name="Alice", age=30) -alice3 = Person(age=30, name="Alice") -alice4 = Person("Alice", age=30) - -reveal_type(alice1) # revealed: Person -reveal_type(type(alice1)) # revealed: type[Person] - -reveal_type(alice1.name) # revealed: str -reveal_type(alice1.age) # revealed: int | None - -reveal_type(repr(alice1)) # revealed: str - -reveal_type(alice1 == alice2) # revealed: bool -reveal_type(alice1 == "Alice") # revealed: bool - -bob = Person("Bob") -bob2 = Person("Bob", None) -bob3 = Person(name="Bob") -bob4 = Person(name="Bob", age=None) -``` - -The signature of the `__init__` method is generated based on the classes attributes. The following -calls are not valid: - -```py -# error: [missing-argument] -Person() - -# error: [too-many-positional-arguments] -Person("Eve", 20, "too many arguments") - -# error: [invalid-argument-type] -Person("Eve", "string instead of int") - -# error: [invalid-argument-type] -# error: [invalid-argument-type] -Person(20, "Eve") -``` - -## Signature of `__init__` - -TODO: All of the following tests are missing the `self` argument in the `__init__` signature. - -Declarations in the class body are used to generate the signature of the `__init__` method. If the -attributes are not just declarations, but also bindings, the type inferred from bindings is used as -the default value. - -```py -from dataclasses import dataclass - -@dataclass -class D: - x: int - y: str = "default" - z: int | None = 1 + 2 - -reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None -``` - -This also works if the declaration and binding are split: - -```py -@dataclass -class D: - x: int | None - x = None - -reveal_type(D.__init__) # revealed: (x: int | None = None) -> None -``` - -Non-fully static types are handled correctly: - -```py -from typing import Any - -@dataclass -class C: - x: Any - y: int | Any - z: tuple[int, Any] - -reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None -``` - -Variables without annotations are ignored: - -```py -@dataclass -class D: - x: int - y = 1 - -reveal_type(D.__init__) # revealed: (x: int) -> None -``` - -If attributes without default values are declared after attributes with default values, a -`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case: - -```py -@dataclass -class D: - x: int = 1 - # TODO: this should be an error: field without default defined after field with default - y: str -``` - -Pure class attributes (`ClassVar`) are not included in the signature of `__init__`: - -```py -from typing import ClassVar - -@dataclass -class D: - x: int - y: ClassVar[str] = "default" - z: bool - -reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None - -d = D(1, True) -reveal_type(d.x) # revealed: int -reveal_type(d.y) # revealed: str -reveal_type(d.z) # revealed: bool -``` - -Function declarations do not affect the signature of `__init__`: - -```py -@dataclass -class D: - x: int - - def y(self) -> str: - return "" - -reveal_type(D.__init__) # revealed: (x: int) -> None -``` - -And neither do nested class declarations: - -```py -@dataclass -class D: - x: int - - class Nested: - y: str - -reveal_type(D.__init__) # revealed: (x: int) -> None -``` - -But if there is a variable annotation with a function or class literal type, the signature of -`__init__` will include this field: - -```py -from knot_extensions import TypeOf - -class SomeClass: ... - -def some_function() -> None: ... -@dataclass -class D: - function_literal: TypeOf[some_function] - class_literal: TypeOf[SomeClass] - class_subtype_of: type[SomeClass] - -# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None -reveal_type(D.__init__) -``` - -More realistically, dataclasses can have `Callable` attributes: - -```py -from typing import Callable - -@dataclass -class D: - c: Callable[[int], str] - -reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None -``` - -Implicit instance attributes do not affect the signature of `__init__`: - -```py -@dataclass -class D: - x: int - - def f(self, y: str) -> None: - self.y: str = y - -reveal_type(D(1).y) # revealed: str - -reveal_type(D.__init__) # revealed: (x: int) -> None -``` - -Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't -be included in the signature of `__init__`. This is a case that we currently don't detect: - -```py -@dataclass -class D: - # (x) is an expression, not a "simple name" - (x): int = 1 - -# TODO: should ideally not include a `x` parameter -reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None -``` - -## `@dataclass` calls with arguments - -The `@dataclass` decorator can take several arguments to customize the existence of the generated -methods. The following test makes sure that we still treat the class as a dataclass if (the default) -arguments are passed in: - -```py -from dataclasses import dataclass - -@dataclass(init=True, repr=True, eq=True) -class Person: - name: str - age: int | None = None - -alice = Person("Alice", 30) -reveal_type(repr(alice)) # revealed: str -reveal_type(alice == alice) # revealed: bool -``` - -If `init` is set to `False`, no `__init__` method is generated: - -```py -from dataclasses import dataclass - -@dataclass(init=False) -class C: - x: int - -C() # Okay - -# error: [too-many-positional-arguments] -C(1) - -repr(C()) - -C() == C() -``` - -## Other dataclass parameters - -### `repr` - -A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but -in that case `__repr__` is still available via `object.__repr__`: - -```py -from dataclasses import dataclass - -@dataclass(repr=False) -class WithoutRepr: - x: int - -reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str -``` - -### `eq` - -The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but -`__eq__` is still available via `object.__eq__`: - -```py -from dataclasses import dataclass - -@dataclass(eq=False) -class WithoutEq: - x: int - -reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool -``` - -### `order` - -`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__` -methods will be generated: - -```py -from dataclasses import dataclass - -@dataclass -class WithoutOrder: - x: int - -WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator] -WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator] -WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator] -WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator] - -@dataclass(order=True) -class WithOrder: - x: int - -WithOrder(1) < WithOrder(2) -WithOrder(1) <= WithOrder(2) -WithOrder(1) > WithOrder(2) -WithOrder(1) >= WithOrder(2) -``` - -Comparisons are only allowed for `WithOrder` instances: - -```py -WithOrder(1) < 2 # error: [unsupported-operator] -WithOrder(1) <= 2 # error: [unsupported-operator] -WithOrder(1) > 2 # error: [unsupported-operator] -WithOrder(1) >= 2 # error: [unsupported-operator] -``` - -This also works for generic dataclasses: - -```py -from dataclasses import dataclass - -@dataclass(order=True) -class GenericWithOrder[T]: - x: T - -GenericWithOrder[int](1) < GenericWithOrder[int](1) - -GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator] -``` - -If a class already defines one of the comparison methods, a `TypeError` is raised at runtime. -Ideally, we would emit a diagnostic in that case: - -```py -@dataclass(order=True) -class AlreadyHasCustomDunderLt: - x: int - - # TODO: Ideally, we would emit a diagnostic here - def __lt__(self, other: object) -> bool: - return False -``` - -### `unsafe_hash` - -To do - -### `frozen` - -To do - -### `match_args` - -To do - -### `kw_only` - -To do - -### `slots` - -To do - -### `weakref_slot` - -To do - -## Inheritance - -### Normal class inheriting from a dataclass - -```py -from dataclasses import dataclass - -@dataclass -class Base: - x: int - -class Derived(Base): ... - -d = Derived(1) # OK -reveal_type(d.x) # revealed: int -``` - -### Dataclass inheriting from normal class - -```py -from dataclasses import dataclass - -class Base: - x: int = 1 - -@dataclass -class Derived(Base): - y: str - -d = Derived("a") - -# error: [too-many-positional-arguments] -# error: [invalid-argument-type] -Derived(1, "a") -``` - -### Dataclass inheriting from another dataclass - -```py -from dataclasses import dataclass - -@dataclass -class Base: - x: int - y: str - -@dataclass -class Derived(Base): - z: bool - -d = Derived(1, "a", True) # OK - -reveal_type(d.x) # revealed: int -reveal_type(d.y) # revealed: str -reveal_type(d.z) # revealed: bool - -# error: [missing-argument] -Derived(1, "a") - -# error: [missing-argument] -Derived(True) -``` - -### Overwriting attributes from base class - -The following example comes from the -[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x` -attribute appears just once in the `__init__` signature, and the default value is taken from the -derived class - -```py -from dataclasses import dataclass -from typing import Any - -@dataclass -class Base: - x: Any = 15.0 - y: int = 0 - -@dataclass -class C(Base): - z: int = 10 - x: int = 15 - -reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None -``` - -## Generic dataclasses - -```py -from dataclasses import dataclass - -@dataclass -class DataWithDescription[T]: - data: T - description: str - -reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]] - -d_int = DataWithDescription[int](1, "description") # OK -reveal_type(d_int.data) # revealed: int -reveal_type(d_int.description) # revealed: str - -# error: [invalid-argument-type] -DataWithDescription[int](None, "description") -``` - -## Descriptor-typed fields - -### Same type in `__get__` and `__set__` - -For the following descriptor, the return type of `__get__` and the type of the `value` parameter in -`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of -the type of the descriptor), and the default value is also of this type: - -```py -from typing import overload -from dataclasses import dataclass - -class UppercaseString: - _value: str = "" - - def __get__(self, instance: object, owner: None | type) -> str: - return self._value - - def __set__(self, instance: object, value: str) -> None: - self._value = value.upper() - -@dataclass -class C: - upper: UppercaseString = UppercaseString() - -reveal_type(C.__init__) # revealed: (upper: str = str) -> None - -c = C("abc") -reveal_type(c.upper) # revealed: str - -# This is also okay: -C() - -# error: [invalid-argument-type] -C(1) - -# error: [too-many-positional-arguments] -C("a", "b") -``` - -### Different types in `__get__` and `__set__` - -In general, the type of the `__init__` parameter is determined by the `value` parameter type of the -`__set__` method (`str` in the example below). However, the default value is generated by calling -the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None` -for the `instance` argument. - -```py -from typing import overload -from dataclasses import dataclass - -class ConvertToLength: - _len: int = 0 - - @overload - def __get__(self, instance: None, owner: type) -> str: ... - @overload - def __get__(self, instance: object, owner: type | None) -> int: ... - def __get__(self, instance: object | None, owner: type | None) -> str | int: - if instance is None: - return "" - - return self._len - - def __set__(self, instance, value: str) -> None: - self._len = len(value) - -@dataclass -class C: - converter: ConvertToLength = ConvertToLength() - -# TODO: Should be `(converter: str = Literal[""]) -> None` once we understand overloads -reveal_type(C.__init__) # revealed: (converter: str = str | int) -> None - -c = C("abc") -# TODO: Should be `int` once we understand overloads -reveal_type(c.converter) # revealed: str | int - -# This is also okay: -C() - -# error: [invalid-argument-type] -C(1) - -# error: [too-many-positional-arguments] -C("a", "b") -``` - -### With overloaded `__set__` method - -If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the -union of all possible `value` parameter types: - -```py -from typing import overload -from dataclasses import dataclass - -class AcceptsStrAndInt: - def __get__(self, instance, owner) -> int: - return 0 - - @overload - def __set__(self, instance: object, value: str) -> None: ... - @overload - def __set__(self, instance: object, value: int) -> None: ... - def __set__(self, instance: object, value) -> None: - pass - -@dataclass -class C: - field: AcceptsStrAndInt = AcceptsStrAndInt() - -# TODO: Should be `field: str | int = int` once we understand overloads -reveal_type(C.__init__) # revealed: (field: Unknown = int) -> None -``` - -## `dataclasses.field` - -To do - -## Other special cases - -### `dataclasses.dataclass` - -We also understand dataclasses if they are decorated with the fully qualified name: - -```py -import dataclasses - -@dataclasses.dataclass -class C: - x: str - -reveal_type(C.__init__) # revealed: (x: str) -> None -``` - -### Dataclass with custom `__init__` method - -If a class already defines `__init__`, it is not replaced by the `dataclass` decorator. - -```py -from dataclasses import dataclass - -@dataclass(init=True) -class C: - x: str - - def __init__(self, x: int) -> None: - self.x = str(x) - -C(1) # OK - -# error: [invalid-argument-type] -C("a") -``` - -Similarly, if we set `init=False`, we still recognize the custom `__init__` method: - -```py -@dataclass(init=False) -class D: - def __init__(self, x: int) -> None: - self.x = str(x) - -D(1) # OK -D() # error: [missing-argument] -``` - -### Accessing instance attributes on the class itself - -Just like for normal classes, accessing instance attributes on the class itself is not allowed: - -```py -from dataclasses import dataclass - -@dataclass -class C: - x: int - -# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself." -C.x -``` - -### Return type of `dataclass(...)` - -A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator. -We can store the callable in a variable and later use it as a decorator: - -```py -from dataclasses import dataclass - -dataclass_with_order = dataclass(order=True) - -reveal_type(dataclass_with_order) # revealed: - -@dataclass_with_order -class C: - x: int - -C(1) < C(2) # ok -``` - -### Using `dataclass` as a function - -To do - -## Internals - -The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`, -and attributes like the MRO are unchanged: - -```py -from dataclasses import dataclass - -@dataclass -class Person: - name: str - age: int | None = None - -reveal_type(type(Person)) # revealed: Literal[type] -reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]] -``` - -The generated methods have the following signatures: - -```py -# TODO: `self` is missing here -reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None - -reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str - -reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md deleted file mode 100644 index 3f435d8e5d700..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md +++ /dev/null @@ -1,75 +0,0 @@ -# Errors while declaring - -## Violates previous assignment - -```py -x = 1 -x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`" -``` - -## Incompatible declarations - -```py -def _(flag: bool): - if flag: - x: str - else: - x: int - - x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int" -``` - -## Incompatible declarations for 2 (out of 3) types - -```py -def _(flag1: bool, flag2: bool): - if flag1: - x: str - elif flag2: - x: int - - # Here, the declared type for `x` is `int | str | Unknown`. - x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int" -``` - -## Incompatible declarations with bad assignment - -```py -def _(flag: bool): - if flag: - x: str - else: - x: int - - # error: [conflicting-declarations] - # error: [invalid-assignment] - x = b"foo" -``` - -## No errors - -Currently, we avoid raising the conflicting-declarations for the following cases: - -### Partial declarations - -```py -def _(flag: bool): - if flag: - x: int - - x = 1 -``` - -### Partial declarations in try-except - -Refer to - -```py -def _(): - try: - x: int = 1 - except: - x = 2 - - x = 3 -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md deleted file mode 100644 index d0d3c916df9d2..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +++ /dev/null @@ -1,14 +0,0 @@ -# No matching overload diagnostics - - - -## Calls to overloaded functions - -TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in -real Python code. We are instead testing a special-cased function where we create an overloaded -signature internally. Update this to an `@overload` function in the Python snippet itself once we -can. - -```py -type("Foo", ()) # error: [no-matching-overload] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md deleted file mode 100644 index 481d55a151539..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +++ /dev/null @@ -1,21 +0,0 @@ -# Unpacking - - - -## Right hand side not iterable - -```py -a, b = 1 # error: [not-iterable] -``` - -## Too many values to unpack - -```py -a, b = (1, 2, 3) # error: [invalid-assignment] -``` - -## Too few values to unpack - -```py -a, b = (1,) # error: [invalid-assignment] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md deleted file mode 100644 index 96a01d90d9e5f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ /dev/null @@ -1,70 +0,0 @@ -# `cast` - -`cast()` takes two arguments, one type and one value, and returns a value of the given type. - -The (inferred) type of the value and the given type do not need to have any correlation. - -```py -from typing import Literal, cast, Any - -reveal_type(True) # revealed: Literal[True] -reveal_type(cast(str, True)) # revealed: str -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 - -# 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") - -def function_returning_int() -> int: - return 10 - -# error: [redundant-cast] "Value is already of type `int`" -cast(int, function_returning_int()) - -def function_returning_any() -> Any: - return "blah" - -# error: [redundant-cast] "Value is already of type `Any`" -cast(Any, function_returning_any()) -``` - -Complex type expressions (which may be unsupported) do not lead to spurious `[redundant-cast]` -diagnostics. - -```py -from typing import Callable - -def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]): - a = cast(Callable[[list[bytes]], None], x) - b = cast(tuple[list[bytes]], y) -``` - -A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are -understood as gradually equivalent types by red-knot, they are understood as different types by -human readers of red-knot's output. For `Unknown` in particular, we may consider it differently in -the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to -an invalid annotation, missing annotation or missing type argument somewhere. - -```py -from knot_extensions import Unknown - -def f(x: Any, y: Unknown, z: Any | str | int): - a = cast(dict[str, Any], x) - reveal_type(a) # revealed: @Todo(generics) - - b = cast(Any, y) - reveal_type(b) # revealed: Any - - c = cast(str | int | Any, z) # error: [redundant-cast] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md deleted file mode 100644 index f313532303f7d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md +++ /dev/null @@ -1,179 +0,0 @@ -# Exception Handling - -## Single Exception - -```py -import re - -try: - help() -except NameError as e: - reveal_type(e) # revealed: NameError -except re.error as f: - reveal_type(f) # revealed: error -``` - -## Unknown type in except handler does not cause spurious diagnostic - -```py -from nonexistent_module import foo # error: [unresolved-import] - -try: - help() -except foo as e: - reveal_type(foo) # revealed: Unknown - reveal_type(e) # revealed: Unknown -``` - -## Multiple Exceptions in a Tuple - -```py -EXCEPTIONS = (AttributeError, TypeError) - -try: - help() -except (RuntimeError, OSError) as e: - reveal_type(e) # revealed: RuntimeError | OSError -except EXCEPTIONS as f: - reveal_type(f) # revealed: AttributeError | TypeError -``` - -## Dynamic exception types - -```py -def foo( - x: type[AttributeError], - y: tuple[type[OSError], type[RuntimeError]], - z: tuple[type[BaseException], ...], -): - try: - help() - except x as e: - reveal_type(e) # revealed: AttributeError - except y as f: - reveal_type(f) # revealed: OSError | RuntimeError - except z as g: - # TODO: should be `BaseException` - reveal_type(g) # revealed: @Todo(full tuple[...] support) -``` - -## Invalid exception handlers - -```py -try: - pass -# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" -except 3 as e: - reveal_type(e) # revealed: Unknown - -try: - pass -# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" -# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" -except (ValueError, OSError, "foo", b"bar") as e: - reveal_type(e) # revealed: ValueError | OSError | Unknown - -def foo( - x: type[str], - y: tuple[type[OSError], type[RuntimeError], int], - z: tuple[type[str], ...], -): - try: - help() - # error: [invalid-exception-caught] - except x as e: - reveal_type(e) # revealed: Unknown - # error: [invalid-exception-caught] - except y as f: - reveal_type(f) # revealed: OSError | RuntimeError | Unknown - except z as g: - # TODO: should emit a diagnostic here: - reveal_type(g) # revealed: @Todo(full tuple[...] support) -``` - -## Object raised is not an exception - -```py -try: - raise AttributeError() # fine -except: - ... - -try: - raise FloatingPointError # fine -except: - ... - -try: - raise 1 # error: [invalid-raise] -except: - ... - -try: - raise int # error: [invalid-raise] -except: - ... - -def _(e: Exception | type[Exception]): - raise e # fine - -def _(e: Exception | type[Exception] | None): - raise e # error: [invalid-raise] -``` - -## Exception cause is not an exception - -```py -def _(): - try: - raise EOFError() from GeneratorExit # fine - except: - ... - -def _(): - try: - raise StopIteration from MemoryError() # fine - except: - ... - -def _(): - try: - raise BufferError() from None # fine - except: - ... - -def _(): - try: - raise ZeroDivisionError from False # error: [invalid-raise] - except: - ... - -def _(): - try: - raise SystemExit from bool() # error: [invalid-raise] - except: - ... - -def _(): - try: - raise - except KeyboardInterrupt as e: # fine - reveal_type(e) # revealed: KeyboardInterrupt - raise LookupError from e # fine - -def _(): - try: - raise - except int as e: # error: [invalid-exception-caught] - reveal_type(e) # revealed: Unknown - raise KeyError from e - -def _(e: Exception | type[Exception]): - raise ModuleNotFoundError from e # fine - -def _(e: Exception | type[Exception] | None): - raise IndexError from e # fine - -def _(e: int | None): - raise IndexError from e # error: [invalid-raise] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md deleted file mode 100644 index da1a3de3f040d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md +++ /dev/null @@ -1,66 +0,0 @@ -# `except*` - -`except*` is only available in Python 3.11 and later: - -```toml -[environment] -python-version = "3.11" -``` - -## `except*` with `BaseException` - -```py -try: - help() -except* BaseException as e: - # TODO: should be `BaseExceptionGroup[BaseException]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup -``` - -## `except*` with specific exception - -```py -try: - help() -except* OSError as e: - # TODO: more precise would be `ExceptionGroup[OSError]` --Alex - # (needs homogeneous tuples + generics) - reveal_type(e) # revealed: BaseExceptionGroup -``` - -## `except*` with multiple exceptions - -```py -try: - help() -except* (TypeError, AttributeError) as e: - # TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex - # (needs homogeneous tuples + generics) - reveal_type(e) # revealed: BaseExceptionGroup -``` - -## `except*` with mix of `Exception`s and `BaseException`s - -```py -try: - help() -except* (KeyboardInterrupt, AttributeError) as e: - # TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup -``` - -## Invalid `except*` handlers - -```py -try: - help() -except* 3 as e: # error: [invalid-exception-caught] - # TODO: Should be `BaseExceptionGroup[Unknown]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup - -try: - help() -except* (AttributeError, 42) as e: # error: [invalid-exception-caught] - # TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md b/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md deleted file mode 100644 index 54073f9170fe3..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md +++ /dev/null @@ -1,9 +0,0 @@ -## Condition with object that implements `__bool__` incorrectly - -```py -class NotBoolable: - __bool__: int = 3 - -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" -assert NotBoolable() -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md deleted file mode 100644 index ce3363636d740..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md +++ /dev/null @@ -1,155 +0,0 @@ -# Expressions - -## OR - -```py -def _(foo: str): - reveal_type(True or False) # revealed: Literal[True] - reveal_type("x" or "y" or "z") # revealed: Literal["x"] - reveal_type("" or "y" or "z") # revealed: Literal["y"] - reveal_type(False or "z") # revealed: Literal["z"] - reveal_type(False or True) # revealed: Literal[True] - reveal_type(False or False) # revealed: Literal[False] - reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False] - reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True] -``` - -## AND - -```py -def _(foo: str): - reveal_type(True and False) # revealed: Literal[False] - reveal_type(False and True) # revealed: Literal[False] - reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False] - reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True] - reveal_type("x" and "y" and "z") # revealed: Literal["z"] - reveal_type("x" and "y" and "") # revealed: Literal[""] - reveal_type("" and "y") # revealed: Literal[""] -``` - -## Simple function calls to bool - -```py -def _(flag: bool): - if flag: - x = True - else: - x = False - - reveal_type(x) # revealed: bool -``` - -## Complex - -```py -reveal_type("x" and "y" or "z") # revealed: Literal["y"] -reveal_type("x" or "y" and "z") # revealed: Literal["x"] -reveal_type("" and "y" or "z") # revealed: Literal["z"] -reveal_type("" or "y" and "z") # revealed: Literal["z"] -reveal_type("x" and "y" or "") # revealed: Literal["y"] -reveal_type("x" or "y" and "") # revealed: Literal["x"] -``` - -## `bool()` function - -## Evaluates to builtin - -`a.py`: - -```py -redefined_builtin_bool: type[bool] = bool - -def my_bool(x) -> bool: - return True -``` - -```py -from a import redefined_builtin_bool, my_bool - -reveal_type(redefined_builtin_bool(0)) # revealed: Literal[False] -reveal_type(my_bool(0)) # revealed: bool -``` - -## Truthy values - -```py -reveal_type(bool(1)) # revealed: Literal[True] -reveal_type(bool((0,))) # revealed: Literal[True] -reveal_type(bool("NON EMPTY")) # revealed: Literal[True] -reveal_type(bool(True)) # revealed: Literal[True] - -def foo(): ... - -reveal_type(bool(foo)) # revealed: Literal[True] -``` - -## Falsy values - -```py -reveal_type(bool(0)) # revealed: Literal[False] -reveal_type(bool(())) # revealed: Literal[False] -reveal_type(bool(None)) # revealed: Literal[False] -reveal_type(bool("")) # revealed: Literal[False] -reveal_type(bool(False)) # revealed: Literal[False] -reveal_type(bool()) # revealed: Literal[False] -``` - -## Ambiguous values - -```py -reveal_type(bool([])) # revealed: bool -reveal_type(bool({})) # revealed: bool -reveal_type(bool(set())) # revealed: bool -``` - -## `__bool__` returning `NoReturn` - -```py -from typing import NoReturn - -class NotBoolable: - def __bool__(self) -> NoReturn: - raise NotImplementedError("This object can't be converted to a boolean") - -# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't -# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix. -if NotBoolable(): - ... -``` - -## Not callable `__bool__` - -```py -class NotBoolable: - __bool__: None = None - -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" -if NotBoolable(): - ... -``` - -## Not-boolable union - -```py -def test(cond: bool): - class NotBoolable: - __bool__: int | None = None if cond else 3 - - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" - if NotBoolable(): - ... -``` - -## Union with some variants implementing `__bool__` incorrectly - -```py -def test(cond: bool): - class NotBoolable: - __bool__: None = None - - a = 10 if cond else NotBoolable() - - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable" - if a: - ... -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md b/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md deleted file mode 100644 index 9afeda9f75222..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md +++ /dev/null @@ -1,334 +0,0 @@ -# Function return type - -When a function's return type is annotated, all return statements are checked to ensure that the -type of the returned value is assignable to the annotated return type. - -## Basic examples - -A return value assignable to the annotated return type is valid. - -```py -def f() -> int: - return 1 -``` - -The type of the value obtained by calling a function is the annotated return type, not the inferred -return type. - -```py -reveal_type(f()) # revealed: int -``` - -A `raise` is equivalent to a return of `Never`, which is assignable to any annotated return type. - -```py -def f() -> str: - raise ValueError() - -reveal_type(f()) # revealed: str -``` - -## Stub functions - -"Stub" function definitions (that is, function definitions with an empty body) are permissible in -stub files, or in a few other locations: Protocol method definitions, abstract methods, and -overloads. In this case the function body is considered to be omitted (thus no return type checking -is performed on it), not assumed to implicitly return `None`. - -A stub function's "empty" body may contain only an optional docstring, followed (optionally) by an -ellipsis (`...`) or `pass`. - -### In stub file - -```pyi -def f() -> int: ... - -def f() -> int: - pass - -def f() -> int: - """Some docstring""" - -def f() -> int: - """Some docstring""" - ... -``` - -### In Protocol - -```py -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 - -```py -from abc import ABC, abstractmethod - -class Foo(ABC): - @abstractmethod - def f(self) -> int: ... - @abstractmethod - 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 - -```py -from typing import overload - -@overload -def f(x: int) -> int: ... -@overload -def f(x: str) -> str: ... -def f(x: int | str): - return x -``` - -## Conditional return type - -```py -def f(cond: bool) -> int: - if cond: - return 1 - else: - return 2 - -def f(cond: bool) -> int | None: - if cond: - return 1 - else: - return - -def f(cond: bool) -> int: - if cond: - return 1 - else: - raise ValueError() - -def f(cond: bool) -> str | int: - if cond: - return "a" - else: - return 1 -``` - -## Implicit return type - -```py -def f(cond: bool) -> int | None: - if cond: - return 1 - -# no implicit return -def f() -> int: - if True: - return 1 - -# no implicit return -def f(cond: bool) -> int: - cond = True - if cond: - return 1 - -def f(cond: bool) -> int: - if cond: - cond = True - else: - return 1 - if cond: - return 2 -``` - -## Invalid return type - - - -```py -# error: [invalid-return-type] -def f() -> int: - 1 - -def f() -> str: - # error: [invalid-return-type] - return 1 - -def f() -> int: - # error: [invalid-return-type] - return - -from typing import TypeVar - -T = TypeVar("T") - -# TODO: `invalid-return-type` error should be emitted -def m(x: T) -> T: ... -``` - -## Invalid return type in stub file - - - -```pyi -def f() -> int: - # error: [invalid-return-type] - return ... - -# error: [invalid-return-type] -def foo() -> int: - print("...") - ... - -# error: [invalid-return-type] -def foo() -> int: - f"""{foo} is a function that ...""" - ... -``` - -## Invalid conditional return type - - - -```py -def f(cond: bool) -> str: - if cond: - return "a" - else: - # error: [invalid-return-type] - return 1 - -def f(cond: bool) -> str: - if cond: - # error: [invalid-return-type] - return 1 - else: - # error: [invalid-return-type] - return 2 -``` - -## Invalid implicit return type - - - -```py -def f() -> None: - if False: - # error: [invalid-return-type] - return 1 - -# error: [invalid-return-type] -def f(cond: bool) -> int: - if cond: - return 1 - -# error: [invalid-return-type] -def f(cond: bool) -> int: - if cond: - raise ValueError() - -# error: [invalid-return-type] -def f(cond: bool) -> int: - if cond: - cond = False - else: - return 1 - if cond: - return 2 -``` - -## NotImplemented - -### Default Python version - -`NotImplemented` is a special symbol in Python. It is commonly used to control the fallback behavior -of special dunder methods. You can find more details in the -[documentation](https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations). - -```py -from __future__ import annotations - -class A: - def __add__(self, o: A) -> A: - return NotImplemented -``` - -However, as shown below, `NotImplemented` should not cause issues with the declared return type. - -```py -def f() -> int: - return NotImplemented - -def f(cond: bool) -> int: - if cond: - return 1 - else: - return NotImplemented - -def f(x: int) -> int | str: - if x < 0: - return -1 - elif x == 0: - return NotImplemented - else: - return "test" - -def f(cond: bool) -> str: - return "hello" if cond else NotImplemented - -def f(cond: bool) -> int: - # error: [invalid-return-type] "Return type does not match returned value: Expected `int`, found `Literal["hello"]`" - return "hello" if cond else NotImplemented -``` - -### Python 3.10+ - -Unlike Ellipsis, `_NotImplementedType` remains in `builtins.pyi` regardless of the Python version. -Even if `builtins._NotImplementedType` is fully replaced by `types.NotImplementedType` in the -future, it should still work as expected. - -```toml -[environment] -python-version = "3.10" -``` - -```py -def f() -> int: - return NotImplemented - -def f(cond: bool) -> str: - return "hello" if cond else NotImplemented -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md deleted file mode 100644 index 85574c615c9e6..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ /dev/null @@ -1,332 +0,0 @@ -# Generic classes - -## PEP 695 syntax - -TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic. - -This is a generic class defined using PEP 695 syntax: - -```py -class C[T]: ... -``` - -A class that inherits from a generic class, and fills its type parameters with typevars, is generic: - -```py -class D[U](C[U]): ... -``` - -A class that inherits from a generic class, but fills its type parameters with concrete types, is -_not_ generic: - -```py -class E(C[int]): ... -``` - -A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly -uses the default value for the typevar. In this case, that default type is `Unknown`, so `F` -inherits from `C[Unknown]` and is not itself generic. - -```py -class F(C): ... -``` - -## Legacy syntax - -This is a generic class defined using the legacy syntax: - -```py -from typing import Generic, TypeVar - -T = TypeVar("T") - -# TODO: no error -# error: [invalid-base] -class C(Generic[T]): ... -``` - -A class that inherits from a generic class, and fills its type parameters with typevars, is generic. - -```py -class D(C[T]): ... -``` - -(Examples `E` and `F` from above do not have analogues in the legacy syntax.) - -## Specializing generic classes explicitly - -The type parameter can be specified explicitly: - -```py -class C[T]: - x: T - -reveal_type(C[int]()) # revealed: C[int] -``` - -The specialization must match the generic types: - -```py -# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2" -reveal_type(C[int, int]()) # revealed: Unknown -``` - -If the type variable has an upper bound, the specialized type must satisfy that bound: - -```py -class Bounded[T: int]: ... -class BoundedByUnion[T: int | str]: ... -class IntSubclass(int): ... - -reveal_type(Bounded[int]()) # revealed: Bounded[int] -reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] - -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`" -reveal_type(Bounded[str]()) # revealed: Unknown - -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`" -reveal_type(Bounded[int | str]()) # revealed: Unknown - -reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] -reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass] -reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] -reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] -``` - -If the type variable is constrained, the specialized type must satisfy those constraints: - -```py -class Constrained[T: (int, str)]: ... - -reveal_type(Constrained[int]()) # revealed: Constrained[int] - -# TODO: error: [invalid-argument-type] -# TODO: revealed: Constrained[Unknown] -reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] - -reveal_type(Constrained[str]()) # revealed: Constrained[str] - -# TODO: error: [invalid-argument-type] -# TODO: revealed: Unknown -reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] - -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`" -reveal_type(Constrained[object]()) # revealed: Unknown -``` - -## Inferring generic class parameters - -We can infer the type parameter from a type context: - -```py -class C[T]: - x: T - -c: C[int] = C() -# TODO: revealed: C[int] -reveal_type(c) # revealed: C[Unknown] -``` - -The typevars of a fully specialized generic class should no longer be visible: - -```py -# TODO: revealed: int -reveal_type(c.x) # revealed: Unknown -``` - -If the type parameter is not specified explicitly, and there are no constraints that let us infer a -specific type, we infer the typevar's default type: - -```py -class D[T = int]: ... - -reveal_type(D()) # revealed: D[int] -``` - -If a typevar does not provide a default, we use `Unknown`: - -```py -reveal_type(C()) # revealed: C[Unknown] -``` - -## Inferring generic class parameters from constructors - -If the type of a constructor parameter is a class typevar, we can use that to infer the type -parameter. The types inferred from a type context and from a constructor parameter must be -consistent with each other. - -## `__new__` only - -```py -class C[T]: - def __new__(cls, x: T) -> "C"[T]: - return object.__new__(cls) - -reveal_type(C(1)) # revealed: C[Literal[1]] - -# TODO: error: [invalid-argument-type] -wrong_innards: C[int] = C("five") -``` - -## `__init__` only - -```py -class C[T]: - def __init__(self, x: T) -> None: ... - -reveal_type(C(1)) # revealed: C[Literal[1]] - -# TODO: error: [invalid-argument-type] -wrong_innards: C[int] = C("five") -``` - -## Identical `__new__` and `__init__` signatures - -```py -class C[T]: - def __new__(cls, x: T) -> "C"[T]: - return object.__new__(cls) - - def __init__(self, x: T) -> None: ... - -reveal_type(C(1)) # revealed: C[Literal[1]] - -# TODO: error: [invalid-argument-type] -wrong_innards: C[int] = C("five") -``` - -## Compatible `__new__` and `__init__` signatures - -```py -class C[T]: - def __new__(cls, *args, **kwargs) -> "C"[T]: - return object.__new__(cls) - - def __init__(self, x: T) -> None: ... - -reveal_type(C(1)) # revealed: C[Literal[1]] - -# TODO: error: [invalid-argument-type] -wrong_innards: C[int] = C("five") - -class D[T]: - def __new__(cls, x: T) -> "D"[T]: - return object.__new__(cls) - - def __init__(self, *args, **kwargs) -> None: ... - -reveal_type(D(1)) # revealed: D[Literal[1]] - -# TODO: error: [invalid-argument-type] -wrong_innards: D[int] = D("five") -``` - -## `__init__` is itself generic - -TODO: These do not currently work yet, because we don't correctly model the nested generic contexts. - -```py -class C[T]: - def __init__[S](self, x: T, y: S) -> None: ... - -# TODO: no error -# TODO: revealed: C[Literal[1]] -# error: [invalid-argument-type] -reveal_type(C(1, 1)) # revealed: C[Unknown] -# TODO: no error -# TODO: revealed: C[Literal[1]] -# error: [invalid-argument-type] -reveal_type(C(1, "string")) # revealed: C[Unknown] -# TODO: no error -# TODO: revealed: C[Literal[1]] -# error: [invalid-argument-type] -reveal_type(C(1, True)) # revealed: C[Unknown] - -# TODO: error for the correct reason -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`" -wrong_innards: C[int] = C("five", 1) -``` - -## Generic subclass - -When a generic subclass fills its superclass's type parameter with one of its own, the actual types -propagate through: - -```py -class Base[T]: - x: T | None = None - -class Sub[U](Base[U]): ... - -reveal_type(Base[int].x) # revealed: int | None -reveal_type(Sub[int].x) # revealed: int | None -``` - -## Generic methods - -Generic classes can contain methods that are themselves generic. The generic methods can refer to -the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in -scope for the method. - -```py -class C[T]: - def method[U](self, u: U) -> U: - return u - # error: [unresolved-reference] - def cannot_use_outside_of_method(self, u: U): ... - - # TODO: error - def cannot_shadow_class_typevar[T](self, t: T): ... - -c: C[int] = C[int]() -reveal_type(c.method("string")) # revealed: Literal["string"] -``` - -## Cyclic class definition - -A class can use itself as the type parameter of one of its superclasses. (This is also known as the -[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) - -Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). - -`stub.pyi`: - -```pyi -class Base[T]: ... -class Sub(Base[Sub]): ... - -reveal_type(Sub) # revealed: Literal[Sub] -``` - -A similar case can work in a non-stub file, if forward references are stringified: - -`string_annotation.py`: - -```py -class Base[T]: ... -class Sub(Base["Sub"]): ... - -reveal_type(Sub) # revealed: Literal[Sub] -``` - -In a non-stub file, without stringified forward references, this raises a `NameError`: - -`bare_annotation.py`: - -```py -class Base[T]: ... - -# error: [unresolved-reference] -class Sub(Base[Sub]): ... -``` - -## Another cyclic case - -```pyi -# TODO no error (generics) -# error: [invalid-base] -class Derived[T](list[Derived[T]]): ... -``` - -[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern -[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md deleted file mode 100644 index 7fe3cf9ef0644..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ /dev/null @@ -1,196 +0,0 @@ -# Generic functions - -## Typevar must be used at least twice - -If you're only using a typevar for a single parameter, you don't need the typevar — just use -`object` (or the typevar's upper bound): - -```py -# TODO: error, should be (x: object) -def typevar_not_needed[T](x: T) -> None: - pass - -# TODO: error, should be (x: int) -def bounded_typevar_not_needed[T: int](x: T) -> None: - pass -``` - -Typevars are only needed if you use them more than once. For instance, to specify that two -parameters must both have the same type: - -```py -def two_params[T](x: T, y: T) -> T: - return x -``` - -or to specify that a return value is the same as a parameter: - -```py -def return_value[T](x: T) -> T: - return x -``` - -Each typevar must also appear _somewhere_ in the parameter list: - -```py -def absurd[T]() -> T: - # There's no way to construct a T! - raise ValueError("absurd") -``` - -## Inferring generic function parameter types - -If the type of a generic function parameter is a typevar, then we can infer what type that typevar -is bound to at each call site. - -```py -def f[T](x: T) -> T: - return x - -reveal_type(f(1)) # revealed: Literal[1] -reveal_type(f(1.0)) # revealed: float -reveal_type(f(True)) # revealed: Literal[True] -reveal_type(f("string")) # revealed: Literal["string"] -``` - -## Inferring “deep” generic parameter types - -The matching up of call arguments and discovery of constraints on typevars can be a recursive -process for arbitrarily-nested generic types in parameters. - -```py -def f[T](x: list[T]) -> T: - return x[0] - -# TODO: revealed: float -reveal_type(f([1.0, 2.0])) # revealed: Unknown -``` - -## Typevar constraints - -If a type parameter has an upper bound, that upper bound constrains which types can be used for that -typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar -in the function. - -```py -def good_param[T: int](x: T) -> None: - reveal_type(x) # revealed: T -``` - -If the function is annotated as returning the typevar, this means that the upper bound is _not_ -assignable to that typevar, since return types are contravariant. In `bad`, we can infer that -`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the -return value is not guaranteed to be compatible for all `T: int`. - -```py -def good_return[T: int](x: T) -> T: - return x - -def bad_return[T: int](x: T) -> T: - # error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`" - return x + 1 -``` - -## All occurrences of the same typevar have the same type - -If a typevar appears multiple times in a function signature, all occurrences have the same type. - -```py -def different_types[T, S](cond: bool, t: T, s: S) -> T: - if cond: - return t - else: - # error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`" - return s - -def same_types[T](cond: bool, t1: T, t2: T) -> T: - if cond: - return t1 - else: - return t2 -``` - -## All occurrences of the same constrained typevar have the same type - -The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__` -methods that are compatible with the return type, so the `return` expression is always well-typed: - -```py -def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: - # TODO: no error - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`" - return t1 + t2 -``` - -This is _not_ the same as a union type, because of this additional constraint that the two -occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types, -and an `int` and a `str` cannot be added together: - -```py -def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" - return t1 + t2 -``` - -## Typevar inference is a unification problem - -When inferring typevar assignments in a generic function call, we cannot simply solve constraints -eagerly for each parameter in turn. We must solve a unification problem involving all of the -parameters simultaneously. - -```py -def two_params[T](x: T, y: T) -> T: - return x - -reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] -reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] -``` - -When one of the parameters is a union, we attempt to find the smallest specialization that satisfies -all of the constraints. - -```py -def union_param[T](x: T | None) -> T: - if x is None: - raise ValueError - return x - -reveal_type(union_param("a")) # revealed: Literal["a"] -reveal_type(union_param(1)) # revealed: Literal[1] -reveal_type(union_param(None)) # revealed: Unknown -``` - -```py -def union_and_nonunion_params[T](x: T | int, y: T) -> T: - return y - -reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] -reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] -reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] -reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] -reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] -``` - -```py -def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]: - return y - -reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] -reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] -``` - -## Inferring nested generic function calls - -We can infer type assignments in nested calls to multiple generic functions. If they use the same -type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below. - -```py -def f[T](x: T) -> tuple[T, int]: - return (x, 1) - -def g[T](x: T) -> T | None: - return x - -reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] -reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md deleted file mode 100644 index 6aea25ed820c2..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md +++ /dev/null @@ -1,72 +0,0 @@ -# Legacy type variables - -The tests in this file focus on how type variables are defined using the legacy notation. Most -_uses_ of type variables are tested in other files in this directory; we do not duplicate every test -for both type variable syntaxes. - -Unless otherwise specified, all quotations come from the [Generics] section of the typing spec. - -## Type variables - -### Defining legacy type variables - -> Generics can be parameterized by using a factory available in `typing` called `TypeVar`. - -This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available -in newer Python releases. - -```py -from typing import TypeVar - -T = TypeVar("T") -``` - -### Directly assigned to a variable - -> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as -> part of a larger expression). - -```py -from typing import TypeVar - -# TODO: error -TestList = list[TypeVar("W")] -``` - -### `TypeVar` parameter must match variable name - -> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned. - -```py -from typing import TypeVar - -# TODO: error -T = TypeVar("Q") -``` - -### No redefinition - -> Type variables must not be redefined. - -```py -from typing import TypeVar - -T = TypeVar("T") - -# TODO: error -T = TypeVar("T") -``` - -### Cannot have only one constraint - -> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should -> be at least two constraints, if any; specifying a single constraint is disallowed. - -```py -from typing import TypeVar - -# TODO: error: [invalid-type-variable-constraints] -T = TypeVar("T", int) -``` - -[generics]: https://typing.python.org/en/latest/spec/generics.html diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md deleted file mode 100644 index 907e18c3073ac..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ /dev/null @@ -1,584 +0,0 @@ -# PEP 695 Generics - -[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables. - -## Type variables - -### Defining PEP 695 type variables - -PEP 695 introduces a new syntax for defining type variables. The resulting type variables are -instances of `typing.TypeVar`, just like legacy type variables. - -```py -def f[T](): - reveal_type(type(T)) # revealed: Literal[TypeVar] - reveal_type(T) # revealed: T - reveal_type(T.__name__) # revealed: Literal["T"] -``` - -### Cannot have only one constraint - -> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should -> be at least two constraints, if any; specifying a single constraint is disallowed. - -```py -# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types" -def f[T: (int,)](): - pass -``` - -## Invalid uses - -Note that many of the invalid uses of legacy typevars do not apply to PEP 695 typevars, since the -PEP 695 syntax is only allowed places where typevars are allowed. - -## Displaying typevars - -We use a suffix when displaying the typevars of a generic function or class. This helps distinguish -different uses of the same typevar. - -```py -def f[T](x: T, y: T) -> None: - # TODO: revealed: T@f - reveal_type(x) # revealed: T - -class C[T]: - def m(self, x: T) -> None: - # TODO: revealed: T@c - reveal_type(x) # revealed: T -``` - -## Fully static typevars - -We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This -is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This -is similar to how you can assign an expression whose type is not fully static to a target whose type -is.) - -```py -from knot_extensions import is_fully_static, static_assert -from typing import Any - -def unbounded_unconstrained[T](t: list[T]) -> None: - static_assert(is_fully_static(T)) - -def bounded[T: int](t: list[T]) -> None: - static_assert(is_fully_static(T)) - -def bounded_by_gradual[T: Any](t: list[T]) -> None: - static_assert(not is_fully_static(T)) - -def constrained[T: (int, str)](t: list[T]) -> None: - static_assert(is_fully_static(T)) - -def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: - static_assert(not is_fully_static(T)) -``` - -## Subtyping and assignability - -(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static -typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual -typevars.) - -We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be -specialized to. Properties are true of the typevar only if they are true for every valid -specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type -(including other typevars). - -```py -from knot_extensions import is_assignable_to, is_subtype_of, static_assert - -class Super: ... -class Base(Super): ... -class Sub(Base): ... -class Unrelated: ... - -def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: - static_assert(is_assignable_to(T, T)) - static_assert(is_assignable_to(T, object)) - static_assert(not is_assignable_to(T, Super)) - static_assert(is_assignable_to(U, U)) - static_assert(is_assignable_to(U, object)) - static_assert(not is_assignable_to(U, Super)) - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) - - static_assert(is_subtype_of(T, T)) - static_assert(is_subtype_of(T, object)) - static_assert(not is_subtype_of(T, Super)) - static_assert(is_subtype_of(U, U)) - static_assert(is_subtype_of(U, object)) - static_assert(not is_subtype_of(U, Super)) - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) -``` - -A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of -its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does -not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the -typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound -is a final class, since the typevar can still be specialized to `Never`.) - -```py -from typing import Any -from typing_extensions import final - -def bounded[T: Super](t: list[T]) -> None: - static_assert(is_assignable_to(T, Super)) - static_assert(not is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(Super, T)) - static_assert(not is_assignable_to(Sub, T)) - - static_assert(is_subtype_of(T, Super)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(Sub, T)) - -def bounded_by_gradual[T: Any](t: list[T]) -> None: - static_assert(is_assignable_to(T, Any)) - static_assert(is_assignable_to(Any, T)) - static_assert(is_assignable_to(T, Super)) - static_assert(not is_assignable_to(Super, T)) - static_assert(is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(Sub, T)) - - static_assert(not is_subtype_of(T, Any)) - static_assert(not is_subtype_of(Any, T)) - static_assert(not is_subtype_of(T, Super)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(Sub, T)) - -@final -class FinalClass: ... - -def bounded_final[T: FinalClass](t: list[T]) -> None: - static_assert(is_assignable_to(T, FinalClass)) - static_assert(not is_assignable_to(FinalClass, T)) - - static_assert(is_subtype_of(T, FinalClass)) - static_assert(not is_subtype_of(FinalClass, T)) -``` - -Two distinct fully static typevars are not subtypes of each other, even if they have the same -bounds, since there is (still) no guarantee that they will be specialized to the same type. This is -true even if both typevars are bounded by the same final class, since you can specialize the -typevars to `Never` in addition to that final class. - -```py -def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) - - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) - -def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) - - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) -``` - -A constrained fully static typevar is assignable to the union of its constraints, but not to any of -the constraints individually. None of the constraints are subtypes of the typevar, though the -intersection of all of its constraints is a subtype of the typevar. - -```py -from knot_extensions import Intersection - -def constrained[T: (Base, Unrelated)](t: list[T]) -> None: - static_assert(not is_assignable_to(T, Super)) - static_assert(not is_assignable_to(T, Base)) - static_assert(not is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(T, Unrelated)) - static_assert(is_assignable_to(T, Super | Unrelated)) - static_assert(is_assignable_to(T, Base | Unrelated)) - static_assert(not is_assignable_to(T, Sub | Unrelated)) - static_assert(not is_assignable_to(Super, T)) - static_assert(not is_assignable_to(Unrelated, T)) - static_assert(not is_assignable_to(Super | Unrelated, T)) - static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) - - static_assert(not is_subtype_of(T, Super)) - static_assert(not is_subtype_of(T, Base)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(T, Unrelated)) - static_assert(is_subtype_of(T, Super | Unrelated)) - static_assert(is_subtype_of(T, Base | Unrelated)) - static_assert(not is_subtype_of(T, Sub | Unrelated)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(Unrelated, T)) - static_assert(not is_subtype_of(Super | Unrelated, T)) - static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) - -def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None: - static_assert(is_assignable_to(T, Super)) - static_assert(is_assignable_to(T, Base)) - static_assert(not is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(T, Unrelated)) - static_assert(is_assignable_to(T, Any)) - static_assert(is_assignable_to(T, Super | Any)) - static_assert(is_assignable_to(T, Super | Unrelated)) - static_assert(not is_assignable_to(Super, T)) - static_assert(is_assignable_to(Base, T)) - static_assert(not is_assignable_to(Unrelated, T)) - static_assert(is_assignable_to(Any, T)) - static_assert(not is_assignable_to(Super | Any, T)) - static_assert(is_assignable_to(Base | Any, T)) - static_assert(not is_assignable_to(Super | Unrelated, T)) - static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) - static_assert(is_assignable_to(Intersection[Base, Any], T)) - - static_assert(not is_subtype_of(T, Super)) - static_assert(not is_subtype_of(T, Base)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(T, Unrelated)) - static_assert(not is_subtype_of(T, Any)) - static_assert(not is_subtype_of(T, Super | Any)) - static_assert(not is_subtype_of(T, Super | Unrelated)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(Base, T)) - static_assert(not is_subtype_of(Unrelated, T)) - static_assert(not is_subtype_of(Any, T)) - static_assert(not is_subtype_of(Super | Any, T)) - static_assert(not is_subtype_of(Base | Any, T)) - static_assert(not is_subtype_of(Super | Unrelated, T)) - static_assert(not is_subtype_of(Intersection[Base, Unrelated], T)) - static_assert(not is_subtype_of(Intersection[Base, Any], T)) -``` - -Two distinct fully static typevars are not subtypes of each other, even if they have the same -constraints, and even if any of the constraints are final. There must always be at least two -distinct constraints, meaning that there is (still) no guarantee that they will be specialized to -the same type. - -```py -def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) - - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) - -@final -class AnotherFinalClass: ... - -def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) - - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) -``` - -## Singletons and single-valued types - -(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the -claims also apply to _single-valued_ types.) - -An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a -non-singleton type. - -```py -from knot_extensions import is_singleton, is_single_valued, static_assert - -def unbounded_unconstrained[T](t: list[T]) -> None: - static_assert(not is_singleton(T)) - static_assert(not is_single_valued(T)) -``` - -A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be -specialized to `Never`. - -```py -def bounded[T: None](t: list[T]) -> None: - static_assert(not is_singleton(T)) - static_assert(not is_single_valued(T)) -``` - -A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot -specialize a constrained typevar to a subtype of a constraint.) - -```py -from typing_extensions import Literal - -def constrained_non_singletons[T: (int, str)](t: list[T]) -> None: - static_assert(not is_singleton(T)) - static_assert(not is_single_valued(T)) - -def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None: - static_assert(is_singleton(T)) - -def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None: - static_assert(is_single_valued(T)) -``` - -## Unions involving typevars - -The union of an unbounded unconstrained typevar with any other type cannot be simplified, since -there is no guarantee what type the typevar will be specialized to. - -```py -from typing import Any - -class Super: ... -class Base(Super): ... -class Sub(Base): ... -class Unrelated: ... - -def unbounded_unconstrained[T](t: T) -> None: - def _(x: T | Super) -> None: - reveal_type(x) # revealed: T | Super - - def _(x: T | Base) -> None: - reveal_type(x) # revealed: T | Base - - def _(x: T | Sub) -> None: - reveal_type(x) # revealed: T | Sub - - def _(x: T | Unrelated) -> None: - reveal_type(x) # revealed: T | Unrelated - - def _(x: T | Any) -> None: - reveal_type(x) # revealed: T | Any -``` - -The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be -specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound -cannot be simplified. (The typevar might be specialized to a different subtype of the bound.) - -```py -def bounded[T: Base](t: T) -> None: - def _(x: T | Super) -> None: - reveal_type(x) # revealed: Super - - def _(x: T | Base) -> None: - reveal_type(x) # revealed: Base - - def _(x: T | Sub) -> None: - reveal_type(x) # revealed: T | Sub - - def _(x: T | Unrelated) -> None: - reveal_type(x) # revealed: T | Unrelated - - def _(x: T | Any) -> None: - reveal_type(x) # revealed: T | Any -``` - -The union of a constrained typevar with a type depends on how that type relates to the constraints. -If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely, -if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the -union cannot be simplified. - -```py -def constrained[T: (Base, Sub)](t: T) -> None: - def _(x: T | Super) -> None: - reveal_type(x) # revealed: Super - - def _(x: T | Base) -> None: - reveal_type(x) # revealed: Base - - def _(x: T | Sub) -> None: - reveal_type(x) # revealed: T - - def _(x: T | Unrelated) -> None: - reveal_type(x) # revealed: T | Unrelated - - def _(x: T | Any) -> None: - reveal_type(x) # revealed: T | Any -``` - -## Intersections involving typevars - -The intersection of an unbounded unconstrained typevar with any other type cannot be simplified, -since there is no guarantee what type the typevar will be specialized to. - -```py -from knot_extensions import Intersection -from typing import Any - -class Super: ... -class Base(Super): ... -class Sub(Base): ... -class Unrelated: ... - -def unbounded_unconstrained[T](t: T) -> None: - def _(x: Intersection[T, Super]) -> None: - reveal_type(x) # revealed: T & Super - - def _(x: Intersection[T, Base]) -> None: - reveal_type(x) # revealed: T & Base - - def _(x: Intersection[T, Sub]) -> None: - reveal_type(x) # revealed: T & Sub - - def _(x: Intersection[T, Unrelated]) -> None: - reveal_type(x) # revealed: T & Unrelated - - def _(x: Intersection[T, Any]) -> None: - reveal_type(x) # revealed: T & Any -``` - -The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar -itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded -typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a -different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint -from its bound is `Never`. - -```py -def bounded[T: Base](t: T) -> None: - def _(x: Intersection[T, Super]) -> None: - reveal_type(x) # revealed: T - - def _(x: Intersection[T, Base]) -> None: - reveal_type(x) # revealed: T - - def _(x: Intersection[T, Sub]) -> None: - reveal_type(x) # revealed: T & Sub - - def _(x: Intersection[T, None]) -> None: - reveal_type(x) # revealed: Never - - def _(x: Intersection[T, Any]) -> None: - reveal_type(x) # revealed: T & Any -``` - -Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must -be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints, -since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The -`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it -would not represent a single set of runtime objects; it would instead represent a _set of_ sets of -runtime objects. This is one reason we have not actually added this connector to our data model yet. -Nevertheless, describing constrained typevars this way helps explain how we simplify intersections -involving them. - -This means that when intersecting a constrained typevar with a type `T`, constraints that are -supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover, -constraints that are disjoint from `T` are no longer valid specializations of the typevar, since -`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we -can simplify the intersection as a whole to that constraint. - -```py -def constrained[T: (Base, Sub, Unrelated)](t: T) -> None: - def _(x: Intersection[T, Base]) -> None: - # With OneOf this would be OneOf[Base, Sub] - reveal_type(x) # revealed: T & Base - - def _(x: Intersection[T, Unrelated]) -> None: - reveal_type(x) # revealed: Unrelated - - def _(x: Intersection[T, Sub]) -> None: - reveal_type(x) # revealed: Sub - - def _(x: Intersection[T, None]) -> None: - reveal_type(x) # revealed: Never - - def _(x: Intersection[T, Any]) -> None: - reveal_type(x) # revealed: T & Any -``` - -We can simplify the intersection similarly when removing a type from a constrained typevar, since -this is modeled internally as an intersection with a negation. - -```py -from knot_extensions import Not - -def remove_constraint[T: (int, str, bool)](t: T) -> None: - def _(x: Intersection[T, Not[int]]) -> None: - reveal_type(x) # revealed: str & ~int - - def _(x: Intersection[T, Not[str]]) -> None: - # With OneOf this would be OneOf[int, bool] - reveal_type(x) # revealed: T & ~str - - def _(x: Intersection[T, Not[bool]]) -> None: - reveal_type(x) # revealed: T & ~bool - - def _(x: Intersection[T, Not[int], Not[str]]) -> None: - reveal_type(x) # revealed: Never - - def _(x: Intersection[T, Not[None]]) -> None: - reveal_type(x) # revealed: T - - def _(x: Intersection[T, Not[Any]]) -> None: - reveal_type(x) # revealed: T & Any -``` - -The intersection of a typevar with any other type is assignable to (and if fully static, a subtype -of) itself. - -```py -from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not - -def intersection_is_assignable[T](t: T) -> None: - static_assert(is_assignable_to(Intersection[T, None], T)) - static_assert(is_assignable_to(Intersection[T, Not[None]], T)) - - static_assert(is_subtype_of(Intersection[T, None], T)) - static_assert(is_subtype_of(Intersection[T, Not[None]], T)) -``` - -## Narrowing - -We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar: - -```py -class P: ... -class Q: ... -class R: ... - -def f[T: (P, Q)](t: T) -> None: - if isinstance(t, P): - reveal_type(t) # revealed: P - p: P = t - else: - reveal_type(t) # revealed: Q & ~P - q: Q = t - - if isinstance(t, Q): - reveal_type(t) # revealed: Q - q: Q = t - else: - reveal_type(t) # revealed: P & ~Q - p: P = t - -def g[T: (P, Q, R)](t: T) -> None: - if isinstance(t, P): - reveal_type(t) # revealed: P - p: P = t - elif isinstance(t, Q): - reveal_type(t) # revealed: Q & ~P - q: Q = t - else: - reveal_type(t) # revealed: R & ~P & ~Q - r: R = t - - if isinstance(t, P): - reveal_type(t) # revealed: P - p: P = t - elif isinstance(t, Q): - reveal_type(t) # revealed: Q & ~P - q: Q = t - elif isinstance(t, R): - reveal_type(t) # revealed: R & ~P & ~Q - r: R = t - else: - reveal_type(t) # revealed: Never -``` - -If the constraints are disjoint, simplification does eliminate the redundant negative: - -```py -def h[T: (P, None)](t: T) -> None: - if t is None: - reveal_type(t) # revealed: None - p: None = t - else: - reveal_type(t) # revealed: P - p: P = t -``` - -[pep 695]: https://peps.python.org/pep-0695/ diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/basic.md b/crates/red_knot_python_semantic/resources/mdtest/import/basic.md deleted file mode 100644 index df890bffdab7f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/import/basic.md +++ /dev/null @@ -1,166 +0,0 @@ -# Structures - -## Class import following - -```py -from b import C as D - -E = D -reveal_type(E) # revealed: Literal[C] -``` - -`b.py`: - -```py -class C: ... -``` - -## Module member resolution - -```py -import b - -D = b.C -reveal_type(D) # revealed: Literal[C] -``` - -`b.py`: - -```py -class C: ... -``` - -## Nested - -```py -import a.b - -reveal_type(a.b.C) # revealed: Literal[C] -``` - -`a/__init__.py`: - -```py -``` - -`a/b.py`: - -```py -class C: ... -``` - -## Deeply nested - -```py -import a.b.c - -reveal_type(a.b.c.C) # revealed: Literal[C] -``` - -`a/__init__.py`: - -```py -``` - -`a/b/__init__.py`: - -```py -``` - -`a/b/c.py`: - -```py -class C: ... -``` - -## Nested with rename - -```py -import a.b as b - -reveal_type(b.C) # revealed: Literal[C] -``` - -`a/__init__.py`: - -```py -``` - -`a/b.py`: - -```py -class C: ... -``` - -## Deeply nested with rename - -```py -import a.b.c as c - -reveal_type(c.C) # revealed: Literal[C] -``` - -`a/__init__.py`: - -```py -``` - -`a/b/__init__.py`: - -```py -``` - -`a/b/c.py`: - -```py -class C: ... -``` - -## Unresolvable module import - - - -```py -import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" -``` - -## Unresolvable submodule imports - - - -```py -# Topmost component resolvable, submodule not resolvable: -import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`" - -# Topmost component unresolvable: -import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" -``` - -`a/__init__.py`: - -```py -``` - -## Long paths - -It's unlikely that a single module component is as long as in this example, but Windows treats paths -that are longer than 200 and something specially. This test ensures that Red Knot can handle those -paths gracefully. - -```toml -system = "os" -``` - -`AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath/__init__.py`: - -```py -class Foo: ... -``` - -```py -from AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath import ( - Foo, -) - -reveal_type(Foo()) # revealed: Foo -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md deleted file mode 100644 index 1b2305fb410c2..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md +++ /dev/null @@ -1,78 +0,0 @@ -# Builtins - -## Importing builtin module - -Builtin symbols can be explicitly imported: - -```py -import builtins - -reveal_type(builtins.chr) # revealed: def chr(i: int | SupportsIndex, /) -> str -``` - -## Implicit use of builtin - -Or used implicitly: - -```py -reveal_type(chr) # revealed: def chr(i: int | SupportsIndex, /) -> str -reveal_type(str) # revealed: Literal[str] -``` - -## Builtin symbol from custom typeshed - -If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the -builtins from the "actual" vendored typeshed: - -```toml -[environment] -typeshed = "/typeshed" -``` - -`/typeshed/stdlib/builtins.pyi`: - -```pyi -class Custom: ... - -custom_builtin: Custom -``` - -`/typeshed/stdlib/typing_extensions.pyi`: - -```pyi -def reveal_type(obj, /): ... -``` - -```py -reveal_type(custom_builtin) # revealed: Custom - -# error: [unresolved-reference] -reveal_type(str) # revealed: Unknown -``` - -## Unknown builtin (later defined) - -`foo` has a type of `Unknown` in this example, as it relies on `bar` which has not been defined at -that point: - -```toml -[environment] -typeshed = "/typeshed" -``` - -`/typeshed/stdlib/builtins.pyi`: - -```pyi -foo = bar -bar = 1 -``` - -`/typeshed/stdlib/typing_extensions.pyi`: - -```pyi -def reveal_type(obj, /): ... -``` - -```py -reveal_type(foo) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/errors.md b/crates/red_knot_python_semantic/resources/mdtest/import/errors.md deleted file mode 100644 index 22e747d351cbe..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/import/errors.md +++ /dev/null @@ -1,88 +0,0 @@ -# Unresolved Imports - -## Unresolved import statement - -```py -import bar # error: "Cannot resolve import `bar`" - -reveal_type(bar) # revealed: Unknown -``` - -## Unresolved import from statement - -```py -from bar import baz # error: "Cannot resolve import `bar`" - -reveal_type(baz) # revealed: Unknown -``` - -## Unresolved import from resolved module - -`a.py`: - -```py -``` - -```py -from a import thing # error: "Module `a` has no member `thing`" - -reveal_type(thing) # revealed: Unknown -``` - -## Resolved import of symbol from unresolved import - -`a.py`: - -```py -import foo as foo # error: "Cannot resolve import `foo`" - -reveal_type(foo) # revealed: Unknown -``` - -Importing the unresolved import into a second file should not trigger an additional "unresolved -import" violation: - -```py -from a import foo - -reveal_type(foo) # revealed: Unknown -``` - -## No implicit shadowing - -`b.py`: - -```py -x: int -``` - -```py -from b import x - -x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]" -``` - -## Import cycle - -`a.py`: - -```py -class A: ... - -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]] -import b - -class C(b.B): ... - -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]] -``` - -`b.py`: - -```py -from a import A - -class B(A): ... - -reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/invalid_syntax.md b/crates/red_knot_python_semantic/resources/mdtest/invalid_syntax.md deleted file mode 100644 index 2de26eb1d30c2..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/invalid_syntax.md +++ /dev/null @@ -1,106 +0,0 @@ -# Syntax errors - -Test cases to ensure that red knot does not panic if there are syntax errors in the source code. - -The parser cannot recover from certain syntax errors completely which is why the number of syntax -errors could be more than expected in the following examples. For instance, if there's a keyword -(like `for`) in the middle of another statement (like function definition), then it's more likely -that the rest of the tokens are going to be part of the `for` statement and not the function -definition. But, it's not necessary that the remaining tokens are valid in the context of a `for` -statement. - -## Keyword as identifiers - -When keywords are used as identifiers, the parser recovers from this syntax error by emitting an -error and including the text value of the keyword to create the `Identifier` node. - -### Name expression - -#### Assignment - -```py -# error: [invalid-syntax] -pass = 1 -``` - -#### Type alias - -```py -# error: [invalid-syntax] -# error: [invalid-syntax] -type pass = 1 -``` - -#### Function definition - -```py -# error: [invalid-syntax] -# error: [invalid-syntax] -# error: [invalid-syntax] -# error: [invalid-syntax] -# error: [invalid-syntax] -def True(for): - # error: [invalid-syntax] - # error: [invalid-syntax] - pass -``` - -#### For - -```py -# error: [invalid-syntax] -# error: [invalid-syntax] -# error: [unresolved-reference] "Name `pass` used when not defined" -for while in pass: - pass -``` - -#### While - -```py -# error: [invalid-syntax] -# error: [unresolved-reference] "Name `in` used when not defined" -while in: - pass -``` - -#### Match - -```py -# error: [invalid-syntax] -# error: [invalid-syntax] -# error: [unresolved-reference] "Name `match` used when not defined" -match while: - # error: [invalid-syntax] - # error: [invalid-syntax] - # error: [invalid-syntax] - # error: [unresolved-reference] "Name `case` used when not defined" - case in: - # error: [invalid-syntax] - # error: [invalid-syntax] - pass -``` - -### Attribute expression - -```py -# TODO: Check when support for attribute expressions is added - -# error: [invalid-syntax] -# error: [unresolved-reference] "Name `foo` used when not defined" -for x in foo.pass: - pass -``` - -## Invalid annotation - -### `typing.Callable` - -```py -from typing import Callable - -# error: [invalid-syntax] "Expected index or slice expression" -# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" -def _(c: Callable[]): - reveal_type(c) # revealed: (...) -> Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/literal/bytes.md deleted file mode 100644 index 0cf6222c534ee..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/bytes.md +++ /dev/null @@ -1,10 +0,0 @@ -# Bytes literals - -## Simple - -```py -reveal_type(b"red" b"knot") # revealed: Literal[b"redknot"] -reveal_type(b"hello") # revealed: Literal[b"hello"] -reveal_type(b"world" + b"!") # revealed: Literal[b"world!"] -reveal_type(b"\xff\x00") # revealed: Literal[b"\xff\x00"] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md deleted file mode 100644 index 9a57135f2a343..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ /dev/null @@ -1,7 +0,0 @@ -# Dictionaries - -## Empty dictionary - -```py -reveal_type({}) # revealed: dict -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md deleted file mode 100644 index 70c98aa3a60d9..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md +++ /dev/null @@ -1,7 +0,0 @@ -# Lists - -## Empty list - -```py -reveal_type([]) # revealed: list -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md deleted file mode 100644 index 452fc719dbca7..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md +++ /dev/null @@ -1,7 +0,0 @@ -# Sets - -## Basic set - -```py -reveal_type({1, 2}) # revealed: set -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/mro.md b/crates/red_knot_python_semantic/resources/mdtest/mro.md deleted file mode 100644 index 7803e45e56288..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/mro.md +++ /dev/null @@ -1,408 +0,0 @@ -# Method Resolution Order tests - -Tests that assert that we can infer the correct type for a class's `__mro__` attribute. - -This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to -know the precise possible values of a class's Method Resolution Order, or we won't be able to infer -the correct type of attributes accessed from instances. - -For documentation on method resolution orders, see: - -- -- - -## No bases - -```py -class C: ... - -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]] -``` - -## The special case: `object` itself - -```py -reveal_type(object.__mro__) # revealed: tuple[Literal[object]] -``` - -## Explicit inheritance from `object` - -```py -class C(object): ... - -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]] -``` - -## Explicit inheritance from non-`object` single base - -```py -class A: ... -class B(A): ... - -reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]] -``` - -## Linearization of multiple bases - -```py -class A: ... -class B: ... -class C(A, B): ... - -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]] -``` - -## Complex diamond inheritance (1) - -This is "ex_2" from - -```py -class O: ... -class X(O): ... -class Y(O): ... -class A(X, Y): ... -class B(Y, X): ... - -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]] -reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]] -``` - -## Complex diamond inheritance (2) - -This is "ex_5" from - -```py -class O: ... -class F(O): ... -class E(O): ... -class D(O): ... -class C(D, F): ... -class B(D, E): ... -class A(B, C): ... - -# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]] -reveal_type(C.__mro__) -# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]] -reveal_type(B.__mro__) -# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]] -reveal_type(A.__mro__) -``` - -## Complex diamond inheritance (3) - -This is "ex_6" from - -```py -class O: ... -class F(O): ... -class E(O): ... -class D(O): ... -class C(D, F): ... -class B(E, D): ... -class A(B, C): ... - -# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]] -reveal_type(C.__mro__) -# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]] -reveal_type(B.__mro__) -# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]] -reveal_type(A.__mro__) -``` - -## Complex diamond inheritance (4) - -This is "ex_9" from - -```py -class O: ... -class A(O): ... -class B(O): ... -class C(O): ... -class D(O): ... -class E(O): ... -class K1(A, B, C): ... -class K2(D, B, E): ... -class K3(D, A): ... -class Z(K1, K2, K3): ... - -# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]] -reveal_type(K1.__mro__) -# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]] -reveal_type(K2.__mro__) -# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]] -reveal_type(K3.__mro__) -# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]] -reveal_type(Z.__mro__) -``` - -## Inheritance from `Unknown` - -```py -from does_not_exist import DoesNotExist # error: [unresolved-import] - -class A(DoesNotExist): ... -class B: ... -class C: ... -class D(A, B, C): ... -class E(B, C): ... -class F(E, A): ... - -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]] -reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]] -reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]] -reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]] -``` - -## `__bases__` lists that cause errors at runtime - -If the class's `__bases__` cause an exception to be raised at runtime and therefore the class -creation to fail, we infer the class's `__mro__` as being `[, Unknown, object]`: - -```py -# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[, ]`" -class Foo(object, int): ... - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] - -class Bar(Foo): ... - -reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]] - -# This is the `TypeError` at the bottom of "ex_2" -# in the examples at -class O: ... -class X(O): ... -class Y(O): ... -class A(X, Y): ... -class B(Y, X): ... - -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]] -reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]] - -# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[, ]`" -class Z(A, B): ... - -reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]] - -class AA(Z): ... - -reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]] -``` - -## `__bases__` includes a `Union` - -We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`. -If we find a union type in a class's bases, we infer the class's `__mro__` as being -`[, Unknown, object]`, the same as for MROs that cause errors at runtime. - -```py -def returns_bool() -> bool: - return True - -class A: ... -class B: ... - -if returns_bool(): - x = A -else: - x = B - -reveal_type(x) # revealed: Literal[A, B] - -# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -class Foo(x): ... - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -``` - -## `__bases__` includes multiple `Union`s - -```py -def returns_bool() -> bool: - return True - -class A: ... -class B: ... -class C: ... -class D: ... - -if returns_bool(): - x = A -else: - x = B - -if returns_bool(): - y = C -else: - y = D - -reveal_type(x) # revealed: Literal[A, B] -reveal_type(y) # revealed: Literal[C, D] - -# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -class Foo(x, y): ... - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -``` - -## `__bases__` lists that cause errors... now with `Union`s - -```py -def returns_bool() -> bool: - return True - -class O: ... -class X(O): ... -class Y(O): ... - -if returns_bool(): - foo = Y -else: - foo = object - -# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -class PossibleError(foo, X): ... - -reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]] - -class A(X, Y): ... - -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]] - -if returns_bool(): - class B(X, Y): ... - -else: - class B(Y, X): ... - -# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]] -reveal_type(B.__mro__) - -# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -class Z(A, B): ... - -reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]] -``` - -## `__bases__` lists with duplicate bases - -```py -class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`" - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] - -class Spam: ... -class Eggs: ... -class Ham( - Spam, - Eggs, - Spam, # error: [duplicate-base] "Duplicate base class `Spam`" - Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`" -): ... - -reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]] - -class Mushrooms: ... -class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] - -reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]] -``` - -## `__bases__` lists with duplicate `Unknown` bases - -```py -# error: [unresolved-import] -# error: [unresolved-import] -from does_not_exist import unknown_object_1, unknown_object_2 - -reveal_type(unknown_object_1) # revealed: Unknown -reveal_type(unknown_object_2) # revealed: Unknown - -# We *should* emit an error here to warn the user that we have no idea -# what the MRO of this class should really be. -# However, we don't complain about "duplicate base classes" here, -# even though two classes are both inferred as being `Unknown`. -# -# (TODO: should we revisit this? Does it violate the gradual guarantee? -# Should we just silently infer `[Foo, Unknown, object]` as the MRO here -# without emitting any error at all? Not sure...) -# -# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`" -class Foo(unknown_object_1, unknown_object_2): ... - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -``` - -## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes - -```py -from does_not_exist import unknown_object # error: [unresolved-import] - -reveal_type(unknown_object) # revealed: Unknown -reveal_type(unknown_object.__mro__) # revealed: Unknown -``` - -## Classes that inherit from themselves - -These are invalid, but we need to be able to handle them gracefully without panicking. - -```pyi -class Foo(Foo): ... # error: [cyclic-class-definition] - -reveal_type(Foo) # revealed: Literal[Foo] -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] - -class Bar: ... -class Baz: ... -class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition] - -reveal_type(Boz) # revealed: Literal[Boz] -reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]] -``` - -## Classes with indirect cycles in their MROs - -These are similarly unlikely, but we still shouldn't crash: - -```pyi -class Foo(Bar): ... # error: [cyclic-class-definition] -class Bar(Baz): ... # error: [cyclic-class-definition] -class Baz(Foo): ... # error: [cyclic-class-definition] - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]] -reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]] -``` - -## Classes with cycles in their MROs, and multiple inheritance - -```pyi -class Spam: ... -class Foo(Bar): ... # error: [cyclic-class-definition] -class Bar(Baz): ... # error: [cyclic-class-definition] -class Baz(Foo, Spam): ... # error: [cyclic-class-definition] - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]] -reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]] -``` - -## Classes with cycles in their MRO, and a sub-graph - -```pyi -class FooCycle(BarCycle): ... # error: [cyclic-class-definition] -class Foo: ... -class BarCycle(FooCycle): ... # error: [cyclic-class-definition] -class Bar(Foo): ... - -# Avoid emitting the errors for these. The classes have cyclic superclasses, -# but are not themselves cyclic... -class Baz(Bar, BarCycle): ... -class Spam(Baz): ... - -reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]] -reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]] -reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]] -reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md deleted file mode 100644 index c0e1af2f3dd9a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md +++ /dev/null @@ -1,225 +0,0 @@ -# Narrowing for conditionals with boolean expressions - -## Narrowing in `and` conditional - -```py -class A: ... -class B: ... - -def _(x: A | B): - if isinstance(x, A) and isinstance(x, B): - reveal_type(x) # revealed: A & B - else: - reveal_type(x) # revealed: B & ~A | A & ~B -``` - -## Arms might not add narrowing constraints - -```py -class A: ... -class B: ... - -def _(flag: bool, x: A | B): - if isinstance(x, A) and flag: - reveal_type(x) # revealed: A - else: - reveal_type(x) # revealed: A | B - - if flag and isinstance(x, A): - reveal_type(x) # revealed: A - else: - reveal_type(x) # revealed: A | B - - reveal_type(x) # revealed: A | B -``` - -## Statically known arms - -```py -class A: ... -class B: ... - -def _(x: A | B): - if isinstance(x, A) and True: - reveal_type(x) # revealed: A - else: - reveal_type(x) # revealed: B & ~A - - if True and isinstance(x, A): - reveal_type(x) # revealed: A - else: - reveal_type(x) # revealed: B & ~A - - if False and isinstance(x, A): - # TODO: should emit an `unreachable code` diagnostic - reveal_type(x) # revealed: A - else: - reveal_type(x) # revealed: A | B - - if False or isinstance(x, A): - reveal_type(x) # revealed: A - else: - reveal_type(x) # revealed: B & ~A - - if True or isinstance(x, A): - reveal_type(x) # revealed: A | B - else: - # TODO: should emit an `unreachable code` diagnostic - reveal_type(x) # revealed: B & ~A - - reveal_type(x) # revealed: A | B -``` - -## The type of multiple symbols can be narrowed down - -```py -class A: ... -class B: ... - -def _(x: A | B, y: A | B): - if isinstance(x, A) and isinstance(y, B): - reveal_type(x) # revealed: A - reveal_type(y) # revealed: B - else: - # No narrowing: Only-one or both checks might have failed - reveal_type(x) # revealed: A | B - reveal_type(y) # revealed: A | B - - reveal_type(x) # revealed: A | B - reveal_type(y) # revealed: A | B -``` - -## Narrowing in `or` conditional - -```py -class A: ... -class B: ... -class C: ... - -def _(x: A | B | C): - if isinstance(x, A) or isinstance(x, B): - reveal_type(x) # revealed: A | B - else: - reveal_type(x) # revealed: C & ~A & ~B -``` - -## In `or`, all arms should add constraint in order to narrow - -```py -class A: ... -class B: ... -class C: ... - -def _(flag: bool, x: A | B | C): - if isinstance(x, A) or isinstance(x, B) or flag: - reveal_type(x) # revealed: A | B | C - else: - reveal_type(x) # revealed: C & ~A & ~B -``` - -## in `or`, all arms should narrow the same set of symbols - -```py -class A: ... -class B: ... -class C: ... - -def _(x: A | B | C, y: A | B | C): - if isinstance(x, A) or isinstance(y, A): - # The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here. - reveal_type(x) # revealed: A | B | C - # The same for `y` - reveal_type(y) # revealed: A | B | C - else: - reveal_type(x) # revealed: B & ~A | C & ~A - reveal_type(y) # revealed: B & ~A | C & ~A - - if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)): - # Here, types of `x` and `y` can be narrowd since all `or` arms constraint them. - reveal_type(x) # revealed: A | B - reveal_type(y) # revealed: A | B - else: - reveal_type(x) # revealed: A | B | C - reveal_type(y) # revealed: A | B | C -``` - -## mixing `and` and `not` - -```py -class A: ... -class B: ... -class C: ... - -def _(x: A | B | C): - if isinstance(x, B) and not isinstance(x, C): - reveal_type(x) # revealed: B & ~C - else: - # ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C - reveal_type(x) # revealed: A & ~B | C -``` - -## mixing `or` and `not` - -```py -class A: ... -class B: ... -class C: ... - -def _(x: A | B | C): - if isinstance(x, B) or not isinstance(x, C): - reveal_type(x) # revealed: B | A & ~C - else: - reveal_type(x) # revealed: C & ~B -``` - -## `or` with nested `and` - -```py -class A: ... -class B: ... -class C: ... - -def _(x: A | B | C): - if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)): - reveal_type(x) # revealed: A | B & ~C - else: - # ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B) - reveal_type(x) # revealed: C & ~A -``` - -## `and` with nested `or` - -```py -class A: ... -class B: ... -class C: ... - -def _(x: A | B | C): - if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)): - # A & (B | ~C) -> (A & B) | (A & ~C) - reveal_type(x) # revealed: A & B | A & ~C - else: - # ~((A & B) | (A & ~C)) -> - # ~(A & B) & ~(A & ~C) -> - # (~A | ~B) & (~A | C) -> - # [(~A | ~B) & ~A] | [(~A | ~B) & C] -> - # ~A | (~A & C) | (~B & C) -> - # ~A | (C & ~B) -> - # ~A | (C & ~B) The positive side of ~A is A | B | C -> - reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B -``` - -## Boolean expression internal narrowing - -```py -def _(x: str | None, y: str | None): - if x is None and y is not x: - reveal_type(y) # revealed: str - - # Neither of the conditions alone is sufficient for narrowing y's type: - if x is None: - reveal_type(y) # revealed: str | None - - if y is not x: - reveal_type(y) # revealed: str | None -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md deleted file mode 100644 index 76eae880ef39e..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md +++ /dev/null @@ -1,49 +0,0 @@ -# Narrowing for conditionals with elif and else - -## Positive contributions become negative in elif-else blocks - -```py -def _(x: int): - if x == 1: - # cannot narrow; could be a subclass of `int` - reveal_type(x) # revealed: int - elif x == 2: - reveal_type(x) # revealed: int & ~Literal[1] - elif x != 3: - reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] -``` - -## Positive contributions become negative in elif-else blocks, with simplification - -```py -def _(flag1: bool, flag2: bool): - x = 1 if flag1 else 2 if flag2 else 3 - - if x == 1: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 2, 3] - elif x == 2: - # TODO should be Literal[2] - reveal_type(x) # revealed: Literal[2, 3] - else: - reveal_type(x) # revealed: Literal[3] -``` - -## Multiple negative contributions using elif, with simplification - -```py -def _(flag1: bool, flag2: bool): - x = 1 if flag1 else 2 if flag2 else 3 - - if x != 1: - reveal_type(x) # revealed: Literal[2, 3] - elif x != 2: - # TODO should be `Literal[1]` - reveal_type(x) # revealed: Literal[1, 3] - elif x == 3: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2, 3] - else: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md deleted file mode 100644 index dad037470270e..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ /dev/null @@ -1,80 +0,0 @@ -# Narrowing for `in` conditionals - -## `in` for tuples - -```py -def _(x: int): - if x in (1, 2, 3): - reveal_type(x) # revealed: int - else: - reveal_type(x) # revealed: int -``` - -```py -def _(x: str): - if x in ("a", "b", "c"): - reveal_type(x) # revealed: str - else: - reveal_type(x) # revealed: str -``` - -```py -from typing import Literal - -def _(x: Literal[1, 2, "a", "b", False, b"abc"]): - if x in (1,): - reveal_type(x) # revealed: Literal[1] - elif x in (2, "a"): - reveal_type(x) # revealed: Literal[2, "a"] - elif x in (b"abc",): - reveal_type(x) # revealed: Literal[b"abc"] - elif x not in (3,): - reveal_type(x) # revealed: Literal["b", False] - else: - reveal_type(x) # revealed: Never -``` - -```py -def _(x: Literal["a", "b", "c", 1]): - if x in ("a", "b", "c", 2): - reveal_type(x) # revealed: Literal["a", "b", "c"] - else: - reveal_type(x) # revealed: Literal[1] -``` - -## `in` for `str` and literal strings - -```py -def _(x: str): - if x in "abc": - reveal_type(x) # revealed: str - else: - reveal_type(x) # revealed: str -``` - -```py -from typing import Literal - -def _(x: Literal["a", "b", "c", "d"]): - if x in "abc": - reveal_type(x) # revealed: Literal["a", "b", "c"] - else: - reveal_type(x) # revealed: Literal["d"] -``` - -```py -def _(x: Literal["a", "b", "c", "e"]): - if x in "abcd": - reveal_type(x) # revealed: Literal["a", "b", "c"] - else: - reveal_type(x) # revealed: Literal["e"] -``` - -```py -def _(x: Literal[1, "a", "b", "c", "d"]): - # error: [unsupported-operator] - if x in "abc": - reveal_type(x) # revealed: Literal["a", "b", "c"] - else: - reveal_type(x) # revealed: Literal[1, "d"] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md deleted file mode 100644 index fa69fe8863bc0..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ /dev/null @@ -1,47 +0,0 @@ -# Narrowing for nested conditionals - -## Multiple negative contributions - -```py -def _(x: int): - if x != 1: - if x != 2: - if x != 3: - reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] -``` - -## Multiple negative contributions with simplification - -```py -def _(flag1: bool, flag2: bool): - x = 1 if flag1 else 2 if flag2 else 3 - - if x != 1: - reveal_type(x) # revealed: Literal[2, 3] - if x != 2: - reveal_type(x) # revealed: Literal[3] -``` - -## elif-else blocks - -```py -def _(flag1: bool, flag2: bool): - x = 1 if flag1 else 2 if flag2 else 3 - - if x != 1: - reveal_type(x) # revealed: Literal[2, 3] - if x == 2: - # TODO should be `Literal[2]` - reveal_type(x) # revealed: Literal[2, 3] - elif x == 3: - reveal_type(x) # revealed: Literal[3] - else: - reveal_type(x) # revealed: Never - - elif x != 2: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 3] - else: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2, 3] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md deleted file mode 100644 index abe0c4d5aaea1..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md +++ /dev/null @@ -1,91 +0,0 @@ -# Narrowing for `!=` conditionals - -## `x != None` - -```py -def _(flag: bool): - x = None if flag else 1 - - if x != None: - reveal_type(x) # revealed: Literal[1] - else: - # TODO should be None - reveal_type(x) # revealed: None | Literal[1] -``` - -## `!=` for other singleton types - -```py -def _(flag: bool): - x = True if flag else False - - if x != False: - reveal_type(x) # revealed: Literal[True] - else: - # TODO should be Literal[False] - reveal_type(x) # revealed: bool -``` - -## `x != y` where `y` is of literal type - -```py -def _(flag: bool): - x = 1 if flag else 2 - - if x != 1: - reveal_type(x) # revealed: Literal[2] -``` - -## `x != y` where `y` is a single-valued type - -```py -def _(flag: bool): - class A: ... - class B: ... - C = A if flag else B - - if C != A: - reveal_type(C) # revealed: Literal[B] - else: - # TODO should be Literal[A] - reveal_type(C) # revealed: Literal[A, B] -``` - -## `x != y` where `y` has multiple single-valued options - -```py -def _(flag1: bool, flag2: bool): - x = 1 if flag1 else 2 - y = 2 if flag2 else 3 - - if x != y: - reveal_type(x) # revealed: Literal[1, 2] - else: - # TODO should be Literal[2] - reveal_type(x) # revealed: Literal[1, 2] -``` - -## `!=` for non-single-valued types - -Only single-valued types should narrow the type: - -```py -def _(flag: bool, a: int, y: int): - x = a if flag else None - - if x != y: - reveal_type(x) # revealed: int | None -``` - -## Mix of single-valued and non-single-valued types - -```py -def _(flag1: bool, flag2: bool, a: int): - x = 1 if flag1 else 2 - y = 2 if flag2 else a - - if x != y: - reveal_type(x) # revealed: Literal[1, 2] - else: - reveal_type(x) # revealed: Literal[1, 2] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md deleted file mode 100644 index 1c4e05e678837..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md +++ /dev/null @@ -1,213 +0,0 @@ -# Narrowing for `isinstance` checks - -Narrowing for `isinstance(object, classinfo)` expressions. - -## `classinfo` is a single type - -```py -def _(flag: bool): - x = 1 if flag else "a" - - if isinstance(x, int): - reveal_type(x) # revealed: Literal[1] - - if isinstance(x, str): - reveal_type(x) # revealed: Literal["a"] - if isinstance(x, int): - reveal_type(x) # revealed: Never - - if isinstance(x, (int, object)): - reveal_type(x) # revealed: Literal[1, "a"] -``` - -## `classinfo` is a tuple of types - -Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tuple[(int, str)])`. -The former is equivalent to `isinstance(x, int | str)`: - -```py -def _(flag: bool, flag1: bool, flag2: bool): - x = 1 if flag else "a" - - if isinstance(x, (int, str)): - reveal_type(x) # revealed: Literal[1, "a"] - else: - reveal_type(x) # revealed: Never - - if isinstance(x, (int, bytes)): - reveal_type(x) # revealed: Literal[1] - - if isinstance(x, (bytes, str)): - reveal_type(x) # revealed: Literal["a"] - - # No narrowing should occur if a larger type is also - # one of the possibilities: - if isinstance(x, (int, object)): - reveal_type(x) # revealed: Literal[1, "a"] - else: - reveal_type(x) # revealed: Never - - y = 1 if flag1 else "a" if flag2 else b"b" - if isinstance(y, (int, str)): - reveal_type(y) # revealed: Literal[1, "a"] - - if isinstance(y, (int, bytes)): - reveal_type(y) # revealed: Literal[1, b"b"] - - if isinstance(y, (str, bytes)): - reveal_type(y) # revealed: Literal["a", b"b"] -``` - -## `classinfo` is a nested tuple of types - -```py -def _(flag: bool): - x = 1 if flag else "a" - - if isinstance(x, (bool, (bytes, int))): - reveal_type(x) # revealed: Literal[1] - else: - reveal_type(x) # revealed: Literal["a"] -``` - -## Class types - -```py -class A: ... -class B: ... -class C: ... - -x = object() - -if isinstance(x, A): - reveal_type(x) # revealed: A - if isinstance(x, B): - reveal_type(x) # revealed: A & B - else: - reveal_type(x) # revealed: A & ~B - -if isinstance(x, (A, B)): - reveal_type(x) # revealed: A | B -elif isinstance(x, (A, C)): - reveal_type(x) # revealed: C & ~A & ~B -else: - reveal_type(x) # revealed: ~A & ~B & ~C -``` - -## No narrowing for instances of `builtins.type` - -```py -def _(flag: bool, t: type): - x = 1 if flag else "foo" - - if isinstance(x, t): - reveal_type(x) # revealed: Literal[1, "foo"] -``` - -## Do not use custom `isinstance` for narrowing - -```py -def _(flag: bool): - def isinstance(x, t): - return True - x = 1 if flag else "a" - - if isinstance(x, int): - reveal_type(x) # revealed: Literal[1, "a"] -``` - -## Do support narrowing if `isinstance` is aliased - -```py -def _(flag: bool): - isinstance_alias = isinstance - - x = 1 if flag else "a" - - if isinstance_alias(x, int): - reveal_type(x) # revealed: Literal[1] -``` - -## Do support narrowing if `isinstance` is imported - -```py -from builtins import isinstance as imported_isinstance - -def _(flag: bool): - x = 1 if flag else "a" - - if imported_isinstance(x, int): - reveal_type(x) # revealed: Literal[1] -``` - -## Do not narrow if second argument is not a type - -```py -def _(flag: bool): - x = 1 if flag else "a" - - # TODO: this should cause us to emit a diagnostic during - # type checking - if isinstance(x, "a"): - reveal_type(x) # revealed: Literal[1, "a"] - - # TODO: this should cause us to emit a diagnostic during - # type checking - if isinstance(x, "int"): - reveal_type(x) # revealed: Literal[1, "a"] -``` - -## Do not narrow if there are keyword arguments - -```py -def _(flag: bool): - x = 1 if flag else "a" - - # error: [unknown-argument] - if isinstance(x, int, foo="bar"): - reveal_type(x) # revealed: Literal[1, "a"] -``` - -## `type[]` types are narrowed as well as class-literal types - -```py -def _(x: object, y: type[int]): - if isinstance(x, y): - reveal_type(x) # revealed: int -``` - -## Adding a disjoint element to an existing intersection - -We used to incorrectly infer `Literal` booleans for some of these. - -```py -from knot_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy - -class P: ... - -def f( - a: Intersection[P, AlwaysTruthy], - b: Intersection[P, AlwaysFalsy], - c: Intersection[P, Not[AlwaysTruthy]], - d: Intersection[P, Not[AlwaysFalsy]], -): - if isinstance(a, bool): - reveal_type(a) # revealed: Never - else: - reveal_type(a) # revealed: P & AlwaysTruthy - - if isinstance(b, bool): - reveal_type(b) # revealed: Never - else: - reveal_type(b) # revealed: P & AlwaysFalsy - - if isinstance(c, bool): - reveal_type(c) # revealed: Never - else: - reveal_type(c) # revealed: P & ~AlwaysTruthy - - if isinstance(d, bool): - reveal_type(d) # revealed: Never - else: - reveal_type(d) # revealed: P & ~AlwaysFalsy -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md deleted file mode 100644 index 3ad543361de3e..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md +++ /dev/null @@ -1,280 +0,0 @@ -# Narrowing for `issubclass` checks - -Narrowing for `issubclass(class, classinfo)` expressions. - -## `classinfo` is a single type - -### Basic example - -```py -def _(flag: bool): - t = int if flag else str - - if issubclass(t, bytes): - reveal_type(t) # revealed: Never - - if issubclass(t, object): - reveal_type(t) # revealed: Literal[int, str] - - if issubclass(t, int): - reveal_type(t) # revealed: Literal[int] - else: - reveal_type(t) # revealed: Literal[str] - - if issubclass(t, str): - reveal_type(t) # revealed: Literal[str] - if issubclass(t, int): - reveal_type(t) # revealed: Never -``` - -### Proper narrowing in `elif` and `else` branches - -```py -def _(flag1: bool, flag2: bool): - t = int if flag1 else str if flag2 else bytes - - if issubclass(t, int): - reveal_type(t) # revealed: Literal[int] - else: - reveal_type(t) # revealed: Literal[str, bytes] - - if issubclass(t, int): - reveal_type(t) # revealed: Literal[int] - elif issubclass(t, str): - reveal_type(t) # revealed: Literal[str] - else: - reveal_type(t) # revealed: Literal[bytes] -``` - -### Multiple derived classes - -```py -class Base: ... -class Derived1(Base): ... -class Derived2(Base): ... -class Unrelated: ... - -def _(flag1: bool, flag2: bool, flag3: bool): - t1 = Derived1 if flag1 else Derived2 - - if issubclass(t1, Base): - reveal_type(t1) # revealed: Literal[Derived1, Derived2] - - if issubclass(t1, Derived1): - reveal_type(t1) # revealed: Literal[Derived1] - else: - reveal_type(t1) # revealed: Literal[Derived2] - - t2 = Derived1 if flag2 else Base - - if issubclass(t2, Base): - reveal_type(t2) # revealed: Literal[Derived1, Base] - - t3 = Derived1 if flag3 else Unrelated - - if issubclass(t3, Base): - reveal_type(t3) # revealed: Literal[Derived1] - else: - reveal_type(t3) # revealed: Literal[Unrelated] -``` - -### Narrowing for non-literals - -```py -class A: ... -class B: ... - -def _(t: type[object]): - if issubclass(t, A): - reveal_type(t) # revealed: type[A] - if issubclass(t, B): - reveal_type(t) # revealed: type[A] & type[B] - else: - reveal_type(t) # revealed: type & ~type[A] -``` - -### Handling of `None` - -`types.NoneType` is only available in Python 3.10 and later: - -```toml -[environment] -python-version = "3.10" -``` - -```py -from types import NoneType - -def _(flag: bool): - t = int if flag else NoneType - - if issubclass(t, NoneType): - reveal_type(t) # revealed: Literal[NoneType] - - if issubclass(t, type(None)): - reveal_type(t) # revealed: Literal[NoneType] -``` - -## `classinfo` contains multiple types - -### (Nested) tuples of types - -```py -class Unrelated: ... - -def _(flag1: bool, flag2: bool): - t = int if flag1 else str if flag2 else bytes - - if issubclass(t, (int, (Unrelated, (bytes,)))): - reveal_type(t) # revealed: Literal[int, bytes] - else: - reveal_type(t) # revealed: Literal[str] -``` - -## Special cases - -### Emit a diagnostic if the first argument is of wrong type - -#### Too wide - -`type[object]` is a subtype of `object`, but not every `object` can be passed as the first argument -to `issubclass`: - -```py -class A: ... - -t = object() - -# error: [invalid-argument-type] -if issubclass(t, A): - reveal_type(t) # revealed: type[A] -``` - -#### Wrong - -`Literal[1]` and `type` are entirely disjoint, so the inferred type of `Literal[1] & type[int]` is -eagerly simplified to `Never` as a result of the type narrowing in the `if issubclass(t, int)` -branch: - -```py -t = 1 - -# error: [invalid-argument-type] -if issubclass(t, int): - reveal_type(t) # revealed: Never -``` - -### Do not use custom `issubclass` for narrowing - -```py -def issubclass(c, ci): - return True - -def flag() -> bool: - return True - -t = int if flag() else str -if issubclass(t, int): - reveal_type(t) # revealed: Literal[int, str] -``` - -### Do support narrowing if `issubclass` is aliased - -```py -issubclass_alias = issubclass - -def flag() -> bool: - return True - -t = int if flag() else str -if issubclass_alias(t, int): - reveal_type(t) # revealed: Literal[int] -``` - -### Do support narrowing if `issubclass` is imported - -```py -from builtins import issubclass as imported_issubclass - -def flag() -> bool: - return True - -t = int if flag() else str -if imported_issubclass(t, int): - reveal_type(t) # revealed: Literal[int] -``` - -### Do not narrow if second argument is not a proper `classinfo` argument - -```py -from typing import Any - -def flag() -> bool: - return True - -t = int if flag() else str - -# TODO: this should cause us to emit a diagnostic during -# type checking -if issubclass(t, "str"): - reveal_type(t) # revealed: Literal[int, str] - -# TODO: this should cause us to emit a diagnostic during -# type checking -if issubclass(t, (bytes, "str")): - reveal_type(t) # revealed: Literal[int, str] - -# TODO: this should cause us to emit a diagnostic during -# type checking -if issubclass(t, Any): - reveal_type(t) # revealed: Literal[int, str] -``` - -### Do not narrow if there are keyword arguments - -```py -def flag() -> bool: - return True - -t = int if flag() else str - -# error: [unknown-argument] -if issubclass(t, int, foo="bar"): - reveal_type(t) # revealed: Literal[int, str] -``` - -### `type[]` types are narrowed as well as class-literal types - -```py -def _(x: type, y: type[int]): - if issubclass(x, y): - reveal_type(x) # revealed: type[int] -``` - -### Disjoint `type[]` types are narrowed to `Never` - -Here, `type[UsesMeta1]` and `type[UsesMeta2]` are disjoint because a common subclass of `UsesMeta1` -and `UsesMeta2` could only exist if a common subclass of their metaclasses could exist. This is -known to be impossible due to the fact that `Meta1` is marked as `@final`. - -```py -from typing import final - -@final -class Meta1(type): ... - -class Meta2(type): ... -class UsesMeta1(metaclass=Meta1): ... -class UsesMeta2(metaclass=Meta2): ... - -def _(x: type[UsesMeta1], y: type[UsesMeta2]): - if issubclass(x, y): - reveal_type(x) # revealed: Never - else: - reveal_type(x) # revealed: type[UsesMeta1] - - if issubclass(y, x): - reveal_type(y) # revealed: Never - else: - reveal_type(y) # revealed: type[UsesMeta2] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md deleted file mode 100644 index d6f0246f293f8..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ /dev/null @@ -1,162 +0,0 @@ -# Narrowing for `match` statements - -## Single `match` pattern - -```py -def _(flag: bool): - x = None if flag else 1 - - reveal_type(x) # revealed: None | Literal[1] - - y = 0 - - match x: - case None: - y = x - - reveal_type(y) # revealed: Literal[0] | None -``` - -## Class patterns - -```py -def get_object() -> object: - return object() - -class A: ... -class B: ... - -x = get_object() - -reveal_type(x) # revealed: object - -match x: - case A(): - reveal_type(x) # revealed: A - case B(): - # TODO could be `B & ~A` - reveal_type(x) # revealed: B - -reveal_type(x) # revealed: object -``` - -## Class pattern with guard - -```py -def get_object() -> object: - return object() - -class A: - def y() -> int: - return 1 - -class B: ... - -x = get_object() - -reveal_type(x) # revealed: object - -match x: - case A() if reveal_type(x): # revealed: A - pass - case B() if reveal_type(x): # revealed: B - pass - -reveal_type(x) # revealed: object -``` - -## Value patterns - -```py -def get_object() -> object: - return object() - -x = get_object() - -reveal_type(x) # revealed: object - -match x: - case "foo": - reveal_type(x) # revealed: Literal["foo"] - case 42: - reveal_type(x) # revealed: Literal[42] - case 6.0: - reveal_type(x) # revealed: float - case 1j: - reveal_type(x) # revealed: complex - case b"foo": - reveal_type(x) # revealed: Literal[b"foo"] - -reveal_type(x) # revealed: object -``` - -## Value patterns with guard - -```py -def get_object() -> object: - return object() - -x = get_object() - -reveal_type(x) # revealed: object - -match x: - case "foo" if reveal_type(x): # revealed: Literal["foo"] - pass - case 42 if reveal_type(x): # revealed: Literal[42] - pass - case 6.0 if reveal_type(x): # revealed: float - pass - case 1j if reveal_type(x): # revealed: complex - pass - case b"foo" if reveal_type(x): # revealed: Literal[b"foo"] - pass - -reveal_type(x) # revealed: object -``` - -## Or patterns - -```py -def get_object() -> object: - return object() - -x = get_object() - -reveal_type(x) # revealed: object - -match x: - case "foo" | 42 | None: - reveal_type(x) # revealed: Literal["foo", 42] | None - case "foo" | tuple(): - reveal_type(x) # revealed: Literal["foo"] | tuple - case True | False: - reveal_type(x) # revealed: bool - case 3.14 | 2.718 | 1.414: - reveal_type(x) # revealed: float - -reveal_type(x) # revealed: object -``` - -## Or patterns with guard - -```py -def get_object() -> object: - return object() - -x = get_object() - -reveal_type(x) # revealed: object - -match x: - case "foo" | 42 | None if reveal_type(x): # revealed: Literal["foo", 42] | None - pass - case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple - pass - case True | False if reveal_type(x): # revealed: bool - pass - case 3.14 | 2.718 | 1.414 if reveal_type(x): # revealed: float - pass - -reveal_type(x) # revealed: object -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md deleted file mode 100644 index 84a14f93f5af8..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ /dev/null @@ -1,349 +0,0 @@ -# Narrowing For Truthiness Checks (`if x` or `if not x`) - -## Value Literals - -```py -from typing import Literal - -def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]: - return 0 - -x = foo() - -if x: - reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"] -else: - reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()] - -if not x: - reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()] -else: - reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"] - -if x and not x: - reveal_type(x) # revealed: Never -else: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] - -if not (x and not x): - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] -else: - reveal_type(x) # revealed: Never - -if x or not x: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] -else: - reveal_type(x) # revealed: Never - -if not (x or not x): - reveal_type(x) # revealed: Never -else: - reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] - -if (isinstance(x, int) or isinstance(x, str)) and x: - reveal_type(x) # revealed: Literal[-1, True, "foo"] -else: - reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()] -``` - -## Function Literals - -Basically functions are always truthy. - -```py -def flag() -> bool: - return True - -def foo(hello: int) -> bytes: - return b"" - -def bar(world: str, *args, **kwargs) -> float: - return 0.0 - -x = foo if flag() else bar - -if x: - reveal_type(x) # revealed: (def foo(hello: int) -> bytes) | (def bar(world: str, *args, **kwargs) -> int | float) -else: - reveal_type(x) # revealed: Never -``` - -## Mutable Truthiness - -### Truthiness of Instances - -The boolean value of an instance is not always consistent. For example, `__bool__` can be customized -to return random values, or in the case of a `list()`, the result depends on the number of elements -in the list. Therefore, these types should not be narrowed by `if x` or `if not x`. - -```py -class A: ... -class B: ... - -def f(x: A | B): - if x: - reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy - else: - reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy - - if x and not x: - reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy - else: - reveal_type(x) # revealed: A | B - - if x or not x: - reveal_type(x) # revealed: A | B - else: - reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy -``` - -### Truthiness of Types - -Also, types may not be Truthy. This is because `__bool__` can be customized via a metaclass. -Although this is a very rare case, we may consider metaclass checks in the future to handle this -more accurately. - -```py -def flag() -> bool: - return True - -x = int if flag() else str -reveal_type(x) # revealed: Literal[int, str] - -if x: - reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy -else: - reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy -``` - -## Determined Truthiness - -Some custom classes can have a boolean value that is consistently determined as either `True` or -`False`, regardless of the instance's state. This is achieved by defining a `__bool__` method that -always returns a fixed value. - -These types can always be fully narrowed in boolean contexts, as shown below: - -```py -from typing import Literal - -class T: - def __bool__(self) -> Literal[True]: - return True - -class F: - def __bool__(self) -> Literal[False]: - return False - -t = T() - -if t: - reveal_type(t) # revealed: T -else: - reveal_type(t) # revealed: Never - -f = F() - -if f: - reveal_type(f) # revealed: Never -else: - reveal_type(f) # revealed: F -``` - -## Narrowing Complex Intersection and Union - -```py -from typing import Literal - -class A: ... -class B: ... - -def flag() -> bool: - return True - -def instance() -> A | B: - return A() - -def literals() -> Literal[0, 42, "", "hello"]: - return 42 - -x = instance() -y = literals() - -if isinstance(x, str) and not isinstance(x, B): - reveal_type(x) # revealed: A & str & ~B - reveal_type(y) # revealed: Literal[0, 42, "", "hello"] - - z = x if flag() else y - - reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"] - - if z: - reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"] - else: - reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""] -``` - -## Narrowing Multiple Variables - -```py -from typing import Literal - -def f(x: Literal[0, 1], y: Literal["", "hello"]): - if x and y and not x and not y: - reveal_type(x) # revealed: Never - reveal_type(y) # revealed: Never - else: - # ~(x or not x) and ~(y or not y) - reveal_type(x) # revealed: Literal[0, 1] - reveal_type(y) # revealed: Literal["", "hello"] - - if (x or not x) and (y and not y): - reveal_type(x) # revealed: Literal[0, 1] - reveal_type(y) # revealed: Never - else: - # ~(x or not x) or ~(y and not y) - reveal_type(x) # revealed: Literal[0, 1] - reveal_type(y) # revealed: Literal["", "hello"] -``` - -## Control Flow Merging - -After merging control flows, when we take the union of all constraints applied in each branch, we -should return to the original state. - -```py -class A: ... - -x = A() - -if x and not x: - y = x - reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy -else: - y = x - reveal_type(y) # revealed: A - -reveal_type(y) # revealed: A -``` - -## Truthiness of classes - -```py -from typing import Literal - -class MetaAmbiguous(type): - def __bool__(self) -> bool: - return True - -class MetaFalsy(type): - def __bool__(self) -> Literal[False]: - return False - -class MetaTruthy(type): - def __bool__(self) -> Literal[True]: - return True - -class MetaDeferred(type): - def __bool__(self) -> MetaAmbiguous: - return MetaAmbiguous() - -class AmbiguousClass(metaclass=MetaAmbiguous): ... -class FalsyClass(metaclass=MetaFalsy): ... -class TruthyClass(metaclass=MetaTruthy): ... -class DeferredClass(metaclass=MetaDeferred): ... - -def _( - a: type[AmbiguousClass], - t: type[TruthyClass], - f: type[FalsyClass], - d: type[DeferredClass], - ta: type[TruthyClass | AmbiguousClass], - af: type[AmbiguousClass] | type[FalsyClass], - flag: bool, -): - reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] - if ta: - reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy - - reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass] - if af: - reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy - - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool" - if d: - # TODO: Should be `Unknown` - reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy - - tf = TruthyClass if flag else FalsyClass - reveal_type(tf) # revealed: Literal[TruthyClass, FalsyClass] - - if tf: - reveal_type(tf) # revealed: Literal[TruthyClass] - else: - reveal_type(tf) # revealed: Literal[FalsyClass] -``` - -## Narrowing in chained boolean expressions - -```py -from typing import Literal - -class A: ... - -def _(x: Literal[0, 1]): - reveal_type(x or A()) # revealed: Literal[1] | A - reveal_type(x and A()) # revealed: Literal[0] | A - -def _(x: str): - reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A - reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A - -def _(x: bool | str): - reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A - reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A - -class Falsy: - def __bool__(self) -> Literal[False]: - return False - -class Truthy: - def __bool__(self) -> Literal[True]: - return True - -def _(x: Falsy | Truthy): - reveal_type(x or A()) # revealed: Truthy | A - reveal_type(x and A()) # revealed: Falsy | A - -class MetaFalsy(type): - def __bool__(self) -> Literal[False]: - return False - -class MetaTruthy(type): - def __bool__(self) -> Literal[True]: - return True - -class FalsyClass(metaclass=MetaFalsy): ... -class TruthyClass(metaclass=MetaTruthy): ... - -def _(x: type[FalsyClass] | type[TruthyClass]): - reveal_type(x or A()) # revealed: type[TruthyClass] | A - reveal_type(x and A()) # revealed: type[FalsyClass] | A -``` - -## Truthiness narrowing for `LiteralString` - -```py -from typing_extensions import LiteralString - -def _(x: LiteralString): - if x: - reveal_type(x) # revealed: LiteralString & ~Literal[""] - else: - reveal_type(x) # revealed: Literal[""] - - if not x: - reveal_type(x) # revealed: Literal[""] - else: - reveal_type(x) # revealed: LiteralString & ~Literal[""] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md deleted file mode 100644 index 43a0cf6c6a7e3..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md +++ /dev/null @@ -1,92 +0,0 @@ -# PEP 695 type aliases - -PEP 695 type aliases are only available in Python 3.12 and later: - -```toml -[environment] -python-version = "3.12" -``` - -## Basic - -```py -type IntOrStr = int | str - -reveal_type(IntOrStr) # revealed: typing.TypeAliasType -reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"] - -x: IntOrStr = 1 - -reveal_type(x) # revealed: Literal[1] - -def f() -> None: - reveal_type(x) # revealed: int | str -``` - -## `__value__` attribute - -```py -type IntOrStr = int | str - -reveal_type(IntOrStr.__value__) # revealed: Any -``` - -## Invalid assignment - -```py -type OptionalInt = int | None - -# error: [invalid-assignment] -x: OptionalInt = "1" -``` - -## Type aliases in type aliases - -```py -type IntOrStr = int | str -type IntOrStrOrBytes = IntOrStr | bytes - -x: IntOrStrOrBytes = 1 - -def f() -> None: - reveal_type(x) # revealed: int | str | bytes -``` - -## Aliased type aliases - -```py -type IntOrStr = int | str -MyIntOrStr = IntOrStr - -x: MyIntOrStr = 1 - -# error: [invalid-assignment] -y: MyIntOrStr = None -``` - -## Generic type aliases - -```py -type ListOrSet[T] = list[T] | set[T] - -# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`, -# as specified in the `typeshed` stubs. -reveal_type(ListOrSet.__type_params__) # revealed: @Todo(full tuple[...] support) -``` - -## `TypeAliasType` properties - -Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type - -```py -from knot_extensions import static_assert, is_equivalent_to, is_disjoint_from, TypeOf - -type Alias1 = int -type Alias2 = int - -type TypeAliasType1 = TypeOf[Alias1] -type TypeAliasType2 = TypeOf[Alias2] - -static_assert(not is_equivalent_to(TypeAliasType1, TypeAliasType2)) -static_assert(is_disjoint_from(TypeAliasType1, TypeAliasType2)) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md deleted file mode 100644 index 0b2c0eacc3d2d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ /dev/null @@ -1,1158 +0,0 @@ -# Protocols - -> [!NOTE] -> -> See also: -> -> - The [typing specification section on protocols][typing_spec_protocols] -> - The many [protocol conformance tests] provided by the Typing Council for type checkers -> - Mypy's [documentation][mypy_protocol_docs] and [tests][mypy_protocol_tests] for protocols - -Most types in Python are *nominal* types: a fully static nominal type `X` is only a subtype of -another fully static nominal type `Y` if the class `X` is a subclass of the class `Y`. -`typing.Protocol` (or its backport, `typing_extensions.Protocol`) can be used to define *structural* -types, on the other hand: a type which is defined by its properties and behaviour. - -## Defining a protocol - -A protocol is defined by inheriting from the `Protocol` class, which is annotated as an instance of -`_SpecialForm` in typeshed's stubs. - -```py -from typing import Protocol - -class MyProtocol(Protocol): ... - -# TODO: at runtime this is `(, , , )` -reveal_type(MyProtocol.__mro__) # revealed: tuple[Literal[MyProtocol], @Todo(protocol), Literal[object]] -``` - -Just like for any other class base, it is an error for `Protocol` to appear multiple times in a -class's bases: - -```py -class Foo(Protocol, Protocol): ... # error: [inconsistent-mro] - -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -``` - -The introspection helper `typing(_extensions).is_protocol` can be used to verify whether a class is -a protocol class or not: - -```py -from typing_extensions import is_protocol - -# TODO: should be `Literal[True]` -reveal_type(is_protocol(MyProtocol)) # revealed: bool - -class NotAProtocol: ... - -# TODO: should be `Literal[False]` -reveal_type(is_protocol(NotAProtocol)) # revealed: bool -``` - -A type checker should follow the typeshed stubs if a non-class is passed in, and typeshed's stubs -indicate that the argument passed in must be an instance of `type`. `Literal[False]` should be -inferred as the return type, however. - -```py -# TODO: the diagnostic is correct, but should infer `Literal[False]` -# error: [invalid-argument-type] -reveal_type(is_protocol("not a class")) # revealed: bool -``` - -For a class to be considered a protocol class, it must have `Protocol` directly in its bases tuple: -it is not sufficient for it to have `Protocol` in its MRO. - -```py -class SubclassOfMyProtocol(MyProtocol): ... - -# TODO -# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], @Todo(protocol), Literal[object]] -reveal_type(SubclassOfMyProtocol.__mro__) - -# TODO: should be `Literal[False]` -reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: bool -``` - -A protocol class may inherit from other protocols, however, as long as it re-inherits from -`Protocol`: - -```py -class SubProtocol(MyProtocol, Protocol): ... - -# TODO: should be `Literal[True]` -reveal_type(is_protocol(SubProtocol)) # revealed: bool - -class OtherProtocol(Protocol): - some_attribute: str - -class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... - -# TODO -# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], @Todo(protocol), Literal[object]] -reveal_type(ComplexInheritance.__mro__) - -# TODO: should be `Literal[True]` -reveal_type(is_protocol(ComplexInheritance)) # revealed: bool -``` - -If `Protocol` is present in the bases tuple, all other bases in the tuple must be protocol classes, -or `TypeError` is raised at runtime when the class is created. - -```py -# TODO: should emit `[invalid-protocol]` -class Invalid(NotAProtocol, Protocol): ... - -# TODO -# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], @Todo(protocol), Literal[object]] -reveal_type(Invalid.__mro__) - -# TODO: should emit an `[invalid-protocol`] error -class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... - -# TODO -# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], @Todo(protocol), Literal[object]] -reveal_type(AlsoInvalid.__mro__) -``` - -But two exceptions to this rule are `object` and `Generic`: - -```py -from typing import TypeVar, Generic - -T = TypeVar("T") - -# Note: pyright and pyrefly do not consider this to be a valid `Protocol` class, -# but mypy does (and has an explicit test for this behaviour). Mypy was the -# reference implementation for PEP-544, and its behaviour also matches the CPython -# runtime, so we choose to follow its behaviour here rather than that of the other -# type checkers. -class Fine(Protocol, object): ... - -# TODO -reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], @Todo(protocol), Literal[object]] - -# TODO: should not error -class StillFine(Protocol, Generic[T], object): ... # error: [invalid-base] -class EvenThis[T](Protocol, object): ... -``` - -And multiple inheritance from a mix of protocol and non-protocol classes is fine as long as -`Protocol` itself is not in the bases list: - -```py -class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ... - -# TODO -# revealed: tuple[Literal[FineAndDandy], Literal[MyProtocol], Literal[OtherProtocol], @Todo(protocol), Literal[NotAProtocol], Literal[object]] -reveal_type(FineAndDandy.__mro__) -``` - -But if `Protocol` is not present in the bases list, the resulting class doesn't count as a protocol -class anymore: - -```py -# TODO: should reveal `Literal[False]` -reveal_type(is_protocol(FineAndDandy)) # revealed: bool -``` - -A class does not *have* to inherit from a protocol class in order for it to be considered a subtype -of that protocol (more on that below). However, classes that explicitly inherit from a protocol -class are understood as subtypes of that protocol, the same as with nominal types: - -```py -from knot_extensions import static_assert, is_subtype_of, is_assignable_to - -static_assert(is_subtype_of(SubclassOfMyProtocol, MyProtocol)) -static_assert(is_assignable_to(SubclassOfMyProtocol, MyProtocol)) - -static_assert(is_subtype_of(SubProtocol, MyProtocol)) -static_assert(is_assignable_to(SubProtocol, MyProtocol)) - -static_assert(is_subtype_of(ComplexInheritance, SubProtocol)) -static_assert(is_assignable_to(ComplexInheritance, SubProtocol)) - -static_assert(is_subtype_of(ComplexInheritance, OtherProtocol)) -static_assert(is_assignable_to(ComplexInheritance, SubProtocol)) - -static_assert(is_subtype_of(FineAndDandy, MyProtocol)) -static_assert(is_assignable_to(FineAndDandy, MyProtocol)) - -static_assert(is_subtype_of(FineAndDandy, OtherProtocol)) -static_assert(is_assignable_to(FineAndDandy, OtherProtocol)) -``` - -Note, however, that `Protocol` itself is not a type, so it is an error to pass it to `is_subtype_of` -or `is_assignable_to`: - -```py -is_subtype_of(MyProtocol, Protocol) # error: [invalid-type-form] -is_assignable_to(MyProtocol, Protocol) # error: [invalid-type-form] -``` - -And it is also an error to use `Protocol` in type expressions: - -```py -# fmt: off - -def f( - x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions" - y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too -) -> None: - reveal_type(x) # revealed: Unknown - - # TODO: should be `type[Unknown]` - reveal_type(y) # revealed: @Todo(unsupported type[X] special form) - -# fmt: on -``` - -Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime: - -```py -# TODO: should be `Literal[True]` -reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool -``` - -## `typing.Protocol` versus `typing_extensions.Protocol` - -`typing.Protocol` and its backport in `typing_extensions` should be treated as exactly equivalent. - -```py -import typing -import typing_extensions -from knot_extensions import static_assert, is_equivalent_to - -class Foo(typing.Protocol): - x: int - -# TODO: should not error -class Bar(typing_extensions.Protocol): # error: [invalid-base] - x: int - -# TODO: these should pass -static_assert(typing_extensions.is_protocol(Foo)) # error: [static-assert-error] -static_assert(typing_extensions.is_protocol(Bar)) # error: [static-assert-error] -static_assert(is_equivalent_to(Foo, Bar)) # error: [static-assert-error] -``` - -The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`: - -```py -@typing_extensions.runtime_checkable -class RuntimeCheckableFoo(typing.Protocol): - x: int - -# TODO: should not error -@typing.runtime_checkable -class RuntimeCheckableBar(typing_extensions.Protocol): # error: [invalid-base] - x: int - -# TODO: these should pass -static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) # error: [static-assert-error] -static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) # error: [static-assert-error] -static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # error: [static-assert-error] - -# These should not error because the protocols are decorated with `@runtime_checkable` -isinstance(object(), RuntimeCheckableFoo) -isinstance(object(), RuntimeCheckableBar) -``` - -## Calls to protocol classes - -Neither `Protocol`, nor any protocol class, can be directly instantiated: - -```py -from typing import Protocol - -# error: [call-non-callable] -reveal_type(Protocol()) # revealed: Unknown - -class MyProtocol(Protocol): - x: int - -# error -reveal_type(MyProtocol()) # revealed: MyProtocol -``` - -But a non-protocol class can be instantiated, even if it has `Protocol` in its MRO: - -```py -class SubclassOfMyProtocol(MyProtocol): ... - -reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol -``` - -And as a corollary, `type[MyProtocol]` can also be called: - -```py -def f(x: type[MyProtocol]): - reveal_type(x()) # revealed: MyProtocol -``` - -## Members of a protocol - -A protocol defines an interface through its *members*: if a protocol `Foo` has members `X` and `Y`, -a type `Bar` can only be a subtype of `Foo` if inhabitants of `Bar` also have attributes `X` and -`Y`. - -A protocol class defines its members through declarations in the class body. The members of a -protocol can be introspected using the function `typing.get_protocol_members`, which is backported -via `typing_extensions`. - -```py -from typing_extensions import Protocol, get_protocol_members - -# TODO: should not error -class Foo(Protocol): # error: [invalid-base] - x: int - - @property - def y(self) -> str: - return "y" - - @property - def z(self) -> int: - return 42 - - @z.setter - def z(self, z: int) -> None: ... - def method_member(self) -> bytes: - return b"foo" - -# TODO: at runtime, `get_protocol_members` returns a `frozenset`, -# but for now we might pretend it returns a `tuple`, as we support heterogeneous `tuple` types -# but not yet generic `frozenset`s -# -# So this should either be -# -# `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]` -# -# `frozenset[Literal["x", "y", "z", "method_member"]]` -reveal_type(get_protocol_members(Foo)) # revealed: @Todo(generics) -``` - -Calling `get_protocol_members` on a non-protocol class raises an error at runtime: - -```py -class NotAProtocol: ... - -# TODO: should emit `[invalid-protocol]` error, should reveal `Unknown` -reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(generics) -``` - -Certain special attributes and methods are not considered protocol members at runtime, and should -not be considered protocol members by type checkers either: - -```py -# TODO: should not error -class Lumberjack(Protocol): # error: [invalid-base] - __slots__ = () - __match_args__ = () - x: int - - def __new__(cls, x: int) -> "Lumberjack": - return object.__new__(cls) - - def __init__(self, x: int) -> None: - self.x = x - -# TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]` -reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(generics) -``` - -## Subtyping of protocols with attribute members - -In the following example, the protocol class `HasX` defines an interface such that any other fully -static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a -mutable `x` attribute of type `int`: - -```py -from typing import Protocol -from knot_extensions import static_assert, is_assignable_to, is_subtype_of - -class HasX(Protocol): - x: int - -class Foo: - x: int - -# TODO: these should pass -static_assert(is_subtype_of(Foo, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(Foo, HasX)) # error: [static-assert-error] - -class FooSub(Foo): ... - -# TODO: these should pass -static_assert(is_subtype_of(FooSub, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(FooSub, HasX)) # error: [static-assert-error] - -class Bar: - x: str - -static_assert(not is_subtype_of(Bar, HasX)) -static_assert(not is_assignable_to(Bar, HasX)) - -class Baz: - y: int - -static_assert(not is_subtype_of(Baz, HasX)) -static_assert(not is_assignable_to(Baz, HasX)) -``` - -Note that declaring an attribute member on a protocol mandates that the attribute must be mutable. A -type with a read-only `x` property does not satisfy the `HasX` interface; nor does a type with a -`Final` `x` attribute. The type of the attribute must also be treated as invariant due to the -attribute's mutability: - -```py -from typing import Final - -class A: - @property - def x(self) -> int: - return 42 - -static_assert(not is_subtype_of(A, HasX)) -static_assert(not is_assignable_to(A, HasX)) - -class B: - x: Final = 42 - -static_assert(not is_subtype_of(A, HasX)) -static_assert(not is_assignable_to(A, HasX)) - -class IntSub(int): ... - -class C: - x: IntSub - -# due to invariance, a type is only a subtype of `HasX` -# if its `x` attribute is of type *exactly* `int`: -# a subclass of `int` does not satisfy the interface -static_assert(not is_subtype_of(C, HasX)) -static_assert(not is_assignable_to(C, HasX)) -``` - -All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes -can never be considered to inhabit a protocol that declares a mutable-attribute member: - -```py -from dataclasses import dataclass -from typing import NamedTuple - -@dataclass -class MutableDataclass: - x: int - -# TODO: these should pass -static_assert(is_subtype_of(MutableDataclass, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(MutableDataclass, HasX)) # error: [static-assert-error] - -@dataclass(frozen=True) -class ImmutableDataclass: - x: int - -static_assert(not is_subtype_of(ImmutableDataclass, HasX)) -static_assert(not is_assignable_to(ImmutableDataclass, HasX)) - -class NamedTupleWithX(NamedTuple): - x: int - -static_assert(not is_subtype_of(NamedTupleWithX, HasX)) -static_assert(not is_assignable_to(NamedTupleWithX, HasX)) -``` - -However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX` -protocol only specifies what the type of `x` should be when accessed from instances; instances of -`XProperty` in the below example have a mutable attribute `x` of type `int`: - -```py -class XProperty: - _x: int - - @property - def x(self) -> int: - return self._x - - @x.setter - def x(self, x: int) -> None: - self._x = x**2 - -# TODO: these should pass -static_assert(is_subtype_of(XProperty, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(XProperty, HasX)) # error: [static-assert-error] -``` - -Attribute members on protocol classes are allowed to have default values, just like instance -attributes on other classes. Similar to nominal classes, attributes with defaults can be accessed on -the class object itself and any explicit subclasses of the protocol class. It cannot be assumed to -exist on the meta-type of any arbitrary inhabitant of the protocol type, however; an implicit -subtype of the protocol will not necessarily have a default value for the instance attribute -provided in its class body: - -```py -class HasXWithDefault(Protocol): - x: int = 42 - -reveal_type(HasXWithDefault.x) # revealed: int - -class ExplicitSubclass(HasXWithDefault): ... - -reveal_type(ExplicitSubclass.x) # revealed: int - -def f(arg: HasXWithDefault): - # TODO: should emit `[unresolved-reference]` and reveal `Unknown` - reveal_type(type(arg).x) # revealed: int -``` - -Attribute members are allowed to have assignments in methods on the protocol class, just like -non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that -are not declared in the class body -- are not allowed: - -```py -class Foo(Protocol): - x: int - y: str - - def __init__(self) -> None: - self.x = 42 # fine - self.a = 56 # error - - def non_init_method(self) -> None: - self.y = 64 # fine - self.b = 72 # error -``` - -If a protocol has 0 members, then all other types are assignable to it, and all fully static types -are subtypes of it: - -```py -from typing import Protocol - -class UniversalSet(Protocol): ... - -# TODO: these should pass -static_assert(is_assignable_to(object, UniversalSet)) # error: [static-assert-error] -static_assert(is_subtype_of(object, UniversalSet)) # error: [static-assert-error] -``` - -Which means that `UniversalSet` here is in fact an equivalent type to `object`: - -```py -from knot_extensions import is_equivalent_to - -# TODO: this should pass -static_assert(is_equivalent_to(UniversalSet, object)) # error: [static-assert-error] -``` - -`object` is a subtype of certain other protocols too. Since all fully static types (whether nominal -or structural) are subtypes of `object`, these protocols are also subtypes of `object`; and this -means that these protocols are also equivalent to `UniversalSet` and `object`: - -```py -class SupportsStr(Protocol): - def __str__(self) -> str: ... - -# TODO: these should pass -static_assert(is_equivalent_to(SupportsStr, UniversalSet)) # error: [static-assert-error] -static_assert(is_equivalent_to(SupportsStr, object)) # error: [static-assert-error] - -class SupportsClass(Protocol): - __class__: type - -# TODO: these should pass -static_assert(is_equivalent_to(SupportsClass, UniversalSet)) # error: [static-assert-error] -static_assert(is_equivalent_to(SupportsClass, SupportsStr)) # error: [static-assert-error] -static_assert(is_equivalent_to(SupportsClass, object)) # error: [static-assert-error] -``` - -If a protocol contains members that are not defined on `object`, then that protocol will (like all -types in Python) still be assignable to `object`, but `object` will not be assignable to that -protocol: - -```py -static_assert(is_assignable_to(HasX, object)) -static_assert(is_subtype_of(HasX, object)) -static_assert(not is_assignable_to(object, HasX)) -static_assert(not is_subtype_of(object, HasX)) -``` - -But `object` is the *only* fully static nominal type that a protocol type can ever be assignable to -or a subtype of: - -```py -static_assert(not is_assignable_to(HasX, Foo)) -static_assert(not is_subtype_of(HasX, Foo)) -``` - -## Equivalence of protocols - -Two protocols are considered equivalent types if they specify the same interface, even if they have -different names: - -```py -from typing import Protocol -from knot_extensions import is_equivalent_to, static_assert - -class HasX(Protocol): - x: int - -class AlsoHasX(Protocol): - x: int - -# TODO: this should pass -static_assert(is_equivalent_to(HasX, AlsoHasX)) # error: [static-assert-error] -``` - -And unions containing equivalent protocols are recognised as equivalent, even when the order is not -identical: - -```py -class HasY(Protocol): - y: str - -class AlsoHasY(Protocol): - y: str - -class A: ... -class B: ... - -# TODO: this should pass -static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) # error: [static-assert-error] -``` - -## Intersections of protocols - -An intersection of two protocol types `X` and `Y` is equivalent to a protocol type `Z` that inherits -from both `X` and `Y`: - -```py -from typing import Protocol -from knot_extensions import Intersection, static_assert, is_equivalent_to - -class HasX(Protocol): - x: int - -class HasY(Protocol): - y: str - -class HasXAndYProto(HasX, HasY, Protocol): ... - -# TODO: this should pass -static_assert(is_equivalent_to(HasXAndYProto, Intersection[HasX, HasY])) # error: [static-assert-error] -``` - -But this is only true if the subclass has `Protocol` in its explicit bases (otherwise, it is a -nominal type rather than a structural type): - -```py -class HasXAndYNominal(HasX, HasY): ... - -static_assert(not is_equivalent_to(HasXAndYNominal, Intersection[HasX, HasY])) -``` - -A protocol type `X` and a nominal type `Y` can be inferred as disjoint types if `Y` is a `@final` -type and `Y` does not satisfy the interface declared by `X`. But if `Y` is not `@final`, then this -does not hold true, since a subclass of `Y` could always provide additional methods or attributes -that would lead to it satisfying `X`'s interface: - -```py -from typing import final -from knot_extensions import is_disjoint_from - -class NotFinalNominal: ... - -@final -class FinalNominal: ... - -static_assert(not is_disjoint_from(NotFinalNominal, HasX)) -static_assert(is_disjoint_from(FinalNominal, HasX)) - -def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalNominal]): - reveal_type(arg1) # revealed: HasX & NotFinalNominal - reveal_type(arg2) # revealed: Never -``` - -## Satisfying a protocol's interface - -A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other -protocols can be a subtype of a protocol, as can `ModuleLiteral` types, `ClassLiteral` types, and -others. Another protocol can be a subtype of `HasX` either through "explicit" (nominal) inheritance -from `HasX`, or by specifying a superset of `HasX`'s interface: - -`module.py`: - -```py -x: int = 42 -``` - -`main.py`: - -```py -import module -from typing import Protocol -from knot_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf - -class HasX(Protocol): - x: int - -# TODO: these should pass -static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(TypeOf[module], HasX)) # error: [static-assert-error] - -class ExplicitProtocolSubtype(HasX, Protocol): - y: int - -static_assert(is_subtype_of(ExplicitProtocolSubtype, HasX)) -static_assert(is_assignable_to(ExplicitProtocolSubtype, HasX)) - -class ImplicitProtocolSubtype(Protocol): - x: int - y: str - -# TODO: these should pass -static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error] - -class Meta(type): - x: int - -class UsesMeta(metaclass=Meta): ... - -# TODO: these should pass -static_assert(is_subtype_of(UsesMeta, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(UsesMeta, HasX)) # error: [static-assert-error] -``` - -## `ClassVar` attribute members - -If a protocol `ClassVarX` has a `ClassVar` attribute member `x` with type `int`, this indicates that -a readable `x` attribute must be accessible on any inhabitant of `ClassVarX`, and that a readable -`x` attribute must *also* be accessible on the *type* of that inhabitant: - -`classvars.py`: - -```py -from typing import ClassVar, Protocol -from knot_extensions import is_subtype_of, is_assignable_to, static_assert - -class ClassVarXProto(Protocol): - x: ClassVar[int] - -def f(obj: ClassVarXProto): - reveal_type(obj.x) # revealed: int - reveal_type(type(obj).x) # revealed: int - obj.x = 42 # error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `ClassVarXProto`" - -class InstanceAttrX: - x: int - -static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) -static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) - -class PropertyX: - @property - def x(self) -> int: - return 42 - -static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) -static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) - -class ClassVarX: - x: ClassVar[int] = 42 - -# TODO: these should pass -static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) # error: [static-assert-error] -static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) # error: [static-assert-error] -``` - -This is mentioned by the -[spec](https://typing.python.org/en/latest/spec/protocol.html#protocol-members) and tested in the -[conformance suite](https://github.com/python/typing/blob/main/conformance/tests/protocols_definition.py) -as something that must be supported by type checkers: - -> To distinguish between protocol class variables and protocol instance variables, the special -> `ClassVar` annotation should be used. - -## Subtyping of protocols with property members - -A read-only property on a protocol can be satisfied by a mutable attribute, a read-only property, a -read/write property, a `Final` attribute, or a `ClassVar` attribute: - -```py -from typing import ClassVar, Final, Protocol -from knot_extensions import is_subtype_of, is_assignable_to, static_assert - -class HasXProperty(Protocol): - @property - def x(self) -> int: ... - -class XAttr: - x: int - -# TODO: these should pass -static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error] - -class XReadProperty: - @property - def x(self) -> int: - return 42 - -# TODO: these should pass -static_assert(is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error] - -class XReadWriteProperty: - @property - def x(self) -> int: - return 42 - - @x.setter - def x(self, val: int) -> None: ... - -# TODO: these should pass -static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] - -class XClassVar: - x: ClassVar[int] = 42 - -static_assert(is_subtype_of(XClassVar, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XClassVar, HasXProperty)) # error: [static-assert-error] - -class XFinal: - x: Final = 42 - -# TODO: these should pass -static_assert(is_subtype_of(XFinal, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XFinal, HasXProperty)) # error: [static-assert-error] -``` - -A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below -example satisfies the `HasXProperty` interface even though the type of the `x` attribute on `XSub` -is a subtype of `int` rather than being exactly `int`. - -```py -class MyInt(int): ... - -class XSub: - x: MyInt - -# TODO: these should pass -static_assert(is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error] -``` - -A read/write property on a protocol, where the getter returns the same type that the setter takes, -is equivalent to a normal mutable attribute on a protocol. - -```py -class HasMutableXProperty(Protocol): - @property - def x(self) -> int: ... - @x.setter - def x(self, val: int) -> None: ... - -class XAttr: - x: int - -# TODO: these should pass -static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error] - -class XReadProperty: - @property - def x(self) -> int: - return 42 - -static_assert(not is_subtype_of(XReadProperty, HasXProperty)) -static_assert(not is_assignable_to(XReadProperty, HasXProperty)) - -class XReadWriteProperty: - @property - def x(self) -> int: - return 42 - - @x.setter - def x(self, val: int) -> None: ... - -# TODO: these should pass -static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] - -class XSub: - x: MyInt - -static_assert(not is_subtype_of(XSub, HasXProperty)) -static_assert(not is_assignable_to(XSub, HasXProperty)) -``` - -A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable -attribute `x`. Both are subtypes of a protocol with a read-only prooperty `x`: - -```py -from knot_extensions import is_equivalent_to - -class HasMutableXAttr(Protocol): - x: int - -# TODO: this should pass -static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error] - -# TODO: these should pass -static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) # error: [static-assert-error] - -# TODO: these should pass -static_assert(is_subtype_of(HasMutableXProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) # error: [static-assert-error] -``` - -A read/write property on a protocol, where the setter accepts a subtype of the type returned by the -getter, can be satisfied by a mutable attribute of any type bounded by the upper bound of the -getter-returned type and the lower bound of the setter-accepted type. - -This follows from the principle that a type `X` can only be a subtype of a given protocol if the -`X`'s behaviour is a superset of the behaviour specified by the interface declared by the protocol. -In the below example, the behaviour of an instance of `XAttr` is a superset of the behaviour -specified by the protocol `HasAsymmetricXProperty`. The protocol specifies that reading an `x` -attribute on the instance must resolve to an instance of `int` or a subclass thereof, and `XAttr` -satisfies this requirement. The protocol also specifies that you must be able to assign instances of -`MyInt` to the `x` attribute, and again this is satisfied by `XAttr`: on instances of `XAttr`, you -can assign *any* instance of `int` to the `x` attribute, and thus by extension you can assign any -instance of `IntSub` to the `x` attribute, since any instance of `IntSub` is an instance of `int`: - -```py -class HasAsymmetricXProperty(Protocol): - @property - def x(self) -> int: ... - @x.setter - def x(self, val: MyInt) -> None: ... - -class XAttr: - x: int - -# TODO: these should pass -static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error] -``` - -The end conclusion of this is that the getter-returned type of a property is always covariant and -the setter-accepted type is always contravariant. The combination of these leads to invariance for a -regular mutable attribute, where the implied getter-returned and setter-accepted types are the same. - -```py -class XAttrSub: - x: MyInt - -# TODO: these should pass -static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error] - -class MyIntSub(MyInt): - pass - -class XAttrSubSub: - x: MyIntSub - -static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) -static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) -``` - -An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal -class whose getter and setter types satisfy the covariant and contravariant requirements, -respectively. - -```py -class XAsymmetricProperty: - @property - def x(self) -> MyInt: - return MyInt(0) - - @x.setter - def x(self, x: int) -> None: ... - -# TODO: these should pass -static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error] -``` - -A custom descriptor attribute on the nominal class will also suffice: - -```py -class Descriptor: - def __get__(self, instance, owner) -> MyInt: - return MyInt(0) - - def __set__(self, value: int) -> None: ... - -class XCustomDescriptor: - x: Descriptor = Descriptor() - -# TODO: these should pass -static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error] -``` - -Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a -`__getattr__` method returning a suitable type. A read/write property can be satisfied by a nominal -class that defines a `__getattr__` method returning a suitable type *and* a `__setattr__` method -accepting a suitable type: - -```py -class HasGetAttr: - def __getattr__(self, attr: str) -> int: - return 42 - -# TODO: these should pass -static_assert(is_subtype_of(HasGetAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasGetAttr, HasXProperty)) # error: [static-assert-error] - -static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) -static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) - -class HasGetAttrWithUnsuitableReturn: - def __getattr__(self, attr: str) -> tuple[int, int]: - return (1, 2) - -static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) -static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) - -class HasGetAttrAndSetAttr: - def __getattr__(self, attr: str) -> MyInt: - return MyInt(0) - - def __setattr__(self, attr: str, value: int) -> None: ... - -# TODO: these should pass -static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] -``` - -## Narrowing of protocols - -By default, a protocol class cannot be used as the second argument to `isinstance()` or -`issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the -type inside these branches (this matches the behaviour of other type checkers): - -```py -from typing import Protocol - -class HasX(Protocol): - x: int - -def f(arg: object, arg2: type): - if isinstance(arg, HasX): # error - reveal_type(arg) # revealed: HasX - else: - reveal_type(arg) # revealed: ~HasX - - if issubclass(arg2, HasX): # error - reveal_type(arg2) # revealed: type[HasX] - else: - reveal_type(arg2) # revealed: type & ~type[HasX] -``` - -A protocol class decorated with `@typing(_extensions).runtime_checkable` *can* be used as the second -argument to `isisinstance()` at runtime: - -```py -from typing import runtime_checkable - -@runtime_checkable -class RuntimeCheckableHasX(Protocol): - x: int - -def f(arg: object): - if isinstance(arg, RuntimeCheckableHasX): # no error! - reveal_type(arg) # revealed: RuntimeCheckableHasX - else: - reveal_type(arg) # revealed: ~RuntimeCheckableHasX -``` - -but in order for a protocol class to be used as the second argument to `issubclass()`, it must -satisfy two conditions: - -1. It must be decorated with `@runtime_checkable` -1. It must *only* have method members (protocols with attribute members are not permitted) - -```py -@runtime_checkable -class OnlyMethodMembers(Protocol): - def method(self) -> None: ... - -def f(arg1: type, arg2: type): - if issubclass(arg1, OnlyMethodMembers): # error - reveal_type(arg1) # revealed: type[OnlyMethodMembers] - else: - reveal_type(arg1) # revealed: type & ~type[OnlyMethodMembers] - - if issubclass(arg2, OnlyMethodMembers): # no error! - reveal_type(arg2) # revealed: type[OnlyMethodMembers] - else: - reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] -``` - -## `typing.SupportsIndex` and `typing.Sized` - -`typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot. - -```py -from typing import SupportsIndex, Literal - -def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex): - a: SupportsIndex = some_int - b: SupportsIndex = some_literal_int - c: SupportsIndex = some_indexable -``` - -The same goes for `typing.Sized`: - -```py -from typing import Sized - -def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): - a: Sized = some_list - b: Sized = some_tuple - c: Sized = some_sized -``` - -## TODO - -Add tests for: - -- Assignments without declarations in protocol class bodies. And various weird ways of creating - attributes in a class body or instance method. [Example mypy tests][mypy_weird_protocols]. -- More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec]. -- Protocols with instance-method members -- Protocols with `@classmethod` and `@staticmethod` -- Assignability of non-instance types to protocols with instance-method members (e.g. a - class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method) -- Protocols with methods that have annotated `self` parameters. - [Spec reference][self_types_protocols_spec]. -- Protocols with overloaded method members -- `super()` on nominal subtypes (explicit and implicit) of protocol classes -- [Recursive protocols][recursive_protocols_spec] -- Generic protocols -- Non-generic protocols with function-scoped generic methods -- Protocols with instance attributes annotated with `Callable` (can a nominal type with a method - satisfy that protocol, and if so in what cases?) -- Protocols decorated with `@final` -- Protocols with attribute members annotated with `Any` -- Protocols with methods that have parameters or the return type unannotated -- Protocols with methods that have parameters or the return type annotated with `Any` -- Equivalence and subtyping between `Callable` types and protocols that define `__call__` - -[mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping -[mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test -[mypy_weird_protocols]: https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d8b/test-data/unit/check-protocols.test#L2131-L2132 -[protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests -[protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols -[recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols -[self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols -[typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md deleted file mode 100644 index 5aa4175f8e659..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md +++ /dev/null @@ -1,32 +0,0 @@ -# Builtin scope - -## Conditionally global or builtin - -If a builtin name is conditionally defined as a global, a name lookup should union the builtin type -with the conditionally-defined type: - -```py -def returns_bool() -> bool: - return True - -if returns_bool(): - chr: int = 1 - -def f(): - reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str) -``` - -## Conditionally global or builtin, with annotation - -Same is true if the name is annotated: - -```py -def returns_bool() -> bool: - return True - -if returns_bool(): - chr: int = 1 - -def f(): - reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md deleted file mode 100644 index 65d9a930aff1e..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ /dev/null @@ -1,173 +0,0 @@ -# Implicit globals from `types.ModuleType` - -## Implicit `ModuleType` globals - -All modules are instances of `types.ModuleType`. If a name can't be found in any local or global -scope, we look it up as an attribute on `types.ModuleType` in typeshed before deciding that the name -is unbound. - -```py -reveal_type(__name__) # revealed: str -reveal_type(__file__) # revealed: str | None -reveal_type(__loader__) # revealed: LoaderProtocol | None -reveal_type(__package__) # revealed: str | None -reveal_type(__doc__) # revealed: str | None -reveal_type(__spec__) # revealed: ModuleSpec | None - -reveal_type(__path__) # revealed: @Todo(generics) - -class X: - reveal_type(__name__) # revealed: str - -def foo(): - reveal_type(__name__) # revealed: str -``` - -However, three attributes on `types.ModuleType` are not present as implicit module globals; these -are excluded: - -```py -# error: [unresolved-reference] -# revealed: Unknown -reveal_type(__getattr__) - -# error: [unresolved-reference] -# revealed: Unknown -reveal_type(__dict__) - -# error: [unresolved-reference] -# revealed: Unknown -reveal_type(__init__) -``` - -## Accessed as attributes - -`ModuleType` attributes can also be accessed as attributes on module-literal types. The special -attributes `__dict__` and `__init__`, and all attributes on `builtins.object`, can also be accessed -as attributes on module-literal types, despite the fact that these are inaccessible as globals from -inside the module: - -```py -import typing - -reveal_type(typing.__name__) # revealed: str -reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ellipsis) -> None - -# These come from `builtins.object`, not `types.ModuleType`: -reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool - -reveal_type(typing.__class__) # revealed: Literal[ModuleType] - -# TODO: needs support generics; should be `dict[str, Any]`: -reveal_type(typing.__dict__) # revealed: @Todo(generics) -``` - -Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with -dynamic imports; but we ignore that for module-literal types where we know exactly which module -we're dealing with: - -```py -# error: [unresolved-attribute] -reveal_type(typing.__getattr__) # revealed: Unknown -``` - -## `types.ModuleType.__dict__` takes precedence over global variable `__dict__` - -It's impossible to override the `__dict__` attribute of `types.ModuleType` instances from inside the -module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named -`__dict__` in the module's global namespace: - -`foo.py`: - -```py -__dict__ = "foo" - -reveal_type(__dict__) # revealed: Literal["foo"] -``` - -`bar.py`: - -```py -import foo -from foo import __dict__ as foo_dict - -# TODO: needs support generics; should be `dict[str, Any]` for both of these: -reveal_type(foo.__dict__) # revealed: @Todo(generics) -reveal_type(foo_dict) # revealed: @Todo(generics) -``` - -## Conditionally global or `ModuleType` attribute - -Attributes overridden in the module namespace take priority. If a builtin name is conditionally -defined as a global, however, a name lookup should union the `ModuleType` type with the -conditionally defined type: - -```py -__file__ = 42 - -def returns_bool() -> bool: - return True - -if returns_bool(): - __name__ = 1 - -reveal_type(__file__) # revealed: Literal[42] -reveal_type(__name__) # revealed: Literal[1] | str -``` - -## Conditionally global or `ModuleType` attribute, with annotation - -The same is true if the name is annotated: - -```py -__file__: int = 42 - -def returns_bool() -> bool: - return True - -if returns_bool(): - __name__: int = 1 - -reveal_type(__file__) # revealed: Literal[42] -reveal_type(__name__) # revealed: Literal[1] | str -``` - -## Implicit global attributes in the current module override implicit globals from builtins - -Here, we take the type of the implicit global symbol `__name__` from the `types.ModuleType` stub -(which in this custom typeshed specifies the type as `bytes`). This is because the `main` module has -an implicit `__name__` global that shadows the builtin `__name__` symbol. - -```toml -[environment] -typeshed = "/typeshed" -``` - -`/typeshed/stdlib/builtins.pyi`: - -```pyi -class object: ... -class int: ... -class bytes: ... - -__name__: int = 42 -``` - -`/typeshed/stdlib/types.pyi`: - -```pyi -class ModuleType: - __name__: bytes -``` - -`/typeshed/stdlib/typing_extensions.pyi`: - -```pyi -def reveal_type(obj, /): ... -``` - -`main.py`: - -```py -reveal_type(__name__) # revealed: bytes -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md deleted file mode 100644 index 212c97baf9978..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md +++ /dev/null @@ -1,19 +0,0 @@ -# Classes shadowing - -## Implicit error - -```py -class C: ... - -C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional" -``` - -## Explicit - -No diagnostic is raised in the case of explicit shadowing: - -```py -class C: ... - -C: int = 1 -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md deleted file mode 100644 index ea976c3593b3b..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md +++ /dev/null @@ -1,53 +0,0 @@ -# Function shadowing - -## Parameter - -Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function. -No diagnostics should be generated. - -```py -def f(x: str): - x: int = int(x) -``` - -## Implicit error - -```py -def f(): ... - -f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional" -``` - -## Explicit shadowing - -```py -def f(): ... - -f: int = 1 -``` - -## Explicit shadowing involving `def` statements - -Since a `def` statement is a declaration, one `def` can shadow another `def`, or shadow a previous -non-`def` declaration, without error. - -```py -f = 1 -reveal_type(f) # revealed: Literal[1] - -def f(): ... - -reveal_type(f) # revealed: def f() -> Unknown - -def f(x: int) -> int: - raise NotImplementedError - -reveal_type(f) # revealed: def f(x: int) -> int - -f: int = 1 -reveal_type(f) # revealed: Literal[1] - -def f(): ... - -reveal_type(f) # revealed: def f() -> Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/slots.md b/crates/red_knot_python_semantic/resources/mdtest/slots.md deleted file mode 100644 index 05e44eb4a3e38..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/slots.md +++ /dev/null @@ -1,217 +0,0 @@ -# `__slots__` - -## Not specified and empty - -```py -class A: ... - -class B: - __slots__ = () - -class C: - __slots__ = ("lorem", "ipsum") - -class AB(A, B): ... # fine -class AC(A, C): ... # fine -class BC(B, C): ... # fine -class ABC(A, B, C): ... # fine -``` - -## Incompatible tuples - -```py -class A: - __slots__ = ("a", "b") - -class B: - __slots__ = ("c", "d") - -class C( - A, # error: [incompatible-slots] - B, # error: [incompatible-slots] -): ... -``` - -## Same value - -```py -class A: - __slots__ = ("a", "b") - -class B: - __slots__ = ("a", "b") - -class C( - A, # error: [incompatible-slots] - B, # error: [incompatible-slots] -): ... -``` - -## Strings - -```py -class A: - __slots__ = "abc" - -class B: - __slots__ = ("abc",) - -class AB( - A, # error: [incompatible-slots] - B, # error: [incompatible-slots] -): ... -``` - -## Invalid - -TODO: Emit diagnostics - -```py -class NonString1: - __slots__ = 42 - -class NonString2: - __slots__ = b"ar" - -class NonIdentifier1: - __slots__ = "42" - -class NonIdentifier2: - __slots__ = ("lorem", "42") - -class NonIdentifier3: - __slots__ = (e for e in ("lorem", "42")) -``` - -## Inheritance - -```py -class A: - __slots__ = ("a", "b") - -class B(A): ... - -class C: - __slots__ = ("c", "d") - -class D(C): ... -class E( - B, # error: [incompatible-slots] - D, # error: [incompatible-slots] -): ... -``` - -## Single solid base - -```py -class A: - __slots__ = ("a", "b") - -class B(A): ... -class C(A): ... -class D(B, A): ... # fine -class E(B, C, A): ... # fine -``` - -## Post-hoc modifications - -```py -class A: - __slots__ = () - __slots__ += ("a", "b") - -reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]] - -class B: - __slots__ = ("c", "d") - -class C( - A, # error: [incompatible-slots] - B, # error: [incompatible-slots] -): ... -``` - -## False negatives - -### Possibly unbound - -```py -def _(flag: bool): - class A: - if flag: - __slots__ = ("a", "b") - - class B: - __slots__ = ("c", "d") - - # Might or might not be fine at runtime - class C(A, B): ... -``` - -### Bound but with different types - -```py -def _(flag: bool): - class A: - if flag: - __slots__ = ("a", "b") - else: - __slots__ = () - - class B: - __slots__ = ("c", "d") - - # Might or might not be fine at runtime - class C(A, B): ... -``` - -### Non-tuples - -```py -class A: - __slots__ = ["a", "b"] # This is treated as "dynamic" - -class B: - __slots__ = ("c", "d") - -# False negative: [incompatible-slots] -class C(A, B): ... -``` - -### Built-ins with implicit layouts - -```py -# False negative: [incompatible-slots] -class A(int, str): ... -``` - -### Diagnostic if `__slots__` is externally modified - -We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol -is not declared — a case in which we union with `Unknown` for other public symbols. The reason for -this is that `__slots__` has a special handling in the runtime. Modifying it externally is actually -allowed, but those changes do not take effect. If you have a class `C` with `__slots__ = ("foo",)` -and externally set `C.__slots__ = ("bar",)`, you still can't access `C.bar`. And you can still -access `C.foo`. We therefore issue a diagnostic for such assignments: - -```py -class A: - __slots__ = ("a",) - - # Modifying `__slots__` from within the class body is fine: - __slots__ = ("a", "b") - -# No `Unknown` here: -reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]] - -# But modifying it externally is not: - -# error: [invalid-assignment] -A.__slots__ = ("a",) - -# error: [invalid-assignment] -A.__slots__ = ("a", "b_new") - -# error: [invalid-assignment] -A.__slots__ = ("a", "b", "c") -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap deleted file mode 100644 index 2914deba0c353..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | class WrongDescriptor: - 2 | def __set__(self, instance: object, value: int, extra: int) -> None: - 3 | pass - 4 | - 5 | class C: - 6 | attr: WrongDescriptor = WrongDescriptor() - 7 | - 8 | instance = C() - 9 | -10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) -11 | instance.attr = 1 # error: [invalid-assignment] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:11:1 - | -10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) -11 | instance.attr = 1 # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap deleted file mode 100644 index 90b4a81494efb..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | class Descriptor: - 2 | def __set__(self, instance: object, value: int) -> None: - 3 | pass - 4 | - 5 | class C: - 6 | attr: Descriptor = Descriptor() - 7 | - 8 | instance = C() - 9 | instance.attr = 1 # fine -10 | -11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) -12 | instance.attr = "wrong" # error: [invalid-assignment] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:12:1 - | -11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) -12 | instance.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap deleted file mode 100644 index eb06b09308944..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class C: -2 | attr: int = 0 -3 | -4 | instance = C() -5 | instance.attr = 1 # fine -6 | instance.attr = "wrong" # error: [invalid-assignment] -7 | -8 | C.attr = 1 # fine -9 | C.attr = "wrong" # error: [invalid-assignment] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:6:1 - | -4 | instance = C() -5 | instance.attr = 1 # fine -6 | instance.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` -7 | -8 | C.attr = 1 # fine - | - -``` - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:9:1 - | -8 | C.attr = 1 # fine -9 | C.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap deleted file mode 100644 index 8bc9529169efa..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def _(flag: bool) -> None: -2 | class C: -3 | if flag: -4 | attr: int = 0 -5 | -6 | C.attr = 1 # error: [possibly-unbound-attribute] -7 | -8 | instance = C() -9 | instance.attr = 1 # error: [possibly-unbound-attribute] -``` - -# Diagnostics - -``` -warning: lint:possibly-unbound-attribute - --> /src/mdtest_snippet.py:6:5 - | -4 | attr: int = 0 -5 | -6 | C.attr = 1 # error: [possibly-unbound-attribute] - | ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound -7 | -8 | instance = C() - | - -``` - -``` -warning: lint:possibly-unbound-attribute - --> /src/mdtest_snippet.py:9:5 - | -8 | instance = C() -9 | instance.attr = 1 # error: [possibly-unbound-attribute] - | ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap deleted file mode 100644 index 78fe6d1da50b8..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap +++ /dev/null @@ -1,52 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class C: -2 | def __init__(self): -3 | self.attr: int = 0 -4 | -5 | instance = C() -6 | instance.attr = 1 # fine -7 | instance.attr = "wrong" # error: [invalid-assignment] -8 | -9 | C.attr = 1 # error: [invalid-attribute-access] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:7:1 - | -5 | instance = C() -6 | instance.attr = 1 # fine -7 | instance.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` -8 | -9 | C.attr = 1 # error: [invalid-attribute-access] - | - -``` - -``` -error: lint:invalid-attribute-access - --> /src/mdtest_snippet.py:9:1 - | -7 | instance.attr = "wrong" # error: [invalid-assignment] -8 | -9 | C.attr = 1 # error: [invalid-attribute-access] - | ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap deleted file mode 100644 index ec479c5896551..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Setting attributes on union types -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | def _(flag: bool) -> None: - 2 | if flag: - 3 | class C1: - 4 | attr: int = 0 - 5 | - 6 | else: - 7 | class C1: - 8 | attr: str = "" - 9 | -10 | # TODO: The error message here could be improved to explain why the assignment fails. -11 | C1.attr = 1 # error: [invalid-assignment] -12 | -13 | class C2: -14 | if flag: -15 | attr: int = 0 -16 | else: -17 | attr: str = "" -18 | -19 | # TODO: This should be an error -20 | C2.attr = 1 -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:11:5 - | -10 | # TODO: The error message here could be improved to explain why the assignment fails. -11 | C1.attr = 1 # error: [invalid-assignment] - | ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]` -12 | -13 | class C2: - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap deleted file mode 100644 index 53ef65468070f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap +++ /dev/null @@ -1,48 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - Unknown attributes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class C: ... -2 | -3 | C.non_existent = 1 # error: [unresolved-attribute] -4 | -5 | instance = C() -6 | instance.non_existent = 1 # error: [unresolved-attribute] -``` - -# Diagnostics - -``` -error: lint:unresolved-attribute - --> /src/mdtest_snippet.py:3:1 - | -1 | class C: ... -2 | -3 | C.non_existent = 1 # error: [unresolved-attribute] - | ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`. -4 | -5 | instance = C() - | - -``` - -``` -error: lint:unresolved-attribute - --> /src/mdtest_snippet.py:6:1 - | -5 | instance = C() -6 | instance.non_existent = 1 # error: [unresolved-attribute] - | ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`. - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap deleted file mode 100644 index 59389d203668a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: attribute_assignment.md - Attribute assignment - `ClassVar`s -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing import ClassVar - 2 | - 3 | class C: - 4 | attr: ClassVar[int] = 0 - 5 | - 6 | C.attr = 1 # fine - 7 | C.attr = "wrong" # error: [invalid-assignment] - 8 | - 9 | instance = C() -10 | instance.attr = 1 # error: [invalid-attribute-access] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:7:1 - | -6 | C.attr = 1 # fine -7 | C.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` -8 | -9 | instance = C() - | - -``` - -``` -error: lint:invalid-attribute-access - --> /src/mdtest_snippet.py:10:1 - | - 9 | instance = C() -10 | instance.attr = 1 # error: [invalid-attribute-access] - | ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap deleted file mode 100644 index 47bb7fae45c49..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: basic.md - Structures - Unresolvable module import -mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:1:8 - | -1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" - | ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap deleted file mode 100644 index bbcb0e3f40435..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: basic.md - Structures - Unresolvable submodule imports -mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | # Topmost component resolvable, submodule not resolvable: -2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`" -3 | -4 | # Topmost component unresolvable: -5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" -``` - -## a/__init__.py - -``` -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:2:8 - | -1 | # Topmost component resolvable, submodule not resolvable: -2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`" - | ^^^^^ Cannot resolve import `a.foo` -3 | -4 | # Topmost component unresolvable: - | - -``` - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:5:8 - | -4 | # Topmost component unresolvable: -5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" - | ^^^^^ Cannot resolve import `b.foo` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap deleted file mode 100644 index b80fd4586d566..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap +++ /dev/null @@ -1,52 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Bad `__getitem__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class Iterable: - 4 | # invalid because it will implicitly be passed an `int` - 5 | # by the interpreter - 6 | def __getitem__(self, key: str) -> int: - 7 | return 42 - 8 | - 9 | # error: [not-iterable] -10 | for x in Iterable(): -11 | reveal_type(x) # revealed: int -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:10:10 - | - 9 | # error: [not-iterable] -10 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) -11 | reveal_type(x) # revealed: int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:11:5 - | - 9 | # error: [not-iterable] -10 | for x in Iterable(): -11 | reveal_type(x) # revealed: int - | ^^^^^^^^^^^^^^ `int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap deleted file mode 100644 index a8600a1547751..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Invalid iterable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | nonsense = 123 -2 | for x in nonsense: # error: [not-iterable] -3 | pass -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:2:10 - | -1 | nonsense = 123 -2 | for x in nonsense: # error: [not-iterable] - | ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method -3 | pass - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap deleted file mode 100644 index 59cf2147b8392..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap +++ /dev/null @@ -1,37 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - New over old style iteration protocol -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class NotIterable: -2 | def __getitem__(self, key: int) -> int: -3 | return 42 -4 | __iter__: None = None -5 | -6 | for x in NotIterable(): # error: [not-iterable] -7 | pass -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:6:10 - | -4 | __iter__: None = None -5 | -6 | for x in NotIterable(): # error: [not-iterable] - | ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable -7 | pass - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap deleted file mode 100644 index a9d5964a6b1df..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - No `__iter__` method and `__getitem__` is not callable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing_extensions import reveal_type -2 | -3 | class Bad: -4 | __getitem__: None = None -5 | -6 | # error: [not-iterable] -7 | for x in Bad(): -8 | reveal_type(x) # revealed: Unknown -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:7:10 - | -6 | # error: [not-iterable] -7 | for x in Bad(): - | ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable -8 | reveal_type(x) # revealed: Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:8:5 - | -6 | # error: [not-iterable] -7 | for x in Bad(): -8 | reveal_type(x) # revealed: Unknown - | ^^^^^^^^^^^^^^ `Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap deleted file mode 100644 index 62695a6d1ca81..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap +++ /dev/null @@ -1,98 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly-not-callable `__getitem__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | def _(flag: bool): - 4 | class CustomCallable: - 5 | if flag: - 6 | def __call__(self, *args, **kwargs) -> int: - 7 | return 42 - 8 | else: - 9 | __call__: None = None -10 | -11 | class Iterable1: -12 | __getitem__: CustomCallable = CustomCallable() -13 | -14 | class Iterable2: -15 | if flag: -16 | def __getitem__(self, key: int) -> int: -17 | return 42 -18 | else: -19 | __getitem__: None = None -20 | -21 | # error: [not-iterable] -22 | for x in Iterable1(): -23 | # TODO... `int` might be ideal here? -24 | reveal_type(x) # revealed: int | Unknown -25 | -26 | # error: [not-iterable] -27 | for y in Iterable2(): -28 | # TODO... `int` might be ideal here? -29 | reveal_type(y) # revealed: int | Unknown -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:22:14 - | -21 | # error: [not-iterable] -22 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable -23 | # TODO... `int` might be ideal here? -24 | reveal_type(x) # revealed: int | Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:24:9 - | -22 | for x in Iterable1(): -23 | # TODO... `int` might be ideal here? -24 | reveal_type(x) # revealed: int | Unknown - | ^^^^^^^^^^^^^^ `int | Unknown` -25 | -26 | # error: [not-iterable] - | - -``` - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:27:14 - | -26 | # error: [not-iterable] -27 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable -28 | # TODO... `int` might be ideal here? -29 | reveal_type(y) # revealed: int | Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:29:9 - | -27 | for y in Iterable2(): -28 | # TODO... `int` might be ideal here? -29 | reveal_type(y) # revealed: int | Unknown - | ^^^^^^^^^^^^^^ `int | Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap deleted file mode 100644 index 1a0330059d18c..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap +++ /dev/null @@ -1,94 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly invalid `__getitem__` methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | def _(flag: bool): - 4 | class Iterable1: - 5 | if flag: - 6 | def __getitem__(self, item: int) -> str: - 7 | return "foo" - 8 | else: - 9 | __getitem__: None = None -10 | -11 | class Iterable2: -12 | if flag: -13 | def __getitem__(self, item: int) -> str: -14 | return "foo" -15 | else: -16 | def __getitem__(self, item: str) -> int: -17 | return 42 -18 | -19 | # error: [not-iterable] -20 | for x in Iterable1(): -21 | # TODO: `str` might be better -22 | reveal_type(x) # revealed: str | Unknown -23 | -24 | # error: [not-iterable] -25 | for y in Iterable2(): -26 | reveal_type(y) # revealed: str | int -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:20:14 - | -19 | # error: [not-iterable] -20 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable -21 | # TODO: `str` might be better -22 | reveal_type(x) # revealed: str | Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:22:9 - | -20 | for x in Iterable1(): -21 | # TODO: `str` might be better -22 | reveal_type(x) # revealed: str | Unknown - | ^^^^^^^^^^^^^^ `str | Unknown` -23 | -24 | # error: [not-iterable] - | - -``` - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:25:14 - | -24 | # error: [not-iterable] -25 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) -26 | reveal_type(y) # revealed: str | int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:26:9 - | -24 | # error: [not-iterable] -25 | for y in Iterable2(): -26 | reveal_type(y) # revealed: str | int - | ^^^^^^^^^^^^^^ `str | int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap deleted file mode 100644 index d59db051e7f34..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap +++ /dev/null @@ -1,98 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly invalid `__iter__` methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class Iterator: - 4 | def __next__(self) -> int: - 5 | return 42 - 6 | - 7 | def _(flag: bool): - 8 | class Iterable1: - 9 | if flag: -10 | def __iter__(self) -> Iterator: -11 | return Iterator() -12 | else: -13 | def __iter__(self, invalid_extra_arg) -> Iterator: -14 | return Iterator() -15 | -16 | # error: [not-iterable] -17 | for x in Iterable1(): -18 | reveal_type(x) # revealed: int -19 | -20 | class Iterable2: -21 | if flag: -22 | def __iter__(self) -> Iterator: -23 | return Iterator() -24 | else: -25 | __iter__: None = None -26 | -27 | # error: [not-iterable] -28 | for x in Iterable2(): -29 | # TODO: `int` would probably be better here: -30 | reveal_type(x) # revealed: int | Unknown -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:17:14 - | -16 | # error: [not-iterable] -17 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`) -18 | reveal_type(x) # revealed: int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:18:9 - | -16 | # error: [not-iterable] -17 | for x in Iterable1(): -18 | reveal_type(x) # revealed: int - | ^^^^^^^^^^^^^^ `int` -19 | -20 | class Iterable2: - | - -``` - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:28:14 - | -27 | # error: [not-iterable] -28 | for x in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable -29 | # TODO: `int` would probably be better here: -30 | reveal_type(x) # revealed: int | Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:30:9 - | -28 | for x in Iterable2(): -29 | # TODO: `int` would probably be better here: -30 | reveal_type(x) # revealed: int | Unknown - | ^^^^^^^^^^^^^^ `int | Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap deleted file mode 100644 index e4957e08fd0ed..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap +++ /dev/null @@ -1,102 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly invalid `__next__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | def _(flag: bool): - 4 | class Iterator1: - 5 | if flag: - 6 | def __next__(self) -> int: - 7 | return 42 - 8 | else: - 9 | def __next__(self, invalid_extra_arg) -> str: -10 | return "foo" -11 | -12 | class Iterator2: -13 | if flag: -14 | def __next__(self) -> int: -15 | return 42 -16 | else: -17 | __next__: None = None -18 | -19 | class Iterable1: -20 | def __iter__(self) -> Iterator1: -21 | return Iterator1() -22 | -23 | class Iterable2: -24 | def __iter__(self) -> Iterator2: -25 | return Iterator2() -26 | -27 | # error: [not-iterable] -28 | for x in Iterable1(): -29 | reveal_type(x) # revealed: int | str -30 | -31 | # error: [not-iterable] -32 | for y in Iterable2(): -33 | # TODO: `int` would probably be better here: -34 | reveal_type(y) # revealed: int | Unknown -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:28:14 - | -27 | # error: [not-iterable] -28 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`) -29 | reveal_type(x) # revealed: int | str - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:29:9 - | -27 | # error: [not-iterable] -28 | for x in Iterable1(): -29 | reveal_type(x) # revealed: int | str - | ^^^^^^^^^^^^^^ `int | str` -30 | -31 | # error: [not-iterable] - | - -``` - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:32:14 - | -31 | # error: [not-iterable] -32 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable -33 | # TODO: `int` would probably be better here: -34 | reveal_type(y) # revealed: int | Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:34:9 - | -32 | for y in Iterable2(): -33 | # TODO: `int` would probably be better here: -34 | reveal_type(y) # revealed: int | Unknown - | ^^^^^^^^^^^^^^ `int | Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap deleted file mode 100644 index dfcb11a238d26..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap +++ /dev/null @@ -1,60 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | def _(flag: bool): - 4 | class Iterator: - 5 | def __next__(self) -> int: - 6 | return 42 - 7 | - 8 | class Iterable: - 9 | if flag: -10 | def __iter__(self) -> Iterator: -11 | return Iterator() -12 | # invalid signature because it only accepts a `str`, -13 | # but the old-style iteration protocol will pass it an `int` -14 | def __getitem__(self, key: str) -> bytes: -15 | return bytes() -16 | -17 | # error: [not-iterable] -18 | for x in Iterable(): -19 | reveal_type(x) # revealed: int | bytes -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:18:14 - | -17 | # error: [not-iterable] -18 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) -19 | reveal_type(x) # revealed: int | bytes - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:19:9 - | -17 | # error: [not-iterable] -18 | for x in Iterable(): -19 | reveal_type(x) # revealed: int | bytes - | ^^^^^^^^^^^^^^ `int | bytes` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap deleted file mode 100644 index 90a743e3008e8..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap +++ /dev/null @@ -1,105 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class Iterator: - 4 | def __next__(self) -> bytes: - 5 | return b"foo" - 6 | - 7 | def _(flag: bool, flag2: bool): - 8 | class Iterable1: - 9 | if flag: -10 | def __getitem__(self, item: int) -> str: -11 | return "foo" -12 | else: -13 | __getitem__: None = None -14 | -15 | if flag2: -16 | def __iter__(self) -> Iterator: -17 | return Iterator() -18 | -19 | class Iterable2: -20 | if flag: -21 | def __getitem__(self, item: int) -> str: -22 | return "foo" -23 | else: -24 | def __getitem__(self, item: str) -> int: -25 | return 42 -26 | if flag2: -27 | def __iter__(self) -> Iterator: -28 | return Iterator() -29 | -30 | # error: [not-iterable] -31 | for x in Iterable1(): -32 | # TODO: `bytes | str` might be better -33 | reveal_type(x) # revealed: bytes | str | Unknown -34 | -35 | # error: [not-iterable] -36 | for y in Iterable2(): -37 | reveal_type(y) # revealed: bytes | str | int -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:31:14 - | -30 | # error: [not-iterable] -31 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable -32 | # TODO: `bytes | str` might be better -33 | reveal_type(x) # revealed: bytes | str | Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:33:9 - | -31 | for x in Iterable1(): -32 | # TODO: `bytes | str` might be better -33 | reveal_type(x) # revealed: bytes | str | Unknown - | ^^^^^^^^^^^^^^ `bytes | str | Unknown` -34 | -35 | # error: [not-iterable] - | - -``` - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:36:14 - | -35 | # error: [not-iterable] -36 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) -37 | reveal_type(y) # revealed: bytes | str | int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:37:9 - | -35 | # error: [not-iterable] -36 | for y in Iterable2(): -37 | reveal_type(y) # revealed: bytes | str | int - | ^^^^^^^^^^^^^^ `bytes | str | int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap deleted file mode 100644 index dacd22e758d5a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap +++ /dev/null @@ -1,59 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class Iterator: - 4 | def __next__(self) -> int: - 5 | return 42 - 6 | - 7 | def _(flag1: bool, flag2: bool): - 8 | class Iterable: - 9 | if flag1: -10 | def __iter__(self) -> Iterator: -11 | return Iterator() -12 | if flag2: -13 | def __getitem__(self, key: int) -> bytes: -14 | return bytes() -15 | -16 | # error: [not-iterable] -17 | for x in Iterable(): -18 | reveal_type(x) # revealed: int | bytes -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:17:14 - | -16 | # error: [not-iterable] -17 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method -18 | reveal_type(x) # revealed: int | bytes - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:18:9 - | -16 | # error: [not-iterable] -17 | for x in Iterable(): -18 | reveal_type(x) # revealed: int | bytes - | ^^^^^^^^^^^^^^ `int | bytes` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap deleted file mode 100644 index f5f70d0c134f5..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Union type as iterable where one union element has invalid `__iter__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class TestIter: - 4 | def __next__(self) -> int: - 5 | return 42 - 6 | - 7 | class Test: - 8 | def __iter__(self) -> TestIter: - 9 | return TestIter() -10 | -11 | class Test2: -12 | def __iter__(self) -> int: -13 | return 42 -14 | -15 | def _(flag: bool): -16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) -17 | # error: [not-iterable] -18 | for x in Test() if flag else Test2(): -19 | reveal_type(x) # revealed: int -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:18:14 - | -16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) -17 | # error: [not-iterable] -18 | for x in Test() if flag else Test2(): - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method -19 | reveal_type(x) # revealed: int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:19:9 - | -17 | # error: [not-iterable] -18 | for x in Test() if flag else Test2(): -19 | reveal_type(x) # revealed: int - | ^^^^^^^^^^^^^^ `int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap deleted file mode 100644 index c474ec522ccab..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap +++ /dev/null @@ -1,56 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - Union type as iterable where one union element has no `__iter__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class TestIter: - 4 | def __next__(self) -> int: - 5 | return 42 - 6 | - 7 | class Test: - 8 | def __iter__(self) -> TestIter: - 9 | return TestIter() -10 | -11 | def _(flag: bool): -12 | # error: [not-iterable] -13 | for x in Test() if flag else 42: -14 | reveal_type(x) # revealed: int -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:13:14 - | -11 | def _(flag: bool): -12 | # error: [not-iterable] -13 | for x in Test() if flag else 42: - | ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method -14 | reveal_type(x) # revealed: int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:14:9 - | -12 | # error: [not-iterable] -13 | for x in Test() if flag else 42: -14 | reveal_type(x) # revealed: int - | ^^^^^^^^^^^^^^ `int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap deleted file mode 100644 index 2fb989fd46909..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap +++ /dev/null @@ -1,69 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - With non-callable iterator -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | def _(flag: bool): - 4 | class NotIterable: - 5 | if flag: - 6 | __iter__: int = 1 - 7 | else: - 8 | __iter__: None = None - 9 | -10 | # error: [not-iterable] -11 | for x in NotIterable(): -12 | pass -13 | -14 | # revealed: Unknown -15 | # error: [possibly-unresolved-reference] -16 | reveal_type(x) -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:11:14 - | -10 | # error: [not-iterable] -11 | for x in NotIterable(): - | ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable -12 | pass - | - -``` - -``` -warning: lint:possibly-unresolved-reference - --> /src/mdtest_snippet.py:16:17 - | -14 | # revealed: Unknown -15 | # error: [possibly-unresolved-reference] -16 | reveal_type(x) - | ^ Name `x` used when possibly not defined - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:16:5 - | -14 | # revealed: Unknown -15 | # error: [possibly-unresolved-reference] -16 | reveal_type(x) - | ^^^^^^^^^^^^^^ `Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap deleted file mode 100644 index cf9109de802d6..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - `__iter__` does not return an iterator -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing_extensions import reveal_type -2 | -3 | class Bad: -4 | def __iter__(self) -> int: -5 | return 42 -6 | -7 | # error: [not-iterable] -8 | for x in Bad(): -9 | reveal_type(x) # revealed: Unknown -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:8:10 - | -7 | # error: [not-iterable] -8 | for x in Bad(): - | ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method -9 | reveal_type(x) # revealed: Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:9:5 - | -7 | # error: [not-iterable] -8 | for x in Bad(): -9 | reveal_type(x) # revealed: Unknown - | ^^^^^^^^^^^^^^ `Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap deleted file mode 100644 index 65331fe1971d0..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - `__iter__` method with a bad signature -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class Iterator: - 4 | def __next__(self) -> int: - 5 | return 42 - 6 | - 7 | class Iterable: - 8 | def __iter__(self, extra_arg) -> Iterator: - 9 | return Iterator() -10 | -11 | # error: [not-iterable] -12 | for x in Iterable(): -13 | reveal_type(x) # revealed: int -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:12:10 - | -11 | # error: [not-iterable] -12 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`) -13 | reveal_type(x) # revealed: int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:13:5 - | -11 | # error: [not-iterable] -12 | for x in Iterable(): -13 | reveal_type(x) # revealed: int - | ^^^^^^^^^^^^^^ `int` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap deleted file mode 100644 index 5807ade696e66..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap +++ /dev/null @@ -1,91 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: for.md - For loops - `__iter__` returns an iterator with an invalid `__next__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import reveal_type - 2 | - 3 | class Iterator1: - 4 | def __next__(self, extra_arg) -> int: - 5 | return 42 - 6 | - 7 | class Iterator2: - 8 | __next__: None = None - 9 | -10 | class Iterable1: -11 | def __iter__(self) -> Iterator1: -12 | return Iterator1() -13 | -14 | class Iterable2: -15 | def __iter__(self) -> Iterator2: -16 | return Iterator2() -17 | -18 | # error: [not-iterable] -19 | for x in Iterable1(): -20 | reveal_type(x) # revealed: int -21 | -22 | # error: [not-iterable] -23 | for y in Iterable2(): -24 | reveal_type(y) # revealed: Unknown -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:19:10 - | -18 | # error: [not-iterable] -19 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`) -20 | reveal_type(x) # revealed: int - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:20:5 - | -18 | # error: [not-iterable] -19 | for x in Iterable1(): -20 | reveal_type(x) # revealed: int - | ^^^^^^^^^^^^^^ `int` -21 | -22 | # error: [not-iterable] - | - -``` - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:23:10 - | -22 | # error: [not-iterable] -23 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable -24 | reveal_type(y) # revealed: Unknown - | - -``` - -``` -info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:24:5 - | -22 | # error: [not-iterable] -23 | for y in Iterable2(): -24 | reveal_type(y) # revealed: Unknown - | ^^^^^^^^^^^^^^ `Unknown` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap deleted file mode 100644 index cebcc8765538f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class NotBoolable: -2 | __bool__: int = 3 -3 | -4 | a = NotBoolable() -5 | -6 | # error: [unsupported-bool-conversion] -7 | 10 and a and True -``` - -# Diagnostics - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:7:8 - | -6 | # error: [unsupported-bool-conversion] -7 | 10 and a and True - | ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap deleted file mode 100644 index a95ae257f481a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int) -> int: -2 | return x * x -3 | -4 | foo("hello") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:5 - | -2 | return x * x -3 | -4 | foo("hello") # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int) -> int: - | ^^^ ------ Parameter declared here -2 | return x * x - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap deleted file mode 100644 index b6ef2802033d8..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class C: -2 | def square(self, x: int) -> int: -3 | return x * x -4 | -5 | c = C() -6 | c.square("hello") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:6:10 - | -5 | c = C() -6 | c.square("hello") # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:2:9 - | -1 | class C: -2 | def square(self, x: int) -> int: - | ^^^^^^ ------ Parameter declared here -3 | return x * x - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap deleted file mode 100644 index 25d65a1940435..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## package.py - -``` -1 | def foo(x: int) -> int: -2 | return x * x -``` - -## mdtest_snippet.py - -``` -1 | import package -2 | -3 | package.foo("hello") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:3:13 - | -1 | import package -2 | -3 | package.foo("hello") # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/package.py:1:5 - | -1 | def foo(x: int) -> int: - | ^^^ ------ Parameter declared here -2 | return x * x - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap deleted file mode 100644 index 8910a6e1bf636..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def bar(): -2 | foo("hello") # error: [invalid-argument-type] -3 | -4 | def foo(x: int) -> int: -5 | return x * x -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:2:9 - | -1 | def bar(): -2 | foo("hello") # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` -3 | -4 | def foo(x: int) -> int: - | -info: Function defined here - --> /src/mdtest_snippet.py:4:5 - | -2 | foo("hello") # error: [invalid-argument-type] -3 | -4 | def foo(x: int) -> int: - | ^^^ ------ Parameter declared here -5 | return x * x - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap deleted file mode 100644 index b23b61c9ba216..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int, y: int, z: int) -> int: -2 | return x * y * z -3 | -4 | foo(1, "hello", 3) # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:8 - | -2 | return x * y * z -3 | -4 | foo(1, "hello", 3) # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, z: int) -> int: - | ^^^ ------ Parameter declared here -2 | return x * y * z - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap deleted file mode 100644 index 8d08b8b9dec2a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap +++ /dev/null @@ -1,48 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo( -2 | x: int, -3 | y: int, -4 | z: int, -5 | ) -> int: -6 | return x * y * z -7 | -8 | foo(1, "hello", 3) # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:8:8 - | -6 | return x * y * z -7 | -8 | foo(1, "hello", 3) # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo( - | ^^^ -2 | x: int, -3 | y: int, - | ------ Parameter declared here -4 | z: int, -5 | ) -> int: - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap deleted file mode 100644 index c4b5dd367734d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap +++ /dev/null @@ -1,81 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int, y: int, z: int) -> int: -2 | return x * y * z -3 | -4 | # error: [invalid-argument-type] -5 | # error: [invalid-argument-type] -6 | # error: [invalid-argument-type] -7 | foo("a", "b", "c") -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:7:5 - | -5 | # error: [invalid-argument-type] -6 | # error: [invalid-argument-type] -7 | foo("a", "b", "c") - | ^^^ Expected `int`, found `Literal["a"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, z: int) -> int: - | ^^^ ------ Parameter declared here -2 | return x * y * z - | - -``` - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:7:10 - | -5 | # error: [invalid-argument-type] -6 | # error: [invalid-argument-type] -7 | foo("a", "b", "c") - | ^^^ Expected `int`, found `Literal["b"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, z: int) -> int: - | ^^^ ------ Parameter declared here -2 | return x * y * z - | - -``` - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:7:15 - | -5 | # error: [invalid-argument-type] -6 | # error: [invalid-argument-type] -7 | foo("a", "b", "c") - | ^^^ Expected `int`, found `Literal["c"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, z: int) -> int: - | ^^^ ------ Parameter declared here -2 | return x * y * z - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap deleted file mode 100644 index 8ff31730b7675..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | import json -2 | -3 | json.loads(5) # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:3:12 - | -1 | import json -2 | -3 | json.loads(5) # error: [invalid-argument-type] - | ^ Expected `str | bytes | bytearray`, found `Literal[5]` - | -info: Function defined here - --> stdlib/json/__init__.pyi:39:5 - | -37 | **kwds: Any, -38 | ) -> None: ... -39 | def loads( - | ^^^^^ -40 | s: str | bytes | bytearray, - | -------------------------- Parameter declared here -41 | *, -42 | cls: type[JSONDecoder] | None = None, - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap deleted file mode 100644 index 6bb4b90f2280a..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int, y: int, *, z: int = 0) -> int: -2 | return x * y * z -3 | -4 | foo(1, 2, z="hello") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:11 - | -2 | return x * y * z -3 | -4 | foo(1, 2, z="hello") # error: [invalid-argument-type] - | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, *, z: int = 0) -> int: - | ^^^ ---------- Parameter declared here -2 | return x * y * z - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap deleted file mode 100644 index e9fc47e2bcacf..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int, /, y: int, *, z: int = 0) -> int: -2 | return x * y * z -3 | -4 | foo(1, 2, z="hello") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:11 - | -2 | return x * y * z -3 | -4 | foo(1, 2, z="hello") # error: [invalid-argument-type] - | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, /, y: int, *, z: int = 0) -> int: - | ^^^ ---------- Parameter declared here -2 | return x * y * z - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap deleted file mode 100644 index 6add6e72c6d7f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int, y: int, z: int = 0) -> int: -2 | return x * y * z -3 | -4 | foo(1, 2, "hello") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:11 - | -2 | return x * y * z -3 | -4 | foo(1, 2, "hello") # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, z: int = 0) -> int: - | ^^^ ---------- Parameter declared here -2 | return x * y * z - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap deleted file mode 100644 index 7834637de1151..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(x: int, y: int, z: int, /) -> int: -2 | return x * y * z -3 | -4 | foo(1, "hello", 3) # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:8 - | -2 | return x * y * z -3 | -4 | foo(1, "hello", 3) # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(x: int, y: int, z: int, /) -> int: - | ^^^ ------ Parameter declared here -2 | return x * y * z - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap deleted file mode 100644 index 6dfa130e9bc6d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class C: -2 | def __call__(self, x: int) -> int: -3 | return 1 -4 | -5 | c = C() -6 | c("wrong") # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:6:3 - | -5 | c = C() -6 | c("wrong") # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["wrong"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:2:9 - | -1 | class C: -2 | def __call__(self, x: int) -> int: - | ^^^^^^^^ ------ Parameter declared here -3 | return 1 - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap deleted file mode 100644 index b9ef2e2e33ed7..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(*numbers: int) -> int: -2 | return len(numbers) -3 | -4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:14 - | -2 | return len(numbers) -3 | -4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type] - | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(*numbers: int) -> int: - | ^^^ ------------- Parameter declared here -2 | return len(numbers) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap deleted file mode 100644 index d61b5503490e1..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | def foo(**numbers: int) -> int: -2 | return len(numbers) -3 | -4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type] -``` - -# Diagnostics - -``` -error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:20 - | -2 | return len(numbers) -3 | -4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type] - | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` - | -info: Function defined here - --> /src/mdtest_snippet.py:1:5 - | -1 | def foo(**numbers: int) -> int: - | ^^^ -------------- Parameter declared here -2 | return len(numbers) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap deleted file mode 100644 index c811afea2fa0d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap +++ /dev/null @@ -1,53 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | class NotBoolable: - 2 | __bool__: int = 3 - 3 | - 4 | class WithContains: - 5 | def __contains__(self, item) -> NotBoolable: - 6 | return NotBoolable() - 7 | - 8 | # error: [unsupported-bool-conversion] - 9 | 10 in WithContains() -10 | # error: [unsupported-bool-conversion] -11 | 10 not in WithContains() -``` - -# Diagnostics - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:9:1 - | - 8 | # error: [unsupported-bool-conversion] - 9 | 10 in WithContains() - | ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable -10 | # error: [unsupported-bool-conversion] -11 | 10 not in WithContains() - | - -``` - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:11:1 - | - 9 | 10 in WithContains() -10 | # error: [unsupported-bool-conversion] -11 | 10 not in WithContains() - | ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap deleted file mode 100644 index c824ee37da98f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | type("Foo", ()) # error: [no-matching-overload] -``` - -# Diagnostics - -``` -error: lint:no-matching-overload - --> /src/mdtest_snippet.py:1:1 - | -1 | type("Foo", ()) # error: [no-matching-overload] - | ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap deleted file mode 100644 index 3482463acd311..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap +++ /dev/null @@ -1,33 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly -mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class NotBoolable: -2 | __bool__: int = 3 -3 | -4 | # error: [unsupported-bool-conversion] -5 | not NotBoolable() -``` - -# Diagnostics - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:5:1 - | -4 | # error: [unsupported-bool-conversion] -5 | not NotBoolable() - | ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap deleted file mode 100644 index f21ec59ed7e0d..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap +++ /dev/null @@ -1,88 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: return_type.md - Function return type - Invalid conditional return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | def f(cond: bool) -> str: - 2 | if cond: - 3 | return "a" - 4 | else: - 5 | # error: [invalid-return-type] - 6 | return 1 - 7 | - 8 | def f(cond: bool) -> str: - 9 | if cond: -10 | # error: [invalid-return-type] -11 | return 1 -12 | else: -13 | # error: [invalid-return-type] -14 | return 2 -``` - -# Diagnostics - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:1:22 - | -1 | def f(cond: bool) -> str: - | --- Expected `str` because of return type -2 | if cond: -3 | return "a" -4 | else: -5 | # error: [invalid-return-type] -6 | return 1 - | ^ Expected `str`, found `Literal[1]` -7 | -8 | def f(cond: bool) -> str: - | - -``` - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:8:22 - | - 6 | return 1 - 7 | - 8 | def f(cond: bool) -> str: - | --- Expected `str` because of return type - 9 | if cond: -10 | # error: [invalid-return-type] -11 | return 1 - | ^ Expected `str`, found `Literal[1]` -12 | else: -13 | # error: [invalid-return-type] - | - -``` - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:14:16 - | -12 | else: -13 | # error: [invalid-return-type] -14 | return 2 - | ^ Expected `str`, found `Literal[2]` - | - ::: /src/mdtest_snippet.py:8:22 - | - 6 | return 1 - 7 | - 8 | def f(cond: bool) -> str: - | --- Expected `str` because of return type - 9 | if cond: -10 | # error: [invalid-return-type] - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap deleted file mode 100644 index e88e7a29a7de5..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap +++ /dev/null @@ -1,95 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: return_type.md - Function return type - Invalid implicit return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | def f() -> None: - 2 | if False: - 3 | # error: [invalid-return-type] - 4 | return 1 - 5 | - 6 | # error: [invalid-return-type] - 7 | def f(cond: bool) -> int: - 8 | if cond: - 9 | return 1 -10 | -11 | # error: [invalid-return-type] -12 | def f(cond: bool) -> int: -13 | if cond: -14 | raise ValueError() -15 | -16 | # error: [invalid-return-type] -17 | def f(cond: bool) -> int: -18 | if cond: -19 | cond = False -20 | else: -21 | return 1 -22 | if cond: -23 | return 2 -``` - -# Diagnostics - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:1:12 - | -1 | def f() -> None: - | ---- Expected `None` because of return type -2 | if False: -3 | # error: [invalid-return-type] -4 | return 1 - | ^ Expected `None`, found `Literal[1]` -5 | -6 | # error: [invalid-return-type] - | - -``` - -``` -error: lint:invalid-return-type - --> /src/mdtest_snippet.py:7:22 - | -6 | # error: [invalid-return-type] -7 | def f(cond: bool) -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` -8 | if cond: -9 | return 1 - | - -``` - -``` -error: lint:invalid-return-type - --> /src/mdtest_snippet.py:12:22 - | -11 | # error: [invalid-return-type] -12 | def f(cond: bool) -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` -13 | if cond: -14 | raise ValueError() - | - -``` - -``` -error: lint:invalid-return-type - --> /src/mdtest_snippet.py:17:22 - | -16 | # error: [invalid-return-type] -17 | def f(cond: bool) -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` -18 | if cond: -19 | cond = False - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap deleted file mode 100644 index b54a9b6abb548..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap +++ /dev/null @@ -1,81 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: return_type.md - Function return type - Invalid return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | # error: [invalid-return-type] - 2 | def f() -> int: - 3 | 1 - 4 | - 5 | def f() -> str: - 6 | # error: [invalid-return-type] - 7 | return 1 - 8 | - 9 | def f() -> int: -10 | # error: [invalid-return-type] -11 | return -12 | -13 | from typing import TypeVar -14 | -15 | T = TypeVar("T") -16 | -17 | # TODO: `invalid-return-type` error should be emitted -18 | def m(x: T) -> T: ... -``` - -# Diagnostics - -``` -error: lint:invalid-return-type - --> /src/mdtest_snippet.py:2:12 - | -1 | # error: [invalid-return-type] -2 | def f() -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` -3 | 1 - | - -``` - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:5:12 - | -3 | 1 -4 | -5 | def f() -> str: - | --- Expected `str` because of return type -6 | # error: [invalid-return-type] -7 | return 1 - | ^ Expected `str`, found `Literal[1]` -8 | -9 | def f() -> int: - | - -``` - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:9:12 - | - 7 | return 1 - 8 | - 9 | def f() -> int: - | --- Expected `int` because of return type -10 | # error: [invalid-return-type] -11 | return - | ^^^^^^ Expected `int`, found `None` -12 | -13 | from typing import TypeVar - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap deleted file mode 100644 index 3514f85b6cd42..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap +++ /dev/null @@ -1,71 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: return_type.md - Function return type - Invalid return type in stub file -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md ---- - -# Python source files - -## mdtest_snippet.pyi - -``` - 1 | def f() -> int: - 2 | # error: [invalid-return-type] - 3 | return ... - 4 | - 5 | # error: [invalid-return-type] - 6 | def foo() -> int: - 7 | print("...") - 8 | ... - 9 | -10 | # error: [invalid-return-type] -11 | def foo() -> int: -12 | f"""{foo} is a function that ...""" -13 | ... -``` - -# Diagnostics - -``` -error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.pyi:1:12 - | -1 | def f() -> int: - | --- Expected `int` because of return type -2 | # error: [invalid-return-type] -3 | return ... - | ^^^ Expected `int`, found `ellipsis` -4 | -5 | # error: [invalid-return-type] - | - -``` - -``` -error: lint:invalid-return-type - --> /src/mdtest_snippet.pyi:6:14 - | -5 | # error: [invalid-return-type] -6 | def foo() -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` -7 | print("...") -8 | ... - | - -``` - -``` -error: lint:invalid-return-type - --> /src/mdtest_snippet.pyi:11:14 - | -10 | # error: [invalid-return-type] -11 | def foo() -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` -12 | f"""{foo} is a function that ...""" -13 | ... - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap deleted file mode 100644 index c0004ad58d0f0..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap +++ /dev/null @@ -1,60 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | class NotBoolable: - 2 | __bool__: int = 3 - 3 | - 4 | class Comparable: - 5 | def __lt__(self, item) -> NotBoolable: - 6 | return NotBoolable() - 7 | - 8 | def __gt__(self, item) -> NotBoolable: - 9 | return NotBoolable() -10 | -11 | # error: [unsupported-bool-conversion] -12 | 10 < Comparable() < 20 -13 | # error: [unsupported-bool-conversion] -14 | 10 < Comparable() < Comparable() -15 | -16 | Comparable() < Comparable() # fine -``` - -# Diagnostics - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:12:1 - | -11 | # error: [unsupported-bool-conversion] -12 | 10 < Comparable() < 20 - | ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable -13 | # error: [unsupported-bool-conversion] -14 | 10 < Comparable() < Comparable() - | - -``` - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:14:1 - | -12 | 10 < Comparable() < 20 -13 | # error: [unsupported-bool-conversion] -14 | 10 < Comparable() < Comparable() - | ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable -15 | -16 | Comparable() < Comparable() # fine - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap deleted file mode 100644 index b741702c18837..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap +++ /dev/null @@ -1,47 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | class NotBoolable: - 2 | __bool__: int = 5 - 3 | - 4 | class Comparable: - 5 | def __lt__(self, other) -> NotBoolable: - 6 | return NotBoolable() - 7 | - 8 | def __gt__(self, other) -> NotBoolable: - 9 | return NotBoolable() -10 | -11 | a = (1, Comparable()) -12 | b = (1, Comparable()) -13 | -14 | # error: [unsupported-bool-conversion] -15 | a < b < b -16 | -17 | a < b # fine -``` - -# Diagnostics - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:15:1 - | -14 | # error: [unsupported-bool-conversion] -15 | a < b < b - | ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable -16 | -17 | a < b # fine - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap deleted file mode 100644 index 1580c863e8099..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap +++ /dev/null @@ -1,37 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | class NotBoolable: -2 | __bool__: None = None -3 | -4 | class A: -5 | def __eq__(self, other) -> NotBoolable: -6 | return NotBoolable() -7 | -8 | # error: [unsupported-bool-conversion] -9 | (A(),) == (A(),) -``` - -# Diagnostics - -``` -error: lint:unsupported-bool-conversion - --> /src/mdtest_snippet.py:9:1 - | -8 | # error: [unsupported-bool-conversion] -9 | (A(),) == (A(),) - | ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap deleted file mode 100644 index 6f2eb7f739937..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unpacking.md - Unpacking - Right hand side not iterable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | a, b = 1 # error: [not-iterable] -``` - -# Diagnostics - -``` -error: lint:not-iterable - --> /src/mdtest_snippet.py:1:8 - | -1 | a, b = 1 # error: [not-iterable] - | ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap deleted file mode 100644 index 52ded77098410..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unpacking.md - Unpacking - Too few values to unpack -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | a, b = (1,) # error: [invalid-assignment] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:1:1 - | -1 | a, b = (1,) # error: [invalid-assignment] - | ^^^^ Not enough values to unpack (expected 2, got 1) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap deleted file mode 100644 index f1f5bb30d13d9..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unpacking.md - Unpacking - Too many values to unpack -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | a, b = (1, 2, 3) # error: [invalid-assignment] -``` - -# Diagnostics - -``` -error: lint:invalid-assignment - --> /src/mdtest_snippet.py:1:1 - | -1 | a, b = (1, 2, 3) # error: [invalid-assignment] - | ^^^^ Too many values to unpack (expected 2, got 3) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap deleted file mode 100644 index 096616ac07445..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unresolved_import.md - Unresolved import diagnostics - An unresolvable import that does not use `from` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | import does_not_exist # error: [unresolved-import] -2 | -3 | x = does_not_exist.foo -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:1:8 - | -1 | import does_not_exist # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` -2 | -3 | x = does_not_exist.foo - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap deleted file mode 100644 index f297f87e8aa9f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with a resolvable module but unresolvable item -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md ---- - -# Python source files - -## a.py - -``` -1 | does_exist1 = 1 -2 | does_exist2 = 2 -``` - -## mdtest_snippet.py - -``` -1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:1:28 - | -1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist` - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap deleted file mode 100644 index 88bdb9d791337..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown current module -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from .does_not_exist import add # error: [unresolved-import] -2 | -3 | stat = add(10, 15) -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:1:7 - | -1 | from .does_not_exist import add # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist` -2 | -3 | stat = add(10, 15) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap deleted file mode 100644 index 5a0c60321e850..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown nested module -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] -2 | -3 | stat = add(10, 15) -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:1:7 - | -1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] - | ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar` -2 | -3 | stat = add(10, 15) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap deleted file mode 100644 index e7b2303977d89..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unresolvable module -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from does_not_exist import add # error: [unresolved-import] -2 | -3 | stat = add(10, 15) -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/mdtest_snippet.py:1:6 - | -1 | from does_not_exist import add # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` -2 | -3 | stat = add(10, 15) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap deleted file mode 100644 index 603a2137d07a6..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/red_knot_test/src/lib.rs -expression: snapshot ---- ---- -mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with too many leading dots -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md ---- - -# Python source files - -## package/__init__.py - -``` -``` - -## package/foo.py - -``` -1 | def add(x, y): -2 | return x + y -``` - -## package/subpackage/subsubpackage/__init__.py - -``` -1 | from ....foo import add # error: [unresolved-import] -2 | -3 | stat = add(10, 15) -``` - -# Diagnostics - -``` -error: lint:unresolved-import - --> /src/package/subpackage/subsubpackage/__init__.py:1:10 - | -1 | from ....foo import add # error: [unresolved-import] - | ^^^ Cannot resolve import `....foo` -2 | -3 | stat = add(10, 15) - | - -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md deleted file mode 100644 index 233fa3f6f28c1..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md +++ /dev/null @@ -1,42 +0,0 @@ -# Class definitions in stubs - -## Cyclical class definition - -In type stubs, classes can reference themselves in their base class definitions. For example, in -`typeshed`, we have `class str(Sequence[str]): ...`. - -```pyi -class Foo[T]: ... - -class Bar(Foo[Bar]): ... - -reveal_type(Bar) # revealed: Literal[Bar] -reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo[Bar]], Literal[object]] -``` - -## 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 -too. That is, `symbol: type` is the same as `symbol: type = ...`. - -One implication of this is that we'll always treat symbols in class scope as safe to be accessed -from the class object itself. We'll never infer a "pure instance attribute" from a stub. - -`b.pyi`: - -```pyi -from typing import ClassVar - -class C: - class_or_instance_var: int -``` - -```py -from typing import ClassVar, Literal - -from b import C - -# No error here, since we treat `class_or_instance_var` as bound on the class. -reveal_type(C.class_or_instance_var) # revealed: int -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md deleted file mode 100644 index 255cb6bc9d0f0..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ /dev/null @@ -1,53 +0,0 @@ -# Bytes subscripts - -## Indexing - -```py -b = b"\x00abc\xff" - -reveal_type(b[0]) # revealed: Literal[b"\x00"] -reveal_type(b[1]) # revealed: Literal[b"a"] -reveal_type(b[4]) # revealed: Literal[b"\xff"] - -reveal_type(b[-1]) # revealed: Literal[b"\xff"] -reveal_type(b[-2]) # revealed: Literal[b"c"] -reveal_type(b[-5]) # revealed: Literal[b"\x00"] - -reveal_type(b[False]) # revealed: Literal[b"\x00"] -reveal_type(b[True]) # revealed: Literal[b"a"] - -x = b[5] # error: [index-out-of-bounds] "Index 5 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" -reveal_type(x) # revealed: Unknown - -y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" -reveal_type(y) # revealed: Unknown - -def _(n: int): - a = b"abcde"[n] - # TODO: Support overloads... Should be `bytes` - reveal_type(a) # revealed: @Todo(return type of overloaded function) -``` - -## Slices - -```py -b: bytes = b"\x00abc\xff" - -reveal_type(b[0:2]) # revealed: Literal[b"\x00a"] -reveal_type(b[-3:]) # revealed: Literal[b"bc\xff"] - -b[0:4:0] # error: [zero-stepsize-in-slice] -b[:4:0] # error: [zero-stepsize-in-slice] -b[0::0] # error: [zero-stepsize-in-slice] -b[::0] # error: [zero-stepsize-in-slice] - -def _(m: int, n: int): - byte_slice1 = b[m:n] - # TODO: Support overloads... Should be `bytes` - reveal_type(byte_slice1) # revealed: @Todo(return type of overloaded function) - -def _(s: bytes) -> bytes: - byte_slice2 = s[0:5] - # TODO: Support overloads... Should be `bytes` - return reveal_type(byte_slice2) # revealed: @Todo(return type of overloaded function) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md deleted file mode 100644 index cbe37b302b15f..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md +++ /dev/null @@ -1,86 +0,0 @@ -# Class subscript - -## Class getitem unbound - -```py -class NotSubscriptable: ... - -a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method" -``` - -## Class getitem - -```py -class Identity: - def __class_getitem__(cls, item: int) -> str: - return str(item) - -reveal_type(Identity[0]) # revealed: str -``` - -## Class getitem union - -```py -def _(flag: bool): - class UnionClassGetItem: - if flag: - def __class_getitem__(cls, item: int) -> str: - return str(item) - else: - def __class_getitem__(cls, item: int) -> int: - return item - - reveal_type(UnionClassGetItem[0]) # revealed: str | int -``` - -## Class getitem with class union - -```py -def _(flag: bool): - class A: - def __class_getitem__(cls, item: int) -> str: - return str(item) - - class B: - def __class_getitem__(cls, item: int) -> int: - return item - - x = A if flag else B - - reveal_type(x) # revealed: Literal[A, B] - reveal_type(x[0]) # revealed: str | int -``` - -## Class getitem with unbound method union - -```py -def _(flag: bool): - if flag: - class Spam: - def __class_getitem__(self, x: int) -> str: - return "foo" - - else: - class Spam: ... - # error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound" - # revealed: str - reveal_type(Spam[42]) -``` - -## TODO: Class getitem non-class union - -```py -def _(flag: bool): - if flag: - class Eggs: - def __class_getitem__(self, x: int) -> str: - return "foo" - - else: - Eggs = 1 - - a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method" - - # TODO: should _probably_ emit `str | Unknown` - reveal_type(a) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md deleted file mode 100644 index 408a4f43b6dec..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ /dev/null @@ -1,37 +0,0 @@ -# List subscripts - -## Indexing into lists - -A list can be indexed into with: - -- numbers -- slices - -```py -x = [1, 2, 3] -reveal_type(x) # revealed: list - -# TODO reveal int -reveal_type(x[0]) # revealed: @Todo(return type of overloaded function) - -# TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo(return type of overloaded function) - -# TODO error -reveal_type(x["a"]) # revealed: @Todo(return type of overloaded function) -``` - -## Assignments within list assignment - -In assignment, we might also have a named assignment. This should also get type checked. - -```py -x = [1, 2, 3] -x[0 if (y := 2) else 1] = 5 - -# TODO error? (indeterminite index type) -x["a" if (y := 2) else 1] = 6 - -# TODO error (can't index via string) -x["a" if (y := 2) else "b"] = 6 -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md deleted file mode 100644 index 9a875dc3231c1..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md +++ /dev/null @@ -1,95 +0,0 @@ -# String subscripts - -## Indexing - -```py -s = "abcde" - -reveal_type(s[0]) # revealed: Literal["a"] -reveal_type(s[1]) # revealed: Literal["b"] -reveal_type(s[-1]) # revealed: Literal["e"] -reveal_type(s[-2]) # revealed: Literal["d"] - -reveal_type(s[False]) # revealed: Literal["a"] -reveal_type(s[True]) # revealed: Literal["b"] - -a = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5" -reveal_type(a) # revealed: Unknown - -b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5" -reveal_type(b) # revealed: Unknown - -def _(n: int): - a = "abcde"[n] - # TODO: Support overloads... Should be `str` - reveal_type(a) # revealed: @Todo(return type of overloaded function) -``` - -## Slices - -```py -def _(m: int, n: int, s2: str): - s = "abcde" - - reveal_type(s[0:0]) # revealed: Literal[""] - reveal_type(s[0:1]) # revealed: Literal["a"] - reveal_type(s[0:2]) # revealed: Literal["ab"] - reveal_type(s[0:5]) # revealed: Literal["abcde"] - reveal_type(s[0:6]) # revealed: Literal["abcde"] - reveal_type(s[1:3]) # revealed: Literal["bc"] - - reveal_type(s[-3:5]) # revealed: Literal["cde"] - reveal_type(s[-4:-2]) # revealed: Literal["bc"] - reveal_type(s[-10:10]) # revealed: Literal["abcde"] - - reveal_type(s[0:]) # revealed: Literal["abcde"] - reveal_type(s[2:]) # revealed: Literal["cde"] - reveal_type(s[5:]) # revealed: Literal[""] - reveal_type(s[:2]) # revealed: Literal["ab"] - reveal_type(s[:0]) # revealed: Literal[""] - reveal_type(s[:2]) # revealed: Literal["ab"] - reveal_type(s[:10]) # revealed: Literal["abcde"] - reveal_type(s[:]) # revealed: Literal["abcde"] - - reveal_type(s[::-1]) # revealed: Literal["edcba"] - reveal_type(s[::2]) # revealed: Literal["ace"] - reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"] - reveal_type(s[::-2]) # revealed: Literal["eca"] - reveal_type(s[-1::-3]) # revealed: Literal["eb"] - - reveal_type(s[None:2:None]) # revealed: Literal["ab"] - reveal_type(s[1:None:1]) # revealed: Literal["bcde"] - reveal_type(s[None:None:None]) # revealed: Literal["abcde"] - - start = 1 - stop = None - step = 2 - reveal_type(s[start:stop:step]) # revealed: Literal["bd"] - - reveal_type(s[False:True]) # revealed: Literal["a"] - reveal_type(s[True:3]) # revealed: Literal["bc"] - - s[0:4:0] # error: [zero-stepsize-in-slice] - s[:4:0] # error: [zero-stepsize-in-slice] - s[0::0] # error: [zero-stepsize-in-slice] - s[::0] # error: [zero-stepsize-in-slice] - - substring1 = s[m:n] - # TODO: Support overloads... Should be `LiteralString` - reveal_type(substring1) # revealed: @Todo(return type of overloaded function) - - substring2 = s2[0:5] - # TODO: Support overloads... Should be `str` - reveal_type(substring2) # revealed: @Todo(return type of overloaded function) -``` - -## Unsupported slice types - -```py -# TODO: It would be great if we raised an error here. This can be done once -# we have support for overloads and generics, and once typeshed has a more -# precise annotation for `str.__getitem__`, that makes use of the generic -# `slice[..]` type. We could then infer `slice[str, str]` here and see that -# it doesn't match the signature of `str.__getitem__`. -"foo"["bar":"baz"] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md deleted file mode 100644 index 8ac7ff2534276..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ /dev/null @@ -1,122 +0,0 @@ -# Tuple subscripts - -## Indexing - -```py -t = (1, "a", "b") - -reveal_type(t[0]) # revealed: Literal[1] -reveal_type(t[1]) # revealed: Literal["a"] -reveal_type(t[-1]) # revealed: Literal["b"] -reveal_type(t[-2]) # revealed: Literal["a"] - -reveal_type(t[False]) # revealed: Literal[1] -reveal_type(t[True]) # revealed: Literal["a"] - -a = t[4] # error: [index-out-of-bounds] -reveal_type(a) # revealed: Unknown - -b = t[-4] # error: [index-out-of-bounds] -reveal_type(b) # revealed: Unknown -``` - -## Slices - -```py -def _(m: int, n: int): - t = (1, "a", None, b"b") - - reveal_type(t[0:0]) # revealed: tuple[()] - reveal_type(t[0:1]) # revealed: tuple[Literal[1]] - reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]] - reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None] - - reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]] - reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None] - reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - - reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]] - reveal_type(t[4:]) # revealed: tuple[()] - reveal_type(t[:0]) # revealed: tuple[()] - reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]] - reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - - reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]] - reveal_type(t[::2]) # revealed: tuple[Literal[1], None] - reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]] - reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]] - reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]] - - reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]] - reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]] - reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] - - start = 1 - stop = None - step = 2 - reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]] - - reveal_type(t[False:True]) # revealed: tuple[Literal[1]] - reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None] - - t[0:4:0] # error: [zero-stepsize-in-slice] - t[:4:0] # error: [zero-stepsize-in-slice] - t[0::0] # error: [zero-stepsize-in-slice] - t[::0] # error: [zero-stepsize-in-slice] - - tuple_slice = t[m:n] - # TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` - reveal_type(tuple_slice) # revealed: @Todo(return type of overloaded function) -``` - -## Inheritance - -```toml -[environment] -python-version = "3.9" -``` - -```py -# TODO: `tuple[int, str]` is a valid base (generics) -# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -class A(tuple[int, str]): ... - -# Runtime value: `(A, tuple, object)` -# TODO: Generics -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]] -``` - -## `typing.Tuple` - -### Correspondence with `tuple` - -`typing.Tuple` can be used interchangeably with `tuple`: - -```py -from typing import Any, Tuple - -class A: ... - -def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]): - reveal_type(c) # revealed: tuple - reveal_type(d) # revealed: tuple[int, A] - reveal_type(e) # revealed: @Todo(full tuple[...] support) -``` - -### Inheritance - -Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself -is not a class. - -```py -from typing import Tuple - -class C(Tuple): ... - -# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] -reveal_type(C.__mro__) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md b/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md deleted file mode 100644 index fb8f78bd432bb..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md +++ /dev/null @@ -1,191 +0,0 @@ -# Suppressing errors with `knot: ignore` - -Type check errors can be suppressed by a `knot: ignore` comment on the same line as the violation. - -## Simple `knot: ignore` - -```py -a = 4 + test # knot: ignore -``` - -## Suppressing a specific code - -```py -a = 4 + test # knot: ignore[unresolved-reference] -``` - -## Unused suppression - -```py -test = 10 -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'" -a = test + 3 # knot: ignore[possibly-unresolved-reference] -``` - -## Unused suppression if the error codes don't match - -```py -# error: [unresolved-reference] -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'" -a = test + 3 # knot: ignore[possibly-unresolved-reference] -``` - -## Suppressed unused comment - -```py -# error: [unused-ignore-comment] -a = 10 / 2 # knot: ignore[division-by-zero] -a = 10 / 2 # knot: ignore[division-by-zero, unused-ignore-comment] -a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero] -a = 10 / 2 # knot: ignore[unused-ignore-comment] # type: ignore -a = 10 / 2 # type: ignore # knot: ignore[unused-ignore-comment] -``` - -## Unused ignore comment - -```py -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unused-ignore-comment'" -a = 10 / 0 # knot: ignore[division-by-zero, unused-ignore-comment] -``` - -## Multiple unused comments - -Today, Red Knot emits a diagnostic for every unused code. We might want to group the codes by -comment at some point in the future. - -```py -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'division-by-zero'" -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" -a = 10 / 2 # knot: ignore[division-by-zero, unresolved-reference] - -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'invalid-assignment'" -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" -a = 10 / 0 # knot: ignore[invalid-assignment, division-by-zero, unresolved-reference] -``` - -## Multiple suppressions - -```py -# fmt: off -def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # knot: ignore[fstring-type-annotation, byte-string-type-annotation] -``` - -## Can't suppress syntax errors - - - -```py -# error: [invalid-syntax] -# error: [unused-ignore-comment] -def test($): # knot: ignore - pass -``` - - - -## Can't suppress `revealed-type` diagnostics - -```py -a = 10 -# revealed: Literal[10] -# error: [unknown-rule] "Unknown rule `revealed-type`" -reveal_type(a) # knot: ignore[revealed-type] -``` - -## Extra whitespace in type ignore comments is allowed - -```py -a = 10 / 0 # knot : ignore -a = 10 / 0 # knot: ignore [ division-by-zero ] -``` - -## Whitespace is optional - -```py -# fmt: off -a = 10 / 0 #knot:ignore[division-by-zero] -``` - -## Trailing codes comma - -Trailing commas in the codes section are allowed: - -```py -a = 10 / 0 # knot: ignore[division-by-zero,] -``` - -## Invalid characters in codes - -```py -# error: [division-by-zero] -# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a alphanumeric character or `-` or `_` as code" -a = 10 / 0 # knot: ignore[*-*] -``` - -## Trailing whitespace - - - -```py -a = 10 / 0 # knot: ignore[division-by-zero] - # ^^^^^^ trailing whitespace -``` - - - -## Missing comma - -A missing comma results in an invalid suppression comment. We may want to recover from this in the -future. - -```py -# error: [unresolved-reference] -# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes" -a = x / 0 # knot: ignore[division-by-zero unresolved-reference] -``` - -## Missing closing bracket - -```py -# error: [unresolved-reference] "Name `x` used when not defined" -# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes" -a = x / 2 # knot: ignore[unresolved-reference -``` - -## Empty codes - -An empty codes array suppresses no-diagnostics and is always useless - -```py -# error: [division-by-zero] -# error: [unused-ignore-comment] "Unused `knot: ignore` without a code" -a = 4 / 0 # knot: ignore[] -``` - -## File-level suppression comments - -File level suppression comments are currently intentionally unsupported because we've yet to decide -if they should use a different syntax that also supports enabling rules or changing the rule's -severity: `knot: possibly-undefined-reference=error` - -```py -# error: [unused-ignore-comment] -# knot: ignore[division-by-zero] - -a = 4 / 0 # error: [division-by-zero] -``` - -## Unknown rule - -```py -# error: [unknown-rule] "Unknown rule `is-equal-14`" -a = 10 + 4 # knot: ignore[is-equal-14] -``` - -## Code with `lint:` prefix - -```py -# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?" -# error: [division-by-zero] -a = 10 / 0 # knot: ignore[lint:division-by-zero] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md deleted file mode 100644 index 1e5f4bdc5b258..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md +++ /dev/null @@ -1,176 +0,0 @@ -# type special form - -## Class literal - -```py -class A: ... - -def _(c: type[A]): - reveal_type(c) # revealed: type[A] -``` - -## Nested class literal - -```py -class A: - class B: ... - -def f(c: type[A.B]): - reveal_type(c) # revealed: type[B] -``` - -## Deeply nested class literal - -```py -class A: - class B: - class C: ... - -def f(c: type[A.B.C]): - reveal_type(c) # revealed: type[C] -``` - -## Class literal from another module - -```py -from a import A - -def f(c: type[A]): - reveal_type(c) # revealed: type[A] -``` - -`a.py`: - -```py -class A: ... -``` - -## Qualified class literal from another module - -```py -import a - -def f(c: type[a.B]): - reveal_type(c) # revealed: type[B] -``` - -`a.py`: - -```py -class B: ... -``` - -## Deeply qualified class literal from another module - -`a/test.py`: - -```py -import a.b - -def f(c: type[a.b.C]): - reveal_type(c) # revealed: type[C] -``` - -`a/__init__.py`: - -```py -``` - -`a/b.py`: - -```py -class C: ... -``` - -## New-style union of classes - -```py -class BasicUser: ... -class ProUser: ... - -class A: - class B: - class C: ... - -def _(u: type[BasicUser | ProUser | A.B.C]): - # revealed: type[BasicUser] | type[ProUser] | type[C] - reveal_type(u) -``` - -## Old-style union of classes - -```py -from typing import Union - -class BasicUser: ... -class ProUser: ... - -class A: - class B: - class C: ... - -def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]): - reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] - reveal_type(b) # revealed: type[str] - reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C] -``` - -## New-style and old-style unions in combination - -```py -from typing import Union - -class BasicUser: ... -class ProUser: ... - -class A: - class B: - class C: ... - -def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]): - reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] - reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str] -``` - -## Illegal parameters - -```py -class A: ... -class B: ... - -# error: [invalid-type-form] -_: type[A, B] -``` - -## As a base class - -```py -# TODO: this is a false positive -# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)" -class Foo(type[int]): ... - -# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]] -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] -``` - -## `@final` classes - -`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is -used as the type argument. This applies to standard-library classes and user-defined classes: - -```toml -[environment] -python-version = "3.10" -``` - -```py -from types import EllipsisType -from typing import final - -@final -class Foo: ... - -def _(x: type[Foo], y: type[EllipsisType]): - reveal_type(x) # revealed: Literal[Foo] - reveal_type(y) # revealed: Literal[EllipsisType] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md deleted file mode 100644 index 5ca7c9c6284e5..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ /dev/null @@ -1,525 +0,0 @@ -# Assignable-to relation - -The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target). -This allows us to check if a type `S` can be used in a context where a type `T` is expected -(function arguments, variable assignments). See the [typing documentation] for a precise definition -of this concept. - -## Basic types - -### Fully static - -Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be -assignable to `T`. Two equivalent types are subtypes of each other: - -```py -from knot_extensions import static_assert, is_assignable_to - -class Parent: ... -class Child1(Parent): ... -class Child2(Parent): ... -class Grandchild(Child1, Child2): ... -class Unrelated: ... - -static_assert(is_assignable_to(int, int)) -static_assert(is_assignable_to(Parent, Parent)) -static_assert(is_assignable_to(Child1, Parent)) -static_assert(is_assignable_to(Grandchild, Parent)) -static_assert(is_assignable_to(Unrelated, Unrelated)) - -static_assert(not is_assignable_to(str, int)) -static_assert(not is_assignable_to(object, int)) -static_assert(not is_assignable_to(Parent, Child1)) -static_assert(not is_assignable_to(Unrelated, Parent)) -static_assert(not is_assignable_to(Child1, Child2)) -``` - -### Gradual types - -Gradual types do not participate in subtyping, but can still be assignable to other types (and -static types can be assignable to gradual types): - -```py -from knot_extensions import static_assert, is_assignable_to, Unknown -from typing import Any, Literal - -static_assert(is_assignable_to(Unknown, Literal[1])) -static_assert(is_assignable_to(Any, Literal[1])) -static_assert(is_assignable_to(Literal[1], Unknown)) -static_assert(is_assignable_to(Literal[1], Any)) -``` - -## Literal types - -### Boolean literals - -`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`, -which is in turn a subtype of `int`: - -```py -from knot_extensions import static_assert, is_assignable_to -from typing import Literal - -static_assert(is_assignable_to(Literal[True], Literal[True])) -static_assert(is_assignable_to(Literal[True], bool)) -static_assert(is_assignable_to(Literal[True], int)) - -static_assert(not is_assignable_to(Literal[True], Literal[False])) -static_assert(not is_assignable_to(bool, Literal[True])) -``` - -### Integer literals - -```py -from knot_extensions import static_assert, is_assignable_to -from typing import Literal - -static_assert(is_assignable_to(Literal[1], Literal[1])) -static_assert(is_assignable_to(Literal[1], int)) - -static_assert(not is_assignable_to(Literal[1], Literal[2])) -static_assert(not is_assignable_to(int, Literal[1])) -static_assert(not is_assignable_to(Literal[1], str)) -``` - -### String literals and `LiteralString` - -All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in -turn a subtype of `str`: - -```py -from knot_extensions import static_assert, is_assignable_to -from typing_extensions import Literal, LiteralString - -static_assert(is_assignable_to(Literal["foo"], Literal["foo"])) -static_assert(is_assignable_to(Literal["foo"], LiteralString)) -static_assert(is_assignable_to(Literal["foo"], str)) - -static_assert(is_assignable_to(LiteralString, str)) - -static_assert(not is_assignable_to(Literal["foo"], Literal["bar"])) -static_assert(not is_assignable_to(str, Literal["foo"])) -static_assert(not is_assignable_to(str, LiteralString)) -``` - -### Byte literals - -```py -from knot_extensions import static_assert, is_assignable_to -from typing_extensions import Literal, LiteralString - -static_assert(is_assignable_to(Literal[b"foo"], bytes)) -static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"])) - -static_assert(not is_assignable_to(Literal[b"foo"], str)) -static_assert(not is_assignable_to(Literal[b"foo"], LiteralString)) -static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"])) -static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"])) -static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"])) -``` - -## `type[…]` and class literals - -In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`. -This contrasts with `type[str]`, which represents "all possible subclasses of `str`". - -Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent -"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to -`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which -is known to be no larger than the set of possible objects represented by `type`. - -```py -from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf -from typing import Any - -static_assert(is_assignable_to(type, type)) -static_assert(is_assignable_to(type[object], type[object])) - -static_assert(is_assignable_to(type, type[object])) -static_assert(is_assignable_to(type[object], type)) - -static_assert(is_assignable_to(type[str], type[object])) -static_assert(is_assignable_to(TypeOf[str], type[object])) -static_assert(is_assignable_to(type[str], type)) -static_assert(is_assignable_to(TypeOf[str], type)) - -static_assert(is_assignable_to(type[str], type[str])) -static_assert(is_assignable_to(TypeOf[str], type[str])) - -static_assert(not is_assignable_to(TypeOf[int], type[str])) -static_assert(not is_assignable_to(type, type[str])) -static_assert(not is_assignable_to(type[object], type[str])) - -static_assert(is_assignable_to(type[Any], type[Any])) -static_assert(is_assignable_to(type[Any], type[object])) -static_assert(is_assignable_to(type[object], type[Any])) -static_assert(is_assignable_to(type, type[Any])) -static_assert(is_assignable_to(type[Any], type[str])) -static_assert(is_assignable_to(type[str], type[Any])) -static_assert(is_assignable_to(TypeOf[str], type[Any])) - -static_assert(is_assignable_to(type[Unknown], type[Unknown])) -static_assert(is_assignable_to(type[Unknown], type[object])) -static_assert(is_assignable_to(type[object], type[Unknown])) -static_assert(is_assignable_to(type, type[Unknown])) -static_assert(is_assignable_to(type[Unknown], type[str])) -static_assert(is_assignable_to(type[str], type[Unknown])) -static_assert(is_assignable_to(TypeOf[str], type[Unknown])) - -static_assert(is_assignable_to(type[Unknown], type[Any])) -static_assert(is_assignable_to(type[Any], type[Unknown])) - -static_assert(not is_assignable_to(object, type[Any])) -static_assert(not is_assignable_to(str, type[Any])) - -class Meta(type): ... - -static_assert(is_assignable_to(type[Any], Meta)) -static_assert(is_assignable_to(type[Unknown], Meta)) -static_assert(is_assignable_to(Meta, type[Any])) -static_assert(is_assignable_to(Meta, type[Unknown])) -``` - -## Tuple types - -```py -from knot_extensions import static_assert, is_assignable_to, AlwaysTruthy, AlwaysFalsy -from typing import Literal, Any - -static_assert(is_assignable_to(tuple[()], tuple[()])) -static_assert(is_assignable_to(tuple[int], tuple[int])) -static_assert(is_assignable_to(tuple[int], tuple[Any])) -static_assert(is_assignable_to(tuple[Any], tuple[int])) -static_assert(is_assignable_to(tuple[int, str], tuple[int, str])) -static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int])) -static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int])) -static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int])) -static_assert(is_assignable_to(tuple[()], tuple)) -static_assert(is_assignable_to(tuple[int, str], tuple)) -static_assert(is_assignable_to(tuple[Any], tuple)) - -# TODO: It is not yet clear if we want the following two assertions to hold. -# See https://github.com/astral-sh/ruff/issues/15528 for more details. The -# short version is: We either need to special-case enforcement of the Liskov -# substitution principle on `__bool__` and `__len__` for tuple subclasses, -# or we need to negate these assertions. -static_assert(is_assignable_to(tuple[()], AlwaysFalsy)) -static_assert(is_assignable_to(tuple[int], AlwaysTruthy)) - -static_assert(not is_assignable_to(tuple[()], tuple[int])) -static_assert(not is_assignable_to(tuple[int], tuple[str])) -static_assert(not is_assignable_to(tuple[int], tuple[int, str])) -static_assert(not is_assignable_to(tuple[int, str], tuple[int])) -static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int])) -static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str])) -``` - -## Union types - -```py -from knot_extensions import AlwaysTruthy, AlwaysFalsy, static_assert, is_assignable_to, Unknown -from typing_extensions import Literal, Any, LiteralString - -static_assert(is_assignable_to(int, int | str)) -static_assert(is_assignable_to(str, int | str)) -static_assert(is_assignable_to(int | str, int | str)) -static_assert(is_assignable_to(str | int, int | str)) -static_assert(is_assignable_to(Literal[1], int | str)) -static_assert(is_assignable_to(Literal[1], Unknown | str)) -static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2])) -static_assert(is_assignable_to(Literal[1] | Literal[2], int)) -static_assert(is_assignable_to(Literal[1] | None, int | None)) -static_assert(is_assignable_to(Any, int | str)) -static_assert(is_assignable_to(Any | int, int)) -static_assert(is_assignable_to(str, int | Any)) - -static_assert(not is_assignable_to(int | None, int)) -static_assert(not is_assignable_to(int | None, str | None)) -static_assert(not is_assignable_to(Literal[1] | None, int)) -static_assert(not is_assignable_to(Literal[1] | None, str | None)) -static_assert(not is_assignable_to(Any | int | str, int)) - -# TODO: No errors -# error: [static-assert-error] -static_assert(is_assignable_to(bool, Literal[False] | AlwaysTruthy)) -# error: [static-assert-error] -static_assert(is_assignable_to(bool, Literal[True] | AlwaysFalsy)) -# error: [static-assert-error] -static_assert(is_assignable_to(LiteralString, Literal[""] | AlwaysTruthy)) -static_assert(not is_assignable_to(Literal[True] | AlwaysFalsy, Literal[False] | AlwaysTruthy)) -``` - -## Intersection types - -```py -from knot_extensions import static_assert, is_assignable_to, Intersection, Not, AlwaysTruthy, AlwaysFalsy -from typing_extensions import Any, Literal, final, LiteralString - -class Parent: ... -class Child1(Parent): ... -class Child2(Parent): ... -class Grandchild(Child1, Child2): ... -class Unrelated: ... - -static_assert(is_assignable_to(Intersection[Child1, Child2], Child1)) -static_assert(is_assignable_to(Intersection[Child1, Child2], Child2)) -static_assert(is_assignable_to(Intersection[Child1, Child2], Parent)) -static_assert(is_assignable_to(Intersection[Child1, Parent], Parent)) - -static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent)) -static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1)) -static_assert(is_assignable_to(Intersection[Child1, Unrelated, Child2], Intersection[Child1, Unrelated])) - -static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1)) -static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent)) -static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent)) - -static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2])) -static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1])) -static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2])) -static_assert(not is_assignable_to(Intersection[Child1, Child2], Intersection[Parent, Unrelated])) - -static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated])) -static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]])) -# The literal `1` is not assignable to `Parent`, so the intersection of int and Parent is definitely an int that is not `1` -static_assert(is_assignable_to(Intersection[int, Parent], Intersection[int, Not[Literal[1]]])) -static_assert(not is_assignable_to(int, Not[int])) -static_assert(not is_assignable_to(int, Not[Literal[1]])) - -static_assert(is_assignable_to(Not[Parent], Not[Child1])) -static_assert(not is_assignable_to(Not[Parent], Parent)) -static_assert(not is_assignable_to(Intersection[Unrelated, Not[Parent]], Parent)) - -# Intersection with `Any` dominates the left hand side of intersections -static_assert(is_assignable_to(Intersection[Any, Parent], Parent)) -static_assert(is_assignable_to(Intersection[Any, Child1], Parent)) -static_assert(is_assignable_to(Intersection[Any, Child2, Not[Child1]], Parent)) -static_assert(is_assignable_to(Intersection[Any, Parent], Unrelated)) -static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Parent, Unrelated])) -static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Parent)) -static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Parent, Unrelated])) - -# Even Any & Not[Parent] is assignable to Parent, since it could be Never -static_assert(is_assignable_to(Intersection[Any, Not[Parent]], Parent)) -static_assert(is_assignable_to(Intersection[Any, Not[Parent]], Not[Parent])) - -# Intersection with `Any` is effectively ignored on the right hand side for the sake of assignment -static_assert(is_assignable_to(Parent, Intersection[Any, Parent])) -static_assert(is_assignable_to(Parent, Parent | Intersection[Any, Unrelated])) -static_assert(is_assignable_to(Child1, Intersection[Any, Parent])) -static_assert(not is_assignable_to(Literal[1], Intersection[Any, Parent])) -static_assert(not is_assignable_to(Unrelated, Intersection[Any, Parent])) - -# Intersections with Any on both sides combine the above logic - the LHS dominates and Any is ignored on the right hand side -static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Any, Parent])) -static_assert(is_assignable_to(Intersection[Any, Unrelated], Intersection[Any, Parent])) -static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Any, Parent, Unrelated])) -static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]])) -static_assert(is_assignable_to(Intersection[Literal[1], Any], Intersection[Unrelated, Not[Any]])) - -# TODO: No errors -# The condition `is_assignable_to(T & U, U)` should still be satisfied after the following transformations: -# `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` -# error: [static-assert-error] -static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal[""]]], AlwaysTruthy)) -# error: [static-assert-error] -static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal["", "a"]]], AlwaysTruthy)) -# `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` -# error: [static-assert-error] -static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy])) -# error: [static-assert-error] -static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy])) -``` - -## General properties - -See also: our property tests in `property_tests.rs`. - -### Everything is assignable to `object` - -`object` is Python's top type; the set of all possible objects at runtime: - -```py -from knot_extensions import static_assert, is_assignable_to, Unknown -from typing import Literal, Any - -static_assert(is_assignable_to(str, object)) -static_assert(is_assignable_to(Literal[1], object)) -static_assert(is_assignable_to(object, object)) -static_assert(is_assignable_to(type, object)) -static_assert(is_assignable_to(Any, object)) -static_assert(is_assignable_to(Unknown, object)) -static_assert(is_assignable_to(type[object], object)) -static_assert(is_assignable_to(type[str], object)) -static_assert(is_assignable_to(type[Any], object)) -``` - -### Every type is assignable to `Any` / `Unknown` - -`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so -any type is assignable to them: - -```py -from knot_extensions import static_assert, is_assignable_to, Unknown -from typing import Literal, Any - -static_assert(is_assignable_to(str, Any)) -static_assert(is_assignable_to(Literal[1], Any)) -static_assert(is_assignable_to(object, Any)) -static_assert(is_assignable_to(type, Any)) -static_assert(is_assignable_to(Any, Any)) -static_assert(is_assignable_to(Unknown, Any)) -static_assert(is_assignable_to(type[object], Any)) -static_assert(is_assignable_to(type[str], Any)) -static_assert(is_assignable_to(type[Any], Any)) - -static_assert(is_assignable_to(str, Unknown)) -static_assert(is_assignable_to(Literal[1], Unknown)) -static_assert(is_assignable_to(object, Unknown)) -static_assert(is_assignable_to(type, Unknown)) -static_assert(is_assignable_to(Any, Unknown)) -static_assert(is_assignable_to(Unknown, Unknown)) -static_assert(is_assignable_to(type[object], Unknown)) -static_assert(is_assignable_to(type[str], Unknown)) -static_assert(is_assignable_to(type[Any], Unknown)) -``` - -### `Never` is assignable to every type - -`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore -assignable to any arbitrary type. - -```py -from knot_extensions import static_assert, is_assignable_to, Unknown -from typing_extensions import Never, Any, Literal - -static_assert(is_assignable_to(Never, str)) -static_assert(is_assignable_to(Never, Literal[1])) -static_assert(is_assignable_to(Never, object)) -static_assert(is_assignable_to(Never, type)) -static_assert(is_assignable_to(Never, Any)) -static_assert(is_assignable_to(Never, Unknown)) -static_assert(is_assignable_to(Never, type[object])) -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 CallableTypeOf, 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 CallableTypeOf, 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(CallableTypeOf[positional_only], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[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(CallableTypeOf[positional_only], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None])) -static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None])) -``` - -### Function types - -```py -from typing import Any, Callable - -def f(x: Any) -> str: - return "" - -def g(x: Any) -> int: - return 1 - -c: Callable[[Any], str] = f - -# error: [invalid-assignment] "Object of type `def g(x: Any) -> int` is not assignable to `(Any, /) -> str`" -c: Callable[[Any], str] = g -``` - -### Method types - -```py -from typing import Any, Callable - -class A: - def f(self, x: Any) -> str: - return "" - - def g(self, x: Any) -> int: - return 1 - -c: Callable[[Any], str] = A().f - -# error: [invalid-assignment] "Object of type `bound method A.g(x: Any) -> int` is not assignable to `(Any, /) -> str`" -c: Callable[[Any], str] = A().g -``` - -[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md deleted file mode 100644 index fb5fe478cfbb1..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ /dev/null @@ -1,393 +0,0 @@ -# Disjointness relation - -Two types `S` and `T` are disjoint if their intersection `S & T` is empty (equivalent to `Never`). -This means that it is known that no possible runtime object inhabits both types simultaneously. - -## Basic builtin types - -```py -from typing_extensions import Literal, LiteralString, Any -from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert - -static_assert(is_disjoint_from(bool, str)) -static_assert(not is_disjoint_from(bool, bool)) -static_assert(not is_disjoint_from(bool, int)) -static_assert(not is_disjoint_from(bool, object)) - -static_assert(not is_disjoint_from(Any, bool)) -static_assert(not is_disjoint_from(Any, Any)) -static_assert(not is_disjoint_from(Any, Not[Any])) - -static_assert(not is_disjoint_from(LiteralString, LiteralString)) -static_assert(not is_disjoint_from(str, LiteralString)) -static_assert(not is_disjoint_from(str, type)) -static_assert(not is_disjoint_from(str, type[Any])) -``` - -## Class hierarchies - -```py -from knot_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of -from typing import final - -class A: ... -class B1(A): ... -class B2(A): ... - -# B1 and B2 are subclasses of A, so they are not disjoint from A: -static_assert(not is_disjoint_from(A, B1)) -static_assert(not is_disjoint_from(A, B2)) - -# The two subclasses B1 and B2 are also not disjoint ... -static_assert(not is_disjoint_from(B1, B2)) - -# ... because they could share a common subclass ... -class C(B1, B2): ... - -# ... which lies in their intersection: -static_assert(is_subtype_of(C, Intersection[B1, B2])) - -# However, if a class is marked final, it can not be subclassed ... -@final -class FinalSubclass(A): ... - -static_assert(not is_disjoint_from(FinalSubclass, A)) - -# ... which makes it disjoint from B1, B2: -static_assert(is_disjoint_from(B1, FinalSubclass)) -static_assert(is_disjoint_from(B2, FinalSubclass)) -``` - -## Tuple types - -```py -from typing_extensions import Literal, Never -from knot_extensions import TypeOf, is_disjoint_from, static_assert - -static_assert(is_disjoint_from(tuple[()], TypeOf[object])) -static_assert(is_disjoint_from(tuple[()], TypeOf[Literal])) - -static_assert(is_disjoint_from(tuple[None], None)) -static_assert(is_disjoint_from(tuple[None], Literal[b"a"])) -static_assert(is_disjoint_from(tuple[None], Literal["a"])) -static_assert(is_disjoint_from(tuple[None], Literal[1])) -static_assert(is_disjoint_from(tuple[None], Literal[True])) - -static_assert(is_disjoint_from(tuple[Literal[1]], tuple[Literal[2]])) -static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1]])) -static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[3]])) - -static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], int])) -``` - -## Unions - -```py -from typing_extensions import Literal -from knot_extensions import Intersection, is_disjoint_from, static_assert - -static_assert(is_disjoint_from(Literal[1, 2], Literal[3])) -static_assert(is_disjoint_from(Literal[1, 2], Literal[3, 4])) - -static_assert(not is_disjoint_from(Literal[1, 2], Literal[2])) -static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) -``` - -## Intersections - -```py -from typing_extensions import Literal, final, Any -from knot_extensions import Intersection, is_disjoint_from, static_assert, Not - -@final -class P: ... - -@final -class Q: ... - -@final -class R: ... - -# For three pairwise disjoint classes ... -static_assert(is_disjoint_from(P, Q)) -static_assert(is_disjoint_from(P, R)) -static_assert(is_disjoint_from(Q, R)) - -# ... their intersections are also disjoint: -static_assert(is_disjoint_from(Intersection[P, Q], R)) -static_assert(is_disjoint_from(Intersection[P, R], Q)) -static_assert(is_disjoint_from(Intersection[Q, R], P)) - -# On the other hand, for non-disjoint classes ... -class X: ... -class Y: ... -class Z: ... - -static_assert(not is_disjoint_from(X, Y)) -static_assert(not is_disjoint_from(X, Z)) -static_assert(not is_disjoint_from(Y, Z)) - -# ... their intersections are also not disjoint: -static_assert(not is_disjoint_from(Intersection[X, Y], Z)) -static_assert(not is_disjoint_from(Intersection[X, Z], Y)) -static_assert(not is_disjoint_from(Intersection[Y, Z], X)) - -# If one side has a positive fully-static element and the other side has a negative of that element, they are disjoint -static_assert(is_disjoint_from(int, Not[int])) -static_assert(is_disjoint_from(Intersection[X, Y, Not[Z]], Intersection[X, Z])) -static_assert(is_disjoint_from(Intersection[X, Not[Literal[1]]], Literal[1])) - -class Parent: ... -class Child(Parent): ... - -static_assert(not is_disjoint_from(Parent, Child)) -static_assert(not is_disjoint_from(Parent, Not[Child])) -static_assert(not is_disjoint_from(Not[Parent], Not[Child])) -static_assert(is_disjoint_from(Not[Parent], Child)) -static_assert(is_disjoint_from(Intersection[X, Not[Parent]], Child)) -static_assert(is_disjoint_from(Intersection[X, Not[Parent]], Intersection[X, Child])) - -static_assert(not is_disjoint_from(Intersection[Any, X], Intersection[Any, Not[Y]])) -static_assert(not is_disjoint_from(Intersection[Any, Not[Y]], Intersection[Any, X])) - -static_assert(is_disjoint_from(Intersection[int, Any], Not[int])) -static_assert(is_disjoint_from(Not[int], Intersection[int, Any])) -``` - -## Special types - -### `Never` - -`Never` is disjoint from every type, including itself. - -```py -from typing_extensions import Never -from knot_extensions import is_disjoint_from, static_assert - -static_assert(is_disjoint_from(Never, Never)) -static_assert(is_disjoint_from(Never, None)) -static_assert(is_disjoint_from(Never, int)) -static_assert(is_disjoint_from(Never, object)) -``` - -### `None` - -```py -from typing_extensions import Literal, LiteralString -from knot_extensions import is_disjoint_from, static_assert, Intersection, Not - -static_assert(is_disjoint_from(None, Literal[True])) -static_assert(is_disjoint_from(None, Literal[1])) -static_assert(is_disjoint_from(None, Literal["test"])) -static_assert(is_disjoint_from(None, Literal[b"test"])) -static_assert(is_disjoint_from(None, LiteralString)) -static_assert(is_disjoint_from(None, int)) -static_assert(is_disjoint_from(None, type[object])) - -static_assert(not is_disjoint_from(None, None)) -static_assert(not is_disjoint_from(None, int | None)) -static_assert(not is_disjoint_from(None, object)) - -static_assert(is_disjoint_from(Intersection[int, Not[str]], None)) -static_assert(is_disjoint_from(None, Intersection[int, Not[str]])) -``` - -### Literals - -```py -from typing_extensions import Literal, LiteralString -from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert, AlwaysFalsy, AlwaysTruthy - -static_assert(is_disjoint_from(Literal[True], Literal[False])) -static_assert(is_disjoint_from(Literal[True], Literal[1])) -static_assert(is_disjoint_from(Literal[False], Literal[0])) - -static_assert(is_disjoint_from(Literal[1], Literal[2])) - -static_assert(is_disjoint_from(Literal["a"], Literal["b"])) - -static_assert(is_disjoint_from(Literal[b"a"], LiteralString)) -static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"])) -static_assert(is_disjoint_from(Literal[b"a"], Literal["a"])) - -static_assert(is_disjoint_from(type[object], TypeOf[Literal])) -static_assert(is_disjoint_from(type[str], LiteralString)) - -static_assert(not is_disjoint_from(Literal[True], Literal[True])) -static_assert(not is_disjoint_from(Literal[False], Literal[False])) -static_assert(not is_disjoint_from(Literal[True], bool)) -static_assert(not is_disjoint_from(Literal[True], int)) - -static_assert(not is_disjoint_from(Literal[1], Literal[1])) - -static_assert(not is_disjoint_from(Literal["a"], Literal["a"])) -static_assert(not is_disjoint_from(Literal["a"], LiteralString)) -static_assert(not is_disjoint_from(Literal["a"], str)) - -# TODO: No errors -# error: [static-assert-error] -static_assert(is_disjoint_from(AlwaysFalsy, Intersection[LiteralString, Not[Literal[""]]])) -# error: [static-assert-error] -static_assert(is_disjoint_from(Intersection[Not[Literal[True]], Not[Literal[False]]], bool)) -# error: [static-assert-error] -static_assert(is_disjoint_from(Intersection[AlwaysFalsy, Not[Literal[False]]], bool)) -# error: [static-assert-error] -static_assert(is_disjoint_from(Intersection[AlwaysTruthy, Not[Literal[True]]], bool)) - -# TODO: No errors -# The condition `is_disjoint(T, Not[T])` must still be satisfied after the following transformations: -# `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` -# error: [static-assert-error] -static_assert(is_disjoint_from(Intersection[LiteralString, AlwaysTruthy], Not[LiteralString] | AlwaysFalsy)) -# `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` -# error: [static-assert-error] -static_assert(is_disjoint_from(Intersection[LiteralString, Not[AlwaysFalsy]], Not[LiteralString] | AlwaysFalsy)) -``` - -### Class, module and function literals - -```py -from types import ModuleType, FunctionType -from knot_extensions import TypeOf, is_disjoint_from, static_assert - -class A: ... -class B: ... - -type LiteralA = TypeOf[A] -type LiteralB = TypeOf[B] - -# Class literals for different classes are always disjoint. -# They are singleton types that only contain the class object itself. -static_assert(is_disjoint_from(LiteralA, LiteralB)) - -# The class A is a subclass of A, so A is not disjoint from type[A]: -static_assert(not is_disjoint_from(LiteralA, type[A])) - -# The class A is disjoint from type[B] because it's not a subclass of B: -static_assert(is_disjoint_from(LiteralA, type[B])) - -# However, type[A] is not disjoint from type[B], as there could be -# classes that inherit from both A and B: -static_assert(not is_disjoint_from(type[A], type[B])) - -import random -import math - -static_assert(is_disjoint_from(TypeOf[random], TypeOf[math])) -static_assert(not is_disjoint_from(TypeOf[random], ModuleType)) -static_assert(not is_disjoint_from(TypeOf[random], object)) - -def f(): ... -def g(): ... - -static_assert(is_disjoint_from(TypeOf[f], TypeOf[g])) -static_assert(not is_disjoint_from(TypeOf[f], FunctionType)) -static_assert(not is_disjoint_from(TypeOf[f], object)) -``` - -### `AlwaysTruthy` and `AlwaysFalsy` - -```py -from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert -from typing import Literal - -static_assert(is_disjoint_from(None, AlwaysTruthy)) -static_assert(not is_disjoint_from(None, AlwaysFalsy)) - -static_assert(is_disjoint_from(AlwaysFalsy, AlwaysTruthy)) -static_assert(not is_disjoint_from(str, AlwaysFalsy)) -static_assert(not is_disjoint_from(str, AlwaysTruthy)) - -static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy)) -static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy)) -``` - -### Instance types versus `type[T]` types - -An instance type is disjoint from a `type[T]` type if the instance type is `@final` and the class of -the instance type is not a subclass of `T`'s metaclass. - -```py -from typing import final -from knot_extensions import is_disjoint_from, static_assert - -@final -class Foo: ... - -static_assert(is_disjoint_from(Foo, type[int])) -static_assert(is_disjoint_from(type[object], Foo)) -static_assert(is_disjoint_from(type[dict], Foo)) - -# Instance types can be disjoint from `type[]` types -# even if the instance type is a subtype of `type` - -@final -class Meta1(type): ... - -class UsesMeta1(metaclass=Meta1): ... - -static_assert(not is_disjoint_from(Meta1, type[UsesMeta1])) - -class Meta2(type): ... -class UsesMeta2(metaclass=Meta2): ... - -static_assert(not is_disjoint_from(Meta2, type[UsesMeta2])) -static_assert(is_disjoint_from(Meta1, type[UsesMeta2])) -``` - -### `type[T]` versus `type[S]` - -By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the -metaclass of `S`. - -```py -from typing import final -from knot_extensions import static_assert, is_disjoint_from - -@final -class Meta1(type): ... - -class Meta2(type): ... -class UsesMeta1(metaclass=Meta1): ... -class UsesMeta2(metaclass=Meta2): ... - -static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2])) -``` - -## Callables - -No two callable types are disjoint because there exists a non-empty callable type -`(*args: object, **kwargs: object) -> Never` that is a subtype of all fully static callable types. -As such, for any two callable types, it is possible to conceive of a runtime callable object that -would inhabit both types simultaneously. - -```py -from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert -from typing_extensions import Callable, Literal, Never - -def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ... - -static_assert(not is_disjoint_from(Callable[[], Never], CallableTypeOf[mixed])) -static_assert(not is_disjoint_from(Callable[[int, str], float], CallableTypeOf[mixed])) - -# Using gradual form -static_assert(not is_disjoint_from(Callable[..., None], Callable[[], None])) -static_assert(not is_disjoint_from(Callable[..., None], Callable[..., None])) -static_assert(not is_disjoint_from(Callable[..., None], Callable[[Literal[1]], None])) - -# Using `Never` -static_assert(not is_disjoint_from(Callable[[], Never], Callable[[], Never])) -static_assert(not is_disjoint_from(Callable[[Never], str], Callable[[Never], int])) -``` - -A callable type is disjoint from all literal types. - -```py -from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert -from typing_extensions import Callable, Literal, Never - -static_assert(is_disjoint_from(Callable[[], None], Literal[""])) -static_assert(is_disjoint_from(Callable[[], None], Literal[b""])) -static_assert(is_disjoint_from(Callable[[], None], Literal[1])) -static_assert(is_disjoint_from(Callable[[], None], Literal[True])) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md deleted file mode 100644 index 39aa18fc43286..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ /dev/null @@ -1,257 +0,0 @@ -# Equivalence relation - -`is_equivalent_to` implements [the equivalence relation] for fully static types. - -Two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is a subtype of `A`. - -## Basic - -```py -from typing import Any -from typing_extensions import Literal -from knot_extensions import Unknown, is_equivalent_to, static_assert - -static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2])) -static_assert(is_equivalent_to(type[object], type)) - -static_assert(not is_equivalent_to(Any, Any)) -static_assert(not is_equivalent_to(Unknown, Unknown)) -static_assert(not is_equivalent_to(Any, None)) -static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0])) -static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3])) -``` - -## Equivalence is commutative - -```py -from typing_extensions import Literal -from knot_extensions import is_equivalent_to, static_assert - -static_assert(is_equivalent_to(type, type[object])) -static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2])) -static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2])) -``` - -## Differently ordered intersections and unions are equivalent - -```py -from knot_extensions import is_equivalent_to, static_assert, Intersection, Not - -class P: ... -class Q: ... -class R: ... -class S: ... - -static_assert(is_equivalent_to(P | Q | R, P | R | Q)) # 1 -static_assert(is_equivalent_to(P | Q | R, Q | P | R)) # 2 -static_assert(is_equivalent_to(P | Q | R, Q | R | P)) # 3 -static_assert(is_equivalent_to(P | Q | R, R | P | Q)) # 4 -static_assert(is_equivalent_to(P | Q | R, R | Q | P)) # 5 -static_assert(is_equivalent_to(P | R | Q, Q | P | R)) # 6 -static_assert(is_equivalent_to(P | R | Q, Q | R | P)) # 7 -static_assert(is_equivalent_to(P | R | Q, R | P | Q)) # 8 -static_assert(is_equivalent_to(P | R | Q, R | Q | P)) # 9 -static_assert(is_equivalent_to(Q | P | R, Q | R | P)) # 10 -static_assert(is_equivalent_to(Q | P | R, R | P | Q)) # 11 -static_assert(is_equivalent_to(Q | P | R, R | Q | P)) # 12 -static_assert(is_equivalent_to(Q | R | P, R | P | Q)) # 13 -static_assert(is_equivalent_to(Q | R | P, R | Q | P)) # 14 -static_assert(is_equivalent_to(R | P | Q, R | Q | P)) # 15 - -static_assert(is_equivalent_to(str | None, None | str)) - -static_assert(is_equivalent_to(Intersection[P, Q], Intersection[Q, P])) -static_assert(is_equivalent_to(Intersection[Q, Not[P]], Intersection[Not[P], Q])) -static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P], R, Q])) -static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q])) -``` - -## Tuples containing equivalent but differently ordered unions/intersections are equivalent - -```py -from knot_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not -from typing import Literal - -class P: ... -class Q: ... -class R: ... -class S: ... - -static_assert(is_equivalent_to(tuple[P | Q], tuple[Q | P])) -static_assert(is_equivalent_to(tuple[P | None], tuple[None | P])) -static_assert( - is_equivalent_to(tuple[Intersection[P, Q] | Intersection[R, Not[S]]], tuple[Intersection[Not[S], R] | Intersection[Q, P]]) -) -``` - -## Unions containing tuples containing tuples containing unions (etc.) - -```py -from knot_extensions import is_equivalent_to, static_assert, Intersection - -class P: ... -class Q: ... - -static_assert( - is_equivalent_to( - tuple[tuple[tuple[P | Q]]] | P, - tuple[tuple[tuple[Q | P]]] | P, - ) -) -static_assert( - is_equivalent_to( - tuple[tuple[tuple[tuple[tuple[Intersection[P, Q]]]]]], - tuple[tuple[tuple[tuple[tuple[Intersection[Q, P]]]]]], - ) -) -``` - -## Intersections containing tuples containing unions - -```py -from knot_extensions import is_equivalent_to, static_assert, Intersection - -class P: ... -class Q: ... -class R: ... - -static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R])) -``` - -## Callable - -### Equivalent - -For an equivalence relationship, the default value does not necessarily need to be the same but if -the parameter in one of the callable has a default value then the corresponding parameter in the -other callable should also have a default value. - -```py -from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert -from typing import Callable - -def f1(a: int = 1) -> None: ... -def f2(a: int = 2) -> None: ... - -static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) -static_assert(is_equivalent_to(CallableTypeOf[f1] | bool | CallableTypeOf[f2], CallableTypeOf[f2] | bool | CallableTypeOf[f1])) -``` - -The names of the positional-only, variadic and keyword-variadic parameters does not need to be the -same. - -```py -def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ... -def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ... - -static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) -static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])) -``` - -Putting it all together, the following two callables are equivalent: - -```py -def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ... -def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ... - -static_assert(is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6])) -static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], CallableTypeOf[f6] | bool | CallableTypeOf[f5])) -``` - -### Not equivalent - -There are multiple cases when two callable types are not equivalent which are enumerated below. - -```py -from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert -from typing import Callable -``` - -When the number of parameters is different: - -```py -def f1(a: int) -> None: ... -def f2(a: int, b: int) -> None: ... - -static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) -``` - -When either of the callable types uses a gradual form for the parameters: - -```py -static_assert(not is_equivalent_to(Callable[..., None], Callable[[int], None])) -static_assert(not is_equivalent_to(Callable[[int], None], Callable[..., None])) -``` - -When the return types are not equivalent or absent in one or both of the callable types: - -```py -def f3(): ... -def f4() -> None: ... - -static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None])) -static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3])) -static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) -static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3])) -``` - -When the parameter names are different: - -```py -def f5(a: int) -> None: ... -def f6(b: int) -> None: ... - -static_assert(not is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6])) -``` - -When only one of the callable types has parameter names: - -```py -static_assert(not is_equivalent_to(CallableTypeOf[f5], Callable[[int], None])) -``` - -When the parameter kinds are different: - -```py -def f7(a: int, /) -> None: ... -def f8(a: int) -> None: ... - -static_assert(not is_equivalent_to(CallableTypeOf[f7], CallableTypeOf[f8])) -``` - -When the annotated types of the parameters are not equivalent or absent in one or both of the -callable types: - -```py -def f9(a: int) -> None: ... -def f10(a: str) -> None: ... -def f11(a) -> None: ... - -static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10])) -static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11])) -static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10])) -static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11])) -``` - -When the default value for a parameter is present only in one of the callable type: - -```py -def f12(a: int) -> None: ... -def f13(a: int = 2) -> None: ... - -static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13])) -static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12])) -``` - -### Unions containing `Callable`s containing unions - -Differently ordered unions inside `Callable`s inside unions can still be equivalent: - -```py -from typing import Callable -from knot_extensions import is_equivalent_to, static_assert - -static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int)) -``` - -[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md deleted file mode 100644 index 8af5af41545ad..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md +++ /dev/null @@ -1,101 +0,0 @@ -# Fully-static types - -A type is fully static iff it does not contain any gradual forms. - -## Fully-static - -```py -from typing_extensions import Literal, LiteralString, Never, Callable -from knot_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert - -static_assert(is_fully_static(Never)) -static_assert(is_fully_static(None)) - -static_assert(is_fully_static(Literal[1])) -static_assert(is_fully_static(Literal[True])) -static_assert(is_fully_static(Literal["abc"])) -static_assert(is_fully_static(Literal[b"abc"])) - -static_assert(is_fully_static(LiteralString)) - -static_assert(is_fully_static(str)) -static_assert(is_fully_static(object)) -static_assert(is_fully_static(type)) - -static_assert(is_fully_static(TypeOf[str])) -static_assert(is_fully_static(TypeOf[Literal])) - -static_assert(is_fully_static(str | None)) -static_assert(is_fully_static(Intersection[str, Not[LiteralString]])) - -static_assert(is_fully_static(tuple[()])) -static_assert(is_fully_static(tuple[int, object])) - -static_assert(is_fully_static(type[str])) -static_assert(is_fully_static(type[object])) -``` - -## Non-fully-static - -```py -from typing_extensions import Any, Literal, LiteralString, Callable -from knot_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert - -static_assert(not is_fully_static(Any)) -static_assert(not is_fully_static(Unknown)) - -static_assert(not is_fully_static(Any | str)) -static_assert(not is_fully_static(str | Unknown)) -static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]])) - -static_assert(not is_fully_static(tuple[Any, ...])) -static_assert(not is_fully_static(tuple[int, Any])) -static_assert(not is_fully_static(type[Any])) -``` - -## Callable - -```py -from typing_extensions import Callable, Any -from knot_extensions import Unknown, is_fully_static, static_assert - -static_assert(is_fully_static(Callable[[], int])) -static_assert(is_fully_static(Callable[[int, str], int])) - -static_assert(not is_fully_static(Callable[..., int])) -static_assert(not is_fully_static(Callable[[], Any])) -static_assert(not is_fully_static(Callable[[int, Unknown], int])) -``` - -The invalid forms of `Callable` annotation are never fully static because we represent them with the -`(...) -> Unknown` signature. - -```py -static_assert(not is_fully_static(Callable)) -# error: [invalid-type-form] -static_assert(not is_fully_static(Callable[int, int])) -``` - -Using function literals, we can check more variations of callable types as it allows us to define -parameters without annotations and no return type. - -```py -from knot_extensions import CallableTypeOf, is_fully_static, static_assert - -def f00() -> None: ... -def f01(a: int, b: str) -> None: ... -def f11(): ... -def f12(a, b): ... -def f13(a, b: int): ... -def f14(a, b: int) -> None: ... -def f15(a, b) -> None: ... - -static_assert(is_fully_static(CallableTypeOf[f00])) -static_assert(is_fully_static(CallableTypeOf[f01])) - -static_assert(not is_fully_static(CallableTypeOf[f11])) -static_assert(not is_fully_static(CallableTypeOf[f12])) -static_assert(not is_fully_static(CallableTypeOf[f13])) -static_assert(not is_fully_static(CallableTypeOf[f14])) -static_assert(not is_fully_static(CallableTypeOf[f15])) -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md deleted file mode 100644 index 43062b1802bb8..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ /dev/null @@ -1,160 +0,0 @@ -# Gradual equivalence relation - -Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also -materializations of `B`, and all materializations of `B` are also materializations of `A`. - -## Basic - -```py -from typing import Any -from typing_extensions import Literal, LiteralString, Never -from knot_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert - -static_assert(is_gradual_equivalent_to(Any, Any)) -static_assert(is_gradual_equivalent_to(Unknown, Unknown)) -static_assert(is_gradual_equivalent_to(Any, Unknown)) - -static_assert(is_gradual_equivalent_to(Never, Never)) -static_assert(is_gradual_equivalent_to(AlwaysTruthy, AlwaysTruthy)) -static_assert(is_gradual_equivalent_to(AlwaysFalsy, AlwaysFalsy)) -static_assert(is_gradual_equivalent_to(LiteralString, LiteralString)) - -static_assert(is_gradual_equivalent_to(Literal[True], Literal[True])) -static_assert(is_gradual_equivalent_to(Literal[False], Literal[False])) -static_assert(is_gradual_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2])) - -static_assert(is_gradual_equivalent_to(TypeOf[str], TypeOf[str])) -static_assert(is_gradual_equivalent_to(type, type[object])) - -static_assert(not is_gradual_equivalent_to(type, type[Any])) -static_assert(not is_gradual_equivalent_to(type[object], type[Any])) -``` - -## Unions and intersections - -```py -from typing import Any -from knot_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert - -static_assert(is_gradual_equivalent_to(str | int, str | int)) -static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown)) -static_assert(is_gradual_equivalent_to(str | int, int | str)) -static_assert( - is_gradual_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]]) -) -static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]])) - -static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes)) -static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict)) - -# TODO: No errors -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any)) -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any])) -``` - -## Tuples - -```py -from knot_extensions import Unknown, is_gradual_equivalent_to, static_assert -from typing import Any - -static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown])) - -static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, bytes])) -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, CallableTypeOf, is_gradual_equivalent_to, static_assert -from typing import Any, Callable - -static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int])) -static_assert(is_gradual_equivalent_to(Callable[..., Any], Callable[..., Unknown])) -static_assert(is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None])) - -static_assert(not is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None])) -static_assert(not is_gradual_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None])) -static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[[], None])) -``` - -A function with no explicit return type should be gradual equivalent to a callable with a return -type of `Any`. - -```py -def f1(): - return - -static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any])) -``` - -And, similarly for parameters with no annotations. - -```py -def f2(a, b, /) -> None: - return - -static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None])) -``` - -Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs` -parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable -with `...` as the parameter type. - -```py -def variadic_without_annotation(*args, **kwargs): - return - -def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any: - return - -static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any])) -static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any])) -``` - -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): - return - -def variadic_kwargs(**kwargs): - return - -static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any])) -static_assert(not is_gradual_equivalent_to(CallableTypeOf[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(CallableTypeOf[f1], CallableTypeOf[f2])) - -def f3(a=1): ... -def f4(a=2): ... -def f5(a): ... - -static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) -static_assert( - is_gradual_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]) -) -static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5])) - -def f6(a, /): ... - -static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6])) -``` - -[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md deleted file mode 100644 index 7763b3b1a68a9..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ /dev/null @@ -1,1152 +0,0 @@ -# Subtype relation - -The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`. - -A fully static type `S` is a subtype of another fully static type `T` iff the set of values -represented by `S` is a subset of the set of values represented by `T`. - -See the [typing documentation] for more information. - -## Basic builtin types - -- `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a - supertype of `bool` (present in `bool`s bases and MRO). -- `int` is not a subtype of `float`/`complex`, although this is muddied by the - [special case for float and complex] where annotations of `float` and `complex` are interpreted - as `int | float` and `int | float | complex`, respectively. - -```py -from knot_extensions import is_subtype_of, static_assert, TypeOf - -type JustFloat = TypeOf[1.0] -type JustComplex = TypeOf[1j] - -static_assert(is_subtype_of(bool, bool)) -static_assert(is_subtype_of(bool, int)) -static_assert(is_subtype_of(bool, object)) - -static_assert(is_subtype_of(int, int)) -static_assert(is_subtype_of(int, object)) - -static_assert(is_subtype_of(object, object)) - -static_assert(not is_subtype_of(int, bool)) -static_assert(not is_subtype_of(int, str)) -static_assert(not is_subtype_of(object, int)) - -static_assert(not is_subtype_of(int, JustFloat)) -static_assert(not is_subtype_of(int, JustComplex)) - -static_assert(is_subtype_of(TypeError, Exception)) -static_assert(is_subtype_of(FloatingPointError, Exception)) -``` - -## Class hierarchies - -```py -from knot_extensions import is_subtype_of, static_assert -from typing_extensions import Never - -class A: ... -class B1(A): ... -class B2(A): ... -class C(B1, B2): ... - -static_assert(is_subtype_of(B1, A)) -static_assert(not is_subtype_of(A, B1)) - -static_assert(is_subtype_of(B2, A)) -static_assert(not is_subtype_of(A, B2)) - -static_assert(not is_subtype_of(B1, B2)) -static_assert(not is_subtype_of(B2, B1)) - -static_assert(is_subtype_of(C, B1)) -static_assert(is_subtype_of(C, B2)) -static_assert(not is_subtype_of(B1, C)) -static_assert(not is_subtype_of(B2, C)) -static_assert(is_subtype_of(C, A)) -static_assert(not is_subtype_of(A, C)) - -static_assert(is_subtype_of(Never, A)) -static_assert(is_subtype_of(Never, B1)) -static_assert(is_subtype_of(Never, B2)) -static_assert(is_subtype_of(Never, C)) - -static_assert(is_subtype_of(A, object)) -static_assert(is_subtype_of(B1, object)) -static_assert(is_subtype_of(B2, object)) -static_assert(is_subtype_of(C, object)) -``` - -## Literal types - -```py -from typing_extensions import Literal, LiteralString -from knot_extensions import is_subtype_of, static_assert, TypeOf - -type JustFloat = TypeOf[1.0] - -# Boolean literals -static_assert(is_subtype_of(Literal[True], bool)) -static_assert(is_subtype_of(Literal[True], int)) -static_assert(is_subtype_of(Literal[True], object)) - -# Integer literals -static_assert(is_subtype_of(Literal[1], int)) -static_assert(is_subtype_of(Literal[1], object)) - -static_assert(not is_subtype_of(Literal[1], bool)) - -static_assert(not is_subtype_of(Literal[1], JustFloat)) - -# String literals -static_assert(is_subtype_of(Literal["foo"], LiteralString)) -static_assert(is_subtype_of(Literal["foo"], str)) -static_assert(is_subtype_of(Literal["foo"], object)) - -static_assert(is_subtype_of(LiteralString, str)) -static_assert(is_subtype_of(LiteralString, object)) - -# Bytes literals -static_assert(is_subtype_of(Literal[b"foo"], bytes)) -static_assert(is_subtype_of(Literal[b"foo"], object)) -``` - -## Tuple types - -```py -from knot_extensions import is_subtype_of, static_assert - -class A1: ... -class B1(A1): ... -class A2: ... -class B2(A2): ... -class Unrelated: ... - -static_assert(is_subtype_of(B1, A1)) -static_assert(is_subtype_of(B2, A2)) - -# Zero-element tuples -static_assert(is_subtype_of(tuple[()], tuple[()])) -static_assert(not is_subtype_of(tuple[()], tuple[Unrelated])) - -# One-element tuples -static_assert(is_subtype_of(tuple[B1], tuple[A1])) -static_assert(not is_subtype_of(tuple[B1], tuple[Unrelated])) -static_assert(not is_subtype_of(tuple[B1], tuple[()])) -static_assert(not is_subtype_of(tuple[B1], tuple[A1, Unrelated])) - -# Two-element tuples -static_assert(is_subtype_of(tuple[B1, B2], tuple[A1, A2])) -static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, A2])) -static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, Unrelated])) -static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, Unrelated])) -static_assert(not is_subtype_of(tuple[B1, B2], tuple[()])) -static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1])) -static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, A2, Unrelated])) - -static_assert(is_subtype_of(tuple[int], tuple)) -``` - -## Union types - -```py -from knot_extensions import is_subtype_of, static_assert -from typing import Literal - -class A: ... -class B1(A): ... -class B2(A): ... -class Unrelated1: ... -class Unrelated2: ... - -static_assert(is_subtype_of(B1, A)) -static_assert(is_subtype_of(B2, A)) - -# Union on the right hand side -static_assert(is_subtype_of(B1, A | Unrelated1)) -static_assert(is_subtype_of(B1, Unrelated1 | A)) - -static_assert(not is_subtype_of(B1, Unrelated1 | Unrelated2)) - -# Union on the left hand side -static_assert(is_subtype_of(B1 | B2, A)) -static_assert(is_subtype_of(B1 | B2 | A, object)) - -static_assert(not is_subtype_of(B1 | Unrelated1, A)) -static_assert(not is_subtype_of(Unrelated1 | B1, A)) - -# Union on both sides -static_assert(is_subtype_of(B1 | bool, A | int)) -static_assert(is_subtype_of(B1 | bool, int | A)) - -static_assert(not is_subtype_of(B1 | bool, Unrelated1 | int)) -static_assert(not is_subtype_of(B1 | bool, int | Unrelated1)) - -# Example: Unions of literals -static_assert(is_subtype_of(Literal[1, 2, 3], int)) -static_assert(not is_subtype_of(Literal[1, "two", 3], int)) -``` - -## Intersection types - -```py -from typing_extensions import Literal, LiteralString -from knot_extensions import Intersection, Not, is_subtype_of, static_assert - -class A: ... -class B1(A): ... -class B2(A): ... -class C(B1, B2): ... -class Unrelated: ... - -static_assert(is_subtype_of(B1, A)) -static_assert(is_subtype_of(B2, A)) -static_assert(is_subtype_of(C, A)) -static_assert(is_subtype_of(C, B1)) -static_assert(is_subtype_of(C, B2)) - -# For complements, the subtyping relation is reversed: -static_assert(is_subtype_of(Not[A], Not[B1])) -static_assert(is_subtype_of(Not[A], Not[B2])) -static_assert(is_subtype_of(Not[A], Not[C])) -static_assert(is_subtype_of(Not[B1], Not[C])) -static_assert(is_subtype_of(Not[B2], Not[C])) - -# The intersection of two types is a subtype of both: -static_assert(is_subtype_of(Intersection[B1, B2], B1)) -static_assert(is_subtype_of(Intersection[B1, B2], B2)) -# … and of their common supertype: -static_assert(is_subtype_of(Intersection[B1, B2], A)) - -# A common subtype of two types is a subtype of their intersection: -static_assert(is_subtype_of(C, Intersection[B1, B2])) -# … but not the other way around: -static_assert(not is_subtype_of(Intersection[B1, B2], C)) - -# "Removing" B1 from A leaves a subtype of A. -static_assert(is_subtype_of(Intersection[A, Not[B1]], A)) -static_assert(is_subtype_of(Intersection[A, Not[B1]], Not[B1])) - -# B1 and B2 are not disjoint, so this is not true: -static_assert(not is_subtype_of(B2, Intersection[A, Not[B1]])) -# … but for two disjoint subtypes, it is: -static_assert(is_subtype_of(Literal[2], Intersection[int, Not[Literal[1]]])) - -# A and Unrelated are not related, so this is not true: -static_assert(not is_subtype_of(Intersection[A, Not[B1]], Not[Unrelated])) -# … but for a disjoint type like `None`, it is: -static_assert(is_subtype_of(Intersection[A, Not[B1]], Not[None])) - -# Complements of types are still subtypes of `object`: -static_assert(is_subtype_of(Not[A], object)) - -# More examples: -static_assert(is_subtype_of(type[str], Not[None])) -static_assert(is_subtype_of(Not[LiteralString], object)) - -static_assert(not is_subtype_of(Intersection[int, Not[Literal[2]]], Intersection[int, Not[Literal[3]]])) -static_assert(not is_subtype_of(Not[Literal[2]], Not[Literal[3]])) -static_assert(not is_subtype_of(Not[Literal[2]], Not[int])) -static_assert(not is_subtype_of(int, Not[Literal[3]])) -static_assert(not is_subtype_of(Literal[1], Intersection[int, Not[Literal[1]]])) -``` - -## Special types - -### `Never` - -`Never` is a subtype of all types. - -```py -from typing_extensions import Literal, Never -from knot_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert - -static_assert(is_subtype_of(Never, Never)) -static_assert(is_subtype_of(Never, Literal[True])) -static_assert(is_subtype_of(Never, bool)) -static_assert(is_subtype_of(Never, int)) -static_assert(is_subtype_of(Never, object)) - -static_assert(is_subtype_of(Never, AlwaysTruthy)) -static_assert(is_subtype_of(Never, AlwaysFalsy)) -``` - -### `AlwaysTruthy` and `AlwaysFalsy` - -```py -from knot_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert -from typing_extensions import Literal, LiteralString - -static_assert(is_subtype_of(Literal[1], AlwaysTruthy)) -static_assert(is_subtype_of(Literal[0], AlwaysFalsy)) - -static_assert(is_subtype_of(AlwaysTruthy, object)) -static_assert(is_subtype_of(AlwaysFalsy, object)) - -static_assert(not is_subtype_of(Literal[1], AlwaysFalsy)) -static_assert(not is_subtype_of(Literal[0], AlwaysTruthy)) - -static_assert(not is_subtype_of(str, AlwaysTruthy)) -static_assert(not is_subtype_of(str, AlwaysFalsy)) - -# TODO: No errors -# error: [static-assert-error] -static_assert(is_subtype_of(bool, Literal[False] | AlwaysTruthy)) -# error: [static-assert-error] -static_assert(is_subtype_of(bool, Literal[True] | AlwaysFalsy)) -# error: [static-assert-error] -static_assert(is_subtype_of(LiteralString, Literal[""] | AlwaysTruthy)) -static_assert(not is_subtype_of(Literal[True] | AlwaysFalsy, Literal[False] | AlwaysTruthy)) - -# TODO: No errors -# The condition `is_subtype_of(T & U, U)` must still be satisfied after the following transformations: -# `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` -# error: [static-assert-error] -static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], AlwaysTruthy)) -# error: [static-assert-error] -static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], AlwaysTruthy)) -# `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` -# error: [static-assert-error] -static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy])) -# error: [static-assert-error] -static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy])) -``` - -### Module literals - -```py -from types import ModuleType -from knot_extensions import TypeOf, is_subtype_of, static_assert -from typing_extensions import assert_type -import typing - -assert_type(typing, TypeOf[typing]) - -static_assert(is_subtype_of(TypeOf[typing], ModuleType)) -``` - -### Slice literals - -```py -from knot_extensions import TypeOf, is_subtype_of, static_assert - -static_assert(is_subtype_of(TypeOf[1:2:3], slice)) -``` - -### Special forms - -```py -from typing import _SpecialForm, Literal -from knot_extensions import TypeOf, is_subtype_of, static_assert - -static_assert(is_subtype_of(TypeOf[Literal], _SpecialForm)) -static_assert(is_subtype_of(TypeOf[Literal], object)) - -static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal])) -``` - -## Class literal types and `type[…]` - -### Basic - -```py -from typing import _SpecialForm -from typing_extensions import Literal, assert_type -from knot_extensions import TypeOf, is_subtype_of, static_assert - -class Meta(type): ... -class HasCustomMetaclass(metaclass=Meta): ... - -type LiteralBool = TypeOf[bool] -type LiteralInt = TypeOf[int] -type LiteralStr = TypeOf[str] -type LiteralObject = TypeOf[object] - -assert_type(bool, LiteralBool) -assert_type(int, LiteralInt) -assert_type(str, LiteralStr) -assert_type(object, LiteralObject) - -# bool - -static_assert(is_subtype_of(LiteralBool, LiteralBool)) -static_assert(is_subtype_of(LiteralBool, type[bool])) -static_assert(is_subtype_of(LiteralBool, type[int])) -static_assert(is_subtype_of(LiteralBool, type[object])) -static_assert(is_subtype_of(LiteralBool, type)) -static_assert(is_subtype_of(LiteralBool, object)) - -static_assert(not is_subtype_of(LiteralBool, LiteralInt)) -static_assert(not is_subtype_of(LiteralBool, LiteralObject)) -static_assert(not is_subtype_of(LiteralBool, bool)) - -static_assert(not is_subtype_of(type, type[bool])) - -# int - -static_assert(is_subtype_of(LiteralInt, LiteralInt)) -static_assert(is_subtype_of(LiteralInt, type[int])) -static_assert(is_subtype_of(LiteralInt, type[object])) -static_assert(is_subtype_of(LiteralInt, type)) -static_assert(is_subtype_of(LiteralInt, object)) - -static_assert(not is_subtype_of(LiteralInt, LiteralObject)) -static_assert(not is_subtype_of(LiteralInt, int)) - -static_assert(not is_subtype_of(type, type[int])) - -# LiteralString - -static_assert(is_subtype_of(LiteralStr, type[str])) -static_assert(is_subtype_of(LiteralStr, type)) -static_assert(is_subtype_of(LiteralStr, type[object])) - -static_assert(not is_subtype_of(type[str], LiteralStr)) - -# custom metaclasses - -type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass] - -static_assert(is_subtype_of(LiteralHasCustomMetaclass, Meta)) -static_assert(is_subtype_of(Meta, type[object])) -static_assert(is_subtype_of(Meta, type)) - -static_assert(not is_subtype_of(Meta, type[type])) -``` - -### Unions of class literals - -```py -from typing_extensions import assert_type -from knot_extensions import TypeOf, is_subtype_of, static_assert - -class Base: ... -class Derived(Base): ... -class Unrelated: ... - -type LiteralBase = TypeOf[Base] -type LiteralDerived = TypeOf[Derived] -type LiteralUnrelated = TypeOf[Unrelated] - -assert_type(Base, LiteralBase) -assert_type(Derived, LiteralDerived) -assert_type(Unrelated, LiteralUnrelated) - -static_assert(is_subtype_of(LiteralBase, type)) -static_assert(is_subtype_of(LiteralBase, object)) - -static_assert(is_subtype_of(LiteralBase, type[Base])) -static_assert(is_subtype_of(LiteralDerived, type[Base])) -static_assert(is_subtype_of(LiteralDerived, type[Derived])) - -static_assert(not is_subtype_of(LiteralBase, type[Derived])) -static_assert(is_subtype_of(type[Derived], type[Base])) - -static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, type)) -static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object)) -``` - -## Non-fully-static types - -`Any`, `Unknown`, `Todo` and derivatives thereof do not participate in subtyping. - -```py -from knot_extensions import Unknown, is_subtype_of, static_assert, Intersection -from typing_extensions import Any - -static_assert(not is_subtype_of(Any, Any)) -static_assert(not is_subtype_of(Any, int)) -static_assert(not is_subtype_of(int, Any)) -static_assert(not is_subtype_of(Any, object)) -static_assert(not is_subtype_of(object, Any)) - -static_assert(not is_subtype_of(int, Any | int)) -static_assert(not is_subtype_of(Intersection[Any, int], int)) -static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any])) - -# The same for `Unknown`: -static_assert(not is_subtype_of(Unknown, Unknown)) -static_assert(not is_subtype_of(Unknown, int)) -static_assert(not is_subtype_of(int, Unknown)) -static_assert(not is_subtype_of(Unknown, object)) -static_assert(not is_subtype_of(object, Unknown)) - -static_assert(not is_subtype_of(int, Unknown | int)) -static_assert(not is_subtype_of(Intersection[Unknown, int], int)) -static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown])) -``` - -## Callable - -The general principle is that a callable type is a subtype of another if it's more flexible in what -it accepts and more specific in what it returns. - -References: - -- -- - -### Return type - -Return types are covariant. - -```py -from typing import Callable -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. - -#### Positional-only - -```py -from typing import Callable -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf - -def float_param(a: float, /) -> None: ... -def int_param(a: int, /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param])) -static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[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: - -```py -def int_param_different_name(b: int, /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_param_different_name])) -static_assert(is_subtype_of(CallableTypeOf[int_param_different_name], CallableTypeOf[int_param])) -``` - -Multiple positional-only parameters are checked in order: - -```py -def multi_param1(a: float, b: int, c: str, /) -> None: ... -def multi_param2(b: int, c: bool, a: str, /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2])) -static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[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 - -If the parameter has a default value, it's treated as optional. This means that the parameter at the -corresponding position in the supertype does not need to have a default value. - -```py -from typing import Callable -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf - -def float_with_default(a: float = 1, /) -> None: ... -def int_with_default(a: int = 1, /) -> None: ... -def int_without_default(a: int, /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) -static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) - -static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_without_default])) -static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[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: - -```py -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) -``` - -The subtype can include any number of positional-only parameters as long as they have the default -value: - -```py -def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[multi_param], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param])) -``` - -#### Positional-only with other kinds - -If a parameter is declared as positional-only, then the corresponding parameter in the supertype -cannot be any other parameter kind. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def positional_only(a: int, /) -> None: ... -def standard(a: int) -> None: ... -def keyword_only(*, a: int) -> None: ... -def variadic(*a: int) -> None: ... -def keyword_variadic(**a: int) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[standard])) -static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_only])) -static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[variadic])) -static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_variadic])) -``` - -#### Standard - -A standard parameter is either a positional or a keyword parameter. - -Unlike positional-only parameters, standard parameters should have the same name in the subtype. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def int_param_a(a: int) -> None: ... -def int_param_b(b: int) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[int_param_a], CallableTypeOf[int_param_b])) -static_assert(not is_subtype_of(CallableTypeOf[int_param_b], CallableTypeOf[int_param_a])) -``` - -Apart from the name, it behaves the same as positional-only parameters. - -```py -def float_param(a: float) -> None: ... -def int_param(a: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param])) -static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param])) -``` - -With the same rules for default values as well. - -```py -def float_with_default(a: float = 1) -> None: ... -def int_with_default(a: int = 1) -> None: ... -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) -static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) - -static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_param])) -static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_with_default])) - -static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) -``` - -Multiple standard parameters are checked in order along with their names: - -```py -def multi_param1(a: float, b: int, c: str) -> None: ... -def multi_param2(a: int, b: bool, c: str) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2])) -static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1])) -``` - -The subtype can include as many standard parameters as long as they have the default value: - -```py -def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[multi_param_default], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param_default])) -``` - -#### Standard with keyword-only - -A keyword-only parameter in the supertype can be substituted with the corresponding standard -parameter in the subtype with the same name. This is because a standard parameter is more flexible -than a keyword-only parameter. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def standard_a(a: int) -> None: ... -def keyword_b(*, b: int) -> None: ... - -# The name of the parameters are different -static_assert(not is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[keyword_b])) - -def standard_float(a: float) -> None: ... -def keyword_int(*, a: int) -> None: ... - -# Here, the name of the parameters are the same -static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[keyword_int])) - -def standard_with_default(a: int = 1) -> None: ... -def keyword_with_default(*, a: int = 1) -> None: ... -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keyword_with_default])) -static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty])) -``` - -The position of the keyword-only parameters does not matter: - -```py -def multi_standard(a: float, b: int, c: str) -> None: ... -def multi_keyword(*, b: bool, c: str, a: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_keyword])) -``` - -#### Standard with positional-only - -A positional-only parameter in the supertype can be substituted with the corresponding standard -parameter in the subtype at the same position. This is because a standard parameter is more flexible -than a positional-only parameter. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def standard_a(a: int) -> None: ... -def positional_b(b: int, /) -> None: ... - -# The names are not important in this context -static_assert(is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[positional_b])) - -def standard_float(a: float) -> None: ... -def positional_int(a: int, /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[positional_int])) - -def standard_with_default(a: int = 1) -> None: ... -def positional_with_default(a: int = 1, /) -> None: ... -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[positional_with_default])) -static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty])) -``` - -The position of the positional-only parameters matter: - -```py -def multi_standard(a: float, b: int, c: str) -> None: ... -def multi_positional1(b: int, c: bool, a: str, /) -> None: ... - -# Here, the type of the parameter `a` makes the subtype relation invalid -def multi_positional2(b: int, a: float, c: str, /) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional1])) -static_assert(not is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional2])) -``` - -#### Standard with variadic - -A variadic or keyword-variadic parameter in the supertype cannot be substituted with a standard -parameter in the subtype. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def standard(a: int) -> None: ... -def variadic(*a: int) -> None: ... -def keyword_variadic(**a: int) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[variadic])) -static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword_variadic])) -``` - -#### Variadic - -The name of the variadic parameter does not need to be the same in the subtype. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def variadic_float(*args2: float) -> None: ... -def variadic_int(*args1: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[variadic_float], CallableTypeOf[variadic_int])) -static_assert(not is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[variadic_float])) -``` - -The variadic parameter does not need to be present in the supertype: - -```py -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[variadic_int])) -``` - -#### Variadic with positional-only - -If the subtype has a variadic parameter then any unmatched positional-only parameter from the -supertype should be checked against the variadic parameter. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def variadic(a: int, /, *args: float) -> None: ... - -# Here, the parameter `b` and `c` are unmatched -def positional_only(a: int, b: float, c: int, /) -> None: ... - -# Here, the parameter `b` is unmatched and there's also a variadic parameter -def positional_variadic(a: int, b: float, /, *args: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_only])) -static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_variadic])) -``` - -#### Variadic with other kinds - -Variadic parameter in a subtype can only be used to match against an unmatched positional-only -parameters from the supertype, not any other parameter kind. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def variadic(*args: int) -> None: ... - -# Both positional-only parameters are unmatched so uses the variadic parameter but the other -# parameter `c` remains and cannot be matched. -def standard(a: int, b: float, /, c: int) -> None: ... - -# Similarly, for other kinds -def keyword_only(a: int, /, *, b: int) -> None: ... -def keyword_variadic(a: int, /, **kwargs: int) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[standard])) -static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_only])) -static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic])) -``` - -But, there are special cases when matching against standard parameters. This is due to the fact that -a standard parameter can be passed as a positional or keyword parameter. This means that the -subtyping relation needs to consider both cases. - -```py -def variadic_keyword(*args: int, **kwargs: int) -> None: ... -def standard_int(a: int) -> None: ... -def standard_float(a: float) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int])) -static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float])) -``` - -If the type of either the variadic or keyword-variadic parameter is not a supertype of the standard -parameter, then the subtyping relation is invalid. - -```py -def variadic_bool(*args: bool, **kwargs: int) -> None: ... -def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int])) -static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int])) -``` - -The standard parameter can follow a variadic parameter in the subtype. - -```py -def standard_variadic_int(a: int, *args: int) -> None: ... -def standard_variadic_float(a: int, *args: float) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int])) -static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float])) -``` - -The keyword part of the standard parameter can be matched against keyword-only parameter with the -same name if the keyword-variadic parameter is absent. - -```py -def variadic_a(*args: int, a: int) -> None: ... -def variadic_b(*args: int, b: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int])) -# The parameter name is different -static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int])) -``` - -#### Keyword-only - -For keyword-only parameters, the name should be the same: - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def keyword_int(*, a: int) -> None: ... -def keyword_float(*, a: float) -> None: ... -def keyword_b(*, b: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[keyword_float], CallableTypeOf[keyword_int])) -static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_float])) -static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_b])) -``` - -But, the order of the keyword-only parameters is not required to be the same: - -```py -def keyword_ab(*, a: float, b: float) -> None: ... -def keyword_ba(*, b: int, a: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[keyword_ab], CallableTypeOf[keyword_ba])) -static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keyword_ab])) -``` - -#### Keyword-only with default - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def float_with_default(*, a: float = 1) -> None: ... -def int_with_default(*, a: int = 1) -> None: ... -def int_keyword(*, a: int) -> None: ... -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) -static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) - -static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_keyword])) -static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[int_with_default])) - -static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) -``` - -Keyword-only parameters with default values can be mixed with the ones without default values in any -order: - -```py -# A keyword-only parameter with a default value follows the one without a default value (it's valid) -def mixed(*, b: int = 1, a: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[int_keyword])) -static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixed])) -``` - -#### Keyword-only with standard - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def keywords1(*, a: int, b: int) -> None: ... -def standard(b: float, a: float) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[keywords1], CallableTypeOf[standard])) -static_assert(is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keywords1])) -``` - -The subtype can include additional standard parameters as long as it has the default value: - -```py -def standard_with_default(b: float, a: float, c: float = 1) -> None: ... -def standard_without_default(b: float, a: float, c: float) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[standard_without_default], CallableTypeOf[keywords1])) -static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keywords1])) -``` - -Here, we mix keyword-only parameters with standard parameters: - -```py -def keywords2(*, a: int, c: int, b: int) -> None: ... -def mixed(b: float, a: float, *, c: float) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[keywords2], CallableTypeOf[mixed])) -static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[keywords2])) -``` - -But, we shouldn't consider any unmatched positional-only parameters: - -```py -def mixed_positional(b: float, /, a: float, *, c: float) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[mixed_positional], CallableTypeOf[keywords2])) -``` - -But, an unmatched variadic parameter is still valid: - -```py -def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywords2])) -``` - -#### Keyword-variadic - -The name of the keyword-variadic parameter does not need to be the same in the subtype. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def kwargs_float(**kwargs2: float) -> None: ... -def kwargs_int(**kwargs1: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[kwargs_float], CallableTypeOf[kwargs_int])) -static_assert(not is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[kwargs_float])) -``` - -A variadic parameter can be omitted in the subtype: - -```py -def empty() -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[kwargs_int])) -``` - -#### Keyword-variadic with keyword-only - -If the subtype has a keyword-variadic parameter then any unmatched keyword-only parameter from the -supertype should be checked against the keyword-variadic parameter. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def kwargs(**kwargs: float) -> None: ... -def keyword_only(*, a: int, b: float, c: bool) -> None: ... -def keyword_variadic(*, a: int, **kwargs: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_only])) -static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_variadic])) -``` - -This is valid only for keyword-only parameters, not any other parameter kind: - -```py -def mixed1(a: int, *, b: int) -> None: ... - -# Same as above but with the default value -def mixed2(a: int = 1, *, b: int) -> None: ... - -static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed1])) -static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed2])) -``` - -#### Empty - -When the supertype has an empty list of parameters, then the subtype can have any kind of parameters -as long as they contain the default values for non-variadic parameters. - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert - -def empty() -> None: ... -def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[empty])) -static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed])) -``` - -#### Object - -```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf -from typing import Callable - -def f1(a: int, b: str, /, *c: float, d: int = 1, **e: float) -> None: ... - -static_assert(is_subtype_of(CallableTypeOf[f1], object)) -static_assert(not is_subtype_of(object, CallableTypeOf[f1])) - -def _( - f3: Callable[[int, str], None], -) -> None: - static_assert(is_subtype_of(TypeOf[f3], object)) - static_assert(not is_subtype_of(object, TypeOf[f3])) - -class C: - def foo(self) -> None: ... - -static_assert(is_subtype_of(TypeOf[C.foo], object)) -static_assert(not is_subtype_of(object, TypeOf[C.foo])) -``` - -### Classes with `__call__` - -```py -from typing import Callable -from knot_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to - -class A: - def __call__(self, a: int) -> int: - return a - -a = A() - -static_assert(is_subtype_of(A, Callable[[int], int])) -static_assert(not is_subtype_of(A, Callable[[], int])) -static_assert(not is_subtype_of(Callable[[int], int], A)) - -def f(fn: Callable[[int], int]) -> None: ... - -f(a) -``` - -### Bound methods - -```py -from typing import Callable -from knot_extensions import TypeOf, static_assert, is_subtype_of - -class A: - def f(self, a: int) -> int: - return a - - @classmethod - def g(cls, a: int) -> int: - return a - -a = A() - -static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], int])) -static_assert(is_subtype_of(TypeOf[a.g], Callable[[int], int])) -static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], int])) - -static_assert(not is_subtype_of(TypeOf[a.f], Callable[[float], int])) -static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int])) - -# TODO: This assertion should be true -# error: [static-assert-error] "Static assertion error: argument evaluates to `False`" -static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int])) -``` - -[special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex -[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md deleted file mode 100644 index 00e7c09539088..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md +++ /dev/null @@ -1,145 +0,0 @@ -# Truthiness - -## Literals - -```py -from typing_extensions import Literal, LiteralString -from knot_extensions import AlwaysFalsy, AlwaysTruthy - -def _( - a: Literal[1], - b: Literal[-1], - c: Literal["foo"], - d: tuple[Literal[0]], - e: Literal[1, 2], - f: AlwaysTruthy, -): - reveal_type(bool(a)) # revealed: Literal[True] - reveal_type(bool(b)) # revealed: Literal[True] - reveal_type(bool(c)) # revealed: Literal[True] - reveal_type(bool(d)) # revealed: Literal[True] - reveal_type(bool(e)) # revealed: Literal[True] - reveal_type(bool(f)) # revealed: Literal[True] - -def _( - a: tuple[()], - b: Literal[0], - c: Literal[""], - d: Literal[b""], - e: Literal[0, 0], - f: AlwaysFalsy, -): - reveal_type(bool(a)) # revealed: Literal[False] - reveal_type(bool(b)) # revealed: Literal[False] - reveal_type(bool(c)) # revealed: Literal[False] - reveal_type(bool(d)) # revealed: Literal[False] - reveal_type(bool(e)) # revealed: Literal[False] - reveal_type(bool(f)) # revealed: Literal[False] - -def _( - a: str, - b: Literal[1, 0], - c: str | Literal[0], - d: str | Literal[1], -): - reveal_type(bool(a)) # revealed: bool - reveal_type(bool(b)) # revealed: bool - reveal_type(bool(c)) # revealed: bool - reveal_type(bool(d)) # revealed: bool -``` - -## Instances - -Checks that we don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin: - -### __bool__ is bool - -```py -class BoolIsBool: - __bool__ = bool - -reveal_type(bool(BoolIsBool())) # revealed: bool -``` - -### Conditional __bool__ method - -```py -def flag() -> bool: - return True - -class Boom: - if flag(): - __bool__ = bool - else: - __bool__ = int - -reveal_type(bool(Boom())) # revealed: bool -``` - -### Possibly unbound __bool__ method - -```py -from typing import Literal - -def flag() -> bool: - return True - -class PossiblyUnboundTrue: - if flag(): - def __bool__(self) -> Literal[True]: - return True - -reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool -``` - -### Special-cased classes - -Some special-cased `@final` classes are known by red-knot to have instances that are either always -truthy or always falsy. - -```toml -[environment] -python-version = "3.12" -``` - -```py -import types -import typing -import sys -from knot_extensions import AlwaysTruthy, static_assert, is_subtype_of -from typing_extensions import _NoDefaultType - -static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy)) -static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy)) -static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy)) -static_assert(is_subtype_of(slice, AlwaysTruthy)) -static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy)) -static_assert(is_subtype_of(types.MethodType, AlwaysTruthy)) -static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy)) -static_assert(is_subtype_of(typing.TypeAliasType, AlwaysTruthy)) -static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy)) -static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy)) -``` - -### `Callable` types always have ambiguous truthiness - -```py -from typing import Callable - -def f(x: Callable, y: Callable[[int], str]): - reveal_type(bool(x)) # revealed: bool - reveal_type(bool(y)) # revealed: bool -``` - -But certain callable single-valued types are known to be always truthy: - -```py -from types import FunctionType - -class A: - def method(self): ... - -reveal_type(bool(A().method)) # revealed: Literal[True] -reveal_type(bool(f.__get__)) # revealed: Literal[True] -reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/final.md deleted file mode 100644 index 32e84412eb681..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/final.md +++ /dev/null @@ -1,87 +0,0 @@ -# `typing.Final` - -[`typing.Final`] is a type qualifier that is used to indicate that a symbol may not be reassigned in -any scope. Final names declared in class scopes cannot be overridden in subclasses. - -## Basic - -`mod.py`: - -```py -from typing import Final, Annotated - -FINAL_A: int = 1 -FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1 -FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1 -FINAL_D: Final = 1 -FINAL_E: "Final[int]" = 1 - -reveal_type(FINAL_A) # revealed: Literal[1] -reveal_type(FINAL_B) # revealed: Literal[1] -reveal_type(FINAL_C) # revealed: Literal[1] -reveal_type(FINAL_D) # revealed: Literal[1] -reveal_type(FINAL_E) # revealed: Literal[1] - -# TODO: All of these should be errors: -FINAL_A = 2 -FINAL_B = 2 -FINAL_C = 2 -FINAL_D = 2 -FINAL_E = 2 -``` - -Public types: - -```py -from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E - -# TODO: All of these should be Literal[1] -reveal_type(FINAL_A) # revealed: int -reveal_type(FINAL_B) # revealed: int -reveal_type(FINAL_C) # revealed: int -reveal_type(FINAL_D) # revealed: Unknown -reveal_type(FINAL_E) # revealed: int -``` - -## Too many arguments - -```py -from typing import Final - -class C: - # error: [invalid-type-form] "Type qualifier `typing.Final` expects exactly one type parameter" - x: Final[int, str] = 1 -``` - -## Illegal `Final` in type expression - -```py -from typing import Final - -class C: - # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" - x: Final | int - - # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" - y: int | Final[str] -``` - -## No assignment - -```py -from typing import Final - -DECLARED_THEN_BOUND: Final[int] -DECLARED_THEN_BOUND = 1 -``` - -## No assignment for bare `Final` - -```py -from typing import Final - -# TODO: This should be an error -NO_RHS: Final -``` - -[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md b/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md deleted file mode 100644 index 1ad79dd3bc5e0..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md +++ /dev/null @@ -1,169 +0,0 @@ -# Custom unary operations - -## Class instances - -```py -class Yes: - def __pos__(self) -> bool: - return False - - def __neg__(self) -> str: - return "negative" - - def __invert__(self) -> int: - return 17 - -class Sub(Yes): ... -class No: ... - -reveal_type(+Yes()) # revealed: bool -reveal_type(-Yes()) # revealed: str -reveal_type(~Yes()) # revealed: int - -reveal_type(+Sub()) # revealed: bool -reveal_type(-Sub()) # revealed: str -reveal_type(~Sub()) # revealed: int - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`" -reveal_type(+No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`" -reveal_type(-No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`" -reveal_type(~No()) # revealed: Unknown -``` - -## Classes - -Dunder methods defined in a class are available to instances of that class, but not to the class -itself. (For these operators to work on the class itself, they would have to be defined on the -class's type, i.e. `type`.) - -```py -class Yes: - def __pos__(self) -> bool: - return False - - def __neg__(self) -> str: - return "negative" - - def __invert__(self) -> int: - return 17 - -class Sub(Yes): ... -class No: ... - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`" -reveal_type(+Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`" -reveal_type(-Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`" -reveal_type(~Yes) # revealed: Unknown - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`" -reveal_type(+Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`" -reveal_type(-Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`" -reveal_type(~Sub) # revealed: Unknown - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`" -reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`" -reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`" -reveal_type(~No) # revealed: Unknown -``` - -## Function literals - -```py -def f(): - pass - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `def f() -> Unknown`" -reveal_type(+f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `def f() -> Unknown`" -reveal_type(-f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `def f() -> Unknown`" -reveal_type(~f) # revealed: Unknown -``` - -## Subclass - -```py -class Yes: - def __pos__(self) -> bool: - return False - - def __neg__(self) -> str: - return "negative" - - def __invert__(self) -> int: - return 17 - -class Sub(Yes): ... -class No: ... - -def yes() -> type[Yes]: - return Yes - -def sub() -> type[Sub]: - return Sub - -def no() -> type[No]: - return No - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`" -reveal_type(+yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`" -reveal_type(-yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`" -reveal_type(~yes()) # revealed: Unknown - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`" -reveal_type(+sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`" -reveal_type(-sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`" -reveal_type(~sub()) # revealed: Unknown - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`" -reveal_type(+no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`" -reveal_type(-no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`" -reveal_type(~no()) # revealed: Unknown -``` - -## Metaclass - -```py -class Meta(type): - def __pos__(self) -> bool: - return False - - def __neg__(self) -> str: - return "negative" - - def __invert__(self) -> int: - return 17 - -class Yes(metaclass=Meta): ... -class Sub(Yes): ... -class No: ... - -reveal_type(+Yes) # revealed: bool -reveal_type(-Yes) # revealed: str -reveal_type(~Yes) # revealed: int - -reveal_type(+Sub) # revealed: bool -reveal_type(-Sub) # revealed: str -reveal_type(~Sub) # revealed: int - -# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`" -reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`" -reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`" -reveal_type(~No) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md deleted file mode 100644 index 44d4d93d1d178..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ /dev/null @@ -1,168 +0,0 @@ -# Union types - -This test suite covers certain basic properties and simplification strategies for union types. - -## Basic unions - -```py -from typing import Literal - -def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None: - reveal_type(u1) # revealed: int | str - reveal_type(u2) # revealed: Literal[0, 1] -``` - -## Duplicate elements are collapsed - -```py -def _(u1: int | int | str, u2: int | str | int) -> None: - reveal_type(u1) # revealed: int | str - reveal_type(u2) # revealed: int | str -``` - -## `Never` is removed - -`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant, -and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`. - -```py -from typing_extensions import Never, NoReturn - -def never(u1: int | Never, u2: int | Never | str) -> None: - reveal_type(u1) # revealed: int - reveal_type(u2) # revealed: int | str - -def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None: - reveal_type(u1) # revealed: int - reveal_type(u2) # revealed: int | str -``` - -## `object` subsumes everything - -Unions with `object` can be simplified to `object`: - -```py -from typing_extensions import Never, Any - -def _( - u1: int | object, - u2: object | int, - u3: Any | object, - u4: object | Any, - u5: object | Never, - u6: Never | object, - u7: int | str | object | bytes | Any, -) -> None: - reveal_type(u1) # revealed: object - reveal_type(u2) # revealed: object - reveal_type(u3) # revealed: object - reveal_type(u4) # revealed: object - reveal_type(u5) # revealed: object - reveal_type(u6) # revealed: object - reveal_type(u7) # revealed: object -``` - -## Flattening of nested unions - -```py -from typing import Literal - -def _( - u1: (int | str) | bytes, - u2: int | (str | bytes), - u3: int | (str | (bytes | bytearray)), -) -> None: - reveal_type(u1) # revealed: int | str | bytes - reveal_type(u2) # revealed: int | str | bytes - reveal_type(u3) # revealed: int | str | bytes | bytearray -``` - -## Simplification using subtyping - -The type `S | T` can be simplified to `T` if `S` is a subtype of `T`: - -```py -from typing_extensions import Literal, LiteralString - -def _( - u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString -) -> None: - reveal_type(u1) # revealed: str - reveal_type(u2) # revealed: str - reveal_type(u3) # revealed: str - reveal_type(u4) # revealed: str | bytes -``` - -## Boolean literals - -The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`: - -```py -from typing import Literal - -def _( - u1: Literal[True, False], - u2: bool | Literal[True], - u3: Literal[True] | bool, - u4: Literal[True] | Literal[True, 17], - u5: Literal[True, False, True, 17], -) -> None: - reveal_type(u1) # revealed: bool - reveal_type(u2) # revealed: bool - reveal_type(u3) # revealed: bool - reveal_type(u4) # revealed: Literal[True, 17] - reveal_type(u5) # revealed: bool | Literal[17] -``` - -## Do not erase `Unknown` - -```py -from knot_extensions import Unknown - -def _(u1: Unknown | str, u2: str | Unknown) -> None: - reveal_type(u1) # revealed: Unknown | str - reveal_type(u2) # revealed: str | Unknown -``` - -## Collapse multiple `Unknown`s - -Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a -union are still redundant: - -```py -from knot_extensions import Unknown - -def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: - reveal_type(u1) # revealed: Unknown | str - reveal_type(u2) # revealed: Unknown | str - reveal_type(u3) # revealed: str | Unknown -``` - -## Subsume multiple elements - -Simplifications still apply when `Unknown` is present. - -```py -from knot_extensions import Unknown - -def _(u1: int | Unknown | bool) -> None: - reveal_type(u1) # revealed: int | Unknown -``` - -## Union of intersections - -We can simplify unions of intersections: - -```py -from knot_extensions import Intersection, Not - -class P: ... -class Q: ... - -def _( - i1: Intersection[P, Q] | Intersection[P, Q], - i2: Intersection[P, Q] | Intersection[Q, P], -) -> None: - reveal_type(i1) # revealed: P & Q - reveal_type(i2) # revealed: P & Q -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md deleted file mode 100644 index 50a7a64388d3b..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md +++ /dev/null @@ -1,710 +0,0 @@ -# Unpacking - -If there are not enough or too many values ​​when unpacking, an error will occur and the types of -all variables (if nested tuple unpacking fails, only the variables within the failed tuples) is -inferred to be `Unknown`. - -## Tuple - -### Simple tuple - -```py -(a, b, c) = (1, 2, 3) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[2] -reveal_type(c) # revealed: Literal[3] -``` - -### Simple list - -```py -[a, b, c] = (1, 2, 3) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[2] -reveal_type(c) # revealed: Literal[3] -``` - -### Simple mixed - -```py -[a, (b, c), d] = (1, (2, 3), 4) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[2] -reveal_type(c) # revealed: Literal[3] -reveal_type(d) # revealed: Literal[4] -``` - -### Multiple assignment - -```py -a, b = c = 1, 2 -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[2] -reveal_type(c) # revealed: tuple[Literal[1], Literal[2]] -``` - -### Nested tuple with unpacking - -```py -(a, (b, c), d) = (1, (2, 3), 4) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[2] -reveal_type(c) # revealed: Literal[3] -reveal_type(d) # revealed: Literal[4] -``` - -### Nested tuple without unpacking - -```py -(a, b, c) = (1, (2, 3), 4) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: tuple[Literal[2], Literal[3]] -reveal_type(c) # revealed: Literal[4] -``` - -### Uneven unpacking (1) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" -(a, b, c) = (1, 2) -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -``` - -### Uneven unpacking (2) - -```py -# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" -(a, b) = (1, 2, 3) -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -``` - -### Nested uneven unpacking (1) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" -(a, (b, c), d) = (1, (2,), 3) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: Literal[3] -``` - -### Nested uneven unpacking (2) - -```py -# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" -(a, (b, c), d) = (1, (2, 3, 4), 5) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: Literal[5] -``` - -### Starred expression (1) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)" -[a, *b, c, d] = (1, 2) -reveal_type(a) # revealed: Unknown -# TODO: Should be list[Any] once support for assigning to starred expression is added -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: Unknown -``` - -### Starred expression (2) - -```py -[a, *b, c] = (1, 2) -reveal_type(a) # revealed: Literal[1] -# TODO: Should be list[Any] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo(starred unpacking) -reveal_type(c) # revealed: Literal[2] -``` - -### Starred expression (3) - -```py -[a, *b, c] = (1, 2, 3) -reveal_type(a) # revealed: Literal[1] -# TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo(starred unpacking) -reveal_type(c) # revealed: Literal[3] -``` - -### Starred expression (4) - -```py -[a, *b, c, d] = (1, 2, 3, 4, 5, 6) -reveal_type(a) # revealed: Literal[1] -# TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo(starred unpacking) -reveal_type(c) # revealed: Literal[5] -reveal_type(d) # revealed: Literal[6] -``` - -### Starred expression (5) - -```py -[a, b, *c] = (1, 2, 3, 4) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: Literal[2] -# TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(c) # revealed: @Todo(starred unpacking) -``` - -### Starred expression (6) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)" -(a, b, c, *d, e, f) = (1,) -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: Unknown -reveal_type(e) # revealed: Unknown -reveal_type(f) # revealed: Unknown -``` - -### Non-iterable unpacking - -```py -# error: "Object of type `Literal[1]` is not iterable" -a, b = 1 -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -``` - -### Custom iterator unpacking - -```py -class Iterator: - def __next__(self) -> int: - return 42 - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -(a, b) = Iterable() -reveal_type(a) # revealed: int -reveal_type(b) # revealed: int -``` - -### Custom iterator unpacking nested - -```py -class Iterator: - def __next__(self) -> int: - return 42 - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -(a, (b, c), d) = (1, Iterable(), 2) -reveal_type(a) # revealed: Literal[1] -reveal_type(b) # revealed: int -reveal_type(c) # revealed: int -reveal_type(d) # revealed: Literal[2] -``` - -## String - -### Simple unpacking - -```py -a, b = "ab" -reveal_type(a) # revealed: LiteralString -reveal_type(b) # revealed: LiteralString -``` - -### Uneven unpacking (1) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" -a, b, c = "ab" -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -``` - -### Uneven unpacking (2) - -```py -# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" -a, b = "abc" -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -``` - -### Starred expression (1) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)" -(a, *b, c, d) = "ab" -reveal_type(a) # revealed: Unknown -# TODO: Should be list[LiteralString] once support for assigning to starred expression is added -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: Unknown -``` - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)" -(a, b, *c, d) = "a" -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: Unknown -``` - -### Starred expression (2) - -```py -(a, *b, c) = "ab" -reveal_type(a) # revealed: LiteralString -# TODO: Should be list[Any] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo(starred unpacking) -reveal_type(c) # revealed: LiteralString -``` - -### Starred expression (3) - -```py -(a, *b, c) = "abc" -reveal_type(a) # revealed: LiteralString -# TODO: Should be list[LiteralString] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo(starred unpacking) -reveal_type(c) # revealed: LiteralString -``` - -### Starred expression (4) - -```py -(a, *b, c, d) = "abcdef" -reveal_type(a) # revealed: LiteralString -# TODO: Should be list[LiteralString] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo(starred unpacking) -reveal_type(c) # revealed: LiteralString -reveal_type(d) # revealed: LiteralString -``` - -### Starred expression (5) - -```py -(a, b, *c) = "abcd" -reveal_type(a) # revealed: LiteralString -reveal_type(b) # revealed: LiteralString -# TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(c) # revealed: @Todo(starred unpacking) -``` - -### Unicode - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" -(a, b) = "é" - -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -``` - -### Unicode escape (1) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" -(a, b) = "\u9e6c" - -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -``` - -### Unicode escape (2) - -```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" -(a, b) = "\U0010ffff" - -reveal_type(a) # revealed: Unknown -reveal_type(b) # revealed: Unknown -``` - -### Surrogates - -```py -(a, b) = "\ud800\udfff" - -reveal_type(a) # revealed: LiteralString -reveal_type(b) # revealed: LiteralString -``` - -## Union - -### Same types - -Union of two tuples of equal length and each element is of the same type. - -```py -def _(arg: tuple[int, int] | tuple[int, int]): - (a, b) = arg - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int -``` - -### Mixed types (1) - -Union of two tuples of equal length and one element differs in its type. - -```py -def _(arg: tuple[int, int] | tuple[int, str]): - a, b = arg - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int | str -``` - -### Mixed types (2) - -Union of two tuples of equal length and both the element types are different. - -```py -def _(arg: tuple[int, str] | tuple[str, int]): - a, b = arg - reveal_type(a) # revealed: int | str - reveal_type(b) # revealed: str | int -``` - -### Mixed types (3) - -Union of three tuples of equal length and various combination of element types: - -1. All same types -1. One different type -1. All different types - -```py -def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str]): - a, b, c = arg - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int | str - reveal_type(c) # revealed: int | bytes | str -``` - -### Nested - -```py -from typing import Literal - -def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]): - a, (b, c) = arg - reveal_type(a) # revealed: int | tuple[int, bytes] - reveal_type(b) # revealed: str - reveal_type(c) # revealed: bytes | LiteralString -``` - -### Starred expression - -```py -def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): - a, *b, c = arg - reveal_type(a) # revealed: int - # TODO: Should be `list[bytes | int | str]` - reveal_type(b) # revealed: @Todo(starred unpacking) - reveal_type(c) # revealed: int | bytes -``` - -### Size mismatch (1) - -```py -def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): - # error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" - # error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)" - a, b = arg - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown -``` - -### Size mismatch (2) - -```py -def _(arg: tuple[int, bytes] | tuple[int, str]): - # error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" - # error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" - a, b, c = arg - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown -``` - -### Same literal types - -```py -def _(flag: bool): - if flag: - value = (1, 2) - else: - value = (3, 4) - - a, b = value - reveal_type(a) # revealed: Literal[1, 3] - reveal_type(b) # revealed: Literal[2, 4] -``` - -### Mixed literal types - -```py -def _(flag: bool): - if flag: - value = (1, 2) - else: - value = ("a", "b") - - a, b = value - reveal_type(a) # revealed: Literal[1, "a"] - reveal_type(b) # revealed: Literal[2, "b"] -``` - -### Typing literal - -```py -from typing import Literal - -def _(arg: tuple[int, int] | Literal["ab"]): - a, b = arg - reveal_type(a) # revealed: int | LiteralString - reveal_type(b) # revealed: int | LiteralString -``` - -### Custom iterator (1) - -```py -class Iterator: - def __next__(self) -> tuple[int, int] | tuple[int, str]: - return (1, 2) - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -((a, b), c) = Iterable() -reveal_type(a) # revealed: int -reveal_type(b) # revealed: int | str -reveal_type(c) # revealed: tuple[int, int] | tuple[int, str] -``` - -### Custom iterator (2) - -```py -class Iterator: - def __next__(self) -> bytes: - return b"" - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -def _(arg: tuple[int, str] | Iterable): - a, b = arg - reveal_type(a) # revealed: int | bytes - reveal_type(b) # revealed: str | bytes -``` - -## For statement - -Unpacking in a `for` statement. - -### Same types - -```py -def _(arg: tuple[tuple[int, int], tuple[int, int]]): - for a, b in arg: - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int -``` - -### Mixed types (1) - -```py -def _(arg: tuple[tuple[int, int], tuple[int, str]]): - for a, b in arg: - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int | str -``` - -### Mixed types (2) - -```py -def _(arg: tuple[tuple[int, str], tuple[str, int]]): - for a, b in arg: - reveal_type(a) # revealed: int | str - reveal_type(b) # revealed: str | int -``` - -### Mixed types (3) - -```py -def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]): - for a, b, c in arg: - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int | str - reveal_type(c) # revealed: int | bytes | str -``` - -### Same literal values - -```py -for a, b in ((1, 2), (3, 4)): - reveal_type(a) # revealed: Literal[1, 3] - reveal_type(b) # revealed: Literal[2, 4] -``` - -### Mixed literal values (1) - -```py -for a, b in ((1, 2), ("a", "b")): - reveal_type(a) # revealed: Literal[1, "a"] - reveal_type(b) # revealed: Literal[2, "b"] -``` - -### Mixed literals values (2) - -```py -# error: "Object of type `Literal[1]` is not iterable" -# error: "Object of type `Literal[2]` is not iterable" -# error: "Object of type `Literal[4]` is not iterable" -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" -for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"): - reveal_type(a) # revealed: Unknown | Literal[3, 5] - reveal_type(b) # revealed: Unknown | Literal["a", "b"] -``` - -### Custom iterator (1) - -```py -class Iterator: - def __next__(self) -> tuple[int, int]: - return (1, 2) - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -for a, b in Iterable(): - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int -``` - -### Custom iterator (2) - -```py -class Iterator: - def __next__(self) -> bytes: - return b"" - -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - -def _(arg: tuple[tuple[int, str], Iterable]): - for a, b in arg: - reveal_type(a) # revealed: int | bytes - reveal_type(b) # revealed: str | bytes -``` - -## With statement - -Unpacking in a `with` statement. - -### Same types - -```py -class ContextManager: - def __enter__(self) -> tuple[int, int]: - return (1, 2) - - def __exit__(self, exc_type, exc_value, traceback) -> None: - pass - -with ContextManager() as (a, b): - reveal_type(a) # revealed: int - reveal_type(b) # revealed: int -``` - -### Mixed types - -```py -class ContextManager: - def __enter__(self) -> tuple[int, str]: - return (1, "a") - - def __exit__(self, exc_type, exc_value, traceback) -> None: - pass - -with ContextManager() as (a, b): - reveal_type(a) # revealed: int - reveal_type(b) # revealed: str -``` - -### Nested - -```py -class ContextManager: - def __enter__(self) -> tuple[int, tuple[str, bytes]]: - return (1, ("a", b"bytes")) - - def __exit__(self, exc_type, exc_value, traceback) -> None: - pass - -with ContextManager() as (a, (b, c)): - reveal_type(a) # revealed: int - reveal_type(b) # revealed: str - reveal_type(c) # revealed: bytes -``` - -### Starred expression - -```py -class ContextManager: - def __enter__(self) -> tuple[int, int, int]: - return (1, 2, 3) - - def __exit__(self, exc_type, exc_value, traceback) -> None: - pass - -with ContextManager() as (a, *b): - reveal_type(a) # revealed: int - # TODO: Should be list[int] once support for assigning to starred expression is added - reveal_type(b) # revealed: @Todo(starred unpacking) -``` - -### Unbound context manager expression - -```py -# TODO: should only be one diagnostic -# error: [unresolved-reference] "Name `nonexistant` used when not defined" -# error: [unresolved-reference] "Name `nonexistant` used when not defined" -# error: [unresolved-reference] "Name `nonexistant` used when not defined" -with nonexistant as (x, y): - reveal_type(x) # revealed: Unknown - reveal_type(y) # revealed: Unknown -``` - -### Invalid unpacking - -```py -class ContextManager: - def __enter__(self) -> tuple[int, str]: - return (1, "a") - - def __exit__(self, *args) -> None: - pass - -# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" -with ContextManager() as (a, b, c): - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md deleted file mode 100644 index b180d6f55c4b9..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md +++ /dev/null @@ -1,151 +0,0 @@ -# With statements - -## Basic `with` statement - -The type of the target variable in a `with` statement is the return type from the context manager's -`__enter__` method. - -```py -class Target: ... - -class Manager: - def __enter__(self) -> Target: - return Target() - - def __exit__(self, exc_type, exc_value, traceback): ... - -with Manager() as f: - reveal_type(f) # revealed: Target -``` - -## Union context manager - -```py -def _(flag: bool): - class Manager1: - def __enter__(self) -> str: - return "foo" - - def __exit__(self, exc_type, exc_value, traceback): ... - - class Manager2: - def __enter__(self) -> int: - return 42 - - def __exit__(self, exc_type, exc_value, traceback): ... - - context_expr = Manager1() if flag else Manager2() - - with context_expr as f: - reveal_type(f) # revealed: str | int -``` - -## Context manager without an `__enter__` or `__exit__` method - -```py -class Manager: ... - -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" -with Manager(): - ... -``` - -## Context manager without an `__enter__` method - -```py -class Manager: - def __exit__(self, exc_tpe, exc_value, traceback): ... - -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__`" -with Manager(): - ... -``` - -## Context manager without an `__exit__` method - -```py -class Manager: - def __enter__(self): ... - -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__exit__`" -with Manager(): - ... -``` - -## Context manager with non-callable `__enter__` attribute - -```py -class Manager: - __enter__: int = 42 - - def __exit__(self, exc_tpe, exc_value, traceback): ... - -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" -with Manager(): - ... -``` - -## Context manager with non-callable `__exit__` attribute - -```py -from typing_extensions import Self - -class Manager: - def __enter__(self) -> Self: - return self - __exit__: int = 32 - -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`" -with Manager(): - ... -``` - -## Context expression with possibly-unbound union variants - -```py -def _(flag: bool): - class Manager1: - def __enter__(self) -> str: - return "foo" - - def __exit__(self, exc_type, exc_value, traceback): ... - - class NotAContextManager: ... - context_expr = Manager1() if flag else NotAContextManager() - - # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound" - with context_expr as f: - reveal_type(f) # revealed: str -``` - -## Context expression with "sometimes" callable `__enter__` method - -```py -def _(flag: bool): - class Manager: - if flag: - def __enter__(self) -> str: - return "abcd" - - def __exit__(self, *args): ... - - # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound" - with Manager() as f: - reveal_type(f) # revealed: str -``` - -## Invalid `__enter__` signature - -```py -class Manager: - def __enter__() -> str: - return "foo" - - def __exit__(self, exc_type, exc_value, traceback): ... - -context_expr = Manager() - -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" -with context_expr as f: - reveal_type(f) # revealed: str -``` diff --git a/crates/red_knot_python_semantic/src/ast_node_ref.rs b/crates/red_knot_python_semantic/src/ast_node_ref.rs deleted file mode 100644 index 692191b5b715b..0000000000000 --- a/crates/red_knot_python_semantic/src/ast_node_ref.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::hash::Hash; -use std::ops::Deref; - -use ruff_db::parsed::ParsedModule; - -/// Ref-counted owned reference to an AST node. -/// -/// The type holds an owned reference to the node's ref-counted [`ParsedModule`]. -/// Holding on to the node's [`ParsedModule`] guarantees that the reference to the -/// node must still be valid. -/// -/// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released. -/// -/// ## Equality -/// Two `AstNodeRef` are considered equal if their pointer addresses are equal. -/// -/// ## 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 -/// 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` -/// has the effect that salsa will consider the expression as "unchanged" for as long as it: -/// -/// * belongs to the same file -/// * belongs to the same scope -/// * has the same kind -/// * was created in the same order -/// -/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving -/// us some form of scope-stable identity for expressions. Only queries accessing the node field -/// run on every AST change. All other queries only run when the expression's identity changes. -#[derive(Clone)] -pub struct AstNodeRef { - /// Owned reference to the node's [`ParsedModule`]. - /// - /// The node's reference is guaranteed to remain valid as long as it's enclosing - /// [`ParsedModule`] is alive. - parsed: ParsedModule, - - /// Pointer to the referenced node. - node: std::ptr::NonNull, -} - -#[allow(unsafe_code)] -impl AstNodeRef { - /// Creates a new `AstNodeRef` that references `node`. The `parsed` is the [`ParsedModule`] to - /// which the `AstNodeRef` belongs. - /// - /// ## Safety - /// - /// Dereferencing the `node` can result in undefined behavior if `parsed` isn't the - /// [`ParsedModule`] to which `node` belongs. It's the caller's responsibility to ensure that - /// the invariant `node belongs to parsed` is upheld. - pub(super) unsafe fn new(parsed: ParsedModule, node: &T) -> Self { - Self { - parsed, - node: std::ptr::NonNull::from(node), - } - } - - /// Returns a reference to the wrapped node. - pub const fn node(&self) -> &T { - // SAFETY: Holding on to `parsed` ensures that the AST to which `node` belongs is still - // alive and not moved. - unsafe { self.node.as_ref() } - } -} - -impl Deref for AstNodeRef { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.node() - } -} - -impl std::fmt::Debug for AstNodeRef -where - T: std::fmt::Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("AstNodeRef").field(&self.node()).finish() - } -} - -impl PartialEq for AstNodeRef -where - T: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - if self.parsed == other.parsed { - // Comparing the pointer addresses is sufficient to determine equality - // if the parsed are the same. - self.node.eq(&other.node) - } else { - // Otherwise perform a deep comparison. - self.node().eq(other.node()) - } - } -} - -impl Eq for AstNodeRef where T: Eq {} - -impl Hash for AstNodeRef -where - T: Hash, -{ - fn hash(&self, state: &mut H) { - self.node().hash(state); - } -} - -#[allow(unsafe_code)] -unsafe impl salsa::Update for AstNodeRef { - unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { - let old_ref = &mut (*old_pointer); - - if old_ref.parsed == new_value.parsed && old_ref.node.eq(&new_value.node) { - false - } else { - *old_ref = new_value; - true - } - } -} - -#[allow(unsafe_code)] -unsafe impl Send for AstNodeRef where T: Send {} -#[allow(unsafe_code)] -unsafe impl Sync for AstNodeRef where T: Sync {} - -#[cfg(test)] -mod tests { - use crate::ast_node_ref::AstNodeRef; - use ruff_db::parsed::ParsedModule; - use ruff_python_ast::PySourceType; - use ruff_python_parser::parse_unchecked_source; - - #[test] - #[allow(unsafe_code)] - fn equality() { - let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python); - let parsed = ParsedModule::new(parsed_raw.clone()); - - let stmt = &parsed.syntax().body[0]; - - let node1 = unsafe { AstNodeRef::new(parsed.clone(), stmt) }; - let node2 = unsafe { AstNodeRef::new(parsed.clone(), stmt) }; - - assert_eq!(node1, node2); - - // Compare from different trees - let cloned = ParsedModule::new(parsed_raw); - let stmt_cloned = &cloned.syntax().body[0]; - let cloned_node = unsafe { AstNodeRef::new(cloned.clone(), stmt_cloned) }; - - assert_eq!(node1, cloned_node); - - let other_raw = parse_unchecked_source("2 + 2", PySourceType::Python); - let other = ParsedModule::new(other_raw); - - let other_stmt = &other.syntax().body[0]; - let other_node = unsafe { AstNodeRef::new(other.clone(), other_stmt) }; - - assert_ne!(node1, other_node); - } - - #[allow(unsafe_code)] - #[test] - fn inequality() { - let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python); - let parsed = ParsedModule::new(parsed_raw); - - let stmt = &parsed.syntax().body[0]; - let node = unsafe { AstNodeRef::new(parsed.clone(), stmt) }; - - let other_raw = parse_unchecked_source("2 + 2", PySourceType::Python); - let other = ParsedModule::new(other_raw); - - let other_stmt = &other.syntax().body[0]; - let other_node = unsafe { AstNodeRef::new(other.clone(), other_stmt) }; - - assert_ne!(node, other_node); - } - - #[test] - #[allow(unsafe_code)] - fn debug() { - let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python); - let parsed = ParsedModule::new(parsed_raw); - - let stmt = &parsed.syntax().body[0]; - - let stmt_node = unsafe { AstNodeRef::new(parsed.clone(), stmt) }; - - let debug = format!("{stmt_node:?}"); - - assert_eq!(debug, format!("AstNodeRef({stmt:?})")); - } -} diff --git a/crates/red_knot_python_semantic/src/db.rs b/crates/red_knot_python_semantic/src/db.rs deleted file mode 100644 index 976235f926111..0000000000000 --- a/crates/red_knot_python_semantic/src/db.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::sync::Arc; - -use crate::lint::{LintRegistry, RuleSelection}; -use ruff_db::files::File; -use ruff_db::{Db as SourceDb, Upcast}; - -/// Database giving access to semantic information about a Python program. -#[salsa::db] -pub trait Db: SourceDb + Upcast { - fn is_file_open(&self, file: File) -> bool; - - fn rule_selection(&self) -> Arc; - - fn lint_registry(&self) -> &LintRegistry; -} - -#[cfg(test)] -pub(crate) mod tests { - use std::sync::Arc; - - use crate::program::{Program, SearchPathSettings}; - use crate::{default_lint_registry, ProgramSettings, PythonPlatform}; - - use super::Db; - use crate::lint::{LintRegistry, RuleSelection}; - use anyhow::Context; - use ruff_db::files::{File, Files}; - use ruff_db::system::{ - DbWithTestSystem, DbWithWritableSystem as _, System, SystemPath, SystemPathBuf, TestSystem, - }; - use ruff_db::vendored::VendoredFileSystem; - use ruff_db::{Db as SourceDb, Upcast}; - use ruff_python_ast::PythonVersion; - - #[salsa::db] - #[derive(Clone)] - pub(crate) struct TestDb { - storage: salsa::Storage, - files: Files, - system: TestSystem, - vendored: VendoredFileSystem, - events: Arc>>, - rule_selection: Arc, - } - - impl TestDb { - pub(crate) fn new() -> Self { - Self { - storage: salsa::Storage::default(), - system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), - events: Arc::default(), - files: Files::default(), - rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), - } - } - - /// Takes the salsa events. - /// - /// ## Panics - /// If there are any pending salsa snapshots. - pub(crate) fn take_salsa_events(&mut self) -> Vec { - let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots"); - - let events = inner.get_mut().unwrap(); - std::mem::take(&mut *events) - } - - /// Clears the salsa events. - /// - /// ## Panics - /// If there are any pending salsa snapshots. - pub(crate) fn clear_salsa_events(&mut self) { - self.take_salsa_events(); - } - } - - impl DbWithTestSystem for TestDb { - fn test_system(&self) -> &TestSystem { - &self.system - } - - fn test_system_mut(&mut self) -> &mut TestSystem { - &mut self.system - } - } - - #[salsa::db] - impl SourceDb for TestDb { - fn vendored(&self) -> &VendoredFileSystem { - &self.vendored - } - - fn system(&self) -> &dyn System { - &self.system - } - - fn files(&self) -> &Files { - &self.files - } - } - - impl Upcast for TestDb { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } - } - - #[salsa::db] - impl Db for TestDb { - fn is_file_open(&self, file: File) -> bool { - !file.path(self).is_vendored_path() - } - - fn rule_selection(&self) -> Arc { - self.rule_selection.clone() - } - - fn lint_registry(&self) -> &LintRegistry { - default_lint_registry() - } - } - - #[salsa::db] - impl salsa::Database for TestDb { - fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) { - let event = event(); - tracing::trace!("event: {event:?}"); - let mut events = self.events.lock().unwrap(); - events.push(event); - } - } - - pub(crate) struct TestDbBuilder<'a> { - /// Target Python version - python_version: PythonVersion, - /// Target Python platform - python_platform: PythonPlatform, - /// Path and content pairs for files that should be present - files: Vec<(&'a str, &'a str)>, - } - - impl<'a> TestDbBuilder<'a> { - pub(crate) fn new() -> Self { - Self { - python_version: PythonVersion::default(), - python_platform: PythonPlatform::default(), - files: vec![], - } - } - - pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self { - self.python_version = version; - self - } - - pub(crate) fn with_file( - mut self, - path: &'a (impl AsRef + ?Sized), - content: &'a str, - ) -> Self { - self.files.push((path.as_ref().as_str(), content)); - self - } - - pub(crate) fn build(self) -> anyhow::Result { - let mut db = TestDb::new(); - - let src_root = SystemPathBuf::from("/src"); - db.memory_file_system().create_directory_all(&src_root)?; - - db.write_files(self.files) - .context("Failed to write test files")?; - - Program::from_settings( - &db, - ProgramSettings { - python_version: self.python_version, - python_platform: self.python_platform, - search_paths: SearchPathSettings::new(vec![src_root]), - }, - ) - .context("Failed to configure Program settings")?; - - Ok(db) - } - } - - pub(crate) fn setup_db() -> TestDb { - TestDbBuilder::new().build().expect("valid TestDb setup") - } -} diff --git a/crates/red_knot_python_semantic/src/lib.rs b/crates/red_knot_python_semantic/src/lib.rs deleted file mode 100644 index fd3b35e4df729..0000000000000 --- a/crates/red_knot_python_semantic/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::hash::BuildHasherDefault; - -use rustc_hash::FxHasher; - -use crate::lint::{LintRegistry, LintRegistryBuilder}; -use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COMMENT}; -pub use db::Db; -pub use module_name::ModuleName; -pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module}; -pub use program::{Program, ProgramSettings, PythonPath, SearchPathSettings}; -pub use python_platform::PythonPlatform; -pub use semantic_model::{HasType, SemanticModel}; -pub use site_packages::SysPrefixPathOrigin; - -pub mod ast_node_ref; -mod db; -pub mod lint; -pub(crate) mod list; -mod module_name; -mod module_resolver; -mod node_key; -mod program; -mod python_platform; -pub mod semantic_index; -mod semantic_model; -pub(crate) mod site_packages; -mod suppression; -pub(crate) mod symbol; -pub mod types; -mod unpack; -mod util; - -type FxOrderSet = ordermap::set::OrderSet>; - -/// Returns the default registry with all known semantic lints. -pub fn default_lint_registry() -> &'static LintRegistry { - static REGISTRY: std::sync::LazyLock = std::sync::LazyLock::new(|| { - let mut registry = LintRegistryBuilder::default(); - register_lints(&mut registry); - registry.build() - }); - - ®ISTRY -} - -/// Register all known semantic lints. -pub fn register_lints(registry: &mut LintRegistryBuilder) { - types::register_lints(registry); - registry.register_lint(&UNUSED_IGNORE_COMMENT); - registry.register_lint(&UNKNOWN_RULE); - registry.register_lint(&INVALID_IGNORE_COMMENT); -} diff --git a/crates/red_knot_python_semantic/src/module_resolver/mod.rs b/crates/red_knot_python_semantic/src/module_resolver/mod.rs deleted file mode 100644 index 53b68c7c8c831..0000000000000 --- a/crates/red_knot_python_semantic/src/module_resolver/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::iter::FusedIterator; - -pub use module::{KnownModule, Module}; -pub use resolver::resolve_module; -pub(crate) use resolver::{file_to_module, SearchPaths}; -use ruff_db::system::SystemPath; - -use crate::module_resolver::resolver::search_paths; -use crate::Db; -use resolver::SearchPathIterator; - -mod module; -mod path; -mod resolver; -mod typeshed; - -#[cfg(test)] -mod testing; - -/// Returns an iterator over all search paths pointing to a system path -pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter { - SystemModuleSearchPathsIter { - inner: search_paths(db), - } -} - -pub struct SystemModuleSearchPathsIter<'db> { - inner: SearchPathIterator<'db>, -} - -impl<'db> Iterator for SystemModuleSearchPathsIter<'db> { - type Item = &'db SystemPath; - - fn next(&mut self) -> Option { - loop { - let next = self.inner.next()?; - - if let Some(system_path) = next.as_system_path() { - return Some(system_path); - } - } - } -} - -impl FusedIterator for SystemModuleSearchPathsIter<'_> {} diff --git a/crates/red_knot_python_semantic/src/module_resolver/module.rs b/crates/red_knot_python_semantic/src/module_resolver/module.rs deleted file mode 100644 index afcc6687bac21..0000000000000 --- a/crates/red_knot_python_semantic/src/module_resolver/module.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::fmt::Formatter; -use std::str::FromStr; -use std::sync::Arc; - -use ruff_db::files::File; - -use super::path::SearchPath; -use crate::module_name::ModuleName; - -/// Representation of a Python module. -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct Module { - inner: Arc, -} - -impl Module { - pub(crate) fn new( - name: ModuleName, - kind: ModuleKind, - search_path: SearchPath, - file: File, - ) -> Self { - let known = KnownModule::try_from_search_path_and_name(&search_path, &name); - Self { - inner: Arc::new(ModuleInner { - name, - kind, - search_path, - file, - known, - }), - } - } - - /// The absolute name of the module (e.g. `foo.bar`) - pub fn name(&self) -> &ModuleName { - &self.inner.name - } - - /// The file to the source code that defines this module - pub fn file(&self) -> File { - self.inner.file - } - - /// Is this a module that we special-case somehow? If so, which one? - pub fn known(&self) -> Option { - self.inner.known - } - - /// Does this module represent the given known module? - pub fn is_known(&self, known_module: KnownModule) -> bool { - self.known() == Some(known_module) - } - - /// The search path from which the module was resolved. - pub(crate) fn search_path(&self) -> &SearchPath { - &self.inner.search_path - } - - /// Determine whether this module is a single-file module or a package - pub fn kind(&self) -> ModuleKind { - self.inner.kind - } -} - -impl std::fmt::Debug for Module { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Module") - .field("name", &self.name()) - .field("kind", &self.kind()) - .field("file", &self.file()) - .field("search_path", &self.search_path()) - .finish() - } -} - -#[derive(PartialEq, Eq, Hash)] -struct ModuleInner { - name: ModuleName, - kind: ModuleKind, - search_path: SearchPath, - file: File, - known: Option, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub enum ModuleKind { - /// A single-file module (e.g. `foo.py` or `foo.pyi`) - Module, - - /// A python package (`foo/__init__.py` or `foo/__init__.pyi`) - Package, -} - -impl ModuleKind { - pub const fn is_package(self) -> bool { - matches!(self, ModuleKind::Package) - } - pub const fn is_module(self) -> bool { - matches!(self, ModuleKind::Module) - } -} - -/// Enumeration of various core stdlib modules in which important types are located -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum_macros::EnumString)] -#[cfg_attr(test, derive(strum_macros::EnumIter))] -#[strum(serialize_all = "snake_case")] -pub enum KnownModule { - Builtins, - Enum, - Types, - #[strum(serialize = "_typeshed")] - Typeshed, - TypingExtensions, - Typing, - Sys, - #[allow(dead_code)] - Abc, // currently only used in tests - Dataclasses, - Collections, - Inspect, - KnotExtensions, -} - -impl KnownModule { - pub const fn as_str(self) -> &'static str { - match self { - Self::Builtins => "builtins", - Self::Enum => "enum", - Self::Types => "types", - Self::Typing => "typing", - Self::Typeshed => "_typeshed", - Self::TypingExtensions => "typing_extensions", - Self::Sys => "sys", - Self::Abc => "abc", - Self::Dataclasses => "dataclasses", - Self::Collections => "collections", - Self::Inspect => "inspect", - Self::KnotExtensions => "knot_extensions", - } - } - - pub fn name(self) -> ModuleName { - ModuleName::new_static(self.as_str()) - .unwrap_or_else(|| panic!("{self} should be a valid module name!")) - } - - pub(crate) fn try_from_search_path_and_name( - search_path: &SearchPath, - name: &ModuleName, - ) -> Option { - if search_path.is_standard_library() { - Self::from_str(name.as_str()).ok() - } else { - None - } - } - - pub const fn is_builtins(self) -> bool { - matches!(self, Self::Builtins) - } - - pub const fn is_typing(self) -> bool { - matches!(self, Self::Typing) - } - - pub const fn is_knot_extensions(self) -> bool { - matches!(self, Self::KnotExtensions) - } - - pub const fn is_inspect(self) -> bool { - matches!(self, Self::Inspect) - } - - pub const fn is_enum(self) -> bool { - matches!(self, Self::Enum) - } -} - -impl std::fmt::Display for KnownModule { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use strum::IntoEnumIterator; - - #[test] - fn known_module_roundtrip_from_str() { - let stdlib_search_path = SearchPath::vendored_stdlib(); - - for module in KnownModule::iter() { - let module_name = module.name(); - - assert_eq!( - KnownModule::try_from_search_path_and_name(&stdlib_search_path, &module_name), - Some(module), - "The strum `EnumString` implementation appears to be incorrect for `{module_name}`" - ); - } - } -} diff --git a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs b/crates/red_knot_python_semantic/src/module_resolver/resolver.rs deleted file mode 100644 index 37fb4d1d40fc7..0000000000000 --- a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs +++ /dev/null @@ -1,2100 +0,0 @@ -use std::borrow::Cow; -use std::fmt; -use std::iter::FusedIterator; -use std::str::Split; - -use compact_str::format_compact; -use rustc_hash::{FxBuildHasher, FxHashSet}; - -use ruff_db::files::{File, FilePath, FileRootKind}; -use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf}; -use ruff_db::vendored::{VendoredFileSystem, VendoredPath}; -use ruff_python_ast::PythonVersion; - -use crate::db::Db; -use crate::module_name::ModuleName; -use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions}; -use crate::site_packages::{SitePackagesDiscoveryError, SysPrefixPathOrigin, VirtualEnvironment}; -use crate::{Program, PythonPath, SearchPathSettings}; - -use super::module::{Module, ModuleKind}; -use super::path::{ModulePath, SearchPath, SearchPathValidationError}; - -/// Resolves a module name to a module. -pub fn resolve_module(db: &dyn Db, module_name: &ModuleName) -> Option { - let interned_name = ModuleNameIngredient::new(db, module_name); - - resolve_module_query(db, interned_name) -} - -/// Salsa query that resolves an interned [`ModuleNameIngredient`] to a module. -/// -/// This query should not be called directly. Instead, use [`resolve_module`]. It only exists -/// because Salsa requires the module name to be an ingredient. -#[salsa::tracked] -pub(crate) fn resolve_module_query<'db>( - db: &'db dyn Db, - module_name: ModuleNameIngredient<'db>, -) -> Option { - let name = module_name.name(db); - let _span = tracing::trace_span!("resolve_module", %name).entered(); - - let Some((search_path, resolved_module)) = resolve_name(db, name) else { - tracing::debug!("Module `{name}` not found in search paths"); - return None; - }; - - tracing::trace!( - "Resolved module `{name}` to `{path}`", - path = resolved_module.file.path(db) - ); - - let module = Module::new( - name.clone(), - resolved_module.kind, - search_path, - resolved_module.file, - ); - - Some(module) -} - -/// Resolves the module for the given path. -/// -/// Returns `None` if the path is not a module locatable via any of the known search paths. -#[allow(unused)] -pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option { - // It's not entirely clear on first sight why this method calls `file_to_module` instead of - // it being the other way round, considering that the first thing that `file_to_module` does - // is to retrieve the file's path. - // - // The reason is that `file_to_module` is a tracked Salsa query and salsa queries require that - // all arguments are Salsa ingredients (something stored in Salsa). `Path`s aren't salsa ingredients but - // `VfsFile` is. So what we do here is to retrieve the `path`'s `VfsFile` so that we can make - // use of Salsa's caching and invalidation. - let file = path.to_file(db.upcast())?; - file_to_module(db, file) -} - -#[derive(Debug, Clone, Copy)] -enum SystemOrVendoredPathRef<'a> { - System(&'a SystemPath), - Vendored(&'a VendoredPath), -} - -impl std::fmt::Display for SystemOrVendoredPathRef<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SystemOrVendoredPathRef::System(system) => system.fmt(f), - SystemOrVendoredPathRef::Vendored(vendored) => vendored.fmt(f), - } - } -} - -/// Resolves the module for the file with the given id. -/// -/// Returns `None` if the file is not a module locatable via any of the known search paths. -#[salsa::tracked] -pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option { - let _span = tracing::trace_span!("file_to_module", ?file).entered(); - - let path = match file.path(db.upcast()) { - FilePath::System(system) => SystemOrVendoredPathRef::System(system), - FilePath::Vendored(vendored) => SystemOrVendoredPathRef::Vendored(vendored), - FilePath::SystemVirtual(_) => return None, - }; - - let module_name = search_paths(db).find_map(|candidate| { - let relative_path = match path { - SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path), - SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path), - }?; - relative_path.to_module_name() - })?; - - // Resolve the module name to see if Python would resolve the name to the same path. - // If it doesn't, then that means that multiple modules have the same name in different - // root paths, but that the module corresponding to `path` is in a lower priority search path, - // in which case we ignore it. - let module = resolve_module(db, &module_name)?; - - if file.path(db) == module.file().path(db) { - Some(module) - } else { - // This path is for a module with the same name but with a different precedence. For example: - // ``` - // src/foo.py - // src/foo/__init__.py - // ``` - // The module name of `src/foo.py` is `foo`, but the module loaded by Python is `src/foo/__init__.py`. - // That means we need to ignore `src/foo.py` even though it resolves to the same module name. - None - } -} - -pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator { - Program::get(db).search_paths(db).iter(db) -} - -/// Searches for a `.venv` directory in `project_root` that contains a `pyvenv.cfg` file. -fn discover_venv_in(system: &dyn System, project_root: &SystemPath) -> Option { - let virtual_env_directory = project_root.join(".venv"); - - system - .is_file(&virtual_env_directory.join("pyvenv.cfg")) - .then_some(virtual_env_directory) -} - -#[derive(Debug, PartialEq, Eq)] -pub struct SearchPaths { - /// Search paths that have been statically determined purely from reading Ruff's configuration settings. - /// These shouldn't ever change unless the config settings themselves change. - static_paths: Vec, - - /// site-packages paths are not included in the above field: - /// if there are multiple site-packages paths, editable installations can appear - /// *between* the site-packages paths on `sys.path` at runtime. - /// That means we can't know where a second or third `site-packages` path should sit - /// in terms of module-resolution priority until we've discovered the editable installs - /// for the first `site-packages` path - site_packages: Vec, - - typeshed_versions: TypeshedVersions, -} - -impl SearchPaths { - /// Validate and normalize the raw settings given by the user - /// into settings we can use for module resolution - /// - /// This method also implements the typing spec's [module resolution order]. - /// - /// [module resolution order]: https://typing.python.org/en/latest/spec/distributing.html#import-resolution-ordering - pub(crate) fn from_settings( - db: &dyn Db, - settings: &SearchPathSettings, - ) -> Result { - fn canonicalize(path: &SystemPath, system: &dyn System) -> SystemPathBuf { - system - .canonicalize_path(path) - .unwrap_or_else(|_| path.to_path_buf()) - } - - let SearchPathSettings { - extra_paths, - src_roots, - custom_typeshed: typeshed, - python_path, - } = settings; - - let system = db.system(); - let files = db.files(); - - let mut static_paths = vec![]; - - for path in extra_paths { - let path = canonicalize(path, system); - files.try_add_root(db.upcast(), &path, FileRootKind::LibrarySearchPath); - tracing::debug!("Adding extra search-path '{path}'"); - - static_paths.push(SearchPath::extra(system, path)?); - } - - for src_root in src_roots { - tracing::debug!("Adding first-party search path '{src_root}'"); - static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?); - } - - let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed { - let typeshed = canonicalize(typeshed, system); - tracing::debug!("Adding custom-stdlib search path '{typeshed}'"); - - files.try_add_root(db.upcast(), &typeshed, FileRootKind::LibrarySearchPath); - - let versions_path = typeshed.join("stdlib/VERSIONS"); - - let versions_content = system.read_to_string(&versions_path).map_err(|error| { - SearchPathValidationError::FailedToReadVersionsFile { - path: versions_path, - error, - } - })?; - - let parsed: TypeshedVersions = versions_content.parse()?; - - let search_path = SearchPath::custom_stdlib(db, &typeshed)?; - - (parsed, search_path) - } else { - tracing::debug!("Using vendored stdlib"); - ( - vendored_typeshed_versions(db), - SearchPath::vendored_stdlib(), - ) - }; - - static_paths.push(stdlib_path); - - 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 - // venv is out of date. - VirtualEnvironment::new(sys_prefix, *origin, system) - .and_then(|venv| venv.site_packages_directories(system))? - } - - PythonPath::Discover(root) => { - tracing::debug!("Discovering virtual environment in `{root}`"); - let virtual_env_path = discover_venv_in(db.system(), root); - if let Some(virtual_env_path) = virtual_env_path { - tracing::debug!("Found `.venv` folder at `{}`", virtual_env_path); - - let handle_invalid_virtual_env = |error: SitePackagesDiscoveryError| { - tracing::debug!( - "Ignoring automatically detected virtual environment at `{}`: {}", - virtual_env_path, - error - ); - vec![] - }; - - match VirtualEnvironment::new( - virtual_env_path.clone(), - SysPrefixPathOrigin::LocalVenv, - system, - ) { - Ok(venv) => venv - .site_packages_directories(system) - .unwrap_or_else(handle_invalid_virtual_env), - Err(error) => handle_invalid_virtual_env(error), - } - } else { - tracing::debug!("No virtual environment found"); - vec![] - } - } - - PythonPath::KnownSitePackages(paths) => paths - .iter() - .map(|path| canonicalize(path, system)) - .collect(), - }; - - let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len()); - - for path in site_packages_paths { - tracing::debug!("Adding site-packages search path '{path}'"); - files.try_add_root(db.upcast(), &path, FileRootKind::LibrarySearchPath); - site_packages.push(SearchPath::site_packages(system, path)?); - } - - // TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step - - // Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]). - // (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo` - // as module resolution paths simultaneously.) - // - // This code doesn't use an `IndexSet` because the key is the system path and not the search root. - // - // [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site - let mut seen_paths = FxHashSet::with_capacity_and_hasher(static_paths.len(), FxBuildHasher); - - static_paths.retain(|path| { - if let Some(path) = path.as_system_path() { - seen_paths.insert(path.to_path_buf()) - } else { - true - } - }); - - Ok(SearchPaths { - static_paths, - site_packages, - typeshed_versions, - }) - } - - pub(super) fn iter<'a>(&'a self, db: &'a dyn Db) -> SearchPathIterator<'a> { - SearchPathIterator { - db, - static_paths: self.static_paths.iter(), - dynamic_paths: None, - } - } - - pub(crate) fn custom_stdlib(&self) -> Option<&SystemPath> { - self.static_paths.iter().find_map(|search_path| { - if search_path.is_standard_library() { - search_path.as_system_path() - } else { - None - } - }) - } - - pub(super) fn typeshed_versions(&self) -> &TypeshedVersions { - &self.typeshed_versions - } -} - -/// Collect all dynamic search paths. For each `site-packages` path: -/// - Collect that `site-packages` path -/// - Collect any search paths listed in `.pth` files in that `site-packages` directory -/// due to editable installations of third-party packages. -/// -/// The editable-install search paths for the first `site-packages` directory -/// should come between the two `site-packages` directories when it comes to -/// module-resolution priority. -#[salsa::tracked(return_ref)] -pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec { - tracing::debug!("Resolving dynamic module resolution paths"); - - let SearchPaths { - static_paths, - site_packages, - typeshed_versions: _, - } = Program::get(db).search_paths(db); - - let mut dynamic_paths = Vec::new(); - - if site_packages.is_empty() { - return dynamic_paths; - } - - let mut existing_paths: FxHashSet<_> = static_paths - .iter() - .filter_map(|path| path.as_system_path()) - .map(Cow::Borrowed) - .collect(); - - let files = db.files(); - let system = db.system(); - - for site_packages_search_path in site_packages { - let site_packages_dir = site_packages_search_path - .as_system_path() - .expect("Expected site package path to be a system path"); - - if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) { - continue; - } - - let site_packages_root = files - .root(db.upcast(), site_packages_dir) - .expect("Site-package root to have been created"); - - // This query needs to be re-executed each time a `.pth` file - // is added, modified or removed from the `site-packages` directory. - // However, we don't use Salsa queries to read the source text of `.pth` files; - // we use the APIs on the `System` trait directly. As such, add a dependency on the - // site-package directory's revision. - site_packages_root.revision(db.upcast()); - - dynamic_paths.push(site_packages_search_path.clone()); - - // As well as modules installed directly into `site-packages`, - // the directory may also contain `.pth` files. - // Each `.pth` file in `site-packages` may contain one or more lines - // containing a (relative or absolute) path. - // Each of these paths may point to an editable install of a package, - // so should be considered an additional search path. - let pth_file_iterator = match PthFileIterator::new(db, site_packages_dir) { - Ok(iterator) => iterator, - Err(error) => { - tracing::warn!( - "Failed to search for editable installation in {site_packages_dir}: {error}" - ); - continue; - } - }; - - // The Python documentation specifies that `.pth` files in `site-packages` - // are processed in alphabetical order, so collecting and then sorting is necessary. - // https://docs.python.org/3/library/site.html#module-site - let mut all_pth_files: Vec = pth_file_iterator.collect(); - all_pth_files.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - - let installations = all_pth_files.iter().flat_map(PthFile::items); - - for installation in installations { - let installation = system - .canonicalize_path(&installation) - .unwrap_or(installation); - - if existing_paths.insert(Cow::Owned(installation.clone())) { - match SearchPath::editable(system, installation.clone()) { - Ok(search_path) => { - tracing::debug!( - "Adding editable installation to module resolution path {path}", - path = installation - ); - dynamic_paths.push(search_path); - } - - Err(error) => { - tracing::debug!("Skipping editable installation: {error}"); - } - } - } - } - } - - dynamic_paths -} - -/// Iterate over the available module-resolution search paths, -/// following the invariants maintained by [`sys.path` at runtime]: -/// "No item is added to `sys.path` more than once." -/// Dynamic search paths (required for editable installs into `site-packages`) -/// are only calculated lazily. -/// -/// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site -pub(crate) struct SearchPathIterator<'db> { - db: &'db dyn Db, - static_paths: std::slice::Iter<'db, SearchPath>, - dynamic_paths: Option>, -} - -impl<'db> Iterator for SearchPathIterator<'db> { - type Item = &'db SearchPath; - - fn next(&mut self) -> Option { - let SearchPathIterator { - db, - static_paths, - dynamic_paths, - } = self; - - static_paths.next().or_else(|| { - dynamic_paths - .get_or_insert_with(|| dynamic_resolution_paths(*db).iter()) - .next() - }) - } -} - -impl FusedIterator for SearchPathIterator<'_> {} - -/// Represents a single `.pth` file in a `site-packages` directory. -/// One or more lines in a `.pth` file may be a (relative or absolute) -/// path that represents an editable installation of a package. -struct PthFile<'db> { - path: SystemPathBuf, - contents: String, - site_packages: &'db SystemPath, -} - -impl<'db> PthFile<'db> { - /// Yield paths in this `.pth` file that appear to represent editable installations, - /// and should therefore be added as module-resolution search paths. - fn items(&'db self) -> impl Iterator + 'db { - let PthFile { - path: _, - contents, - site_packages, - } = self; - - // Empty lines or lines starting with '#' are ignored by the Python interpreter. - // Lines that start with "import " or "import\t" do not represent editable installs at all; - // instead, these are lines that are executed by Python at startup. - // https://docs.python.org/3/library/site.html#module-site - contents.lines().filter_map(move |line| { - let line = line.trim_end(); - if line.is_empty() - || line.starts_with('#') - || line.starts_with("import ") - || line.starts_with("import\t") - { - return None; - } - - Some(SystemPath::absolute(line, site_packages)) - }) - } -} - -/// Iterator that yields a [`PthFile`] instance for every `.pth` file -/// found in a given `site-packages` directory. -struct PthFileIterator<'db> { - db: &'db dyn Db, - directory_iterator: Box> + 'db>, - site_packages: &'db SystemPath, -} - -impl<'db> PthFileIterator<'db> { - fn new(db: &'db dyn Db, site_packages: &'db SystemPath) -> std::io::Result { - Ok(Self { - db, - directory_iterator: db.system().read_directory(site_packages)?, - site_packages, - }) - } -} - -impl<'db> Iterator for PthFileIterator<'db> { - type Item = PthFile<'db>; - - fn next(&mut self) -> Option { - let PthFileIterator { - db, - directory_iterator, - site_packages, - } = self; - - let system = db.system(); - - loop { - let entry_result = directory_iterator.next()?; - let Ok(entry) = entry_result else { - continue; - }; - let file_type = entry.file_type(); - if file_type.is_directory() { - continue; - } - let path = entry.into_path(); - if path.extension() != Some("pth") { - continue; - } - - let contents = match system.read_to_string(&path) { - Ok(contents) => contents, - Err(error) => { - tracing::warn!("Failed to read .pth file '{path}': {error}"); - continue; - } - }; - - return Some(PthFile { - path, - contents, - site_packages, - }); - } - } -} - -/// A thin wrapper around `ModuleName` to make it a Salsa ingredient. -/// -/// This is needed because Salsa requires that all query arguments are salsa ingredients. -#[salsa::interned(debug)] -struct ModuleNameIngredient<'db> { - #[return_ref] - pub(super) name: ModuleName, -} - -/// Given a module name and a list of search paths in which to lookup modules, -/// attempt to resolve the module name -fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, ResolvedModule)> { - let program = Program::get(db); - let python_version = program.python_version(db); - let resolver_state = ResolverContext::new(db, python_version); - let is_builtin_module = - ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str()); - - let name = RelaxedModuleName::new(name); - let stub_name = name.to_stub_package(); - - for search_path in search_paths(db) { - // When a builtin module is imported, standard module resolution is bypassed: - // the module name always resolves to the stdlib module, - // even if there's a module of the same name in the first-party root - // (which would normally result in the stdlib module being overridden). - if is_builtin_module && !search_path.is_standard_library() { - continue; - } - - if !search_path.is_standard_library() { - match resolve_module_in_search_path(&resolver_state, &stub_name, search_path) { - Ok(resolved_module) => { - if resolved_module.package_kind.is_root() && resolved_module.kind.is_module() { - tracing::trace!("Search path '{search_path} contains a module named `{stub_name}` but a standalone module isn't a valid stub."); - } else { - return Some((search_path.clone(), resolved_module)); - } - } - Err(PackageKind::Root) => { - tracing::trace!( - "Search path '{search_path}' contains no stub package named `{stub_name}`." - ); - } - Err(PackageKind::Regular) => { - tracing::trace!( - "Stub-package in `{search_path} doesn't contain module: `{name}`" - ); - // stub exists, but the module doesn't. - // TODO: Support partial packages. - return None; - } - Err(PackageKind::Namespace) => { - tracing::trace!( - "Stub-package in `{search_path} doesn't contain module: `{name}` but it is a namespace package, keep going." - ); - // stub exists, but the module doesn't. But this is a namespace package, - // keep searching the next search path for a stub package with the same name. - continue; - } - } - } - - match resolve_module_in_search_path(&resolver_state, &name, search_path) { - Ok(resolved_module) => return Some((search_path.clone(), resolved_module)), - Err(kind) => match kind { - PackageKind::Root => { - tracing::trace!( - "Search path '{search_path}' contains no package named `{name}`." - ); - } - PackageKind::Regular => { - // For regular packages, don't search the next search path. All files of that - // package must be in the same location - tracing::trace!("Package in `{search_path} doesn't contain module: `{name}`"); - return None; - } - PackageKind::Namespace => { - tracing::trace!( - "Package in `{search_path} doesn't contain module: `{name}` but it is a namespace package, keep going." - ); - } - }, - } - } - - None -} - -#[derive(Debug)] -struct ResolvedModule { - kind: ModuleKind, - package_kind: PackageKind, - file: File, -} - -fn resolve_module_in_search_path( - context: &ResolverContext, - name: &RelaxedModuleName, - search_path: &SearchPath, -) -> Result { - let mut components = name.components(); - let module_name = components.next_back().unwrap(); - - let resolved_package = resolve_package(search_path, components, context)?; - - let mut package_path = resolved_package.path; - - package_path.push(module_name); - - // Check for a regular package first (highest priority) - package_path.push("__init__"); - if let Some(regular_package) = resolve_file_module(&package_path, context) { - return Ok(ResolvedModule { - file: regular_package, - kind: ModuleKind::Package, - package_kind: resolved_package.kind, - }); - } - - // Check for a file module next - package_path.pop(); - - if let Some(file_module) = resolve_file_module(&package_path, context) { - return Ok(ResolvedModule { - file: file_module, - kind: ModuleKind::Module, - package_kind: resolved_package.kind, - }); - } - - Err(resolved_package.kind) -} - -/// If `module` exists on disk with either a `.pyi` or `.py` extension, -/// return the [`File`] corresponding to that path. -/// -/// `.pyi` files take priority, as they always have priority when -/// resolving modules. -fn resolve_file_module(module: &ModulePath, resolver_state: &ResolverContext) -> Option { - // Stubs have precedence over source files - let file = module - .with_pyi_extension() - .to_file(resolver_state) - .or_else(|| { - module - .with_py_extension() - .and_then(|path| path.to_file(resolver_state)) - })?; - - // For system files, test if the path has the correct casing. - // We can skip this step for vendored files or virtual files because - // those file systems are case sensitive (we wouldn't get to this point). - if let Some(path) = file.path(resolver_state.db).as_system_path() { - let system = resolver_state.db.system(); - if !system.case_sensitivity().is_case_sensitive() - && !system - .path_exists_case_sensitive(path, module.search_path().as_system_path().unwrap()) - { - return None; - } - } - - Some(file) -} - -fn resolve_package<'a, 'db, I>( - module_search_path: &SearchPath, - components: I, - resolver_state: &ResolverContext<'db>, -) -> Result -where - I: Iterator, -{ - let mut package_path = module_search_path.to_module_path(); - - // `true` if inside a folder that is a namespace package (has no `__init__.py`). - // Namespace packages are special because they can be spread across multiple search paths. - // https://peps.python.org/pep-0420/ - let mut in_namespace_package = false; - - // `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`. - let mut in_sub_package = false; - - // For `foo.bar.baz`, test that `foo` and `baz` both contain a `__init__.py`. - for folder in components { - package_path.push(folder); - - let is_regular_package = package_path.is_regular_package(resolver_state); - - if is_regular_package { - in_namespace_package = false; - } else if package_path.is_directory(resolver_state) - // Pure modules hide namespace packages with the same name - && resolve_file_module(&package_path, resolver_state).is_none() - { - // A directory without an `__init__.py(i)` is a namespace package, continue with the next folder. - in_namespace_package = true; - } else if in_namespace_package { - // Package not found but it is part of a namespace package. - return Err(PackageKind::Namespace); - } else if in_sub_package { - // A regular sub package wasn't found. - return Err(PackageKind::Regular); - } else { - // We couldn't find `foo` for `foo.bar.baz`, search the next search path. - return Err(PackageKind::Root); - } - - in_sub_package = true; - } - - let kind = if in_namespace_package { - PackageKind::Namespace - } else if in_sub_package { - PackageKind::Regular - } else { - PackageKind::Root - }; - - Ok(ResolvedPackage { - kind, - path: package_path, - }) -} - -#[derive(Debug)] -struct ResolvedPackage { - path: ModulePath, - kind: PackageKind, -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -enum PackageKind { - /// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`. - Root, - - /// A regular sub-package where the parent contains an `__init__.py`. - /// - /// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`. - Regular, - - /// A sub-package in a namespace package. A namespace package is a package without an `__init__.py`. - /// - /// For example, `bar` in `foo.bar` if the `foo` directory contains no `__init__.py`. - Namespace, -} - -impl PackageKind { - pub(crate) const fn is_root(self) -> bool { - matches!(self, PackageKind::Root) - } -} - -pub(super) struct ResolverContext<'db> { - pub(super) db: &'db dyn Db, - pub(super) python_version: PythonVersion, -} - -impl<'db> ResolverContext<'db> { - pub(super) fn new(db: &'db dyn Db, python_version: PythonVersion) -> Self { - Self { db, python_version } - } - - pub(super) fn vendored(&self) -> &VendoredFileSystem { - self.db.vendored() - } -} - -/// A [`ModuleName`] but with relaxed semantics to allow `-stubs.path` -#[derive(Debug)] -struct RelaxedModuleName(compact_str::CompactString); - -impl RelaxedModuleName { - fn new(name: &ModuleName) -> Self { - Self(name.as_str().into()) - } - - fn components(&self) -> Split<'_, char> { - self.0.split('.') - } - - fn to_stub_package(&self) -> Self { - if let Some((package, rest)) = self.0.split_once('.') { - Self(format_compact!("{package}-stubs.{rest}")) - } else { - Self(format_compact!("{package}-stubs", package = self.0)) - } - } -} - -impl fmt::Display for RelaxedModuleName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -#[cfg(test)] -mod tests { - use ruff_db::files::{system_path_to_file, File, FilePath}; - use ruff_db::system::{DbWithTestSystem as _, DbWithWritableSystem as _}; - use ruff_db::testing::{ - assert_const_function_query_was_not_run, assert_function_query_was_not_run, - }; - use ruff_db::Db; - use ruff_python_ast::PythonVersion; - - use crate::db::tests::TestDb; - use crate::module_name::ModuleName; - use crate::module_resolver::module::ModuleKind; - use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; - use crate::{ProgramSettings, PythonPlatform}; - - use super::*; - - #[test] - fn first_party_module() { - let TestCase { db, src, .. } = TestCaseBuilder::new() - .with_src_files(&[("foo.py", "print('Hello, world!')")]) - .build(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - - assert_eq!( - Some(&foo_module), - resolve_module(&db, &foo_module_name).as_ref() - ); - - assert_eq!("foo", foo_module.name()); - assert_eq!(&src, foo_module.search_path()); - assert_eq!(ModuleKind::Module, foo_module.kind()); - - let expected_foo_path = src.join("foo.py"); - assert_eq!(&expected_foo_path, foo_module.file().path(&db)); - assert_eq!( - Some(foo_module), - path_to_module(&db, &FilePath::System(expected_foo_path)) - ); - } - - #[test] - fn builtins_vendored() { - let TestCase { db, stdlib, .. } = TestCaseBuilder::new() - .with_vendored_typeshed() - .with_src_files(&[("builtins.py", "FOOOO = 42")]) - .build(); - - let builtins_module_name = ModuleName::new_static("builtins").unwrap(); - let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve"); - - assert_eq!(builtins.file().path(&db), &stdlib.join("builtins.pyi")); - } - - #[test] - fn builtins_custom() { - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: &[("builtins.pyi", "def min(a, b): ...")], - versions: "builtins: 3.8-", - }; - - const SRC: &[FileSpec] = &[("builtins.py", "FOOOO = 42")]; - - let TestCase { db, stdlib, .. } = TestCaseBuilder::new() - .with_src_files(SRC) - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let builtins_module_name = ModuleName::new_static("builtins").unwrap(); - let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve"); - - assert_eq!(builtins.file().path(&db), &stdlib.join("builtins.pyi")); - } - - #[test] - fn stdlib() { - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], - versions: "functools: 3.8-", - }; - - let TestCase { db, stdlib, .. } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let functools_module_name = ModuleName::new_static("functools").unwrap(); - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - - assert_eq!( - Some(&functools_module), - resolve_module(&db, &functools_module_name).as_ref() - ); - - assert_eq!(&stdlib, functools_module.search_path()); - assert_eq!(ModuleKind::Module, functools_module.kind()); - - let expected_functools_path = stdlib.join("functools.pyi"); - assert_eq!(&expected_functools_path, functools_module.file().path(&db)); - - assert_eq!( - Some(functools_module), - path_to_module(&db, &FilePath::System(expected_functools_path)) - ); - } - - fn create_module_names(raw_names: &[&str]) -> Vec { - raw_names - .iter() - .map(|raw| ModuleName::new(raw).unwrap()) - .collect() - } - - #[test] - fn stdlib_resolution_respects_versions_file_py38_existing_modules() { - const VERSIONS: &str = "\ - asyncio: 3.8- # 'Regular' package on py38+ - asyncio.tasks: 3.9-3.11 # Submodule on py39+ only - functools: 3.8- # Top-level single-file module - xml: 3.8-3.8 # Namespace package on py38 only - "; - - const STDLIB: &[FileSpec] = &[ - ("asyncio/__init__.pyi", ""), - ("asyncio/tasks.pyi", ""), - ("functools.pyi", ""), - ("xml/etree.pyi", ""), - ]; - - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: STDLIB, - versions: VERSIONS, - }; - - let TestCase { db, stdlib, .. } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]); - for module_name in existing_modules { - let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| { - panic!("Expected module {module_name} to exist in the mock stdlib") - }); - let search_path = resolved_module.search_path(); - assert_eq!( - &stdlib, search_path, - "Search path for {module_name} was unexpectedly {search_path:?}" - ); - assert!( - search_path.is_standard_library(), - "Expected a stdlib search path, but got {search_path:?}" - ); - } - } - - #[test] - fn stdlib_resolution_respects_versions_file_py38_nonexisting_modules() { - const VERSIONS: &str = "\ - asyncio: 3.8- # 'Regular' package on py38+ - asyncio.tasks: 3.9-3.11 # Submodule on py39+ only - collections: 3.9- # 'Regular' package on py39+ - importlib: 3.9- # Namespace package on py39+ - xml: 3.8-3.8 # Namespace package on 3.8 only - "; - - const STDLIB: &[FileSpec] = &[ - ("collections/__init__.pyi", ""), - ("asyncio/__init__.pyi", ""), - ("asyncio/tasks.pyi", ""), - ("importlib/abc.pyi", ""), - ("xml/etree.pyi", ""), - ]; - - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: STDLIB, - versions: VERSIONS, - }; - - let TestCase { db, .. } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let nonexisting_modules = create_module_names(&[ - "collections", - "importlib", - "importlib.abc", - "xml", - "asyncio.tasks", - ]); - - for module_name in nonexisting_modules { - assert!( - resolve_module(&db, &module_name).is_none(), - "Unexpectedly resolved a module for {module_name}" - ); - } - } - - #[test] - fn stdlib_resolution_respects_versions_file_py39_existing_modules() { - const VERSIONS: &str = "\ - asyncio: 3.8- # 'Regular' package on py38+ - asyncio.tasks: 3.9-3.11 # Submodule on py39+ only - collections: 3.9- # 'Regular' package on py39+ - functools: 3.8- # Top-level single-file module - importlib: 3.9- # Namespace package on py39+ - "; - - const STDLIB: &[FileSpec] = &[ - ("asyncio/__init__.pyi", ""), - ("asyncio/tasks.pyi", ""), - ("collections/__init__.pyi", ""), - ("functools.pyi", ""), - ("importlib/abc.pyi", ""), - ]; - - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: STDLIB, - versions: VERSIONS, - }; - - let TestCase { db, stdlib, .. } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY39) - .build(); - - let existing_modules = create_module_names(&[ - "asyncio", - "functools", - "importlib.abc", - "collections", - "asyncio.tasks", - ]); - - for module_name in existing_modules { - let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| { - panic!("Expected module {module_name} to exist in the mock stdlib") - }); - let search_path = resolved_module.search_path(); - assert_eq!( - &stdlib, search_path, - "Search path for {module_name} was unexpectedly {search_path:?}" - ); - assert!( - search_path.is_standard_library(), - "Expected a stdlib search path, but got {search_path:?}" - ); - } - } - #[test] - fn stdlib_resolution_respects_versions_file_py39_nonexisting_modules() { - const VERSIONS: &str = "\ - importlib: 3.9- # Namespace package on py39+ - xml: 3.8-3.8 # Namespace package on 3.8 only - "; - - const STDLIB: &[FileSpec] = &[("importlib/abc.pyi", ""), ("xml/etree.pyi", "")]; - - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: STDLIB, - versions: VERSIONS, - }; - - let TestCase { db, .. } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY39) - .build(); - - let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]); - for module_name in nonexisting_modules { - assert!( - resolve_module(&db, &module_name).is_none(), - "Unexpectedly resolved a module for {module_name}" - ); - } - } - - #[test] - fn first_party_precedence_over_stdlib() { - const SRC: &[FileSpec] = &[("functools.py", "def update_wrapper(): ...")]; - - const TYPESHED: MockedTypeshed = MockedTypeshed { - stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], - versions: "functools: 3.8-", - }; - - let TestCase { db, src, .. } = TestCaseBuilder::new() - .with_src_files(SRC) - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let functools_module_name = ModuleName::new_static("functools").unwrap(); - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - - assert_eq!( - Some(&functools_module), - resolve_module(&db, &functools_module_name).as_ref() - ); - assert_eq!(&src, functools_module.search_path()); - assert_eq!(ModuleKind::Module, functools_module.kind()); - assert_eq!(&src.join("functools.py"), functools_module.file().path(&db)); - - assert_eq!( - Some(functools_module), - path_to_module(&db, &FilePath::System(src.join("functools.py"))) - ); - } - - #[test] - fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() { - let TestCase { db, stdlib, .. } = TestCaseBuilder::new() - .with_vendored_typeshed() - .with_python_version(PythonVersion::default()) - .build(); - - let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap(); - let pydoc_data_topics = resolve_module(&db, &pydoc_data_topics_name).unwrap(); - - assert_eq!("pydoc_data.topics", pydoc_data_topics.name()); - assert_eq!(pydoc_data_topics.search_path(), &stdlib); - assert_eq!( - pydoc_data_topics.file().path(&db), - &stdlib.join("pydoc_data/topics.pyi") - ); - } - - #[test] - fn resolve_package() { - let TestCase { src, db, .. } = TestCaseBuilder::new() - .with_src_files(&[("foo/__init__.py", "print('Hello, world!'")]) - .build(); - - let foo_path = src.join("foo/__init__.py"); - let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); - - assert_eq!("foo", foo_module.name()); - assert_eq!(&src, foo_module.search_path()); - assert_eq!(&foo_path, foo_module.file().path(&db)); - - assert_eq!( - Some(&foo_module), - path_to_module(&db, &FilePath::System(foo_path)).as_ref() - ); - - // Resolving by directory doesn't resolve to the init file. - assert_eq!( - None, - path_to_module(&db, &FilePath::System(src.join("foo"))) - ); - } - - #[test] - fn package_priority_over_module() { - const SRC: &[FileSpec] = &[ - ("foo/__init__.py", "print('Hello, world!')"), - ("foo.py", "print('Hello, world!')"), - ]; - - let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); - - let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); - let foo_init_path = src.join("foo/__init__.py"); - - assert_eq!(&src, foo_module.search_path()); - assert_eq!(&foo_init_path, foo_module.file().path(&db)); - assert_eq!(ModuleKind::Package, foo_module.kind()); - - assert_eq!( - Some(foo_module), - path_to_module(&db, &FilePath::System(foo_init_path)) - ); - assert_eq!( - None, - path_to_module(&db, &FilePath::System(src.join("foo.py"))) - ); - } - - #[test] - fn single_file_takes_priority_over_namespace_package() { - //const SRC: &[FileSpec] = &[("foo.py", "x = 1")]; - const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/bar.py", "x = 2")]; - - let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap(); - - // `foo.py` takes priority over the `foo` namespace package - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - assert_eq!(foo_module.file().path(&db), &src.join("foo.py")); - - // `foo.bar` isn't recognised as a module - let foo_bar_module = resolve_module(&db, &foo_bar_module_name); - assert_eq!(foo_bar_module, None); - } - - #[test] - fn typing_stub_over_module() { - const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")]; - - let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); - - let foo = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); - let foo_stub = src.join("foo.pyi"); - - assert_eq!(&src, foo.search_path()); - assert_eq!(&foo_stub, foo.file().path(&db)); - - assert_eq!(Some(foo), path_to_module(&db, &FilePath::System(foo_stub))); - assert_eq!( - None, - path_to_module(&db, &FilePath::System(src.join("foo.py"))) - ); - } - - #[test] - fn sub_packages() { - const SRC: &[FileSpec] = &[ - ("foo/__init__.py", ""), - ("foo/bar/__init__.py", ""), - ("foo/bar/baz.py", "print('Hello, world!)'"), - ]; - - let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); - - let baz_module = - resolve_module(&db, &ModuleName::new_static("foo.bar.baz").unwrap()).unwrap(); - let baz_path = src.join("foo/bar/baz.py"); - - assert_eq!(&src, baz_module.search_path()); - assert_eq!(&baz_path, baz_module.file().path(&db)); - - assert_eq!( - Some(baz_module), - path_to_module(&db, &FilePath::System(baz_path)) - ); - } - - #[test] - fn namespace_package() { - // From [PEP420](https://peps.python.org/pep-0420/#nested-namespace-packages). - // But uses `src` for `project1` and `site-packages` for `project2`. - // ``` - // src - // parent - // child - // one.py - // site_packages - // parent - // child - // two.py - // ``` - let TestCase { - db, - src, - site_packages, - .. - } = TestCaseBuilder::new() - .with_src_files(&[("parent/child/one.py", "print('Hello, world!')")]) - .with_site_packages_files(&[("parent/child/two.py", "print('Hello, world!')")]) - .build(); - - let one_module_name = ModuleName::new_static("parent.child.one").unwrap(); - let one_module_path = FilePath::System(src.join("parent/child/one.py")); - assert_eq!( - resolve_module(&db, &one_module_name), - path_to_module(&db, &one_module_path) - ); - - let two_module_name = ModuleName::new_static("parent.child.two").unwrap(); - let two_module_path = FilePath::System(site_packages.join("parent/child/two.py")); - assert_eq!( - resolve_module(&db, &two_module_name), - path_to_module(&db, &two_module_path) - ); - } - - #[test] - fn regular_package_in_namespace_package() { - // Adopted test case from the [PEP420 examples](https://peps.python.org/pep-0420/#nested-namespace-packages). - // The `src/parent/child` package is a regular package. Therefore, `site_packages/parent/child/two.py` should not be resolved. - // ``` - // src - // parent - // child - // one.py - // site_packages - // parent - // child - // two.py - // ``` - const SRC: &[FileSpec] = &[ - ("parent/child/__init__.py", "print('Hello, world!')"), - ("parent/child/one.py", "print('Hello, world!')"), - ]; - - const SITE_PACKAGES: &[FileSpec] = &[("parent/child/two.py", "print('Hello, world!')")]; - - let TestCase { db, src, .. } = TestCaseBuilder::new() - .with_src_files(SRC) - .with_site_packages_files(SITE_PACKAGES) - .build(); - - let one_module_path = FilePath::System(src.join("parent/child/one.py")); - let one_module_name = - resolve_module(&db, &ModuleName::new_static("parent.child.one").unwrap()); - assert_eq!(one_module_name, path_to_module(&db, &one_module_path)); - - assert_eq!( - None, - resolve_module(&db, &ModuleName::new_static("parent.child.two").unwrap()) - ); - } - - #[test] - fn module_search_path_priority() { - let TestCase { - db, - src, - site_packages, - .. - } = TestCaseBuilder::new() - .with_src_files(&[("foo.py", "")]) - .with_site_packages_files(&[("foo.py", "")]) - .build(); - - let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); - let foo_src_path = src.join("foo.py"); - - assert_eq!(&src, foo_module.search_path()); - assert_eq!(&foo_src_path, foo_module.file().path(&db)); - assert_eq!( - Some(foo_module), - path_to_module(&db, &FilePath::System(foo_src_path)) - ); - - assert_eq!( - None, - path_to_module(&db, &FilePath::System(site_packages.join("foo.py"))) - ); - } - - #[test] - #[cfg(target_family = "unix")] - fn symlink() -> anyhow::Result<()> { - use anyhow::Context; - - use crate::{program::Program, PythonPlatform}; - use ruff_db::system::{OsSystem, SystemPath}; - - use crate::db::tests::TestDb; - - let mut db = TestDb::new(); - - let temp_dir = tempfile::tempdir()?; - let root = temp_dir - .path() - .canonicalize() - .context("Failed to canonicalize temp dir")?; - let root = SystemPath::from_std_path(&root).unwrap(); - db.use_system(OsSystem::new(root)); - - let src = root.join("src"); - let site_packages = root.join("site-packages"); - let custom_typeshed = root.join("typeshed"); - - let foo = src.join("foo.py"); - let bar = src.join("bar.py"); - - std::fs::create_dir_all(src.as_std_path())?; - std::fs::create_dir_all(site_packages.as_std_path())?; - std::fs::create_dir_all(custom_typeshed.join("stdlib").as_std_path())?; - std::fs::File::create(custom_typeshed.join("stdlib/VERSIONS").as_std_path())?; - - std::fs::write(foo.as_std_path(), "")?; - std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?; - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::PY38, - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![src.clone()], - custom_typeshed: Some(custom_typeshed), - python_path: PythonPath::KnownSitePackages(vec![site_packages]), - }, - }, - ) - .context("Invalid program settings")?; - - let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); - let bar_module = resolve_module(&db, &ModuleName::new_static("bar").unwrap()).unwrap(); - - assert_ne!(foo_module, bar_module); - - assert_eq!(&src, foo_module.search_path()); - assert_eq!(&foo, foo_module.file().path(&db)); - - // `foo` and `bar` shouldn't resolve to the same file - - assert_eq!(&src, bar_module.search_path()); - assert_eq!(&bar, bar_module.file().path(&db)); - assert_eq!(&foo, foo_module.file().path(&db)); - - assert_ne!(&foo_module, &bar_module); - - assert_eq!( - Some(foo_module), - path_to_module(&db, &FilePath::System(foo)) - ); - assert_eq!( - Some(bar_module), - path_to_module(&db, &FilePath::System(bar)) - ); - - Ok(()) - } - - #[test] - fn deleting_an_unrelated_file_doesnt_change_module_resolution() { - let TestCase { mut db, src, .. } = TestCaseBuilder::new() - .with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")]) - .with_python_version(PythonVersion::PY38) - .build(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - - let bar_path = src.join("bar.py"); - let bar = system_path_to_file(&db, &bar_path).expect("bar.py to exist"); - - db.clear_salsa_events(); - - // Delete `bar.py` - db.memory_file_system().remove_file(&bar_path).unwrap(); - bar.sync(&mut db); - - // Re-query the foo module. The foo module should still be cached because `bar.py` isn't relevant - // for resolving `foo`. - - let foo_module2 = resolve_module(&db, &foo_module_name); - - assert!(!db - .take_salsa_events() - .iter() - .any(|event| { matches!(event.kind, salsa::EventKind::WillExecute { .. }) })); - - assert_eq!(Some(foo_module), foo_module2); - } - - #[test] - fn adding_file_on_which_module_resolution_depends_invalidates_previously_failing_query_that_now_succeeds( - ) -> anyhow::Result<()> { - let TestCase { mut db, src, .. } = TestCaseBuilder::new().build(); - let foo_path = src.join("foo.py"); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - assert_eq!(resolve_module(&db, &foo_module_name), None); - - // Now write the foo file - db.write_file(&foo_path, "x = 1")?; - - let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist"); - - let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve"); - assert_eq!(foo_file, foo_module.file()); - - Ok(()) - } - - #[test] - fn removing_file_on_which_module_resolution_depends_invalidates_previously_successful_query_that_now_fails( - ) -> anyhow::Result<()> { - const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/__init__.py", "x = 2")]; - - let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_module = resolve_module(&db, &foo_module_name).expect("foo module to exist"); - let foo_init_path = src.join("foo/__init__.py"); - - assert_eq!(&foo_init_path, foo_module.file().path(&db)); - - // Delete `foo/__init__.py` and the `foo` folder. `foo` should now resolve to `foo.py` - db.memory_file_system().remove_file(&foo_init_path)?; - db.memory_file_system() - .remove_directory(foo_init_path.parent().unwrap())?; - File::sync_path(&mut db, &foo_init_path); - File::sync_path(&mut db, foo_init_path.parent().unwrap()); - - let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve"); - assert_eq!(&src.join("foo.py"), foo_module.file().path(&db)); - - Ok(()) - } - - #[test] - fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() { - const TYPESHED: MockedTypeshed = MockedTypeshed { - versions: "functools: 3.8-", - stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], - }; - - let TestCase { - mut db, - stdlib, - site_packages, - .. - } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let functools_module_name = ModuleName::new_static("functools").unwrap(); - let stdlib_functools_path = stdlib.join("functools.pyi"); - - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - assert_eq!(functools_module.search_path(), &stdlib); - assert_eq!( - Ok(functools_module.file()), - system_path_to_file(&db, &stdlib_functools_path) - ); - - // Adding a file to site-packages does not invalidate the query, - // since site-packages takes lower priority in the module resolution - db.clear_salsa_events(); - let site_packages_functools_path = site_packages.join("functools.py"); - db.write_file(&site_packages_functools_path, "f: int") - .unwrap(); - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - let events = db.take_salsa_events(); - assert_function_query_was_not_run( - &db, - resolve_module_query, - ModuleNameIngredient::new(&db, functools_module_name), - &events, - ); - assert_eq!(functools_module.search_path(), &stdlib); - assert_eq!( - Ok(functools_module.file()), - system_path_to_file(&db, &stdlib_functools_path) - ); - } - - #[test] - fn adding_file_to_search_path_with_higher_priority_invalidates_the_query() { - const TYPESHED: MockedTypeshed = MockedTypeshed { - versions: "functools: 3.8-", - stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], - }; - - let TestCase { - mut db, - stdlib, - src, - .. - } = TestCaseBuilder::new() - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let functools_module_name = ModuleName::new_static("functools").unwrap(); - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - assert_eq!(functools_module.search_path(), &stdlib); - assert_eq!( - Ok(functools_module.file()), - system_path_to_file(&db, stdlib.join("functools.pyi")) - ); - - // Adding a first-party file invalidates the query, - // since first-party files take higher priority in module resolution: - let src_functools_path = src.join("functools.py"); - db.write_file(&src_functools_path, "FOO: int").unwrap(); - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - assert_eq!(functools_module.search_path(), &src); - assert_eq!( - Ok(functools_module.file()), - system_path_to_file(&db, &src_functools_path) - ); - } - - #[test] - fn deleting_file_from_higher_priority_search_path_invalidates_the_query() { - const SRC: &[FileSpec] = &[("functools.py", "FOO: int")]; - - const TYPESHED: MockedTypeshed = MockedTypeshed { - versions: "functools: 3.8-", - stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], - }; - - let TestCase { - mut db, - stdlib, - src, - .. - } = TestCaseBuilder::new() - .with_src_files(SRC) - .with_mocked_typeshed(TYPESHED) - .with_python_version(PythonVersion::PY38) - .build(); - - let functools_module_name = ModuleName::new_static("functools").unwrap(); - let src_functools_path = src.join("functools.py"); - - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - assert_eq!(functools_module.search_path(), &src); - assert_eq!( - Ok(functools_module.file()), - system_path_to_file(&db, &src_functools_path) - ); - - // If we now delete the first-party file, - // it should resolve to the stdlib: - db.memory_file_system() - .remove_file(&src_functools_path) - .unwrap(); - File::sync_path(&mut db, &src_functools_path); - let functools_module = resolve_module(&db, &functools_module_name).unwrap(); - assert_eq!(functools_module.search_path(), &stdlib); - assert_eq!( - Ok(functools_module.file()), - system_path_to_file(&db, stdlib.join("functools.pyi")) - ); - } - - #[test] - fn editable_install_absolute_path() { - const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")]; - let x_directory = [("/x/src/foo/__init__.py", ""), ("/x/src/foo/bar.py", "")]; - - let TestCase { mut db, .. } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - db.write_files(x_directory).unwrap(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap(); - - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - let foo_bar_module = resolve_module(&db, &foo_bar_module_name).unwrap(); - - assert_eq!( - foo_module.file().path(&db), - &FilePath::system("/x/src/foo/__init__.py") - ); - assert_eq!( - foo_bar_module.file().path(&db), - &FilePath::system("/x/src/foo/bar.py") - ); - } - - #[test] - fn editable_install_pth_file_with_whitespace() { - const SITE_PACKAGES: &[FileSpec] = &[ - ("_foo.pth", " /x/src"), - ("_bar.pth", "/y/src "), - ]; - let external_files = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")]; - - let TestCase { mut db, .. } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - db.write_files(external_files).unwrap(); - - // Lines with leading whitespace in `.pth` files do not parse: - let foo_module_name = ModuleName::new_static("foo").unwrap(); - assert_eq!(resolve_module(&db, &foo_module_name), None); - - // Lines with trailing whitespace in `.pth` files do: - let bar_module_name = ModuleName::new_static("bar").unwrap(); - let bar_module = resolve_module(&db, &bar_module_name).unwrap(); - assert_eq!( - bar_module.file().path(&db), - &FilePath::system("/y/src/bar.py") - ); - } - - #[test] - fn editable_install_relative_path() { - const SITE_PACKAGES: &[FileSpec] = &[ - ("_foo.pth", "../../x/../x/y/src"), - ("../x/y/src/foo.pyi", ""), - ]; - - let TestCase { db, .. } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - - assert_eq!( - foo_module.file().path(&db), - &FilePath::system("/x/y/src/foo.pyi") - ); - } - - #[test] - fn editable_install_multiple_pth_files_with_multiple_paths() { - const COMPLEX_PTH_FILE: &str = "\ -/ - -# a comment -/baz - -import not_an_editable_install; do_something_else_crazy_dynamic() - -# another comment -spam - -not_a_directory -"; - - const SITE_PACKAGES: &[FileSpec] = &[ - ("_foo.pth", "../../x/../x/y/src"), - ("_lots_of_others.pth", COMPLEX_PTH_FILE), - ("../x/y/src/foo.pyi", ""), - ("spam/spam.py", ""), - ]; - - let root_files = [("/a.py", ""), ("/baz/b.py", "")]; - - let TestCase { - mut db, - site_packages, - .. - } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - db.write_files(root_files).unwrap(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let a_module_name = ModuleName::new_static("a").unwrap(); - let b_module_name = ModuleName::new_static("b").unwrap(); - let spam_module_name = ModuleName::new_static("spam").unwrap(); - - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - let a_module = resolve_module(&db, &a_module_name).unwrap(); - let b_module = resolve_module(&db, &b_module_name).unwrap(); - let spam_module = resolve_module(&db, &spam_module_name).unwrap(); - - assert_eq!( - foo_module.file().path(&db), - &FilePath::system("/x/y/src/foo.pyi") - ); - assert_eq!(a_module.file().path(&db), &FilePath::system("/a.py")); - assert_eq!(b_module.file().path(&db), &FilePath::system("/baz/b.py")); - assert_eq!( - spam_module.file().path(&db), - &FilePath::System(site_packages.join("spam/spam.py")) - ); - } - - #[test] - fn module_resolution_paths_cached_between_different_module_resolutions() { - const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src"), ("_bar.pth", "/y/src")]; - let external_directories = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")]; - - let TestCase { mut db, .. } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - db.write_files(external_directories).unwrap(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let bar_module_name = ModuleName::new_static("bar").unwrap(); - - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - assert_eq!( - foo_module.file().path(&db), - &FilePath::system("/x/src/foo.py") - ); - - db.clear_salsa_events(); - let bar_module = resolve_module(&db, &bar_module_name).unwrap(); - assert_eq!( - bar_module.file().path(&db), - &FilePath::system("/y/src/bar.py") - ); - let events = db.take_salsa_events(); - assert_const_function_query_was_not_run(&db, dynamic_resolution_paths, &events); - } - - #[test] - fn deleting_pth_file_on_which_module_resolution_depends_invalidates_cache() { - const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")]; - let x_directory = [("/x/src/foo.py", "")]; - - let TestCase { - mut db, - site_packages, - .. - } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - db.write_files(x_directory).unwrap(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - assert_eq!( - foo_module.file().path(&db), - &FilePath::system("/x/src/foo.py") - ); - - db.memory_file_system() - .remove_file(site_packages.join("_foo.pth")) - .unwrap(); - - File::sync_path(&mut db, &site_packages.join("_foo.pth")); - - assert_eq!(resolve_module(&db, &foo_module_name), None); - } - - #[test] - fn deleting_editable_install_on_which_module_resolution_depends_invalidates_cache() { - const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")]; - let x_directory = [("/x/src/foo.py", "")]; - - let TestCase { mut db, .. } = TestCaseBuilder::new() - .with_site_packages_files(SITE_PACKAGES) - .build(); - - db.write_files(x_directory).unwrap(); - - let foo_module_name = ModuleName::new_static("foo").unwrap(); - let foo_module = resolve_module(&db, &foo_module_name).unwrap(); - let src_path = SystemPathBuf::from("/x/src"); - assert_eq!( - foo_module.file().path(&db), - &FilePath::System(src_path.join("foo.py")) - ); - - db.memory_file_system() - .remove_file(src_path.join("foo.py")) - .unwrap(); - db.memory_file_system().remove_directory(&src_path).unwrap(); - File::sync_path(&mut db, &src_path.join("foo.py")); - File::sync_path(&mut db, &src_path); - assert_eq!(resolve_module(&db, &foo_module_name), None); - } - - #[test] - fn no_duplicate_search_paths_added() { - let TestCase { db, .. } = TestCaseBuilder::new() - .with_src_files(&[("foo.py", "")]) - .with_site_packages_files(&[("_foo.pth", "/src")]) - .build(); - - let search_paths: Vec<&SearchPath> = search_paths(&db).collect(); - - assert!(search_paths.contains( - &&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap() - )); - assert!(!search_paths - .contains(&&SearchPath::editable(db.system(), SystemPathBuf::from("/src")).unwrap())); - } - - #[test] - fn multiple_site_packages_with_editables() { - let mut db = TestDb::new(); - - let venv_site_packages = SystemPathBuf::from("/venv-site-packages"); - let site_packages_pth = venv_site_packages.join("foo.pth"); - let system_site_packages = SystemPathBuf::from("/system-site-packages"); - let editable_install_location = SystemPathBuf::from("/x/y/a.py"); - let system_site_packages_location = system_site_packages.join("a.py"); - - db.memory_file_system() - .create_directory_all("/src") - .unwrap(); - db.write_files([ - (&site_packages_pth, "/x/y"), - (&editable_install_location, ""), - (&system_site_packages_location, ""), - ]) - .unwrap(); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::default(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![SystemPathBuf::from("/src")], - custom_typeshed: None, - python_path: PythonPath::KnownSitePackages(vec![ - venv_site_packages, - system_site_packages, - ]), - }, - }, - ) - .expect("Valid program settings"); - - // The editable installs discovered from the `.pth` file in the first `site-packages` directory - // take precedence over the second `site-packages` directory... - let a_module_name = ModuleName::new_static("a").unwrap(); - let a_module = resolve_module(&db, &a_module_name).unwrap(); - assert_eq!(a_module.file().path(&db), &editable_install_location); - - db.memory_file_system() - .remove_file(&site_packages_pth) - .unwrap(); - File::sync_path(&mut db, &site_packages_pth); - - // ...But now that the `.pth` file in the first `site-packages` directory has been deleted, - // the editable install no longer exists, so the module now resolves to the file in the - // second `site-packages` directory - let a_module = resolve_module(&db, &a_module_name).unwrap(); - assert_eq!(a_module.file().path(&db), &system_site_packages_location); - } - - #[test] - #[cfg(unix)] - fn case_sensitive_resolution_with_symlinked_directory() -> anyhow::Result<()> { - use anyhow::Context; - use ruff_db::system::OsSystem; - - let temp_dir = tempfile::TempDir::new()?; - let root = SystemPathBuf::from_path_buf( - temp_dir - .path() - .canonicalize() - .context("Failed to canonicalized path")?, - ) - .expect("UTF8 path for temp dir"); - - let mut db = TestDb::new(); - - let src = root.join("src"); - let a_package_target = root.join("a-package"); - let a_src = src.join("a"); - - db.use_system(OsSystem::new(&root)); - - db.write_file( - a_package_target.join("__init__.py"), - "class Foo: x: int = 4", - ) - .context("Failed to write `a-package/__init__.py`")?; - - db.write_file(src.join("main.py"), "print('Hy')") - .context("Failed to write `main.py`")?; - - // The symlink triggers the slow-path in the `OsSystem`'s `exists_path_case_sensitive` - // code because canonicalizing the path for `a/__init__.py` results in `a-package/__init__.py` - std::os::unix::fs::symlink(a_package_target.as_std_path(), a_src.as_std_path()) - .context("Failed to symlink `src/a` to `a-package`")?; - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::default(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![src], - custom_typeshed: None, - python_path: PythonPath::KnownSitePackages(vec![]), - }, - }, - ) - .expect("Valid program settings"); - - // Now try to resolve the module `A` (note the capital `A` instead of `a`). - let a_module_name = ModuleName::new_static("A").unwrap(); - assert_eq!(resolve_module(&db, &a_module_name), None); - - // Now lookup the same module using the lowercase `a` and it should resolve to the file in the system site-packages - let a_module_name = ModuleName::new_static("a").unwrap(); - let a_module = resolve_module(&db, &a_module_name).expect("a.py to resolve"); - assert!(a_module - .file() - .path(&db) - .as_str() - .ends_with("src/a/__init__.py"),); - - Ok(()) - } - - #[test] - fn file_to_module_where_one_search_path_is_subdirectory_of_other() { - let project_directory = SystemPathBuf::from("/project"); - let site_packages = project_directory.join(".venv/lib/python3.13/site-packages"); - let installed_foo_module = site_packages.join("foo/__init__.py"); - - let mut db = TestDb::new(); - db.write_file(&installed_foo_module, "").unwrap(); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::default(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![project_directory], - custom_typeshed: None, - python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]), - }, - }, - ) - .unwrap(); - - let foo_module_file = File::new(&db, FilePath::System(installed_foo_module)); - let module = file_to_module(&db, foo_module_file).unwrap(); - assert_eq!(module.search_path(), &site_packages); - } -} diff --git a/crates/red_knot_python_semantic/src/node_key.rs b/crates/red_knot_python_semantic/src/node_key.rs deleted file mode 100644 index 4c2ade0b7e1ef..0000000000000 --- a/crates/red_knot_python_semantic/src/node_key.rs +++ /dev/null @@ -1,21 +0,0 @@ -use ruff_python_ast::AnyNodeRef; - -/// Compact key for a node for use in a hash map. -/// -/// Stores the memory address of the node, because using the range and the kind -/// of the node is not enough to uniquely identify them in ASTs resulting from -/// invalid syntax. For example, parsing the input `for` results in a `StmtFor` -/// AST node where both the `target` and the `iter` field are `ExprName` nodes -/// with the same (empty) range `3..3`. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub(super) struct NodeKey(usize); - -impl NodeKey { - pub(super) fn from_node<'a, N>(node: N) -> Self - where - N: Into>, - { - let node = node.into(); - NodeKey(node.as_ptr().as_ptr() as usize) - } -} diff --git a/crates/red_knot_python_semantic/src/program.rs b/crates/red_knot_python_semantic/src/program.rs deleted file mode 100644 index af3bbe0ce235e..0000000000000 --- a/crates/red_knot_python_semantic/src/program.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::module_resolver::SearchPaths; -use crate::python_platform::PythonPlatform; -use crate::site_packages::SysPrefixPathOrigin; -use crate::Db; - -use anyhow::Context; -use ruff_db::system::{SystemPath, SystemPathBuf}; -use ruff_python_ast::PythonVersion; -use salsa::Durability; -use salsa::Setter; - -#[salsa::input(singleton)] -pub struct Program { - pub python_version: PythonVersion, - - #[return_ref] - pub python_platform: PythonPlatform, - - #[return_ref] - pub(crate) search_paths: SearchPaths, -} - -impl Program { - pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> anyhow::Result { - let ProgramSettings { - python_version, - python_platform, - search_paths, - } = settings; - - tracing::info!("Python version: Python {python_version}, platform: {python_platform}"); - - let search_paths = SearchPaths::from_settings(db, &search_paths) - .with_context(|| "Invalid search path settings")?; - - Ok( - Program::builder(python_version, python_platform, search_paths) - .durability(Durability::HIGH) - .new(db), - ) - } - - pub fn update_from_settings( - self, - db: &mut dyn Db, - settings: ProgramSettings, - ) -> anyhow::Result<()> { - let ProgramSettings { - python_version, - python_platform, - search_paths, - } = settings; - - if &python_platform != self.python_platform(db) { - tracing::debug!("Updating python platform: `{python_platform:?}`"); - self.set_python_platform(db).to(python_platform); - } - - if python_version != self.python_version(db) { - tracing::debug!("Updating python version: Python {python_version}"); - self.set_python_version(db).to(python_version); - } - - self.update_search_paths(db, &search_paths)?; - - Ok(()) - } - - pub fn update_search_paths( - self, - db: &mut dyn Db, - search_path_settings: &SearchPathSettings, - ) -> anyhow::Result<()> { - let search_paths = SearchPaths::from_settings(db, search_path_settings)?; - - if self.search_paths(db) != &search_paths { - tracing::debug!("Update search paths"); - self.set_search_paths(db).to(search_paths); - } - - Ok(()) - } - - pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> { - self.search_paths(db).custom_stdlib() - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct ProgramSettings { - pub python_version: PythonVersion, - pub python_platform: PythonPlatform, - pub search_paths: SearchPathSettings, -} - -/// Configures the search paths for module resolution. -#[derive(Eq, PartialEq, Debug, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct SearchPathSettings { - /// List of user-provided paths that should take first priority in the module resolution. - /// Examples in other type checkers are mypy's MYPYPATH environment variable, - /// or pyright's stubPath configuration setting. - pub extra_paths: Vec, - - /// The root of the project, used for finding first-party modules. - pub src_roots: Vec, - - /// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types. - /// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, - /// bundled as a zip file in the binary - pub custom_typeshed: Option, - - /// Path to the Python installation from which Red Knot resolves third party dependencies - /// and their type information. - pub python_path: PythonPath, -} - -impl SearchPathSettings { - pub fn new(src_roots: Vec) -> Self { - Self { - src_roots, - extra_paths: vec![], - custom_typeshed: None, - python_path: PythonPath::KnownSitePackages(vec![]), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum PythonPath { - /// A path that represents the value of [`sys.prefix`] at runtime in Python - /// for a given Python executable. - /// - /// For the case of a virtual environment, where a - /// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to - /// the virtual environment the Python binary lies inside, i.e. `/.venv`, - /// and `site-packages` will be at `.venv/lib/python3.X/site-packages`. - /// System Python installations generally work the same way: if a system - /// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix` - /// will be `/opt/homebrew`, and `site-packages` will be at - /// `/opt/homebrew/lib/python3.X/site-packages`. - /// - /// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix - SysPrefix(SystemPathBuf, SysPrefixPathOrigin), - - /// Tries to discover a virtual environment in the given path. - Discover(SystemPathBuf), - - /// Resolved site packages paths. - /// - /// This variant is mainly intended for testing where we want to skip resolving `site-packages` - /// because it would unnecessarily complicate the test setup. - KnownSitePackages(Vec), -} - -impl PythonPath { - pub fn from_virtual_env_var(path: impl Into) -> Self { - Self::SysPrefix(path.into(), SysPrefixPathOrigin::VirtualEnvVar) - } - - pub fn from_cli_flag(path: SystemPathBuf) -> Self { - Self::SysPrefix(path, SysPrefixPathOrigin::PythonCliFlag) - } -} diff --git a/crates/red_knot_python_semantic/src/python_platform.rs b/crates/red_knot_python_semantic/src/python_platform.rs deleted file mode 100644 index 5bef91f5559e9..0000000000000 --- a/crates/red_knot_python_semantic/src/python_platform.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::fmt::{Display, Formatter}; - -/// The target platform to assume when resolving types. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "kebab-case") -)] -pub enum PythonPlatform { - /// Do not make any assumptions about the target platform. - All, - - /// Assume a specific target platform like `linux`, `darwin` or `win32`. - /// - /// We use a string (instead of individual enum variants), as the set of possible platforms - /// may change over time. See for - /// some known platform identifiers. - #[cfg_attr(feature = "serde", serde(untagged))] - Identifier(String), -} - -impl From for PythonPlatform { - fn from(platform: String) -> Self { - match platform.as_str() { - "all" => PythonPlatform::All, - _ => PythonPlatform::Identifier(platform.to_string()), - } - } -} - -impl Display for PythonPlatform { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - PythonPlatform::All => f.write_str("all"), - PythonPlatform::Identifier(name) => f.write_str(name), - } - } -} - -impl Default for PythonPlatform { - fn default() -> Self { - if cfg!(target_os = "windows") { - PythonPlatform::Identifier("win32".to_string()) - } else if cfg!(target_os = "macos") { - PythonPlatform::Identifier("darwin".to_string()) - } else if cfg!(target_os = "android") { - PythonPlatform::Identifier("android".to_string()) - } else if cfg!(target_os = "ios") { - PythonPlatform::Identifier("ios".to_string()) - } else { - PythonPlatform::Identifier("linux".to_string()) - } - } -} - -#[cfg(feature = "schemars")] -mod schema { - use crate::PythonPlatform; - use schemars::_serde_json::Value; - use schemars::gen::SchemaGenerator; - use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation}; - use schemars::JsonSchema; - - impl JsonSchema for PythonPlatform { - fn schema_name() -> String { - "PythonPlatform".to_string() - } - - fn json_schema(_gen: &mut SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - // Hard code some well known values, but allow any other string as well. - subschemas: Some(Box::new(SubschemaValidation { - any_of: Some(vec![ - Schema::Object(SchemaObject { - instance_type: Some(schemars::schema::InstanceType::String.into()), - ..SchemaObject::default() - }), - // Promote well-known values for better auto-completion. - // Using `const` over `enumValues` as recommended [here](https://github.com/SchemaStore/schemastore/blob/master/CONTRIBUTING.md#documenting-enums). - Schema::Object(SchemaObject { - const_value: Some(Value::String("all".to_string())), - metadata: Some(Box::new(Metadata { - description: Some( - "Do not make any assumptions about the target platform." - .to_string(), - ), - ..Metadata::default() - })), - - ..SchemaObject::default() - }), - Schema::Object(SchemaObject { - const_value: Some(Value::String("darwin".to_string())), - metadata: Some(Box::new(Metadata { - description: Some("Darwin".to_string()), - ..Metadata::default() - })), - - ..SchemaObject::default() - }), - Schema::Object(SchemaObject { - const_value: Some(Value::String("linux".to_string())), - metadata: Some(Box::new(Metadata { - description: Some("Linux".to_string()), - ..Metadata::default() - })), - - ..SchemaObject::default() - }), - Schema::Object(SchemaObject { - const_value: Some(Value::String("win32".to_string())), - metadata: Some(Box::new(Metadata { - description: Some("Windows".to_string()), - ..Metadata::default() - })), - - ..SchemaObject::default() - }), - ]), - - ..SubschemaValidation::default() - })), - - ..SchemaObject::default() - }) - } - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs deleted file mode 100644 index 577122078864d..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ /dev/null @@ -1,1408 +0,0 @@ -use std::iter::FusedIterator; -use std::sync::Arc; - -use ruff_db::files::File; -use ruff_db::parsed::parsed_module; -use ruff_index::{IndexSlice, IndexVec}; - -use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; -use salsa::plumbing::AsId; -use salsa::Update; - -use crate::module_name::ModuleName; -use crate::node_key::NodeKey; -use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; -use crate::semantic_index::ast_ids::AstIds; -use crate::semantic_index::builder::SemanticIndexBuilder; -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, -}; -use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, UseDefMap}; -use crate::Db; - -pub mod ast_ids; -mod builder; -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; - -pub(crate) use self::use_def::{ - BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint, - DeclarationsIterator, -}; - -type SymbolMap = hashbrown::HashMap; - -/// Returns the semantic index for `file`. -/// -/// Prefer using [`symbol_table`] when working with symbols from a single scope. -#[salsa::tracked(return_ref, no_eq)] -pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { - let _span = tracing::trace_span!("semantic_index", ?file).entered(); - - let parsed = parsed_module(db.upcast(), file); - - SemanticIndexBuilder::new(db, file, parsed).build() -} - -/// Returns the symbol table for a specific `scope`. -/// -/// Using [`symbol_table`] over [`semantic_index`] has the advantage that -/// Salsa can avoid invalidating dependent queries if this scope's symbol table -/// is unchanged. -#[salsa::tracked] -pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { - let file = scope.file(db); - let _span = tracing::trace_span!("symbol_table", scope=?scope.as_id(), ?file).entered(); - let index = semantic_index(db, file); - - index.symbol_table(scope.file_scope_id(db)) -} - -/// Returns the set of modules that are imported anywhere in `file`. -/// -/// This set only considers `import` statements, not `from...import` statements, because: -/// -/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is -/// therefore imported) without looking outside the content of this file. (We could turn this -/// into a _potentially_ imported modules set, but that would change how it's used in our type -/// inference logic.) -/// -/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without -/// knowing the name of the current module, and whether it's a package. -#[salsa::tracked] -pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc> { - semantic_index(db, file).imported_modules.clone() -} - -/// Returns the use-def map for a specific `scope`. -/// -/// Using [`use_def_map`] over [`semantic_index`] has the advantage that -/// Salsa can avoid invalidating dependent queries if this scope's use-def map -/// is unchanged. -#[salsa::tracked] -pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { - let file = scope.file(db); - let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?file).entered(); - let index = semantic_index(db, file); - - index.use_def_map(scope.file_scope_id(db)) -} - -/// Returns all attribute assignments (and their method scope IDs) for a specific class body scope. -/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it -/// introduces a direct dependency on that file's AST. -pub(crate) fn attribute_assignments<'db, 's>( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: &'s str, -) -> impl Iterator, FileScopeId)> + use<'s, 'db> { - let file = class_body_scope.file(db); - let index = semantic_index(db, file); - let class_scope_id = class_body_scope.file_scope_id(db); - - ChildrenIter::new(index, class_scope_id).filter_map(|(file_scope_id, maybe_method)| { - maybe_method.node().as_function()?; - let attribute_table = index.instance_attribute_table(file_scope_id); - let symbol = attribute_table.symbol_id_by_name(name)?; - let use_def = &index.use_def_maps[file_scope_id]; - Some((use_def.instance_attribute_bindings(symbol), file_scope_id)) - }) -} - -/// Returns the module global scope of `file`. -#[salsa::tracked] -pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { - let _span = tracing::trace_span!("global_scope", ?file).entered(); - - FileScopeId::global().to_scope_id(db, file) -} - -pub(crate) enum EagerBindingsResult<'map, 'db> { - Found(BindingWithConstraintsIterator<'map, 'db>), - NotFound, - NoLongerInEagerContext, -} - -/// The symbol tables and use-def maps for all scopes in a file. -#[derive(Debug, Update)] -pub(crate) struct SemanticIndex<'db> { - /// List of all symbol tables in this file, indexed by scope. - symbol_tables: IndexVec>, - - /// List of all instance attribute tables in this file, indexed by scope. - instance_attribute_tables: IndexVec, - - /// List of all scopes in this file. - scopes: IndexVec, - - /// Map expressions to their corresponding scope. - scopes_by_expression: FxHashMap, - - /// Map from a node creating a definition to its definition. - definitions_by_node: FxHashMap>, - - /// Map from a standalone expression to its [`Expression`] ingredient. - expressions_by_node: FxHashMap>, - - /// Map from nodes that create a scope to the scope they create. - scopes_by_node: FxHashMap, - - /// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`]. - scope_ids_by_scope: IndexVec>, - - /// Use-def map for each scope in this file. - use_def_maps: IndexVec>>, - - /// Lookup table to map between node ids and ast nodes. - /// - /// Note: We should not depend on this map when analysing other files or - /// changing a file invalidates all dependents. - ast_ids: IndexVec, - - /// The set of modules that are imported anywhere within this file. - imported_modules: Arc>, - - /// Flags about the global scope (code usage impacting inference) - has_future_annotations: bool, - - /// Map of all of the eager bindings that appear in this file. - eager_bindings: FxHashMap, -} - -impl<'db> SemanticIndex<'db> { - /// Returns the symbol table for a specific scope. - /// - /// Use the Salsa cached [`symbol_table()`] query if you only need the - /// symbol table for a single scope. - #[track_caller] - pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc { - self.symbol_tables[scope_id].clone() - } - - pub(super) fn instance_attribute_table(&self, scope_id: FileScopeId) -> &SymbolTable { - &self.instance_attribute_tables[scope_id] - } - - /// Returns the use-def map for a specific scope. - /// - /// Use the Salsa cached [`use_def_map()`] query if you only need the - /// use-def map for a single scope. - #[track_caller] - pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc { - self.use_def_maps[scope_id].clone() - } - - #[track_caller] - pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds { - &self.ast_ids[scope_id] - } - - /// Returns the ID of the `expression`'s enclosing scope. - #[track_caller] - pub(crate) fn expression_scope_id( - &self, - expression: impl Into, - ) -> FileScopeId { - self.scopes_by_expression[&expression.into()] - } - - /// Returns the [`Scope`] of the `expression`'s enclosing scope. - #[allow(unused)] - #[track_caller] - pub(crate) fn expression_scope(&self, expression: impl Into) -> &Scope { - &self.scopes[self.expression_scope_id(expression)] - } - - /// Returns the [`Scope`] with the given id. - #[track_caller] - pub(crate) fn scope(&self, id: FileScopeId) -> &Scope { - &self.scopes[id] - } - - pub(crate) fn scope_ids(&self) -> impl Iterator { - self.scope_ids_by_scope.iter().copied() - } - - /// Returns the id of the parent scope. - pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option { - let scope = self.scope(scope_id); - scope.parent() - } - - /// Returns the parent scope of `scope_id`. - #[allow(unused)] - #[track_caller] - pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> { - Some(&self.scopes[self.parent_scope_id(scope_id)?]) - } - - fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { - self.parent_scope_id(scope_id) - .is_none_or(|parent_scope_id| { - if !self.is_scope_reachable(db, parent_scope_id) { - return false; - } - - let parent_use_def = self.use_def_map(parent_scope_id); - let reachability = self.scope(scope_id).reachability(); - - parent_use_def.is_reachable(db, reachability) - }) - } - - /// Returns true if a given AST node is reachable from the start of the scope. For example, - /// in the following code, expression `2` is reachable, but expressions `1` and `3` are not: - /// ```py - /// def f(): - /// x = 1 - /// if False: - /// x # 1 - /// x # 2 - /// return - /// x # 3 - /// ``` - pub(crate) fn is_node_reachable( - &self, - db: &'db dyn crate::Db, - scope_id: FileScopeId, - node_key: NodeKey, - ) -> bool { - self.is_scope_reachable(db, scope_id) - && self.use_def_map(scope_id).is_node_reachable(db, node_key) - } - - /// Returns an iterator over the descendent scopes of `scope`. - #[allow(unused)] - pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter { - DescendantsIter::new(self, scope) - } - - /// Returns an iterator over the direct child scopes of `scope`. - #[allow(unused)] - pub(crate) fn child_scopes(&self, scope: FileScopeId) -> ChildrenIter { - ChildrenIter::new(self, scope) - } - - /// Returns an iterator over all ancestors of `scope`, starting with `scope` itself. - pub(crate) fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter { - AncestorsIter::new(self, scope) - } - - /// 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 definitions( - &self, - definition_key: impl Into, - ) -> &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 + std::fmt::Debug + Copy, - ) -> Definition<'db> { - 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. - /// Panics if we have no expression ingredient for that node. We can only call this method for - /// standalone-inferable expressions, which we call `add_standalone_expression` for in - /// [`SemanticIndexBuilder`]. - #[track_caller] - pub(crate) fn expression( - &self, - expression_key: impl Into, - ) -> Expression<'db> { - self.expressions_by_node[&expression_key.into()] - } - - pub(crate) fn try_expression( - &self, - expression_key: impl Into, - ) -> Option> { - self.expressions_by_node - .get(&expression_key.into()) - .copied() - } - - /// 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 { - self.scopes_by_node[&node.node_key()] - } - - /// Checks if there is an import of `__future__.annotations` in the global scope, which affects - /// the logic for type inference. - pub(super) fn has_future_annotations(&self) -> bool { - self.has_future_annotations - } - - /// Returns - /// * `NoLongerInEagerContext` if the nested scope is no longer in an eager context - /// (that is, not every scope that will be traversed is eager). - /// * an iterator of bindings for a particular nested eager scope reference if the bindings exist. - /// * `NotFound` if the bindings do not exist in the nested eager scope. - pub(crate) fn eager_bindings( - &self, - enclosing_scope: FileScopeId, - symbol: &str, - nested_scope: FileScopeId, - ) -> EagerBindingsResult<'_, 'db> { - for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) { - if ancestor_scope_id == enclosing_scope { - break; - } - if !ancestor_scope.is_eager() { - return EagerBindingsResult::NoLongerInEagerContext; - } - } - let Some(symbol_id) = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol) else { - return EagerBindingsResult::NotFound; - }; - let key = EagerBindingsKey { - enclosing_scope, - enclosing_symbol: symbol_id, - nested_scope, - }; - let Some(id) = self.eager_bindings.get(&key) else { - return EagerBindingsResult::NotFound; - }; - match self.use_def_maps[enclosing_scope].eager_bindings(*id) { - Some(bindings) => EagerBindingsResult::Found(bindings), - None => EagerBindingsResult::NotFound, - } - } -} - -pub struct AncestorsIter<'a> { - scopes: &'a IndexSlice, - next_id: Option, -} - -impl<'a> AncestorsIter<'a> { - fn new(module_symbol_table: &'a SemanticIndex, start: FileScopeId) -> Self { - Self { - scopes: &module_symbol_table.scopes, - next_id: Some(start), - } - } -} - -impl<'a> Iterator for AncestorsIter<'a> { - type Item = (FileScopeId, &'a Scope); - - fn next(&mut self) -> Option { - let current_id = self.next_id?; - let current = &self.scopes[current_id]; - self.next_id = current.parent(); - - Some((current_id, current)) - } -} - -impl FusedIterator for AncestorsIter<'_> {} - -pub struct DescendantsIter<'a> { - next_id: FileScopeId, - descendants: std::slice::Iter<'a, Scope>, -} - -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.descendants()]; - - Self { - next_id: scope_id + 1, - descendants: scopes.iter(), - } - } -} - -impl<'a> Iterator for DescendantsIter<'a> { - type Item = (FileScopeId, &'a Scope); - - fn next(&mut self) -> Option { - let descendant = self.descendants.next()?; - let id = self.next_id; - self.next_id = self.next_id + 1; - - Some((id, descendant)) - } - - fn size_hint(&self) -> (usize, Option) { - self.descendants.size_hint() - } -} - -impl FusedIterator for DescendantsIter<'_> {} - -impl ExactSizeIterator for DescendantsIter<'_> {} - -pub struct ChildrenIter<'a> { - parent: FileScopeId, - descendants: DescendantsIter<'a>, -} - -impl<'a> ChildrenIter<'a> { - fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self { - let descendants = DescendantsIter::new(module_symbol_table, parent); - - Self { - parent, - descendants, - } - } -} - -impl<'a> Iterator for ChildrenIter<'a> { - type Item = (FileScopeId, &'a Scope); - - fn next(&mut self) -> Option { - self.descendants - .find(|(_, scope)| scope.parent() == Some(self.parent)) - } -} - -impl FusedIterator for ChildrenIter<'_> {} - -#[cfg(test)] -mod tests { - use ruff_db::files::{system_path_to_file, File}; - use ruff_db::parsed::parsed_module; - use ruff_db::system::DbWithWritableSystem as _; - use ruff_python_ast as ast; - use ruff_text_size::{Ranged, TextRange}; - - use crate::db::tests::TestDb; - use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId}; - use crate::semantic_index::definition::{Definition, DefinitionKind}; - use crate::semantic_index::symbol::{ - FileScopeId, Scope, ScopeKind, ScopedSymbolId, SymbolTable, - }; - use crate::semantic_index::use_def::UseDefMap; - use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; - use crate::Db; - - impl UseDefMap<'_> { - fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option> { - self.public_bindings(symbol) - .find_map(|constrained_binding| constrained_binding.binding) - } - - fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option> { - self.bindings_at_use(use_id) - .find_map(|constrained_binding| constrained_binding.binding) - } - } - - struct TestCase { - db: TestDb, - file: File, - } - - fn test_case(content: impl AsRef) -> TestCase { - let mut db = TestDb::new(); - db.write_file("test.py", content).unwrap(); - - let file = system_path_to_file(&db, "test.py").unwrap(); - - TestCase { db, file } - } - - fn names(table: &SymbolTable) -> Vec { - table - .symbols() - .map(|symbol| symbol.name().to_string()) - .collect() - } - - #[test] - fn empty() { - let TestCase { db, file } = test_case(""); - let global_table = symbol_table(&db, global_scope(&db, file)); - - let global_names = names(&global_table); - - assert_eq!(global_names, Vec::<&str>::new()); - } - - #[test] - fn simple() { - let TestCase { db, file } = test_case("x"); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert_eq!(names(&global_table), vec!["x"]); - } - - #[test] - fn annotation_only() { - let TestCase { db, file } = test_case("x: int"); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert_eq!(names(&global_table), vec!["int", "x"]); - // TODO record definition - } - - #[test] - fn import() { - let TestCase { db, file } = test_case("import foo"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(names(&global_table), vec!["foo"]); - let foo = global_table.symbol_id_by_name("foo").unwrap(); - - let use_def = use_def_map(&db, scope); - let binding = use_def.first_public_binding(foo).unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Import(_))); - } - - #[test] - fn import_sub() { - let TestCase { db, file } = test_case("import foo.bar"); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert_eq!(names(&global_table), vec!["foo"]); - } - - #[test] - fn import_as() { - let TestCase { db, file } = test_case("import foo.bar as baz"); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert_eq!(names(&global_table), vec!["baz"]); - } - - #[test] - fn import_from() { - let TestCase { db, file } = test_case("from bar import foo"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(names(&global_table), vec!["foo"]); - assert!( - global_table - .symbol_by_name("foo") - .is_some_and(|symbol| { symbol.is_bound() && !symbol.is_used() }), - "symbols that are defined get the defined flag" - ); - - let use_def = use_def_map(&db, scope); - let binding = use_def - .first_public_binding( - global_table - .symbol_id_by_name("foo") - .expect("symbol to exist"), - ) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::ImportFrom(_))); - } - - #[test] - fn assign() { - let TestCase { db, file } = test_case("x = foo"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(names(&global_table), vec!["foo", "x"]); - assert!( - global_table - .symbol_by_name("foo") - .is_some_and(|symbol| { !symbol.is_bound() && symbol.is_used() }), - "a symbol used but not bound in a scope should have only the used flag" - ); - let use_def = use_def_map(&db, scope); - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").expect("symbol exists")) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); - } - - #[test] - fn augmented_assignment() { - let TestCase { db, file } = test_case("x += 1"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(names(&global_table), vec!["x"]); - - let use_def = use_def_map(&db, scope); - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").unwrap()) - .unwrap(); - - assert!(matches!( - binding.kind(&db), - DefinitionKind::AugmentedAssignment(_) - )); - } - - #[test] - fn class_scope() { - let TestCase { db, file } = test_case( - " -class C: - x = 1 -y = 2 -", - ); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert_eq!(names(&global_table), vec!["C", "y"]); - - let index = semantic_index(&db, file); - - let [(class_scope_id, class_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope") - }; - assert_eq!(class_scope.kind(), ScopeKind::Class); - assert_eq!(class_scope_id.to_scope_id(&db, file).name(&db), "C"); - - let class_table = index.symbol_table(class_scope_id); - assert_eq!(names(&class_table), vec!["x"]); - - let use_def = index.use_def_map(class_scope_id); - let binding = use_def - .first_public_binding(class_table.symbol_id_by_name("x").expect("symbol exists")) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); - } - - #[test] - fn function_scope() { - let TestCase { db, file } = test_case( - " -def func(): - x = 1 -y = 2 -", - ); - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["func", "y"]); - - let [(function_scope_id, function_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope") - }; - assert_eq!(function_scope.kind(), ScopeKind::Function); - assert_eq!(function_scope_id.to_scope_id(&db, file).name(&db), "func"); - - let function_table = index.symbol_table(function_scope_id); - assert_eq!(names(&function_table), vec!["x"]); - - let use_def = index.use_def_map(function_scope_id); - let binding = use_def - .first_public_binding( - function_table - .symbol_id_by_name("x") - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); - } - - #[test] - fn function_parameter_symbols() { - let TestCase { db, file } = test_case( - " -def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): - pass -", - ); - - let index = semantic_index(&db, file); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert_eq!(names(&global_table), vec!["str", "int", "f"]); - - let [(function_scope_id, _function_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("Expected a function scope") - }; - - let function_table = index.symbol_table(function_scope_id); - assert_eq!( - names(&function_table), - vec!["a", "b", "c", "d", "args", "kwargs"], - ); - - let use_def = index.use_def_map(function_scope_id); - for name in ["a", "b", "c", "d"] { - let binding = use_def - .first_public_binding( - function_table - .symbol_id_by_name(name) - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_))); - } - let args_binding = use_def - .first_public_binding( - function_table - .symbol_id_by_name("args") - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!( - args_binding.kind(&db), - DefinitionKind::VariadicPositionalParameter(_) - )); - let kwargs_binding = use_def - .first_public_binding( - function_table - .symbol_id_by_name("kwargs") - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!( - kwargs_binding.kind(&db), - DefinitionKind::VariadicKeywordParameter(_) - )); - } - - #[test] - fn lambda_parameter_symbols() { - let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None"); - - let index = semantic_index(&db, file); - let global_table = symbol_table(&db, global_scope(&db, file)); - - assert!(names(&global_table).is_empty()); - - let [(lambda_scope_id, _lambda_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("Expected a lambda scope") - }; - - let lambda_table = index.symbol_table(lambda_scope_id); - assert_eq!( - names(&lambda_table), - vec!["a", "b", "c", "d", "args", "kwargs"], - ); - - let use_def = index.use_def_map(lambda_scope_id); - for name in ["a", "b", "c", "d"] { - let binding = use_def - .first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists")) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_))); - } - let args_binding = use_def - .first_public_binding( - lambda_table - .symbol_id_by_name("args") - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!( - args_binding.kind(&db), - DefinitionKind::VariadicPositionalParameter(_) - )); - let kwargs_binding = use_def - .first_public_binding( - lambda_table - .symbol_id_by_name("kwargs") - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!( - kwargs_binding.kind(&db), - DefinitionKind::VariadicKeywordParameter(_) - )); - } - - /// Test case to validate that the comprehension scope is correctly identified and that the target - /// variable is defined only in the comprehension scope and not in the global scope. - #[test] - fn comprehension_scope() { - let TestCase { db, file } = test_case( - " -[x for x, y in iter1] -", - ); - - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["iter1"]); - - let [(comprehension_scope_id, comprehension_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope") - }; - - assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); - assert_eq!( - comprehension_scope_id.to_scope_id(&db, file).name(&db), - "" - ); - - let comprehension_symbol_table = index.symbol_table(comprehension_scope_id); - - assert_eq!(names(&comprehension_symbol_table), vec!["x", "y"]); - - let use_def = index.use_def_map(comprehension_scope_id); - for name in ["x", "y"] { - let binding = use_def - .first_public_binding( - comprehension_symbol_table - .symbol_id_by_name(name) - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!( - binding.kind(&db), - DefinitionKind::Comprehension(_) - )); - } - } - - /// Test case to validate that the `x` variable used in the comprehension is referencing the - /// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one. - #[test] - fn multiple_generators() { - let TestCase { db, file } = test_case( - " -[x for x in iter1 for x in iter2] -", - ); - - let index = semantic_index(&db, file); - let [(comprehension_scope_id, _)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope") - }; - - let use_def = index.use_def_map(comprehension_scope_id); - - let module = parsed_module(&db, file).syntax(); - let element = module.body[0] - .as_expr_stmt() - .unwrap() - .value - .as_list_comp_expr() - .unwrap() - .elt - .as_name_expr() - .unwrap(); - let element_use_id = - element.scoped_use_id(&db, comprehension_scope_id.to_scope_id(&db, file)); - - let binding = use_def.first_binding_at_use(element_use_id).unwrap(); - let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else { - panic!("expected generator definition") - }; - let target = comprehension.target(); - let name = target.id().as_str(); - - assert_eq!(name, "x"); - assert_eq!(target.range(), TextRange::new(23.into(), 24.into())); - } - - /// Test case to validate that the nested comprehension creates a new scope which is a child of - /// the outer comprehension scope and the variables are correctly defined in the respective - /// scopes. - #[test] - fn nested_generators() { - let TestCase { db, file } = test_case( - " -[{x for x in iter2} for y in iter1] -", - ); - - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["iter1"]); - - let [(comprehension_scope_id, comprehension_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope") - }; - - assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); - assert_eq!( - comprehension_scope_id.to_scope_id(&db, file).name(&db), - "" - ); - - let comprehension_symbol_table = index.symbol_table(comprehension_scope_id); - - assert_eq!(names(&comprehension_symbol_table), vec!["y", "iter2"]); - - let [(inner_comprehension_scope_id, inner_comprehension_scope)] = index - .child_scopes(comprehension_scope_id) - .collect::>()[..] - else { - panic!("expected one inner generator scope") - }; - - assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension); - assert_eq!( - inner_comprehension_scope_id - .to_scope_id(&db, file) - .name(&db), - "" - ); - - let inner_comprehension_symbol_table = index.symbol_table(inner_comprehension_scope_id); - - assert_eq!(names(&inner_comprehension_symbol_table), vec!["x"]); - } - - #[test] - fn with_item_definition() { - let TestCase { db, file } = test_case( - " -with item1 as x, item2 as y: - pass -", - ); - - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["item1", "x", "item2", "y"]); - - let use_def = index.use_def_map(FileScopeId::global()); - for name in ["x", "y"] { - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) - .expect("Expected with item definition for {name}"); - assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_))); - } - } - - #[test] - fn with_item_unpacked_definition() { - let TestCase { db, file } = test_case( - " -with context() as (x, y): - pass -", - ); - - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["context", "x", "y"]); - - let use_def = index.use_def_map(FileScopeId::global()); - for name in ["x", "y"] { - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) - .expect("Expected with item definition for {name}"); - assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_))); - } - } - - #[test] - fn dupes() { - let TestCase { db, file } = test_case( - " -def func(): - x = 1 -def func(): - y = 2 -", - ); - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["func"]); - let [(func_scope1_id, func_scope_1), (func_scope2_id, func_scope_2)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected two child scopes"); - }; - - assert_eq!(func_scope_1.kind(), ScopeKind::Function); - - assert_eq!(func_scope1_id.to_scope_id(&db, file).name(&db), "func"); - assert_eq!(func_scope_2.kind(), ScopeKind::Function); - assert_eq!(func_scope2_id.to_scope_id(&db, file).name(&db), "func"); - - let func1_table = index.symbol_table(func_scope1_id); - let func2_table = index.symbol_table(func_scope2_id); - assert_eq!(names(&func1_table), vec!["x"]); - assert_eq!(names(&func2_table), vec!["y"]); - - let use_def = index.use_def_map(FileScopeId::global()); - let binding = use_def - .first_public_binding( - global_table - .symbol_id_by_name("func") - .expect("symbol exists"), - ) - .unwrap(); - assert!(matches!(binding.kind(&db), DefinitionKind::Function(_))); - } - - #[test] - fn generic_function() { - let TestCase { db, file } = test_case( - " -def func[T](): - x = 1 -", - ); - - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["func"]); - - let [(ann_scope_id, ann_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope"); - }; - - assert_eq!(ann_scope.kind(), ScopeKind::Annotation); - assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db), "func"); - let ann_table = index.symbol_table(ann_scope_id); - assert_eq!(names(&ann_table), vec!["T"]); - - let [(func_scope_id, func_scope)] = - index.child_scopes(ann_scope_id).collect::>()[..] - else { - panic!("expected one child scope"); - }; - assert_eq!(func_scope.kind(), ScopeKind::Function); - assert_eq!(func_scope_id.to_scope_id(&db, file).name(&db), "func"); - let func_table = index.symbol_table(func_scope_id); - assert_eq!(names(&func_table), vec!["x"]); - } - - #[test] - fn generic_class() { - let TestCase { db, file } = test_case( - " -class C[T]: - x = 1 -", - ); - - let index = semantic_index(&db, file); - let global_table = index.symbol_table(FileScopeId::global()); - - assert_eq!(names(&global_table), vec!["C"]); - - let [(ann_scope_id, ann_scope)] = index - .child_scopes(FileScopeId::global()) - .collect::>()[..] - else { - panic!("expected one child scope"); - }; - - assert_eq!(ann_scope.kind(), ScopeKind::Annotation); - assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db), "C"); - let ann_table = index.symbol_table(ann_scope_id); - assert_eq!(names(&ann_table), vec!["T"]); - assert!( - ann_table - .symbol_by_name("T") - .is_some_and(|s| s.is_bound() && !s.is_used()), - "type parameters are defined by the scope that introduces them" - ); - - let [(class_scope_id, class_scope)] = - index.child_scopes(ann_scope_id).collect::>()[..] - else { - panic!("expected one child scope"); - }; - - assert_eq!(class_scope.kind(), ScopeKind::Class); - assert_eq!(class_scope_id.to_scope_id(&db, file).name(&db), "C"); - assert_eq!(names(&index.symbol_table(class_scope_id)), vec!["x"]); - } - - #[test] - fn reachability_trivial() { - let TestCase { db, file } = test_case("x = 1; x"); - let parsed = parsed_module(&db, file); - let scope = global_scope(&db, file); - let ast = parsed.syntax(); - let ast::Stmt::Expr(ast::StmtExpr { - value: x_use_expr, .. - }) = &ast.body[1] - else { - panic!("should be an expr") - }; - let ast::Expr::Name(x_use_expr_name) = x_use_expr.as_ref() else { - panic!("expected a Name"); - }; - let x_use_id = x_use_expr_name.scoped_use_id(&db, scope); - let use_def = use_def_map(&db, scope); - let binding = use_def.first_binding_at_use(x_use_id).unwrap(); - let DefinitionKind::Assignment(assignment) = binding.kind(&db) else { - panic!("should be an assignment definition") - }; - let ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Int(num), - .. - }) = assignment.value() - else { - panic!("should be a number literal") - }; - assert_eq!(*num, 1); - } - - #[test] - fn expression_scope() { - let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4"); - - let index = semantic_index(&db, file); - let parsed = parsed_module(&db, file); - let ast = parsed.syntax(); - - let x_stmt = ast.body[0].as_assign_stmt().unwrap(); - let x = &x_stmt.targets[0]; - - assert_eq!(index.expression_scope(x).kind(), ScopeKind::Module); - assert_eq!(index.expression_scope_id(x), FileScopeId::global()); - - let def = ast.body[1].as_function_def_stmt().unwrap(); - let y_stmt = def.body[0].as_assign_stmt().unwrap(); - let y = &y_stmt.targets[0]; - - assert_eq!(index.expression_scope(y).kind(), ScopeKind::Function); - } - - #[test] - fn scope_iterators() { - fn scope_names<'a>( - scopes: impl Iterator, - db: &'a dyn Db, - file: File, - ) -> Vec<&'a str> { - scopes - .into_iter() - .map(|(scope_id, _)| scope_id.to_scope_id(db, file).name(db)) - .collect() - } - - let TestCase { db, file } = test_case( - r" -class Test: - def foo(): - def bar(): - ... - def baz(): - pass - -def x(): - pass", - ); - - let index = semantic_index(&db, file); - - let descendants = index.descendent_scopes(FileScopeId::global()); - assert_eq!( - scope_names(descendants, &db, file), - vec!["Test", "foo", "bar", "baz", "x"] - ); - - let children = index.child_scopes(FileScopeId::global()); - assert_eq!(scope_names(children, &db, file), vec!["Test", "x"]); - - let test_class = index.child_scopes(FileScopeId::global()).next().unwrap().0; - let test_child_scopes = index.child_scopes(test_class); - assert_eq!( - scope_names(test_child_scopes, &db, file), - vec!["foo", "baz"] - ); - - let bar_scope = index - .descendent_scopes(FileScopeId::global()) - .nth(2) - .unwrap() - .0; - let ancestors = index.ancestor_scopes(bar_scope); - - assert_eq!( - scope_names(ancestors, &db, file), - vec!["bar", "foo", "Test", ""] - ); - } - - #[test] - fn match_stmt() { - let TestCase { db, file } = test_case( - " -match subject: - case a: ... - case [b, c, *d]: ... - case e as f: ... - case {'x': g, **h}: ... - case Foo(i, z=j): ... - case k | l: ... - case _: ... -", - ); - - let global_scope_id = global_scope(&db, file); - let global_table = symbol_table(&db, global_scope_id); - - assert!(global_table.symbol_by_name("Foo").unwrap().is_used()); - assert_eq!( - names(&global_table), - vec!["subject", "a", "b", "c", "d", "e", "f", "g", "h", "Foo", "i", "j", "k", "l"] - ); - - let use_def = use_def_map(&db, global_scope_id); - for (name, expected_index) in [ - ("a", 0), - ("b", 0), - ("c", 1), - ("d", 2), - ("e", 0), - ("f", 1), - ("g", 0), - ("h", 1), - ("i", 0), - ("j", 1), - ("k", 0), - ("l", 1), - ] { - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) - .expect("Expected with item definition for {name}"); - if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) { - assert_eq!(pattern.index(), expected_index); - } else { - panic!("Expected match pattern definition for {name}"); - } - } - } - - #[test] - fn nested_match_case() { - let TestCase { db, file } = test_case( - " -match 1: - case first: - match 2: - case second: - pass -", - ); - - let global_scope_id = global_scope(&db, file); - let global_table = symbol_table(&db, global_scope_id); - - assert_eq!(names(&global_table), vec!["first", "second"]); - - let use_def = use_def_map(&db, global_scope_id); - for (name, expected_index) in [("first", 0), ("second", 0)] { - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists")) - .expect("Expected with item definition for {name}"); - if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) { - assert_eq!(pattern.index(), expected_index); - } else { - panic!("Expected match pattern definition for {name}"); - } - } - } - - #[test] - fn for_loops_single_assignment() { - let TestCase { db, file } = test_case("for x in a: pass"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(&names(&global_table), &["a", "x"]); - - let use_def = use_def_map(&db, scope); - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").unwrap()) - .unwrap(); - - assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); - } - - #[test] - fn for_loops_simple_unpacking() { - let TestCase { db, file } = test_case("for (x, y) in a: pass"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(&names(&global_table), &["a", "x", "y"]); - - let use_def = use_def_map(&db, scope); - let x_binding = use_def - .first_public_binding(global_table.symbol_id_by_name("x").unwrap()) - .unwrap(); - let y_binding = use_def - .first_public_binding(global_table.symbol_id_by_name("y").unwrap()) - .unwrap(); - - assert!(matches!(x_binding.kind(&db), DefinitionKind::For(_))); - assert!(matches!(y_binding.kind(&db), DefinitionKind::For(_))); - } - - #[test] - fn for_loops_complex_unpacking() { - let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass"); - let scope = global_scope(&db, file); - let global_table = symbol_table(&db, scope); - - assert_eq!(&names(&global_table), &["e", "a", "b", "c", "d"]); - - let use_def = use_def_map(&db, scope); - let binding = use_def - .first_public_binding(global_table.symbol_id_by_name("a").unwrap()) - .unwrap(); - - assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs b/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs deleted file mode 100644 index 160f3af7b9cd2..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs +++ /dev/null @@ -1,199 +0,0 @@ -use rustc_hash::FxHashMap; - -use ruff_index::newtype_index; -use ruff_python_ast as ast; -use ruff_python_ast::ExprRef; - -use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; -use crate::semantic_index::semantic_index; -use crate::semantic_index::symbol::ScopeId; -use crate::Db; - -/// AST ids for a single scope. -/// -/// The motivation for building the AST ids per scope isn't about reducing invalidation because -/// the struct changes whenever the parsed AST changes. Instead, it's mainly that we can -/// build the AST ids struct when building the symbol table and also keep the property that -/// IDs of outer scopes are unaffected by changes in inner scopes. -/// -/// For example, we don't want that adding new statements to `foo` changes the statement id of `x = foo()` in: -/// -/// ```python -/// def foo(): -/// return 5 -/// -/// x = foo() -/// ``` -#[derive(Debug, salsa::Update)] -pub(crate) struct AstIds { - /// Maps expressions to their expression id. - expressions_map: FxHashMap, - /// Maps expressions which "use" a symbol (that is, [`ast::ExprName`]) to a use id. - uses_map: FxHashMap, -} - -impl AstIds { - fn expression_id(&self, key: impl Into) -> ScopedExpressionId { - let key = &key.into(); - *self.expressions_map.get(key).unwrap_or_else(|| { - panic!("Could not find expression ID for {key:?}"); - }) - } - - fn use_id(&self, key: impl Into) -> ScopedUseId { - self.uses_map[&key.into()] - } -} - -fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { - semantic_index(db, scope.file(db)).ast_ids(scope.file_scope_id(db)) -} - -/// Uniquely identifies a use of a name in a [`crate::semantic_index::symbol::FileScopeId`]. -#[newtype_index] -pub struct ScopedUseId; - -pub trait HasScopedUseId { - /// Returns the ID that uniquely identifies the use in `scope`. - fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId; -} - -impl HasScopedUseId for ast::ExprName { - fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { - let expression_ref = ExprRef::from(self); - expression_ref.scoped_use_id(db, scope) - } -} - -impl HasScopedUseId for ast::ExprRef<'_> { - fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { - let ast_ids = ast_ids(db, scope); - ast_ids.use_id(*self) - } -} - -/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`]. -#[newtype_index] -#[derive(salsa::Update)] -pub struct ScopedExpressionId; - -pub trait HasScopedExpressionId { - /// Returns the ID that uniquely identifies the node in `scope`. - fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId; -} - -impl HasScopedExpressionId for Box { - fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId { - self.as_ref().scoped_expression_id(db, scope) - } -} - -macro_rules! impl_has_scoped_expression_id { - ($ty: ty) => { - impl HasScopedExpressionId for $ty { - fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId { - let expression_ref = ExprRef::from(self); - expression_ref.scoped_expression_id(db, scope) - } - } - }; -} - -impl_has_scoped_expression_id!(ast::ExprBoolOp); -impl_has_scoped_expression_id!(ast::ExprName); -impl_has_scoped_expression_id!(ast::ExprBinOp); -impl_has_scoped_expression_id!(ast::ExprUnaryOp); -impl_has_scoped_expression_id!(ast::ExprLambda); -impl_has_scoped_expression_id!(ast::ExprIf); -impl_has_scoped_expression_id!(ast::ExprDict); -impl_has_scoped_expression_id!(ast::ExprSet); -impl_has_scoped_expression_id!(ast::ExprListComp); -impl_has_scoped_expression_id!(ast::ExprSetComp); -impl_has_scoped_expression_id!(ast::ExprDictComp); -impl_has_scoped_expression_id!(ast::ExprGenerator); -impl_has_scoped_expression_id!(ast::ExprAwait); -impl_has_scoped_expression_id!(ast::ExprYield); -impl_has_scoped_expression_id!(ast::ExprYieldFrom); -impl_has_scoped_expression_id!(ast::ExprCompare); -impl_has_scoped_expression_id!(ast::ExprCall); -impl_has_scoped_expression_id!(ast::ExprFString); -impl_has_scoped_expression_id!(ast::ExprStringLiteral); -impl_has_scoped_expression_id!(ast::ExprBytesLiteral); -impl_has_scoped_expression_id!(ast::ExprNumberLiteral); -impl_has_scoped_expression_id!(ast::ExprBooleanLiteral); -impl_has_scoped_expression_id!(ast::ExprNoneLiteral); -impl_has_scoped_expression_id!(ast::ExprEllipsisLiteral); -impl_has_scoped_expression_id!(ast::ExprAttribute); -impl_has_scoped_expression_id!(ast::ExprSubscript); -impl_has_scoped_expression_id!(ast::ExprStarred); -impl_has_scoped_expression_id!(ast::ExprNamed); -impl_has_scoped_expression_id!(ast::ExprList); -impl_has_scoped_expression_id!(ast::ExprTuple); -impl_has_scoped_expression_id!(ast::ExprSlice); -impl_has_scoped_expression_id!(ast::ExprIpyEscapeCommand); -impl_has_scoped_expression_id!(ast::Expr); - -impl HasScopedExpressionId for ast::ExprRef<'_> { - fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId { - let ast_ids = ast_ids(db, scope); - ast_ids.expression_id(*self) - } -} - -#[derive(Debug, Default)] -pub(super) struct AstIdsBuilder { - expressions_map: FxHashMap, - uses_map: FxHashMap, -} - -impl AstIdsBuilder { - /// Adds `expr` to the expression ids map and returns its id. - pub(super) fn record_expression(&mut self, expr: &ast::Expr) -> ScopedExpressionId { - let expression_id = self.expressions_map.len().into(); - - self.expressions_map.insert(expr.into(), expression_id); - - expression_id - } - - /// Adds `expr` to the use ids map and returns its id. - pub(super) fn record_use(&mut self, expr: &ast::Expr) -> ScopedUseId { - let use_id = self.uses_map.len().into(); - - self.uses_map.insert(expr.into(), use_id); - - use_id - } - - pub(super) fn finish(mut self) -> AstIds { - self.expressions_map.shrink_to_fit(); - self.uses_map.shrink_to_fit(); - - AstIds { - expressions_map: self.expressions_map, - uses_map: self.uses_map, - } - } -} - -/// Node key that can only be constructed for expressions. -pub(crate) mod node_key { - use ruff_python_ast as ast; - - use crate::node_key::NodeKey; - - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)] - pub(crate) struct ExpressionNodeKey(NodeKey); - - impl From> for ExpressionNodeKey { - fn from(value: ast::ExprRef<'_>) -> Self { - Self(NodeKey::from_node(value)) - } - } - - impl From<&ast::Expr> for ExpressionNodeKey { - fn from(value: &ast::Expr) -> Self { - Self(NodeKey::from_node(value)) - } - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs deleted file mode 100644 index f2d2a30224c51..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ /dev/null @@ -1,2299 +0,0 @@ -use std::sync::Arc; - -use except_handlers::TryNodeContextStackManager; -use rustc_hash::{FxHashMap, FxHashSet}; - -use ruff_db::files::File; -use ruff_db::parsed::ParsedModule; -use ruff_index::IndexVec; -use ruff_python_ast::name::Name; -use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor}; -use ruff_python_ast::{self as ast}; - -use crate::ast_node_ref::AstNodeRef; -use crate::module_name::ModuleName; -use crate::module_resolver::resolve_module; -use crate::node_key::NodeKey; -use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; -use crate::semantic_index::ast_ids::AstIdsBuilder; -use crate::semantic_index::definition::{ - AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef, - AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, - Definition, DefinitionCategory, DefinitionKind, DefinitionNodeKey, DefinitionNodeRef, - Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionKind, ForStmtDefinitionNodeRef, - ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, - StarImportDefinitionNodeRef, TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef, -}; -use crate::semantic_index::expression::{Expression, ExpressionKind}; -use crate::semantic_index::predicate::{ - PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, ScopedPredicateId, - StarImportPlaceholderPredicate, -}; -use crate::semantic_index::re_exports::exported_names; -use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, - SymbolTableBuilder, -}; -use crate::semantic_index::use_def::{ - EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder, -}; -use crate::semantic_index::visibility_constraints::{ - ScopedVisibilityConstraintId, VisibilityConstraintsBuilder, -}; -use crate::semantic_index::SemanticIndex; -use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; -use crate::Db; - -mod except_handlers; - -#[derive(Clone, Debug, Default)] -struct Loop { - /// Flow states at each `break` in the current loop. - break_states: Vec, -} - -impl Loop { - fn push_break(&mut self, state: FlowSnapshot) { - self.break_states.push(state); - } -} - -struct ScopeInfo { - file_scope_id: FileScopeId, - /// Current loop state; None if we are not currently visiting a loop - current_loop: Option, -} - -pub(super) struct SemanticIndexBuilder<'db> { - // Builder state - db: &'db dyn Db, - file: File, - module: &'db ParsedModule, - scope_stack: Vec, - /// The assignments we're currently visiting, with - /// the most recent visit at the end of the Vec - current_assignments: Vec>, - /// The match case we're currently visiting. - current_match_case: Option>, - /// The name of the first function parameter of the innermost function that we're currently visiting. - current_first_parameter_name: Option<&'db str>, - - /// Per-scope contexts regarding nested `try`/`except` statements - try_node_context_stack_manager: TryNodeContextStackManager, - - /// Flags about the file's global scope - has_future_annotations: bool, - - // Semantic Index fields - scopes: IndexVec, - scope_ids_by_scope: IndexVec>, - symbol_tables: IndexVec, - instance_attribute_tables: IndexVec, - ast_ids: IndexVec, - use_def_maps: IndexVec>, - scopes_by_node: FxHashMap, - scopes_by_expression: FxHashMap, - definitions_by_node: FxHashMap>, - expressions_by_node: FxHashMap>, - imported_modules: FxHashSet, - eager_bindings: FxHashMap, -} - -impl<'db> SemanticIndexBuilder<'db> { - pub(super) fn new(db: &'db dyn Db, file: File, parsed: &'db ParsedModule) -> Self { - let mut builder = Self { - db, - file, - module: parsed, - scope_stack: Vec::new(), - current_assignments: vec![], - current_match_case: None, - current_first_parameter_name: None, - try_node_context_stack_manager: TryNodeContextStackManager::default(), - - has_future_annotations: false, - - scopes: IndexVec::new(), - symbol_tables: IndexVec::new(), - instance_attribute_tables: IndexVec::new(), - ast_ids: IndexVec::new(), - scope_ids_by_scope: IndexVec::new(), - use_def_maps: IndexVec::new(), - - scopes_by_expression: FxHashMap::default(), - scopes_by_node: FxHashMap::default(), - definitions_by_node: FxHashMap::default(), - expressions_by_node: FxHashMap::default(), - - imported_modules: FxHashSet::default(), - - eager_bindings: FxHashMap::default(), - }; - - builder.push_scope_with_parent( - NodeWithScopeRef::Module, - None, - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - - builder - } - - fn current_scope_info(&self) -> &ScopeInfo { - self.scope_stack - .last() - .expect("SemanticIndexBuilder should have created a root scope") - } - - fn current_scope_info_mut(&mut self) -> &mut ScopeInfo { - self.scope_stack - .last_mut() - .expect("SemanticIndexBuilder should have created a root scope") - } - - fn current_scope(&self) -> FileScopeId { - 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 - /// function body. - fn is_method_of_class(&self) -> Option { - let mut scopes_rev = self.scope_stack.iter().rev(); - let current = scopes_rev.next()?; - let parent = scopes_rev.next()?; - - match ( - self.scopes[current.file_scope_id].kind(), - self.scopes[parent.file_scope_id].kind(), - ) { - (ScopeKind::Function, ScopeKind::Class) => Some(parent.file_scope_id), - _ => None, - } - } - - /// Push a new loop, returning the outer loop, if any. - fn push_loop(&mut self) -> Option { - self.current_scope_info_mut() - .current_loop - .replace(Loop::default()) - } - - /// Pop a loop, replacing with the previous saved outer loop, if any. - fn pop_loop(&mut self, outer_loop: Option) -> Loop { - std::mem::replace(&mut self.current_scope_info_mut().current_loop, outer_loop) - .expect("pop_loop() should not be called without a prior push_loop()") - } - - fn current_loop_mut(&mut self) -> Option<&mut Loop> { - self.current_scope_info_mut().current_loop.as_mut() - } - - fn push_scope(&mut self, node: NodeWithScopeRef) { - let parent = self.current_scope(); - let reachabililty = self.current_use_def_map().reachability; - self.push_scope_with_parent(node, Some(parent), reachabililty); - } - - fn push_scope_with_parent( - &mut self, - node: NodeWithScopeRef, - parent: Option, - reachability: ScopedVisibilityConstraintId, - ) { - let children_start = self.scopes.next_index() + 1; - - // SAFETY: `node` is guaranteed to be a child of `self.module` - #[allow(unsafe_code)] - let node_with_kind = unsafe { node.to_kind(self.module.clone()) }; - - let scope = Scope::new( - parent, - node_with_kind, - children_start..children_start, - reachability, - ); - self.try_node_context_stack_manager.enter_nested_scope(); - - let file_scope_id = self.scopes.push(scope); - self.symbol_tables.push(SymbolTableBuilder::default()); - self.instance_attribute_tables - .push(SymbolTableBuilder::default()); - self.use_def_maps.push(UseDefMapBuilder::default()); - let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default()); - - let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default()); - - self.scope_ids_by_scope.push(scope_id); - let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id); - debug_assert_eq!(previous, None); - - debug_assert_eq!(ast_id_scope, file_scope_id); - - self.scope_stack.push(ScopeInfo { - file_scope_id, - current_loop: None, - }); - } - - fn pop_scope(&mut self) -> FileScopeId { - self.try_node_context_stack_manager.exit_scope(); - - let ScopeInfo { - file_scope_id: popped_scope_id, - .. - } = self - .scope_stack - .pop() - .expect("Root scope should be present"); - - let children_end = self.scopes.next_index(); - let popped_scope = &mut self.scopes[popped_scope_id]; - popped_scope.extend_descendants(children_end); - - if !popped_scope.is_eager() { - return popped_scope_id; - } - - // If the scope that we just popped off is an eager scope, we need to "lock" our view of - // which bindings reach each of the uses in the scope. Loop through each enclosing scope, - // looking for any that bind each symbol. - for enclosing_scope_info in self.scope_stack.iter().rev() { - let enclosing_scope_id = enclosing_scope_info.file_scope_id; - let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind(); - let enclosing_symbol_table = &self.symbol_tables[enclosing_scope_id]; - - // Names bound in class scopes are never visible to nested scopes, so we never need to - // save eager scope bindings in a class scope. - if enclosing_scope_kind.is_class() { - continue; - } - - for nested_symbol in self.symbol_tables[popped_scope_id].symbols() { - // Skip this symbol if this enclosing scope doesn't contain any bindings for it. - // Note that even if this symbol is bound in the popped scope, - // it may refer to the enclosing scope bindings - // so we also need to snapshot the bindings of the enclosing scope. - - let Some(enclosing_symbol_id) = - enclosing_symbol_table.symbol_id_by_name(nested_symbol.name()) - else { - continue; - }; - let enclosing_symbol = enclosing_symbol_table.symbol(enclosing_symbol_id); - if !enclosing_symbol.is_bound() { - continue; - } - - // Snapshot the bindings of this symbol that are visible at this point in this - // enclosing scope. - let key = EagerBindingsKey { - enclosing_scope: enclosing_scope_id, - enclosing_symbol: enclosing_symbol_id, - nested_scope: popped_scope_id, - }; - let eager_bindings = self.use_def_maps[enclosing_scope_id] - .snapshot_eager_bindings(enclosing_symbol_id); - self.eager_bindings.insert(key, eager_bindings); - } - - // Lazy scopes are "sticky": once we see a lazy scope we stop doing lookups - // eagerly, even if we would encounter another eager enclosing scope later on. - if !enclosing_scope_kind.is_eager() { - break; - } - } - - popped_scope_id - } - - fn current_symbol_table(&mut self) -> &mut SymbolTableBuilder { - let scope_id = self.current_scope(); - &mut self.symbol_tables[scope_id] - } - - fn current_attribute_table(&mut self) -> &mut SymbolTableBuilder { - let scope_id = self.current_scope(); - &mut self.instance_attribute_tables[scope_id] - } - - fn current_use_def_map_mut(&mut self) -> &mut UseDefMapBuilder<'db> { - let scope_id = self.current_scope(); - &mut self.use_def_maps[scope_id] - } - - fn current_use_def_map(&self) -> &UseDefMapBuilder<'db> { - let scope_id = self.current_scope(); - &self.use_def_maps[scope_id] - } - - fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder { - let scope_id = self.current_scope(); - &mut self.use_def_maps[scope_id].visibility_constraints - } - - fn current_ast_ids(&mut self) -> &mut AstIdsBuilder { - let scope_id = self.current_scope(); - &mut self.ast_ids[scope_id] - } - - fn flow_snapshot(&self) -> FlowSnapshot { - self.current_use_def_map().snapshot() - } - - fn flow_restore(&mut self, state: FlowSnapshot) { - self.current_use_def_map_mut().restore(state); - } - - fn flow_merge(&mut self, state: FlowSnapshot) { - self.current_use_def_map_mut().merge(state); - } - - /// Return a 2-element tuple, where the first element is the [`ScopedSymbolId`] of the - /// symbol added, and the second element is a boolean indicating whether the symbol was *newly* - /// added or not - fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) { - let (symbol_id, added) = self.current_symbol_table().add_symbol(name); - if added { - self.current_use_def_map_mut().add_symbol(symbol_id); - } - (symbol_id, added) - } - - fn add_attribute(&mut self, name: Name) -> ScopedSymbolId { - let (symbol_id, added) = self.current_attribute_table().add_symbol(name); - if added { - self.current_use_def_map_mut().add_attribute(symbol_id); - } - symbol_id - } - - fn mark_symbol_bound(&mut self, id: ScopedSymbolId) { - self.current_symbol_table().mark_symbol_bound(id); - } - - fn mark_symbol_declared(&mut self, id: ScopedSymbolId) { - self.current_symbol_table().mark_symbol_declared(id); - } - - fn mark_symbol_used(&mut self, id: ScopedSymbolId) { - 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> + 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>, - ) -> (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, - self.current_scope(), - symbol, - kind, - is_reexported, - countme::Count::default(), - ); - - 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); - } - if category.is_declaration() { - self.mark_symbol_declared(symbol); - } - - let use_def = self.current_use_def_map_mut(); - match category { - DefinitionCategory::DeclarationAndBinding => { - use_def.record_declaration_and_binding(symbol, definition); - } - DefinitionCategory::Declaration => use_def.record_declaration(symbol, definition), - DefinitionCategory::Binding => use_def.record_binding(symbol, definition), - } - - let mut try_node_stack_manager = std::mem::take(&mut self.try_node_context_stack_manager); - try_node_stack_manager.record_definition(self); - self.try_node_context_stack_manager = try_node_stack_manager; - - (definition, num_definitions) - } - - fn add_attribute_definition( - &mut self, - symbol: ScopedSymbolId, - definition_kind: DefinitionKind<'db>, - ) -> Definition { - let definition = Definition::new( - self.db, - self.file, - self.current_scope(), - symbol, - definition_kind, - false, - countme::Count::default(), - ); - self.current_use_def_map_mut() - .record_attribute_binding(symbol, definition); - definition - } - - fn record_expression_narrowing_constraint( - &mut self, - precide_node: &ast::Expr, - ) -> Predicate<'db> { - let predicate = self.build_predicate(precide_node); - self.record_narrowing_constraint(predicate); - predicate - } - - fn build_predicate(&mut self, predicate_node: &ast::Expr) -> Predicate<'db> { - let expression = self.add_standalone_expression(predicate_node); - Predicate { - node: PredicateNode::Expression(expression), - is_positive: true, - } - } - - /// Adds a new predicate to the list of all predicates, but does not record it. Returns the - /// predicate ID for later recording using - /// [`SemanticIndexBuilder::record_narrowing_constraint_id`]. - fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { - self.current_use_def_map_mut().add_predicate(predicate) - } - - /// Negates a predicate and adds it to the list of all predicates, does not record it. - fn add_negated_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { - let negated = Predicate { - node: predicate.node, - is_positive: false, - }; - self.current_use_def_map_mut().add_predicate(negated) - } - - /// Records a previously added narrowing constraint by adding it to all live bindings. - fn record_narrowing_constraint_id(&mut self, predicate: ScopedPredicateId) { - self.current_use_def_map_mut() - .record_narrowing_constraint(predicate); - } - - /// Adds and records a narrowing constraint, i.e. adds it to all live bindings. - fn record_narrowing_constraint(&mut self, predicate: Predicate<'db>) { - let use_def = self.current_use_def_map_mut(); - let predicate_id = use_def.add_predicate(predicate); - use_def.record_narrowing_constraint(predicate_id); - } - - /// Negates the given predicate and then adds it as a narrowing constraint to all live - /// bindings. - fn record_negated_narrowing_constraint( - &mut self, - predicate: Predicate<'db>, - ) -> ScopedPredicateId { - let id = self.add_negated_predicate(predicate); - self.record_narrowing_constraint_id(id); - id - } - - /// Records a previously added visibility constraint by applying it to all live bindings - /// and declarations. - fn record_visibility_constraint_id(&mut self, constraint: ScopedVisibilityConstraintId) { - self.current_use_def_map_mut() - .record_visibility_constraint(constraint); - } - - /// Negates the given visibility constraint and then adds it to all live bindings and declarations. - fn record_negated_visibility_constraint( - &mut self, - constraint: ScopedVisibilityConstraintId, - ) -> ScopedVisibilityConstraintId { - let id = self - .current_visibility_constraints_mut() - .add_not_constraint(constraint); - self.record_visibility_constraint_id(id); - id - } - - /// Records a visibility constraint by applying it to all live bindings and declarations. - #[must_use = "A visibility constraint must always be negated after it is added"] - fn record_visibility_constraint( - &mut self, - predicate: Predicate<'db>, - ) -> ScopedVisibilityConstraintId { - let predicate_id = self.current_use_def_map_mut().add_predicate(predicate); - let id = self - .current_visibility_constraints_mut() - .add_atom(predicate_id); - self.record_visibility_constraint_id(id); - id - } - - /// Records that all remaining statements in the current block are unreachable, and therefore - /// not visible. - fn mark_unreachable(&mut self) { - self.current_use_def_map_mut().mark_unreachable(); - } - - /// Records a visibility constraint that always evaluates to "ambiguous". - fn record_ambiguous_visibility(&mut self) { - self.current_use_def_map_mut() - .record_visibility_constraint(ScopedVisibilityConstraintId::AMBIGUOUS); - } - - /// Simplifies (resets) visibility constraints on all live bindings and declarations that did - /// not see any new definitions since the given snapshot. - fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) { - self.current_use_def_map_mut() - .simplify_visibility_constraints(snapshot); - } - - /// Record a constraint that affects the reachability of the current position in the semantic - /// index analysis. For example, if we encounter a `if test:` branch, we immediately record - /// a `test` constraint, because if `test` later (during type checking) evaluates to `False`, - /// we know that all statements that follow in this path of control flow will be unreachable. - fn record_reachability_constraint( - &mut self, - predicate: Predicate<'db>, - ) -> ScopedVisibilityConstraintId { - let predicate_id = self.add_predicate(predicate); - self.record_reachability_constraint_id(predicate_id) - } - - /// Similar to [`Self::record_reachability_constraint`], but takes a [`ScopedPredicateId`]. - fn record_reachability_constraint_id( - &mut self, - predicate_id: ScopedPredicateId, - ) -> ScopedVisibilityConstraintId { - let visibility_constraint = self - .current_visibility_constraints_mut() - .add_atom(predicate_id); - self.current_use_def_map_mut() - .record_reachability_constraint(visibility_constraint) - } - - /// Record the negation of a given reachability/visibility constraint. - fn record_negated_reachability_constraint( - &mut self, - reachability_constraint: ScopedVisibilityConstraintId, - ) { - let negated_constraint = self - .current_visibility_constraints_mut() - .add_not_constraint(reachability_constraint); - self.current_use_def_map_mut() - .record_reachability_constraint(negated_constraint); - } - - fn push_assignment(&mut self, assignment: CurrentAssignment<'db>) { - self.current_assignments.push(assignment); - } - - fn pop_assignment(&mut self) { - let popped_assignment = self.current_assignments.pop(); - debug_assert!(popped_assignment.is_some()); - } - - fn current_assignment(&self) -> Option> { - self.current_assignments.last().copied() - } - - fn current_assignment_mut(&mut self) -> Option<&mut CurrentAssignment<'db>> { - self.current_assignments.last_mut() - } - - /// Records the fact that we saw an attribute assignment of the form - /// `object.attr: ( = …)` or `object.attr = `. - fn register_attribute_assignment( - &mut self, - object: &ast::Expr, - attr: &'db ast::Identifier, - definition_kind: DefinitionKind<'db>, - ) { - if self.is_method_of_class().is_some() { - // We only care about attribute assignments to the first parameter of a method, - // i.e. typically `self` or `cls`. - let accessed_object_refers_to_first_parameter = - object.as_name_expr().map(|name| name.id.as_str()) - == self.current_first_parameter_name; - - if accessed_object_refers_to_first_parameter { - let symbol = self.add_attribute(attr.id().clone()); - self.add_attribute_definition(symbol, definition_kind); - } - } - } - - fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> { - match pattern { - ast::Pattern::MatchValue(pattern) => { - let value = self.add_standalone_expression(&pattern.value); - PatternPredicateKind::Value(value) - } - ast::Pattern::MatchSingleton(singleton) => { - PatternPredicateKind::Singleton(singleton.value) - } - ast::Pattern::MatchClass(pattern) => { - let cls = self.add_standalone_expression(&pattern.cls); - PatternPredicateKind::Class(cls) - } - ast::Pattern::MatchOr(pattern) => { - let predicates = pattern - .patterns - .iter() - .map(|pattern| self.predicate_kind(pattern)) - .collect(); - PatternPredicateKind::Or(predicates) - } - _ => PatternPredicateKind::Unsupported, - } - } - - fn add_pattern_narrowing_constraint( - &mut self, - subject: Expression<'db>, - pattern: &ast::Pattern, - guard: Option<&ast::Expr>, - ) -> Predicate<'db> { - // This is called for the top-level pattern of each match arm. We need to create a - // standalone expression for each arm of a match statement, since they can introduce - // constraints on the match subject. (Or more accurately, for the match arm's pattern, - // since its the pattern that introduces any constraints, not the body.) Ideally, that - // standalone expression would wrap the match arm's pattern as a whole. But a standalone - // expression can currently only wrap an ast::Expr, which patterns are not. So, we need to - // choose an Expr that can “stand in” for the pattern, which we can wrap in a standalone - // expression. - // - // See the comment in TypeInferenceBuilder::infer_match_pattern for more details. - - let kind = self.predicate_kind(pattern); - let guard = guard.map(|guard| self.add_standalone_expression(guard)); - - let pattern_predicate = PatternPredicate::new( - self.db, - self.file, - self.current_scope(), - subject, - kind, - guard, - countme::Count::default(), - ); - let predicate = Predicate { - node: PredicateNode::Pattern(pattern_predicate), - is_positive: true, - }; - self.record_narrowing_constraint(predicate); - predicate - } - - /// Record an expression that needs to be a Salsa ingredient, because we need to infer its type - /// standalone (type narrowing tests, RHS of an assignment.) - fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> { - self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal) - } - - /// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a - /// *type* expression, which makes sure that it will later be inferred as such. - fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> { - self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression) - } - - fn add_standalone_expression_impl( - &mut self, - expression_node: &ast::Expr, - expression_kind: ExpressionKind, - ) -> Expression<'db> { - let expression = Expression::new( - self.db, - self.file, - self.current_scope(), - #[allow(unsafe_code)] - unsafe { - AstNodeRef::new(self.module.clone(), expression_node) - }, - expression_kind, - countme::Count::default(), - ); - self.expressions_by_node - .insert(expression_node.into(), expression); - expression - } - - fn with_type_params( - &mut self, - with_scope: NodeWithScopeRef, - type_params: Option<&'db ast::TypeParams>, - nested: impl FnOnce(&mut Self) -> FileScopeId, - ) -> FileScopeId { - if let Some(type_params) = type_params { - self.push_scope(with_scope); - - for type_param in &type_params.type_params { - let (name, bound, default) = match type_param { - ast::TypeParam::TypeVar(ast::TypeParamTypeVar { - range: _, - name, - bound, - default, - }) => (name, bound, default), - ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { - name, default, .. - }) => (name, &None, default), - ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { - name, - default, - .. - }) => (name, &None, default), - }; - let (symbol, _) = self.add_symbol(name.id.clone()); - // TODO create Definition for PEP 695 typevars - // note that the "bound" on the typevar is a totally different thing than whether - // or not a name is "bound" by a typevar declaration; the latter is always true. - self.mark_symbol_bound(symbol); - self.mark_symbol_declared(symbol); - if let Some(bounds) = bound { - self.visit_expr(bounds); - } - if let Some(default) = default { - self.visit_expr(default); - } - match type_param { - ast::TypeParam::TypeVar(node) => self.add_definition(symbol, node), - ast::TypeParam::ParamSpec(node) => self.add_definition(symbol, node), - ast::TypeParam::TypeVarTuple(node) => self.add_definition(symbol, node), - }; - } - } - - let nested_scope = nested(self); - - if type_params.is_some() { - self.pop_scope(); - } - - nested_scope - } - - /// This method does several things: - /// - It pushes a new scope onto the stack for visiting - /// a list/dict/set comprehension or generator expression - /// - Inside that scope, it visits a list of [`Comprehension`] nodes, - /// assumed to be the "generators" that compose a comprehension - /// (that is, the `for x in y` and `for y in z` parts of `x for x in y for y in z`). - /// - Inside that scope, it also calls a closure for visiting the outer `elt` - /// of a list/dict/set comprehension or generator expression - /// - It then pops the new scope off the stack - /// - /// [`Comprehension`]: ast::Comprehension - fn with_generators_scope( - &mut self, - scope: NodeWithScopeRef, - generators: &'db [ast::Comprehension], - visit_outer_elt: impl FnOnce(&mut Self), - ) { - let mut generators_iter = generators.iter(); - - let Some(generator) = generators_iter.next() else { - unreachable!("Expression must contain at least one generator"); - }; - - // The `iter` of the first generator is evaluated in the outer scope, while all subsequent - // nodes are evaluated in the inner scope. - self.add_standalone_expression(&generator.iter); - self.visit_expr(&generator.iter); - self.push_scope(scope); - - self.push_assignment(CurrentAssignment::Comprehension { - node: generator, - first: true, - }); - self.visit_expr(&generator.target); - self.pop_assignment(); - - for expr in &generator.ifs { - self.visit_expr(expr); - } - - for generator in generators_iter { - self.add_standalone_expression(&generator.iter); - self.visit_expr(&generator.iter); - - self.push_assignment(CurrentAssignment::Comprehension { - node: generator, - first: false, - }); - self.visit_expr(&generator.target); - self.pop_assignment(); - - for expr in &generator.ifs { - self.visit_expr(expr); - } - } - - visit_outer_elt(self); - self.pop_scope(); - } - - fn declare_parameters(&mut self, parameters: &'db ast::Parameters) { - for parameter in parameters.iter_non_variadic_params() { - self.declare_parameter(parameter); - } - if let Some(vararg) = parameters.vararg.as_ref() { - let (symbol, _) = self.add_symbol(vararg.name.id().clone()); - self.add_definition( - symbol, - DefinitionNodeRef::VariadicPositionalParameter(vararg), - ); - } - if let Some(kwarg) = parameters.kwarg.as_ref() { - let (symbol, _) = self.add_symbol(kwarg.name.id().clone()); - self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg)); - } - } - - fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) { - let (symbol, _) = self.add_symbol(parameter.name().id().clone()); - - let definition = self.add_definition(symbol, parameter); - - // 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(), - Definitions::single(definition), - ); - debug_assert_eq!(existing_definition, None); - } - - /// Add an unpackable assignment for the given [`Unpackable`]. - /// - /// This method handles assignments that can contain unpacking like assignment statements, - /// for statements, etc. - fn add_unpackable_assignment( - &mut self, - unpackable: &Unpackable<'db>, - target: &'db ast::Expr, - value: Expression<'db>, - ) { - // We only handle assignments to names and unpackings here, other targets like - // attribute and subscript are handled separately as they don't create a new - // definition. - - let current_assignment = match target { - ast::Expr::List(_) | ast::Expr::Tuple(_) => { - let unpack = Some(Unpack::new( - self.db, - self.file, - self.current_scope(), - // SAFETY: `target` belongs to the `self.module` tree - #[allow(unsafe_code)] - unsafe { - AstNodeRef::new(self.module.clone(), target) - }, - UnpackValue::new(unpackable.kind(), value), - countme::Count::default(), - )); - Some(unpackable.as_current_assignment(unpack)) - } - ast::Expr::Name(_) | ast::Expr::Attribute(_) => { - Some(unpackable.as_current_assignment(None)) - } - _ => None, - }; - - if let Some(current_assignment) = current_assignment { - self.push_assignment(current_assignment); - } - - self.visit_expr(target); - - if current_assignment.is_some() { - // Only need to pop in the case where we pushed something - self.pop_assignment(); - } - } - - pub(super) fn build(mut self) -> SemanticIndex<'db> { - let module = self.module; - self.visit_body(module.suite()); - - // Pop the root scope - self.pop_scope(); - assert!(self.scope_stack.is_empty()); - - assert_eq!(&self.current_assignments, &[]); - - let mut symbol_tables: IndexVec<_, _> = self - .symbol_tables - .into_iter() - .map(|builder| Arc::new(builder.finish())) - .collect(); - - let mut instance_attribute_tables: IndexVec<_, _> = self - .instance_attribute_tables - .into_iter() - .map(SymbolTableBuilder::finish) - .collect(); - - let mut use_def_maps: IndexVec<_, _> = self - .use_def_maps - .into_iter() - .map(|builder| Arc::new(builder.finish())) - .collect(); - - let mut ast_ids: IndexVec<_, _> = self - .ast_ids - .into_iter() - .map(super::ast_ids::AstIdsBuilder::finish) - .collect(); - - self.scopes.shrink_to_fit(); - symbol_tables.shrink_to_fit(); - instance_attribute_tables.shrink_to_fit(); - use_def_maps.shrink_to_fit(); - ast_ids.shrink_to_fit(); - self.scopes_by_expression.shrink_to_fit(); - self.definitions_by_node.shrink_to_fit(); - - self.scope_ids_by_scope.shrink_to_fit(); - self.scopes_by_node.shrink_to_fit(); - self.eager_bindings.shrink_to_fit(); - - SemanticIndex { - symbol_tables, - instance_attribute_tables, - scopes: self.scopes, - definitions_by_node: self.definitions_by_node, - expressions_by_node: self.expressions_by_node, - scope_ids_by_scope: self.scope_ids_by_scope, - ast_ids, - scopes_by_expression: self.scopes_by_expression, - scopes_by_node: self.scopes_by_node, - use_def_maps, - imported_modules: Arc::new(self.imported_modules), - has_future_annotations: self.has_future_annotations, - eager_bindings: self.eager_bindings, - } - } -} - -impl<'db, 'ast> Visitor<'ast> for SemanticIndexBuilder<'db> -where - 'ast: 'db, -{ - fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) { - match stmt { - ast::Stmt::FunctionDef(function_def) => { - let ast::StmtFunctionDef { - decorator_list, - parameters, - type_params, - name, - returns, - body, - is_async: _, - range: _, - } = function_def; - for decorator in decorator_list { - self.visit_decorator(decorator); - } - - self.with_type_params( - NodeWithScopeRef::FunctionTypeParameters(function_def), - type_params.as_deref(), - |builder| { - builder.visit_parameters(parameters); - if let Some(returns) = returns { - builder.visit_annotation(returns); - } - - builder.push_scope(NodeWithScopeRef::Function(function_def)); - - builder.declare_parameters(parameters); - - let mut first_parameter_name = parameters - .iter_non_variadic_params() - .next() - .map(|first_param| first_param.parameter.name.id().as_str()); - std::mem::swap( - &mut builder.current_first_parameter_name, - &mut first_parameter_name, - ); - - // TODO: Fix how we determine the public types of symbols in a - // function-like scope: https://github.com/astral-sh/ruff/issues/15777 - // - // In the meantime, visit the function body, but treat the last statement - // specially if it is a return. If it is, this would cause all definitions - // in the function to be marked as non-visible with our current treatment - // of terminal statements. Since we currently model the externally visible - // definitions in a function scope as the set of bindings that are visible - // at the end of the body, we then consider this function to have no - // externally visible definitions. To get around this, we take a flow - // snapshot just before processing the return statement, and use _that_ as - // the "end-of-body" state that we resolve external references against. - if let Some((last_stmt, first_stmts)) = body.split_last() { - builder.visit_body(first_stmts); - let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_)) - .then(|| builder.flow_snapshot()); - builder.visit_stmt(last_stmt); - let scope_start_visibility = - builder.current_use_def_map().scope_start_visibility; - if let Some(pre_return_state) = pre_return_state { - builder.flow_restore(pre_return_state); - builder.current_use_def_map_mut().scope_start_visibility = - scope_start_visibility; - } - } - - builder.current_first_parameter_name = first_parameter_name; - builder.pop_scope() - }, - ); - // The default value of the parameters needs to be evaluated in the - // enclosing scope. - for default in parameters - .iter_non_variadic_params() - .filter_map(|param| param.default.as_deref()) - { - self.visit_expr(default); - } - // The symbol for the function name itself has to be evaluated - // at the end to match the runtime evaluation of parameter defaults - // and return-type annotations. - let (symbol, _) = self.add_symbol(name.id.clone()); - self.add_definition(symbol, function_def); - } - ast::Stmt::ClassDef(class) => { - for decorator in &class.decorator_list { - self.visit_decorator(decorator); - } - - self.with_type_params( - NodeWithScopeRef::ClassTypeParameters(class), - class.type_params.as_deref(), - |builder| { - if let Some(arguments) = &class.arguments { - builder.visit_arguments(arguments); - } - - builder.push_scope(NodeWithScopeRef::Class(class)); - builder.visit_body(&class.body); - - 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( - type_alias - .name - .as_name_expr() - .map(|name| name.id.clone()) - .unwrap_or("".into()), - ); - self.add_definition(symbol, type_alias); - self.visit_expr(&type_alias.name); - - self.with_type_params( - NodeWithScopeRef::TypeAliasTypeParameters(type_alias), - type_alias.type_params.as_deref(), - |builder| { - builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias)); - builder.visit_expr(&type_alias.value); - builder.pop_scope() - }, - ); - } - ast::Stmt::Import(node) => { - self.current_use_def_map_mut() - .record_node_reachability(NodeKey::from_node(node)); - - for (alias_index, alias) in node.names.iter().enumerate() { - // Mark the imported module, and all of its parents, as being imported in this - // file. - if let Some(module_name) = ModuleName::new(&alias.name) { - self.imported_modules.extend(module_name.ancestors()); - } - - let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname { - (asname.id.clone(), asname.id == alias.name.id) - } else { - (Name::new(alias.name.id.split('.').next().unwrap()), false) - }; - - let (symbol, _) = self.add_symbol(symbol_name); - self.add_definition( - symbol, - ImportDefinitionNodeRef { - node, - alias_index, - is_reexported, - }, - ); - } - } - ast::Stmt::ImportFrom(node) => { - self.current_use_def_map_mut() - .record_node_reachability(NodeKey::from_node(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` 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; - }; - - let referenced_module = module.file(); - - // In order to understand the visibility of definitions created by a `*` import, - // we need to know the visibility of the global-scope definitions in the - // `referenced_module` the symbols imported from. Much like predicates for `if` - // statements can only have their visibility constraints resolved at type-inference - // time, the visibility of these global-scope definitions in the external module - // cannot be resolved at this point. As such, we essentially model each definition - // stemming from a `from exporter *` import as something like: - // - // ```py - // if : - // from exporter import name - // ``` - // - // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. - for export in exported_names(self.db, referenced_module) { - let (symbol_id, newly_added) = self.add_symbol(export.clone()); - let node_ref = StarImportDefinitionNodeRef { node, symbol_id }; - let star_import = StarImportPlaceholderPredicate::new( - self.db, - self.file, - symbol_id, - referenced_module, - ); - - // Fast path for if there were no previous definitions - // of the symbol defined through the `*` import: - // we can apply the visibility constraint to *only* the added definition, - // rather than all definitions - if newly_added { - self.push_additional_definition(symbol_id, node_ref); - self.current_use_def_map_mut() - .record_and_negate_star_import_visibility_constraint( - star_import, - symbol_id, - ); - } else { - let pre_definition = self.flow_snapshot(); - self.push_additional_definition(symbol_id, node_ref); - let constraint_id = - self.record_visibility_constraint(star_import.into()); - let post_definition = self.flow_snapshot(); - self.flow_restore(pre_definition.clone()); - self.record_negated_visibility_constraint(constraint_id); - self.flow_merge(post_definition); - self.simplify_visibility_constraints(pre_definition); - } - } - - continue; - } - - let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname { - (&asname.id, asname.id == alias.name.id) - } else { - (&alias.name.id, false) - }; - - // Look for imports `from __future__ import annotations`, ignore `as ...` - // We intentionally don't enforce the rules about location of `__future__` - // imports here, we assume the user's intent was to apply the `__future__` - // import, so we still check using it (and will also emit a diagnostic about a - // miss-placed `__future__` import.) - self.has_future_annotations |= alias.name.id == "annotations" - && node.module.as_deref() == Some("__future__"); - - let (symbol, _) = self.add_symbol(symbol_name.clone()); - - self.add_definition( - symbol, - ImportFromDefinitionNodeRef { - node, - alias_index, - is_reexported, - }, - ); - } - } - ast::Stmt::Assign(node) => { - debug_assert_eq!(&self.current_assignments, &[]); - - self.visit_expr(&node.value); - let value = self.add_standalone_expression(&node.value); - - for target in &node.targets { - self.add_unpackable_assignment(&Unpackable::Assign(node), target, value); - } - } - ast::Stmt::AnnAssign(node) => { - debug_assert_eq!(&self.current_assignments, &[]); - self.visit_expr(&node.annotation); - if let Some(value) = &node.value { - self.visit_expr(value); - } - - // See https://docs.python.org/3/library/ast.html#ast.AnnAssign - if matches!( - *node.target, - ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_) - ) { - self.push_assignment(node.into()); - self.visit_expr(&node.target); - self.pop_assignment(); - } else { - self.visit_expr(&node.target); - } - } - ast::Stmt::AugAssign( - aug_assign @ ast::StmtAugAssign { - range: _, - target, - op: _, - value, - }, - ) => { - debug_assert_eq!(&self.current_assignments, &[]); - self.visit_expr(value); - - // See https://docs.python.org/3/library/ast.html#ast.AugAssign - if matches!( - **target, - ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_) - ) { - self.push_assignment(aug_assign.into()); - self.visit_expr(target); - self.pop_assignment(); - } else { - self.visit_expr(target); - } - } - ast::Stmt::If(node) => { - self.visit_expr(&node.test); - let mut no_branch_taken = self.flow_snapshot(); - let mut last_predicate = self.record_expression_narrowing_constraint(&node.test); - let mut reachability_constraint = - self.record_reachability_constraint(last_predicate); - self.visit_body(&node.body); - - let visibility_constraint_id = self.record_visibility_constraint(last_predicate); - let mut vis_constraints = vec![visibility_constraint_id]; - - let mut post_clauses: Vec = vec![]; - let elif_else_clauses = node - .elif_else_clauses - .iter() - .map(|clause| (clause.test.as_ref(), clause.body.as_slice())); - let has_else = node - .elif_else_clauses - .last() - .is_some_and(|clause| clause.test.is_none()); - let elif_else_clauses = elif_else_clauses.chain(if has_else { - // if there's an `else` clause already, we don't need to add another - None - } else { - // if there's no `else` branch, we should add a no-op `else` branch - Some((None, Default::default())) - }); - for (clause_test, clause_body) in elif_else_clauses { - // snapshot after every block except the last; the last one will just become - // the state that we merge the other snapshots into - post_clauses.push(self.flow_snapshot()); - // we can only take an elif/else branch if none of the previous ones were - // taken - self.flow_restore(no_branch_taken.clone()); - self.record_negated_narrowing_constraint(last_predicate); - self.record_negated_reachability_constraint(reachability_constraint); - - let elif_predicate = if let Some(elif_test) = clause_test { - self.visit_expr(elif_test); - // A test expression is evaluated whether the branch is taken or not - no_branch_taken = self.flow_snapshot(); - reachability_constraint = - self.record_reachability_constraint(last_predicate); - let predicate = self.record_expression_narrowing_constraint(elif_test); - Some(predicate) - } else { - None - }; - - self.visit_body(clause_body); - - for id in &vis_constraints { - self.record_negated_visibility_constraint(*id); - } - if let Some(elif_predicate) = elif_predicate { - last_predicate = elif_predicate; - let id = self.record_visibility_constraint(elif_predicate); - vis_constraints.push(id); - } - } - - for post_clause_state in post_clauses { - self.flow_merge(post_clause_state); - } - - self.simplify_visibility_constraints(no_branch_taken); - } - ast::Stmt::While(ast::StmtWhile { - test, - body, - orelse, - range: _, - }) => { - self.visit_expr(test); - - let pre_loop = self.flow_snapshot(); - let predicate = self.record_expression_narrowing_constraint(test); - self.record_reachability_constraint(predicate); - - // We need multiple copies of the visibility constraint for the while condition, - // since we need to model situations where the first evaluation of the condition - // returns True, but a later evaluation returns False. - let first_predicate_id = self.current_use_def_map_mut().add_predicate(predicate); - let later_predicate_id = self.current_use_def_map_mut().add_predicate(predicate); - let first_vis_constraint_id = self - .current_visibility_constraints_mut() - .add_atom(first_predicate_id); - let later_vis_constraint_id = self - .current_visibility_constraints_mut() - .add_atom(later_predicate_id); - - let outer_loop = self.push_loop(); - self.visit_body(body); - let this_loop = self.pop_loop(outer_loop); - - // If the body is executed, we know that we've evaluated the condition at least - // once, and that the first evaluation was True. We might not have evaluated the - // condition more than once, so we can't assume that later evaluations were True. - // So the body's full visibility constraint is `first`. - let body_vis_constraint_id = first_vis_constraint_id; - self.record_visibility_constraint_id(body_vis_constraint_id); - - // We execute the `else` once the condition evaluates to false. This could happen - // without ever executing the body, if the condition is false the first time it's - // tested. So the starting flow state of the `else` clause is the union of: - // - the pre-loop state with a visibility constraint that the first evaluation of - // the while condition was false, - // - the post-body state (which already has a visibility constraint that the - // first evaluation was true) with a visibility constraint that a _later_ - // evaluation of the while condition was false. - // To model this correctly, we need two copies of the while condition constraint, - // since the first and later evaluations might produce different results. - let post_body = self.flow_snapshot(); - self.flow_restore(pre_loop.clone()); - self.record_negated_visibility_constraint(first_vis_constraint_id); - self.flow_merge(post_body); - self.record_negated_narrowing_constraint(predicate); - self.visit_body(orelse); - self.record_negated_visibility_constraint(later_vis_constraint_id); - - // Breaking out of a while loop bypasses the `else` clause, so merge in the break - // states after visiting `else`. - for break_state in this_loop.break_states { - let snapshot = self.flow_snapshot(); - self.flow_restore(break_state); - self.record_visibility_constraint_id(body_vis_constraint_id); - self.flow_merge(snapshot); - } - - self.simplify_visibility_constraints(pre_loop); - } - ast::Stmt::With(ast::StmtWith { - items, - body, - is_async, - .. - }) => { - for item @ ast::WithItem { - range: _, - context_expr, - optional_vars, - } in items - { - self.visit_expr(context_expr); - if let Some(optional_vars) = optional_vars.as_deref() { - let context_manager = self.add_standalone_expression(context_expr); - self.add_unpackable_assignment( - &Unpackable::WithItem { - item, - is_async: *is_async, - }, - optional_vars, - context_manager, - ); - } - } - self.visit_body(body); - } - - ast::Stmt::For( - for_stmt @ ast::StmtFor { - range: _, - is_async: _, - target, - iter, - body, - orelse, - }, - ) => { - debug_assert_eq!(&self.current_assignments, &[]); - - let iter_expr = self.add_standalone_expression(iter); - self.visit_expr(iter); - - self.record_ambiguous_visibility(); - - let pre_loop = self.flow_snapshot(); - - self.add_unpackable_assignment(&Unpackable::For(for_stmt), target, iter_expr); - - let outer_loop = self.push_loop(); - self.visit_body(body); - let this_loop = self.pop_loop(outer_loop); - - // We may execute the `else` clause without ever executing the body, so merge in - // the pre-loop state before visiting `else`. - self.flow_merge(pre_loop); - self.visit_body(orelse); - - // Breaking out of a `for` loop bypasses the `else` clause, so merge in the break - // states after visiting `else`. - for break_state in this_loop.break_states { - self.flow_merge(break_state); - } - } - ast::Stmt::Match(ast::StmtMatch { - subject, - cases, - range: _, - }) => { - debug_assert_eq!(self.current_match_case, None); - - let subject_expr = self.add_standalone_expression(subject); - self.visit_expr(subject); - if cases.is_empty() { - return; - } - - let after_subject = self.flow_snapshot(); - let mut vis_constraints = vec![]; - let mut post_case_snapshots = vec![]; - for (i, case) in cases.iter().enumerate() { - if i != 0 { - post_case_snapshots.push(self.flow_snapshot()); - self.flow_restore(after_subject.clone()); - } - - self.current_match_case = Some(CurrentMatchCase::new(&case.pattern)); - self.visit_pattern(&case.pattern); - self.current_match_case = None; - let predicate = self.add_pattern_narrowing_constraint( - subject_expr, - &case.pattern, - case.guard.as_deref(), - ); - self.record_reachability_constraint(predicate); - if let Some(expr) = &case.guard { - self.visit_expr(expr); - } - self.visit_body(&case.body); - for id in &vis_constraints { - self.record_negated_visibility_constraint(*id); - } - let vis_constraint_id = self.record_visibility_constraint(predicate); - vis_constraints.push(vis_constraint_id); - } - - // If there is no final wildcard match case, pretend there is one. This is similar to how - // we add an implicit `else` block in if-elif chains, in case it's not present. - if !cases - .last() - .is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard()) - { - post_case_snapshots.push(self.flow_snapshot()); - self.flow_restore(after_subject.clone()); - - for id in &vis_constraints { - self.record_negated_visibility_constraint(*id); - } - } - - for post_clause_state in post_case_snapshots { - self.flow_merge(post_clause_state); - } - - self.simplify_visibility_constraints(after_subject); - } - ast::Stmt::Try(ast::StmtTry { - body, - handlers, - orelse, - finalbody, - is_star, - range: _, - }) => { - self.record_ambiguous_visibility(); - - // Save the state prior to visiting any of the `try` block. - // - // Potentially none of the `try` block could have been executed prior to executing - // the `except` block(s) and/or the `finally` block. - // We will merge this state with all of the intermediate - // states during the `try` block before visiting those suites. - let pre_try_block_state = self.flow_snapshot(); - - self.try_node_context_stack_manager.push_context(); - - // Visit the `try` block! - self.visit_body(body); - - let mut post_except_states = vec![]; - - // Take a record also of all the intermediate states we encountered - // while visiting the `try` block - let try_block_snapshots = self.try_node_context_stack_manager.pop_context(); - - if !handlers.is_empty() { - // Save the state immediately *after* visiting the `try` block - // but *before* we prepare for visiting the `except` block(s). - // - // We will revert to this state prior to visiting the the `else` block, - // as there necessarily must have been 0 `except` blocks executed - // if we hit the `else` block. - let post_try_block_state = self.flow_snapshot(); - - // Prepare for visiting the `except` block(s) - self.flow_restore(pre_try_block_state); - for state in try_block_snapshots { - self.flow_merge(state); - } - - let pre_except_state = self.flow_snapshot(); - let num_handlers = handlers.len(); - - for (i, except_handler) in handlers.iter().enumerate() { - let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler; - let ast::ExceptHandlerExceptHandler { - name: symbol_name, - type_: handled_exceptions, - body: handler_body, - range: _, - } = except_handler; - - if let Some(handled_exceptions) = handled_exceptions { - self.visit_expr(handled_exceptions); - } - - // If `handled_exceptions` above was `None`, it's something like `except as e:`, - // which is invalid syntax. However, it's still pretty obvious here that the user - // *wanted* `e` to be bound, so we should still create a definition here nonetheless. - if let Some(symbol_name) = symbol_name { - let (symbol, _) = self.add_symbol(symbol_name.id.clone()); - - self.add_definition( - symbol, - DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef { - handler: except_handler, - is_star: *is_star, - }), - ); - } - - self.visit_body(handler_body); - // Each `except` block is mutually exclusive with all other `except` blocks. - post_except_states.push(self.flow_snapshot()); - - // It's unnecessary to do the `self.flow_restore()` call for the final except handler, - // as we'll immediately call `self.flow_restore()` to a different state - // as soon as this loop over the handlers terminates. - if i < (num_handlers - 1) { - self.flow_restore(pre_except_state.clone()); - } - } - - // If we get to the `else` block, we know that 0 of the `except` blocks can have been executed, - // and the entire `try` block must have been executed: - self.flow_restore(post_try_block_state); - } - - self.visit_body(orelse); - - for post_except_state in post_except_states { - self.flow_merge(post_except_state); - } - - // TODO: there's lots of complexity here that isn't yet handled by our model. - // In order to accurately model the semantics of `finally` suites, we in fact need to visit - // the suite twice: once under the (current) assumption that either the `try + else` suite - // ran to completion or exactly one `except` branch ran to completion, and then again under - // the assumption that potentially none of the branches ran to completion and we in fact - // jumped from a `try`, `else` or `except` branch straight into the `finally` branch. - // This requires rethinking some fundamental assumptions semantic indexing makes. - // For more details, see: - // - https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d - // - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702 - self.visit_body(finalbody); - } - - ast::Stmt::Raise(_) | ast::Stmt::Return(_) | ast::Stmt::Continue(_) => { - walk_stmt(self, stmt); - // Everything in the current block after a terminal statement is unreachable. - self.mark_unreachable(); - } - - ast::Stmt::Break(_) => { - let snapshot = self.flow_snapshot(); - if let Some(current_loop) = self.current_loop_mut() { - current_loop.push_break(snapshot); - } - // Everything in the current block after a terminal statement is unreachable. - self.mark_unreachable(); - } - - _ => { - walk_stmt(self, stmt); - } - } - } - - fn visit_expr(&mut self, expr: &'ast ast::Expr) { - self.scopes_by_expression - .insert(expr.into(), self.current_scope()); - self.current_ast_ids().record_expression(expr); - - let node_key = NodeKey::from_node(expr); - - match expr { - ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => { - let (is_use, is_definition) = match (ctx, self.current_assignment()) { - (ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => { - // For augmented assignment, the target expression is also used. - (true, true) - } - (ast::ExprContext::Load, _) => (true, false), - (ast::ExprContext::Store, _) => (false, true), - (ast::ExprContext::Del, _) => (false, true), - (ast::ExprContext::Invalid, _) => (false, false), - }; - let (symbol, _) = self.add_symbol(id.clone()); - - if is_use { - self.mark_symbol_used(symbol); - let use_id = self.current_ast_ids().record_use(expr); - self.current_use_def_map_mut() - .record_use(symbol, use_id, node_key); - } - - if is_definition { - match self.current_assignment() { - Some(CurrentAssignment::Assign { node, unpack }) => { - self.add_definition( - symbol, - AssignmentDefinitionNodeRef { - unpack, - value: &node.value, - target: expr, - }, - ); - } - Some(CurrentAssignment::AnnAssign(ann_assign)) => { - self.add_definition( - symbol, - AnnotatedAssignmentDefinitionNodeRef { - node: ann_assign, - annotation: &ann_assign.annotation, - value: ann_assign.value.as_deref(), - target: expr, - }, - ); - } - Some(CurrentAssignment::AugAssign(aug_assign)) => { - self.add_definition(symbol, aug_assign); - } - Some(CurrentAssignment::For { node, unpack }) => { - self.add_definition( - symbol, - ForStmtDefinitionNodeRef { - unpack, - iterable: &node.iter, - target: expr, - is_async: node.is_async, - }, - ); - } - Some(CurrentAssignment::Named(named)) => { - // TODO(dhruvmanila): If the current scope is a comprehension, then the - // named expression is implicitly nonlocal. This is yet to be - // implemented. - self.add_definition(symbol, named); - } - Some(CurrentAssignment::Comprehension { node, first }) => { - self.add_definition( - symbol, - ComprehensionDefinitionNodeRef { - iterable: &node.iter, - target: name_node, - first, - is_async: node.is_async, - }, - ); - } - Some(CurrentAssignment::WithItem { - item, - is_async, - unpack, - }) => { - self.add_definition( - symbol, - WithItemDefinitionNodeRef { - unpack, - context_expr: &item.context_expr, - target: expr, - is_async, - }, - ); - } - None => {} - } - } - - if let Some(unpack_position) = self - .current_assignment_mut() - .and_then(CurrentAssignment::unpack_position_mut) - { - *unpack_position = UnpackPosition::Other; - } - - walk_expr(self, expr); - } - ast::Expr::Named(node) => { - // TODO walrus in comprehensions is implicitly nonlocal - self.visit_expr(&node.value); - - // See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements - if node.target.is_name_expr() { - self.push_assignment(node.into()); - self.visit_expr(&node.target); - self.pop_assignment(); - } else { - self.visit_expr(&node.target); - } - } - ast::Expr::Lambda(lambda) => { - if let Some(parameters) = &lambda.parameters { - // The default value of the parameters needs to be evaluated in the - // enclosing scope. - for default in parameters - .iter_non_variadic_params() - .filter_map(|param| param.default.as_deref()) - { - self.visit_expr(default); - } - self.visit_parameters(parameters); - } - self.push_scope(NodeWithScopeRef::Lambda(lambda)); - - // Add symbols and definitions for the parameters to the lambda scope. - if let Some(parameters) = lambda.parameters.as_ref() { - self.declare_parameters(parameters); - } - - self.visit_expr(lambda.body.as_ref()); - self.pop_scope(); - } - ast::Expr::If(ast::ExprIf { - body, test, orelse, .. - }) => { - self.visit_expr(test); - let pre_if = self.flow_snapshot(); - let predicate = self.record_expression_narrowing_constraint(test); - let reachability_constraint = self.record_reachability_constraint(predicate); - self.visit_expr(body); - let visibility_constraint = self.record_visibility_constraint(predicate); - let post_body = self.flow_snapshot(); - self.flow_restore(pre_if.clone()); - - self.record_negated_narrowing_constraint(predicate); - self.record_negated_reachability_constraint(reachability_constraint); - self.visit_expr(orelse); - self.record_negated_visibility_constraint(visibility_constraint); - self.flow_merge(post_body); - self.simplify_visibility_constraints(pre_if); - } - ast::Expr::ListComp( - list_comprehension @ ast::ExprListComp { - elt, generators, .. - }, - ) => { - self.with_generators_scope( - NodeWithScopeRef::ListComprehension(list_comprehension), - generators, - |builder| builder.visit_expr(elt), - ); - } - ast::Expr::SetComp( - set_comprehension @ ast::ExprSetComp { - elt, generators, .. - }, - ) => { - self.with_generators_scope( - NodeWithScopeRef::SetComprehension(set_comprehension), - generators, - |builder| builder.visit_expr(elt), - ); - } - ast::Expr::Generator( - generator @ ast::ExprGenerator { - elt, generators, .. - }, - ) => { - self.with_generators_scope( - NodeWithScopeRef::GeneratorExpression(generator), - generators, - |builder| builder.visit_expr(elt), - ); - } - ast::Expr::DictComp( - dict_comprehension @ ast::ExprDictComp { - key, - value, - generators, - .. - }, - ) => { - self.with_generators_scope( - NodeWithScopeRef::DictComprehension(dict_comprehension), - generators, - |builder| { - builder.visit_expr(key); - builder.visit_expr(value); - }, - ); - } - ast::Expr::BoolOp(ast::ExprBoolOp { - values, - range: _, - op, - }) => { - let pre_op = self.flow_snapshot(); - - let mut snapshots = vec![]; - let mut visibility_constraints = vec![]; - - for (index, value) in values.iter().enumerate() { - self.visit_expr(value); - - for vid in &visibility_constraints { - self.record_visibility_constraint_id(*vid); - } - - // For the last value, we don't need to model control flow. There is no short-circuiting - // anymore. - if index < values.len() - 1 { - let predicate = self.build_predicate(value); - let predicate_id = match op { - ast::BoolOp::And => self.add_predicate(predicate), - ast::BoolOp::Or => self.add_negated_predicate(predicate), - }; - let visibility_constraint = self - .current_visibility_constraints_mut() - .add_atom(predicate_id); - - let after_expr = self.flow_snapshot(); - - // We first model the short-circuiting behavior. We take the short-circuit - // path here if all of the previous short-circuit paths were not taken, so - // we record all previously existing visibility constraints, and negate the - // one for the current expression. - for vid in &visibility_constraints { - self.record_visibility_constraint_id(*vid); - } - self.record_negated_visibility_constraint(visibility_constraint); - snapshots.push(self.flow_snapshot()); - - // Then we model the non-short-circuiting behavior. Here, we need to delay - // the application of the visibility constraint until after the expression - // has been evaluated, so we only push it onto the stack here. - self.flow_restore(after_expr); - self.record_narrowing_constraint_id(predicate_id); - self.record_reachability_constraint_id(predicate_id); - visibility_constraints.push(visibility_constraint); - } - } - - for snapshot in snapshots { - self.flow_merge(snapshot); - } - - self.simplify_visibility_constraints(pre_op); - } - ast::Expr::Attribute(ast::ExprAttribute { - value: object, - attr, - ctx, - range: _, - }) => { - if ctx.is_store() { - match self.current_assignment() { - Some(CurrentAssignment::Assign { node, unpack, .. }) => { - // SAFETY: `value` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] - let assignment = AssignmentDefinitionKind::new( - TargetKind::from(unpack), - unsafe { AstNodeRef::new(self.module.clone(), &node.value) }, - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::Assignment(assignment), - ); - } - Some(CurrentAssignment::AnnAssign(ann_assign)) => { - self.add_standalone_type_expression(&ann_assign.annotation); - // SAFETY: `annotation`, `value` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] - let assignment = AnnotatedAssignmentDefinitionKind::new( - unsafe { - AstNodeRef::new(self.module.clone(), &ann_assign.annotation) - }, - ann_assign.value.as_deref().map(|value| unsafe { - AstNodeRef::new(self.module.clone(), value) - }), - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::AnnotatedAssignment(assignment), - ); - } - Some(CurrentAssignment::For { node, unpack, .. }) => { - // // SAFETY: `iter` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] - let assignment = ForStmtDefinitionKind::new( - TargetKind::from(unpack), - unsafe { AstNodeRef::new(self.module.clone(), &node.iter) }, - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - node.is_async, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::For(assignment), - ); - } - Some(CurrentAssignment::WithItem { - item, - unpack, - is_async, - .. - }) => { - // SAFETY: `context_expr` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] - let assignment = WithItemDefinitionKind::new( - TargetKind::from(unpack), - unsafe { AstNodeRef::new(self.module.clone(), &item.context_expr) }, - unsafe { AstNodeRef::new(self.module.clone(), expr) }, - is_async, - ); - self.register_attribute_assignment( - object, - attr, - DefinitionKind::WithItem(assignment), - ); - } - Some(CurrentAssignment::Comprehension { .. }) => { - // TODO: - } - Some(CurrentAssignment::AugAssign(_)) => { - // TODO: - } - Some(CurrentAssignment::Named(_)) => { - // TODO: - } - None => {} - } - } - - // Track reachability of attribute expressions to silence `unresolved-attribute` - // diagnostics in unreachable code. - self.current_use_def_map_mut() - .record_node_reachability(node_key); - - walk_expr(self, expr); - } - ast::Expr::StringLiteral(_) => { - // Track reachability of string literals, as they could be a stringified annotation - // with child expressions whose reachability we are interested in. - self.current_use_def_map_mut() - .record_node_reachability(node_key); - - walk_expr(self, expr); - } - _ => { - walk_expr(self, expr); - } - } - } - - fn visit_parameters(&mut self, parameters: &'ast ast::Parameters) { - // Intentionally avoid walking default expressions, as we handle them in the enclosing - // scope. - for parameter in parameters.iter().map(ast::AnyParameterRef::as_parameter) { - self.visit_parameter(parameter); - } - } - - fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) { - if let ast::Pattern::MatchStar(ast::PatternMatchStar { - name: Some(name), - range: _, - }) = pattern - { - let (symbol, _) = self.add_symbol(name.id().clone()); - let state = self.current_match_case.as_ref().unwrap(); - self.add_definition( - symbol, - MatchPatternDefinitionNodeRef { - pattern: state.pattern, - identifier: name, - index: state.index, - }, - ); - } - - walk_pattern(self, pattern); - - if let ast::Pattern::MatchAs(ast::PatternMatchAs { - name: Some(name), .. - }) - | ast::Pattern::MatchMapping(ast::PatternMatchMapping { - rest: Some(name), .. - }) = pattern - { - let (symbol, _) = self.add_symbol(name.id().clone()); - let state = self.current_match_case.as_ref().unwrap(); - self.add_definition( - symbol, - MatchPatternDefinitionNodeRef { - pattern: state.pattern, - identifier: name, - index: state.index, - }, - ); - } - - self.current_match_case.as_mut().unwrap().index += 1; - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -enum CurrentAssignment<'a> { - Assign { - node: &'a ast::StmtAssign, - unpack: Option<(UnpackPosition, Unpack<'a>)>, - }, - AnnAssign(&'a ast::StmtAnnAssign), - AugAssign(&'a ast::StmtAugAssign), - For { - node: &'a ast::StmtFor, - unpack: Option<(UnpackPosition, Unpack<'a>)>, - }, - Named(&'a ast::ExprNamed), - Comprehension { - node: &'a ast::Comprehension, - first: bool, - }, - WithItem { - item: &'a ast::WithItem, - is_async: bool, - unpack: Option<(UnpackPosition, Unpack<'a>)>, - }, -} - -impl CurrentAssignment<'_> { - fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> { - match self { - Self::Assign { unpack, .. } - | Self::For { unpack, .. } - | Self::WithItem { unpack, .. } => unpack.as_mut().map(|(position, _)| position), - Self::AnnAssign(_) - | Self::AugAssign(_) - | Self::Named(_) - | Self::Comprehension { .. } => None, - } - } -} - -impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> { - fn from(value: &'a ast::StmtAnnAssign) -> Self { - Self::AnnAssign(value) - } -} - -impl<'a> From<&'a ast::StmtAugAssign> for CurrentAssignment<'a> { - fn from(value: &'a ast::StmtAugAssign) -> Self { - Self::AugAssign(value) - } -} - -impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> { - fn from(value: &'a ast::ExprNamed) -> Self { - Self::Named(value) - } -} - -#[derive(Debug, PartialEq)] -struct CurrentMatchCase<'a> { - /// The pattern that's part of the current match case. - pattern: &'a ast::Pattern, - - /// The index of the sub-pattern that's being currently visited within the pattern. - /// - /// For example: - /// ```py - /// match subject: - /// case a as b: ... - /// case [a, b]: ... - /// case a | b: ... - /// ``` - /// - /// In all of the above cases, the index would be 0 for `a` and 1 for `b`. - index: u32, -} - -impl<'a> CurrentMatchCase<'a> { - fn new(pattern: &'a ast::Pattern) -> Self { - Self { pattern, index: 0 } - } -} - -enum Unpackable<'a> { - Assign(&'a ast::StmtAssign), - For(&'a ast::StmtFor), - WithItem { - item: &'a ast::WithItem, - is_async: bool, - }, -} - -impl<'a> Unpackable<'a> { - const fn kind(&self) -> UnpackKind { - match self { - Unpackable::Assign(_) => UnpackKind::Assign, - Unpackable::For(_) => UnpackKind::Iterable, - Unpackable::WithItem { .. } => UnpackKind::ContextManager, - } - } - - fn as_current_assignment(&self, unpack: Option>) -> CurrentAssignment<'a> { - let unpack = unpack.map(|unpack| (UnpackPosition::First, unpack)); - match self { - Unpackable::Assign(stmt) => CurrentAssignment::Assign { node: stmt, unpack }, - Unpackable::For(stmt) => CurrentAssignment::For { node: stmt, unpack }, - Unpackable::WithItem { item, is_async } => CurrentAssignment::WithItem { - item, - is_async: *is_async, - unpack, - }, - } - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs deleted file mode 100644 index 9334ac69814b8..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ /dev/null @@ -1,1085 +0,0 @@ -use std::ops::Deref; - -use ruff_db::files::{File, FileRange}; -use ruff_db::parsed::ParsedModule; -use ruff_python_ast as ast; -use ruff_text_size::{Ranged, TextRange}; - -use crate::ast_node_ref::AstNodeRef; -use crate::node_key::NodeKey; -use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId}; -use crate::unpack::{Unpack, UnpackPosition}; -use crate::Db; - -/// A definition of a symbol. -/// -/// ## ID stability -/// The `Definition`'s ID is stable when the only field that change is its `kind` (AST node). -/// -/// The `Definition` changes when the `file`, `scope`, or `symbol` change. This can be -/// because a new scope gets inserted before the `Definition` or a new symbol is inserted -/// before this `Definition`. However, the ID can be considered stable and it is okay to use -/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs. -#[salsa::tracked(debug)] -pub struct Definition<'db> { - /// The file in which the definition occurs. - pub(crate) file: File, - - /// The scope in which the definition occurs. - pub(crate) file_scope: FileScopeId, - - /// The symbol defined. - pub(crate) symbol: ScopedSymbolId, - - /// WARNING: Only access this field when doing type inference for the same - /// file as where `Definition` is defined to avoid cross-file query dependencies. - #[no_eq] - #[return_ref] - #[tracked] - pub(crate) kind: DefinitionKind<'db>, - - /// This is a dedicated field to avoid accessing `kind` to compute this value. - pub(crate) is_reexported: bool, - - count: countme::Count>, -} - -impl<'db> Definition<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) - } - - pub fn full_range(self, db: &'db dyn Db) -> FileRange { - FileRange::new(self.file(db), self.kind(db).full_range()) - } - - pub fn focus_range(self, db: &'db dyn Db) -> FileRange { - FileRange::new(self.file(db), self.kind(db).target_range()) - } -} - -/// 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), - TypeAlias(&'a ast::StmtTypeAlias), - NamedExpression(&'a ast::ExprNamed), - Assignment(AssignmentDefinitionNodeRef<'a>), - AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef<'a>), - AugmentedAssignment(&'a ast::StmtAugAssign), - Comprehension(ComprehensionDefinitionNodeRef<'a>), - VariadicPositionalParameter(&'a ast::Parameter), - VariadicKeywordParameter(&'a ast::Parameter), - Parameter(&'a ast::ParameterWithDefault), - WithItem(WithItemDefinitionNodeRef<'a>), - MatchPattern(MatchPatternDefinitionNodeRef<'a>), - ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>), - TypeVar(&'a ast::TypeParamTypeVar), - ParamSpec(&'a ast::TypeParamParamSpec), - TypeVarTuple(&'a ast::TypeParamTypeVarTuple), -} - -impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> { - fn from(node: &'a ast::StmtFunctionDef) -> Self { - Self::Function(node) - } -} - -impl<'a> From<&'a ast::StmtClassDef> for DefinitionNodeRef<'a> { - fn from(node: &'a ast::StmtClassDef) -> Self { - Self::Class(node) - } -} - -impl<'a> From<&'a ast::StmtTypeAlias> for DefinitionNodeRef<'a> { - fn from(node: &'a ast::StmtTypeAlias) -> Self { - Self::TypeAlias(node) - } -} - -impl<'a> From<&'a ast::ExprNamed> for DefinitionNodeRef<'a> { - fn from(node: &'a ast::ExprNamed) -> Self { - Self::NamedExpression(node) - } -} - -impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> { - fn from(node: &'a ast::StmtAugAssign) -> Self { - Self::AugmentedAssignment(node) - } -} - -impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> { - fn from(value: &'a ast::TypeParamTypeVar) -> Self { - Self::TypeVar(value) - } -} - -impl<'a> From<&'a ast::TypeParamParamSpec> for DefinitionNodeRef<'a> { - fn from(value: &'a ast::TypeParamParamSpec) -> Self { - Self::ParamSpec(value) - } -} - -impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> { - fn from(value: &'a ast::TypeParamTypeVarTuple) -> Self { - Self::TypeVarTuple(value) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node_ref: ImportDefinitionNodeRef<'a>) -> Self { - Self::Import(node_ref) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self { - Self::ImportFrom(node_ref) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(value: ForStmtDefinitionNodeRef<'a>) -> Self { - Self::For(value) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node_ref: AssignmentDefinitionNodeRef<'a>) -> Self { - Self::Assignment(node_ref) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node_ref: AnnotatedAssignmentDefinitionNodeRef<'a>) -> Self { - Self::AnnotatedAssignment(node_ref) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node_ref: WithItemDefinitionNodeRef<'a>) -> Self { - Self::WithItem(node_ref) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node: ComprehensionDefinitionNodeRef<'a>) -> Self { - Self::Comprehension(node) - } -} - -impl<'a> From<&'a ast::ParameterWithDefault> for DefinitionNodeRef<'a> { - fn from(node: &'a ast::ParameterWithDefault) -> Self { - Self::Parameter(node) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node: MatchPatternDefinitionNodeRef<'a>) -> Self { - Self::MatchPattern(node) - } -} - -impl<'a> From> for DefinitionNodeRef<'a> { - fn from(node: StarImportDefinitionNodeRef<'a>) -> Self { - Self::ImportStar(node) - } -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct ImportDefinitionNodeRef<'a> { - pub(crate) node: &'a ast::StmtImport, - pub(crate) alias_index: usize, - 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, - pub(crate) alias_index: usize, - pub(crate) is_reexported: bool, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct AssignmentDefinitionNodeRef<'a> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, - pub(crate) value: &'a ast::Expr, - pub(crate) target: &'a ast::Expr, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct AnnotatedAssignmentDefinitionNodeRef<'a> { - pub(crate) node: &'a ast::StmtAnnAssign, - pub(crate) annotation: &'a ast::Expr, - pub(crate) value: Option<&'a ast::Expr>, - pub(crate) target: &'a ast::Expr, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct WithItemDefinitionNodeRef<'a> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, - pub(crate) context_expr: &'a ast::Expr, - pub(crate) target: &'a ast::Expr, - pub(crate) is_async: bool, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct ForStmtDefinitionNodeRef<'a> { - pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, - pub(crate) iterable: &'a ast::Expr, - pub(crate) target: &'a ast::Expr, - pub(crate) is_async: bool, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> { - pub(crate) handler: &'a ast::ExceptHandlerExceptHandler, - pub(crate) is_star: bool, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct ComprehensionDefinitionNodeRef<'a> { - pub(crate) iterable: &'a ast::Expr, - pub(crate) target: &'a ast::ExprName, - pub(crate) first: bool, - pub(crate) is_async: bool, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct MatchPatternDefinitionNodeRef<'a> { - /// The outermost pattern node in which the identifier being defined occurs. - pub(crate) pattern: &'a ast::Pattern, - /// The identifier being defined. - pub(crate) identifier: &'a ast::Identifier, - /// The index of the identifier in the pattern when visiting the `pattern` node in evaluation - /// order. - pub(crate) index: u32, -} - -impl<'db> DefinitionNodeRef<'db> { - #[allow(unsafe_code)] - pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> { - match self { - DefinitionNodeRef::Import(ImportDefinitionNodeRef { - node, - alias_index, - is_reexported, - }) => DefinitionKind::Import(ImportDefinitionKind { - node: AstNodeRef::new(parsed, node), - alias_index, - is_reexported, - }), - - DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef { - node, - alias_index, - is_reexported, - }) => DefinitionKind::ImportFrom(ImportFromDefinitionKind { - node: AstNodeRef::new(parsed, node), - 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)) - } - DefinitionNodeRef::Class(class) => { - DefinitionKind::Class(AstNodeRef::new(parsed, class)) - } - DefinitionNodeRef::TypeAlias(type_alias) => { - DefinitionKind::TypeAlias(AstNodeRef::new(parsed, type_alias)) - } - DefinitionNodeRef::NamedExpression(named) => { - DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named)) - } - DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef { - unpack, - value, - target, - }) => DefinitionKind::Assignment(AssignmentDefinitionKind { - target_kind: TargetKind::from(unpack), - value: AstNodeRef::new(parsed.clone(), value), - target: AstNodeRef::new(parsed, target), - }), - DefinitionNodeRef::AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef { - node: _, - annotation, - value, - target, - }) => DefinitionKind::AnnotatedAssignment(AnnotatedAssignmentDefinitionKind { - target: AstNodeRef::new(parsed.clone(), target), - annotation: AstNodeRef::new(parsed.clone(), annotation), - value: value.map(|v| AstNodeRef::new(parsed, v)), - }), - DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => { - DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment)) - } - DefinitionNodeRef::For(ForStmtDefinitionNodeRef { - unpack, - iterable, - target, - is_async, - }) => DefinitionKind::For(ForStmtDefinitionKind { - target_kind: TargetKind::from(unpack), - iterable: AstNodeRef::new(parsed.clone(), iterable), - target: AstNodeRef::new(parsed, target), - is_async, - }), - DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef { - iterable, - target, - first, - is_async, - }) => DefinitionKind::Comprehension(ComprehensionDefinitionKind { - iterable: AstNodeRef::new(parsed.clone(), iterable), - target: AstNodeRef::new(parsed, target), - first, - is_async, - }), - DefinitionNodeRef::VariadicPositionalParameter(parameter) => { - DefinitionKind::VariadicPositionalParameter(AstNodeRef::new(parsed, parameter)) - } - DefinitionNodeRef::VariadicKeywordParameter(parameter) => { - DefinitionKind::VariadicKeywordParameter(AstNodeRef::new(parsed, parameter)) - } - DefinitionNodeRef::Parameter(parameter) => { - DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter)) - } - DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef { - unpack, - context_expr, - target, - is_async, - }) => DefinitionKind::WithItem(WithItemDefinitionKind { - target_kind: TargetKind::from(unpack), - context_expr: AstNodeRef::new(parsed.clone(), context_expr), - target: AstNodeRef::new(parsed, target), - is_async, - }), - DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef { - pattern, - identifier, - index, - }) => DefinitionKind::MatchPattern(MatchPatternDefinitionKind { - pattern: AstNodeRef::new(parsed.clone(), pattern), - identifier: AstNodeRef::new(parsed, identifier), - index, - }), - DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef { - handler, - is_star, - }) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind { - handler: AstNodeRef::new(parsed, handler), - is_star, - }), - DefinitionNodeRef::TypeVar(node) => { - DefinitionKind::TypeVar(AstNodeRef::new(parsed, node)) - } - DefinitionNodeRef::ParamSpec(node) => { - DefinitionKind::ParamSpec(AstNodeRef::new(parsed, node)) - } - DefinitionNodeRef::TypeVarTuple(node) => { - DefinitionKind::TypeVarTuple(AstNodeRef::new(parsed, node)) - } - } - } - - pub(super) fn key(self) -> DefinitionNodeKey { - match self { - Self::Import(ImportDefinitionNodeRef { - node, - alias_index, - is_reexported: _, - }) => (&node.names[alias_index]).into(), - Self::ImportFrom(ImportFromDefinitionNodeRef { - node, - 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(), - Self::NamedExpression(node) => node.into(), - Self::Assignment(AssignmentDefinitionNodeRef { - value: _, - unpack: _, - target, - }) => DefinitionNodeKey(NodeKey::from_node(target)), - Self::AnnotatedAssignment(ann_assign) => ann_assign.node.into(), - Self::AugmentedAssignment(node) => node.into(), - Self::For(ForStmtDefinitionNodeRef { - target, - iterable: _, - unpack: _, - is_async: _, - }) => DefinitionNodeKey(NodeKey::from_node(target)), - Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(), - Self::VariadicPositionalParameter(node) => node.into(), - Self::VariadicKeywordParameter(node) => node.into(), - Self::Parameter(node) => node.into(), - Self::WithItem(WithItemDefinitionNodeRef { - context_expr: _, - unpack: _, - is_async: _, - target, - }) => DefinitionNodeKey(NodeKey::from_node(target)), - Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => { - identifier.into() - } - Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(), - Self::TypeVar(node) => node.into(), - Self::ParamSpec(node) => node.into(), - Self::TypeVarTuple(node) => node.into(), - } - } -} - -#[derive(Clone, Copy, Debug)] -pub(crate) enum DefinitionCategory { - /// A Definition which binds a value to a name (e.g. `x = 1`). - Binding, - /// A Definition which declares the upper-bound of acceptable types for this name (`x: int`). - Declaration, - /// A Definition which both declares a type and binds a value (e.g. `x: int = 1`). - DeclarationAndBinding, -} - -impl DefinitionCategory { - /// True if this definition establishes a "declared type" for the symbol. - /// - /// If so, any assignments reached by this definition are in error if they assign a value of a - /// type not assignable to the declared type. - /// - /// Annotations establish a declared type. So do function and class definitions, and imports. - pub(crate) fn is_declaration(self) -> bool { - matches!( - self, - DefinitionCategory::Declaration | DefinitionCategory::DeclarationAndBinding - ) - } - - /// True if this definition assigns a value to the symbol. - /// - /// False only for annotated assignments without a RHS. - pub(crate) fn is_binding(self) -> bool { - matches!( - self, - DefinitionCategory::Binding | DefinitionCategory::DeclarationAndBinding - ) - } -} - -/// The kind of a definition. -/// -/// ## Usage in salsa tracked structs -/// -/// [`DefinitionKind`] fields in salsa tracked structs should be tracked (attributed with `#[tracked]`) -/// because the kind is a thin wrapper around [`AstNodeRef`]. See the [`AstNodeRef`] documentation -/// for an in-depth explanation of why this is necessary. -#[derive(Clone, Debug)] -pub enum DefinitionKind<'db> { - Import(ImportDefinitionKind), - ImportFrom(ImportFromDefinitionKind), - StarImport(StarImportDefinitionKind), - Function(AstNodeRef), - Class(AstNodeRef), - TypeAlias(AstNodeRef), - NamedExpression(AstNodeRef), - Assignment(AssignmentDefinitionKind<'db>), - AnnotatedAssignment(AnnotatedAssignmentDefinitionKind), - AugmentedAssignment(AstNodeRef), - For(ForStmtDefinitionKind<'db>), - Comprehension(ComprehensionDefinitionKind), - VariadicPositionalParameter(AstNodeRef), - VariadicKeywordParameter(AstNodeRef), - Parameter(AstNodeRef), - WithItem(WithItemDefinitionKind<'db>), - MatchPattern(MatchPatternDefinitionKind), - ExceptHandler(ExceptHandlerDefinitionKind), - TypeVar(AstNodeRef), - ParamSpec(AstNodeRef), - TypeVarTuple(AstNodeRef), -} - -impl DefinitionKind<'_> { - pub(crate) fn is_reexported(&self) -> bool { - match self { - DefinitionKind::Import(import) => import.is_reexported(), - DefinitionKind::ImportFrom(import) => import.is_reexported(), - _ => true, - } - } - - 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., - /// [`ast::ExprName`] or [`ast::Identifier`] but could also be other nodes. - pub(crate) fn target_range(&self) -> TextRange { - 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(), - DefinitionKind::NamedExpression(named) => named.target.range(), - DefinitionKind::Assignment(assignment) => assignment.target.range(), - DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(), - DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.target.range(), - DefinitionKind::For(for_stmt) => for_stmt.target.range(), - DefinitionKind::Comprehension(comp) => comp.target().range(), - DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(), - DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(), - DefinitionKind::Parameter(parameter) => parameter.parameter.name.range(), - DefinitionKind::WithItem(with_item) => with_item.target.range(), - DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(), - DefinitionKind::ExceptHandler(handler) => handler.node().range(), - DefinitionKind::TypeVar(type_var) => type_var.name.range(), - DefinitionKind::ParamSpec(param_spec) => param_spec.name.range(), - DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.name.range(), - } - } - - /// Returns the [`TextRange`] of the entire definition. - pub(crate) fn full_range(&self) -> TextRange { - match self { - DefinitionKind::Import(import) => import.alias().range(), - DefinitionKind::ImportFrom(import) => import.alias().range(), - DefinitionKind::StarImport(import) => import.import().range(), - DefinitionKind::Function(function) => function.range(), - DefinitionKind::Class(class) => class.range(), - DefinitionKind::TypeAlias(type_alias) => type_alias.range(), - DefinitionKind::NamedExpression(named) => named.range(), - DefinitionKind::Assignment(assignment) => assignment.target.range(), - DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(), - DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.range(), - DefinitionKind::For(for_stmt) => for_stmt.target.range(), - DefinitionKind::Comprehension(comp) => comp.target().range(), - DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(), - DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(), - DefinitionKind::Parameter(parameter) => parameter.parameter.range(), - DefinitionKind::WithItem(with_item) => with_item.target.range(), - DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(), - DefinitionKind::ExceptHandler(handler) => handler.node().range(), - DefinitionKind::TypeVar(type_var) => type_var.range(), - DefinitionKind::ParamSpec(param_spec) => param_spec.range(), - DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.range(), - } - } - - pub(crate) fn category(&self, in_stub: bool) -> DefinitionCategory { - match self { - // functions, classes, and imports always bind, and we consider them declarations - DefinitionKind::Function(_) - | DefinitionKind::Class(_) - | DefinitionKind::TypeAlias(_) - | DefinitionKind::Import(_) - | DefinitionKind::ImportFrom(_) - | DefinitionKind::StarImport(_) - | DefinitionKind::TypeVar(_) - | DefinitionKind::ParamSpec(_) - | DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding, - // a parameter always binds a value, but is only a declaration if annotated - DefinitionKind::VariadicPositionalParameter(parameter) - | DefinitionKind::VariadicKeywordParameter(parameter) => { - if parameter.annotation.is_some() { - DefinitionCategory::DeclarationAndBinding - } else { - DefinitionCategory::Binding - } - } - // presence of a default is irrelevant, same logic as for a no-default parameter - DefinitionKind::Parameter(parameter_with_default) => { - if parameter_with_default.parameter.annotation.is_some() { - DefinitionCategory::DeclarationAndBinding - } else { - DefinitionCategory::Binding - } - } - // Annotated assignment is always a declaration. It is also a binding if there is a RHS - // or if we are in a stub file. Unfortunately, it is common for stubs to omit even an `...` value placeholder. - DefinitionKind::AnnotatedAssignment(ann_assign) => { - if in_stub || ann_assign.value.is_some() { - DefinitionCategory::DeclarationAndBinding - } else { - DefinitionCategory::Declaration - } - } - // all of these bind values without declaring a type - DefinitionKind::NamedExpression(_) - | DefinitionKind::Assignment(_) - | DefinitionKind::AugmentedAssignment(_) - | DefinitionKind::For(_) - | DefinitionKind::Comprehension(_) - | DefinitionKind::WithItem(_) - | DefinitionKind::MatchPattern(_) - | DefinitionKind::ExceptHandler(_) => DefinitionCategory::Binding, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Hash)] -pub(crate) enum TargetKind<'db> { - Sequence(UnpackPosition, Unpack<'db>), - NameOrAttribute, -} - -impl<'db> From)>> for TargetKind<'db> { - fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self { - match value { - Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack), - None => TargetKind::NameOrAttribute, - } - } -} - -#[derive(Clone, Debug)] -pub struct StarImportDefinitionKind { - node: AstNodeRef, - 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, - identifier: AstNodeRef, - index: u32, -} - -impl MatchPatternDefinitionKind { - pub(crate) fn pattern(&self) -> &ast::Pattern { - self.pattern.node() - } - - pub(crate) fn index(&self) -> u32 { - self.index - } -} - -#[derive(Clone, Debug)] -pub struct ComprehensionDefinitionKind { - iterable: AstNodeRef, - target: AstNodeRef, - first: bool, - is_async: bool, -} - -impl ComprehensionDefinitionKind { - pub(crate) fn iterable(&self) -> &ast::Expr { - self.iterable.node() - } - - pub(crate) fn target(&self) -> &ast::ExprName { - self.target.node() - } - - pub(crate) fn is_first(&self) -> bool { - self.first - } - - pub(crate) fn is_async(&self) -> bool { - self.is_async - } -} - -#[derive(Clone, Debug)] -pub struct ImportDefinitionKind { - node: AstNodeRef, - alias_index: usize, - is_reexported: bool, -} - -impl ImportDefinitionKind { - pub(crate) fn import(&self) -> &ast::StmtImport { - self.node.node() - } - - pub(crate) fn alias(&self) -> &ast::Alias { - &self.node.node().names[self.alias_index] - } - - pub(crate) fn is_reexported(&self) -> bool { - self.is_reexported - } -} - -#[derive(Clone, Debug)] -pub struct ImportFromDefinitionKind { - node: AstNodeRef, - alias_index: usize, - is_reexported: bool, -} - -impl ImportFromDefinitionKind { - pub(crate) fn import(&self) -> &ast::StmtImportFrom { - self.node.node() - } - - pub(crate) fn alias(&self) -> &ast::Alias { - &self.node.node().names[self.alias_index] - } - - pub(crate) fn is_reexported(&self) -> bool { - self.is_reexported - } -} - -#[derive(Clone, Debug)] -pub struct AssignmentDefinitionKind<'db> { - target_kind: TargetKind<'db>, - value: AstNodeRef, - target: AstNodeRef, -} - -impl<'db> AssignmentDefinitionKind<'db> { - pub(crate) fn new( - target_kind: TargetKind<'db>, - value: AstNodeRef, - target: AstNodeRef, - ) -> Self { - Self { - target_kind, - value, - target, - } - } - - pub(crate) fn target_kind(&self) -> TargetKind<'db> { - self.target_kind - } - - pub(crate) fn value(&self) -> &ast::Expr { - self.value.node() - } - - pub(crate) fn target(&self) -> &ast::Expr { - self.target.node() - } -} - -#[derive(Clone, Debug)] -pub struct AnnotatedAssignmentDefinitionKind { - annotation: AstNodeRef, - value: Option>, - target: AstNodeRef, -} - -impl AnnotatedAssignmentDefinitionKind { - pub(crate) fn new( - annotation: AstNodeRef, - value: Option>, - target: AstNodeRef, - ) -> Self { - Self { - annotation, - value, - target, - } - } - - pub(crate) fn value(&self) -> Option<&ast::Expr> { - self.value.as_deref() - } - - pub(crate) fn annotation(&self) -> &ast::Expr { - self.annotation.node() - } - - pub(crate) fn target(&self) -> &ast::Expr { - self.target.node() - } -} - -#[derive(Clone, Debug)] -pub struct WithItemDefinitionKind<'db> { - target_kind: TargetKind<'db>, - context_expr: AstNodeRef, - target: AstNodeRef, - is_async: bool, -} - -impl<'db> WithItemDefinitionKind<'db> { - pub(crate) fn new( - target_kind: TargetKind<'db>, - context_expr: AstNodeRef, - target: AstNodeRef, - is_async: bool, - ) -> Self { - Self { - target_kind, - context_expr, - target, - is_async, - } - } - - pub(crate) fn context_expr(&self) -> &ast::Expr { - self.context_expr.node() - } - - pub(crate) fn target_kind(&self) -> TargetKind<'db> { - self.target_kind - } - - pub(crate) fn target(&self) -> &ast::Expr { - self.target.node() - } - - pub(crate) const fn is_async(&self) -> bool { - self.is_async - } -} - -#[derive(Clone, Debug)] -pub struct ForStmtDefinitionKind<'db> { - target_kind: TargetKind<'db>, - iterable: AstNodeRef, - target: AstNodeRef, - is_async: bool, -} - -impl<'db> ForStmtDefinitionKind<'db> { - pub(crate) fn new( - target_kind: TargetKind<'db>, - iterable: AstNodeRef, - target: AstNodeRef, - is_async: bool, - ) -> Self { - Self { - target_kind, - iterable, - target, - is_async, - } - } - - pub(crate) fn iterable(&self) -> &ast::Expr { - self.iterable.node() - } - - pub(crate) fn target_kind(&self) -> TargetKind<'db> { - self.target_kind - } - - pub(crate) fn target(&self) -> &ast::Expr { - self.target.node() - } - - pub(crate) const fn is_async(&self) -> bool { - self.is_async - } -} - -#[derive(Clone, Debug)] -pub struct ExceptHandlerDefinitionKind { - handler: AstNodeRef, - is_star: bool, -} - -impl ExceptHandlerDefinitionKind { - pub(crate) fn node(&self) -> &ast::ExceptHandlerExceptHandler { - self.handler.node() - } - - pub(crate) fn handled_exceptions(&self) -> Option<&ast::Expr> { - self.node().type_.as_deref() - } - - pub(crate) fn is_star(&self) -> bool { - self.is_star - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)] -pub(crate) struct DefinitionNodeKey(NodeKey); - -impl From<&ast::Alias> for DefinitionNodeKey { - fn from(node: &ast::Alias) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::StmtFunctionDef> for DefinitionNodeKey { - fn from(node: &ast::StmtFunctionDef) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::StmtClassDef> for DefinitionNodeKey { - fn from(node: &ast::StmtClassDef) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::StmtTypeAlias> for DefinitionNodeKey { - fn from(node: &ast::StmtTypeAlias) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::ExprName> for DefinitionNodeKey { - fn from(node: &ast::ExprName) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::ExprNamed> for DefinitionNodeKey { - fn from(node: &ast::ExprNamed) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::StmtAnnAssign> for DefinitionNodeKey { - fn from(node: &ast::StmtAnnAssign) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::StmtAugAssign> for DefinitionNodeKey { - fn from(node: &ast::StmtAugAssign) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::Parameter> for DefinitionNodeKey { - fn from(node: &ast::Parameter) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From<&ast::ParameterWithDefault> for DefinitionNodeKey { - fn from(node: &ast::ParameterWithDefault) -> Self { - Self(NodeKey::from_node(node)) - } -} - -impl From> for DefinitionNodeKey { - fn from(value: ast::AnyParameterRef) -> Self { - Self(match value { - ast::AnyParameterRef::Variadic(node) => NodeKey::from_node(node), - ast::AnyParameterRef::NonVariadic(node) => NodeKey::from_node(node), - }) - } -} - -impl From<&ast::Identifier> for DefinitionNodeKey { - fn from(identifier: &ast::Identifier) -> Self { - Self(NodeKey::from_node(identifier)) - } -} - -impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey { - fn from(handler: &ast::ExceptHandlerExceptHandler) -> Self { - Self(NodeKey::from_node(handler)) - } -} - -impl From<&ast::TypeParamTypeVar> for DefinitionNodeKey { - fn from(value: &ast::TypeParamTypeVar) -> Self { - Self(NodeKey::from_node(value)) - } -} - -impl From<&ast::TypeParamParamSpec> for DefinitionNodeKey { - fn from(value: &ast::TypeParamParamSpec) -> Self { - Self(NodeKey::from_node(value)) - } -} - -impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey { - fn from(value: &ast::TypeParamTypeVarTuple) -> Self { - Self(NodeKey::from_node(value)) - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/expression.rs b/crates/red_knot_python_semantic/src/semantic_index/expression.rs deleted file mode 100644 index 9ac1fd30b81eb..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/expression.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::ast_node_ref::AstNodeRef; -use crate::db::Db; -use crate::semantic_index::symbol::{FileScopeId, ScopeId}; -use ruff_db::files::File; -use ruff_python_ast as ast; -use salsa; - -/// Whether or not this expression should be inferred as a normal expression or -/// a type expression. For example, in `self.x: = `, the -/// `` is inferred as a type expression, while `` is inferred -/// as a normal expression. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) enum ExpressionKind { - Normal, - TypeExpression, -} - -/// An independently type-inferable expression. -/// -/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment. -/// -/// ## Module-local type -/// This type should not be used as part of any cross-module API because -/// it holds a reference to the AST node. Range-offset changes -/// then propagate through all usages, and deserialization requires -/// reparsing the entire module. -/// -/// E.g. don't use this type in: -/// -/// * a return type of a cross-module query -/// * a field of a type that is a return type of a cross-module query -/// * an argument of a cross-module query -#[salsa::tracked(debug)] -pub(crate) struct Expression<'db> { - /// The file in which the expression occurs. - pub(crate) file: File, - - /// The scope in which the expression occurs. - pub(crate) file_scope: FileScopeId, - - /// The expression node. - #[no_eq] - #[tracked] - #[return_ref] - pub(crate) node_ref: AstNodeRef, - - /// Should this expression be inferred as a normal expression or a type expression? - pub(crate) kind: ExpressionKind, - - count: countme::Count>, -} - -impl<'db> Expression<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/predicate.rs b/crates/red_knot_python_semantic/src/semantic_index/predicate.rs deleted file mode 100644 index c2885022e8cef..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/predicate.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! _Predicates_ are Python expressions whose runtime values can affect type inference. -//! -//! We currently use predicates in two places: -//! -//! - [_Narrowing constraints_][crate::semantic_index::narrowing_constraints] constrain the type of -//! a binding that is visible at a particular use. -//! - [_Visibility constraints_][crate::semantic_index::visibility_constraints] determine the -//! static visibility of a binding, and the reachability of a statement. - -use ruff_db::files::File; -use ruff_index::{newtype_index, IndexVec}; -use ruff_python_ast::Singleton; - -use crate::db::Db; -use crate::semantic_index::expression::Expression; -use crate::semantic_index::global_scope; -use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId}; - -// A scoped identifier for each `Predicate` in a scope. -#[newtype_index] -#[derive(Ord, PartialOrd)] -pub(crate) struct ScopedPredicateId; - -// A collection of predicates for a given scope. -pub(crate) type Predicates<'db> = IndexVec>; - -#[derive(Debug, Default)] -pub(crate) struct PredicatesBuilder<'db> { - predicates: IndexVec>, -} - -impl<'db> PredicatesBuilder<'db> { - /// Adds a predicate. Note that we do not deduplicate predicates. If you add a `Predicate` - /// more than once, you will get distinct `ScopedPredicateId`s for each one. (This lets you - /// model predicates that might evaluate to different values at different points of execution.) - pub(crate) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { - self.predicates.push(predicate) - } - - pub(crate) fn build(mut self) -> Predicates<'db> { - self.predicates.shrink_to_fit(); - self.predicates - } -} - -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)] -pub(crate) struct Predicate<'db> { - pub(crate) node: PredicateNode<'db>, - pub(crate) is_positive: bool, -} - -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)] -pub(crate) enum PredicateNode<'db> { - Expression(Expression<'db>), - Pattern(PatternPredicate<'db>), - StarImportPlaceholder(StarImportPlaceholderPredicate<'db>), -} - -/// Pattern kinds for which we support type narrowing and/or static visibility analysis. -#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)] -pub(crate) enum PatternPredicateKind<'db> { - Singleton(Singleton), - Value(Expression<'db>), - Or(Vec>), - Class(Expression<'db>), - Unsupported, -} - -#[salsa::tracked(debug)] -pub(crate) struct PatternPredicate<'db> { - pub(crate) file: File, - - pub(crate) file_scope: FileScopeId, - - pub(crate) subject: Expression<'db>, - - #[return_ref] - pub(crate) kind: PatternPredicateKind<'db>, - - pub(crate) guard: Option>, - - count: countme::Count>, -} - -impl<'db> PatternPredicate<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) - } -} - -/// A "placeholder predicate" that is used to model the fact that the boundness of a -/// (possible) definition or declaration caused by a `*` import cannot be fully determined -/// until type-inference time. This is essentially the same as a standard visibility constraint, -/// so we reuse the [`Predicate`] infrastructure to model it. -/// -/// To illustrate, say we have a module `exporter.py` like so: -/// -/// ```py -/// if : -/// class A: ... -/// ``` -/// -/// and we have a module `importer.py` like so: -/// -/// ```py -/// A = 1 -/// -/// from importer import * -/// ``` -/// -/// Since we cannot know whether or not is true at semantic-index time, -/// we record a definition for `A` in `b.py` as a result of the `from a import *` -/// statement, but place a predicate on it to record the fact that we don't yet -/// know whether this definition will be visible from all control-flow paths or not. -/// Essentially, we model `b.py` as something similar to this: -/// -/// ```py -/// A = 1 -/// -/// if : -/// from a import A -/// ``` -/// -/// At type-check time, the placeholder predicate for the `A` definition is evaluated by -/// attempting to resolve the `A` symbol in `a.py`'s global namespace: -/// - If it resolves to a definitely bound symbol, then the predicate resolves to [`Truthiness::AlwaysTrue`] -/// - If it resolves to an unbound symbol, then the predicate resolves to [`Truthiness::AlwaysFalse`] -/// - If it resolves to a possibly bound symbol, then the predicate resolves to [`Truthiness::Ambiguous`] -/// -/// [Truthiness]: [crate::types::Truthiness] -#[salsa::tracked(debug)] -pub(crate) struct StarImportPlaceholderPredicate<'db> { - pub(crate) importing_file: File, - - /// Each symbol imported by a `*` import has a separate predicate associated with it: - /// this field identifies which symbol that is. - /// - /// Note that a [`ScopedSymbolId`] is only meaningful if you also know the scope - /// it is relative to. For this specific struct, however, there's no need to store a - /// separate field to hold the ID of the scope. `StarImportPredicate`s are only created - /// for valid `*`-import definitions, and valid `*`-import definitions can only ever - /// exist in the global scope; thus, we know that the `symbol_id` here will be relative - /// to the global scope of the importing file. - pub(crate) symbol_id: ScopedSymbolId, - - pub(crate) referenced_file: File, -} - -impl<'db> StarImportPlaceholderPredicate<'db> { - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - // See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]: - // valid `*`-import definitions can only take place in the global scope. - global_scope(db, self.importing_file(db)) - } -} - -impl<'db> From> for Predicate<'db> { - fn from(predicate: StarImportPlaceholderPredicate<'db>) -> Self { - Predicate { - node: PredicateNode::StarImportPlaceholder(predicate), - is_positive: true, - } - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs deleted file mode 100644 index ec9ef3886eff3..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs +++ /dev/null @@ -1,576 +0,0 @@ -use std::hash::{Hash, Hasher}; -use std::ops::Range; - -use bitflags::bitflags; -use hashbrown::hash_map::RawEntryMut; -use ruff_db::files::File; -use ruff_db::parsed::ParsedModule; -use ruff_index::{newtype_index, IndexVec}; -use ruff_python_ast as ast; -use ruff_python_ast::name::Name; -use rustc_hash::FxHasher; - -use crate::ast_node_ref::AstNodeRef; -use crate::node_key::NodeKey; -use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId; -use crate::semantic_index::{semantic_index, SymbolMap}; -use crate::Db; - -#[derive(Eq, PartialEq, Debug)] -pub struct Symbol { - name: Name, - flags: SymbolFlags, -} - -impl Symbol { - fn new(name: Name) -> Self { - Self { - name, - flags: SymbolFlags::empty(), - } - } - - fn insert_flags(&mut self, flags: SymbolFlags) { - self.flags.insert(flags); - } - - /// The symbol's name. - pub fn name(&self) -> &Name { - &self.name - } - - /// Is the symbol used in its containing scope? - pub fn is_used(&self) -> bool { - self.flags.contains(SymbolFlags::IS_USED) - } - - /// Is the symbol defined in its containing scope? - pub fn is_bound(&self) -> bool { - self.flags.contains(SymbolFlags::IS_BOUND) - } - - /// Is the symbol declared in its containing scope? - pub fn is_declared(&self) -> bool { - self.flags.contains(SymbolFlags::IS_DECLARED) - } -} - -bitflags! { - /// Flags that can be queried to obtain information about a symbol in a given scope. - /// - /// See the doc-comment at the top of [`super::use_def`] for explanations of what it - /// means for a symbol to be *bound* as opposed to *declared*. - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - struct SymbolFlags: u8 { - const IS_USED = 1 << 0; - const IS_BOUND = 1 << 1; - const IS_DECLARED = 1 << 2; - /// TODO: This flag is not yet set by anything - const MARKED_GLOBAL = 1 << 3; - /// TODO: This flag is not yet set by anything - const MARKED_NONLOCAL = 1 << 4; - } -} - -/// ID that uniquely identifies a symbol in a file. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct FileSymbolId { - scope: FileScopeId, - scoped_symbol_id: ScopedSymbolId, -} - -impl FileSymbolId { - pub fn scope(self) -> FileScopeId { - self.scope - } - - pub(crate) fn scoped_symbol_id(self) -> ScopedSymbolId { - self.scoped_symbol_id - } -} - -impl From for ScopedSymbolId { - fn from(val: FileSymbolId) -> Self { - val.scoped_symbol_id() - } -} - -/// Symbol ID that uniquely identifies a symbol inside a [`Scope`]. -#[newtype_index] -#[derive(salsa::Update)] -pub struct ScopedSymbolId; - -/// A cross-module identifier of a scope that can be used as a salsa query parameter. -#[salsa::tracked(debug)] -pub struct ScopeId<'db> { - pub file: File, - - pub file_scope_id: FileScopeId, - - count: countme::Count>, -} - -impl<'db> ScopeId<'db> { - pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { - self.node(db).scope_kind().is_function_like() - } - - pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { - self.node(db).scope_kind().is_type_parameter() - } - - pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind { - self.scope(db).node() - } - - pub(crate) fn scope(self, db: &dyn Db) -> &Scope { - semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) - } - - #[cfg(test)] - pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { - match self.node(db) { - NodeWithScopeKind::Module => "", - NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => { - class.name.as_str() - } - NodeWithScopeKind::Function(function) - | NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(), - NodeWithScopeKind::TypeAlias(type_alias) - | NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias - .name - .as_name_expr() - .map(|name| name.id.as_str()) - .unwrap_or(""), - NodeWithScopeKind::Lambda(_) => "", - NodeWithScopeKind::ListComprehension(_) => "", - NodeWithScopeKind::SetComprehension(_) => "", - NodeWithScopeKind::DictComprehension(_) => "", - NodeWithScopeKind::GeneratorExpression(_) => "", - } - } -} - -/// ID that uniquely identifies a scope inside of a module. -#[newtype_index] -#[derive(salsa::Update)] -pub struct FileScopeId; - -impl FileScopeId { - /// Returns the scope id of the module-global scope. - pub fn global() -> Self { - FileScopeId::from_u32(0) - } - - pub fn is_global(self) -> bool { - self == FileScopeId::global() - } - - pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { - let index = semantic_index(db, file); - index.scope_ids_by_scope[self] - } -} - -#[derive(Debug, salsa::Update)] -pub struct Scope { - parent: Option, - node: NodeWithScopeKind, - descendants: Range, - reachability: ScopedVisibilityConstraintId, -} - -impl Scope { - pub(super) fn new( - parent: Option, - node: NodeWithScopeKind, - descendants: Range, - reachability: ScopedVisibilityConstraintId, - ) -> Self { - Scope { - parent, - node, - descendants, - reachability, - } - } - - pub fn parent(&self) -> Option { - self.parent - } - - pub fn node(&self) -> &NodeWithScopeKind { - &self.node - } - - pub fn kind(&self) -> ScopeKind { - self.node().scope_kind() - } - - pub fn descendants(&self) -> Range { - self.descendants.clone() - } - - pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) { - self.descendants = self.descendants.start..children_end; - } - - pub(crate) fn is_eager(&self) -> bool { - self.kind().is_eager() - } - - pub(crate) fn reachability(&self) -> ScopedVisibilityConstraintId { - self.reachability - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ScopeKind { - Module, - Annotation, - Class, - Function, - Lambda, - Comprehension, - TypeAlias, -} - -impl ScopeKind { - pub(crate) fn is_eager(self) -> bool { - match self { - ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true, - ScopeKind::Annotation - | ScopeKind::Function - | ScopeKind::Lambda - | ScopeKind::TypeAlias => false, - } - } - - pub(crate) fn is_function_like(self) -> bool { - // Type parameter scopes behave like function scopes in terms of name resolution; CPython - // symbol table also uses the term "function-like" for these scopes. - matches!( - self, - ScopeKind::Annotation - | ScopeKind::Function - | ScopeKind::Lambda - | ScopeKind::TypeAlias - | ScopeKind::Comprehension - ) - } - - pub(crate) fn is_class(self) -> bool { - matches!(self, ScopeKind::Class) - } - - pub(crate) fn is_type_parameter(self) -> bool { - matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) - } -} - -/// Symbol table for a specific [`Scope`]. -#[derive(Default, salsa::Update)] -pub struct SymbolTable { - /// The symbols in this scope. - symbols: IndexVec, - - /// The symbols indexed by name. - symbols_by_name: SymbolMap, -} - -impl SymbolTable { - fn shrink_to_fit(&mut self) { - self.symbols.shrink_to_fit(); - } - - pub(crate) fn symbol(&self, symbol_id: impl Into) -> &Symbol { - &self.symbols[symbol_id.into()] - } - - #[allow(unused)] - pub(crate) fn symbol_ids(&self) -> impl Iterator { - self.symbols.indices() - } - - pub fn symbols(&self) -> impl Iterator { - self.symbols.iter() - } - - /// Returns the symbol named `name`. - pub(crate) fn symbol_by_name(&self, name: &str) -> Option<&Symbol> { - let id = self.symbol_id_by_name(name)?; - Some(self.symbol(id)) - } - - /// Returns the [`ScopedSymbolId`] of the symbol named `name`. - pub(crate) fn symbol_id_by_name(&self, name: &str) -> Option { - let (id, ()) = self - .symbols_by_name - .raw_entry() - .from_hash(Self::hash_name(name), |id| { - self.symbol(*id).name().as_str() == name - })?; - - Some(*id) - } - - fn hash_name(name: &str) -> u64 { - let mut hasher = FxHasher::default(); - name.hash(&mut hasher); - hasher.finish() - } -} - -impl PartialEq for SymbolTable { - fn eq(&self, other: &Self) -> bool { - // We don't need to compare the symbols_by_name because the name is already captured in `Symbol`. - self.symbols == other.symbols - } -} - -impl Eq for SymbolTable {} - -impl std::fmt::Debug for SymbolTable { - /// Exclude the `symbols_by_name` field from the debug output. - /// It's very noisy and not useful for debugging. - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("SymbolTable") - .field(&self.symbols) - .finish_non_exhaustive() - } -} - -#[derive(Debug, Default)] -pub(super) struct SymbolTableBuilder { - table: SymbolTable, -} - -impl SymbolTableBuilder { - pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) { - let hash = SymbolTable::hash_name(&name); - let entry = self - .table - .symbols_by_name - .raw_entry_mut() - .from_hash(hash, |id| self.table.symbols[*id].name() == &name); - - match entry { - RawEntryMut::Occupied(entry) => (*entry.key(), false), - RawEntryMut::Vacant(entry) => { - let symbol = Symbol::new(name); - - let id = self.table.symbols.push(symbol); - entry.insert_with_hasher(hash, id, (), |id| { - SymbolTable::hash_name(self.table.symbols[*id].name().as_str()) - }); - (id, true) - } - } - } - - pub(super) fn mark_symbol_bound(&mut self, id: ScopedSymbolId) { - self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND); - } - - pub(super) fn mark_symbol_declared(&mut self, id: ScopedSymbolId) { - self.table.symbols[id].insert_flags(SymbolFlags::IS_DECLARED); - } - - pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) { - self.table.symbols[id].insert_flags(SymbolFlags::IS_USED); - } - - pub(super) fn symbols(&self) -> impl Iterator { - self.table.symbols() - } - - pub(super) fn symbol_id_by_name(&self, name: &str) -> Option { - self.table.symbol_id_by_name(name) - } - - pub(super) fn symbol(&self, symbol_id: impl Into) -> &Symbol { - self.table.symbol(symbol_id) - } - - pub(super) fn finish(mut self) -> SymbolTable { - self.table.shrink_to_fit(); - self.table - } -} - -/// Reference to a node that introduces a new scope. -#[derive(Copy, Clone, Debug)] -pub(crate) enum NodeWithScopeRef<'a> { - Module, - Class(&'a ast::StmtClassDef), - Function(&'a ast::StmtFunctionDef), - Lambda(&'a ast::ExprLambda), - FunctionTypeParameters(&'a ast::StmtFunctionDef), - ClassTypeParameters(&'a ast::StmtClassDef), - TypeAlias(&'a ast::StmtTypeAlias), - TypeAliasTypeParameters(&'a ast::StmtTypeAlias), - ListComprehension(&'a ast::ExprListComp), - SetComprehension(&'a ast::ExprSetComp), - DictComprehension(&'a ast::ExprDictComp), - GeneratorExpression(&'a ast::ExprGenerator), -} - -impl NodeWithScopeRef<'_> { - /// Converts the unowned reference to an owned [`NodeWithScopeKind`]. - /// - /// # Safety - /// The node wrapped by `self` must be a child of `module`. - #[allow(unsafe_code)] - pub(super) unsafe fn to_kind(self, module: ParsedModule) -> NodeWithScopeKind { - match self { - NodeWithScopeRef::Module => NodeWithScopeKind::Module, - NodeWithScopeRef::Class(class) => { - NodeWithScopeKind::Class(AstNodeRef::new(module, class)) - } - NodeWithScopeRef::Function(function) => { - NodeWithScopeKind::Function(AstNodeRef::new(module, function)) - } - NodeWithScopeRef::TypeAlias(type_alias) => { - NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias)) - } - NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { - NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias)) - } - NodeWithScopeRef::Lambda(lambda) => { - NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda)) - } - NodeWithScopeRef::FunctionTypeParameters(function) => { - NodeWithScopeKind::FunctionTypeParameters(AstNodeRef::new(module, function)) - } - NodeWithScopeRef::ClassTypeParameters(class) => { - NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class)) - } - NodeWithScopeRef::ListComprehension(comprehension) => { - NodeWithScopeKind::ListComprehension(AstNodeRef::new(module, comprehension)) - } - NodeWithScopeRef::SetComprehension(comprehension) => { - NodeWithScopeKind::SetComprehension(AstNodeRef::new(module, comprehension)) - } - NodeWithScopeRef::DictComprehension(comprehension) => { - NodeWithScopeKind::DictComprehension(AstNodeRef::new(module, comprehension)) - } - NodeWithScopeRef::GeneratorExpression(generator) => { - NodeWithScopeKind::GeneratorExpression(AstNodeRef::new(module, generator)) - } - } - } - - pub(crate) fn node_key(self) -> NodeWithScopeKey { - match self { - NodeWithScopeRef::Module => NodeWithScopeKey::Module, - NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), - NodeWithScopeRef::Function(function) => { - NodeWithScopeKey::Function(NodeKey::from_node(function)) - } - NodeWithScopeRef::Lambda(lambda) => { - NodeWithScopeKey::Lambda(NodeKey::from_node(lambda)) - } - NodeWithScopeRef::FunctionTypeParameters(function) => { - NodeWithScopeKey::FunctionTypeParameters(NodeKey::from_node(function)) - } - NodeWithScopeRef::ClassTypeParameters(class) => { - NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class)) - } - NodeWithScopeRef::TypeAlias(type_alias) => { - NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias)) - } - NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { - NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias)) - } - NodeWithScopeRef::ListComprehension(comprehension) => { - NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension)) - } - NodeWithScopeRef::SetComprehension(comprehension) => { - NodeWithScopeKey::SetComprehension(NodeKey::from_node(comprehension)) - } - NodeWithScopeRef::DictComprehension(comprehension) => { - NodeWithScopeKey::DictComprehension(NodeKey::from_node(comprehension)) - } - NodeWithScopeRef::GeneratorExpression(generator) => { - NodeWithScopeKey::GeneratorExpression(NodeKey::from_node(generator)) - } - } - } -} - -/// Node that introduces a new scope. -#[derive(Clone, Debug, salsa::Update)] -pub enum NodeWithScopeKind { - Module, - Class(AstNodeRef), - ClassTypeParameters(AstNodeRef), - Function(AstNodeRef), - FunctionTypeParameters(AstNodeRef), - TypeAliasTypeParameters(AstNodeRef), - TypeAlias(AstNodeRef), - Lambda(AstNodeRef), - ListComprehension(AstNodeRef), - SetComprehension(AstNodeRef), - DictComprehension(AstNodeRef), - GeneratorExpression(AstNodeRef), -} - -impl NodeWithScopeKind { - pub(crate) const fn scope_kind(&self) -> ScopeKind { - match self { - Self::Module => ScopeKind::Module, - Self::Class(_) => ScopeKind::Class, - Self::Function(_) => ScopeKind::Function, - Self::Lambda(_) => ScopeKind::Lambda, - Self::FunctionTypeParameters(_) - | Self::ClassTypeParameters(_) - | Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation, - Self::TypeAlias(_) => ScopeKind::TypeAlias, - Self::ListComprehension(_) - | Self::SetComprehension(_) - | Self::DictComprehension(_) - | Self::GeneratorExpression(_) => ScopeKind::Comprehension, - } - } - - pub fn expect_class(&self) -> &ast::StmtClassDef { - match self { - Self::Class(class) => class.node(), - _ => panic!("expected class"), - } - } - - pub fn expect_function(&self) -> &ast::StmtFunctionDef { - self.as_function().expect("expected function") - } - - pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias { - match self { - Self::TypeAlias(type_alias) => type_alias.node(), - _ => panic!("expected type alias"), - } - } - - pub const fn as_function(&self) -> Option<&ast::StmtFunctionDef> { - match self { - Self::Function(function) => Some(function.node()), - _ => None, - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub(crate) enum NodeWithScopeKey { - Module, - Class(NodeKey), - ClassTypeParameters(NodeKey), - Function(NodeKey), - FunctionTypeParameters(NodeKey), - TypeAlias(NodeKey), - TypeAliasTypeParameters(NodeKey), - Lambda(NodeKey), - ListComprehension(NodeKey), - SetComprehension(NodeKey), - DictComprehension(NodeKey), - GeneratorExpression(NodeKey), -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs deleted file mode 100644 index 50b6d804d5809..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs +++ /dev/null @@ -1,1089 +0,0 @@ -//! First, some terminology: -//! -//! * A "binding" gives a new value to a variable. This includes many different Python statements -//! (assignment statements of course, but also imports, `def` and `class` statements, `as` -//! clauses in `with` and `except` statements, match patterns, and others) and even one -//! expression kind (named expressions). It notably does not include annotated assignment -//! statements without a right-hand side value; these do not assign any new value to the -//! variable. We consider function parameters to be bindings as well, since (from the perspective -//! of the function's internal scope), a function parameter begins the scope bound to a value. -//! -//! * A "declaration" establishes an upper bound type for the values that a variable may be -//! permitted to take on. Annotated assignment statements (with or without an RHS value) are -//! declarations; annotated function parameters are also declarations. We consider `def` and -//! `class` statements to also be declarations, so as to prohibit accidentally shadowing them. -//! -//! Annotated assignments with a right-hand side, and annotated function parameters, are both -//! bindings and declarations. -//! -//! We use [`Definition`] as the universal term (and Salsa tracked struct) encompassing both -//! bindings and declarations. (This sacrifices a bit of type safety in exchange for improved -//! performance via fewer Salsa tracked structs and queries, since most declarations -- typed -//! parameters and annotated assignments with RHS -- are both bindings and declarations.) -//! -//! At any given use of a variable, we can ask about both its "declared type" and its "inferred -//! type". These may be different, but the inferred type must always be assignable to the declared -//! type; that is, the declared type is always wider, and the inferred type may be more precise. If -//! we see an invalid assignment, we emit a diagnostic and abandon our inferred type, deferring to -//! the declared type (this allows an explicit annotation to override bad inference, without a -//! cast), maintaining the invariant. -//! -//! The **inferred type** represents the most precise type we believe encompasses all possible -//! values for the variable at a given use. It is based on a union of the bindings which can reach -//! that use through some control flow path, and the narrowing constraints that control flow must -//! have passed through between the binding and the use. For example, in this code: -//! -//! ```python -//! x = 1 if flag else None -//! if x is not None: -//! use(x) -//! ``` -//! -//! For the use of `x` on the third line, the inferred type should be `Literal[1]`. This is based -//! on the binding on the first line, which assigns the type `Literal[1] | None`, and the narrowing -//! constraint on the second line, which rules out the type `None`, since control flow must pass -//! through this constraint to reach the use in question. -//! -//! The **declared type** represents the code author's declaration (usually through a type -//! annotation) that a given variable should not be assigned any type outside the declared type. In -//! our model, declared types are also control-flow-sensitive; we allow the code author to -//! explicitly redeclare the same variable with a different type. So for a given binding of a -//! variable, we will want to ask which declarations of that variable can reach that binding, in -//! order to determine whether the binding is permitted, or should be a type error. For example: -//! -//! ```python -//! from pathlib import Path -//! def f(path: str): -//! path: Path = Path(path) -//! ``` -//! -//! In this function, the initial declared type of `path` is `str`, meaning that the assignment -//! `path = Path(path)` would be a type error, since it assigns to `path` a value whose type is not -//! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment -//! of the wrong type to a variable. -//! -//! But in some cases it is useful to "shadow" or "redeclare" a variable with a new type, and we -//! permit this, as long as it is done with an explicit re-annotation. So `path: Path = -//! Path(path)`, with the explicit `: Path` annotation, is permitted. -//! -//! The general rule is that whatever declaration(s) can reach a given binding determine the -//! validity of that binding. If there is a path in which the symbol is not declared, that is a -//! declaration of `Unknown`. If multiple declarations can reach a binding, we union them, but by -//! default we also issue a type error, since this implicit union of declared types may hide an -//! error. -//! -//! To support type inference, we build a map from each use of a symbol to the bindings live at -//! that use, and the type narrowing constraints that apply to each binding. -//! -//! Let's take this code sample: -//! -//! ```python -//! x = 1 -//! x = 2 -//! y = x -//! if flag: -//! x = 3 -//! else: -//! x = 4 -//! z = x -//! ``` -//! -//! In this snippet, we have four bindings of `x` (the statements assigning `1`, `2`, `3`, and `4` -//! to it), and two uses of `x` (the `y = x` and `z = x` assignments). The first binding of `x` -//! does not reach any use, because it's immediately replaced by the second binding, before any use -//! happens. (A linter could thus flag the statement `x = 1` as likely superfluous.) -//! -//! The first use of `x` has one live binding: the assignment `x = 2`. -//! -//! Things get a bit more complex when we have branches. We will definitely take either the `if` or -//! the `else` branch. Thus, the second use of `x` has two live bindings: `x = 3` and `x = 4`. The -//! `x = 2` assignment is no longer visible, because it must be replaced by either `x = 3` or `x = -//! 4`, no matter which branch was taken. We don't know which branch was taken, so we must consider -//! both bindings as live, which means eventually we would (in type inference) look at these two -//! bindings and infer a type of `Literal[3, 4]` -- the union of `Literal[3]` and `Literal[4]` -- -//! for the second use of `x`. -//! -//! So that's one question our use-def map needs to answer: given a specific use of a symbol, which -//! binding(s) can reach that use. In [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number -//! all uses (that means a `Name` node with `Load` context) so we have a `ScopedUseId` to -//! efficiently represent each use. -//! -//! We also need to know, for a given definition of a symbol, what type narrowing constraints apply -//! to it. For instance, in this code sample: -//! -//! ```python -//! x = 1 if flag else None -//! if x is not None: -//! use(x) -//! ``` -//! -//! At the use of `x`, the live binding of `x` is `1 if flag else None`, which would infer as the -//! type `Literal[1] | None`. But the constraint `x is not None` dominates this use, which means we -//! can rule out the possibility that `x` is `None` here, which should give us the type -//! `Literal[1]` for this use. -//! -//! For declared types, we need to be able to answer the question "given a binding to a symbol, -//! which declarations of that symbol can reach the binding?" This allows us to emit a diagnostic -//! if the binding is attempting to bind a value of a type that is not assignable to the declared -//! type for that symbol, at that point in control flow. -//! -//! We also need to know, given a declaration of a symbol, what the inferred type of that symbol is -//! at that point. This allows us to emit a diagnostic in a case like `x = "foo"; x: int`. The -//! binding `x = "foo"` occurs before the declaration `x: int`, so according to our -//! control-flow-sensitive interpretation of declarations, the assignment is not an error. But the -//! declaration is an error, since it would violate the "inferred type must be assignable to -//! declared type" rule. -//! -//! Another case we need to handle is when a symbol is referenced from a different scope (for -//! example, an import or a nonlocal reference). We call this "public" use of a symbol. For public -//! use of a symbol, we prefer the declared type, if there are any declarations of that symbol; if -//! not, we fall back to the inferred type. So we also need to know which declarations and bindings -//! can reach the end of the scope. -//! -//! Technically, public use of a symbol could occur from any point in control flow of the scope -//! where the symbol is defined (via inline imports and import cycles, in the case of an import, or -//! via a function call partway through the local scope that ends up using a symbol from the scope -//! via a global or nonlocal reference.) But modeling this fully accurately requires whole-program -//! analysis that isn't tractable for an efficient analysis, since it means a given symbol could -//! have a different type every place it's referenced throughout the program, depending on the -//! shape of arbitrarily-sized call/import graphs. So we follow other Python type checkers in -//! making the simplifying assumption that usually the scope will finish execution before its -//! symbols are made visible to other scopes; for instance, most imports will import from a -//! complete module, not a partially-executed module. (We may want to get a little smarter than -//! this in the future for some closures, but for now this is where we start.) -//! -//! The data structure we build to answer these questions is the `UseDefMap`. It has a -//! `bindings_by_use` vector of [`SymbolBindings`] indexed by [`ScopedUseId`], a -//! `declarations_by_binding` vector of [`SymbolDeclarations`] indexed by [`ScopedDefinitionId`], a -//! `bindings_by_declaration` vector of [`SymbolBindings`] indexed by [`ScopedDefinitionId`], and -//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedSymbolId`]. The values in -//! each of these vectors are (in principle) a list of live bindings at that use/definition, or at -//! the end of the scope for that symbol, with a list of the dominating constraints for each -//! binding. -//! -//! In order to avoid vectors-of-vectors-of-vectors and all the allocations that would entail, we -//! don't actually store these "list of visible definitions" as a vector of [`Definition`]. -//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track -//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and -//! [`ScopedPredicateId`], which are indices into the `all_definitions` and `predicates` -//! indexvecs in the [`UseDefMap`]. -//! -//! There is another special kind of possible "definition" for a symbol: there might be a path from -//! the scope entry to a given use in which the symbol is never bound. We model this with a special -//! "unbound" definition (a `None` entry at the start of the `all_definitions` vector). If that -//! sentinel definition is present in the live bindings at a given use, it means that there is a -//! possible path through control flow in which that symbol is unbound. Similarly, if that sentinel -//! is present in the live declarations, it means that the symbol is (possibly) undeclared. -//! -//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and -//! constraint as they are encountered by the -//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder) AST visit. For -//! each symbol, the builder tracks the `SymbolState` (`SymbolBindings` and `SymbolDeclarations`) -//! for that symbol. When we hit a use or definition of a symbol, we record the necessary parts of -//! the current state for that symbol that we need for that use or definition. When we reach the -//! end of the scope, it records the state for each symbol as the public definitions of that -//! symbol. -//! -//! Let's walk through the above example. Initially we do not have any record of `x`. When we add -//! the new symbol (before we process the first binding), we create a new undefined `SymbolState` -//! which has a single live binding (the "unbound" definition) and a single live declaration (the -//! "undeclared" definition). When we see `x = 1`, we record that as the sole live binding of `x`. -//! The "unbound" binding is no longer visible. Then we see `x = 2`, and we replace `x = 1` as the -//! sole live binding of `x`. When we get to `y = x`, we record that the live bindings for that use -//! of `x` are just the `x = 2` definition. -//! -//! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will -//! happen regardless. Then we take a pre-branch snapshot of the current state for all symbols, -//! which we'll need later. Then we record `flag` as a possible constraint on the current binding -//! (`x = 2`), and go ahead and visit the `if` body. When we see `x = 3`, it replaces `x = 2` -//! (constrained by `flag`) as the sole live binding of `x`. At the end of the `if` body, we take -//! another snapshot of the current symbol state; we'll call this the post-if-body snapshot. -//! -//! Now we need to visit the `else` clause. The conditions when entering the `else` clause should -//! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test -//! failed and we didn't execute the `if` body. So we first reset the builder to the pre-if state, -//! using the snapshot we took previously (meaning we now have `x = 2` as the sole binding for `x` -//! again), and record a *negative* `flag` constraint for all live bindings (`x = 2`). We then -//! visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding of `x`. -//! -//! Now we reach the end of the if/else, and want to visit the following code. The state here needs -//! to reflect that we might have gone through the `if` branch, or we might have gone through the -//! `else` branch, and we don't know which. So we need to "merge" our current builder state -//! (reflecting the end-of-else state, with `x = 4` as the only live binding) with our post-if-body -//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now -//! have two live bindings of `x`: `x = 3` and `x = 4`. -//! -//! Another piece of information that the `UseDefMap` needs to provide are visibility constraints. -//! These are similar to the narrowing constraints, but apply to bindings and declarations within a -//! control flow path. Consider the following example: -//! ```py -//! x = 1 -//! if test: -//! x = 2 -//! y = "y" -//! ``` -//! In principle, there are two possible control flow paths here. However, if we can statically -//! infer `test` to be always truthy or always falsy (that is, `__bool__` of `test` is of type -//! `Literal[True]` or `Literal[False]`), we can rule out one of the possible paths. To support -//! this feature, we record a visibility constraint of `test` to all live bindings and declarations -//! *after* visiting the body of the `if` statement. And we record a negative visibility constraint -//! `~test` to all live bindings/declarations in the (implicit) `else` branch. For the example -//! above, we would record the following visibility constraints (adding the implicit "unbound" -//! definitions for clarity): -//! ```py -//! x = # not live, shadowed by `x = 1` -//! y = # visibility constraint: ~test -//! -//! x = 1 # visibility constraint: ~test -//! if test: -//! x = 2 # visibility constraint: test -//! y = "y" # visibility constraint: test -//! ``` -//! When we encounter a use of `x` after this `if` statement, we would record two live bindings: `x -//! = 1` with a constraint of `~test`, and `x = 2` with a constraint of `test`. In type inference, -//! when we iterate over all live bindings, we can evaluate these constraints to determine if a -//! particular binding is actually visible. For example, if `test` is always truthy, we only see -//! the `x = 2` binding. If `test` is always falsy, we only see the `x = 1` binding. And if the -//! `__bool__` method of `test` returns type `bool`, we can see both bindings. -//! -//! Note that we also record visibility constraints for the start of the scope. This is important -//! to determine if a symbol is definitely bound, possibly unbound, or definitely unbound. In the -//! example above, The `y = ` binding is constrained by `~test`, so `y` would only be -//! definitely-bound if `test` is always truthy. -//! -//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a -//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in -//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it -//! visits a `StmtIf` node. - -use ruff_index::{newtype_index, IndexVec}; -use rustc_hash::FxHashMap; - -use self::symbol_state::ScopedDefinitionId; -use self::symbol_state::{ - LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, SymbolBindings, - SymbolDeclarations, SymbolState, -}; -use crate::node_key::NodeKey; -use crate::semantic_index::ast_ids::ScopedUseId; -use crate::semantic_index::definition::Definition; -use crate::semantic_index::narrowing_constraints::{ - NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator, -}; -use crate::semantic_index::predicate::{ - Predicate, Predicates, PredicatesBuilder, ScopedPredicateId, StarImportPlaceholderPredicate, -}; -use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId}; -use crate::semantic_index::visibility_constraints::{ - ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder, -}; -use crate::types::Truthiness; - -mod symbol_state; - -/// Applicable definitions and constraints for every use of a name. -#[derive(Debug, PartialEq, Eq, salsa::Update)] -pub(crate) struct UseDefMap<'db> { - /// Array of [`Definition`] in this scope. Only the first entry should be `None`; - /// this represents the implicit "unbound"/"undeclared" definition of every symbol. - all_definitions: IndexVec>>, - - /// Array of predicates in this scope. - predicates: Predicates<'db>, - - /// Array of narrowing constraints in this scope. - narrowing_constraints: NarrowingConstraints, - - /// Array of visibility constraints in this scope. - visibility_constraints: VisibilityConstraints, - - /// [`SymbolBindings`] reaching a [`ScopedUseId`]. - bindings_by_use: IndexVec, - - /// Tracks whether or not a given AST node is reachable from the start of the scope. - node_reachability: FxHashMap, - - /// If the definition is a binding (only) -- `x = 1` for example -- then we need - /// [`SymbolDeclarations`] to know whether this binding is permitted by the live declarations. - /// - /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then - /// we don't actually need anything here, all we'll need to validate is that our own RHS is a - /// valid assignment to our own annotation. - declarations_by_binding: FxHashMap, SymbolDeclarations>, - - /// If the definition is a declaration (only) -- `x: int` for example -- then we need - /// [`SymbolBindings`] to know whether this declaration is consistent with the previously - /// inferred type. - /// - /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then - /// we don't actually need anything here, all we'll need to validate is that our own RHS is a - /// valid assignment to our own annotation. - bindings_by_declaration: FxHashMap, SymbolBindings>, - - /// [`SymbolState`] visible at end of scope for each symbol. - public_symbols: IndexVec, - - /// [`SymbolState`] for each instance attribute. - instance_attributes: IndexVec, - - /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested - /// eager scope. - eager_bindings: EagerBindings, - - /// Whether or not the start of the scope is visible. - /// This is used to check if the function can implicitly return `None`. - /// For example: - /// - /// ```python - /// def f(cond: bool) -> int: - /// if cond: - /// return 1 - /// ``` - /// - /// In this case, the function may implicitly return `None`. - /// - /// This is used by `UseDefMap::can_implicit_return`. - scope_start_visibility: ScopedVisibilityConstraintId, -} - -impl<'db> UseDefMap<'db> { - pub(crate) fn bindings_at_use( - &self, - use_id: ScopedUseId, - ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(&self.bindings_by_use[use_id]) - } - - pub(super) fn is_reachable( - &self, - db: &dyn crate::Db, - reachability: ScopedVisibilityConstraintId, - ) -> bool { - !self - .visibility_constraints - .evaluate(db, &self.predicates, reachability) - .is_always_false() - } - - /// Check whether or not a given expression is reachable from the start of the scope. This - /// is a local analysis which does not capture the possibility that the entire scope might - /// be unreachable. Use [`super::SemanticIndex::is_node_reachable`] for the global - /// analysis. - #[track_caller] - pub(super) fn is_node_reachable(&self, db: &dyn crate::Db, node_key: NodeKey) -> bool { - !self - .visibility_constraints - .evaluate( - db, - &self.predicates, - *self - .node_reachability - .get(&node_key) - .expect("`is_node_reachable` should only be called on AST nodes with recorded reachability"), - ) - .is_always_false() - } - - pub(crate) fn public_bindings( - &self, - symbol: ScopedSymbolId, - ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(self.public_symbols[symbol].bindings()) - } - - pub(crate) fn instance_attribute_bindings( - &self, - symbol: ScopedSymbolId, - ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(self.instance_attributes[symbol].bindings()) - } - - pub(crate) fn eager_bindings( - &self, - eager_bindings: ScopedEagerBindingsId, - ) -> Option> { - self.eager_bindings - .get(eager_bindings) - .map(|symbol_bindings| self.bindings_iterator(symbol_bindings)) - } - - pub(crate) fn bindings_at_declaration( - &self, - declaration: Definition<'db>, - ) -> BindingWithConstraintsIterator<'_, 'db> { - self.bindings_iterator(&self.bindings_by_declaration[&declaration]) - } - - pub(crate) fn declarations_at_binding( - &self, - binding: Definition<'db>, - ) -> DeclarationsIterator<'_, 'db> { - self.declarations_iterator(&self.declarations_by_binding[&binding]) - } - - pub(crate) fn public_declarations<'map>( - &'map self, - symbol: ScopedSymbolId, - ) -> DeclarationsIterator<'map, 'db> { - let declarations = self.public_symbols[symbol].declarations(); - self.declarations_iterator(declarations) - } - - pub(crate) fn all_public_declarations<'map>( - &'map self, - ) -> impl Iterator)> + 'map { - (0..self.public_symbols.len()) - .map(ScopedSymbolId::from_usize) - .map(|symbol_id| (symbol_id, self.public_declarations(symbol_id))) - } - - /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. - pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool { - !self - .visibility_constraints - .evaluate(db, &self.predicates, self.scope_start_visibility) - .is_always_false() - } - - pub(crate) fn is_binding_visible( - &self, - db: &dyn crate::Db, - binding: &BindingWithConstraints<'_, 'db>, - ) -> Truthiness { - self.visibility_constraints - .evaluate(db, &self.predicates, binding.visibility_constraint) - } - - fn bindings_iterator<'map>( - &'map self, - bindings: &'map SymbolBindings, - ) -> BindingWithConstraintsIterator<'map, 'db> { - BindingWithConstraintsIterator { - all_definitions: &self.all_definitions, - predicates: &self.predicates, - narrowing_constraints: &self.narrowing_constraints, - visibility_constraints: &self.visibility_constraints, - inner: bindings.iter(), - } - } - - fn declarations_iterator<'map>( - &'map self, - declarations: &'map SymbolDeclarations, - ) -> DeclarationsIterator<'map, 'db> { - DeclarationsIterator { - all_definitions: &self.all_definitions, - predicates: &self.predicates, - visibility_constraints: &self.visibility_constraints, - inner: declarations.iter(), - } - } -} - -/// Uniquely identifies a snapshot of bindings that can be used to resolve a reference in a nested -/// eager scope. -/// -/// An eager scope has its entire body executed immediately at the location where it is defined. -/// For any free references in the nested scope, we use the bindings that are visible at the point -/// where the nested scope is defined, instead of using the public type of the symbol. -/// -/// There is a unique ID for each distinct [`EagerBindingsKey`] in the file. -#[newtype_index] -pub(crate) struct ScopedEagerBindingsId; - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub(crate) struct EagerBindingsKey { - /// The enclosing scope containing the bindings - pub(crate) enclosing_scope: FileScopeId, - /// The referenced symbol (in the enclosing scope) - pub(crate) enclosing_symbol: ScopedSymbolId, - /// The nested eager scope containing the reference - pub(crate) nested_scope: FileScopeId, -} - -/// A snapshot of bindings that can be used to resolve a reference in a nested eager scope. -type EagerBindings = IndexVec; - -#[derive(Debug)] -pub(crate) struct BindingWithConstraintsIterator<'map, 'db> { - all_definitions: &'map IndexVec>>, - pub(crate) predicates: &'map Predicates<'db>, - pub(crate) narrowing_constraints: &'map NarrowingConstraints, - pub(crate) visibility_constraints: &'map VisibilityConstraints, - inner: LiveBindingsIterator<'map>, -} - -impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> { - type Item = BindingWithConstraints<'map, 'db>; - - fn next(&mut self) -> Option { - let predicates = self.predicates; - let narrowing_constraints = self.narrowing_constraints; - - self.inner - .next() - .map(|live_binding| BindingWithConstraints { - binding: self.all_definitions[live_binding.binding], - narrowing_constraint: ConstraintsIterator { - predicates, - constraint_ids: narrowing_constraints - .iter_predicates(live_binding.narrowing_constraint), - }, - visibility_constraint: live_binding.visibility_constraint, - }) - } -} - -impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {} - -pub(crate) struct BindingWithConstraints<'map, 'db> { - pub(crate) binding: Option>, - pub(crate) narrowing_constraint: ConstraintsIterator<'map, 'db>, - pub(crate) visibility_constraint: ScopedVisibilityConstraintId, -} - -pub(crate) struct ConstraintsIterator<'map, 'db> { - predicates: &'map Predicates<'db>, - constraint_ids: NarrowingConstraintsIterator<'map>, -} - -impl<'db> Iterator for ConstraintsIterator<'_, 'db> { - type Item = Predicate<'db>; - - fn next(&mut self) -> Option { - self.constraint_ids - .next() - .map(|narrowing_constraint| self.predicates[narrowing_constraint.predicate()]) - } -} - -impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {} - -#[derive(Clone)] -pub(crate) struct DeclarationsIterator<'map, 'db> { - all_definitions: &'map IndexVec>>, - pub(crate) predicates: &'map Predicates<'db>, - pub(crate) visibility_constraints: &'map VisibilityConstraints, - inner: LiveDeclarationsIterator<'map>, -} - -pub(crate) struct DeclarationWithConstraint<'db> { - pub(crate) declaration: Option>, - pub(crate) visibility_constraint: ScopedVisibilityConstraintId, -} - -impl<'db> Iterator for DeclarationsIterator<'_, 'db> { - type Item = DeclarationWithConstraint<'db>; - - fn next(&mut self) -> Option { - self.inner.next().map( - |LiveDeclaration { - declaration, - visibility_constraint, - }| { - DeclarationWithConstraint { - declaration: self.all_definitions[*declaration], - visibility_constraint: *visibility_constraint, - } - }, - ) - } -} - -impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {} - -/// A snapshot of the definitions and constraints state at a particular point in control flow. -#[derive(Clone, Debug)] -pub(super) struct FlowSnapshot { - symbol_states: IndexVec, - instance_attribute_states: IndexVec, - scope_start_visibility: ScopedVisibilityConstraintId, - reachability: ScopedVisibilityConstraintId, -} - -#[derive(Debug)] -pub(super) struct UseDefMapBuilder<'db> { - /// Append-only array of [`Definition`]. - all_definitions: IndexVec>>, - - /// Builder of predicates. - pub(super) predicates: PredicatesBuilder<'db>, - - /// Builder of narrowing constraints. - pub(super) narrowing_constraints: NarrowingConstraintsBuilder, - - /// Builder of visibility constraints. - pub(super) visibility_constraints: VisibilityConstraintsBuilder, - - /// A constraint which describes the visibility of the unbound/undeclared state, i.e. - /// whether or not a use of a symbol at the current point in control flow would see - /// the fake `x = ` binding at the start of the scope. This is important for - /// cases like the following, where we need to hide the implicit unbound binding in - /// the "else" branch: - /// ```py - /// # x = - /// - /// if True: - /// x = 1 - /// - /// use(x) # the `x = ` binding is not visible here - /// ``` - pub(super) scope_start_visibility: ScopedVisibilityConstraintId, - - /// Live bindings at each so-far-recorded use. - bindings_by_use: IndexVec, - - /// Tracks whether or not the scope start is visible at the current point in control flow. - /// This is subtly different from `scope_start_visibility`, as we apply these constraints - /// at the beginnging of a branch. Visibility constraints, on the other hand, need to be - /// applied at the end of a branch, as we apply them retroactively to all live bindings: - /// ```py - /// y = 1 - /// - /// if test: - /// # we record a reachability constraint of [test] here, - /// # so that it can affect the use of `x`: - /// - /// x # we store a reachability constraint of [test] for this use of `x` - /// - /// y = 2 - /// - /// # we record a visibility constraint of [test] here, which retroactively affects - /// # the `y = 1` and the `y = 2` binding. - /// else: - /// # we record a reachability constraint of [~test] here. - /// - /// pass - /// - /// # we record a visibility constraint of [~test] here, which retroactively affects - /// # the `y = 1` binding. - /// - /// use(y) - /// ``` - /// Depending on the value of `test`, the `y = 1`, `y = 2`, or both bindings may be visible. - /// The use of `x` is recorded with a reachability constraint of `[test]`. - pub(super) reachability: ScopedVisibilityConstraintId, - - /// Tracks whether or not a given AST node is reachable from the start of the scope. - node_reachability: FxHashMap, - - /// Live declarations for each so-far-recorded binding. - declarations_by_binding: FxHashMap, SymbolDeclarations>, - - /// Live bindings for each so-far-recorded declaration. - bindings_by_declaration: FxHashMap, SymbolBindings>, - - /// Currently live bindings and declarations for each symbol. - symbol_states: IndexVec, - - /// Currently live bindings for each instance attribute. - instance_attribute_states: IndexVec, - - /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested - /// eager scope. - eager_bindings: EagerBindings, -} - -impl Default for UseDefMapBuilder<'_> { - fn default() -> Self { - Self { - all_definitions: IndexVec::from_iter([None]), - predicates: PredicatesBuilder::default(), - narrowing_constraints: NarrowingConstraintsBuilder::default(), - visibility_constraints: VisibilityConstraintsBuilder::default(), - scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE, - bindings_by_use: IndexVec::new(), - reachability: ScopedVisibilityConstraintId::ALWAYS_TRUE, - node_reachability: FxHashMap::default(), - declarations_by_binding: FxHashMap::default(), - bindings_by_declaration: FxHashMap::default(), - symbol_states: IndexVec::new(), - eager_bindings: EagerBindings::default(), - instance_attribute_states: IndexVec::new(), - } - } -} - -impl<'db> UseDefMapBuilder<'db> { - pub(super) fn mark_unreachable(&mut self) { - self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE); - self.reachability = ScopedVisibilityConstraintId::ALWAYS_FALSE; - } - - pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) { - let new_symbol = self - .symbol_states - .push(SymbolState::undefined(self.scope_start_visibility)); - debug_assert_eq!(symbol, new_symbol); - } - - pub(super) fn add_attribute(&mut self, symbol: ScopedSymbolId) { - let new_symbol = self - .instance_attribute_states - .push(SymbolState::undefined(self.scope_start_visibility)); - debug_assert_eq!(symbol, new_symbol); - } - - pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) { - let def_id = self.all_definitions.push(Some(binding)); - let symbol_state = &mut self.symbol_states[symbol]; - self.declarations_by_binding - .insert(binding, symbol_state.declarations().clone()); - symbol_state.record_binding(def_id, self.scope_start_visibility); - } - - pub(super) fn record_attribute_binding( - &mut self, - symbol: ScopedSymbolId, - binding: Definition<'db>, - ) { - let def_id = self.all_definitions.push(Some(binding)); - let attribute_state = &mut self.instance_attribute_states[symbol]; - self.declarations_by_binding - .insert(binding, attribute_state.declarations().clone()); - attribute_state.record_binding(def_id, self.scope_start_visibility); - } - - pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { - self.predicates.add_predicate(predicate) - } - - pub(super) fn record_narrowing_constraint(&mut self, predicate: ScopedPredicateId) { - let narrowing_constraint = predicate.into(); - for state in &mut self.symbol_states { - state - .record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint); - } - for state in &mut self.instance_attribute_states { - state - .record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint); - } - } - - pub(super) fn record_visibility_constraint( - &mut self, - constraint: ScopedVisibilityConstraintId, - ) { - for state in &mut self.symbol_states { - state.record_visibility_constraint(&mut self.visibility_constraints, constraint); - } - for state in &mut self.instance_attribute_states { - state.record_visibility_constraint(&mut self.visibility_constraints, constraint); - } - self.scope_start_visibility = self - .visibility_constraints - .add_and_constraint(self.scope_start_visibility, constraint); - } - - /// This method exists solely as a fast path for handling `*`-import visibility constraints. - /// - /// The reason why we add visibility constraints for [`Definition`]s created by `*` imports - /// is laid out in the doc-comment for [`StarImportPlaceholderPredicate`]. But treating these - /// visibility constraints in the use-def map the same way as all other visibility constraints - /// was shown to lead to [significant regressions] for small codebases where typeshed - /// dominates. (Although `*` imports are not common generally, they are used in several - /// important places by typeshed.) - /// - /// To solve these regressions, it was observed that we could add a fast path for `*`-import - /// definitions which added a new symbol to the global scope (as opposed to `*`-import definitions - /// that provided redefinitions for *pre-existing* global-scope symbols). The fast path does a - /// number of things differently to our normal handling of visibility constraints: - /// - /// - It only applies and negates the visibility constraints to a single symbol, rather than to - /// all symbols. This is possible here because, unlike most definitions, we know in advance that - /// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly - /// which definition it is. - /// - /// Doing things this way is cheaper in and of itself. However, it also allows us to avoid - /// calling [`Self::simplify_visibility_constraints`] after the constraint has been applied to - /// the "if-predicate-true" branch and negated for the "if-predicate-false" branch. Simplifying - /// the visibility constraints is only important for symbols that did not have any new - /// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch. - /// - /// - It avoids multiple expensive calls to [`Self::snapshot`]. This is possible because we know - /// the symbol is newly added, so we know the prior state of the symbol was - /// [`SymbolState::undefined`]. - /// - /// - Normally we take care to check whether an "if-predicate-true" branch or an - /// "if-predicate-false" branch contains a terminal statement: these can affect the visibility - /// of symbols defined inside either branch. However, in the case of `*`-import definitions, - /// this is unnecessary (and therefore not done in this method), since we know that a `*`-import - /// predicate cannot create a terminal statement inside either branch. - /// - /// [significant regressions]: https://github.com/astral-sh/ruff/pull/17286#issuecomment-2786755746 - pub(super) fn record_and_negate_star_import_visibility_constraint( - &mut self, - star_import: StarImportPlaceholderPredicate<'db>, - symbol: ScopedSymbolId, - ) { - let predicate_id = self.add_predicate(star_import.into()); - let visibility_id = self.visibility_constraints.add_atom(predicate_id); - let negated_visibility_id = self - .visibility_constraints - .add_not_constraint(visibility_id); - - let mut post_definition_state = std::mem::replace( - &mut self.symbol_states[symbol], - SymbolState::undefined(self.scope_start_visibility), - ); - post_definition_state - .record_visibility_constraint(&mut self.visibility_constraints, visibility_id); - - self.symbol_states[symbol] - .record_visibility_constraint(&mut self.visibility_constraints, negated_visibility_id); - - self.symbol_states[symbol].merge( - post_definition_state, - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - } - - /// This method resets the visibility constraints for all symbols to a previous state - /// *if* there have been no new declarations or bindings since then. Consider the - /// following example: - /// ```py - /// x = 0 - /// y = 0 - /// if test_a: - /// y = 1 - /// elif test_b: - /// y = 2 - /// elif test_c: - /// y = 3 - /// - /// # RESET - /// ``` - /// We build a complex visibility constraint for the `y = 0` binding. We build the same - /// constraint for the `x = 0` binding as well, but at the `RESET` point, we can get rid - /// of it, as the `if`-`elif`-`elif` chain doesn't include any new bindings of `x`. - pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) { - debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len()); - debug_assert!( - self.instance_attribute_states.len() >= snapshot.instance_attribute_states.len() - ); - - // If there are any control flow paths that have become unreachable between `snapshot` and - // now, then it's not valid to simplify any visibility constraints to `snapshot`. - if self.scope_start_visibility != snapshot.scope_start_visibility { - return; - } - - // Note that this loop terminates when we reach a symbol not present in the snapshot. - // This means we keep visibility constraints for all new symbols, which is intended, - // since these symbols have been introduced in the corresponding branch, which might - // be subject to visibility constraints. We only simplify/reset visibility constraints - // for symbols that have the same bindings and declarations present compared to the - // snapshot. - for (current, snapshot) in self.symbol_states.iter_mut().zip(snapshot.symbol_states) { - current.simplify_visibility_constraints(snapshot); - } - for (current, snapshot) in self - .instance_attribute_states - .iter_mut() - .zip(snapshot.instance_attribute_states) - { - current.simplify_visibility_constraints(snapshot); - } - } - - pub(super) fn record_reachability_constraint( - &mut self, - constraint: ScopedVisibilityConstraintId, - ) -> ScopedVisibilityConstraintId { - self.reachability = self - .visibility_constraints - .add_and_constraint(self.reachability, constraint); - self.reachability - } - - pub(super) fn record_declaration( - &mut self, - symbol: ScopedSymbolId, - declaration: Definition<'db>, - ) { - let def_id = self.all_definitions.push(Some(declaration)); - let symbol_state = &mut self.symbol_states[symbol]; - self.bindings_by_declaration - .insert(declaration, symbol_state.bindings().clone()); - symbol_state.record_declaration(def_id); - } - - pub(super) fn record_declaration_and_binding( - &mut self, - symbol: ScopedSymbolId, - definition: Definition<'db>, - ) { - // We don't need to store anything in self.bindings_by_declaration or - // self.declarations_by_binding. - let def_id = self.all_definitions.push(Some(definition)); - let symbol_state = &mut self.symbol_states[symbol]; - symbol_state.record_declaration(def_id); - symbol_state.record_binding(def_id, self.scope_start_visibility); - } - - pub(super) fn record_use( - &mut self, - symbol: ScopedSymbolId, - use_id: ScopedUseId, - node_key: NodeKey, - ) { - // We have a use of a symbol; clone the current bindings for that symbol, and record them - // as the live bindings for this use. - let new_use = self - .bindings_by_use - .push(self.symbol_states[symbol].bindings().clone()); - debug_assert_eq!(use_id, new_use); - - // Track reachability of all uses of symbols to silence `unresolved-reference` - // diagnostics in unreachable code. - self.record_node_reachability(node_key); - } - - pub(super) fn record_node_reachability(&mut self, node_key: NodeKey) { - self.node_reachability.insert(node_key, self.reachability); - } - - pub(super) fn snapshot_eager_bindings( - &mut self, - enclosing_symbol: ScopedSymbolId, - ) -> ScopedEagerBindingsId { - self.eager_bindings - .push(self.symbol_states[enclosing_symbol].bindings().clone()) - } - - /// Take a snapshot of the current visible-symbols state. - pub(super) fn snapshot(&self) -> FlowSnapshot { - FlowSnapshot { - symbol_states: self.symbol_states.clone(), - instance_attribute_states: self.instance_attribute_states.clone(), - scope_start_visibility: self.scope_start_visibility, - reachability: self.reachability, - } - } - - /// Restore the current builder symbols state to the given snapshot. - pub(super) fn restore(&mut self, snapshot: FlowSnapshot) { - // We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol - // IDs must line up), so the current number of known symbols must always be equal to or - // greater than the number of known symbols in a previously-taken snapshot. - let num_symbols = self.symbol_states.len(); - debug_assert!(num_symbols >= snapshot.symbol_states.len()); - let num_attributes = self.instance_attribute_states.len(); - debug_assert!(num_attributes >= snapshot.instance_attribute_states.len()); - - // Restore the current visible-definitions state to the given snapshot. - self.symbol_states = snapshot.symbol_states; - self.instance_attribute_states = snapshot.instance_attribute_states; - self.scope_start_visibility = snapshot.scope_start_visibility; - self.reachability = snapshot.reachability; - - // If the snapshot we are restoring is missing some symbols we've recorded since, we need - // to fill them in so the symbol IDs continue to line up. Since they don't exist in the - // snapshot, the correct state to fill them in with is "undefined". - self.symbol_states.resize( - num_symbols, - SymbolState::undefined(self.scope_start_visibility), - ); - self.instance_attribute_states.resize( - num_attributes, - SymbolState::undefined(self.scope_start_visibility), - ); - } - - /// Merge the given snapshot into the current state, reflecting that we might have taken either - /// path to get here. The new state for each symbol should include definitions from both the - /// prior state and the snapshot. - pub(super) fn merge(&mut self, snapshot: FlowSnapshot) { - // As an optimization, if we know statically that either of the snapshots is always - // unreachable, we can leave it out of the merged result entirely. Note that we cannot - // perform any type inference at this point, so this is largely limited to unreachability - // via terminal statements. If a flow's reachability depends on an expression in the code, - // we will include the flow in the merged result; the visibility constraints of its - // bindings will include this reachability condition, so that later during type inference, - // we can determine whether any particular binding is non-visible due to unreachability. - if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE { - return; - } - if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE { - self.restore(snapshot); - return; - } - - // We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol - // IDs must line up), so the current number of known symbols must always be equal to or - // greater than the number of known symbols in a previously-taken snapshot. - debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len()); - debug_assert!( - self.instance_attribute_states.len() >= snapshot.instance_attribute_states.len() - ); - - let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter(); - for current in &mut self.symbol_states { - if let Some(snapshot) = snapshot_definitions_iter.next() { - current.merge( - snapshot, - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - } else { - current.merge( - SymbolState::undefined(snapshot.scope_start_visibility), - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - // Symbol not present in snapshot, so it's unbound/undeclared from that path. - } - } - let mut snapshot_definitions_iter = snapshot.instance_attribute_states.into_iter(); - for current in &mut self.instance_attribute_states { - if let Some(snapshot) = snapshot_definitions_iter.next() { - current.merge( - snapshot, - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - } else { - current.merge( - SymbolState::undefined(snapshot.scope_start_visibility), - &mut self.narrowing_constraints, - &mut self.visibility_constraints, - ); - } - } - - self.scope_start_visibility = self - .visibility_constraints - .add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility); - - self.reachability = self - .visibility_constraints - .add_or_constraint(self.reachability, snapshot.reachability); - } - - pub(super) fn finish(mut self) -> UseDefMap<'db> { - self.all_definitions.shrink_to_fit(); - self.symbol_states.shrink_to_fit(); - self.instance_attribute_states.shrink_to_fit(); - self.bindings_by_use.shrink_to_fit(); - self.node_reachability.shrink_to_fit(); - self.declarations_by_binding.shrink_to_fit(); - self.bindings_by_declaration.shrink_to_fit(); - self.eager_bindings.shrink_to_fit(); - - UseDefMap { - all_definitions: self.all_definitions, - predicates: self.predicates.build(), - narrowing_constraints: self.narrowing_constraints.build(), - visibility_constraints: self.visibility_constraints.build(), - bindings_by_use: self.bindings_by_use, - node_reachability: self.node_reachability, - public_symbols: self.symbol_states, - instance_attributes: self.instance_attribute_states, - declarations_by_binding: self.declarations_by_binding, - bindings_by_declaration: self.bindings_by_declaration, - eager_bindings: self.eager_bindings, - scope_start_visibility: self.scope_start_visibility, - } - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs deleted file mode 100644 index 09c1b42747d68..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs +++ /dev/null @@ -1,633 +0,0 @@ -//! Track live bindings per symbol, applicable constraints per binding, and live declarations. -//! -//! These data structures operate entirely on scope-local newtype-indices for definitions and -//! constraints, referring to their location in the `all_definitions` and `all_constraints` -//! indexvecs in [`super::UseDefMapBuilder`]. -//! -//! We need to track arbitrary associations between bindings and constraints, not just a single set -//! of currently dominating constraints (where "dominating" means "control flow must have passed -//! through it to reach this point"), because we can have dominating constraints that apply to some -//! bindings but not others, as in this code: -//! -//! ```python -//! x = 1 if flag else None -//! if x is not None: -//! if flag2: -//! x = 2 if flag else None -//! x -//! ``` -//! -//! The `x is not None` constraint dominates the final use of `x`, but it applies only to the first -//! binding of `x`, not the second, so `None` is a possible value for `x`. -//! -//! And we can't just track, for each binding, an index into a list of dominating constraints, -//! either, because we can have bindings which are still visible, but subject to constraints that -//! are no longer dominating, as in this code: -//! -//! ```python -//! x = 0 -//! if flag1: -//! x = 1 if flag2 else None -//! assert x is not None -//! x -//! ``` -//! -//! From the point of view of the final use of `x`, the `x is not None` constraint no longer -//! dominates, but it does dominate the `x = 1 if flag2 else None` binding, so we have to keep -//! track of that. -//! -//! The data structures use `IndexVec` arenas to store all data compactly and contiguously, while -//! supporting very cheap clones. -//! -//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very -//! similar to tracking live bindings. - -use itertools::{EitherOrBoth, Itertools}; -use ruff_index::newtype_index; -use smallvec::{smallvec, SmallVec}; - -use crate::semantic_index::narrowing_constraints::{ - NarrowingConstraintsBuilder, ScopedNarrowingConstraint, ScopedNarrowingConstraintPredicate, -}; -use crate::semantic_index::visibility_constraints::{ - ScopedVisibilityConstraintId, VisibilityConstraintsBuilder, -}; - -/// A newtype-index for a definition in a particular scope. -#[newtype_index] -#[derive(Ord, PartialOrd)] -pub(super) struct ScopedDefinitionId; - -impl ScopedDefinitionId { - /// A special ID that is used to describe an implicit start-of-scope state. When - /// we see that this definition is live, we know that the symbol is (possibly) - /// unbound or undeclared at a given usage site. - /// When creating a use-def-map builder, we always add an empty `None` definition - /// at index 0, so this ID is always present. - pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0); -} - -/// Can keep inline this many live bindings or declarations per symbol at a given time; more will -/// go to heap. -const INLINE_DEFINITIONS_PER_SYMBOL: usize = 4; - -/// Live declarations for a single symbol at some point in control flow, with their -/// corresponding visibility constraints. -#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)] -pub(super) struct SymbolDeclarations { - /// A list of live declarations for this symbol, sorted by their `ScopedDefinitionId` - live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_SYMBOL]>, -} - -/// One of the live declarations for a single symbol at some point in control flow. -#[derive(Clone, Debug, PartialEq, Eq)] -pub(super) struct LiveDeclaration { - pub(super) declaration: ScopedDefinitionId, - pub(super) visibility_constraint: ScopedVisibilityConstraintId, -} - -pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>; - -impl SymbolDeclarations { - fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self { - let initial_declaration = LiveDeclaration { - declaration: ScopedDefinitionId::UNBOUND, - visibility_constraint: scope_start_visibility, - }; - Self { - live_declarations: smallvec![initial_declaration], - } - } - - /// Record a newly-encountered declaration for this symbol. - fn record_declaration(&mut self, declaration: ScopedDefinitionId) { - // The new declaration replaces all previous live declaration in this path. - self.live_declarations.clear(); - self.live_declarations.push(LiveDeclaration { - declaration, - visibility_constraint: ScopedVisibilityConstraintId::ALWAYS_TRUE, - }); - } - - /// Add given visibility constraint to all live declarations. - pub(super) fn record_visibility_constraint( - &mut self, - visibility_constraints: &mut VisibilityConstraintsBuilder, - constraint: ScopedVisibilityConstraintId, - ) { - for declaration in &mut self.live_declarations { - declaration.visibility_constraint = visibility_constraints - .add_and_constraint(declaration.visibility_constraint, constraint); - } - } - - /// Return an iterator over live declarations for this symbol. - pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> { - self.live_declarations.iter() - } - - /// Iterate over the IDs of each currently live declaration for this symbol - fn iter_declarations(&self) -> impl Iterator + '_ { - self.iter().map(|lb| lb.declaration) - } - - fn simplify_visibility_constraints(&mut self, other: SymbolDeclarations) { - // If the set of live declarations hasn't changed, don't simplify. - if self.live_declarations.len() != other.live_declarations.len() - || !self.iter_declarations().eq(other.iter_declarations()) - { - return; - } - - for (declaration, other_declaration) in self - .live_declarations - .iter_mut() - .zip(other.live_declarations) - { - declaration.visibility_constraint = other_declaration.visibility_constraint; - } - } - - fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) { - let a = std::mem::take(self); - - // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that - // the merged `live_declarations` vec remains sorted. If a definition is found in both `a` - // and `b`, we compose the constraints from the two paths in an appropriate way - // (intersection for narrowing constraints; ternary OR for visibility constraints). If a - // definition is found in only one path, it is used as-is. - let a = a.live_declarations.into_iter(); - let b = b.live_declarations.into_iter(); - for zipped in a.merge_join_by(b, |a, b| a.declaration.cmp(&b.declaration)) { - match zipped { - EitherOrBoth::Both(a, b) => { - let visibility_constraint = visibility_constraints - .add_or_constraint(a.visibility_constraint, b.visibility_constraint); - self.live_declarations.push(LiveDeclaration { - declaration: a.declaration, - visibility_constraint, - }); - } - - EitherOrBoth::Left(declaration) | EitherOrBoth::Right(declaration) => { - self.live_declarations.push(declaration); - } - } - } - } -} - -/// Live bindings for a single symbol at some point in control flow. Each live binding comes -/// with a set of narrowing constraints and a visibility constraint. -#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)] -pub(super) struct SymbolBindings { - /// A list of live bindings for this symbol, sorted by their `ScopedDefinitionId` - live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_SYMBOL]>, -} - -/// One of the live bindings for a single symbol at some point in control flow. -#[derive(Clone, Debug, PartialEq, Eq)] -pub(super) struct LiveBinding { - pub(super) binding: ScopedDefinitionId, - pub(super) narrowing_constraint: ScopedNarrowingConstraint, - pub(super) visibility_constraint: ScopedVisibilityConstraintId, -} - -pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>; - -impl SymbolBindings { - fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self { - let initial_binding = LiveBinding { - binding: ScopedDefinitionId::UNBOUND, - narrowing_constraint: ScopedNarrowingConstraint::empty(), - visibility_constraint: scope_start_visibility, - }; - Self { - live_bindings: smallvec![initial_binding], - } - } - - /// Record a newly-encountered binding for this symbol. - pub(super) fn record_binding( - &mut self, - binding: ScopedDefinitionId, - visibility_constraint: ScopedVisibilityConstraintId, - ) { - // The new binding replaces all previous live bindings in this path, and has no - // constraints. - self.live_bindings.clear(); - self.live_bindings.push(LiveBinding { - binding, - narrowing_constraint: ScopedNarrowingConstraint::empty(), - visibility_constraint, - }); - } - - /// Add given constraint to all live bindings. - pub(super) fn record_narrowing_constraint( - &mut self, - narrowing_constraints: &mut NarrowingConstraintsBuilder, - predicate: ScopedNarrowingConstraintPredicate, - ) { - for binding in &mut self.live_bindings { - binding.narrowing_constraint = narrowing_constraints - .add_predicate_to_constraint(binding.narrowing_constraint, predicate); - } - } - - /// Add given visibility constraint to all live bindings. - pub(super) fn record_visibility_constraint( - &mut self, - visibility_constraints: &mut VisibilityConstraintsBuilder, - constraint: ScopedVisibilityConstraintId, - ) { - for binding in &mut self.live_bindings { - binding.visibility_constraint = visibility_constraints - .add_and_constraint(binding.visibility_constraint, constraint); - } - } - - /// Iterate over currently live bindings for this symbol - pub(super) fn iter(&self) -> LiveBindingsIterator<'_> { - self.live_bindings.iter() - } - - /// Iterate over the IDs of each currently live binding for this symbol - fn iter_bindings(&self) -> impl Iterator + '_ { - self.iter().map(|lb| lb.binding) - } - - fn simplify_visibility_constraints(&mut self, other: SymbolBindings) { - // If the set of live bindings hasn't changed, don't simplify. - if self.live_bindings.len() != other.live_bindings.len() - || !self.iter_bindings().eq(other.iter_bindings()) - { - return; - } - - for (binding, other_binding) in self.live_bindings.iter_mut().zip(other.live_bindings) { - binding.visibility_constraint = other_binding.visibility_constraint; - } - } - - fn merge( - &mut self, - b: Self, - narrowing_constraints: &mut NarrowingConstraintsBuilder, - visibility_constraints: &mut VisibilityConstraintsBuilder, - ) { - let a = std::mem::take(self); - - // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that - // the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and - // `b`, we compose the constraints from the two paths in an appropriate way (intersection - // for narrowing constraints; ternary OR for visibility constraints). If a definition is - // found in only one path, it is used as-is. - let a = a.live_bindings.into_iter(); - let b = b.live_bindings.into_iter(); - for zipped in a.merge_join_by(b, |a, b| a.binding.cmp(&b.binding)) { - match zipped { - EitherOrBoth::Both(a, b) => { - // If the same definition is visible through both paths, any constraint - // that applies on only one path is irrelevant to the resulting type from - // unioning the two paths, so we intersect the constraints. - let narrowing_constraint = narrowing_constraints - .intersect_constraints(a.narrowing_constraint, b.narrowing_constraint); - - // For visibility constraints, we merge them using a ternary OR operation: - let visibility_constraint = visibility_constraints - .add_or_constraint(a.visibility_constraint, b.visibility_constraint); - - self.live_bindings.push(LiveBinding { - binding: a.binding, - narrowing_constraint, - visibility_constraint, - }); - } - - EitherOrBoth::Left(binding) | EitherOrBoth::Right(binding) => { - self.live_bindings.push(binding); - } - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(super) struct SymbolState { - declarations: SymbolDeclarations, - bindings: SymbolBindings, -} - -impl SymbolState { - /// Return a new [`SymbolState`] representing an unbound, undeclared symbol. - pub(super) fn undefined(scope_start_visibility: ScopedVisibilityConstraintId) -> Self { - Self { - declarations: SymbolDeclarations::undeclared(scope_start_visibility), - bindings: SymbolBindings::unbound(scope_start_visibility), - } - } - - /// Record a newly-encountered binding for this symbol. - pub(super) fn record_binding( - &mut self, - binding_id: ScopedDefinitionId, - visibility_constraint: ScopedVisibilityConstraintId, - ) { - debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND); - self.bindings - .record_binding(binding_id, visibility_constraint); - } - - /// Add given constraint to all live bindings. - pub(super) fn record_narrowing_constraint( - &mut self, - narrowing_constraints: &mut NarrowingConstraintsBuilder, - constraint: ScopedNarrowingConstraintPredicate, - ) { - self.bindings - .record_narrowing_constraint(narrowing_constraints, constraint); - } - - /// Add given visibility constraint to all live bindings. - pub(super) fn record_visibility_constraint( - &mut self, - visibility_constraints: &mut VisibilityConstraintsBuilder, - constraint: ScopedVisibilityConstraintId, - ) { - self.bindings - .record_visibility_constraint(visibility_constraints, constraint); - self.declarations - .record_visibility_constraint(visibility_constraints, constraint); - } - - /// Simplifies this snapshot to have the same visibility constraints as a previous point in the - /// control flow, but only if the set of live bindings or declarations for this symbol hasn't - /// changed. - pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState) { - self.bindings - .simplify_visibility_constraints(snapshot_state.bindings); - self.declarations - .simplify_visibility_constraints(snapshot_state.declarations); - } - - /// Record a newly-encountered declaration of this symbol. - pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) { - self.declarations.record_declaration(declaration_id); - } - - /// Merge another [`SymbolState`] into this one. - pub(super) fn merge( - &mut self, - b: SymbolState, - narrowing_constraints: &mut NarrowingConstraintsBuilder, - visibility_constraints: &mut VisibilityConstraintsBuilder, - ) { - self.bindings - .merge(b.bindings, narrowing_constraints, visibility_constraints); - self.declarations - .merge(b.declarations, visibility_constraints); - } - - pub(super) fn bindings(&self) -> &SymbolBindings { - &self.bindings - } - - pub(super) fn declarations(&self) -> &SymbolDeclarations { - &self.declarations - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::semantic_index::predicate::ScopedPredicateId; - - #[track_caller] - fn assert_bindings( - narrowing_constraints: &NarrowingConstraintsBuilder, - symbol: &SymbolState, - expected: &[&str], - ) { - let actual = symbol - .bindings() - .iter() - .map(|live_binding| { - let def_id = live_binding.binding; - let def = if def_id == ScopedDefinitionId::UNBOUND { - "unbound".into() - } else { - def_id.as_u32().to_string() - }; - let predicates = narrowing_constraints - .iter_predicates(live_binding.narrowing_constraint) - .map(|idx| idx.as_u32().to_string()) - .collect::>() - .join(", "); - format!("{def}<{predicates}>") - }) - .collect::>(); - assert_eq!(actual, expected); - } - - #[track_caller] - pub(crate) fn assert_declarations(symbol: &SymbolState, expected: &[&str]) { - let actual = symbol - .declarations() - .iter() - .map( - |LiveDeclaration { - declaration, - visibility_constraint: _, - }| { - if *declaration == ScopedDefinitionId::UNBOUND { - "undeclared".into() - } else { - declaration.as_u32().to_string() - } - }, - ) - .collect::>(); - assert_eq!(actual, expected); - } - - #[test] - fn unbound() { - let narrowing_constraints = NarrowingConstraintsBuilder::default(); - let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - - assert_bindings(&narrowing_constraints, &sym, &["unbound<>"]); - } - - #[test] - fn with() { - let narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym.record_binding( - ScopedDefinitionId::from_u32(1), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - - assert_bindings(&narrowing_constraints, &sym, &["1<>"]); - } - - #[test] - fn record_constraint() { - let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym.record_binding( - ScopedDefinitionId::from_u32(1), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - let predicate = ScopedPredicateId::from_u32(0).into(); - sym.record_narrowing_constraint(&mut narrowing_constraints, predicate); - - assert_bindings(&narrowing_constraints, &sym, &["1<0>"]); - } - - #[test] - fn merge() { - let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut visibility_constraints = VisibilityConstraintsBuilder::default(); - - // merging the same definition with the same constraint keeps the constraint - let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym1a.record_binding( - ScopedDefinitionId::from_u32(1), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - let predicate = ScopedPredicateId::from_u32(0).into(); - sym1a.record_narrowing_constraint(&mut narrowing_constraints, predicate); - - let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym1b.record_binding( - ScopedDefinitionId::from_u32(1), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - let predicate = ScopedPredicateId::from_u32(0).into(); - sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); - - sym1a.merge( - sym1b, - &mut narrowing_constraints, - &mut visibility_constraints, - ); - let mut sym1 = sym1a; - assert_bindings(&narrowing_constraints, &sym1, &["1<0>"]); - - // merging the same definition with differing constraints drops all constraints - let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym2a.record_binding( - ScopedDefinitionId::from_u32(2), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - let predicate = ScopedPredicateId::from_u32(1).into(); - sym2a.record_narrowing_constraint(&mut narrowing_constraints, predicate); - - let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym1b.record_binding( - ScopedDefinitionId::from_u32(2), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - let predicate = ScopedPredicateId::from_u32(2).into(); - sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); - - sym2a.merge( - sym1b, - &mut narrowing_constraints, - &mut visibility_constraints, - ); - let sym2 = sym2a; - assert_bindings(&narrowing_constraints, &sym2, &["2<>"]); - - // merging a constrained definition with unbound keeps both - let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym3a.record_binding( - ScopedDefinitionId::from_u32(3), - ScopedVisibilityConstraintId::ALWAYS_TRUE, - ); - let predicate = ScopedPredicateId::from_u32(3).into(); - sym3a.record_narrowing_constraint(&mut narrowing_constraints, predicate); - - let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - - sym3a.merge( - sym2b, - &mut narrowing_constraints, - &mut visibility_constraints, - ); - let sym3 = sym3a; - assert_bindings(&narrowing_constraints, &sym3, &["unbound<>", "3<3>"]); - - // merging different definitions keeps them each with their existing constraints - sym1.merge( - sym3, - &mut narrowing_constraints, - &mut visibility_constraints, - ); - let sym = sym1; - assert_bindings(&narrowing_constraints, &sym, &["unbound<>", "1<0>", "3<3>"]); - } - - #[test] - fn no_declaration() { - let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - - assert_declarations(&sym, &["undeclared"]); - } - - #[test] - fn record_declaration() { - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym.record_declaration(ScopedDefinitionId::from_u32(1)); - - assert_declarations(&sym, &["1"]); - } - - #[test] - fn record_declaration_override() { - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym.record_declaration(ScopedDefinitionId::from_u32(1)); - sym.record_declaration(ScopedDefinitionId::from_u32(2)); - - assert_declarations(&sym, &["2"]); - } - - #[test] - fn record_declaration_merge() { - let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut visibility_constraints = VisibilityConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym.record_declaration(ScopedDefinitionId::from_u32(1)); - - let mut sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym2.record_declaration(ScopedDefinitionId::from_u32(2)); - - sym.merge( - sym2, - &mut narrowing_constraints, - &mut visibility_constraints, - ); - - assert_declarations(&sym, &["1", "2"]); - } - - #[test] - fn record_declaration_merge_partial_undeclared() { - let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); - let mut visibility_constraints = VisibilityConstraintsBuilder::default(); - let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - sym.record_declaration(ScopedDefinitionId::from_u32(1)); - - let sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE); - - sym.merge( - sym2, - &mut narrowing_constraints, - &mut visibility_constraints, - ); - - assert_declarations(&sym, &["undeclared", "1"]); - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_index/visibility_constraints.rs b/crates/red_knot_python_semantic/src/semantic_index/visibility_constraints.rs deleted file mode 100644 index 84337680ab4be..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_index/visibility_constraints.rs +++ /dev/null @@ -1,670 +0,0 @@ -//! # Visibility constraints -//! -//! During semantic index building, we collect visibility constraints for each binding and -//! declaration. These constraints are then used during type-checking to determine the static -//! visibility of a certain definition. This allows us to re-analyze control flow during type -//! checking, potentially "hiding" some branches that we can statically determine to never be -//! taken. Consider the following example first. We added implicit "unbound" definitions at the -//! start of the scope. Note how visibility constraints can apply to bindings outside of the -//! if-statement: -//! ```py -//! x = # not a live binding for the use of x below, shadowed by `x = 1` -//! y = # visibility constraint: ~test -//! -//! x = 1 # visibility constraint: ~test -//! if test: -//! x = 2 # visibility constraint: test -//! -//! y = 2 # visibility constraint: test -//! -//! use(x) -//! use(y) -//! ``` -//! The static truthiness of the `test` condition can either be always-false, ambiguous, or -//! always-true. Similarly, we have the same three options when evaluating a visibility constraint. -//! This outcome determines the visibility of a definition: always-true means that the definition -//! is definitely visible for a given use, always-false means that the definition is definitely -//! not visible, and ambiguous means that we might see this definition or not. In the latter case, -//! we need to consider both options during type inference and boundness analysis. For the example -//! above, these are the possible type inference / boundness results for the uses of `x` and `y`: -//! -//! ```text -//! | `test` truthiness | `~test` truthiness | type of `x` | boundness of `y` | -//! |-------------------|--------------------|-----------------|------------------| -//! | always false | always true | `Literal[1]` | unbound | -//! | ambiguous | ambiguous | `Literal[1, 2]` | possibly unbound | -//! | always true | always false | `Literal[2]` | bound | -//! ``` -//! -//! ### Sequential constraints (ternary AND) -//! -//! As we have seen above, visibility constraints can apply outside of a control flow element. -//! So we need to consider the possibility that multiple constraints apply to the same binding. -//! Here, we consider what happens if multiple `if`-statements lead to a sequence of constraints. -//! Consider the following example: -//! ```py -//! x = 0 -//! -//! if test1: -//! x = 1 -//! -//! if test2: -//! x = 2 -//! ``` -//! The binding `x = 2` is easy to analyze. Its visibility corresponds to the truthiness of `test2`. -//! For the `x = 1` binding, things are a bit more interesting. It is always visible if `test1` is -//! always-true *and* `test2` is always-false. It is never visible if `test1` is always-false *or* -//! `test2` is always-true. And it is ambiguous otherwise. This corresponds to a ternary *test1 AND -//! ~test2* operation in three-valued Kleene logic [Kleene]: -//! -//! ```text -//! | AND | always-false | ambiguous | always-true | -//! |--------------|--------------|--------------|--------------| -//! | always false | always-false | always-false | always-false | -//! | ambiguous | always-false | ambiguous | ambiguous | -//! | always true | always-false | ambiguous | always-true | -//! ``` -//! -//! The `x = 0` binding can be handled similarly, with the difference that both `test1` and `test2` -//! are negated: -//! ```py -//! x = 0 # ~test1 AND ~test2 -//! -//! if test1: -//! x = 1 # test1 AND ~test2 -//! -//! if test2: -//! x = 2 # test2 -//! ``` -//! -//! ### Merged constraints (ternary OR) -//! -//! Finally, we consider what happens in "parallel" control flow. Consider the following example -//! where we have omitted the test condition for the outer `if` for clarity: -//! ```py -//! x = 0 -//! -//! if <…>: -//! if test1: -//! x = 1 -//! else: -//! if test2: -//! x = 2 -//! -//! use(x) -//! ``` -//! At the usage of `x`, i.e. after control flow has been merged again, the visibility of the `x = -//! 0` binding behaves as follows: the binding is always visible if `test1` is always-false *or* -//! `test2` is always-false; and it is never visible if `test1` is always-true *and* `test2` is -//! always-true. This corresponds to a ternary *OR* operation in Kleene logic: -//! -//! ```text -//! | OR | always-false | ambiguous | always-true | -//! |--------------|--------------|--------------|--------------| -//! | always false | always-false | ambiguous | always-true | -//! | ambiguous | ambiguous | ambiguous | always-true | -//! | always true | always-true | always-true | always-true | -//! ``` -//! -//! Using this, we can annotate the visibility constraints for the example above: -//! ```py -//! x = 0 # ~test1 OR ~test2 -//! -//! if <…>: -//! if test1: -//! x = 1 # test1 -//! else: -//! if test2: -//! x = 2 # test2 -//! -//! use(x) -//! ``` -//! -//! ### Explicit ambiguity -//! -//! In some cases, we explicitly add an “ambiguous” constraint to all bindings -//! in a certain control flow path. We do this when branching on something that we can not (or -//! intentionally do not want to) analyze statically. `for` loops are one example: -//! ```py -//! x = -//! -//! for _ in range(2): -//! x = 1 -//! ``` -//! Here, we report an ambiguous visibility constraint before branching off. If we don't do this, -//! the `x = ` binding would be considered unconditionally visible in the no-loop case. -//! And since the other branch does not have the live `x = ` binding, we would incorrectly -//! create a state where the `x = ` binding is always visible. -//! -//! -//! ### Representing formulas -//! -//! Given everything above, we can represent a visibility constraint as a _ternary formula_. This -//! is like a boolean formula (which maps several true/false variables to a single true/false -//! result), but which allows the third "ambiguous" value in addition to "true" and "false". -//! -//! [_Binary decision diagrams_][bdd] (BDDs) are a common way to represent boolean formulas when -//! doing program analysis. We extend this to a _ternary decision diagram_ (TDD) to support -//! ambiguous values. -//! -//! A TDD is a graph, and a ternary formula is represented by a node in this graph. There are three -//! possible leaf nodes representing the "true", "false", and "ambiguous" constant functions. -//! Interior nodes consist of a ternary variable to evaluate, and outgoing edges for whether the -//! variable evaluates to true, false, or ambiguous. -//! -//! Our TDDs are _reduced_ and _ordered_ (as is typical for BDDs). -//! -//! An ordered TDD means that variables appear in the same order in all paths within the graph. -//! -//! A reduced TDD means two things: First, we intern the graph nodes, so that we only keep a single -//! copy of interior nodes with the same contents. Second, we eliminate any nodes that are "noops", -//! where the "true" and "false" outgoing edges lead to the same node. (This implies that it -//! doesn't matter what value that variable has when evaluating the formula, and we can leave it -//! out of the evaluation chain completely.) -//! -//! Reduced and ordered decision diagrams are _normal forms_, which means that two equivalent -//! formulas (which have the same outputs for every combination of inputs) are represented by -//! exactly the same graph node. (Because of interning, this is not _equal_ nodes, but _identical_ -//! ones.) That means that we can compare formulas for equivalence in constant time, and in -//! particular, can check whether a visibility constraint is statically always true or false, -//! regardless of any Python program state, by seeing if the constraint's formula is the "true" or -//! "false" leaf node. -//! -//! [Kleene]: -//! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram - -use std::cmp::Ordering; - -use ruff_index::{Idx, IndexVec}; -use rustc_hash::FxHashMap; - -use crate::semantic_index::expression::Expression; -use crate::semantic_index::predicate::{ - PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId, -}; -use crate::semantic_index::symbol_table; -use crate::symbol::imported_symbol; -use crate::types::{infer_expression_type, Truthiness, Type}; -use crate::Db; - -/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula -/// is just like a boolean formula, but with `Ambiguous` as a third potential result. See the -/// module documentation for more details.) -/// -/// The primitive atoms of the formula are [`Predicate`]s, which express some property of the -/// runtime state of the code that we are analyzing. -/// -/// We assume that each atom has a stable value each time that the formula is evaluated. An atom -/// that resolves to `Ambiguous` might be true or false, and we can't tell which — but within that -/// evaluation, we assume that the atom has the _same_ unknown value each time it appears. That -/// allows us to perform simplifications like `A ∨ !A → true` and `A ∧ !A → false`. -/// -/// That means that when you are constructing a formula, you might need to create distinct atoms -/// for a particular [`Predicate`], if your formula needs to consider how a particular runtime -/// property might be different at different points in the execution of the program. -/// -/// Visibility constraints are normalized, so equivalent constraints are guaranteed to have equal -/// IDs. -#[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub(crate) struct ScopedVisibilityConstraintId(u32); - -impl std::fmt::Debug for ScopedVisibilityConstraintId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut f = f.debug_tuple("ScopedVisibilityConstraintId"); - match *self { - // We use format_args instead of rendering the strings directly so that we don't get - // any quotes in the output: ScopedVisibilityConstraintId(AlwaysTrue) instead of - // ScopedVisibilityConstraintId("AlwaysTrue"). - ALWAYS_TRUE => f.field(&format_args!("AlwaysTrue")), - AMBIGUOUS => f.field(&format_args!("Ambiguous")), - ALWAYS_FALSE => f.field(&format_args!("AlwaysFalse")), - _ => f.field(&self.0), - }; - f.finish() - } -} - -// Internal details: -// -// There are 3 terminals, with hard-coded constraint IDs: true, ambiguous, and false. -// -// _Atoms_ are the underlying Predicates, which are the variables that are evaluated by the -// ternary function. -// -// _Interior nodes_ provide the TDD structure for the formula. Interior nodes are stored in an -// arena Vec, with the constraint ID providing an index into the arena. - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct InteriorNode { - /// A "variable" that is evaluated as part of a TDD ternary function. For visibility - /// constraints, this is a `Predicate` that represents some runtime property of the Python - /// code that we are evaluating. - atom: ScopedPredicateId, - if_true: ScopedVisibilityConstraintId, - if_ambiguous: ScopedVisibilityConstraintId, - if_false: ScopedVisibilityConstraintId, -} - -impl ScopedVisibilityConstraintId { - /// A special ID that is used for an "always true" / "always visible" constraint. - pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId = - ScopedVisibilityConstraintId(0xffff_ffff); - - /// A special ID that is used for an ambiguous constraint. - pub(crate) const AMBIGUOUS: ScopedVisibilityConstraintId = - ScopedVisibilityConstraintId(0xffff_fffe); - - /// A special ID that is used for an "always false" / "never visible" constraint. - pub(crate) const ALWAYS_FALSE: ScopedVisibilityConstraintId = - ScopedVisibilityConstraintId(0xffff_fffd); - - fn is_terminal(self) -> bool { - self.0 >= SMALLEST_TERMINAL.0 - } -} - -impl Idx for ScopedVisibilityConstraintId { - #[inline] - fn new(value: usize) -> Self { - assert!(value <= (SMALLEST_TERMINAL.0 as usize)); - #[allow(clippy::cast_possible_truncation)] - Self(value as u32) - } - - #[inline] - fn index(self) -> usize { - debug_assert!(!self.is_terminal()); - self.0 as usize - } -} - -// Rebind some constants locally so that we don't need as many qualifiers below. -const ALWAYS_TRUE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_TRUE; -const AMBIGUOUS: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::AMBIGUOUS; -const ALWAYS_FALSE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_FALSE; -const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE; - -/// A collection of visibility constraints for a given scope. -#[derive(Debug, PartialEq, Eq, salsa::Update)] -pub(crate) struct VisibilityConstraints { - interiors: IndexVec, -} - -#[derive(Debug, Default, PartialEq, Eq)] -pub(crate) struct VisibilityConstraintsBuilder { - interiors: IndexVec, - interior_cache: FxHashMap, - not_cache: FxHashMap, - and_cache: FxHashMap< - (ScopedVisibilityConstraintId, ScopedVisibilityConstraintId), - ScopedVisibilityConstraintId, - >, - or_cache: FxHashMap< - (ScopedVisibilityConstraintId, ScopedVisibilityConstraintId), - ScopedVisibilityConstraintId, - >, -} - -impl VisibilityConstraintsBuilder { - pub(crate) fn build(self) -> VisibilityConstraints { - VisibilityConstraints { - interiors: self.interiors, - } - } - - /// Returns whether `a` or `b` has a "larger" atom. TDDs are ordered such that interior nodes - /// can only have edges to "larger" nodes. Terminals are considered to have a larger atom than - /// any internal node, since they are leaf nodes. - fn cmp_atoms( - &self, - a: ScopedVisibilityConstraintId, - b: ScopedVisibilityConstraintId, - ) -> Ordering { - if a == b || (a.is_terminal() && b.is_terminal()) { - Ordering::Equal - } else if a.is_terminal() { - Ordering::Greater - } else if b.is_terminal() { - Ordering::Less - } else { - self.interiors[a].atom.cmp(&self.interiors[b].atom) - } - } - - /// Adds an interior node, ensuring that we always use the same visibility constraint ID for - /// equal nodes. - fn add_interior(&mut self, node: InteriorNode) -> ScopedVisibilityConstraintId { - // If the true and false branches lead to the same node, we can override the ambiguous - // branch to go there too. And this node is then redundant and can be reduced. - if node.if_true == node.if_false { - return node.if_true; - } - - *self - .interior_cache - .entry(node) - .or_insert_with(|| self.interiors.push(node)) - } - - /// Adds a new visibility constraint that checks a single [`Predicate`]. - /// - /// [`ScopedPredicateId`]s are the “variables” that are evaluated by a TDD. A TDD variable has - /// the same value no matter how many times it appears in the ternary formula that the TDD - /// represents. - /// - /// However, we sometimes have to model how a `Predicate` can have a different runtime - /// value at different points in the execution of the program. To handle this, you can take - /// advantage of the fact that the [`Predicates`] arena does not deduplicate `Predicate`s. - /// You can add a `Predicate` multiple times, yielding different `ScopedPredicateId`s, which - /// you can then create separate TDD atoms for. - pub(crate) fn add_atom( - &mut self, - predicate: ScopedPredicateId, - ) -> ScopedVisibilityConstraintId { - self.add_interior(InteriorNode { - atom: predicate, - if_true: ALWAYS_TRUE, - if_ambiguous: AMBIGUOUS, - if_false: ALWAYS_FALSE, - }) - } - - /// Adds a new visibility constraint that is the ternary NOT of an existing one. - pub(crate) fn add_not_constraint( - &mut self, - a: ScopedVisibilityConstraintId, - ) -> ScopedVisibilityConstraintId { - if a == ALWAYS_TRUE { - return ALWAYS_FALSE; - } else if a == AMBIGUOUS { - return AMBIGUOUS; - } else if a == ALWAYS_FALSE { - return ALWAYS_TRUE; - } - - if let Some(cached) = self.not_cache.get(&a) { - return *cached; - } - let a_node = self.interiors[a]; - let if_true = self.add_not_constraint(a_node.if_true); - let if_ambiguous = self.add_not_constraint(a_node.if_ambiguous); - let if_false = self.add_not_constraint(a_node.if_false); - let result = self.add_interior(InteriorNode { - atom: a_node.atom, - if_true, - if_ambiguous, - if_false, - }); - self.not_cache.insert(a, result); - result - } - - /// Adds a new visibility constraint that is the ternary OR of two existing ones. - pub(crate) fn add_or_constraint( - &mut self, - a: ScopedVisibilityConstraintId, - b: ScopedVisibilityConstraintId, - ) -> ScopedVisibilityConstraintId { - match (a, b) { - (ALWAYS_TRUE, _) | (_, ALWAYS_TRUE) => return ALWAYS_TRUE, - (ALWAYS_FALSE, other) | (other, ALWAYS_FALSE) => return other, - (AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS, - _ => {} - } - - // OR is commutative, which lets us halve the cache requirements - let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) }; - if let Some(cached) = self.or_cache.get(&(a, b)) { - return *cached; - } - - let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) { - Ordering::Equal => { - let a_node = self.interiors[a]; - let b_node = self.interiors[b]; - let if_true = self.add_or_constraint(a_node.if_true, b_node.if_true); - let if_false = self.add_or_constraint(a_node.if_false, b_node.if_false); - let if_ambiguous = if if_true == if_false { - if_true - } else { - self.add_or_constraint(a_node.if_ambiguous, b_node.if_ambiguous) - }; - (a_node.atom, if_true, if_ambiguous, if_false) - } - Ordering::Less => { - let a_node = self.interiors[a]; - let if_true = self.add_or_constraint(a_node.if_true, b); - let if_false = self.add_or_constraint(a_node.if_false, b); - let if_ambiguous = if if_true == if_false { - if_true - } else { - self.add_or_constraint(a_node.if_ambiguous, b) - }; - (a_node.atom, if_true, if_ambiguous, if_false) - } - Ordering::Greater => { - let b_node = self.interiors[b]; - let if_true = self.add_or_constraint(a, b_node.if_true); - let if_false = self.add_or_constraint(a, b_node.if_false); - let if_ambiguous = if if_true == if_false { - if_true - } else { - self.add_or_constraint(a, b_node.if_ambiguous) - }; - (b_node.atom, if_true, if_ambiguous, if_false) - } - }; - - let result = self.add_interior(InteriorNode { - atom, - if_true, - if_ambiguous, - if_false, - }); - self.or_cache.insert((a, b), result); - result - } - - /// Adds a new visibility constraint that is the ternary AND of two existing ones. - pub(crate) fn add_and_constraint( - &mut self, - a: ScopedVisibilityConstraintId, - b: ScopedVisibilityConstraintId, - ) -> ScopedVisibilityConstraintId { - match (a, b) { - (ALWAYS_FALSE, _) | (_, ALWAYS_FALSE) => return ALWAYS_FALSE, - (ALWAYS_TRUE, other) | (other, ALWAYS_TRUE) => return other, - (AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS, - _ => {} - } - - // AND is commutative, which lets us halve the cache requirements - let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) }; - if let Some(cached) = self.and_cache.get(&(a, b)) { - return *cached; - } - - let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) { - Ordering::Equal => { - let a_node = self.interiors[a]; - let b_node = self.interiors[b]; - let if_true = self.add_and_constraint(a_node.if_true, b_node.if_true); - let if_false = self.add_and_constraint(a_node.if_false, b_node.if_false); - let if_ambiguous = if if_true == if_false { - if_true - } else { - self.add_and_constraint(a_node.if_ambiguous, b_node.if_ambiguous) - }; - (a_node.atom, if_true, if_ambiguous, if_false) - } - Ordering::Less => { - let a_node = self.interiors[a]; - let if_true = self.add_and_constraint(a_node.if_true, b); - let if_false = self.add_and_constraint(a_node.if_false, b); - let if_ambiguous = if if_true == if_false { - if_true - } else { - self.add_and_constraint(a_node.if_ambiguous, b) - }; - (a_node.atom, if_true, if_ambiguous, if_false) - } - Ordering::Greater => { - let b_node = self.interiors[b]; - let if_true = self.add_and_constraint(a, b_node.if_true); - let if_false = self.add_and_constraint(a, b_node.if_false); - let if_ambiguous = if if_true == if_false { - if_true - } else { - self.add_and_constraint(a, b_node.if_ambiguous) - }; - (b_node.atom, if_true, if_ambiguous, if_false) - } - }; - - let result = self.add_interior(InteriorNode { - atom, - if_true, - if_ambiguous, - if_false, - }); - self.and_cache.insert((a, b), result); - result - } -} - -impl VisibilityConstraints { - /// Analyze the statically known visibility for a given visibility constraint. - pub(crate) fn evaluate<'db>( - &self, - db: &'db dyn Db, - predicates: &Predicates<'db>, - mut id: ScopedVisibilityConstraintId, - ) -> Truthiness { - loop { - let node = match id { - ALWAYS_TRUE => return Truthiness::AlwaysTrue, - AMBIGUOUS => return Truthiness::Ambiguous, - ALWAYS_FALSE => return Truthiness::AlwaysFalse, - _ => self.interiors[id], - }; - let predicate = &predicates[node.atom]; - match Self::analyze_single(db, predicate) { - Truthiness::AlwaysTrue => id = node.if_true, - Truthiness::Ambiguous => id = node.if_ambiguous, - Truthiness::AlwaysFalse => id = node.if_false, - } - } - } - - fn analyze_single_pattern_predicate_kind<'db>( - db: &'db dyn Db, - predicate_kind: &PatternPredicateKind<'db>, - subject: Expression<'db>, - ) -> Truthiness { - match predicate_kind { - PatternPredicateKind::Value(value) => { - let subject_ty = infer_expression_type(db, subject); - let value_ty = infer_expression_type(db, *value); - - if subject_ty.is_single_valued(db) { - Truthiness::from(subject_ty.is_equivalent_to(db, value_ty)) - } else { - Truthiness::Ambiguous - } - } - PatternPredicateKind::Singleton(singleton) => { - let subject_ty = infer_expression_type(db, subject); - - let singleton_ty = match singleton { - ruff_python_ast::Singleton::None => Type::none(db), - ruff_python_ast::Singleton::True => Type::BooleanLiteral(true), - ruff_python_ast::Singleton::False => Type::BooleanLiteral(false), - }; - - debug_assert!(singleton_ty.is_singleton(db)); - - if subject_ty.is_equivalent_to(db, singleton_ty) { - Truthiness::AlwaysTrue - } else if subject_ty.is_disjoint_from(db, singleton_ty) { - Truthiness::AlwaysFalse - } else { - Truthiness::Ambiguous - } - } - PatternPredicateKind::Or(predicates) => { - use std::ops::ControlFlow; - let (ControlFlow::Break(truthiness) | ControlFlow::Continue(truthiness)) = - predicates - .iter() - .map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject)) - // this is just a "max", but with a slight optimization: `AlwaysTrue` is the "greatest" possible element, so we short-circuit if we get there - .try_fold(Truthiness::AlwaysFalse, |acc, next| match (acc, next) { - (Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => { - ControlFlow::Break(Truthiness::AlwaysTrue) - } - (Truthiness::Ambiguous, _) | (_, Truthiness::Ambiguous) => { - ControlFlow::Continue(Truthiness::Ambiguous) - } - (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => { - ControlFlow::Continue(Truthiness::AlwaysFalse) - } - }); - truthiness - } - PatternPredicateKind::Class(class_expr) => { - let subject_ty = infer_expression_type(db, subject); - let class_ty = infer_expression_type(db, *class_expr).to_instance(db); - - class_ty.map_or(Truthiness::Ambiguous, |class_ty| { - if subject_ty.is_subtype_of(db, class_ty) { - Truthiness::AlwaysTrue - } else if subject_ty.is_disjoint_from(db, class_ty) { - Truthiness::AlwaysFalse - } else { - Truthiness::Ambiguous - } - }) - } - PatternPredicateKind::Unsupported => Truthiness::Ambiguous, - } - } - - fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness { - let truthiness = Self::analyze_single_pattern_predicate_kind( - db, - predicate.kind(db), - predicate.subject(db), - ); - - if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() { - // Fall back to ambiguous, the guard might change the result. - // TODO: actually analyze guard truthiness - Truthiness::Ambiguous - } else { - truthiness - } - } - - fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { - match predicate.node { - PredicateNode::Expression(test_expr) => { - let ty = infer_expression_type(db, test_expr); - ty.bool(db).negate_if(!predicate.is_positive) - } - PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner), - PredicateNode::StarImportPlaceholder(star_import) => { - let symbol_table = symbol_table(db, star_import.scope(db)); - let symbol_name = symbol_table.symbol(star_import.symbol_id(db)).name(); - match imported_symbol(db, star_import.referenced_file(db), symbol_name).symbol { - crate::symbol::Symbol::Type(_, crate::symbol::Boundness::Bound) => { - Truthiness::AlwaysTrue - } - crate::symbol::Symbol::Type(_, crate::symbol::Boundness::PossiblyUnbound) => { - Truthiness::Ambiguous - } - crate::symbol::Symbol::Unbound => Truthiness::AlwaysFalse, - } - } - } - } -} diff --git a/crates/red_knot_python_semantic/src/semantic_model.rs b/crates/red_knot_python_semantic/src/semantic_model.rs deleted file mode 100644 index 01928c3ba5af8..0000000000000 --- a/crates/red_knot_python_semantic/src/semantic_model.rs +++ /dev/null @@ -1,241 +0,0 @@ -use ruff_db::files::{File, FilePath}; -use ruff_db::source::line_index; -use ruff_python_ast as ast; -use ruff_python_ast::{Expr, ExprRef}; -use ruff_source_file::LineIndex; - -use crate::module_name::ModuleName; -use crate::module_resolver::{resolve_module, Module}; -use crate::semantic_index::ast_ids::HasScopedExpressionId; -use crate::semantic_index::semantic_index; -use crate::types::{binding_type, infer_scope_types, Type}; -use crate::Db; - -pub struct SemanticModel<'db> { - db: &'db dyn Db, - file: File, -} - -impl<'db> SemanticModel<'db> { - pub fn new(db: &'db dyn Db, file: File) -> Self { - Self { db, file } - } - - // TODO we don't actually want to expose the Db directly to lint rules, but we need to find a - // solution for exposing information from types - pub fn db(&self) -> &dyn Db { - self.db - } - - pub fn file_path(&self) -> &FilePath { - self.file.path(self.db) - } - - pub fn line_index(&self) -> LineIndex { - line_index(self.db.upcast(), self.file) - } - - pub fn resolve_module(&self, module_name: &ModuleName) -> Option { - resolve_module(self.db, module_name) - } -} - -pub trait HasType { - /// Returns the inferred type of `self`. - /// - /// ## Panics - /// May panic if `self` is from another file than `model`. - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>; -} - -impl HasType for ast::ExprRef<'_> { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { - let index = semantic_index(model.db, model.file); - let file_scope = index.expression_scope_id(*self); - let scope = file_scope.to_scope_id(model.db, model.file); - - let expression_id = self.scoped_expression_id(model.db, scope); - infer_scope_types(model.db, scope).expression_type(expression_id) - } -} - -macro_rules! impl_expression_has_type { - ($ty: ty) => { - impl HasType for $ty { - #[inline] - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { - let expression_ref = ExprRef::from(self); - expression_ref.inferred_type(model) - } - } - }; -} - -impl_expression_has_type!(ast::ExprBoolOp); -impl_expression_has_type!(ast::ExprNamed); -impl_expression_has_type!(ast::ExprBinOp); -impl_expression_has_type!(ast::ExprUnaryOp); -impl_expression_has_type!(ast::ExprLambda); -impl_expression_has_type!(ast::ExprIf); -impl_expression_has_type!(ast::ExprDict); -impl_expression_has_type!(ast::ExprSet); -impl_expression_has_type!(ast::ExprListComp); -impl_expression_has_type!(ast::ExprSetComp); -impl_expression_has_type!(ast::ExprDictComp); -impl_expression_has_type!(ast::ExprGenerator); -impl_expression_has_type!(ast::ExprAwait); -impl_expression_has_type!(ast::ExprYield); -impl_expression_has_type!(ast::ExprYieldFrom); -impl_expression_has_type!(ast::ExprCompare); -impl_expression_has_type!(ast::ExprCall); -impl_expression_has_type!(ast::ExprFString); -impl_expression_has_type!(ast::ExprStringLiteral); -impl_expression_has_type!(ast::ExprBytesLiteral); -impl_expression_has_type!(ast::ExprNumberLiteral); -impl_expression_has_type!(ast::ExprBooleanLiteral); -impl_expression_has_type!(ast::ExprNoneLiteral); -impl_expression_has_type!(ast::ExprEllipsisLiteral); -impl_expression_has_type!(ast::ExprAttribute); -impl_expression_has_type!(ast::ExprSubscript); -impl_expression_has_type!(ast::ExprStarred); -impl_expression_has_type!(ast::ExprName); -impl_expression_has_type!(ast::ExprList); -impl_expression_has_type!(ast::ExprTuple); -impl_expression_has_type!(ast::ExprSlice); -impl_expression_has_type!(ast::ExprIpyEscapeCommand); - -impl HasType for ast::Expr { - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { - match self { - Expr::BoolOp(inner) => inner.inferred_type(model), - Expr::Named(inner) => inner.inferred_type(model), - Expr::BinOp(inner) => inner.inferred_type(model), - Expr::UnaryOp(inner) => inner.inferred_type(model), - Expr::Lambda(inner) => inner.inferred_type(model), - Expr::If(inner) => inner.inferred_type(model), - Expr::Dict(inner) => inner.inferred_type(model), - Expr::Set(inner) => inner.inferred_type(model), - Expr::ListComp(inner) => inner.inferred_type(model), - Expr::SetComp(inner) => inner.inferred_type(model), - Expr::DictComp(inner) => inner.inferred_type(model), - Expr::Generator(inner) => inner.inferred_type(model), - Expr::Await(inner) => inner.inferred_type(model), - Expr::Yield(inner) => inner.inferred_type(model), - Expr::YieldFrom(inner) => inner.inferred_type(model), - Expr::Compare(inner) => inner.inferred_type(model), - Expr::Call(inner) => inner.inferred_type(model), - Expr::FString(inner) => inner.inferred_type(model), - Expr::StringLiteral(inner) => inner.inferred_type(model), - Expr::BytesLiteral(inner) => inner.inferred_type(model), - Expr::NumberLiteral(inner) => inner.inferred_type(model), - Expr::BooleanLiteral(inner) => inner.inferred_type(model), - Expr::NoneLiteral(inner) => inner.inferred_type(model), - Expr::EllipsisLiteral(inner) => inner.inferred_type(model), - Expr::Attribute(inner) => inner.inferred_type(model), - Expr::Subscript(inner) => inner.inferred_type(model), - Expr::Starred(inner) => inner.inferred_type(model), - Expr::Name(inner) => inner.inferred_type(model), - Expr::List(inner) => inner.inferred_type(model), - Expr::Tuple(inner) => inner.inferred_type(model), - Expr::Slice(inner) => inner.inferred_type(model), - Expr::IpyEscapeCommand(inner) => inner.inferred_type(model), - } - } -} - -macro_rules! impl_binding_has_ty { - ($ty: ty) => { - impl HasType for $ty { - #[inline] - fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { - let index = semantic_index(model.db, model.file); - let binding = index.expect_single_definition(self); - binding_type(model.db, binding) - } - } - }; -} - -impl_binding_has_ty!(ast::StmtFunctionDef); -impl_binding_has_ty!(ast::StmtClassDef); -impl_binding_has_ty!(ast::Parameter); -impl_binding_has_ty!(ast::ParameterWithDefault); -impl_binding_has_ty!(ast::ExceptHandlerExceptHandler); - -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; - use ruff_db::parsed::parsed_module; - - use crate::db::tests::TestDbBuilder; - use crate::{HasType, SemanticModel}; - - #[test] - fn function_type() -> anyhow::Result<()> { - let db = TestDbBuilder::new() - .with_file("/src/foo.py", "def test(): pass") - .build()?; - - let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - - let ast = parsed_module(&db, foo); - - let function = ast.suite()[0].as_function_def_stmt().unwrap(); - let model = SemanticModel::new(&db, foo); - let ty = function.inferred_type(&model); - - assert!(ty.is_function_literal()); - - Ok(()) - } - - #[test] - fn class_type() -> anyhow::Result<()> { - let db = TestDbBuilder::new() - .with_file("/src/foo.py", "class Test: pass") - .build()?; - - let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - - let ast = parsed_module(&db, foo); - - let class = ast.suite()[0].as_class_def_stmt().unwrap(); - let model = SemanticModel::new(&db, foo); - let ty = class.inferred_type(&model); - - assert!(ty.is_class_literal()); - - Ok(()) - } - - #[test] - fn alias_type() -> anyhow::Result<()> { - let db = TestDbBuilder::new() - .with_file("/src/foo.py", "class Test: pass") - .with_file("/src/bar.py", "from foo import Test") - .build()?; - - let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); - - let ast = parsed_module(&db, bar); - - let import = ast.suite()[0].as_import_from_stmt().unwrap(); - let alias = &import.names[0]; - let model = SemanticModel::new(&db, bar); - let ty = alias.inferred_type(&model); - - assert!(ty.is_class_literal()); - - Ok(()) - } -} diff --git a/crates/red_knot_python_semantic/src/site_packages.rs b/crates/red_knot_python_semantic/src/site_packages.rs deleted file mode 100644 index c4b76d877e4dd..0000000000000 --- a/crates/red_knot_python_semantic/src/site_packages.rs +++ /dev/null @@ -1,916 +0,0 @@ -//! Utilities for finding the `site-packages` directory, -//! into which third-party packages are installed. -//! -//! The routines exposed by this module have different behaviour depending -//! on the platform of the *host machine*, which may be -//! different from the *target platform for type checking*. (A user -//! might be running red-knot on a Windows machine, but might -//! reasonably ask us to type-check code assuming that the code runs -//! on Linux.) - -use std::fmt; -use std::fmt::Display; -use std::io; -use std::num::NonZeroUsize; -use std::ops::Deref; - -use ruff_db::system::{System, SystemPath, SystemPathBuf}; -use ruff_python_ast::PythonVersion; - -type SitePackagesDiscoveryResult = Result; - -/// Abstraction for a Python virtual environment. -/// -/// Most of this information is derived from the virtual environment's `pyvenv.cfg` file. -/// The format of this file is not defined anywhere, and exactly which keys are present -/// depends on the tool that was used to create the virtual environment. -#[derive(Debug)] -pub(crate) struct VirtualEnvironment { - venv_path: SysPrefixPath, - base_executable_home_path: PythonHomePath, - include_system_site_packages: bool, - - /// The version of the Python executable that was used to create this virtual environment. - /// - /// The Python version is encoded under different keys and in different formats - /// by different virtual-environment creation tools, - /// and the key is never read by the standard-library `site.py` module, - /// so it's possible that we might not be able to find this information - /// in an acceptable format under any of the keys we expect. - /// This field will be `None` if so. - version: Option, -} - -impl VirtualEnvironment { - pub(crate) fn new( - path: impl AsRef, - origin: SysPrefixPathOrigin, - system: &dyn System, - ) -> SitePackagesDiscoveryResult { - Self::new_impl(path.as_ref(), origin, system) - } - - fn new_impl( - path: &SystemPath, - origin: SysPrefixPathOrigin, - system: &dyn System, - ) -> SitePackagesDiscoveryResult { - fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize { - index.checked_add(1).and_then(NonZeroUsize::new).unwrap() - } - - let venv_path = SysPrefixPath::new(path, origin, system)?; - let pyvenv_cfg_path = venv_path.join("pyvenv.cfg"); - tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'"); - - let pyvenv_cfg = system - .read_to_string(&pyvenv_cfg_path) - .map_err(|io_err| SitePackagesDiscoveryError::NoPyvenvCfgFile(origin, io_err))?; - - let mut include_system_site_packages = false; - let mut base_executable_home_path = None; - let mut version_info_string = None; - - // A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax! - // The Python standard-library's `site` module parses these files by splitting each line on - // '=' characters, so that's what we should do as well. - // - // See also: https://snarky.ca/how-virtual-environments-work/ - for (index, line) in pyvenv_cfg.lines().enumerate() { - if let Some((key, value)) = line.split_once('=') { - let key = key.trim(); - if key.is_empty() { - return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - pyvenv_cfg_path, - PyvenvCfgParseErrorKind::MalformedKeyValuePair { - line_number: pyvenv_cfg_line_number(index), - }, - )); - } - - let value = value.trim(); - if value.is_empty() { - return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - pyvenv_cfg_path, - PyvenvCfgParseErrorKind::MalformedKeyValuePair { - line_number: pyvenv_cfg_line_number(index), - }, - )); - } - - if value.contains('=') { - return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - pyvenv_cfg_path, - PyvenvCfgParseErrorKind::TooManyEquals { - line_number: pyvenv_cfg_line_number(index), - }, - )); - } - - match key { - "include-system-site-packages" => { - include_system_site_packages = value.eq_ignore_ascii_case("true"); - } - "home" => base_executable_home_path = Some(value), - // `virtualenv` and `uv` call this key `version_info`, - // but the stdlib venv module calls it `version` - "version" | "version_info" => version_info_string = Some(value), - _ => continue, - } - } - } - - // The `home` key is read by the standard library's `site.py` module, - // so if it's missing from the `pyvenv.cfg` file - // (or the provided value is invalid), - // it's reasonable to consider the virtual environment irredeemably broken. - let Some(base_executable_home_path) = base_executable_home_path else { - return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - pyvenv_cfg_path, - PyvenvCfgParseErrorKind::NoHomeKey, - )); - }; - let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system) - .map_err(|io_err| { - SitePackagesDiscoveryError::PyvenvCfgParseError( - pyvenv_cfg_path, - PyvenvCfgParseErrorKind::InvalidHomeValue(io_err), - ) - })?; - - // but the `version`/`version_info` key is not read by the standard library, - // and is provided under different keys depending on which virtual-environment creation tool - // created the `pyvenv.cfg` file. Lenient parsing is appropriate here: - // the file isn't really *invalid* if it doesn't have this key, - // or if the value doesn't parse according to our expectations. - let version = version_info_string.and_then(|version_string| { - let mut version_info_parts = version_string.split('.'); - let (major, minor) = (version_info_parts.next()?, version_info_parts.next()?); - PythonVersion::try_from((major, minor)).ok() - }); - - let metadata = Self { - venv_path, - base_executable_home_path, - include_system_site_packages, - version, - }; - - tracing::trace!("Resolved metadata for virtual environment: {metadata:?}"); - Ok(metadata) - } - - /// Return a list of `site-packages` directories that are available from this virtual environment - /// - /// See the documentation for `site_packages_dir_from_sys_prefix` for more details. - pub(crate) fn site_packages_directories( - &self, - system: &dyn System, - ) -> SitePackagesDiscoveryResult> { - let VirtualEnvironment { - venv_path, - base_executable_home_path, - include_system_site_packages, - version, - } = self; - - let mut site_packages_directories = vec![site_packages_directory_from_sys_prefix( - venv_path, *version, system, - )?]; - - if *include_system_site_packages { - let system_sys_prefix = - SysPrefixPath::from_executable_home_path(base_executable_home_path); - - // If we fail to resolve the `sys.prefix` path from the base executable home path, - // or if we fail to resolve the `site-packages` from the `sys.prefix` path, - // we should probably print a warning but *not* abort type checking - if let Some(sys_prefix_path) = system_sys_prefix { - match site_packages_directory_from_sys_prefix(&sys_prefix_path, *version, system) { - Ok(site_packages_directory) => { - site_packages_directories.push(site_packages_directory); - } - Err(error) => tracing::warn!( - "{error}. System site-packages will not be used for module resolution." - ), - } - } else { - tracing::warn!( - "Failed to resolve `sys.prefix` of the system Python installation \ -from the `home` value in the `pyvenv.cfg` file at `{}`. \ -System site-packages will not be used for module resolution.", - venv_path.join("pyvenv.cfg") - ); - } - } - - tracing::debug!("Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}"); - Ok(site_packages_directories) - } -} - -#[derive(Debug, thiserror::Error)] -pub(crate) enum SitePackagesDiscoveryError { - #[error("Invalid {1}: `{0}` could not be canonicalized")] - VenvDirCanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, #[source] io::Error), - #[error("Invalid {1}: `{0}` does not point to a directory on disk")] - VenvDirIsNotADirectory(SystemPathBuf, SysPrefixPathOrigin), - #[error("{0} points to a broken venv with no pyvenv.cfg file")] - NoPyvenvCfgFile(SysPrefixPathOrigin, #[source] io::Error), - #[error("Failed to parse the pyvenv.cfg file at {0} because {1}")] - PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind), - #[error("Failed to search the `lib` directory of the Python installation at {1} for `site-packages`")] - CouldNotReadLibDirectory(#[source] io::Error, SysPrefixPath), - #[error("Could not find the `site-packages` directory for the Python installation at {0}")] - NoSitePackagesDirFound(SysPrefixPath), -} - -/// The various ways in which parsing a `pyvenv.cfg` file could fail -#[derive(Debug)] -pub(crate) enum PyvenvCfgParseErrorKind { - TooManyEquals { line_number: NonZeroUsize }, - MalformedKeyValuePair { line_number: NonZeroUsize }, - NoHomeKey, - InvalidHomeValue(io::Error), -} - -impl fmt::Display for PyvenvCfgParseErrorKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TooManyEquals { line_number } => { - write!(f, "line {line_number} has too many '=' characters") - } - Self::MalformedKeyValuePair { line_number } => write!( - f, - "line {line_number} has a malformed ` = ` pair" - ), - Self::NoHomeKey => f.write_str("the file does not have a `home` key"), - Self::InvalidHomeValue(io_err) => { - write!( - f, - "the following error was encountered \ -when trying to resolve the `home` value to a directory on disk: {io_err}" - ) - } - } - } -} - -/// Attempt to retrieve the `site-packages` directory -/// associated with a given Python installation. -/// -/// The location of the `site-packages` directory can vary according to the -/// Python version that this installation represents. The Python version may -/// or may not be known at this point, which is why the `python_version` -/// parameter is an `Option`. -fn site_packages_directory_from_sys_prefix( - sys_prefix_path: &SysPrefixPath, - python_version: Option, - system: &dyn System, -) -> SitePackagesDiscoveryResult { - tracing::debug!("Searching for site-packages directory in {sys_prefix_path}"); - - if cfg!(target_os = "windows") { - let site_packages = sys_prefix_path.join(r"Lib\site-packages"); - return system - .is_directory(&site_packages) - .then_some(site_packages) - .ok_or(SitePackagesDiscoveryError::NoSitePackagesDirFound( - sys_prefix_path.to_owned(), - )); - } - - // In the Python standard library's `site.py` module (used for finding `site-packages` - // at runtime), we can find this in [the non-Windows branch]: - // - // ```py - // libdirs = [sys.platlibdir] - // if sys.platlibdir != "lib": - // libdirs.append("lib") - // ``` - // - // Pyright therefore searches for both a `lib/python3.X/site-packages` directory - // and a `lib64/python3.X/site-packages` directory on non-MacOS Unix systems, - // since `sys.platlibdir` can sometimes be equal to `"lib64"`. - // - // However, we only care about the `site-packages` directory insofar as it allows - // us to discover Python source code that can be used for inferring type - // information regarding third-party dependencies. That means that we don't need - // to care about any possible `lib64/site-packages` directories, since - // [the `sys`-module documentation] states that `sys.platlibdir` is *only* ever - // used for C extensions, never for pure-Python modules. - // - // [the non-Windows branch]: https://github.com/python/cpython/blob/a8be8fc6c4682089be45a87bd5ee1f686040116c/Lib/site.py#L401-L410 - // [the `sys`-module documentation]: https://docs.python.org/3/library/sys.html#sys.platlibdir - - // If we were able to figure out what Python version this installation is, - // we should be able to avoid iterating through all items in the `lib/` directory: - if let Some(version) = python_version { - let expected_path = sys_prefix_path.join(format!("lib/python{version}/site-packages")); - if system.is_directory(&expected_path) { - return Ok(expected_path); - } - if version.free_threaded_build_available() { - // Nearly the same as `expected_path`, but with an additional `t` after {version}: - let alternative_path = - sys_prefix_path.join(format!("lib/python{version}t/site-packages")); - if system.is_directory(&alternative_path) { - return Ok(alternative_path); - } - } - } - - // Either we couldn't figure out the version before calling this function - // (e.g., from a `pyvenv.cfg` file if this was a venv), - // or we couldn't find a `site-packages` folder at the expected location given - // the parsed version - // - // Note: the `python3.x` part of the `site-packages` path can't be computed from - // the `--python-version` the user has passed, as they might be running Python 3.12 locally - // even if they've requested that we type check their code "as if" they're running 3.8. - for entry_result in system - .read_directory(&sys_prefix_path.join("lib")) - .map_err(|io_err| { - SitePackagesDiscoveryError::CouldNotReadLibDirectory(io_err, sys_prefix_path.to_owned()) - })? - { - let Ok(entry) = entry_result else { - continue; - }; - - if !entry.file_type().is_directory() { - continue; - } - - let mut path = entry.into_path(); - - let name = path - .file_name() - .expect("File name to be non-null because path is guaranteed to be a child of `lib`"); - - if !name.starts_with("python3.") { - continue; - } - - path.push("site-packages"); - if system.is_directory(&path) { - return Ok(path); - } - } - Err(SitePackagesDiscoveryError::NoSitePackagesDirFound( - sys_prefix_path.to_owned(), - )) -} - -/// A path that represents the value of [`sys.prefix`] at runtime in Python -/// for a given Python executable. -/// -/// For the case of a virtual environment, where a -/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to -/// the virtual environment the Python binary lies inside, i.e. `/.venv`, -/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`. -/// System Python installations generally work the same way: if a system -/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix` -/// will be `/opt/homebrew`, and `site-packages` will be at -/// `/opt/homebrew/lib/python3.X/site-packages`. -/// -/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix -#[derive(Debug, PartialEq, Eq, Clone)] -pub(crate) struct SysPrefixPath { - inner: SystemPathBuf, - origin: SysPrefixPathOrigin, -} - -impl SysPrefixPath { - fn new( - unvalidated_path: impl AsRef, - origin: SysPrefixPathOrigin, - system: &dyn System, - ) -> SitePackagesDiscoveryResult { - Self::new_impl(unvalidated_path.as_ref(), origin, system) - } - - fn new_impl( - unvalidated_path: &SystemPath, - origin: SysPrefixPathOrigin, - system: &dyn System, - ) -> SitePackagesDiscoveryResult { - // It's important to resolve symlinks here rather than simply making the path absolute, - // since system Python installations often only put symlinks in the "expected" - // locations for `home` and `site-packages` - let canonicalized = system - .canonicalize_path(unvalidated_path) - .map_err(|io_err| { - SitePackagesDiscoveryError::VenvDirCanonicalizationError( - unvalidated_path.to_path_buf(), - origin, - io_err, - ) - })?; - system - .is_directory(&canonicalized) - .then_some(Self { - inner: canonicalized, - origin, - }) - .ok_or_else(|| { - SitePackagesDiscoveryError::VenvDirIsNotADirectory( - unvalidated_path.to_path_buf(), - origin, - ) - }) - } - - fn from_executable_home_path(path: &PythonHomePath) -> Option { - // No need to check whether `path.parent()` is a directory: - // the parent of a canonicalised path that is known to exist - // is guaranteed to be a directory. - if cfg!(target_os = "windows") { - Some(Self { - inner: path.to_path_buf(), - origin: SysPrefixPathOrigin::Derived, - }) - } else { - path.parent().map(|path| Self { - inner: path.to_path_buf(), - origin: SysPrefixPathOrigin::Derived, - }) - } - } -} - -impl Deref for SysPrefixPath { - type Target = SystemPath; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl fmt::Display for SysPrefixPath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "`sys.prefix` path `{}`", self.inner) - } -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum SysPrefixPathOrigin { - PythonCliFlag, - VirtualEnvVar, - Derived, - LocalVenv, -} - -impl Display for SysPrefixPathOrigin { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::PythonCliFlag => f.write_str("`--python` argument"), - Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"), - Self::Derived => f.write_str("derived `sys.prefix` path"), - Self::LocalVenv => f.write_str("local virtual environment"), - } - } -} - -/// The value given by the `home` key in `pyvenv.cfg` files. -/// -/// This is equivalent to `{sys_prefix_path}/bin`, and points -/// to a directory in which a Python executable can be found. -/// Confusingly, it is *not* the same as the [`PYTHONHOME`] -/// environment variable that Python provides! However, it's -/// consistent among all mainstream creators of Python virtual -/// environments (the stdlib Python `venv` module, the third-party -/// `virtualenv` library, and `uv`), was specified by -/// [the original PEP adding the `venv` module], -/// and it's one of the few fields that's read by the Python -/// standard library's `site.py` module. -/// -/// Although it doesn't appear to be specified anywhere, -/// all existing virtual environment tools always use an absolute path -/// for the `home` value, and the Python standard library also assumes -/// that the `home` value will be an absolute path. -/// -/// Other values, such as the path to the Python executable or the -/// base-executable `sys.prefix` value, are either only provided in -/// `pyvenv.cfg` files by some virtual-environment creators, -/// or are included under different keys depending on which -/// virtual-environment creation tool you've used. -/// -/// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME -/// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/ -#[derive(Debug, PartialEq, Eq)] -struct PythonHomePath(SystemPathBuf); - -impl PythonHomePath { - fn new(path: impl AsRef, system: &dyn System) -> io::Result { - let path = path.as_ref(); - // It's important to resolve symlinks here rather than simply making the path absolute, - // since system Python installations often only put symlinks in the "expected" - // locations for `home` and `site-packages` - let canonicalized = system.canonicalize_path(path)?; - system - .is_directory(&canonicalized) - .then_some(Self(canonicalized)) - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "not a directory")) - } -} - -impl Deref for PythonHomePath { - type Target = SystemPath; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for PythonHomePath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "`home` location `{}`", self.0) - } -} - -impl PartialEq for PythonHomePath { - fn eq(&self, other: &SystemPath) -> bool { - &*self.0 == other - } -} - -impl PartialEq for PythonHomePath { - fn eq(&self, other: &SystemPathBuf) -> bool { - self == &**other - } -} - -#[cfg(test)] -mod tests { - use ruff_db::system::TestSystem; - - use super::*; - - struct VirtualEnvironmentTester { - system: TestSystem, - minor_version: u8, - free_threaded: bool, - system_site_packages: bool, - pyvenv_cfg_version_field: Option<&'static str>, - } - - impl VirtualEnvironmentTester { - /// Builds a mock virtual environment, and returns the path to the venv - fn build_mock_venv(&self) -> SystemPathBuf { - let VirtualEnvironmentTester { - system, - minor_version, - system_site_packages, - free_threaded, - pyvenv_cfg_version_field, - } = self; - let memory_fs = system.memory_file_system(); - let unix_site_packages = if *free_threaded { - format!("lib/python3.{minor_version}t/site-packages") - } else { - format!("lib/python3.{minor_version}/site-packages") - }; - - let system_install_sys_prefix = - SystemPathBuf::from(&*format!("/Python3.{minor_version}")); - let (system_home_path, system_exe_path, system_site_packages_path) = - if cfg!(target_os = "windows") { - let system_home_path = system_install_sys_prefix.clone(); - let system_exe_path = system_home_path.join("python.exe"); - let system_site_packages_path = - system_install_sys_prefix.join(r"Lib\site-packages"); - (system_home_path, system_exe_path, system_site_packages_path) - } else { - let system_home_path = system_install_sys_prefix.join("bin"); - let system_exe_path = system_home_path.join("python"); - let system_site_packages_path = - system_install_sys_prefix.join(&unix_site_packages); - (system_home_path, system_exe_path, system_site_packages_path) - }; - memory_fs.write_file_all(system_exe_path, "").unwrap(); - memory_fs - .create_directory_all(&system_site_packages_path) - .unwrap(); - - let venv_sys_prefix = SystemPathBuf::from("/.venv"); - let (venv_exe, site_packages_path) = if cfg!(target_os = "windows") { - ( - venv_sys_prefix.join(r"Scripts\python.exe"), - venv_sys_prefix.join(r"Lib\site-packages"), - ) - } else { - ( - venv_sys_prefix.join("bin/python"), - venv_sys_prefix.join(&unix_site_packages), - ) - }; - memory_fs.write_file_all(&venv_exe, "").unwrap(); - memory_fs.create_directory_all(&site_packages_path).unwrap(); - - let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg"); - let mut pyvenv_cfg_contents = format!("home = {system_home_path}\n"); - if let Some(version_field) = pyvenv_cfg_version_field { - pyvenv_cfg_contents.push_str(version_field); - pyvenv_cfg_contents.push('\n'); - } - // Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive: - if *system_site_packages { - pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n"); - } - memory_fs - .write_file_all(pyvenv_cfg_path, &pyvenv_cfg_contents) - .unwrap(); - - venv_sys_prefix - } - - fn test(self) { - let venv_path = self.build_mock_venv(); - let venv = VirtualEnvironment::new( - venv_path.clone(), - SysPrefixPathOrigin::VirtualEnvVar, - &self.system, - ) - .unwrap(); - - assert_eq!( - venv.venv_path, - SysPrefixPath { - inner: self.system.canonicalize_path(&venv_path).unwrap(), - origin: SysPrefixPathOrigin::VirtualEnvVar, - } - ); - assert_eq!(venv.include_system_site_packages, self.system_site_packages); - - if self.pyvenv_cfg_version_field.is_some() { - assert_eq!( - venv.version, - Some(PythonVersion { - major: 3, - minor: self.minor_version - }) - ); - } else { - assert_eq!(venv.version, None); - } - - let expected_home = if cfg!(target_os = "windows") { - SystemPathBuf::from(&*format!(r"\Python3.{}", self.minor_version)) - } else { - SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version)) - }; - assert_eq!(venv.base_executable_home_path, expected_home); - - let site_packages_directories = venv.site_packages_directories(&self.system).unwrap(); - let expected_venv_site_packages = if cfg!(target_os = "windows") { - SystemPathBuf::from(r"\.venv\Lib\site-packages") - } else if self.free_threaded { - SystemPathBuf::from(&*format!( - "/.venv/lib/python3.{}t/site-packages", - self.minor_version - )) - } else { - SystemPathBuf::from(&*format!( - "/.venv/lib/python3.{}/site-packages", - self.minor_version - )) - }; - - let expected_system_site_packages = if cfg!(target_os = "windows") { - SystemPathBuf::from(&*format!( - r"\Python3.{}\Lib\site-packages", - self.minor_version - )) - } else if self.free_threaded { - SystemPathBuf::from(&*format!( - "/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages", - minor_version = self.minor_version - )) - } else { - SystemPathBuf::from(&*format!( - "/Python3.{minor_version}/lib/python3.{minor_version}/site-packages", - minor_version = self.minor_version - )) - }; - - if self.system_site_packages { - assert_eq!( - &site_packages_directories, - &[expected_venv_site_packages, expected_system_site_packages] - ); - } else { - assert_eq!(&site_packages_directories, &[expected_venv_site_packages]); - } - } - } - - #[test] - fn can_find_site_packages_directory_no_version_field_in_pyvenv_cfg() { - let tester = VirtualEnvironmentTester { - system: TestSystem::default(), - minor_version: 12, - free_threaded: false, - system_site_packages: false, - pyvenv_cfg_version_field: None, - }; - tester.test(); - } - - #[test] - fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() { - let tester = VirtualEnvironmentTester { - system: TestSystem::default(), - minor_version: 12, - free_threaded: false, - system_site_packages: false, - pyvenv_cfg_version_field: Some("version = 3.12"), - }; - tester.test(); - } - - #[test] - fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() { - let tester = VirtualEnvironmentTester { - system: TestSystem::default(), - minor_version: 12, - free_threaded: false, - system_site_packages: false, - pyvenv_cfg_version_field: Some("version_info = 3.12"), - }; - tester.test(); - } - - #[test] - fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() { - let tester = VirtualEnvironmentTester { - system: TestSystem::default(), - minor_version: 12, - free_threaded: false, - system_site_packages: false, - pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"), - }; - tester.test(); - } - - #[test] - fn can_find_site_packages_directory_freethreaded_build() { - let tester = VirtualEnvironmentTester { - system: TestSystem::default(), - minor_version: 13, - free_threaded: true, - system_site_packages: false, - pyvenv_cfg_version_field: Some("version_info = 3.13"), - }; - tester.test(); - } - - #[test] - fn finds_system_site_packages() { - let tester = VirtualEnvironmentTester { - system: TestSystem::default(), - minor_version: 13, - free_threaded: true, - system_site_packages: true, - pyvenv_cfg_version_field: Some("version_info = 3.13"), - }; - tester.test(); - } - - #[test] - fn reject_venv_that_does_not_exist() { - let system = TestSystem::default(); - assert!(matches!( - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system), - Err(SitePackagesDiscoveryError::VenvDirCanonicalizationError(..)) - )); - } - - #[test] - fn reject_venv_that_is_not_a_directory() { - let system = TestSystem::default(); - system - .memory_file_system() - .write_file_all("/.venv", "") - .unwrap(); - assert!(matches!( - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system), - Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(..)) - )); - } - - #[test] - fn reject_venv_with_no_pyvenv_cfg_file() { - let system = TestSystem::default(); - system - .memory_file_system() - .create_directory_all("/.venv") - .unwrap(); - assert!(matches!( - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system), - Err(SitePackagesDiscoveryError::NoPyvenvCfgFile( - SysPrefixPathOrigin::VirtualEnvVar, - _ - )) - )); - } - - #[test] - fn parsing_pyvenv_cfg_with_too_many_equals() { - let system = TestSystem::default(); - let memory_fs = system.memory_file_system(); - let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); - memory_fs - .write_file_all(&pyvenv_cfg_path, "home = bar = /.venv/bin") - .unwrap(); - let venv_result = - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); - assert!(matches!( - venv_result, - Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - path, - PyvenvCfgParseErrorKind::TooManyEquals { line_number } - )) - if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1) - )); - } - - #[test] - fn parsing_pyvenv_cfg_with_key_but_no_value_fails() { - let system = TestSystem::default(); - let memory_fs = system.memory_file_system(); - let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); - memory_fs - .write_file_all(&pyvenv_cfg_path, "home =") - .unwrap(); - let venv_result = - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); - assert!(matches!( - venv_result, - Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - path, - PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number } - )) - if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1) - )); - } - - #[test] - fn parsing_pyvenv_cfg_with_value_but_no_key_fails() { - let system = TestSystem::default(); - let memory_fs = system.memory_file_system(); - let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); - memory_fs - .write_file_all(&pyvenv_cfg_path, "= whatever") - .unwrap(); - let venv_result = - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); - assert!(matches!( - venv_result, - Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - path, - PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number } - )) - if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1) - )); - } - - #[test] - fn parsing_pyvenv_cfg_with_no_home_key_fails() { - let system = TestSystem::default(); - let memory_fs = system.memory_file_system(); - let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); - memory_fs.write_file_all(&pyvenv_cfg_path, "").unwrap(); - let venv_result = - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); - assert!(matches!( - venv_result, - Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - path, - PyvenvCfgParseErrorKind::NoHomeKey - )) - if path == pyvenv_cfg_path - )); - } - - #[test] - fn parsing_pyvenv_cfg_with_invalid_home_key_fails() { - let system = TestSystem::default(); - let memory_fs = system.memory_file_system(); - let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); - memory_fs - .write_file_all(&pyvenv_cfg_path, "home = foo") - .unwrap(); - let venv_result = - VirtualEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); - assert!(matches!( - venv_result, - Err(SitePackagesDiscoveryError::PyvenvCfgParseError( - path, - PyvenvCfgParseErrorKind::InvalidHomeValue(_) - )) - if path == pyvenv_cfg_path - )); - } -} diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs deleted file mode 100644 index dae79c3b63aba..0000000000000 --- a/crates/red_knot_python_semantic/src/symbol.rs +++ /dev/null @@ -1,1138 +0,0 @@ -use ruff_db::files::File; - -use crate::module_resolver::file_to_module; -use crate::semantic_index::definition::Definition; -use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId}; -use crate::semantic_index::{global_scope, use_def_map, DeclarationWithConstraint}; -use crate::semantic_index::{ - symbol_table, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, -}; -use crate::types::{ - binding_type, declaration_type, infer_narrowing_constraint, todo_type, IntersectionBuilder, - KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, -}; -use crate::{resolve_module, Db, KnownModule, Program}; - -pub(crate) use implicit_globals::module_type_implicit_global_symbol; - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub(crate) enum Boundness { - Bound, - PossiblyUnbound, -} - -impl Boundness { - pub(crate) const fn max(self, other: Self) -> Self { - match (self, other) { - (Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound, - (Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound, - } - } -} - -/// The result of a symbol lookup, which can either be a (possibly unbound) type -/// or a completely unbound symbol. -/// -/// Consider this example: -/// ```py -/// bound = 1 -/// -/// if flag: -/// possibly_unbound = 2 -/// ``` -/// -/// If we look up symbols in this scope, we would get the following results: -/// ```rs -/// bound: Symbol::Type(Type::IntLiteral(1), Boundness::Bound), -/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound), -/// non_existent: Symbol::Unbound, -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(crate) enum Symbol<'db> { - Type(Type<'db>, Boundness), - Unbound, -} - -impl<'db> Symbol<'db> { - /// Constructor that creates a `Symbol` with boundness [`Boundness::Bound`]. - pub(crate) fn bound(ty: impl Into>) -> Self { - Symbol::Type(ty.into(), Boundness::Bound) - } - - pub(crate) fn possibly_unbound(ty: impl Into>) -> Self { - Symbol::Type(ty.into(), Boundness::PossiblyUnbound) - } - - /// Constructor that creates a [`Symbol`] with a [`crate::types::TodoType`] type - /// and boundness [`Boundness::Bound`]. - #[allow(unused_variables)] // Only unused in release builds - pub(crate) fn todo(message: &'static str) -> Self { - Symbol::Type(todo_type!(message), Boundness::Bound) - } - - pub(crate) fn is_unbound(&self) -> bool { - matches!(self, Symbol::Unbound) - } - - /// Returns the type of the symbol, ignoring possible unboundness. - /// - /// If the symbol is *definitely* unbound, this function will return `None`. Otherwise, - /// if there is at least one control-flow path where the symbol is bound, return the type. - pub(crate) fn ignore_possibly_unbound(&self) -> Option> { - match self { - Symbol::Type(ty, _) => Some(*ty), - Symbol::Unbound => None, - } - } - - #[cfg(test)] - #[track_caller] - pub(crate) fn expect_type(self) -> Type<'db> { - self.ignore_possibly_unbound() - .expect("Expected a (possibly unbound) type, not an unbound symbol") - } - - #[must_use] - pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> { - match self { - Symbol::Type(ty, boundness) => Symbol::Type(f(ty), boundness), - Symbol::Unbound => Symbol::Unbound, - } - } - - #[must_use] - pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> SymbolAndQualifiers<'db> { - SymbolAndQualifiers { - symbol: self, - qualifiers, - } - } -} - -impl<'db> From> for SymbolAndQualifiers<'db> { - fn from(value: LookupResult<'db>) -> Self { - match value { - Ok(type_and_qualifiers) => { - Symbol::Type(type_and_qualifiers.inner_type(), Boundness::Bound) - .with_qualifiers(type_and_qualifiers.qualifiers()) - } - Err(LookupError::Unbound(qualifiers)) => Symbol::Unbound.with_qualifiers(qualifiers), - Err(LookupError::PossiblyUnbound(type_and_qualifiers)) => { - Symbol::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound) - .with_qualifiers(type_and_qualifiers.qualifiers()) - } - } - } -} - -/// Possible ways in which a symbol lookup can (possibly or definitely) fail. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub(crate) enum LookupError<'db> { - Unbound(TypeQualifiers), - PossiblyUnbound(TypeAndQualifiers<'db>), -} - -impl<'db> LookupError<'db> { - /// Fallback (wholly or partially) to `fallback` to create a new [`LookupResult`]. - pub(crate) fn or_fall_back_to( - self, - db: &'db dyn Db, - fallback: SymbolAndQualifiers<'db>, - ) -> LookupResult<'db> { - let fallback = fallback.into_lookup_result(); - match (&self, &fallback) { - (LookupError::Unbound(_), _) => fallback, - (LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound(_))) => Err(self), - (LookupError::PossiblyUnbound(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new( - UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), - ty.qualifiers().union(ty2.qualifiers()), - )), - (LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => { - Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( - UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), - ty.qualifiers().union(ty2.qualifiers()), - ))) - } - } - } -} - -/// A [`Result`] type in which the `Ok` variant represents a definitely bound symbol -/// and the `Err` variant represents a symbol that is either definitely or possibly unbound. -/// -/// Note that this type is exactly isomorphic to [`Symbol`]. -/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead. -pub(crate) type LookupResult<'db> = Result, LookupError<'db>>; - -/// Infer the public type of a symbol (its type as seen from outside its scope) in the given -/// `scope`. -pub(crate) fn symbol<'db>( - db: &'db dyn Db, - scope: ScopeId<'db>, - name: &str, -) -> SymbolAndQualifiers<'db> { - symbol_impl(db, scope, name, RequiresExplicitReExport::No) -} - -/// Infer the public type of a class symbol (its type as seen from outside its scope) in the given -/// `scope`. -pub(crate) fn class_symbol<'db>( - db: &'db dyn Db, - scope: ScopeId<'db>, - name: &str, -) -> SymbolAndQualifiers<'db> { - symbol_table(db, scope) - .symbol_id_by_name(name) - .map(|symbol| { - let symbol_and_quals = symbol_by_id(db, scope, symbol, RequiresExplicitReExport::No); - - if symbol_and_quals.is_class_var() { - // For declared class vars we do not need to check if they have bindings, - // we just trust the declaration. - return symbol_and_quals; - } - - if let SymbolAndQualifiers { - symbol: Symbol::Type(ty, _), - qualifiers, - } = symbol_and_quals - { - // Otherwise, we need to check if the symbol has bindings - let use_def = use_def_map(db, scope); - let bindings = use_def.public_bindings(symbol); - let inferred = - symbol_from_bindings_impl(db, bindings, RequiresExplicitReExport::No); - - // TODO: we should not need to calculate inferred type second time. This is a temporary - // solution until the notion of Boundness and Declaredness is split. See #16036, #16264 - match inferred { - Symbol::Unbound => Symbol::Unbound.with_qualifiers(qualifiers), - Symbol::Type(_, boundness) => { - Symbol::Type(ty, boundness).with_qualifiers(qualifiers) - } - } - } else { - Symbol::Unbound.into() - } - }) - .unwrap_or_default() -} - -/// Infers the public type of an explicit module-global symbol as seen from within the same file. -/// -/// Note that all global scopes also include various "implicit globals" such as `__name__`, -/// `__doc__` and `__file__`. This function **does not** consider those symbols; it will return -/// `Symbol::Unbound` for them. Use the (currently test-only) `global_symbol` query to also include -/// those additional symbols. -/// -/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). -pub(crate) fn explicit_global_symbol<'db>( - db: &'db dyn Db, - file: File, - name: &str, -) -> SymbolAndQualifiers<'db> { - symbol_impl( - db, - global_scope(db, file), - name, - RequiresExplicitReExport::No, - ) -} - -/// Infers the public type of an explicit module-global symbol as seen from within the same file. -/// -/// Unlike [`explicit_global_symbol`], this function also considers various "implicit globals" -/// such as `__name__`, `__doc__` and `__file__`. These are looked up as attributes on `types.ModuleType` -/// rather than being looked up as symbols explicitly defined/declared in the global scope. -/// -/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). -#[cfg(test)] -pub(crate) fn global_symbol<'db>( - db: &'db dyn Db, - file: File, - name: &str, -) -> SymbolAndQualifiers<'db> { - explicit_global_symbol(db, file, name) - .or_fall_back_to(db, || module_type_implicit_global_symbol(db, name)) -} - -/// Infers the public type of an imported symbol. -pub(crate) fn imported_symbol<'db>( - db: &'db dyn Db, - file: File, - name: &str, -) -> SymbolAndQualifiers<'db> { - // If it's not found in the global scope, check if it's present as an instance on - // `types.ModuleType` or `builtins.object`. - // - // We do a more limited version of this in `module_type_implicit_global_symbol`, - // but there are two crucial differences here: - // - If a member is looked up as an attribute, `__init__` is also available on the module, but - // it isn't available as a global from inside the module - // - If a member is looked up as an attribute, members on `builtins.object` are also available - // (because `types.ModuleType` inherits from `object`); these attributes are also not - // available as globals from inside the module. - // - // The same way as in `module_type_implicit_global_symbol`, however, we need to be careful to - // ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with - // dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which - // module we're dealing with. - external_symbol_impl(db, file, name).or_fall_back_to(db, || { - if name == "__getattr__" { - Symbol::Unbound.into() - } else { - KnownClass::ModuleType.to_instance(db).member(db, name) - } - }) -} - -/// Lookup the type of `symbol` in the builtins namespace. -/// -/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason. -/// -/// Note that this function is only intended for use in the context of the builtins *namespace* -/// and should not be used when a symbol is being explicitly imported from the `builtins` module -/// (e.g. `from builtins import int`). -pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> { - resolve_module(db, &KnownModule::Builtins.name()) - .map(|module| { - external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || { - // We're looking up in the builtins namespace and not the module, so we should - // do the normal lookup in `types.ModuleType` and not the special one as in - // `imported_symbol`. - module_type_implicit_global_symbol(db, symbol) - }) - }) - .unwrap_or_default() -} - -/// Lookup the type of `symbol` in a given known module. -/// -/// Returns `Symbol::Unbound` if the given known module cannot be resolved for some reason. -pub(crate) fn known_module_symbol<'db>( - db: &'db dyn Db, - known_module: KnownModule, - symbol: &str, -) -> SymbolAndQualifiers<'db> { - resolve_module(db, &known_module.name()) - .map(|module| imported_symbol(db, module.file(), symbol)) - .unwrap_or_default() -} - -/// Lookup the type of `symbol` in the `typing` module namespace. -/// -/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason. -#[inline] -#[cfg(test)] -pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> { - known_module_symbol(db, KnownModule::Typing, symbol) -} - -/// Lookup the type of `symbol` in the `typing_extensions` module namespace. -/// -/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason. -#[inline] -pub(crate) fn typing_extensions_symbol<'db>( - db: &'db dyn Db, - symbol: &str, -) -> SymbolAndQualifiers<'db> { - known_module_symbol(db, KnownModule::TypingExtensions, symbol) -} - -/// Get the `builtins` module scope. -/// -/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`. -pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option> { - core_module_scope(db, KnownModule::Builtins) -} - -/// Get the scope of a core stdlib module. -/// -/// Can return `None` if a custom typeshed is used that is missing the core module in question. -fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option> { - resolve_module(db, &core_module.name()).map(|module| global_scope(db, module.file())) -} - -/// Infer the combined type from an iterator of bindings, and return it -/// together with boundness information in a [`Symbol`]. -/// -/// The type will be a union if there are multiple bindings with different types. -pub(super) fn symbol_from_bindings<'db>( - db: &'db dyn Db, - bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, -) -> Symbol<'db> { - symbol_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No) -} - -/// Build a declared type from a [`DeclarationsIterator`]. -/// -/// If there is only one declaration, or all declarations declare the same type, returns -/// `Ok(..)`. If there are conflicting declarations, returns an `Err(..)` variant with -/// a union of the declared types as well as a list of all conflicting types. -/// -/// This function also returns declaredness information (see [`Symbol`]) and a set of -/// [`TypeQualifiers`] that have been specified on the declaration(s). -pub(crate) fn symbol_from_declarations<'db>( - db: &'db dyn Db, - declarations: DeclarationsIterator<'_, 'db>, -) -> SymbolFromDeclarationsResult<'db> { - symbol_from_declarations_impl(db, declarations, RequiresExplicitReExport::No) -} - -/// The result of looking up a declared type from declarations; see [`symbol_from_declarations`]. -pub(crate) type SymbolFromDeclarationsResult<'db> = - Result, (TypeAndQualifiers<'db>, Box<[Type<'db>]>)>; - -/// A type with declaredness information, and a set of type qualifiers. -/// -/// This is used to represent the result of looking up the declared type. Consider this -/// example: -/// ```py -/// class C: -/// if flag: -/// variable: ClassVar[int] -/// ``` -/// If we look up the declared type of `variable` in the scope of class `C`, we will get -/// the type `int`, a "declaredness" of [`Boundness::PossiblyUnbound`], and the information -/// that this comes with a [`CLASS_VAR`] type qualifier. -/// -/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR -#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(crate) struct SymbolAndQualifiers<'db> { - pub(crate) symbol: Symbol<'db>, - pub(crate) qualifiers: TypeQualifiers, -} - -impl Default for SymbolAndQualifiers<'_> { - fn default() -> Self { - SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers: TypeQualifiers::empty(), - } - } -} - -impl<'db> SymbolAndQualifiers<'db> { - /// Constructor that creates a [`SymbolAndQualifiers`] instance with a [`TodoType`] type - /// and no qualifiers. - /// - /// [`TodoType`]: crate::types::TodoType - pub(crate) fn todo(message: &'static str) -> Self { - Self { - symbol: Symbol::todo(message), - qualifiers: TypeQualifiers::empty(), - } - } - - /// Returns `true` if the symbol has a `ClassVar` type qualifier. - pub(crate) fn is_class_var(&self) -> bool { - self.qualifiers.contains(TypeQualifiers::CLASS_VAR) - } - - #[must_use] - pub(crate) fn map_type( - self, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> SymbolAndQualifiers<'db> { - SymbolAndQualifiers { - symbol: self.symbol.map_type(f), - qualifiers: self.qualifiers, - } - } - - /// Transform symbol and qualifiers into a [`LookupResult`], - /// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol - /// and the `Err` variant represents a symbol that is either definitely or possibly unbound. - pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { - match self { - SymbolAndQualifiers { - symbol: Symbol::Type(ty, Boundness::Bound), - qualifiers, - } => Ok(TypeAndQualifiers::new(ty, qualifiers)), - SymbolAndQualifiers { - symbol: Symbol::Type(ty, Boundness::PossiblyUnbound), - qualifiers, - } => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( - ty, qualifiers, - ))), - SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers, - } => Err(LookupError::Unbound(qualifiers)), - } - } - - /// Safely unwrap the symbol and the qualifiers into a [`TypeQualifiers`]. - /// - /// If the symbol is definitely unbound or possibly unbound, it will be transformed into a - /// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning - /// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller - /// to ensure that a diagnostic is emitted if the symbol is possibly or definitely unbound. - pub(crate) fn unwrap_with_diagnostic( - self, - diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, - ) -> TypeAndQualifiers<'db> { - self.into_lookup_result().unwrap_or_else(diagnostic_fn) - } - - /// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound. - /// - /// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`. - /// 2. Else, evaluate `fallback_fn()`: - /// 1. If `self` is definitely unbound, return the result of `fallback_fn()`. - /// 2. Else, if `fallback` is definitely unbound, return `self`. - /// 3. Else, if `self` is possibly unbound and `fallback` is definitely bound, - /// return `Symbol(, Boundness::Bound)` - /// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound, - /// return `Symbol(, Boundness::PossiblyUnbound)` - #[must_use] - pub(crate) fn or_fall_back_to( - self, - db: &'db dyn Db, - fallback_fn: impl FnOnce() -> SymbolAndQualifiers<'db>, - ) -> Self { - self.into_lookup_result() - .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) - .into() - } -} - -impl<'db> From> for SymbolAndQualifiers<'db> { - fn from(symbol: Symbol<'db>) -> Self { - symbol.with_qualifiers(TypeQualifiers::empty()) - } -} - -fn symbol_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &SymbolAndQualifiers<'db>, - _count: u32, - _scope: ScopeId<'db>, - _symbol_id: ScopedSymbolId, - _requires_explicit_reexport: RequiresExplicitReExport, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn symbol_cycle_initial<'db>( - _db: &'db dyn Db, - _scope: ScopeId<'db>, - _symbol_id: ScopedSymbolId, - _requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolAndQualifiers<'db> { - Symbol::bound(Type::Never).into() -} - -#[salsa::tracked(cycle_fn=symbol_cycle_recover, cycle_initial=symbol_cycle_initial)] -fn symbol_by_id<'db>( - db: &'db dyn Db, - scope: ScopeId<'db>, - symbol_id: ScopedSymbolId, - requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolAndQualifiers<'db> { - let use_def = use_def_map(db, scope); - - // If the symbol is declared, the public type is based on declarations; otherwise, it's based - // on inference from bindings. - - let declarations = use_def.public_declarations(symbol_id); - let declared = symbol_from_declarations_impl(db, declarations, requires_explicit_reexport); - - match declared { - // Symbol is declared, trust the declared type - Ok( - symbol_and_quals @ SymbolAndQualifiers { - symbol: Symbol::Type(_, Boundness::Bound), - qualifiers: _, - }, - ) => symbol_and_quals, - // Symbol is possibly declared - Ok(SymbolAndQualifiers { - symbol: Symbol::Type(declared_ty, Boundness::PossiblyUnbound), - qualifiers, - }) => { - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport); - - let symbol = match inferred { - // Symbol is possibly undeclared and definitely unbound - Symbol::Unbound => { - // TODO: We probably don't want to report `Bound` here. This requires a bit of - // design work though as we might want a different behavior for stubs and for - // normal modules. - Symbol::Type(declared_ty, Boundness::Bound) - } - // Symbol is possibly undeclared and (possibly) bound - Symbol::Type(inferred_ty, boundness) => Symbol::Type( - UnionType::from_elements(db, [inferred_ty, declared_ty]), - boundness, - ), - }; - - SymbolAndQualifiers { symbol, qualifiers } - } - // Symbol is undeclared, return the union of `Unknown` with the inferred type - Ok(SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers: _, - }) => { - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport); - - // `__slots__` is a symbol with special behavior in Python's runtime. It can be - // modified externally, but those changes do not take effect. We therefore issue - // a diagnostic if we see it being modified externally. In type inference, we - // can assign a "narrow" type to it even if it is not *declared*. This means, we - // do not have to call [`widen_type_for_undeclared_public_symbol`]. - // - // `TYPE_CHECKING` is a special variable that should only be assigned `False` - // at runtime, but is always considered `True` in type checking. - // See mdtest/known_constants.md#user-defined-type_checking for details. - let is_considered_non_modifiable = matches!( - symbol_table(db, scope).symbol(symbol_id).name().as_str(), - "__slots__" | "TYPE_CHECKING" - ); - - widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable) - .into() - } - // Symbol has conflicting declared types - Err((declared, _)) => { - // Intentionally ignore conflicting declared types; that's not our problem, - // it's the problem of the module we are importing from. - Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) - } - } - - // TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness - // currently only depends on bindings, and ignores declarations. This is inconsistent, since - // we only look at bindings if the symbol may be undeclared. Consider the following example: - // ```py - // x: int - // - // if flag: - // y: int - // else - // y = 3 - // ``` - // If we import from this module, we will currently report `x` as a definitely-bound symbol - // (even though it has no bindings at all!) but report `y` as possibly-unbound (even though - // every path has either a binding or a declaration for it.) -} - -/// Implementation of [`symbol`]. -fn symbol_impl<'db>( - db: &'db dyn Db, - scope: ScopeId<'db>, - name: &str, - requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolAndQualifiers<'db> { - let _span = tracing::trace_span!("symbol", ?name).entered(); - - if name == "platform" - && file_to_module(db, scope.file(db)) - .is_some_and(|module| module.is_known(KnownModule::Sys)) - { - match Program::get(db).python_platform(db) { - crate::PythonPlatform::Identifier(platform) => { - return Symbol::bound(Type::string_literal(db, platform.as_str())).into(); - } - crate::PythonPlatform::All => { - // Fall through to the looked up type - } - } - } - - symbol_table(db, scope) - .symbol_id_by_name(name) - .map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport)) - .unwrap_or_default() -} - -/// Implementation of [`symbol_from_bindings`]. -/// -/// ## Implementation Note -/// This function gets called cross-module. It, therefore, shouldn't -/// access any AST nodes from the file containing the declarations. -fn symbol_from_bindings_impl<'db>( - db: &'db dyn Db, - bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, - requires_explicit_reexport: RequiresExplicitReExport, -) -> Symbol<'db> { - let predicates = bindings_with_constraints.predicates; - let visibility_constraints = bindings_with_constraints.visibility_constraints; - let mut bindings_with_constraints = bindings_with_constraints.peekable(); - - let is_non_exported = |binding: Definition<'db>| { - requires_explicit_reexport.is_yes() && !binding.is_reexported(db) - }; - - let unbound_visibility_constraint = match bindings_with_constraints.peek() { - Some(BindingWithConstraints { - binding, - visibility_constraint, - narrowing_constraint: _, - }) if binding.is_none_or(is_non_exported) => Some(*visibility_constraint), - _ => None, - }; - - // Evaluate this lazily because we don't always need it (for example, if there are no visible - // bindings at all, we don't need it), and it can cause us to evaluate visibility constraint - // expressions, which is extra work and can lead to cycles. - let unbound_visibility = || { - unbound_visibility_constraint - .map(|visibility_constraint| { - visibility_constraints.evaluate(db, predicates, visibility_constraint) - }) - .unwrap_or(Truthiness::AlwaysFalse) - }; - - let mut types = bindings_with_constraints.filter_map( - |BindingWithConstraints { - binding, - narrowing_constraint, - visibility_constraint, - }| { - let binding = binding?; - - if is_non_exported(binding) { - return None; - } - - let static_visibility = - visibility_constraints.evaluate(db, predicates, visibility_constraint); - - if static_visibility.is_always_false() { - // We found a binding that we have statically determined to not be visible from - // the use of the symbol that we are investigating. There are three interesting - // cases to consider: - // - // ```py - // def f1(): - // if False: - // x = 1 - // use(x) - // - // def f2(): - // y = 1 - // return - // use(y) - // - // def f3(flag: bool): - // z = 1 - // if flag: - // z = 2 - // return - // use(z) - // ``` - // - // In the first case, there is a single binding for `x`, and due to the statically - // known `False` condition, it is not visible at the use of `x`. However, we *can* - // see/reach the start of the scope from `use(x)`. This means that `x` is unbound - // and we should return `None`. - // - // In the second case, `y` is also not visible at the use of `y`, but here, we can - // not see/reach the start of the scope. There is only one path of control flow, - // and it passes through that binding of `y` (which we can not see). This implies - // that we are in an unreachable section of code. We return `Never` in order to - // silence the `unresolve-reference` diagnostic that would otherwise be emitted at - // the use of `y`. - // - // In the third case, we have two bindings for `z`. The first one is visible, so we - // consider the case that we now encounter the second binding `z = 2`, which is not - // visible due to the early return. We *also* can not see the start of the scope - // from `use(z)` because both paths of control flow pass through a binding of `z`. - // The `z = 1` binding is visible, and so we are *not* in an unreachable section of - // code. However, it is still okay to return `Never` in this case, because we will - // union the types of all bindings, and `Never` will be eliminated automatically. - - if unbound_visibility().is_always_false() { - // The scope-start is not visible - return Some(Type::Never); - } - return None; - } - - let constraint_tys: Vec<_> = narrowing_constraint - .filter_map(|constraint| infer_narrowing_constraint(db, constraint, binding)) - .collect(); - - let binding_ty = binding_type(db, binding); - if constraint_tys.is_empty() { - Some(binding_ty) - } else { - let intersection_ty = constraint_tys - .into_iter() - .rev() - .fold( - IntersectionBuilder::new(db).add_positive(binding_ty), - IntersectionBuilder::add_positive, - ) - .build(); - Some(intersection_ty) - } - }, - ); - - if let Some(first) = types.next() { - let boundness = match unbound_visibility() { - Truthiness::AlwaysTrue => { - unreachable!("If we have at least one binding, the scope-start should not be definitely visible") - } - Truthiness::AlwaysFalse => Boundness::Bound, - Truthiness::Ambiguous => Boundness::PossiblyUnbound, - }; - - if let Some(second) = types.next() { - Symbol::Type( - UnionType::from_elements(db, [first, second].into_iter().chain(types)), - boundness, - ) - } else { - Symbol::Type(first, boundness) - } - } else { - Symbol::Unbound - } -} - -/// Implementation of [`symbol_from_declarations`]. -/// -/// ## Implementation Note -/// This function gets called cross-module. It, therefore, shouldn't -/// access any AST nodes from the file containing the declarations. -fn symbol_from_declarations_impl<'db>( - db: &'db dyn Db, - declarations: DeclarationsIterator<'_, 'db>, - requires_explicit_reexport: RequiresExplicitReExport, -) -> SymbolFromDeclarationsResult<'db> { - let predicates = declarations.predicates; - let visibility_constraints = declarations.visibility_constraints; - let mut declarations = declarations.peekable(); - - let is_non_exported = |declaration: Definition<'db>| { - requires_explicit_reexport.is_yes() && !declaration.is_reexported(db) - }; - - let undeclared_visibility = match declarations.peek() { - Some(DeclarationWithConstraint { - declaration, - visibility_constraint, - }) if declaration.is_none_or(is_non_exported) => { - visibility_constraints.evaluate(db, predicates, *visibility_constraint) - } - _ => Truthiness::AlwaysFalse, - }; - - let mut types = declarations.filter_map( - |DeclarationWithConstraint { - declaration, - visibility_constraint, - }| { - let declaration = declaration?; - - if is_non_exported(declaration) { - return None; - } - - let static_visibility = - visibility_constraints.evaluate(db, predicates, visibility_constraint); - - if static_visibility.is_always_false() { - None - } else { - Some(declaration_type(db, declaration)) - } - }, - ); - - if let Some(first) = types.next() { - let mut conflicting: Vec> = vec![]; - let declared = if let Some(second) = types.next() { - let ty_first = first.inner_type(); - let mut qualifiers = first.qualifiers(); - - let mut builder = UnionBuilder::new(db).add(ty_first); - for other in std::iter::once(second).chain(types) { - let other_ty = other.inner_type(); - if !ty_first.is_equivalent_to(db, other_ty) { - conflicting.push(other_ty); - } - builder = builder.add(other_ty); - qualifiers = qualifiers.union(other.qualifiers()); - } - TypeAndQualifiers::new(builder.build(), qualifiers) - } else { - first - }; - if conflicting.is_empty() { - let boundness = match undeclared_visibility { - Truthiness::AlwaysTrue => { - unreachable!("If we have at least one declaration, the scope-start should not be definitely visible") - } - Truthiness::AlwaysFalse => Boundness::Bound, - Truthiness::Ambiguous => Boundness::PossiblyUnbound, - }; - - Ok(Symbol::Type(declared.inner_type(), boundness) - .with_qualifiers(declared.qualifiers())) - } else { - Err(( - declared, - std::iter::once(first.inner_type()) - .chain(conflicting) - .collect(), - )) - } - } else { - Ok(Symbol::Unbound.into()) - } -} - -mod implicit_globals { - use ruff_python_ast as ast; - - use crate::db::Db; - use crate::semantic_index::{self, symbol_table}; - use crate::symbol::SymbolAndQualifiers; - use crate::types::KnownClass; - - use super::Symbol; - - /// Looks up the type of an "implicit global symbol". Returns [`Symbol::Unbound`] if - /// `name` is not present as an implicit symbol in module-global namespaces. - /// - /// Implicit global symbols are symbols such as `__doc__`, `__name__`, and `__file__` - /// that are implicitly defined in every module's global scope. Because their type is - /// always the same, we simply look these up as instance attributes on `types.ModuleType`. - /// - /// Note that this function should only be used as a fallback if a symbol is being looked - /// up in the global scope **from within the same file**. If the symbol is being looked up - /// from outside the file (e.g. via imports), use [`super::imported_symbol`] (or fallback logic - /// like the logic used in that function) instead. The reason is that this function returns - /// [`Symbol::Unbound`] for `__init__` and `__dict__` (which cannot be found in globals if - /// the lookup is being done from the same file) -- but these symbols *are* available in the - /// global scope if they're being imported **from a different file**. - pub(crate) fn module_type_implicit_global_symbol<'db>( - db: &'db dyn Db, - name: &str, - ) -> SymbolAndQualifiers<'db> { - // In general we wouldn't check to see whether a symbol exists on a class before doing the - // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance - // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` - // when absolutely necessary is that this function is used in a very hot path (name resolution - // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. - if module_type_symbols(db) - .iter() - .any(|module_type_member| &**module_type_member == name) - { - KnownClass::ModuleType.to_instance(db).member(db, name) - } else { - Symbol::Unbound.into() - } - } - - /// An internal micro-optimisation for `module_type_implicit_global_symbol`. - /// - /// This function returns a list of the symbols that typeshed declares in the - /// body scope of the stub for the class `types.ModuleType`. - /// - /// The returned list excludes the attributes `__dict__` and `__init__`. These are very - /// special members that can be accessed as attributes on the module when imported, - /// but cannot be accessed as globals *inside* the module. - /// - /// The list also excludes `__getattr__`. `__getattr__` is even more special: it doesn't - /// exist at runtime, but typeshed includes it to reduce false positives associated with - /// functions that dynamically import modules and return `Instance(types.ModuleType)`. - /// We should ignore it for any known module-literal type. - /// - /// Conceptually this function could be a `Set` rather than a list, - /// but the number of symbols declared in this scope is likely to be very small, - /// so the cost of hashing the names is likely to be more expensive than it's worth. - #[salsa::tracked(return_ref)] - fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { - let Some(module_type) = KnownClass::ModuleType - .to_class_literal(db) - .into_class_literal() - else { - // The most likely way we get here is if a user specified a `--custom-typeshed-dir` - // without a `types.pyi` stub in the `stdlib/` directory - return smallvec::SmallVec::default(); - }; - - let module_type_scope = module_type.body_scope(db); - let module_type_symbol_table = symbol_table(db, module_type_scope); - - module_type_symbol_table - .symbols() - .filter(|symbol| symbol.is_declared()) - .map(semantic_index::symbol::Symbol::name) - .filter(|symbol_name| { - !matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__") - }) - .cloned() - .collect() - } - - #[cfg(test)] - mod tests { - use super::*; - use crate::db::tests::setup_db; - - #[test] - fn module_type_symbols_includes_declared_types_but_not_referenced_types() { - let db = setup_db(); - let symbol_names = module_type_symbols(&db); - - let dunder_name_symbol_name = ast::name::Name::new_static("__name__"); - assert!(symbol_names.contains(&dunder_name_symbol_name)); - - let property_symbol_name = ast::name::Name::new_static("property"); - assert!(!symbol_names.contains(&property_symbol_name)); - } - } -} - -/// Implementation of looking up a module-global symbol as seen from outside the file (e.g. via -/// imports). -/// -/// This will take into account whether the definition of the symbol is being explicitly -/// re-exported from a stub file or not. -fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> SymbolAndQualifiers<'db> { - symbol_impl( - db, - global_scope(db, file), - name, - if file.is_stub(db.upcast()) { - RequiresExplicitReExport::Yes - } else { - RequiresExplicitReExport::No - }, - ) -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -enum RequiresExplicitReExport { - Yes, - No, -} - -impl RequiresExplicitReExport { - const fn is_yes(self) -> bool { - matches!(self, RequiresExplicitReExport::Yes) - } -} - -/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred` -/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is -/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses -/// of symbols that have no declared type. -fn widen_type_for_undeclared_public_symbol<'db>( - db: &'db dyn Db, - inferred: Symbol<'db>, - is_considered_non_modifiable: bool, -) -> Symbol<'db> { - // We special-case known-instance types here since symbols like `typing.Any` are typically - // not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as - // such. - let is_known_instance = inferred - .ignore_possibly_unbound() - .is_some_and(|ty| matches!(ty, Type::KnownInstance(_))); - - if is_considered_non_modifiable || is_known_instance { - inferred - } else { - inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty])) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::db::tests::setup_db; - - #[test] - fn test_symbol_or_fall_back_to() { - use Boundness::{Bound, PossiblyUnbound}; - - let db = setup_db(); - let ty1 = Type::IntLiteral(1); - let ty2 = Type::IntLiteral(2); - - let unbound = || Symbol::Unbound.with_qualifiers(TypeQualifiers::empty()); - - let possibly_unbound_ty1 = - || Symbol::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); - let possibly_unbound_ty2 = - || Symbol::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); - - let bound_ty1 = || Symbol::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty()); - let bound_ty2 = || Symbol::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty()); - - // Start from an unbound symbol - assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound()); - assert_eq!( - unbound().or_fall_back_to(&db, possibly_unbound_ty1), - possibly_unbound_ty1() - ); - assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1()); - - // Start from a possibly unbound symbol - assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, unbound), - possibly_unbound_ty1() - ); - assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), - Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into() - ); - assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2), - Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into() - ); - - // Start from a definitely bound symbol - assert_eq!(bound_ty1().or_fall_back_to(&db, unbound), bound_ty1()); - assert_eq!( - bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), - bound_ty1() - ); - assert_eq!(bound_ty1().or_fall_back_to(&db, bound_ty2), bound_ty1()); - } - - #[track_caller] - fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Symbol<'db>) { - assert!(matches!( - symbol, - Symbol::Type(Type::Instance(_), Boundness::Bound) - )); - assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); - } - - #[test] - fn implicit_builtin_globals() { - let db = setup_db(); - assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").symbol); - } - - #[test] - fn implicit_typing_globals() { - let db = setup_db(); - assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").symbol); - } - - #[test] - fn implicit_typing_extensions_globals() { - let db = setup_db(); - assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").symbol); - } - - #[test] - fn implicit_sys_globals() { - let db = setup_db(); - assert_bound_string_symbol( - &db, - known_module_symbol(&db, KnownModule::Sys, "__name__").symbol, - ); - } -} diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs deleted file mode 100644 index ed0174d27392a..0000000000000 --- a/crates/red_knot_python_semantic/src/types.rs +++ /dev/null @@ -1,7224 +0,0 @@ -use itertools::Either; - -use std::slice::Iter; -use std::str::FromStr; - -use bitflags::bitflags; -use call::{CallDunderError, CallError, CallErrorKind}; -use context::InferContext; -use diagnostic::{ - CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, - UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, -}; -use ruff_db::files::{File, FileRange}; -use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, AnyNodeRef}; -use ruff_text_size::{Ranged, TextRange}; -use type_ordering::union_or_intersection_elements_ordering; - -pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; -pub(crate) use self::diagnostic::register_lints; -pub use self::diagnostic::TypeCheckDiagnostics; -pub(crate) use self::display::TypeArrayDisplay; -pub(crate) use self::infer::{ - infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, - infer_scope_types, -}; -pub(crate) use self::narrow::KnownConstraintFunction; -pub(crate) use self::signatures::{CallableSignature, Signature, Signatures}; -pub(crate) use self::subclass_of::SubclassOfType; -use crate::module_name::ModuleName; -use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; -use crate::semantic_index::ast_ids::HasScopedExpressionId; -use crate::semantic_index::definition::Definition; -use crate::semantic_index::symbol::ScopeId; -use crate::semantic_index::{imported_modules, semantic_index}; -use crate::suppression::check_suppressions; -use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; -use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding}; -pub(crate) use crate::types::class_base::ClassBase; -use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; -use crate::types::generics::{GenericContext, Specialization}; -use crate::types::infer::infer_unpack_types; -use crate::types::mro::{Mro, MroError, MroIterator}; -pub(crate) use crate::types::narrow::infer_narrowing_constraint; -use crate::types::signatures::{Parameter, ParameterForm, Parameters}; -use crate::{Db, FxOrderSet, Module, Program}; -pub(crate) use class::{ - Class, ClassLiteralType, ClassType, GenericAlias, GenericClass, InstanceType, KnownClass, - KnownInstanceType, NonGenericClass, -}; - -mod builder; -mod call; -mod class; -mod class_base; -mod context; -mod diagnostic; -mod display; -mod generics; -mod infer; -mod mro; -mod narrow; -mod signatures; -mod slots; -mod string_annotation; -mod subclass_of; -mod type_ordering; -mod unpacker; - -mod definition; -#[cfg(test)] -mod property_tests; - -#[salsa::tracked(return_ref)] -pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics { - let _span = tracing::trace_span!("check_types", ?file).entered(); - - tracing::debug!("Checking file '{path}'", path = file.path(db)); - - let index = semantic_index(db, file); - let mut diagnostics = TypeCheckDiagnostics::default(); - - for scope_id in index.scope_ids() { - let result = infer_scope_types(db, scope_id); - diagnostics.extend(result.diagnostics()); - } - - check_suppressions(db, file, &mut diagnostics); - - diagnostics -} - -/// Infer the type of a binding. -pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> { - let inference = infer_definition_types(db, definition); - inference.binding_type(definition) -} - -/// Infer the type of a declaration. -pub(crate) fn declaration_type<'db>( - db: &'db dyn Db, - definition: Definition<'db>, -) -> TypeAndQualifiers<'db> { - let inference = infer_definition_types(db, definition); - inference.declaration_type(definition) -} - -/// Infer the type of a (possibly deferred) sub-expression of a [`Definition`]. -/// -/// Supports expressions that are evaluated within a type-params sub-scope. -/// -/// ## Panics -/// If the given expression is not a sub-expression of the given [`Definition`]. -fn definition_expression_type<'db>( - db: &'db dyn Db, - definition: Definition<'db>, - expression: &ast::Expr, -) -> Type<'db> { - let file = definition.file(db); - let index = semantic_index(db, file); - let file_scope = index.expression_scope_id(expression); - let scope = file_scope.to_scope_id(db, file); - let expr_id = expression.scoped_expression_id(db, scope); - if scope == definition.scope(db) { - // expression is in the definition scope - let inference = infer_definition_types(db, definition); - if let Some(ty) = inference.try_expression_type(expr_id) { - ty - } else { - infer_deferred_types(db, definition).expression_type(expr_id) - } - } else { - // expression is in a type-params sub-scope - infer_scope_types(db, scope).expression_type(expr_id) - } -} - -/// The descriptor protocol distinguishes two kinds of descriptors. Non-data descriptors -/// define a `__get__` method, while data descriptors additionally define a `__set__` -/// method or a `__delete__` method. This enum is used to categorize attributes into two -/// groups: (1) data descriptors and (2) normal attributes or non-data descriptors. -#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, salsa::Update)] -enum AttributeKind { - DataDescriptor, - NormalOrNonDataDescriptor, -} - -/// This enum is used to control the behavior of the descriptor protocol implementation. -/// When invoked on a class object, the fallback type (a class attribute) can shadow a -/// non-data descriptor of the meta-type (the class's metaclass). However, this is not -/// true for instances. When invoked on an instance, the fallback type (an attribute on -/// the instance) can not completely shadow a non-data descriptor of the meta-type (the -/// class), because we do not currently attempt to statically infer if an instance -/// attribute is definitely defined (i.e. to check whether a particular method has been -/// called). -#[derive(Clone, Debug, Copy, PartialEq)] -enum InstanceFallbackShadowsNonDataDescriptor { - Yes, - No, -} - -bitflags! { - #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)] - pub(crate) struct MemberLookupPolicy: u8 { - /// Dunder methods are looked up on the meta-type of a type without potentially falling - /// back on attributes on the type itself. For example, when implicitly invoked on an - /// instance, dunder methods are not looked up as instance attributes. And when invoked - /// on a class, dunder methods are only looked up on the metaclass, not the class itself. - /// - /// All other attributes use the `WithInstanceFallback` policy. - /// - /// If this flag is set - look up the attribute on the meta-type only. - const NO_INSTANCE_FALLBACK = 1 << 0; - - /// When looking up an attribute on a class, we sometimes need to avoid - /// looking up attributes defined on the `object` class. Usually because - /// typeshed doesn't properly encode runtime behavior (e.g. see how `__new__` & `__init__` - /// are handled during class creation). - /// - /// If this flag is set - exclude attributes defined on `object` when looking up attributes. - const MRO_NO_OBJECT_FALLBACK = 1 << 1; - - /// When looking up an attribute on a class, we sometimes need to avoid - /// looking up attributes defined on `type` if this is the metaclass of the class. - /// - /// This is similar to no object fallback above - const META_CLASS_NO_TYPE_FALLBACK = 1 << 2; - } -} - -impl MemberLookupPolicy { - /// Only look up the attribute on the meta-type. - /// - /// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance - /// if the meta-type attribute is not found or if the meta-type attribute is not a data - /// descriptor. - pub(crate) const fn no_instance_fallback(self) -> bool { - self.contains(Self::NO_INSTANCE_FALLBACK) - } - - /// Exclude attributes defined on `object` when looking up attributes. - pub(crate) const fn mro_no_object_fallback(self) -> bool { - self.contains(Self::MRO_NO_OBJECT_FALLBACK) - } - - /// Exclude attributes defined on `type` when looking up meta-class-attributes. - pub(crate) const fn meta_class_no_type_fallback(self) -> bool { - self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) - } -} - -impl Default for MemberLookupPolicy { - fn default() -> Self { - Self::empty() - } -} - -impl AttributeKind { - const fn is_data(self) -> bool { - matches!(self, Self::DataDescriptor) - } -} - -/// Meta data for `Type::Todo`, which represents a known limitation in red-knot. -#[cfg(debug_assertions)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct TodoType(pub &'static str); - -#[cfg(debug_assertions)] -impl std::fmt::Display for TodoType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({msg})", msg = self.0) - } -} - -#[cfg(not(debug_assertions))] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct TodoType; - -#[cfg(not(debug_assertions))] -impl std::fmt::Display for TodoType { - fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} - -/// Create a `Type::Todo` variant to represent a known limitation in the type system. -/// -/// It can be created by specifying a custom message: `todo_type!("PEP 604 not supported")`. -#[cfg(debug_assertions)] -macro_rules! todo_type { - ($message:literal) => {{ - const _: () = { - let s = $message; - - if !s.is_ascii() { - panic!("todo_type! message must be ASCII"); - } - - let bytes = s.as_bytes(); - let mut i = 0; - while i < bytes.len() { - // Check each byte for '(' or ')' - let ch = bytes[i]; - - assert!( - !40u8.eq_ignore_ascii_case(&ch) && !41u8.eq_ignore_ascii_case(&ch), - "todo_type! message must not contain parentheses", - ); - i += 1; - } - }; - $crate::types::Type::Dynamic($crate::types::DynamicType::Todo($crate::types::TodoType( - $message, - ))) - }}; - ($message:ident) => { - $crate::types::Type::Dynamic($crate::types::DynamicType::Todo($crate::types::TodoType( - $message, - ))) - }; -} - -#[cfg(not(debug_assertions))] -macro_rules! todo_type { - () => { - $crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType)) - }; - ($message:literal) => { - $crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType)) - }; - ($message:ident) => { - $crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType)) - }; -} - -pub use crate::types::definition::TypeDefinition; -pub(crate) use todo_type; - -/// Represents an instance of `builtins.property`. -#[salsa::interned(debug)] -pub struct PropertyInstanceType<'db> { - getter: Option>, - setter: Option>, -} - -impl<'db> PropertyInstanceType<'db> { - fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let getter = self - .getter(db) - .map(|ty| ty.apply_specialization(db, specialization)); - let setter = self - .setter(db) - .map(|ty| ty.apply_specialization(db, specialization)); - Self::new(db, getter, setter) - } -} - -bitflags! { - /// Used as the return type of `dataclass(…)` calls. Keeps track of the arguments - /// that were passed in. For the precise meaning of the fields, see [1]. - /// - /// [1]: https://docs.python.org/3/library/dataclasses.html - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct DataclassMetadata: u16 { - const INIT = 0b0000_0000_0001; - const REPR = 0b0000_0000_0010; - const EQ = 0b0000_0000_0100; - const ORDER = 0b0000_0000_1000; - const UNSAFE_HASH = 0b0000_0001_0000; - const FROZEN = 0b0000_0010_0000; - const MATCH_ARGS = 0b0000_0100_0000; - const KW_ONLY = 0b0000_1000_0000; - const SLOTS = 0b0001_0000_0000; - const WEAKREF_SLOT = 0b0010_0000_0000; - } -} - -impl Default for DataclassMetadata { - fn default() -> Self { - Self::INIT | Self::REPR | Self::EQ | Self::MATCH_ARGS - } -} - -/// Representation of a type: a set of possible values at runtime. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub enum Type<'db> { - /// The dynamic type: a statically unknown set of values - Dynamic(DynamicType), - /// The empty set of values - Never, - /// A specific function object - FunctionLiteral(FunctionType<'db>), - /// Represents a callable `instance.method` where `instance` is an instance of a class - /// and `method` is a method (of that class). - /// - /// See [`BoundMethodType`] for more information. - /// - /// TODO: consider replacing this with `Callable & Instance(MethodType)`? - /// I.e. if we have a method `def f(self, x: int) -> str`, and see it being called as - /// `instance.f`, we could partially apply (and check) the `instance` argument against - /// the `self` parameter, and return a `MethodType & Callable[[int], str]`. - /// One drawback would be that we could not show the bound instance when that type is displayed. - BoundMethod(BoundMethodType<'db>), - /// Represents a specific instance of `types.MethodWrapperType`. - /// - /// TODO: consider replacing this with `Callable & types.MethodWrapperType` type? - /// Requires `Callable` to be able to represent overloads, e.g. `types.FunctionType.__get__` has - /// this behaviour when a method is accessed on a class vs an instance: - /// - /// ```txt - /// * (None, type) -> Literal[function_on_which_it_was_called] - /// * (object, type | None) -> BoundMethod[instance, function_on_which_it_was_called] - /// ``` - MethodWrapper(MethodWrapperKind<'db>), - /// Represents a specific instance of `types.WrapperDescriptorType`. - /// - /// TODO: Similar to above, this could eventually be replaced by a generic `Callable` - /// type. We currently add this as a separate variant because `FunctionType.__get__` - /// is an overloaded method and we do not support `@overload` yet. - WrapperDescriptor(WrapperDescriptorKind), - /// A special callable that is returned by a `dataclass(…)` call. It is usually - /// used as a decorator. Note that this is only used as a return type for actual - /// `dataclass` calls, not for the argumentless `@dataclass` decorator. - DataclassDecorator(DataclassMetadata), - /// The type of an arbitrary callable object with a certain specified signature. - Callable(CallableType<'db>), - /// A specific module object - ModuleLiteral(ModuleLiteralType<'db>), - /// A specific class object - ClassLiteral(ClassLiteralType<'db>), - /// A specialization of a generic class - GenericAlias(GenericAlias<'db>), - /// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`. - SubclassOf(SubclassOfType<'db>), - /// The set of Python objects with the given class in their __class__'s method resolution order - Instance(InstanceType<'db>), - /// A single Python object that requires special treatment in the type system - KnownInstance(KnownInstanceType<'db>), - /// An instance of `builtins.property` - PropertyInstance(PropertyInstanceType<'db>), - /// The set of objects in any of the types in the union - Union(UnionType<'db>), - /// The set of objects in all of the types in the intersection - Intersection(IntersectionType<'db>), - /// Represents objects whose `__bool__` method is deterministic: - /// - `AlwaysTruthy`: `__bool__` always returns `True` - /// - `AlwaysFalsy`: `__bool__` always returns `False` - AlwaysTruthy, - AlwaysFalsy, - /// An integer literal - IntLiteral(i64), - /// A boolean literal, either `True` or `False`. - BooleanLiteral(bool), - /// A string literal whose value is known - StringLiteral(StringLiteralType<'db>), - /// A string known to originate only from literal values, but whose value is not known (unlike - /// `StringLiteral` above). - LiteralString, - /// A bytes literal - BytesLiteral(BytesLiteralType<'db>), - /// A slice literal, e.g. `1:5`, `10:0:-1` or `:` - SliceLiteral(SliceLiteralType<'db>), - /// A heterogeneous tuple type, with elements of the given types in source order. - // TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`. - Tuple(TupleType<'db>), - /// An instance of a typevar in a generic class or function. When the generic class or function - /// is specialized, we will replace this typevar with its specialization. - TypeVar(TypeVarInstance<'db>), - // A bound super object like `super()` or `super(A, A())` - // This type doesn't handle an unbound super object like `super(A)`; for that we just use - // a `Type::Instance` of `builtins.super`. - BoundSuper(BoundSuperType<'db>), - // TODO protocols, overloads, generics -} - -#[salsa::tracked] -impl<'db> Type<'db> { - pub const fn any() -> Self { - Self::Dynamic(DynamicType::Any) - } - - pub const fn unknown() -> Self { - Self::Dynamic(DynamicType::Unknown) - } - - pub fn object(db: &'db dyn Db) -> Self { - KnownClass::Object.to_instance(db) - } - - pub const fn is_unknown(&self) -> bool { - matches!(self, Type::Dynamic(DynamicType::Unknown)) - } - - pub const fn is_never(&self) -> bool { - matches!(self, Type::Never) - } - - fn is_none(&self, db: &'db dyn Db) -> bool { - self.into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) - } - - pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) - } - - pub fn is_object(&self, db: &'db dyn Db) -> bool { - self.into_instance() - .is_some_and(|instance| instance.class.is_object(db)) - } - - pub const fn is_todo(&self) -> bool { - matches!(self, Type::Dynamic(DynamicType::Todo(_))) - } - - pub fn contains_todo(&self, db: &'db dyn Db) -> bool { - match self { - Self::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) => true, - - Self::AlwaysFalsy - | Self::AlwaysTruthy - | Self::Never - | Self::BooleanLiteral(_) - | Self::BytesLiteral(_) - | Self::FunctionLiteral(_) - | Self::Instance(_) - | Self::ModuleLiteral(_) - | Self::ClassLiteral(_) - | Self::KnownInstance(_) - | Self::PropertyInstance(_) - | Self::StringLiteral(_) - | Self::IntLiteral(_) - | Self::LiteralString - | Self::SliceLiteral(_) - | Self::Dynamic(DynamicType::Unknown | DynamicType::Any) - | Self::BoundMethod(_) - | Self::WrapperDescriptor(_) - | Self::MethodWrapper(_) - | Self::DataclassDecorator(_) => false, - - Self::GenericAlias(generic) => generic - .specialization(db) - .types(db) - .iter() - .any(|ty| ty.contains_todo(db)), - - Self::Callable(callable) => { - let signature = callable.signature(db); - signature.parameters().iter().any(|param| { - param - .annotated_type() - .is_some_and(|ty| ty.contains_todo(db)) - }) || signature.return_ty.is_some_and(|ty| ty.contains_todo(db)) - } - - Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) => true, - ClassBase::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, - ClassBase::Class(_) => false, - }, - - Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.contains_todo(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .any(|constraint| constraint.contains_todo(db)), - }, - - Self::BoundSuper(bound_super) => { - matches!( - bound_super.pivot_class(db), - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) - ) || matches!( - bound_super.owner(db), - SuperOwnerKind::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) - ) - } - - Self::Tuple(tuple) => tuple.elements(db).iter().any(|ty| ty.contains_todo(db)), - - Self::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)), - - Self::Intersection(intersection) => { - intersection - .positive(db) - .iter() - .any(|ty| ty.contains_todo(db)) - || intersection - .negative(db) - .iter() - .any(|ty| ty.contains_todo(db)) - } - } - } - - pub const fn into_class_literal(self) -> Option> { - match self { - Type::ClassLiteral(class_type) => Some(class_type), - _ => None, - } - } - - #[track_caller] - pub fn expect_class_literal(self) -> ClassLiteralType<'db> { - self.into_class_literal() - .expect("Expected a Type::ClassLiteral variant") - } - - pub const fn is_subclass_of(&self) -> bool { - matches!(self, Type::SubclassOf(..)) - } - - pub const fn is_class_literal(&self) -> bool { - matches!(self, Type::ClassLiteral(..)) - } - - pub const fn into_class_type(self) -> Option> { - match self { - Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { - Some(ClassType::NonGeneric(non_generic)) - } - Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), - _ => None, - } - } - - #[track_caller] - pub fn expect_class_type(self) -> ClassType<'db> { - self.into_class_type() - .expect("Expected a Type::GenericAlias or non-generic Type::ClassLiteral variant") - } - - pub const fn is_class_type(&self) -> bool { - matches!( - self, - Type::ClassLiteral(ClassLiteralType::NonGeneric(_)) | Type::GenericAlias(_) - ) - } - - pub const fn is_instance(&self) -> bool { - matches!(self, Type::Instance(..)) - } - - pub const fn is_property_instance(&self) -> bool { - matches!(self, Type::PropertyInstance(..)) - } - - pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self { - Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule)) - } - - pub const fn into_module_literal(self) -> Option> { - match self { - Type::ModuleLiteral(module) => Some(module), - _ => None, - } - } - - #[track_caller] - pub fn expect_module_literal(self) -> ModuleLiteralType<'db> { - self.into_module_literal() - .expect("Expected a Type::ModuleLiteral variant") - } - - pub const fn into_union(self) -> Option> { - match self { - Type::Union(union_type) => Some(union_type), - _ => None, - } - } - - #[track_caller] - pub fn expect_union(self) -> UnionType<'db> { - self.into_union().expect("Expected a Type::Union variant") - } - - pub const fn is_union(&self) -> bool { - matches!(self, Type::Union(..)) - } - - pub const fn into_intersection(self) -> Option> { - match self { - Type::Intersection(intersection_type) => Some(intersection_type), - _ => None, - } - } - - #[track_caller] - pub fn expect_intersection(self) -> IntersectionType<'db> { - self.into_intersection() - .expect("Expected a Type::Intersection variant") - } - - pub const fn into_function_literal(self) -> Option> { - match self { - Type::FunctionLiteral(function_type) => Some(function_type), - _ => None, - } - } - - #[track_caller] - pub fn expect_function_literal(self) -> FunctionType<'db> { - self.into_function_literal() - .expect("Expected a Type::FunctionLiteral variant") - } - - pub const fn is_function_literal(&self) -> bool { - matches!(self, Type::FunctionLiteral(..)) - } - - pub const fn is_bound_method(&self) -> bool { - matches!(self, Type::BoundMethod(..)) - } - - pub fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { - self.into_union() - .is_some_and(|union| union.elements(db).iter().all(|ty| ty.is_single_valued(db))) - } - - pub const fn into_int_literal(self) -> Option { - match self { - Type::IntLiteral(value) => Some(value), - _ => None, - } - } - - pub fn into_string_literal(self) -> Option> { - match self { - Type::StringLiteral(string_literal) => Some(string_literal), - _ => None, - } - } - - pub fn is_string_literal(&self) -> bool { - matches!(self, Type::StringLiteral(..)) - } - - #[track_caller] - pub fn expect_int_literal(self) -> i64 { - self.into_int_literal() - .expect("Expected a Type::IntLiteral variant") - } - - pub const fn into_instance(self) -> Option> { - match self { - Type::Instance(instance_type) => Some(instance_type), - _ => None, - } - } - - pub const fn into_known_instance(self) -> Option> { - match self { - Type::KnownInstance(known_instance) => Some(known_instance), - _ => None, - } - } - - #[track_caller] - pub fn expect_known_instance(self) -> KnownInstanceType<'db> { - self.into_known_instance() - .expect("Expected a Type::KnownInstance variant") - } - - pub const fn into_tuple(self) -> Option> { - match self { - Type::Tuple(tuple_type) => Some(tuple_type), - _ => None, - } - } - - pub const fn is_boolean_literal(&self) -> bool { - matches!(self, Type::BooleanLiteral(..)) - } - - pub const fn is_literal_string(&self) -> bool { - matches!(self, Type::LiteralString) - } - - pub const fn instance(class: ClassType<'db>) -> Self { - Self::Instance(InstanceType { class }) - } - - pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { - Self::StringLiteral(StringLiteralType::new(db, string)) - } - - pub fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { - Self::BytesLiteral(BytesLiteralType::new(db, bytes)) - } - - #[must_use] - pub fn negate(&self, db: &'db dyn Db) -> Type<'db> { - IntersectionBuilder::new(db).add_negative(*self).build() - } - - #[must_use] - pub fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { - if yes { - self.negate(db) - } else { - *self - } - } - - /// Return a "normalized" version of `self` that ensures that equivalent types have the same Salsa ID. - /// - /// A normalized type: - /// - Has all unions and intersections sorted according to a canonical order, - /// no matter how "deeply" a union/intersection may be nested. - /// - Strips the names of positional-only parameters and variadic parameters from `Callable` types, - /// as these are irrelevant to whether a callable type `X` is equivalent to a callable type `Y`. - /// - Strips the types of default values from parameters in `Callable` types: only whether a parameter - /// *has* or *does not have* a default value is relevant to whether two `Callable` types are equivalent. - #[must_use] - pub fn normalized(self, db: &'db dyn Db) -> Self { - match self { - Type::Union(union) => Type::Union(union.normalized(db)), - Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), - Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), - Type::Callable(callable) => Type::Callable(callable.normalized(db)), - Type::LiteralString - | Type::Instance(_) - | Type::PropertyInstance(_) - | Type::AlwaysFalsy - | Type::AlwaysTruthy - | Type::BooleanLiteral(_) - | Type::SliceLiteral(_) - | Type::BytesLiteral(_) - | Type::StringLiteral(_) - | Type::Dynamic(_) - | Type::Never - | Type::FunctionLiteral(_) - | Type::MethodWrapper(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Self::DataclassDecorator(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::KnownInstance(_) - | Type::IntLiteral(_) - | Type::BoundSuper(_) - | Type::SubclassOf(_) => self, - Type::GenericAlias(generic) => { - let specialization = generic.specialization(db).normalized(db); - Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) - } - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - Type::TypeVar(TypeVarInstance::new( - db, - typevar.name(db).clone(), - typevar.definition(db), - Some(TypeVarBoundOrConstraints::UpperBound(bound.normalized(db))), - typevar.default_ty(db), - )) - } - Some(TypeVarBoundOrConstraints::Constraints(union)) => { - Type::TypeVar(TypeVarInstance::new( - db, - typevar.name(db).clone(), - typevar.definition(db), - Some(TypeVarBoundOrConstraints::Constraints(union.normalized(db))), - typevar.default_ty(db), - )) - } - None => self, - }, - } - } - - /// Return true if this type is a [subtype of] type `target`. - /// - /// This method returns `false` if either `self` or `other` is not fully static. - /// - /// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { - // Two equivalent types are always subtypes of each other. - // - // "Equivalent to" here means that the two types are both fully static - // and describe exactly the same set of possible runtime objects. - // For example, `int` is a subtype of `int` because `int` and `int` are equivalent to each other. - // Equally, `type[object]` is a subtype of `type`, - // because the former type expresses "all subclasses of `object`" - // while the latter expresses "all instances of `type`", - // and these are exactly the same set of objects at runtime. - if self.is_equivalent_to(db, target) { - return true; - } - - // Non-fully-static types do not participate in subtyping. - // - // Type `A` can only be a subtype of type `B` if the set of possible runtime objects - // that `A` represents is a subset of the set of possible runtime objects that `B` represents. - // But the set of objects described by a non-fully-static type is (either partially or wholly) unknown, - // so the question is simply unanswerable for non-fully-static types. - if !self.is_fully_static(db) || !target.is_fully_static(db) { - return false; - } - - match (self, target) { - // We should have handled these immediately above. - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => { - unreachable!("Non-fully-static types do not participate in subtyping!") - } - - // `Never` is the bottom type, the empty set. - // It is a subtype of all other fully static types. - // No other fully static type is a subtype of `Never`. - (Type::Never, _) => true, - (_, Type::Never) => false, - - // Everything is a subtype of `object`. - (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, - - // A fully static typevar is always a subtype of itself, and is never a subtype of any - // other typevar, since there is no guarantee that they will be specialized to the same - // type. (This is true even if both typevars are bounded by the same final class, since - // you can specialize the typevars to `Never` in addition to that final class.) - (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { - self_typevar == other_typevar - } - - // A fully static typevar is a subtype of its upper bound, and to something similar to - // the union of its constraints. An unbound, unconstrained, fully static typevar has an - // implicit upper bound of `object` (which is handled above). - (Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => { - match typevar.bound_or_constraints(db) { - None => unreachable!(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_subtype_of(db, target) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_subtype_of(db, target)), - } - } - - (Type::Union(union), _) => union - .elements(db) - .iter() - .all(|&elem_ty| elem_ty.is_subtype_of(db, target)), - - (_, Type::Union(union)) => union - .elements(db) - .iter() - .any(|&elem_ty| self.is_subtype_of(db, elem_ty)), - - // If the typevar is constrained, there must be multiple constraints, and the typevar - // might be specialized to any one of them. However, the constraints do not have to be - // disjoint, which means an lhs type might be a subtype of all of the constraints. - (_, Type::TypeVar(typevar)) - if typevar.constraints(db).is_some_and(|constraints| { - constraints - .iter() - .all(|constraint| self.is_subtype_of(db, *constraint)) - }) => - { - true - } - - // If both sides are intersections we need to handle the right side first - // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, - // but none of A, B, or C is a subtype of (A & B). - (_, Type::Intersection(intersection)) => { - intersection - .positive(db) - .iter() - .all(|&pos_ty| self.is_subtype_of(db, pos_ty)) - && intersection - .negative(db) - .iter() - .all(|&neg_ty| self.is_disjoint_from(db, neg_ty)) - } - - (Type::Intersection(intersection), _) => intersection - .positive(db) - .iter() - .any(|&elem_ty| elem_ty.is_subtype_of(db, target)), - - // Other than the special cases checked above, no other types are a subtype of a - // typevar, since there's no guarantee what type the typevar will be specialized to. - // (If the typevar is bounded, it might be specialized to a smaller type than the - // bound. This is true even if the bound is a final class, since the typevar can still - // be specialized to `Never`.) - (_, Type::TypeVar(_)) => false, - - // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. - // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. - (left, Type::AlwaysFalsy) => left.bool(db).is_always_false(), - (left, Type::AlwaysTruthy) => left.bool(db).is_always_true(), - // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). - (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { - target.is_equivalent_to(db, Type::object(db)) - } - - // No literal type is a subtype of any other literal type, unless they are the same - // type (which is handled above). This case is not necessary from a correctness - // perspective (the fallback cases below will handle it correctly), but it is important - // for performance of simplifying large unions of literal types. - ( - Type::StringLiteral(_) - | Type::IntLiteral(_) - | Type::BytesLiteral(_) - | Type::ClassLiteral(_) - | Type::FunctionLiteral(_) - | Type::ModuleLiteral(_) - | Type::SliceLiteral(_), - Type::StringLiteral(_) - | Type::IntLiteral(_) - | Type::BytesLiteral(_) - | Type::ClassLiteral(_) - | Type::FunctionLiteral(_) - | Type::ModuleLiteral(_) - | Type::SliceLiteral(_), - ) => false, - - // All `StringLiteral` types are a subtype of `LiteralString`. - (Type::StringLiteral(_), Type::LiteralString) => true, - - // Except for the special `LiteralString` case above, - // most `Literal` types delegate to their instance fallbacks - // unless `self` is exactly equivalent to `target` (handled above) - (Type::StringLiteral(_) | Type::LiteralString, _) => { - KnownClass::Str.to_instance(db).is_subtype_of(db, target) - } - (Type::BooleanLiteral(_), _) => { - KnownClass::Bool.to_instance(db).is_subtype_of(db, target) - } - (Type::IntLiteral(_), _) => KnownClass::Int.to_instance(db).is_subtype_of(db, target), - (Type::BytesLiteral(_), _) => { - KnownClass::Bytes.to_instance(db).is_subtype_of(db, target) - } - (Type::ModuleLiteral(_), _) => KnownClass::ModuleType - .to_instance(db) - .is_subtype_of(db, target), - (Type::SliceLiteral(_), _) => { - KnownClass::Slice.to_instance(db).is_subtype_of(db, target) - } - - (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { - self_function_literal - .into_callable_type(db) - .is_subtype_of(db, target) - } - - (Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method - .into_callable_type(db) - .is_subtype_of(db, target), - - // A `FunctionLiteral` type is a single-valued type like the other literals handled above, - // so it also, for now, just delegates to its instance fallback. - (Type::FunctionLiteral(_), _) => KnownClass::FunctionType - .to_instance(db) - .is_subtype_of(db, target), - - // The same reasoning applies for these special callable types: - (Type::BoundMethod(_), _) => KnownClass::MethodType - .to_instance(db) - .is_subtype_of(db, target), - (Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType - .to_instance(db) - .is_subtype_of(db, target), - (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType - .to_instance(db) - .is_subtype_of(db, target), - - (Type::Callable(self_callable), Type::Callable(other_callable)) => self_callable - .signature(db) - .is_subtype_of(db, other_callable.signature(db)), - - (Type::DataclassDecorator(_), _) => { - // TODO: Implement subtyping using an equivalent `Callable` type. - false - } - - (Type::Callable(_), _) => { - // TODO: Implement subtyping between callable types and other types like - // function literals, bound methods, class literals, `type[]`, etc.) - false - } - - // A fully static heterogeneous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B` - // iff the two tuple types have the same number of elements and each element-type in `A` is a subtype - // of the element-type at the same index in `B`. (Now say that 5 times fast.) - // - // For example: `tuple[bool, bool]` is a subtype of `tuple[int, int]`, - // but `tuple[bool, bool, bool]` is not a subtype of `tuple[int, int]` - (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { - let self_elements = self_tuple.elements(db); - let target_elements = target_tuple.elements(db); - self_elements.len() == target_elements.len() - && self_elements.iter().zip(target_elements).all( - |(self_element, target_element)| { - self_element.is_subtype_of(db, *target_element) - }, - ) - } - - // Other than the special tuple-to-tuple case handled, above, - // tuple subtyping delegates to `Instance(tuple)` in the same way as the literal types. - // - // All heterogeneous tuple types are subtypes of `Instance()`: - // `Instance()` expresses "the set of all possible instances of the class `T`"; - // consequently, `Instance()` expresses "the set of all possible instances of the class `tuple`". - // This type can be spelled in type annotations as `tuple[object, ...]` (since `tuple` is covariant). - // - // Note that this is not the same type as the type spelled in type annotations as `tuple`; - // as that type is equivalent to `type[Any, ...]` (and therefore not a fully static type). - (Type::Tuple(_), _) => KnownClass::Tuple.to_instance(db).is_subtype_of(db, target), - - (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), - (Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target), - - // `Literal[]` is a subtype of `type[B]` if `C` is a subclass of `B`, - // since `type[B]` describes all possible runtime subclasses of the class object `B`. - (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty - .subclass_of() - .into_class() - .is_some_and(|target_class| class.is_subclass_of(db, None, target_class)), - (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty - .subclass_of() - .into_class() - .is_some_and(|target_class| { - ClassType::from(alias).is_subclass_of(db, target_class) - }), - - // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? - (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { - self_subclass_ty.is_subtype_of(db, target_subclass_ty) - } - - // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. - // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object - // is an instance of its metaclass `abc.ABCMeta`. - (Type::ClassLiteral(class), _) => { - class.metaclass_instance_type(db).is_subtype_of(db, target) - } - (Type::GenericAlias(alias), _) => ClassType::from(alias) - .metaclass_instance_type(db) - .is_subtype_of(db, target), - - // `type[str]` (== `SubclassOf("str")` in red-knot) describes all possible runtime subclasses - // of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str` - // is an instance of `type`, and so all possible subclasses of `str` will also be instances of `type`. - // - // Similarly `type[enum.Enum]` is a subtype of `enum.EnumMeta` because `enum.Enum` - // is an instance of `enum.EnumMeta`. `type[Any]` and `type[Unknown]` do not participate in subtyping, - // however, as they are not fully static types. - (Type::SubclassOf(subclass_of_ty), _) => subclass_of_ty - .subclass_of() - .into_class() - .map(|class| class.metaclass_instance_type(db)) - .is_some_and(|metaclass_instance_type| { - metaclass_instance_type.is_subtype_of(db, target) - }), - - // For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::Instance(_SpecialForm)`, - // because `Type::KnownInstance(KnownInstanceType::Type)` is a set with exactly one runtime value in it - // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime. - (Type::KnownInstance(left), right) => { - left.instance_fallback(db).is_subtype_of(db, right) - } - - // `bool` is a subtype of `int`, because `bool` subclasses `int`, - // which means that all instances of `bool` are also instances of `int` - (Type::Instance(self_instance), Type::Instance(target_instance)) => { - self_instance.is_subtype_of(db, target_instance) - } - - (Type::Instance(_), Type::Callable(_)) => { - let call_symbol = self.member(db, "__call__").symbol; - match call_symbol { - Symbol::Type(Type::BoundMethod(call_function), _) => call_function - .into_callable_type(db) - .is_subtype_of(db, target), - _ => false, - } - } - - (Type::PropertyInstance(_), _) => KnownClass::Property - .to_instance(db) - .is_subtype_of(db, target), - (_, Type::PropertyInstance(_)) => { - self.is_subtype_of(db, KnownClass::Property.to_instance(db)) - } - - // Other than the special cases enumerated above, `Instance` types and typevars are - // never subtypes of any other variants - (Type::Instance(_) | Type::TypeVar(_), _) => false, - } - } - - /// Return true if this type is [assignable to] type `target`. - /// - /// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation - pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { - if self.is_gradual_equivalent_to(db, target) { - return true; - } - - match (self, target) { - // Never can be assigned to any type. - (Type::Never, _) => true, - - // The dynamic type is assignable-to and assignable-from any type. - (Type::Dynamic(_), _) => true, - (_, Type::Dynamic(_)) => true, - - // All types are assignable to `object`. - // TODO this special case might be removable once the below cases are comprehensive - (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, - - // A typevar is always assignable to itself, and is never assignable to any other - // typevar, since there is no guarantee that they will be specialized to the same - // type. (This is true even if both typevars are bounded by the same final class, since - // you can specialize the typevars to `Never` in addition to that final class.) - (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) => { - self_typevar == other_typevar - } - - // A typevar is assignable to its upper bound, and to something similar to the union of - // its constraints. An unbound, unconstrained typevar has an implicit upper bound of - // `object` (which is handled above). - (Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => { - match typevar.bound_or_constraints(db) { - None => unreachable!(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_assignable_to(db, target) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_assignable_to(db, target)), - } - } - - // A union is assignable to a type T iff every element of the union is assignable to T. - (Type::Union(union), ty) => union - .elements(db) - .iter() - .all(|&elem_ty| elem_ty.is_assignable_to(db, ty)), - - // A type T is assignable to a union iff T is assignable to any element of the union. - (ty, Type::Union(union)) => union - .elements(db) - .iter() - .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)), - - // If the typevar is constrained, there must be multiple constraints, and the typevar - // might be specialized to any one of them. However, the constraints do not have to be - // disjoint, which means an lhs type might be assignable to all of the constraints. - (_, Type::TypeVar(typevar)) - if typevar.constraints(db).is_some_and(|constraints| { - constraints - .iter() - .all(|constraint| self.is_assignable_to(db, *constraint)) - }) => - { - true - } - - // If both sides are intersections we need to handle the right side first - // (A & B & C) is assignable to (A & B) because the left is assignable to both A and B, - // but none of A, B, or C is assignable to (A & B). - // - // A type S is assignable to an intersection type T if - // S is assignable to all positive elements of T (e.g. `str & int` is assignable to `str & Any`), and - // S is disjoint from all negative elements of T (e.g. `int` is not assignable to Intersection[int, Not[Literal[1]]]). - (ty, Type::Intersection(intersection)) => { - intersection - .positive(db) - .iter() - .all(|&elem_ty| ty.is_assignable_to(db, elem_ty)) - && intersection - .negative(db) - .iter() - .all(|&neg_ty| ty.is_disjoint_from(db, neg_ty)) - } - - // An intersection type S is assignable to a type T if - // Any element of S is assignable to T (e.g. `A & B` is assignable to `A`) - // Negative elements do not have an effect on assignability - if S is assignable to T then S & ~P is also assignable to T. - (Type::Intersection(intersection), ty) => intersection - .positive(db) - .iter() - .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), - - // Other than the special cases checked above, no other types are assignable to a - // typevar, since there's no guarantee what type the typevar will be specialized to. - // (If the typevar is bounded, it might be specialized to a smaller type than the - // bound. This is true even if the bound is a final class, since the typevar can still - // be specialized to `Never`.) - (_, Type::TypeVar(_)) => false, - - // A tuple type S is assignable to a tuple type T if their lengths are the same, and - // each element of S is assignable to the corresponding element of T. - (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { - let self_elements = self_tuple.elements(db); - let target_elements = target_tuple.elements(db); - self_elements.len() == target_elements.len() - && self_elements.iter().zip(target_elements).all( - |(self_element, target_element)| { - self_element.is_assignable_to(db, *target_element) - }, - ) - } - - // This special case is required because the left-hand side tuple might be a - // gradual type, so we can not rely on subtyping. This allows us to assign e.g. - // `tuple[Any, int]` to `tuple`. - (Type::Tuple(_), _) - if KnownClass::Tuple - .to_instance(db) - .is_assignable_to(db, target) => - { - true - } - - // `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can - // materialize to any `type[...]` type. - (Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_)) - if subclass_of_ty.is_dynamic() => - { - true - } - - // All `type[...]` types are assignable to `type[Any]`, because `type[Any]` can - // materialize to any `type[...]` type. - // - // Every class literal type is also assignable to `type[Any]`, because the class - // literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable - // to `type[Any]`. - ( - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_), - Type::SubclassOf(target_subclass_of), - ) if target_subclass_of.is_dynamic() => true, - - // `type[Any]` is assignable to any type that `type[object]` is assignable to, because - // `type[Any]` can materialize to `type[object]`. - // - // `type[Any]` is also assignable to any subtype of `type[object]`, because all - // subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can - // materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to - // `Never`.) - (Type::SubclassOf(subclass_of_ty), Type::Instance(_)) - if subclass_of_ty.is_dynamic() - && (KnownClass::Type - .to_instance(db) - .is_assignable_to(db, target) - || target.is_subtype_of(db, KnownClass::Type.to_instance(db))) => - { - true - } - - // Any type that is assignable to `type[object]` is also assignable to `type[Any]`, - // because `type[Any]` can materialize to `type[object]`. - (Type::Instance(_), Type::SubclassOf(subclass_of_ty)) - if subclass_of_ty.is_dynamic() - && self.is_assignable_to(db, KnownClass::Type.to_instance(db)) => - { - true - } - - // TODO: This is a workaround to avoid false positives (e.g. when checking function calls - // with `SupportsIndex` parameters), which should be removed when we understand protocols. - (lhs, Type::Instance(InstanceType { class })) - if class.is_known(db, KnownClass::SupportsIndex) => - { - match lhs { - Type::Instance(InstanceType { class }) - if matches!( - class.known(db), - Some(KnownClass::Int | KnownClass::SupportsIndex) - ) => - { - true - } - Type::IntLiteral(_) => true, - _ => false, - } - } - - // TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters. - (lhs, Type::Instance(InstanceType { class })) - if class.is_known(db, KnownClass::Sized) => - { - matches!( - lhs.to_meta_type(db).member(db, "__len__"), - SymbolAndQualifiers { - symbol: Symbol::Type(..), - .. - } - ) - } - - (Type::Callable(self_callable), Type::Callable(target_callable)) => self_callable - .signature(db) - .is_assignable_to(db, target_callable.signature(db)), - - (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { - self_function_literal - .into_callable_type(db) - .is_assignable_to(db, target) - } - - (Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method - .into_callable_type(db) - .is_assignable_to(db, target), - - // TODO other types containing gradual forms (e.g. generics containing Any/Unknown) - _ => self.is_subtype_of(db, target), - } - } - - /// Return true if this type is [equivalent to] type `other`. - /// - /// This method returns `false` if either `self` or `other` is not fully static. - /// - /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - // TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc. - - match (self, other) { - (Type::Union(left), Type::Union(right)) => left.is_equivalent_to(db, right), - (Type::Intersection(left), Type::Intersection(right)) => { - left.is_equivalent_to(db, right) - } - (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), - (Type::Callable(left), Type::Callable(right)) => { - left.signature(db).is_equivalent_to(db, right.signature(db)) - } - _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), - } - } - - /// Returns true if both `self` and `other` are the same gradual form - /// (limited to `Any`, `Unknown`, or `Todo`). - pub(crate) fn is_same_gradual_form(self, other: Type<'db>) -> bool { - matches!( - (self, other), - ( - Type::Dynamic(DynamicType::Any), - Type::Dynamic(DynamicType::Any) - ) | ( - Type::Dynamic(DynamicType::Unknown), - Type::Dynamic(DynamicType::Unknown) - ) | ( - Type::Dynamic(DynamicType::Todo(_)), - Type::Dynamic(DynamicType::Todo(_)) - ) - ) - } - - /// Returns true if this type and `other` are gradual equivalent. - /// - /// > Two gradual types `A` and `B` are equivalent - /// > (that is, the same gradual type, not merely consistent with one another) - /// > if and only if all materializations of `A` are also materializations of `B`, - /// > and all materializations of `B` are also materializations of `A`. - /// > - /// > — [Summary of type relations] - /// - /// This powers the `assert_type()` directive. - /// - /// [Summary of type relations]: https://typing.python.org/en/latest/spec/concepts.html#summary-of-type-relations - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - if self == other { - return true; - } - - match (self, other) { - (Type::Dynamic(_), Type::Dynamic(_)) => true, - - (Type::SubclassOf(first), Type::SubclassOf(second)) => { - match (first.subclass_of(), second.subclass_of()) { - (first, second) if first == second => true, - (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => true, - _ => false, - } - } - - (Type::TypeVar(first), Type::TypeVar(second)) => first == second, - - (Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second), - - (Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second), - - (Type::Intersection(first), Type::Intersection(second)) => { - first.is_gradual_equivalent_to(db, second) - } - - (Type::Callable(first), Type::Callable(second)) => first - .signature(db) - .is_gradual_equivalent_to(db, second.signature(db)), - - _ => false, - } - } - - /// Return true if this type and `other` have no common elements. - /// - /// Note: This function aims to have no false positives, but might return - /// wrong `false` answers in some cases. - pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { - match (self, other) { - (Type::Never, _) | (_, Type::Never) => true, - - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false, - - // A typevar is never disjoint from itself, since all occurrences of the typevar must - // be specialized to the same type. (This is an important difference between typevars - // and `Any`!) Different typevars might be disjoint, depending on their bounds and - // constraints, which are handled below. - (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) - if self_typevar == other_typevar => - { - false - } - - // An unbounded typevar is never disjoint from any other type, since it might be - // specialized to any type. A bounded typevar is not disjoint from its bound, and is - // only disjoint from other types if its bound is. A constrained typevar is disjoint - // from a type if all of its constraints are. - (Type::TypeVar(typevar), other) | (other, Type::TypeVar(typevar)) => { - match typevar.bound_or_constraints(db) { - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.is_disjoint_from(db, other) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_disjoint_from(db, other)), - } - } - - (Type::Union(union), other) | (other, Type::Union(union)) => union - .elements(db) - .iter() - .all(|e| e.is_disjoint_from(db, other)), - - // If we have two intersections, we test the positive elements of each one against the other intersection - // Negative elements need a positive element on the other side in order to be disjoint. - // This is similar to what would happen if we tried to build a new intersection that combines the two - (Type::Intersection(self_intersection), Type::Intersection(other_intersection)) => { - self_intersection - .positive(db) - .iter() - .any(|p| p.is_disjoint_from(db, other)) - || other_intersection - .positive(db) - .iter() - .any(|p: &Type<'_>| p.is_disjoint_from(db, self)) - } - - (Type::Intersection(intersection), other) - | (other, Type::Intersection(intersection)) => { - intersection - .positive(db) - .iter() - .any(|p| p.is_disjoint_from(db, other)) - // A & B & Not[C] is disjoint from C - || intersection - .negative(db) - .iter() - .any(|&neg_ty| other.is_subtype_of(db, neg_ty)) - } - - // any single-valued type is disjoint from another single-valued type - // iff the two types are nonequal - ( - left @ (Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::BytesLiteral(..) - | Type::SliceLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::ModuleLiteral(..) - | Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::KnownInstance(..)), - right @ (Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::BytesLiteral(..) - | Type::SliceLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::ModuleLiteral(..) - | Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::KnownInstance(..)), - ) => left != right, - - // One tuple type can be a subtype of another tuple type, - // but we know for sure that any given tuple type is disjoint from all single-valued types - ( - Type::Tuple(..), - Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::DataclassDecorator(..) - | Type::IntLiteral(..) - | Type::SliceLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString, - ) - | ( - Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::DataclassDecorator(..) - | Type::IntLiteral(..) - | Type::SliceLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString, - Type::Tuple(..), - ) => true, - - (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) - | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { - match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), - } - } - - (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) - | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { - match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => { - !ClassType::from(alias_b).is_subclass_of(db, class_a) - } - } - } - - ( - Type::SubclassOf(_), - Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..) - | Type::SliceLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::ModuleLiteral(..), - ) - | ( - Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString - | Type::BytesLiteral(..) - | Type::SliceLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::ModuleLiteral(..), - Type::SubclassOf(_), - ) => true, - - (Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => { - // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. - // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. - ty.bool(db).is_always_false() - } - (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { - // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. - ty.bool(db).is_always_true() - } - - // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, - // so although the type is dynamic we can still determine disjointedness in some situations - (Type::SubclassOf(subclass_of_ty), other) - | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => { - KnownClass::Type.to_instance(db).is_disjoint_from(db, other) - } - ClassBase::Class(class) => class - .metaclass_instance_type(db) - .is_disjoint_from(db, other), - }, - - (Type::KnownInstance(known_instance), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => { - !known_instance.is_instance_of(db, class) - } - - (known_instance_ty @ Type::KnownInstance(_), Type::Tuple(_)) - | (Type::Tuple(_), known_instance_ty @ Type::KnownInstance(_)) => { - known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) - } - - (Type::BooleanLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => { - // A `Type::BooleanLiteral()` must be an instance of exactly `bool` - // (it cannot be an instance of a `bool` subclass) - !KnownClass::Bool.is_subclass_of(db, class) - } - - (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, - - (Type::IntLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::IntLiteral(..)) => { - // A `Type::IntLiteral()` must be an instance of exactly `int` - // (it cannot be an instance of an `int` subclass) - !KnownClass::Int.is_subclass_of(db, class) - } - - (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, - - (Type::StringLiteral(..), Type::LiteralString) - | (Type::LiteralString, Type::StringLiteral(..)) => false, - - ( - Type::StringLiteral(..) | Type::LiteralString, - Type::Instance(InstanceType { class }), - ) - | ( - Type::Instance(InstanceType { class }), - Type::StringLiteral(..) | Type::LiteralString, - ) => { - // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` - // (it cannot be an instance of a `str` subclass) - !KnownClass::Str.is_subclass_of(db, class) - } - - (Type::LiteralString, Type::LiteralString) => false, - (Type::LiteralString, _) | (_, Type::LiteralString) => true, - - (Type::BytesLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::BytesLiteral(..)) => { - // A `Type::BytesLiteral()` must be an instance of exactly `bytes` - // (it cannot be an instance of a `bytes` subclass) - !KnownClass::Bytes.is_subclass_of(db, class) - } - - (Type::SliceLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::SliceLiteral(..)) => { - // A `Type::SliceLiteral` must be an instance of exactly `slice` - // (it cannot be an instance of a `slice` subclass) - !KnownClass::Slice.is_subclass_of(db, class) - } - - // A class-literal type `X` is always disjoint from an instance type `Y`, - // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, - // where `Z` is `X`'s metaclass. - (Type::ClassLiteral(class), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::ClassLiteral(class)) => !class - .metaclass_instance_type(db) - .is_subtype_of(db, instance), - (Type::GenericAlias(alias), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::GenericAlias(alias)) => !ClassType::from(alias) - .metaclass_instance_type(db) - .is_subtype_of(db, instance), - - (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => { - // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` - // (it cannot be an instance of a `types.FunctionType` subclass) - !KnownClass::FunctionType.is_subclass_of(db, class) - } - - (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType - .to_instance(db) - .is_disjoint_from(db, other), - - (Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => { - KnownClass::MethodWrapperType - .to_instance(db) - .is_disjoint_from(db, other) - } - - (Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => { - KnownClass::WrapperDescriptorType - .to_instance(db) - .is_disjoint_from(db, other) - } - - (Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_)) - | (Type::Callable(_), Type::FunctionLiteral(_)) => { - // No two callable types are ever disjoint because - // `(*args: object, **kwargs: object) -> Never` is a subtype of all fully static - // callable types. - false - } - - ( - Type::Callable(_), - Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::SliceLiteral(_), - ) - | ( - Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::SliceLiteral(_), - Type::Callable(_), - ) => { - // A callable type is disjoint from other literal types. For example, - // `Type::StringLiteral` must be an instance of exactly `str`, not a subclass - // of `str`, and `str` is not callable. The same applies to other literal types. - true - } - - (Type::Callable(_) | Type::DataclassDecorator(_), _) - | (_, Type::Callable(_) | Type::DataclassDecorator(_)) => { - // TODO: Implement disjointness for general callable type with other types - false - } - - (Type::ModuleLiteral(..), other @ Type::Instance(..)) - | (other @ Type::Instance(..), Type::ModuleLiteral(..)) => { - // Modules *can* actually be instances of `ModuleType` subclasses - other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db)) - } - - ( - Type::Instance(InstanceType { class: left_class }), - Type::Instance(InstanceType { class: right_class }), - ) => { - (left_class.is_final(db) && !left_class.is_subclass_of(db, right_class)) - || (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class)) - } - - (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { - let self_elements = tuple.elements(db); - let other_elements = other_tuple.elements(db); - self_elements.len() != other_elements.len() - || self_elements - .iter() - .zip(other_elements) - .any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) - } - - (Type::Tuple(..), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::Tuple(..)) => { - // We cannot be sure if the tuple is disjoint from the instance because: - // - 'other' might be the homogeneous arbitrary-length tuple type - // tuple[T, ...] (which we don't have support for yet); if all of - // our element types are not disjoint with T, this is not disjoint - // - 'other' might be a user subtype of tuple, which, if generic - // over the same or compatible *Ts, would overlap with tuple. - // - // TODO: add checks for the above cases once we support them - instance.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) - } - - (Type::PropertyInstance(_), _) | (_, Type::PropertyInstance(_)) => KnownClass::Property - .to_instance(db) - .is_disjoint_from(db, other), - - (Type::BoundSuper(_), Type::BoundSuper(_)) => !self.is_equivalent_to(db, other), - (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => KnownClass::Super - .to_instance(db) - .is_disjoint_from(db, other), - } - } - - /// Returns true if the type does not contain any gradual forms (as a sub-part). - pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { - match self { - Type::Dynamic(_) => false, - Type::Never - | Type::FunctionLiteral(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::ModuleLiteral(..) - | Type::IntLiteral(_) - | Type::BooleanLiteral(_) - | Type::StringLiteral(_) - | Type::LiteralString - | Type::BytesLiteral(_) - | Type::SliceLiteral(_) - | Type::KnownInstance(_) - | Type::AlwaysFalsy - | Type::AlwaysTruthy - | Type::PropertyInstance(_) => true, - - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => true, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_fully_static(db)), - }, - - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(), - Type::BoundSuper(bound_super) => { - !matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_)) - && !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_)) - } - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => { - // TODO: Ideally, we would iterate over the MRO of the class, check if all - // bases are fully static, and only return `true` if that is the case. - // - // This does not work yet, because we currently infer `Unknown` for some - // generic base classes that we don't understand yet. For example, `str` - // is defined as `class str(Sequence[str])` in typeshed and we currently - // compute its MRO as `(str, Unknown, object)`. This would make us think - // that `str` is a gradual type, which causes all sorts of downstream - // issues because it does not participate in equivalence/subtyping etc. - // - // Another problem is that we run into problems if we eagerly query the - // MRO of class literals here. I have not fully investigated this, but - // iterating over the MRO alone, without even acting on it, causes us to - // infer `Unknown` for many classes. - - true - } - Type::Union(union) => union.is_fully_static(db), - Type::Intersection(intersection) => intersection.is_fully_static(db), - // TODO: Once we support them, make sure that we return `false` for other types - // containing gradual forms such as `tuple[Any, ...]`. - // Conversely, make sure to return `true` for homogeneous tuples such as - // `tuple[int, ...]`, once we add support for them. - Type::Tuple(tuple) => tuple - .elements(db) - .iter() - .all(|elem| elem.is_fully_static(db)), - Type::Callable(callable) => callable.signature(db).is_fully_static(db), - } - } - - /// Return true if there is just a single inhabitant for this type. - /// - /// Note: This function aims to have no false positives, but might return `false` - /// for more complicated types that are actually singletons. - pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool { - match self { - Type::Dynamic(_) - | Type::Never - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::BytesLiteral(..) - | Type::SliceLiteral(..) - | Type::LiteralString => { - // Note: The literal types included in this pattern are not true singletons. - // There can be multiple Python objects (at different memory locations) that - // are both of type Literal[345], for example. - false - } - - // An unbounded, unconstrained typevar is not a singleton, because it can be - // specialized to a non-singleton type. A bounded typevar is not a singleton, even if - // the bound is a final singleton class, since it can still be specialized to `Never`. - // A constrained typevar is a singleton if all of its constraints are singletons. (Note - // that you cannot specialize a constrained typevar to a subtype of a constraint.) - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_singleton(db)), - }, - - // We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton. - Type::SubclassOf(..) => false, - Type::BoundSuper(..) => false, - Type::BooleanLiteral(_) - | Type::FunctionLiteral(..) - | Type::WrapperDescriptor(..) - | Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::KnownInstance(..) => true, - Type::Callable(_) => { - // A callable type is never a singleton because for any given signature, - // there could be any number of distinct objects that are all callable with that - // signature. - false - } - Type::BoundMethod(..) => { - // `BoundMethod` types are single-valued types, but not singleton types: - // ```pycon - // >>> class Foo: - // ... def bar(self): pass - // >>> f = Foo() - // >>> f.bar is f.bar - // False - // ``` - false - } - Type::MethodWrapper(_) => { - // Just a special case of `BoundMethod` really - // (this variant represents `f.__get__`, where `f` is any function) - false - } - Type::DataclassDecorator(_) => false, - Type::Instance(InstanceType { class }) => { - class.known(db).is_some_and(KnownClass::is_singleton) - } - Type::PropertyInstance(_) => false, - Type::Tuple(..) => { - // The empty tuple is a singleton on CPython and PyPy, but not on other Python - // implementations such as GraalPy. Its *use* as a singleton is discouraged and - // should not be relied on for type narrowing, so we do not treat it as one. - // See: - // https://docs.python.org/3/reference/expressions.html#parenthesized-forms - false - } - Type::Union(..) => { - // A single-element union, where the sole element was a singleton, would itself - // be a singleton type. However, unions with length < 2 should never appear in - // our model due to [`UnionBuilder::build`]. - false - } - Type::Intersection(..) => { - // Here, we assume that all intersection types that are singletons would have - // been reduced to a different form via [`IntersectionBuilder::build`] by now. - // For example: - // - // bool & ~Literal[False] = Literal[True] - // None & (None | int) = None | None & int = None - // - false - } - Type::AlwaysTruthy | Type::AlwaysFalsy => false, - } - } - - /// Return true if this type is non-empty and all inhabitants of this type compare equal. - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - match self { - Type::FunctionLiteral(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::ModuleLiteral(..) - | Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::IntLiteral(..) - | Type::BooleanLiteral(..) - | Type::StringLiteral(..) - | Type::BytesLiteral(..) - | Type::SliceLiteral(..) - | Type::KnownInstance(..) => true, - - // An unbounded, unconstrained typevar is not single-valued, because it can be - // specialized to a multiple-valued type. A bounded typevar is not single-valued, even - // if the bound is a final single-valued class, since it can still be specialized to - // `Never`. A constrained typevar is single-valued if all of its constraints are - // single-valued. (Note that you cannot specialize a constrained typevar to a subtype - // of a constraint.) - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_single_valued(db)), - }, - - Type::SubclassOf(..) => { - // TODO: Same comment as above for `is_singleton` - false - } - - Type::Tuple(tuple) => tuple - .elements(db) - .iter() - .all(|elem| elem.is_single_valued(db)), - - Type::Instance(InstanceType { class }) => { - class.known(db).is_some_and(KnownClass::is_single_valued) - } - - Type::BoundSuper(_) => { - // At runtime two super instances never compare equal, even if their arguments are identical. - false - } - - Type::Dynamic(_) - | Type::Never - | Type::Union(..) - | Type::Intersection(..) - | Type::LiteralString - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::Callable(_) - | Type::PropertyInstance(_) - | Type::DataclassDecorator(_) => false, - } - } - - /// This function is roughly equivalent to `find_name_in_mro` as defined in the [descriptor guide] or - /// [`_PyType_Lookup`] in CPython's `Objects/typeobject.c`. It should typically be called through - /// [Type::class_member], unless it is known that `self` is a class-like type. This function returns - /// `None` if called on an instance-like type. - /// - /// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance - /// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223 - fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option> { - self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default()) - } - - fn find_name_in_mro_with_policy( - &self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> Option> { - match self { - Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro_with_policy(db, name, policy) - // If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class - // elements instead of short-circuiting the whole result to `None`. We would need a more detailed - // return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is - // not a problem. - .unwrap_or_default() - })), - Type::Intersection(inter) => { - Some(inter.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro_with_policy(db, name, policy) - // Fall back to Unbound, similar to the union case (see above). - .unwrap_or_default() - })) - } - - Type::Dynamic(_) | Type::Never => Some(Symbol::bound(self).into()), - - Type::ClassLiteral(class) => { - match (class.known(db), name) { - (Some(KnownClass::FunctionType), "__get__") => Some( - Symbol::bound(Type::WrapperDescriptor( - WrapperDescriptorKind::FunctionTypeDunderGet, - )) - .into(), - ), - (Some(KnownClass::FunctionType), "__set__" | "__delete__") => { - // Hard code this knowledge, as we look up `__set__` and `__delete__` on `FunctionType` often. - Some(Symbol::Unbound.into()) - } - (Some(KnownClass::Property), "__get__") => Some( - Symbol::bound(Type::WrapperDescriptor( - WrapperDescriptorKind::PropertyDunderGet, - )) - .into(), - ), - (Some(KnownClass::Property), "__set__") => Some( - Symbol::bound(Type::WrapperDescriptor( - WrapperDescriptorKind::PropertyDunderSet, - )) - .into(), - ), - // TODO: - // We currently hard-code the knowledge that the following known classes are not - // descriptors, i.e. that they have no `__get__` method. This is not wrong and - // potentially even beneficial for performance, but it's not very principled. - // This case can probably be removed eventually, but we include it at the moment - // because we make extensive use of these types in our test suite. Note that some - // builtin types are not included here, since they do not have generic bases and - // are correctly handled by the `find_name_in_mro` method. - ( - Some( - KnownClass::Int - | KnownClass::Str - | KnownClass::Bytes - | KnownClass::Tuple - | KnownClass::Slice - | KnownClass::Range, - ), - "__get__" | "__set__" | "__delete__", - ) => Some(Symbol::Unbound.into()), - - _ => Some(class.class_member(db, name, policy)), - } - } - - Type::GenericAlias(alias) => { - Some(ClassType::from(*alias).class_member(db, name, policy)) - } - - Type::SubclassOf(subclass_of) - if name == "__get__" - && matches!( - subclass_of - .subclass_of() - .into_class() - .and_then(|c| c.known(db)), - Some( - KnownClass::Int - | KnownClass::Str - | KnownClass::Bytes - | KnownClass::Tuple - | KnownClass::Slice - | KnownClass::Range, - ) - ) => - { - Some(Symbol::Unbound.into()) - } - Type::SubclassOf(subclass_of_ty) => { - subclass_of_ty.find_name_in_mro_with_policy(db, name, policy) - } - - // Note: `super(pivot, owner).__class__` is `builtins.super`, not the owner's class. - // `BoundSuper` should look up the name in the MRO of `builtins.super`. - Type::BoundSuper(_) => KnownClass::Super - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy), - - // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type). - // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the - // MRO of the class `object`. - Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => { - KnownClass::Object - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) - } - - Type::FunctionLiteral(_) - | Type::Callable(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::ModuleLiteral(_) - | Type::KnownInstance(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::IntLiteral(_) - | Type::BooleanLiteral(_) - | Type::StringLiteral(_) - | Type::LiteralString - | Type::BytesLiteral(_) - | Type::SliceLiteral(_) - | Type::Tuple(_) - | Type::TypeVar(_) - | Type::Instance(_) - | Type::PropertyInstance(_) => None, - } - } - - /// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes - /// when called on an instance-like type, and metaclass attributes when called on a class-like type. - /// - /// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling - /// of union and intersection types. - fn class_member(self, db: &'db dyn Db, name: Name) -> SymbolAndQualifiers<'db> { - self.class_member_with_policy(db, name, MemberLookupPolicy::default()) - } - - #[salsa::tracked] - fn class_member_with_policy( - self, - db: &'db dyn Db, - name: Name, - policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - tracing::trace!("class_member: {}.{}", self.display(db), name); - match self { - Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { - elem.class_member_with_policy(db, name.clone(), policy) - }), - Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| { - elem.class_member_with_policy(db, name.clone(), policy) - }), - _ => self - .to_meta_type(db) - .find_name_in_mro_with_policy(db, name.as_str(), policy) - .expect( - "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", - ), - } - } - - /// This function roughly corresponds to looking up an attribute in the `__dict__` of an object. - /// For instance-like types, this goes through the classes MRO and discovers attribute assignments - /// in methods, as well as class-body declarations that we consider to be evidence for the presence - /// of an instance attribute. - /// - /// For example, an instance of the following class has instance members `a` and `b`, but `c` is - /// just a class attribute that would not be discovered by this method: - /// ```py - /// class C: - /// a: int - /// - /// c = 1 - /// - /// def __init__(self): - /// self.b: str = "a" - /// ``` - fn instance_member(&self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - match self { - Type::Union(union) => { - union.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)) - } - - Type::Intersection(intersection) => intersection - .map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)), - - Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), - - Type::Instance(InstanceType { class }) => class.instance_member(db, name), - - Type::FunctionLiteral(_) => KnownClass::FunctionType - .to_instance(db) - .instance_member(db, name), - - Type::BoundMethod(_) => KnownClass::MethodType - .to_instance(db) - .instance_member(db, name), - Type::MethodWrapper(_) => KnownClass::MethodWrapperType - .to_instance(db) - .instance_member(db, name), - Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType - .to_instance(db) - .instance_member(db, name), - Type::DataclassDecorator(_) => KnownClass::FunctionType - .to_instance(db) - .instance_member(db, name), - Type::Callable(_) => KnownClass::Object.to_instance(db).instance_member(db, name), - - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => KnownClass::Object.to_instance(db).instance_member(db, name), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.instance_member(db, name) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .map_with_boundness_and_qualifiers(db, |constraint| { - constraint.instance_member(db, name) - }), - }, - - Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name), - Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db).instance_member(db, name), - Type::StringLiteral(_) | Type::LiteralString => { - KnownClass::Str.to_instance(db).instance_member(db, name) - } - Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name), - Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).instance_member(db, name), - Type::Tuple(_) => KnownClass::Tuple.to_instance(db).instance_member(db, name), - - Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name), - Type::ModuleLiteral(_) => KnownClass::ModuleType - .to_instance(db) - .instance_member(db, name), - - Type::KnownInstance(_) => Symbol::Unbound.into(), - - Type::PropertyInstance(_) => KnownClass::Property - .to_instance(db) - .instance_member(db, name), - - // Note: `super(pivot, owner).__dict__` refers to the `__dict__` of the `builtins.super` instance, - // not that of the owner. - // This means we should only look up instance members defined on the `builtins.super()` instance itself. - // If you want to look up a member in the MRO of the `super`'s owner, - // refer to [`Type::member`] instead. - Type::BoundSuper(_) => KnownClass::Super.to_instance(db).instance_member(db, name), - - // TODO: we currently don't model the fact that class literals and subclass-of types have - // a `__dict__` that is filled with class level attributes. Modeling this is currently not - // required, as `instance_member` is only called for instance-like types through `member`, - // but we might want to add this in the future. - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - Symbol::Unbound.into() - } - } - } - - /// Access an attribute of this type without invoking the descriptor protocol. This - /// method corresponds to `inspect.getattr_static(, name)`. - /// - /// See also: [`Type::member`] - fn static_member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { - if let Type::ModuleLiteral(module) = self { - module.static_member(db, name) - } else if let symbol @ Symbol::Type(_, _) = self.class_member(db, name.into()).symbol { - symbol - } else if let Some(symbol @ Symbol::Type(_, _)) = - self.find_name_in_mro(db, name).map(|inner| inner.symbol) - { - symbol - } else { - self.instance_member(db, name).symbol - } - } - - /// Look up `__get__` on the meta-type of self, and call it with the arguments `self`, `instance`, - /// and `owner`. `__get__` is different than other dunder methods in that it is not looked up using - /// the descriptor protocol itself. - /// - /// In addition to the return type of `__get__`, this method also returns the *kind* of attribute - /// that `self` represents: (1) a data descriptor or (2) a non-data descriptor / normal attribute. - /// - /// If `__get__` is not defined on the meta-type, this method returns `None`. - #[salsa::tracked] - fn try_call_dunder_get( - self, - db: &'db dyn Db, - instance: Type<'db>, - owner: Type<'db>, - ) -> Option<(Type<'db>, AttributeKind)> { - tracing::trace!( - "try_call_dunder_get: {}, {}, {}", - self.display(db), - instance.display(db), - owner.display(db) - ); - let descr_get = self.class_member(db, "__get__".into()).symbol; - - if let Symbol::Type(descr_get, descr_get_boundness) = descr_get { - let return_ty = descr_get - .try_call(db, CallArgumentTypes::positional([self, instance, owner])) - .map(|bindings| { - if descr_get_boundness == Boundness::Bound { - bindings.return_type(db) - } else { - UnionType::from_elements(db, [bindings.return_type(db), self]) - } - }) - .ok()?; - - let descriptor_kind = if self.class_member(db, "__set__".into()).symbol.is_unbound() - && self - .class_member(db, "__delete__".into()) - .symbol - .is_unbound() - { - AttributeKind::NormalOrNonDataDescriptor - } else { - AttributeKind::DataDescriptor - }; - - Some((return_ty, descriptor_kind)) - } else { - None - } - } - - /// Look up `__get__` on the meta-type of `attribute`, and call it with `attribute`, `instance`, - /// and `owner` as arguments. This method exists as a separate step as we need to handle unions - /// and intersections explicitly. - fn try_call_dunder_get_on_attribute( - db: &'db dyn Db, - attribute: SymbolAndQualifiers<'db>, - instance: Type<'db>, - owner: Type<'db>, - ) -> (SymbolAndQualifiers<'db>, AttributeKind) { - match attribute { - // This branch is not strictly needed, but it short-circuits the lookup of various dunder - // methods and calls that would otherwise be made. - // - // Note that attribute accesses on dynamic types always succeed. For this reason, they also - // have `__get__`, `__set__`, and `__delete__` methods and are therefore considered to be - // data descriptors. - // - // The same is true for `Never`. - SymbolAndQualifiers { - symbol: Symbol::Type(Type::Dynamic(_) | Type::Never, _), - qualifiers: _, - } => (attribute, AttributeKind::DataDescriptor), - - SymbolAndQualifiers { - symbol: Symbol::Type(Type::Union(union), boundness), - qualifiers, - } => ( - union - .map_with_boundness(db, |elem| { - Symbol::Type( - elem.try_call_dunder_get(db, instance, owner) - .map_or(*elem, |(ty, _)| ty), - boundness, - ) - }) - .with_qualifiers(qualifiers), - // TODO: avoid the duplication here: - if union.elements(db).iter().all(|elem| { - elem.try_call_dunder_get(db, instance, owner) - .is_some_and(|(_, kind)| kind.is_data()) - }) { - AttributeKind::DataDescriptor - } else { - AttributeKind::NormalOrNonDataDescriptor - }, - ), - - SymbolAndQualifiers { - symbol: Symbol::Type(Type::Intersection(intersection), boundness), - qualifiers, - } => ( - intersection - .map_with_boundness(db, |elem| { - Symbol::Type( - elem.try_call_dunder_get(db, instance, owner) - .map_or(*elem, |(ty, _)| ty), - boundness, - ) - }) - .with_qualifiers(qualifiers), - // TODO: Discover data descriptors in intersections. - AttributeKind::NormalOrNonDataDescriptor, - ), - - SymbolAndQualifiers { - symbol: Symbol::Type(attribute_ty, boundness), - qualifiers: _, - } => { - if let Some((return_ty, attribute_kind)) = - attribute_ty.try_call_dunder_get(db, instance, owner) - { - (Symbol::Type(return_ty, boundness).into(), attribute_kind) - } else { - (attribute, AttributeKind::NormalOrNonDataDescriptor) - } - } - - _ => (attribute, AttributeKind::NormalOrNonDataDescriptor), - } - } - - /// Implementation of the descriptor protocol. - /// - /// This method roughly performs the following steps: - /// - /// - Look up the attribute `name` on the meta-type of `self`. Call the result `meta_attr`. - /// - Call `__get__` on the meta-type of `meta_attr`, if it exists. If the call succeeds, - /// replace `meta_attr` with the result of the call. Also check if `meta_attr` is a *data* - /// descriptor by testing if `__set__` or `__delete__` exist. - /// - If `meta_attr` is a data descriptor, return it. - /// - Otherwise, if `fallback` is bound, return `fallback`. - /// - Otherwise, return `meta_attr`. - /// - /// In addition to that, we also handle various cases of possibly-unbound symbols and fall - /// back to lower-precedence stages of the descriptor protocol by building union types. - fn invoke_descriptor_protocol( - self, - db: &'db dyn Db, - name: &str, - fallback: SymbolAndQualifiers<'db>, - policy: InstanceFallbackShadowsNonDataDescriptor, - member_policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - let ( - SymbolAndQualifiers { - symbol: meta_attr, - qualifiers: meta_attr_qualifiers, - }, - meta_attr_kind, - ) = Self::try_call_dunder_get_on_attribute( - db, - self.class_member_with_policy(db, name.into(), member_policy), - self, - self.to_meta_type(db), - ); - - let SymbolAndQualifiers { - symbol: fallback, - qualifiers: fallback_qualifiers, - } = fallback; - - match (meta_attr, meta_attr_kind, fallback) { - // The fallback type is unbound, so we can just return `meta_attr` unconditionally, - // no matter if it's data descriptor, a non-data descriptor, or a normal attribute. - (meta_attr @ Symbol::Type(_, _), _, Symbol::Unbound) => { - meta_attr.with_qualifiers(meta_attr_qualifiers) - } - - // `meta_attr` is the return type of a data descriptor and definitely bound, so we - // return it. - (meta_attr @ Symbol::Type(_, Boundness::Bound), AttributeKind::DataDescriptor, _) => { - meta_attr.with_qualifiers(meta_attr_qualifiers) - } - - // `meta_attr` is the return type of a data descriptor, but the attribute on the - // meta-type is possibly-unbound. This means that we "fall through" to the next - // stage of the descriptor protocol and union with the fallback type. - ( - Symbol::Type(meta_attr_ty, Boundness::PossiblyUnbound), - AttributeKind::DataDescriptor, - Symbol::Type(fallback_ty, fallback_boundness), - ) => Symbol::Type( - UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), - fallback_boundness, - ) - .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), - - // `meta_attr` is *not* a data descriptor. This means that the `fallback` type has - // now the highest priority. However, we only return the pure `fallback` type if the - // policy allows it. When invoked on class objects, the policy is set to `Yes`, which - // means that class-level attributes (the fallback) can shadow non-data descriptors - // on metaclasses. However, for instances, the policy is set to `No`, because we do - // allow instance-level attributes to shadow class-level non-data descriptors. This - // would require us to statically infer if an instance attribute is always set, which - // is something we currently don't attempt to do. - ( - Symbol::Type(_, _), - AttributeKind::NormalOrNonDataDescriptor, - fallback @ Symbol::Type(_, Boundness::Bound), - ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => { - fallback.with_qualifiers(fallback_qualifiers) - } - - // `meta_attr` is *not* a data descriptor. The `fallback` symbol is either possibly - // unbound or the policy argument is `No`. In both cases, the `fallback` type does - // not completely shadow the non-data descriptor, so we build a union of the two. - ( - Symbol::Type(meta_attr_ty, meta_attr_boundness), - AttributeKind::NormalOrNonDataDescriptor, - Symbol::Type(fallback_ty, fallback_boundness), - ) => Symbol::Type( - UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), - meta_attr_boundness.max(fallback_boundness), - ) - .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), - - // If the attribute is not found on the meta-type, we simply return the fallback. - (Symbol::Unbound, _, fallback) => fallback.with_qualifiers(fallback_qualifiers), - } - } - - /// Access an attribute of this type, potentially invoking the descriptor protocol. - /// Corresponds to `getattr(, name)`. - /// - /// See also: [`Type::static_member`] - /// - /// TODO: We should return a `Result` here to handle errors that can appear during attribute - /// lookup, like a failed `__get__` call on a descriptor. - #[must_use] - pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default()) - } - - /// Similar to [`Type::member`], but allows the caller to specify what policy should be used - /// when looking up attributes. See [`MemberLookupPolicy`] for more information. - #[salsa::tracked] - fn member_lookup_with_policy( - self, - db: &'db dyn Db, - name: Name, - policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name); - if name == "__class__" { - return Symbol::bound(self.to_meta_type(db)).into(); - } - - let name_str = name.as_str(); - - match self { - Type::Union(union) => union - .map_with_boundness(db, |elem| { - elem.member_lookup_with_policy(db, name_str.into(), policy) - .symbol - }) - .into(), - - Type::Intersection(intersection) => intersection - .map_with_boundness(db, |elem| { - elem.member_lookup_with_policy(db, name_str.into(), policy) - .symbol - }) - .into(), - - Type::Dynamic(..) | Type::Never => Symbol::bound(self).into(), - - Type::FunctionLiteral(function) if name == "__get__" => Symbol::bound( - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)), - ) - .into(), - Type::FunctionLiteral(function) if name == "__call__" => Symbol::bound( - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)), - ) - .into(), - Type::PropertyInstance(property) if name == "__get__" => Symbol::bound( - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)), - ) - .into(), - Type::PropertyInstance(property) if name == "__set__" => Symbol::bound( - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)), - ) - .into(), - Type::StringLiteral(literal) if name == "startswith" => Symbol::bound( - Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)), - ) - .into(), - - Type::ClassLiteral(class) - if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => - { - Symbol::bound(Type::WrapperDescriptor( - WrapperDescriptorKind::FunctionTypeDunderGet, - )) - .into() - } - Type::ClassLiteral(class) - if name == "__get__" && class.is_known(db, KnownClass::Property) => - { - Symbol::bound(Type::WrapperDescriptor( - WrapperDescriptorKind::PropertyDunderGet, - )) - .into() - } - Type::ClassLiteral(class) - if name == "__set__" && class.is_known(db, KnownClass::Property) => - { - Symbol::bound(Type::WrapperDescriptor( - WrapperDescriptorKind::PropertyDunderSet, - )) - .into() - } - Type::BoundMethod(bound_method) => match name_str { - "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), - "__func__" => { - Symbol::bound(Type::FunctionLiteral(bound_method.function(db))).into() - } - _ => { - KnownClass::MethodType - .to_instance(db) - .member_lookup_with_policy(db, name.clone(), policy) - .or_fall_back_to(db, || { - // If an attribute is not available on the bound method object, - // it will be looked up on the underlying function object: - Type::FunctionLiteral(bound_method.function(db)) - .member_lookup_with_policy(db, name, policy) - }) - } - }, - Type::MethodWrapper(_) => KnownClass::MethodWrapperType - .to_instance(db) - .member_lookup_with_policy(db, name, policy), - Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType - .to_instance(db) - .member_lookup_with_policy(db, name, policy), - Type::DataclassDecorator(_) => KnownClass::FunctionType - .to_instance(db) - .member_lookup_with_policy(db, name, policy), - Type::Callable(_) => KnownClass::Object - .to_instance(db) - .member_lookup_with_policy(db, name, policy), - - Type::Instance(InstanceType { class }) - if matches!(name.as_str(), "major" | "minor") - && class.is_known(db, KnownClass::VersionInfo) => - { - let python_version = Program::get(db).python_version(db); - let segment = if name == "major" { - python_version.major - } else { - python_version.minor - }; - Symbol::bound(Type::IntLiteral(segment.into())).into() - } - - Type::PropertyInstance(property) if name == "fget" => { - Symbol::bound(property.getter(db).unwrap_or(Type::none(db))).into() - } - Type::PropertyInstance(property) if name == "fset" => { - Symbol::bound(property.setter(db).unwrap_or(Type::none(db))).into() - } - - Type::IntLiteral(_) if matches!(name_str, "real" | "numerator") => { - Symbol::bound(self).into() - } - - Type::BooleanLiteral(bool_value) if matches!(name_str, "real" | "numerator") => { - Symbol::bound(Type::IntLiteral(i64::from(bool_value))).into() - } - - Type::ModuleLiteral(module) => module.static_member(db, name_str).into(), - - Type::AlwaysFalsy | Type::AlwaysTruthy => { - self.class_member_with_policy(db, name, policy) - } - - _ if policy.no_instance_fallback() => self.invoke_descriptor_protocol( - db, - name_str, - Symbol::Unbound.into(), - InstanceFallbackShadowsNonDataDescriptor::No, - policy, - ), - - Type::Instance(..) - | Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::BytesLiteral(..) - | Type::LiteralString - | Type::SliceLiteral(..) - | Type::Tuple(..) - | Type::TypeVar(..) - | Type::KnownInstance(..) - | Type::PropertyInstance(..) - | Type::FunctionLiteral(..) => { - let fallback = self.instance_member(db, name_str); - - let result = self.invoke_descriptor_protocol( - db, - name_str, - fallback, - InstanceFallbackShadowsNonDataDescriptor::No, - policy, - ); - - let custom_getattr_result = || { - // Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with - // dynamic imports. We explicitly hide it here to prevent arbitrary attributes - // from being available on modules. Same for `types.GenericAlias` - its - // `__getattr__` method will delegate to `__origin__` to allow looking up - // attributes on the original type. But in typeshed its return type is `Any`. - // It will need a special handling, so it remember the origin type to properly - // resolve the attribute. - if self.into_instance().is_some_and(|instance| { - instance.class.is_known(db, KnownClass::ModuleType) - || instance.class.is_known(db, KnownClass::GenericAlias) - }) { - return Symbol::Unbound.into(); - } - - self.try_call_dunder( - db, - "__getattr__", - CallArgumentTypes::positional([Type::StringLiteral( - StringLiteralType::new(db, Box::from(name.as_str())), - )]), - ) - .map(|outcome| Symbol::bound(outcome.return_type(db))) - // TODO: Handle call errors here. - .unwrap_or(Symbol::Unbound) - .into() - }; - - match result { - member @ SymbolAndQualifiers { - symbol: Symbol::Type(_, Boundness::Bound), - qualifiers: _, - } => member, - member @ SymbolAndQualifiers { - symbol: Symbol::Type(_, Boundness::PossiblyUnbound), - qualifiers: _, - } => member.or_fall_back_to(db, custom_getattr_result), - SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers: _, - } => custom_getattr_result(), - } - } - - Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { - let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect( - "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", - ); - - if name == "__mro__" { - return class_attr_plain; - } - - if self.is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) { - return SymbolAndQualifiers::todo("Attribute access on enum classes"); - } - - let class_attr_fallback = Self::try_call_dunder_get_on_attribute( - db, - class_attr_plain, - Type::none(db), - self, - ) - .0; - - self.invoke_descriptor_protocol( - db, - name_str, - class_attr_fallback, - InstanceFallbackShadowsNonDataDescriptor::Yes, - policy, - ) - } - - // Unlike other objects, `super` has a unique member lookup behavior. - // It's simpler than other objects: - // - // 1. Search for the attribute in the MRO, starting just after the pivot class. - // 2. If the attribute is a descriptor, invoke its `__get__` method. - Type::BoundSuper(bound_super) => { - let owner_attr = bound_super.find_name_in_mro_after_pivot(db, name_str, policy); - - bound_super - .try_call_dunder_get_on_attribute(db, owner_attr.clone()) - .unwrap_or(owner_attr) - } - } - } - - /// Resolves the boolean value of the type and falls back to [`Truthiness::Ambiguous`] if the type doesn't implement `__bool__` correctly. - /// - /// This method should only be used outside type checking or when evaluating if a type - /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. - /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. - pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { - self.try_bool_impl(db, true) - .unwrap_or_else(|err| err.fallback_truthiness()) - } - - /// Resolves the boolean value of a type. - /// - /// This is used to determine the value that would be returned - /// when `bool(x)` is called on an object `x`. - /// - /// Returns an error if the type doesn't implement `__bool__` correctly. - pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { - self.try_bool_impl(db, false) - } - - /// Resolves the boolean value of a type. - /// - /// Setting `allow_short_circuit` to `true` allows the implementation to - /// early return if the bool value of any union variant is `Truthiness::Ambiguous`. - /// Early returning shows a 1-2% perf improvement on our benchmarks because - /// `bool` (which doesn't care about errors) is used heavily when evaluating statically known branches. - /// - /// An alternative to this flag is to implement a trait similar to Rust's `Try` trait. - /// The advantage of that is that it would allow collecting the errors as well. However, - /// it is significantly more complex and duplicating the logic into `bool` without the error - /// handling didn't show any significant performance difference to when using the `allow_short_circuit` flag. - #[inline] - fn try_bool_impl( - &self, - db: &'db dyn Db, - allow_short_circuit: bool, - ) -> Result> { - let type_to_truthiness = |ty| { - if let Type::BooleanLiteral(bool_val) = ty { - Truthiness::from(bool_val) - } else { - Truthiness::Ambiguous - } - }; - - let try_dunder_bool = || { - // We only check the `__bool__` method for truth testing, even though at - // runtime there is a fallback to `__len__`, since `__bool__` takes precedence - // and a subclass could add a `__bool__` method. - - match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) { - Ok(outcome) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { - // The type has a `__bool__` method, but it doesn't return a - // boolean. - return Err(BoolError::IncorrectReturnType { - return_type, - not_boolable_type: *self, - }); - } - Ok(type_to_truthiness(return_type)) - } - - Err(CallDunderError::PossiblyUnbound(outcome)) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { - // The type has a `__bool__` method, but it doesn't return a - // boolean. - return Err(BoolError::IncorrectReturnType { - return_type: outcome.return_type(db), - not_boolable_type: *self, - }); - } - - // Don't trust possibly unbound `__bool__` method. - Ok(Truthiness::Ambiguous) - } - - Err(CallDunderError::MethodNotAvailable) => Ok(Truthiness::Ambiguous), - Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => { - Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(bindings.return_type(db)), - not_boolable_type: *self, - }) - } - Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => { - Err(BoolError::NotCallable { - not_boolable_type: *self, - }) - } - Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => { - Err(BoolError::Other { - not_boolable_type: *self, - }) - } - } - }; - - let try_union = |union: UnionType<'db>| { - let mut truthiness = None; - let mut all_not_callable = true; - let mut has_errors = false; - - for element in union.elements(db) { - let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) { - Ok(truthiness) => truthiness, - Err(err) => { - has_errors = true; - all_not_callable &= matches!(err, BoolError::NotCallable { .. }); - err.fallback_truthiness() - } - }; - - truthiness.get_or_insert(element_truthiness); - - if Some(element_truthiness) != truthiness { - truthiness = Some(Truthiness::Ambiguous); - - if allow_short_circuit { - return Ok(Truthiness::Ambiguous); - } - } - } - - if has_errors { - if all_not_callable { - return Err(BoolError::NotCallable { - not_boolable_type: *self, - }); - } - return Err(BoolError::Union { - union, - truthiness: truthiness.unwrap_or(Truthiness::Ambiguous), - }); - } - Ok(truthiness.unwrap_or(Truthiness::Ambiguous)) - }; - - let truthiness = match self { - Type::Dynamic(_) | Type::Never | Type::Callable(_) | Type::LiteralString => { - Truthiness::Ambiguous - } - - Type::FunctionLiteral(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::ModuleLiteral(_) - | Type::SliceLiteral(_) - | Type::AlwaysTruthy => Truthiness::AlwaysTrue, - - Type::AlwaysFalsy => Truthiness::AlwaysFalse, - - Type::ClassLiteral(class) => class - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit)?, - Type::GenericAlias(alias) => ClassType::from(*alias) - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit)?, - - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => Truthiness::Ambiguous, - ClassBase::Class(class) => { - Type::from(class).try_bool_impl(db, allow_short_circuit)? - } - }, - - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => Truthiness::Ambiguous, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.try_bool_impl(db, allow_short_circuit)? - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - try_union(constraints)? - } - }, - - Type::Instance(InstanceType { class }) => match class.known(db) { - Some(known_class) => known_class.bool(), - None => try_dunder_bool()?, - }, - - Type::KnownInstance(known_instance) => known_instance.bool(), - - Type::PropertyInstance(_) => Truthiness::AlwaysTrue, - - Type::Union(union) => try_union(*union)?, - - Type::Intersection(_) => { - // TODO - Truthiness::Ambiguous - } - - Type::IntLiteral(num) => Truthiness::from(*num != 0), - Type::BooleanLiteral(bool) => Truthiness::from(*bool), - Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), - Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), - Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()), - Type::BoundSuper(_) => Truthiness::AlwaysTrue, - }; - - Ok(truthiness) - } - - /// Return the type of `len()` on a type if it is known more precisely than `int`, - /// or `None` otherwise. - /// - /// In the second case, the return type of `len()` in `typeshed` (`int`) - /// is used as a fallback. - fn len(&self, db: &'db dyn Db) -> Option> { - fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - match ty { - // TODO: Emit diagnostic for non-integers and negative integers - Type::IntLiteral(value) => (value >= 0).then_some(ty), - Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())), - Type::Union(union) => { - let mut builder = UnionBuilder::new(db); - for element in union.elements(db) { - builder = builder.add(non_negative_int_literal(db, *element)?); - } - Some(builder.build()) - } - _ => None, - } - } - - let usize_len = match self { - Type::BytesLiteral(bytes) => Some(bytes.python_len(db)), - Type::StringLiteral(string) => Some(string.python_len(db)), - Type::Tuple(tuple) => Some(tuple.len(db)), - _ => None, - }; - - if let Some(usize_len) = usize_len { - return usize_len.try_into().ok().map(Type::IntLiteral); - } - - let return_ty = match self.try_call_dunder(db, "__len__", CallArgumentTypes::none()) { - Ok(bindings) => bindings.return_type(db), - Err(CallDunderError::PossiblyUnbound(bindings)) => bindings.return_type(db), - - // TODO: emit a diagnostic - Err(CallDunderError::MethodNotAvailable) => return None, - Err(CallDunderError::CallError(_, bindings)) => bindings.return_type(db), - }; - - non_negative_int_literal(db, return_ty) - } - - /// Returns the call signatures of a type. - /// - /// Note that all types have a valid [`Signatures`], even if the type is not callable. - /// Moreover, "callable" can be subtle for a union type, since some union elements might be - /// callable and some not. A union is callable if every element type is callable — and even - /// then, the elements might be inconsistent, such that there's no argument list that's valid - /// for all elements. It's usually best to only worry about "callability" relative to a - /// particular argument list, via [`try_call`][Self::try_call] and - /// [`CallErrorKind::NotCallable`]. - fn signatures(self, db: &'db dyn Db) -> Signatures<'db> { - match self { - Type::Callable(callable) => Signatures::single(CallableSignature::single( - self, - callable.signature(db).clone(), - )), - - Type::BoundMethod(bound_method) => { - let signature = bound_method.function(db).signature(db); - let signature = CallableSignature::single(self, signature.clone()) - .with_bound_type(bound_method.self_instance(db)); - Signatures::single(signature) - } - - Type::MethodWrapper( - MethodWrapperKind::FunctionTypeDunderGet(_) - | MethodWrapperKind::PropertyDunderGet(_), - ) => { - // Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`. - // This is required because we need to return more precise types than what the signature in - // typeshed provides: - // - // ```py - // class FunctionType: - // # ... - // @overload - // def __get__(self, instance: None, owner: type, /) -> FunctionType: ... - // @overload - // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... - // ``` - // - // For `builtins.property.__get__`, we use the same signature. The return types are not - // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`. - - let not_none = Type::none(db).negate(db); - let signature = CallableSignature::from_overloads( - self, - [ - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(KnownClass::Type.to_instance(db)), - ]), - None, - ), - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(not_none), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::Type.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), - None, - ), - ], - ); - Signatures::single(signature) - } - - Type::WrapperDescriptor( - kind @ (WrapperDescriptorKind::FunctionTypeDunderGet - | WrapperDescriptorKind::PropertyDunderGet), - ) => { - // Here, we also model `types.FunctionType.__get__` (or builtins.property.__get__), - // but now we consider a call to this as a function, i.e. we also expect the `self` - // argument to be passed in. - - // TODO: Consider merging this signature with the one in the previous match clause, - // since the previous one is just this signature with the `self` parameters - // removed. - let not_none = Type::none(db).negate(db); - let descriptor = match kind { - WrapperDescriptorKind::FunctionTypeDunderGet => { - KnownClass::FunctionType.to_instance(db) - } - WrapperDescriptorKind::PropertyDunderGet => { - KnownClass::Property.to_instance(db) - } - WrapperDescriptorKind::PropertyDunderSet => { - unreachable!("Not part of outer match pattern") - } - }; - let signature = CallableSignature::from_overloads( - self, - [ - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(descriptor), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(KnownClass::Type.to_instance(db)), - ]), - None, - ), - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(descriptor), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(not_none), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::Type.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), - None, - ), - ], - ); - Signatures::single(signature) - } - - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => { - Signatures::single(CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object(db)), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::object(db)), - ]), - None, - ), - )) - } - Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { - Signatures::single(CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object(db)), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::object(db)), - ]), - None, - ), - )) - } - - Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => { - Signatures::single(CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("prefix"))) - .with_annotated_type(UnionType::from_elements( - db, - [ - KnownClass::Str.to_instance(db), - // TODO: tuple[str, ...] - KnownClass::Tuple.to_instance(db), - ], - )), - Parameter::positional_only(Some(Name::new_static("start"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("end"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), - Some(KnownClass::Bool.to_instance(db)), - ), - )) - } - - Type::FunctionLiteral(function_type) => match function_type.known(db) { - Some( - KnownFunction::IsEquivalentTo - | KnownFunction::IsSubtypeOf - | KnownFunction::IsAssignableTo - | KnownFunction::IsDisjointFrom - | KnownFunction::IsGradualEquivalentTo, - ) => { - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("a"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("b"))) - .type_form() - .with_annotated_type(Type::any()), - ]), - Some(KnownClass::Bool.to_instance(db)), - ), - ); - Signatures::single(signature) - } - - Some( - KnownFunction::IsFullyStatic - | KnownFunction::IsSingleton - | KnownFunction::IsSingleValued, - ) => { - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "a", - ))) - .type_form() - .with_annotated_type(Type::any())]), - Some(KnownClass::Bool.to_instance(db)), - ), - ); - Signatures::single(signature) - } - - Some(KnownFunction::AssertType) => { - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("type"))) - .type_form() - .with_annotated_type(Type::any()), - ]), - Some(Type::none(db)), - ), - ); - Signatures::single(signature) - } - - Some(KnownFunction::AssertNever) => { - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "arg", - ))) - // We need to set the type to `Any` here (instead of `Never`), - // in order for every `assert_never` call to pass the argument - // check. If we set it to `Never`, we'll get invalid-argument-type - // errors instead of `type-assertion-failure` errors. - .with_annotated_type(Type::any())]), - Some(Type::none(db)), - ), - ); - Signatures::single(signature) - } - - Some(KnownFunction::Cast) => { - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("typ")) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_or_keyword(Name::new_static("val")) - .with_annotated_type(Type::any()), - ]), - Some(Type::any()), - ), - ); - Signatures::single(signature) - } - - Some(KnownFunction::Dataclass) => { - let signature = CallableSignature::from_overloads( - self, - [ - // def dataclass(cls: None, /) -> Callable[[type[_T]], type[_T]]: ... - Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("cls"), - )) - .with_annotated_type(Type::none(db))]), - None, - ), - // def dataclass(cls: type[_T], /) -> type[_T]: ... - Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("cls"), - )) - // TODO: type[_T] - .with_annotated_type(Type::any())]), - None, - ), - // TODO: make this overload Python-version-dependent - - // def dataclass( - // *, - // init: bool = True, - // repr: bool = True, - // eq: bool = True, - // order: bool = False, - // unsafe_hash: bool = False, - // frozen: bool = False, - // match_args: bool = True, - // kw_only: bool = False, - // slots: bool = False, - // weakref_slot: bool = False, - // ) -> Callable[[type[_T]], type[_T]]: ... - Signature::new( - Parameters::new([ - Parameter::keyword_only(Name::new_static("init")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("repr")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("eq")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("order")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("unsafe_hash")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("frozen")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("match_args")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("kw_only")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("slots")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("weakref_slot")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - ]), - None, - ), - ], - ); - - Signatures::single(signature) - } - - _ => Signatures::single(CallableSignature::single( - self, - function_type.signature(db).clone(), - )), - }, - - Type::ClassLiteral(class) => match class.known(db) { - // TODO: Ideally we'd use `try_call_constructor` for all constructor calls. - // Currently we don't for a few special known types, either because their - // constructors are defined with overloads, or because we want to special case - // their return type beyond what typeshed provides (though this support could - // likely be moved into the `try_call_constructor` path). Once we support - // overloads, re-evaluate the need for these arms. - Some(KnownClass::Bool) => { - // ```py - // class bool(int): - // def __new__(cls, o: object = ..., /) -> Self: ... - // ``` - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "o", - ))) - .with_annotated_type(Type::any()) - .with_default_type(Type::BooleanLiteral(false))]), - Some(KnownClass::Bool.to_instance(db)), - ), - ); - Signatures::single(signature) - } - - Some(KnownClass::Str) => { - // ```py - // class str(Sequence[str]): - // @overload - // def __new__(cls, object: object = ...) -> Self: ... - // @overload - // def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ... - // ``` - let signature = CallableSignature::from_overloads( - self, - [ - Signature::new( - Parameters::new([Parameter::positional_or_keyword( - Name::new_static("object"), - ) - .with_annotated_type(Type::object(db)) - .with_default_type(Type::string_literal(db, ""))]), - Some(KnownClass::Str.to_instance(db)), - ), - Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("object")) - // TODO: Should be `ReadableBuffer` instead of this union type: - .with_annotated_type(UnionType::from_elements( - db, - [ - KnownClass::Bytes.to_instance(db), - KnownClass::Bytearray.to_instance(db), - ], - )) - .with_default_type(Type::bytes_literal(db, b"")), - Parameter::positional_or_keyword(Name::new_static("encoding")) - .with_annotated_type(KnownClass::Str.to_instance(db)) - .with_default_type(Type::string_literal(db, "utf-8")), - Parameter::positional_or_keyword(Name::new_static("errors")) - .with_annotated_type(KnownClass::Str.to_instance(db)) - .with_default_type(Type::string_literal(db, "strict")), - ]), - Some(KnownClass::Str.to_instance(db)), - ), - ], - ); - Signatures::single(signature) - } - - Some(KnownClass::Type) => { - // ```py - // class type: - // @overload - // def __init__(self, o: object, /) -> None: ... - // @overload - // def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... - // ``` - let signature = CallableSignature::from_overloads( - self, - [ - Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("o"), - )) - .with_annotated_type(Type::any())]), - Some(KnownClass::Type.to_instance(db)), - ), - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("name"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("bases"))) - // TODO: Should be tuple[type, ...] once we have support for homogenous tuples - .with_annotated_type(KnownClass::Tuple.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("dict"))) - // TODO: Should be `dict[str, Any]` once we have support for generics - .with_annotated_type(KnownClass::Dict.to_instance(db)), - ]), - Some(KnownClass::Type.to_instance(db)), - ), - ], - ); - Signatures::single(signature) - } - Some(KnownClass::Object) => { - // ```py - // class object: - // def __init__(self) -> None: ... - // def __new__(cls) -> Self: ... - // ``` - let signature = CallableSignature::from_overloads( - self, - [Signature::new( - Parameters::empty(), - Some(KnownClass::Object.to_instance(db)), - )], - ); - Signatures::single(signature) - } - - Some(KnownClass::Super) => { - // ```py - // class super: - // @overload - // def __init__(self, t: Any, obj: Any, /) -> None: ... - // @overload - // def __init__(self, t: Any, /) -> None: ... - // @overload - // def __init__(self) -> None: ... - // ``` - let signature = CallableSignature::from_overloads( - self, - [ - Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("t"))) - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("obj"))) - .with_annotated_type(Type::any()), - ]), - Some(KnownClass::Super.to_instance(db)), - ), - Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("t"), - )) - .with_annotated_type(Type::any())]), - Some(KnownClass::Super.to_instance(db)), - ), - Signature::new( - Parameters::empty(), - Some(KnownClass::Super.to_instance(db)), - ), - ], - ); - Signatures::single(signature) - } - - Some(KnownClass::Property) => { - let getter_signature = Signature::new( - Parameters::new([ - Parameter::positional_only(None).with_annotated_type(Type::any()) - ]), - Some(Type::any()), - ); - let setter_signature = Signature::new( - Parameters::new([ - Parameter::positional_only(None).with_annotated_type(Type::any()), - Parameter::positional_only(None).with_annotated_type(Type::any()), - ]), - Some(Type::none(db)), - ); - let deleter_signature = Signature::new( - Parameters::new([ - Parameter::positional_only(None).with_annotated_type(Type::any()) - ]), - Some(Type::any()), - ); - - let signature = CallableSignature::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("fget")) - .with_annotated_type(UnionType::from_elements( - db, - [ - Type::Callable(CallableType::new(db, getter_signature)), - Type::none(db), - ], - )) - .with_default_type(Type::none(db)), - Parameter::positional_or_keyword(Name::new_static("fset")) - .with_annotated_type(UnionType::from_elements( - db, - [ - Type::Callable(CallableType::new(db, setter_signature)), - Type::none(db), - ], - )) - .with_default_type(Type::none(db)), - Parameter::positional_or_keyword(Name::new_static("fdel")) - .with_annotated_type(UnionType::from_elements( - db, - [ - Type::Callable(CallableType::new( - db, - deleter_signature, - )), - Type::none(db), - ], - )) - .with_default_type(Type::none(db)), - Parameter::positional_or_keyword(Name::new_static("doc")) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::Str.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), - None, - ), - ); - Signatures::single(signature) - } - - // Most class literal constructor calls are handled by `try_call_constructor` and - // not via getting the signature here. This signature can still be used in some - // cases (e.g. evaluating callable subtyping). TODO improve this definition - // (intersection of `__new__` and `__init__` signatures? and respect metaclass - // `__call__`). - _ => { - let signature = CallableSignature::single( - self, - Signature::new_generic( - class.generic_context(db), - Parameters::gradual_form(), - self.to_instance(db), - ), - ); - Signatures::single(signature) - } - }, - - Type::GenericAlias(_) => { - // TODO annotated return type on `__new__` or metaclass `__call__` - // TODO check call vs signatures of `__new__` and/or `__init__` - let signature = CallableSignature::single( - self, - Signature::new(Parameters::gradual_form(), self.to_instance(db)), - ); - Signatures::single(signature) - } - - Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db), - // Most type[] constructor calls are handled by `try_call_constructor` and not via - // getting the signature here. This signature can still be used in some cases (e.g. - // evaluating callable subtyping). TODO improve this definition (intersection of - // `__new__` and `__init__` signatures? and respect metaclass `__call__`). - ClassBase::Class(class) => Type::from(class).signatures(db), - }, - - Type::Instance(_) => { - // Note that for objects that have a (possibly not callable!) `__call__` attribute, - // we will get the signature of the `__call__` attribute, but will pass in the type - // of the original object as the "callable type". That ensures that we get errors - // like "`X` is not callable" instead of "`` is not - // callable". - match self - .member_lookup_with_policy( - db, - Name::new_static("__call__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .symbol - { - Symbol::Type(dunder_callable, boundness) => { - let mut signatures = dunder_callable.signatures(db).clone(); - signatures.replace_callable_type(dunder_callable, self); - if boundness == Boundness::PossiblyUnbound { - signatures.set_dunder_call_is_possibly_unbound(); - } - signatures - } - Symbol::Unbound => Signatures::not_callable(self), - } - } - - // Dynamic types are callable, and the return type is the same dynamic type. Similarly, - // `Never` is always callable and returns `Never`. - Type::Dynamic(_) | Type::Never => Signatures::single(CallableSignature::dynamic(self)), - - // Note that this correctly returns `None` if none of the union elements are callable. - Type::Union(union) => Signatures::from_union( - self, - union - .elements(db) - .iter() - .map(|element| element.signatures(db)), - ), - - Type::Intersection(_) => { - Signatures::single(CallableSignature::todo("Type::Intersection.call()")) - } - - _ => Signatures::not_callable(self), - } - } - - /// Calls `self`. Returns a [`CallError`] if `self` is (always or possibly) not callable, or if - /// the arguments are not compatible with the formal parameters. - /// - /// You get back a [`Bindings`] for both successful and unsuccessful calls. - /// It contains information about which formal parameters each argument was matched to, - /// and about any errors matching arguments and parameters. - fn try_call( - self, - db: &'db dyn Db, - mut argument_types: CallArgumentTypes<'_, 'db>, - ) -> Result, CallError<'db>> { - let signatures = self.signatures(db); - Bindings::match_parameters(signatures, &mut argument_types) - .check_types(db, &mut argument_types) - } - - /// Look up a dunder method on the meta-type of `self` and call it. - /// - /// Returns an `Err` if the dunder method can't be called, - /// or the given arguments are not valid. - fn try_call_dunder( - self, - db: &'db dyn Db, - name: &str, - mut argument_types: CallArgumentTypes<'_, 'db>, - ) -> Result, CallDunderError<'db>> { - self.try_call_dunder_with_policy( - db, - name, - &mut argument_types, - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - } - - /// Same as `try_call_dunder`, but allows specifying a policy for the member lookup. In - /// particular, this allows to specify `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK` to avoid - /// looking up dunder methods on `object`, which is needed for functions like `__init__`, - /// `__new__`, or `__setattr__`. - fn try_call_dunder_with_policy( - self, - db: &'db dyn Db, - name: &str, - argument_types: &mut CallArgumentTypes<'_, 'db>, - policy: MemberLookupPolicy, - ) -> Result, CallDunderError<'db>> { - match self - .member_lookup_with_policy(db, name.into(), policy) - .symbol - { - Symbol::Type(dunder_callable, boundness) => { - let signatures = dunder_callable.signatures(db); - let bindings = Bindings::match_parameters(signatures, argument_types) - .check_types(db, argument_types)?; - if boundness == Boundness::PossiblyUnbound { - return Err(CallDunderError::PossiblyUnbound(Box::new(bindings))); - } - Ok(bindings) - } - Symbol::Unbound => Err(CallDunderError::MethodNotAvailable), - } - } - - /// Returns the element type when iterating over `self`. - /// - /// This method should only be used outside of type checking because it omits any errors. - /// For type checking, use [`try_iterate`](Self::try_iterate) instead. - fn iterate(self, db: &'db dyn Db) -> Type<'db> { - self.try_iterate(db) - .unwrap_or_else(|err| err.fallback_element_type(db)) - } - - /// Given the type of an object that is iterated over in some way, - /// return the type of objects that are yielded by that iteration. - /// - /// E.g., for the following loop, given the type of `x`, infer the type of `y`: - /// ```python - /// for y in x: - /// pass - /// ``` - fn try_iterate(self, db: &'db dyn Db) -> Result, IterationError<'db>> { - if let Type::Tuple(tuple_type) = self { - return Ok(UnionType::from_elements(db, tuple_type.elements(db))); - } - - let try_call_dunder_getitem = || { - self.try_call_dunder( - db, - "__getitem__", - CallArgumentTypes::positional([KnownClass::Int.to_instance(db)]), - ) - .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) - }; - - let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { - iterator - .try_call_dunder(db, "__next__", CallArgumentTypes::none()) - .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) - }; - - let dunder_iter_result = self - .try_call_dunder(db, "__iter__", CallArgumentTypes::none()) - .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); - - match dunder_iter_result { - Ok(iterator) => { - // `__iter__` is definitely bound and calling it succeeds. - // See what calling `__next__` on the object returned by `__iter__` gives us... - try_call_dunder_next_on_iterator(iterator).map_err(|dunder_next_error| { - IterationError::IterReturnsInvalidIterator { - iterator, - dunder_next_error, - } - }) - } - - // `__iter__` is possibly unbound... - Err(CallDunderError::PossiblyUnbound(dunder_iter_outcome)) => { - let iterator = dunder_iter_outcome.return_type(db); - - match try_call_dunder_next_on_iterator(iterator) { - Ok(dunder_next_return) => { - try_call_dunder_getitem() - .map(|dunder_getitem_return_type| { - // If `__iter__` is possibly unbound, - // but it returns an object that has a bound and valid `__next__` method, - // *and* the object has a bound and valid `__getitem__` method, - // we infer a union of the type returned by the `__next__` method - // and the type returned by the `__getitem__` method. - // - // No diagnostic is emitted; iteration will always succeed! - UnionType::from_elements( - db, - [dunder_next_return, dunder_getitem_return_type], - ) - }) - .map_err(|dunder_getitem_error| { - IterationError::PossiblyUnboundIterAndGetitemError { - dunder_next_return, - dunder_getitem_error, - } - }) - } - - Err(dunder_next_error) => Err(IterationError::IterReturnsInvalidIterator { - iterator, - dunder_next_error, - }), - } - } - - // `__iter__` is definitely bound but it can't be called with the expected arguments - Err(CallDunderError::CallError(kind, bindings)) => { - Err(IterationError::IterCallError(kind, bindings)) - } - - // There's no `__iter__` method. Try `__getitem__` instead... - Err(CallDunderError::MethodNotAvailable) => { - try_call_dunder_getitem().map_err(|dunder_getitem_error| { - IterationError::UnboundIterAndGetitemError { - dunder_getitem_error, - } - }) - } - } - } - - /// Returns the type bound from a context manager with type `self`. - /// - /// This method should only be used outside of type checking because it omits any errors. - /// For type checking, use [`try_enter`](Self::try_enter) instead. - fn enter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter(db) - .unwrap_or_else(|err| err.fallback_enter_type(db)) - } - - /// Given the type of an object that is used as a context manager (i.e. in a `with` statement), - /// return the return type of its `__enter__` method, which is bound to any potential targets. - /// - /// E.g., for the following `with` statement, given the type of `x`, infer the type of `y`: - /// ```python - /// with x as y: - /// pass - /// ``` - fn try_enter(self, db: &'db dyn Db) -> Result, ContextManagerError<'db>> { - let enter = self.try_call_dunder(db, "__enter__", CallArgumentTypes::none()); - let exit = self.try_call_dunder( - db, - "__exit__", - CallArgumentTypes::positional([Type::none(db), Type::none(db), Type::none(db)]), - ); - - // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). - match (enter, exit) { - (Ok(enter), Ok(_)) => Ok(enter.return_type(db)), - (Ok(enter), Err(exit_error)) => Err(ContextManagerError::Exit { - enter_return_type: enter.return_type(db), - exit_error, - }), - // TODO: Use the `exit_ty` to determine if any raised exception is suppressed. - (Err(enter_error), Ok(_)) => Err(ContextManagerError::Enter(enter_error)), - (Err(enter_error), Err(exit_error)) => Err(ContextManagerError::EnterAndExit { - enter_error, - exit_error, - }), - } - } - - /// Given a class literal or non-dynamic SubclassOf type, try calling it (creating an instance) - /// and return the resulting instance type. - /// - /// Models `type.__call__` behavior. - /// TODO: model metaclass `__call__`. - /// - /// E.g., for the following code, infer the type of `Foo()`: - /// ```python - /// class Foo: - /// pass - /// - /// Foo() - /// ``` - fn try_call_constructor( - self, - db: &'db dyn Db, - mut argument_types: CallArgumentTypes<'_, 'db>, - ) -> Result, ConstructorCallError<'db>> { - debug_assert!(matches!( - self, - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) - )); - - // If we are trying to construct a non-specialized generic class, we should use the - // constructor parameters to try to infer the class specialization. To do this, we need to - // tweak our member lookup logic a bit. Normally, when looking up a class or instance - // member, we first apply the class's default specialization, and apply that specialization - // to the type of the member. To infer a specialization from the argument types, we need to - // have the class's typevars still in the method signature when we attempt to call it. To - // do this, we instead use the _identity_ specialization, which maps each of the class's - // generic typevars to itself. - let (generic_origin, self_type) = match self { - Type::ClassLiteral(ClassLiteralType::Generic(generic)) => { - let specialization = generic.generic_context(db).identity_specialization(db); - ( - Some(generic), - Type::GenericAlias(GenericAlias::new(db, generic, specialization)), - ) - } - _ => (None, self), - }; - - // As of now we do not model custom `__call__` on meta-classes, so the code below - // only deals with interplay between `__new__` and `__init__` methods. - // The logic is roughly as follows: - // 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always - // present), we call it and analyze outcome. We then analyze `__init__` call, but only - // if it is defined somewhere except object. This is because `object.__init__` - // allows arbitrary arguments if and only if `__new__` is defined, but typeshed - // defines `__init__` for `object` with no arguments. - // 2. If `__new__` is not found, we call `__init__`. Here, we allow it to fallback all - // the way to `object` (single `self` argument call). This time it is correct to - // fallback to `object.__init__`, since it will indeed check that no arguments are - // passed. - // - // Note that we currently ignore `__new__` return type, since we do not yet support `Self` - // and most builtin classes use it as return type annotation. We always return the instance - // type. - - // Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must - // avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on - // a class, metaclass attribute would take precedence. But by avoiding `__new__` on - // `object` we would inadvertently unhide `__new__` on `type`, which is not what we want. - // An alternative might be to not skip `object.__new__` but instead mark it such that it's - // easy to check if that's the one we found? - // Note that `__new__` is a static method, so we must inject the `cls` argument. - let new_call_outcome = argument_types.with_self(Some(self_type), |argument_types| { - let result = self_type.try_call_dunder_with_policy( - db, - "__new__", - argument_types, - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ); - match result { - Err(CallDunderError::MethodNotAvailable) => None, - _ => Some(result), - } - }); - - // Construct an instance type that we can use to look up the `__init__` instance method. - // This performs the same logic as `Type::to_instance`, except for generic class literals. - // TODO: we should use the actual return type of `__new__` to determine the instance type - let init_ty = self_type - .to_instance(db) - .expect("type should be convertible to instance type"); - - let init_call_outcome = if new_call_outcome.is_none() - || !init_ty - .member_lookup_with_policy( - db, - "__init__".into(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) - .symbol - .is_unbound() - { - Some(init_ty.try_call_dunder(db, "__init__", argument_types)) - } else { - None - }; - - // Note that we use `self` here, not `self_type`, so that if constructor argument inference - // fails, we fail back to the default specialization. - let instance_ty = self - .to_instance(db) - .expect("type should be convertible to instance type"); - - match (generic_origin, new_call_outcome, init_call_outcome) { - // All calls are successful or not called at all - ( - Some(generic_origin), - new_call_outcome @ (None | Some(Ok(_))), - init_call_outcome @ (None | Some(Ok(_))), - ) => { - let new_specialization = new_call_outcome - .and_then(Result::ok) - .as_ref() - .and_then(Bindings::single_element) - .and_then(CallableBinding::matching_overload) - .and_then(|(_, binding)| binding.specialization()); - let init_specialization = init_call_outcome - .and_then(Result::ok) - .as_ref() - .and_then(Bindings::single_element) - .and_then(CallableBinding::matching_overload) - .and_then(|(_, binding)| binding.specialization()); - let specialization = match (new_specialization, init_specialization) { - (None, None) => None, - (Some(specialization), None) | (None, Some(specialization)) => { - Some(specialization) - } - (Some(new_specialization), Some(init_specialization)) => { - Some(new_specialization.combine(db, init_specialization)) - } - }; - let specialized = specialization - .map(|specialization| { - Type::instance(ClassType::Generic(GenericAlias::new( - db, - generic_origin, - specialization, - ))) - }) - .unwrap_or(instance_ty); - Ok(specialized) - } - - (None, None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty), - - (_, None | Some(Ok(_)), Some(Err(error))) => { - // no custom `__new__` or it was called and succeeded, but `__init__` failed. - Err(ConstructorCallError::Init(instance_ty, error)) - } - (_, Some(Err(error)), None | Some(Ok(_))) => { - // custom `__new__` was called and failed, but init is ok - Err(ConstructorCallError::New(instance_ty, error)) - } - (_, Some(Err(new_error)), Some(Err(init_error))) => { - // custom `__new__` was called and failed, and `__init__` is also not ok - Err(ConstructorCallError::NewAndInit( - instance_ty, - new_error, - init_error, - )) - } - } - } - - #[must_use] - pub fn to_instance(&self, db: &'db dyn Db) -> Option> { - match self { - Type::Dynamic(_) | Type::Never => Some(*self), - Type::ClassLiteral(class) => Some(Type::instance(class.default_specialization(db))), - Type::GenericAlias(alias) => Some(Type::instance(ClassType::from(*alias))), - Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()), - Type::Union(union) => { - let mut builder = UnionBuilder::new(db); - for element in union.elements(db) { - builder = builder.add(element.to_instance(db)?); - } - Some(builder.build()) - } - Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance")), - Type::BooleanLiteral(_) - | Type::BytesLiteral(_) - | Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::MethodWrapper(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::Instance(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::ModuleLiteral(_) - | Type::IntLiteral(_) - | Type::StringLiteral(_) - | Type::SliceLiteral(_) - | Type::Tuple(_) - | Type::TypeVar(_) - | Type::LiteralString - | Type::BoundSuper(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy => None, - } - } - - /// If we see a value of this type used as a type expression, what type does it name? - /// - /// For example, the builtin `int` as a value expression is of type - /// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type - /// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose - /// `__class__` is `int`. - pub fn in_type_expression( - &self, - db: &'db dyn Db, - ) -> Result, InvalidTypeExpressionError<'db>> { - match self { - // Special cases for `float` and `complex` - // https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex - Type::ClassLiteral(class) => { - let ty = match class.known(db) { - Some(KnownClass::Any) => Type::any(), - Some(KnownClass::Complex) => UnionType::from_elements( - db, - [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - KnownClass::Complex.to_instance(db), - ], - ), - Some(KnownClass::Float) => UnionType::from_elements( - db, - [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - ], - ), - _ => Type::instance(class.default_specialization(db)), - }; - Ok(ty) - } - Type::GenericAlias(alias) => Ok(Type::instance(ClassType::from(*alias))), - - Type::SubclassOf(_) - | Type::BooleanLiteral(_) - | Type::BytesLiteral(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::SliceLiteral(_) - | Type::IntLiteral(_) - | Type::LiteralString - | Type::ModuleLiteral(_) - | Type::StringLiteral(_) - | Type::Tuple(_) - | Type::TypeVar(_) - | Type::Callable(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::Never - | Type::FunctionLiteral(_) - | Type::BoundSuper(_) - | Type::PropertyInstance(_) => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)], - fallback_type: Type::unknown(), - }), - - Type::KnownInstance(known_instance) => match known_instance { - KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), - KnownInstanceType::Never | KnownInstanceType::NoReturn => Ok(Type::Never), - KnownInstanceType::LiteralString => Ok(Type::LiteralString), - KnownInstanceType::Any => Ok(Type::any()), - KnownInstanceType::Unknown => Ok(Type::unknown()), - KnownInstanceType::AlwaysTruthy => Ok(Type::AlwaysTruthy), - KnownInstanceType::AlwaysFalsy => Ok(Type::AlwaysFalsy), - - // We treat `typing.Type` exactly the same as `builtins.type`: - KnownInstanceType::Type => Ok(KnownClass::Type.to_instance(db)), - KnownInstanceType::Tuple => Ok(KnownClass::Tuple.to_instance(db)), - - // Legacy `typing` aliases - KnownInstanceType::List => Ok(KnownClass::List.to_instance(db)), - KnownInstanceType::Dict => Ok(KnownClass::Dict.to_instance(db)), - KnownInstanceType::Set => Ok(KnownClass::Set.to_instance(db)), - KnownInstanceType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)), - KnownInstanceType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)), - KnownInstanceType::Counter => Ok(KnownClass::Counter.to_instance(db)), - KnownInstanceType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)), - KnownInstanceType::Deque => Ok(KnownClass::Deque.to_instance(db)), - KnownInstanceType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), - - KnownInstanceType::TypeVar(typevar) => Ok(Type::TypeVar(*typevar)), - - // TODO: Use an opt-in rule for a bare `Callable` - KnownInstanceType::Callable => Ok(Type::Callable(CallableType::unknown(db))), - - KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")), - KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")), - - KnownInstanceType::Protocol => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], - fallback_type: Type::unknown(), - }), - - KnownInstanceType::Literal - | KnownInstanceType::Union - | KnownInstanceType::Intersection => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![ - InvalidTypeExpression::RequiresArguments(*self) - ], - fallback_type: Type::unknown(), - }), - - KnownInstanceType::Optional - | KnownInstanceType::Not - | KnownInstanceType::TypeOf - | KnownInstanceType::TypeIs - | KnownInstanceType::TypeGuard - | KnownInstanceType::Unpack - | KnownInstanceType::CallableTypeOf => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![ - InvalidTypeExpression::RequiresOneArgument(*self) - ], - fallback_type: Type::unknown(), - }), - - KnownInstanceType::Annotated | KnownInstanceType::Concatenate => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![ - InvalidTypeExpression::RequiresTwoArguments(*self) - ], - fallback_type: Type::unknown(), - }) - } - - KnownInstanceType::ClassVar | KnownInstanceType::Final => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![ - InvalidTypeExpression::TypeQualifier(*known_instance) - ], - fallback_type: Type::unknown(), - }) - } - - KnownInstanceType::ReadOnly - | KnownInstanceType::NotRequired - | KnownInstanceType::Required => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![ - InvalidTypeExpression::TypeQualifierRequiresOneArgument(*known_instance) - ], - fallback_type: Type::unknown(), - }), - }, - - Type::Union(union) => { - let mut builder = UnionBuilder::new(db); - let mut invalid_expressions = smallvec::SmallVec::default(); - for element in union.elements(db) { - match element.in_type_expression(db) { - Ok(type_expr) => builder = builder.add(type_expr), - Err(InvalidTypeExpressionError { - fallback_type, - invalid_expressions: new_invalid_expressions, - }) => { - invalid_expressions.extend(new_invalid_expressions); - builder = builder.add(fallback_type); - } - } - } - if invalid_expressions.is_empty() { - Ok(builder.build()) - } else { - Err(InvalidTypeExpressionError { - fallback_type: builder.build(), - invalid_expressions, - }) - } - } - - Type::Dynamic(_) => Ok(*self), - - Type::Instance(InstanceType { class }) => match class.known(db) { - Some(KnownClass::TypeVar) => Ok(todo_type!( - "Support for `typing.TypeVar` instances in type expressions" - )), - Some( - KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs, - ) => Ok(todo_type!("Support for `typing.ParamSpec`")), - Some(KnownClass::TypeVarTuple) => Ok(todo_type!( - "Support for `typing.TypeVarTuple` instances in type expressions" - )), - Some(KnownClass::NewType) => Ok(todo_type!( - "Support for `typing.NewType` instances in type expressions" - )), - Some(KnownClass::GenericAlias) => Ok(todo_type!( - "Support for `typing.GenericAlias` instances in type expressions" - )), - Some(KnownClass::UnionType) => Ok(todo_type!( - "Support for `types.UnionType` instances in type expressions" - )), - _ => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType( - *self - )], - fallback_type: Type::unknown(), - }), - }, - - Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), - } - } - - /// The type `NoneType` / `None` - pub fn none(db: &'db dyn Db) -> Type<'db> { - KnownClass::NoneType.to_instance(db) - } - - /// Return the type of `tuple(sys.version_info)`. - /// - /// This is not exactly the type that `sys.version_info` has at runtime, - /// but it's a useful fallback for us in order to infer `Literal` types from `sys.version_info` comparisons. - fn version_info_tuple(db: &'db dyn Db) -> Self { - let python_version = Program::get(db).python_version(db); - let int_instance_ty = KnownClass::Int.to_instance(db); - - // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) - let release_level_ty = { - let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"] - .iter() - .map(|level| Type::string_literal(db, level)) - .collect(); - - // For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`; - // those techniques ensure that union elements are deduplicated and unions are eagerly simplified - // into other types where necessary. Here, however, we know that there are no duplicates - // in this union, so it's probably more efficient to use `UnionType::new()` directly. - Type::Union(UnionType::new(db, elements)) - }; - - TupleType::from_elements( - db, - [ - Type::IntLiteral(python_version.major.into()), - Type::IntLiteral(python_version.minor.into()), - int_instance_ty, - release_level_ty, - int_instance_ty, - ], - ) - } - - /// Given a type that is assumed to represent an instance of a class, - /// return a type that represents that class itself. - #[must_use] - pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { - match self { - Type::Never => Type::Never, - Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class), - Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db), - Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), - Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), - Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db), - Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db), - Type::SliceLiteral(_) => KnownClass::Slice.to_class_literal(db), - Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), - Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), - Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db), - Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), - Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db), - Type::Callable(_) => KnownClass::Type.to_instance(db), - Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), - Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), - - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => KnownClass::Object.to_class_literal(db), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - // TODO: If we add a proper `OneOf` connector, we should use that here instead - // of union. (Using a union here doesn't break anything, but it is imprecise.) - constraints.map(db, |constraint| constraint.to_meta_type(db)) - } - }, - - Type::ClassLiteral(class) => class.metaclass(db), - Type::GenericAlias(alias) => ClassType::from(*alias).metaclass(db), - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => *self, - ClassBase::Class(class) => SubclassOfType::from( - db, - ClassBase::try_from_type(db, class.metaclass(db)) - .unwrap_or(ClassBase::unknown()), - ), - }, - - Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), - Type::Dynamic(dynamic) => SubclassOfType::from(db, ClassBase::Dynamic(*dynamic)), - // TODO intersections - Type::Intersection(_) => SubclassOfType::from( - db, - ClassBase::try_from_type(db, todo_type!("Intersection meta-type")) - .expect("Type::Todo should be a valid ClassBase"), - ), - Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), - Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), - } - } - - /// Applies a specialization to this type, replacing any typevars with the types that they are - /// specialized to. - /// - /// Note that this does not specialize generic classes, functions, or type aliases! That is a - /// different operation that is performed explicitly (via a subscript operation), or implicitly - /// via a call to the generic object. - #[must_use] - #[salsa::tracked] - pub fn apply_specialization( - self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) -> Type<'db> { - match self { - Type::TypeVar(typevar) => specialization.get(db, typevar).unwrap_or(self), - - Type::FunctionLiteral(function) => { - Type::FunctionLiteral(function.apply_specialization(db, specialization)) - } - - // Note that we don't need to apply the specialization to `self_instance`, since it - // must either be a non-generic class literal (which cannot have any typevars to - // specialize) or a generic alias (which has already been fully specialized). For a - // generic alias, the specialization being applied here must be for some _other_ - // generic context nested within the generic alias's class literal, which the generic - // alias's context cannot refer to. (The _method_ does need to be specialized, since it - // might be a nested generic method, whose generic context is what is now being - // specialized.) - Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( - db, - method.function(db).apply_specialization(db, specialization), - method.self_instance(db), - )), - - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet( - function.apply_specialization(db, specialization), - )) - } - - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => { - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall( - function.apply_specialization(db, specialization), - )) - } - - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet( - property.apply_specialization(db, specialization), - )) - } - - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet( - property.apply_specialization(db, specialization), - )) - } - - Type::Callable(callable) => { - Type::Callable(callable.apply_specialization(db, specialization)) - } - - Type::GenericAlias(generic) => { - let specialization = generic - .specialization(db) - .apply_specialization(db, specialization); - Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) - } - - Type::PropertyInstance(property) => { - Type::PropertyInstance(property.apply_specialization(db, specialization)) - } - - Type::Union(union) => union.map(db, |element| { - element.apply_specialization(db, specialization) - }), - Type::Intersection(intersection) => { - let mut builder = IntersectionBuilder::new(db); - for positive in intersection.positive(db) { - builder = - builder.add_positive(positive.apply_specialization(db, specialization)); - } - for negative in intersection.negative(db) { - builder = - builder.add_negative(negative.apply_specialization(db, specialization)); - } - builder.build() - } - Type::Tuple(tuple) => TupleType::from_elements( - db, - tuple - .iter(db) - .map(|ty| ty.apply_specialization(db, specialization)), - ), - - Type::Dynamic(_) - | Type::Never - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) - | Type::DataclassDecorator(_) - | Type::ModuleLiteral(_) - // A non-generic class never needs to be specialized. A generic class is specialized - // explicitly (via a subscript expression) or implicitly (via a call), and not because - // some other generic context's specialization is applied to it. - | Type::ClassLiteral(_) - // SubclassOf contains a ClassType, which has already been specialized if needed, like - // above with BoundMethod's self_instance. - | Type::SubclassOf(_) - | Type::IntLiteral(_) - | Type::BooleanLiteral(_) - | Type::LiteralString - | Type::StringLiteral(_) - | Type::BytesLiteral(_) - | Type::SliceLiteral(_) - | Type::BoundSuper(_) - // Instance contains a ClassType, which has already been specialized if needed, like - // above with BoundMethod's self_instance. - | Type::Instance(_) - | Type::KnownInstance(_) => self, - } - } - - /// Return the string representation of this type when converted to string as it would be - /// provided by the `__str__` method. - /// - /// When not available, this should fall back to the value of `[Type::repr]`. - /// Note: this method is used in the builtins `format`, `print`, `str.format` and `f-strings`. - #[must_use] - pub fn str(&self, db: &'db dyn Db) -> Type<'db> { - match self { - Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), - Type::StringLiteral(_) | Type::LiteralString => *self, - Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db)) - } - // TODO: handle more complex types - _ => KnownClass::Str.to_instance(db), - } - } - - /// Return the string representation of this type as it would be provided by the `__repr__` - /// method at runtime. - #[must_use] - pub fn repr(&self, db: &'db dyn Db) -> Type<'db> { - match self { - Type::IntLiteral(number) => Type::string_literal(db, &number.to_string()), - Type::BooleanLiteral(true) => Type::string_literal(db, "True"), - Type::BooleanLiteral(false) => Type::string_literal(db, "False"), - Type::StringLiteral(literal) => { - Type::string_literal(db, &format!("'{}'", literal.value(db).escape_default())) - } - Type::LiteralString => Type::LiteralString, - Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db)) - } - // TODO: handle more complex types - _ => KnownClass::Str.to_instance(db), - } - } - - /// Returns where this type is defined. - /// - /// It's the foundation for the editor's "Go to type definition" feature - /// where the user clicks on a value and it takes them to where the value's type is defined. - /// - /// This method returns `None` for unions and intersections because how these - /// should be handled, especially when some variants don't have definitions, is - /// specific to the call site. - pub fn definition(&self, db: &'db dyn Db) -> Option> { - match self { - Self::BoundMethod(method) => { - Some(TypeDefinition::Function(method.function(db).definition(db))) - } - Self::FunctionLiteral(function) => { - Some(TypeDefinition::Function(function.definition(db))) - } - Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))), - Self::ClassLiteral(class_literal) => { - Some(TypeDefinition::Class(class_literal.definition(db))) - } - Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), - Self::Instance(instance) => Some(TypeDefinition::Class(instance.class.definition(db))), - Self::KnownInstance(instance) => match instance { - KnownInstanceType::TypeVar(var) => { - Some(TypeDefinition::TypeVar(var.definition(db))) - } - KnownInstanceType::TypeAliasType(type_alias) => { - Some(TypeDefinition::TypeAlias(type_alias.definition(db))) - } - _ => None, - }, - - Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Class(class) => Some(TypeDefinition::Class(class.definition(db))), - ClassBase::Dynamic(_) => None, - }, - - Self::StringLiteral(_) - | Self::BooleanLiteral(_) - | Self::LiteralString - | Self::IntLiteral(_) - | Self::BytesLiteral(_) - | Self::SliceLiteral(_) - | Self::MethodWrapper(_) - | Self::WrapperDescriptor(_) - | Self::DataclassDecorator(_) - | Self::PropertyInstance(_) - | Self::BoundSuper(_) - | Self::Tuple(_) => self.to_meta_type(db).definition(db), - - Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))), - - Self::Union(_) | Self::Intersection(_) => None, - - // These types have no definition - Self::Dynamic(_) - | Self::Never - | Self::Callable(_) - | Self::AlwaysTruthy - | Self::AlwaysFalsy => None, - } - } -} - -impl<'db> From<&Type<'db>> for Type<'db> { - fn from(value: &Type<'db>) -> Self { - *value - } -} - -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub enum DynamicType { - // An explicitly annotated `typing.Any` - Any, - // An unannotated value, or a dynamic type resulting from an error - Unknown, - /// Temporary type for symbols that can't be inferred yet because of missing implementations. - /// - /// This variant should eventually be removed once red-knot is spec-compliant. - /// - /// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the - /// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not - /// `Todo` would change the output type. - /// - /// This variant should be created with the `todo_type!` macro. - Todo(TodoType), - /// Temporary type until we support protocols. We use a separate variant (instead of `Todo(…)`) - /// in order to be able to match on them explicitly. - TodoProtocol, -} - -impl std::fmt::Display for DynamicType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DynamicType::Any => f.write_str("Any"), - DynamicType::Unknown => f.write_str("Unknown"), - // `DynamicType::Todo`'s display should be explicit that is not a valid display of - // any other type - DynamicType::Todo(todo) => write!(f, "@Todo{todo}"), - DynamicType::TodoProtocol => f.write_str(if cfg!(debug_assertions) { - "@Todo(protocol)" - } else { - "@Todo" - }), - } - } -} - -bitflags! { - /// Type qualifiers that appear in an annotation expression. - #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] - pub(crate) struct TypeQualifiers: u8 { - /// `typing.ClassVar` - const CLASS_VAR = 1 << 0; - /// `typing.Final` - const FINAL = 1 << 1; - } -} - -/// When inferring the type of an annotation expression, we can also encounter type qualifiers -/// such as `ClassVar` or `Final`. These do not affect the inferred type itself, but rather -/// control how a particular symbol can be accessed or modified. This struct holds a type and -/// a set of type qualifiers. -/// -/// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the -/// qualifier `ClassVar`. -#[derive(Clone, Debug, Copy, Eq, PartialEq, salsa::Update)] -pub(crate) struct TypeAndQualifiers<'db> { - inner: Type<'db>, - qualifiers: TypeQualifiers, -} - -impl<'db> TypeAndQualifiers<'db> { - pub(crate) fn new(inner: Type<'db>, qualifiers: TypeQualifiers) -> Self { - Self { inner, qualifiers } - } - - /// Constructor that creates a [`TypeAndQualifiers`] instance with type `Unknown` and no qualifiers. - pub(crate) fn unknown() -> Self { - Self { - inner: Type::unknown(), - qualifiers: TypeQualifiers::empty(), - } - } - - /// Forget about type qualifiers and only return the inner type. - pub(crate) fn inner_type(&self) -> Type<'db> { - self.inner - } - - /// Insert/add an additional type qualifier. - pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) { - self.qualifiers |= qualifier; - } - - /// Return the set of type qualifiers. - pub(crate) fn qualifiers(&self) -> TypeQualifiers { - self.qualifiers - } -} - -impl<'db> From> for TypeAndQualifiers<'db> { - fn from(inner: Type<'db>) -> Self { - Self { - inner, - qualifiers: TypeQualifiers::empty(), - } - } -} - -/// Error struct providing information on type(s) that were deemed to be invalid -/// in a type expression context, and the type we should therefore fallback to -/// for the problematic type expression. -#[derive(Debug, PartialEq, Eq)] -pub struct InvalidTypeExpressionError<'db> { - fallback_type: Type<'db>, - invalid_expressions: smallvec::SmallVec<[InvalidTypeExpression<'db>; 1]>, -} - -impl<'db> InvalidTypeExpressionError<'db> { - fn into_fallback_type( - self, - context: &InferContext, - node: &ast::Expr, - is_reachable: bool, - ) -> Type<'db> { - let InvalidTypeExpressionError { - fallback_type, - invalid_expressions, - } = self; - if is_reachable { - for error in invalid_expressions { - context.report_lint_old( - &INVALID_TYPE_FORM, - node, - format_args!("{}", error.reason(context.db())), - ); - } - } - fallback_type - } -} - -/// Enumeration of various types that are invalid in type-expression contexts -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum InvalidTypeExpression<'db> { - /// Some types always require exactly one argument when used in a type expression - RequiresOneArgument(Type<'db>), - /// Some types always require at least one argument when used in a type expression - RequiresArguments(Type<'db>), - /// Some types always require at least two arguments when used in a type expression - RequiresTwoArguments(Type<'db>), - /// The `Protocol` type is invalid in type expressions - Protocol, - /// Type qualifiers are always invalid in *type expressions*, - /// but these ones are okay with 0 arguments in *annotation expressions* - TypeQualifier(KnownInstanceType<'db>), - /// Type qualifiers that are invalid in type expressions, - /// and which would require exactly one argument even if they appeared in an annotation expression - TypeQualifierRequiresOneArgument(KnownInstanceType<'db>), - /// Some types are always invalid in type expressions - InvalidType(Type<'db>), -} - -impl<'db> InvalidTypeExpression<'db> { - const fn reason(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { - struct Display<'db> { - error: InvalidTypeExpression<'db>, - db: &'db dyn Db, - } - - impl std::fmt::Display for Display<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.error { - InvalidTypeExpression::RequiresOneArgument(ty) => write!( - f, - "`{ty}` requires exactly one argument when used in a type expression", - ty = ty.display(self.db) - ), - InvalidTypeExpression::RequiresArguments(ty) => write!( - f, - "`{ty}` requires at least one argument when used in a type expression", - ty = ty.display(self.db) - ), - InvalidTypeExpression::RequiresTwoArguments(ty) => write!( - f, - "`{ty}` requires at least two arguments when used in a type expression", - ty = ty.display(self.db) - ), - InvalidTypeExpression::Protocol => f.write_str( - "`typing.Protocol` is not allowed in type expressions" - ), - InvalidTypeExpression::TypeQualifier(qualifier) => write!( - f, - "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)", - q = qualifier.repr(self.db) - ), - InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!( - f, - "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)", - q = qualifier.repr(self.db) - ), - InvalidTypeExpression::InvalidType(ty) => write!( - f, - "Variable of type `{ty}` is not allowed in a type expression", - ty = ty.display(self.db) - ), - } - } - } - - Display { error: self, db } - } -} - -/// Data regarding a single type variable. -/// -/// This is referenced by `KnownInstanceType::TypeVar` (to represent the singleton type of the -/// runtime `typing.TypeVar` object itself), and by `Type::TypeVar` to represent the type that this -/// typevar represents as an annotation: that is, an unknown set of objects, constrained by the -/// upper-bound/constraints on this type var, defaulting to the default type of this type var when -/// not otherwise bound to a type. -/// -/// This must be a tracked struct, not an interned one, because typevar equivalence is by identity, -/// not by value. Two typevars that have the same name, bound/constraints, and default, are still -/// different typevars: if used in the same scope, they may be bound to different types. -#[salsa::tracked(debug)] -pub struct TypeVarInstance<'db> { - /// The name of this TypeVar (e.g. `T`) - #[return_ref] - name: ast::name::Name, - - /// The type var's definition - pub definition: Definition<'db>, - - /// The upper bound or constraint on the type of this TypeVar - bound_or_constraints: Option>, - - /// The default type for this TypeVar - default_ty: Option>, -} - -impl<'db> TypeVarInstance<'db> { - #[allow(unused)] - pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { - if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { - Some(ty) - } else { - None - } - } - - #[allow(unused)] - pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { - if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { - Some(tuple.elements(db)) - } else { - None - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)] -pub enum TypeVarBoundOrConstraints<'db> { - UpperBound(Type<'db>), - Constraints(UnionType<'db>), -} - -/// Error returned if a type is not (or may not be) a context manager. -#[derive(Debug)] -enum ContextManagerError<'db> { - Enter(CallDunderError<'db>), - Exit { - enter_return_type: Type<'db>, - exit_error: CallDunderError<'db>, - }, - EnterAndExit { - enter_error: CallDunderError<'db>, - exit_error: CallDunderError<'db>, - }, -} - -impl<'db> ContextManagerError<'db> { - fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { - self.enter_type(db).unwrap_or(Type::unknown()) - } - - /// Returns the `__enter__` return type if it is known, - /// or `None` if the type never has a callable `__enter__` attribute - fn enter_type(&self, db: &'db dyn Db) -> Option> { - match self { - Self::Exit { - enter_return_type, - exit_error: _, - } => Some(*enter_return_type), - Self::Enter(enter_error) - | Self::EnterAndExit { - enter_error, - exit_error: _, - } => match enter_error { - CallDunderError::PossiblyUnbound(call_outcome) => { - Some(call_outcome.return_type(db)) - } - CallDunderError::CallError(CallErrorKind::NotCallable, _) => None, - CallDunderError::CallError(_, bindings) => Some(bindings.return_type(db)), - CallDunderError::MethodNotAvailable => None, - }, - } - } - - fn report_diagnostic( - &self, - context: &InferContext<'db>, - context_expression_type: Type<'db>, - context_expression_node: ast::AnyNodeRef, - ) { - let format_call_dunder_error = |call_dunder_error: &CallDunderError<'db>, name: &str| { - match call_dunder_error { - CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"), - CallDunderError::PossiblyUnbound(_) => { - format!("the method `{name}` is possibly unbound") - } - // TODO: Use more specific error messages for the different error cases. - // E.g. hint toward the union variant that doesn't correctly implement enter, - // distinguish between a not callable `__enter__` attribute and a wrong signature. - CallDunderError::CallError(_, _) => { - format!("it does not correctly implement `{name}`") - } - } - }; - - let format_call_dunder_errors = |error_a: &CallDunderError<'db>, - name_a: &str, - error_b: &CallDunderError<'db>, - name_b: &str| { - match (error_a, error_b) { - (CallDunderError::PossiblyUnbound(_), CallDunderError::PossiblyUnbound(_)) => { - format!("the methods `{name_a}` and `{name_b}` are possibly unbound") - } - (CallDunderError::MethodNotAvailable, CallDunderError::MethodNotAvailable) => { - format!("it does not implement `{name_a}` and `{name_b}`") - } - (CallDunderError::CallError(_, _), CallDunderError::CallError(_, _)) => { - format!("it does not correctly implement `{name_a}` or `{name_b}`") - } - (_, _) => format!( - "{format_a}, and {format_b}", - format_a = format_call_dunder_error(error_a, name_a), - format_b = format_call_dunder_error(error_b, name_b) - ), - } - }; - - let db = context.db(); - - let formatted_errors = match self { - Self::Exit { - enter_return_type: _, - exit_error, - } => format_call_dunder_error(exit_error, "__exit__"), - Self::Enter(enter_error) => format_call_dunder_error(enter_error, "__enter__"), - Self::EnterAndExit { - enter_error, - exit_error, - } => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"), - }; - - context.report_lint_old( - &INVALID_CONTEXT_MANAGER, - context_expression_node, - format_args!( - "Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}", - context_expression = context_expression_type.display(db) - ), - ); - } -} - -/// Error returned if a type is not (or may not be) iterable. -#[derive(Debug)] -enum IterationError<'db> { - /// The object being iterated over has a bound `__iter__` method, - /// but calling it with the expected arguments results in an error. - IterCallError(CallErrorKind, Box>), - - /// The object being iterated over has a bound `__iter__` method that can be called - /// with the expected types, but it returns an object that is not a valid iterator. - IterReturnsInvalidIterator { - /// The type of the object returned by the `__iter__` method. - iterator: Type<'db>, - /// The error we encountered when we tried to call `__next__` on the type - /// returned by `__iter__` - dunder_next_error: CallDunderError<'db>, - }, - - /// The object being iterated over has a bound `__iter__` method that returns a - /// valid iterator. However, the `__iter__` method is possibly unbound, and there - /// either isn't a `__getitem__` method to fall back to, or calling the `__getitem__` - /// method returns some kind of error. - PossiblyUnboundIterAndGetitemError { - /// The type of the object returned by the `__next__` method on the iterator. - /// (The iterator being the type returned by the `__iter__` method on the iterable.) - dunder_next_return: Type<'db>, - /// The error we encountered when we tried to call `__getitem__` on the iterable. - dunder_getitem_error: CallDunderError<'db>, - }, - - /// The object being iterated over doesn't have an `__iter__` method. - /// It also either doesn't have a `__getitem__` method to fall back to, - /// or calling the `__getitem__` method returns some kind of error. - UnboundIterAndGetitemError { - dunder_getitem_error: CallDunderError<'db>, - }, -} - -impl<'db> IterationError<'db> { - fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { - self.element_type(db).unwrap_or(Type::unknown()) - } - - /// Returns the element type if it is known, or `None` if the type is never iterable. - fn element_type(&self, db: &'db dyn Db) -> Option> { - match self { - Self::IterReturnsInvalidIterator { - dunder_next_error, .. - } => dunder_next_error.return_type(db), - - Self::IterCallError(_, dunder_iter_bindings) => dunder_iter_bindings - .return_type(db) - .try_call_dunder(db, "__next__", CallArgumentTypes::none()) - .map(|dunder_next_outcome| Some(dunder_next_outcome.return_type(db))) - .unwrap_or_else(|dunder_next_call_error| dunder_next_call_error.return_type(db)), - - Self::PossiblyUnboundIterAndGetitemError { - dunder_next_return, - dunder_getitem_error, - } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => Some(*dunder_next_return), - CallDunderError::PossiblyUnbound(dunder_getitem_outcome) => { - Some(UnionType::from_elements( - db, - [*dunder_next_return, dunder_getitem_outcome.return_type(db)], - )) - } - CallDunderError::CallError(CallErrorKind::NotCallable, _) => { - Some(*dunder_next_return) - } - CallDunderError::CallError(_, dunder_getitem_bindings) => { - let dunder_getitem_return = dunder_getitem_bindings.return_type(db); - let elements = [*dunder_next_return, dunder_getitem_return]; - Some(UnionType::from_elements(db, elements)) - } - }, - - Self::UnboundIterAndGetitemError { - dunder_getitem_error, - } => dunder_getitem_error.return_type(db), - } - } - - /// Reports the diagnostic for this error. - fn report_diagnostic( - &self, - context: &InferContext<'db>, - iterable_type: Type<'db>, - iterable_node: ast::AnyNodeRef, - ) { - let db = context.db(); - - let report_not_iterable = |arguments: std::fmt::Arguments| { - context.report_lint_old(&NOT_ITERABLE, iterable_node, arguments); - }; - - // TODO: for all of these error variants, the "explanation" for the diagnostic - // (everything after the "because") should really be presented as a "help:", "note", - // or similar, rather than as part of the same sentence as the error message. - match self { - Self::IterCallError(CallErrorKind::NotCallable, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` attribute has type `{dunder_iter_type}`, \ - which is not callable", - iterable_type = iterable_type.display(db), - dunder_iter_type = bindings.callable_type().display(db), - )), - Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) if bindings.is_single() => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` attribute (with type `{dunder_iter_type}`) \ - may not be callable", - iterable_type = iterable_type.display(db), - dunder_iter_type = bindings.callable_type().display(db), - )); - } - Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` attribute (with type `{dunder_iter_type}`) \ - may not be callable", - iterable_type = iterable_type.display(db), - dunder_iter_type = bindings.callable_type().display(db), - )); - } - Self::IterCallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method has an invalid signature \ - (expected `def __iter__(self): ...`)", - iterable_type = iterable_type.display(db), - )), - Self::IterCallError(CallErrorKind::BindingError, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method (with type `{dunder_iter_type}`) \ - may have an invalid signature (expected `def __iter__(self): ...`)", - iterable_type = iterable_type.display(db), - dunder_iter_type = bindings.callable_type().display(db), - )), - - Self::IterReturnsInvalidIterator { - iterator, - dunder_next_error - } => match dunder_next_error { - CallDunderError::MethodNotAvailable => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has no `__next__` method", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which may not have a `__next__` method", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::NotCallable, _) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has a `__next__` attribute that is not callable", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has a `__next__` attribute that may not be callable", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has an invalid `__next__` method (expected `def __next__(self): ...`)", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, _) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which may have an invalid `__next__` method (expected `def __next__(self): ...`)", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - } - - Self::PossiblyUnboundIterAndGetitemError { - dunder_getitem_error, .. - } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => report_not_iterable(format_args!( - "Object of type `{}` may not be iterable \ - because it may not have an `__iter__` method \ - and it doesn't have a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!( - "Object of type `{}` may not be iterable \ - because it may not have an `__iter__` method or a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` attribute may not be callable", - iterable_type = iterable_type.display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \ - may not be callable", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` method has an incorrect signature \ - for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` method (with type `{dunder_getitem_type}`) \ - may have an incorrect signature for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - } - - Self::UnboundIterAndGetitemError { dunder_getitem_error } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => report_not_iterable(format_args!( - "Object of type `{}` is not iterable because it doesn't have \ - an `__iter__` method or a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!( - "Object of type `{}` may not be iterable because it has no `__iter__` method \ - and it may not have a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because it has no `__iter__` method and \ - its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it has no `__iter__` method and its `__getitem__` attribute \ - may not be callable", - iterable_type = iterable_type.display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it has no `__iter__` method and its `__getitem__` attribute \ - (with type `{dunder_getitem_type}`) may not be callable", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )); - } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because it has no `__iter__` method and \ - its `__getitem__` method has an incorrect signature \ - for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it has no `__iter__` method and \ - its `__getitem__` method (with type `{dunder_getitem_type}`) \ - may have an incorrect signature for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum BoolError<'db> { - /// The type has a `__bool__` attribute but it can't be called. - NotCallable { not_boolable_type: Type<'db> }, - - /// The type has a callable `__bool__` attribute, but it isn't callable - /// with the given arguments. - IncorrectArguments { - not_boolable_type: Type<'db>, - truthiness: Truthiness, - }, - - /// The type has a `__bool__` method, is callable with the given arguments, - /// but the return type isn't assignable to `bool`. - IncorrectReturnType { - not_boolable_type: Type<'db>, - return_type: Type<'db>, - }, - - /// A union type doesn't implement `__bool__` correctly. - Union { - union: UnionType<'db>, - truthiness: Truthiness, - }, - - /// Any other reason why the type can't be converted to a bool. - /// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or - /// because `__bool__` points to a type that has a possibly unbound `__call__` method. - Other { not_boolable_type: Type<'db> }, -} - -impl<'db> BoolError<'db> { - pub(super) fn fallback_truthiness(&self) -> Truthiness { - match self { - BoolError::NotCallable { .. } - | BoolError::IncorrectReturnType { .. } - | BoolError::Other { .. } => Truthiness::Ambiguous, - BoolError::IncorrectArguments { truthiness, .. } - | BoolError::Union { truthiness, .. } => *truthiness, - } - } - - fn not_boolable_type(&self) -> Type<'db> { - match self { - BoolError::NotCallable { - not_boolable_type, .. - } - | BoolError::IncorrectArguments { - not_boolable_type, .. - } - | BoolError::Other { not_boolable_type } - | BoolError::IncorrectReturnType { - not_boolable_type, .. - } => *not_boolable_type, - BoolError::Union { union, .. } => Type::Union(*union), - } - } - - pub(super) fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) { - self.report_diagnostic_impl(context, condition.range()); - } - - fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { - match self { - Self::IncorrectArguments { - not_boolable_type, .. - } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) - ), - ); - } - Self::IncorrectReturnType { - not_boolable_type, - return_type, - } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{not_boolable}`; the return type of its bool method (`{return_type}`) isn't assignable to `bool", - not_boolable = not_boolable_type.display(context.db()), - return_type = return_type.display(context.db()) - ), - ); - } - Self::NotCallable { not_boolable_type } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{}`; its `__bool__` method isn't callable", - not_boolable_type.display(context.db()) - ), - ); - } - Self::Union { union, .. } => { - let first_error = union - .elements(context.db()) - .iter() - .find_map(|element| element.try_bool(context.db()).err()) - .unwrap(); - - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for union `{}` because `{}` doesn't implement `__bool__` correctly", - Type::Union(*union).display(context.db()), - first_error.not_boolable_type().display(context.db()), - ), - ); - } - - Self::Other { not_boolable_type } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) - ), - ); - } - } - } -} - -/// Error returned if a class instantiation call failed -#[derive(Debug)] -enum ConstructorCallError<'db> { - Init(Type<'db>, CallDunderError<'db>), - New(Type<'db>, CallDunderError<'db>), - NewAndInit(Type<'db>, CallDunderError<'db>, CallDunderError<'db>), -} - -impl<'db> ConstructorCallError<'db> { - fn return_type(&self) -> Type<'db> { - match self { - Self::Init(ty, _) => *ty, - Self::New(ty, _) => *ty, - Self::NewAndInit(ty, _, _) => *ty, - } - } - - fn report_diagnostic( - &self, - context: &InferContext<'db>, - context_expression_type: Type<'db>, - context_expression_node: ast::AnyNodeRef, - ) { - let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { - CallDunderError::MethodNotAvailable => { - // If we are using vendored typeshed, it should be impossible to have missing - // or unbound `__init__` method on a class, as all classes have `object` in MRO. - // Thus the following may only trigger if a custom typeshed is used. - context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - context_expression_node, - format_args!( - "`__init__` method is missing on type `{}`. Make sure your `object` in typeshed has its definition.", - context_expression_type.display(context.db()), - ), - ); - } - CallDunderError::PossiblyUnbound(bindings) => { - context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - context_expression_node, - format_args!( - "Method `__init__` on type `{}` is possibly unbound.", - context_expression_type.display(context.db()), - ), - ); - - bindings.report_diagnostics(context, context_expression_node); - } - CallDunderError::CallError(_, bindings) => { - bindings.report_diagnostics(context, context_expression_node); - } - }; - - let report_new_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { - CallDunderError::MethodNotAvailable => { - // We are explicitly checking for `__new__` before attempting to call it, - // so this should never happen. - unreachable!("`__new__` method may not be called if missing"); - } - CallDunderError::PossiblyUnbound(bindings) => { - context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - context_expression_node, - format_args!( - "Method `__new__` on type `{}` is possibly unbound.", - context_expression_type.display(context.db()), - ), - ); - - bindings.report_diagnostics(context, context_expression_node); - } - CallDunderError::CallError(_, bindings) => { - bindings.report_diagnostics(context, context_expression_node); - } - }; - - match self { - Self::Init(_, call_dunder_error) => { - report_init_error(call_dunder_error); - } - Self::New(_, call_dunder_error) => { - report_new_error(call_dunder_error); - } - Self::NewAndInit(_, new_call_dunder_error, init_call_dunder_error) => { - report_new_error(new_call_dunder_error); - report_init_error(init_call_dunder_error); - } - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Truthiness { - /// For an object `x`, `bool(x)` will always return `True` - AlwaysTrue, - /// For an object `x`, `bool(x)` will always return `False` - AlwaysFalse, - /// For an object `x`, `bool(x)` could return either `True` or `False` - Ambiguous, -} - -impl Truthiness { - pub(crate) const fn is_ambiguous(self) -> bool { - matches!(self, Truthiness::Ambiguous) - } - - pub(crate) const fn is_always_false(self) -> bool { - matches!(self, Truthiness::AlwaysFalse) - } - - pub(crate) const fn is_always_true(self) -> bool { - matches!(self, Truthiness::AlwaysTrue) - } - - pub(crate) const fn negate(self) -> Self { - match self { - Self::AlwaysTrue => Self::AlwaysFalse, - Self::AlwaysFalse => Self::AlwaysTrue, - Self::Ambiguous => Self::Ambiguous, - } - } - - pub(crate) const fn negate_if(self, condition: bool) -> Self { - if condition { - self.negate() - } else { - self - } - } - - pub(crate) fn and(self, other: Self) -> Self { - match (self, other) { - (Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue, - (Truthiness::AlwaysFalse, _) | (_, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse, - _ => Truthiness::Ambiguous, - } - } - - fn into_type(self, db: &dyn Db) -> Type { - match self { - Self::AlwaysTrue => Type::BooleanLiteral(true), - Self::AlwaysFalse => Type::BooleanLiteral(false), - Self::Ambiguous => KnownClass::Bool.to_instance(db), - } - } -} - -impl From for Truthiness { - fn from(value: bool) -> Self { - if value { - Truthiness::AlwaysTrue - } else { - Truthiness::AlwaysFalse - } - } -} - -bitflags! { - #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Hash)] - pub struct FunctionDecorators: u8 { - /// `@classmethod` - const CLASSMETHOD = 1 << 0; - /// `@no_type_check` - const NO_TYPE_CHECK = 1 << 1; - /// `@overload` - const OVERLOAD = 1 << 2; - } -} - -#[salsa::interned(debug)] -pub struct FunctionType<'db> { - /// name of the function at definition - #[return_ref] - pub name: ast::name::Name, - - /// Is this a function that we special-case somehow? If so, which one? - known: Option, - - body_scope: ScopeId<'db>, - - /// A set of special decorators that were applied to this function - decorators: FunctionDecorators, - - /// The generic context of a generic function. - generic_context: Option>, - - /// A specialization that should be applied to the function's parameter and return types, - /// either because the function is itself generic, or because it appears in the body of a - /// generic class. - specialization: Option>, -} - -#[salsa::tracked] -impl<'db> FunctionType<'db> { - pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { - self.decorators(db).contains(decorator) - } - - /// Convert the `FunctionType` into a [`Type::Callable`]. - /// - /// This powers the `CallableTypeOf` special form from the `knot_extensions` module. - pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - Type::Callable(CallableType::new(db, self.signature(db).clone())) - } - - /// Returns the [`FileRange`] of the function's name. - pub fn focus_range(self, db: &dyn Db) -> FileRange { - FileRange::new( - self.body_scope(db).file(db), - self.body_scope(db).node(db).expect_function().name.range, - ) - } - - pub fn full_range(self, db: &dyn Db) -> FileRange { - FileRange::new( - self.body_scope(db).file(db), - self.body_scope(db).node(db).expect_function().range, - ) - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - let body_scope = self.body_scope(db); - let index = semantic_index(db, body_scope.file(db)); - index.expect_single_definition(body_scope.node(db).expect_function()) - } - - /// Typed externally-visible signature for this function. - /// - /// This is the signature as seen by external callers, possibly modified by decorators and/or - /// overloaded. - /// - /// ## Why is this a salsa query? - /// - /// This is a salsa query to short-circuit the invalidation - /// when the function's AST node changes. - /// - /// Were this not a salsa query, then the calling query - /// would depend on the function's AST and rerun for every change in that file. - #[salsa::tracked(return_ref)] - pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { - let mut internal_signature = self.internal_signature(db); - - if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - return Signature::todo("return type of overloaded function"); - } - - if let Some(specialization) = self.specialization(db) { - internal_signature.apply_specialization(db, specialization); - } - - internal_signature - } - - /// Typed internally-visible signature for this function. - /// - /// This represents the annotations on the function itself, unmodified by decorators and - /// overloads. - /// - /// These are the parameter and return types that should be used for type checking the body of - /// the function. - /// - /// Don't call this when checking any other file; only when type-checking the function body - /// scope. - fn internal_signature(self, db: &'db dyn Db) -> Signature<'db> { - let scope = self.body_scope(db); - let function_stmt_node = scope.node(db).expect_function(); - let definition = self.definition(db); - Signature::from_function(db, self.generic_context(db), definition, function_stmt_node) - } - - pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { - self.known(db) == Some(known_function) - } - - fn with_generic_context(self, db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { - Self::new( - db, - self.name(db).clone(), - self.known(db), - self.body_scope(db), - self.decorators(db), - Some(generic_context), - self.specialization(db), - ) - } - - fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let specialization = match self.specialization(db) { - Some(existing) => existing.apply_specialization(db, specialization), - None => specialization, - }; - Self::new( - db, - self.name(db).clone(), - self.known(db), - self.body_scope(db), - self.decorators(db), - self.generic_context(db), - Some(specialization), - ) - } -} - -/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might -/// have special behavior. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString)] -#[strum(serialize_all = "snake_case")] -#[cfg_attr(test, derive(strum_macros::EnumIter, strum_macros::IntoStaticStr))] -pub enum KnownFunction { - /// `builtins.isinstance` - #[strum(serialize = "isinstance")] - IsInstance, - /// `builtins.issubclass` - #[strum(serialize = "issubclass")] - IsSubclass, - /// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type` - RevealType, - /// `builtins.len` - Len, - /// `builtins.repr` - Repr, - /// `typing(_extensions).final` - Final, - - /// [`typing(_extensions).no_type_check`](https://typing.python.org/en/latest/spec/directives.html#no-type-check) - NoTypeCheck, - - /// `typing(_extensions).assert_type` - AssertType, - /// `typing(_extensions).assert_never` - AssertNever, - /// `typing(_extensions).cast` - Cast, - /// `typing(_extensions).overload` - Overload, - - /// `abc.abstractmethod` - #[strum(serialize = "abstractmethod")] - AbstractMethod, - - /// `dataclasses.dataclass` - Dataclass, - - /// `inspect.getattr_static` - GetattrStatic, - - /// `knot_extensions.static_assert` - StaticAssert, - /// `knot_extensions.is_equivalent_to` - IsEquivalentTo, - /// `knot_extensions.is_subtype_of` - IsSubtypeOf, - /// `knot_extensions.is_assignable_to` - IsAssignableTo, - /// `knot_extensions.is_disjoint_from` - IsDisjointFrom, - /// `knot_extensions.is_gradual_equivalent_to` - IsGradualEquivalentTo, - /// `knot_extensions.is_fully_static` - IsFullyStatic, - /// `knot_extensions.is_singleton` - IsSingleton, - /// `knot_extensions.is_single_valued` - IsSingleValued, -} - -impl KnownFunction { - pub fn into_constraint_function(self) -> Option { - match self { - Self::IsInstance => Some(KnownConstraintFunction::IsInstance), - Self::IsSubclass => Some(KnownConstraintFunction::IsSubclass), - _ => None, - } - } - - fn try_from_definition_and_name<'db>( - db: &'db dyn Db, - definition: Definition<'db>, - name: &str, - ) -> Option { - let candidate = Self::from_str(name).ok()?; - candidate - .check_module(file_to_module(db, definition.file(db))?.known()?) - .then_some(candidate) - } - - /// Return `true` if `self` is defined in `module` at runtime. - const fn check_module(self, module: KnownModule) -> bool { - match self { - Self::IsInstance | Self::IsSubclass | Self::Len | Self::Repr => module.is_builtins(), - Self::AssertType - | Self::AssertNever - | Self::Cast - | Self::Overload - | Self::RevealType - | Self::Final - | Self::NoTypeCheck => { - matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) - } - Self::AbstractMethod => { - matches!(module, KnownModule::Abc) - } - Self::Dataclass => { - matches!(module, KnownModule::Dataclasses) - } - Self::GetattrStatic => module.is_inspect(), - Self::IsAssignableTo - | Self::IsDisjointFrom - | Self::IsEquivalentTo - | Self::IsGradualEquivalentTo - | Self::IsFullyStatic - | Self::IsSingleValued - | Self::IsSingleton - | Self::IsSubtypeOf - | Self::StaticAssert => module.is_knot_extensions(), - } - } -} - -/// This type represents bound method objects that are created when a method is accessed -/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates -/// a bound method object that represents the `Path.touch` method which is bound to the -/// instance `Path("a.txt")`. -#[salsa::tracked(debug)] -pub struct BoundMethodType<'db> { - /// The function that is being bound. Corresponds to the `__func__` attribute on a - /// bound method object - pub(crate) function: FunctionType<'db>, - /// The instance on which this method has been called. Corresponds to the `__self__` - /// attribute on a bound method object - self_instance: Type<'db>, -} - -impl<'db> BoundMethodType<'db> { - pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - Type::Callable(CallableType::new( - db, - self.function(db).signature(db).bind_self(), - )) - } -} - -/// This type represents the set of all callable objects with a certain signature. -/// It can be written in type expressions using `typing.Callable`. -/// `lambda` expressions are inferred directly as `CallableType`s; all function-literal types -/// are subtypes of a `CallableType`. -#[salsa::interned(debug)] -pub struct CallableType<'db> { - #[return_ref] - signature: Signature<'db>, -} - -impl<'db> CallableType<'db> { - /// Create a callable type which accepts any parameters and returns an `Unknown` type. - pub(crate) fn unknown(db: &'db dyn Db) -> Self { - CallableType::new( - db, - Signature::new(Parameters::unknown(), Some(Type::unknown())), - ) - } - - /// Return a "normalized" version of this `Callable` type. - /// - /// See [`Type::normalized`] for more details. - fn normalized(self, db: &'db dyn Db) -> Self { - let signature = self.signature(db); - let parameters = signature - .parameters() - .iter() - .map(|param| param.normalized(db)) - .collect(); - let return_ty = signature - .return_ty - .map(|return_ty| return_ty.normalized(db)); - CallableType::new(db, Signature::new(parameters, return_ty)) - } - - fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let mut signature = self.signature(db).clone(); - signature.apply_specialization(db, specialization); - Self::new(db, signature) - } -} - -/// Represents a specific instance of `types.MethodWrapperType` -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update)] -pub enum MethodWrapperKind<'db> { - /// Method wrapper for `some_function.__get__` - FunctionTypeDunderGet(FunctionType<'db>), - /// Method wrapper for `some_function.__call__` - FunctionTypeDunderCall(FunctionType<'db>), - /// Method wrapper for `some_property.__get__` - PropertyDunderGet(PropertyInstanceType<'db>), - /// Method wrapper for `some_property.__set__` - PropertyDunderSet(PropertyInstanceType<'db>), - /// Method wrapper for `str.startswith`. - /// We treat this method specially because we want to be able to infer precise Boolean - /// literal return types if the instance and the prefix are both string literals, and - /// this allows us to understand statically known branches for common tests such as - /// `if sys.platform.startswith("freebsd")`. - StrStartswith(StringLiteralType<'db>), -} - -/// Represents a specific instance of `types.WrapperDescriptorType` -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update)] -pub enum WrapperDescriptorKind { - /// `FunctionType.__get__` - FunctionTypeDunderGet, - /// `property.__get__` - PropertyDunderGet, - /// `property.__set__` - PropertyDunderSet, -} - -#[salsa::interned(debug)] -pub struct ModuleLiteralType<'db> { - /// The file in which this module was imported. - /// - /// We need this in order to know which submodules should be attached to it as attributes - /// (because the submodules were also imported in this file). - pub importing_file: File, - - /// The imported module. - pub module: Module, -} - -impl<'db> ModuleLiteralType<'db> { - fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { - // `__dict__` is a very special member that is never overridden by module globals; - // we should always look it up directly as an attribute on `types.ModuleType`, - // never in the global scope of the module. - if name == "__dict__" { - return KnownClass::ModuleType - .to_instance(db) - .member(db, "__dict__") - .symbol; - } - - // If the file that originally imported the module has also imported a submodule - // named `name`, then the result is (usually) that submodule, even if the module - // also defines a (non-module) symbol with that name. - // - // Note that technically, either the submodule or the non-module symbol could take - // priority, depending on the ordering of when the submodule is loaded relative to - // the parent module's `__init__.py` file being evaluated. That said, we have - // chosen to always have the submodule take priority. (This matches pyright's - // current behavior, but is the opposite of mypy's current behavior.) - if let Some(submodule_name) = ModuleName::new(name) { - let importing_file = self.importing_file(db); - let imported_submodules = imported_modules(db, importing_file); - let mut full_submodule_name = self.module(db).name().clone(); - full_submodule_name.extend(&submodule_name); - if imported_submodules.contains(&full_submodule_name) { - if let Some(submodule) = resolve_module(db, &full_submodule_name) { - return Symbol::bound(Type::module_literal(db, importing_file, submodule)); - } - } - } - - imported_symbol(db, self.module(db).file(), name).symbol - } -} - -#[salsa::interned(debug)] -pub struct TypeAliasType<'db> { - #[return_ref] - pub name: ast::name::Name, - - rhs_scope: ScopeId<'db>, -} - -#[salsa::tracked] -impl<'db> TypeAliasType<'db> { - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - let scope = self.rhs_scope(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(); - - semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) - } - - #[salsa::tracked] - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { - let scope = self.rhs_scope(db); - let type_alias_stmt_node = scope.node(db).expect_type_alias(); - let definition = self.definition(db); - definition_expression_type(db, definition, &type_alias_stmt_node.value) - } -} - -/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. -#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(super) struct MetaclassCandidate<'db> { - metaclass: ClassType<'db>, - explicit_metaclass_of: ClassLiteralType<'db>, -} - -#[salsa::interned(debug)] -pub struct UnionType<'db> { - /// The union type includes values in any of these types. - #[return_ref] - elements_boxed: Box<[Type<'db>]>, -} - -impl<'db> UnionType<'db> { - fn elements(self, db: &'db dyn Db) -> &'db [Type<'db>] { - self.elements_boxed(db) - } - - /// Create a union from a list of elements - /// (which may be eagerly simplified into a different variant of [`Type`] altogether). - pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold(UnionBuilder::new(db), |builder, element| { - builder.add(element.into()) - }) - .build() - } - - /// Apply a transformation function to all elements of the union, - /// and create a new union from the resulting set of types. - pub fn map( - &self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - Self::from_elements(db, self.elements(db).iter().map(transform_fn)) - } - - pub(crate) fn filter( - self, - db: &'db dyn Db, - filter_fn: impl FnMut(&&Type<'db>) -> bool, - ) -> Type<'db> { - Self::from_elements(db, self.elements(db).iter().filter(filter_fn)) - } - - pub fn iter(&self, db: &'db dyn Db) -> Iter> { - self.elements(db).iter() - } - - pub(crate) fn map_with_boundness( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, - ) -> Symbol<'db> { - let mut builder = UnionBuilder::new(db); - - let mut all_unbound = true; - let mut possibly_unbound = false; - for ty in self.elements(db) { - let ty_member = transform_fn(ty); - match ty_member { - Symbol::Unbound => { - possibly_unbound = true; - } - Symbol::Type(ty_member, member_boundness) => { - if member_boundness == Boundness::PossiblyUnbound { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - - if all_unbound { - Symbol::Unbound - } else { - Symbol::Type( - builder.build(), - if possibly_unbound { - Boundness::PossiblyUnbound - } else { - Boundness::Bound - }, - ) - } - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> SymbolAndQualifiers<'db>, - ) -> SymbolAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut all_unbound = true; - let mut possibly_unbound = false; - for ty in self.elements(db) { - let SymbolAndQualifiers { - symbol: ty_member, - qualifiers: new_qualifiers, - } = transform_fn(ty); - qualifiers |= new_qualifiers; - match ty_member { - Symbol::Unbound => { - possibly_unbound = true; - } - Symbol::Type(ty_member, member_boundness) => { - if member_boundness == Boundness::PossiblyUnbound { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - SymbolAndQualifiers { - symbol: if all_unbound { - Symbol::Unbound - } else { - Symbol::Type( - builder.build(), - if possibly_unbound { - Boundness::PossiblyUnbound - } else { - Boundness::Bound - }, - ) - }, - qualifiers, - } - } - - pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.elements(db).iter().all(|ty| ty.is_fully_static(db)) - } - - /// Create a new union type with the elements normalized. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - let mut new_elements: Vec> = self - .elements(db) - .iter() - .map(|element| element.normalized(db)) - .collect(); - new_elements.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); - UnionType::new(db, new_elements.into_boxed_slice()) - } - - /// Return `true` if `self` represents the exact same set of possible runtime objects as `other` - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - /// Inlined version of [`UnionType::is_fully_static`] to avoid having to lookup - /// `self.elements` multiple times in the Salsa db in this single method. - #[inline] - fn all_fully_static(db: &dyn Db, elements: &[Type]) -> bool { - elements.iter().all(|ty| ty.is_fully_static(db)) - } - - let self_elements = self.elements(db); - let other_elements = other.elements(db); - - if self_elements.len() != other_elements.len() { - return false; - } - - if !all_fully_static(db, self_elements) { - return false; - } - - if !all_fully_static(db, other_elements) { - return false; - } - - if self == other { - return true; - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return true; - } - - sorted_self == other.normalized(db) - } - - /// Return `true` if `self` has exactly the same set of possible static materializations as `other` - /// (if `self` represents the same set of possible sets of possible runtime objects as `other`) - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - if self == other { - return true; - } - - // TODO: `T | Unknown` should be gradually equivalent to `T | Unknown | Any`, - // since they have exactly the same set of possible static materializations - // (they represent the same set of possible sets of possible runtime objects) - if self.elements(db).len() != other.elements(db).len() { - return false; - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return true; - } - - let sorted_other = other.normalized(db); - - if sorted_self == sorted_other { - return true; - } - - sorted_self - .elements(db) - .iter() - .zip(sorted_other.elements(db)) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } -} - -#[salsa::interned(debug)] -pub struct IntersectionType<'db> { - /// The intersection type includes only values in all of these types. - #[return_ref] - positive: FxOrderSet>, - - /// The intersection type does not include any value in any of these types. - /// - /// Negation types aren't expressible in annotations, and are most likely to arise from type - /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them - /// directly in intersections rather than as a separate type. - #[return_ref] - negative: FxOrderSet>, -} - -impl<'db> IntersectionType<'db> { - /// Return a new `IntersectionType` instance with the positive and negative types sorted - /// according to a canonical ordering, and other normalizations applied to each element as applicable. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - fn normalized_set<'db>( - db: &'db dyn Db, - elements: &FxOrderSet>, - ) -> FxOrderSet> { - let mut elements: FxOrderSet> = - elements.iter().map(|ty| ty.normalized(db)).collect(); - - elements.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); - elements - } - - IntersectionType::new( - db, - normalized_set(db, self.positive(db)), - normalized_set(db, self.negative(db)), - ) - } - - pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.positive(db).iter().all(|ty| ty.is_fully_static(db)) - && self.negative(db).iter().all(|ty| ty.is_fully_static(db)) - } - - /// Return `true` if `self` represents exactly the same set of possible runtime objects as `other` - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - /// Inlined version of [`IntersectionType::is_fully_static`] to avoid having to lookup - /// `positive` and `negative` multiple times in the Salsa db in this single method. - #[inline] - fn all_fully_static(db: &dyn Db, elements: &FxOrderSet) -> bool { - elements.iter().all(|ty| ty.is_fully_static(db)) - } - - let self_positive = self.positive(db); - - if !all_fully_static(db, self_positive) { - return false; - } - - let other_positive = other.positive(db); - - if self_positive.len() != other_positive.len() { - return false; - } - - if !all_fully_static(db, other_positive) { - return false; - } - - let self_negative = self.negative(db); - - if !all_fully_static(db, self_negative) { - return false; - } - - let other_negative = other.negative(db); - - if self_negative.len() != other_negative.len() { - return false; - } - - if !all_fully_static(db, other_negative) { - return false; - } - - if self == other { - return true; - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return true; - } - - sorted_self == other.normalized(db) - } - - /// Return `true` if `self` has exactly the same set of possible static materializations as `other` - /// (if `self` represents the same set of possible sets of possible runtime objects as `other`) - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - if self == other { - return true; - } - - if self.positive(db).len() != other.positive(db).len() - || self.negative(db).len() != other.negative(db).len() - { - return false; - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return true; - } - - let sorted_other = other.normalized(db); - - if sorted_self == sorted_other { - return true; - } - - sorted_self - .positive(db) - .iter() - .zip(sorted_other.positive(db)) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - && sorted_self - .negative(db) - .iter() - .zip(sorted_other.negative(db)) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } - - pub(crate) fn map_with_boundness( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, - ) -> Symbol<'db> { - if !self.negative(db).is_empty() { - return Symbol::todo("map_with_boundness: intersections with negative contributions"); - } - - let mut builder = IntersectionBuilder::new(db); - - let mut all_unbound = true; - let mut any_definitely_bound = false; - for ty in self.positive(db) { - let ty_member = transform_fn(ty); - match ty_member { - Symbol::Unbound => {} - Symbol::Type(ty_member, member_boundness) => { - all_unbound = false; - if member_boundness == Boundness::Bound { - any_definitely_bound = true; - } - - builder = builder.add_positive(ty_member); - } - } - } - - if all_unbound { - Symbol::Unbound - } else { - Symbol::Type( - builder.build(), - if any_definitely_bound { - Boundness::Bound - } else { - Boundness::PossiblyUnbound - }, - ) - } - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> SymbolAndQualifiers<'db>, - ) -> SymbolAndQualifiers<'db> { - if !self.negative(db).is_empty() { - return Symbol::todo("map_with_boundness: intersections with negative contributions") - .into(); - } - - let mut builder = IntersectionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut any_unbound = false; - let mut any_possibly_unbound = false; - for ty in self.positive(db) { - let SymbolAndQualifiers { - symbol: member, - qualifiers: new_qualifiers, - } = transform_fn(ty); - qualifiers |= new_qualifiers; - match member { - Symbol::Unbound => { - any_unbound = true; - } - Symbol::Type(ty_member, member_boundness) => { - if member_boundness == Boundness::PossiblyUnbound { - any_possibly_unbound = true; - } - - builder = builder.add_positive(ty_member); - } - } - } - - SymbolAndQualifiers { - symbol: if any_unbound { - Symbol::Unbound - } else { - Symbol::Type( - builder.build(), - if any_possibly_unbound { - Boundness::PossiblyUnbound - } else { - Boundness::Bound - }, - ) - }, - qualifiers, - } - } - - pub fn iter_positive(&self, db: &'db dyn Db) -> impl Iterator> { - self.positive(db).iter().copied() - } -} - -#[salsa::interned(debug)] -pub struct StringLiteralType<'db> { - #[return_ref] - value: Box, -} - -impl<'db> StringLiteralType<'db> { - /// The length of the string, as would be returned by Python's `len()`. - pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { - self.value(db).chars().count() - } - - /// Return an iterator over each character in the string literal. - /// as would be returned by Python's `iter()`. - pub(crate) fn iter_each_char(self, db: &'db dyn Db) -> impl Iterator { - self.value(db) - .chars() - .map(|c| StringLiteralType::new(db, c.to_string().as_str())) - } -} - -#[salsa::interned(debug)] -pub struct BytesLiteralType<'db> { - #[return_ref] - value: Box<[u8]>, -} - -impl<'db> BytesLiteralType<'db> { - pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { - self.value(db).len() - } -} - -#[salsa::interned(debug)] -pub struct SliceLiteralType<'db> { - start: Option, - stop: Option, - step: Option, -} - -impl SliceLiteralType<'_> { - fn as_tuple(self, db: &dyn Db) -> (Option, Option, Option) { - (self.start(db), self.stop(db), self.step(db)) - } -} -#[salsa::interned(debug)] -pub struct TupleType<'db> { - #[return_ref] - elements: Box<[Type<'db>]>, -} - -impl<'db> TupleType<'db> { - pub(crate) fn from_elements>>( - db: &'db dyn Db, - types: impl IntoIterator, - ) -> Type<'db> { - let mut elements = vec![]; - - for ty in types { - let ty = ty.into(); - if ty.is_never() { - return Type::Never; - } - elements.push(ty); - } - - Type::Tuple(Self::new(db, elements.into_boxed_slice())) - } - - /// Return a normalized version of `self`. - /// - /// See [`Type::normalized`] for more details. - #[must_use] - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - let elements: Box<[Type<'db>]> = self - .elements(db) - .iter() - .map(|ty| ty.normalized(db)) - .collect(); - TupleType::new(db, elements) - } - - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - let self_elements = self.elements(db); - let other_elements = other.elements(db); - self_elements.len() == other_elements.len() - && self_elements - .iter() - .zip(other_elements) - .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) - } - - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - let self_elements = self.elements(db); - let other_elements = other.elements(db); - self_elements.len() == other_elements.len() - && self_elements - .iter() - .zip(other_elements) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } - - pub fn get(&self, db: &'db dyn Db, index: usize) -> Option> { - self.elements(db).get(index).copied() - } - - pub fn len(&self, db: &'db dyn Db) -> usize { - self.elements(db).len() - } - - pub fn iter(&self, db: &'db dyn Db) -> impl Iterator> + 'db + '_ { - self.elements(db).iter().copied() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum BoundSuperError<'db> { - InvalidPivotClassType { - pivot_class: Type<'db>, - }, - FailingConditionCheck { - pivot_class: Type<'db>, - owner: Type<'db>, - }, - UnavailableImplicitArguments, -} - -impl BoundSuperError<'_> { - pub(super) fn report_diagnostic(&self, context: &InferContext, node: AnyNodeRef) { - match self { - BoundSuperError::InvalidPivotClassType { pivot_class } => { - context.report_lint_old( - &INVALID_SUPER_ARGUMENT, - node, - format_args!( - "`{pivot_class}` is not a valid class", - pivot_class = pivot_class.display(context.db()), - ), - ); - } - BoundSuperError::FailingConditionCheck { pivot_class, owner } => { - context.report_lint_old( - &INVALID_SUPER_ARGUMENT, - node, - format_args!( - "`{owner}` is not an instance or subclass of `{pivot_class}` in `super({pivot_class}, {owner})` call", - pivot_class = pivot_class.display(context.db()), - owner = owner.display(context.db()), - ), - ); - } - BoundSuperError::UnavailableImplicitArguments => { - context.report_lint_old( - &UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, - node, - format_args!( - "Cannot determine implicit arguments for 'super()' in this context", - ), - ); - } - } - } -} - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub enum SuperOwnerKind<'db> { - Dynamic(DynamicType), - Class(ClassType<'db>), - Instance(InstanceType<'db>), -} - -impl<'db> SuperOwnerKind<'db> { - fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { - match self { - SuperOwnerKind::Dynamic(dynamic) => Either::Left(ClassBase::Dynamic(dynamic).mro(db)), - SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), - SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)), - } - } - - fn into_type(self) -> Type<'db> { - match self { - SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic), - SuperOwnerKind::Class(class) => class.into(), - SuperOwnerKind::Instance(instance) => instance.into(), - } - } - - fn into_class(self) -> Option> { - match self { - SuperOwnerKind::Dynamic(_) => None, - SuperOwnerKind::Class(class) => Some(class), - SuperOwnerKind::Instance(instance) => Some(instance.class), - } - } - - fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { - match ty { - Type::Dynamic(dynamic) => Some(SuperOwnerKind::Dynamic(dynamic)), - Type::ClassLiteral(class_literal) => Some(SuperOwnerKind::Class( - class_literal.apply_optional_specialization(db, None), - )), - Type::Instance(instance) => Some(SuperOwnerKind::Instance(instance)), - Type::BooleanLiteral(_) => { - SuperOwnerKind::try_from_type(db, KnownClass::Bool.to_instance(db)) - } - Type::IntLiteral(_) => { - SuperOwnerKind::try_from_type(db, KnownClass::Int.to_instance(db)) - } - Type::StringLiteral(_) => { - SuperOwnerKind::try_from_type(db, KnownClass::Str.to_instance(db)) - } - Type::LiteralString => { - SuperOwnerKind::try_from_type(db, KnownClass::Str.to_instance(db)) - } - Type::BytesLiteral(_) => { - SuperOwnerKind::try_from_type(db, KnownClass::Bytes.to_instance(db)) - } - Type::KnownInstance(known_instance) => { - SuperOwnerKind::try_from_type(db, known_instance.instance_fallback(db)) - } - _ => None, - } - } -} - -/// Represent a bound super object like `super(PivotClass, owner)` -#[salsa::interned(debug)] -pub struct BoundSuperType<'db> { - #[return_ref] - pub pivot_class: ClassBase<'db>, - #[return_ref] - pub owner: SuperOwnerKind<'db>, -} - -impl<'db> BoundSuperType<'db> { - /// Attempts to build a `Type::BoundSuper` based on the given `pivot_class` and `owner`. - /// - /// This mimics the behavior of Python's built-in `super(pivot, owner)` at runtime. - /// - `super(pivot, owner_class)` is valid only if `issubclass(owner_class, pivot)` - /// - `super(pivot, owner_instance)` is valid only if `isinstance(owner_instance, pivot)` - /// - /// However, the checking is skipped when any of the arguments is a dynamic type. - fn build( - db: &'db dyn Db, - pivot_class_type: Type<'db>, - owner_type: Type<'db>, - ) -> Result, BoundSuperError<'db>> { - if let Type::Union(union) = owner_type { - return Ok(UnionType::from_elements( - db, - union - .elements(db) - .iter() - .map(|ty| BoundSuperType::build(db, pivot_class_type, *ty)) - .collect::, _>>()?, - )); - } - - let pivot_class = ClassBase::try_from_type(db, pivot_class_type).ok_or({ - BoundSuperError::InvalidPivotClassType { - pivot_class: pivot_class_type, - } - })?; - - let owner = SuperOwnerKind::try_from_type(db, owner_type) - .and_then(|owner| { - let Some(pivot_class) = pivot_class.into_class() else { - return Some(owner); - }; - let Some(owner_class) = owner.into_class() else { - return Some(owner); - }; - if owner_class.is_subclass_of(db, pivot_class) { - Some(owner) - } else { - None - } - }) - .ok_or(BoundSuperError::FailingConditionCheck { - pivot_class: pivot_class_type, - owner: owner_type, - })?; - - Ok(Type::BoundSuper(BoundSuperType::new( - db, - pivot_class, - owner, - ))) - } - - /// Skips elements in the MRO up to and including the pivot class. - /// - /// If the pivot class is a dynamic type, its MRO can't be determined, - /// so we fall back to using the MRO of `DynamicType::Unknown`. - fn skip_until_after_pivot( - self, - db: &'db dyn Db, - mro_iter: impl Iterator>, - ) -> impl Iterator> { - let Some(pivot_class) = self.pivot_class(db).into_class() else { - return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db)); - }; - - let mut pivot_found = false; - - Either::Right(mro_iter.skip_while(move |superclass| { - if pivot_found { - false - } else if Some(pivot_class) == superclass.into_class() { - pivot_found = true; - true - } else { - true - } - })) - } - - /// Tries to call `__get__` on the attribute. - /// The arguments passed to `__get__` depend on whether the owner is an instance or a class. - /// See the `CPython` implementation for reference: - /// - fn try_call_dunder_get_on_attribute( - self, - db: &'db dyn Db, - attribute: SymbolAndQualifiers<'db>, - ) -> Option> { - let owner = self.owner(db); - - match owner { - // If the owner is a dynamic type, we can't tell whether it's a class or an instance. - // Also, invoking a descriptor on a dynamic attribute is meaningless, so we don't handle this. - SuperOwnerKind::Dynamic(_) => None, - SuperOwnerKind::Class(_) => Some( - Type::try_call_dunder_get_on_attribute( - db, - attribute, - Type::none(db), - owner.into_type(), - ) - .0, - ), - SuperOwnerKind::Instance(_) => Some( - Type::try_call_dunder_get_on_attribute( - db, - attribute, - owner.into_type(), - owner.into_type().to_meta_type(db), - ) - .0, - ), - } - } - - /// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the - /// pivot class in the MRO, based on the `owner` type instead of the `super` type. - fn find_name_in_mro_after_pivot( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - let owner = self.owner(db); - match owner { - SuperOwnerKind::Dynamic(_) => owner - .into_type() - .find_name_in_mro_with_policy(db, name, policy) - .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"), - SuperOwnerKind::Class(class) | SuperOwnerKind::Instance(InstanceType { class }) => { - let (class_literal, _) = class.class_literal(db); - // TODO properly support super() with generic types - // * requires a fix for https://github.com/astral-sh/ruff/issues/17432 - // * also requires understanding how we should handle cases like this: - // ```python - // b_int: B[int] - // b_unknown: B - // - // super(B, b_int) - // super(B[int], b_unknown) - // ``` - match class_literal { - ClassLiteralType::Generic(_) => { - Symbol::bound(todo_type!("super in generic class")).into() - } - ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro( - db, - name, - policy, - self.skip_until_after_pivot(db, owner.iter_mro(db)), - ), - } - } - } - } -} - -// Make sure that the `Type` enum does not grow unexpectedly. -#[cfg(not(debug_assertions))] -#[cfg(target_pointer_width = "64")] -static_assertions::assert_eq_size!(Type, [u8; 16]); - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::db::tests::{setup_db, TestDbBuilder}; - use crate::symbol::{ - global_symbol, known_module_symbol, typing_extensions_symbol, typing_symbol, - }; - use ruff_db::files::system_path_to_file; - use ruff_db::parsed::parsed_module; - use ruff_db::system::DbWithWritableSystem as _; - use ruff_db::testing::assert_function_query_was_not_run; - use ruff_python_ast::PythonVersion; - use strum::IntoEnumIterator; - use test_case::test_case; - - /// Explicitly test for Python version <3.13 and >=3.13, to ensure that - /// the fallback to `typing_extensions` is working correctly. - /// See [`KnownClass::canonical_module`] for more information. - #[test_case(PythonVersion::PY312)] - #[test_case(PythonVersion::PY313)] - fn no_default_type_is_singleton(python_version: PythonVersion) { - let db = TestDbBuilder::new() - .with_python_version(python_version) - .build() - .unwrap(); - - let no_default = KnownClass::NoDefaultType.to_instance(&db); - - assert!(no_default.is_singleton(&db)); - } - - #[test] - fn typing_vs_typeshed_no_default() { - let db = TestDbBuilder::new() - .with_python_version(PythonVersion::PY313) - .build() - .unwrap(); - - let typing_no_default = typing_symbol(&db, "NoDefault").symbol.expect_type(); - let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") - .symbol - .expect_type(); - - assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); - assert_eq!( - typing_extensions_no_default.display(&db).to_string(), - "NoDefault" - ); - } - - /// Inferring the result of a call-expression shouldn't need to re-run after - /// a trivial change to the function's file (e.g. by adding a docstring to the function). - #[test] - fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/foo.py", - r#" - def foo() -> int: - return 5 - "#, - )?; - db.write_dedented( - "src/bar.py", - r#" - from foo import foo - - a = foo() - "#, - )?; - - let bar = system_path_to_file(&db, "src/bar.py")?; - let a = global_symbol(&db, bar, "a").symbol; - - assert_eq!( - a.expect_type(), - UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)]) - ); - - // Add a docstring to foo to trigger a re-run. - // The bar-call site of foo should not be re-run because of that - db.write_dedented( - "src/foo.py", - r#" - def foo() -> int: - "Computes a value" - return 5 - "#, - )?; - db.clear_salsa_events(); - - let a = global_symbol(&db, bar, "a").symbol; - - assert_eq!( - a.expect_type(), - UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)]) - ); - let events = db.take_salsa_events(); - - let call = &*parsed_module(&db, bar).syntax().body[1] - .as_assign_stmt() - .unwrap() - .value; - let foo_call = semantic_index(&db, bar).expression(call); - - assert_function_query_was_not_run(&db, infer_expression_types, foo_call, &events); - - Ok(()) - } - - /// All other tests also make sure that `Type::Todo` works as expected. This particular - /// test makes sure that we handle `Todo` types correctly, even if they originate from - /// different sources. - #[test] - fn todo_types() { - let db = setup_db(); - - let todo1 = todo_type!("1"); - let todo2 = todo_type!("2"); - - let int = KnownClass::Int.to_instance(&db); - - assert!(int.is_assignable_to(&db, todo1)); - - assert!(todo1.is_assignable_to(&db, int)); - - // We lose information when combining several `Todo` types. This is an - // acknowledged limitation of the current implementation. We can not - // easily store the meta information of several `Todo`s in a single - // variant, as `TodoType` needs to implement `Copy`, meaning it can't - // contain `Vec`/`Box`/etc., and can't be boxed itself. - // - // Lifting this restriction would require us to intern `TodoType` in - // salsa, but that would mean we would have to pass in `db` everywhere. - - // A union of several `Todo` types collapses to a single `Todo` type: - assert!(UnionType::from_elements(&db, vec![todo1, todo2]).is_todo()); - - // And similar for intersection types: - assert!(IntersectionBuilder::new(&db) - .add_positive(todo1) - .add_positive(todo2) - .build() - .is_todo()); - assert!(IntersectionBuilder::new(&db) - .add_positive(todo1) - .add_negative(todo2) - .build() - .is_todo()); - } - - #[test] - fn known_function_roundtrip_from_str() { - let db = setup_db(); - - for function in KnownFunction::iter() { - let function_name: &'static str = function.into(); - - let module = match function { - KnownFunction::Len - | KnownFunction::Repr - | KnownFunction::IsInstance - | KnownFunction::IsSubclass => KnownModule::Builtins, - - KnownFunction::AbstractMethod => KnownModule::Abc, - - KnownFunction::Dataclass => KnownModule::Dataclasses, - - KnownFunction::GetattrStatic => KnownModule::Inspect, - - KnownFunction::Cast - | KnownFunction::Final - | KnownFunction::Overload - | KnownFunction::RevealType - | KnownFunction::AssertType - | KnownFunction::AssertNever - | KnownFunction::NoTypeCheck => KnownModule::TypingExtensions, - - KnownFunction::IsSingleton - | KnownFunction::IsSubtypeOf - | KnownFunction::StaticAssert - | KnownFunction::IsFullyStatic - | KnownFunction::IsDisjointFrom - | KnownFunction::IsSingleValued - | KnownFunction::IsAssignableTo - | KnownFunction::IsEquivalentTo - | KnownFunction::IsGradualEquivalentTo => KnownModule::KnotExtensions, - }; - - let function_definition = known_module_symbol(&db, module, function_name) - .symbol - .expect_type() - .expect_function_literal() - .definition(&db); - - assert_eq!( - KnownFunction::try_from_definition_and_name(&db, function_definition, function_name), - Some(function), - "The strum `EnumString` implementation appears to be incorrect for `{function_name}`" - ); - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs deleted file mode 100644 index 411c080257843..0000000000000 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ /dev/null @@ -1,832 +0,0 @@ -//! Smart builders for union and intersection types. -//! -//! Invariants we maintain here: -//! * No single-element union types (should just be the contained type instead.) -//! * No single-positive-element intersection types. Single-negative-element are OK, we don't -//! have a standalone negation type so there's no other representation for this. -//! * The same type should never appear more than once in a union or intersection. (This should -//! be expanded to cover subtyping -- see below -- but for now we only implement it for type -//! identity.) -//! * Disjunctive normal form (DNF): the tree of unions and intersections can never be deeper -//! than a union-of-intersections. Unions cannot contain other unions (the inner union just -//! flattens into the outer one), intersections cannot contain other intersections (also -//! flattens), and intersections cannot contain unions (the intersection distributes over the -//! union, inverting it into a union-of-intersections). -//! * No type in a union can be a subtype of any other type in the union (just eliminate the -//! subtype from the union). -//! * No type in an intersection can be a supertype of any other type in the intersection (just -//! eliminate the supertype from the intersection). -//! * An intersection containing two non-overlapping types simplifies to [`Type::Never`]. -//! -//! The implication of these invariants is that a [`UnionBuilder`] does not necessarily build a -//! [`Type::Union`]. For example, if only one type is added to the [`UnionBuilder`], `build()` will -//! just return that type directly. The same is true for [`IntersectionBuilder`]; for example, if a -//! union type is added to the intersection, it will distribute and [`IntersectionBuilder::build`] -//! may end up returning a [`Type::Union`] of intersections. -//! -//! ## Performance -//! -//! In practice, there are two kinds of unions found in the wild: relatively-small unions made up -//! of normal user types (classes, etc), and large unions made up of literals, which can occur via -//! large enums (not yet implemented) or from string/integer/bytes literals, which can grow due to -//! literal arithmetic or operations on literal strings/bytes. For normal unions, it's most -//! efficient to just store the member types in a vector, and do O(n^2) `is_subtype_of` checks to -//! maintain the union in simplified form. But literal unions can grow to a size where this becomes -//! a performance problem. For this reason, we group literal types in `UnionBuilder`. Since every -//! different string literal type shares exactly the same possible super-types, and none of them -//! are subtypes of each other (unless exactly the same literal type), we can avoid many -//! unnecessary `is_subtype_of` checks. - -use crate::types::{ - BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type, - TypeVarBoundOrConstraints, UnionType, -}; -use crate::{Db, FxOrderSet}; -use smallvec::SmallVec; - -enum UnionElement<'db> { - IntLiterals(FxOrderSet), - StringLiterals(FxOrderSet>), - BytesLiterals(FxOrderSet>), - Type(Type<'db>), -} - -// TODO increase this once we extend `UnionElement` throughout all union/intersection -// representations, so that we can make large unions of literals fast in all operations. -const MAX_UNION_LITERALS: usize = 200; - -pub(crate) struct UnionBuilder<'db> { - elements: Vec>, - db: &'db dyn Db, -} - -impl<'db> UnionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { - Self { - db, - elements: vec![], - } - } - - pub(crate) fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - /// Collapse the union to a single type: `object`. - fn collapse_to_object(&mut self) { - self.elements.clear(); - self.elements - .push(UnionElement::Type(Type::object(self.db))); - } - - /// Adds a type to this union. - pub(crate) fn add(mut self, ty: Type<'db>) -> Self { - self.add_in_place(ty); - self - } - - /// Adds a type to this union. - pub(crate) fn add_in_place(&mut self, ty: Type<'db>) { - match ty { - Type::Union(union) => { - let new_elements = union.elements(self.db); - self.elements.reserve(new_elements.len()); - for element in new_elements { - self.add_in_place(*element); - } - } - // Adding `Never` to a union is a no-op. - Type::Never => {} - // If adding a string literal, look for an existing `UnionElement::StringLiterals` to - // add it to, or an existing element that is a super-type of string literals, which - // means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals` - // containing it. - Type::StringLiteral(literal) => { - let mut found = false; - for element in &mut self.elements { - match element { - UnionElement::StringLiterals(literals) => { - if literals.len() >= MAX_UNION_LITERALS { - let replace_with = KnownClass::Str.to_instance(self.db); - self.add_in_place(replace_with); - return; - } - literals.insert(literal); - found = true; - break; - } - UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => { - return; - } - _ => {} - } - } - if !found { - self.elements - .push(UnionElement::StringLiterals(FxOrderSet::from_iter([ - literal, - ]))); - } - } - // Same for bytes literals as for string literals, above. - Type::BytesLiteral(literal) => { - let mut found = false; - for element in &mut self.elements { - match element { - UnionElement::BytesLiterals(literals) => { - if literals.len() >= MAX_UNION_LITERALS { - let replace_with = KnownClass::Bytes.to_instance(self.db); - self.add_in_place(replace_with); - return; - } - literals.insert(literal); - found = true; - break; - } - UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => { - return; - } - _ => {} - } - } - if !found { - self.elements - .push(UnionElement::BytesLiterals(FxOrderSet::from_iter([ - literal, - ]))); - } - } - // And same for int literals as well. - Type::IntLiteral(literal) => { - let mut found = false; - for element in &mut self.elements { - match element { - UnionElement::IntLiterals(literals) => { - if literals.len() >= MAX_UNION_LITERALS { - let replace_with = KnownClass::Int.to_instance(self.db); - self.add_in_place(replace_with); - return; - } - literals.insert(literal); - found = true; - break; - } - UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => { - return; - } - _ => {} - } - } - if !found { - self.elements - .push(UnionElement::IntLiterals(FxOrderSet::from_iter([literal]))); - } - } - // Adding `object` to a union results in `object`. - ty if ty.is_object(self.db) => { - self.collapse_to_object(); - } - _ => { - let bool_pair = if let Type::BooleanLiteral(b) = ty { - Some(Type::BooleanLiteral(!b)) - } else { - None - }; - - let mut to_add = ty; - let mut to_remove = SmallVec::<[usize; 2]>::new(); - let ty_negated = ty.negate(self.db); - - for (index, element) in self - .elements - .iter() - .map(|element| { - // For literals, the first element in the set can stand in for all the rest, - // since they all have the same super-types. SAFETY: a `UnionElement` of - // literal kind must always have at least one element in it. - match element { - UnionElement::IntLiterals(literals) => Type::IntLiteral(literals[0]), - UnionElement::StringLiterals(literals) => { - Type::StringLiteral(literals[0]) - } - UnionElement::BytesLiterals(literals) => { - Type::BytesLiteral(literals[0]) - } - UnionElement::Type(ty) => *ty, - } - }) - .enumerate() - { - if Some(element) == bool_pair { - to_add = KnownClass::Bool.to_instance(self.db); - to_remove.push(index); - // The type we are adding is a BooleanLiteral, which doesn't have any - // subtypes. And we just found that the union already contained our - // mirror-image BooleanLiteral, so it can't also contain bool or any - // supertype of bool. Therefore, we are done. - break; - } - - if ty.is_same_gradual_form(element) - || ty.is_subtype_of(self.db, element) - || element.is_object(self.db) - { - return; - } else if element.is_subtype_of(self.db, ty) { - to_remove.push(index); - } else if ty_negated.is_subtype_of(self.db, element) { - // We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`. - // This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the - // first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is - // just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that - // `element | ty` must be `object` (object has no other supertypes). This means we can simplify - // the whole union to just `object`, since all other potential elements would also be subtypes of - // `object`. - self.collapse_to_object(); - return; - } - } - if let Some((&first, rest)) = to_remove.split_first() { - self.elements[first] = UnionElement::Type(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(UnionElement::Type(to_add)); - } - } - } - } - - pub(crate) fn build(self) -> Type<'db> { - let mut types = vec![]; - for element in self.elements { - match element { - UnionElement::IntLiterals(literals) => { - types.extend(literals.into_iter().map(Type::IntLiteral)); - } - UnionElement::StringLiterals(literals) => { - types.extend(literals.into_iter().map(Type::StringLiteral)); - } - UnionElement::BytesLiterals(literals) => { - types.extend(literals.into_iter().map(Type::BytesLiteral)); - } - UnionElement::Type(ty) => types.push(ty), - } - } - match types.len() { - 0 => Type::Never, - 1 => types[0], - _ => Type::Union(UnionType::new(self.db, types.into_boxed_slice())), - } - } -} - -#[derive(Clone)] -pub(crate) struct IntersectionBuilder<'db> { - // Really this builds a union-of-intersections, because we always keep our set-theoretic types - // in disjunctive normal form (DNF), a union of intersections. In the simplest case there's - // just a single intersection in this vector, and we are building a single intersection type, - // but if a union is added to the intersection, we'll distribute ourselves over that union and - // create a union of intersections. - intersections: Vec>, - db: &'db dyn Db, -} - -impl<'db> IntersectionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { - Self { - db, - intersections: vec![InnerIntersectionBuilder::default()], - } - } - - fn empty(db: &'db dyn Db) -> Self { - Self { - db, - intersections: vec![], - } - } - - pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self { - if let Type::Union(union) = ty { - // Distribute ourself over this union: for each union element, clone ourself and - // intersect with that union element, then create a new union-of-intersections with all - // of those sub-intersections in it. E.g. if `self` is a simple intersection `T1 & T2` - // and we add `T3 | T4` to the intersection, we don't get `T1 & T2 & (T3 | T4)` (that's - // not in DNF), we distribute the union and get `(T1 & T3) | (T2 & T3) | (T1 & T4) | - // (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)` - // and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 & - // T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea. - union - .elements(self.db) - .iter() - .map(|elem| self.clone().add_positive(*elem)) - .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { - builder.intersections.extend(sub.intersections); - builder - }) - } else { - // If we are already a union-of-intersections, distribute the new intersected element - // across all of those intersections. - for inner in &mut self.intersections { - inner.add_positive(self.db, ty); - } - self - } - } - - pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self { - // See comments above in `add_positive`; this is just the negated version. - if let Type::Union(union) = ty { - for elem in union.elements(self.db) { - self = self.add_negative(*elem); - } - self - } else if let Type::Intersection(intersection) = ty { - // (A | B) & ~(C & ~D) - // -> (A | B) & (~C | D) - // -> ((A | B) & ~C) | ((A | B) & D) - // i.e. if we have an intersection of positive constraints C - // and negative constraints D, then our new intersection - // is (existing & ~C) | (existing & D) - - let positive_side = intersection - .positive(self.db) - .iter() - // we negate all the positive constraints while distributing - .map(|elem| self.clone().add_negative(*elem)); - - let negative_side = intersection - .negative(self.db) - .iter() - // all negative constraints end up becoming positive constraints - .map(|elem| self.clone().add_positive(*elem)); - - positive_side.chain(negative_side).fold( - IntersectionBuilder::empty(self.db), - |mut builder, sub| { - builder.intersections.extend(sub.intersections); - builder - }, - ) - } else { - for inner in &mut self.intersections { - inner.add_negative(self.db, ty); - } - self - } - } - - pub(crate) fn build(mut self) -> Type<'db> { - // Avoid allocating the UnionBuilder unnecessarily if we have just one intersection: - if self.intersections.len() == 1 { - self.intersections.pop().unwrap().build(self.db) - } else { - UnionType::from_elements( - self.db, - self.intersections - .into_iter() - .map(|inner| inner.build(self.db)), - ) - } - } -} - -#[derive(Debug, Clone, Default)] -struct InnerIntersectionBuilder<'db> { - positive: FxOrderSet>, - negative: FxOrderSet>, -} - -impl<'db> InnerIntersectionBuilder<'db> { - /// Adds a positive type to this intersection. - fn add_positive(&mut self, db: &'db dyn Db, mut new_positive: Type<'db>) { - match new_positive { - // `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` - Type::AlwaysTruthy if self.positive.contains(&Type::LiteralString) => { - self.add_negative(db, Type::string_literal(db, "")); - } - // `LiteralString & AlwaysFalsy` -> `Literal[""]` - Type::AlwaysFalsy if self.positive.swap_remove(&Type::LiteralString) => { - self.add_positive(db, Type::string_literal(db, "")); - } - // `AlwaysTruthy & LiteralString` -> `LiteralString & ~Literal[""]` - Type::LiteralString if self.positive.swap_remove(&Type::AlwaysTruthy) => { - self.add_positive(db, Type::LiteralString); - self.add_negative(db, Type::string_literal(db, "")); - } - // `AlwaysFalsy & LiteralString` -> `Literal[""]` - Type::LiteralString if self.positive.swap_remove(&Type::AlwaysFalsy) => { - self.add_positive(db, Type::string_literal(db, "")); - } - // `LiteralString & ~AlwaysTruthy` -> `LiteralString & AlwaysFalsy` -> `Literal[""]` - Type::LiteralString if self.negative.swap_remove(&Type::AlwaysTruthy) => { - self.add_positive(db, Type::string_literal(db, "")); - } - // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` - Type::LiteralString if self.negative.swap_remove(&Type::AlwaysFalsy) => { - self.add_positive(db, Type::LiteralString); - self.add_negative(db, Type::string_literal(db, "")); - } - // `(A & B & ~C) & (D & E & ~F)` -> `A & B & D & E & ~C & ~F` - Type::Intersection(other) => { - for pos in other.positive(db) { - self.add_positive(db, *pos); - } - for neg in other.negative(db) { - self.add_negative(db, *neg); - } - } - _ => { - let known_instance = new_positive - .into_instance() - .and_then(|instance| instance.class.known(db)); - - if known_instance == Some(KnownClass::Object) { - // `object & T` -> `T`; it is always redundant to add `object` to an intersection - return; - } - - let addition_is_bool_instance = known_instance == Some(KnownClass::Bool); - - for (index, existing_positive) in self.positive.iter().enumerate() { - match existing_positive { - // `AlwaysTruthy & bool` -> `Literal[True]` - Type::AlwaysTruthy if addition_is_bool_instance => { - new_positive = Type::BooleanLiteral(true); - } - // `AlwaysFalsy & bool` -> `Literal[False]` - Type::AlwaysFalsy if addition_is_bool_instance => { - new_positive = Type::BooleanLiteral(false); - } - Type::Instance(instance) - if instance.class.is_known(db, KnownClass::Bool) => - { - match new_positive { - // `bool & AlwaysTruthy` -> `Literal[True]` - Type::AlwaysTruthy => { - new_positive = Type::BooleanLiteral(true); - } - // `bool & AlwaysFalsy` -> `Literal[False]` - Type::AlwaysFalsy => { - new_positive = Type::BooleanLiteral(false); - } - _ => continue, - } - } - _ => continue, - } - self.positive.swap_remove_index(index); - break; - } - - if addition_is_bool_instance { - for (index, existing_negative) in self.negative.iter().enumerate() { - match existing_negative { - // `bool & ~Literal[False]` -> `Literal[True]` - // `bool & ~Literal[True]` -> `Literal[False]` - Type::BooleanLiteral(bool_value) => { - new_positive = Type::BooleanLiteral(!bool_value); - } - // `bool & ~AlwaysTruthy` -> `Literal[False]` - Type::AlwaysTruthy => { - new_positive = Type::BooleanLiteral(false); - } - // `bool & ~AlwaysFalsy` -> `Literal[True]` - Type::AlwaysFalsy => { - new_positive = Type::BooleanLiteral(true); - } - _ => continue, - } - self.negative.swap_remove_index(index); - break; - } - } - - let mut to_remove = SmallVec::<[usize; 1]>::new(); - for (index, existing_positive) in self.positive.iter().enumerate() { - // S & T = S if S <: T - if existing_positive.is_subtype_of(db, new_positive) - || existing_positive.is_same_gradual_form(new_positive) - { - return; - } - // same rule, reverse order - if new_positive.is_subtype_of(db, *existing_positive) { - to_remove.push(index); - } - // A & B = Never if A and B are disjoint - if new_positive.is_disjoint_from(db, *existing_positive) { - *self = Self::default(); - self.positive.insert(Type::Never); - return; - } - } - for index in to_remove.into_iter().rev() { - self.positive.swap_remove_index(index); - } - - let mut to_remove = SmallVec::<[usize; 1]>::new(); - for (index, existing_negative) in self.negative.iter().enumerate() { - // S & ~T = Never if S <: T - if new_positive.is_subtype_of(db, *existing_negative) { - *self = Self::default(); - self.positive.insert(Type::Never); - return; - } - // A & ~B = A if A and B are disjoint - if existing_negative.is_disjoint_from(db, new_positive) { - to_remove.push(index); - } - } - for index in to_remove.into_iter().rev() { - self.negative.swap_remove_index(index); - } - - self.positive.insert(new_positive); - } - } - } - - /// Adds a negative type to this intersection. - fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) { - let contains_bool = || { - self.positive - .iter() - .filter_map(|ty| ty.into_instance()) - .filter_map(|instance| instance.class.known(db)) - .any(KnownClass::is_bool) - }; - - match new_negative { - Type::Intersection(inter) => { - for pos in inter.positive(db) { - self.add_negative(db, *pos); - } - for neg in inter.negative(db) { - self.add_positive(db, *neg); - } - } - Type::Never => { - // Adding ~Never to an intersection is a no-op. - } - Type::Instance(instance) if instance.class.is_object(db) => { - // Adding ~object to an intersection results in Never. - *self = Self::default(); - self.positive.insert(Type::Never); - } - ty @ Type::Dynamic(_) => { - // Adding any of these types to the negative side of an intersection - // is equivalent to adding it to the positive side. We do this to - // simplify the representation. - self.add_positive(db, ty); - } - // `bool & ~AlwaysTruthy` -> `bool & Literal[False]` - // `bool & ~Literal[True]` -> `bool & Literal[False]` - Type::AlwaysTruthy | Type::BooleanLiteral(true) if contains_bool() => { - self.add_positive(db, Type::BooleanLiteral(false)); - } - // `LiteralString & ~AlwaysTruthy` -> `LiteralString & Literal[""]` - Type::AlwaysTruthy if self.positive.contains(&Type::LiteralString) => { - self.add_positive(db, Type::string_literal(db, "")); - } - // `bool & ~AlwaysFalsy` -> `bool & Literal[True]` - // `bool & ~Literal[False]` -> `bool & Literal[True]` - Type::AlwaysFalsy | Type::BooleanLiteral(false) if contains_bool() => { - self.add_positive(db, Type::BooleanLiteral(true)); - } - // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` - Type::AlwaysFalsy if self.positive.contains(&Type::LiteralString) => { - self.add_negative(db, Type::string_literal(db, "")); - } - _ => { - let mut to_remove = SmallVec::<[usize; 1]>::new(); - for (index, existing_negative) in self.negative.iter().enumerate() { - // ~S & ~T = ~T if S <: T - if existing_negative.is_subtype_of(db, new_negative) { - to_remove.push(index); - } - // same rule, reverse order - if new_negative.is_subtype_of(db, *existing_negative) { - return; - } - } - for index in to_remove.into_iter().rev() { - self.negative.swap_remove_index(index); - } - - for existing_positive in &self.positive { - // S & ~T = Never if S <: T - if existing_positive.is_subtype_of(db, new_negative) { - *self = Self::default(); - self.positive.insert(Type::Never); - return; - } - // A & ~B = A if A and B are disjoint - if existing_positive.is_disjoint_from(db, new_negative) { - return; - } - } - - self.negative.insert(new_negative); - } - } - } - - /// Tries to simplify any constrained typevars in the intersection: - /// - /// - If the intersection contains a positive entry for exactly one of the constraints, we can - /// remove the typevar (effectively replacing it with that one positive constraint). - /// - /// - If the intersection contains negative entries for all but one of the constraints, we can - /// remove the negative constraints and replace the typevar with the remaining positive - /// constraint. - /// - /// - If the intersection contains negative entries for all of the constraints, the overall - /// intersection is `Never`. - fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) { - let mut to_add = SmallVec::<[Type<'db>; 1]>::new(); - let mut positive_to_remove = SmallVec::<[usize; 1]>::new(); - - for (typevar_index, ty) in self.positive.iter().enumerate() { - let Type::TypeVar(typevar) = ty else { - continue; - }; - let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - typevar.bound_or_constraints(db) - else { - continue; - }; - - // Determine which constraints appear as positive entries in the intersection. Note - // that we shouldn't have duplicate entries in the positive or negative lists, so we - // don't need to worry about finding any particular constraint more than once. - let constraints = constraints.elements(db); - let mut positive_constraint_count = 0; - for positive in &self.positive { - // This linear search should be fine as long as we don't encounter typevars with - // thousands of constraints. - positive_constraint_count += constraints - .iter() - .filter(|c| c.is_subtype_of(db, *positive)) - .count(); - } - - // If precisely one constraint appears as a positive element, we can replace the - // typevar with that positive constraint. - if positive_constraint_count == 1 { - positive_to_remove.push(typevar_index); - continue; - } - - // Determine which constraints appear as negative entries in the intersection. - let mut to_remove = Vec::with_capacity(constraints.len()); - let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect(); - for (negative_index, negative) in self.negative.iter().enumerate() { - // This linear search should be fine as long as we don't encounter typevars with - // thousands of constraints. - let matching_constraints = constraints - .iter() - .enumerate() - .filter(|(_, c)| c.is_subtype_of(db, *negative)); - for (constraint_index, _) in matching_constraints { - to_remove.push(negative_index); - remaining_constraints[constraint_index] = None; - } - } - - let mut iter = remaining_constraints.into_iter().flatten(); - let Some(remaining_constraint) = iter.next() else { - // All of the typevar constraints have been removed, so the entire intersection is - // `Never`. - *self = Self::default(); - self.positive.insert(Type::Never); - return; - }; - - let more_than_one_remaining_constraint = iter.next().is_some(); - if more_than_one_remaining_constraint { - // This typevar cannot be simplified. - continue; - } - - // Only one typevar constraint remains. Remove all of the negative constraints, and - // replace the typevar itself with the remaining positive constraint. - to_add.push(remaining_constraint); - positive_to_remove.push(typevar_index); - } - - // We don't need to sort the positive list, since we only append to it in increasing order. - for index in positive_to_remove.into_iter().rev() { - self.positive.swap_remove_index(index); - } - - for remaining_constraint in to_add { - self.add_positive(db, remaining_constraint); - } - } - - fn build(mut self, db: &'db dyn Db) -> Type<'db> { - self.simplify_constrained_typevars(db); - match (self.positive.len(), self.negative.len()) { - (0, 0) => Type::object(db), - (1, 0) => self.positive[0], - _ => { - self.positive.shrink_to_fit(); - self.negative.shrink_to_fit(); - Type::Intersection(IntersectionType::new(db, self.positive, self.negative)) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::{IntersectionBuilder, Type, UnionBuilder, UnionType}; - - use crate::db::tests::setup_db; - use crate::types::{KnownClass, Truthiness}; - - use test_case::test_case; - - #[test] - fn build_union_no_elements() { - let db = setup_db(); - - let empty_union = UnionBuilder::new(&db).build(); - assert_eq!(empty_union, Type::Never); - } - - #[test] - fn build_union_single_element() { - let db = setup_db(); - - let t0 = Type::IntLiteral(0); - let union = UnionType::from_elements(&db, [t0]); - assert_eq!(union, t0); - } - - #[test] - fn build_union_two_elements() { - let db = setup_db(); - - let t0 = Type::IntLiteral(0); - let t1 = Type::IntLiteral(1); - let union = UnionType::from_elements(&db, [t0, t1]).expect_union(); - - assert_eq!(union.elements(&db), &[t0, t1]); - } - - #[test] - fn build_intersection_empty_intersection_equals_object() { - let db = setup_db(); - - let intersection = IntersectionBuilder::new(&db).build(); - assert_eq!(intersection, Type::object(&db)); - } - - #[test_case(Type::BooleanLiteral(true))] - #[test_case(Type::BooleanLiteral(false))] - #[test_case(Type::AlwaysTruthy)] - #[test_case(Type::AlwaysFalsy)] - fn build_intersection_simplify_split_bool(t_splitter: Type) { - let db = setup_db(); - let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue; - - // We add t_object in various orders (in first or second position) in - // the tests below to ensure that the boolean simplification eliminates - // everything from the intersection, not just `bool`. - let t_object = Type::object(&db); - let t_bool = KnownClass::Bool.to_instance(&db); - - let ty = IntersectionBuilder::new(&db) - .add_positive(t_object) - .add_positive(t_bool) - .add_negative(t_splitter) - .build(); - assert_eq!(ty, Type::BooleanLiteral(!bool_value)); - - let ty = IntersectionBuilder::new(&db) - .add_positive(t_bool) - .add_positive(t_object) - .add_negative(t_splitter) - .build(); - assert_eq!(ty, Type::BooleanLiteral(!bool_value)); - - let ty = IntersectionBuilder::new(&db) - .add_positive(t_object) - .add_negative(t_splitter) - .add_positive(t_bool) - .build(); - assert_eq!(ty, Type::BooleanLiteral(!bool_value)); - - let ty = IntersectionBuilder::new(&db) - .add_negative(t_splitter) - .add_positive(t_object) - .add_positive(t_bool) - .build(); - assert_eq!(ty, Type::BooleanLiteral(!bool_value)); - } -} diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs deleted file mode 100644 index cce0c81c0ba37..0000000000000 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::collections::VecDeque; -use std::ops::{Deref, DerefMut}; - -use super::Type; - -/// Arguments for a single call, in source order. -#[derive(Clone, Debug, Default)] -pub(crate) struct CallArguments<'a>(VecDeque>); - -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(&mut self, bound_self: Option>, 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 - } - - pub(crate) fn len(&self) -> usize { - self.0.len() - } - - pub(crate) fn iter(&self) -> impl Iterator> + '_ { - self.0.iter().copied() - } -} - -impl<'a> FromIterator> for CallArguments<'a> { - fn from_iter>>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - -#[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, - /// A positional argument. - Positional, - /// A starred positional argument (e.g. `*args`). - Variadic, - /// A keyword argument (e.g. `a=1`). - Keyword(&'a str), - /// The double-starred keywords argument (e.g. `**kwargs`). - Keywords, -} - -/// 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>, -} - -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>) -> 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(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(&mut self, bound_self: Option>, 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, 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 - } -} diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs deleted file mode 100644 index 16f00029e53c1..0000000000000 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ /dev/null @@ -1,1474 +0,0 @@ -//! When analyzing a call site, we create _bindings_, which match and type-check the actual -//! arguments against the parameters of the callable. Like with -//! [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 smallvec::SmallVec; - -use super::{ - 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, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, - UNKNOWN_ARGUMENT, -}; -use crate::types::generics::{Specialization, SpecializationBuilder}; -use crate::types::signatures::{Parameter, ParameterForm}; -use crate::types::{ - todo_type, BoundMethodType, DataclassMetadata, FunctionDecorators, KnownClass, KnownFunction, - KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind, -}; -use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic}; -use ruff_python_ast as ast; -use ruff_text_size::Ranged; - -/// Binding information for a possible union of callables. At a call site, the arguments must be -/// 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)] -pub(crate) struct Bindings<'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]>, - - conflicting_forms: Box<[bool]>, -} - -impl<'db> Bindings<'db> { - /// Match the arguments of a call site against the parameters of a collection of possibly - /// unioned, possibly overloaded signatures. - /// - /// 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 check_types( - mut self, - db: &'db dyn Db, - argument_types: &mut CallArgumentTypes<'_, 'db>, - ) -> Result> { - 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: - // - // - If every union element is Ok, then the union is too. - // - If any element has a BindingError, the union has a BindingError. - // - If every element is NotCallable, then the union is also NotCallable. - // - Otherwise, the elements are some mixture of Ok, NotCallable, and PossiblyNotCallable. - // The union as a whole is PossiblyNotCallable. - // - // For example, the union type `Callable[[int], int] | None` may not be callable at all, - // because the `None` element in this union has no `__call__` method. - // - // On the other hand, the union type `Callable[[int], int] | Callable[[str], str]` is - // always *callable*, but it would produce a `BindingError` if an inhabitant of this type - // was called with a single `int` argument passed in. That's because the second element in - // the union doesn't accept an `int` when it's called: it only accepts a `str`. - let mut all_ok = true; - let mut any_binding_error = false; - let mut all_not_callable = true; - 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)); - } - - if all_ok { - Ok(self) - } else if any_binding_error { - Err(CallError(CallErrorKind::BindingError, Box::new(self))) - } else if all_not_callable { - Err(CallError(CallErrorKind::NotCallable, Box::new(self))) - } else { - Err(CallError( - CallErrorKind::PossiblyNotCallable, - Box::new(self), - )) - } - } - - pub(crate) fn is_single(&self) -> bool { - self.elements.len() == 1 - } - - pub(crate) fn single_element(&self) -> Option<&CallableBinding<'db>> { - match self.elements.as_slice() { - [element] => Some(element), - _ => None, - } - } - - 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`. - pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { - if let [binding] = self.elements.as_slice() { - return binding.return_type(); - } - UnionType::from_elements(db, self.into_iter().map(CallableBinding::return_type)) - } - - /// Report diagnostics for all of the errors that occurred when trying to match actual - /// arguments to formal parameters. If the callable is a union, or has multiple overloads, we - /// report a single diagnostic if we couldn't match any union element or overload. - /// TODO: Update this to add subdiagnostics about how we failed to match each union element and - /// overload. - pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { - // If all union elements are not callable, report that the union as a whole is not - // callable. - if self.into_iter().all(|b| !b.is_callable()) { - context.report_lint_old( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable", - self.callable_type().display(context.db()) - ), - ); - return; - } - - for (index, conflicting_form) in self.conflicting_forms.iter().enumerate() { - if *conflicting_form { - context.report_lint_old( - &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. - if let Some(first) = self.into_iter().find(|b| b.as_result().is_err()) { - 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::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { - if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { - match overload.parameter_types() { - [_, Some(owner)] => { - overload.set_return_type(Type::BoundMethod(BoundMethodType::new( - db, function, *owner, - ))); - } - [Some(instance), None] => { - overload.set_return_type(Type::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::BoundMethod(BoundMethodType::new( - db, function, *first, - ))); - } - } - } - - Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => { - if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] = - overload.parameter_types() - { - if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { - match overload.parameter_types() { - [_, _, Some(owner)] => { - overload.set_return_type(Type::BoundMethod( - BoundMethodType::new(db, *function, *owner), - )); - } - - [_, Some(instance), None] => { - overload.set_return_type(Type::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(instance), _] => { - overload.set_return_type(Type::BoundMethod( - BoundMethodType::new(db, *function, *instance), - )); - } - - _ => {} - } - } - } - } - - Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => { - match overload.parameter_types() { - [Some(property @ Type::PropertyInstance(_)), Some(instance), ..] - if instance.is_none(db) => - { - overload.set_return_type(*property); - } - [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..] - if property.getter(db).is_some_and(|getter| { - getter - .into_function_literal() - .is_some_and(|f| f.name(db) == "__name__") - }) => - { - overload.set_return_type(Type::string_literal(db, type_alias.name(db))); - } - [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(type_var))), ..] - if property.getter(db).is_some_and(|getter| { - getter - .into_function_literal() - .is_some_and(|f| f.name(db) == "__name__") - }) => - { - overload.set_return_type(Type::string_literal(db, type_var.name(db))); - } - [Some(Type::PropertyInstance(property)), Some(instance), ..] => { - if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, CallArgumentTypes::positional([*instance])) - .map(|binding| binding.return_type(db)) - { - overload.set_return_type(return_ty); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", - )); - overload.set_return_type(Type::unknown()); - } - } else { - overload.errors.push(BindingError::InternalCallError( - "property has no getter", - )); - overload.set_return_type(Type::Never); - } - } - _ => {} - } - } - - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { - match overload.parameter_types() { - [Some(instance), ..] if instance.is_none(db) => { - overload.set_return_type(Type::PropertyInstance(property)); - } - [Some(instance), ..] => { - if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, CallArgumentTypes::positional([*instance])) - .map(|binding| binding.return_type(db)) - { - overload.set_return_type(return_ty); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", - )); - overload.set_return_type(Type::unknown()); - } - } else { - overload.set_return_type(Type::Never); - overload.errors.push(BindingError::InternalCallError( - "property has no getter", - )); - } - } - _ => {} - } - } - - Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { - if let [Some(Type::PropertyInstance(property)), Some(instance), Some(value), ..] = - overload.parameter_types() - { - if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter - .try_call(db, CallArgumentTypes::positional([*instance, *value])) - { - overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", - )); - } - } else { - overload - .errors - .push(BindingError::InternalCallError("property has no setter")); - } - } - } - - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { - if let [Some(instance), Some(value), ..] = overload.parameter_types() { - if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter - .try_call(db, CallArgumentTypes::positional([*instance, *value])) - { - overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", - )); - } - } else { - overload - .errors - .push(BindingError::InternalCallError("property has no setter")); - } - } - } - - Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)) => { - if let [Some(Type::StringLiteral(prefix)), None, None] = - overload.parameter_types() - { - overload.set_return_type(Type::BooleanLiteral( - literal.value(db).starts_with(&**prefix.value(db)), - )); - } - } - - Type::BoundMethod(bound_method) - if bound_method.self_instance(db).is_property_instance() => - { - match bound_method.function(db).name(db).as_str() { - "setter" => { - if let [Some(_), Some(setter)] = overload.parameter_types() { - let mut ty_property = bound_method.self_instance(db); - if let Type::PropertyInstance(property) = ty_property { - ty_property = - Type::PropertyInstance(PropertyInstanceType::new( - db, - property.getter(db), - Some(*setter), - )); - } - overload.set_return_type(ty_property); - } - } - "getter" => { - if let [Some(_), Some(getter)] = overload.parameter_types() { - let mut ty_property = bound_method.self_instance(db); - if let Type::PropertyInstance(property) = ty_property { - ty_property = - Type::PropertyInstance(PropertyInstanceType::new( - db, - Some(*getter), - property.setter(db), - )); - } - overload.set_return_type(ty_property); - } - } - "deleter" => { - // TODO: we do not store deleters yet - let ty_property = bound_method.self_instance(db); - overload.set_return_type(ty_property); - } - _ => { - // Fall back to typeshed stubs for all other methods - } - } - } - - 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, - }, - ); - } - - Some(KnownFunction::Dataclass) => { - if let [init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot] = - overload.parameter_types() - { - let to_bool = |ty: &Option>, default: bool| -> bool { - if let Some(Type::BooleanLiteral(value)) = ty { - *value - } else { - // TODO: emit a diagnostic if we receive `bool` - default - } - }; - - let mut metadata = DataclassMetadata::empty(); - - if to_bool(init, true) { - metadata |= DataclassMetadata::INIT; - } - if to_bool(repr, true) { - metadata |= DataclassMetadata::REPR; - } - if to_bool(eq, true) { - metadata |= DataclassMetadata::EQ; - } - if to_bool(order, false) { - metadata |= DataclassMetadata::ORDER; - } - if to_bool(unsafe_hash, false) { - metadata |= DataclassMetadata::UNSAFE_HASH; - } - if to_bool(frozen, false) { - metadata |= DataclassMetadata::FROZEN; - } - if to_bool(match_args, true) { - metadata |= DataclassMetadata::MATCH_ARGS; - } - if to_bool(kw_only, false) { - metadata |= DataclassMetadata::KW_ONLY; - } - if to_bool(slots, false) { - metadata |= DataclassMetadata::SLOTS; - } - if to_bool(weakref_slot, false) { - metadata |= DataclassMetadata::WEAKREF_SLOT; - } - - overload.set_return_type(Type::DataclassDecorator(metadata)); - } - } - - _ => {} - }, - - Type::ClassLiteral(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)); - } - } - - Some(KnownClass::Property) => { - if let [getter, setter, ..] = overload.parameter_types() { - overload.set_return_type(Type::PropertyInstance( - PropertyInstanceType::new(db, *getter, *setter), - )); - } - } - - _ => {} - }, - - // Not a special case - _ => {} - } - } - } -} - -impl<'a, 'db> IntoIterator for &'a Bindings<'db> { - type Item = &'a CallableBinding<'db>; - type IntoIter = std::slice::Iter<'a, CallableBinding<'db>>; - - fn into_iter(self) -> Self::IntoIter { - self.elements.iter() - } -} - -impl<'a, 'db> IntoIterator for &'a mut Bindings<'db> { - type Item = &'a mut CallableBinding<'db>; - type IntoIter = std::slice::IterMut<'a, CallableBinding<'db>>; - - fn into_iter(self) -> Self::IntoIter { - self.elements.iter_mut() - } -} - -/// Binding information for a single callable. If the callable is overloaded, there is a separate -/// [`Binding`] for each overload. -/// -/// For a successful binding, each argument is mapped to one of the callable's formal parameters. -/// If the callable has multiple overloads, the first one that matches is used as the overall -/// binding match. -/// -/// TODO: Implement the call site evaluation algorithm in the [proposed updated typing -/// spec][overloads], which is much more subtle than “first match wins”. -/// -/// If the arguments cannot be matched to formal parameters, we store information about the -/// specific errors that occurred when trying to match them up. If the callable has multiple -/// overloads, we store this error information for each overload. -/// -/// [overloads]: https://github.com/python/typing/pull/1839 -#[derive(Debug)] -pub(crate) struct CallableBinding<'db> { - pub(crate) callable_type: Type<'db>, - pub(crate) signature_type: Type<'db>, - pub(crate) dunder_call_is_possibly_unbound: bool, - - /// The bindings of each overload of this callable. Will be empty if the type is not callable. - /// - /// By using `SmallVec`, we avoid an extra heap allocation for the common case of a - /// non-overloaded callable. - overloads: SmallVec<[Binding<'db>; 1]>, -} - -impl<'db> CallableBinding<'db> { - fn match_parameters( - signature: &CallableSignature<'db>, - arguments: &mut CallArguments<'_>, - argument_forms: &mut [Option], - conflicting_forms: &mut [bool], - ) -> Self { - // If this callable is a bound method, prepend the self instance onto the arguments list - // before checking. - 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(); - - 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> { - if !self.is_callable() { - return Err(CallErrorKind::NotCallable); - } - - if self.has_binding_errors() { - return Err(CallErrorKind::BindingError); - } - - if self.dunder_call_is_possibly_unbound { - return Err(CallErrorKind::PossiblyNotCallable); - } - - Ok(()) - } - - fn is_callable(&self) -> bool { - !self.overloads.is_empty() - } - - /// Returns whether there were any errors binding this call site. If the callable has multiple - /// overloads, they must _all_ have errors. - pub(crate) fn has_binding_errors(&self) -> bool { - self.matching_overload().is_none() - } - - /// Returns the overload that matched for this call binding. Returns `None` if none of the - /// overloads matched. - pub(crate) fn matching_overload(&self) -> Option<(usize, &Binding<'db>)> { - self.overloads - .iter() - .enumerate() - .find(|(_, overload)| overload.as_result().is_ok()) - } - - /// Returns the overload that matched for this call binding. Returns `None` if none of the - /// overloads matched. - pub(crate) fn matching_overload_mut(&mut self) -> Option<(usize, &mut Binding<'db>)> { - self.overloads - .iter_mut() - .enumerate() - .find(|(_, overload)| overload.as_result().is_ok()) - } - - /// Returns the return type of this call. For a valid call, this is the return type of the - /// overload that the arguments matched against. For an invalid call to a non-overloaded - /// function, this is the return type of the function. For an invalid call to an overloaded - /// function, we return `Type::unknown`, since we cannot make any useful conclusions about - /// which overload was intended to be called. - pub(crate) fn return_type(&self) -> Type<'db> { - if let Some((_, overload)) = self.matching_overload() { - return overload.return_type(); - } - if let [overload] = self.overloads.as_slice() { - return overload.return_type(); - } - Type::unknown() - } - - fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { - if !self.is_callable() { - context.report_lint_old( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable", - self.callable_type.display(context.db()), - ), - ); - return; - } - - if self.dunder_call_is_possibly_unbound { - context.report_lint_old( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (possibly unbound `__call__` method)", - self.callable_type.display(context.db()), - ), - ); - return; - } - - let callable_description = CallableDescription::new(context.db(), self.callable_type); - if self.overloads.len() > 1 { - context.report_lint_old( - &NO_MATCHING_OVERLOAD, - node, - format_args!( - "No overload{} matches arguments", - if let Some(CallableDescription { kind, name }) = callable_description { - format!(" of {kind} `{name}`") - } else { - String::new() - } - ), - ); - return; - } - - let callable_description = CallableDescription::new(context.db(), self.signature_type); - for overload in &self.overloads { - overload.report_diagnostics( - context, - node, - self.signature_type, - callable_description.as_ref(), - ); - } - } -} - -/// Binding information for one of the overloads of a callable. -#[derive(Debug)] -pub(crate) struct Binding<'db> { - /// Return type of the call. - return_ty: Type<'db>, - - /// The specialization that was inferred from the argument types, if the callable is generic. - specialization: Option>, - - /// 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]>, - - /// Bound types for parameters, in parameter source order, or `None` if no argument was matched - /// to that parameter. - parameter_tys: Box<[Option>]>, - - /// Call binding errors, if any. - errors: Vec>, -} - -impl<'db> Binding<'db> { - fn match_parameters( - signature: &Signature<'db>, - arguments: &CallArguments<'_>, - argument_forms: &mut [Option], - conflicting_forms: &mut [bool], - ) -> Self { - let parameters = signature.parameters(); - // 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; - 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) in arguments.iter().enumerate() { - let (index, parameter, positional) = match argument { - Argument::Positional | Argument::Synthetic => { - if matches!(argument, Argument::Synthetic) { - num_synthetic_args += 1; - } - let Some((index, parameter)) = parameters - .get_positional(next_positional) - .map(|param| (next_positional, param)) - .or_else(|| parameters.variadic()) - else { - first_excess_positional.get_or_insert(argument_index); - next_positional += 1; - continue; - }; - next_positional += 1; - (index, parameter, !parameter.is_variadic()) - } - Argument::Keyword(name) => { - let Some((index, parameter)) = parameters - .keyword_by_name(name) - .or_else(|| parameters.keyword_variadic()) - else { - errors.push(BindingError::UnknownArgument { - argument_name: ast::name::Name::new(name), - argument_index: get_argument_index(argument_index, num_synthetic_args), - }); - continue; - }; - (index, parameter, false) - } - - Argument::Variadic | Argument::Keywords => { - // TODO - continue; - } - }; - 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 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 { - first_excess_argument_index: get_argument_index( - first_excess_argument_index, - num_synthetic_args, - ), - expected_positional_count: parameters - .positional() - .count() - // using saturating_sub to avoid negative values due to invalid syntax in source code - .saturating_sub(num_synthetic_args), - provided_positional_count: next_positional - // using saturating_sub to avoid negative values due to invalid syntax in source code - .saturating_sub(num_synthetic_args), - }); - } - let mut missing = vec![]; - for (index, matched) in parameter_matched.iter().copied().enumerate() { - if !matched { - let param = ¶meters[index]; - if param.is_variadic() - || param.is_keyword_variadic() - || param.default_type().is_some() - { - // variadic/keywords and defaulted arguments are not required - continue; - } - missing.push(ParameterContext::new(param, index, false)); - } - } - - if !missing.is_empty() { - errors.push(BindingError::MissingArguments { - parameters: ParameterContexts(missing), - }); - } - - Self { - return_ty: signature.return_ty.unwrap_or(Type::unknown()), - specialization: None, - 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>, - ) { - // If this overload is generic, first see if we can infer a specialization of the function - // from the arguments that were passed in. - let parameters = signature.parameters(); - self.specialization = signature.generic_context.map(|generic_context| { - let mut builder = SpecializationBuilder::new(db, generic_context); - for (argument_index, (_, argument_type)) in argument_types.iter().enumerate() { - 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]; - let Some(expected_type) = parameter.annotated_type() else { - continue; - }; - builder.infer(expected_type, argument_type); - } - builder.build() - }); - - 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(mut expected_ty) = parameter.annotated_type() { - if let Some(specialization) = self.specialization { - expected_ty = expected_ty.apply_specialization(db, specialization); - } - 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); - } - } - - if let Some(specialization) = self.specialization { - self.return_ty = self.return_ty.apply_specialization(db, specialization); - } - } - - pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { - self.return_ty = return_ty; - } - - pub(crate) fn return_type(&self) -> Type<'db> { - self.return_ty - } - - pub(crate) fn specialization(&self) -> Option> { - self.specialization - } - - pub(crate) fn parameter_types(&self) -> &[Option>] { - &self.parameter_tys - } - - fn report_diagnostics( - &self, - context: &InferContext<'db>, - node: ast::AnyNodeRef, - callable_ty: Type<'db>, - callable_description: Option<&CallableDescription>, - ) { - for error in &self.errors { - error.report_diagnostic(context, node, callable_ty, callable_description); - } - } - - fn as_result(&self) -> Result<(), CallErrorKind> { - if !self.errors.is_empty() { - return Err(CallErrorKind::BindingError); - } - Ok(()) - } -} - -/// Describes a callable for the purposes of diagnostics. -#[derive(Debug)] -pub(crate) struct CallableDescription<'a> { - name: &'a str, - kind: &'a str, -} - -impl<'db> CallableDescription<'db> { - fn new(db: &'db dyn Db, callable_type: Type<'db>) -> Option> { - match callable_type { - Type::FunctionLiteral(function) => Some(CallableDescription { - kind: "function", - name: function.name(db), - }), - Type::ClassLiteral(class_type) => Some(CallableDescription { - kind: "class", - name: class_type.name(db), - }), - Type::BoundMethod(bound_method) => Some(CallableDescription { - kind: "bound method", - name: bound_method.function(db).name(db), - }), - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { - Some(CallableDescription { - kind: "method wrapper `__get__` of function", - name: function.name(db), - }) - } - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { - Some(CallableDescription { - kind: "method wrapper", - name: "`__get__` of property", - }) - } - Type::WrapperDescriptor(kind) => Some(CallableDescription { - kind: "wrapper descriptor", - name: match kind { - WrapperDescriptorKind::FunctionTypeDunderGet => "FunctionType.__get__", - WrapperDescriptorKind::PropertyDunderGet => "property.__get__", - WrapperDescriptorKind::PropertyDunderSet => "property.__set__", - }, - }), - _ => None, - } - } -} - -/// Information needed to emit a diagnostic regarding a parameter. -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct ParameterContext { - name: Option, - index: usize, - - /// Was the argument for this parameter passed positionally, and matched to a non-variadic - /// positional parameter? (If so, we will provide the index in the diagnostic, not just the - /// name.) - positional: bool, -} - -impl ParameterContext { - fn new(parameter: &Parameter, index: usize, positional: bool) -> Self { - Self { - name: parameter.display_name(), - index, - positional, - } - } -} - -impl std::fmt::Display for ParameterContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(name) = &self.name { - if self.positional { - write!(f, "{} (`{name}`)", self.index + 1) - } else { - write!(f, "`{name}`") - } - } else { - write!(f, "{}", self.index + 1) - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct ParameterContexts(Vec); - -impl std::fmt::Display for ParameterContexts { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut iter = self.0.iter(); - if let Some(first) = iter.next() { - write!(f, "{first}")?; - for param in iter { - f.write_str(", ")?; - write!(f, "{param}")?; - } - } - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) enum BindingError<'db> { - /// The type of an argument is not assignable to the annotated type of its corresponding - /// parameter. - InvalidArgumentType { - parameter: ParameterContext, - argument_index: Option, - expected_ty: Type<'db>, - provided_ty: Type<'db>, - }, - /// One or more required parameters (that is, with no default) is not supplied by any argument. - MissingArguments { parameters: ParameterContexts }, - /// A call argument can't be matched to any parameter. - UnknownArgument { - argument_name: ast::name::Name, - argument_index: Option, - }, - /// More positional arguments are provided in the call than can be handled by the signature. - TooManyPositionalArguments { - first_excess_argument_index: Option, - expected_positional_count: usize, - provided_positional_count: usize, - }, - /// Multiple arguments were provided for a single parameter. - ParameterAlreadyAssigned { - argument_index: Option, - parameter: ParameterContext, - }, - /// The call itself might be well constructed, but an error occurred while evaluating the call. - /// We use this variant to report errors in `property.__get__` and `property.__set__`, which - /// can occur when the call to the underlying getter/setter fails. - InternalCallError(&'static str), -} - -impl<'db> BindingError<'db> { - /// Returns a tuple of two spans. The first is - /// the span for the identifier of the function - /// definition for `callable_ty`. The second is - /// the span for the parameter in the function - /// definition for `callable_ty`. - /// - /// If there are no meaningful spans, then this - /// returns `None`. - fn parameter_span_from_index( - db: &'db dyn Db, - callable_ty: Type<'db>, - parameter_index: usize, - ) -> Option<(Span, Span)> { - match callable_ty { - Type::FunctionLiteral(function) => { - let function_scope = function.body_scope(db); - let span = Span::from(function_scope.file(db)); - let node = function_scope.node(db); - if let Some(func_def) = node.as_function() { - let range = func_def - .parameters - .iter() - .nth(parameter_index) - .map(|param| param.range()) - .unwrap_or(func_def.parameters.range); - let name_span = span.clone().with_range(func_def.name.range); - let parameter_span = span.with_range(range); - Some((name_span, parameter_span)) - } else { - None - } - } - Type::BoundMethod(bound_method) => Self::parameter_span_from_index( - db, - Type::FunctionLiteral(bound_method.function(db)), - parameter_index, - ), - _ => None, - } - } - - pub(super) fn report_diagnostic( - &self, - context: &InferContext<'db>, - node: ast::AnyNodeRef, - callable_ty: Type<'db>, - callable_description: Option<&CallableDescription>, - ) { - match self { - Self::InvalidArgumentType { - parameter, - argument_index, - expected_ty, - provided_ty, - } => { - let range = Self::get_node(node, *argument_index); - let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { - return; - }; - - let provided_ty_display = provided_ty.display(context.db()); - let expected_ty_display = expected_ty.display(context.db()); - - let mut diag = builder.into_diagnostic("Argument to this function is incorrect"); - diag.set_primary_message(format_args!( - "Expected `{expected_ty_display}`, found `{provided_ty_display}`" - )); - if let Some((name_span, parameter_span)) = - Self::parameter_span_from_index(context.db(), callable_ty, parameter.index) - { - let mut sub = SubDiagnostic::new(Severity::Info, "Function defined here"); - sub.annotate(Annotation::primary(name_span)); - sub.annotate( - Annotation::secondary(parameter_span).message("Parameter declared here"), - ); - diag.sub(sub); - } - } - - Self::TooManyPositionalArguments { - first_excess_argument_index, - expected_positional_count, - provided_positional_count, - } => { - context.report_lint_old( - &TOO_MANY_POSITIONAL_ARGUMENTS, - Self::get_node(node, *first_excess_argument_index), - format_args!( - "Too many positional arguments{}: expected \ - {expected_positional_count}, got {provided_positional_count}", - if let Some(CallableDescription { kind, name }) = callable_description { - format!(" to {kind} `{name}`") - } else { - String::new() - } - ), - ); - } - - Self::MissingArguments { parameters } => { - let s = if parameters.0.len() == 1 { "" } else { "s" }; - context.report_lint_old( - &MISSING_ARGUMENT, - node, - format_args!( - "No argument{s} provided for required parameter{s} {parameters}{}", - if let Some(CallableDescription { kind, name }) = callable_description { - format!(" of {kind} `{name}`") - } else { - String::new() - } - ), - ); - } - - Self::UnknownArgument { - argument_name, - argument_index, - } => { - context.report_lint_old( - &UNKNOWN_ARGUMENT, - Self::get_node(node, *argument_index), - format_args!( - "Argument `{argument_name}` does not match any known parameter{}", - if let Some(CallableDescription { kind, name }) = callable_description { - format!(" of {kind} `{name}`") - } else { - String::new() - } - ), - ); - } - - Self::ParameterAlreadyAssigned { - argument_index, - parameter, - } => { - context.report_lint_old( - &PARAMETER_ALREADY_ASSIGNED, - Self::get_node(node, *argument_index), - format_args!( - "Multiple values provided for parameter {parameter}{}", - if let Some(CallableDescription { kind, name }) = callable_description { - format!(" of {kind} `{name}`") - } else { - String::new() - } - ), - ); - } - - Self::InternalCallError(reason) => { - context.report_lint_old( - &CALL_NON_CALLABLE, - Self::get_node(node, None), - format_args!( - "Call{} failed: {reason}", - if let Some(CallableDescription { kind, name }) = callable_description { - format!(" of {kind} `{name}`") - } else { - String::new() - } - ), - ); - } - } - } - - fn get_node(node: ast::AnyNodeRef, argument_index: Option) -> ast::AnyNodeRef { - // If we have a Call node and an argument index, report the diagnostic on the correct - // argument node; otherwise, report it on the entire provided node. - match (node, argument_index) { - (ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => { - match call_node - .arguments - .arguments_source_order() - .nth(argument_index) - .expect("argument index should not be out of range") - { - ast::ArgOrKeyword::Arg(expr) => expr.into(), - ast::ArgOrKeyword::Keyword(keyword) => keyword.into(), - } - } - _ => node, - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs deleted file mode 100644 index b690f6ea8c306..0000000000000 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ /dev/null @@ -1,2683 +0,0 @@ -use std::hash::BuildHasherDefault; -use std::sync::{LazyLock, Mutex}; - -use super::{ - class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder, - KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness, - Type, TypeAliasType, TypeQualifiers, TypeVarInstance, -}; -use crate::semantic_index::definition::Definition; -use crate::semantic_index::DeclarationWithConstraint; -use crate::types::generics::{GenericContext, Specialization}; -use crate::types::signatures::{Parameter, Parameters}; -use crate::types::{CallableType, DataclassMetadata, Signature}; -use crate::{ - module_resolver::file_to_module, - semantic_index::{ - ast_ids::HasScopedExpressionId, - attribute_assignments, - definition::{DefinitionKind, TargetKind}, - semantic_index, - symbol::ScopeId, - symbol_table, use_def_map, - }, - symbol::{ - class_symbol, known_module_symbol, symbol_from_bindings, symbol_from_declarations, - Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers, - }, - types::{ - definition_expression_type, CallArgumentTypes, CallError, CallErrorKind, DynamicType, - MetaclassCandidate, TupleType, UnionBuilder, UnionType, - }, - Db, KnownModule, Program, -}; -use indexmap::IndexSet; -use itertools::Itertools as _; -use ruff_db::files::File; -use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, PythonVersion}; -use rustc_hash::{FxHashSet, FxHasher}; - -type FxOrderMap = ordermap::map::OrderMap>; - -fn explicit_bases_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &[Type<'db>], - _count: u32, - _self: ClassLiteralType<'db>, -) -> salsa::CycleRecoveryAction]>> { - salsa::CycleRecoveryAction::Iterate -} - -fn explicit_bases_cycle_initial<'db>( - _db: &'db dyn Db, - _self: ClassLiteralType<'db>, -) -> Box<[Type<'db>]> { - Box::default() -} - -fn try_mro_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Result, MroError<'db>>, - _count: u32, - _self: ClassLiteralType<'db>, - _specialization: Option>, -) -> salsa::CycleRecoveryAction, MroError<'db>>> { - salsa::CycleRecoveryAction::Iterate -} - -#[allow(clippy::unnecessary_wraps)] -fn try_mro_cycle_initial<'db>( - db: &'db dyn Db, - self_: ClassLiteralType<'db>, - specialization: Option>, -) -> Result, MroError<'db>> { - Ok(Mro::from_error( - db, - self_.apply_optional_specialization(db, specialization), - )) -} - -#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] -fn inheritance_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option, - _count: u32, - _self: ClassLiteralType<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn inheritance_cycle_initial<'db>( - _db: &'db dyn Db, - _self: ClassLiteralType<'db>, -) -> Option { - None -} - -/// Representation of a class definition statement in the AST. This does not in itself represent a -/// type, but is used as the inner data for several structs that *do* represent types. -#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)] -pub struct Class<'db> { - /// Name of the class at definition - pub(crate) name: ast::name::Name, - - pub(crate) body_scope: ScopeId<'db>, - - pub(crate) known: Option, - - pub(crate) dataclass_metadata: Option, -} - -impl<'db> Class<'db> { - fn file(&self, db: &dyn Db) -> File { - self.body_scope.file(db) - } - - /// Return the original [`ast::StmtClassDef`] node associated with this class - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn node(&self, db: &'db dyn Db) -> &'db ast::StmtClassDef { - self.body_scope.node(db).expect_class() - } - - fn definition(&self, db: &'db dyn Db) -> Definition<'db> { - let index = semantic_index(db, self.body_scope.file(db)); - index.expect_single_definition(self.body_scope.node(db).expect_class()) - } -} - -/// A [`Class`] that is not generic. -#[salsa::interned(debug)] -pub struct NonGenericClass<'db> { - #[return_ref] - pub(crate) class: Class<'db>, -} - -impl<'db> From> for Type<'db> { - fn from(class: NonGenericClass<'db>) -> Type<'db> { - Type::ClassLiteral(ClassLiteralType::NonGeneric(class)) - } -} - -/// A [`Class`] that is generic. -#[salsa::interned(debug)] -pub struct GenericClass<'db> { - #[return_ref] - pub(crate) class: Class<'db>, - pub(crate) generic_context: GenericContext<'db>, -} - -impl<'db> From> for Type<'db> { - fn from(class: GenericClass<'db>) -> Type<'db> { - Type::ClassLiteral(ClassLiteralType::Generic(class)) - } -} - -/// A specialization of a generic class with a particular assignment of types to typevars. -#[salsa::interned(debug)] -pub struct GenericAlias<'db> { - pub(crate) origin: GenericClass<'db>, - pub(crate) specialization: Specialization<'db>, -} - -impl<'db> GenericAlias<'db> { - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - self.origin(db).class(db).definition(db) - } -} - -impl<'db> From> for Type<'db> { - fn from(alias: GenericAlias<'db>) -> Type<'db> { - Type::GenericAlias(alias) - } -} - -/// Represents a class type, which might be a non-generic class, or a specialization of a generic -/// class. -#[derive( - Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update, -)] -pub enum ClassType<'db> { - NonGeneric(NonGenericClass<'db>), - Generic(GenericAlias<'db>), -} - -#[salsa::tracked] -impl<'db> ClassType<'db> { - fn class(self, db: &'db dyn Db) -> &'db Class<'db> { - match self { - Self::NonGeneric(non_generic) => non_generic.class(db), - Self::Generic(generic) => generic.origin(db).class(db), - } - } - - /// Returns the class literal and specialization for this class. For a non-generic class, this - /// is the class itself. For a generic alias, this is the alias's origin. - pub(crate) fn class_literal( - self, - db: &'db dyn Db, - ) -> (ClassLiteralType<'db>, Option>) { - match self { - Self::NonGeneric(non_generic) => (ClassLiteralType::NonGeneric(non_generic), None), - Self::Generic(generic) => ( - ClassLiteralType::Generic(generic.origin(db)), - Some(generic.specialization(db)), - ), - } - } - - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - &self.class(db).name - } - - pub(crate) fn known(self, db: &'db dyn Db) -> Option { - self.class(db).known - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - self.class(db).definition(db) - } - - fn specialize_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - match self { - Self::NonGeneric(_) => ty, - Self::Generic(generic) => ty.apply_specialization(db, generic.specialization(db)), - } - } - - /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.class(db).known == Some(known_class) - } - - /// Return `true` if this class represents the builtin class `object` - pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { - self.is_known(db, KnownClass::Object) - } - - /// Iterate over the [method resolution order] ("MRO") of the class. - /// - /// If the MRO could not be accurately resolved, this method falls back to iterating - /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`ClassLiteralType::try_mro`] if you need to distinguish between the success and failure - /// cases rather than simply iterating over the inferred resolution order for the class. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { - let (class_literal, specialization) = self.class_literal(db); - class_literal.iter_mro(db, specialization) - } - - /// Is this class final? - pub(super) fn is_final(self, db: &'db dyn Db) -> bool { - let (class_literal, _) = self.class_literal(db); - class_literal.is_final(db) - } - - /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - // `is_subclass_of` is checking the subtype relation, in which gradual types do not - // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db).contains(&ClassBase::Class(other)) - } - - /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. - pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - let (class_literal, _) = self.class_literal(db); - self.specialize_type(db, class_literal.metaclass(db)) - } - - /// Return a type representing "the set of all instances of the metaclass of this class". - pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { - self - .metaclass(db) - .to_instance(db) - .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") - } - - /// Returns the class member of this class named `name`. - /// - /// The member resolves to a member on the class itself or any of its proper superclasses. - /// - /// TODO: Should this be made private...? - pub(super) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - let (class_literal, specialization) = self.class_literal(db); - class_literal - .class_member_inner(db, specialization, name, policy) - .map_type(|ty| self.specialize_type(db, ty)) - } - - /// Returns the inferred type of the class member named `name`. Only bound members - /// or those marked as ClassVars are considered. - /// - /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope - /// directly. Use [`ClassType::class_member`] if you require a method that will - /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - let (class_literal, specialization) = self.class_literal(db); - class_literal - .own_class_member(db, specialization, name) - .map_type(|ty| self.specialize_type(db, ty)) - } - - /// Returns the `name` attribute of an instance of this class. - /// - /// The attribute could be defined in the class body, but it could also be an implicitly - /// defined attribute that is only present in a method (typically `__init__`). - /// - /// The attribute might also be defined in a superclass of this class. - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - let (class_literal, specialization) = self.class_literal(db); - class_literal - .instance_member(db, specialization, name) - .map_type(|ty| self.specialize_type(db, ty)) - } - - /// A helper function for `instance_member` that looks up the `name` attribute only on - /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - let (class_literal, _) = self.class_literal(db); - class_literal - .own_instance_member(db, name) - .map_type(|ty| self.specialize_type(db, ty)) - } -} - -impl<'db> From> for ClassType<'db> { - fn from(generic: GenericAlias<'db>) -> ClassType<'db> { - ClassType::Generic(generic) - } -} - -impl<'db> From> for Type<'db> { - fn from(class: ClassType<'db>) -> Type<'db> { - match class { - ClassType::NonGeneric(non_generic) => non_generic.into(), - ClassType::Generic(generic) => generic.into(), - } - } -} - -/// Represents a single class object at runtime, which might be a non-generic class, or a generic -/// class that has not been specialized. -#[derive( - Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update, -)] -pub enum ClassLiteralType<'db> { - NonGeneric(NonGenericClass<'db>), - Generic(GenericClass<'db>), -} - -#[salsa::tracked] -impl<'db> ClassLiteralType<'db> { - fn class(self, db: &'db dyn Db) -> &'db Class<'db> { - match self { - Self::NonGeneric(non_generic) => non_generic.class(db), - Self::Generic(generic) => generic.class(db), - } - } - - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - &self.class(db).name - } - - pub(crate) fn known(self, db: &'db dyn Db) -> Option { - self.class(db).known - } - - pub(crate) fn dataclass_metadata(self, db: &'db dyn Db) -> Option { - self.class(db).dataclass_metadata - } - - /// Return `true` if this class represents `known_class` - pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.class(db).known == Some(known_class) - } - - pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - match self { - Self::NonGeneric(_) => None, - Self::Generic(generic) => Some(generic.generic_context(db)), - } - } - - /// Return `true` if this class represents the builtin class `object` - pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { - self.is_known(db, KnownClass::Object) - } - - pub(crate) fn body_scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.class(db).body_scope - } - - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - self.class(db).definition(db) - } - - pub(crate) fn apply_optional_specialization( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> ClassType<'db> { - match (self, specialization) { - (Self::NonGeneric(non_generic), _) => ClassType::NonGeneric(non_generic), - (Self::Generic(generic), None) => { - let specialization = generic.generic_context(db).default_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) - } - (Self::Generic(generic), Some(specialization)) => { - ClassType::Generic(GenericAlias::new(db, generic, specialization)) - } - } - } - - /// Returns the default specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// applies the default specialization to the class's typevars. - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), - Self::Generic(generic) => { - let specialization = generic.generic_context(db).default_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) - } - } - } - - /// Returns the unknown specialization of this class. For non-generic classes, the class is - /// returned unchanged. For a non-specialized generic class, we return a generic alias that - /// maps each of the class's typevars to `Unknown`. - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), - Self::Generic(generic) => { - let specialization = generic.generic_context(db).unknown_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) - } - } - } - - /// Return an iterator over the inferred types of this class's *explicit* bases. - /// - /// Note that any class (except for `object`) that has no explicit - /// bases will implicitly inherit from `object` at runtime. Nonetheless, - /// this method does *not* include `object` in the bases it iterates over. - /// - /// ## Why is this a salsa query? - /// - /// This is a salsa query to short-circuit the invalidation - /// when the class's AST node changes. - /// - /// Were this not a salsa query, then the calling query - /// would depend on the class's AST and rerun for every change in that file. - pub(super) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { - self.explicit_bases_query(db) - } - - /// Iterate over this class's explicit bases, filtering out any bases that are not class objects. - fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { - self.explicit_bases(db) - .iter() - .copied() - .filter_map(Type::into_class_type) - } - - #[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>]> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::explicit_bases_query: {}", class.name); - - let class_stmt = class.node(db); - let class_definition = - semantic_index(db, class.file(db)).expect_single_definition(class_stmt); - - class_stmt - .bases() - .iter() - .map(|base_node| definition_expression_type(db, class_definition, base_node)) - .collect() - } - - /// Return the types of the decorators on this class - #[salsa::tracked(return_ref)] - fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::decorators: {}", class.name); - - let class_stmt = class.node(db); - if class_stmt.decorator_list.is_empty() { - return Box::new([]); - } - - let class_definition = - semantic_index(db, class.file(db)).expect_single_definition(class_stmt); - - class_stmt - .decorator_list - .iter() - .map(|decorator_node| { - definition_expression_type(db, class_definition, &decorator_node.expression) - }) - .collect() - } - - /// Is this class final? - pub(super) fn is_final(self, db: &'db dyn Db) -> bool { - self.decorators(db) - .iter() - .filter_map(|deco| deco.into_function_literal()) - .any(|decorator| decorator.is_known(db, KnownFunction::Final)) - } - - /// Attempt to resolve the [method resolution order] ("MRO") for this class. - /// If the MRO is unresolvable, return an error indicating why the class's MRO - /// cannot be accurately determined. The error returned contains a fallback MRO - /// that will be used instead for the purposes of type inference. - /// - /// The MRO is the tuple of classes that can be retrieved as the `__mro__` - /// attribute on a class at runtime. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - #[salsa::tracked(return_ref, cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial)] - pub(super) fn try_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> Result, MroError<'db>> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::try_mro: {}", class.name); - Mro::of_class(db, self, specialization) - } - - /// Iterate over the [method resolution order] ("MRO") of the class. - /// - /// If the MRO could not be accurately resolved, this method falls back to iterating - /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`ClassLiteralType::try_mro`] if you need to distinguish between the success and failure - /// cases rather than simply iterating over the inferred resolution order for the class. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> impl Iterator> { - MroIterator::new(db, self, specialization) - } - - /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of( - self, - db: &'db dyn Db, - specialization: Option>, - other: ClassType<'db>, - ) -> bool { - // `is_subclass_of` is checking the subtype relation, in which gradual types do not - // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db, specialization) - .contains(&ClassBase::Class(other)) - } - - /// Return the explicit `metaclass` of this class, if one is defined. - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn explicit_metaclass(self, db: &'db dyn Db) -> Option> { - let class = self.class(db); - let class_stmt = class.node(db); - let metaclass_node = &class_stmt - .arguments - .as_ref()? - .find_keyword("metaclass")? - .value; - - let class_definition = class.definition(db); - - Some(definition_expression_type( - db, - class_definition, - metaclass_node, - )) - } - - /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. - pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - self.try_metaclass(db) - .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) - } - - /// Return a type representing "the set of all instances of the metaclass of this class". - pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { - self - .metaclass(db) - .to_instance(db) - .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") - } - - /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. - #[salsa::tracked] - pub(super) fn try_metaclass(self, db: &'db dyn Db) -> Result, MetaclassError<'db>> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::try_metaclass: {}", class.name); - - // Identify the class's own metaclass (or take the first base class's metaclass). - let mut base_classes = self.fully_static_explicit_bases(db).peekable(); - - if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() { - // We emit diagnostics for cyclic class definitions elsewhere. - // Avoid attempting to infer the metaclass if the class is cyclically defined: - // it would be easy to enter an infinite loop. - return Ok(SubclassOfType::subclass_of_unknown()); - } - - let explicit_metaclass = self.explicit_metaclass(db); - let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { - (metaclass, self) - } else if let Some(base_class) = base_classes.next() { - let (base_class_literal, _) = base_class.class_literal(db); - (base_class.metaclass(db), base_class_literal) - } else { - (KnownClass::Type.to_class_literal(db), self) - }; - - let mut candidate = if let Some(metaclass_ty) = metaclass.into_class_type() { - MetaclassCandidate { - metaclass: metaclass_ty, - explicit_metaclass_of: class_metaclass_was_from, - } - } else { - let name = Type::string_literal(db, &class.name); - let bases = TupleType::from_elements(db, self.explicit_bases(db)); - // TODO: Should be `dict[str, Any]` - let namespace = KnownClass::Dict.to_instance(db); - - // TODO: Other keyword arguments? - let arguments = CallArgumentTypes::positional([name, bases, namespace]); - - 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()), - }), - - // TODO we should also check for binding errors that would indicate the metaclass - // does not accept the right arguments - Err(CallError(CallErrorKind::BindingError, bindings)) => { - Ok(bindings.return_type(db)) - } - - Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { - kind: MetaclassErrorKind::PartlyNotCallable(metaclass), - }), - }; - - return return_ty_result.map(|ty| ty.to_meta_type(db)); - }; - - // Reconcile all base classes' metaclasses with the candidate metaclass. - // - // See: - // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass - // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 - for base_class in base_classes { - let metaclass = base_class.metaclass(db); - let Some(metaclass) = metaclass.into_class_type() else { - continue; - }; - if metaclass.is_subclass_of(db, candidate.metaclass) { - let (base_class_literal, _) = base_class.class_literal(db); - candidate = MetaclassCandidate { - metaclass, - explicit_metaclass_of: base_class_literal, - }; - continue; - } - if candidate.metaclass.is_subclass_of(db, metaclass) { - continue; - } - let (base_class_literal, _) = base_class.class_literal(db); - return Err(MetaclassError { - kind: MetaclassErrorKind::Conflict { - candidate1: candidate, - candidate2: MetaclassCandidate { - metaclass, - explicit_metaclass_of: base_class_literal, - }, - candidate1_is_base_class: explicit_metaclass.is_none(), - }, - }); - } - - Ok(candidate.metaclass.into()) - } - - /// Returns the class member of this class named `name`. - /// - /// The member resolves to a member on the class itself or any of its proper superclasses. - /// - /// TODO: Should this be made private...? - pub(super) fn class_member( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - self.class_member_inner(db, None, name, policy) - } - - fn class_member_inner( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - policy: MemberLookupPolicy, - ) -> SymbolAndQualifiers<'db> { - if name == "__mro__" { - let tuple_elements = self.iter_mro(db, specialization).map(Type::from); - return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into(); - } - - self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) - } - - pub(super) fn class_member_from_mro( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - mro_iter: impl Iterator>, - ) -> SymbolAndQualifiers<'db> { - // If we encounter a dynamic type in this class's MRO, we'll save that dynamic type - // in this variable. After we've traversed the MRO, we'll either: - // (1) Use that dynamic type as the type for this attribute, - // if no other classes in the MRO define the attribute; or, - // (2) Intersect that dynamic type with the type of the attribute - // from the non-dynamic members of the class's MRO. - let mut dynamic_type_to_intersect_with: Option> = None; - - let mut lookup_result: LookupResult<'db> = - Err(LookupError::Unbound(TypeQualifiers::empty())); - - for superclass in mro_iter { - match superclass { - ClassBase::Dynamic(DynamicType::TodoProtocol) => { - // TODO: We currently skip `Protocol` when looking up class members, in order to - // avoid creating many dynamic types in our test suite that would otherwise - // result from looking up attributes on builtin types like `str`, `list`, `tuple` - } - ClassBase::Dynamic(_) => { - // Note: calling `Type::from(superclass).member()` would be incorrect here. - // What we'd really want is a `Type::Any.own_class_member()` method, - // but adding such a method wouldn't make much sense -- it would always return `Any`! - dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass)); - } - ClassBase::Class(class) => { - if class.is_known(db, KnownClass::Object) - // Only exclude `object` members if this is not an `object` class itself - && (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object)) - { - continue; - } - - if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback() - { - continue; - } - - lookup_result = lookup_result.or_else(|lookup_error| { - lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) - }); - } - } - if lookup_result.is_ok() { - break; - } - } - - match ( - SymbolAndQualifiers::from(lookup_result), - dynamic_type_to_intersect_with, - ) { - (symbol_and_qualifiers, None) => symbol_and_qualifiers, - - ( - SymbolAndQualifiers { - symbol: Symbol::Type(ty, _), - qualifiers, - }, - Some(dynamic_type), - ) => Symbol::bound( - IntersectionBuilder::new(db) - .add_positive(ty) - .add_positive(dynamic_type) - .build(), - ) - .with_qualifiers(qualifiers), - - ( - SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers, - }, - Some(dynamic_type), - ) => Symbol::bound(dynamic_type).with_qualifiers(qualifiers), - } - } - - /// Returns the inferred type of the class member named `name`. Only bound members - /// or those marked as ClassVars are considered. - /// - /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope - /// directly. Use [`ClassLiteralType::class_member`] if you require a method that will - /// traverse through the MRO until it finds the member. - pub(super) fn own_class_member( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - ) -> SymbolAndQualifiers<'db> { - let body_scope = self.body_scope(db); - let symbol = class_symbol(db, body_scope, name).map_type(|ty| { - // The `__new__` and `__init__` members of a non-specialized generic class are handled - // specially: they inherit the generic context of their class. That lets us treat them - // as generic functions when constructing the class, and infer the specialization of - // the class from the arguments that are passed in. - // - // We might decide to handle other class methods the same way, having them inherit the - // class's generic context, and performing type inference on calls to them to determine - // the specialization of the class. If we do that, we would update this to also apply - // to any method with a `@classmethod` decorator. (`__init__` would remain a special - // case, since it's an _instance_ method where we don't yet know the generic class's - // specialization.) - match (self, ty, specialization, name) { - ( - ClassLiteralType::Generic(origin), - Type::FunctionLiteral(function), - Some(_), - "__new__" | "__init__", - ) => Type::FunctionLiteral( - function.with_generic_context(db, origin.generic_context(db)), - ), - _ => ty, - } - }); - - if symbol.symbol.is_unbound() { - if let Some(metadata) = self.dataclass_metadata(db) { - if let Some(dataclass_member) = - self.own_dataclass_member(db, specialization, metadata, name) - { - return Symbol::bound(dataclass_member).into(); - } - } - } - - symbol - } - - /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`. - fn own_dataclass_member( - self, - db: &'db dyn Db, - specialization: Option>, - metadata: DataclassMetadata, - name: &str, - ) -> Option> { - if name == "__init__" && metadata.contains(DataclassMetadata::INIT) { - let mut parameters = vec![]; - - for (name, (mut attr_ty, mut default_ty)) in self.dataclass_fields(db, specialization) { - // The descriptor handling below is guarded by this fully-static check, because dynamic - // types like `Any` are valid (data) descriptors: since they have all possible attributes, - // they also have a (callable) `__set__` method. The problem is that we can't determine - // the type of the value parameter this way. Instead, we want to use the dynamic type - // itself in this case, so we skip the special descriptor handling. - if attr_ty.is_fully_static(db) { - let dunder_set = attr_ty.class_member(db, "__set__".into()); - if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { - // This type of this attribute is a data descriptor. Instead of overwriting the - // descriptor attribute, data-classes will (implicitly) call the `__set__` method - // of the descriptor. This means that the synthesized `__init__` parameter for - // this attribute is determined by possible `value` parameter types with which - // the `__set__` method can be called. We build a union of all possible options - // to account for possible overloads. - let mut value_types = UnionBuilder::new(db); - for signature in &dunder_set.signatures(db) { - for overload in signature { - if let Some(value_param) = overload.parameters().get_positional(2) { - value_types = value_types.add( - value_param.annotated_type().unwrap_or_else(Type::unknown), - ); - } else if overload.parameters().is_gradual() { - value_types = value_types.add(Type::unknown()); - } - } - } - attr_ty = value_types.build(); - - // The default value of the attribute is *not* determined by the right hand side - // of the class-body assignment. Instead, the runtime invokes `__get__` on the - // descriptor, as if it had been called on the class itself, i.e. it passes `None` - // for the `instance` argument. - - if let Some(ref mut default_ty) = default_ty { - *default_ty = default_ty - .try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self)) - .map(|(return_ty, _)| return_ty) - .unwrap_or_else(Type::unknown); - } - } - } - - let mut parameter = - Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); - - if let Some(default_ty) = default_ty { - parameter = parameter.with_default_type(default_ty); - } - - parameters.push(parameter); - } - - let init_signature = Signature::new(Parameters::new(parameters), Some(Type::none(db))); - - return Some(Type::Callable(CallableType::new(db, init_signature))); - } else if matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") { - if metadata.contains(DataclassMetadata::ORDER) { - let signature = Signature::new( - Parameters::new([Parameter::positional_or_keyword(Name::new_static("other")) - // TODO: could be `Self`. - .with_annotated_type(Type::instance( - self.apply_optional_specialization(db, specialization), - ))]), - Some(KnownClass::Bool.to_instance(db)), - ); - - return Some(Type::Callable(CallableType::new(db, signature))); - } - } - - None - } - - /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. - /// - /// See [`ClassLiteralType::own_dataclass_fields`] for more details. - fn dataclass_fields( - self, - db: &'db dyn Db, - specialization: Option>, - ) -> FxOrderMap, Option>)> { - let dataclasses_in_mro: Vec<_> = self - .iter_mro(db, specialization) - .filter_map(|superclass| { - if let Some(class) = superclass.into_class() { - let class_literal = class.class_literal(db).0; - if class_literal.dataclass_metadata(db).is_some() { - Some(class_literal) - } else { - None - } - } else { - None - } - }) - // We need to collect into a `Vec` here because we iterate the MRO in reverse order - .collect(); - - dataclasses_in_mro - .into_iter() - .rev() - .flat_map(|class| class.own_dataclass_fields(db)) - // We collect into a FxOrderMap here to deduplicate attributes - .collect() - } - - /// Returns a list of all annotated attributes defined in the body of this class. This is similar - /// to the `__annotations__` attribute at runtime, but also contains default values. - /// - /// For a class body like - /// ```py - /// @dataclass - /// class C: - /// x: int - /// y: str = "a" - /// ``` - /// we return a map `{"x": (int, None), "y": (str, Some(Literal["a"]))}`. - fn own_dataclass_fields( - self, - db: &'db dyn Db, - ) -> FxOrderMap, Option>)> { - let mut attributes = FxOrderMap::default(); - - let class_body_scope = self.body_scope(db); - let table = symbol_table(db, class_body_scope); - - let use_def = use_def_map(db, class_body_scope); - for (symbol_id, declarations) in use_def.all_public_declarations() { - // Here, we exclude all declarations that are not annotated assignments. We need this because - // things like function definitions and nested classes would otherwise be considered dataclass - // fields. The check is too broad in the sense that it also excludes (weird) constructs where - // a symbol would have multiple declarations, one of which is an annotated assignment. If we - // want to improve this, we could instead pass a definition-kind filter to the use-def map - // query, or to the `symbol_from_declarations` call below. Doing so would potentially require - // us to generate a union of `__init__` methods. - if !declarations - .clone() - .all(|DeclarationWithConstraint { declaration, .. }| { - declaration.is_some_and(|declaration| { - matches!( - declaration.kind(db), - DefinitionKind::AnnotatedAssignment(..) - ) - }) - }) - { - continue; - } - - let symbol = table.symbol(symbol_id); - - if let Ok(attr) = symbol_from_declarations(db, declarations) { - if attr.is_class_var() { - continue; - } - - if let Some(attr_ty) = attr.symbol.ignore_possibly_unbound() { - let bindings = use_def.public_bindings(symbol_id); - let default_ty = symbol_from_bindings(db, bindings).ignore_possibly_unbound(); - - attributes.insert(symbol.name().clone(), (attr_ty, default_ty)); - } - } - } - - attributes - } - - /// Returns the `name` attribute of an instance of this class. - /// - /// The attribute could be defined in the class body, but it could also be an implicitly - /// defined attribute that is only present in a method (typically `__init__`). - /// - /// The attribute might also be defined in a superclass of this class. - pub(super) fn instance_member( - self, - db: &'db dyn Db, - specialization: Option>, - name: &str, - ) -> SymbolAndQualifiers<'db> { - let mut union = UnionBuilder::new(db); - let mut union_qualifiers = TypeQualifiers::empty(); - - for superclass in self.iter_mro(db, specialization) { - match superclass { - ClassBase::Dynamic(DynamicType::TodoProtocol) => { - // TODO: We currently skip `Protocol` when looking up instance members, in order to - // avoid creating many dynamic types in our test suite that would otherwise - // result from looking up attributes on builtin types like `str`, `list`, `tuple` - } - ClassBase::Dynamic(_) => { - return SymbolAndQualifiers::todo( - "instance attribute on class with dynamic base", - ); - } - ClassBase::Class(class) => { - if let member @ SymbolAndQualifiers { - symbol: Symbol::Type(ty, boundness), - qualifiers, - } = class.own_instance_member(db, name) - { - // TODO: We could raise a diagnostic here if there are conflicting type qualifiers - union_qualifiers |= qualifiers; - - if boundness == Boundness::Bound { - if union.is_empty() { - // Short-circuit, no need to allocate inside the union builder - return member; - } - - return Symbol::bound(union.add(ty).build()) - .with_qualifiers(union_qualifiers); - } - - // If we see a possibly-unbound symbol, we need to keep looking - // higher up in the MRO. - union = union.add(ty); - } - } - } - } - - if union.is_empty() { - Symbol::Unbound.with_qualifiers(TypeQualifiers::empty()) - } else { - // If we have reached this point, we know that we have only seen possibly-unbound symbols. - // This means that the final result is still possibly-unbound. - - Symbol::Type(union.build(), Boundness::PossiblyUnbound) - .with_qualifiers(union_qualifiers) - } - } - - /// Tries to find declarations/bindings of an instance attribute named `name` that are only - /// "implicitly" defined in a method of the class that corresponds to `class_body_scope`. - fn implicit_instance_attribute( - db: &'db dyn Db, - class_body_scope: ScopeId<'db>, - name: &str, - ) -> Symbol<'db> { - // If we do not see any declarations of an attribute, neither in the class body nor in - // any method, we build a union of `Unknown` with the inferred types of all bindings of - // that attribute. We include `Unknown` in that union to account for the fact that the - // attribute might be externally modified. - let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); - - let mut is_attribute_bound = Truthiness::AlwaysFalse; - - let file = class_body_scope.file(db); - let index = semantic_index(db, file); - let class_map = use_def_map(db, class_body_scope); - let class_table = symbol_table(db, class_body_scope); - - for (attribute_assignments, method_scope_id) in - attribute_assignments(db, class_body_scope, name) - { - let method_scope = method_scope_id.to_scope_id(db, file); - let method_map = use_def_map(db, method_scope); - - // The attribute assignment inherits the visibility of the method which contains it - let is_method_visible = if let Some(method_def) = method_scope.node(db).as_function() { - let method = index.expect_single_definition(method_def); - let method_symbol = class_table.symbol_id_by_name(&method_def.name).unwrap(); - class_map - .public_bindings(method_symbol) - .find_map(|bind| { - (bind.binding == Some(method)) - .then(|| class_map.is_binding_visible(db, &bind)) - }) - .unwrap_or(Truthiness::AlwaysFalse) - } else { - Truthiness::AlwaysFalse - }; - if is_method_visible.is_always_false() { - continue; - } - - let mut attribute_assignments = attribute_assignments.peekable(); - let unbound_visibility = attribute_assignments - .peek() - .map(|attribute_assignment| { - if attribute_assignment.binding.is_none() { - method_map.is_binding_visible(db, attribute_assignment) - } else { - Truthiness::AlwaysFalse - } - }) - .unwrap_or(Truthiness::AlwaysFalse); - - for attribute_assignment in attribute_assignments { - let Some(binding) = attribute_assignment.binding else { - continue; - }; - match method_map - .is_binding_visible(db, &attribute_assignment) - .and(is_method_visible) - { - Truthiness::AlwaysTrue => { - is_attribute_bound = Truthiness::AlwaysTrue; - } - Truthiness::Ambiguous => { - if is_attribute_bound.is_always_false() { - is_attribute_bound = Truthiness::Ambiguous; - } - } - Truthiness::AlwaysFalse => { - continue; - } - } - - // There is at least one attribute assignment that may be visible, - // so if `unbound_visibility` is always false then this attribute is considered bound. - // TODO: this is incomplete logic since the attributes bound after termination are considered visible. - if unbound_visibility - .negate() - .and(is_method_visible) - .is_always_true() - { - is_attribute_bound = Truthiness::AlwaysTrue; - } - - match binding.kind(db) { - DefinitionKind::AnnotatedAssignment(ann_assign) => { - // We found an annotated assignment of one of the following forms (using 'self' in these - // examples, but we support arbitrary names for the first parameters of methods): - // - // self.name: - // self.name: = … - - let annotation_ty = - infer_expression_type(db, index.expression(ann_assign.annotation())); - - // TODO: check if there are conflicting declarations - match is_attribute_bound { - Truthiness::AlwaysTrue => { - return Symbol::bound(annotation_ty); - } - Truthiness::Ambiguous => { - return Symbol::possibly_unbound(annotation_ty); - } - Truthiness::AlwaysFalse => unreachable!("If the attribute assignments are all invisible, inference of their types should be skipped"), - } - } - DefinitionKind::Assignment(assign) => { - match assign.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // .., self.name, .. = - // (.., self.name, ..) = - // [.., self.name, ..] = - - let unpacked = infer_unpack_types(db, unpack); - let target_ast_id = - assign.target().scoped_expression_id(db, method_scope); - let inferred_ty = unpacked.expression_type(target_ast_id); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::NameOrAttribute => { - // We found an un-annotated attribute assignment of the form: - // - // self.name = - - let inferred_ty = - infer_expression_type(db, index.expression(assign.value())); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::For(for_stmt) => { - match for_stmt.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // for .., self.name, .. in : - - let unpacked = infer_unpack_types(db, unpack); - let target_ast_id = - for_stmt.target().scoped_expression_id(db, method_scope); - let inferred_ty = unpacked.expression_type(target_ast_id); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::NameOrAttribute => { - // We found an attribute assignment like: - // - // for self.name in : - - let iterable_ty = infer_expression_type( - db, - index.expression(for_stmt.iterable()), - ); - // TODO: Potential diagnostics resulting from the iterable are currently not reported. - let inferred_ty = iterable_ty.iterate(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::WithItem(with_item) => { - match with_item.target_kind() { - TargetKind::Sequence(_, unpack) => { - // We found an unpacking assignment like: - // - // with as .., self.name, ..: - - let unpacked = infer_unpack_types(db, unpack); - let target_ast_id = - with_item.target().scoped_expression_id(db, method_scope); - let inferred_ty = unpacked.expression_type(target_ast_id); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - TargetKind::NameOrAttribute => { - // We found an attribute assignment like: - // - // with as self.name: - - let context_ty = infer_expression_type( - db, - index.expression(with_item.context_expr()), - ); - let inferred_ty = context_ty.enter(db); - - union_of_inferred_types = union_of_inferred_types.add(inferred_ty); - } - } - } - DefinitionKind::Comprehension(_) => { - // TODO: - } - DefinitionKind::AugmentedAssignment(_) => { - // TODO: - } - DefinitionKind::NamedExpression(_) => { - // TODO: - } - _ => {} - } - } - } - - match is_attribute_bound { - Truthiness::AlwaysTrue => Symbol::bound(union_of_inferred_types.build()), - Truthiness::Ambiguous => Symbol::possibly_unbound(union_of_inferred_types.build()), - Truthiness::AlwaysFalse => Symbol::Unbound, - } - } - - /// A helper function for `instance_member` that looks up the `name` attribute only on - /// this class, not on its superclasses. - fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - // TODO: There are many things that are not yet implemented here: - // - `typing.Final` - // - Proper diagnostics - - let body_scope = self.body_scope(db); - let table = symbol_table(db, body_scope); - - if let Some(symbol_id) = table.symbol_id_by_name(name) { - let use_def = use_def_map(db, body_scope); - - let declarations = use_def.public_declarations(symbol_id); - let declared_and_qualifiers = symbol_from_declarations(db, declarations); - match declared_and_qualifiers { - Ok(SymbolAndQualifiers { - symbol: declared @ Symbol::Type(declared_ty, declaredness), - qualifiers, - }) => { - // The attribute is declared in the class body. - - let bindings = use_def.public_bindings(symbol_id); - let inferred = symbol_from_bindings(db, bindings); - let has_binding = !inferred.is_unbound(); - - if has_binding { - // The attribute is declared and bound in the class body. - - if let Some(implicit_ty) = - Self::implicit_instance_attribute(db, body_scope, name) - .ignore_possibly_unbound() - { - if declaredness == Boundness::Bound { - // If a symbol is definitely declared, and we see - // attribute assignments in methods of the class, - // we trust the declared type. - declared.with_qualifiers(qualifiers) - } else { - Symbol::Type( - UnionType::from_elements(db, [declared_ty, implicit_ty]), - declaredness, - ) - .with_qualifiers(qualifiers) - } - } else { - // The symbol is declared and bound in the class body, - // but we did not find any attribute assignments in - // methods of the class. This means that the attribute - // has a class-level default value, but it would not be - // found in a `__dict__` lookup. - - Symbol::Unbound.into() - } - } else { - // The attribute is declared but not bound in the class body. - // We take this as a sign that this is intended to be a pure - // instance attribute, and we trust the declared type, unless - // it is possibly-undeclared. In the latter case, we also - // union with the inferred type from attribute assignments. - - if declaredness == Boundness::Bound { - declared.with_qualifiers(qualifiers) - } else { - if let Some(implicit_ty) = - Self::implicit_instance_attribute(db, body_scope, name) - .ignore_possibly_unbound() - { - Symbol::Type( - UnionType::from_elements(db, [declared_ty, implicit_ty]), - declaredness, - ) - .with_qualifiers(qualifiers) - } else { - declared.with_qualifiers(qualifiers) - } - } - } - } - - Ok(SymbolAndQualifiers { - symbol: Symbol::Unbound, - qualifiers: _, - }) => { - // The attribute is not *declared* in the class body. It could still be declared/bound - // in a method. - - Self::implicit_instance_attribute(db, body_scope, name).into() - } - Err((declared, _conflicting_declarations)) => { - // There are conflicting declarations for this attribute in the class body. - Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) - } - } - } else { - // This attribute is neither declared nor bound in the class body. - // It could still be implicitly defined in a method. - - Self::implicit_instance_attribute(db, body_scope, name).into() - } - } - - /// Return this class' involvement in an inheritance cycle, if any. - /// - /// A class definition like this will fail at runtime, - /// but we must be resilient to it or we could panic. - #[salsa::tracked(cycle_fn=inheritance_cycle_recover, cycle_initial=inheritance_cycle_initial)] - pub(super) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { - /// Return `true` if the class is cyclically defined. - /// - /// Also, populates `visited_classes` with all base classes of `self`. - fn is_cyclically_defined_recursive<'db>( - db: &'db dyn Db, - class: ClassLiteralType<'db>, - classes_on_stack: &mut IndexSet>, - visited_classes: &mut IndexSet>, - ) -> bool { - let mut result = false; - for explicit_base_class in class.fully_static_explicit_bases(db) { - let (explicit_base_class_literal, _) = explicit_base_class.class_literal(db); - if !classes_on_stack.insert(explicit_base_class_literal) { - return true; - } - - if visited_classes.insert(explicit_base_class_literal) { - // If we find a cycle, keep searching to check if we can reach the starting class. - result |= is_cyclically_defined_recursive( - db, - explicit_base_class_literal, - classes_on_stack, - visited_classes, - ); - } - - classes_on_stack.pop(); - } - result - } - - tracing::trace!("Class::inheritance_cycle: {}", self.name(db)); - - let visited_classes = &mut IndexSet::new(); - if !is_cyclically_defined_recursive(db, self, &mut IndexSet::new(), visited_classes) { - None - } else if visited_classes.contains(&self) { - Some(InheritanceCycle::Participant) - } else { - Some(InheritanceCycle::Inherited) - } - } -} - -impl<'db> From> for Type<'db> { - fn from(class: ClassLiteralType<'db>) -> Type<'db> { - match class { - ClassLiteralType::NonGeneric(non_generic) => non_generic.into(), - ClassLiteralType::Generic(generic) => generic.into(), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(super) enum InheritanceCycle { - /// The class is cyclically defined and is a participant in the cycle. - /// i.e., it inherits either directly or indirectly from itself. - Participant, - /// The class inherits from a class that is a `Participant` in an inheritance cycle, - /// but is not itself a participant. - Inherited, -} - -impl InheritanceCycle { - pub(super) const fn is_participant(self) -> bool { - matches!(self, InheritanceCycle::Participant) - } -} - -/// A type representing the set of runtime objects which are instances of a certain class. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] -pub struct InstanceType<'db> { - pub class: ClassType<'db>, -} - -impl<'db> InstanceType<'db> { - pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { - // N.B. The subclass relation is fully static - self.class.is_subclass_of(db, other.class) - } -} - -impl<'db> From> for Type<'db> { - fn from(value: InstanceType<'db>) -> Self { - Self::Instance(value) - } -} -/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow -/// for easier syntax when interacting with very common classes. -/// -/// Feel free to expand this enum if you ever find yourself using the same class in multiple -/// places. -/// Note: good candidates are any classes in `[crate::module_resolver::module::KnownModule]` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(test, derive(strum_macros::EnumIter))] -pub(crate) enum KnownClass { - // To figure out where an stdlib symbol is defined, you can go into `crates/red_knot_vendored` - // and grep for the symbol name in any `.pyi` file. - - // Builtins - Bool, - Object, - Bytes, - Bytearray, - Type, - Int, - Float, - Complex, - Str, - List, - Tuple, - Set, - FrozenSet, - Dict, - Slice, - Range, - Property, - BaseException, - BaseExceptionGroup, - Classmethod, - Super, - // enum - Enum, - // Types - GenericAlias, - ModuleType, - FunctionType, - MethodType, - MethodWrapperType, - WrapperDescriptorType, - UnionType, - // Typeshed - NoneType, // Part of `types` for Python >= 3.10 - // Typing - Any, - StdlibAlias, - SpecialForm, - TypeVar, - ParamSpec, - ParamSpecArgs, - ParamSpecKwargs, - TypeVarTuple, - TypeAliasType, - NoDefaultType, - NewType, - Sized, - // TODO: This can probably be removed when we have support for protocols - SupportsIndex, - // Collections - ChainMap, - Counter, - DefaultDict, - Deque, - OrderedDict, - // sys - VersionInfo, - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - EllipsisType, - NotImplementedType, -} - -impl<'db> KnownClass { - pub(crate) const fn is_bool(self) -> bool { - matches!(self, Self::Bool) - } - - pub(crate) const fn is_special_form(self) -> bool { - matches!(self, Self::SpecialForm) - } - - /// Determine whether instances of this class are always truthy, always falsy, - /// or have an ambiguous truthiness. - pub(crate) const fn bool(self) -> Truthiness { - match self { - // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` - // variant if the class's `__bool__` method always returns the same thing *and* the - // class is `@final`. - // - // E.g. `ModuleType.__bool__` always returns `True`, but `ModuleType` is not `@final`. - // Equally, `range` is `@final`, but its `__bool__` method can return `False`. - Self::EllipsisType - | Self::NoDefaultType - | Self::MethodType - | Self::Slice - | Self::FunctionType - | Self::VersionInfo - | Self::TypeAliasType - | Self::TypeVar - | Self::ParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Super - | Self::WrapperDescriptorType - | Self::UnionType - | Self::MethodWrapperType => Truthiness::AlwaysTrue, - - Self::NoneType => Truthiness::AlwaysFalse, - - Self::Any - | Self::BaseException - | Self::Object - | Self::OrderedDict - | Self::BaseExceptionGroup - | Self::Bool - | Self::Str - | Self::List - | Self::GenericAlias - | Self::NewType - | Self::StdlibAlias - | Self::SupportsIndex - | Self::Set - | Self::Tuple - | Self::Int - | Self::Type - | Self::Bytes - | Self::Bytearray - | Self::FrozenSet - | Self::Range - | Self::Property - | Self::SpecialForm - | Self::Dict - | Self::ModuleType - | Self::ChainMap - | Self::Complex - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::Float - | Self::Sized - | Self::Enum - // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 - // and raises a `TypeError` in Python >=3.14 - // (see https://docs.python.org/3/library/constants.html#NotImplemented) - | Self::NotImplementedType - | Self::Classmethod => Truthiness::Ambiguous, - } - } - - pub(crate) fn name(self, db: &'db dyn Db) -> &'static str { - match self { - Self::Any => "Any", - Self::Bool => "bool", - Self::Object => "object", - Self::Bytes => "bytes", - Self::Bytearray => "bytearray", - Self::Tuple => "tuple", - Self::Int => "int", - Self::Float => "float", - Self::Complex => "complex", - Self::FrozenSet => "frozenset", - Self::Str => "str", - Self::Set => "set", - Self::Dict => "dict", - Self::List => "list", - Self::Type => "type", - Self::Slice => "slice", - Self::Range => "range", - Self::Property => "property", - Self::BaseException => "BaseException", - Self::BaseExceptionGroup => "BaseExceptionGroup", - Self::Classmethod => "classmethod", - Self::GenericAlias => "GenericAlias", - Self::ModuleType => "ModuleType", - Self::FunctionType => "FunctionType", - Self::MethodType => "MethodType", - Self::UnionType => "UnionType", - Self::MethodWrapperType => "MethodWrapperType", - Self::WrapperDescriptorType => "WrapperDescriptorType", - Self::NoneType => "NoneType", - Self::SpecialForm => "_SpecialForm", - Self::TypeVar => "TypeVar", - Self::ParamSpec => "ParamSpec", - Self::ParamSpecArgs => "ParamSpecArgs", - Self::ParamSpecKwargs => "ParamSpecKwargs", - Self::TypeVarTuple => "TypeVarTuple", - Self::TypeAliasType => "TypeAliasType", - Self::NoDefaultType => "_NoDefaultType", - Self::NewType => "NewType", - Self::SupportsIndex => "SupportsIndex", - Self::ChainMap => "ChainMap", - Self::Counter => "Counter", - Self::DefaultDict => "defaultdict", - Self::Deque => "deque", - Self::Sized => "Sized", - Self::OrderedDict => "OrderedDict", - Self::Enum => "Enum", - Self::Super => "super", - // For example, `typing.List` is defined as `List = _Alias()` in typeshed - Self::StdlibAlias => "_Alias", - // This is the name the type of `sys.version_info` has in typeshed, - // which is different to what `type(sys.version_info).__name__` is at runtime. - // (At runtime, `type(sys.version_info).__name__ == "version_info"`, - // which is impossible to replicate in the stubs since the sole instance of the class - // also has that name in the `sys` module.) - Self::VersionInfo => "_version_info", - Self::EllipsisType => { - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - "EllipsisType" - } else { - "ellipsis" - } - } - Self::NotImplementedType => "_NotImplementedType", - } - } - - fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { - struct KnownClassDisplay<'db> { - db: &'db dyn Db, - class: KnownClass, - } - - impl std::fmt::Display for KnownClassDisplay<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let KnownClassDisplay { - class: known_class, - db, - } = *self; - write!( - f, - "{module}.{class}", - module = known_class.canonical_module(db), - class = known_class.name(db) - ) - } - } - - KnownClassDisplay { db, class: self } - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing all possible instances of the class. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { - self.to_class_literal(db) - .into_class_type() - .map(Type::instance) - .unwrap_or_else(Type::unknown) - } - - /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// Return an error if the symbol cannot be found in the expected typeshed module, - /// or if the symbol is not a class definition, or if the symbol is possibly unbound. - pub(crate) fn try_to_class_literal( - self, - db: &'db dyn Db, - ) -> Result, KnownClassLookupError<'db>> { - let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).symbol; - match symbol { - Symbol::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal), - Symbol::Type(Type::ClassLiteral(class_literal), Boundness::PossiblyUnbound) => { - Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }) - } - Symbol::Type(found_type, _) => { - Err(KnownClassLookupError::SymbolNotAClass { found_type }) - } - Symbol::Unbound => Err(KnownClassLookupError::ClassNotFound), - } - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> { - // a cache of the `KnownClass`es that we have already failed to lookup in typeshed - // (and therefore that we've already logged a warning for) - static MESSAGES: LazyLock>> = LazyLock::new(Mutex::default); - - self.try_to_class_literal(db) - .map(Type::ClassLiteral) - .unwrap_or_else(|lookup_error| { - if MESSAGES.lock().unwrap().insert(self) { - if matches!( - lookup_error, - KnownClassLookupError::ClassPossiblyUnbound { .. } - ) { - tracing::info!("{}", lookup_error.display(db, self)); - } else { - tracing::info!( - "{}. Falling back to `Unknown` for the symbol instead.", - lookup_error.display(db, self) - ); - } - } - - match lookup_error { - KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { - class_literal.into() - } - KnownClassLookupError::ClassNotFound { .. } - | KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(), - } - }) - } - - /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] - /// representing that class and all possible subclasses of the class. - /// - /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. - pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> { - self.to_class_literal(db) - .into_class_type() - .map(|class| SubclassOfType::from(db, class)) - .unwrap_or_else(SubclassOfType::subclass_of_unknown) - } - - /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, - /// *and* `class` is a subclass of `other`. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.try_to_class_literal(db) - .is_ok_and(|class| class.is_subclass_of(db, None, other)) - } - - /// Return the module in which we should look up the definition for this class - fn canonical_module(self, db: &'db dyn Db) -> KnownModule { - match self { - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Classmethod - | Self::Slice - | Self::Range - | Self::Super - | Self::Property => KnownModule::Builtins, - Self::VersionInfo => KnownModule::Sys, - Self::Enum => KnownModule::Enum, - Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::UnionType - | Self::WrapperDescriptorType => KnownModule::Types, - Self::NoneType => KnownModule::Typeshed, - Self::Any - | Self::SpecialForm - | Self::TypeVar - | Self::StdlibAlias - | Self::SupportsIndex - | Self::Sized => KnownModule::Typing, - Self::TypeAliasType - | Self::TypeVarTuple - | Self::ParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::NewType => KnownModule::TypingExtensions, - Self::NoDefaultType => { - let python_version = Program::get(db).python_version(db); - - // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` - // singleton, but not for `typing._NoDefaultType`. So we need to switch - // to `typing._NoDefaultType` for newer versions: - if python_version >= PythonVersion::PY313 { - KnownModule::Typing - } else { - KnownModule::TypingExtensions - } - } - Self::EllipsisType => { - // Exposed as `types.EllipsisType` on Python >=3.10; - // backported as `builtins.ellipsis` by typeshed on Python <=3.9 - if Program::get(db).python_version(db) >= PythonVersion::PY310 { - KnownModule::Types - } else { - KnownModule::Builtins - } - } - Self::NotImplementedType => KnownModule::Builtins, - Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict => KnownModule::Collections, - } - } - - /// Return true if all instances of this `KnownClass` compare equal. - pub(super) const fn is_single_valued(self) -> bool { - match self { - Self::NoneType - | Self::NoDefaultType - | Self::VersionInfo - | Self::EllipsisType - | Self::TypeAliasType - | Self::UnionType - | Self::NotImplementedType => true, - - Self::Any - | Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Range - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Classmethod - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::SupportsIndex - | Self::StdlibAlias - | Self::TypeVar - | Self::ParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Sized - | Self::Enum - | Self::Super - | Self::NewType => false, - } - } - - /// Is this class a singleton class? - /// - /// A singleton class is a class where it is known that only one instance can ever exist at runtime. - pub(super) const fn is_singleton(self) -> bool { - match self { - Self::NoneType - | Self::EllipsisType - | Self::NoDefaultType - | Self::VersionInfo - | Self::TypeAliasType - | Self::NotImplementedType => true, - - Self::Any - | Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Tuple - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::List - | Self::Type - | Self::Slice - | Self::Range - | Self::Property - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::StdlibAlias - | Self::SupportsIndex - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Classmethod - | Self::TypeVar - | Self::ParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Sized - | Self::Enum - | Self::Super - | Self::UnionType - | Self::NewType => false, - } - } - - pub(super) fn try_from_file_and_name( - db: &dyn Db, - file: File, - class_name: &str, - ) -> Option { - // We assert that this match is exhaustive over the right-hand side in the unit test - // `known_class_roundtrip_from_str()` - let candidate = match class_name { - "Any" => Self::Any, - "bool" => Self::Bool, - "object" => Self::Object, - "bytes" => Self::Bytes, - "bytearray" => Self::Bytearray, - "tuple" => Self::Tuple, - "type" => Self::Type, - "int" => Self::Int, - "float" => Self::Float, - "complex" => Self::Complex, - "str" => Self::Str, - "set" => Self::Set, - "frozenset" => Self::FrozenSet, - "dict" => Self::Dict, - "list" => Self::List, - "slice" => Self::Slice, - "range" => Self::Range, - "property" => Self::Property, - "BaseException" => Self::BaseException, - "BaseExceptionGroup" => Self::BaseExceptionGroup, - "classmethod" => Self::Classmethod, - "GenericAlias" => Self::GenericAlias, - "NoneType" => Self::NoneType, - "ModuleType" => Self::ModuleType, - "FunctionType" => Self::FunctionType, - "MethodType" => Self::MethodType, - "UnionType" => Self::UnionType, - "MethodWrapperType" => Self::MethodWrapperType, - "WrapperDescriptorType" => Self::WrapperDescriptorType, - "NewType" => Self::NewType, - "TypeAliasType" => Self::TypeAliasType, - "TypeVar" => Self::TypeVar, - "ParamSpec" => Self::ParamSpec, - "ParamSpecArgs" => Self::ParamSpecArgs, - "ParamSpecKwargs" => Self::ParamSpecKwargs, - "TypeVarTuple" => Self::TypeVarTuple, - "ChainMap" => Self::ChainMap, - "Counter" => Self::Counter, - "defaultdict" => Self::DefaultDict, - "deque" => Self::Deque, - "OrderedDict" => Self::OrderedDict, - "_Alias" => Self::StdlibAlias, - "_SpecialForm" => Self::SpecialForm, - "_NoDefaultType" => Self::NoDefaultType, - "SupportsIndex" => Self::SupportsIndex, - "Sized" => Self::Sized, - "Enum" => Self::Enum, - "super" => Self::Super, - "_version_info" => Self::VersionInfo, - "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { - Self::EllipsisType - } - "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { - Self::EllipsisType - } - "_NotImplementedType" => Self::NotImplementedType, - _ => return None, - }; - - candidate - .check_module(db, file_to_module(db, file)?.known()?) - .then_some(candidate) - } - - /// Return `true` if the module of `self` matches `module` - fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool { - match self { - Self::Any - | Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Tuple - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Range - | Self::Property - | Self::GenericAlias - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will - | Self::ModuleType - | Self::VersionInfo - | Self::BaseException - | Self::EllipsisType - | Self::BaseExceptionGroup - | Self::Classmethod - | Self::FunctionType - | Self::MethodType - | Self::MethodWrapperType - | Self::Enum - | Self::Super - | Self::NotImplementedType - | Self::UnionType - | Self::WrapperDescriptorType => module == self.canonical_module(db), - Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), - Self::SpecialForm - | Self::TypeVar - | Self::TypeAliasType - | Self::NoDefaultType - | Self::SupportsIndex - | Self::ParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::Sized - | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), - } - } -} - -/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownClassLookupError<'db> { - /// There is no symbol by that name in the expected typeshed module. - ClassNotFound, - /// There is a symbol by that name in the expected typeshed module, - /// but it's not a class. - SymbolNotAClass { found_type: Type<'db> }, - /// There is a symbol by that name in the expected typeshed module, - /// and it's a class definition, but it's possibly unbound. - ClassPossiblyUnbound { - class_literal: ClassLiteralType<'db>, - }, -} - -impl<'db> KnownClassLookupError<'db> { - fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { - struct ErrorDisplay<'db> { - db: &'db dyn Db, - class: KnownClass, - error: KnownClassLookupError<'db>, - } - - impl std::fmt::Display for ErrorDisplay<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let ErrorDisplay { db, class, error } = *self; - - let class = class.display(db); - let python_version = Program::get(db).python_version(db); - - match error { - KnownClassLookupError::ClassNotFound => write!( - f, - "Could not find class `{class}` in typeshed on Python {python_version}", - ), - KnownClassLookupError::SymbolNotAClass { found_type } => write!( - f, - "Error looking up `{class}` in typeshed: expected to find a class definition \ - on Python {python_version}, but found a symbol of type `{found_type}` instead", - found_type = found_type.display(db), - ), - KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( - f, - "Error looking up `{class}` in typeshed on Python {python_version}: \ - expected to find a fully bound symbol, but found one that is possibly unbound", - ) - } - } - } - - ErrorDisplay { - db, - class, - error: *self, - } - } -} - -/// Enumeration of specific runtime that are special enough to be considered their own type. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub enum KnownInstanceType<'db> { - /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) - Annotated, - /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) - Literal, - /// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`) - LiteralString, - /// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`) - Optional, - /// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`) - Union, - /// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`) - NoReturn, - /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) - Never, - /// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`) - /// This is not used since typeshed switched to representing `Any` as a class; now we use - /// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at - /// least for now. TODO maybe remove? - Any, - /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) - Tuple, - /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) - List, - /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) - Dict, - /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) - Set, - /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) - FrozenSet, - /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) - ChainMap, - /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) - Counter, - /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) - DefaultDict, - /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) - Deque, - /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) - OrderedDict, - /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) - Protocol, - /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) - Type, - /// A single instance of `typing.TypeVar` - TypeVar(TypeVarInstance<'db>), - /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) - TypeAliasType(TypeAliasType<'db>), - /// The symbol `knot_extensions.Unknown` - Unknown, - /// The symbol `knot_extensions.AlwaysTruthy` - AlwaysTruthy, - /// The symbol `knot_extensions.AlwaysFalsy` - AlwaysFalsy, - /// The symbol `knot_extensions.Not` - Not, - /// The symbol `knot_extensions.Intersection` - Intersection, - /// The symbol `knot_extensions.TypeOf` - TypeOf, - /// The symbol `knot_extensions.CallableTypeOf` - CallableTypeOf, - - // Various special forms, special aliases and type qualifiers that we don't yet understand - // (all currently inferred as TODO in most contexts): - TypingSelf, - Final, - ClassVar, - Callable, - Concatenate, - Unpack, - Required, - NotRequired, - TypeAlias, - TypeGuard, - TypeIs, - ReadOnly, - // TODO: fill this enum out with more special forms, etc. -} - -impl<'db> KnownInstanceType<'db> { - /// Evaluate the known instance in boolean context - pub(crate) const fn bool(self) -> Truthiness { - match self { - Self::Annotated - | Self::Literal - | Self::LiteralString - | Self::Optional - | Self::TypeVar(_) - | Self::Union - | Self::NoReturn - | Self::Never - | Self::Any - | Self::Tuple - | Self::Type - | Self::TypingSelf - | Self::Final - | Self::ClassVar - | Self::Callable - | Self::Concatenate - | Self::Unpack - | Self::Required - | Self::NotRequired - | Self::TypeAlias - | Self::TypeGuard - | Self::TypeIs - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::Deque - | Self::ChainMap - | Self::OrderedDict - | Self::Protocol - | Self::ReadOnly - | Self::TypeAliasType(_) - | Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy - | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf => Truthiness::AlwaysTrue, - } - } - - /// Return the repr of the symbol at runtime - pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str { - match self { - Self::Annotated => "typing.Annotated", - Self::Literal => "typing.Literal", - Self::LiteralString => "typing.LiteralString", - Self::Optional => "typing.Optional", - Self::Union => "typing.Union", - Self::NoReturn => "typing.NoReturn", - Self::Never => "typing.Never", - Self::Any => "typing.Any", - Self::Tuple => "typing.Tuple", - Self::Type => "typing.Type", - Self::TypingSelf => "typing.Self", - Self::Final => "typing.Final", - Self::ClassVar => "typing.ClassVar", - Self::Callable => "typing.Callable", - Self::Concatenate => "typing.Concatenate", - Self::Unpack => "typing.Unpack", - Self::Required => "typing.Required", - Self::NotRequired => "typing.NotRequired", - Self::TypeAlias => "typing.TypeAlias", - Self::TypeGuard => "typing.TypeGuard", - Self::TypeIs => "typing.TypeIs", - Self::List => "typing.List", - Self::Dict => "typing.Dict", - Self::DefaultDict => "typing.DefaultDict", - Self::Set => "typing.Set", - Self::FrozenSet => "typing.FrozenSet", - Self::Counter => "typing.Counter", - Self::Deque => "typing.Deque", - Self::ChainMap => "typing.ChainMap", - Self::OrderedDict => "typing.OrderedDict", - Self::Protocol => "typing.Protocol", - Self::ReadOnly => "typing.ReadOnly", - Self::TypeVar(typevar) => typevar.name(db), - Self::TypeAliasType(_) => "typing.TypeAliasType", - Self::Unknown => "knot_extensions.Unknown", - Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy", - Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy", - Self::Not => "knot_extensions.Not", - Self::Intersection => "knot_extensions.Intersection", - Self::TypeOf => "knot_extensions.TypeOf", - Self::CallableTypeOf => "knot_extensions.CallableTypeOf", - } - } - - /// Return the [`KnownClass`] which this symbol is an instance of - pub(crate) const fn class(self) -> KnownClass { - match self { - Self::Annotated => KnownClass::SpecialForm, - Self::Literal => KnownClass::SpecialForm, - Self::LiteralString => KnownClass::SpecialForm, - Self::Optional => KnownClass::SpecialForm, - Self::Union => KnownClass::SpecialForm, - Self::NoReturn => KnownClass::SpecialForm, - Self::Never => KnownClass::SpecialForm, - Self::Any => KnownClass::Object, - Self::Tuple => KnownClass::SpecialForm, - Self::Type => KnownClass::SpecialForm, - Self::TypingSelf => KnownClass::SpecialForm, - Self::Final => KnownClass::SpecialForm, - Self::ClassVar => KnownClass::SpecialForm, - Self::Callable => KnownClass::SpecialForm, - Self::Concatenate => KnownClass::SpecialForm, - Self::Unpack => KnownClass::SpecialForm, - Self::Required => KnownClass::SpecialForm, - Self::NotRequired => KnownClass::SpecialForm, - Self::TypeAlias => KnownClass::SpecialForm, - Self::TypeGuard => KnownClass::SpecialForm, - Self::TypeIs => KnownClass::SpecialForm, - Self::ReadOnly => KnownClass::SpecialForm, - Self::List => KnownClass::StdlibAlias, - Self::Dict => KnownClass::StdlibAlias, - Self::DefaultDict => KnownClass::StdlibAlias, - Self::Set => KnownClass::StdlibAlias, - Self::FrozenSet => KnownClass::StdlibAlias, - Self::Counter => KnownClass::StdlibAlias, - Self::Deque => KnownClass::StdlibAlias, - Self::ChainMap => KnownClass::StdlibAlias, - Self::OrderedDict => KnownClass::StdlibAlias, - Self::Protocol => KnownClass::SpecialForm, - Self::TypeVar(_) => KnownClass::TypeVar, - Self::TypeAliasType(_) => KnownClass::TypeAliasType, - Self::TypeOf => KnownClass::SpecialForm, - Self::Not => KnownClass::SpecialForm, - Self::Intersection => KnownClass::SpecialForm, - Self::CallableTypeOf => KnownClass::SpecialForm, - Self::Unknown => KnownClass::Object, - Self::AlwaysTruthy => KnownClass::Object, - Self::AlwaysFalsy => KnownClass::Object, - } - } - - /// Return the instance type which this type is a subtype of. - /// - /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, - /// so `KnownInstanceType::Literal.instance_fallback(db)` - /// returns `Type::Instance(InstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &dyn Db) -> Type { - self.class().to_instance(db) - } - - /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool { - self.class().is_subclass_of(db, class) - } - - pub(super) fn try_from_file_and_name( - db: &'db dyn Db, - file: File, - symbol_name: &str, - ) -> Option { - let candidate = match symbol_name { - "Any" => Self::Any, - "ClassVar" => Self::ClassVar, - "Deque" => Self::Deque, - "List" => Self::List, - "Dict" => Self::Dict, - "DefaultDict" => Self::DefaultDict, - "Set" => Self::Set, - "FrozenSet" => Self::FrozenSet, - "Counter" => Self::Counter, - "ChainMap" => Self::ChainMap, - "OrderedDict" => Self::OrderedDict, - "Protocol" => Self::Protocol, - "Optional" => Self::Optional, - "Union" => Self::Union, - "NoReturn" => Self::NoReturn, - "Tuple" => Self::Tuple, - "Type" => Self::Type, - "Callable" => Self::Callable, - "Annotated" => Self::Annotated, - "Literal" => Self::Literal, - "Never" => Self::Never, - "Self" => Self::TypingSelf, - "Final" => Self::Final, - "Unpack" => Self::Unpack, - "Required" => Self::Required, - "TypeAlias" => Self::TypeAlias, - "TypeGuard" => Self::TypeGuard, - "TypeIs" => Self::TypeIs, - "ReadOnly" => Self::ReadOnly, - "Concatenate" => Self::Concatenate, - "NotRequired" => Self::NotRequired, - "LiteralString" => Self::LiteralString, - "Unknown" => Self::Unknown, - "AlwaysTruthy" => Self::AlwaysTruthy, - "AlwaysFalsy" => Self::AlwaysFalsy, - "Not" => Self::Not, - "Intersection" => Self::Intersection, - "TypeOf" => Self::TypeOf, - "CallableTypeOf" => Self::CallableTypeOf, - _ => return None, - }; - - candidate - .check_module(file_to_module(db, file)?.known()?) - .then_some(candidate) - } - - /// Return `true` if `module` is a module from which this `KnownInstance` variant can validly originate. - /// - /// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`. - /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. - fn check_module(self, module: KnownModule) -> bool { - match self { - Self::Any - | Self::ClassVar - | Self::Deque - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::ChainMap - | Self::OrderedDict - | Self::Protocol - | Self::Optional - | Self::Union - | Self::NoReturn - | Self::Tuple - | Self::Type - | Self::Callable => module.is_typing(), - Self::Annotated - | Self::Literal - | Self::LiteralString - | Self::Never - | Self::TypingSelf - | Self::Final - | Self::Concatenate - | Self::Unpack - | Self::Required - | Self::NotRequired - | Self::TypeAlias - | Self::TypeGuard - | Self::TypeIs - | Self::ReadOnly - | Self::TypeAliasType(_) - | Self::TypeVar(_) => { - matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) - } - Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy - | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf => module.is_knot_extensions(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(super) struct MetaclassError<'db> { - kind: MetaclassErrorKind<'db>, -} - -impl<'db> MetaclassError<'db> { - /// Return an [`MetaclassErrorKind`] variant describing why we could not resolve the metaclass for this class. - pub(super) fn reason(&self) -> &MetaclassErrorKind<'db> { - &self.kind - } -} - -#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] -pub(super) enum MetaclassErrorKind<'db> { - /// The class has incompatible metaclasses in its inheritance hierarchy. - /// - /// The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all - /// its bases. - Conflict { - /// `candidate1` will either be the explicit `metaclass=` keyword in the class definition, - /// or the inferred metaclass of a base class - candidate1: MetaclassCandidate<'db>, - - /// `candidate2` will always be the inferred metaclass of a base class - candidate2: MetaclassCandidate<'db>, - - /// Flag to indicate whether `candidate1` is the explicit `metaclass=` keyword or the - /// inferred metaclass of a base class. This helps us give better error messages in diagnostics. - candidate1_is_base_class: bool, - }, - /// The metaclass is not callable - NotCallable(Type<'db>), - /// The metaclass is of a union type whose some members are not callable - PartlyNotCallable(Type<'db>), -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::db::tests::setup_db; - use crate::module_resolver::resolve_module; - use salsa::Setter; - use strum::IntoEnumIterator; - - #[test] - fn known_class_roundtrip_from_str() { - let db = setup_db(); - for class in KnownClass::iter() { - let class_name = class.name(&db); - let class_module = resolve_module(&db, &class.canonical_module(&db).name()).unwrap(); - - assert_eq!( - KnownClass::try_from_file_and_name(&db, class_module.file(), class_name), - Some(class), - "`KnownClass::candidate_from_str` appears to be missing a case for `{class_name}`" - ); - } - } - - #[test] - fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { - let mut db = setup_db(); - - Program::get(&db) - .set_python_version(&mut db) - .to(PythonVersion::latest()); - - for class in KnownClass::iter() { - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}`" - ); - } - } - - #[test] - fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { - let mut db = setup_db(); - - for class in KnownClass::iter() { - let version_added = match class { - KnownClass::UnionType => PythonVersion::PY310, - KnownClass::BaseExceptionGroup => PythonVersion::PY311, - KnownClass::GenericAlias => PythonVersion::PY39, - _ => PythonVersion::PY37, - }; - - Program::get(&db) - .set_python_version(&mut db) - .to(version_added); - - assert_ne!( - class.to_instance(&db), - Type::unknown(), - "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" - ); - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs deleted file mode 100644 index 4b33c60b8ccf1..0000000000000 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::types::{todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, Type}; -use crate::Db; -use itertools::Either; - -/// Enumeration of the possible kinds of types we allow in class bases. -/// -/// This is much more limited than the [`Type`] enum: all types that would be invalid to have as a -/// class base are transformed into [`ClassBase::unknown`] -/// -/// Note that a non-specialized generic class _cannot_ be a class base. When we see a -/// non-specialized generic class in any type expression (including the list of base classes), we -/// automatically construct the default specialization for that class. -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)] -pub enum ClassBase<'db> { - Dynamic(DynamicType), - Class(ClassType<'db>), -} - -impl<'db> ClassBase<'db> { - pub(crate) const fn any() -> Self { - Self::Dynamic(DynamicType::Any) - } - - pub(crate) const fn unknown() -> Self { - Self::Dynamic(DynamicType::Unknown) - } - - pub(crate) const fn is_dynamic(self) -> bool { - match self { - ClassBase::Dynamic(_) => true, - ClassBase::Class(_) => false, - } - } - - pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { - struct Display<'db> { - base: ClassBase<'db>, - db: &'db dyn Db, - } - - impl std::fmt::Display for Display<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.base { - ClassBase::Dynamic(dynamic) => dynamic.fmt(f), - ClassBase::Class(class @ ClassType::NonGeneric(_)) => { - write!(f, "", class.name(self.db)) - } - ClassBase::Class(ClassType::Generic(alias)) => { - write!(f, "", alias.display(self.db)) - } - } - } - } - - Display { base: self, db } - } - - /// Return a `ClassBase` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { - KnownClass::Object - .to_class_literal(db) - .into_class_type() - .map_or(Self::unknown(), Self::Class) - } - - /// Attempt to resolve `ty` into a `ClassBase`. - /// - /// Return `None` if `ty` is not an acceptable type for a class base. - pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { - match ty { - Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), - Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) { - Self::Dynamic(DynamicType::Any) - } else { - Self::Class(literal.default_specialization(db)) - }), - Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), - Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? - Type::Intersection(_) => None, // TODO -- probably incorrect? - Type::Instance(_) => None, // TODO -- handle `__mro_entries__`? - Type::PropertyInstance(_) => None, - Type::Never - | Type::BooleanLiteral(_) - | Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::MethodWrapper(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::BytesLiteral(_) - | Type::IntLiteral(_) - | Type::StringLiteral(_) - | Type::LiteralString - | Type::Tuple(_) - | Type::SliceLiteral(_) - | Type::ModuleLiteral(_) - | Type::SubclassOf(_) - | Type::TypeVar(_) - | Type::BoundSuper(_) - | Type::AlwaysFalsy - | Type::AlwaysTruthy => None, - Type::KnownInstance(known_instance) => match known_instance { - KnownInstanceType::TypeVar(_) - | KnownInstanceType::TypeAliasType(_) - | KnownInstanceType::Annotated - | KnownInstanceType::Literal - | KnownInstanceType::LiteralString - | KnownInstanceType::Union - | KnownInstanceType::NoReturn - | KnownInstanceType::Never - | KnownInstanceType::Final - | KnownInstanceType::NotRequired - | KnownInstanceType::TypeGuard - | KnownInstanceType::TypeIs - | KnownInstanceType::TypingSelf - | KnownInstanceType::Unpack - | KnownInstanceType::ClassVar - | KnownInstanceType::Concatenate - | KnownInstanceType::Required - | KnownInstanceType::TypeAlias - | KnownInstanceType::ReadOnly - | KnownInstanceType::Optional - | KnownInstanceType::Not - | KnownInstanceType::Intersection - | KnownInstanceType::TypeOf - | KnownInstanceType::CallableTypeOf - | KnownInstanceType::AlwaysTruthy - | KnownInstanceType::AlwaysFalsy => None, - KnownInstanceType::Unknown => Some(Self::unknown()), - KnownInstanceType::Any => Some(Self::any()), - // TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO - KnownInstanceType::Dict => { - Self::try_from_type(db, KnownClass::Dict.to_class_literal(db)) - } - KnownInstanceType::List => { - Self::try_from_type(db, KnownClass::List.to_class_literal(db)) - } - KnownInstanceType::Type => { - Self::try_from_type(db, KnownClass::Type.to_class_literal(db)) - } - KnownInstanceType::Tuple => { - Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db)) - } - KnownInstanceType::Set => { - Self::try_from_type(db, KnownClass::Set.to_class_literal(db)) - } - KnownInstanceType::FrozenSet => { - Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db)) - } - KnownInstanceType::ChainMap => { - Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db)) - } - KnownInstanceType::Counter => { - Self::try_from_type(db, KnownClass::Counter.to_class_literal(db)) - } - KnownInstanceType::DefaultDict => { - Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db)) - } - KnownInstanceType::Deque => { - Self::try_from_type(db, KnownClass::Deque.to_class_literal(db)) - } - KnownInstanceType::OrderedDict => { - Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) - } - KnownInstanceType::Callable => { - Self::try_from_type(db, todo_type!("Support for Callable as a base class")) - } - KnownInstanceType::Protocol => Some(ClassBase::Dynamic(DynamicType::TodoProtocol)), - }, - } - } - - pub(super) fn into_class(self) -> Option> { - match self { - Self::Class(class) => Some(class), - Self::Dynamic(_) => None, - } - } - - /// Iterate over the MRO of this base - pub(super) fn mro( - self, - db: &'db dyn Db, - ) -> Either>, impl Iterator>> { - match self { - ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()), - ClassBase::Class(class) => Either::Right(class.iter_mro(db)), - } - } -} - -impl<'db> From> for ClassBase<'db> { - fn from(value: ClassType<'db>) -> Self { - ClassBase::Class(value) - } -} - -impl<'db> From> for Type<'db> { - fn from(value: ClassBase<'db>) -> Self { - match value { - ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), - ClassBase::Class(class) => class.into(), - } - } -} - -impl<'db> From<&ClassBase<'db>> for Type<'db> { - fn from(value: &ClassBase<'db>) -> Self { - Self::from(*value) - } -} diff --git a/crates/red_knot_python_semantic/src/types/definition.rs b/crates/red_knot_python_semantic/src/types/definition.rs deleted file mode 100644 index 207aed39b685b..0000000000000 --- a/crates/red_knot_python_semantic/src/types/definition.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::semantic_index::definition::Definition; -use crate::{Db, Module}; -use ruff_db::files::FileRange; -use ruff_db::source::source_text; -use ruff_text_size::{TextLen, TextRange}; - -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum TypeDefinition<'db> { - Module(Module), - Class(Definition<'db>), - Function(Definition<'db>), - TypeVar(Definition<'db>), - TypeAlias(Definition<'db>), -} - -impl TypeDefinition<'_> { - pub fn focus_range(&self, db: &dyn Db) -> Option { - match self { - Self::Module(_) => None, - Self::Class(definition) - | Self::Function(definition) - | Self::TypeVar(definition) - | Self::TypeAlias(definition) => Some(definition.focus_range(db)), - } - } - - pub fn full_range(&self, db: &dyn Db) -> FileRange { - match self { - Self::Module(module) => { - let source = source_text(db.upcast(), module.file()); - FileRange::new(module.file(), TextRange::up_to(source.text_len())) - } - Self::Class(definition) - | Self::Function(definition) - | Self::TypeVar(definition) - | Self::TypeAlias(definition) => definition.full_range(db), - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs deleted file mode 100644 index bca2c739a8d4e..0000000000000 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ /dev/null @@ -1,1302 +0,0 @@ -use super::context::InferContext; -use crate::declare_lint; -use crate::lint::{Level, LintRegistryBuilder, LintStatus}; -use crate::suppression::FileSuppressionId; -use crate::types::string_annotation::{ - BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, - IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, - RAW_STRING_TYPE_ANNOTATION, -}; -use crate::types::{KnownInstanceType, Type}; -use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; -use ruff_python_ast::{self as ast, AnyNodeRef}; -use ruff_text_size::Ranged; -use rustc_hash::FxHashSet; -use std::fmt::Formatter; - -/// Registers all known type check lints. -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); - registry.register_lint(&DIVISION_BY_ZERO); - registry.register_lint(&DUPLICATE_BASE); - registry.register_lint(&INCOMPATIBLE_SLOTS); - registry.register_lint(&INCONSISTENT_MRO); - registry.register_lint(&INDEX_OUT_OF_BOUNDS); - registry.register_lint(&INVALID_ARGUMENT_TYPE); - registry.register_lint(&INVALID_RETURN_TYPE); - registry.register_lint(&INVALID_ASSIGNMENT); - registry.register_lint(&INVALID_BASE); - registry.register_lint(&INVALID_CONTEXT_MANAGER); - registry.register_lint(&INVALID_DECLARATION); - registry.register_lint(&INVALID_EXCEPTION_CAUGHT); - registry.register_lint(&INVALID_METACLASS); - registry.register_lint(&INVALID_PARAMETER_DEFAULT); - registry.register_lint(&INVALID_RAISE); - registry.register_lint(&INVALID_SUPER_ARGUMENT); - registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT); - registry.register_lint(&INVALID_TYPE_FORM); - registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); - registry.register_lint(&MISSING_ARGUMENT); - registry.register_lint(&NO_MATCHING_OVERLOAD); - registry.register_lint(&NON_SUBSCRIPTABLE); - registry.register_lint(&NOT_ITERABLE); - registry.register_lint(&UNSUPPORTED_BOOL_CONVERSION); - registry.register_lint(&PARAMETER_ALREADY_ASSIGNED); - registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE); - registry.register_lint(&POSSIBLY_UNBOUND_IMPORT); - registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE); - registry.register_lint(&SUBCLASS_OF_FINAL_CLASS); - registry.register_lint(&TYPE_ASSERTION_FAILURE); - registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS); - registry.register_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS); - registry.register_lint(&UNDEFINED_REVEAL); - registry.register_lint(&UNKNOWN_ARGUMENT); - registry.register_lint(&UNRESOLVED_ATTRIBUTE); - registry.register_lint(&UNRESOLVED_IMPORT); - registry.register_lint(&UNRESOLVED_REFERENCE); - registry.register_lint(&UNSUPPORTED_OPERATOR); - registry.register_lint(&ZERO_STEPSIZE_IN_SLICE); - registry.register_lint(&STATIC_ASSERT_ERROR); - registry.register_lint(&INVALID_ATTRIBUTE_ACCESS); - registry.register_lint(&REDUNDANT_CAST); - - // String annotations - registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION); - registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION); - registry.register_lint(&FSTRING_TYPE_ANNOTATION); - registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION); - registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION); - registry.register_lint(&RAW_STRING_TYPE_ANNOTATION); -} - -declare_lint! { - /// ## What it does - /// Checks for calls to non-callable objects. - /// - /// ## Why is this bad? - /// Calling a non-callable object will raise a `TypeError` at runtime. - /// - /// ## Examples - /// ```python - /// 4() # TypeError: 'int' object is not callable - /// ``` - pub(crate) static CALL_NON_CALLABLE = { - summary: "detects calls to non-callable objects", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for calls to possibly unbound methods. - /// - /// TODO #14889 - pub(crate) static CALL_POSSIBLY_UNBOUND_METHOD = { - summary: "detects calls to possibly unbound methods", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -declare_lint! { - /// ## What it does - /// Checks 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 = { - summary: "detects conflicting declarations", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static CONFLICTING_METACLASS = { - summary: "detects conflicting metaclasses", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for class definitions with a cyclic inheritance chain. - /// - /// ## Why is it bad? - /// TODO #14889 - pub(crate) static CYCLIC_CLASS_DEFINITION = { - summary: "detects cyclic class definitions", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// It detects division by zero. - /// - /// ## Why is this bad? - /// Dividing by zero raises a `ZeroDivisionError` at runtime. - /// - /// ## Examples - /// ```python - /// 5 / 0 - /// ``` - pub(crate) static DIVISION_BY_ZERO = { - summary: "detects division by zero", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static DUPLICATE_BASE = { - summary: "detects class definitions with duplicate bases", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for classes whose bases define incompatible `__slots__`. - /// - /// ## Why is this bad? - /// Inheriting from bases with incompatible `__slots__`s - /// will lead to a `TypeError` at runtime. - /// - /// Classes with no or empty `__slots__` are always compatible: - /// - /// ```python - /// class A: ... - /// class B: - /// __slots__ = () - /// class C: - /// __slots__ = ("a", "b") - /// - /// # fine - /// class D(A, B, C): ... - /// ``` - /// - /// Multiple inheritance from more than one different class - /// defining non-empty `__slots__` is not allowed: - /// - /// ```python - /// class A: - /// __slots__ = ("a", "b") - /// - /// class B: - /// __slots__ = ("a", "b") # Even if the values are the same - /// - /// # TypeError: multiple bases have instance lay-out conflict - /// class C(A, B): ... - /// ``` - /// - /// ## Known problems - /// Dynamic (not tuple or string literal) `__slots__` are not checked. - /// Additionally, classes inheriting from built-in classes with implicit layouts - /// like `str` or `int` are also not checked. - /// - /// ```pycon - /// >>> hasattr(int, "__slots__") - /// False - /// >>> hasattr(str, "__slots__") - /// False - /// >>> class A(int, str): ... - /// Traceback (most recent call last): - /// File "", line 1, in - /// class A(int, str): ... - /// TypeError: multiple bases have instance lay-out conflict - /// ``` - pub(crate) static INCOMPATIBLE_SLOTS = { - summary: "detects class definitions whose MRO has conflicting `__slots__`", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INCONSISTENT_MRO = { - summary: "detects class definitions with an inconsistent MRO", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// TODO #14889 - pub(crate) static INDEX_OUT_OF_BOUNDS = { - summary: "detects index out of bounds errors", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Detects call arguments whose type is not assignable to the corresponding typed parameter. - /// - /// ## Why is this bad? - /// Passing an argument of a type the function (or callable object) does not accept violates - /// the expectations of the function author and may cause unexpected runtime errors within the - /// body of the function. - /// - /// ## Examples - /// ```python - /// def func(x: int): ... - /// func("foo") # error: [invalid-argument-type] - /// ``` - pub(crate) static INVALID_ARGUMENT_TYPE = { - summary: "detects call arguments whose type is not assignable to the corresponding typed parameter", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Detects returned values that can't be assigned to the function's annotated return type. - /// - /// ## Why is this bad? - /// Returning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function. - /// - /// ## Examples - /// ```python - /// def func() -> int: - /// return "a" # error: [invalid-return-type] - /// ``` - pub(crate) static INVALID_RETURN_TYPE = { - summary: "detects returned values that can't be assigned to the function's annotated return type", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INVALID_ASSIGNMENT = { - summary: "detects invalid assignments", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INVALID_BASE = { - summary: "detects class definitions with an invalid base", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INVALID_CONTEXT_MANAGER = { - summary: "detects expressions used in with statements that don't implement the context manager protocol", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INVALID_DECLARATION = { - summary: "detects invalid declarations", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for exception handlers that catch non-exception classes. - /// - /// ## Why is this bad? - /// Catching classes that do not inherit from `BaseException` will raise a TypeError at runtime. - /// - /// ## Example - /// ```python - /// try: - /// 1 / 0 - /// except 1: - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// try: - /// 1 / 0 - /// except ZeroDivisionError: - /// ... - /// ``` - /// - /// ## References - /// - [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) - /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) - /// - /// ## Ruff rule - /// This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes) - pub(crate) static INVALID_EXCEPTION_CAUGHT = { - summary: "detects exception handlers that catch classes that do not inherit from `BaseException`", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for arguments to `metaclass=` that are invalid. - /// - /// ## Why is this bad? - /// Python allows arbitrary expressions to be used as the argument to `metaclass=`. - /// These expressions, however, need to be callable and accept the same arguments - /// as `type.__new__`. - /// - /// ## Example - /// - /// ```python - /// def f(): ... - /// - /// # TypeError: f() takes 0 positional arguments but 3 were given - /// class B(metaclass=f): ... - /// ``` - /// - /// ## References - /// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses) - pub(crate) static INVALID_METACLASS = { - summary: "detects invalid `metaclass=` arguments", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for default values that can't be assigned to the parameter's annotated type. - /// - /// ## Why is this bad? - /// TODO #14889 - pub(crate) static INVALID_PARAMETER_DEFAULT = { - summary: "detects default values that can't be assigned to the parameter's annotated type", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// Checks for `raise` statements that raise non-exceptions or use invalid - /// causes for their raised exceptions. - /// - /// ## Why is this bad? - /// Only subclasses or instances of `BaseException` can be raised. - /// For an exception's cause, the same rules apply, except that `None` is also - /// permitted. Violating these rules results in a `TypeError` at runtime. - /// - /// ## Examples - /// ```python - /// def f(): - /// try: - /// something() - /// except NameError: - /// raise "oops!" from f - /// - /// def g(): - /// raise NotImplemented from 42 - /// ``` - /// - /// Use instead: - /// ```python - /// def f(): - /// try: - /// something() - /// except NameError as e: - /// raise RuntimeError("oops!") from e - /// - /// def g(): - /// raise NotImplementedError from None - /// ``` - /// - /// ## References - /// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise) - /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) - pub(crate) static INVALID_RAISE = { - summary: "detects `raise` statements that raise invalid exceptions or use invalid causes", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Detects `super()` calls where: - /// - the first argument is not a valid class literal, or - /// - the second argument is not an instance or subclass of the first argument. - /// - /// ## Why is this bad? - /// `super(type, obj)` expects: - /// - the first argument to be a class, - /// - and the second argument to satisfy one of the following: - /// - `isinstance(obj, type)` is `True` - /// - `issubclass(obj, type)` is `True` - /// - /// Violating this relationship will raise a `TypeError` at runtime. - /// - /// ## Examples - /// ```python - /// class A: - /// ... - /// class B(A): - /// ... - /// - /// super(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)` - /// - /// super(A(), B()) # error: `A()` is not a class - /// - /// super(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)` - /// super(B, A) # error: `A` does not satisfy `issubclass(A, B)` - /// ``` - /// - /// ## References - /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) - pub(crate) static INVALID_SUPER_ARGUMENT = { - summary: "detects invalid arguments for `super()`", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an - /// annotation not assignable from `bool`. - /// - /// ## Why is this bad? - /// The name `TYPE_CHECKING` is reserved for a flag that can be used to provide conditional - /// code seen only by the type checker, and not at runtime. Normally this flag is imported from - /// `typing` or `typing_extensions`, but it can also be defined locally. If defined locally, it - /// must be assigned the value `False` at runtime; the type checker will consider its value to - /// be `True`. If annotated, it must be annotated as a type that can accept `bool` values. - pub(crate) static INVALID_TYPE_CHECKING_CONSTANT = { - summary: "detects invalid TYPE_CHECKING constant assignments", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for invalid type expressions. - /// - /// ## Why is this bad? - /// TODO #14889 - pub(crate) static INVALID_TYPE_FORM = { - summary: "detects invalid type forms", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = { - summary: "detects invalid type variable constraints", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for missing required arguments in a call. - /// - /// ## Why is this bad? - /// Failing to provide a required argument will raise a `TypeError` at runtime. - /// - /// ## Examples - /// ```python - /// def func(x: int): ... - /// func() # TypeError: func() missing 1 required positional argument: 'x' - /// ``` - pub(crate) static MISSING_ARGUMENT = { - summary: "detects missing required arguments in a call", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for calls to an overloaded function that do not match any of the overloads. - /// - /// ## Why is this bad? - /// Failing to provide the correct arguments to one of the overloads will raise a `TypeError` - /// at runtime. - /// - /// ## Examples - /// ```python - /// @overload - /// def func(x: int): ... - /// @overload - /// def func(x: bool): ... - /// func("string") # error: [no-matching-overload] - /// ``` - pub(crate) static NO_MATCHING_OVERLOAD = { - summary: "detects calls that do not match any overload", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for subscripting objects that do not support subscripting. - /// - /// ## Why is this bad? - /// Subscripting an object that does not support it will raise a `TypeError` at runtime. - /// - /// ## Examples - /// ```python - /// 4[1] # TypeError: 'int' object is not subscriptable - /// ``` - pub(crate) static NON_SUBSCRIPTABLE = { - summary: "detects subscripting objects that do not support subscripting", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for objects that are not iterable but are used in a context that requires them to be. - /// - /// ## Why is this bad? - /// Iterating over an object that is not iterable will raise a `TypeError` at runtime. - /// - /// ## Examples - /// - /// ```python - /// for i in 34: # TypeError: 'int' object is not iterable - /// pass - /// ``` - pub(crate) static NOT_ITERABLE = { - summary: "detects iteration over an object that is not iterable", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for bool conversions where the object doesn't correctly implement `__bool__`. - /// - /// ## Why is this bad? - /// If an exception is raised when you attempt to evaluate the truthiness of an object, - /// using the object in a boolean context will fail at runtime. - /// - /// ## Examples - /// - /// ```python - /// class NotBoolable: - /// __bool__ = None - /// - /// b1 = NotBoolable() - /// b2 = NotBoolable() - /// - /// if b1: # exception raised here - /// pass - /// - /// b1 and b2 # exception raised here - /// not b1 # exception raised here - /// b1 < b2 < b1 # exception raised here - /// ``` - pub(crate) static UNSUPPORTED_BOOL_CONVERSION = { - summary: "detects boolean conversion where the object incorrectly implements `__bool__`", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for calls which provide more than one argument for a single parameter. - /// - /// ## Why is this bad? - /// Providing multiple values for a single parameter will raise a `TypeError` at runtime. - /// - /// ## Examples - /// - /// ```python - /// def f(x: int) -> int: ... - /// - /// f(1, x=2) # Error raised here - /// ``` - pub(crate) static PARAMETER_ALREADY_ASSIGNED = { - summary: "detects multiple arguments for the same parameter", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for possibly unbound attributes. - /// - /// TODO #14889 - pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = { - summary: "detects references to possibly unbound attributes", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static POSSIBLY_UNBOUND_IMPORT = { - summary: "detects possibly unbound imports", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -declare_lint! { - /// ## What it does - /// Checks for references to names that are possibly not defined. - /// - /// ## Why is this bad? - /// Using an undefined variable will raise a `NameError` at runtime. - /// - /// ## Example - /// - /// ```python - /// for i in range(0): - /// x = i - /// - /// print(x) # NameError: name 'x' is not defined - /// ``` - pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = { - summary: "detects references to possibly undefined names", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -declare_lint! { - /// ## What it does - /// Checks for classes that subclass final classes. - /// - /// ## Why is this bad? - /// Decorating a class with `@final` declares to the type checker that it should not be subclassed. - /// - /// ## Example - /// - /// ```python - /// from typing import final - /// - /// @final - /// class A: ... - /// class B(A): ... # Error raised here - /// ``` - pub(crate) static SUBCLASS_OF_FINAL_CLASS = { - summary: "detects subclasses of final classes", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for `assert_type()` and `assert_never()` calls where the actual type - /// is not the same as the asserted type. - /// - /// ## Why is this bad? - /// `assert_type()` allows confirming the inferred type of a certain value. - /// - /// ## Example - /// - /// ```python - /// def _(x: int): - /// assert_type(x, int) # fine - /// assert_type(x, str) # error: Actual type does not match asserted type - /// ``` - pub(crate) static TYPE_ASSERTION_FAILURE = { - summary: "detects failed type assertions", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for calls that pass more positional arguments than the callable can accept. - /// - /// ## Why is this bad? - /// Passing too many positional arguments will raise `TypeError` at runtime. - /// - /// ## Example - /// - /// ```python - /// def f(): ... - /// - /// f("foo") # Error raised here - /// ``` - pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = { - summary: "detects calls passing too many positional arguments", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Detects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable. - /// - /// ## Why is this bad? - /// When `super()` is used without arguments, Python tries to find two things: - /// the nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls). - /// If either of these is missing, the call will fail at runtime with a `RuntimeError`. - /// - /// ## Examples - /// ```python - /// super() # error: no enclosing class or function found - /// - /// def func(): - /// super() # error: no enclosing class or first argument exists - /// - /// class A: - /// f = super() # error: no enclosing function to provide the first argument - /// - /// def method(self): - /// def nested(): - /// super() # error: first argument does not exist in this nested function - /// - /// lambda: super() # error: first argument does not exist in this lambda - /// - /// (super() for _ in range(10)) # error: argument is not available in generator expression - /// - /// super() # okay! both enclosing class and first argument are available - /// ``` - /// - /// ## References - /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) - pub(crate) static UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS = { - summary: "detects invalid `super()` calls where implicit arguments are unavailable.", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for calls to `reveal_type` without importing it. - /// - /// ## Why is this bad? - /// Using `reveal_type` without importing it will raise a `NameError` at runtime. - /// - /// ## Examples - /// TODO #14889 - pub(crate) static UNDEFINED_REVEAL = { - summary: "detects usages of `reveal_type` without importing it", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -declare_lint! { - /// ## What it does - /// Checks for keyword arguments in calls that don't match any parameter of the callable. - /// - /// ## Why is this bad? - /// Providing an unknown keyword argument will raise `TypeError` at runtime. - /// - /// ## Example - /// - /// ```python - /// def f(x: int) -> int: ... - /// - /// f(x=1, y=2) # Error raised here - /// ``` - pub(crate) static UNKNOWN_ARGUMENT = { - summary: "detects unknown keyword arguments in calls", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for unresolved attributes. - /// - /// TODO #14889 - pub(crate) static UNRESOLVED_ATTRIBUTE = { - summary: "detects references to unresolved attributes", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for import statements for which the module cannot be resolved. - /// - /// ## Why is this bad? - /// Importing a module that cannot be resolved will raise an `ImportError` at runtime. - pub(crate) static UNRESOLVED_IMPORT = { - summary: "detects unresolved imports", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for references to names that are not defined. - /// - /// ## Why is this bad? - /// Using an undefined variable will raise a `NameError` at runtime. - /// - /// ## Example - /// - /// ```python - /// print(x) # NameError: name 'x' is not defined - /// ``` - pub(crate) static UNRESOLVED_REFERENCE = { - summary: "detects references to names that are not defined", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -declare_lint! { - /// ## What it does - /// Checks for binary expressions, comparisons, and unary expressions where the operands don't support the operator. - /// - /// TODO #14889 - pub(crate) static UNSUPPORTED_OPERATOR = { - summary: "detects binary, unary, or comparison expressions where the operands don't support the operator", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for step size 0 in slices. - /// - /// ## Why is this bad? - /// A slice with a step size of zero will raise a `ValueError` at runtime. - /// - /// ## Examples - /// ```python - /// l = list(range(10)) - /// l[1:10:0] # ValueError: slice step cannot be zero - /// ``` - pub(crate) static ZERO_STEPSIZE_IN_SLICE = { - summary: "detects a slice step size of zero", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Makes sure that the argument of `static_assert` is statically known to be true. - /// - /// ## Examples - /// ```python - /// from knot_extensions import static_assert - /// - /// static_assert(1 + 1 == 3) # error: evaluates to `False` - /// - /// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness - /// ``` - pub(crate) static STATIC_ASSERT_ERROR = { - summary: "Failed static assertion", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Makes sure that instance attribute accesses are valid. - /// - /// ## Examples - /// ```python - /// class C: - /// var: ClassVar[int] = 1 - /// - /// C.var = 3 # okay - /// C().var = 3 # error: Cannot assign to class variable - /// ``` - pub(crate) static INVALID_ATTRIBUTE_ACCESS = { - summary: "Invalid attribute access", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Detects redundant `cast` calls where the value already has the target type. - /// - /// ## Why is this bad? - /// These casts have no effect and can be removed. - /// - /// ## Example - /// ```python - /// def f() -> int: - /// return 10 - /// - /// cast(int, f()) # Redundant - /// ``` - pub(crate) static REDUNDANT_CAST = { - summary: "detects redundant `cast` calls", - status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, - } -} - -/// A collection of type check diagnostics. -#[derive(Default, Eq, PartialEq)] -pub struct TypeCheckDiagnostics { - diagnostics: Vec, - used_suppressions: FxHashSet, -} - -impl TypeCheckDiagnostics { - pub(crate) fn push(&mut self, diagnostic: Diagnostic) { - self.diagnostics.push(diagnostic); - } - - pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) { - self.diagnostics.extend_from_slice(&other.diagnostics); - self.used_suppressions.extend(&other.used_suppressions); - } - - pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) { - self.used_suppressions.insert(suppression_id); - } - - pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool { - self.used_suppressions.contains(&suppression_id) - } - - pub(crate) fn used_len(&self) -> usize { - self.used_suppressions.len() - } - - pub(crate) fn shrink_to_fit(&mut self) { - self.used_suppressions.shrink_to_fit(); - self.diagnostics.shrink_to_fit(); - } - - pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> { - self.diagnostics.iter() - } -} - -impl std::fmt::Debug for TypeCheckDiagnostics { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.diagnostics.fmt(f) - } -} - -impl IntoIterator for TypeCheckDiagnostics { - type Item = Diagnostic; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.diagnostics.into_iter() - } -} - -impl<'a> IntoIterator for &'a TypeCheckDiagnostics { - type Item = &'a Diagnostic; - type IntoIter = std::slice::Iter<'a, Diagnostic>; - - fn into_iter(self) -> Self::IntoIter { - self.diagnostics.iter() - } -} - -/// Emit a diagnostic declaring that an index is out of bounds for a tuple. -pub(super) fn report_index_out_of_bounds( - context: &InferContext, - kind: &'static str, - node: AnyNodeRef, - tuple_ty: Type, - length: usize, - index: i64, -) { - context.report_lint_old( - &INDEX_OUT_OF_BOUNDS, - node, - format_args!( - "Index {index} is out of bounds for {kind} `{}` with length {length}", - tuple_ty.display(context.db()) - ), - ); -} - -/// Emit a diagnostic declaring that a type does not support subscripting. -pub(super) fn report_non_subscriptable( - context: &InferContext, - node: AnyNodeRef, - non_subscriptable_ty: Type, - method: &str, -) { - context.report_lint_old( - &NON_SUBSCRIPTABLE, - node, - format_args!( - "Cannot subscript object of type `{}` with no `{method}` method", - non_subscriptable_ty.display(context.db()) - ), - ); -} - -pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) { - context.report_lint_old( - &ZERO_STEPSIZE_IN_SLICE, - node, - format_args!("Slice step size can not be zero"), - ); -} - -fn report_invalid_assignment_with_message( - context: &InferContext, - node: AnyNodeRef, - target_ty: Type, - message: std::fmt::Arguments, -) { - match target_ty { - Type::ClassLiteral(class) => { - context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!( - "Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional", - class.name(context.db()))); - } - Type::FunctionLiteral(function) => { - context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!( - "Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional", - function.name(context.db()))); - } - _ => { - context.report_lint_old(&INVALID_ASSIGNMENT, node, message); - } - } -} - -pub(super) fn report_invalid_assignment( - context: &InferContext, - node: AnyNodeRef, - target_ty: Type, - source_ty: Type, -) { - report_invalid_assignment_with_message( - context, - node, - target_ty, - format_args!( - "Object of type `{}` is not assignable to `{}`", - source_ty.display(context.db()), - target_ty.display(context.db()), - ), - ); -} - -pub(super) fn report_invalid_attribute_assignment( - context: &InferContext, - node: AnyNodeRef, - target_ty: Type, - source_ty: Type, - attribute_name: &'_ str, -) { - report_invalid_assignment_with_message( - context, - node, - target_ty, - format_args!( - "Object of type `{}` is not assignable to attribute `{attribute_name}` of type `{}`", - source_ty.display(context.db()), - target_ty.display(context.db()), - ), - ); -} - -pub(super) fn report_invalid_return_type( - context: &InferContext, - object_range: impl Ranged, - return_type_range: impl Ranged, - expected_ty: Type, - actual_ty: Type, -) { - let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, object_range) else { - return; - }; - - let return_type_span = Span::from(context.file()).with_range(return_type_range.range()); - - let mut diag = builder.into_diagnostic("Return type does not match returned value"); - diag.set_primary_message(format_args!( - "Expected `{expected_ty}`, found `{actual_ty}`", - expected_ty = expected_ty.display(context.db()), - actual_ty = actual_ty.display(context.db()), - )); - diag.annotate( - Annotation::secondary(return_type_span).message(format_args!( - "Expected `{expected_ty}` because of return type", - expected_ty = expected_ty.display(context.db()), - )), - ); -} - -pub(super) fn report_implicit_return_type( - context: &InferContext, - range: impl Ranged, - expected_ty: Type, -) { - context.report_lint_old( - &INVALID_RETURN_TYPE, - range, - format_args!( - "Function can implicitly return `None`, which is not assignable to return type `{}`", - expected_ty.display(context.db()) - ), - ); -} - -pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) { - context.report_lint_old( - &INVALID_TYPE_CHECKING_CONSTANT, - node, - format_args!("The name TYPE_CHECKING is reserved for use as a flag; only False can be assigned to it.",), - ); -} - -pub(super) fn report_possibly_unresolved_reference( - context: &InferContext, - expr_name_node: &ast::ExprName, -) { - let ast::ExprName { id, .. } = expr_name_node; - - context.report_lint_old( - &POSSIBLY_UNRESOLVED_REFERENCE, - expr_name_node, - format_args!("Name `{id}` used when possibly not defined"), - ); -} - -pub(super) fn report_possibly_unbound_attribute( - context: &InferContext, - target: &ast::ExprAttribute, - attribute: &str, - object_ty: Type, -) { - context.report_lint_old( - &POSSIBLY_UNBOUND_ATTRIBUTE, - target, - format_args!( - "Attribute `{attribute}` on type `{}` is possibly unbound", - object_ty.display(context.db()), - ), - ); -} - -pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) { - let ast::ExprName { id, .. } = expr_name_node; - - context.report_lint_old( - &UNRESOLVED_REFERENCE, - expr_name_node, - format_args!("Name `{id}` used when not defined"), - ); -} - -pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { - context.report_lint_old( - &INVALID_EXCEPTION_CAUGHT, - node, - format_args!( - "Cannot catch object of type `{}` in an exception handler \ - (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", - ty.display(context.db()) - ), - ); -} - -pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) { - context.report_lint_old( - &INVALID_RAISE, - node, - format_args!( - "Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)", - ty.display(context.db()) - ), - ); -} - -pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { - context.report_lint_old( - &INVALID_RAISE, - node, - format_args!( - "Cannot use object of type `{}` as exception cause \ - (must be a `BaseException` subclass or instance or `None`)", - ty.display(context.db()) - ), - ); -} - -pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) { - context.report_lint_old( - &INCOMPATIBLE_SLOTS, - node, - format_args!("Class base has incompatible `__slots__`"), - ); -} - -pub(crate) fn report_invalid_arguments_to_annotated( - context: &InferContext, - subscript: &ast::ExprSubscript, -) { - context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected at least 2 arguments (one type and at least one metadata element)", - KnownInstanceType::Annotated.repr(context.db()) - ), - ); -} - -pub(crate) fn report_invalid_arguments_to_callable( - context: &InferContext, - subscript: &ast::ExprSubscript, -) { - context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected exactly two arguments (parameter types and return type)", - KnownInstanceType::Callable.repr(context.db()) - ), - ); -} diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs deleted file mode 100644 index 5a6da8013258c..0000000000000 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ /dev/null @@ -1,904 +0,0 @@ -//! Display implementations for types. - -use std::fmt::{self, Display, Formatter, Write}; - -use ruff_db::display::FormatterJoinExtension; -use ruff_python_ast::str::{Quote, TripleQuotes}; -use ruff_python_literal::escape::AsciiEscape; - -use crate::types::class::{ClassType, GenericAlias, GenericClass}; -use crate::types::class_base::ClassBase; -use crate::types::generics::{GenericContext, Specialization}; -use crate::types::signatures::{Parameter, Parameters, Signature}; -use crate::types::{ - InstanceType, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, Type, - TypeVarBoundOrConstraints, TypeVarInstance, UnionType, WrapperDescriptorKind, -}; -use crate::Db; -use rustc_hash::FxHashMap; - -impl<'db> Type<'db> { - pub fn display(&self, db: &'db dyn Db) -> DisplayType { - DisplayType { ty: self, db } - } - fn representation(self, db: &'db dyn Db) -> DisplayRepresentation<'db> { - DisplayRepresentation { db, ty: self } - } -} - -#[derive(Copy, Clone)] -pub struct DisplayType<'db> { - ty: &'db Type<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let representation = self.ty.representation(self.db); - match self.ty { - Type::ClassLiteral(literal) if literal.is_known(self.db, KnownClass::Any) => { - write!(f, "typing.Any") - } - Type::IntLiteral(_) - | Type::BooleanLiteral(_) - | Type::StringLiteral(_) - | Type::BytesLiteral(_) - | Type::ClassLiteral(_) - | Type::GenericAlias(_) => { - write!(f, "Literal[{representation}]") - } - _ => representation.fmt(f), - } - } -} - -impl fmt::Debug for DisplayType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -/// Writes the string representation of a type, which is the value displayed either as -/// `Literal[]` or `Literal[, ]` for literal types or as `` for -/// non literals -struct DisplayRepresentation<'db> { - ty: Type<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayRepresentation<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self.ty { - Type::Dynamic(dynamic) => dynamic.fmt(f), - Type::Never => f.write_str("Never"), - Type::Instance(InstanceType { class }) => match (class, class.known(self.db)) { - (_, Some(KnownClass::NoneType)) => f.write_str("None"), - (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - (ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name), - (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), - }, - Type::PropertyInstance(_) => f.write_str("property"), - Type::ModuleLiteral(module) => { - write!(f, "", module.module(self.db).name()) - } - // TODO functions and classes should display using a fully qualified name - Type::ClassLiteral(class) => f.write_str(class.name(self.db)), - Type::GenericAlias(generic) => { - write!(f, "{}", generic.display(self.db)) - } - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - // Only show the bare class name here; ClassBase::display would render this as - // type[] instead of type[Foo]. - ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)), - ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), - }, - Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), - Type::FunctionLiteral(function) => { - let signature = function.signature(self.db); - // TODO: when generic function types are supported, we should add - // the generic type parameters to the signature, i.e. - // show `def foo[T](x: T) -> T`. - - write!( - f, - // "def {name}{specialization}{signature}", - "def {name}{signature}", - name = function.name(self.db), - signature = signature.display(self.db) - ) - } - Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f), - Type::BoundMethod(bound_method) => { - let function = bound_method.function(self.db); - - // TODO: use the specialization from the method. Similar to the comment above - // about the function specialization, - - write!( - f, - "bound method {instance}.{method}{signature}", - method = function.name(self.db), - instance = bound_method.self_instance(self.db).display(self.db), - signature = function.signature(self.db).bind_self().display(self.db) - ) - } - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { - write!( - f, - "", - function = function.name(self.db), - specialization = if let Some(specialization) = function.specialization(self.db) - { - specialization.display_short(self.db).to_string() - } else { - String::new() - }, - ) - } - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => { - write!( - f, - "", - function = function.name(self.db), - specialization = if let Some(specialization) = function.specialization(self.db) - { - specialization.display_short(self.db).to_string() - } else { - String::new() - }, - ) - } - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { - write!(f, "",) - } - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => { - write!(f, "",) - } - Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => { - write!(f, "",) - } - Type::WrapperDescriptor(kind) => { - let (method, object) = match kind { - WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), - WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"), - WrapperDescriptorKind::PropertyDunderSet => ("__set__", "property"), - }; - write!(f, "") - } - Type::DataclassDecorator(_) => { - f.write_str("") - } - Type::Union(union) => union.display(self.db).fmt(f), - Type::Intersection(intersection) => intersection.display(self.db).fmt(f), - Type::IntLiteral(n) => n.fmt(f), - Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }), - Type::StringLiteral(string) => string.display(self.db).fmt(f), - Type::LiteralString => f.write_str("LiteralString"), - Type::BytesLiteral(bytes) => { - let escape = - AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double); - - escape.bytes_repr(TripleQuotes::No).write(f) - } - Type::SliceLiteral(slice) => { - f.write_str("slice[")?; - if let Some(start) = slice.start(self.db) { - write!(f, "Literal[{start}]")?; - } else { - f.write_str("None")?; - } - - f.write_str(", ")?; - - if let Some(stop) = slice.stop(self.db) { - write!(f, "Literal[{stop}]")?; - } else { - f.write_str("None")?; - } - - if let Some(step) = slice.step(self.db) { - write!(f, ", Literal[{step}]")?; - } - - f.write_str("]") - } - Type::Tuple(tuple) => { - f.write_str("tuple[")?; - let elements = tuple.elements(self.db); - if elements.is_empty() { - f.write_str("()")?; - } else { - elements.display(self.db).fmt(f)?; - } - f.write_str("]") - } - Type::TypeVar(typevar) => { - write!(f, "{}", typevar.name(self.db)) - } - Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), - Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), - Type::BoundSuper(bound_super) => { - write!( - f, - "", - pivot = Type::from(bound_super.pivot_class(self.db)).display(self.db), - owner = bound_super.owner(self.db).into_type().display(self.db) - ) - } - } - } -} - -impl<'db> GenericAlias<'db> { - pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { - DisplayGenericAlias { - origin: self.origin(db), - specialization: self.specialization(db), - db, - } - } -} - -pub(crate) struct DisplayGenericAlias<'db> { - origin: GenericClass<'db>, - specialization: Specialization<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayGenericAlias<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "{origin}{specialization}", - origin = self.origin.class(self.db).name, - specialization = self.specialization.display_short(self.db), - ) - } -} - -impl<'db> GenericContext<'db> { - pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> { - DisplayGenericContext { - typevars: self.variables(db), - db, - } - } -} - -pub struct DisplayGenericContext<'db> { - typevars: &'db [TypeVarInstance<'db>], - db: &'db dyn Db, -} - -impl Display for DisplayGenericContext<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_char('[')?; - for (idx, var) in self.typevars.iter().enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - write!(f, "{}", var.name(self.db))?; - match var.bound_or_constraints(self.db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - write!(f, ": {}", bound.display(self.db))?; - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - f.write_str(": (")?; - for (idx, constraint) in constraints.iter(self.db).enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - write!(f, "{}", constraint.display(self.db))?; - } - f.write_char(')')?; - } - None => {} - } - if let Some(default_type) = var.default_ty(self.db) { - write!(f, " = {}", default_type.display(self.db))?; - } - } - f.write_char(']') - } -} - -impl<'db> Specialization<'db> { - /// Renders the specialization in full, e.g. `{T = int, U = str}`. - pub fn display(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { - DisplaySpecialization { - typevars: self.generic_context(db).variables(db), - types: self.types(db), - db, - full: true, - } - } - - /// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`. - pub fn display_short(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { - DisplaySpecialization { - typevars: self.generic_context(db).variables(db), - types: self.types(db), - db, - full: false, - } - } -} - -pub struct DisplaySpecialization<'db> { - typevars: &'db [TypeVarInstance<'db>], - types: &'db [Type<'db>], - db: &'db dyn Db, - full: bool, -} - -impl Display for DisplaySpecialization<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if self.full { - f.write_char('{')?; - for (idx, (var, ty)) in self.typevars.iter().zip(self.types).enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - write!(f, "{} = {}", var.name(self.db), ty.display(self.db))?; - } - f.write_char('}') - } else { - f.write_char('[')?; - for (idx, (_, ty)) in self.typevars.iter().zip(self.types).enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - write!(f, "{}", ty.display(self.db))?; - } - f.write_char(']') - } - } -} - -impl<'db> Signature<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> { - DisplaySignature { - parameters: self.parameters(), - return_ty: self.return_ty, - db, - } - } -} - -struct DisplaySignature<'db> { - parameters: &'db Parameters<'db>, - return_ty: Option>, - db: &'db dyn Db, -} - -impl Display for DisplaySignature<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_char('(')?; - - if self.parameters.is_gradual() { - // We represent gradual form as `...` in the signature, internally the parameters still - // contain `(*args, **kwargs)` parameters. - f.write_str("...")?; - } else { - let mut star_added = false; - let mut needs_slash = false; - let mut join = f.join(", "); - - for parameter in self.parameters.as_slice() { - if !star_added && parameter.is_keyword_only() { - join.entry(&'*'); - star_added = true; - } - if parameter.is_positional_only() { - needs_slash = true; - } else if needs_slash { - join.entry(&'/'); - needs_slash = false; - } - join.entry(¶meter.display(self.db)); - } - if needs_slash { - join.entry(&'/'); - } - join.finish()?; - } - - write!( - f, - ") -> {}", - self.return_ty.unwrap_or(Type::unknown()).display(self.db) - )?; - - Ok(()) - } -} - -impl<'db> Parameter<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> { - DisplayParameter { param: self, db } - } -} - -struct DisplayParameter<'db> { - param: &'db Parameter<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayParameter<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Some(name) = self.param.display_name() { - write!(f, "{name}")?; - if let Some(annotated_type) = self.param.annotated_type() { - write!(f, ": {}", annotated_type.display(self.db))?; - } - // Default value can only be specified if `name` is given. - if let Some(default_ty) = self.param.default_type() { - if self.param.annotated_type().is_some() { - write!(f, " = {}", default_ty.display(self.db))?; - } else { - write!(f, "={}", default_ty.display(self.db))?; - } - } - } else if let Some(ty) = self.param.annotated_type() { - // This case is specifically for the `Callable` signature where name and default value - // cannot be provided. - ty.display(self.db).fmt(f)?; - } - Ok(()) - } -} - -impl<'db> UnionType<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> { - DisplayUnionType { db, ty: self } - } -} - -struct DisplayUnionType<'db> { - ty: &'db UnionType<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayUnionType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let elements = self.ty.elements(self.db); - - // Group condensed-display types by kind. - let mut grouped_condensed_kinds = FxHashMap::default(); - - for element in elements { - if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) { - grouped_condensed_kinds - .entry(kind) - .or_insert_with(Vec::new) - .push(*element); - } - } - - let mut join = f.join(" | "); - - for element in elements { - if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) { - let Some(condensed_kind) = grouped_condensed_kinds.remove(&kind) else { - continue; - }; - join.entry(&DisplayLiteralGroup { - literals: condensed_kind, - db: self.db, - }); - } else { - join.entry(&DisplayMaybeParenthesizedType { - ty: *element, - db: self.db, - }); - } - } - - join.finish()?; - - debug_assert!(grouped_condensed_kinds.is_empty()); - - Ok(()) - } -} - -impl fmt::Debug for DisplayUnionType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -struct DisplayLiteralGroup<'db> { - literals: Vec>, - db: &'db dyn Db, -} - -impl Display for DisplayLiteralGroup<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("Literal[")?; - f.join(", ") - .entries(self.literals.iter().map(|ty| ty.representation(self.db))) - .finish()?; - f.write_str("]") - } -} - -/// Enumeration of literal types that are displayed in a "condensed way" inside `Literal` slices. -/// -/// For example, `Literal[1] | Literal[2] | Literal["s"]` is displayed as `"Literal[1, 2, "s"]"`. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -enum CondensedDisplayTypeKind { - Class, - LiteralExpression, -} - -impl TryFrom> for CondensedDisplayTypeKind { - type Error = (); - - fn try_from(value: Type<'_>) -> Result { - match value { - Type::ClassLiteral(_) => Ok(Self::Class), - Type::IntLiteral(_) - | Type::StringLiteral(_) - | Type::BytesLiteral(_) - | Type::BooleanLiteral(_) => Ok(Self::LiteralExpression), - _ => Err(()), - } - } -} - -impl<'db> IntersectionType<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> { - DisplayIntersectionType { db, ty: self } - } -} - -struct DisplayIntersectionType<'db> { - ty: &'db IntersectionType<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayIntersectionType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let tys = self - .ty - .positive(self.db) - .iter() - .map(|&ty| DisplayMaybeNegatedType { - ty, - db: self.db, - negated: false, - }) - .chain( - self.ty - .negative(self.db) - .iter() - .map(|&ty| DisplayMaybeNegatedType { - ty, - db: self.db, - negated: true, - }), - ); - f.join(" & ").entries(tys).finish() - } -} - -impl fmt::Debug for DisplayIntersectionType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -struct DisplayMaybeNegatedType<'db> { - ty: Type<'db>, - db: &'db dyn Db, - negated: bool, -} - -impl Display for DisplayMaybeNegatedType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if self.negated { - f.write_str("~")?; - } - 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(_) - | Type::MethodWrapper(_) - | Type::FunctionLiteral(_) - | Type::BoundMethod(_) = self.ty - { - write!(f, "({})", self.ty.display(self.db)) - } else { - self.ty.display(self.db).fmt(f) - } - } -} - -pub(crate) trait TypeArrayDisplay<'db> { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray; -} - -impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray { - DisplayTypeArray { types: self, db } - } -} - -impl<'db> TypeArrayDisplay<'db> for Vec> { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray { - DisplayTypeArray { types: self, db } - } -} - -impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { - fn display(&self, db: &'db dyn Db) -> DisplayTypeArray { - DisplayTypeArray { types: self, db } - } -} - -pub(crate) struct DisplayTypeArray<'b, 'db> { - types: &'b [Type<'db>], - db: &'db dyn Db, -} - -impl Display for DisplayTypeArray<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.join(", ") - .entries(self.types.iter().map(|ty| ty.display(self.db))) - .finish() - } -} - -impl<'db> StringLiteralType<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> { - DisplayStringLiteralType { db, ty: self } - } -} - -struct DisplayStringLiteralType<'db> { - ty: &'db StringLiteralType<'db>, - db: &'db dyn Db, -} - -impl Display for DisplayStringLiteralType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let value = self.ty.value(self.db); - f.write_char('"')?; - for ch in value.chars() { - match ch { - // `escape_debug` will escape even single quotes, which is not necessary for our - // use case as we are already using double quotes to wrap the string. - '\'' => f.write_char('\'')?, - _ => write!(f, "{}", ch.escape_debug())?, - } - } - f.write_char('"') - } -} - -#[cfg(test)] -mod tests { - use ruff_python_ast::name::Name; - - use crate::db::tests::setup_db; - use crate::types::{ - KnownClass, Parameter, Parameters, Signature, SliceLiteralType, StringLiteralType, Type, - }; - use crate::Db; - - #[test] - fn test_slice_literal_display() { - let db = setup_db(); - - assert_eq!( - Type::SliceLiteral(SliceLiteralType::new(&db, None, None, None)) - .display(&db) - .to_string(), - "slice[None, None]" - ); - assert_eq!( - Type::SliceLiteral(SliceLiteralType::new(&db, Some(1), None, None)) - .display(&db) - .to_string(), - "slice[Literal[1], None]" - ); - assert_eq!( - Type::SliceLiteral(SliceLiteralType::new(&db, None, Some(2), None)) - .display(&db) - .to_string(), - "slice[None, Literal[2]]" - ); - assert_eq!( - Type::SliceLiteral(SliceLiteralType::new(&db, Some(1), Some(5), None)) - .display(&db) - .to_string(), - "slice[Literal[1], Literal[5]]" - ); - assert_eq!( - Type::SliceLiteral(SliceLiteralType::new(&db, Some(1), Some(5), Some(2))) - .display(&db) - .to_string(), - "slice[Literal[1], Literal[5], Literal[2]]" - ); - assert_eq!( - Type::SliceLiteral(SliceLiteralType::new(&db, None, None, Some(2))) - .display(&db) - .to_string(), - "slice[None, None, Literal[2]]" - ); - } - - #[test] - fn string_literal_display() { - let db = setup_db(); - - assert_eq!( - Type::StringLiteral(StringLiteralType::new(&db, r"\n")) - .display(&db) - .to_string(), - r#"Literal["\\n"]"# - ); - assert_eq!( - Type::StringLiteral(StringLiteralType::new(&db, "'")) - .display(&db) - .to_string(), - r#"Literal["'"]"# - ); - assert_eq!( - Type::StringLiteral(StringLiteralType::new(&db, r#"""#)) - .display(&db) - .to_string(), - r#"Literal["\""]"# - ); - } - - fn display_signature<'db>( - db: &dyn Db, - parameters: impl IntoIterator>, - return_ty: Option>, - ) -> String { - Signature::new(Parameters::new(parameters), return_ty) - .display(db) - .to_string() - } - - #[test] - fn signature_display() { - let db = setup_db(); - - // Empty parameters with no return type. - assert_eq!(display_signature(&db, [], None), "() -> Unknown"); - - // Empty parameters with a return type. - assert_eq!( - display_signature(&db, [], Some(Type::none(&db))), - "() -> None" - ); - - // Single parameter type (no name) with a return type. - assert_eq!( - display_signature( - &db, - [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], - Some(Type::none(&db)) - ), - "(None, /) -> None" - ); - - // Two parameters where one has annotation and the other doesn't. - assert_eq!( - display_signature( - &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)) - ), - "(x=int, y: str = str) -> None" - ); - - // All positional only parameters. - assert_eq!( - display_signature( - &db, - [ - Parameter::positional_only(Some(Name::new_static("x"))), - Parameter::positional_only(Some(Name::new_static("y"))), - ], - Some(Type::none(&db)) - ), - "(x, y, /) -> None" - ); - - // Positional-only parameters mixed with non-positional-only parameters. - assert_eq!( - display_signature( - &db, - [ - Parameter::positional_only(Some(Name::new_static("x"))), - Parameter::positional_or_keyword(Name::new_static("y")), - ], - Some(Type::none(&db)) - ), - "(x, /, y) -> None" - ); - - // All keyword-only parameters. - assert_eq!( - display_signature( - &db, - [ - Parameter::keyword_only(Name::new_static("x")), - Parameter::keyword_only(Name::new_static("y")), - ], - Some(Type::none(&db)) - ), - "(*, x, y) -> None" - ); - - // Keyword-only parameters mixed with non-keyword-only parameters. - assert_eq!( - display_signature( - &db, - [ - Parameter::positional_or_keyword(Name::new_static("x")), - Parameter::keyword_only(Name::new_static("y")), - ], - Some(Type::none(&db)) - ), - "(x, *, y) -> None" - ); - - // A mix of all parameter kinds. - assert_eq!( - display_signature( - &db, - [ - 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)) - ), - "(a, b: int, c=Literal[1], d: int = Literal[2], \ - /, e=Literal[3], f: int = Literal[4], *args: object, \ - *, g=Literal[5], h: int = Literal[6], **kwargs: str) -> bytes" - ); - } -} diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs deleted file mode 100644 index 3e350112f9abf..0000000000000 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ /dev/null @@ -1,295 +0,0 @@ -use ruff_python_ast as ast; -use rustc_hash::FxHashMap; - -use crate::semantic_index::SemanticIndex; -use crate::types::signatures::{Parameter, Parameters, Signature}; -use crate::types::{ - declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance, - UnionBuilder, UnionType, -}; -use crate::Db; - -/// A list of formal type variables for a generic function, class, or type alias. -/// -/// TODO: Handle nested generic contexts better, with actual parent links to the lexically -/// containing context. -#[salsa::interned(debug)] -pub struct GenericContext<'db> { - #[return_ref] - pub(crate) variables: Box<[TypeVarInstance<'db>]>, -} - -impl<'db> GenericContext<'db> { - pub(crate) fn from_type_params( - db: &'db dyn Db, - index: &'db SemanticIndex<'db>, - type_params_node: &ast::TypeParams, - ) -> Self { - let variables: Box<[_]> = type_params_node - .iter() - .filter_map(|type_param| Self::variable_from_type_param(db, index, type_param)) - .collect(); - Self::new(db, variables) - } - - fn variable_from_type_param( - db: &'db dyn Db, - index: &'db SemanticIndex<'db>, - type_param_node: &ast::TypeParam, - ) -> Option> { - match type_param_node { - ast::TypeParam::TypeVar(node) => { - let definition = index.expect_single_definition(node); - let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = - declaration_type(db, definition).inner_type() - else { - panic!("typevar should be inferred as a TypeVarInstance"); - }; - Some(typevar) - } - // TODO: Support these! - ast::TypeParam::ParamSpec(_) => None, - ast::TypeParam::TypeVarTuple(_) => None, - } - } - - pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { - let parameters = Parameters::new( - self.variables(db) - .iter() - .map(|typevar| Self::parameter_from_typevar(db, *typevar)), - ); - Signature::new(parameters, None) - } - - fn parameter_from_typevar(db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Parameter<'db> { - let mut parameter = Parameter::positional_only(Some(typevar.name(db).clone())); - match typevar.bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - // TODO: This should be a type form. - parameter = parameter.with_annotated_type(bound); - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - // TODO: This should be a new type variant where only these exact types are - // assignable, and not subclasses of them, nor a union of them. - parameter = parameter - .with_annotated_type(UnionType::from_elements(db, constraints.iter(db))); - } - None => {} - } - parameter - } - - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> Specialization<'db> { - let types = self - .variables(db) - .iter() - .map(|typevar| typevar.default_ty(db).unwrap_or(Type::unknown())) - .collect(); - self.specialize(db, types) - } - - pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { - let types = self - .variables(db) - .iter() - .map(|typevar| Type::TypeVar(*typevar)) - .collect(); - self.specialize(db, types) - } - - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { - let types = vec![Type::unknown(); self.variables(db).len()]; - self.specialize(db, types.into()) - } - - pub(crate) fn specialize( - self, - db: &'db dyn Db, - types: Box<[Type<'db>]>, - ) -> Specialization<'db> { - Specialization::new(db, self, types) - } -} - -/// An assignment of a specific type to each type variable in a generic scope. -/// -/// TODO: Handle nested specializations better, with actual parent links to the specialization of -/// the lexically containing context. -#[salsa::interned(debug)] -pub struct Specialization<'db> { - pub(crate) generic_context: GenericContext<'db>, - #[return_ref] - pub(crate) types: Box<[Type<'db>]>, -} - -impl<'db> Specialization<'db> { - /// Applies a specialization to this specialization. This is used, for instance, when a generic - /// class inherits from a generic alias: - /// - /// ```py - /// class A[T]: ... - /// class B[U](A[U]): ... - /// ``` - /// - /// `B` is a generic class, whose MRO includes the generic alias `A[U]`, which specializes `A` - /// with the specialization `{T: U}`. If `B` is specialized to `B[int]`, with specialization - /// `{U: int}`, we can apply the second specialization to the first, resulting in `T: int`. - /// That lets us produce the generic alias `A[int]`, which is the corresponding entry in the - /// MRO of `B[int]`. - pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self { - let types: Box<[_]> = self - .types(db) - .into_iter() - .map(|ty| ty.apply_specialization(db, other)) - .collect(); - Specialization::new(db, self.generic_context(db), types) - } - - /// Combines two specializations of the same generic context. If either specialization maps a - /// typevar to `Type::Unknown`, the other specialization's mapping is used. If both map the - /// typevar to a known type, those types are unioned together. - /// - /// Panics if the two specializations are not for the same generic context. - pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { - let generic_context = self.generic_context(db); - assert!(other.generic_context(db) == generic_context); - let types: Box<[_]> = self - .types(db) - .into_iter() - .zip(other.types(db)) - .map(|(self_type, other_type)| match (self_type, other_type) { - (unknown, known) | (known, unknown) if unknown.is_unknown() => *known, - _ => UnionType::from_elements(db, [self_type, other_type]), - }) - .collect(); - Specialization::new(db, self.generic_context(db), types) - } - - pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { - let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect(); - Self::new(db, self.generic_context(db), types) - } - - /// Returns the type that a typevar is specialized to, or None if the typevar isn't part of - /// this specialization. - pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option> { - self.generic_context(db) - .variables(db) - .into_iter() - .zip(self.types(db)) - .find(|(var, _)| **var == typevar) - .map(|(_, ty)| *ty) - } -} - -/// Performs type inference between parameter annotations and argument types, producing a -/// specialization of a generic function. -pub(crate) struct SpecializationBuilder<'db> { - db: &'db dyn Db, - generic_context: GenericContext<'db>, - types: FxHashMap, UnionBuilder<'db>>, -} - -impl<'db> SpecializationBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { - Self { - db, - generic_context, - types: FxHashMap::default(), - } - } - - pub(crate) fn build(mut self) -> Specialization<'db> { - let types: Box<[_]> = self - .generic_context - .variables(self.db) - .iter() - .map(|variable| { - self.types - .remove(variable) - .map(UnionBuilder::build) - .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) - }) - .collect(); - Specialization::new(self.db, self.generic_context, types) - } - - fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) { - let builder = self - .types - .entry(typevar) - .or_insert_with(|| UnionBuilder::new(self.db)); - builder.add_in_place(ty); - } - - pub(crate) fn infer(&mut self, formal: Type<'db>, actual: Type<'db>) { - // If the actual type is already assignable to the formal type, then return without adding - // any new type mappings. (Note that if the formal type contains any typevars, this check - // will fail, since no non-typevar types are assignable to a typevar.) - // - // In particular, this handles a case like - // - // ```py - // def f[T](t: T | None): ... - // - // f(None) - // ``` - // - // without specializing `T` to `None`. - if actual.is_assignable_to(self.db, formal) { - return; - } - - match (formal, actual) { - (Type::TypeVar(typevar), _) => self.add_type_mapping(typevar, actual), - - (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { - let formal_elements = formal_tuple.elements(self.db); - let actual_elements = actual_tuple.elements(self.db); - if formal_elements.len() == actual_elements.len() { - for (formal_element, actual_element) in - formal_elements.iter().zip(actual_elements) - { - self.infer(*formal_element, *actual_element); - } - } - } - - (Type::Union(formal), _) => { - // TODO: We haven't implemented a full unification solver yet. If typevars appear - // in multiple union elements, we ideally want to express that _only one_ of them - // needs to match, and that we should infer the smallest type mapping that allows - // that. - // - // For now, we punt on handling multiple typevar elements. Instead, if _precisely - // one_ union element _is_ a typevar (not _contains_ a typevar), then we go ahead - // and add a mapping between that typevar and the actual type. (Note that we've - // already handled above the case where the actual is assignable to a _non-typevar_ - // union element.) - let mut typevars = formal.iter(self.db).filter_map(|ty| match ty { - Type::TypeVar(typevar) => Some(*typevar), - _ => None, - }); - let typevar = typevars.next(); - let additional_typevars = typevars.next(); - if let (Some(typevar), None) = (typevar, additional_typevars) { - self.add_type_mapping(typevar, actual); - } - } - - (Type::Intersection(formal), _) => { - // The actual type must be assignable to every (positive) element of the - // formal intersection, so we must infer type mappings for each of them. (The - // actual type must also be disjoint from every negative element of the - // intersection, but that doesn't help us infer any type mappings.) - for positive in formal.iter_positive(self.db) { - self.infer(positive, actual); - } - } - - // TODO: Add more forms that we can structurally induct into: type[C], callables - _ => {} - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs deleted file mode 100644 index 59248f9f7bc2c..0000000000000 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ /dev/null @@ -1,8439 +0,0 @@ -//! We have Salsa queries for inferring types at three different granularities: scope-level, -//! definition-level, and expression-level. -//! -//! Scope-level inference is for when we are actually checking a file, and need to check types for -//! everything in that file's scopes, or give a linter access to types of arbitrary expressions -//! (via the [`HasType`](crate::semantic_model::HasType) trait). -//! -//! Definition-level inference allows us to look up the types of symbols in other scopes (e.g. for -//! imports) with the minimum inference necessary, so that if we're looking up one symbol from a -//! very large module, we can avoid a bunch of unnecessary work. Definition-level inference also -//! allows us to handle import cycles without getting into a cycle of scope-level inference -//! queries. -//! -//! The expression-level inference query is needed in only a few cases. Since some assignments can -//! have multiple targets (via `x = y = z` or unpacking `(x, y) = z`, they can be associated with -//! multiple definitions (one per assigned symbol). In order to avoid inferring the type of the -//! right-hand side once per definition, we infer it as a standalone query, so its result will be -//! cached by Salsa. We also need the expression-level query for inferring types in type guard -//! expressions (e.g. the test clause of an `if` statement.) -//! -//! Inferring types at any of the three region granularities returns a [`TypeInference`], which -//! holds types for every [`Definition`] and expression within the inferred region. -//! -//! Some type expressions can require deferred evaluation. This includes all type expressions in -//! stub files, or annotation expressions in modules with `from __future__ import annotations`, or -//! stringified annotations. We have a fourth Salsa query for inferring the deferred types -//! associated with a particular definition. Scope-level inference infers deferred types for all -//! definitions once the rest of the types in the scope have been inferred. -//! -//! Many of our type inference Salsa queries implement cycle recovery via fixed-point iteration. In -//! general, they initiate fixed-point iteration by returning a `TypeInference` that returns -//! `Type::Never` for all expressions, bindings, and declarations, and then they continue iterating -//! 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 itertools::{Either, Itertools}; -use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity}; -use ruff_db::files::File; -use ruff_db::parsed::parsed_module; -use ruff_python_ast::visitor::{walk_expr, Visitor}; -use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext}; -use ruff_text_size::{Ranged, TextRange}; -use rustc_hash::{FxHashMap, FxHashSet}; -use salsa; -use salsa::plumbing::AsId; - -use crate::module_name::{ModuleName, ModuleNameResolutionError}; -use crate::module_resolver::resolve_module; -use crate::node_key::NodeKey; -use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId}; -use crate::semantic_index::definition::{ - AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, Definition, DefinitionKind, - DefinitionNodeKey, ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, - WithItemDefinitionKind, -}; -use crate::semantic_index::expression::{Expression, ExpressionKind}; -use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind, -}; -use crate::semantic_index::{semantic_index, EagerBindingsResult, 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, Bindings, CallArgumentTypes, CallArguments, CallError}; -use crate::types::class::MetaclassErrorKind; -use crate::types::diagnostic::{ - report_implicit_return_type, report_invalid_arguments_to_annotated, - report_invalid_arguments_to_callable, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_return_type, - report_possibly_unbound_attribute, TypeCheckDiagnostics, CALL_NON_CALLABLE, - CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, - CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, - INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, - INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS, - POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, - UNSUPPORTED_OPERATOR, -}; -use crate::types::generics::GenericContext; -use crate::types::mro::MroErrorKind; -use crate::types::unpacker::{UnpackResult, Unpacker}; -use crate::types::{ - todo_type, CallDunderError, CallableSignature, CallableType, Class, ClassLiteralType, - ClassType, DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, GenericAlias, - GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, - KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter, - ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType, - SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, - TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarInstance, UnionBuilder, UnionType, -}; -use crate::unpack::{Unpack, UnpackPosition}; -use crate::util::subscript::{PyIndex, PySlice}; -use crate::Db; - -use super::class_base::ClassBase; -use super::context::{InNoTypeCheck, InferContext}; -use super::diagnostic::{ - report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_type_checking_constant, - report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero, - report_unresolved_reference, INVALID_METACLASS, REDUNDANT_CAST, STATIC_ASSERT_ERROR, - SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, -}; -use super::slots::check_class_slots; -use super::string_annotation::{ - parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, -}; -use super::{BoundSuperError, BoundSuperType}; - -/// 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 -/// scope. -#[salsa::tracked(return_ref, cycle_fn=scope_cycle_recover, cycle_initial=scope_cycle_initial)] -pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> TypeInference<'db> { - let file = scope.file(db); - let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?file).entered(); - - // Using the index here is fine because the code below depends on the AST anyway. - // The isolation of the query is by the return inferred types. - let index = semantic_index(db, file); - - TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope), index).finish() -} - -fn scope_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &TypeInference<'db>, - _count: u32, - _scope: ScopeId<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> TypeInference<'db> { - TypeInference::cycle_fallback(scope, Type::Never) -} - -/// Infer all types for a [`Definition`] (including sub-expressions). -/// Use when resolving a symbol name use or public type of a symbol. -#[salsa::tracked(return_ref, cycle_fn=definition_cycle_recover, cycle_initial=definition_cycle_initial)] -pub(crate) fn infer_definition_types<'db>( - db: &'db dyn Db, - definition: Definition<'db>, -) -> TypeInference<'db> { - let file = definition.file(db); - let _span = tracing::trace_span!( - "infer_definition_types", - range = ?definition.kind(db).target_range(), - ?file - ) - .entered(); - - let index = semantic_index(db, file); - - TypeInferenceBuilder::new(db, InferenceRegion::Definition(definition), index).finish() -} - -fn definition_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &TypeInference<'db>, - _count: u32, - _definition: Definition<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn definition_cycle_initial<'db>( - db: &'db dyn Db, - definition: Definition<'db>, -) -> TypeInference<'db> { - TypeInference::cycle_fallback(definition.scope(db), Type::Never) -} - -/// Infer types for all deferred type expressions in a [`Definition`]. -/// -/// Deferred expressions are type expressions (annotations, base classes, aliases...) in a stub -/// file, or in a file with `from __future__ import annotations`, or stringified annotations. -#[salsa::tracked(return_ref, cycle_fn=deferred_cycle_recover, cycle_initial=deferred_cycle_initial)] -pub(crate) fn infer_deferred_types<'db>( - db: &'db dyn Db, - definition: Definition<'db>, -) -> TypeInference<'db> { - let file = definition.file(db); - let _span = tracing::trace_span!( - "infer_deferred_types", - definition = ?definition.as_id(), - range = ?definition.kind(db).target_range(), - ?file - ) - .entered(); - - let index = semantic_index(db, file); - - TypeInferenceBuilder::new(db, InferenceRegion::Deferred(definition), index).finish() -} - -fn deferred_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &TypeInference<'db>, - _count: u32, - _definition: Definition<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn deferred_cycle_initial<'db>(db: &'db dyn Db, definition: Definition<'db>) -> TypeInference<'db> { - TypeInference::cycle_fallback(definition.scope(db), Type::Never) -} - -/// Infer all types for an [`Expression`] (including sub-expressions). -/// Use rarely; only for cases where we'd otherwise risk double-inferring an expression: RHS of an -/// assignment, which might be unpacking/multi-target and thus part of multiple definitions, or a -/// type narrowing guard expression (e.g. if statement test node). -#[salsa::tracked(return_ref, cycle_fn=expression_cycle_recover, cycle_initial=expression_cycle_initial)] -pub(crate) fn infer_expression_types<'db>( - db: &'db dyn Db, - expression: Expression<'db>, -) -> TypeInference<'db> { - let file = expression.file(db); - let _span = tracing::trace_span!( - "infer_expression_types", - expression = ?expression.as_id(), - range = ?expression.node_ref(db).range(), - ?file - ) - .entered(); - - let index = semantic_index(db, file); - - TypeInferenceBuilder::new(db, InferenceRegion::Expression(expression), index).finish() -} - -fn expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &TypeInference<'db>, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn expression_cycle_initial<'db>( - db: &'db dyn Db, - expression: Expression<'db>, -) -> TypeInference<'db> { - TypeInference::cycle_fallback(expression.scope(db), Type::Never) -} - -/// Infers the type of an `expression` that is guaranteed to be in the same file as the calling query. -/// -/// This is a small helper around [`infer_expression_types()`] to reduce the boilerplate. -/// Use [`infer_expression_type()`] if it isn't guaranteed that `expression` is in the same file to -/// avoid cross-file query dependencies. -pub(super) fn infer_same_file_expression_type<'db>( - db: &'db dyn Db, - expression: Expression<'db>, -) -> Type<'db> { - let inference = infer_expression_types(db, expression); - let scope = expression.scope(db); - inference.expression_type(expression.node_ref(db).scoped_expression_id(db, scope)) -} - -/// Infers the type of an expression where the expression might come from another file. -/// -/// Use this over [`infer_expression_types`] if the expression might come from another file than the -/// enclosing query to avoid cross-file query dependencies. -/// -/// Use [`infer_same_file_expression_type`] if it is guaranteed that `expression` is in the same -/// to avoid unnecessary salsa ingredients. This is normally the case inside the `TypeInferenceBuilder`. -#[salsa::tracked(cycle_fn=single_expression_cycle_recover, cycle_initial=single_expression_cycle_initial)] -pub(crate) fn infer_expression_type<'db>( - db: &'db dyn Db, - expression: Expression<'db>, -) -> Type<'db> { - // It's okay to call the "same file" version here because we're inside a salsa query. - infer_same_file_expression_type(db, expression) -} - -fn single_expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Type<'db>, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate -} - -fn single_expression_cycle_initial<'db>( - _db: &'db dyn Db, - _expression: Expression<'db>, -) -> Type<'db> { - Type::Never -} - -/// Infer the types for an [`Unpack`] operation. -/// -/// This infers the expression type and performs structural match against the target expression -/// involved in an unpacking operation. It returns a result-like object that can be used to get the -/// type of the variables involved in this unpacking along with any violations that are detected -/// during this unpacking. -#[salsa::tracked(return_ref)] -pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> { - let file = unpack.file(db); - let _span = - tracing::trace_span!("infer_unpack_types", range=?unpack.range(db), ?file).entered(); - - let mut unpacker = Unpacker::new(db, unpack.scope(db)); - unpacker.unpack(unpack.target(db), unpack.value(db)); - unpacker.finish() -} - -/// A region within which we can infer types. -#[derive(Copy, Clone, Debug)] -pub(crate) enum InferenceRegion<'db> { - /// infer types for a standalone [`Expression`] - Expression(Expression<'db>), - /// infer types for a [`Definition`] - Definition(Definition<'db>), - /// infer deferred types for a [`Definition`] - Deferred(Definition<'db>), - /// infer types for an entire [`ScopeId`] - Scope(ScopeId<'db>), -} - -impl<'db> InferenceRegion<'db> { - fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - match self { - InferenceRegion::Expression(expression) => expression.scope(db), - InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => { - definition.scope(db) - } - InferenceRegion::Scope(scope) => scope, - } - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -struct TypeAndRange<'db> { - ty: Type<'db>, - range: TextRange, -} - -/// The inferred types for a single region. -#[derive(Debug, Eq, PartialEq, salsa::Update)] -pub(crate) struct TypeInference<'db> { - /// The types of every expression in this region. - expressions: FxHashMap>, - - /// The types of every binding in this region. - bindings: FxHashMap, Type<'db>>, - - /// The types and type qualifiers of every declaration in this region. - declarations: FxHashMap, TypeAndQualifiers<'db>>, - - /// The definitions that are deferred. - deferred: FxHashSet>, - - /// The diagnostics for this region. - diagnostics: TypeCheckDiagnostics, - - /// The scope this region is part of. - scope: ScopeId<'db>, - - /// The fallback type for missing expressions/bindings/declarations. - /// - /// This is used only when constructing a cycle-recovery `TypeInference`. - cycle_fallback_type: Option>, -} - -impl<'db> TypeInference<'db> { - pub(crate) fn empty(scope: ScopeId<'db>) -> Self { - Self { - expressions: FxHashMap::default(), - bindings: FxHashMap::default(), - declarations: FxHashMap::default(), - deferred: FxHashSet::default(), - diagnostics: TypeCheckDiagnostics::default(), - scope, - cycle_fallback_type: None, - } - } - - fn cycle_fallback(scope: ScopeId<'db>, cycle_fallback_type: Type<'db>) -> Self { - Self { - expressions: FxHashMap::default(), - bindings: FxHashMap::default(), - declarations: FxHashMap::default(), - deferred: FxHashSet::default(), - diagnostics: TypeCheckDiagnostics::default(), - scope, - cycle_fallback_type: Some(cycle_fallback_type), - } - } - - #[track_caller] - pub(crate) fn expression_type(&self, expression: ScopedExpressionId) -> Type<'db> { - self.try_expression_type(expression).expect( - "expression should belong to this TypeInference region and \ - TypeInferenceBuilder should have inferred a type for it", - ) - } - - pub(crate) fn try_expression_type(&self, expression: ScopedExpressionId) -> Option> { - self.expressions - .get(&expression) - .copied() - .or(self.cycle_fallback_type) - } - - #[track_caller] - pub(crate) fn binding_type(&self, definition: Definition<'db>) -> Type<'db> { - self.bindings - .get(&definition) - .copied() - .or(self.cycle_fallback_type) - .expect( - "definition should belong to this TypeInference region and - TypeInferenceBuilder should have inferred a type for it", - ) - } - - #[track_caller] - pub(crate) fn declaration_type(&self, definition: Definition<'db>) -> TypeAndQualifiers<'db> { - self.declarations - .get(&definition) - .copied() - .or(self.cycle_fallback_type.map(Into::into)) - .expect( - "definition should belong to this TypeInference region and - TypeInferenceBuilder should have inferred a type for it", - ) - } - - pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { - &self.diagnostics - } - - fn shrink_to_fit(&mut self) { - self.expressions.shrink_to_fit(); - self.bindings.shrink_to_fit(); - self.declarations.shrink_to_fit(); - self.diagnostics.shrink_to_fit(); - self.deferred.shrink_to_fit(); - } -} - -/// Whether the intersection type is on the left or right side of the comparison. -#[derive(Debug, Clone, Copy)] -enum IntersectionOn { - Left, - Right, -} - -/// A helper to track if we already know that declared and inferred types are the same. -#[derive(Debug, Clone, PartialEq, Eq)] -enum DeclaredAndInferredType<'db> { - /// We know that both the declared and inferred types are the same. - AreTheSame(Type<'db>), - /// Declared and inferred types might be different, we need to check assignability. - MightBeDifferent { - declared_ty: TypeAndQualifiers<'db>, - inferred_ty: Type<'db>, - }, -} - -/// Builder to infer all types in a region. -/// -/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling -/// [`finish()`](TypeInferenceBuilder::finish) on it, which returns the resulting -/// [`TypeInference`]. -/// -/// There are a few different kinds of methods in the type inference builder, and the naming -/// distinctions are a bit subtle. -/// -/// The `finish` method calls [`infer_region`](TypeInferenceBuilder::infer_region), which delegates -/// to one of [`infer_region_scope`](TypeInferenceBuilder::infer_region_scope), -/// [`infer_region_definition`](TypeInferenceBuilder::infer_region_definition), or -/// [`infer_region_expression`](TypeInferenceBuilder::infer_region_expression), depending which -/// kind of [`InferenceRegion`] we are inferring types for. -/// -/// Scope inference starts with the scope body, walking all statements and expressions and -/// recording the types of each expression in the [`TypeInference`] result. Most of the methods -/// here (with names like `infer_*_statement` or `infer_*_expression` or some other node kind) take -/// a single AST node and are called as part of this AST visit. -/// -/// When the visit encounters a node which creates a [`Definition`], we look up the definition in -/// the semantic index and call the [`infer_definition_types()`] query on it, which creates another -/// [`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 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 -/// called by [`infer_region_definition`](TypeInferenceBuilder::infer_region_definition). -/// -/// So for example we have both -/// [`infer_function_definition_statement`](TypeInferenceBuilder::infer_function_definition_statement), -/// which takes just the function AST node, and -/// [`infer_function_definition`](TypeInferenceBuilder::infer_function_definition), which takes -/// both the node and the [`Definition`] id. The former is called as part of walking the AST, and -/// it just looks up the [`Definition`] for that function in the semantic index and calls -/// [`infer_definition_types()`] on it, which will create a new [`TypeInferenceBuilder`] with -/// [`InferenceRegion::Definition`], and in that builder -/// [`infer_region_definition`](TypeInferenceBuilder::infer_region_definition) will call -/// [`infer_function_definition`](TypeInferenceBuilder::infer_function_definition) to actually -/// infer a type for the definition. -/// -/// Similarly, when we encounter a standalone-inferable expression (right-hand side of an -/// assignment, type narrowing guard), we use the [`infer_expression_types()`] query to ensure we -/// don't infer its types more than once. -pub(super) struct TypeInferenceBuilder<'db> { - context: InferContext<'db>, - index: &'db SemanticIndex<'db>, - region: InferenceRegion<'db>, - - /// The type inference results - types: TypeInference<'db>, - - /// The returned types and their corresponding ranges of the region, if it is a function body. - return_types_and_ranges: Vec>, - - /// The deferred state of inferring types of certain expressions within the region. - /// - /// This is different from [`InferenceRegion::Deferred`] which works on the entire definition - /// while this is relevant for specific expressions within the region itself and is updated - /// during the inference process. - /// - /// For example, when inferring the types of an annotated assignment, the type of an annotation - /// expression could be deferred if the file has `from __future__ import annotations` import or - /// is a stub file but we're still in a non-deferred region. - deferred_state: DeferredExpressionState, -} - -impl<'db> TypeInferenceBuilder<'db> { - /// How big a string do we build before bailing? - /// - /// This is a fairly arbitrary number. It should be *far* more than enough - /// for most use cases, but we can reevaluate it later if useful. - const MAX_STRING_LITERAL_SIZE: usize = 4096; - - /// Creates a new builder for inferring types in a region. - pub(super) fn new( - db: &'db dyn Db, - region: InferenceRegion<'db>, - index: &'db SemanticIndex<'db>, - ) -> Self { - let scope = region.scope(db); - - Self { - context: InferContext::new(db, scope), - index, - region, - return_types_and_ranges: vec![], - deferred_state: DeferredExpressionState::None, - types: TypeInference::empty(scope), - } - } - - fn extend(&mut self, inference: &TypeInference<'db>) { - debug_assert_eq!(self.types.scope, inference.scope); - - self.types.bindings.extend(inference.bindings.iter()); - self.types - .declarations - .extend(inference.declarations.iter()); - self.types.expressions.extend(inference.expressions.iter()); - self.types.deferred.extend(inference.deferred.iter()); - self.context.extend(inference.diagnostics()); - } - - fn file(&self) -> File { - self.context.file() - } - - fn db(&self) -> &'db dyn Db { - self.context.db() - } - - fn scope(&self) -> ScopeId<'db> { - self.types.scope - } - - /// Are we currently inferring types in file with deferred types? - /// This is true for stub files and files with `__future__.annotations` - fn are_all_types_deferred(&self) -> bool { - self.index.has_future_annotations() || self.file().is_stub(self.db().upcast()) - } - - /// Are we currently inferring deferred types? - fn is_deferred(&self) -> bool { - matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred() - } - - /// Return the node key of the given AST node, or the key of the outermost enclosing string - /// literal, if the node originates from inside a stringified annotation. - fn enclosing_node_key(&self, node: AnyNodeRef<'_>) -> NodeKey { - match self.deferred_state { - DeferredExpressionState::InStringAnnotation(enclosing_node_key) => enclosing_node_key, - _ => NodeKey::from_node(node), - } - } - - /// Check if a given AST node is reachable. - /// - /// Note that this only works if reachability is explicitly tracked for this specific - /// type of node (see `node_reachability` in the use-def map). - fn is_reachable<'a, N>(&self, node: N) -> bool - where - N: Into>, - { - let file_scope_id = self.scope().file_scope_id(self.db()); - self.index.is_node_reachable( - self.db(), - file_scope_id, - self.enclosing_node_key(node.into()), - ) - } - - fn in_stub(&self) -> bool { - self.context.in_stub() - } - - /// Get the already-inferred type of an expression node. - /// - /// ## Panics - /// If the expression is not within this region, or if no type has yet been inferred for - /// this node. - #[track_caller] - fn expression_type(&self, expr: &ast::Expr) -> Type<'db> { - self.types - .expression_type(expr.scoped_expression_id(self.db(), self.scope())) - } - - /// Get the type of an expression from any scope in the same file. - /// - /// If the expression is in the current scope, and we are inferring the entire scope, just look - /// up the expression in our own results, otherwise call [`infer_scope_types()`] for the scope - /// of the expression. - /// - /// ## Panics - /// - /// If the expression is in the current scope but we haven't yet inferred a type for it. - /// - /// Can cause query cycles if the expression is from a different scope and type inference is - /// already in progress for that scope (further up the stack). - fn file_expression_type(&self, expression: &ast::Expr) -> Type<'db> { - let file_scope = self.index.expression_scope_id(expression); - let expr_scope = file_scope.to_scope_id(self.db(), self.file()); - let expr_id = expression.scoped_expression_id(self.db(), expr_scope); - match self.region { - InferenceRegion::Scope(scope) if scope == expr_scope => { - self.expression_type(expression) - } - _ => infer_scope_types(self.db(), expr_scope).expression_type(expr_id), - } - } - - /// Infers types in the given [`InferenceRegion`]. - fn infer_region(&mut self) { - match self.region { - InferenceRegion::Scope(scope) => self.infer_region_scope(scope), - InferenceRegion::Definition(definition) => self.infer_region_definition(definition), - InferenceRegion::Deferred(definition) => self.infer_region_deferred(definition), - InferenceRegion::Expression(expression) => self.infer_region_expression(expression), - } - } - - fn infer_region_scope(&mut self, scope: ScopeId<'db>) { - let node = scope.node(self.db()); - match node { - NodeWithScopeKind::Module => { - let parsed = parsed_module(self.db().upcast(), self.file()); - self.infer_module(parsed.syntax()); - } - NodeWithScopeKind::Function(function) => self.infer_function_body(function.node()), - NodeWithScopeKind::Lambda(lambda) => self.infer_lambda_body(lambda.node()), - NodeWithScopeKind::Class(class) => self.infer_class_body(class.node()), - NodeWithScopeKind::ClassTypeParameters(class) => { - self.infer_class_type_params(class.node()); - } - NodeWithScopeKind::FunctionTypeParameters(function) => { - self.infer_function_type_params(function.node()); - } - NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { - self.infer_type_alias_type_params(type_alias.node()); - } - NodeWithScopeKind::TypeAlias(type_alias) => { - self.infer_type_alias(type_alias.node()); - } - NodeWithScopeKind::ListComprehension(comprehension) => { - self.infer_list_comprehension_expression_scope(comprehension.node()); - } - NodeWithScopeKind::SetComprehension(comprehension) => { - self.infer_set_comprehension_expression_scope(comprehension.node()); - } - NodeWithScopeKind::DictComprehension(comprehension) => { - self.infer_dict_comprehension_expression_scope(comprehension.node()); - } - NodeWithScopeKind::GeneratorExpression(generator) => { - self.infer_generator_expression_scope(generator.node()); - } - } - - // Infer the deferred types for the definitions here to consider the end-of-scope - // semantics. - for definition in std::mem::take(&mut self.types.deferred) { - self.extend(infer_deferred_types(self.db(), definition)); - } - assert!( - self.types.deferred.is_empty(), - "Inferring deferred types should not add more deferred definitions" - ); - - // TODO: Only call this function when diagnostics are enabled. - self.check_class_definitions(); - } - - /// Iterate over all class definitions to check that the definition will not cause an exception - /// to be raised at runtime. This needs to be done after most other types in the scope have been - /// inferred, due to the fact that base classes can be deferred. If it looks like a class - /// definition is invalid in some way, issue a diagnostic. - /// - /// Among the things we check for in this method are whether Python will be able to determine a - /// consistent "[method resolution order]" and [metaclass] for each class. - /// - /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - /// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses - fn check_class_definitions(&mut self) { - let class_definitions = self - .types - .declarations - .iter() - .filter_map(|(definition, ty)| { - // Filter out class literals that result from imports - if let DefinitionKind::Class(class) = definition.kind(self.db()) { - ty.inner_type() - .into_class_literal() - .map(|ty| (ty, class.node())) - } else { - None - } - }); - - // Iterate through all class definitions in this scope. - for (class, class_node) in class_definitions { - // (1) Check that the class does not have a cyclic definition - if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) { - if inheritance_cycle.is_participant() { - self.context.report_lint_old( - &CYCLIC_CLASS_DEFINITION, - class_node, - format_args!( - "Cyclic definition of `{}` (class cannot inherit from itself)", - class.name(self.db()) - ), - ); - } - // Attempting to determine the MRO of a class or if the class has a metaclass conflict - // is impossible if the class is cyclically defined; there's nothing more to do here. - continue; - } - - // (2) Check for classes that inherit from `@final` classes - for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { - // dynamic/unknown bases are never `@final` - let Some(base_class) = base_class.into_class_literal() else { - continue; - }; - if !base_class.is_final(self.db()) { - continue; - } - self.context.report_lint_old( - &SUBCLASS_OF_FINAL_CLASS, - &class_node.bases()[i], - format_args!( - "Class `{}` cannot inherit from final class `{}`", - class.name(self.db()), - base_class.name(self.db()), - ), - ); - } - - // (3) Check that the class's MRO is resolvable - match class.try_mro(self.db(), None).as_ref() { - Err(mro_error) => { - match mro_error.reason() { - MroErrorKind::DuplicateBases(duplicates) => { - let base_nodes = class_node.bases(); - for (index, duplicate) in duplicates { - self.context.report_lint_old( - &DUPLICATE_BASE, - &base_nodes[*index], - format_args!("Duplicate base class `{}`", duplicate.name(self.db())), - ); - } - } - MroErrorKind::InvalidBases(bases) => { - let base_nodes = class_node.bases(); - for (index, base_ty) in bases { - if base_ty.is_never() { - // A class base of type `Never` can appear in unreachable code. It - // does not indicate a problem, since the actual construction of the - // class will never happen. - continue; - } - self.context.report_lint_old( - &INVALID_BASE, - &base_nodes[*index], - format_args!( - "Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)", - base_ty.display(self.db()) - ), - ); - } - } - MroErrorKind::UnresolvableMro { bases_list } => self.context.report_lint_old( - &INCONSISTENT_MRO, - class_node, - format_args!( - "Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`", - class.name(self.db()), - bases_list.iter().map(|base| base.display(self.db())).join(", ") - ), - ) - } - } - Ok(_) => check_class_slots(&self.context, class, class_node), - } - - // (4) Check that the class's metaclass can be determined without error. - if let Err(metaclass_error) = class.try_metaclass(self.db()) { - match metaclass_error.reason() { - MetaclassErrorKind::NotCallable(ty) => self.context.report_lint_old( - &INVALID_METACLASS, - class_node, - format_args!("Metaclass type `{}` is not callable", ty.display(self.db())), - ), - MetaclassErrorKind::PartlyNotCallable(ty) => self.context.report_lint_old( - &INVALID_METACLASS, - class_node, - format_args!( - "Metaclass type `{}` is partly not callable", - ty.display(self.db()) - ), - ), - MetaclassErrorKind::Conflict { - candidate1: - MetaclassCandidate { - metaclass: metaclass1, - explicit_metaclass_of: class1, - }, - candidate2: - MetaclassCandidate { - metaclass: metaclass2, - explicit_metaclass_of: class2, - }, - candidate1_is_base_class, - } => { - if *candidate1_is_base_class { - self.context.report_lint_old( - &CONFLICTING_METACLASS, - class_node, - format_args!( - "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ - but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \ - have no subclass relationship", - class = class.name(self.db()), - metaclass1 = metaclass1.name(self.db()), - base1 = class1.name(self.db()), - metaclass2 = metaclass2.name(self.db()), - base2 = class2.name(self.db()), - ), - ); - } else { - self.context.report_lint_old( - &CONFLICTING_METACLASS, - class_node, - format_args!( - "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ - but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \ - have no subclass relationship", - class = class.name(self.db()), - metaclass_of_class = metaclass1.name(self.db()), - metaclass_of_base = metaclass2.name(self.db()), - base = class2.name(self.db()), - ), - ); - } - } - } - } - } - } - - fn infer_region_definition(&mut self, definition: Definition<'db>) { - match definition.kind(self.db()) { - DefinitionKind::Function(function) => { - self.infer_function_definition(function.node(), definition); - } - DefinitionKind::Class(class) => self.infer_class_definition(class.node(), definition), - DefinitionKind::TypeAlias(type_alias) => { - self.infer_type_alias_definition(type_alias.node(), definition); - } - DefinitionKind::Import(import) => { - self.infer_import_definition(import.import(), import.alias(), definition); - } - DefinitionKind::ImportFrom(import_from) => { - self.infer_import_from_definition( - import_from.import(), - import_from.alias(), - definition, - ); - } - DefinitionKind::StarImport(import) => { - self.infer_import_from_definition(import.import(), import.alias(), definition); - } - DefinitionKind::Assignment(assignment) => { - self.infer_assignment_definition(assignment, definition); - } - DefinitionKind::AnnotatedAssignment(annotated_assignment) => { - self.infer_annotated_assignment_definition(annotated_assignment, definition); - } - DefinitionKind::AugmentedAssignment(augmented_assignment) => { - self.infer_augment_assignment_definition(augmented_assignment.node(), definition); - } - DefinitionKind::For(for_statement_definition) => { - self.infer_for_statement_definition(for_statement_definition, definition); - } - DefinitionKind::NamedExpression(named_expression) => { - self.infer_named_expression_definition(named_expression.node(), definition); - } - DefinitionKind::Comprehension(comprehension) => { - self.infer_comprehension_definition( - comprehension.iterable(), - comprehension.target(), - comprehension.is_first(), - comprehension.is_async(), - definition, - ); - } - DefinitionKind::VariadicPositionalParameter(parameter) => { - self.infer_variadic_positional_parameter_definition(parameter, definition); - } - DefinitionKind::VariadicKeywordParameter(parameter) => { - self.infer_variadic_keyword_parameter_definition(parameter, definition); - } - DefinitionKind::Parameter(parameter_with_default) => { - self.infer_parameter_definition(parameter_with_default, definition); - } - DefinitionKind::WithItem(with_item_definition) => { - self.infer_with_item_definition(with_item_definition, definition); - } - DefinitionKind::MatchPattern(match_pattern) => { - self.infer_match_pattern_definition( - match_pattern.pattern(), - match_pattern.index(), - definition, - ); - } - DefinitionKind::ExceptHandler(except_handler_definition) => { - self.infer_except_handler_definition(except_handler_definition, definition); - } - DefinitionKind::TypeVar(node) => { - self.infer_typevar_definition(node, definition); - } - DefinitionKind::ParamSpec(node) => { - self.infer_paramspec_definition(node, definition); - } - DefinitionKind::TypeVarTuple(node) => { - self.infer_typevartuple_definition(node, definition); - } - } - } - - fn infer_region_deferred(&mut self, definition: Definition<'db>) { - // N.B. We don't defer the types for an annotated assignment here because it is done in - // the same definition query. It utilizes the deferred expression state instead. - // - // This is because for partially stringified annotations like `a: tuple[int, "ForwardRef"]`, - // we need to defer the types of non-stringified expressions like `tuple` and `int` in the - // definition query while the stringified expression `"ForwardRef"` would need to deferred - // to use end-of-scope semantics. This would require custom and possibly a complex - // implementation to allow this "split" to happen. - - match definition.kind(self.db()) { - DefinitionKind::Function(function) => self.infer_function_deferred(function.node()), - DefinitionKind::Class(class) => self.infer_class_deferred(class.node()), - _ => {} - } - } - - fn infer_region_expression(&mut self, expression: Expression<'db>) { - match expression.kind(self.db()) { - ExpressionKind::Normal => { - self.infer_expression_impl(expression.node_ref(self.db())); - } - ExpressionKind::TypeExpression => { - self.infer_type_expression(expression.node_ref(self.db())); - } - } - } - - /// Raise a diagnostic if the given type cannot be divided by zero. - /// - /// Expects the resolved type of the left side of the binary expression. - fn check_division_by_zero( - &mut self, - node: AnyNodeRef<'_>, - op: ast::Operator, - left: Type<'db>, - ) -> bool { - match left { - Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} - Type::Instance(instance) - if matches!( - instance.class.known(self.db()), - Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) - ) => {} - _ => return false, - } - - let (op, by_zero) = match op { - ast::Operator::Div => ("divide", "by zero"), - ast::Operator::FloorDiv => ("floor divide", "by zero"), - ast::Operator::Mod => ("reduce", "modulo zero"), - _ => return false, - }; - - self.context.report_lint_old( - &DIVISION_BY_ZERO, - node, - format_args!( - "Cannot {op} object of type `{}` {by_zero}", - left.display(self.db()) - ), - ); - - true - } - - fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) { - debug_assert!(binding - .kind(self.db()) - .category(self.context.in_stub()) - .is_binding()); - let use_def = self.index.use_def_map(binding.file_scope(self.db())); - let declarations = use_def.declarations_at_binding(binding); - let mut bound_ty = ty; - let declared_ty = symbol_from_declarations(self.db(), declarations) - .map(|SymbolAndQualifiers { symbol, .. }| { - symbol.ignore_possibly_unbound().unwrap_or(Type::unknown()) - }) - .unwrap_or_else(|(ty, conflicting)| { - // TODO point out the conflicting declarations in the diagnostic? - let symbol_table = self.index.symbol_table(binding.file_scope(self.db())); - let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name(); - self.context.report_lint_old( - &CONFLICTING_DECLARATIONS, - node, - format_args!( - "Conflicting declared types for `{symbol_name}`: {}", - conflicting.display(self.db()) - ), - ); - ty.inner_type() - }); - if !bound_ty.is_assignable_to(self.db(), declared_ty) { - report_invalid_assignment(&self.context, node, declared_ty, bound_ty); - // allow declarations to override inference in case of invalid assignment - bound_ty = declared_ty; - } - - self.types.bindings.insert(binding, bound_ty); - } - - fn add_declaration( - &mut self, - node: AnyNodeRef, - declaration: Definition<'db>, - ty: TypeAndQualifiers<'db>, - ) { - debug_assert!(declaration - .kind(self.db()) - .category(self.context.in_stub()) - .is_declaration()); - let use_def = self.index.use_def_map(declaration.file_scope(self.db())); - let prior_bindings = use_def.bindings_at_declaration(declaration); - // unbound_ty is Never because for this check we don't care about unbound - let inferred_ty = symbol_from_bindings(self.db(), prior_bindings) - .ignore_possibly_unbound() - .unwrap_or(Type::Never); - let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { - ty - } else { - self.context.report_lint_old( - &INVALID_DECLARATION, - node, - format_args!( - "Cannot declare type `{}` for inferred type `{}`", - ty.inner_type().display(self.db()), - inferred_ty.display(self.db()) - ), - ); - TypeAndQualifiers::unknown() - }; - self.types.declarations.insert(declaration, ty); - } - - fn add_declaration_with_binding( - &mut self, - node: AnyNodeRef, - definition: Definition<'db>, - declared_and_inferred_ty: &DeclaredAndInferredType<'db>, - ) { - debug_assert!(definition - .kind(self.db()) - .category(self.context.in_stub()) - .is_binding()); - debug_assert!(definition - .kind(self.db()) - .category(self.context.in_stub()) - .is_declaration()); - - let (declared_ty, inferred_ty) = match *declared_and_inferred_ty { - DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty), - DeclaredAndInferredType::MightBeDifferent { - declared_ty, - inferred_ty, - } => { - if inferred_ty.is_assignable_to(self.db(), declared_ty.inner_type()) { - (declared_ty, inferred_ty) - } else { - report_invalid_assignment( - &self.context, - node, - declared_ty.inner_type(), - inferred_ty, - ); - // if the assignment is invalid, fall back to assuming the annotation is correct - (declared_ty, declared_ty.inner_type()) - } - } - }; - self.types.declarations.insert(definition, declared_ty); - self.types.bindings.insert(definition, inferred_ty); - } - - fn add_unknown_declaration_with_binding( - &mut self, - node: AnyNodeRef, - definition: Definition<'db>, - ) { - self.add_declaration_with_binding( - node, - definition, - &DeclaredAndInferredType::AreTheSame(Type::unknown()), - ); - } - - fn record_return_type(&mut self, ty: Type<'db>, range: TextRange) { - self.return_types_and_ranges - .push(TypeAndRange { ty, range }); - } - - fn infer_module(&mut self, module: &ast::ModModule) { - self.infer_body(&module.body); - } - - fn infer_class_type_params(&mut self, class: &ast::StmtClassDef) { - let type_params = class - .type_params - .as_deref() - .expect("class type params scope without type params"); - - self.infer_type_parameters(type_params); - - if let Some(arguments) = class.arguments.as_deref() { - 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); - } - } - - fn infer_class_body(&mut self, class: &ast::StmtClassDef) { - self.infer_body(&class.body); - } - - fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { - let type_params = function - .type_params - .as_deref() - .expect("function type params scope without type params"); - - self.infer_optional_annotation_expression( - function.returns.as_deref(), - DeferredExpressionState::None, - ); - self.infer_type_parameters(type_params); - self.infer_parameters(&function.parameters); - } - - fn infer_type_alias_type_params(&mut self, type_alias: &ast::StmtTypeAlias) { - let type_params = type_alias - .type_params - .as_ref() - .expect("type alias type params scope without type params"); - - self.infer_type_parameters(type_params); - } - - fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) { - self.infer_annotation_expression(&type_alias.value, DeferredExpressionState::Deferred); - } - - /// Returns `true` if the current scope is the function body scope of a method of a protocol - /// (that is, a class which directly inherits `typing.Protocol`.) - fn in_class_that_inherits_protocol_directly(&self) -> 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) - ) - }) - } - - /// Returns `true` if the current scope is the function body scope of a function overload (that - /// is, the stub declaration decorated with `@overload`, not the implementation), or an - /// abstract method (decorated with `@abstractmethod`.) - fn in_function_overload_or_abstractmethod(&self) -> bool { - let current_scope_id = self.scope().file_scope_id(self.db()); - let current_scope = self.index.scope(current_scope_id); - - let function_scope = match current_scope.kind() { - ScopeKind::Function => current_scope, - _ => return false, - }; - - let NodeWithScopeKind::Function(node_ref) = function_scope.node() else { - return false; - }; - - node_ref.decorator_list.iter().any(|decorator| { - let decorator_type = self.file_expression_type(&decorator.expression); - - match decorator_type { - Type::FunctionLiteral(function) => matches!( - function.known(self.db()), - Some(KnownFunction::Overload | KnownFunction::AbstractMethod) - ), - _ => false, - } - }) - } - - fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { - // Parameters are odd: they are Definitions in the function body scope, but have no - // constituent nodes that are part of the function body. In order to get diagnostics - // merged/emitted for them, we need to explicitly infer their definitions here. - for parameter in &function.parameters { - self.infer_definition(parameter); - } - self.infer_body(&function.body); - - if let Some(declared_ty) = function - .returns - .as_deref() - .map(|ret| self.file_expression_type(ret)) - { - fn is_stub_suite(suite: &[ast::Stmt]) -> bool { - match suite { - [ast::Stmt::Expr(ast::StmtExpr { value: first, .. }), ast::Stmt::Expr(ast::StmtExpr { value: second, .. }), ..] => { - first.is_string_literal_expr() && second.is_ellipsis_literal_expr() - } - [ast::Stmt::Expr(ast::StmtExpr { value, .. }), ast::Stmt::Pass(_), ..] => { - value.is_string_literal_expr() - } - [ast::Stmt::Expr(ast::StmtExpr { value, .. }), ..] => { - value.is_ellipsis_literal_expr() || value.is_string_literal_expr() - } - [ast::Stmt::Pass(_)] => true, - _ => false, - } - } - - if (self.in_stub() - || self.in_function_overload_or_abstractmethod() - || self.in_class_that_inherits_protocol_directly()) - && self.return_types_and_ranges.is_empty() - && is_stub_suite(&function.body) - { - return; - } - - for invalid in self - .return_types_and_ranges - .iter() - .copied() - .filter_map(|ty_range| match ty_range.ty { - // We skip `is_assignable_to` checks for `NotImplemented`, - // so we remove it beforehand. - Type::Union(union) => Some(TypeAndRange { - ty: union.filter(self.db(), |ty| !ty.is_notimplemented(self.db())), - range: ty_range.range, - }), - ty if ty.is_notimplemented(self.db()) => None, - _ => Some(ty_range), - }) - .filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), declared_ty)) - { - report_invalid_return_type( - &self.context, - invalid.range, - function.returns.as_ref().unwrap().range(), - declared_ty, - invalid.ty, - ); - } - let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); - let use_def = self.index.use_def_map(scope_id); - if use_def.can_implicit_return(self.db()) - && !KnownClass::NoneType - .to_instance(self.db()) - .is_assignable_to(self.db(), declared_ty) - { - report_implicit_return_type( - &self.context, - function.returns.as_ref().unwrap().range(), - declared_ty, - ); - } - } - } - - fn infer_body(&mut self, suite: &[ast::Stmt]) { - for statement in suite { - self.infer_statement(statement); - } - } - - fn infer_statement(&mut self, statement: &ast::Stmt) { - match statement { - ast::Stmt::FunctionDef(function) => self.infer_function_definition_statement(function), - ast::Stmt::ClassDef(class) => self.infer_class_definition_statement(class), - ast::Stmt::Expr(ast::StmtExpr { range: _, value }) => { - self.infer_expression(value); - } - ast::Stmt::If(if_statement) => self.infer_if_statement(if_statement), - ast::Stmt::Try(try_statement) => self.infer_try_statement(try_statement), - ast::Stmt::With(with_statement) => self.infer_with_statement(with_statement), - ast::Stmt::Match(match_statement) => self.infer_match_statement(match_statement), - ast::Stmt::Assign(assign) => self.infer_assignment_statement(assign), - ast::Stmt::AnnAssign(assign) => self.infer_annotated_assignment_statement(assign), - ast::Stmt::AugAssign(aug_assign) => { - self.infer_augmented_assignment_statement(aug_assign); - } - ast::Stmt::TypeAlias(type_statement) => self.infer_type_alias_statement(type_statement), - ast::Stmt::For(for_statement) => self.infer_for_statement(for_statement), - ast::Stmt::While(while_statement) => self.infer_while_statement(while_statement), - ast::Stmt::Import(import) => self.infer_import_statement(import), - ast::Stmt::ImportFrom(import) => self.infer_import_from_statement(import), - ast::Stmt::Assert(assert_statement) => self.infer_assert_statement(assert_statement), - ast::Stmt::Raise(raise) => self.infer_raise_statement(raise), - ast::Stmt::Return(ret) => self.infer_return_statement(ret), - ast::Stmt::Delete(delete) => self.infer_delete_statement(delete), - ast::Stmt::Break(_) - | ast::Stmt::Continue(_) - | ast::Stmt::Pass(_) - | ast::Stmt::IpyEscapeCommand(_) - | ast::Stmt::Global(_) - | ast::Stmt::Nonlocal(_) => { - // No-op - } - } - } - - fn infer_definition(&mut self, node: impl Into + std::fmt::Debug + Copy) { - let definition = self.index.expect_single_definition(node); - let result = infer_definition_types(self.db(), definition); - self.extend(result); - } - - fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) { - self.infer_definition(function); - } - - fn infer_function_definition( - &mut self, - function: &ast::StmtFunctionDef, - definition: Definition<'db>, - ) { - let ast::StmtFunctionDef { - range: _, - is_async: _, - name, - type_params, - parameters, - returns, - body: _, - decorator_list, - } = function; - - let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); - let mut function_decorators = FunctionDecorators::empty(); - - for decorator in decorator_list { - let decorator_ty = self.infer_decorator(decorator); - - if let Type::FunctionLiteral(function) = decorator_ty { - if function.is_known(self.db(), KnownFunction::NoTypeCheck) { - // If the function is decorated with the `no_type_check` decorator, - // we need to suppress any errors that come after the decorators. - self.context.set_in_no_type_check(InNoTypeCheck::Yes); - function_decorators |= FunctionDecorators::NO_TYPE_CHECK; - continue; - } else if function.is_known(self.db(), KnownFunction::Overload) { - function_decorators |= FunctionDecorators::OVERLOAD; - continue; - } - } else if let Type::ClassLiteral(class) = decorator_ty { - if class.is_known(self.db(), KnownClass::Classmethod) { - function_decorators |= FunctionDecorators::CLASSMETHOD; - continue; - } - } - - decorator_types_and_nodes.push((decorator_ty, decorator)); - } - - for default in parameters - .iter_non_variadic_params() - .filter_map(|param| param.default.as_deref()) - { - self.infer_expression(default); - } - - // If there are type params, parameters and returns are evaluated in that scope, that is, in - // `infer_function_type_params`, rather than here. - if type_params.is_none() { - if self.are_all_types_deferred() { - self.types.deferred.insert(definition); - } else { - self.infer_optional_annotation_expression( - returns.as_deref(), - DeferredExpressionState::None, - ); - self.infer_parameters(parameters); - } - } - - let generic_context = type_params.as_ref().map(|type_params| { - GenericContext::from_type_params(self.db(), self.index, type_params) - }); - - let function_kind = - KnownFunction::try_from_definition_and_name(self.db(), definition, name); - - let body_scope = self - .index - .node_scope(NodeWithScopeRef::Function(function)) - .to_scope_id(self.db(), self.file()); - - let specialization = None; - - let mut inferred_ty = Type::FunctionLiteral(FunctionType::new( - self.db(), - &name.id, - function_kind, - body_scope, - function_decorators, - generic_context, - specialization, - )); - - for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { - inferred_ty = match decorator_ty - .try_call(self.db(), CallArgumentTypes::positional([inferred_ty])) - .map(|bindings| bindings.return_type(self.db())) - { - Ok(return_ty) => return_ty, - Err(CallError(_, bindings)) => { - bindings.report_diagnostics(&self.context, (*decorator_node).into()); - bindings.return_type(self.db()) - } - }; - } - - self.add_declaration_with_binding( - function.into(), - definition, - &DeclaredAndInferredType::AreTheSame(inferred_ty), - ); - } - - fn infer_parameters(&mut self, parameters: &ast::Parameters) { - let ast::Parameters { - range: _, - posonlyargs: _, - args: _, - vararg, - kwonlyargs: _, - kwarg, - } = parameters; - - for param_with_default in parameters.iter_non_variadic_params() { - self.infer_parameter_with_default(param_with_default); - } - if let Some(vararg) = vararg { - self.infer_parameter(vararg); - } - if let Some(kwarg) = kwarg { - self.infer_parameter(kwarg); - } - } - - fn infer_parameter_with_default(&mut self, parameter_with_default: &ast::ParameterWithDefault) { - let ast::ParameterWithDefault { - range: _, - parameter, - default: _, - } = parameter_with_default; - - self.infer_optional_annotation_expression( - parameter.annotation.as_deref(), - DeferredExpressionState::None, - ); - } - - fn infer_parameter(&mut self, parameter: &ast::Parameter) { - let ast::Parameter { - range: _, - name: _, - annotation, - } = parameter; - - self.infer_optional_annotation_expression( - annotation.as_deref(), - DeferredExpressionState::None, - ); - } - - /// Set initial declared type (if annotated) and inferred type for a function-parameter symbol, - /// in the function body scope. - /// - /// The declared type is the annotated type, if any, or `Unknown`. - /// - /// The inferred type is the annotated type, unioned with the type of the default value, if - /// any. If both types are fully static, this union is a no-op (it should simplify to just the - /// annotated type.) But in a case like `f(x=None)` with no annotated type, we want to infer - /// the type `Unknown | None` for `x`, not just `Unknown`, so that we can error on usage of `x` - /// that would not be valid for `None`. - /// - /// If the default-value type is not assignable to the declared (annotated) type, we ignore the - /// default-value type and just infer the annotated type; this is the same way we handle - /// assignments, and allows an explicit annotation to override a bad inference. - /// - /// Parameter definitions are odd in that they define a symbol in the function-body scope, so - /// the Definition belongs to the function body scope, but the expressions (annotation and - /// default value) both belong to outer scopes. (The default value always belongs to the outer - /// scope in which the function is defined, the annotation belongs either to the outer scope, - /// or maybe to an intervening type-params scope, if it's a generic function.) So we don't use - /// `self.infer_expression` or store any expression types here, we just use `expression_ty` to - /// get the types of the expressions from their respective scopes. - /// - /// It is safe (non-cycle-causing) to use `expression_ty` here, because an outer scope can't - /// depend on a definition from an inner scope, so we shouldn't be in-process of inferring the - /// outer scope here. - fn infer_parameter_definition( - &mut self, - parameter_with_default: &ast::ParameterWithDefault, - definition: Definition<'db>, - ) { - let ast::ParameterWithDefault { - parameter, - default, - range: _, - } = parameter_with_default; - let default_ty = default - .as_ref() - .map(|default| self.file_expression_type(default)); - if let Some(annotation) = parameter.annotation.as_ref() { - let declared_ty = self.file_expression_type(annotation); - let declared_and_inferred_ty = if let Some(default_ty) = default_ty { - if default_ty.is_assignable_to(self.db(), declared_ty) { - DeclaredAndInferredType::MightBeDifferent { - declared_ty: declared_ty.into(), - inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]), - } - } else if (self.in_stub() - || self.in_function_overload_or_abstractmethod() - || self.in_class_that_inherits_protocol_directly()) - && default - .as_ref() - .is_some_and(|d| d.is_ellipsis_literal_expr()) - { - DeclaredAndInferredType::AreTheSame(declared_ty) - } else { - self.context.report_lint_old( - &INVALID_PARAMETER_DEFAULT, - parameter_with_default, - format_args!( - "Default value of type `{}` is not assignable to annotated parameter type `{}`", - default_ty.display(self.db()), declared_ty.display(self.db())), - ); - DeclaredAndInferredType::AreTheSame(declared_ty) - } - } else { - DeclaredAndInferredType::AreTheSame(declared_ty) - }; - self.add_declaration_with_binding( - parameter.into(), - definition, - &declared_and_inferred_ty, - ); - } else { - let ty = if let Some(default_ty) = default_ty { - UnionType::from_elements(self.db(), [Type::unknown(), default_ty]) - } else { - Type::unknown() - }; - self.add_binding(parameter.into(), definition, ty); - } - } - - /// Set initial declared/inferred types for a `*args` variadic positional parameter. - /// - /// The annotated type is implicitly wrapped in a homogeneous tuple. - /// - /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. - /// - /// [`infer_parameter_definition`]: Self::infer_parameter_definition - fn infer_variadic_positional_parameter_definition( - &mut self, - parameter: &ast::Parameter, - definition: Definition<'db>, - ) { - if let Some(annotation) = parameter.annotation() { - let _annotated_ty = self.file_expression_type(annotation); - // TODO `tuple[annotated_type, ...]` - let ty = KnownClass::Tuple.to_instance(self.db()); - self.add_declaration_with_binding( - parameter.into(), - definition, - &DeclaredAndInferredType::AreTheSame(ty), - ); - } else { - self.add_binding( - parameter.into(), - definition, - // TODO `tuple[Unknown, ...]` - KnownClass::Tuple.to_instance(self.db()), - ); - } - } - - /// Set initial declared/inferred types for a `*args` variadic positional parameter. - /// - /// The annotated type is implicitly wrapped in a string-keyed dictionary. - /// - /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. - /// - /// [`infer_parameter_definition`]: Self::infer_parameter_definition - fn infer_variadic_keyword_parameter_definition( - &mut self, - parameter: &ast::Parameter, - definition: Definition<'db>, - ) { - if let Some(annotation) = parameter.annotation() { - let _annotated_ty = self.file_expression_type(annotation); - // TODO `dict[str, annotated_type]` - let ty = KnownClass::Dict.to_instance(self.db()); - self.add_declaration_with_binding( - parameter.into(), - definition, - &DeclaredAndInferredType::AreTheSame(ty), - ); - } else { - self.add_binding( - parameter.into(), - definition, - // TODO `dict[str, Unknown]` - KnownClass::Dict.to_instance(self.db()), - ); - } - } - - fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) { - self.infer_definition(class); - } - - fn infer_class_definition( - &mut self, - class_node: &ast::StmtClassDef, - definition: Definition<'db>, - ) { - let ast::StmtClassDef { - range: _, - name, - type_params, - decorator_list, - arguments: _, - body: _, - } = class_node; - - let mut dataclass_metadata = None; - for decorator in decorator_list { - let decorator_ty = self.infer_decorator(decorator); - if decorator_ty - .into_function_literal() - .is_some_and(|function| function.is_known(self.db(), KnownFunction::Dataclass)) - { - dataclass_metadata = Some(DataclassMetadata::default()); - continue; - } - - if let Type::DataclassDecorator(metadata) = decorator_ty { - dataclass_metadata = Some(metadata); - continue; - } - } - - let generic_context = type_params.as_ref().map(|type_params| { - GenericContext::from_type_params(self.db(), self.index, type_params) - }); - - let body_scope = self - .index - .node_scope(NodeWithScopeRef::Class(class_node)) - .to_scope_id(self.db(), self.file()); - - let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name); - - let class = Class { - name: name.id.clone(), - body_scope, - known: maybe_known_class, - dataclass_metadata, - }; - let class_literal = match generic_context { - Some(generic_context) => { - ClassLiteralType::Generic(GenericClass::new(self.db(), class, generic_context)) - } - None => ClassLiteralType::NonGeneric(NonGenericClass::new(self.db(), class)), - }; - let class_ty = Type::from(class_literal); - - self.add_declaration_with_binding( - class_node.into(), - definition, - &DeclaredAndInferredType::AreTheSame(class_ty), - ); - - // if there are type parameters, then the keywords and bases are within that scope - // and we don't need to run inference here - if type_params.is_none() { - for keyword in class_node.keywords() { - self.infer_expression(&keyword.value); - } - - // Inference of bases deferred in stubs - // TODO: Only defer the references that are actually string literals, instead of - // deferring the entire class definition if a string literal occurs anywhere in the - // base class list. - if self.are_all_types_deferred() - || class_node.bases().iter().any(contains_string_literal) - { - self.types.deferred.insert(definition); - } else { - for base in class_node.bases() { - self.infer_expression(base); - } - } - } - } - - fn infer_function_deferred(&mut self, function: &ast::StmtFunctionDef) { - self.infer_optional_annotation_expression( - function.returns.as_deref(), - DeferredExpressionState::Deferred, - ); - self.infer_parameters(function.parameters.as_ref()); - } - - fn infer_class_deferred(&mut self, class: &ast::StmtClassDef) { - for base in class.bases() { - self.infer_expression(base); - } - } - - fn infer_type_alias_definition( - &mut self, - type_alias: &ast::StmtTypeAlias, - definition: Definition<'db>, - ) { - self.infer_expression(&type_alias.name); - - let rhs_scope = self - .index - .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) - .to_scope_id(self.db(), self.file()); - - let type_alias_ty = - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::new( - self.db(), - &type_alias.name.as_name_expr().unwrap().id, - rhs_scope, - ))); - - self.add_declaration_with_binding( - type_alias.into(), - definition, - &DeclaredAndInferredType::AreTheSame(type_alias_ty), - ); - } - - fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { - let ast::StmtIf { - range: _, - test, - body, - elif_else_clauses, - } = if_statement; - - let test_ty = self.infer_standalone_expression(test); - - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, &**test); - } - - self.infer_body(body); - - for clause in elif_else_clauses { - let ast::ElifElseClause { - range: _, - test, - body, - } = clause; - - if let Some(test) = &test { - let test_ty = self.infer_standalone_expression(test); - - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, test); - } - } - - self.infer_body(body); - } - } - - fn infer_try_statement(&mut self, try_statement: &ast::StmtTry) { - let ast::StmtTry { - range: _, - body, - handlers, - orelse, - finalbody, - is_star: _, - } = try_statement; - - self.infer_body(body); - - for handler in handlers { - let ast::ExceptHandler::ExceptHandler(handler) = handler; - let ast::ExceptHandlerExceptHandler { - type_: handled_exceptions, - name: symbol_name, - body, - range: _, - } = handler; - - // If `symbol_name` is `Some()` and `handled_exceptions` is `None`, - // it's invalid syntax (something like `except as e:`). - // However, it's obvious that the user *wanted* `e` to be bound here, - // so we'll have created a definition in the semantic-index stage anyway. - if symbol_name.is_some() { - self.infer_definition(handler); - } else { - self.infer_optional_expression(handled_exceptions.as_deref()); - } - - self.infer_body(body); - } - - self.infer_body(orelse); - self.infer_body(finalbody); - } - - fn infer_with_statement(&mut self, with_statement: &ast::StmtWith) { - let ast::StmtWith { - range: _, - is_async, - items, - body, - } = with_statement; - for item in items { - let target = item.optional_vars.as_deref(); - if let Some(target) = target { - self.infer_target(target, &item.context_expr, |db, ctx_manager_ty| { - // TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager - // but only if the target is a name. We should report a diagnostic here if the target isn't a name: - // `with not_context_manager as a.x: ... - ctx_manager_ty.enter(db) - }); - } else { - // Call into the context expression inference to validate that it evaluates - // to a valid context manager. - let context_expression_ty = self.infer_expression(&item.context_expr); - self.infer_context_expression(&item.context_expr, context_expression_ty, *is_async); - self.infer_optional_expression(target); - } - } - - self.infer_body(body); - } - - fn infer_with_item_definition( - &mut self, - with_item: &WithItemDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let context_expr = with_item.context_expr(); - let target = with_item.target(); - - let context_expr_ty = self.infer_standalone_expression(context_expr); - - let target_ty = if with_item.is_async() { - todo_type!("async `with` statement") - } else { - match with_item.target_kind() { - TargetKind::Sequence(unpack_position, unpack) => { - let unpacked = infer_unpack_types(self.db(), unpack); - let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); - if unpack_position == UnpackPosition::First { - self.context.extend(unpacked.diagnostics()); - } - unpacked.expression_type(target_ast_id) - } - TargetKind::NameOrAttribute => self.infer_context_expression( - context_expr, - context_expr_ty, - with_item.is_async(), - ), - } - }; - - self.store_expression_type(target, target_ty); - self.add_binding(target.into(), definition, target_ty); - } - - /// Infers the type of a context expression (`with expr`) and returns the target's type - /// - /// Returns [`Type::unknown`] if the context expression doesn't implement the context manager protocol. - /// - /// ## Terminology - /// See [PEP343](https://peps.python.org/pep-0343/#standard-terminology). - fn infer_context_expression( - &mut self, - context_expression: &ast::Expr, - context_expression_type: Type<'db>, - is_async: bool, - ) -> Type<'db> { - // TODO: Handle async with statements (they use `aenter` and `aexit`) - if is_async { - return todo_type!("async `with` statement"); - } - - context_expression_type - .try_enter(self.db()) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - context_expression_type, - context_expression.into(), - ); - err.fallback_enter_type(self.db()) - }) - } - - fn infer_except_handler_definition( - &mut self, - except_handler_definition: &ExceptHandlerDefinitionKind, - definition: Definition<'db>, - ) { - let node = except_handler_definition.handled_exceptions(); - - // If there is no handled exception, it's invalid syntax; - // a diagnostic will have already been emitted - let node_ty = node.map_or(Type::unknown(), |ty| self.infer_expression(ty)); - - // If it's an `except*` handler, this won't actually be the type of the bound symbol; - // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. - let symbol_ty = if let Type::Tuple(tuple) = node_ty { - let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); - let mut builder = UnionBuilder::new(self.db()); - for element in tuple.elements(self.db()).iter().copied() { - builder = builder.add( - if element.is_assignable_to(self.db(), type_base_exception) { - element.to_instance(self.db()).expect( - "`Type::to_instance()` should always return `Some()` \ - if called on a type assignable to `type[BaseException]`", - ) - } else { - if let Some(node) = node { - report_invalid_exception_caught(&self.context, node, element); - } - Type::unknown() - }, - ); - } - builder.build() - } else if node_ty.is_subtype_of(self.db(), KnownClass::Tuple.to_instance(self.db())) { - todo_type!("Homogeneous tuple in exception handler") - } else { - let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); - if node_ty.is_assignable_to(self.db(), type_base_exception) { - node_ty.to_instance(self.db()).expect( - "`Type::to_instance()` should always return `Some()` \ - if called on a type assignable to `type[BaseException]`", - ) - } else { - if let Some(node) = node { - report_invalid_exception_caught(&self.context, node, node_ty); - } - Type::unknown() - } - }; - - let symbol_ty = if except_handler_definition.is_star() { - // TODO: we should infer `ExceptionGroup` if `node_ty` is a subtype of `tuple[type[Exception], ...]` - // (needs support for homogeneous tuples). - // - // TODO: should be generic with `symbol_ty` as the generic parameter - KnownClass::BaseExceptionGroup.to_instance(self.db()) - } else { - symbol_ty - }; - - self.add_binding( - except_handler_definition.node().into(), - definition, - symbol_ty, - ); - } - - fn infer_typevar_definition( - &mut self, - node: &ast::TypeParamTypeVar, - definition: Definition<'db>, - ) { - let ast::TypeParamTypeVar { - range: _, - name, - bound, - default, - } = node; - let bound_or_constraint = match bound.as_deref() { - Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { - if elts.len() < 2 { - self.context.report_lint_old( - &INVALID_TYPE_VARIABLE_CONSTRAINTS, - expr, - format_args!("TypeVar must have at least two constrained types"), - ); - self.infer_expression(expr); - None - } else { - // We don't use UnionType::from_elements or UnionBuilder here, because we don't - // want to simplify the list of constraints like we do with the elements of an - // actual union type. - // TODO: Consider using a new `OneOfType` connective here instead, since that - // more accurately represents the actual semantics of typevar constraints. - let elements = UnionType::new( - self.db(), - elts.iter() - .map(|expr| self.infer_type_expression(expr)) - .collect::>(), - ); - let constraints = TypeVarBoundOrConstraints::Constraints(elements); - // But when we construct an actual union type for the constraint expression as - // a whole, we do use UnionType::from_elements to maintain the invariant that - // all union types are simplified. - self.store_expression_type( - expr, - UnionType::from_elements(self.db(), elements.elements(self.db())), - ); - Some(constraints) - } - } - Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound( - self.infer_type_expression(expr), - )), - None => None, - }; - let default_ty = self.infer_optional_type_expression(default.as_deref()); - let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - self.db(), - name.id.clone(), - definition, - bound_or_constraint, - default_ty, - ))); - self.add_declaration_with_binding( - node.into(), - definition, - &DeclaredAndInferredType::AreTheSame(ty), - ); - } - - fn infer_paramspec_definition( - &mut self, - node: &ast::TypeParamParamSpec, - definition: Definition<'db>, - ) { - let ast::TypeParamParamSpec { - range: _, - name: _, - default, - } = node; - self.infer_optional_expression(default.as_deref()); - let pep_695_todo = todo_type!("PEP-695 ParamSpec definition types"); - self.add_declaration_with_binding( - node.into(), - definition, - &DeclaredAndInferredType::AreTheSame(pep_695_todo), - ); - } - - fn infer_typevartuple_definition( - &mut self, - node: &ast::TypeParamTypeVarTuple, - definition: Definition<'db>, - ) { - let ast::TypeParamTypeVarTuple { - range: _, - name: _, - default, - } = node; - self.infer_optional_expression(default.as_deref()); - let pep_695_todo = todo_type!("PEP-695 TypeVarTuple definition types"); - self.add_declaration_with_binding( - node.into(), - definition, - &DeclaredAndInferredType::AreTheSame(pep_695_todo), - ); - } - - fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { - let ast::StmtMatch { - range: _, - subject, - cases, - } = match_statement; - - self.infer_standalone_expression(subject); - - for case in cases { - let ast::MatchCase { - range: _, - body, - pattern, - guard, - } = case; - self.infer_match_pattern(pattern); - - if let Some(guard) = guard.as_deref() { - let guard_ty = self.infer_standalone_expression(guard); - - if let Err(err) = guard_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, guard); - } - } - - self.infer_body(body); - } - } - - fn infer_match_pattern_definition( - &mut self, - pattern: &ast::Pattern, - _index: u32, - definition: Definition<'db>, - ) { - // TODO(dhruvmanila): The correct way to infer types here is to perform structural matching - // against the subject expression type (which we can query via `infer_expression_types`) - // and extract the type at the `index` position if the pattern matches. This will be - // similar to the logic in `self.infer_assignment_definition`. - self.add_binding( - pattern.into(), - definition, - todo_type!("`match` pattern definition types"), - ); - } - - fn infer_match_pattern(&mut self, pattern: &ast::Pattern) { - // We need to create a standalone expression for each arm of a match statement, since they - // can introduce constraints on the match subject. (Or more accurately, for the match arm's - // pattern, since its the pattern that introduces any constraints, not the body.) Ideally, - // that standalone expression would wrap the match arm's pattern as a whole. But a - // standalone expression can currently only wrap an ast::Expr, which patterns are not. So, - // we need to choose an Expr that can “stand in” for the pattern, which we can wrap in a - // standalone expression. - // - // That said, when inferring the type of a standalone expression, we don't have access to - // its parent or sibling nodes. That means, for instance, that in a class pattern, where - // we are currently using the class name as the standalone expression, we do not have - // access to the class pattern's arguments in the standalone expression inference scope. - // At the moment, we aren't trying to do anything with those arguments when creating a - // narrowing constraint for the pattern. But in the future, if we do, we will have to - // either wrap those arguments in their own standalone expressions, or update Expression to - // be able to wrap other AST node types besides just ast::Expr. - // - // This function is only called for the top-level pattern of a match arm, and is - // responsible for inferring the standalone expression for each supported pattern type. It - // then hands off to `infer_nested_match_pattern` for any subexpressions and subpatterns, - // where we do NOT have any additional standalone expressions to infer through. - // - // TODO(dhruvmanila): Add a Salsa query for inferring pattern types and matching against - // the subject expression: https://github.com/astral-sh/ruff/pull/13147#discussion_r1739424510 - match pattern { - ast::Pattern::MatchValue(match_value) => { - self.infer_standalone_expression(&match_value.value); - } - ast::Pattern::MatchClass(match_class) => { - let ast::PatternMatchClass { - range: _, - cls, - arguments, - } = match_class; - for pattern in &arguments.patterns { - self.infer_nested_match_pattern(pattern); - } - for keyword in &arguments.keywords { - self.infer_nested_match_pattern(&keyword.pattern); - } - self.infer_standalone_expression(cls); - } - ast::Pattern::MatchOr(match_or) => { - for pattern in &match_or.patterns { - self.infer_match_pattern(pattern); - } - } - _ => { - self.infer_nested_match_pattern(pattern); - } - } - } - - fn infer_nested_match_pattern(&mut self, pattern: &ast::Pattern) { - match pattern { - ast::Pattern::MatchValue(match_value) => { - self.infer_expression(&match_value.value); - } - ast::Pattern::MatchSequence(match_sequence) => { - for pattern in &match_sequence.patterns { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchMapping(match_mapping) => { - let ast::PatternMatchMapping { - range: _, - keys, - patterns, - rest: _, - } = match_mapping; - for key in keys { - self.infer_expression(key); - } - for pattern in patterns { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchClass(match_class) => { - let ast::PatternMatchClass { - range: _, - cls, - arguments, - } = match_class; - for pattern in &arguments.patterns { - self.infer_nested_match_pattern(pattern); - } - for keyword in &arguments.keywords { - self.infer_nested_match_pattern(&keyword.pattern); - } - self.infer_expression(cls); - } - ast::Pattern::MatchAs(match_as) => { - if let Some(pattern) = &match_as.pattern { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchOr(match_or) => { - for pattern in &match_or.patterns { - self.infer_nested_match_pattern(pattern); - } - } - ast::Pattern::MatchStar(_) | ast::Pattern::MatchSingleton(_) => {} - } - } - - fn infer_assignment_statement(&mut self, assignment: &ast::StmtAssign) { - let ast::StmtAssign { - range: _, - targets, - value, - } = assignment; - - for target in targets { - self.infer_target(target, value, |_, ty| ty); - } - } - - /// Infer the (definition) types involved in a `target` expression. - /// - /// This is used for assignment statements, for statements, etc. with a single or multiple - /// targets (unpacking). If `target` is an attribute expression, we check that the assignment - /// is valid. For 'target's that are definitions, this check happens elsewhere. - /// - /// The `to_assigned_ty` function is used to convert the inferred type of the `value` expression - /// to the type that is eventually assigned to the `target`. - /// - /// # Panics - /// - /// If the `value` is not a standalone expression. - fn infer_target(&mut self, target: &ast::Expr, value: &ast::Expr, to_assigned_ty: F) - where - F: Fn(&'db dyn Db, Type<'db>) -> Type<'db>, - { - let assigned_ty = match target { - ast::Expr::Name(_) => None, - _ => { - let value_ty = self.infer_standalone_expression(value); - - Some(to_assigned_ty(self.db(), value_ty)) - } - }; - self.infer_target_impl(target, assigned_ty); - } - - /// Make sure that the attribute assignment `obj.attribute = value` is valid. - /// - /// `target` is the node for the left-hand side, `object_ty` is the type of `obj`, `attribute` is - /// the name of the attribute being assigned, and `value_ty` is the type of the right-hand side of - /// the assignment. If the assignment is invalid, emit diagnostics. - fn validate_attribute_assignment( - &mut self, - target: &ast::ExprAttribute, - object_ty: Type<'db>, - attribute: &str, - value_ty: Type<'db>, - emit_diagnostics: bool, - ) -> bool { - let db = self.db(); - - let ensure_assignable_to = |attr_ty| -> bool { - let assignable = value_ty.is_assignable_to(db, attr_ty); - if !assignable && emit_diagnostics { - report_invalid_attribute_assignment( - &self.context, - target.into(), - attr_ty, - value_ty, - attribute, - ); - } - assignable - }; - - match object_ty { - Type::Union(union) => { - if union.elements(self.db()).iter().all(|elem| { - self.validate_attribute_assignment(target, *elem, attribute, value_ty, false) - }) { - true - } else { - // TODO: This is not a very helpful error message, as it does not include the underlying reason - // why the assignment is invalid. This would be a good use case for nested diagnostics. - if emit_diagnostics { - self.context.report_lint_old(&INVALID_ASSIGNMENT, target, format_args!( - "Object of type `{}` is not assignable to attribute `{attribute}` on type `{}`", - value_ty.display(self.db()), - object_ty.display(self.db()), - )); - } - - false - } - } - - Type::Intersection(intersection) => { - // TODO: Handle negative intersection elements - if intersection.positive(db).iter().any(|elem| { - self.validate_attribute_assignment(target, *elem, attribute, value_ty, false) - }) { - true - } else { - if emit_diagnostics { - // TODO: same here, see above - self.context.report_lint_old(&INVALID_ASSIGNMENT, target, format_args!( - "Object of type `{}` is not assignable to attribute `{attribute}` on type `{}`", - value_ty.display(self.db()), - object_ty.display(self.db()), - )); - } - false - } - } - - // Super instances do not allow attribute assignment - Type::Instance(instance) if instance.class.is_known(db, KnownClass::Super) => { - if emit_diagnostics { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Cannot assign to attribute `{attribute}` on type `{}`", - object_ty.display(self.db()), - ), - ); - } - false - } - Type::BoundSuper(_) => { - if emit_diagnostics { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Cannot assign to attribute `{attribute}` on type `{}`", - object_ty.display(self.db()), - ), - ); - } - false - } - - Type::Dynamic(..) | Type::Never => true, - - Type::Instance(..) - | Type::BooleanLiteral(..) - | Type::IntLiteral(..) - | Type::StringLiteral(..) - | Type::BytesLiteral(..) - | Type::LiteralString - | Type::SliceLiteral(..) - | Type::Tuple(..) - | Type::KnownInstance(..) - | Type::PropertyInstance(..) - | Type::FunctionLiteral(..) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::MethodWrapper(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::TypeVar(..) - | Type::AlwaysTruthy - | Type::AlwaysFalsy => { - match object_ty.class_member(db, attribute.into()) { - meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { - if emit_diagnostics { - self.context.report_lint_old( - &INVALID_ATTRIBUTE_ACCESS, - target, - format_args!( - "Cannot assign to ClassVar `{attribute}` from an instance of type `{ty}`", - ty = object_ty.display(self.db()), - ), - ); - } - false - } - SymbolAndQualifiers { - symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), - qualifiers: _, - } => { - let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) = - meta_attr_ty.class_member(db, "__set__".into()).symbol - { - let successful_call = meta_dunder_set - .try_call( - db, - CallArgumentTypes::positional([ - meta_attr_ty, - object_ty, - value_ty, - ]), - ) - .is_ok(); - - if !successful_call && emit_diagnostics { - // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Invalid assignment to data descriptor attribute `{attribute}` on type `{}` with custom `__set__` method", - object_ty.display(db) - ), - ); - } - - successful_call - } else { - ensure_assignable_to(meta_attr_ty) - }; - - let assignable_to_instance_attribute = if meta_attr_boundness - == Boundness::PossiblyUnbound - { - let (assignable, boundness) = - if let Symbol::Type(instance_attr_ty, instance_attr_boundness) = - object_ty.instance_member(db, attribute).symbol - { - ( - ensure_assignable_to(instance_attr_ty), - instance_attr_boundness, - ) - } else { - (true, Boundness::PossiblyUnbound) - }; - - if boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - assignable - } else { - true - }; - - assignable_to_meta_attr && assignable_to_instance_attribute - } - - SymbolAndQualifiers { - symbol: Symbol::Unbound, - .. - } => { - if let Symbol::Type(instance_attr_ty, instance_attr_boundness) = - object_ty.instance_member(db, attribute).symbol - { - if instance_attr_boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - ensure_assignable_to(instance_attr_ty) - } else { - let result = object_ty.try_call_dunder_with_policy( - db, - "__setattr__", - &mut CallArgumentTypes::positional([ - Type::StringLiteral(StringLiteralType::new( - db, - Box::from(attribute), - )), - value_ty, - ]), - MemberLookupPolicy::NO_INSTANCE_FALLBACK - | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ); - - match result { - Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true, - Err(CallDunderError::CallError(..)) => { - if emit_diagnostics { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( - "Can not assign object of `{}` to attribute `{attribute}` on type `{}` with custom `__setattr__` method.", - value_ty.display(db), - object_ty.display(db) - ), - ); - } - false - } - Err(CallDunderError::MethodNotAvailable) => { - if emit_diagnostics { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( - "Unresolved attribute `{}` on type `{}`.", - attribute, - object_ty.display(db) - ), - ); - } - - false - } - } - } - } - } - } - - Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { - match object_ty.class_member(db, attribute.into()) { - SymbolAndQualifiers { - symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), - qualifiers: _, - } => { - let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) = - meta_attr_ty.class_member(db, "__set__".into()).symbol - { - let successful_call = meta_dunder_set - .try_call( - db, - CallArgumentTypes::positional([ - meta_attr_ty, - object_ty, - value_ty, - ]), - ) - .is_ok(); - - if !successful_call && emit_diagnostics { - // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Invalid assignment to data descriptor attribute `{attribute}` on type `{}` with custom `__set__` method", - object_ty.display(db) - ), - ); - } - - successful_call - } else { - ensure_assignable_to(meta_attr_ty) - }; - - let assignable_to_class_attr = if meta_attr_boundness - == Boundness::PossiblyUnbound - { - let (assignable, boundness) = if let Symbol::Type( - class_attr_ty, - class_attr_boundness, - ) = object_ty - .find_name_in_mro(db, attribute) - .expect("called on Type::ClassLiteral or Type::SubclassOf") - .symbol - { - (ensure_assignable_to(class_attr_ty), class_attr_boundness) - } else { - (true, Boundness::PossiblyUnbound) - }; - - if boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - assignable - } else { - true - }; - - assignable_to_meta_attr && assignable_to_class_attr - } - SymbolAndQualifiers { - symbol: Symbol::Unbound, - .. - } => { - if let Symbol::Type(class_attr_ty, class_attr_boundness) = object_ty - .find_name_in_mro(db, attribute) - .expect("called on Type::ClassLiteral or Type::SubclassOf") - .symbol - { - if class_attr_boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - ensure_assignable_to(class_attr_ty) - } else { - let attribute_is_bound_on_instance = - object_ty.to_instance(self.db()).is_some_and(|instance| { - !instance - .instance_member(self.db(), attribute) - .symbol - .is_unbound() - }); - - // Attribute is declared or bound on instance. Forbid access from the class object - if emit_diagnostics { - if attribute_is_bound_on_instance { - self.context.report_lint_old( - &INVALID_ATTRIBUTE_ACCESS, - target, - format_args!( - "Cannot assign to instance attribute `{attribute}` from the class object `{ty}`", - ty = object_ty.display(self.db()), - )); - } else { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( - "Unresolved attribute `{}` on type `{}`.", - attribute, - object_ty.display(db) - ), - ); - } - } - - false - } - } - } - } - - Type::ModuleLiteral(module) => { - if let Symbol::Type(attr_ty, _) = module.static_member(db, attribute) { - let assignable = value_ty.is_assignable_to(db, attr_ty); - if !assignable { - report_invalid_attribute_assignment( - &self.context, - target.into(), - attr_ty, - value_ty, - attribute, - ); - } - - false - } else { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( - "Unresolved attribute `{}` on type `{}`.", - attribute, - object_ty.display(db) - ), - ); - - false - } - } - } - } - - fn infer_target_impl(&mut self, target: &ast::Expr, assigned_ty: Option>) { - match target { - ast::Expr::Name(name) => self.infer_definition(name), - ast::Expr::List(ast::ExprList { elts, .. }) - | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { - let mut assigned_tys = match assigned_ty { - Some(Type::Tuple(tuple)) => { - Either::Left(tuple.elements(self.db()).into_iter().copied()) - } - Some(_) | None => Either::Right(std::iter::empty()), - }; - - for element in elts { - self.infer_target_impl(element, assigned_tys.next()); - } - } - ast::Expr::Attribute( - attr_expr @ ast::ExprAttribute { - value: object, - ctx: ExprContext::Store, - attr, - .. - }, - ) => { - self.store_expression_type(target, Type::Never); - - let object_ty = self.infer_expression(object); - - if let Some(assigned_ty) = assigned_ty { - self.validate_attribute_assignment( - attr_expr, - object_ty, - attr.id(), - assigned_ty, - true, - ); - } - } - _ => { - // TODO: Remove this once we handle all possible assignment targets. - self.infer_expression(target); - } - } - } - - fn infer_assignment_definition( - &mut self, - assignment: &AssignmentDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let value = assignment.value(); - let target = assignment.target(); - - let value_ty = self.infer_standalone_expression(value); - - let mut target_ty = match assignment.target_kind() { - TargetKind::Sequence(unpack_position, unpack) => { - let unpacked = infer_unpack_types(self.db(), unpack); - // Only copy the diagnostics if this is the first assignment to avoid duplicating the - // unpack assignments. - if unpack_position == UnpackPosition::First { - self.context.extend(unpacked.diagnostics()); - } - - let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); - unpacked.expression_type(target_ast_id) - } - TargetKind::NameOrAttribute => { - // `TYPE_CHECKING` is a special variable that should only be assigned `False` - // at runtime, but is always considered `True` in type checking. - // See mdtest/known_constants.md#user-defined-type_checking for details. - if target.as_name_expr().map(|name| name.id.as_str()) == Some("TYPE_CHECKING") { - if !matches!( - value.as_boolean_literal_expr(), - Some(ast::ExprBooleanLiteral { value: false, .. }) - ) { - report_invalid_type_checking_constant(&self.context, target.into()); - } - Type::BooleanLiteral(true) - } else if self.in_stub() && value.is_ellipsis_literal_expr() { - Type::unknown() - } else { - value_ty - } - } - }; - - if let Some(known_instance) = target.as_name_expr().and_then(|name| { - KnownInstanceType::try_from_file_and_name(self.db(), self.file(), &name.id) - }) { - target_ty = Type::KnownInstance(known_instance); - } - - self.store_expression_type(target, target_ty); - self.add_binding(target.into(), definition, target_ty); - } - - fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) { - // assignments to non-Names are not Definitions - if matches!(*assignment.target, ast::Expr::Name(_)) { - self.infer_definition(assignment); - } else { - let ast::StmtAnnAssign { - range: _, - annotation, - value, - target, - simple: _, - } = assignment; - self.infer_annotation_expression(annotation, DeferredExpressionState::None); - self.infer_optional_expression(value.as_deref()); - self.infer_expression(target); - } - } - - /// Infer the types in an annotated assignment definition. - fn infer_annotated_assignment_definition( - &mut self, - assignment: &'db AnnotatedAssignmentDefinitionKind, - definition: Definition<'db>, - ) { - let annotation = assignment.annotation(); - let target = assignment.target(); - let value = assignment.value(); - - let mut declared_ty = self.infer_annotation_expression( - annotation, - DeferredExpressionState::from(self.are_all_types_deferred()), - ); - - if target - .as_name_expr() - .is_some_and(|name| &name.id == "TYPE_CHECKING") - { - if !KnownClass::Bool - .to_instance(self.db()) - .is_assignable_to(self.db(), declared_ty.inner_type()) - { - // annotation not assignable from `bool` is an error - report_invalid_type_checking_constant(&self.context, target.into()); - } else if self.in_stub() - && value - .as_ref() - .is_none_or(|value| value.is_ellipsis_literal_expr()) - { - // stub file assigning nothing or `...` is fine - } else if !matches!( - value - .as_ref() - .and_then(|value| value.as_boolean_literal_expr()), - Some(ast::ExprBooleanLiteral { value: false, .. }) - ) { - // otherwise, assigning something other than `False` is an error - report_invalid_type_checking_constant(&self.context, target.into()); - } - declared_ty.inner = Type::BooleanLiteral(true); - } - - // Handle various singletons. - if let Type::Instance(instance) = declared_ty.inner_type() { - if instance.class.is_known(self.db(), KnownClass::SpecialForm) { - if let Some(name_expr) = target.as_name_expr() { - if let Some(known_instance) = KnownInstanceType::try_from_file_and_name( - self.db(), - self.file(), - &name_expr.id, - ) { - declared_ty.inner = Type::KnownInstance(known_instance); - } - } - } - } - - if let Some(value) = value { - let inferred_ty = self.infer_expression(value); - let inferred_ty = if target - .as_name_expr() - .is_some_and(|name| &name.id == "TYPE_CHECKING") - { - Type::BooleanLiteral(true) - } else if self.in_stub() && value.is_ellipsis_literal_expr() { - declared_ty.inner_type() - } else { - inferred_ty - }; - self.add_declaration_with_binding( - target.into(), - definition, - &DeclaredAndInferredType::MightBeDifferent { - declared_ty, - inferred_ty, - }, - ); - } else { - if self.in_stub() { - self.add_declaration_with_binding( - target.into(), - definition, - &DeclaredAndInferredType::AreTheSame(declared_ty.inner_type()), - ); - } else { - self.add_declaration(target.into(), definition, declared_ty); - } - } - - self.infer_expression(target); - } - - fn infer_augmented_assignment_statement(&mut self, assignment: &ast::StmtAugAssign) { - if assignment.target.is_name_expr() { - self.infer_definition(assignment); - } else { - // TODO currently we don't consider assignments to non-Names to be Definitions - self.infer_augment_assignment(assignment); - } - } - - fn infer_augmented_op( - &mut self, - assignment: &ast::StmtAugAssign, - target_type: Type<'db>, - value_type: Type<'db>, - ) -> Type<'db> { - // If the target defines, e.g., `__iadd__`, infer the augmented assignment as a call to that - // dunder. - let op = assignment.op; - let db = self.db(); - - let report_unsupported_augmented_op = |ctx: &mut InferContext| { - ctx.report_lint_old( - &UNSUPPORTED_OPERATOR, - assignment, - format_args!( - "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", - target_type.display(db), - value_type.display(db) - ), - ); - }; - - // Fall back to non-augmented binary operator inference. - let mut binary_return_ty = || { - self.infer_binary_expression_type(assignment.into(), false, target_type, value_type, op) - .unwrap_or_else(|| { - report_unsupported_augmented_op(&mut self.context); - Type::unknown() - }) - }; - - match target_type { - Type::Union(union) => union.map(db, |&elem_type| { - self.infer_augmented_op(assignment, elem_type, value_type) - }), - _ => { - let call = target_type.try_call_dunder( - db, - op.in_place_dunder(), - CallArgumentTypes::positional([value_type]), - ); - - match call { - Ok(outcome) => outcome.return_type(db), - Err(CallDunderError::MethodNotAvailable) => binary_return_ty(), - Err(CallDunderError::PossiblyUnbound(outcome)) => { - UnionType::from_elements(db, [outcome.return_type(db), binary_return_ty()]) - } - Err(CallDunderError::CallError(_, bindings)) => { - report_unsupported_augmented_op(&mut self.context); - bindings.return_type(db) - } - } - } - } - } - - fn infer_augment_assignment_definition( - &mut self, - assignment: &ast::StmtAugAssign, - definition: Definition<'db>, - ) { - let target_ty = self.infer_augment_assignment(assignment); - self.add_binding(assignment.into(), definition, target_ty); - } - - fn infer_augment_assignment(&mut self, assignment: &ast::StmtAugAssign) -> Type<'db> { - let ast::StmtAugAssign { - range: _, - target, - op: _, - value, - } = assignment; - - // Resolve the target type, assuming a load context. - let target_type = match &**target { - ast::Expr::Name(name) => { - self.store_expression_type(target, Type::Never); - self.infer_name_load(name) - } - ast::Expr::Attribute(attr) => { - self.store_expression_type(target, Type::Never); - self.infer_attribute_load(attr) - } - _ => self.infer_expression(target), - }; - let value_type = self.infer_expression(value); - - self.infer_augmented_op(assignment, target_type, value_type) - } - - fn infer_type_alias_statement(&mut self, node: &ast::StmtTypeAlias) { - self.infer_definition(node); - } - - fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) { - let ast::StmtFor { - range: _, - target, - iter, - body, - orelse, - is_async: _, - } = for_statement; - - self.infer_target(target, iter, |db, iter_ty| { - // TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable - // but only if the target is a name. We should report a diagnostic here if the target isn't a name: - // `for a.x in not_iterable: ... - iter_ty.iterate(db) - }); - - self.infer_body(body); - self.infer_body(orelse); - } - - fn infer_for_statement_definition( - &mut self, - for_stmt: &ForStmtDefinitionKind<'db>, - definition: Definition<'db>, - ) { - let iterable = for_stmt.iterable(); - let target = for_stmt.target(); - - let iterable_type = self.infer_standalone_expression(iterable); - - let loop_var_value_type = if for_stmt.is_async() { - todo_type!("async iterables/iterators") - } else { - match for_stmt.target_kind() { - TargetKind::Sequence(unpack_position, unpack) => { - let unpacked = infer_unpack_types(self.db(), unpack); - if unpack_position == UnpackPosition::First { - self.context.extend(unpacked.diagnostics()); - } - let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); - unpacked.expression_type(target_ast_id) - } - TargetKind::NameOrAttribute => { - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) - }) - } - } - }; - - self.store_expression_type(target, loop_var_value_type); - self.add_binding(target.into(), definition, loop_var_value_type); - } - - fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) { - let ast::StmtWhile { - range: _, - test, - body, - orelse, - } = while_statement; - - let test_ty = self.infer_standalone_expression(test); - - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, &**test); - } - - self.infer_body(body); - self.infer_body(orelse); - } - - fn infer_import_statement(&mut self, import: &ast::StmtImport) { - let ast::StmtImport { range: _, names } = import; - - for alias in names { - self.infer_definition(alias); - } - } - - fn report_unresolved_import( - &self, - import_node: AnyNodeRef<'_>, - range: TextRange, - level: u32, - module: Option<&str>, - ) { - let is_import_reachable = self.is_reachable(import_node); - - if !is_import_reachable { - return; - } - - self.context.report_lint_old( - &UNRESOLVED_IMPORT, - range, - format_args!( - "Cannot resolve import `{}{}`", - ".".repeat(level as usize), - module.unwrap_or_default() - ), - ); - } - - fn infer_import_definition( - &mut self, - node: &ast::StmtImport, - alias: &'db ast::Alias, - definition: Definition<'db>, - ) { - let ast::Alias { - range: _, - name, - asname, - } = alias; - - // The name of the module being imported - let Some(full_module_name) = ModuleName::new(name) else { - tracing::debug!("Failed to resolve import due to invalid syntax"); - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; - - // Resolve the module being imported. - let Some(full_module_ty) = self.module_type_from_name(&full_module_name) else { - self.report_unresolved_import(node.into(), alias.range(), 0, Some(name)); - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; - - let binding_ty = if asname.is_some() { - // If we are renaming the imported module via an `as` clause, then we bind the resolved - // module's type to that name, even if that module is nested. - full_module_ty - } else if full_module_name.contains('.') { - // If there's no `as` clause and the imported module is nested, we're not going to bind - // the resolved module itself into the current scope; we're going to bind the top-most - // parent package of that module. - let topmost_parent_name = - ModuleName::new(full_module_name.components().next().unwrap()).unwrap(); - let Some(topmost_parent_ty) = self.module_type_from_name(&topmost_parent_name) else { - self.add_unknown_declaration_with_binding(alias.into(), definition); - return; - }; - topmost_parent_ty - } else { - // If there's no `as` clause and the imported module isn't nested, then the imported - // module _is_ what we bind into the current scope. - full_module_ty - }; - - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::AreTheSame(binding_ty), - ); - } - - fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { - let ast::StmtImportFrom { - range: _, - module: _, - names, - level: _, - } = import; - - for alias in names { - 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)); - } - } - } - } - - fn infer_assert_statement(&mut self, assert: &ast::StmtAssert) { - let ast::StmtAssert { - range: _, - test, - msg, - } = assert; - - let test_ty = self.infer_expression(test); - - if let Err(err) = test_ty.try_bool(self.db()) { - err.report_diagnostic(&self.context, &**test); - } - - self.infer_optional_expression(msg.as_deref()); - } - - fn infer_raise_statement(&mut self, raise: &ast::StmtRaise) { - let ast::StmtRaise { - range: _, - exc, - cause, - } = raise; - - let base_exception_type = KnownClass::BaseException.to_subclass_of(self.db()); - let base_exception_instance = KnownClass::BaseException.to_instance(self.db()); - - let can_be_raised = - UnionType::from_elements(self.db(), [base_exception_type, base_exception_instance]); - let can_be_exception_cause = - UnionType::from_elements(self.db(), [can_be_raised, Type::none(self.db())]); - - if let Some(raised) = exc { - let raised_type = self.infer_expression(raised); - - if !raised_type.is_assignable_to(self.db(), can_be_raised) { - report_invalid_exception_raised(&self.context, raised, raised_type); - } - } - - if let Some(cause) = cause { - let cause_type = self.infer_expression(cause); - - if !cause_type.is_assignable_to(self.db(), can_be_exception_cause) { - report_invalid_exception_cause(&self.context, cause, cause_type); - } - } - } - - /// 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: &ast::StmtImportFrom, - alias: &ast::Alias, - ) -> 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. - let module_ref = module - .as_ref() - .map(AnyNodeRef::from) - .unwrap_or_else(|| AnyNodeRef::from(import_from)); - let module = module.as_deref(); - - 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. - return None; - } - Err(ModuleNameResolutionError::TooManyDots) => { - tracing::debug!( - "Relative module resolution `{}` failed: too many leading dots", - format_import_from_module(*level, module), - ); - self.report_unresolved_import( - import_from.into(), - module_ref.range(), - *level, - module, - ); - return None; - } - Err(ModuleNameResolutionError::UnknownCurrentModule) => { - tracing::debug!( - "Relative module resolution `{}` failed; could not resolve file `{}` to a module", - format_import_from_module(*level, module), - self.file().path(self.db()) - ); - self.report_unresolved_import( - import_from.into(), - module_ref.range(), - *level, - module, - ); - return None; - } - }; - - let Some(module_ty) = self.module_type_from_name(&module_name) else { - self.report_unresolved_import(import_from.into(), module_ref.range(), *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; - }; - - // 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).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_old( - &POSSIBLY_UNBOUND_IMPORT, - AnyNodeRef::Alias(alias), - format_args!("Member `{name}` of module `{module_name}` is possibly unbound",), - ); - } - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::AreTheSame(ty), - ); - return; - } - - // If the module doesn't bind the symbol, check if it's a submodule. This won't get - // handled by the `Type::member` call because it relies on the semantic index's - // `imported_modules` set. The semantic index does not include information about - // `from...import` statements because there are two things it cannot determine while only - // inspecting the content of the current file: - // - // - whether the imported symbol is an attribute or submodule - // - whether the containing file is in a module or a package (needed to correctly resolve - // relative imports) - // - // The first would be solvable by making it a _potentially_ imported modules set. The - // second is not. - // - // Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute - // check here when inferring types for a `from...import` statement. - if let Some(submodule_name) = ModuleName::new(name) { - let mut full_submodule_name = module_name.clone(); - full_submodule_name.extend(&submodule_name); - if let Some(submodule_ty) = self.module_type_from_name(&full_submodule_name) { - self.add_declaration_with_binding( - alias.into(), - definition, - &DeclaredAndInferredType::AreTheSame(submodule_ty), - ); - return; - } - } - - if &alias.name != "*" { - let is_import_reachable = self.is_reachable(import_from); - - if is_import_reachable { - self.context.report_lint_old( - &UNRESOLVED_IMPORT, - AnyNodeRef::Alias(alias), - format_args!("Module `{module_name}` has no member `{name}`",), - ); - } - } - - self.add_unknown_declaration_with_binding(alias.into(), definition); - } - - fn infer_return_statement(&mut self, ret: &ast::StmtReturn) { - if let Some(ty) = self.infer_optional_expression(ret.value.as_deref()) { - let range = ret - .value - .as_ref() - .map_or(ret.range(), |value| value.range()); - self.record_return_type(ty, range); - } else { - self.record_return_type(KnownClass::NoneType.to_instance(self.db()), ret.range()); - } - } - - fn infer_delete_statement(&mut self, delete: &ast::StmtDelete) { - let ast::StmtDelete { range: _, targets } = delete; - for target in targets { - self.infer_expression(target); - } - } - - fn module_type_from_name(&self, module_name: &ModuleName) -> Option> { - resolve_module(self.db(), module_name) - .map(|module| Type::module_literal(self.db(), self.file(), module)) - } - - fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> { - let ast::Decorator { - range: _, - expression, - } = decorator; - - self.infer_expression(expression) - } - - fn parse_arguments(arguments: &ast::Arguments) -> CallArguments<'_> { - arguments - .arguments_source_order() - .map(|arg_or_keyword| { - match arg_or_keyword { - ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, - // TODO diagnostic if after a keyword argument - _ => Argument::Positional, - }, - ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { - if let Some(arg) = arg { - Argument::Keyword(&arg.id) - } else { - // TODO diagnostic if not last - Argument::Keywords - } - } - } - }) - .collect() - } - - fn infer_argument_types<'a>( - &mut self, - ast_arguments: &ast::Arguments, - arguments: CallArguments<'a>, - argument_forms: &[Option], - ) -> 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, - ) -> 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> { - expression.map(|expr| self.infer_expression(expr)) - } - - #[track_caller] - fn infer_expression(&mut self, expression: &ast::Expr) -> Type<'db> { - debug_assert_eq!( - self.index.try_expression(expression), - None, - "Calling `self.infer_expression` on a standalone-expression is not allowed because it can lead to double-inference. Use `self.infer_standalone_expression` instead." - ); - - self.infer_expression_impl(expression) - } - - fn infer_standalone_expression(&mut self, expression: &ast::Expr) -> Type<'db> { - let standalone_expression = self.index.expression(expression); - let types = infer_expression_types(self.db(), standalone_expression); - self.extend(types); - self.expression_type(expression) - } - - fn infer_expression_impl(&mut self, expression: &ast::Expr) -> Type<'db> { - let ty = match expression { - ast::Expr::NoneLiteral(ast::ExprNoneLiteral { range: _ }) => Type::none(self.db()), - ast::Expr::NumberLiteral(literal) => self.infer_number_literal_expression(literal), - ast::Expr::BooleanLiteral(literal) => self.infer_boolean_literal_expression(literal), - ast::Expr::StringLiteral(literal) => self.infer_string_literal_expression(literal), - ast::Expr::BytesLiteral(bytes_literal) => { - self.infer_bytes_literal_expression(bytes_literal) - } - ast::Expr::FString(fstring) => self.infer_fstring_expression(fstring), - ast::Expr::EllipsisLiteral(literal) => self.infer_ellipsis_literal_expression(literal), - ast::Expr::Tuple(tuple) => self.infer_tuple_expression(tuple), - ast::Expr::List(list) => self.infer_list_expression(list), - ast::Expr::Set(set) => self.infer_set_expression(set), - ast::Expr::Dict(dict) => self.infer_dict_expression(dict), - ast::Expr::Generator(generator) => self.infer_generator_expression(generator), - ast::Expr::ListComp(listcomp) => self.infer_list_comprehension_expression(listcomp), - ast::Expr::DictComp(dictcomp) => self.infer_dict_comprehension_expression(dictcomp), - ast::Expr::SetComp(setcomp) => self.infer_set_comprehension_expression(setcomp), - ast::Expr::Name(name) => self.infer_name_expression(name), - ast::Expr::Attribute(attribute) => self.infer_attribute_expression(attribute), - ast::Expr::UnaryOp(unary_op) => self.infer_unary_expression(unary_op), - ast::Expr::BinOp(binary) => self.infer_binary_expression(binary), - ast::Expr::BoolOp(bool_op) => self.infer_boolean_expression(bool_op), - ast::Expr::Compare(compare) => self.infer_compare_expression(compare), - ast::Expr::Subscript(subscript) => self.infer_subscript_expression(subscript), - ast::Expr::Slice(slice) => self.infer_slice_expression(slice), - ast::Expr::Named(named) => self.infer_named_expression(named), - ast::Expr::If(if_expression) => self.infer_if_expression(if_expression), - ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression), - ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression), - ast::Expr::Starred(starred) => self.infer_starred_expression(starred), - ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression), - ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from), - ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression), - ast::Expr::IpyEscapeCommand(_) => { - todo_type!("Ipy escape command support") - } - }; - - self.store_expression_type(expression, ty); - - ty - } - - fn store_expression_type(&mut self, expression: &impl HasScopedExpressionId, ty: Type<'db>) { - if self.deferred_state.in_string_annotation() { - // Avoid storing the type of expressions that are part of a string annotation because - // the expression ids don't exists in the semantic index. Instead, we'll store the type - // on the string expression itself that represents the annotation. - return; - } - let expr_id = expression.scoped_expression_id(self.db(), self.scope()); - let previous = self.types.expressions.insert(expr_id, ty); - assert_eq!(previous, None); - } - - fn infer_number_literal_expression(&mut self, literal: &ast::ExprNumberLiteral) -> Type<'db> { - let ast::ExprNumberLiteral { range: _, value } = literal; - let db = self.db(); - - match value { - ast::Number::Int(n) => n - .as_i64() - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), - ast::Number::Float(_) => KnownClass::Float.to_instance(db), - ast::Number::Complex { .. } => KnownClass::Complex.to_instance(db), - } - } - - #[allow(clippy::unused_self)] - fn infer_boolean_literal_expression(&mut self, literal: &ast::ExprBooleanLiteral) -> Type<'db> { - let ast::ExprBooleanLiteral { range: _, value } = literal; - - Type::BooleanLiteral(*value) - } - - fn infer_string_literal_expression(&mut self, literal: &ast::ExprStringLiteral) -> Type<'db> { - if literal.value.len() <= Self::MAX_STRING_LITERAL_SIZE { - Type::string_literal(self.db(), literal.value.to_str()) - } else { - Type::LiteralString - } - } - - fn infer_bytes_literal_expression(&mut self, literal: &ast::ExprBytesLiteral) -> Type<'db> { - // TODO: ignoring r/R prefixes for now, should normalize bytes values - let bytes: Vec = literal.value.bytes().collect(); - Type::bytes_literal(self.db(), &bytes) - } - - fn infer_fstring_expression(&mut self, fstring: &ast::ExprFString) -> Type<'db> { - let ast::ExprFString { range: _, value } = fstring; - - let mut collector = StringPartsCollector::new(); - for part in value { - // Make sure we iter through every parts to infer all sub-expressions. The `collector` - // struct ensures we don't allocate unnecessary strings. - match part { - ast::FStringPart::Literal(literal) => { - collector.push_str(&literal.value); - } - ast::FStringPart::FString(fstring) => { - for element in &fstring.elements { - match element { - ast::FStringElement::Expression(expression) => { - let ast::FStringExpressionElement { - range: _, - expression, - debug_text: _, - conversion, - format_spec, - } = expression; - let ty = self.infer_expression(expression); - - if let Some(ref format_spec) = format_spec { - for element in format_spec.elements.expressions() { - self.infer_expression(&element.expression); - } - } - - // TODO: handle format specifiers by calling a method - // (`Type::format`?) that handles the `__format__` method. - // Conversion flags should be handled before calling `__format__`. - // https://docs.python.org/3/library/string.html#format-string-syntax - if !conversion.is_none() || format_spec.is_some() { - collector.add_expression(); - } else { - if let Type::StringLiteral(literal) = ty.str(self.db()) { - collector.push_str(literal.value(self.db())); - } else { - collector.add_expression(); - } - } - } - ast::FStringElement::Literal(literal) => { - collector.push_str(&literal.value); - } - } - } - } - } - } - collector.string_type(self.db()) - } - - fn infer_ellipsis_literal_expression( - &mut self, - _literal: &ast::ExprEllipsisLiteral, - ) -> Type<'db> { - KnownClass::EllipsisType.to_instance(self.db()) - } - - fn infer_tuple_expression(&mut self, tuple: &ast::ExprTuple) -> Type<'db> { - let ast::ExprTuple { - range: _, - elts, - ctx: _, - parenthesized: _, - } = tuple; - - // Collecting all elements is necessary to infer all sub-expressions even if some - // element types are `Never` (which leads `from_elements` to return early without - // consuming the whole iterator). - let element_types: Vec<_> = elts.iter().map(|elt| self.infer_expression(elt)).collect(); - - TupleType::from_elements(self.db(), element_types) - } - - fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> { - let ast::ExprList { - range: _, - elts, - ctx: _, - } = list; - - for elt in elts { - self.infer_expression(elt); - } - - // TODO generic - KnownClass::List.to_instance(self.db()) - } - - fn infer_set_expression(&mut self, set: &ast::ExprSet) -> Type<'db> { - let ast::ExprSet { range: _, elts } = set; - - for elt in elts { - self.infer_expression(elt); - } - - // TODO generic - KnownClass::Set.to_instance(self.db()) - } - - fn infer_dict_expression(&mut self, dict: &ast::ExprDict) -> Type<'db> { - let ast::ExprDict { range: _, items } = dict; - - for item in items { - self.infer_optional_expression(item.key.as_ref()); - self.infer_expression(&item.value); - } - - // TODO generic - KnownClass::Dict.to_instance(self.db()) - } - - /// Infer the type of the `iter` expression of the first comprehension. - fn infer_first_comprehension_iter(&mut self, comprehensions: &[ast::Comprehension]) { - let mut comprehensions_iter = comprehensions.iter(); - let Some(first_comprehension) = comprehensions_iter.next() else { - unreachable!("Comprehension must contain at least one generator"); - }; - self.infer_standalone_expression(&first_comprehension.iter); - } - - fn infer_generator_expression(&mut self, generator: &ast::ExprGenerator) -> Type<'db> { - let ast::ExprGenerator { - range: _, - elt: _, - generators, - parenthesized: _, - } = generator; - - self.infer_first_comprehension_iter(generators); - - todo_type!("generator type") - } - - fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> { - let ast::ExprListComp { - range: _, - elt: _, - generators, - } = listcomp; - - self.infer_first_comprehension_iter(generators); - - todo_type!("list comprehension type") - } - - fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> { - let ast::ExprDictComp { - range: _, - key: _, - value: _, - generators, - } = dictcomp; - - self.infer_first_comprehension_iter(generators); - - todo_type!("dict comprehension type") - } - - fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> { - let ast::ExprSetComp { - range: _, - elt: _, - generators, - } = setcomp; - - self.infer_first_comprehension_iter(generators); - - todo_type!("set comprehension type") - } - - fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) { - let ast::ExprGenerator { - range: _, - elt, - generators, - parenthesized: _, - } = generator; - - self.infer_expression(elt); - self.infer_comprehensions(generators); - } - - fn infer_list_comprehension_expression_scope(&mut self, listcomp: &ast::ExprListComp) { - let ast::ExprListComp { - range: _, - elt, - generators, - } = listcomp; - - self.infer_expression(elt); - self.infer_comprehensions(generators); - } - - fn infer_dict_comprehension_expression_scope(&mut self, dictcomp: &ast::ExprDictComp) { - let ast::ExprDictComp { - range: _, - key, - value, - generators, - } = dictcomp; - - self.infer_expression(key); - self.infer_expression(value); - self.infer_comprehensions(generators); - } - - fn infer_set_comprehension_expression_scope(&mut self, setcomp: &ast::ExprSetComp) { - let ast::ExprSetComp { - range: _, - elt, - generators, - } = setcomp; - - self.infer_expression(elt); - self.infer_comprehensions(generators); - } - - fn infer_comprehensions(&mut self, comprehensions: &[ast::Comprehension]) { - let mut comprehensions_iter = comprehensions.iter(); - let Some(first_comprehension) = comprehensions_iter.next() else { - unreachable!("Comprehension must contain at least one generator"); - }; - self.infer_comprehension(first_comprehension, true); - for comprehension in comprehensions_iter { - self.infer_comprehension(comprehension, false); - } - } - - fn infer_comprehension(&mut self, comprehension: &ast::Comprehension, is_first: bool) { - let ast::Comprehension { - range: _, - target, - iter, - ifs, - is_async: _, - } = comprehension; - - if !is_first { - self.infer_standalone_expression(iter); - } - // TODO more complex assignment targets - if let ast::Expr::Name(name) = target { - self.infer_definition(name); - } else { - self.infer_expression(target); - } - for expr in ifs { - self.infer_expression(expr); - } - } - - fn infer_comprehension_definition( - &mut self, - iterable: &ast::Expr, - target: &ast::ExprName, - is_first: bool, - is_async: bool, - definition: Definition<'db>, - ) { - let expression = self.index.expression(iterable); - let result = infer_expression_types(self.db(), expression); - - // Two things are different if it's the first comprehension: - // (1) We must lookup the `ScopedExpressionId` of the iterable expression in the outer scope, - // because that's the scope we visit it in in the semantic index builder - // (2) We must *not* call `self.extend()` on the result of the type inference, - // because `ScopedExpressionId`s are only meaningful within their own scope, so - // we'd add types for random wrong expressions in the current scope - let iterable_type = if is_first { - let lookup_scope = self - .index - .parent_scope_id(self.scope().file_scope_id(self.db())) - .expect("A comprehension should never be the top-level scope") - .to_scope_id(self.db(), self.file()); - result.expression_type(iterable.scoped_expression_id(self.db(), lookup_scope)) - } else { - self.extend(result); - result.expression_type(iterable.scoped_expression_id(self.db(), self.scope())) - }; - - let target_type = if is_async { - // TODO: async iterables/iterators! -- Alex - todo_type!("async iterables/iterators") - } else { - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) - }) - }; - - self.types.expressions.insert( - target.scoped_expression_id(self.db(), self.scope()), - target_type, - ); - self.add_binding(target.into(), definition, target_type); - } - - 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.expect_single_definition(named); - let result = infer_definition_types(self.db(), definition); - self.extend(result); - result.binding_type(definition) - } else { - // For syntactically invalid targets, we still need to run type inference: - self.infer_expression(&named.target); - self.infer_expression(&named.value); - Type::unknown() - } - } - - fn infer_named_expression_definition( - &mut self, - named: &ast::ExprNamed, - definition: Definition<'db>, - ) -> Type<'db> { - let ast::ExprNamed { - range: _, - target, - value, - } = named; - - let value_ty = self.infer_expression(value); - self.infer_expression(target); - - self.add_binding(named.into(), definition, value_ty); - - value_ty - } - - fn infer_if_expression(&mut self, if_expression: &ast::ExprIf) -> Type<'db> { - let ast::ExprIf { - range: _, - test, - body, - orelse, - } = if_expression; - - let test_ty = self.infer_standalone_expression(test); - let body_ty = self.infer_expression(body); - let orelse_ty = self.infer_expression(orelse); - - match test_ty.try_bool(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, &**test); - err.fallback_truthiness() - }) { - Truthiness::AlwaysTrue => body_ty, - Truthiness::AlwaysFalse => orelse_ty, - Truthiness::Ambiguous => UnionType::from_elements(self.db(), [body_ty, orelse_ty]), - } - } - - fn infer_lambda_body(&mut self, lambda_expression: &ast::ExprLambda) { - self.infer_expression(&lambda_expression.body); - } - - fn infer_lambda_expression(&mut self, lambda_expression: &ast::ExprLambda) -> Type<'db> { - let ast::ExprLambda { - range: _, - parameters, - body: _, - } = lambda_expression; - - let parameters = if let Some(parameters) = parameters { - let positional_only = parameters - .posonlyargs - .iter() - .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::>(); - let positional_or_keyword = parameters - .args - .iter() - .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::>(); - let variadic = parameters - .vararg - .as_ref() - .map(|param| Parameter::variadic(param.name().id.clone())); - let keyword_only = parameters - .kwonlyargs - .iter() - .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::>(); - let keyword_variadic = parameters - .kwarg - .as_ref() - .map(|param| Parameter::keyword_variadic(param.name().id.clone())); - - Parameters::new( - positional_only - .into_iter() - .chain(positional_or_keyword) - .chain(variadic) - .chain(keyword_only) - .chain(keyword_variadic), - ) - } else { - Parameters::empty() - }; - - // TODO: Useful inference of a lambda's return type will require a different approach, - // which does the inference of the body expression based on arguments at each call site, - // rather than eagerly computing a return type without knowing the argument types. - Type::Callable(CallableType::new( - self.db(), - Signature::new(parameters, Some(Type::unknown())), - )) - } - - /// Returns the type of the first parameter if the given scope is function-like (i.e. function or lambda). - /// Returns `None` if the scope is not function-like, or has no parameters. - fn first_param_type_in_scope(&self, scope: ScopeId) -> Option> { - let first_param = match scope.node(self.db()) { - NodeWithScopeKind::Function(f) => f.parameters.iter().next(), - NodeWithScopeKind::Lambda(l) => l.parameters.as_ref()?.iter().next(), - _ => None, - }?; - - let definition = self.index.expect_single_definition(first_param); - - Some(infer_definition_types(self.db(), definition).binding_type(definition)) - } - - /// Returns the type of the nearest enclosing class for the given scope. - /// - /// This function walks up the ancestor scopes starting from the given scope, - /// and finds the closest class definition. - /// - /// Returns `None` if no enclosing class is found.a - fn enclosing_class_symbol(&self, scope: ScopeId) -> Option> { - self.index - .ancestor_scopes(scope.file_scope_id(self.db())) - .find_map(|(_, ancestor_scope)| { - if let NodeWithScopeKind::Class(class) = ancestor_scope.node() { - let definition = self.index.expect_single_definition(class.node()); - let result = infer_definition_types(self.db(), definition); - - Some(result.declaration_type(definition).inner_type()) - } else { - None - } - }) - } - - fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> { - let ast::ExprCall { - range: _, - func, - 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 callable_type = self.infer_expression(func); - - // For class literals we model the entire class instantiation logic, so it is handled - // in a separate function. For some known classes we have manual signatures defined and use - // the `try_call` path below. - // TODO: it should be possible to move these special cases into the `try_call_constructor` - // path instead, or even remove some entirely once we support overloads fully. - let (call_constructor, known_class) = match callable_type { - Type::ClassLiteral(class) => (true, class.known(self.db())), - Type::GenericAlias(generic) => (true, ClassType::Generic(generic).known(self.db())), - Type::SubclassOf(subclass) => ( - true, - subclass - .subclass_of() - .into_class() - .and_then(|class| class.known(self.db())), - ), - _ => (false, None), - }; - - if call_constructor - && !matches!( - known_class, - Some( - KnownClass::Bool - | KnownClass::Str - | KnownClass::Type - | KnownClass::Object - | KnownClass::Property - | KnownClass::Super - ) - ) - { - let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; - let call_argument_types = - self.infer_argument_types(arguments, call_arguments, &argument_forms); - - return callable_type - .try_call_constructor(self.db(), call_argument_types) - .unwrap_or_else(|err| { - err.report_diagnostic(&self.context, callable_type, call_expression.into()); - err.return_type() - }); - } - - let signatures = callable_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); - - match bindings.check_types(self.db(), &mut call_argument_types) { - Ok(mut bindings) => { - for binding in &mut bindings { - let binding_type = binding.callable_type; - let Some((_, overload)) = binding.matching_overload_mut() else { - continue; - }; - - match binding_type { - Type::FunctionLiteral(function_literal) => { - let Some(known_function) = function_literal.known(self.db()) else { - continue; - }; - - match known_function { - KnownFunction::RevealType => { - if let [Some(revealed_type)] = overload.parameter_types() { - if let Some(builder) = self.context.report_diagnostic( - DiagnosticId::RevealedType, - Severity::Info, - ) { - let mut diag = builder.into_diagnostic("Revealed type"); - let span = self.context.span(call_expression); - diag.annotate(Annotation::primary(span).message( - format_args!( - "`{}`", - revealed_type.display(self.db()) - ), - )); - } - } - } - KnownFunction::AssertType => { - 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_old( - &TYPE_ASSERTION_FAILURE, - call_expression, - format_args!( - "Actual type `{}` is not the same as asserted type `{}`", - actual_ty.display(self.db()), - asserted_ty.display(self.db()), - ), - ); - } - } - } - KnownFunction::AssertNever => { - if let [Some(actual_ty)] = overload.parameter_types() { - if !actual_ty.is_equivalent_to(self.db(), Type::Never) { - self.context.report_lint_old( - &TYPE_ASSERTION_FAILURE, - call_expression, - format_args!( - "Expected type `Never`, got `{}` instead", - actual_ty.display(self.db()), - ), - ); - } - } - } - KnownFunction::StaticAssert => { - if let [Some(parameter_ty), message] = - overload.parameter_types() - { - let truthiness = match parameter_ty.try_bool(self.db()) { - Ok(truthiness) => truthiness, - Err(err) => { - let condition = arguments - .find_argument("condition", 0) - .map(|argument| match argument { - ruff_python_ast::ArgOrKeyword::Arg( - expr, - ) => ast::AnyNodeRef::from(expr), - ruff_python_ast::ArgOrKeyword::Keyword( - keyword, - ) => ast::AnyNodeRef::from(keyword), - }) - .unwrap_or(ast::AnyNodeRef::from( - call_expression, - )); - - err.report_diagnostic(&self.context, condition); - - continue; - } - }; - - if !truthiness.is_always_true() { - if let Some(message) = message - .and_then(Type::into_string_literal) - .map(|s| &**s.value(self.db())) - { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!( - "Static assertion error: {message}" - ), - ); - } else if *parameter_ty == Type::BooleanLiteral(false) { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!("Static assertion error: argument evaluates to `False`"), - ); - } else if truthiness.is_always_false() { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!( - "Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", - parameter_ty=parameter_ty.display(self.db()) - ), - ); - } else { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!( - "Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", - parameter_ty=parameter_ty.display(self.db()) - ), - ); - } - } - } - } - KnownFunction::Cast => { - if let [Some(casted_type), Some(source_type)] = - overload.parameter_types() - { - let db = self.db(); - if (source_type.is_equivalent_to(db, *casted_type) - || source_type.normalized(db) - == casted_type.normalized(db)) - && !source_type.contains_todo(db) - { - self.context.report_lint_old( - &REDUNDANT_CAST, - call_expression, - format_args!( - "Value is already of type `{}`", - casted_type.display(db), - ), - ); - } - } - } - _ => {} - } - } - Type::ClassLiteral(class) - if class.is_known(self.db(), KnownClass::Super) => - { - // Handle the case where `super()` is called with no arguments. - // In this case, we need to infer the two arguments: - // 1. The nearest enclosing class - // 2. The first parameter of the current function (typically `self` or `cls`) - match overload.parameter_types() { - [] => { - let scope = self.scope(); - - let Some(enclosing_class) = self.enclosing_class_symbol(scope) - else { - overload.set_return_type(Type::unknown()); - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic( - &self.context, - call_expression.into(), - ); - continue; - }; - - let Some(first_param) = self.first_param_type_in_scope(scope) - else { - overload.set_return_type(Type::unknown()); - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic( - &self.context, - call_expression.into(), - ); - continue; - }; - - let bound_super = BoundSuperType::build( - self.db(), - enclosing_class, - first_param, - ) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - call_expression.into(), - ); - Type::unknown() - }); - - overload.set_return_type(bound_super); - } - [Some(pivot_class_type), Some(owner_type)] => { - let bound_super = BoundSuperType::build( - self.db(), - *pivot_class_type, - *owner_type, - ) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - call_expression.into(), - ); - Type::unknown() - }); - - overload.set_return_type(bound_super); - } - _ => (), - } - } - _ => (), - } - } - bindings.return_type(self.db()) - } - - Err(CallError(_, bindings)) => { - bindings.report_diagnostics(&self.context, call_expression.into()); - bindings.return_type(self.db()) - } - } - } - - fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { - let ast::ExprStarred { - range: _, - value, - ctx: _, - } = starred; - - let iterable_type = self.infer_expression(value); - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - err.fallback_element_type(self.db()) - }); - - // TODO - todo_type!("starred expression") - } - - fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> { - let ast::ExprYield { range: _, value } = yield_expression; - self.infer_optional_expression(value.as_deref()); - todo_type!("yield expressions") - } - - fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> { - let ast::ExprYieldFrom { range: _, value } = yield_from; - - let iterable_type = self.infer_expression(value); - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - err.fallback_element_type(self.db()) - }); - - // TODO get type from `ReturnType` of generator - todo_type!("Generic `typing.Generator` type") - } - - fn infer_await_expression(&mut self, await_expression: &ast::ExprAwait) -> Type<'db> { - let ast::ExprAwait { range: _, value } = await_expression; - self.infer_expression(value); - todo_type!("generic `typing.Awaitable` type") - } - - /// Infer the type of a [`ast::ExprName`] expression, assuming a load context. - fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> { - let ast::ExprName { - range: _, - id: symbol_name, - ctx: _, - } = name_node; - - let db = self.db(); - let scope = self.scope(); - let file_scope_id = scope.file_scope_id(db); - let symbol_table = self.index.symbol_table(file_scope_id); - let use_def = self.index.use_def_map(file_scope_id); - - // If we're inferring types of deferred expressions, always treat them as public symbols - let local_scope_symbol = if self.is_deferred() { - if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) { - symbol_from_bindings(db, use_def.public_bindings(symbol_id)) - } else { - assert!( - self.deferred_state.in_string_annotation(), - "Expected the symbol table to create a symbol for every Name node" - ); - Symbol::Unbound - } - } else { - let use_id = name_node.scoped_use_id(db, scope); - symbol_from_bindings(db, use_def.bindings_at_use(use_id)) - }; - - let symbol = SymbolAndQualifiers::from(local_scope_symbol).or_fall_back_to(db, || { - let has_bindings_in_this_scope = match symbol_table.symbol_by_name(symbol_name) { - Some(symbol) => symbol.is_bound(), - None => { - assert!( - self.deferred_state.in_string_annotation(), - "Expected the symbol table to create a symbol for every Name node" - ); - false - } - }; - - // If it's a function-like scope and there is one or more binding in this scope (but - // none of those bindings are visible from where we are in the control flow), we cannot - // fallback to any bindings in enclosing scopes. As such, we can immediately short-circuit - // here and return `Symbol::Unbound`. - // - // This is because Python is very strict in its categorisation of whether a variable is - // a local variable or not in function-like scopes. If a variable has any bindings in a - // function-like scope, it is considered a local variable; it never references another - // scope. (At runtime, it would use the `LOAD_FAST` opcode.) - if has_bindings_in_this_scope && scope.is_function_like(db) { - return Symbol::Unbound.into(); - } - - let current_file = self.file(); - - // Walk up parent scopes looking for a possible enclosing scope that may have a - // definition of this name visible to us (would be `LOAD_DEREF` at runtime.) - // Note that we skip the scope containing the use that we are resolving, since we - // already looked for the symbol there up above. - for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) { - // Class scopes are not visible to nested scopes, and we need to handle global - // scope differently (because an unbound name there falls back to builtins), so - // check only function-like scopes. - // There is one exception to this rule: type parameter scopes can see - // names defined in an immediately-enclosing class scope. - let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file); - let is_immediately_enclosing_scope = scope.is_type_parameter(db) - && scope - .scope(db) - .parent() - .is_some_and(|parent| parent == enclosing_scope_file_id); - if !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope { - continue; - } - - // If the reference is in a nested eager scope, we need to look for the symbol at - // the point where the previous enclosing scope was defined, instead of at the end - // of the scope. (Note that the semantic index builder takes care of only - // registering eager bindings for nested scopes that are actually eager, and for - // enclosing scopes that actually contain bindings that we should use when - // resolving the reference.) - if !self.is_deferred() { - match self.index.eager_bindings( - enclosing_scope_file_id, - symbol_name, - file_scope_id, - ) { - EagerBindingsResult::Found(bindings) => { - return symbol_from_bindings(db, bindings).into(); - } - // There are no visible bindings here. - // Don't fall back to non-eager symbol resolution. - EagerBindingsResult::NotFound => { - continue; - } - EagerBindingsResult::NoLongerInEagerContext => {} - } - } - - let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id); - let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(symbol_name) - else { - continue; - }; - if enclosing_symbol.is_bound() { - // We can return early here, because the nearest function-like scope that - // defines a name must be the only source for the nonlocal reference (at - // runtime, it is the scope that creates the cell for our closure.) If the name - // isn't bound in that scope, we should get an unbound name, not continue - // falling back to other scopes / globals / builtins. - return symbol(db, enclosing_scope_id, symbol_name); - } - } - - SymbolAndQualifiers::from(Symbol::Unbound) - // No nonlocal binding? Check the module's explicit globals. - // Avoid infinite recursion if `self.scope` already is the module's global scope. - .or_fall_back_to(db, || { - if file_scope_id.is_global() { - return Symbol::Unbound.into(); - } - - if !self.is_deferred() { - match self.index.eager_bindings( - FileScopeId::global(), - symbol_name, - file_scope_id, - ) { - EagerBindingsResult::Found(bindings) => { - return symbol_from_bindings(db, bindings).into(); - } - // There are no visible bindings here. - EagerBindingsResult::NotFound => { - return Symbol::Unbound.into(); - } - EagerBindingsResult::NoLongerInEagerContext => {} - } - } - - explicit_global_symbol(db, self.file(), symbol_name) - }) - // Not found in the module's explicitly declared global symbols? - // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. - // These are looked up as attributes on `types.ModuleType`. - .or_fall_back_to(db, || module_type_implicit_global_symbol(db, symbol_name)) - // Not found in globals? Fallback to builtins - // (without infinite recursion if we're already in builtins.) - .or_fall_back_to(db, || { - if Some(self.scope()) == builtins_module_scope(db) { - Symbol::Unbound.into() - } else { - builtins_symbol(db, symbol_name) - } - }) - // Still not found? It might be `reveal_type`... - .or_fall_back_to(db, || { - if symbol_name == "reveal_type" { - self.context.report_lint_old( - &UNDEFINED_REVEAL, - name_node, - format_args!( - "`reveal_type` used without importing it; \ - this is allowed for debugging convenience but will fail at runtime" - ), - ); - typing_extensions_symbol(db, symbol_name) - } else { - Symbol::Unbound.into() - } - }) - }); - - symbol - .unwrap_with_diagnostic(|lookup_error| match lookup_error { - LookupError::Unbound(qualifiers) => { - if self.is_reachable(name_node) { - report_unresolved_reference(&self.context, name_node); - } - TypeAndQualifiers::new(Type::unknown(), qualifiers) - } - LookupError::PossiblyUnbound(type_when_bound) => { - if self.is_reachable(name_node) { - report_possibly_unresolved_reference(&self.context, name_node); - } - type_when_bound - } - }) - .inner_type() - } - - fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> { - match name.ctx { - ExprContext::Load => self.infer_name_load(name), - ExprContext::Store | ExprContext::Del => Type::Never, - ExprContext::Invalid => Type::unknown(), - } - } - - /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context. - fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { - let ast::ExprAttribute { - value, - attr, - range: _, - ctx: _, - } = attribute; - - let value_type = self.infer_expression(value); - let db = self.db(); - - value_type - .member(db, &attr.id) - .unwrap_with_diagnostic(|lookup_error| match lookup_error { - LookupError::Unbound(_) => { - let report_unresolved_attribute = self.is_reachable(attribute); - - if report_unresolved_attribute { - let bound_on_instance = match value_type { - Type::ClassLiteral(class) => { - !class.instance_member(db, None, attr).symbol.is_unbound() - } - Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { - match subclass_of.subclass_of() { - ClassBase::Class(class) => { - !class.instance_member(db, attr).symbol.is_unbound() - } - ClassBase::Dynamic(_) => unreachable!( - "Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol" - ), - } - } - _ => false, - }; - - if bound_on_instance { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - attribute, - format_args!( - "Attribute `{}` can only be accessed on instances, not on the class object `{}` itself.", - attr.id, - value_type.display(db) - ), - ); - } else { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - attribute, - format_args!( - "Type `{}` has no attribute `{}`", - value_type.display(db), - attr.id - ), - ); - } - } - - Type::unknown().into() - } - LookupError::PossiblyUnbound(type_when_bound) => { - report_possibly_unbound_attribute( - &self.context, - attribute, - &attr.id, - value_type, - ); - - type_when_bound - } - }).inner_type() - } - - fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { - let ast::ExprAttribute { - value, - attr: _, - range: _, - ctx, - } = attribute; - - match ctx { - ExprContext::Load => self.infer_attribute_load(attribute), - ExprContext::Store | ExprContext::Del => { - self.infer_expression(value); - Type::Never - } - ExprContext::Invalid => { - self.infer_expression(value); - Type::unknown() - } - } - } - - fn infer_unary_expression(&mut self, unary: &ast::ExprUnaryOp) -> Type<'db> { - let ast::ExprUnaryOp { - range: _, - op, - operand, - } = unary; - - let operand_type = self.infer_expression(operand); - - match (op, operand_type) { - (_, Type::Dynamic(_)) => operand_type, - (_, Type::Never) => Type::Never, - - (ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), - (ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), - (ast::UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value), - - (ast::UnaryOp::UAdd, Type::BooleanLiteral(bool)) => Type::IntLiteral(i64::from(bool)), - (ast::UnaryOp::USub, Type::BooleanLiteral(bool)) => Type::IntLiteral(-i64::from(bool)), - (ast::UnaryOp::Invert, Type::BooleanLiteral(bool)) => { - Type::IntLiteral(!i64::from(bool)) - } - - (ast::UnaryOp::Not, ty) => ty - .try_bool(self.db()) - .unwrap_or_else(|err| { - err.report_diagnostic(&self.context, unary); - err.fallback_truthiness() - }) - .negate() - .into_type(self.db()), - ( - op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert), - Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::BoundMethod(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::GenericAlias(_) - | Type::SubclassOf(_) - | Type::Instance(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::Union(_) - | Type::Intersection(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::StringLiteral(_) - | Type::LiteralString - | Type::BytesLiteral(_) - | Type::SliceLiteral(_) - | Type::Tuple(_) - | Type::BoundSuper(_) - | Type::TypeVar(_), - ) => { - let unary_dunder_method = match op { - ast::UnaryOp::Invert => "__invert__", - ast::UnaryOp::UAdd => "__pos__", - ast::UnaryOp::USub => "__neg__", - ast::UnaryOp::Not => { - unreachable!("Not operator is handled in its own case"); - } - }; - - match operand_type.try_call_dunder( - self.db(), - unary_dunder_method, - CallArgumentTypes::none(), - ) { - Ok(outcome) => outcome.return_type(self.db()), - Err(e) => { - self.context.report_lint_old( - &UNSUPPORTED_OPERATOR, - unary, - format_args!( - "Unary operator `{op}` is unsupported for type `{}`", - operand_type.display(self.db()), - ), - ); - e.fallback_return_type(self.db()) - } - } - } - } - } - - fn infer_binary_expression(&mut self, binary: &ast::ExprBinOp) -> Type<'db> { - let ast::ExprBinOp { - left, - op, - right, - range: _, - } = binary; - - let left_ty = self.infer_expression(left); - let right_ty = self.infer_expression(right); - - self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op) - .unwrap_or_else(|| { - self.context.report_lint_old( - &UNSUPPORTED_OPERATOR, - binary, - format_args!( - "Operator `{op}` is unsupported between objects of type `{}` and `{}`", - left_ty.display(self.db()), - right_ty.display(self.db()) - ), - ); - Type::unknown() - }) - } - - fn infer_binary_expression_type( - &mut self, - node: AnyNodeRef<'_>, - mut emitted_division_by_zero_diagnostic: bool, - left_ty: Type<'db>, - right_ty: Type<'db>, - op: ast::Operator, - ) -> Option> { - // Check for division by zero; this doesn't change the inferred type for the expression, but - // may emit a diagnostic - if !emitted_division_by_zero_diagnostic - && matches!( - (op, right_ty), - ( - ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod, - Type::IntLiteral(0) | Type::BooleanLiteral(false) - ) - ) - { - emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty); - } - - match (left_ty, right_ty, op) { - (Type::Union(lhs_union), rhs, _) => { - let mut union = UnionBuilder::new(self.db()); - for lhs in lhs_union.elements(self.db()) { - let result = self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - *lhs, - rhs, - op, - )?; - union = union.add(result); - } - Some(union.build()) - } - (lhs, Type::Union(rhs_union), _) => { - let mut union = UnionBuilder::new(self.db()); - for rhs in rhs_union.elements(self.db()) { - let result = self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - lhs, - *rhs, - op, - )?; - union = union.add(result); - } - Some(union.build()) - } - - // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, - // the result would then become Any or Unknown, respectively). - (any @ Type::Dynamic(DynamicType::Any), _, _) - | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), - (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) - | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), - (todo @ Type::Dynamic(DynamicType::Todo(_)), _, _) - | (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo), - (todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _) - | (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo), - (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), - - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( - n.checked_add(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Sub) => Some( - n.checked_sub(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mult) => Some( - n.checked_mul(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - (Type::IntLiteral(_), Type::IntLiteral(_), ast::Operator::Div) => { - Some(KnownClass::Float.to_instance(self.db())) - } - - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::FloorDiv) => Some( - n.checked_div(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mod) => Some( - n.checked_rem(m) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ), - - (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => Some({ - if m < 0 { - KnownClass::Float.to_instance(self.db()) - } else { - u32::try_from(m) - .ok() - .and_then(|m| n.checked_pow(m)) - .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) - } - }), - - (Type::BytesLiteral(lhs), Type::BytesLiteral(rhs), ast::Operator::Add) => { - let bytes = [&**lhs.value(self.db()), &**rhs.value(self.db())].concat(); - Some(Type::bytes_literal(self.db(), &bytes)) - } - - (Type::StringLiteral(lhs), Type::StringLiteral(rhs), ast::Operator::Add) => { - let lhs_value = lhs.value(self.db()).to_string(); - let rhs_value = rhs.value(self.db()).as_ref(); - let ty = if lhs_value.len() + rhs_value.len() <= Self::MAX_STRING_LITERAL_SIZE { - Type::string_literal(self.db(), &(lhs_value + rhs_value)) - } else { - Type::LiteralString - }; - Some(ty) - } - - ( - Type::StringLiteral(_) | Type::LiteralString, - Type::StringLiteral(_) | Type::LiteralString, - ast::Operator::Add, - ) => Some(Type::LiteralString), - - (Type::StringLiteral(s), Type::IntLiteral(n), ast::Operator::Mult) - | (Type::IntLiteral(n), Type::StringLiteral(s), ast::Operator::Mult) => { - let ty = if n < 1 { - Type::string_literal(self.db(), "") - } else if let Ok(n) = usize::try_from(n) { - if n.checked_mul(s.value(self.db()).len()) - .is_some_and(|new_length| new_length <= Self::MAX_STRING_LITERAL_SIZE) - { - let new_literal = s.value(self.db()).repeat(n); - Type::string_literal(self.db(), &new_literal) - } else { - Type::LiteralString - } - } else { - Type::LiteralString - }; - Some(ty) - } - - (Type::LiteralString, Type::IntLiteral(n), ast::Operator::Mult) - | (Type::IntLiteral(n), Type::LiteralString, ast::Operator::Mult) => { - let ty = if n < 1 { - Type::string_literal(self.db(), "") - } else { - Type::LiteralString - }; - Some(ty) - } - - (Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => { - Some(Type::BooleanLiteral(b1 | b2)) - } - - (Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - Type::IntLiteral(i64::from(bool_value)), - right, - op, - ), - (left, Type::BooleanLiteral(bool_value), op) => self.infer_binary_expression_type( - node, - emitted_division_by_zero_diagnostic, - left, - Type::IntLiteral(i64::from(bool_value)), - op, - ), - - (Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => { - // Note: this only works on heterogeneous tuples. - let lhs_elements = lhs.elements(self.db()); - let rhs_elements = rhs.elements(self.db()); - - Some(TupleType::from_elements( - self.db(), - lhs_elements - .iter() - .copied() - .chain(rhs_elements.iter().copied()), - )) - } - - // We've handled all of the special cases that we support for literals, so we need to - // fall back on looking for dunder methods on one of the operand types. - ( - Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::GenericAlias(_) - | Type::SubclassOf(_) - | Type::Instance(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::Intersection(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::IntLiteral(_) - | Type::StringLiteral(_) - | Type::LiteralString - | Type::BytesLiteral(_) - | Type::SliceLiteral(_) - | Type::Tuple(_) - | Type::BoundSuper(_) - | Type::TypeVar(_), - Type::FunctionLiteral(_) - | Type::Callable(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::GenericAlias(_) - | Type::SubclassOf(_) - | Type::Instance(_) - | Type::KnownInstance(_) - | Type::PropertyInstance(_) - | Type::Intersection(_) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::IntLiteral(_) - | Type::StringLiteral(_) - | Type::LiteralString - | Type::BytesLiteral(_) - | Type::SliceLiteral(_) - | Type::Tuple(_) - | Type::BoundSuper(_) - | Type::TypeVar(_), - op, - ) => { - // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from - // the Python spec [1] is: - // - // - If rhs is a (proper) subclass of lhs, and it provides a different - // implementation of __rop__, use that. - // - Otherwise, if lhs implements __op__, use that. - // - Otherwise, if lhs and rhs are different types, and rhs implements __rop__, - // use that. - // - // [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__ - - // Technically we don't have to check left_ty != right_ty here, since if the types - // are the same, they will trivially have the same implementation of the reflected - // dunder, and so we'll fail the inner check. But the type equality check will be - // faster for the common case, and allow us to skip the (two) class member lookups. - let left_class = left_ty.to_meta_type(self.db()); - let right_class = right_ty.to_meta_type(self.db()); - if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) { - let reflected_dunder = op.reflected_dunder(); - let rhs_reflected = right_class.member(self.db(), reflected_dunder).symbol; - // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible - // Bindings together - if !rhs_reflected.is_unbound() - && rhs_reflected != left_class.member(self.db(), reflected_dunder).symbol - { - return right_ty - .try_call_dunder( - self.db(), - reflected_dunder, - CallArgumentTypes::positional([left_ty]), - ) - .map(|outcome| outcome.return_type(self.db())) - .or_else(|_| { - left_ty - .try_call_dunder( - self.db(), - op.dunder(), - CallArgumentTypes::positional([right_ty]), - ) - .map(|outcome| outcome.return_type(self.db())) - }) - .ok(); - } - } - - let call_on_left_instance = left_ty - .try_call_dunder( - self.db(), - op.dunder(), - CallArgumentTypes::positional([right_ty]), - ) - .map(|outcome| outcome.return_type(self.db())) - .ok(); - - call_on_left_instance.or_else(|| { - if left_ty == right_ty { - None - } else { - right_ty - .try_call_dunder( - self.db(), - op.reflected_dunder(), - CallArgumentTypes::positional([left_ty]), - ) - .map(|outcome| outcome.return_type(self.db())) - .ok() - } - }) - } - } - } - - fn infer_boolean_expression(&mut self, bool_op: &ast::ExprBoolOp) -> Type<'db> { - let ast::ExprBoolOp { - range: _, - op, - values, - } = bool_op; - self.infer_chained_boolean_types( - *op, - values.iter().enumerate(), - |builder, (index, value)| { - let ty = if index == values.len() - 1 { - builder.infer_expression(value) - } else { - builder.infer_standalone_expression(value) - }; - - (ty, value.range()) - }, - ) - } - - /// Computes the output of a chain of (one) boolean operation, consuming as input an iterator - /// of operations and calling the `infer_ty` for each to infer their types. - /// The iterator is consumed even if the boolean evaluation can be short-circuited, - /// in order to ensure the invariant that all expressions are evaluated when inferring types. - fn infer_chained_boolean_types( - &mut self, - op: ast::BoolOp, - operations: Iterator, - infer_ty: F, - ) -> Type<'db> - where - Iterator: IntoIterator, - F: Fn(&mut Self, Item) -> (Type<'db>, TextRange), - { - let mut done = false; - let db = self.db(); - - let elements = operations - .into_iter() - .with_position() - .map(|(position, item)| { - let (ty, range) = infer_ty(self, item); - - let is_last = matches!( - position, - itertools::Position::Last | itertools::Position::Only - ); - - if is_last { - if done { - Type::Never - } else { - ty - } - } else { - let truthiness = ty.try_bool(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, range); - err.fallback_truthiness() - }); - - if done { - return Type::Never; - } - - match (truthiness, op) { - (Truthiness::AlwaysTrue, ast::BoolOp::And) => Type::Never, - (Truthiness::AlwaysFalse, ast::BoolOp::Or) => Type::Never, - - (Truthiness::AlwaysFalse, ast::BoolOp::And) - | (Truthiness::AlwaysTrue, ast::BoolOp::Or) => { - done = true; - ty - } - - (Truthiness::Ambiguous, _) => IntersectionBuilder::new(db) - .add_positive(ty) - .add_negative(match op { - ast::BoolOp::And => Type::AlwaysTruthy, - ast::BoolOp::Or => Type::AlwaysFalsy, - }) - .build(), - } - } - }); - - UnionType::from_elements(db, elements) - } - - fn infer_compare_expression(&mut self, compare: &ast::ExprCompare) -> Type<'db> { - let ast::ExprCompare { - range: _, - left, - ops, - comparators, - } = compare; - - self.infer_expression(left); - - // https://docs.python.org/3/reference/expressions.html#comparisons - // > Formally, if `a, b, c, …, y, z` are expressions and `op1, op2, …, opN` are comparison - // > operators, then `a op1 b op2 c ... y opN z` is equivalent to `a op1 b and b op2 c and - // ... > y opN z`, except that each expression is evaluated at most once. - // - // As some operators (==, !=, <, <=, >, >=) *can* return an arbitrary type, the logic below - // is shared with the one in `infer_binary_type_comparison`. - self.infer_chained_boolean_types( - ast::BoolOp::And, - std::iter::once(&**left) - .chain(comparators) - .tuple_windows::<(_, _)>() - .zip(ops), - |builder, ((left, right), op)| { - let left_ty = builder.expression_type(left); - let right_ty = builder.infer_expression(right); - - let range = TextRange::new(left.start(), right.end()); - - let ty = builder - .infer_binary_type_comparison(left_ty, *op, right_ty, range) - .unwrap_or_else(|error| { - // Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome) - builder.context.report_lint_old( - &UNSUPPORTED_OPERATOR, - range, - format_args!( - "Operator `{}` is not supported for types `{}` and `{}`{}", - error.op, - error.left_ty.display(builder.db()), - error.right_ty.display(builder.db()), - if (left_ty, right_ty) == (error.left_ty, error.right_ty) { - String::new() - } else { - format!( - ", in comparing `{}` with `{}`", - left_ty.display(builder.db()), - right_ty.display(builder.db()) - ) - } - ), - ); - - match op { - // `in, not in, is, is not` always return bool instances - ast::CmpOp::In - | ast::CmpOp::NotIn - | ast::CmpOp::Is - | ast::CmpOp::IsNot => KnownClass::Bool.to_instance(builder.db()), - // Other operators can return arbitrary types - _ => Type::unknown(), - } - }); - - (ty, range) - }, - ) - } - - fn infer_binary_intersection_type_comparison( - &mut self, - intersection: IntersectionType<'db>, - op: ast::CmpOp, - other: Type<'db>, - intersection_on: IntersectionOn, - range: TextRange, - ) -> Result, CompareUnsupportedError<'db>> { - // If a comparison yields a definitive true/false answer on a (positive) part - // of an intersection type, it will also yield a definitive answer on the full - // intersection type, which is even more specific. - for pos in intersection.positive(self.db()) { - let result = match intersection_on { - IntersectionOn::Left => { - self.infer_binary_type_comparison(*pos, op, other, range)? - } - IntersectionOn::Right => { - self.infer_binary_type_comparison(other, op, *pos, range)? - } - }; - if let Type::BooleanLiteral(b) = result { - return Ok(Type::BooleanLiteral(b)); - } - } - - // For negative contributions to the intersection type, there are only a few - // special cases that allow us to narrow down the result type of the comparison. - for neg in intersection.negative(self.db()) { - let result = match intersection_on { - IntersectionOn::Left => self - .infer_binary_type_comparison(*neg, op, other, range) - .ok(), - IntersectionOn::Right => self - .infer_binary_type_comparison(other, op, *neg, range) - .ok(), - }; - - match (op, result) { - (ast::CmpOp::Is, Some(Type::BooleanLiteral(true))) => { - return Ok(Type::BooleanLiteral(false)); - } - (ast::CmpOp::IsNot, Some(Type::BooleanLiteral(false))) => { - return Ok(Type::BooleanLiteral(true)); - } - _ => {} - } - } - - // If none of the simplifications above apply, we still need to return *some* - // result type for the comparison 'T_inter `op` T_other' (or reversed), where - // - // T_inter = P1 & P2 & ... & Pn & ~N1 & ~N2 & ... & ~Nm - // - // is the intersection type. If f(T) is the function that computes the result - // type of a `op`-comparison with `T_other`, we are interested in f(T_inter). - // Since we can't compute it exactly, we return the following approximation: - // - // f(T_inter) = f(P1) & f(P2) & ... & f(Pn) - // - // The reason for this is the following: In general, for any function 'f', the - // set f(A) & f(B) is *larger than or equal to* the set f(A & B). This means - // that we will return a type that is possibly wider than it could be, but - // never wrong. - // - // However, we do have to leave out the negative contributions. If we were to - // add a contribution like ~f(N1), we would potentially infer result types - // that are too narrow. - // - // As an example for this, consider the intersection type `int & ~Literal[1]`. - // If 'f' would be the `==`-comparison with 2, we obviously can't tell if that - // answer would be true or false, so we need to return `bool`. And indeed, we - // we have (glossing over notational details): - // - // f(int & ~1) - // = f({..., -1, 0, 2, 3, ...}) - // = {..., False, False, True, False, ...} - // = bool - // - // On the other hand, if we were to compute - // - // f(int) & ~f(1) - // = bool & ~False - // = True - // - // we would get a result type `Literal[True]` which is too narrow. - // - let mut builder = IntersectionBuilder::new(self.db()); - - builder = builder.add_positive(KnownClass::Bool.to_instance(self.db())); - - for pos in intersection.positive(self.db()) { - let result = match intersection_on { - IntersectionOn::Left => { - self.infer_binary_type_comparison(*pos, op, other, range)? - } - IntersectionOn::Right => { - self.infer_binary_type_comparison(other, op, *pos, range)? - } - }; - builder = builder.add_positive(result); - } - - Ok(builder.build()) - } - - /// Infers the type of a binary comparison (e.g. 'left == right'). See - /// `infer_compare_expression` for the higher level logic dealing with multi-comparison - /// expressions. - /// - /// If the operation is not supported, return None (we need upstream context to emit a - /// diagnostic). - fn infer_binary_type_comparison( - &mut self, - left: Type<'db>, - op: ast::CmpOp, - right: Type<'db>, - range: TextRange, - ) -> Result, CompareUnsupportedError<'db>> { - // Note: identity (is, is not) for equal builtin types is unreliable and not part of the - // language spec. - // - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal - // - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal - match (left, right) { - (Type::Union(union), other) => { - let mut builder = UnionBuilder::new(self.db()); - for element in union.elements(self.db()) { - builder = - builder.add(self.infer_binary_type_comparison(*element, op, other, range)?); - } - Ok(builder.build()) - } - (other, Type::Union(union)) => { - let mut builder = UnionBuilder::new(self.db()); - for element in union.elements(self.db()) { - builder = - builder.add(self.infer_binary_type_comparison(other, op, *element, range)?); - } - Ok(builder.build()) - } - - (Type::Intersection(intersection), right) => self - .infer_binary_intersection_type_comparison( - intersection, - op, - right, - IntersectionOn::Left, - range, - ), - (left, Type::Intersection(intersection)) => self - .infer_binary_intersection_type_comparison( - intersection, - op, - left, - IntersectionOn::Right, - range, - ), - - (Type::IntLiteral(n), Type::IntLiteral(m)) => match op { - ast::CmpOp::Eq => Ok(Type::BooleanLiteral(n == m)), - ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(n != m)), - ast::CmpOp::Lt => Ok(Type::BooleanLiteral(n < m)), - ast::CmpOp::LtE => Ok(Type::BooleanLiteral(n <= m)), - ast::CmpOp::Gt => Ok(Type::BooleanLiteral(n > m)), - ast::CmpOp::GtE => Ok(Type::BooleanLiteral(n >= m)), - // We cannot say that two equal int Literals will return True from an `is` or `is not` comparison. - // Even if they are the same value, they may not be the same object. - ast::CmpOp::Is => { - if n == m { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::BooleanLiteral(false)) - } - } - ast::CmpOp::IsNot => { - if n == m { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::BooleanLiteral(true)) - } - } - // Undefined for (int, int) - ast::CmpOp::In | ast::CmpOp::NotIn => Err(CompareUnsupportedError { - op, - left_ty: left, - right_ty: right, - }), - }, - (Type::IntLiteral(_), Type::Instance(_)) => self.infer_binary_type_comparison( - KnownClass::Int.to_instance(self.db()), - op, - right, - range, - ), - (Type::Instance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison( - left, - op, - KnownClass::Int.to_instance(self.db()), - range, - ), - - // Booleans are coded as integers (False = 0, True = 1) - (Type::IntLiteral(n), Type::BooleanLiteral(b)) => self.infer_binary_type_comparison( - Type::IntLiteral(n), - op, - Type::IntLiteral(i64::from(b)), - range, - ), - (Type::BooleanLiteral(b), Type::IntLiteral(m)) => self.infer_binary_type_comparison( - Type::IntLiteral(i64::from(b)), - op, - Type::IntLiteral(m), - range, - ), - (Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => self - .infer_binary_type_comparison( - Type::IntLiteral(i64::from(a)), - op, - Type::IntLiteral(i64::from(b)), - range, - ), - - (Type::StringLiteral(salsa_s1), Type::StringLiteral(salsa_s2)) => { - let s1 = salsa_s1.value(self.db()); - let s2 = salsa_s2.value(self.db()); - match op { - ast::CmpOp::Eq => Ok(Type::BooleanLiteral(s1 == s2)), - ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(s1 != s2)), - ast::CmpOp::Lt => Ok(Type::BooleanLiteral(s1 < s2)), - ast::CmpOp::LtE => Ok(Type::BooleanLiteral(s1 <= s2)), - ast::CmpOp::Gt => Ok(Type::BooleanLiteral(s1 > s2)), - ast::CmpOp::GtE => Ok(Type::BooleanLiteral(s1 >= s2)), - ast::CmpOp::In => Ok(Type::BooleanLiteral(s2.contains(s1.as_ref()))), - ast::CmpOp::NotIn => Ok(Type::BooleanLiteral(!s2.contains(s1.as_ref()))), - ast::CmpOp::Is => { - if s1 == s2 { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::BooleanLiteral(false)) - } - } - ast::CmpOp::IsNot => { - if s1 == s2 { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::BooleanLiteral(true)) - } - } - } - } - (Type::StringLiteral(_), _) => self.infer_binary_type_comparison( - KnownClass::Str.to_instance(self.db()), - op, - right, - range, - ), - (_, Type::StringLiteral(_)) => self.infer_binary_type_comparison( - left, - op, - KnownClass::Str.to_instance(self.db()), - range, - ), - - (Type::LiteralString, _) => self.infer_binary_type_comparison( - KnownClass::Str.to_instance(self.db()), - op, - right, - range, - ), - (_, Type::LiteralString) => self.infer_binary_type_comparison( - left, - op, - KnownClass::Str.to_instance(self.db()), - range, - ), - - (Type::BytesLiteral(salsa_b1), Type::BytesLiteral(salsa_b2)) => { - let b1 = &**salsa_b1.value(self.db()); - let b2 = &**salsa_b2.value(self.db()); - match op { - ast::CmpOp::Eq => Ok(Type::BooleanLiteral(b1 == b2)), - ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(b1 != b2)), - ast::CmpOp::Lt => Ok(Type::BooleanLiteral(b1 < b2)), - ast::CmpOp::LtE => Ok(Type::BooleanLiteral(b1 <= b2)), - ast::CmpOp::Gt => Ok(Type::BooleanLiteral(b1 > b2)), - ast::CmpOp::GtE => Ok(Type::BooleanLiteral(b1 >= b2)), - ast::CmpOp::In => { - Ok(Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_some())) - } - ast::CmpOp::NotIn => { - Ok(Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_none())) - } - ast::CmpOp::Is => { - if b1 == b2 { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::BooleanLiteral(false)) - } - } - ast::CmpOp::IsNot => { - if b1 == b2 { - Ok(KnownClass::Bool.to_instance(self.db())) - } else { - Ok(Type::BooleanLiteral(true)) - } - } - } - } - (Type::BytesLiteral(_), _) => self.infer_binary_type_comparison( - KnownClass::Bytes.to_instance(self.db()), - op, - right, - range, - ), - (_, Type::BytesLiteral(_)) => self.infer_binary_type_comparison( - left, - op, - KnownClass::Bytes.to_instance(self.db()), - range, - ), - (Type::Tuple(_), Type::Instance(instance)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_binary_type_comparison( - left, - op, - Type::version_info_tuple(self.db()), - range, - ) - } - (Type::Instance(instance), Type::Tuple(_)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_binary_type_comparison( - Type::version_info_tuple(self.db()), - op, - right, - range, - ) - } - (Type::Tuple(lhs), Type::Tuple(rhs)) => { - // Note: This only works on heterogeneous tuple types. - let lhs_elements = lhs.elements(self.db()); - let rhs_elements = rhs.elements(self.db()); - - let mut tuple_rich_comparison = - |op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements, range); - - match op { - ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In | ast::CmpOp::NotIn => { - let mut eq_count = 0usize; - let mut not_eq_count = 0usize; - - for ty in rhs_elements { - let eq_result = self.infer_binary_type_comparison( - Type::Tuple(lhs), - ast::CmpOp::Eq, - *ty, - range, - ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); - - match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for different union variants. Instead, this is just for us to - // evaluate a possibly truthy value to `false` or `true`. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysTrue => eq_count += 1, - Truthiness::AlwaysFalse => not_eq_count += 1, - Truthiness::Ambiguous => (), - }, - } - } - - if eq_count >= 1 { - Ok(Type::BooleanLiteral(op.is_in())) - } else if not_eq_count == rhs_elements.len() { - Ok(Type::BooleanLiteral(op.is_not_in())) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - ast::CmpOp::Is | ast::CmpOp::IsNot => { - // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` - // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` - let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( - "infer_binary_type_comparison should never return None for `CmpOp::Eq`", - ); - - Ok(match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for `is` and `is not` comparisons. This is an implementation detail - // for how we determine the truthiness of a type. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), - _ => KnownClass::Bool.to_instance(self.db()), - }, - }) - } - } - } - - // Lookup the rich comparison `__dunder__` methods - _ => { - let rich_comparison = |op| self.infer_rich_comparison(left, right, op); - let membership_test_comparison = |op, range: TextRange| { - self.infer_membership_test_comparison(left, right, op, range) - }; - match op { - ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In => { - membership_test_comparison(MembershipTestCompareOperator::In, range) - } - ast::CmpOp::NotIn => { - membership_test_comparison(MembershipTestCompareOperator::NotIn, range) - } - ast::CmpOp::Is => { - if left.is_disjoint_from(self.db(), right) { - Ok(Type::BooleanLiteral(false)) - } else if left.is_singleton(self.db()) - && left.is_equivalent_to(self.db(), right) - { - Ok(Type::BooleanLiteral(true)) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - ast::CmpOp::IsNot => { - if left.is_disjoint_from(self.db(), right) { - Ok(Type::BooleanLiteral(true)) - } else if left.is_singleton(self.db()) - && left.is_equivalent_to(self.db(), right) - { - Ok(Type::BooleanLiteral(false)) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - } - } - } - } - - /// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their - /// behaviour can be edited for classes by implementing corresponding dunder methods. - /// This function performs rich comparison between two types and returns the resulting type. - /// see `` - fn infer_rich_comparison( - &self, - left: Type<'db>, - right: Type<'db>, - op: RichCompareOperator, - ) -> Result, CompareUnsupportedError<'db>> { - let db = self.db(); - // The following resource has details about the rich comparison algorithm: - // https://snarky.ca/unravelling-rich-comparison-operators/ - let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| { - left.try_call_dunder(db, op.dunder(), CallArgumentTypes::positional([right])) - .map(|outcome| outcome.return_type(db)) - .ok() - }; - - // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. - if left != right && right.is_subtype_of(db, left) { - call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) - } else { - call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) - } - .or_else(|| { - // When no appropriate method returns any value other than NotImplemented, - // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. - // refer to `` - if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) { - Some(KnownClass::Bool.to_instance(db)) - } else { - None - } - }) - .ok_or_else(|| CompareUnsupportedError { - op: op.into(), - left_ty: left, - right_ty: right, - }) - } - - /// Performs a membership test (`in` and `not in`) between two instances and returns the resulting type, or `None` if the test is unsupported. - /// The behavior can be customized in Python by implementing `__contains__`, `__iter__`, or `__getitem__` methods. - /// See `` - /// and `` - fn infer_membership_test_comparison( - &self, - left: Type<'db>, - right: Type<'db>, - op: MembershipTestCompareOperator, - range: TextRange, - ) -> Result, CompareUnsupportedError<'db>> { - let db = self.db(); - - let contains_dunder = right.class_member(db, "__contains__".into()).symbol; - let compare_result_opt = match contains_dunder { - Symbol::Type(contains_dunder, Boundness::Bound) => { - // If `__contains__` is available, it is used directly for the membership test. - contains_dunder - .try_call(db, CallArgumentTypes::positional([right, left])) - .map(|bindings| bindings.return_type(db)) - .ok() - } - _ => { - // iteration-based membership test - right - .try_iterate(db) - .map(|_| KnownClass::Bool.to_instance(db)) - .ok() - } - }; - - compare_result_opt - .map(|ty| { - if matches!(ty, Type::Dynamic(DynamicType::Todo(_))) { - return ty; - } - - let truthiness = ty.try_bool(db).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, range); - err.fallback_truthiness() - }); - - match op { - MembershipTestCompareOperator::In => truthiness.into_type(db), - MembershipTestCompareOperator::NotIn => truthiness.negate().into_type(db), - } - }) - .ok_or_else(|| CompareUnsupportedError { - op: op.into(), - left_ty: left, - right_ty: right, - }) - } - - /// Simulates rich comparison between tuples and returns the inferred result. - /// This performs a lexicographic comparison, returning a union of all possible return types that could result from the comparison. - /// - /// basically it's based on cpython's `tuple_richcompare` - /// see `` - fn infer_tuple_rich_comparison( - &mut self, - left: &[Type<'db>], - op: RichCompareOperator, - right: &[Type<'db>], - range: TextRange, - ) -> Result, CompareUnsupportedError<'db>> { - let left_iter = left.iter().copied(); - let right_iter = right.iter().copied(); - - let mut builder = UnionBuilder::new(self.db()); - - for (l_ty, r_ty) in left_iter.zip(right_iter) { - let pairwise_eq_result = self - .infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty, range) - .expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); - - match pairwise_eq_result - .try_bool(self.db()) - .unwrap_or_else(|err| { - // TODO: We should, whenever possible, pass the range of the left and right elements - // instead of the range of the whole tuple. - err.report_diagnostic(&self.context, range); - err.fallback_truthiness() - }) { - // - AlwaysTrue : Continue to the next pair for lexicographic comparison - Truthiness::AlwaysTrue => continue, - // - AlwaysFalse: - // Lexicographic comparisons will always terminate with this pair. - // Complete the comparison and return the result. - // - Ambiguous: - // Lexicographic comparisons might continue to the next pair (if eq_result is true), - // or terminate here (if eq_result is false). - // To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder. - eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => { - let pairwise_compare_result = match op { - RichCompareOperator::Lt - | RichCompareOperator::Le - | RichCompareOperator::Gt - | RichCompareOperator::Ge => { - self.infer_binary_type_comparison(l_ty, op.into(), r_ty, range)? - } - // For `==` and `!=`, we already figure out the result from `pairwise_eq_result` - // NOTE: The CPython implementation does not account for non-boolean return types - // or cases where `!=` is not the negation of `==`, we also do not consider these cases. - RichCompareOperator::Eq => Type::BooleanLiteral(false), - RichCompareOperator::Ne => Type::BooleanLiteral(true), - }; - - builder = builder.add(pairwise_compare_result); - - if eq_truthiness.is_ambiguous() { - continue; - } - - return Ok(builder.build()); - } - } - } - - // if no more items to compare, we just compare sizes - let (left_len, right_len) = (left.len(), right.len()); - - builder = builder.add(Type::BooleanLiteral(match op { - RichCompareOperator::Eq => left_len == right_len, - RichCompareOperator::Ne => left_len != right_len, - RichCompareOperator::Lt => left_len < right_len, - RichCompareOperator::Le => left_len <= right_len, - RichCompareOperator::Gt => left_len > right_len, - RichCompareOperator::Ge => left_len >= right_len, - })); - - Ok(builder.build()) - } - - fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { - let ast::ExprSubscript { - range: _, - value, - slice, - ctx: _, - } = subscript; - - // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the - // subscript inference logic and treat this as an explicit specialization. - // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return - // this callable as the `__class_getitem__` method on `type`. That probably requires - // updating all of the subscript logic below to use custom callables for all of the _other_ - // special cases, too. - let value_ty = self.infer_expression(value); - if let Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) = value_ty { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - generic_class, - slice, - ); - } - - let slice_ty = self.infer_expression(slice); - self.infer_subscript_expression_types(value, value_ty, slice_ty) - } - - fn infer_explicit_class_specialization( - &mut self, - subscript: &ast::ExprSubscript, - value_ty: Type<'db>, - generic_class: GenericClass<'db>, - slice_node: &ast::Expr, - ) -> Type<'db> { - let mut call_argument_types = match slice_node { - ast::Expr::Tuple(tuple) => CallArgumentTypes::positional( - tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), - ), - _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), - }; - let generic_context = generic_class.generic_context(self.db()); - let signatures = Signatures::single(CallableSignature::single( - value_ty, - generic_context.signature(self.db()), - )); - let bindings = match Bindings::match_parameters(signatures, &mut call_argument_types) - .check_types(self.db(), &mut call_argument_types) - { - Ok(bindings) => bindings, - Err(CallError(_, bindings)) => { - bindings.report_diagnostics(&self.context, subscript.into()); - return Type::unknown(); - } - }; - let callable = bindings - .into_iter() - .next() - .expect("valid bindings should have one callable"); - let (_, overload) = callable - .matching_overload() - .expect("valid bindings should have matching overload"); - let specialization = generic_context.specialize( - self.db(), - overload - .parameter_types() - .iter() - .map(|ty| ty.unwrap_or(Type::unknown())) - .collect(), - ); - Type::from(GenericAlias::new(self.db(), generic_class, specialization)) - } - - fn infer_subscript_expression_types( - &mut self, - value_node: &ast::Expr, - value_ty: Type<'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()); - elements - .iter() - .py_index(i32::try_from(int).expect("checked in branch arm")) - .copied() - .unwrap_or_else(|_| { - report_index_out_of_bounds( - &self.context, - "tuple", - value_node.into(), - value_ty, - elements.len(), - int, - ); - Type::unknown() - }) - } - // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` - (Type::Tuple(tuple_ty), Type::SliceLiteral(slice_ty)) => { - let elements = tuple_ty.elements(self.db()); - let (start, stop, step) = slice_ty.as_tuple(self.db()); - - if let Ok(new_elements) = elements.py_slice(start, stop, step) { - TupleType::from_elements(self.db(), new_elements) - } else { - report_slice_step_size_zero(&self.context, value_node.into()); - Type::unknown() - } - } - // Ex) Given `"value"[1]`, return `"a"` - (Type::StringLiteral(literal_ty), Type::IntLiteral(int)) - if i32::try_from(int).is_ok() => - { - let literal_value = literal_ty.value(self.db()); - literal_value - .chars() - .py_index(i32::try_from(int).expect("checked in branch arm")) - .map(|ch| Type::string_literal(self.db(), &ch.to_string())) - .unwrap_or_else(|_| { - report_index_out_of_bounds( - &self.context, - "string", - value_node.into(), - value_ty, - literal_value.chars().count(), - int, - ); - Type::unknown() - }) - } - // Ex) Given `"value"[1:3]`, return `"al"` - (Type::StringLiteral(literal_ty), Type::SliceLiteral(slice_ty)) => { - let literal_value = literal_ty.value(self.db()); - let (start, stop, step) = slice_ty.as_tuple(self.db()); - - let chars: Vec<_> = literal_value.chars().collect(); - let result = if let Ok(new_chars) = chars.py_slice(start, stop, step) { - let literal: String = new_chars.collect(); - Type::string_literal(self.db(), &literal) - } else { - report_slice_step_size_zero(&self.context, value_node.into()); - Type::unknown() - }; - result - } - // Ex) Given `b"value"[1]`, return `b"a"` - (Type::BytesLiteral(literal_ty), Type::IntLiteral(int)) - if i32::try_from(int).is_ok() => - { - let literal_value = literal_ty.value(self.db()); - literal_value - .iter() - .py_index(i32::try_from(int).expect("checked in branch arm")) - .map(|byte| Type::bytes_literal(self.db(), &[*byte])) - .unwrap_or_else(|_| { - report_index_out_of_bounds( - &self.context, - "bytes literal", - value_node.into(), - value_ty, - literal_value.len(), - int, - ); - Type::unknown() - }) - } - // Ex) Given `b"value"[1:3]`, return `b"al"` - (Type::BytesLiteral(literal_ty), Type::SliceLiteral(slice_ty)) => { - let literal_value = literal_ty.value(self.db()); - let (start, stop, step) = slice_ty.as_tuple(self.db()); - - if let Ok(new_bytes) = literal_value.py_slice(start, stop, step) { - let new_bytes: Vec = new_bytes.copied().collect(); - Type::bytes_literal(self.db(), &new_bytes) - } else { - report_slice_step_size_zero(&self.context, value_node.into()); - Type::unknown() - } - } - // Ex) Given `"value"[True]`, return `"a"` - ( - Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_), - Type::BooleanLiteral(bool), - ) => self.infer_subscript_expression_types( - value_node, - value_ty, - Type::IntLiteral(i64::from(bool)), - ), - (Type::KnownInstance(KnownInstanceType::Protocol), _) => { - Type::Dynamic(DynamicType::TodoProtocol) - } - (Type::KnownInstance(known_instance), _) - if known_instance.class().is_special_form() => - { - todo_type!("Inference of subscript on special form") - } - (value_ty, slice_ty) => { - // If the class defines `__getitem__`, return its return type. - // - // See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem - match value_ty.try_call_dunder( - self.db(), - "__getitem__", - CallArgumentTypes::positional([slice_ty]), - ) { - Ok(outcome) => return outcome.return_type(self.db()), - Err(err @ CallDunderError::PossiblyUnbound { .. }) => { - self.context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - value_node, - format_args!( - "Method `__getitem__` of type `{}` is possibly unbound", - value_ty.display(self.db()), - ), - ); - - return err.fallback_return_type(self.db()); - } - Err(CallDunderError::CallError(_, bindings)) => { - self.context.report_lint_old( - &CALL_NON_CALLABLE, - value_node, - format_args!( - "Method `__getitem__` of type `{}` is not callable on object of type `{}`", - bindings.callable_type().display(self.db()), - value_ty.display(self.db()), - ), - ); - - return bindings.return_type(self.db()); - } - Err(CallDunderError::MethodNotAvailable) => { - // try `__class_getitem__` - } - } - - // Otherwise, if the value is itself a class and defines `__class_getitem__`, - // return its return type. - // - // TODO: lots of classes are only subscriptable at runtime on Python 3.9+, - // *but* we should also allow them to be subscripted in stubs - // (and in annotations if `from __future__ import annotations` is enabled), - // even if the target version is Python 3.8 or lower, - // despite the fact that there will be no corresponding `__class_getitem__` - // method in these `sys.version_info` branches. - if value_ty.is_subtype_of(self.db(), KnownClass::Type.to_instance(self.db())) { - let dunder_class_getitem_method = - value_ty.member(self.db(), "__class_getitem__").symbol; - - match dunder_class_getitem_method { - Symbol::Unbound => {} - Symbol::Type(ty, boundness) => { - if boundness == Boundness::PossiblyUnbound { - self.context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - value_node, - format_args!( - "Method `__class_getitem__` of type `{}` is possibly unbound", - value_ty.display(self.db()), - ), - ); - } - - match ty.try_call( - self.db(), - CallArgumentTypes::positional([value_ty, slice_ty]), - ) { - Ok(bindings) => return bindings.return_type(self.db()), - Err(CallError(_, bindings)) => { - self.context.report_lint_old( - &CALL_NON_CALLABLE, - value_node, - format_args!( - "Method `__class_getitem__` of type `{}` is not callable on object of type `{}`", - bindings.callable_type().display(self.db()), - value_ty.display(self.db()), - ), - ); - return bindings.return_type(self.db()); - } - } - } - } - - if let Type::ClassLiteral(class) = value_ty { - if class.is_known(self.db(), KnownClass::Type) { - return KnownClass::GenericAlias.to_instance(self.db()); - } - - if let ClassLiteralType::Generic(_) = class { - // TODO: specialize the generic class using these explicit type - // variable assignments - return value_ty; - } - } - - report_non_subscriptable( - &self.context, - value_node.into(), - value_ty, - "__class_getitem__", - ); - } else { - report_non_subscriptable( - &self.context, - value_node.into(), - value_ty, - "__getitem__", - ); - } - - match value_ty { - Type::ClassLiteral(_) => { - // TODO: proper support for generic classes - // For now, just infer `Sequence`, if we see something like `Sequence[str]`. This allows us - // to look up attributes on generic base classes, even if we don't understand generics yet. - // Note that this isn't handled by the clause up above for generic classes - // that use legacy type variables and an explicit `Generic` base class. - // Once we handle legacy typevars, this special case will be removed in - // favor of the specialization logic above. - value_ty - } - _ => Type::unknown(), - } - } - } - } - - fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { - enum SliceArg { - Arg(Option), - Unsupported, - } - - let ast::ExprSlice { - range: _, - lower, - upper, - step, - } = slice; - - let ty_lower = self.infer_optional_expression(lower.as_deref()); - let ty_upper = self.infer_optional_expression(upper.as_deref()); - let ty_step = self.infer_optional_expression(step.as_deref()); - - let type_to_slice_argument = |ty: Option>| match ty { - Some(Type::IntLiteral(n)) => match i32::try_from(n) { - Ok(n) => SliceArg::Arg(Some(n)), - Err(_) => SliceArg::Unsupported, - }, - Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), - Some(Type::Instance(instance)) - if instance.class.is_known(self.db(), KnownClass::NoneType) => - { - SliceArg::Arg(None) - } - None => SliceArg::Arg(None), - _ => SliceArg::Unsupported, - }; - - match ( - type_to_slice_argument(ty_lower), - type_to_slice_argument(ty_upper), - type_to_slice_argument(ty_step), - ) { - (SliceArg::Arg(lower), SliceArg::Arg(upper), SliceArg::Arg(step)) => { - Type::SliceLiteral(SliceLiteralType::new(self.db(), lower, upper, step)) - } - _ => KnownClass::Slice.to_instance(self.db()), - } - } - - fn infer_type_parameters(&mut self, type_parameters: &ast::TypeParams) { - let ast::TypeParams { - range: _, - type_params, - } = type_parameters; - for type_param in type_params { - match type_param { - ast::TypeParam::TypeVar(node) => self.infer_definition(node), - ast::TypeParam::ParamSpec(node) => self.infer_definition(node), - ast::TypeParam::TypeVarTuple(node) => self.infer_definition(node), - } - } - } - - pub(super) fn finish(mut self) -> TypeInference<'db> { - self.infer_region(); - self.types.diagnostics = self.context.finish(); - self.types.shrink_to_fit(); - self.types - } -} - -/// Annotation expressions. -impl<'db> TypeInferenceBuilder<'db> { - /// Infer the type of an annotation expression with the given [`DeferredExpressionState`]. - fn infer_annotation_expression( - &mut self, - annotation: &ast::Expr, - deferred_state: DeferredExpressionState, - ) -> TypeAndQualifiers<'db> { - let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state); - let annotation_ty = self.infer_annotation_expression_impl(annotation); - self.deferred_state = previous_deferred_state; - annotation_ty - } - - /// Similar to [`infer_annotation_expression`], but accepts an optional annotation expression - /// and returns [`None`] if the annotation is [`None`]. - /// - /// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression - fn infer_optional_annotation_expression( - &mut self, - annotation: Option<&ast::Expr>, - deferred_state: DeferredExpressionState, - ) -> Option> { - annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state)) - } - - /// Implementation of [`infer_annotation_expression`]. - /// - /// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression - fn infer_annotation_expression_impl( - &mut self, - annotation: &ast::Expr, - ) -> TypeAndQualifiers<'db> { - // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression - let annotation_ty = match annotation { - // String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations - ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string), - - // Annotation expressions also get special handling for `*args` and `**kwargs`. - ast::Expr::Starred(starred) => self.infer_starred_expression(starred).into(), - - ast::Expr::BytesLiteral(bytes) => { - self.context.report_lint_old( - &BYTE_STRING_TYPE_ANNOTATION, - bytes, - format_args!("Type expressions cannot use bytes literal"), - ); - TypeAndQualifiers::unknown() - } - - ast::Expr::FString(fstring) => { - self.context.report_lint_old( - &FSTRING_TYPE_ANNOTATION, - fstring, - format_args!("Type expressions cannot use f-strings"), - ); - self.infer_fstring_expression(fstring); - TypeAndQualifiers::unknown() - } - - ast::Expr::Name(name) => match name.ctx { - ast::ExprContext::Load => { - let name_expr_ty = self.infer_name_expression(name); - match name_expr_ty { - Type::KnownInstance(KnownInstanceType::ClassVar) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR) - } - Type::KnownInstance(KnownInstanceType::Final) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL) - } - _ => name_expr_ty - .in_type_expression(self.db()) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - annotation, - self.is_reachable(annotation), - ) - }) - .into(), - } - } - ast::ExprContext::Invalid => TypeAndQualifiers::unknown(), - ast::ExprContext::Store | ast::ExprContext::Del => { - todo_type!("Name expression annotation in Store/Del context").into() - } - }, - - ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => { - let value_ty = self.infer_expression(value); - - let slice = &**slice; - - match value_ty { - Type::KnownInstance(KnownInstanceType::Annotated) => { - // This branch is similar to the corresponding branch in `infer_parameterized_known_instance_type_expression`, but - // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly - // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). - if let ast::Expr::Tuple(ast::ExprTuple { - elts: arguments, .. - }) = slice - { - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - if let [inner_annotation, metadata @ ..] = &arguments[..] { - for element in metadata { - self.infer_expression(element); - } - - let inner_annotation_ty = - self.infer_annotation_expression_impl(inner_annotation); - - self.store_expression_type(slice, inner_annotation_ty.inner_type()); - inner_annotation_ty - } else { - self.infer_type_expression(slice); - TypeAndQualifiers::unknown() - } - } else { - report_invalid_arguments_to_annotated(&self.context, subscript); - self.infer_annotation_expression_impl(slice) - } - } - Type::KnownInstance( - known_instance @ (KnownInstanceType::ClassVar | KnownInstanceType::Final), - ) => match slice { - ast::Expr::Tuple(..) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type qualifier `{type_qualifier}` expects exactly one type parameter", - type_qualifier = known_instance.repr(self.db()), - ), - ); - Type::unknown().into() - } - _ => { - let mut type_and_qualifiers = - self.infer_annotation_expression_impl(slice); - match known_instance { - KnownInstanceType::ClassVar => { - type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); - } - KnownInstanceType::Final => { - type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL); - } - _ => unreachable!(), - } - type_and_qualifiers - } - }, - _ => self - .infer_subscript_type_expression_no_store(subscript, slice, value_ty) - .into(), - } - } - - // All other annotation expressions are (possibly) valid type expressions, so handle - // them there instead. - type_expr => self.infer_type_expression_no_store(type_expr).into(), - }; - - self.store_expression_type(annotation, annotation_ty.inner_type()); - - annotation_ty - } - - /// Infer the type of a string annotation expression. - fn infer_string_annotation_expression( - &mut self, - string: &ast::ExprStringLiteral, - ) -> TypeAndQualifiers<'db> { - match parse_string_annotation(&self.context, string) { - Some(parsed) => { - // String annotations are always evaluated in the deferred context. - self.infer_annotation_expression( - parsed.expr(), - DeferredExpressionState::InStringAnnotation( - self.enclosing_node_key(string.into()), - ), - ) - } - None => TypeAndQualifiers::unknown(), - } - } -} - -/// Type expressions -impl<'db> TypeInferenceBuilder<'db> { - /// Infer the type of a type expression. - fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> { - let ty = self.infer_type_expression_no_store(expression); - self.store_expression_type(expression, ty); - ty - } - - /// Similar to [`infer_type_expression`], but accepts an optional type expression and returns - /// [`None`] if the expression is [`None`]. - /// - /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression - fn infer_optional_type_expression( - &mut self, - expression: Option<&ast::Expr>, - ) -> Option> { - expression.map(|expr| self.infer_type_expression(expr)) - } - - /// Similar to [`infer_type_expression`], but accepts a [`DeferredExpressionState`]. - /// - /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression - fn infer_type_expression_with_state( - &mut self, - expression: &ast::Expr, - deferred_state: DeferredExpressionState, - ) -> Type<'db> { - let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state); - let annotation_ty = self.infer_type_expression(expression); - self.deferred_state = previous_deferred_state; - annotation_ty - } - - fn report_invalid_type_expression( - &mut self, - expression: &ast::Expr, - message: std::fmt::Arguments, - ) -> Type<'db> { - self.context - .report_lint_old(&INVALID_TYPE_FORM, expression, message); - Type::unknown() - } - - /// Infer the type of a type expression without storing the result. - fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> { - // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression - match expression { - ast::Expr::Name(name) => match name.ctx { - ast::ExprContext::Load => self - .infer_name_expression(name) - .in_type_expression(self.db()) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - expression, - self.is_reachable(expression), - ) - }), - ast::ExprContext::Invalid => Type::unknown(), - ast::ExprContext::Store | ast::ExprContext::Del => { - todo_type!("Name expression annotation in Store/Del context") - } - }, - - ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx { - ast::ExprContext::Load => self - .infer_attribute_expression(attribute_expression) - .in_type_expression(self.db()) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - expression, - self.is_reachable(expression), - ) - }), - ast::ExprContext::Invalid => Type::unknown(), - ast::ExprContext::Store | ast::ExprContext::Del => { - todo_type!("Attribute expression annotation in Store/Del context") - } - }, - - ast::Expr::NoneLiteral(_literal) => Type::none(self.db()), - - // https://typing.python.org/en/latest/spec/annotations.html#string-annotations - ast::Expr::StringLiteral(string) => self.infer_string_type_expression(string), - - ast::Expr::Subscript(subscript) => { - let ast::ExprSubscript { - value, - slice, - ctx: _, - range: _, - } = subscript; - - let value_ty = self.infer_expression(value); - - self.infer_subscript_type_expression_no_store(subscript, slice, value_ty) - } - - ast::Expr::BinOp(binary) => { - match binary.op { - // PEP-604 unions are okay, e.g., `int | str` - ast::Operator::BitOr => { - let left_ty = self.infer_type_expression(&binary.left); - let right_ty = self.infer_type_expression(&binary.right); - UnionType::from_elements(self.db(), [left_ty, right_ty]) - } - // anything else is an invalid annotation: - _ => { - self.infer_binary_expression(binary); - Type::unknown() - } - } - } - - // Avoid inferring the types of invalid type expressions that have been parsed from a - // string annotation, as they are not present in the semantic index. - _ if self.deferred_state.in_string_annotation() => Type::unknown(), - - // ===================================================================================== - // Forms which are invalid in the context of annotation expressions: we infer their - // nested expressions as normal expressions, but the type of the top-level expression is - // always `Type::unknown` in these cases. - // ===================================================================================== - - // TODO: add a subdiagnostic linking to type-expression grammar - // and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]` - ast::Expr::BytesLiteral(_) => self.report_invalid_type_expression( - expression, - format_args!("Bytes literals are not allowed in this context in a type expression"), - ), - - // TODO: add a subdiagnostic linking to type-expression grammar - // and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]` - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Int(_), - .. - }) => self.report_invalid_type_expression( - expression, - format_args!("Int literals are not allowed in this context in a type expression"), - ), - - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Float(_), - .. - }) => self.report_invalid_type_expression( - expression, - format_args!("Float literals are not allowed in type expressions"), - ), - - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Complex { .. }, - .. - }) => self.report_invalid_type_expression( - expression, - format_args!("Complex literals are not allowed in type expressions"), - ), - - // TODO: add a subdiagnostic linking to type-expression grammar - // and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]` - ast::Expr::BooleanLiteral(_) => self.report_invalid_type_expression( - expression, - format_args!( - "Boolean literals are not allowed in this context in a type expression" - ), - ), - - // TODO: add a subdiagnostic linking to type-expression grammar - // and stating that it is only valid as first argument to `typing.Callable[]` - ast::Expr::List(list) => { - self.infer_list_expression(list); - self.report_invalid_type_expression( - expression, - format_args!( - "List literals are not allowed in this context in a type expression" - ), - ) - } - - ast::Expr::BoolOp(bool_op) => { - self.infer_boolean_expression(bool_op); - self.report_invalid_type_expression( - expression, - format_args!("Boolean operations are not allowed in type expressions"), - ) - } - - ast::Expr::Named(named) => { - self.infer_named_expression(named); - self.report_invalid_type_expression( - expression, - format_args!("Named expressions are not allowed in type expressions"), - ) - } - - ast::Expr::UnaryOp(unary) => { - self.infer_unary_expression(unary); - self.report_invalid_type_expression( - expression, - format_args!("Unary operations are not allowed in type expressions"), - ) - } - - ast::Expr::Lambda(lambda_expression) => { - self.infer_lambda_expression(lambda_expression); - self.report_invalid_type_expression( - expression, - format_args!("`lambda` expressions are not allowed in type expressions"), - ) - } - - ast::Expr::If(if_expression) => { - self.infer_if_expression(if_expression); - self.report_invalid_type_expression( - expression, - format_args!("`if` expressions are not allowed in type expressions"), - ) - } - - ast::Expr::Dict(dict) => { - self.infer_dict_expression(dict); - self.report_invalid_type_expression( - expression, - format_args!("Dict literals are not allowed in type expressions"), - ) - } - - ast::Expr::Set(set) => { - self.infer_set_expression(set); - self.report_invalid_type_expression( - expression, - format_args!("Set literals are not allowed in type expressions"), - ) - } - - ast::Expr::DictComp(dictcomp) => { - self.infer_dict_comprehension_expression(dictcomp); - self.report_invalid_type_expression( - expression, - format_args!("Dict comprehensions are not allowed in type expressions"), - ) - } - - ast::Expr::ListComp(listcomp) => { - self.infer_list_comprehension_expression(listcomp); - self.report_invalid_type_expression( - expression, - format_args!("List comprehensions are not allowed in type expressions"), - ) - } - - ast::Expr::SetComp(setcomp) => { - self.infer_set_comprehension_expression(setcomp); - self.report_invalid_type_expression( - expression, - format_args!("Set comprehensions are not allowed in type expressions"), - ) - } - - ast::Expr::Generator(generator) => { - self.infer_generator_expression(generator); - self.report_invalid_type_expression( - expression, - format_args!("Generator expressions are not allowed in type expressions"), - ) - } - - ast::Expr::Await(await_expression) => { - self.infer_await_expression(await_expression); - self.report_invalid_type_expression( - expression, - format_args!("`await` expressions are not allowed in type expressions"), - ) - } - - ast::Expr::Yield(yield_expression) => { - self.infer_yield_expression(yield_expression); - self.report_invalid_type_expression( - expression, - format_args!("`yield` expressions are not allowed in type expressions"), - ) - } - - ast::Expr::YieldFrom(yield_from) => { - self.infer_yield_from_expression(yield_from); - self.report_invalid_type_expression( - expression, - format_args!("`yield from` expressions are not allowed in type expressions"), - ) - } - - ast::Expr::Compare(compare) => { - self.infer_compare_expression(compare); - self.report_invalid_type_expression( - expression, - format_args!("Comparison expressions are not allowed in type expressions"), - ) - } - - ast::Expr::Call(call_expr) => { - self.infer_call_expression(call_expr); - self.report_invalid_type_expression( - expression, - format_args!("Function calls are not allowed in type expressions"), - ) - } - - ast::Expr::FString(fstring) => { - self.infer_fstring_expression(fstring); - self.report_invalid_type_expression( - expression, - format_args!("F-strings are not allowed in type expressions"), - ) - } - - ast::Expr::Slice(slice) => { - self.infer_slice_expression(slice); - self.report_invalid_type_expression( - expression, - format_args!("Slices are not allowed in type expressions"), - ) - } - - // ================================================================================= - // Branches where we probably should emit diagnostics in some context, but don't yet - // ================================================================================= - ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"), - - ast::Expr::EllipsisLiteral(_) => { - todo_type!("ellipsis literal in type expression") - } - - ast::Expr::Tuple(tuple) => { - self.infer_tuple_expression(tuple); - Type::unknown() - } - - ast::Expr::Starred(starred) => { - self.infer_starred_expression(starred); - todo_type!("PEP 646") - } - } - } - - fn infer_subscript_type_expression_no_store( - &mut self, - subscript: &ast::ExprSubscript, - slice: &ast::Expr, - value_ty: Type<'db>, - ) -> Type<'db> { - match value_ty { - Type::ClassLiteral(class_literal) => match class_literal.known(self.db()) { - Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice), - Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice), - _ => self.infer_subscript_type_expression(subscript, value_ty), - }, - _ => self.infer_subscript_type_expression(subscript, value_ty), - } - } - - /// Infer the type of a string type expression. - fn infer_string_type_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> { - match parse_string_annotation(&self.context, string) { - Some(parsed) => { - // String annotations are always evaluated in the deferred context. - self.infer_type_expression_with_state( - parsed.expr(), - DeferredExpressionState::InStringAnnotation( - self.enclosing_node_key(string.into()), - ), - ) - } - None => Type::unknown(), - } - } - - /// Given the slice of a `tuple[]` annotation, return the type that the annotation represents - fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> { - /// In most cases, if a subelement of the tuple is inferred as `Todo`, - /// we should only infer `Todo` for that specific subelement. - /// Certain specific AST nodes can however change the meaning of the entire tuple, - /// however: for example, `tuple[int, ...]` or `tuple[int, *tuple[str, ...]]` are a - /// homogeneous tuple and a partly homogeneous tuple (respectively) due to the `...` - /// and the starred expression (respectively), Neither is supported by us right now, - /// so we should infer `Todo` for the *entire* tuple if we encounter one of those elements. - fn element_could_alter_type_of_whole_tuple( - element: &ast::Expr, - element_ty: Type, - builder: &TypeInferenceBuilder, - ) -> bool { - if !element_ty.is_todo() { - return false; - } - - match element { - ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) => true, - ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => { - matches!( - builder.expression_type(value), - Type::KnownInstance(KnownInstanceType::Unpack) - ) - } - _ => false, - } - } - - // TODO: - // - homogeneous tuples - // - PEP 646 - match tuple_slice { - ast::Expr::Tuple(elements) => { - let mut element_types = Vec::with_capacity(elements.len()); - - // Whether to infer `Todo` for the whole tuple - // (see docstring for `element_could_alter_type_of_whole_tuple`) - let mut return_todo = false; - - for element in elements { - let element_ty = self.infer_type_expression(element); - return_todo |= - element_could_alter_type_of_whole_tuple(element, element_ty, self); - element_types.push(element_ty); - } - - let ty = if return_todo { - todo_type!("full tuple[...] support") - } else { - TupleType::from_elements(self.db(), element_types) - }; - - // Here, we store the type for the inner `int, str` tuple-expression, - // while the type for the outer `tuple[int, str]` slice-expression is - // stored in the surrounding `infer_type_expression` call: - self.store_expression_type(tuple_slice, ty); - - ty - } - single_element => { - let single_element_ty = self.infer_type_expression(single_element); - if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self) - { - todo_type!("full tuple[...] support") - } else { - TupleType::from_elements(self.db(), std::iter::once(single_element_ty)) - } - } - } - } - - /// Given the slice of a `type[]` annotation, return the type that the annotation represents - fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> { - match slice { - ast::Expr::Name(_) | ast::Expr::Attribute(_) => { - let name_ty = self.infer_expression(slice); - match name_ty { - Type::ClassLiteral(class_literal) => { - if class_literal.is_known(self.db(), KnownClass::Any) { - SubclassOfType::subclass_of_any() - } else { - SubclassOfType::from( - self.db(), - class_literal.default_specialization(self.db()), - ) - } - } - Type::KnownInstance(KnownInstanceType::Any) => { - SubclassOfType::subclass_of_any() - } - Type::KnownInstance(KnownInstanceType::Unknown) => { - SubclassOfType::subclass_of_unknown() - } - _ => todo_type!("unsupported type[X] special form"), - } - } - ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => { - let union_ty = UnionType::from_elements( - self.db(), - [ - self.infer_subclass_of_type_expression(&binary.left), - self.infer_subclass_of_type_expression(&binary.right), - ], - ); - self.store_expression_type(slice, union_ty); - - union_ty - } - ast::Expr::Tuple(_) => { - self.infer_type_expression(slice); - self.context.report_lint_old( - &INVALID_TYPE_FORM, - slice, - format_args!("type[...] must have exactly one type argument"), - ); - Type::unknown() - } - ast::Expr::Subscript(ast::ExprSubscript { - value, - slice: parameters, - .. - }) => { - let parameters_ty = match self.infer_expression(value) { - Type::KnownInstance(KnownInstanceType::Union) => match &**parameters { - ast::Expr::Tuple(tuple) => { - let ty = UnionType::from_elements( - self.db(), - tuple - .iter() - .map(|element| self.infer_subclass_of_type_expression(element)), - ); - self.store_expression_type(parameters, ty); - ty - } - _ => self.infer_subclass_of_type_expression(parameters), - }, - _ => { - self.infer_type_expression(parameters); - todo_type!("unsupported nested subscript in type[X]") - } - }; - self.store_expression_type(slice, parameters_ty); - parameters_ty - } - // TODO: subscripts, etc. - _ => { - self.infer_type_expression(slice); - todo_type!("unsupported type[X] special form") - } - } - } - - fn infer_subscript_type_expression( - &mut self, - subscript: &ast::ExprSubscript, - value_ty: Type<'db>, - ) -> Type<'db> { - let ast::ExprSubscript { - range: _, - value: _, - slice, - ctx: _, - } = subscript; - - match value_ty { - Type::ClassLiteral(literal) if literal.is_known(self.db(), KnownClass::Any) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!("Type `typing.Any` expected no type parameter",), - ); - Type::unknown() - } - Type::KnownInstance(known_instance) => { - self.infer_parameterized_known_instance_type_expression(subscript, known_instance) - } - Type::Dynamic(DynamicType::Todo(_)) => { - self.infer_type_expression(slice); - value_ty - } - _ => { - self.infer_type_expression(slice); - todo_type!("generics") - } - } - } - - fn infer_parameterized_known_instance_type_expression( - &mut self, - subscript: &ast::ExprSubscript, - known_instance: KnownInstanceType, - ) -> Type<'db> { - let db = self.db(); - let arguments_slice = &*subscript.slice; - match known_instance { - KnownInstanceType::Annotated => { - let ast::Expr::Tuple(ast::ExprTuple { - elts: arguments, .. - }) = arguments_slice - else { - report_invalid_arguments_to_annotated(&self.context, subscript); - - // `Annotated[]` with less than two arguments is an error at runtime. - // However, we still treat `Annotated[T]` as `T` here for the purpose of - // giving better diagnostics later on. - // Pyright also does this. Mypy doesn't; it falls back to `Any` instead. - return self.infer_type_expression(arguments_slice); - }; - - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - let [type_expr, metadata @ ..] = &arguments[..] else { - self.infer_type_expression(arguments_slice); - return Type::unknown(); - }; - - for element in metadata { - self.infer_expression(element); - } - - let ty = self.infer_type_expression(type_expr); - self.store_expression_type(arguments_slice, ty); - ty - } - KnownInstanceType::Literal => { - match self.infer_literal_parameter_type(arguments_slice) { - Ok(ty) => ty, - Err(nodes) => { - for node in nodes { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - node, - format_args!( - "Type arguments for `Literal` must be `None`, \ - a literal value (int, bool, str, or bytes), or an enum value" - ), - ); - } - Type::unknown() - } - } - } - KnownInstanceType::Optional => { - let param_type = self.infer_type_expression(arguments_slice); - UnionType::from_elements(db, [param_type, Type::none(db)]) - } - KnownInstanceType::Union => match arguments_slice { - ast::Expr::Tuple(t) => { - let union_ty = UnionType::from_elements( - db, - t.iter().map(|elt| self.infer_type_expression(elt)), - ); - self.store_expression_type(arguments_slice, union_ty); - union_ty - } - _ => self.infer_type_expression(arguments_slice), - }, - KnownInstanceType::TypeVar(_) => { - self.infer_type_expression(arguments_slice); - todo_type!("TypeVar annotations") - } - KnownInstanceType::TypeAliasType(_) => { - self.infer_type_expression(arguments_slice); - todo_type!("Generic PEP-695 type alias") - } - KnownInstanceType::Callable => { - let mut arguments = match arguments_slice { - ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), - _ => { - self.infer_callable_parameter_types(arguments_slice); - Either::Right(std::iter::empty::<&ast::Expr>()) - } - }; - - let first_argument = arguments.next(); - - let parameters = - first_argument.and_then(|arg| self.infer_callable_parameter_types(arg)); - - let return_type = arguments.next().map(|arg| self.infer_type_expression(arg)); - - let correct_argument_number = if let Some(third_argument) = arguments.next() { - self.infer_type_expression(third_argument); - for argument in arguments { - self.infer_type_expression(argument); - } - false - } else { - return_type.is_some() - }; - - if !correct_argument_number { - report_invalid_arguments_to_callable(&self.context, subscript); - } - - let callable_type = if let (Some(parameters), Some(return_type), true) = - (parameters, return_type, correct_argument_number) - { - CallableType::new(db, Signature::new(parameters, Some(return_type))) - } else { - CallableType::unknown(db) - }; - - let callable_type = Type::Callable(callable_type); - - // `Signature` / `Parameters` are not a `Type` variant, so we're storing - // the outer callable type on the these expressions instead. - self.store_expression_type(arguments_slice, callable_type); - if let Some(first_argument) = first_argument { - self.store_expression_type(first_argument, callable_type); - } - - callable_type - } - - // Type API special forms - KnownInstanceType::Not => match arguments_slice { - ast::Expr::Tuple(_) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected exactly one type parameter", - known_instance.repr(db) - ), - ); - Type::unknown() - } - _ => { - let argument_type = self.infer_type_expression(arguments_slice); - argument_type.negate(db) - } - }, - KnownInstanceType::Intersection => { - let elements = match arguments_slice { - ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), - element => Either::Right(std::iter::once(element)), - }; - - elements - .fold(IntersectionBuilder::new(db), |builder, element| { - builder.add_positive(self.infer_type_expression(element)) - }) - .build() - } - KnownInstanceType::TypeOf => match arguments_slice { - ast::Expr::Tuple(_) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected exactly one type parameter", - known_instance.repr(db) - ), - ); - Type::unknown() - } - _ => { - // NB: This calls `infer_expression` instead of `infer_type_expression`. - let argument_type = self.infer_expression(arguments_slice); - argument_type - } - }, - KnownInstanceType::CallableTypeOf => match arguments_slice { - ast::Expr::Tuple(_) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected exactly one type parameter", - known_instance.repr(db) - ), - ); - Type::unknown() - } - _ => { - let argument_type = self.infer_expression(arguments_slice); - let signatures = argument_type.signatures(db); - - // TODO overloads - let Some(signature) = signatures.iter().flatten().next() else { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - arguments_slice, - format_args!( - "Expected the first argument to `{}` to be a callable object, but got an object of type `{}`", - known_instance.repr(db), - argument_type.display(db) - ), - ); - return Type::unknown(); - }; - - let revealed_signature = if argument_type.is_bound_method() { - signature.bind_self() - } else { - signature.clone() - }; - - Type::Callable(CallableType::new(db, revealed_signature)) - } - }, - - // TODO: Generics - KnownInstanceType::ChainMap => { - self.infer_type_expression(arguments_slice); - KnownClass::ChainMap.to_instance(db) - } - KnownInstanceType::OrderedDict => { - self.infer_type_expression(arguments_slice); - KnownClass::OrderedDict.to_instance(db) - } - KnownInstanceType::Dict => { - self.infer_type_expression(arguments_slice); - KnownClass::Dict.to_instance(db) - } - KnownInstanceType::List => { - self.infer_type_expression(arguments_slice); - KnownClass::List.to_instance(db) - } - KnownInstanceType::DefaultDict => { - self.infer_type_expression(arguments_slice); - KnownClass::DefaultDict.to_instance(db) - } - KnownInstanceType::Counter => { - self.infer_type_expression(arguments_slice); - KnownClass::Counter.to_instance(db) - } - KnownInstanceType::Set => { - self.infer_type_expression(arguments_slice); - KnownClass::Set.to_instance(db) - } - KnownInstanceType::FrozenSet => { - self.infer_type_expression(arguments_slice); - KnownClass::FrozenSet.to_instance(db) - } - KnownInstanceType::Deque => { - self.infer_type_expression(arguments_slice); - KnownClass::Deque.to_instance(db) - } - - KnownInstanceType::ReadOnly => { - self.infer_type_expression(arguments_slice); - todo_type!("`ReadOnly[]` type qualifier") - } - KnownInstanceType::NotRequired => { - self.infer_type_expression(arguments_slice); - todo_type!("`NotRequired[]` type qualifier") - } - KnownInstanceType::ClassVar | KnownInstanceType::Final => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type qualifier `{}` is not allowed in type expressions (only in annotation expressions)", - known_instance.repr(db) - ), - ); - self.infer_type_expression(arguments_slice) - } - KnownInstanceType::Required => { - self.infer_type_expression(arguments_slice); - todo_type!("`Required[]` type qualifier") - } - KnownInstanceType::TypeIs => { - self.infer_type_expression(arguments_slice); - todo_type!("`TypeIs[]` special form") - } - KnownInstanceType::TypeGuard => { - self.infer_type_expression(arguments_slice); - todo_type!("`TypeGuard[]` special form") - } - KnownInstanceType::Concatenate => { - self.infer_type_expression(arguments_slice); - todo_type!("`Concatenate[]` special form") - } - KnownInstanceType::Unpack => { - self.infer_type_expression(arguments_slice); - todo_type!("`Unpack[]` special form") - } - KnownInstanceType::Protocol => { - self.infer_type_expression(arguments_slice); - Type::Dynamic(DynamicType::TodoProtocol) - } - KnownInstanceType::NoReturn - | KnownInstanceType::Never - | KnownInstanceType::Any - | KnownInstanceType::AlwaysTruthy - | KnownInstanceType::AlwaysFalsy => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type `{}` expected no type parameter", - known_instance.repr(db) - ), - ); - Type::unknown() - } - KnownInstanceType::TypingSelf - | KnownInstanceType::TypeAlias - | KnownInstanceType::Unknown => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected no type parameter", - known_instance.repr(db) - ), - ); - Type::unknown() - } - KnownInstanceType::LiteralString => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?", - known_instance.repr(db) - ), - ); - Type::unknown() - } - KnownInstanceType::Type => self.infer_subclass_of_type_expression(arguments_slice), - KnownInstanceType::Tuple => self.infer_tuple_type_expression(arguments_slice), - } - } - - fn infer_literal_parameter_type<'ast>( - &mut self, - parameters: &'ast ast::Expr, - ) -> Result, Vec<&'ast ast::Expr>> { - Ok(match parameters { - // TODO handle type aliases - ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { - let value_ty = self.infer_expression(value); - if matches!(value_ty, Type::KnownInstance(KnownInstanceType::Literal)) { - let ty = self.infer_literal_parameter_type(slice)?; - - // This branch deals with annotations such as `Literal[Literal[1]]`. - // Here, we store the type for the inner `Literal[1]` expression: - self.store_expression_type(parameters, ty); - ty - } else { - self.store_expression_type(parameters, Type::unknown()); - - return Err(vec![parameters]); - } - } - ast::Expr::Tuple(tuple) if !tuple.parenthesized => { - let mut errors = vec![]; - let mut builder = UnionBuilder::new(self.db()); - for elt in tuple { - match self.infer_literal_parameter_type(elt) { - Ok(ty) => { - builder = builder.add(ty); - } - Err(nodes) => { - errors.extend(nodes); - } - } - } - if errors.is_empty() { - let union_type = builder.build(); - - // This branch deals with annotations such as `Literal[1, 2]`. Here, we - // store the type for the inner `1, 2` tuple-expression: - self.store_expression_type(parameters, union_type); - - union_type - } else { - self.store_expression_type(parameters, Type::unknown()); - - return Err(errors); - } - } - - literal @ (ast::Expr::StringLiteral(_) - | ast::Expr::BytesLiteral(_) - | ast::Expr::BooleanLiteral(_) - | ast::Expr::NoneLiteral(_)) => self.infer_expression(literal), - literal @ ast::Expr::NumberLiteral(ref number) if number.value.is_int() => { - self.infer_expression(literal) - } - // For enum values - ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { - let value_ty = self.infer_expression(value); - // TODO: Check that value type is enum otherwise return None - value_ty - .member(self.db(), &attr.id) - .symbol - .ignore_possibly_unbound() - .unwrap_or(Type::unknown()) - } - // for negative and positive numbers - ast::Expr::UnaryOp(ref u) - if matches!(u.op, ast::UnaryOp::USub | ast::UnaryOp::UAdd) - && u.operand.is_number_literal_expr() => - { - self.infer_unary_expression(u) - } - _ => { - self.infer_expression(parameters); - return Err(vec![parameters]); - } - }) - } - - /// Infer the first argument to a `typing.Callable` type expression and returns the - /// corresponding [`Parameters`]. - /// - /// It returns `None` if the argument is invalid i.e., not a list of types, parameter - /// specification, `typing.Concatenate`, or `...`. - fn infer_callable_parameter_types( - &mut self, - parameters: &ast::Expr, - ) -> Option> { - Some(match parameters { - ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { .. }) => { - Parameters::gradual_form() - } - ast::Expr::List(ast::ExprList { elts: params, .. }) => { - let mut parameter_types = Vec::with_capacity(params.len()); - - // Whether to infer `Todo` for the parameters - let mut return_todo = false; - - for param in params { - let param_type = self.infer_type_expression(param); - // This is similar to what we currently do for inferring tuple type expression. - // We currently infer `Todo` for the parameters to avoid invalid diagnostics - // when trying to check for assignability or any other relation. For example, - // `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported. - return_todo |= param_type.is_todo() - && matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_)); - parameter_types.push(param_type); - } - - if return_todo { - // TODO: `Unpack` - Parameters::todo() - } else { - Parameters::new(parameter_types.iter().map(|param_type| { - Parameter::positional_only(None).with_annotated_type(*param_type) - })) - } - } - ast::Expr::Subscript(_) => { - // TODO: Support `Concatenate[...]` - Parameters::todo() - } - ast::Expr::Name(name) if name.is_invalid() => { - // This is a special case to avoid raising the error suggesting what the first - // argument should be. This only happens when there's already a syntax error like - // `Callable[]`. - return None; - } - _ => { - // TODO: Check whether `Expr::Name` is a ParamSpec - self.context.report_lint_old( - &INVALID_TYPE_FORM, - parameters, - format_args!( - "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`", - ), - ); - return None; - } - }) - } -} - -/// The deferred state of a specific expression in an inference region. -#[derive(Default, Debug, Clone, Copy)] -enum DeferredExpressionState { - /// The expression is not deferred. - #[default] - None, - - /// The expression is deferred. - /// - /// In the following example, - /// ```py - /// from __future__ import annotation - /// - /// a: tuple[int, "ForwardRef"] = ... - /// ``` - /// - /// The expression `tuple` and `int` are deferred but `ForwardRef` (after parsing) is both - /// deferred and in a string annotation context. - Deferred, - - /// The expression is in a string annotation context. - /// - /// This is required to differentiate between a deferred annotation and a string annotation. - /// The former can occur when there's a `from __future__ import annotations` statement or we're - /// in a stub file. - /// - /// In the following example, - /// ```py - /// a: "List[int]" = ... - /// b: tuple[int, "ForwardRef"] = ... - /// ``` - /// - /// The annotation of `a` is completely inside a string while for `b`, it's only partially - /// stringified. - /// - /// This variant wraps a [`NodeKey`] that allows us to retrieve the original - /// [`ast::ExprStringLiteral`] node which created the string annotation. - InStringAnnotation(NodeKey), -} - -impl DeferredExpressionState { - const fn is_deferred(self) -> bool { - matches!( - self, - DeferredExpressionState::Deferred | DeferredExpressionState::InStringAnnotation(_) - ) - } - - const fn in_string_annotation(self) -> bool { - matches!(self, DeferredExpressionState::InStringAnnotation(_)) - } -} - -impl From for DeferredExpressionState { - fn from(value: bool) -> Self { - if value { - DeferredExpressionState::Deferred - } else { - DeferredExpressionState::None - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RichCompareOperator { - Eq, - Ne, - Gt, - Ge, - Lt, - Le, -} - -impl From for ast::CmpOp { - fn from(value: RichCompareOperator) -> Self { - match value { - RichCompareOperator::Eq => ast::CmpOp::Eq, - RichCompareOperator::Ne => ast::CmpOp::NotEq, - RichCompareOperator::Lt => ast::CmpOp::Lt, - RichCompareOperator::Le => ast::CmpOp::LtE, - RichCompareOperator::Gt => ast::CmpOp::Gt, - RichCompareOperator::Ge => ast::CmpOp::GtE, - } - } -} - -impl RichCompareOperator { - #[must_use] - const fn dunder(self) -> &'static str { - match self { - RichCompareOperator::Eq => "__eq__", - RichCompareOperator::Ne => "__ne__", - RichCompareOperator::Lt => "__lt__", - RichCompareOperator::Le => "__le__", - RichCompareOperator::Gt => "__gt__", - RichCompareOperator::Ge => "__ge__", - } - } - - #[must_use] - const fn reflect(self) -> Self { - match self { - RichCompareOperator::Eq => RichCompareOperator::Eq, - RichCompareOperator::Ne => RichCompareOperator::Ne, - RichCompareOperator::Lt => RichCompareOperator::Gt, - RichCompareOperator::Le => RichCompareOperator::Ge, - RichCompareOperator::Gt => RichCompareOperator::Lt, - RichCompareOperator::Ge => RichCompareOperator::Le, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MembershipTestCompareOperator { - In, - NotIn, -} - -impl From for ast::CmpOp { - fn from(value: MembershipTestCompareOperator) -> Self { - match value { - MembershipTestCompareOperator::In => ast::CmpOp::In, - MembershipTestCompareOperator::NotIn => ast::CmpOp::NotIn, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct CompareUnsupportedError<'db> { - op: ast::CmpOp, - left_ty: Type<'db>, - right_ty: Type<'db>, -} - -fn format_import_from_module(level: u32, module: Option<&str>) -> String { - format!( - "{}{}", - ".".repeat(level as usize), - module.unwrap_or_default() - ) -} - -/// 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. -/// -/// If the formatted string contains an expression (with a representation unknown at compile time), -/// infers an instance of `builtins.str`. -#[derive(Debug)] -struct StringPartsCollector { - concatenated: Option, - expression: bool, -} - -impl StringPartsCollector { - fn new() -> Self { - Self { - concatenated: Some(String::new()), - expression: false, - } - } - - fn push_str(&mut self, literal: &str) { - if let Some(mut concatenated) = self.concatenated.take() { - if concatenated.len().saturating_add(literal.len()) - <= TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE - { - concatenated.push_str(literal); - self.concatenated = Some(concatenated); - } else { - self.concatenated = None; - } - } - } - - fn add_expression(&mut self) { - self.concatenated = None; - self.expression = true; - } - - fn string_type(self, db: &dyn Db) -> Type { - if self.expression { - KnownClass::Str.to_instance(db) - } else if let Some(concatenated) = self.concatenated { - Type::string_literal(db, &concatenated) - } else { - Type::LiteralString - } - } -} - -fn contains_string_literal(expr: &ast::Expr) -> bool { - struct ContainsStringLiteral(bool); - - impl<'a> Visitor<'a> for ContainsStringLiteral { - fn visit_expr(&mut self, expr: &'a ast::Expr) { - self.0 |= matches!(expr, ast::Expr::StringLiteral(_)); - walk_expr(self, expr); - } - } - - let mut visitor = ContainsStringLiteral(false); - visitor.visit_expr(expr); - visitor.0 -} - -#[cfg(test)] -mod tests { - use crate::db::tests::{setup_db, TestDb}; - use crate::semantic_index::definition::Definition; - use crate::semantic_index::symbol::FileScopeId; - use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; - use crate::symbol::global_symbol; - use crate::types::check_types; - use ruff_db::diagnostic::Diagnostic; - use ruff_db::files::{system_path_to_file, File}; - use ruff_db::system::DbWithWritableSystem as _; - use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; - - use super::*; - - #[track_caller] - fn get_symbol<'db>( - db: &'db TestDb, - file_name: &str, - scopes: &[&str], - symbol_name: &str, - ) -> Symbol<'db> { - let file = system_path_to_file(db, file_name).expect("file to exist"); - let index = semantic_index(db, file); - let mut file_scope_id = FileScopeId::global(); - let mut scope = file_scope_id.to_scope_id(db, file); - for expected_scope_name in scopes { - file_scope_id = index - .child_scopes(file_scope_id) - .next() - .unwrap_or_else(|| panic!("scope of {expected_scope_name}")) - .0; - scope = file_scope_id.to_scope_id(db, file); - assert_eq!(scope.name(db), *expected_scope_name); - } - - symbol(db, scope, symbol_name).symbol - } - - #[track_caller] - fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) { - let messages: Vec<&str> = diagnostics - .iter() - .map(Diagnostic::primary_message) - .collect(); - assert_eq!(&messages, expected); - } - - #[track_caller] - fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { - let file = system_path_to_file(db, filename).unwrap(); - let diagnostics = check_types(db, file); - - assert_diagnostic_messages(diagnostics, expected); - } - - #[test] - fn not_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - from typing_extensions import Literal, assert_type - - assert_type(not "{y}", bool) - assert_type(not 10*"{y}", bool) - assert_type(not "{y}"*10, bool) - assert_type(not 0*"{y}", Literal[True]) - assert_type(not (-100)*"{y}", Literal[True]) - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - ); - db.write_dedented("src/a.py", &content)?; - - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn multiplied_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - from typing_extensions import Literal, LiteralString, assert_type - - assert_type(2 * "hello", Literal["hellohello"]) - assert_type("goodbye" * 3, Literal["goodbyegoodbyegoodbye"]) - assert_type("a" * {y}, Literal["{a_repeated}"]) - assert_type({z} * "b", LiteralString) - assert_type(0 * "hello", Literal[""]) - assert_type(-3 * "hello", Literal[""]) - "#, - y = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE, - z = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1, - a_repeated = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE), - ); - db.write_dedented("src/a.py", &content)?; - - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn multiplied_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - from typing_extensions import Literal, LiteralString, assert_type - - assert_type("{y}", LiteralString) - assert_type(10*"{y}", LiteralString) - assert_type("{y}"*10, LiteralString) - assert_type(0*"{y}", Literal[""]) - assert_type((-100)*"{y}", Literal[""]) - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - ); - db.write_dedented("src/a.py", &content)?; - - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn truncated_string_literals_become_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - from typing_extensions import LiteralString, assert_type - - assert_type("{y}", LiteralString) - assert_type("a" + "{z}", LiteralString) - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - z = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE), - ); - db.write_dedented("src/a.py", &content)?; - - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn adding_string_literals_and_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - from typing_extensions import LiteralString, assert_type - - assert_type("{y}", LiteralString) - assert_type("{y}" + "a", LiteralString) - assert_type("a" + "{y}", LiteralString) - assert_type("{y}" + "{y}", LiteralString) - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - ); - db.write_dedented("src/a.py", &content)?; - - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn pep695_type_params() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def f[T, U: A, V: (A, B), W = A, X: A = A1, Y: (int,)](): - pass - - class A: ... - class B: ... - class A1(A): ... - ", - ) - .unwrap(); - - let check_typevar = |var: &'static str, - upper_bound: Option<&'static str>, - constraints: Option<&[&'static str]>, - default: Option<&'static str>| { - let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type(); - assert_eq!(var_ty.display(&db).to_string(), var); - - let expected_name_ty = format!(r#"Literal["{var}"]"#); - let name_ty = var_ty.member(&db, "__name__").symbol.expect_type(); - assert_eq!(name_ty.display(&db).to_string(), expected_name_ty); - - let KnownInstanceType::TypeVar(typevar) = var_ty.expect_known_instance() else { - panic!("expected TypeVar"); - }; - - assert_eq!( - typevar - .upper_bound(&db) - .map(|ty| ty.display(&db).to_string()), - upper_bound.map(std::borrow::ToOwned::to_owned) - ); - assert_eq!( - typevar.constraints(&db).map(|tys| tys - .iter() - .map(|ty| ty.display(&db).to_string()) - .collect::>()), - constraints.map(|strings| strings - .iter() - .map(std::string::ToString::to_string) - .collect::>()) - ); - assert_eq!( - typevar - .default_ty(&db) - .map(|ty| ty.display(&db).to_string()), - default.map(std::borrow::ToOwned::to_owned) - ); - }; - - check_typevar("T", None, None, None); - check_typevar("U", Some("A"), None, None); - check_typevar("V", None, Some(&["A", "B"]), None); - check_typevar("W", None, None, Some("A")); - check_typevar("X", Some("A"), None, Some("A1")); - - // a typevar with less than two constraints is treated as unconstrained - check_typevar("Y", None, None, None); - } - - /// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing - /// visibility-constraint checks in that scope. - #[test] - fn unbound_symbol_no_visibility_constraint_check() { - let mut db = setup_db(); - - // If the bug we are testing for is not fixed, what happens is that when inferring the - // `flag: bool = True` definitions, we look up `bool` as a deferred name (thus from end of - // scope), and because of the early return its "unbound" binding has a visibility - // constraint of `~flag`, which we evaluate, meaning we have to evaluate the definition of - // `flag` -- and we are in a cycle. With the fix, we short-circuit evaluating visibility - // constraints on "unbound" if a symbol is otherwise not bound. - db.write_dedented( - "src/a.py", - " - from __future__ import annotations - - def f(): - flag: bool = True - if flag: - return True - ", - ) - .unwrap(); - - db.clear_salsa_events(); - assert_file_diagnostics(&db, "src/a.py", &[]); - let events = db.take_salsa_events(); - let cycles = salsa::plumbing::attach(&db, || { - events - .iter() - .filter_map(|event| { - if let salsa::EventKind::WillIterateCycle { database_key, .. } = event.kind { - Some(format!("{database_key:?}")) - } else { - None - } - }) - .collect::>() - }); - let expected: Vec = vec![]; - assert_eq!(cycles, expected); - } - - // Incremental inference tests - #[track_caller] - fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> { - let scope = global_scope(db, file); - use_def_map(db, scope) - .public_bindings(symbol_table(db, scope).symbol_id_by_name(name).unwrap()) - .find_map(|b| b.binding) - .expect("no binding found") - } - - #[test] - fn dependency_public_symbol_type_change() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_files([ - ("/src/a.py", "from foo import x"), - ("/src/foo.py", "x: int = 10\ndef foo(): ..."), - ])?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").symbol.expect_type(); - - assert_eq!(x_ty.display(&db).to_string(), "int"); - - // Change `x` to a different value - db.write_file("/src/foo.py", "x: bool = True\ndef foo(): ...")?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - - let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type(); - - assert_eq!(x_ty_2.display(&db).to_string(), "bool"); - - Ok(()) - } - - #[test] - fn dependency_internal_symbol_change() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_files([ - ("/src/a.py", "from foo import x"), - ("/src/foo.py", "x: int = 10\ndef foo(): y = 1"), - ])?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").symbol.expect_type(); - - assert_eq!(x_ty.display(&db).to_string(), "int"); - - db.write_file("/src/foo.py", "x: int = 10\ndef foo(): pass")?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - - db.clear_salsa_events(); - - let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type(); - - assert_eq!(x_ty_2.display(&db).to_string(), "int"); - - let events = db.take_salsa_events(); - - assert_function_query_was_not_run( - &db, - infer_definition_types, - first_public_binding(&db, a, "x"), - &events, - ); - - Ok(()) - } - - #[test] - fn dependency_unrelated_symbol() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_files([ - ("/src/a.py", "from foo import x"), - ("/src/foo.py", "x: int = 10\ny: bool = True"), - ])?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol(&db, a, "x").symbol.expect_type(); - - assert_eq!(x_ty.display(&db).to_string(), "int"); - - db.write_file("/src/foo.py", "x: int = 10\ny: bool = False")?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - - db.clear_salsa_events(); - - let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type(); - - assert_eq!(x_ty_2.display(&db).to_string(), "int"); - - let events = db.take_salsa_events(); - - assert_function_query_was_not_run( - &db, - infer_definition_types, - first_public_binding(&db, a, "x"), - &events, - ); - Ok(()) - } - - #[test] - fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { - fn x_rhs_expression(db: &TestDb) -> Expression<'_> { - let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, file_main); - // Get the second statement in `main.py` (x = …) and extract the expression - // node on the right-hand side: - let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; - - let index = semantic_index(db, file_main); - index.expression(x_rhs_node.as_ref()) - } - - let mut db = setup_db(); - - db.write_dedented( - "/src/mod.py", - r#" - class C: - def f(self): - self.attr: int | None = None - "#, - )?; - db.write_dedented( - "/src/main.py", - r#" - from mod import C - x = C().attr - "#, - )?; - - let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None"); - - // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred - db.write_dedented( - "/src/mod.py", - r#" - class C: - def f(self): - self.attr: str | None = None - "#, - )?; - - let events = { - db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); - db.take_salsa_events() - }; - assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events); - - // Add a comment; this should not trigger the type of `x` to be re-inferred - db.write_dedented( - "/src/mod.py", - r#" - class C: - def f(self): - # a comment! - self.attr: str | None = None - "#, - )?; - - let events = { - db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); - db.take_salsa_events() - }; - - assert_function_query_was_not_run( - &db, - infer_expression_types, - x_rhs_expression(&db), - &events, - ); - - Ok(()) - } - - /// This test verifies that changing a class's declaration in a non-meaningful way (e.g. by adding a comment) - /// doesn't trigger type inference for expressions that depend on the class's members. - #[test] - fn dependency_own_instance_member() -> anyhow::Result<()> { - fn x_rhs_expression(db: &TestDb) -> Expression<'_> { - let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, file_main); - // Get the second statement in `main.py` (x = …) and extract the expression - // node on the right-hand side: - let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; - - let index = semantic_index(db, file_main); - index.expression(x_rhs_node.as_ref()) - } - - let mut db = setup_db(); - - db.write_dedented( - "/src/mod.py", - r#" - class C: - if random.choice([True, False]): - attr: int = 42 - else: - attr: None = None - "#, - )?; - db.write_dedented( - "/src/main.py", - r#" - from mod import C - x = C().attr - "#, - )?; - - let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None"); - - // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred - db.write_dedented( - "/src/mod.py", - r#" - class C: - if random.choice([True, False]): - attr: str = "42" - else: - attr: None = None - "#, - )?; - - let events = { - db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); - db.take_salsa_events() - }; - assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events); - - // Add a comment; this should not trigger the type of `x` to be re-inferred - db.write_dedented( - "/src/mod.py", - r#" - class C: - # comment - if random.choice([True, False]): - attr: str = "42" - else: - attr: None = None - "#, - )?; - - let events = { - db.clear_salsa_events(); - let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); - db.take_salsa_events() - }; - - assert_function_query_was_not_run( - &db, - infer_expression_types, - x_rhs_expression(&db), - &events, - ); - - Ok(()) - } -} diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs deleted file mode 100644 index f1d945cae8a8e..0000000000000 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::collections::VecDeque; -use std::ops::Deref; - -use rustc_hash::FxHashSet; - -use crate::types::class_base::ClassBase; -use crate::types::generics::Specialization; -use crate::types::{ClassLiteralType, ClassType, Type}; -use crate::Db; - -/// The inferred method resolution order of a given class. -/// -/// An MRO cannot contain non-specialized generic classes. (This is why [`ClassBase`] contains a -/// [`ClassType`], not a [`ClassLiteralType`].) Any generic classes in a base class list are always -/// specialized — either because the class is explicitly specialized if there is a subscript -/// expression, or because we create the default specialization if there isn't. -/// -/// The MRO of a non-specialized generic class can contain generic classes that are specialized -/// with a typevar from the inheriting class. When the inheriting class is specialized, the MRO of -/// the resulting generic alias will substitute those type variables accordingly. For instance, in -/// the following example, the MRO of `D[int]` includes `C[int]`, and the MRO of `D[U]` includes -/// `C[U]` (which is a generic alias, not a non-specialized generic class): -/// -/// ```py -/// class C[T]: ... -/// class D[U](C[U]): ... -/// ``` -/// -/// See [`ClassType::iter_mro`] for more details. -#[derive(PartialEq, Eq, Clone, Debug, salsa::Update)] -pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>); - -impl<'db> Mro<'db> { - /// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of - /// base classes in the class definition, this operation is performed on a [class - /// literal][ClassLiteralType], not a [class type][ClassType]. (You can _also_ get the MRO of a - /// class type, but this is done by first getting the MRO of the underlying class literal, and - /// specializing each base class as needed if the class type is a generic alias.) - /// - /// In the event that a possible list of bases would (or could) lead to a `TypeError` being - /// raised at runtime due to an unresolvable MRO, we infer the MRO of the class as being `[, Unknown, object]`. This seems most likely to reduce the possibility of - /// cascading errors elsewhere. (For a generic class, the first entry in this fallback MRO uses - /// the default specialization of the class's type variables.) - /// - /// (We emit a diagnostic warning about the runtime `TypeError` in - /// [`super::infer::TypeInferenceBuilder::infer_region_scope`].) - pub(super) fn of_class( - db: &'db dyn Db, - class: ClassLiteralType<'db>, - specialization: Option>, - ) -> Result> { - Self::of_class_impl(db, class, specialization).map_err(|err| { - err.into_mro_error(db, class.apply_optional_specialization(db, specialization)) - }) - } - - pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { - Self::from([ - ClassBase::Class(class), - ClassBase::unknown(), - ClassBase::object(db), - ]) - } - - fn of_class_impl( - db: &'db dyn Db, - class: ClassLiteralType<'db>, - specialization: Option>, - ) -> Result> { - let class_bases = class.explicit_bases(db); - - if !class_bases.is_empty() && class.inheritance_cycle(db).is_some() { - // We emit errors for cyclically defined classes elsewhere. - // It's important that we don't even try to infer the MRO for a cyclically defined class, - // or we'll end up in an infinite loop. - return Ok(Mro::from_error( - db, - class.apply_optional_specialization(db, specialization), - )); - } - - match class_bases { - // `builtins.object` is the special case: - // the only class in Python that has an MRO with length <2 - [] if class.is_object(db) => Ok(Self::from([ - // object is not generic, so the default specialization should be a no-op - ClassBase::Class(class.apply_optional_specialization(db, specialization)), - ])), - - // All other classes in Python have an MRO with length >=2. - // Even if a class has no explicit base classes, - // it will implicitly inherit from `object` at runtime; - // `object` will appear in the class's `__bases__` list and `__mro__`: - // - // ```pycon - // >>> class Foo: ... - // ... - // >>> Foo.__bases__ - // (,) - // >>> Foo.__mro__ - // (, ) - // ``` - [] => Ok(Self::from([ - ClassBase::Class(class.apply_optional_specialization(db, specialization)), - ClassBase::object(db), - ])), - - // Fast path for a class that has only a single explicit base. - // - // This *could* theoretically be handled by the final branch below, - // but it's a common case (i.e., worth optimizing for), - // and the `c3_merge` function requires lots of allocations. - [single_base] => ClassBase::try_from_type(db, *single_base).map_or_else( - || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))), - |single_base| { - Ok(std::iter::once(ClassBase::Class( - class.apply_optional_specialization(db, specialization), - )) - .chain(single_base.mro(db)) - .collect()) - }, - ), - - // The class has multiple explicit bases. - // - // We'll fallback to a full implementation of the C3-merge algorithm to determine - // what MRO Python will give this class at runtime - // (if an MRO is indeed resolvable at all!) - multiple_bases => { - let mut valid_bases = vec![]; - let mut invalid_bases = vec![]; - - for (i, base) in multiple_bases.iter().enumerate() { - match ClassBase::try_from_type(db, *base) { - Some(valid_base) => valid_bases.push(valid_base), - None => invalid_bases.push((i, *base)), - } - } - - if !invalid_bases.is_empty() { - return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())); - } - - let mut seqs = vec![VecDeque::from([ClassBase::Class( - class.apply_optional_specialization(db, specialization), - )])]; - for base in &valid_bases { - seqs.push(base.mro(db).collect()); - } - seqs.push(valid_bases.iter().copied().collect()); - - c3_merge(seqs).ok_or_else(|| { - let mut seen_bases = FxHashSet::default(); - let mut duplicate_bases = vec![]; - for (index, base) in valid_bases - .iter() - .enumerate() - .filter_map(|(index, base)| Some((index, base.into_class()?))) - { - if !seen_bases.insert(base) { - let (base_class_literal, _) = base.class_literal(db); - duplicate_bases.push((index, base_class_literal)); - } - } - - if duplicate_bases.is_empty() { - MroErrorKind::UnresolvableMro { - bases_list: valid_bases.into_boxed_slice(), - } - } else { - MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice()) - } - }) - } - } - } -} - -impl<'db, const N: usize> From<[ClassBase<'db>; N]> for Mro<'db> { - fn from(value: [ClassBase<'db>; N]) -> Self { - Self(Box::from(value)) - } -} - -impl<'db> From>> for Mro<'db> { - fn from(value: Vec>) -> Self { - Self(value.into_boxed_slice()) - } -} - -impl<'db> Deref for Mro<'db> { - type Target = [ClassBase<'db>]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'db> FromIterator> for Mro<'db> { - fn from_iter>>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - -/// Iterator that yields elements of a class's MRO. -/// -/// We avoid materialising the *full* MRO unless it is actually necessary: -/// - Materialising the full MRO is expensive -/// - We need to do it for every class in the code that we're checking, as we need to make sure -/// that there are no class definitions in the code we're checking that would cause an -/// exception to be raised at runtime. But the same does *not* necessarily apply for every class -/// in third-party and stdlib dependencies: we never emit diagnostics about non-first-party code. -/// - However, we *do* need to resolve attribute accesses on classes/instances from -/// third-party and stdlib dependencies. That requires iterating over the MRO of third-party/stdlib -/// classes, but not necessarily the *whole* MRO: often just the first element is enough. -/// Luckily we know that for any class `X`, the first element of `X`'s MRO will always be `X` itself. -/// We can therefore avoid resolving the full MRO for many third-party/stdlib classes while still -/// being faithful to the runtime semantics. -/// -/// Even for first-party code, where we will have to resolve the MRO for every class we encounter, -/// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the -/// Salsa-tracked [`ClassLiteralType::try_mro`] method unless it's absolutely necessary. -pub(super) struct MroIterator<'db> { - db: &'db dyn Db, - - /// The class whose MRO we're iterating over - class: ClassLiteralType<'db>, - - /// The specialization to apply to each MRO element, if any - specialization: Option>, - - /// Whether or not we've already yielded the first element of the MRO - first_element_yielded: bool, - - /// Iterator over all elements of the MRO except the first. - /// - /// The full MRO is expensive to materialize, so this field is `None` - /// unless we actually *need* to iterate past the first element of the MRO, - /// at which point it is lazily materialized. - subsequent_elements: Option>>, -} - -impl<'db> MroIterator<'db> { - pub(super) fn new( - db: &'db dyn Db, - class: ClassLiteralType<'db>, - specialization: Option>, - ) -> Self { - Self { - db, - class, - specialization, - first_element_yielded: false, - subsequent_elements: None, - } - } - - /// Materialize the full MRO of the class. - /// Return an iterator over that MRO which skips the first element of the MRO. - fn full_mro_except_first_element(&mut self) -> impl Iterator> + '_ { - self.subsequent_elements - .get_or_insert_with(|| { - let mut full_mro_iter = match self.class.try_mro(self.db, self.specialization) { - Ok(mro) => mro.iter(), - Err(error) => error.fallback_mro().iter(), - }; - full_mro_iter.next(); - full_mro_iter - }) - .copied() - } -} - -impl<'db> Iterator for MroIterator<'db> { - type Item = ClassBase<'db>; - - fn next(&mut self) -> Option { - if !self.first_element_yielded { - self.first_element_yielded = true; - return Some(ClassBase::Class( - self.class - .apply_optional_specialization(self.db, self.specialization), - )); - } - self.full_mro_except_first_element().next() - } -} - -impl std::iter::FusedIterator for MroIterator<'_> {} - -#[derive(Debug, PartialEq, Eq, salsa::Update)] -pub(super) struct MroError<'db> { - kind: MroErrorKind<'db>, - fallback_mro: Mro<'db>, -} - -impl<'db> MroError<'db> { - /// Return an [`MroErrorKind`] variant describing why we could not resolve the MRO for this class. - pub(super) fn reason(&self) -> &MroErrorKind<'db> { - &self.kind - } - - /// Return the fallback MRO we should infer for this class during type inference - /// (since accurate resolution of its "true" MRO was impossible) - pub(super) fn fallback_mro(&self) -> &Mro<'db> { - &self.fallback_mro - } -} - -/// Possible ways in which attempting to resolve the MRO of a class might fail. -#[derive(Debug, PartialEq, Eq, salsa::Update)] -pub(super) enum MroErrorKind<'db> { - /// The class inherits from one or more invalid bases. - /// - /// To avoid excessive complexity in our implementation, - /// we only permit classes to inherit from class-literal types, - /// `Todo`, `Unknown` or `Any`. Anything else results in us - /// emitting a diagnostic. - /// - /// This variant records the indices and types of class bases - /// that we deem to be invalid. The indices are the indices of nodes - /// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node. - /// Each index is the index of a node representing an invalid base. - InvalidBases(Box<[(usize, Type<'db>)]>), - - /// The class has one or more duplicate bases. - /// - /// This variant records the indices and [`ClassLiteralType`]s - /// of the duplicate bases. The indices are the indices of nodes - /// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node. - /// Each index is the index of a node representing a duplicate base. - DuplicateBases(Box<[(usize, ClassLiteralType<'db>)]>), - - /// The MRO is otherwise unresolvable through the C3-merge algorithm. - /// - /// See [`c3_merge`] for more details. - UnresolvableMro { bases_list: Box<[ClassBase<'db>]> }, -} - -impl<'db> MroErrorKind<'db> { - pub(super) fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> MroError<'db> { - MroError { - kind: self, - fallback_mro: Mro::from_error(db, class), - } - } -} - -/// Implementation of the [C3-merge algorithm] for calculating a Python class's -/// [method resolution order]. -/// -/// [C3-merge algorithm]: https://docs.python.org/3/howto/mro.html#python-2-3-mro -/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order -fn c3_merge(mut sequences: Vec>) -> Option { - // Most MROs aren't that long... - let mut mro = Vec::with_capacity(8); - - loop { - sequences.retain(|sequence| !sequence.is_empty()); - - if sequences.is_empty() { - return Some(Mro::from(mro)); - } - - // If the candidate exists "deeper down" in the inheritance hierarchy, - // we should refrain from adding it to the MRO for now. Add the first candidate - // for which this does not hold true. If this holds true for all candidates, - // return `None`; it will be impossible to find a consistent MRO for the class - // with the given bases. - let mro_entry = sequences.iter().find_map(|outer_sequence| { - let candidate = outer_sequence[0]; - - let not_head = sequences - .iter() - .all(|sequence| sequence.iter().skip(1).all(|base| base != &candidate)); - - not_head.then_some(candidate) - })?; - - mro.push(mro_entry); - - // Make sure we don't try to add the candidate to the MRO twice: - for sequence in &mut sequences { - if sequence[0] == mro_entry { - sequence.pop_front(); - } - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs deleted file mode 100644 index cf5431b47ec80..0000000000000 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ /dev/null @@ -1,678 +0,0 @@ -use crate::semantic_index::ast_ids::HasScopedExpressionId; -use crate::semantic_index::definition::Definition; -use crate::semantic_index::expression::Expression; -use crate::semantic_index::predicate::{ - PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, -}; -use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; -use crate::semantic_index::symbol_table; -use crate::types::infer::infer_same_file_expression_type; -use crate::types::{ - infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, SubclassOfType, - Truthiness, Type, UnionBuilder, -}; -use crate::Db; -use itertools::Itertools; -use ruff_python_ast as ast; -use ruff_python_ast::{BoolOp, ExprBoolOp}; -use rustc_hash::FxHashMap; -use std::collections::hash_map::Entry; -use std::sync::Arc; - -use super::UnionType; - -/// Return the type constraint that `test` (if true) would place on `definition`, if any. -/// -/// For example, if we have this code: -/// -/// ```python -/// y = 1 if flag else None -/// x = 1 if flag else None -/// if x is not None: -/// ... -/// ``` -/// -/// The `test` expression `x is not None` places the constraint "not None" on the definition of -/// `x`, so in that case we'd return `Some(Type::Intersection(negative=[Type::None]))`. -/// -/// But if we called this with the same `test` expression, but the `definition` of `y`, no -/// constraint is applied to that definition, so we'd just return `None`. -pub(crate) fn infer_narrowing_constraint<'db>( - db: &'db dyn Db, - predicate: Predicate<'db>, - definition: Definition<'db>, -) -> Option> { - let constraints = match predicate.node { - PredicateNode::Expression(expression) => { - if predicate.is_positive { - all_narrowing_constraints_for_expression(db, expression) - } else { - all_negative_narrowing_constraints_for_expression(db, expression) - } - } - PredicateNode::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern), - PredicateNode::StarImportPlaceholder(_) => return None, - }; - if let Some(constraints) = constraints { - constraints.get(&definition.symbol(db)).copied() - } else { - None - } -} - -#[allow(clippy::ref_option)] -#[salsa::tracked(return_ref)] -fn all_narrowing_constraints_for_pattern<'db>( - db: &'db dyn Db, - pattern: PatternPredicate<'db>, -) -> Option> { - NarrowingConstraintsBuilder::new(db, PredicateNode::Pattern(pattern), true).finish() -} - -#[allow(clippy::ref_option)] -#[salsa::tracked( - return_ref, - cycle_fn=constraints_for_expression_cycle_recover, - cycle_initial=constraints_for_expression_cycle_initial, -)] -fn all_narrowing_constraints_for_expression<'db>( - db: &'db dyn Db, - expression: Expression<'db>, -) -> Option> { - NarrowingConstraintsBuilder::new(db, PredicateNode::Expression(expression), true).finish() -} - -#[allow(clippy::ref_option)] -#[salsa::tracked( - return_ref, - cycle_fn=negative_constraints_for_expression_cycle_recover, - cycle_initial=negative_constraints_for_expression_cycle_initial, -)] -fn all_negative_narrowing_constraints_for_expression<'db>( - db: &'db dyn Db, - expression: Expression<'db>, -) -> Option> { - NarrowingConstraintsBuilder::new(db, PredicateNode::Expression(expression), false).finish() -} - -#[allow(clippy::ref_option)] -fn constraints_for_expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - -fn constraints_for_expression_cycle_initial<'db>( - _db: &'db dyn Db, - _expression: Expression<'db>, -) -> Option> { - None -} - -#[allow(clippy::ref_option)] -fn negative_constraints_for_expression_cycle_recover<'db>( - _db: &'db dyn Db, - _value: &Option>, - _count: u32, - _expression: Expression<'db>, -) -> salsa::CycleRecoveryAction>> { - salsa::CycleRecoveryAction::Iterate -} - -fn negative_constraints_for_expression_cycle_initial<'db>( - _db: &'db dyn Db, - _expression: Expression<'db>, -) -> Option> { - None -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KnownConstraintFunction { - /// `builtins.isinstance` - IsInstance, - /// `builtins.issubclass` - IsSubclass, -} - -impl KnownConstraintFunction { - /// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`. - /// - /// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604 - /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. - fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { - let constraint_fn = |class| match self { - KnownConstraintFunction::IsInstance => Type::instance(class), - KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class), - }; - - match classinfo { - Type::Tuple(tuple) => { - let mut builder = UnionBuilder::new(db); - for element in tuple.elements(db) { - builder = builder.add(self.generate_constraint(db, *element)?); - } - Some(builder.build()) - } - Type::ClassLiteral(class_literal) => { - // At runtime (on Python 3.11+), this will return `True` for classes that actually - // do inherit `typing.Any` and `False` otherwise. We could accurately model that? - if class_literal.is_known(db, KnownClass::Any) { - None - } else { - Some(constraint_fn(class_literal.default_specialization(db))) - } - } - Type::SubclassOf(subclass_of_ty) => { - subclass_of_ty.subclass_of().into_class().map(constraint_fn) - } - _ => None, - } - } -} - -type NarrowingConstraints<'db> = FxHashMap>; - -fn merge_constraints_and<'db>( - into: &mut NarrowingConstraints<'db>, - from: NarrowingConstraints<'db>, - db: &'db dyn Db, -) { - for (key, value) in from { - match into.entry(key) { - Entry::Occupied(mut entry) => { - *entry.get_mut() = IntersectionBuilder::new(db) - .add_positive(*entry.get()) - .add_positive(value) - .build(); - } - Entry::Vacant(entry) => { - entry.insert(value); - } - } - } -} - -fn merge_constraints_or<'db>( - into: &mut NarrowingConstraints<'db>, - from: &NarrowingConstraints<'db>, - db: &'db dyn Db, -) { - for (key, value) in from { - match into.entry(*key) { - Entry::Occupied(mut entry) => { - *entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build(); - } - Entry::Vacant(entry) => { - entry.insert(Type::object(db)); - } - } - } - for (key, value) in into.iter_mut() { - if !from.contains_key(key) { - *value = Type::object(db); - } - } -} - -struct NarrowingConstraintsBuilder<'db> { - db: &'db dyn Db, - predicate: PredicateNode<'db>, - is_positive: bool, -} - -impl<'db> NarrowingConstraintsBuilder<'db> { - fn new(db: &'db dyn Db, predicate: PredicateNode<'db>, is_positive: bool) -> Self { - Self { - db, - predicate, - is_positive, - } - } - - fn finish(mut self) -> Option> { - let constraints: Option> = match self.predicate { - PredicateNode::Expression(expression) => { - self.evaluate_expression_predicate(expression, self.is_positive) - } - PredicateNode::Pattern(pattern) => self.evaluate_pattern_predicate(pattern), - PredicateNode::StarImportPlaceholder(_) => return None, - }; - if let Some(mut constraints) = constraints { - constraints.shrink_to_fit(); - Some(constraints) - } else { - None - } - } - - fn evaluate_expression_predicate( - &mut self, - expression: Expression<'db>, - is_positive: bool, - ) -> Option> { - let expression_node = expression.node_ref(self.db).node(); - self.evaluate_expression_node_predicate(expression_node, expression, is_positive) - } - - fn evaluate_expression_node_predicate( - &mut self, - expression_node: &ruff_python_ast::Expr, - expression: Expression<'db>, - is_positive: bool, - ) -> Option> { - match expression_node { - ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)), - ast::Expr::Compare(expr_compare) => { - self.evaluate_expr_compare(expr_compare, expression, is_positive) - } - ast::Expr::Call(expr_call) => { - self.evaluate_expr_call(expr_call, expression, is_positive) - } - ast::Expr::UnaryOp(unary_op) if unary_op.op == ast::UnaryOp::Not => { - self.evaluate_expression_node_predicate(&unary_op.operand, expression, !is_positive) - } - ast::Expr::BoolOp(bool_op) => self.evaluate_bool_op(bool_op, expression, is_positive), - _ => None, // TODO other test expression kinds - } - } - - fn evaluate_pattern_predicate_kind( - &mut self, - pattern_predicate_kind: &PatternPredicateKind<'db>, - subject: Expression<'db>, - ) -> Option> { - match pattern_predicate_kind { - PatternPredicateKind::Singleton(singleton) => { - self.evaluate_match_pattern_singleton(subject, *singleton) - } - PatternPredicateKind::Class(cls) => self.evaluate_match_pattern_class(subject, *cls), - PatternPredicateKind::Value(expr) => self.evaluate_match_pattern_value(subject, *expr), - PatternPredicateKind::Or(predicates) => { - self.evaluate_match_pattern_or(subject, predicates) - } - PatternPredicateKind::Unsupported => None, - } - } - - fn evaluate_pattern_predicate( - &mut self, - pattern: PatternPredicate<'db>, - ) -> Option> { - let subject = pattern.subject(self.db); - - self.evaluate_pattern_predicate_kind(pattern.kind(self.db), subject) - } - - fn symbols(&self) -> Arc { - symbol_table(self.db, self.scope()) - } - - fn scope(&self) -> ScopeId<'db> { - match self.predicate { - PredicateNode::Expression(expression) => expression.scope(self.db), - PredicateNode::Pattern(pattern) => pattern.scope(self.db), - PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db), - } - } - - #[track_caller] - fn expect_expr_name_symbol(&self, symbol: &str) -> ScopedSymbolId { - self.symbols() - .symbol_id_by_name(symbol) - .expect("We should always have a symbol for every `Name` node") - } - - fn evaluate_expr_name( - &mut self, - expr_name: &ast::ExprName, - is_positive: bool, - ) -> NarrowingConstraints<'db> { - let ast::ExprName { id, .. } = expr_name; - - let symbol = self.expect_expr_name_symbol(id); - - let ty = if is_positive { - Type::AlwaysFalsy.negate(self.db) - } else { - Type::AlwaysTruthy.negate(self.db) - }; - - NarrowingConstraints::from_iter([(symbol, ty)]) - } - - fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { - if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { - match rhs_ty { - Type::Tuple(rhs_tuple) => Some(UnionType::from_elements( - self.db, - rhs_tuple.elements(self.db), - )), - - Type::StringLiteral(string_literal) => Some(UnionType::from_elements( - self.db, - string_literal - .iter_each_char(self.db) - .map(Type::StringLiteral), - )), - - _ => None, - } - } else { - None - } - } - - fn evaluate_expr_compare( - &mut self, - expr_compare: &ast::ExprCompare, - expression: Expression<'db>, - is_positive: bool, - ) -> Option> { - fn is_narrowing_target_candidate(expr: &ast::Expr) -> bool { - matches!(expr, ast::Expr::Name(_) | ast::Expr::Call(_)) - } - - let ast::ExprCompare { - range: _, - left, - ops, - comparators, - } = expr_compare; - - // Performance optimization: early return if there are no potential narrowing targets. - if !is_narrowing_target_candidate(left) - && comparators - .iter() - .all(|c| !is_narrowing_target_candidate(c)) - { - return None; - } - - if !is_positive && comparators.len() > 1 { - // We can't negate a constraint made by a multi-comparator expression, since we can't - // know which comparison part is the one being negated. - // For example, the negation of `x is 1 is y is 2`, would be `(x is not 1) or (y is not 1) or (y is not 2)` - // and that requires cross-symbol constraints, which we don't support yet. - return None; - } - let scope = self.scope(); - let inference = infer_expression_types(self.db, expression); - - let comparator_tuples = std::iter::once(&**left) - .chain(comparators) - .tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>(); - let mut constraints = NarrowingConstraints::default(); - - let mut last_rhs_ty: Option = None; - - for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) { - let lhs_ty = last_rhs_ty.unwrap_or_else(|| { - inference.expression_type(left.scoped_expression_id(self.db, scope)) - }); - let rhs_ty = inference.expression_type(right.scoped_expression_id(self.db, scope)); - last_rhs_ty = Some(rhs_ty); - - match left { - ast::Expr::Name(ast::ExprName { - range: _, - id, - ctx: _, - }) => { - let symbol = self.expect_expr_name_symbol(id); - - match if is_positive { *op } else { op.negate() } { - ast::CmpOp::IsNot => { - if rhs_ty.is_singleton(self.db) { - let ty = IntersectionBuilder::new(self.db) - .add_negative(rhs_ty) - .build(); - constraints.insert(symbol, ty); - } else { - // Non-singletons cannot be safely narrowed using `is not` - } - } - ast::CmpOp::Is => { - constraints.insert(symbol, rhs_ty); - } - ast::CmpOp::NotEq => { - if rhs_ty.is_single_valued(self.db) { - let ty = IntersectionBuilder::new(self.db) - .add_negative(rhs_ty) - .build(); - constraints.insert(symbol, ty); - } - } - ast::CmpOp::Eq if lhs_ty.is_literal_string() => { - constraints.insert(symbol, rhs_ty); - } - ast::CmpOp::In => { - if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) { - constraints.insert(symbol, ty); - } - } - ast::CmpOp::NotIn => { - if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) { - constraints.insert(symbol, ty.negate(self.db)); - } - } - _ => { - // TODO other comparison types - } - } - } - ast::Expr::Call(ast::ExprCall { - range: _, - func: callable, - arguments: - ast::Arguments { - args, - keywords, - range: _, - }, - }) if keywords.is_empty() => { - let rhs_class = match rhs_ty { - Type::ClassLiteral(class) => class, - Type::GenericAlias(alias) => { - ClassLiteralType::Generic(alias.origin(self.db)) - } - _ => { - continue; - } - }; - - let [ast::Expr::Name(ast::ExprName { id, .. })] = &**args else { - continue; - }; - - let is_valid_constraint = if is_positive { - op == &ast::CmpOp::Is - } else { - op == &ast::CmpOp::IsNot - }; - - if !is_valid_constraint { - continue; - } - - let callable_type = - inference.expression_type(callable.scoped_expression_id(self.db, scope)); - - if callable_type - .into_class_literal() - .is_some_and(|c| c.is_known(self.db, KnownClass::Type)) - { - let symbol = self.expect_expr_name_symbol(id); - constraints.insert( - symbol, - Type::instance(rhs_class.unknown_specialization(self.db)), - ); - } - } - _ => {} - } - } - Some(constraints) - } - - fn evaluate_expr_call( - &mut self, - expr_call: &ast::ExprCall, - expression: Expression<'db>, - is_positive: bool, - ) -> Option> { - let scope = self.scope(); - let inference = infer_expression_types(self.db, expression); - - let callable_ty = - inference.expression_type(expr_call.func.scoped_expression_id(self.db, scope)); - - // TODO: add support for PEP 604 union types on the right hand side of `isinstance` - // and `issubclass`, for example `isinstance(x, str | (int | float))`. - match callable_ty { - Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => { - let function = function_type.known(self.db)?.into_constraint_function()?; - - let [ast::Expr::Name(ast::ExprName { id, .. }), class_info] = - &*expr_call.arguments.args - else { - return None; - }; - - let symbol = self.expect_expr_name_symbol(id); - - let class_info_ty = - inference.expression_type(class_info.scoped_expression_id(self.db, scope)); - - function - .generate_constraint(self.db, class_info_ty) - .map(|constraint| { - NarrowingConstraints::from_iter([( - symbol, - constraint.negate_if(self.db, !is_positive), - )]) - }) - } - // for the expression `bool(E)`, we further narrow the type based on `E` - Type::ClassLiteral(class_type) - if expr_call.arguments.args.len() == 1 - && expr_call.arguments.keywords.is_empty() - && class_type.is_known(self.db, KnownClass::Bool) => - { - self.evaluate_expression_node_predicate( - &expr_call.arguments.args[0], - expression, - is_positive, - ) - } - _ => None, - } - } - - fn evaluate_match_pattern_singleton( - &mut self, - subject: Expression<'db>, - singleton: ast::Singleton, - ) -> Option> { - let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id); - - let ty = match singleton { - ast::Singleton::None => Type::none(self.db), - ast::Singleton::True => Type::BooleanLiteral(true), - ast::Singleton::False => Type::BooleanLiteral(false), - }; - Some(NarrowingConstraints::from_iter([(symbol, ty)])) - } - - fn evaluate_match_pattern_class( - &mut self, - subject: Expression<'db>, - cls: Expression<'db>, - ) -> Option> { - let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id); - let ty = infer_same_file_expression_type(self.db, cls).to_instance(self.db)?; - - Some(NarrowingConstraints::from_iter([(symbol, ty)])) - } - - fn evaluate_match_pattern_value( - &mut self, - subject: Expression<'db>, - value: Expression<'db>, - ) -> Option> { - let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id); - let ty = infer_same_file_expression_type(self.db, value); - Some(NarrowingConstraints::from_iter([(symbol, ty)])) - } - - fn evaluate_match_pattern_or( - &mut self, - subject: Expression<'db>, - predicates: &Vec>, - ) -> Option> { - let db = self.db; - - predicates - .iter() - .filter_map(|predicate| self.evaluate_pattern_predicate_kind(predicate, subject)) - .reduce(|mut constraints, constraints_| { - merge_constraints_or(&mut constraints, &constraints_, db); - constraints - }) - } - - fn evaluate_bool_op( - &mut self, - expr_bool_op: &ExprBoolOp, - expression: Expression<'db>, - is_positive: bool, - ) -> Option> { - let inference = infer_expression_types(self.db, expression); - let scope = self.scope(); - let mut sub_constraints = expr_bool_op - .values - .iter() - // filter our arms with statically known truthiness - .filter(|expr| { - inference - .expression_type(expr.scoped_expression_id(self.db, scope)) - .bool(self.db) - != match expr_bool_op.op { - BoolOp::And => Truthiness::AlwaysTrue, - BoolOp::Or => Truthiness::AlwaysFalse, - } - }) - .map(|sub_expr| { - self.evaluate_expression_node_predicate(sub_expr, expression, is_positive) - }) - .collect::>(); - match (expr_bool_op.op, is_positive) { - (BoolOp::And, true) | (BoolOp::Or, false) => { - let mut aggregation: Option = None; - for sub_constraint in sub_constraints.into_iter().flatten() { - if let Some(ref mut some_aggregation) = aggregation { - merge_constraints_and(some_aggregation, sub_constraint, self.db); - } else { - aggregation = Some(sub_constraint); - } - } - aggregation - } - (BoolOp::Or, true) | (BoolOp::And, false) => { - let (first, rest) = sub_constraints.split_first_mut()?; - if let Some(ref mut first) = first { - for rest_constraint in rest { - if let Some(rest_constraint) = rest_constraint { - merge_constraints_or(first, rest_constraint, self.db); - } else { - return None; - } - } - } - first.clone() - } - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/red_knot_python_semantic/src/types/property_tests.rs deleted file mode 100644 index cbcfef74ac2db..0000000000000 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ /dev/null @@ -1,298 +0,0 @@ -//! This module contains quickcheck-based property tests for `Type`s. -//! -//! These tests are disabled by default, as they are non-deterministic and slow. You can -//! run them explicitly using: -//! -//! ```sh -//! cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable -//! ``` -//! -//! The number of tests (default: 100) can be controlled by setting the `QUICKCHECK_TESTS` -//! environment variable. For example: -//! -//! ```sh -//! QUICKCHECK_TESTS=10000 cargo test … -//! ``` -//! -//! If you want to run these tests for a longer period of time, it's advisable to run them -//! in release mode. As some tests are slower than others, it's advisable to run them in a -//! loop until they fail: -//! -//! ```sh -//! export QUICKCHECK_TESTS=100000 -//! while cargo test --release -p red_knot_python_semantic -- \ -//! --ignored types::property_tests::stable; do :; done -//! ``` -mod setup; -mod type_generation; - -use type_generation::{intersection, union}; - -/// A macro to define a property test for types. -/// -/// The `$test_name` identifier specifies the name of the test function. The `$db` identifier -/// is used to refer to the salsa database in the property to be tested. The actual property is -/// specified using the syntax: -/// -/// forall types t1, t2, ..., tn . ` -/// -/// where `t1`, `t2`, ..., `tn` are identifiers that represent arbitrary types, and `` -/// is an expression using these identifiers. -/// -macro_rules! type_property_test { - ($test_name:ident, $db:ident, forall types $($types:ident),+ . $property:expr) => { - #[quickcheck_macros::quickcheck] - #[ignore] - fn $test_name($($types: crate::types::property_tests::type_generation::Ty),+) -> bool { - let $db = &crate::types::property_tests::setup::get_cached_db(); - $(let $types = $types.into_type($db);)+ - - $property - } - }; - // A property test with a logical implication. - ($name:ident, $db:ident, forall types $($types:ident),+ . $premise:expr => $conclusion:expr) => { - type_property_test!($name, $db, forall types $($types),+ . !($premise) || ($conclusion)); - }; -} - -mod stable { - use super::union; - use crate::types::Type; - - // Reflexivity: `T` is equivalent to itself. - type_property_test!( - equivalent_to_is_reflexive, db, - forall types t. t.is_fully_static(db) => t.is_equivalent_to(db, t) - ); - - // Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`. - // Note that this (trivially) holds true for gradual types as well. - type_property_test!( - equivalent_to_is_symmetric, db, - forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s) - ); - - // Transitivity: If `S` is equivalent to `T` and `T` is equivalent to `U`, then `S` must be equivalent to `U`. - type_property_test!( - equivalent_to_is_transitive, db, - forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u) - ); - - // Symmetry: If `S` is gradual equivalent to `T`, `T` is gradual equivalent to `S`. - type_property_test!( - gradual_equivalent_to_is_symmetric, db, - forall types s, t. s.is_gradual_equivalent_to(db, t) => t.is_gradual_equivalent_to(db, s) - ); - - // A fully static type `T` is a subtype of itself. - type_property_test!( - subtype_of_is_reflexive, db, - forall types t. t.is_fully_static(db) => t.is_subtype_of(db, t) - ); - - // `S <: T` and `T <: U` implies that `S <: U`. - type_property_test!( - subtype_of_is_transitive, db, - forall types s, t, u. s.is_subtype_of(db, t) && t.is_subtype_of(db, u) => s.is_subtype_of(db, u) - ); - - // `S <: T` and `T <: S` implies that `S` is equivalent to `T`. - type_property_test!( - subtype_of_is_antisymmetric, db, - forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t) - ); - - // `T` is not disjoint from itself, unless `T` is `Never`. - type_property_test!( - disjoint_from_is_irreflexive, db, - forall types t. t.is_disjoint_from(db, t) => t.is_never() - ); - - // `S` is disjoint from `T` implies that `T` is disjoint from `S`. - type_property_test!( - disjoint_from_is_symmetric, db, - forall types s, t. s.is_disjoint_from(db, t) == t.is_disjoint_from(db, s) - ); - - // `S <: T` implies that `S` is not disjoint from `T`, unless `S` is `Never`. - type_property_test!( - subtype_of_implies_not_disjoint_from, db, - forall types s, t. s.is_subtype_of(db, t) => !s.is_disjoint_from(db, t) || s.is_never() - ); - - // `S <: T` implies that `S` can be assigned to `T`. - type_property_test!( - subtype_of_implies_assignable_to, db, - forall types s, t. s.is_subtype_of(db, t) => s.is_assignable_to(db, t) - ); - - // If `T` is a singleton, it is also single-valued. - type_property_test!( - singleton_implies_single_valued, db, - forall types t. t.is_singleton(db) => t.is_single_valued(db) - ); - - // If `T` contains a gradual form, it should not participate in equivalence - type_property_test!( - non_fully_static_types_do_not_participate_in_equivalence, db, - forall types s, t. !s.is_fully_static(db) => !s.is_equivalent_to(db, t) && !t.is_equivalent_to(db, s) - ); - - // If `T` contains a gradual form, it should not participate in subtyping - type_property_test!( - non_fully_static_types_do_not_participate_in_subtyping, db, - forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s) - ); - - // All types should be assignable to `object` - type_property_test!( - all_types_assignable_to_object, db, - forall types t. t.is_assignable_to(db, Type::object(db)) - ); - - // And for fully static types, they should also be subtypes of `object` - type_property_test!( - all_fully_static_types_subtype_of_object, db, - forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db)) - ); - - // Never should be assignable to every type - type_property_test!( - never_assignable_to_every_type, db, - forall types t. Type::Never.is_assignable_to(db, t) - ); - - // And it should be a subtype of all fully static types - type_property_test!( - never_subtype_of_every_fully_static_type, db, - forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t) - ); - - // For any two fully static types, each type in the pair must be a subtype of their union. - type_property_test!( - all_fully_static_type_pairs_are_subtype_of_their_union, db, - forall types s, t. - s.is_fully_static(db) && t.is_fully_static(db) - => s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t])) - ); - - // A fully static type does not have any materializations. - // Thus, two equivalent (fully static) types are also gradual equivalent. - type_property_test!( - two_equivalent_types_are_also_gradual_equivalent, db, - forall types s, t. s.is_equivalent_to(db, t) => s.is_gradual_equivalent_to(db, t) - ); - - // Two gradual equivalent fully static types are also equivalent. - type_property_test!( - two_gradual_equivalent_fully_static_types_are_also_equivalent, db, - forall types s, t. - s.is_fully_static(db) && s.is_gradual_equivalent_to(db, t) => s.is_equivalent_to(db, t) - ); - - // `T` can be assigned to itself. - type_property_test!( - assignable_to_is_reflexive, db, - forall types t. t.is_assignable_to(db, t) - ); - - // For *any* pair of types, whether fully static or not, - // each of the pair should be assignable to the union of the two. - type_property_test!( - all_type_pairs_are_assignable_to_their_union, db, - forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t])) - ); -} - -/// This module contains property tests that currently lead to many false positives. -/// -/// The reason for this is our insufficient understanding of equivalence of types. For -/// example, we currently consider `int | str` and `str | int` to be different types. -/// Similar issues exist for intersection types. Once this is resolved, we can move these -/// tests to the `stable` section. In the meantime, it can still be useful to run these -/// tests (using [`types::property_tests::flaky`]), to see if there are any new obvious bugs. -mod flaky { - use itertools::Itertools; - - use super::{intersection, union}; - - // Negating `T` twice is equivalent to `T`. - type_property_test!( - double_negation_is_identity, db, - forall types t. t.negate(db).negate(db).is_equivalent_to(db, t) - ); - - // ~T should be disjoint from T - type_property_test!( - negation_is_disjoint, db, - forall types t. t.is_fully_static(db) => t.negate(db).is_disjoint_from(db, t) - ); - - // For two fully static types, their intersection must be a subtype of each type in the pair. - type_property_test!( - all_fully_static_type_pairs_are_supertypes_of_their_intersection, db, - forall types s, t. - s.is_fully_static(db) && t.is_fully_static(db) - => intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t) - ); - - // And for non-fully-static types, the intersection of a pair of types - // should be assignable to both types of the pair. - // Currently fails due to https://github.com/astral-sh/ruff/issues/14899 - type_property_test!( - all_type_pairs_can_be_assigned_from_their_intersection, db, - forall types s, t. intersection(db, [s, t]).is_assignable_to(db, s) && intersection(db, [s, t]).is_assignable_to(db, t) - ); - - // Equal element sets of intersections implies equivalence - // flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513 - type_property_test!( - intersection_equivalence_not_order_dependent, db, - forall types s, t, u. - s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db) - => [s, t, u] - .into_iter() - .permutations(3) - .map(|trio_of_types| intersection(db, trio_of_types)) - .permutations(2) - .all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, vec_of_intersections[1])) - ); - - // Equal element sets of unions implies equivalence - // 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. - s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db) - => [s, t, u] - .into_iter() - .permutations(3) - .map(|trio_of_types| union(db, trio_of_types)) - .permutations(2) - .all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, vec_of_unions[1])) - ); - - // `S | T` is always a supertype of `S`. - // Thus, `S` is never disjoint from `S | T`. - type_property_test!( - constituent_members_of_union_is_not_disjoint_from_that_union, db, - 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)) - ); -} diff --git a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs deleted file mode 100644 index 807219015961a..0000000000000 --- a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs +++ /dev/null @@ -1,469 +0,0 @@ -use crate::db::tests::TestDb; -use crate::symbol::{builtins_symbol, known_module_symbol}; -use crate::types::{ - BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, Parameter, - Parameters, Signature, SubclassOfType, TupleType, Type, UnionType, -}; -use crate::{Db, KnownModule}; -use hashbrown::HashSet; -use quickcheck::{Arbitrary, Gen}; -use ruff_python_ast::name::Name; - -/// A test representation of a type that can be transformed unambiguously into a real Type, -/// given a db. -/// -/// TODO: We should add some variants that exercise generic classes and specializations thereof. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum Ty { - Never, - Unknown, - None, - Any, - IntLiteral(i64), - BooleanLiteral(bool), - StringLiteral(&'static str), - LiteralString, - BytesLiteral(&'static str), - // BuiltinInstance("str") corresponds to an instance of the builtin `str` class - BuiltinInstance(&'static str), - /// Members of the `abc` stdlib module - AbcInstance(&'static str), - AbcClassLiteral(&'static str), - TypingLiteral, - // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself - BuiltinClassLiteral(&'static str), - KnownClassInstance(KnownClass), - Union(Vec), - Intersection { - pos: Vec, - neg: Vec, - }, - Tuple(Vec), - SubclassOfAny, - SubclassOfBuiltinClass(&'static str), - SubclassOfAbcClass(&'static str), - AlwaysTruthy, - AlwaysFalsy, - BuiltinsFunction(&'static str), - BuiltinsBoundMethod { - class: &'static str, - method: &'static str, - }, - Callable { - params: CallableParams, - returns: Option>, - }, -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum CallableParams { - GradualForm, - List(Vec), -} - -impl CallableParams { - pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> { - match self { - CallableParams::GradualForm => Parameters::gradual_form(), - CallableParams::List(params) => Parameters::new(params.into_iter().map(|param| { - let mut parameter = match param.kind { - ParamKind::PositionalOnly => Parameter::positional_only(param.name), - ParamKind::PositionalOrKeyword => { - Parameter::positional_or_keyword(param.name.unwrap()) - } - ParamKind::Variadic => Parameter::variadic(param.name.unwrap()), - ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()), - ParamKind::KeywordVariadic => Parameter::keyword_variadic(param.name.unwrap()), - }; - if let Some(annotated_ty) = param.annotated_ty { - parameter = parameter.with_annotated_type(annotated_ty.into_type(db)); - } - if let Some(default_ty) = param.default_ty { - parameter = parameter.with_default_type(default_ty.into_type(db)); - } - parameter - })), - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Param { - kind: ParamKind, - name: Option, - annotated_ty: Option, - default_ty: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -enum ParamKind { - PositionalOnly, - PositionalOrKeyword, - Variadic, - KeywordOnly, - KeywordVariadic, -} - -#[salsa::tracked] -fn create_bound_method<'db>( - db: &'db dyn Db, - function: Type<'db>, - builtins_class: Type<'db>, -) -> Type<'db> { - Type::BoundMethod(BoundMethodType::new( - db, - function.expect_function_literal(), - builtins_class.to_instance(db).unwrap(), - )) -} - -impl Ty { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { - match self { - Ty::Never => Type::Never, - Ty::Unknown => Type::unknown(), - Ty::None => Type::none(db), - Ty::Any => Type::any(), - Ty::IntLiteral(n) => Type::IntLiteral(n), - Ty::StringLiteral(s) => Type::string_literal(db, s), - Ty::BooleanLiteral(b) => Type::BooleanLiteral(b), - Ty::LiteralString => Type::LiteralString, - Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), - Ty::BuiltinInstance(s) => builtins_symbol(db, s) - .symbol - .expect_type() - .to_instance(db) - .unwrap(), - Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) - .symbol - .expect_type() - .to_instance(db) - .unwrap(), - Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) - .symbol - .expect_type(), - Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal), - Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).symbol.expect_type(), - Ty::KnownClassInstance(known_class) => known_class.to_instance(db), - Ty::Union(tys) => { - UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) - } - Ty::Intersection { pos, neg } => { - let mut builder = IntersectionBuilder::new(db); - for p in pos { - builder = builder.add_positive(p.into_type(db)); - } - for n in neg { - builder = builder.add_negative(n.into_type(db)); - } - builder.build() - } - Ty::Tuple(tys) => { - let elements = tys.into_iter().map(|ty| ty.into_type(db)); - TupleType::from_elements(db, elements) - } - Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), - Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( - db, - builtins_symbol(db, s) - .symbol - .expect_type() - .expect_class_literal() - .default_specialization(db), - ), - Ty::SubclassOfAbcClass(s) => SubclassOfType::from( - db, - known_module_symbol(db, KnownModule::Abc, s) - .symbol - .expect_type() - .expect_class_literal() - .default_specialization(db), - ), - Ty::AlwaysTruthy => Type::AlwaysTruthy, - Ty::AlwaysFalsy => Type::AlwaysFalsy, - Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(), - Ty::BuiltinsBoundMethod { class, method } => { - let builtins_class = builtins_symbol(db, class).symbol.expect_type(); - let function = builtins_class.member(db, method).symbol.expect_type(); - - create_bound_method(db, function, builtins_class) - } - Ty::Callable { params, returns } => Type::Callable(CallableType::new( - db, - Signature::new( - params.into_parameters(db), - returns.map(|ty| ty.into_type(db)), - ), - )), - } - } -} - -fn arbitrary_core_type(g: &mut Gen) -> Ty { - // We could select a random integer here, but this would make it much less - // likely to explore interesting edge cases: - let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap()); - let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g)); - g.choose(&[ - Ty::Never, - Ty::Unknown, - Ty::None, - Ty::Any, - int_lit, - bool_lit, - Ty::StringLiteral(""), - Ty::StringLiteral("a"), - Ty::LiteralString, - Ty::BytesLiteral(""), - Ty::BytesLiteral("\x00"), - Ty::KnownClassInstance(KnownClass::Object), - Ty::KnownClassInstance(KnownClass::Str), - Ty::KnownClassInstance(KnownClass::Int), - Ty::KnownClassInstance(KnownClass::Bool), - Ty::KnownClassInstance(KnownClass::List), - Ty::KnownClassInstance(KnownClass::Tuple), - Ty::KnownClassInstance(KnownClass::FunctionType), - Ty::KnownClassInstance(KnownClass::SpecialForm), - Ty::KnownClassInstance(KnownClass::TypeVar), - Ty::KnownClassInstance(KnownClass::TypeAliasType), - Ty::KnownClassInstance(KnownClass::NoDefaultType), - Ty::TypingLiteral, - Ty::BuiltinClassLiteral("str"), - Ty::BuiltinClassLiteral("int"), - Ty::BuiltinClassLiteral("bool"), - Ty::BuiltinClassLiteral("object"), - Ty::BuiltinInstance("type"), - Ty::AbcInstance("ABC"), - Ty::AbcInstance("ABCMeta"), - Ty::SubclassOfAny, - Ty::SubclassOfBuiltinClass("object"), - Ty::SubclassOfBuiltinClass("str"), - Ty::SubclassOfBuiltinClass("type"), - Ty::AbcClassLiteral("ABC"), - Ty::AbcClassLiteral("ABCMeta"), - Ty::SubclassOfAbcClass("ABC"), - Ty::SubclassOfAbcClass("ABCMeta"), - Ty::AlwaysTruthy, - Ty::AlwaysFalsy, - Ty::BuiltinsFunction("chr"), - Ty::BuiltinsFunction("ascii"), - Ty::BuiltinsBoundMethod { - class: "str", - method: "isascii", - }, - Ty::BuiltinsBoundMethod { - class: "int", - method: "bit_length", - }, - ]) - .unwrap() - .clone() -} - -/// Constructs an arbitrary type. -/// -/// The `size` parameter controls the depth of the type tree. For example, -/// a simple type like `int` has a size of 0, `Union[int, str]` has a size -/// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc. -fn arbitrary_type(g: &mut Gen, size: u32) -> Ty { - if size == 0 { - arbitrary_core_type(g) - } else { - match u32::arbitrary(g) % 5 { - 0 => arbitrary_core_type(g), - 1 => Ty::Union( - (0..*g.choose(&[2, 3]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - ), - 2 => Ty::Tuple( - (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - ), - 3 => Ty::Intersection { - pos: (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - neg: (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - }, - 4 => Ty::Callable { - params: match u32::arbitrary(g) % 2 { - 0 => CallableParams::GradualForm, - 1 => CallableParams::List(arbitrary_parameter_list(g, size)), - _ => unreachable!(), - }, - returns: arbitrary_optional_type(g, size - 1).map(Box::new), - }, - _ => unreachable!(), - } - } -} - -fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec { - let mut params: Vec = vec![]; - let mut used_names = HashSet::new(); - - // First, choose the number of parameters to generate. - for _ in 0..*g.choose(&[0, 1, 2, 3, 4, 5]).unwrap() { - // Next, choose the kind of parameters that can be generated based on the last parameter. - let next_kind = match params.last().map(|p| p.kind) { - None | Some(ParamKind::PositionalOnly) => *g - .choose(&[ - ParamKind::PositionalOnly, - ParamKind::PositionalOrKeyword, - ParamKind::Variadic, - ParamKind::KeywordOnly, - ParamKind::KeywordVariadic, - ]) - .unwrap(), - Some(ParamKind::PositionalOrKeyword) => *g - .choose(&[ - ParamKind::PositionalOrKeyword, - ParamKind::Variadic, - ParamKind::KeywordOnly, - ParamKind::KeywordVariadic, - ]) - .unwrap(), - Some(ParamKind::Variadic | ParamKind::KeywordOnly) => *g - .choose(&[ParamKind::KeywordOnly, ParamKind::KeywordVariadic]) - .unwrap(), - Some(ParamKind::KeywordVariadic) => { - // There can't be any other parameter kind after a keyword variadic parameter. - break; - } - }; - - let name = loop { - let name = if matches!(next_kind, ParamKind::PositionalOnly) { - arbitrary_optional_name(g) - } else { - Some(arbitrary_name(g)) - }; - if let Some(name) = name { - if used_names.insert(name.clone()) { - break Some(name); - } - } else { - break None; - } - }; - - params.push(Param { - kind: next_kind, - name, - annotated_ty: arbitrary_optional_type(g, size), - default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) { - None - } else { - arbitrary_optional_type(g, size) - }, - }); - } - - params -} - -fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option { - match u32::arbitrary(g) % 2 { - 0 => None, - 1 => Some(arbitrary_type(g, size)), - _ => unreachable!(), - } -} - -fn arbitrary_name(g: &mut Gen) -> Name { - Name::new(format!("n{}", u32::arbitrary(g) % 10)) -} - -fn arbitrary_optional_name(g: &mut Gen) -> Option { - match u32::arbitrary(g) % 2 { - 0 => None, - 1 => Some(arbitrary_name(g)), - _ => unreachable!(), - } -} - -impl Arbitrary for Ty { - fn arbitrary(g: &mut Gen) -> Ty { - const MAX_SIZE: u32 = 2; - arbitrary_type(g, MAX_SIZE) - } - - fn shrink(&self) -> Box> { - match self.clone() { - Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { - 0 => None, - 1 => Some(elts.into_iter().next().unwrap()), - _ => Some(Ty::Union(elts)), - })), - Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { - 0 => None, - 1 => Some(elts.into_iter().next().unwrap()), - _ => Some(Ty::Tuple(elts)), - })), - Ty::Intersection { pos, neg } => { - // Shrinking on intersections is not exhaustive! - // - // We try to shrink the positive side or the negative side, - // but we aren't shrinking both at the same time. - // - // This should remove positive or negative constraints but - // won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink - // iteration. - // - // Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails - // so that shrinking can happen there. - let pos_orig = pos.clone(); - let neg_orig = neg.clone(); - Box::new( - // we shrink negative constraints first, as - // intersections with only negative constraints are - // more confusing - neg.shrink() - .map(move |shrunk_neg| Ty::Intersection { - pos: pos_orig.clone(), - neg: shrunk_neg, - }) - .chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection { - pos: shrunk_pos, - neg: neg_orig.clone(), - })) - .filter_map(|ty| { - if let Ty::Intersection { pos, neg } = &ty { - match (pos.len(), neg.len()) { - // an empty intersection does not mean - // anything - (0, 0) => None, - // a single positive element should be - // unwrapped - (1, 0) => Some(pos[0].clone()), - _ => Some(ty), - } - } else { - unreachable!() - } - }), - ) - } - _ => Box::new(std::iter::empty()), - } - } -} - -pub(crate) fn intersection<'db>( - db: &'db TestDb, - tys: impl IntoIterator>, -) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); - for ty in tys { - builder = builder.add_positive(ty); - } - builder.build() -} - -pub(crate) fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { - UnionType::from_elements(db, tys) -} diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs deleted file mode 100644 index 50c022e8d6de2..0000000000000 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ /dev/null @@ -1,1632 +0,0 @@ -//! _Signatures_ describe the expected parameters and return type of a function or other callable. -//! Overloads and unions add complexity to this simple description. -//! -//! In a call expression, the type of the callable might be a union of several types. The call must -//! be compatible with _all_ of these types, since at runtime the callable might be an instance of -//! any of them. -//! -//! Each of the atomic types in the union must be callable. Each callable might be _overloaded_, -//! containing multiple _overload signatures_, each of which describes a different combination of -//! argument types and return types. For each callable type in the union, the call expression's -//! arguments must match _at least one_ overload. - -use std::{collections::HashMap, slice::Iter}; - -use itertools::EitherOrBoth; -use smallvec::{smallvec, SmallVec}; - -use super::{definition_expression_type, DynamicType, Type}; -use crate::semantic_index::definition::Definition; -use crate::types::generics::{GenericContext, Specialization}; -use crate::types::todo_type; -use crate::Db; -use ruff_python_ast::{self as ast, name::Name}; - -/// The signature of a possible union of callables. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) struct Signatures<'db> { - /// The type that is (hopefully) callable. - pub(crate) callable_type: Type<'db>, - /// The type we'll use for error messages referring to details of the called signature. For calls to functions this - /// will be the same as `callable_type`; for other callable instances it may be a `__call__` method. - pub(crate) signature_type: Type<'db>, - /// By using `SmallVec`, we avoid an extra heap allocation for the common case of a non-union - /// type. - elements: SmallVec<[CallableSignature<'db>; 1]>, -} - -impl<'db> Signatures<'db> { - pub(crate) fn not_callable(signature_type: Type<'db>) -> Self { - Self { - callable_type: signature_type, - signature_type, - elements: smallvec![CallableSignature::not_callable(signature_type)], - } - } - - pub(crate) fn single(signature: CallableSignature<'db>) -> Self { - Self { - callable_type: signature.callable_type, - signature_type: signature.signature_type, - elements: smallvec![signature], - } - } - - /// Creates a new `Signatures` from an iterator of [`Signature`]s. Panics if the iterator is - /// empty. - pub(crate) fn from_union(signature_type: Type<'db>, elements: I) -> Self - where - I: IntoIterator>, - { - let elements: SmallVec<_> = elements - .into_iter() - .flat_map(|s| s.elements.into_iter()) - .collect(); - assert!(!elements.is_empty()); - Self { - callable_type: signature_type, - signature_type, - elements, - } - } - - 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; - } - for signature in &mut self.elements { - signature.replace_callable_type(before, after); - } - } - - pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) { - for signature in &mut self.elements { - signature.dunder_call_is_possibly_unbound = true; - } - } -} - -impl<'a, 'db> IntoIterator for &'a Signatures<'db> { - type Item = &'a CallableSignature<'db>; - type IntoIter = std::slice::Iter<'a, CallableSignature<'db>>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -/// The signature of a single callable. If the callable is overloaded, there is a separate -/// [`Signature`] for each overload. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) struct CallableSignature<'db> { - /// The type that is (hopefully) callable. - pub(crate) callable_type: Type<'db>, - - /// The type we'll use for error messages referring to details of the called signature. For - /// calls to functions this will be the same as `callable_type`; for other callable instances - /// it may be a `__call__` method. - pub(crate) signature_type: Type<'db>, - - /// If this is a callable object (i.e. called via a `__call__` method), the boundness of - /// that call method. - pub(crate) dunder_call_is_possibly_unbound: bool, - - /// The type of the bound `self` or `cls` parameter if this signature is for a bound method. - pub(crate) bound_type: Option>, - - /// The signatures of each overload of this callable. Will be empty if the type is not - /// callable. - /// - /// By using `SmallVec`, we avoid an extra heap allocation for the common case of a - /// non-overloaded callable. - overloads: SmallVec<[Signature<'db>; 1]>, -} - -impl<'db> CallableSignature<'db> { - pub(crate) fn not_callable(signature_type: Type<'db>) -> Self { - Self { - callable_type: signature_type, - signature_type, - dunder_call_is_possibly_unbound: false, - bound_type: None, - overloads: smallvec![], - } - } - - pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Self { - Self { - callable_type: signature_type, - signature_type, - dunder_call_is_possibly_unbound: false, - bound_type: None, - overloads: smallvec![signature], - } - } - - /// Creates a new `CallableSignature` from an iterator of [`Signature`]s. Returns a - /// non-callable signature if the iterator is empty. - pub(crate) fn from_overloads(signature_type: Type<'db>, overloads: I) -> Self - where - I: IntoIterator>, - { - Self { - callable_type: signature_type, - signature_type, - dunder_call_is_possibly_unbound: false, - bound_type: None, - overloads: overloads.into_iter().collect(), - } - } - - /// Return a signature for a dynamic callable - pub(crate) fn dynamic(signature_type: Type<'db>) -> Self { - let signature = Signature { - generic_context: None, - parameters: Parameters::gradual_form(), - return_ty: Some(signature_type), - }; - Self::single(signature_type, signature) - } - - /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo - #[allow(unused_variables)] // 'reason' only unused in debug builds - pub(crate) fn todo(reason: &'static str) -> Self { - let signature_type = todo_type!(reason); - let signature = Signature { - generic_context: None, - parameters: Parameters::todo(), - return_ty: Some(signature_type), - }; - Self::single(signature_type, signature) - } - - pub(crate) fn with_bound_type(mut self, bound_type: Type<'db>) -> Self { - self.bound_type = Some(bound_type); - 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; - } - } -} - -impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { - type Item = &'a Signature<'db>; - type IntoIter = std::slice::Iter<'a, Signature<'db>>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -/// The signature of one of the overloads of a callable. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub struct Signature<'db> { - /// The generic context for this overload, if it is generic. - pub(crate) generic_context: Option>, - - /// Parameters, in source order. - /// - /// The ordering of parameters in a valid signature must be: first positional-only parameters, - /// then positional-or-keyword, then optionally the variadic parameter, then keyword-only - /// parameters, and last, optionally the variadic keywords parameter. Parameters with defaults - /// must come after parameters without defaults. - /// - /// We may get invalid signatures, though, and need to handle them without panicking. - parameters: Parameters<'db>, - - /// Annotated return type, if any. - pub(crate) return_ty: Option>, -} - -impl<'db> Signature<'db> { - pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option>) -> Self { - Self { - generic_context: None, - parameters, - return_ty, - } - } - - pub(crate) fn new_generic( - generic_context: Option>, - parameters: Parameters<'db>, - return_ty: Option>, - ) -> Self { - Self { - generic_context, - parameters, - return_ty, - } - } - - /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo - #[allow(unused_variables)] // 'reason' only unused in debug builds - pub(crate) fn todo(reason: &'static str) -> Self { - Signature { - generic_context: None, - parameters: Parameters::todo(), - return_ty: Some(todo_type!(reason)), - } - } - - /// Return a typed signature from a function definition. - pub(super) fn from_function( - db: &'db dyn Db, - generic_context: Option>, - definition: Definition<'db>, - function_node: &ast::StmtFunctionDef, - ) -> Self { - let return_ty = function_node.returns.as_ref().map(|returns| { - if function_node.is_async { - todo_type!("generic types.CoroutineType") - } else { - definition_expression_type(db, definition, returns.as_ref()) - } - }); - - Self { - generic_context, - parameters: Parameters::from_parameters( - db, - definition, - function_node.parameters.as_ref(), - ), - return_ty, - } - } - - pub(crate) fn apply_specialization( - &mut self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) { - self.parameters.apply_specialization(db, specialization); - self.return_ty = self - .return_ty - .map(|ty| ty.apply_specialization(db, specialization)); - } - - /// Return the parameters in this signature. - pub(crate) fn parameters(&self) -> &Parameters<'db> { - &self.parameters - } - - pub(crate) fn bind_self(&self) -> Self { - Self { - generic_context: self.generic_context, - parameters: Parameters::new(self.parameters().iter().skip(1).cloned()), - return_ty: self.return_ty, - } - } - - /// Returns `true` if this is a fully static signature. - /// - /// A signature is fully static if all of its parameters and return type are fully static and - /// if it does not use gradual form (`...`) for its parameters. - pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { - if self.parameters.is_gradual() { - return false; - } - - if self.parameters.iter().any(|parameter| { - parameter - .annotated_type() - .is_none_or(|annotated_type| !annotated_type.is_fully_static(db)) - }) { - return false; - } - - self.return_ty - .is_some_and(|return_type| return_type.is_fully_static(db)) - } - - /// Return `true` if `self` has exactly the same set of possible static materializations as - /// `other` (if `self` represents the same set of possible sets of possible runtime objects as - /// `other`). - pub(crate) fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_equivalent_to_impl(other, |self_type, other_type| { - self_type - .unwrap_or(Type::unknown()) - .is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown())) - }) - } - - /// Return `true` if `self` represents the exact same set of possible runtime objects as `other`. - pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_equivalent_to_impl(other, |self_type, other_type| { - match (self_type, other_type) { - (Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type), - // We need the catch-all case here because it's not guaranteed that this is a fully - // static type. - _ => false, - } - }) - } - - /// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for signature. - /// - /// [`is_equivalent_to`]: Self::is_equivalent_to - /// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to - fn is_equivalent_to_impl(&self, other: &Signature<'db>, check_types: F) -> bool - where - F: Fn(Option>, Option>) -> bool, - { - // N.B. We don't need to explicitly check for the use of gradual form (`...`) in the - // parameters because it is internally represented by adding `*Any` and `**Any` to the - // parameter list. - - if self.parameters.len() != other.parameters.len() { - return false; - } - - if !check_types(self.return_ty, other.return_ty) { - return false; - } - - for (self_parameter, other_parameter) in self.parameters.iter().zip(&other.parameters) { - match (self_parameter.kind(), other_parameter.kind()) { - ( - ParameterKind::PositionalOnly { - default_type: self_default, - .. - }, - ParameterKind::PositionalOnly { - default_type: other_default, - .. - }, - ) if self_default.is_some() == other_default.is_some() => {} - - ( - ParameterKind::PositionalOrKeyword { - name: self_name, - default_type: self_default, - }, - ParameterKind::PositionalOrKeyword { - name: other_name, - default_type: other_default, - }, - ) if self_default.is_some() == other_default.is_some() - && self_name == other_name => {} - - (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {} - - ( - ParameterKind::KeywordOnly { - name: self_name, - default_type: self_default, - }, - ParameterKind::KeywordOnly { - name: other_name, - default_type: other_default, - }, - ) if self_default.is_some() == other_default.is_some() - && self_name == other_name => {} - - (ParameterKind::KeywordVariadic { .. }, ParameterKind::KeywordVariadic { .. }) => {} - - _ => return false, - } - - if !check_types( - self_parameter.annotated_type(), - other_parameter.annotated_type(), - ) { - return false; - } - } - - true - } - - /// Return `true` if a callable with signature `self` is assignable to a callable with - /// signature `other`. - pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_assignable_to_impl(other, |type1, type2| { - // In the context of a callable type, the `None` variant represents an `Unknown` type. - type1 - .unwrap_or(Type::unknown()) - .is_assignable_to(db, type2.unwrap_or(Type::unknown())) - }) - } - - /// Return `true` if a callable with signature `self` is a subtype of a callable with signature - /// `other`. - /// - /// # Panics - /// - /// Panics if `self` or `other` is not a fully static signature. - pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_assignable_to_impl(other, |type1, type2| { - // SAFETY: Subtype relation is only checked for fully static types. - type1.unwrap().is_subtype_of(db, type2.unwrap()) - }) - } - - /// Implementation for the [`is_assignable_to`] and [`is_subtype_of`] for signature. - /// - /// [`is_assignable_to`]: Self::is_assignable_to - /// [`is_subtype_of`]: Self::is_subtype_of - fn is_assignable_to_impl(&self, other: &Signature<'db>, check_types: F) -> bool - where - F: Fn(Option>, Option>) -> bool, - { - /// A helper struct to zip two slices of parameters together that provides control over the - /// two iterators individually. It also keeps track of the current parameter in each - /// iterator. - struct ParametersZip<'a, 'db> { - current_self: Option<&'a Parameter<'db>>, - current_other: Option<&'a Parameter<'db>>, - iter_self: Iter<'a, Parameter<'db>>, - iter_other: Iter<'a, Parameter<'db>>, - } - - impl<'a, 'db> ParametersZip<'a, 'db> { - /// Move to the next parameter in both the `self` and `other` parameter iterators, - /// [`None`] if both iterators are exhausted. - fn next(&mut self) -> Option, &'a Parameter<'db>>> { - match (self.next_self(), self.next_other()) { - (Some(self_param), Some(other_param)) => { - Some(EitherOrBoth::Both(self_param, other_param)) - } - (Some(self_param), None) => Some(EitherOrBoth::Left(self_param)), - (None, Some(other_param)) => Some(EitherOrBoth::Right(other_param)), - (None, None) => None, - } - } - - /// Move to the next parameter in the `self` parameter iterator, [`None`] if the - /// iterator is exhausted. - fn next_self(&mut self) -> Option<&'a Parameter<'db>> { - self.current_self = self.iter_self.next(); - self.current_self - } - - /// Move to the next parameter in the `other` parameter iterator, [`None`] if the - /// iterator is exhausted. - fn next_other(&mut self) -> Option<&'a Parameter<'db>> { - self.current_other = self.iter_other.next(); - self.current_other - } - - /// Peek at the next parameter in the `other` parameter iterator without consuming it. - fn peek_other(&mut self) -> Option<&'a Parameter<'db>> { - self.iter_other.clone().next() - } - - /// Consumes the `ParametersZip` and returns a two-element tuple containing the - /// remaining parameters in the `self` and `other` iterators respectively. - /// - /// The returned iterators starts with the current parameter, if any, followed by the - /// remaining parameters in the respective iterators. - fn into_remaining( - self, - ) -> ( - impl Iterator>, - impl Iterator>, - ) { - ( - self.current_self.into_iter().chain(self.iter_self), - self.current_other.into_iter().chain(self.iter_other), - ) - } - } - - // Return types are covariant. - if !check_types(self.return_ty, other.return_ty) { - return false; - } - - if self.parameters.is_gradual() || other.parameters.is_gradual() { - // If either of the parameter lists contains a gradual form (`...`), then it is - // assignable / subtype to and from any other callable type. - return true; - } - - let mut parameters = ParametersZip { - current_self: None, - current_other: None, - iter_self: self.parameters.iter(), - iter_other: other.parameters.iter(), - }; - - // Collect all the standard parameters that have only been matched against a variadic - // parameter which means that the keyword variant is still unmatched. - let mut other_keywords = Vec::new(); - - loop { - let Some(next_parameter) = parameters.next() else { - // All parameters have been checked or both the parameter lists were empty. In - // either case, `self` is a subtype of `other`. - return true; - }; - - match next_parameter { - EitherOrBoth::Left(self_parameter) => match self_parameter.kind() { - ParameterKind::KeywordOnly { .. } | ParameterKind::KeywordVariadic { .. } - if !other_keywords.is_empty() => - { - // If there are any unmatched keyword parameters in `other`, they need to - // be checked against the keyword-only / keyword-variadic parameters that - // will be done after this loop. - break; - } - ParameterKind::PositionalOnly { default_type, .. } - | ParameterKind::PositionalOrKeyword { default_type, .. } - | ParameterKind::KeywordOnly { default_type, .. } => { - // For `self <: other` to be valid, if there are no more parameters in - // `other`, then the non-variadic parameters in `self` must have a default - // value. - if default_type.is_none() { - return false; - } - } - ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => { - // Variadic parameters don't have any restrictions in this context, so - // we'll just continue to the next parameter set. - } - }, - - EitherOrBoth::Right(_) => { - // If there are more parameters in `other` than in `self`, then `self` is not a - // subtype of `other`. - return false; - } - - EitherOrBoth::Both(self_parameter, other_parameter) => { - match (self_parameter.kind(), other_parameter.kind()) { - ( - ParameterKind::PositionalOnly { - default_type: self_default, - .. - } - | ParameterKind::PositionalOrKeyword { - default_type: self_default, - .. - }, - ParameterKind::PositionalOnly { - default_type: other_default, - .. - }, - ) => { - if self_default.is_none() && other_default.is_some() { - return false; - } - if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), - ) { - return false; - } - } - - ( - ParameterKind::PositionalOrKeyword { - name: self_name, - default_type: self_default, - }, - ParameterKind::PositionalOrKeyword { - name: other_name, - default_type: other_default, - }, - ) => { - if self_name != other_name { - return false; - } - // The following checks are the same as positional-only parameters. - if self_default.is_none() && other_default.is_some() { - return false; - } - if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), - ) { - return false; - } - } - - ( - ParameterKind::Variadic { .. }, - ParameterKind::PositionalOnly { .. } - | ParameterKind::PositionalOrKeyword { .. }, - ) => { - if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), - ) { - return false; - } - - if matches!( - other_parameter.kind(), - ParameterKind::PositionalOrKeyword { .. } - ) { - other_keywords.push(other_parameter); - } - - // We've reached a variadic parameter in `self` which means there can - // be no more positional parameters after this in a valid AST. But, the - // current parameter in `other` is a positional-only which means there - // can be more positional parameters after this which could be either - // more positional-only parameters, standard parameters or a variadic - // parameter. - // - // So, any remaining positional parameters in `other` would need to be - // checked against the variadic parameter in `self`. This loop does - // that by only moving the `other` iterator forward. - loop { - let Some(other_parameter) = parameters.peek_other() else { - break; - }; - match other_parameter.kind() { - ParameterKind::PositionalOrKeyword { .. } => { - other_keywords.push(other_parameter); - } - ParameterKind::PositionalOnly { .. } - | ParameterKind::Variadic { .. } => {} - _ => { - // Any other parameter kind cannot be checked against a - // variadic parameter and is deferred to the next iteration. - break; - } - } - if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), - ) { - return false; - } - parameters.next_other(); - } - } - - (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => { - if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), - ) { - return false; - } - } - - ( - _, - ParameterKind::KeywordOnly { .. } - | ParameterKind::KeywordVariadic { .. }, - ) => { - // Keyword parameters are not considered in this loop as the order of - // parameters is not important for them and so they are checked by - // doing name-based lookups. - break; - } - - _ => return false, - } - } - } - } - - // At this point, the remaining parameters in `other` are keyword-only or keyword variadic. - // But, `self` could contain any unmatched positional parameters. - let (self_parameters, other_parameters) = parameters.into_remaining(); - - // Collect all the keyword-only parameters and the unmatched standard parameters. - let mut self_keywords = HashMap::new(); - - // Type of the variadic keyword parameter in `self`. - // - // This is a nested option where the outer option represents the presence of a keyword - // variadic parameter in `self` and the inner option represents the annotated type of the - // keyword variadic parameter. - let mut self_keyword_variadic: Option>> = None; - - for self_parameter in self_parameters { - match self_parameter.kind() { - ParameterKind::KeywordOnly { name, .. } - | ParameterKind::PositionalOrKeyword { name, .. } => { - self_keywords.insert(name.clone(), self_parameter); - } - ParameterKind::KeywordVariadic { .. } => { - self_keyword_variadic = Some(self_parameter.annotated_type()); - } - ParameterKind::PositionalOnly { .. } => { - // These are the unmatched positional-only parameters in `self` from the - // previous loop. They cannot be matched against any parameter in `other` which - // only contains keyword-only and keyword-variadic parameters so the subtype - // relation is invalid. - return false; - } - ParameterKind::Variadic { .. } => {} - } - } - - for other_parameter in other_keywords.into_iter().chain(other_parameters) { - match other_parameter.kind() { - ParameterKind::KeywordOnly { - name: other_name, - default_type: other_default, - } - | ParameterKind::PositionalOrKeyword { - name: other_name, - default_type: other_default, - } => { - if let Some(self_parameter) = self_keywords.remove(other_name) { - match self_parameter.kind() { - ParameterKind::PositionalOrKeyword { - default_type: self_default, - .. - } - | ParameterKind::KeywordOnly { - default_type: self_default, - .. - } => { - if self_default.is_none() && other_default.is_some() { - return false; - } - if !check_types( - other_parameter.annotated_type(), - self_parameter.annotated_type(), - ) { - return false; - } - } - _ => unreachable!( - "`self_keywords` should only contain keyword-only or standard parameters" - ), - } - } else if let Some(self_keyword_variadic_type) = self_keyword_variadic { - if !check_types( - other_parameter.annotated_type(), - self_keyword_variadic_type, - ) { - return false; - } - } else { - return false; - } - } - ParameterKind::KeywordVariadic { .. } => { - let Some(self_keyword_variadic_type) = self_keyword_variadic else { - // For a `self <: other` relationship, if `other` has a keyword variadic - // parameter, `self` must also have a keyword variadic parameter. - return false; - }; - if !check_types(other_parameter.annotated_type(), self_keyword_variadic_type) { - return false; - } - } - _ => { - // This can only occur in case of a syntax error. - return false; - } - } - } - - // If there are still unmatched keyword parameters from `self`, then they should be - // optional otherwise the subtype relation is invalid. - for (_, self_parameter) in self_keywords { - if self_parameter.default_type().is_none() { - return false; - } - } - - true - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) struct Parameters<'db> { - // TODO: use SmallVec here once invariance bug is fixed - value: Vec>, - - /// Whether this parameter list represents a gradual form using `...` as the only parameter. - /// - /// If this is `true`, the `value` will still contain the variadic and keyword-variadic - /// parameters. This flag is used to distinguish between an explicit `...` in the callable type - /// as in `Callable[..., int]` and the variadic arguments in `lambda` expression as in - /// `lambda *args, **kwargs: None`. - /// - /// The display implementation utilizes this flag to use `...` instead of displaying the - /// individual variadic and keyword-variadic parameters. - /// - /// Note: This flag is also used to indicate invalid forms of `Callable` annotations. - is_gradual: bool, -} - -impl<'db> Parameters<'db> { - pub(crate) fn new(parameters: impl IntoIterator>) -> Self { - Self { - value: parameters.into_iter().collect(), - is_gradual: false, - } - } - - /// Create an empty parameter list. - pub(crate) fn empty() -> Self { - Self { - value: Vec::new(), - is_gradual: false, - } - } - - pub(crate) fn as_slice(&self) -> &[Parameter<'db>] { - self.value.as_slice() - } - - pub(crate) const fn is_gradual(&self) -> bool { - self.is_gradual - } - - /// Return todo parameters: (*args: Todo, **kwargs: Todo) - pub(crate) fn todo() -> Self { - Self { - value: vec![ - 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, - } - } - - /// Return parameters that represents a gradual form using `...` as the only parameter. - /// - /// Internally, this is represented as `(*Any, **Any)` that accepts parameters of type [`Any`]. - /// - /// [`Any`]: crate::types::DynamicType::Any - pub(crate) fn gradual_form() -> Self { - Self { - value: vec![ - 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, - } - } - - /// Return parameters that represents an unknown list of parameters. - /// - /// Internally, this is represented as `(*Unknown, **Unknown)` that accepts parameters of type - /// [`Unknown`]. - /// - /// [`Unknown`]: crate::types::DynamicType::Unknown - pub(crate) fn unknown() -> Self { - Self { - value: vec![ - 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, - } - } - - fn from_parameters( - db: &'db dyn Db, - definition: Definition<'db>, - parameters: &ast::Parameters, - ) -> Self { - let ast::Parameters { - posonlyargs, - args, - vararg, - kwonlyargs, - kwarg, - range: _, - } = parameters; - let default_type = |param: &ast::ParameterWithDefault| { - param - .default() - .map(|default| definition_expression_type(db, definition, default)) - }; - let positional_only = posonlyargs.iter().map(|arg| { - Parameter::from_node_and_kind( - db, - definition, - &arg.parameter, - ParameterKind::PositionalOnly { - name: Some(arg.parameter.name.id.clone()), - default_type: default_type(arg), - }, - ) - }); - let positional_or_keyword = args.iter().map(|arg| { - Parameter::from_node_and_kind( - db, - definition, - &arg.parameter, - ParameterKind::PositionalOrKeyword { - name: arg.parameter.name.id.clone(), - default_type: default_type(arg), - }, - ) - }); - let variadic = vararg.as_ref().map(|arg| { - Parameter::from_node_and_kind( - db, - definition, - arg, - ParameterKind::Variadic { - name: arg.name.id.clone(), - }, - ) - }); - let keyword_only = kwonlyargs.iter().map(|arg| { - Parameter::from_node_and_kind( - db, - definition, - &arg.parameter, - ParameterKind::KeywordOnly { - name: arg.parameter.name.id.clone(), - default_type: default_type(arg), - }, - ) - }); - let keywords = kwarg.as_ref().map(|arg| { - Parameter::from_node_and_kind( - db, - definition, - arg, - ParameterKind::KeywordVariadic { - name: arg.name.id.clone(), - }, - ) - }); - Self::new( - positional_only - .chain(positional_or_keyword) - .chain(variadic) - .chain(keyword_only) - .chain(keywords), - ) - } - - fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { - self.value - .iter_mut() - .for_each(|param| param.apply_specialization(db, specialization)); - } - - pub(crate) fn len(&self) -> usize { - self.value.len() - } - - pub(crate) fn iter(&self) -> std::slice::Iter> { - self.value.iter() - } - - /// Iterate initial positional parameters, not including variadic parameter, if any. - /// - /// For a valid signature, this will be all positional parameters. In an invalid signature, - /// there could be non-initial positional parameters; effectively, we just won't consider those - /// to be positional, which is fine. - pub(crate) fn positional(&self) -> impl Iterator> { - self.iter().take_while(|param| param.is_positional()) - } - - /// Return parameter at given index, or `None` if index is out-of-range. - pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> { - self.value.get(index) - } - - /// Return positional parameter at given index, or `None` if `index` is out of range. - /// - /// Does not return variadic parameter. - pub(crate) fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> { - self.get(index) - .and_then(|parameter| parameter.is_positional().then_some(parameter)) - } - - /// Return the variadic parameter (`*args`), if any, and its index, or `None`. - pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> { - self.iter() - .enumerate() - .find(|(_, parameter)| parameter.is_variadic()) - } - - /// Return parameter (with index) for given name, or `None` if no such parameter. - /// - /// Does not return keywords (`**kwargs`) parameter. - /// - /// In an invalid signature, there could be multiple parameters with the same name; we will - /// just return the first that matches. - pub(crate) fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { - self.iter() - .enumerate() - .find(|(_, parameter)| parameter.callable_by_name(name)) - } - - /// Return the keywords parameter (`**kwargs`), if any, and its index, or `None`. - pub(crate) fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> { - self.iter() - .enumerate() - .rfind(|(_, parameter)| parameter.is_keyword_variadic()) - } -} - -impl<'db, 'a> IntoIterator for &'a Parameters<'db> { - type Item = &'a Parameter<'db>; - type IntoIter = std::slice::Iter<'a, Parameter<'db>>; - - fn into_iter(self) -> Self::IntoIter { - self.value.iter() - } -} - -impl<'db> FromIterator> for Parameters<'db> { - fn from_iter>>(iter: T) -> Self { - Self::new(iter) - } -} - -impl<'db> std::ops::Index for Parameters<'db> { - type Output = Parameter<'db>; - - fn index(&self, index: usize) -> &Self::Output { - &self.value[index] - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) struct Parameter<'db> { - /// Annotated type of the parameter. - annotated_type: Option>, - - kind: ParameterKind<'db>, - pub(crate) form: ParameterForm, -} - -impl<'db> Parameter<'db> { - pub(crate) fn positional_only(name: Option) -> 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 apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { - self.annotated_type = self - .annotated_type - .map(|ty| ty.apply_specialization(db, specialization)); - self.kind.apply_specialization(db, specialization); - } - - /// Strip information from the parameter so that two equivalent parameters compare equal. - /// Normalize nested unions and intersections in the annotated type, if any. - /// - /// See [`Type::normalized`] for more details. - pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self { - let Parameter { - annotated_type, - kind, - form, - } = self; - - // Ensure unions and intersections are ordered in the annotated type (if there is one) - let annotated_type = annotated_type.map(|ty| ty.normalized(db)); - - // Ensure that parameter names are stripped from positional-only, variadic and keyword-variadic parameters. - // Ensure that we only record whether a parameter *has* a default - // (strip the precise *type* of the default from the parameter, replacing it with `Never`). - let kind = match kind { - ParameterKind::PositionalOnly { - name: _, - default_type, - } => ParameterKind::PositionalOnly { - name: None, - default_type: default_type.map(|_| Type::Never), - }, - ParameterKind::PositionalOrKeyword { name, default_type } => { - ParameterKind::PositionalOrKeyword { - name: name.clone(), - default_type: default_type.map(|_| Type::Never), - } - } - ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly { - name: name.clone(), - default_type: default_type.map(|_| Type::Never), - }, - ParameterKind::Variadic { name: _ } => ParameterKind::Variadic { - name: Name::new_static("args"), - }, - ParameterKind::KeywordVariadic { name: _ } => ParameterKind::KeywordVariadic { - name: Name::new_static("kwargs"), - }, - }; - - Self { - annotated_type, - kind, - form: *form, - } - } - - fn from_node_and_kind( - db: &'db dyn Db, - definition: Definition<'db>, - parameter: &ast::Parameter, - kind: ParameterKind<'db>, - ) -> Self { - Self { - annotated_type: parameter - .annotation() - .map(|annotation| definition_expression_type(db, definition, annotation)), - kind, - form: ParameterForm::Value, - } - } - - /// Returns `true` if this is a keyword-only parameter. - pub(crate) fn is_keyword_only(&self) -> bool { - matches!(self.kind, ParameterKind::KeywordOnly { .. }) - } - - /// Returns `true` if this is a positional-only parameter. - pub(crate) fn is_positional_only(&self) -> bool { - matches!(self.kind, ParameterKind::PositionalOnly { .. }) - } - - /// Returns `true` if this is a variadic parameter. - pub(crate) fn is_variadic(&self) -> bool { - matches!(self.kind, ParameterKind::Variadic { .. }) - } - - /// Returns `true` if this is a keyword-variadic parameter. - pub(crate) fn is_keyword_variadic(&self) -> bool { - matches!(self.kind, ParameterKind::KeywordVariadic { .. }) - } - - /// Returns `true` if this is either a positional-only or standard (positional or keyword) - /// parameter. - pub(crate) fn is_positional(&self) -> bool { - matches!( - self.kind, - ParameterKind::PositionalOnly { .. } | ParameterKind::PositionalOrKeyword { .. } - ) - } - - pub(crate) fn callable_by_name(&self, name: &str) -> bool { - match &self.kind { - ParameterKind::PositionalOrKeyword { - name: param_name, .. - } - | ParameterKind::KeywordOnly { - name: param_name, .. - } => param_name == name, - _ => false, - } - } - - /// Annotated type of the parameter, if annotated. - pub(crate) fn annotated_type(&self) -> Option> { - self.annotated_type - } - - /// Kind of the parameter. - pub(crate) fn kind(&self) -> &ParameterKind<'db> { - &self.kind - } - - /// Name of the parameter (if it has one). - pub(crate) fn name(&self) -> Option<&ast::name::Name> { - match &self.kind { - ParameterKind::PositionalOnly { name, .. } => name.as_ref(), - ParameterKind::PositionalOrKeyword { name, .. } => Some(name), - ParameterKind::Variadic { name } => Some(name), - ParameterKind::KeywordOnly { name, .. } => Some(name), - ParameterKind::KeywordVariadic { name } => Some(name), - } - } - - /// Display name of the parameter, if it has one. - pub(crate) fn display_name(&self) -> Option { - self.name().map(|name| match self.kind { - ParameterKind::Variadic { .. } => ast::name::Name::new(format!("*{name}")), - ParameterKind::KeywordVariadic { .. } => ast::name::Name::new(format!("**{name}")), - _ => name.clone(), - }) - } - - /// Default-value type of the parameter, if any. - pub(crate) fn default_type(&self) -> Option> { - match self.kind { - ParameterKind::PositionalOnly { default_type, .. } - | ParameterKind::PositionalOrKeyword { default_type, .. } - | ParameterKind::KeywordOnly { default_type, .. } => default_type, - ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => None, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -pub(crate) enum ParameterKind<'db> { - /// Positional-only parameter, e.g. `def f(x, /): ...` - PositionalOnly { - /// Parameter name. - /// - /// It is possible for signatures to be defined in ways that leave positional-only parameters - /// nameless (e.g. via `Callable` annotations). - name: Option, - default_type: Option>, - }, - - /// Positional-or-keyword parameter, e.g. `def f(x): ...` - PositionalOrKeyword { - /// Parameter name. - name: Name, - default_type: Option>, - }, - - /// Variadic parameter, e.g. `def f(*args): ...` - Variadic { - /// Parameter name. - name: Name, - }, - - /// Keyword-only parameter, e.g. `def f(*, x): ...` - KeywordOnly { - /// Parameter name. - name: Name, - default_type: Option>, - }, - - /// Variadic keywords parameter, e.g. `def f(**kwargs): ...` - KeywordVariadic { - /// Parameter name. - name: Name, - }, -} - -impl<'db> ParameterKind<'db> { - fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { - match self { - Self::PositionalOnly { default_type, .. } - | Self::PositionalOrKeyword { default_type, .. } - | Self::KeywordOnly { default_type, .. } => { - *default_type = default_type.map(|ty| ty.apply_specialization(db, specialization)); - } - Self::Variadic { .. } | Self::KeywordVariadic { .. } => {} - } - } -} - -/// 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::*; - use crate::db::tests::{setup_db, TestDb}; - use crate::symbol::global_symbol; - use crate::types::{FunctionType, KnownClass}; - use ruff_db::system::DbWithWritableSystem as _; - - #[track_caller] - fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { - let module = ruff_db::files::system_path_to_file(db, file).unwrap(); - global_symbol(db, module, "f") - .symbol - .expect_type() - .expect_function_literal() - } - - #[track_caller] - fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) { - assert_eq!(signature.parameters.value.as_slice(), expected); - } - - #[test] - fn empty() { - let mut db = setup_db(); - db.write_dedented("/src/a.py", "def f(): ...").unwrap(); - let func = get_function_f(&db, "/src/a.py"); - - let sig = func.internal_signature(&db); - - assert!(sig.return_ty.is_none()); - assert_params(&sig, &[]); - } - - #[test] - #[allow(clippy::many_single_char_names)] - fn full() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - from typing import Literal - - def f(a, b: int, c = 1, d: int = 2, /, - e = 3, f: Literal[4] = 4, *args: object, - g = 5, h: Literal[6] = 6, **kwargs: str) -> bytes: ... - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.py"); - - let sig = func.internal_signature(&db); - - assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes"); - assert_params( - &sig, - &[ - 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)), - ], - ); - } - - #[test] - fn not_deferred() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - class A: ... - class B: ... - - alias = A - - def f(a: alias): ... - - alias = B - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.py"); - - let sig = func.internal_signature(&db); - - let [Parameter { - 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_type.unwrap().display(&db).to_string(), "A"); - } - - #[test] - fn deferred_in_stub() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.pyi", - " - class A: ... - class B: ... - - alias = A - - def f(a: alias): ... - - alias = B - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.pyi"); - - let sig = func.internal_signature(&db); - - let [Parameter { - 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_type.unwrap().display(&db).to_string(), "B"); - } - - #[test] - fn generic_not_deferred() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - class A: ... - class B: ... - - alias = A - - def f[T](a: alias, b: T) -> T: ... - - alias = B - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.py"); - - let sig = func.internal_signature(&db); - - let [Parameter { - annotated_type: a_annotated_ty, - kind: ParameterKind::PositionalOrKeyword { name: a_name, .. }, - .. - }, Parameter { - annotated_type: b_annotated_ty, - kind: ParameterKind::PositionalOrKeyword { name: b_name, .. }, - .. - }] = &sig.parameters.value[..] - else { - panic!("expected two positional-or-keyword parameters"); - }; - assert_eq!(a_name, "a"); - assert_eq!(b_name, "b"); - // TODO resolution should not be deferred; we should see A not B - assert_eq!( - a_annotated_ty.unwrap().display(&db).to_string(), - "Unknown | B" - ); - assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T"); - } - - #[test] - fn generic_deferred_in_stub() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.pyi", - " - class A: ... - class B: ... - - alias = A - - def f[T](a: alias, b: T) -> T: ... - - alias = B - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.pyi"); - - let sig = func.internal_signature(&db); - - let [Parameter { - annotated_type: a_annotated_ty, - kind: ParameterKind::PositionalOrKeyword { name: a_name, .. }, - .. - }, Parameter { - annotated_type: b_annotated_ty, - kind: ParameterKind::PositionalOrKeyword { name: b_name, .. }, - .. - }] = &sig.parameters.value[..] - else { - panic!("expected two positional-or-keyword parameters"); - }; - assert_eq!(a_name, "a"); - assert_eq!(b_name, "b"); - // Parameter resolution deferred; we should see B - assert_eq!( - a_annotated_ty.unwrap().display(&db).to_string(), - "Unknown | B" - ); - assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T"); - } - - #[test] - fn external_signature_no_decorator() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - def f(a: int) -> int: ... - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.py"); - - let expected_sig = func.internal_signature(&db); - - // With no decorators, internal and external signature are the same - assert_eq!(func.signature(&db), &expected_sig); - } -} diff --git a/crates/red_knot_python_semantic/src/types/slots.rs b/crates/red_knot_python_semantic/src/types/slots.rs deleted file mode 100644 index b86f2e2fa3b12..0000000000000 --- a/crates/red_knot_python_semantic/src/types/slots.rs +++ /dev/null @@ -1,109 +0,0 @@ -use ruff_python_ast as ast; - -use crate::db::Db; -use crate::symbol::{Boundness, Symbol}; -use crate::types::class_base::ClassBase; -use crate::types::diagnostic::report_base_with_incompatible_slots; -use crate::types::{ClassLiteralType, Type}; - -use super::InferContext; - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum SlotsKind { - /// `__slots__` is not found in the class. - NotSpecified, - /// `__slots__` is defined but empty: `__slots__ = ()`. - Empty, - /// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`. - NotEmpty, - /// `__slots__` is defined but its value is dynamic: - /// * `__slots__ = tuple(a for a in b)` - /// * `__slots__ = ["a", "b"]` - Dynamic, -} - -impl SlotsKind { - fn from(db: &dyn Db, base: ClassLiteralType) -> Self { - let Symbol::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").symbol - else { - return Self::NotSpecified; - }; - - if matches!(bound, Boundness::PossiblyUnbound) { - return Self::Dynamic; - } - - match slots_ty { - // __slots__ = ("a", "b") - Type::Tuple(tuple) => { - if tuple.elements(db).is_empty() { - Self::Empty - } else { - Self::NotEmpty - } - } - - // __slots__ = "abc" # Same as `("abc",)` - Type::StringLiteral(_) => Self::NotEmpty, - - _ => Self::Dynamic, - } - } -} - -pub(super) fn check_class_slots( - context: &InferContext, - class: ClassLiteralType, - node: &ast::StmtClassDef, -) { - let db = context.db(); - - let mut first_with_solid_base = None; - let mut common_solid_base = None; - let mut found_second = false; - - for (index, base) in class.explicit_bases(db).iter().enumerate() { - let Type::ClassLiteral(base) = base else { - continue; - }; - - let solid_base = base.iter_mro(db, None).find_map(|current| { - let ClassBase::Class(current) = current else { - return None; - }; - - let (class_literal, _) = current.class_literal(db); - match SlotsKind::from(db, class_literal) { - SlotsKind::NotEmpty => Some(current), - SlotsKind::NotSpecified | SlotsKind::Empty => None, - SlotsKind::Dynamic => None, - } - }); - - if solid_base.is_none() { - continue; - } - - let base_node = &node.bases()[index]; - - if first_with_solid_base.is_none() { - first_with_solid_base = Some(index); - common_solid_base = solid_base; - continue; - } - - if solid_base == common_solid_base { - continue; - } - - found_second = true; - report_base_with_incompatible_slots(context, base_node); - } - - if found_second { - if let Some(index) = first_with_solid_base { - let base_node = &node.bases()[index]; - report_base_with_incompatible_slots(context, base_node); - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/string_annotation.rs b/crates/red_knot_python_semantic/src/types/string_annotation.rs deleted file mode 100644 index 64a0d001ce04d..0000000000000 --- a/crates/red_knot_python_semantic/src/types/string_annotation.rs +++ /dev/null @@ -1,180 +0,0 @@ -use ruff_db::source::source_text; -use ruff_python_ast::{self as ast, ModExpression}; -use ruff_python_parser::Parsed; -use ruff_text_size::Ranged; - -use crate::declare_lint; -use crate::lint::{Level, LintStatus}; - -use super::context::InferContext; - -declare_lint! { - /// ## What it does - /// Checks for f-strings in type annotation positions. - /// - /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use f-string notation. - /// - /// ## Examples - /// ```python - /// def test(): -> f"int": - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// def test(): -> "int": - /// ... - /// ``` - pub(crate) static FSTRING_TYPE_ANNOTATION = { - summary: "detects F-strings in type annotation positions", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for byte-strings in type annotation positions. - /// - /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use byte-string notation. - /// - /// ## Examples - /// ```python - /// def test(): -> b"int": - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// def test(): -> "int": - /// ... - /// ``` - pub(crate) static BYTE_STRING_TYPE_ANNOTATION = { - summary: "detects byte strings in type annotation positions", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for raw-strings in type annotation positions. - /// - /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use raw-string notation. - /// - /// ## Examples - /// ```python - /// def test(): -> r"int": - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// def test(): -> "int": - /// ... - /// ``` - pub(crate) static RAW_STRING_TYPE_ANNOTATION = { - summary: "detects raw strings in type annotation positions", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// ## What it does - /// Checks for implicit concatenated strings in type annotation positions. - /// - /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings. - /// - /// ## Examples - /// ```python - /// def test(): -> "Literal[" "5" "]": - /// ... - /// ``` - /// - /// Use instead: - /// ```python - /// def test(): -> "Literal[5]": - /// ... - /// ``` - pub(crate) static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = { - summary: "detects implicit concatenated strings in type annotations", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = { - summary: "detects invalid syntax in forward annotations", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -declare_lint! { - /// TODO #14889 - pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = { - summary: "detects forward type annotations with escape characters", - status: LintStatus::preview("1.0.0"), - default_level: Level::Error, - } -} - -/// Parses the given expression as a string annotation. -pub(crate) fn parse_string_annotation( - context: &InferContext, - string_expr: &ast::ExprStringLiteral, -) -> Option> { - let file = context.file(); - let db = context.db(); - - let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), ?file) - .entered(); - - let source = source_text(db.upcast(), file); - - if let Some(string_literal) = string_expr.as_single_part_string() { - let prefix = string_literal.flags.prefix(); - if prefix.is_raw() { - context.report_lint_old( - &RAW_STRING_TYPE_ANNOTATION, - string_literal, - format_args!("Type expressions cannot use raw string literal"), - ); - // Compare the raw contents (without quotes) of the expression with the parsed contents - // contained in the string literal. - } else if &source[string_literal.content_range()] == string_literal.as_str() { - match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) { - Ok(parsed) => return Some(parsed), - Err(parse_error) => context.report_lint_old( - &INVALID_SYNTAX_IN_FORWARD_ANNOTATION, - string_literal, - format_args!("Syntax error in forward annotation: {}", parse_error.error), - ), - } - } else { - // The raw contents of the string doesn't match the parsed content. This could be the - // case for annotations that contain escape sequences. - context.report_lint_old( - &ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, - string_expr, - format_args!("Type expressions cannot contain escape characters"), - ); - } - } else { - // String is implicitly concatenated. - context.report_lint_old( - &IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, - string_expr, - format_args!("Type expressions cannot span multiple string literals"), - ); - } - - None -} diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs deleted file mode 100644 index 49c1d168fc63c..0000000000000 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::symbol::SymbolAndQualifiers; - -use super::{ClassBase, Db, KnownClass, MemberLookupPolicy, Type}; - -/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub struct SubclassOfType<'db> { - // Keep this field private, so that the only way of constructing the struct is through the `from` method. - subclass_of: ClassBase<'db>, -} - -impl<'db> SubclassOfType<'db> { - /// Construct a new [`Type`] instance representing a given class object (or a given dynamic type) - /// and all possible subclasses of that class object/dynamic type. - /// - /// This method does not always return a [`Type::SubclassOf`] variant. - /// If the class object is known to be a final class, - /// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type. - /// If the class object is `builtins.object`, `Type::Instance()` will be returned; - /// this is no more precise, but it is exactly equivalent to `type[object]`. - /// - /// The eager normalization here means that we do not need to worry elsewhere about distinguishing - /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. - pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { - let subclass_of = subclass_of.into(); - match subclass_of { - ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), - ClassBase::Class(class) => { - if class.is_final(db) { - Type::from(class) - } else if class.is_object(db) { - KnownClass::Type.to_instance(db) - } else { - Type::SubclassOf(Self { subclass_of }) - } - } - } - } - - /// Return a [`Type`] instance representing the type `type[Unknown]`. - pub(crate) const fn subclass_of_unknown() -> Type<'db> { - Type::SubclassOf(SubclassOfType { - subclass_of: ClassBase::unknown(), - }) - } - - /// Return a [`Type`] instance representing the type `type[Any]`. - pub(crate) const fn subclass_of_any() -> Type<'db> { - Type::SubclassOf(SubclassOfType { - subclass_of: ClassBase::any(), - }) - } - - /// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`. - pub(crate) const fn subclass_of(self) -> ClassBase<'db> { - self.subclass_of - } - - pub(crate) const fn is_dynamic(self) -> bool { - // Unpack `self` so that we're forced to update this method if any more fields are added in the future. - let Self { subclass_of } = self; - subclass_of.is_dynamic() - } - - pub(crate) const fn is_fully_static(self) -> bool { - !self.is_dynamic() - } - - pub(crate) fn find_name_in_mro_with_policy( - self, - db: &'db dyn Db, - name: &str, - policy: MemberLookupPolicy, - ) -> Option> { - Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy) - } - - /// Return `true` if `self` is a subtype of `other`. - /// - /// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant; - /// only fully static types participate in subtyping. - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool { - match (self.subclass_of, other.subclass_of) { - // Non-fully-static types do not participate in subtyping - (ClassBase::Dynamic(_), _) | (_, ClassBase::Dynamic(_)) => false, - - // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, - // and `type[int]` describes all possible runtime subclasses of the class `int`. - // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. - (ClassBase::Class(self_class), ClassBase::Class(other_class)) => { - // N.B. The subclass relation is fully static - self_class.is_subclass_of(db, other_class) - } - } - } - - pub(crate) fn to_instance(self) -> Type<'db> { - match self.subclass_of { - ClassBase::Class(class) => Type::instance(class), - ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), - } - } -} diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs deleted file mode 100644 index e3b341fee6ed7..0000000000000 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ /dev/null @@ -1,370 +0,0 @@ -use std::cmp::Ordering; - -use crate::db::Db; - -use super::{ - class_base::ClassBase, DynamicType, InstanceType, KnownInstanceType, SuperOwnerKind, TodoType, - Type, -}; - -/// Return an [`Ordering`] that describes the canonical order in which two types should appear -/// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them -/// to be compared for equivalence. -/// -/// Two intersections are compared lexicographically. Element types in the intersection must -/// already be sorted. Two unions are never compared in this function because DNF does not permit -/// nested unions. -/// -/// ## Why not just implement [`Ord`] on [`Type`]? -/// -/// It would be fairly easy to slap `#[derive(PartialOrd, Ord)]` on [`Type`], and the ordering we -/// create here is not user-facing. However, it doesn't really "make sense" for `Type` to implement -/// [`Ord`] in terms of the semantics. There are many different ways in which you could plausibly -/// sort a list of types; this is only one (somewhat arbitrary, at times) possible ordering. -pub(super) fn union_or_intersection_elements_ordering<'db>( - db: &'db dyn Db, - left: &Type<'db>, - right: &Type<'db>, -) -> Ordering { - if left == right { - return Ordering::Equal; - } - - match (left, right) { - (Type::Never, _) => Ordering::Less, - (_, Type::Never) => Ordering::Greater, - - (Type::LiteralString, _) => Ordering::Less, - (_, Type::LiteralString) => Ordering::Greater, - - (Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left.cmp(right), - (Type::BooleanLiteral(_), _) => Ordering::Less, - (_, Type::BooleanLiteral(_)) => Ordering::Greater, - - (Type::IntLiteral(left), Type::IntLiteral(right)) => left.cmp(right), - (Type::IntLiteral(_), _) => Ordering::Less, - (_, Type::IntLiteral(_)) => Ordering::Greater, - - (Type::StringLiteral(left), Type::StringLiteral(right)) => left.cmp(right), - (Type::StringLiteral(_), _) => Ordering::Less, - (_, Type::StringLiteral(_)) => Ordering::Greater, - - (Type::BytesLiteral(left), Type::BytesLiteral(right)) => left.cmp(right), - (Type::BytesLiteral(_), _) => Ordering::Less, - (_, Type::BytesLiteral(_)) => Ordering::Greater, - - (Type::SliceLiteral(left), Type::SliceLiteral(right)) => left.cmp(right), - (Type::SliceLiteral(_), _) => Ordering::Less, - (_, Type::SliceLiteral(_)) => Ordering::Greater, - - (Type::FunctionLiteral(left), Type::FunctionLiteral(right)) => left.cmp(right), - (Type::FunctionLiteral(_), _) => Ordering::Less, - (_, Type::FunctionLiteral(_)) => Ordering::Greater, - - (Type::BoundMethod(left), Type::BoundMethod(right)) => left.cmp(right), - (Type::BoundMethod(_), _) => Ordering::Less, - (_, Type::BoundMethod(_)) => Ordering::Greater, - - (Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right), - (Type::MethodWrapper(_), _) => Ordering::Less, - (_, Type::MethodWrapper(_)) => Ordering::Greater, - - (Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right), - (Type::WrapperDescriptor(_), _) => Ordering::Less, - (_, Type::WrapperDescriptor(_)) => Ordering::Greater, - - (Type::DataclassDecorator(left), Type::DataclassDecorator(right)) => { - left.bits().cmp(&right.bits()) - } - (Type::DataclassDecorator(_), _) => Ordering::Less, - (_, Type::DataclassDecorator(_)) => Ordering::Greater, - - (Type::Callable(left), Type::Callable(right)) => { - debug_assert_eq!(*left, left.normalized(db)); - debug_assert_eq!(*right, right.normalized(db)); - left.cmp(right) - } - (Type::Callable(_), _) => Ordering::Less, - (_, Type::Callable(_)) => Ordering::Greater, - - (Type::Tuple(left), Type::Tuple(right)) => { - debug_assert_eq!(*left, left.normalized(db)); - debug_assert_eq!(*right, right.normalized(db)); - left.cmp(right) - } - (Type::Tuple(_), _) => Ordering::Less, - (_, Type::Tuple(_)) => Ordering::Greater, - - (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => left.cmp(right), - (Type::ModuleLiteral(_), _) => Ordering::Less, - (_, Type::ModuleLiteral(_)) => Ordering::Greater, - - (Type::ClassLiteral(left), Type::ClassLiteral(right)) => left.cmp(right), - (Type::ClassLiteral(_), _) => Ordering::Less, - (_, Type::ClassLiteral(_)) => Ordering::Greater, - - (Type::GenericAlias(left), Type::GenericAlias(right)) => left.cmp(right), - (Type::GenericAlias(_), _) => Ordering::Less, - (_, Type::GenericAlias(_)) => Ordering::Greater, - - (Type::SubclassOf(left), Type::SubclassOf(right)) => { - match (left.subclass_of(), right.subclass_of()) { - (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), - (ClassBase::Class(_), _) => Ordering::Less, - (_, ClassBase::Class(_)) => Ordering::Greater, - (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { - dynamic_elements_ordering(left, right) - } - } - } - - (Type::SubclassOf(_), _) => Ordering::Less, - (_, Type::SubclassOf(_)) => Ordering::Greater, - ( - Type::Instance(InstanceType { class: left }), - Type::Instance(InstanceType { class: right }), - ) => left.cmp(right), - - (Type::Instance(_), _) => Ordering::Less, - (_, Type::Instance(_)) => Ordering::Greater, - - (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), - (Type::TypeVar(_), _) => Ordering::Less, - (_, Type::TypeVar(_)) => Ordering::Greater, - - (Type::AlwaysTruthy, _) => Ordering::Less, - (_, Type::AlwaysTruthy) => Ordering::Greater, - - (Type::AlwaysFalsy, _) => Ordering::Less, - (_, Type::AlwaysFalsy) => Ordering::Greater, - - (Type::BoundSuper(left), Type::BoundSuper(right)) => { - (match (left.pivot_class(db), right.pivot_class(db)) { - (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(right), - (ClassBase::Class(_), _) => Ordering::Less, - (_, ClassBase::Class(_)) => Ordering::Greater, - (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { - dynamic_elements_ordering(*left, *right) - } - }) - .then_with(|| match (left.owner(db), right.owner(db)) { - (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => left.cmp(right), - (SuperOwnerKind::Class(_), _) => Ordering::Less, - (_, SuperOwnerKind::Class(_)) => Ordering::Greater, - ( - SuperOwnerKind::Instance(InstanceType { class: left }), - SuperOwnerKind::Instance(InstanceType { class: right }), - ) => left.cmp(right), - (SuperOwnerKind::Instance(_), _) => Ordering::Less, - (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, - (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { - dynamic_elements_ordering(*left, *right) - } - }) - } - (Type::BoundSuper(_), _) => Ordering::Less, - (_, Type::BoundSuper(_)) => Ordering::Greater, - - (Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) => { - match (left_instance, right_instance) { - (KnownInstanceType::Any, _) => Ordering::Less, - (_, KnownInstanceType::Any) => Ordering::Greater, - - (KnownInstanceType::Tuple, _) => Ordering::Less, - (_, KnownInstanceType::Tuple) => Ordering::Greater, - - (KnownInstanceType::AlwaysFalsy, _) => Ordering::Less, - (_, KnownInstanceType::AlwaysFalsy) => Ordering::Greater, - - (KnownInstanceType::AlwaysTruthy, _) => Ordering::Less, - (_, KnownInstanceType::AlwaysTruthy) => Ordering::Greater, - - (KnownInstanceType::Annotated, _) => Ordering::Less, - (_, KnownInstanceType::Annotated) => Ordering::Greater, - - (KnownInstanceType::Callable, _) => Ordering::Less, - (_, KnownInstanceType::Callable) => Ordering::Greater, - - (KnownInstanceType::ChainMap, _) => Ordering::Less, - (_, KnownInstanceType::ChainMap) => Ordering::Greater, - - (KnownInstanceType::ClassVar, _) => Ordering::Less, - (_, KnownInstanceType::ClassVar) => Ordering::Greater, - - (KnownInstanceType::Concatenate, _) => Ordering::Less, - (_, KnownInstanceType::Concatenate) => Ordering::Greater, - - (KnownInstanceType::Counter, _) => Ordering::Less, - (_, KnownInstanceType::Counter) => Ordering::Greater, - - (KnownInstanceType::DefaultDict, _) => Ordering::Less, - (_, KnownInstanceType::DefaultDict) => Ordering::Greater, - - (KnownInstanceType::Deque, _) => Ordering::Less, - (_, KnownInstanceType::Deque) => Ordering::Greater, - - (KnownInstanceType::Dict, _) => Ordering::Less, - (_, KnownInstanceType::Dict) => Ordering::Greater, - - (KnownInstanceType::Final, _) => Ordering::Less, - (_, KnownInstanceType::Final) => Ordering::Greater, - - (KnownInstanceType::FrozenSet, _) => Ordering::Less, - (_, KnownInstanceType::FrozenSet) => Ordering::Greater, - - (KnownInstanceType::TypeGuard, _) => Ordering::Less, - (_, KnownInstanceType::TypeGuard) => Ordering::Greater, - - (KnownInstanceType::List, _) => Ordering::Less, - (_, KnownInstanceType::List) => Ordering::Greater, - - (KnownInstanceType::Literal, _) => Ordering::Less, - (_, KnownInstanceType::Literal) => Ordering::Greater, - - (KnownInstanceType::LiteralString, _) => Ordering::Less, - (_, KnownInstanceType::LiteralString) => Ordering::Greater, - - (KnownInstanceType::Optional, _) => Ordering::Less, - (_, KnownInstanceType::Optional) => Ordering::Greater, - - (KnownInstanceType::OrderedDict, _) => Ordering::Less, - (_, KnownInstanceType::OrderedDict) => Ordering::Greater, - - (KnownInstanceType::Protocol, _) => Ordering::Less, - (_, KnownInstanceType::Protocol) => Ordering::Greater, - - (KnownInstanceType::NoReturn, _) => Ordering::Less, - (_, KnownInstanceType::NoReturn) => Ordering::Greater, - - (KnownInstanceType::Never, _) => Ordering::Less, - (_, KnownInstanceType::Never) => Ordering::Greater, - - (KnownInstanceType::Set, _) => Ordering::Less, - (_, KnownInstanceType::Set) => Ordering::Greater, - - (KnownInstanceType::Type, _) => Ordering::Less, - (_, KnownInstanceType::Type) => Ordering::Greater, - - (KnownInstanceType::TypeAlias, _) => Ordering::Less, - (_, KnownInstanceType::TypeAlias) => Ordering::Greater, - - (KnownInstanceType::Unknown, _) => Ordering::Less, - (_, KnownInstanceType::Unknown) => Ordering::Greater, - - (KnownInstanceType::Not, _) => Ordering::Less, - (_, KnownInstanceType::Not) => Ordering::Greater, - - (KnownInstanceType::Intersection, _) => Ordering::Less, - (_, KnownInstanceType::Intersection) => Ordering::Greater, - - (KnownInstanceType::TypeOf, _) => Ordering::Less, - (_, KnownInstanceType::TypeOf) => Ordering::Greater, - - (KnownInstanceType::CallableTypeOf, _) => Ordering::Less, - (_, KnownInstanceType::CallableTypeOf) => Ordering::Greater, - - (KnownInstanceType::Unpack, _) => Ordering::Less, - (_, KnownInstanceType::Unpack) => Ordering::Greater, - - (KnownInstanceType::TypingSelf, _) => Ordering::Less, - (_, KnownInstanceType::TypingSelf) => Ordering::Greater, - - (KnownInstanceType::Required, _) => Ordering::Less, - (_, KnownInstanceType::Required) => Ordering::Greater, - - (KnownInstanceType::NotRequired, _) => Ordering::Less, - (_, KnownInstanceType::NotRequired) => Ordering::Greater, - - (KnownInstanceType::TypeIs, _) => Ordering::Less, - (_, KnownInstanceType::TypeIs) => Ordering::Greater, - - (KnownInstanceType::ReadOnly, _) => Ordering::Less, - (_, KnownInstanceType::ReadOnly) => Ordering::Greater, - - (KnownInstanceType::Union, _) => Ordering::Less, - (_, KnownInstanceType::Union) => Ordering::Greater, - - ( - KnownInstanceType::TypeAliasType(left), - KnownInstanceType::TypeAliasType(right), - ) => left.cmp(right), - (KnownInstanceType::TypeAliasType(_), _) => Ordering::Less, - (_, KnownInstanceType::TypeAliasType(_)) => Ordering::Greater, - - (KnownInstanceType::TypeVar(left), KnownInstanceType::TypeVar(right)) => { - left.cmp(right) - } - } - } - - (Type::KnownInstance(_), _) => Ordering::Less, - (_, Type::KnownInstance(_)) => Ordering::Greater, - - (Type::PropertyInstance(left), Type::PropertyInstance(right)) => left.cmp(right), - (Type::PropertyInstance(_), _) => Ordering::Less, - (_, Type::PropertyInstance(_)) => Ordering::Greater, - - (Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right), - (Type::Dynamic(_), _) => Ordering::Less, - (_, Type::Dynamic(_)) => Ordering::Greater, - - (Type::Union(_), _) | (_, Type::Union(_)) => { - unreachable!("our type representation does not permit nested unions"); - } - - (Type::Intersection(left), Type::Intersection(right)) => { - debug_assert_eq!(*left, left.normalized(db)); - debug_assert_eq!(*right, right.normalized(db)); - - if left == right { - return Ordering::Equal; - } - - // Lexicographically compare the elements of the two unequal intersections. - let left_positive = left.positive(db); - let right_positive = right.positive(db); - if left_positive.len() != right_positive.len() { - return left_positive.len().cmp(&right_positive.len()); - } - let left_negative = left.negative(db); - let right_negative = right.negative(db); - if left_negative.len() != right_negative.len() { - return left_negative.len().cmp(&right_negative.len()); - } - for (left, right) in left_positive.iter().zip(right_positive) { - let ordering = union_or_intersection_elements_ordering(db, left, right); - if ordering != Ordering::Equal { - return ordering; - } - } - for (left, right) in left_negative.iter().zip(right_negative) { - let ordering = union_or_intersection_elements_ordering(db, left, right); - if ordering != Ordering::Equal { - return ordering; - } - } - - unreachable!("Two equal intersections that both have sorted elements should share the same Salsa ID") - } - } -} - -/// Determine a canonical order for two instances of [`DynamicType`]. -fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering { - match (left, right) { - (DynamicType::Any, _) => Ordering::Less, - (_, DynamicType::Any) => Ordering::Greater, - - (DynamicType::Unknown, _) => Ordering::Less, - (_, DynamicType::Unknown) => Ordering::Greater, - - #[cfg(debug_assertions)] - (DynamicType::Todo(TodoType(left)), DynamicType::Todo(TodoType(right))) => left.cmp(right), - - #[cfg(not(debug_assertions))] - (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, - - (DynamicType::TodoProtocol, _) => Ordering::Less, - (_, DynamicType::TodoProtocol) => Ordering::Greater, - } -} diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs deleted file mode 100644 index a711357c879b8..0000000000000 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ /dev/null @@ -1,293 +0,0 @@ -use std::borrow::Cow; -use std::cmp::Ordering; - -use rustc_hash::FxHashMap; - -use ruff_python_ast::{self as ast, AnyNodeRef}; - -use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId}; -use crate::semantic_index::symbol::ScopeId; -use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics}; -use crate::unpack::{UnpackKind, UnpackValue}; -use crate::Db; - -use super::context::InferContext; -use super::diagnostic::INVALID_ASSIGNMENT; -use super::{TupleType, UnionType}; - -/// Unpacks the value expression type to their respective targets. -pub(crate) struct Unpacker<'db> { - context: InferContext<'db>, - scope: ScopeId<'db>, - targets: FxHashMap>, -} - -impl<'db> Unpacker<'db> { - pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self { - Self { - context: InferContext::new(db, scope), - targets: FxHashMap::default(), - scope, - } - } - - fn db(&self) -> &'db dyn Db { - self.context.db() - } - - /// Unpack the value to the target expression. - pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) { - debug_assert!( - matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)), - "Unpacking target must be a list or tuple expression" - ); - - let value_type = infer_expression_types(self.db(), value.expression()) - .expression_type(value.scoped_expression_id(self.db(), self.scope)); - - let value_type = match value.kind() { - UnpackKind::Assign => { - if self.context.in_stub() - && value - .expression() - .node_ref(self.db()) - .is_ellipsis_literal_expr() - { - Type::unknown() - } else { - value_type - } - } - UnpackKind::Iterable => value_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, value_type, value.as_any_node_ref(self.db())); - err.fallback_element_type(self.db()) - }), - UnpackKind::ContextManager => value_type.try_enter(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, value_type, value.as_any_node_ref(self.db())); - err.fallback_enter_type(self.db()) - }), - }; - - self.unpack_inner(target, value.as_any_node_ref(self.db()), value_type); - } - - fn unpack_inner( - &mut self, - target: &ast::Expr, - value_expr: AnyNodeRef<'db>, - value_ty: Type<'db>, - ) { - match target { - ast::Expr::Name(_) | ast::Expr::Attribute(_) => { - self.targets - .insert(target.scoped_expression_id(self.db(), self.scope), value_ty); - } - ast::Expr::Starred(ast::ExprStarred { value, .. }) => { - self.unpack_inner(value, value_expr, value_ty); - } - ast::Expr::List(ast::ExprList { elts, .. }) - | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { - // Initialize the vector of target types, one for each target. - // - // This is mainly useful for the union type where the target type at index `n` is - // going to be a union of types from every union type element at index `n`. - // - // For example, if the type is `tuple[int, int] | tuple[int, str]` and the target - // has two elements `(a, b)`, then - // * The type of `a` will be a union of `int` and `int` which are at index 0 in the - // first and second tuple respectively which resolves to an `int`. - // * Similarly, the type of `b` will be a union of `int` and `str` which are at - // index 1 in the first and second tuple respectively which will be `int | str`. - let mut target_types = vec![vec![]; elts.len()]; - - let unpack_types = match value_ty { - Type::Union(union_ty) => union_ty.elements(self.db()), - _ => std::slice::from_ref(&value_ty), - }; - - for ty in unpack_types.iter().copied() { - // Deconstruct certain types to delegate the inference back to the tuple type - // for correct handling of starred expressions. - let ty = match ty { - Type::StringLiteral(string_literal_ty) => { - // We could go further and deconstruct to an array of `StringLiteral` - // with each individual character, instead of just an array of - // `LiteralString`, but there would be a cost and it's not clear that - // it's worth it. - TupleType::from_elements( - self.db(), - std::iter::repeat_n( - Type::LiteralString, - string_literal_ty.python_len(self.db()), - ), - ) - } - _ => ty, - }; - - if let Some(tuple_ty) = ty.into_tuple() { - let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty); - - let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) { - Ordering::Less => { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Too many values to unpack (expected {}, got {})", - elts.len(), - tuple_ty_elements.len() - ), - ); - true - } - Ordering::Greater => { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Not enough values to unpack (expected {}, got {})", - elts.len(), - tuple_ty_elements.len() - ), - ); - true - } - Ordering::Equal => false, - }; - - for (index, ty) in tuple_ty_elements.iter().enumerate() { - if let Some(element_types) = target_types.get_mut(index) { - if length_mismatch { - element_types.push(Type::unknown()); - } else { - element_types.push(*ty); - } - } - } - } else { - let ty = if ty.is_literal_string() { - Type::LiteralString - } else { - ty.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, ty, value_expr); - err.fallback_element_type(self.db()) - }) - }; - for target_type in &mut target_types { - target_type.push(ty); - } - } - } - - for (index, element) in elts.iter().enumerate() { - // SAFETY: `target_types` is initialized with the same length as `elts`. - let element_ty = match target_types[index].as_slice() { - [] => Type::unknown(), - types => UnionType::from_elements(self.db(), types), - }; - self.unpack_inner(element, value_expr, element_ty); - } - } - _ => {} - } - } - - /// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there - /// can be a starred expression in the `elements`. - fn tuple_ty_elements( - &self, - expr: &ast::Expr, - targets: &[ast::Expr], - tuple_ty: TupleType<'db>, - ) -> Cow<'_, [Type<'db>]> { - // If there is a starred expression, it will consume all of the types at that location. - let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else { - // Otherwise, the types will be unpacked 1-1 to the targets. - return Cow::Borrowed(tuple_ty.elements(self.db()).as_ref()); - }; - - if tuple_ty.len(self.db()) >= targets.len() - 1 { - // This branch is only taken when there are enough elements in the tuple type to - // combine for the starred expression. So, the arithmetic and indexing operations are - // safe to perform. - let mut element_types = Vec::with_capacity(targets.len()); - - // Insert all the elements before the starred expression. - element_types.extend_from_slice( - // SAFETY: Safe because of the length check above. - &tuple_ty.elements(self.db())[..starred_index], - ); - - // The number of target expressions that are remaining after the starred expression. - // For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the - // remaining elements after that are 2. - let remaining = targets.len() - (starred_index + 1); - - // This index represents the position of the last element that belongs to the starred - // expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the - // starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and - // the index value would be 3. - let starred_end_index = tuple_ty.len(self.db()) - remaining; - - // SAFETY: Safe because of the length check above. - let _starred_element_types = - &tuple_ty.elements(self.db())[starred_index..starred_end_index]; - // TODO: Combine the types into a list type. If the - // starred_element_types is empty, then it should be `List[Any]`. - // combine_types(starred_element_types); - element_types.push(todo_type!("starred unpacking")); - - // Insert the types remaining that aren't consumed by the starred expression. - element_types.extend_from_slice( - // SAFETY: Safe because of the length check above. - &tuple_ty.elements(self.db())[starred_end_index..], - ); - - Cow::Owned(element_types) - } else { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - expr, - format_args!( - "Not enough values to unpack (expected {} or more, got {})", - targets.len() - 1, - tuple_ty.len(self.db()) - ), - ); - - Cow::Owned(vec![Type::unknown(); targets.len()]) - } - } - - pub(crate) fn finish(mut self) -> UnpackResult<'db> { - self.targets.shrink_to_fit(); - UnpackResult { - diagnostics: self.context.finish(), - targets: self.targets, - } - } -} - -#[derive(Debug, Default, PartialEq, Eq, salsa::Update)] -pub(crate) struct UnpackResult<'db> { - targets: FxHashMap>, - diagnostics: TypeCheckDiagnostics, -} - -impl<'db> UnpackResult<'db> { - /// Returns the inferred type for a given sub-expression of the left-hand side target - /// of an unpacking assignment. - /// - /// Panics if a scoped expression ID is passed in that does not correspond to a sub- - /// expression of the target. - #[track_caller] - pub(crate) fn expression_type(&self, expr_id: ScopedExpressionId) -> Type<'db> { - self.targets[&expr_id] - } - - /// Returns the diagnostics in this unpacking assignment. - pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { - &self.diagnostics - } -} diff --git a/crates/red_knot_python_semantic/src/util/mod.rs b/crates/red_knot_python_semantic/src/util/mod.rs deleted file mode 100644 index 0f80fb9c6ef09..0000000000000 --- a/crates/red_knot_python_semantic/src/util/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod subscript; diff --git a/crates/red_knot_python_semantic/src/util/subscript.rs b/crates/red_knot_python_semantic/src/util/subscript.rs deleted file mode 100644 index 6dca232046e3e..0000000000000 --- a/crates/red_knot_python_semantic/src/util/subscript.rs +++ /dev/null @@ -1,502 +0,0 @@ -//! This module provides utility functions for indexing (`PyIndex`) and slicing -//! operations (`PySlice`) on iterators, following the semantics of equivalent -//! operations in Python. - -use itertools::Either; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct OutOfBoundsError; - -pub(crate) trait PyIndex { - type Item; - - fn py_index(&mut self, index: i32) -> Result; -} - -fn from_nonnegative_i32(index: i32) -> usize { - static_assertions::const_assert!(usize::BITS >= 32); - debug_assert!(index >= 0); - - usize::try_from(index) - .expect("Should only ever pass a positive integer to `from_nonnegative_i32`") -} - -fn from_negative_i32(index: i32) -> usize { - static_assertions::const_assert!(usize::BITS >= 32); - - index.checked_neg().map(from_nonnegative_i32).unwrap_or({ - // 'checked_neg' only fails for i32::MIN. We can not - // represent -i32::MIN as a i32, but we can represent - // it as a usize, since usize is at least 32 bits. - from_nonnegative_i32(i32::MAX) + 1 - }) -} - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -enum Position { - BeforeStart, - AtIndex(usize), - AfterEnd, -} - -enum Nth { - FromStart(usize), - FromEnd(usize), -} - -impl Nth { - fn from_index(index: i32) -> Self { - if index >= 0 { - Nth::FromStart(from_nonnegative_i32(index)) - } else { - Nth::FromEnd(from_negative_i32(index) - 1) - } - } - - fn to_position(&self, len: usize) -> Position { - debug_assert!(len > 0); - - match self { - Nth::FromStart(nth) => { - if *nth < len { - Position::AtIndex(*nth) - } else { - Position::AfterEnd - } - } - Nth::FromEnd(nth_rev) => { - if *nth_rev < len { - Position::AtIndex(len - 1 - *nth_rev) - } else { - Position::BeforeStart - } - } - } - } -} - -impl PyIndex for T -where - T: DoubleEndedIterator, -{ - type Item = I; - - fn py_index(&mut self, index: i32) -> Result { - match Nth::from_index(index) { - Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError), - Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct StepSizeZeroError; - -pub(crate) trait PySlice { - type Item; - - fn py_slice( - &self, - start: Option, - stop: Option, - step: Option, - ) -> Result< - Either, impl Iterator>, - StepSizeZeroError, - >; -} - -impl PySlice for [T] { - type Item = T; - - fn py_slice( - &self, - start: Option, - stop: Option, - step_int: Option, - ) -> Result< - Either, impl Iterator>, - StepSizeZeroError, - > { - let step_int = step_int.unwrap_or(1); - if step_int == 0 { - return Err(StepSizeZeroError); - } - - let len = self.len(); - if len == 0 { - // The iterator needs to have the same type as the step>0 case below, - // so we need to use `.skip(0)`. - #[allow(clippy::iter_skip_zero)] - return Ok(Either::Left(self.iter().skip(0).take(0).step_by(1))); - } - - let to_position = |index| Nth::from_index(index).to_position(len); - - if step_int.is_positive() { - let step = from_nonnegative_i32(step_int); - - let start = start.map(to_position).unwrap_or(Position::BeforeStart); - let stop = stop.map(to_position).unwrap_or(Position::AfterEnd); - - let (skip, take, step) = if start < stop { - let skip = match start { - Position::BeforeStart => 0, - Position::AtIndex(start_index) => start_index, - Position::AfterEnd => len, - }; - - let take = match stop { - Position::BeforeStart => 0, - Position::AtIndex(stop_index) => stop_index - skip, - Position::AfterEnd => len - skip, - }; - - (skip, take, step) - } else { - (0, 0, step) - }; - - Ok(Either::Left( - self.iter().skip(skip).take(take).step_by(step), - )) - } else { - let step = from_negative_i32(step_int); - - let start = start.map(to_position).unwrap_or(Position::AfterEnd); - let stop = stop.map(to_position).unwrap_or(Position::BeforeStart); - - let (skip, take, step) = if start <= stop { - (0, 0, step) - } else { - let skip = match start { - Position::BeforeStart => len, - Position::AtIndex(start_index) => len - 1 - start_index, - Position::AfterEnd => 0, - }; - - let take = match stop { - Position::BeforeStart => len - skip, - Position::AtIndex(stop_index) => (len - 1) - skip - stop_index, - Position::AfterEnd => 0, - }; - - (skip, take, step) - }; - - Ok(Either::Right( - self.iter().rev().skip(skip).take(take).step_by(step), - )) - } - } -} - -#[cfg(test)] -#[allow(clippy::redundant_clone)] -mod tests { - use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError}; - - use super::{PyIndex, PySlice}; - use itertools::assert_equal; - - #[test] - fn py_index_empty() { - let iter = std::iter::empty::(); - - assert_eq!(iter.clone().py_index(0), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(-1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(i32::MIN), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(i32::MAX), Err(OutOfBoundsError)); - } - - #[test] - fn py_index_single_element() { - let iter = ['a'].into_iter(); - - assert_eq!(iter.clone().py_index(0), Ok('a')); - assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(-1), Ok('a')); - assert_eq!(iter.clone().py_index(-2), Err(OutOfBoundsError)); - } - - #[test] - fn py_index_more_elements() { - let iter = ['a', 'b', 'c', 'd', 'e'].into_iter(); - - assert_eq!(iter.clone().py_index(0), Ok('a')); - assert_eq!(iter.clone().py_index(1), Ok('b')); - assert_eq!(iter.clone().py_index(4), Ok('e')); - assert_eq!(iter.clone().py_index(5), Err(OutOfBoundsError)); - - assert_eq!(iter.clone().py_index(-1), Ok('e')); - assert_eq!(iter.clone().py_index(-2), Ok('d')); - assert_eq!(iter.clone().py_index(-5), Ok('a')); - assert_eq!(iter.clone().py_index(-6), Err(OutOfBoundsError)); - } - - #[test] - fn py_index_uses_full_index_range() { - let iter = 0..=u32::MAX; - - // u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31 - assert_eq!(iter.clone().py_index(i32::MIN), Ok(2u32.pow(31))); - assert_eq!(iter.clone().py_index(-2), Ok(u32::MAX - 2 + 1)); - assert_eq!(iter.clone().py_index(-1), Ok(u32::MAX - 1 + 1)); - - assert_eq!(iter.clone().py_index(0), Ok(0)); - assert_eq!(iter.clone().py_index(1), Ok(1)); - assert_eq!(iter.clone().py_index(i32::MAX), Ok(i32::MAX as u32)); - } - - #[track_caller] - fn assert_eq_slice( - input: &[char; N], - start: Option, - stop: Option, - step: Option, - expected: &[char; M], - ) { - assert_equal(input.py_slice(start, stop, step).unwrap(), expected.iter()); - } - - #[test] - fn py_slice_empty_input() { - let input = []; - - assert_eq_slice(&input, None, None, None, &[]); - assert_eq_slice(&input, Some(0), None, None, &[]); - assert_eq_slice(&input, None, Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(0), None, &[]); - assert_eq_slice(&input, Some(-5), Some(-5), None, &[]); - assert_eq_slice(&input, None, None, Some(-1), &[]); - assert_eq_slice(&input, None, None, Some(2), &[]); - } - - #[test] - fn py_slice_single_element_input() { - let input = ['a']; - - assert_eq_slice(&input, None, None, None, &['a']); - - assert_eq_slice(&input, Some(0), None, None, &['a']); - assert_eq_slice(&input, None, Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(1), None, &['a']); - assert_eq_slice(&input, Some(0), Some(2), None, &['a']); - - assert_eq_slice(&input, Some(-1), None, None, &['a']); - assert_eq_slice(&input, Some(-1), Some(-1), None, &[]); - assert_eq_slice(&input, Some(-1), Some(0), None, &[]); - assert_eq_slice(&input, Some(-1), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-1), Some(2), None, &['a']); - assert_eq_slice(&input, None, Some(-1), None, &[]); - - assert_eq_slice(&input, Some(-2), None, None, &['a']); - assert_eq_slice(&input, Some(-2), Some(-1), None, &[]); - assert_eq_slice(&input, Some(-2), Some(0), None, &[]); - assert_eq_slice(&input, Some(-2), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-2), Some(2), None, &['a']); - } - - #[test] - fn py_slice_nonnegative_indices() { - let input = ['a', 'b', 'c', 'd', 'e']; - - assert_eq_slice(&input, None, Some(0), None, &[]); - assert_eq_slice(&input, None, Some(1), None, &['a']); - assert_eq_slice(&input, None, Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, None, None, None, &['a', 'b', 'c', 'd', 'e']); - - assert_eq_slice(&input, Some(0), Some(0), None, &[]); - assert_eq_slice(&input, Some(0), Some(1), None, &['a']); - assert_eq_slice(&input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(0), Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(0), Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']); - - assert_eq_slice(&input, Some(1), Some(0), None, &[]); - assert_eq_slice(&input, Some(1), Some(1), None, &[]); - assert_eq_slice(&input, Some(1), Some(2), None, &['b']); - assert_eq_slice(&input, Some(1), Some(4), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(1), None, None, &['b', 'c', 'd', 'e']); - - assert_eq_slice(&input, Some(4), Some(0), None, &[]); - assert_eq_slice(&input, Some(4), Some(4), None, &[]); - assert_eq_slice(&input, Some(4), Some(5), None, &['e']); - assert_eq_slice(&input, Some(4), Some(6), None, &['e']); - assert_eq_slice(&input, Some(4), None, None, &['e']); - - assert_eq_slice(&input, Some(5), Some(0), None, &[]); - assert_eq_slice(&input, Some(5), Some(5), None, &[]); - assert_eq_slice(&input, Some(5), Some(6), None, &[]); - assert_eq_slice(&input, Some(5), None, None, &[]); - - assert_eq_slice(&input, Some(6), Some(0), None, &[]); - assert_eq_slice(&input, Some(6), Some(6), None, &[]); - assert_eq_slice(&input, Some(6), None, None, &[]); - } - - #[test] - fn py_slice_negatice_indices() { - let input = ['a', 'b', 'c', 'd', 'e']; - - assert_eq_slice(&input, Some(-6), None, None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-6), Some(-4), None, &['a']); - assert_eq_slice(&input, Some(-6), Some(-5), None, &[]); - assert_eq_slice(&input, Some(-6), Some(-6), None, &[]); - assert_eq_slice(&input, Some(-6), Some(-10), None, &[]); - - assert_eq_slice(&input, Some(-5), None, None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-5), Some(-4), None, &['a']); - assert_eq_slice(&input, Some(-5), Some(-5), None, &[]); - assert_eq_slice(&input, Some(-5), Some(-6), None, &[]); - assert_eq_slice(&input, Some(-5), Some(-10), None, &[]); - - assert_eq_slice(&input, Some(-4), None, None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-4), Some(-1), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(-4), Some(-3), None, &['b']); - assert_eq_slice(&input, Some(-4), Some(-4), None, &[]); - assert_eq_slice(&input, Some(-4), Some(-10), None, &[]); - - assert_eq_slice(&input, Some(-1), None, None, &['e']); - assert_eq_slice(&input, Some(-1), Some(-1), None, &[]); - assert_eq_slice(&input, Some(-1), Some(-10), None, &[]); - - assert_eq_slice(&input, None, Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, None, Some(-4), None, &['a']); - assert_eq_slice(&input, None, Some(-5), None, &[]); - assert_eq_slice(&input, None, Some(-6), None, &[]); - } - - #[test] - fn py_slice_mixed_positive_negative_indices() { - let input = ['a', 'b', 'c', 'd', 'e']; - - assert_eq_slice(&input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(1), Some(-1), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(3), Some(-1), None, &['d']); - assert_eq_slice(&input, Some(4), Some(-1), None, &[]); - assert_eq_slice(&input, Some(5), Some(-1), None, &[]); - - assert_eq_slice(&input, Some(0), Some(-4), None, &['a']); - assert_eq_slice(&input, Some(1), Some(-4), None, &[]); - assert_eq_slice(&input, Some(3), Some(-4), None, &[]); - - assert_eq_slice(&input, Some(0), Some(-5), None, &[]); - assert_eq_slice(&input, Some(1), Some(-5), None, &[]); - assert_eq_slice(&input, Some(3), Some(-5), None, &[]); - - assert_eq_slice(&input, Some(0), Some(-6), None, &[]); - assert_eq_slice(&input, Some(1), Some(-6), None, &[]); - - assert_eq_slice(&input, Some(-6), Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-6), Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-6), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-6), Some(0), None, &[]); - - assert_eq_slice(&input, Some(-5), Some(6), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-5), Some(5), None, &['a', 'b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']); - assert_eq_slice(&input, Some(-5), Some(1), None, &['a']); - assert_eq_slice(&input, Some(-5), Some(0), None, &[]); - - assert_eq_slice(&input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']); - assert_eq_slice(&input, Some(-4), Some(4), None, &['b', 'c', 'd']); - assert_eq_slice(&input, Some(-4), Some(2), None, &['b']); - assert_eq_slice(&input, Some(-4), Some(1), None, &[]); - assert_eq_slice(&input, Some(-4), Some(0), None, &[]); - - assert_eq_slice(&input, Some(-1), Some(6), None, &['e']); - assert_eq_slice(&input, Some(-1), Some(5), None, &['e']); - assert_eq_slice(&input, Some(-1), Some(4), None, &[]); - assert_eq_slice(&input, Some(-1), Some(1), None, &[]); - } - - #[test] - fn py_slice_step_forward() { - // indices: 0 1 2 3 4 5 6 - let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; - - // Step size zero is invalid: - assert!(matches!( - input.py_slice(None, None, Some(0)), - Err(StepSizeZeroError) - )); - assert!(matches!( - input.py_slice(Some(0), Some(5), Some(0)), - Err(StepSizeZeroError) - )); - assert!(matches!( - input.py_slice(Some(0), Some(0), Some(0)), - Err(StepSizeZeroError) - )); - - assert_eq_slice(&input, Some(0), Some(8), Some(2), &['a', 'c', 'e', 'g']); - assert_eq_slice(&input, Some(0), Some(7), Some(2), &['a', 'c', 'e', 'g']); - assert_eq_slice(&input, Some(0), Some(6), Some(2), &['a', 'c', 'e']); - assert_eq_slice(&input, Some(0), Some(5), Some(2), &['a', 'c', 'e']); - assert_eq_slice(&input, Some(0), Some(4), Some(2), &['a', 'c']); - assert_eq_slice(&input, Some(0), Some(3), Some(2), &['a', 'c']); - assert_eq_slice(&input, Some(0), Some(2), Some(2), &['a']); - assert_eq_slice(&input, Some(0), Some(1), Some(2), &['a']); - assert_eq_slice(&input, Some(0), Some(0), Some(2), &[]); - assert_eq_slice(&input, Some(1), Some(5), Some(2), &['b', 'd']); - - assert_eq_slice(&input, Some(0), Some(7), Some(3), &['a', 'd', 'g']); - assert_eq_slice(&input, Some(0), Some(6), Some(3), &['a', 'd']); - - assert_eq_slice(&input, Some(0), None, Some(10), &['a']); - } - - #[test] - fn py_slice_step_backward() { - // indices: 0 1 2 3 4 5 6 - let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; - - assert_eq_slice(&input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']); - assert_eq_slice(&input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']); - assert_eq_slice(&input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']); - assert_eq_slice(&input, Some(4), Some(0), Some(-2), &['e', 'c']); - assert_eq_slice(&input, Some(3), Some(0), Some(-2), &['d', 'b']); - assert_eq_slice(&input, Some(2), Some(0), Some(-2), &['c']); - assert_eq_slice(&input, Some(1), Some(0), Some(-2), &['b']); - assert_eq_slice(&input, Some(0), Some(0), Some(-2), &[]); - - assert_eq_slice(&input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']); - assert_eq_slice(&input, None, None, Some(-2), &['g', 'e', 'c', 'a']); - assert_eq_slice(&input, None, Some(0), Some(-2), &['g', 'e', 'c']); - - assert_eq_slice(&input, Some(5), Some(1), Some(-2), &['f', 'd']); - assert_eq_slice(&input, Some(5), Some(2), Some(-2), &['f', 'd']); - assert_eq_slice(&input, Some(5), Some(3), Some(-2), &['f']); - assert_eq_slice(&input, Some(5), Some(4), Some(-2), &['f']); - assert_eq_slice(&input, Some(5), Some(5), Some(-2), &[]); - - assert_eq_slice(&input, Some(6), None, Some(-3), &['g', 'd', 'a']); - assert_eq_slice(&input, Some(6), Some(0), Some(-3), &['g', 'd']); - - assert_eq_slice(&input, Some(7), None, Some(-10), &['g']); - - assert_eq_slice(&input, Some(-6), Some(-9), Some(-1), &['b', 'a']); - assert_eq_slice(&input, Some(-6), Some(-8), Some(-1), &['b', 'a']); - assert_eq_slice(&input, Some(-6), Some(-7), Some(-1), &['b']); - assert_eq_slice(&input, Some(-6), Some(-6), Some(-1), &[]); - - assert_eq_slice(&input, Some(-7), Some(-9), Some(-1), &['a']); - - assert_eq_slice(&input, Some(-8), Some(-9), Some(-1), &[]); - assert_eq_slice(&input, Some(-9), Some(-9), Some(-1), &[]); - - assert_eq_slice(&input, Some(-6), Some(-2), Some(-1), &[]); - assert_eq_slice(&input, Some(-9), Some(-6), Some(-1), &[]); - } -} diff --git a/crates/red_knot_server/Cargo.toml b/crates/red_knot_server/Cargo.toml deleted file mode 100644 index c5c17c156190a..0000000000000 --- a/crates/red_knot_server/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "red_knot_server" -version = "0.0.0" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[dependencies] -red_knot_ide = { workspace = true } -red_knot_project = { workspace = true } -red_knot_python_semantic = { workspace = true } - -ruff_db = { workspace = true, features = ["os"] } -ruff_notebook = { workspace = true } -ruff_source_file = { workspace = true } -ruff_text_size = { workspace = true } - -anyhow = { workspace = true } -crossbeam = { workspace = true } -jod-thread = { workspace = true } -lsp-server = { workspace = true } -lsp-types = { workspace = true } -rustc-hash = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -shellexpand = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } - -[dev-dependencies] - -[target.'cfg(target_vendor = "apple")'.dependencies] -libc = { workspace = true } - -[lints] -workspace = true diff --git a/crates/red_knot_server/src/document.rs b/crates/red_knot_server/src/document.rs deleted file mode 100644 index 7dca10720fe2b..0000000000000 --- a/crates/red_knot_server/src/document.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion. - -mod location; -mod notebook; -mod range; -mod text_document; - -pub(crate) use location::ToLink; -use lsp_types::{PositionEncodingKind, Url}; -pub use notebook::NotebookDocument; -pub(crate) use range::{FileRangeExt, PositionExt, RangeExt, TextSizeExt, ToRangeExt}; -pub(crate) use text_document::DocumentVersion; -pub use text_document::TextDocument; - -/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. -// Please maintain the order from least to greatest priority for the derived `Ord` impl. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum PositionEncoding { - /// UTF 16 is the encoding supported by all LSP clients. - #[default] - UTF16, - - /// Second choice because UTF32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy) - UTF32, - - /// Ruff's preferred encoding - UTF8, -} - -/// A unique document ID, derived from a URL passed as part of an LSP request. -/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. -#[derive(Clone, Debug)] -pub enum DocumentKey { - Notebook(Url), - NotebookCell(Url), - Text(Url), -} - -impl DocumentKey { - /// Returns the URL associated with the key. - pub(crate) fn url(&self) -> &Url { - match self { - DocumentKey::NotebookCell(url) - | DocumentKey::Notebook(url) - | DocumentKey::Text(url) => url, - } - } -} - -impl std::fmt::Display for DocumentKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NotebookCell(url) | Self::Notebook(url) | Self::Text(url) => url.fmt(f), - } - } -} - -impl From for PositionEncodingKind { - fn from(value: PositionEncoding) -> Self { - match value { - PositionEncoding::UTF8 => PositionEncodingKind::UTF8, - PositionEncoding::UTF16 => PositionEncodingKind::UTF16, - PositionEncoding::UTF32 => PositionEncodingKind::UTF32, - } - } -} - -impl TryFrom<&PositionEncodingKind> for PositionEncoding { - type Error = (); - - fn try_from(value: &PositionEncodingKind) -> Result { - Ok(if value == &PositionEncodingKind::UTF8 { - PositionEncoding::UTF8 - } else if value == &PositionEncodingKind::UTF16 { - PositionEncoding::UTF16 - } else if value == &PositionEncodingKind::UTF32 { - PositionEncoding::UTF32 - } else { - return Err(()); - }) - } -} diff --git a/crates/red_knot_server/src/document/location.rs b/crates/red_knot_server/src/document/location.rs deleted file mode 100644 index c2e73fcb246c4..0000000000000 --- a/crates/red_knot_server/src/document/location.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::document::{FileRangeExt, ToRangeExt}; -use crate::system::file_to_url; -use crate::PositionEncoding; -use lsp_types::Location; -use red_knot_ide::{Db, NavigationTarget}; -use ruff_db::files::FileRange; -use ruff_db::source::{line_index, source_text}; -use ruff_text_size::Ranged; - -pub(crate) trait ToLink { - fn to_location( - &self, - db: &dyn red_knot_ide::Db, - encoding: PositionEncoding, - ) -> Option; - - fn to_link( - &self, - db: &dyn red_knot_ide::Db, - src: Option, - encoding: PositionEncoding, - ) -> Option; -} - -impl ToLink for NavigationTarget { - fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option { - FileRange::new(self.file(), self.focus_range()).to_location(db.upcast(), encoding) - } - - fn to_link( - &self, - db: &dyn Db, - src: Option, - encoding: PositionEncoding, - ) -> Option { - let file = self.file(); - let uri = file_to_url(db.upcast(), file)?; - let source = source_text(db.upcast(), file); - let index = line_index(db.upcast(), file); - - let target_range = self.full_range().to_lsp_range(&source, &index, encoding); - let selection_range = self.focus_range().to_lsp_range(&source, &index, encoding); - - let src = src.map(|src| { - let source = source_text(db.upcast(), src.file()); - let index = line_index(db.upcast(), src.file()); - - src.range().to_lsp_range(&source, &index, encoding) - }); - - Some(lsp_types::LocationLink { - target_uri: uri, - target_range, - target_selection_range: selection_range, - origin_selection_range: src, - }) - } -} diff --git a/crates/red_knot_server/src/document/range.rs b/crates/red_knot_server/src/document/range.rs deleted file mode 100644 index 0e268dcfb1cdb..0000000000000 --- a/crates/red_knot_server/src/document/range.rs +++ /dev/null @@ -1,254 +0,0 @@ -use super::notebook; -use super::PositionEncoding; -use crate::system::file_to_url; - -use lsp_types as types; -use lsp_types::Location; - -use red_knot_python_semantic::Db; -use ruff_db::files::FileRange; -use ruff_db::source::{line_index, source_text}; -use ruff_notebook::NotebookIndex; -use ruff_source_file::OneIndexed; -use ruff_source_file::{LineIndex, SourceLocation}; -use ruff_text_size::{Ranged, TextRange, TextSize}; - -#[expect(dead_code)] -pub(crate) struct NotebookRange { - pub(crate) cell: notebook::CellId, - pub(crate) range: types::Range, -} - -pub(crate) trait RangeExt { - fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) - -> TextRange; -} - -pub(crate) trait PositionExt { - fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize; -} - -pub(crate) trait TextSizeExt { - fn to_position( - self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> types::Position - where - Self: Sized; -} - -impl TextSizeExt for TextSize { - fn to_position( - self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> types::Position { - let source_location = offset_to_source_location(self, text, index, encoding); - source_location_to_position(&source_location) - } -} - -pub(crate) trait ToRangeExt { - fn to_lsp_range( - &self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> types::Range; - - #[expect(dead_code)] - fn to_notebook_range( - &self, - text: &str, - source_index: &LineIndex, - notebook_index: &NotebookIndex, - encoding: PositionEncoding, - ) -> NotebookRange; -} - -fn u32_index_to_usize(index: u32) -> usize { - usize::try_from(index).expect("u32 fits in usize") -} - -impl PositionExt for lsp_types::Position { - fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize { - let start_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)), - text, - ); - - let start_column_offset = match encoding { - PositionEncoding::UTF8 => TextSize::new(self.character), - - PositionEncoding::UTF16 => { - // Fast path for ASCII only documents - if index.is_ascii() { - TextSize::new(self.character) - } else { - // UTF16 encodes characters either as one or two 16 bit words. - // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) - // UTF-16 with a text that may use variable-length characters. - utf8_column_offset(self.character, &text[start_line]) - } - } - PositionEncoding::UTF32 => { - // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. - return index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.character)), - text, - ); - } - }; - - start_line.start() + start_column_offset.clamp(TextSize::new(0), start_line.end()) - } -} - -impl RangeExt for lsp_types::Range { - fn to_text_range( - &self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> TextRange { - TextRange::new( - self.start.to_text_size(text, index, encoding), - self.end.to_text_size(text, index, encoding), - ) - } -} - -impl ToRangeExt for TextRange { - fn to_lsp_range( - &self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> types::Range { - types::Range { - start: self.start().to_position(text, index, encoding), - end: self.end().to_position(text, index, encoding), - } - } - - fn to_notebook_range( - &self, - text: &str, - source_index: &LineIndex, - notebook_index: &NotebookIndex, - encoding: PositionEncoding, - ) -> NotebookRange { - let start = offset_to_source_location(self.start(), text, source_index, encoding); - let mut end = offset_to_source_location(self.end(), text, source_index, encoding); - let starting_cell = notebook_index.cell(start.row); - - // weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds') - // we need to move it one character back (which should place it at the end of the last line). - // we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset. - if notebook_index.cell(end.row) != starting_cell { - end.row = end.row.saturating_sub(1); - end.column = offset_to_source_location( - self.end().checked_sub(1.into()).unwrap_or_default(), - text, - source_index, - encoding, - ) - .column; - } - - let start = source_location_to_position(¬ebook_index.translate_location(&start)); - let end = source_location_to_position(¬ebook_index.translate_location(&end)); - - NotebookRange { - cell: starting_cell - .map(OneIndexed::to_zero_indexed) - .unwrap_or_default(), - range: types::Range { start, end }, - } - } -} - -/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. -fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::new(0); - - let mut i = 0u32; - - for c in line.chars() { - if i >= utf16_code_unit_offset { - break; - } - - // Count characters encoded as two 16 bit words as 2 characters. - { - utf8_code_unit_offset += - TextSize::new(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); - i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); - } - } - - utf8_code_unit_offset -} - -fn offset_to_source_location( - offset: TextSize, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, -) -> SourceLocation { - match encoding { - PositionEncoding::UTF8 => { - let row = index.line_index(offset); - let column = offset - index.line_start(row, text); - - SourceLocation { - column: OneIndexed::from_zero_indexed(column.to_usize()), - row, - } - } - PositionEncoding::UTF16 => { - let row = index.line_index(offset); - - let column = if index.is_ascii() { - (offset - index.line_start(row, text)).to_usize() - } else { - let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; - up_to_line.encode_utf16().count() - }; - - SourceLocation { - column: OneIndexed::from_zero_indexed(column), - row, - } - } - PositionEncoding::UTF32 => index.source_location(offset, text), - } -} - -fn source_location_to_position(location: &SourceLocation) -> types::Position { - types::Position { - line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), - character: u32::try_from(location.column.to_zero_indexed()) - .expect("character usize fits in u32"), - } -} - -pub(crate) trait FileRangeExt { - fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option; -} - -impl FileRangeExt for FileRange { - fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option { - let file = self.file(); - let uri = file_to_url(db, file)?; - let source = source_text(db.upcast(), file); - let line_index = line_index(db.upcast(), file); - - let range = self.range().to_lsp_range(&source, &line_index, encoding); - Some(Location { uri, range }) - } -} diff --git a/crates/red_knot_server/src/lib.rs b/crates/red_knot_server/src/lib.rs deleted file mode 100644 index f5ba01d689490..0000000000000 --- a/crates/red_knot_server/src/lib.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::server::Server; -use anyhow::Context; -pub use document::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument}; -pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session}; -use std::num::NonZeroUsize; - -#[macro_use] -mod message; - -mod document; -mod logging; -mod server; -mod session; -mod system; - -pub(crate) const SERVER_NAME: &str = "red-knot"; -pub(crate) const DIAGNOSTIC_NAME: &str = "Red Knot"; - -/// A common result type used in most cases where a -/// result type is needed. -pub(crate) type Result = anyhow::Result; - -pub(crate) fn version() -> &'static str { - env!("CARGO_PKG_VERSION") -} - -pub fn run_server() -> anyhow::Result<()> { - let four = NonZeroUsize::new(4).unwrap(); - - // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. - let worker_threads = std::thread::available_parallelism() - .unwrap_or(four) - .max(four); - - Server::new(worker_threads) - .context("Failed to start server")? - .run()?; - - Ok(()) -} diff --git a/crates/red_knot_server/src/logging.rs b/crates/red_knot_server/src/logging.rs deleted file mode 100644 index bde8c57d09982..0000000000000 --- a/crates/red_knot_server/src/logging.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! The logging system for `red_knot server`. -//! -//! Log messages are controlled by the `logLevel` setting which defaults to `"info"`. Log messages -//! are written to `stderr` by default, which should appear in the logs for most LSP clients. A -//! `logFile` path can also be specified in the settings, and output will be directed there -//! instead. -use core::str; -use serde::Deserialize; -use std::{path::PathBuf, str::FromStr, sync::Arc}; -use tracing::level_filters::LevelFilter; -use tracing_subscriber::{ - fmt::{time::Uptime, writer::BoxMakeWriter}, - layer::SubscriberExt, - Layer, -}; - -pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Path>) { - let log_file = log_file - .map(|path| { - // this expands `logFile` so that tildes and environment variables - // are replaced with their values, if possible. - if let Some(expanded) = shellexpand::full(&path.to_string_lossy()) - .ok() - .and_then(|path| PathBuf::from_str(&path).ok()) - { - expanded - } else { - path.to_path_buf() - } - }) - .and_then(|path| { - std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&path) - .map_err(|err| { - #[allow(clippy::print_stderr)] - { - eprintln!( - "Failed to open file at {} for logging: {err}", - path.display() - ); - } - }) - .ok() - }); - - let logger = match log_file { - Some(file) => BoxMakeWriter::new(Arc::new(file)), - None => BoxMakeWriter::new(std::io::stderr), - }; - let subscriber = tracing_subscriber::Registry::default().with( - tracing_subscriber::fmt::layer() - .with_timer(Uptime::default()) - .with_thread_names(true) - .with_ansi(false) - .with_writer(logger) - .with_filter(LogLevelFilter { filter: log_level }), - ); - - tracing::subscriber::set_global_default(subscriber) - .expect("should be able to set global default subscriber"); -} - -/// The log level for the server as provided by the client during initialization. -/// -/// The default log level is `info`. -#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "lowercase")] -pub(crate) enum LogLevel { - Error, - Warn, - #[default] - Info, - Debug, - Trace, -} - -impl LogLevel { - fn trace_level(self) -> tracing::Level { - match self { - Self::Error => tracing::Level::ERROR, - Self::Warn => tracing::Level::WARN, - Self::Info => tracing::Level::INFO, - Self::Debug => tracing::Level::DEBUG, - Self::Trace => tracing::Level::TRACE, - } - } -} - -/// Filters out traces which have a log level lower than the `logLevel` set by the client. -struct LogLevelFilter { - filter: LogLevel, -} - -impl tracing_subscriber::layer::Filter for LogLevelFilter { - fn enabled( - &self, - meta: &tracing::Metadata<'_>, - _: &tracing_subscriber::layer::Context<'_, S>, - ) -> bool { - let filter = if meta.target().starts_with("red_knot") { - self.filter.trace_level() - } else { - tracing::Level::WARN - }; - - meta.level() <= &filter - } - - fn max_level_hint(&self) -> Option { - Some(LevelFilter::from_level(self.filter.trace_level())) - } -} diff --git a/crates/red_knot_server/src/message.rs b/crates/red_knot_server/src/message.rs deleted file mode 100644 index 038416aa46e57..0000000000000 --- a/crates/red_knot_server/src/message.rs +++ /dev/null @@ -1,46 +0,0 @@ -use anyhow::Context; -use lsp_types::notification::Notification; -use std::sync::OnceLock; - -use crate::server::ClientSender; - -static MESSENGER: OnceLock = OnceLock::new(); - -pub(crate) fn init_messenger(client_sender: ClientSender) { - MESSENGER - .set(client_sender) - .expect("messenger should only be initialized once"); -} - -pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) { - try_show_message(message, message_type).unwrap(); -} - -pub(super) fn try_show_message( - message: String, - message_type: lsp_types::MessageType, -) -> crate::Result<()> { - MESSENGER - .get() - .ok_or_else(|| anyhow::anyhow!("messenger not initialized"))? - .send(lsp_server::Message::Notification( - lsp_server::Notification { - method: lsp_types::notification::ShowMessage::METHOD.into(), - params: serde_json::to_value(lsp_types::ShowMessageParams { - typ: message_type, - message, - })?, - }, - )) - .context("Failed to send message")?; - - Ok(()) -} - -/// Sends an error to the client with a formatted message. The error is sent in a -/// `window/showMessage` notification. -macro_rules! show_err_msg { - ($msg:expr$(, $($arg:tt)*)?) => { - crate::message::show_message(::core::format_args!($msg$(, $($arg)*)?).to_string(), lsp_types::MessageType::ERROR) - }; -} diff --git a/crates/red_knot_server/src/server.rs b/crates/red_knot_server/src/server.rs deleted file mode 100644 index e57d1f7bbf94b..0000000000000 --- a/crates/red_knot_server/src/server.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! Scheduling, I/O, and API endpoints. - -use std::num::NonZeroUsize; -// The new PanicInfoHook name requires MSRV >= 1.82 -#[allow(deprecated)] -use std::panic::PanicInfo; - -use lsp_server::Message; -use lsp_types::{ - ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, - InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities, - TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, - TypeDefinitionProviderCapability, Url, -}; - -use self::connection::{Connection, ConnectionInitializer}; -use self::schedule::event_loop_thread; -use crate::session::{AllSettings, ClientSettings, Session}; -use crate::PositionEncoding; - -mod api; -mod client; -mod connection; -mod schedule; - -use crate::message::try_show_message; -pub(crate) use connection::ClientSender; - -pub(crate) type Result = std::result::Result; - -pub(crate) struct Server { - connection: Connection, - client_capabilities: ClientCapabilities, - worker_threads: NonZeroUsize, - session: Session, -} - -impl Server { - pub(crate) fn new(worker_threads: NonZeroUsize) -> crate::Result { - let connection = ConnectionInitializer::stdio(); - - let (id, init_params) = connection.initialize_start()?; - - let client_capabilities = init_params.capabilities; - let position_encoding = Self::find_best_position_encoding(&client_capabilities); - let server_capabilities = Self::server_capabilities(position_encoding); - - let connection = connection.initialize_finish( - id, - &server_capabilities, - crate::SERVER_NAME, - crate::version(), - )?; - - crate::message::init_messenger(connection.make_sender()); - - let AllSettings { - global_settings, - mut workspace_settings, - } = AllSettings::from_value( - init_params - .initialization_options - .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())), - ); - - crate::logging::init_logging( - global_settings.tracing.log_level.unwrap_or_default(), - global_settings.tracing.log_file.as_deref(), - ); - - let mut workspace_for_url = |url: Url| { - let Some(workspace_settings) = workspace_settings.as_mut() else { - return (url, ClientSettings::default()); - }; - let settings = workspace_settings.remove(&url).unwrap_or_else(|| { - tracing::warn!("No workspace settings found for {}", url); - ClientSettings::default() - }); - (url, settings) - }; - - let workspaces = init_params - .workspace_folders - .filter(|folders| !folders.is_empty()) - .map(|folders| folders.into_iter().map(|folder| { - workspace_for_url(folder.uri) - }).collect()) - .or_else(|| { - tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace..."); - let uri = Url::from_file_path(std::env::current_dir().ok()?).ok()?; - Some(vec![workspace_for_url(uri)]) - }) - .ok_or_else(|| { - anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.") - })?; - - if workspaces.len() > 1 { - // TODO(dhruvmanila): Support multi-root workspaces - anyhow::bail!("Multi-root workspaces are not supported yet"); - } - - Ok(Self { - connection, - worker_threads, - session: Session::new( - &client_capabilities, - position_encoding, - global_settings, - &workspaces, - )?, - client_capabilities, - }) - } - - pub(crate) fn run(self) -> crate::Result<()> { - // The new PanicInfoHook name requires MSRV >= 1.82 - #[allow(deprecated)] - type PanicHook = Box) + 'static + Sync + Send>; - struct RestorePanicHook { - hook: Option, - } - - impl Drop for RestorePanicHook { - fn drop(&mut self) { - if let Some(hook) = self.hook.take() { - std::panic::set_hook(hook); - } - } - } - - // unregister any previously registered panic hook - // The hook will be restored when this function exits. - let _ = RestorePanicHook { - hook: Some(std::panic::take_hook()), - }; - - // When we panic, try to notify the client. - std::panic::set_hook(Box::new(move |panic_info| { - use std::io::Write; - - let backtrace = std::backtrace::Backtrace::force_capture(); - tracing::error!("{panic_info}\n{backtrace}"); - - // we also need to print to stderr directly for when using `$logTrace` because - // the message won't be sent to the client. - // But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken. - let mut stderr = std::io::stderr().lock(); - writeln!(stderr, "{panic_info}\n{backtrace}").ok(); - - try_show_message( - "The Ruff language server exited with a panic. See the logs for more details." - .to_string(), - MessageType::ERROR, - ) - .ok(); - })); - - event_loop_thread(move || { - Self::event_loop( - &self.connection, - &self.client_capabilities, - self.session, - self.worker_threads, - )?; - self.connection.close()?; - Ok(()) - })? - .join() - } - - #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. - fn event_loop( - connection: &Connection, - _client_capabilities: &ClientCapabilities, - mut session: Session, - worker_threads: NonZeroUsize, - ) -> crate::Result<()> { - let mut scheduler = - schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender()); - - for msg in connection.incoming() { - if connection.handle_shutdown(&msg)? { - break; - } - let task = match msg { - Message::Request(req) => api::request(req), - Message::Notification(notification) => api::notification(notification), - Message::Response(response) => scheduler.response(response), - }; - scheduler.dispatch(task); - } - - Ok(()) - } - - fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding { - client_capabilities - .general - .as_ref() - .and_then(|general_capabilities| general_capabilities.position_encodings.as_ref()) - .and_then(|encodings| { - encodings - .iter() - .filter_map(|encoding| PositionEncoding::try_from(encoding).ok()) - .max() // this selects the highest priority position encoding - }) - .unwrap_or_default() - } - - fn server_capabilities(position_encoding: PositionEncoding) -> ServerCapabilities { - ServerCapabilities { - position_encoding: Some(position_encoding.into()), - diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { - identifier: Some(crate::DIAGNOSTIC_NAME.into()), - inter_file_dependencies: true, - ..Default::default() - })), - text_document_sync: Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::INCREMENTAL), - ..Default::default() - }, - )), - type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), - hover_provider: Some(HoverProviderCapability::Simple(true)), - inlay_hint_provider: Some(lsp_types::OneOf::Right( - InlayHintServerCapabilities::Options(InlayHintOptions::default()), - )), - ..Default::default() - } - } -} diff --git a/crates/red_knot_server/src/server/api.rs b/crates/red_knot_server/src/server/api.rs deleted file mode 100644 index 478666ea61d7b..0000000000000 --- a/crates/red_knot_server/src/server/api.rs +++ /dev/null @@ -1,269 +0,0 @@ -use crate::server::schedule::Task; -use crate::session::Session; -use crate::system::{url_to_any_system_path, AnySystemPath}; -use lsp_server as server; -use lsp_types::notification::Notification; - -mod diagnostics; -mod notifications; -mod requests; -mod traits; - -use notifications as notification; -use requests as request; - -use self::traits::{NotificationHandler, RequestHandler}; - -use super::{client::Responder, schedule::BackgroundSchedule, Result}; - -pub(super) fn request<'a>(req: server::Request) -> Task<'a> { - let id = req.id.clone(); - - match req.method.as_str() { - request::DocumentDiagnosticRequestHandler::METHOD => background_request_task::< - request::DocumentDiagnosticRequestHandler, - >( - req, BackgroundSchedule::Worker - ), - request::GotoTypeDefinitionRequestHandler::METHOD => background_request_task::< - request::GotoTypeDefinitionRequestHandler, - >( - req, BackgroundSchedule::Worker - ), - request::HoverRequestHandler::METHOD => { - background_request_task::(req, BackgroundSchedule::Worker) - } - request::InlayHintRequestHandler::METHOD => background_request_task::< - request::InlayHintRequestHandler, - >(req, BackgroundSchedule::Worker), - - method => { - tracing::warn!("Received request {method} which does not have a handler"); - return Task::nothing(); - } - } - .unwrap_or_else(|err| { - tracing::error!("Encountered error when routing request with ID {id}: {err}"); - show_err_msg!( - "Ruff failed to handle a request from the editor. Check the logs for more details." - ); - let result: Result<()> = Err(err); - Task::immediate(id, result) - }) -} - -pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { - match notif.method.as_str() { - notification::DidCloseTextDocumentHandler::METHOD => local_notification_task::(notif), - notification::DidOpenTextDocumentHandler::METHOD => local_notification_task::(notif), - notification::DidChangeTextDocumentHandler::METHOD => local_notification_task::(notif), - notification::DidOpenNotebookHandler::METHOD => { - local_notification_task::(notif) - } - notification::DidCloseNotebookHandler::METHOD => { - local_notification_task::(notif) - } - lsp_types::notification::SetTrace::METHOD => { - tracing::trace!("Ignoring `setTrace` notification"); - return Task::nothing(); - }, - - method => { - tracing::warn!("Received notification {method} which does not have a handler."); - return Task::nothing(); - } - } - .unwrap_or_else(|err| { - tracing::error!("Encountered error when routing notification: {err}"); - show_err_msg!("Ruff failed to handle a notification from the editor. Check the logs for more details."); - Task::nothing() - }) -} - -fn _local_request_task<'a, R: traits::SyncRequestHandler>( - req: server::Request, -) -> super::Result> { - let (id, params) = cast_request::(req)?; - Ok(Task::local(|session, notifier, requester, responder| { - let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered(); - let result = R::run(session, notifier, requester, params); - respond::(id, result, &responder); - })) -} - -// TODO(micha): Calls to `db` could panic if the db gets mutated while this task is running. -// We should either wrap `R::run_with_snapshot` with a salsa catch cancellation handler or -// use `SemanticModel` instead of passing `db` which uses a Result for all it's methods -// that propagate cancellations. -fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( - req: server::Request, - schedule: BackgroundSchedule, -) -> super::Result> { - let (id, params) = cast_request::(req)?; - Ok(Task::background(schedule, move |session: &Session| { - let url = R::document_url(¶ms).into_owned(); - - let Ok(path) = url_to_any_system_path(&url) else { - return Box::new(|_, _| {}); - }; - let db = match path { - AnySystemPath::System(path) => match session.project_db_for_path(path.as_std_path()) { - Some(db) => db.clone(), - None => session.default_project_db().clone(), - }, - AnySystemPath::SystemVirtual(_) => session.default_project_db().clone(), - }; - - let Some(snapshot) = session.take_snapshot(url) else { - return Box::new(|_, _| {}); - }; - - Box::new(move |notifier, responder| { - let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered(); - let result = R::run_with_snapshot(snapshot, db, notifier, params); - respond::(id, result, &responder); - }) - })) -} - -fn local_notification_task<'a, N: traits::SyncNotificationHandler>( - notif: server::Notification, -) -> super::Result> { - let (id, params) = cast_notification::(notif)?; - Ok(Task::local(move |session, notifier, requester, _| { - let _span = tracing::trace_span!("notification", method = N::METHOD).entered(); - if let Err(err) = N::run(session, notifier, requester, params) { - tracing::error!("An error occurred while running {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); - } - })) -} - -#[allow(dead_code)] -fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>( - req: server::Notification, - schedule: BackgroundSchedule, -) -> super::Result> { - let (id, params) = cast_notification::(req)?; - Ok(Task::background(schedule, move |session: &Session| { - // TODO(jane): we should log an error if we can't take a snapshot. - let Some(snapshot) = session.take_snapshot(N::document_url(¶ms).into_owned()) else { - return Box::new(|_, _| {}); - }; - Box::new(move |notifier, _| { - let _span = tracing::trace_span!("notification", method = N::METHOD).entered(); - if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) { - tracing::error!("An error occurred while running {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); - } - }) - })) -} - -/// Tries to cast a serialized request from the server into -/// a parameter type for a specific request handler. -/// It is *highly* recommended to not override this function in your -/// implementation. -fn cast_request( - request: server::Request, -) -> super::Result<( - server::RequestId, - <::RequestType as lsp_types::request::Request>::Params, -)> -where - Req: traits::RequestHandler, -{ - request - .extract(Req::METHOD) - .map_err(|err| match err { - json_err @ server::ExtractError::JsonError { .. } => { - anyhow::anyhow!("JSON parsing failure:\n{json_err}") - } - server::ExtractError::MethodMismatch(_) => { - unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \ - than the one whose method name was matched against earlier.") - } - }) - .with_failure_code(server::ErrorCode::InternalError) -} - -/// Sends back a response to the server using a [`Responder`]. -fn respond( - id: server::RequestId, - result: crate::server::Result< - <::RequestType as lsp_types::request::Request>::Result, - >, - responder: &Responder, -) where - Req: traits::RequestHandler, -{ - if let Err(err) = &result { - tracing::error!("An error occurred with request ID {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); - } - if let Err(err) = responder.respond(id, result) { - tracing::error!("Failed to send response: {err}"); - } -} - -/// Tries to cast a serialized request from the server into -/// a parameter type for a specific request handler. -fn cast_notification( - notification: server::Notification, -) -> super::Result< - ( - &'static str, - <::NotificationType as lsp_types::notification::Notification>::Params, -)> where N: traits::NotificationHandler{ - Ok(( - N::METHOD, - notification - .extract(N::METHOD) - .map_err(|err| match err { - json_err @ server::ExtractError::JsonError { .. } => { - anyhow::anyhow!("JSON parsing failure:\n{json_err}") - } - server::ExtractError::MethodMismatch(_) => { - unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \ - than the one whose method name was matched against earlier.") - } - }) - .with_failure_code(server::ErrorCode::InternalError)?, - )) -} - -pub(crate) struct Error { - pub(crate) code: server::ErrorCode, - pub(crate) error: anyhow::Error, -} - -/// A trait to convert result types into the server result type, [`super::Result`]. -trait LSPResult { - fn with_failure_code(self, code: server::ErrorCode) -> super::Result; -} - -impl> LSPResult for core::result::Result { - fn with_failure_code(self, code: server::ErrorCode) -> super::Result { - self.map_err(|err| Error::new(err.into(), code)) - } -} - -impl Error { - pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { - Self { code, error: err } - } -} - -// Right now, we treat the error code as invisible data that won't -// be printed. -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.error.fmt(f) - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.error.fmt(f) - } -} diff --git a/crates/red_knot_server/src/server/api/diagnostics.rs b/crates/red_knot_server/src/server/api/diagnostics.rs deleted file mode 100644 index 0a209252a888c..0000000000000 --- a/crates/red_knot_server/src/server/api/diagnostics.rs +++ /dev/null @@ -1,18 +0,0 @@ -use lsp_server::ErrorCode; -use lsp_types::{notification::PublishDiagnostics, PublishDiagnosticsParams, Url}; - -use crate::server::client::Notifier; -use crate::server::Result; - -use super::LSPResult; - -pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> { - notifier - .notify::(PublishDiagnosticsParams { - uri: uri.clone(), - diagnostics: vec![], - version: None, - }) - .with_failure_code(ErrorCode::InternalError)?; - Ok(()) -} diff --git a/crates/red_knot_server/src/server/api/notifications.rs b/crates/red_knot_server/src/server/api/notifications.rs deleted file mode 100644 index 9482c44cf8c10..0000000000000 --- a/crates/red_knot_server/src/server/api/notifications.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod did_change; -mod did_close; -mod did_close_notebook; -mod did_open; -mod did_open_notebook; - -pub(super) use did_change::DidChangeTextDocumentHandler; -pub(super) use did_close::DidCloseTextDocumentHandler; -pub(super) use did_close_notebook::DidCloseNotebookHandler; -pub(super) use did_open::DidOpenTextDocumentHandler; -pub(super) use did_open_notebook::DidOpenNotebookHandler; diff --git a/crates/red_knot_server/src/server/api/notifications/did_change.rs b/crates/red_knot_server/src/server/api/notifications/did_change.rs deleted file mode 100644 index 93e23f01e17aa..0000000000000 --- a/crates/red_knot_server/src/server/api/notifications/did_change.rs +++ /dev/null @@ -1,55 +0,0 @@ -use lsp_server::ErrorCode; -use lsp_types::notification::DidChangeTextDocument; -use lsp_types::DidChangeTextDocumentParams; - -use red_knot_project::watch::ChangeEvent; - -use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; -use crate::server::Result; -use crate::session::Session; -use crate::system::{url_to_any_system_path, AnySystemPath}; - -pub(crate) struct DidChangeTextDocumentHandler; - -impl NotificationHandler for DidChangeTextDocumentHandler { - type NotificationType = DidChangeTextDocument; -} - -impl SyncNotificationHandler for DidChangeTextDocumentHandler { - fn run( - session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, - params: DidChangeTextDocumentParams, - ) -> Result<()> { - let Ok(path) = url_to_any_system_path(¶ms.text_document.uri) else { - return Ok(()); - }; - - let key = session.key_from_url(params.text_document.uri); - - session - .update_text_document(&key, params.content_changes, params.text_document.version) - .with_failure_code(ErrorCode::InternalError)?; - - match path { - AnySystemPath::System(path) => { - let db = match session.project_db_for_path_mut(path.as_std_path()) { - Some(db) => db, - None => session.default_project_db_mut(), - }; - db.apply_changes(vec![ChangeEvent::file_content_changed(path)], None); - } - AnySystemPath::SystemVirtual(virtual_path) => { - let db = session.default_project_db_mut(); - db.apply_changes(vec![ChangeEvent::ChangedVirtual(virtual_path)], None); - } - } - - // TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics - - Ok(()) - } -} diff --git a/crates/red_knot_server/src/server/api/notifications/did_close.rs b/crates/red_knot_server/src/server/api/notifications/did_close.rs deleted file mode 100644 index 9aea65ccd475d..0000000000000 --- a/crates/red_knot_server/src/server/api/notifications/did_close.rs +++ /dev/null @@ -1,45 +0,0 @@ -use lsp_server::ErrorCode; -use lsp_types::notification::DidCloseTextDocument; -use lsp_types::DidCloseTextDocumentParams; -use red_knot_project::watch::ChangeEvent; - -use crate::server::api::diagnostics::clear_diagnostics; -use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; -use crate::server::Result; -use crate::session::Session; -use crate::system::{url_to_any_system_path, AnySystemPath}; - -pub(crate) struct DidCloseTextDocumentHandler; - -impl NotificationHandler for DidCloseTextDocumentHandler { - type NotificationType = DidCloseTextDocument; -} - -impl SyncNotificationHandler for DidCloseTextDocumentHandler { - fn run( - session: &mut Session, - notifier: Notifier, - _requester: &mut Requester, - params: DidCloseTextDocumentParams, - ) -> Result<()> { - let Ok(path) = url_to_any_system_path(¶ms.text_document.uri) else { - return Ok(()); - }; - - let key = session.key_from_url(params.text_document.uri); - session - .close_document(&key) - .with_failure_code(ErrorCode::InternalError)?; - - if let AnySystemPath::SystemVirtual(virtual_path) = path { - let db = session.default_project_db_mut(); - db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None); - } - - clear_diagnostics(key.url(), ¬ifier)?; - - Ok(()) - } -} diff --git a/crates/red_knot_server/src/server/api/notifications/did_close_notebook.rs b/crates/red_knot_server/src/server/api/notifications/did_close_notebook.rs deleted file mode 100644 index 240d7beebbce6..0000000000000 --- a/crates/red_knot_server/src/server/api/notifications/did_close_notebook.rs +++ /dev/null @@ -1,42 +0,0 @@ -use lsp_types::notification::DidCloseNotebookDocument; -use lsp_types::DidCloseNotebookDocumentParams; - -use red_knot_project::watch::ChangeEvent; - -use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; -use crate::server::Result; -use crate::session::Session; -use crate::system::{url_to_any_system_path, AnySystemPath}; - -pub(crate) struct DidCloseNotebookHandler; - -impl NotificationHandler for DidCloseNotebookHandler { - type NotificationType = DidCloseNotebookDocument; -} - -impl SyncNotificationHandler for DidCloseNotebookHandler { - fn run( - session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, - params: DidCloseNotebookDocumentParams, - ) -> Result<()> { - let Ok(path) = url_to_any_system_path(¶ms.notebook_document.uri) else { - return Ok(()); - }; - - let key = session.key_from_url(params.notebook_document.uri); - session - .close_document(&key) - .with_failure_code(lsp_server::ErrorCode::InternalError)?; - - if let AnySystemPath::SystemVirtual(virtual_path) = path { - let db = session.default_project_db_mut(); - db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None); - } - - Ok(()) - } -} diff --git a/crates/red_knot_server/src/server/api/notifications/did_open.rs b/crates/red_knot_server/src/server/api/notifications/did_open.rs deleted file mode 100644 index 2530640bf6231..0000000000000 --- a/crates/red_knot_server/src/server/api/notifications/did_open.rs +++ /dev/null @@ -1,60 +0,0 @@ -use lsp_types::notification::DidOpenTextDocument; -use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem}; - -use red_knot_project::watch::ChangeEvent; -use ruff_db::Db; - -use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; -use crate::server::client::{Notifier, Requester}; -use crate::server::Result; -use crate::session::Session; -use crate::system::{url_to_any_system_path, AnySystemPath}; -use crate::TextDocument; - -pub(crate) struct DidOpenTextDocumentHandler; - -impl NotificationHandler for DidOpenTextDocumentHandler { - type NotificationType = DidOpenTextDocument; -} - -impl SyncNotificationHandler for DidOpenTextDocumentHandler { - fn run( - session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, - DidOpenTextDocumentParams { - text_document: - TextDocumentItem { - uri, - text, - version, - language_id, - }, - }: DidOpenTextDocumentParams, - ) -> Result<()> { - let Ok(path) = url_to_any_system_path(&uri) else { - return Ok(()); - }; - - let document = TextDocument::new(text, version).with_language_id(&language_id); - session.open_text_document(uri, document); - - match path { - AnySystemPath::System(path) => { - let db = match session.project_db_for_path_mut(path.as_std_path()) { - Some(db) => db, - None => session.default_project_db_mut(), - }; - db.apply_changes(vec![ChangeEvent::Opened(path)], None); - } - AnySystemPath::SystemVirtual(virtual_path) => { - let db = session.default_project_db_mut(); - db.files().virtual_file(db, &virtual_path); - } - } - - // TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics - - Ok(()) - } -} diff --git a/crates/red_knot_server/src/server/api/notifications/did_open_notebook.rs b/crates/red_knot_server/src/server/api/notifications/did_open_notebook.rs deleted file mode 100644 index ea355e7e0fc8b..0000000000000 --- a/crates/red_knot_server/src/server/api/notifications/did_open_notebook.rs +++ /dev/null @@ -1,60 +0,0 @@ -use lsp_server::ErrorCode; -use lsp_types::notification::DidOpenNotebookDocument; -use lsp_types::DidOpenNotebookDocumentParams; - -use red_knot_project::watch::ChangeEvent; -use ruff_db::Db; - -use crate::document::NotebookDocument; -use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; -use crate::server::Result; -use crate::session::Session; -use crate::system::{url_to_any_system_path, AnySystemPath}; - -pub(crate) struct DidOpenNotebookHandler; - -impl NotificationHandler for DidOpenNotebookHandler { - type NotificationType = DidOpenNotebookDocument; -} - -impl SyncNotificationHandler for DidOpenNotebookHandler { - fn run( - session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, - params: DidOpenNotebookDocumentParams, - ) -> Result<()> { - let Ok(path) = url_to_any_system_path(¶ms.notebook_document.uri) else { - return Ok(()); - }; - - let notebook = NotebookDocument::new( - params.notebook_document.version, - params.notebook_document.cells, - params.notebook_document.metadata.unwrap_or_default(), - params.cell_text_documents, - ) - .with_failure_code(ErrorCode::InternalError)?; - session.open_notebook_document(params.notebook_document.uri, notebook); - - match path { - AnySystemPath::System(path) => { - let db = match session.project_db_for_path_mut(path.as_std_path()) { - Some(db) => db, - None => session.default_project_db_mut(), - }; - db.apply_changes(vec![ChangeEvent::Opened(path)], None); - } - AnySystemPath::SystemVirtual(virtual_path) => { - let db = session.default_project_db_mut(); - db.files().virtual_file(db, &virtual_path); - } - } - - // TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics - - Ok(()) - } -} diff --git a/crates/red_knot_server/src/server/api/requests.rs b/crates/red_knot_server/src/server/api/requests.rs deleted file mode 100644 index b6e907aa0c405..0000000000000 --- a/crates/red_knot_server/src/server/api/requests.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod diagnostic; -mod goto_type_definition; -mod hover; -mod inlay_hints; - -pub(super) use diagnostic::DocumentDiagnosticRequestHandler; -pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; -pub(super) use hover::HoverRequestHandler; -pub(super) use inlay_hints::InlayHintRequestHandler; diff --git a/crates/red_knot_server/src/server/api/requests/diagnostic.rs b/crates/red_knot_server/src/server/api/requests/diagnostic.rs deleted file mode 100644 index 8b3d03d34b2dc..0000000000000 --- a/crates/red_knot_server/src/server/api/requests/diagnostic.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::borrow::Cow; - -use lsp_types::request::DocumentDiagnosticRequest; -use lsp_types::{ - Diagnostic, DiagnosticSeverity, DocumentDiagnosticParams, DocumentDiagnosticReport, - DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, NumberOrString, Range, - RelatedFullDocumentDiagnosticReport, Url, -}; - -use crate::document::ToRangeExt; -use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; -use crate::server::{client::Notifier, Result}; -use crate::session::DocumentSnapshot; -use red_knot_project::{Db, ProjectDatabase}; -use ruff_db::diagnostic::Severity; -use ruff_db::source::{line_index, source_text}; - -pub(crate) struct DocumentDiagnosticRequestHandler; - -impl RequestHandler for DocumentDiagnosticRequestHandler { - type RequestType = DocumentDiagnosticRequest; -} - -impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler { - fn document_url(params: &DocumentDiagnosticParams) -> Cow { - Cow::Borrowed(¶ms.text_document.uri) - } - - fn run_with_snapshot( - snapshot: DocumentSnapshot, - db: ProjectDatabase, - _notifier: Notifier, - _params: DocumentDiagnosticParams, - ) -> Result { - let diagnostics = compute_diagnostics(&snapshot, &db); - - Ok(DocumentDiagnosticReportResult::Report( - DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { - related_documents: None, - full_document_diagnostic_report: FullDocumentDiagnosticReport { - result_id: None, - items: diagnostics, - }, - }), - )) - } -} - -fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &ProjectDatabase) -> Vec { - let Some(file) = snapshot.file(db) else { - tracing::info!( - "No file found for snapshot for `{}`", - snapshot.query().file_url() - ); - return vec![]; - }; - - let diagnostics = match db.check_file(file) { - Ok(diagnostics) => diagnostics, - Err(cancelled) => { - tracing::info!("Diagnostics computation {cancelled}"); - return vec![]; - } - }; - - diagnostics - .as_slice() - .iter() - .map(|message| to_lsp_diagnostic(db, message, snapshot.encoding())) - .collect() -} - -fn to_lsp_diagnostic( - db: &dyn Db, - diagnostic: &ruff_db::diagnostic::Diagnostic, - encoding: crate::PositionEncoding, -) -> Diagnostic { - let range = if let Some(span) = diagnostic.primary_span() { - let index = line_index(db.upcast(), span.file()); - let source = source_text(db.upcast(), span.file()); - - span.range() - .map(|range| range.to_lsp_range(&source, &index, encoding)) - .unwrap_or_default() - } else { - Range::default() - }; - - let severity = match diagnostic.severity() { - Severity::Info => DiagnosticSeverity::INFORMATION, - Severity::Warning => DiagnosticSeverity::WARNING, - Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR, - }; - - Diagnostic { - range, - severity: Some(severity), - tags: None, - code: Some(NumberOrString::String(diagnostic.id().to_string())), - code_description: None, - source: Some("red-knot".into()), - message: diagnostic.concise_message().to_string(), - related_information: None, - data: None, - } -} diff --git a/crates/red_knot_server/src/server/api/requests/goto_type_definition.rs b/crates/red_knot_server/src/server/api/requests/goto_type_definition.rs deleted file mode 100644 index bb3a4e6e5804d..0000000000000 --- a/crates/red_knot_server/src/server/api/requests/goto_type_definition.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::borrow::Cow; - -use lsp_types::request::{GotoTypeDefinition, GotoTypeDefinitionParams}; -use lsp_types::{GotoDefinitionResponse, Url}; -use red_knot_ide::goto_type_definition; -use red_knot_project::ProjectDatabase; -use ruff_db::source::{line_index, source_text}; - -use crate::document::{PositionExt, ToLink}; -use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; -use crate::server::client::Notifier; -use crate::DocumentSnapshot; - -pub(crate) struct GotoTypeDefinitionRequestHandler; - -impl RequestHandler for GotoTypeDefinitionRequestHandler { - type RequestType = GotoTypeDefinition; -} - -impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { - fn document_url(params: &GotoTypeDefinitionParams) -> Cow { - Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) - } - - fn run_with_snapshot( - snapshot: DocumentSnapshot, - db: ProjectDatabase, - _notifier: Notifier, - params: GotoTypeDefinitionParams, - ) -> crate::server::Result> { - let Some(file) = snapshot.file(&db) else { - tracing::debug!("Failed to resolve file for {:?}", params); - return Ok(None); - }; - - let source = source_text(&db, file); - let line_index = line_index(&db, file); - let offset = params.text_document_position_params.position.to_text_size( - &source, - &line_index, - snapshot.encoding(), - ); - - let Some(ranged) = goto_type_definition(&db, file, offset) else { - return Ok(None); - }; - - if snapshot - .resolved_client_capabilities() - .type_definition_link_support - { - let src = Some(ranged.range); - let links: Vec<_> = ranged - .into_iter() - .filter_map(|target| target.to_link(&db, src, snapshot.encoding())) - .collect(); - - Ok(Some(GotoDefinitionResponse::Link(links))) - } else { - let locations: Vec<_> = ranged - .into_iter() - .filter_map(|target| target.to_location(&db, snapshot.encoding())) - .collect(); - - Ok(Some(GotoDefinitionResponse::Array(locations))) - } - } -} diff --git a/crates/red_knot_server/src/server/api/requests/hover.rs b/crates/red_knot_server/src/server/api/requests/hover.rs deleted file mode 100644 index 2677f67c3231f..0000000000000 --- a/crates/red_knot_server/src/server/api/requests/hover.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::borrow::Cow; - -use crate::document::{PositionExt, ToRangeExt}; -use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; -use crate::server::client::Notifier; -use crate::DocumentSnapshot; -use lsp_types::request::HoverRequest; -use lsp_types::{HoverContents, HoverParams, MarkupContent, Url}; -use red_knot_ide::{hover, MarkupKind}; -use red_knot_project::ProjectDatabase; -use ruff_db::source::{line_index, source_text}; -use ruff_text_size::Ranged; - -pub(crate) struct HoverRequestHandler; - -impl RequestHandler for HoverRequestHandler { - type RequestType = HoverRequest; -} - -impl BackgroundDocumentRequestHandler for HoverRequestHandler { - fn document_url(params: &HoverParams) -> Cow { - Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) - } - - fn run_with_snapshot( - snapshot: DocumentSnapshot, - db: ProjectDatabase, - _notifier: Notifier, - params: HoverParams, - ) -> crate::server::Result> { - let Some(file) = snapshot.file(&db) else { - tracing::debug!("Failed to resolve file for {:?}", params); - return Ok(None); - }; - - let source = source_text(&db, file); - let line_index = line_index(&db, file); - let offset = params.text_document_position_params.position.to_text_size( - &source, - &line_index, - snapshot.encoding(), - ); - - let Some(range_info) = hover(&db, file, offset) else { - return Ok(None); - }; - - let (markup_kind, lsp_markup_kind) = if snapshot - .resolved_client_capabilities() - .hover_prefer_markdown - { - (MarkupKind::Markdown, lsp_types::MarkupKind::Markdown) - } else { - (MarkupKind::PlainText, lsp_types::MarkupKind::PlainText) - }; - - let contents = range_info.display(&db, markup_kind).to_string(); - - Ok(Some(lsp_types::Hover { - contents: HoverContents::Markup(MarkupContent { - kind: lsp_markup_kind, - value: contents, - }), - range: Some(range_info.file_range().range().to_lsp_range( - &source, - &line_index, - snapshot.encoding(), - )), - })) - } -} diff --git a/crates/red_knot_server/src/server/api/requests/inlay_hints.rs b/crates/red_knot_server/src/server/api/requests/inlay_hints.rs deleted file mode 100644 index f299fdf9757a3..0000000000000 --- a/crates/red_knot_server/src/server/api/requests/inlay_hints.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::borrow::Cow; - -use crate::document::{RangeExt, TextSizeExt}; -use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; -use crate::server::client::Notifier; -use crate::DocumentSnapshot; -use lsp_types::request::InlayHintRequest; -use lsp_types::{InlayHintParams, Url}; -use red_knot_ide::inlay_hints; -use red_knot_project::ProjectDatabase; -use ruff_db::source::{line_index, source_text}; - -pub(crate) struct InlayHintRequestHandler; - -impl RequestHandler for InlayHintRequestHandler { - type RequestType = InlayHintRequest; -} - -impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { - fn document_url(params: &InlayHintParams) -> Cow { - Cow::Borrowed(¶ms.text_document.uri) - } - - fn run_with_snapshot( - snapshot: DocumentSnapshot, - db: ProjectDatabase, - _notifier: Notifier, - params: InlayHintParams, - ) -> crate::server::Result>> { - let Some(file) = snapshot.file(&db) else { - tracing::debug!("Failed to resolve file for {:?}", params); - return Ok(None); - }; - - let index = line_index(&db, file); - let source = source_text(&db, file); - - let range = params - .range - .to_text_range(&source, &index, snapshot.encoding()); - - let inlay_hints = inlay_hints(&db, file, range); - - let inlay_hints = inlay_hints - .into_iter() - .map(|hint| lsp_types::InlayHint { - position: hint - .position - .to_position(&source, &index, snapshot.encoding()), - label: lsp_types::InlayHintLabel::String(hint.display(&db).to_string()), - kind: Some(lsp_types::InlayHintKind::TYPE), - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - text_edits: None, - }) - .collect(); - - Ok(Some(inlay_hints)) - } -} diff --git a/crates/red_knot_server/src/server/api/traits.rs b/crates/red_knot_server/src/server/api/traits.rs deleted file mode 100644 index e5c9a609078c5..0000000000000 --- a/crates/red_knot_server/src/server/api/traits.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! A stateful LSP implementation that calls into the Ruff API. - -use crate::server::client::{Notifier, Requester}; -use crate::session::{DocumentSnapshot, Session}; - -use lsp_types::notification::Notification as LSPNotification; -use lsp_types::request::Request; -use red_knot_project::ProjectDatabase; - -/// A supertrait for any server request handler. -pub(super) trait RequestHandler { - type RequestType: Request; - const METHOD: &'static str = <::RequestType as Request>::METHOD; -} - -/// A request handler that needs mutable access to the session. -/// This will block the main message receiver loop, meaning that no -/// incoming requests or notifications will be handled while `run` is -/// executing. Try to avoid doing any I/O or long-running computations. -#[expect(dead_code)] -pub(super) trait SyncRequestHandler: RequestHandler { - fn run( - session: &mut Session, - notifier: Notifier, - requester: &mut Requester, - params: <::RequestType as Request>::Params, - ) -> super::Result<<::RequestType as Request>::Result>; -} - -/// A request handler that can be run on a background thread. -pub(super) trait BackgroundDocumentRequestHandler: RequestHandler { - fn document_url( - params: &<::RequestType as Request>::Params, - ) -> std::borrow::Cow; - - fn run_with_snapshot( - snapshot: DocumentSnapshot, - db: ProjectDatabase, - notifier: Notifier, - params: <::RequestType as Request>::Params, - ) -> super::Result<<::RequestType as Request>::Result>; -} - -/// A supertrait for any server notification handler. -pub(super) trait NotificationHandler { - type NotificationType: LSPNotification; - const METHOD: &'static str = - <::NotificationType as LSPNotification>::METHOD; -} - -/// A notification handler that needs mutable access to the session. -/// This will block the main message receiver loop, meaning that no -/// incoming requests or notifications will be handled while `run` is -/// executing. Try to avoid doing any I/O or long-running computations. -pub(super) trait SyncNotificationHandler: NotificationHandler { - fn run( - session: &mut Session, - notifier: Notifier, - requester: &mut Requester, - params: <::NotificationType as LSPNotification>::Params, - ) -> super::Result<()>; -} - -/// A notification handler that can be run on a background thread. -pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler { - /// `document_url` can be implemented automatically with - /// `define_document_url!(params: &)` in the trait - /// implementation. - fn document_url( - params: &<::NotificationType as LSPNotification>::Params, - ) -> std::borrow::Cow; - - fn run_with_snapshot( - snapshot: DocumentSnapshot, - notifier: Notifier, - params: <::NotificationType as LSPNotification>::Params, - ) -> super::Result<()>; -} diff --git a/crates/red_knot_server/src/server/client.rs b/crates/red_knot_server/src/server/client.rs deleted file mode 100644 index c5a502e213c5e..0000000000000 --- a/crates/red_knot_server/src/server/client.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::any::TypeId; - -use lsp_server::{Notification, RequestId}; -use rustc_hash::FxHashMap; -use serde_json::Value; - -use super::{schedule::Task, ClientSender}; - -type ResponseBuilder<'s> = Box Task<'s>>; - -pub(crate) struct Client<'s> { - notifier: Notifier, - responder: Responder, - pub(super) requester: Requester<'s>, -} - -#[derive(Clone)] -pub(crate) struct Notifier(ClientSender); - -#[derive(Clone)] -pub(crate) struct Responder(ClientSender); - -pub(crate) struct Requester<'s> { - sender: ClientSender, - next_request_id: i32, - response_handlers: FxHashMap>, -} - -impl Client<'_> { - pub(super) fn new(sender: ClientSender) -> Self { - Self { - notifier: Notifier(sender.clone()), - responder: Responder(sender.clone()), - requester: Requester { - sender, - next_request_id: 1, - response_handlers: FxHashMap::default(), - }, - } - } - - pub(super) fn notifier(&self) -> Notifier { - self.notifier.clone() - } - - pub(super) fn responder(&self) -> Responder { - self.responder.clone() - } -} - -#[allow(dead_code)] // we'll need to use `Notifier` in the future -impl Notifier { - pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> - where - N: lsp_types::notification::Notification, - { - let method = N::METHOD.to_string(); - - let message = lsp_server::Message::Notification(Notification::new(method, params)); - - self.0.send(message) - } - - pub(crate) fn notify_method(&self, method: String) -> crate::Result<()> { - self.0 - .send(lsp_server::Message::Notification(Notification::new( - method, - Value::Null, - ))) - } -} - -impl Responder { - pub(crate) fn respond( - &self, - id: RequestId, - result: crate::server::Result, - ) -> crate::Result<()> - where - R: serde::Serialize, - { - self.0.send( - match result { - Ok(res) => lsp_server::Response::new_ok(id, res), - Err(crate::server::api::Error { code, error }) => { - lsp_server::Response::new_err(id, code as i32, format!("{error}")) - } - } - .into(), - ) - } -} - -impl<'s> Requester<'s> { - /// Sends a request of kind `R` to the client, with associated parameters. - /// The task provided by `response_handler` will be dispatched as soon as the response - /// comes back from the client. - pub(crate) fn request( - &mut self, - params: R::Params, - response_handler: impl Fn(R::Result) -> Task<'s> + 'static, - ) -> crate::Result<()> - where - R: lsp_types::request::Request, - { - let serialized_params = serde_json::to_value(params)?; - - self.response_handlers.insert( - self.next_request_id.into(), - Box::new(move |response: lsp_server::Response| { - match (response.error, response.result) { - (Some(err), _) => { - tracing::error!( - "Got an error from the client (code {}): {}", - err.code, - err.message - ); - Task::nothing() - } - (None, Some(response)) => match serde_json::from_value(response) { - Ok(response) => response_handler(response), - Err(error) => { - tracing::error!("Failed to deserialize response from server: {error}"); - Task::nothing() - } - }, - (None, None) => { - if TypeId::of::() == TypeId::of::<()>() { - // We can't call `response_handler(())` directly here, but - // since we _know_ the type expected is `()`, we can use - // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, - // so this branch works in the general case but we'll only - // hit it if the concrete type is `()`, so the `unwrap()` is safe here. - response_handler(serde_json::from_value(Value::Null).unwrap()); - } else { - tracing::error!( - "Server response was invalid: did not contain a result or error" - ); - } - Task::nothing() - } - } - }), - ); - - self.sender - .send(lsp_server::Message::Request(lsp_server::Request { - id: self.next_request_id.into(), - method: R::METHOD.into(), - params: serialized_params, - }))?; - - self.next_request_id += 1; - - Ok(()) - } - - pub(crate) fn pop_response_task(&mut self, response: lsp_server::Response) -> Task<'s> { - if let Some(handler) = self.response_handlers.remove(&response.id) { - handler(response) - } else { - tracing::error!( - "Received a response with ID {}, which was not expected", - response.id - ); - Task::nothing() - } - } -} diff --git a/crates/red_knot_server/src/server/connection.rs b/crates/red_knot_server/src/server/connection.rs deleted file mode 100644 index 5993023be8303..0000000000000 --- a/crates/red_knot_server/src/server/connection.rs +++ /dev/null @@ -1,165 +0,0 @@ -use lsp_server as lsp; -use lsp_types::{notification::Notification, request::Request}; -use std::sync::{Arc, Weak}; - -type ConnectionSender = crossbeam::channel::Sender; -type ConnectionReceiver = crossbeam::channel::Receiver; - -/// A builder for `Connection` that handles LSP initialization. -pub(crate) struct ConnectionInitializer { - connection: lsp::Connection, - threads: lsp::IoThreads, -} - -/// Handles inbound and outbound messages with the client. -pub(crate) struct Connection { - sender: Arc, - receiver: ConnectionReceiver, - threads: lsp::IoThreads, -} - -impl ConnectionInitializer { - /// Create a new LSP server connection over stdin/stdout. - pub(super) fn stdio() -> Self { - let (connection, threads) = lsp::Connection::stdio(); - Self { - connection, - threads, - } - } - - /// Starts the initialization process with the client by listening for an initialization request. - /// Returns a request ID that should be passed into `initialize_finish` later, - /// along with the initialization parameters that were provided. - pub(super) fn initialize_start( - &self, - ) -> crate::Result<(lsp::RequestId, lsp_types::InitializeParams)> { - let (id, params) = self.connection.initialize_start()?; - Ok((id, serde_json::from_value(params)?)) - } - - /// Finishes the initialization process with the client, - /// returning an initialized `Connection`. - pub(super) fn initialize_finish( - self, - id: lsp::RequestId, - server_capabilities: &lsp_types::ServerCapabilities, - name: &str, - version: &str, - ) -> crate::Result { - self.connection.initialize_finish( - id, - serde_json::json!({ - "capabilities": server_capabilities, - "serverInfo": { - "name": name, - "version": version - } - }), - )?; - let Self { - connection: lsp::Connection { sender, receiver }, - threads, - } = self; - Ok(Connection { - sender: Arc::new(sender), - receiver, - threads, - }) - } -} - -impl Connection { - /// Make a new `ClientSender` for sending messages to the client. - pub(super) fn make_sender(&self) -> ClientSender { - ClientSender { - weak_sender: Arc::downgrade(&self.sender), - } - } - - /// An iterator over incoming messages from the client. - pub(super) fn incoming(&self) -> crossbeam::channel::Iter { - self.receiver.iter() - } - - /// Check and respond to any incoming shutdown requests; returns`true` if the server should be shutdown. - pub(super) fn handle_shutdown(&self, message: &lsp::Message) -> crate::Result { - match message { - lsp::Message::Request(lsp::Request { id, method, .. }) - if method == lsp_types::request::Shutdown::METHOD => - { - self.sender - .send(lsp::Response::new_ok(id.clone(), ()).into())?; - tracing::info!("Shutdown request received. Waiting for an exit notification..."); - - loop { - match &self - .receiver - .recv_timeout(std::time::Duration::from_secs(30))? - { - lsp::Message::Notification(lsp::Notification { method, .. }) - if method == lsp_types::notification::Exit::METHOD => - { - tracing::info!("Exit notification received. Server shutting down..."); - return Ok(true); - } - lsp::Message::Request(lsp::Request { id, method, .. }) => { - tracing::warn!( - "Server received unexpected request {method} ({id}) while waiting for exit notification", - ); - self.sender.send(lsp::Message::Response(lsp::Response::new_err( - id.clone(), - lsp::ErrorCode::InvalidRequest as i32, - "Server received unexpected request while waiting for exit notification".to_string(), - )))?; - } - message => { - tracing::warn!( - "Server received unexpected message while waiting for exit notification: {message:?}" - ); - } - } - } - } - lsp::Message::Notification(lsp::Notification { method, .. }) - if method == lsp_types::notification::Exit::METHOD => - { - anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting..."); - } - _ => Ok(false), - } - } - - /// Join the I/O threads that underpin this connection. - /// This is guaranteed to be nearly immediate since - /// we close the only active channels to these threads prior - /// to joining them. - pub(super) fn close(self) -> crate::Result<()> { - std::mem::drop( - Arc::into_inner(self.sender) - .expect("the client sender shouldn't have more than one strong reference"), - ); - std::mem::drop(self.receiver); - self.threads.join()?; - Ok(()) - } -} - -/// A weak reference to an underlying sender channel, used for communication with the client. -/// If the `Connection` that created this `ClientSender` is dropped, any `send` calls will throw -/// an error. -#[derive(Clone, Debug)] -pub(crate) struct ClientSender { - weak_sender: Weak, -} - -// note: additional wrapper functions for senders may be implemented as needed. -impl ClientSender { - pub(crate) fn send(&self, msg: lsp::Message) -> crate::Result<()> { - let Some(sender) = self.weak_sender.upgrade() else { - anyhow::bail!("The connection with the client has been closed"); - }; - - Ok(sender.send(msg)?) - } -} diff --git a/crates/red_knot_server/src/server/schedule.rs b/crates/red_knot_server/src/server/schedule.rs deleted file mode 100644 index 942f59ad10073..0000000000000 --- a/crates/red_knot_server/src/server/schedule.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::num::NonZeroUsize; - -use crate::session::Session; - -mod task; -mod thread; - -pub(super) use task::{BackgroundSchedule, Task}; - -use self::{ - task::{BackgroundTaskBuilder, SyncTask}, - thread::ThreadPriority, -}; - -use super::{client::Client, ClientSender}; - -/// The event loop thread is actually a secondary thread that we spawn from the -/// _actual_ main thread. This secondary thread has a larger stack size -/// than some OS defaults (Windows, for example) and is also designated as -/// high-priority. -pub(crate) fn event_loop_thread( - func: impl FnOnce() -> crate::Result<()> + Send + 'static, -) -> crate::Result>> { - // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. - const MAIN_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024; - const MAIN_THREAD_NAME: &str = "ruff:main"; - Ok( - thread::Builder::new(thread::ThreadPriority::LatencySensitive) - .name(MAIN_THREAD_NAME.into()) - .stack_size(MAIN_THREAD_STACK_SIZE) - .spawn(func)?, - ) -} - -pub(crate) struct Scheduler<'s> { - session: &'s mut Session, - client: Client<'s>, - fmt_pool: thread::Pool, - background_pool: thread::Pool, -} - -impl<'s> Scheduler<'s> { - pub(super) fn new( - session: &'s mut Session, - worker_threads: NonZeroUsize, - sender: ClientSender, - ) -> Self { - const FMT_THREADS: usize = 1; - Self { - session, - fmt_pool: thread::Pool::new(NonZeroUsize::try_from(FMT_THREADS).unwrap()), - background_pool: thread::Pool::new(worker_threads), - client: Client::new(sender), - } - } - - /// Immediately sends a request of kind `R` to the client, with associated parameters. - /// The task provided by `response_handler` will be dispatched as soon as the response - /// comes back from the client. - #[expect(dead_code)] - pub(super) fn request( - &mut self, - params: R::Params, - response_handler: impl Fn(R::Result) -> Task<'s> + 'static, - ) -> crate::Result<()> - where - R: lsp_types::request::Request, - { - self.client.requester.request::(params, response_handler) - } - - /// Creates a task to handle a response from the client. - pub(super) fn response(&mut self, response: lsp_server::Response) -> Task<'s> { - self.client.requester.pop_response_task(response) - } - - /// Dispatches a `task` by either running it as a blocking function or - /// executing it on a background thread pool. - pub(super) fn dispatch(&mut self, task: task::Task<'s>) { - match task { - Task::Sync(SyncTask { func }) => { - let notifier = self.client.notifier(); - let responder = self.client.responder(); - func( - self.session, - notifier, - &mut self.client.requester, - responder, - ); - } - Task::Background(BackgroundTaskBuilder { - schedule, - builder: func, - }) => { - let static_func = func(self.session); - let notifier = self.client.notifier(); - let responder = self.client.responder(); - let task = move || static_func(notifier, responder); - match schedule { - BackgroundSchedule::Worker => { - self.background_pool.spawn(ThreadPriority::Worker, task); - } - BackgroundSchedule::LatencySensitive => self - .background_pool - .spawn(ThreadPriority::LatencySensitive, task), - BackgroundSchedule::Fmt => { - self.fmt_pool.spawn(ThreadPriority::LatencySensitive, task); - } - } - } - } - } -} diff --git a/crates/red_knot_server/src/server/schedule/task.rs b/crates/red_knot_server/src/server/schedule/task.rs deleted file mode 100644 index b9ae6f4a59718..0000000000000 --- a/crates/red_knot_server/src/server/schedule/task.rs +++ /dev/null @@ -1,98 +0,0 @@ -use lsp_server::RequestId; -use serde::Serialize; - -use crate::{ - server::client::{Notifier, Requester, Responder}, - session::Session, -}; - -type LocalFn<'s> = Box; - -type BackgroundFn = Box; - -type BackgroundFnBuilder<'s> = Box BackgroundFn + 's>; - -/// Describes how the task should be run. -#[derive(Clone, Copy, Debug, Default)] -pub(in crate::server) enum BackgroundSchedule { - /// The task should be run on the background thread designated - /// for formatting actions. This is a high priority thread. - #[expect(dead_code)] - Fmt, - /// The task should be run on the general high-priority background - /// thread. Reserved for actions caused by the user typing (e.g.syntax highlighting). - #[expect(dead_code)] - LatencySensitive, - /// The task should be run on a regular-priority background thread. - /// The default for any request that isn't in the critical path of the user typing. - #[default] - Worker, -} - -/// A [`Task`] is a future that has not yet started, and it is the job of -/// the [`super::Scheduler`] to make that happen, via [`super::Scheduler::dispatch`]. -/// A task can either run on the main thread (in other words, the same thread as the -/// scheduler) or it can run in a background thread. The main difference between -/// the two is that background threads only have a read-only snapshot of the session, -/// while local tasks have exclusive access and can modify it as they please. Keep in mind that -/// local tasks will **block** the main event loop, so only use local tasks if you **need** -/// mutable state access or you need the absolute lowest latency possible. -pub(in crate::server) enum Task<'s> { - Background(BackgroundTaskBuilder<'s>), - Sync(SyncTask<'s>), -} - -// The reason why this isn't just a 'static background closure -// is because we need to take a snapshot of the session before sending -// this task to the background, and the inner closure can't take the session -// as an immutable reference since it's used mutably elsewhere. So instead, -// a background task is built using an outer closure that borrows the session to take a snapshot, -// that the inner closure can capture. This builder closure has a lifetime linked to the scheduler. -// When the task is dispatched, the scheduler runs the synchronous builder, which takes the session -// as a reference, to create the inner 'static closure. That closure is then moved to a background task pool. -pub(in crate::server) struct BackgroundTaskBuilder<'s> { - pub(super) schedule: BackgroundSchedule, - pub(super) builder: BackgroundFnBuilder<'s>, -} - -pub(in crate::server) struct SyncTask<'s> { - pub(super) func: LocalFn<'s>, -} - -impl<'s> Task<'s> { - /// Creates a new background task. - pub(crate) fn background( - schedule: BackgroundSchedule, - func: impl FnOnce(&Session) -> Box + 's, - ) -> Self { - Self::Background(BackgroundTaskBuilder { - schedule, - builder: Box::new(func), - }) - } - /// Creates a new local task. - pub(crate) fn local( - func: impl FnOnce(&mut Session, Notifier, &mut Requester, Responder) + 's, - ) -> Self { - Self::Sync(SyncTask { - func: Box::new(func), - }) - } - /// Creates a local task that immediately - /// responds with the provided `request`. - pub(crate) fn immediate(id: RequestId, result: crate::server::Result) -> Self - where - R: Serialize + Send + 'static, - { - Self::local(move |_, _, _, responder| { - if let Err(err) = responder.respond(id, result) { - tracing::error!("Unable to send immediate response: {err}"); - } - }) - } - - /// Creates a local task that does nothing. - pub(crate) fn nothing() -> Self { - Self::local(move |_, _, _, _| {}) - } -} diff --git a/crates/red_knot_server/src/server/schedule/thread/pool.rs b/crates/red_knot_server/src/server/schedule/thread/pool.rs deleted file mode 100644 index ea654a11d2af4..0000000000000 --- a/crates/red_knot_server/src/server/schedule/thread/pool.rs +++ /dev/null @@ -1,113 +0,0 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/rust-lang/rust-analyzer.git | -// | File: `crates/stdx/src/thread/pool.rs` | -// | Commit: 03b3cb6be9f21c082f4206b35c7fe7f291c94eaa | -// +------------------------------------------------------------+ -//! [`Pool`] implements a basic custom thread pool -//! inspired by the [`threadpool` crate](http://docs.rs/threadpool). -//! When you spawn a task you specify a thread priority -//! so the pool can schedule it to run on a thread with that priority. -//! rust-analyzer uses this to prioritize work based on latency requirements. -//! -//! The thread pool is implemented entirely using -//! the threading utilities in [`crate::server::schedule::thread`]. - -use std::{ - num::NonZeroUsize, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, -}; - -use crossbeam::channel::{Receiver, Sender}; - -use super::{Builder, JoinHandle, ThreadPriority}; - -pub(crate) struct Pool { - // `_handles` is never read: the field is present - // only for its `Drop` impl. - - // The worker threads exit once the channel closes; - // make sure to keep `job_sender` above `handles` - // so that the channel is actually closed - // before we join the worker threads! - job_sender: Sender, - _handles: Vec, - extant_tasks: Arc, -} - -struct Job { - requested_priority: ThreadPriority, - f: Box, -} - -impl Pool { - pub(crate) fn new(threads: NonZeroUsize) -> Pool { - // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. - const STACK_SIZE: usize = 2 * 1024 * 1024; - const INITIAL_PRIORITY: ThreadPriority = ThreadPriority::Worker; - - let threads = usize::from(threads); - - // Channel buffer capacity is between 2 and 4, depending on the pool size. - let (job_sender, job_receiver) = crossbeam::channel::bounded(std::cmp::min(threads * 2, 4)); - let extant_tasks = Arc::new(AtomicUsize::new(0)); - - let mut handles = Vec::with_capacity(threads); - for i in 0..threads { - let handle = Builder::new(INITIAL_PRIORITY) - .stack_size(STACK_SIZE) - .name(format!("ruff:worker:{i}")) - .spawn({ - let extant_tasks = Arc::clone(&extant_tasks); - let job_receiver: Receiver = job_receiver.clone(); - move || { - let mut current_priority = INITIAL_PRIORITY; - for job in job_receiver { - if job.requested_priority != current_priority { - job.requested_priority.apply_to_current_thread(); - current_priority = job.requested_priority; - } - extant_tasks.fetch_add(1, Ordering::SeqCst); - (job.f)(); - extant_tasks.fetch_sub(1, Ordering::SeqCst); - } - } - }) - .expect("failed to spawn thread"); - - handles.push(handle); - } - - Pool { - _handles: handles, - extant_tasks, - job_sender, - } - } - - pub(crate) fn spawn(&self, priority: ThreadPriority, f: F) - where - F: FnOnce() + Send + 'static, - { - let f = Box::new(move || { - if cfg!(debug_assertions) { - priority.assert_is_used_on_current_thread(); - } - f(); - }); - - let job = Job { - requested_priority: priority, - f, - }; - self.job_sender.send(job).unwrap(); - } - - #[allow(dead_code)] - pub(super) fn len(&self) -> usize { - self.extant_tasks.load(Ordering::SeqCst) - } -} diff --git a/crates/red_knot_server/src/session.rs b/crates/red_knot_server/src/session.rs deleted file mode 100644 index 470592e8bda92..0000000000000 --- a/crates/red_knot_server/src/session.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Data model, state management, and configuration resolution. - -use std::collections::BTreeMap; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use anyhow::anyhow; -use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url}; - -use red_knot_project::{ProjectDatabase, ProjectMetadata}; -use ruff_db::files::{system_path_to_file, File}; -use ruff_db::system::SystemPath; -use ruff_db::Db; - -use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; -use crate::system::{url_to_any_system_path, AnySystemPath, LSPSystem}; -use crate::{PositionEncoding, TextDocument}; - -pub(crate) use self::capabilities::ResolvedClientCapabilities; -pub use self::index::DocumentQuery; -pub(crate) use self::settings::AllSettings; -pub use self::settings::ClientSettings; - -mod capabilities; -pub(crate) mod index; -mod settings; - -// TODO(dhruvmanila): In general, the server shouldn't use any salsa queries directly and instead -// should use methods on `ProjectDatabase`. - -/// The global state for the LSP -pub struct Session { - /// Used to retrieve information about open documents and settings. - /// - /// This will be [`None`] when a mutable reference is held to the index via [`index_mut`] - /// to prevent the index from being accessed while it is being modified. It will be restored - /// when the mutable reference ([`MutIndexGuard`]) is dropped. - /// - /// [`index_mut`]: Session::index_mut - index: Option>, - - /// Maps workspace folders to their respective project databases. - projects_by_workspace_folder: BTreeMap, - - /// The global position encoding, negotiated during LSP initialization. - position_encoding: PositionEncoding, - /// Tracks what LSP features the client supports and doesn't support. - resolved_client_capabilities: Arc, -} - -impl Session { - pub fn new( - client_capabilities: &ClientCapabilities, - position_encoding: PositionEncoding, - global_settings: ClientSettings, - workspace_folders: &[(Url, ClientSettings)], - ) -> crate::Result { - let mut workspaces = BTreeMap::new(); - let index = Arc::new(index::Index::new(global_settings)); - - for (url, _) in workspace_folders { - let path = url - .to_file_path() - .map_err(|()| anyhow!("Workspace URL is not a file or directory: {:?}", url))?; - let system_path = SystemPath::from_std_path(&path) - .ok_or_else(|| anyhow!("Workspace path is not a valid UTF-8 path: {:?}", path))?; - let system = LSPSystem::new(index.clone()); - - // TODO(dhruvmanila): Get the values from the client settings - let mut metadata = ProjectMetadata::discover(system_path, &system)?; - metadata.apply_configuration_files(&system)?; - - // TODO(micha): Handle the case where the program settings are incorrect more gracefully. - workspaces.insert(path, ProjectDatabase::new(metadata, system)?); - } - - Ok(Self { - position_encoding, - projects_by_workspace_folder: workspaces, - index: Some(index), - resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( - client_capabilities, - )), - }) - } - - // TODO(dhruvmanila): Ideally, we should have a single method for `workspace_db_for_path_mut` - // and `default_workspace_db_mut` but the borrow checker doesn't allow that. - // https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437 - - /// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if - /// any. - pub(crate) fn project_db_for_path(&self, path: impl AsRef) -> Option<&ProjectDatabase> { - self.projects_by_workspace_folder - .range(..=path.as_ref().to_path_buf()) - .next_back() - .map(|(_, db)| db) - } - - /// Returns a mutable reference to the project [`ProjectDatabase`] corresponding to the given - /// path, if any. - pub(crate) fn project_db_for_path_mut( - &mut self, - path: impl AsRef, - ) -> Option<&mut ProjectDatabase> { - self.projects_by_workspace_folder - .range_mut(..=path.as_ref().to_path_buf()) - .next_back() - .map(|(_, db)| db) - } - - /// Returns a reference to the default project [`ProjectDatabase`]. The default project is the - /// minimum root path in the project map. - pub(crate) fn default_project_db(&self) -> &ProjectDatabase { - // SAFETY: Currently, red knot only support a single project. - self.projects_by_workspace_folder.values().next().unwrap() - } - - /// Returns a mutable reference to the default project [`ProjectDatabase`]. - pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase { - // SAFETY: Currently, red knot only support a single project. - self.projects_by_workspace_folder - .values_mut() - .next() - .unwrap() - } - - pub fn key_from_url(&self, url: Url) -> DocumentKey { - self.index().key_from_url(url) - } - - /// Creates a document snapshot with the URL referencing the document to snapshot. - pub fn take_snapshot(&self, url: Url) -> Option { - let key = self.key_from_url(url); - Some(DocumentSnapshot { - resolved_client_capabilities: self.resolved_client_capabilities.clone(), - document_ref: self.index().make_document_ref(key)?, - position_encoding: self.position_encoding, - }) - } - - /// Registers a notebook document at the provided `url`. - /// If a document is already open here, it will be overwritten. - pub fn open_notebook_document(&mut self, url: Url, document: NotebookDocument) { - self.index_mut().open_notebook_document(url, document); - } - - /// Registers a text document at the provided `url`. - /// If a document is already open here, it will be overwritten. - pub(crate) fn open_text_document(&mut self, url: Url, document: TextDocument) { - self.index_mut().open_text_document(url, document); - } - - /// Updates a text document at the associated `key`. - /// - /// The document key must point to a text document, or this will throw an error. - pub(crate) fn update_text_document( - &mut self, - key: &DocumentKey, - content_changes: Vec, - new_version: DocumentVersion, - ) -> crate::Result<()> { - let position_encoding = self.position_encoding; - self.index_mut() - .update_text_document(key, content_changes, new_version, position_encoding) - } - - /// De-registers a document, specified by its key. - /// Calling this multiple times for the same document is a logic error. - pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { - self.index_mut().close_document(key)?; - Ok(()) - } - - /// Returns a reference to the index. - /// - /// # Panics - /// - /// Panics if there's a mutable reference to the index via [`index_mut`]. - /// - /// [`index_mut`]: Session::index_mut - fn index(&self) -> &index::Index { - self.index.as_ref().unwrap() - } - - /// Returns a mutable reference to the index. - /// - /// This method drops all references to the index and returns a guard that will restore the - /// references when dropped. This guard holds the only reference to the index and allows - /// modifying it. - fn index_mut(&mut self) -> MutIndexGuard { - let index = self.index.take().unwrap(); - - for db in self.projects_by_workspace_folder.values_mut() { - // Remove the `index` from each database. This drops the count of `Arc` down to 1 - db.system_mut() - .as_any_mut() - .downcast_mut::() - .unwrap() - .take_index(); - } - - // There should now be exactly one reference to index which is self.index. - let index = Arc::into_inner(index); - - MutIndexGuard { - session: self, - index, - } - } -} - -/// A guard that holds the only reference to the index and allows modifying it. -/// -/// When dropped, this guard restores all references to the index. -struct MutIndexGuard<'a> { - session: &'a mut Session, - index: Option, -} - -impl Deref for MutIndexGuard<'_> { - type Target = index::Index; - - fn deref(&self) -> &Self::Target { - self.index.as_ref().unwrap() - } -} - -impl DerefMut for MutIndexGuard<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.index.as_mut().unwrap() - } -} - -impl Drop for MutIndexGuard<'_> { - fn drop(&mut self) { - if let Some(index) = self.index.take() { - let index = Arc::new(index); - for db in self.session.projects_by_workspace_folder.values_mut() { - db.system_mut() - .as_any_mut() - .downcast_mut::() - .unwrap() - .set_index(index.clone()); - } - - self.session.index = Some(index); - } - } -} - -/// An immutable snapshot of `Session` that references -/// a specific document. -#[derive(Debug)] -pub struct DocumentSnapshot { - resolved_client_capabilities: Arc, - document_ref: index::DocumentQuery, - position_encoding: PositionEncoding, -} - -impl DocumentSnapshot { - pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { - &self.resolved_client_capabilities - } - - pub fn query(&self) -> &index::DocumentQuery { - &self.document_ref - } - - pub(crate) fn encoding(&self) -> PositionEncoding { - self.position_encoding - } - - pub(crate) fn file(&self, db: &dyn Db) -> Option { - match url_to_any_system_path(self.document_ref.file_url()).ok()? { - AnySystemPath::System(path) => system_path_to_file(db, path).ok(), - AnySystemPath::SystemVirtual(virtual_path) => db - .files() - .try_virtual_file(&virtual_path) - .map(|virtual_file| virtual_file.file()), - } - } -} diff --git a/crates/red_knot_server/src/session/capabilities.rs b/crates/red_knot_server/src/session/capabilities.rs deleted file mode 100644 index 5ba4c3e0a5963..0000000000000 --- a/crates/red_knot_server/src/session/capabilities.rs +++ /dev/null @@ -1,95 +0,0 @@ -use lsp_types::{ClientCapabilities, MarkupKind}; - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -#[allow(clippy::struct_excessive_bools)] -pub(crate) struct ResolvedClientCapabilities { - pub(crate) code_action_deferred_edit_resolution: bool, - pub(crate) apply_edit: bool, - pub(crate) document_changes: bool, - pub(crate) workspace_refresh: bool, - pub(crate) pull_diagnostics: bool, - /// Whether `textDocument.typeDefinition.linkSupport` is `true` - pub(crate) type_definition_link_support: bool, - - /// `true`, if the first markup kind in `textDocument.hover.contentFormat` is `Markdown` - pub(crate) hover_prefer_markdown: bool, -} - -impl ResolvedClientCapabilities { - pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { - let code_action_settings = client_capabilities - .text_document - .as_ref() - .and_then(|doc_settings| doc_settings.code_action.as_ref()); - let code_action_data_support = code_action_settings - .and_then(|code_action_settings| code_action_settings.data_support) - .unwrap_or_default(); - let code_action_edit_resolution = code_action_settings - .and_then(|code_action_settings| code_action_settings.resolve_support.as_ref()) - .is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into())); - - let apply_edit = client_capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.apply_edit) - .unwrap_or_default(); - - let document_changes = client_capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.workspace_edit.as_ref()) - .and_then(|workspace_edit| workspace_edit.document_changes) - .unwrap_or_default(); - - let declaration_link_support = client_capabilities - .text_document - .as_ref() - .and_then(|document| document.type_definition?.link_support) - .unwrap_or_default(); - - let workspace_refresh = true; - - // TODO(jane): Once the bug involving workspace.diagnostic(s) deserialization has been fixed, - // uncomment this. - /* - let workspace_refresh = client_capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.diagnostic.as_ref()) - .and_then(|diagnostic| diagnostic.refresh_support) - .unwrap_or_default(); - */ - - let pull_diagnostics = client_capabilities - .text_document - .as_ref() - .and_then(|text_document| text_document.diagnostic.as_ref()) - .is_some(); - - let hover_prefer_markdown = client_capabilities - .text_document - .as_ref() - .and_then(|text_document| { - Some( - text_document - .hover - .as_ref()? - .content_format - .as_ref()? - .contains(&MarkupKind::Markdown), - ) - }) - .unwrap_or_default(); - - Self { - code_action_deferred_edit_resolution: code_action_data_support - && code_action_edit_resolution, - apply_edit, - document_changes, - workspace_refresh, - pull_diagnostics, - type_definition_link_support: declaration_link_support, - hover_prefer_markdown, - } - } -} diff --git a/crates/red_knot_server/src/session/index.rs b/crates/red_knot_server/src/session/index.rs deleted file mode 100644 index f5e12e768d1a4..0000000000000 --- a/crates/red_knot_server/src/session/index.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::path::Path; -use std::sync::Arc; - -use lsp_types::Url; -use rustc_hash::FxHashMap; - -use crate::{ - document::{DocumentKey, DocumentVersion, NotebookDocument}, - PositionEncoding, TextDocument, -}; - -use super::ClientSettings; - -/// Stores and tracks all open documents in a session, along with their associated settings. -#[derive(Default, Debug)] -pub(crate) struct Index { - /// Maps all document file URLs to the associated document controller - documents: FxHashMap, - - /// Maps opaque cell URLs to a notebook URL (document) - notebook_cells: FxHashMap, - - /// Global settings provided by the client. - #[expect(dead_code)] - global_settings: ClientSettings, -} - -impl Index { - pub(super) fn new(global_settings: ClientSettings) -> Self { - Self { - documents: FxHashMap::default(), - notebook_cells: FxHashMap::default(), - global_settings, - } - } - - #[expect(dead_code)] - pub(super) fn text_document_urls(&self) -> impl Iterator + '_ { - self.documents - .iter() - .filter_map(|(url, doc)| doc.as_text().and(Some(url))) - } - - #[expect(dead_code)] - pub(super) fn notebook_document_urls(&self) -> impl Iterator + '_ { - self.documents - .iter() - .filter(|(_, doc)| doc.as_notebook().is_some()) - .map(|(url, _)| url) - } - - pub(super) fn update_text_document( - &mut self, - key: &DocumentKey, - content_changes: Vec, - new_version: DocumentVersion, - encoding: PositionEncoding, - ) -> crate::Result<()> { - let controller = self.document_controller_for_key(key)?; - let Some(document) = controller.as_text_mut() else { - anyhow::bail!("Text document URI does not point to a text document"); - }; - - if content_changes.is_empty() { - document.update_version(new_version); - return Ok(()); - } - - document.apply_changes(content_changes, new_version, encoding); - - Ok(()) - } - - pub(crate) fn key_from_url(&self, url: Url) -> DocumentKey { - if self.notebook_cells.contains_key(&url) { - DocumentKey::NotebookCell(url) - } else if Path::new(url.path()) - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb")) - { - DocumentKey::Notebook(url) - } else { - DocumentKey::Text(url) - } - } - - #[expect(dead_code)] - pub(super) fn update_notebook_document( - &mut self, - key: &DocumentKey, - cells: Option, - metadata: Option>, - new_version: DocumentVersion, - encoding: PositionEncoding, - ) -> crate::Result<()> { - // update notebook cell index - if let Some(lsp_types::NotebookDocumentCellChangeStructure { - did_open: Some(did_open), - .. - }) = cells.as_ref().and_then(|cells| cells.structure.as_ref()) - { - let Some(path) = self.url_for_key(key).cloned() else { - anyhow::bail!("Tried to open unavailable document `{key}`"); - }; - - for opened_cell in did_open { - self.notebook_cells - .insert(opened_cell.uri.clone(), path.clone()); - } - // deleted notebook cells are closed via textDocument/didClose - we don't close them here. - } - - let controller = self.document_controller_for_key(key)?; - let Some(notebook) = controller.as_notebook_mut() else { - anyhow::bail!("Notebook document URI does not point to a notebook document"); - }; - - notebook.update(cells, metadata, new_version, encoding)?; - Ok(()) - } - - pub(crate) fn make_document_ref(&self, key: DocumentKey) -> Option { - let url = self.url_for_key(&key)?.clone(); - let controller = self.documents.get(&url)?; - let cell_url = match key { - DocumentKey::NotebookCell(cell_url) => Some(cell_url), - _ => None, - }; - Some(controller.make_ref(cell_url, url)) - } - - pub(super) fn open_text_document(&mut self, url: Url, document: TextDocument) { - self.documents - .insert(url, DocumentController::new_text(document)); - } - - pub(super) fn open_notebook_document(&mut self, notebook_url: Url, document: NotebookDocument) { - for cell_url in document.urls() { - self.notebook_cells - .insert(cell_url.clone(), notebook_url.clone()); - } - self.documents - .insert(notebook_url, DocumentController::new_notebook(document)); - } - - pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { - // Notebook cells URIs are removed from the index here, instead of during - // `update_notebook_document`. This is because a notebook cell, as a text document, - // is requested to be `closed` by VS Code after the notebook gets updated. - // This is not documented in the LSP specification explicitly, and this assumption - // may need revisiting in the future as we support more editors with notebook support. - if let DocumentKey::NotebookCell(uri) = key { - if self.notebook_cells.remove(uri).is_none() { - tracing::warn!("Tried to remove a notebook cell that does not exist: {uri}",); - } - return Ok(()); - } - let Some(url) = self.url_for_key(key).cloned() else { - anyhow::bail!("Tried to close unavailable document `{key}`"); - }; - - let Some(_) = self.documents.remove(&url) else { - anyhow::bail!("tried to close document that didn't exist at {}", url) - }; - Ok(()) - } - - fn document_controller_for_key( - &mut self, - key: &DocumentKey, - ) -> crate::Result<&mut DocumentController> { - let Some(url) = self.url_for_key(key).cloned() else { - anyhow::bail!("Tried to open unavailable document `{key}`"); - }; - let Some(controller) = self.documents.get_mut(&url) else { - anyhow::bail!("Document controller not available at `{}`", url); - }; - Ok(controller) - } - - fn url_for_key<'a>(&'a self, key: &'a DocumentKey) -> Option<&'a Url> { - match key { - DocumentKey::Notebook(path) | DocumentKey::Text(path) => Some(path), - DocumentKey::NotebookCell(uri) => self.notebook_cells.get(uri), - } - } -} - -/// A mutable handler to an underlying document. -#[derive(Debug)] -enum DocumentController { - Text(Arc), - Notebook(Arc), -} - -impl DocumentController { - fn new_text(document: TextDocument) -> Self { - Self::Text(Arc::new(document)) - } - - fn new_notebook(document: NotebookDocument) -> Self { - Self::Notebook(Arc::new(document)) - } - - fn make_ref(&self, cell_url: Option, file_url: Url) -> DocumentQuery { - match &self { - Self::Notebook(notebook) => DocumentQuery::Notebook { - cell_url, - file_url, - notebook: notebook.clone(), - }, - Self::Text(document) => DocumentQuery::Text { - file_url, - document: document.clone(), - }, - } - } - - pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { - Some(match self { - Self::Notebook(notebook) => Arc::make_mut(notebook), - Self::Text(_) => return None, - }) - } - - pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> { - match self { - Self::Notebook(notebook) => Some(notebook), - Self::Text(_) => None, - } - } - - #[allow(dead_code)] - pub(crate) fn as_text(&self) -> Option<&TextDocument> { - match self { - Self::Text(document) => Some(document), - Self::Notebook(_) => None, - } - } - - pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> { - Some(match self { - Self::Text(document) => Arc::make_mut(document), - Self::Notebook(_) => return None, - }) - } -} - -/// A read-only query to an open document. -/// This query can 'select' a text document, full notebook, or a specific notebook cell. -/// It also includes document settings. -#[derive(Debug, Clone)] -pub enum DocumentQuery { - Text { - file_url: Url, - document: Arc, - }, - Notebook { - /// The selected notebook cell, if it exists. - cell_url: Option, - /// The URL of the notebook. - file_url: Url, - notebook: Arc, - }, -} - -impl DocumentQuery { - /// Retrieve the original key that describes this document query. - #[expect(dead_code)] - pub(crate) fn make_key(&self) -> DocumentKey { - match self { - Self::Text { file_url, .. } => DocumentKey::Text(file_url.clone()), - Self::Notebook { - cell_url: Some(cell_uri), - .. - } => DocumentKey::NotebookCell(cell_uri.clone()), - Self::Notebook { file_url, .. } => DocumentKey::Notebook(file_url.clone()), - } - } - - /// Attempts to access the underlying notebook document that this query is selecting. - pub fn as_notebook(&self) -> Option<&NotebookDocument> { - match self { - Self::Notebook { notebook, .. } => Some(notebook), - Self::Text { .. } => None, - } - } - - /// Get the version of document selected by this query. - pub(crate) fn version(&self) -> DocumentVersion { - match self { - Self::Text { document, .. } => document.version(), - Self::Notebook { notebook, .. } => notebook.version(), - } - } - - /// Get the URL for the document selected by this query. - pub(crate) fn file_url(&self) -> &Url { - match self { - Self::Text { file_url, .. } | Self::Notebook { file_url, .. } => file_url, - } - } - - /// Attempt to access the single inner text document selected by the query. - /// If this query is selecting an entire notebook document, this will return `None`. - #[expect(dead_code)] - pub(crate) fn as_single_document(&self) -> Option<&TextDocument> { - match self { - Self::Text { document, .. } => Some(document), - Self::Notebook { - notebook, - cell_url: cell_uri, - .. - } => cell_uri - .as_ref() - .and_then(|cell_uri| notebook.cell_document_by_uri(cell_uri)), - } - } -} diff --git a/crates/red_knot_server/src/session/settings.rs b/crates/red_knot_server/src/session/settings.rs deleted file mode 100644 index f140b9c4b1847..0000000000000 --- a/crates/red_knot_server/src/session/settings.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::path::PathBuf; - -use lsp_types::Url; -use rustc_hash::FxHashMap; -use serde::Deserialize; - -/// Maps a workspace URI to its associated client settings. Used during server initialization. -pub(crate) type WorkspaceSettingsMap = FxHashMap; - -/// This is a direct representation of the settings schema sent by the client. -#[derive(Debug, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub struct ClientSettings { - // These settings are only needed for tracing, and are only read from the global configuration. - // These will not be in the resolved settings. - #[serde(flatten)] - pub(crate) tracing: TracingSettings, -} - -/// Settings needed to initialize tracing. These will only be -/// read from the global configuration. -#[derive(Debug, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub(crate) struct TracingSettings { - pub(crate) log_level: Option, - /// Path to the log file - tildes and environment variables are supported. - pub(crate) log_file: Option, -} - -/// This is a direct representation of the workspace settings schema, -/// which inherits the schema of [`ClientSettings`] and adds extra fields -/// to describe the workspace it applies to. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct WorkspaceSettings { - #[serde(flatten)] - settings: ClientSettings, - workspace: Url, -} - -/// This is the exact schema for initialization options sent in by the client -/// during initialization. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(untagged)] -enum InitializationOptions { - #[serde(rename_all = "camelCase")] - HasWorkspaces { - global_settings: ClientSettings, - #[serde(rename = "settings")] - workspace_settings: Vec, - }, - GlobalOnly { - #[serde(default)] - settings: ClientSettings, - }, -} - -/// Built from the initialization options provided by the client. -#[derive(Debug)] -pub(crate) struct AllSettings { - pub(crate) global_settings: ClientSettings, - /// If this is `None`, the client only passed in global settings. - pub(crate) workspace_settings: Option, -} - -impl AllSettings { - /// Initializes the controller from the serialized initialization options. - /// This fails if `options` are not valid initialization options. - pub(crate) fn from_value(options: serde_json::Value) -> Self { - Self::from_init_options( - serde_json::from_value(options) - .map_err(|err| { - tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); - show_err_msg!("Ruff received invalid client settings - falling back to default client settings."); - }) - .unwrap_or_default(), - ) - } - - fn from_init_options(options: InitializationOptions) -> Self { - let (global_settings, workspace_settings) = match options { - InitializationOptions::GlobalOnly { settings } => (settings, None), - InitializationOptions::HasWorkspaces { - global_settings, - workspace_settings, - } => (global_settings, Some(workspace_settings)), - }; - - Self { - global_settings, - workspace_settings: workspace_settings.map(|workspace_settings| { - workspace_settings - .into_iter() - .map(|settings| (settings.workspace, settings.settings)) - .collect() - }), - } - } -} - -impl Default for InitializationOptions { - fn default() -> Self { - Self::GlobalOnly { - settings: ClientSettings::default(), - } - } -} diff --git a/crates/red_knot_server/src/system.rs b/crates/red_knot_server/src/system.rs deleted file mode 100644 index 12d4057a72758..0000000000000 --- a/crates/red_knot_server/src/system.rs +++ /dev/null @@ -1,263 +0,0 @@ -use std::any::Any; -use std::fmt::Display; -use std::sync::Arc; - -use lsp_types::Url; -use red_knot_python_semantic::Db; -use ruff_db::file_revision::FileRevision; -use ruff_db::files::{File, FilePath}; -use ruff_db::system::walk_directory::WalkDirectoryBuilder; -use ruff_db::system::{ - CaseSensitivity, DirectoryEntry, FileType, GlobError, Metadata, OsSystem, PatternError, Result, - System, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, -}; -use ruff_notebook::{Notebook, NotebookError}; - -use crate::session::index::Index; -use crate::DocumentQuery; - -/// Converts the given [`Url`] to an [`AnySystemPath`]. -/// -/// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the -/// URL is converted to a [`SystemVirtualPathBuf`]. -/// -/// This fails in the following cases: -/// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]). -/// * If the URL is not a valid UTF-8 string. -pub(crate) fn url_to_any_system_path(url: &Url) -> std::result::Result { - if url.scheme() == "file" { - Ok(AnySystemPath::System( - SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?, - )) - } else { - Ok(AnySystemPath::SystemVirtual( - SystemVirtualPath::new(url.as_str()).to_path_buf(), - )) - } -} - -pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option { - match file.path(db) { - FilePath::System(system) => Url::from_file_path(system.as_std_path()).ok(), - FilePath::SystemVirtual(path) => Url::parse(path.as_str()).ok(), - // TODO: Not yet supported, consider an approach similar to Sorbet's custom paths - // https://sorbet.org/docs/sorbet-uris - FilePath::Vendored(_) => None, - } -} - -/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`]. -#[derive(Debug)] -pub(crate) enum AnySystemPath { - System(SystemPathBuf), - SystemVirtual(SystemVirtualPathBuf), -} - -#[derive(Debug)] -pub(crate) struct LSPSystem { - /// A read-only copy of the index where the server stores all the open documents and settings. - /// - /// This will be [`None`] when a mutable reference is held to the index via [`index_mut`] - /// method to prevent the index from being accessed while it is being modified. It will be - /// restored when the mutable reference is dropped. - /// - /// [`index_mut`]: crate::Session::index_mut - index: Option>, - - /// A system implementation that uses the local file system. - os_system: OsSystem, -} - -impl LSPSystem { - pub(crate) fn new(index: Arc) -> Self { - let cwd = std::env::current_dir().unwrap(); - let os_system = OsSystem::new(SystemPathBuf::from_path_buf(cwd).unwrap()); - - Self { - index: Some(index), - os_system, - } - } - - /// Takes the index out of the system. - pub(crate) fn take_index(&mut self) -> Option> { - self.index.take() - } - - /// Sets the index for the system. - pub(crate) fn set_index(&mut self, index: Arc) { - self.index = Some(index); - } - - /// Returns a reference to the contained index. - /// - /// # Panics - /// - /// Panics if the index is `None`. - fn index(&self) -> &Index { - self.index.as_ref().unwrap() - } - - fn make_document_ref(&self, url: Url) -> Option { - let index = self.index(); - let key = index.key_from_url(url); - index.make_document_ref(key) - } - - fn system_path_to_document_ref(&self, path: &SystemPath) -> Result> { - let url = Url::from_file_path(path.as_std_path()).map_err(|()| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Failed to convert system path to URL: {path:?}"), - ) - })?; - Ok(self.make_document_ref(url)) - } - - fn system_virtual_path_to_document_ref( - &self, - path: &SystemVirtualPath, - ) -> Result> { - let url = Url::parse(path.as_str()).map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Failed to convert virtual path to URL: {path:?}"), - ) - })?; - Ok(self.make_document_ref(url)) - } -} - -impl System for LSPSystem { - fn path_metadata(&self, path: &SystemPath) -> Result { - let document = self.system_path_to_document_ref(path)?; - - if let Some(document) = document { - Ok(Metadata::new( - document_revision(&document), - None, - FileType::File, - )) - } else { - self.os_system.path_metadata(path) - } - } - - fn canonicalize_path(&self, path: &SystemPath) -> Result { - self.os_system.canonicalize_path(path) - } - - fn path_exists_case_sensitive(&self, path: &SystemPath, prefix: &SystemPath) -> bool { - self.os_system.path_exists_case_sensitive(path, prefix) - } - - fn read_to_string(&self, path: &SystemPath) -> Result { - let document = self.system_path_to_document_ref(path)?; - - match document { - Some(DocumentQuery::Text { document, .. }) => Ok(document.contents().to_string()), - _ => self.os_system.read_to_string(path), - } - } - - fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result { - let document = self.system_path_to_document_ref(path)?; - - match document { - Some(DocumentQuery::Text { document, .. }) => { - Notebook::from_source_code(document.contents()) - } - Some(DocumentQuery::Notebook { notebook, .. }) => Ok(notebook.make_ruff_notebook()), - None => self.os_system.read_to_notebook(path), - } - } - - fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result { - let document = self - .system_virtual_path_to_document_ref(path)? - .ok_or_else(|| virtual_path_not_found(path))?; - - if let DocumentQuery::Text { document, .. } = &document { - Ok(document.contents().to_string()) - } else { - Err(not_a_text_document(path)) - } - } - - fn read_virtual_path_to_notebook( - &self, - path: &SystemVirtualPath, - ) -> std::result::Result { - let document = self - .system_virtual_path_to_document_ref(path)? - .ok_or_else(|| virtual_path_not_found(path))?; - - match document { - DocumentQuery::Text { document, .. } => Notebook::from_source_code(document.contents()), - DocumentQuery::Notebook { notebook, .. } => Ok(notebook.make_ruff_notebook()), - } - } - - fn current_directory(&self) -> &SystemPath { - self.os_system.current_directory() - } - - fn user_config_directory(&self) -> Option { - self.os_system.user_config_directory() - } - - fn read_directory<'a>( - &'a self, - path: &SystemPath, - ) -> Result> + 'a>> { - self.os_system.read_directory(path) - } - - fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder { - self.os_system.walk_directory(path) - } - - fn glob( - &self, - pattern: &str, - ) -> std::result::Result< - Box>>, - PatternError, - > { - self.os_system.glob(pattern) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn case_sensitivity(&self) -> CaseSensitivity { - self.os_system.case_sensitivity() - } -} - -fn not_a_text_document(path: impl Display) -> std::io::Error { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Input is not a text document: {path}"), - ) -} - -fn virtual_path_not_found(path: impl Display) -> std::io::Error { - std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Virtual path does not exist: {path}"), - ) -} - -/// Helper function to get the [`FileRevision`] of the given document. -fn document_revision(document: &DocumentQuery) -> FileRevision { - // The file revision is just an opaque number which doesn't have any significant meaning other - // than that the file has changed if the revisions are different. - #[allow(clippy::cast_sign_loss)] - FileRevision::new(document.version() as u128) -} diff --git a/crates/red_knot_test/Cargo.toml b/crates/red_knot_test/Cargo.toml deleted file mode 100644 index 06727647c9cd1..0000000000000 --- a/crates/red_knot_test/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "red_knot_test" -version = "0.0.0" -publish = false -edition.workspace = true -rust-version.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true -authors.workspace = true -license.workspace = true - -[dependencies] -red_knot_python_semantic = { workspace = true, features = ["serde"] } -red_knot_vendored = { workspace = true } -ruff_db = { workspace = true, features = ["os", "testing"] } -ruff_index = { workspace = true } -ruff_notebook = { workspace = true } -ruff_python_trivia = { workspace = true } -ruff_source_file = { workspace = true } -ruff_text_size = { workspace = true } -ruff_python_ast = { workspace = true } - -anyhow = { workspace = true } -camino = { workspace = true } -colored = { workspace = true } -insta = { workspace = true, features = ["filters"] } -memchr = { workspace = true } -regex = { workspace = true } -rustc-hash = { workspace = true } -salsa = { workspace = true } -smallvec = { workspace = true } -serde = { workspace = true } -tempfile = { workspace = true } -toml = { workspace = true } -thiserror = { workspace = true } - -[lints] -workspace = true diff --git a/crates/red_knot_test/README.md b/crates/red_knot_test/README.md deleted file mode 100644 index 893c4d3b36961..0000000000000 --- a/crates/red_knot_test/README.md +++ /dev/null @@ -1,542 +0,0 @@ -# Writing type-checking / type-inference tests - -Any Markdown file can be a test suite. - -In order for it to be run as one, `red_knot_test::run` must be called with its path; see -`crates/red_knot_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files -under a certain directory as test suites. - -A Markdown test suite can contain any number of tests. A test consists of one or more embedded -"files", each defined by a triple-backticks fenced code block. The code block must have a tag string -specifying its language. We currently support `py` (Python files) and `pyi` (type stub files), as -well as [typeshed `VERSIONS`] files and `toml` for configuration. - -The simplest possible test suite consists of just a single test, with a single embedded file: - -````markdown -```py -reveal_type(1) # revealed: Literal[1] -``` -```` - -When running this test, the mdtest framework will write a file with these contents to the default -file path (`/src/mdtest_snippet.py`) in its in-memory file system, run a type check on that file, -and then match the resulting diagnostics with the assertions in the test. Assertions are in the form -of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, -it fails. - - - -See actual example mdtest suites in -[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest). - -> [!NOTE] -> If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory, -> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`, -> you will likely want to also make sure that the crate the tests are in is rebuilt every time a -> Markdown file is added or removed from the directory. See -> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs) -> for an example of how to do this. -> -> This is because these macros generate their tests at build time rather than at runtime. -> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed, -> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild -> following the addition of the new test file. - -## Assertions - -Two kinds of assertions are supported: `# revealed:` (shown above) and `# error:`. - -### Assertion kinds - -#### revealed - -A `# revealed:` assertion should always be paired with a call to the `reveal_type` utility, which -reveals (via a diagnostic) the inferred type of its argument (which can be any expression). The text -after `# revealed:` must match exactly with the displayed form of the revealed type of that -expression. - -The `reveal_type` function can be imported from the `typing` standard library module (or, for older -Python versions, from the `typing_extensions` pseudo-standard-library module[^extensions]): - -```py -from typing import reveal_type - -reveal_type("foo") # revealed: Literal["foo"] -``` - -For convenience, type checkers also pretend that `reveal_type` is a built-in, so that this import is -not required. Using `reveal_type` without importing it issues a diagnostic warning that it was used -without importing it, in addition to the diagnostic revealing the type of the expression. - -The `# revealed:` assertion must always match a revealed-type diagnostic, and will also match the -undefined-reveal diagnostic, if present, so it's safe to use `reveal_type` in tests either with or -without importing it. (Style preference is to not import it in tests, unless specifically testing -something about the behavior of importing it.) - -#### error - -A comment beginning with `# error:` is an assertion that a type checker diagnostic will be emitted, -with text span starting on that line. The matching can be narrowed in three ways: - -- `# error: [invalid-assignment]` requires that the matched diagnostic have the rule code - `invalid-assignment`. (The square brackets are required.) -- `# error: "Some text"` requires that the diagnostic's full message contain the text `Some text`. - (The double quotes are required in the assertion comment; they are not part of the matched text.) -- `# error: 8 [rule-code]` or `# error: 8 "Some text"` additionally requires that the matched - diagnostic's text span begins on column 8 (one-indexed) of this line. - -Assertions must contain either a rule code or a contains-text, or both, and may optionally also -include a column number. They must come in order: first column, if present; then rule code, if -present; then contains-text, if present. For example, an assertion using all three would look like -`# error: 8 [invalid-assignment] "Some text"`. - -Error assertions in tests intended to test type checker semantics should primarily use rule-code -assertions, with occasional contains-text assertions where needed to disambiguate or validate some -details of the diagnostic message. - -### Assertion locations - -An assertion comment may be a line-trailing comment, in which case it applies to the line it is on: - -```py -x: str = 1 # error: [invalid-assignment] -``` - -Or it may be a comment on its own line, in which case it applies to the next line that does not -contain an assertion comment: - -```py -# error: [invalid-assignment] -x: str = 1 -``` - -Multiple assertions applying to the same line may be stacked: - -```py -# error: [invalid-assignment] -# revealed: Literal[1] -x: str = reveal_type(1) -``` - -Intervening empty lines or non-assertion comments are not allowed; an assertion stack must be one -assertion per line, immediately following each other, with the line immediately following the last -assertion as the line of source code on which the matched diagnostics are emitted. - -## Literate style - -If multiple code blocks (without an explicit path, see below) are present in a single test, they will -be merged into a single file in the order they appear in the Markdown file. This allows for tests that -interleave code and explanations: - -````markdown -# My literate test - -This first snippet here: - -```py -from typing import Literal - -def f(x: Literal[1]): - pass -``` - -will be merged with this second snippet here, i.e. `f` is defined here: - -```py -f(2) # error: [invalid-argument-type] -``` -```` - -## Diagnostic Snapshotting - -In addition to inline assertions, one can also snapshot the full diagnostic -output of a test. This is done by adding a `` directive -in the corresponding section. For example: - -````markdown -## Unresolvable module import - - - -```py -import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" -``` -```` - -The `snapshot-diagnostics` directive must appear before anything else in -the section. - -This will use `insta` to manage an external file snapshot of all diagnostic -output generated. - -Inline assertions, as described above, may be used in conjunction with diagnostic -snapshotting. - -At present, there is no way to do inline snapshotting or to request more granular -snapshotting of specific diagnostics. - -## Multi-file tests - -Some tests require multiple files, with imports from one file into another. For this purpose, -tests can specify explicit file paths in a separate line before the code block (`b.py` below): - -````markdown -```py -from b import C -reveal_type(C) # revealed: Literal[C] -``` - -`b.py`: - -```py -class C: pass -``` -```` - -Relative file names are always relative to the "workspace root", which is also an import root (that -is, the equivalent of a runtime entry on `sys.path`). - -The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but -this is a feature we will want to add in the future. - -So the above test creates two files, `/src/mdtest_snippet.py` and `/src/b.py`, and sets the workspace -root to `/src/`, allowing imports from `b.py` using the module name `b`. - -## Multi-test suites - -A single test suite (Markdown file) can contain multiple tests, by demarcating them using Markdown -header lines: - -````markdown -# Same-file invalid assignment - -```py -x: int = "foo" # error: [invalid-assignment] -``` - -# Cross-file invalid assignment - -```py -from b import y -x: int = y # error: [invalid-assignment] -``` - -`b.py`: - -```py -y = "foo" -``` -```` - -This test suite contains two tests, one named "Same-file invalid assignment" and the other named -"Cross-file invalid assignment". The first test involves only a single embedded file, and the second -test involves two embedded files. - -The tests are run independently, in independent in-memory file systems and with new red-knot -[Salsa](https://github.com/salsa-rs/salsa) databases. This means that each is a from-scratch run of -the type checker, with no data persisting from any previous test. - -It is possible to filter to individual tests within a single markdown file using the -`MDTEST_TEST_FILTER` environment variable. This variable will match any tests which contain the -value as a case-sensitive substring in its name. An example test name is -`unpacking.md - Unpacking - Tuple - Multiple assignment`, which contains the name of the markdown -file and its parent headers joined together with hyphens. - -## Structured test suites - -Markdown headers can also be used to group related tests within a suite: - -````markdown -# Literals - -## Numbers - -### Integer - -```py -reveal_type(1) # revealed: Literal[1] -``` - -### Float - -```py -reveal_type(1.0) # revealed: float -``` - -## Strings - -```py -reveal_type("foo") # revealed: Literal["foo"] -``` -```` - -This test suite contains three tests, named "Literals - Numbers - Integer", "Literals - Numbers - -Float", and "Literals - Strings". - -A header-demarcated section must either be a test or a grouping header; it cannot be both. That is, -a header section can either contain embedded files (making it a test), or it can contain more -deeply-nested headers (headers with more `#`), but it cannot contain both. - -## Configuration - -The test framework supports a TOML-based configuration format, which is a subset of the full red-knot -configuration format. This configuration can be specified in fenced code blocks with `toml` as the -language tag: - -````markdown -```toml -[environment] -python-version = "3.10" -``` -```` - -This configuration will apply to all tests in the same section, and all nested sections within that -section. Nested sections can override configurations from their parent sections. - -See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_test/src/config.rs) for the full list of supported configuration options. - -### Specifying a custom typeshed - -Some tests will need to override the default typeshed with custom files. The `[environment]` -configuration option `typeshed` can be used to do this: - -````markdown -```toml -[environment] -typeshed = "/typeshed" -``` -```` - -For more details, take a look at the [custom-typeshed Markdown test]. - -### Mocking a virtual environment - -Mdtest supports mocking a virtual environment for a specific test at an arbitrary location, again -using the `[environment]` configuration option: - -````markdown -```toml -[environment] -python = ".venv" -``` -```` - -Red-knot will reject virtual environments that do not have valid `pyvenv.cfg` files at the -virtual-environment directory root (here, `.venv/pyvenv.cfg`). However, if a `pyvenv.cfg` file does -not have its contents specified by the test, mdtest will automatically generate one for you, to -make mocking a virtual environment more ergonomic. - -Mdtest also makes it easy to write Python packages to the mock virtual environment's -`site-packages` directory using the `` magic path segment. This would -otherwise be hard, due to the fact that the `site-packages` subdirectory in a virtual environment -is located at a different relative path depending on the platform the virtual environment was -created on. In the following test, mdtest will write the Python file to -`.venv/Lib/site-packages/foo.py` in its in-memory filesystem used for the test if the test is being -executed on Windows, and `.venv/lib/python3.13/site-packages/foo.py` otherwise: - -````markdown -```toml -[environment] -python = ".venv" -python-version = "3.13" -``` - -`.venv//foo.py`: - -```py -X = 1 -``` -```` - -## Documentation of tests - -Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by -the test framework) between fenced code blocks. This permits natural documentation of -why a test exists, and what it intends to assert: - -````markdown -Assigning a string to a variable annotated as `int` is not permitted: - -```py -x: int = "foo" # error: [invalid-assignment] -``` -```` - -## Running the tests - -All Markdown-based tests are executed in a normal `cargo test` / `cargo run nextest` run. If you want to run the Markdown tests -*only*, you can filter the tests using `mdtest__`: - -```bash -cargo test -p red_knot_python_semantic -- mdtest__ -``` - -Alternatively, you can use the `mdtest.py` runner which has a watch mode that will re-run corresponding tests when Markdown files change, and recompile automatically when Rust code changes: - -```bash -uv run crates/red_knot_python_semantic/mdtest.py -``` - -## Planned features - -There are some designed features that we intend for the test framework to have, but have not yet -implemented: - -### Multi-line diagnostic assertions - -We may want to be able to assert that a diagnostic spans multiple lines, and to assert the columns it -begins and/or ends on. The planned syntax for this will use `<<<` and `>>>` to mark the start and end lines for -an assertion: - -```py -(3 # error: 2 [unsupported-operands] <<< - + - "foo") # error: 6 >>> -``` - -The column assertion `6` on the ending line should be optional. - -In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins -an assertion ended by `>>>>`, etc. - -### Configuring search paths and kinds - -The red-knot TOML configuration format hasn't been finalized, and we may want to implement -support in the test framework for configuring search paths before it is designed. If so, we can -define some configuration options for now under the `[tests]` namespace. In the future, perhaps -some of these can be replaced by real red-knot configuration options; some or all may also be -kept long-term as test-specific options. - -Some configuration options we will want to provide: - -- We should be able to configure the default workspace root to something other than `/src/` using a - `workspace-root` configuration option. - -- We should be able to add a third-party root using the `third-party-root` configuration option. - -- We may want to add additional configuration options for setting additional search path kinds. - -Paths for `workspace-root` and `third-party-root` must be absolute. - -Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a -non-default value using the `workspace-root` config. - -### I/O errors - -We could use an `error=` configuration option in the tag string to make an embedded file cause an -I/O error on read. - -### Asserting on full diagnostic output - -> [!NOTE] -> At present, one can opt into diagnostic snapshotting that is managed via external files. See -> the section above for more details. The feature outlined below, *inline* diagnostic snapshotting, -> is still desirable. - -The inline comment diagnostic assertions are useful for making quick, readable assertions about -diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic -output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will -want to assert on diagnostics in a file, without impacting the contents of that file by changing a -comment in it. In these cases, a Python code block in a test could be followed by a fenced code -block with language `output`; this would contain the full diagnostic output for the preceding test -file: - -````markdown -# full output - -```py -x = 1 -reveal_type(x) -``` - -This is just an example, not a proposal that red-knot would ever actually output diagnostics in -precisely this format: - -```output -mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]' -``` -```` - -We will want to build tooling to automatically capture and update these “full diagnostic output” -blocks, when tests are run in an update-output mode (probably specified by an environment variable.) - -By default, an `output` block will specify diagnostic output for the file -`/mdtest_snippet.py`. An `output` block can be prefixed by a -`<path>`: label as usual, to explicitly specify the Python file for which it asserts -diagnostic output. - -It is an error for an `output` block to exist, if there is no `py` or `python` block in the same -test for the same file path. - -### Incremental tests - -Some tests should validate incremental checking, by initially creating some files, checking them, -and then modifying/adding/deleting files and checking again. - -We should add the capability to create an incremental test by using the `stage=` option on some -fenced code blocks in the test: - -````markdown -# Incremental - -## modify a file - -Initial file contents: - -```py -from b import x -reveal_type(x) -``` - -`b.py`: - -```py -x = 1 -``` - -Initial expected output for the unnamed file: - -```output -/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]' -``` - -Now in our first incremental stage, modify the contents of `b.py`: - -`b.py`: - -```py stage=1 -# b.py -x = 2 -``` - -And this is our updated expected output for the unnamed file at stage 1: - -```output stage=1 -/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[2]' -``` - -(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment -diagnostic assertions for `mdtest_snippet.py` would require specifying new contents for -`mdtest_snippet.py` in stage 1, which we don't want to do in this test.) -```` - -It will be possible to provide any number of stages in an incremental test. If a stage re-specifies -a filename that was specified in a previous stage (or the initial stage), that file is modified. A -new filename appearing for the first time in a new stage will create a new file. To delete a -previously created file, specify that file with the tag `delete` in its tag string (in this case, it -is an error to provide non-empty contents). Any previously-created files that are not re-specified -in a later stage continue to exist with their previously-specified contents, and are not "touched". - -All stages should be run in order, incrementally, and then the final state should also be re-checked -cold, to validate equivalence of cold and incremental check results. - -[^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers - also, treat it as part of the standard library. - -[custom-typeshed markdown test]: ../red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md -[typeshed `versions`]: https://github.com/python/typeshed/blob/c546278aae47de0b2b664973da4edb613400f6ce/stdlib/VERSIONS#L1-L18%3E diff --git a/crates/red_knot_test/src/config.rs b/crates/red_knot_test/src/config.rs deleted file mode 100644 index 8f2c4c3caaa43..0000000000000 --- a/crates/red_knot_test/src/config.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! TOML-deserializable Red Knot configuration, similar to `knot.toml`, to be able to -//! control some configuration options from Markdown files. For now, this supports the -//! following limited structure: -//! -//! ```toml -//! log = true # or log = "red_knot=WARN" -//! [environment] -//! python-version = "3.10" -//! ``` - -use anyhow::Context; -use red_knot_python_semantic::PythonPlatform; -use ruff_db::system::{SystemPath, SystemPathBuf}; -use ruff_python_ast::PythonVersion; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Debug, Default, Clone)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub(crate) struct MarkdownTestConfig { - pub(crate) environment: Option, - - pub(crate) log: Option, - - /// The [`ruff_db::system::System`] to use for tests. - /// - /// Defaults to the case-sensitive [`ruff_db::system::InMemorySystem`]. - pub(crate) system: Option, -} - -impl MarkdownTestConfig { - pub(crate) fn from_str(s: &str) -> anyhow::Result { - toml::from_str(s).context("Error while parsing Markdown TOML config") - } - - pub(crate) fn python_version(&self) -> Option { - self.environment.as_ref()?.python_version - } - - pub(crate) fn python_platform(&self) -> Option { - self.environment.as_ref()?.python_platform.clone() - } - - pub(crate) fn typeshed(&self) -> Option<&SystemPath> { - self.environment.as_ref()?.typeshed.as_deref() - } - - pub(crate) fn extra_paths(&self) -> Option<&[SystemPathBuf]> { - self.environment.as_ref()?.extra_paths.as_deref() - } - - pub(crate) fn python(&self) -> Option<&SystemPath> { - self.environment.as_ref()?.python.as_deref() - } -} - -#[derive(Deserialize, Debug, Default, Clone)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -pub(crate) struct Environment { - /// Target Python version to assume when resolving types. - pub(crate) python_version: Option, - - /// Target platform to assume when resolving types. - pub(crate) python_platform: Option, - - /// Path to a custom typeshed directory. - pub(crate) typeshed: Option, - - /// Additional search paths to consider when resolving modules. - pub(crate) extra_paths: Option>, - - /// Path to the Python installation from which Red Knot resolves type information and third-party dependencies. - /// - /// Red Knot will search in the path's `site-packages` directories for type information and - /// third-party imports. - /// - /// This option is commonly used to specify the path to a virtual environment. - #[serde(skip_serializing_if = "Option::is_none")] - pub python: Option, -} - -#[derive(Deserialize, Debug, Clone)] -#[serde(untagged)] -pub(crate) enum Log { - /// Enable logging with tracing when `true`. - Bool(bool), - /// Enable logging and only show filters that match the given [env-filter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) - Filter(String), -} - -/// The system to use for tests. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] -#[serde(rename_all = "kebab-case")] -pub(crate) enum SystemKind { - /// Use an in-memory system with a case sensitive file system.. - /// - /// This is recommended for all tests because it's fast. - #[default] - InMemory, - - /// Use the os system. - /// - /// This system should only be used when testing system or OS specific behavior. - Os, -} diff --git a/crates/red_knot_test/src/db.rs b/crates/red_knot_test/src/db.rs deleted file mode 100644 index 770be68128aa7..0000000000000 --- a/crates/red_knot_test/src/db.rs +++ /dev/null @@ -1,277 +0,0 @@ -use camino::{Utf8Component, Utf8PathBuf}; -use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; -use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb}; -use ruff_db::files::{File, Files}; -use ruff_db::system::{ - CaseSensitivity, DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath, - SystemPathBuf, WritableSystem, -}; -use ruff_db::vendored::VendoredFileSystem; -use ruff_db::{Db as SourceDb, Upcast}; -use ruff_notebook::{Notebook, NotebookError}; -use std::borrow::Cow; -use std::sync::Arc; -use tempfile::TempDir; - -#[salsa::db] -#[derive(Clone)] -pub(crate) struct Db { - storage: salsa::Storage, - files: Files, - system: MdtestSystem, - vendored: VendoredFileSystem, - rule_selection: Arc, -} - -impl Db { - pub(crate) fn setup() -> Self { - let rule_selection = RuleSelection::from_registry(default_lint_registry()); - - Self { - system: MdtestSystem::in_memory(), - storage: salsa::Storage::default(), - vendored: red_knot_vendored::file_system().clone(), - files: Files::default(), - rule_selection: Arc::new(rule_selection), - } - } - - pub(crate) fn use_os_system_with_temp_dir(&mut self, cwd: SystemPathBuf, temp_dir: TempDir) { - self.system.with_os(cwd, temp_dir); - Files::sync_all(self); - } - - pub(crate) fn use_in_memory_system(&mut self) { - self.system.with_in_memory(); - Files::sync_all(self); - } - - pub(crate) fn create_directory_all(&self, path: &SystemPath) -> ruff_db::system::Result<()> { - self.system.create_directory_all(path) - } -} - -#[salsa::db] -impl SourceDb for Db { - fn vendored(&self) -> &VendoredFileSystem { - &self.vendored - } - - fn system(&self) -> &dyn System { - &self.system - } - - fn files(&self) -> &Files { - &self.files - } -} - -impl Upcast for Db { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } -} - -#[salsa::db] -impl SemanticDb for Db { - fn is_file_open(&self, file: File) -> bool { - !file.path(self).is_vendored_path() - } - - fn rule_selection(&self) -> Arc { - self.rule_selection.clone() - } - - fn lint_registry(&self) -> &LintRegistry { - default_lint_registry() - } -} - -#[salsa::db] -impl salsa::Database for Db { - fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {} -} - -impl DbWithWritableSystem for Db { - type System = MdtestSystem; - fn writable_system(&self) -> &Self::System { - &self.system - } -} - -#[derive(Debug, Clone)] -pub(crate) struct MdtestSystem(Arc); - -#[derive(Debug)] -enum MdtestSystemInner { - InMemory(InMemorySystem), - Os { - os_system: OsSystem, - _temp_dir: TempDir, - }, -} - -impl MdtestSystem { - fn in_memory() -> Self { - Self(Arc::new(MdtestSystemInner::InMemory( - InMemorySystem::default(), - ))) - } - - fn as_system(&self) -> &dyn WritableSystem { - match &*self.0 { - MdtestSystemInner::InMemory(system) => system, - MdtestSystemInner::Os { os_system, .. } => os_system, - } - } - - fn with_os(&mut self, cwd: SystemPathBuf, temp_dir: TempDir) { - self.0 = Arc::new(MdtestSystemInner::Os { - os_system: OsSystem::new(cwd), - _temp_dir: temp_dir, - }); - } - - fn with_in_memory(&mut self) { - if let MdtestSystemInner::InMemory(in_memory) = &*self.0 { - in_memory.fs().remove_all(); - } else { - self.0 = Arc::new(MdtestSystemInner::InMemory(InMemorySystem::default())); - } - } - - fn normalize_path<'a>(&self, path: &'a SystemPath) -> Cow<'a, SystemPath> { - match &*self.0 { - MdtestSystemInner::InMemory(_) => Cow::Borrowed(path), - MdtestSystemInner::Os { os_system, .. } => { - // Make all paths relative to the current directory - // to avoid writing or reading from outside the temp directory. - let without_root: Utf8PathBuf = path - .components() - .skip_while(|component| { - matches!( - component, - Utf8Component::RootDir | Utf8Component::Prefix(..) - ) - }) - .collect(); - Cow::Owned(os_system.current_directory().join(&without_root)) - } - } - } -} - -impl System for MdtestSystem { - fn path_metadata( - &self, - path: &SystemPath, - ) -> ruff_db::system::Result { - self.as_system().path_metadata(&self.normalize_path(path)) - } - - fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result { - let canonicalized = self - .as_system() - .canonicalize_path(&self.normalize_path(path))?; - - if let MdtestSystemInner::Os { os_system, .. } = &*self.0 { - // Make the path relative to the current directory - Ok(canonicalized - .strip_prefix(os_system.current_directory()) - .unwrap() - .to_owned()) - } else { - Ok(canonicalized) - } - } - - fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result { - self.as_system().read_to_string(&self.normalize_path(path)) - } - - fn read_to_notebook(&self, path: &SystemPath) -> Result { - self.as_system() - .read_to_notebook(&self.normalize_path(path)) - } - - fn read_virtual_path_to_string( - &self, - path: &ruff_db::system::SystemVirtualPath, - ) -> ruff_db::system::Result { - self.as_system().read_virtual_path_to_string(path) - } - - fn read_virtual_path_to_notebook( - &self, - path: &ruff_db::system::SystemVirtualPath, - ) -> Result { - self.as_system().read_virtual_path_to_notebook(path) - } - - fn path_exists_case_sensitive(&self, path: &SystemPath, prefix: &SystemPath) -> bool { - self.as_system() - .path_exists_case_sensitive(&self.normalize_path(path), &self.normalize_path(prefix)) - } - - fn case_sensitivity(&self) -> CaseSensitivity { - self.as_system().case_sensitivity() - } - - fn current_directory(&self) -> &SystemPath { - self.as_system().current_directory() - } - - fn user_config_directory(&self) -> Option { - self.as_system().user_config_directory() - } - - fn read_directory<'a>( - &'a self, - path: &SystemPath, - ) -> ruff_db::system::Result< - Box> + 'a>, - > { - self.as_system().read_directory(&self.normalize_path(path)) - } - - fn walk_directory( - &self, - path: &SystemPath, - ) -> ruff_db::system::walk_directory::WalkDirectoryBuilder { - self.as_system().walk_directory(&self.normalize_path(path)) - } - - fn glob( - &self, - pattern: &str, - ) -> Result< - Box>>, - ruff_db::system::PatternError, - > { - self.as_system() - .glob(self.normalize_path(SystemPath::new(pattern)).as_str()) - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } -} - -impl WritableSystem for MdtestSystem { - fn write_file(&self, path: &SystemPath, content: &str) -> ruff_db::system::Result<()> { - self.as_system() - .write_file(&self.normalize_path(path), content) - } - - fn create_directory_all(&self, path: &SystemPath) -> ruff_db::system::Result<()> { - self.as_system() - .create_directory_all(&self.normalize_path(path)) - } -} diff --git a/crates/red_knot_test/src/lib.rs b/crates/red_knot_test/src/lib.rs deleted file mode 100644 index c31a68040a4fa..0000000000000 --- a/crates/red_knot_test/src/lib.rs +++ /dev/null @@ -1,445 +0,0 @@ -use crate::config::Log; -use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap}; -use camino::Utf8Path; -use colored::Colorize; -use config::SystemKind; -use parser as test_parser; -use red_knot_python_semantic::types::check_types; -use red_knot_python_semantic::{ - Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, SysPrefixPathOrigin, -}; -use ruff_db::diagnostic::{create_parse_diagnostic, Diagnostic, DisplayDiagnosticConfig}; -use ruff_db::files::{system_path_to_file, File}; -use ruff_db::panic::catch_unwind; -use ruff_db::parsed::parsed_module; -use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; -use ruff_db::testing::{setup_logging, setup_logging_with_filter}; -use ruff_source_file::{LineIndex, OneIndexed}; -use std::fmt::Write; - -mod assertion; -mod config; -mod db; -mod diagnostic; -mod matcher; -mod parser; - -const MDTEST_TEST_FILTER: &str = "MDTEST_TEST_FILTER"; - -/// Run `path` as a markdown test suite with given `title`. -/// -/// Panic on test failure, and print failure details. -#[allow(clippy::print_stdout)] -pub fn run( - absolute_fixture_path: &Utf8Path, - relative_fixture_path: &Utf8Path, - snapshot_path: &Utf8Path, - short_title: &str, - test_name: &str, - output_format: OutputFormat, -) { - let source = std::fs::read_to_string(absolute_fixture_path).unwrap(); - let suite = match test_parser::parse(short_title, &source) { - Ok(suite) => suite, - Err(err) => { - panic!("Error parsing `{absolute_fixture_path}`: {err:?}") - } - }; - - let mut db = db::Db::setup(); - - let filter = std::env::var(MDTEST_TEST_FILTER).ok(); - let mut any_failures = false; - for test in suite.tests() { - if filter.as_ref().is_some_and(|f| !test.name().contains(f)) { - continue; - } - - let _tracing = test.configuration().log.as_ref().and_then(|log| match log { - Log::Bool(enabled) => enabled.then(setup_logging), - Log::Filter(filter) => setup_logging_with_filter(filter), - }); - - if let Err(failures) = run_test(&mut db, relative_fixture_path, snapshot_path, &test) { - any_failures = true; - - if output_format.is_cli() { - println!("\n{}\n", test.name().bold().underline()); - } - - let md_index = LineIndex::from_source_text(&source); - - for test_failures in failures { - let source_map = - EmbeddedFileSourceMap::new(&md_index, test_failures.backtick_offsets); - - for (relative_line_number, failures) in test_failures.by_line.iter() { - let absolute_line_number = - source_map.to_absolute_line_number(relative_line_number); - - for failure in failures { - match output_format { - OutputFormat::Cli => { - let line_info = - format!("{relative_fixture_path}:{absolute_line_number}") - .cyan(); - println!(" {line_info} {failure}"); - } - OutputFormat::GitHub => println!( - "::error file={absolute_fixture_path},line={absolute_line_number}::{failure}" - ), - } - } - } - } - - let escaped_test_name = test.name().replace('\'', "\\'"); - - if output_format.is_cli() { - println!( - "\nTo rerun this specific test, set the environment variable: {MDTEST_TEST_FILTER}='{escaped_test_name}'", - ); - println!( - "{MDTEST_TEST_FILTER}='{escaped_test_name}' cargo test -p red_knot_python_semantic --test mdtest -- {test_name}", - ); - } - } - } - - println!("\n{}\n", "-".repeat(50)); - - assert!(!any_failures, "Some tests failed."); -} - -/// Defines the format in which mdtest should print an error to the terminal -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OutputFormat { - /// The format `cargo test` should use by default. - Cli, - /// A format that will provide annotations from GitHub Actions - /// if mdtest fails on a PR. - /// See - GitHub, -} - -impl OutputFormat { - const fn is_cli(self) -> bool { - matches!(self, OutputFormat::Cli) - } -} - -fn run_test( - db: &mut db::Db, - relative_fixture_path: &Utf8Path, - snapshot_path: &Utf8Path, - test: &parser::MarkdownTest, -) -> Result<(), Failures> { - // Initialize the system and remove all files and directories to reset the system to a clean state. - match test.configuration().system.unwrap_or_default() { - SystemKind::InMemory => { - db.use_in_memory_system(); - } - SystemKind::Os => { - let dir = tempfile::TempDir::new().expect("Creating a temporary directory to succeed"); - let root_path = dir - .path() - .canonicalize() - .expect("Canonicalizing to succeed"); - let root_path = SystemPathBuf::from_path_buf(root_path) - .expect("Temp directory to be a valid UTF8 path") - .simplified() - .to_path_buf(); - - db.use_os_system_with_temp_dir(root_path, dir); - } - } - - let project_root = SystemPathBuf::from("/src"); - db.create_directory_all(&project_root) - .expect("Creating the project root to succeed"); - - let src_path = project_root.clone(); - let custom_typeshed_path = test.configuration().typeshed(); - let python_path = test.configuration().python(); - let python_version = test.configuration().python_version().unwrap_or_default(); - - let mut typeshed_files = vec![]; - let mut has_custom_versions_file = false; - let mut has_custom_pyvenv_cfg_file = false; - - let test_files: Vec<_> = test - .files() - .filter_map(|embedded| { - if embedded.lang == "ignore" { - return None; - } - - assert!( - matches!(embedded.lang, "py" | "pyi" | "python" | "text" | "cfg"), - "Supported file types are: py (or python), pyi, text, cfg and ignore" - ); - - let mut full_path = embedded.full_path(&project_root); - - if let Some(typeshed_path) = custom_typeshed_path { - if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) { - if relative_path.as_str() == "VERSIONS" { - has_custom_versions_file = true; - } else if relative_path.extension().is_some_and(|ext| ext == "pyi") { - typeshed_files.push(relative_path.to_path_buf()); - } - } - } else if let Some(python_path) = python_path { - if let Ok(relative_path) = full_path.strip_prefix(python_path) { - if relative_path.as_str() == "pyvenv.cfg" { - has_custom_pyvenv_cfg_file = true; - } else { - let mut new_path = SystemPathBuf::new(); - for component in full_path.components() { - let component = component.as_str(); - if component == "" { - if cfg!(target_os = "windows") { - new_path.push("Lib"); - new_path.push("site-packages"); - } else { - new_path.push("lib"); - new_path.push(format!("python{python_version}")); - new_path.push("site-packages"); - } - } else { - new_path.push(component); - } - } - full_path = new_path; - } - } - } - - db.write_file(&full_path, &embedded.code).unwrap(); - - if !(full_path.starts_with(&src_path) && matches!(embedded.lang, "py" | "pyi")) { - // These files need to be written to the file system (above), but we don't run any checks on them. - return None; - } - - let file = system_path_to_file(db, full_path).unwrap(); - - Some(TestFile { - file, - backtick_offsets: embedded.backtick_offsets.clone(), - }) - }) - .collect(); - - // Create a custom typeshed `VERSIONS` file if none was provided. - if let Some(typeshed_path) = custom_typeshed_path { - if !has_custom_versions_file { - let versions_file = typeshed_path.join("stdlib/VERSIONS"); - let contents = typeshed_files - .iter() - .fold(String::new(), |mut content, path| { - // This is intentionally kept simple: - let module_name = path - .as_str() - .trim_end_matches(".pyi") - .trim_end_matches("/__init__") - .replace('/', "."); - let _ = writeln!(content, "{module_name}: 3.8-"); - content - }); - db.write_file(&versions_file, contents).unwrap(); - } - } - - if let Some(python_path) = python_path { - if !has_custom_pyvenv_cfg_file { - let pyvenv_cfg_file = python_path.join("pyvenv.cfg"); - let home_directory = SystemPathBuf::from(format!("/Python{python_version}")); - db.create_directory_all(&home_directory).unwrap(); - db.write_file(&pyvenv_cfg_file, format!("home = {home_directory}")) - .unwrap(); - } - } - - let configuration = test.configuration(); - - let settings = ProgramSettings { - python_version, - python_platform: configuration - .python_platform() - .unwrap_or(PythonPlatform::Identifier("linux".to_string())), - search_paths: SearchPathSettings { - src_roots: vec![src_path], - extra_paths: configuration.extra_paths().unwrap_or_default().to_vec(), - custom_typeshed: custom_typeshed_path.map(SystemPath::to_path_buf), - python_path: configuration - .python() - .map(|sys_prefix| { - PythonPath::SysPrefix( - sys_prefix.to_path_buf(), - SysPrefixPathOrigin::PythonCliFlag, - ) - }) - .unwrap_or(PythonPath::KnownSitePackages(vec![])), - }, - }; - - match Program::try_get(db) { - Some(program) => program.update_from_settings(db, settings), - None => Program::from_settings(db, settings).map(|_| ()), - } - .expect("Failed to update Program settings in TestDb"); - - // When snapshot testing is enabled, this is populated with - // all diagnostics. Otherwise it remains empty. - let mut snapshot_diagnostics = vec![]; - - let failures: Failures = test_files - .into_iter() - .filter_map(|test_file| { - let parsed = parsed_module(db, test_file.file); - - let mut diagnostics: Vec = parsed - .errors() - .iter() - .map(|error| create_parse_diagnostic(test_file.file, error)) - .collect(); - - let type_diagnostics = match catch_unwind(|| check_types(db, test_file.file)) { - Ok(type_diagnostics) => type_diagnostics, - Err(info) => { - let mut by_line = matcher::FailuresByLine::default(); - let mut messages = vec![]; - match info.location { - Some(location) => messages.push(format!("panicked at {location}")), - None => messages.push("panicked at unknown location".to_string()), - } - match info.payload { - Some(payload) => messages.push(payload), - // Mimic the default panic hook's rendering of the panic payload if it's - // not a string. - None => messages.push("Box".to_string()), - } - if let Some(backtrace) = info.backtrace { - if std::env::var("RUST_BACKTRACE").is_ok() { - messages.extend(backtrace.to_string().split('\n').map(String::from)); - } - } - by_line.push(OneIndexed::from_zero_indexed(0), messages); - return Some(FileFailures { - backtick_offsets: test_file.backtick_offsets, - by_line, - }); - } - }; - diagnostics.extend(type_diagnostics.into_iter().cloned()); - - let failure = match matcher::match_file(db, test_file.file, &diagnostics) { - Ok(()) => None, - Err(line_failures) => Some(FileFailures { - backtick_offsets: test_file.backtick_offsets, - by_line: line_failures, - }), - }; - if test.should_snapshot_diagnostics() { - snapshot_diagnostics.extend(diagnostics); - } - failure - }) - .collect(); - - if snapshot_diagnostics.is_empty() && test.should_snapshot_diagnostics() { - panic!( - "Test `{}` requested snapshotting diagnostics but it didn't produce any.", - test.name() - ); - } else if !snapshot_diagnostics.is_empty() { - let snapshot = - create_diagnostic_snapshot(db, relative_fixture_path, test, snapshot_diagnostics); - let name = test.name().replace(' ', "_").replace(':', "__"); - insta::with_settings!( - { - snapshot_path => snapshot_path, - input_file => name.clone(), - filters => vec![(r"\\", "/")], - prepend_module_to_snapshot => false, - }, - { insta::assert_snapshot!(name, snapshot) } - ); - } - - if failures.is_empty() { - Ok(()) - } else { - Err(failures) - } -} - -type Failures = Vec; - -/// The failures for a single file in a test by line number. -struct FileFailures { - /// Positional information about the code block(s) to reconstruct absolute line numbers. - backtick_offsets: Vec, - - /// The failures by lines in the file. - by_line: matcher::FailuresByLine, -} - -/// File in a test. -struct TestFile { - file: File, - - /// Positional information about the code block(s) to reconstruct absolute line numbers. - backtick_offsets: Vec, -} - -fn create_diagnostic_snapshot( - db: &mut db::Db, - relative_fixture_path: &Utf8Path, - test: &parser::MarkdownTest, - diagnostics: impl IntoIterator, -) -> String { - let display_config = DisplayDiagnosticConfig::default().color(false); - - let mut snapshot = String::new(); - writeln!(snapshot).unwrap(); - writeln!(snapshot, "---").unwrap(); - writeln!(snapshot, "mdtest name: {}", test.name()).unwrap(); - writeln!(snapshot, "mdtest path: {relative_fixture_path}").unwrap(); - writeln!(snapshot, "---").unwrap(); - writeln!(snapshot).unwrap(); - - writeln!(snapshot, "# Python source files").unwrap(); - writeln!(snapshot).unwrap(); - for file in test.files() { - writeln!(snapshot, "## {}", file.relative_path()).unwrap(); - writeln!(snapshot).unwrap(); - // Note that we don't use ```py here because the line numbering - // we add makes it invalid Python. This sacrifices syntax - // highlighting when you look at the snapshot on GitHub, - // but the line numbers are extremely useful for analyzing - // snapshots. So we keep them. - writeln!(snapshot, "```").unwrap(); - - let line_number_width = file.code.lines().count().to_string().len(); - for (i, line) in file.code.lines().enumerate() { - let line_number = i + 1; - writeln!(snapshot, "{line_number:>line_number_width$} | {line}").unwrap(); - } - writeln!(snapshot, "```").unwrap(); - writeln!(snapshot).unwrap(); - } - - writeln!(snapshot, "# Diagnostics").unwrap(); - writeln!(snapshot).unwrap(); - for (i, diag) in diagnostics.into_iter().enumerate() { - if i > 0 { - writeln!(snapshot).unwrap(); - } - writeln!(snapshot, "```").unwrap(); - write!(snapshot, "{}", diag.display(db, &display_config)).unwrap(); - writeln!(snapshot, "```").unwrap(); - } - snapshot -} diff --git a/crates/red_knot_vendored/Cargo.toml b/crates/red_knot_vendored/Cargo.toml deleted file mode 100644 index b740bef3f4db5..0000000000000 --- a/crates/red_knot_vendored/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "red_knot_vendored" -version = "0.0.0" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[dependencies] -ruff_db = { workspace = true } -zip = { workspace = true } - -[build-dependencies] -path-slash = { workspace = true } -walkdir = { workspace = true } -zip = { workspace = true, features = ["zstd", "deflate"] } - -[dev-dependencies] -walkdir = { workspace = true } - -[features] -zstd = ["zip/zstd"] -deflate = ["zip/deflate"] - -[lints] -workspace = true - diff --git a/crates/red_knot_vendored/README.md b/crates/red_knot_vendored/README.md deleted file mode 100644 index dd9a5849b00bd..0000000000000 --- a/crates/red_knot_vendored/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Vendored types for the stdlib - -This crate vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/red_knot_vendored/vendor/typeshed`. The file `crates/red_knot_vendored/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to. - -The typeshed stubs are updated every two weeks via an automated PR using the `sync_typeshed.yaml` workflow in the `.github/workflows` directory. This workflow can also be triggered at any time via [workflow dispatch](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow#running-a-workflow). diff --git a/crates/red_knot_vendored/build.rs b/crates/red_knot_vendored/build.rs deleted file mode 100644 index 2062db3d6e76a..0000000000000 --- a/crates/red_knot_vendored/build.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Build script to package our vendored typeshed files -//! into a zip archive that can be included in the Ruff binary. -//! -//! This script should be automatically run at build time -//! whenever the script itself changes, or whenever any files -//! in `crates/red_knot_vendored/vendor/typeshed` change. - -use std::fs::File; -use std::io::Write; -use std::path::Path; - -use path_slash::PathExt; -use zip::result::ZipResult; -use zip::write::{FileOptions, ZipWriter}; -use zip::CompressionMethod; - -const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed"; -const KNOT_EXTENSIONS_STUBS: &str = "knot_extensions/knot_extensions.pyi"; -const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip"; - -/// Recursively zip the contents of the entire typeshed directory and patch typeshed -/// on the fly to include the `knot_extensions` module. -/// -/// This routine is adapted from a recipe at -/// -fn write_zipped_typeshed_to(writer: File) -> ZipResult { - let mut zip = ZipWriter::new(writer); - - // Use deflated compression for WASM builds because compiling `zstd-sys` requires clang - // [source](https://github.com/gyscos/zstd-rs/wiki/Compile-for-WASM) which complicates the build - // by a lot. Deflated compression is slower but it shouldn't matter much for the WASM use case - // (WASM itself is already slower than a native build for a specific platform). - // We can't use `#[cfg(...)]` here because the target-arch in a build script is the - // architecture of the system running the build script and not the architecture of the build-target. - // That's why we use the `TARGET` environment variable here. - let method = if cfg!(feature = "zstd") { - CompressionMethod::Zstd - } else if cfg!(feature = "deflate") { - CompressionMethod::Deflated - } else { - CompressionMethod::Stored - }; - - let options = FileOptions::default() - .compression_method(method) - .unix_permissions(0o644); - - for entry in walkdir::WalkDir::new(TYPESHED_SOURCE_DIR) { - let dir_entry = entry.unwrap(); - let absolute_path = dir_entry.path(); - let normalized_relative_path = absolute_path - .strip_prefix(Path::new(TYPESHED_SOURCE_DIR)) - .unwrap() - .to_slash() - .expect("Unexpected non-utf8 typeshed path!"); - - // Write file or directory explicitly - // Some unzip tools unzip files with directory paths correctly, some do not! - if absolute_path.is_file() { - println!("adding file {absolute_path:?} as {normalized_relative_path:?} ..."); - zip.start_file(&*normalized_relative_path, options)?; - let mut f = File::open(absolute_path)?; - std::io::copy(&mut f, &mut zip).unwrap(); - - // Patch the VERSIONS file to make `knot_extensions` available - if normalized_relative_path == "stdlib/VERSIONS" { - writeln!(&mut zip, "knot_extensions: 3.0-")?; - } - } else if !normalized_relative_path.is_empty() { - // Only if not root! Avoids path spec / warning - // and mapname conversion failed error on unzip - println!("adding dir {absolute_path:?} as {normalized_relative_path:?} ..."); - zip.add_directory(normalized_relative_path, options)?; - } - } - - // Patch typeshed and add the stubs for the `knot_extensions` module - println!("adding file {KNOT_EXTENSIONS_STUBS} as stdlib/knot_extensions.pyi ..."); - zip.start_file("stdlib/knot_extensions.pyi", options)?; - let mut f = File::open(KNOT_EXTENSIONS_STUBS)?; - std::io::copy(&mut f, &mut zip).unwrap(); - - zip.finish() -} - -fn main() { - assert!( - Path::new(TYPESHED_SOURCE_DIR).is_dir(), - "Where is typeshed?" - ); - let out_dir = std::env::var("OUT_DIR").unwrap(); - - // N.B. Deliberately using `format!()` instead of `Path::join()` here, - // so that we use `/` as a path separator on all platforms. - // That enables us to load the typeshed zip at compile time in `module.rs` - // (otherwise we'd have to dynamically determine the exact path to the typeshed zip - // based on the default path separator for the specific platform we're on, - // which can't be done at compile time.) - let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}"); - - let zipped_typeshed_file = File::create(zipped_typeshed_location).unwrap(); - write_zipped_typeshed_to(zipped_typeshed_file).unwrap(); -} diff --git a/crates/red_knot_vendored/knot_extensions/README.md b/crates/red_knot_vendored/knot_extensions/README.md deleted file mode 100644 index d26f2802b53d3..0000000000000 --- a/crates/red_knot_vendored/knot_extensions/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The `knot_extensions.pyi` file in this directory will be symlinked into -the `vendor/typeshed/stdlib` directory every time we sync our `typeshed` -stubs (see `.github/workflows/sync_typeshed.yaml`). diff --git a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi deleted file mode 100644 index d7e69093d8ac0..0000000000000 --- a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, LiteralString, _SpecialForm - -# Special operations -def static_assert(condition: object, msg: LiteralString | None = None) -> None: ... - -# Types -Unknown = object() -AlwaysTruthy = object() -AlwaysFalsy = object() - -# Special forms -Not: _SpecialForm -Intersection: _SpecialForm -TypeOf: _SpecialForm -CallableTypeOf: _SpecialForm - -# Predicates on types -# -# Ideally, these would be annotated using `TypeForm`, but that has not been -# standardized yet (https://peps.python.org/pep-0747). -def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ... -def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ... -def is_assignable_to(type_target: Any, type_source: Any) -> bool: ... -def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ... -def is_gradual_equivalent_to(type_a: Any, type_b: Any) -> bool: ... -def is_fully_static(type: Any) -> bool: ... -def is_singleton(type: Any) -> bool: ... -def is_single_valued(type: Any) -> bool: ... diff --git a/crates/red_knot_vendored/src/lib.rs b/crates/red_knot_vendored/src/lib.rs deleted file mode 100644 index d1a5816ec2383..0000000000000 --- a/crates/red_knot_vendored/src/lib.rs +++ /dev/null @@ -1,98 +0,0 @@ -use ruff_db::vendored::VendoredFileSystem; -use std::sync::LazyLock; - -// The file path here is hardcoded in this crate's `build.rs` script. -// Luckily this crate will fail to build if this file isn't available at build time. -static TYPESHED_ZIP_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip")); - -pub fn file_system() -> &'static VendoredFileSystem { - static VENDORED_TYPESHED_STUBS: LazyLock = - LazyLock::new(|| VendoredFileSystem::new_static(TYPESHED_ZIP_BYTES).unwrap()); - &VENDORED_TYPESHED_STUBS -} - -#[cfg(test)] -mod tests { - use std::io::{self, Read}; - use std::path::Path; - - use ruff_db::vendored::VendoredPath; - - use super::*; - - #[test] - fn typeshed_zip_created_at_build_time() { - let mut typeshed_zip_archive = - zip::ZipArchive::new(io::Cursor::new(TYPESHED_ZIP_BYTES)).unwrap(); - - let mut functools_module_stub = typeshed_zip_archive - .by_name("stdlib/functools.pyi") - .unwrap(); - assert!(functools_module_stub.is_file()); - - let mut functools_module_stub_source = String::new(); - functools_module_stub - .read_to_string(&mut functools_module_stub_source) - .unwrap(); - - assert!(functools_module_stub_source.contains("def update_wrapper(")); - } - - #[test] - fn typeshed_vfs_consistent_with_vendored_stubs() { - let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap(); - let vendored_typeshed_stubs = file_system(); - - let mut empty_iterator = true; - for entry in walkdir::WalkDir::new(&vendored_typeshed_dir).min_depth(1) { - empty_iterator = false; - let entry = entry.unwrap(); - let absolute_path = entry.path(); - let file_type = entry.file_type(); - - let relative_path = absolute_path - .strip_prefix(&vendored_typeshed_dir) - .unwrap_or_else(|_| { - panic!("Expected {absolute_path:?} to be a child of {vendored_typeshed_dir:?}") - }); - - let vendored_path = <&VendoredPath>::try_from(relative_path) - .unwrap_or_else(|_| panic!("Expected {relative_path:?} to be valid UTF-8")); - - assert!( - vendored_typeshed_stubs.exists(vendored_path), - "Expected {vendored_path:?} to exist in the `VendoredFileSystem`! - - Vendored file system: - - {vendored_typeshed_stubs:#?} - " - ); - - let vendored_path_kind = vendored_typeshed_stubs - .metadata(vendored_path) - .unwrap_or_else(|_| { - panic!( - "Expected metadata for {vendored_path:?} to be retrievable from the `VendoredFileSystem! - - Vendored file system: - - {vendored_typeshed_stubs:#?} - " - ) - }) - .kind(); - - assert_eq!( - vendored_path_kind.is_directory(), - file_type.is_dir(), - "{vendored_path:?} had type {vendored_path_kind:?}, inconsistent with fs path {relative_path:?}: {file_type:?}" - ); - } - - assert!( - !empty_iterator, - "Expected there to be at least one file or directory in the vendored typeshed stubs!" - ); - } -} diff --git a/crates/red_knot_vendored/vendor/typeshed/README.md b/crates/red_knot_vendored/vendor/typeshed/README.md deleted file mode 100644 index b52ecf3a5de98..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/README.md +++ /dev/null @@ -1,124 +0,0 @@ -# typeshed - -[![Tests](https://github.com/python/typeshed/actions/workflows/tests.yml/badge.svg)](https://github.com/python/typeshed/actions/workflows/tests.yml) -[![Chat at https://gitter.im/python/typing](https://badges.gitter.im/python/typing.svg)](https://gitter.im/python/typing?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Pull Requests Welcome](https://img.shields.io/badge/pull%20requests-welcome-brightgreen.svg)](https://github.com/python/typeshed/blob/main/CONTRIBUTING.md) - -## About - -Typeshed contains external type annotations for the Python standard library -and Python builtins, as well as third party packages as contributed by -people external to those projects. - -This data can e.g. be used for static analysis, type checking, type inference, -and autocompletion. - -For information on how to use typeshed, read below. Information for -contributors can be found in [CONTRIBUTING.md](CONTRIBUTING.md). **Please read -it before submitting pull requests; do not report issues with annotations to -the project the stubs are for, but instead report them here to typeshed.** - -Further documentation on stub files, typeshed, and Python's typing system in -general, can also be found at https://typing.readthedocs.io/en/latest/. - -Typeshed supports Python versions 3.9 to 3.13. - -## Using - -If you're just using a type checker ([mypy](https://github.com/python/mypy/), -[pyright](https://github.com/microsoft/pyright), -[pytype](https://github.com/google/pytype/), PyCharm, ...), as opposed to -developing it, you don't need to interact with the typeshed repo at -all: a copy of standard library part of typeshed is bundled with type checkers. -And type stubs for third party packages and modules you are using can -be installed from PyPI. For example, if you are using `html5lib` and `requests`, -you can install the type stubs using - -```bash -$ pip install types-html5lib types-requests -``` - -These PyPI packages follow [PEP 561](http://www.python.org/dev/peps/pep-0561/) -and are automatically released (up to once a day) by -[typeshed internal machinery](https://github.com/typeshed-internal/stub_uploader). - -Type checkers should be able to use these stub packages when installed. For more -details, see the documentation for your type checker. - -### Package versioning for third-party stubs - -Version numbers of third-party stub packages consist of at least four parts. -All parts of the stub version, except for the last part, correspond to the -version of the runtime package being stubbed. For example, if the `types-foo` -package has version `1.2.0.20240309`, this guarantees that the `types-foo` package -contains stubs targeted against `foo==1.2.*` and tested against the latest -version of `foo` matching that specifier. In this example, the final element -of the version number (20240309) indicates that the stub package was pushed on -March 9, 2024. - -At typeshed, we try to keep breaking changes to a minimum. However, due to the -nature of stubs, any version bump can introduce changes that might make your -code fail to type check. - -There are several strategies available for specifying the version of a stubs -package you're using, each with its own tradeoffs: - -1. Use the same bounds that you use for the package being stubbed. For example, - if you use `requests>=2.30.0,<2.32`, you can use - `types-requests>=2.30.0,<2.32`. This ensures that the stubs are compatible - with the package you are using, but it carries a small risk of breaking - type checking due to changes in the stubs. - - Another risk of this strategy is that stubs often lag behind - the package being stubbed. You might want to force the package being stubbed - to a certain minimum version because it fixes a critical bug, but if - correspondingly updated stubs have not been released, your type - checking results may not be fully accurate. -2. Pin the stubs to a known good version and update the pin from time to time - (either manually, or using a tool such as dependabot or renovate). - - For example, if you use `types-requests==2.31.0.1`, you can have confidence - that upgrading dependencies will not break type checking. However, you will - miss out on improvements in the stubs that could potentially improve type - checking until you update the pin. This strategy also has the risk that the - stubs you are using might become incompatible with the package being stubbed. -3. Don't pin the stubs. This is the option that demands the least work from - you when it comes to updating version pins, and has the advantage that you - will automatically benefit from improved stubs whenever a new version of the - stubs package is released. However, it carries the risk that the stubs - become incompatible with the package being stubbed. - - For example, if a new major version of the package is released, there's a - chance the stubs might be updated to reflect the new version of the runtime - package before you update the package being stubbed. - -You can also switch between the different strategies as needed. For example, -you could default to strategy (1), but fall back to strategy (2) when -a problem arises that can't easily be fixed. - -### The `_typeshed` package - -typeshed includes a package `_typeshed` as part of the standard library. -This package and its submodules contain utility types, but are not -available at runtime. For more information about how to use this package, -[see the `stdlib/_typeshed` directory](https://github.com/python/typeshed/tree/main/stdlib/_typeshed). - -## Discussion - -If you've run into behavior in the type checker that suggests the type -stubs for a given library are incorrect or incomplete, -we want to hear from you! - -Our main forum for discussion is the project's [GitHub issue -tracker](https://github.com/python/typeshed/issues). This is the right -place to start a discussion of any of the above or most any other -topic concerning the project. - -If you have general questions about typing with Python, or you need -a review of your type annotations or stubs outside of typeshed, head over to -[our discussion forum](https://github.com/python/typing/discussions). -For less formal discussion, try the typing chat room on -[gitter.im](https://gitter.im/python/typing). Some typeshed maintainers -are almost always present; feel free to find us there and we're happy -to chat. Substantive technical discussion will be directed to the -issue tracker. diff --git a/crates/red_knot_vendored/vendor/typeshed/source_commit.txt b/crates/red_knot_vendored/vendor/typeshed/source_commit.txt deleted file mode 100644 index 1c8127c897890..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/source_commit.txt +++ /dev/null @@ -1 +0,0 @@ -f65bdc1acde54fda93c802459280da74518d2eef diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/__main__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/__main__.pyi deleted file mode 100644 index e27843e533821..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/__main__.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any - -def __getattr__(name: str) -> Any: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_compression.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/_compression.pyi deleted file mode 100644 index a41a8142cc3ac..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_compression.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from _typeshed import WriteableBuffer -from collections.abc import Callable -from io import DEFAULT_BUFFER_SIZE, BufferedIOBase, RawIOBase -from typing import Any, Protocol - -BUFFER_SIZE = DEFAULT_BUFFER_SIZE - -class _Reader(Protocol): - def read(self, n: int, /) -> bytes: ... - def seekable(self) -> bool: ... - def seek(self, n: int, /) -> Any: ... - -class BaseStream(BufferedIOBase): ... - -class DecompressReader(RawIOBase): - def __init__( - self, - fp: _Reader, - decomp_factory: Callable[..., object], - trailing_error: type[Exception] | tuple[type[Exception], ...] = (), - **decomp_args: Any, - ) -> None: ... - def readinto(self, b: WriteableBuffer) -> int: ... - def read(self, size: int = -1) -> bytes: ... - def seek(self, offset: int, whence: int = 0) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_hashlib.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/_hashlib.pyi deleted file mode 100644 index 746b1657e2dbf..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_hashlib.pyi +++ /dev/null @@ -1,82 +0,0 @@ -import sys -from _typeshed import ReadableBuffer -from collections.abc import Callable -from types import ModuleType -from typing import AnyStr, Protocol, final, overload, type_check_only -from typing_extensions import Self, TypeAlias - -_DigestMod: TypeAlias = str | Callable[[], _HashObject] | ModuleType | None - -openssl_md_meth_names: frozenset[str] - -@type_check_only -class _HashObject(Protocol): - @property - def digest_size(self) -> int: ... - @property - def block_size(self) -> int: ... - @property - def name(self) -> str: ... - def copy(self) -> Self: ... - def digest(self) -> bytes: ... - def hexdigest(self) -> str: ... - def update(self, obj: ReadableBuffer, /) -> None: ... - -class HASH: - @property - def digest_size(self) -> int: ... - @property - def block_size(self) -> int: ... - @property - def name(self) -> str: ... - def copy(self) -> Self: ... - def digest(self) -> bytes: ... - def hexdigest(self) -> str: ... - def update(self, obj: ReadableBuffer, /) -> None: ... - -if sys.version_info >= (3, 10): - class UnsupportedDigestmodError(ValueError): ... - -class HASHXOF(HASH): - def digest(self, length: int) -> bytes: ... # type: ignore[override] - def hexdigest(self, length: int) -> str: ... # type: ignore[override] - -@final -class HMAC: - @property - def digest_size(self) -> int: ... - @property - def block_size(self) -> int: ... - @property - def name(self) -> str: ... - def copy(self) -> Self: ... - def digest(self) -> bytes: ... - def hexdigest(self) -> str: ... - def update(self, msg: ReadableBuffer) -> None: ... - -@overload -def compare_digest(a: ReadableBuffer, b: ReadableBuffer, /) -> bool: ... -@overload -def compare_digest(a: AnyStr, b: AnyStr, /) -> bool: ... -def get_fips_mode() -> int: ... -def hmac_new(key: bytes | bytearray, msg: ReadableBuffer = b"", digestmod: _DigestMod = None) -> HMAC: ... -def new(name: str, string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_md5(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha1(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha224(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha384(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha512(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha3_224(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha3_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha3_384(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_sha3_512(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... -def openssl_shake_128(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ... -def openssl_shake_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ... -def hmac_digest(key: bytes | bytearray, msg: ReadableBuffer, digest: str) -> bytes: ... -def pbkdf2_hmac( - hash_name: str, password: ReadableBuffer, salt: ReadableBuffer, iterations: int, dklen: int | None = None -) -> bytes: ... -def scrypt( - password: ReadableBuffer, *, salt: ReadableBuffer, n: int, r: int, p: int, maxmem: int = 0, dklen: int = 64 -) -> bytes: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_heapq.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/_heapq.pyi deleted file mode 100644 index 9f731bf91eefd..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_heapq.pyi +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any, Final, TypeVar - -_T = TypeVar("_T") - -__about__: Final[str] - -def heapify(heap: list[Any], /) -> None: ... -def heappop(heap: list[_T], /) -> _T: ... -def heappush(heap: list[_T], item: _T, /) -> None: ... -def heappushpop(heap: list[_T], item: _T, /) -> _T: ... -def heapreplace(heap: list[_T], item: _T, /) -> _T: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi deleted file mode 100644 index df05dcd80be80..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi +++ /dev/null @@ -1,32 +0,0 @@ -import sys -from _typeshed import StrOrBytesPath -from collections.abc import Callable, Sequence -from typing import SupportsIndex - -if sys.platform != "win32": - def fork_exec( - args: Sequence[StrOrBytesPath] | None, - executable_list: Sequence[bytes], - close_fds: bool, - pass_fds: tuple[int, ...], - cwd: str, - env: Sequence[bytes] | None, - p2cread: int, - p2cwrite: int, - c2pread: int, - c2pwrite: int, - errread: int, - errwrite: int, - errpipe_read: int, - errpipe_write: int, - restore_signals: int, - call_setsid: int, - pgid_to_set: int, - gid: SupportsIndex | None, - extra_groups: list[int] | None, - uid: SupportsIndex | None, - child_umask: int, - preexec_fn: Callable[[], None], - allow_vfork: bool, - /, - ) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi deleted file mode 100644 index a503637998d02..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi +++ /dev/null @@ -1,366 +0,0 @@ -# Utility types for typeshed -# -# See the README.md file in this directory for more information. - -import sys -from collections.abc import Awaitable, Callable, Iterable, Sequence, Set as AbstractSet, Sized -from dataclasses import Field -from os import PathLike -from types import FrameType, TracebackType -from typing import ( - Any, - AnyStr, - ClassVar, - Final, - Generic, - Literal, - Protocol, - SupportsFloat, - SupportsIndex, - SupportsInt, - TypeVar, - final, - overload, -) -from typing_extensions import Buffer, LiteralString, Self as _Self, TypeAlias - -_KT = TypeVar("_KT") -_KT_co = TypeVar("_KT_co", covariant=True) -_KT_contra = TypeVar("_KT_contra", contravariant=True) -_VT = TypeVar("_VT") -_VT_co = TypeVar("_VT_co", covariant=True) -_T = TypeVar("_T") -_T_co = TypeVar("_T_co", covariant=True) -_T_contra = TypeVar("_T_contra", contravariant=True) - -# Alternative to `typing_extensions.Self`, exclusively for use with `__new__` -# in metaclasses: -# def __new__(cls: type[Self], ...) -> Self: ... -# In other cases, use `typing_extensions.Self`. -Self = TypeVar("Self") # noqa: Y001 - -# covariant version of typing.AnyStr, useful for protocols -AnyStr_co = TypeVar("AnyStr_co", str, bytes, covariant=True) # noqa: Y001 - -# For partially known annotations. Usually, fields where type annotations -# haven't been added are left unannotated, but in some situations this -# isn't possible or a type is already partially known. In cases like these, -# use Incomplete instead of Any as a marker. For example, use -# "Incomplete | None" instead of "Any | None". -Incomplete: TypeAlias = Any # stable - -# To describe a function parameter that is unused and will work with anything. -Unused: TypeAlias = object # stable - -# Marker for return types that include None, but where forcing the user to -# check for None can be detrimental. Sometimes called "the Any trick". See -# CONTRIBUTING.md for more information. -MaybeNone: TypeAlias = Any # stable - -# Used to mark arguments that default to a sentinel value. This prevents -# stubtest from complaining about the default value not matching. -# -# def foo(x: int | None = sentinel) -> None: ... -# -# In cases where the sentinel object is exported and can be used by user code, -# a construct like this is better: -# -# _SentinelType = NewType("_SentinelType", object) -# sentinel: _SentinelType -# def foo(x: int | None | _SentinelType = ...) -> None: ... -sentinel: Any - -# stable -class IdentityFunction(Protocol): - def __call__(self, x: _T, /) -> _T: ... - -# stable -class SupportsNext(Protocol[_T_co]): - def __next__(self) -> _T_co: ... - -# stable -class SupportsAnext(Protocol[_T_co]): - def __anext__(self) -> Awaitable[_T_co]: ... - -# Comparison protocols - -class SupportsDunderLT(Protocol[_T_contra]): - def __lt__(self, other: _T_contra, /) -> bool: ... - -class SupportsDunderGT(Protocol[_T_contra]): - def __gt__(self, other: _T_contra, /) -> bool: ... - -class SupportsDunderLE(Protocol[_T_contra]): - def __le__(self, other: _T_contra, /) -> bool: ... - -class SupportsDunderGE(Protocol[_T_contra]): - def __ge__(self, other: _T_contra, /) -> bool: ... - -class SupportsAllComparisons( - SupportsDunderLT[Any], SupportsDunderGT[Any], SupportsDunderLE[Any], SupportsDunderGE[Any], Protocol -): ... - -SupportsRichComparison: TypeAlias = SupportsDunderLT[Any] | SupportsDunderGT[Any] -SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichComparison) # noqa: Y001 - -# Dunder protocols - -class SupportsAdd(Protocol[_T_contra, _T_co]): - def __add__(self, x: _T_contra, /) -> _T_co: ... - -class SupportsRAdd(Protocol[_T_contra, _T_co]): - def __radd__(self, x: _T_contra, /) -> _T_co: ... - -class SupportsSub(Protocol[_T_contra, _T_co]): - def __sub__(self, x: _T_contra, /) -> _T_co: ... - -class SupportsRSub(Protocol[_T_contra, _T_co]): - def __rsub__(self, x: _T_contra, /) -> _T_co: ... - -class SupportsMul(Protocol[_T_contra, _T_co]): - def __mul__(self, x: _T_contra, /) -> _T_co: ... - -class SupportsRMul(Protocol[_T_contra, _T_co]): - def __rmul__(self, x: _T_contra, /) -> _T_co: ... - -class SupportsDivMod(Protocol[_T_contra, _T_co]): - def __divmod__(self, other: _T_contra, /) -> _T_co: ... - -class SupportsRDivMod(Protocol[_T_contra, _T_co]): - def __rdivmod__(self, other: _T_contra, /) -> _T_co: ... - -# This protocol is generic over the iterator type, while Iterable is -# generic over the type that is iterated over. -class SupportsIter(Protocol[_T_co]): - def __iter__(self) -> _T_co: ... - -# This protocol is generic over the iterator type, while AsyncIterable is -# generic over the type that is iterated over. -class SupportsAiter(Protocol[_T_co]): - def __aiter__(self) -> _T_co: ... - -class SupportsLenAndGetItem(Protocol[_T_co]): - def __len__(self) -> int: ... - def __getitem__(self, k: int, /) -> _T_co: ... - -class SupportsTrunc(Protocol): - def __trunc__(self) -> int: ... - -# Mapping-like protocols - -# stable -class SupportsItems(Protocol[_KT_co, _VT_co]): - def items(self) -> AbstractSet[tuple[_KT_co, _VT_co]]: ... - -# stable -class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): - def keys(self) -> Iterable[_KT]: ... - def __getitem__(self, key: _KT, /) -> _VT_co: ... - -# stable -class SupportsGetItem(Protocol[_KT_contra, _VT_co]): - def __getitem__(self, key: _KT_contra, /) -> _VT_co: ... - -# stable -class SupportsContainsAndGetItem(Protocol[_KT_contra, _VT_co]): - def __contains__(self, x: Any, /) -> bool: ... - def __getitem__(self, key: _KT_contra, /) -> _VT_co: ... - -# stable -class SupportsItemAccess(Protocol[_KT_contra, _VT]): - def __contains__(self, x: Any, /) -> bool: ... - def __getitem__(self, key: _KT_contra, /) -> _VT: ... - def __setitem__(self, key: _KT_contra, value: _VT, /) -> None: ... - def __delitem__(self, key: _KT_contra, /) -> None: ... - -StrPath: TypeAlias = str | PathLike[str] # stable -BytesPath: TypeAlias = bytes | PathLike[bytes] # stable -GenericPath: TypeAlias = AnyStr | PathLike[AnyStr] -StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] # stable - -OpenTextModeUpdating: TypeAlias = Literal[ - "r+", - "+r", - "rt+", - "r+t", - "+rt", - "tr+", - "t+r", - "+tr", - "w+", - "+w", - "wt+", - "w+t", - "+wt", - "tw+", - "t+w", - "+tw", - "a+", - "+a", - "at+", - "a+t", - "+at", - "ta+", - "t+a", - "+ta", - "x+", - "+x", - "xt+", - "x+t", - "+xt", - "tx+", - "t+x", - "+tx", -] -OpenTextModeWriting: TypeAlias = Literal["w", "wt", "tw", "a", "at", "ta", "x", "xt", "tx"] -OpenTextModeReading: TypeAlias = Literal["r", "rt", "tr", "U", "rU", "Ur", "rtU", "rUt", "Urt", "trU", "tUr", "Utr"] -OpenTextMode: TypeAlias = OpenTextModeUpdating | OpenTextModeWriting | OpenTextModeReading -OpenBinaryModeUpdating: TypeAlias = Literal[ - "rb+", - "r+b", - "+rb", - "br+", - "b+r", - "+br", - "wb+", - "w+b", - "+wb", - "bw+", - "b+w", - "+bw", - "ab+", - "a+b", - "+ab", - "ba+", - "b+a", - "+ba", - "xb+", - "x+b", - "+xb", - "bx+", - "b+x", - "+bx", -] -OpenBinaryModeWriting: TypeAlias = Literal["wb", "bw", "ab", "ba", "xb", "bx"] -OpenBinaryModeReading: TypeAlias = Literal["rb", "br", "rbU", "rUb", "Urb", "brU", "bUr", "Ubr"] -OpenBinaryMode: TypeAlias = OpenBinaryModeUpdating | OpenBinaryModeReading | OpenBinaryModeWriting - -# stable -class HasFileno(Protocol): - def fileno(self) -> int: ... - -FileDescriptor: TypeAlias = int # stable -FileDescriptorLike: TypeAlias = int | HasFileno # stable -FileDescriptorOrPath: TypeAlias = int | StrOrBytesPath - -# stable -class SupportsRead(Protocol[_T_co]): - def read(self, length: int = ..., /) -> _T_co: ... - -# stable -class SupportsReadline(Protocol[_T_co]): - def readline(self, length: int = ..., /) -> _T_co: ... - -# stable -class SupportsNoArgReadline(Protocol[_T_co]): - def readline(self) -> _T_co: ... - -# stable -class SupportsWrite(Protocol[_T_contra]): - def write(self, s: _T_contra, /) -> object: ... - -# stable -class SupportsFlush(Protocol): - def flush(self) -> object: ... - -# Unfortunately PEP 688 does not allow us to distinguish read-only -# from writable buffers. We use these aliases for readability for now. -# Perhaps a future extension of the buffer protocol will allow us to -# distinguish these cases in the type system. -ReadOnlyBuffer: TypeAlias = Buffer # stable -# Anything that implements the read-write buffer interface. -WriteableBuffer: TypeAlias = Buffer -# Same as WriteableBuffer, but also includes read-only buffer types (like bytes). -ReadableBuffer: TypeAlias = Buffer # stable - -class SliceableBuffer(Buffer, Protocol): - def __getitem__(self, slice: slice, /) -> Sequence[int]: ... - -class IndexableBuffer(Buffer, Protocol): - def __getitem__(self, i: int, /) -> int: ... - -class SupportsGetItemBuffer(SliceableBuffer, IndexableBuffer, Protocol): - def __contains__(self, x: Any, /) -> bool: ... - @overload - def __getitem__(self, slice: slice, /) -> Sequence[int]: ... - @overload - def __getitem__(self, i: int, /) -> int: ... - -class SizedBuffer(Sized, Buffer, Protocol): ... - -# for compatibility with third-party stubs that may use this -_BufferWithLen: TypeAlias = SizedBuffer # not stable # noqa: Y047 - -ExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType] -OptExcInfo: TypeAlias = ExcInfo | tuple[None, None, None] - -# stable -if sys.version_info >= (3, 10): - from types import NoneType as NoneType -else: - # Used by type checkers for checks involving None (does not exist at runtime) - @final - class NoneType: - def __bool__(self) -> Literal[False]: ... - -# This is an internal CPython type that is like, but subtly different from, a NamedTuple -# Subclasses of this type are found in multiple modules. -# In typeshed, `structseq` is only ever used as a mixin in combination with a fixed-length `Tuple` -# See discussion at #6546 & #6560 -# `structseq` classes are unsubclassable, so are all decorated with `@final`. -class structseq(Generic[_T_co]): - n_fields: Final[int] - n_unnamed_fields: Final[int] - n_sequence_fields: Final[int] - # The first parameter will generally only take an iterable of a specific length. - # E.g. `os.uname_result` takes any iterable of length exactly 5. - # - # The second parameter will accept a dict of any kind without raising an exception, - # but only has any meaning if you supply it a dict where the keys are strings. - # https://github.com/python/typeshed/pull/6560#discussion_r767149830 - def __new__(cls, sequence: Iterable[_T_co], dict: dict[str, Any] = ...) -> _Self: ... - if sys.version_info >= (3, 13): - def __replace__(self, **kwargs: Any) -> _Self: ... - -# Superset of typing.AnyStr that also includes LiteralString -AnyOrLiteralStr = TypeVar("AnyOrLiteralStr", str, bytes, LiteralString) # noqa: Y001 - -# Represents when str or LiteralStr is acceptable. Useful for string processing -# APIs where literalness of return value depends on literalness of inputs -StrOrLiteralStr = TypeVar("StrOrLiteralStr", LiteralString, str) # noqa: Y001 - -# Objects suitable to be passed to sys.setprofile, threading.setprofile, and similar -ProfileFunction: TypeAlias = Callable[[FrameType, str, Any], object] - -# Objects suitable to be passed to sys.settrace, threading.settrace, and similar -TraceFunction: TypeAlias = Callable[[FrameType, str, Any], TraceFunction | None] - -# experimental -# Might not work as expected for pyright, see -# https://github.com/python/typeshed/pull/9362 -# https://github.com/microsoft/pyright/issues/4339 -class DataclassInstance(Protocol): - __dataclass_fields__: ClassVar[dict[str, Field[Any]]] - -# Anything that can be passed to the int/float constructors -ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc -ConvertibleToFloat: TypeAlias = str | ReadableBuffer | SupportsFloat | SupportsIndex - -# A few classes updated from Foo(str, Enum) to Foo(StrEnum). This is a convenience so these -# can be accurate on all python versions without getting too wordy -if sys.version_info >= (3, 11): - from enum import StrEnum as StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi deleted file mode 100644 index c314acbea1ca3..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi +++ /dev/null @@ -1,990 +0,0 @@ -# ruff: noqa: PLR5501 # This condition is so big, it's clearer to keep to platform condition in two blocks -# Can't NOQA on a specific line: https://github.com/plinss/flake8-noqa/issues/22 -import sys -from collections.abc import Awaitable, Coroutine, Generator -from typing import Any, TypeVar -from typing_extensions import TypeAlias - -# As at runtime, this depends on all submodules defining __all__ accurately. -from .base_events import * -from .coroutines import * -from .events import * -from .exceptions import * -from .futures import * -from .locks import * -from .protocols import * -from .queues import * -from .runners import * -from .streams import * -from .subprocess import * -from .tasks import * -from .threads import * -from .transports import * - -if sys.version_info >= (3, 11): - from .taskgroups import * - from .timeouts import * - -if sys.platform == "win32": - from .windows_events import * -else: - from .unix_events import * - -if sys.platform == "win32": - if sys.version_info >= (3, 14): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "QueueShutDown", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "create_eager_task_factory", # from tasks - "eager_task_factory", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "TaskGroup", # from taskgroups - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from windows_events - "ProactorEventLoop", # from windows_events - "IocpProactor", # from windows_events - "DefaultEventLoopPolicy", # from windows_events - "WindowsSelectorEventLoopPolicy", # from windows_events - "WindowsProactorEventLoopPolicy", # from windows_events - "EventLoop", # from windows_events - ) - elif sys.version_info >= (3, 13): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "QueueShutDown", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "create_eager_task_factory", # from tasks - "eager_task_factory", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "TaskGroup", # from taskgroups - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from windows_events - "ProactorEventLoop", # from windows_events - "IocpProactor", # from windows_events - "DefaultEventLoopPolicy", # from windows_events - "WindowsSelectorEventLoopPolicy", # from windows_events - "WindowsProactorEventLoopPolicy", # from windows_events - "EventLoop", # from windows_events - ) - elif sys.version_info >= (3, 12): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "create_eager_task_factory", # from tasks - "eager_task_factory", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "TaskGroup", # from taskgroups - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from windows_events - "ProactorEventLoop", # from windows_events - "IocpProactor", # from windows_events - "DefaultEventLoopPolicy", # from windows_events - "WindowsSelectorEventLoopPolicy", # from windows_events - "WindowsProactorEventLoopPolicy", # from windows_events - ) - elif sys.version_info >= (3, 11): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from windows_events - "ProactorEventLoop", # from windows_events - "IocpProactor", # from windows_events - "DefaultEventLoopPolicy", # from windows_events - "WindowsSelectorEventLoopPolicy", # from windows_events - "WindowsProactorEventLoopPolicy", # from windows_events - ) - else: - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "coroutine", # from coroutines - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "to_thread", # from threads - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from windows_events - "ProactorEventLoop", # from windows_events - "IocpProactor", # from windows_events - "DefaultEventLoopPolicy", # from windows_events - "WindowsSelectorEventLoopPolicy", # from windows_events - "WindowsProactorEventLoopPolicy", # from windows_events - ) -else: - if sys.version_info >= (3, 14): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "QueueShutDown", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "open_unix_connection", # from streams - "start_unix_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "create_eager_task_factory", # from tasks - "eager_task_factory", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "TaskGroup", # from taskgroups - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from unix_events - "DefaultEventLoopPolicy", # from unix_events - "EventLoop", # from unix_events - ) - elif sys.version_info >= (3, 13): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "QueueShutDown", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "open_unix_connection", # from streams - "start_unix_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "create_eager_task_factory", # from tasks - "eager_task_factory", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "TaskGroup", # from taskgroups - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from unix_events - "AbstractChildWatcher", # from unix_events - "SafeChildWatcher", # from unix_events - "FastChildWatcher", # from unix_events - "PidfdChildWatcher", # from unix_events - "MultiLoopChildWatcher", # from unix_events - "ThreadedChildWatcher", # from unix_events - "DefaultEventLoopPolicy", # from unix_events - "EventLoop", # from unix_events - ) - elif sys.version_info >= (3, 12): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "open_unix_connection", # from streams - "start_unix_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "create_eager_task_factory", # from tasks - "eager_task_factory", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "TaskGroup", # from taskgroups - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from unix_events - "AbstractChildWatcher", # from unix_events - "SafeChildWatcher", # from unix_events - "FastChildWatcher", # from unix_events - "PidfdChildWatcher", # from unix_events - "MultiLoopChildWatcher", # from unix_events - "ThreadedChildWatcher", # from unix_events - "DefaultEventLoopPolicy", # from unix_events - ) - elif sys.version_info >= (3, 11): - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "BrokenBarrierError", # from exceptions - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "Barrier", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "Runner", # from runners - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "open_unix_connection", # from streams - "start_unix_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "to_thread", # from threads - "Timeout", # from timeouts - "timeout", # from timeouts - "timeout_at", # from timeouts - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from unix_events - "AbstractChildWatcher", # from unix_events - "SafeChildWatcher", # from unix_events - "FastChildWatcher", # from unix_events - "PidfdChildWatcher", # from unix_events - "MultiLoopChildWatcher", # from unix_events - "ThreadedChildWatcher", # from unix_events - "DefaultEventLoopPolicy", # from unix_events - ) - else: - __all__ = ( - "BaseEventLoop", # from base_events - "Server", # from base_events - "coroutine", # from coroutines - "iscoroutinefunction", # from coroutines - "iscoroutine", # from coroutines - "AbstractEventLoopPolicy", # from events - "AbstractEventLoop", # from events - "AbstractServer", # from events - "Handle", # from events - "TimerHandle", # from events - "get_event_loop_policy", # from events - "set_event_loop_policy", # from events - "get_event_loop", # from events - "set_event_loop", # from events - "new_event_loop", # from events - "get_child_watcher", # from events - "set_child_watcher", # from events - "_set_running_loop", # from events - "get_running_loop", # from events - "_get_running_loop", # from events - "CancelledError", # from exceptions - "InvalidStateError", # from exceptions - "TimeoutError", # from exceptions - "IncompleteReadError", # from exceptions - "LimitOverrunError", # from exceptions - "SendfileNotAvailableError", # from exceptions - "Future", # from futures - "wrap_future", # from futures - "isfuture", # from futures - "Lock", # from locks - "Event", # from locks - "Condition", # from locks - "Semaphore", # from locks - "BoundedSemaphore", # from locks - "BaseProtocol", # from protocols - "Protocol", # from protocols - "DatagramProtocol", # from protocols - "SubprocessProtocol", # from protocols - "BufferedProtocol", # from protocols - "run", # from runners - "Queue", # from queues - "PriorityQueue", # from queues - "LifoQueue", # from queues - "QueueFull", # from queues - "QueueEmpty", # from queues - "StreamReader", # from streams - "StreamWriter", # from streams - "StreamReaderProtocol", # from streams - "open_connection", # from streams - "start_server", # from streams - "open_unix_connection", # from streams - "start_unix_server", # from streams - "create_subprocess_exec", # from subprocess - "create_subprocess_shell", # from subprocess - "Task", # from tasks - "create_task", # from tasks - "FIRST_COMPLETED", # from tasks - "FIRST_EXCEPTION", # from tasks - "ALL_COMPLETED", # from tasks - "wait", # from tasks - "wait_for", # from tasks - "as_completed", # from tasks - "sleep", # from tasks - "gather", # from tasks - "shield", # from tasks - "ensure_future", # from tasks - "run_coroutine_threadsafe", # from tasks - "current_task", # from tasks - "all_tasks", # from tasks - "_register_task", # from tasks - "_unregister_task", # from tasks - "_enter_task", # from tasks - "_leave_task", # from tasks - "to_thread", # from threads - "BaseTransport", # from transports - "ReadTransport", # from transports - "WriteTransport", # from transports - "Transport", # from transports - "DatagramTransport", # from transports - "SubprocessTransport", # from transports - "SelectorEventLoop", # from unix_events - "AbstractChildWatcher", # from unix_events - "SafeChildWatcher", # from unix_events - "FastChildWatcher", # from unix_events - "PidfdChildWatcher", # from unix_events - "MultiLoopChildWatcher", # from unix_events - "ThreadedChildWatcher", # from unix_events - "DefaultEventLoopPolicy", # from unix_events - ) - -_T_co = TypeVar("_T_co", covariant=True) - -# Aliases imported by multiple submodules in typeshed -if sys.version_info >= (3, 12): - _AwaitableLike: TypeAlias = Awaitable[_T_co] # noqa: Y047 - _CoroutineLike: TypeAlias = Coroutine[Any, Any, _T_co] # noqa: Y047 -else: - _AwaitableLike: TypeAlias = Generator[Any, None, _T_co] | Awaitable[_T_co] - _CoroutineLike: TypeAlias = Generator[Any, None, _T_co] | Coroutine[Any, Any, _T_co] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi deleted file mode 100644 index cb2785012fb27..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi +++ /dev/null @@ -1,17 +0,0 @@ -from _asyncio import Future as Future -from concurrent.futures._base import Future as _ConcurrentFuture -from typing import Any, TypeVar -from typing_extensions import TypeIs - -from .events import AbstractEventLoop - -# Keep asyncio.__all__ updated with any changes to __all__ here -__all__ = ("Future", "wrap_future", "isfuture") - -_T = TypeVar("_T") - -# asyncio defines 'isfuture()' in base_futures.py and re-imports it in futures.py -# but it leads to circular import error in pytype tool. -# That's why the import order is reversed. -def isfuture(obj: object) -> TypeIs[Future[Any]]: ... -def wrap_future(future: _ConcurrentFuture[_T] | Future[_T], *, loop: AbstractEventLoop | None = None) -> Future[_T]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/codeop.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/codeop.pyi deleted file mode 100644 index cfe52e9b35de7..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/codeop.pyi +++ /dev/null @@ -1,17 +0,0 @@ -import sys -from types import CodeType - -__all__ = ["compile_command", "Compile", "CommandCompiler"] - -def compile_command(source: str, filename: str = "", symbol: str = "single") -> CodeType | None: ... - -class Compile: - flags: int - if sys.version_info >= (3, 13): - def __call__(self, source: str, filename: str, symbol: str, flags: int = 0) -> CodeType: ... - else: - def __call__(self, source: str, filename: str, symbol: str) -> CodeType: ... - -class CommandCompiler: - compiler: Compile - def __call__(self, source: str, filename: str = "", symbol: str = "single") -> CodeType | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/collections/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/collections/__init__.pyi deleted file mode 100644 index b9e4f84ec0b63..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/collections/__init__.pyi +++ /dev/null @@ -1,495 +0,0 @@ -import sys -from _collections_abc import dict_items, dict_keys, dict_values -from _typeshed import SupportsItems, SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT -from types import GenericAlias -from typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload -from typing_extensions import Self - -if sys.version_info >= (3, 10): - from collections.abc import ( - Callable, - ItemsView, - Iterable, - Iterator, - KeysView, - Mapping, - MutableMapping, - MutableSequence, - Sequence, - ValuesView, - ) -else: - from _collections_abc import * - -__all__ = ["ChainMap", "Counter", "OrderedDict", "UserDict", "UserList", "UserString", "defaultdict", "deque", "namedtuple"] - -_S = TypeVar("_S") -_T = TypeVar("_T") -_T1 = TypeVar("_T1") -_T2 = TypeVar("_T2") -_KT = TypeVar("_KT") -_VT = TypeVar("_VT") -_KT_co = TypeVar("_KT_co", covariant=True) -_VT_co = TypeVar("_VT_co", covariant=True) - -# namedtuple is special-cased in the type checker; the initializer is ignored. -def namedtuple( - typename: str, - field_names: str | Iterable[str], - *, - rename: bool = False, - module: str | None = None, - defaults: Iterable[Any] | None = None, -) -> type[tuple[Any, ...]]: ... - -class UserDict(MutableMapping[_KT, _VT]): - data: dict[_KT, _VT] - # __init__ should be kept roughly in line with `dict.__init__`, which has the same semantics - @overload - def __init__(self, dict: None = None, /) -> None: ... - @overload - def __init__( - self: UserDict[str, _VT], dict: None = None, /, **kwargs: _VT # pyright: ignore[reportInvalidTypeVarUse] #11780 - ) -> None: ... - @overload - def __init__(self, dict: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... - @overload - def __init__( - self: UserDict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 - dict: SupportsKeysAndGetItem[str, _VT], - /, - **kwargs: _VT, - ) -> None: ... - @overload - def __init__(self, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ... - @overload - def __init__( - self: UserDict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 - iterable: Iterable[tuple[str, _VT]], - /, - **kwargs: _VT, - ) -> None: ... - @overload - def __init__(self: UserDict[str, str], iterable: Iterable[list[str]], /) -> None: ... - @overload - def __init__(self: UserDict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ... - def __len__(self) -> int: ... - def __getitem__(self, key: _KT) -> _VT: ... - def __setitem__(self, key: _KT, item: _VT) -> None: ... - def __delitem__(self, key: _KT) -> None: ... - def __iter__(self) -> Iterator[_KT]: ... - def __contains__(self, key: object) -> bool: ... - def copy(self) -> Self: ... - def __copy__(self) -> Self: ... - - # `UserDict.fromkeys` has the same semantics as `dict.fromkeys`, so should be kept in line with `dict.fromkeys`. - # TODO: Much like `dict.fromkeys`, the true signature of `UserDict.fromkeys` is inexpressible in the current type system. - # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. - @classmethod - @overload - def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> UserDict[_T, Any | None]: ... - @classmethod - @overload - def fromkeys(cls, iterable: Iterable[_T], value: _S) -> UserDict[_T, _S]: ... - @overload - def __or__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... - @overload - def __or__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... - @overload - def __ror__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... - # UserDict.__ior__ should be kept roughly in line with MutableMapping.update() - @overload # type: ignore[misc] - def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... - @overload - def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... - if sys.version_info >= (3, 12): - @overload - def get(self, key: _KT, default: None = None) -> _VT | None: ... - @overload - def get(self, key: _KT, default: _T) -> _VT | _T: ... - -class UserList(MutableSequence[_T]): - data: list[_T] - @overload - def __init__(self, initlist: None = None) -> None: ... - @overload - def __init__(self, initlist: Iterable[_T]) -> None: ... - __hash__: ClassVar[None] # type: ignore[assignment] - def __lt__(self, other: list[_T] | UserList[_T]) -> bool: ... - def __le__(self, other: list[_T] | UserList[_T]) -> bool: ... - def __gt__(self, other: list[_T] | UserList[_T]) -> bool: ... - def __ge__(self, other: list[_T] | UserList[_T]) -> bool: ... - def __eq__(self, other: object) -> bool: ... - def __contains__(self, item: object) -> bool: ... - def __len__(self) -> int: ... - @overload - def __getitem__(self, i: SupportsIndex) -> _T: ... - @overload - def __getitem__(self, i: slice) -> Self: ... - @overload - def __setitem__(self, i: SupportsIndex, item: _T) -> None: ... - @overload - def __setitem__(self, i: slice, item: Iterable[_T]) -> None: ... - def __delitem__(self, i: SupportsIndex | slice) -> None: ... - def __add__(self, other: Iterable[_T]) -> Self: ... - def __radd__(self, other: Iterable[_T]) -> Self: ... - def __iadd__(self, other: Iterable[_T]) -> Self: ... - def __mul__(self, n: int) -> Self: ... - def __rmul__(self, n: int) -> Self: ... - def __imul__(self, n: int) -> Self: ... - def append(self, item: _T) -> None: ... - def insert(self, i: int, item: _T) -> None: ... - def pop(self, i: int = -1) -> _T: ... - def remove(self, item: _T) -> None: ... - def copy(self) -> Self: ... - def __copy__(self) -> Self: ... - def count(self, item: _T) -> int: ... - # The runtime signature is "item, *args", and the arguments are then passed - # to `list.index`. In order to give more precise types, we pretend that the - # `item` argument is positional-only. - def index(self, item: _T, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ... - # All arguments are passed to `list.sort` at runtime, so the signature should be kept in line with `list.sort`. - @overload - def sort(self: UserList[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ... - @overload - def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ... - def extend(self, other: Iterable[_T]) -> None: ... - -class UserString(Sequence[UserString]): - data: str - def __init__(self, seq: object) -> None: ... - def __int__(self) -> int: ... - def __float__(self) -> float: ... - def __complex__(self) -> complex: ... - def __getnewargs__(self) -> tuple[str]: ... - def __lt__(self, string: str | UserString) -> bool: ... - def __le__(self, string: str | UserString) -> bool: ... - def __gt__(self, string: str | UserString) -> bool: ... - def __ge__(self, string: str | UserString) -> bool: ... - def __eq__(self, string: object) -> bool: ... - def __hash__(self) -> int: ... - def __contains__(self, char: object) -> bool: ... - def __len__(self) -> int: ... - def __getitem__(self, index: SupportsIndex | slice) -> Self: ... - def __iter__(self) -> Iterator[Self]: ... - def __reversed__(self) -> Iterator[Self]: ... - def __add__(self, other: object) -> Self: ... - def __radd__(self, other: object) -> Self: ... - def __mul__(self, n: int) -> Self: ... - def __rmul__(self, n: int) -> Self: ... - def __mod__(self, args: Any) -> Self: ... - def __rmod__(self, template: object) -> Self: ... - def capitalize(self) -> Self: ... - def casefold(self) -> Self: ... - def center(self, width: int, *args: Any) -> Self: ... - def count(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... - def encode(self: UserString, encoding: str | None = "utf-8", errors: str | None = "strict") -> bytes: ... - def endswith(self, suffix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ... - def expandtabs(self, tabsize: int = 8) -> Self: ... - def find(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... - def format(self, *args: Any, **kwds: Any) -> str: ... - def format_map(self, mapping: Mapping[str, Any]) -> str: ... - def index(self, sub: str, start: int = 0, end: int = sys.maxsize) -> int: ... - def isalpha(self) -> bool: ... - def isalnum(self) -> bool: ... - def isdecimal(self) -> bool: ... - def isdigit(self) -> bool: ... - def isidentifier(self) -> bool: ... - def islower(self) -> bool: ... - def isnumeric(self) -> bool: ... - def isprintable(self) -> bool: ... - def isspace(self) -> bool: ... - def istitle(self) -> bool: ... - def isupper(self) -> bool: ... - def isascii(self) -> bool: ... - def join(self, seq: Iterable[str]) -> str: ... - def ljust(self, width: int, *args: Any) -> Self: ... - def lower(self) -> Self: ... - def lstrip(self, chars: str | None = None) -> Self: ... - maketrans = str.maketrans - def partition(self, sep: str) -> tuple[str, str, str]: ... - def removeprefix(self, prefix: str | UserString, /) -> Self: ... - def removesuffix(self, suffix: str | UserString, /) -> Self: ... - def replace(self, old: str | UserString, new: str | UserString, maxsplit: int = -1) -> Self: ... - def rfind(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... - def rindex(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... - def rjust(self, width: int, *args: Any) -> Self: ... - def rpartition(self, sep: str) -> tuple[str, str, str]: ... - def rstrip(self, chars: str | None = None) -> Self: ... - def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ... - def rsplit(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ... - def splitlines(self, keepends: bool = False) -> list[str]: ... - def startswith(self, prefix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ... - def strip(self, chars: str | None = None) -> Self: ... - def swapcase(self) -> Self: ... - def title(self) -> Self: ... - def translate(self, *args: Any) -> Self: ... - def upper(self) -> Self: ... - def zfill(self, width: int) -> Self: ... - -class deque(MutableSequence[_T]): - @property - def maxlen(self) -> int | None: ... - @overload - def __init__(self, *, maxlen: int | None = None) -> None: ... - @overload - def __init__(self, iterable: Iterable[_T], maxlen: int | None = None) -> None: ... - def append(self, x: _T, /) -> None: ... - def appendleft(self, x: _T, /) -> None: ... - def copy(self) -> Self: ... - def count(self, x: _T, /) -> int: ... - def extend(self, iterable: Iterable[_T], /) -> None: ... - def extendleft(self, iterable: Iterable[_T], /) -> None: ... - def insert(self, i: int, x: _T, /) -> None: ... - def index(self, x: _T, start: int = 0, stop: int = ..., /) -> int: ... - def pop(self) -> _T: ... # type: ignore[override] - def popleft(self) -> _T: ... - def remove(self, value: _T, /) -> None: ... - def rotate(self, n: int = 1, /) -> None: ... - def __copy__(self) -> Self: ... - def __len__(self) -> int: ... - __hash__: ClassVar[None] # type: ignore[assignment] - # These methods of deque don't take slices, unlike MutableSequence, hence the type: ignores - def __getitem__(self, key: SupportsIndex, /) -> _T: ... # type: ignore[override] - def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ... # type: ignore[override] - def __delitem__(self, key: SupportsIndex, /) -> None: ... # type: ignore[override] - def __contains__(self, key: object, /) -> bool: ... - def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ... - def __iadd__(self, value: Iterable[_T], /) -> Self: ... - def __add__(self, value: Self, /) -> Self: ... - def __mul__(self, value: int, /) -> Self: ... - def __imul__(self, value: int, /) -> Self: ... - def __lt__(self, value: deque[_T], /) -> bool: ... - def __le__(self, value: deque[_T], /) -> bool: ... - def __gt__(self, value: deque[_T], /) -> bool: ... - def __ge__(self, value: deque[_T], /) -> bool: ... - def __eq__(self, value: object, /) -> bool: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -class Counter(dict[_T, int], Generic[_T]): - @overload - def __init__(self, iterable: None = None, /) -> None: ... - @overload - def __init__(self: Counter[str], iterable: None = None, /, **kwargs: int) -> None: ... - @overload - def __init__(self, mapping: SupportsKeysAndGetItem[_T, int], /) -> None: ... - @overload - def __init__(self, iterable: Iterable[_T], /) -> None: ... - def copy(self) -> Self: ... - def elements(self) -> Iterator[_T]: ... - def most_common(self, n: int | None = None) -> list[tuple[_T, int]]: ... - @classmethod - def fromkeys(cls, iterable: Any, v: int | None = None) -> NoReturn: ... # type: ignore[override] - @overload - def subtract(self, iterable: None = None, /) -> None: ... - @overload - def subtract(self, mapping: Mapping[_T, int], /) -> None: ... - @overload - def subtract(self, iterable: Iterable[_T], /) -> None: ... - # Unlike dict.update(), use Mapping instead of SupportsKeysAndGetItem for the first overload - # (source code does an `isinstance(other, Mapping)` check) - # - # The second overload is also deliberately different to dict.update() - # (if it were `Iterable[_T] | Iterable[tuple[_T, int]]`, - # the tuples would be added as keys, breaking type safety) - @overload # type: ignore[override] - def update(self, m: Mapping[_T, int], /, **kwargs: int) -> None: ... - @overload - def update(self, iterable: Iterable[_T], /, **kwargs: int) -> None: ... - @overload - def update(self, iterable: None = None, /, **kwargs: int) -> None: ... - def __missing__(self, key: _T) -> int: ... - def __delitem__(self, elem: object) -> None: ... - if sys.version_info >= (3, 10): - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - - def __add__(self, other: Counter[_S]) -> Counter[_T | _S]: ... - def __sub__(self, other: Counter[_T]) -> Counter[_T]: ... - def __and__(self, other: Counter[_T]) -> Counter[_T]: ... - def __or__(self, other: Counter[_S]) -> Counter[_T | _S]: ... # type: ignore[override] - def __pos__(self) -> Counter[_T]: ... - def __neg__(self) -> Counter[_T]: ... - # several type: ignores because __iadd__ is supposedly incompatible with __add__, etc. - def __iadd__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[misc] - def __isub__(self, other: SupportsItems[_T, int]) -> Self: ... - def __iand__(self, other: SupportsItems[_T, int]) -> Self: ... - def __ior__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[override,misc] - if sys.version_info >= (3, 10): - def total(self) -> int: ... - def __le__(self, other: Counter[Any]) -> bool: ... - def __lt__(self, other: Counter[Any]) -> bool: ... - def __ge__(self, other: Counter[Any]) -> bool: ... - def __gt__(self, other: Counter[Any]) -> bool: ... - -# The pure-Python implementations of the "views" classes -# These are exposed at runtime in `collections/__init__.py` -class _OrderedDictKeysView(KeysView[_KT_co]): - def __reversed__(self) -> Iterator[_KT_co]: ... - -class _OrderedDictItemsView(ItemsView[_KT_co, _VT_co]): - def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... - -class _OrderedDictValuesView(ValuesView[_VT_co]): - def __reversed__(self) -> Iterator[_VT_co]: ... - -# The C implementations of the "views" classes -# (At runtime, these are called `odict_keys`, `odict_items` and `odict_values`, -# but they are not exposed anywhere) -# pyright doesn't have a specific error code for subclassing error! -@final -class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] - def __reversed__(self) -> Iterator[_KT_co]: ... - -@final -class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] - def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... - -@final -class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] - def __reversed__(self) -> Iterator[_VT_co]: ... - -class OrderedDict(dict[_KT, _VT]): - def popitem(self, last: bool = True) -> tuple[_KT, _VT]: ... - def move_to_end(self, key: _KT, last: bool = True) -> None: ... - def copy(self) -> Self: ... - def __reversed__(self) -> Iterator[_KT]: ... - def keys(self) -> _odict_keys[_KT, _VT]: ... - def items(self) -> _odict_items[_KT, _VT]: ... - def values(self) -> _odict_values[_KT, _VT]: ... - # The signature of OrderedDict.fromkeys should be kept in line with `dict.fromkeys`, modulo positional-only differences. - # Like dict.fromkeys, its true signature is not expressible in the current type system. - # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. - @classmethod - @overload - def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> OrderedDict[_T, Any | None]: ... - @classmethod - @overload - def fromkeys(cls, iterable: Iterable[_T], value: _S) -> OrderedDict[_T, _S]: ... - # Keep OrderedDict.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. - @overload - def setdefault(self: OrderedDict[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... - @overload - def setdefault(self, key: _KT, default: _VT) -> _VT: ... - # Same as dict.pop, but accepts keyword arguments - @overload - def pop(self, key: _KT) -> _VT: ... - @overload - def pop(self, key: _KT, default: _VT) -> _VT: ... - @overload - def pop(self, key: _KT, default: _T) -> _VT | _T: ... - def __eq__(self, value: object, /) -> bool: ... - @overload - def __or__(self, value: dict[_KT, _VT], /) -> Self: ... - @overload - def __or__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... - @overload - def __ror__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] - -class defaultdict(dict[_KT, _VT]): - default_factory: Callable[[], _VT] | None - @overload - def __init__(self) -> None: ... - @overload - def __init__(self: defaultdict[str, _VT], **kwargs: _VT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 - @overload - def __init__(self, default_factory: Callable[[], _VT] | None, /) -> None: ... - @overload - def __init__( - self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 - default_factory: Callable[[], _VT] | None, - /, - **kwargs: _VT, - ) -> None: ... - @overload - def __init__(self, default_factory: Callable[[], _VT] | None, map: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... - @overload - def __init__( - self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 - default_factory: Callable[[], _VT] | None, - map: SupportsKeysAndGetItem[str, _VT], - /, - **kwargs: _VT, - ) -> None: ... - @overload - def __init__(self, default_factory: Callable[[], _VT] | None, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ... - @overload - def __init__( - self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 - default_factory: Callable[[], _VT] | None, - iterable: Iterable[tuple[str, _VT]], - /, - **kwargs: _VT, - ) -> None: ... - def __missing__(self, key: _KT, /) -> _VT: ... - def __copy__(self) -> Self: ... - def copy(self) -> Self: ... - @overload - def __or__(self, value: dict[_KT, _VT], /) -> Self: ... - @overload - def __or__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... - @overload - def __ror__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] - -class ChainMap(MutableMapping[_KT, _VT]): - maps: list[MutableMapping[_KT, _VT]] - def __init__(self, *maps: MutableMapping[_KT, _VT]) -> None: ... - def new_child(self, m: MutableMapping[_KT, _VT] | None = None) -> Self: ... - @property - def parents(self) -> Self: ... - def __setitem__(self, key: _KT, value: _VT) -> None: ... - def __delitem__(self, key: _KT) -> None: ... - def __getitem__(self, key: _KT) -> _VT: ... - def __iter__(self) -> Iterator[_KT]: ... - def __len__(self) -> int: ... - def __contains__(self, key: object) -> bool: ... - @overload - def get(self, key: _KT, default: None = None) -> _VT | None: ... - @overload - def get(self, key: _KT, default: _T) -> _VT | _T: ... - def __missing__(self, key: _KT) -> _VT: ... # undocumented - def __bool__(self) -> bool: ... - # Keep ChainMap.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. - @overload - def setdefault(self: ChainMap[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... - @overload - def setdefault(self, key: _KT, default: _VT) -> _VT: ... - @overload - def pop(self, key: _KT) -> _VT: ... - @overload - def pop(self, key: _KT, default: _VT) -> _VT: ... - @overload - def pop(self, key: _KT, default: _T) -> _VT | _T: ... - def copy(self) -> Self: ... - __copy__ = copy - # All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime, - # so the signature should be kept in line with `dict.fromkeys`. - @classmethod - @overload - def fromkeys(cls, iterable: Iterable[_T]) -> ChainMap[_T, Any | None]: ... - @classmethod - @overload - # Special-case None: the user probably wants to add non-None values later. - def fromkeys(cls, iterable: Iterable[_T], value: None, /) -> ChainMap[_T, Any | None]: ... - @classmethod - @overload - def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> ChainMap[_T, _S]: ... - @overload - def __or__(self, other: Mapping[_KT, _VT]) -> Self: ... - @overload - def __or__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, other: Mapping[_KT, _VT]) -> Self: ... - @overload - def __ror__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... - # ChainMap.__ior__ should be kept roughly in line with MutableMapping.update() - @overload # type: ignore[misc] - def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... - @overload - def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi deleted file mode 100644 index 68fd0bc5acb43..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi +++ /dev/null @@ -1,51 +0,0 @@ -import sys - -from ._base import ( - ALL_COMPLETED as ALL_COMPLETED, - FIRST_COMPLETED as FIRST_COMPLETED, - FIRST_EXCEPTION as FIRST_EXCEPTION, - BrokenExecutor as BrokenExecutor, - CancelledError as CancelledError, - Executor as Executor, - Future as Future, - InvalidStateError as InvalidStateError, - TimeoutError as TimeoutError, - as_completed as as_completed, - wait as wait, -) -from .process import ProcessPoolExecutor as ProcessPoolExecutor -from .thread import ThreadPoolExecutor as ThreadPoolExecutor - -if sys.version_info >= (3, 13): - __all__ = ( - "FIRST_COMPLETED", - "FIRST_EXCEPTION", - "ALL_COMPLETED", - "CancelledError", - "TimeoutError", - "InvalidStateError", - "BrokenExecutor", - "Future", - "Executor", - "wait", - "as_completed", - "ProcessPoolExecutor", - "ThreadPoolExecutor", - ) -else: - __all__ = ( - "FIRST_COMPLETED", - "FIRST_EXCEPTION", - "ALL_COMPLETED", - "CancelledError", - "TimeoutError", - "BrokenExecutor", - "Future", - "Executor", - "wait", - "as_completed", - "ProcessPoolExecutor", - "ThreadPoolExecutor", - ) - -def __dir__() -> tuple[str, ...]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi deleted file mode 100644 index da3e006b6f138..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi +++ /dev/null @@ -1,76 +0,0 @@ -import queue -from collections.abc import Callable, Iterable, Mapping, Set as AbstractSet -from threading import Lock, Semaphore, Thread -from types import GenericAlias -from typing import Any, Generic, TypeVar, overload -from typing_extensions import TypeVarTuple, Unpack -from weakref import ref - -from ._base import BrokenExecutor, Executor, Future - -_Ts = TypeVarTuple("_Ts") - -_threads_queues: Mapping[Any, Any] -_shutdown: bool -_global_shutdown_lock: Lock - -def _python_exit() -> None: ... - -_S = TypeVar("_S") - -class _WorkItem(Generic[_S]): - future: Future[_S] - fn: Callable[..., _S] - args: Iterable[Any] - kwargs: Mapping[str, Any] - def __init__(self, future: Future[_S], fn: Callable[..., _S], args: Iterable[Any], kwargs: Mapping[str, Any]) -> None: ... - def run(self) -> None: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -def _worker( - executor_reference: ref[Any], - work_queue: queue.SimpleQueue[Any], - initializer: Callable[[Unpack[_Ts]], object], - initargs: tuple[Unpack[_Ts]], -) -> None: ... - -class BrokenThreadPool(BrokenExecutor): ... - -class ThreadPoolExecutor(Executor): - _max_workers: int - _idle_semaphore: Semaphore - _threads: AbstractSet[Thread] - _broken: bool - _shutdown: bool - _shutdown_lock: Lock - _thread_name_prefix: str | None - _initializer: Callable[..., None] | None - _initargs: tuple[Any, ...] - _work_queue: queue.SimpleQueue[_WorkItem[Any]] - @overload - def __init__( - self, - max_workers: int | None = None, - thread_name_prefix: str = "", - initializer: Callable[[], object] | None = None, - initargs: tuple[()] = (), - ) -> None: ... - @overload - def __init__( - self, - max_workers: int | None = None, - thread_name_prefix: str = "", - *, - initializer: Callable[[Unpack[_Ts]], object], - initargs: tuple[Unpack[_Ts]], - ) -> None: ... - @overload - def __init__( - self, - max_workers: int | None, - thread_name_prefix: str, - initializer: Callable[[Unpack[_Ts]], object], - initargs: tuple[Unpack[_Ts]], - ) -> None: ... - def _adjust_thread_count(self) -> None: ... - def _initializer_failed(self) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi deleted file mode 100644 index a7e19483301ce..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi +++ /dev/null @@ -1,306 +0,0 @@ -import sys -from _ctypes import ( - POINTER as POINTER, - RTLD_GLOBAL as RTLD_GLOBAL, - RTLD_LOCAL as RTLD_LOCAL, - Array as Array, - CFuncPtr as _CFuncPtr, - Structure as Structure, - Union as Union, - _CanCastTo as _CanCastTo, - _CArgObject as _CArgObject, - _CData as _CData, - _CDataType as _CDataType, - _CField as _CField, - _Pointer as _Pointer, - _PointerLike as _PointerLike, - _SimpleCData as _SimpleCData, - addressof as addressof, - alignment as alignment, - byref as byref, - get_errno as get_errno, - pointer as pointer, - resize as resize, - set_errno as set_errno, - sizeof as sizeof, -) -from _typeshed import StrPath -from ctypes._endian import BigEndianStructure as BigEndianStructure, LittleEndianStructure as LittleEndianStructure -from types import GenericAlias -from typing import Any, ClassVar, Generic, Literal, TypeVar, type_check_only -from typing_extensions import Self, TypeAlias, deprecated - -if sys.platform == "win32": - from _ctypes import FormatError as FormatError, get_last_error as get_last_error, set_last_error as set_last_error - -if sys.version_info >= (3, 11): - from ctypes._endian import BigEndianUnion as BigEndianUnion, LittleEndianUnion as LittleEndianUnion - -_T = TypeVar("_T", default=Any) -_DLLT = TypeVar("_DLLT", bound=CDLL) -_CT = TypeVar("_CT", bound=_CData) - -DEFAULT_MODE: int - -class ArgumentError(Exception): ... - -# defined within CDLL.__init__ -# Runtime name is ctypes.CDLL.__init__.._FuncPtr -@type_check_only -class _CDLLFuncPointer(_CFuncPtr): - _flags_: ClassVar[int] - _restype_: ClassVar[type[_CDataType]] - -# Not a real class; _CDLLFuncPointer with a __name__ set on it. -@type_check_only -class _NamedFuncPointer(_CDLLFuncPointer): - __name__: str - -if sys.version_info >= (3, 12): - _NameTypes: TypeAlias = StrPath | None -else: - _NameTypes: TypeAlias = str | None - -class CDLL: - _func_flags_: ClassVar[int] - _func_restype_: ClassVar[type[_CDataType]] - _name: str - _handle: int - _FuncPtr: type[_CDLLFuncPointer] - def __init__( - self, - name: _NameTypes, - mode: int = ..., - handle: int | None = None, - use_errno: bool = False, - use_last_error: bool = False, - winmode: int | None = None, - ) -> None: ... - def __getattr__(self, name: str) -> _NamedFuncPointer: ... - def __getitem__(self, name_or_ordinal: str) -> _NamedFuncPointer: ... - -if sys.platform == "win32": - class OleDLL(CDLL): ... - class WinDLL(CDLL): ... - -class PyDLL(CDLL): ... - -class LibraryLoader(Generic[_DLLT]): - def __init__(self, dlltype: type[_DLLT]) -> None: ... - def __getattr__(self, name: str) -> _DLLT: ... - def __getitem__(self, name: str) -> _DLLT: ... - def LoadLibrary(self, name: str) -> _DLLT: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -cdll: LibraryLoader[CDLL] -if sys.platform == "win32": - windll: LibraryLoader[WinDLL] - oledll: LibraryLoader[OleDLL] -pydll: LibraryLoader[PyDLL] -pythonapi: PyDLL - -# Class definition within CFUNCTYPE / WINFUNCTYPE / PYFUNCTYPE -# Names at runtime are -# ctypes.CFUNCTYPE..CFunctionType -# ctypes.WINFUNCTYPE..WinFunctionType -# ctypes.PYFUNCTYPE..CFunctionType -@type_check_only -class _CFunctionType(_CFuncPtr): - _argtypes_: ClassVar[list[type[_CData | _CDataType]]] - _restype_: ClassVar[type[_CData | _CDataType] | None] - _flags_: ClassVar[int] - -# Alias for either function pointer type -_FuncPointer: TypeAlias = _CDLLFuncPointer | _CFunctionType # noqa: Y047 # not used here - -def CFUNCTYPE( - restype: type[_CData | _CDataType] | None, - *argtypes: type[_CData | _CDataType], - use_errno: bool = False, - use_last_error: bool = False, -) -> type[_CFunctionType]: ... - -if sys.platform == "win32": - def WINFUNCTYPE( - restype: type[_CData | _CDataType] | None, - *argtypes: type[_CData | _CDataType], - use_errno: bool = False, - use_last_error: bool = False, - ) -> type[_CFunctionType]: ... - -def PYFUNCTYPE(restype: type[_CData | _CDataType] | None, *argtypes: type[_CData | _CDataType]) -> type[_CFunctionType]: ... - -# Any type that can be implicitly converted to c_void_p when passed as a C function argument. -# (bytes is not included here, see below.) -_CVoidPLike: TypeAlias = _PointerLike | Array[Any] | _CArgObject | int -# Same as above, but including types known to be read-only (i. e. bytes). -# This distinction is not strictly necessary (ctypes doesn't differentiate between const -# and non-const pointers), but it catches errors like memmove(b'foo', buf, 4) -# when memmove(buf, b'foo', 4) was intended. -_CVoidConstPLike: TypeAlias = _CVoidPLike | bytes - -_CastT = TypeVar("_CastT", bound=_CanCastTo) - -def cast(obj: _CData | _CDataType | _CArgObject | int, typ: type[_CastT]) -> _CastT: ... -def create_string_buffer(init: int | bytes, size: int | None = None) -> Array[c_char]: ... - -c_buffer = create_string_buffer - -def create_unicode_buffer(init: int | str, size: int | None = None) -> Array[c_wchar]: ... -@deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.15") -def SetPointerType(pointer: type[_Pointer[Any]], cls: Any) -> None: ... # noqa: F811 -def ARRAY(typ: _CT, len: int) -> Array[_CT]: ... # Soft Deprecated, no plans to remove - -if sys.platform == "win32": - def DllCanUnloadNow() -> int: ... - def DllGetClassObject(rclsid: Any, riid: Any, ppv: Any) -> int: ... # TODO: not documented - - # Actually just an instance of _NamedFuncPointer (aka _CDLLFuncPointer), - # but we want to set a more specific __call__ - @type_check_only - class _GetLastErrorFunctionType(_NamedFuncPointer): - def __call__(self) -> int: ... - - GetLastError: _GetLastErrorFunctionType - -# Actually just an instance of _CFunctionType, but we want to set a more -# specific __call__. -@type_check_only -class _MemmoveFunctionType(_CFunctionType): - def __call__(self, dst: _CVoidPLike, src: _CVoidConstPLike, count: int) -> int: ... - -memmove: _MemmoveFunctionType - -# Actually just an instance of _CFunctionType, but we want to set a more -# specific __call__. -@type_check_only -class _MemsetFunctionType(_CFunctionType): - def __call__(self, dst: _CVoidPLike, c: int, count: int) -> int: ... - -memset: _MemsetFunctionType - -def string_at(ptr: _CVoidConstPLike, size: int = -1) -> bytes: ... - -if sys.platform == "win32": - def WinError(code: int | None = None, descr: str | None = None) -> OSError: ... - -def wstring_at(ptr: _CVoidConstPLike, size: int = -1) -> str: ... - -class py_object(_CanCastTo, _SimpleCData[_T]): - _type_: ClassVar[Literal["O"]] - -class c_bool(_SimpleCData[bool]): - _type_: ClassVar[Literal["?"]] - def __init__(self, value: bool = ...) -> None: ... - -class c_byte(_SimpleCData[int]): - _type_: ClassVar[Literal["b"]] - -class c_ubyte(_SimpleCData[int]): - _type_: ClassVar[Literal["B"]] - -class c_short(_SimpleCData[int]): - _type_: ClassVar[Literal["h"]] - -class c_ushort(_SimpleCData[int]): - _type_: ClassVar[Literal["H"]] - -class c_long(_SimpleCData[int]): - _type_: ClassVar[Literal["l"]] - -class c_ulong(_SimpleCData[int]): - _type_: ClassVar[Literal["L"]] - -class c_int(_SimpleCData[int]): # can be an alias for c_long - _type_: ClassVar[Literal["i", "l"]] - -class c_uint(_SimpleCData[int]): # can be an alias for c_ulong - _type_: ClassVar[Literal["I", "L"]] - -class c_longlong(_SimpleCData[int]): # can be an alias for c_long - _type_: ClassVar[Literal["q", "l"]] - -class c_ulonglong(_SimpleCData[int]): # can be an alias for c_ulong - _type_: ClassVar[Literal["Q", "L"]] - -c_int8 = c_byte -c_uint8 = c_ubyte - -class c_int16(_SimpleCData[int]): # can be an alias for c_short or c_int - _type_: ClassVar[Literal["h", "i"]] - -class c_uint16(_SimpleCData[int]): # can be an alias for c_ushort or c_uint - _type_: ClassVar[Literal["H", "I"]] - -class c_int32(_SimpleCData[int]): # can be an alias for c_int or c_long - _type_: ClassVar[Literal["i", "l"]] - -class c_uint32(_SimpleCData[int]): # can be an alias for c_uint or c_ulong - _type_: ClassVar[Literal["I", "L"]] - -class c_int64(_SimpleCData[int]): # can be an alias for c_long or c_longlong - _type_: ClassVar[Literal["l", "q"]] - -class c_uint64(_SimpleCData[int]): # can be an alias for c_ulong or c_ulonglong - _type_: ClassVar[Literal["L", "Q"]] - -class c_ssize_t(_SimpleCData[int]): # alias for c_int, c_long, or c_longlong - _type_: ClassVar[Literal["i", "l", "q"]] - -class c_size_t(_SimpleCData[int]): # alias for c_uint, c_ulong, or c_ulonglong - _type_: ClassVar[Literal["I", "L", "Q"]] - -class c_float(_SimpleCData[float]): - _type_: ClassVar[Literal["f"]] - -class c_double(_SimpleCData[float]): - _type_: ClassVar[Literal["d"]] - -class c_longdouble(_SimpleCData[float]): # can be an alias for c_double - _type_: ClassVar[Literal["d", "g"]] - -if sys.version_info >= (3, 14): - class c_float_complex(_SimpleCData[complex]): - _type_: ClassVar[Literal["E"]] - - class c_double_complex(_SimpleCData[complex]): - _type_: ClassVar[Literal["C"]] - - class c_longdouble_complex(_SimpleCData[complex]): - _type_: ClassVar[Literal["F"]] - -class c_char(_SimpleCData[bytes]): - _type_: ClassVar[Literal["c"]] - def __init__(self, value: int | bytes | bytearray = ...) -> None: ... - -class c_char_p(_PointerLike, _SimpleCData[bytes | None]): - _type_: ClassVar[Literal["z"]] - def __init__(self, value: int | bytes | None = ...) -> None: ... - @classmethod - def from_param(cls, value: Any, /) -> Self | _CArgObject: ... - -class c_void_p(_PointerLike, _SimpleCData[int | None]): - _type_: ClassVar[Literal["P"]] - @classmethod - def from_param(cls, value: Any, /) -> Self | _CArgObject: ... - -c_voidp = c_void_p # backwards compatibility (to a bug) - -class c_wchar(_SimpleCData[str]): - _type_: ClassVar[Literal["u"]] - -class c_wchar_p(_PointerLike, _SimpleCData[str | None]): - _type_: ClassVar[Literal["Z"]] - def __init__(self, value: int | str | None = ...) -> None: ... - @classmethod - def from_param(cls, value: Any, /) -> Self | _CArgObject: ... - -if sys.platform == "win32": - class HRESULT(_SimpleCData[int]): # TODO: undocumented - _type_: ClassVar[Literal["l"]] - -if sys.version_info >= (3, 12): - # At runtime, this is an alias for either c_int32 or c_int64, - # which are themselves an alias for one of c_int, c_long, or c_longlong - # This covers all our bases. - c_time_t: type[c_int32 | c_int64 | c_int | c_long | c_longlong] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/util.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/util.pyi deleted file mode 100644 index 316f7a2b3e2f5..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/util.pyi +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -def find_library(name: str) -> str | None: ... - -if sys.platform == "win32": - def find_msvcrt() -> str | None: ... - -def test() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi deleted file mode 100644 index edc64a00cd39f..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi +++ /dev/null @@ -1,45 +0,0 @@ -import sys -from _curses import * -from _curses import window as window -from _typeshed import structseq -from collections.abc import Callable -from typing import Final, TypeVar, final, type_check_only -from typing_extensions import Concatenate, ParamSpec - -# NOTE: The _curses module is ordinarily only available on Unix, but the -# windows-curses package makes it available on Windows as well with the same -# contents. - -_T = TypeVar("_T") -_P = ParamSpec("_P") - -# available after calling `curses.initscr()` -LINES: int -COLS: int - -# available after calling `curses.start_color()` -COLORS: int -COLOR_PAIRS: int - -def wrapper(func: Callable[Concatenate[window, _P], _T], /, *arg: _P.args, **kwds: _P.kwargs) -> _T: ... - -# typeshed used the name _CursesWindow for the underlying C class before -# it was mapped to the name 'window' in 3.8. -# Kept here as a legacy alias in case any third-party code is relying on it. -_CursesWindow = window - -# At runtime this class is unexposed and calls itself curses.ncurses_version. -# That name would conflict with the actual curses.ncurses_version, which is -# an instance of this class. -@final -@type_check_only -class _ncurses_version(structseq[int], tuple[int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("major", "minor", "patch") - - @property - def major(self) -> int: ... - @property - def minor(self) -> int: ... - @property - def patch(self) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dataclasses.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/dataclasses.pyi deleted file mode 100644 index e08b1919d8e59..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/dataclasses.pyi +++ /dev/null @@ -1,312 +0,0 @@ -import enum -import sys -import types -from _typeshed import DataclassInstance -from builtins import type as Type # alias to avoid name clashes with fields named "type" -from collections.abc import Callable, Iterable, Mapping -from types import GenericAlias -from typing import Any, Generic, Literal, Protocol, TypeVar, overload -from typing_extensions import Never, TypeIs - -_T = TypeVar("_T") -_T_co = TypeVar("_T_co", covariant=True) - -__all__ = [ - "dataclass", - "field", - "Field", - "FrozenInstanceError", - "InitVar", - "MISSING", - "fields", - "asdict", - "astuple", - "make_dataclass", - "replace", - "is_dataclass", -] - -if sys.version_info >= (3, 10): - __all__ += ["KW_ONLY"] - -_DataclassT = TypeVar("_DataclassT", bound=DataclassInstance) - -# define _MISSING_TYPE as an enum within the type stubs, -# even though that is not really its type at runtime -# this allows us to use Literal[_MISSING_TYPE.MISSING] -# for background, see: -# https://github.com/python/typeshed/pull/5900#issuecomment-895513797 -class _MISSING_TYPE(enum.Enum): - MISSING = enum.auto() - -MISSING = _MISSING_TYPE.MISSING - -if sys.version_info >= (3, 10): - class KW_ONLY: ... - -@overload -def asdict(obj: DataclassInstance) -> dict[str, Any]: ... -@overload -def asdict(obj: DataclassInstance, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T: ... -@overload -def astuple(obj: DataclassInstance) -> tuple[Any, ...]: ... -@overload -def astuple(obj: DataclassInstance, *, tuple_factory: Callable[[list[Any]], _T]) -> _T: ... -@overload -def dataclass(cls: None, /) -> Callable[[type[_T]], type[_T]]: ... -@overload -def dataclass(cls: type[_T], /) -> type[_T]: ... - -if sys.version_info >= (3, 11): - @overload - def dataclass( - *, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - match_args: bool = True, - kw_only: bool = False, - slots: bool = False, - weakref_slot: bool = False, - ) -> Callable[[type[_T]], type[_T]]: ... - -elif sys.version_info >= (3, 10): - @overload - def dataclass( - *, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - match_args: bool = True, - kw_only: bool = False, - slots: bool = False, - ) -> Callable[[type[_T]], type[_T]]: ... - -else: - @overload - def dataclass( - *, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - ) -> Callable[[type[_T]], type[_T]]: ... - -# See https://github.com/python/mypy/issues/10750 -class _DefaultFactory(Protocol[_T_co]): - def __call__(self) -> _T_co: ... - -class Field(Generic[_T]): - name: str - type: Type[_T] | str | Any - default: _T | Literal[_MISSING_TYPE.MISSING] - default_factory: _DefaultFactory[_T] | Literal[_MISSING_TYPE.MISSING] - repr: bool - hash: bool | None - init: bool - compare: bool - metadata: types.MappingProxyType[Any, Any] - if sys.version_info >= (3, 10): - kw_only: bool | Literal[_MISSING_TYPE.MISSING] - def __init__( - self, - default: _T, - default_factory: Callable[[], _T], - init: bool, - repr: bool, - hash: bool | None, - compare: bool, - metadata: Mapping[Any, Any], - kw_only: bool, - ) -> None: ... - else: - def __init__( - self, - default: _T, - default_factory: Callable[[], _T], - init: bool, - repr: bool, - hash: bool | None, - compare: bool, - metadata: Mapping[Any, Any], - ) -> None: ... - - def __set_name__(self, owner: Type[Any], name: str) -> None: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -# NOTE: Actual return type is 'Field[_T]', but we want to help type checkers -# to understand the magic that happens at runtime. -if sys.version_info >= (3, 10): - @overload # `default` and `default_factory` are optional and mutually exclusive. - def field( - *, - default: _T, - default_factory: Literal[_MISSING_TYPE.MISSING] = ..., - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., - ) -> _T: ... - @overload - def field( - *, - default: Literal[_MISSING_TYPE.MISSING] = ..., - default_factory: Callable[[], _T], - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., - ) -> _T: ... - @overload - def field( - *, - default: Literal[_MISSING_TYPE.MISSING] = ..., - default_factory: Literal[_MISSING_TYPE.MISSING] = ..., - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., - ) -> Any: ... - -else: - @overload # `default` and `default_factory` are optional and mutually exclusive. - def field( - *, - default: _T, - default_factory: Literal[_MISSING_TYPE.MISSING] = ..., - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - ) -> _T: ... - @overload - def field( - *, - default: Literal[_MISSING_TYPE.MISSING] = ..., - default_factory: Callable[[], _T], - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - ) -> _T: ... - @overload - def field( - *, - default: Literal[_MISSING_TYPE.MISSING] = ..., - default_factory: Literal[_MISSING_TYPE.MISSING] = ..., - init: bool = True, - repr: bool = True, - hash: bool | None = None, - compare: bool = True, - metadata: Mapping[Any, Any] | None = None, - ) -> Any: ... - -def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tuple[Field[Any], ...]: ... - -# HACK: `obj: Never` typing matches if object argument is using `Any` type. -@overload -def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] -@overload -def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ... -@overload -def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... - -class FrozenInstanceError(AttributeError): ... - -class InitVar(Generic[_T], metaclass=type): - type: Type[_T] - def __init__(self, type: Type[_T]) -> None: ... - @overload - def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm] - @overload - def __class_getitem__(cls, type: Any) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm] - -if sys.version_info >= (3, 12): - def make_dataclass( - cls_name: str, - fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], - *, - bases: tuple[type, ...] = (), - namespace: dict[str, Any] | None = None, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - match_args: bool = True, - kw_only: bool = False, - slots: bool = False, - weakref_slot: bool = False, - module: str | None = None, - ) -> type: ... - -elif sys.version_info >= (3, 11): - def make_dataclass( - cls_name: str, - fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], - *, - bases: tuple[type, ...] = (), - namespace: dict[str, Any] | None = None, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - match_args: bool = True, - kw_only: bool = False, - slots: bool = False, - weakref_slot: bool = False, - ) -> type: ... - -elif sys.version_info >= (3, 10): - def make_dataclass( - cls_name: str, - fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], - *, - bases: tuple[type, ...] = (), - namespace: dict[str, Any] | None = None, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - match_args: bool = True, - kw_only: bool = False, - slots: bool = False, - ) -> type: ... - -else: - def make_dataclass( - cls_name: str, - fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], - *, - bases: tuple[type, ...] = (), - namespace: dict[str, Any] | None = None, - init: bool = True, - repr: bool = True, - eq: bool = True, - order: bool = False, - unsafe_hash: bool = False, - frozen: bool = False, - ) -> type: ... - -def replace(obj: _DataclassT, /, **changes: Any) -> _DataclassT: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/__init__.pyi deleted file mode 100644 index 628ffb2b793a2..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/__init__.pyi +++ /dev/null @@ -1,59 +0,0 @@ -from collections.abc import Callable -from email.message import Message -from email.policy import Policy, _MessageT -from typing import IO, overload -from typing_extensions import TypeAlias - -# At runtime, listing submodules in __all__ without them being imported is -# valid, and causes them to be included in a star import. See #6523 - -__all__ = [ # noqa: F822 # Undefined names in __all__ - "base64mime", # pyright: ignore[reportUnsupportedDunderAll] - "charset", # pyright: ignore[reportUnsupportedDunderAll] - "encoders", # pyright: ignore[reportUnsupportedDunderAll] - "errors", # pyright: ignore[reportUnsupportedDunderAll] - "feedparser", # pyright: ignore[reportUnsupportedDunderAll] - "generator", # pyright: ignore[reportUnsupportedDunderAll] - "header", # pyright: ignore[reportUnsupportedDunderAll] - "iterators", # pyright: ignore[reportUnsupportedDunderAll] - "message", # pyright: ignore[reportUnsupportedDunderAll] - "message_from_file", - "message_from_binary_file", - "message_from_string", - "message_from_bytes", - "mime", # pyright: ignore[reportUnsupportedDunderAll] - "parser", # pyright: ignore[reportUnsupportedDunderAll] - "quoprimime", # pyright: ignore[reportUnsupportedDunderAll] - "utils", # pyright: ignore[reportUnsupportedDunderAll] -] - -# Definitions imported by multiple submodules in typeshed -_ParamType: TypeAlias = str | tuple[str | None, str | None, str] # noqa: Y047 -_ParamsType: TypeAlias = str | None | tuple[str, str | None, str] # noqa: Y047 - -@overload -def message_from_string(s: str) -> Message: ... -@overload -def message_from_string(s: str, _class: Callable[[], _MessageT]) -> _MessageT: ... -@overload -def message_from_string(s: str, _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... -@overload -def message_from_bytes(s: bytes | bytearray) -> Message: ... -@overload -def message_from_bytes(s: bytes | bytearray, _class: Callable[[], _MessageT]) -> _MessageT: ... -@overload -def message_from_bytes( - s: bytes | bytearray, _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT] -) -> _MessageT: ... -@overload -def message_from_file(fp: IO[str]) -> Message: ... -@overload -def message_from_file(fp: IO[str], _class: Callable[[], _MessageT]) -> _MessageT: ... -@overload -def message_from_file(fp: IO[str], _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... -@overload -def message_from_binary_file(fp: IO[bytes]) -> Message: ... -@overload -def message_from_binary_file(fp: IO[bytes], _class: Callable[[], _MessageT]) -> _MessageT: ... -@overload -def message_from_binary_file(fp: IO[bytes], _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi deleted file mode 100644 index f5dbbd96da147..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi +++ /dev/null @@ -1,77 +0,0 @@ -from abc import ABCMeta, abstractmethod -from email.errors import MessageDefect -from email.header import Header -from email.message import Message -from typing import Generic, Protocol, TypeVar, type_check_only -from typing_extensions import Self - -__all__ = ["Policy", "Compat32", "compat32"] - -_MessageT = TypeVar("_MessageT", bound=Message, default=Message) - -@type_check_only -class _MessageFactory(Protocol[_MessageT]): - def __call__(self, policy: Policy[_MessageT]) -> _MessageT: ... - -# Policy below is the only known direct subclass of _PolicyBase. We therefore -# assume that the __init__ arguments and attributes of _PolicyBase are -# the same as those of Policy. -class _PolicyBase(Generic[_MessageT]): - max_line_length: int | None - linesep: str - cte_type: str - raise_on_defect: bool - mangle_from_: bool - message_factory: _MessageFactory[_MessageT] | None - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 - verify_generated_headers: bool - - def __init__( - self, - *, - max_line_length: int | None = 78, - linesep: str = "\n", - cte_type: str = "8bit", - raise_on_defect: bool = False, - mangle_from_: bool = ..., # default depends on sub-class - message_factory: _MessageFactory[_MessageT] | None = None, - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 - verify_generated_headers: bool = True, - ) -> None: ... - def clone( - self, - *, - max_line_length: int | None = ..., - linesep: str = ..., - cte_type: str = ..., - raise_on_defect: bool = ..., - mangle_from_: bool = ..., - message_factory: _MessageFactory[_MessageT] | None = ..., - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 - verify_generated_headers: bool = ..., - ) -> Self: ... - def __add__(self, other: Policy) -> Self: ... - -class Policy(_PolicyBase[_MessageT], metaclass=ABCMeta): - def handle_defect(self, obj: _MessageT, defect: MessageDefect) -> None: ... - def register_defect(self, obj: _MessageT, defect: MessageDefect) -> None: ... - def header_max_count(self, name: str) -> int | None: ... - @abstractmethod - def header_source_parse(self, sourcelines: list[str]) -> tuple[str, str]: ... - @abstractmethod - def header_store_parse(self, name: str, value: str) -> tuple[str, str]: ... - @abstractmethod - def header_fetch_parse(self, name: str, value: str) -> str: ... - @abstractmethod - def fold(self, name: str, value: str) -> str: ... - @abstractmethod - def fold_binary(self, name: str, value: str) -> bytes: ... - -class Compat32(Policy[_MessageT]): - def header_source_parse(self, sourcelines: list[str]) -> tuple[str, str]: ... - def header_store_parse(self, name: str, value: str) -> tuple[str, str]: ... - def header_fetch_parse(self, name: str, value: str) -> str | Header: ... # type: ignore[override] - def fold(self, name: str, value: str) -> str: ... - def fold_binary(self, name: str, value: str) -> bytes: ... - -compat32: Compat32[Message] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi deleted file mode 100644 index f105576c5ee49..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi +++ /dev/null @@ -1,42 +0,0 @@ -import sys - -class MessageError(Exception): ... -class MessageParseError(MessageError): ... -class HeaderParseError(MessageParseError): ... -class BoundaryError(MessageParseError): ... -class MultipartConversionError(MessageError, TypeError): ... -class CharsetError(MessageError): ... - -# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 -class HeaderWriteError(MessageError): ... - -class MessageDefect(ValueError): - def __init__(self, line: str | None = None) -> None: ... - -class NoBoundaryInMultipartDefect(MessageDefect): ... -class StartBoundaryNotFoundDefect(MessageDefect): ... -class FirstHeaderLineIsContinuationDefect(MessageDefect): ... -class MisplacedEnvelopeHeaderDefect(MessageDefect): ... -class MultipartInvariantViolationDefect(MessageDefect): ... -class InvalidMultipartContentTransferEncodingDefect(MessageDefect): ... -class UndecodableBytesDefect(MessageDefect): ... -class InvalidBase64PaddingDefect(MessageDefect): ... -class InvalidBase64CharactersDefect(MessageDefect): ... -class InvalidBase64LengthDefect(MessageDefect): ... -class CloseBoundaryNotFoundDefect(MessageDefect): ... -class MissingHeaderBodySeparatorDefect(MessageDefect): ... - -MalformedHeaderDefect = MissingHeaderBodySeparatorDefect - -class HeaderDefect(MessageDefect): ... -class InvalidHeaderDefect(HeaderDefect): ... -class HeaderMissingRequiredValue(HeaderDefect): ... - -class NonPrintableDefect(HeaderDefect): - def __init__(self, non_printables: str | None) -> None: ... - -class ObsoleteHeaderDefect(HeaderDefect): ... -class NonASCIILocalPartDefect(HeaderDefect): ... - -if sys.version_info >= (3, 10): - class InvalidDateDefect(HeaderDefect): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/message.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/message.pyi deleted file mode 100644 index ebad05a1cf7b6..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/message.pyi +++ /dev/null @@ -1,172 +0,0 @@ -from _typeshed import MaybeNone -from collections.abc import Generator, Iterator, Sequence -from email import _ParamsType, _ParamType -from email.charset import Charset -from email.contentmanager import ContentManager -from email.errors import MessageDefect -from email.policy import Policy -from typing import Any, Generic, Literal, Protocol, TypeVar, overload -from typing_extensions import Self, TypeAlias - -__all__ = ["Message", "EmailMessage"] - -_T = TypeVar("_T") -# Type returned by Policy.header_fetch_parse, often str or Header. -_HeaderT = TypeVar("_HeaderT", default=str) -_HeaderParamT = TypeVar("_HeaderParamT", default=str) -# Represents headers constructed by HeaderRegistry. Those are sub-classes -# of BaseHeader and another header type. -_HeaderRegistryT = TypeVar("_HeaderRegistryT", default=Any) -_HeaderRegistryParamT = TypeVar("_HeaderRegistryParamT", default=Any) - -_PayloadType: TypeAlias = Message | str -_EncodedPayloadType: TypeAlias = Message | bytes -_MultipartPayloadType: TypeAlias = list[_PayloadType] -_CharsetType: TypeAlias = Charset | str | None - -class _SupportsEncodeToPayload(Protocol): - def encode(self, encoding: str, /) -> _PayloadType | _MultipartPayloadType | _SupportsDecodeToPayload: ... - -class _SupportsDecodeToPayload(Protocol): - def decode(self, encoding: str, errors: str, /) -> _PayloadType | _MultipartPayloadType: ... - -class Message(Generic[_HeaderT, _HeaderParamT]): - # The policy attributes and arguments in this class and its subclasses - # would ideally use Policy[Self], but this is not possible. - policy: Policy[Any] # undocumented - preamble: str | None - epilogue: str | None - defects: list[MessageDefect] - def __init__(self, policy: Policy[Any] = ...) -> None: ... - def is_multipart(self) -> bool: ... - def set_unixfrom(self, unixfrom: str) -> None: ... - def get_unixfrom(self) -> str | None: ... - def attach(self, payload: _PayloadType) -> None: ... - # `i: int` without a multipart payload results in an error - # `| MaybeNone` acts like `| Any`: can be None for cleared or unset payload, but annoying to check - @overload # multipart - def get_payload(self, i: int, decode: Literal[True]) -> None: ... - @overload # multipart - def get_payload(self, i: int, decode: Literal[False] = False) -> _PayloadType | MaybeNone: ... - @overload # either - def get_payload(self, i: None = None, decode: Literal[False] = False) -> _PayloadType | _MultipartPayloadType | MaybeNone: ... - @overload # not multipart - def get_payload(self, i: None = None, *, decode: Literal[True]) -> _EncodedPayloadType | MaybeNone: ... - @overload # not multipart, IDEM but w/o kwarg - def get_payload(self, i: None, decode: Literal[True]) -> _EncodedPayloadType | MaybeNone: ... - # If `charset=None` and payload supports both `encode` AND `decode`, - # then an invalid payload could be passed, but this is unlikely - # Not[_SupportsEncodeToPayload] - @overload - def set_payload( - self, payload: _SupportsDecodeToPayload | _PayloadType | _MultipartPayloadType, charset: None = None - ) -> None: ... - @overload - def set_payload( - self, - payload: _SupportsEncodeToPayload | _SupportsDecodeToPayload | _PayloadType | _MultipartPayloadType, - charset: Charset | str, - ) -> None: ... - def set_charset(self, charset: _CharsetType) -> None: ... - def get_charset(self) -> _CharsetType: ... - def __len__(self) -> int: ... - def __contains__(self, name: str) -> bool: ... - def __iter__(self) -> Iterator[str]: ... - # Same as `get` with `failobj=None`, but with the expectation that it won't return None in most scenarios - # This is important for protocols using __getitem__, like SupportsKeysAndGetItem - # Morally, the return type should be `AnyOf[_HeaderType, None]`, - # so using "the Any trick" instead. - def __getitem__(self, name: str) -> _HeaderT | MaybeNone: ... - def __setitem__(self, name: str, val: _HeaderParamT) -> None: ... - def __delitem__(self, name: str) -> None: ... - def keys(self) -> list[str]: ... - def values(self) -> list[_HeaderT]: ... - def items(self) -> list[tuple[str, _HeaderT]]: ... - @overload - def get(self, name: str, failobj: None = None) -> _HeaderT | None: ... - @overload - def get(self, name: str, failobj: _T) -> _HeaderT | _T: ... - @overload - def get_all(self, name: str, failobj: None = None) -> list[_HeaderT] | None: ... - @overload - def get_all(self, name: str, failobj: _T) -> list[_HeaderT] | _T: ... - def add_header(self, _name: str, _value: str, **_params: _ParamsType) -> None: ... - def replace_header(self, _name: str, _value: _HeaderParamT) -> None: ... - def get_content_type(self) -> str: ... - def get_content_maintype(self) -> str: ... - def get_content_subtype(self) -> str: ... - def get_default_type(self) -> str: ... - def set_default_type(self, ctype: str) -> None: ... - @overload - def get_params( - self, failobj: None = None, header: str = "content-type", unquote: bool = True - ) -> list[tuple[str, str]] | None: ... - @overload - def get_params(self, failobj: _T, header: str = "content-type", unquote: bool = True) -> list[tuple[str, str]] | _T: ... - @overload - def get_param( - self, param: str, failobj: None = None, header: str = "content-type", unquote: bool = True - ) -> _ParamType | None: ... - @overload - def get_param(self, param: str, failobj: _T, header: str = "content-type", unquote: bool = True) -> _ParamType | _T: ... - def del_param(self, param: str, header: str = "content-type", requote: bool = True) -> None: ... - def set_type(self, type: str, header: str = "Content-Type", requote: bool = True) -> None: ... - @overload - def get_filename(self, failobj: None = None) -> str | None: ... - @overload - def get_filename(self, failobj: _T) -> str | _T: ... - @overload - def get_boundary(self, failobj: None = None) -> str | None: ... - @overload - def get_boundary(self, failobj: _T) -> str | _T: ... - def set_boundary(self, boundary: str) -> None: ... - @overload - def get_content_charset(self) -> str | None: ... - @overload - def get_content_charset(self, failobj: _T) -> str | _T: ... - @overload - def get_charsets(self, failobj: None = None) -> list[str | None]: ... - @overload - def get_charsets(self, failobj: _T) -> list[str | _T]: ... - def walk(self) -> Generator[Self, None, None]: ... - def get_content_disposition(self) -> str | None: ... - def as_string(self, unixfrom: bool = False, maxheaderlen: int = 0, policy: Policy[Any] | None = None) -> str: ... - def as_bytes(self, unixfrom: bool = False, policy: Policy[Any] | None = None) -> bytes: ... - def __bytes__(self) -> bytes: ... - def set_param( - self, - param: str, - value: str, - header: str = "Content-Type", - requote: bool = True, - charset: str | None = None, - language: str = "", - replace: bool = False, - ) -> None: ... - # The following two methods are undocumented, but a source code comment states that they are public API - def set_raw(self, name: str, value: _HeaderParamT) -> None: ... - def raw_items(self) -> Iterator[tuple[str, _HeaderT]]: ... - -class MIMEPart(Message[_HeaderRegistryT, _HeaderRegistryParamT]): - def __init__(self, policy: Policy[Any] | None = None) -> None: ... - def get_body(self, preferencelist: Sequence[str] = ("related", "html", "plain")) -> MIMEPart[_HeaderRegistryT] | None: ... - 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. - 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: ... - def set_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> None: ... - def make_related(self, boundary: str | None = None) -> None: ... - def make_alternative(self, boundary: str | None = None) -> None: ... - def make_mixed(self, boundary: str | None = None) -> None: ... - def add_related(self, *args: Any, content_manager: ContentManager | None = ..., **kw: Any) -> None: ... - def add_alternative(self, *args: Any, content_manager: ContentManager | None = ..., **kw: Any) -> None: ... - def add_attachment(self, *args: Any, content_manager: ContentManager | None = ..., **kw: Any) -> None: ... - def clear(self) -> None: ... - def clear_content(self) -> None: ... - def as_string(self, unixfrom: bool = False, maxheaderlen: int | None = None, policy: Policy[Any] | None = None) -> str: ... - def is_attachment(self) -> bool: ... - -class EmailMessage(MIMEPart): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/message.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/message.pyi deleted file mode 100644 index 2a5f46296150b..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/message.pyi +++ /dev/null @@ -1,7 +0,0 @@ -from email.mime.nonmultipart import MIMENonMultipart -from email.policy import Policy, _MessageT - -__all__ = ["MIMEMessage"] - -class MIMEMessage(MIMENonMultipart): - def __init__(self, _msg: _MessageT, _subtype: str = "rfc822", *, policy: Policy[_MessageT] | None = None) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/parser.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/parser.pyi deleted file mode 100644 index a1a57b4eef4b1..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/parser.pyi +++ /dev/null @@ -1,38 +0,0 @@ -from _typeshed import SupportsRead -from collections.abc import Callable -from email.feedparser import BytesFeedParser as BytesFeedParser, FeedParser as FeedParser -from email.message import Message -from email.policy import Policy -from io import _WrappedBuffer -from typing import Generic, TypeVar, overload - -__all__ = ["Parser", "HeaderParser", "BytesParser", "BytesHeaderParser", "FeedParser", "BytesFeedParser"] - -_MessageT = TypeVar("_MessageT", bound=Message, default=Message) - -class Parser(Generic[_MessageT]): - @overload - def __init__(self: Parser[Message[str, str]], _class: None = None, *, policy: Policy[Message[str, str]] = ...) -> None: ... - @overload - def __init__(self, _class: Callable[[], _MessageT], *, policy: Policy[_MessageT] = ...) -> None: ... - def parse(self, fp: SupportsRead[str], headersonly: bool = False) -> _MessageT: ... - def parsestr(self, text: str, headersonly: bool = False) -> _MessageT: ... - -class HeaderParser(Parser[_MessageT]): - def parse(self, fp: SupportsRead[str], headersonly: bool = True) -> _MessageT: ... - def parsestr(self, text: str, headersonly: bool = True) -> _MessageT: ... - -class BytesParser(Generic[_MessageT]): - parser: Parser[_MessageT] - @overload - def __init__( - self: BytesParser[Message[str, str]], _class: None = None, *, policy: Policy[Message[str, str]] = ... - ) -> None: ... - @overload - def __init__(self, _class: Callable[[], _MessageT], *, policy: Policy[_MessageT] = ...) -> None: ... - def parse(self, fp: _WrappedBuffer, headersonly: bool = False) -> _MessageT: ... - def parsebytes(self, text: bytes | bytearray, headersonly: bool = False) -> _MessageT: ... - -class BytesHeaderParser(BytesParser[_MessageT]): - def parse(self, fp: _WrappedBuffer, headersonly: bool = True) -> _MessageT: ... - def parsebytes(self, text: bytes | bytearray, headersonly: bool = True) -> _MessageT: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi deleted file mode 100644 index 2e83f0f65a71a..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi +++ /dev/null @@ -1,10 +0,0 @@ -from _typeshed import Incomplete -from codecs import CodecInfo - -class CodecRegistryError(LookupError, SystemError): ... - -def normalize_encoding(encoding: str | bytes) -> str: ... -def search_function(encoding: str) -> CodecInfo | None: ... - -# Needed for submodules -def __getattr__(name: str) -> Incomplete: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fnmatch.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/fnmatch.pyi deleted file mode 100644 index 7051c999c4305..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/fnmatch.pyi +++ /dev/null @@ -1,9 +0,0 @@ -from collections.abc import Iterable -from typing import AnyStr - -__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] - -def fnmatch(name: AnyStr, pat: AnyStr) -> bool: ... -def fnmatchcase(name: AnyStr, pat: AnyStr) -> bool: ... -def filter(names: Iterable[AnyStr], pat: AnyStr) -> list[AnyStr]: ... -def translate(pat: str) -> str: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/functools.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/functools.pyi deleted file mode 100644 index d35c295754e5e..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/functools.pyi +++ /dev/null @@ -1,206 +0,0 @@ -import sys -import types -from _typeshed import SupportsAllComparisons, SupportsItems -from collections.abc import Callable, Hashable, Iterable, Sized -from types import GenericAlias -from typing import Any, Generic, Literal, NamedTuple, TypedDict, TypeVar, final, overload -from typing_extensions import ParamSpec, Self, TypeAlias - -__all__ = [ - "update_wrapper", - "wraps", - "WRAPPER_ASSIGNMENTS", - "WRAPPER_UPDATES", - "total_ordering", - "cmp_to_key", - "lru_cache", - "reduce", - "partial", - "partialmethod", - "singledispatch", - "cached_property", - "singledispatchmethod", - "cache", -] - -_T = TypeVar("_T") -_T_co = TypeVar("_T_co", covariant=True) -_S = TypeVar("_S") -_PWrapped = ParamSpec("_PWrapped") -_RWrapped = TypeVar("_RWrapped") -_PWrapper = ParamSpec("_PWrapper") -_RWrapper = TypeVar("_RWrapper") - -@overload -def reduce(function: Callable[[_T, _S], _T], sequence: Iterable[_S], initial: _T, /) -> _T: ... -@overload -def reduce(function: Callable[[_T, _T], _T], sequence: Iterable[_T], /) -> _T: ... - -class _CacheInfo(NamedTuple): - hits: int - misses: int - maxsize: int | None - currsize: int - -class _CacheParameters(TypedDict): - maxsize: int - typed: bool - -@final -class _lru_cache_wrapper(Generic[_T]): - __wrapped__: Callable[..., _T] - def __call__(self, *args: Hashable, **kwargs: Hashable) -> _T: ... - def cache_info(self) -> _CacheInfo: ... - def cache_clear(self) -> None: ... - def cache_parameters(self) -> _CacheParameters: ... - def __copy__(self) -> _lru_cache_wrapper[_T]: ... - def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_T]: ... - -@overload -def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _T]], _lru_cache_wrapper[_T]]: ... -@overload -def lru_cache(maxsize: Callable[..., _T], typed: bool = False) -> _lru_cache_wrapper[_T]: ... - -if sys.version_info >= (3, 12): - WRAPPER_ASSIGNMENTS: tuple[ - Literal["__module__"], - Literal["__name__"], - Literal["__qualname__"], - Literal["__doc__"], - Literal["__annotations__"], - Literal["__type_params__"], - ] -else: - WRAPPER_ASSIGNMENTS: tuple[ - Literal["__module__"], Literal["__name__"], Literal["__qualname__"], Literal["__doc__"], Literal["__annotations__"] - ] -WRAPPER_UPDATES: tuple[Literal["__dict__"]] - -class _Wrapped(Generic[_PWrapped, _RWrapped, _PWrapper, _RWrapper]): - __wrapped__: Callable[_PWrapped, _RWrapped] - def __call__(self, *args: _PWrapper.args, **kwargs: _PWrapper.kwargs) -> _RWrapper: ... - # as with ``Callable``, we'll assume that these attributes exist - __name__: str - __qualname__: str - -class _Wrapper(Generic[_PWrapped, _RWrapped]): - def __call__(self, f: Callable[_PWrapper, _RWrapper]) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... - -if sys.version_info >= (3, 12): - def update_wrapper( - wrapper: Callable[_PWrapper, _RWrapper], - wrapped: Callable[_PWrapped, _RWrapped], - assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__", "__type_params__"), - updated: Iterable[str] = ("__dict__",), - ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... - def wraps( - wrapped: Callable[_PWrapped, _RWrapped], - assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__", "__type_params__"), - updated: Iterable[str] = ("__dict__",), - ) -> _Wrapper[_PWrapped, _RWrapped]: ... - -else: - def update_wrapper( - wrapper: Callable[_PWrapper, _RWrapper], - wrapped: Callable[_PWrapped, _RWrapped], - assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"), - updated: Iterable[str] = ("__dict__",), - ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... - def wraps( - wrapped: Callable[_PWrapped, _RWrapped], - assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"), - updated: Iterable[str] = ("__dict__",), - ) -> _Wrapper[_PWrapped, _RWrapped]: ... - -def total_ordering(cls: type[_T]) -> type[_T]: ... -def cmp_to_key(mycmp: Callable[[_T, _T], int]) -> Callable[[_T], SupportsAllComparisons]: ... - -class partial(Generic[_T]): - @property - def func(self) -> Callable[..., _T]: ... - @property - def args(self) -> tuple[Any, ...]: ... - @property - def keywords(self) -> dict[str, Any]: ... - def __new__(cls, func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> Self: ... - def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -# With protocols, this could change into a generic protocol that defines __get__ and returns _T -_Descriptor: TypeAlias = Any - -class partialmethod(Generic[_T]): - func: Callable[..., _T] | _Descriptor - args: tuple[Any, ...] - keywords: dict[str, Any] - @overload - def __init__(self, func: Callable[..., _T], /, *args: Any, **keywords: Any) -> None: ... - @overload - def __init__(self, func: _Descriptor, /, *args: Any, **keywords: Any) -> None: ... - def __get__(self, obj: Any, cls: type[Any] | None = None) -> Callable[..., _T]: ... - @property - def __isabstractmethod__(self) -> bool: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -if sys.version_info >= (3, 11): - _RegType: TypeAlias = type[Any] | types.UnionType -else: - _RegType: TypeAlias = type[Any] - -class _SingleDispatchCallable(Generic[_T]): - registry: types.MappingProxyType[Any, Callable[..., _T]] - def dispatch(self, cls: Any) -> Callable[..., _T]: ... - # @fun.register(complex) - # def _(arg, verbose=False): ... - @overload - def register(self, cls: _RegType, func: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... - # @fun.register - # def _(arg: int, verbose=False): - @overload - def register(self, cls: Callable[..., _T], func: None = None) -> Callable[..., _T]: ... - # fun.register(int, lambda x: x) - @overload - def register(self, cls: _RegType, func: Callable[..., _T]) -> Callable[..., _T]: ... - def _clear_cache(self) -> None: ... - def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... - -def singledispatch(func: Callable[..., _T]) -> _SingleDispatchCallable[_T]: ... - -class singledispatchmethod(Generic[_T]): - dispatcher: _SingleDispatchCallable[_T] - func: Callable[..., _T] - def __init__(self, func: Callable[..., _T]) -> None: ... - @property - def __isabstractmethod__(self) -> bool: ... - @overload - def register(self, cls: _RegType, method: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... - @overload - def register(self, cls: Callable[..., _T], method: None = None) -> Callable[..., _T]: ... - @overload - def register(self, cls: _RegType, method: Callable[..., _T]) -> Callable[..., _T]: ... - def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]: ... - -class cached_property(Generic[_T_co]): - func: Callable[[Any], _T_co] - attrname: str | None - def __init__(self, func: Callable[[Any], _T_co]) -> None: ... - @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... - @overload - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T_co: ... - def __set_name__(self, owner: type[Any], name: str) -> None: ... - # __set__ is not defined at runtime, but @cached_property is designed to be settable - def __set__(self, instance: object, value: _T_co) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -def cache(user_function: Callable[..., _T], /) -> _lru_cache_wrapper[_T]: ... -def _make_key( - args: tuple[Hashable, ...], - kwds: SupportsItems[Any, Any], - typed: bool, - kwd_mark: tuple[object, ...] = ..., - fasttypes: set[type] = ..., - tuple: type = ..., - type: Any = ..., - len: Callable[[Sized], int] = ..., -) -> Hashable: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/getpass.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/getpass.pyi deleted file mode 100644 index 6104e0dedfee4..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/getpass.pyi +++ /dev/null @@ -1,8 +0,0 @@ -from typing import TextIO - -__all__ = ["getpass", "getuser", "GetPassWarning"] - -def getpass(prompt: str = "Password: ", stream: TextIO | None = None) -> str: ... -def getuser() -> str: ... - -class GetPassWarning(UserWarning): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/server.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/http/server.pyi deleted file mode 100644 index 1a6fde6000d9f..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/server.pyi +++ /dev/null @@ -1,84 +0,0 @@ -import _socket -import email.message -import io -import socketserver -import sys -from _typeshed import StrPath, SupportsRead, SupportsWrite -from collections.abc import Mapping, Sequence -from typing import Any, AnyStr, BinaryIO, ClassVar -from typing_extensions import deprecated - -__all__ = ["HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler"] - -class HTTPServer(socketserver.TCPServer): - server_name: str - server_port: int - -class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): ... - -class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): - client_address: tuple[str, int] - close_connection: bool - requestline: str - command: str - path: str - request_version: str - headers: email.message.Message - server_version: str - sys_version: str - error_message_format: str - error_content_type: str - protocol_version: str - MessageClass: type - responses: Mapping[int, tuple[str, str]] - default_request_version: str # undocumented - weekdayname: ClassVar[Sequence[str]] # undocumented - monthname: ClassVar[Sequence[str | None]] # undocumented - def handle_one_request(self) -> None: ... - def handle_expect_100(self) -> bool: ... - def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None: ... - def send_response(self, code: int, message: str | None = None) -> None: ... - def send_header(self, keyword: str, value: str) -> None: ... - def send_response_only(self, code: int, message: str | None = None) -> None: ... - def end_headers(self) -> None: ... - def flush_headers(self) -> None: ... - def log_request(self, code: int | str = "-", size: int | str = "-") -> None: ... - def log_error(self, format: str, *args: Any) -> None: ... - def log_message(self, format: str, *args: Any) -> None: ... - def version_string(self) -> str: ... - def date_time_string(self, timestamp: float | None = None) -> str: ... - def log_date_time_string(self) -> str: ... - def address_string(self) -> str: ... - def parse_request(self) -> bool: ... # undocumented - -class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): - extensions_map: dict[str, str] - if sys.version_info >= (3, 12): - index_pages: ClassVar[tuple[str, ...]] - directory: str - def __init__( - self, - request: socketserver._RequestType, - client_address: _socket._RetAddress, - server: socketserver.BaseServer, - *, - directory: StrPath | None = None, - ) -> None: ... - def do_GET(self) -> None: ... - def do_HEAD(self) -> None: ... - def send_head(self) -> io.BytesIO | BinaryIO | None: ... # undocumented - def list_directory(self, path: StrPath) -> io.BytesIO | None: ... # undocumented - def translate_path(self, path: str) -> str: ... # undocumented - def copyfile(self, source: SupportsRead[AnyStr], outputfile: SupportsWrite[AnyStr]) -> None: ... # undocumented - def guess_type(self, path: StrPath) -> str: ... # undocumented - -def executable(path: StrPath) -> bool: ... # undocumented -@deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.15") -class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): - cgi_directories: list[str] - have_fork: bool # undocumented - def do_POST(self) -> None: ... - def is_cgi(self) -> bool: ... # undocumented - def is_executable(self, path: StrPath) -> bool: ... # undocumented - def is_python(self, path: StrPath) -> bool: ... # undocumented - def run_cgi(self) -> None: ... # undocumented diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi deleted file mode 100644 index 8a106b3a64d7f..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi +++ /dev/null @@ -1,172 +0,0 @@ -import _ast -import sys -import types -from _typeshed import ReadableBuffer, StrPath -from abc import ABCMeta, abstractmethod -from collections.abc import Iterator, Mapping, Sequence -from importlib import _bootstrap_external -from importlib.machinery import ModuleSpec -from io import BufferedReader -from typing import IO, Any, Literal, Protocol, overload, runtime_checkable - -if sys.version_info >= (3, 11): - __all__ = [ - "Loader", - "MetaPathFinder", - "PathEntryFinder", - "ResourceLoader", - "InspectLoader", - "ExecutionLoader", - "FileLoader", - "SourceLoader", - ] - - if sys.version_info < (3, 12): - __all__ += ["Finder", "ResourceReader", "Traversable", "TraversableResources"] - -if sys.version_info >= (3, 10): - from importlib._abc import Loader as Loader -else: - class Loader(metaclass=ABCMeta): - def load_module(self, fullname: str) -> types.ModuleType: ... - def module_repr(self, module: types.ModuleType) -> str: ... - def create_module(self, spec: ModuleSpec) -> types.ModuleType | None: ... - # Not defined on the actual class for backwards-compatibility reasons, - # but expected in new code. - def exec_module(self, module: types.ModuleType) -> None: ... - -if sys.version_info < (3, 12): - class Finder(metaclass=ABCMeta): ... - -class ResourceLoader(Loader): - @abstractmethod - def get_data(self, path: str) -> bytes: ... - -class InspectLoader(Loader): - def is_package(self, fullname: str) -> bool: ... - def get_code(self, fullname: str) -> types.CodeType | None: ... - @abstractmethod - def get_source(self, fullname: str) -> str | None: ... - def exec_module(self, module: types.ModuleType) -> None: ... - @staticmethod - def source_to_code( - data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive, path: ReadableBuffer | StrPath = "" - ) -> types.CodeType: ... - -class ExecutionLoader(InspectLoader): - @abstractmethod - def get_filename(self, fullname: str) -> str: ... - -class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLoader, metaclass=ABCMeta): # type: ignore[misc] # incompatible definitions of source_to_code in the base classes - def path_mtime(self, path: str) -> float: ... - def set_data(self, path: str, data: bytes) -> None: ... - def get_source(self, fullname: str) -> str | None: ... - def path_stats(self, path: str) -> Mapping[str, Any]: ... - -# The base classes differ starting in 3.10: -if sys.version_info >= (3, 10): - # Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol - class MetaPathFinder(metaclass=ABCMeta): - if sys.version_info < (3, 12): - def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ... - - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec( - self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / - ) -> ModuleSpec | None: ... - - class PathEntryFinder(metaclass=ABCMeta): - if sys.version_info < (3, 12): - def find_module(self, fullname: str) -> Loader | None: ... - def find_loader(self, fullname: str) -> tuple[Loader | None, Sequence[str]]: ... - - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... - -else: - # Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol - class MetaPathFinder(Finder): - def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ... - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec( - self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / - ) -> ModuleSpec | None: ... - - class PathEntryFinder(Finder): - def find_module(self, fullname: str) -> Loader | None: ... - def find_loader(self, fullname: str) -> tuple[Loader | None, Sequence[str]]: ... - def invalidate_caches(self) -> None: ... - # Not defined on the actual class, but expected to exist. - def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... - -class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader, metaclass=ABCMeta): - name: str - path: str - def __init__(self, fullname: str, path: str) -> None: ... - def get_data(self, path: str) -> bytes: ... - def get_filename(self, name: str | None = None) -> str: ... - def load_module(self, name: str | None = None) -> types.ModuleType: ... - -class ResourceReader(metaclass=ABCMeta): - @abstractmethod - def open_resource(self, resource: str) -> IO[bytes]: ... - @abstractmethod - def resource_path(self, resource: str) -> str: ... - if sys.version_info >= (3, 10): - @abstractmethod - def is_resource(self, path: str) -> bool: ... - else: - @abstractmethod - def is_resource(self, name: str) -> bool: ... - - @abstractmethod - def contents(self) -> Iterator[str]: ... - -@runtime_checkable -class Traversable(Protocol): - @abstractmethod - def is_dir(self) -> bool: ... - @abstractmethod - def is_file(self) -> bool: ... - @abstractmethod - def iterdir(self) -> Iterator[Traversable]: ... - if sys.version_info >= (3, 11): - @abstractmethod - def joinpath(self, *descendants: str) -> Traversable: ... - else: - @abstractmethod - def joinpath(self, child: str, /) -> Traversable: ... - - # The documentation and runtime protocol allows *args, **kwargs arguments, - # but this would mean that all implementers would have to support them, - # which is not the case. - @overload - @abstractmethod - def open(self, mode: Literal["r"] = "r", *, encoding: str | None = None, errors: str | None = None) -> IO[str]: ... - @overload - @abstractmethod - def open(self, mode: Literal["rb"]) -> IO[bytes]: ... - @property - @abstractmethod - def name(self) -> str: ... - if sys.version_info >= (3, 10): - def __truediv__(self, child: str, /) -> Traversable: ... - else: - @abstractmethod - def __truediv__(self, child: str, /) -> Traversable: ... - - @abstractmethod - def read_bytes(self) -> bytes: ... - @abstractmethod - def read_text(self, encoding: str | None = None) -> str: ... - -class TraversableResources(ResourceReader): - @abstractmethod - def files(self) -> Traversable: ... - def open_resource(self, resource: str) -> BufferedReader: ... - def resource_path(self, resource: Any) -> str: ... - def is_resource(self, path: str) -> bool: ... - def contents(self) -> Iterator[str]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi deleted file mode 100644 index bb1a6f93d0e09..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi +++ /dev/null @@ -1,20 +0,0 @@ -import sys -from importlib._bootstrap import BuiltinImporter as BuiltinImporter, FrozenImporter as FrozenImporter, ModuleSpec as ModuleSpec -from importlib._bootstrap_external import ( - BYTECODE_SUFFIXES as BYTECODE_SUFFIXES, - DEBUG_BYTECODE_SUFFIXES as DEBUG_BYTECODE_SUFFIXES, - EXTENSION_SUFFIXES as EXTENSION_SUFFIXES, - OPTIMIZED_BYTECODE_SUFFIXES as OPTIMIZED_BYTECODE_SUFFIXES, - SOURCE_SUFFIXES as SOURCE_SUFFIXES, - ExtensionFileLoader as ExtensionFileLoader, - FileFinder as FileFinder, - PathFinder as PathFinder, - SourceFileLoader as SourceFileLoader, - SourcelessFileLoader as SourcelessFileLoader, - WindowsRegistryFinder as WindowsRegistryFinder, -) - -if sys.version_info >= (3, 11): - from importlib._bootstrap_external import NamespaceLoader as NamespaceLoader - -def all_suffixes() -> list[str]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi deleted file mode 100644 index 15d8b50b09d21..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi +++ /dev/null @@ -1,290 +0,0 @@ -import abc -import pathlib -import sys -import types -from _collections_abc import dict_keys, dict_values -from _typeshed import StrPath -from collections.abc import Iterable, Iterator, Mapping -from email.message import Message -from importlib.abc import MetaPathFinder -from os import PathLike -from pathlib import Path -from re import Pattern -from typing import Any, ClassVar, Generic, NamedTuple, TypeVar, overload -from typing_extensions import Self, TypeAlias - -_T = TypeVar("_T") -_KT = TypeVar("_KT") -_VT = TypeVar("_VT") - -__all__ = [ - "Distribution", - "DistributionFinder", - "PackageNotFoundError", - "distribution", - "distributions", - "entry_points", - "files", - "metadata", - "requires", - "version", -] - -if sys.version_info >= (3, 10): - __all__ += ["PackageMetadata", "packages_distributions"] - -if sys.version_info >= (3, 10): - from importlib.metadata._meta import PackageMetadata as PackageMetadata, SimplePath - def packages_distributions() -> Mapping[str, list[str]]: ... - - _SimplePath: TypeAlias = SimplePath - -else: - _SimplePath: TypeAlias = Path - -class PackageNotFoundError(ModuleNotFoundError): - @property - def name(self) -> str: ... # type: ignore[override] - -if sys.version_info >= (3, 13): - _EntryPointBase = object -elif sys.version_info >= (3, 11): - class DeprecatedTuple: - def __getitem__(self, item: int) -> str: ... - - _EntryPointBase = DeprecatedTuple -else: - class _EntryPointBase(NamedTuple): - name: str - value: str - group: str - -class EntryPoint(_EntryPointBase): - pattern: ClassVar[Pattern[str]] - if sys.version_info >= (3, 11): - name: str - value: str - group: str - - def __init__(self, name: str, value: str, group: str) -> None: ... - - def load(self) -> Any: ... # Callable[[], Any] or an importable module - @property - def extras(self) -> list[str]: ... - @property - def module(self) -> str: ... - @property - def attr(self) -> str: ... - if sys.version_info >= (3, 10): - dist: ClassVar[Distribution | None] - def matches( - self, - *, - name: str = ..., - value: str = ..., - group: str = ..., - module: str = ..., - attr: str = ..., - extras: list[str] = ..., - ) -> bool: ... # undocumented - - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - if sys.version_info >= (3, 11): - def __lt__(self, other: object) -> bool: ... - if sys.version_info < (3, 12): - def __iter__(self) -> Iterator[Any]: ... # result of iter((str, Self)), really - -if sys.version_info >= (3, 12): - class EntryPoints(tuple[EntryPoint, ...]): - def __getitem__(self, name: str) -> EntryPoint: ... # type: ignore[override] - def select( - self, - *, - name: str = ..., - value: str = ..., - group: str = ..., - module: str = ..., - attr: str = ..., - extras: list[str] = ..., - ) -> EntryPoints: ... - @property - def names(self) -> set[str]: ... - @property - def groups(self) -> set[str]: ... - -elif sys.version_info >= (3, 10): - class DeprecatedList(list[_T]): ... - - class EntryPoints(DeprecatedList[EntryPoint]): # use as list is deprecated since 3.10 - # int argument is deprecated since 3.10 - def __getitem__(self, name: int | str) -> EntryPoint: ... # type: ignore[override] - def select( - self, - *, - name: str = ..., - value: str = ..., - group: str = ..., - module: str = ..., - attr: str = ..., - extras: list[str] = ..., - ) -> EntryPoints: ... - @property - def names(self) -> set[str]: ... - @property - def groups(self) -> set[str]: ... - -if sys.version_info >= (3, 10) and sys.version_info < (3, 12): - class Deprecated(Generic[_KT, _VT]): - def __getitem__(self, name: _KT) -> _VT: ... - @overload - def get(self, name: _KT, default: None = None) -> _VT | None: ... - @overload - def get(self, name: _KT, default: _T) -> _VT | _T: ... - def __iter__(self) -> Iterator[_KT]: ... - def __contains__(self, *args: object) -> bool: ... - def keys(self) -> dict_keys[_KT, _VT]: ... - def values(self) -> dict_values[_KT, _VT]: ... - - class SelectableGroups(Deprecated[str, EntryPoints], dict[str, EntryPoints]): # use as dict is deprecated since 3.10 - @classmethod - def load(cls, eps: Iterable[EntryPoint]) -> Self: ... - @property - def groups(self) -> set[str]: ... - @property - def names(self) -> set[str]: ... - @overload - def select(self) -> Self: ... - @overload - def select( - self, - *, - name: str = ..., - value: str = ..., - group: str = ..., - module: str = ..., - attr: str = ..., - extras: list[str] = ..., - ) -> EntryPoints: ... - -class PackagePath(pathlib.PurePosixPath): - def read_text(self, encoding: str = "utf-8") -> str: ... - def read_binary(self) -> bytes: ... - def locate(self) -> PathLike[str]: ... - # The following attributes are not defined on PackagePath, but are dynamically added by Distribution.files: - hash: FileHash | None - size: int | None - dist: Distribution - -class FileHash: - mode: str - value: str - def __init__(self, spec: str) -> None: ... - -if sys.version_info >= (3, 12): - class DeprecatedNonAbstract: ... - _distribution_parent = DeprecatedNonAbstract -else: - _distribution_parent = object - -class Distribution(_distribution_parent): - @abc.abstractmethod - def read_text(self, filename: str) -> str | None: ... - @abc.abstractmethod - def locate_file(self, path: StrPath) -> _SimplePath: ... - @classmethod - def from_name(cls, name: str) -> Distribution: ... - @overload - @classmethod - def discover(cls, *, context: DistributionFinder.Context) -> Iterable[Distribution]: ... - @overload - @classmethod - def discover( - cls, *, context: None = None, name: str | None = ..., path: list[str] = ..., **kwargs: Any - ) -> Iterable[Distribution]: ... - @staticmethod - def at(path: StrPath) -> PathDistribution: ... - - if sys.version_info >= (3, 10): - @property - def metadata(self) -> PackageMetadata: ... - @property - def entry_points(self) -> EntryPoints: ... - else: - @property - def metadata(self) -> Message: ... - @property - def entry_points(self) -> list[EntryPoint]: ... - - @property - def version(self) -> str: ... - @property - def files(self) -> list[PackagePath] | None: ... - @property - def requires(self) -> list[str] | None: ... - if sys.version_info >= (3, 10): - @property - def name(self) -> str: ... - if sys.version_info >= (3, 13): - @property - def origin(self) -> types.SimpleNamespace: ... - -class DistributionFinder(MetaPathFinder): - class Context: - name: str | None - def __init__(self, *, name: str | None = ..., path: list[str] = ..., **kwargs: Any) -> None: ... - @property - def path(self) -> list[str]: ... - - @abc.abstractmethod - def find_distributions(self, context: DistributionFinder.Context = ...) -> Iterable[Distribution]: ... - -class MetadataPathFinder(DistributionFinder): - @classmethod - def find_distributions(cls, context: DistributionFinder.Context = ...) -> Iterable[PathDistribution]: ... - if sys.version_info >= (3, 11): - @classmethod - def invalidate_caches(cls) -> None: ... - elif sys.version_info >= (3, 10): - # Yes, this is an instance method that has a parameter named "cls" - def invalidate_caches(cls) -> None: ... - -class PathDistribution(Distribution): - _path: _SimplePath - def __init__(self, path: _SimplePath) -> None: ... - def read_text(self, filename: StrPath) -> str | None: ... - def locate_file(self, path: StrPath) -> _SimplePath: ... - -def distribution(distribution_name: str) -> Distribution: ... -@overload -def distributions(*, context: DistributionFinder.Context) -> Iterable[Distribution]: ... -@overload -def distributions( - *, context: None = None, name: str | None = ..., path: list[str] = ..., **kwargs: Any -) -> Iterable[Distribution]: ... - -if sys.version_info >= (3, 10): - def metadata(distribution_name: str) -> PackageMetadata: ... - -else: - def metadata(distribution_name: str) -> Message: ... - -if sys.version_info >= (3, 12): - def entry_points( - *, name: str = ..., value: str = ..., group: str = ..., module: str = ..., attr: str = ..., extras: list[str] = ... - ) -> EntryPoints: ... - -elif sys.version_info >= (3, 10): - @overload - def entry_points() -> SelectableGroups: ... - @overload - def entry_points( - *, name: str = ..., value: str = ..., group: str = ..., module: str = ..., attr: str = ..., extras: list[str] = ... - ) -> EntryPoints: ... - -else: - def entry_points() -> dict[str, list[EntryPoint]]: ... - -def version(distribution_name: str) -> str: ... -def files(distribution_name: str) -> list[PackagePath] | None: ... -def requires(distribution_name: str) -> list[str] | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi deleted file mode 100644 index 2cf6366b6cb3b..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi +++ /dev/null @@ -1,76 +0,0 @@ -import os -import sys -from collections.abc import Iterator -from contextlib import AbstractContextManager -from importlib.abc import Traversable -from pathlib import Path -from types import ModuleType -from typing import Any, BinaryIO, Literal, TextIO -from typing_extensions import TypeAlias - -if sys.version_info >= (3, 11): - from importlib.resources._common import Package as Package -else: - Package: TypeAlias = str | ModuleType - -__all__ = [ - "Package", - "as_file", - "contents", - "files", - "is_resource", - "open_binary", - "open_text", - "path", - "read_binary", - "read_text", -] - -if sys.version_info >= (3, 10): - __all__ += ["ResourceReader"] - -if sys.version_info < (3, 13): - __all__ += ["Resource"] - -if sys.version_info < (3, 11): - Resource: TypeAlias = str | os.PathLike[Any] -elif sys.version_info < (3, 13): - Resource: TypeAlias = str - -if sys.version_info >= (3, 12): - from importlib.resources._common import Anchor as Anchor - - __all__ += ["Anchor"] - -if sys.version_info >= (3, 13): - from importlib.resources._functional import ( - contents as contents, - is_resource as is_resource, - open_binary as open_binary, - open_text as open_text, - path as path, - read_binary as read_binary, - read_text as read_text, - ) - -else: - def open_binary(package: Package, resource: Resource) -> BinaryIO: ... - def open_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> TextIO: ... - def read_binary(package: Package, resource: Resource) -> bytes: ... - def read_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> str: ... - def path(package: Package, resource: Resource) -> AbstractContextManager[Path, Literal[False]]: ... - def is_resource(package: Package, name: str) -> bool: ... - def contents(package: Package) -> Iterator[str]: ... - -if sys.version_info >= (3, 11): - from importlib.resources._common import as_file as as_file -else: - def as_file(path: Traversable) -> AbstractContextManager[Path, Literal[False]]: ... - -if sys.version_info >= (3, 11): - from importlib.resources._common import files as files -else: - def files(package: Package) -> Traversable: ... - -if sys.version_info >= (3, 10): - from importlib.abc import ResourceReader as ResourceReader diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi deleted file mode 100644 index ad80605f7c71d..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi +++ /dev/null @@ -1,14 +0,0 @@ -import sys - -if sys.version_info >= (3, 11): - # These are all actually defined in this file on 3.11+, - # and re-exported from importlib.abc, - # but it's much less code duplication for typeshed if we pretend that they're still defined - # in importlib.abc on 3.11+, and re-exported from this file - from importlib.abc import ( - ResourceReader as ResourceReader, - Traversable as Traversable, - TraversableResources as TraversableResources, - ) - - __all__ = ["ResourceReader", "Traversable", "TraversableResources"] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/util.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/util.pyi deleted file mode 100644 index cc1c98ae4d0e4..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/util.pyi +++ /dev/null @@ -1,33 +0,0 @@ -import importlib.abc -import importlib.machinery -import sys -import types -from _typeshed import ReadableBuffer -from collections.abc import Callable -from importlib._bootstrap import module_from_spec as module_from_spec, spec_from_loader as spec_from_loader -from importlib._bootstrap_external import ( - MAGIC_NUMBER as MAGIC_NUMBER, - cache_from_source as cache_from_source, - decode_source as decode_source, - source_from_cache as source_from_cache, - spec_from_file_location as spec_from_file_location, -) -from typing_extensions import ParamSpec - -_P = ParamSpec("_P") - -if sys.version_info < (3, 12): - def module_for_loader(fxn: Callable[_P, types.ModuleType]) -> Callable[_P, types.ModuleType]: ... - def set_loader(fxn: Callable[_P, types.ModuleType]) -> Callable[_P, types.ModuleType]: ... - def set_package(fxn: Callable[_P, types.ModuleType]) -> Callable[_P, types.ModuleType]: ... - -def resolve_name(name: str, package: str | None) -> str: ... -def find_spec(name: str, package: str | None = None) -> importlib.machinery.ModuleSpec | None: ... - -class LazyLoader(importlib.abc.Loader): - def __init__(self, loader: importlib.abc.Loader) -> None: ... - @classmethod - def factory(cls, loader: importlib.abc.Loader) -> Callable[..., LazyLoader]: ... - def exec_module(self, module: types.ModuleType) -> None: ... - -def source_hash(source_bytes: ReadableBuffer) -> bytes: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/__init__.pyi deleted file mode 100644 index e555f74a81af7..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/__init__.pyi +++ /dev/null @@ -1,657 +0,0 @@ -import sys -import threading -from _typeshed import StrPath, SupportsWrite -from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence -from io import TextIOWrapper -from re import Pattern -from string import Template -from time import struct_time -from types import FrameType, GenericAlias, TracebackType -from typing import Any, ClassVar, Final, Generic, Literal, Protocol, TextIO, TypeVar, overload -from typing_extensions import Self, TypeAlias, deprecated - -__all__ = [ - "BASIC_FORMAT", - "BufferingFormatter", - "CRITICAL", - "DEBUG", - "ERROR", - "FATAL", - "FileHandler", - "Filter", - "Formatter", - "Handler", - "INFO", - "LogRecord", - "Logger", - "LoggerAdapter", - "NOTSET", - "NullHandler", - "StreamHandler", - "WARN", - "WARNING", - "addLevelName", - "basicConfig", - "captureWarnings", - "critical", - "debug", - "disable", - "error", - "exception", - "fatal", - "getLevelName", - "getLogger", - "getLoggerClass", - "info", - "log", - "makeLogRecord", - "setLoggerClass", - "shutdown", - "warning", - "getLogRecordFactory", - "setLogRecordFactory", - "lastResort", - "raiseExceptions", - "warn", -] - -if sys.version_info >= (3, 11): - __all__ += ["getLevelNamesMapping"] -if sys.version_info >= (3, 12): - __all__ += ["getHandlerByName", "getHandlerNames"] - -_SysExcInfoType: TypeAlias = tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] -_ExcInfoType: TypeAlias = None | bool | _SysExcInfoType | BaseException -_ArgsType: TypeAlias = tuple[object, ...] | Mapping[str, object] -_Level: TypeAlias = int | str -_FormatStyle: TypeAlias = Literal["%", "{", "$"] - -if sys.version_info >= (3, 12): - class _SupportsFilter(Protocol): - def filter(self, record: LogRecord, /) -> bool | LogRecord: ... - - _FilterType: TypeAlias = Filter | Callable[[LogRecord], bool | LogRecord] | _SupportsFilter -else: - class _SupportsFilter(Protocol): - def filter(self, record: LogRecord, /) -> bool: ... - - _FilterType: TypeAlias = Filter | Callable[[LogRecord], bool] | _SupportsFilter - -raiseExceptions: bool -logThreads: bool -logMultiprocessing: bool -logProcesses: bool -_srcfile: str | None - -def currentframe() -> FrameType: ... - -_levelToName: dict[int, str] -_nameToLevel: dict[str, int] - -class Filterer: - filters: list[_FilterType] - def addFilter(self, filter: _FilterType) -> None: ... - def removeFilter(self, filter: _FilterType) -> None: ... - if sys.version_info >= (3, 12): - def filter(self, record: LogRecord) -> bool | LogRecord: ... - else: - def filter(self, record: LogRecord) -> bool: ... - -class Manager: # undocumented - root: RootLogger - disable: int - emittedNoHandlerWarning: bool - loggerDict: dict[str, Logger | PlaceHolder] - loggerClass: type[Logger] | None - logRecordFactory: Callable[..., LogRecord] | None - def __init__(self, rootnode: RootLogger) -> None: ... - def getLogger(self, name: str) -> Logger: ... - def setLoggerClass(self, klass: type[Logger]) -> None: ... - def setLogRecordFactory(self, factory: Callable[..., LogRecord]) -> None: ... - -class Logger(Filterer): - name: str # undocumented - level: int # undocumented - parent: Logger | None # undocumented - propagate: bool - handlers: list[Handler] # undocumented - disabled: bool # undocumented - root: ClassVar[RootLogger] # undocumented - manager: Manager # undocumented - def __init__(self, name: str, level: _Level = 0) -> None: ... - def setLevel(self, level: _Level) -> None: ... - def isEnabledFor(self, level: int) -> bool: ... - def getEffectiveLevel(self) -> int: ... - def getChild(self, suffix: str) -> Self: ... # see python/typing#980 - if sys.version_info >= (3, 12): - def getChildren(self) -> set[Logger]: ... - - def debug( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def info( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def warning( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - @deprecated("Deprecated; use warning() instead.") - def warn( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def error( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def exception( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = True, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def critical( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def log( - self, - level: int, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - ) -> None: ... - def _log( - self, - level: int, - msg: object, - args: _ArgsType, - exc_info: _ExcInfoType | None = None, - extra: Mapping[str, object] | None = None, - stack_info: bool = False, - stacklevel: int = 1, - ) -> None: ... # undocumented - fatal = critical - def addHandler(self, hdlr: Handler) -> None: ... - def removeHandler(self, hdlr: Handler) -> None: ... - def findCaller(self, stack_info: bool = False, stacklevel: int = 1) -> tuple[str, int, str, str | None]: ... - def handle(self, record: LogRecord) -> None: ... - def makeRecord( - self, - name: str, - level: int, - fn: str, - lno: int, - msg: object, - args: _ArgsType, - exc_info: _SysExcInfoType | None, - func: str | None = None, - extra: Mapping[str, object] | None = None, - sinfo: str | None = None, - ) -> LogRecord: ... - def hasHandlers(self) -> bool: ... - def callHandlers(self, record: LogRecord) -> None: ... # undocumented - -CRITICAL: Final = 50 -FATAL: Final = CRITICAL -ERROR: Final = 40 -WARNING: Final = 30 -WARN: Final = WARNING -INFO: Final = 20 -DEBUG: Final = 10 -NOTSET: Final = 0 - -class Handler(Filterer): - level: int # undocumented - formatter: Formatter | None # undocumented - lock: threading.Lock | None # undocumented - name: str | None # undocumented - def __init__(self, level: _Level = 0) -> None: ... - def get_name(self) -> str: ... # undocumented - def set_name(self, name: str) -> None: ... # undocumented - def createLock(self) -> None: ... - def acquire(self) -> None: ... - def release(self) -> None: ... - def setLevel(self, level: _Level) -> None: ... - def setFormatter(self, fmt: Formatter | None) -> None: ... - def flush(self) -> None: ... - def close(self) -> None: ... - def handle(self, record: LogRecord) -> bool: ... - def handleError(self, record: LogRecord) -> None: ... - def format(self, record: LogRecord) -> str: ... - def emit(self, record: LogRecord) -> None: ... - -if sys.version_info >= (3, 12): - def getHandlerByName(name: str) -> Handler | None: ... - def getHandlerNames() -> frozenset[str]: ... - -class Formatter: - converter: Callable[[float | None], struct_time] - _fmt: str | None # undocumented - datefmt: str | None # undocumented - _style: PercentStyle # undocumented - default_time_format: str - default_msec_format: str | None - - if sys.version_info >= (3, 10): - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - style: _FormatStyle = "%", - validate: bool = True, - *, - defaults: Mapping[str, Any] | None = None, - ) -> None: ... - else: - def __init__( - self, fmt: str | None = None, datefmt: str | None = None, style: _FormatStyle = "%", validate: bool = True - ) -> None: ... - - def format(self, record: LogRecord) -> str: ... - def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str: ... - def formatException(self, ei: _SysExcInfoType) -> str: ... - def formatMessage(self, record: LogRecord) -> str: ... # undocumented - def formatStack(self, stack_info: str) -> str: ... - def usesTime(self) -> bool: ... # undocumented - -class BufferingFormatter: - linefmt: Formatter - def __init__(self, linefmt: Formatter | None = None) -> None: ... - def formatHeader(self, records: Sequence[LogRecord]) -> str: ... - def formatFooter(self, records: Sequence[LogRecord]) -> str: ... - def format(self, records: Sequence[LogRecord]) -> str: ... - -class Filter: - name: str # undocumented - nlen: int # undocumented - def __init__(self, name: str = "") -> None: ... - if sys.version_info >= (3, 12): - def filter(self, record: LogRecord) -> bool | LogRecord: ... - else: - def filter(self, record: LogRecord) -> bool: ... - -class LogRecord: - # args can be set to None by logging.handlers.QueueHandler - # (see https://bugs.python.org/issue44473) - args: _ArgsType | None - asctime: str - created: float - exc_info: _SysExcInfoType | None - exc_text: str | None - filename: str - funcName: str - levelname: str - levelno: int - lineno: int - module: str - msecs: float - # Only created when logging.Formatter.format is called. See #6132. - message: str - msg: str | Any # The runtime accepts any object, but will be a str in 99% of cases - name: str - pathname: str - process: int | None - processName: str | None - relativeCreated: float - stack_info: str | None - thread: int | None - threadName: str | None - if sys.version_info >= (3, 12): - taskName: str | None - - def __init__( - self, - name: str, - level: int, - pathname: str, - lineno: int, - msg: object, - args: _ArgsType | None, - exc_info: _SysExcInfoType | None, - func: str | None = None, - sinfo: str | None = None, - ) -> None: ... - def getMessage(self) -> str: ... - # Allows setting contextual information on LogRecord objects as per the docs, see #7833 - def __setattr__(self, name: str, value: Any, /) -> None: ... - -_L = TypeVar("_L", bound=Logger | LoggerAdapter[Any]) - -class LoggerAdapter(Generic[_L]): - logger: _L - manager: Manager # undocumented - - if sys.version_info >= (3, 13): - def __init__(self, logger: _L, extra: Mapping[str, object] | None = None, merge_extra: bool = False) -> None: ... - elif sys.version_info >= (3, 10): - def __init__(self, logger: _L, extra: Mapping[str, object] | None = None) -> None: ... - else: - def __init__(self, logger: _L, extra: Mapping[str, object]) -> None: ... - - if sys.version_info >= (3, 10): - extra: Mapping[str, object] | None - else: - extra: Mapping[str, object] - - def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]: ... - def debug( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def info( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def warning( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - @deprecated("Deprecated; use warning() instead.") - def warn( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def error( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def exception( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = True, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def critical( - self, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def log( - self, - level: int, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, - **kwargs: object, - ) -> None: ... - def isEnabledFor(self, level: int) -> bool: ... - def getEffectiveLevel(self) -> int: ... - def setLevel(self, level: _Level) -> None: ... - def hasHandlers(self) -> bool: ... - if sys.version_info >= (3, 11): - def _log( - self, - level: int, - msg: object, - args: _ArgsType, - *, - exc_info: _ExcInfoType | None = None, - extra: Mapping[str, object] | None = None, - stack_info: bool = False, - ) -> None: ... # undocumented - else: - def _log( - self, - level: int, - msg: object, - args: _ArgsType, - exc_info: _ExcInfoType | None = None, - extra: Mapping[str, object] | None = None, - stack_info: bool = False, - ) -> None: ... # undocumented - - @property - def name(self) -> str: ... # undocumented - if sys.version_info >= (3, 11): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -def getLogger(name: str | None = None) -> Logger: ... -def getLoggerClass() -> type[Logger]: ... -def getLogRecordFactory() -> Callable[..., LogRecord]: ... -def debug( - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -def info( - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -def warning( - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -@deprecated("Deprecated; use warning() instead.") -def warn( - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -def error( - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -def critical( - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -def exception( - msg: object, - *args: object, - exc_info: _ExcInfoType = True, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... -def log( - level: int, - msg: object, - *args: object, - exc_info: _ExcInfoType = None, - stack_info: bool = False, - stacklevel: int = 1, - extra: Mapping[str, object] | None = None, -) -> None: ... - -fatal = critical - -def disable(level: int = 50) -> None: ... -def addLevelName(level: int, levelName: str) -> None: ... -@overload -def getLevelName(level: int) -> str: ... -@overload -@deprecated("The str -> int case is considered a mistake.") -def getLevelName(level: str) -> Any: ... - -if sys.version_info >= (3, 11): - def getLevelNamesMapping() -> dict[str, int]: ... - -def makeLogRecord(dict: Mapping[str, object]) -> LogRecord: ... -def basicConfig( - *, - filename: StrPath | None = ..., - filemode: str = ..., - format: str = ..., - datefmt: str | None = ..., - style: _FormatStyle = ..., - level: _Level | None = ..., - stream: SupportsWrite[str] | None = ..., - handlers: Iterable[Handler] | None = ..., - force: bool | None = ..., - encoding: str | None = ..., - errors: str | None = ..., -) -> None: ... -def shutdown(handlerList: Sequence[Any] = ...) -> None: ... # handlerList is undocumented -def setLoggerClass(klass: type[Logger]) -> None: ... -def captureWarnings(capture: bool) -> None: ... -def setLogRecordFactory(factory: Callable[..., LogRecord]) -> None: ... - -lastResort: Handler | None - -_StreamT = TypeVar("_StreamT", bound=SupportsWrite[str]) - -class StreamHandler(Handler, Generic[_StreamT]): - stream: _StreamT # undocumented - terminator: str - @overload - def __init__(self: StreamHandler[TextIO], stream: None = None) -> None: ... - @overload - def __init__(self: StreamHandler[_StreamT], stream: _StreamT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 - def setStream(self, stream: _StreamT) -> _StreamT | None: ... - if sys.version_info >= (3, 11): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -class FileHandler(StreamHandler[TextIOWrapper]): - baseFilename: str # undocumented - mode: str # undocumented - encoding: str | None # undocumented - delay: bool # undocumented - errors: str | None # undocumented - def __init__( - self, filename: StrPath, mode: str = "a", encoding: str | None = None, delay: bool = False, errors: str | None = None - ) -> None: ... - def _open(self) -> TextIOWrapper: ... # undocumented - -class NullHandler(Handler): ... - -class PlaceHolder: # undocumented - loggerMap: dict[Logger, None] - def __init__(self, alogger: Logger) -> None: ... - def append(self, alogger: Logger) -> None: ... - -# Below aren't in module docs but still visible - -class RootLogger(Logger): - def __init__(self, level: int) -> None: ... - -root: RootLogger - -class PercentStyle: # undocumented - default_format: str - asctime_format: str - asctime_search: str - validation_pattern: Pattern[str] - _fmt: str - if sys.version_info >= (3, 10): - def __init__(self, fmt: str, *, defaults: Mapping[str, Any] | None = None) -> None: ... - else: - def __init__(self, fmt: str) -> None: ... - - def usesTime(self) -> bool: ... - def validate(self) -> None: ... - def format(self, record: Any) -> str: ... - -class StrFormatStyle(PercentStyle): # undocumented - fmt_spec: Pattern[str] - field_spec: Pattern[str] - -class StringTemplateStyle(PercentStyle): # undocumented - _tpl: Template - -_STYLES: Final[dict[str, tuple[PercentStyle, str]]] - -BASIC_FORMAT: Final[str] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi deleted file mode 100644 index 31b9828563554..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi +++ /dev/null @@ -1,31 +0,0 @@ -from _typeshed import FileDescriptorLike, Unused -from collections.abc import Sequence -from struct import Struct -from typing import Any, Final - -__all__ = ["ensure_running", "get_inherited_fds", "connect_to_new_process", "set_forkserver_preload"] - -MAXFDS_TO_SEND: Final = 256 -SIGNED_STRUCT: Final[Struct] - -class ForkServer: - def set_forkserver_preload(self, modules_names: list[str]) -> None: ... - def get_inherited_fds(self) -> list[int] | None: ... - def connect_to_new_process(self, fds: Sequence[int]) -> tuple[int, int]: ... - def ensure_running(self) -> None: ... - -def main( - listener_fd: int | None, - alive_r: FileDescriptorLike, - preload: Sequence[str], - main_path: str | None = None, - sys_path: Unused = None, -) -> None: ... -def read_signed(fd: int) -> Any: ... -def write_signed(fd: int, n: int) -> None: ... - -_forkserver: ForkServer -ensure_running = _forkserver.ensure_running -get_inherited_fds = _forkserver.get_inherited_fds -connect_to_new_process = _forkserver.connect_to_new_process -set_forkserver_preload = _forkserver.set_forkserver_preload diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi deleted file mode 100644 index d5b6384afd5ed..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi +++ /dev/null @@ -1,98 +0,0 @@ -import threading -from _typeshed import ConvertibleToInt, Incomplete, Unused -from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence -from logging import Logger, _Level as _LoggingLevel -from typing import Any, Final, Generic, TypeVar, overload - -__all__ = [ - "sub_debug", - "debug", - "info", - "sub_warning", - "get_logger", - "log_to_stderr", - "get_temp_dir", - "register_after_fork", - "is_exiting", - "Finalize", - "ForkAwareThreadLock", - "ForkAwareLocal", - "close_all_fds_except", - "SUBDEBUG", - "SUBWARNING", -] - -_T = TypeVar("_T") -_R_co = TypeVar("_R_co", default=Any, covariant=True) - -NOTSET: Final[int] -SUBDEBUG: Final[int] -DEBUG: Final[int] -INFO: Final[int] -SUBWARNING: Final[int] - -LOGGER_NAME: Final[str] -DEFAULT_LOGGING_FORMAT: Final[str] - -def sub_debug(msg: object, *args: object) -> None: ... -def debug(msg: object, *args: object) -> None: ... -def info(msg: object, *args: object) -> None: ... -def sub_warning(msg: object, *args: object) -> None: ... -def get_logger() -> Logger: ... -def log_to_stderr(level: _LoggingLevel | None = None) -> Logger: ... -def is_abstract_socket_namespace(address: str | bytes | None) -> bool: ... - -abstract_sockets_supported: bool - -def get_temp_dir() -> str: ... -def register_after_fork(obj: _T, func: Callable[[_T], object]) -> None: ... - -class Finalize(Generic[_R_co]): - # "args" and "kwargs" are passed as arguments to "callback". - @overload - def __init__( - self, - obj: None, - callback: Callable[..., _R_co], - *, - args: Sequence[Any] = (), - kwargs: Mapping[str, Any] | None = None, - exitpriority: int, - ) -> None: ... - @overload - def __init__( - self, obj: None, callback: Callable[..., _R_co], args: Sequence[Any], kwargs: Mapping[str, Any] | None, exitpriority: int - ) -> None: ... - @overload - def __init__( - self, - obj: Any, - callback: Callable[..., _R_co], - args: Sequence[Any] = (), - kwargs: Mapping[str, Any] | None = None, - exitpriority: int | None = None, - ) -> None: ... - def __call__( - self, - wr: Unused = None, - _finalizer_registry: MutableMapping[Incomplete, Incomplete] = {}, - sub_debug: Callable[..., object] = ..., - getpid: Callable[[], int] = ..., - ) -> _R_co: ... - def cancel(self) -> None: ... - def still_active(self) -> bool: ... - -def is_exiting() -> bool: ... - -class ForkAwareThreadLock: - acquire: Callable[[bool, float], bool] - release: Callable[[], None] - def __enter__(self) -> bool: ... - def __exit__(self, *args: Unused) -> None: ... - -class ForkAwareLocal(threading.local): ... - -MAXFD: Final[int] - -def close_all_fds_except(fds: Iterable[int]) -> None: ... -def spawnv_passfds(path: bytes, args: Sequence[ConvertibleToInt], passfds: Sequence[int]) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nturl2path.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/nturl2path.pyi deleted file mode 100644 index b8ad8d6821554..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/nturl2path.pyi +++ /dev/null @@ -1,2 +0,0 @@ -def url2pathname(url: str) -> str: ... -def pathname2url(p: str) -> str: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/os/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/os/__init__.pyi deleted file mode 100644 index d0ef614abbceb..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/os/__init__.pyi +++ /dev/null @@ -1,1653 +0,0 @@ -import sys -from _typeshed import ( - AnyStr_co, - BytesPath, - FileDescriptor, - FileDescriptorLike, - FileDescriptorOrPath, - GenericPath, - OpenBinaryMode, - OpenBinaryModeReading, - OpenBinaryModeUpdating, - OpenBinaryModeWriting, - OpenTextMode, - ReadableBuffer, - StrOrBytesPath, - StrPath, - SupportsLenAndGetItem, - Unused, - WriteableBuffer, - structseq, -) -from abc import ABC, abstractmethod -from builtins import OSError -from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping, Sequence -from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper -from subprocess import Popen -from types import GenericAlias, TracebackType -from typing import ( - IO, - Any, - AnyStr, - BinaryIO, - Final, - Generic, - Literal, - NoReturn, - Protocol, - TypeVar, - final, - overload, - runtime_checkable, -) -from typing_extensions import Self, TypeAlias, Unpack, deprecated - -from . import path as _path - -__all__ = [ - "F_OK", - "O_APPEND", - "O_CREAT", - "O_EXCL", - "O_RDONLY", - "O_RDWR", - "O_TRUNC", - "O_WRONLY", - "P_NOWAIT", - "P_NOWAITO", - "P_WAIT", - "R_OK", - "SEEK_CUR", - "SEEK_END", - "SEEK_SET", - "TMP_MAX", - "W_OK", - "X_OK", - "DirEntry", - "_exit", - "abort", - "access", - "altsep", - "chdir", - "chmod", - "close", - "closerange", - "cpu_count", - "curdir", - "defpath", - "device_encoding", - "devnull", - "dup", - "dup2", - "environ", - "error", - "execl", - "execle", - "execlp", - "execlpe", - "execv", - "execve", - "execvp", - "execvpe", - "extsep", - "fdopen", - "fsdecode", - "fsencode", - "fspath", - "fstat", - "fsync", - "ftruncate", - "get_exec_path", - "get_inheritable", - "get_terminal_size", - "getcwd", - "getcwdb", - "getenv", - "getlogin", - "getpid", - "getppid", - "isatty", - "kill", - "linesep", - "link", - "listdir", - "lseek", - "lstat", - "makedirs", - "mkdir", - "name", - "open", - "pardir", - "path", - "pathsep", - "pipe", - "popen", - "putenv", - "read", - "readlink", - "remove", - "removedirs", - "rename", - "renames", - "replace", - "rmdir", - "scandir", - "sep", - "set_inheritable", - "spawnl", - "spawnle", - "spawnv", - "spawnve", - "stat", - "stat_result", - "statvfs_result", - "strerror", - "supports_bytes_environ", - "symlink", - "system", - "terminal_size", - "times", - "times_result", - "truncate", - "umask", - "uname_result", - "unlink", - "unsetenv", - "urandom", - "utime", - "waitpid", - "waitstatus_to_exitcode", - "walk", - "write", -] -if sys.platform == "darwin" and sys.version_info >= (3, 12): - __all__ += ["PRIO_DARWIN_BG", "PRIO_DARWIN_NONUI", "PRIO_DARWIN_PROCESS", "PRIO_DARWIN_THREAD"] -if sys.platform == "darwin" and sys.version_info >= (3, 10): - __all__ += ["O_EVTONLY", "O_NOFOLLOW_ANY", "O_SYMLINK"] -if sys.platform == "linux": - __all__ += [ - "GRND_NONBLOCK", - "GRND_RANDOM", - "MFD_ALLOW_SEALING", - "MFD_CLOEXEC", - "MFD_HUGETLB", - "MFD_HUGE_16GB", - "MFD_HUGE_16MB", - "MFD_HUGE_1GB", - "MFD_HUGE_1MB", - "MFD_HUGE_256MB", - "MFD_HUGE_2GB", - "MFD_HUGE_2MB", - "MFD_HUGE_32MB", - "MFD_HUGE_512KB", - "MFD_HUGE_512MB", - "MFD_HUGE_64KB", - "MFD_HUGE_8MB", - "MFD_HUGE_MASK", - "MFD_HUGE_SHIFT", - "O_DIRECT", - "O_LARGEFILE", - "O_NOATIME", - "O_PATH", - "O_RSYNC", - "O_TMPFILE", - "P_PIDFD", - "RTLD_DEEPBIND", - "SCHED_BATCH", - "SCHED_IDLE", - "SCHED_RESET_ON_FORK", - "XATTR_CREATE", - "XATTR_REPLACE", - "XATTR_SIZE_MAX", - "copy_file_range", - "getrandom", - "getxattr", - "listxattr", - "memfd_create", - "pidfd_open", - "removexattr", - "setxattr", - ] -if sys.platform == "linux" and sys.version_info >= (3, 13): - __all__ += [ - "POSIX_SPAWN_CLOSEFROM", - "TFD_CLOEXEC", - "TFD_NONBLOCK", - "TFD_TIMER_ABSTIME", - "TFD_TIMER_CANCEL_ON_SET", - "timerfd_create", - "timerfd_gettime", - "timerfd_gettime_ns", - "timerfd_settime", - "timerfd_settime_ns", - ] -if sys.platform == "linux" and sys.version_info >= (3, 12): - __all__ += [ - "CLONE_FILES", - "CLONE_FS", - "CLONE_NEWCGROUP", - "CLONE_NEWIPC", - "CLONE_NEWNET", - "CLONE_NEWNS", - "CLONE_NEWPID", - "CLONE_NEWTIME", - "CLONE_NEWUSER", - "CLONE_NEWUTS", - "CLONE_SIGHAND", - "CLONE_SYSVSEM", - "CLONE_THREAD", - "CLONE_VM", - "setns", - "unshare", - "PIDFD_NONBLOCK", - ] -if sys.platform == "linux" and sys.version_info >= (3, 10): - __all__ += [ - "EFD_CLOEXEC", - "EFD_NONBLOCK", - "EFD_SEMAPHORE", - "RWF_APPEND", - "SPLICE_F_MORE", - "SPLICE_F_MOVE", - "SPLICE_F_NONBLOCK", - "eventfd", - "eventfd_read", - "eventfd_write", - "splice", - ] -if sys.platform == "win32": - __all__ += [ - "O_BINARY", - "O_NOINHERIT", - "O_RANDOM", - "O_SEQUENTIAL", - "O_SHORT_LIVED", - "O_TEMPORARY", - "O_TEXT", - "P_DETACH", - "P_OVERLAY", - "get_handle_inheritable", - "set_handle_inheritable", - "startfile", - ] -if sys.platform == "win32" and sys.version_info >= (3, 12): - __all__ += ["listdrives", "listmounts", "listvolumes"] -if sys.platform != "win32": - __all__ += [ - "CLD_CONTINUED", - "CLD_DUMPED", - "CLD_EXITED", - "CLD_KILLED", - "CLD_STOPPED", - "CLD_TRAPPED", - "EX_CANTCREAT", - "EX_CONFIG", - "EX_DATAERR", - "EX_IOERR", - "EX_NOHOST", - "EX_NOINPUT", - "EX_NOPERM", - "EX_NOUSER", - "EX_OSERR", - "EX_OSFILE", - "EX_PROTOCOL", - "EX_SOFTWARE", - "EX_TEMPFAIL", - "EX_UNAVAILABLE", - "EX_USAGE", - "F_LOCK", - "F_TEST", - "F_TLOCK", - "F_ULOCK", - "NGROUPS_MAX", - "O_ACCMODE", - "O_ASYNC", - "O_CLOEXEC", - "O_DIRECTORY", - "O_DSYNC", - "O_NDELAY", - "O_NOCTTY", - "O_NOFOLLOW", - "O_NONBLOCK", - "O_SYNC", - "POSIX_SPAWN_CLOSE", - "POSIX_SPAWN_DUP2", - "POSIX_SPAWN_OPEN", - "PRIO_PGRP", - "PRIO_PROCESS", - "PRIO_USER", - "P_ALL", - "P_PGID", - "P_PID", - "RTLD_GLOBAL", - "RTLD_LAZY", - "RTLD_LOCAL", - "RTLD_NODELETE", - "RTLD_NOLOAD", - "RTLD_NOW", - "SCHED_FIFO", - "SCHED_OTHER", - "SCHED_RR", - "SEEK_DATA", - "SEEK_HOLE", - "ST_NOSUID", - "ST_RDONLY", - "WCONTINUED", - "WCOREDUMP", - "WEXITED", - "WEXITSTATUS", - "WIFCONTINUED", - "WIFEXITED", - "WIFSIGNALED", - "WIFSTOPPED", - "WNOHANG", - "WNOWAIT", - "WSTOPPED", - "WSTOPSIG", - "WTERMSIG", - "WUNTRACED", - "chown", - "chroot", - "confstr", - "confstr_names", - "ctermid", - "environb", - "fchdir", - "fchown", - "fork", - "forkpty", - "fpathconf", - "fstatvfs", - "fwalk", - "getegid", - "getenvb", - "geteuid", - "getgid", - "getgrouplist", - "getgroups", - "getloadavg", - "getpgid", - "getpgrp", - "getpriority", - "getsid", - "getuid", - "initgroups", - "killpg", - "lchown", - "lockf", - "major", - "makedev", - "minor", - "mkfifo", - "mknod", - "nice", - "openpty", - "pathconf", - "pathconf_names", - "posix_spawn", - "posix_spawnp", - "pread", - "preadv", - "pwrite", - "pwritev", - "readv", - "register_at_fork", - "sched_get_priority_max", - "sched_get_priority_min", - "sched_yield", - "sendfile", - "setegid", - "seteuid", - "setgid", - "setgroups", - "setpgid", - "setpgrp", - "setpriority", - "setregid", - "setreuid", - "setsid", - "setuid", - "spawnlp", - "spawnlpe", - "spawnvp", - "spawnvpe", - "statvfs", - "sync", - "sysconf", - "sysconf_names", - "tcgetpgrp", - "tcsetpgrp", - "ttyname", - "uname", - "wait", - "wait3", - "wait4", - "writev", - ] -if sys.platform != "win32" and sys.version_info >= (3, 13): - __all__ += ["grantpt", "posix_openpt", "ptsname", "unlockpt"] -if sys.platform != "win32" and sys.version_info >= (3, 11): - __all__ += ["login_tty"] -if sys.platform != "win32" and sys.version_info >= (3, 10): - __all__ += ["O_FSYNC"] -if sys.platform != "darwin" and sys.platform != "win32": - __all__ += [ - "POSIX_FADV_DONTNEED", - "POSIX_FADV_NOREUSE", - "POSIX_FADV_NORMAL", - "POSIX_FADV_RANDOM", - "POSIX_FADV_SEQUENTIAL", - "POSIX_FADV_WILLNEED", - "RWF_DSYNC", - "RWF_HIPRI", - "RWF_NOWAIT", - "RWF_SYNC", - "ST_APPEND", - "ST_MANDLOCK", - "ST_NOATIME", - "ST_NODEV", - "ST_NODIRATIME", - "ST_NOEXEC", - "ST_RELATIME", - "ST_SYNCHRONOUS", - "ST_WRITE", - "fdatasync", - "getresgid", - "getresuid", - "pipe2", - "posix_fadvise", - "posix_fallocate", - "sched_getaffinity", - "sched_getparam", - "sched_getscheduler", - "sched_param", - "sched_rr_get_interval", - "sched_setaffinity", - "sched_setparam", - "sched_setscheduler", - "setresgid", - "setresuid", - ] -if sys.platform != "linux" and sys.platform != "win32": - __all__ += ["O_EXLOCK", "O_SHLOCK", "chflags", "lchflags"] -if sys.platform != "linux" and sys.platform != "win32" and sys.version_info >= (3, 13): - __all__ += ["O_EXEC", "O_SEARCH"] -if sys.platform != "darwin" or sys.version_info >= (3, 13): - if sys.platform != "win32": - __all__ += ["waitid", "waitid_result"] -if sys.platform != "win32" or sys.version_info >= (3, 13): - __all__ += ["fchmod"] - if sys.platform != "linux": - __all__ += ["lchmod"] -if sys.platform != "win32" or sys.version_info >= (3, 12): - __all__ += ["get_blocking", "set_blocking"] -if sys.platform != "win32" or sys.version_info >= (3, 11): - __all__ += ["EX_OK"] - -# This unnecessary alias is to work around various errors -path = _path - -_T = TypeVar("_T") -_T1 = TypeVar("_T1") -_T2 = TypeVar("_T2") - -# ----- os variables ----- - -error = OSError - -supports_bytes_environ: bool - -supports_dir_fd: set[Callable[..., Any]] -supports_fd: set[Callable[..., Any]] -supports_effective_ids: set[Callable[..., Any]] -supports_follow_symlinks: set[Callable[..., Any]] - -if sys.platform != "win32": - # Unix only - PRIO_PROCESS: int - PRIO_PGRP: int - PRIO_USER: int - - F_LOCK: int - F_TLOCK: int - F_ULOCK: int - F_TEST: int - - if sys.platform != "darwin": - POSIX_FADV_NORMAL: int - POSIX_FADV_SEQUENTIAL: int - POSIX_FADV_RANDOM: int - POSIX_FADV_NOREUSE: int - POSIX_FADV_WILLNEED: int - POSIX_FADV_DONTNEED: int - - if sys.platform != "linux" and sys.platform != "darwin": - # In the os-module docs, these are marked as being available - # on "Unix, not Emscripten, not WASI." - # However, in the source code, a comment indicates they're "FreeBSD constants". - # sys.platform could have one of many values on a FreeBSD Python build, - # so the sys-module docs recommend doing `if sys.platform.startswith('freebsd')` - # to detect FreeBSD builds. Unfortunately that would be too dynamic - # for type checkers, however. - SF_NODISKIO: int - SF_MNOWAIT: int - SF_SYNC: int - - if sys.version_info >= (3, 11): - SF_NOCACHE: int - - if sys.platform == "linux": - XATTR_SIZE_MAX: int - XATTR_CREATE: int - XATTR_REPLACE: int - - P_PID: int - P_PGID: int - P_ALL: int - - if sys.platform == "linux": - P_PIDFD: int - - WEXITED: int - WSTOPPED: int - WNOWAIT: int - - CLD_EXITED: int - CLD_DUMPED: int - CLD_TRAPPED: int - CLD_CONTINUED: int - CLD_KILLED: int - CLD_STOPPED: int - - SCHED_OTHER: int - SCHED_FIFO: int - SCHED_RR: int - if sys.platform != "darwin" and sys.platform != "linux": - SCHED_SPORADIC: int - -if sys.platform == "linux": - SCHED_BATCH: int - SCHED_IDLE: int - SCHED_RESET_ON_FORK: int - -if sys.platform != "win32": - RTLD_LAZY: int - RTLD_NOW: int - RTLD_GLOBAL: int - RTLD_LOCAL: int - RTLD_NODELETE: int - RTLD_NOLOAD: int - -if sys.platform == "linux": - RTLD_DEEPBIND: int - GRND_NONBLOCK: int - GRND_RANDOM: int - -if sys.platform == "darwin" and sys.version_info >= (3, 12): - PRIO_DARWIN_BG: int - PRIO_DARWIN_NONUI: int - PRIO_DARWIN_PROCESS: int - PRIO_DARWIN_THREAD: int - -SEEK_SET: int -SEEK_CUR: int -SEEK_END: int -if sys.platform != "win32": - SEEK_DATA: int - SEEK_HOLE: int - -O_RDONLY: int -O_WRONLY: int -O_RDWR: int -O_APPEND: int -O_CREAT: int -O_EXCL: int -O_TRUNC: int -if sys.platform == "win32": - O_BINARY: int - O_NOINHERIT: int - O_SHORT_LIVED: int - O_TEMPORARY: int - O_RANDOM: int - O_SEQUENTIAL: int - O_TEXT: int - -if sys.platform != "win32": - O_DSYNC: int - O_SYNC: int - O_NDELAY: int - O_NONBLOCK: int - O_NOCTTY: int - O_CLOEXEC: int - O_ASYNC: int # Gnu extension if in C library - O_DIRECTORY: int # Gnu extension if in C library - O_NOFOLLOW: int # Gnu extension if in C library - O_ACCMODE: int # TODO: when does this exist? - -if sys.platform == "linux": - O_RSYNC: int - O_DIRECT: int # Gnu extension if in C library - O_NOATIME: int # Gnu extension if in C library - O_PATH: int # Gnu extension if in C library - O_TMPFILE: int # Gnu extension if in C library - O_LARGEFILE: int # Gnu extension if in C library - -if sys.platform != "linux" and sys.platform != "win32": - O_SHLOCK: int - O_EXLOCK: int - -if sys.platform == "darwin" and sys.version_info >= (3, 10): - O_EVTONLY: int - O_NOFOLLOW_ANY: int - O_SYMLINK: int - -if sys.platform != "win32" and sys.version_info >= (3, 10): - O_FSYNC: int - -if sys.platform != "linux" and sys.platform != "win32" and sys.version_info >= (3, 13): - O_EXEC: int - O_SEARCH: int - -if sys.platform != "win32" and sys.platform != "darwin": - # posix, but apparently missing on macos - ST_APPEND: int - ST_MANDLOCK: int - ST_NOATIME: int - ST_NODEV: int - ST_NODIRATIME: int - ST_NOEXEC: int - ST_RELATIME: int - ST_SYNCHRONOUS: int - ST_WRITE: int - -if sys.platform != "win32": - NGROUPS_MAX: int - ST_NOSUID: int - ST_RDONLY: int - -curdir: str -pardir: str -sep: str -if sys.platform == "win32": - altsep: str -else: - altsep: str | None -extsep: str -pathsep: str -defpath: str -linesep: str -devnull: str -name: str - -F_OK: int -R_OK: int -W_OK: int -X_OK: int - -_EnvironCodeFunc: TypeAlias = Callable[[AnyStr], AnyStr] - -class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): - encodekey: _EnvironCodeFunc[AnyStr] - decodekey: _EnvironCodeFunc[AnyStr] - encodevalue: _EnvironCodeFunc[AnyStr] - decodevalue: _EnvironCodeFunc[AnyStr] - def __init__( - self, - data: MutableMapping[AnyStr, AnyStr], - encodekey: _EnvironCodeFunc[AnyStr], - decodekey: _EnvironCodeFunc[AnyStr], - encodevalue: _EnvironCodeFunc[AnyStr], - decodevalue: _EnvironCodeFunc[AnyStr], - ) -> None: ... - def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... - def copy(self) -> dict[AnyStr, AnyStr]: ... - def __delitem__(self, key: AnyStr) -> None: ... - def __getitem__(self, key: AnyStr) -> AnyStr: ... - def __setitem__(self, key: AnyStr, value: AnyStr) -> None: ... - def __iter__(self) -> Iterator[AnyStr]: ... - def __len__(self) -> int: ... - def __or__(self, other: Mapping[_T1, _T2]) -> dict[AnyStr | _T1, AnyStr | _T2]: ... - def __ror__(self, other: Mapping[_T1, _T2]) -> dict[AnyStr | _T1, AnyStr | _T2]: ... - # We use @overload instead of a Union for reasons similar to those given for - # overloading MutableMapping.update in stdlib/typing.pyi - # The type: ignore is needed due to incompatible __or__/__ior__ signatures - @overload # type: ignore[misc] - def __ior__(self, other: Mapping[AnyStr, AnyStr]) -> Self: ... - @overload - def __ior__(self, other: Iterable[tuple[AnyStr, AnyStr]]) -> Self: ... - -environ: _Environ[str] -if sys.platform != "win32": - environb: _Environ[bytes] - -if sys.version_info >= (3, 11) or sys.platform != "win32": - EX_OK: int - -if sys.platform != "win32": - confstr_names: dict[str, int] - pathconf_names: dict[str, int] - sysconf_names: dict[str, int] - - EX_USAGE: int - EX_DATAERR: int - EX_NOINPUT: int - EX_NOUSER: int - EX_NOHOST: int - EX_UNAVAILABLE: int - EX_SOFTWARE: int - EX_OSERR: int - EX_OSFILE: int - EX_CANTCREAT: int - EX_IOERR: int - EX_TEMPFAIL: int - EX_PROTOCOL: int - EX_NOPERM: int - EX_CONFIG: int - -# Exists on some Unix platforms, e.g. Solaris. -if sys.platform != "win32" and sys.platform != "darwin" and sys.platform != "linux": - EX_NOTFOUND: int - -P_NOWAIT: int -P_NOWAITO: int -P_WAIT: int -if sys.platform == "win32": - P_DETACH: int - P_OVERLAY: int - -# wait()/waitpid() options -if sys.platform != "win32": - WNOHANG: int # Unix only - WCONTINUED: int # some Unix systems - WUNTRACED: int # Unix only - -TMP_MAX: int # Undocumented, but used by tempfile - -# ----- os classes (structures) ----- -@final -class stat_result(structseq[float], tuple[int, int, int, int, int, int, int, float, float, float]): - # The constructor of this class takes an iterable of variable length (though it must be at least 10). - # - # However, this class behaves like a tuple of 10 elements, - # no matter how long the iterable supplied to the constructor is. - # https://github.com/python/typeshed/pull/6560#discussion_r767162532 - # - # The 10 elements always present are st_mode, st_ino, st_dev, st_nlink, - # st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime. - # - # More items may be added at the end by some implementations. - if sys.version_info >= (3, 10): - __match_args__: Final = ("st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size") - - @property - def st_mode(self) -> int: ... # protection bits, - @property - def st_ino(self) -> int: ... # inode number, - @property - def st_dev(self) -> int: ... # device, - @property - def st_nlink(self) -> int: ... # number of hard links, - @property - def st_uid(self) -> int: ... # user id of owner, - @property - def st_gid(self) -> int: ... # group id of owner, - @property - def st_size(self) -> int: ... # size of file, in bytes, - @property - def st_atime(self) -> float: ... # time of most recent access, - @property - def st_mtime(self) -> float: ... # time of most recent content modification, - # platform dependent (time of most recent metadata change on Unix, or the time of creation on Windows) - if sys.version_info >= (3, 12) and sys.platform == "win32": - @property - @deprecated( - """\ -Use st_birthtime instead to retrieve the file creation time. \ -In the future, this property will contain the last metadata change time.""" - ) - def st_ctime(self) -> float: ... - else: - @property - def st_ctime(self) -> float: ... - - @property - def st_atime_ns(self) -> int: ... # time of most recent access, in nanoseconds - @property - def st_mtime_ns(self) -> int: ... # time of most recent content modification in nanoseconds - # platform dependent (time of most recent metadata change on Unix, or the time of creation on Windows) in nanoseconds - @property - def st_ctime_ns(self) -> int: ... - if sys.platform == "win32": - @property - def st_file_attributes(self) -> int: ... - @property - def st_reparse_tag(self) -> int: ... - if sys.version_info >= (3, 12): - @property - def st_birthtime(self) -> float: ... # time of file creation in seconds - @property - def st_birthtime_ns(self) -> int: ... # time of file creation in nanoseconds - else: - @property - def st_blocks(self) -> int: ... # number of blocks allocated for file - @property - def st_blksize(self) -> int: ... # filesystem blocksize - @property - def st_rdev(self) -> int: ... # type of device if an inode device - if sys.platform != "linux": - # These properties are available on MacOS, but not Ubuntu. - # On other Unix systems (such as FreeBSD), the following attributes may be - # available (but may be only filled out if root tries to use them): - @property - def st_gen(self) -> int: ... # file generation number - @property - def st_birthtime(self) -> float: ... # time of file creation in seconds - if sys.platform == "darwin": - @property - def st_flags(self) -> int: ... # user defined flags for file - # Attributes documented as sometimes appearing, but deliberately omitted from the stub: `st_creator`, `st_rsize`, `st_type`. - # See https://github.com/python/typeshed/pull/6560#issuecomment-991253327 - -# mypy and pyright object to this being both ABC and Protocol. -# At runtime it inherits from ABC and is not a Protocol, but it will be -# on the allowlist for use as a Protocol starting in 3.14. -@runtime_checkable -class PathLike(ABC, Protocol[AnyStr_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] - @abstractmethod - def __fspath__(self) -> AnyStr_co: ... - -@overload -def listdir(path: StrPath | None = None) -> list[str]: ... -@overload -def listdir(path: BytesPath) -> list[bytes]: ... -@overload -def listdir(path: int) -> list[str]: ... -@final -class DirEntry(Generic[AnyStr]): - # This is what the scandir iterator yields - # The constructor is hidden - - @property - def name(self) -> AnyStr: ... - @property - def path(self) -> AnyStr: ... - def inode(self) -> int: ... - def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... - def is_file(self, *, follow_symlinks: bool = True) -> bool: ... - def is_symlink(self) -> bool: ... - def stat(self, *, follow_symlinks: bool = True) -> stat_result: ... - def __fspath__(self) -> AnyStr: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - if sys.version_info >= (3, 12): - def is_junction(self) -> bool: ... - -@final -class statvfs_result(structseq[int], tuple[int, int, int, int, int, int, int, int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ( - "f_bsize", - "f_frsize", - "f_blocks", - "f_bfree", - "f_bavail", - "f_files", - "f_ffree", - "f_favail", - "f_flag", - "f_namemax", - ) - - @property - def f_bsize(self) -> int: ... - @property - def f_frsize(self) -> int: ... - @property - def f_blocks(self) -> int: ... - @property - def f_bfree(self) -> int: ... - @property - def f_bavail(self) -> int: ... - @property - def f_files(self) -> int: ... - @property - def f_ffree(self) -> int: ... - @property - def f_favail(self) -> int: ... - @property - def f_flag(self) -> int: ... - @property - def f_namemax(self) -> int: ... - @property - def f_fsid(self) -> int: ... - -# ----- os function stubs ----- -def fsencode(filename: StrOrBytesPath) -> bytes: ... -def fsdecode(filename: StrOrBytesPath) -> str: ... -@overload -def fspath(path: str) -> str: ... -@overload -def fspath(path: bytes) -> bytes: ... -@overload -def fspath(path: PathLike[AnyStr]) -> AnyStr: ... -def get_exec_path(env: Mapping[str, str] | None = None) -> list[str]: ... -def getlogin() -> str: ... -def getpid() -> int: ... -def getppid() -> int: ... -def strerror(code: int, /) -> str: ... -def umask(mask: int, /) -> int: ... -@final -class uname_result(structseq[str], tuple[str, str, str, str, str]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("sysname", "nodename", "release", "version", "machine") - - @property - def sysname(self) -> str: ... - @property - def nodename(self) -> str: ... - @property - def release(self) -> str: ... - @property - def version(self) -> str: ... - @property - def machine(self) -> str: ... - -if sys.platform != "win32": - def ctermid() -> str: ... - def getegid() -> int: ... - def geteuid() -> int: ... - def getgid() -> int: ... - def getgrouplist(user: str, group: int, /) -> list[int]: ... - def getgroups() -> list[int]: ... # Unix only, behaves differently on Mac - def initgroups(username: str, gid: int, /) -> None: ... - def getpgid(pid: int) -> int: ... - def getpgrp() -> int: ... - def getpriority(which: int, who: int) -> int: ... - def setpriority(which: int, who: int, priority: int) -> None: ... - if sys.platform != "darwin": - def getresuid() -> tuple[int, int, int]: ... - def getresgid() -> tuple[int, int, int]: ... - - def getuid() -> int: ... - def setegid(egid: int, /) -> None: ... - def seteuid(euid: int, /) -> None: ... - def setgid(gid: int, /) -> None: ... - def setgroups(groups: Sequence[int], /) -> None: ... - def setpgrp() -> None: ... - def setpgid(pid: int, pgrp: int, /) -> None: ... - def setregid(rgid: int, egid: int, /) -> None: ... - if sys.platform != "darwin": - def setresgid(rgid: int, egid: int, sgid: int, /) -> None: ... - def setresuid(ruid: int, euid: int, suid: int, /) -> None: ... - - def setreuid(ruid: int, euid: int, /) -> None: ... - def getsid(pid: int, /) -> int: ... - def setsid() -> None: ... - def setuid(uid: int, /) -> None: ... - def uname() -> uname_result: ... - -@overload -def getenv(key: str) -> str | None: ... -@overload -def getenv(key: str, default: _T) -> str | _T: ... - -if sys.platform != "win32": - @overload - def getenvb(key: bytes) -> bytes | None: ... - @overload - def getenvb(key: bytes, default: _T) -> bytes | _T: ... - def putenv(name: StrOrBytesPath, value: StrOrBytesPath, /) -> None: ... - def unsetenv(name: StrOrBytesPath, /) -> None: ... - -else: - def putenv(name: str, value: str, /) -> None: ... - def unsetenv(name: str, /) -> None: ... - -_Opener: TypeAlias = Callable[[str, int], int] - -@overload -def fdopen( - fd: int, - mode: OpenTextMode = "r", - buffering: int = -1, - encoding: str | None = None, - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: _Opener | None = ..., -) -> TextIOWrapper: ... -@overload -def fdopen( - fd: int, - mode: OpenBinaryMode, - buffering: Literal[0], - encoding: None = None, - errors: None = None, - newline: None = None, - closefd: bool = ..., - opener: _Opener | None = ..., -) -> FileIO: ... -@overload -def fdopen( - fd: int, - mode: OpenBinaryModeUpdating, - buffering: Literal[-1, 1] = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - closefd: bool = ..., - opener: _Opener | None = ..., -) -> BufferedRandom: ... -@overload -def fdopen( - fd: int, - mode: OpenBinaryModeWriting, - buffering: Literal[-1, 1] = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - closefd: bool = ..., - opener: _Opener | None = ..., -) -> BufferedWriter: ... -@overload -def fdopen( - fd: int, - mode: OpenBinaryModeReading, - buffering: Literal[-1, 1] = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - closefd: bool = ..., - opener: _Opener | None = ..., -) -> BufferedReader: ... -@overload -def fdopen( - fd: int, - mode: OpenBinaryMode, - buffering: int = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - closefd: bool = ..., - opener: _Opener | None = ..., -) -> BinaryIO: ... -@overload -def fdopen( - fd: int, - mode: str, - buffering: int = -1, - encoding: str | None = None, - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: _Opener | None = ..., -) -> IO[Any]: ... -def close(fd: int) -> None: ... -def closerange(fd_low: int, fd_high: int, /) -> None: ... -def device_encoding(fd: int) -> str | None: ... -def dup(fd: int, /) -> int: ... -def dup2(fd: int, fd2: int, inheritable: bool = True) -> int: ... -def fstat(fd: int) -> stat_result: ... -def ftruncate(fd: int, length: int, /) -> None: ... -def fsync(fd: FileDescriptorLike) -> None: ... -def isatty(fd: int, /) -> bool: ... - -if sys.platform != "win32" and sys.version_info >= (3, 11): - def login_tty(fd: int, /) -> None: ... - -if sys.version_info >= (3, 11): - def lseek(fd: int, position: int, whence: int, /) -> int: ... - -else: - def lseek(fd: int, position: int, how: int, /) -> int: ... - -def open(path: StrOrBytesPath, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: ... -def pipe() -> tuple[int, int]: ... -def read(fd: int, length: int, /) -> bytes: ... - -if sys.version_info >= (3, 12) or sys.platform != "win32": - def get_blocking(fd: int, /) -> bool: ... - def set_blocking(fd: int, blocking: bool, /) -> None: ... - -if sys.platform != "win32": - def fchown(fd: int, uid: int, gid: int) -> None: ... - def fpathconf(fd: int, name: str | int, /) -> int: ... - def fstatvfs(fd: int, /) -> statvfs_result: ... - def lockf(fd: int, command: int, length: int, /) -> None: ... - def openpty() -> tuple[int, int]: ... # some flavors of Unix - if sys.platform != "darwin": - def fdatasync(fd: FileDescriptorLike) -> None: ... - def pipe2(flags: int, /) -> tuple[int, int]: ... # some flavors of Unix - def posix_fallocate(fd: int, offset: int, length: int, /) -> None: ... - def posix_fadvise(fd: int, offset: int, length: int, advice: int, /) -> None: ... - - def pread(fd: int, length: int, offset: int, /) -> bytes: ... - def pwrite(fd: int, buffer: ReadableBuffer, offset: int, /) -> int: ... - # In CI, stubtest sometimes reports that these are available on MacOS, sometimes not - def preadv(fd: int, buffers: SupportsLenAndGetItem[WriteableBuffer], offset: int, flags: int = 0, /) -> int: ... - def pwritev(fd: int, buffers: SupportsLenAndGetItem[ReadableBuffer], offset: int, flags: int = 0, /) -> int: ... - if sys.platform != "darwin": - if sys.version_info >= (3, 10): - RWF_APPEND: int # docs say available on 3.7+, stubtest says otherwise - RWF_DSYNC: int - RWF_SYNC: int - RWF_HIPRI: int - RWF_NOWAIT: int - - if sys.platform == "linux": - def sendfile(out_fd: FileDescriptor, in_fd: FileDescriptor, offset: int | None, count: int) -> int: ... - else: - def sendfile( - out_fd: FileDescriptor, - in_fd: FileDescriptor, - offset: int, - count: int, - headers: Sequence[ReadableBuffer] = ..., - trailers: Sequence[ReadableBuffer] = ..., - flags: int = 0, - ) -> int: ... # FreeBSD and Mac OS X only - - def readv(fd: int, buffers: SupportsLenAndGetItem[WriteableBuffer], /) -> int: ... - def writev(fd: int, buffers: SupportsLenAndGetItem[ReadableBuffer], /) -> int: ... - -@final -class terminal_size(structseq[int], tuple[int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("columns", "lines") - - @property - def columns(self) -> int: ... - @property - def lines(self) -> int: ... - -def get_terminal_size(fd: int = ..., /) -> terminal_size: ... -def get_inheritable(fd: int, /) -> bool: ... -def set_inheritable(fd: int, inheritable: bool, /) -> None: ... - -if sys.platform == "win32": - def get_handle_inheritable(handle: int, /) -> bool: ... - def set_handle_inheritable(handle: int, inheritable: bool, /) -> None: ... - -if sys.platform != "win32": - # Unix only - def tcgetpgrp(fd: int, /) -> int: ... - def tcsetpgrp(fd: int, pgid: int, /) -> None: ... - def ttyname(fd: int, /) -> str: ... - -def write(fd: int, data: ReadableBuffer, /) -> int: ... -def access( - path: FileDescriptorOrPath, mode: int, *, dir_fd: int | None = None, effective_ids: bool = False, follow_symlinks: bool = True -) -> bool: ... -def chdir(path: FileDescriptorOrPath) -> None: ... - -if sys.platform != "win32": - def fchdir(fd: FileDescriptorLike) -> None: ... - -def getcwd() -> str: ... -def getcwdb() -> bytes: ... -def chmod(path: FileDescriptorOrPath, mode: int, *, dir_fd: int | None = None, follow_symlinks: bool = ...) -> None: ... - -if sys.platform != "win32" and sys.platform != "linux": - def chflags(path: StrOrBytesPath, flags: int, follow_symlinks: bool = True) -> None: ... # some flavors of Unix - def lchflags(path: StrOrBytesPath, flags: int) -> None: ... - -if sys.platform != "win32": - def chroot(path: StrOrBytesPath) -> None: ... - def chown( - path: FileDescriptorOrPath, uid: int, gid: int, *, dir_fd: int | None = None, follow_symlinks: bool = True - ) -> None: ... - def lchown(path: StrOrBytesPath, uid: int, gid: int) -> None: ... - -def link( - src: StrOrBytesPath, - dst: StrOrBytesPath, - *, - src_dir_fd: int | None = None, - dst_dir_fd: int | None = None, - follow_symlinks: bool = True, -) -> None: ... -def lstat(path: StrOrBytesPath, *, dir_fd: int | None = None) -> stat_result: ... -def mkdir(path: StrOrBytesPath, mode: int = 0o777, *, dir_fd: int | None = None) -> None: ... - -if sys.platform != "win32": - def mkfifo(path: StrOrBytesPath, mode: int = 0o666, *, dir_fd: int | None = None) -> None: ... # Unix only - -def makedirs(name: StrOrBytesPath, mode: int = 0o777, exist_ok: bool = False) -> None: ... - -if sys.platform != "win32": - def mknod(path: StrOrBytesPath, mode: int = 0o600, device: int = 0, *, dir_fd: int | None = None) -> None: ... - def major(device: int, /) -> int: ... - def minor(device: int, /) -> int: ... - def makedev(major: int, minor: int, /) -> int: ... - def pathconf(path: FileDescriptorOrPath, name: str | int) -> int: ... # Unix only - -def readlink(path: GenericPath[AnyStr], *, dir_fd: int | None = None) -> AnyStr: ... -def remove(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... -def removedirs(name: StrOrBytesPath) -> None: ... -def rename(src: StrOrBytesPath, dst: StrOrBytesPath, *, src_dir_fd: int | None = None, dst_dir_fd: int | None = None) -> None: ... -def renames(old: StrOrBytesPath, new: StrOrBytesPath) -> None: ... -def replace( - src: StrOrBytesPath, dst: StrOrBytesPath, *, src_dir_fd: int | None = None, dst_dir_fd: int | None = None -) -> None: ... -def rmdir(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... -@final -class _ScandirIterator(Generic[AnyStr]): - def __del__(self) -> None: ... - def __iter__(self) -> Self: ... - def __next__(self) -> DirEntry[AnyStr]: ... - def __enter__(self) -> Self: ... - def __exit__(self, *args: Unused) -> None: ... - def close(self) -> None: ... - -@overload -def scandir(path: None = None) -> _ScandirIterator[str]: ... -@overload -def scandir(path: int) -> _ScandirIterator[str]: ... -@overload -def scandir(path: GenericPath[AnyStr]) -> _ScandirIterator[AnyStr]: ... -def stat(path: FileDescriptorOrPath, *, dir_fd: int | None = None, follow_symlinks: bool = True) -> stat_result: ... - -if sys.platform != "win32": - def statvfs(path: FileDescriptorOrPath) -> statvfs_result: ... # Unix only - -def symlink( - src: StrOrBytesPath, dst: StrOrBytesPath, target_is_directory: bool = False, *, dir_fd: int | None = None -) -> None: ... - -if sys.platform != "win32": - def sync() -> None: ... # Unix only - -def truncate(path: FileDescriptorOrPath, length: int) -> None: ... # Unix only up to version 3.4 -def unlink(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... -def utime( - path: FileDescriptorOrPath, - times: tuple[int, int] | tuple[float, float] | None = None, - *, - ns: tuple[int, int] = ..., - dir_fd: int | None = None, - follow_symlinks: bool = True, -) -> None: ... - -_OnError: TypeAlias = Callable[[OSError], object] - -def walk( - top: GenericPath[AnyStr], topdown: bool = True, onerror: _OnError | None = None, followlinks: bool = False -) -> Iterator[tuple[AnyStr, list[AnyStr], list[AnyStr]]]: ... - -if sys.platform != "win32": - @overload - def fwalk( - top: StrPath = ".", - topdown: bool = True, - onerror: _OnError | None = None, - *, - follow_symlinks: bool = False, - dir_fd: int | None = None, - ) -> Iterator[tuple[str, list[str], list[str], int]]: ... - @overload - def fwalk( - top: BytesPath, - topdown: bool = True, - onerror: _OnError | None = None, - *, - follow_symlinks: bool = False, - dir_fd: int | None = None, - ) -> Iterator[tuple[bytes, list[bytes], list[bytes], int]]: ... - if sys.platform == "linux": - def getxattr(path: FileDescriptorOrPath, attribute: StrOrBytesPath, *, follow_symlinks: bool = True) -> bytes: ... - def listxattr(path: FileDescriptorOrPath | None = None, *, follow_symlinks: bool = True) -> list[str]: ... - def removexattr(path: FileDescriptorOrPath, attribute: StrOrBytesPath, *, follow_symlinks: bool = True) -> None: ... - def setxattr( - path: FileDescriptorOrPath, - attribute: StrOrBytesPath, - value: ReadableBuffer, - flags: int = 0, - *, - follow_symlinks: bool = True, - ) -> None: ... - -def abort() -> NoReturn: ... - -# These are defined as execl(file, *args) but the first *arg is mandatory. -def execl(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> NoReturn: ... -def execlp(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> NoReturn: ... - -# These are: execle(file, *args, env) but env is pulled from the last element of the args. -def execle( - file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]] -) -> NoReturn: ... -def execlpe( - file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]] -) -> NoReturn: ... - -# The docs say `args: tuple or list of strings` -# The implementation enforces tuple or list so we can't use Sequence. -# Not separating out PathLike[str] and PathLike[bytes] here because it doesn't make much difference -# in practice, and doing so would explode the number of combinations in this already long union. -# All these combinations are necessary due to list being invariant. -_ExecVArgs: TypeAlias = ( - tuple[StrOrBytesPath, ...] - | list[bytes] - | list[str] - | list[PathLike[Any]] - | list[bytes | str] - | list[bytes | PathLike[Any]] - | list[str | PathLike[Any]] - | list[bytes | str | PathLike[Any]] -) -# Depending on the OS, the keys and values are passed either to -# PyUnicode_FSDecoder (which accepts str | ReadableBuffer) or to -# PyUnicode_FSConverter (which accepts StrOrBytesPath). For simplicity, -# we limit to str | bytes. -_ExecEnv: TypeAlias = Mapping[bytes, bytes | str] | Mapping[str, bytes | str] - -def execv(path: StrOrBytesPath, argv: _ExecVArgs, /) -> NoReturn: ... -def execve(path: FileDescriptorOrPath, argv: _ExecVArgs, env: _ExecEnv) -> NoReturn: ... -def execvp(file: StrOrBytesPath, args: _ExecVArgs) -> NoReturn: ... -def execvpe(file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> NoReturn: ... -def _exit(status: int) -> NoReturn: ... -def kill(pid: int, signal: int, /) -> None: ... - -if sys.platform != "win32": - # Unix only - def fork() -> int: ... - def forkpty() -> tuple[int, int]: ... # some flavors of Unix - def killpg(pgid: int, signal: int, /) -> None: ... - def nice(increment: int, /) -> int: ... - if sys.platform != "darwin" and sys.platform != "linux": - def plock(op: int, /) -> None: ... - -class _wrap_close: - def __init__(self, stream: TextIOWrapper, proc: Popen[str]) -> None: ... - def close(self) -> int | None: ... - def __enter__(self) -> Self: ... - def __exit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: ... - def __iter__(self) -> Iterator[str]: ... - # Methods below here don't exist directly on the _wrap_close object, but - # are copied from the wrapped TextIOWrapper object via __getattr__. - # The full set of TextIOWrapper methods are technically available this way, - # but undocumented. Only a subset are currently included here. - def read(self, size: int | None = -1, /) -> str: ... - def readable(self) -> bool: ... - def readline(self, size: int = -1, /) -> str: ... - def readlines(self, hint: int = -1, /) -> list[str]: ... - def writable(self) -> bool: ... - def write(self, s: str, /) -> int: ... - def writelines(self, lines: Iterable[str], /) -> None: ... - -def popen(cmd: str, mode: str = "r", buffering: int = -1) -> _wrap_close: ... -def spawnl(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... -def spawnle(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise sig - -if sys.platform != "win32": - def spawnv(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... - def spawnve(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... - -else: - def spawnv(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, /) -> int: ... - def spawnve(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, env: _ExecEnv, /) -> int: ... - -def system(command: StrOrBytesPath) -> int: ... -@final -class times_result(structseq[float], tuple[float, float, float, float, float]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("user", "system", "children_user", "children_system", "elapsed") - - @property - def user(self) -> float: ... - @property - def system(self) -> float: ... - @property - def children_user(self) -> float: ... - @property - def children_system(self) -> float: ... - @property - def elapsed(self) -> float: ... - -def times() -> times_result: ... -def waitpid(pid: int, options: int, /) -> tuple[int, int]: ... - -if sys.platform == "win32": - if sys.version_info >= (3, 10): - def startfile( - filepath: StrOrBytesPath, - operation: str = ..., - arguments: str = "", - cwd: StrOrBytesPath | None = None, - show_cmd: int = 1, - ) -> None: ... - else: - def startfile(filepath: StrOrBytesPath, operation: str = ...) -> None: ... - -else: - def spawnlp(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... - def spawnlpe(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise signature - def spawnvp(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... - def spawnvpe(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... - def wait() -> tuple[int, int]: ... # Unix only - # Added to MacOS in 3.13 - if sys.platform != "darwin" or sys.version_info >= (3, 13): - @final - class waitid_result(structseq[int], tuple[int, int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("si_pid", "si_uid", "si_signo", "si_status", "si_code") - - @property - def si_pid(self) -> int: ... - @property - def si_uid(self) -> int: ... - @property - def si_signo(self) -> int: ... - @property - def si_status(self) -> int: ... - @property - def si_code(self) -> int: ... - - def waitid(idtype: int, ident: int, options: int, /) -> waitid_result | None: ... - - from resource import struct_rusage - - def wait3(options: int) -> tuple[int, int, struct_rusage]: ... - def wait4(pid: int, options: int) -> tuple[int, int, struct_rusage]: ... - def WCOREDUMP(status: int, /) -> bool: ... - def WIFCONTINUED(status: int) -> bool: ... - def WIFSTOPPED(status: int) -> bool: ... - def WIFSIGNALED(status: int) -> bool: ... - def WIFEXITED(status: int) -> bool: ... - def WEXITSTATUS(status: int) -> int: ... - def WSTOPSIG(status: int) -> int: ... - def WTERMSIG(status: int) -> int: ... - def posix_spawn( - path: StrOrBytesPath, - argv: _ExecVArgs, - env: _ExecEnv, - /, - *, - file_actions: Sequence[tuple[Any, ...]] | None = ..., - setpgroup: int | None = ..., - resetids: bool = ..., - setsid: bool = ..., - setsigmask: Iterable[int] = ..., - setsigdef: Iterable[int] = ..., - scheduler: tuple[Any, sched_param] | None = ..., - ) -> int: ... - def posix_spawnp( - path: StrOrBytesPath, - argv: _ExecVArgs, - env: _ExecEnv, - /, - *, - file_actions: Sequence[tuple[Any, ...]] | None = ..., - setpgroup: int | None = ..., - resetids: bool = ..., - setsid: bool = ..., - setsigmask: Iterable[int] = ..., - setsigdef: Iterable[int] = ..., - scheduler: tuple[Any, sched_param] | None = ..., - ) -> int: ... - POSIX_SPAWN_OPEN: int - POSIX_SPAWN_CLOSE: int - POSIX_SPAWN_DUP2: int - -if sys.platform != "win32": - @final - class sched_param(structseq[int], tuple[int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("sched_priority",) - - def __new__(cls, sched_priority: int) -> Self: ... - @property - def sched_priority(self) -> int: ... - - def sched_get_priority_min(policy: int) -> int: ... # some flavors of Unix - def sched_get_priority_max(policy: int) -> int: ... # some flavors of Unix - def sched_yield() -> None: ... # some flavors of Unix - if sys.platform != "darwin": - def sched_setscheduler(pid: int, policy: int, param: sched_param, /) -> None: ... # some flavors of Unix - def sched_getscheduler(pid: int, /) -> int: ... # some flavors of Unix - def sched_rr_get_interval(pid: int, /) -> float: ... # some flavors of Unix - def sched_setparam(pid: int, param: sched_param, /) -> None: ... # some flavors of Unix - def sched_getparam(pid: int, /) -> sched_param: ... # some flavors of Unix - def sched_setaffinity(pid: int, mask: Iterable[int], /) -> None: ... # some flavors of Unix - def sched_getaffinity(pid: int, /) -> set[int]: ... # some flavors of Unix - -def cpu_count() -> int | None: ... - -if sys.version_info >= (3, 13): - # Documented to return `int | None`, but falls back to `len(sched_getaffinity(0))` when - # available. See https://github.com/python/cpython/blob/417c130/Lib/os.py#L1175-L1186. - if sys.platform != "win32" and sys.platform != "darwin": - def process_cpu_count() -> int: ... - else: - def process_cpu_count() -> int | None: ... - -if sys.platform != "win32": - # Unix only - def confstr(name: str | int, /) -> str | None: ... - def getloadavg() -> tuple[float, float, float]: ... - def sysconf(name: str | int, /) -> int: ... - -if sys.platform == "linux": - def getrandom(size: int, flags: int = 0) -> bytes: ... - -def urandom(size: int, /) -> bytes: ... - -if sys.platform != "win32": - def register_at_fork( - *, - before: Callable[..., Any] | None = ..., - after_in_parent: Callable[..., Any] | None = ..., - after_in_child: Callable[..., Any] | None = ..., - ) -> None: ... - -if sys.platform == "win32": - class _AddedDllDirectory: - path: str | None - def __init__(self, path: str | None, cookie: _T, remove_dll_directory: Callable[[_T], object]) -> None: ... - def close(self) -> None: ... - def __enter__(self) -> Self: ... - def __exit__(self, *args: Unused) -> None: ... - - def add_dll_directory(path: str) -> _AddedDllDirectory: ... - -if sys.platform == "linux": - MFD_CLOEXEC: int - MFD_ALLOW_SEALING: int - MFD_HUGETLB: int - MFD_HUGE_SHIFT: int - MFD_HUGE_MASK: int - MFD_HUGE_64KB: int - MFD_HUGE_512KB: int - MFD_HUGE_1MB: int - MFD_HUGE_2MB: int - MFD_HUGE_8MB: int - MFD_HUGE_16MB: int - MFD_HUGE_32MB: int - MFD_HUGE_256MB: int - MFD_HUGE_512MB: int - MFD_HUGE_1GB: int - MFD_HUGE_2GB: int - MFD_HUGE_16GB: int - def memfd_create(name: str, flags: int = ...) -> int: ... - def copy_file_range(src: int, dst: int, count: int, offset_src: int | None = ..., offset_dst: int | None = ...) -> int: ... - -def waitstatus_to_exitcode(status: int) -> int: ... - -if sys.platform == "linux": - def pidfd_open(pid: int, flags: int = ...) -> int: ... - -if sys.version_info >= (3, 12) and sys.platform == "linux": - PIDFD_NONBLOCK: Final = 2048 - -if sys.version_info >= (3, 12) and sys.platform == "win32": - def listdrives() -> list[str]: ... - def listmounts(volume: str) -> list[str]: ... - def listvolumes() -> list[str]: ... - -if sys.version_info >= (3, 10) and sys.platform == "linux": - EFD_CLOEXEC: int - EFD_NONBLOCK: int - EFD_SEMAPHORE: int - SPLICE_F_MORE: int - SPLICE_F_MOVE: int - SPLICE_F_NONBLOCK: int - def eventfd(initval: int, flags: int = 524288) -> FileDescriptor: ... - def eventfd_read(fd: FileDescriptor) -> int: ... - def eventfd_write(fd: FileDescriptor, value: int) -> None: ... - def splice( - src: FileDescriptor, - dst: FileDescriptor, - count: int, - offset_src: int | None = ..., - offset_dst: int | None = ..., - flags: int = 0, - ) -> int: ... - -if sys.version_info >= (3, 12) and sys.platform == "linux": - CLONE_FILES: int - CLONE_FS: int - CLONE_NEWCGROUP: int # Linux 4.6+ - CLONE_NEWIPC: int # Linux 2.6.19+ - CLONE_NEWNET: int # Linux 2.6.24+ - CLONE_NEWNS: int - CLONE_NEWPID: int # Linux 3.8+ - CLONE_NEWTIME: int # Linux 5.6+ - CLONE_NEWUSER: int # Linux 3.8+ - CLONE_NEWUTS: int # Linux 2.6.19+ - CLONE_SIGHAND: int - CLONE_SYSVSEM: int # Linux 2.6.26+ - CLONE_THREAD: int - CLONE_VM: int - def unshare(flags: int) -> None: ... - def setns(fd: FileDescriptorLike, nstype: int = 0) -> None: ... - -if sys.version_info >= (3, 13) and sys.platform != "win32": - def posix_openpt(oflag: int, /) -> int: ... - def grantpt(fd: FileDescriptorLike, /) -> None: ... - def unlockpt(fd: FileDescriptorLike, /) -> None: ... - def ptsname(fd: FileDescriptorLike, /) -> str: ... - -if sys.version_info >= (3, 13) and sys.platform == "linux": - TFD_TIMER_ABSTIME: Final = 1 - TFD_TIMER_CANCEL_ON_SET: Final = 2 - TFD_NONBLOCK: Final[int] - TFD_CLOEXEC: Final[int] - POSIX_SPAWN_CLOSEFROM: Final[int] - - def timerfd_create(clockid: int, /, *, flags: int = 0) -> int: ... - def timerfd_settime( - fd: FileDescriptor, /, *, flags: int = 0, initial: float = 0.0, interval: float = 0.0 - ) -> tuple[float, float]: ... - def timerfd_settime_ns(fd: FileDescriptor, /, *, flags: int = 0, initial: int = 0, interval: int = 0) -> tuple[int, int]: ... - def timerfd_gettime(fd: FileDescriptor, /) -> tuple[float, float]: ... - def timerfd_gettime_ns(fd: FileDescriptor, /) -> tuple[int, int]: ... - -if sys.version_info >= (3, 13) or sys.platform != "win32": - # Added to Windows in 3.13. - def fchmod(fd: int, mode: int) -> None: ... - -if sys.platform != "linux": - if sys.version_info >= (3, 13) or sys.platform != "win32": - # Added to Windows in 3.13. - def lchmod(path: StrOrBytesPath, mode: int) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pathlib.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/pathlib.pyi deleted file mode 100644 index 1e4d97770b7bd..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/pathlib.pyi +++ /dev/null @@ -1,296 +0,0 @@ -import sys -import types -from _typeshed import ( - OpenBinaryMode, - OpenBinaryModeReading, - OpenBinaryModeUpdating, - OpenBinaryModeWriting, - OpenTextMode, - ReadableBuffer, - StrOrBytesPath, - StrPath, - Unused, -) -from collections.abc import Callable, Generator, Iterator, Sequence -from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper -from os import PathLike, stat_result -from types import GenericAlias, TracebackType -from typing import IO, Any, BinaryIO, ClassVar, Literal, overload -from typing_extensions import Never, Self, deprecated - -__all__ = ["PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath"] - -if sys.version_info >= (3, 13): - __all__ += ["UnsupportedOperation"] - -class PurePath(PathLike[str]): - if sys.version_info >= (3, 13): - parser: ClassVar[types.ModuleType] - def full_match(self, pattern: StrPath, *, case_sensitive: bool | None = None) -> bool: ... - - @property - def parts(self) -> tuple[str, ...]: ... - @property - def drive(self) -> str: ... - @property - def root(self) -> str: ... - @property - def anchor(self) -> str: ... - @property - def name(self) -> str: ... - @property - def suffix(self) -> str: ... - @property - def suffixes(self) -> list[str]: ... - @property - def stem(self) -> str: ... - if sys.version_info >= (3, 12): - def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... - def __init__(self, *args: StrPath) -> None: ... # pyright: ignore[reportInconsistentConstructor] - else: - def __new__(cls, *args: StrPath) -> Self: ... - - def __hash__(self) -> int: ... - def __fspath__(self) -> str: ... - def __lt__(self, other: PurePath) -> bool: ... - def __le__(self, other: PurePath) -> bool: ... - def __gt__(self, other: PurePath) -> bool: ... - def __ge__(self, other: PurePath) -> bool: ... - def __truediv__(self, key: StrPath) -> Self: ... - def __rtruediv__(self, key: StrPath) -> Self: ... - def __bytes__(self) -> bytes: ... - def as_posix(self) -> str: ... - def as_uri(self) -> str: ... - def is_absolute(self) -> bool: ... - def is_reserved(self) -> bool: ... - if sys.version_info >= (3, 12): - def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool: ... - else: - def is_relative_to(self, *other: StrPath) -> bool: ... - - if sys.version_info >= (3, 12): - def match(self, path_pattern: str, *, case_sensitive: bool | None = None) -> bool: ... - else: - def match(self, path_pattern: str) -> bool: ... - - if sys.version_info >= (3, 12): - def relative_to(self, other: StrPath, /, *_deprecated: StrPath, walk_up: bool = False) -> Self: ... - else: - def relative_to(self, *other: StrPath) -> Self: ... - - def with_name(self, name: str) -> Self: ... - def with_stem(self, stem: str) -> Self: ... - def with_suffix(self, suffix: str) -> Self: ... - def joinpath(self, *other: StrPath) -> Self: ... - @property - def parents(self) -> Sequence[Self]: ... - @property - def parent(self) -> Self: ... - if sys.version_info < (3, 11): - def __class_getitem__(cls, type: Any) -> GenericAlias: ... - - if sys.version_info >= (3, 12): - def with_segments(self, *args: StrPath) -> Self: ... - -class PurePosixPath(PurePath): ... -class PureWindowsPath(PurePath): ... - -class Path(PurePath): - if sys.version_info >= (3, 12): - def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... # pyright: ignore[reportInconsistentConstructor] - else: - def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... - - @classmethod - def cwd(cls) -> Self: ... - if sys.version_info >= (3, 10): - def stat(self, *, follow_symlinks: bool = True) -> stat_result: ... - def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ... - else: - def stat(self) -> stat_result: ... - def chmod(self, mode: int) -> None: ... - - if sys.version_info >= (3, 13): - @classmethod - def from_uri(cls, uri: str) -> Self: ... - def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... - def is_file(self, *, follow_symlinks: bool = True) -> bool: ... - def read_text(self, encoding: str | None = None, errors: str | None = None, newline: str | None = None) -> str: ... - else: - def __enter__(self) -> Self: ... - def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ... - def is_dir(self) -> bool: ... - def is_file(self) -> bool: ... - def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ... - - if sys.version_info >= (3, 13): - def glob(self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False) -> Iterator[Self]: ... - def rglob( - self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False - ) -> Iterator[Self]: ... - elif sys.version_info >= (3, 12): - def glob(self, pattern: str, *, case_sensitive: bool | None = None) -> Generator[Self, None, None]: ... - def rglob(self, pattern: str, *, case_sensitive: bool | None = None) -> Generator[Self, None, None]: ... - else: - def glob(self, pattern: str) -> Generator[Self, None, None]: ... - def rglob(self, pattern: str) -> Generator[Self, None, None]: ... - - if sys.version_info >= (3, 12): - def exists(self, *, follow_symlinks: bool = True) -> bool: ... - else: - def exists(self) -> bool: ... - - def is_symlink(self) -> bool: ... - def is_socket(self) -> bool: ... - def is_fifo(self) -> bool: ... - def is_block_device(self) -> bool: ... - def is_char_device(self) -> bool: ... - if sys.version_info >= (3, 12): - def is_junction(self) -> bool: ... - - def iterdir(self) -> Generator[Self, None, None]: ... - def lchmod(self, mode: int) -> None: ... - def lstat(self) -> stat_result: ... - def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... - - if sys.version_info >= (3, 14): - def copy(self, target: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> None: ... - def copytree( - self, - target: StrPath, - *, - follow_symlinks: bool = True, - preserve_metadata: bool = False, - dirs_exist_ok: bool = False, - ignore: Callable[[Self], bool] | None = None, - on_error: Callable[[OSError], object] | None = None, - ) -> None: ... - - # Adapted from builtins.open - # Text mode: always returns a TextIOWrapper - # The Traversable .open in stdlib/importlib/abc.pyi should be kept in sync with this. - @overload - def open( - self, - mode: OpenTextMode = "r", - buffering: int = -1, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> TextIOWrapper: ... - # Unbuffered binary mode: returns a FileIO - @overload - def open( - self, mode: OpenBinaryMode, buffering: Literal[0], encoding: None = None, errors: None = None, newline: None = None - ) -> FileIO: ... - # Buffering is on: return BufferedRandom, BufferedReader, or BufferedWriter - @overload - def open( - self, - mode: OpenBinaryModeUpdating, - buffering: Literal[-1, 1] = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - ) -> BufferedRandom: ... - @overload - def open( - self, - mode: OpenBinaryModeWriting, - buffering: Literal[-1, 1] = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - ) -> BufferedWriter: ... - @overload - def open( - self, - mode: OpenBinaryModeReading, - buffering: Literal[-1, 1] = -1, - encoding: None = None, - errors: None = None, - newline: None = None, - ) -> BufferedReader: ... - # Buffering cannot be determined: fall back to BinaryIO - @overload - def open( - self, mode: OpenBinaryMode, buffering: int = -1, encoding: None = None, errors: None = None, newline: None = None - ) -> BinaryIO: ... - # Fallback if mode is not specified - @overload - def open( - self, mode: str, buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None - ) -> IO[Any]: ... - - # These methods do "exist" on Windows on <3.13, but they always raise NotImplementedError. - if sys.platform == "win32": - if sys.version_info < (3, 13): - def owner(self: Never) -> str: ... # type: ignore[misc] - def group(self: Never) -> str: ... # type: ignore[misc] - else: - if sys.version_info >= (3, 13): - def owner(self, *, follow_symlinks: bool = True) -> str: ... - def group(self, *, follow_symlinks: bool = True) -> str: ... - else: - def owner(self) -> str: ... - def group(self) -> str: ... - - # This method does "exist" on Windows on <3.12, but always raises NotImplementedError - # On py312+, it works properly on Windows, as with all other platforms - if sys.platform == "win32" and sys.version_info < (3, 12): - def is_mount(self: Never) -> bool: ... # type: ignore[misc] - else: - def is_mount(self) -> bool: ... - - def readlink(self) -> Self: ... - - if sys.version_info >= (3, 10): - def rename(self, target: StrPath) -> Self: ... - def replace(self, target: StrPath) -> Self: ... - else: - def rename(self, target: str | PurePath) -> Self: ... - def replace(self, target: str | PurePath) -> Self: ... - - def resolve(self, strict: bool = False) -> Self: ... - def rmdir(self) -> None: ... - if sys.version_info >= (3, 14): - def delete(self, ignore_errors: bool = False, on_error: Callable[[OSError], object] | None = None) -> None: ... - - def symlink_to(self, target: StrOrBytesPath, target_is_directory: bool = False) -> None: ... - if sys.version_info >= (3, 10): - def hardlink_to(self, target: StrOrBytesPath) -> None: ... - - def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ... - def unlink(self, missing_ok: bool = False) -> None: ... - @classmethod - def home(cls) -> Self: ... - def absolute(self) -> Self: ... - def expanduser(self) -> Self: ... - def read_bytes(self) -> bytes: ... - def samefile(self, other_path: StrPath) -> bool: ... - def write_bytes(self, data: ReadableBuffer) -> int: ... - if sys.version_info >= (3, 10): - def write_text( - self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None - ) -> int: ... - else: - def write_text(self, data: str, encoding: str | None = None, errors: str | None = None) -> int: ... - if sys.version_info < (3, 12): - if sys.version_info >= (3, 10): - @deprecated("Deprecated as of Python 3.10 and removed in Python 3.12. Use hardlink_to() instead.") - def link_to(self, target: StrOrBytesPath) -> None: ... - else: - def link_to(self, target: StrOrBytesPath) -> None: ... - if sys.version_info >= (3, 12): - def walk( - self, top_down: bool = ..., on_error: Callable[[OSError], object] | None = ..., follow_symlinks: bool = ... - ) -> Iterator[tuple[Self, list[str], list[str]]]: ... - - if sys.version_info >= (3, 14): - def rmtree(self, ignore_errors: bool = False, on_error: Callable[[OSError], object] | None = None) -> None: ... - -class PosixPath(Path, PurePosixPath): ... -class WindowsPath(Path, PureWindowsPath): ... - -if sys.version_info >= (3, 13): - class UnsupportedOperation(NotImplementedError): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi deleted file mode 100644 index cae4da089161f..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi +++ /dev/null @@ -1,51 +0,0 @@ -import sys -from typing import Final -from typing_extensions import LiteralString - -codes: dict[str, int] -messages: dict[int, str] - -XML_ERROR_ABORTED: Final[LiteralString] -XML_ERROR_ASYNC_ENTITY: Final[LiteralString] -XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF: Final[LiteralString] -XML_ERROR_BAD_CHAR_REF: Final[LiteralString] -XML_ERROR_BINARY_ENTITY_REF: Final[LiteralString] -XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING: Final[LiteralString] -XML_ERROR_DUPLICATE_ATTRIBUTE: Final[LiteralString] -XML_ERROR_ENTITY_DECLARED_IN_PE: Final[LiteralString] -XML_ERROR_EXTERNAL_ENTITY_HANDLING: Final[LiteralString] -XML_ERROR_FEATURE_REQUIRES_XML_DTD: Final[LiteralString] -XML_ERROR_FINISHED: Final[LiteralString] -XML_ERROR_INCOMPLETE_PE: Final[LiteralString] -XML_ERROR_INCORRECT_ENCODING: Final[LiteralString] -XML_ERROR_INVALID_TOKEN: Final[LiteralString] -XML_ERROR_JUNK_AFTER_DOC_ELEMENT: Final[LiteralString] -XML_ERROR_MISPLACED_XML_PI: Final[LiteralString] -XML_ERROR_NOT_STANDALONE: Final[LiteralString] -XML_ERROR_NOT_SUSPENDED: Final[LiteralString] -XML_ERROR_NO_ELEMENTS: Final[LiteralString] -XML_ERROR_NO_MEMORY: Final[LiteralString] -XML_ERROR_PARAM_ENTITY_REF: Final[LiteralString] -XML_ERROR_PARTIAL_CHAR: Final[LiteralString] -XML_ERROR_PUBLICID: Final[LiteralString] -XML_ERROR_RECURSIVE_ENTITY_REF: Final[LiteralString] -XML_ERROR_SUSPENDED: Final[LiteralString] -XML_ERROR_SUSPEND_PE: Final[LiteralString] -XML_ERROR_SYNTAX: Final[LiteralString] -XML_ERROR_TAG_MISMATCH: Final[LiteralString] -XML_ERROR_TEXT_DECL: Final[LiteralString] -XML_ERROR_UNBOUND_PREFIX: Final[LiteralString] -XML_ERROR_UNCLOSED_CDATA_SECTION: Final[LiteralString] -XML_ERROR_UNCLOSED_TOKEN: Final[LiteralString] -XML_ERROR_UNDECLARING_PREFIX: Final[LiteralString] -XML_ERROR_UNDEFINED_ENTITY: Final[LiteralString] -XML_ERROR_UNEXPECTED_STATE: Final[LiteralString] -XML_ERROR_UNKNOWN_ENCODING: Final[LiteralString] -XML_ERROR_XML_DECL: Final[LiteralString] -if sys.version_info >= (3, 11): - XML_ERROR_RESERVED_PREFIX_XML: Final[LiteralString] - XML_ERROR_RESERVED_PREFIX_XMLNS: Final[LiteralString] - XML_ERROR_RESERVED_NAMESPACE_URI: Final[LiteralString] - XML_ERROR_INVALID_ARGUMENT: Final[LiteralString] - XML_ERROR_NO_BUFFER: Final[LiteralString] - XML_ERROR_AMPLIFICATION_LIMIT_BREACH: Final[LiteralString] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi deleted file mode 100644 index b83516b4d4eb8..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi +++ /dev/null @@ -1,467 +0,0 @@ -import sys -from _typeshed import MaybeNone, ReadableBuffer, StrOrBytesPath, SupportsLenAndGetItem, Unused -from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence -from sqlite3.dbapi2 import ( - PARSE_COLNAMES as PARSE_COLNAMES, - PARSE_DECLTYPES as PARSE_DECLTYPES, - SQLITE_ALTER_TABLE as SQLITE_ALTER_TABLE, - SQLITE_ANALYZE as SQLITE_ANALYZE, - SQLITE_ATTACH as SQLITE_ATTACH, - SQLITE_CREATE_INDEX as SQLITE_CREATE_INDEX, - SQLITE_CREATE_TABLE as SQLITE_CREATE_TABLE, - SQLITE_CREATE_TEMP_INDEX as SQLITE_CREATE_TEMP_INDEX, - SQLITE_CREATE_TEMP_TABLE as SQLITE_CREATE_TEMP_TABLE, - SQLITE_CREATE_TEMP_TRIGGER as SQLITE_CREATE_TEMP_TRIGGER, - SQLITE_CREATE_TEMP_VIEW as SQLITE_CREATE_TEMP_VIEW, - SQLITE_CREATE_TRIGGER as SQLITE_CREATE_TRIGGER, - SQLITE_CREATE_VIEW as SQLITE_CREATE_VIEW, - SQLITE_CREATE_VTABLE as SQLITE_CREATE_VTABLE, - SQLITE_DELETE as SQLITE_DELETE, - SQLITE_DENY as SQLITE_DENY, - SQLITE_DETACH as SQLITE_DETACH, - SQLITE_DONE as SQLITE_DONE, - SQLITE_DROP_INDEX as SQLITE_DROP_INDEX, - SQLITE_DROP_TABLE as SQLITE_DROP_TABLE, - SQLITE_DROP_TEMP_INDEX as SQLITE_DROP_TEMP_INDEX, - SQLITE_DROP_TEMP_TABLE as SQLITE_DROP_TEMP_TABLE, - SQLITE_DROP_TEMP_TRIGGER as SQLITE_DROP_TEMP_TRIGGER, - SQLITE_DROP_TEMP_VIEW as SQLITE_DROP_TEMP_VIEW, - SQLITE_DROP_TRIGGER as SQLITE_DROP_TRIGGER, - SQLITE_DROP_VIEW as SQLITE_DROP_VIEW, - SQLITE_DROP_VTABLE as SQLITE_DROP_VTABLE, - SQLITE_FUNCTION as SQLITE_FUNCTION, - SQLITE_IGNORE as SQLITE_IGNORE, - SQLITE_INSERT as SQLITE_INSERT, - SQLITE_OK as SQLITE_OK, - SQLITE_PRAGMA as SQLITE_PRAGMA, - SQLITE_READ as SQLITE_READ, - SQLITE_RECURSIVE as SQLITE_RECURSIVE, - SQLITE_REINDEX as SQLITE_REINDEX, - SQLITE_SAVEPOINT as SQLITE_SAVEPOINT, - SQLITE_SELECT as SQLITE_SELECT, - SQLITE_TRANSACTION as SQLITE_TRANSACTION, - SQLITE_UPDATE as SQLITE_UPDATE, - Binary as Binary, - Date as Date, - DateFromTicks as DateFromTicks, - Time as Time, - TimeFromTicks as TimeFromTicks, - TimestampFromTicks as TimestampFromTicks, - adapt as adapt, - adapters as adapters, - apilevel as apilevel, - complete_statement as complete_statement, - connect as connect, - converters as converters, - enable_callback_tracebacks as enable_callback_tracebacks, - paramstyle as paramstyle, - register_adapter as register_adapter, - register_converter as register_converter, - sqlite_version as sqlite_version, - sqlite_version_info as sqlite_version_info, - threadsafety as threadsafety, - version_info as version_info, -) -from types import TracebackType -from typing import Any, Literal, Protocol, SupportsIndex, TypeVar, final, overload, type_check_only -from typing_extensions import Self, TypeAlias - -if sys.version_info >= (3, 12): - from sqlite3.dbapi2 import ( - LEGACY_TRANSACTION_CONTROL as LEGACY_TRANSACTION_CONTROL, - SQLITE_DBCONFIG_DEFENSIVE as SQLITE_DBCONFIG_DEFENSIVE, - SQLITE_DBCONFIG_DQS_DDL as SQLITE_DBCONFIG_DQS_DDL, - SQLITE_DBCONFIG_DQS_DML as SQLITE_DBCONFIG_DQS_DML, - SQLITE_DBCONFIG_ENABLE_FKEY as SQLITE_DBCONFIG_ENABLE_FKEY, - SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER as SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, - SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION as SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, - SQLITE_DBCONFIG_ENABLE_QPSG as SQLITE_DBCONFIG_ENABLE_QPSG, - SQLITE_DBCONFIG_ENABLE_TRIGGER as SQLITE_DBCONFIG_ENABLE_TRIGGER, - SQLITE_DBCONFIG_ENABLE_VIEW as SQLITE_DBCONFIG_ENABLE_VIEW, - SQLITE_DBCONFIG_LEGACY_ALTER_TABLE as SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, - SQLITE_DBCONFIG_LEGACY_FILE_FORMAT as SQLITE_DBCONFIG_LEGACY_FILE_FORMAT, - SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE as SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, - SQLITE_DBCONFIG_RESET_DATABASE as SQLITE_DBCONFIG_RESET_DATABASE, - SQLITE_DBCONFIG_TRIGGER_EQP as SQLITE_DBCONFIG_TRIGGER_EQP, - SQLITE_DBCONFIG_TRUSTED_SCHEMA as SQLITE_DBCONFIG_TRUSTED_SCHEMA, - SQLITE_DBCONFIG_WRITABLE_SCHEMA as SQLITE_DBCONFIG_WRITABLE_SCHEMA, - ) - -if sys.version_info >= (3, 11): - from sqlite3.dbapi2 import ( - SQLITE_ABORT as SQLITE_ABORT, - SQLITE_ABORT_ROLLBACK as SQLITE_ABORT_ROLLBACK, - SQLITE_AUTH as SQLITE_AUTH, - SQLITE_AUTH_USER as SQLITE_AUTH_USER, - SQLITE_BUSY as SQLITE_BUSY, - SQLITE_BUSY_RECOVERY as SQLITE_BUSY_RECOVERY, - SQLITE_BUSY_SNAPSHOT as SQLITE_BUSY_SNAPSHOT, - SQLITE_BUSY_TIMEOUT as SQLITE_BUSY_TIMEOUT, - SQLITE_CANTOPEN as SQLITE_CANTOPEN, - SQLITE_CANTOPEN_CONVPATH as SQLITE_CANTOPEN_CONVPATH, - SQLITE_CANTOPEN_DIRTYWAL as SQLITE_CANTOPEN_DIRTYWAL, - SQLITE_CANTOPEN_FULLPATH as SQLITE_CANTOPEN_FULLPATH, - SQLITE_CANTOPEN_ISDIR as SQLITE_CANTOPEN_ISDIR, - SQLITE_CANTOPEN_NOTEMPDIR as SQLITE_CANTOPEN_NOTEMPDIR, - SQLITE_CANTOPEN_SYMLINK as SQLITE_CANTOPEN_SYMLINK, - SQLITE_CONSTRAINT as SQLITE_CONSTRAINT, - SQLITE_CONSTRAINT_CHECK as SQLITE_CONSTRAINT_CHECK, - SQLITE_CONSTRAINT_COMMITHOOK as SQLITE_CONSTRAINT_COMMITHOOK, - SQLITE_CONSTRAINT_FOREIGNKEY as SQLITE_CONSTRAINT_FOREIGNKEY, - SQLITE_CONSTRAINT_FUNCTION as SQLITE_CONSTRAINT_FUNCTION, - SQLITE_CONSTRAINT_NOTNULL as SQLITE_CONSTRAINT_NOTNULL, - SQLITE_CONSTRAINT_PINNED as SQLITE_CONSTRAINT_PINNED, - SQLITE_CONSTRAINT_PRIMARYKEY as SQLITE_CONSTRAINT_PRIMARYKEY, - SQLITE_CONSTRAINT_ROWID as SQLITE_CONSTRAINT_ROWID, - SQLITE_CONSTRAINT_TRIGGER as SQLITE_CONSTRAINT_TRIGGER, - SQLITE_CONSTRAINT_UNIQUE as SQLITE_CONSTRAINT_UNIQUE, - SQLITE_CONSTRAINT_VTAB as SQLITE_CONSTRAINT_VTAB, - SQLITE_CORRUPT as SQLITE_CORRUPT, - SQLITE_CORRUPT_INDEX as SQLITE_CORRUPT_INDEX, - SQLITE_CORRUPT_SEQUENCE as SQLITE_CORRUPT_SEQUENCE, - SQLITE_CORRUPT_VTAB as SQLITE_CORRUPT_VTAB, - SQLITE_EMPTY as SQLITE_EMPTY, - SQLITE_ERROR as SQLITE_ERROR, - SQLITE_ERROR_MISSING_COLLSEQ as SQLITE_ERROR_MISSING_COLLSEQ, - SQLITE_ERROR_RETRY as SQLITE_ERROR_RETRY, - SQLITE_ERROR_SNAPSHOT as SQLITE_ERROR_SNAPSHOT, - SQLITE_FORMAT as SQLITE_FORMAT, - SQLITE_FULL as SQLITE_FULL, - SQLITE_INTERNAL as SQLITE_INTERNAL, - SQLITE_INTERRUPT as SQLITE_INTERRUPT, - SQLITE_IOERR as SQLITE_IOERR, - SQLITE_IOERR_ACCESS as SQLITE_IOERR_ACCESS, - SQLITE_IOERR_AUTH as SQLITE_IOERR_AUTH, - SQLITE_IOERR_BEGIN_ATOMIC as SQLITE_IOERR_BEGIN_ATOMIC, - SQLITE_IOERR_BLOCKED as SQLITE_IOERR_BLOCKED, - SQLITE_IOERR_CHECKRESERVEDLOCK as SQLITE_IOERR_CHECKRESERVEDLOCK, - SQLITE_IOERR_CLOSE as SQLITE_IOERR_CLOSE, - SQLITE_IOERR_COMMIT_ATOMIC as SQLITE_IOERR_COMMIT_ATOMIC, - SQLITE_IOERR_CONVPATH as SQLITE_IOERR_CONVPATH, - SQLITE_IOERR_CORRUPTFS as SQLITE_IOERR_CORRUPTFS, - SQLITE_IOERR_DATA as SQLITE_IOERR_DATA, - SQLITE_IOERR_DELETE as SQLITE_IOERR_DELETE, - SQLITE_IOERR_DELETE_NOENT as SQLITE_IOERR_DELETE_NOENT, - SQLITE_IOERR_DIR_CLOSE as SQLITE_IOERR_DIR_CLOSE, - SQLITE_IOERR_DIR_FSYNC as SQLITE_IOERR_DIR_FSYNC, - SQLITE_IOERR_FSTAT as SQLITE_IOERR_FSTAT, - SQLITE_IOERR_FSYNC as SQLITE_IOERR_FSYNC, - SQLITE_IOERR_GETTEMPPATH as SQLITE_IOERR_GETTEMPPATH, - SQLITE_IOERR_LOCK as SQLITE_IOERR_LOCK, - SQLITE_IOERR_MMAP as SQLITE_IOERR_MMAP, - SQLITE_IOERR_NOMEM as SQLITE_IOERR_NOMEM, - SQLITE_IOERR_RDLOCK as SQLITE_IOERR_RDLOCK, - SQLITE_IOERR_READ as SQLITE_IOERR_READ, - SQLITE_IOERR_ROLLBACK_ATOMIC as SQLITE_IOERR_ROLLBACK_ATOMIC, - SQLITE_IOERR_SEEK as SQLITE_IOERR_SEEK, - SQLITE_IOERR_SHMLOCK as SQLITE_IOERR_SHMLOCK, - SQLITE_IOERR_SHMMAP as SQLITE_IOERR_SHMMAP, - SQLITE_IOERR_SHMOPEN as SQLITE_IOERR_SHMOPEN, - SQLITE_IOERR_SHMSIZE as SQLITE_IOERR_SHMSIZE, - SQLITE_IOERR_SHORT_READ as SQLITE_IOERR_SHORT_READ, - SQLITE_IOERR_TRUNCATE as SQLITE_IOERR_TRUNCATE, - SQLITE_IOERR_UNLOCK as SQLITE_IOERR_UNLOCK, - SQLITE_IOERR_VNODE as SQLITE_IOERR_VNODE, - SQLITE_IOERR_WRITE as SQLITE_IOERR_WRITE, - SQLITE_LIMIT_ATTACHED as SQLITE_LIMIT_ATTACHED, - SQLITE_LIMIT_COLUMN as SQLITE_LIMIT_COLUMN, - SQLITE_LIMIT_COMPOUND_SELECT as SQLITE_LIMIT_COMPOUND_SELECT, - SQLITE_LIMIT_EXPR_DEPTH as SQLITE_LIMIT_EXPR_DEPTH, - SQLITE_LIMIT_FUNCTION_ARG as SQLITE_LIMIT_FUNCTION_ARG, - SQLITE_LIMIT_LENGTH as SQLITE_LIMIT_LENGTH, - SQLITE_LIMIT_LIKE_PATTERN_LENGTH as SQLITE_LIMIT_LIKE_PATTERN_LENGTH, - SQLITE_LIMIT_SQL_LENGTH as SQLITE_LIMIT_SQL_LENGTH, - SQLITE_LIMIT_TRIGGER_DEPTH as SQLITE_LIMIT_TRIGGER_DEPTH, - SQLITE_LIMIT_VARIABLE_NUMBER as SQLITE_LIMIT_VARIABLE_NUMBER, - SQLITE_LIMIT_VDBE_OP as SQLITE_LIMIT_VDBE_OP, - SQLITE_LIMIT_WORKER_THREADS as SQLITE_LIMIT_WORKER_THREADS, - SQLITE_LOCKED as SQLITE_LOCKED, - SQLITE_LOCKED_SHAREDCACHE as SQLITE_LOCKED_SHAREDCACHE, - SQLITE_LOCKED_VTAB as SQLITE_LOCKED_VTAB, - SQLITE_MISMATCH as SQLITE_MISMATCH, - SQLITE_MISUSE as SQLITE_MISUSE, - SQLITE_NOLFS as SQLITE_NOLFS, - SQLITE_NOMEM as SQLITE_NOMEM, - SQLITE_NOTADB as SQLITE_NOTADB, - SQLITE_NOTFOUND as SQLITE_NOTFOUND, - SQLITE_NOTICE as SQLITE_NOTICE, - SQLITE_NOTICE_RECOVER_ROLLBACK as SQLITE_NOTICE_RECOVER_ROLLBACK, - SQLITE_NOTICE_RECOVER_WAL as SQLITE_NOTICE_RECOVER_WAL, - SQLITE_OK_LOAD_PERMANENTLY as SQLITE_OK_LOAD_PERMANENTLY, - SQLITE_OK_SYMLINK as SQLITE_OK_SYMLINK, - SQLITE_PERM as SQLITE_PERM, - SQLITE_PROTOCOL as SQLITE_PROTOCOL, - SQLITE_RANGE as SQLITE_RANGE, - SQLITE_READONLY as SQLITE_READONLY, - SQLITE_READONLY_CANTINIT as SQLITE_READONLY_CANTINIT, - SQLITE_READONLY_CANTLOCK as SQLITE_READONLY_CANTLOCK, - SQLITE_READONLY_DBMOVED as SQLITE_READONLY_DBMOVED, - SQLITE_READONLY_DIRECTORY as SQLITE_READONLY_DIRECTORY, - SQLITE_READONLY_RECOVERY as SQLITE_READONLY_RECOVERY, - SQLITE_READONLY_ROLLBACK as SQLITE_READONLY_ROLLBACK, - SQLITE_ROW as SQLITE_ROW, - SQLITE_SCHEMA as SQLITE_SCHEMA, - SQLITE_TOOBIG as SQLITE_TOOBIG, - SQLITE_WARNING as SQLITE_WARNING, - SQLITE_WARNING_AUTOINDEX as SQLITE_WARNING_AUTOINDEX, - ) - -if sys.version_info < (3, 12): - from sqlite3.dbapi2 import enable_shared_cache as enable_shared_cache, version as version - -if sys.version_info < (3, 10): - from sqlite3.dbapi2 import OptimizedUnicode as OptimizedUnicode - -_CursorT = TypeVar("_CursorT", bound=Cursor) -_SqliteData: TypeAlias = str | ReadableBuffer | int | float | None -# Data that is passed through adapters can be of any type accepted by an adapter. -_AdaptedInputData: TypeAlias = _SqliteData | Any -# The Mapping must really be a dict, but making it invariant is too annoying. -_Parameters: TypeAlias = SupportsLenAndGetItem[_AdaptedInputData] | Mapping[str, _AdaptedInputData] - -class _AnyParamWindowAggregateClass(Protocol): - def step(self, *args: Any) -> object: ... - def inverse(self, *args: Any) -> object: ... - def value(self) -> _SqliteData: ... - def finalize(self) -> _SqliteData: ... - -class _WindowAggregateClass(Protocol): - step: Callable[..., object] - inverse: Callable[..., object] - def value(self) -> _SqliteData: ... - def finalize(self) -> _SqliteData: ... - -class _AggregateProtocol(Protocol): - def step(self, value: int, /) -> object: ... - def finalize(self) -> int: ... - -class _SingleParamWindowAggregateClass(Protocol): - def step(self, param: Any, /) -> object: ... - def inverse(self, param: Any, /) -> object: ... - def value(self) -> _SqliteData: ... - def finalize(self) -> _SqliteData: ... - -# These classes are implemented in the C module _sqlite3. At runtime, they're imported -# from there into sqlite3.dbapi2 and from that module to here. However, they -# consider themselves to live in the sqlite3.* namespace, so we'll define them here. - -class Error(Exception): - if sys.version_info >= (3, 11): - sqlite_errorcode: int - sqlite_errorname: str - -class DatabaseError(Error): ... -class DataError(DatabaseError): ... -class IntegrityError(DatabaseError): ... -class InterfaceError(Error): ... -class InternalError(DatabaseError): ... -class NotSupportedError(DatabaseError): ... -class OperationalError(DatabaseError): ... -class ProgrammingError(DatabaseError): ... -class Warning(Exception): ... - -class Connection: - @property - def DataError(self) -> type[DataError]: ... - @property - def DatabaseError(self) -> type[DatabaseError]: ... - @property - def Error(self) -> type[Error]: ... - @property - def IntegrityError(self) -> type[IntegrityError]: ... - @property - def InterfaceError(self) -> type[InterfaceError]: ... - @property - def InternalError(self) -> type[InternalError]: ... - @property - def NotSupportedError(self) -> type[NotSupportedError]: ... - @property - def OperationalError(self) -> type[OperationalError]: ... - @property - def ProgrammingError(self) -> type[ProgrammingError]: ... - @property - def Warning(self) -> type[Warning]: ... - @property - def in_transaction(self) -> bool: ... - isolation_level: str | None # one of '', 'DEFERRED', 'IMMEDIATE' or 'EXCLUSIVE' - @property - def total_changes(self) -> int: ... - if sys.version_info >= (3, 12): - @property - def autocommit(self) -> int: ... - @autocommit.setter - def autocommit(self, val: int) -> None: ... - row_factory: Any - text_factory: Any - if sys.version_info >= (3, 12): - def __init__( - self, - database: StrOrBytesPath, - timeout: float = ..., - detect_types: int = ..., - isolation_level: str | None = ..., - check_same_thread: bool = ..., - factory: type[Connection] | None = ..., - cached_statements: int = ..., - uri: bool = ..., - autocommit: bool = ..., - ) -> None: ... - else: - def __init__( - self, - database: StrOrBytesPath, - timeout: float = ..., - detect_types: int = ..., - isolation_level: str | None = ..., - check_same_thread: bool = ..., - factory: type[Connection] | None = ..., - cached_statements: int = ..., - uri: bool = ..., - ) -> None: ... - - def close(self) -> None: ... - if sys.version_info >= (3, 11): - def blobopen(self, table: str, column: str, row: int, /, *, readonly: bool = False, name: str = "main") -> Blob: ... - - def commit(self) -> None: ... - def create_aggregate(self, name: str, n_arg: int, aggregate_class: Callable[[], _AggregateProtocol]) -> None: ... - if sys.version_info >= (3, 11): - # num_params determines how many params will be passed to the aggregate class. We provide an overload - # for the case where num_params = 1, which is expected to be the common case. - @overload - def create_window_function( - self, name: str, num_params: Literal[1], aggregate_class: Callable[[], _SingleParamWindowAggregateClass] | None, / - ) -> None: ... - # And for num_params = -1, which means the aggregate must accept any number of parameters. - @overload - def create_window_function( - self, name: str, num_params: Literal[-1], aggregate_class: Callable[[], _AnyParamWindowAggregateClass] | None, / - ) -> None: ... - @overload - def create_window_function( - self, name: str, num_params: int, aggregate_class: Callable[[], _WindowAggregateClass] | None, / - ) -> None: ... - - def create_collation(self, name: str, callback: Callable[[str, str], int | SupportsIndex] | None, /) -> None: ... - def create_function( - self, name: str, narg: int, func: Callable[..., _SqliteData] | None, *, deterministic: bool = False - ) -> None: ... - @overload - def cursor(self, factory: None = None) -> Cursor: ... - @overload - def cursor(self, factory: Callable[[Connection], _CursorT]) -> _CursorT: ... - def execute(self, sql: str, parameters: _Parameters = ..., /) -> Cursor: ... - def executemany(self, sql: str, parameters: Iterable[_Parameters], /) -> Cursor: ... - def executescript(self, sql_script: str, /) -> Cursor: ... - def interrupt(self) -> None: ... - if sys.version_info >= (3, 13): - def iterdump(self, *, filter: str | None = None) -> Generator[str, None, None]: ... - else: - def iterdump(self) -> Generator[str, None, None]: ... - - def rollback(self) -> None: ... - def set_authorizer( - self, authorizer_callback: Callable[[int, str | None, str | None, str | None, str | None], int] | None - ) -> None: ... - def set_progress_handler(self, progress_handler: Callable[[], int | None] | None, n: int) -> None: ... - def set_trace_callback(self, trace_callback: Callable[[str], object] | None) -> None: ... - # enable_load_extension and load_extension is not available on python distributions compiled - # without sqlite3 loadable extension support. see footnotes https://docs.python.org/3/library/sqlite3.html#f1 - def enable_load_extension(self, enable: bool, /) -> None: ... - if sys.version_info >= (3, 12): - def load_extension(self, name: str, /, *, entrypoint: str | None = None) -> None: ... - else: - def load_extension(self, name: str, /) -> None: ... - - def backup( - self, - target: Connection, - *, - pages: int = -1, - progress: Callable[[int, int, int], object] | None = None, - name: str = "main", - sleep: float = 0.25, - ) -> None: ... - if sys.version_info >= (3, 11): - def setlimit(self, category: int, limit: int, /) -> int: ... - def getlimit(self, category: int, /) -> int: ... - def serialize(self, *, name: str = "main") -> bytes: ... - def deserialize(self, data: ReadableBuffer, /, *, name: str = "main") -> None: ... - if sys.version_info >= (3, 12): - def getconfig(self, op: int, /) -> bool: ... - def setconfig(self, op: int, enable: bool = True, /) -> bool: ... - - def __call__(self, sql: str, /) -> _Statement: ... - def __enter__(self) -> Self: ... - def __exit__( - self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None, / - ) -> Literal[False]: ... - -class Cursor: - arraysize: int - @property - def connection(self) -> Connection: ... - # May be None, but using `| MaybeNone` (`| Any`) instead to avoid slightly annoying false positives. - @property - def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | MaybeNone: ... - @property - def lastrowid(self) -> int | None: ... - row_factory: Callable[[Cursor, Row], object] | None - @property - def rowcount(self) -> int: ... - def __init__(self, cursor: Connection, /) -> None: ... - def close(self) -> None: ... - def execute(self, sql: str, parameters: _Parameters = (), /) -> Self: ... - def executemany(self, sql: str, seq_of_parameters: Iterable[_Parameters], /) -> Self: ... - def executescript(self, sql_script: str, /) -> Cursor: ... - def fetchall(self) -> list[Any]: ... - def fetchmany(self, size: int | None = 1) -> list[Any]: ... - # Returns either a row (as created by the row_factory) or None, but - # putting None in the return annotation causes annoying false positives. - def fetchone(self) -> Any: ... - def setinputsizes(self, sizes: Unused, /) -> None: ... # does nothing - def setoutputsize(self, size: Unused, column: Unused = None, /) -> None: ... # does nothing - def __iter__(self) -> Self: ... - def __next__(self) -> Any: ... - -@final -class PrepareProtocol: - def __init__(self, *args: object, **kwargs: object) -> None: ... - -class Row(Sequence[Any]): - def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> Self: ... - def keys(self) -> list[str]: ... - @overload - def __getitem__(self, key: int | str, /) -> Any: ... - @overload - def __getitem__(self, key: slice, /) -> tuple[Any, ...]: ... - def __hash__(self) -> int: ... - def __iter__(self) -> Iterator[Any]: ... - def __len__(self) -> int: ... - # These return NotImplemented for anything that is not a Row. - def __eq__(self, value: object, /) -> bool: ... - def __ge__(self, value: object, /) -> bool: ... - def __gt__(self, value: object, /) -> bool: ... - def __le__(self, value: object, /) -> bool: ... - def __lt__(self, value: object, /) -> bool: ... - def __ne__(self, value: object, /) -> bool: ... - -# This class is not exposed. It calls itself sqlite3.Statement. -@final -@type_check_only -class _Statement: ... - -if sys.version_info >= (3, 11): - @final - class Blob: - def close(self) -> None: ... - def read(self, length: int = -1, /) -> bytes: ... - def write(self, data: ReadableBuffer, /) -> None: ... - def tell(self) -> int: ... - # whence must be one of os.SEEK_SET, os.SEEK_CUR, os.SEEK_END - def seek(self, offset: int, origin: int = 0, /) -> None: ... - def __len__(self) -> int: ... - def __enter__(self) -> Self: ... - def __exit__(self, type: object, val: object, tb: object, /) -> Literal[False]: ... - def __getitem__(self, key: SupportsIndex | slice, /) -> int: ... - def __setitem__(self, key: SupportsIndex | slice, value: int, /) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/string.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/string.pyi deleted file mode 100644 index da752327d3f78..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/string.pyi +++ /dev/null @@ -1,76 +0,0 @@ -import sys -from _typeshed import StrOrLiteralStr -from collections.abc import Iterable, Mapping, Sequence -from re import Pattern, RegexFlag -from typing import Any, ClassVar, overload -from typing_extensions import LiteralString - -__all__ = [ - "ascii_letters", - "ascii_lowercase", - "ascii_uppercase", - "capwords", - "digits", - "hexdigits", - "octdigits", - "printable", - "punctuation", - "whitespace", - "Formatter", - "Template", -] - -ascii_letters: LiteralString -ascii_lowercase: LiteralString -ascii_uppercase: LiteralString -digits: LiteralString -hexdigits: LiteralString -octdigits: LiteralString -punctuation: LiteralString -printable: LiteralString -whitespace: LiteralString - -def capwords(s: StrOrLiteralStr, sep: StrOrLiteralStr | None = None) -> StrOrLiteralStr: ... - -class Template(metaclass=type): - template: str - delimiter: ClassVar[str] - idpattern: ClassVar[str] - braceidpattern: ClassVar[str | None] - flags: ClassVar[RegexFlag] - pattern: ClassVar[Pattern[str]] - def __init__(self, template: str) -> None: ... - def substitute(self, mapping: Mapping[str, object] = {}, /, **kwds: object) -> str: ... - def safe_substitute(self, mapping: Mapping[str, object] = {}, /, **kwds: object) -> str: ... - if sys.version_info >= (3, 11): - def get_identifiers(self) -> list[str]: ... - def is_valid(self) -> bool: ... - -class Formatter: - @overload - def format(self, format_string: LiteralString, /, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ... - @overload - def format(self, format_string: str, /, *args: Any, **kwargs: Any) -> str: ... - @overload - def vformat( - self, format_string: LiteralString, args: Sequence[LiteralString], kwargs: Mapping[LiteralString, LiteralString] - ) -> LiteralString: ... - @overload - def vformat(self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> str: ... - def _vformat( # undocumented - self, - format_string: str, - args: Sequence[Any], - kwargs: Mapping[str, Any], - used_args: set[int | str], - recursion_depth: int, - auto_arg_index: int = 0, - ) -> tuple[str, int]: ... - def parse( - self, format_string: StrOrLiteralStr - ) -> Iterable[tuple[StrOrLiteralStr, StrOrLiteralStr | None, StrOrLiteralStr | None, StrOrLiteralStr | None]]: ... - def get_field(self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> Any: ... - def get_value(self, key: int | str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> Any: ... - def check_unused_args(self, used_args: set[int | str], args: Sequence[Any], kwargs: Mapping[str, Any]) -> None: ... - def format_field(self, value: Any, format_spec: str) -> Any: ... - def convert_field(self, value: Any, conversion: str | None) -> Any: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi deleted file mode 100644 index a2cca3509a9ce..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi +++ /dev/null @@ -1,472 +0,0 @@ -import sys -from _typeshed import MaybeNone, OptExcInfo, ProfileFunction, TraceFunction, structseq -from _typeshed.importlib import MetaPathFinderProtocol, PathEntryFinderProtocol -from builtins import object as _object -from collections.abc import AsyncGenerator, Callable, Sequence -from io import TextIOWrapper -from types import FrameType, ModuleType, TracebackType -from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeVar, final, type_check_only -from typing_extensions import LiteralString, TypeAlias - -_T = TypeVar("_T") - -# see https://github.com/python/typeshed/issues/8513#issue-1333671093 for the rationale behind this alias -_ExitCode: TypeAlias = str | int | None - -# ----- sys variables ----- -if sys.platform != "win32": - abiflags: str -argv: list[str] -base_exec_prefix: str -base_prefix: str -byteorder: Literal["little", "big"] -builtin_module_names: Sequence[str] # actually a tuple of strings -copyright: str -if sys.platform == "win32": - dllhandle: int -dont_write_bytecode: bool -displayhook: Callable[[object], Any] -excepthook: Callable[[type[BaseException], BaseException, TracebackType | None], Any] -exec_prefix: str -executable: str -float_repr_style: Literal["short", "legacy"] -hexversion: int -last_type: type[BaseException] | None -last_value: BaseException | None -last_traceback: TracebackType | None -if sys.version_info >= (3, 12): - last_exc: BaseException # or undefined. -maxsize: int -maxunicode: int -meta_path: list[MetaPathFinderProtocol] -modules: dict[str, ModuleType] -if sys.version_info >= (3, 10): - orig_argv: list[str] -path: list[str] -path_hooks: list[Callable[[str], PathEntryFinderProtocol]] -path_importer_cache: dict[str, PathEntryFinderProtocol | None] -platform: LiteralString -platlibdir: str -prefix: str -pycache_prefix: str | None -ps1: object -ps2: object - -# TextIO is used instead of more specific types for the standard streams, -# since they are often monkeypatched at runtime. At startup, the objects -# are initialized to instances of TextIOWrapper, but can also be None under -# some circumstances. -# -# To use methods from TextIOWrapper, use an isinstance check to ensure that -# the streams have not been overridden: -# -# if isinstance(sys.stdout, io.TextIOWrapper): -# sys.stdout.reconfigure(...) -stdin: TextIO | MaybeNone -stdout: TextIO | MaybeNone -stderr: TextIO | MaybeNone - -if sys.version_info >= (3, 10): - stdlib_module_names: frozenset[str] - -__stdin__: Final[TextIOWrapper | None] # Contains the original value of stdin -__stdout__: Final[TextIOWrapper | None] # Contains the original value of stdout -__stderr__: Final[TextIOWrapper | None] # Contains the original value of stderr -tracebacklimit: int | None -version: str -api_version: int -warnoptions: Any -# Each entry is a tuple of the form (action, message, category, module, -# lineno) -if sys.platform == "win32": - winver: str -_xoptions: dict[Any, Any] - -# Type alias used as a mixin for structseq classes that cannot be instantiated at runtime -# This can't be represented in the type system, so we just use `structseq[Any]` -_UninstantiableStructseq: TypeAlias = structseq[Any] - -flags: _flags - -# This class is not exposed at runtime. It calls itself sys.flags. -# As a tuple, it can have a length between 15 and 18. We don't model -# the exact length here because that varies by patch version due to -# the backported security fix int_max_str_digits. The exact length shouldn't -# be relied upon. See #13031 -# This can be re-visited when typeshed drops support for 3.10, -# at which point all supported versions will include int_max_str_digits -# in all patch versions. -# 3.8 and 3.9 are 15 or 16-tuple -# 3.10 is 16 or 17-tuple -# 3.11+ is an 18-tuple. -@final -@type_check_only -class _flags(_UninstantiableStructseq, tuple[int, ...]): - # `safe_path` was added in py311 - if sys.version_info >= (3, 11): - __match_args__: Final = ( - "debug", - "inspect", - "interactive", - "optimize", - "dont_write_bytecode", - "no_user_site", - "no_site", - "ignore_environment", - "verbose", - "bytes_warning", - "quiet", - "hash_randomization", - "isolated", - "dev_mode", - "utf8_mode", - "warn_default_encoding", - "safe_path", - "int_max_str_digits", - ) - elif sys.version_info >= (3, 10): - __match_args__: Final = ( - "debug", - "inspect", - "interactive", - "optimize", - "dont_write_bytecode", - "no_user_site", - "no_site", - "ignore_environment", - "verbose", - "bytes_warning", - "quiet", - "hash_randomization", - "isolated", - "dev_mode", - "utf8_mode", - "warn_default_encoding", - "int_max_str_digits", - ) - - @property - def debug(self) -> int: ... - @property - def inspect(self) -> int: ... - @property - def interactive(self) -> int: ... - @property - def optimize(self) -> int: ... - @property - def dont_write_bytecode(self) -> int: ... - @property - def no_user_site(self) -> int: ... - @property - def no_site(self) -> int: ... - @property - def ignore_environment(self) -> int: ... - @property - def verbose(self) -> int: ... - @property - def bytes_warning(self) -> int: ... - @property - def quiet(self) -> int: ... - @property - def hash_randomization(self) -> int: ... - @property - def isolated(self) -> int: ... - @property - def dev_mode(self) -> bool: ... - @property - def utf8_mode(self) -> int: ... - if sys.version_info >= (3, 10): - @property - def warn_default_encoding(self) -> int: ... - if sys.version_info >= (3, 11): - @property - def safe_path(self) -> bool: ... - # Whether or not this exists on lower versions of Python - # may depend on which patch release you're using - # (it was backported to all Python versions on 3.8+ as a security fix) - # Added in: 3.8.14, 3.9.14, 3.10.7 - # and present in all versions of 3.11 and later. - @property - def int_max_str_digits(self) -> int: ... - -float_info: _float_info - -# This class is not exposed at runtime. It calls itself sys.float_info. -@final -@type_check_only -class _float_info(structseq[float], tuple[float, int, int, float, int, int, int, int, float, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ( - "max", - "max_exp", - "max_10_exp", - "min", - "min_exp", - "min_10_exp", - "dig", - "mant_dig", - "epsilon", - "radix", - "rounds", - ) - - @property - def max(self) -> float: ... # DBL_MAX - @property - def max_exp(self) -> int: ... # DBL_MAX_EXP - @property - def max_10_exp(self) -> int: ... # DBL_MAX_10_EXP - @property - def min(self) -> float: ... # DBL_MIN - @property - def min_exp(self) -> int: ... # DBL_MIN_EXP - @property - def min_10_exp(self) -> int: ... # DBL_MIN_10_EXP - @property - def dig(self) -> int: ... # DBL_DIG - @property - def mant_dig(self) -> int: ... # DBL_MANT_DIG - @property - def epsilon(self) -> float: ... # DBL_EPSILON - @property - def radix(self) -> int: ... # FLT_RADIX - @property - def rounds(self) -> int: ... # FLT_ROUNDS - -hash_info: _hash_info - -# This class is not exposed at runtime. It calls itself sys.hash_info. -@final -@type_check_only -class _hash_info(structseq[Any | int], tuple[int, int, int, int, int, str, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("width", "modulus", "inf", "nan", "imag", "algorithm", "hash_bits", "seed_bits", "cutoff") - - @property - def width(self) -> int: ... - @property - def modulus(self) -> int: ... - @property - def inf(self) -> int: ... - @property - def nan(self) -> int: ... - @property - def imag(self) -> int: ... - @property - def algorithm(self) -> str: ... - @property - def hash_bits(self) -> int: ... - @property - def seed_bits(self) -> int: ... - @property - def cutoff(self) -> int: ... # undocumented - -implementation: _implementation - -# This class isn't really a thing. At runtime, implementation is an instance -# of types.SimpleNamespace. This allows for better typing. -@type_check_only -class _implementation: - name: str - version: _version_info - hexversion: int - cache_tag: str - # Define __getattr__, as the documentation states: - # > sys.implementation may contain additional attributes specific to the Python implementation. - # > These non-standard attributes must start with an underscore, and are not described here. - def __getattr__(self, name: str) -> Any: ... - -int_info: _int_info - -# This class is not exposed at runtime. It calls itself sys.int_info. -@final -@type_check_only -class _int_info(structseq[int], tuple[int, int, int, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("bits_per_digit", "sizeof_digit", "default_max_str_digits", "str_digits_check_threshold") - - @property - def bits_per_digit(self) -> int: ... - @property - def sizeof_digit(self) -> int: ... - @property - def default_max_str_digits(self) -> int: ... - @property - def str_digits_check_threshold(self) -> int: ... - -_ThreadInfoName: TypeAlias = Literal["nt", "pthread", "pthread-stubs", "solaris"] -_ThreadInfoLock: TypeAlias = Literal["semaphore", "mutex+cond"] | None - -# This class is not exposed at runtime. It calls itself sys.thread_info. -@final -@type_check_only -class _thread_info(_UninstantiableStructseq, tuple[_ThreadInfoName, _ThreadInfoLock, str | None]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("name", "lock", "version") - - @property - def name(self) -> _ThreadInfoName: ... - @property - def lock(self) -> _ThreadInfoLock: ... - @property - def version(self) -> str | None: ... - -thread_info: _thread_info -_ReleaseLevel: TypeAlias = Literal["alpha", "beta", "candidate", "final"] - -# This class is not exposed at runtime. It calls itself sys.version_info. -@final -@type_check_only -class _version_info(_UninstantiableStructseq, tuple[int, int, int, _ReleaseLevel, int]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("major", "minor", "micro", "releaselevel", "serial") - - @property - def major(self) -> int: ... - @property - def minor(self) -> int: ... - @property - def micro(self) -> int: ... - @property - def releaselevel(self) -> _ReleaseLevel: ... - @property - def serial(self) -> int: ... - -version_info: _version_info - -def call_tracing(func: Callable[..., _T], args: Any, /) -> _T: ... -def _clear_type_cache() -> None: ... -def _current_frames() -> dict[int, FrameType]: ... -def _getframe(depth: int = 0, /) -> FrameType: ... -def _debugmallocstats() -> None: ... -def __displayhook__(object: object, /) -> None: ... -def __excepthook__(exctype: type[BaseException], value: BaseException, traceback: TracebackType | None, /) -> None: ... -def exc_info() -> OptExcInfo: ... - -if sys.version_info >= (3, 11): - def exception() -> BaseException | None: ... - -def exit(status: _ExitCode = None, /) -> NoReturn: ... -def getallocatedblocks() -> int: ... -def getdefaultencoding() -> str: ... - -if sys.platform != "win32": - def getdlopenflags() -> int: ... - -def getfilesystemencoding() -> str: ... -def getfilesystemencodeerrors() -> str: ... -def getrefcount(object: Any, /) -> int: ... -def getrecursionlimit() -> int: ... -def getsizeof(obj: object, default: int = ...) -> int: ... -def getswitchinterval() -> float: ... -def getprofile() -> ProfileFunction | None: ... -def setprofile(function: ProfileFunction | None, /) -> None: ... -def gettrace() -> TraceFunction | None: ... -def settrace(function: TraceFunction | None, /) -> None: ... - -if sys.platform == "win32": - # A tuple of length 5, even though it has more than 5 attributes. - @final - class _WinVersion(_UninstantiableStructseq, tuple[int, int, int, int, str]): - @property - def major(self) -> int: ... - @property - def minor(self) -> int: ... - @property - def build(self) -> int: ... - @property - def platform(self) -> int: ... - @property - def service_pack(self) -> str: ... - @property - def service_pack_minor(self) -> int: ... - @property - def service_pack_major(self) -> int: ... - @property - def suite_mask(self) -> int: ... - @property - def product_type(self) -> int: ... - @property - def platform_version(self) -> tuple[int, int, int]: ... - - def getwindowsversion() -> _WinVersion: ... - -def intern(string: str, /) -> str: ... - -if sys.version_info >= (3, 13): - def _is_gil_enabled() -> bool: ... - def _clear_internal_caches() -> None: ... - def _is_interned(string: str, /) -> bool: ... - -def is_finalizing() -> bool: ... -def breakpointhook(*args: Any, **kwargs: Any) -> Any: ... - -__breakpointhook__ = breakpointhook # Contains the original value of breakpointhook - -if sys.platform != "win32": - def setdlopenflags(flags: int, /) -> None: ... - -def setrecursionlimit(limit: int, /) -> None: ... -def setswitchinterval(interval: float, /) -> None: ... -def gettotalrefcount() -> int: ... # Debug builds only - -# Doesn't exist at runtime, but exported in the stubs so pytest etc. can annotate their code more easily. -@type_check_only -class UnraisableHookArgs(Protocol): - exc_type: type[BaseException] - exc_value: BaseException | None - exc_traceback: TracebackType | None - err_msg: str | None - object: _object - -unraisablehook: Callable[[UnraisableHookArgs], Any] - -def __unraisablehook__(unraisable: UnraisableHookArgs, /) -> Any: ... -def addaudithook(hook: Callable[[str, tuple[Any, ...]], Any]) -> None: ... -def audit(event: str, /, *args: Any) -> None: ... - -_AsyncgenHook: TypeAlias = Callable[[AsyncGenerator[Any, Any]], None] | None - -# This class is not exposed at runtime. It calls itself builtins.asyncgen_hooks. -@final -@type_check_only -class _asyncgen_hooks(structseq[_AsyncgenHook], tuple[_AsyncgenHook, _AsyncgenHook]): - if sys.version_info >= (3, 10): - __match_args__: Final = ("firstiter", "finalizer") - - @property - def firstiter(self) -> _AsyncgenHook: ... - @property - def finalizer(self) -> _AsyncgenHook: ... - -def get_asyncgen_hooks() -> _asyncgen_hooks: ... -def set_asyncgen_hooks(firstiter: _AsyncgenHook = ..., finalizer: _AsyncgenHook = ...) -> None: ... - -if sys.platform == "win32": - def _enablelegacywindowsfsencoding() -> None: ... - -def get_coroutine_origin_tracking_depth() -> int: ... -def set_coroutine_origin_tracking_depth(depth: int) -> None: ... - -# The following two functions were added in 3.11.0, 3.10.7, 3.9.14, and 3.8.14, -# as part of the response to CVE-2020-10735 -def set_int_max_str_digits(maxdigits: int) -> None: ... -def get_int_max_str_digits() -> int: ... - -if sys.version_info >= (3, 12): - if sys.version_info >= (3, 13): - def getunicodeinternedsize(*, _only_immortal: bool = False) -> int: ... - else: - def getunicodeinternedsize() -> int: ... - - def deactivate_stack_trampoline() -> None: ... - def is_stack_trampoline_active() -> bool: ... - # It always exists, but raises on non-linux platforms: - if sys.platform == "linux": - def activate_stack_trampoline(backend: str, /) -> None: ... - else: - def activate_stack_trampoline(backend: str, /) -> NoReturn: ... - - from . import _monitoring - - monitoring = _monitoring diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi deleted file mode 100644 index 291e2fc5108f6..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi +++ /dev/null @@ -1,4087 +0,0 @@ -import _tkinter -import sys -from _typeshed import Incomplete, MaybeNone, StrOrBytesPath -from collections.abc import Callable, Iterable, Mapping, Sequence -from tkinter.constants import * -from tkinter.font import _FontDescription -from types import TracebackType -from typing import Any, ClassVar, Generic, Literal, NamedTuple, Protocol, TypedDict, TypeVar, overload, type_check_only -from typing_extensions import TypeAlias, TypeVarTuple, Unpack, deprecated - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - -__all__ = [ - "TclError", - "NO", - "FALSE", - "OFF", - "YES", - "TRUE", - "ON", - "N", - "S", - "W", - "E", - "NW", - "SW", - "NE", - "SE", - "NS", - "EW", - "NSEW", - "CENTER", - "NONE", - "X", - "Y", - "BOTH", - "LEFT", - "TOP", - "RIGHT", - "BOTTOM", - "RAISED", - "SUNKEN", - "FLAT", - "RIDGE", - "GROOVE", - "SOLID", - "HORIZONTAL", - "VERTICAL", - "NUMERIC", - "CHAR", - "WORD", - "BASELINE", - "INSIDE", - "OUTSIDE", - "SEL", - "SEL_FIRST", - "SEL_LAST", - "END", - "INSERT", - "CURRENT", - "ANCHOR", - "ALL", - "NORMAL", - "DISABLED", - "ACTIVE", - "HIDDEN", - "CASCADE", - "CHECKBUTTON", - "COMMAND", - "RADIOBUTTON", - "SEPARATOR", - "SINGLE", - "BROWSE", - "MULTIPLE", - "EXTENDED", - "DOTBOX", - "UNDERLINE", - "PIESLICE", - "CHORD", - "ARC", - "FIRST", - "LAST", - "BUTT", - "PROJECTING", - "ROUND", - "BEVEL", - "MITER", - "MOVETO", - "SCROLL", - "UNITS", - "PAGES", - "TkVersion", - "TclVersion", - "READABLE", - "WRITABLE", - "EXCEPTION", - "EventType", - "Event", - "NoDefaultRoot", - "Variable", - "StringVar", - "IntVar", - "DoubleVar", - "BooleanVar", - "mainloop", - "getint", - "getdouble", - "getboolean", - "Misc", - "CallWrapper", - "XView", - "YView", - "Wm", - "Tk", - "Tcl", - "Pack", - "Place", - "Grid", - "BaseWidget", - "Widget", - "Toplevel", - "Button", - "Canvas", - "Checkbutton", - "Entry", - "Frame", - "Label", - "Listbox", - "Menu", - "Menubutton", - "Message", - "Radiobutton", - "Scale", - "Scrollbar", - "Text", - "OptionMenu", - "Image", - "PhotoImage", - "BitmapImage", - "image_names", - "image_types", - "Spinbox", - "LabelFrame", - "PanedWindow", -] - -# Using anything from tkinter.font in this file means that 'import tkinter' -# seems to also load tkinter.font. That's not how it actually works, but -# unfortunately not much can be done about it. https://github.com/python/typeshed/pull/4346 - -TclError = _tkinter.TclError -wantobjects: int -TkVersion: float -TclVersion: float -READABLE = _tkinter.READABLE -WRITABLE = _tkinter.WRITABLE -EXCEPTION = _tkinter.EXCEPTION - -# Quick guide for figuring out which widget class to choose: -# - Misc: any widget (don't use BaseWidget because Tk doesn't inherit from BaseWidget) -# - Widget: anything that is meant to be put into another widget with e.g. pack or grid -# -# Don't trust tkinter's docstrings, because they have been created by copy/pasting from -# Tk's manual pages more than 10 years ago. Use the latest manual pages instead: -# -# $ sudo apt install tk-doc tcl-doc -# $ man 3tk label # tkinter.Label -# $ man 3tk ttk_label # tkinter.ttk.Label -# $ man 3tcl after # tkinter.Misc.after -# -# You can also read the manual pages online: https://www.tcl.tk/doc/ - -# Some widgets have an option named -compound that accepts different values -# than the _Compound defined here. Many other options have similar things. -_Anchor: TypeAlias = Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] # manual page: Tk_GetAnchor -_ButtonCommand: TypeAlias = str | Callable[[], Any] # accepts string of tcl code, return value is returned from Button.invoke() -_Compound: TypeAlias = Literal["top", "left", "center", "right", "bottom", "none"] # -compound in manual page named 'options' -# manual page: Tk_GetCursor -_Cursor: TypeAlias = str | tuple[str] | tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] -# example when it's sequence: entry['invalidcommand'] = [entry.register(print), '%P'] -_EntryValidateCommand: TypeAlias = str | list[str] | tuple[str, ...] | Callable[[], bool] -_ImageSpec: TypeAlias = _Image | str # str can be from e.g. tkinter.image_names() -_Relief: TypeAlias = Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] # manual page: Tk_GetRelief -_ScreenUnits: TypeAlias = str | float # Often the right type instead of int. Manual page: Tk_GetPixels -# -xscrollcommand and -yscrollcommand in 'options' manual page -_XYScrollCommand: TypeAlias = str | Callable[[float, float], object] -_TakeFocusValue: TypeAlias = bool | Literal[0, 1, ""] | Callable[[str], bool | None] # -takefocus in manual page named 'options' - -if sys.version_info >= (3, 11): - @type_check_only - class _VersionInfoTypeBase(NamedTuple): - major: int - minor: int - micro: int - releaselevel: str - serial: int - - class _VersionInfoType(_VersionInfoTypeBase): ... - -if sys.version_info >= (3, 11): - class EventType(StrEnum): - Activate = "36" - ButtonPress = "4" - Button = ButtonPress - ButtonRelease = "5" - Circulate = "26" - CirculateRequest = "27" - ClientMessage = "33" - Colormap = "32" - Configure = "22" - ConfigureRequest = "23" - Create = "16" - Deactivate = "37" - Destroy = "17" - Enter = "7" - Expose = "12" - FocusIn = "9" - FocusOut = "10" - GraphicsExpose = "13" - Gravity = "24" - KeyPress = "2" - Key = "2" - KeyRelease = "3" - Keymap = "11" - Leave = "8" - Map = "19" - MapRequest = "20" - Mapping = "34" - Motion = "6" - MouseWheel = "38" - NoExpose = "14" - Property = "28" - Reparent = "21" - ResizeRequest = "25" - Selection = "31" - SelectionClear = "29" - SelectionRequest = "30" - Unmap = "18" - VirtualEvent = "35" - Visibility = "15" - -else: - class EventType(str, Enum): - Activate = "36" - ButtonPress = "4" - Button = ButtonPress - ButtonRelease = "5" - Circulate = "26" - CirculateRequest = "27" - ClientMessage = "33" - Colormap = "32" - Configure = "22" - ConfigureRequest = "23" - Create = "16" - Deactivate = "37" - Destroy = "17" - Enter = "7" - Expose = "12" - FocusIn = "9" - FocusOut = "10" - GraphicsExpose = "13" - Gravity = "24" - KeyPress = "2" - Key = KeyPress - KeyRelease = "3" - Keymap = "11" - Leave = "8" - Map = "19" - MapRequest = "20" - Mapping = "34" - Motion = "6" - MouseWheel = "38" - NoExpose = "14" - Property = "28" - Reparent = "21" - ResizeRequest = "25" - Selection = "31" - SelectionClear = "29" - SelectionRequest = "30" - Unmap = "18" - VirtualEvent = "35" - Visibility = "15" - -_W = TypeVar("_W", bound=Misc) -# Events considered covariant because you should never assign to event.widget. -_W_co = TypeVar("_W_co", covariant=True, bound=Misc) - -class Event(Generic[_W_co]): - serial: int - num: int - focus: bool - height: int - width: int - keycode: int - state: int | str - time: int - x: int - y: int - x_root: int - y_root: int - char: str - send_event: bool - keysym: str - keysym_num: int - type: EventType - widget: _W_co - delta: int - -def NoDefaultRoot() -> None: ... - -class Variable: - def __init__(self, master: Misc | None = None, value: Incomplete | None = None, name: str | None = None) -> None: ... - def set(self, value) -> None: ... - initialize = set - def get(self): ... - def trace_add(self, mode: Literal["array", "read", "write", "unset"], callback: Callable[[str, str, str], object]) -> str: ... - def trace_remove(self, mode: Literal["array", "read", "write", "unset"], cbname: str) -> None: ... - def trace_info(self) -> list[tuple[tuple[Literal["array", "read", "write", "unset"], ...], str]]: ... - @deprecated("use trace_add() instead of trace()") - def trace(self, mode, callback): ... - @deprecated("use trace_add() instead of trace_variable()") - def trace_variable(self, mode, callback): ... - @deprecated("use trace_remove() instead of trace_vdelete()") - def trace_vdelete(self, mode, cbname) -> None: ... - @deprecated("use trace_info() instead of trace_vinfo()") - def trace_vinfo(self): ... - def __eq__(self, other: object) -> bool: ... - def __del__(self) -> None: ... - __hash__: ClassVar[None] # type: ignore[assignment] - -class StringVar(Variable): - def __init__(self, master: Misc | None = None, value: str | None = None, name: str | None = None) -> None: ... - def set(self, value: str) -> None: ... - initialize = set - def get(self) -> str: ... - -class IntVar(Variable): - def __init__(self, master: Misc | None = None, value: int | None = None, name: str | None = None) -> None: ... - def set(self, value: int) -> None: ... - initialize = set - def get(self) -> int: ... - -class DoubleVar(Variable): - def __init__(self, master: Misc | None = None, value: float | None = None, name: str | None = None) -> None: ... - def set(self, value: float) -> None: ... - initialize = set - def get(self) -> float: ... - -class BooleanVar(Variable): - def __init__(self, master: Misc | None = None, value: bool | None = None, name: str | None = None) -> None: ... - def set(self, value: bool) -> None: ... - initialize = set - def get(self) -> bool: ... - -def mainloop(n: int = 0) -> None: ... - -getint: Incomplete -getdouble: Incomplete - -def getboolean(s): ... - -_Ts = TypeVarTuple("_Ts") - -class _GridIndexInfo(TypedDict, total=False): - minsize: _ScreenUnits - pad: _ScreenUnits - uniform: str | None - weight: int - -class _BusyInfo(TypedDict): - cursor: _Cursor - -class Misc: - master: Misc | None - tk: _tkinter.TkappType - children: dict[str, Widget] - def destroy(self) -> None: ... - def deletecommand(self, name: str) -> None: ... - def tk_strictMotif(self, boolean: Incomplete | None = None): ... - def tk_bisque(self) -> None: ... - def tk_setPalette(self, *args, **kw) -> None: ... - def wait_variable(self, name: str | Variable = "PY_VAR") -> None: ... - waitvar = wait_variable - def wait_window(self, window: Misc | None = None) -> None: ... - def wait_visibility(self, window: Misc | None = None) -> None: ... - def setvar(self, name: str = "PY_VAR", value: str = "1") -> None: ... - def getvar(self, name: str = "PY_VAR"): ... - def getint(self, s): ... - def getdouble(self, s): ... - def getboolean(self, s): ... - def focus_set(self) -> None: ... - focus = focus_set - def focus_force(self) -> None: ... - def focus_get(self) -> Misc | None: ... - def focus_displayof(self) -> Misc | None: ... - def focus_lastfor(self) -> Misc | None: ... - def tk_focusFollowsMouse(self) -> None: ... - def tk_focusNext(self) -> Misc | None: ... - def tk_focusPrev(self) -> Misc | None: ... - # .after() can be called without the "func" argument, but it is basically never what you want. - # It behaves like time.sleep() and freezes the GUI app. - def after(self, ms: int | Literal["idle"], func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... - # after_idle is essentially partialmethod(after, "idle") - def after_idle(self, func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... - def after_cancel(self, id: str) -> None: ... - if sys.version_info >= (3, 13): - def after_info(self, id: str | None = None) -> tuple[str, ...]: ... - - def bell(self, displayof: Literal[0] | Misc | None = 0) -> None: ... - if sys.version_info >= (3, 13): - # Supports options from `_BusyInfo`` - def tk_busy_cget(self, option: Literal["cursor"]) -> _Cursor: ... - busy_cget = tk_busy_cget - def tk_busy_configure(self, cnf: Any = None, **kw: Any) -> Any: ... - tk_busy_config = tk_busy_configure - busy_configure = tk_busy_configure - busy_config = tk_busy_configure - def tk_busy_current(self, pattern: str | None = None) -> list[Misc]: ... - busy_current = tk_busy_current - def tk_busy_forget(self) -> None: ... - busy_forget = tk_busy_forget - def tk_busy_hold(self, **kw: Unpack[_BusyInfo]) -> None: ... - tk_busy = tk_busy_hold - busy_hold = tk_busy_hold - busy = tk_busy_hold - def tk_busy_status(self) -> bool: ... - busy_status = tk_busy_status - - def clipboard_get(self, *, displayof: Misc = ..., type: str = ...) -> str: ... - def clipboard_clear(self, *, displayof: Misc = ...) -> None: ... - def clipboard_append(self, string: str, *, displayof: Misc = ..., format: str = ..., type: str = ...) -> None: ... - def grab_current(self): ... - def grab_release(self) -> None: ... - def grab_set(self) -> None: ... - def grab_set_global(self) -> None: ... - def grab_status(self) -> Literal["local", "global"] | None: ... - def option_add( - self, pattern, value, priority: int | Literal["widgetDefault", "startupFile", "userDefault", "interactive"] | None = None - ) -> None: ... - def option_clear(self) -> None: ... - def option_get(self, name, className): ... - def option_readfile(self, fileName, priority: Incomplete | None = None) -> None: ... - def selection_clear(self, **kw) -> None: ... - def selection_get(self, **kw): ... - def selection_handle(self, command, **kw) -> None: ... - def selection_own(self, **kw) -> None: ... - def selection_own_get(self, **kw): ... - def send(self, interp, cmd, *args): ... - def lower(self, belowThis: Incomplete | None = None) -> None: ... - def tkraise(self, aboveThis: Incomplete | None = None) -> None: ... - lift = tkraise - if sys.version_info >= (3, 11): - def info_patchlevel(self) -> _VersionInfoType: ... - - def winfo_atom(self, name: str, displayof: Literal[0] | Misc | None = 0) -> int: ... - def winfo_atomname(self, id: int, displayof: Literal[0] | Misc | None = 0) -> str: ... - def winfo_cells(self) -> int: ... - def winfo_children(self) -> list[Widget]: ... # Widget because it can't be Toplevel or Tk - def winfo_class(self) -> str: ... - def winfo_colormapfull(self) -> bool: ... - def winfo_containing(self, rootX: int, rootY: int, displayof: Literal[0] | Misc | None = 0) -> Misc | None: ... - def winfo_depth(self) -> int: ... - def winfo_exists(self) -> bool: ... - def winfo_fpixels(self, number: _ScreenUnits) -> float: ... - def winfo_geometry(self) -> str: ... - def winfo_height(self) -> int: ... - def winfo_id(self) -> int: ... - def winfo_interps(self, displayof: Literal[0] | Misc | None = 0) -> tuple[str, ...]: ... - def winfo_ismapped(self) -> bool: ... - def winfo_manager(self) -> str: ... - def winfo_name(self) -> str: ... - def winfo_parent(self) -> str: ... # return value needs nametowidget() - def winfo_pathname(self, id: int, displayof: Literal[0] | Misc | None = 0): ... - def winfo_pixels(self, number: _ScreenUnits) -> int: ... - def winfo_pointerx(self) -> int: ... - def winfo_pointerxy(self) -> tuple[int, int]: ... - def winfo_pointery(self) -> int: ... - def winfo_reqheight(self) -> int: ... - def winfo_reqwidth(self) -> int: ... - def winfo_rgb(self, color: str) -> tuple[int, int, int]: ... - def winfo_rootx(self) -> int: ... - def winfo_rooty(self) -> int: ... - def winfo_screen(self) -> str: ... - def winfo_screencells(self) -> int: ... - def winfo_screendepth(self) -> int: ... - def winfo_screenheight(self) -> int: ... - def winfo_screenmmheight(self) -> int: ... - def winfo_screenmmwidth(self) -> int: ... - def winfo_screenvisual(self) -> str: ... - def winfo_screenwidth(self) -> int: ... - def winfo_server(self) -> str: ... - def winfo_toplevel(self) -> Tk | Toplevel: ... - def winfo_viewable(self) -> bool: ... - def winfo_visual(self) -> str: ... - def winfo_visualid(self) -> str: ... - def winfo_visualsavailable(self, includeids: bool = False) -> list[tuple[str, int]]: ... - def winfo_vrootheight(self) -> int: ... - def winfo_vrootwidth(self) -> int: ... - def winfo_vrootx(self) -> int: ... - def winfo_vrooty(self) -> int: ... - def winfo_width(self) -> int: ... - def winfo_x(self) -> int: ... - def winfo_y(self) -> int: ... - def update(self) -> None: ... - def update_idletasks(self) -> None: ... - @overload - def bindtags(self, tagList: None = None) -> tuple[str, ...]: ... - @overload - def bindtags(self, tagList: list[str] | tuple[str, ...]) -> None: ... - # bind with isinstance(func, str) doesn't return anything, but all other - # binds do. The default value of func is not str. - @overload - def bind( - self, - sequence: str | None = None, - func: Callable[[Event[Misc]], object] | None = None, - add: Literal["", "+"] | bool | None = None, - ) -> str: ... - @overload - def bind(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - @overload - def bind(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - # There's no way to know what type of widget bind_all and bind_class - # callbacks will get, so those are Misc. - @overload - def bind_all( - self, - sequence: str | None = None, - func: Callable[[Event[Misc]], object] | None = None, - add: Literal["", "+"] | bool | None = None, - ) -> str: ... - @overload - def bind_all(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - @overload - def bind_all(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - @overload - def bind_class( - self, - className: str, - sequence: str | None = None, - func: Callable[[Event[Misc]], object] | None = None, - add: Literal["", "+"] | bool | None = None, - ) -> str: ... - @overload - def bind_class(self, className: str, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - @overload - def bind_class(self, className: str, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - def unbind(self, sequence: str, funcid: str | None = None) -> None: ... - def unbind_all(self, sequence: str) -> None: ... - def unbind_class(self, className: str, sequence: str) -> None: ... - def mainloop(self, n: int = 0) -> None: ... - def quit(self) -> None: ... - @property - def _windowingsystem(self) -> Literal["win32", "aqua", "x11"]: ... - def nametowidget(self, name: str | Misc | _tkinter.Tcl_Obj) -> Any: ... - def register( - self, func: Callable[..., object], subst: Callable[..., Sequence[Any]] | None = None, needcleanup: int = 1 - ) -> str: ... - def keys(self) -> list[str]: ... - @overload - def pack_propagate(self, flag: bool) -> bool | None: ... - @overload - def pack_propagate(self) -> None: ... - propagate = pack_propagate - def grid_anchor(self, anchor: _Anchor | None = None) -> None: ... - anchor = grid_anchor - @overload - def grid_bbox( - self, column: None = None, row: None = None, col2: None = None, row2: None = None - ) -> tuple[int, int, int, int] | None: ... - @overload - def grid_bbox(self, column: int, row: int, col2: None = None, row2: None = None) -> tuple[int, int, int, int] | None: ... - @overload - def grid_bbox(self, column: int, row: int, col2: int, row2: int) -> tuple[int, int, int, int] | None: ... - bbox = grid_bbox - def grid_columnconfigure( - self, - index: int | str | list[int] | tuple[int, ...], - cnf: _GridIndexInfo = {}, - *, - minsize: _ScreenUnits = ..., - pad: _ScreenUnits = ..., - uniform: str = ..., - weight: int = ..., - ) -> _GridIndexInfo | MaybeNone: ... # can be None but annoying to check - def grid_rowconfigure( - self, - index: int | str | list[int] | tuple[int, ...], - cnf: _GridIndexInfo = {}, - *, - minsize: _ScreenUnits = ..., - pad: _ScreenUnits = ..., - uniform: str = ..., - weight: int = ..., - ) -> _GridIndexInfo | MaybeNone: ... # can be None but annoying to check - columnconfigure = grid_columnconfigure - rowconfigure = grid_rowconfigure - def grid_location(self, x: _ScreenUnits, y: _ScreenUnits) -> tuple[int, int]: ... - @overload - def grid_propagate(self, flag: bool) -> None: ... - @overload - def grid_propagate(self) -> bool: ... - def grid_size(self) -> tuple[int, int]: ... - size = grid_size - # Widget because Toplevel or Tk is never a slave - def pack_slaves(self) -> list[Widget]: ... - def grid_slaves(self, row: int | None = None, column: int | None = None) -> list[Widget]: ... - def place_slaves(self) -> list[Widget]: ... - slaves = pack_slaves - def event_add(self, virtual: str, *sequences: str) -> None: ... - def event_delete(self, virtual: str, *sequences: str) -> None: ... - def event_generate( - self, - sequence: str, - *, - above: Misc | int = ..., - borderwidth: _ScreenUnits = ..., - button: int = ..., - count: int = ..., - data: Any = ..., # anything with usable str() value - delta: int = ..., - detail: str = ..., - focus: bool = ..., - height: _ScreenUnits = ..., - keycode: int = ..., - keysym: str = ..., - mode: str = ..., - override: bool = ..., - place: Literal["PlaceOnTop", "PlaceOnBottom"] = ..., - root: Misc | int = ..., - rootx: _ScreenUnits = ..., - rooty: _ScreenUnits = ..., - sendevent: bool = ..., - serial: int = ..., - state: int | str = ..., - subwindow: Misc | int = ..., - time: int = ..., - warp: bool = ..., - width: _ScreenUnits = ..., - when: Literal["now", "tail", "head", "mark"] = ..., - x: _ScreenUnits = ..., - y: _ScreenUnits = ..., - ) -> None: ... - def event_info(self, virtual: str | None = None) -> tuple[str, ...]: ... - def image_names(self) -> tuple[str, ...]: ... - def image_types(self) -> tuple[str, ...]: ... - # See #4363 and #4891 - def __setitem__(self, key: str, value: Any) -> None: ... - def __getitem__(self, key: str) -> Any: ... - def cget(self, key: str) -> Any: ... - def configure(self, cnf: Any = None) -> Any: ... - # TODO: config is an alias of configure, but adding that here creates - # conflict with the type of config in the subclasses. See #13149 - -class CallWrapper: - func: Incomplete - subst: Incomplete - widget: Incomplete - def __init__(self, func, subst, widget) -> None: ... - def __call__(self, *args): ... - -class XView: - @overload - def xview(self) -> tuple[float, float]: ... - @overload - def xview(self, *args): ... - def xview_moveto(self, fraction: float) -> None: ... - @overload - def xview_scroll(self, number: int, what: Literal["units", "pages"]) -> None: ... - @overload - def xview_scroll(self, number: _ScreenUnits, what: Literal["pixels"]) -> None: ... - -class YView: - @overload - def yview(self) -> tuple[float, float]: ... - @overload - def yview(self, *args): ... - def yview_moveto(self, fraction: float) -> None: ... - @overload - def yview_scroll(self, number: int, what: Literal["units", "pages"]) -> None: ... - @overload - def yview_scroll(self, number: _ScreenUnits, what: Literal["pixels"]) -> None: ... - -if sys.platform == "darwin": - @type_check_only - class _WmAttributes(TypedDict): - alpha: float - fullscreen: bool - modified: bool - notify: bool - titlepath: str - topmost: bool - transparent: bool - type: str # Present, but not actually used on darwin - -elif sys.platform == "win32": - @type_check_only - class _WmAttributes(TypedDict): - alpha: float - transparentcolor: str - disabled: bool - fullscreen: bool - toolwindow: bool - topmost: bool - -else: - # X11 - @type_check_only - class _WmAttributes(TypedDict): - alpha: float - topmost: bool - zoomed: bool - fullscreen: bool - type: str - -class Wm: - @overload - def wm_aspect(self, minNumer: int, minDenom: int, maxNumer: int, maxDenom: int) -> None: ... - @overload - def wm_aspect( - self, minNumer: None = None, minDenom: None = None, maxNumer: None = None, maxDenom: None = None - ) -> tuple[int, int, int, int] | None: ... - aspect = wm_aspect - if sys.version_info >= (3, 13): - @overload - def wm_attributes(self, *, return_python_dict: Literal[False] = False) -> tuple[Any, ...]: ... - @overload - def wm_attributes(self, *, return_python_dict: Literal[True]) -> _WmAttributes: ... - - else: - @overload - def wm_attributes(self) -> tuple[Any, ...]: ... - - @overload - def wm_attributes(self, option: Literal["-alpha"], /) -> float: ... - @overload - def wm_attributes(self, option: Literal["-fullscreen"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["-topmost"], /) -> bool: ... - if sys.platform == "darwin": - @overload - def wm_attributes(self, option: Literal["-modified"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["-notify"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["-titlepath"], /) -> str: ... - @overload - def wm_attributes(self, option: Literal["-transparent"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["-type"], /) -> str: ... - elif sys.platform == "win32": - @overload - def wm_attributes(self, option: Literal["-transparentcolor"], /) -> str: ... - @overload - def wm_attributes(self, option: Literal["-disabled"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["-toolwindow"], /) -> bool: ... - else: - # X11 - @overload - def wm_attributes(self, option: Literal["-zoomed"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["-type"], /) -> str: ... - if sys.version_info >= (3, 13): - @overload - def wm_attributes(self, option: Literal["alpha"], /) -> float: ... - @overload - def wm_attributes(self, option: Literal["fullscreen"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["topmost"], /) -> bool: ... - if sys.platform == "darwin": - @overload - def wm_attributes(self, option: Literal["modified"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["notify"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["titlepath"], /) -> str: ... - @overload - def wm_attributes(self, option: Literal["transparent"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["type"], /) -> str: ... - elif sys.platform == "win32": - @overload - def wm_attributes(self, option: Literal["transparentcolor"], /) -> str: ... - @overload - def wm_attributes(self, option: Literal["disabled"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["toolwindow"], /) -> bool: ... - else: - # X11 - @overload - def wm_attributes(self, option: Literal["zoomed"], /) -> bool: ... - @overload - def wm_attributes(self, option: Literal["type"], /) -> str: ... - - @overload - def wm_attributes(self, option: str, /): ... - @overload - def wm_attributes(self, option: Literal["-alpha"], value: float, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-fullscreen"], value: bool, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-topmost"], value: bool, /) -> Literal[""]: ... - if sys.platform == "darwin": - @overload - def wm_attributes(self, option: Literal["-modified"], value: bool, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-notify"], value: bool, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-titlepath"], value: str, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-transparent"], value: bool, /) -> Literal[""]: ... - elif sys.platform == "win32": - @overload - def wm_attributes(self, option: Literal["-transparentcolor"], value: str, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-disabled"], value: bool, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-toolwindow"], value: bool, /) -> Literal[""]: ... - else: - # X11 - @overload - def wm_attributes(self, option: Literal["-zoomed"], value: bool, /) -> Literal[""]: ... - @overload - def wm_attributes(self, option: Literal["-type"], value: str, /) -> Literal[""]: ... - - @overload - def wm_attributes(self, option: str, value, /, *__other_option_value_pairs: Any) -> Literal[""]: ... - if sys.version_info >= (3, 13): - if sys.platform == "darwin": - @overload - def wm_attributes( - self, - *, - alpha: float = ..., - fullscreen: bool = ..., - modified: bool = ..., - notify: bool = ..., - titlepath: str = ..., - topmost: bool = ..., - transparent: bool = ..., - ) -> None: ... - elif sys.platform == "win32": - @overload - def wm_attributes( - self, - *, - alpha: float = ..., - transparentcolor: str = ..., - disabled: bool = ..., - fullscreen: bool = ..., - toolwindow: bool = ..., - topmost: bool = ..., - ) -> None: ... - else: - # X11 - @overload - def wm_attributes( - self, *, alpha: float = ..., topmost: bool = ..., zoomed: bool = ..., fullscreen: bool = ..., type: str = ... - ) -> None: ... - - attributes = wm_attributes - def wm_client(self, name: str | None = None) -> str: ... - client = wm_client - @overload - def wm_colormapwindows(self) -> list[Misc]: ... - @overload - def wm_colormapwindows(self, wlist: list[Misc] | tuple[Misc, ...], /) -> None: ... - @overload - def wm_colormapwindows(self, first_wlist_item: Misc, /, *other_wlist_items: Misc) -> None: ... - colormapwindows = wm_colormapwindows - def wm_command(self, value: str | None = None) -> str: ... - command = wm_command - # Some of these always return empty string, but return type is set to None to prevent accidentally using it - def wm_deiconify(self) -> None: ... - deiconify = wm_deiconify - def wm_focusmodel(self, model: Literal["active", "passive"] | None = None) -> Literal["active", "passive", ""]: ... - focusmodel = wm_focusmodel - def wm_forget(self, window: Wm) -> None: ... - forget = wm_forget - def wm_frame(self) -> str: ... - frame = wm_frame - @overload - def wm_geometry(self, newGeometry: None = None) -> str: ... - @overload - def wm_geometry(self, newGeometry: str) -> None: ... - geometry = wm_geometry - def wm_grid( - self, - baseWidth: Incomplete | None = None, - baseHeight: Incomplete | None = None, - widthInc: Incomplete | None = None, - heightInc: Incomplete | None = None, - ): ... - grid = wm_grid - def wm_group(self, pathName: Incomplete | None = None): ... - group = wm_group - def wm_iconbitmap(self, bitmap: Incomplete | None = None, default: Incomplete | None = None): ... - iconbitmap = wm_iconbitmap - def wm_iconify(self) -> None: ... - iconify = wm_iconify - def wm_iconmask(self, bitmap: Incomplete | None = None): ... - iconmask = wm_iconmask - def wm_iconname(self, newName: Incomplete | None = None) -> str: ... - iconname = wm_iconname - def wm_iconphoto(self, default: bool, image1: _PhotoImageLike | str, /, *args: _PhotoImageLike | str) -> None: ... - iconphoto = wm_iconphoto - def wm_iconposition(self, x: int | None = None, y: int | None = None) -> tuple[int, int] | None: ... - iconposition = wm_iconposition - def wm_iconwindow(self, pathName: Incomplete | None = None): ... - iconwindow = wm_iconwindow - def wm_manage(self, widget) -> None: ... - manage = wm_manage - @overload - def wm_maxsize(self, width: None = None, height: None = None) -> tuple[int, int]: ... - @overload - def wm_maxsize(self, width: int, height: int) -> None: ... - maxsize = wm_maxsize - @overload - def wm_minsize(self, width: None = None, height: None = None) -> tuple[int, int]: ... - @overload - def wm_minsize(self, width: int, height: int) -> None: ... - minsize = wm_minsize - @overload - def wm_overrideredirect(self, boolean: None = None) -> bool | None: ... # returns True or None - @overload - def wm_overrideredirect(self, boolean: bool) -> None: ... - overrideredirect = wm_overrideredirect - def wm_positionfrom(self, who: Literal["program", "user"] | None = None) -> Literal["", "program", "user"]: ... - positionfrom = wm_positionfrom - @overload - def wm_protocol(self, name: str, func: Callable[[], object] | str) -> None: ... - @overload - def wm_protocol(self, name: str, func: None = None) -> str: ... - @overload - def wm_protocol(self, name: None = None, func: None = None) -> tuple[str, ...]: ... - protocol = wm_protocol - @overload - def wm_resizable(self, width: None = None, height: None = None) -> tuple[bool, bool]: ... - @overload - def wm_resizable(self, width: bool, height: bool) -> None: ... - resizable = wm_resizable - def wm_sizefrom(self, who: Literal["program", "user"] | None = None) -> Literal["", "program", "user"]: ... - sizefrom = wm_sizefrom - @overload - def wm_state(self, newstate: None = None) -> str: ... - @overload - def wm_state(self, newstate: str) -> None: ... - state = wm_state - @overload - def wm_title(self, string: None = None) -> str: ... - @overload - def wm_title(self, string: str) -> None: ... - title = wm_title - @overload - def wm_transient(self, master: None = None) -> _tkinter.Tcl_Obj: ... - @overload - def wm_transient(self, master: Wm | _tkinter.Tcl_Obj) -> None: ... - transient = wm_transient - def wm_withdraw(self) -> None: ... - withdraw = wm_withdraw - -class Tk(Misc, Wm): - master: None - def __init__( - # Make sure to keep in sync with other functions that use the same - # args. - # use `git grep screenName` to find them - self, - screenName: str | None = None, - baseName: str | None = None, - className: str = "Tk", - useTk: bool = True, - sync: bool = False, - use: str | None = None, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - menu: Menu = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - takefocus: _TakeFocusValue = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def destroy(self) -> None: ... - def readprofile(self, baseName: str, className: str) -> None: ... - report_callback_exception: Callable[[type[BaseException], BaseException, TracebackType | None], object] - # Tk has __getattr__ so that tk_instance.foo falls back to tk_instance.tk.foo - # Please keep in sync with _tkinter.TkappType. - # Some methods are intentionally missing because they are inherited from Misc instead. - def adderrorinfo(self, msg, /): ... - def call(self, command: Any, /, *args: Any) -> Any: ... - def createcommand(self, name, func, /): ... - if sys.platform != "win32": - def createfilehandler(self, file, mask, func, /): ... - def deletefilehandler(self, file, /): ... - - def createtimerhandler(self, milliseconds, func, /): ... - def dooneevent(self, flags: int = ..., /): ... - def eval(self, script: str, /) -> str: ... - def evalfile(self, fileName, /): ... - def exprboolean(self, s, /): ... - def exprdouble(self, s, /): ... - def exprlong(self, s, /): ... - def exprstring(self, s, /): ... - def globalgetvar(self, *args, **kwargs): ... - def globalsetvar(self, *args, **kwargs): ... - def globalunsetvar(self, *args, **kwargs): ... - def interpaddr(self): ... - def loadtk(self) -> None: ... - def record(self, script, /): ... - if sys.version_info < (3, 11): - def split(self, arg, /): ... - - def splitlist(self, arg, /): ... - def unsetvar(self, *args, **kwargs): ... - def wantobjects(self, *args, **kwargs): ... - def willdispatch(self): ... - -def Tcl(screenName: str | None = None, baseName: str | None = None, className: str = "Tk", useTk: bool = False) -> Tk: ... - -_InMiscTotal = TypedDict("_InMiscTotal", {"in": Misc}) -_InMiscNonTotal = TypedDict("_InMiscNonTotal", {"in": Misc}, total=False) - -class _PackInfo(_InMiscTotal): - # 'before' and 'after' never appear in _PackInfo - anchor: _Anchor - expand: bool - fill: Literal["none", "x", "y", "both"] - side: Literal["left", "right", "top", "bottom"] - # Paddings come out as int or tuple of int, even though any _ScreenUnits - # can be specified in pack(). - ipadx: int - ipady: int - padx: int | tuple[int, int] - pady: int | tuple[int, int] - -class Pack: - # _PackInfo is not the valid type for cnf because pad stuff accepts any - # _ScreenUnits instead of int only. I didn't bother to create another - # TypedDict for cnf because it appears to be a legacy thing that was - # replaced by **kwargs. - def pack_configure( - self, - cnf: Mapping[str, Any] | None = {}, - *, - after: Misc = ..., - anchor: _Anchor = ..., - before: Misc = ..., - expand: bool | Literal[0, 1] = 0, - fill: Literal["none", "x", "y", "both"] = ..., - side: Literal["left", "right", "top", "bottom"] = ..., - ipadx: _ScreenUnits = ..., - ipady: _ScreenUnits = ..., - padx: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., - pady: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., - in_: Misc = ..., - **kw: Any, # allow keyword argument named 'in', see #4836 - ) -> None: ... - def pack_forget(self) -> None: ... - def pack_info(self) -> _PackInfo: ... # errors if widget hasn't been packed - pack = pack_configure - forget = pack_forget - propagate = Misc.pack_propagate - -class _PlaceInfo(_InMiscNonTotal): # empty dict if widget hasn't been placed - anchor: _Anchor - bordermode: Literal["inside", "outside", "ignore"] - width: str # can be int()ed (even after e.g. widget.place(height='2.3c') or similar) - height: str # can be int()ed - x: str # can be int()ed - y: str # can be int()ed - relheight: str # can be float()ed if not empty string - relwidth: str # can be float()ed if not empty string - relx: str # can be float()ed if not empty string - rely: str # can be float()ed if not empty string - -class Place: - def place_configure( - self, - cnf: Mapping[str, Any] | None = {}, - *, - anchor: _Anchor = ..., - bordermode: Literal["inside", "outside", "ignore"] = ..., - width: _ScreenUnits = ..., - height: _ScreenUnits = ..., - x: _ScreenUnits = ..., - y: _ScreenUnits = ..., - # str allowed for compatibility with place_info() - relheight: str | float = ..., - relwidth: str | float = ..., - relx: str | float = ..., - rely: str | float = ..., - in_: Misc = ..., - **kw: Any, # allow keyword argument named 'in', see #4836 - ) -> None: ... - def place_forget(self) -> None: ... - def place_info(self) -> _PlaceInfo: ... - place = place_configure - info = place_info - -class _GridInfo(_InMiscNonTotal): # empty dict if widget hasn't been gridded - column: int - columnspan: int - row: int - rowspan: int - ipadx: int - ipady: int - padx: int | tuple[int, int] - pady: int | tuple[int, int] - sticky: str # consists of letters 'n', 's', 'w', 'e', no repeats, may be empty - -class Grid: - def grid_configure( - self, - cnf: Mapping[str, Any] | None = {}, - *, - column: int = ..., - columnspan: int = ..., - row: int = ..., - rowspan: int = ..., - ipadx: _ScreenUnits = ..., - ipady: _ScreenUnits = ..., - padx: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., - pady: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., - sticky: str = ..., # consists of letters 'n', 's', 'w', 'e', may contain repeats, may be empty - in_: Misc = ..., - **kw: Any, # allow keyword argument named 'in', see #4836 - ) -> None: ... - def grid_forget(self) -> None: ... - def grid_remove(self) -> None: ... - def grid_info(self) -> _GridInfo: ... - grid = grid_configure - location = Misc.grid_location - size = Misc.grid_size - -class BaseWidget(Misc): - master: Misc - widgetName: Incomplete - def __init__(self, master, widgetName, cnf={}, kw={}, extra=()) -> None: ... - def destroy(self) -> None: ... - -# This class represents any widget except Toplevel or Tk. -class Widget(BaseWidget, Pack, Place, Grid): - # Allow bind callbacks to take e.g. Event[Label] instead of Event[Misc]. - # Tk and Toplevel get notified for their child widgets' events, but other - # widgets don't. - @overload - def bind( - self: _W, - sequence: str | None = None, - func: Callable[[Event[_W]], object] | None = None, - add: Literal["", "+"] | bool | None = None, - ) -> str: ... - @overload - def bind(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - @overload - def bind(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - -class Toplevel(BaseWidget, Wm): - # Toplevel and Tk have the same options because they correspond to the same - # Tcl/Tk toplevel widget. For some reason, config and configure must be - # copy/pasted here instead of aliasing as 'config = Tk.config'. - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - background: str = ..., - bd: _ScreenUnits = 0, - bg: str = ..., - border: _ScreenUnits = 0, - borderwidth: _ScreenUnits = 0, - class_: str = "Toplevel", - colormap: Literal["new", ""] | Misc = "", - container: bool = False, - cursor: _Cursor = "", - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - menu: Menu = ..., - name: str = ..., - padx: _ScreenUnits = 0, - pady: _ScreenUnits = 0, - relief: _Relief = "flat", - screen: str = "", # can't be changed after creating widget - takefocus: _TakeFocusValue = 0, - use: int = ..., - visual: str | tuple[str, int] = "", - width: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - menu: Menu = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - takefocus: _TakeFocusValue = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - -class Button(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = "center", - background: str = ..., - bd: _ScreenUnits = ..., # same as borderwidth - bg: str = ..., # same as background - bitmap: str = "", - border: _ScreenUnits = ..., # same as borderwidth - borderwidth: _ScreenUnits = ..., - command: _ButtonCommand = "", - compound: _Compound = "none", - cursor: _Cursor = "", - default: Literal["normal", "active", "disabled"] = "disabled", - disabledforeground: str = ..., - fg: str = ..., # same as foreground - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - # width and height must be int for buttons containing just text, but - # ints are also valid _ScreenUnits - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 1, - image: _ImageSpec = "", - justify: Literal["left", "center", "right"] = "center", - name: str = ..., - overrelief: _Relief | Literal[""] = "", - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - repeatdelay: int = ..., - repeatinterval: int = ..., - state: Literal["normal", "active", "disabled"] = "normal", - takefocus: _TakeFocusValue = "", - text: float | str = "", - # We allow the textvariable to be any Variable, not necessarily - # StringVar. This is useful for e.g. a button that displays the value - # of an IntVar. - textvariable: Variable = ..., - underline: int = -1, - width: _ScreenUnits = 0, - wraplength: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: _ButtonCommand = ..., - compound: _Compound = ..., - cursor: _Cursor = ..., - default: Literal["normal", "active", "disabled"] = ..., - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - image: _ImageSpec = ..., - justify: Literal["left", "center", "right"] = ..., - overrelief: _Relief | Literal[""] = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - repeatdelay: int = ..., - repeatinterval: int = ..., - state: Literal["normal", "active", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - textvariable: Variable = ..., - underline: int = ..., - width: _ScreenUnits = ..., - wraplength: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def flash(self) -> None: ... - def invoke(self) -> Any: ... - -class Canvas(Widget, XView, YView): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - background: str = ..., - bd: _ScreenUnits = 0, - bg: str = ..., - border: _ScreenUnits = 0, - borderwidth: _ScreenUnits = 0, - closeenough: float = 1.0, - confine: bool = True, - cursor: _Cursor = "", - # canvas manual page has a section named COORDINATES, and the first - # part of it describes _ScreenUnits. - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = 0, - insertofftime: int = 300, - insertontime: int = 600, - insertwidth: _ScreenUnits = 2, - name: str = ..., - offset=..., # undocumented - relief: _Relief = "flat", - # Setting scrollregion to None doesn't reset it back to empty, - # but setting it to () does. - scrollregion: tuple[_ScreenUnits, _ScreenUnits, _ScreenUnits, _ScreenUnits] | tuple[()] = (), - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = 1, - selectforeground: str = ..., - # man page says that state can be 'hidden', but it can't - state: Literal["normal", "disabled"] = "normal", - takefocus: _TakeFocusValue = "", - width: _ScreenUnits = ..., - xscrollcommand: _XYScrollCommand = "", - xscrollincrement: _ScreenUnits = 0, - yscrollcommand: _XYScrollCommand = "", - yscrollincrement: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - closeenough: float = ..., - confine: bool = ..., - cursor: _Cursor = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = ..., - insertofftime: int = ..., - insertontime: int = ..., - insertwidth: _ScreenUnits = ..., - offset=..., # undocumented - relief: _Relief = ..., - scrollregion: tuple[_ScreenUnits, _ScreenUnits, _ScreenUnits, _ScreenUnits] | tuple[()] = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - state: Literal["normal", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - width: _ScreenUnits = ..., - xscrollcommand: _XYScrollCommand = ..., - xscrollincrement: _ScreenUnits = ..., - yscrollcommand: _XYScrollCommand = ..., - yscrollincrement: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def addtag(self, *args): ... # internal method - def addtag_above(self, newtag: str, tagOrId: str | int) -> None: ... - def addtag_all(self, newtag: str) -> None: ... - def addtag_below(self, newtag: str, tagOrId: str | int) -> None: ... - def addtag_closest( - self, newtag: str, x: _ScreenUnits, y: _ScreenUnits, halo: _ScreenUnits | None = None, start: str | int | None = None - ) -> None: ... - def addtag_enclosed(self, newtag: str, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: _ScreenUnits) -> None: ... - def addtag_overlapping(self, newtag: str, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: _ScreenUnits) -> None: ... - def addtag_withtag(self, newtag: str, tagOrId: str | int) -> None: ... - def find(self, *args): ... # internal method - def find_above(self, tagOrId: str | int) -> tuple[int, ...]: ... - def find_all(self) -> tuple[int, ...]: ... - def find_below(self, tagOrId: str | int) -> tuple[int, ...]: ... - def find_closest( - self, x: _ScreenUnits, y: _ScreenUnits, halo: _ScreenUnits | None = None, start: str | int | None = None - ) -> tuple[int, ...]: ... - def find_enclosed(self, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: _ScreenUnits) -> tuple[int, ...]: ... - def find_overlapping(self, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: float) -> tuple[int, ...]: ... - def find_withtag(self, tagOrId: str | int) -> tuple[int, ...]: ... - # Incompatible with Misc.bbox(), tkinter violates LSP - def bbox(self, *args: str | int) -> tuple[int, int, int, int]: ... # type: ignore[override] - @overload - def tag_bind( - self, - tagOrId: str | int, - sequence: str | None = None, - func: Callable[[Event[Canvas]], object] | None = None, - add: Literal["", "+"] | bool | None = None, - ) -> str: ... - @overload - def tag_bind( - self, tagOrId: str | int, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None - ) -> None: ... - @overload - def tag_bind(self, tagOrId: str | int, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - def tag_unbind(self, tagOrId: str | int, sequence: str, funcid: str | None = None) -> None: ... - def canvasx(self, screenx, gridspacing: Incomplete | None = None): ... - def canvasy(self, screeny, gridspacing: Incomplete | None = None): ... - @overload - def coords(self, tagOrId: str | int, /) -> list[float]: ... - @overload - def coords(self, tagOrId: str | int, args: list[int] | list[float] | tuple[float, ...], /) -> None: ... - @overload - def coords(self, tagOrId: str | int, x1: float, y1: float, /, *args: float) -> None: ... - # create_foo() methods accept coords as a list or tuple, or as separate arguments. - # Lists and tuples can be flat as in [1, 2, 3, 4], or nested as in [(1, 2), (3, 4)]. - # Keyword arguments should be the same in all overloads of each method. - def create_arc(self, *args, **kw) -> int: ... - def create_bitmap(self, *args, **kw) -> int: ... - def create_image(self, *args, **kw) -> int: ... - @overload - def create_line( - self, - x0: float, - y0: float, - x1: float, - y1: float, - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - arrow: Literal["first", "last", "both"] = ..., - arrowshape: tuple[float, float, float] = ..., - capstyle: Literal["round", "projecting", "butt"] = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - joinstyle: Literal["round", "bevel", "miter"] = ..., - offset: _ScreenUnits = ..., - smooth: bool = ..., - splinesteps: float = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_line( - self, - xy_pair_0: tuple[float, float], - xy_pair_1: tuple[float, float], - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - arrow: Literal["first", "last", "both"] = ..., - arrowshape: tuple[float, float, float] = ..., - capstyle: Literal["round", "projecting", "butt"] = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - joinstyle: Literal["round", "bevel", "miter"] = ..., - offset: _ScreenUnits = ..., - smooth: bool = ..., - splinesteps: float = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_line( - self, - coords: ( - tuple[float, float, float, float] - | tuple[tuple[float, float], tuple[float, float]] - | list[int] - | list[float] - | list[tuple[int, int]] - | list[tuple[float, float]] - ), - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - arrow: Literal["first", "last", "both"] = ..., - arrowshape: tuple[float, float, float] = ..., - capstyle: Literal["round", "projecting", "butt"] = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - joinstyle: Literal["round", "bevel", "miter"] = ..., - offset: _ScreenUnits = ..., - smooth: bool = ..., - splinesteps: float = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_oval( - self, - x0: float, - y0: float, - x1: float, - y1: float, - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_oval( - self, - xy_pair_0: tuple[float, float], - xy_pair_1: tuple[float, float], - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_oval( - self, - coords: ( - tuple[float, float, float, float] - | tuple[tuple[float, float], tuple[float, float]] - | list[int] - | list[float] - | list[tuple[int, int]] - | list[tuple[float, float]] - ), - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_polygon( - self, - x0: float, - y0: float, - x1: float, - y1: float, - /, - *xy_pairs: float, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - joinstyle: Literal["round", "bevel", "miter"] = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - smooth: bool = ..., - splinesteps: float = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_polygon( - self, - xy_pair_0: tuple[float, float], - xy_pair_1: tuple[float, float], - /, - *xy_pairs: tuple[float, float], - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - joinstyle: Literal["round", "bevel", "miter"] = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - smooth: bool = ..., - splinesteps: float = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_polygon( - self, - coords: ( - tuple[float, ...] - | tuple[tuple[float, float], ...] - | list[int] - | list[float] - | list[tuple[int, int]] - | list[tuple[float, float]] - ), - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - joinstyle: Literal["round", "bevel", "miter"] = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - smooth: bool = ..., - splinesteps: float = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_rectangle( - self, - x0: float, - y0: float, - x1: float, - y1: float, - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_rectangle( - self, - xy_pair_0: tuple[float, float], - xy_pair_1: tuple[float, float], - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_rectangle( - self, - coords: ( - tuple[float, float, float, float] - | tuple[tuple[float, float], tuple[float, float]] - | list[int] - | list[float] - | list[tuple[int, int]] - | list[tuple[float, float]] - ), - /, - *, - activedash: str | int | list[int] | tuple[int, ...] = ..., - activefill: str = ..., - activeoutline: str = ..., - activeoutlinestipple: str = ..., - activestipple: str = ..., - activewidth: _ScreenUnits = ..., - dash: str | int | list[int] | tuple[int, ...] = ..., - dashoffset: _ScreenUnits = ..., - disableddash: str | int | list[int] | tuple[int, ...] = ..., - disabledfill: str = ..., - disabledoutline: str = ..., - disabledoutlinestipple: str = ..., - disabledstipple: str = ..., - disabledwidth: _ScreenUnits = ..., - fill: str = ..., - offset: _ScreenUnits = ..., - outline: str = ..., - outlineoffset: _ScreenUnits = ..., - outlinestipple: str = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_text( - self, - x: float, - y: float, - /, - *, - activefill: str = ..., - activestipple: str = ..., - anchor: _Anchor = ..., - angle: float | str = ..., - disabledfill: str = ..., - disabledstipple: str = ..., - fill: str = ..., - font: _FontDescription = ..., - justify: Literal["left", "center", "right"] = ..., - offset: _ScreenUnits = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - text: float | str = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_text( - self, - coords: tuple[float, float] | list[int] | list[float], - /, - *, - activefill: str = ..., - activestipple: str = ..., - anchor: _Anchor = ..., - angle: float | str = ..., - disabledfill: str = ..., - disabledstipple: str = ..., - fill: str = ..., - font: _FontDescription = ..., - justify: Literal["left", "center", "right"] = ..., - offset: _ScreenUnits = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - stipple: str = ..., - tags: str | list[str] | tuple[str, ...] = ..., - text: float | str = ..., - width: _ScreenUnits = ..., - ) -> int: ... - @overload - def create_window( - self, - x: float, - y: float, - /, - *, - anchor: _Anchor = ..., - height: _ScreenUnits = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - window: Widget = ..., - ) -> int: ... - @overload - def create_window( - self, - coords: tuple[float, float] | list[int] | list[float], - /, - *, - anchor: _Anchor = ..., - height: _ScreenUnits = ..., - state: Literal["normal", "hidden", "disabled"] = ..., - tags: str | list[str] | tuple[str, ...] = ..., - width: _ScreenUnits = ..., - window: Widget = ..., - ) -> int: ... - def dchars(self, *args) -> None: ... - def delete(self, *tagsOrCanvasIds: str | int) -> None: ... - @overload - def dtag(self, tag: str, tag_to_delete: str | None = ..., /) -> None: ... - @overload - def dtag(self, id: int, tag_to_delete: str, /) -> None: ... - def focus(self, *args): ... - def gettags(self, tagOrId: str | int, /) -> tuple[str, ...]: ... - def icursor(self, *args) -> None: ... - def index(self, *args): ... - def insert(self, *args) -> None: ... - def itemcget(self, tagOrId, option): ... - # itemconfigure kwargs depend on item type, which is not known when type checking - def itemconfigure( - self, tagOrId: str | int, cnf: dict[str, Any] | None = None, **kw: Any - ) -> dict[str, tuple[str, str, str, str, str]] | None: ... - itemconfig = itemconfigure - def move(self, *args) -> None: ... - def moveto(self, tagOrId: str | int, x: Literal[""] | float = "", y: Literal[""] | float = "") -> None: ... - def postscript(self, cnf={}, **kw): ... - # tkinter does: - # lower = tag_lower - # lift = tkraise = tag_raise - # - # But mypy doesn't like aliasing here (maybe because Misc defines the same names) - def tag_lower(self, first: str | int, second: str | int | None = ..., /) -> None: ... - def lower(self, first: str | int, second: str | int | None = ..., /) -> None: ... # type: ignore[override] - def tag_raise(self, first: str | int, second: str | int | None = ..., /) -> None: ... - def tkraise(self, first: str | int, second: str | int | None = ..., /) -> None: ... # type: ignore[override] - def lift(self, first: str | int, second: str | int | None = ..., /) -> None: ... # type: ignore[override] - def scale( - self, tagOrId: str | int, xOrigin: _ScreenUnits, yOrigin: _ScreenUnits, xScale: float, yScale: float, / - ) -> None: ... - def scan_mark(self, x, y) -> None: ... - def scan_dragto(self, x, y, gain: int = 10) -> None: ... - def select_adjust(self, tagOrId, index) -> None: ... - def select_clear(self) -> None: ... - def select_from(self, tagOrId, index) -> None: ... - def select_item(self): ... - def select_to(self, tagOrId, index) -> None: ... - def type(self, tagOrId: str | int) -> int | None: ... - -class Checkbutton(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = "center", - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = "", - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: _ButtonCommand = "", - compound: _Compound = "none", - cursor: _Cursor = "", - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 1, - image: _ImageSpec = "", - indicatoron: bool = True, - justify: Literal["left", "center", "right"] = "center", - name: str = ..., - offrelief: _Relief = ..., - # The checkbutton puts a value to its variable when it's checked or - # unchecked. We don't restrict the type of that value here, so - # Any-typing is fine. - # - # I think Checkbutton shouldn't be generic, because then specifying - # "any checkbutton regardless of what variable it uses" would be - # difficult, and we might run into issues just like how list[float] - # and list[int] are incompatible. Also, we would need a way to - # specify "Checkbutton not associated with any variable", which is - # done by setting variable to empty string (the default). - offvalue: Any = 0, - onvalue: Any = 1, - overrelief: _Relief | Literal[""] = "", - padx: _ScreenUnits = 1, - pady: _ScreenUnits = 1, - relief: _Relief = "flat", - selectcolor: str = ..., - selectimage: _ImageSpec = "", - state: Literal["normal", "active", "disabled"] = "normal", - takefocus: _TakeFocusValue = "", - text: float | str = "", - textvariable: Variable = ..., - tristateimage: _ImageSpec = "", - tristatevalue: Any = "", - underline: int = -1, - variable: Variable | Literal[""] = ..., - width: _ScreenUnits = 0, - wraplength: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: _ButtonCommand = ..., - compound: _Compound = ..., - cursor: _Cursor = ..., - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - justify: Literal["left", "center", "right"] = ..., - offrelief: _Relief = ..., - offvalue: Any = ..., - onvalue: Any = ..., - overrelief: _Relief | Literal[""] = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - selectcolor: str = ..., - selectimage: _ImageSpec = ..., - state: Literal["normal", "active", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - textvariable: Variable = ..., - tristateimage: _ImageSpec = ..., - tristatevalue: Any = ..., - underline: int = ..., - variable: Variable | Literal[""] = ..., - width: _ScreenUnits = ..., - wraplength: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def deselect(self) -> None: ... - def flash(self) -> None: ... - def invoke(self) -> Any: ... - def select(self) -> None: ... - def toggle(self) -> None: ... - -class Entry(Widget, XView): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = "xterm", - disabledbackground: str = ..., - disabledforeground: str = ..., - exportselection: bool = True, - fg: str = ..., - font: _FontDescription = "TkTextFont", - foreground: str = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = 0, - insertofftime: int = 300, - insertontime: int = 600, - insertwidth: _ScreenUnits = ..., - invalidcommand: _EntryValidateCommand = "", - invcmd: _EntryValidateCommand = "", # same as invalidcommand - justify: Literal["left", "center", "right"] = "left", - name: str = ..., - readonlybackground: str = ..., - relief: _Relief = "sunken", - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - show: str = "", - state: Literal["normal", "disabled", "readonly"] = "normal", - takefocus: _TakeFocusValue = "", - textvariable: Variable = ..., - validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = "none", - validatecommand: _EntryValidateCommand = "", - vcmd: _EntryValidateCommand = "", # same as validatecommand - width: int = 20, - xscrollcommand: _XYScrollCommand = "", - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - disabledbackground: str = ..., - disabledforeground: str = ..., - exportselection: bool = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = ..., - insertofftime: int = ..., - insertontime: int = ..., - insertwidth: _ScreenUnits = ..., - invalidcommand: _EntryValidateCommand = ..., - invcmd: _EntryValidateCommand = ..., - justify: Literal["left", "center", "right"] = ..., - readonlybackground: str = ..., - relief: _Relief = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - show: str = ..., - state: Literal["normal", "disabled", "readonly"] = ..., - takefocus: _TakeFocusValue = ..., - textvariable: Variable = ..., - validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = ..., - validatecommand: _EntryValidateCommand = ..., - vcmd: _EntryValidateCommand = ..., - width: int = ..., - xscrollcommand: _XYScrollCommand = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def delete(self, first: str | int, last: str | int | None = None) -> None: ... - def get(self) -> str: ... - def icursor(self, index: str | int) -> None: ... - def index(self, index: str | int) -> int: ... - def insert(self, index: str | int, string: str) -> None: ... - def scan_mark(self, x) -> None: ... - def scan_dragto(self, x) -> None: ... - def selection_adjust(self, index: str | int) -> None: ... - def selection_clear(self) -> None: ... # type: ignore[override] - def selection_from(self, index: str | int) -> None: ... - def selection_present(self) -> bool: ... - def selection_range(self, start: str | int, end: str | int) -> None: ... - def selection_to(self, index: str | int) -> None: ... - select_adjust = selection_adjust - select_clear = selection_clear - select_from = selection_from - select_present = selection_present - select_range = selection_range - select_to = selection_to - -class Frame(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - background: str = ..., - bd: _ScreenUnits = 0, - bg: str = ..., - border: _ScreenUnits = 0, - borderwidth: _ScreenUnits = 0, - class_: str = "Frame", # can't be changed with configure() - colormap: Literal["new", ""] | Misc = "", # can't be changed with configure() - container: bool = False, # can't be changed with configure() - cursor: _Cursor = "", - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - name: str = ..., - padx: _ScreenUnits = 0, - pady: _ScreenUnits = 0, - relief: _Relief = "flat", - takefocus: _TakeFocusValue = 0, - visual: str | tuple[str, int] = "", # can't be changed with configure() - width: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - takefocus: _TakeFocusValue = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - -class Label(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = "center", - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = "", - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - compound: _Compound = "none", - cursor: _Cursor = "", - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - image: _ImageSpec = "", - justify: Literal["left", "center", "right"] = "center", - name: str = ..., - padx: _ScreenUnits = 1, - pady: _ScreenUnits = 1, - relief: _Relief = "flat", - state: Literal["normal", "active", "disabled"] = "normal", - takefocus: _TakeFocusValue = 0, - text: float | str = "", - textvariable: Variable = ..., - underline: int = -1, - width: _ScreenUnits = 0, - wraplength: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - compound: _Compound = ..., - cursor: _Cursor = ..., - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - image: _ImageSpec = ..., - justify: Literal["left", "center", "right"] = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - state: Literal["normal", "active", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - textvariable: Variable = ..., - underline: int = ..., - width: _ScreenUnits = ..., - wraplength: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - -class Listbox(Widget, XView, YView): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activestyle: Literal["dotbox", "none", "underline"] = ..., - background: str = ..., - bd: _ScreenUnits = 1, - bg: str = ..., - border: _ScreenUnits = 1, - borderwidth: _ScreenUnits = 1, - cursor: _Cursor = "", - disabledforeground: str = ..., - exportselection: bool | Literal[0, 1] = 1, - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: int = 10, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - justify: Literal["left", "center", "right"] = "left", - # There's no tkinter.ListVar, but seems like bare tkinter.Variable - # actually works for this: - # - # >>> import tkinter - # >>> lb = tkinter.Listbox() - # >>> var = lb['listvariable'] = tkinter.Variable() - # >>> var.set(['foo', 'bar', 'baz']) - # >>> lb.get(0, 'end') - # ('foo', 'bar', 'baz') - listvariable: Variable = ..., - name: str = ..., - relief: _Relief = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = 0, - selectforeground: str = ..., - # from listbox man page: "The value of the [selectmode] option may be - # arbitrary, but the default bindings expect it to be either single, - # browse, multiple, or extended" - # - # I have never seen anyone setting this to something else than what - # "the default bindings expect", but let's support it anyway. - selectmode: str | Literal["single", "browse", "multiple", "extended"] = "browse", # noqa: Y051 - setgrid: bool = False, - state: Literal["normal", "disabled"] = "normal", - takefocus: _TakeFocusValue = "", - width: int = 20, - xscrollcommand: _XYScrollCommand = "", - yscrollcommand: _XYScrollCommand = "", - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activestyle: Literal["dotbox", "none", "underline"] = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - disabledforeground: str = ..., - exportselection: bool = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: int = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - justify: Literal["left", "center", "right"] = ..., - listvariable: Variable = ..., - relief: _Relief = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - selectmode: str | Literal["single", "browse", "multiple", "extended"] = ..., # noqa: Y051 - setgrid: bool = ..., - state: Literal["normal", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - width: int = ..., - xscrollcommand: _XYScrollCommand = ..., - yscrollcommand: _XYScrollCommand = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def activate(self, index: str | int) -> None: ... - def bbox(self, index: str | int) -> tuple[int, int, int, int] | None: ... # type: ignore[override] - def curselection(self): ... - def delete(self, first: str | int, last: str | int | None = None) -> None: ... - def get(self, first: str | int, last: str | int | None = None): ... - def index(self, index: str | int) -> int: ... - def insert(self, index: str | int, *elements: str | float) -> None: ... - def nearest(self, y): ... - def scan_mark(self, x, y) -> None: ... - def scan_dragto(self, x, y) -> None: ... - def see(self, index: str | int) -> None: ... - def selection_anchor(self, index: str | int) -> None: ... - select_anchor = selection_anchor - def selection_clear(self, first: str | int, last: str | int | None = None) -> None: ... # type: ignore[override] - select_clear = selection_clear - def selection_includes(self, index: str | int): ... - select_includes = selection_includes - def selection_set(self, first: str | int, last: str | int | None = None) -> None: ... - select_set = selection_set - def size(self) -> int: ... # type: ignore[override] - def itemcget(self, index: str | int, option): ... - def itemconfigure(self, index: str | int, cnf: Incomplete | None = None, **kw): ... - itemconfig = itemconfigure - -class Menu(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activeborderwidth: _ScreenUnits = ..., - activeforeground: str = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = "arrow", - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - name: str = ..., - postcommand: Callable[[], object] | str = "", - relief: _Relief = ..., - selectcolor: str = ..., - takefocus: _TakeFocusValue = 0, - tearoff: bool | Literal[0, 1] = 1, - # I guess tearoffcommand arguments are supposed to be widget objects, - # but they are widget name strings. Use nametowidget() to handle the - # arguments of tearoffcommand. - tearoffcommand: Callable[[str, str], object] | str = "", - title: str = "", - type: Literal["menubar", "tearoff", "normal"] = "normal", - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activeborderwidth: _ScreenUnits = ..., - activeforeground: str = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - postcommand: Callable[[], object] | str = ..., - relief: _Relief = ..., - selectcolor: str = ..., - takefocus: _TakeFocusValue = ..., - tearoff: bool = ..., - tearoffcommand: Callable[[str, str], object] | str = ..., - title: str = ..., - type: Literal["menubar", "tearoff", "normal"] = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def tk_popup(self, x: int, y: int, entry: str | int = "") -> None: ... - def activate(self, index: str | int) -> None: ... - def add(self, itemType, cnf={}, **kw): ... # docstring says "Internal function." - def insert(self, index, itemType, cnf={}, **kw): ... # docstring says "Internal function." - def add_cascade( - self, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - label: str = ..., - menu: Menu = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - ) -> None: ... - def add_checkbutton( - self, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - label: str = ..., - offvalue: Any = ..., - onvalue: Any = ..., - selectcolor: str = ..., - selectimage: _ImageSpec = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - variable: Variable = ..., - ) -> None: ... - def add_command( - self, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - label: str = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - ) -> None: ... - def add_radiobutton( - self, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - label: str = ..., - selectcolor: str = ..., - selectimage: _ImageSpec = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - value: Any = ..., - variable: Variable = ..., - ) -> None: ... - def add_separator(self, cnf: dict[str, Any] | None = {}, *, background: str = ...) -> None: ... - def insert_cascade( - self, - index: str | int, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - label: str = ..., - menu: Menu = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - ) -> None: ... - def insert_checkbutton( - self, - index: str | int, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - label: str = ..., - offvalue: Any = ..., - onvalue: Any = ..., - selectcolor: str = ..., - selectimage: _ImageSpec = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - variable: Variable = ..., - ) -> None: ... - def insert_command( - self, - index: str | int, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - label: str = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - ) -> None: ... - def insert_radiobutton( - self, - index: str | int, - cnf: dict[str, Any] | None = {}, - *, - accelerator: str = ..., - activebackground: str = ..., - activeforeground: str = ..., - background: str = ..., - bitmap: str = ..., - columnbreak: int = ..., - command: Callable[[], object] | str = ..., - compound: _Compound = ..., - font: _FontDescription = ..., - foreground: str = ..., - hidemargin: bool = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - label: str = ..., - selectcolor: str = ..., - selectimage: _ImageSpec = ..., - state: Literal["normal", "active", "disabled"] = ..., - underline: int = ..., - value: Any = ..., - variable: Variable = ..., - ) -> None: ... - def insert_separator(self, index: str | int, cnf: dict[str, Any] | None = {}, *, background: str = ...) -> None: ... - def delete(self, index1: str | int, index2: str | int | None = None) -> None: ... - def entrycget(self, index: str | int, option: str) -> Any: ... - def entryconfigure( - self, index: str | int, cnf: dict[str, Any] | None = None, **kw: Any - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - entryconfig = entryconfigure - def index(self, index: str | int) -> int | None: ... - def invoke(self, index: str | int) -> Any: ... - def post(self, x: int, y: int) -> None: ... - def type(self, index: str | int) -> Literal["cascade", "checkbutton", "command", "radiobutton", "separator"]: ... - def unpost(self) -> None: ... - def xposition(self, index: str | int) -> int: ... - def yposition(self, index: str | int) -> int: ... - -class Menubutton(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = "", - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - compound: _Compound = "none", - cursor: _Cursor = "", - direction: Literal["above", "below", "left", "right", "flush"] = "below", - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - image: _ImageSpec = "", - indicatoron: bool = ..., - justify: Literal["left", "center", "right"] = ..., - menu: Menu = ..., - name: str = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = "flat", - state: Literal["normal", "active", "disabled"] = "normal", - takefocus: _TakeFocusValue = 0, - text: float | str = "", - textvariable: Variable = ..., - underline: int = -1, - width: _ScreenUnits = 0, - wraplength: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - compound: _Compound = ..., - cursor: _Cursor = ..., - direction: Literal["above", "below", "left", "right", "flush"] = ..., - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - justify: Literal["left", "center", "right"] = ..., - menu: Menu = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - state: Literal["normal", "active", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - textvariable: Variable = ..., - underline: int = ..., - width: _ScreenUnits = ..., - wraplength: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - -class Message(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - anchor: _Anchor = "center", - aspect: int = 150, - background: str = ..., - bd: _ScreenUnits = 1, - bg: str = ..., - border: _ScreenUnits = 1, - borderwidth: _ScreenUnits = 1, - cursor: _Cursor = "", - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - justify: Literal["left", "center", "right"] = "left", - name: str = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = "flat", - takefocus: _TakeFocusValue = 0, - text: float | str = "", - textvariable: Variable = ..., - # there's width but no height - width: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - anchor: _Anchor = ..., - aspect: int = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - justify: Literal["left", "center", "right"] = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - textvariable: Variable = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - -class Radiobutton(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = "center", - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = "", - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: _ButtonCommand = "", - compound: _Compound = "none", - cursor: _Cursor = "", - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 1, - image: _ImageSpec = "", - indicatoron: bool = True, - justify: Literal["left", "center", "right"] = "center", - name: str = ..., - offrelief: _Relief = ..., - overrelief: _Relief | Literal[""] = "", - padx: _ScreenUnits = 1, - pady: _ScreenUnits = 1, - relief: _Relief = "flat", - selectcolor: str = ..., - selectimage: _ImageSpec = "", - state: Literal["normal", "active", "disabled"] = "normal", - takefocus: _TakeFocusValue = "", - text: float | str = "", - textvariable: Variable = ..., - tristateimage: _ImageSpec = "", - tristatevalue: Any = "", - underline: int = -1, - value: Any = "", - variable: Variable | Literal[""] = ..., - width: _ScreenUnits = 0, - wraplength: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activeforeground: str = ..., - anchor: _Anchor = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bitmap: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: _ButtonCommand = ..., - compound: _Compound = ..., - cursor: _Cursor = ..., - disabledforeground: str = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - image: _ImageSpec = ..., - indicatoron: bool = ..., - justify: Literal["left", "center", "right"] = ..., - offrelief: _Relief = ..., - overrelief: _Relief | Literal[""] = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - selectcolor: str = ..., - selectimage: _ImageSpec = ..., - state: Literal["normal", "active", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - textvariable: Variable = ..., - tristateimage: _ImageSpec = ..., - tristatevalue: Any = ..., - underline: int = ..., - value: Any = ..., - variable: Variable | Literal[""] = ..., - width: _ScreenUnits = ..., - wraplength: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def deselect(self) -> None: ... - def flash(self) -> None: ... - def invoke(self) -> Any: ... - def select(self) -> None: ... - -class Scale(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - background: str = ..., - bd: _ScreenUnits = 1, - bg: str = ..., - bigincrement: float = 0.0, - border: _ScreenUnits = 1, - borderwidth: _ScreenUnits = 1, - # don't know why the callback gets string instead of float - command: str | Callable[[str], object] = "", - cursor: _Cursor = "", - digits: int = 0, - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - from_: float = 0.0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - label: str = "", - length: _ScreenUnits = 100, - name: str = ..., - orient: Literal["horizontal", "vertical"] = "vertical", - relief: _Relief = "flat", - repeatdelay: int = 300, - repeatinterval: int = 100, - resolution: float = 1.0, - showvalue: bool = True, - sliderlength: _ScreenUnits = 30, - sliderrelief: _Relief = "raised", - state: Literal["normal", "active", "disabled"] = "normal", - takefocus: _TakeFocusValue = "", - tickinterval: float = 0.0, - to: float = 100.0, - troughcolor: str = ..., - variable: IntVar | DoubleVar = ..., - width: _ScreenUnits = 15, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - bigincrement: float = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: str | Callable[[str], object] = ..., - cursor: _Cursor = ..., - digits: int = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - from_: float = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - label: str = ..., - length: _ScreenUnits = ..., - orient: Literal["horizontal", "vertical"] = ..., - relief: _Relief = ..., - repeatdelay: int = ..., - repeatinterval: int = ..., - resolution: float = ..., - showvalue: bool = ..., - sliderlength: _ScreenUnits = ..., - sliderrelief: _Relief = ..., - state: Literal["normal", "active", "disabled"] = ..., - takefocus: _TakeFocusValue = ..., - tickinterval: float = ..., - to: float = ..., - troughcolor: str = ..., - variable: IntVar | DoubleVar = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def get(self) -> float: ... - def set(self, value) -> None: ... - def coords(self, value: float | None = None) -> tuple[int, int]: ... - def identify(self, x, y) -> Literal["", "slider", "trough1", "trough2"]: ... - -class Scrollbar(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - activerelief: _Relief = "raised", - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - # There are many ways how the command may get called. Search for - # 'SCROLLING COMMANDS' in scrollbar man page. There doesn't seem to - # be any way to specify an overloaded callback function, so we say - # that it can take any args while it can't in reality. - command: Callable[..., tuple[float, float] | None] | str = "", - cursor: _Cursor = "", - elementborderwidth: _ScreenUnits = -1, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - jump: bool = False, - name: str = ..., - orient: Literal["horizontal", "vertical"] = "vertical", - relief: _Relief = ..., - repeatdelay: int = 300, - repeatinterval: int = 100, - takefocus: _TakeFocusValue = "", - troughcolor: str = ..., - width: _ScreenUnits = ..., - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - activerelief: _Relief = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - command: Callable[..., tuple[float, float] | None] | str = ..., - cursor: _Cursor = ..., - elementborderwidth: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - jump: bool = ..., - orient: Literal["horizontal", "vertical"] = ..., - relief: _Relief = ..., - repeatdelay: int = ..., - repeatinterval: int = ..., - takefocus: _TakeFocusValue = ..., - troughcolor: str = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def activate(self, index: Incomplete | None = None): ... - def delta(self, deltax: int, deltay: int) -> float: ... - def fraction(self, x: int, y: int) -> float: ... - def identify(self, x: int, y: int) -> Literal["arrow1", "arrow2", "slider", "trough1", "trough2", ""]: ... - def get(self) -> tuple[float, float, float, float] | tuple[float, float]: ... - def set(self, first: float | str, last: float | str) -> None: ... - -_TextIndex: TypeAlias = _tkinter.Tcl_Obj | str | float | Misc -_WhatToCount: TypeAlias = Literal[ - "chars", "displaychars", "displayindices", "displaylines", "indices", "lines", "xpixels", "ypixels" -] - -class Text(Widget, XView, YView): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - autoseparators: bool = True, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - blockcursor: bool = False, - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = "xterm", - endline: int | Literal[""] = "", - exportselection: bool = True, - fg: str = ..., - font: _FontDescription = "TkFixedFont", - foreground: str = ..., - # width is always int, but height is allowed to be ScreenUnits. - # This doesn't make any sense to me, and this isn't documented. - # The docs seem to say that both should be integers. - height: _ScreenUnits = 24, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - inactiveselectbackground: str = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = 0, - insertofftime: int = 300, - insertontime: int = 600, - insertunfocussed: Literal["none", "hollow", "solid"] = "none", - insertwidth: _ScreenUnits = ..., - maxundo: int = 0, - name: str = ..., - padx: _ScreenUnits = 1, - pady: _ScreenUnits = 1, - relief: _Relief = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - setgrid: bool = False, - spacing1: _ScreenUnits = 0, - spacing2: _ScreenUnits = 0, - spacing3: _ScreenUnits = 0, - startline: int | Literal[""] = "", - state: Literal["normal", "disabled"] = "normal", - # Literal inside Tuple doesn't actually work - tabs: _ScreenUnits | str | tuple[_ScreenUnits | str, ...] = "", - tabstyle: Literal["tabular", "wordprocessor"] = "tabular", - takefocus: _TakeFocusValue = "", - undo: bool = False, - width: int = 80, - wrap: Literal["none", "char", "word"] = "char", - xscrollcommand: _XYScrollCommand = "", - yscrollcommand: _XYScrollCommand = "", - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - autoseparators: bool = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - blockcursor: bool = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - endline: int | Literal[""] = ..., - exportselection: bool = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - inactiveselectbackground: str = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = ..., - insertofftime: int = ..., - insertontime: int = ..., - insertunfocussed: Literal["none", "hollow", "solid"] = ..., - insertwidth: _ScreenUnits = ..., - maxundo: int = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - setgrid: bool = ..., - spacing1: _ScreenUnits = ..., - spacing2: _ScreenUnits = ..., - spacing3: _ScreenUnits = ..., - startline: int | Literal[""] = ..., - state: Literal["normal", "disabled"] = ..., - tabs: _ScreenUnits | str | tuple[_ScreenUnits | str, ...] = ..., - tabstyle: Literal["tabular", "wordprocessor"] = ..., - takefocus: _TakeFocusValue = ..., - undo: bool = ..., - width: int = ..., - wrap: Literal["none", "char", "word"] = ..., - xscrollcommand: _XYScrollCommand = ..., - yscrollcommand: _XYScrollCommand = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def bbox(self, index: _TextIndex) -> tuple[int, int, int, int] | None: ... # type: ignore[override] - def compare(self, index1: _TextIndex, op: Literal["<", "<=", "==", ">=", ">", "!="], index2: _TextIndex) -> bool: ... - if sys.version_info >= (3, 13): - @overload - def count(self, index1: _TextIndex, index2: _TextIndex, *, return_ints: Literal[True]) -> int: ... - @overload - def count( - self, index1: _TextIndex, index2: _TextIndex, arg: _WhatToCount | Literal["update"], /, *, return_ints: Literal[True] - ) -> int: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: Literal["update"], - arg2: _WhatToCount, - /, - *, - return_ints: Literal[True], - ) -> int: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: _WhatToCount, - arg2: Literal["update"], - /, - *, - return_ints: Literal[True], - ) -> int: ... - @overload - def count( - self, index1: _TextIndex, index2: _TextIndex, arg1: _WhatToCount, arg2: _WhatToCount, /, *, return_ints: Literal[True] - ) -> tuple[int, int]: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: _WhatToCount | Literal["update"], - arg2: _WhatToCount | Literal["update"], - arg3: _WhatToCount | Literal["update"], - /, - *args: _WhatToCount | Literal["update"], - return_ints: Literal[True], - ) -> tuple[int, ...]: ... - @overload - def count(self, index1: _TextIndex, index2: _TextIndex, *, return_ints: Literal[False] = False) -> tuple[int] | None: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg: _WhatToCount | Literal["update"], - /, - *, - return_ints: Literal[False] = False, - ) -> tuple[int] | None: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: Literal["update"], - arg2: _WhatToCount, - /, - *, - return_ints: Literal[False] = False, - ) -> int | None: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: _WhatToCount, - arg2: Literal["update"], - /, - *, - return_ints: Literal[False] = False, - ) -> int | None: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: _WhatToCount, - arg2: _WhatToCount, - /, - *, - return_ints: Literal[False] = False, - ) -> tuple[int, int]: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: _WhatToCount | Literal["update"], - arg2: _WhatToCount | Literal["update"], - arg3: _WhatToCount | Literal["update"], - /, - *args: _WhatToCount | Literal["update"], - return_ints: Literal[False] = False, - ) -> tuple[int, ...]: ... - else: - @overload - def count(self, index1: _TextIndex, index2: _TextIndex) -> tuple[int] | None: ... - @overload - def count( - self, index1: _TextIndex, index2: _TextIndex, arg: _WhatToCount | Literal["update"], / - ) -> tuple[int] | None: ... - @overload - def count(self, index1: _TextIndex, index2: _TextIndex, arg1: Literal["update"], arg2: _WhatToCount, /) -> int | None: ... - @overload - def count(self, index1: _TextIndex, index2: _TextIndex, arg1: _WhatToCount, arg2: Literal["update"], /) -> int | None: ... - @overload - def count(self, index1: _TextIndex, index2: _TextIndex, arg1: _WhatToCount, arg2: _WhatToCount, /) -> tuple[int, int]: ... - @overload - def count( - self, - index1: _TextIndex, - index2: _TextIndex, - arg1: _WhatToCount | Literal["update"], - arg2: _WhatToCount | Literal["update"], - arg3: _WhatToCount | Literal["update"], - /, - *args: _WhatToCount | Literal["update"], - ) -> tuple[int, ...]: ... - - @overload - def debug(self, boolean: None = None) -> bool: ... - @overload - def debug(self, boolean: bool) -> None: ... - def delete(self, index1: _TextIndex, index2: _TextIndex | None = None) -> None: ... - def dlineinfo(self, index: _TextIndex) -> tuple[int, int, int, int, int] | None: ... - @overload - def dump( - self, - index1: _TextIndex, - index2: _TextIndex | None = None, - command: None = None, - *, - all: bool = ..., - image: bool = ..., - mark: bool = ..., - tag: bool = ..., - text: bool = ..., - window: bool = ..., - ) -> list[tuple[str, str, str]]: ... - @overload - def dump( - self, - index1: _TextIndex, - index2: _TextIndex | None, - command: Callable[[str, str, str], object] | str, - *, - all: bool = ..., - image: bool = ..., - mark: bool = ..., - tag: bool = ..., - text: bool = ..., - window: bool = ..., - ) -> None: ... - @overload - def dump( - self, - index1: _TextIndex, - index2: _TextIndex | None = None, - *, - command: Callable[[str, str, str], object] | str, - all: bool = ..., - image: bool = ..., - mark: bool = ..., - tag: bool = ..., - text: bool = ..., - window: bool = ..., - ) -> None: ... - def edit(self, *args): ... # docstring says "Internal method" - @overload - def edit_modified(self, arg: None = None) -> bool: ... # actually returns Literal[0, 1] - @overload - def edit_modified(self, arg: bool) -> None: ... # actually returns empty string - def edit_redo(self) -> None: ... # actually returns empty string - def edit_reset(self) -> None: ... # actually returns empty string - def edit_separator(self) -> None: ... # actually returns empty string - def edit_undo(self) -> None: ... # actually returns empty string - def get(self, index1: _TextIndex, index2: _TextIndex | None = None) -> str: ... - @overload - def image_cget(self, index: _TextIndex, option: Literal["image", "name"]) -> str: ... - @overload - def image_cget(self, index: _TextIndex, option: Literal["padx", "pady"]) -> int: ... - @overload - def image_cget(self, index: _TextIndex, option: Literal["align"]) -> Literal["baseline", "bottom", "center", "top"]: ... - @overload - def image_cget(self, index: _TextIndex, option: str) -> Any: ... - @overload - def image_configure(self, index: _TextIndex, cnf: str) -> tuple[str, str, str, str, str | int]: ... - @overload - def image_configure( - self, - index: _TextIndex, - cnf: dict[str, Any] | None = {}, - *, - align: Literal["baseline", "bottom", "center", "top"] = ..., - image: _ImageSpec = ..., - name: str = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, str, str | int]] | None: ... - def image_create( - self, - index: _TextIndex, - cnf: dict[str, Any] | None = {}, - *, - align: Literal["baseline", "bottom", "center", "top"] = ..., - image: _ImageSpec = ..., - name: str = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - ) -> str: ... - def image_names(self) -> tuple[str, ...]: ... - def index(self, index: _TextIndex) -> str: ... - def insert(self, index: _TextIndex, chars: str, *args: str | list[str] | tuple[str, ...]) -> None: ... - @overload - def mark_gravity(self, markName: str, direction: None = None) -> Literal["left", "right"]: ... - @overload - def mark_gravity(self, markName: str, direction: Literal["left", "right"]) -> None: ... # actually returns empty string - def mark_names(self) -> tuple[str, ...]: ... - def mark_set(self, markName: str, index: _TextIndex) -> None: ... - def mark_unset(self, *markNames: str) -> None: ... - def mark_next(self, index: _TextIndex) -> str | None: ... - def mark_previous(self, index: _TextIndex) -> str | None: ... - # **kw of peer_create is same as the kwargs of Text.__init__ - def peer_create(self, newPathName: str | Text, cnf: dict[str, Any] = {}, **kw) -> None: ... - def peer_names(self) -> tuple[_tkinter.Tcl_Obj, ...]: ... - def replace(self, index1: _TextIndex, index2: _TextIndex, chars: str, *args: str | list[str] | tuple[str, ...]) -> None: ... - def scan_mark(self, x: int, y: int) -> None: ... - def scan_dragto(self, x: int, y: int) -> None: ... - def search( - self, - pattern: str, - index: _TextIndex, - stopindex: _TextIndex | None = None, - forwards: bool | None = None, - backwards: bool | None = None, - exact: bool | None = None, - regexp: bool | None = None, - nocase: bool | None = None, - count: Variable | None = None, - elide: bool | None = None, - ) -> str: ... # returns empty string for not found - def see(self, index: _TextIndex) -> None: ... - def tag_add(self, tagName: str, index1: _TextIndex, *args: _TextIndex) -> None: ... - # tag_bind stuff is very similar to Canvas - @overload - def tag_bind( - self, - tagName: str, - sequence: str | None, - func: Callable[[Event[Text]], object] | None, - add: Literal["", "+"] | bool | None = None, - ) -> str: ... - @overload - def tag_bind(self, tagName: str, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... - def tag_unbind(self, tagName: str, sequence: str, funcid: str | None = None) -> None: ... - # allowing any string for cget instead of just Literals because there's no other way to look up tag options - def tag_cget(self, tagName: str, option: str): ... - @overload - def tag_configure( - self, - tagName: str, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bgstipple: str = ..., - borderwidth: _ScreenUnits = ..., - border: _ScreenUnits = ..., # alias for borderwidth - elide: bool = ..., - fgstipple: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - justify: Literal["left", "right", "center"] = ..., - lmargin1: _ScreenUnits = ..., - lmargin2: _ScreenUnits = ..., - lmargincolor: str = ..., - offset: _ScreenUnits = ..., - overstrike: bool = ..., - overstrikefg: str = ..., - relief: _Relief = ..., - rmargin: _ScreenUnits = ..., - rmargincolor: str = ..., - selectbackground: str = ..., - selectforeground: str = ..., - spacing1: _ScreenUnits = ..., - spacing2: _ScreenUnits = ..., - spacing3: _ScreenUnits = ..., - tabs: Any = ..., # the exact type is kind of complicated, see manual page - tabstyle: Literal["tabular", "wordprocessor"] = ..., - underline: bool = ..., - underlinefg: str = ..., - wrap: Literal["none", "char", "word"] = ..., # be careful with "none" vs None - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def tag_configure(self, tagName: str, cnf: str) -> tuple[str, str, str, Any, Any]: ... - tag_config = tag_configure - def tag_delete(self, first_tag_name: str, /, *tagNames: str) -> None: ... # error if no tag names given - def tag_lower(self, tagName: str, belowThis: str | None = None) -> None: ... - def tag_names(self, index: _TextIndex | None = None) -> tuple[str, ...]: ... - def tag_nextrange( - self, tagName: str, index1: _TextIndex, index2: _TextIndex | None = None - ) -> tuple[str, str] | tuple[()]: ... - def tag_prevrange( - self, tagName: str, index1: _TextIndex, index2: _TextIndex | None = None - ) -> tuple[str, str] | tuple[()]: ... - def tag_raise(self, tagName: str, aboveThis: str | None = None) -> None: ... - def tag_ranges(self, tagName: str) -> tuple[_tkinter.Tcl_Obj, ...]: ... - # tag_remove and tag_delete are different - def tag_remove(self, tagName: str, index1: _TextIndex, index2: _TextIndex | None = None) -> None: ... - @overload - def window_cget(self, index: _TextIndex, option: Literal["padx", "pady"]) -> int: ... - @overload - def window_cget(self, index: _TextIndex, option: Literal["stretch"]) -> bool: ... # actually returns Literal[0, 1] - @overload - def window_cget(self, index: _TextIndex, option: Literal["align"]) -> Literal["baseline", "bottom", "center", "top"]: ... - @overload # window is set to a widget, but read as the string name. - def window_cget(self, index: _TextIndex, option: Literal["create", "window"]) -> str: ... - @overload - def window_cget(self, index: _TextIndex, option: str) -> Any: ... - @overload - def window_configure(self, index: _TextIndex, cnf: str) -> tuple[str, str, str, str, str | int]: ... - @overload - def window_configure( - self, - index: _TextIndex, - cnf: dict[str, Any] | None = None, - *, - align: Literal["baseline", "bottom", "center", "top"] = ..., - create: str = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - stretch: bool | Literal[0, 1] = ..., - window: Misc | str = ..., - ) -> dict[str, tuple[str, str, str, str, str | int]] | None: ... - window_config = window_configure - def window_create( - self, - index: _TextIndex, - cnf: dict[str, Any] | None = {}, - *, - align: Literal["baseline", "bottom", "center", "top"] = ..., - create: str = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - stretch: bool | Literal[0, 1] = ..., - window: Misc | str = ..., - ) -> None: ... - def window_names(self) -> tuple[str, ...]: ... - def yview_pickplace(self, *what): ... # deprecated - -class _setit: - def __init__(self, var, value, callback: Incomplete | None = None) -> None: ... - def __call__(self, *args) -> None: ... - -# manual page: tk_optionMenu -class OptionMenu(Menubutton): - widgetName: Incomplete - menuname: Incomplete - def __init__( - # differs from other widgets - self, - master: Misc | None, - variable: StringVar, - value: str, - *values: str, - # kwarg only from now on - command: Callable[[StringVar], object] | None = ..., - ) -> None: ... - # configure, config, cget are inherited from Menubutton - # destroy and __getitem__ are overridden, signature does not change - -# This matches tkinter's image classes (PhotoImage and BitmapImage) -# and PIL's tkinter-compatible class (PIL.ImageTk.PhotoImage), -# but not a plain PIL image that isn't tkinter compatible. -# The reason is that PIL has width and height attributes, not methods. -@type_check_only -class _Image(Protocol): - def width(self) -> int: ... - def height(self) -> int: ... - -@type_check_only -class _BitmapImageLike(_Image): ... - -@type_check_only -class _PhotoImageLike(_Image): ... - -class Image(_Image): - name: Incomplete - tk: _tkinter.TkappType - def __init__( - self, imgtype, name: Incomplete | None = None, cnf={}, master: Misc | _tkinter.TkappType | None = None, **kw - ) -> None: ... - def __del__(self) -> None: ... - def __setitem__(self, key, value) -> None: ... - def __getitem__(self, key): ... - configure: Incomplete - config: Incomplete - def type(self): ... - -class PhotoImage(Image, _PhotoImageLike): - # This should be kept in sync with PIL.ImageTK.PhotoImage.__init__() - def __init__( - self, - name: str | None = None, - cnf: dict[str, Any] = {}, - master: Misc | _tkinter.TkappType | None = None, - *, - data: str | bytes = ..., # not same as data argument of put() - format: str = ..., - file: StrOrBytesPath = ..., - gamma: float = ..., - height: int = ..., - palette: int | str = ..., - width: int = ..., - ) -> None: ... - def configure( - self, - *, - data: str | bytes = ..., - format: str = ..., - file: StrOrBytesPath = ..., - gamma: float = ..., - height: int = ..., - palette: int | str = ..., - width: int = ..., - ) -> None: ... - config = configure - def blank(self) -> None: ... - def cget(self, option: str) -> str: ... - def __getitem__(self, key: str) -> str: ... # always string: image['height'] can be '0' - if sys.version_info >= (3, 13): - def copy( - self, - *, - from_coords: Iterable[int] | None = None, - zoom: int | tuple[int, int] | list[int] | None = None, - subsample: int | tuple[int, int] | list[int] | None = None, - ) -> PhotoImage: ... - def subsample(self, x: int, y: Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... - def zoom(self, x: int, y: Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... - def copy_replace( - self, - sourceImage: PhotoImage | str, - *, - from_coords: Iterable[int] | None = None, - to: Iterable[int] | None = None, - shrink: bool = False, - zoom: int | tuple[int, int] | list[int] | None = None, - subsample: int | tuple[int, int] | list[int] | None = None, - # `None` defaults to overlay. - compositingrule: Literal["overlay", "set"] | None = None, - ) -> None: ... - else: - def copy(self) -> PhotoImage: ... - def zoom(self, x: int, y: int | Literal[""] = "") -> PhotoImage: ... - def subsample(self, x: int, y: int | Literal[""] = "") -> PhotoImage: ... - - def get(self, x: int, y: int) -> tuple[int, int, int]: ... - def put( - self, - data: ( - str - | list[str] - | list[list[str]] - | list[tuple[str, ...]] - | tuple[str, ...] - | tuple[list[str], ...] - | tuple[tuple[str, ...], ...] - ), - to: tuple[int, int] | None = None, - ) -> None: ... - if sys.version_info >= (3, 13): - def read( - self, - filename: StrOrBytesPath, - format: str | None = None, - *, - from_coords: Iterable[int] | None = None, - to: Iterable[int] | None = None, - shrink: bool = False, - ) -> None: ... - def write( - self, - filename: StrOrBytesPath, - format: str | None = None, - from_coords: Iterable[int] | None = None, - *, - background: str | None = None, - grayscale: bool = False, - ) -> None: ... - @overload - def data( - self, format: str, *, from_coords: Iterable[int] | None = None, background: str | None = None, grayscale: bool = False - ) -> bytes: ... - @overload - def data( - self, - format: None = None, - *, - from_coords: Iterable[int] | None = None, - background: str | None = None, - grayscale: bool = False, - ) -> tuple[str, ...]: ... - - else: - def write( - self, filename: StrOrBytesPath, format: str | None = None, from_coords: tuple[int, int] | None = None - ) -> None: ... - - def transparency_get(self, x: int, y: int) -> bool: ... - def transparency_set(self, x: int, y: int, boolean: bool) -> None: ... - -class BitmapImage(Image, _BitmapImageLike): - # This should be kept in sync with PIL.ImageTK.BitmapImage.__init__() - def __init__( - self, - name: Incomplete | None = None, - cnf: dict[str, Any] = {}, - master: Misc | _tkinter.TkappType | None = None, - *, - background: str = ..., - data: str | bytes = ..., - file: StrOrBytesPath = ..., - foreground: str = ..., - maskdata: str = ..., - maskfile: StrOrBytesPath = ..., - ) -> None: ... - -def image_names() -> tuple[str, ...]: ... -def image_types() -> tuple[str, ...]: ... - -class Spinbox(Widget, XView): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - activebackground: str = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - buttonbackground: str = ..., - buttoncursor: _Cursor = "", - buttondownrelief: _Relief = ..., - buttonuprelief: _Relief = ..., - # percent substitutions don't seem to be supported, it's similar to Entry's validation stuff - command: Callable[[], object] | str | list[str] | tuple[str, ...] = "", - cursor: _Cursor = "xterm", - disabledbackground: str = ..., - disabledforeground: str = ..., - exportselection: bool = True, - fg: str = ..., - font: _FontDescription = "TkTextFont", - foreground: str = ..., - format: str = "", - from_: float = 0.0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - increment: float = 1.0, - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = 0, - insertofftime: int = 300, - insertontime: int = 600, - insertwidth: _ScreenUnits = ..., - invalidcommand: _EntryValidateCommand = "", - invcmd: _EntryValidateCommand = "", - justify: Literal["left", "center", "right"] = "left", - name: str = ..., - readonlybackground: str = ..., - relief: _Relief = "sunken", - repeatdelay: int = 400, - repeatinterval: int = 100, - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - state: Literal["normal", "disabled", "readonly"] = "normal", - takefocus: _TakeFocusValue = "", - textvariable: Variable = ..., - to: float = 0.0, - validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = "none", - validatecommand: _EntryValidateCommand = "", - vcmd: _EntryValidateCommand = "", - values: list[str] | tuple[str, ...] = ..., - width: int = 20, - wrap: bool = False, - xscrollcommand: _XYScrollCommand = "", - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - activebackground: str = ..., - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - buttonbackground: str = ..., - buttoncursor: _Cursor = ..., - buttondownrelief: _Relief = ..., - buttonuprelief: _Relief = ..., - command: Callable[[], object] | str | list[str] | tuple[str, ...] = ..., - cursor: _Cursor = ..., - disabledbackground: str = ..., - disabledforeground: str = ..., - exportselection: bool = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - format: str = ..., - from_: float = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - increment: float = ..., - insertbackground: str = ..., - insertborderwidth: _ScreenUnits = ..., - insertofftime: int = ..., - insertontime: int = ..., - insertwidth: _ScreenUnits = ..., - invalidcommand: _EntryValidateCommand = ..., - invcmd: _EntryValidateCommand = ..., - justify: Literal["left", "center", "right"] = ..., - readonlybackground: str = ..., - relief: _Relief = ..., - repeatdelay: int = ..., - repeatinterval: int = ..., - selectbackground: str = ..., - selectborderwidth: _ScreenUnits = ..., - selectforeground: str = ..., - state: Literal["normal", "disabled", "readonly"] = ..., - takefocus: _TakeFocusValue = ..., - textvariable: Variable = ..., - to: float = ..., - validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = ..., - validatecommand: _EntryValidateCommand = ..., - vcmd: _EntryValidateCommand = ..., - values: list[str] | tuple[str, ...] = ..., - width: int = ..., - wrap: bool = ..., - xscrollcommand: _XYScrollCommand = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def bbox(self, index) -> tuple[int, int, int, int] | None: ... # type: ignore[override] - def delete(self, first, last: Incomplete | None = None) -> Literal[""]: ... - def get(self) -> str: ... - def icursor(self, index): ... - def identify(self, x: int, y: int) -> Literal["", "buttondown", "buttonup", "entry"]: ... - def index(self, index: str | int) -> int: ... - def insert(self, index: str | int, s: str) -> Literal[""]: ... - # spinbox.invoke("asdf") gives error mentioning .invoke("none"), but it's not documented - def invoke(self, element: Literal["none", "buttonup", "buttondown"]) -> Literal[""]: ... - def scan(self, *args): ... - def scan_mark(self, x): ... - def scan_dragto(self, x): ... - def selection(self, *args) -> tuple[int, ...]: ... - def selection_adjust(self, index): ... - def selection_clear(self): ... # type: ignore[override] - def selection_element(self, element: Incomplete | None = None): ... - def selection_from(self, index: int) -> None: ... - def selection_present(self) -> None: ... - def selection_range(self, start: int, end: int) -> None: ... - def selection_to(self, index: int) -> None: ... - -class LabelFrame(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - background: str = ..., - bd: _ScreenUnits = 2, - bg: str = ..., - border: _ScreenUnits = 2, - borderwidth: _ScreenUnits = 2, - class_: str = "Labelframe", # can't be changed with configure() - colormap: Literal["new", ""] | Misc = "", # can't be changed with configure() - container: bool = False, # undocumented, can't be changed with configure() - cursor: _Cursor = "", - fg: str = ..., - font: _FontDescription = "TkDefaultFont", - foreground: str = ..., - height: _ScreenUnits = 0, - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = 0, - # 'ne' and 'en' are valid labelanchors, but only 'ne' is a valid _Anchor. - labelanchor: Literal["nw", "n", "ne", "en", "e", "es", "se", "s", "sw", "ws", "w", "wn"] = "nw", - labelwidget: Misc = ..., - name: str = ..., - padx: _ScreenUnits = 0, - pady: _ScreenUnits = 0, - relief: _Relief = "groove", - takefocus: _TakeFocusValue = 0, - text: float | str = "", - visual: str | tuple[str, int] = "", # can't be changed with configure() - width: _ScreenUnits = 0, - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - fg: str = ..., - font: _FontDescription = ..., - foreground: str = ..., - height: _ScreenUnits = ..., - highlightbackground: str = ..., - highlightcolor: str = ..., - highlightthickness: _ScreenUnits = ..., - labelanchor: Literal["nw", "n", "ne", "en", "e", "es", "se", "s", "sw", "ws", "w", "wn"] = ..., - labelwidget: Misc = ..., - padx: _ScreenUnits = ..., - pady: _ScreenUnits = ..., - relief: _Relief = ..., - takefocus: _TakeFocusValue = ..., - text: float | str = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - -class PanedWindow(Widget): - def __init__( - self, - master: Misc | None = None, - cnf: dict[str, Any] | None = {}, - *, - background: str = ..., - bd: _ScreenUnits = 1, - bg: str = ..., - border: _ScreenUnits = 1, - borderwidth: _ScreenUnits = 1, - cursor: _Cursor = "", - handlepad: _ScreenUnits = 8, - handlesize: _ScreenUnits = 8, - height: _ScreenUnits = "", - name: str = ..., - opaqueresize: bool = True, - orient: Literal["horizontal", "vertical"] = "horizontal", - proxybackground: str = "", - proxyborderwidth: _ScreenUnits = 2, - proxyrelief: _Relief = "flat", - relief: _Relief = "flat", - sashcursor: _Cursor = "", - sashpad: _ScreenUnits = 0, - sashrelief: _Relief = "flat", - sashwidth: _ScreenUnits = 3, - showhandle: bool = False, - width: _ScreenUnits = "", - ) -> None: ... - @overload - def configure( - self, - cnf: dict[str, Any] | None = None, - *, - background: str = ..., - bd: _ScreenUnits = ..., - bg: str = ..., - border: _ScreenUnits = ..., - borderwidth: _ScreenUnits = ..., - cursor: _Cursor = ..., - handlepad: _ScreenUnits = ..., - handlesize: _ScreenUnits = ..., - height: _ScreenUnits = ..., - opaqueresize: bool = ..., - orient: Literal["horizontal", "vertical"] = ..., - proxybackground: str = ..., - proxyborderwidth: _ScreenUnits = ..., - proxyrelief: _Relief = ..., - relief: _Relief = ..., - sashcursor: _Cursor = ..., - sashpad: _ScreenUnits = ..., - sashrelief: _Relief = ..., - sashwidth: _ScreenUnits = ..., - showhandle: bool = ..., - width: _ScreenUnits = ..., - ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... - @overload - def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... - config = configure - def add(self, child: Widget, **kw) -> None: ... - def remove(self, child) -> None: ... - forget: Incomplete - def identify(self, x: int, y: int): ... - def proxy(self, *args): ... - def proxy_coord(self): ... - def proxy_forget(self): ... - def proxy_place(self, x, y): ... - def sash(self, *args): ... - def sash_coord(self, index): ... - def sash_mark(self, index): ... - def sash_place(self, index, x, y): ... - def panecget(self, child, option): ... - def paneconfigure(self, tagOrId, cnf: Incomplete | None = None, **kw): ... - paneconfig: Incomplete - def panes(self): ... - -def _test() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi deleted file mode 100644 index 201ca13ddd9c5..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi +++ /dev/null @@ -1,12 +0,0 @@ -from _typeshed import Incomplete -from collections.abc import Mapping -from typing import ClassVar - -__all__ = ["Dialog"] - -class Dialog: - command: ClassVar[str | None] - master: Incomplete | None - options: Mapping[str, Incomplete] - def __init__(self, master: Incomplete | None = None, **options) -> None: ... - def show(self, **options): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi deleted file mode 100644 index 3dc059940964c..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi +++ /dev/null @@ -1,14 +0,0 @@ -from _typeshed import Incomplete -from collections.abc import Mapping -from tkinter import Widget -from typing import Any, Final - -__all__ = ["Dialog"] - -DIALOG_ICON: Final = "questhead" - -class Dialog(Widget): - widgetName: str - num: int - def __init__(self, master: Incomplete | None = None, cnf: Mapping[str, Any] = {}, **kw) -> None: ... - def destroy(self) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi deleted file mode 100644 index 902fab62ac05a..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi +++ /dev/null @@ -1,33 +0,0 @@ -from tkinter.commondialog import Dialog -from typing import ClassVar, Final - -__all__ = ["showinfo", "showwarning", "showerror", "askquestion", "askokcancel", "askyesno", "askyesnocancel", "askretrycancel"] - -ERROR: Final = "error" -INFO: Final = "info" -QUESTION: Final = "question" -WARNING: Final = "warning" -ABORTRETRYIGNORE: Final = "abortretryignore" -OK: Final = "ok" -OKCANCEL: Final = "okcancel" -RETRYCANCEL: Final = "retrycancel" -YESNO: Final = "yesno" -YESNOCANCEL: Final = "yesnocancel" -ABORT: Final = "abort" -RETRY: Final = "retry" -IGNORE: Final = "ignore" -CANCEL: Final = "cancel" -YES: Final = "yes" -NO: Final = "no" - -class Message(Dialog): - command: ClassVar[str] - -def showinfo(title: str | None = None, message: str | None = None, **options) -> str: ... -def showwarning(title: str | None = None, message: str | None = None, **options) -> str: ... -def showerror(title: str | None = None, message: str | None = None, **options) -> str: ... -def askquestion(title: str | None = None, message: str | None = None, **options) -> str: ... -def askokcancel(title: str | None = None, message: str | None = None, **options) -> bool: ... -def askyesno(title: str | None = None, message: str | None = None, **options) -> bool: ... -def askyesnocancel(title: str | None = None, message: str | None = None, **options) -> bool | None: ... -def askretrycancel(title: str | None = None, message: str | None = None, **options) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/token.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/token.pyi deleted file mode 100644 index 741ce5b035b77..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/token.pyi +++ /dev/null @@ -1,160 +0,0 @@ -import sys - -__all__ = [ - "AMPER", - "AMPEREQUAL", - "AT", - "ATEQUAL", - "CIRCUMFLEX", - "CIRCUMFLEXEQUAL", - "COLON", - "COLONEQUAL", - "COMMA", - "DEDENT", - "DOT", - "DOUBLESLASH", - "DOUBLESLASHEQUAL", - "DOUBLESTAR", - "DOUBLESTAREQUAL", - "ELLIPSIS", - "ENDMARKER", - "EQEQUAL", - "EQUAL", - "ERRORTOKEN", - "GREATER", - "GREATEREQUAL", - "INDENT", - "ISEOF", - "ISNONTERMINAL", - "ISTERMINAL", - "LBRACE", - "LEFTSHIFT", - "LEFTSHIFTEQUAL", - "LESS", - "LESSEQUAL", - "LPAR", - "LSQB", - "MINEQUAL", - "MINUS", - "NAME", - "NEWLINE", - "NOTEQUAL", - "NT_OFFSET", - "NUMBER", - "N_TOKENS", - "OP", - "PERCENT", - "PERCENTEQUAL", - "PLUS", - "PLUSEQUAL", - "RARROW", - "RBRACE", - "RIGHTSHIFT", - "RIGHTSHIFTEQUAL", - "RPAR", - "RSQB", - "SEMI", - "SLASH", - "SLASHEQUAL", - "STAR", - "STAREQUAL", - "STRING", - "TILDE", - "TYPE_COMMENT", - "TYPE_IGNORE", - "VBAR", - "VBAREQUAL", - "tok_name", - "ENCODING", - "NL", - "COMMENT", -] -if sys.version_info < (3, 13): - __all__ += ["ASYNC", "AWAIT"] - -if sys.version_info >= (3, 10): - __all__ += ["SOFT_KEYWORD"] - -if sys.version_info >= (3, 12): - __all__ += ["EXCLAMATION", "FSTRING_END", "FSTRING_MIDDLE", "FSTRING_START", "EXACT_TOKEN_TYPES"] - -ENDMARKER: int -NAME: int -NUMBER: int -STRING: int -NEWLINE: int -INDENT: int -DEDENT: int -LPAR: int -RPAR: int -LSQB: int -RSQB: int -COLON: int -COMMA: int -SEMI: int -PLUS: int -MINUS: int -STAR: int -SLASH: int -VBAR: int -AMPER: int -LESS: int -GREATER: int -EQUAL: int -DOT: int -PERCENT: int -LBRACE: int -RBRACE: int -EQEQUAL: int -NOTEQUAL: int -LESSEQUAL: int -GREATEREQUAL: int -TILDE: int -CIRCUMFLEX: int -LEFTSHIFT: int -RIGHTSHIFT: int -DOUBLESTAR: int -PLUSEQUAL: int -MINEQUAL: int -STAREQUAL: int -SLASHEQUAL: int -PERCENTEQUAL: int -AMPEREQUAL: int -VBAREQUAL: int -CIRCUMFLEXEQUAL: int -LEFTSHIFTEQUAL: int -RIGHTSHIFTEQUAL: int -DOUBLESTAREQUAL: int -DOUBLESLASH: int -DOUBLESLASHEQUAL: int -AT: int -RARROW: int -ELLIPSIS: int -ATEQUAL: int -if sys.version_info < (3, 13): - AWAIT: int - ASYNC: int -OP: int -ERRORTOKEN: int -N_TOKENS: int -NT_OFFSET: int -tok_name: dict[int, str] -COMMENT: int -NL: int -ENCODING: int -TYPE_COMMENT: int -TYPE_IGNORE: int -COLONEQUAL: int -EXACT_TOKEN_TYPES: dict[str, int] -if sys.version_info >= (3, 10): - SOFT_KEYWORD: int - -if sys.version_info >= (3, 12): - EXCLAMATION: int - FSTRING_END: int - FSTRING_MIDDLE: int - FSTRING_START: int - -def ISTERMINAL(x: int) -> bool: ... -def ISNONTERMINAL(x: int) -> bool: ... -def ISEOF(x: int) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tomllib.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tomllib.pyi deleted file mode 100644 index d559568b912b5..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tomllib.pyi +++ /dev/null @@ -1,10 +0,0 @@ -from _typeshed import SupportsRead -from collections.abc import Callable -from typing import Any - -__all__ = ("loads", "load", "TOMLDecodeError") - -class TOMLDecodeError(ValueError): ... - -def load(fp: SupportsRead[bytes], /, *, parse_float: Callable[[str], Any] = ...) -> dict[str, Any]: ... -def loads(s: str, /, *, parse_float: Callable[[str], Any] = ...) -> dict[str, Any]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/types.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/types.pyi deleted file mode 100644 index fe443be271215..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/types.pyi +++ /dev/null @@ -1,693 +0,0 @@ -import sys -from _typeshed import MaybeNone, SupportsKeysAndGetItem -from _typeshed.importlib import LoaderProtocol -from collections.abc import ( - AsyncGenerator, - Awaitable, - Callable, - Coroutine, - Generator, - ItemsView, - Iterable, - Iterator, - KeysView, - Mapping, - MutableSequence, - ValuesView, -) -from importlib.machinery import ModuleSpec -from typing import Any, ClassVar, Literal, TypeVar, final, overload -from typing_extensions import ParamSpec, Self, TypeAliasType, TypeVarTuple, deprecated - -__all__ = [ - "FunctionType", - "LambdaType", - "CodeType", - "MappingProxyType", - "SimpleNamespace", - "GeneratorType", - "CoroutineType", - "AsyncGeneratorType", - "MethodType", - "BuiltinFunctionType", - "ModuleType", - "TracebackType", - "FrameType", - "GetSetDescriptorType", - "MemberDescriptorType", - "new_class", - "prepare_class", - "DynamicClassAttribute", - "coroutine", - "BuiltinMethodType", - "ClassMethodDescriptorType", - "MethodDescriptorType", - "MethodWrapperType", - "WrapperDescriptorType", - "resolve_bases", - "CellType", - "GenericAlias", -] - -if sys.version_info >= (3, 10): - __all__ += ["EllipsisType", "NoneType", "NotImplementedType", "UnionType"] - -if sys.version_info >= (3, 12): - __all__ += ["get_original_bases"] - -if sys.version_info >= (3, 13): - __all__ += ["CapsuleType"] - -# Note, all classes "defined" here require special handling. - -_T1 = TypeVar("_T1") -_T2 = TypeVar("_T2") -_KT = TypeVar("_KT") -_VT_co = TypeVar("_VT_co", covariant=True) - -# Make sure this class definition stays roughly in line with `builtins.function` -@final -class FunctionType: - @property - def __closure__(self) -> tuple[CellType, ...] | None: ... - __code__: CodeType - __defaults__: tuple[Any, ...] | None - __dict__: dict[str, Any] - @property - def __globals__(self) -> dict[str, Any]: ... - __name__: str - __qualname__: str - __annotations__: dict[str, Any] - __kwdefaults__: dict[str, Any] | None - if sys.version_info >= (3, 10): - @property - def __builtins__(self) -> dict[str, Any]: ... - if sys.version_info >= (3, 12): - __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] - - __module__: str - if sys.version_info >= (3, 13): - def __new__( - cls, - code: CodeType, - globals: dict[str, Any], - name: str | None = None, - argdefs: tuple[object, ...] | None = None, - closure: tuple[CellType, ...] | None = None, - kwdefaults: dict[str, object] | None = None, - ) -> Self: ... - else: - def __new__( - cls, - code: CodeType, - globals: dict[str, Any], - name: str | None = None, - argdefs: tuple[object, ...] | None = None, - closure: tuple[CellType, ...] | None = None, - ) -> Self: ... - - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - @overload - def __get__(self, instance: None, owner: type, /) -> FunctionType: ... - @overload - def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... - -LambdaType = FunctionType - -@final -class CodeType: - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - @property - def co_argcount(self) -> int: ... - @property - def co_posonlyargcount(self) -> int: ... - @property - def co_kwonlyargcount(self) -> int: ... - @property - def co_nlocals(self) -> int: ... - @property - def co_stacksize(self) -> int: ... - @property - def co_flags(self) -> int: ... - @property - def co_code(self) -> bytes: ... - @property - def co_consts(self) -> tuple[Any, ...]: ... - @property - def co_names(self) -> tuple[str, ...]: ... - @property - def co_varnames(self) -> tuple[str, ...]: ... - @property - def co_filename(self) -> str: ... - @property - def co_name(self) -> str: ... - @property - def co_firstlineno(self) -> int: ... - if sys.version_info >= (3, 10): - @property - @deprecated("Will be removed in Python 3.14. Use the co_lines() method instead.") - def co_lnotab(self) -> bytes: ... - else: - @property - def co_lnotab(self) -> bytes: ... - - @property - def co_freevars(self) -> tuple[str, ...]: ... - @property - def co_cellvars(self) -> tuple[str, ...]: ... - if sys.version_info >= (3, 10): - @property - def co_linetable(self) -> bytes: ... - def co_lines(self) -> Iterator[tuple[int, int, int | None]]: ... - if sys.version_info >= (3, 11): - @property - def co_exceptiontable(self) -> bytes: ... - @property - def co_qualname(self) -> str: ... - def co_positions(self) -> Iterable[tuple[int | None, int | None, int | None, int | None]]: ... - - if sys.version_info >= (3, 11): - def __new__( - cls, - argcount: int, - posonlyargcount: int, - kwonlyargcount: int, - nlocals: int, - stacksize: int, - flags: int, - codestring: bytes, - constants: tuple[object, ...], - names: tuple[str, ...], - varnames: tuple[str, ...], - filename: str, - name: str, - qualname: str, - firstlineno: int, - linetable: bytes, - exceptiontable: bytes, - freevars: tuple[str, ...] = ..., - cellvars: tuple[str, ...] = ..., - /, - ) -> Self: ... - elif sys.version_info >= (3, 10): - def __new__( - cls, - argcount: int, - posonlyargcount: int, - kwonlyargcount: int, - nlocals: int, - stacksize: int, - flags: int, - codestring: bytes, - constants: tuple[object, ...], - names: tuple[str, ...], - varnames: tuple[str, ...], - filename: str, - name: str, - firstlineno: int, - linetable: bytes, - freevars: tuple[str, ...] = ..., - cellvars: tuple[str, ...] = ..., - /, - ) -> Self: ... - else: - def __new__( - cls, - argcount: int, - posonlyargcount: int, - kwonlyargcount: int, - nlocals: int, - stacksize: int, - flags: int, - codestring: bytes, - constants: tuple[object, ...], - names: tuple[str, ...], - varnames: tuple[str, ...], - filename: str, - name: str, - firstlineno: int, - lnotab: bytes, - freevars: tuple[str, ...] = ..., - cellvars: tuple[str, ...] = ..., - /, - ) -> Self: ... - if sys.version_info >= (3, 11): - def replace( - self, - *, - co_argcount: int = -1, - co_posonlyargcount: int = -1, - co_kwonlyargcount: int = -1, - co_nlocals: int = -1, - co_stacksize: int = -1, - co_flags: int = -1, - co_firstlineno: int = -1, - co_code: bytes = ..., - co_consts: tuple[object, ...] = ..., - co_names: tuple[str, ...] = ..., - co_varnames: tuple[str, ...] = ..., - co_freevars: tuple[str, ...] = ..., - co_cellvars: tuple[str, ...] = ..., - co_filename: str = ..., - co_name: str = ..., - co_qualname: str = ..., - co_linetable: bytes = ..., - co_exceptiontable: bytes = ..., - ) -> Self: ... - elif sys.version_info >= (3, 10): - def replace( - self, - *, - co_argcount: int = -1, - co_posonlyargcount: int = -1, - co_kwonlyargcount: int = -1, - co_nlocals: int = -1, - co_stacksize: int = -1, - co_flags: int = -1, - co_firstlineno: int = -1, - co_code: bytes = ..., - co_consts: tuple[object, ...] = ..., - co_names: tuple[str, ...] = ..., - co_varnames: tuple[str, ...] = ..., - co_freevars: tuple[str, ...] = ..., - co_cellvars: tuple[str, ...] = ..., - co_filename: str = ..., - co_name: str = ..., - co_linetable: bytes = ..., - ) -> Self: ... - else: - def replace( - self, - *, - co_argcount: int = -1, - co_posonlyargcount: int = -1, - co_kwonlyargcount: int = -1, - co_nlocals: int = -1, - co_stacksize: int = -1, - co_flags: int = -1, - co_firstlineno: int = -1, - co_code: bytes = ..., - co_consts: tuple[object, ...] = ..., - co_names: tuple[str, ...] = ..., - co_varnames: tuple[str, ...] = ..., - co_freevars: tuple[str, ...] = ..., - co_cellvars: tuple[str, ...] = ..., - co_filename: str = ..., - co_name: str = ..., - co_lnotab: bytes = ..., - ) -> Self: ... - - if sys.version_info >= (3, 13): - __replace__ = replace - -@final -class MappingProxyType(Mapping[_KT, _VT_co]): - __hash__: ClassVar[None] # type: ignore[assignment] - def __new__(cls, mapping: SupportsKeysAndGetItem[_KT, _VT_co]) -> Self: ... - def __getitem__(self, key: _KT, /) -> _VT_co: ... - def __iter__(self) -> Iterator[_KT]: ... - def __len__(self) -> int: ... - def __eq__(self, value: object, /) -> bool: ... - def copy(self) -> dict[_KT, _VT_co]: ... - def keys(self) -> KeysView[_KT]: ... - def values(self) -> ValuesView[_VT_co]: ... - def items(self) -> ItemsView[_KT, _VT_co]: ... - @overload - def get(self, key: _KT, /) -> _VT_co | None: ... - @overload - def get(self, key: _KT, default: _VT_co | _T2, /) -> _VT_co | _T2: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - def __reversed__(self) -> Iterator[_KT]: ... - def __or__(self, value: Mapping[_T1, _T2], /) -> dict[_KT | _T1, _VT_co | _T2]: ... - def __ror__(self, value: Mapping[_T1, _T2], /) -> dict[_KT | _T1, _VT_co | _T2]: ... - -class SimpleNamespace: - __hash__: ClassVar[None] # type: ignore[assignment] - if sys.version_info >= (3, 13): - def __init__(self, mapping_or_iterable: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), /, **kwargs: Any) -> None: ... - else: - def __init__(self, **kwargs: Any) -> None: ... - - def __eq__(self, value: object, /) -> bool: ... - def __getattribute__(self, name: str, /) -> Any: ... - def __setattr__(self, name: str, value: Any, /) -> None: ... - def __delattr__(self, name: str, /) -> None: ... - if sys.version_info >= (3, 13): - def __replace__(self, **kwargs: Any) -> Self: ... - -class ModuleType: - __name__: str - __file__: str | None - @property - def __dict__(self) -> dict[str, Any]: ... # type: ignore[override] - __loader__: LoaderProtocol | None - __package__: str | None - __path__: MutableSequence[str] - __spec__: ModuleSpec | None - # N.B. Although this is the same type as `builtins.object.__doc__`, - # it is deliberately redeclared here. Most symbols declared in the namespace - # of `types.ModuleType` are available as "implicit globals" within a module's - # namespace, but this is not true for symbols declared in the namespace of `builtins.object`. - # Redeclaring `__doc__` here helps some type checkers understand that `__doc__` is available - # as an implicit global in all modules, similar to `__name__`, `__file__`, `__spec__`, etc. - __doc__: str | None - def __init__(self, name: str, doc: str | None = ...) -> None: ... - # __getattr__ doesn't exist at runtime, - # but having it here in typeshed makes dynamic imports - # using `builtins.__import__` or `importlib.import_module` less painful - def __getattr__(self, name: str) -> Any: ... - -@final -class CellType: - def __new__(cls, contents: object = ..., /) -> Self: ... - __hash__: ClassVar[None] # type: ignore[assignment] - cell_contents: Any - -_YieldT_co = TypeVar("_YieldT_co", covariant=True) -_SendT_contra = TypeVar("_SendT_contra", contravariant=True) -_ReturnT_co = TypeVar("_ReturnT_co", covariant=True) - -@final -class GeneratorType(Generator[_YieldT_co, _SendT_contra, _ReturnT_co]): - @property - def gi_code(self) -> CodeType: ... - @property - def gi_frame(self) -> FrameType: ... - @property - def gi_running(self) -> bool: ... - @property - def gi_yieldfrom(self) -> GeneratorType[_YieldT_co, _SendT_contra, Any] | None: ... - if sys.version_info >= (3, 11): - @property - def gi_suspended(self) -> bool: ... - __name__: str - __qualname__: str - def __iter__(self) -> Self: ... - def __next__(self) -> _YieldT_co: ... - def send(self, arg: _SendT_contra, /) -> _YieldT_co: ... - @overload - def throw( - self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / - ) -> _YieldT_co: ... - @overload - def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... - if sys.version_info >= (3, 13): - def __class_getitem__(cls, item: Any, /) -> Any: ... - -@final -class AsyncGeneratorType(AsyncGenerator[_YieldT_co, _SendT_contra]): - @property - def ag_await(self) -> Awaitable[Any] | None: ... - @property - def ag_code(self) -> CodeType: ... - @property - def ag_frame(self) -> FrameType: ... - @property - def ag_running(self) -> bool: ... - __name__: str - __qualname__: str - if sys.version_info >= (3, 12): - @property - def ag_suspended(self) -> bool: ... - - def __aiter__(self) -> Self: ... - def __anext__(self) -> Coroutine[Any, Any, _YieldT_co]: ... - def asend(self, val: _SendT_contra, /) -> Coroutine[Any, Any, _YieldT_co]: ... - @overload - async def athrow( - self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / - ) -> _YieldT_co: ... - @overload - async def athrow(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... - def aclose(self) -> Coroutine[Any, Any, None]: ... - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - -@final -class CoroutineType(Coroutine[_YieldT_co, _SendT_contra, _ReturnT_co]): - __name__: str - __qualname__: str - @property - def cr_await(self) -> Any | None: ... - @property - def cr_code(self) -> CodeType: ... - @property - def cr_frame(self) -> FrameType: ... - @property - def cr_running(self) -> bool: ... - @property - def cr_origin(self) -> tuple[tuple[str, int, str], ...] | None: ... - if sys.version_info >= (3, 11): - @property - def cr_suspended(self) -> bool: ... - - def close(self) -> None: ... - def __await__(self) -> Generator[Any, None, _ReturnT_co]: ... - def send(self, arg: _SendT_contra, /) -> _YieldT_co: ... - @overload - def throw( - self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / - ) -> _YieldT_co: ... - @overload - def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... - if sys.version_info >= (3, 13): - def __class_getitem__(cls, item: Any, /) -> Any: ... - -@final -class MethodType: - @property - def __closure__(self) -> tuple[CellType, ...] | None: ... # inherited from the added function - @property - def __code__(self) -> CodeType: ... # inherited from the added function - @property - def __defaults__(self) -> tuple[Any, ...] | None: ... # inherited from the added function - @property - def __func__(self) -> Callable[..., Any]: ... - @property - def __self__(self) -> object: ... - @property - def __name__(self) -> str: ... # inherited from the added function - @property - def __qualname__(self) -> str: ... # inherited from the added function - def __new__(cls, func: Callable[..., Any], instance: object, /) -> Self: ... - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - -@final -class BuiltinFunctionType: - @property - def __self__(self) -> object | ModuleType: ... - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - -BuiltinMethodType = BuiltinFunctionType - -@final -class WrapperDescriptorType: - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - @property - def __objclass__(self) -> type: ... - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... - -@final -class MethodWrapperType: - @property - def __self__(self) -> object: ... - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - @property - def __objclass__(self) -> type: ... - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - def __eq__(self, value: object, /) -> bool: ... - def __ne__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - -@final -class MethodDescriptorType: - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - @property - def __objclass__(self) -> type: ... - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... - -@final -class ClassMethodDescriptorType: - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - @property - def __objclass__(self) -> type: ... - def __call__(self, *args: Any, **kwargs: Any) -> Any: ... - def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... - -@final -class TracebackType: - def __new__(cls, tb_next: TracebackType | None, tb_frame: FrameType, tb_lasti: int, tb_lineno: int) -> Self: ... - tb_next: TracebackType | None - # the rest are read-only - @property - def tb_frame(self) -> FrameType: ... - @property - def tb_lasti(self) -> int: ... - @property - def tb_lineno(self) -> int: ... - -@final -class FrameType: - @property - def f_back(self) -> FrameType | None: ... - @property - def f_builtins(self) -> dict[str, Any]: ... - @property - def f_code(self) -> CodeType: ... - @property - def f_globals(self) -> dict[str, Any]: ... - @property - def f_lasti(self) -> int: ... - # see discussion in #6769: f_lineno *can* sometimes be None, - # but you should probably file a bug report with CPython if you encounter it being None in the wild. - # An `int | None` annotation here causes too many false-positive errors, so applying `int | Any`. - @property - def f_lineno(self) -> int | MaybeNone: ... - @property - def f_locals(self) -> dict[str, Any]: ... - f_trace: Callable[[FrameType, str, Any], Any] | None - f_trace_lines: bool - f_trace_opcodes: bool - def clear(self) -> None: ... - -@final -class GetSetDescriptorType: - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - @property - def __objclass__(self) -> type: ... - def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... - def __set__(self, instance: Any, value: Any, /) -> None: ... - def __delete__(self, instance: Any, /) -> None: ... - -@final -class MemberDescriptorType: - @property - def __name__(self) -> str: ... - @property - def __qualname__(self) -> str: ... - @property - def __objclass__(self) -> type: ... - def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... - def __set__(self, instance: Any, value: Any, /) -> None: ... - def __delete__(self, instance: Any, /) -> None: ... - -def new_class( - name: str, - bases: Iterable[object] = (), - kwds: dict[str, Any] | None = None, - exec_body: Callable[[dict[str, Any]], object] | None = None, -) -> type: ... -def resolve_bases(bases: Iterable[object]) -> tuple[Any, ...]: ... -def prepare_class( - name: str, bases: tuple[type, ...] = (), kwds: dict[str, Any] | None = None -) -> tuple[type, dict[str, Any], dict[str, Any]]: ... - -if sys.version_info >= (3, 12): - def get_original_bases(cls: type, /) -> tuple[Any, ...]: ... - -# Does not actually inherit from property, but saying it does makes sure that -# pyright handles this class correctly. -class DynamicClassAttribute(property): - fget: Callable[[Any], Any] | None - fset: Callable[[Any, Any], object] | None # type: ignore[assignment] - fdel: Callable[[Any], object] | None # type: ignore[assignment] - overwrite_doc: bool - __isabstractmethod__: bool - def __init__( - self, - fget: Callable[[Any], Any] | None = None, - fset: Callable[[Any, Any], object] | None = None, - fdel: Callable[[Any], object] | None = None, - doc: str | None = None, - ) -> None: ... - def __get__(self, instance: Any, ownerclass: type | None = None) -> Any: ... - def __set__(self, instance: Any, value: Any) -> None: ... - def __delete__(self, instance: Any) -> None: ... - def getter(self, fget: Callable[[Any], Any]) -> DynamicClassAttribute: ... - def setter(self, fset: Callable[[Any, Any], object]) -> DynamicClassAttribute: ... - def deleter(self, fdel: Callable[[Any], object]) -> DynamicClassAttribute: ... - -_Fn = TypeVar("_Fn", bound=Callable[..., object]) -_R = TypeVar("_R") -_P = ParamSpec("_P") - -# it's not really an Awaitable, but can be used in an await expression. Real type: Generator & Awaitable -@overload -def coroutine(func: Callable[_P, Generator[Any, Any, _R]]) -> Callable[_P, Awaitable[_R]]: ... -@overload -def coroutine(func: _Fn) -> _Fn: ... - -class GenericAlias: - @property - def __origin__(self) -> type | TypeAliasType: ... - @property - def __args__(self) -> tuple[Any, ...]: ... - @property - def __parameters__(self) -> tuple[Any, ...]: ... - def __new__(cls, origin: type, args: Any, /) -> Self: ... - def __getitem__(self, typeargs: Any, /) -> GenericAlias: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - def __mro_entries__(self, bases: Iterable[object], /) -> tuple[type, ...]: ... - if sys.version_info >= (3, 11): - @property - def __unpacked__(self) -> bool: ... - @property - def __typing_unpacked_tuple_args__(self) -> tuple[Any, ...] | None: ... - if sys.version_info >= (3, 10): - def __or__(self, value: Any, /) -> UnionType: ... - def __ror__(self, value: Any, /) -> UnionType: ... - - # GenericAlias delegates attr access to `__origin__` - def __getattr__(self, name: str) -> Any: ... - -if sys.version_info >= (3, 10): - @final - class NoneType: - def __bool__(self) -> Literal[False]: ... - - @final - class EllipsisType: ... - - from builtins import _NotImplementedType - - NotImplementedType = _NotImplementedType - @final - class UnionType: - @property - def __args__(self) -> tuple[Any, ...]: ... - @property - def __parameters__(self) -> tuple[Any, ...]: ... - def __or__(self, value: Any, /) -> UnionType: ... - def __ror__(self, value: Any, /) -> UnionType: ... - def __eq__(self, value: object, /) -> bool: ... - def __hash__(self) -> int: ... - -if sys.version_info >= (3, 13): - @final - class CapsuleType: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing_extensions.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/typing_extensions.pyi deleted file mode 100644 index bad5fae880c0f..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing_extensions.pyi +++ /dev/null @@ -1,626 +0,0 @@ -import abc -import enum -import sys -from _collections_abc import dict_items, dict_keys, dict_values -from _typeshed import IdentityFunction, Incomplete, Unused -from collections.abc import ( - AsyncGenerator as AsyncGenerator, - AsyncIterable as AsyncIterable, - AsyncIterator as AsyncIterator, - Awaitable as Awaitable, - Collection as Collection, - Container as Container, - Coroutine as Coroutine, - Generator as Generator, - Hashable as Hashable, - ItemsView as ItemsView, - Iterable as Iterable, - Iterator as Iterator, - KeysView as KeysView, - Mapping as Mapping, - MappingView as MappingView, - MutableMapping as MutableMapping, - MutableSequence as MutableSequence, - MutableSet as MutableSet, - Reversible as Reversible, - Sequence as Sequence, - Sized as Sized, - ValuesView as ValuesView, -) -from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager -from re import Match as Match, Pattern as Pattern -from types import GenericAlias, ModuleType -from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 - IO as IO, - TYPE_CHECKING as TYPE_CHECKING, - AbstractSet as AbstractSet, - Any as Any, - AnyStr as AnyStr, - BinaryIO as BinaryIO, - Callable as Callable, - ChainMap as ChainMap, - ClassVar as ClassVar, - Counter as Counter, - DefaultDict as DefaultDict, - Deque as Deque, - Dict as Dict, - ForwardRef as ForwardRef, - FrozenSet as FrozenSet, - Generic as Generic, - List as List, - NoReturn as NoReturn, - Optional as Optional, - Set as Set, - Text as Text, - TextIO as TextIO, - Tuple as Tuple, - Type as Type, - TypedDict as TypedDict, - TypeVar as _TypeVar, - Union as Union, - _Alias, - cast as cast, - no_type_check as no_type_check, - no_type_check_decorator as no_type_check_decorator, - overload as overload, - type_check_only, -) - -if sys.version_info >= (3, 10): - from types import UnionType - -# Please keep order the same as at runtime. -__all__ = [ - # Super-special typing primitives. - "Any", - "ClassVar", - "Concatenate", - "Final", - "LiteralString", - "ParamSpec", - "ParamSpecArgs", - "ParamSpecKwargs", - "Self", - "Type", - "TypeVar", - "TypeVarTuple", - "Unpack", - # ABCs (from collections.abc). - "Awaitable", - "AsyncIterator", - "AsyncIterable", - "Coroutine", - "AsyncGenerator", - "AsyncContextManager", - "Buffer", - "ChainMap", - # Concrete collection types. - "ContextManager", - "Counter", - "Deque", - "DefaultDict", - "NamedTuple", - "OrderedDict", - "TypedDict", - # Structural checks, a.k.a. protocols. - "SupportsAbs", - "SupportsBytes", - "SupportsComplex", - "SupportsFloat", - "SupportsIndex", - "SupportsInt", - "SupportsRound", - # One-off things. - "Annotated", - "assert_never", - "assert_type", - "clear_overloads", - "dataclass_transform", - "deprecated", - "Doc", - "evaluate_forward_ref", - "get_overloads", - "final", - "Format", - "get_annotations", - "get_args", - "get_origin", - "get_original_bases", - "get_protocol_members", - "get_type_hints", - "IntVar", - "is_protocol", - "is_typeddict", - "Literal", - "NewType", - "overload", - "override", - "Protocol", - "reveal_type", - "runtime", - "runtime_checkable", - "Text", - "TypeAlias", - "TypeAliasType", - "TypeForm", - "TypeGuard", - "TypeIs", - "TYPE_CHECKING", - "Never", - "NoReturn", - "ReadOnly", - "Required", - "NotRequired", - "NoDefault", - "NoExtraItems", - # Pure aliases, have always been in typing - "AbstractSet", - "AnyStr", - "BinaryIO", - "Callable", - "Collection", - "Container", - "Dict", - "ForwardRef", - "FrozenSet", - "Generator", - "Generic", - "Hashable", - "IO", - "ItemsView", - "Iterable", - "Iterator", - "KeysView", - "List", - "Mapping", - "MappingView", - "Match", - "MutableMapping", - "MutableSequence", - "MutableSet", - "Optional", - "Pattern", - "Reversible", - "Sequence", - "Set", - "Sized", - "TextIO", - "Tuple", - "Union", - "ValuesView", - "cast", - "no_type_check", - "no_type_check_decorator", - # Added dynamically - "CapsuleType", -] - -_T = _TypeVar("_T") -_F = _TypeVar("_F", bound=Callable[..., Any]) -_TC = _TypeVar("_TC", bound=type[object]) -_T_co = _TypeVar("_T_co", covariant=True) # Any type covariant containers. - -class _Final: ... # This should be imported from typing but that breaks pytype - -# unfortunately we have to duplicate this class definition from typing.pyi or we break pytype -class _SpecialForm(_Final): - def __getitem__(self, parameters: Any) -> object: ... - if sys.version_info >= (3, 10): - def __or__(self, other: Any) -> _SpecialForm: ... - def __ror__(self, other: Any) -> _SpecialForm: ... - -# Do not import (and re-export) Protocol or runtime_checkable from -# typing module because type checkers need to be able to distinguish -# typing.Protocol and typing_extensions.Protocol so they can properly -# warn users about potential runtime exceptions when using typing.Protocol -# on older versions of Python. -Protocol: _SpecialForm - -def runtime_checkable(cls: _TC) -> _TC: ... - -# This alias for above is kept here for backwards compatibility. -runtime = runtime_checkable -Final: _SpecialForm - -def final(f: _F) -> _F: ... - -Literal: _SpecialForm - -def IntVar(name: str) -> Any: ... # returns a new TypeVar - -# Internal mypy fallback type for all typed dicts (does not exist at runtime) -# N.B. Keep this mostly in sync with typing._TypedDict/mypy_extensions._TypedDict -@type_check_only -class _TypedDict(Mapping[str, object], metaclass=abc.ABCMeta): - __required_keys__: ClassVar[frozenset[str]] - __optional_keys__: ClassVar[frozenset[str]] - __total__: ClassVar[bool] - __orig_bases__: ClassVar[tuple[Any, ...]] - # PEP 705 - __readonly_keys__: ClassVar[frozenset[str]] - __mutable_keys__: ClassVar[frozenset[str]] - # PEP 728 - __closed__: ClassVar[bool] - __extra_items__: ClassVar[Any] - def copy(self) -> Self: ... - # Using Never so that only calls using mypy plugin hook that specialize the signature - # can go through. - def setdefault(self, k: Never, default: object) -> object: ... - # Mypy plugin hook for 'pop' expects that 'default' has a type variable type. - def pop(self, k: Never, default: _T = ...) -> object: ... # pyright: ignore[reportInvalidTypeVarUse] - def update(self, m: Self, /) -> None: ... - def items(self) -> dict_items[str, object]: ... - def keys(self) -> dict_keys[str, object]: ... - def values(self) -> dict_values[str, object]: ... - def __delitem__(self, k: Never) -> None: ... - @overload - def __or__(self, value: Self, /) -> Self: ... - @overload - def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ... - @overload - def __ror__(self, value: Self, /) -> Self: ... - @overload - def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ... - # supposedly incompatible definitions of `__ior__` and `__or__`: - # Since this module defines "Self" it is not recognized by Ruff as typing_extensions.Self - def __ior__(self, value: Self, /) -> Self: ... # type: ignore[misc] - -OrderedDict = _Alias() - -def get_type_hints( - obj: Callable[..., Any], - globalns: dict[str, Any] | None = None, - localns: Mapping[str, Any] | None = None, - include_extras: bool = False, -) -> dict[str, Any]: ... -def get_args(tp: Any) -> tuple[Any, ...]: ... - -if sys.version_info >= (3, 10): - @overload - def get_origin(tp: UnionType) -> type[UnionType]: ... - -@overload -def get_origin(tp: GenericAlias) -> type: ... -@overload -def get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ... -@overload -def get_origin(tp: Any) -> Any | None: ... - -Annotated: _SpecialForm -_AnnotatedAlias: Any # undocumented - -# New and changed things in 3.10 -if sys.version_info >= (3, 10): - from typing import ( - Concatenate as Concatenate, - ParamSpecArgs as ParamSpecArgs, - ParamSpecKwargs as ParamSpecKwargs, - TypeAlias as TypeAlias, - TypeGuard as TypeGuard, - is_typeddict as is_typeddict, - ) -else: - @final - class ParamSpecArgs: - @property - def __origin__(self) -> ParamSpec: ... - def __init__(self, origin: ParamSpec) -> None: ... - - @final - class ParamSpecKwargs: - @property - def __origin__(self) -> ParamSpec: ... - def __init__(self, origin: ParamSpec) -> None: ... - - Concatenate: _SpecialForm - TypeAlias: _SpecialForm - TypeGuard: _SpecialForm - def is_typeddict(tp: object) -> bool: ... - -# New and changed things in 3.11 -if sys.version_info >= (3, 11): - from typing import ( - LiteralString as LiteralString, - NamedTuple as NamedTuple, - Never as Never, - NewType as NewType, - NotRequired as NotRequired, - Required as Required, - Self as Self, - Unpack as Unpack, - assert_never as assert_never, - assert_type as assert_type, - clear_overloads as clear_overloads, - dataclass_transform as dataclass_transform, - get_overloads as get_overloads, - reveal_type as reveal_type, - ) -else: - Self: _SpecialForm - Never: _SpecialForm - def reveal_type(obj: _T, /) -> _T: ... - def assert_never(arg: Never, /) -> Never: ... - def assert_type(val: _T, typ: Any, /) -> _T: ... - def clear_overloads() -> None: ... - def get_overloads(func: Callable[..., object]) -> Sequence[Callable[..., object]]: ... - - Required: _SpecialForm - NotRequired: _SpecialForm - LiteralString: _SpecialForm - Unpack: _SpecialForm - - def dataclass_transform( - *, - eq_default: bool = True, - order_default: bool = False, - kw_only_default: bool = False, - frozen_default: bool = False, - field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), - **kwargs: object, - ) -> IdentityFunction: ... - - class NamedTuple(tuple[Any, ...]): - _field_defaults: ClassVar[dict[str, Any]] - _fields: ClassVar[tuple[str, ...]] - __orig_bases__: ClassVar[tuple[Any, ...]] - @overload - def __init__(self, typename: str, fields: Iterable[tuple[str, Any]] = ...) -> None: ... - @overload - def __init__(self, typename: str, fields: None = None, **kwargs: Any) -> None: ... - @classmethod - def _make(cls, iterable: Iterable[Any]) -> Self: ... - def _asdict(self) -> dict[str, Any]: ... - def _replace(self, **kwargs: Any) -> Self: ... - - class NewType: - def __init__(self, name: str, tp: Any) -> None: ... - def __call__(self, obj: _T, /) -> _T: ... - __supertype__: type | NewType - if sys.version_info >= (3, 10): - def __or__(self, other: Any) -> _SpecialForm: ... - def __ror__(self, other: Any) -> _SpecialForm: ... - -if sys.version_info >= (3, 12): - from collections.abc import Buffer as Buffer - from types import get_original_bases as get_original_bases - from typing import ( - SupportsAbs as SupportsAbs, - SupportsBytes as SupportsBytes, - SupportsComplex as SupportsComplex, - SupportsFloat as SupportsFloat, - SupportsIndex as SupportsIndex, - SupportsInt as SupportsInt, - SupportsRound as SupportsRound, - override as override, - ) -else: - def override(arg: _F, /) -> _F: ... - def get_original_bases(cls: type, /) -> tuple[Any, ...]: ... - - # mypy and pyright object to this being both ABC and Protocol. - # At runtime it inherits from ABC and is not a Protocol, but it is on the - # allowlist for use as a Protocol. - @runtime_checkable - class Buffer(Protocol, abc.ABC): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] - # Not actually a Protocol at runtime; see - # https://github.com/python/typeshed/issues/10224 for why we're defining it this way - def __buffer__(self, flags: int, /) -> memoryview: ... - - @runtime_checkable - class SupportsInt(Protocol, metaclass=abc.ABCMeta): - @abc.abstractmethod - def __int__(self) -> int: ... - - @runtime_checkable - class SupportsFloat(Protocol, metaclass=abc.ABCMeta): - @abc.abstractmethod - def __float__(self) -> float: ... - - @runtime_checkable - class SupportsComplex(Protocol, metaclass=abc.ABCMeta): - @abc.abstractmethod - def __complex__(self) -> complex: ... - - @runtime_checkable - class SupportsBytes(Protocol, metaclass=abc.ABCMeta): - @abc.abstractmethod - def __bytes__(self) -> bytes: ... - - @runtime_checkable - class SupportsIndex(Protocol, metaclass=abc.ABCMeta): - @abc.abstractmethod - def __index__(self) -> int: ... - - @runtime_checkable - class SupportsAbs(Protocol[_T_co]): - @abc.abstractmethod - def __abs__(self) -> _T_co: ... - - @runtime_checkable - class SupportsRound(Protocol[_T_co]): - @overload - @abc.abstractmethod - def __round__(self) -> int: ... - @overload - @abc.abstractmethod - def __round__(self, ndigits: int, /) -> _T_co: ... - -if sys.version_info >= (3, 13): - from types import CapsuleType as CapsuleType - from typing import ( - NoDefault as NoDefault, - ParamSpec as ParamSpec, - ReadOnly as ReadOnly, - TypeIs as TypeIs, - TypeVar as TypeVar, - TypeVarTuple as TypeVarTuple, - get_protocol_members as get_protocol_members, - is_protocol as is_protocol, - ) - from warnings import deprecated as deprecated -else: - def is_protocol(tp: type, /) -> bool: ... - def get_protocol_members(tp: type, /) -> frozenset[str]: ... - @final - class _NoDefaultType: ... - - NoDefault: _NoDefaultType - @final - class CapsuleType: ... - - class deprecated: - message: LiteralString - category: type[Warning] | None - stacklevel: int - def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ... - def __call__(self, arg: _T, /) -> _T: ... - - @final - class TypeVar: - @property - def __name__(self) -> str: ... - @property - def __bound__(self) -> Any | None: ... - @property - def __constraints__(self) -> tuple[Any, ...]: ... - @property - def __covariant__(self) -> bool: ... - @property - def __contravariant__(self) -> bool: ... - @property - def __infer_variance__(self) -> bool: ... - @property - def __default__(self) -> Any: ... - def __init__( - self, - name: str, - *constraints: Any, - bound: Any | None = None, - covariant: bool = False, - contravariant: bool = False, - default: Any = ..., - infer_variance: bool = False, - ) -> None: ... - def has_default(self) -> bool: ... - def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any) -> _SpecialForm: ... - def __ror__(self, left: Any) -> _SpecialForm: ... - if sys.version_info >= (3, 11): - def __typing_subst__(self, arg: Any) -> Any: ... - - @final - class ParamSpec: - @property - def __name__(self) -> str: ... - @property - def __bound__(self) -> Any | None: ... - @property - def __covariant__(self) -> bool: ... - @property - def __contravariant__(self) -> bool: ... - @property - def __infer_variance__(self) -> bool: ... - @property - def __default__(self) -> Any: ... - def __init__( - self, - name: str, - *, - bound: None | type[Any] | str = None, - contravariant: bool = False, - covariant: bool = False, - default: Any = ..., - ) -> None: ... - @property - def args(self) -> ParamSpecArgs: ... - @property - def kwargs(self) -> ParamSpecKwargs: ... - def has_default(self) -> bool: ... - def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any) -> _SpecialForm: ... - def __ror__(self, left: Any) -> _SpecialForm: ... - - @final - class TypeVarTuple: - @property - def __name__(self) -> str: ... - @property - def __default__(self) -> Any: ... - def __init__(self, name: str, *, default: Any = ...) -> None: ... - def __iter__(self) -> Any: ... # Unpack[Self] - def has_default(self) -> bool: ... - def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... - - ReadOnly: _SpecialForm - TypeIs: _SpecialForm - -# TypeAliasType was added in Python 3.12, but had significant changes in 3.14. -if sys.version_info >= (3, 14): - from typing import TypeAliasType as TypeAliasType -else: - @final - class TypeAliasType: - def __init__( - self, name: str, value: Any, *, type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = () - ) -> None: ... # value is a type expression - @property - def __value__(self) -> Any: ... # a type expression - @property - def __type_params__(self) -> tuple[TypeVar | ParamSpec | TypeVarTuple, ...]: ... - @property - # `__parameters__` can include special forms if a `TypeVarTuple` was - # passed as a `type_params` element to the constructor method. - def __parameters__(self) -> tuple[TypeVar | ParamSpec | Any, ...]: ... - @property - def __name__(self) -> str: ... - # It's writable on types, but not on instances of TypeAliasType. - @property - def __module__(self) -> str | None: ... # type: ignore[override] - # Returns typing._GenericAlias, which isn't stubbed. - def __getitem__(self, parameters: Incomplete | tuple[Incomplete, ...]) -> Any: ... - def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> NoReturn: ... - if sys.version_info >= (3, 10): - def __or__(self, right: Any) -> _SpecialForm: ... - def __ror__(self, left: Any) -> _SpecialForm: ... - -# PEP 727 -class Doc: - documentation: str - def __init__(self, documentation: str, /) -> None: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - -# PEP 728 -class _NoExtraItemsType: ... - -NoExtraItems: _NoExtraItemsType - -# PEP 747 -TypeForm: _SpecialForm - -class Format(enum.IntEnum): - VALUE = 1 - FORWARDREF = 2 - STRING = 3 - -# PEP 649/749 -def get_annotations( - obj: Callable[..., object] | type[object] | ModuleType, # any callable, class, or module - *, - globals: Mapping[str, Any] | None = None, # value types depend on the key - locals: Mapping[str, Any] | None = None, # value types depend on the key - eval_str: bool = False, - format: Format = Format.VALUE, # noqa: Y011 -) -> dict[str, Any]: ... # values are type expressions -def evaluate_forward_ref( - forward_ref: ForwardRef, - *, - owner: Callable[..., object] | type[object] | ModuleType | None = None, # any callable, class, or module - globals: Mapping[str, Any] | None = None, # value types depend on the key - locals: Mapping[str, Any] | None = None, # value types depend on the key - type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None, - format: Format = Format.VALUE, # noqa: Y011 - _recursive_guard: Container[str] = ..., -) -> Any: ... # str if format is Format.STRING, otherwise a type expression diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/request.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/request.pyi deleted file mode 100644 index 1f453fd1e1d60..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/request.pyi +++ /dev/null @@ -1,408 +0,0 @@ -import ssl -import sys -from _typeshed import ReadableBuffer, StrOrBytesPath, SupportsRead -from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence -from email.message import Message -from http.client import HTTPConnection, HTTPMessage, HTTPResponse -from http.cookiejar import CookieJar -from re import Pattern -from typing import IO, Any, ClassVar, NoReturn, Protocol, TypeVar, overload -from typing_extensions import TypeAlias -from urllib.error import HTTPError as HTTPError -from urllib.response import addclosehook, addinfourl - -__all__ = [ - "Request", - "OpenerDirector", - "BaseHandler", - "HTTPDefaultErrorHandler", - "HTTPRedirectHandler", - "HTTPCookieProcessor", - "ProxyHandler", - "HTTPPasswordMgr", - "HTTPPasswordMgrWithDefaultRealm", - "HTTPPasswordMgrWithPriorAuth", - "AbstractBasicAuthHandler", - "HTTPBasicAuthHandler", - "ProxyBasicAuthHandler", - "AbstractDigestAuthHandler", - "HTTPDigestAuthHandler", - "ProxyDigestAuthHandler", - "HTTPHandler", - "FileHandler", - "FTPHandler", - "CacheFTPHandler", - "DataHandler", - "UnknownHandler", - "HTTPErrorProcessor", - "urlopen", - "install_opener", - "build_opener", - "pathname2url", - "url2pathname", - "getproxies", - "urlretrieve", - "urlcleanup", - "URLopener", - "FancyURLopener", - "HTTPSHandler", -] - -_T = TypeVar("_T") -_UrlopenRet: TypeAlias = Any -_DataType: TypeAlias = ReadableBuffer | SupportsRead[bytes] | Iterable[bytes] | None - -if sys.version_info >= (3, 13): - def urlopen( - url: str | Request, data: _DataType | None = None, timeout: float | None = ..., *, context: ssl.SSLContext | None = None - ) -> _UrlopenRet: ... - -else: - def urlopen( - url: str | Request, - data: _DataType | None = None, - timeout: float | None = ..., - *, - cafile: str | None = None, - capath: str | None = None, - cadefault: bool = False, - context: ssl.SSLContext | None = None, - ) -> _UrlopenRet: ... - -def install_opener(opener: OpenerDirector) -> None: ... -def build_opener(*handlers: BaseHandler | Callable[[], BaseHandler]) -> OpenerDirector: ... - -if sys.platform == "win32": - from nturl2path import pathname2url as pathname2url, url2pathname as url2pathname -else: - def url2pathname(pathname: str) -> str: ... - def pathname2url(pathname: str) -> str: ... - -def getproxies() -> dict[str, str]: ... -def getproxies_environment() -> dict[str, str]: ... -def parse_http_list(s: str) -> list[str]: ... -def parse_keqv_list(l: list[str]) -> dict[str, str]: ... - -if sys.platform == "win32" or sys.platform == "darwin": - def proxy_bypass(host: str) -> Any: ... # undocumented - -else: - def proxy_bypass(host: str, proxies: Mapping[str, str] | None = None) -> Any: ... # undocumented - -class Request: - @property - def full_url(self) -> str: ... - @full_url.setter - def full_url(self, value: str) -> None: ... - @full_url.deleter - def full_url(self) -> None: ... - type: str - host: str - origin_req_host: str - selector: str - data: _DataType - headers: MutableMapping[str, str] - unredirected_hdrs: dict[str, str] - unverifiable: bool - method: str | None - timeout: float | None # Undocumented, only set after __init__() by OpenerDirector.open() - def __init__( - self, - url: str, - data: _DataType = None, - headers: MutableMapping[str, str] = {}, - origin_req_host: str | None = None, - unverifiable: bool = False, - method: str | None = None, - ) -> None: ... - def get_method(self) -> str: ... - def add_header(self, key: str, val: str) -> None: ... - def add_unredirected_header(self, key: str, val: str) -> None: ... - def has_header(self, header_name: str) -> bool: ... - def remove_header(self, header_name: str) -> None: ... - def get_full_url(self) -> str: ... - def set_proxy(self, host: str, type: str) -> None: ... - @overload - def get_header(self, header_name: str) -> str | None: ... - @overload - def get_header(self, header_name: str, default: _T) -> str | _T: ... - def header_items(self) -> list[tuple[str, str]]: ... - def has_proxy(self) -> bool: ... - -class OpenerDirector: - addheaders: list[tuple[str, str]] - def add_handler(self, handler: BaseHandler) -> None: ... - def open(self, fullurl: str | Request, data: _DataType = None, timeout: float | None = ...) -> _UrlopenRet: ... - def error(self, proto: str, *args: Any) -> _UrlopenRet: ... - def close(self) -> None: ... - -class BaseHandler: - handler_order: ClassVar[int] - parent: OpenerDirector - def add_parent(self, parent: OpenerDirector) -> None: ... - def close(self) -> None: ... - def __lt__(self, other: object) -> bool: ... - -class HTTPDefaultErrorHandler(BaseHandler): - def http_error_default( - self, req: Request, fp: IO[bytes], code: int, msg: str, hdrs: HTTPMessage - ) -> HTTPError: ... # undocumented - -class HTTPRedirectHandler(BaseHandler): - max_redirections: ClassVar[int] # undocumented - max_repeats: ClassVar[int] # undocumented - inf_msg: ClassVar[str] # undocumented - def redirect_request( - self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage, newurl: str - ) -> Request | None: ... - def http_error_301(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - def http_error_302(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - def http_error_303(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - def http_error_307(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - if sys.version_info >= (3, 11): - def http_error_308( - self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage - ) -> _UrlopenRet | None: ... - -class HTTPCookieProcessor(BaseHandler): - cookiejar: CookieJar - def __init__(self, cookiejar: CookieJar | None = None) -> None: ... - def http_request(self, request: Request) -> Request: ... # undocumented - def http_response(self, request: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented - def https_request(self, request: Request) -> Request: ... # undocumented - def https_response(self, request: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented - -class ProxyHandler(BaseHandler): - def __init__(self, proxies: dict[str, str] | None = None) -> None: ... - def proxy_open(self, req: Request, proxy: str, type: str) -> _UrlopenRet | None: ... # undocumented - # TODO: add a method for every (common) proxy protocol - -class HTTPPasswordMgr: - def add_password(self, realm: str, uri: str | Sequence[str], user: str, passwd: str) -> None: ... - def find_user_password(self, realm: str, authuri: str) -> tuple[str | None, str | None]: ... - def is_suburi(self, base: str, test: str) -> bool: ... # undocumented - def reduce_uri(self, uri: str, default_port: bool = True) -> tuple[str, str]: ... # undocumented - -class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr): - def add_password(self, realm: str | None, uri: str | Sequence[str], user: str, passwd: str) -> None: ... - def find_user_password(self, realm: str | None, authuri: str) -> tuple[str | None, str | None]: ... - -class HTTPPasswordMgrWithPriorAuth(HTTPPasswordMgrWithDefaultRealm): - def add_password( - self, realm: str | None, uri: str | Sequence[str], user: str, passwd: str, is_authenticated: bool = False - ) -> None: ... - def update_authenticated(self, uri: str | Sequence[str], is_authenticated: bool = False) -> None: ... - def is_authenticated(self, authuri: str) -> bool | None: ... - -class AbstractBasicAuthHandler: - rx: ClassVar[Pattern[str]] # undocumented - passwd: HTTPPasswordMgr - add_password: Callable[[str, str | Sequence[str], str, str], None] - def __init__(self, password_mgr: HTTPPasswordMgr | None = None) -> None: ... - def http_error_auth_reqed(self, authreq: str, host: str, req: Request, headers: HTTPMessage) -> None: ... - def http_request(self, req: Request) -> Request: ... # undocumented - def http_response(self, req: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented - def https_request(self, req: Request) -> Request: ... # undocumented - def https_response(self, req: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented - def retry_http_basic_auth(self, host: str, req: Request, realm: str) -> _UrlopenRet | None: ... # undocumented - -class HTTPBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): - auth_header: ClassVar[str] # undocumented - def http_error_401(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - -class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): - auth_header: ClassVar[str] - def http_error_407(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - -class AbstractDigestAuthHandler: - def __init__(self, passwd: HTTPPasswordMgr | None = None) -> None: ... - def reset_retry_count(self) -> None: ... - def http_error_auth_reqed(self, auth_header: str, host: str, req: Request, headers: HTTPMessage) -> None: ... - def retry_http_digest_auth(self, req: Request, auth: str) -> _UrlopenRet | None: ... - def get_cnonce(self, nonce: str) -> str: ... - def get_authorization(self, req: Request, chal: Mapping[str, str]) -> str | None: ... - def get_algorithm_impls(self, algorithm: str) -> tuple[Callable[[str], str], Callable[[str, str], str]]: ... - def get_entity_digest(self, data: ReadableBuffer | None, chal: Mapping[str, str]) -> str | None: ... - -class HTTPDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): - auth_header: ClassVar[str] # undocumented - def http_error_401(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - -class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): - auth_header: ClassVar[str] # undocumented - def http_error_407(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... - -class _HTTPConnectionProtocol(Protocol): - def __call__( - self, - host: str, - /, - *, - port: int | None = ..., - timeout: float = ..., - source_address: tuple[str, int] | None = ..., - blocksize: int = ..., - ) -> HTTPConnection: ... - -class AbstractHTTPHandler(BaseHandler): # undocumented - if sys.version_info >= (3, 12): - def __init__(self, debuglevel: int | None = None) -> None: ... - else: - def __init__(self, debuglevel: int = 0) -> None: ... - - def set_http_debuglevel(self, level: int) -> None: ... - def do_request_(self, request: Request) -> Request: ... - def do_open(self, http_class: _HTTPConnectionProtocol, req: Request, **http_conn_args: Any) -> HTTPResponse: ... - -class HTTPHandler(AbstractHTTPHandler): - def http_open(self, req: Request) -> HTTPResponse: ... - def http_request(self, request: Request) -> Request: ... # undocumented - -class HTTPSHandler(AbstractHTTPHandler): - if sys.version_info >= (3, 12): - def __init__( - self, debuglevel: int | None = None, context: ssl.SSLContext | None = None, check_hostname: bool | None = None - ) -> None: ... - else: - def __init__( - self, debuglevel: int = 0, context: ssl.SSLContext | None = None, check_hostname: bool | None = None - ) -> None: ... - - def https_open(self, req: Request) -> HTTPResponse: ... - def https_request(self, request: Request) -> Request: ... # undocumented - -class FileHandler(BaseHandler): - names: ClassVar[tuple[str, ...] | None] # undocumented - def file_open(self, req: Request) -> addinfourl: ... - def get_names(self) -> tuple[str, ...]: ... # undocumented - def open_local_file(self, req: Request) -> addinfourl: ... # undocumented - -class DataHandler(BaseHandler): - def data_open(self, req: Request) -> addinfourl: ... - -class ftpwrapper: # undocumented - def __init__( - self, user: str, passwd: str, host: str, port: int, dirs: str, timeout: float | None = None, persistent: bool = True - ) -> None: ... - def close(self) -> None: ... - def endtransfer(self) -> None: ... - def file_close(self) -> None: ... - def init(self) -> None: ... - def real_close(self) -> None: ... - def retrfile(self, file: str, type: str) -> tuple[addclosehook, int | None]: ... - -class FTPHandler(BaseHandler): - def ftp_open(self, req: Request) -> addinfourl: ... - def connect_ftp( - self, user: str, passwd: str, host: str, port: int, dirs: str, timeout: float - ) -> ftpwrapper: ... # undocumented - -class CacheFTPHandler(FTPHandler): - def setTimeout(self, t: float) -> None: ... - def setMaxConns(self, m: int) -> None: ... - def check_cache(self) -> None: ... # undocumented - def clear_cache(self) -> None: ... # undocumented - -class UnknownHandler(BaseHandler): - def unknown_open(self, req: Request) -> NoReturn: ... - -class HTTPErrorProcessor(BaseHandler): - def http_response(self, request: Request, response: HTTPResponse) -> _UrlopenRet: ... - def https_response(self, request: Request, response: HTTPResponse) -> _UrlopenRet: ... - -def urlretrieve( - url: str, - filename: StrOrBytesPath | None = None, - reporthook: Callable[[int, int, int], object] | None = None, - data: _DataType = None, -) -> tuple[str, HTTPMessage]: ... -def urlcleanup() -> None: ... - -class URLopener: - version: ClassVar[str] - def __init__(self, proxies: dict[str, str] | None = None, **x509: str) -> None: ... - def open(self, fullurl: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... - def open_unknown(self, fullurl: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... - def retrieve( - self, - url: str, - filename: str | None = None, - reporthook: Callable[[int, int, int], object] | None = None, - data: ReadableBuffer | None = None, - ) -> tuple[str, Message | None]: ... - def addheader(self, *args: tuple[str, str]) -> None: ... # undocumented - def cleanup(self) -> None: ... # undocumented - def close(self) -> None: ... # undocumented - def http_error( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: bytes | None = None - ) -> _UrlopenRet: ... # undocumented - def http_error_default( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage - ) -> _UrlopenRet: ... # undocumented - def open_data(self, url: str, data: ReadableBuffer | None = None) -> addinfourl: ... # undocumented - def open_file(self, url: str) -> addinfourl: ... # undocumented - def open_ftp(self, url: str) -> addinfourl: ... # undocumented - def open_http(self, url: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... # undocumented - def open_https(self, url: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... # undocumented - def open_local_file(self, url: str) -> addinfourl: ... # undocumented - def open_unknown_proxy(self, proxy: str, fullurl: str, data: ReadableBuffer | None = None) -> None: ... # undocumented - def __del__(self) -> None: ... - -class FancyURLopener(URLopener): - def prompt_user_passwd(self, host: str, realm: str) -> tuple[str, str]: ... - def get_user_passwd(self, host: str, realm: str, clear_cache: int = 0) -> tuple[str, str]: ... # undocumented - def http_error_301( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None - ) -> _UrlopenRet | addinfourl | None: ... # undocumented - def http_error_302( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None - ) -> _UrlopenRet | addinfourl | None: ... # undocumented - def http_error_303( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None - ) -> _UrlopenRet | addinfourl | None: ... # undocumented - def http_error_307( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None - ) -> _UrlopenRet | addinfourl | None: ... # undocumented - if sys.version_info >= (3, 11): - def http_error_308( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None - ) -> _UrlopenRet | addinfourl | None: ... # undocumented - - def http_error_401( - self, - url: str, - fp: IO[bytes], - errcode: int, - errmsg: str, - headers: HTTPMessage, - data: ReadableBuffer | None = None, - retry: bool = False, - ) -> _UrlopenRet | None: ... # undocumented - def http_error_407( - self, - url: str, - fp: IO[bytes], - errcode: int, - errmsg: str, - headers: HTTPMessage, - data: ReadableBuffer | None = None, - retry: bool = False, - ) -> _UrlopenRet | None: ... # undocumented - def http_error_default( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage - ) -> addinfourl: ... # undocumented - def redirect_internal( - self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None - ) -> _UrlopenRet | None: ... # undocumented - def retry_http_basic_auth( - self, url: str, realm: str, data: ReadableBuffer | None = None - ) -> _UrlopenRet | None: ... # undocumented - def retry_https_basic_auth( - self, url: str, realm: str, data: ReadableBuffer | None = None - ) -> _UrlopenRet | None: ... # undocumented - def retry_proxy_http_basic_auth( - self, url: str, realm: str, data: ReadableBuffer | None = None - ) -> _UrlopenRet | None: ... # undocumented - def retry_proxy_https_basic_auth( - self, url: str, realm: str, data: ReadableBuffer | None = None - ) -> _UrlopenRet | None: ... # undocumented diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi deleted file mode 100644 index a2eecc5a78641..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from _typeshed import ReadableBuffer, StrPath, SupportsRead, _T_co -from collections.abc import Iterable -from typing import Protocol -from typing_extensions import TypeAlias -from xml.sax._exceptions import ( - SAXException as SAXException, - SAXNotRecognizedException as SAXNotRecognizedException, - SAXNotSupportedException as SAXNotSupportedException, - SAXParseException as SAXParseException, - SAXReaderNotAvailable as SAXReaderNotAvailable, -) -from xml.sax.handler import ContentHandler as ContentHandler, ErrorHandler as ErrorHandler -from xml.sax.xmlreader import XMLReader - -class _SupportsReadClose(SupportsRead[_T_co], Protocol[_T_co]): - def close(self) -> None: ... - -_Source: TypeAlias = StrPath | _SupportsReadClose[bytes] | _SupportsReadClose[str] - -default_parser_list: list[str] - -def make_parser(parser_list: Iterable[str] = ()) -> XMLReader: ... -def parse(source: _Source, handler: ContentHandler, errorHandler: ErrorHandler = ...) -> None: ... -def parseString(string: ReadableBuffer | str, handler: ContentHandler, errorHandler: ErrorHandler | None = ...) -> None: ... -def _create_parser(parser_name: str) -> XMLReader: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi deleted file mode 100644 index ede732c0f86ae..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi +++ /dev/null @@ -1,370 +0,0 @@ -import io -import sys -from _typeshed import SizedBuffer, StrOrBytesPath, StrPath -from collections.abc import Callable, Iterable, Iterator -from io import TextIOWrapper -from os import PathLike -from types import TracebackType -from typing import IO, Final, Literal, Protocol, overload -from typing_extensions import Self, TypeAlias - -__all__ = [ - "BadZipFile", - "BadZipfile", - "Path", - "error", - "ZIP_STORED", - "ZIP_DEFLATED", - "ZIP_BZIP2", - "ZIP_LZMA", - "is_zipfile", - "ZipInfo", - "ZipFile", - "PyZipFile", - "LargeZipFile", -] - -# TODO: use TypeAlias for these two when mypy bugs are fixed -# https://github.com/python/mypy/issues/16581 -_DateTuple = tuple[int, int, int, int, int, int] # noqa: Y026 -_ZipFileMode = Literal["r", "w", "x", "a"] # noqa: Y026 - -_ReadWriteMode: TypeAlias = Literal["r", "w"] - -class BadZipFile(Exception): ... - -BadZipfile = BadZipFile -error = BadZipfile - -class LargeZipFile(Exception): ... - -class _ZipStream(Protocol): - def read(self, n: int, /) -> bytes: ... - # The following methods are optional: - # def seekable(self) -> bool: ... - # def tell(self) -> int: ... - # def seek(self, n: int, /) -> object: ... - -# Stream shape as required by _EndRecData() and _EndRecData64(). -class _SupportsReadSeekTell(Protocol): - def read(self, n: int = ..., /) -> bytes: ... - def seek(self, cookie: int, whence: int, /) -> object: ... - def tell(self) -> int: ... - -class _ClosableZipStream(_ZipStream, Protocol): - def close(self) -> object: ... - -class ZipExtFile(io.BufferedIOBase): - MAX_N: int - MIN_READ_SIZE: int - MAX_SEEK_READ: int - newlines: list[bytes] | None - mode: _ReadWriteMode - name: str - @overload - def __init__( - self, fileobj: _ClosableZipStream, mode: _ReadWriteMode, zipinfo: ZipInfo, pwd: bytes | None, close_fileobj: Literal[True] - ) -> None: ... - @overload - def __init__( - self, - fileobj: _ClosableZipStream, - mode: _ReadWriteMode, - zipinfo: ZipInfo, - pwd: bytes | None = None, - *, - close_fileobj: Literal[True], - ) -> None: ... - @overload - def __init__( - self, - fileobj: _ZipStream, - mode: _ReadWriteMode, - zipinfo: ZipInfo, - pwd: bytes | None = None, - close_fileobj: Literal[False] = False, - ) -> None: ... - def read(self, n: int | None = -1) -> bytes: ... - def readline(self, limit: int = -1) -> bytes: ... # type: ignore[override] - def peek(self, n: int = 1) -> bytes: ... - def read1(self, n: int | None) -> bytes: ... # type: ignore[override] - def seek(self, offset: int, whence: int = 0) -> int: ... - -class _Writer(Protocol): - def write(self, s: str, /) -> object: ... - -class _ZipReadable(Protocol): - def seek(self, offset: int, whence: int = 0, /) -> int: ... - def read(self, n: int = -1, /) -> bytes: ... - -class _ZipTellable(Protocol): - def tell(self) -> int: ... - -class _ZipReadableTellable(_ZipReadable, _ZipTellable, Protocol): ... - -class _ZipWritable(Protocol): - def flush(self) -> None: ... - def close(self) -> None: ... - def write(self, b: bytes, /) -> int: ... - -class ZipFile: - filename: str | None - debug: int - comment: bytes - filelist: list[ZipInfo] - fp: IO[bytes] | None - NameToInfo: dict[str, ZipInfo] - start_dir: int # undocumented - compression: int # undocumented - compresslevel: int | None # undocumented - mode: _ZipFileMode # undocumented - pwd: bytes | None # undocumented - # metadata_encoding is new in 3.11 - if sys.version_info >= (3, 11): - @overload - def __init__( - self, - file: StrPath | IO[bytes], - mode: _ZipFileMode = "r", - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - metadata_encoding: str | None = None, - ) -> None: ... - # metadata_encoding is only allowed for read mode - @overload - def __init__( - self, - file: StrPath | _ZipReadable, - mode: Literal["r"] = "r", - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - metadata_encoding: str | None = None, - ) -> None: ... - @overload - def __init__( - self, - file: StrPath | _ZipWritable, - mode: Literal["w", "x"] = ..., - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - metadata_encoding: None = None, - ) -> None: ... - @overload - def __init__( - self, - file: StrPath | _ZipReadableTellable, - mode: Literal["a"] = ..., - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - metadata_encoding: None = None, - ) -> None: ... - else: - @overload - def __init__( - self, - file: StrPath | IO[bytes], - mode: _ZipFileMode = "r", - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - ) -> None: ... - @overload - def __init__( - self, - file: StrPath | _ZipReadable, - mode: Literal["r"] = "r", - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - ) -> None: ... - @overload - def __init__( - self, - file: StrPath | _ZipWritable, - mode: Literal["w", "x"] = ..., - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - ) -> None: ... - @overload - def __init__( - self, - file: StrPath | _ZipReadableTellable, - mode: Literal["a"] = ..., - compression: int = 0, - allowZip64: bool = True, - compresslevel: int | None = None, - *, - strict_timestamps: bool = True, - ) -> None: ... - - def __enter__(self) -> Self: ... - def __exit__( - self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> None: ... - def close(self) -> None: ... - def getinfo(self, name: str) -> ZipInfo: ... - def infolist(self) -> list[ZipInfo]: ... - def namelist(self) -> list[str]: ... - def open( - self, name: str | ZipInfo, mode: _ReadWriteMode = "r", pwd: bytes | None = None, *, force_zip64: bool = False - ) -> IO[bytes]: ... - def extract(self, member: str | ZipInfo, path: StrPath | None = None, pwd: bytes | None = None) -> str: ... - def extractall( - self, path: StrPath | None = None, members: Iterable[str | ZipInfo] | None = None, pwd: bytes | None = None - ) -> None: ... - def printdir(self, file: _Writer | None = None) -> None: ... - def setpassword(self, pwd: bytes) -> None: ... - def read(self, name: str | ZipInfo, pwd: bytes | None = None) -> bytes: ... - def testzip(self) -> str | None: ... - def write( - self, - filename: StrPath, - arcname: StrPath | None = None, - compress_type: int | None = None, - compresslevel: int | None = None, - ) -> None: ... - def writestr( - self, - zinfo_or_arcname: str | ZipInfo, - data: SizedBuffer | str, - compress_type: int | None = None, - compresslevel: int | None = None, - ) -> None: ... - if sys.version_info >= (3, 11): - def mkdir(self, zinfo_or_directory_name: str | ZipInfo, mode: int = 0o777) -> None: ... - - def __del__(self) -> None: ... - -class PyZipFile(ZipFile): - def __init__( - self, file: str | IO[bytes], mode: _ZipFileMode = "r", compression: int = 0, allowZip64: bool = True, optimize: int = -1 - ) -> None: ... - def writepy(self, pathname: str, basename: str = "", filterfunc: Callable[[str], bool] | None = None) -> None: ... - -class ZipInfo: - filename: str - date_time: _DateTuple - compress_type: int - comment: bytes - extra: bytes - create_system: int - create_version: int - extract_version: int - reserved: int - flag_bits: int - volume: int - internal_attr: int - external_attr: int - header_offset: int - CRC: int - compress_size: int - file_size: int - orig_filename: str # undocumented - if sys.version_info >= (3, 13): - compress_level: int | None - - def __init__(self, filename: str = "NoName", date_time: _DateTuple = (1980, 1, 1, 0, 0, 0)) -> None: ... - @classmethod - def from_file(cls, filename: StrPath, arcname: StrPath | None = None, *, strict_timestamps: bool = True) -> Self: ... - def is_dir(self) -> bool: ... - def FileHeader(self, zip64: bool | None = None) -> bytes: ... - -if sys.version_info >= (3, 12): - from zipfile._path import CompleteDirs as CompleteDirs, Path as Path - -else: - class CompleteDirs(ZipFile): - def resolve_dir(self, name: str) -> str: ... - @overload - @classmethod - def make(cls, source: ZipFile) -> CompleteDirs: ... - @overload - @classmethod - def make(cls, source: StrPath | IO[bytes]) -> Self: ... - - class Path: - root: CompleteDirs - at: str - def __init__(self, root: ZipFile | StrPath | IO[bytes], at: str = "") -> None: ... - @property - def name(self) -> str: ... - @property - def parent(self) -> PathLike[str]: ... # undocumented - if sys.version_info >= (3, 10): - @property - def filename(self) -> PathLike[str]: ... # undocumented - if sys.version_info >= (3, 11): - @property - def suffix(self) -> str: ... - @property - def suffixes(self) -> list[str]: ... - @property - def stem(self) -> str: ... - - @overload - def open( - self, - mode: Literal["r", "w"] = "r", - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - line_buffering: bool = ..., - write_through: bool = ..., - *, - pwd: bytes | None = None, - ) -> TextIOWrapper: ... - @overload - def open(self, mode: Literal["rb", "wb"], *, pwd: bytes | None = None) -> IO[bytes]: ... - - if sys.version_info >= (3, 10): - def iterdir(self) -> Iterator[Self]: ... - else: - def iterdir(self) -> Iterator[Path]: ... - - def is_dir(self) -> bool: ... - def is_file(self) -> bool: ... - def exists(self) -> bool: ... - def read_text( - self, - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - line_buffering: bool = ..., - write_through: bool = ..., - ) -> str: ... - def read_bytes(self) -> bytes: ... - if sys.version_info >= (3, 10): - def joinpath(self, *other: StrPath) -> Path: ... - else: - def joinpath(self, add: StrPath) -> Path: ... # undocumented - - def __truediv__(self, add: StrPath) -> Path: ... - -def is_zipfile(filename: StrOrBytesPath | _SupportsReadSeekTell) -> bool: ... - -ZIP_STORED: Final[int] -ZIP_DEFLATED: Final[int] -ZIP64_LIMIT: Final[int] -ZIP_FILECOUNT_LIMIT: Final[int] -ZIP_MAX_COMMENT: Final[int] -ZIP_BZIP2: Final[int] -ZIP_LZMA: Final[int] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi deleted file mode 100644 index 35381758a1b7e..0000000000000 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi +++ /dev/null @@ -1,28 +0,0 @@ -from collections.abc import Iterable -from datetime import datetime, timedelta, tzinfo -from typing_extensions import Self -from zoneinfo._common import ZoneInfoNotFoundError as ZoneInfoNotFoundError, _IOBytes -from zoneinfo._tzpath import ( - TZPATH as TZPATH, - InvalidTZPathWarning as InvalidTZPathWarning, - available_timezones as available_timezones, - reset_tzpath as reset_tzpath, -) - -__all__ = ["ZoneInfo", "reset_tzpath", "available_timezones", "TZPATH", "ZoneInfoNotFoundError", "InvalidTZPathWarning"] - -class ZoneInfo(tzinfo): - @property - def key(self) -> str: ... - def __new__(cls, key: str) -> Self: ... - @classmethod - def no_cache(cls, key: str) -> Self: ... - @classmethod - def from_file(cls, fobj: _IOBytes, /, key: str | None = None) -> Self: ... - @classmethod - def clear_cache(cls, *, only_keys: Iterable[str] | None = None) -> None: ... - def tzname(self, dt: datetime | None, /) -> str | None: ... - def utcoffset(self, dt: datetime | None, /) -> timedelta | None: ... - def dst(self, dt: datetime | None, /) -> timedelta | None: ... - -def __dir__() -> list[str]: ... diff --git a/crates/red_knot_wasm/Cargo.toml b/crates/red_knot_wasm/Cargo.toml deleted file mode 100644 index 14bc206629959..0000000000000 --- a/crates/red_knot_wasm/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "red_knot_wasm" -version = "0.0.0" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } -description = "WebAssembly bindings for Red Knot" - -[lib] -crate-type = ["cdylib", "rlib"] -doctest = false - -[features] -default = ["console_error_panic_hook"] - -[dependencies] -red_knot_ide = { workspace = true } -red_knot_project = { workspace = true, default-features = false, features = [ - "deflate", - "format" -] } -red_knot_python_semantic = { workspace = true } - -ruff_db = { workspace = true, default-features = false, features = [] } -ruff_notebook = { workspace = true } -ruff_python_formatter = { workspace = true } -ruff_source_file = { workspace = true } -ruff_text_size = { workspace = true } - -console_error_panic_hook = { workspace = true, optional = true } -console_log = { workspace = true } -js-sys = { workspace = true } -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 } - -[dev-dependencies] -wasm-bindgen-test = { workspace = true } - -[lints] -workspace = true diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/red_knot_wasm/src/lib.rs deleted file mode 100644 index ed96760409b84..0000000000000 --- a/crates/red_knot_wasm/src/lib.rs +++ /dev/null @@ -1,629 +0,0 @@ -use std::any::Any; - -use js_sys::{Error, JsString}; -use red_knot_ide::{goto_type_definition, hover, inlay_hints, MarkupKind}; -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::{self, DisplayDiagnosticConfig}; -use ruff_db::files::{system_path_to_file, File, FileRange}; -use ruff_db::source::{line_index, source_text}; -use ruff_db::system::walk_directory::WalkDirectoryBuilder; -use ruff_db::system::{ - CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System, - SystemPath, SystemPathBuf, SystemVirtualPath, -}; -use ruff_db::Upcast; -use ruff_notebook::Notebook; -use ruff_python_formatter::formatted_file; -use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; -use ruff_text_size::{Ranged, TextSize}; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(start)] -pub fn run() { - use log::Level; - - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); - - console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong."); -} - -#[wasm_bindgen] -pub struct Workspace { - db: ProjectDatabase, - system: WasmSystem, -} - -#[wasm_bindgen] -impl Workspace { - #[wasm_bindgen(constructor)] - pub fn new(root: &str, options: JsValue) -> Result { - 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 project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None) - .map_err(into_error)?; - - let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?; - - Ok(Self { db, system }) - } - - #[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)?; - - 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 { - let path = SystemPath::absolute(path, self.db.project().root(&self.db)); - - self.system - .fs - .write_file_all(&path, contents) - .map_err(into_error)?; - - self.db.apply_changes( - vec![ChangeEvent::Created { - path: path.clone(), - kind: CreatedKind::File, - }], - None, - ); - - let file = system_path_to_file(&self.db, &path).expect("File to exist"); - - self.db.project().open_file(&mut self.db, file); - - Ok(FileHandle { path, file }) - } - - #[wasm_bindgen(js_name = "updateFile")] - pub fn update_file(&mut self, file_id: &FileHandle, contents: &str) -> Result<(), Error> { - if !self.system.fs.exists(&file_id.path) { - return Err(Error::new("File does not exist")); - } - - self.system - .fs - .write_file(&file_id.path, contents) - .map_err(into_error)?; - - self.db.apply_changes( - vec![ - ChangeEvent::Changed { - path: file_id.path.to_path_buf(), - kind: ChangedKind::FileContent, - }, - ChangeEvent::Changed { - path: file_id.path.to_path_buf(), - kind: ChangedKind::FileMetadata, - }, - ], - None, - ); - - Ok(()) - } - - #[wasm_bindgen(js_name = "closeFile")] - #[allow( - clippy::needless_pass_by_value, - reason = "It's intentional that the file handle is consumed because it is no longer valid after closing" - )] - pub fn close_file(&mut self, file_id: FileHandle) -> Result<(), Error> { - let file = file_id.file; - - self.db.project().close_file(&mut self.db, file); - self.system - .fs - .remove_file(&file_id.path) - .map_err(into_error)?; - - self.db.apply_changes( - vec![ChangeEvent::Deleted { - path: file_id.path.to_path_buf(), - kind: DeletedKind::File, - }], - None, - ); - - Ok(()) - } - - /// Checks a single file. - #[wasm_bindgen(js_name = "checkFile")] - pub fn check_file(&self, file_id: &FileHandle) -> Result, Error> { - let result = self.db.check_file(file_id.file).map_err(into_error)?; - - Ok(result.into_iter().map(Diagnostic::wrap).collect()) - } - - /// Checks all open files - pub fn check(&self) -> Result, Error> { - let result = self.db.check().map_err(into_error)?; - - Ok(result.into_iter().map(Diagnostic::wrap).collect()) - } - - /// Returns the parsed AST for `path` - pub fn parsed(&self, file_id: &FileHandle) -> Result { - let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file); - - Ok(format!("{:#?}", parsed.syntax())) - } - - pub fn format(&self, file_id: &FileHandle) -> Result, Error> { - formatted_file(&self.db, file_id.file).map_err(into_error) - } - - /// Returns the token stream for `path` serialized as a string. - pub fn tokens(&self, file_id: &FileHandle) -> Result { - let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file); - - Ok(format!("{:#?}", parsed.tokens())) - } - - #[wasm_bindgen(js_name = "sourceText")] - pub fn source_text(&self, file_id: &FileHandle) -> Result { - let source_text = ruff_db::source::source_text(&self.db, file_id.file); - - Ok(source_text.to_string()) - } - - #[wasm_bindgen(js_name = "gotoTypeDefinition")] - pub fn goto_type_definition( - &self, - file_id: &FileHandle, - position: Position, - ) -> Result, Error> { - let source = source_text(&self.db, file_id.file); - let index = line_index(&self.db, file_id.file); - - let offset = position.to_text_size(&source, &index)?; - - let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else { - return Ok(Vec::new()); - }; - - let source_range = Range::from_text_range(targets.file_range().range(), &index, &source); - - let links: Vec<_> = targets - .into_iter() - .map(|target| LocationLink { - path: target.file().path(&self.db).to_string(), - full_range: Range::from_file_range( - &self.db, - FileRange::new(target.file(), target.full_range()), - ), - selection_range: Some(Range::from_file_range( - &self.db, - FileRange::new(target.file(), target.focus_range()), - )), - origin_selection_range: Some(source_range), - }) - .collect(); - - Ok(links) - } - - #[wasm_bindgen] - pub fn hover(&self, file_id: &FileHandle, position: Position) -> Result, Error> { - let source = source_text(&self.db, file_id.file); - let index = line_index(&self.db, file_id.file); - - let offset = position.to_text_size(&source, &index)?; - - let Some(range_info) = hover(&self.db, file_id.file, offset) else { - return Ok(None); - }; - - let source_range = Range::from_text_range(range_info.file_range().range(), &index, &source); - - Ok(Some(Hover { - markdown: range_info - .display(&self.db, MarkupKind::Markdown) - .to_string(), - range: source_range, - })) - } - - #[wasm_bindgen(js_name = "inlayHints")] - pub fn inlay_hints(&self, file_id: &FileHandle, range: Range) -> Result, Error> { - let index = line_index(&self.db, file_id.file); - let source = source_text(&self.db, file_id.file); - - let result = inlay_hints( - &self.db, - file_id.file, - range.to_text_range(&index, &source)?, - ); - - Ok(result - .into_iter() - .map(|hint| InlayHint { - markdown: hint.display(&self.db).to_string(), - position: Position::from_text_size(hint.position, &index, &source), - }) - .collect()) - } -} - -pub(crate) fn into_error(err: E) -> Error { - Error::new(&err.to_string()) -} - -#[derive(Debug, Eq, PartialEq)] -#[wasm_bindgen(inspectable)] -pub struct FileHandle { - path: SystemPathBuf, - file: File, -} - -#[wasm_bindgen] -impl FileHandle { - #[wasm_bindgen(js_name = toString)] - pub fn js_to_string(&self) -> String { - format!("file(id: {:?}, path: {})", self.file, self.path) - } - - pub fn path(&self) -> String { - self.path.to_string() - } -} - -#[wasm_bindgen] -pub struct Diagnostic { - #[wasm_bindgen(readonly)] - inner: diagnostic::Diagnostic, -} - -#[wasm_bindgen] -impl Diagnostic { - fn wrap(diagnostic: diagnostic::Diagnostic) -> Self { - Self { inner: diagnostic } - } - - #[wasm_bindgen] - pub fn message(&self) -> JsString { - JsString::from(self.inner.concise_message().to_string()) - } - - #[wasm_bindgen] - pub fn id(&self) -> JsString { - JsString::from(self.inner.id().to_string()) - } - - #[wasm_bindgen] - pub fn severity(&self) -> Severity { - Severity::from(self.inner.severity()) - } - - #[wasm_bindgen(js_name = "textRange")] - pub fn text_range(&self) -> Option { - self.inner - .primary_span() - .and_then(|span| Some(TextRange::from(span.range()?))) - } - - #[wasm_bindgen(js_name = "toRange")] - pub fn to_range(&self, workspace: &Workspace) -> Option { - self.inner.primary_span().and_then(|span| { - Some(Range::from_file_range( - &workspace.db, - FileRange::new(span.file(), span.range()?), - )) - }) - } - - #[wasm_bindgen] - pub fn display(&self, workspace: &Workspace) -> JsString { - let config = DisplayDiagnosticConfig::default().color(false); - self.inner - .display(workspace.db.upcast(), &config) - .to_string() - .into() - } -} - -#[wasm_bindgen] -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub struct Range { - pub start: Position, - pub end: Position, -} - -#[wasm_bindgen] -impl Range { - #[wasm_bindgen(constructor)] - pub fn new(start: Position, end: Position) -> Self { - Self { start, end } - } -} - -impl Range { - fn from_file_range(db: &dyn Db, file_range: FileRange) -> Self { - let index = line_index(db.upcast(), file_range.file()); - let source = source_text(db.upcast(), file_range.file()); - - Self::from_text_range(file_range.range(), &index, &source) - } - - fn from_text_range( - text_range: ruff_text_size::TextRange, - line_index: &LineIndex, - source: &str, - ) -> Self { - Self { - start: Position::from_text_size(text_range.start(), line_index, source), - end: Position::from_text_size(text_range.end(), line_index, source), - } - } - - fn to_text_range( - self, - line_index: &LineIndex, - source: &str, - ) -> Result { - let start = self.start.to_text_size(source, line_index)?; - let end = self.end.to_text_size(source, line_index)?; - - Ok(ruff_text_size::TextRange::new(start, end)) - } -} - -impl From<(SourceLocation, SourceLocation)> for Range { - fn from((start, end): (SourceLocation, SourceLocation)) -> Self { - Self { - start: start.into(), - end: end.into(), - } - } -} - -#[wasm_bindgen] -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub struct Position { - /// One indexed line number - pub line: usize, - - /// One indexed column number (the nth character on the line) - pub column: usize, -} - -#[wasm_bindgen] -impl Position { - #[wasm_bindgen(constructor)] - pub fn new(line: usize, column: usize) -> Self { - Self { line, column } - } -} - -impl Position { - fn to_text_size(self, text: &str, index: &LineIndex) -> Result { - let text_size = index.offset( - OneIndexed::new(self.line).ok_or_else(|| { - Error::new("Invalid value `0` for `position.line`. The line index is 1-indexed.") - })?, - OneIndexed::new(self.column).ok_or_else(|| { - Error::new( - "Invalid value `0` for `position.column`. The column index is 1-indexed.", - ) - })?, - text, - ); - - Ok(text_size) - } - - fn from_text_size(offset: TextSize, line_index: &LineIndex, source: &str) -> Self { - line_index.source_location(offset, source).into() - } -} - -impl From for Position { - fn from(location: SourceLocation) -> Self { - Self { - line: location.row.get(), - column: location.column.get(), - } - } -} - -#[wasm_bindgen] -#[derive(Copy, Clone, Hash, PartialEq, Eq)] -pub enum Severity { - Info, - Warning, - Error, - Fatal, -} - -impl From for Severity { - fn from(value: diagnostic::Severity) -> Self { - match value { - diagnostic::Severity::Info => Self::Info, - diagnostic::Severity::Warning => Self::Warning, - diagnostic::Severity::Error => Self::Error, - diagnostic::Severity::Fatal => Self::Fatal, - } - } -} - -#[wasm_bindgen] -pub struct TextRange { - pub start: u32, - pub end: u32, -} - -impl From for TextRange { - fn from(value: ruff_text_size::TextRange) -> Self { - Self { - start: value.start().into(), - end: value.end().into(), - } - } -} - -#[wasm_bindgen] -pub struct LocationLink { - /// The target file path - #[wasm_bindgen(getter_with_clone)] - pub path: String, - - /// The full range of the target - pub full_range: Range, - /// The target's range that should be selected/highlighted - pub selection_range: Option, - /// The range of the origin. - pub origin_selection_range: Option, -} - -#[wasm_bindgen] -pub struct Hover { - #[wasm_bindgen(getter_with_clone)] - pub markdown: String, - - pub range: Range, -} - -#[wasm_bindgen] -pub struct InlayHint { - #[wasm_bindgen(getter_with_clone)] - pub markdown: String, - - pub position: Position, -} - -#[derive(Debug, Clone)] -struct WasmSystem { - fs: MemoryFileSystem, -} - -impl WasmSystem { - fn new(root: &SystemPath) -> Self { - Self { - fs: MemoryFileSystem::with_current_directory(root), - } - } -} - -impl System for WasmSystem { - fn path_metadata(&self, path: &SystemPath) -> ruff_db::system::Result { - self.fs.metadata(path) - } - - fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result { - self.fs.canonicalize(path) - } - - fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result { - self.fs.read_to_string(path) - } - - fn read_to_notebook( - &self, - path: &SystemPath, - ) -> Result { - let content = self.read_to_string(path)?; - Notebook::from_source_code(&content) - } - - fn read_virtual_path_to_string( - &self, - _path: &SystemVirtualPath, - ) -> ruff_db::system::Result { - Err(not_found()) - } - - fn read_virtual_path_to_notebook( - &self, - _path: &SystemVirtualPath, - ) -> Result { - Err(ruff_notebook::NotebookError::Io(not_found())) - } - - fn path_exists_case_sensitive(&self, path: &SystemPath, _prefix: &SystemPath) -> bool { - self.path_exists(path) - } - - fn case_sensitivity(&self) -> CaseSensitivity { - CaseSensitivity::CaseSensitive - } - - fn current_directory(&self) -> &SystemPath { - self.fs.current_directory() - } - - fn user_config_directory(&self) -> Option { - None - } - - fn read_directory<'a>( - &'a self, - path: &SystemPath, - ) -> ruff_db::system::Result< - Box> + 'a>, - > { - Ok(Box::new(self.fs.read_directory(path)?)) - } - - fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder { - self.fs.walk_directory(path) - } - - fn glob( - &self, - pattern: &str, - ) -> Result>>, PatternError> { - Ok(Box::new(self.fs.glob(pattern)?)) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -fn not_found() -> std::io::Error { - std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory") -} diff --git a/crates/red_knot_wasm/tests/api.rs b/crates/red_knot_wasm/tests/api.rs deleted file mode 100644 index 65ba79b28331c..0000000000000 --- a/crates/red_knot_wasm/tests/api.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use red_knot_wasm::{Position, Workspace}; -use wasm_bindgen_test::wasm_bindgen_test; - -#[wasm_bindgen_test] -fn check() { - let mut workspace = - Workspace::new("/", js_sys::JSON::parse("{}").unwrap()).expect("Workspace to be created"); - - workspace - .open_file("test.py", "import random22\n") - .expect("File to be opened"); - - let result = workspace.check().expect("Check to succeed"); - - assert_eq!(result.len(), 1); - - let diagnostic = &result[0]; - - assert_eq!(diagnostic.id(), "lint:unresolved-import"); - assert_eq!( - diagnostic.to_range(&workspace).unwrap().start, - Position { line: 1, column: 8 } - ); - assert_eq!(diagnostic.message(), "Cannot resolve import `random22`"); -} diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 16a60d1d4c4b1..c2572fc13c91e 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.11.6" +version = "0.12.3" publish = true authors = { workspace = true } edition = { workspace = true } @@ -20,6 +20,7 @@ ruff_graph = { workspace = true, features = ["serde", "clap"] } ruff_linter = { workspace = true, features = ["clap"] } ruff_macros = { workspace = true } ruff_notebook = { workspace = true } +ruff_options_metadata = { workspace = true, features = ["serde"] } ruff_python_ast = { workspace = true } ruff_python_formatter = { workspace = true } ruff_python_parser = { workspace = true } @@ -30,7 +31,7 @@ ruff_workspace = { workspace = true } anyhow = { workspace = true } argfile = { workspace = true } -bincode = { workspace = true } +bincode = { workspace = true, features = ["serde"] } bitflags = { workspace = true } cachedir = { workspace = true } clap = { workspace = true, features = ["derive", "env", "wrap_help"] } @@ -67,6 +68,7 @@ ruff_linter = { workspace = true, features = ["clap", "test-rules"] } assert_fs = { workspace = true } # Avoid writing colored snapshots when running tests from the terminal colored = { workspace = true, features = ["no-color"] } +dunce = { workspace = true } indoc = { workspace = true } insta = { workspace = true, features = ["filters", "json"] } insta-cmd = { workspace = true } @@ -77,10 +79,13 @@ test-case = { workspace = true } # Used via macro expansion. ignored = ["jiff"] +[package.metadata.dist] +dist = true + [target.'cfg(target_os = "windows")'.dependencies] mimalloc = { workspace = true } -[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies] +[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies] tikv-jemallocator = { workspace = true } [lints] diff --git a/crates/ruff/build.rs b/crates/ruff/build.rs index db70699e7e755..befb62375c374 100644 --- a/crates/ruff/build.rs +++ b/crates/ruff/build.rs @@ -13,7 +13,6 @@ fn main() { commit_info(&workspace_root); - #[allow(clippy::disallowed_methods)] let target = std::env::var("TARGET").unwrap(); println!("cargo::rustc-env=RUST_HOST_TARGET={target}"); } diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index fa4655de57a42..45db84d0536e8 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -5,9 +5,10 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; +use crate::commands::completions::config::{OptionString, OptionStringParser}; use anyhow::bail; use clap::builder::{TypedValueParser, ValueParserFactory}; -use clap::{command, Parser, Subcommand}; +use clap::{Parser, Subcommand, command}; use colored::Colorize; use itertools::Itertools; use path_absolutize::path_dedot; @@ -21,18 +22,16 @@ use ruff_linter::settings::types::{ PythonVersion, UnsafeFixes, }; use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser}; +use ruff_options_metadata::{OptionEntry, OptionsMetadata}; use ruff_python_ast as ast; -use ruff_source_file::{LineIndex, OneIndexed}; +use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding}; use ruff_text_size::TextRange; use ruff_workspace::configuration::{Configuration, RuleSelection}; use ruff_workspace::options::{Options, PycodestyleOptions}; -use ruff_workspace::options_base::{OptionEntry, OptionsMetadata}; use ruff_workspace::resolver::ConfigurationTransformer; use rustc_hash::FxHashMap; use toml; -use crate::commands::completions::config::{OptionString, OptionStringParser}; - /// All configuration options that can be passed "globally", /// i.e., can be passed to all subcommands #[derive(Debug, Default, Clone, clap::Args)] @@ -94,7 +93,7 @@ pub struct Args { pub(crate) global_options: GlobalConfigArgs, } -#[allow(clippy::large_enum_variant)] +#[expect(clippy::large_enum_variant)] #[derive(Debug, clap::Subcommand)] pub enum Command { /// Run Ruff on the given files or directories. @@ -178,11 +177,14 @@ pub struct AnalyzeGraphCommand { /// The minimum Python version that should be supported. #[arg(long, value_enum)] target_version: Option, + /// Path to a virtual environment to use for resolving additional dependencies + #[arg(long)] + python: Option, } // The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient #[derive(Clone, Debug, clap::Parser)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct CheckCommand { /// List of files or directories to check. #[clap(help = "List of files or directories to check [default: .]")] @@ -444,7 +446,7 @@ pub struct CheckCommand { } #[derive(Clone, Debug, clap::Parser)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct FormatCommand { /// List of files or directories to format. #[clap(help = "List of files or directories to format [default: .]")] @@ -558,7 +560,7 @@ pub enum HelpFormat { Json, } -#[allow(clippy::module_name_repetitions)] +#[expect(clippy::module_name_repetitions)] #[derive(Debug, Default, Clone, clap::Args)] pub struct LogLevelArgs { /// Enable verbose logging. @@ -797,6 +799,7 @@ impl AnalyzeGraphCommand { let format_arguments = AnalyzeGraphArgs { files: self.files, direction: self.direction, + python: self.python, }; let cli_overrides = ExplicitConfigOverrides { @@ -1028,7 +1031,7 @@ Possible choices: /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct CheckArguments { pub add_noqa: bool, pub diff: bool, @@ -1047,7 +1050,7 @@ pub struct CheckArguments { /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct FormatArguments { pub check: bool, pub no_cache: bool, @@ -1070,8 +1073,9 @@ impl FormatRange { /// /// Returns an empty range if the start range is past the end of `source`. pub(super) fn to_text_range(self, source: &str, line_index: &LineIndex) -> TextRange { - let start_byte_offset = line_index.offset(self.start.line, self.start.column, source); - let end_byte_offset = line_index.offset(self.end.line, self.end.column, source); + let start_byte_offset = + line_index.offset(self.start.into(), source, PositionEncoding::Utf32); + let end_byte_offset = line_index.offset(self.end.into(), source, PositionEncoding::Utf32); TextRange::new(start_byte_offset, end_byte_offset) } @@ -1122,10 +1126,10 @@ impl std::fmt::Display for FormatRangeParseError { write!( f, "the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'", - start_invalid=start.to_string().bold().yellow(), - end_invalid=end.to_string().bold().yellow(), - start=start.to_string().green().bold(), - end=end.to_string().green().bold() + start_invalid = start.to_string().bold().yellow(), + end_invalid = end.to_string().bold().yellow(), + start = start.to_string().green().bold(), + end = end.to_string().green().bold() ) } FormatRangeParseError::InvalidStart(inner) => inner.write(f, true), @@ -1142,6 +1146,15 @@ pub struct LineColumn { pub column: OneIndexed, } +impl From for ruff_source_file::SourceLocation { + fn from(value: LineColumn) -> Self { + Self { + line: value.line, + character_offset: value.column, + } + } +} + impl std::fmt::Display for LineColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{line}:{column}", line = self.line, column = self.column) @@ -1217,30 +1230,36 @@ impl LineColumnParseError { match self { LineColumnParseError::ColumnParseError(inner) => { - write!(f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'.") + write!( + f, + "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'." + ) } LineColumnParseError::LineParseError(inner) => { - write!(f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'.") + write!( + f, + "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'." + ) } LineColumnParseError::ZeroColumnIndex { line } => { write!( f, "the {range} column is 0, but it should be 1 or greater.\n {tip} The column numbers start at 1.\n {tip} Try {suggestion} instead.", - suggestion=format!("{line}:1").green().bold() + suggestion = format!("{line}:1").green().bold() ) } LineColumnParseError::ZeroLineIndex { column } => { write!( f, "the {range} line is 0, but it should be 1 or greater.\n {tip} The line numbers start at 1.\n {tip} Try {suggestion} instead.", - suggestion=format!("1:{column}").green().bold() + suggestion = format!("1:{column}").green().bold() ) } LineColumnParseError::ZeroLineAndColumnIndex => { write!( f, "the {range} line and column are both 0, but they should be 1 or greater.\n {tip} The line and column numbers start at 1.\n {tip} Try {suggestion} instead.", - suggestion="1:1".to_string().green().bold() + suggestion = "1:1".to_string().green().bold() ) } } @@ -1252,12 +1271,12 @@ impl LineColumnParseError { pub struct AnalyzeGraphArgs { pub files: Vec, pub direction: Direction, + pub python: Option, } /// Configuration overrides provided via dedicated CLI flags: /// `--line-length`, `--respect-gitignore`, etc. #[derive(Clone, Default)] -#[allow(clippy::struct_excessive_bools)] struct ExplicitConfigOverrides { dummy_variable_rgx: Option, exclude: Option>, diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index 14bb1463b4cf3..6eaef9382da09 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -3,8 +3,8 @@ use std::fs::{self, File}; use std::hash::Hasher; use std::io::{self, BufReader, Write}; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, SystemTime}; use anyhow::{Context, Result}; @@ -13,21 +13,22 @@ use itertools::Itertools; use log::{debug, error}; use rayon::iter::ParallelIterator; use rayon::iter::{IntoParallelIterator, ParallelBridge}; +use ruff_linter::codes::Rule; use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; use ruff_cache::{CacheKey, CacheKeyHasher}; -use ruff_diagnostics::{DiagnosticKind, Fix}; -use ruff_linter::message::{DiagnosticMessage, Message}; +use ruff_db::diagnostic::Diagnostic; +use ruff_diagnostics::Fix; +use ruff_linter::message::create_lint_diagnostic; use ruff_linter::package::PackageRoot; -use ruff_linter::{warn_user, VERSION}; +use ruff_linter::{VERSION, warn_user}; use ruff_macros::CacheKey; use ruff_notebook::NotebookIndex; use ruff_source_file::SourceFileBuilder; use ruff_text_size::{TextRange, TextSize}; -use ruff_workspace::resolver::Resolver; use ruff_workspace::Settings; +use ruff_workspace::resolver::Resolver; use crate::diagnostics::Diagnostics; @@ -86,7 +87,7 @@ pub(crate) struct Cache { changes: Mutex>, /// The "current" timestamp used as cache for the updates of /// [`FileCache::last_seen`] - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] last_seen_cache: u64, } @@ -117,13 +118,14 @@ impl Cache { } }; - let mut package: PackageCache = match bincode::deserialize_from(BufReader::new(file)) { - Ok(package) => package, - Err(err) => { - warn_user!("Failed parse cache file `{}`: {err}", path.display()); - return Cache::empty(path, package_root); - } - }; + let mut package: PackageCache = + match bincode::decode_from_reader(BufReader::new(file), bincode::config::standard()) { + Ok(package) => package, + Err(err) => { + warn_user!("Failed parse cache file `{}`: {err}", path.display()); + return Cache::empty(path, package_root); + } + }; // Sanity check. if package.package_root != package_root { @@ -146,7 +148,7 @@ impl Cache { Cache::new(path, package) } - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn new(path: PathBuf, package: PackageCache) -> Self { Cache { path, @@ -175,8 +177,8 @@ impl Cache { // Serialize to in-memory buffer because hyperfine benchmark showed that it's faster than // using a `BufWriter` and our cache files are small enough that streaming isn't necessary. - let serialized = - bincode::serialize(&self.package).context("Failed to serialize cache data")?; + let serialized = bincode::encode_to_vec(&self.package, bincode::config::standard()) + .context("Failed to serialize cache data")?; temp_file .write_all(&serialized) .context("Failed to write serialized cache to temporary file.")?; @@ -204,7 +206,7 @@ impl Cache { } /// Applies the pending changes without storing the cache to disk. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] pub(crate) fn save(&mut self) -> bool { /// Maximum duration for which we keep a file in cache that hasn't been seen. const MAX_LAST_SEEN: Duration = Duration::from_secs(30 * 24 * 60 * 60); // 30 days. @@ -311,7 +313,7 @@ impl Cache { } /// On disk representation of a cache of a package. -#[derive(Deserialize, Debug, Serialize)] +#[derive(bincode::Encode, Debug, bincode::Decode)] struct PackageCache { /// Path to the root of the package. /// @@ -323,7 +325,7 @@ struct PackageCache { } /// On disk representation of the cache per source file. -#[derive(Deserialize, Debug, Serialize)] +#[derive(bincode::Decode, Debug, bincode::Encode)] pub(crate) struct FileCache { /// Key that determines if the cached item is still valid. key: u64, @@ -340,21 +342,23 @@ impl FileCache { /// Convert the file cache into `Diagnostics`, using `path` as file name. pub(crate) fn to_diagnostics(&self, path: &Path) -> Option { self.data.lint.as_ref().map(|lint| { - let messages = if lint.messages.is_empty() { + let diagnostics = if lint.messages.is_empty() { Vec::new() } else { let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish(); lint.messages .iter() .map(|msg| { - Message::Diagnostic(DiagnosticMessage { - kind: msg.kind.clone(), - range: msg.range, - fix: msg.fix.clone(), - file: file.clone(), - noqa_offset: msg.noqa_offset, - parent: msg.parent, - }) + create_lint_diagnostic( + &msg.body, + msg.suggestion.as_ref(), + msg.range, + msg.fix.clone(), + msg.parent, + file.clone(), + msg.noqa_offset, + msg.rule, + ) }) .collect() }; @@ -363,12 +367,12 @@ impl FileCache { } else { FxHashMap::default() }; - Diagnostics::new(messages, notebook_indexes) + Diagnostics::new(diagnostics, notebook_indexes) }) } } -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, bincode::Decode, bincode::Encode)] struct FileCacheData { lint: Option, formatted: bool, @@ -406,7 +410,7 @@ pub(crate) fn init(path: &Path) -> Result<()> { Ok(()) } -#[derive(Deserialize, Debug, Serialize, PartialEq)] +#[derive(bincode::Decode, Debug, bincode::Encode, PartialEq)] pub(crate) struct LintCacheData { /// Imports made. // pub(super) imports: ImportMap, @@ -419,36 +423,42 @@ pub(crate) struct LintCacheData { /// This will be empty if `messages` is empty. pub(super) source: String, /// Notebook index if this file is a Jupyter Notebook. + #[bincode(with_serde)] pub(super) notebook_index: Option, } impl LintCacheData { - pub(crate) fn from_messages( - messages: &[Message], + pub(crate) fn from_diagnostics( + diagnostics: &[Diagnostic], notebook_index: Option, ) -> Self { - let source = if let Some(msg) = messages.first() { - msg.source_file().source_text().to_owned() + let source = if let Some(msg) = diagnostics.first() { + msg.expect_ruff_source_file().source_text().to_owned() } else { String::new() // No messages, no need to keep the source! }; - let messages = messages + let messages = diagnostics .iter() - .filter_map(|message| message.as_diagnostic_message()) - .map(|msg| { + // Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so + // this also serves to filter them out, but we shouldn't be caching files with syntax + // errors anyway. + .filter_map(|msg| Some((msg.name().parse().ok()?, msg))) + .map(|(rule, msg)| { // Make sure that all message use the same source file. assert_eq!( - &msg.file, - messages.first().unwrap().source_file(), + msg.expect_ruff_source_file(), + diagnostics.first().unwrap().expect_ruff_source_file(), "message uses a different source file" ); CacheMessage { - kind: msg.kind.clone(), - range: msg.range, - parent: msg.parent, - fix: msg.fix.clone(), - noqa_offset: msg.noqa_offset, + rule, + body: msg.body().to_string(), + suggestion: msg.suggestion().map(ToString::to_string), + range: msg.expect_range(), + parent: msg.parent(), + fix: msg.fix().cloned(), + noqa_offset: msg.noqa_offset(), } }) .collect(); @@ -462,14 +472,24 @@ impl LintCacheData { } /// On disk representation of a diagnostic message. -#[derive(Deserialize, Debug, Serialize, PartialEq)] +#[derive(bincode::Decode, Debug, bincode::Encode, PartialEq)] pub(super) struct CacheMessage { - kind: DiagnosticKind, + /// The rule for the cached diagnostic. + #[bincode(with_serde)] + rule: Rule, + /// The message body to display to the user, to explain the diagnostic. + body: String, + /// The message to display to the user, to explain the suggested fix. + suggestion: Option, /// Range into the message's [`FileCache::source`]. + #[bincode(with_serde)] range: TextRange, + #[bincode(with_serde)] parent: Option, + #[bincode(with_serde)] fix: Option, - noqa_offset: TextSize, + #[bincode(with_serde)] + noqa_offset: Option, } pub(crate) trait PackageCaches { @@ -587,14 +607,14 @@ mod tests { use std::time::SystemTime; use anyhow::Result; - use filetime::{set_file_mtime, FileTime}; + use filetime::{FileTime, set_file_mtime}; use itertools::Itertools; - use ruff_linter::settings::LinterSettings; use test_case::test_case; use ruff_cache::CACHE_DIR_NAME; - use ruff_linter::message::Message; + use ruff_db::diagnostic::Diagnostic; use ruff_linter::package::PackageRoot; + use ruff_linter::settings::LinterSettings; use ruff_linter::settings::flags; use ruff_linter::settings::types::UnsafeFixes; use ruff_python_ast::{PySourceType, PythonVersion}; @@ -602,8 +622,8 @@ mod tests { use crate::cache::{self, FileCache, FileCacheData, FileCacheKey}; use crate::cache::{Cache, RelativePathBuf}; - use crate::commands::format::{format_path, FormatCommandError, FormatMode, FormatResult}; - use crate::diagnostics::{lint_path, Diagnostics}; + use crate::commands::format::{FormatCommandError, FormatMode, FormatResult, format_path}; + use crate::diagnostics::{Diagnostics, lint_path}; #[test_case("../ruff_linter/resources/test/fixtures", "ruff_tests/cache_same_results_ruff_linter"; "ruff_linter_fixtures")] #[test_case("../ruff_notebook/resources/test/fixtures", "ruff_tests/cache_same_results_ruff_notebook"; "ruff_notebook_fixtures")] @@ -616,7 +636,7 @@ mod tests { let settings = Settings { cache_dir, linter: LinterSettings { - unresolved_target_version: PythonVersion::latest(), + unresolved_target_version: PythonVersion::latest().into(), ..Default::default() }, ..Settings::default() @@ -661,7 +681,7 @@ mod tests { UnsafeFixes::Enabled, ) .unwrap(); - if diagnostics.messages.iter().any(Message::is_syntax_error) { + if diagnostics.inner.iter().any(Diagnostic::is_invalid_syntax) { parse_errors.push(path.clone()); } paths.push(path); @@ -834,7 +854,6 @@ mod tests { // Regression test for issue #3086. #[cfg(unix)] - #[allow(clippy::items_after_statements)] fn flip_execute_permission_bit(path: &Path) -> io::Result<()> { use std::os::unix::fs::PermissionsExt; let file = fs::OpenOptions::new().write(true).open(path)?; @@ -843,7 +862,6 @@ mod tests { } #[cfg(windows)] - #[allow(clippy::items_after_statements)] fn flip_read_only_permission(path: &Path) -> io::Result<()> { let file = fs::OpenOptions::new().write(true).open(path)?; let mut perms = file.metadata()?.permissions(); diff --git a/crates/ruff/src/commands/add_noqa.rs b/crates/ruff/src/commands/add_noqa.rs index c5a417ec3cf6f..d5eaeb0170ccb 100644 --- a/crates/ruff/src/commands/add_noqa.rs +++ b/crates/ruff/src/commands/add_noqa.rs @@ -11,7 +11,7 @@ use ruff_linter::source_kind::SourceKind; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; use ruff_workspace::resolver::{ - match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, + PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path, }; use crate::args::ConfigArguments; diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 6069f6d052097..75ff28d0dcbc4 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -1,6 +1,6 @@ use crate::args::{AnalyzeGraphArgs, ConfigArguments}; use crate::resolve::resolve; -use crate::{resolve_default_files, ExitStatus}; +use crate::{ExitStatus, resolve_default_files}; use anyhow::Result; use log::{debug, warn}; use path_absolutize::CWD; @@ -9,7 +9,7 @@ use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports}; use ruff_linter::package::PackageRoot; use ruff_linter::{warn_user, warn_user_once}; use ruff_python_ast::{PySourceType, SourceType}; -use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile}; +use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path}; use rustc_hash::FxHashMap; use std::io::Write; use std::path::{Path, PathBuf}; @@ -75,6 +75,8 @@ pub(crate) fn analyze_graph( .target_version .as_tuple() .into(), + args.python + .and_then(|python| SystemPathBuf::from_path_buf(python).ok()), )?; let imports = { diff --git a/crates/ruff/src/commands/check.rs b/crates/ruff/src/commands/check.rs index 98cf0b5281415..b33dcfe8cb2d4 100644 --- a/crates/ruff/src/commands/check.rs +++ b/crates/ruff/src/commands/check.rs @@ -11,18 +11,17 @@ use log::{debug, error, warn}; use rayon::prelude::*; use rustc_hash::FxHashMap; +use ruff_db::diagnostic::Diagnostic; use ruff_db::panic::catch_unwind; -use ruff_diagnostics::Diagnostic; -use ruff_linter::message::Message; use ruff_linter::package::PackageRoot; use ruff_linter::registry::Rule; use ruff_linter::settings::types::UnsafeFixes; -use ruff_linter::settings::{flags, LinterSettings}; -use ruff_linter::{fs, warn_user_once, IOError}; +use ruff_linter::settings::{LinterSettings, flags}; +use ruff_linter::{IOError, Violation, fs, warn_user_once}; use ruff_source_file::SourceFileBuilder; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::TextRange; use ruff_workspace::resolver::{ - match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, + PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path, }; use crate::args::ConfigArguments; @@ -30,7 +29,6 @@ use crate::cache::{Cache, PackageCacheMap, PackageCaches}; use crate::diagnostics::Diagnostics; /// Run the linter over a collection of files. -#[allow(clippy::too_many_arguments)] pub(crate) fn check( files: &[PathBuf], pyproject_config: &PyprojectConfig, @@ -131,11 +129,7 @@ pub(crate) fn check( SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); Diagnostics::new( - vec![Message::from_diagnostic( - Diagnostic::new(IOError { message }, TextRange::default()), - dummy, - TextSize::default(), - )], + vec![IOError { message }.into_diagnostic(TextRange::default(), &dummy)], FxHashMap::default(), ) } else { @@ -168,7 +162,9 @@ pub(crate) fn check( |a, b| (a.0 + b.0, a.1 + b.1), ); - all_diagnostics.messages.sort(); + all_diagnostics + .inner + .sort_by(Diagnostic::ruff_start_ordering); // Store the caches. caches.persist()?; @@ -181,7 +177,6 @@ pub(crate) fn check( /// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits /// a diagnostic if the linting the file panics. -#[allow(clippy::too_many_arguments)] fn lint_path( path: &Path, package: Option>, @@ -230,9 +225,9 @@ mod test { use ruff_linter::message::{Emitter, EmitterContext, TextEmitter}; use ruff_linter::registry::Rule; use ruff_linter::settings::types::UnsafeFixes; - use ruff_linter::settings::{flags, LinterSettings}; - use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; + use ruff_linter::settings::{LinterSettings, flags}; use ruff_workspace::Settings; + use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use crate::args::ConfigArguments; @@ -286,7 +281,7 @@ mod test { .with_show_fix_status(true) .emit( &mut output, - &diagnostics.messages, + &diagnostics.inner, &EmitterContext::new(&FxHashMap::default()), ) .unwrap(); diff --git a/crates/ruff/src/commands/check_stdin.rs b/crates/ruff/src/commands/check_stdin.rs index 4e7be0b540e14..f76b3ab2df69e 100644 --- a/crates/ruff/src/commands/check_stdin.rs +++ b/crates/ruff/src/commands/check_stdin.rs @@ -1,13 +1,14 @@ use std::path::Path; use anyhow::Result; +use ruff_db::diagnostic::Diagnostic; use ruff_linter::package::PackageRoot; use ruff_linter::packaging; use ruff_linter::settings::flags; -use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver}; +use ruff_workspace::resolver::{PyprojectConfig, Resolver, match_exclusion, python_file_at_path}; use crate::args::ConfigArguments; -use crate::diagnostics::{lint_stdin, Diagnostics}; +use crate::diagnostics::{Diagnostics, lint_stdin}; use crate::stdin::{parrot_stdin, read_from_stdin}; /// Run the linter over a single file, read from `stdin`. @@ -52,6 +53,8 @@ pub(crate) fn check_stdin( noqa, fix_mode, )?; - diagnostics.messages.sort_unstable(); + diagnostics + .inner + .sort_unstable_by(Diagnostic::ruff_start_ordering); Ok(diagnostics) } diff --git a/crates/ruff/src/commands/completions/config.rs b/crates/ruff/src/commands/completions/config.rs index 3d6ddeadc8bfb..9b9aa773da4b0 100644 --- a/crates/ruff/src/commands/completions/config.rs +++ b/crates/ruff/src/commands/completions/config.rs @@ -2,10 +2,8 @@ use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory}; use itertools::Itertools; use std::str::FromStr; -use ruff_workspace::{ - options::Options, - options_base::{OptionField, OptionSet, OptionsMetadata, Visit}, -}; +use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; +use ruff_workspace::options::Options; #[derive(Default)] struct CollectOptionsVisitor { diff --git a/crates/ruff/src/commands/config.rs b/crates/ruff/src/commands/config.rs index 4f83d7525a932..f6c00548394ab 100644 --- a/crates/ruff/src/commands/config.rs +++ b/crates/ruff/src/commands/config.rs @@ -1,11 +1,11 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use crate::args::HelpFormat; +use ruff_options_metadata::OptionsMetadata; use ruff_workspace::options::Options; -use ruff_workspace::options_base::OptionsMetadata; -#[allow(clippy::print_stdout)] +#[expect(clippy::print_stdout)] pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> { match key { None => { diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 66648fc42a1a4..1006346008d31 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; use std::fs::File; use std::io; -use std::io::{stderr, stdout, Write}; +use std::io::{Write, stderr, stdout}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -16,7 +16,7 @@ use rustc_hash::FxHashSet; use thiserror::Error; use tracing::debug; -use ruff_db::panic::{catch_unwind, PanicError}; +use ruff_db::panic::{PanicError, catch_unwind}; use ruff_diagnostics::SourceMap; use ruff_linter::fs; use ruff_linter::logging::{DisplayParseError, LogLevel}; @@ -26,16 +26,16 @@ use ruff_linter::rules::flake8_quotes::settings::Quote; use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; -use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle}; +use ruff_python_formatter::{FormatModuleError, QuoteStyle, format_module_source, format_range}; use ruff_source_file::LineIndex; use ruff_text_size::{TextLen, TextRange, TextSize}; -use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver}; use ruff_workspace::FormatterSettings; +use ruff_workspace::resolver::{ResolvedFile, Resolver, match_exclusion, python_files_in_path}; use crate::args::{ConfigArguments, FormatArguments, FormatRange}; use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches}; use crate::resolve::resolve; -use crate::{resolve_default_files, ExitStatus}; +use crate::{ExitStatus, resolve_default_files}; #[derive(Debug, Copy, Clone, is_macro::Is)] pub(crate) enum FormatMode { @@ -160,7 +160,7 @@ pub(crate) fn format( }), Err(error) => Err(FormatCommandError::Panic( Some(resolved_file.path().to_path_buf()), - error, + Box::new(error), )), }, ) @@ -362,7 +362,7 @@ pub(crate) fn format_source( }) } else { // Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless. - #[allow(clippy::redundant_closure_for_method_calls)] + #[expect(clippy::redundant_closure_for_method_calls)] format_module_source(unformatted, options).map(|formatted| formatted.into_code()) }; @@ -635,7 +635,7 @@ impl<'a> FormatResults<'a> { pub(crate) enum FormatCommandError { Ignore(#[from] ignore::Error), Parse(#[from] DisplayParseError), - Panic(Option, PanicError), + Panic(Option, Box), Read(Option, SourceError), Format(Option, FormatModuleError), Write(Option, SourceError), @@ -821,9 +821,14 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { .collect(); rule_names.sort(); if let [rule] = rule_names.as_slice() { - warn_user_once!("The following rule may cause conflicts when used with the formatter: {rule}. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration."); + warn_user_once!( + "The following rule may cause conflicts when used with the formatter: {rule}. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration." + ); } else { - warn_user_once!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.", rule_names.join(", ")); + warn_user_once!( + "The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `lint.select` or `lint.extend-select` configuration, or adding them to the `lint.ignore` configuration.", + rule_names.join(", ") + ); } } @@ -833,7 +838,9 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { if setting.linter.rules.enabled(Rule::TabIndentation) && setting.formatter.indent_style.is_tab() { - warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`."); + warn_user_once!( + "The `format.indent-style=\"tab\"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`." + ); } if !setting @@ -846,14 +853,18 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { .enabled(Rule::MultiLineImplicitStringConcatenation) && !setting.linter.flake8_implicit_str_concat.allow_multiline { - warn_user_once!("The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`."); + warn_user_once!( + "The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`." + ); } // Validate all rules that rely on tab styles. if setting.linter.rules.enabled(Rule::DocstringTabIndentation) && setting.formatter.indent_style.is_tab() { - warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`."); + warn_user_once!( + "The `format.indent-style=\"tab\"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`." + ); } // Validate all rules that rely on custom indent widths. @@ -862,7 +873,9 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { Rule::IndentationWithInvalidMultipleComment, ]) && setting.formatter.indent_width.value() != 4 { - warn_user_once!("The `format.indent-width` option with a value other than 4 is incompatible with `E111` and `E114`. We recommend disabling these rules when using the formatter, which enforces a consistent indentation width. Alternatively, set the `format.indent-width` option to `4`."); + warn_user_once!( + "The `format.indent-width` option with a value other than 4 is incompatible with `E111` and `E114`. We recommend disabling these rules when using the formatter, which enforces a consistent indentation width. Alternatively, set the `format.indent-width` option to `4`." + ); } // Validate all rules that rely on quote styles. @@ -876,10 +889,14 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { setting.formatter.quote_style, ) { (Quote::Double, QuoteStyle::Single) => { - warn_user_once!("The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`."); + warn_user_once!( + "The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`." + ); } (Quote::Single, QuoteStyle::Double) => { - warn_user_once!("The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`."); + warn_user_once!( + "The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`." + ); } _ => {} } @@ -892,7 +909,9 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { QuoteStyle::Single | QuoteStyle::Double ) { - warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`"); + warn_user_once!( + "The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`" + ); } if setting.linter.rules.enabled(Rule::BadQuotesDocstring) @@ -902,7 +921,9 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { QuoteStyle::Single | QuoteStyle::Double ) { - warn_user_once!("The `flake8-quotes.docstring-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`"); + warn_user_once!( + "The `flake8-quotes.docstring-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`" + ); } // Validate all isort settings. @@ -910,12 +931,16 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { // The formatter removes empty lines if the value is larger than 2 but always inserts a empty line after imports. // Two empty lines are okay because `isort` only uses this setting for top-level imports (not in nested blocks). if !matches!(setting.linter.isort.lines_after_imports, 1 | 2 | -1) { - warn_user_once!("The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default)."); + warn_user_once!( + "The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default)." + ); } // Values larger than two get reduced to one line by the formatter if the import is in a nested block. if setting.linter.isort.lines_between_types > 1 { - warn_user_once!("The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default)."); + warn_user_once!( + "The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default)." + ); } // isort inserts a trailing comma which the formatter preserves, but only if `skip-magic-trailing-comma` isn't false. @@ -924,11 +949,15 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { && !setting.linter.isort.force_single_line { if setting.linter.isort.force_wrap_aliases { - warn_user_once!("The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`."); + warn_user_once!( + "The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`." + ); } if setting.linter.isort.split_on_trailing_comma { - warn_user_once!("The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`."); + warn_user_once!( + "The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`." + ); } } } diff --git a/crates/ruff/src/commands/format_stdin.rs b/crates/ruff/src/commands/format_stdin.rs index 9c2d8ff3e0e07..f5b8ea6c603c3 100644 --- a/crates/ruff/src/commands/format_stdin.rs +++ b/crates/ruff/src/commands/format_stdin.rs @@ -6,17 +6,17 @@ use log::error; use ruff_linter::source_kind::SourceKind; use ruff_python_ast::{PySourceType, SourceType}; -use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver}; use ruff_workspace::FormatterSettings; +use ruff_workspace::resolver::{Resolver, match_exclusion, python_file_at_path}; +use crate::ExitStatus; use crate::args::{ConfigArguments, FormatArguments, FormatRange}; use crate::commands::format::{ - format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode, - FormatResult, FormattedSource, + FormatCommandError, FormatMode, FormatResult, FormattedSource, format_source, + warn_incompatible_formatter_settings, }; use crate::resolve::resolve; use crate::stdin::{parrot_stdin, read_from_stdin}; -use crate::ExitStatus; /// Run the formatter over a single file, read from `stdin`. pub(crate) fn format_stdin( diff --git a/crates/ruff/src/commands/rule.rs b/crates/ruff/src/commands/rule.rs index fd2e682b0fbdd..adc761b3e5160 100644 --- a/crates/ruff/src/commands/rule.rs +++ b/crates/ruff/src/commands/rule.rs @@ -6,7 +6,7 @@ use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; use strum::IntoEnumIterator; -use ruff_diagnostics::FixAvailability; +use ruff_linter::FixAvailability; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; use crate::args::HelpFormat; @@ -19,7 +19,7 @@ struct Explanation<'a> { summary: &'a str, message_formats: &'a [&'a str], fix: String, - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] explanation: Option<&'a str>, preview: bool, } @@ -30,7 +30,7 @@ impl<'a> Explanation<'a> { let (linter, _) = Linter::parse_code(&code).unwrap(); let fix = rule.fixable().to_string(); Self { - name: rule.as_ref(), + name: rule.name().as_str(), code, linter: linter.name(), summary: rule.message_formats()[0], @@ -44,7 +44,7 @@ impl<'a> Explanation<'a> { fn format_rule_text(rule: Rule) -> String { let mut output = String::new(); - let _ = write!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code()); + let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code()); output.push('\n'); output.push('\n'); diff --git a/crates/ruff/src/commands/server.rs b/crates/ruff/src/commands/server.rs index 817269bc7e63b..837662e7d3c21 100644 --- a/crates/ruff/src/commands/server.rs +++ b/crates/ruff/src/commands/server.rs @@ -1,14 +1,7 @@ -use std::num::NonZeroUsize; - use crate::ExitStatus; use anyhow::Result; -use ruff_server::Server; - -pub(crate) fn run_server( - worker_threads: NonZeroUsize, - preview: Option, -) -> Result { - let server = Server::new(worker_threads, preview)?; - server.run().map(|()| ExitStatus::Success) +pub(crate) fn run_server(preview: Option) -> Result { + ruff_server::run(preview)?; + Ok(ExitStatus::Success) } diff --git a/crates/ruff/src/commands/show_files.rs b/crates/ruff/src/commands/show_files.rs index f21a9aa9430cc..7c74837fd3495 100644 --- a/crates/ruff/src/commands/show_files.rs +++ b/crates/ruff/src/commands/show_files.rs @@ -5,7 +5,7 @@ use anyhow::Result; use itertools::Itertools; use ruff_linter::warn_user_once; -use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; +use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, python_files_in_path}; use crate::args::ConfigArguments; diff --git a/crates/ruff/src/commands/show_settings.rs b/crates/ruff/src/commands/show_settings.rs index 679c2733dff37..ec4b0d54825d1 100644 --- a/crates/ruff/src/commands/show_settings.rs +++ b/crates/ruff/src/commands/show_settings.rs @@ -1,10 +1,10 @@ use std::io::Write; use std::path::PathBuf; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use itertools::Itertools; -use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; +use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, python_files_in_path}; use crate::args::ConfigArguments; diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index fc462de30ff3d..38bfedbc448c8 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -10,40 +10,39 @@ use std::path::Path; use anyhow::{Context, Result}; use colored::Colorize; use log::{debug, warn}; -use rustc_hash::FxHashMap; - -use ruff_diagnostics::Diagnostic; +use ruff_db::diagnostic::Diagnostic; use ruff_linter::codes::Rule; -use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource}; -use ruff_linter::message::{Message, SyntaxErrorMessage}; +use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only}; +use ruff_linter::message::create_syntax_error_diagnostic; use ruff_linter::package::PackageRoot; use ruff_linter::pyproject_toml::lint_pyproject_toml; use ruff_linter::settings::types::UnsafeFixes; -use ruff_linter::settings::{flags, LinterSettings}; +use ruff_linter::settings::{LinterSettings, flags}; use ruff_linter::source_kind::{SourceError, SourceKind}; -use ruff_linter::{fs, IOError}; +use ruff_linter::{IOError, Violation, fs}; use ruff_notebook::{Notebook, NotebookError, NotebookIndex}; use ruff_python_ast::{PySourceType, SourceType, TomlSourceType}; use ruff_source_file::SourceFileBuilder; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::TextRange; use ruff_workspace::Settings; +use rustc_hash::FxHashMap; use crate::cache::{Cache, FileCacheKey, LintCacheData}; #[derive(Debug, Default, PartialEq)] pub(crate) struct Diagnostics { - pub(crate) messages: Vec, + pub(crate) inner: Vec, pub(crate) fixed: FixMap, pub(crate) notebook_indexes: FxHashMap, } impl Diagnostics { pub(crate) fn new( - messages: Vec, + diagnostics: Vec, notebook_indexes: FxHashMap, ) -> Self { Self { - messages, + inner: diagnostics, fixed: FixMap::default(), notebook_indexes, } @@ -63,16 +62,12 @@ impl Diagnostics { let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); let source_file = SourceFileBuilder::new(name, "").finish(); Self::new( - vec![Message::from_diagnostic( - Diagnostic::new( - IOError { - message: err.to_string(), - }, - TextRange::default(), - ), - source_file, - TextSize::default(), - )], + vec![ + IOError { + message: err.to_string(), + } + .into_diagnostic(TextRange::default(), &source_file), + ], FxHashMap::default(), ) } else { @@ -102,11 +97,11 @@ impl Diagnostics { let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); let dummy = SourceFileBuilder::new(name, "").finish(); Self::new( - vec![Message::SyntaxError(SyntaxErrorMessage { - message: err.to_string(), - range: TextRange::default(), - file: dummy, - })], + vec![create_syntax_error_diagnostic( + dummy, + err, + TextRange::default(), + )], FxHashMap::default(), ) } @@ -125,7 +120,7 @@ impl Add for Diagnostics { impl AddAssign for Diagnostics { fn add_assign(&mut self, other: Self) { - self.messages.extend(other.messages); + self.inner.extend(other.inner); self.fixed += other.fixed; self.notebook_indexes.extend(other.notebook_indexes); } @@ -169,9 +164,9 @@ impl AddAssign for FixMap { continue; } let fixed_in_file = self.0.entry(filename).or_default(); - for (rule, count) in fixed { + for (rule, name, count) in fixed.iter() { if count > 0 { - *fixed_in_file.entry(rule).or_default() += count; + *fixed_in_file.entry(rule).or_default(name) += count; } } } @@ -206,7 +201,7 @@ pub(crate) fn lint_path( if match fix_mode { flags::FixMode::Generate => true, flags::FixMode::Apply | flags::FixMode::Diff => { - diagnostics.messages.is_empty() && diagnostics.fixed.is_empty() + diagnostics.inner.is_empty() && diagnostics.fixed.is_empty() } } { return Ok(diagnostics); @@ -226,7 +221,7 @@ pub(crate) fn lint_path( Some(source_type) => source_type, None => match SourceType::from(path) { SourceType::Toml(TomlSourceType::Pyproject) => { - let messages = if settings + let diagnostics = if settings .rules .iter_enabled() .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) @@ -239,12 +234,12 @@ pub(crate) fn lint_path( }; let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); - lint_pyproject_toml(source_file, settings) + lint_pyproject_toml(&source_file, settings) } else { vec![] }; return Ok(Diagnostics { - messages, + inner: diagnostics, ..Diagnostics::default() }); } @@ -309,7 +304,7 @@ pub(crate) fn lint_path( ParseSource::None, ); let transformed = source_kind; - let fixed = FxHashMap::default(); + let fixed = FixTable::default(); (result, transformed, fixed) } } else { @@ -323,12 +318,12 @@ pub(crate) fn lint_path( ParseSource::None, ); let transformed = source_kind; - let fixed = FxHashMap::default(); + let fixed = FixTable::default(); (result, transformed, fixed) }; let has_error = result.has_syntax_errors(); - let messages = result.messages; + let diagnostics = result.diagnostics; if let Some((cache, relative_path, key)) = caching { // We don't cache parsing errors. @@ -339,14 +334,14 @@ pub(crate) fn lint_path( if match fix_mode { flags::FixMode::Generate => true, flags::FixMode::Apply | flags::FixMode::Diff => { - messages.is_empty() && fixed.is_empty() + diagnostics.is_empty() && fixed.is_empty() } } { cache.update_lint( relative_path.to_owned(), &key, - LintCacheData::from_messages( - &messages, + LintCacheData::from_diagnostics( + &diagnostics, transformed.as_ipy_notebook().map(Notebook::index).cloned(), ), ); @@ -361,7 +356,7 @@ pub(crate) fn lint_path( }; Ok(Diagnostics { - messages, + inner: diagnostics, fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]), notebook_indexes, }) @@ -400,7 +395,7 @@ pub(crate) fn lint_stdin( } return Ok(Diagnostics { - messages: lint_pyproject_toml(source_file, &settings.linter), + inner: lint_pyproject_toml(&source_file, &settings.linter), fixed: FixMap::from_iter([(fs::relativize_path(path), FixTable::default())]), notebook_indexes: FxHashMap::default(), }); @@ -421,7 +416,7 @@ pub(crate) fn lint_stdin( }; // Lint the inputs. - let (LinterResult { messages, .. }, transformed, fixed) = + let (LinterResult { diagnostics, .. }, transformed, fixed) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { if let Ok(FixerResult { result, @@ -477,7 +472,7 @@ pub(crate) fn lint_stdin( } let transformed = source_kind; - let fixed = FxHashMap::default(); + let fixed = FixTable::default(); (result, transformed, fixed) } } else { @@ -491,7 +486,7 @@ pub(crate) fn lint_stdin( ParseSource::None, ); let transformed = source_kind; - let fixed = FxHashMap::default(); + let fixed = FixTable::default(); (result, transformed, fixed) }; @@ -505,7 +500,7 @@ pub(crate) fn lint_stdin( }; Ok(Diagnostics { - messages, + inner: diagnostics, fixed: FixMap::from_iter([( fs::relativize_path(path.unwrap_or_else(|| Path::new("-"))), fixed, diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 309bfc6fd0550..bd5facff5c972 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -1,8 +1,7 @@ #![allow(clippy::print_stdout)] use std::fs::File; -use std::io::{self, stdout, BufWriter, Write}; -use std::num::NonZeroUsize; +use std::io::{self, BufWriter, Write, stdout}; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::sync::mpsc::channel; @@ -11,10 +10,10 @@ use anyhow::Result; use clap::CommandFactory; use colored::Colorize; use log::warn; -use notify::{recommended_watcher, RecursiveMode, Watcher}; +use notify::{RecursiveMode, Watcher, recommended_watcher}; use args::{GlobalConfigArgs, ServerCommand}; -use ruff_linter::logging::{set_up_logging, LogLevel}; +use ruff_linter::logging::{LogLevel, set_up_logging}; use ruff_linter::settings::flags::FixMode; use ruff_linter::settings::types::OutputFormat; use ruff_linter::{fs, warn_user, warn_user_once}; @@ -134,7 +133,7 @@ pub fn run( { let default_panic_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!( r#" @@ -223,13 +222,7 @@ fn analyze_graph( } fn server(args: ServerCommand) -> Result { - let four = NonZeroUsize::new(4).unwrap(); - - // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. - let worker_threads = std::thread::available_parallelism() - .unwrap_or(four) - .max(four); - commands::server::run_server(worker_threads, args.resolve_preview()) + commands::server::run_server(args.resolve_preview()) } pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result { @@ -326,7 +319,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result 0 && config_arguments.log_level >= LogLevel::Default { let s = if modifications == 1 { "" } else { "s" }; - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!("Added {modifications} noqa directive{s}."); } @@ -370,7 +363,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result Result Result Result return Err(err.into()), } @@ -470,11 +463,11 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result Result, - name: SerializeMessageKindAsTitle, +struct ExpandedStatistics<'a> { + code: Option<&'a SecondaryCode>, + name: &'static str, count: usize, fixable: bool, } -#[derive(Copy, Clone)] -struct SerializeRuleAsCode(Rule); - -impl Serialize for SerializeRuleAsCode { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.0.noqa_code().to_string()) - } -} - -impl Display for SerializeRuleAsCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.noqa_code()) - } -} - -impl From for SerializeRuleAsCode { - fn from(rule: Rule) -> Self { - Self(rule) - } -} - -struct SerializeMessageKindAsTitle(MessageKind); - -impl Serialize for SerializeMessageKindAsTitle { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.0.as_str()) - } -} - -impl Display for SerializeMessageKindAsTitle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.0.as_str()) - } -} - -impl From for SerializeMessageKindAsTitle { - fn from(kind: MessageKind) -> Self { - Self(kind) - } -} - pub(crate) struct Printer { format: OutputFormat, log_level: LogLevel, @@ -128,11 +81,11 @@ impl Printer { let fixed = diagnostics .fixed .values() - .flat_map(std::collections::HashMap::values) + .flat_map(FixTable::counts) .sum::(); if self.flags.intersects(Flags::SHOW_VIOLATIONS) { - let remaining = diagnostics.messages.len(); + let remaining = diagnostics.inner.len(); let total = fixed + remaining; if fixed > 0 { let s = if total == 1 { "" } else { "s" }; @@ -157,7 +110,8 @@ impl Printer { } else { "es" }; - writeln!(writer, + writeln!( + writer, "{fix_prefix} {} fixable with the `--fix` option ({} hidden fix{es} can be enabled with the `--unsafe-fixes` option).", fixables.applicable, fixables.inapplicable_unsafe )?; @@ -175,7 +129,8 @@ impl Printer { } else { "es" }; - writeln!(writer, + writeln!( + writer, "No fixes available ({} hidden fix{es} can be enabled with the `--unsafe-fixes` option).", fixables.inapplicable_unsafe )?; @@ -205,15 +160,27 @@ impl Printer { if fixed > 0 { let s = if fixed == 1 { "" } else { "s" }; if self.fix_mode.is_apply() { - writeln!(writer, "Fixed {fixed} error{s} ({unapplied} additional fix{es} available with `--unsafe-fixes`).")?; + writeln!( + writer, + "Fixed {fixed} error{s} ({unapplied} additional fix{es} available with `--unsafe-fixes`)." + )?; } else { - writeln!(writer, "Would fix {fixed} error{s} ({unapplied} additional fix{es} available with `--unsafe-fixes`).")?; + writeln!( + writer, + "Would fix {fixed} error{s} ({unapplied} additional fix{es} available with `--unsafe-fixes`)." + )?; } } else { if self.fix_mode.is_apply() { - writeln!(writer, "No errors fixed ({unapplied} fix{es} available with `--unsafe-fixes`).")?; + writeln!( + writer, + "No errors fixed ({unapplied} fix{es} available with `--unsafe-fixes`)." + )?; } else { - writeln!(writer, "No errors would be fixed ({unapplied} fix{es} available with `--unsafe-fixes`).")?; + writeln!( + writer, + "No errors would be fixed ({unapplied} fix{es} available with `--unsafe-fixes`)." + )?; } } } else { @@ -241,7 +208,6 @@ impl Printer { } if !self.flags.intersects(Flags::SHOW_VIOLATIONS) { - #[allow(deprecated)] if matches!( self.format, OutputFormat::Full | OutputFormat::Concise | OutputFormat::Grouped @@ -263,16 +229,16 @@ impl Printer { match self.format { OutputFormat::Json => { - JsonEmitter.emit(writer, &diagnostics.messages, &context)?; + JsonEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Rdjson => { - RdjsonEmitter.emit(writer, &diagnostics.messages, &context)?; + RdjsonEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::JsonLines => { - JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?; + JsonLinesEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Junit => { - JunitEmitter.emit(writer, &diagnostics.messages, &context)?; + JunitEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Concise | OutputFormat::Full => { TextEmitter::default() @@ -280,7 +246,7 @@ impl Printer { .with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF)) .with_show_source(self.format == OutputFormat::Full) .with_unsafe_fixes(self.unsafe_fixes) - .emit(writer, &diagnostics.messages, &context)?; + .emit(writer, &diagnostics.inner, &context)?; if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if !diagnostics.fixed.is_empty() { @@ -296,7 +262,7 @@ impl Printer { GroupedEmitter::default() .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_unsafe_fixes(self.unsafe_fixes) - .emit(writer, &diagnostics.messages, &context)?; + .emit(writer, &diagnostics.inner, &context)?; if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if !diagnostics.fixed.is_empty() { @@ -308,19 +274,19 @@ impl Printer { self.write_summary_text(writer, diagnostics)?; } OutputFormat::Github => { - GithubEmitter.emit(writer, &diagnostics.messages, &context)?; + GithubEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Gitlab => { - GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?; + GitlabEmitter::default().emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Pylint => { - PylintEmitter.emit(writer, &diagnostics.messages, &context)?; + PylintEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Azure => { - AzureEmitter.emit(writer, &diagnostics.messages, &context)?; + AzureEmitter.emit(writer, &diagnostics.inner, &context)?; } OutputFormat::Sarif => { - SarifEmitter.emit(writer, &diagnostics.messages, &context)?; + SarifEmitter.emit(writer, &diagnostics.inner, &context)?; } } @@ -335,23 +301,27 @@ impl Printer { writer: &mut dyn Write, ) -> Result<()> { let statistics: Vec = diagnostics - .messages + .inner .iter() - .sorted_by_key(|message| (message.rule(), message.fixable())) - .fold(vec![], |mut acc: Vec<(&Message, usize)>, message| { - if let Some((prev_message, count)) = acc.last_mut() { - if prev_message.rule() == message.rule() { - *count += 1; - return acc; + .map(|message| (message.secondary_code(), message)) + .sorted_by_key(|(code, message)| (*code, message.fixable())) + .fold( + vec![], + |mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| { + if let Some(((prev_code, _prev_message), count)) = acc.last_mut() { + if *prev_code == code { + *count += 1; + return acc; + } } - } - acc.push((message, 1)); - acc - }) + acc.push(((code, message), 1)); + acc + }, + ) .iter() - .map(|&(message, count)| ExpandedStatistics { - code: message.rule().map(std::convert::Into::into), - name: message.kind().into(), + .map(|&((code, message), count)| ExpandedStatistics { + code, + name: message.name(), count, fixable: if let Some(fix) = message.fix() { fix.applies(self.unsafe_fixes.required_applicability()) @@ -379,12 +349,7 @@ impl Printer { ); let code_width = statistics .iter() - .map(|statistic| { - statistic - .code - .map_or_else(String::new, |rule| rule.to_string()) - .len() - }) + .map(|statistic| statistic.code.map_or(0, |s| s.len())) .max() .unwrap(); let any_fixable = statistics.iter().any(|statistic| statistic.fixable); @@ -400,7 +365,8 @@ impl Printer { statistic.count.to_string().bold(), statistic .code - .map_or_else(String::new, |rule| rule.to_string()) + .map(SecondaryCode::as_str) + .unwrap_or_default() .red() .bold(), if any_fixable { @@ -446,20 +412,20 @@ impl Printer { } if self.log_level >= LogLevel::Default { - let s = if diagnostics.messages.len() == 1 { + let s = if diagnostics.inner.len() == 1 { "" } else { "s" }; notify_user!( "Found {} error{s}. Watching for file changes.", - diagnostics.messages.len() + diagnostics.inner.len() ); } let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes); - if !diagnostics.messages.is_empty() { + if !diagnostics.inner.is_empty() { if self.log_level >= LogLevel::Default { writeln!(writer)?; } @@ -469,7 +435,7 @@ impl Printer { .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_show_source(preview) .with_unsafe_fixes(self.unsafe_fixes) - .emit(writer, &diagnostics.messages, &context)?; + .emit(writer, &diagnostics.inner, &context)?; } writer.flush()?; @@ -503,13 +469,13 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> { let total = fixed .values() - .map(|table| table.values().sum::()) + .map(|table| table.counts().sum::()) .sum::(); assert!(total > 0); let num_digits = num_digits( - *fixed + fixed .values() - .filter_map(|table| table.values().max()) + .filter_map(|table| table.counts().max()) .max() .unwrap(), ); @@ -529,12 +495,11 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> { relativize_path(filename).bold(), ":".cyan() )?; - for (rule, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) { + for (code, name, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) { writeln!( writer, - " {count:>num_digits$} × {} ({})", - rule.noqa_code().to_string().red().bold(), - rule.as_ref(), + " {count:>num_digits$} × {code} ({name})", + code = code.to_string().red().bold(), )?; } } @@ -553,7 +518,7 @@ impl FixableStatistics { let mut applicable = 0; let mut inapplicable_unsafe = 0; - for message in &diagnostics.messages { + for message in &diagnostics.inner { if let Some(fix) = message.fix() { if fix.applies(unsafe_fixes.required_applicability()) { applicable += 1; diff --git a/crates/ruff/src/resolve.rs b/crates/ruff/src/resolve.rs index b68d2c61fd7ac..8c6b0c4bbb262 100644 --- a/crates/ruff/src/resolve.rs +++ b/crates/ruff/src/resolve.rs @@ -1,14 +1,14 @@ use std::path::Path; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use log::debug; use path_absolutize::path_dedot; use ruff_workspace::configuration::Configuration; use ruff_workspace::pyproject::{self, find_fallback_target_version}; use ruff_workspace::resolver::{ - resolve_root_settings, ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, - PyprojectDiscoveryStrategy, + ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, + resolve_root_settings, }; use ruff_python_ast as ast; diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs index 62f4a6c3f9158..485aeb511b6c6 100644 --- a/crates/ruff/tests/analyze_graph.rs +++ b/crates/ruff/tests/analyze_graph.rs @@ -17,6 +17,7 @@ fn command() -> Command { command.arg("analyze"); command.arg("graph"); command.arg("--preview"); + command.env_clear(); command } @@ -422,3 +423,153 @@ fn nested_imports() -> Result<()> { Ok(()) } + +/// Test for venv resolution with the `--python` flag. +/// +/// Based on the [albatross-virtual-workspace] example from the uv repo and the report in [#16598]. +/// +/// [albatross-virtual-workspace]: https://github.com/astral-sh/uv/tree/aa629c4a/scripts/workspaces/albatross-virtual-workspace +/// [#16598]: https://github.com/astral-sh/ruff/issues/16598 +#[test] +fn venv() -> Result<()> { + let tempdir = TempDir::new()?; + let root = ChildPath::new(tempdir.path()); + + // packages + // ├── albatross + // │ ├── check_installed_albatross.py + // │ ├── pyproject.toml + // │ └── src + // │ └── albatross + // │ └── __init__.py + // └── bird-feeder + // ├── check_installed_bird_feeder.py + // ├── pyproject.toml + // └── src + // └── bird_feeder + // └── __init__.py + + let packages = root.child("packages"); + + let albatross = packages.child("albatross"); + albatross + .child("check_installed_albatross.py") + .write_str("from albatross import fly")?; + albatross + .child("pyproject.toml") + .write_str(indoc::indoc! {r#" + [project] + name = "albatross" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["bird-feeder", "tqdm>=4,<5"] + + [tool.uv.sources] + bird-feeder = { workspace = true } + "#})?; + albatross + .child("src") + .child("albatross") + .child("__init__.py") + .write_str("import tqdm; from bird_feeder import use")?; + + let bird_feeder = packages.child("bird-feeder"); + bird_feeder + .child("check_installed_bird_feeder.py") + .write_str("from bird_feeder import use; from albatross import fly")?; + bird_feeder + .child("pyproject.toml") + .write_str(indoc::indoc! {r#" + [project] + name = "bird-feeder" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = ["anyio>=4.3.0,<5"] + "#})?; + bird_feeder + .child("src") + .child("bird_feeder") + .child("__init__.py") + .write_str("import anyio")?; + + let venv = root.child(".venv"); + let bin = venv.child("bin"); + bin.child("python").touch()?; + let home = format!("home = {}", bin.to_string_lossy()); + venv.child("pyvenv.cfg").write_str(&home)?; + let site_packages = venv.child("lib").child("python3.12").child("site-packages"); + site_packages + .child("_albatross.pth") + .write_str(&albatross.join("src").to_string_lossy())?; + site_packages + .child("_bird_feeder.pth") + .write_str(&bird_feeder.join("src").to_string_lossy())?; + site_packages.child("tqdm").child("__init__.py").touch()?; + + // without `--python .venv`, the result should only include dependencies within the albatross + // package + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!( + command().arg("packages/albatross").current_dir(&root), + @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "packages/albatross/check_installed_albatross.py": [ + "packages/albatross/src/albatross/__init__.py" + ], + "packages/albatross/src/albatross/__init__.py": [] + } + + ----- stderr ----- + "#); + }); + + // with `--python .venv` both workspace and third-party dependencies are included + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!( + command().args(["--python", ".venv"]).arg("packages/albatross").current_dir(&root), + @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "packages/albatross/check_installed_albatross.py": [ + "packages/albatross/src/albatross/__init__.py" + ], + "packages/albatross/src/albatross/__init__.py": [ + ".venv/lib/python3.12/site-packages/tqdm/__init__.py", + "packages/bird-feeder/src/bird_feeder/__init__.py" + ] + } + + ----- stderr ----- + "#); + }); + + // test the error message for a non-existent venv. it's important that the `ruff analyze graph` + // flag matches the ty flag used to generate the error message (`--python`) + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!( + command().args(["--python", "none"]).arg("packages/albatross").current_dir(&root), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk + Cause: No such file or directory (os error 2) + "); + }); + + Ok(()) +} diff --git a/crates/ruff/tests/format.rs b/crates/ruff/tests/format.rs index 6ba5e83d1932d..31991f9b28bf4 100644 --- a/crates/ruff/tests/format.rs +++ b/crates/ruff/tests/format.rs @@ -862,7 +862,7 @@ if condition: print('Should change quotes') ----- stderr ----- - warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration. + warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration. "#); Ok(()) } @@ -999,7 +999,7 @@ def say_hy(name: str): 1 file reformatted ----- stderr ----- - warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration. + warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration. warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`. warning: The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`. warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`. @@ -1059,7 +1059,7 @@ def say_hy(name: str): print(f"Hy {name}") ----- stderr ----- - warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration. + warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration. warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`. warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`. warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`. @@ -1199,7 +1199,7 @@ def say_hy(name: str): ----- stderr ----- warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`. warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`. - warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration. + warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `lint.select` or `lint.extend-select` configuration, or adding it to the `lint.ignore` configuration. "); Ok(()) } diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 3e8f5497ede43..2041040b9529f 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -1067,7 +1067,7 @@ fn show_statistics_syntax_errors() { success: false exit_code: 1 ----- stdout ----- - 1 syntax-error + 1 invalid-syntax Found 1 error. ----- stderr ----- @@ -1080,7 +1080,7 @@ fn show_statistics_syntax_errors() { success: false exit_code: 1 ----- stdout ----- - 1 syntax-error + 1 invalid-syntax Found 1 error. ----- stderr ----- @@ -1093,7 +1093,7 @@ fn show_statistics_syntax_errors() { success: false exit_code: 1 ----- stdout ----- - 1 syntax-error + 1 invalid-syntax Found 1 error. ----- stderr ----- diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 41b44d5292351..8b795520ce4e7 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -612,7 +612,7 @@ fn extend_passed_via_config_argument() { #[test] fn nonexistent_extend_file() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; fs::write( project_dir.join("ruff.toml"), r#" @@ -653,7 +653,7 @@ extend = "ruff3.toml" #[test] fn circular_extend() -> Result<()> { let tempdir = TempDir::new()?; - let project_path = tempdir.path().canonicalize()?; + let project_path = dunce::canonicalize(tempdir.path())?; fs::write( project_path.join("ruff.toml"), @@ -698,7 +698,7 @@ extend = "ruff.toml" #[test] fn parse_error_extends() -> Result<()> { let tempdir = TempDir::new()?; - let project_path = tempdir.path().canonicalize()?; + let project_path = dunce::canonicalize(tempdir.path())?; fs::write( project_path.join("ruff.toml"), @@ -994,6 +994,7 @@ fn value_given_to_table_key_is_not_inline_table_2() { - `lint.extend-per-file-ignores` - `lint.exclude` - `lint.preview` + - `lint.typing-extensions` For more information, try '--help'. "); @@ -1156,18 +1157,20 @@ include = ["*.ipy"] #[test] fn warn_invalid_noqa_with_no_diagnostics() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--isolated"]) - .arg("--select") - .arg("F401") - .arg("-") - .pass_stdin( - r#" + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--isolated"]) + .arg("--select") + .arg("F401") + .arg("-") + .pass_stdin( + r#" # ruff: noqa: AAA101 print("Hello world!") "# - )); + ) + ); } #[test] @@ -1899,6 +1902,40 @@ def first_square(): Ok(()) } +/// Regression test for +#[test] +fn add_noqa_parent() -> Result<()> { + let tempdir = TempDir::new()?; + let test_path = tempdir.path().join("noqa.py"); + fs::write( + &test_path, + r#" +from foo import ( # noqa: F401 + bar +) + "#, + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--add-noqa") + .arg("--select=F401") + .arg("noqa.py") + .current_dir(&tempdir), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + }); + + Ok(()) +} + /// Infer `3.11` from `requires-python` in `pyproject.toml`. #[test] fn requires_python() -> Result<()> { @@ -2093,7 +2130,7 @@ select = ["UP006"] #[test] fn requires_python_no_tool() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("pyproject.toml"); fs::write( &ruff_toml, @@ -2117,7 +2154,7 @@ requires-python = ">= 3.11" .arg("test.py") .arg("-") .current_dir(project_dir) - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2207,6 +2244,7 @@ requires-python = ">= 3.11" XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -2390,7 +2428,7 @@ requires-python = ">= 3.11" analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -2403,7 +2441,7 @@ requires-python = ">= 3.11" #[test] fn requires_python_no_tool_target_version_override() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("pyproject.toml"); fs::write( &ruff_toml, @@ -2428,7 +2466,7 @@ requires-python = ">= 3.11" .arg("test.py") .arg("-") .current_dir(project_dir) - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2518,6 +2556,7 @@ requires-python = ">= 3.11" XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -2701,7 +2740,7 @@ requires-python = ">= 3.11" analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -2713,7 +2752,7 @@ requires-python = ">= 3.11" #[test] fn requires_python_no_tool_with_check() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("pyproject.toml"); fs::write( &ruff_toml, @@ -2758,7 +2797,7 @@ requires-python = ">= 3.11" #[test] fn requires_python_ruff_toml_no_target_fallback() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("ruff.toml"); fs::write( &ruff_toml, @@ -2790,7 +2829,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("test.py") .arg("--show-settings") - .current_dir(project_dir), @r###" + .current_dir(project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -2881,6 +2920,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3064,7 +3104,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3078,7 +3118,7 @@ from typing import Union;foo: Union[int, str] = 1 #[test] fn requires_python_ruff_toml_no_target_fallback_check() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("ruff.toml"); fs::write( &ruff_toml, @@ -3133,7 +3173,7 @@ from typing import Union;foo: Union[int, str] = 1 #[test] fn requires_python_pyproject_toml_above() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let outer_pyproject = tempdir.path().join("pyproject.toml"); fs::write( &outer_pyproject, @@ -3160,7 +3200,7 @@ from typing import Union;foo: Union[int, str] = 1 "#, )?; - let testpy_canon = testpy.canonicalize()?; + let testpy_canon = dunce::canonicalize(testpy)?; insta::with_settings!({ filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"(?m)^foo\\test","foo/test")] @@ -3170,7 +3210,7 @@ from typing import Union;foo: Union[int, str] = 1 .arg("--show-settings") .args(["--select","UP007"]) .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3260,6 +3300,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3443,7 +3484,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3458,7 +3499,7 @@ from typing import Union;foo: Union[int, str] = 1 #[test] fn requires_python_pyproject_toml_above_with_tool() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let outer_pyproject = tempdir.path().join("pyproject.toml"); fs::write( &outer_pyproject, @@ -3475,7 +3516,7 @@ requires-python = ">= 3.11" &inner_pyproject, r#" [tool.ruff] -target-version = "py310" +target-version = "py310" "#, )?; @@ -3487,7 +3528,7 @@ from typing import Union;foo: Union[int, str] = 1 "#, )?; - let testpy_canon = testpy.canonicalize()?; + let testpy_canon = dunce::canonicalize(testpy)?; insta::with_settings!({ filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"foo\\","foo/")] @@ -3497,7 +3538,7 @@ from typing import Union;foo: Union[int, str] = 1 .arg("--show-settings") .args(["--select","UP007"]) .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3587,6 +3628,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3770,7 +3812,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3785,7 +3827,7 @@ from typing import Union;foo: Union[int, str] = 1 #[test] fn requires_python_ruff_toml_above() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("ruff.toml"); fs::write( &ruff_toml, @@ -3814,7 +3856,7 @@ from typing import Union;foo: Union[int, str] = 1 "#, )?; - let testpy_canon = testpy.canonicalize()?; + let testpy_canon = dunce::canonicalize(testpy)?; insta::with_settings!({ filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] @@ -3823,7 +3865,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3890,7 +3932,7 @@ from typing import Union;foo: Union[int, str] = 1 linter.per_file_ignores = {} linter.safety_table.forced_safe = [] linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.9 + linter.unresolved_target_version = none linter.per_file_target_version = {} linter.preview = disabled linter.explicit_preview_rules = false @@ -3914,6 +3956,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4097,7 +4140,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); insta::with_settings!({ @@ -4107,7 +4150,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("test.py") - .current_dir(project_dir.join("foo")), @r###" + .current_dir(project_dir.join("foo")), @r#" success: true exit_code: 0 ----- stdout ----- @@ -4174,7 +4217,7 @@ from typing import Union;foo: Union[int, str] = 1 linter.per_file_ignores = {} linter.safety_table.forced_safe = [] linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.9 + linter.unresolved_target_version = none linter.per_file_target_version = {} linter.preview = disabled linter.explicit_preview_rules = false @@ -4198,6 +4241,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4381,7 +4425,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -4397,7 +4441,7 @@ from typing import Union;foo: Union[int, str] = 1 #[test] fn requires_python_extend_from_shared_config() -> Result<()> { let tempdir = TempDir::new()?; - let project_dir = tempdir.path().canonicalize()?; + let project_dir = dunce::canonicalize(tempdir.path())?; let ruff_toml = tempdir.path().join("ruff.toml"); fs::write( &ruff_toml, @@ -4435,7 +4479,7 @@ from typing import Union;foo: Union[int, str] = 1 "#, )?; - let testpy_canon = testpy.canonicalize()?; + let testpy_canon = dunce::canonicalize(testpy)?; insta::with_settings!({ filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] @@ -4444,7 +4488,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -4535,6 +4579,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4718,7 +4763,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) @@ -4954,30 +4999,81 @@ fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> { #[test] fn flake8_import_convention_unused_aliased_import() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(r#"lint.isort.required-imports = ["import pandas"]"#) - .args(["--select", "I002,ICN001,F401"]) - .args(["--stdin-filename", "test.py"]) - .arg("--unsafe-fixes") - .arg("--fix") - .arg("-") - .pass_stdin("1")); + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["import pandas"]"#) + .args(["--select", "I002,ICN001,F401"]) + .args(["--stdin-filename", "test.py"]) + .arg("--unsafe-fixes") + .arg("--fix") + .arg("-") + .pass_stdin("1") + ); } #[test] fn flake8_import_convention_unused_aliased_import_no_conflict() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#) + .args(["--select", "I002,ICN001,F401"]) + .args(["--stdin-filename", "test.py"]) + .arg("--unsafe-fixes") + .arg("--fix") + .arg("-") + .pass_stdin("1") + ); +} + +// See: https://github.com/astral-sh/ruff/issues/16177 +#[test] +fn flake8_pyi_redundant_none_literal() { + let snippet = r#" +from typing import Literal + +# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements +# but not both, as if both were autofixed it would result in `None | None`, +# which leads to a `TypeError` at runtime. +a: Literal[None,] | Literal[None,] +b: Literal[None] | Literal[None] +c: Literal[None] | Literal[None,] +d: Literal[None,] | Literal[None] +"#; + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#) - .args(["--select", "I002,ICN001,F401"]) + .args(["--select", "PYI061"]) .args(["--stdin-filename", "test.py"]) - .arg("--unsafe-fixes") - .arg("--fix") + .arg("--preview") + .arg("--diff") .arg("-") - .pass_stdin("1")); + .pass_stdin(snippet), @r" + success: false + exit_code: 1 + ----- stdout ----- + --- test.py + +++ test.py + @@ -4,7 +4,7 @@ + # For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements + # but not both, as if both were autofixed it would result in `None | None`, + # which leads to a `TypeError` at runtime. + -a: Literal[None,] | Literal[None,] + -b: Literal[None] | Literal[None] + -c: Literal[None] | Literal[None,] + -d: Literal[None,] | Literal[None] + +a: None | Literal[None,] + +b: None | Literal[None] + +c: None | Literal[None,] + +d: None | Literal[None] + + + ----- stderr ----- + Would fix 4 errors. + "); } /// Test that private, old-style `TypeVar` generics @@ -5175,8 +5271,8 @@ fn a005_module_shadowing_non_strict() -> Result<()> { } /// Test A005 with `strict-checking` unset -/// TODO(brent) This should currently match the strict version, but after the next minor -/// release it will match the non-strict version directly above +/// +/// This should match the non-strict version directly above #[test] fn a005_module_shadowing_strict_default() -> Result<()> { let tempdir = TempDir::new()?; @@ -5340,14 +5436,15 @@ match 2: print("it's one") "# ), - @r" - success: true - exit_code: 0 + @r###" + success: false + exit_code: 1 ----- stdout ----- - All checks passed! + test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + Found 1 error. ----- stderr ----- - " + "### ); // syntax error on 3.9 with preview @@ -5564,3 +5661,88 @@ fn semantic_syntax_errors() -> Result<()> { Ok(()) } + +/// Regression test for . +/// +/// `lint.typing-extensions = false` with Python 3.9 should disable the PYI019 lint because it would +/// try to import `Self` from `typing_extensions` +#[test] +fn combine_typing_extensions_config() { + let contents = " +from typing import TypeVar +T = TypeVar('T') +class Foo: + def f(self: T) -> T: ... +"; + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "lint.typing-extensions = false"]) + .arg("--select=PYI019") + .arg("--target-version=py39") + .arg("-") + .pass_stdin(contents), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); +} + +#[test_case::test_case("concise")] +#[test_case::test_case("full")] +#[test_case::test_case("json")] +#[test_case::test_case("json-lines")] +#[test_case::test_case("junit")] +#[test_case::test_case("grouped")] +#[test_case::test_case("github")] +#[test_case::test_case("gitlab")] +#[test_case::test_case("pylint")] +#[test_case::test_case("rdjson")] +#[test_case::test_case("azure")] +#[test_case::test_case("sarif")] +fn output_format(output_format: &str) -> Result<()> { + const CONTENT: &str = "\ +import os # F401 +x = y # F821 +match 42: # invalid-syntax + case _: ... +"; + + let tempdir = TempDir::new()?; + let input = tempdir.path().join("input.py"); + fs::write(&input, CONTENT)?; + + let snapshot = format!("output_format_{output_format}"); + + insta::with_settings!({ + filters => vec![ + (tempdir_filter(&tempdir).as_str(), "[TMP]/"), + (r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#), + (ruff_linter::VERSION, "[VERSION]"), + ] + }, { + assert_cmd_snapshot!( + snapshot, + Command::new(get_cargo_bin(BIN_NAME)) + .args([ + "check", + "--no-cache", + "--output-format", + output_format, + "--select", + "F401,F821", + "--target-version", + "py39", + "input.py", + ]) + .current_dir(&tempdir), + ); + }); + + Ok(()) +} diff --git a/crates/ruff/tests/show_settings.rs b/crates/ruff/tests/show_settings.rs index d6fc8e2f96c1d..9a20b16afdccc 100644 --- a/crates/ruff/tests/show_settings.rs +++ b/crates/ruff/tests/show_settings.rs @@ -12,10 +12,8 @@ fn display_default_settings() -> anyhow::Result<()> { // Tempdir path's on macos are symlinks, which doesn't play nicely with // our snapshot filtering. - let project_dir = tempdir - .path() - .canonicalize() - .context("Failed to canonical tempdir path.")?; + let project_dir = + dunce::canonicalize(tempdir.path()).context("Failed to canonical tempdir path.")?; std::fs::write( project_dir.join("pyproject.toml"), diff --git a/crates/ruff/tests/snapshots/integration_test__rule_f401.snap b/crates/ruff/tests/snapshots/integration_test__rule_f401.snap index 3e9270c33e369..a861fe395ce50 100644 --- a/crates/ruff/tests/snapshots/integration_test__rule_f401.snap +++ b/crates/ruff/tests/snapshots/integration_test__rule_f401.snap @@ -5,7 +5,6 @@ info: args: - rule - F401 -snapshot_kind: text --- success: true exit_code: 0 @@ -84,6 +83,11 @@ else: print("numpy is not installed") ``` +## Preview +When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +the criterion for determining whether an import is first-party +is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. + ## Options - `lint.ignore-init-module-imports` - `lint.pyflakes.allowed-unused-imports` diff --git a/crates/ruff/tests/snapshots/lint__output_format_azure.snap b/crates/ruff/tests/snapshots/lint__output_format_azure.snap new file mode 100644 index 0000000000000..36ba1f8790b8c --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_azure.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - azure + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused +##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y` +##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_concise.snap b/crates/ruff/tests/snapshots/lint__output_format_concise.snap new file mode 100644 index 0000000000000..9b9e4fd0f3f94 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_concise.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - concise + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +input.py:1:8: F401 [*] `os` imported but unused +input.py:2:5: F821 Undefined name `y` +input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) +Found 3 errors. +[*] 1 fixable with the `--fix` option. + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_full.snap b/crates/ruff/tests/snapshots/lint__output_format_full.snap new file mode 100644 index 0000000000000..d9527c596f6f7 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_full.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - full + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +input.py:1:8: F401 [*] `os` imported but unused + | +1 | import os # F401 + | ^^ F401 +2 | x = y # F821 +3 | match 42: # invalid-syntax + | + = help: Remove unused import: `os` + +input.py:2:5: F821 Undefined name `y` + | +1 | import os # F401 +2 | x = y # F821 + | ^ F821 +3 | match 42: # invalid-syntax +4 | case _: ... + | + +input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + | +1 | import os # F401 +2 | x = y # F821 +3 | match 42: # invalid-syntax + | ^^^^^ +4 | case _: ... + | + +Found 3 errors. +[*] 1 fixable with the `--fix` option. + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_github.snap b/crates/ruff/tests/snapshots/lint__output_format_github.snap new file mode 100644 index 0000000000000..3f38f236526e7 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_github.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - github + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused +::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y` +::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_gitlab.snap b/crates/ruff/tests/snapshots/lint__output_format_gitlab.snap new file mode 100644 index 0000000000000..e25a54bfc315b --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_gitlab.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - gitlab + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +[ + { + "check_name": "F401", + "description": "`os` imported but unused", + "fingerprint": "4dbad37161e65c72", + "location": { + "lines": { + "begin": 1, + "end": 1 + }, + "path": "input.py" + }, + "severity": "major" + }, + { + "check_name": "F821", + "description": "Undefined name `y`", + "fingerprint": "7af59862a085230", + "location": { + "lines": { + "begin": 2, + "end": 2 + }, + "path": "input.py" + }, + "severity": "major" + }, + { + "check_name": "syntax-error", + "description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)", + "fingerprint": "e558cec859bb66e8", + "location": { + "lines": { + "begin": 3, + "end": 3 + }, + "path": "input.py" + }, + "severity": "major" + } +] +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_grouped.snap b/crates/ruff/tests/snapshots/lint__output_format_grouped.snap new file mode 100644 index 0000000000000..378ffe9d37684 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_grouped.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - grouped + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +input.py: + 1:8 F401 [*] `os` imported but unused + 2:5 F821 Undefined name `y` + 3:1 SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + +Found 3 errors. +[*] 1 fixable with the `--fix` option. + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_json-lines.snap b/crates/ruff/tests/snapshots/lint__output_format_json-lines.snap new file mode 100644 index 0000000000000..efadfd1cbc718 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_json-lines.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - json-lines + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"} +{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"} +{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null} + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_json.snap b/crates/ruff/tests/snapshots/lint__output_format_json.snap new file mode 100644 index 0000000000000..ca01d686006c4 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_json.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - json + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +[ + { + "cell": null, + "code": "F401", + "end_location": { + "column": 10, + "row": 1 + }, + "filename": "[TMP]/input.py", + "fix": { + "applicability": "safe", + "edits": [ + { + "content": "", + "end_location": { + "column": 1, + "row": 2 + }, + "location": { + "column": 1, + "row": 1 + } + } + ], + "message": "Remove unused import: `os`" + }, + "location": { + "column": 8, + "row": 1 + }, + "message": "`os` imported but unused", + "noqa_row": 1, + "url": "https://docs.astral.sh/ruff/rules/unused-import" + }, + { + "cell": null, + "code": "F821", + "end_location": { + "column": 6, + "row": 2 + }, + "filename": "[TMP]/input.py", + "fix": null, + "location": { + "column": 5, + "row": 2 + }, + "message": "Undefined name `y`", + "noqa_row": 2, + "url": "https://docs.astral.sh/ruff/rules/undefined-name" + }, + { + "cell": null, + "code": null, + "end_location": { + "column": 6, + "row": 3 + }, + "filename": "[TMP]/input.py", + "fix": null, + "location": { + "column": 1, + "row": 3 + }, + "message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)", + "noqa_row": null, + "url": null + } +] +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_junit.snap b/crates/ruff/tests/snapshots/lint__output_format_junit.snap new file mode 100644 index 0000000000000..30b5739172f49 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_junit.snap @@ -0,0 +1,34 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - junit + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- + + + + + line 1, col 8, `os` imported but unused + + + line 2, col 5, Undefined name `y` + + + line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + + + + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_pylint.snap b/crates/ruff/tests/snapshots/lint__output_format_pylint.snap new file mode 100644 index 0000000000000..8b30234a82b9b --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_pylint.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - pylint + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +input.py:1: [F401] `os` imported but unused +input.py:2: [F821] Undefined name `y` +input.py:3: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_rdjson.snap b/crates/ruff/tests/snapshots/lint__output_format_rdjson.snap new file mode 100644 index 0000000000000..65a188cbb7b0b --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_rdjson.snap @@ -0,0 +1,103 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - rdjson + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +{ + "diagnostics": [ + { + "code": { + "url": "https://docs.astral.sh/ruff/rules/unused-import", + "value": "F401" + }, + "location": { + "path": "[TMP]/input.py", + "range": { + "end": { + "column": 10, + "line": 1 + }, + "start": { + "column": 8, + "line": 1 + } + } + }, + "message": "`os` imported but unused", + "suggestions": [ + { + "range": { + "end": { + "column": 1, + "line": 2 + }, + "start": { + "column": 1, + "line": 1 + } + }, + "text": "" + } + ] + }, + { + "code": { + "url": "https://docs.astral.sh/ruff/rules/undefined-name", + "value": "F821" + }, + "location": { + "path": "[TMP]/input.py", + "range": { + "end": { + "column": 6, + "line": 2 + }, + "start": { + "column": 5, + "line": 2 + } + } + }, + "message": "Undefined name `y`" + }, + { + "code": { + "url": null, + "value": null + }, + "location": { + "path": "[TMP]/input.py", + "range": { + "end": { + "column": 6, + "line": 3 + }, + "start": { + "column": 1, + "line": 3 + } + } + }, + "message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" + } + ], + "severity": "warning", + "source": { + "name": "ruff", + "url": "https://docs.astral.sh/ruff" + } +} +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/lint__output_format_sarif.snap b/crates/ruff/tests/snapshots/lint__output_format_sarif.snap new file mode 100644 index 0000000000000..664f8f0d8a9d1 --- /dev/null +++ b/crates/ruff/tests/snapshots/lint__output_format_sarif.snap @@ -0,0 +1,142 @@ +--- +source: crates/ruff/tests/lint.rs +info: + program: ruff + args: + - check + - "--no-cache" + - "--output-format" + - sarif + - "--select" + - "F401,F821" + - "--target-version" + - py39 + - input.py +--- +success: false +exit_code: 1 +----- stdout ----- +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "[TMP]/input.py" + }, + "region": { + "endColumn": 10, + "endLine": 1, + "startColumn": 8, + "startLine": 1 + } + } + } + ], + "message": { + "text": "`os` imported but unused" + }, + "ruleId": "F401" + }, + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "[TMP]/input.py" + }, + "region": { + "endColumn": 6, + "endLine": 2, + "startColumn": 5, + "startLine": 2 + } + } + } + ], + "message": { + "text": "Undefined name `y`" + }, + "ruleId": "F821" + }, + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "[TMP]/input.py" + }, + "region": { + "endColumn": 6, + "endLine": 3, + "startColumn": 1, + "startLine": 3 + } + } + } + ], + "message": { + "text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" + }, + "ruleId": null + } + ], + "tool": { + "driver": { + "informationUri": "https://github.com/astral-sh/ruff", + "name": "ruff", + "rules": [ + { + "fullDescription": { + "text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n" + }, + "help": { + "text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" + }, + "helpUri": "https://docs.astral.sh/ruff/rules/unused-import", + "id": "F401", + "properties": { + "id": "F401", + "kind": "Pyflakes", + "name": "unused-import", + "problem.severity": "error" + }, + "shortDescription": { + "text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" + } + }, + { + "fullDescription": { + "text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n" + }, + "help": { + "text": "Undefined name `{name}`. {tip}" + }, + "helpUri": "https://docs.astral.sh/ruff/rules/undefined-name", + "id": "F821", + "properties": { + "id": "F821", + "kind": "Pyflakes", + "name": "undefined-name", + "problem.severity": "error" + }, + "shortDescription": { + "text": "Undefined name `{name}`. {tip}" + } + } + ], + "version": "[VERSION]" + } + } + } + ], + "version": "2.1.0" +} +----- stderr ----- diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index c404d07c8cd1f..37e8eae6bcac7 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -213,6 +213,7 @@ linter.task_tags = [ XXX, ] linter.typing_modules = [] +linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false diff --git a/crates/ruff_annotate_snippets/src/renderer/display_list.rs b/crates/ruff_annotate_snippets/src/renderer/display_list.rs index 48269294f1cfe..c445c6c898d46 100644 --- a/crates/ruff_annotate_snippets/src/renderer/display_list.rs +++ b/crates/ruff_annotate_snippets/src/renderer/display_list.rs @@ -32,7 +32,7 @@ //! //! The above snippet has been built out of the following structure: use crate::snippet; -use std::cmp::{max, min, Reverse}; +use std::cmp::{Reverse, max, min}; use std::collections::HashMap; use std::fmt::Display; use std::ops::Range; @@ -41,7 +41,7 @@ use std::{cmp, fmt}; use unicode_width::UnicodeWidthStr; use crate::renderer::styled_buffer::StyledBuffer; -use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH}; +use crate::renderer::{DEFAULT_TERM_WIDTH, Margin, Style, stylesheet::Stylesheet}; const ANONYMIZED_LINE_NUM: &str = "LL"; const ERROR_TXT: &str = "error"; @@ -352,7 +352,7 @@ impl DisplaySet<'_> { // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` // is. For now, just accept that sometimes the code line will be longer than // desired. - let next = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1); + let next = char_width(ch).unwrap_or(1); if taken + next > right - left { was_cut_right = true; break; @@ -377,7 +377,7 @@ impl DisplaySet<'_> { let left: usize = text .chars() .take(left) - .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)) + .map(|ch| char_width(ch).unwrap_or(1)) .sum(); let mut annotations = annotations.clone(); @@ -821,11 +821,7 @@ impl DisplaySourceAnnotation<'_> { // Length of this annotation as displayed in the stderr output fn len(&self) -> usize { // Account for usize underflows - if self.range.1 > self.range.0 { - self.range.1 - self.range.0 - } else { - self.range.0 - self.range.1 - } + self.range.1.abs_diff(self.range.0) } fn takes_space(&self) -> bool { @@ -1273,10 +1269,7 @@ fn fold_body(body: Vec>) -> Vec> { let inline_marks = lines .last() .and_then(|line| { - if let DisplayLine::Source { - ref inline_marks, .. - } = line - { + if let DisplayLine::Source { inline_marks, .. } = line { let inline_marks = inline_marks.clone(); Some(inline_marks) } else { @@ -1396,6 +1389,7 @@ fn format_body<'m>( let line_length: usize = line.len(); let line_range = (current_index, current_index + line_length); let end_line_size = end_line.len(); + body.push(DisplayLine::Source { lineno: Some(current_line), inline_marks: vec![], @@ -1455,12 +1449,12 @@ fn format_body<'m>( let annotation_start_col = line [0..(start - line_start_index).min(line_length)] .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .map(|c| char_width(c).unwrap_or(0)) .sum::(); let mut annotation_end_col = line [0..(end - line_start_index).min(line_length)] .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .map(|c| char_width(c).unwrap_or(0)) .sum::(); if annotation_start_col == annotation_end_col { // At least highlight something @@ -1502,7 +1496,7 @@ fn format_body<'m>( let annotation_start_col = line [0..(start - line_start_index).min(line_length)] .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .map(|c| char_width(c).unwrap_or(0)) .sum::(); let annotation_end_col = annotation_start_col + 1; @@ -1561,7 +1555,7 @@ fn format_body<'m>( { let end_mark = line[0..(end - line_start_index).min(line_length)] .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .map(|c| char_width(c).unwrap_or(0)) .sum::() .saturating_sub(1); // If the annotation ends on a line-end character, we @@ -1757,3 +1751,11 @@ fn format_inline_marks( } Ok(()) } + +fn char_width(c: char) -> Option { + if c == '\t' { + Some(4) + } else { + unicode_width::UnicodeWidthChar::width(c) + } +} diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.svg b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.svg new file mode 100644 index 0000000000000..e666b9076a41d --- /dev/null +++ b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.svg @@ -0,0 +1,38 @@ + + + + + + + error[E0308]: call-non-callable + + --> $DIR/main.py:5:9 + + | + + 4 | def f(): + + 5 | return (1 == '2')() # Tab indented + + | ^^^^^^^^^^^^ + + | + + + + diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.toml b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.toml new file mode 100644 index 0000000000000..d8d68473d6528 --- /dev/null +++ b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.toml @@ -0,0 +1,45 @@ + +# [crates/ruff_db/src/diagnostic/render.rs:123:47] diag.to_annotate() = Message { + # level: Error, + # id: Some( + # "call-non-callable", + # ), + # title: "Object of type `bool` is not callable", + # snippets: [ + # Snippet { + # origin: Some( + # "main.py", + # ), + # line_start: 1, + # source: "def f():\n\treturn (1 == '2')() # Tab indented\n", + # annotations: [ + # Annotation { + # range: 17..29, + # label: None, + # level: Error, + # }, + # ], + # fold: false, + # }, + # ], + # footer: [], +# } + +[message] +level = "Error" +id = "E0308" +title = "call-non-callable" + +[[message.snippets]] +source = "def f():\n\treturn (1 == '2')() # Tab indented\n" +line_start = 4 +origin = "$DIR/main.py" + +[[message.snippets.annotations]] +label = "" +level = "Error" +range = [17, 29] + +[renderer] +# anonymized_line_numbers = true +color = true diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.svg b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.svg new file mode 100644 index 0000000000000..228ec94551a8f --- /dev/null +++ b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.svg @@ -0,0 +1,36 @@ + + + + + + + error[E0308]: mismatched types + + --> $DIR/non-whitespace-trimming.rs:4:6 + + | + + 4 | ... s_data['d_dails'] = bb['contacted'][hostip]['ansible_facts']['ansible_devices']['vda']['vendor'] + " " + bb['contacted'][hostip... + + | ^^^^^^ + + | + + + + diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.toml b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.toml new file mode 100644 index 0000000000000..dee7e271d4591 --- /dev/null +++ b/crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.toml @@ -0,0 +1,20 @@ +[message] +level = "Error" +id = "E0308" +title = "mismatched types" + +[[message.snippets]] +source = """ + s_data['d_dails'] = bb['contacted'][hostip]['ansible_facts']['ansible_devices']['vda']['vendor'] + " " + bb['contacted'][hostip]['an +""" +line_start = 4 +origin = "$DIR/non-whitespace-trimming.rs" + +[[message.snippets.annotations]] +label = "" +level = "Error" +range = [5, 11] + +[renderer] +# anonymized_line_numbers = true +color = true diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.svg b/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.svg index e4f8a856b649b..67e2cfd9b2644 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.svg +++ b/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.svg @@ -21,7 +21,7 @@ error[E0308]: mismatched types - --> $DIR/non-whitespace-trimming.rs:4:242 + --> $DIR/non-whitespace-trimming.rs:4:238 | diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.toml b/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.toml index c6573ff40aa37..bfe9f317f2347 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.toml +++ b/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.toml @@ -13,12 +13,12 @@ origin = "$DIR/non-whitespace-trimming.rs" [[message.snippets.annotations]] label = "expected `()`, found integer" level = "Error" -range = [241, 243] +range = [237, 239] [[message.snippets.annotations]] label = "expected due to this" level = "Error" -range = [236, 238] +range = [232, 234] [renderer] diff --git a/crates/ruff_annotate_snippets/tests/fixtures/main.rs b/crates/ruff_annotate_snippets/tests/fixtures/main.rs index 0f116b56d8ad6..3ffafc393fa85 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/main.rs +++ b/crates/ruff_annotate_snippets/tests/fixtures/main.rs @@ -2,8 +2,8 @@ mod deserialize; use crate::deserialize::Fixture; use ruff_annotate_snippets::{Message, Renderer}; -use snapbox::data::DataFormat; use snapbox::Data; +use snapbox::data::DataFormat; use std::error::Error; fn main() { diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index cea63ae29f4c1..b6b4b40de2b33 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -19,43 +19,69 @@ doctest = false [[bench]] name = "linter" harness = false +required-features = ["instrumented"] [[bench]] name = "lexer" harness = false +required-features = ["instrumented"] [[bench]] name = "parser" harness = false +required-features = ["instrumented"] [[bench]] name = "formatter" harness = false +required-features = ["instrumented"] [[bench]] -name = "red_knot" +name = "ty" harness = false +required-features = ["instrumented"] + +[[bench]] +name = "ty_walltime" +harness = false +required-features = ["walltime"] [dependencies] +ruff_db = { workspace = true, features = ["testing"] } +ruff_python_ast = { workspace = true } +ruff_linter = { workspace = true, optional = true } +ruff_python_formatter = { workspace = true, optional = true } +ruff_python_parser = { workspace = true, optional = true } +ruff_python_trivia = { workspace = true, optional = true } +ty_project = { workspace = true, optional = true } + +divan = { workspace = true, optional = true } +anyhow = { workspace = true } codspeed-criterion-compat = { workspace = true, default-features = false, optional = true } -criterion = { workspace = true, default-features = false } +criterion = { workspace = true, default-features = false, optional = true } rayon = { workspace = true } rustc-hash = { workspace = true } - -[dev-dependencies] -ruff_db = { workspace = true } -ruff_linter = { workspace = true } -ruff_python_ast = { workspace = true } -ruff_python_formatter = { workspace = true } -ruff_python_parser = { workspace = true } -ruff_python_trivia = { workspace = true } -red_knot_project = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } [lints] workspace = true [features] +default = ["instrumented", "walltime"] +# Enables the benchmark that should only run with codspeed's instrumented runner +instrumented = [ + "criterion", + "ruff_linter", + "ruff_python_formatter", + "ruff_python_parser", + "ruff_python_trivia", + "ty_project", +] codspeed = ["codspeed-criterion-compat"] +# Enables benchmark that should only run with codspeed's walltime runner. +walltime = ["ruff_db/os", "ty_project", "divan"] [target.'cfg(target_os = "windows")'.dev-dependencies] mimalloc = { workspace = true } diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs index 69cb238f2ad3e..9b445b1dbd048 100644 --- a/crates/ruff_benchmark/benches/formatter.rs +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -1,14 +1,14 @@ use std::path::Path; use ruff_benchmark::criterion::{ - criterion_group, criterion_main, BenchmarkId, Criterion, Throughput, + BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, }; use ruff_benchmark::{ - TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN, + LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN, }; -use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions}; -use ruff_python_parser::{parse, Mode, ParseOptions}; +use ruff_python_formatter::{PreviewMode, PyFormatOptions, format_module_ast}; +use ruff_python_parser::{Mode, ParseOptions, parse}; use ruff_python_trivia::CommentRanges; #[cfg(target_os = "windows")] diff --git a/crates/ruff_benchmark/benches/lexer.rs b/crates/ruff_benchmark/benches/lexer.rs index 40d06761dd780..cbba77c78b9e4 100644 --- a/crates/ruff_benchmark/benches/lexer.rs +++ b/crates/ruff_benchmark/benches/lexer.rs @@ -1,12 +1,12 @@ use ruff_benchmark::criterion; use criterion::{ - criterion_group, criterion_main, measurement::WallTime, BenchmarkId, Criterion, Throughput, + BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, }; use ruff_benchmark::{ - TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN, + LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN, }; -use ruff_python_parser::{lexer, Mode, TokenKind}; +use ruff_python_parser::{Mode, TokenKind, lexer}; #[cfg(target_os = "windows")] #[global_allocator] diff --git a/crates/ruff_benchmark/benches/linter.rs b/crates/ruff_benchmark/benches/linter.rs index 712bd73b6ca88..f90893334b38d 100644 --- a/crates/ruff_benchmark/benches/linter.rs +++ b/crates/ruff_benchmark/benches/linter.rs @@ -1,18 +1,18 @@ use ruff_benchmark::criterion; use criterion::{ - criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput, + BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, }; use ruff_benchmark::{ - TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN, + LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN, }; -use ruff_linter::linter::{lint_only, ParseSource}; +use ruff_linter::linter::{ParseSource, lint_only}; use ruff_linter::rule_selector::PreviewOptions; use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::types::PreviewMode; -use ruff_linter::settings::{flags, LinterSettings}; +use ruff_linter::settings::{LinterSettings, flags}; use ruff_linter::source_kind::SourceKind; -use ruff_linter::{registry::Rule, RuleSelector}; +use ruff_linter::{RuleSelector, registry::Rule}; use ruff_python_ast::PySourceType; use ruff_python_parser::parse_module; @@ -45,9 +45,9 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; target_arch = "powerpc64" ) ))] -#[allow(non_upper_case_globals)] -#[export_name = "_rjem_malloc_conf"] -#[allow(unsafe_code)] +#[unsafe(export_name = "_rjem_malloc_conf")] +#[expect(non_upper_case_globals)] +#[expect(unsafe_code)] pub static _rjem_malloc_conf: &[u8] = b"dirty_decay_ms:-1,muzzy_decay_ms:-1\0"; fn create_test_cases() -> Vec { diff --git a/crates/ruff_benchmark/benches/parser.rs b/crates/ruff_benchmark/benches/parser.rs index a78a64c85b801..ac648aabc48c0 100644 --- a/crates/ruff_benchmark/benches/parser.rs +++ b/crates/ruff_benchmark/benches/parser.rs @@ -1,13 +1,13 @@ use ruff_benchmark::criterion; use criterion::{ - criterion_group, criterion_main, measurement::WallTime, BenchmarkId, Criterion, Throughput, + BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, }; use ruff_benchmark::{ - TestCase, LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, UNICODE_PYPINYIN, + LARGE_DATASET, NUMPY_CTYPESLIB, NUMPY_GLOBALS, PYDANTIC_TYPES, TestCase, UNICODE_PYPINYIN, }; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; use ruff_python_ast::Stmt; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_parser::parse_module; #[cfg(target_os = "windows")] diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs deleted file mode 100644 index e0b0da246ba74..0000000000000 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ /dev/null @@ -1,312 +0,0 @@ -#![allow(clippy::disallowed_names)] -use ruff_benchmark::criterion; - -use std::ops::Range; - -use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; -use rayon::ThreadPoolBuilder; -use rustc_hash::FxHashSet; - -use red_knot_project::metadata::options::{EnvironmentOptions, Options}; -use red_knot_project::metadata::value::RangedValue; -use red_knot_project::watch::{ChangeEvent, ChangedKind}; -use red_knot_project::{Db, ProjectDatabase, ProjectMetadata}; -use ruff_benchmark::TestFile; -use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; -use ruff_db::files::{system_path_to_file, File}; -use ruff_db::source::source_text; -use ruff_db::system::{MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem}; -use ruff_python_ast::PythonVersion; - -struct Case { - db: ProjectDatabase, - fs: MemoryFileSystem, - file: File, - file_path: SystemPathBuf, -} - -// "https://raw.githubusercontent.com/python/cpython/8e8a4baf652f6e1cee7acde9d78c4b6154539748/Lib/tomllib"; -static TOMLLIB_FILES: [TestFile; 4] = [ - TestFile::new( - "tomllib/__init__.py", - include_str!("../resources/tomllib/__init__.py"), - ), - TestFile::new( - "tomllib/_parser.py", - include_str!("../resources/tomllib/_parser.py"), - ), - TestFile::new( - "tomllib/_re.py", - include_str!("../resources/tomllib/_re.py"), - ), - TestFile::new( - "tomllib/_types.py", - include_str!("../resources/tomllib/_types.py"), - ), -]; - -/// A structured set of fields we use to do diagnostic comparisons. -/// -/// This helps assert benchmark results. Previously, we would compare -/// the actual diagnostic output, but using `insta` inside benchmarks is -/// problematic, and updating the strings otherwise when diagnostic rendering -/// changes is a PITA. -type KeyDiagnosticFields = ( - DiagnosticId, - Option<&'static str>, - Option>, - &'static str, - Severity, -); - -static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[( - DiagnosticId::lint("unused-ignore-comment"), - Some("/src/tomllib/_parser.py"), - Some(22299..22333), - "Unused blanket `type: ignore` directive", - Severity::Warning, -)]; - -fn tomllib_path(file: &TestFile) -> SystemPathBuf { - SystemPathBuf::from("src").join(file.name()) -} - -fn setup_tomllib_case() -> Case { - let system = TestSystem::default(); - let fs = system.memory_file_system().clone(); - - fs.write_files_all( - TOMLLIB_FILES - .iter() - .map(|file| (tomllib_path(file), file.code().to_string())), - ) - .unwrap(); - - let src_root = SystemPath::new("/src"); - let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); - metadata.apply_cli_options(Options { - environment: Some(EnvironmentOptions { - python_version: Some(RangedValue::cli(PythonVersion::PY312)), - ..EnvironmentOptions::default() - }), - ..Options::default() - }); - - let mut db = ProjectDatabase::new(metadata, system).unwrap(); - let mut tomllib_files = FxHashSet::default(); - let mut re: Option = None; - - for test_file in &TOMLLIB_FILES { - let file = system_path_to_file(&db, tomllib_path(test_file)).unwrap(); - if test_file.name().ends_with("_re.py") { - re = Some(file); - } - tomllib_files.insert(file); - } - - let re = re.unwrap(); - - db.project().set_open_files(&mut db, tomllib_files); - - let re_path = re.path(&db).as_system_path().unwrap().to_owned(); - Case { - db, - fs, - file: re, - file_path: re_path, - } -} - -static RAYON_INITIALIZED: std::sync::Once = std::sync::Once::new(); - -fn setup_rayon() { - // Initialize the rayon thread pool outside the benchmark because it has a significant cost. - // We limit the thread pool to only one (the current thread) because we're focused on - // where red knot spends time and less about how well the code runs concurrently. - // We might want to add a benchmark focusing on concurrency to detect congestion in the future. - RAYON_INITIALIZED.call_once(|| { - ThreadPoolBuilder::new() - .num_threads(1) - .use_current_thread() - .build_global() - .unwrap(); - }); -} - -fn benchmark_incremental(criterion: &mut Criterion) { - fn setup() -> Case { - let case = setup_tomllib_case(); - - let result: Vec<_> = case.db.check().unwrap(); - - assert_diagnostics(&case.db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); - - case.fs - .write_file_all( - &case.file_path, - format!( - "{}\n# A comment\n", - source_text(&case.db, case.file).as_str() - ), - ) - .unwrap(); - - case - } - - fn incremental(case: &mut Case) { - let Case { db, .. } = case; - - db.apply_changes( - vec![ChangeEvent::Changed { - path: case.file_path.clone(), - kind: ChangedKind::FileContent, - }], - None, - ); - - let result = db.check().unwrap(); - - assert_eq!(result.len(), EXPECTED_TOMLLIB_DIAGNOSTICS.len()); - } - - setup_rayon(); - - criterion.bench_function("red_knot_check_file[incremental]", |b| { - b.iter_batched_ref(setup, incremental, BatchSize::SmallInput); - }); -} - -fn benchmark_cold(criterion: &mut Criterion) { - setup_rayon(); - - criterion.bench_function("red_knot_check_file[cold]", |b| { - b.iter_batched_ref( - setup_tomllib_case, - |case| { - let Case { db, .. } = case; - let result: Vec<_> = db.check().unwrap(); - - assert_diagnostics(db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); - }, - BatchSize::SmallInput, - ); - }); -} - -#[track_caller] -fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDiagnosticFields]) { - let normalized: Vec<_> = diagnostics - .iter() - .map(|diagnostic| { - ( - diagnostic.id(), - diagnostic - .primary_span() - .map(|span| span.file()) - .map(|file| file.path(db).as_str()), - diagnostic - .primary_span() - .and_then(|span| span.range()) - .map(Range::::from), - diagnostic.primary_message(), - diagnostic.severity(), - ) - }) - .collect(); - assert_eq!(&normalized, expected); -} - -fn setup_micro_case(code: &str) -> Case { - let system = TestSystem::default(); - let fs = system.memory_file_system().clone(); - - let file_path = "src/test.py"; - fs.write_file_all( - SystemPathBuf::from(file_path), - ruff_python_trivia::textwrap::dedent(code), - ) - .unwrap(); - - let src_root = SystemPath::new("/src"); - let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); - metadata.apply_cli_options(Options { - environment: Some(EnvironmentOptions { - python_version: Some(RangedValue::cli(PythonVersion::PY312)), - ..EnvironmentOptions::default() - }), - ..Options::default() - }); - - let mut db = ProjectDatabase::new(metadata, system).unwrap(); - let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap(); - - db.project() - .set_open_files(&mut db, FxHashSet::from_iter([file])); - - let file_path = file.path(&db).as_system_path().unwrap().to_owned(); - - Case { - db, - fs, - file, - file_path, - } -} - -fn benchmark_many_string_assignments(criterion: &mut Criterion) { - setup_rayon(); - - criterion.bench_function("red_knot_micro[many_string_assignments]", |b| { - b.iter_batched_ref( - || { - // This is a micro benchmark, but it is effectively identical to a code sample - // observed "in the wild": - setup_micro_case( - r#" - def f(x) -> str: - s = "" - # Each conditional doubles the size of the union of string literal types, - # so if we go up to attr10, we have 2**10 = 1024 string literal types - if x.attr1: - s += "attr1" - if x.attr2: - s += "attr2" - if x.attr3: - s += "attr3" - if x.attr4: - s += "attr4" - if x.attr5: - s += "attr5" - if x.attr6: - s += "attr6" - if x.attr7: - s += "attr7" - if x.attr8: - s += "attr8" - if x.attr9: - s += "attr9" - if x.attr10: - s += "attr10" - # The above checked how fast we are in building the union; this checks how - # we manage it once it is built. If implemented naively, this has to check - # each member of the union for compatibility with the Sized protocol. - if len(s) > 0: - s = s[:-3] - return s - "#, - ) - }, - |case| { - let Case { db, .. } = case; - let result = db.check().unwrap(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -criterion_group!(check_file, benchmark_cold, benchmark_incremental); -criterion_group!(micro, benchmark_many_string_assignments); -criterion_main!(check_file, micro); diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs new file mode 100644 index 0000000000000..905f30b9bd69e --- /dev/null +++ b/crates/ruff_benchmark/benches/ty.rs @@ -0,0 +1,596 @@ +#![allow(clippy::disallowed_names)] +use ruff_benchmark::criterion; +use ruff_benchmark::real_world_projects::{InstalledProject, RealWorldProject}; + +use std::ops::Range; + +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use rayon::ThreadPoolBuilder; +use rustc_hash::FxHashSet; + +use ruff_benchmark::TestFile; +use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; +use ruff_db::files::{File, system_path_to_file}; +use ruff_db::source::source_text; +use ruff_db::system::{InMemorySystem, MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem}; +use ruff_python_ast::PythonVersion; +use ty_project::metadata::options::{EnvironmentOptions, Options}; +use ty_project::metadata::value::{RangedValue, RelativePathBuf}; +use ty_project::watch::{ChangeEvent, ChangedKind}; +use ty_project::{Db, ProjectDatabase, ProjectMetadata}; + +struct Case { + db: ProjectDatabase, + fs: MemoryFileSystem, + file: File, + file_path: SystemPathBuf, +} + +// "https://raw.githubusercontent.com/python/cpython/8e8a4baf652f6e1cee7acde9d78c4b6154539748/Lib/tomllib"; +static TOMLLIB_FILES: [TestFile; 4] = [ + TestFile::new( + "tomllib/__init__.py", + include_str!("../resources/tomllib/__init__.py"), + ), + TestFile::new( + "tomllib/_parser.py", + include_str!("../resources/tomllib/_parser.py"), + ), + TestFile::new( + "tomllib/_re.py", + include_str!("../resources/tomllib/_re.py"), + ), + TestFile::new( + "tomllib/_types.py", + include_str!("../resources/tomllib/_types.py"), + ), +]; + +/// A structured set of fields we use to do diagnostic comparisons. +/// +/// This helps assert benchmark results. Previously, we would compare +/// the actual diagnostic output, but using `insta` inside benchmarks is +/// problematic, and updating the strings otherwise when diagnostic rendering +/// changes is a PITA. +type KeyDiagnosticFields = ( + DiagnosticId, + Option<&'static str>, + Option>, + &'static str, + Severity, +); + +static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[]; + +fn tomllib_path(file: &TestFile) -> SystemPathBuf { + SystemPathBuf::from("src").join(file.name()) +} + +fn setup_tomllib_case() -> Case { + let system = TestSystem::default(); + let fs = system.memory_file_system().clone(); + + fs.write_files_all( + TOMLLIB_FILES + .iter() + .map(|file| (tomllib_path(file), file.code().to_string())), + ) + .unwrap(); + + let src_root = SystemPath::new("/src"); + let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); + metadata.apply_options(Options { + environment: Some(EnvironmentOptions { + python_version: Some(RangedValue::cli(PythonVersion::PY312)), + ..EnvironmentOptions::default() + }), + ..Options::default() + }); + + let mut db = ProjectDatabase::new(metadata, system).unwrap(); + let mut tomllib_files = FxHashSet::default(); + let mut re: Option = None; + + for test_file in &TOMLLIB_FILES { + let file = system_path_to_file(&db, tomllib_path(test_file)).unwrap(); + if test_file.name().ends_with("_re.py") { + re = Some(file); + } + tomllib_files.insert(file); + } + + let re = re.unwrap(); + + db.project().set_open_files(&mut db, tomllib_files); + + let re_path = re.path(&db).as_system_path().unwrap().to_owned(); + Case { + db, + fs, + file: re, + file_path: re_path, + } +} + +static RAYON_INITIALIZED: std::sync::Once = std::sync::Once::new(); + +fn setup_rayon() { + // Initialize the rayon thread pool outside the benchmark because it has a significant cost. + // We limit the thread pool to only one (the current thread) because we're focused on + // where ty spends time and less about how well the code runs concurrently. + // We might want to add a benchmark focusing on concurrency to detect congestion in the future. + RAYON_INITIALIZED.call_once(|| { + ThreadPoolBuilder::new() + .num_threads(1) + .use_current_thread() + .build_global() + .unwrap(); + }); +} + +fn benchmark_incremental(criterion: &mut Criterion) { + fn setup() -> Case { + let case = setup_tomllib_case(); + + let result: Vec<_> = case.db.check(); + + assert_diagnostics(&case.db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); + + case.fs + .write_file_all( + &case.file_path, + format!( + "{}\n# A comment\n", + source_text(&case.db, case.file).as_str() + ), + ) + .unwrap(); + + case + } + + fn incremental(case: &mut Case) { + let Case { db, .. } = case; + + db.apply_changes( + vec![ChangeEvent::Changed { + path: case.file_path.clone(), + kind: ChangedKind::FileContent, + }], + None, + ); + + let result = db.check(); + + assert_eq!(result.len(), EXPECTED_TOMLLIB_DIAGNOSTICS.len()); + } + + setup_rayon(); + + criterion.bench_function("ty_check_file[incremental]", |b| { + b.iter_batched_ref(setup, incremental, BatchSize::SmallInput); + }); +} + +fn benchmark_cold(criterion: &mut Criterion) { + setup_rayon(); + + criterion.bench_function("ty_check_file[cold]", |b| { + b.iter_batched_ref( + setup_tomllib_case, + |case| { + let Case { db, .. } = case; + let result: Vec<_> = db.check(); + + assert_diagnostics(db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); + }, + BatchSize::SmallInput, + ); + }); +} + +#[track_caller] +fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDiagnosticFields]) { + let normalized: Vec<_> = diagnostics + .iter() + .map(|diagnostic| { + ( + diagnostic.id(), + diagnostic + .primary_span() + .map(|span| span.expect_ty_file()) + .map(|file| file.path(db).as_str()), + diagnostic + .primary_span() + .and_then(|span| span.range()) + .map(Range::::from), + diagnostic.primary_message(), + diagnostic.severity(), + ) + }) + .collect(); + assert_eq!(&normalized, expected); +} + +fn setup_micro_case(code: &str) -> Case { + let system = TestSystem::default(); + let fs = system.memory_file_system().clone(); + + let file_path = "src/test.py"; + fs.write_file_all( + SystemPathBuf::from(file_path), + ruff_python_trivia::textwrap::dedent(code), + ) + .unwrap(); + + let src_root = SystemPath::new("/src"); + let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); + metadata.apply_options(Options { + environment: Some(EnvironmentOptions { + python_version: Some(RangedValue::cli(PythonVersion::PY312)), + ..EnvironmentOptions::default() + }), + ..Options::default() + }); + + let mut db = ProjectDatabase::new(metadata, system).unwrap(); + let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap(); + + db.project() + .set_open_files(&mut db, FxHashSet::from_iter([file])); + + let file_path = file.path(&db).as_system_path().unwrap().to_owned(); + + Case { + db, + fs, + file, + file_path, + } +} + +fn benchmark_many_string_assignments(criterion: &mut Criterion) { + setup_rayon(); + + criterion.bench_function("ty_micro[many_string_assignments]", |b| { + b.iter_batched_ref( + || { + // This is a micro benchmark, but it is effectively identical to a code sample + // observed "in the wild": + setup_micro_case( + r#" + def f(x) -> str: + s = "" + # Each conditional doubles the size of the union of string literal types, + # so if we go up to attr10, we have 2**10 = 1024 string literal types + if x.attr1: + s += "attr1" + if x.attr2: + s += "attr2" + if x.attr3: + s += "attr3" + if x.attr4: + s += "attr4" + if x.attr5: + s += "attr5" + if x.attr6: + s += "attr6" + if x.attr7: + s += "attr7" + if x.attr8: + s += "attr8" + if x.attr9: + s += "attr9" + if x.attr10: + s += "attr10" + # The above checked how fast we are in building the union; this checks how + # we manage it once it is built. If implemented naively, this has to check + # each member of the union for compatibility with the Sized protocol. + if len(s) > 0: + s = s[:-3] + return s + "#, + ) + }, + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_many_tuple_assignments(criterion: &mut Criterion) { + setup_rayon(); + + criterion.bench_function("ty_micro[many_tuple_assignments]", |b| { + b.iter_batched_ref( + || { + // This is a micro benchmark, but it is effectively identical to a code sample + // observed in https://github.com/astral-sh/ty/issues/362 + setup_micro_case( + r#" + def flag() -> bool: + return True + + t = () + if flag(): + t += (1,) + if flag(): + t += (2,) + if flag(): + t += (3,) + if flag(): + t += (4,) + if flag(): + t += (5,) + if flag(): + t += (6,) + if flag(): + t += (7,) + if flag(): + t += (8,) + + # Perform some kind of operation on the union type + print(1 in t) + "#, + ) + }, + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) { + setup_rayon(); + + criterion.bench_function("ty_micro[complex_constrained_attributes_1]", |b| { + b.iter_batched_ref( + || { + // This is a regression benchmark for https://github.com/astral-sh/ty/issues/627. + // Before this was fixed, the following sample would take >1s to type check. + setup_micro_case( + r#" + class C: + def f(self: "C"): + if isinstance(self.a, str): + return + + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + "#, + ) + }, + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert!(!result.is_empty()); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) { + setup_rayon(); + + criterion.bench_function("ty_micro[complex_constrained_attributes_2]", |b| { + b.iter_batched_ref( + || { + // This is is similar to the case above, but now the attributes are actually defined. + // https://github.com/astral-sh/ty/issues/711 + setup_micro_case( + r#" + class C: + def f(self: "C"): + self.a = "" + self.b = "" + + if isinstance(self.a, str): + return + + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + if isinstance(self.b, str): + return + "#, + ) + }, + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +struct ProjectBenchmark<'a> { + project: InstalledProject<'a>, + fs: MemoryFileSystem, + max_diagnostics: usize, +} + +impl<'a> ProjectBenchmark<'a> { + fn new(project: RealWorldProject<'a>, max_diagnostics: usize) -> Self { + let setup_project = project.setup().expect("Failed to setup project"); + let fs = setup_project + .copy_to_memory_fs() + .expect("Failed to copy project to memory fs"); + + Self { + project: setup_project, + fs, + max_diagnostics, + } + } + + fn setup_iteration(&self) -> ProjectDatabase { + let system = TestSystem::new(InMemorySystem::from_memory_fs(self.fs.clone())); + + let src_root = SystemPath::new("/"); + let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); + + metadata.apply_options(Options { + environment: Some(EnvironmentOptions { + python_version: Some(RangedValue::cli(self.project.config.python_version)), + python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))), + ..EnvironmentOptions::default() + }), + ..Options::default() + }); + + let mut db = ProjectDatabase::new(metadata, system).unwrap(); + + db.project().set_included_paths( + &mut db, + self.project + .check_paths() + .iter() + .map(|path| path.to_path_buf()) + .collect(), + ); + + db + } +} + +#[track_caller] +fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) { + fn check_project(db: &mut ProjectDatabase, max_diagnostics: usize) { + let result = db.check(); + let diagnostics = result.len(); + + assert!( + diagnostics <= max_diagnostics, + "Expected <={max_diagnostics} diagnostics but got {diagnostics}" + ); + } + + setup_rayon(); + + let mut group = criterion.benchmark_group("project"); + group.sampling_mode(criterion::SamplingMode::Flat); + group.bench_function(benchmark.project.config.name, |b| { + b.iter_batched_ref( + || benchmark.setup_iteration(), + |db| check_project(db, benchmark.max_diagnostics), + BatchSize::SmallInput, + ); + }); +} + +fn hydra(criterion: &mut Criterion) { + let benchmark = ProjectBenchmark::new( + RealWorldProject { + name: "hydra-zen", + repository: "https://github.com/mit-ll-responsible-ai/hydra-zen", + commit: "dd2b50a9614c6f8c46c5866f283c8f7e7a960aa8", + paths: vec![SystemPath::new("src")], + dependencies: vec!["pydantic", "beartype", "hydra-core"], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY313, + }, + 100, + ); + + bench_project(&benchmark, criterion); +} + +fn attrs(criterion: &mut Criterion) { + let benchmark = ProjectBenchmark::new( + RealWorldProject { + name: "attrs", + repository: "https://github.com/python-attrs/attrs", + commit: "a6ae894aad9bc09edc7cdad8c416898784ceec9b", + paths: vec![SystemPath::new("src")], + dependencies: vec![], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY313, + }, + 100, + ); + + bench_project(&benchmark, criterion); +} + +fn anyio(criterion: &mut Criterion) { + let benchmark = ProjectBenchmark::new( + RealWorldProject { + name: "anyio", + repository: "https://github.com/agronholm/anyio", + commit: "561d81270a12f7c6bbafb5bc5fad99a2a13f96be", + paths: vec![SystemPath::new("src")], + dependencies: vec![], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY313, + }, + 100, + ); + + bench_project(&benchmark, criterion); +} + +fn datetype(criterion: &mut Criterion) { + let benchmark = ProjectBenchmark::new( + RealWorldProject { + name: "DateType", + repository: "https://github.com/glyph/DateType", + commit: "57c9c93cf2468069f72945fc04bf27b64100dad8", + paths: vec![SystemPath::new("src")], + dependencies: vec![], + max_dep_date: "2025-07-04", + python_version: PythonVersion::PY313, + }, + 0, + ); + + bench_project(&benchmark, criterion); +} + +criterion_group!(check_file, benchmark_cold, benchmark_incremental); +criterion_group!( + micro, + benchmark_many_string_assignments, + benchmark_many_tuple_assignments, + benchmark_complex_constrained_attributes_1, + benchmark_complex_constrained_attributes_2, +); +criterion_group!(project, anyio, attrs, hydra, datetype); +criterion_main!(check_file, micro, project); diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs new file mode 100644 index 0000000000000..77d0ced3e8c64 --- /dev/null +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -0,0 +1,281 @@ +use std::fmt::{Display, Formatter}; + +use divan::{Bencher, bench}; + +use rayon::ThreadPoolBuilder; +use ruff_benchmark::real_world_projects::{InstalledProject, RealWorldProject}; +use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; + +use ruff_db::testing::setup_logging_with_filter; +use ruff_python_ast::PythonVersion; +use ty_project::metadata::options::{EnvironmentOptions, Options}; +use ty_project::metadata::value::{RangedValue, RelativePathBuf}; +use ty_project::{Db, ProjectDatabase, ProjectMetadata}; + +struct Benchmark<'a> { + project: InstalledProject<'a>, + max_diagnostics: usize, +} + +impl<'a> Benchmark<'a> { + fn new(project: RealWorldProject<'a>, max_diagnostics: usize) -> Self { + let setup_project = project.setup().expect("Failed to setup project"); + + Self { + project: setup_project, + max_diagnostics, + } + } + + fn setup_iteration(&self) -> ProjectDatabase { + let root = SystemPathBuf::from_path_buf(self.project.path.clone()).unwrap(); + let system = OsSystem::new(&root); + + let mut metadata = ProjectMetadata::discover(&root, &system).unwrap(); + + metadata.apply_options(Options { + environment: Some(EnvironmentOptions { + python_version: Some(RangedValue::cli(self.project.config.python_version)), + python: Some(RelativePathBuf::cli(SystemPath::new(".venv"))), + ..EnvironmentOptions::default() + }), + ..Options::default() + }); + + let mut db = ProjectDatabase::new(metadata, system).unwrap(); + + db.project().set_included_paths( + &mut db, + self.project + .check_paths() + .iter() + .map(|path| SystemPath::absolute(path, &root)) + .collect(), + ); + db + } +} + +impl Display for Benchmark<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.project.config.name) + } +} + +fn check_project(db: &ProjectDatabase, max_diagnostics: usize) { + let result = db.check(); + let diagnostics = result.len(); + + assert!( + diagnostics > 1 && diagnostics <= max_diagnostics, + "Expected between {} and {} diagnostics but got {}", + 1, + max_diagnostics, + diagnostics + ); +} + +static ALTAIR: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "altair", + repository: "https://github.com/vega/altair", + commit: "d1f4a1ef89006e5f6752ef1f6df4b7a509336fba", + paths: vec![SystemPath::new("altair")], + dependencies: vec![ + "jinja2", + "narwhals", + "numpy", + "packaging", + "pandas-stubs", + "pyarrow-stubs", + "pytest", + "scipy-stubs", + "types-jsonschema", + ], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY312, + }, + 1000, + ) +}); + +static COLOUR_SCIENCE: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "colour-science", + repository: "https://github.com/colour-science/colour", + commit: "a17e2335c29e7b6f08080aa4c93cfa9b61f84757", + paths: vec![SystemPath::new("colour")], + dependencies: vec![ + "matplotlib", + "numpy", + "pandas-stubs", + "pytest", + "scipy-stubs", + ], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY310, + }, + 477, + ) +}); + +static FREQTRADE: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "freqtrade", + repository: "https://github.com/freqtrade/freqtrade", + commit: "2d842ea129e56575852ee0c45383c8c3f706be19", + paths: vec![SystemPath::new("freqtrade")], + dependencies: vec![ + "numpy", + "pandas-stubs", + "pydantic", + "sqlalchemy", + "types-cachetools", + "types-filelock", + "types-python-dateutil", + "types-requests", + "types-tabulate", + ], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY312, + }, + 400, + ) +}); + +static PANDAS: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "pandas", + repository: "https://github.com/pandas-dev/pandas", + commit: "5909621e2267eb67943a95ef5e895e8484c53432", + paths: vec![SystemPath::new("pandas")], + dependencies: vec![ + "numpy", + "types-python-dateutil", + "types-pytz", + "types-PyMySQL", + "types-setuptools", + "pytest", + ], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY312, + }, + 3000, + ) +}); + +static PYDANTIC: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "pydantic", + repository: "https://github.com/pydantic/pydantic", + commit: "0c4a22b64b23dfad27387750cf07487efc45eb05", + paths: vec![SystemPath::new("pydantic")], + dependencies: vec![ + "annotated-types", + "pydantic-core", + "typing-extensions", + "typing-inspection", + ], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY39, + }, + 1000, + ) +}); + +static SYMPY: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "sympy", + repository: "https://github.com/sympy/sympy", + commit: "22fc107a94eaabc4f6eb31470b39db65abb7a394", + paths: vec![SystemPath::new("sympy")], + dependencies: vec!["mpmath"], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY312, + }, + 13000, + ) +}); + +static TANJUN: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + Benchmark::new( + RealWorldProject { + name: "tanjun", + repository: "https://github.com/FasterSpeeding/Tanjun", + commit: "69f40db188196bc59516b6c69849c2d85fbc2f4a", + paths: vec![SystemPath::new("tanjun")], + dependencies: vec!["hikari", "alluka"], + max_dep_date: "2025-06-17", + python_version: PythonVersion::PY312, + }, + 100, + ) +}); + +#[track_caller] +fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) { + bencher + .with_inputs(|| benchmark.setup_iteration()) + .bench_local_refs(|db| { + check_project(db, benchmark.max_diagnostics); + }); +} + +#[bench(args=[&*ALTAIR, &*FREQTRADE, &*PYDANTIC, &*TANJUN], sample_size=2, sample_count=3)] +fn small(bencher: Bencher, benchmark: &Benchmark) { + run_single_threaded(bencher, benchmark); +} + +#[bench(args=[&*COLOUR_SCIENCE, &*PANDAS], sample_size=1, sample_count=3)] +fn medium(bencher: Bencher, benchmark: &Benchmark) { + run_single_threaded(bencher, benchmark); +} + +#[bench(args=[&*SYMPY], sample_size=1, sample_count=2)] +fn large(bencher: Bencher, benchmark: &Benchmark) { + run_single_threaded(bencher, benchmark); +} + +#[bench(args=[&*PYDANTIC], sample_size=3, sample_count=8)] +fn multithreaded(bencher: Bencher, benchmark: &Benchmark) { + let thread_pool = ThreadPoolBuilder::new().build().unwrap(); + + bencher + .with_inputs(|| benchmark.setup_iteration()) + .bench_local_values(|db| { + thread_pool.install(|| { + check_project(&db, benchmark.max_diagnostics); + db + }) + }); +} + +fn main() { + ThreadPoolBuilder::new() + .num_threads(1) + .use_current_thread() + .build_global() + .unwrap(); + + let filter = + std::env::var("TY_LOG").unwrap_or("ty_walltime=info,ruff_benchmark=info".to_string()); + + let _logging = setup_logging_with_filter(&filter).expect("Filter to be valid"); + + // Salsa uses an optimized lookup for the ingredient index when using only a single database. + // This optimization results in at least a 10% speedup compared to when using multiple databases. + // To reduce noise, run one benchmark so that all benchmarks take the less optimized "not the first db" + // branch when looking up the ingredient index. + { + let db = TANJUN.setup_iteration(); + check_project(&db, TANJUN.max_diagnostics); + } + + divan::main(); +} diff --git a/crates/ruff_benchmark/src/lib.rs b/crates/ruff_benchmark/src/lib.rs index 3ecde5e8f8ee2..34ba0d63640de 100644 --- a/crates/ruff_benchmark/src/lib.rs +++ b/crates/ruff_benchmark/src/lib.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; +#[cfg(feature = "instrumented")] pub mod criterion; +pub mod real_world_projects; pub static NUMPY_GLOBALS: TestFile = TestFile::new( "numpy/globals.py", diff --git a/crates/ruff_benchmark/src/real_world_projects.rs b/crates/ruff_benchmark/src/real_world_projects.rs new file mode 100644 index 0000000000000..0e23e674945fe --- /dev/null +++ b/crates/ruff_benchmark/src/real_world_projects.rs @@ -0,0 +1,398 @@ +#![allow(clippy::print_stderr)] + +//! Infrastructure for benchmarking real-world Python projects. +//! +//! The module uses a setup similar to mypy primer's, which should make it easy +//! to add new benchmarks for projects in [mypy primer's project's list](https://github.com/hauntsaninja/mypy_primer/blob/ebaa9fd27b51a278873b63676fd25490cec6823b/mypy_primer/projects.py#L74). +//! +//! The basic steps for a project are: +//! 1. Clone or update the project into a directory inside `./target`. The commits are pinnted to prevent flaky benchmark results due to new commits. +//! 2. For projects with dependencies, run uv to create a virtual environment and install the dependencies. +//! 3. (optionally) Copy the entire project structure into a memory file system to reduce the IO noise in benchmarks. +//! 4. (not in this module) Create a `ProjectDatabase` and run the benchmark. + +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::Instant; + +use anyhow::{Context, Result}; +use ruff_db::system::{MemoryFileSystem, SystemPath, SystemPathBuf}; +use ruff_python_ast::PythonVersion; + +/// Configuration for a real-world project to benchmark +#[derive(Debug, Clone)] +pub struct RealWorldProject<'a> { + // The name of the project. + pub name: &'a str, + /// The project's GIT repository. Must be publicly accessible. + pub repository: &'a str, + /// Specific commit hash to checkout + pub commit: &'a str, + /// List of paths within the project to check (`ty check `) + pub paths: Vec<&'a SystemPath>, + /// Dependencies to install via uv + pub dependencies: Vec<&'a str>, + /// Limit candidate packages to those that were uploaded prior to a given point in time (ISO 8601 format). + /// Maps to uv's `exclude-newer`. + pub max_dep_date: &'a str, + /// Python version to use + pub python_version: PythonVersion, +} + +impl<'a> RealWorldProject<'a> { + /// Setup a real-world project for benchmarking + pub fn setup(self) -> Result> { + let start = Instant::now(); + tracing::debug!("Setting up project {}", self.name); + + // Create project directory in cargo target + let project_root = get_project_cache_dir(self.name)?; + + // Clone the repository if it doesn't exist, or update if it does + if project_root.exists() { + tracing::debug!("Updating repository for project '{}'...", self.name); + let start = std::time::Instant::now(); + update_repository(&project_root, self.commit)?; + tracing::debug!( + "Repository update completed in {:.2}s", + start.elapsed().as_secs_f64() + ); + } else { + tracing::debug!("Cloning repository for project '{}'...", self.name); + let start = std::time::Instant::now(); + clone_repository(self.repository, &project_root, self.commit)?; + tracing::debug!( + "Repository clone completed in {:.2}s", + start.elapsed().as_secs_f64() + ); + } + + let checkout = Checkout { + path: project_root, + project: self, + }; + + // Install dependencies if specified + tracing::debug!( + "Installing {} dependencies for project '{}'...", + checkout.project().dependencies.len(), + checkout.project().name + ); + let start_install = std::time::Instant::now(); + install_dependencies(&checkout)?; + tracing::debug!( + "Dependency installation completed in {:.2}s", + start_install.elapsed().as_secs_f64() + ); + + tracing::debug!("Project setup took: {:.2}s", start.elapsed().as_secs_f64()); + + Ok(InstalledProject { + path: checkout.path, + config: checkout.project, + }) + } +} + +struct Checkout<'a> { + project: RealWorldProject<'a>, + path: PathBuf, +} + +impl<'a> Checkout<'a> { + /// Get the virtual environment path + fn venv_path(&self) -> PathBuf { + self.path.join(".venv") + } + + fn project(&self) -> &RealWorldProject<'a> { + &self.project + } +} + +/// Checked out project with its dependencies installed. +pub struct InstalledProject<'a> { + /// Path to the cloned project + pub path: PathBuf, + /// Project configuration + pub config: RealWorldProject<'a>, +} + +impl<'a> InstalledProject<'a> { + /// Get the project configuration + pub fn config(&self) -> &RealWorldProject<'a> { + &self.config + } + + /// Get the benchmark paths as `SystemPathBuf` + pub fn check_paths(&self) -> &[&SystemPath] { + &self.config.paths + } + + /// Get the virtual environment path + pub fn venv_path(&self) -> PathBuf { + self.path.join(".venv") + } + + /// Copies the entire project to a memory file system. + pub fn copy_to_memory_fs(&self) -> anyhow::Result { + let fs = MemoryFileSystem::new(); + + copy_directory_recursive(&fs, &self.path, &SystemPathBuf::from("/"))?; + + Ok(fs) + } +} + +/// Get the cache directory for a project in the cargo target directory +fn get_project_cache_dir(project_name: &str) -> Result { + let target_dir = cargo_target_directory() + .cloned() + .unwrap_or_else(|| PathBuf::from("target")); + let target_dir = + std::path::absolute(target_dir).context("Failed to construct an absolute path")?; + let cache_dir = target_dir.join("benchmark_cache").join(project_name); + + if let Some(parent) = cache_dir.parent() { + std::fs::create_dir_all(parent).context("Failed to create cache directory")?; + } + + Ok(cache_dir) +} + +/// Update an existing repository +fn update_repository(project_root: &Path, commit: &str) -> Result<()> { + let output = Command::new("git") + .args(["fetch", "origin", commit]) + .current_dir(project_root) + .output() + .context("Failed to execute git fetch command")?; + + if !output.status.success() { + anyhow::bail!( + "Git fetch of commit {} failed: {}", + commit, + String::from_utf8_lossy(&output.stderr) + ); + } + + // Checkout specific commit + let output = Command::new("git") + .args(["checkout", commit]) + .current_dir(project_root) + .output() + .context("Failed to execute git checkout command")?; + + anyhow::ensure!( + output.status.success(), + "Git checkout of commit {} failed: {}", + commit, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) +} + +/// Clone a git repository to the specified directory +fn clone_repository(repo_url: &str, target_dir: &Path, commit: &str) -> Result<()> { + // Create parent directory if it doesn't exist + if let Some(parent) = target_dir.parent() { + std::fs::create_dir_all(parent).context("Failed to create parent directory for clone")?; + } + + // Clone with minimal depth and fetch only the specific commit + let output = Command::new("git") + .args([ + "clone", + "--filter=blob:none", // Don't download large files initially + "--no-checkout", // Don't checkout files yet + repo_url, + target_dir.to_str().unwrap(), + ]) + .output() + .context("Failed to execute git clone command")?; + + anyhow::ensure!( + output.status.success(), + "Git clone failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Fetch the specific commit + let output = Command::new("git") + .args(["fetch", "origin", commit]) + .current_dir(target_dir) + .output() + .context("Failed to execute git fetch command")?; + + anyhow::ensure!( + output.status.success(), + "Git fetch of commit {} failed: {}", + commit, + String::from_utf8_lossy(&output.stderr) + ); + + // Checkout the specific commit + let output = Command::new("git") + .args(["checkout", commit]) + .current_dir(target_dir) + .output() + .context("Failed to execute git checkout command")?; + + anyhow::ensure!( + output.status.success(), + "Git checkout of commit {} failed: {}", + commit, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) +} + +/// Install dependencies using uv with date constraints +fn install_dependencies(checkout: &Checkout) -> Result<()> { + // Check if uv is available + let uv_check = Command::new("uv") + .arg("--version") + .output() + .context("Failed to execute uv version check.")?; + + if !uv_check.status.success() { + anyhow::bail!( + "uv is not installed or not found in PATH. If you need to install it, follow the instructions at https://docs.astral.sh/uv/getting-started/installation/" + ); + } + + let venv_path = checkout.venv_path(); + let python_version_str = checkout.project().python_version.to_string(); + + let output = Command::new("uv") + .args(["venv", "--python", &python_version_str, "--allow-existing"]) + .arg(&venv_path) + .output() + .context("Failed to execute uv venv command")?; + + anyhow::ensure!( + output.status.success(), + "Failed to create virtual environment: {}", + String::from_utf8_lossy(&output.stderr) + ); + + if checkout.project().dependencies.is_empty() { + tracing::debug!( + "No dependencies to install for project '{}'", + checkout.project().name + ); + return Ok(()); + } + + // Install dependencies with date constraint in the isolated environment + let mut cmd = Command::new("uv"); + cmd.args([ + "pip", + "install", + "--python", + venv_path.to_str().unwrap(), + "--exclude-newer", + checkout.project().max_dep_date, + ]) + .args(&checkout.project().dependencies); + + let output = cmd + .output() + .context("Failed to execute uv pip install command")?; + + anyhow::ensure!( + output.status.success(), + "Dependency installation failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) +} + +/// Recursively load a directory into the memory filesystem +fn copy_directory_recursive( + fs: &MemoryFileSystem, + source_path: &Path, + dest_path: &SystemPath, +) -> Result<()> { + if source_path.is_file() { + if source_path.file_name().and_then(OsStr::to_str) == Some("pyvenv.cfg") { + // Skip pyvenv.cfg files because the Python path will be invalid. + return Ok(()); + } + + match std::fs::read_to_string(source_path) { + Ok(content) => { + fs.write_file_all(dest_path.to_path_buf(), content) + .with_context(|| { + format!("Failed to write file to memory filesystem: {dest_path}") + })?; + } + Err(error) => { + if error.kind() == std::io::ErrorKind::InvalidData { + // Skip binary files. + return Ok(()); + } + return Err(error) + .with_context(|| format!("Failed to read file: {}", source_path.display())); + } + } + } else if source_path.is_dir() { + // Create directory in memory fs + fs.create_directory_all(dest_path.to_path_buf()) + .with_context(|| { + format!("Failed to create directory in memory filesystem: {dest_path}") + })?; + + // Read directory contents + let entries = std::fs::read_dir(source_path) + .with_context(|| format!("Failed to read directory: {}", source_path.display()))?; + + for entry in entries { + let entry = entry.with_context(|| { + format!("Failed to read directory entry: {}", source_path.display()) + })?; + + let file_name = entry.file_name(); + let file_name = file_name.to_str().context("Expected UTF8 path")?; + let source_child = source_path.join(file_name); + let dest_child = dest_path.join(file_name); + + // Skip hidden files and common non-Python directories + if file_name != ".venv" && (file_name.starts_with('.') || matches!(file_name, ".git")) { + continue; + } + + copy_directory_recursive(fs, &source_child, &dest_child)?; + } + } + + Ok(()) +} + +static CARGO_TARGET_DIR: std::sync::OnceLock> = std::sync::OnceLock::new(); + +fn cargo_target_directory() -> Option<&'static PathBuf> { + CARGO_TARGET_DIR + .get_or_init(|| { + #[derive(serde::Deserialize)] + struct Metadata { + target_directory: PathBuf, + } + + std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .or_else(|| { + let output = Command::new(std::env::var_os("CARGO")?) + .args(["metadata", "--format-version", "1"]) + .output() + .ok()?; + let metadata: Metadata = serde_json::from_slice(&output.stdout).ok()?; + Some(metadata.target_directory) + }) + }) + .as_ref() +} diff --git a/crates/ruff_cache/src/cache_key.rs b/crates/ruff_cache/src/cache_key.rs index de66961dce44f..e371f4baba35b 100644 --- a/crates/ruff_cache/src/cache_key.rs +++ b/crates/ruff_cache/src/cache_key.rs @@ -2,8 +2,8 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::num::{ - NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroU128, NonZeroU16, - NonZeroU32, NonZeroU64, NonZeroU8, + NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroU8, NonZeroU16, NonZeroU32, + NonZeroU64, NonZeroU128, }; use std::path::{Path, PathBuf}; @@ -213,7 +213,7 @@ macro_rules! impl_cache_key_tuple { ( $($name:ident)+) => ( impl<$($name: CacheKey),+> CacheKey for ($($name,)+) where last_type!($($name,)+): ?Sized { - #[allow(non_snake_case)] + #[expect(non_snake_case)] #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { let ($(ref $name,)+) = *self; diff --git a/crates/ruff_cache/tests/cache_key.rs b/crates/ruff_cache/tests/cache_key.rs index f78697880661c..0027e32953624 100644 --- a/crates/ruff_cache/tests/cache_key.rs +++ b/crates/ruff_cache/tests/cache_key.rs @@ -47,7 +47,7 @@ fn struct_ignored_fields() { struct NamedFieldsStruct { a: String, #[cache_key(ignore)] - #[allow(unused)] + #[expect(unused)] b: String, } diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index b37af0f97866d..ac90e96214ded 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -13,30 +13,34 @@ license = { workspace = true } [dependencies] ruff_annotate_snippets = { workspace = true } ruff_cache = { workspace = true, optional = true } +ruff_diagnostics = { workspace = true } ruff_notebook = { workspace = true } -ruff_python_ast = { workspace = true } +ruff_python_ast = { workspace = true, features = ["get-size"] } ruff_python_parser = { workspace = true } ruff_python_trivia = { workspace = true } -ruff_source_file = { workspace = true } +ruff_source_file = { workspace = true, features = ["get-size"] } ruff_text_size = { workspace = true } +ty_static = { workspace = true } +anstyle = { workspace = true } +arc-swap = { workspace = true } camino = { workspace = true } countme = { workspace = true } dashmap = { workspace = true } dunce = { workspace = true } filetime = { workspace = true } +get-size2 = { workspace = true } glob = { workspace = true } ignore = { workspace = true, optional = true } matchit = { workspace = true } +path-slash = { workspace = true } +rustc-hash = { workspace = true } salsa = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } -path-slash = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, optional = true } -tracing-tree = { workspace = true, optional = true } -rustc-hash = { workspace = true } zip = { workspace = true } [target.'cfg(target_arch="wasm32")'.dependencies] @@ -54,4 +58,4 @@ cache = ["ruff_cache"] os = ["ignore", "dep:etcetera"] serde = ["dep:serde", "camino/serde1"] # Exposes testing utilities. -testing = ["tracing-subscriber", "tracing-tree"] +testing = ["tracing-subscriber"] diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 3bb2b24070287..9b449ee6f8298 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -1,16 +1,17 @@ use std::{fmt::Formatter, sync::Arc}; -use thiserror::Error; +use render::{FileResolver, Input}; +use ruff_diagnostics::Fix; +use ruff_source_file::{LineColumn, SourceCode, SourceFile}; use ruff_annotate_snippets::Level as AnnotateLevel; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange, TextSize}; pub use self::render::DisplayDiagnostic; -use crate::files::File; -use crate::Db; +use crate::{Db, files::File}; -use self::render::FileResolver; mod render; +mod stylesheet; /// A collection of information that can be rendered into a diagnostic. /// @@ -19,7 +20,7 @@ mod render; /// characteristics in the inputs given to the tool. Typically, but not always, /// a characteristic is a deficiency. An example of a characteristic that is /// _not_ a deficiency is the `reveal_type` diagnostic for our type checker. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] pub struct Diagnostic { /// The actual diagnostic. /// @@ -62,10 +63,37 @@ impl Diagnostic { message: message.into_diagnostic_message(), annotations: vec![], subs: vec![], + fix: None, + parent: None, + noqa_offset: None, + secondary_code: None, }); Diagnostic { inner } } + /// Creates a `Diagnostic` for a syntax error. + /// + /// Unlike the more general [`Diagnostic::new`], this requires a [`Span`] and a [`TextRange`] + /// attached to it. + /// + /// This should _probably_ be a method on the syntax errors, but + /// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of + /// the other way around. And since we want to do this conversion in a couple + /// places, it makes sense to centralize it _somewhere_. So it's here for now. + /// + /// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic + /// message. + pub fn invalid_syntax( + span: impl Into, + message: impl IntoDiagnosticMessage, + range: impl Ranged, + ) -> Diagnostic { + let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, ""); + let span = span.into().with_range(range.range()); + diag.annotate(Annotation::primary(span).message(message)); + diag + } + /// Add an annotation to this diagnostic. /// /// Annotations for a diagnostic are optional, but if any are added, @@ -114,10 +142,9 @@ impl Diagnostic { /// callers should prefer using this with `write!` instead of `writeln!`. pub fn display<'a>( &'a self, - db: &'a dyn Db, + resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig, ) -> DisplayDiagnostic<'a> { - let resolver = FileResolver::new(db); DisplayDiagnostic::new(resolver, config, self) } @@ -133,20 +160,20 @@ impl Diagnostic { /// NOTE: At present, this routine will return the first primary /// annotation's message as the primary message when the main diagnostic /// message is empty. This is meant to facilitate an incremental migration - /// in Red Knot over to the new diagnostic data model. (The old data model + /// in ty over to the new diagnostic data model. (The old data model /// didn't distinguish between messages on the entire diagnostic and /// messages attached to a particular span.) pub fn primary_message(&self) -> &str { if !self.inner.message.as_str().is_empty() { return self.inner.message.as_str(); } - // FIXME: As a special case, while we're migrating Red Knot + // FIXME: As a special case, while we're migrating ty // to the new diagnostic data model, we'll look for a primary // message from the primary annotation. This is because most - // Red Knot diagnostics are created with an empty diagnostic + // ty diagnostics are created with an empty diagnostic // message and instead attach the message to the annotation. // Fixing this will require touching basically every diagnostic - // in Red Knot, so we do it this way for now to match the old + // in ty, so we do it this way for now to match the old // semantics. ---AG self.primary_annotation() .and_then(|ann| ann.get_message()) @@ -164,7 +191,7 @@ impl Diagnostic { /// /// The reason why we don't just always return both the main diagnostic /// message and the primary annotation message is because this was written - /// in the midst of an incremental migration of Red Knot over to the new + /// in the midst of an incremental migration of ty over to the new /// diagnostic data model. At time of writing, diagnostics were still /// constructed in the old model where the main diagnostic message and the /// primary annotation message were not distinguished from each other. So @@ -226,17 +253,280 @@ impl Diagnostic { pub fn primary_span(&self) -> Option { self.primary_annotation().map(|ann| ann.span.clone()) } + + /// Returns a reference to the primary span of this diagnostic. + pub fn primary_span_ref(&self) -> Option<&Span> { + self.primary_annotation().map(|ann| &ann.span) + } + + /// Returns the tags from the primary annotation of this diagnostic if it exists. + pub fn primary_tags(&self) -> Option<&[DiagnosticTag]> { + self.primary_annotation().map(|ann| ann.tags.as_slice()) + } + + /// Returns the "primary" span of this diagnostic, panicking if it does not exist. + /// + /// This should typically only be used when working with diagnostics in Ruff, where diagnostics + /// are currently required to have a primary span. + /// + /// See [`Diagnostic::primary_span`] for more details. + pub fn expect_primary_span(&self) -> Span { + self.primary_span().expect("Expected a primary span") + } + + /// Returns a key that can be used to sort two diagnostics into the canonical order + /// in which they should appear when rendered. + pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a { + RenderingSortKey { + db, + diagnostic: self, + } + } + + /// Returns all annotations, skipping the first primary annotation. + pub fn secondary_annotations(&self) -> impl Iterator { + let mut seen_primary = false; + self.inner.annotations.iter().filter(move |ann| { + if seen_primary { + true + } else if ann.is_primary { + seen_primary = true; + false + } else { + true + } + }) + } + + pub fn sub_diagnostics(&self) -> &[SubDiagnostic] { + &self.inner.subs + } + + /// Returns the fix for this diagnostic if it exists. + pub fn fix(&self) -> Option<&Fix> { + self.inner.fix.as_ref() + } + + /// Set the fix for this diagnostic. + pub fn set_fix(&mut self, fix: Fix) { + Arc::make_mut(&mut self.inner).fix = Some(fix); + } + + /// Remove the fix for this diagnostic. + pub fn remove_fix(&mut self) { + Arc::make_mut(&mut self.inner).fix = None; + } + + /// Returns `true` if the diagnostic contains a [`Fix`]. + pub fn fixable(&self) -> bool { + self.fix().is_some() + } + + /// Returns the offset of the parent statement for this diagnostic if it exists. + /// + /// This is primarily used for checking noqa/secondary code suppressions. + pub fn parent(&self) -> Option { + self.inner.parent + } + + /// Set the offset of the diagnostic's parent statement. + pub fn set_parent(&mut self, parent: TextSize) { + Arc::make_mut(&mut self.inner).parent = Some(parent); + } + + /// Returns the remapped offset for a suppression comment if it exists. + /// + /// Like [`Diagnostic::parent`], this is used for noqa code suppression comments in Ruff. + pub fn noqa_offset(&self) -> Option { + self.inner.noqa_offset + } + + /// Set the remapped offset for a suppression comment. + pub fn set_noqa_offset(&mut self, noqa_offset: TextSize) { + Arc::make_mut(&mut self.inner).noqa_offset = Some(noqa_offset); + } + + /// Returns the secondary code for the diagnostic if it exists. + /// + /// The "primary" code for the diagnostic is its lint name. Diagnostics in ty don't have + /// secondary codes (yet), but in Ruff the noqa code is used. + pub fn secondary_code(&self) -> Option<&SecondaryCode> { + self.inner.secondary_code.as_ref() + } + + /// Set the secondary code for this diagnostic. + pub fn set_secondary_code(&mut self, code: SecondaryCode) { + Arc::make_mut(&mut self.inner).secondary_code = Some(code); + } + + /// Returns the name used to represent the diagnostic. + pub fn name(&self) -> &'static str { + self.id().as_str() + } + + /// Returns `true` if `self` is a syntax error message. + pub fn is_invalid_syntax(&self) -> bool { + self.id().is_invalid_syntax() + } + + /// Returns the message body to display to the user. + pub fn body(&self) -> &str { + self.primary_message() + } + + /// Returns the fix suggestion for the violation. + pub fn suggestion(&self) -> Option<&str> { + self.primary_annotation()?.get_message() + } + + /// Returns the URL for the rule documentation, if it exists. + pub fn to_url(&self) -> Option { + if self.is_invalid_syntax() { + None + } else { + Some(format!( + "{}/rules/{}", + env!("CARGO_PKG_HOMEPAGE"), + self.name() + )) + } + } + + /// Returns the filename for the message. + /// + /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`. + pub fn expect_ruff_filename(&self) -> String { + self.expect_primary_span() + .expect_ruff_file() + .name() + .to_string() + } + + /// Computes the start source location for the message. + /// + /// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the + /// span has no range. + pub fn expect_ruff_start_location(&self) -> LineColumn { + self.expect_primary_span() + .expect_ruff_file() + .to_source_code() + .line_column(self.expect_range().start()) + } + + /// Computes the end source location for the message. + /// + /// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the + /// span has no range. + pub fn expect_ruff_end_location(&self) -> LineColumn { + self.expect_primary_span() + .expect_ruff_file() + .to_source_code() + .line_column(self.expect_range().end()) + } + + /// Returns the [`SourceFile`] which the message belongs to. + pub fn ruff_source_file(&self) -> Option<&SourceFile> { + self.primary_span_ref()?.as_ruff_file() + } + + /// Returns the [`SourceFile`] which the message belongs to. + /// + /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`. + pub fn expect_ruff_source_file(&self) -> SourceFile { + self.expect_primary_span().expect_ruff_file().clone() + } + + /// Returns the [`TextRange`] for the diagnostic. + pub fn range(&self) -> Option { + self.primary_span()?.range() + } + + /// Returns the [`TextRange`] for the diagnostic. + /// + /// Panics if the diagnostic has no primary span or if the span has no range. + pub fn expect_range(&self) -> TextRange { + self.range().expect("Expected a range for the primary span") + } + + /// Returns the ordering of diagnostics based on the start of their ranges, if they have any. + /// + /// Panics if either diagnostic has no primary span, if the span has no range, or if its file is + /// not a `SourceFile`. + pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering { + (self.expect_ruff_source_file(), self.expect_range().start()).cmp(&( + other.expect_ruff_source_file(), + other.expect_range().start(), + )) + } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] struct DiagnosticInner { id: DiagnosticId, severity: Severity, message: DiagnosticMessage, annotations: Vec, subs: Vec, + fix: Option, + parent: Option, + noqa_offset: Option, + secondary_code: Option, +} + +struct RenderingSortKey<'a> { + db: &'a dyn Db, + diagnostic: &'a Diagnostic, } +impl Ord for RenderingSortKey<'_> { + // We sort diagnostics in a way that keeps them in source order + // and grouped by file. After that, we fall back to severity + // (with fatal messages sorting before info messages) and then + // finally the diagnostic ID. + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + if let (Some(span1), Some(span2)) = ( + self.diagnostic.primary_span(), + other.diagnostic.primary_span(), + ) { + let order = span1.file().path(&self.db).cmp(span2.file().path(&self.db)); + if order.is_ne() { + return order; + } + + if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) { + let order = range1.start().cmp(&range2.start()); + if order.is_ne() { + return order; + } + } + } + // Reverse so that, e.g., Fatal sorts before Info. + let order = self + .diagnostic + .severity() + .cmp(&other.diagnostic.severity()) + .reverse(); + if order.is_ne() { + return order; + } + self.diagnostic.id().cmp(&other.diagnostic.id()) + } +} + +impl PartialOrd for RenderingSortKey<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for RenderingSortKey<'_> { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for RenderingSortKey<'_> {} + /// A collection of information subservient to a diagnostic. /// /// A sub-diagnostic is always rendered after the parent diagnostic it is @@ -246,7 +536,7 @@ struct DiagnosticInner { /// Currently, the order in which sub-diagnostics are rendered relative to one /// another (for a single parent diagnostic) is the order in which they were /// attached to the diagnostic. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] pub struct SubDiagnostic { /// Like with `Diagnostic`, we box the `SubDiagnostic` to make it /// pointer-sized. @@ -294,9 +584,60 @@ impl SubDiagnostic { pub fn annotate(&mut self, ann: Annotation) { self.inner.annotations.push(ann); } + + pub fn annotations(&self) -> &[Annotation] { + &self.inner.annotations + } + + /// Returns a shared borrow of the "primary" annotation of this diagnostic + /// if one exists. + /// + /// When there are multiple primary annotations, then the first one that + /// was added to this diagnostic is returned. + pub fn primary_annotation(&self) -> Option<&Annotation> { + self.inner.annotations.iter().find(|ann| ann.is_primary) + } + + /// Introspects this diagnostic and returns what kind of "primary" message + /// it contains for concise formatting. + /// + /// When we concisely format diagnostics, we likely want to not only + /// include the primary diagnostic message but also the message attached + /// to the primary annotation. In particular, the primary annotation often + /// contains *essential* information or context for understanding the + /// diagnostic. + /// + /// The reason why we don't just always return both the main diagnostic + /// message and the primary annotation message is because this was written + /// in the midst of an incremental migration of ty over to the new + /// diagnostic data model. At time of writing, diagnostics were still + /// constructed in the old model where the main diagnostic message and the + /// primary annotation message were not distinguished from each other. So + /// for now, we carefully return what kind of messages this diagnostic + /// contains. In effect, if this diagnostic has a non-empty main message + /// *and* a non-empty primary annotation message, then the diagnostic is + /// 100% using the new diagnostic data model and we can format things + /// appropriately. + /// + /// The type returned implements the `std::fmt::Display` trait. In most + /// cases, just converting it to a string (or printing it) will do what + /// you want. + pub fn concise_message(&self) -> ConciseMessage { + let main = self.inner.message.as_str(); + let annotation = self + .primary_annotation() + .and_then(|ann| ann.get_message()) + .unwrap_or_default(); + match (main.is_empty(), annotation.is_empty()) { + (false, true) => ConciseMessage::MainDiagnostic(main), + (true, false) => ConciseMessage::PrimaryAnnotation(annotation), + (false, false) => ConciseMessage::Both { main, annotation }, + (true, true) => ConciseMessage::Empty, + } + } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] struct SubDiagnosticInner { severity: Severity, message: DiagnosticMessage, @@ -324,7 +665,7 @@ struct SubDiagnosticInner { /// /// Messages attached to annotations should also be as brief and specific as /// possible. Long messages could negative impact the quality of rendering. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] pub struct Annotation { /// The span of this annotation, corresponding to some subsequence of the /// user's input that we want to highlight. @@ -337,6 +678,8 @@ pub struct Annotation { /// Whether this annotation is "primary" or not. When it isn't primary, an /// annotation is said to be "secondary." is_primary: bool, + /// The diagnostic tags associated with this annotation. + tags: Vec, } impl Annotation { @@ -354,6 +697,7 @@ impl Annotation { span, message: None, is_primary: true, + tags: Vec::new(), } } @@ -369,6 +713,7 @@ impl Annotation { span, message: None, is_primary: false, + tags: Vec::new(), } } @@ -411,6 +756,41 @@ impl Annotation { pub fn get_span(&self) -> &Span { &self.span } + + /// Sets the span on this annotation. + pub fn set_span(&mut self, span: Span) { + self.span = span; + } + + /// Returns the tags associated with this annotation. + pub fn get_tags(&self) -> &[DiagnosticTag] { + &self.tags + } + + /// Attaches this tag to this annotation. + /// + /// It will not replace any existing tags. + pub fn tag(mut self, tag: DiagnosticTag) -> Annotation { + self.tags.push(tag); + self + } + + /// Attaches an additional tag to this annotation. + pub fn push_tag(&mut self, tag: DiagnosticTag) { + self.tags.push(tag); + } +} + +/// Tags that can be associated with an annotation. +/// +/// These tags are used to provide additional information about the annotation. +/// and are passed through to the language server protocol. +#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] +pub enum DiagnosticTag { + /// Unused or unnecessary code. Used for unused parameters, unreachable code, etc. + Unnecessary, + /// Deprecated or obsolete code. + Deprecated, } /// A string identifier for a lint rule. @@ -419,7 +799,7 @@ impl Annotation { /// be in kebab case, e.g. `no-foo` (all lower case). /// /// Rules use kebab case, e.g. `no-foo`. -#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct LintName(&'static str); impl LintName { @@ -459,8 +839,10 @@ impl PartialEq<&str> for LintName { } /// Uniquely identifies the kind of a diagnostic. -#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)] pub enum DiagnosticId { + Panic, + /// Some I/O operation failed Io, @@ -477,6 +859,79 @@ pub enum DiagnosticId { /// No rule with the given name exists. UnknownRule, + + /// A glob pattern doesn't follow the expected syntax. + InvalidGlob, + + /// An `include` glob without any patterns. + /// + /// ## Why is this bad? + /// An `include` glob without any patterns won't match any files. This is probably a mistake and + /// either the `include` should be removed or a pattern should be added. + /// + /// ## Example + /// ```toml + /// [src] + /// include = [] + /// ``` + /// + /// Use instead: + /// + /// ```toml + /// [src] + /// include = ["src"] + /// ``` + /// + /// or remove the `include` option. + EmptyInclude, + + /// An override configuration is unnecessary because it applies to all files. + /// + /// ## Why is this bad? + /// An overrides section that applies to all files is probably a mistake and can be rolled-up into the root configuration. + /// + /// ## Example + /// ```toml + /// [[overrides]] + /// [overrides.rules] + /// unused-reference = "ignore" + /// ``` + /// + /// Use instead: + /// + /// ```toml + /// [rules] + /// unused-reference = "ignore" + /// ``` + /// + /// or + /// + /// ```toml + /// [[overrides]] + /// include = ["test"] + /// + /// [overrides.rules] + /// unused-reference = "ignore" + /// ``` + UnnecessaryOverridesSection, + + /// An `overrides` section in the configuration that doesn't contain any overrides. + /// + /// ## Why is this bad? + /// An `overrides` section without any configuration overrides is probably a mistake. + /// It is either a leftover after removing overrides, or a user forgot to add any overrides, + /// or used an incorrect syntax to do so (e.g. used `rules` instead of `overrides.rules`). + /// + /// ## Example + /// ```toml + /// [[overrides]] + /// include = ["test"] + /// # no `[overrides.rules]` + /// ``` + UselessOverridesSection, + + /// Use of a deprecated setting. + DeprecatedSetting, } impl DiagnosticId { @@ -499,61 +954,89 @@ impl DiagnosticId { code.split_once(':').map(|(_, rest)| rest) } - /// Returns `true` if this `DiagnosticId` matches the given name. - /// - /// ## Examples - /// ``` - /// use ruff_db::diagnostic::DiagnosticId; + /// Returns a concise description of this diagnostic ID. /// - /// assert!(DiagnosticId::Io.matches("io")); - /// assert!(DiagnosticId::lint("test").matches("lint:test")); - /// assert!(!DiagnosticId::lint("test").matches("test")); - /// ``` - pub fn matches(&self, expected_name: &str) -> bool { - match self.as_str() { - Ok(id) => id == expected_name, - Err(DiagnosticAsStrError::Category { category, name }) => expected_name - .strip_prefix(category) - .and_then(|prefix| prefix.strip_prefix(":")) - .is_some_and(|rest| rest == name), - } - } - - pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> { - Ok(match self { + /// Note that this doesn't include the lint's category. It + /// only includes the lint's name. + pub fn as_str(&self) -> &'static str { + match self { + DiagnosticId::Panic => "panic", DiagnosticId::Io => "io", DiagnosticId::InvalidSyntax => "invalid-syntax", - DiagnosticId::Lint(name) => { - return Err(DiagnosticAsStrError::Category { - category: "lint", - name: name.as_str(), - }) - } + DiagnosticId::Lint(name) => name.as_str(), DiagnosticId::RevealedType => "revealed-type", DiagnosticId::UnknownRule => "unknown-rule", - }) + DiagnosticId::InvalidGlob => "invalid-glob", + DiagnosticId::EmptyInclude => "empty-include", + DiagnosticId::UnnecessaryOverridesSection => "unnecessary-overrides-section", + DiagnosticId::UselessOverridesSection => "useless-overrides-section", + DiagnosticId::DeprecatedSetting => "deprecated-setting", + } } -} -#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)] -pub enum DiagnosticAsStrError { - /// The id can't be converted to a string because it belongs to a sub-category. - #[error("id from a sub-category: {category}:{name}")] - Category { - /// The id's category. - category: &'static str, - /// The diagnostic id in this category. - name: &'static str, - }, + pub fn is_invalid_syntax(&self) -> bool { + matches!(self, Self::InvalidSyntax) + } } impl std::fmt::Display for DiagnosticId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self.as_str() { - Ok(name) => f.write_str(name), - Err(DiagnosticAsStrError::Category { category, name }) => { - write!(f, "{category}:{name}") - } + write!(f, "{}", self.as_str()) + } +} + +/// A unified file representation for both ruff and ty. +/// +/// Such a representation is needed for rendering [`Diagnostic`]s that can optionally contain +/// [`Annotation`]s with [`Span`]s that need to refer to the text of a file. However, ty and ruff +/// use very different file types: a `Copy`-able salsa-interned [`File`], and a heavier-weight +/// [`SourceFile`], respectively. +/// +/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and +/// emitting diagnostics from both ty and ruff. +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +pub enum UnifiedFile { + Ty(File), + Ruff(SourceFile), +} + +impl UnifiedFile { + pub fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str { + match self { + UnifiedFile::Ty(file) => resolver.path(*file), + UnifiedFile::Ruff(file) => file.name(), + } + } + + fn diagnostic_source(&self, resolver: &dyn FileResolver) -> DiagnosticSource { + match self { + UnifiedFile::Ty(file) => DiagnosticSource::Ty(resolver.input(*file)), + UnifiedFile::Ruff(file) => DiagnosticSource::Ruff(file.clone()), + } + } +} + +/// A unified wrapper for types that can be converted to a [`SourceCode`]. +/// +/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code. +/// [`DiagnosticSource`] wraps both of these and provides the single +/// [`DiagnosticSource::as_source_code`] method to produce a [`SourceCode`] with the appropriate +/// lifetimes. +/// +/// See [`UnifiedFile::diagnostic_source`] for a way to obtain a [`DiagnosticSource`] from a file +/// and [`FileResolver`]. +#[derive(Clone, Debug)] +enum DiagnosticSource { + Ty(Input), + Ruff(SourceFile), +} + +impl DiagnosticSource { + /// Returns this input as a `SourceCode` for convenient querying. + fn as_source_code(&self) -> SourceCode { + match self { + DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index), + DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()), } } } @@ -563,16 +1046,16 @@ impl std::fmt::Display for DiagnosticId { /// It consists of a `File` and an optional range into that file. When the /// range isn't present, it semantically implies that the diagnostic refers to /// the entire file. For example, when the file should be executable but isn't. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct Span { - file: File, + file: UnifiedFile, range: Option, } impl Span { - /// Returns the `File` attached to this `Span`. - pub fn file(&self) -> File { - self.file + /// Returns the `UnifiedFile` attached to this `Span`. + pub fn file(&self) -> &UnifiedFile { + &self.file } /// Returns the range, if available, attached to this `Span`. @@ -593,15 +1076,55 @@ impl Span { pub fn with_optional_range(self, range: Option) -> Span { Span { range, ..self } } + + /// Returns the [`File`] attached to this [`Span`]. + /// + /// Panics if the file is a [`UnifiedFile::Ruff`] instead of a [`UnifiedFile::Ty`]. + pub fn expect_ty_file(&self) -> File { + match self.file { + UnifiedFile::Ty(file) => file, + UnifiedFile::Ruff(_) => panic!("Expected a ty `File`, found a ruff `SourceFile`"), + } + } + + /// Returns the [`SourceFile`] attached to this [`Span`]. + /// + /// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`]. + pub fn expect_ruff_file(&self) -> &SourceFile { + self.as_ruff_file() + .expect("Expected a ruff `SourceFile`, found a ty `File`") + } + + /// Returns the [`SourceFile`] attached to this [`Span`]. + pub fn as_ruff_file(&self) -> Option<&SourceFile> { + match &self.file { + UnifiedFile::Ty(_) => None, + UnifiedFile::Ruff(file) => Some(file), + } + } } impl From for Span { fn from(file: File) -> Span { + let file = UnifiedFile::Ty(file); + Span { file, range: None } + } +} + +impl From for Span { + fn from(file: SourceFile) -> Self { + let file = UnifiedFile::Ruff(file); Span { file, range: None } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +impl From for Span { + fn from(file_range: crate::files::FileRange) -> Span { + Span::from(file_range.file()).with_range(file_range.range()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)] pub enum Severity { Info, Warning, @@ -625,6 +1148,10 @@ impl Severity { Severity::Fatal => AnnotateLevel::Error, } } + + pub const fn is_fatal(self) -> bool { + matches!(self, Severity::Fatal) + } } /// Configuration for rendering diagnostics. @@ -760,7 +1287,7 @@ impl std::fmt::Display for ConciseMessage<'_> { /// In most cases, callers shouldn't need to use this. Instead, there is /// a blanket trait implementation for `IntoDiagnosticMessage` for /// anything that implements `std::fmt::Display`. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] pub struct DiagnosticMessage(Box); impl DiagnosticMessage { @@ -820,15 +1347,52 @@ impl IntoDiagnosticMessage for T { } } -/// Creates a `Diagnostic` from a parse error. +/// A secondary identifier for a lint diagnostic. /// -/// This should _probably_ be a method on `ruff_python_parser::ParseError`, but -/// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of -/// the other way around. And since we want to do this conversion in a couple -/// places, it makes sense to centralize it _somewhere_. So it's here for now. -pub fn create_parse_diagnostic(file: File, err: &ruff_python_parser::ParseError) -> Diagnostic { - let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, ""); - let span = Span::from(file).with_range(err.location); - diag.annotate(Annotation::primary(span).message(&err.error)); - diag +/// For Ruff rules this means the noqa code. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)] +#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +pub struct SecondaryCode(String); + +impl SecondaryCode { + pub fn new(code: String) -> Self { + Self(code) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for SecondaryCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::ops::Deref for SecondaryCode { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq<&str> for SecondaryCode { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq for &str { + fn eq(&self, other: &SecondaryCode) -> bool { + other.eq(self) + } +} + +// for `hashbrown::EntryRef` +impl From<&SecondaryCode> for SecondaryCode { + fn from(value: &SecondaryCode) -> Self { + value.clone() + } } diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 111af97b6d214..59e762c568288 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -7,14 +7,17 @@ use ruff_annotate_snippets::{ use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_text_size::{TextRange, TextSize}; +use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled}; use crate::{ - files::File, - source::{line_index, source_text, SourceText}, Db, + files::File, + source::{SourceText, line_index, source_text}, + system::SystemPath, }; use super::{ - Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, Severity, SubDiagnostic, + Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity, + SubDiagnostic, }; /// A type that implements `std::fmt::Display` for diagnostic rendering. @@ -28,17 +31,16 @@ use super::{ /// values. When using Salsa, this most commonly corresponds to the lifetime /// of a Salsa `Db`. /// * The lifetime of the diagnostic being rendered. -#[derive(Debug)] pub struct DisplayDiagnostic<'a> { config: &'a DisplayDiagnosticConfig, - resolver: FileResolver<'a>, + resolver: &'a dyn FileResolver, annotate_renderer: AnnotateRenderer, diag: &'a Diagnostic, } impl<'a> DisplayDiagnostic<'a> { pub(crate) fn new( - resolver: FileResolver<'a>, + resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig, diag: &'a Diagnostic, ) -> DisplayDiagnostic<'a> { @@ -47,6 +49,7 @@ impl<'a> DisplayDiagnostic<'a> { } else { AnnotateRenderer::plain() }; + DisplayDiagnostic { config, resolver, @@ -58,31 +61,66 @@ impl<'a> DisplayDiagnostic<'a> { impl std::fmt::Display for DisplayDiagnostic<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let stylesheet = if self.config.color { + DiagnosticStylesheet::styled() + } else { + DiagnosticStylesheet::plain() + }; + if matches!(self.config.format, DiagnosticFormat::Concise) { - match self.diag.severity() { - Severity::Info => f.write_str("info")?, - Severity::Warning => f.write_str("warning")?, - Severity::Error => f.write_str("error")?, - Severity::Fatal => f.write_str("fatal")?, - } + let (severity, severity_style) = match self.diag.severity() { + Severity::Info => ("info", stylesheet.info), + Severity::Warning => ("warning", stylesheet.warning), + Severity::Error => ("error", stylesheet.error), + Severity::Fatal => ("fatal", stylesheet.error), + }; + + write!( + f, + "{severity}[{id}]", + severity = fmt_styled(severity, severity_style), + id = fmt_styled(self.diag.id(), stylesheet.emphasis) + )?; - write!(f, "[{rule}]", rule = self.diag.id())?; if let Some(span) = self.diag.primary_span() { - write!(f, " {path}", path = self.resolver.path(span.file()))?; + write!( + f, + " {path}", + path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis) + )?; if let Some(range) = span.range() { - let input = self.resolver.input(span.file()); - let start = input.as_source_code().source_location(range.start()); - write!(f, ":{line}:{col}", line = start.row, col = start.column)?; + let diagnostic_source = span.file().diagnostic_source(self.resolver); + let start = diagnostic_source + .as_source_code() + .line_column(range.start()); + + write!( + f, + ":{line}:{col}", + line = fmt_styled(start.line, stylesheet.emphasis), + col = fmt_styled(start.column, stylesheet.emphasis), + )?; } write!(f, ":")?; } - return writeln!(f, " {}", self.diag.concise_message()); + return writeln!(f, " {message}", message = self.diag.concise_message()); } - let resolved = Resolved::new(&self.resolver, self.diag); + let mut renderer = self.annotate_renderer.clone(); + renderer = renderer + .error(stylesheet.error) + .warning(stylesheet.warning) + .info(stylesheet.info) + .note(stylesheet.note) + .help(stylesheet.help) + .line_no(stylesheet.line_no) + .emphasis(stylesheet.emphasis) + .none(stylesheet.none); + + let resolved = Resolved::new(self.resolver, self.diag); let renderable = resolved.to_renderable(self.config.context); for diag in renderable.diagnostics.iter() { - writeln!(f, "{}", self.annotate_renderer.render(diag.to_annotate()))?; + writeln!(f, "{}", renderer.render(diag.to_annotate()))?; } writeln!(f) } @@ -102,26 +140,23 @@ impl std::fmt::Display for DisplayDiagnostic<'_> { /// both.) #[derive(Debug)] struct Resolved<'a> { - id: String, diagnostics: Vec>, } impl<'a> Resolved<'a> { /// Creates a new resolved set of diagnostics. - fn new(resolver: &FileResolver<'a>, diag: &'a Diagnostic) -> Resolved<'a> { + fn new(resolver: &'a dyn FileResolver, diag: &'a Diagnostic) -> Resolved<'a> { let mut diagnostics = vec![]; diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, diag)); for sub in &diag.inner.subs { diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub)); } - let id = diag.inner.id.to_string(); - Resolved { id, diagnostics } + Resolved { diagnostics } } /// Creates a value that is amenable to rendering directly. fn to_renderable(&self, context: usize) -> Renderable<'_> { Renderable { - id: &self.id, diagnostics: self .diagnostics .iter() @@ -139,6 +174,7 @@ impl<'a> Resolved<'a> { #[derive(Debug)] struct ResolvedDiagnostic<'a> { severity: Severity, + id: Option, message: String, annotations: Vec>, } @@ -146,7 +182,7 @@ struct ResolvedDiagnostic<'a> { impl<'a> ResolvedDiagnostic<'a> { /// Resolve a single diagnostic. fn from_diagnostic( - resolver: &FileResolver<'a>, + resolver: &'a dyn FileResolver, diag: &'a Diagnostic, ) -> ResolvedDiagnostic<'a> { let annotations: Vec<_> = diag @@ -154,25 +190,16 @@ impl<'a> ResolvedDiagnostic<'a> { .annotations .iter() .filter_map(|ann| { - let path = resolver.path(ann.span.file); - let input = resolver.input(ann.span.file); - ResolvedAnnotation::new(path, &input, ann) + let path = ann.span.file.path(resolver); + let diagnostic_source = ann.span.file.diagnostic_source(resolver); + ResolvedAnnotation::new(path, &diagnostic_source, ann) }) .collect(); - let message = if diag.inner.message.as_str().is_empty() { - diag.inner.id.to_string() - } else { - // TODO: See the comment on `Renderable::id` for - // a plausible better idea than smushing the ID - // into the diagnostic message. - format!( - "{id}: {message}", - id = diag.inner.id, - message = diag.inner.message.as_str(), - ) - }; + let id = Some(diag.inner.id.to_string()); + let message = diag.inner.message.as_str().to_string(); ResolvedDiagnostic { severity: diag.inner.severity, + id, message, annotations, } @@ -180,7 +207,7 @@ impl<'a> ResolvedDiagnostic<'a> { /// Resolve a single sub-diagnostic. fn from_sub_diagnostic( - resolver: &FileResolver<'a>, + resolver: &'a dyn FileResolver, diag: &'a SubDiagnostic, ) -> ResolvedDiagnostic<'a> { let annotations: Vec<_> = diag @@ -188,13 +215,14 @@ impl<'a> ResolvedDiagnostic<'a> { .annotations .iter() .filter_map(|ann| { - let path = resolver.path(ann.span.file); - let input = resolver.input(ann.span.file); - ResolvedAnnotation::new(path, &input, ann) + let path = ann.span.file.path(resolver); + let diagnostic_source = ann.span.file.diagnostic_source(resolver); + ResolvedAnnotation::new(path, &diagnostic_source, ann) }) .collect(); ResolvedDiagnostic { severity: diag.inner.severity, + id: None, message: diag.inner.message.as_str().to_string(), annotations, } @@ -223,13 +251,21 @@ impl<'a> ResolvedDiagnostic<'a> { continue; }; - let prev_context_ends = - context_after(&prev.input.as_source_code(), context, prev.line_end).get(); - let this_context_begins = - context_before(&ann.input.as_source_code(), context, ann.line_start).get(); + let prev_context_ends = context_after( + &prev.diagnostic_source.as_source_code(), + context, + prev.line_end, + ) + .get(); + let this_context_begins = context_before( + &ann.diagnostic_source.as_source_code(), + context, + ann.line_start, + ) + .get(); // The boundary case here is when `prev_context_ends` // is exactly one less than `this_context_begins`. In - // that case, the context windows are adajcent and we + // that case, the context windows are adjacent and we // should fall through below to add this annotation to // the existing snippet. if this_context_begins.saturating_sub(prev_context_ends) > 1 { @@ -253,6 +289,7 @@ impl<'a> ResolvedDiagnostic<'a> { .sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse()); RenderableDiagnostic { severity: self.severity, + id: self.id.as_deref(), message: &self.message, snippets_by_input, } @@ -268,7 +305,7 @@ impl<'a> ResolvedDiagnostic<'a> { #[derive(Debug)] struct ResolvedAnnotation<'a> { path: &'a str, - input: Input, + diagnostic_source: DiagnosticSource, range: TextRange, line_start: OneIndexed, line_end: OneIndexed, @@ -282,12 +319,15 @@ impl<'a> ResolvedAnnotation<'a> { /// `path` is the path of the file that this annotation points to. /// /// `input` is the contents of the file that this annotation points to. - fn new(path: &'a str, input: &Input, ann: &'a Annotation) -> Option> { - let source = input.as_source_code(); + fn new( + path: &'a str, + diagnostic_source: &DiagnosticSource, + ann: &'a Annotation, + ) -> Option> { + let source = diagnostic_source.as_source_code(); let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) { // An annotation with no range AND no message is probably(?) - // meaningless, so just ignore it. - (None, false) => return None, + // meaningless, but we should try to render it anyway. (None, _) => ( TextRange::empty(TextSize::new(0)), OneIndexed::MIN, @@ -310,7 +350,7 @@ impl<'a> ResolvedAnnotation<'a> { }; Some(ResolvedAnnotation { path, - input: input.clone(), + diagnostic_source: diagnostic_source.clone(), range, line_start, line_end, @@ -329,20 +369,6 @@ impl<'a> ResolvedAnnotation<'a> { /// renderable value. This is usually the lifetime of `Resolved`. #[derive(Debug)] struct Renderable<'r> { - // TODO: This is currently unused in the rendering logic below. I'm not - // 100% sure yet where I want to put it, but I like what `rustc` does: - // - // error[E0599]: no method named `sub_builder` <..snip..> - // - // I believe in order to do this, we'll need to patch it in to - // `ruff_annotate_snippets` though. We leave it here for now with that plan - // in mind. - // - // (At time of writing, 2025-03-13, we currently render the diagnostic - // ID into the main message of the parent diagnostic. We don't use this - // specific field to do that though.) - #[allow(dead_code)] - id: &'r str, diagnostics: Vec>, } @@ -351,6 +377,12 @@ struct Renderable<'r> { struct RenderableDiagnostic<'r> { /// The severity of the diagnostic. severity: Severity, + /// The ID of the diagnostic. The ID can usually be used on the CLI or in a + /// config file to change the severity of a lint. + /// + /// An ID is always present for top-level diagnostics and always absent for + /// sub-diagnostics. + id: Option<&'r str>, /// The message emitted with the diagnostic, before any snippets are /// rendered. message: &'r str, @@ -371,7 +403,11 @@ impl RenderableDiagnostic<'_> { .iter() .map(|snippet| snippet.to_annotate(path)) }); - level.title(self.message).snippets(snippets) + let mut message = level.title(self.message); + if let Some(id) = self.id { + message = message.id(id); + } + message.snippets(snippets) } } @@ -475,8 +511,8 @@ impl<'r> RenderableSnippet<'r> { !anns.is_empty(), "creating a renderable snippet requires a non-zero number of annotations", ); - let input = &anns[0].input; - let source = input.as_source_code(); + let diagnostic_source = &anns[0].diagnostic_source; + let source = diagnostic_source.as_source_code(); let has_primary = anns.iter().any(|ann| ann.is_primary); let line_start = context_before( @@ -492,7 +528,7 @@ impl<'r> RenderableSnippet<'r> { let snippet_start = source.line_start(line_start); let snippet_end = source.line_end(line_end); - let snippet = input + let snippet = diagnostic_source .as_source_code() .slice(TextRange::new(snippet_start, snippet_end)); @@ -578,7 +614,7 @@ impl<'r> RenderableAnnotation<'r> { } } -/// A type that facilitates the retrieval of source code from a `Span`. +/// A trait that facilitates the retrieval of source code from a `Span`. /// /// At present, this is tightly coupled with a Salsa database. In the future, /// it is intended for this resolver to become an abstraction providing a @@ -589,37 +625,44 @@ impl<'r> RenderableAnnotation<'r> { /// For example, at time of writing (2025-03-07), the plan is (roughly) for /// Ruff to grow its own interner of file paths so that a `Span` can store an /// interned ID instead of a (roughly) `Arc`. This interner is planned -/// to be entirely separate from the Salsa interner used by Red Knot, and so, +/// to be entirely separate from the Salsa interner used by ty, and so, /// callers will need to pass in a different "resolver" for turning `Span`s /// into actual file paths/contents. The infrastructure for this isn't fully in /// place, but this type serves to demarcate the intended abstraction boundary. -pub(crate) struct FileResolver<'a> { - db: &'a dyn Db, -} +pub trait FileResolver { + /// Returns the path associated with the file given. + fn path(&self, file: File) -> &str; -impl<'a> FileResolver<'a> { - /// Creates a new resolver from a Salsa database. - pub(crate) fn new(db: &'a dyn Db) -> FileResolver<'a> { - FileResolver { db } - } + /// Returns the input contents associated with the file given. + fn input(&self, file: File) -> Input; +} - /// Returns the path associated with the file given. - fn path(&self, file: File) -> &'a str { - file.path(self.db).as_str() +impl FileResolver for T +where + T: Db, +{ + fn path(&self, file: File) -> &str { + relativize_path(self.system().current_directory(), file.path(self).as_str()) } - /// Returns the input contents associated with the file given. fn input(&self, file: File) -> Input { Input { - text: source_text(self.db, file), - line_index: line_index(self.db, file), + text: source_text(self, file), + line_index: line_index(self, file), } } } -impl std::fmt::Debug for FileResolver<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "") +impl FileResolver for &dyn Db { + fn path(&self, file: File) -> &str { + relativize_path(self.system().current_directory(), file.path(*self).as_str()) + } + + fn input(&self, file: File) -> Input { + Input { + text: source_text(*self, file), + line_index: line_index(*self, file), + } } } @@ -629,16 +672,9 @@ impl std::fmt::Debug for FileResolver<'_> { /// This contains the actual content of that input as well as a /// line index for efficiently querying its contents. #[derive(Clone, Debug)] -struct Input { - text: SourceText, - line_index: LineIndex, -} - -impl Input { - /// Returns this input as a `SourceCode` for convenient querying. - fn as_source_code(&self) -> SourceCode<'_, '_> { - SourceCode::new(self.text.as_str(), &self.line_index) - } +pub struct Input { + pub(crate) text: SourceText, + pub(crate) line_index: LineIndex, } /// Returns the line number accounting for the given `len` @@ -677,6 +713,14 @@ fn context_after(source: &SourceCode<'_, '_>, len: usize, start: OneIndexed) -> line } +/// Convert an absolute path to be relative to the current working directory. +fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str { + if let Ok(path) = SystemPath::new(path).strip_prefix(cwd) { + return path.as_str(); + } + path +} + #[cfg(test)] mod tests { @@ -725,7 +769,7 @@ kangaroo static FRUITS: &str = "\ apple banana -cantelope +cantaloupe lime orange pear @@ -757,8 +801,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -781,8 +825,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - warning: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + warning[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -801,8 +845,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - info: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + info[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -815,6 +859,50 @@ watermelon ); } + #[test] + fn no_range() { + let mut env = TestEnvironment::new(); + env.add("animals", ANIMALS); + + let mut builder = env.err(); + builder + .diag + .annotate(Annotation::primary(builder.env.path("animals"))); + let diag = builder.build(); + insta::assert_snapshot!( + env.render(&diag), + @r" + error[test-diagnostic]: main diagnostic message + --> animals:1:1 + | + 1 | aardvark + | ^ + 2 | beetle + 3 | canary + | + ", + ); + + let mut builder = env.err(); + builder.diag.annotate( + Annotation::primary(builder.env.path("animals")).message("primary annotation message"), + ); + let diag = builder.build(); + insta::assert_snapshot!( + env.render(&diag), + @r" + error[test-diagnostic]: main diagnostic message + --> animals:1:1 + | + 1 | aardvark + | ^ primary annotation message + 2 | beetle + 3 | canary + | + ", + ); + } + #[test] fn non_ascii() { let mut env = TestEnvironment::new(); @@ -824,8 +912,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /non-ascii:5:1 + error[test-diagnostic]: main diagnostic message + --> non-ascii:5:1 | 3 | ΔΔΔΔΔΔΔΔΔΔΔΔ 4 | ββββββββββββ @@ -843,8 +931,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /non-ascii:2:2 + error[test-diagnostic]: main diagnostic message + --> non-ascii:2:2 | 1 | ☃☃☃☃☃☃☃☃☃☃☃☃ 2 | 💩💩💩💩💩💩💩💩💩💩💩💩 @@ -867,8 +955,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 4 | dog 5 | elephant @@ -884,8 +972,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 5 | elephant | ^^^^^^^^ @@ -899,8 +987,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -916,8 +1004,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:11:1 + error[test-diagnostic]: main diagnostic message + --> animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -933,8 +1021,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 1 | aardvark 2 | beetle @@ -966,15 +1054,15 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ 2 | beetle 3 | canary | - ::: /animals:11:1 + ::: animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -1010,8 +1098,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1035,8 +1123,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1063,14 +1151,14 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ 2 | beetle | - ::: /animals:5:1 + ::: animals:5:1 | 4 | dog 5 | elephant @@ -1091,8 +1179,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1116,8 +1204,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1147,8 +1235,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + error[test-diagnostic]: main diagnostic message + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1156,7 +1244,7 @@ watermelon 3 | canary 4 | dog | - ::: /animals:9:1 + ::: animals:9:1 | 6 | finch 7 | gorilla @@ -1185,8 +1273,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:8:1 + error[test-diagnostic]: main diagnostic message + --> spacey-animals:8:1 | 7 | dog 8 | elephant @@ -1202,8 +1290,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:12:1 + error[test-diagnostic]: main diagnostic message + --> spacey-animals:12:1 | 11 | gorilla 12 | hippopotamus @@ -1220,8 +1308,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:13:1 + error[test-diagnostic]: main diagnostic message + --> spacey-animals:13:1 | 11 | gorilla 12 | hippopotamus @@ -1260,13 +1348,13 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:3:1 + error[test-diagnostic]: main diagnostic message + --> spacey-animals:3:1 | 3 | beetle | ^^^^^^ | - ::: /spacey-animals:5:1 + ::: spacey-animals:5:1 | 5 | canary | ^^^^^^ @@ -1289,8 +1377,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1299,12 +1387,12 @@ watermelon 4 | dog 5 | elephant | - ::: /fruits:3:1 + ::: fruits:3:1 | 1 | apple 2 | banana - 3 | cantelope - | ^^^^^^^^^ + 3 | cantaloupe + | ^^^^^^^^^^ 4 | lime 5 | orange | @@ -1326,8 +1414,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1363,8 +1451,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1391,8 +1479,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1402,12 +1490,12 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /fruits:3:1 + --> fruits:3:1 | 1 | apple 2 | banana - 3 | cantelope - | ^^^^^^^^^ + 3 | cantaloupe + | ^^^^^^^^^^ 4 | lime 5 | orange | @@ -1427,8 +1515,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1438,17 +1526,17 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /fruits:3:1 + --> fruits:3:1 | 1 | apple 2 | banana - 3 | cantelope - | ^^^^^^^^^ + 3 | cantaloupe + | ^^^^^^^^^^ 4 | lime 5 | orange | warning: sub-diagnostic message - --> /animals:11:1 + --> animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -1466,8 +1554,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1477,7 +1565,7 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /animals:11:1 + --> animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -1485,12 +1573,12 @@ watermelon | ^^^^^^^^ | warning: sub-diagnostic message - --> /fruits:3:1 + --> fruits:3:1 | 1 | apple 2 | banana - 3 | cantelope - | ^^^^^^^^^ + 3 | cantaloupe + | ^^^^^^^^^^ 4 | lime 5 | orange | @@ -1514,8 +1602,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + error[test-diagnostic]: main diagnostic message + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1525,7 +1613,7 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1550,8 +1638,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -1573,8 +1661,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -1593,8 +1681,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -1613,8 +1701,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:4 + error[test-diagnostic]: main diagnostic message + --> animals:5:4 | 3 | canary 4 | dog @@ -1635,8 +1723,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:4 + error[test-diagnostic]: main diagnostic message + --> animals:5:4 | 3 | canary 4 | dog @@ -1667,8 +1755,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:4:1 + error[test-diagnostic]: main diagnostic message + --> animals:4:1 | 2 | beetle 3 | canary @@ -1696,8 +1784,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:4:1 + error[test-diagnostic]: main diagnostic message + --> animals:4:1 | 2 | beetle 3 | canary @@ -1727,8 +1815,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -1762,8 +1850,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -1790,8 +1878,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 3 | canary 4 | dog @@ -1822,8 +1910,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:3 + error[test-diagnostic]: main diagnostic message + --> animals:5:3 | 3 | canary 4 | dog @@ -1844,8 +1932,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:3 + error[test-diagnostic]: main diagnostic message + --> animals:5:3 | 3 | canary 4 | dog @@ -1877,8 +1965,8 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:8:1 + error[test-diagnostic]: main diagnostic message + --> animals:8:1 | 6 | finch 7 | gorilla @@ -1887,7 +1975,7 @@ watermelon 9 | inchworm 10 | jackrabbit | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary @@ -1917,28 +2005,28 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + error[test-diagnostic]: main diagnostic message + --> animals:5:1 | 5 | elephant | ^^^^^^^^ primary 5 | - ::: /animals:9:1 + ::: animals:9:1 | 9 | inchworm | ^^^^^^^^ primary 9 | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary 1 | - ::: /animals:3:1 + ::: animals:3:1 | 3 | canary | ------ secondary 3 | - ::: /animals:7:1 + ::: animals:7:1 | 7 | gorilla | ------- secondary 7 @@ -1961,15 +2049,15 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /fruits:1:1 + error[test-diagnostic]: main diagnostic message + --> fruits:1:1 | 1 | apple | ^^^^^ primary 2 | banana - 3 | cantelope + 3 | cantaloupe | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary @@ -1996,33 +2084,33 @@ watermelon insta::assert_snapshot!( env.render(&diag), @r" - error: lint:test-diagnostic: main diagnostic message - --> /animals:11:1 + error[test-diagnostic]: main diagnostic message + --> animals:11:1 | 11 | kangaroo | ^^^^^^^^ primary animals 11 | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary animals 1 | - ::: /animals:3:1 + ::: animals:3:1 | 3 | canary | ------ secondary animals 3 | - ::: /animals:7:1 + ::: animals:7:1 | 7 | gorilla | ------- secondary animals 7 | - ::: /fruits:10:1 + ::: fruits:10:1 | 10 | watermelon | ^^^^^^^^^^ primary fruits 10 | - ::: /fruits:2:1 + ::: fruits:2:1 | 2 | banana | ------ secondary fruits 2 @@ -2082,8 +2170,9 @@ watermelon /// otherwise, the span will end where the next line begins, and this /// confuses `ruff_annotate_snippets` as of 2025-03-13.) fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span { - let file = system_path_to_file(&self.db, path).unwrap(); + let span = self.path(path); + let file = span.expect_ty_file(); let text = source_text(&self.db, file); let line_index = line_index(&self.db, file); let source = SourceCode::new(text.as_str(), &line_index); @@ -2099,7 +2188,13 @@ watermelon None => source.line_end(line_end) - TextSize::from(1), Some(offset) => source.line_start(line_end) + offset, }; - Span::from(file).with_range(TextRange::new(start, end)) + span.with_range(TextRange::new(start, end)) + } + + /// Like `span`, but only attaches a file path. + fn path(&self, path: &str) -> Span { + let file = system_path_to_file(&self.db, path).unwrap(); + Span::from(file) } /// A convenience function for returning a builder for a diagnostic diff --git a/crates/ruff_db/src/diagnostic/stylesheet.rs b/crates/ruff_db/src/diagnostic/stylesheet.rs new file mode 100644 index 0000000000000..f3d451030bcb8 --- /dev/null +++ b/crates/ruff_db/src/diagnostic/stylesheet.rs @@ -0,0 +1,80 @@ +use anstyle::{AnsiColor, Effects, Style}; +use std::fmt::Formatter; + +pub(super) const fn fmt_styled<'a, T>( + content: T, + style: anstyle::Style, +) -> impl std::fmt::Display + 'a +where + T: std::fmt::Display + 'a, +{ + struct FmtStyled { + content: T, + style: anstyle::Style, + } + + impl std::fmt::Display for FmtStyled + where + T: std::fmt::Display, + { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{style_start}{content}{style_end}", + style_start = self.style.render(), + content = self.content, + style_end = self.style.render_reset() + ) + } + } + + FmtStyled { content, style } +} + +#[derive(Clone, Debug)] +pub struct DiagnosticStylesheet { + pub(crate) error: Style, + pub(crate) warning: Style, + pub(crate) info: Style, + pub(crate) note: Style, + pub(crate) help: Style, + pub(crate) line_no: Style, + pub(crate) emphasis: Style, + pub(crate) none: Style, +} + +impl Default for DiagnosticStylesheet { + fn default() -> Self { + Self::plain() + } +} + +impl DiagnosticStylesheet { + /// Default terminal styling + pub fn styled() -> Self { + let bright_blue = AnsiColor::BrightBlue.on_default(); + Self { + error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD), + warning: AnsiColor::Yellow.on_default().effects(Effects::BOLD), + info: bright_blue.effects(Effects::BOLD), + note: AnsiColor::BrightGreen.on_default().effects(Effects::BOLD), + help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD), + line_no: bright_blue.effects(Effects::BOLD), + emphasis: Style::new().effects(Effects::BOLD), + none: Style::new(), + } + } + + pub fn plain() -> Self { + Self { + error: Style::new(), + warning: Style::new(), + info: Style::new(), + note: Style::new(), + help: Style::new(), + line_no: Style::new(), + emphasis: Style::new(), + none: Style::new(), + } + } +} diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 74a058f57ddfb..a5cb501f9f78b 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -11,12 +11,13 @@ use ruff_text_size::{Ranged, TextRange}; use salsa::plumbing::AsId; use salsa::{Durability, Setter}; +use crate::diagnostic::{Span, UnifiedFile}; use crate::file_revision::FileRevision; use crate::files::file_root::FileRoots; use crate::files::private::FileStatus; use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf}; use crate::vendored::{VendoredPath, VendoredPathBuf}; -use crate::{vendored, Db, FxDashMap}; +use crate::{Db, FxDashMap, vendored}; mod file_root; mod path; @@ -94,7 +95,9 @@ impl Files { .root(db, path) .map_or(Durability::default(), |root| root.durability(db)); - let builder = File::builder(FilePath::System(absolute)).durability(durability); + let builder = File::builder(FilePath::System(absolute)) + .durability(durability) + .path_durability(Durability::HIGH); let builder = match metadata { Ok(metadata) if metadata.file_type().is_file() => builder @@ -159,9 +162,11 @@ impl Files { tracing::trace!("Adding virtual file {}", path); let virtual_file = VirtualFile( File::builder(FilePath::SystemVirtual(path.to_path_buf())) + .path_durability(Durability::HIGH) .status(FileStatus::Exists) .revision(FileRevision::zero()) .permissions(None) + .permissions_durability(Durability::HIGH) .new(db), ); self.inner @@ -258,22 +263,38 @@ impl Files { impl fmt::Debug for Files { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut map = f.debug_map(); + if f.alternate() { + let mut map = f.debug_map(); - for entry in self.inner.system_by_path.iter() { - map.entry(entry.key(), entry.value()); + for entry in self.inner.system_by_path.iter() { + map.entry(entry.key(), entry.value()); + } + map.finish() + } else { + f.debug_struct("Files") + .field("system_by_path", &self.inner.system_by_path.len()) + .field( + "system_virtual_by_path", + &self.inner.system_virtual_by_path.len(), + ) + .field("vendored_by_path", &self.inner.vendored_by_path.len()) + .finish() } - map.finish() } } impl std::panic::RefUnwindSafe for Files {} /// A file that's either stored on the host system's file system or in the vendored file system. +/// +/// # Ordering +/// Ordering is based on the file's salsa-assigned id and not on its values. +/// The id may change between runs. #[salsa::input] +#[derive(PartialOrd, Ord)] pub struct File { - /// The path of the file. - #[return_ref] + /// The path of the file (immutable). + #[returns(ref)] pub path: FilePath, /// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows @@ -298,6 +319,9 @@ pub struct File { count: Count, } +// The Salsa heap is tracked separately. +impl get_size2::GetSize for File {} + impl File { /// Reads the content of the file into a [`String`]. /// @@ -545,10 +569,33 @@ impl Ranged for FileRange { } } +impl TryFrom<&Span> for FileRange { + type Error = (); + + fn try_from(value: &Span) -> Result { + let UnifiedFile::Ty(file) = value.file() else { + return Err(()); + }; + + Ok(Self { + file: *file, + range: value.range().ok_or(())?, + }) + } +} + +impl TryFrom for FileRange { + type Error = (); + + fn try_from(value: Span) -> Result { + Self::try_from(&value) + } +} + #[cfg(test)] mod tests { use crate::file_revision::FileRevision; - use crate::files::{system_path_to_file, vendored_path_to_file, FileError}; + use crate::files::{FileError, system_path_to_file, vendored_path_to_file}; use crate::system::DbWithWritableSystem as _; use crate::tests::TestDb; use crate::vendored::VendoredFileSystemBuilder; diff --git a/crates/ruff_db/src/files/file_root.rs b/crates/ruff_db/src/files/file_root.rs index c991d337ac2a8..baf4716f15fe0 100644 --- a/crates/ruff_db/src/files/file_root.rs +++ b/crates/ruff_db/src/files/file_root.rs @@ -3,9 +3,9 @@ use std::fmt::Formatter; use path_slash::PathExt; use salsa::Durability; +use crate::Db; use crate::file_revision::FileRevision; use crate::system::{SystemPath, SystemPathBuf}; -use crate::Db; /// A root path for files tracked by the database. /// @@ -19,8 +19,8 @@ use crate::Db; #[salsa::input(debug)] pub struct FileRoot { /// The path of a root is guaranteed to never change. - #[return_ref] - path_buf: SystemPathBuf, + #[returns(deref)] + pub path: SystemPathBuf, /// The kind of the root at the time of its creation. kind_at_time_of_creation: FileRootKind, @@ -32,10 +32,6 @@ pub struct FileRoot { } impl FileRoot { - pub fn path(self, db: &dyn Db) -> &SystemPath { - self.path_buf(db) - } - pub fn durability(self, db: &dyn Db) -> salsa::Durability { self.kind_at_time_of_creation(db).durability() } diff --git a/crates/ruff_db/src/files/path.rs b/crates/ruff_db/src/files/path.rs index fddac0fa226fc..ebe6e7261a1f9 100644 --- a/crates/ruff_db/src/files/path.rs +++ b/crates/ruff_db/src/files/path.rs @@ -1,7 +1,7 @@ -use crate::files::{system_path_to_file, vendored_path_to_file, File}; +use crate::Db; +use crate::files::{File, system_path_to_file, vendored_path_to_file}; use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf}; use crate::vendored::{VendoredPath, VendoredPathBuf}; -use crate::Db; use std::fmt::{Display, Formatter}; /// Path to a file. diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index 55a47d96d1f45..b501956807523 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -1,10 +1,11 @@ -use std::hash::BuildHasherDefault; - -use rustc_hash::FxHasher; - use crate::files::Files; use crate::system::System; use crate::vendored::VendoredFileSystem; +use ruff_python_ast::PythonVersion; +use rustc_hash::FxHasher; +use std::hash::BuildHasherDefault; +use std::num::NonZeroUsize; +use ty_static::EnvVars; pub mod diagnostic; pub mod display; @@ -18,6 +19,12 @@ pub mod system; pub mod testing; pub mod vendored; +#[cfg(not(target_arch = "wasm32"))] +pub use std::time::{Instant, SystemTime, SystemTimeError}; + +#[cfg(target_arch = "wasm32")] +pub use web_time::{Instant, SystemTime, SystemTimeError}; + pub type FxDashMap = dashmap::DashMap>; pub type FxDashSet = dashmap::DashSet>; @@ -27,23 +34,50 @@ pub trait Db: salsa::Database { fn vendored(&self) -> &VendoredFileSystem; fn system(&self) -> &dyn System; fn files(&self) -> &Files; + fn python_version(&self) -> PythonVersion; +} + +/// Returns the maximum number of tasks that ty is allowed +/// to process in parallel. +/// +/// Returns [`std::thread::available_parallelism`], unless the environment +/// variable `TY_MAX_PARALLELISM` or `RAYON_NUM_THREADS` is set. `TY_MAX_PARALLELISM` takes +/// precedence over `RAYON_NUM_THREADS`. +/// +/// Falls back to `1` if `available_parallelism` is not available. +/// +/// Setting `TY_MAX_PARALLELISM` to `2` only restricts the number of threads that ty spawns +/// to process work in parallel. For example, to index a directory or checking the files of a project. +/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or +/// watching the files for changes. +pub fn max_parallelism() -> NonZeroUsize { + std::env::var(EnvVars::TY_MAX_PARALLELISM) + .or_else(|_| std::env::var(EnvVars::RAYON_NUM_THREADS)) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + std::thread::available_parallelism().unwrap_or_else(|_| NonZeroUsize::new(1).unwrap()) + }) } -/// Trait for upcasting a reference to a base trait object. -pub trait Upcast { - fn upcast(&self) -> &T; - fn upcast_mut(&mut self) -> &mut T; +/// Trait for types that can provide Rust documentation. +/// +/// Use `derive(RustDoc)` to automatically implement this trait for types that have a static string documentation. +pub trait RustDoc { + fn rust_doc() -> &'static str; } #[cfg(test)] mod tests { - use std::sync::Arc; + use std::sync::{Arc, Mutex}; + use crate::Db; use crate::files::Files; use crate::system::TestSystem; use crate::system::{DbWithTestSystem, System}; use crate::vendored::VendoredFileSystem; - use crate::Db; + + type Events = Arc>>; /// Database that can be used for testing. /// @@ -55,36 +89,37 @@ mod tests { files: Files, system: TestSystem, vendored: VendoredFileSystem, - events: Arc>>, + events: Events, } impl TestDb { pub(crate) fn new() -> Self { + let events = Events::default(); Self { - storage: salsa::Storage::default(), + storage: salsa::Storage::new(Some(Box::new({ + let events = events.clone(); + move |event| { + tracing::trace!("event: {:?}", event); + let mut events = events.lock().unwrap(); + events.push(event); + } + }))), system: TestSystem::default(), vendored: VendoredFileSystem::default(), - events: std::sync::Arc::default(), + events, files: Files::default(), } } /// Empties the internal store of salsa events that have been emitted, /// and returns them as a `Vec` (equivalent to [`std::mem::take`]). - /// - /// ## Panics - /// If there are pending database snapshots. pub(crate) fn take_salsa_events(&mut self) -> Vec { - let inner = Arc::get_mut(&mut self.events) - .expect("expected no pending salsa database snapshots."); + let mut events = self.events.lock().unwrap(); - std::mem::take(inner.get_mut().unwrap()) + std::mem::take(&mut *events) } /// Clears the emitted salsa events. - /// - /// ## Panics - /// If there are pending database snapshots. pub(crate) fn clear_salsa_events(&mut self) { self.take_salsa_events(); } @@ -107,6 +142,10 @@ mod tests { fn files(&self) -> &Files { &self.files } + + fn python_version(&self) -> ruff_python_ast::PythonVersion { + ruff_python_ast::PythonVersion::latest_ty() + } } impl DbWithTestSystem for TestDb { @@ -120,12 +159,5 @@ mod tests { } #[salsa::db] - impl salsa::Database for TestDb { - fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) { - let event = event(); - tracing::trace!("event: {:?}", event); - let mut events = self.events.lock().unwrap(); - events.push(event); - } - } + impl salsa::Database for TestDb {} } diff --git a/crates/ruff_db/src/panic.rs b/crates/ruff_db/src/panic.rs index 576425a99e0fe..a6b67f1f1de0b 100644 --- a/crates/ruff_db/src/panic.rs +++ b/crates/ruff_db/src/panic.rs @@ -1,34 +1,80 @@ +use std::any::Any; +use std::backtrace::BacktraceStatus; use std::cell::Cell; use std::panic::Location; use std::sync::OnceLock; -#[derive(Default, Debug)] +#[derive(Debug)] pub struct PanicError { pub location: Option, - pub payload: Option, + pub payload: Payload, pub backtrace: Option, + pub salsa_backtrace: Option, +} + +#[derive(Debug)] +pub struct Payload(Box); + +impl Payload { + pub fn as_str(&self) -> Option<&str> { + if let Some(s) = self.0.downcast_ref::() { + Some(s) + } else if let Some(s) = self.0.downcast_ref::<&str>() { + Some(s) + } else { + None + } + } + + pub fn downcast_ref(&self) -> Option<&R> { + self.0.downcast_ref::() + } } impl std::fmt::Display for PanicError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "panicked at")?; + write!(f, "panicked at")?; if let Some(location) = &self.location { write!(f, " {location}")?; } - if let Some(payload) = &self.payload { + if let Some(payload) = self.payload.as_str() { write!(f, ":\n{payload}")?; } + if let Some(query_trace) = self.salsa_backtrace.as_ref() { + let _ = writeln!(f, "{query_trace}"); + } + if let Some(backtrace) = &self.backtrace { - writeln!(f, "\nBacktrace: {backtrace}")?; + match backtrace.status() { + BacktraceStatus::Disabled => { + writeln!( + f, + "\nrun with `RUST_BACKTRACE=1` environment variable to display a backtrace" + )?; + } + BacktraceStatus::Captured => { + writeln!(f, "\nBacktrace: {backtrace}")?; + } + _ => {} + } } + Ok(()) } } +#[derive(Default)] +struct CapturedPanicInfo { + backtrace: Option, + location: Option, + salsa_backtrace: Option, +} + thread_local! { static CAPTURE_PANIC_INFO: Cell = const { Cell::new(false) }; - static OUR_HOOK_RAN: Cell = const { Cell::new(false) }; - static LAST_PANIC: Cell> = const { Cell::new(None) }; + static LAST_BACKTRACE: Cell = const { + Cell::new(CapturedPanicInfo { backtrace: None, location: None, salsa_backtrace: None }) + }; } fn install_hook() { @@ -36,24 +82,18 @@ fn install_hook() { ONCE.get_or_init(|| { let prev = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - OUR_HOOK_RAN.with(|cell| cell.set(true)); let should_capture = CAPTURE_PANIC_INFO.with(Cell::get); if !should_capture { return (*prev)(info); } - let payload = if let Some(s) = info.payload().downcast_ref::<&str>() { - Some(s.to_string()) - } else { - info.payload().downcast_ref::().cloned() - }; + let location = info.location().map(Location::to_string); - let backtrace = std::backtrace::Backtrace::force_capture(); - LAST_PANIC.with(|cell| { - cell.set(Some(PanicError { - payload, - location, - backtrace: Some(backtrace), - })); + let backtrace = Some(std::backtrace::Backtrace::capture()); + + LAST_BACKTRACE.set(CapturedPanicInfo { + backtrace, + location, + salsa_backtrace: salsa::Backtrace::capture(), }); })); }); @@ -70,7 +110,7 @@ fn install_hook() { /// stderr). /// /// We assume that there is nothing else running in this process that needs to install a competing -/// panic hook. We are careful to install our custom hook only once, and we do not ever restore +/// panic hook. We are careful to install our custom hook only once, and we do not ever restore /// the previous hook (since you can always retain the previous hook's behavior by not calling this /// wrapper). pub fn catch_unwind(f: F) -> Result @@ -78,15 +118,83 @@ where F: FnOnce() -> R + std::panic::UnwindSafe, { install_hook(); - OUR_HOOK_RAN.with(|cell| cell.set(false)); - let prev_should_capture = CAPTURE_PANIC_INFO.with(|cell| cell.replace(true)); - let result = std::panic::catch_unwind(f).map_err(|_| { - let our_hook_ran = OUR_HOOK_RAN.with(Cell::get); - if !our_hook_ran { - panic!("detected a competing panic hook"); + let prev_should_capture = CAPTURE_PANIC_INFO.replace(true); + let result = std::panic::catch_unwind(f).map_err(|payload| { + // Try to get the backtrace and location from our custom panic hook. + // The custom panic hook only runs once when `panic!` is called (or similar). It doesn't + // run when the panic is propagated with `std::panic::resume_unwind`. The panic hook + // is also not called when the panic is raised with `std::panic::resum_unwind` as is the + // case for salsa unwinds (see the ignored test below). + // Because of that, always take the payload from `catch_unwind` because it may have been transformed + // by an inner `std::panic::catch_unwind` handlers and only use the information + // from the custom handler to enrich the error with the backtrace and location. + let CapturedPanicInfo { + location, + backtrace, + salsa_backtrace, + } = LAST_BACKTRACE.with(Cell::take); + + PanicError { + location, + payload: Payload(payload), + backtrace, + salsa_backtrace, } - LAST_PANIC.with(Cell::take).unwrap_or_default() }); - CAPTURE_PANIC_INFO.with(|cell| cell.set(prev_should_capture)); + CAPTURE_PANIC_INFO.set(prev_should_capture); result } + +#[cfg(test)] +mod tests { + use salsa::{Database, Durability}; + + #[test] + #[ignore = "super::catch_unwind installs a custom panic handler, which could effect test isolation"] + fn no_backtrace_for_salsa_cancelled() { + #[salsa::input] + struct Input { + value: u32, + } + + #[salsa::tracked] + fn test_query(db: &dyn Database, input: Input) -> u32 { + loop { + // This should throw a cancelled error + let _ = input.value(db); + } + } + + let db = salsa::DatabaseImpl::new(); + + let input = Input::new(&db, 42); + + let result = std::thread::scope(move |scope| { + { + let mut db = db.clone(); + scope.spawn(move || { + // This will cancel the other thread by throwing a `salsa::Cancelled` error. + db.synthetic_write(Durability::MEDIUM); + }); + } + + { + scope.spawn(move || { + super::catch_unwind(|| { + test_query(&db, input); + }) + }) + } + .join() + .unwrap() + }); + + match result { + Ok(_) => panic!("Expected query to panic"), + Err(err) => { + // Panics triggered with `resume_unwind` have no backtrace. + assert!(err.backtrace.is_none()); + } + } + } +} diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index a72ef55f71fb4..dd98114e3a368 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -1,13 +1,14 @@ use std::fmt::Formatter; -use std::ops::Deref; use std::sync::Arc; -use ruff_python_ast::ModModule; -use ruff_python_parser::{parse_unchecked_source, Parsed}; +use arc_swap::ArcSwapOption; +use get_size2::GetSize; +use ruff_python_ast::{AnyRootNodeRef, ModModule, NodeIndex}; +use ruff_python_parser::{ParseOptions, Parsed, parse_unchecked}; +use crate::Db; use crate::files::File; use crate::source::source_text; -use crate::Db; /// Returns the parsed AST of `file`, including its token stream. /// @@ -18,42 +19,87 @@ use crate::Db; /// The query is only cached when the [`source_text()`] hasn't changed. This is because /// comparing two ASTs is a non-trivial operation and every offset change is directly /// reflected in the changed AST offsets. -/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires +/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires /// for determining if a query result is unchanged. -#[salsa::tracked(return_ref, no_eq)] +#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)] pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule { let _span = tracing::trace_span!("parsed_module", ?file).entered(); + let parsed = parsed_module_impl(db, file); + + ParsedModule::new(file, parsed) +} + +pub fn parsed_module_impl(db: &dyn Db, file: File) -> Parsed { let source = source_text(db, file); let ty = file.source_type(db); - ParsedModule::new(parse_unchecked_source(&source, ty)) + let target_version = db.python_version(); + let options = ParseOptions::from(ty).with_target_version(target_version); + parse_unchecked(&source, options) + .try_into_module() + .expect("PySourceType always parses into a module") } -/// Cheap cloneable wrapper around the parsed module. -#[derive(Clone)] +/// A wrapper around a parsed module. +/// +/// This type manages instances of the module AST. A particular instance of the AST +/// is represented with the [`ParsedModuleRef`] type. +#[derive(Clone, get_size2::GetSize)] pub struct ParsedModule { - inner: Arc>, + file: File, + #[get_size(size_fn = arc_swap_size)] + inner: Arc>, } impl ParsedModule { - pub fn new(parsed: Parsed) -> Self { + pub fn new(file: File, parsed: Parsed) -> Self { Self { - inner: Arc::new(parsed), + file, + inner: Arc::new(ArcSwapOption::new(Some(indexed::IndexedModule::new( + parsed, + )))), } } - - /// Consumes `self` and returns the Arc storing the parsed module. - pub fn into_arc(self) -> Arc> { - self.inner + /// Loads a reference to the parsed module. + /// + /// Note that holding on to the reference will prevent garbage collection + /// of the AST. This method will reparse the module if it has been collected. + pub fn load(&self, db: &dyn Db) -> ParsedModuleRef { + let parsed = match self.inner.load_full() { + Some(parsed) => parsed, + None => { + // Re-parse the file. + let parsed = indexed::IndexedModule::new(parsed_module_impl(db, self.file)); + tracing::debug!( + "File `{}` was reparsed after being collected in the current Salsa revision", + self.file.path(db) + ); + + self.inner.store(Some(parsed.clone())); + parsed + } + }; + + ParsedModuleRef { + module: self.clone(), + indexed: parsed, + } } -} -impl Deref for ParsedModule { - type Target = Parsed; + /// Clear the parsed module, dropping the AST once all references to it are dropped. + pub fn clear(&self) { + self.inner.store(None); + } - fn deref(&self) -> &Self::Target { - &self.inner + /// Returns a pointer for this [`ParsedModule`]. + /// + /// The pointer uniquely identifies the module within the current Salsa revision, + /// regardless of whether particular [`ParsedModuleRef`] instances are garbage collected. + pub fn as_ptr(&self) -> *const () { + // Note that the outer `Arc` in `inner` is stable across garbage collection, while the inner + // `Arc` within the `ArcSwap` may change. + Arc::as_ptr(&self.inner).cast() } } @@ -71,8 +117,288 @@ impl PartialEq for ParsedModule { impl Eq for ParsedModule {} +/// Cheap cloneable wrapper around an instance of a module AST. +#[derive(Clone)] +pub struct ParsedModuleRef { + module: ParsedModule, + indexed: Arc, +} + +impl ParsedModuleRef { + /// Returns a reference to the [`ParsedModule`] that this instance was loaded from. + pub fn module(&self) -> &ParsedModule { + &self.module + } + + /// Returns a reference to the AST node at the given index. + pub fn get_by_index<'ast>(&'ast self, index: NodeIndex) -> AnyRootNodeRef<'ast> { + self.indexed.get_by_index(index) + } +} + +impl std::ops::Deref for ParsedModuleRef { + type Target = Parsed; + + fn deref(&self) -> &Self::Target { + &self.indexed.parsed + } +} + +/// Returns the heap-size of the currently stored `T` in the `ArcSwap`. +fn arc_swap_size(arc_swap: &Arc>) -> usize +where + T: GetSize, +{ + if let Some(value) = &*arc_swap.load() { + T::get_heap_size(value) + } else { + 0 + } +} + +mod indexed { + use std::sync::Arc; + + use ruff_python_ast::visitor::source_order::*; + use ruff_python_ast::*; + use ruff_python_parser::Parsed; + + /// A wrapper around the AST that allows access to AST nodes by index. + #[derive(Debug, get_size2::GetSize)] + pub struct IndexedModule { + index: Box<[AnyRootNodeRef<'static>]>, + pub parsed: Parsed, + } + + impl IndexedModule { + /// Create a new [`IndexedModule`] from the given AST. + #[allow(clippy::unnecessary_cast)] + pub fn new(parsed: Parsed) -> Arc { + let mut visitor = Visitor { + nodes: Vec::new(), + index: 0, + }; + + let mut inner = Arc::new(IndexedModule { + parsed, + index: Box::new([]), + }); + + AnyNodeRef::from(inner.parsed.syntax()).visit_source_order(&mut visitor); + + let index: Box<[AnyRootNodeRef<'_>]> = visitor.nodes.into_boxed_slice(); + + // SAFETY: We cast from `Box<[AnyRootNodeRef<'_>]>` to `Box<[AnyRootNodeRef<'static>]>`, + // faking the 'static lifetime to create the self-referential struct. The node references + // are into the `Arc>`, so are valid for as long as the `IndexedModule` + // is alive. We make sure to restore the correct lifetime in `get_by_index`. + // + // Note that we can never move the data within the `Arc` after this point. + Arc::get_mut(&mut inner).unwrap().index = + unsafe { Box::from_raw(Box::into_raw(index) as *mut [AnyRootNodeRef<'static>]) }; + + inner + } + + /// Returns the node at the given index. + pub fn get_by_index<'ast>(&'ast self, index: NodeIndex) -> AnyRootNodeRef<'ast> { + // Note that this method restores the correct lifetime: the nodes are valid for as + // long as the reference to `IndexedModule` is alive. + self.index[index.as_usize()] + } + } + + /// A visitor that collects nodes in source order. + pub struct Visitor<'a> { + pub index: u32, + pub nodes: Vec>, + } + + impl<'a> Visitor<'a> { + fn visit_node(&mut self, node: &'a T) + where + T: HasNodeIndex + std::fmt::Debug, + AnyRootNodeRef<'a>: From<&'a T>, + { + node.node_index().set(self.index); + self.nodes.push(AnyRootNodeRef::from(node)); + self.index += 1; + } + } + + impl<'a> SourceOrderVisitor<'a> for Visitor<'a> { + #[inline] + fn visit_mod(&mut self, module: &'a Mod) { + self.visit_node(module); + walk_module(self, module); + } + + #[inline] + fn visit_stmt(&mut self, stmt: &'a Stmt) { + self.visit_node(stmt); + walk_stmt(self, stmt); + } + + #[inline] + fn visit_annotation(&mut self, expr: &'a Expr) { + self.visit_node(expr); + walk_annotation(self, expr); + } + + #[inline] + fn visit_expr(&mut self, expr: &'a Expr) { + self.visit_node(expr); + walk_expr(self, expr); + } + + #[inline] + fn visit_decorator(&mut self, decorator: &'a Decorator) { + self.visit_node(decorator); + walk_decorator(self, decorator); + } + + #[inline] + fn visit_comprehension(&mut self, comprehension: &'a Comprehension) { + self.visit_node(comprehension); + walk_comprehension(self, comprehension); + } + + #[inline] + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + self.visit_node(except_handler); + walk_except_handler(self, except_handler); + } + + #[inline] + fn visit_arguments(&mut self, arguments: &'a Arguments) { + self.visit_node(arguments); + walk_arguments(self, arguments); + } + + #[inline] + fn visit_parameters(&mut self, parameters: &'a Parameters) { + self.visit_node(parameters); + walk_parameters(self, parameters); + } + + #[inline] + fn visit_parameter(&mut self, arg: &'a Parameter) { + self.visit_node(arg); + walk_parameter(self, arg); + } + + fn visit_parameter_with_default( + &mut self, + parameter_with_default: &'a ParameterWithDefault, + ) { + self.visit_node(parameter_with_default); + walk_parameter_with_default(self, parameter_with_default); + } + + #[inline] + fn visit_keyword(&mut self, keyword: &'a Keyword) { + self.visit_node(keyword); + walk_keyword(self, keyword); + } + + #[inline] + fn visit_alias(&mut self, alias: &'a Alias) { + self.visit_node(alias); + walk_alias(self, alias); + } + + #[inline] + fn visit_with_item(&mut self, with_item: &'a WithItem) { + self.visit_node(with_item); + walk_with_item(self, with_item); + } + + #[inline] + fn visit_type_params(&mut self, type_params: &'a TypeParams) { + self.visit_node(type_params); + walk_type_params(self, type_params); + } + + #[inline] + fn visit_type_param(&mut self, type_param: &'a TypeParam) { + self.visit_node(type_param); + walk_type_param(self, type_param); + } + + #[inline] + fn visit_match_case(&mut self, match_case: &'a MatchCase) { + self.visit_node(match_case); + walk_match_case(self, match_case); + } + + #[inline] + fn visit_pattern(&mut self, pattern: &'a Pattern) { + self.visit_node(pattern); + walk_pattern(self, pattern); + } + + #[inline] + fn visit_pattern_arguments(&mut self, pattern_arguments: &'a PatternArguments) { + self.visit_node(pattern_arguments); + walk_pattern_arguments(self, pattern_arguments); + } + + #[inline] + fn visit_pattern_keyword(&mut self, pattern_keyword: &'a PatternKeyword) { + self.visit_node(pattern_keyword); + walk_pattern_keyword(self, pattern_keyword); + } + + #[inline] + fn visit_elif_else_clause(&mut self, elif_else_clause: &'a ElifElseClause) { + self.visit_node(elif_else_clause); + walk_elif_else_clause(self, elif_else_clause); + } + + #[inline] + fn visit_f_string(&mut self, f_string: &'a FString) { + self.visit_node(f_string); + walk_f_string(self, f_string); + } + + #[inline] + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { + self.visit_node(interpolated_string_element); + walk_interpolated_string_element(self, interpolated_string_element); + } + + #[inline] + fn visit_t_string(&mut self, t_string: &'a TString) { + self.visit_node(t_string); + walk_t_string(self, t_string); + } + + #[inline] + fn visit_string_literal(&mut self, string_literal: &'a StringLiteral) { + self.visit_node(string_literal); + walk_string_literal(self, string_literal); + } + + #[inline] + fn visit_bytes_literal(&mut self, bytes_literal: &'a BytesLiteral) { + self.visit_node(bytes_literal); + walk_bytes_literal(self, bytes_literal); + } + + #[inline] + fn visit_identifier(&mut self, identifier: &'a Identifier) { + self.visit_node(identifier); + walk_identifier(self, identifier); + } + } +} + #[cfg(test)] mod tests { + use crate::Db; use crate::files::{system_path_to_file, vendored_path_to_file}; use crate::parsed::parsed_module; use crate::system::{ @@ -80,7 +406,6 @@ mod tests { }; use crate::tests::TestDb; use crate::vendored::{VendoredFileSystemBuilder, VendoredPath}; - use crate::Db; use zip::CompressionMethod; #[test] @@ -92,7 +417,7 @@ mod tests { let file = system_path_to_file(&db, path).unwrap(); - let parsed = parsed_module(&db, file); + let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); @@ -108,7 +433,7 @@ mod tests { let file = system_path_to_file(&db, path).unwrap(); - let parsed = parsed_module(&db, file); + let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); @@ -124,7 +449,7 @@ mod tests { let virtual_file = db.files().virtual_file(&db, path); - let parsed = parsed_module(&db, virtual_file.file()); + let parsed = parsed_module(&db, virtual_file.file()).load(&db); assert!(parsed.has_valid_syntax()); @@ -140,7 +465,7 @@ mod tests { let virtual_file = db.files().virtual_file(&db, path); - let parsed = parsed_module(&db, virtual_file.file()); + let parsed = parsed_module(&db, virtual_file.file()).load(&db); assert!(parsed.has_valid_syntax()); @@ -171,7 +496,7 @@ else: let file = vendored_path_to_file(&db, VendoredPath::new("path.pyi")).unwrap(); - let parsed = parsed_module(&db, file); + let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); } diff --git a/crates/ruff_db/src/source.rs b/crates/ruff_db/src/source.rs index 6d7d9157519c2..824c17cae41a7 100644 --- a/crates/ruff_db/src/source.rs +++ b/crates/ruff_db/src/source.rs @@ -7,11 +7,11 @@ use ruff_notebook::Notebook; use ruff_python_ast::PySourceType; use ruff_source_file::LineIndex; -use crate::files::{File, FilePath}; use crate::Db; +use crate::files::{File, FilePath}; /// Reads the source text of a python text file (must be valid UTF8) or notebook. -#[salsa::tracked] +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] pub fn source_text(db: &dyn Db, file: File) -> SourceText { let path = file.path(db); let _span = tracing::trace_span!("source_text", file = %path).entered(); @@ -65,7 +65,7 @@ fn is_notebook(path: &FilePath) -> bool { /// The file containing the source text can either be a text file or a notebook. /// /// Cheap cloneable in `O(1)`. -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, get_size2::GetSize)] pub struct SourceText { inner: Arc, } @@ -123,8 +123,9 @@ impl std::fmt::Debug for SourceText { } } -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, get_size2::GetSize)] struct SourceTextInner { + #[get_size(ignore)] count: Count, kind: SourceTextKind, read_error: Option, @@ -133,7 +134,20 @@ struct SourceTextInner { #[derive(Eq, PartialEq)] enum SourceTextKind { Text(String), - Notebook(Notebook), + Notebook(Box), +} + +impl get_size2::GetSize for SourceTextKind { + fn get_heap_size(&self) -> usize { + match self { + SourceTextKind::Text(text) => text.get_heap_size(), + // TODO: The `get-size` derive does not support ignoring enum variants. + // + // Jupyter notebooks are not very relevant for memory profiling, and contain + // arbitrary JSON values that do not implement the `GetSize` trait. + SourceTextKind::Notebook(_) => 0, + } + } } impl From for SourceTextKind { @@ -144,11 +158,11 @@ impl From for SourceTextKind { impl From for SourceTextKind { fn from(notebook: Notebook) -> Self { - SourceTextKind::Notebook(notebook) + SourceTextKind::Notebook(Box::new(notebook)) } } -#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)] +#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, get_size2::GetSize)] pub enum SourceTextError { #[error("Failed to read notebook: {0}`")] FailedToReadNotebook(String), @@ -157,7 +171,7 @@ pub enum SourceTextError { } /// Computes the [`LineIndex`] for `file`. -#[salsa::tracked] +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] pub fn line_index(db: &dyn Db, file: File) -> LineIndex { let _span = tracing::trace_span!("line_index", ?file).entered(); @@ -216,9 +230,11 @@ mod tests { let events = db.take_salsa_events(); - assert!(!events - .iter() - .any(|event| matches!(event.kind, EventKind::WillExecute { .. }))); + assert!( + !events + .iter() + .any(|event| matches!(event.kind, EventKind::WillExecute { .. })) + ); Ok(()) } diff --git a/crates/ruff_db/src/system.rs b/crates/ruff_db/src/system.rs index 0c5efa64461cf..e8b3062f9f862 100644 --- a/crates/ruff_db/src/system.rs +++ b/crates/ruff_db/src/system.rs @@ -19,8 +19,8 @@ use walk_directory::WalkDirectoryBuilder; use crate::file_revision::FileRevision; pub use self::path::{ - deduplicate_nested_paths, DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, - SystemVirtualPath, SystemVirtualPathBuf, + DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath, + SystemVirtualPathBuf, deduplicate_nested_paths, }; mod memory_fs; @@ -124,6 +124,11 @@ pub trait System: Debug { /// Returns `None` if no such convention exists for the system. fn user_config_directory(&self) -> Option; + /// Returns the directory path where cached files are stored. + /// + /// Returns `None` if no such convention exists for the system. + fn cache_dir(&self) -> Option; + /// Iterate over the contents of the directory at `path`. /// /// The returned iterator must have the following properties: @@ -167,10 +172,28 @@ pub trait System: Debug { &self, pattern: &str, ) -> std::result::Result< - Box>>, + Box> + '_>, PatternError, >; + /// Fetches the environment variable `key` from the current process. + /// + /// # Errors + /// + /// Returns [`std::env::VarError::NotPresent`] if: + /// - The variable is not set. + /// - The variable's name contains an equal sign or NUL (`'='` or `'\0'`). + /// + /// Returns [`std::env::VarError::NotUnicode`] if the variable's value is not valid + /// Unicode. + fn env_var(&self, name: &str) -> std::result::Result { + let _ = name; + Err(std::env::VarError::NotPresent) + } + + /// Returns a handle to a [`WritableSystem`] if this system is writeable. + fn as_writable(&self) -> Option<&dyn WritableSystem>; + fn as_any(&self) -> &dyn std::any::Any; fn as_any_mut(&mut self) -> &mut dyn std::any::Any; @@ -211,11 +234,52 @@ impl fmt::Display for CaseSensitivity { /// System trait for non-readonly systems. pub trait WritableSystem: System { + /// Creates a file at the given path. + /// + /// Returns an error if the file already exists. + fn create_new_file(&self, path: &SystemPath) -> Result<()>; + /// Writes the given content to the file at the given path. fn write_file(&self, path: &SystemPath, content: &str) -> Result<()>; /// Creates a directory at `path` as well as any intermediate directories. fn create_directory_all(&self, path: &SystemPath) -> Result<()>; + + /// Reads the provided file from the system cache, or creates the file if necessary. + /// + /// Returns `Ok(None)` if the system does not expose a suitable cache directory. + fn get_or_cache( + &self, + path: &SystemPath, + read_contents: &dyn Fn() -> Result, + ) -> Result> { + let Some(cache_dir) = self.cache_dir() else { + return Ok(None); + }; + + let cache_path = cache_dir.join(path); + + // The file has already been cached. + if self.is_file(&cache_path) { + return Ok(Some(cache_path)); + } + + // Read the file contents. + let contents = read_contents()?; + + // Create the parent directory. + self.create_directory_all(cache_path.parent().unwrap())?; + + // Create and write to the file on the system. + // + // Note that `create_new_file` will fail if the file has already been created. This + // ensures that only one thread/process ever attempts to write to it to avoid corrupting + // the cache. + self.create_new_file(&cache_path)?; + self.write_file(&cache_path, &contents)?; + + Ok(Some(cache_path)) + } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/ruff_db/src/system/memory_fs.rs b/crates/ruff_db/src/system/memory_fs.rs index f773b53d73b09..d5cbaa7d637ff 100644 --- a/crates/ruff_db/src/system/memory_fs.rs +++ b/crates/ruff_db/src/system/memory_fs.rs @@ -1,4 +1,5 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, btree_map}; +use std::io; use std::iter::FusedIterator; use std::sync::{Arc, RwLock, RwLockWriteGuard}; @@ -7,8 +8,8 @@ use filetime::FileTime; use rustc_hash::FxHashMap; use crate::system::{ - file_time_now, walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, - Result, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, + DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, SystemPath, + SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, file_time_now, walk_directory, }; use super::walk_directory::{ @@ -153,6 +154,26 @@ impl MemoryFileSystem { virtual_files.contains_key(&path.to_path_buf()) } + pub(crate) fn create_new_file(&self, path: &SystemPath) -> Result<()> { + let normalized = self.normalize_path(path); + + let mut by_path = self.inner.by_path.write().unwrap(); + match by_path.entry(normalized) { + btree_map::Entry::Vacant(entry) => { + entry.insert(Entry::File(File { + content: String::new(), + last_modified: file_time_now(), + })); + + Ok(()) + } + btree_map::Entry::Occupied(_) => Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "File already exists", + )), + } + } + /// Stores a new file in the file system. /// /// The operation overrides the content for an existing file with the same normalized `path`. @@ -236,7 +257,7 @@ impl MemoryFileSystem { &self, pattern: &str, ) -> std::result::Result< - impl Iterator>, + impl Iterator> + '_, glob::PatternError, > { // Very naive implementation that iterates over all files and collects all that match the given pattern. @@ -278,14 +299,14 @@ impl MemoryFileSystem { let normalized = fs.normalize_path(path); match by_path.entry(normalized) { - std::collections::btree_map::Entry::Occupied(entry) => match entry.get() { + btree_map::Entry::Occupied(entry) => match entry.get() { Entry::File(_) => { entry.remove(); Ok(()) } Entry::Directory(_) => Err(is_a_directory()), }, - std::collections::btree_map::Entry::Vacant(_) => Err(not_found()), + btree_map::Entry::Vacant(_) => Err(not_found()), } } @@ -345,14 +366,14 @@ impl MemoryFileSystem { } match by_path.entry(normalized.clone()) { - std::collections::btree_map::Entry::Occupied(entry) => match entry.get() { + btree_map::Entry::Occupied(entry) => match entry.get() { Entry::Directory(_) => { entry.remove(); Ok(()) } Entry::File(_) => Err(not_a_directory()), }, - std::collections::btree_map::Entry::Vacant(_) => Err(not_found()), + btree_map::Entry::Vacant(_) => Err(not_found()), } } @@ -463,17 +484,17 @@ fn not_found() -> std::io::Error { fn is_a_directory() -> std::io::Error { // Note: Rust returns `ErrorKind::IsADirectory` for this error but this is a nightly only variant :(. // So we have to use other for now. - std::io::Error::new(std::io::ErrorKind::Other, "Is a directory") + std::io::Error::other("Is a directory") } fn not_a_directory() -> std::io::Error { // Note: Rust returns `ErrorKind::NotADirectory` for this error but this is a nightly only variant :(. // So we have to use `Other` for now. - std::io::Error::new(std::io::ErrorKind::Other, "Not a directory") + std::io::Error::other("Not a directory") } fn directory_not_empty() -> std::io::Error { - std::io::Error::new(std::io::ErrorKind::Other, "directory not empty") + std::io::Error::other("directory not empty") } fn create_dir_all( @@ -701,8 +722,8 @@ mod tests { use std::time::Duration; - use crate::system::walk_directory::tests::DirectoryEntryToString; use crate::system::walk_directory::WalkState; + use crate::system::walk_directory::tests::DirectoryEntryToString; use crate::system::{ DirectoryEntry, FileType, MemoryFileSystem, Result, SystemPath, SystemPathBuf, SystemVirtualPath, diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index 91f97a63aa4aa..dd6a8eea99bab 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -1,20 +1,20 @@ +use super::walk_directory::{ + self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration, + WalkDirectoryVisitorBuilder, WalkState, +}; +use crate::max_parallelism; +use crate::system::{ + CaseSensitivity, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, + SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, +}; use filetime::FileTime; use ruff_notebook::{Notebook, NotebookError}; use rustc_hash::FxHashSet; +use std::num::NonZeroUsize; use std::panic::RefUnwindSafe; use std::sync::Arc; use std::{any::Any, path::PathBuf}; -use crate::system::{ - CaseSensitivity, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, - SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, -}; - -use super::walk_directory::{ - self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration, - WalkDirectoryVisitorBuilder, WalkState, -}; - /// A system implementation that uses the OS file system. #[derive(Debug, Clone)] pub struct OsSystem { @@ -50,7 +50,6 @@ impl OsSystem { Self { // Spreading `..Default` because it isn't possible to feature gate the initializer of a single field. - #[allow(clippy::needless_update)] inner: Arc::new(OsSystemInner { cwd: cwd.to_path_buf(), case_sensitivity, @@ -161,6 +160,39 @@ impl System for OsSystem { None } + /// Returns an absolute cache directory on the system. + /// + /// On Linux and macOS, uses `$XDG_CACHE_HOME/ty` or `.cache/ty`. + /// On Windows, uses `C:\Users\User\AppData\Local\ty\cache`. + #[cfg(not(target_arch = "wasm32"))] + fn cache_dir(&self) -> Option { + use etcetera::BaseStrategy as _; + + let cache_dir = etcetera::base_strategy::choose_base_strategy() + .ok() + .map(|dirs| dirs.cache_dir().join("ty")) + .map(|cache_dir| { + if cfg!(windows) { + // On Windows, we append `cache` to the LocalAppData directory, i.e., prefer + // `C:\Users\User\AppData\Local\ty\cache` over `C:\Users\User\AppData\Local\ty`. + cache_dir.join("cache") + } else { + cache_dir + } + }) + .and_then(|path| SystemPathBuf::from_path_buf(path).ok()) + .unwrap_or_else(|| SystemPathBuf::from(".ty_cache")); + + Some(cache_dir) + } + + // TODO: Remove this feature gating once `ruff_wasm` no longer indirectly depends on `ruff_db` with the + // `os` feature enabled (via `ruff_workspace` -> `ruff_graph` -> `ruff_db`). + #[cfg(target_arch = "wasm32")] + fn cache_dir(&self) -> Option { + None + } + /// Creates a builder to recursively walk `path`. /// /// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`] @@ -193,6 +225,10 @@ impl System for OsSystem { }) } + fn as_writable(&self) -> Option<&dyn WritableSystem> { + Some(self) + } + fn as_any(&self) -> &dyn Any { self } @@ -215,6 +251,10 @@ impl System for OsSystem { }) }))) } + + fn env_var(&self, name: &str) -> std::result::Result { + std::env::var(name) + } } impl OsSystem { @@ -257,7 +297,9 @@ impl OsSystem { let Ok(canonicalized) = SystemPathBuf::from_path_buf(canonicalized) else { // The original path is valid UTF8 but the canonicalized path isn't. This definitely suggests // that a symlink is involved. Fall back to the slow path. - tracing::debug!("Falling back to the slow case-sensitive path existence check because the canonicalized path of `{simplified}` is not valid UTF-8"); + tracing::debug!( + "Falling back to the slow case-sensitive path existence check because the canonicalized path of `{simplified}` is not valid UTF-8" + ); return None; }; @@ -267,7 +309,9 @@ impl OsSystem { // `path` pointed to a symlink (or some other none reversible path normalization happened). // In this case, fall back to the slow path. if simplified_canonicalized.as_str().to_lowercase() != simplified.as_str().to_lowercase() { - tracing::debug!("Falling back to the slow case-sensitive path existence check for `{simplified}` because the canonicalized path `{simplified_canonicalized}` differs not only by casing"); + tracing::debug!( + "Falling back to the slow case-sensitive path existence check for `{simplified}` because the canonicalized path `{simplified_canonicalized}` differs not only by casing" + ); return None; } @@ -303,6 +347,10 @@ impl OsSystem { } impl WritableSystem for OsSystem { + fn create_new_file(&self, path: &SystemPath) -> Result<()> { + std::fs::File::create_new(path).map(drop) + } + fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> { std::fs::write(path.as_std_path(), content) } @@ -427,11 +475,7 @@ impl DirectoryWalker for OsDirectoryWalker { builder.add(additional_path.as_std_path()); } - builder.threads( - std::thread::available_parallelism() - .map_or(1, std::num::NonZeroUsize::get) - .min(12), - ); + builder.threads(max_parallelism().min(NonZeroUsize::new(12).unwrap()).get()); builder.build_parallel().run(|| { let mut visitor = visitor_builder.build(); @@ -667,8 +711,8 @@ fn detect_case_sensitivity(path: &SystemPath) -> CaseSensitivity { mod tests { use tempfile::TempDir; - use crate::system::walk_directory::tests::DirectoryEntryToString; use crate::system::DirectoryEntry; + use crate::system::walk_directory::tests::DirectoryEntryToString; use super::*; diff --git a/crates/ruff_db/src/system/path.rs b/crates/ruff_db/src/system/path.rs index 4aea0cbe8b641..fbba17ebcefd0 100644 --- a/crates/ruff_db/src/system/path.rs +++ b/crates/ruff_db/src/system/path.rs @@ -45,6 +45,30 @@ impl SystemPath { SystemPath::from_std_path(dunce::simplified(self.as_std_path())).unwrap() } + /// Returns `true` if the `SystemPath` is absolute, i.e., if it is independent of + /// the current directory. + /// + /// * On Unix, a path is absolute if it starts with the root, so + /// `is_absolute` and [`has_root`] are equivalent. + /// + /// * On Windows, a path is absolute if it has a prefix and starts with the + /// root: `c:\windows` is absolute, while `c:temp` and `\temp` are not. + /// + /// # Examples + /// + /// ``` + /// use ruff_db::system::SystemPath; + /// + /// assert!(!SystemPath::new("foo.txt").is_absolute()); + /// ``` + /// + /// [`has_root`]: Utf8Path::has_root + #[inline] + #[must_use] + pub fn is_absolute(&self) -> bool { + self.0.is_absolute() + } + /// Extracts the file extension, if possible. /// /// The extension is: @@ -479,6 +503,12 @@ impl ToOwned for SystemPath { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf); +impl get_size2::GetSize for SystemPathBuf { + fn get_heap_size(&self) -> usize { + self.0.capacity() + } +} + impl SystemPathBuf { pub fn new() -> Self { Self(Utf8PathBuf::new()) @@ -538,6 +568,10 @@ impl SystemPathBuf { self.0.into_std_path_buf() } + pub fn into_string(self) -> String { + self.0.into_string() + } + #[inline] pub fn as_path(&self) -> &SystemPath { SystemPath::new(&self.0) @@ -596,6 +630,13 @@ impl AsRef for Utf8PathBuf { } } +impl AsRef for camino::Utf8Component<'_> { + #[inline] + fn as_ref(&self) -> &SystemPath { + SystemPath::new(self.as_str()) + } +} + impl AsRef for str { #[inline] fn as_ref(&self) -> &SystemPath { @@ -626,6 +667,22 @@ impl Deref for SystemPathBuf { } } +impl> FromIterator

for SystemPathBuf { + fn from_iter>(iter: I) -> Self { + let mut buf = SystemPathBuf::new(); + buf.extend(iter); + buf + } +} + +impl> Extend

for SystemPathBuf { + fn extend>(&mut self, iter: I) { + for path in iter { + self.push(path); + } + } +} + impl std::fmt::Debug for SystemPath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index bb57354664b41..f595aadca7a2f 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -3,15 +3,15 @@ use ruff_notebook::{Notebook, NotebookError}; use std::panic::RefUnwindSafe; use std::sync::{Arc, Mutex}; +use crate::Db; use crate::files::File; use crate::system::{ CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, Result, System, SystemPath, SystemPathBuf, SystemVirtualPath, }; -use crate::Db; -use super::walk_directory::WalkDirectoryBuilder; use super::WritableSystem; +use super::walk_directory::WalkDirectoryBuilder; /// System implementation intended for testing. /// @@ -102,6 +102,10 @@ impl System for TestSystem { self.system().user_config_directory() } + fn cache_dir(&self) -> Option { + self.system().cache_dir() + } + fn read_directory<'a>( &'a self, path: &SystemPath, @@ -117,12 +121,16 @@ impl System for TestSystem { &self, pattern: &str, ) -> std::result::Result< - Box>>, + Box> + '_>, PatternError, > { self.system().glob(pattern) } + fn as_writable(&self) -> Option<&dyn WritableSystem> { + Some(self) + } + fn as_any(&self) -> &dyn std::any::Any { self } @@ -149,6 +157,10 @@ impl Default for TestSystem { } impl WritableSystem for TestSystem { + fn create_new_file(&self, path: &SystemPath) -> Result<()> { + self.system().create_new_file(path) + } + fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> { self.system().write_file(path, content) } @@ -280,6 +292,13 @@ impl InMemorySystem { } } + pub fn from_memory_fs(memory_fs: MemoryFileSystem) -> Self { + Self { + user_config_directory: Mutex::new(None), + memory_fs, + } + } + pub fn fs(&self) -> &MemoryFileSystem { &self.memory_fs } @@ -328,6 +347,10 @@ impl System for InMemorySystem { self.user_config_directory.lock().unwrap().clone() } + fn cache_dir(&self) -> Option { + None + } + fn read_directory<'a>( &'a self, path: &SystemPath, @@ -343,13 +366,17 @@ impl System for InMemorySystem { &self, pattern: &str, ) -> std::result::Result< - Box>>, + Box> + '_>, PatternError, > { let iterator = self.memory_fs.glob(pattern)?; Ok(Box::new(iterator)) } + fn as_writable(&self) -> Option<&dyn WritableSystem> { + Some(self) + } + fn as_any(&self) -> &dyn std::any::Any { self } @@ -370,6 +397,10 @@ impl System for InMemorySystem { } impl WritableSystem for InMemorySystem { + fn create_new_file(&self, path: &SystemPath) -> Result<()> { + self.memory_fs.create_new_file(path) + } + fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> { self.memory_fs.write_file(path, content) } diff --git a/crates/ruff_db/src/system/walk_directory.rs b/crates/ruff_db/src/system/walk_directory.rs index 7932ccfae7114..5796321c72dfb 100644 --- a/crates/ruff_db/src/system/walk_directory.rs +++ b/crates/ruff_db/src/system/walk_directory.rs @@ -35,7 +35,7 @@ impl WalkDirectoryBuilder { /// Each additional path is traversed recursively. /// This should be preferred over building multiple /// walkers since it enables reusing resources. - #[allow(clippy::should_implement_trait)] + #[expect(clippy::should_implement_trait)] pub fn add(mut self, path: impl AsRef) -> Self { self.paths.push(path.as_ref().to_path_buf()); self @@ -212,7 +212,7 @@ impl Display for Error { path: Some(path), err, } => { - write!(f, "IO error for operation on {}: {}", path, err) + write!(f, "IO error for operation on {path}: {err}") } ErrorKind::Io { path: None, err } => err.fmt(f), ErrorKind::NonUtf8Path { path } => { diff --git a/crates/ruff_db/src/testing.rs b/crates/ruff_db/src/testing.rs index 9bd683ccad8d8..1ed58f1977c23 100644 --- a/crates/ruff_db/src/testing.rs +++ b/crates/ruff_db/src/testing.rs @@ -1,7 +1,7 @@ //! Test helpers for working with Salsa databases -use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; pub fn assert_function_query_was_not_run( db: &Db, @@ -13,12 +13,12 @@ pub fn assert_function_query_was_not_run( Q: Fn(QDb, I) -> R, I: salsa::plumbing::AsId + std::fmt::Debug + Copy, { - let id = input.as_id().as_u32(); + let id = input.as_id(); let (query_name, will_execute_event) = find_will_execute_event(db, query, input, events); db.attach(|_| { if let Some(will_execute_event) = will_execute_event { - panic!("Expected query {query_name}({id}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}"); + panic!("Expected query {query_name}({id:?}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}"); } }); } @@ -65,7 +65,7 @@ pub fn assert_function_query_was_run( Q: Fn(QDb, I) -> R, I: salsa::plumbing::AsId + std::fmt::Debug + Copy, { - let id = input.as_id().as_u32(); + let id = input.as_id(); let (query_name, will_execute_event) = find_will_execute_event(db, query, input, events); db.attach(|_| { @@ -107,7 +107,7 @@ fn query_name(_query: &Q) -> &'static str { .unwrap_or(full_qualified_query_name) } -/// Sets up logging for the current thread. It captures all `red_knot` and `ruff` events. +/// Sets up logging for the current thread. It captures all `ty` and `ruff` events. /// /// Useful for capturing the tracing output in a failing test. /// @@ -128,7 +128,7 @@ pub fn setup_logging() -> LoggingGuard { /// # Examples /// ``` /// use ruff_db::testing::setup_logging_with_filter; -/// let _logging = setup_logging_with_filter("red_knot_module_resolver::resolver"); +/// let _logging = setup_logging_with_filter("ty_module_resolver::resolver"); /// ``` /// /// # Filter @@ -141,67 +141,38 @@ pub fn setup_logging_with_filter(filter: &str) -> Option { #[derive(Debug)] pub struct LoggingBuilder { filter: EnvFilter, - hierarchical: bool, } impl LoggingBuilder { pub fn new() -> Self { Self { filter: EnvFilter::default() - .add_directive( - "red_knot=trace" - .parse() - .expect("Hardcoded directive to be valid"), - ) + .add_directive("ty=trace".parse().expect("Hardcoded directive to be valid")) .add_directive( "ruff=trace" .parse() .expect("Hardcoded directive to be valid"), ), - hierarchical: false, } } pub fn with_filter(filter: &str) -> Option { let filter = EnvFilter::builder().parse(filter).ok()?; - Some(Self { - filter, - hierarchical: false, - }) - } - - pub fn with_hierarchical(mut self, hierarchical: bool) -> Self { - self.hierarchical = hierarchical; - self + Some(Self { filter }) } pub fn build(self) -> LoggingGuard { let registry = tracing_subscriber::registry().with(self.filter); - let guard = if self.hierarchical { - let subscriber = registry.with( - tracing_tree::HierarchicalLayer::default() - .with_indent_lines(true) - .with_indent_amount(2) - .with_bracketed_fields(true) - .with_thread_ids(true) - .with_targets(true) - .with_writer(std::io::stderr) - .with_timer(tracing_tree::time::Uptime::default()), - ); - - tracing::subscriber::set_default(subscriber) - } else { - let subscriber = registry.with( - tracing_subscriber::fmt::layer() - .compact() - .with_writer(std::io::stderr) - .with_timer(tracing_subscriber::fmt::time()), - ); + let subscriber = registry.with( + tracing_subscriber::fmt::layer() + .compact() + .with_writer(std::io::stderr) + .with_timer(tracing_subscriber::fmt::time()), + ); - tracing::subscriber::set_default(subscriber) - }; + let guard = tracing::subscriber::set_default(subscriber); LoggingGuard { _guard: guard } } @@ -253,7 +224,7 @@ fn query_was_not_run() { } #[test] -#[should_panic(expected = "Expected query len(0) not to have run but it did:")] +#[should_panic(expected = "Expected query len(Id(0)) not to have run but it did:")] fn query_was_not_run_fails_if_query_was_run() { use crate::tests::TestDb; use salsa::prelude::*; @@ -316,7 +287,7 @@ fn const_query_was_not_run_fails_if_query_was_run() { } #[test] -#[should_panic(expected = "Expected query len(0) to have run but it did not:")] +#[should_panic(expected = "Expected query len(Id(0)) to have run but it did not:")] fn query_was_run_fails_if_query_was_not_run() { use crate::tests::TestDb; use salsa::prelude::*; diff --git a/crates/ruff_db/src/vendored.rs b/crates/ruff_db/src/vendored.rs index 923824d1e95a3..b1227af2a44a6 100644 --- a/crates/ruff_db/src/vendored.rs +++ b/crates/ruff_db/src/vendored.rs @@ -4,12 +4,12 @@ use std::fmt::{self, Debug}; use std::io::{self, Read, Write}; use std::sync::{Arc, Mutex, MutexGuard}; -use crate::file_revision::FileRevision; use zip::result::ZipResult; use zip::write::FileOptions; -use zip::{read::ZipFile, CompressionMethod, ZipArchive, ZipWriter}; +use zip::{CompressionMethod, ZipArchive, ZipWriter, read::ZipFile}; pub use self::path::{VendoredPath, VendoredPathBuf}; +use crate::file_revision::FileRevision; mod path; @@ -172,7 +172,7 @@ impl Default for VendoredFileSystem { /// that users of the `VendoredFileSystem` could realistically need. /// For debugging purposes, however, we want to have all information /// available. -#[allow(unused)] +#[expect(unused)] #[derive(Debug)] struct ZipFileDebugInfo { crc32_hash: u32, @@ -503,9 +503,11 @@ pub(crate) mod tests { let path = VendoredPath::new(path); assert!(!mock_typeshed.exists(path)); assert!(mock_typeshed.metadata(path).is_err()); - assert!(mock_typeshed - .read_to_string(path) - .is_err_and(|err| err.to_string().contains("file not found"))); + assert!( + mock_typeshed + .read_to_string(path) + .is_err_and(|err| err.to_string().contains("file not found")) + ); } #[test] diff --git a/crates/ruff_db/src/vendored/path.rs b/crates/ruff_db/src/vendored/path.rs index a8cb07a672363..f32ce08239d38 100644 --- a/crates/ruff_db/src/vendored/path.rs +++ b/crates/ruff_db/src/vendored/path.rs @@ -87,6 +87,12 @@ impl ToOwned for VendoredPath { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct VendoredPathBuf(Utf8PathBuf); +impl get_size2::GetSize for VendoredPathBuf { + fn get_heap_size(&self) -> usize { + self.0.capacity() + } +} + impl Default for VendoredPathBuf { fn default() -> Self { Self::new() diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 9432476a5aefd..b7068640f50a5 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -11,12 +11,14 @@ repository = { workspace = true } license = { workspace = true } [dependencies] -red_knot_project = { workspace = true, features = ["schemars"] } +ty = { workspace = true } +ty_project = { workspace = true, features = ["schemars"] } +ty_static = { workspace = true } ruff = { workspace = true } -ruff_diagnostics = { workspace = true } ruff_formatter = { workspace = true } ruff_linter = { workspace = true, features = ["schemars"] } ruff_notebook = { workspace = true } +ruff_options_metadata = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_codegen = { workspace = true } ruff_python_formatter = { workspace = true } @@ -31,6 +33,7 @@ imara-diff = { workspace = true } indicatif = { workspace = true } itertools = { workspace = true } libcst = { workspace = true } +markdown = { version = "1.0.0" } pretty_assertions = { workspace = true } rayon = { workspace = true } regex = { workspace = true } @@ -44,6 +47,7 @@ toml = { workspace = true, features = ["parse"] } tracing = { workspace = true } tracing-indicatif = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +url = { workspace = true } [dev-dependencies] indoc = { workspace = true } diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index f9be6b0bf591a..8f0dcc5183c32 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -9,11 +9,11 @@ use std::process::ExitCode; use std::time::{Duration, Instant}; use std::{fmt, fs, io, iter}; -use anyhow::{bail, format_err, Context, Error}; +use anyhow::{Context, Error, bail, format_err}; use clap::{CommandFactory, FromArgMatches}; use imara_diff::intern::InternedInput; use imara_diff::sink::Counter; -use imara_diff::{diff, Algorithm}; +use imara_diff::{Algorithm, diff}; use indicatif::ProgressStyle; #[cfg_attr(feature = "singlethreaded", allow(unused_imports))] use rayon::iter::{IntoParallelIterator, ParallelIterator}; @@ -21,11 +21,11 @@ use serde::Deserialize; use similar::{ChangeTag, TextDiff}; use tempfile::NamedTempFile; use tracing::{debug, error, info, info_span}; -use tracing_indicatif::span_ext::IndicatifSpanExt; use tracing_indicatif::IndicatifLayer; +use tracing_indicatif::span_ext::IndicatifSpanExt; +use tracing_subscriber::EnvFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::EnvFilter; use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, GlobalConfigArgs, LogLevelArgs}; use ruff::resolve::resolve; @@ -33,10 +33,10 @@ use ruff_formatter::{FormatError, LineWidth, PrintError}; use ruff_linter::logging::LogLevel; use ruff_linter::settings::types::{FilePattern, FilePatternSet}; use ruff_python_formatter::{ - format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions, + FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions, format_module_source, }; use ruff_python_parser::ParseError; -use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver}; +use ruff_workspace::resolver::{PyprojectConfig, ResolvedFile, Resolver, python_files_in_path}; fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArguments)> { let args_matches = FormatCommand::command() @@ -63,7 +63,6 @@ fn find_pyproject_config( } /// Find files that ruff would check so we can format them. Adapted from `ruff`. -#[allow(clippy::type_complexity)] fn ruff_check_paths<'a>( pyproject_config: &'a PyprojectConfig, cli: &FormatArguments, @@ -135,12 +134,12 @@ impl Statistics { } /// We currently prefer the similarity index, but i'd like to keep this around - #[allow(clippy::cast_precision_loss, unused)] + #[expect(clippy::cast_precision_loss, unused)] pub(crate) fn jaccard_index(&self) -> f32 { self.intersection as f32 / (self.black_input + self.ruff_output + self.intersection) as f32 } - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] pub(crate) fn similarity_index(&self) -> f32 { self.intersection as f32 / (self.black_input + self.intersection) as f32 } @@ -177,7 +176,7 @@ pub(crate) enum Format { Full, } -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] #[derive(clap::Args)] pub(crate) struct Args { /// Like `ruff check`'s files. See `--multi-project` if you want to format an ecosystem @@ -222,7 +221,7 @@ pub(crate) struct Args { #[arg(long)] pub(crate) files_with_errors: Option, #[clap(flatten)] - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] pub(crate) log_level_args: LogLevelArgs, } diff --git a/crates/ruff_dev/src/generate_all.rs b/crates/ruff_dev/src/generate_all.rs index f22c562ae6ef4..f5cefa70efb22 100644 --- a/crates/ruff_dev/src/generate_all.rs +++ b/crates/ruff_dev/src/generate_all.rs @@ -2,7 +2,10 @@ use anyhow::Result; -use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_knot_schema}; +use crate::{ + generate_cli_help, generate_docs, generate_json_schema, generate_ty_cli_reference, + generate_ty_env_vars_reference, generate_ty_options, generate_ty_rules, generate_ty_schema, +}; pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all"; @@ -33,10 +36,16 @@ impl Mode { pub(crate) fn main(args: &Args) -> Result<()> { generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?; - generate_knot_schema::main(&generate_knot_schema::Args { mode: args.mode })?; + generate_ty_schema::main(&generate_ty_schema::Args { mode: args.mode })?; generate_cli_help::main(&generate_cli_help::Args { mode: args.mode })?; generate_docs::main(&generate_docs::Args { dry_run: args.mode.is_dry_run(), })?; + generate_ty_options::main(&generate_ty_options::Args { mode: args.mode })?; + generate_ty_rules::main(&generate_ty_rules::Args { mode: args.mode })?; + generate_ty_cli_reference::main(&generate_ty_cli_reference::Args { mode: args.mode })?; + generate_ty_env_vars_reference::main(&generate_ty_env_vars_reference::Args { + mode: args.mode, + })?; Ok(()) } diff --git a/crates/ruff_dev/src/generate_cli_help.rs b/crates/ruff_dev/src/generate_cli_help.rs index 20eb41bb2f6a6..26f2bd52d0096 100644 --- a/crates/ruff_dev/src/generate_cli_help.rs +++ b/crates/ruff_dev/src/generate_cli_help.rs @@ -1,17 +1,16 @@ //! Generate CLI help. -#![allow(clippy::print_stdout)] use std::path::PathBuf; use std::{fs, str}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use clap::CommandFactory; use pretty_assertions::StrComparison; use ruff::args; -use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; use crate::ROOT_DIR; +use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; const COMMAND_HELP_BEGIN_PRAGMA: &str = "\n"; const COMMAND_HELP_END_PRAGMA: &str = ""; @@ -141,7 +140,7 @@ mod tests { use crate::generate_all::Mode; - use super::{main, Args}; + use super::{Args, main}; #[test] fn test_generate_json_schema() -> Result<()> { diff --git a/crates/ruff_dev/src/generate_docs.rs b/crates/ruff_dev/src/generate_docs.rs index 362ea4411f502..5f2309328ef0a 100644 --- a/crates/ruff_dev/src/generate_docs.rs +++ b/crates/ruff_dev/src/generate_docs.rs @@ -1,5 +1,4 @@ //! Generate Markdown documentation for applicable rules. -#![allow(clippy::print_stdout, clippy::print_stderr)] use std::collections::HashSet; use std::fmt::Write as _; @@ -11,10 +10,10 @@ use itertools::Itertools; use regex::{Captures, Regex}; use strum::IntoEnumIterator; -use ruff_diagnostics::FixAvailability; +use ruff_linter::FixAvailability; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; +use ruff_options_metadata::{OptionEntry, OptionsMetadata}; use ruff_workspace::options::Options; -use ruff_workspace::options_base::{OptionEntry, OptionsMetadata}; use crate::ROOT_DIR; @@ -30,7 +29,7 @@ pub(crate) fn main(args: &Args) -> Result<()> { if let Some(explanation) = rule.explanation() { let mut output = String::new(); - let _ = writeln!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code()); + let _ = writeln!(&mut output, "# {} ({})", rule.name(), rule.noqa_code()); let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); if linter.url().is_some() { @@ -102,7 +101,7 @@ pub(crate) fn main(args: &Args) -> Result<()> { let filename = PathBuf::from(ROOT_DIR) .join("docs") .join("rules") - .join(rule.as_ref()) + .join(&*rule.name()) .with_extension("md"); if args.dry_run { diff --git a/crates/ruff_dev/src/generate_json_schema.rs b/crates/ruff_dev/src/generate_json_schema.rs index c82843eef2597..61d616cfb4b22 100644 --- a/crates/ruff_dev/src/generate_json_schema.rs +++ b/crates/ruff_dev/src/generate_json_schema.rs @@ -1,14 +1,12 @@ -#![allow(clippy::print_stdout, clippy::print_stderr)] - use std::fs; use std::path::PathBuf; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use pretty_assertions::StrComparison; use schemars::schema_for; -use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; use crate::ROOT_DIR; +use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; use ruff_workspace::options::Options; #[derive(clap::Args)] @@ -58,7 +56,7 @@ mod tests { use crate::generate_all::Mode; - use super::{main, Args}; + use super::{Args, main}; #[test] fn test_generate_json_schema() -> Result<()> { diff --git a/crates/ruff_dev/src/generate_knot_schema.rs b/crates/ruff_dev/src/generate_knot_schema.rs deleted file mode 100644 index 0c6f8320981cc..0000000000000 --- a/crates/ruff_dev/src/generate_knot_schema.rs +++ /dev/null @@ -1,72 +0,0 @@ -#![allow(clippy::print_stdout, clippy::print_stderr)] - -use std::fs; -use std::path::PathBuf; - -use anyhow::{bail, Result}; -use pretty_assertions::StrComparison; -use schemars::schema_for; - -use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; -use crate::ROOT_DIR; -use red_knot_project::metadata::options::Options; - -#[derive(clap::Args)] -pub(crate) struct Args { - /// Write the generated table to stdout (rather than to `knot.schema.json`). - #[arg(long, default_value_t, value_enum)] - pub(crate) mode: Mode, -} - -pub(crate) fn main(args: &Args) -> Result<()> { - let schema = schema_for!(Options); - let schema_string = serde_json::to_string_pretty(&schema).unwrap(); - let filename = "knot.schema.json"; - let schema_path = PathBuf::from(ROOT_DIR).join(filename); - - match args.mode { - Mode::DryRun => { - println!("{schema_string}"); - } - Mode::Check => { - let current = fs::read_to_string(schema_path)?; - if current == schema_string { - println!("Up-to-date: {filename}"); - } else { - let comparison = StrComparison::new(¤t, &schema_string); - bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}"); - } - } - Mode::Write => { - let current = fs::read_to_string(&schema_path)?; - if current == schema_string { - println!("Up-to-date: {filename}"); - } else { - println!("Updating: {filename}"); - fs::write(schema_path, schema_string.as_bytes())?; - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use anyhow::Result; - use std::env; - - use crate::generate_all::Mode; - - use super::{main, Args}; - - #[test] - fn test_generate_json_schema() -> Result<()> { - let mode = if env::var("KNOT_UPDATE_SCHEMA").as_deref() == Ok("1") { - Mode::Write - } else { - Mode::Check - }; - main(&Args { mode }) - } -} diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 7c187878b85bc..8b8579d730594 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -4,9 +4,9 @@ use itertools::Itertools; use std::fmt::Write; +use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; use ruff_python_trivia::textwrap; use ruff_workspace::options::Options; -use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit}; pub(crate) fn generate() -> String { let mut output = String::new(); @@ -100,8 +100,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S if parents_anchor.is_empty() { let _ = writeln!(output, "{header_level} [`{name}`](#{name}) {{: #{name} }}"); } else { - let _ = - writeln!(output, + let _ = writeln!( + output, "{header_level} [`{name}`](#{parents_anchor}_{name}) {{: #{parents_anchor}_{name} }}" ); diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 35723bae076bf..3255f8f42b276 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -8,54 +8,54 @@ use std::borrow::Cow; use std::fmt::Write; use strum::IntoEnumIterator; -use ruff_diagnostics::FixAvailability; +use ruff_linter::FixAvailability; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix; +use ruff_options_metadata::OptionsMetadata; use ruff_workspace::options::Options; -use ruff_workspace::options_base::OptionsMetadata; const FIX_SYMBOL: &str = "🛠️"; const PREVIEW_SYMBOL: &str = "🧪"; const REMOVED_SYMBOL: &str = "❌"; const WARNING_SYMBOL: &str = "⚠️"; -const STABLE_SYMBOL: &str = "✔️"; const SPACER: &str = "    "; +/// Style for the rule's fixability and status icons. +const SYMBOL_STYLE: &str = "style='width: 1em; display: inline-block;'"; +/// Style for the container wrapping the fixability and status icons. +const SYMBOLS_CONTAINER: &str = "style='display: flex; gap: 0.5rem; justify-content: end;'"; + fn generate_table(table_out: &mut String, rules: impl IntoIterator, linter: &Linter) { - table_out.push_str("| Code | Name | Message | |"); + table_out.push_str("| Code | Name | Message | |"); table_out.push('\n'); - table_out.push_str("| ---- | ---- | ------- | ------: |"); + table_out.push_str("| ---- | ---- | ------- | -: |"); table_out.push('\n'); for rule in rules { let status_token = match rule.group() { RuleGroup::Removed => { - format!("{REMOVED_SYMBOL}") + format!( + "{REMOVED_SYMBOL}" + ) } RuleGroup::Deprecated => { - format!("{WARNING_SYMBOL}") + format!( + "{WARNING_SYMBOL}" + ) } - #[allow(deprecated)] RuleGroup::Preview => { - format!("{PREVIEW_SYMBOL}") - } - RuleGroup::Stable => { - // A full opacity checkmark is a bit aggressive for indicating stable - format!("{STABLE_SYMBOL}") + format!("{PREVIEW_SYMBOL}") } + RuleGroup::Stable => format!(""), }; let fix_token = match rule.fixable() { FixAvailability::Always | FixAvailability::Sometimes => { - format!("{FIX_SYMBOL}") - } - FixAvailability::None => { - format!("") + format!("{FIX_SYMBOL}") } + FixAvailability::None => format!(""), }; - let tokens = format!("{status_token} {fix_token}"); - - let rule_name = rule.as_ref(); + let rule_name = rule.name(); // If the message ends in a bracketed expression (like: "Use {replacement}"), escape the // brackets. Otherwise, it'll be interpreted as an HTML attribute via the `attr_list` @@ -78,18 +78,17 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, se = ""; } - #[allow(clippy::or_fun_call)] + #[expect(clippy::or_fun_call)] let _ = write!( table_out, - "| {ss}{0}{1}{se} {{ #{0}{1} }} | {ss}{2}{se} | {ss}{3}{se} | {ss}{4}{se} |", - linter.common_prefix(), - linter.code_for_rule(rule).unwrap(), - rule.explanation() + "| {ss}{prefix}{code}{se} {{ #{prefix}{code} }} | {ss}{explanation}{se} | {ss}{message}{se} |

{status_token}{fix_token}
|", + prefix = linter.common_prefix(), + code = linter.code_for_rule(rule).unwrap(), + explanation = rule + .explanation() .is_some() .then_some(format_args!("[{rule_name}](rules/{rule_name}.md)")) .unwrap_or(format_args!("{rule_name}")), - message, - tokens, ); table_out.push('\n'); } @@ -105,29 +104,28 @@ pub(crate) fn generate() -> String { let _ = write!( &mut table_out, - "{SPACER}{STABLE_SYMBOL}{SPACER} The rule is stable." - ); - table_out.push_str("
"); - - let _ = write!(&mut table_out, "{SPACER}{PREVIEW_SYMBOL}{SPACER} The rule is unstable and is in [\"preview\"](faq.md#what-is-preview)." ); table_out.push_str("
"); - let _ = write!(&mut table_out, + let _ = write!( + &mut table_out, "{SPACER}{WARNING_SYMBOL}{SPACER} The rule has been deprecated and will be removed in a future release." ); table_out.push_str("
"); - let _ = write!(&mut table_out, + let _ = write!( + &mut table_out, "{SPACER}{REMOVED_SYMBOL}{SPACER} The rule has been removed only the documentation is available." ); table_out.push_str("
"); - let _ = write!(&mut table_out, + let _ = write!( + &mut table_out, "{SPACER}{FIX_SYMBOL}{SPACER} The rule is automatically fixable by the `--fix` command-line option." ); - table_out.push_str("
"); + table_out.push_str("\n\n"); + table_out.push_str("All rules not marked as preview, deprecated or removed are stable."); table_out.push('\n'); for linter in Linter::iter() { diff --git a/crates/ruff_dev/src/generate_ty_cli_reference.rs b/crates/ruff_dev/src/generate_ty_cli_reference.rs new file mode 100644 index 0000000000000..58914d74147a6 --- /dev/null +++ b/crates/ruff_dev/src/generate_ty_cli_reference.rs @@ -0,0 +1,335 @@ +//! Generate a Markdown-compatible reference for the ty command-line interface. +use std::cmp::max; +use std::path::PathBuf; + +use anyhow::{Result, bail}; +use clap::{Command, CommandFactory}; +use itertools::Itertools; +use pretty_assertions::StrComparison; + +use crate::ROOT_DIR; +use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; + +use ty::Cli; + +const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"]; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[arg(long, default_value_t, value_enum)] + pub(crate) mode: Mode, +} + +pub(crate) fn main(args: &Args) -> Result<()> { + let reference_string = generate(); + let filename = "crates/ty/docs/cli.md"; + let reference_path = PathBuf::from(ROOT_DIR).join(filename); + + match args.mode { + Mode::DryRun => { + println!("{reference_string}"); + } + Mode::Check => match std::fs::read_to_string(reference_path) { + Ok(current) => { + if current == reference_string { + println!("Up-to-date: {filename}"); + } else { + let comparison = StrComparison::new(¤t, &reference_string); + bail!( + "{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}" + ); + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + bail!("{filename} not found, please run `{REGENERATE_ALL_COMMAND}`"); + } + Err(err) => { + bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{err}"); + } + }, + Mode::Write => match std::fs::read_to_string(&reference_path) { + Ok(current) => { + if current == reference_string { + println!("Up-to-date: {filename}"); + } else { + println!("Updating: {filename}"); + std::fs::write(reference_path, reference_string.as_bytes())?; + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + println!("Updating: {filename}"); + std::fs::write(reference_path, reference_string.as_bytes())?; + } + Err(err) => { + bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}"); + } + }, + } + + Ok(()) +} + +fn generate() -> String { + let mut output = String::new(); + + let mut ty = Cli::command(); + + // It is very important to build the command before beginning inspection or subcommands + // will be missing all of the propagated options. + ty.build(); + + let mut parents = Vec::new(); + + output.push_str("\n\n"); + output.push_str("# CLI Reference\n\n"); + generate_command(&mut output, &ty, &mut parents); + + output +} + +#[allow(clippy::format_push_string)] +fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) { + if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) { + return; + } + + // Generate the command header. + let name = if parents.is_empty() { + command.get_name().to_string() + } else { + format!( + "{} {}", + parents.iter().map(|cmd| cmd.get_name()).join(" "), + command.get_name() + ) + }; + + // Display the top-level `ty` command at the same level as its children + let level = max(2, parents.len() + 1); + output.push_str(&format!("{} {name}\n\n", "#".repeat(level))); + + // Display the command description. + if let Some(about) = command.get_long_about().or_else(|| command.get_about()) { + output.push_str(&about.to_string()); + output.push_str("\n\n"); + } + + // Display the usage + { + // This appears to be the simplest way to get rendered usage from Clap, + // it is complicated to render it manually. It's annoying that it + // requires a mutable reference but it doesn't really matter. + let mut command = command.clone(); + output.push_str("

Usage

\n\n"); + output.push_str(&format!( + "```\n{}\n```", + command + .render_usage() + .to_string() + .trim_start_matches("Usage: "), + )); + output.push_str("\n\n"); + } + + if command.get_name() == "help" { + return; + } + + // Display a list of child commands + let mut subcommands = command.get_subcommands().peekable(); + let has_subcommands = subcommands.peek().is_some(); + if has_subcommands { + output.push_str("

Commands

\n\n"); + output.push_str("
"); + + for subcommand in subcommands { + if subcommand.is_hide_set() { + continue; + } + let subcommand_name = format!("{name} {}", subcommand.get_name()); + output.push_str(&format!( + "
{subcommand_name}
", + subcommand_name.replace(' ', "-") + )); + if let Some(about) = subcommand.get_about() { + output.push_str(&format!( + "
{}
\n", + markdown::to_html(&about.to_string()) + )); + } + } + + output.push_str("
\n\n"); + } + + // Do not display options for commands with children + if !has_subcommands { + let name_key = name.replace(' ', "-"); + + // Display positional arguments + let mut arguments = command + .get_positionals() + .filter(|arg| !arg.is_hide_set()) + .peekable(); + + if arguments.peek().is_some() { + output.push_str("

Arguments

\n\n"); + output.push_str("
"); + + for arg in arguments { + let id = format!("{name_key}--{}", arg.get_id()); + output.push_str(&format!("
")); + output.push_str(&format!( + "{}", + arg.get_id().to_string().to_uppercase(), + )); + output.push_str("
"); + if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) { + output.push_str("
"); + output.push_str(&format!("{}\n", markdown::to_html(&help.to_string()))); + output.push_str("
"); + } + } + + output.push_str("
\n\n"); + } + + // Display options and flags + let mut options = command + .get_arguments() + .filter(|arg| !arg.is_positional()) + .filter(|arg| !arg.is_hide_set()) + .sorted_by_key(|arg| arg.get_id()) + .peekable(); + + if options.peek().is_some() { + output.push_str("

Options

\n\n"); + output.push_str("
"); + for opt in options { + let Some(long) = opt.get_long() else { continue }; + let id = format!("{name_key}--{long}"); + + output.push_str(&format!("
")); + output.push_str(&format!("--{long}")); + for long_alias in opt.get_all_aliases().into_iter().flatten() { + output.push_str(&format!(", --{long_alias}")); + } + if let Some(short) = opt.get_short() { + output.push_str(&format!(", -{short}")); + } + for short_alias in opt.get_all_short_aliases().into_iter().flatten() { + output.push_str(&format!(", -{short_alias}")); + } + + // Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts` + if opt + .get_num_args() + .unwrap_or_else(|| 1.into()) + .takes_values() + { + if let Some(values) = opt.get_value_names() { + for value in values { + output.push_str(&format!( + " {}", + value.to_lowercase().replace('_', "-") + )); + } + } + } + output.push_str("
"); + if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) { + output.push_str("
"); + output.push_str(&format!("{}\n", markdown::to_html(&help.to_string()))); + emit_env_option(opt, output); + emit_default_option(opt, output); + emit_possible_options(opt, output); + output.push_str("
"); + } + } + + output.push_str("
"); + } + + output.push_str("\n\n"); + } + + parents.push(command); + + // Recurse to all of the subcommands. + for subcommand in command.get_subcommands() { + generate_command(output, subcommand, parents); + } + + parents.pop(); +} + +fn emit_env_option(opt: &clap::Arg, output: &mut String) { + if opt.is_hide_env_set() { + return; + } + if let Some(env) = opt.get_env() { + output.push_str(&markdown::to_html(&format!( + "May also be set with the `{}` environment variable.", + env.to_string_lossy() + ))); + } +} + +fn emit_default_option(opt: &clap::Arg, output: &mut String) { + if opt.is_hide_default_value_set() || !opt.get_num_args().expect("built").takes_values() { + return; + } + + let values = opt.get_default_values(); + if !values.is_empty() { + let value = format!( + "\n[default: {}]", + opt.get_default_values() + .iter() + .map(|s| s.to_string_lossy()) + .join(",") + ); + output.push_str(&markdown::to_html(&value)); + } +} + +fn emit_possible_options(opt: &clap::Arg, output: &mut String) { + if opt.is_hide_possible_values_set() { + return; + } + + let values = opt.get_possible_values(); + if !values.is_empty() { + let value = format!( + "\nPossible values:\n{}", + values + .into_iter() + .filter(|value| !value.is_hide_set()) + .map(|value| { + let name = value.get_name(); + value.get_help().map_or_else( + || format!(" - `{name}`"), + |help| format!(" - `{name}`: {help}"), + ) + }) + .collect_vec() + .join("\n"), + ); + output.push_str(&markdown::to_html(&value)); + } +} + +#[cfg(test)] +mod tests { + + use anyhow::Result; + + use crate::generate_all::Mode; + + use super::{Args, main}; + + #[test] + fn ty_cli_reference_is_up_to_date() -> Result<()> { + main(&Args { mode: Mode::Check }) + } +} diff --git a/crates/ruff_dev/src/generate_ty_env_vars_reference.rs b/crates/ruff_dev/src/generate_ty_env_vars_reference.rs new file mode 100644 index 0000000000000..8b2127df66164 --- /dev/null +++ b/crates/ruff_dev/src/generate_ty_env_vars_reference.rs @@ -0,0 +1,119 @@ +//! Generate the environment variables reference from `ty_static::EnvVars`. + +use std::collections::BTreeSet; +use std::fs; +use std::path::PathBuf; + +use anyhow::bail; +use pretty_assertions::StrComparison; + +use ty_static::EnvVars; + +use crate::generate_all::Mode; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[arg(long, default_value_t, value_enum)] + pub(crate) mode: Mode, +} + +pub(crate) fn main(args: &Args) -> anyhow::Result<()> { + let reference_string = generate(); + let filename = "environment.md"; + let reference_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("crates") + .join("ty") + .join("docs") + .join(filename); + + match args.mode { + Mode::DryRun => { + println!("{reference_string}"); + } + Mode::Check => match fs::read_to_string(&reference_path) { + Ok(current) => { + if current == reference_string { + println!("Up-to-date: {filename}"); + } else { + let comparison = StrComparison::new(¤t, &reference_string); + bail!( + "{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{comparison}" + ); + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + bail!( + "{filename} not found, please run `cargo dev generate-ty-env-vars-reference`" + ); + } + Err(err) => { + bail!( + "{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}" + ); + } + }, + Mode::Write => { + // Ensure the docs directory exists + if let Some(parent) = reference_path.parent() { + fs::create_dir_all(parent)?; + } + + match fs::read_to_string(&reference_path) { + Ok(current) => { + if current == reference_string { + println!("Up-to-date: {filename}"); + } else { + println!("Updating: {filename}"); + fs::write(&reference_path, reference_string.as_bytes())?; + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + println!("Updating: {filename}"); + fs::write(&reference_path, reference_string.as_bytes())?; + } + Err(err) => { + bail!( + "{filename} changed, please run `cargo dev generate-ty-env-vars-reference`:\n{err}" + ); + } + } + } + } + + Ok(()) +} + +fn generate() -> String { + let mut output = String::new(); + + output.push_str("# Environment variables\n\n"); + + // Partition and sort environment variables into TY_ and external variables. + let (ty_vars, external_vars): (BTreeSet<_>, BTreeSet<_>) = EnvVars::metadata() + .iter() + .partition(|(var, _)| var.starts_with("TY_")); + + output.push_str("ty defines and respects the following environment variables:\n\n"); + + for (var, doc) in ty_vars { + output.push_str(&render(var, doc)); + } + + output.push_str("## Externally-defined variables\n\n"); + output.push_str("ty also reads the following externally defined environment variables:\n\n"); + + for (var, doc) in external_vars { + output.push_str(&render(var, doc)); + } + + output +} + +/// Render an environment variable and its documentation. +fn render(var: &str, doc: &str) -> String { + format!("### `{var}`\n\n{doc}\n\n") +} diff --git a/crates/ruff_dev/src/generate_ty_options.rs b/crates/ruff_dev/src/generate_ty_options.rs new file mode 100644 index 0000000000000..af7794a0b2a62 --- /dev/null +++ b/crates/ruff_dev/src/generate_ty_options.rs @@ -0,0 +1,262 @@ +//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`. + +use anyhow::bail; +use itertools::Itertools; +use pretty_assertions::StrComparison; +use std::{fmt::Write, path::PathBuf}; + +use ruff_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; +use ty_project::metadata::Options; + +use crate::{ + ROOT_DIR, + generate_all::{Mode, REGENERATE_ALL_COMMAND}, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + /// Write the generated table to stdout (rather than to `crates/ty/docs/configuration.md`). + #[arg(long, default_value_t, value_enum)] + pub(crate) mode: Mode, +} + +pub(crate) fn main(args: &Args) -> anyhow::Result<()> { + let mut output = String::new(); + let file_name = "crates/ty/docs/configuration.md"; + let markdown_path = PathBuf::from(ROOT_DIR).join(file_name); + + output.push_str( + "\n\n", + ); + + generate_set( + &mut output, + Set::Toplevel(Options::metadata()), + &mut Vec::new(), + ); + + match args.mode { + Mode::DryRun => { + println!("{output}"); + } + Mode::Check => { + let current = std::fs::read_to_string(&markdown_path)?; + if output == current { + println!("Up-to-date: {file_name}",); + } else { + let comparison = StrComparison::new(¤t, &output); + bail!("{file_name} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}",); + } + } + Mode::Write => { + let current = std::fs::read_to_string(&markdown_path)?; + if current == output { + println!("Up-to-date: {file_name}",); + } else { + println!("Updating: {file_name}",); + std::fs::write(markdown_path, output.as_bytes())?; + } + } + } + + Ok(()) +} + +fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { + match &set { + Set::Toplevel(_) => { + output.push_str("# Configuration\n"); + } + Set::Named { name, .. } => { + let title = parents + .iter() + .filter_map(|set| set.name()) + .chain(std::iter::once(name.as_str())) + .join("."); + writeln!(output, "## `{title}`\n",).unwrap(); + } + } + + if let Some(documentation) = set.metadata().documentation() { + output.push_str(documentation); + output.push('\n'); + output.push('\n'); + } + + let mut visitor = CollectOptionsVisitor::default(); + set.metadata().record(&mut visitor); + + let (mut fields, mut sets) = (visitor.fields, visitor.groups); + + fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); + sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); + + parents.push(set); + + // Generate the fields. + for (name, field) in &fields { + emit_field(output, name, field, parents.as_slice()); + output.push_str("---\n\n"); + } + + // Generate all the sub-sets. + for (set_name, sub_set) in &sets { + generate_set( + output, + Set::Named { + name: set_name.to_string(), + set: *sub_set, + }, + parents, + ); + } + + parents.pop(); +} + +#[derive(Debug)] +enum Set { + Toplevel(OptionSet), + Named { name: String, set: OptionSet }, +} + +impl Set { + fn name(&self) -> Option<&str> { + match self { + Set::Toplevel(_) => None, + Set::Named { name, .. } => Some(name), + } + } + + fn metadata(&self) -> &OptionSet { + match self { + Set::Toplevel(set) => set, + Set::Named { set, .. } => set, + } + } +} + +fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) { + let header_level = "#".repeat(parents.len() + 1); + + let _ = writeln!(output, "{header_level} `{name}`"); + + output.push('\n'); + + if let Some(deprecated) = &field.deprecated { + output.push_str("> [!WARN] \"Deprecated\"\n"); + output.push_str("> This option has been deprecated"); + + if let Some(since) = deprecated.since { + write!(output, " in {since}").unwrap(); + } + + output.push('.'); + + if let Some(message) = deprecated.message { + writeln!(output, " {message}").unwrap(); + } + + output.push('\n'); + } + + output.push_str(field.doc); + output.push_str("\n\n"); + let _ = writeln!(output, "**Default value**: `{}`", field.default); + output.push('\n'); + let _ = writeln!(output, "**Type**: `{}`", field.value_type); + output.push('\n'); + output.push_str("**Example usage** (`pyproject.toml`):\n\n"); + output.push_str(&format_example( + &format_header( + field.scope, + field.example, + parents, + ConfigurationFile::PyprojectToml, + ), + field.example, + )); + output.push('\n'); +} + +fn format_example(header: &str, content: &str) -> String { + if header.is_empty() { + format!("```toml\n{content}\n```\n",) + } else { + format!("```toml\n{header}\n{content}\n```\n",) + } +} + +/// Format the TOML header for the example usage for a given option. +/// +/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`. +fn format_header( + scope: Option<&str>, + example: &str, + parents: &[Set], + configuration: ConfigurationFile, +) -> String { + let tool_parent = match configuration { + ConfigurationFile::PyprojectToml => Some("tool.ty"), + ConfigurationFile::TyToml => None, + }; + + let header = tool_parent + .into_iter() + .chain(parents.iter().filter_map(|parent| parent.name())) + .chain(scope) + .join("."); + + // Ex) `[[tool.ty.xx]]` + if example.starts_with(&format!("[[{header}")) { + return String::new(); + } + // Ex) `[tool.ty.rules]` + if example.starts_with(&format!("[{header}")) { + return String::new(); + } + + if header.is_empty() { + String::new() + } else { + format!("[{header}]") + } +} + +#[derive(Default)] +struct CollectOptionsVisitor { + groups: Vec<(String, OptionSet)>, + fields: Vec<(String, OptionField)>, +} + +impl Visit for CollectOptionsVisitor { + fn record_set(&mut self, name: &str, group: OptionSet) { + self.groups.push((name.to_owned(), group)); + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.fields.push((name.to_owned(), field)); + } +} + +#[derive(Debug, Copy, Clone)] +enum ConfigurationFile { + PyprojectToml, + #[expect(dead_code)] + TyToml, +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::generate_all::Mode; + + use super::{Args, main}; + + #[test] + fn ty_configuration_markdown_up_to_date() -> Result<()> { + main(&Args { mode: Mode::Check })?; + Ok(()) + } +} diff --git a/crates/ruff_dev/src/generate_ty_rules.rs b/crates/ruff_dev/src/generate_ty_rules.rs new file mode 100644 index 0000000000000..1f379b69a3fac --- /dev/null +++ b/crates/ruff_dev/src/generate_ty_rules.rs @@ -0,0 +1,130 @@ +//! Generates the rules table for ty + +use std::borrow::Cow; +use std::fmt::Write as _; +use std::fs; +use std::path::PathBuf; + +use anyhow::{Result, bail}; +use itertools::Itertools as _; +use pretty_assertions::StrComparison; + +use crate::ROOT_DIR; +use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; + +#[derive(clap::Args)] +pub(crate) struct Args { + /// Write the generated table to stdout (rather than to `ty.schema.json`). + #[arg(long, default_value_t, value_enum)] + pub(crate) mode: Mode, +} + +pub(crate) fn main(args: &Args) -> Result<()> { + let markdown = generate_markdown(); + let filename = "crates/ty/docs/rules.md"; + let schema_path = PathBuf::from(ROOT_DIR).join(filename); + + match args.mode { + Mode::DryRun => { + println!("{markdown}"); + } + Mode::Check => { + let current = fs::read_to_string(schema_path)?; + if current == markdown { + println!("Up-to-date: {filename}"); + } else { + let comparison = StrComparison::new(¤t, &markdown); + bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}"); + } + } + Mode::Write => { + let current = fs::read_to_string(&schema_path)?; + if current == markdown { + println!("Up-to-date: {filename}"); + } else { + println!("Updating: {filename}"); + fs::write(schema_path, markdown.as_bytes())?; + } + } + } + + Ok(()) +} + +fn generate_markdown() -> String { + let registry = &*ty_project::DEFAULT_LINT_REGISTRY; + + let mut output = String::new(); + + let _ = writeln!( + &mut output, + "\n" + ); + let _ = writeln!(&mut output, "# Rules\n"); + + let mut lints: Vec<_> = registry.lints().iter().collect(); + lints.sort_by(|a, b| { + a.default_level() + .cmp(&b.default_level()) + .reverse() + .then_with(|| a.name().cmp(&b.name())) + }); + + for lint in lints { + let _ = writeln!(&mut output, "## `{rule_name}`\n", rule_name = lint.name()); + + // Reformat headers as bold text + let mut in_code_fence = false; + let documentation = lint + .documentation_lines() + .map(|line| { + // Toggle the code fence state if we encounter a boundary + if line.starts_with("```") { + in_code_fence = !in_code_fence; + } + if !in_code_fence && line.starts_with('#') { + Cow::Owned(format!( + "**{line}**\n", + line = line.trim_start_matches('#').trim_start() + )) + } else { + Cow::Borrowed(line) + } + }) + .join("\n"); + + let _ = writeln!( + &mut output, + r#" +Default level: [`{level}`](../rules.md#rule-levels "This lint has a default level of '{level}'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}) · +[View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line}) + + +{documentation} +"#, + level = lint.default_level(), + encoded_name = url::form_urlencoded::byte_serialize(lint.name().as_str().as_bytes()) + .collect::(), + file = url::form_urlencoded::byte_serialize(lint.file().replace('\\', "/").as_bytes()) + .collect::(), + line = lint.line(), + ); + } + + output +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::generate_all::Mode; + + use super::{Args, main}; + + #[test] + fn ty_rules_up_to_date() -> Result<()> { + main(&Args { mode: Mode::Check }) + } +} diff --git a/crates/ruff_dev/src/generate_ty_schema.rs b/crates/ruff_dev/src/generate_ty_schema.rs new file mode 100644 index 0000000000000..eac09963b36ea --- /dev/null +++ b/crates/ruff_dev/src/generate_ty_schema.rs @@ -0,0 +1,70 @@ +use std::fs; +use std::path::PathBuf; + +use anyhow::{Result, bail}; +use pretty_assertions::StrComparison; +use schemars::schema_for; + +use crate::ROOT_DIR; +use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; +use ty_project::metadata::options::Options; + +#[derive(clap::Args)] +pub(crate) struct Args { + /// Write the generated table to stdout (rather than to `ty.schema.json`). + #[arg(long, default_value_t, value_enum)] + pub(crate) mode: Mode, +} + +pub(crate) fn main(args: &Args) -> Result<()> { + let schema = schema_for!(Options); + let schema_string = serde_json::to_string_pretty(&schema).unwrap(); + let filename = "ty.schema.json"; + let schema_path = PathBuf::from(ROOT_DIR).join(filename); + + match args.mode { + Mode::DryRun => { + println!("{schema_string}"); + } + Mode::Check => { + let current = fs::read_to_string(schema_path)?; + if current == schema_string { + println!("Up-to-date: {filename}"); + } else { + let comparison = StrComparison::new(¤t, &schema_string); + bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}"); + } + } + Mode::Write => { + let current = fs::read_to_string(&schema_path)?; + if current == schema_string { + println!("Up-to-date: {filename}"); + } else { + println!("Updating: {filename}"); + fs::write(schema_path, schema_string.as_bytes())?; + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use std::env; + + use crate::generate_all::Mode; + + use super::{Args, main}; + + #[test] + fn test_generate_json_schema() -> Result<()> { + let mode = if env::var("TY_UPDATE_SCHEMA").as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) + } +} diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index 5fb10476da47b..93aac0ec93be8 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -2,6 +2,8 @@ //! //! Within the ruff repository you can run it with `cargo dev`. +#![allow(clippy::print_stdout, clippy::print_stderr)] + use anyhow::Result; use clap::{Parser, Subcommand}; use ruff::{args::GlobalConfigArgs, check}; @@ -13,9 +15,13 @@ mod generate_all; mod generate_cli_help; mod generate_docs; mod generate_json_schema; -mod generate_knot_schema; mod generate_options; mod generate_rules_table; +mod generate_ty_cli_reference; +mod generate_ty_env_vars_reference; +mod generate_ty_options; +mod generate_ty_rules; +mod generate_ty_schema; mod print_ast; mod print_cst; mod print_tokens; @@ -34,18 +40,22 @@ struct Args { } #[derive(Subcommand)] -#[allow(clippy::large_enum_variant)] +#[expect(clippy::large_enum_variant)] enum Command { /// Run all code and documentation generation steps. GenerateAll(generate_all::Args), /// Generate JSON schema for the TOML configuration file. GenerateJSONSchema(generate_json_schema::Args), - /// Generate JSON schema for the Red Knot TOML configuration file. - GenerateKnotSchema(generate_knot_schema::Args), + /// Generate JSON schema for the ty TOML configuration file. + GenerateTySchema(generate_ty_schema::Args), /// Generate a Markdown-compatible table of supported lint rules. GenerateRulesTable, + GenerateTyRules(generate_ty_rules::Args), /// Generate a Markdown-compatible listing of configuration options. GenerateOptions, + GenerateTyOptions(generate_ty_options::Args), + /// Generate environment variables reference for ty. + GenerateTyEnvVarsReference(generate_ty_env_vars_reference::Args), /// Generate CLI help. GenerateCliHelp(generate_cli_help::Args), /// Generate Markdown docs. @@ -82,13 +92,16 @@ fn main() -> Result { command, global_options, } = Args::parse(); - #[allow(clippy::print_stdout)] + #[expect(clippy::print_stdout)] match command { Command::GenerateAll(args) => generate_all::main(&args)?, Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, - Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?, + Command::GenerateTySchema(args) => generate_ty_schema::main(&args)?, Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), + Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?, Command::GenerateOptions => println!("{}", generate_options::generate()), + Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?, + Command::GenerateTyEnvVarsReference(args) => generate_ty_env_vars_reference::main(&args)?, Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?, Command::GenerateDocs(args) => generate_docs::main(&args)?, Command::PrintAST(args) => print_ast::main(&args)?, diff --git a/crates/ruff_dev/src/print_ast.rs b/crates/ruff_dev/src/print_ast.rs index b3682e84f60a0..7dc1c4e80185d 100644 --- a/crates/ruff_dev/src/print_ast.rs +++ b/crates/ruff_dev/src/print_ast.rs @@ -1,5 +1,4 @@ //! Print the AST for a given Python file. -#![allow(clippy::print_stdout, clippy::print_stderr)] use std::path::PathBuf; @@ -7,7 +6,7 @@ use anyhow::Result; use ruff_linter::source_kind::SourceKind; use ruff_python_ast::PySourceType; -use ruff_python_parser::{parse, ParseOptions}; +use ruff_python_parser::{ParseOptions, parse}; #[derive(clap::Args)] pub(crate) struct Args { diff --git a/crates/ruff_dev/src/print_cst.rs b/crates/ruff_dev/src/print_cst.rs index 166923486e149..e85c4b447567a 100644 --- a/crates/ruff_dev/src/print_cst.rs +++ b/crates/ruff_dev/src/print_cst.rs @@ -1,10 +1,9 @@ //! Print the `LibCST` CST for a given Python file. -#![allow(clippy::print_stdout, clippy::print_stderr)] use std::fs; use std::path::PathBuf; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; #[derive(clap::Args)] pub(crate) struct Args { diff --git a/crates/ruff_dev/src/print_tokens.rs b/crates/ruff_dev/src/print_tokens.rs index 2c83affbb5f39..73c7844ccebe2 100644 --- a/crates/ruff_dev/src/print_tokens.rs +++ b/crates/ruff_dev/src/print_tokens.rs @@ -1,5 +1,4 @@ //! Print the token stream for a given Python file. -#![allow(clippy::print_stdout, clippy::print_stderr)] use std::path::PathBuf; diff --git a/crates/ruff_dev/src/round_trip.rs b/crates/ruff_dev/src/round_trip.rs index 6a070c65c1836..7501790b509bc 100644 --- a/crates/ruff_dev/src/round_trip.rs +++ b/crates/ruff_dev/src/round_trip.rs @@ -1,5 +1,4 @@ //! Run round-trip source code generation on a given Python or Jupyter notebook file. -#![allow(clippy::print_stdout, clippy::print_stderr)] use std::fs; use std::path::PathBuf; diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 3bc24b1dbcfc5..810364a8441dc 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -16,7 +16,6 @@ doctest = false [dependencies] ruff_text_size = { workspace = true } -anyhow = { workspace = true } -log = { workspace = true } +get-size2 = { workspace = true } is-macro = { workspace = true } serde = { workspace = true, optional = true, features = [] } diff --git a/crates/ruff_diagnostics/src/diagnostic.rs b/crates/ruff_diagnostics/src/diagnostic.rs deleted file mode 100644 index be54a8de0e247..0000000000000 --- a/crates/ruff_diagnostics/src/diagnostic.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use log::debug; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use ruff_text_size::{Ranged, TextRange, TextSize}; - -use crate::Fix; - -#[derive(Debug, PartialEq, Eq, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct DiagnosticKind { - /// The identifier of the diagnostic, used to align the diagnostic with a rule. - pub name: String, - /// The message body to display to the user, to explain the diagnostic. - pub body: String, - /// The message to display to the user, to explain the suggested fix. - pub suggestion: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Diagnostic { - pub kind: DiagnosticKind, - pub range: TextRange, - pub fix: Option, - pub parent: Option, -} - -impl Diagnostic { - pub fn new>(kind: T, range: TextRange) -> Self { - Self { - kind: kind.into(), - range, - fix: None, - parent: None, - } - } - - /// Consumes `self` and returns a new `Diagnostic` with the given `fix`. - #[inline] - #[must_use] - pub fn with_fix(mut self, fix: Fix) -> Self { - self.set_fix(fix); - self - } - - /// Set the [`Fix`] used to fix the diagnostic. - #[inline] - pub fn set_fix(&mut self, fix: Fix) { - self.fix = Some(fix); - } - - /// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`. - /// Otherwise, log the error. - #[inline] - pub fn try_set_fix(&mut self, func: impl FnOnce() -> Result) { - match func() { - Ok(fix) => self.fix = Some(fix), - Err(err) => debug!("Failed to create fix for {}: {}", self.kind.name, err), - } - } - - /// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`. - /// Otherwise, log the error. - #[inline] - pub fn try_set_optional_fix(&mut self, func: impl FnOnce() -> Result>) { - match func() { - Ok(None) => {} - Ok(Some(fix)) => self.fix = Some(fix), - Err(err) => debug!("Failed to create fix for {}: {}", self.kind.name, err), - } - } - - /// Consumes `self` and returns a new `Diagnostic` with the given parent node. - #[inline] - #[must_use] - pub fn with_parent(mut self, parent: TextSize) -> Self { - self.set_parent(parent); - self - } - - /// Set the location of the diagnostic's parent node. - #[inline] - pub fn set_parent(&mut self, parent: TextSize) { - self.parent = Some(parent); - } -} - -impl Ranged for Diagnostic { - fn range(&self) -> TextRange { - self.range - } -} diff --git a/crates/ruff_diagnostics/src/edit.rs b/crates/ruff_diagnostics/src/edit.rs index 1195fdc540ed9..194b4e44945f4 100644 --- a/crates/ruff_diagnostics/src/edit.rs +++ b/crates/ruff_diagnostics/src/edit.rs @@ -7,7 +7,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; /// A text edit to be applied to a source file. Inserts, deletes, or replaces /// content at a given location. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Edit { /// The start location of the edit. diff --git a/crates/ruff_diagnostics/src/fix.rs b/crates/ruff_diagnostics/src/fix.rs index 2db9d799fe35b..2fbc15225344e 100644 --- a/crates/ruff_diagnostics/src/fix.rs +++ b/crates/ruff_diagnostics/src/fix.rs @@ -6,7 +6,9 @@ use ruff_text_size::{Ranged, TextSize}; use crate::edit::Edit; /// Indicates if a fix can be applied. -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, is_macro::Is)] +#[derive( + Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, is_macro::Is, get_size2::GetSize, +)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum Applicability { @@ -30,7 +32,7 @@ pub enum Applicability { } /// Indicates the level of isolation required to apply a fix. -#[derive(Default, Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Default, Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, get_size2::GetSize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum IsolationLevel { /// The fix should be applied as long as no other fixes in the same group have been applied. @@ -41,7 +43,7 @@ pub enum IsolationLevel { } /// A collection of [`Edit`] elements to be applied to a source file. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Fix { /// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order. diff --git a/crates/ruff_diagnostics/src/lib.rs b/crates/ruff_diagnostics/src/lib.rs index 75a9bec240c3b..0fd267c5d799b 100644 --- a/crates/ruff_diagnostics/src/lib.rs +++ b/crates/ruff_diagnostics/src/lib.rs @@ -1,11 +1,7 @@ -pub use diagnostic::{Diagnostic, DiagnosticKind}; pub use edit::Edit; pub use fix::{Applicability, Fix, IsolationLevel}; pub use source_map::{SourceMap, SourceMarker}; -pub use violation::{AlwaysFixableViolation, FixAvailability, Violation, ViolationMetadata}; -mod diagnostic; mod edit; mod fix; mod source_map; -mod violation; diff --git a/crates/ruff_formatter/src/arguments.rs b/crates/ruff_formatter/src/arguments.rs index d26306c4ea502..6beb19c534844 100644 --- a/crates/ruff_formatter/src/arguments.rs +++ b/crates/ruff_formatter/src/arguments.rs @@ -17,7 +17,7 @@ impl<'fmt, Context> Argument<'fmt, Context> { /// Called by the [ruff_formatter::format_args] macro. #[doc(hidden)] #[inline] - pub fn new>(value: &'fmt F) -> Self { + pub const fn new>(value: &'fmt F) -> Self { Self { value } } @@ -55,7 +55,7 @@ pub struct Arguments<'fmt, Context>(pub &'fmt [Argument<'fmt, Context>]); impl<'fmt, Context> Arguments<'fmt, Context> { #[doc(hidden)] #[inline] - pub fn new(arguments: &'fmt [Argument<'fmt, Context>]) -> Self { + pub const fn new(arguments: &'fmt [Argument<'fmt, Context>]) -> Self { Self(arguments) } @@ -98,7 +98,7 @@ impl<'fmt, Context> From<&'fmt Argument<'fmt, Context>> for Arguments<'fmt, Cont mod tests { use crate::format_element::tag::Tag; use crate::prelude::*; - use crate::{format_args, write, FormatState, VecBuffer}; + use crate::{FormatState, VecBuffer, format_args, write}; #[test] fn test_nesting() { diff --git a/crates/ruff_formatter/src/buffer.rs b/crates/ruff_formatter/src/buffer.rs index 12d24c7294122..90822020a3124 100644 --- a/crates/ruff_formatter/src/buffer.rs +++ b/crates/ruff_formatter/src/buffer.rs @@ -1,4 +1,4 @@ -use super::{write, Arguments, FormatElement}; +use super::{Arguments, FormatElement, write}; use crate::format_element::Interned; use crate::prelude::{LineMode, Tag}; use crate::{FormatResult, FormatState}; diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 21ab988b5e38e..48e29795cc933 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2,14 +2,14 @@ use std::cell::Cell; use std::marker::PhantomData; use std::num::NonZeroU8; -use ruff_text_size::TextRange; #[allow(clippy::enum_glob_use)] use Tag::*; +use ruff_text_size::TextRange; use crate::format_element::tag::{Condition, Tag}; use crate::prelude::tag::{DedentMode, GroupMode, LabelId}; use crate::prelude::*; -use crate::{write, Argument, Arguments, FormatContext, FormatOptions, GroupId, TextSize}; +use crate::{Argument, Arguments, FormatContext, FormatOptions, GroupId, TextSize, write}; use crate::{Buffer, VecBuffer}; /// A line break that only gets printed if the enclosing `Group` doesn't fit on a single line. @@ -402,7 +402,10 @@ where } fn debug_assert_no_newlines(text: &str) { - debug_assert!(!text.contains('\r'), "The content '{text}' contains an unsupported '\\r' line terminator character but text must only use line feeds '\\n' as line separator. Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings."); + debug_assert!( + !text.contains('\r'), + "The content '{text}' contains an unsupported '\\r' line terminator character but text must only use line feeds '\\n' as line separator. Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings." + ); } /// Pushes some content to the end of the current line. @@ -1388,7 +1391,7 @@ pub fn soft_space_or_block_indent(content: &impl Format) -> Bl pub fn group(content: &impl Format) -> Group { Group { content: Argument::new(content), - group_id: None, + id: None, should_expand: false, } } @@ -1396,14 +1399,14 @@ pub fn group(content: &impl Format) -> Group { #[derive(Copy, Clone)] pub struct Group<'a, Context> { content: Argument<'a, Context>, - group_id: Option, + id: Option, should_expand: bool, } impl Group<'_, Context> { #[must_use] - pub fn with_group_id(mut self, group_id: Option) -> Self { - self.group_id = group_id; + pub fn with_id(mut self, group_id: Option) -> Self { + self.id = group_id; self } @@ -1429,7 +1432,7 @@ impl Format for Group<'_, Context> { }; f.write_element(FormatElement::Tag(StartGroup( - tag::Group::new().with_id(self.group_id).with_mode(mode), + tag::Group::new().with_id(self.id).with_mode(mode), ))); Arguments::from(&self.content).fmt(f)?; @@ -1443,7 +1446,7 @@ impl Format for Group<'_, Context> { impl std::fmt::Debug for Group<'_, Context> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Group") - .field("group_id", &self.group_id) + .field("id", &self.id) .field("should_expand", &self.should_expand) .field("content", &"{{content}}") .finish() @@ -1642,7 +1645,7 @@ impl std::fmt::Debug for BestFitParenthesize<'_, Context> { /// soft_line_break(), /// if_group_breaks(&token(")")) /// ]) -/// .with_group_id(Some(parentheses_id)) +/// .with_id(Some(parentheses_id)) /// .fmt(f) /// }); /// @@ -1991,7 +1994,7 @@ impl IfGroupBreaks<'_, Context> { /// })), /// token("]") /// ], - /// ).with_group_id(Some(group_id)) + /// ).with_id(Some(group_id)) /// ]) /// })])?; /// @@ -2046,7 +2049,7 @@ impl std::fmt::Debug for IfGroupBreaks<'_, Context> { /// let id = f.group_id("head"); /// /// write!(f, [ -/// group(&token("Head")).with_group_id(Some(id)), +/// group(&token("Head")).with_id(Some(id)), /// if_group_breaks(&indent(&token("indented"))).with_group_id(Some(id)), /// if_group_fits_on_line(&token("indented")).with_group_id(Some(id)) /// ]) @@ -2071,7 +2074,7 @@ impl std::fmt::Debug for IfGroupBreaks<'_, Context> { /// let group_id = f.group_id("header"); /// /// write!(f, [ -/// group(&token("(aLongHeaderThatBreaksForSomeReason) =>")).with_group_id(Some(group_id)), +/// group(&token("(aLongHeaderThatBreaksForSomeReason) =>")).with_id(Some(group_id)), /// indent_if_group_breaks(&format_args![hard_line_break(), token("a => b")], group_id) /// ]) /// }); @@ -2101,7 +2104,7 @@ impl std::fmt::Debug for IfGroupBreaks<'_, Context> { /// let group_id = f.group_id("header"); /// /// write!(f, [ -/// group(&token("(aLongHeaderThatBreaksForSomeReason) =>")).with_group_id(Some(group_id)), +/// group(&token("(aLongHeaderThatBreaksForSomeReason) =>")).with_id(Some(group_id)), /// indent_if_group_breaks(&format_args![hard_line_break(), token("a => b")], group_id) /// ]) /// }); @@ -2564,7 +2567,7 @@ impl<'a, Context> BestFitting<'a, Context> { /// # Panics /// /// When the slice contains less than two variants. - pub fn from_arguments_unchecked(variants: Arguments<'a, Context>) -> Self { + pub const fn from_arguments_unchecked(variants: Arguments<'a, Context>) -> Self { assert!( variants.0.len() >= 2, "Requires at least the least expanded and most expanded variants" @@ -2572,7 +2575,7 @@ impl<'a, Context> BestFitting<'a, Context> { Self { variants, - mode: BestFittingMode::default(), + mode: BestFittingMode::FirstLine, } } diff --git a/crates/ruff_formatter/src/diagnostics.rs b/crates/ruff_formatter/src/diagnostics.rs index 00eae242e889d..eb2f1435b3af9 100644 --- a/crates/ruff_formatter/src/diagnostics.rs +++ b/crates/ruff_formatter/src/diagnostics.rs @@ -1,5 +1,5 @@ -use crate::prelude::TagKind; use crate::GroupId; +use crate::prelude::TagKind; use ruff_text_size::TextRange; use std::error::Error; @@ -29,16 +29,22 @@ pub enum FormatError { impl std::fmt::Display for FormatError { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - FormatError::SyntaxError {message} => { + FormatError::SyntaxError { message } => { std::write!(fmt, "syntax error: {message}") - }, + } FormatError::RangeError { input, tree } => std::write!( fmt, "formatting range {input:?} is larger than syntax tree {tree:?}" ), - FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please report if necessary."), + FormatError::InvalidDocument(error) => std::write!( + fmt, + "Invalid document: {error}\n\n This is an internal Rome error. Please report if necessary." + ), FormatError::PoorLayout => { - std::write!(fmt, "Poor layout: The formatter wasn't able to pick a good layout for your document. This is an internal Rome error. Please report if necessary.") + std::write!( + fmt, + "Poor layout: The formatter wasn't able to pick a good layout for your document. This is an internal Rome error. Please report if necessary." + ) } } } @@ -139,24 +145,37 @@ impl std::fmt::Display for InvalidDocumentError { InvalidDocumentError::ExpectedStart { expected_start, actual, - } => { - match actual { - ActualStart::EndOfDocument => { - std::write!(f, "Expected start tag of kind {expected_start:?} but at the end of document.") - } - ActualStart::Start(start) => { - std::write!(f, "Expected start tag of kind {expected_start:?} but found start tag of kind {start:?}.") - } - ActualStart::End(end) => { - std::write!(f, "Expected start tag of kind {expected_start:?} but found end tag of kind {end:?}.") - } - ActualStart::Content => { - std::write!(f, "Expected start tag of kind {expected_start:?} but found non-tag element.") - } + } => match actual { + ActualStart::EndOfDocument => { + std::write!( + f, + "Expected start tag of kind {expected_start:?} but at the end of document." + ) } - } + ActualStart::Start(start) => { + std::write!( + f, + "Expected start tag of kind {expected_start:?} but found start tag of kind {start:?}." + ) + } + ActualStart::End(end) => { + std::write!( + f, + "Expected start tag of kind {expected_start:?} but found end tag of kind {end:?}." + ) + } + ActualStart::Content => { + std::write!( + f, + "Expected start tag of kind {expected_start:?} but found non-tag element." + ) + } + }, InvalidDocumentError::UnknownGroupId { group_id } => { - std::write!(f, "Encountered unknown group id {group_id:?}. Ensure that the group with the id {group_id:?} exists and that the group is a parent of or comes before the element referring to it.") + std::write!( + f, + "Encountered unknown group id {group_id:?}. Ensure that the group with the id {group_id:?} exists and that the group is a parent of or comes before the element referring to it." + ) } } } diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index 8623f9850f487..1d2c502b63acd 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -519,7 +519,7 @@ impl TextWidth { let char_width = match c { '\t' => indent_width.value(), '\n' => return TextWidth::Multiline, - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] c => c.width().unwrap_or(0) as u32, }; width += char_width; @@ -543,7 +543,7 @@ impl TextWidth { #[cfg(test)] mod tests { - use crate::format_element::{normalize_newlines, LINE_TERMINATORS}; + use crate::format_element::{LINE_TERMINATORS, normalize_newlines}; #[test] fn test_normalize_newlines() { diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 8236d92ee0ead..2f19f13c0fa09 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -8,8 +8,8 @@ use crate::prelude::tag::GroupMode; use crate::prelude::*; use crate::source_code::SourceCode; use crate::{ - format, write, BufferExtensions, Format, FormatContext, FormatElement, FormatOptions, - FormatResult, Formatter, IndentStyle, IndentWidth, LineWidth, PrinterOptions, + BufferExtensions, Format, FormatContext, FormatElement, FormatOptions, FormatResult, Formatter, + IndentStyle, IndentWidth, LineWidth, PrinterOptions, format, write, }; use super::tag::Tag; @@ -280,7 +280,7 @@ impl Format> for &[FormatElement] { | FormatElement::SourceCodeSlice { .. }) => { fn write_escaped(element: &FormatElement, f: &mut Formatter) { let (text, text_width) = match element { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] FormatElement::Token { text } => { (*text, TextWidth::Width(Width::new(text.len() as u32))) } @@ -811,8 +811,8 @@ mod tests { use ruff_text_size::{TextRange, TextSize}; use crate::prelude::*; - use crate::{format, format_args, write}; use crate::{SimpleFormatContext, SourceCode}; + use crate::{format, format_args, write}; #[test] fn display_elements() { diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index bb3b7854641f9..606922648241d 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -370,7 +370,10 @@ impl PartialEq for LabelId { #[cfg(debug_assertions)] { if is_equal { - assert_eq!(self.name, other.name, "Two `LabelId`s with different names have the same `value`. Are you mixing labels of two different `LabelDefinition` or are the values returned by the `LabelDefinition` not unique?"); + assert_eq!( + self.name, other.name, + "Two `LabelId`s with different names have the same `value`. Are you mixing labels of two different `LabelDefinition` or are the values returned by the `LabelDefinition` not unique?" + ); } } @@ -379,7 +382,7 @@ impl PartialEq for LabelId { } impl LabelId { - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn of(label: T) -> Self { Self { value: label.value(), diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index f27ee19bec64a..133b92108bb8b 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -38,7 +38,7 @@ use crate::prelude::TagKind; use std::fmt; use std::fmt::{Debug, Display}; use std::marker::PhantomData; -use std::num::{NonZeroU16, NonZeroU8, TryFromIntError}; +use std::num::{NonZeroU8, NonZeroU16, TryFromIntError}; use crate::format_element::document::Document; use crate::printer::{Printer, PrinterOptions}; @@ -50,7 +50,7 @@ pub use builders::BestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; -pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS}; +pub use format_element::{FormatElement, LINE_TERMINATORS, normalize_newlines}; pub use group_id::GroupId; use ruff_macros::CacheKey; use ruff_text_size::{TextLen, TextRange, TextSize}; @@ -92,7 +92,7 @@ impl std::fmt::Display for IndentStyle { } } -/// The visual width of a indentation. +/// The visual width of an indentation. /// /// Determines the visual width of a tab character (`\t`) and the number of /// spaces per indent when using [`IndentStyle::Space`]. @@ -207,7 +207,7 @@ pub trait FormatOptions { /// What's the max width of a line. Defaults to 80. fn line_width(&self) -> LineWidth; - /// Derives the print options from the these format options + /// Derives the print options from these format options fn as_print_options(&self) -> PrinterOptions; } @@ -925,7 +925,7 @@ pub struct FormatState { group_id_builder: UniqueGroupIdBuilder, } -#[allow(clippy::missing_fields_in_debug)] +#[expect(clippy::missing_fields_in_debug)] impl std::fmt::Debug for FormatState where Context: std::fmt::Debug, diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index 2e327739a9091..4d0d3ef234c80 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -328,16 +328,16 @@ macro_rules! format { /// [`MostExpanded`]: crate::format_element::BestFittingVariants::most_expanded #[macro_export] macro_rules! best_fitting { - ($least_expanded:expr, $($tail:expr),+ $(,)?) => {{ + ($least_expanded:expr, $($tail:expr),+ $(,)?) => { // OK because the macro syntax requires at least two variants. $crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) - }} + } } #[cfg(test)] mod tests { use crate::prelude::*; - use crate::{write, FormatState, SimpleFormatOptions, VecBuffer}; + use crate::{FormatState, SimpleFormatOptions, VecBuffer, write}; struct TestFormat; @@ -386,7 +386,7 @@ mod tests { #[test] fn best_fitting_variants_print_as_lists() { use crate::prelude::*; - use crate::{format, format_args, Formatted}; + use crate::{Formatted, format, format_args}; // The second variant below should be selected when printing at a width of 30 let formatted_best_fitting = format!( @@ -398,34 +398,36 @@ mod tests { format_args![token( "Something that will not fit on a line with 30 character print width." )], - format_args![group(&format_args![ - token("Start"), - soft_line_break(), - group(&soft_block_indent(&format_args![ - token("1,"), - soft_line_break_or_space(), - token("2,"), - soft_line_break_or_space(), - token("3"), - ])), - soft_line_break_or_space(), - soft_block_indent(&format_args![ - token("1,"), - soft_line_break_or_space(), - token("2,"), + format_args![ + group(&format_args![ + token("Start"), + soft_line_break(), + group(&soft_block_indent(&format_args![ + token("1,"), + soft_line_break_or_space(), + token("2,"), + soft_line_break_or_space(), + token("3"), + ])), soft_line_break_or_space(), - group(&format_args!( - token("A,"), + soft_block_indent(&format_args![ + token("1,"), + soft_line_break_or_space(), + token("2,"), soft_line_break_or_space(), - token("B") - )), + group(&format_args!( + token("A,"), + soft_line_break_or_space(), + token("B") + )), + soft_line_break_or_space(), + token("3") + ]), soft_line_break_or_space(), - token("3") - ]), - soft_line_break_or_space(), - token("End") - ]) - .should_expand(true)], + token("End") + ]) + .should_expand(true) + ], format_args!(token("Most"), hard_line_break(), token("Expanded")) ] ] diff --git a/crates/ruff_formatter/src/prelude.rs b/crates/ruff_formatter/src/prelude.rs index fb8b58d6e3930..f12d7c8020b96 100644 --- a/crates/ruff_formatter/src/prelude.rs +++ b/crates/ruff_formatter/src/prelude.rs @@ -7,6 +7,6 @@ pub use crate::formatter::Formatter; pub use crate::printer::PrinterOptions; pub use crate::{ - best_fitting, dbg_write, format, format_args, write, Buffer as _, BufferExtensions, Format, - Format as _, FormatResult, FormatRule, FormatWithRule as _, SimpleFormatContext, + Buffer as _, BufferExtensions, Format, Format as _, FormatResult, FormatRule, + FormatWithRule as _, SimpleFormatContext, best_fitting, dbg_write, format, format_args, write, }; diff --git a/crates/ruff_formatter/src/printer/call_stack.rs b/crates/ruff_formatter/src/printer/call_stack.rs index aa5c73dfda597..7c57ff4b3efde 100644 --- a/crates/ruff_formatter/src/printer/call_stack.rs +++ b/crates/ruff_formatter/src/printer/call_stack.rs @@ -1,5 +1,5 @@ -use crate::format_element::tag::TagKind; use crate::format_element::PrintMode; +use crate::format_element::tag::TagKind; use crate::printer::stack::{Stack, StackedStack}; use crate::printer::{Indentation, MeasureMode}; use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintResult}; diff --git a/crates/ruff_formatter/src/printer/line_suffixes.rs b/crates/ruff_formatter/src/printer/line_suffixes.rs index 309499d9a7866..3aa2591824850 100644 --- a/crates/ruff_formatter/src/printer/line_suffixes.rs +++ b/crates/ruff_formatter/src/printer/line_suffixes.rs @@ -1,5 +1,5 @@ -use crate::printer::call_stack::PrintElementArgs; use crate::FormatElement; +use crate::printer::call_stack::PrintElementArgs; /// Stores the queued line suffixes. #[derive(Debug, Default)] diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index efdef79be46fc..2b52841beb0a5 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -10,7 +10,7 @@ use crate::format_element::document::Document; use crate::format_element::tag::{Condition, GroupMode}; use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode}; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; -use crate::prelude::{tag, TextWidth}; +use crate::prelude::{TextWidth, tag}; use crate::printer::call_stack::{ CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame, }; @@ -331,7 +331,7 @@ impl<'a> Printer<'a> { FormatElement::Tag(StartVerbatim(kind)) => { if let VerbatimKind::Verbatim { length } = kind { // SAFETY: Ruff only supports formatting files <= 4GB - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] self.state.verbatim_markers.push(TextRange::at( TextSize::from(self.state.buffer.len() as u32), *length, @@ -464,7 +464,7 @@ impl<'a> Printer<'a> { self.push_marker(); match text { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Text::Token(token) => { self.state.buffer.push_str(token); self.state.line_width += token.len() as u32; @@ -831,7 +831,7 @@ impl<'a> Printer<'a> { } else { self.state.buffer.push(char); - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] let char_width = if char == '\t' { self.options.indent_width.value() } else { @@ -1199,7 +1199,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { text_width: *text_width, }, args, - )) + )); } FormatElement::SourceCodeSlice { slice, text_width } => { let text = slice.text(self.printer.source_code); @@ -1480,7 +1480,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align()); match text { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Text::Token(token) => { self.state.line_width += token.len() as u32; } @@ -1511,7 +1511,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } } // SAFETY: A u32 is sufficient to format files <= 4GB - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] c => c.width().unwrap_or(0) as u32, }; self.state.line_width += char_width; @@ -1597,11 +1597,7 @@ enum Fits { impl From for Fits { fn from(value: bool) -> Self { - if value { - Fits::Yes - } else { - Fits::No - } + if value { Fits::Yes } else { Fits::No } } } @@ -1662,8 +1658,8 @@ mod tests { use crate::printer::{LineEnding, Printer, PrinterOptions}; use crate::source_code::SourceCode; use crate::{ - format_args, write, Document, FormatState, IndentStyle, IndentWidth, LineWidth, Printed, - VecBuffer, + Document, FormatState, IndentStyle, IndentWidth, LineWidth, Printed, VecBuffer, + format_args, write, }; fn format(root: &dyn Format) -> Printed { @@ -1985,10 +1981,21 @@ two lines`, token("]") ]), token(";"), - line_suffix(&format_args![space(), token("// Using reserved width causes this content to not fit even though it's a line suffix element")], 93) + line_suffix( + &format_args![ + space(), + token( + "// Using reserved width causes this content to not fit even though it's a line suffix element" + ) + ], + 93 + ) ]); - assert_eq!(printed.as_code(), "[\n 1, 2, 3\n]; // Using reserved width causes this content to not fit even though it's a line suffix element"); + assert_eq!( + printed.as_code(), + "[\n 1, 2, 3\n]; // Using reserved width causes this content to not fit even though it's a line suffix element" + ); } #[test] @@ -2002,7 +2009,7 @@ two lines`, token("The referenced group breaks."), hard_line_break() ]) - .with_group_id(Some(group_id)), + .with_id(Some(group_id)), group(&format_args![ token("This group breaks because:"), soft_line_break_or_space(), @@ -2015,7 +2022,10 @@ two lines`, let printed = format(&content); - assert_eq!(printed.as_code(), "The referenced group breaks.\nThis group breaks because:\nIt measures with the 'if_group_breaks' variant because the referenced group breaks and that's just way too much text."); + assert_eq!( + printed.as_code(), + "The referenced group breaks.\nThis group breaks because:\nIt measures with the 'if_group_breaks' variant because the referenced group breaks and that's just way too much text." + ); } #[test] @@ -2027,7 +2037,7 @@ two lines`, write!( f, [ - group(&token("Group with id-2")).with_group_id(Some(id_2)), + group(&token("Group with id-2")).with_id(Some(id_2)), hard_line_break() ] )?; @@ -2035,7 +2045,7 @@ two lines`, write!( f, [ - group(&token("Group with id-1 does not fit on the line because it exceeds the line width of 80 characters by")).with_group_id(Some(id_1)), + group(&token("Group with id-1 does not fit on the line because it exceeds the line width of 80 characters by")).with_id(Some(id_1)), hard_line_break() ] )?; diff --git a/crates/ruff_formatter/src/printer/queue.rs b/crates/ruff_formatter/src/printer/queue.rs index e7ae215409212..d68c742951895 100644 --- a/crates/ruff_formatter/src/printer/queue.rs +++ b/crates/ruff_formatter/src/printer/queue.rs @@ -325,10 +325,10 @@ impl FitsEndPredicate for SingleEntryPredicate { #[cfg(test)] mod tests { + use crate::FormatElement; use crate::format_element::LineMode; use crate::prelude::Tag; use crate::printer::queue::{PrintQueue, Queue}; - use crate::FormatElement; #[test] fn extend_back_pop_last() { diff --git a/crates/ruff_formatter/src/source_code.rs b/crates/ruff_formatter/src/source_code.rs index cc81875aadff9..790e87f4a11da 100644 --- a/crates/ruff_formatter/src/source_code.rs +++ b/crates/ruff_formatter/src/source_code.rs @@ -65,7 +65,10 @@ pub struct SourceCodeSlice { impl SourceCodeSlice { /// Returns the slice's text. pub fn text<'a>(&self, code: SourceCode<'a>) -> &'a str { - assert!(usize::from(self.range.end()) <= code.text.len(), "The range of this slice is out of bounds. Did you provide the correct source code for this slice?"); + assert!( + usize::from(self.range.end()) <= code.text.len(), + "The range of this slice is out of bounds. Did you provide the correct source code for this slice?" + ); &code.text[self.range] } } diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index 645c9cd2e4255..917a2f4e05c35 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -10,13 +10,13 @@ authors.workspace = true license.workspace = true [dependencies] -red_knot_python_semantic = { workspace = true } ruff_cache = { workspace = true } ruff_db = { workspace = true, features = ["os", "serde"] } ruff_linter = { workspace = true } ruff_macros = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_parser = { workspace = true } +ty_python_semantic = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, optional = true } diff --git a/crates/ruff_graph/src/collector.rs b/crates/ruff_graph/src/collector.rs index 7946b2c5a6ef1..73b37991deaab 100644 --- a/crates/ruff_graph/src/collector.rs +++ b/crates/ruff_graph/src/collector.rs @@ -1,8 +1,8 @@ -use red_knot_python_semantic::ModuleName; use ruff_python_ast::visitor::source_order::{ - walk_expr, walk_module, walk_stmt, SourceOrderVisitor, + SourceOrderVisitor, walk_expr, walk_module, walk_stmt, }; use ruff_python_ast::{self as ast, Expr, Mod, Stmt}; +use ty_python_semantic::ModuleName; /// Collect all imports for a given Python file. #[derive(Default, Debug)] @@ -39,6 +39,7 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> { module, level, range: _, + node_index: _, }) => { let module = module.as_deref(); let level = *level; @@ -78,7 +79,11 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> { } } } - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { for alias in names { if let Some(module_name) = ModuleName::new(alias.name.as_str()) { self.imports.push(CollectedImport::Import(module_name)); @@ -122,7 +127,12 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> { fn visit_expr(&mut self, expr: &'ast Expr) { if self.string_imports { - if let Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) = expr { + if let Expr::StringLiteral(ast::ExprStringLiteral { + value, + range: _, + node_index: _, + }) = expr + { // Determine whether the string literal "looks like" an import statement: contains // a dot, and consists solely of valid Python identifiers. let value = value.to_str(); diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index e768c6282a0ba..48ee62a348624 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -1,16 +1,17 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use std::sync::Arc; use zip::CompressionMethod; -use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; -use red_knot_python_semantic::{ - default_lint_registry, Db, Program, ProgramSettings, PythonPlatform, SearchPathSettings, -}; +use ruff_db::Db as SourceDb; use ruff_db::files::{File, Files}; use ruff_db::system::{OsSystem, System, SystemPathBuf}; use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder}; -use ruff_db::{Db as SourceDb, Upcast}; use ruff_python_ast::PythonVersion; +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::{ + Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource, + PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, default_lint_registry, +}; static EMPTY_VENDORED: std::sync::LazyLock = std::sync::LazyLock::new(|| { let mut builder = VendoredFileSystemBuilder::new(CompressionMethod::Stored); @@ -32,32 +33,39 @@ impl ModuleDb { pub fn from_src_roots( src_roots: Vec, python_version: PythonVersion, + venv_path: Option, ) -> Result { - let search_paths = SearchPathSettings::new(src_roots); - let db = Self::default(); + let mut search_paths = SearchPathSettings::new(src_roots); + // TODO: Consider calling `PythonEnvironment::discover` if the `venv_path` is not provided. + if let Some(venv_path) = venv_path { + let environment = + PythonEnvironment::new(venv_path, SysPrefixPathOrigin::PythonCliFlag, db.system())?; + search_paths.site_packages_paths = environment + .site_packages_paths(db.system()) + .context("Failed to discover the site-packages directory")? + .into_vec(); + } + let search_paths = search_paths + .to_search_paths(db.system(), db.vendored()) + .context("Invalid search path settings")?; + Program::from_settings( &db, ProgramSettings { - python_version, + python_version: PythonVersionWithSource { + version: python_version, + source: PythonVersionSource::default(), + }, python_platform: PythonPlatform::default(), search_paths, }, - )?; + ); Ok(db) } } -impl Upcast for ModuleDb { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } -} - #[salsa::db] impl SourceDb for ModuleDb { fn vendored(&self) -> &VendoredFileSystem { @@ -71,6 +79,10 @@ impl SourceDb for ModuleDb { fn files(&self) -> &Files { &self.files } + + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } } #[salsa::db] @@ -79,8 +91,8 @@ impl Db for ModuleDb { !file.path(self).is_vendored_path() } - fn rule_selection(&self) -> Arc { - self.rule_selection.clone() + fn rule_selection(&self, _file: File) -> &RuleSelection { + &self.rule_selection } fn lint_registry(&self) -> &LintRegistry { @@ -89,6 +101,4 @@ impl Db for ModuleDb { } #[salsa::db] -impl salsa::Database for ModuleDb { - fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {} -} +impl salsa::Database for ModuleDb {} diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index 327b07963471e..6d2435efcd848 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -4,7 +4,7 @@ use anyhow::Result; use ruff_db::system::{SystemPath, SystemPathBuf}; use ruff_python_ast::helpers::to_module_path; -use ruff_python_parser::{parse, Mode, ParseOptions}; +use ruff_python_parser::{Mode, ParseOptions, parse}; use crate::collector::Collector; pub use crate::db::ModuleDb; diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index f5eb04402092e..baf43c71c9eb5 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -1,8 +1,8 @@ -use red_knot_python_semantic::resolve_module; use ruff_db::files::FilePath; +use ty_python_semantic::resolve_module; -use crate::collector::CollectedImport; use crate::ModuleDb; +use crate::collector::CollectedImport; /// Collect all imports for a given Python file. pub(crate) struct Resolver<'a> { @@ -19,19 +19,20 @@ impl<'a> Resolver<'a> { pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> { match import { CollectedImport::Import(import) => { - resolve_module(self.db, &import).map(|module| module.file().path(self.db)) + let module = resolve_module(self.db, &import)?; + Some(module.file()?.path(self.db)) } CollectedImport::ImportFrom(import) => { // Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`). let parent = import.parent(); - resolve_module(self.db, &import) - .map(|module| module.file().path(self.db)) - .or_else(|| { - // Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`). + let module = resolve_module(self.db, &import).or_else(|| { + // Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`). + + resolve_module(self.db, &parent?) + })?; - resolve_module(self.db, &parent?).map(|module| module.file().path(self.db)) - }) + Some(module.file()?.path(self.db)) } } } diff --git a/crates/ruff_graph/src/settings.rs b/crates/ruff_graph/src/settings.rs index f7174c859272e..3e47b97ef340c 100644 --- a/crates/ruff_graph/src/settings.rs +++ b/crates/ruff_graph/src/settings.rs @@ -36,14 +36,20 @@ impl fmt::Display for AnalyzeSettings { } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum Direction { /// Construct a map from module to its dependencies (i.e., the modules that it imports). #[default] + #[cfg_attr(feature = "serde", serde(alias = "Dependencies"))] Dependencies, /// Construct a map from module to its dependents (i.e., the modules that import it). + #[cfg_attr(feature = "serde", serde(alias = "Dependents"))] Dependents, } diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index a96fa738f6452..df11c268cf1a5 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -14,6 +14,7 @@ license = { workspace = true } doctest = false [dependencies] +get-size2 = { workspace = true } ruff_macros = { workspace = true } salsa = { workspace = true, optional = true } diff --git a/crates/ruff_index/src/slice.rs b/crates/ruff_index/src/slice.rs index 9b3f9523f7a9c..75f52c2368af5 100644 --- a/crates/ruff_index/src/slice.rs +++ b/crates/ruff_index/src/slice.rs @@ -1,5 +1,5 @@ -use crate::vec::IndexVec; use crate::Idx; +use crate::vec::IndexVec; use std::fmt::{Debug, Formatter}; use std::marker::PhantomData; use std::ops::{Index, IndexMut, Range}; @@ -22,7 +22,7 @@ impl IndexSlice { pub const fn from_raw(raw: &[T]) -> &Self { let ptr: *const [T] = raw; - #[allow(unsafe_code)] + #[expect(unsafe_code)] // SAFETY: `IndexSlice` is `repr(transparent)` over a normal slice unsafe { &*(ptr as *const Self) @@ -33,7 +33,7 @@ impl IndexSlice { pub fn from_raw_mut(raw: &mut [T]) -> &mut Self { let ptr: *mut [T] = raw; - #[allow(unsafe_code)] + #[expect(unsafe_code)] // SAFETY: `IndexSlice` is `repr(transparent)` over a normal slice unsafe { &mut *(ptr as *mut Self) @@ -209,5 +209,5 @@ impl Default for &mut IndexSlice { // Whether `IndexSlice` is `Send` depends only on the data, // not the phantom data. -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl Send for IndexSlice where T: Send {} diff --git a/crates/ruff_index/src/vec.rs b/crates/ruff_index/src/vec.rs index 4e9bc479229af..648f9cb132cc4 100644 --- a/crates/ruff_index/src/vec.rs +++ b/crates/ruff_index/src/vec.rs @@ -1,12 +1,12 @@ -use crate::slice::IndexSlice; use crate::Idx; +use crate::slice::IndexSlice; use std::borrow::{Borrow, BorrowMut}; use std::fmt::{Debug, Formatter}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut, RangeBounds}; /// An owned sequence of `T` indexed by `I` -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, get_size2::GetSize)] #[repr(transparent)] pub struct IndexVec { pub raw: Vec, @@ -179,18 +179,18 @@ impl From<[T; N]> for IndexVec { // Whether `IndexVec` is `Send` depends only on the data, // not the phantom data. -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl Send for IndexVec where T: Send {} -#[allow(unsafe_code)] +#[expect(unsafe_code)] #[cfg(feature = "salsa")] unsafe impl salsa::Update for IndexVec where T: salsa::Update, { - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { let old_vec: &mut IndexVec = unsafe { &mut *old_pointer }; - salsa::Update::maybe_update(&mut old_vec.raw, new_value.raw) + unsafe { salsa::Update::maybe_update(&raw mut old_vec.raw, new_value.raw) } } } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 1cc1fbe68016a..e84a077910c0b 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.11.6" +version = "0.12.3" publish = false authors = { workspace = true } edition = { workspace = true } @@ -15,6 +15,7 @@ license = { workspace = true } [dependencies] ruff_annotate_snippets = { workspace = true } ruff_cache = { workspace = true } +ruff_db = { workspace = true, features = ["serde"] } ruff_diagnostics = { workspace = true, features = ["serde"] } ruff_notebook = { workspace = true } ruff_macros = { workspace = true } @@ -37,6 +38,7 @@ colored = { workspace = true } fern = { workspace = true } glob = { workspace = true } globset = { workspace = true } +hashbrown = { workspace = true } imperative = { workspace = true } is-macro = { workspace = true } is-wsl = { workspace = true } @@ -76,6 +78,7 @@ insta = { workspace = true, features = ["filters", "json", "redactions"] } test-case = { workspace = true } # Disable colored output in tests colored = { workspace = true, features = ["no-color"] } +tempfile = { workspace = true } [features] default = [] diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py index 17839131339ac..ce35d79338195 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_args.py @@ -5,6 +5,7 @@ from airflow import DAG, dag from airflow.operators.datetime import BranchDateTimeOperator from airflow.operators.trigger_dagrun import TriggerDagRunOperator +from airflow.operators.weekday import BranchDayOfWeekOperator from airflow.providers.amazon.aws.log.s3_task_handler import S3TaskHandler from airflow.providers.apache.hdfs.log.hdfs_task_handler import HdfsTaskHandler from airflow.providers.elasticsearch.log.es_task_handler import ElasticsearchTaskHandler @@ -12,7 +13,7 @@ from airflow.providers.google.cloud.log.gcs_task_handler import GCSTaskHandler from airflow.providers.standard.operators import datetime, trigger_dagrun from airflow.providers.standard.sensors import weekday -from airflow.sensors.weekday import BranchDayOfWeekOperator, DayOfWeekSensor +from airflow.sensors.weekday import DayOfWeekSensor from airflow.timetables.simple import NullTimetable DAG(dag_id="class_schedule", schedule="@hourly") @@ -50,10 +51,10 @@ def decorator_timetable(): @dag() def decorator_deprecated_operator_args(): trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( - task_id="trigger_dagrun_op1", execution_date="2024-12-04" + task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04" ) trigger_dagrun_op2 = TriggerDagRunOperator( - task_id="trigger_dagrun_op2", execution_date="2024-12-04" + task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04" ) branch_dt_op = datetime.BranchDateTimeOperator( @@ -66,16 +67,30 @@ def decorator_deprecated_operator_args(): ) dof_task_sensor = weekday.DayOfWeekSensor( - task_id="dof_task_sensor", use_task_execution_day=True + task_id="dof_task_sensor", + week_day=1, + use_task_execution_day=True, ) dof_task_sensor2 = DayOfWeekSensor( - task_id="dof_task_sensor2", use_task_execution_day=True + task_id="dof_task_sensor2", + week_day=1, + use_task_execution_day=True, ) bdow_op = weekday.BranchDayOfWeekOperator( - task_id="bdow_op", use_task_execution_day=True + task_id="bdow_op", + follow_task_ids_if_false=None, + follow_task_ids_if_true=None, + week_day=1, + use_task_execution_day=True, + ) + bdow_op2 = BranchDayOfWeekOperator( + task_id="bdow_op2", + follow_task_ids_if_false=None, + follow_task_ids_if_true=None, + week_day=1, + use_task_execution_day=True, ) - bdow_op2 = BranchDayOfWeekOperator(task_id="bdow_op2", use_task_execution_day=True) trigger_dagrun_op >> trigger_dagrun_op2 branch_dt_op >> branch_dt_op2 diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py index c12a39e9f1507..605ad5b07e52a 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_class_attribute.py @@ -10,7 +10,7 @@ ) from airflow.datasets.manager import DatasetManager from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector -from airflow.providers.amazon.auth_manager.aws_auth_manager import AwsAuthManager +from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError from airflow.providers.google.cloud.secrets.secret_manager import ( CloudSecretManagerBackend, @@ -83,8 +83,7 @@ # airflow.providers_manager pm = ProvidersManager() -pm.initialize_providers_asset_uri_resources() -pm.dataset_factories +pm.initialize_providers_dataset_uri_resources() pm.dataset_factories pm.dataset_uri_handlers pm.dataset_to_openlineage_converters diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py index bd9606b91819e..89335ae88b0c6 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py @@ -9,47 +9,11 @@ PY311, PY312, ) -from airflow.api_connexion.security import requires_access, requires_access_dataset -from airflow.auth.managers.base_auth_manager import is_authorized_dataset -from airflow.auth.managers.models.resource_details import DatasetDetails -from airflow.configuration import ( - as_dict, - get, - getboolean, - getfloat, - getint, - has_option, - remove_option, - set, -) +from airflow.api_connexion.security import requires_access from airflow.contrib.aws_athena_hook import AWSAthenaHook from airflow.datasets import DatasetAliasEvent -from airflow.datasets.manager import ( - DatasetManager, - dataset_manager, - resolve_dataset_manager, -) -from airflow.hooks.base_hook import BaseHook -from airflow.lineage.hook import DatasetLineageInfo -from airflow.listeners.spec.dataset import on_dataset_changed, on_dataset_created -from airflow.metrics.validators import AllowListValidator, BlockListValidator from airflow.operators.subdag import SubDagOperator -from airflow.providers.amazon.aws.auth_manager.avp.entities import AvpEntities -from airflow.providers.amazon.aws.datasets import s3 -from airflow.providers.common.io.datasets import file as common_io_file -from airflow.providers.fab.auth_manager import fab_auth_manager -from airflow.providers.google.datasets import bigquery, gcs -from airflow.providers.mysql.datasets import mysql -from airflow.providers.openlineage.utils.utils import ( - DatasetInfo, - translate_airflow_dataset, -) -from airflow.providers.postgres.datasets import postgres -from airflow.providers.trino.datasets import trino -from airflow.secrets.local_filesystem import LocalFilesystemBackend, load_connections -from airflow.security.permissions import RESOURCE_DATASET -from airflow.sensors.base_sensor_operator import BaseSensorOperator -from airflow.timetables.simple import DatasetTriggeredTimetable +from airflow.secrets.local_filesystem import LocalFilesystemBackend from airflow.triggers.external_task import TaskStateTrigger from airflow.utils import dates from airflow.utils.dag_cycle_tester import test_cycle @@ -64,10 +28,7 @@ ) from airflow.utils.db import create_session from airflow.utils.decorators import apply_defaults -from airflow.utils.file import TemporaryDirectory, mkdirs -from airflow.utils.helpers import chain as helper_chain -from airflow.utils.helpers import cross_downstream as helper_cross_downstream -from airflow.utils.log import secrets_masker +from airflow.utils.file import mkdirs from airflow.utils.state import SHUTDOWN, terminating_states from airflow.utils.trigger_rule import TriggerRule from airflow.www.auth import has_access, has_access_dataset @@ -75,18 +36,9 @@ # airflow root PY36, PY37, PY38, PY39, PY310, PY311, PY312 -DatasetFromRoot() # airflow.api_connexion.security -requires_access, requires_access_dataset - -# airflow.auth.managers -is_authorized_dataset -DatasetDetails() - -# airflow.configuration -get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - +requires_access # airflow.contrib.* AWSAthenaHook() @@ -95,97 +47,16 @@ # airflow.datasets DatasetAliasEvent() -# airflow.datasets.manager -DatasetManager() -dataset_manager -resolve_dataset_manager - -# airflow.hooks -BaseHook() - -# airflow.lineage.hook -DatasetLineageInfo() - -# airflow.listeners.spec.dataset -on_dataset_changed -on_dataset_created - -# airflow.metrics.validators -AllowListValidator() -BlockListValidator() - - -# airflow.operators.branch_operator -BaseBranchOperator() - -# airflow.operators.dagrun_operator -TriggerDagRunLink() -TriggerDagRunOperator() - -# airflow.operators.email_operator -EmailOperator() - -# airflow.operators.latest_only_operator -LatestOnlyOperator() - -# airflow.operators.python_operator -BranchPythonOperator() -PythonOperator() -PythonVirtualenvOperator() -ShortCircuitOperator() # airflow.operators.subdag.* SubDagOperator() -# airflow.providers.amazon -AvpEntities.DATASET -s3.create_dataset -s3.convert_dataset_to_openlineage -s3.sanitize_uri - -# airflow.providers.common.io -common_io_file.convert_dataset_to_openlineage -common_io_file.create_dataset -common_io_file.sanitize_uri - -# airflow.providers.fab -fab_auth_manager.is_authorized_dataset - -# airflow.providers.google -bigquery.sanitize_uri - -gcs.create_dataset -gcs.sanitize_uri -gcs.convert_dataset_to_openlineage - -# airflow.providers.mysql -mysql.sanitize_uri - -# airflow.providers.openlineage -DatasetInfo() -translate_airflow_dataset - -# airflow.providers.postgres -postgres.sanitize_uri - -# airflow.providers.trino -trino.sanitize_uri # airflow.secrets # get_connection LocalFilesystemBackend() -load_connections - -# airflow.security.permissions -RESOURCE_DATASET - -# airflow.sensors.base_sensor_operator -BaseSensorOperator() -# airflow.timetables -DatasetTriggeredTimetable() - # airflow.triggers.external_task TaskStateTrigger() @@ -215,15 +86,8 @@ apply_defaults # airflow.utils.file -TemporaryDirectory() mkdirs -# airflow.utils.helpers -helper_chain -helper_cross_downstream - -# airflow.utils.log -secrets_masker # airflow.utils.state SHUTDOWN @@ -233,6 +97,7 @@ TriggerRule.DUMMY TriggerRule.NONE_FAILED_OR_SKIPPED + # airflow.www.auth has_access has_access_dataset diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py new file mode 100644 index 0000000000000..556c211c7990f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from airflow.api_connexion.security import requires_access_dataset +from airflow.auth.managers.models.resource_details import ( + DatasetDetails, +) +from airflow.datasets.manager import ( + DatasetManager, + dataset_manager, + resolve_dataset_manager, +) +from airflow.lineage.hook import DatasetLineageInfo +from airflow.metrics.validators import AllowListValidator, BlockListValidator +from airflow.secrets.local_filesystem import load_connections +from airflow.security.permissions import RESOURCE_DATASET + +requires_access_dataset() + +DatasetDetails() + +DatasetManager() +dataset_manager() +resolve_dataset_manager() + +DatasetLineageInfo() + +AllowListValidator() +BlockListValidator() + +load_connections() + +RESOURCE_DATASET + + +from airflow.listeners.spec.dataset import ( + on_dataset_changed, + on_dataset_created, +) + +on_dataset_created() +on_dataset_changed() + + +# airflow.operators.python +from airflow.operators.python import get_current_context + +get_current_context() + +# airflow.providers.mysql +from airflow.providers.mysql.datasets.mysql import sanitize_uri + +sanitize_uri + +# airflow.providers.postgres +from airflow.providers.postgres.datasets.postgres import sanitize_uri + +sanitize_uri + +# airflow.providers.trino +from airflow.providers.trino.datasets.trino import sanitize_uri + +sanitize_uri + +# airflow.notifications.basenotifier +from airflow.notifications.basenotifier import BaseNotifier + +BaseNotifier() + +# airflow.auth.manager +from airflow.auth.managers.base_auth_manager import BaseAuthManager + +BaseAuthManager() + + +from airflow.configuration import ( + as_dict, + get, + getboolean, + getfloat, + getint, + has_option, + remove_option, + set, +) + +# airflow.configuration +get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +from airflow.hooks.base_hook import BaseHook + +# airflow.hooks +BaseHook() + +from airflow.sensors.base_sensor_operator import BaseSensorOperator + +# airflow.sensors.base_sensor_operator +BaseSensorOperator() +BaseHook() + +from airflow.utils.helpers import chain as helper_chain +from airflow.utils.helpers import cross_downstream as helper_cross_downstream + +# airflow.utils.helpers +helper_chain +helper_cross_downstream + +# airflow.utils.file +from airflow.utils.file import TemporaryDirectory + +TemporaryDirectory() + +from airflow.utils.log import secrets_masker + +# airflow.utils.log +secrets_masker diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py index 5d4c3da974b80..e4202741eddbf 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py @@ -1,8 +1,8 @@ from __future__ import annotations try: - from airflow.sdk import Asset + from airflow.assets.manager import AssetManager except ModuleNotFoundError: - from airflow.datasets import Dataset as Asset + from airflow.datasets.manager import DatasetManager as AssetManager -Asset +AssetManager() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py new file mode 100644 index 0000000000000..a89048d3e47f8 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from airflow.providers.amazon.aws.auth_manager.avp.entities import AvpEntities +from airflow.providers.openlineage.utils.utils import ( + DatasetInfo, + translate_airflow_dataset, +) +from airflow.secrets.local_filesystem import load_connections +from airflow.security.permissions import RESOURCE_DATASET + +AvpEntities.DATASET + +# airflow.providers.openlineage.utils.utils +DatasetInfo() +translate_airflow_dataset() + +# airflow.secrets.local_filesystem +load_connections() + +# airflow.security.permissions +RESOURCE_DATASET + +from airflow.providers.amazon.aws.datasets.s3 import ( + convert_dataset_to_openlineage as s3_convert_dataset_to_openlineage, +) +from airflow.providers.amazon.aws.datasets.s3 import create_dataset as s3_create_dataset + +s3_create_dataset() +s3_convert_dataset_to_openlineage() + +from airflow.providers.common.io.dataset.file import ( + convert_dataset_to_openlineage as io_convert_dataset_to_openlineage, +) +from airflow.providers.common.io.dataset.file import create_dataset as io_create_dataset + +io_create_dataset() +io_convert_dataset_to_openlineage() + + +# # airflow.providers.google.datasets.bigquery +from airflow.providers.google.datasets.bigquery import ( + create_dataset as bigquery_create_dataset, +) + +bigquery_create_dataset() + +# airflow.providers.google.datasets.gcs +from airflow.providers.google.datasets.gcs import ( + convert_dataset_to_openlineage as gcs_convert_dataset_to_openlineage, +) +from airflow.providers.google.datasets.gcs import create_dataset as gcs_create_dataset + +gcs_create_dataset() +gcs_convert_dataset_to_openlineage() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py deleted file mode 100644 index df479f4d937d0..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py +++ /dev/null @@ -1,481 +0,0 @@ -from __future__ import annotations - -from airflow.api.auth.backend import basic_auth, kerberos_auth -from airflow.api.auth.backend.basic_auth import auth_current_user -from airflow.auth.managers.fab.api.auth.backend import ( - kerberos_auth as backend_kerberos_auth, -) -from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager -from airflow.auth.managers.fab.security_manager import override as fab_override -from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG -from airflow.executors.celery_executor import CeleryExecutor, app -from airflow.executors.celery_kubernetes_executor import CeleryKubernetesExecutor -from airflow.executors.dask_executor import DaskExecutor -from airflow.executors.kubernetes_executor_types import ( - ALL_NAMESPACES, - POD_EXECUTOR_DONE_KEY, -) -from airflow.hooks.dbapi import ConnectorProtocol, DbApiHook -from airflow.hooks.dbapi_hook import DbApiHook as DbApiHook2 -from airflow.hooks.docker_hook import DockerHook -from airflow.hooks.druid_hook import DruidDbApiHook, DruidHook -from airflow.hooks.filesystem import FSHook -from airflow.hooks.hive_hooks import ( - HIVE_QUEUE_PRIORITIES, - HiveCliHook, - HiveMetastoreHook, - HiveServer2Hook, -) -from airflow.hooks.http_hook import HttpHook -from airflow.hooks.jdbc_hook import JdbcHook, jaydebeapi -from airflow.hooks.mssql_hook import MsSqlHook -from airflow.hooks.mysql_hook import MySqlHook -from airflow.hooks.oracle_hook import OracleHook -from airflow.hooks.package_index import PackageIndexHook -from airflow.hooks.pig_hook import PigCliHook -from airflow.hooks.postgres_hook import PostgresHook -from airflow.hooks.presto_hook import PrestoHook -from airflow.hooks.S3_hook import S3Hook, provide_bucket_name -from airflow.hooks.samba_hook import SambaHook -from airflow.hooks.slack_hook import SlackHook -from airflow.hooks.sqlite_hook import SqliteHook -from airflow.hooks.subprocess import SubprocessHook, SubprocessResult, working_directory -from airflow.hooks.webhdfs_hook import WebHDFSHook -from airflow.hooks.zendesk_hook import ZendeskHook -from airflow.kubernetes.k8s_model import K8SModel, append_to_pod -from airflow.kubernetes.kube_client import ( - _disable_verify_ssl, - _enable_tcp_keepalive, - get_kube_client, -) -from airflow.kubernetes.kubernetes_helper_functions import ( - add_pod_suffix, - annotations_for_logging_task_metadata, - annotations_to_key, - create_pod_id, - get_logs_task_metadata, - rand_str, -) -from airflow.kubernetes.pod import Port, Resources -from airflow.kubernetes.pod_generator import ( - PodDefaults, - PodGenerator, - PodGeneratorDeprecated, - datetime_to_label_safe_datestring, - extend_object_field, - label_safe_datestring_to_datetime, - make_safe_label_value, - merge_objects, -) -from airflow.kubernetes.pod_generator import ( - add_pod_suffix as add_pod_suffix2, -) -from airflow.kubernetes.pod_generator import ( - rand_str as rand_str2, -) -from airflow.kubernetes.pod_generator_deprecated import ( - PodDefaults as PodDefaults3, -) -from airflow.kubernetes.pod_generator_deprecated import ( - PodGenerator as PodGenerator2, -) -from airflow.kubernetes.pod_generator_deprecated import ( - make_safe_label_value as make_safe_label_value2, -) -from airflow.kubernetes.pod_launcher import PodLauncher, PodStatus -from airflow.kubernetes.pod_launcher_deprecated import ( - PodDefaults as PodDefaults2, -) -from airflow.kubernetes.pod_launcher_deprecated import ( - PodLauncher as PodLauncher2, -) -from airflow.kubernetes.pod_launcher_deprecated import ( - PodStatus as PodStatus2, -) -from airflow.kubernetes.pod_launcher_deprecated import ( - get_kube_client as get_kube_client2, -) -from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv -from airflow.kubernetes.secret import K8SModel2, Secret -from airflow.kubernetes.volume import Volume -from airflow.kubernetes.volume_mount import VolumeMount -from airflow.macros.hive import closest_ds_partition, max_partition -from airflow.operators.bash import BashOperator -from airflow.operators.bash_operator import BashOperator as LegacyBashOperator -from airflow.operators.check_operator import ( - CheckOperator, - IntervalCheckOperator, - SQLCheckOperator, - SQLIntervalCheckOperator, - SQLThresholdCheckOperator, - SQLValueCheckOperator, - ThresholdCheckOperator, - ValueCheckOperator, -) -from airflow.operators.datetime import BranchDateTimeOperator, target_times_as_dates -from airflow.operators.docker_operator import DockerOperator -from airflow.operators.druid_check_operator import DruidCheckOperator -from airflow.operators.dummy import DummyOperator, EmptyOperator -from airflow.operators.email import EmailOperator -from airflow.operators.email_operator import EmailOperator -from airflow.operators.gcs_to_s3 import GCSToS3Operator -from airflow.operators.google_api_to_s3_transfer import ( - GoogleApiToS3Operator, - GoogleApiToS3Transfer, -) -from airflow.operators.hive_operator import HiveOperator -from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator -from airflow.operators.hive_to_druid import HiveToDruidOperator, HiveToDruidTransfer -from airflow.operators.hive_to_mysql import HiveToMySqlOperator, HiveToMySqlTransfer -from airflow.operators.hive_to_samba_operator import HiveToSambaOperator -from airflow.operators.http_operator import SimpleHttpOperator -from airflow.operators.jdbc_operator import JdbcOperator -from airflow.operators.mssql_operator import MsSqlOperator -from airflow.operators.mssql_to_hive import MsSqlToHiveOperator, MsSqlToHiveTransfer -from airflow.operators.mysql_operator import MySqlOperator -from airflow.operators.mysql_to_hive import MySqlToHiveOperator, MySqlToHiveTransfer -from airflow.operators.oracle_operator import OracleOperator -from airflow.operators.papermill_operator import PapermillOperator -from airflow.operators.pig_operator import PigOperator -from airflow.operators.postgres_operator import Mapping, PostgresOperator -from airflow.operators.presto_check_operator import ( - PrestoCheckOperator, - PrestoIntervalCheckOperator, - PrestoValueCheckOperator, -) -from airflow.operators.presto_check_operator import ( - SQLCheckOperator as SQLCheckOperator2, -) -from airflow.operators.presto_check_operator import ( - SQLIntervalCheckOperator as SQLIntervalCheckOperator2, -) -from airflow.operators.presto_check_operator import ( - SQLValueCheckOperator as SQLValueCheckOperator2, -) -from airflow.operators.presto_to_mysql import ( - PrestoToMySqlOperator, - PrestoToMySqlTransfer, -) -from airflow.operators.python import ( - BranchPythonOperator, - PythonOperator, - PythonVirtualenvOperator, - ShortCircuitOperator, -) -from airflow.operators.redshift_to_s3_operator import ( - RedshiftToS3Operator, - RedshiftToS3Transfer, -) -from airflow.operators.s3_file_transform_operator import S3FileTransformOperator -from airflow.operators.s3_to_hive_operator import S3ToHiveOperator, S3ToHiveTransfer -from airflow.operators.s3_to_redshift_operator import ( - S3ToRedshiftOperator, - S3ToRedshiftTransfer, -) -from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator -from airflow.operators.sql import ( - BaseSQLOperator, - BranchSQLOperator, - SQLTableCheckOperator, - _convert_to_float_if_possible, - parse_boolean, -) -from airflow.operators.sql import ( - SQLCheckOperator as SQLCheckOperator3, -) -from airflow.operators.sql import ( - SQLColumnCheckOperator as SQLColumnCheckOperator2, -) -from airflow.operators.sql import ( - SQLIntervalCheckOperator as SQLIntervalCheckOperator3, -) -from airflow.operators.sql import ( - SQLThresholdCheckOperator as SQLThresholdCheckOperator2, -) -from airflow.operators.sql import ( - SQLValueCheckOperator as SQLValueCheckOperator3, -) -from airflow.operators.sqlite_operator import SqliteOperator -from airflow.operators.trigger_dagrun import TriggerDagRunOperator -from airflow.operators.weekday import BranchDayOfWeekOperator -from airflow.sensors import filesystem -from airflow.sensors.date_time import DateTimeSensor, DateTimeSensorAsync -from airflow.sensors.date_time_sensor import DateTimeSensor -from airflow.sensors.external_task import ( - ExternalTaskMarker, - ExternalTaskSensor, - ExternalTaskSensorLink, -) -from airflow.sensors.filesystem import FileSensor -from airflow.sensors.hive_partition_sensor import HivePartitionSensor -from airflow.sensors.http_sensor import HttpSensor -from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor -from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor -from airflow.sensors.sql import SqlSensor -from airflow.sensors.sql_sensor import SqlSensor2 -from airflow.sensors.time_delta import TimeDeltaSensor, TimeDeltaSensorAsync, WaitSensor -from airflow.sensors.time_sensor import TimeSensor, TimeSensorAsync -from airflow.sensors.web_hdfs_sensor import WebHdfsSensor -from airflow.sensors.weekday import DayOfWeekSensor -from airflow.triggers.external_task import DagStateTrigger, WorkflowTrigger -from airflow.triggers.file import FileTrigger -from airflow.triggers.temporal import DateTimeTrigger, TimeDeltaTrigger -from airflow.www.security import FabAirflowSecurityManagerOverride - -# apache-airflow-providers-amazon -provide_bucket_name() -GCSToS3Operator() -GoogleApiToS3Operator() -GoogleApiToS3Transfer() -RedshiftToS3Operator() -RedshiftToS3Transfer() -S3FileTransformOperator() -S3Hook() -SSQLTableCheckOperator3KeySensor() -S3ToRedshiftOperator() -S3ToRedshiftTransfer() - -# apache-airflow-providers-celery -DEFAULT_CELERY_CONFIG -app -CeleryExecutor() -CeleryKubernetesExecutor() - -# apache-airflow-providers-common-sql -_convert_to_float_if_possible() -parse_boolean() -BaseSQLOperator() -BashOperator() -LegacyBashOperator() -BranchSQLOperator() -CheckOperator() -ConnectorProtocol() -DbApiHook() -DbApiHook2() -IntervalCheckOperator() -PrestoCheckOperator() -PrestoIntervalCheckOperator() -PrestoValueCheckOperator() -SQLCheckOperator() -SQLCheckOperator2() -SQLCheckOperator3() -SQLColumnCheckOperator2() -SQLIntervalCheckOperator() -SQLIntervalCheckOperator2() -SQLIntervalCheckOperator3() -SQLTableCheckOperator() -SQLThresholdCheckOperator() -SQLThresholdCheckOperator2() -SQLValueCheckOperator() -SQLValueCheckOperator2() -SQLValueCheckOperator3() -SqlSensor() -SqlSensor2() -ThresholdCheckOperator() -ValueCheckOperator() - -# apache-airflow-providers-daskexecutor -DaskExecutor() - -# apache-airflow-providers-docker -DockerHook() -DockerOperator() - -# apache-airflow-providers-apache-druid -DruidDbApiHook() -DruidHook() -DruidCheckOperator() - -# apache-airflow-providers-apache-hdfs -WebHDFSHook() -WebHdfsSensor() - -# apache-airflow-providers-apache-hive -HIVE_QUEUE_PRIORITIES -closest_ds_partition() -max_partition() -HiveCliHook() -HiveMetastoreHook() -HiveOperator() -HivePartitionSensor() -HiveServer2Hook() -HiveStatsCollectionOperator() -HiveToDruidOperator() -HiveToDruidTransfer() -HiveToSambaOperator() -S3ToHiveOperator() -S3ToHiveTransfer() -MetastorePartitionSensor() -NamedHivePartitionSensor() - -# apache-airflow-providers-http -HttpHook() -HttpSensor() -SimpleHttpOperator() - -# apache-airflow-providers-jdbc -jaydebeapi -JdbcHook() -JdbcOperator() - -# apache-airflow-providers-fab -basic_auth.CLIENT_AUTH -basic_auth.init_app -basic_auth.auth_current_user -basic_auth.requires_authentication - -kerberos_auth.log -kerberos_auth.CLIENT_AUTH -kerberos_auth.find_user -kerberos_auth.init_app -kerberos_auth.requires_authentication -auth_current_user -backend_kerberos_auth -fab_override -FabAuthManager() -FabAirflowSecurityManagerOverride() - -# check whether attribute access -basic_auth.auth_current_user - -# apache-airflow-providers-cncf-kubernetes -ALL_NAMESPACES -POD_EXECUTOR_DONE_KEY -_disable_verify_ssl() -_enable_tcp_keepalive() -append_to_pod() -annotations_for_logging_task_metadata() -annotations_to_key() -create_pod_id() -datetime_to_label_safe_datestring() -extend_object_field() -get_logs_task_metadata() -label_safe_datestring_to_datetime() -merge_objects() -Port() -Resources() -PodRuntimeInfoEnv() -PodGeneratorDeprecated() -Volume() -VolumeMount() -Secret() - -add_pod_suffix() -add_pod_suffix2() -get_kube_client() -get_kube_client2() -make_safe_label_value() -make_safe_label_value2() -rand_str() -rand_str2() -K8SModel() -K8SModel2() -PodLauncher() -PodLauncher2() -PodStatus() -PodStatus2() -PodDefaults() -PodDefaults2() -PodDefaults3() -PodGenerator() -PodGenerator2() - - -# apache-airflow-providers-microsoft-mssql -MsSqlHook() -MsSqlOperator() -MsSqlToHiveOperator() -MsSqlToHiveTransfer() - -# apache-airflow-providers-mysql -HiveToMySqlOperator() -HiveToMySqlTransfer() -MySqlHook() -MySqlOperator() -MySqlToHiveOperator() -MySqlToHiveTransfer() -PrestoToMySqlOperator() -PrestoToMySqlTransfer() - -# apache-airflow-providers-oracle -OracleHook() -OracleOperator() - -# apache-airflow-providers-papermill -PapermillOperator() - -# apache-airflow-providers-apache-pig -PigCliHook() -PigOperator() - -# apache-airflow-providers-postgres -Mapping -PostgresHook() -PostgresOperator() - -# apache-airflow-providers-presto -PrestoHook() - -# apache-airflow-providers-samba -SambaHook() - -# apache-airflow-providers-slack -SlackHook() -SlackAPIOperator() -SlackAPIPostOperator() - -# apache-airflow-providers-sqlite -SqliteHook() -SqliteOperator() - -# apache-airflow-providers-zendesk -ZendeskHook() - -# apache-airflow-providers-smtp -EmailOperator() - -# apache-airflow-providers-standard -filesystem.FileSensor() -FileSensor() -TriggerDagRunOperator() -ExternalTaskMarker() -ExternalTaskSensor() -BranchDateTimeOperator() -BranchDayOfWeekOperator() -BranchPythonOperator() -DateTimeSensor() -DateTimeSensorAsync() -TimeSensor() -TimeDeltaSensor() -DayOfWeekSensor() -DummyOperator() -EmptyOperator() -ExternalTaskMarker() -ExternalTaskSensor() -ExternalTaskSensorLink() -FileSensor() -FileTrigger() -FSHook() -PackageIndexHook() -SubprocessHook() -ShortCircuitOperator() -TimeDeltaSensor() -TimeSensor() -TriggerDagRunOperator() -WorkflowTrigger() -PythonOperator() -PythonVirtualenvOperator() -DagStateTrigger() -FileTrigger() -DateTimeTrigger() -TimeDeltaTrigger() -SubprocessResult() -SubprocessHook() -TimeDeltaSensor() -TimeDeltaSensorAsync() -WaitSensor() -TimeSensor() -TimeSensorAsync() -BranchDateTimeOperator() -working_directory() -target_times_as_dates() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py new file mode 100644 index 0000000000000..befad4b8e03f3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from airflow.hooks.S3_hook import ( + S3Hook, + provide_bucket_name, +) +from airflow.operators.gcs_to_s3 import GCSToS3Operator +from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +from airflow.sensors.s3_key_sensor import S3KeySensor + +S3Hook() +provide_bucket_name() + +GCSToS3Operator() +GoogleApiToS3Operator() +RedshiftToS3Operator() +S3FileTransformOperator() +S3ToRedshiftOperator() +S3KeySensor() + +from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Transfer + +GoogleApiToS3Transfer() + +from airflow.operators.redshift_to_s3_operator import RedshiftToS3Transfer + +RedshiftToS3Transfer() + +from airflow.operators.s3_to_redshift_operator import S3ToRedshiftTransfer + +S3ToRedshiftTransfer() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py new file mode 100644 index 0000000000000..defe8fe915d00 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG +from airflow.executors.celery_executor import ( + CeleryExecutor, + app, +) + +DEFAULT_CELERY_CONFIG + +app +CeleryExecutor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py new file mode 100644 index 0000000000000..56e74a188dc4b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from airflow.hooks.dbapi import ( + ConnectorProtocol, + DbApiHook, +) + +ConnectorProtocol() +DbApiHook() + +from airflow.hooks.dbapi_hook import DbApiHook +from airflow.operators.check_operator import SQLCheckOperator + +DbApiHook() +SQLCheckOperator() + + +from airflow.operators.check_operator import CheckOperator +from airflow.operators.sql import SQLCheckOperator + +SQLCheckOperator() +CheckOperator() + + +from airflow.operators.druid_check_operator import CheckOperator + +CheckOperator() + + +from airflow.operators.presto_check_operator import CheckOperator + +CheckOperator() + + +from airflow.operators.check_operator import ( + IntervalCheckOperator, + SQLIntervalCheckOperator, +) +from airflow.operators.druid_check_operator import DruidCheckOperator +from airflow.operators.presto_check_operator import PrestoCheckOperator + +DruidCheckOperator() +PrestoCheckOperator() +IntervalCheckOperator() +SQLIntervalCheckOperator() + + +from airflow.operators.presto_check_operator import ( + IntervalCheckOperator, + PrestoIntervalCheckOperator, +) +from airflow.operators.sql import SQLIntervalCheckOperator + +IntervalCheckOperator() +SQLIntervalCheckOperator() +PrestoIntervalCheckOperator() + + +from airflow.operators.check_operator import ( + SQLThresholdCheckOperator, + ThresholdCheckOperator, +) + +SQLThresholdCheckOperator() +ThresholdCheckOperator() + + +from airflow.operators.sql import SQLThresholdCheckOperator + +SQLThresholdCheckOperator() + + +from airflow.operators.check_operator import ( + SQLValueCheckOperator, + ValueCheckOperator, +) + +SQLValueCheckOperator() +ValueCheckOperator() + + +from airflow.operators.presto_check_operator import ( + PrestoValueCheckOperator, + ValueCheckOperator, +) +from airflow.operators.sql import SQLValueCheckOperator + +SQLValueCheckOperator() +ValueCheckOperator() +PrestoValueCheckOperator() + + +from airflow.operators.sql import ( + BaseSQLOperator, + BranchSQLOperator, + SQLColumnCheckOperator, + SQLTableCheckOperator, + _convert_to_float_if_possible, + parse_boolean, +) + +BaseSQLOperator() +BranchSQLOperator() +SQLTableCheckOperator() +SQLColumnCheckOperator() +_convert_to_float_if_possible() +parse_boolean() + + +from airflow.sensors.sql import SqlSensor + +SqlSensor() + + +from airflow.sensors.sql_sensor import SqlSensor + +SqlSensor() + + +from airflow.providers.common.sql.operators.sql import SQLExecuteQueryOperator + +SQLExecuteQueryOperator() +SQLExecuteQueryOperator() +SQLExecuteQueryOperator() +SQLExecuteQueryOperator() +SQLExecuteQueryOperator() +SQLExecuteQueryOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py new file mode 100644 index 0000000000000..9b6c273b13712 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.executors.dask_executor import DaskExecutor + +DaskExecutor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py new file mode 100644 index 0000000000000..85b816a8be423 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.docker_hook import DockerHook +from airflow.operators.docker_operator import DockerOperator + +DockerHook() +DockerOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py new file mode 100644 index 0000000000000..1de011de9f069 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from airflow.hooks.druid_hook import ( + DruidDbApiHook, + DruidHook, +) +from airflow.operators.hive_to_druid import ( + HiveToDruidOperator, + HiveToDruidTransfer, +) + +DruidDbApiHook() +DruidHook() + +HiveToDruidOperator() +HiveToDruidTransfer() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py new file mode 100644 index 0000000000000..6a5a5ba7bd6a8 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from airflow.api.auth.backend.basic_auth import ( + CLIENT_AUTH, + auth_current_user, + init_app, + requires_authentication, +) + +CLIENT_AUTH +init_app() +auth_current_user() +requires_authentication() + +from airflow.api.auth.backend.kerberos_auth import ( + CLIENT_AUTH, + find_user, + init_app, + log, + requires_authentication, +) + +log() +CLIENT_AUTH +find_user() +init_app() +requires_authentication() + +from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( + CLIENT_AUTH, + find_user, + init_app, + log, + requires_authentication, +) + +log() +CLIENT_AUTH +find_user() +init_app() +requires_authentication() + +from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager +from airflow.auth.managers.fab.security_manager.override import ( + MAX_NUM_DATABASE_USER_SESSIONS, + FabAirflowSecurityManagerOverride, +) + +FabAuthManager() +MAX_NUM_DATABASE_USER_SESSIONS +FabAirflowSecurityManagerOverride() + +from airflow.www.security import FabAirflowSecurityManagerOverride + +FabAirflowSecurityManagerOverride() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py new file mode 100644 index 0000000000000..a52239fe80bc5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.webhdfs_hook import WebHDFSHook +from airflow.sensors.web_hdfs_sensor import WebHdfsSensor + +WebHDFSHook() +WebHdfsSensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py new file mode 100644 index 0000000000000..8f3076b2d364e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from airflow.hooks.hive_hooks import ( + HIVE_QUEUE_PRIORITIES, + HiveCliHook, + HiveMetastoreHook, + HiveServer2Hook, +) +from airflow.macros.hive import ( + closest_ds_partition, + max_partition, +) +from airflow.operators.hive_operator import HiveOperator +from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +from airflow.operators.hive_to_mysql import HiveToMySqlOperator +from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + +HIVE_QUEUE_PRIORITIES +HiveCliHook() +HiveMetastoreHook() +HiveServer2Hook() + +closest_ds_partition() +max_partition() + +HiveOperator() +HiveStatsCollectionOperator() +HiveToMySqlOperator() +HiveToSambaOperator() + + +from airflow.operators.hive_to_mysql import HiveToMySqlTransfer + +HiveToMySqlTransfer() + +from airflow.operators.mysql_to_hive import MySqlToHiveOperator + +MySqlToHiveOperator() + +from airflow.operators.mysql_to_hive import MySqlToHiveTransfer + +MySqlToHiveTransfer() + +from airflow.operators.mssql_to_hive import MsSqlToHiveOperator + +MsSqlToHiveOperator() + +from airflow.operators.mssql_to_hive import MsSqlToHiveTransfer + +MsSqlToHiveTransfer() + +from airflow.operators.s3_to_hive_operator import S3ToHiveOperator + +S3ToHiveOperator() + +from airflow.operators.s3_to_hive_operator import S3ToHiveTransfer + +S3ToHiveTransfer() + +from airflow.sensors.hive_partition_sensor import HivePartitionSensor + +HivePartitionSensor() + +from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor + +MetastorePartitionSensor() + +from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor + +NamedHivePartitionSensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py new file mode 100644 index 0000000000000..d2ca3a4206859 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from airflow.hooks.http_hook import HttpHook +from airflow.operators.http_operator import SimpleHttpOperator +from airflow.sensors.http_sensor import HttpSensor + +HttpHook() +SimpleHttpOperator() +HttpSensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py new file mode 100644 index 0000000000000..2a00135a9f372 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from airflow.hooks.jdbc_hook import ( + JdbcHook, + jaydebeapi, +) + +JdbcHook() +jaydebeapi() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py new file mode 100644 index 0000000000000..0f9a6614dab5a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from airflow.executors.kubernetes_executor_types import ( + ALL_NAMESPACES, + POD_EXECUTOR_DONE_KEY, +) +from airflow.kubernetes.k8s_model import ( + K8SModel, + append_to_pod, +) +from airflow.kubernetes.kube_client import ( + _disable_verify_ssl, + _enable_tcp_keepalive, + get_kube_client, +) +from airflow.kubernetes.kubernetes_helper_functions import ( + add_pod_suffix, + annotations_for_logging_task_metadata, + create_pod_id, +) + +ALL_NAMESPACES +POD_EXECUTOR_DONE_KEY + +K8SModel() +append_to_pod() + +_disable_verify_ssl() +_enable_tcp_keepalive() +get_kube_client() + +add_pod_suffix() +annotations_for_logging_task_metadata() +create_pod_id() + + +from airflow.kubernetes.pod_generator import ( + PodDefaults, + PodGenerator, + add_pod_suffix, + datetime_to_label_safe_datestring, + extend_object_field, + label_safe_datestring_to_datetime, + make_safe_label_value, + merge_objects, + rand_str, +) + +PodDefaults() +PodGenerator() +add_pod_suffix() +datetime_to_label_safe_datestring() +extend_object_field() +label_safe_datestring_to_datetime() +make_safe_label_value() +merge_objects() +rand_str() + +from airflow.kubernetes.pod_generator_deprecated import ( + PodDefaults, + PodGenerator, + make_safe_label_value, +) +from airflow.kubernetes.pod_launcher import ( + PodLauncher, + PodStatus, +) + +PodDefaults() +PodGenerator() +make_safe_label_value() + +PodLauncher() +PodStatus() + +from airflow.kubernetes.pod_launcher_deprecated import ( + PodDefaults, + PodLauncher, + PodStatus, + get_kube_client, +) +from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv +from airflow.kubernetes.secret import ( + K8SModel, + Secret, +) +from airflow.kubernetes.volume import Volume +from airflow.kubernetes.volume_mount import VolumeMount + +PodDefaults() +PodLauncher() +PodStatus() +get_kube_client() + +PodRuntimeInfoEnv() +K8SModel() +Secret() +Volume() +VolumeMount() + +from airflow.kubernetes.kubernetes_helper_functions import ( + annotations_to_key, + get_logs_task_metadata, + rand_str, +) + +annotations_to_key() +get_logs_task_metadata() +rand_str() + +from airflow.kubernetes.pod_generator import PodGeneratorDeprecated + +PodGeneratorDeprecated() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py new file mode 100644 index 0000000000000..0bc3eab8dd04e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from airflow.hooks.mysql_hook import MySqlHook +from airflow.operators.presto_to_mysql import ( + PrestoToMySqlOperator, + PrestoToMySqlTransfer, +) + +MySqlHook() +PrestoToMySqlOperator() +PrestoToMySqlTransfer() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py new file mode 100644 index 0000000000000..3c525e4eb705c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.oracle_hook import OracleHook + +OracleHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py new file mode 100644 index 0000000000000..42be2959e3660 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.operators.papermill_operator import PapermillOperator + +PapermillOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py new file mode 100644 index 0000000000000..e09bb35fa6b18 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.pig_hook import PigCliHook +from airflow.operators.pig_operator import PigOperator + +PigCliHook() +PigOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py new file mode 100644 index 0000000000000..bbd79299fa24e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.postgres_hook import PostgresHook +from airflow.operators.postgres_operator import Mapping + +PostgresHook() +Mapping() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py new file mode 100644 index 0000000000000..f5117884a7b42 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.presto_hook import PrestoHook + +PrestoHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py new file mode 100644 index 0000000000000..f18cd97a6e7e9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.samba_hook import SambaHook + +SambaHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py new file mode 100644 index 0000000000000..644f7d06b9084 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from airflow.hooks.slack_hook import SlackHook +from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator + +SlackHook() +SlackAPIOperator() +SlackAPIPostOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py new file mode 100644 index 0000000000000..ea3795460e67b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from airflow.operators.email_operator import EmailOperator + +EmailOperator() + +from airflow.operators.email import EmailOperator + +EmailOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py new file mode 100644 index 0000000000000..a4b256ff03a61 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.sqlite_hook import SqliteHook + +SqliteHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py new file mode 100644 index 0000000000000..a43031057ed38 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from airflow.operators.bash_operator import BashOperator +from airflow.operators.dagrun_operator import ( + TriggerDagRunLink, + TriggerDagRunOperator, +) +from airflow.operators.latest_only_operator import LatestOnlyOperator +from airflow.operators.python_operator import ( + BranchPythonOperator, + PythonOperator, + PythonVirtualenvOperator, + ShortCircuitOperator, +) +from airflow.sensors.external_task_sensor import ( + ExternalTaskMarker, + ExternalTaskSensor, +) + +BashOperator() + +TriggerDagRunLink() +TriggerDagRunOperator() + +LatestOnlyOperator() + +BranchPythonOperator() +PythonOperator() +PythonVirtualenvOperator() +ShortCircuitOperator() + +ExternalTaskMarker() +ExternalTaskSensor() + + +from airflow.hooks.subprocess import SubprocessResult + +SubprocessResult() + +from airflow.hooks.subprocess import working_directory + +working_directory() + +from airflow.operators.datetime import target_times_as_dates + +target_times_as_dates() + +from airflow.operators.trigger_dagrun import TriggerDagRunLink + +TriggerDagRunLink() + +from airflow.sensors.external_task import ExternalTaskSensorLink + +ExternalTaskSensorLink() + +from airflow.sensors.time_delta import WaitSensor + +WaitSensor() + +from airflow.operators.dummy import DummyOperator + +DummyOperator() + +from airflow.operators.dummy import EmptyOperator + +EmptyOperator() + +from airflow.operators.dummy_operator import DummyOperator + +DummyOperator() + +from airflow.operators.dummy_operator import EmptyOperator + +EmptyOperator() + +from airflow.sensors.external_task_sensor import ExternalTaskSensorLink + +ExternalTaskSensorLink() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_try.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_try.py new file mode 100644 index 0000000000000..c062aeb61a17e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_try.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +try: + from airflow.providers.http.operators.http import HttpOperator +except ModuleNotFoundError: + from airflow.operators.http_operator import SimpleHttpOperator as HttpOperator + +HttpOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py new file mode 100644 index 0000000000000..b64d4c45cf31f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.zendesk_hook import ZendeskHook + +ZendeskHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py index 2b96376395c29..47c7b4270d8ff 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_names.py @@ -9,15 +9,12 @@ expand_alias_to_datasets, ) from airflow.datasets.metadata import Metadata -from airflow.decorators import dag, setup, task, task_group, teardown -from airflow.io.path import ObjectStoragePath -from airflow.io.storage import attach -from airflow.models import DAG as DAGFromModel -from airflow.models.baseoperator import chain, chain_linear, cross_downstream -from airflow.models.baseoperatorlink import BaseOperatorLink -from airflow.models.dag import DAG as DAGFromDag -from airflow.timetables.datasets import DatasetOrTimeSchedule -from airflow.utils.dag_parsing_context import get_parsing_context +from airflow.decorators import ( + dag, + setup, + task, + task_group, +) # airflow DatasetFromRoot() @@ -35,14 +32,29 @@ task() task_group() setup() +from airflow.decorators import teardown +from airflow.io.path import ObjectStoragePath +from airflow.io.storage import attach +from airflow.models import DAG as DAGFromModel +from airflow.models import ( + Connection, + Variable, +) +from airflow.models.baseoperator import chain, chain_linear, cross_downstream +from airflow.models.baseoperatorlink import BaseOperatorLink +from airflow.models.dag import DAG as DAGFromDag + +# airflow.decorators teardown() -# airflow.io +# # airflow.io ObjectStoragePath() attach() # airflow.models +Connection() DAGFromModel() +Variable() # airflow.models.baseoperator chain() @@ -54,6 +66,9 @@ # airflow.models.dag DAGFromDag() +from airflow.timetables.datasets import DatasetOrTimeSchedule +from airflow.utils.dag_parsing_context import get_parsing_context + # airflow.timetables.datasets DatasetOrTimeSchedule() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_try.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_try.py new file mode 100644 index 0000000000000..5f6a1df032855 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR311_try.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +try: + from airflow.sdk import Asset +except ModuleNotFoundError: + from airflow.datasets import Dataset as Asset + +Asset() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR312.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR312.py index a0e04a408d308..0752511706ced 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR312.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR312.py @@ -2,54 +2,76 @@ from airflow.hooks.filesystem import FSHook from airflow.hooks.package_index import PackageIndexHook -from airflow.hooks.subprocess import SubprocessHook, SubprocessResult, working_directory +from airflow.hooks.subprocess import SubprocessHook from airflow.operators.bash import BashOperator -from airflow.operators.datetime import BranchDateTimeOperator, target_times_as_dates +from airflow.operators.datetime import BranchDateTimeOperator from airflow.operators.empty import EmptyOperator from airflow.operators.latest_only import LatestOnlyOperator +from airflow.operators.trigger_dagrun import TriggerDagRunOperator +from airflow.operators.weekday import BranchDayOfWeekOperator +from airflow.sensors.date_time import DateTimeSensor + +FSHook() +PackageIndexHook() +SubprocessHook() + +BashOperator() +BranchDateTimeOperator() +TriggerDagRunOperator() +EmptyOperator() + +LatestOnlyOperator() +BranchDayOfWeekOperator() +DateTimeSensor() + from airflow.operators.python import ( BranchPythonOperator, PythonOperator, PythonVirtualenvOperator, ShortCircuitOperator, ) -from airflow.operators.trigger_dagrun import TriggerDagRunLink, TriggerDagRunOperator -from airflow.operators.weekday import BranchDayOfWeekOperator -from airflow.sensors.date_time import DateTimeSensor, DateTimeSensorAsync +from airflow.sensors.date_time import DateTimeSensorAsync from airflow.sensors.external_task import ( ExternalTaskMarker, ExternalTaskSensor, - ExternalTaskSensorLink, +) +from airflow.sensors.time_sensor import ( + TimeSensor, + TimeSensorAsync, ) from airflow.sensors.filesystem import FileSensor -from airflow.sensors.time_delta import TimeDeltaSensor, TimeDeltaSensorAsync, WaitSensor -from airflow.sensors.time_sensor import TimeSensor, TimeSensorAsync + +BranchPythonOperator() +PythonOperator() +PythonVirtualenvOperator() +ShortCircuitOperator() +DateTimeSensorAsync() +ExternalTaskMarker() +ExternalTaskSensor() +FileSensor() +TimeSensor() +TimeSensorAsync() + +from airflow.sensors.time_delta import ( + TimeDeltaSensor, + TimeDeltaSensorAsync, +) from airflow.sensors.weekday import DayOfWeekSensor -from airflow.triggers.external_task import DagStateTrigger, WorkflowTrigger +from airflow.triggers.external_task import ( + DagStateTrigger, + WorkflowTrigger, +) from airflow.triggers.file import FileTrigger -from airflow.triggers.temporal import DateTimeTrigger, TimeDeltaTrigger - -FSHook() -PackageIndexHook() -SubprocessHook(), SubprocessResult(), working_directory() -BashOperator() -BranchDateTimeOperator(), target_times_as_dates() -TriggerDagRunLink(), TriggerDagRunOperator() -EmptyOperator() -LatestOnlyOperator() -( - BranchPythonOperator(), - PythonOperator(), - PythonVirtualenvOperator(), - ShortCircuitOperator(), +from airflow.triggers.temporal import ( + DateTimeTrigger, + TimeDeltaTrigger, ) -BranchDayOfWeekOperator() -DateTimeSensor(), DateTimeSensorAsync() -ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() -FileSensor() -TimeSensor(), TimeSensorAsync() -TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() + +TimeDeltaSensor() +TimeDeltaSensorAsync() DayOfWeekSensor() -DagStateTrigger(), WorkflowTrigger() +DagStateTrigger() +WorkflowTrigger() FileTrigger() -DateTimeTrigger(), TimeDeltaTrigger() +DateTimeTrigger() +TimeDeltaTrigger() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR312_try.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR312_try.py new file mode 100644 index 0000000000000..f8cf9402b609a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR312_try.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +try: + from airflow.providers.standard.triggers.file import FileTrigger +except ModuleNotFoundError: + from airflow.triggers.file import FileTrigger + +FileTrigger() diff --git a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py index ef808e733600d..cd8653026cc45 100644 --- a/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py +++ b/crates/ruff_linter/resources/test/fixtures/fastapi/FAST003.py @@ -178,3 +178,38 @@ async def unknown_1(other: str = Depends(unknown_unresolved)): ... async def unknown_2(other: str = Depends(unknown_not_function)): ... @app.get("/things/{thing_id}") async def unknown_3(other: str = Depends(unknown_imported)): ... + + +# Class dependencies +from pydantic import BaseModel +from dataclasses import dataclass + +class PydanticParams(BaseModel): + my_id: int + + +class InitParams: + def __init__(self, my_id: int): + self.my_id = my_id + + +# Errors +@app.get("/{id}") +async def get_id_pydantic_full( + params: Annotated[PydanticParams, Depends(PydanticParams)], +): ... +@app.get("/{id}") +async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... +@app.get("/{id}") +async def get_id_init_not_annotated(params = Depends(InitParams)): ... + + +# No errors +@app.get("/{my_id}") +async def get_id_pydantic_full( + params: Annotated[PydanticParams, Depends(PydanticParams)], +): ... +@app.get("/{my_id}") +async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... +@app.get("/{my_id}") +async def get_id_init_not_annotated(params = Depends(InitParams)): ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC115.py b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC115.py index 235a64f053674..6ec5d0133eee6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC115.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC115.py @@ -145,3 +145,35 @@ async def main() -> None: sleep = 10 anyio.run(main) + + +async def test_anyio_async115_helpers(): + import anyio + + await anyio.sleep(delay=1) # OK + await anyio.sleep(seconds=1) # OK + + await anyio.sleep(delay=0) # ASYNC115 + await anyio.sleep(seconds=0) # OK + + +async def test_trio_async115_helpers(): + import trio + + await trio.sleep(seconds=1) # OK + await trio.sleep(delay=1) # OK + + await trio.sleep(seconds=0) # ASYNC115 + await trio.sleep(delay=0) # OK + +# https://github.com/astral-sh/ruff/issues/18740 +# The autofix for this is unsafe due to the comments. +async def func(): + import trio + + await ( + trio # comment + .sleep( # comment + 0 # comment + ) + ) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC116.py b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC116.py index 5cfab2eae142b..ecd5268664eb8 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC116.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC116.py @@ -108,3 +108,31 @@ async def import_from_anyio(): # catch from import await sleep(86401) # error: 116, "async" + + +async def test_anyio_async116_helpers(): + import anyio + + await anyio.sleep(delay=1) # OK + await anyio.sleep(seconds=1) # OK + + await anyio.sleep(delay=86401) # ASYNC116 + await anyio.sleep(seconds=86401) # OK + + +async def test_trio_async116_helpers(): + import trio + + await trio.sleep(seconds=1) # OK + await trio.sleep(delay=1) # OK + + await trio.sleep(seconds=86401) # ASYNC116 + await trio.sleep(delay=86401) # OK + + +async def _(): + import trio + from trio import sleep + + await sleep(18446744073709551616) + await trio.sleep(99999999999999999999) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py index d7f9716ef1a6c..9dcd08ecec25b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py @@ -22,3 +22,8 @@ def my_func(): # Implicit string concatenation "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" + +# t-strings - all ok +t"0.0.0.0" +"0.0.0.0" t"0.0.0.0{expr}0.0.0.0" +"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py index 610a6700cdba7..ca73cd6879d7c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py @@ -40,3 +40,7 @@ with TemporaryDirectory(dir="/tmp") as d: pass + +# ok (runtime error from t-string) +with open(t"/foo/bar", "w") as f: + f.write("def") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S112.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S112.py index fccbc6dbd12e7..ef05496a9ecd1 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S112.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S112.py @@ -1,29 +1,30 @@ -try: - pass -except Exception: - continue +for _ in []: + try: + pass + except Exception: + continue -try: - pass -except: - continue + try: + pass + except: + continue -try: - pass -except (Exception,): - continue + try: + pass + except (Exception,): + continue -try: - pass -except (Exception, ValueError): - continue + try: + pass + except (Exception, ValueError): + continue -try: - pass -except ValueError: - continue + try: + pass + except ValueError: + continue -try: - pass -except (ValueError,): - continue + try: + pass + except (ValueError,): + continue diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py index 09f336fd9535c..f46d3d487243c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py @@ -39,3 +39,9 @@ # But non-instant are not. (e := "echo") run(e) + + +# https://github.com/astral-sh/ruff/issues/17798 +# Tuple literals are trusted +check_output(("literal", "cmd", "using", "tuple"), text=True) +Popen(("literal", "cmd", "using", "tuple")) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py index 2e96462c84778..620a18c038849 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py @@ -166,3 +166,16 @@ def query54(): foo FROM ({user_input}) raw """ + +# https://github.com/astral-sh/ruff/issues/17967 +query61 = f"SELECT * FROM table" # skip expressionless f-strings + +# t-strings +query62 = t"SELECT * FROM table" +query63 = t""" + SELECT *, + foo + FROM ({user_input}) raw +""" +query64 = f"update {t"{table}"} set var = {t"{var}"}" +query65 = t"update {f"{table}"} set var = {f"{var}"}" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py index 7748a0ac40c23..41cc8bc44b840 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py @@ -12,7 +12,7 @@ Markup(object="safe") Markup(object="unsafe {}".format(content)) # Not currently detected -# NOTE: We may be able to get rid of these false positives with red-knot +# NOTE: We may be able to get rid of these false positives with ty # if it includes comprehensive constant expression detection/evaluation. Markup("*" * 8) # S704 (false positive) flask.Markup("hello {}".format("world")) # S704 (false positive) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py index 83590228a6673..3a81f8c28b15c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B004.py @@ -29,3 +29,13 @@ def this_is_fine(): o = object() if callable(o): print("Ooh, this is actually callable.") + +# https://github.com/astral-sh/ruff/issues/18741 +# The autofix for this is unsafe due to the comments. +hasattr( + # comment 1 + obj, # comment 2 + # comment 3 + "__call__", # comment 4 + # comment 5 +) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py index c47538b55e1c2..ce6e5c291ecce 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B009_B010.py @@ -67,3 +67,6 @@ import builtins builtins.getattr(foo, "bar") + +# Regression test for: https://github.com/astral-sh/ruff/issues/18353 +setattr(foo, "__debug__", 0) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017.py deleted file mode 100644 index 99def7a555e13..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Should emit: -B017 - on lines 23 and 41 -""" -import asyncio -import unittest -import pytest - -CONSTANT = True - - -def something_else() -> None: - for i in (1, 2, 3): - print(i) - - -class Foo: - pass - - -class Foobar(unittest.TestCase): - def evil_raises(self) -> None: - with self.assertRaises(Exception): - raise Exception("Evil I say!") - - def also_evil_raises(self) -> None: - with self.assertRaises(BaseException): - raise Exception("Evil I say!") - - def context_manager_raises(self) -> None: - with self.assertRaises(Exception) as ex: - raise Exception("Context manager is good") - self.assertEqual("Context manager is good", str(ex.exception)) - - def regex_raises(self) -> None: - with self.assertRaisesRegex(Exception, "Regex is good"): - raise Exception("Regex is good") - - def raises_with_absolute_reference(self): - with self.assertRaises(asyncio.CancelledError): - Foo() - - -def test_pytest_raises(): - with pytest.raises(Exception): - raise ValueError("Hello") - - with pytest.raises(Exception), pytest.raises(ValueError): - raise ValueError("Hello") - - with pytest.raises(Exception, "hello"): - raise ValueError("This is fine") - - with pytest.raises(Exception, match="hello"): - raise ValueError("This is also fine") - - with contextlib.nullcontext(), pytest.raises(Exception): - raise ValueError("Multiple context managers") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_0.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_0.py new file mode 100644 index 0000000000000..9d706168a9a90 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_0.py @@ -0,0 +1,58 @@ +""" +Should emit: +B017 - on lines 24, 28, 46, 49, 52, and 58 +""" +import asyncio +import unittest +import pytest, contextlib + +CONSTANT = True + + +def something_else() -> None: + for i in (1, 2, 3): + print(i) + + +class Foo: + pass + + +class Foobar(unittest.TestCase): + def evil_raises(self) -> None: + with self.assertRaises(Exception): + raise Exception("Evil I say!") + + def also_evil_raises(self) -> None: + with self.assertRaises(BaseException): + raise Exception("Evil I say!") + + def context_manager_raises(self) -> None: + with self.assertRaises(Exception) as ex: + raise Exception("Context manager is good") + self.assertEqual("Context manager is good", str(ex.exception)) + + def regex_raises(self) -> None: + with self.assertRaisesRegex(Exception, "Regex is good"): + raise Exception("Regex is good") + + def raises_with_absolute_reference(self): + with self.assertRaises(asyncio.CancelledError): + Foo() + + +def test_pytest_raises(): + with pytest.raises(Exception): + raise ValueError("Hello") + + with pytest.raises(Exception), pytest.raises(ValueError): + raise ValueError("Hello") + + with pytest.raises(Exception, "hello"): + raise ValueError("This is fine") + + with pytest.raises(Exception, match="hello"): + raise ValueError("This is also fine") + + with contextlib.nullcontext(), pytest.raises(Exception): + raise ValueError("Multiple context managers") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_1.py new file mode 100644 index 0000000000000..8a383a92f2ae1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B017_1.py @@ -0,0 +1,28 @@ +""" +Should emit: +B017 - on lines 20, 21, 25, and 26 +""" +import unittest +import pytest + + +def something_else() -> None: + for i in (1, 2, 3): + print(i) + + +class Foo: + pass + + +class Foobar(unittest.TestCase): + def call_form_raises(self) -> None: + self.assertRaises(Exception, something_else) + self.assertRaises(BaseException, something_else) + + +def test_pytest_call_form() -> None: + pytest.raises(Exception, something_else) + pytest.raises(BaseException, something_else) + + pytest.raises(Exception, something_else, match="hello") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py index 943f5c4d93090..54362a5070eb6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py @@ -25,3 +25,11 @@ # some comments here source = None # no trailing comma ) + +# https://github.com/astral-sh/ruff/issues/18011 +warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),)) +# trigger diagnostic if `skip_file_prefixes` is present and set to the default value +warnings.warn("test", skip_file_prefixes=()) + +_my_prefixes = ("this","that") +warnings.warn("test", skip_file_prefixes = _my_prefixes) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B031.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B031.py index 412b890ef7b69..dfdafc6116e9f 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B031.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B031.py @@ -185,38 +185,45 @@ def collect_shop_items(shopper, items): collect_shop_items(shopper, section_items) # Shouldn't trigger the warning when there is a return statement. -for _section, section_items in groupby(items, key=lambda p: p[1]): - if _section == "greens": +def foo(): + for _section, section_items in groupby(items, key=lambda p: p[1]): + if _section == "greens": + collect_shop_items(shopper, section_items) + return + elif _section == "frozen items": + return section_items collect_shop_items(shopper, section_items) - return - elif _section == "frozen items": - return section_items - collect_shop_items(shopper, section_items) # Should trigger the warning for duplicate access, even if is a return statement after. -for _section, section_items in groupby(items, key=lambda p: p[1]): - if _section == "greens": - collect_shop_items(shopper, section_items) - collect_shop_items(shopper, section_items) - return +def foo(): + from itertools import groupby + for _section, section_items in groupby(items, key=lambda p: p[1]): + if _section == "greens": + collect_shop_items(shopper, section_items) + collect_shop_items(shopper, section_items) + return # Should trigger the warning for duplicate access, even if is a return in another branch. -for _section, section_items in groupby(items, key=lambda p: p[1]): - if _section == "greens": - collect_shop_items(shopper, section_items) - return - elif _section == "frozen items": - collect_shop_items(shopper, section_items) - collect_shop_items(shopper, section_items) +def foo(): + from itertools import groupby + for _section, section_items in groupby(items, key=lambda p: p[1]): + if _section == "greens": + collect_shop_items(shopper, section_items) + return + elif _section == "frozen items": + collect_shop_items(shopper, section_items) + collect_shop_items(shopper, section_items) # Should trigger, since only one branch has a return statement. -for _section, section_items in groupby(items, key=lambda p: p[1]): - if _section == "greens": - collect_shop_items(shopper, section_items) - return - elif _section == "frozen items": - collect_shop_items(shopper, section_items) - collect_shop_items(shopper, section_items) # B031 +def foo(): + from itertools import groupby + for _section, section_items in groupby(items, key=lambda p: p[1]): + if _section == "greens": + collect_shop_items(shopper, section_items) + return + elif _section == "frozen items": + collect_shop_items(shopper, section_items) + collect_shop_items(shopper, section_items) # B031 # Let's redefine the `groupby` function to make sure we pick up the correct one. # NOTE: This should always be at the end of the file. diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py index c9be4f036bd88..08de7b2f5be9d 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B909.py @@ -179,3 +179,17 @@ def func(): for elem in some_list: if some_list.pop() == 2: return + +# should not error - direct return with mutation (Issue #18399) +def fail_map(mapping): + for key in mapping: + return mapping.pop(key) + +def success_map(mapping): + for key in mapping: + ret = mapping.pop(key) # should not error + return ret + +def fail_list(seq): + for val in seq: + return seq.pop(4) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C401.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C401.py index 7d7ee4c556fbe..27721d8116350 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C401.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C401.py @@ -36,9 +36,19 @@ def f(x): # some more ) +# t-strings +print(t"Hello {set(f(a) for a in 'abc')} World") +print(t"Hello { set(f(a) for a in 'abc') } World") +small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +print(t"Hello {set(a for a in range(3))} World") +print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + + # Not built-in set. def set(*args, **kwargs): return None set(2 * x for x in range(3)) set(x for x in range(3)) + diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C403.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C403.py index c1a9feb24cdc1..6c1efbb4495f3 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C403.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C403.py @@ -34,4 +34,16 @@ def f(x): )))) # Test trailing comma case -s = set([x for x in range(3)],) \ No newline at end of file +s = set([x for x in range(3)],) + +s = t"{set([x for x in 'ab'])}" +s = t'{set([x for x in "ab"])}' + +def f(x): + return x + +s = t"{set([f(x) for x in 'ab'])}" + +s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" +s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C405.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C405.py index 9c250811b0044..adef711093454 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C405.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C405.py @@ -24,3 +24,12 @@ f"{ set(['a', 'b']) - set(['a']) }" f"a {set(['a', 'b']) - set(['a'])} b" f"a { set(['a', 'b']) - set(['a']) } b" + +t"{set([1,2,3])}" +t"{set(['a', 'b'])}" +t'{set(["a", "b"])}' + +t"{set(['a', 'b']) - set(['a'])}" +t"{ set(['a', 'b']) - set(['a']) }" +t"a {set(['a', 'b']) - set(['a'])} b" +t"a { set(['a', 'b']) - set(['a']) } b" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py index da0ebcaf713dc..c1ac839e27f3d 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py @@ -27,3 +27,13 @@ def list(): tuple( # comment ) + +t"{dict(x='y')}" +t'{dict(x="y")}' +t"{dict()}" +t"a {dict()} b" + +t"{dict(x='y') | dict(y='z')}" +t"{ dict(x='y') | dict(y='z') }" +t"a {dict(x='y') | dict(y='z')} b" +t"a { dict(x='y') | dict(y='z') } b" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C417.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C417.py index 8380774d9fd53..dd7b93f89054a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C417.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C417.py @@ -70,3 +70,8 @@ def func(arg1: int, arg2: int = 4): list(map(lambda x, y: x, [(1, 2), (3, 4)])) list(map(lambda: 1, "xyz")) list(map(lambda x, y: x, [(1, 2), (3, 4)])) + +# When inside t-string, then the fix should be surrounded by whitespace +_ = t"{set(map(lambda x: x % 2 == 0, nums))}" +_ = t"{dict(map(lambda v: (v, v**2), nums))}" + diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420.py index 8f971d2bf1f53..03a6f44628fa6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420.py @@ -90,3 +90,14 @@ def f(): def func(): {(a, b): a + b for (a, b) in [(1, 2), (3, 4)]} # OK + +# https://github.com/astral-sh/ruff/issues/18764 +{ # 1 +a # 2 +: # 3 +None # 4 +for # 5 +a # 6 +in # 7 +iterable # 8 +} # 9 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420_2.py new file mode 100644 index 0000000000000..04c5de2df478c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420_2.py @@ -0,0 +1,5 @@ +foo or{x: None for x in bar} + + +# C420 fix must make sure to insert a leading space if needed, +# See https://github.com/astral-sh/ruff/issues/18599 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_errmsg/EM101_byte_string.py b/crates/ruff_linter/resources/test/fixtures/flake8_errmsg/EM101_byte_string.py new file mode 100644 index 0000000000000..7761036a13c35 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_errmsg/EM101_byte_string.py @@ -0,0 +1,6 @@ +def f_byte(): + raise RuntimeError(b"This is an example exception") + + +def f_byte_empty(): + raise RuntimeError(b"") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_executable/EXE003_uv_tool.py b/crates/ruff_linter/resources/test/fixtures/flake8_executable/EXE003_uv_tool.py new file mode 100755 index 0000000000000..29fb1f5013cc1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_executable/EXE003_uv_tool.py @@ -0,0 +1,2 @@ +#!/usr/bin/env -S uv tool run ruff check --isolated --select EXE003 +print("hello world") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_executable/EXE003_uvx.py b/crates/ruff_linter/resources/test/fixtures/flake8_executable/EXE003_uvx.py new file mode 100755 index 0000000000000..2dbc2147bf2db --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_executable/EXE003_uvx.py @@ -0,0 +1,2 @@ +#!/usr/bin/env -S uvx ruff check --isolated --select EXE003 +print("hello world") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py index 68fc1e3d9c63c..09fba4a9db940 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py @@ -91,3 +91,99 @@ _ = "\12""8" # fix should be "\128" _ = "\12""foo" # fix should be "\12foo" _ = "\12" "" # fix should be "\12" + + +# Mixed literal + non-literal scenarios +_ = ( + "start" + + variable + + "end" +) + +_ = ( + f"format" + + func_call() + + "literal" +) + +_ = ( + rf"raw_f{x}" + + r"raw_normal" +) + + +# Different prefix combinations +_ = ( + u"unicode" + + r"raw" +) + +_ = ( + rb"raw_bytes" + + b"normal_bytes" +) + +_ = ( + b"bytes" + + b"with_bytes" +) + +# Repeated concatenation + +_ = ("a" + + "b" + + "c" + + "d" + "e" +) + +_ = ("a" + + "b" + + "c" + + "d" + + "e" +) + +_ = ( + "start" + + variable + # comment + "end" +) + +_ = ( + "start" + + variable + # leading comment + + "end" +) + +_ = ( + "first" + + "second" # extra spaces around + +) + +_ = ( + "first" + # trailing spaces before + + "second" +) + +_ = (( + "deep" + + "nesting" +)) + +_ = ( + "contains + plus" + + "another string" +) + +_ = ( + "start" + # leading comment + + "end" +) + +_ = ( + "start" + + # leading comment + "end" +) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_logging/LOG014_0.py b/crates/ruff_linter/resources/test/fixtures/flake8_logging/LOG014_0.py index b3dc5f79bb458..5eb188a98729b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_logging/LOG014_0.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_logging/LOG014_0.py @@ -32,6 +32,12 @@ def _(): ### No errors +logging.info("", exc_info=ValueError()) +logger.info("", exc_info=ValueError()) + +logging.info("", exc_info=(exc_type, exc_value, exc_traceback)) +logger.info("", exc_info=(exc_type, exc_value, exc_traceback)) + logging.info("", exc_info=a) logger.info("", exc_info=a) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE804.py b/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE804.py index a8233d2917f92..57f243e46beb6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE804.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE804.py @@ -26,5 +26,17 @@ abc(a=1, **{'a': c}, **{'b': c}) # PIE804 # Some values need to be parenthesized. -abc(foo=1, **{'bar': (bar := 1)}) # PIE804 -abc(foo=1, **{'bar': (yield 1)}) # PIE804 +def foo(): + abc(foo=1, **{'bar': (bar := 1)}) # PIE804 + abc(foo=1, **{'bar': (yield 1)}) # PIE804 + +# https://github.com/astral-sh/ruff/issues/18036 +# The autofix for this is unsafe due to the comments inside the dictionary. +foo( + **{ + # Comment 1 + "x": 1.0, + # Comment 2 + "y": 2.0, + } +) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE808.py b/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE808.py index 2ba1a545e6dba..83ee814b6e124 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE808.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE808.py @@ -14,3 +14,6 @@ range(0, 10, step=1) range(start=0, stop=10) range(0, stop=10) + +# regression test for https://github.com/astral-sh/ruff/pull/18805 +range((0), 42) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py index 75633e423b37b..89efd61e8f86c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.py @@ -119,4 +119,26 @@ def func2() -> str | str: # PYI016: Duplicate union member `str` # Technically, this falls into the domain of the rule but it is an unlikely edge case, # only works if you have from `__future__ import annotations` at the top of the file, # and stringified annotations are discouraged in stub files. -field36: "int | str" | int # Ok \ No newline at end of file +field36: "int | str" | int # Ok + +# https://github.com/astral-sh/ruff/issues/18546 +# Expand Optional[T] to Union[T, None] +# OK +field37: typing.Optional[int] +field38: typing.Union[int, None] +# equivalent to None +field39: typing.Optional[None] +# equivalent to int | None +field40: typing.Union[typing.Optional[int], None] +field41: typing.Optional[typing.Union[int, None]] +field42: typing.Union[typing.Optional[int], typing.Optional[int]] +field43: typing.Optional[int] | None +field44: typing.Optional[int | None] +field45: typing.Optional[int] | typing.Optional[int] +# equivalent to int | dict | None +field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +field47: typing.Optional[int] | typing.Optional[dict] + +# avoid reporting twice +field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +field49: typing.Optional[complex | complex] | complex diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi index a4ff51f5dea67..07ed45bbd3845 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI016.pyi @@ -111,3 +111,25 @@ field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error # Test case for mixed union type field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +# https://github.com/astral-sh/ruff/issues/18546 +# Expand Optional[T] to Union[T, None] +# OK +field37: typing.Optional[int] +field38: typing.Union[int, None] +# equivalent to None +field39: typing.Optional[None] +# equivalent to int | None +field40: typing.Union[typing.Optional[int], None] +field41: typing.Optional[typing.Union[int, None]] +field42: typing.Union[typing.Optional[int], typing.Optional[int]] +field43: typing.Optional[int] | None +field44: typing.Optional[int | None] +field45: typing.Optional[int] | typing.Optional[int] +# equivalent to int | dict | None +field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +field47: typing.Optional[int] | typing.Optional[dict] + +# avoid reporting twice +field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +field49: typing.Optional[complex | complex] | complex diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py index e502d8fc1f79d..0762e9f15c486 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.py @@ -181,3 +181,34 @@ def m[S](self: S) -> S: class MetaTestClass(type): def m(cls: MetaType) -> MetaType: return cls + + +from __future__ import annotations + +class BadClassWithStringTypeHints: + def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + + @classmethod + def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + + +class BadSubscriptReturnTypeWithStringTypeHints: + @classmethod + def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + + +class GoodClassWiStringTypeHints: + @classmethod + def good_cls_method_with_mixed_annotations(cls: "type[Self]", arg: str) -> Self: ... + @staticmethod + def good_static_method_with_string_annotations(arg: "_S") -> "_S": ... + @classmethod + def good_class_method_with_args_string_annotations(cls, arg1: "_S", arg2: "_S") -> "_S": ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi index 1f2f17fc1b303..bffdb13dbb558 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI019_0.pyi @@ -172,3 +172,36 @@ MetaType = TypeVar("MetaType") class MetaTestClass(type): def m(cls: MetaType) -> MetaType: return cls + + +from __future__ import annotations + + +class BadClassWithStringTypeHints: + def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + + @classmethod + def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + + + @classmethod + def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + + +class BadSubscriptReturnTypeWithStringTypeHints: + @classmethod + def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + + +class GoodClassWithStringTypeHints: + @classmethod + def good_cls_method_with_mixed_annotations(cls: "type[Self]", arg: str) -> Self: ... + @staticmethod + def good_static_method_with_string_annotations(arg: "_S") -> "_S": ... + @classmethod + def good_class_method_with_args_string_annotations(cls, arg1: "_S", arg2: "_S") -> "_S": ... + diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041.py deleted file mode 100644 index c5b367644d8ba..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import ( - Union, -) - -from typing_extensions import ( - TypeAlias, -) - -TA0: TypeAlias = int -TA1: TypeAlias = int | float | bool -TA2: TypeAlias = Union[int, float, bool] - - -def good1(arg: int) -> int | bool: - ... - - -def good2(arg: int, arg2: int | bool) -> None: - ... - - -def f0(arg1: float | int) -> None: - ... - - -def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: - ... - - -def f2(arg1: int, /, arg2: int | int | float) -> None: - ... - - -def f3(arg1: int, *args: Union[int | int | float]) -> None: - ... - - -async def f4(**kwargs: int | int | float) -> None: - ... - - -def f5(arg1: int, *args: Union[int, int, float]) -> None: - ... - - -def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: - ... - - -def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: - ... - - -def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: - ... - - -def f9( - arg: Union[ # comment - float, # another - complex, int] - ) -> None: - ... - -def f10( - arg: ( - int | # comment - float | # another - complex - ) - ) -> None: - ... - - -class Foo: - def good(self, arg: int) -> None: - ... - - def bad(self, arg: int | float | complex) -> None: - ... - - def bad2(self, arg: int | Union[float, complex]) -> None: - ... - - def bad3(self, arg: Union[Union[float, complex], int]) -> None: - ... - - def bad4(self, arg: Union[float | complex, int]) -> None: - ... - - def bad5(self, arg: int | (float | complex)) -> None: - ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041.pyi deleted file mode 100644 index e465c3eb90076..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041.pyi +++ /dev/null @@ -1,72 +0,0 @@ -from typing import ( - Union, -) - -from typing_extensions import ( - TypeAlias, -) - -# Type aliases not flagged -TA0: TypeAlias = int -TA1: TypeAlias = int | float | bool -TA2: TypeAlias = Union[int, float, bool] - - -def good1(arg: int) -> int | bool: ... - - -def good2(arg: int, arg2: int | bool) -> None: ... - - -def f0(arg1: float | int) -> None: ... # PYI041 - - -def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 - - -def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 - - -def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 - - -async def f4(**kwargs: int | int | float) -> None: ... # PYI041 - -def f5( - arg: Union[ # comment - float, # another - complex, int] - ) -> None: ... # PYI041 - -def f6( - arg: ( - int | # comment - float | # another - complex - ) - ) -> None: ... # PYI041 - -def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 - - -def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 - - -def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 - - -def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 - - -class Foo: - def good(self, arg: int) -> None: ... - - def bad(self, arg: int | float | complex) -> None: ... # PYI041 - - def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 - - def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 - - def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 - - def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_1.py new file mode 100644 index 0000000000000..b952f46ca00d4 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_1.py @@ -0,0 +1,112 @@ +from typing import ( + TYPE_CHECKING, + Union, +) + +from typing_extensions import ( + TypeAlias, +) + +TA0: TypeAlias = int +TA1: TypeAlias = int | float | bool +TA2: TypeAlias = Union[int, float, bool] + + +def good1(arg: int) -> int | bool: + ... + + +def good2(arg: int, arg2: int | bool) -> None: + ... + + +def f0(arg1: float | int) -> None: + ... + + +def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: + ... + + +def f2(arg1: int, /, arg2: int | int | float) -> None: + ... + + +def f3(arg1: int, *args: Union[int | int | float]) -> None: + ... + + +async def f4(**kwargs: int | int | float) -> None: + ... + + +def f5(arg1: int, *args: Union[int, int, float]) -> None: + ... + + +def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: + ... + + +def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: + ... + + +def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: + ... + + +def f9( + arg: Union[ # comment + float, # another + complex, int] + ) -> None: + ... + +def f10( + arg: ( + int | # comment + float | # another + complex + ) + ) -> None: + ... + + +class Foo: + def good(self, arg: int) -> None: + ... + + def bad(self, arg: int | float | complex) -> None: + ... + + def bad2(self, arg: int | Union[float, complex]) -> None: + ... + + def bad3(self, arg: Union[Union[float, complex], int]) -> None: + ... + + def bad4(self, arg: Union[float | complex, int]) -> None: + ... + + def bad5(self, arg: int | (float | complex)) -> None: + ... + + +# https://github.com/astral-sh/ruff/issues/18298 +# fix must not yield runtime `None | None | ...` (TypeError) +class Issue18298: + def f1(self, arg: None | int | None | float = None) -> None: # PYI041 - no fix + pass + + if TYPE_CHECKING: + + def f2(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix + + else: + + def f2(self, arg=None) -> None: + pass + + def f3(self, arg: None | float | None | int | None = None) -> None: # PYI041 - with fix + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_1.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_1.pyi new file mode 100644 index 0000000000000..22acb5571a966 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_1.pyi @@ -0,0 +1,80 @@ +from typing import ( + Union, +) + +from typing_extensions import ( + TypeAlias, +) + +# Type aliases not flagged +TA0: TypeAlias = int +TA1: TypeAlias = int | float | bool +TA2: TypeAlias = Union[int, float, bool] + + +def good1(arg: int) -> int | bool: ... + + +def good2(arg: int, arg2: int | bool) -> None: ... + + +def f0(arg1: float | int) -> None: ... # PYI041 + + +def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 + + +def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 + + +def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 + + +async def f4(**kwargs: int | int | float) -> None: ... # PYI041 + +def f5( + arg: Union[ # comment + float, # another + complex, int] + ) -> None: ... # PYI041 + +def f6( + arg: ( + int | # comment + float | # another + complex + ) + ) -> None: ... # PYI041 + +def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 + + +def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 + + +def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 + + +def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 + + +class Foo: + def good(self, arg: int) -> None: ... + + def bad(self, arg: int | float | complex) -> None: ... # PYI041 + + def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 + + def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 + + def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 + + def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 + + +# https://github.com/astral-sh/ruff/issues/18298 +# fix must not yield runtime `None | None | ...` (TypeError) +class Issue18298: + def f1(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix + + def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_2.py new file mode 100644 index 0000000000000..af92b93f8c918 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI041_2.py @@ -0,0 +1,8 @@ +from __future__ import annotations + + +# https://github.com/astral-sh/ruff/issues/18298 +# fix must not yield runtime `None | None | ...` (TypeError) +class Issue18298: + def f1(self, arg: None | int | None | float = None) -> None: + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py index 12cac66d132c9..b3fbf5ab9e446 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py @@ -72,3 +72,5 @@ def not_warnings_dot_deprecated( @not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!") def not_a_deprecated_function() -> None: ... + +baz: str = t"51 character stringgggggggggggggggggggggggggggggggg" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi index 5cb3585c57760..caf9f55e97ea7 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi @@ -80,3 +80,7 @@ x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooo # Ok y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + +ttoo: str = t"50 character stringggggggggggggggggggggggggggggggg" # OK + +tbar: str = t"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI059.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI059.py index 0e4d877d4b485..cfe42a12d3836 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI059.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI059.py @@ -52,3 +52,15 @@ def __init__(self) -> None: class SomeGeneric(Generic[T]): # Only one generic pass + + +# syntax errors with starred and keyword arguments from +# https://github.com/astral-sh/ruff/issues/18602 +class C1(Generic[T], str, **{"metaclass": type}): # PYI059 + ... + +class C2(Generic[T], str, metaclass=type): # PYI059 + ... + +class C3(Generic[T], metaclass=type, *[str]): # PYI059 but no fix + ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py index 51baabe892107..4bdc3d98799b9 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py @@ -78,4 +78,3 @@ def good_func(arg1: Literal[int] | None): c: (None | Literal[None]) | None d: None | (Literal[None] | None) e: None | ((None | Literal[None]) | None) | None -f: Literal[None] | Literal[None] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi index ed879dd646e34..404ac1157edb9 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi @@ -53,4 +53,3 @@ b: None | Literal[None] | None c: (None | Literal[None]) | None d: None | (Literal[None] | None) e: None | ((None | Literal[None]) | None) | None -f: Literal[None] | Literal[None] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT001.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT001.py index 49d53fcb8eef8..fd98ced71b203 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT001.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT001.py @@ -76,3 +76,20 @@ def aliased_parentheses_with_params(): ) def aliased_parentheses_no_params_multiline(): return 42 + +# https://github.com/astral-sh/ruff/issues/18770 +@pytest.fixture( + # TODO: use module scope + # scope="module" +) +def my_fixture(): ... + + +@(pytest.fixture()) +def outer_paren_fixture_no_params(): + return 42 + + +@(fixture()) +def outer_paren_imported_fixture_no_params(): + return 42 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT019.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT019.py index 1df67fd7f8958..2bdbe93116ca7 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT019.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT019.py @@ -12,3 +12,38 @@ def test_xxx(_fixture): # Error arg def test_xxx(*, _fixture): # Error kwonly pass + +# https://github.com/astral-sh/ruff/issues/17599 + +@pytest.mark.parametrize("_foo", [1, 2, 3]) +def test_thingy(_foo): # Ok defined in parametrize + pass + +@pytest.mark.parametrize( + "_test_input,_expected", + [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], +) +def test_eval(_test_input, _expected): # OK defined in parametrize + pass + + +@pytest.mark.parametrize("_foo", [1, 2, 3]) +def test_thingy2(_foo, _bar): # Error _bar is not defined in parametrize + pass + +@pytest.mark.parametrize(["_foo", "_bar"], [1, 2, 3]) +def test_thingy3(_foo, _bar): # OK defined in parametrize + pass + +@pytest.mark.parametrize(("_foo"), [1, 2, 3]) +def test_thingy4(_foo, _bar): # Error _bar is not defined in parametrize + pass + +@pytest.mark.parametrize([" _foo", " _bar "], [1, 2, 3]) +def test_thingy5(_foo, _bar): # OK defined in parametrize + pass + +x = "other" +@pytest.mark.parametrize(x, [1, 2, 3]) +def test_thingy5(_foo, _bar): # known false negative, we don't try to resolve variables + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py index 91a06059420a6..042dbcb3935cb 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py @@ -1,3 +1,8 @@ +@pytest.mark.foo(scope="module") +def ok_due_to_missing_import(): + pass + + import pytest @@ -72,3 +77,25 @@ class TestNestedClass: @pytest.mark.foo() def test_something(): pass + +# https://github.com/astral-sh/ruff/issues/18770 +@pytest.mark.parametrize( + # TODO: fix later + # ("param1", "param2"), + # ( + # (1, 2), + # (3, 4), + # ), +) +def test_bar(param1, param2): ... + + +@(pytest.mark.foo()) +def test_outer_paren_mark_function(): + pass + + +class TestClass: + @(pytest.mark.foo()) + def test_method_outer_paren(): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py index 7f789a22fbad9..92a2744f4bf88 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py @@ -39,3 +39,27 @@ f'\'normal\' {f'nested'} "double quotes"' f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003 f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l + + + +# Same as above, but with t-strings +t'This is a \'string\'' # Q003 +t'This is \\ a \\\'string\'' # Q003 +t'"This" is a \'string\'' +f"This is a 'string'" +f"\"This\" is a 'string'" +fr'This is a \'string\'' +fR'This is a \'string\'' +foo = ( + t'This is a' + t'\'string\'' # Q003 +) +t'\'foo\' {'nested'}' # Q003 +t'\'foo\' {t'nested'}' # Q003 +t'\'foo\' {t'\'nested\''} \'\'' # Q003 + +t'normal {t'nested'} normal' +t'\'normal\' {t'nested'} normal' # Q003 +t'\'normal\' {t'nested'} "double quotes"' +t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py index 815db5bdb7af9..d68017f3805f5 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py @@ -37,3 +37,25 @@ f"\"normal\" {f"nested"} 'single quotes'" f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003 f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + + +# Same as above, but with t-strings +t"This is a \"string\"" +t"'This' is a \"string\"" +f'This is a "string"' +f'\'This\' is a "string"' +fr"This is a \"string\"" +fR"This is a \"string\"" +foo = ( + t"This is a" + t"\"string\"" +) +t"\"foo\" {"foo"}" # Q003 +t"\"foo\" {t"foo"}" # Q003 +t"\"foo\" {t"\"foo\""} \"\"" # Q003 + +t"normal {t"nested"} normal" +t"\"normal\" {t"nested"} normal" # Q003 +t"\"normal\" {t"nested"} 'single quotes'" +t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_raise/RSE102.py b/crates/ruff_linter/resources/test/fixtures/flake8_raise/RSE102.py index 0d737c8ca4fb0..003dc61b7da8f 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_raise/RSE102.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_raise/RSE102.py @@ -105,3 +105,8 @@ def func(): future = executor.submit(float, "a") if future.exception(): raise future.Exception() + + +raise TypeError( + # comment +) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_return/RET501.py b/crates/ruff_linter/resources/test/fixtures/flake8_return/RET501.py index 70346bef98686..6c60a6499faef 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_return/RET501.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_return/RET501.py @@ -49,3 +49,13 @@ def prop3(self) -> None: def prop4(self) -> None: print("I've run out of things to say") return None + + +# https://github.com/astral-sh/ruff/issues/18774 +class _: + def foo(bar): + if not bar: + return + return ( + None # comment + ) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py b/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py index 91a60a7540dc9..b26739c3708a3 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py @@ -421,3 +421,43 @@ def func(a: dict[str, int]) -> list[dict[str, int]]: if "services" in a: services = a["services"] return services + + +# See: https://github.com/astral-sh/ruff/issues/14052 +def outer() -> list[object]: + @register + async def inner() -> None: + print(layout) + + layout = [...] + return layout + +def outer() -> list[object]: + with open("") as f: + async def inner() -> None: + print(layout) + + layout = [...] + return layout + + +def outer() -> list[object]: + def inner(): + with open("") as f: + async def inner_inner() -> None: + print(layout) + + layout = [...] + return layout + + +# See: https://github.com/astral-sh/ruff/issues/18411 +def f(): + (#= + x) = 1 + return x + +def f(): + x = (1 + ) + return x diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM105_0.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM105_0.py index 7d63806c61c2f..a893af9add764 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM105_0.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM105_0.py @@ -128,3 +128,16 @@ def write_models(directory, Models): pass; \ \ # + +# Regression tests for: https://github.com/astral-sh/ruff/issues/18209 +try: + 1 / 0 +except (): + pass + + +BaseException = ValueError +try: + int("a") +except BaseException: + pass \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM115.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM115.py index 88af984ce4684..4f973a647d86f 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM115.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM115.py @@ -27,8 +27,9 @@ close_files = stack.pop_all().close # OK -with contextlib.AsyncExitStack() as exit_stack: - f = await exit_stack.enter_async_context(open("filename")) +async def foo(): + with contextlib.AsyncExitStack() as exit_stack: + f = await exit_stack.enter_async_context(open("filename")) # OK (false negative) with contextlib.ExitStack(): @@ -275,9 +276,10 @@ def setUpClass(cls): cls.enterClassContext(open("filename")) # OK -class ExampleAsyncTests(IsolatedAsyncioTestCase): - async def test_something(self): - await self.enterAsyncContext(open("filename")) +async def foo(): + class ExampleAsyncTests(IsolatedAsyncioTestCase): + async def test_something(self): + await self.enterAsyncContext(open("filename")) # OK class ExampleTests(TestCase): diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM116.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM116.py index 155c3fccc40d0..c313689c02429 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM116.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM116.py @@ -1,98 +1,99 @@ -# Errors -a = "hello" +def foo(): + # Errors + a = "hello" -# SIM116 -if a == "foo": - return "bar" -elif a == "bar": - return "baz" -elif a == "boo": - return "ooh" -else: - return 42 + # SIM116 + if a == "foo": + return "bar" + elif a == "bar": + return "baz" + elif a == "boo": + return "ooh" + else: + return 42 -# SIM116 -if a == 1: - return (1, 2, 3) -elif a == 2: - return (4, 5, 6) -elif a == 3: - return (7, 8, 9) -else: - return (10, 11, 12) + # SIM116 + if a == 1: + return (1, 2, 3) + elif a == 2: + return (4, 5, 6) + elif a == 3: + return (7, 8, 9) + else: + return (10, 11, 12) -# SIM116 -if a == 1: - return (1, 2, 3) -elif a == 2: - return (4, 5, 6) -elif a == 3: - return (7, 8, 9) + # SIM116 + if a == 1: + return (1, 2, 3) + elif a == 2: + return (4, 5, 6) + elif a == 3: + return (7, 8, 9) -# SIM116 -if a == "hello 'sir'": - return (1, 2, 3) -elif a == 'goodbye "mam"': - return (4, 5, 6) -elif a == """Fairwell 'mister'""": - return (7, 8, 9) -else: - return (10, 11, 12) + # SIM116 + if a == "hello 'sir'": + return (1, 2, 3) + elif a == 'goodbye "mam"': + return (4, 5, 6) + elif a == """Fairwell 'mister'""": + return (7, 8, 9) + else: + return (10, 11, 12) -# SIM116 -if a == b"one": - return 1 -elif a == b"two": - return 2 -elif a == b"three": - return 3 + # SIM116 + if a == b"one": + return 1 + elif a == b"two": + return 2 + elif a == b"three": + return 3 -# SIM116 -if a == "hello 'sir'": - return ("hello'", 'hi"', 3) -elif a == 'goodbye "mam"': - return (4, 5, 6) -elif a == """Fairwell 'mister'""": - return (7, 8, 9) -else: - return (10, 11, 12) + # SIM116 + if a == "hello 'sir'": + return ("hello'", 'hi"', 3) + elif a == 'goodbye "mam"': + return (4, 5, 6) + elif a == """Fairwell 'mister'""": + return (7, 8, 9) + else: + return (10, 11, 12) -# OK -if a == "foo": - return "bar" -elif a == "bar": - return baz() -elif a == "boo": - return "ooh" -else: - return 42 + # OK + if a == "foo": + return "bar" + elif a == "bar": + return baz() + elif a == "boo": + return "ooh" + else: + return 42 -# OK -if a == b"one": - return 1 -elif b == b"two": - return 2 -elif a == b"three": - return 3 + # OK + if a == b"one": + return 1 + elif b == b"two": + return 2 + elif a == b"three": + return 3 -# SIM116 -if func_name == "create": - return "A" -elif func_name == "modify": - return "M" -elif func_name == "remove": - return "D" -elif func_name == "move": - return "MV" + # SIM116 + if func_name == "create": + return "A" + elif func_name == "modify": + return "M" + elif func_name == "remove": + return "D" + elif func_name == "move": + return "MV" -# OK -def no_return_in_else(platform): - if platform == "linux": - return "auditwheel repair -w {dest_dir} {wheel}" - elif platform == "macos": - return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" - elif platform == "windows": - return "" - else: - msg = f"Unknown platform: {platform!r}" - raise ValueError(msg) + # OK + def no_return_in_else(platform): + if platform == "linux": + return "auditwheel repair -w {dest_dir} {wheel}" + elif platform == "macos": + return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" + elif platform == "windows": + return "" + else: + msg = f"Unknown platform: {platform!r}" + raise ValueError(msg) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py index 7d767d475385b..049b60dfaffc6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py @@ -31,7 +31,7 @@ " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] "".split() # [] -""" +""" """.split() # [] " ".split() # [] "/abc/".split() # ["/abc/"] @@ -73,7 +73,7 @@ # negatives -# invalid values should not cause panic +# invalid values should not cause panic "a,b,c,d".split(maxsplit="hello") "a,b,c,d".split(maxsplit=-"hello") @@ -106,3 +106,27 @@ '''itemC''' "'itemD'" """.split() + +# https://github.com/astral-sh/ruff/issues/18042 +print("a,b".rsplit(",")) +print("a,b,c".rsplit(",", 1)) + +# https://github.com/astral-sh/ruff/issues/18069 + +print("".split(maxsplit=0)) +print("".split(sep=None, maxsplit=0)) +print(" ".split(maxsplit=0)) +print(" ".split(sep=None, maxsplit=0)) +print(" x ".split(maxsplit=0)) +print(" x ".split(sep=None, maxsplit=0)) +print(" x ".split(maxsplit=0)) +print(" x ".split(sep=None, maxsplit=0)) +print("".rsplit(maxsplit=0)) +print("".rsplit(sep=None, maxsplit=0)) +print(" ".rsplit(maxsplit=0)) +print(" ".rsplit(sep=None, maxsplit=0)) +print(" x ".rsplit(maxsplit=0)) +print(" x ".rsplit(maxsplit=0)) +print(" x ".rsplit(sep=None, maxsplit=0)) +print(" x ".rsplit(maxsplit=0)) +print(" x ".rsplit(sep=None, maxsplit=0)) \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM910.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM910.py index 31c4af016f7c7..550aa2c22c47a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM910.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM910.py @@ -49,3 +49,9 @@ def foo(some_other: object): # OK def foo(some_other): a = some_other.get('a', None) + + +# https://github.com/astral-sh/ruff/issues/18777 +def foo(): + dict = {"Tom": 23, "Maria": 23, "Dog": 11} + age = dict.get("Cat", None) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py index 6a96e6c86066d..3dd52b41398f4 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM911.py @@ -23,3 +23,14 @@ def foo(d: dict[str, str]) -> None: items = zip(x.keys(), x.values()) # OK items.bar = zip(x.keys(), x.values()) # OK + +# https://github.com/astral-sh/ruff/issues/18777 +def foo(): + dict = {} + for country, stars in zip(dict.keys(), dict.values()): + ... + + +# https://github.com/astral-sh/ruff/issues/18776 +flag_stars = {} +for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py index cf10553c49199..a321f2a30e682 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py @@ -89,3 +89,11 @@ def f(): int # TC006 , 6.0 ) + + +def f(): + # Keyword arguments + from typing import cast + + cast(typ=int, val=3.0) # TC006 + cast(val=3.0, typ=int) # TC006 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.py index b6ca0e8e5da62..2b17d7cb0db95 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.py @@ -50,3 +50,23 @@ class Baz: class Nested: a: TypeAlias = 'Baz' # OK type A = 'Baz' # TC008 + +# O should have parenthesis added +o: TypeAlias = """int +| None""" +type O = """int +| None""" + +# P, Q, and R should not have parenthesis added +p: TypeAlias = ("""int +| None""") +type P = ("""int +| None""") + +q: TypeAlias = """(int +| None)""" +type Q = """(int +| None)""" + +r: TypeAlias = """int | None""" +type R = """int | None""" \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py new file mode 100644 index 0000000000000..6cbd20f6397a9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py @@ -0,0 +1,6 @@ +# Regression test for: https://github.com/astral-sh/ruff/issues/19175 +# there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash +from typing import TYPE_CHECKING \ + +if TYPE_CHECKING: import builtins +builtins.print("!") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202.py index ca0bd47a6b931..1379cdf20300e 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202.py @@ -2,13 +2,81 @@ from pathlib import Path from os.path import getsize +filename = "filename" +filename1 = __file__ +filename2 = Path("filename") + os.path.getsize("filename") os.path.getsize(b"filename") os.path.getsize(Path("filename")) os.path.getsize(__file__) +os.path.getsize(filename) +os.path.getsize(filename1) +os.path.getsize(filename2) + +os.path.getsize(filename="filename") +os.path.getsize(filename=b"filename") +os.path.getsize(filename=Path("filename")) +os.path.getsize(filename=__file__) + getsize("filename") getsize(b"filename") getsize(Path("filename")) getsize(__file__) + +getsize(filename="filename") +getsize(filename=b"filename") +getsize(filename=Path("filename")) +getsize(filename=__file__) + +getsize(filename) +getsize(filename1) +getsize(filename2) + + +os.path.getsize( + "filename", # comment +) + +os.path.getsize( + # comment + "filename" + , + # comment +) + +os.path.getsize( + # comment + b"filename" + # comment +) + +os.path.getsize( # comment + Path(__file__) + # comment +) # comment + +getsize( # comment + "filename") + +getsize( # comment + b"filename", + #comment +) + +os.path.getsize("file" + "name") + +getsize \ +\ +\ + ( # comment + "filename", + ) + +getsize(Path("filename").resolve()) + +import pathlib + +os.path.getsize(pathlib.Path("filename")) \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202_2.py new file mode 100644 index 0000000000000..1544c791aa42c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH202_2.py @@ -0,0 +1,5 @@ +import os + +os.path.getsize(filename="filename") +os.path.getsize(filename=b"filename") +os.path.getsize(filename=__file__) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH203.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH203.py index 19cd4f358398a..7c499bfbc32b6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH203.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH203.py @@ -1,4 +1,4 @@ -import os.path +import os.path, pathlib from pathlib import Path from os.path import getatime @@ -10,3 +10,26 @@ getatime("filename") getatime(b"filename") getatime(Path("filename")) + + +file = __file__ + +os.path.getatime(file) +os.path.getatime(filename="filename") +os.path.getatime(filename=Path("filename")) + +os.path.getatime( # comment 1 + # comment 2 + "filename" # comment 3 + # comment 4 + , # comment 5 + # comment 6 +) # comment 7 + +os.path.getatime("file" + "name") + +getatime(Path("filename").resolve()) + +os.path.getatime(pathlib.Path("filename")) + +getatime(Path("dir") / "file.txt") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py index 752e924b8bfd2..0005327d83c03 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py @@ -9,3 +9,7 @@ glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")) list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))) search("*.png") + +# if `dir_fd` is set, suppress the diagnostic +glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1) +list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1)) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py index 8a31b2b472e05..3bdf05ae45f6c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py @@ -21,3 +21,6 @@ if "file" in os.listdir("dir"): ... + +os.listdir(1) +os.listdir(path=1) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH211.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH211.py new file mode 100644 index 0000000000000..5acf2febe27b1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH211.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path + + +os.symlink("usr/bin/python", "tmp/python") +os.symlink(b"usr/bin/python", b"tmp/python") +Path("tmp/python").symlink_to("usr/bin/python") # Ok + +os.symlink("usr/bin/python", "tmp/python", target_is_directory=True) +os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True) +Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok + +fd = os.open(".", os.O_RDONLY) +os.symlink("source.txt", "link.txt", dir_fd=fd) # Ok: dir_fd is not supported by pathlib +os.close(fd) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py index 20197b9703863..04bc90b801cbc 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py @@ -55,3 +55,52 @@ def opener(path, flags): open(x) def foo(y: int): open(y) + + +# https://github.com/astral-sh/ruff/issues/17691 +def f() -> int: + return 1 +open(f()) + +open(b"foo") +byte_str = b"bar" +open(byte_str) + +def bytes_str_func() -> bytes: + return b"foo" +open(bytes_str_func()) + +# https://github.com/astral-sh/ruff/issues/17693 +os.stat(1) +os.stat(x) + + +def func() -> int: + return 2 +os.stat(func()) + + +def bar(x: int): + os.stat(x) + +# https://github.com/astral-sh/ruff/issues/17694 +os.rename("src", "dst", src_dir_fd=3, dst_dir_fd=4) +os.rename("src", "dst", src_dir_fd=3) +os.rename("src", "dst", dst_dir_fd=4) + +# if `dir_fd` is set, suppress the diagnostic +os.readlink(p, dir_fd=1) +os.stat(p, dir_fd=2) +os.unlink(p, dir_fd=3) +os.remove(p, dir_fd=4) +os.rmdir(p, dir_fd=5) +os.mkdir(p, dir_fd=6) +os.chmod(p, dir_fd=7) +# `chmod` can also receive a file descriptor in the first argument +os.chmod(8) +os.chmod(x) + +# if `src_dir_fd` or `dst_dir_fd` are set, suppress the diagnostic +os.replace("src", "dst", src_dir_fd=1, dst_dir_fd=2) +os.replace("src", "dst", src_dir_fd=1) +os.replace("src", "dst", dst_dir_fd=2) diff --git a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N804.py b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N804.py index 3fbf2d7c70229..3001365404d89 100644 --- a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N804.py +++ b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N804.py @@ -81,3 +81,9 @@ def func(x): class Bar(type(foo)): def foo_method(self): pass + +# https://github.com/astral-sh/ruff/issues/18459 +class Example: + @classmethod + def function(this): + cls = 1234 diff --git a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py index d350a39c1236f..03f9d9746e350 100644 --- a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py +++ b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py @@ -134,3 +134,9 @@ def __subclasscheck__(cls, other): ... class MyProtocolMeta(type(Protocol)): def __subclasscheck__(cls, other): ... + + +# https://github.com/astral-sh/ruff/issues/18459 +class C: + def f(this): + self = 123 diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF101.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF101.py index b9ef94dbc3ea1..706eae62f4c8a 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF101.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF101.py @@ -85,3 +85,19 @@ for i in builtins.list(nested_tuple): # PERF101 pass + +# https://github.com/astral-sh/ruff/issues/18783 +items = (1, 2, 3) +for i in(list)(items): + print(i) + +# https://github.com/astral-sh/ruff/issues/18784 +items = (1, 2, 3) +for i in ( # 1 + list # 2 + # 3 +)( # 4 + items # 5 + # 6 +): + print(i) diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py index 24ff17e6c7e41..880ca4c09a091 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py @@ -144,14 +144,14 @@ def f(): def f(): # make sure that `tmp` is not deleted - tmp = 1; result = [] # commment should be protected + tmp = 1; result = [] # comment should be protected for i in range(10): result.append(i + 1) # PERF401 def f(): # make sure that `tmp` is not deleted - result = []; tmp = 1 # commment should be protected + result = []; tmp = 1 # comment should be protected for i in range(10): result.append(i + 1) # PERF401 @@ -260,3 +260,21 @@ def f(): for i in range(5): if j := i: items.append(j) + +def f(): + values = [1, 2, 3] + result = list() # this should be replaced with a comprehension + for i in values: + result.append(i + 1) # PERF401 + +def f(): + src = [1] + dst = [] + + for i in src: + if True if True else False: + dst.append(i) + + for i in src: + if lambda: 0: + dst.append(i) diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py index 8ef430c8fc9f3..868b268ed153b 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py @@ -18,7 +18,9 @@ def foo(): result = {} for idx, name in enumerate(fruit): if idx % 2: - result[idx] = name # Ok (false negative: edge case where `else` is same as `if`) + result[idx] = ( + name # Ok (false negative: edge case where `else` is same as `if`) + ) else: result[idx] = name @@ -85,7 +87,108 @@ def foo(): def foo(): from builtins import dict as SneakyDict + fruit = ["apple", "pear", "orange"] result = SneakyDict() for idx, name in enumerate(fruit): result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result: dict[str, int] = { + # comment 1 + } + for idx, name in enumerate( + fruit # comment 2 + ): + # comment 3 + result[ + name # comment 4 + ] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + a = 1; result = {}; b = 2 + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result = {"kiwi": 3} + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + (_, result) = (None, {"kiwi": 3}) + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result = {} + print(len(result)) + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result = {} + for idx, name in enumerate(fruit): + if last_idx := idx % 3: + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + indices = [0, 1, 2] + result = {} + for idx, name in indices, fruit: + result[name] = idx # PERF403 + + +def foo(): + src = (("x", 1),) + dst = {} + + for k, v in src: + if True if True else False: + dst[k] = v + + for k, v in src: + if lambda: 0: + dst[k] = v + +# https://github.com/astral-sh/ruff/issues/18859 +def foo(): + v = {} + for o,(x,)in(): + v[x,]=o + + +# https://github.com/astral-sh/ruff/issues/19005 +def issue_19005_1(): + c = {} + a = object() + for a.b in (): + c[a.b] = a.b + + +def issue_19005_2(): + a = object() + c = {} + for a.k, a.v in (): + c[a.k] = a.v + + +def issue_19005_3(): + a = [None, None] + c = {} + for a[0], a[1] in (): + c[a[0]] = a[1] diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E27.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E27.py index 73815f6fdf457..585901bea44bf 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E27.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E27.py @@ -81,4 +81,5 @@ def f(): # https://github.com/astral-sh/ruff/issues/12094 pass; -yield, x +def foo(): + yield, x diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py index c0be4d7aa1c47..adbb8d578fb3e 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py @@ -53,3 +53,7 @@ assert (not foo) in bar assert {"x": not foo} in bar assert [42, not foo] in bar + +# https://github.com/astral-sh/ruff/issues/17582 +if True == True: # No duplicated diagnostic + pass diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py index e28d613857469..a5fab10bf48f7 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py @@ -176,4 +176,17 @@ class FilterDataclass: x = lambda: ( # comment y := 10 -) \ No newline at end of file +) + +# https://github.com/astral-sh/ruff/issues/18475 +foo_tooltip = ( + lambda x, data: f"\nfoo: {data['foo'][int(x)]}" + if data["foo"] is not None + else "" +) + +foo_tooltip = ( + lambda x, data: f"\nfoo: {data['foo'][int(x)]}" + + more + +) diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py index 735881301ec5b..c29b7d53ec741 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py @@ -1,4 +1,4 @@ -# Same as `W605_0.py` but using f-strings instead. +# Same as `W605_0.py` but using f-strings and t-strings instead. #: W605:1:10 regex = f'\.png$' @@ -66,3 +66,72 @@ # Debug text (should trigger) t = f"{'\InHere'=}" + + + +#: W605:1:10 +regex = t'\.png$' + +#: W605:2:1 +regex = t''' +\.png$ +''' + +#: W605:2:6 +f( + t'\_' +) + +#: W605:4:6 +t""" +multi-line +literal +with \_ somewhere +in the middle +""" + +#: W605:1:38 +value = t'new line\nand invalid escape \_ here' + + +#: Okay +regex = fr'\.png$' +regex = t'\\.png$' +regex = fr''' +\.png$ +''' +regex = fr''' +\\.png$ +''' +s = t'\\' +regex = t'\w' # noqa +regex = t''' +\w +''' # noqa + +regex = t'\\\_' +value = t'\{{1}}' +value = t'\{1}' +value = t'{1:\}' +value = t"{t"\{1}"}" +value = rt"{t"\{1}"}" + +# Okay +value = rt'\{{1}}' +value = rt'\{1}' +value = rt'{1:\}' +value = t"{rt"\{1}"}" + +# Regression tests for https://github.com/astral-sh/ruff/issues/10434 +t"{{}}+-\d" +t"\n{{}}+-\d+" +t"\n{{}}�+-\d+" + +# See https://github.com/astral-sh/ruff/issues/11491 +total = 10 +ok = 7 +incomplete = 3 +s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" + +# Debug text (should trigger) +t = t"{'\InHere'=}" diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py index 3d912aae4f07d..6fe6810c12ad1 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D413.py @@ -69,3 +69,11 @@ def func(): Returns: the value """ + + +def func(): + ("""Docstring. + + Raises: + ValueError: An error. + """) diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F504.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F504.py index bb5549adaeed5..1178bf64cbf1a 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F504.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F504.py @@ -14,3 +14,6 @@ "" % { 'test1': '', 'test2': '', } + +# https://github.com/astral-sh/ruff/issues/18806 +"Hello, %(name)s" % {"greeting": print(1), "name": "World"} diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F522.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F522.py index d229b23e451d0..1b02a1ff37942 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F522.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F522.py @@ -4,3 +4,11 @@ "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 ('' .format(x=2)) # F522 + +# https://github.com/astral-sh/ruff/issues/18806 +# The fix here is unsafe because the unused argument has side effect +"Hello, {name}".format(greeting=print(1), name="World") + +# The fix here is safe because the unused argument has no side effect, +# even though the used argument has a side effect +"Hello, {name}".format(greeting="Pikachu", name=print(1)) diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F523.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F523.py index 7b4285776102b..0a79b3d36c7c0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F523.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F523.py @@ -35,3 +35,11 @@ # Removing the final argument. "Hello".format("world") "Hello".format("world", key="value") + +# https://github.com/astral-sh/ruff/issues/18806 +# The fix here is unsafe because the unused argument has side effect +"Hello, {0}".format("world", print(1)) + +# The fix here is safe because the unused argument has no side effect, +# even though the used argument has a side effect +"Hello, {0}".format(print(1), "Pikachu") diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py index 2c41ca5071660..70a1272d42b51 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F704.py @@ -9,3 +9,11 @@ class Foo: yield 3 yield from 3 await f() + +def _(): + # Invalid yield scopes; but not outside a function + type X[T: (yield 1)] = int + type Y = (yield 2) + + # Valid yield scope + yield 3 diff --git a/crates/ruff_linter/resources/test/fixtures/pygrep_hooks/PGH005_0.py b/crates/ruff_linter/resources/test/fixtures/pygrep_hooks/PGH005_0.py index cb80cc8c28048..18e741d78bef0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pygrep_hooks/PGH005_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pygrep_hooks/PGH005_0.py @@ -1,3 +1,5 @@ +# Mock objects +# ============ # Errors assert my_mock.not_called() assert my_mock.called_once_with() @@ -17,3 +19,25 @@ my_mock.assert_called_once_with() """like :meth:`Mock.assert_called_once_with`""" """like :meth:`MagicMock.assert_called_once_with`""" + +# AsyncMock objects +# ================= +# Errors +assert my_mock.not_awaited() +assert my_mock.awaited_once_with() +assert my_mock.not_awaited +assert my_mock.awaited_once_with +my_mock.assert_not_awaited +my_mock.assert_awaited +my_mock.assert_awaited_once_with +my_mock.assert_awaited_once_with +MyMock.assert_awaited_once_with +assert my_mock.awaited + +# OK +assert my_mock.await_count == 1 +my_mock.assert_not_awaited() +my_mock.assert_awaited() +my_mock.assert_awaited_once_with() +"""like :meth:`Mock.assert_awaited_once_with`""" +"""like :meth:`MagicMock.assert_awaited_once_with`""" diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/duplicate_bases.py b/crates/ruff_linter/resources/test/fixtures/pylint/duplicate_bases.py index e59b561ec70ac..829791f762ff9 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/duplicate_bases.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/duplicate_bases.py @@ -70,3 +70,9 @@ class D(C): class E(A, C): ... + +# https://github.com/astral-sh/ruff/issues/18814 +class Bar(Foo, # 1 + Foo # 2 +): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py b/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py index e316c3383ca9f..d5fa9d34c3e9f 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py @@ -237,3 +237,15 @@ def foo(self, value) -> None: # case 8: counter["a"] = max(counter["b"], counter["a"]) if counter["a"] > counter["b"]: counter["b"] = counter["a"] + +# https://github.com/astral-sh/ruff/issues/17311 + +# fix marked unsafe as delete comments +a, b = [], [] +if a >= b: + # very important comment + a = b + +# fix marked safe as preserve comments +if a >= b: + a = b # very important comment diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/import_aliasing_2/__init__.py b/crates/ruff_linter/resources/test/fixtures/pylint/import_aliasing_2/__init__.py new file mode 100644 index 0000000000000..c6a3d8d27eefb --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/import_aliasing_2/__init__.py @@ -0,0 +1,4 @@ +import collections as collections +from collections import OrderedDict as OrderedDict +from . import foo as foo +from .foo import bar as bar diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py b/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py index 10c8b4202f0fd..05cf82a535fb4 100644 Binary files a/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py and b/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py differ diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/len_as_condition.py b/crates/ruff_linter/resources/test/fixtures/pylint/len_as_condition.py index 973e14386c519..fbafe4818374d 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/len_as_condition.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/len_as_condition.py @@ -168,7 +168,7 @@ def function_returning_comprehension(r): def function_returning_function(r): return function_returning_generator(r) - assert len(function_returning_list(z)) # [PLC1802] differs from pylint + assert len(function_returning_list(z)) # [PLC1802] differs from pylint assert len(function_returning_int(z)) # This should raise a PLC1802 once astroid can infer it # See https://github.com/pylint-dev/pylint/pull/3821#issuecomment-743771514 @@ -196,7 +196,7 @@ def f(cond:bool): def g(cond:bool): x = [1,2,3] if cond: - x = [4,5,6] + x = [4,5,6] if len(x): # this should be addressed print(x) del x @@ -236,3 +236,15 @@ def j(): # regression tests for https://github.com/astral-sh/ruff/issues/14690 bool(len(ascii(1))) bool(len(sorted(""))) + +# regression tests for https://github.com/astral-sh/ruff/issues/18811 +fruits = [] +if(len)(fruits): + ... + +# regression tests for https://github.com/astral-sh/ruff/issues/18812 +fruits = [] +if len( + fruits # comment +): + ... diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py b/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py index 5ca672e1a1a43..fdbe9cc9b8760 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py @@ -156,3 +156,8 @@ def f(): def f(): global x print(f"{x=}") + + +# surprisingly still an error, global in module scope +x = None +global x diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py b/crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py new file mode 100644 index 0000000000000..f24e23fdbd7f5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/missing_maxsplit_arg.py @@ -0,0 +1,184 @@ +SEQ = "1,2,3" + +class Foo(str): + class_str = "1,2,3" + + def split(self, sep=None, maxsplit=-1) -> list[str]: + return super().split(sep, maxsplit) + +class Bar(): + split = "1,2,3" + +# Errors +## Test split called directly on string literal +"1,2,3".split(",")[0] # [missing-maxsplit-arg] +"1,2,3".split(",")[-1] # [missing-maxsplit-arg] +"1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] +"1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test split called on string variable +SEQ.split(",")[0] # [missing-maxsplit-arg] +SEQ.split(",")[-1] # [missing-maxsplit-arg] +SEQ.rsplit(",")[0] # [missing-maxsplit-arg] +SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test split called on class attribute +Foo.class_str.split(",")[0] # [missing-maxsplit-arg] +Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] +Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test split called on sliced string +"1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] +"1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +"1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] +Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test sep given as named argument +"1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] +"1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +"1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] +"1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + +## Special cases +"1,2,3".split("\n")[0] # [missing-maxsplit-arg] +"1,2,3".split("split")[-1] # [missing-maxsplit-arg] +"1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + +## Test class attribute named split +Bar.split.split(",")[0] # [missing-maxsplit-arg] +Bar.split.split(",")[-1] # [missing-maxsplit-arg] +Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] +Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + +## Test unpacked dict literal kwargs +"1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg] + + +# OK +## Test not accessing the first or last element +### Test split called directly on string literal +"1,2,3".split(",")[1] +"1,2,3".split(",")[-2] +"1,2,3".rsplit(",")[1] +"1,2,3".rsplit(",")[-2] + +### Test split called on string variable +SEQ.split(",")[1] +SEQ.split(",")[-2] +SEQ.rsplit(",")[1] +SEQ.rsplit(",")[-2] + +### Test split called on class attribute +Foo.class_str.split(",")[1] +Foo.class_str.split(",")[-2] +Foo.class_str.rsplit(",")[1] +Foo.class_str.rsplit(",")[-2] + +### Test split called on sliced string +"1,2,3"[::-1].split(",")[1] +SEQ[:3].split(",")[1] +Foo.class_str[1:3].split(",")[-2] +"1,2,3"[::-1].rsplit(",")[1] +SEQ[:3].rsplit(",")[1] +Foo.class_str[1:3].rsplit(",")[-2] + +### Test sep given as named argument +"1,2,3".split(sep=",")[1] +"1,2,3".split(sep=",")[-2] +"1,2,3".rsplit(sep=",")[1] +"1,2,3".rsplit(sep=",")[-2] + +## Test varying maxsplit argument +### str.split() tests +"1,2,3".split(sep=",", maxsplit=1)[-1] +"1,2,3".split(sep=",", maxsplit=1)[0] +"1,2,3".split(sep=",", maxsplit=2)[-1] +"1,2,3".split(sep=",", maxsplit=2)[0] +"1,2,3".split(sep=",", maxsplit=2)[1] + +### str.rsplit() tests +"1,2,3".rsplit(sep=",", maxsplit=1)[-1] +"1,2,3".rsplit(sep=",", maxsplit=1)[0] +"1,2,3".rsplit(sep=",", maxsplit=2)[-1] +"1,2,3".rsplit(sep=",", maxsplit=2)[0] +"1,2,3".rsplit(sep=",", maxsplit=2)[1] + +## Test user-defined split +Foo("1,2,3").split(",")[0] +Foo("1,2,3").split(",")[-1] +Foo("1,2,3").rsplit(",")[0] +Foo("1,2,3").rsplit(",")[-1] + +## Test split called on sliced list +["1", "2", "3"][::-1].split(",")[0] + +## Test class attribute named split +Bar.split[0] +Bar.split[-1] +Bar.split[0] +Bar.split[-1] + +## Test unpacked dict literal kwargs +"1,2,3".split(",", **{"maxsplit": 1})[0] +"1,2,3".split(**{"sep": ",", "maxsplit": 1})[0] + + +# TODO + +## Test variable split result index +## TODO: These require the ability to resolve a variable name to a value +# Errors +result_index = 0 +"1,2,3".split(",")[result_index] # TODO: [missing-maxsplit-arg] +result_index = -1 +"1,2,3".split(",")[result_index] # TODO: [missing-maxsplit-arg] +# OK +result_index = 1 +"1,2,3".split(",")[result_index] +result_index = -2 +"1,2,3".split(",")[result_index] + + +## Test split result index modified in loop +## TODO: These require the ability to recognize being in a loop where: +## - the result of split called on a string is indexed by a variable +## - the variable index above is modified +# OK +result_index = 0 +for j in range(3): + print(SEQ.split(",")[result_index]) + result_index = result_index + 1 + + +## Test accessor +## TODO: These require the ability to get the return type of a method +## (possibly via `typing::is_string`) +class Baz(): + def __init__(self): + self.my_str = "1,2,3" + + def get_string(self) -> str: + return self.my_str + +# Errors +Baz().get_string().split(",")[0] # TODO: [missing-maxsplit-arg] +Baz().get_string().split(",")[-1] # TODO: [missing-maxsplit-arg] +# OK +Baz().get_string().split(",")[1] +Baz().get_string().split(",")[-2] + + +## Test unpacked dict instance kwargs +## TODO: These require the ability to resolve a dict variable name to a value +# Errors +kwargs_without_maxsplit = {"seq": ","} +"1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg] +# OK +kwargs_with_maxsplit = {"maxsplit": 1} +"1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive +kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} +"1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py b/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py index 3481b37d6e1ac..af7cbff7b3438 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/nan_comparison.py @@ -92,3 +92,8 @@ # OK if x == "nan": pass + +# PLW0117 +# https://github.com/astral-sh/ruff/issues/18596 +assert x == float("-NaN ") +assert x == float(" \n+nan \t") diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/nested_min_max.py b/crates/ruff_linter/resources/test/fixtures/pylint/nested_min_max.py index d35f1a50b6b1f..327fac821923c 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/nested_min_max.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/nested_min_max.py @@ -14,7 +14,7 @@ # This will still trigger, to merge the calls without keyword args. min(1, min(2, 3, key=test), min(4, 5)) -# Don't provide a fix if there are comments within the call. +# The fix is already unsafe, so deleting comments is okay. min( 1, # This is a comment. min(2, 3), @@ -55,3 +55,8 @@ *(len(word) for word in "blah blah blah".split(" ")), len("Done!"), ) + + +# Outer call has a single argument, inner call has multiple arguments; should not trigger. +min(min([2, 3], [4, 1])) +max(max([2, 4], [3, 1])) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/redeclared_assigned_name.py b/crates/ruff_linter/resources/test/fixtures/pylint/redeclared_assigned_name.py index 50b3a27925c36..3eff6b43de097 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/redeclared_assigned_name.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/redeclared_assigned_name.py @@ -2,5 +2,8 @@ FIRST, (FIRST, SECOND) = (1, (1, 2)) # PLW0128 FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128 FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 +FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 +FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 +FIRST, *FIRST = (1, 2) # PLW0128 FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py b/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py index 2f46174fd3822..8bdb4d6605316 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_dunder_call.py @@ -129,3 +129,6 @@ def use_descriptor(self, item): # https://github.com/astral-sh/ruff/issues/14597 assert "abc".__str__() == "abc" + +# https://github.com/astral-sh/ruff/issues/18813 +three = 1 if 1 else(3.0).__str__() diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_lambda.py b/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_lambda.py index 8f7dc913967f3..dbdd9110c2599 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_lambda.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/unnecessary_lambda.py @@ -57,3 +57,7 @@ # lambda uses an additional keyword _ = lambda *args: f(*args, y=1) _ = lambda *args: f(*args, y=x) + +# https://github.com/astral-sh/ruff/issues/18675 +_ = lambda x: (string := str)(x) +_ = lambda x: ((x := 1) and str)(x) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py b/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py index fca823bcbc8a2..db89cbf4e2b7b 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py @@ -94,3 +94,6 @@ def func(*args, **kwargs): # Violation but not detectable x = Path("foo.txt") x.open() + +# https://github.com/astral-sh/ruff/issues/18107 +codecs.open("plw1514.py", "r", "utf-8").close() # this is fine diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig b/crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig new file mode 100644 index 0000000000000..048d4deec14e7 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig @@ -0,0 +1,5 @@ +# These rules test for reformatting with specific line endings. +# Using a formatter fixes that and breaks the tests +[UP018_{CR,LF}.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py index cbc3e79b29da2..dd60d4c833548 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py @@ -90,3 +90,52 @@ class AClass: def myfunc(param: "tuple[Union[int, 'AClass', None], str]"): print(param) + + +from typing import NamedTuple, Union + +import typing_extensions +from typing_extensions import ( + NamedTuple as NamedTupleTE, + Union as UnionTE, +) + +# Regression test for https://github.com/astral-sh/ruff/issues/18619 +# Don't emit lint for `NamedTuple` +a_plain_1: Union[NamedTuple, int] = None +a_plain_2: Union[int, NamedTuple] = None +a_plain_3: Union[NamedTuple, None] = None +a_plain_4: Union[None, NamedTuple] = None +a_plain_te_1: UnionTE[NamedTupleTE, int] = None +a_plain_te_2: UnionTE[int, NamedTupleTE] = None +a_plain_te_3: UnionTE[NamedTupleTE, None] = None +a_plain_te_4: UnionTE[None, NamedTupleTE] = None +a_plain_typing_1: UnionTE[typing.NamedTuple, int] = None +a_plain_typing_2: UnionTE[int, typing.NamedTuple] = None +a_plain_typing_3: UnionTE[typing.NamedTuple, None] = None +a_plain_typing_4: UnionTE[None, typing.NamedTuple] = None +a_string_1: "Union[NamedTuple, int]" = None +a_string_2: "Union[int, NamedTuple]" = None +a_string_3: "Union[NamedTuple, None]" = None +a_string_4: "Union[None, NamedTuple]" = None +a_string_te_1: "UnionTE[NamedTupleTE, int]" = None +a_string_te_2: "UnionTE[int, NamedTupleTE]" = None +a_string_te_3: "UnionTE[NamedTupleTE, None]" = None +a_string_te_4: "UnionTE[None, NamedTupleTE]" = None +a_string_typing_1: "typing.Union[typing.NamedTuple, int]" = None +a_string_typing_2: "typing.Union[int, typing.NamedTuple]" = None +a_string_typing_3: "typing.Union[typing.NamedTuple, None]" = None +a_string_typing_4: "typing.Union[None, typing.NamedTuple]" = None + +b_plain_1: Union[NamedTuple] = None +b_plain_2: Union[NamedTuple, None] = None +b_plain_te_1: UnionTE[NamedTupleTE] = None +b_plain_te_2: UnionTE[NamedTupleTE, None] = None +b_plain_typing_1: UnionTE[typing.NamedTuple] = None +b_plain_typing_2: UnionTE[typing.NamedTuple, None] = None +b_string_1: "Union[NamedTuple]" = None +b_string_2: "Union[NamedTuple, None]" = None +b_string_te_1: "UnionTE[NamedTupleTE]" = None +b_string_te_2: "UnionTE[NamedTupleTE, None]" = None +b_string_typing_1: "typing.Union[typing.NamedTuple]" = None +b_string_typing_2: "typing.Union[typing.NamedTuple, None]" = None diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py index e9d68ebc3a73e..7a77087002625 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py @@ -79,3 +79,65 @@ def normal(self): def normal(self): super(DataClass, self).f() # OK super().f() # OK (`TypeError` in practice) + + +# see: https://github.com/astral-sh/ruff/issues/18477 +class A: + def foo(self): + pass + + +class B(A): + def bar(self): + super(__class__, self).foo() + + +# see: https://github.com/astral-sh/ruff/issues/18684 +class C: + def f(self): + super = print + super(C, self) + + +import builtins + + +class C: + def f(self): + builtins.super(C, self) + + +# see: https://github.com/astral-sh/ruff/issues/18533 +class ClassForCommentEnthusiasts(BaseClass): + def with_comments(self): + super( + # super helpful comment + ClassForCommentEnthusiasts, + self + ).f() + super( + ClassForCommentEnthusiasts, + # even more helpful comment + self + ).f() + super( + ClassForCommentEnthusiasts, + self + # also a comment + ).f() + + +# Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed +class Ord(int): + def __len__(self): + return super(Ord, self, uhoh=True, **{"error": True}).bit_length() + +class ExampleWithKeywords: + def method1(self): + super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed + + def method2(self): + super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed + + def method3(self): + super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP010.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP010.py index eb8ece827e4c8..ef7d9537c7d70 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP010.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP010.py @@ -12,3 +12,4 @@ if True: from __future__ import generator_stop from __future__ import invalid_module, generators + from __future__ import generators # comment diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py index 8f2dd70d97006..a5b7e1d8949d6 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py @@ -84,3 +84,9 @@ '''Lorem ipsum''' # Comment ).foo + +# https://github.com/astral-sh/ruff/issues/17606 +bool(True)and None +int(1)and None +float(1.)and None +bool(True)and() diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py new file mode 100644 index 0000000000000..7535093fa1487 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py @@ -0,0 +1 @@ +# Keep parenthesis around preserved CR int(- 1) int(+ 1) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py new file mode 100644 index 0000000000000..3a2d43335e505 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py @@ -0,0 +1,7 @@ +# Keep parentheses around preserved \n + +int(- + 1) + +int(+ + 1) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP024_2.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP024_2.py index 25e846e0288cc..e99b47da49f79 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP024_2.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP024_2.py @@ -6,14 +6,16 @@ raise error # Testing the modules -import socket, mmap, select +import socket, mmap, select, resource raise socket.error raise mmap.error raise select.error +raise resource.error raise socket.error() raise mmap.error(1) raise select.error(1, 2) +raise resource.error(1, "strerror", "filename") raise socket.error( 1, @@ -30,6 +32,9 @@ from select import error raise error(1, 2) +from resource import error +raise error(1, "strerror", "filename") + # Testing the names raise EnvironmentError raise IOError diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP025.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP025.py index 899f827f80866..e1354c3375e3b 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP025.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP025.py @@ -26,3 +26,9 @@ def hello(): f"foo"u"bar" # OK f"foo" u"bar" # OK + +# https://github.com/astral-sh/ruff/issues/18895 +""u"" +""u"hi" +""""""""""""""""""""u"hi" +""U"helloooo" \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py index 50e7cbc968916..b9c3892a9b381 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP035.py @@ -110,6 +110,8 @@ # UP035 on py313+ only from typing_extensions import deprecated +# UP035 on py313+ only +from typing_extensions import get_type_hints # https://github.com/astral-sh/ruff/issues/15780 from typing_extensions import is_typeddict diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py index 697c84a8047d6..68e3f167b6e53 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py @@ -71,3 +71,47 @@ def a(): if sys.version_info <= (3, 14, 15): print() + +# https://github.com/astral-sh/ruff/issues/18165 + +if sys.version_info.major >= 3: + print("3") +else: + print("2") + +if sys.version_info.major > 3: + print("3") +else: + print("2") + +if sys.version_info.major <= 3: + print("3") +else: + print("2") + +if sys.version_info.major < 3: + print("3") +else: + print("2") + +if sys.version_info.major == 3: + print("3") +else: + print("2") + +# Semantically incorrect, skip fixing + +if sys.version_info.major[1] > 3: + print(3) +else: + print(2) + +if sys.version_info.major > (3, 13): + print(3) +else: + print(2) + +if sys.version_info.major[:2] > (3, 13): + print(3) +else: + print(2) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py index 904c1510eabbe..de2b18b52d739 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py @@ -42,3 +42,30 @@ class ServiceRefOrValue: # Regression test for: https://github.com/astral-sh/ruff/issues/7201 class ServiceRefOrValue: service_specification: Optional[str]is not True = None + + +# Test for: https://github.com/astral-sh/ruff/issues/18508 +# Optional[None] should not be offered a fix +foo: Optional[None] = None + + +from typing import NamedTuple, Optional + +import typing_extensions +from typing_extensions import ( + NamedTuple as NamedTupleTE, + Optional as OptionalTE, +) + +# Regression test for https://github.com/astral-sh/ruff/issues/18619 +# Don't emit lint for `NamedTuple` +a1: Optional[NamedTuple] = None +a2: typing.Optional[NamedTuple] = None +a3: OptionalTE[NamedTuple] = None +a4: typing_extensions.Optional[NamedTuple] = None +a5: Optional[typing.NamedTuple] = None +a6: typing.Optional[typing.NamedTuple] = None +a7: OptionalTE[typing.NamedTuple] = None +a8: typing_extensions.Optional[typing.NamedTuple] = None +a9: "Optional[NamedTuple]" = None +a10: Optional[NamedTupleTE] = None diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py new file mode 100644 index 0000000000000..6a04a4acc3cf6 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py @@ -0,0 +1,84 @@ +class A: + ... + + +class A(metaclass=type): + ... + + +class A( + metaclass=type +): + ... + + +class A( + metaclass=type + # +): + ... + + +class A( + # + metaclass=type +): + ... + + +class A( + metaclass=type, + # +): + ... + + +class A( + # + metaclass=type, + # +): + ... + + +class B(A, metaclass=type): + ... + + +class B( + A, + metaclass=type, +): + ... + + +class B( + A, + # comment + metaclass=type, +): + ... + + +def foo(): + class A(metaclass=type): + ... + + +class A( + metaclass=type # comment + , +): + ... + + +type = str + +class Foo(metaclass=type): + ... + + +import builtins + +class A(metaclass=builtins.type): + ... \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py index 9c968e32aa888..6ec754dadc09c 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py @@ -1,3 +1,6 @@ +import datetime +import sys + num = 1337 def return_num() -> int: @@ -10,6 +13,7 @@ def return_num() -> int: print(oct(1337)[2:]) # FURB116 print(hex(1337)[2:]) # FURB116 print(bin(1337)[2:]) # FURB116 +print(bin(+1337)[2:]) # FURB116 print(bin(return_num())[2:]) # FURB116 (no autofix) print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) @@ -17,3 +21,24 @@ def return_num() -> int: ## invalid print(oct(0o1337)[1:]) print(hex(0x1337)[3:]) + +# https://github.com/astral-sh/ruff/issues/16472 +# float and complex numbers should be ignored +print(bin(1.0)[2:]) +print(bin(3.14j)[2:]) + +d = datetime.datetime.now(tz=datetime.UTC) +# autofix is display-only +print(bin(d)[2:]) +# no autofix for Python 3.11 and earlier, as it introduces a syntax error +print(bin(len("xyz").numerator)[2:]) + +# autofix is display-only +print(bin({0: 1}[0].numerator)[2:]) +# no autofix for Python 3.11 and earlier, as it introduces a syntax error +print(bin(ord("\\").numerator)[2:]) +print(hex(sys +.maxunicode)[2:]) + +# for negatives numbers autofix is display-only +print(bin(-1)[2:]) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB122.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB122.py index 9964945f63051..c5af0eb11b0af 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB122.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB122.py @@ -174,3 +174,43 @@ def _(): global global_foo for [a, b, (global_foo, c)] in d: f.write((a, b)) + + +# Test cases for lambda and ternary expressions - https://github.com/astral-sh/ruff/issues/18590 + +def _(): + with Path("file.txt").open("w", encoding="utf-8") as f: + for l in lambda: 0: + f.write(f"[{l}]") + + +def _(): + with Path("file.txt").open("w", encoding="utf-8") as f: + for l in (1,) if True else (2,): + f.write(f"[{l}]") + + +# don't need to add parentheses when making a function argument +def _(): + with open("file", "w") as f: + for line in lambda: 0: + f.write(line) + + +def _(): + with open("file", "w") as f: + for line in (1,) if True else (2,): + f.write(line) + + +# don't add extra parentheses if they're already parenthesized +def _(): + with open("file", "w") as f: + for line in (lambda: 0): + f.write(f"{line}") + + +def _(): + with open("file", "w") as f: + for line in ((1,) if True else (2,)): + f.write(f"{line}") diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py index 51a3cf73a575e..b75a4af952ce1 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py @@ -43,7 +43,6 @@ def func(): import builtins - with builtins.open("FURB129.py") as f: for line in f.readlines(): pass @@ -51,13 +50,12 @@ def func(): from builtins import open as o - with o("FURB129.py") as f: for line in f.readlines(): pass -# False positives +# Ok def func(f): for _line in f.readlines(): pass @@ -89,3 +87,30 @@ def readlines(self) -> list[str]: pass for _not_line in f.readline(): pass + +# https://github.com/astral-sh/ruff/issues/18231 +with open("furb129.py") as f: + for line in (f).readlines(): + pass + +with open("furb129.py") as f: + [line for line in (f).readlines()] + + +with open("furb129.py") as f: + for line in (((f))).readlines(): + pass + for line in(f).readlines(): + pass + + # Test case for issue #17683 (missing space before keyword) + print([line for line in f.readlines()if True]) + +# https://github.com/astral-sh/ruff/issues/18843 +with open("file.txt") as fp: + for line in ( # 1 + fp. # 3 # 2 + readlines( # 4 + ) # 5 + ): + ... diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB142.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB142.py index 88d4188b9b76a..cd39b2f9831c0 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB142.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB142.py @@ -74,3 +74,28 @@ async def f(y): def g(): for x in (set(),): x.add(x) + + +# Test cases for lambda and ternary expressions - https://github.com/astral-sh/ruff/issues/18590 + +s = set() + +for x in lambda: 0: + s.discard(-x) + +for x in (1,) if True else (2,): + s.add(-x) + +# don't add extra parens +for x in (lambda: 0): + s.discard(-x) + +for x in ((1,) if True else (2,)): + s.add(-x) + +# don't add parens directly in function call +for x in lambda: 0: + s.discard(x) + +for x in (1,) if True else (2,): + s.add(x) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py index 0736b9dc3acac..6dda8a11c9457 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py @@ -12,6 +12,7 @@ def ten() -> int: count = bin(ten()).count("1") # FURB161 count = bin((10)).count("1") # FURB161 count = bin("10" "15").count("1") # FURB161 +count = bin("123").count("1") # FURB161 count = x.bit_count() # OK count = (10).bit_count() # OK diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB163.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB163.py index b8aca2ebe8fac..0aa5562681698 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB163.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB163.py @@ -43,3 +43,40 @@ def log(*args): math.log(1, 2.0001) math.log(1, 10.0001) + +# https://github.com/astral-sh/ruff/issues/18747 +def log(): + yield math.log((yield), math.e) + + +def log(): + yield math.log((yield from x), math.e) + +# see: https://github.com/astral-sh/ruff/issues/18639 +math.log(1, 10 # comment + ) + +math.log(1, + 10 # comment + ) + +math.log(1 # comment + , # comment + 10 # comment + ) + +math.log( + 1 # comment + , + 10 # comment +) + +math.log(4.13e223, 2) +math.log(4.14e223, 10) + + +def print_log(*args): + try: + print(math.log(*args, math.e)) + except TypeError as e: + print(repr(e)) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py index 2adbc446331c4..e83f8578b6672 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py @@ -20,6 +20,12 @@ _ = Decimal.from_float(float("Infinity")) _ = Decimal.from_float(float("-Infinity")) _ = Decimal.from_float(float("nan")) +_ = Decimal.from_float(float("-NaN ")) +_ = Decimal.from_float(float(" \n+nan \t")) +_ = Decimal.from_float(float(" iNf \n\t ")) +_ = Decimal.from_float(float("  -inF\n \t")) +_ = Decimal.from_float(float(" InfinIty \n\t ")) +_ = Decimal.from_float(float("  -InfinIty\n \t")) # OK _ = Fraction(0.1) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB168.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB168.py index 17238d520c4c9..a9f31e3a78a95 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB168.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB168.py @@ -85,3 +85,10 @@ def type(*args): ... if isinstance(foo, type(None)): ... + +# https://github.com/astral-sh/ruff/issues/19047 +if isinstance(foo, ()): + pass + +if isinstance(foo, Union[()]): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB171.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB171_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/refurb/FURB171.py rename to crates/ruff_linter/resources/test/fixtures/refurb/FURB171_0.py diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB171_1.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB171_1.py new file mode 100644 index 0000000000000..41109f6cfa618 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB171_1.py @@ -0,0 +1,53 @@ +# Errors. + +if 1 in set([1]): + print("Single-element set") + +if 1 in set((1,)): + print("Single-element set") + +if 1 in set({1}): + print("Single-element set") + +if 1 in frozenset([1]): + print("Single-element set") + +if 1 in frozenset((1,)): + print("Single-element set") + +if 1 in frozenset({1}): + print("Single-element set") + +if 1 in set(set([1])): + print('Recursive solution') + + + +# Non-errors. + +if 1 in set((1, 2)): + pass + +if 1 in set([1, 2]): + pass + +if 1 in set({1, 2}): + pass + +if 1 in frozenset((1, 2)): + pass + +if 1 in frozenset([1, 2]): + pass + +if 1 in frozenset({1, 2}): + pass + +if 1 in set(1,): + pass + +if 1 in set(1,2): + pass + +if 1 in set((x for x in range(2))): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig b/crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig new file mode 100644 index 0000000000000..bf4811bd1a7a2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig @@ -0,0 +1,5 @@ +# These rules test for reformatting with specific line endings. +# Using a formatter fixes that and breaks the tests +[RUF046_{CR,LF}.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py index a98835577c1d9..b8c76574c25bb 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py @@ -110,3 +110,19 @@ class ShouldMatchB008RuleOfImmutableTypeAnnotationIgnored: # ignored this_is_fine: int = f() + +# Test for: +# https://github.com/astral-sh/ruff/issues/17424 +@dataclass(frozen=True) +class C: + foo: int = 1 + + +@dataclass +class D: + c: C = C() + + +@dataclass +class E: + c: C = C() \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs.py index 7910b3932306b..526c12ab88e99 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs.py @@ -73,3 +73,72 @@ def __set__(self, obj, value): @frozen class InventoryItem: quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100) + + +# Test for: +# https://github.com/astral-sh/ruff/issues/17424 +@frozen +class C: + foo: int = 1 + + +@attr.frozen +class D: + foo: int = 1 + + +@define +class E: + c: C = C() + d: D = D() + + +@attr.s +class F: + foo: int = 1 + + +@attr.mutable +class G: + foo: int = 1 + + +@attr.attrs +class H: + f: F = F() + g: G = G() + + +@attr.define +class I: + f: F = F() + g: G = G() + + +@attr.frozen +class J: + f: F = F() + g: G = G() + + +@attr.mutable +class K: + f: F = F() + g: G = G() + + +# Regression test for https://github.com/astral-sh/ruff/issues/19014 +# These are all valid field calls and should not cause diagnostics. +@attr.define +class TestAttrField: + attr_field_factory: list[int] = attr.field(factory=list) + attr_field_default: list[int] = attr.field(default=attr.Factory(list)) + attr_factory: list[int] = attr.Factory(list) + attr_ib: list[int] = attr.ib(factory=list) + attr_attr: list[int] = attr.attr(factory=list) + attr_attrib: list[int] = attr.attrib(factory=list) + + +@attr.attributes +class TestAttrAttributes: + x: list[int] = list() # RUF009 diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF010.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF010.py index 031e08412fb02..af7e596dbfdd3 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF010.py @@ -36,5 +36,19 @@ def ascii(arg): ) -# OK +# https://github.com/astral-sh/ruff/issues/16325 f"{str({})}" + +f"{str({} | {})}" + +import builtins + +f"{builtins.repr(1)}" + +f"{repr(1)=}" + +f"{repr(lambda: 1)}" + +f"{repr(x := 2)}" + +f"{str(object=3)}" diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py index 949f30691f4fc..3ee96bd2259b0 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py @@ -1,5 +1,5 @@ -from collections import deque import collections +from collections import deque def f(): @@ -56,3 +56,49 @@ def f(): def f(): queue = deque() # Ok + +def f(): + x = 0 or(deque)([]) + + +# regression tests for https://github.com/astral-sh/ruff/issues/18612 +def f(): + deque([], *[10]) # RUF037 but no fix + deque([], **{"maxlen": 10}) # RUF037 + deque([], foo=1) # RUF037 + + +# Somewhat related to the issue, both okay because we can't generally look +# inside *args or **kwargs +def f(): + deque(*([], 10)) # Ok + deque(**{"iterable": [], "maxlen": 10}) # Ok + +# The fix was actually always unsafe in the presence of comments. all of these +# are deleted +def f(): + deque( # a comment in deque, deleted + [ # a comment _in_ the list, deleted + ], # a comment after the list, deleted + maxlen=10, # a comment on maxlen, deleted + ) # only this is preserved + + +# `maxlen` can also be passed positionally +def f(): + deque([], 10) + + +def f(): + deque([], iterable=[]) + +# https://github.com/astral-sh/ruff/issues/18854 +deque("") +deque(b"") +deque(f"") +deque(f"" "") +deque(f"" f"") +deque("abc") # OK +deque(b"abc") # OK +deque(f"" "a") # OK +deque(f"{x}" "") # OK diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py new file mode 100644 index 0000000000000..9723c347dcf0c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py @@ -0,0 +1 @@ +int(- 1) # Carriage return as newline \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py new file mode 100644 index 0000000000000..53a879a56e714 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py @@ -0,0 +1,3 @@ +# \n as newline +int(- + 1) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF056.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF056.py index 831ec4bcead15..9beeebd623a61 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF056.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF056.py @@ -23,69 +23,69 @@ # Valid # Using dict.get with a falsy fallback: False -value = my_dict.get("key", False) +value = my_dict.get("key", False) # Using dict.get with a falsy fallback: empty string -value = my_dict.get("key", "") +value = my_dict.get("key", "") # Using dict.get with a falsy fallback: empty list -value = my_dict.get("key", []) +value = my_dict.get("key", []) # Using dict.get with a falsy fallback: empty dict -value = my_dict.get("key", {}) +value = my_dict.get("key", {}) # Using dict.get with a falsy fallback: empty set -value = my_dict.get("key", set()) +value = my_dict.get("key", set()) # Using dict.get with a falsy fallback: zero integer -value = my_dict.get("key", 0) +value = my_dict.get("key", 0) # Using dict.get with a falsy fallback: zero float -value = my_dict.get("key", 0.0) +value = my_dict.get("key", 0.0) # Using dict.get with a falsy fallback: None -value = my_dict.get("key", None) +value = my_dict.get("key", None) # Using dict.get with a falsy fallback via function call -value = my_dict.get("key", list()) -value = my_dict.get("key", dict()) -value = my_dict.get("key", set()) +value = my_dict.get("key", list()) +value = my_dict.get("key", dict()) +value = my_dict.get("key", set()) # Reassigning with falsy fallback def get_value(d): - return d.get("key", False) + return d.get("key", False) # Multiple dict.get calls with mixed fallbacks value1 = my_dict.get("key1", "default") -value2 = my_dict.get("key2", 0) +value2 = my_dict.get("key2", 0) value3 = my_dict.get("key3", "another default") # Using dict.get in a class with falsy fallback class MyClass: def method(self): - return self.data.get("key", {}) + return self.data.get("key", {}) # Using dict.get in a nested function with falsy fallback def outer(): def inner(): - return my_dict.get("key", "") + return my_dict.get("key", "") return inner() # Using dict.get with variable fallback that is falsy falsy_value = None -value = my_dict.get("key", falsy_value) +value = my_dict.get("key", falsy_value) # Using dict.get with variable fallback that is truthy truthy_value = "exists" value = my_dict.get("key", truthy_value) # Using dict.get with complex expressions as fallback -value = my_dict.get("key", 0 or "default") -value = my_dict.get("key", [] if condition else {}) +value = my_dict.get("key", 0 or "default") +value = my_dict.get("key", [] if condition else {}) # testing dict.get call using kwargs -value = my_dict.get(key="key", default=False) -value = my_dict.get(default=[], key="key") +value = my_dict.get(key="key", default=False) +value = my_dict.get(default=[], key="key") # Edge Cases @@ -93,16 +93,16 @@ def inner(): dicts = [my_dict, my_dict, my_dict] # Falsy fallback in a lambda -get_fallback = lambda d: d.get("key", False) +get_fallback = lambda d: d.get("key", False) # Falsy fallback in a list comprehension -results = [d.get("key", "") for d in dicts] +results = [d.get("key", "") for d in dicts] # Falsy fallback in a generator expression -results = (d.get("key", None) for d in dicts) +results = (d.get("key", None) for d in dicts) # Falsy fallback in a ternary expression -value = my_dict.get("key", 0) if True else "default" +value = my_dict.get("key", 0) if True else "default" # Falsy fallback with inline comment @@ -149,23 +149,43 @@ def inner(): value = not my_dict.get("key", 0.0) # [RUF056] value = not my_dict.get("key", "") # [RUF056] -# testing dict.get call using kwargs -value = not my_dict.get(key="key", default=False) # [RUF056] -value = not my_dict.get(default=[], key="key") # [RUF056] - # testing invalid dict.get call with inline comment value = not my_dict.get("key", # comment1 [] # comment2 ) # [RUF056] -# testing invalid dict.get call with kwargs and inline comment -value = not my_dict.get(key="key", # comment1 - default=False # comment2 - ) # [RUF056] -value = not my_dict.get(default=[], # comment1 - key="key" # comment2 - ) # [RUF056] - -# testing invalid dict.get calls -value = not my_dict.get(key="key", other="something", default=False) -value = not my_dict.get(default=False, other="something", key="test") \ No newline at end of file +# regression tests for https://github.com/astral-sh/ruff/issues/18628 +# we should avoid fixes when there are "unknown" arguments present, including +# extra positional arguments, either of the positional-only arguments passed as +# a keyword, or completely unknown keywords. + +# extra positional +not my_dict.get("key", False, "?!") + +# `default` is positional-only, so these are invalid +not my_dict.get("key", default=False) +not my_dict.get(key="key", default=False) +not my_dict.get(default=[], key="key") +not my_dict.get(default=False) +not my_dict.get(key="key", other="something", default=False) +not my_dict.get(default=False, other="something", key="test") + +# comments don't really matter here because of the kwargs but include them for +# completeness +not my_dict.get( + key="key", # comment1 + default=False, # comment2 +) # comment 3 +not my_dict.get( + default=[], # comment1 + key="key", # comment2 +) # comment 3 + +# the fix is arguably okay here because the same `takes no keyword arguments` +# TypeError is raised at runtime before and after the fix, but we still bail +# out for having an unrecognized number of arguments +not my_dict.get("key", False, foo=...) + +# https://github.com/astral-sh/ruff/issues/18798 +d = {} +not d.get("key", (False)) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py index 558060bc0f14a..a9f4df2e9623c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF059_0.py @@ -94,3 +94,9 @@ def f(): (exponential := (exponential * base_multiplier) % 3): i + 1 for i in range(2) } return hash_map + + +# see: https://github.com/astral-sh/ruff/issues/18507 +def f(_x): + x, = "1" + print(_x) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py new file mode 100644 index 0000000000000..c3b96c6b7be52 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF060.py @@ -0,0 +1,44 @@ +# Errors +1 in [] +1 not in [] +2 in list() +2 not in list() +_ in () +_ not in () +'x' in tuple() +'y' not in tuple() +'a' in set() +'a' not in set() +'b' in {} +'b' not in {} +1 in dict() +2 not in dict() +"a" in "" +b'c' in b"" +"b" in f"" +b"a" in bytearray() +b"a" in bytes() +1 in frozenset() +1 in set(set()) +2 in frozenset([]) +'' in set("") + +# OK +1 in [2] +1 in [1, 2, 3] +_ in ('a') +_ not in ('a') +'a' in set('a', 'b') +'a' not in set('b', 'c') +'b' in {1: 2} +'b' not in {3: 4} +"a" in "x" +b'c' in b"x" +"b" in f"x" +b"a" in bytearray([2]) +b"a" in bytes("a", "utf-8") +1 in frozenset("c") +1 in set(set((1,2))) +1 in set(set([1])) +'' in {""} +frozenset() in {frozenset()} diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_deprecated_call.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_deprecated_call.py new file mode 100644 index 0000000000000..63b08e487fbe5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_deprecated_call.py @@ -0,0 +1,25 @@ +import warnings +import pytest + + +def raise_deprecation_warning(s): + warnings.warn(s, DeprecationWarning) + return s + + +def test_ok(): + with pytest.deprecated_call(): + raise_deprecation_warning("") + + +def test_error_trivial(): + pytest.deprecated_call(raise_deprecation_warning, "deprecated") + + +def test_error_assign(): + s = pytest.deprecated_call(raise_deprecation_warning, "deprecated") + print(s) + + +def test_error_lambda(): + pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_raises.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_raises.py new file mode 100644 index 0000000000000..d4e3d900cd59f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_raises.py @@ -0,0 +1,40 @@ +import pytest + + +def func(a, b): + return a / b + + +def test_ok(): + with pytest.raises(ValueError): + raise ValueError + + +def test_ok_as(): + with pytest.raises(ValueError) as excinfo: + raise ValueError + + +def test_error_trivial(): + pytest.raises(ZeroDivisionError, func, 1, b=0) + + +def test_error_match(): + pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") + + +def test_error_assign(): + excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) + + +def test_error_kwargs(): + pytest.raises(func=func, expected_exception=ZeroDivisionError) + + +def test_error_multi_statement(): + excinfo = pytest.raises(ValueError, int, "hello") + assert excinfo.match("^invalid literal") + + +def test_error_lambda(): + pytest.raises(ZeroDivisionError, lambda: 1 / 0) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_warns.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_warns.py new file mode 100644 index 0000000000000..d94573a090574 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_warns.py @@ -0,0 +1,25 @@ +import warnings +import pytest + + +def raise_user_warning(s): + warnings.warn(s, UserWarning) + return s + + +def test_ok(): + with pytest.warns(UserWarning): + raise_user_warning("") + + +def test_error_trivial(): + pytest.warns(UserWarning, raise_user_warning, "warning") + + +def test_error_assign(): + s = pytest.warns(UserWarning, raise_user_warning, "warning") + print(s) + + +def test_error_lambda(): + pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py new file mode 100644 index 0000000000000..7681b5eea127b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF063.py @@ -0,0 +1,18 @@ +# RUF063 +# Cases that should trigger the violation + +foo.__dict__.get("__annotations__") # RUF063 +foo.__dict__.get("__annotations__", None) # RUF063 +foo.__dict__.get("__annotations__", {}) # RUF063 +foo.__dict__["__annotations__"] # RUF063 + +# Cases that should NOT trigger the violation + +foo.__dict__.get("not__annotations__") +foo.__dict__.get("not__annotations__", None) +foo.__dict__.get("not__annotations__", {}) +foo.__dict__["not__annotations__"] +foo.__annotations__ +foo.get("__annotations__") +foo.get("__annotations__", None) +foo.get("__annotations__", {}) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF064.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF064.py new file mode 100644 index 0000000000000..4f565ae15aee8 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF064.py @@ -0,0 +1,53 @@ +import dbm.gnu +import dbm.ndbm +import os +from pathlib import Path + +os.chmod("foo", 444) # Error +os.chmod("foo", 0o444) # OK +os.chmod("foo", 7777) # Error +os.chmod("foo", 10000) # Error +os.chmod("foo", 99999) # Error + +os.umask(777) # Error +os.umask(0o777) # OK + +os.fchmod(0, 400) # Error +os.fchmod(0, 0o400) # OK + +os.lchmod("foo", 755) # Error +os.lchmod("foo", 0o755) # OK + +os.mkdir("foo", 600) # Error +os.mkdir("foo", 0o600) # OK + +os.makedirs("foo", 644) # Error +os.makedirs("foo", 0o644) # OK + +os.mkfifo("foo", 640) # Error +os.mkfifo("foo", 0o640) # OK + +os.mknod("foo", 660) # Error +os.mknod("foo", 0o660) # OK + +os.open("foo", os.O_CREAT, 644) # Error +os.open("foo", os.O_CREAT, 0o644) # OK + +Path("bar").chmod(755) # Error +Path("bar").chmod(0o755) # OK + +path = Path("bar") +path.chmod(755) # Error +path.chmod(0o755) # OK + +dbm.open("db", "r", 600) # Error +dbm.open("db", "r", 0o600) # OK + +dbm.gnu.open("db", "r", 600) # Error +dbm.gnu.open("db", "r", 0o600) # OK + +dbm.ndbm.open("db", "r", 600) # Error +dbm.ndbm.open("db", "r", 0o600) # OK + +os.fchmod(0, 256) # 0o400 +os.fchmod(0, 493) # 0o755 diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py new file mode 100644 index 0000000000000..c2000f141aca4 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py @@ -0,0 +1,11 @@ +async def elements(n): + yield n + +def regular_function(): + [x async for x in elements(1)] + + async with elements(1) as x: + pass + + async for _ in elements(1): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py new file mode 100644 index 0000000000000..dd49cb05f9f6e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py @@ -0,0 +1,5 @@ +def func(): + await 1 + +# Top-level await +await 1 diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py new file mode 100644 index 0000000000000..1dd40c823c60d --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py @@ -0,0 +1,2 @@ +import random +from __future__ import annotations # Error; not at top of file diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py new file mode 100644 index 0000000000000..70183b0891cd3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py @@ -0,0 +1,23 @@ +x = 10 +def test_1(): + global x # ok + x += 1 + + +x = 10 +def test_2(): + x += 1 # error + global x + +def test_3(): + print(x) # error + global x + x = 5 + +def test_4(): + global x + print(x) + x = 1 + +x = 0 +test_4() \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs index 84b233f94481a..8023ed32e4386 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/bindings.rs @@ -1,16 +1,16 @@ -use ruff_diagnostics::{Diagnostic, Fix}; use ruff_text_size::Ranged; +use crate::Fix; use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::rules::{ - flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes, - pylint, pyupgrade, refurb, ruff, + flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return, + flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff, }; /// Run lint rules over the [`Binding`]s. pub(crate) fn bindings(checker: &Checker) { - if !checker.any_enabled(&[ + if !checker.any_rule_enabled(&[ Rule::AssignmentInAssert, Rule::InvalidAllFormat, Rule::InvalidAllObject, @@ -25,109 +25,86 @@ pub(crate) fn bindings(checker: &Checker) { Rule::ForLoopWrites, Rule::CustomTypeVarForSelf, Rule::PrivateTypeParameter, + Rule::UnnecessaryAssign, ]) { return; } for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() { - if checker.enabled(Rule::UnusedVariable) { + if checker.is_rule_enabled(Rule::UnnecessaryAssign) { + if binding.kind.is_function_definition() { + flake8_return::rules::unnecessary_assign( + checker, + binding.statement(checker.semantic()).unwrap(), + ); + } + } + if checker.is_rule_enabled(Rule::UnusedVariable) { if binding.kind.is_bound_exception() && binding.is_unused() && !checker - .settings + .settings() .dummy_variable_rgx .is_match(binding.name(checker.source())) { - let mut diagnostic = Diagnostic::new( - pyflakes::rules::UnusedVariable { - name: binding.name(checker.source()).to_string(), - }, - binding.range(), - ); - diagnostic.try_set_fix(|| { - pyflakes::fixes::remove_exception_handler_assignment(binding, checker.locator) + checker + .report_diagnostic( + pyflakes::rules::UnusedVariable { + name: binding.name(checker.source()).to_string(), + }, + binding.range(), + ) + .try_set_fix(|| { + pyflakes::fixes::remove_exception_handler_assignment( + binding, + checker.locator, + ) .map(Fix::safe_edit) - }); - checker.report_diagnostic(diagnostic); + }); } } - if checker.enabled(Rule::InvalidAllFormat) { - if let Some(diagnostic) = pylint::rules::invalid_all_format(binding) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::InvalidAllFormat) { + pylint::rules::invalid_all_format(checker, binding); } - if checker.enabled(Rule::InvalidAllObject) { - if let Some(diagnostic) = pylint::rules::invalid_all_object(binding) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::InvalidAllObject) { + pylint::rules::invalid_all_object(checker, binding); } - if checker.enabled(Rule::NonAsciiName) { - if let Some(diagnostic) = pylint::rules::non_ascii_name(binding, checker.locator) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::NonAsciiName) { + pylint::rules::non_ascii_name(checker, binding); } - if checker.enabled(Rule::UnconventionalImportAlias) { - if let Some(diagnostic) = flake8_import_conventions::rules::unconventional_import_alias( + if checker.is_rule_enabled(Rule::UnconventionalImportAlias) { + flake8_import_conventions::rules::unconventional_import_alias( checker, binding, - &checker.settings.flake8_import_conventions.aliases, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().flake8_import_conventions.aliases, + ); } - if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) { - if let Some(diagnostic) = - flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::UnaliasedCollectionsAbcSetImport) { + flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding); } - if !checker.source_type.is_stub() && checker.enabled(Rule::UnquotedTypeAlias) { - if let Some(diagnostics) = - flake8_type_checking::rules::unquoted_type_alias(checker, binding) - { - checker.report_diagnostics(diagnostics); - } + if !checker.source_type.is_stub() && checker.is_rule_enabled(Rule::UnquotedTypeAlias) { + flake8_type_checking::rules::unquoted_type_alias(checker, binding); } - if checker.enabled(Rule::UnsortedDunderSlots) { - if let Some(diagnostic) = ruff::rules::sort_dunder_slots(checker, binding) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::UnsortedDunderSlots) { + ruff::rules::sort_dunder_slots(checker, binding); } - if checker.enabled(Rule::UsedDummyVariable) { - if let Some(diagnostic) = ruff::rules::used_dummy_variable(checker, binding, binding_id) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::UsedDummyVariable) { + ruff::rules::used_dummy_variable(checker, binding, binding_id); } - if checker.enabled(Rule::AssignmentInAssert) { - if let Some(diagnostic) = ruff::rules::assignment_in_assert(checker, binding) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::AssignmentInAssert) { + ruff::rules::assignment_in_assert(checker, binding); } - if checker.enabled(Rule::PytestUnittestRaisesAssertion) { - if let Some(diagnostic) = - flake8_pytest_style::rules::unittest_raises_assertion_binding(checker, binding) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::PytestUnittestRaisesAssertion) { + flake8_pytest_style::rules::unittest_raises_assertion_binding(checker, binding); } - if checker.enabled(Rule::ForLoopWrites) { - if let Some(diagnostic) = refurb::rules::for_loop_writes_binding(checker, binding) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::ForLoopWrites) { + refurb::rules::for_loop_writes_binding(checker, binding); } - if checker.enabled(Rule::CustomTypeVarForSelf) { - if let Some(diagnostic) = - flake8_pyi::rules::custom_type_var_instead_of_self(checker, binding) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::CustomTypeVarForSelf) { + flake8_pyi::rules::custom_type_var_instead_of_self(checker, binding); } - if checker.enabled(Rule::PrivateTypeParameter) { - if let Some(diagnostic) = pyupgrade::rules::private_type_parameter(checker, binding) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::PrivateTypeParameter) { + pyupgrade::rules::private_type_parameter(checker, binding); } } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs b/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs index 356c0ca8992e9..46b74c13f0bc1 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/comprehension.rs @@ -6,10 +6,10 @@ use crate::rules::{flake8_simplify, refurb}; /// Run lint rules over a [`Comprehension`] syntax nodes. pub(crate) fn comprehension(comprehension: &Comprehension, checker: &Checker) { - if checker.enabled(Rule::InDictKeys) { + if checker.is_rule_enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension); } - if checker.enabled(Rule::ReadlinesInFor) { + if checker.is_rule_enabled(Rule::ReadlinesInFor) { refurb::rules::readlines_in_comprehension(checker, comprehension); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs index 7bc5cf097f9e3..d337081c610fd 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs @@ -14,28 +14,31 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) { let Stmt::For(stmt_for) = checker.semantic.current_statement() else { unreachable!("Expected Stmt::For"); }; - if checker.enabled(Rule::UnusedLoopControlVariable) { + if checker.is_rule_enabled(Rule::UnusedLoopControlVariable) { flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for); } - if checker.enabled(Rule::IncorrectDictIterator) { + if checker.is_rule_enabled(Rule::IncorrectDictIterator) { perflint::rules::incorrect_dict_iterator(checker, stmt_for); } - if checker.enabled(Rule::YieldInForLoop) { + if checker.is_rule_enabled(Rule::YieldInForLoop) { pyupgrade::rules::yield_in_for_loop(checker, stmt_for); } - if checker.enabled(Rule::UnnecessaryEnumerate) { + if checker.is_rule_enabled(Rule::UnnecessaryEnumerate) { refurb::rules::unnecessary_enumerate(checker, stmt_for); } - if checker.enabled(Rule::EnumerateForLoop) { + if checker.is_rule_enabled(Rule::EnumerateForLoop) { flake8_simplify::rules::enumerate_for_loop(checker, stmt_for); } - if checker.enabled(Rule::LoopIteratorMutation) { + if checker.is_rule_enabled(Rule::LoopIteratorMutation) { flake8_bugbear::rules::loop_iterator_mutation(checker, stmt_for); } - if checker.enabled(Rule::DictIndexMissingItems) { + if checker.is_rule_enabled(Rule::DictIndexMissingItems) { pylint::rules::dict_index_missing_items(checker, stmt_for); } - if checker.enabled(Rule::ManualListComprehension) { + if checker.is_rule_enabled(Rule::ManualDictComprehension) { + perflint::rules::manual_dict_comprehension(checker, stmt_for); + } + if checker.is_rule_enabled(Rule::ManualListComprehension) { perflint::rules::manual_list_comprehension(checker, stmt_for); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_lambdas.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_lambdas.rs index 2ec55d48509ac..c2738492edd32 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_lambdas.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_lambdas.rs @@ -15,13 +15,13 @@ pub(crate) fn deferred_lambdas(checker: &mut Checker) { unreachable!("Expected Expr::Lambda"); }; - if checker.enabled(Rule::UnnecessaryLambda) { + if checker.is_rule_enabled(Rule::UnnecessaryLambda) { pylint::rules::unnecessary_lambda(checker, lambda); } - if checker.enabled(Rule::ReimplementedContainerBuiltin) { + if checker.is_rule_enabled(Rule::ReimplementedContainerBuiltin) { flake8_pie::rules::reimplemented_container_builtin(checker, lambda); } - if checker.enabled(Rule::BuiltinLambdaArgumentShadowing) { + if checker.is_rule_enabled(Rule::BuiltinLambdaArgumentShadowing) { flake8_builtins::rules::builtin_lambda_argument_shadowing(checker, lambda); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs index 36a8af20fa718..44b39d351fb27 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs @@ -1,12 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Fix}; -use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::{Binding, BindingKind, Imported, ResolvedReference, ScopeKind}; -use ruff_text_size::Ranged; -use rustc_hash::FxHashMap; +use ruff_python_semantic::{Binding, ScopeKind}; use crate::checkers::ast::Checker; use crate::codes::Rule; -use crate::fix; use crate::rules::{ flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pep8_naming, pyflakes, pylint, ruff, @@ -14,7 +9,7 @@ use crate::rules::{ /// Run lint rules over all deferred scopes in the [`SemanticModel`]. pub(crate) fn deferred_scopes(checker: &Checker) { - if !checker.any_enabled(&[ + if !checker.any_rule_enabled(&[ Rule::AsyncioDanglingTask, Rule::BadStaticmethodArgument, Rule::BuiltinAttributeShadowing, @@ -58,7 +53,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { // used at runtime, then by default, we avoid flagging any other // imports from that model as typing-only. let enforce_typing_only_imports = !checker.source_type.is_stub() - && checker.any_enabled(&[ + && checker.any_rule_enabled(&[ Rule::TypingOnlyFirstPartyImport, Rule::TypingOnlyStandardLibraryImport, Rule::TypingOnlyThirdPartyImport, @@ -76,7 +71,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { flake8_type_checking::helpers::is_valid_runtime_import( binding, &checker.semantic, - &checker.settings.flake8_type_checking, + &checker.settings().flake8_type_checking, ) }) .collect() @@ -89,309 +84,66 @@ pub(crate) fn deferred_scopes(checker: &Checker) { for scope_id in checker.analyze.scopes.iter().rev().copied() { let scope = &checker.semantic.scopes[scope_id]; - if checker.enabled(Rule::UndefinedLocal) { + if checker.is_rule_enabled(Rule::UndefinedLocal) { pyflakes::rules::undefined_local(checker, scope_id, scope); } - if checker.enabled(Rule::GlobalVariableNotAssigned) { - for (name, binding_id) in scope.bindings() { - let binding = checker.semantic.binding(binding_id); - // If the binding is a `global`, then it's a top-level `global` that was never - // assigned in the current scope. If it were assigned, the `global` would be - // shadowed by the assignment. - if binding.kind.is_global() { - // If the binding was conditionally deleted, it will include a reference within - // a `Del` context, but won't be shadowed by a `BindingKind::Deletion`, as in: - // ```python - // if condition: - // del var - // ``` - if binding - .references - .iter() - .map(|id| checker.semantic.reference(*id)) - .all(ResolvedReference::is_load) - { - checker.report_diagnostic(Diagnostic::new( - pylint::rules::GlobalVariableNotAssigned { - name: (*name).to_string(), - }, - binding.range(), - )); - } - } - } + if checker.is_rule_enabled(Rule::GlobalVariableNotAssigned) { + pylint::rules::global_variable_not_assigned(checker, scope); } - if checker.enabled(Rule::RedefinedArgumentFromLocal) { - for (name, binding_id) in scope.bindings() { - for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) { - let binding = &checker.semantic.bindings[shadow.binding_id()]; - if !matches!( - binding.kind, - BindingKind::LoopVar - | BindingKind::BoundException - | BindingKind::WithItemVar - ) { - continue; - } - let shadowed = &checker.semantic.bindings[shadow.shadowed_id()]; - if !shadowed.kind.is_argument() { - continue; - } - if checker.settings.dummy_variable_rgx.is_match(name) { - continue; - } - let scope = &checker.semantic.scopes[binding.scope]; - if scope.kind.is_generator() { - continue; - } - checker.report_diagnostic(Diagnostic::new( - pylint::rules::RedefinedArgumentFromLocal { - name: name.to_string(), - }, - binding.range(), - )); - } - } + if checker.is_rule_enabled(Rule::RedefinedArgumentFromLocal) { + pylint::rules::redefined_argument_from_local(checker, scope_id, scope); } - if checker.enabled(Rule::ImportShadowedByLoopVar) { - for (name, binding_id) in scope.bindings() { - for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) { - // If the shadowing binding isn't a loop variable, abort. - let binding = &checker.semantic.bindings[shadow.binding_id()]; - if !binding.kind.is_loop_var() { - continue; - } - - // If the shadowed binding isn't an import, abort. - let shadowed = &checker.semantic.bindings[shadow.shadowed_id()]; - if !matches!( - shadowed.kind, - BindingKind::Import(..) - | BindingKind::FromImport(..) - | BindingKind::SubmoduleImport(..) - | BindingKind::FutureImport - ) { - continue; - } - - // If the bindings are in different forks, abort. - if shadowed.source.is_none_or(|left| { - binding - .source - .is_none_or(|right| !checker.semantic.same_branch(left, right)) - }) { - continue; - } - - checker.report_diagnostic(Diagnostic::new( - pyflakes::rules::ImportShadowedByLoopVar { - name: name.to_string(), - row: checker.compute_source_row(shadowed.start()), - }, - binding.range(), - )); - } - } + if checker.is_rule_enabled(Rule::ImportShadowedByLoopVar) { + pyflakes::rules::import_shadowed_by_loop_var(checker, scope_id, scope); } - if checker.enabled(Rule::RedefinedWhileUnused) { - // Index the redefined bindings by statement. - let mut redefinitions = FxHashMap::default(); - - for (name, binding_id) in scope.bindings() { - for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) { - // If the shadowing binding is a loop variable, abort, to avoid overlap - // with F402. - let binding = &checker.semantic.bindings[shadow.binding_id()]; - if binding.kind.is_loop_var() { - continue; - } - - // If the shadowed binding is used, abort. - let shadowed = &checker.semantic.bindings[shadow.shadowed_id()]; - if shadowed.is_used() { - continue; - } - - // If the shadowing binding isn't considered a "redefinition" of the - // shadowed binding, abort. - if !binding.redefines(shadowed) { - continue; - } - - if shadow.same_scope() { - // If the symbol is a dummy variable, abort, unless the shadowed - // binding is an import. - if !matches!( - shadowed.kind, - BindingKind::Import(..) - | BindingKind::FromImport(..) - | BindingKind::SubmoduleImport(..) - | BindingKind::FutureImport - ) && checker.settings.dummy_variable_rgx.is_match(name) - { - continue; - } - - let Some(node_id) = shadowed.source else { - continue; - }; - - // If this is an overloaded function, abort. - if shadowed.kind.is_function_definition() { - if checker - .semantic - .statement(node_id) - .as_function_def_stmt() - .is_some_and(|function| { - visibility::is_overload( - &function.decorator_list, - &checker.semantic, - ) - }) - { - continue; - } - } - } else { - // Only enforce cross-scope shadowing for imports. - if !matches!( - shadowed.kind, - BindingKind::Import(..) - | BindingKind::FromImport(..) - | BindingKind::SubmoduleImport(..) - | BindingKind::FutureImport - ) { - continue; - } - } - - // If the bindings are in different forks, abort. - if shadowed.source.is_none_or(|left| { - binding - .source - .is_none_or(|right| !checker.semantic.same_branch(left, right)) - }) { - continue; - } - - redefinitions - .entry(binding.source) - .or_insert_with(Vec::new) - .push((shadowed, binding)); - } - } - - // Create a fix for each source statement. - let mut fixes = FxHashMap::default(); - for (source, entries) in &redefinitions { - let Some(source) = source else { - continue; - }; - - let member_names = entries - .iter() - .filter_map(|(shadowed, binding)| { - if let Some(shadowed_import) = shadowed.as_any_import() { - if let Some(import) = binding.as_any_import() { - if shadowed_import.qualified_name() == import.qualified_name() { - return Some(import.member_name()); - } - } - } - None - }) - .collect::>(); - - if !member_names.is_empty() { - let statement = checker.semantic.statement(*source); - let parent = checker.semantic.parent_statement(*source); - let Ok(edit) = fix::edits::remove_unused_imports( - member_names.iter().map(std::convert::AsRef::as_ref), - statement, - parent, - checker.locator(), - checker.stylist(), - checker.indexer(), - ) else { - continue; - }; - fixes.insert( - *source, - Fix::safe_edit(edit).isolate(Checker::isolation( - checker.semantic().parent_statement_id(*source), - )), - ); - } - } - - // Create diagnostics for each statement. - for (source, entries) in &redefinitions { - for (shadowed, binding) in entries { - let mut diagnostic = Diagnostic::new( - pyflakes::rules::RedefinedWhileUnused { - name: binding.name(checker.source()).to_string(), - row: checker.compute_source_row(shadowed.start()), - }, - binding.range(), - ); - - if let Some(range) = binding.parent_range(&checker.semantic) { - diagnostic.set_parent(range.start()); - } - - if let Some(fix) = source.as_ref().and_then(|source| fixes.get(source)) { - diagnostic.set_fix(fix.clone()); - } - - checker.report_diagnostic(diagnostic); - } - } + if checker.is_rule_enabled(Rule::RedefinedWhileUnused) { + pyflakes::rules::redefined_while_unused(checker, scope_id, scope); } if checker.source_type.is_stub() || matches!(scope.kind, ScopeKind::Module | ScopeKind::Function(_)) { - if checker.enabled(Rule::UnusedPrivateTypeVar) { + if checker.is_rule_enabled(Rule::UnusedPrivateTypeVar) { flake8_pyi::rules::unused_private_type_var(checker, scope); } - if checker.enabled(Rule::UnusedPrivateProtocol) { + if checker.is_rule_enabled(Rule::UnusedPrivateProtocol) { flake8_pyi::rules::unused_private_protocol(checker, scope); } - if checker.enabled(Rule::UnusedPrivateTypeAlias) { + if checker.is_rule_enabled(Rule::UnusedPrivateTypeAlias) { flake8_pyi::rules::unused_private_type_alias(checker, scope); } - if checker.enabled(Rule::UnusedPrivateTypedDict) { + if checker.is_rule_enabled(Rule::UnusedPrivateTypedDict) { flake8_pyi::rules::unused_private_typed_dict(checker, scope); } } - if checker.enabled(Rule::AsyncioDanglingTask) { + if checker.is_rule_enabled(Rule::AsyncioDanglingTask) { ruff::rules::asyncio_dangling_binding(scope, checker); } if let Some(class_def) = scope.kind.as_class() { - if checker.enabled(Rule::BuiltinAttributeShadowing) { + if checker.is_rule_enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( checker, scope_id, scope, class_def, ); } - if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) { + if checker.is_rule_enabled(Rule::FunctionCallInDataclassDefaultArgument) { ruff::rules::function_call_in_dataclass_default(checker, class_def); } - if checker.enabled(Rule::MutableClassDefault) { + if checker.is_rule_enabled(Rule::MutableClassDefault) { ruff::rules::mutable_class_default(checker, class_def); } - if checker.enabled(Rule::MutableDataclassDefault) { + if checker.is_rule_enabled(Rule::MutableDataclassDefault) { ruff::rules::mutable_dataclass_default(checker, class_def); } } if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) { - if checker.any_enabled(&[Rule::UnusedVariable, Rule::UnusedUnpackedVariable]) + if checker.any_rule_enabled(&[Rule::UnusedVariable, Rule::UnusedUnpackedVariable]) && !(scope.uses_locals() && scope.kind.is_function()) { let unused_bindings = scope @@ -404,7 +156,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { && binding.is_unused() && !binding.is_nonlocal() && !binding.is_global() - && !checker.settings.dummy_variable_rgx.is_match(name) + && !checker.settings().dummy_variable_rgx.is_match(name) && !matches!( name, "__tracebackhide__" @@ -420,22 +172,22 @@ pub(crate) fn deferred_scopes(checker: &Checker) { }); for (unused_name, unused_binding) in unused_bindings { - if checker.enabled(Rule::UnusedVariable) { + if checker.is_rule_enabled(Rule::UnusedVariable) { pyflakes::rules::unused_variable(checker, unused_name, unused_binding); } - if checker.enabled(Rule::UnusedUnpackedVariable) { + if checker.is_rule_enabled(Rule::UnusedUnpackedVariable) { ruff::rules::unused_unpacked_variable(checker, unused_name, unused_binding); } } } - if checker.enabled(Rule::UnusedAnnotation) { + if checker.is_rule_enabled(Rule::UnusedAnnotation) { pyflakes::rules::unused_annotation(checker, scope); } if !checker.source_type.is_stub() { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::UnusedClassMethodArgument, Rule::UnusedFunctionArgument, Rule::UnusedLambdaArgument, @@ -449,7 +201,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { if !checker.source_type.is_stub() - && checker.enabled(Rule::RuntimeImportInTypeCheckingBlock) + && checker.is_rule_enabled(Rule::RuntimeImportInTypeCheckingBlock) { flake8_type_checking::rules::runtime_import_in_type_checking_block(checker, scope); } @@ -469,37 +221,37 @@ pub(crate) fn deferred_scopes(checker: &Checker) { ); } - if checker.enabled(Rule::UnusedImport) { + if checker.is_rule_enabled(Rule::UnusedImport) { pyflakes::rules::unused_import(checker, scope); } - if checker.enabled(Rule::ImportPrivateName) { + if checker.is_rule_enabled(Rule::ImportPrivateName) { pylint::rules::import_private_name(checker, scope); } } if scope.kind.is_function() { - if checker.enabled(Rule::NoSelfUse) { + if checker.is_rule_enabled(Rule::NoSelfUse) { pylint::rules::no_self_use(checker, scope_id, scope); } - if checker.enabled(Rule::TooManyLocals) { + if checker.is_rule_enabled(Rule::TooManyLocals) { pylint::rules::too_many_locals(checker, scope); } - if checker.enabled(Rule::SingledispatchMethod) { + if checker.is_rule_enabled(Rule::SingledispatchMethod) { pylint::rules::singledispatch_method(checker, scope); } - if checker.enabled(Rule::SingledispatchmethodFunction) { + if checker.is_rule_enabled(Rule::SingledispatchmethodFunction) { pylint::rules::singledispatchmethod_function(checker, scope); } - if checker.enabled(Rule::BadStaticmethodArgument) { + if checker.is_rule_enabled(Rule::BadStaticmethodArgument) { pylint::rules::bad_staticmethod_argument(checker, scope); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::InvalidFirstArgumentNameForClassMethod, Rule::InvalidFirstArgumentNameForMethod, ]) { diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index f0a4cd2f0a9dd..9938ba4b33070 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -17,7 +17,7 @@ use crate::{docstrings, warn_user}; /// it is expected that all [`Definition`] nodes have been visited by the time, and that this /// method will not recurse into any other nodes. pub(crate) fn definitions(checker: &mut Checker) { - let enforce_annotations = checker.any_enabled(&[ + let enforce_annotations = checker.any_rule_enabled(&[ Rule::AnyType, Rule::MissingReturnTypeClassMethod, Rule::MissingReturnTypePrivateFunction, @@ -28,10 +28,11 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::MissingTypeFunctionArgument, Rule::MissingTypeKwargs, ]); - let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub); - let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable); - let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName); - let enforce_docstrings = checker.any_enabled(&[ + let enforce_stubs = + checker.source_type.is_stub() && checker.is_rule_enabled(Rule::DocstringInStub); + let enforce_stubs_and_runtime = checker.is_rule_enabled(Rule::IterMethodReturnIterable); + let enforce_dunder_method = checker.is_rule_enabled(Rule::BadDunderMethodName); + let enforce_docstrings = checker.any_rule_enabled(&[ Rule::MissingBlankLineAfterLastSection, Rule::MissingBlankLineAfterSummary, Rule::BlankLineBeforeClass, @@ -79,7 +80,7 @@ pub(crate) fn definitions(checker: &mut Checker) { Rule::UndocumentedPublicNestedClass, Rule::UndocumentedPublicPackage, ]); - let enforce_pydoclint = checker.any_enabled(&[ + let enforce_pydoclint = checker.any_rule_enabled(&[ Rule::DocstringMissingReturns, Rule::DocstringExtraneousReturns, Rule::DocstringMissingYields, @@ -137,11 +138,7 @@ pub(crate) fn definitions(checker: &mut Checker) { &checker.semantic, ) }) { - checker.report_diagnostics(flake8_annotations::rules::definition( - checker, - definition, - *visibility, - )); + flake8_annotations::rules::definition(checker, definition, *visibility); } overloaded_name = flake8_annotations::helpers::overloaded_name(definition, &checker.semantic); @@ -170,7 +167,7 @@ pub(crate) fn definitions(checker: &mut Checker) { if enforce_docstrings || enforce_pydoclint { if pydocstyle::helpers::should_ignore_definition( definition, - &checker.settings.pydocstyle, + &checker.settings().pydocstyle, &checker.semantic, ) { continue; @@ -184,14 +181,14 @@ pub(crate) fn definitions(checker: &mut Checker) { // We don't recognise implicitly concatenated strings as valid docstrings in our model currently. let Some(sole_string_part) = string_literal.as_single_part_string() else { - #[allow(deprecated)] + #[expect(deprecated)] let location = checker .locator .compute_source_location(string_literal.start()); warn_user!( "Docstring at {}:{}:{} contains implicit string concatenation; ignoring...", relativize_path(checker.path), - location.row, + location.line, location.column ); continue; @@ -206,74 +203,76 @@ pub(crate) fn definitions(checker: &mut Checker) { if !pydocstyle::rules::not_empty(checker, &docstring) { continue; } - if checker.enabled(Rule::UnnecessaryMultilineDocstring) { + if checker.is_rule_enabled(Rule::UnnecessaryMultilineDocstring) { pydocstyle::rules::one_liner(checker, &docstring); } - if checker.any_enabled(&[Rule::BlankLineAfterFunction, Rule::BlankLineBeforeFunction]) { + if checker + .any_rule_enabled(&[Rule::BlankLineAfterFunction, Rule::BlankLineBeforeFunction]) + { pydocstyle::rules::blank_before_after_function(checker, &docstring); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::BlankLineBeforeClass, Rule::IncorrectBlankLineAfterClass, Rule::IncorrectBlankLineBeforeClass, ]) { pydocstyle::rules::blank_before_after_class(checker, &docstring); } - if checker.enabled(Rule::MissingBlankLineAfterSummary) { + if checker.is_rule_enabled(Rule::MissingBlankLineAfterSummary) { pydocstyle::rules::blank_after_summary(checker, &docstring); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::DocstringTabIndentation, Rule::OverIndentation, Rule::UnderIndentation, ]) { pydocstyle::rules::indent(checker, &docstring); } - if checker.enabled(Rule::NewLineAfterLastParagraph) { + if checker.is_rule_enabled(Rule::NewLineAfterLastParagraph) { pydocstyle::rules::newline_after_last_paragraph(checker, &docstring); } - if checker.enabled(Rule::SurroundingWhitespace) { + if checker.is_rule_enabled(Rule::SurroundingWhitespace) { pydocstyle::rules::no_surrounding_whitespace(checker, &docstring); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::MultiLineSummaryFirstLine, Rule::MultiLineSummarySecondLine, ]) { pydocstyle::rules::multi_line_summary_start(checker, &docstring); } - if checker.enabled(Rule::TripleSingleQuotes) { + if checker.is_rule_enabled(Rule::TripleSingleQuotes) { pydocstyle::rules::triple_quotes(checker, &docstring); } - if checker.enabled(Rule::EscapeSequenceInDocstring) { + if checker.is_rule_enabled(Rule::EscapeSequenceInDocstring) { pydocstyle::rules::backslashes(checker, &docstring); } - if checker.enabled(Rule::MissingTrailingPeriod) { + if checker.is_rule_enabled(Rule::MissingTrailingPeriod) { pydocstyle::rules::ends_with_period(checker, &docstring); } - if checker.enabled(Rule::NonImperativeMood) { + if checker.is_rule_enabled(Rule::NonImperativeMood) { pydocstyle::rules::non_imperative_mood( checker, &docstring, - &checker.settings.pydocstyle, + &checker.settings().pydocstyle, ); } - if checker.enabled(Rule::SignatureInDocstring) { + if checker.is_rule_enabled(Rule::SignatureInDocstring) { pydocstyle::rules::no_signature(checker, &docstring); } - if checker.enabled(Rule::FirstWordUncapitalized) { + if checker.is_rule_enabled(Rule::FirstWordUncapitalized) { pydocstyle::rules::capitalized(checker, &docstring); } - if checker.enabled(Rule::DocstringStartsWithThis) { + if checker.is_rule_enabled(Rule::DocstringStartsWithThis) { pydocstyle::rules::starts_with_this(checker, &docstring); } - if checker.enabled(Rule::MissingTerminalPunctuation) { + if checker.is_rule_enabled(Rule::MissingTerminalPunctuation) { pydocstyle::rules::ends_with_punctuation(checker, &docstring); } - if checker.enabled(Rule::OverloadWithDocstring) { + if checker.is_rule_enabled(Rule::OverloadWithDocstring) { pydocstyle::rules::if_needed(checker, &docstring); } - let enforce_sections = checker.any_enabled(&[ + let enforce_sections = checker.any_rule_enabled(&[ Rule::MissingBlankLineAfterLastSection, Rule::BlankLinesBetweenHeaderAndContent, Rule::NonCapitalizedSectionName, @@ -293,7 +292,7 @@ pub(crate) fn definitions(checker: &mut Checker) { if enforce_sections || enforce_pydoclint { let section_contexts = pydocstyle::helpers::get_section_contexts( &docstring, - checker.settings.pydocstyle.convention(), + checker.settings().pydocstyle.convention(), ); if enforce_sections { @@ -301,7 +300,7 @@ pub(crate) fn definitions(checker: &mut Checker) { checker, &docstring, §ion_contexts, - checker.settings.pydocstyle.convention(), + checker.settings().pydocstyle.convention(), ); } @@ -311,7 +310,7 @@ pub(crate) fn definitions(checker: &mut Checker) { definition, &docstring, §ion_contexts, - checker.settings.pydocstyle.convention(), + checker.settings().pydocstyle.convention(), ); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/except_handler.rs b/crates/ruff_linter/src/checkers/ast/analyze/except_handler.rs index 9a68550216776..fd8bed0e29991 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/except_handler.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/except_handler.rs @@ -15,25 +15,19 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &Checker) name, body, range: _, + node_index: _, }) => { - if checker.enabled(Rule::BareExcept) { - if let Some(diagnostic) = pycodestyle::rules::bare_except( - type_.as_deref(), - body, - except_handler, - checker.locator, - ) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::BareExcept) { + pycodestyle::rules::bare_except(checker, type_.as_deref(), body, except_handler); } - if checker.enabled(Rule::RaiseWithoutFromInsideExcept) { + if checker.is_rule_enabled(Rule::RaiseWithoutFromInsideExcept) { flake8_bugbear::rules::raise_without_from_inside_except( checker, name.as_deref(), body, ); } - if checker.enabled(Rule::BlindExcept) { + if checker.is_rule_enabled(Rule::BlindExcept) { flake8_blind_except::rules::blind_except( checker, type_.as_deref(), @@ -41,42 +35,42 @@ pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &Checker) body, ); } - if checker.enabled(Rule::TryExceptPass) { + if checker.is_rule_enabled(Rule::TryExceptPass) { flake8_bandit::rules::try_except_pass( checker, except_handler, type_.as_deref(), body, - checker.settings.flake8_bandit.check_typed_exception, + checker.settings().flake8_bandit.check_typed_exception, ); } - if checker.enabled(Rule::TryExceptContinue) { + if checker.is_rule_enabled(Rule::TryExceptContinue) { flake8_bandit::rules::try_except_continue( checker, except_handler, type_.as_deref(), body, - checker.settings.flake8_bandit.check_typed_exception, + checker.settings().flake8_bandit.check_typed_exception, ); } - if checker.enabled(Rule::ExceptWithEmptyTuple) { + if checker.is_rule_enabled(Rule::ExceptWithEmptyTuple) { flake8_bugbear::rules::except_with_empty_tuple(checker, except_handler); } - if checker.enabled(Rule::ExceptWithNonExceptionClasses) { + if checker.is_rule_enabled(Rule::ExceptWithNonExceptionClasses) { flake8_bugbear::rules::except_with_non_exception_classes(checker, except_handler); } - if checker.enabled(Rule::BinaryOpException) { + if checker.is_rule_enabled(Rule::BinaryOpException) { pylint::rules::binary_op_exception(checker, except_handler); } if let Some(name) = name { - if checker.enabled(Rule::AmbiguousVariableName) { + if checker.is_rule_enabled(Rule::AmbiguousVariableName) { pycodestyle::rules::ambiguous_variable_name( checker, name.as_str(), name.range(), ); } - if checker.enabled(Rule::BuiltinVariableShadowing) { + if checker.is_rule_enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range()); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 896b6f9dfea5f..80793a38114ba 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1,14 +1,15 @@ use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Operator}; use ruff_python_literal::cformat::{CFormatError, CFormatErrorType}; -use ruff_diagnostics::Diagnostic; - use ruff_python_ast::types::Node; -use ruff_python_semantic::analyze::typing; use ruff_python_semantic::ScopeKind; +use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::preview::{ + is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled, +}; use crate::registry::Rule; use crate::rules::{ airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, @@ -25,26 +26,26 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { match expr { Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => { // Ex) Optional[...], Union[...] - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::FutureRewritableTypeAnnotation, Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationOptional, ]) { if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic) { - if checker.enabled(Rule::FutureRewritableTypeAnnotation) { + if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() && checker.target_version() < PythonVersion::PY310 && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings.pyupgrade.keep_runtime_typing + && !checker.settings().pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( checker, value, ); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationOptional, ]) { @@ -53,7 +54,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.future_annotations_or_stub() && checker.semantic.in_annotation() - && !checker.settings.pyupgrade.keep_runtime_typing) + && !checker.settings().pyupgrade.keep_runtime_typing) { pyupgrade::rules::non_pep604_annotation(checker, expr, slice, operator); } @@ -62,7 +63,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) list[...] - if checker.enabled(Rule::FutureRequiredTypeAnnotation) { + if checker.is_rule_enabled(Rule::FutureRequiredTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() && checker.target_version() < PythonVersion::PY39 && checker.semantic.in_annotation() @@ -79,7 +80,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) Union[...] - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::UnnecessaryLiteralUnion, Rule::DuplicateUnionMember, Rule::RedundantLiteralUnion, @@ -89,58 +90,64 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { // Avoid duplicate checks if the parent is a union, since these rules already // traverse nested unions. if !checker.semantic.in_nested_union() { - if checker.enabled(Rule::UnnecessaryLiteralUnion) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralUnion) { flake8_pyi::rules::unnecessary_literal_union(checker, expr); } - if checker.enabled(Rule::DuplicateUnionMember) { + if checker.is_rule_enabled(Rule::DuplicateUnionMember) + // Avoid duplicate checks inside `Optional` + && !( + is_optional_as_none_in_union_enabled(checker.settings()) + && checker.semantic.inside_optional() + ) + { flake8_pyi::rules::duplicate_union_member(checker, expr); } - if checker.enabled(Rule::RedundantLiteralUnion) { + if checker.is_rule_enabled(Rule::RedundantLiteralUnion) { flake8_pyi::rules::redundant_literal_union(checker, expr); } - if checker.enabled(Rule::UnnecessaryTypeUnion) { + if checker.is_rule_enabled(Rule::UnnecessaryTypeUnion) { flake8_pyi::rules::unnecessary_type_union(checker, expr); } - if checker.enabled(Rule::NoneNotAtEndOfUnion) { + if checker.is_rule_enabled(Rule::NoneNotAtEndOfUnion) { ruff::rules::none_not_at_end_of_union(checker, expr); } } } // Ex) Literal[...] - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::DuplicateLiteralMember, Rule::RedundantBoolLiteral, Rule::RedundantNoneLiteral, Rule::UnnecessaryNestedLiteral, ]) { if !checker.semantic.in_nested_literal() { - if checker.enabled(Rule::DuplicateLiteralMember) { + if checker.is_rule_enabled(Rule::DuplicateLiteralMember) { flake8_pyi::rules::duplicate_literal_member(checker, expr); } - if checker.enabled(Rule::RedundantBoolLiteral) { + if checker.is_rule_enabled(Rule::RedundantBoolLiteral) { ruff::rules::redundant_bool_literal(checker, expr); } - if checker.enabled(Rule::RedundantNoneLiteral) { + if checker.is_rule_enabled(Rule::RedundantNoneLiteral) { flake8_pyi::rules::redundant_none_literal(checker, expr); } - if checker.enabled(Rule::UnnecessaryNestedLiteral) { + if checker.is_rule_enabled(Rule::UnnecessaryNestedLiteral) { ruff::rules::unnecessary_nested_literal(checker, expr); } } } - if checker.enabled(Rule::NeverUnion) { + if checker.is_rule_enabled(Rule::NeverUnion) { ruff::rules::never_union(checker, expr); } - if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) { + if checker.is_rule_enabled(Rule::UnnecessaryDefaultTypeArgs) { if checker.target_version() >= PythonVersion::PY313 { pyupgrade::rules::unnecessary_default_type_args(checker, expr); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SysVersionSlice3, Rule::SysVersion2, Rule::SysVersion0, @@ -148,97 +155,110 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_2020::rules::subscript(checker, value, slice); } - if checker.enabled(Rule::UncapitalizedEnvironmentVariables) { + if checker.is_rule_enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(checker, expr); } - if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { + if checker.is_rule_enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr); } - if checker.enabled(Rule::InvalidIndexType) { + if checker.is_rule_enabled(Rule::InvalidIndexType) { ruff::rules::invalid_index_type(checker, subscript); } - if checker.enabled(Rule::SliceCopy) { + if checker.is_rule_enabled(Rule::SliceCopy) { refurb::rules::slice_copy(checker, subscript); } - if checker.enabled(Rule::PotentialIndexError) { + if checker.is_rule_enabled(Rule::PotentialIndexError) { pylint::rules::potential_index_error(checker, value, slice); } - if checker.enabled(Rule::SortedMinMax) { + if checker.is_rule_enabled(Rule::SortedMinMax) { refurb::rules::sorted_min_max(checker, subscript); } - if checker.enabled(Rule::FStringNumberFormat) { + if checker.is_rule_enabled(Rule::FStringNumberFormat) { refurb::rules::fstring_number_format(checker, subscript); } - if checker.enabled(Rule::IncorrectlyParenthesizedTupleInSubscript) { + if checker.is_rule_enabled(Rule::IncorrectlyParenthesizedTupleInSubscript) { ruff::rules::subscript_with_parenthesized_tuple(checker, subscript); } - if checker.enabled(Rule::NonPEP646Unpack) { + if checker.is_rule_enabled(Rule::NonPEP646Unpack) { pyupgrade::rules::use_pep646_unpack(checker, subscript); } - if checker.enabled(Rule::Airflow3Removal) { + if checker.is_rule_enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_expr(checker, expr); } + if checker.is_rule_enabled(Rule::MissingMaxsplitArg) { + pylint::rules::missing_maxsplit_arg(checker, value, slice, expr); + } + if checker.is_rule_enabled(Rule::AccessAnnotationsFromClassDict) { + ruff::rules::access_annotations_from_class_dict_by_key(checker, subscript); + } pandas_vet::rules::subscript(checker, value, expr); } Expr::Tuple(ast::ExprTuple { elts, ctx, range: _, + node_index: _, parenthesized: _, }) | Expr::List(ast::ExprList { elts, ctx, range: _, + node_index: _, }) => { if ctx.is_store() { - let check_too_many_expressions = checker.enabled(Rule::ExpressionsInStarAssignment); + let check_too_many_expressions = + checker.is_rule_enabled(Rule::ExpressionsInStarAssignment); let check_two_starred_expressions = - checker.enabled(Rule::MultipleStarredExpressions); - if let Some(diagnostic) = pyflakes::rules::starred_expressions( + checker.is_rule_enabled(Rule::MultipleStarredExpressions); + pyflakes::rules::starred_expressions( + checker, elts, check_too_many_expressions, check_two_starred_expressions, expr.range(), - ) { - checker.report_diagnostic(diagnostic); - } + ); } } - Expr::Name(ast::ExprName { id, ctx, range }) => { + Expr::Name(ast::ExprName { + id, + ctx, + range, + node_index: _, + }) => { match ctx { ExprContext::Load => { - if checker.enabled(Rule::TypingTextStrAlias) { + if checker.is_rule_enabled(Rule::TypingTextStrAlias) { pyupgrade::rules::typing_text_str_alias(checker, expr); } - if checker.enabled(Rule::NumpyDeprecatedTypeAlias) { + if checker.is_rule_enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(checker, expr); } - if checker.enabled(Rule::NumpyDeprecatedFunction) { + if checker.is_rule_enabled(Rule::NumpyDeprecatedFunction) { numpy::rules::deprecated_function(checker, expr); } - if checker.enabled(Rule::Numpy2Deprecation) { + if checker.is_rule_enabled(Rule::Numpy2Deprecation) { numpy::rules::numpy_2_0_deprecation(checker, expr); } - if checker.enabled(Rule::CollectionsNamedTuple) { + if checker.is_rule_enabled(Rule::CollectionsNamedTuple) { flake8_pyi::rules::collections_named_tuple(checker, expr); } - if checker.enabled(Rule::RegexFlagAlias) { + if checker.is_rule_enabled(Rule::RegexFlagAlias) { refurb::rules::regex_flag_alias(checker, expr); } - if checker.enabled(Rule::Airflow3Removal) { + if checker.is_rule_enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_expr(checker, expr); } - if checker.enabled(Rule::Airflow3SuggestedUpdate) { + if checker.is_rule_enabled(Rule::Airflow3SuggestedUpdate) { airflow::rules::airflow_3_0_suggested_update_expr(checker, expr); } - if checker.enabled(Rule::Airflow3MovedToProvider) { + if checker.is_rule_enabled(Rule::Airflow3MovedToProvider) { airflow::rules::moved_to_provider_in_3(checker, expr); } - if checker.enabled(Rule::Airflow3SuggestedToMoveToProvider) { + if checker.is_rule_enabled(Rule::Airflow3SuggestedToMoveToProvider) { airflow::rules::suggested_to_move_to_provider_in_3(checker, expr); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SuspiciousPickleUsage, Rule::SuspiciousMarshalUsage, Rule::SuspiciousInsecureHashUsage, @@ -265,30 +285,30 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) List[...] - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::FutureRewritableTypeAnnotation, Rule::NonPEP585Annotation, ]) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { - if checker.enabled(Rule::FutureRewritableTypeAnnotation) { + if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() && checker.target_version() < PythonVersion::PY39 && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings.pyupgrade.keep_runtime_typing + && !checker.settings().pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation(checker, expr); } } - if checker.enabled(Rule::NonPEP585Annotation) { + if checker.is_rule_enabled(Rule::NonPEP585Annotation) { if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY39 || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.future_annotations_or_stub() && checker.semantic.in_annotation() - && !checker.settings.pyupgrade.keep_runtime_typing) + && !checker.settings().pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation( checker, @@ -301,14 +321,14 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } ExprContext::Store => { - if checker.enabled(Rule::NonLowercaseVariableInFunction) { + if checker.is_rule_enabled(Rule::NonLowercaseVariableInFunction) { if checker.semantic.current_scope().kind.is_function() { pep8_naming::rules::non_lowercase_variable_in_function( checker, expr, id, ); } } - if checker.enabled(Rule::MixedCaseVariableInClassScope) { + if checker.is_rule_enabled(Rule::MixedCaseVariableInClassScope) { if let ScopeKind::Class(class_def) = &checker.semantic.current_scope().kind { pep8_naming::rules::mixed_case_variable_in_class_scope( @@ -316,37 +336,37 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ); } } - if checker.enabled(Rule::Airflow3Removal) { + if checker.is_rule_enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_expr(checker, expr); } - if checker.enabled(Rule::MixedCaseVariableInGlobalScope) { + if checker.is_rule_enabled(Rule::MixedCaseVariableInGlobalScope) { if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) { pep8_naming::rules::mixed_case_variable_in_global_scope( checker, expr, id, ); } } - if checker.enabled(Rule::AmbiguousVariableName) { + if checker.is_rule_enabled(Rule::AmbiguousVariableName) { pycodestyle::rules::ambiguous_variable_name(checker, id, expr.range()); } if !checker.semantic.current_scope().kind.is_class() { - if checker.enabled(Rule::BuiltinVariableShadowing) { + if checker.is_rule_enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing(checker, id, *range); } } } _ => {} } - if checker.enabled(Rule::SixPY3) { + if checker.is_rule_enabled(Rule::SixPY3) { flake8_2020::rules::name_or_attribute(checker, expr); } - if checker.enabled(Rule::UndocumentedWarn) { + if checker.is_rule_enabled(Rule::UndocumentedWarn) { flake8_logging::rules::undocumented_warn(checker, expr); } } Expr::Attribute(attribute) => { if attribute.ctx == ExprContext::Load { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SuspiciousPickleUsage, Rule::SuspiciousMarshalUsage, Rule::SuspiciousInsecureHashUsage, @@ -374,93 +394,93 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) typing.List[...] - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::FutureRewritableTypeAnnotation, Rule::NonPEP585Annotation, ]) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { - if checker.enabled(Rule::FutureRewritableTypeAnnotation) { + if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() && checker.target_version() < PythonVersion::PY39 && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings.pyupgrade.keep_runtime_typing + && !checker.settings().pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( checker, expr, ); } } - if checker.enabled(Rule::NonPEP585Annotation) { + if checker.is_rule_enabled(Rule::NonPEP585Annotation) { if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY39 || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.future_annotations_or_stub() && checker.semantic.in_annotation() - && !checker.settings.pyupgrade.keep_runtime_typing) + && !checker.settings().pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation(checker, expr, &replacement); } } } } - if checker.enabled(Rule::RegexFlagAlias) { + if checker.is_rule_enabled(Rule::RegexFlagAlias) { refurb::rules::regex_flag_alias(checker, expr); } - if checker.enabled(Rule::DatetimeTimezoneUTC) { + if checker.is_rule_enabled(Rule::DatetimeTimezoneUTC) { if checker.target_version() >= PythonVersion::PY311 { pyupgrade::rules::datetime_utc_alias(checker, expr); } } - if checker.enabled(Rule::TypingTextStrAlias) { + if checker.is_rule_enabled(Rule::TypingTextStrAlias) { pyupgrade::rules::typing_text_str_alias(checker, expr); } - if checker.enabled(Rule::NumpyDeprecatedTypeAlias) { + if checker.is_rule_enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(checker, expr); } - if checker.enabled(Rule::NumpyDeprecatedFunction) { + if checker.is_rule_enabled(Rule::NumpyDeprecatedFunction) { numpy::rules::deprecated_function(checker, expr); } - if checker.enabled(Rule::Numpy2Deprecation) { + if checker.is_rule_enabled(Rule::Numpy2Deprecation) { numpy::rules::numpy_2_0_deprecation(checker, expr); } - if checker.enabled(Rule::DeprecatedMockImport) { + if checker.is_rule_enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_attribute(checker, attribute); } - if checker.enabled(Rule::SixPY3) { + if checker.is_rule_enabled(Rule::SixPY3) { flake8_2020::rules::name_or_attribute(checker, expr); } - if checker.enabled(Rule::DatetimeMinMax) { + if checker.is_rule_enabled(Rule::DatetimeMinMax) { flake8_datetimez::rules::datetime_min_max(checker, expr); } - if checker.enabled(Rule::BannedApi) { + if checker.is_rule_enabled(Rule::BannedApi) { flake8_tidy_imports::rules::banned_attribute_access(checker, expr); } - if checker.enabled(Rule::PrivateMemberAccess) { + if checker.is_rule_enabled(Rule::PrivateMemberAccess) { flake8_self::rules::private_member_access(checker, expr); } - if checker.enabled(Rule::CollectionsNamedTuple) { + if checker.is_rule_enabled(Rule::CollectionsNamedTuple) { flake8_pyi::rules::collections_named_tuple(checker, expr); } - if checker.enabled(Rule::UndocumentedWarn) { + if checker.is_rule_enabled(Rule::UndocumentedWarn) { flake8_logging::rules::undocumented_warn(checker, expr); } - if checker.enabled(Rule::PandasUseOfDotValues) { + if checker.is_rule_enabled(Rule::PandasUseOfDotValues) { pandas_vet::rules::attr(checker, attribute); } - if checker.enabled(Rule::ByteStringUsage) { + if checker.is_rule_enabled(Rule::ByteStringUsage) { flake8_pyi::rules::bytestring_attribute(checker, expr); } - if checker.enabled(Rule::Airflow3Removal) { + if checker.is_rule_enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_expr(checker, expr); } - if checker.enabled(Rule::Airflow3SuggestedUpdate) { + if checker.is_rule_enabled(Rule::Airflow3SuggestedUpdate) { airflow::rules::airflow_3_0_suggested_update_expr(checker, expr); } - if checker.enabled(Rule::Airflow3MovedToProvider) { + if checker.is_rule_enabled(Rule::Airflow3MovedToProvider) { airflow::rules::moved_to_provider_in_3(checker, expr); } - if checker.enabled(Rule::Airflow3SuggestedToMoveToProvider) { + if checker.is_rule_enabled(Rule::Airflow3SuggestedToMoveToProvider) { airflow::rules::suggested_to_move_to_provider_in_3(checker, expr); } } @@ -472,11 +492,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, }, ) => { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ // pylint Rule::BadStringFormatCharacter, // pyflakes @@ -504,7 +526,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { { if attr == "join" { // "...".join(...) call - if checker.enabled(Rule::StaticJoinToFString) { + if checker.is_rule_enabled(Rule::StaticJoinToFString) { flynt::rules::static_join_to_fstring( checker, expr, @@ -513,7 +535,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } else if matches!(attr, "split" | "rsplit") { // "...".split(...) call - if checker.enabled(Rule::SplitStaticString) { + if checker.is_rule_enabled(Rule::SplitStaticString) { flake8_simplify::rules::split_static_string( checker, attr, @@ -526,48 +548,52 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { let location = expr.range(); match pyflakes::format::FormatSummary::try_from(string_value.to_str()) { Err(e) => { - if checker.enabled(Rule::StringDotFormatInvalidFormat) { - checker.report_diagnostic(Diagnostic::new( - pyflakes::rules::StringDotFormatInvalidFormat { - message: pyflakes::format::error_to_string(&e), - }, - location, - )); - } + // F521 + checker.report_diagnostic_if_enabled( + pyflakes::rules::StringDotFormatInvalidFormat { + message: pyflakes::format::error_to_string(&e), + }, + location, + ); } Ok(summary) => { - if checker.enabled(Rule::StringDotFormatExtraNamedArguments) { + if checker + .is_rule_enabled(Rule::StringDotFormatExtraNamedArguments) + { pyflakes::rules::string_dot_format_extra_named_arguments( checker, call, &summary, keywords, ); } - if checker - .enabled(Rule::StringDotFormatExtraPositionalArguments) - { + if checker.is_rule_enabled( + Rule::StringDotFormatExtraPositionalArguments, + ) { pyflakes::rules::string_dot_format_extra_positional_arguments( checker, call, &summary, args, ); } - if checker.enabled(Rule::StringDotFormatMissingArguments) { + if checker + .is_rule_enabled(Rule::StringDotFormatMissingArguments) + { pyflakes::rules::string_dot_format_missing_argument( checker, call, &summary, args, keywords, ); } - if checker.enabled(Rule::StringDotFormatMixingAutomatic) { + if checker.is_rule_enabled(Rule::StringDotFormatMixingAutomatic) + { pyflakes::rules::string_dot_format_mixing_automatic( checker, call, &summary, ); } - if checker.enabled(Rule::FormatLiterals) { + if checker.is_rule_enabled(Rule::FormatLiterals) { pyupgrade::rules::format_literals(checker, call, &summary); } - if checker.enabled(Rule::FString) { + if checker.is_rule_enabled(Rule::FString) { pyupgrade::rules::f_strings(checker, call, &summary); } } } - if checker.enabled(Rule::BadStringFormatCharacter) { + if checker.is_rule_enabled(Rule::BadStringFormatCharacter) { pylint::rules::bad_string_format_character::call( checker, string_value.to_str(), @@ -578,82 +604,82 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } } - if checker.enabled(Rule::SuperWithoutBrackets) { + if checker.is_rule_enabled(Rule::SuperWithoutBrackets) { pylint::rules::super_without_brackets(checker, func); } - if checker.enabled(Rule::LenTest) { + if checker.is_rule_enabled(Rule::LenTest) { pylint::rules::len_test(checker, call); } - if checker.enabled(Rule::BitCount) { + if checker.is_rule_enabled(Rule::BitCount) { refurb::rules::bit_count(checker, call); } - if checker.enabled(Rule::TypeOfPrimitive) { + if checker.is_rule_enabled(Rule::TypeOfPrimitive) { pyupgrade::rules::type_of_primitive(checker, expr, func, args); } - if checker.enabled(Rule::DeprecatedUnittestAlias) { + if checker.is_rule_enabled(Rule::DeprecatedUnittestAlias) { pyupgrade::rules::deprecated_unittest_alias(checker, func); } - if checker.enabled(Rule::SuperCallWithParameters) { + if checker.is_rule_enabled(Rule::SuperCallWithParameters) { pyupgrade::rules::super_call_with_parameters(checker, call); } - if checker.enabled(Rule::UnnecessaryEncodeUTF8) { + if checker.is_rule_enabled(Rule::UnnecessaryEncodeUTF8) { pyupgrade::rules::unnecessary_encode_utf8(checker, call); } - if checker.enabled(Rule::RedundantOpenModes) { + if checker.is_rule_enabled(Rule::RedundantOpenModes) { pyupgrade::rules::redundant_open_modes(checker, call); } - if checker.enabled(Rule::NativeLiterals) { + if checker.is_rule_enabled(Rule::NativeLiterals) { pyupgrade::rules::native_literals( checker, call, checker.semantic().current_expression_parent(), ); } - if checker.enabled(Rule::OpenAlias) { + if checker.is_rule_enabled(Rule::OpenAlias) { pyupgrade::rules::open_alias(checker, expr, func); } - if checker.enabled(Rule::ReplaceUniversalNewlines) { + if checker.is_rule_enabled(Rule::ReplaceUniversalNewlines) { pyupgrade::rules::replace_universal_newlines(checker, call); } - if checker.enabled(Rule::ReplaceStdoutStderr) { + if checker.is_rule_enabled(Rule::ReplaceStdoutStderr) { pyupgrade::rules::replace_stdout_stderr(checker, call); } - if checker.enabled(Rule::OSErrorAlias) { + if checker.is_rule_enabled(Rule::OSErrorAlias) { pyupgrade::rules::os_error_alias_call(checker, func); } - if checker.enabled(Rule::TimeoutErrorAlias) { + if checker.is_rule_enabled(Rule::TimeoutErrorAlias) { if checker.target_version() >= PythonVersion::PY310 { pyupgrade::rules::timeout_error_alias_call(checker, func); } } - if checker.enabled(Rule::NonPEP604Isinstance) { + if checker.is_rule_enabled(Rule::NonPEP604Isinstance) { if checker.target_version() >= PythonVersion::PY310 { pyupgrade::rules::use_pep604_isinstance(checker, expr, func, args); } } - if checker.enabled(Rule::BlockingHttpCallInAsyncFunction) { + if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) { flake8_async::rules::blocking_http_call(checker, call); } - if checker.enabled(Rule::BlockingOpenCallInAsyncFunction) { + if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) { flake8_async::rules::blocking_open_call(checker, call); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::CreateSubprocessInAsyncFunction, Rule::RunProcessInAsyncFunction, Rule::WaitForProcessInAsyncFunction, ]) { flake8_async::rules::blocking_process_invocation(checker, call); } - if checker.enabled(Rule::BlockingSleepInAsyncFunction) { + if checker.is_rule_enabled(Rule::BlockingSleepInAsyncFunction) { flake8_async::rules::blocking_sleep(checker, call); } - if checker.enabled(Rule::LongSleepNotForever) { + if checker.is_rule_enabled(Rule::LongSleepNotForever) { flake8_async::rules::long_sleep_not_forever(checker, call); } - if checker.any_enabled(&[Rule::Print, Rule::PPrint]) { + if checker.any_rule_enabled(&[Rule::Print, Rule::PPrint]) { flake8_print::rules::print_call(checker, call); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SuspiciousPickleUsage, Rule::SuspiciousMarshalUsage, Rule::SuspiciousInsecureHashUsage, @@ -678,101 +704,101 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_bandit::rules::suspicious_function_call(checker, call); } - if checker.enabled(Rule::ReSubPositionalArgs) { + if checker.is_rule_enabled(Rule::ReSubPositionalArgs) { flake8_bugbear::rules::re_sub_positional_args(checker, call); } - if checker.enabled(Rule::UnreliableCallableCheck) { + if checker.is_rule_enabled(Rule::UnreliableCallableCheck) { flake8_bugbear::rules::unreliable_callable_check(checker, expr, func, args); } - if checker.enabled(Rule::StripWithMultiCharacters) { + if checker.is_rule_enabled(Rule::StripWithMultiCharacters) { flake8_bugbear::rules::strip_with_multi_characters(checker, expr, func, args); } - if checker.enabled(Rule::GetAttrWithConstant) { + if checker.is_rule_enabled(Rule::GetAttrWithConstant) { flake8_bugbear::rules::getattr_with_constant(checker, expr, func, args); } - if checker.enabled(Rule::SetAttrWithConstant) { + if checker.is_rule_enabled(Rule::SetAttrWithConstant) { flake8_bugbear::rules::setattr_with_constant(checker, expr, func, args); } - if checker.enabled(Rule::UselessContextlibSuppress) { + if checker.is_rule_enabled(Rule::UselessContextlibSuppress) { flake8_bugbear::rules::useless_contextlib_suppress(checker, expr, func, args); } - if checker.enabled(Rule::StarArgUnpackingAfterKeywordArg) { + if checker.is_rule_enabled(Rule::StarArgUnpackingAfterKeywordArg) { flake8_bugbear::rules::star_arg_unpacking_after_keyword_arg( checker, args, keywords, ); } - if checker.enabled(Rule::ZipWithoutExplicitStrict) { + if checker.is_rule_enabled(Rule::ZipWithoutExplicitStrict) { if checker.target_version() >= PythonVersion::PY310 { flake8_bugbear::rules::zip_without_explicit_strict(checker, call); } } - if checker.enabled(Rule::NoExplicitStacklevel) { + if checker.is_rule_enabled(Rule::NoExplicitStacklevel) { flake8_bugbear::rules::no_explicit_stacklevel(checker, call); } - if checker.enabled(Rule::MutableContextvarDefault) { + if checker.is_rule_enabled(Rule::MutableContextvarDefault) { flake8_bugbear::rules::mutable_contextvar_default(checker, call); } - if checker.enabled(Rule::UnnecessaryDictKwargs) { + if checker.is_rule_enabled(Rule::UnnecessaryDictKwargs) { flake8_pie::rules::unnecessary_dict_kwargs(checker, call); } - if checker.enabled(Rule::UnnecessaryRangeStart) { + if checker.is_rule_enabled(Rule::UnnecessaryRangeStart) { flake8_pie::rules::unnecessary_range_start(checker, call); } - if checker.enabled(Rule::ExecBuiltin) { + if checker.is_rule_enabled(Rule::ExecBuiltin) { flake8_bandit::rules::exec_used(checker, func); } - if checker.enabled(Rule::BadFilePermissions) { + if checker.is_rule_enabled(Rule::BadFilePermissions) { flake8_bandit::rules::bad_file_permissions(checker, call); } - if checker.enabled(Rule::RequestWithNoCertValidation) { + if checker.is_rule_enabled(Rule::RequestWithNoCertValidation) { flake8_bandit::rules::request_with_no_cert_validation(checker, call); } - if checker.enabled(Rule::UnsafeYAMLLoad) { + if checker.is_rule_enabled(Rule::UnsafeYAMLLoad) { flake8_bandit::rules::unsafe_yaml_load(checker, call); } - if checker.enabled(Rule::SnmpInsecureVersion) { + if checker.is_rule_enabled(Rule::SnmpInsecureVersion) { flake8_bandit::rules::snmp_insecure_version(checker, call); } - if checker.enabled(Rule::SnmpWeakCryptography) { + if checker.is_rule_enabled(Rule::SnmpWeakCryptography) { flake8_bandit::rules::snmp_weak_cryptography(checker, call); } - if checker.enabled(Rule::Jinja2AutoescapeFalse) { + if checker.is_rule_enabled(Rule::Jinja2AutoescapeFalse) { flake8_bandit::rules::jinja2_autoescape_false(checker, call); } - if checker.enabled(Rule::MakoTemplates) { + if checker.is_rule_enabled(Rule::MakoTemplates) { flake8_bandit::rules::mako_templates(checker, call); } - if checker.enabled(Rule::HardcodedPasswordFuncArg) { + if checker.is_rule_enabled(Rule::HardcodedPasswordFuncArg) { flake8_bandit::rules::hardcoded_password_func_arg(checker, keywords); } - if checker.enabled(Rule::HardcodedSQLExpression) { + if checker.is_rule_enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(checker, expr); } - if checker.enabled(Rule::HashlibInsecureHashFunction) { + if checker.is_rule_enabled(Rule::HashlibInsecureHashFunction) { flake8_bandit::rules::hashlib_insecure_hash_functions(checker, call); } - if checker.enabled(Rule::HashlibDigestHex) { + if checker.is_rule_enabled(Rule::HashlibDigestHex) { refurb::rules::hashlib_digest_hex(checker, call); } - if checker.enabled(Rule::RequestWithoutTimeout) { + if checker.is_rule_enabled(Rule::RequestWithoutTimeout) { flake8_bandit::rules::request_without_timeout(checker, call); } - if checker.enabled(Rule::ParamikoCall) { + if checker.is_rule_enabled(Rule::ParamikoCall) { flake8_bandit::rules::paramiko_call(checker, func); } - if checker.enabled(Rule::SSHNoHostKeyVerification) { + if checker.is_rule_enabled(Rule::SSHNoHostKeyVerification) { flake8_bandit::rules::ssh_no_host_key_verification(checker, call); } - if checker.enabled(Rule::LoggingConfigInsecureListen) { + if checker.is_rule_enabled(Rule::LoggingConfigInsecureListen) { flake8_bandit::rules::logging_config_insecure_listen(checker, call); } - if checker.enabled(Rule::FlaskDebugTrue) { + if checker.is_rule_enabled(Rule::FlaskDebugTrue) { flake8_bandit::rules::flask_debug_true(checker, call); } - if checker.enabled(Rule::WeakCryptographicKey) { + if checker.is_rule_enabled(Rule::WeakCryptographicKey) { flake8_bandit::rules::weak_cryptographic_key(checker, call); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SubprocessWithoutShellEqualsTrue, Rule::SubprocessPopenWithShellEqualsTrue, Rule::CallWithShellEqualsTrue, @@ -783,277 +809,321 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_bandit::rules::shell_injection(checker, call); } - if checker.enabled(Rule::DjangoExtra) { + if checker.is_rule_enabled(Rule::DjangoExtra) { flake8_bandit::rules::django_extra(checker, call); } - if checker.enabled(Rule::DjangoRawSql) { + if checker.is_rule_enabled(Rule::DjangoRawSql) { flake8_bandit::rules::django_raw_sql(checker, call); } - if checker.enabled(Rule::TarfileUnsafeMembers) { + if checker.is_rule_enabled(Rule::TarfileUnsafeMembers) { flake8_bandit::rules::tarfile_unsafe_members(checker, call); } - if checker.enabled(Rule::UnnecessaryGeneratorList) { + if checker.is_rule_enabled(Rule::UnnecessaryGeneratorList) { flake8_comprehensions::rules::unnecessary_generator_list(checker, call); } - if checker.enabled(Rule::UnnecessaryGeneratorSet) { + if checker.is_rule_enabled(Rule::UnnecessaryGeneratorSet) { flake8_comprehensions::rules::unnecessary_generator_set(checker, call); } - if checker.enabled(Rule::UnnecessaryGeneratorDict) { + if checker.is_rule_enabled(Rule::UnnecessaryGeneratorDict) { flake8_comprehensions::rules::unnecessary_generator_dict( checker, expr, func, args, keywords, ); } - if checker.enabled(Rule::UnnecessaryListComprehensionSet) { + if checker.is_rule_enabled(Rule::UnnecessaryListComprehensionSet) { flake8_comprehensions::rules::unnecessary_list_comprehension_set(checker, call); } - if checker.enabled(Rule::UnnecessaryListComprehensionDict) { + if checker.is_rule_enabled(Rule::UnnecessaryListComprehensionDict) { flake8_comprehensions::rules::unnecessary_list_comprehension_dict( checker, expr, func, args, keywords, ); } - if checker.enabled(Rule::UnnecessaryLiteralSet) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralSet) { flake8_comprehensions::rules::unnecessary_literal_set(checker, call); } - if checker.enabled(Rule::UnnecessaryLiteralDict) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralDict) { flake8_comprehensions::rules::unnecessary_literal_dict( checker, expr, func, args, keywords, ); } - if checker.enabled(Rule::UnnecessaryCollectionCall) { + if checker.is_rule_enabled(Rule::UnnecessaryCollectionCall) { flake8_comprehensions::rules::unnecessary_collection_call( checker, call, - &checker.settings.flake8_comprehensions, + &checker.settings().flake8_comprehensions, ); } - if checker.enabled(Rule::UnnecessaryLiteralWithinTupleCall) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinTupleCall) { flake8_comprehensions::rules::unnecessary_literal_within_tuple_call( checker, expr, call, ); } - if checker.enabled(Rule::UnnecessaryLiteralWithinListCall) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinListCall) { flake8_comprehensions::rules::unnecessary_literal_within_list_call(checker, call); } - if checker.enabled(Rule::UnnecessaryLiteralWithinDictCall) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralWithinDictCall) { flake8_comprehensions::rules::unnecessary_literal_within_dict_call(checker, call); } - if checker.enabled(Rule::UnnecessaryListCall) { + if checker.is_rule_enabled(Rule::UnnecessaryListCall) { flake8_comprehensions::rules::unnecessary_list_call(checker, expr, call); } - if checker.enabled(Rule::UnnecessaryCallAroundSorted) { + if checker.is_rule_enabled(Rule::UnnecessaryCallAroundSorted) { flake8_comprehensions::rules::unnecessary_call_around_sorted( checker, expr, func, args, ); } - if checker.enabled(Rule::UnnecessaryDoubleCastOrProcess) { + if checker.is_rule_enabled(Rule::UnnecessaryDoubleCastOrProcess) { flake8_comprehensions::rules::unnecessary_double_cast_or_process( checker, expr, func, args, keywords, ); } - if checker.enabled(Rule::UnnecessarySubscriptReversal) { + if checker.is_rule_enabled(Rule::UnnecessarySubscriptReversal) { flake8_comprehensions::rules::unnecessary_subscript_reversal(checker, call); } - if checker.enabled(Rule::UnnecessaryMap) { + if checker.is_rule_enabled(Rule::UnnecessaryMap) { flake8_comprehensions::rules::unnecessary_map(checker, call); } - if checker.enabled(Rule::UnnecessaryComprehensionInCall) { + if checker.is_rule_enabled(Rule::UnnecessaryComprehensionInCall) { flake8_comprehensions::rules::unnecessary_comprehension_in_call( checker, expr, func, args, keywords, ); } - if checker.enabled(Rule::BooleanPositionalValueInCall) { + if checker.is_rule_enabled(Rule::BooleanPositionalValueInCall) { flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, call); } - if checker.enabled(Rule::Debugger) { + if checker.is_rule_enabled(Rule::Debugger) { flake8_debugger::rules::debugger_call(checker, expr, func); } - if checker.enabled(Rule::PandasUseOfInplaceArgument) { + if checker.is_rule_enabled(Rule::PandasUseOfInplaceArgument) { pandas_vet::rules::inplace_argument(checker, call); } pandas_vet::rules::call(checker, func); - if checker.enabled(Rule::PandasUseOfDotReadTable) { + if checker.is_rule_enabled(Rule::PandasUseOfDotReadTable) { pandas_vet::rules::use_of_read_table(checker, call); } - if checker.enabled(Rule::PandasUseOfPdMerge) { + if checker.is_rule_enabled(Rule::PandasUseOfPdMerge) { pandas_vet::rules::use_of_pd_merge(checker, func); } - if checker.enabled(Rule::CallDatetimeWithoutTzinfo) { + if checker.is_rule_enabled(Rule::CallDatetimeWithoutTzinfo) { flake8_datetimez::rules::call_datetime_without_tzinfo(checker, call); } - if checker.enabled(Rule::CallDatetimeToday) { + if checker.is_rule_enabled(Rule::CallDatetimeToday) { flake8_datetimez::rules::call_datetime_today(checker, func, expr.range()); } - if checker.enabled(Rule::CallDatetimeUtcnow) { + if checker.is_rule_enabled(Rule::CallDatetimeUtcnow) { flake8_datetimez::rules::call_datetime_utcnow(checker, func, expr.range()); } - if checker.enabled(Rule::CallDatetimeUtcfromtimestamp) { + if checker.is_rule_enabled(Rule::CallDatetimeUtcfromtimestamp) { flake8_datetimez::rules::call_datetime_utcfromtimestamp( checker, func, expr.range(), ); } - if checker.enabled(Rule::CallDatetimeNowWithoutTzinfo) { + if checker.is_rule_enabled(Rule::CallDatetimeNowWithoutTzinfo) { flake8_datetimez::rules::call_datetime_now_without_tzinfo(checker, call); } - if checker.enabled(Rule::CallDatetimeFromtimestamp) { + if checker.is_rule_enabled(Rule::CallDatetimeFromtimestamp) { flake8_datetimez::rules::call_datetime_fromtimestamp(checker, call); } - if checker.enabled(Rule::CallDatetimeStrptimeWithoutZone) { + if checker.is_rule_enabled(Rule::CallDatetimeStrptimeWithoutZone) { flake8_datetimez::rules::call_datetime_strptime_without_zone(checker, call); } - if checker.enabled(Rule::CallDateToday) { + if checker.is_rule_enabled(Rule::CallDateToday) { flake8_datetimez::rules::call_date_today(checker, func, expr.range()); } - if checker.enabled(Rule::CallDateFromtimestamp) { + if checker.is_rule_enabled(Rule::CallDateFromtimestamp) { flake8_datetimez::rules::call_date_fromtimestamp(checker, func, expr.range()); } - if checker.enabled(Rule::UnnecessaryDirectLambdaCall) { + if checker.is_rule_enabled(Rule::UnnecessaryDirectLambdaCall) { pylint::rules::unnecessary_direct_lambda_call(checker, expr, func); } - if checker.enabled(Rule::SysExitAlias) { + if checker.is_rule_enabled(Rule::SysExitAlias) { pylint::rules::sys_exit_alias(checker, call); } - if checker.enabled(Rule::BadOpenMode) { + if checker.is_rule_enabled(Rule::BadOpenMode) { pylint::rules::bad_open_mode(checker, call); } - if checker.enabled(Rule::BadStrStripCall) { + if checker.is_rule_enabled(Rule::BadStrStripCall) { pylint::rules::bad_str_strip_call(checker, call); } - if checker.enabled(Rule::ShallowCopyEnviron) { + if checker.is_rule_enabled(Rule::ShallowCopyEnviron) { pylint::rules::shallow_copy_environ(checker, call); } - if checker.enabled(Rule::InvalidEnvvarDefault) { + if checker.is_rule_enabled(Rule::InvalidEnvvarDefault) { pylint::rules::invalid_envvar_default(checker, call); } - if checker.enabled(Rule::InvalidEnvvarValue) { + if checker.is_rule_enabled(Rule::InvalidEnvvarValue) { pylint::rules::invalid_envvar_value(checker, call); } - if checker.enabled(Rule::NestedMinMax) { + if checker.is_rule_enabled(Rule::NestedMinMax) { pylint::rules::nested_min_max(checker, expr, func, args, keywords); } - if checker.enabled(Rule::RepeatedKeywordArgument) { + if checker.is_rule_enabled(Rule::RepeatedKeywordArgument) { pylint::rules::repeated_keyword_argument(checker, call); } - if checker.enabled(Rule::PytestPatchWithLambda) { - if let Some(diagnostic) = flake8_pytest_style::rules::patch_with_lambda(call) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::PytestPatchWithLambda) { + flake8_pytest_style::rules::patch_with_lambda(checker, call); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::PytestParametrizeNamesWrongType, Rule::PytestParametrizeValuesWrongType, Rule::PytestDuplicateParametrizeTestCases, ]) { flake8_pytest_style::rules::parametrize(checker, call); } - if checker.enabled(Rule::PytestUnittestAssertion) { + if checker.is_rule_enabled(Rule::PytestUnittestAssertion) { flake8_pytest_style::rules::unittest_assertion(checker, expr, func, args, keywords); } - if checker.enabled(Rule::PytestUnittestRaisesAssertion) { + if checker.is_rule_enabled(Rule::PytestUnittestRaisesAssertion) { flake8_pytest_style::rules::unittest_raises_assertion_call(checker, call); } - if checker.enabled(Rule::SubprocessPopenPreexecFn) { + if checker.is_rule_enabled(Rule::SubprocessPopenPreexecFn) { pylint::rules::subprocess_popen_preexec_fn(checker, call); } - if checker.enabled(Rule::SubprocessRunWithoutCheck) { + if checker.is_rule_enabled(Rule::SubprocessRunWithoutCheck) { pylint::rules::subprocess_run_without_check(checker, call); } - if checker.enabled(Rule::UnspecifiedEncoding) { + if checker.is_rule_enabled(Rule::UnspecifiedEncoding) { pylint::rules::unspecified_encoding(checker, call); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::PytestRaisesWithoutException, Rule::PytestRaisesTooBroad, ]) { flake8_pytest_style::rules::raises_call(checker, call); } - if checker.any_enabled(&[Rule::PytestWarnsWithoutWarning, Rule::PytestWarnsTooBroad]) { + if checker.is_rule_enabled(Rule::LegacyFormPytestRaises) { + ruff::rules::legacy_raises_warns_deprecated_call(checker, call); + } + if checker + .any_rule_enabled(&[Rule::PytestWarnsWithoutWarning, Rule::PytestWarnsTooBroad]) + { flake8_pytest_style::rules::warns_call(checker, call); } - if checker.enabled(Rule::PytestFailWithoutMessage) { + if checker.is_rule_enabled(Rule::PytestFailWithoutMessage) { flake8_pytest_style::rules::fail_call(checker, call); } - if checker.enabled(Rule::ZipInsteadOfPairwise) { + if checker.is_rule_enabled(Rule::ZipInsteadOfPairwise) { if checker.target_version() >= PythonVersion::PY310 { ruff::rules::zip_instead_of_pairwise(checker, call); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::FStringInGetTextFuncCall, Rule::FormatInGetTextFuncCall, Rule::PrintfInGetTextFuncCall, ]) && flake8_gettext::is_gettext_func_call( func, - &checker.settings.flake8_gettext.functions_names, + &checker.settings().flake8_gettext.functions_names, ) { - if checker.enabled(Rule::FStringInGetTextFuncCall) { + if checker.is_rule_enabled(Rule::FStringInGetTextFuncCall) { flake8_gettext::rules::f_string_in_gettext_func_call(checker, args); } - if checker.enabled(Rule::FormatInGetTextFuncCall) { + if checker.is_rule_enabled(Rule::FormatInGetTextFuncCall) { flake8_gettext::rules::format_in_gettext_func_call(checker, args); } - if checker.enabled(Rule::PrintfInGetTextFuncCall) { + if checker.is_rule_enabled(Rule::PrintfInGetTextFuncCall) { flake8_gettext::rules::printf_in_gettext_func_call(checker, args); } } - if checker.enabled(Rule::UncapitalizedEnvironmentVariables) { + if checker.is_rule_enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(checker, expr); } - if checker.enabled(Rule::OpenFileWithContextHandler) { + if checker.is_rule_enabled(Rule::OpenFileWithContextHandler) { flake8_simplify::rules::open_file_with_context_handler(checker, call); } - if checker.enabled(Rule::DictGetWithNoneDefault) { + if checker.is_rule_enabled(Rule::DictGetWithNoneDefault) { flake8_simplify::rules::dict_get_with_none_default(checker, expr); } - if checker.enabled(Rule::ZipDictKeysAndValues) { + if checker.is_rule_enabled(Rule::ZipDictKeysAndValues) { flake8_simplify::rules::zip_dict_keys_and_values(checker, call); } - if checker.any_enabled(&[ - Rule::OsPathAbspath, + if checker.any_rule_enabled(&[ Rule::OsChmod, Rule::OsMkdir, Rule::OsMakedirs, Rule::OsRename, Rule::OsReplace, - Rule::OsRmdir, - Rule::OsRemove, - Rule::OsUnlink, Rule::OsGetcwd, - Rule::OsPathExists, - Rule::OsPathExpanduser, - Rule::OsPathIsdir, - Rule::OsPathIsfile, - Rule::OsPathIslink, - Rule::OsReadlink, Rule::OsStat, - Rule::OsPathIsabs, Rule::OsPathJoin, - Rule::OsPathBasename, - Rule::OsPathDirname, Rule::OsPathSamefile, Rule::OsPathSplitext, Rule::BuiltinOpen, Rule::PyPath, - Rule::OsPathGetsize, - Rule::OsPathGetatime, - Rule::OsPathGetmtime, - Rule::OsPathGetctime, Rule::Glob, Rule::OsListdir, + Rule::OsSymlink, ]) { flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call); } - if checker.enabled(Rule::PathConstructorCurrentDirectory) { - flake8_use_pathlib::rules::path_constructor_current_directory(checker, call); + if let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) { + let segments = qualified_name.segments(); + if checker.is_rule_enabled(Rule::OsPathGetsize) { + flake8_use_pathlib::rules::os_path_getsize(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathGetatime) { + flake8_use_pathlib::rules::os_path_getatime(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathGetctime) { + flake8_use_pathlib::rules::os_path_getctime(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathGetmtime) { + flake8_use_pathlib::rules::os_path_getmtime(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathAbspath) { + flake8_use_pathlib::rules::os_path_abspath(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsRmdir) { + flake8_use_pathlib::rules::os_rmdir(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsRemove) { + flake8_use_pathlib::rules::os_remove(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsUnlink) { + flake8_use_pathlib::rules::os_unlink(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathExists) { + flake8_use_pathlib::rules::os_path_exists(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathExpanduser) { + flake8_use_pathlib::rules::os_path_expanduser(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathBasename) { + flake8_use_pathlib::rules::os_path_basename(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathDirname) { + flake8_use_pathlib::rules::os_path_dirname(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathIsabs) { + flake8_use_pathlib::rules::os_path_isabs(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathIsdir) { + flake8_use_pathlib::rules::os_path_isdir(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathIsfile) { + flake8_use_pathlib::rules::os_path_isfile(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsPathIslink) { + flake8_use_pathlib::rules::os_path_islink(checker, call, segments); + } + if checker.is_rule_enabled(Rule::OsReadlink) { + flake8_use_pathlib::rules::os_readlink(checker, call, segments); + } + if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) { + flake8_use_pathlib::rules::path_constructor_current_directory( + checker, call, segments, + ); + } } - if checker.enabled(Rule::OsSepSplit) { + + if checker.is_rule_enabled(Rule::OsSepSplit) { flake8_use_pathlib::rules::os_sep_split(checker, call); } - if checker.enabled(Rule::NumpyLegacyRandom) { + if checker.is_rule_enabled(Rule::NumpyLegacyRandom) { numpy::rules::legacy_random(checker, func); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::LoggingStringFormat, Rule::LoggingPercentFormat, Rule::LoggingStringConcat, @@ -1065,183 +1135,194 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_logging_format::rules::logging_call(checker, call); } - if checker.any_enabled(&[Rule::LoggingTooFewArgs, Rule::LoggingTooManyArgs]) { + if checker.any_rule_enabled(&[Rule::LoggingTooFewArgs, Rule::LoggingTooManyArgs]) { pylint::rules::logging_call(checker, call); } - if checker.enabled(Rule::DjangoLocalsInRenderFunction) { + if checker.is_rule_enabled(Rule::DjangoLocalsInRenderFunction) { flake8_django::rules::locals_in_render_function(checker, call); } - if checker.enabled(Rule::UnsupportedMethodCallOnAll) { + if checker.is_rule_enabled(Rule::UnsupportedMethodCallOnAll) { flake8_pyi::rules::unsupported_method_call_on_all(checker, func); } - if checker.enabled(Rule::PrintEmptyString) { + if checker.is_rule_enabled(Rule::PrintEmptyString) { refurb::rules::print_empty_string(checker, call); } - if checker.enabled(Rule::RedundantLogBase) { + if checker.is_rule_enabled(Rule::RedundantLogBase) { refurb::rules::redundant_log_base(checker, call); } - if checker.enabled(Rule::VerboseDecimalConstructor) { + if checker.is_rule_enabled(Rule::VerboseDecimalConstructor) { refurb::rules::verbose_decimal_constructor(checker, call); } - if checker.enabled(Rule::UnnecessaryFromFloat) { + if checker.is_rule_enabled(Rule::UnnecessaryFromFloat) { refurb::rules::unnecessary_from_float(checker, call); } - if checker.enabled(Rule::QuadraticListSummation) { + if checker.is_rule_enabled(Rule::QuadraticListSummation) { ruff::rules::quadratic_list_summation(checker, call); } - if checker.enabled(Rule::DirectLoggerInstantiation) { + if checker.is_rule_enabled(Rule::DirectLoggerInstantiation) { flake8_logging::rules::direct_logger_instantiation(checker, call); } - if checker.enabled(Rule::InvalidGetLoggerArgument) { + if checker.is_rule_enabled(Rule::InvalidGetLoggerArgument) { flake8_logging::rules::invalid_get_logger_argument(checker, call); } - if checker.enabled(Rule::ExceptionWithoutExcInfo) { + if checker.is_rule_enabled(Rule::ExceptionWithoutExcInfo) { flake8_logging::rules::exception_without_exc_info(checker, call); } - if checker.enabled(Rule::RootLoggerCall) { + if checker.is_rule_enabled(Rule::RootLoggerCall) { flake8_logging::rules::root_logger_call(checker, call); } - if checker.enabled(Rule::IsinstanceTypeNone) { + if checker.is_rule_enabled(Rule::IsinstanceTypeNone) { refurb::rules::isinstance_type_none(checker, call); } - if checker.enabled(Rule::ImplicitCwd) { + if checker.is_rule_enabled(Rule::ImplicitCwd) { refurb::rules::no_implicit_cwd(checker, call); } - if checker.enabled(Rule::TrioSyncCall) { + if checker.is_rule_enabled(Rule::TrioSyncCall) { flake8_async::rules::sync_call(checker, call); } - if checker.enabled(Rule::AsyncZeroSleep) { + if checker.is_rule_enabled(Rule::AsyncZeroSleep) { flake8_async::rules::async_zero_sleep(checker, call); } - if checker.enabled(Rule::UnnecessaryDunderCall) { + if checker.is_rule_enabled(Rule::UnnecessaryDunderCall) { pylint::rules::unnecessary_dunder_call(checker, call); } - if checker.enabled(Rule::SslWithNoVersion) { + if checker.is_rule_enabled(Rule::SslWithNoVersion) { flake8_bandit::rules::ssl_with_no_version(checker, call); } - if checker.enabled(Rule::SslInsecureVersion) { + if checker.is_rule_enabled(Rule::SslInsecureVersion) { flake8_bandit::rules::ssl_insecure_version(checker, call); } - if checker.enabled(Rule::MutableFromkeysValue) { + if checker.is_rule_enabled(Rule::MutableFromkeysValue) { ruff::rules::mutable_fromkeys_value(checker, call); } - if checker.enabled(Rule::UnsortedDunderAll) { + if checker.is_rule_enabled(Rule::UnsortedDunderAll) { ruff::rules::sort_dunder_all_extend_call(checker, call); } - if checker.enabled(Rule::DefaultFactoryKwarg) { + if checker.is_rule_enabled(Rule::DefaultFactoryKwarg) { ruff::rules::default_factory_kwarg(checker, call); } - if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { + if checker.is_rule_enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr); } - if checker.enabled(Rule::DecimalFromFloatLiteral) { + if checker.is_rule_enabled(Rule::DecimalFromFloatLiteral) { ruff::rules::decimal_from_float_literal_syntax(checker, call); } - if checker.enabled(Rule::IntOnSlicedStr) { + if checker.is_rule_enabled(Rule::IntOnSlicedStr) { refurb::rules::int_on_sliced_str(checker, call); } - if checker.enabled(Rule::UnsafeMarkupUse) { + if checker.is_rule_enabled(Rule::UnsafeMarkupUse) { flake8_bandit::rules::unsafe_markup_call(checker, call); } - if checker.enabled(Rule::MapIntVersionParsing) { + if checker.is_rule_enabled(Rule::MapIntVersionParsing) { ruff::rules::map_int_version_parsing(checker, call); } - if checker.enabled(Rule::UnrawRePattern) { + if checker.is_rule_enabled(Rule::UnrawRePattern) { ruff::rules::unraw_re_pattern(checker, call); } - if checker.enabled(Rule::AirflowDagNoScheduleArgument) { + if checker.is_rule_enabled(Rule::AirflowDagNoScheduleArgument) { airflow::rules::dag_no_schedule_argument(checker, expr); } - if checker.enabled(Rule::UnnecessaryRegularExpression) { + if checker.is_rule_enabled(Rule::UnnecessaryRegularExpression) { ruff::rules::unnecessary_regular_expression(checker, call); } - if checker.enabled(Rule::Airflow3Removal) { + if checker.is_rule_enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_expr(checker, expr); } - if checker.enabled(Rule::Airflow3SuggestedUpdate) { + if checker.is_rule_enabled(Rule::Airflow3SuggestedUpdate) { airflow::rules::airflow_3_0_suggested_update_expr(checker, expr); } - if checker.enabled(Rule::UnnecessaryCastToInt) { + if checker.is_rule_enabled(Rule::UnnecessaryCastToInt) { ruff::rules::unnecessary_cast_to_int(checker, call); } - if checker.enabled(Rule::InvalidPathlibWithSuffix) { + if checker.is_rule_enabled(Rule::InvalidPathlibWithSuffix) { flake8_use_pathlib::rules::invalid_pathlib_with_suffix(checker, call); } - if checker.enabled(Rule::BatchedWithoutExplicitStrict) { + if checker.is_rule_enabled(Rule::BatchedWithoutExplicitStrict) { flake8_bugbear::rules::batched_without_explicit_strict(checker, call); } - if checker.enabled(Rule::PytestRaisesAmbiguousPattern) { + if checker.is_rule_enabled(Rule::PytestRaisesAmbiguousPattern) { ruff::rules::pytest_raises_ambiguous_pattern(checker, call); } - if checker.enabled(Rule::FalsyDictGetFallback) { + if checker.is_rule_enabled(Rule::FalsyDictGetFallback) { ruff::rules::falsy_dict_get_fallback(checker, expr); } - if checker.enabled(Rule::UnnecessaryRound) { + if checker.is_rule_enabled(Rule::UnnecessaryRound) { ruff::rules::unnecessary_round(checker, call); } - if checker.enabled(Rule::UnnecessaryEmptyIterableWithinDequeCall) { + if checker.is_rule_enabled(Rule::UnnecessaryEmptyIterableWithinDequeCall) { ruff::rules::unnecessary_literal_within_deque_call(checker, call); } - if checker.enabled(Rule::StarmapZip) { + if checker.is_rule_enabled(Rule::StarmapZip) { ruff::rules::starmap_zip(checker, call); } - if checker.enabled(Rule::LogExceptionOutsideExceptHandler) { + if checker.is_rule_enabled(Rule::AccessAnnotationsFromClassDict) { + ruff::rules::access_annotations_from_class_dict_with_get(checker, call); + } + if checker.is_rule_enabled(Rule::LogExceptionOutsideExceptHandler) { flake8_logging::rules::log_exception_outside_except_handler(checker, call); } - if checker.enabled(Rule::ExcInfoOutsideExceptHandler) { + if checker.is_rule_enabled(Rule::ExcInfoOutsideExceptHandler) { flake8_logging::rules::exc_info_outside_except_handler(checker, call); } - if checker.enabled(Rule::FromisoformatReplaceZ) { + if checker.is_rule_enabled(Rule::FromisoformatReplaceZ) { refurb::rules::fromisoformat_replace_z(checker, call); } + if checker.is_rule_enabled(Rule::NonOctalPermissions) { + ruff::rules::non_octal_permissions(checker, call); + } + if checker.is_rule_enabled(Rule::AssertRaisesException) + && is_assert_raises_exception_call_enabled(checker.settings()) + { + flake8_bugbear::rules::assert_raises_exception_call(checker, call); + } } Expr::Dict(dict) => { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::MultiValueRepeatedKeyLiteral, Rule::MultiValueRepeatedKeyVariable, ]) { pyflakes::rules::repeated_keys(checker, dict); } - if checker.enabled(Rule::UnnecessarySpread) { + if checker.is_rule_enabled(Rule::UnnecessarySpread) { flake8_pie::rules::unnecessary_spread(checker, dict); } } Expr::Set(set) => { - if checker.enabled(Rule::DuplicateValue) { + if checker.is_rule_enabled(Rule::DuplicateValue) { flake8_bugbear::rules::duplicate_value(checker, set); } } Expr::Yield(_) => { - if checker.enabled(Rule::YieldInInit) { + if checker.is_rule_enabled(Rule::YieldInInit) { pylint::rules::yield_in_init(checker, expr); } } Expr::YieldFrom(yield_from) => { - if checker.enabled(Rule::YieldInInit) { + if checker.is_rule_enabled(Rule::YieldInInit) { pylint::rules::yield_in_init(checker, expr); } - if checker.enabled(Rule::YieldFromInAsyncFunction) { + if checker.is_rule_enabled(Rule::YieldFromInAsyncFunction) { pylint::rules::yield_from_in_async_function(checker, yield_from); } } Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => { - if checker.enabled(Rule::FStringMissingPlaceholders) { + if checker.is_rule_enabled(Rule::FStringMissingPlaceholders) { pyflakes::rules::f_string_missing_placeholders(checker, f_string_expr); } - if checker.enabled(Rule::ExplicitFStringTypeConversion) { + if checker.is_rule_enabled(Rule::ExplicitFStringTypeConversion) { for f_string in value.f_strings() { ruff::rules::explicit_f_string_type_conversion(checker, f_string); } } - if checker.enabled(Rule::HardcodedSQLExpression) { + if checker.is_rule_enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(checker, expr); } - if checker.enabled(Rule::UnicodeKindPrefix) { + if checker.is_rule_enabled(Rule::UnicodeKindPrefix) { for string_literal in value.literals() { pyupgrade::rules::unicode_kind_prefix(checker, string_literal); } } - if checker.enabled(Rule::MissingFStringSyntax) { + if checker.is_rule_enabled(Rule::MissingFStringSyntax) { for string_literal in value.literals() { ruff::rules::missing_fstring_syntax(checker, string_literal); } @@ -1252,7 +1333,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { op: Operator::RShift, .. }) => { - if checker.enabled(Rule::InvalidPrintSyntax) { + if checker.is_rule_enabled(Rule::InvalidPrintSyntax) { pyflakes::rules::invalid_print_syntax(checker, left); } } @@ -1262,12 +1343,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { op: Operator::Mod, right, range: _, + node_index: _, }, ) => { if let Expr::StringLiteral(format_string @ ast::ExprStringLiteral { value, .. }) = left.as_ref() { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::PercentFormatInvalidFormat, Rule::PercentFormatExpectedMapping, Rule::PercentFormatExpectedSequence, @@ -1284,57 +1366,55 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { typ: CFormatErrorType::UnsupportedFormatChar(c), .. }) => { - if checker.enabled(Rule::PercentFormatUnsupportedFormatCharacter) { - checker.report_diagnostic(Diagnostic::new( - pyflakes::rules::PercentFormatUnsupportedFormatCharacter { - char: c, - }, - location, - )); - } + // F509 + checker.report_diagnostic_if_enabled( + pyflakes::rules::PercentFormatUnsupportedFormatCharacter { + char: c, + }, + location, + ); } Err(e) => { - if checker.enabled(Rule::PercentFormatInvalidFormat) { - checker.report_diagnostic(Diagnostic::new( - pyflakes::rules::PercentFormatInvalidFormat { - message: e.to_string(), - }, - location, - )); - } + // F501 + checker.report_diagnostic_if_enabled( + pyflakes::rules::PercentFormatInvalidFormat { + message: e.to_string(), + }, + location, + ); } Ok(summary) => { - if checker.enabled(Rule::PercentFormatExpectedMapping) { + if checker.is_rule_enabled(Rule::PercentFormatExpectedMapping) { pyflakes::rules::percent_format_expected_mapping( checker, &summary, right, location, ); } - if checker.enabled(Rule::PercentFormatExpectedSequence) { + if checker.is_rule_enabled(Rule::PercentFormatExpectedSequence) { pyflakes::rules::percent_format_expected_sequence( checker, &summary, right, location, ); } - if checker.enabled(Rule::PercentFormatExtraNamedArguments) { + if checker.is_rule_enabled(Rule::PercentFormatExtraNamedArguments) { pyflakes::rules::percent_format_extra_named_arguments( checker, &summary, right, location, ); } - if checker.enabled(Rule::PercentFormatMissingArgument) { + if checker.is_rule_enabled(Rule::PercentFormatMissingArgument) { pyflakes::rules::percent_format_missing_arguments( checker, &summary, right, location, ); } - if checker.enabled(Rule::PercentFormatMixedPositionalAndNamed) { + if checker.is_rule_enabled(Rule::PercentFormatMixedPositionalAndNamed) { pyflakes::rules::percent_format_mixed_positional_and_named( checker, &summary, location, ); } - if checker.enabled(Rule::PercentFormatPositionalCountMismatch) { + if checker.is_rule_enabled(Rule::PercentFormatPositionalCountMismatch) { pyflakes::rules::percent_format_positional_count_mismatch( checker, &summary, right, location, ); } - if checker.enabled(Rule::PercentFormatStarRequiresSequence) { + if checker.is_rule_enabled(Rule::PercentFormatStarRequiresSequence) { pyflakes::rules::percent_format_star_requires_sequence( checker, &summary, right, location, ); @@ -1342,20 +1422,20 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } } - if checker.enabled(Rule::PrintfStringFormatting) { + if checker.is_rule_enabled(Rule::PrintfStringFormatting) { pyupgrade::rules::printf_string_formatting(checker, bin_op, format_string); } - if checker.enabled(Rule::BadStringFormatCharacter) { + if checker.is_rule_enabled(Rule::BadStringFormatCharacter) { pylint::rules::bad_string_format_character::percent( checker, expr, format_string, ); } - if checker.enabled(Rule::BadStringFormatType) { + if checker.is_rule_enabled(Rule::BadStringFormatType) { pylint::rules::bad_string_format_type(checker, bin_op, format_string); } - if checker.enabled(Rule::HardcodedSQLExpression) { + if checker.is_rule_enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(checker, expr); } } @@ -1363,19 +1443,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { Expr::BinOp(ast::ExprBinOp { op: Operator::Add, .. }) => { - if checker.enabled(Rule::ExplicitStringConcatenation) { - if let Some(diagnostic) = flake8_implicit_str_concat::rules::explicit( - expr, - checker.locator, - checker.settings, - ) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::ExplicitStringConcatenation) { + flake8_implicit_str_concat::rules::explicit(checker, expr); } - if checker.enabled(Rule::CollectionLiteralConcatenation) { + if checker.is_rule_enabled(Rule::CollectionLiteralConcatenation) { ruff::rules::collection_literal_concatenation(checker, expr); } - if checker.enabled(Rule::HardcodedSQLExpression) { + if checker.is_rule_enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(checker, expr); } } @@ -1384,7 +1458,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { .. }) => { // Ex) `str | None` - if checker.enabled(Rule::FutureRequiredTypeAnnotation) { + if checker.is_rule_enabled(Rule::FutureRequiredTypeAnnotation) { if !checker.semantic.future_annotations_or_stub() && checker.target_version() < PythonVersion::PY310 && checker.semantic.in_annotation() @@ -1399,31 +1473,36 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } } - if checker.enabled(Rule::NeverUnion) { + if checker.is_rule_enabled(Rule::NeverUnion) { ruff::rules::never_union(checker, expr); } // Avoid duplicate checks if the parent is a union, since these rules already // traverse nested unions. if !checker.semantic.in_nested_union() { - if checker.enabled(Rule::DuplicateUnionMember) + if checker.is_rule_enabled(Rule::DuplicateUnionMember) && checker.semantic.in_type_definition() + // Avoid duplicate checks inside `Optional` + && !( + is_optional_as_none_in_union_enabled(checker.settings()) + && checker.semantic.inside_optional() + ) { flake8_pyi::rules::duplicate_union_member(checker, expr); } - if checker.enabled(Rule::UnnecessaryLiteralUnion) { + if checker.is_rule_enabled(Rule::UnnecessaryLiteralUnion) { flake8_pyi::rules::unnecessary_literal_union(checker, expr); } - if checker.enabled(Rule::RedundantLiteralUnion) { + if checker.is_rule_enabled(Rule::RedundantLiteralUnion) { flake8_pyi::rules::redundant_literal_union(checker, expr); } - if checker.enabled(Rule::UnnecessaryTypeUnion) { + if checker.is_rule_enabled(Rule::UnnecessaryTypeUnion) { flake8_pyi::rules::unnecessary_type_union(checker, expr); } - if checker.enabled(Rule::RuntimeStringUnion) { + if checker.is_rule_enabled(Rule::RuntimeStringUnion) { flake8_type_checking::rules::runtime_string_union(checker, expr); } - if checker.enabled(Rule::NoneNotAtEndOfUnion) { + if checker.is_rule_enabled(Rule::NoneNotAtEndOfUnion) { ruff::rules::none_not_at_end_of_union(checker, expr); } } @@ -1433,23 +1512,24 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { op, operand, range: _, + node_index: _, }, ) => { - if checker.any_enabled(&[Rule::NotInTest, Rule::NotIsTest]) { + if checker.any_rule_enabled(&[Rule::NotInTest, Rule::NotIsTest]) { pycodestyle::rules::not_tests(checker, unary_op); } - if checker.enabled(Rule::UnaryPrefixIncrementDecrement) { + if checker.is_rule_enabled(Rule::UnaryPrefixIncrementDecrement) { flake8_bugbear::rules::unary_prefix_increment_decrement( checker, expr, *op, operand, ); } - if checker.enabled(Rule::NegateEqualOp) { + if checker.is_rule_enabled(Rule::NegateEqualOp) { flake8_simplify::rules::negation_with_equal_op(checker, expr, *op, operand); } - if checker.enabled(Rule::NegateNotEqualOp) { + if checker.is_rule_enabled(Rule::NegateNotEqualOp) { flake8_simplify::rules::negation_with_not_equal_op(checker, expr, *op, operand); } - if checker.enabled(Rule::DoubleNegation) { + if checker.is_rule_enabled(Rule::DoubleNegation) { flake8_simplify::rules::double_negation(checker, expr, *op, operand); } } @@ -1459,18 +1539,19 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ops, comparators, range: _, + node_index: _, }, ) => { - if checker.any_enabled(&[Rule::NoneComparison, Rule::TrueFalseComparison]) { + if checker.any_rule_enabled(&[Rule::NoneComparison, Rule::TrueFalseComparison]) { pycodestyle::rules::literal_comparisons(checker, compare); } - if checker.enabled(Rule::IsLiteral) { + if checker.is_rule_enabled(Rule::IsLiteral) { pyflakes::rules::invalid_literal_comparison(checker, left, ops, comparators, expr); } - if checker.enabled(Rule::TypeComparison) { + if checker.is_rule_enabled(Rule::TypeComparison) { pycodestyle::rules::type_comparison(checker, compare); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SysVersionCmpStr3, Rule::SysVersionInfo0Eq3, Rule::SysVersionInfo1CmpInt, @@ -1479,38 +1560,41 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_2020::rules::compare(checker, left, ops, comparators); } - if checker.enabled(Rule::HardcodedPasswordString) { + if checker.is_rule_enabled(Rule::HardcodedPasswordString) { flake8_bandit::rules::compare_to_hardcoded_password_string( checker, left, comparators, ); } - if checker.enabled(Rule::ComparisonWithItself) { + if checker.is_rule_enabled(Rule::ComparisonWithItself) { pylint::rules::comparison_with_itself(checker, left, ops, comparators); } - if checker.enabled(Rule::LiteralMembership) { + if checker.is_rule_enabled(Rule::LiteralMembership) { pylint::rules::literal_membership(checker, compare); } - if checker.enabled(Rule::ComparisonOfConstant) { + if checker.is_rule_enabled(Rule::ComparisonOfConstant) { pylint::rules::comparison_of_constant(checker, left, ops, comparators); } - if checker.enabled(Rule::CompareToEmptyString) { + if checker.is_rule_enabled(Rule::CompareToEmptyString) { pylint::rules::compare_to_empty_string(checker, left, ops, comparators); } - if checker.enabled(Rule::MagicValueComparison) { + if checker.is_rule_enabled(Rule::MagicValueComparison) { pylint::rules::magic_value_comparison(checker, left, comparators); } - if checker.enabled(Rule::NanComparison) { + if checker.is_rule_enabled(Rule::NanComparison) { pylint::rules::nan_comparison(checker, left, comparators); } - if checker.enabled(Rule::InDictKeys) { + if checker.is_rule_enabled(Rule::InEmptyCollection) { + ruff::rules::in_empty_collection(checker, compare); + } + if checker.is_rule_enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_compare(checker, compare); } - if checker.enabled(Rule::YodaConditions) { + if checker.is_rule_enabled(Rule::YodaConditions) { flake8_simplify::rules::yoda_conditions(checker, expr, left, ops, comparators); } - if checker.enabled(Rule::PandasNuniqueConstantSeriesCheck) { + if checker.is_rule_enabled(Rule::PandasNuniqueConstantSeriesCheck) { pandas_vet::rules::nunique_constant_series_check( checker, expr, @@ -1519,33 +1603,40 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { comparators, ); } - if checker.enabled(Rule::TypeNoneComparison) { + if checker.is_rule_enabled(Rule::TypeNoneComparison) { refurb::rules::type_none_comparison(checker, compare); } - if checker.enabled(Rule::SingleItemMembershipTest) { + if checker.is_rule_enabled(Rule::SingleItemMembershipTest) { refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators); } } Expr::NumberLiteral(number_literal @ ast::ExprNumberLiteral { .. }) => { - if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) { + if checker.source_type.is_stub() && checker.is_rule_enabled(Rule::NumericLiteralTooLong) + { flake8_pyi::rules::numeric_literal_too_long(checker, expr); } - if checker.enabled(Rule::MathConstant) { + if checker.is_rule_enabled(Rule::MathConstant) { refurb::rules::math_constant(checker, number_literal); } } - Expr::StringLiteral(string_like @ ast::ExprStringLiteral { value, range: _ }) => { - if checker.enabled(Rule::UnicodeKindPrefix) { + Expr::StringLiteral( + string_like @ ast::ExprStringLiteral { + value, + range: _, + node_index: _, + }, + ) => { + if checker.is_rule_enabled(Rule::UnicodeKindPrefix) { for string_part in value { pyupgrade::rules::unicode_kind_prefix(checker, string_part); } } - if checker.enabled(Rule::MissingFStringSyntax) { + if checker.is_rule_enabled(Rule::MissingFStringSyntax) { for string_literal in value { ruff::rules::missing_fstring_syntax(checker, string_literal); } } - if checker.enabled(Rule::HardcodedStringCharset) { + if checker.is_rule_enabled(Rule::HardcodedStringCharset) { refurb::rules::hardcoded_string_charset_literal(checker, string_like); } } @@ -1555,32 +1646,33 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { body, orelse, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) { + if checker.is_rule_enabled(Rule::IfElseBlockInsteadOfDictGet) { flake8_simplify::rules::if_exp_instead_of_dict_get( checker, expr, test, body, orelse, ); } - if checker.enabled(Rule::IfExprWithTrueFalse) { + if checker.is_rule_enabled(Rule::IfExprWithTrueFalse) { flake8_simplify::rules::if_expr_with_true_false(checker, expr, test, body, orelse); } - if checker.enabled(Rule::IfExprWithFalseTrue) { + if checker.is_rule_enabled(Rule::IfExprWithFalseTrue) { flake8_simplify::rules::if_expr_with_false_true(checker, expr, test, body, orelse); } - if checker.enabled(Rule::IfExprWithTwistedArms) { + if checker.is_rule_enabled(Rule::IfExprWithTwistedArms) { flake8_simplify::rules::twisted_arms_in_ifexpr(checker, expr, test, body, orelse); } - if checker.enabled(Rule::IfExprMinMax) { + if checker.is_rule_enabled(Rule::IfExprMinMax) { refurb::rules::if_expr_min_max(checker, if_exp); } - if checker.enabled(Rule::IfExpInsteadOfOrOperator) { + if checker.is_rule_enabled(Rule::IfExpInsteadOfOrOperator) { refurb::rules::if_exp_instead_of_or_operator(checker, if_exp); } - if checker.enabled(Rule::UselessIfElse) { + if checker.is_rule_enabled(Rule::UselessIfElse) { ruff::rules::useless_if_else(checker, if_exp); } - if checker.enabled(Rule::SliceToRemovePrefixOrSuffix) { + if checker.is_rule_enabled(Rule::SliceToRemovePrefixOrSuffix) { refurb::rules::slice_to_remove_affix_expr(checker, if_exp); } } @@ -1589,28 +1681,29 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { elt, generators, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::UnnecessaryListIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryListIndexLookup) { pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryDictIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryDictIndexLookup) { pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryComprehension) { + if checker.is_rule_enabled(Rule::UnnecessaryComprehension) { flake8_comprehensions::rules::unnecessary_list_set_comprehension( checker, expr, elt, generators, ); } - if checker.enabled(Rule::FunctionUsesLoopVariable) { + if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) { flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr)); } - if checker.enabled(Rule::IterationOverSet) { + if checker.is_rule_enabled(Rule::IterationOverSet) { for generator in generators { pylint::rules::iteration_over_set(checker, &generator.iter); } } - if checker.enabled(Rule::ReimplementedStarmap) { + if checker.is_rule_enabled(Rule::ReimplementedStarmap) { refurb::rules::reimplemented_starmap(checker, &comp.into()); } } @@ -1619,28 +1712,29 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { elt, generators, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::UnnecessaryListIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryListIndexLookup) { pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryDictIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryDictIndexLookup) { pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryComprehension) { + if checker.is_rule_enabled(Rule::UnnecessaryComprehension) { flake8_comprehensions::rules::unnecessary_list_set_comprehension( checker, expr, elt, generators, ); } - if checker.enabled(Rule::FunctionUsesLoopVariable) { + if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) { flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr)); } - if checker.enabled(Rule::IterationOverSet) { + if checker.is_rule_enabled(Rule::IterationOverSet) { for generator in generators { pylint::rules::iteration_over_set(checker, &generator.iter); } } - if checker.enabled(Rule::ReimplementedStarmap) { + if checker.is_rule_enabled(Rule::ReimplementedStarmap) { refurb::rules::reimplemented_starmap(checker, &comp.into()); } } @@ -1650,36 +1744,37 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { value, generators, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::UnnecessaryListIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryListIndexLookup) { pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryDictIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryDictIndexLookup) { pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryComprehension) { + if checker.is_rule_enabled(Rule::UnnecessaryComprehension) { flake8_comprehensions::rules::unnecessary_dict_comprehension( checker, expr, key, value, generators, ); } - if checker.enabled(Rule::UnnecessaryDictComprehensionForIterable) { + if checker.is_rule_enabled(Rule::UnnecessaryDictComprehensionForIterable) { flake8_comprehensions::rules::unnecessary_dict_comprehension_for_iterable( checker, dict_comp, ); } - if checker.enabled(Rule::FunctionUsesLoopVariable) { + if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) { flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr)); } - if checker.enabled(Rule::IterationOverSet) { + if checker.is_rule_enabled(Rule::IterationOverSet) { for generator in generators { pylint::rules::iteration_over_set(checker, &generator.iter); } } - if checker.enabled(Rule::StaticKeyDictComprehension) { + if checker.is_rule_enabled(Rule::StaticKeyDictComprehension) { flake8_bugbear::rules::static_key_dict_comprehension(checker, dict_comp); } } @@ -1688,67 +1783,68 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { generators, elt: _, range: _, + node_index: _, parenthesized: _, }, ) => { - if checker.enabled(Rule::UnnecessaryListIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryListIndexLookup) { pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::UnnecessaryDictIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryDictIndexLookup) { pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr); } - if checker.enabled(Rule::FunctionUsesLoopVariable) { + if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) { flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr)); } - if checker.enabled(Rule::IterationOverSet) { + if checker.is_rule_enabled(Rule::IterationOverSet) { for generator in generators { pylint::rules::iteration_over_set(checker, &generator.iter); } } - if checker.enabled(Rule::ReimplementedStarmap) { + if checker.is_rule_enabled(Rule::ReimplementedStarmap) { refurb::rules::reimplemented_starmap(checker, &generator.into()); } } Expr::BoolOp(bool_op) => { - if checker.enabled(Rule::BooleanChainedComparison) { + if checker.is_rule_enabled(Rule::BooleanChainedComparison) { pylint::rules::boolean_chained_comparison(checker, bool_op); } - if checker.enabled(Rule::MultipleStartsEndsWith) { + if checker.is_rule_enabled(Rule::MultipleStartsEndsWith) { flake8_pie::rules::multiple_starts_ends_with(checker, expr); } - if checker.enabled(Rule::DuplicateIsinstanceCall) { + if checker.is_rule_enabled(Rule::DuplicateIsinstanceCall) { flake8_simplify::rules::duplicate_isinstance_call(checker, expr); } - if checker.enabled(Rule::CompareWithTuple) { + if checker.is_rule_enabled(Rule::CompareWithTuple) { flake8_simplify::rules::compare_with_tuple(checker, expr); } - if checker.enabled(Rule::ExprAndNotExpr) { + if checker.is_rule_enabled(Rule::ExprAndNotExpr) { flake8_simplify::rules::expr_and_not_expr(checker, expr); } - if checker.enabled(Rule::ExprOrNotExpr) { + if checker.is_rule_enabled(Rule::ExprOrNotExpr) { flake8_simplify::rules::expr_or_not_expr(checker, expr); } - if checker.enabled(Rule::ExprOrTrue) { + if checker.is_rule_enabled(Rule::ExprOrTrue) { flake8_simplify::rules::expr_or_true(checker, expr); } - if checker.enabled(Rule::ExprAndFalse) { + if checker.is_rule_enabled(Rule::ExprAndFalse) { flake8_simplify::rules::expr_and_false(checker, expr); } - if checker.enabled(Rule::RepeatedEqualityComparison) { + if checker.is_rule_enabled(Rule::RepeatedEqualityComparison) { pylint::rules::repeated_equality_comparison(checker, bool_op); } - if checker.enabled(Rule::UnnecessaryKeyCheck) { + if checker.is_rule_enabled(Rule::UnnecessaryKeyCheck) { ruff::rules::unnecessary_key_check(checker, expr); } - if checker.enabled(Rule::ParenthesizeChainedOperators) { + if checker.is_rule_enabled(Rule::ParenthesizeChainedOperators) { ruff::rules::parenthesize_chained_logical_operators(checker, bool_op); } } Expr::Lambda(lambda) => { - if checker.enabled(Rule::ReimplementedOperator) { + if checker.is_rule_enabled(Rule::ReimplementedOperator) { refurb::rules::reimplemented_operator(checker, &lambda.into()); } - if checker.enabled(Rule::InvalidArgumentName) { + if checker.is_rule_enabled(Rule::InvalidArgumentName) { pep8_naming::rules::invalid_argument_name_lambda(checker, lambda); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/module.rs b/crates/ruff_linter/src/checkers/ast/analyze/module.rs index cb897ebf266f7..e4d0a346aa3ba 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/module.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/module.rs @@ -6,10 +6,10 @@ use crate::rules::{flake8_bugbear, ruff}; /// Run lint rules over a module. pub(crate) fn module(suite: &Suite, checker: &Checker) { - if checker.enabled(Rule::FStringDocstring) { + if checker.is_rule_enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(checker, suite); } - if checker.enabled(Rule::InvalidFormatterSuppressionComment) { + if checker.is_rule_enabled(Rule::InvalidFormatterSuppressionComment) { ruff::rules::ignored_formatter_suppression_comment(checker, suite); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/parameter.rs b/crates/ruff_linter/src/checkers/ast/analyze/parameter.rs index 253dc603517c9..831f164e8a5d0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/parameter.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/parameter.rs @@ -7,14 +7,14 @@ use crate::rules::{flake8_builtins, pycodestyle}; /// Run lint rules over a [`Parameter`] syntax node. pub(crate) fn parameter(parameter: &Parameter, checker: &Checker) { - if checker.enabled(Rule::AmbiguousVariableName) { + if checker.is_rule_enabled(Rule::AmbiguousVariableName) { pycodestyle::rules::ambiguous_variable_name( checker, ¶meter.name, parameter.name.range(), ); } - if checker.enabled(Rule::BuiltinArgumentShadowing) { + if checker.is_rule_enabled(Rule::BuiltinArgumentShadowing) { flake8_builtins::rules::builtin_argument_shadowing(checker, parameter); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/parameters.rs b/crates/ruff_linter/src/checkers/ast/analyze/parameters.rs index a9d2c949b7962..cc7a2479b225b 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/parameters.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/parameters.rs @@ -6,17 +6,17 @@ use crate::rules::{flake8_bugbear, flake8_pyi, ruff}; /// Run lint rules over a [`Parameters`] syntax node. pub(crate) fn parameters(parameters: &Parameters, checker: &Checker) { - if checker.enabled(Rule::FunctionCallInDefaultArgument) { + if checker.is_rule_enabled(Rule::FunctionCallInDefaultArgument) { flake8_bugbear::rules::function_call_in_argument_default(checker, parameters); } - if checker.settings.rules.enabled(Rule::ImplicitOptional) { + if checker.is_rule_enabled(Rule::ImplicitOptional) { ruff::rules::implicit_optional(checker, parameters); } if checker.source_type.is_stub() { - if checker.enabled(Rule::TypedArgumentDefaultInStub) { + if checker.is_rule_enabled(Rule::TypedArgumentDefaultInStub) { flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters); } - if checker.enabled(Rule::ArgumentDefaultInStub) { + if checker.is_rule_enabled(Rule::ArgumentDefaultInStub) { flake8_pyi::rules::argument_simple_defaults(checker, parameters); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e503e7df1abf9..0cfb3a3cc7bf8 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1,4 +1,3 @@ -use ruff_diagnostics::Diagnostic; use ruff_python_ast::helpers; use ruff_python_ast::types::Node; use ruff_python_ast::{self as ast, Expr, Stmt}; @@ -19,58 +18,55 @@ use ruff_python_ast::PythonVersion; /// Run lint rules over a [`Stmt`] syntax node. pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { match stmt { - Stmt::Global(ast::StmtGlobal { names, range: _ }) => { - if checker.enabled(Rule::GlobalAtModuleLevel) { + Stmt::Global(ast::StmtGlobal { + names, + range: _, + node_index: _, + }) => { + if checker.is_rule_enabled(Rule::GlobalAtModuleLevel) { pylint::rules::global_at_module_level(checker, stmt); } - if checker.enabled(Rule::AmbiguousVariableName) { + if checker.is_rule_enabled(Rule::AmbiguousVariableName) { for name in names { pycodestyle::rules::ambiguous_variable_name(checker, name, name.range()); } } } - Stmt::Nonlocal(nonlocal @ ast::StmtNonlocal { names, range: _ }) => { - if checker.enabled(Rule::AmbiguousVariableName) { + Stmt::Nonlocal( + nonlocal @ ast::StmtNonlocal { + names, + range: _, + node_index: _, + }, + ) => { + if checker.is_rule_enabled(Rule::AmbiguousVariableName) { for name in names { pycodestyle::rules::ambiguous_variable_name(checker, name, name.range()); } } - if checker.enabled(Rule::NonlocalWithoutBinding) { - if !checker.semantic.scope_id.is_global() { - for name in names { - if checker.semantic.nonlocal(name).is_none() { - checker.report_diagnostic(Diagnostic::new( - pylint::rules::NonlocalWithoutBinding { - name: name.to_string(), - }, - name.range(), - )); - } - } - } + if checker.is_rule_enabled(Rule::NonlocalWithoutBinding) { + pylint::rules::nonlocal_without_binding(checker, nonlocal); } - if checker.enabled(Rule::NonlocalAndGlobal) { + if checker.is_rule_enabled(Rule::NonlocalAndGlobal) { pylint::rules::nonlocal_and_global(checker, nonlocal); } } Stmt::Break(_) => { - if checker.enabled(Rule::BreakOutsideLoop) { - if let Some(diagnostic) = pyflakes::rules::break_outside_loop( + if checker.is_rule_enabled(Rule::BreakOutsideLoop) { + pyflakes::rules::break_outside_loop( + checker, stmt, &mut checker.semantic.current_statements().skip(1), - ) { - checker.report_diagnostic(diagnostic); - } + ); } } Stmt::Continue(_) => { - if checker.enabled(Rule::ContinueOutsideLoop) { - if let Some(diagnostic) = pyflakes::rules::continue_outside_loop( + if checker.is_rule_enabled(Rule::ContinueOutsideLoop) { + pyflakes::rules::continue_outside_loop( + checker, stmt, &mut checker.semantic.current_statements().skip(1), - ) { - checker.report_diagnostic(diagnostic); - } + ); } } Stmt::FunctionDef( @@ -83,69 +79,67 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { body, type_params: _, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) { + if checker.is_rule_enabled(Rule::DjangoNonLeadingReceiverDecorator) { flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list); } - if checker.enabled(Rule::FastApiRedundantResponseModel) { + if checker.is_rule_enabled(Rule::FastApiRedundantResponseModel) { fastapi::rules::fastapi_redundant_response_model(checker, function_def); } - if checker.enabled(Rule::FastApiNonAnnotatedDependency) { + if checker.is_rule_enabled(Rule::FastApiNonAnnotatedDependency) { fastapi::rules::fastapi_non_annotated_dependency(checker, function_def); } - if checker.enabled(Rule::FastApiUnusedPathParameter) { + if checker.is_rule_enabled(Rule::FastApiUnusedPathParameter) { fastapi::rules::fastapi_unused_path_parameter(checker, function_def); } - if checker.enabled(Rule::AmbiguousFunctionName) { - if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::AmbiguousFunctionName) { + pycodestyle::rules::ambiguous_function_name(checker, name); } - if checker.enabled(Rule::InvalidBoolReturnType) { + if checker.is_rule_enabled(Rule::InvalidBoolReturnType) { pylint::rules::invalid_bool_return(checker, function_def); } - if checker.enabled(Rule::InvalidLengthReturnType) { + if checker.is_rule_enabled(Rule::InvalidLengthReturnType) { pylint::rules::invalid_length_return(checker, function_def); } - if checker.enabled(Rule::InvalidBytesReturnType) { + if checker.is_rule_enabled(Rule::InvalidBytesReturnType) { pylint::rules::invalid_bytes_return(checker, function_def); } - if checker.enabled(Rule::InvalidIndexReturnType) { + if checker.is_rule_enabled(Rule::InvalidIndexReturnType) { pylint::rules::invalid_index_return(checker, function_def); } - if checker.enabled(Rule::InvalidHashReturnType) { + if checker.is_rule_enabled(Rule::InvalidHashReturnType) { pylint::rules::invalid_hash_return(checker, function_def); } - if checker.enabled(Rule::InvalidStrReturnType) { + if checker.is_rule_enabled(Rule::InvalidStrReturnType) { pylint::rules::invalid_str_return(checker, function_def); } - if checker.enabled(Rule::InvalidFunctionName) { - if let Some(diagnostic) = pep8_naming::rules::invalid_function_name( + if checker.is_rule_enabled(Rule::InvalidFunctionName) { + pep8_naming::rules::invalid_function_name( + checker, stmt, name, decorator_list, - &checker.settings.pep8_naming.ignore_names, + &checker.settings().pep8_naming.ignore_names, &checker.semantic, - ) { - checker.report_diagnostic(diagnostic); - } + ); } if checker.source_type.is_stub() { - if checker.enabled(Rule::PassStatementStubBody) { + if checker.is_rule_enabled(Rule::PassStatementStubBody) { flake8_pyi::rules::pass_statement_stub_body(checker, body); } - if checker.enabled(Rule::NonEmptyStubBody) { + if checker.is_rule_enabled(Rule::NonEmptyStubBody) { flake8_pyi::rules::non_empty_stub_body(checker, body); } - if checker.enabled(Rule::StubBodyMultipleStatements) { + if checker.is_rule_enabled(Rule::StubBodyMultipleStatements) { flake8_pyi::rules::stub_body_multiple_statements(checker, stmt, body); } } - if checker.enabled(Rule::AnyEqNeAnnotation) { + if checker.is_rule_enabled(Rule::AnyEqNeAnnotation) { flake8_pyi::rules::any_eq_ne_annotation(checker, name, parameters); } - if checker.enabled(Rule::NonSelfReturnType) { + if checker.is_rule_enabled(Rule::NonSelfReturnType) { flake8_pyi::rules::non_self_return_type( checker, stmt, @@ -156,65 +150,63 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { parameters, ); } - if checker.enabled(Rule::GeneratorReturnFromIterMethod) { + if checker.is_rule_enabled(Rule::GeneratorReturnFromIterMethod) { flake8_pyi::rules::bad_generator_return_type(function_def, checker); } if checker.source_type.is_stub() { - if checker.enabled(Rule::StrOrReprDefinedInStub) { + if checker.is_rule_enabled(Rule::StrOrReprDefinedInStub) { flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt); } } if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY311 { - if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) { + if checker.is_rule_enabled(Rule::NoReturnArgumentAnnotationInStub) { flake8_pyi::rules::no_return_argument_annotation(checker, parameters); } } - if checker.enabled(Rule::BadExitAnnotation) { + if checker.is_rule_enabled(Rule::BadExitAnnotation) { flake8_pyi::rules::bad_exit_annotation(checker, function_def); } - if checker.enabled(Rule::RedundantNumericUnion) { + if checker.is_rule_enabled(Rule::RedundantNumericUnion) { flake8_pyi::rules::redundant_numeric_union(checker, parameters); } - if checker.enabled(Rule::Pep484StylePositionalOnlyParameter) { + if checker.is_rule_enabled(Rule::Pep484StylePositionalOnlyParameter) { flake8_pyi::rules::pep_484_positional_parameter(checker, function_def); } - if checker.enabled(Rule::DunderFunctionName) { - if let Some(diagnostic) = pep8_naming::rules::dunder_function_name( + if checker.is_rule_enabled(Rule::DunderFunctionName) { + pep8_naming::rules::dunder_function_name( + checker, checker.semantic.current_scope(), stmt, name, - &checker.settings.pep8_naming.ignore_names, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::GlobalStatement) { + if checker.is_rule_enabled(Rule::GlobalStatement) { pylint::rules::global_statement(checker, name); } - if checker.enabled(Rule::LRUCacheWithoutParameters) { + if checker.is_rule_enabled(Rule::LRUCacheWithoutParameters) { if checker.target_version() >= PythonVersion::PY38 { pyupgrade::rules::lru_cache_without_parameters(checker, decorator_list); } } - if checker.enabled(Rule::LRUCacheWithMaxsizeNone) { + if checker.is_rule_enabled(Rule::LRUCacheWithMaxsizeNone) { if checker.target_version() >= PythonVersion::PY39 { pyupgrade::rules::lru_cache_with_maxsize_none(checker, decorator_list); } } - if checker.enabled(Rule::CachedInstanceMethod) { + if checker.is_rule_enabled(Rule::CachedInstanceMethod) { flake8_bugbear::rules::cached_instance_method(checker, function_def); } - if checker.enabled(Rule::MutableArgumentDefault) { + if checker.is_rule_enabled(Rule::MutableArgumentDefault) { flake8_bugbear::rules::mutable_argument_default(checker, function_def); } - if checker.enabled(Rule::ReturnInGenerator) { + if checker.is_rule_enabled(Rule::ReturnInGenerator) { flake8_bugbear::rules::return_in_generator(checker, function_def); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::UnnecessaryReturnNone, Rule::ImplicitReturnValue, Rule::ImplicitReturn, - Rule::UnnecessaryAssign, Rule::SuperfluousElseReturn, Rule::SuperfluousElseRaise, Rule::SuperfluousElseContinue, @@ -222,7 +214,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ]) { flake8_return::rules::function(checker, function_def); } - if checker.enabled(Rule::UselessReturn) { + if checker.is_rule_enabled(Rule::UselessReturn) { pylint::rules::useless_return( checker, stmt, @@ -230,61 +222,57 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { returns.as_ref().map(AsRef::as_ref), ); } - if checker.enabled(Rule::ComplexStructure) { - if let Some(diagnostic) = mccabe::rules::function_is_too_complex( + if checker.is_rule_enabled(Rule::ComplexStructure) { + mccabe::rules::function_is_too_complex( + checker, stmt, name, body, - checker.settings.mccabe.max_complexity, - ) { - checker.report_diagnostic(diagnostic); - } + checker.settings().mccabe.max_complexity, + ); } - if checker.enabled(Rule::HardcodedPasswordDefault) { + if checker.is_rule_enabled(Rule::HardcodedPasswordDefault) { flake8_bandit::rules::hardcoded_password_default(checker, parameters); } - if checker.enabled(Rule::SuspiciousMarkSafeUsage) { + if checker.is_rule_enabled(Rule::SuspiciousMarkSafeUsage) { for decorator in decorator_list { flake8_bandit::rules::suspicious_function_decorator(checker, decorator); } } - if checker.enabled(Rule::PropertyWithParameters) { + if checker.is_rule_enabled(Rule::PropertyWithParameters) { pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters); } - if checker.enabled(Rule::TooManyArguments) { + if checker.is_rule_enabled(Rule::TooManyArguments) { pylint::rules::too_many_arguments(checker, function_def); } - if checker.enabled(Rule::TooManyPositionalArguments) { + if checker.is_rule_enabled(Rule::TooManyPositionalArguments) { pylint::rules::too_many_positional_arguments(checker, function_def); } - if checker.enabled(Rule::TooManyReturnStatements) { - if let Some(diagnostic) = pylint::rules::too_many_return_statements( + if checker.is_rule_enabled(Rule::TooManyReturnStatements) { + pylint::rules::too_many_return_statements( + checker, stmt, body, - checker.settings.pylint.max_returns, - ) { - checker.report_diagnostic(diagnostic); - } + checker.settings().pylint.max_returns, + ); } - if checker.enabled(Rule::TooManyBranches) { - if let Some(diagnostic) = pylint::rules::too_many_branches( + if checker.is_rule_enabled(Rule::TooManyBranches) { + pylint::rules::too_many_branches( + checker, stmt, body, - checker.settings.pylint.max_branches, - ) { - checker.report_diagnostic(diagnostic); - } + checker.settings().pylint.max_branches, + ); } - if checker.enabled(Rule::TooManyStatements) { - if let Some(diagnostic) = pylint::rules::too_many_statements( + if checker.is_rule_enabled(Rule::TooManyStatements) { + pylint::rules::too_many_statements( + checker, stmt, body, - checker.settings.pylint.max_statements, - ) { - checker.report_diagnostic(diagnostic); - } + checker.settings().pylint.max_statements, + ); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::PytestFixtureIncorrectParenthesesStyle, Rule::PytestFixturePositionalArgs, Rule::PytestExtraneousScopeFunction, @@ -305,13 +293,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::PytestIncorrectMarkParenthesesStyle, Rule::PytestUseFixturesWithoutParameters, ]) { flake8_pytest_style::rules::marks(checker, decorator_list); } - if checker.enabled(Rule::BooleanTypeHintPositionalArgument) { + if checker.is_rule_enabled(Rule::BooleanTypeHintPositionalArgument) { flake8_boolean_trap::rules::boolean_type_hint_positional_argument( checker, name, @@ -319,7 +307,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { parameters, ); } - if checker.enabled(Rule::BooleanDefaultValuePositionalArgument) { + if checker.is_rule_enabled(Rule::BooleanDefaultValuePositionalArgument) { flake8_boolean_trap::rules::boolean_default_value_positional_argument( checker, name, @@ -327,7 +315,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { parameters, ); } - if checker.enabled(Rule::UnexpectedSpecialMethodSignature) { + if checker.is_rule_enabled(Rule::UnexpectedSpecialMethodSignature) { pylint::rules::unexpected_special_method_signature( checker, stmt, @@ -336,51 +324,51 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { parameters, ); } - if checker.enabled(Rule::FStringDocstring) { + if checker.is_rule_enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(checker, body); } if !checker.semantic.current_scope().kind.is_class() { - if checker.enabled(Rule::BuiltinVariableShadowing) { + if checker.is_rule_enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range()); } } - if checker.enabled(Rule::AsyncFunctionWithTimeout) { + if checker.is_rule_enabled(Rule::AsyncFunctionWithTimeout) { flake8_async::rules::async_function_with_timeout(checker, function_def); } #[cfg(any(feature = "test-rules", test))] - if checker.enabled(Rule::UnreachableCode) { + if checker.is_rule_enabled(Rule::UnreachableCode) { pylint::rules::in_function(checker, name, body); } - if checker.enabled(Rule::ReimplementedOperator) { + if checker.is_rule_enabled(Rule::ReimplementedOperator) { refurb::rules::reimplemented_operator(checker, &function_def.into()); } - if checker.enabled(Rule::SslWithBadDefaults) { + if checker.is_rule_enabled(Rule::SslWithBadDefaults) { flake8_bandit::rules::ssl_with_bad_defaults(checker, function_def); } - if checker.enabled(Rule::UnusedAsync) { + if checker.is_rule_enabled(Rule::UnusedAsync) { ruff::rules::unused_async(checker, function_def); } - if checker.enabled(Rule::WhitespaceAfterDecorator) { + if checker.is_rule_enabled(Rule::WhitespaceAfterDecorator) { pycodestyle::rules::whitespace_after_decorator(checker, decorator_list); } - if checker.enabled(Rule::PostInitDefault) { + if checker.is_rule_enabled(Rule::PostInitDefault) { ruff::rules::post_init_default(checker, function_def); } - if checker.enabled(Rule::PytestParameterWithDefaultArgument) { + if checker.is_rule_enabled(Rule::PytestParameterWithDefaultArgument) { flake8_pytest_style::rules::parameter_with_default_argument(checker, function_def); } - if checker.enabled(Rule::Airflow3Removal) { + if checker.is_rule_enabled(Rule::Airflow3Removal) { airflow::rules::airflow_3_removal_function_def(checker, function_def); } - if checker.enabled(Rule::NonPEP695GenericFunction) { + if checker.is_rule_enabled(Rule::NonPEP695GenericFunction) { pyupgrade::rules::non_pep695_generic_function(checker, function_def); } - if checker.enabled(Rule::InvalidArgumentName) { + if checker.is_rule_enabled(Rule::InvalidArgumentName) { pep8_naming::rules::invalid_argument_name_function(checker, function_def); } } Stmt::Return(_) => { - if checker.enabled(Rule::ReturnInInit) { + if checker.is_rule_enabled(Rule::ReturnInInit) { pylint::rules::return_in_init(checker, stmt); } } @@ -392,87 +380,87 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { decorator_list, body, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::NoClassmethodDecorator) { + if checker.is_rule_enabled(Rule::NoClassmethodDecorator) { pylint::rules::no_classmethod_decorator(checker, stmt); } - if checker.enabled(Rule::NoStaticmethodDecorator) { + if checker.is_rule_enabled(Rule::NoStaticmethodDecorator) { pylint::rules::no_staticmethod_decorator(checker, stmt); } - if checker.enabled(Rule::DjangoNullableModelStringField) { + if checker.is_rule_enabled(Rule::DjangoNullableModelStringField) { flake8_django::rules::nullable_model_string_field(checker, body); } - if checker.enabled(Rule::DjangoExcludeWithModelForm) { + if checker.is_rule_enabled(Rule::DjangoExcludeWithModelForm) { flake8_django::rules::exclude_with_model_form(checker, class_def); } - if checker.enabled(Rule::DjangoAllWithModelForm) { + if checker.is_rule_enabled(Rule::DjangoAllWithModelForm) { flake8_django::rules::all_with_model_form(checker, class_def); } - if checker.enabled(Rule::DjangoUnorderedBodyContentInModel) { + if checker.is_rule_enabled(Rule::DjangoUnorderedBodyContentInModel) { flake8_django::rules::unordered_body_content_in_model(checker, class_def); } if !checker.source_type.is_stub() { - if checker.enabled(Rule::DjangoModelWithoutDunderStr) { + if checker.is_rule_enabled(Rule::DjangoModelWithoutDunderStr) { flake8_django::rules::model_without_dunder_str(checker, class_def); } } - if checker.enabled(Rule::EqWithoutHash) { + if checker.is_rule_enabled(Rule::EqWithoutHash) { pylint::rules::object_without_hash_method(checker, class_def); } - if checker.enabled(Rule::ClassAsDataStructure) { + if checker.is_rule_enabled(Rule::ClassAsDataStructure) { flake8_bugbear::rules::class_as_data_structure(checker, class_def); } - if checker.enabled(Rule::RedefinedSlotsInSubclass) { + if checker.is_rule_enabled(Rule::RedefinedSlotsInSubclass) { pylint::rules::redefined_slots_in_subclass(checker, class_def); } - if checker.enabled(Rule::TooManyPublicMethods) { + if checker.is_rule_enabled(Rule::TooManyPublicMethods) { pylint::rules::too_many_public_methods( checker, class_def, - checker.settings.pylint.max_public_methods, + checker.settings().pylint.max_public_methods, ); } - if checker.enabled(Rule::GlobalStatement) { + if checker.is_rule_enabled(Rule::GlobalStatement) { pylint::rules::global_statement(checker, name); } - if checker.enabled(Rule::UselessObjectInheritance) { + if checker.is_rule_enabled(Rule::UselessObjectInheritance) { pyupgrade::rules::useless_object_inheritance(checker, class_def); } - if checker.enabled(Rule::ReplaceStrEnum) { + if checker.is_rule_enabled(Rule::UselessClassMetaclassType) { + pyupgrade::rules::useless_class_metaclass_type(checker, class_def); + } + if checker.is_rule_enabled(Rule::ReplaceStrEnum) { if checker.target_version() >= PythonVersion::PY311 { pyupgrade::rules::replace_str_enum(checker, class_def); } } - if checker.enabled(Rule::UnnecessaryClassParentheses) { + if checker.is_rule_enabled(Rule::UnnecessaryClassParentheses) { pyupgrade::rules::unnecessary_class_parentheses(checker, class_def); } - if checker.enabled(Rule::AmbiguousClassName) { - if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::AmbiguousClassName) { + pycodestyle::rules::ambiguous_class_name(checker, name); } - if checker.enabled(Rule::InvalidClassName) { - if let Some(diagnostic) = pep8_naming::rules::invalid_class_name( + if checker.is_rule_enabled(Rule::InvalidClassName) { + pep8_naming::rules::invalid_class_name( + checker, stmt, name, - &checker.settings.pep8_naming.ignore_names, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::ErrorSuffixOnExceptionName) { - if let Some(diagnostic) = pep8_naming::rules::error_suffix_on_exception_name( + if checker.is_rule_enabled(Rule::ErrorSuffixOnExceptionName) { + pep8_naming::rules::error_suffix_on_exception_name( + checker, stmt, arguments.as_deref(), name, - &checker.settings.pep8_naming.ignore_names, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().pep8_naming.ignore_names, + ); } if !checker.source_type.is_stub() { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::AbstractBaseClassWithoutAbstractMethod, Rule::EmptyMethodWithoutAbstractDecorator, ]) { @@ -486,85 +474,89 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } if checker.source_type.is_stub() { - if checker.enabled(Rule::PassStatementStubBody) { + if checker.is_rule_enabled(Rule::PassStatementStubBody) { flake8_pyi::rules::pass_statement_stub_body(checker, body); } - if checker.enabled(Rule::PassInClassBody) { + if checker.is_rule_enabled(Rule::PassInClassBody) { flake8_pyi::rules::pass_in_class_body(checker, class_def); } } - if checker.enabled(Rule::EllipsisInNonEmptyClassBody) { + if checker.is_rule_enabled(Rule::EllipsisInNonEmptyClassBody) { flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, body); } - if checker.enabled(Rule::GenericNotLastBaseClass) { + if checker.is_rule_enabled(Rule::GenericNotLastBaseClass) { flake8_pyi::rules::generic_not_last_base_class(checker, class_def); } - if checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle) { + if checker.is_rule_enabled(Rule::PytestIncorrectMarkParenthesesStyle) { flake8_pytest_style::rules::marks(checker, decorator_list); } - if checker.enabled(Rule::DuplicateClassFieldDefinition) { + if checker.is_rule_enabled(Rule::DuplicateClassFieldDefinition) { flake8_pie::rules::duplicate_class_field_definition(checker, body); } - if checker.enabled(Rule::NonUniqueEnums) { + if checker.is_rule_enabled(Rule::NonUniqueEnums) { flake8_pie::rules::non_unique_enums(checker, stmt, body); } - if checker.enabled(Rule::FStringDocstring) { + if checker.is_rule_enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(checker, body); } - if checker.enabled(Rule::BuiltinVariableShadowing) { + if checker.is_rule_enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range()); } - if checker.enabled(Rule::DuplicateBases) { + if checker.is_rule_enabled(Rule::DuplicateBases) { pylint::rules::duplicate_bases(checker, name, arguments.as_deref()); } - if checker.enabled(Rule::NoSlotsInStrSubclass) { + if checker.is_rule_enabled(Rule::NoSlotsInStrSubclass) { flake8_slots::rules::no_slots_in_str_subclass(checker, stmt, class_def); } - if checker.enabled(Rule::NoSlotsInTupleSubclass) { + if checker.is_rule_enabled(Rule::NoSlotsInTupleSubclass) { flake8_slots::rules::no_slots_in_tuple_subclass(checker, stmt, class_def); } - if checker.enabled(Rule::NoSlotsInNamedtupleSubclass) { + if checker.is_rule_enabled(Rule::NoSlotsInNamedtupleSubclass) { flake8_slots::rules::no_slots_in_namedtuple_subclass(checker, stmt, class_def); } - if checker.enabled(Rule::NonSlotAssignment) { + if checker.is_rule_enabled(Rule::NonSlotAssignment) { pylint::rules::non_slot_assignment(checker, class_def); } - if checker.enabled(Rule::SingleStringSlots) { + if checker.is_rule_enabled(Rule::SingleStringSlots) { pylint::rules::single_string_slots(checker, class_def); } - if checker.enabled(Rule::MetaClassABCMeta) { + if checker.is_rule_enabled(Rule::MetaClassABCMeta) { refurb::rules::metaclass_abcmeta(checker, class_def); } - if checker.enabled(Rule::WhitespaceAfterDecorator) { + if checker.is_rule_enabled(Rule::WhitespaceAfterDecorator) { pycodestyle::rules::whitespace_after_decorator(checker, decorator_list); } - if checker.enabled(Rule::SubclassBuiltin) { + if checker.is_rule_enabled(Rule::SubclassBuiltin) { refurb::rules::subclass_builtin(checker, class_def); } - if checker.enabled(Rule::DataclassEnum) { + if checker.is_rule_enabled(Rule::DataclassEnum) { ruff::rules::dataclass_enum(checker, class_def); } - if checker.enabled(Rule::NonPEP695GenericClass) { + if checker.is_rule_enabled(Rule::NonPEP695GenericClass) { pyupgrade::rules::non_pep695_generic_class(checker, class_def); } - if checker.enabled(Rule::ClassWithMixedTypeVars) { + if checker.is_rule_enabled(Rule::ClassWithMixedTypeVars) { ruff::rules::class_with_mixed_type_vars(checker, class_def); } - if checker.enabled(Rule::ImplicitClassVarInDataclass) { + if checker.is_rule_enabled(Rule::ImplicitClassVarInDataclass) { ruff::rules::implicit_class_var_in_dataclass(checker, class_def); } } - Stmt::Import(ast::StmtImport { names, range: _ }) => { - if checker.enabled(Rule::MultipleImportsOnOneLine) { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { + if checker.is_rule_enabled(Rule::MultipleImportsOnOneLine) { pycodestyle::rules::multiple_imports_on_one_line(checker, stmt, names); } - if checker.enabled(Rule::ModuleImportNotAtTopOfFile) { + if checker.is_rule_enabled(Rule::ModuleImportNotAtTopOfFile) { pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt); } - if checker.enabled(Rule::ImportOutsideTopLevel) { + if checker.is_rule_enabled(Rule::ImportOutsideTopLevel) { pylint::rules::import_outside_top_level(checker, stmt); } - if checker.enabled(Rule::GlobalStatement) { + if checker.is_rule_enabled(Rule::GlobalStatement) { for name in names { if let Some(asname) = name.asname.as_ref() { pylint::rules::global_statement(checker, asname); @@ -573,13 +565,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - if checker.enabled(Rule::DeprecatedCElementTree) { + if checker.is_rule_enabled(Rule::DeprecatedCElementTree) { pyupgrade::rules::deprecated_c_element_tree(checker, stmt); } - if checker.enabled(Rule::DeprecatedMockImport) { + if checker.is_rule_enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_import(checker, stmt); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SuspiciousTelnetlibImport, Rule::SuspiciousFtplibImport, Rule::SuspiciousPickleImport, @@ -598,23 +590,19 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_bandit::rules::suspicious_imports(checker, stmt); } - if checker.enabled(Rule::BannedModuleLevelImports) { + if checker.is_rule_enabled(Rule::BannedModuleLevelImports) { flake8_tidy_imports::rules::banned_module_level_imports(checker, stmt); } for alias in names { - if checker.enabled(Rule::NonAsciiImportName) { + if checker.is_rule_enabled(Rule::NonAsciiImportName) { pylint::rules::non_ascii_module_import(checker, alias); } - if checker.enabled(Rule::Debugger) { - if let Some(diagnostic) = - flake8_debugger::rules::debugger_import(stmt, None, &alias.name) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::Debugger) { + flake8_debugger::rules::debugger_import(checker, stmt, None, &alias.name); } - if checker.enabled(Rule::BannedApi) { + if checker.is_rule_enabled(Rule::BannedApi) { flake8_tidy_imports::rules::banned_api( checker, &flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent( @@ -627,104 +615,84 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } if !checker.source_type.is_stub() { - if checker.enabled(Rule::UselessImportAlias) { + if checker.is_rule_enabled(Rule::UselessImportAlias) { pylint::rules::useless_import_alias(checker, alias); } } - if checker.enabled(Rule::ManualFromImport) { + if checker.is_rule_enabled(Rule::ManualFromImport) { pylint::rules::manual_from_import(checker, stmt, alias, names); } - if checker.enabled(Rule::ImportSelf) { - if let Some(diagnostic) = - pylint::rules::import_self(alias, checker.module.qualified_name()) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::ImportSelf) { + pylint::rules::import_self(checker, alias, checker.module.qualified_name()); } if let Some(asname) = &alias.asname { let name = alias.name.split('.').next_back().unwrap(); - if checker.enabled(Rule::ConstantImportedAsNonConstant) { - if let Some(diagnostic) = - pep8_naming::rules::constant_imported_as_non_constant( - name, - asname, - alias, - stmt, - &checker.settings.pep8_naming.ignore_names, - ) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::ConstantImportedAsNonConstant) { + pep8_naming::rules::constant_imported_as_non_constant( + checker, + name, + asname, + alias, + stmt, + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::LowercaseImportedAsNonLowercase) { - if let Some(diagnostic) = - pep8_naming::rules::lowercase_imported_as_non_lowercase( - name, - asname, - alias, - stmt, - &checker.settings.pep8_naming.ignore_names, - ) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::LowercaseImportedAsNonLowercase) { + pep8_naming::rules::lowercase_imported_as_non_lowercase( + checker, + name, + asname, + alias, + stmt, + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::CamelcaseImportedAsLowercase) { - if let Some(diagnostic) = - pep8_naming::rules::camelcase_imported_as_lowercase( - name, - asname, - alias, - stmt, - &checker.settings.pep8_naming.ignore_names, - ) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::CamelcaseImportedAsLowercase) { + pep8_naming::rules::camelcase_imported_as_lowercase( + checker, + name, + asname, + alias, + stmt, + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::CamelcaseImportedAsConstant) { - if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( + if checker.is_rule_enabled(Rule::CamelcaseImportedAsConstant) { + pep8_naming::rules::camelcase_imported_as_constant( + checker, name, asname, alias, stmt, - &checker.settings.pep8_naming.ignore_names, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::CamelcaseImportedAsAcronym) { - if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( + if checker.is_rule_enabled(Rule::CamelcaseImportedAsAcronym) { + pep8_naming::rules::camelcase_imported_as_acronym( name, asname, alias, stmt, checker, - ) { - checker.report_diagnostic(diagnostic); - } + ); } } - if checker.enabled(Rule::BannedImportAlias) { + if checker.is_rule_enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { - if let Some(diagnostic) = - flake8_import_conventions::rules::banned_import_alias( - stmt, - &alias.name, - asname, - &checker.settings.flake8_import_conventions.banned_aliases, - ) - { - checker.report_diagnostic(diagnostic); - } + flake8_import_conventions::rules::banned_import_alias( + checker, + stmt, + &alias.name, + asname, + &checker.settings().flake8_import_conventions.banned_aliases, + ); } } - if checker.enabled(Rule::PytestIncorrectPytestImport) { - if let Some(diagnostic) = flake8_pytest_style::rules::import( + if checker.is_rule_enabled(Rule::PytestIncorrectPytestImport) { + flake8_pytest_style::rules::import( + checker, stmt, &alias.name, alias.asname.as_deref(), - ) { - checker.report_diagnostic(diagnostic); - } + ); } - if checker.enabled(Rule::BuiltinImportShadowing) { + if checker.is_rule_enabled(Rule::BuiltinImportShadowing) { flake8_builtins::rules::builtin_import_shadowing(checker, alias); } } @@ -735,17 +703,18 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { module, level, range: _, + node_index: _, }, ) => { let level = *level; let module = module.as_deref(); - if checker.enabled(Rule::ModuleImportNotAtTopOfFile) { + if checker.is_rule_enabled(Rule::ModuleImportNotAtTopOfFile) { pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt); } - if checker.enabled(Rule::ImportOutsideTopLevel) { + if checker.is_rule_enabled(Rule::ImportOutsideTopLevel) { pylint::rules::import_outside_top_level(checker, stmt); } - if checker.enabled(Rule::GlobalStatement) { + if checker.is_rule_enabled(Rule::GlobalStatement) { for name in names { if let Some(asname) = name.asname.as_ref() { pylint::rules::global_statement(checker, asname); @@ -754,33 +723,33 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - if checker.enabled(Rule::NonAsciiImportName) { + if checker.is_rule_enabled(Rule::NonAsciiImportName) { for alias in names { pylint::rules::non_ascii_module_import(checker, alias); } } - if checker.enabled(Rule::UnnecessaryFutureImport) { + if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) { if checker.target_version() >= PythonVersion::PY37 { if let Some("__future__") = module { pyupgrade::rules::unnecessary_future_import(checker, stmt, names); } } } - if checker.enabled(Rule::DeprecatedMockImport) { + if checker.is_rule_enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_import(checker, stmt); } - if checker.enabled(Rule::DeprecatedCElementTree) { + if checker.is_rule_enabled(Rule::DeprecatedCElementTree) { pyupgrade::rules::deprecated_c_element_tree(checker, stmt); } - if checker.enabled(Rule::DeprecatedImport) { + if checker.is_rule_enabled(Rule::DeprecatedImport) { pyupgrade::rules::deprecated_import(checker, import_from); } - if checker.enabled(Rule::UnnecessaryBuiltinImport) { + if checker.is_rule_enabled(Rule::UnnecessaryBuiltinImport) { if let Some(module) = module { pyupgrade::rules::unnecessary_builtin_import(checker, stmt, module, names); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::SuspiciousTelnetlibImport, Rule::SuspiciousFtplibImport, Rule::SuspiciousPickleImport, @@ -798,7 +767,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ]) { flake8_bandit::rules::suspicious_imports(checker, stmt); } - if checker.enabled(Rule::BannedApi) { + if checker.is_rule_enabled(Rule::BannedApi) { if let Some(module) = helpers::resolve_imported_module_path( level, module, @@ -829,189 +798,162 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - if checker.enabled(Rule::BannedModuleLevelImports) { + if checker.is_rule_enabled(Rule::BannedModuleLevelImports) { flake8_tidy_imports::rules::banned_module_level_imports(checker, stmt); } - if checker.enabled(Rule::PytestIncorrectPytestImport) { - if let Some(diagnostic) = - flake8_pytest_style::rules::import_from(stmt, module, level) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::PytestIncorrectPytestImport) { + flake8_pytest_style::rules::import_from(checker, stmt, module, level); } if checker.source_type.is_stub() { - if checker.enabled(Rule::FutureAnnotationsInStub) { + if checker.is_rule_enabled(Rule::FutureAnnotationsInStub) { flake8_pyi::rules::from_future_import(checker, import_from); } } for alias in names { if let Some("__future__") = module { - if checker.enabled(Rule::FutureFeatureNotDefined) { + if checker.is_rule_enabled(Rule::FutureFeatureNotDefined) { pyflakes::rules::future_feature_not_defined(checker, alias); } } else if &alias.name == "*" { - if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { + // F406 + if checker.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( pyflakes::rules::UndefinedLocalWithNestedImportStarUsage { name: helpers::format_import_from(level, module).to_string(), }, stmt.range(), - )); + ); } } - if checker.enabled(Rule::UndefinedLocalWithImportStar) { - checker.report_diagnostic(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStar { - name: helpers::format_import_from(level, module).to_string(), - }, - stmt.range(), - )); - } + // F403 + checker.report_diagnostic_if_enabled( + pyflakes::rules::UndefinedLocalWithImportStar { + name: helpers::format_import_from(level, module).to_string(), + }, + stmt.range(), + ); } - if checker.enabled(Rule::RelativeImports) { - if let Some(diagnostic) = flake8_tidy_imports::rules::banned_relative_import( + if checker.is_rule_enabled(Rule::RelativeImports) { + flake8_tidy_imports::rules::banned_relative_import( checker, stmt, level, module, checker.module.qualified_name(), - checker.settings.flake8_tidy_imports.ban_relative_imports, - ) { - checker.report_diagnostic(diagnostic); - } + checker.settings().flake8_tidy_imports.ban_relative_imports, + ); } - if checker.enabled(Rule::Debugger) { - if let Some(diagnostic) = - flake8_debugger::rules::debugger_import(stmt, module, &alias.name) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::Debugger) { + flake8_debugger::rules::debugger_import(checker, stmt, module, &alias.name); } - if checker.enabled(Rule::BannedImportAlias) { + if checker.is_rule_enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { let qualified_name = helpers::format_import_from_member(level, module, &alias.name); - if let Some(diagnostic) = - flake8_import_conventions::rules::banned_import_alias( - stmt, - &qualified_name, - asname, - &checker.settings.flake8_import_conventions.banned_aliases, - ) - { - checker.report_diagnostic(diagnostic); - } + flake8_import_conventions::rules::banned_import_alias( + checker, + stmt, + &qualified_name, + asname, + &checker.settings().flake8_import_conventions.banned_aliases, + ); } } if let Some(asname) = &alias.asname { - if checker.enabled(Rule::ConstantImportedAsNonConstant) { - if let Some(diagnostic) = - pep8_naming::rules::constant_imported_as_non_constant( - &alias.name, - asname, - alias, - stmt, - &checker.settings.pep8_naming.ignore_names, - ) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::ConstantImportedAsNonConstant) { + pep8_naming::rules::constant_imported_as_non_constant( + checker, + &alias.name, + asname, + alias, + stmt, + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::LowercaseImportedAsNonLowercase) { - if let Some(diagnostic) = - pep8_naming::rules::lowercase_imported_as_non_lowercase( - &alias.name, - asname, - alias, - stmt, - &checker.settings.pep8_naming.ignore_names, - ) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::LowercaseImportedAsNonLowercase) { + pep8_naming::rules::lowercase_imported_as_non_lowercase( + checker, + &alias.name, + asname, + alias, + stmt, + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::CamelcaseImportedAsLowercase) { - if let Some(diagnostic) = - pep8_naming::rules::camelcase_imported_as_lowercase( - &alias.name, - asname, - alias, - stmt, - &checker.settings.pep8_naming.ignore_names, - ) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::CamelcaseImportedAsLowercase) { + pep8_naming::rules::camelcase_imported_as_lowercase( + checker, + &alias.name, + asname, + alias, + stmt, + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::CamelcaseImportedAsConstant) { - if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( + if checker.is_rule_enabled(Rule::CamelcaseImportedAsConstant) { + pep8_naming::rules::camelcase_imported_as_constant( + checker, &alias.name, asname, alias, stmt, - &checker.settings.pep8_naming.ignore_names, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().pep8_naming.ignore_names, + ); } - if checker.enabled(Rule::CamelcaseImportedAsAcronym) { - if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( + if checker.is_rule_enabled(Rule::CamelcaseImportedAsAcronym) { + pep8_naming::rules::camelcase_imported_as_acronym( &alias.name, asname, alias, stmt, checker, - ) { - checker.report_diagnostic(diagnostic); - } + ); } if !checker.source_type.is_stub() { - if checker.enabled(Rule::UselessImportAlias) { + if checker.is_rule_enabled(Rule::UselessImportAlias) { pylint::rules::useless_import_from_alias(checker, alias, module, level); } } } - if checker.enabled(Rule::BuiltinImportShadowing) { + if checker.is_rule_enabled(Rule::BuiltinImportShadowing) { flake8_builtins::rules::builtin_import_shadowing(checker, alias); } } - if checker.enabled(Rule::ImportSelf) { - if let Some(diagnostic) = pylint::rules::import_from_self( + if checker.is_rule_enabled(Rule::ImportSelf) { + pylint::rules::import_from_self( + checker, level, module, names, checker.module.qualified_name(), - ) { - checker.report_diagnostic(diagnostic); - } + ); } - if checker.enabled(Rule::BannedImportFrom) { - if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_from( + if checker.is_rule_enabled(Rule::BannedImportFrom) { + flake8_import_conventions::rules::banned_import_from( + checker, stmt, &helpers::format_import_from(level, module), - &checker.settings.flake8_import_conventions.banned_from, - ) { - checker.report_diagnostic(diagnostic); - } + &checker.settings().flake8_import_conventions.banned_from, + ); } - if checker.enabled(Rule::ByteStringUsage) { + if checker.is_rule_enabled(Rule::ByteStringUsage) { flake8_pyi::rules::bytestring_import(checker, import_from); } } Stmt::Raise(raise @ ast::StmtRaise { exc, .. }) => { - if checker.enabled(Rule::RaiseNotImplemented) { + if checker.is_rule_enabled(Rule::RaiseNotImplemented) { if let Some(expr) = exc { pyflakes::rules::raise_not_implemented(checker, expr); } } - if checker.enabled(Rule::RaiseLiteral) { + if checker.is_rule_enabled(Rule::RaiseLiteral) { if let Some(exc) = exc { flake8_bugbear::rules::raise_literal(checker, exc); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::RawStringInException, Rule::FStringInException, Rule::DotFormatInException, @@ -1020,44 +962,44 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_errmsg::rules::string_in_exception(checker, stmt, exc); } } - if checker.enabled(Rule::OSErrorAlias) { + if checker.is_rule_enabled(Rule::OSErrorAlias) { if let Some(item) = exc { pyupgrade::rules::os_error_alias_raise(checker, item); } } - if checker.enabled(Rule::TimeoutErrorAlias) { + if checker.is_rule_enabled(Rule::TimeoutErrorAlias) { if checker.target_version() >= PythonVersion::PY310 { if let Some(item) = exc { pyupgrade::rules::timeout_error_alias_raise(checker, item); } } } - if checker.enabled(Rule::RaiseVanillaClass) { + if checker.is_rule_enabled(Rule::RaiseVanillaClass) { if let Some(expr) = exc { tryceratops::rules::raise_vanilla_class(checker, expr); } } - if checker.enabled(Rule::RaiseVanillaArgs) { + if checker.is_rule_enabled(Rule::RaiseVanillaArgs) { if let Some(expr) = exc { tryceratops::rules::raise_vanilla_args(checker, expr); } } - if checker.enabled(Rule::UnnecessaryParenOnRaiseException) { + if checker.is_rule_enabled(Rule::UnnecessaryParenOnRaiseException) { if let Some(expr) = exc { flake8_raise::rules::unnecessary_paren_on_raise_exception(checker, expr); } } - if checker.enabled(Rule::MisplacedBareRaise) { + if checker.is_rule_enabled(Rule::MisplacedBareRaise) { pylint::rules::misplaced_bare_raise(checker, raise); } } Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => { - if checker.enabled(Rule::GlobalStatement) { + if checker.is_rule_enabled(Rule::GlobalStatement) { if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { pylint::rules::global_statement(checker, id); } } - if checker.enabled(Rule::UnsortedDunderAll) { + if checker.is_rule_enabled(Rule::UnsortedDunderAll) { ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign); } } @@ -1068,64 +1010,64 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { .. }, ) => { - if checker.enabled(Rule::TooManyNestedBlocks) { + if checker.is_rule_enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } - if checker.enabled(Rule::EmptyTypeCheckingBlock) { + if checker.is_rule_enabled(Rule::EmptyTypeCheckingBlock) { flake8_type_checking::rules::empty_type_checking_block(checker, if_); } - if checker.enabled(Rule::IfTuple) { + if checker.is_rule_enabled(Rule::IfTuple) { pyflakes::rules::if_tuple(checker, if_); } - if checker.enabled(Rule::CollapsibleIf) { + if checker.is_rule_enabled(Rule::CollapsibleIf) { flake8_simplify::rules::nested_if_statements( checker, if_, checker.semantic.current_statement_parent(), ); } - if checker.enabled(Rule::IfWithSameArms) { + if checker.is_rule_enabled(Rule::IfWithSameArms) { flake8_simplify::rules::if_with_same_arms(checker, if_); } - if checker.enabled(Rule::NeedlessBool) { + if checker.is_rule_enabled(Rule::NeedlessBool) { flake8_simplify::rules::needless_bool(checker, stmt); } - if checker.enabled(Rule::IfElseBlockInsteadOfDictLookup) { + if checker.is_rule_enabled(Rule::IfElseBlockInsteadOfDictLookup) { flake8_simplify::rules::if_else_block_instead_of_dict_lookup(checker, if_); } - if checker.enabled(Rule::IfElseBlockInsteadOfIfExp) { + if checker.is_rule_enabled(Rule::IfElseBlockInsteadOfIfExp) { flake8_simplify::rules::if_else_block_instead_of_if_exp(checker, if_); } - if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) { + if checker.is_rule_enabled(Rule::IfElseBlockInsteadOfDictGet) { flake8_simplify::rules::if_else_block_instead_of_dict_get(checker, if_); } - if checker.enabled(Rule::TypeCheckWithoutTypeError) { + if checker.is_rule_enabled(Rule::TypeCheckWithoutTypeError) { tryceratops::rules::type_check_without_type_error( checker, if_, checker.semantic.current_statement_parent(), ); } - if checker.enabled(Rule::OutdatedVersionBlock) { + if checker.is_rule_enabled(Rule::OutdatedVersionBlock) { pyupgrade::rules::outdated_version_block(checker, if_); } - if checker.enabled(Rule::CollapsibleElseIf) { + if checker.is_rule_enabled(Rule::CollapsibleElseIf) { pylint::rules::collapsible_else_if(checker, stmt); } - if checker.enabled(Rule::CheckAndRemoveFromSet) { + if checker.is_rule_enabled(Rule::CheckAndRemoveFromSet) { refurb::rules::check_and_remove_from_set(checker, if_); } - if checker.enabled(Rule::SliceToRemovePrefixOrSuffix) { + if checker.is_rule_enabled(Rule::SliceToRemovePrefixOrSuffix) { refurb::rules::slice_to_remove_affix_stmt(checker, if_); } - if checker.enabled(Rule::TooManyBooleanExpressions) { + if checker.is_rule_enabled(Rule::TooManyBooleanExpressions) { pylint::rules::too_many_boolean_expressions(checker, if_); } - if checker.enabled(Rule::IfStmtMinMax) { + if checker.is_rule_enabled(Rule::IfStmtMinMax) { pylint::rules::if_stmt_min_max(checker, if_); } if checker.source_type.is_stub() { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::UnrecognizedVersionInfoCheck, Rule::PatchVersionComparison, Rule::WrongTupleLengthVersionComparison, @@ -1138,7 +1080,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_pyi::rules::unrecognized_version_info(checker, test); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::UnrecognizedPlatformCheck, Rule::UnrecognizedPlatformName, ]) { @@ -1150,7 +1092,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_pyi::rules::unrecognized_platform(checker, test); } } - if checker.enabled(Rule::ComplexIfStatementInStub) { + if checker.is_rule_enabled(Rule::ComplexIfStatementInStub) { if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { for value in values { flake8_pyi::rules::complex_if_statement_in_stub(checker, value); @@ -1160,7 +1102,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - if checker.any_enabled(&[Rule::BadVersionInfoComparison, Rule::BadVersionInfoOrder]) { + if checker + .any_rule_enabled(&[Rule::BadVersionInfoComparison, Rule::BadVersionInfoOrder]) + { fn bad_version_info_comparison( checker: &Checker, test: &Expr, @@ -1193,10 +1137,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } - if checker.enabled(Rule::IfKeyInDictDel) { + if checker.is_rule_enabled(Rule::IfKeyInDictDel) { ruff::rules::if_key_in_dict_del(checker, if_); } - if checker.enabled(Rule::NeedlessElse) { + if checker.is_rule_enabled(Rule::NeedlessElse) { ruff::rules::needless_else(checker, if_.into()); } } @@ -1205,23 +1149,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { test, msg, range: _, + node_index: _, }, ) => { if !checker.semantic.in_type_checking_block() { - if checker.enabled(Rule::Assert) { - checker.report_diagnostic(flake8_bandit::rules::assert_used(stmt)); + if checker.is_rule_enabled(Rule::Assert) { + flake8_bandit::rules::assert_used(checker, stmt); } } - if checker.enabled(Rule::AssertTuple) { + if checker.is_rule_enabled(Rule::AssertTuple) { pyflakes::rules::assert_tuple(checker, stmt, test); } - if checker.enabled(Rule::AssertFalse) { + if checker.is_rule_enabled(Rule::AssertFalse) { flake8_bugbear::rules::assert_false(checker, stmt, test, msg.as_deref()); } - if checker.enabled(Rule::PytestAssertAlwaysFalse) { + if checker.is_rule_enabled(Rule::PytestAssertAlwaysFalse) { flake8_pytest_style::rules::assert_falsy(checker, stmt, test); } - if checker.enabled(Rule::PytestCompositeAssertion) { + if checker.is_rule_enabled(Rule::PytestCompositeAssertion) { flake8_pytest_style::rules::composite_condition( checker, stmt, @@ -1229,72 +1174,72 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { msg.as_deref(), ); } - if checker.enabled(Rule::AssertOnStringLiteral) { + if checker.is_rule_enabled(Rule::AssertOnStringLiteral) { pylint::rules::assert_on_string_literal(checker, test); } - if checker.enabled(Rule::InvalidMockAccess) { + if checker.is_rule_enabled(Rule::InvalidMockAccess) { pygrep_hooks::rules::non_existent_mock_method(checker, test); } - if checker.enabled(Rule::AssertWithPrintMessage) { + if checker.is_rule_enabled(Rule::AssertWithPrintMessage) { ruff::rules::assert_with_print_message(checker, assert_stmt); } - if checker.enabled(Rule::InvalidAssertMessageLiteralArgument) { + if checker.is_rule_enabled(Rule::InvalidAssertMessageLiteralArgument) { ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt); } } Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => { - if checker.enabled(Rule::TooManyNestedBlocks) { + if checker.is_rule_enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } - if checker.enabled(Rule::AssertRaisesException) { + if checker.is_rule_enabled(Rule::AssertRaisesException) { flake8_bugbear::rules::assert_raises_exception(checker, items); } - if checker.enabled(Rule::PytestRaisesWithMultipleStatements) { + if checker.is_rule_enabled(Rule::PytestRaisesWithMultipleStatements) { flake8_pytest_style::rules::complex_raises(checker, stmt, items, body); } - if checker.enabled(Rule::PytestWarnsWithMultipleStatements) { + if checker.is_rule_enabled(Rule::PytestWarnsWithMultipleStatements) { flake8_pytest_style::rules::complex_warns(checker, stmt, items, body); } - if checker.enabled(Rule::MultipleWithStatements) { + if checker.is_rule_enabled(Rule::MultipleWithStatements) { flake8_simplify::rules::multiple_with_statements( checker, with_stmt, checker.semantic.current_statement_parent(), ); } - if checker.enabled(Rule::RedefinedLoopName) { + if checker.is_rule_enabled(Rule::RedefinedLoopName) { pylint::rules::redefined_loop_name(checker, stmt); } - if checker.enabled(Rule::ReadWholeFile) { + if checker.is_rule_enabled(Rule::ReadWholeFile) { refurb::rules::read_whole_file(checker, with_stmt); } - if checker.enabled(Rule::WriteWholeFile) { + if checker.is_rule_enabled(Rule::WriteWholeFile) { refurb::rules::write_whole_file(checker, with_stmt); } - if checker.enabled(Rule::UselessWithLock) { + if checker.is_rule_enabled(Rule::UselessWithLock) { pylint::rules::useless_with_lock(checker, with_stmt); } - if checker.enabled(Rule::CancelScopeNoCheckpoint) { + if checker.is_rule_enabled(Rule::CancelScopeNoCheckpoint) { flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, items); } } Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => { - if checker.enabled(Rule::TooManyNestedBlocks) { + if checker.is_rule_enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } - if checker.enabled(Rule::FunctionUsesLoopVariable) { + if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) { flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Stmt(stmt)); } - if checker.enabled(Rule::UselessElseOnLoop) { + if checker.is_rule_enabled(Rule::UselessElseOnLoop) { pylint::rules::useless_else_on_loop(checker, stmt, body, orelse); } - if checker.enabled(Rule::TryExceptInLoop) { + if checker.is_rule_enabled(Rule::TryExceptInLoop) { perflint::rules::try_except_in_loop(checker, body); } - if checker.enabled(Rule::AsyncBusyWait) { + if checker.is_rule_enabled(Rule::AsyncBusyWait) { flake8_async::rules::async_busy_wait(checker, while_stmt); } - if checker.enabled(Rule::NeedlessElse) { + if checker.is_rule_enabled(Rule::NeedlessElse) { ruff::rules::needless_else(checker, while_stmt.into()); } } @@ -1306,12 +1251,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { orelse, is_async, range: _, + node_index: _, }, ) => { - if checker.enabled(Rule::TooManyNestedBlocks) { + if checker.is_rule_enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::DictIndexMissingItems, Rule::EnumerateForLoop, Rule::IncorrectDictIterator, @@ -1319,70 +1265,69 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { Rule::UnnecessaryEnumerate, Rule::UnusedLoopControlVariable, Rule::YieldInForLoop, + Rule::ManualDictComprehension, Rule::ManualListComprehension, ]) { checker.analyze.for_loops.push(checker.semantic.snapshot()); } - if checker.enabled(Rule::LoopVariableOverridesIterator) { + if checker.is_rule_enabled(Rule::LoopVariableOverridesIterator) { flake8_bugbear::rules::loop_variable_overrides_iterator(checker, target, iter); } - if checker.enabled(Rule::FunctionUsesLoopVariable) { + if checker.is_rule_enabled(Rule::FunctionUsesLoopVariable) { flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Stmt(stmt)); } - if checker.enabled(Rule::ReuseOfGroupbyGenerator) { + if checker.is_rule_enabled(Rule::ReuseOfGroupbyGenerator) { flake8_bugbear::rules::reuse_of_groupby_generator(checker, target, body, iter); } - if checker.enabled(Rule::UselessElseOnLoop) { + if checker.is_rule_enabled(Rule::UselessElseOnLoop) { pylint::rules::useless_else_on_loop(checker, stmt, body, orelse); } - if checker.enabled(Rule::RedefinedLoopName) { + if checker.is_rule_enabled(Rule::RedefinedLoopName) { pylint::rules::redefined_loop_name(checker, stmt); } - if checker.enabled(Rule::IterationOverSet) { + if checker.is_rule_enabled(Rule::IterationOverSet) { pylint::rules::iteration_over_set(checker, iter); } - if checker.enabled(Rule::DictIterMissingItems) { + if checker.is_rule_enabled(Rule::DictIterMissingItems) { pylint::rules::dict_iter_missing_items(checker, target, iter); } - if checker.enabled(Rule::ManualListCopy) { + if checker.is_rule_enabled(Rule::ManualListCopy) { perflint::rules::manual_list_copy(checker, for_stmt); } - if checker.enabled(Rule::ManualDictComprehension) { - perflint::rules::manual_dict_comprehension(checker, target, body); - } - if checker.enabled(Rule::ModifiedIteratingSet) { + + if checker.is_rule_enabled(Rule::ModifiedIteratingSet) { pylint::rules::modified_iterating_set(checker, for_stmt); } - if checker.enabled(Rule::UnnecessaryListCast) { + if checker.is_rule_enabled(Rule::UnnecessaryListCast) { perflint::rules::unnecessary_list_cast(checker, iter, body); } - if checker.enabled(Rule::UnnecessaryListIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryListIndexLookup) { pylint::rules::unnecessary_list_index_lookup(checker, for_stmt); } - if checker.enabled(Rule::UnnecessaryDictIndexLookup) { + if checker.is_rule_enabled(Rule::UnnecessaryDictIndexLookup) { pylint::rules::unnecessary_dict_index_lookup(checker, for_stmt); } - if checker.enabled(Rule::ReadlinesInFor) { + if checker.is_rule_enabled(Rule::ReadlinesInFor) { refurb::rules::readlines_in_for(checker, for_stmt); } if !*is_async { - if checker.enabled(Rule::ReimplementedBuiltin) { + if checker.is_rule_enabled(Rule::ReimplementedBuiltin) { flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt); } - if checker.enabled(Rule::InDictKeys) { + if checker.is_rule_enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_for(checker, for_stmt); } - if checker.enabled(Rule::TryExceptInLoop) { + if checker.is_rule_enabled(Rule::TryExceptInLoop) { perflint::rules::try_except_in_loop(checker, body); } - if checker.enabled(Rule::ForLoopSetMutations) { + if checker.is_rule_enabled(Rule::ForLoopSetMutations) { refurb::rules::for_loop_set_mutations(checker, for_stmt); } - if checker.enabled(Rule::ForLoopWrites) { + if checker.is_rule_enabled(Rule::ForLoopWrites) { refurb::rules::for_loop_writes_stmt(checker, for_stmt); } } - if checker.enabled(Rule::NeedlessElse) { + if checker.is_rule_enabled(Rule::NeedlessElse) { ruff::rules::needless_else(checker, for_stmt.into()); } } @@ -1395,152 +1340,138 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { .. }, ) => { - if checker.enabled(Rule::TooManyNestedBlocks) { + if checker.is_rule_enabled(Rule::TooManyNestedBlocks) { pylint::rules::too_many_nested_blocks(checker, stmt); } - if checker.enabled(Rule::JumpStatementInFinally) { + if checker.is_rule_enabled(Rule::JumpStatementInFinally) { flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody); } - if checker.enabled(Rule::ContinueInFinally) { + if checker.is_rule_enabled(Rule::ContinueInFinally) { if checker.target_version() <= PythonVersion::PY38 { pylint::rules::continue_in_finally(checker, finalbody); } } - if checker.enabled(Rule::DefaultExceptNotLast) { - if let Some(diagnostic) = - pyflakes::rules::default_except_not_last(handlers, checker.locator) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::DefaultExceptNotLast) { + pyflakes::rules::default_except_not_last(checker, handlers, checker.locator); } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::DuplicateHandlerException, Rule::DuplicateTryBlockException, ]) { flake8_bugbear::rules::duplicate_exceptions(checker, handlers); } - if checker.enabled(Rule::RedundantTupleInExceptionHandler) { + if checker.is_rule_enabled(Rule::RedundantTupleInExceptionHandler) { flake8_bugbear::rules::redundant_tuple_in_exception_handler(checker, handlers); } - if checker.enabled(Rule::OSErrorAlias) { + if checker.is_rule_enabled(Rule::OSErrorAlias) { pyupgrade::rules::os_error_alias_handlers(checker, handlers); } - if checker.enabled(Rule::TimeoutErrorAlias) { + if checker.is_rule_enabled(Rule::TimeoutErrorAlias) { if checker.target_version() >= PythonVersion::PY310 { pyupgrade::rules::timeout_error_alias_handlers(checker, handlers); } } - if checker.enabled(Rule::PytestAssertInExcept) { + if checker.is_rule_enabled(Rule::PytestAssertInExcept) { flake8_pytest_style::rules::assert_in_exception_handler(checker, handlers); } - if checker.enabled(Rule::SuppressibleException) { + if checker.is_rule_enabled(Rule::SuppressibleException) { flake8_simplify::rules::suppressible_exception( checker, stmt, body, handlers, orelse, finalbody, ); } - if checker.enabled(Rule::ReturnInTryExceptFinally) { + if checker.is_rule_enabled(Rule::ReturnInTryExceptFinally) { flake8_simplify::rules::return_in_try_except_finally( checker, body, handlers, finalbody, ); } - if checker.enabled(Rule::TryConsiderElse) { + if checker.is_rule_enabled(Rule::TryConsiderElse) { tryceratops::rules::try_consider_else(checker, body, orelse, handlers); } - if checker.enabled(Rule::VerboseRaise) { + if checker.is_rule_enabled(Rule::VerboseRaise) { tryceratops::rules::verbose_raise(checker, handlers); } - if checker.enabled(Rule::VerboseLogMessage) { + if checker.is_rule_enabled(Rule::VerboseLogMessage) { tryceratops::rules::verbose_log_message(checker, handlers); } - if checker.enabled(Rule::RaiseWithinTry) { + if checker.is_rule_enabled(Rule::RaiseWithinTry) { tryceratops::rules::raise_within_try(checker, body, handlers); } - if checker.enabled(Rule::UselessTryExcept) { + if checker.is_rule_enabled(Rule::UselessTryExcept) { tryceratops::rules::useless_try_except(checker, handlers); } - if checker.enabled(Rule::ErrorInsteadOfException) { + if checker.is_rule_enabled(Rule::ErrorInsteadOfException) { tryceratops::rules::error_instead_of_exception(checker, handlers); } - if checker.enabled(Rule::NeedlessElse) { + if checker.is_rule_enabled(Rule::NeedlessElse) { ruff::rules::needless_else(checker, try_stmt.into()); } } Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => { - if checker.enabled(Rule::SelfOrClsAssignment) { + if checker.is_rule_enabled(Rule::SelfOrClsAssignment) { for target in targets { pylint::rules::self_or_cls_assignment(checker, target); } } - if checker.enabled(Rule::RedeclaredAssignedName) { + if checker.is_rule_enabled(Rule::RedeclaredAssignedName) { pylint::rules::redeclared_assigned_name(checker, targets); } - if checker.enabled(Rule::LambdaAssignment) { + if checker.is_rule_enabled(Rule::LambdaAssignment) { if let [target] = &targets[..] { pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt); } } - if checker.enabled(Rule::AssignmentToOsEnviron) { + if checker.is_rule_enabled(Rule::AssignmentToOsEnviron) { flake8_bugbear::rules::assignment_to_os_environ(checker, targets); } - if checker.enabled(Rule::HardcodedPasswordString) { + if checker.is_rule_enabled(Rule::HardcodedPasswordString) { flake8_bandit::rules::assign_hardcoded_password_string(checker, value, targets); } - if checker.enabled(Rule::GlobalStatement) { + if checker.is_rule_enabled(Rule::GlobalStatement) { for target in targets { if let Expr::Name(ast::ExprName { id, .. }) = target { pylint::rules::global_statement(checker, id); } } } - if checker.enabled(Rule::UselessMetaclassType) { + if checker.is_rule_enabled(Rule::UselessMetaclassType) { pyupgrade::rules::useless_metaclass_type(checker, stmt, value, targets); } - if checker.enabled(Rule::ConvertTypedDictFunctionalToClass) { + if checker.is_rule_enabled(Rule::ConvertTypedDictFunctionalToClass) { pyupgrade::rules::convert_typed_dict_functional_to_class( checker, stmt, targets, value, ); } - if checker.enabled(Rule::ConvertNamedTupleFunctionalToClass) { + if checker.is_rule_enabled(Rule::ConvertNamedTupleFunctionalToClass) { pyupgrade::rules::convert_named_tuple_functional_to_class( checker, stmt, targets, value, ); } - if checker.enabled(Rule::PandasDfVariableName) { - if let Some(diagnostic) = pandas_vet::rules::assignment_to_df(targets) { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::PandasDfVariableName) { + pandas_vet::rules::assignment_to_df(checker, targets); } - if checker - .settings - .rules - .enabled(Rule::AirflowVariableNameTaskIdMismatch) - { + if checker.is_rule_enabled(Rule::AirflowVariableNameTaskIdMismatch) { airflow::rules::variable_name_task_id(checker, targets, value); } - if checker.settings.rules.enabled(Rule::SelfAssigningVariable) { + if checker.is_rule_enabled(Rule::SelfAssigningVariable) { pylint::rules::self_assignment(checker, assign); } - if checker.settings.rules.enabled(Rule::TypeParamNameMismatch) { + if checker.is_rule_enabled(Rule::TypeParamNameMismatch) { pylint::rules::type_param_name_mismatch(checker, value, targets); } - if checker - .settings - .rules - .enabled(Rule::TypeNameIncorrectVariance) - { + if checker.is_rule_enabled(Rule::TypeNameIncorrectVariance) { pylint::rules::type_name_incorrect_variance(checker, value); } - if checker.settings.rules.enabled(Rule::TypeBivariance) { + if checker.is_rule_enabled(Rule::TypeBivariance) { pylint::rules::type_bivariance(checker, value); } - if checker.enabled(Rule::NonAugmentedAssignment) { + if checker.is_rule_enabled(Rule::NonAugmentedAssignment) { pylint::rules::non_augmented_assignment(checker, assign); } - if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + if checker.is_rule_enabled(Rule::UnsortedDunderAll) { ruff::rules::sort_dunder_all_assign(checker, assign); } if checker.source_type.is_stub() { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::UnprefixedTypeParam, Rule::AssignmentDefaultInStub, Rule::UnannotatedAssignmentInStub, @@ -1553,21 +1484,21 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { .current_scopes() .any(|scope| scope.kind.is_function()) { - if checker.enabled(Rule::UnprefixedTypeParam) { + if checker.is_rule_enabled(Rule::UnprefixedTypeParam) { flake8_pyi::rules::prefix_type_params(checker, value, targets); } - if checker.enabled(Rule::AssignmentDefaultInStub) { + if checker.is_rule_enabled(Rule::AssignmentDefaultInStub) { flake8_pyi::rules::assignment_default_in_stub(checker, targets, value); } - if checker.enabled(Rule::UnannotatedAssignmentInStub) { + if checker.is_rule_enabled(Rule::UnannotatedAssignmentInStub) { flake8_pyi::rules::unannotated_assignment_in_stub( checker, targets, value, ); } - if checker.enabled(Rule::ComplexAssignmentInStub) { + if checker.is_rule_enabled(Rule::ComplexAssignmentInStub) { flake8_pyi::rules::complex_assignment_in_stub(checker, assign); } - if checker.enabled(Rule::TypeAliasWithoutAnnotation) { + if checker.is_rule_enabled(Rule::TypeAliasWithoutAnnotation) { flake8_pyi::rules::type_alias_without_annotation( checker, value, targets, ); @@ -1575,10 +1506,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } - if checker.enabled(Rule::ListReverseCopy) { + if checker.is_rule_enabled(Rule::ListReverseCopy) { refurb::rules::list_assign_reversed(checker, assign); } - if checker.enabled(Rule::NonPEP695TypeAlias) { + if checker.is_rule_enabled(Rule::NonPEP695TypeAlias) { pyupgrade::rules::non_pep695_type_alias_type(checker, assign); } } @@ -1591,7 +1522,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { }, ) => { if let Some(value) = value { - if checker.enabled(Rule::LambdaAssignment) { + if checker.is_rule_enabled(Rule::LambdaAssignment) { pycodestyle::rules::lambda_assignment( checker, target, @@ -1601,13 +1532,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ); } } - if checker.enabled(Rule::SelfOrClsAssignment) { + if checker.is_rule_enabled(Rule::SelfOrClsAssignment) { pylint::rules::self_or_cls_assignment(checker, target); } - if checker.enabled(Rule::SelfAssigningVariable) { + if checker.is_rule_enabled(Rule::SelfAssigningVariable) { pylint::rules::self_annotated_assignment(checker, assign_stmt); } - if checker.enabled(Rule::UnintentionalTypeAnnotation) { + if checker.is_rule_enabled(Rule::UnintentionalTypeAnnotation) { flake8_bugbear::rules::unintentional_type_annotation( checker, target, @@ -1615,10 +1546,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { stmt, ); } - if checker.enabled(Rule::NonPEP695TypeAlias) { + if checker.is_rule_enabled(Rule::NonPEP695TypeAlias) { pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt); } - if checker.enabled(Rule::HardcodedPasswordString) { + if checker.is_rule_enabled(Rule::HardcodedPasswordString) { if let Some(value) = value.as_deref() { flake8_bandit::rules::assign_hardcoded_password_string( checker, @@ -1627,12 +1558,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { ); } } - if checker.settings.rules.enabled(Rule::UnsortedDunderAll) { + if checker.is_rule_enabled(Rule::UnsortedDunderAll) { ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt); } if checker.source_type.is_stub() { if let Some(value) = value { - if checker.enabled(Rule::AssignmentDefaultInStub) { + if checker.is_rule_enabled(Rule::AssignmentDefaultInStub) { // Ignore assignments in function bodies; those are covered by other rules. if !checker .semantic @@ -1645,7 +1576,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } } else { - if checker.enabled(Rule::UnassignedSpecialVariableInStub) { + if checker.is_rule_enabled(Rule::UnassignedSpecialVariableInStub) { flake8_pyi::rules::unassigned_special_variable_in_stub( checker, target, stmt, ); @@ -1653,65 +1584,73 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } if checker.semantic.match_typing_expr(annotation, "TypeAlias") { - if checker.enabled(Rule::SnakeCaseTypeAlias) { + if checker.is_rule_enabled(Rule::SnakeCaseTypeAlias) { flake8_pyi::rules::snake_case_type_alias(checker, target); } - if checker.enabled(Rule::TSuffixedTypeAlias) { + if checker.is_rule_enabled(Rule::TSuffixedTypeAlias) { flake8_pyi::rules::t_suffixed_type_alias(checker, target); } } else if checker .semantic .match_typing_expr(helpers::map_subscript(annotation), "Final") { - if checker.enabled(Rule::RedundantFinalLiteral) { + if checker.is_rule_enabled(Rule::RedundantFinalLiteral) { flake8_pyi::rules::redundant_final_literal(checker, assign_stmt); } } } Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => { - if checker.enabled(Rule::SnakeCaseTypeAlias) { + if checker.is_rule_enabled(Rule::SnakeCaseTypeAlias) { flake8_pyi::rules::snake_case_type_alias(checker, name); } - if checker.enabled(Rule::TSuffixedTypeAlias) { + if checker.is_rule_enabled(Rule::TSuffixedTypeAlias) { flake8_pyi::rules::t_suffixed_type_alias(checker, name); } } - Stmt::Delete(delete @ ast::StmtDelete { targets, range: _ }) => { - if checker.enabled(Rule::GlobalStatement) { + Stmt::Delete( + delete @ ast::StmtDelete { + targets, + range: _, + node_index: _, + }, + ) => { + if checker.is_rule_enabled(Rule::GlobalStatement) { for target in targets { if let Expr::Name(ast::ExprName { id, .. }) = target { pylint::rules::global_statement(checker, id); } } } - if checker.enabled(Rule::DeleteFullSlice) { + if checker.is_rule_enabled(Rule::DeleteFullSlice) { refurb::rules::delete_full_slice(checker, delete); } } - Stmt::Expr(expr @ ast::StmtExpr { value, range: _ }) => { - if checker.enabled(Rule::UselessComparison) { + Stmt::Expr( + expr @ ast::StmtExpr { + value, + range: _, + node_index: _, + }, + ) => { + if checker.is_rule_enabled(Rule::UselessComparison) { flake8_bugbear::rules::useless_comparison(checker, value); } - if checker.enabled(Rule::UselessExpression) { + if checker.is_rule_enabled(Rule::UselessExpression) { flake8_bugbear::rules::useless_expression(checker, value); } - if checker.enabled(Rule::InvalidMockAccess) { + if checker.is_rule_enabled(Rule::InvalidMockAccess) { pygrep_hooks::rules::uncalled_mock_method(checker, value); } - if checker.enabled(Rule::NamedExprWithoutContext) { + if checker.is_rule_enabled(Rule::NamedExprWithoutContext) { pylint::rules::named_expr_without_context(checker, value); } - if checker.enabled(Rule::AsyncioDanglingTask) { - if let Some(diagnostic) = - ruff::rules::asyncio_dangling_task(value, checker.semantic()) - { - checker.report_diagnostic(diagnostic); - } + if checker.is_rule_enabled(Rule::AsyncioDanglingTask) { + ruff::rules::asyncio_dangling_task(checker, value, checker.semantic()); } - if checker.enabled(Rule::RepeatedAppend) { + if checker.is_rule_enabled(Rule::RepeatedAppend) { refurb::rules::repeated_append(checker, stmt); } - if checker.enabled(Rule::UselessExceptionStatement) { + if checker.is_rule_enabled(Rule::UselessExceptionStatement) { pylint::rules::useless_exception_statement(checker, expr); } } @@ -1719,8 +1658,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { subject: _, cases, range: _, + node_index: _, }) => { - if checker.enabled(Rule::NanComparison) { + if checker.is_rule_enabled(Rule::NanComparison) { pylint::rules::nan_comparison_match(checker, cases); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/string_like.rs b/crates/ruff_linter/src/checkers/ast/analyze/string_like.rs index acd25d9fc73c8..f28e60d92824a 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/string_like.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/string_like.rs @@ -6,37 +6,39 @@ use crate::rules::{flake8_bandit, flake8_pyi, flake8_quotes, pycodestyle, ruff}; /// Run lint rules over a [`StringLike`] syntax nodes. pub(crate) fn string_like(string_like: StringLike, checker: &Checker) { - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::AmbiguousUnicodeCharacterString, Rule::AmbiguousUnicodeCharacterDocstring, ]) { ruff::rules::ambiguous_unicode_character_string(checker, string_like); } - if checker.enabled(Rule::HardcodedBindAllInterfaces) { + if checker.is_rule_enabled(Rule::HardcodedBindAllInterfaces) { flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like); } - if checker.enabled(Rule::HardcodedTempFile) { + if checker.is_rule_enabled(Rule::HardcodedTempFile) { flake8_bandit::rules::hardcoded_tmp_directory(checker, string_like); } if checker.source_type.is_stub() { - if checker.enabled(Rule::StringOrBytesTooLong) { + if checker.is_rule_enabled(Rule::StringOrBytesTooLong) { flake8_pyi::rules::string_or_bytes_too_long(checker, string_like); } } - if checker.any_enabled(&[ + if checker.any_rule_enabled(&[ Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString, Rule::BadQuotesDocstring, ]) { flake8_quotes::rules::check_string_quotes(checker, string_like); } - if checker.enabled(Rule::UnnecessaryEscapedQuote) { + if checker.is_rule_enabled(Rule::UnnecessaryEscapedQuote) { flake8_quotes::rules::unnecessary_escaped_quote(checker, string_like); } - if checker.enabled(Rule::AvoidableEscapedQuote) && checker.settings.flake8_quotes.avoid_escape { + if checker.is_rule_enabled(Rule::AvoidableEscapedQuote) + && checker.settings().flake8_quotes.avoid_escape + { flake8_quotes::rules::avoidable_escaped_quote(checker, string_like); } - if checker.enabled(Rule::InvalidEscapeSequence) { + if checker.is_rule_enabled(Rule::InvalidEscapeSequence) { pycodestyle::rules::invalid_escape_sequence(checker, string_like); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/suite.rs b/crates/ruff_linter/src/checkers/ast/analyze/suite.rs index f5b56f1081ae9..503a864259471 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/suite.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/suite.rs @@ -7,10 +7,10 @@ use crate::rules::refurb; /// Run lint rules over a suite of [`Stmt`] syntax nodes. pub(crate) fn suite(suite: &[Stmt], checker: &Checker) { - if checker.enabled(Rule::UnnecessaryPlaceholder) { + if checker.is_rule_enabled(Rule::UnnecessaryPlaceholder) { flake8_pie::rules::unnecessary_placeholder(checker, suite); } - if checker.enabled(Rule::RepeatedGlobal) { + if checker.is_rule_enabled(Rule::RepeatedGlobal) { refurb::rules::repeated_global(checker, suite); } } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs b/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs index dbe2c540ef4d8..10ea8514578df 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/unresolved_references.rs @@ -1,4 +1,3 @@ -use ruff_diagnostics::Diagnostic; use ruff_python_semantic::Exceptions; use ruff_python_stdlib::builtins::version_builtin_was_added; @@ -8,22 +7,22 @@ use crate::rules::pyflakes; /// Run lint rules over all [`UnresolvedReference`] entities in the [`SemanticModel`]. pub(crate) fn unresolved_references(checker: &Checker) { - if !checker.any_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) { + if !checker.any_rule_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) { return; } for reference in checker.semantic.unresolved_references() { if reference.is_wildcard_import() { - if checker.enabled(Rule::UndefinedLocalWithImportStarUsage) { - checker.report_diagnostic(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: reference.name(checker.source()).to_string(), - }, - reference.range(), - )); - } + // F406 + checker.report_diagnostic_if_enabled( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: reference.name(checker.source()).to_string(), + }, + reference.range(), + ); } else { - if checker.enabled(Rule::UndefinedName) { + // F821 + if checker.is_rule_enabled(Rule::UndefinedName) { if checker.semantic.in_no_type_check() { continue; } @@ -42,13 +41,13 @@ pub(crate) fn unresolved_references(checker: &Checker) { let symbol_name = reference.name(checker.source()); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( pyflakes::rules::UndefinedName { name: symbol_name.to_string(), minor_version_builtin_added: version_builtin_was_added(symbol_name), }, reference.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/checkers/ast/annotation.rs b/crates/ruff_linter/src/checkers/ast/annotation.rs index 86d1ba50f8d81..d8d516cc92107 100644 --- a/crates/ruff_linter/src/checkers/ast/annotation.rs +++ b/crates/ruff_linter/src/checkers/ast/annotation.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::StmtFunctionDef; +use ruff_python_ast::{PythonVersion, StmtFunctionDef}; use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::rules::flake8_type_checking; @@ -29,7 +29,11 @@ pub(super) enum AnnotationContext { impl AnnotationContext { /// Determine the [`AnnotationContext`] for an annotation based on the current scope of the /// semantic model. - pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self { + pub(super) fn from_model( + semantic: &SemanticModel, + settings: &LinterSettings, + version: PythonVersion, + ) -> Self { // If the annotation is in a class scope (e.g., an annotated assignment for a // class field) or a function scope, and that class or function is marked as // runtime-required, treat the annotation as runtime-required. @@ -42,7 +46,7 @@ impl AnnotationContext { semantic, ) => { - return Self::RuntimeRequired + return Self::RuntimeRequired; } ScopeKind::Function(function_def) if flake8_type_checking::helpers::runtime_required_function( @@ -51,7 +55,7 @@ impl AnnotationContext { semantic, ) => { - return Self::RuntimeRequired + return Self::RuntimeRequired; } _ => {} } @@ -59,7 +63,7 @@ impl AnnotationContext { // If `__future__` annotations are enabled or it's a stub file, // then annotations are never evaluated at runtime, // so we can treat them as typing-only. - if semantic.future_annotations_or_stub() { + if semantic.future_annotations_or_stub() || version.defers_annotations() { return Self::TypingOnly; } @@ -81,6 +85,7 @@ impl AnnotationContext { function_def: &StmtFunctionDef, semantic: &SemanticModel, settings: &LinterSettings, + version: PythonVersion, ) -> Self { if flake8_type_checking::helpers::runtime_required_function( function_def, @@ -88,7 +93,7 @@ impl AnnotationContext { semantic, ) { Self::RuntimeRequired - } else if semantic.future_annotations_or_stub() { + } else if semantic.future_annotations_or_stub() || version.defers_annotations() { Self::TypingOnly } else { Self::RuntimeEvaluated diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index ebff124383a04..0fc24dc8e2a6b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -26,27 +26,28 @@ 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, Edit, IsolationLevel}; +use ruff_db::diagnostic::Diagnostic; +use ruff_diagnostics::{Applicability, Fix, IsolationLevel}; use ruff_notebook::{CellOffsets, NotebookIndex}; use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::str::Quote; -use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor}; +use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern}; use ruff_python_ast::{ self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr, - ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern, - PythonVersion, Stmt, Suite, UnaryOp, + ExprContext, ExprFString, ExprTString, InterpolatedStringElement, Keyword, MatchCase, + ModModule, Parameter, Parameters, Pattern, PythonVersion, Stmt, Suite, UnaryOp, }; -use ruff_python_ast::{helpers, str, visitor, PySourceType}; +use ruff_python_ast::{PySourceType, helpers, str, visitor}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_index::Indexer; -use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind, ParsedAnnotation}; +use ruff_python_parser::semantic_errors::{ + SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, +}; +use ruff_python_parser::typing::{AnnotationKind, ParsedAnnotation, parse_type_annotation}; use ruff_python_parser::{ParseError, Parsed, Tokens}; use ruff_python_semantic::all::{DunderAllDefinition, DunderAllFlags}; use ruff_python_semantic::analyze::{imports, typing}; @@ -55,9 +56,9 @@ use ruff_python_semantic::{ Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, }; -use ruff_python_stdlib::builtins::{python_builtins, MAGIC_GLOBALS}; +use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins}; use ruff_python_trivia::CommentRanges; -use ruff_source_file::{OneIndexed, SourceRow}; +use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::annotation::AnnotationContext; @@ -65,14 +66,17 @@ use crate::docstrings::extraction::ExtractionTarget; use crate::importer::{ImportRequest, Importer, ResolutionError}; use crate::noqa::NoqaMapping; use crate::package::PackageRoot; +use crate::preview::is_undefined_export_in_dunder_init_enabled; use crate::registry::Rule; use crate::rules::pyflakes::rules::{ LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction, }; use crate::rules::pylint::rules::{AwaitOutsideAsync, LoadBeforeGlobalDeclaration}; use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade}; -use crate::settings::{flags, LinterSettings}; -use crate::{docstrings, noqa, Locator}; +use crate::settings::rule_table::RuleTable; +use crate::settings::{LinterSettings, TargetVersion, flags}; +use crate::{Edit, Violation}; +use crate::{Locator, docstrings, noqa}; mod analyze; mod annotation; @@ -204,8 +208,6 @@ pub(crate) struct Checker<'a> { /// The [`NoqaMapping`] for the current analysis (i.e., the mapping from line number to /// suppression commented line number). noqa_line_for: &'a NoqaMapping, - /// The [`LinterSettings`] for the current analysis, including the enabled rules. - pub(crate) settings: &'a LinterSettings, /// The [`Locator`] for the current file, which enables extraction of source code from byte /// offsets. locator: &'a Locator<'a>, @@ -222,8 +224,6 @@ pub(crate) struct Checker<'a> { visit: deferred::Visit<'a>, /// A set of deferred nodes to be analyzed after the AST traversal (e.g., `for` loops). analyze: deferred::Analyze, - /// The cumulative set of diagnostics computed across all lint rules. - diagnostics: RefCell>, /// The list of names already seen by flake8-bugbear diagnostics, to avoid duplicate violations. flake8_bugbear_seen: RefCell>, /// The end offset of the last visited statement. @@ -231,16 +231,17 @@ pub(crate) struct Checker<'a> { /// A state describing if a docstring is expected or not. docstring_state: DocstringState, /// The target [`PythonVersion`] for version-dependent checks. - target_version: PythonVersion, + target_version: TargetVersion, /// Helper visitor for detecting semantic syntax errors. - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] semantic_checker: SemanticSyntaxChecker, /// Errors collected by the `semantic_checker`. semantic_errors: RefCell>, + context: &'a LintContext<'a>, } impl<'a> Checker<'a> { - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub(crate) fn new( parsed: &'a Parsed, parsed_annotations_arena: &'a typed_arena::Arena>, @@ -256,14 +257,14 @@ impl<'a> Checker<'a> { source_type: PySourceType, cell_offsets: Option<&'a CellOffsets>, notebook_index: Option<&'a NotebookIndex>, - target_version: PythonVersion, - ) -> Checker<'a> { + target_version: TargetVersion, + context: &'a LintContext<'a>, + ) -> Self { let semantic = SemanticModel::new(&settings.typing_modules, path, module); Self { parsed, parsed_type_annotation: None, parsed_annotations_cache: ParsedAnnotationsCache::new(parsed_annotations_arena), - settings, noqa_line_for, noqa, path, @@ -277,7 +278,6 @@ impl<'a> Checker<'a> { semantic, visit: deferred::Visit::default(), analyze: deferred::Analyze::default(), - diagnostics: RefCell::default(), flake8_bugbear_seen: RefCell::default(), cell_offsets, notebook_index, @@ -286,6 +286,7 @@ impl<'a> Checker<'a> { target_version, semantic_checker: SemanticSyntaxChecker::new(), semantic_errors: RefCell::default(), + context, } } } @@ -321,7 +322,8 @@ impl<'a> Checker<'a> { /// Return the preferred quote for a generated `StringLiteral` node, given where we are in the /// AST. fn preferred_quote(&self) -> Quote { - self.f_string_quote_style().unwrap_or(self.stylist.quote()) + self.interpolated_string_quote_style() + .unwrap_or(self.stylist.quote()) } /// Return the default string flags a generated `StringLiteral` node should use, given where we @@ -336,32 +338,39 @@ impl<'a> Checker<'a> { ast::BytesLiteralFlags::empty().with_quote_style(self.preferred_quote()) } + // TODO(dylan) add similar method for t-strings /// Return the default f-string flags a generated `FString` node should use, given where we are /// in the AST. pub(crate) fn default_fstring_flags(&self) -> ast::FStringFlags { ast::FStringFlags::empty().with_quote_style(self.preferred_quote()) } - /// Returns the appropriate quoting for f-string by reversing the one used outside of - /// the f-string. + /// Returns the appropriate quoting for interpolated strings by reversing the one used outside of + /// the interpolated string. /// - /// If the current expression in the context is not an f-string, returns ``None``. - pub(crate) fn f_string_quote_style(&self) -> Option { - if !self.semantic.in_f_string() { + /// If the current expression in the context is not an interpolated string, returns ``None``. + pub(crate) fn interpolated_string_quote_style(&self) -> Option { + if !self.semantic.in_interpolated_string() { return None; } - // Find the quote character used to start the containing f-string. - let ast::ExprFString { value, .. } = self - .semantic + // Find the quote character used to start the containing interpolated string. + self.semantic .current_expressions() - .find_map(|expr| expr.as_f_string_expr())?; - Some(value.iter().next()?.quote_style().opposite()) + .find_map(|expr| match expr { + Expr::FString(ExprFString { value, .. }) => { + Some(value.iter().next()?.quote_style().opposite()) + } + Expr::TString(ExprTString { value, .. }) => { + Some(value.iter().next()?.quote_style().opposite()) + } + _ => None, + }) } /// Returns the [`SourceRow`] for the given offset. pub(crate) fn compute_source_row(&self, offset: TextSize) -> SourceRow { - #[allow(deprecated)] + #[expect(deprecated)] let line = self.locator.compute_line_index(offset); if let Some(notebook_index) = self.notebook_index { @@ -378,19 +387,29 @@ impl<'a> Checker<'a> { self.indexer.comment_ranges() } - /// Push a new [`Diagnostic`] to the collection in the [`Checker`] - pub(crate) fn report_diagnostic(&self, diagnostic: Diagnostic) { - let mut diagnostics = self.diagnostics.borrow_mut(); - diagnostics.push(diagnostic); + /// Return a [`DiagnosticGuard`] for reporting a diagnostic. + /// + /// The guard derefs to a [`Diagnostic`], so it can be used to further modify the diagnostic + /// before it is added to the collection in the checker on `Drop`. + pub(crate) fn report_diagnostic<'chk, T: Violation>( + &'chk self, + kind: T, + range: TextRange, + ) -> DiagnosticGuard<'chk, 'a> { + self.context.report_diagnostic(kind, range) } - /// Extend the collection of [`Diagnostic`] objects in the [`Checker`] - pub(crate) fn report_diagnostics(&self, diagnostics: I) - where - I: IntoIterator, - { - let mut checker_diagnostics = self.diagnostics.borrow_mut(); - checker_diagnostics.extend(diagnostics); + /// Return a [`DiagnosticGuard`] for reporting a diagnostic if the corresponding rule is + /// enabled. + /// + /// The guard derefs to a [`Diagnostic`], so it can be used to further modify the diagnostic + /// before it is added to the collection in the checker on `Drop`. + pub(crate) fn report_diagnostic_if_enabled<'chk, T: Violation>( + &'chk self, + kind: T, + range: TextRange, + ) -> Option> { + self.context.report_diagnostic_if_enabled(kind, range) } /// Adds a [`TextRange`] to the set of ranges of variable names @@ -443,6 +462,11 @@ impl<'a> Checker<'a> { &self.semantic } + /// The [`LinterSettings`] for the current analysis, including the enabled rules. + pub(crate) const fn settings(&self) -> &'a LinterSettings { + self.context.settings + } + /// The [`Path`] to the file under analysis. pub(crate) const fn path(&self) -> &'a Path { self.path @@ -460,14 +484,14 @@ impl<'a> Checker<'a> { /// Returns whether the given rule should be checked. #[inline] - pub(crate) const fn enabled(&self, rule: Rule) -> bool { - self.settings.rules.enabled(rule) + pub(crate) const fn is_rule_enabled(&self, rule: Rule) -> bool { + self.context.is_rule_enabled(rule) } /// Returns whether any of the given rules should be checked. #[inline] - pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool { - self.settings.rules.any_enabled(rules) + pub(crate) const fn any_rule_enabled(&self, rules: &[Rule]) -> bool { + self.context.any_rule_enabled(rules) } /// Returns the [`IsolationLevel`] to isolate fixes for a given node. @@ -516,15 +540,22 @@ impl<'a> Checker<'a> { } /// Push `diagnostic` if the checker is not in a `@no_type_check` context. - pub(crate) fn report_type_diagnostic(&self, diagnostic: Diagnostic) { + pub(crate) fn report_type_diagnostic(&self, kind: T, range: TextRange) { if !self.semantic.in_no_type_check() { - self.report_diagnostic(diagnostic); + self.report_diagnostic(kind, range); } } - /// Return the [`PythonVersion`] to use for version-related checks. - pub(crate) const fn target_version(&self) -> PythonVersion { - self.target_version + /// Return the [`PythonVersion`] to use for version-related lint rules. + /// + /// If the user did not provide a target version, this defaults to the lowest supported Python + /// version ([`PythonVersion::default`]). + /// + /// Note that this method should not be used for version-related syntax errors emitted by the + /// parser or the [`SemanticSyntaxChecker`], which should instead default to the _latest_ + /// supported Python version. + pub(crate) fn target_version(&self) -> PythonVersion { + self.target_version.linter_version() } fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker)) { @@ -533,36 +564,59 @@ impl<'a> Checker<'a> { self.semantic_checker = checker; } - /// Attempt to create an [`Edit`] that imports `member`. + /// Create a [`TypingImporter`] that will import `member` from either `typing` or + /// `typing_extensions`. /// /// On Python <`version_added_to_typing`, `member` is imported from `typing_extensions`, while /// on Python >=`version_added_to_typing`, it is imported from `typing`. /// - /// See [`Importer::get_or_import_symbol`] for more details on the returned values. - pub(crate) fn import_from_typing( - &self, - member: &str, - position: TextSize, + /// If the Python version is less than `version_added_to_typing` but + /// `LinterSettings::typing_extensions` is `false`, this method returns `None`. + pub(crate) fn typing_importer<'b>( + &'b self, + member: &'b str, version_added_to_typing: PythonVersion, - ) -> Result<(Edit, String), ResolutionError> { + ) -> Option> { let source_module = if self.target_version() >= version_added_to_typing { "typing" + } else if !self.settings().typing_extensions { + return None; } else { "typing_extensions" }; - let request = ImportRequest::import_from(source_module, member); - self.importer() - .get_or_import_symbol(&request, position, self.semantic()) + Some(TypingImporter { + checker: self, + source_module, + member, + }) } } -impl SemanticSyntaxContext for Checker<'_> { - fn seen_docstring_boundary(&self) -> bool { - self.semantic.seen_module_docstring_boundary() +pub(crate) struct TypingImporter<'a, 'b> { + checker: &'a Checker<'b>, + source_module: &'static str, + member: &'a str, +} + +impl TypingImporter<'_, '_> { + /// Create an [`Edit`] that makes the requested symbol available at `position`. + /// + /// See [`Importer::get_or_import_symbol`] for more details on the returned values and + /// [`Checker::typing_importer`] for a way to construct a [`TypingImporter`]. + pub(crate) fn import(&self, position: TextSize) -> Result<(Edit, String), ResolutionError> { + let request = ImportRequest::import_from(self.source_module, self.member); + self.checker + .importer + .get_or_import_symbol(&request, position, self.checker.semantic()) } +} +impl SemanticSyntaxContext for Checker<'_> { fn python_version(&self) -> PythonVersion { - self.target_version + // Reuse `parser_version` here, which should default to `PythonVersion::latest` instead of + // `PythonVersion::default` to minimize version-related semantic syntax errors when + // `target_version` is unset. + self.target_version.parser_version() } fn global(&self, name: &str) -> Option { @@ -572,41 +626,36 @@ impl SemanticSyntaxContext for Checker<'_> { 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)); + // F404 + if self.is_rule_enabled(Rule::LateFutureImport) { + self.report_diagnostic(LateFutureImport, error.range); } } SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { name, start } => { - if self - .settings - .rules - .enabled(Rule::LoadBeforeGlobalDeclaration) - { - self.report_diagnostic(Diagnostic::new( + if self.is_rule_enabled(Rule::LoadBeforeGlobalDeclaration) { + self.report_diagnostic( LoadBeforeGlobalDeclaration { name, row: self.compute_source_row(start), }, error.range, - )); + ); } } SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => { - if self.settings.rules.enabled(Rule::YieldOutsideFunction) { - self.report_diagnostic(Diagnostic::new( - YieldOutsideFunction::new(kind), - error.range, - )); + if self.is_rule_enabled(Rule::YieldOutsideFunction) { + self.report_diagnostic(YieldOutsideFunction::new(kind), error.range); } } SemanticSyntaxErrorKind::ReturnOutsideFunction => { - if self.settings.rules.enabled(Rule::ReturnOutsideFunction) { - self.report_diagnostic(Diagnostic::new(ReturnOutsideFunction, error.range)); + // F706 + if self.is_rule_enabled(Rule::ReturnOutsideFunction) { + self.report_diagnostic(ReturnOutsideFunction, error.range); } } SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(_) => { - if self.settings.rules.enabled(Rule::AwaitOutsideAsync) { - self.report_diagnostic(Diagnostic::new(AwaitOutsideAsync, error.range)); + if self.is_rule_enabled(Rule::AwaitOutsideAsync) { + self.report_diagnostic(AwaitOutsideAsync, error.range); } } SemanticSyntaxErrorKind::ReboundComprehensionVariable @@ -619,10 +668,10 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression - | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) => { - if self.settings.preview.is_enabled() { - self.semantic_errors.borrow_mut().push(error); - } + | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_) + | SemanticSyntaxErrorKind::DuplicateParameter(_) + | SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { + self.semantic_errors.borrow_mut().push(error); } } } @@ -657,6 +706,17 @@ impl SemanticSyntaxContext for Checker<'_> { false } + fn in_yield_allowed_context(&self) -> bool { + for scope in self.semantic.current_scopes() { + match scope.kind { + ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false, + ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true, + ScopeKind::Module | ScopeKind::Type => {} + } + } + false + } + fn in_sync_comprehension(&self) -> bool { for scope in self.semantic.current_scopes() { if let ScopeKind::Generator { @@ -782,10 +842,15 @@ impl<'a> Visitor<'a> for Checker<'a> { op: _, value: _, range: _, + node_index: _, }) => { self.handle_node_load(target); } - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { if self.semantic.at_top_level() { self.importer.visit_import(stmt); } @@ -839,6 +904,7 @@ impl<'a> Visitor<'a> for Checker<'a> { module, level, range: _, + node_index: _, }) => { if self.semantic.at_top_level() { self.importer.visit_import(stmt); @@ -900,7 +966,11 @@ impl<'a> Visitor<'a> for Checker<'a> { } } } - Stmt::Global(ast::StmtGlobal { names, range: _ }) => { + Stmt::Global(ast::StmtGlobal { + names, + range: _, + node_index: _, + }) => { if !self.semantic.scope_id.is_global() { for name in names { let binding_id = self.semantic.global_scope().get(name); @@ -922,7 +992,11 @@ impl<'a> Visitor<'a> for Checker<'a> { } } } - Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { + Stmt::Nonlocal(ast::StmtNonlocal { + names, + range: _, + node_index: _, + }) => { if !self.semantic.scope_id.is_global() { for name in names { if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) { @@ -981,9 +1055,13 @@ impl<'a> Visitor<'a> for Checker<'a> { } // Function annotations are always evaluated at runtime, unless future annotations - // are enabled. - let annotation = - AnnotationContext::from_function(function_def, &self.semantic, self.settings); + // are enabled or the Python version is at least 3.14. + let annotation = AnnotationContext::from_function( + function_def, + &self.semantic, + self.settings(), + self.target_version(), + ); // The first parameter may be a single dispatch. let singledispatch = @@ -1128,6 +1206,7 @@ impl<'a> Visitor<'a> for Checker<'a> { } Stmt::TypeAlias(ast::StmtTypeAlias { range: _, + node_index: _, name, type_params, value, @@ -1180,7 +1259,11 @@ impl<'a> Visitor<'a> for Checker<'a> { value, .. }) => { - match AnnotationContext::from_model(&self.semantic, self.settings) { + match AnnotationContext::from_model( + &self.semantic, + self.settings(), + self.target_version(), + ) { AnnotationContext::RuntimeRequired => { self.visit_runtime_required_annotation(annotation); } @@ -1218,6 +1301,7 @@ impl<'a> Visitor<'a> for Checker<'a> { test, msg, range: _, + node_index: _, }) => { let snapshot = self.semantic.flags; self.semantic.flags |= SemanticModelFlags::ASSERT_STATEMENT; @@ -1232,6 +1316,7 @@ impl<'a> Visitor<'a> for Checker<'a> { body, is_async: _, range: _, + node_index: _, }) => { for item in items { self.visit_with_item(item); @@ -1245,6 +1330,7 @@ impl<'a> Visitor<'a> for Checker<'a> { body, orelse, range: _, + node_index: _, }) => { self.visit_boolean_test(test); self.visit_body(body); @@ -1256,6 +1342,7 @@ impl<'a> Visitor<'a> for Checker<'a> { body, elif_else_clauses, range: _, + node_index: _, }, ) => { self.visit_boolean_test(test); @@ -1335,7 +1422,7 @@ impl<'a> Visitor<'a> for Checker<'a> { // we can't defer again, or we'll infinitely recurse! && !self.semantic.in_deferred_type_definition() && self.semantic.in_type_definition() - && self.semantic.future_annotations_or_stub() + && (self.semantic.future_annotations_or_stub()||self.target_version().defers_annotations()) && (self.semantic.in_annotation() || self.source_type.is_stub()) { if let Expr::StringLiteral(string_literal) = expr { @@ -1375,15 +1462,27 @@ impl<'a> Visitor<'a> for Checker<'a> { func, arguments: _, range: _, + node_index: _, }) => { - if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { + if let Expr::Name(ast::ExprName { + id, + ctx, + range: _, + node_index: _, + }) = func.as_ref() + { if id == "locals" && ctx.is_load() { let scope = self.semantic.current_scope_mut(); scope.set_uses_locals(); } } } - Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx { + Expr::Name(ast::ExprName { + id, + ctx, + range: _, + node_index: _, + }) => match ctx { ExprContext::Load => self.handle_node_load(expr), ExprContext::Store => self.handle_node_store(id, expr), ExprContext::Del => self.handle_node_delete(expr), @@ -1398,6 +1497,7 @@ impl<'a> Visitor<'a> for Checker<'a> { elt, generators, range: _, + node_index: _, }) => { self.visit_generators(GeneratorKind::ListComprehension, generators); self.visit_expr(elt); @@ -1406,6 +1506,7 @@ impl<'a> Visitor<'a> for Checker<'a> { elt, generators, range: _, + node_index: _, }) => { self.visit_generators(GeneratorKind::SetComprehension, generators); self.visit_expr(elt); @@ -1414,6 +1515,7 @@ impl<'a> Visitor<'a> for Checker<'a> { elt, generators, range: _, + node_index: _, parenthesized: _, }) => { self.visit_generators(GeneratorKind::Generator, generators); @@ -1424,6 +1526,7 @@ impl<'a> Visitor<'a> for Checker<'a> { value, generators, range: _, + node_index: _, }) => { self.visit_generators(GeneratorKind::DictComprehension, generators); self.visit_expr(key); @@ -1434,6 +1537,7 @@ impl<'a> Visitor<'a> for Checker<'a> { parameters, body: _, range: _, + node_index: _, }, ) => { // Visit the default arguments, but avoid the body, which will be deferred. @@ -1455,6 +1559,7 @@ impl<'a> Visitor<'a> for Checker<'a> { body, orelse, range: _, + node_index: _, }) => { self.visit_boolean_test(test); self.visit_expr(body); @@ -1464,6 +1569,7 @@ impl<'a> Visitor<'a> for Checker<'a> { op: UnaryOp::Not, operand, range: _, + node_index: _, }) => { self.visit_boolean_test(operand); } @@ -1471,6 +1577,7 @@ impl<'a> Visitor<'a> for Checker<'a> { func, arguments, range: _, + node_index: _, }) => { self.visit_expr(func); @@ -1539,25 +1646,37 @@ impl<'a> Visitor<'a> for Checker<'a> { } } Some(typing::Callable::Cast) => { - let mut args = arguments.args.iter(); - if let Some(arg) = args.next() { - self.visit_type_definition(arg); - - if !self.source_type.is_stub() && self.enabled(Rule::RuntimeCastValue) { - flake8_type_checking::rules::runtime_cast_value(self, arg); + for (i, arg) in arguments.arguments_source_order().enumerate() { + match (i, arg) { + (0, ArgOrKeyword::Arg(arg)) => self.visit_cast_type_argument(arg), + (_, ArgOrKeyword::Arg(arg)) => self.visit_non_type_definition(arg), + (_, ArgOrKeyword::Keyword(Keyword { arg, value, .. })) => { + if let Some(id) = arg { + if id == "typ" { + self.visit_cast_type_argument(value); + } else { + self.visit_non_type_definition(value); + } + } + } } } - for arg in args { - self.visit_expr(arg); - } } Some(typing::Callable::NewType) => { - let mut args = arguments.args.iter(); - if let Some(arg) = args.next() { - self.visit_non_type_definition(arg); - } - for arg in args { - self.visit_type_definition(arg); + for (i, arg) in arguments.arguments_source_order().enumerate() { + match (i, arg) { + (1, ArgOrKeyword::Arg(arg)) => self.visit_type_definition(arg), + (_, ArgOrKeyword::Arg(arg)) => self.visit_non_type_definition(arg), + (_, ArgOrKeyword::Keyword(Keyword { arg, value, .. })) => { + if let Some(id) = arg { + if id == "tp" { + self.visit_type_definition(value); + } else { + self.visit_non_type_definition(value); + } + } + } + } } } Some(typing::Callable::TypeVar) => { @@ -1573,6 +1692,7 @@ impl<'a> Visitor<'a> for Checker<'a> { arg, value, range: _, + node_index: _, } = keyword; if let Some(id) = arg { if matches!(&**id, "bound" | "default") { @@ -1664,7 +1784,12 @@ impl<'a> Visitor<'a> for Checker<'a> { self.visit_non_type_definition(arg); } for arg in args { - if let Expr::Dict(ast::ExprDict { items, range: _ }) = arg { + if let Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) = arg + { for ast::DictItem { key, value } in items { if let Some(key) = key { self.visit_non_type_definition(key); @@ -1702,6 +1827,7 @@ impl<'a> Visitor<'a> for Checker<'a> { value, arg, range: _, + node_index: _, } = keyword; if arg.as_ref().is_some_and(|arg| arg == "type") { self.visit_type_definition(value); @@ -1730,6 +1856,7 @@ impl<'a> Visitor<'a> for Checker<'a> { slice, ctx, range: _, + node_index: _, }) => { // Only allow annotations in `ExprContext::Load`. If we have, e.g., // `obj["foo"]["bar"]`, we need to avoid treating the `obj["foo"]` @@ -1746,8 +1873,8 @@ impl<'a> Visitor<'a> for Checker<'a> { match typing::match_annotated_subscript( value, &self.semantic, - self.settings.typing_modules.iter().map(String::as_str), - &self.settings.pyflakes.extend_generics, + self.settings().typing_modules.iter().map(String::as_str), + &self.settings().pyflakes.extend_generics, ) { // Ex) Literal["Class"] Some(typing::SubscriptKind::Literal) => { @@ -1769,6 +1896,7 @@ impl<'a> Visitor<'a> for Checker<'a> { elts, ctx, range: _, + node_index: _, parenthesized: _, }) = slice.as_ref() { @@ -1793,7 +1921,12 @@ impl<'a> Visitor<'a> for Checker<'a> { } } Some(typing::SubscriptKind::TypedDict) => { - if let Expr::Dict(ast::ExprDict { items, range: _ }) = slice.as_ref() { + if let Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) = slice.as_ref() + { for item in items { if let Some(key) = &item.key { self.visit_non_type_definition(key); @@ -1824,10 +1957,15 @@ impl<'a> Visitor<'a> for Checker<'a> { self.semantic.flags |= SemanticModelFlags::F_STRING; visitor::walk_expr(self, expr); } + Expr::TString(_) => { + self.semantic.flags |= SemanticModelFlags::T_STRING; + visitor::walk_expr(self, expr); + } Expr::Named(ast::ExprNamed { target, value, range: _, + node_index: _, }) => { self.visit_expr(value); @@ -1857,6 +1995,7 @@ impl<'a> Visitor<'a> for Checker<'a> { } Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self), Expr::FString(f_string) => analyze::string_like(f_string.into(), self), + Expr::TString(t_string) => analyze::string_like(t_string.into(), self), _ => {} } @@ -1876,6 +2015,7 @@ impl<'a> Visitor<'a> for Checker<'a> { name, body: _, range: _, + node_index: _, }) => { if let Some(name) = name { // Store the existing binding, if any. @@ -1950,6 +2090,7 @@ impl<'a> Visitor<'a> for Checker<'a> { | Pattern::MatchStar(ast::PatternMatchStar { name: Some(name), range: _, + node_index: _, }) | Pattern::MatchMapping(ast::PatternMatchMapping { rest: Some(name), .. @@ -2009,6 +2150,7 @@ impl<'a> Visitor<'a> for Checker<'a> { default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = bound { self.visit @@ -2025,6 +2167,7 @@ impl<'a> Visitor<'a> for Checker<'a> { default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = default { self.visit @@ -2036,6 +2179,7 @@ impl<'a> Visitor<'a> for Checker<'a> { default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = default { self.visit @@ -2046,19 +2190,26 @@ impl<'a> Visitor<'a> for Checker<'a> { } } - fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { let snapshot = self.semantic.flags; - if f_string_element.is_expression() { - self.semantic.flags |= SemanticModelFlags::F_STRING_REPLACEMENT_FIELD; + if interpolated_string_element.is_interpolation() { + self.semantic.flags |= SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD; } - visitor::walk_f_string_element(self, f_string_element); + visitor::walk_interpolated_string_element(self, interpolated_string_element); self.semantic.flags = snapshot; } } impl<'a> Checker<'a> { - /// Visit a [`Module`]. Returns `true` if the module contains a module-level docstring. + /// Visit a [`Module`]. fn visit_module(&mut self, python_ast: &'a Suite) { + // Extract any global bindings from the module body. + if let Some(globals) = Globals::from_body(python_ast) { + self.semantic.set_globals(globals); + } analyze::module(python_ast, self); } @@ -2103,7 +2254,9 @@ impl<'a> Checker<'a> { self.visit_expr(&generator.iter); self.semantic.push_scope(ScopeKind::Generator { kind, - is_async: generators.iter().any(|gen| gen.is_async), + is_async: generators + .iter() + .any(|comprehension| comprehension.is_async), }); self.visit_expr(&generator.target); @@ -2209,6 +2362,15 @@ impl<'a> Checker<'a> { self.semantic.flags = snapshot; } + /// Visit an [`Expr`], and treat it as the `typ` argument to `typing.cast`. + fn visit_cast_type_argument(&mut self, arg: &'a Expr) { + self.visit_type_definition(arg); + + if !self.source_type.is_stub() && self.is_rule_enabled(Rule::RuntimeCastValue) { + flake8_type_checking::rules::runtime_cast_value(self, arg); + } + } + /// Visit an [`Expr`], and treat it as a boolean test. This is useful for detecting whether an /// expressions return value is significant, or whether the calling context only relies on /// its truthiness. @@ -2319,6 +2481,7 @@ impl<'a> Checker<'a> { fn bind_builtins(&mut self) { let target_version = self.target_version(); + let settings = self.settings(); let mut bind_builtin = |builtin| { // Add the builtin to the scope. let binding_id = self.semantic.push_builtin(); @@ -2332,7 +2495,7 @@ impl<'a> Checker<'a> { for builtin in MAGIC_GLOBALS { bind_builtin(builtin); } - for builtin in &self.settings.builtins { + for builtin in &settings.builtins { bind_builtin(builtin); } } @@ -2537,7 +2700,8 @@ impl<'a> Checker<'a> { // if they are annotations in a module where `from __future__ import // annotations` is active, or they are type definitions in a stub file. debug_assert!( - self.semantic.future_annotations_or_stub() + (self.semantic.future_annotations_or_stub() + || self.target_version().defers_annotations()) && (self.source_type.is_stub() || self.semantic.in_annotation()) ); @@ -2602,15 +2766,13 @@ impl<'a> Checker<'a> { self.semantic.restore(snapshot); - if self.semantic.in_annotation() - && self.semantic.in_typing_only_annotation() - { - if self.enabled(Rule::QuotedAnnotation) { + if self.semantic.in_typing_only_annotation() { + if self.is_rule_enabled(Rule::QuotedAnnotation) { pyupgrade::rules::quoted_annotation(self, annotation, range); } } if self.source_type.is_stub() { - if self.enabled(Rule::QuotedAnnotationInStub) { + if self.is_rule_enabled(Rule::QuotedAnnotationInStub) { flake8_pyi::rules::quoted_annotation_in_stub( self, annotation, range, ); @@ -2632,7 +2794,9 @@ impl<'a> Checker<'a> { self.visit_expr(parsed_expr); if self.semantic.in_type_alias_value() { // stub files are covered by PYI020 - if !self.source_type.is_stub() && self.enabled(Rule::QuotedTypeAlias) { + if !self.source_type.is_stub() + && self.is_rule_enabled(Rule::QuotedTypeAlias) + { flake8_type_checking::rules::quoted_type_alias( self, parsed_expr, @@ -2645,13 +2809,14 @@ impl<'a> Checker<'a> { Err(parse_error) => { self.semantic.restore(snapshot); - if self.enabled(Rule::ForwardAnnotationSyntaxError) { - self.report_type_diagnostic(Diagnostic::new( + // F722 + if self.is_rule_enabled(Rule::ForwardAnnotationSyntaxError) { + self.report_type_diagnostic( pyflakes::rules::ForwardAnnotationSyntaxError { parse_error: parse_error.error.to_string(), }, string_expr.range(), - )); + ); } } } @@ -2735,6 +2900,7 @@ impl<'a> Checker<'a> { parameters, body, range: _, + node_index: _, })) = self.semantic.current_expression() else { unreachable!("Expected Expr::Lambda"); @@ -2791,31 +2957,29 @@ impl<'a> Checker<'a> { self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION; } else { if self.semantic.global_scope().uses_star_imports() { - if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { - self.diagnostics.get_mut().push( - Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: name.to_string(), - }, - range, - ) - .with_parent(definition.start()), - ); + // F405 + if self.is_rule_enabled(Rule::UndefinedLocalWithImportStarUsage) { + self.report_diagnostic( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: name.to_string(), + }, + range, + ) + .set_parent(definition.start()); } } else { - if self.enabled(Rule::UndefinedExport) { - if self.settings.preview.is_enabled() + // F822 + if self.is_rule_enabled(Rule::UndefinedExport) { + if is_undefined_export_in_dunder_init_enabled(self.settings()) || !self.path.ends_with("__init__.py") { - self.diagnostics.get_mut().push( - Diagnostic::new( - pyflakes::rules::UndefinedExport { - name: name.to_string(), - }, - range, - ) - .with_parent(definition.start()), - ); + self.report_diagnostic( + pyflakes::rules::UndefinedExport { + name: name.to_string(), + }, + range, + ) + .set_parent(definition.start()); } } } @@ -2861,7 +3025,7 @@ impl<'a> ParsedAnnotationsCache<'a> { } } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn check_ast( parsed: &Parsed, locator: &Locator, @@ -2875,8 +3039,9 @@ pub(crate) fn check_ast( source_type: PySourceType, cell_offsets: Option<&CellOffsets>, notebook_index: Option<&NotebookIndex>, - target_version: PythonVersion, -) -> (Vec, Vec) { + target_version: TargetVersion, + context: &LintContext, +) -> Vec { let module_path = package .map(PackageRoot::path) .and_then(|package| to_module_path(package, path)); @@ -2916,6 +3081,7 @@ pub(crate) fn check_ast( cell_offsets, notebook_index, target_version, + context, ); checker.bind_builtins(); @@ -2942,10 +3108,213 @@ pub(crate) fn check_ast( analyze::deferred_scopes(&checker); let Checker { - diagnostics, - semantic_errors, - .. + semantic_errors, .. } = checker; - (diagnostics.into_inner(), semantic_errors.into_inner()) + semantic_errors.into_inner() +} + +/// A type for collecting diagnostics in a given file. +/// +/// [`LintContext::report_diagnostic`] can be used to obtain a [`DiagnosticGuard`], which will push +/// a [`Violation`] to the contained [`Diagnostic`] collection on `Drop`. +pub(crate) struct LintContext<'a> { + diagnostics: RefCell>, + source_file: SourceFile, + rules: RuleTable, + settings: &'a LinterSettings, +} + +impl<'a> LintContext<'a> { + /// Create a new collector with the given `source_file` and an empty collection of + /// `Diagnostic`s. + pub(crate) fn new(path: &Path, contents: &str, settings: &'a LinterSettings) -> Self { + let source_file = + SourceFileBuilder::new(path.to_string_lossy().as_ref(), contents).finish(); + + // Ignore diagnostics based on per-file-ignores. + let mut rules = settings.rules.clone(); + for ignore in crate::fs::ignores_from_path(path, &settings.per_file_ignores) { + rules.disable(ignore); + } + + Self { + diagnostics: RefCell::default(), + source_file, + rules, + settings, + } + } + + /// Return a [`DiagnosticGuard`] for reporting a diagnostic. + /// + /// The guard derefs to a [`Diagnostic`], so it can be used to further modify the diagnostic + /// before it is added to the collection in the context on `Drop`. + pub(crate) fn report_diagnostic<'chk, T: Violation>( + &'chk self, + kind: T, + range: TextRange, + ) -> DiagnosticGuard<'chk, 'a> { + DiagnosticGuard { + context: self, + diagnostic: Some(kind.into_diagnostic(range, &self.source_file)), + rule: T::rule(), + } + } + + /// Return a [`DiagnosticGuard`] for reporting a diagnostic if the corresponding rule is + /// enabled. + /// + /// The guard derefs to a [`Diagnostic`], so it can be used to further modify the diagnostic + /// before it is added to the collection in the context on `Drop`. + pub(crate) fn report_diagnostic_if_enabled<'chk, T: Violation>( + &'chk self, + kind: T, + range: TextRange, + ) -> Option> { + let rule = T::rule(); + if self.is_rule_enabled(rule) { + Some(DiagnosticGuard { + context: self, + diagnostic: Some(kind.into_diagnostic(range, &self.source_file)), + rule, + }) + } else { + None + } + } + + #[inline] + pub(crate) const fn is_rule_enabled(&self, rule: Rule) -> bool { + self.rules.enabled(rule) + } + + #[inline] + pub(crate) const fn any_rule_enabled(&self, rules: &[Rule]) -> bool { + self.rules.any_enabled(rules) + } + + #[inline] + pub(crate) fn iter_enabled_rules(&self) -> impl Iterator + '_ { + self.rules.iter_enabled() + } + + #[inline] + pub(crate) fn into_parts(self) -> (Vec, SourceFile) { + (self.diagnostics.into_inner(), self.source_file) + } + + #[inline] + pub(crate) fn as_mut_vec(&mut self) -> &mut Vec { + self.diagnostics.get_mut() + } + + #[inline] + pub(crate) fn iter(&mut self) -> impl Iterator { + self.diagnostics.get_mut().iter() + } +} + +/// An abstraction for mutating a diagnostic. +/// +/// Callers can build this guard by starting with `Checker::report_diagnostic`. +/// +/// The primary function of this guard is to add the underlying diagnostic to the `Checker`'s list +/// of diagnostics on `Drop`, while dereferencing to the underlying diagnostic for mutations like +/// adding fixes or parent ranges. +pub(crate) struct DiagnosticGuard<'a, 'b> { + /// The parent checker that will receive the diagnostic on `Drop`. + context: &'a LintContext<'b>, + /// The diagnostic that we want to report. + /// + /// This is always `Some` until the `Drop` (or `defuse`) call. + diagnostic: Option, + rule: Rule, +} + +impl DiagnosticGuard<'_, '_> { + /// Consume the underlying `Diagnostic` without emitting it. + /// + /// In general you should avoid constructing diagnostics that may not be emitted, but this + /// method can be used where this is unavoidable. + pub(crate) fn defuse(mut self) { + self.diagnostic = None; + } +} + +impl DiagnosticGuard<'_, '_> { + fn resolve_applicability(&self, fix: &Fix) -> Applicability { + self.context + .settings + .fix_safety + .resolve_applicability(self.rule, fix.applicability()) + } + + /// Set the [`Fix`] used to fix the diagnostic. + #[inline] + pub(crate) fn set_fix(&mut self, fix: Fix) { + if !self.context.rules.should_fix(self.rule) { + self.diagnostic.as_mut().unwrap().remove_fix(); + return; + } + let applicability = self.resolve_applicability(&fix); + self.diagnostic + .as_mut() + .unwrap() + .set_fix(fix.with_applicability(applicability)); + } + + /// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`. + /// Otherwise, log the error. + #[inline] + pub(crate) fn try_set_fix(&mut self, func: impl FnOnce() -> anyhow::Result) { + match func() { + Ok(fix) => self.set_fix(fix), + Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err), + } + } + + /// Set the [`Fix`] used to fix the diagnostic, if the provided function returns `Ok`. + /// Otherwise, log the error. + #[inline] + pub(crate) fn try_set_optional_fix( + &mut self, + func: impl FnOnce() -> anyhow::Result>, + ) { + match func() { + Ok(None) => {} + Ok(Some(fix)) => self.set_fix(fix), + Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err), + } + } +} + +impl std::ops::Deref for DiagnosticGuard<'_, '_> { + type Target = Diagnostic; + + fn deref(&self) -> &Diagnostic { + // OK because `self.diagnostic` is only `None` within `Drop`. + self.diagnostic.as_ref().unwrap() + } +} + +/// Return a mutable borrow of the diagnostic in this guard. +impl std::ops::DerefMut for DiagnosticGuard<'_, '_> { + fn deref_mut(&mut self) -> &mut Diagnostic { + // OK because `self.diagnostic` is only `None` within `Drop`. + self.diagnostic.as_mut().unwrap() + } +} + +impl Drop for DiagnosticGuard<'_, '_> { + fn drop(&mut self) { + if std::thread::panicking() { + // Don't submit diagnostics when panicking because they might be incomplete. + return; + } + + if let Some(diagnostic) = self.diagnostic.take() { + self.context.diagnostics.borrow_mut().push(diagnostic); + } + } } diff --git a/crates/ruff_linter/src/checkers/filesystem.rs b/crates/ruff_linter/src/checkers/filesystem.rs index f6d9c491c8ccc..f7fbff280f818 100644 --- a/crates/ruff_linter/src/checkers/filesystem.rs +++ b/crates/ruff_linter/src/checkers/filesystem.rs @@ -1,16 +1,17 @@ use std::path::Path; -use ruff_diagnostics::Diagnostic; use ruff_python_ast::PythonVersion; use ruff_python_trivia::CommentRanges; +use crate::Locator; +use crate::checkers::ast::LintContext; use crate::package::PackageRoot; +use crate::preview::is_allow_nested_roots_enabled; use crate::registry::Rule; use crate::rules::flake8_builtins::rules::stdlib_module_shadowing; use crate::rules::flake8_no_pep420::rules::implicit_namespace_package; use crate::rules::pep8_naming::rules::invalid_module_name; use crate::settings::LinterSettings; -use crate::Locator; pub(crate) fn check_file_path( path: &Path, @@ -19,39 +20,30 @@ pub(crate) fn check_file_path( comment_ranges: &CommentRanges, settings: &LinterSettings, target_version: PythonVersion, -) -> Vec { - let mut diagnostics: Vec = vec![]; - + context: &LintContext, +) { // flake8-no-pep420 - if settings.rules.enabled(Rule::ImplicitNamespacePackage) { - if let Some(diagnostic) = implicit_namespace_package( + if context.is_rule_enabled(Rule::ImplicitNamespacePackage) { + let allow_nested_roots = is_allow_nested_roots_enabled(settings); + implicit_namespace_package( path, package, locator, comment_ranges, &settings.project_root, &settings.src, - settings.preview, - ) { - diagnostics.push(diagnostic); - } + allow_nested_roots, + context, + ); } // pep8-naming - if settings.rules.enabled(Rule::InvalidModuleName) { - if let Some(diagnostic) = - invalid_module_name(path, package, &settings.pep8_naming.ignore_names) - { - diagnostics.push(diagnostic); - } + if context.is_rule_enabled(Rule::InvalidModuleName) { + invalid_module_name(path, package, &settings.pep8_naming.ignore_names, context); } // flake8-builtins - if settings.rules.enabled(Rule::StdlibModuleShadowing) { - if let Some(diagnostic) = stdlib_module_shadowing(path, settings, target_version) { - diagnostics.push(diagnostic); - } + if context.is_rule_enabled(Rule::StdlibModuleShadowing) { + stdlib_module_shadowing(path, settings, target_version, context); } - - diagnostics } diff --git a/crates/ruff_linter/src/checkers/imports.rs b/crates/ruff_linter/src/checkers/imports.rs index 2dd7a33a29999..dd24614dd677b 100644 --- a/crates/ruff_linter/src/checkers/imports.rs +++ b/crates/ruff_linter/src/checkers/imports.rs @@ -1,6 +1,5 @@ //! Lint rules based on import analysis. -use ruff_diagnostics::Diagnostic; use ruff_notebook::CellOffsets; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::{ModModule, PySourceType, PythonVersion}; @@ -8,15 +7,17 @@ use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::Parsed; +use crate::Locator; use crate::directives::IsortDirectives; use crate::package::PackageRoot; use crate::registry::Rule; use crate::rules::isort; use crate::rules::isort::block::{Block, BlockBuilder}; use crate::settings::LinterSettings; -use crate::Locator; -#[allow(clippy::too_many_arguments)] +use super::ast::LintContext; + +#[expect(clippy::too_many_arguments)] pub(crate) fn check_imports( parsed: &Parsed, locator: &Locator, @@ -28,7 +29,8 @@ pub(crate) fn check_imports( source_type: PySourceType, cell_offsets: Option<&CellOffsets>, target_version: PythonVersion, -) -> Vec { + context: &LintContext, +) { // Extract all import blocks from the AST. let tracker = { let mut tracker = @@ -40,11 +42,10 @@ pub(crate) fn check_imports( let blocks: Vec<&Block> = tracker.iter().collect(); // Enforce import rules. - let mut diagnostics = vec![]; - if settings.rules.enabled(Rule::UnsortedImports) { + if context.is_rule_enabled(Rule::UnsortedImports) { for block in &blocks { if !block.imports.is_empty() { - if let Some(diagnostic) = isort::rules::organize_imports( + isort::rules::organize_imports( block, locator, stylist, @@ -54,21 +55,19 @@ pub(crate) fn check_imports( source_type, parsed.tokens(), target_version, - ) { - diagnostics.push(diagnostic); - } + context, + ); } } } - if settings.rules.enabled(Rule::MissingRequiredImport) { - diagnostics.extend(isort::rules::add_required_imports( + if context.is_rule_enabled(Rule::MissingRequiredImport) { + isort::rules::add_required_imports( parsed, locator, stylist, settings, source_type, - )); + context, + ); } - - diagnostics } diff --git a/crates/ruff_linter/src/checkers/logical_lines.rs b/crates/ruff_linter/src/checkers/logical_lines.rs index 1933889387b92..5c60a3171a597 100644 --- a/crates/ruff_linter/src/checkers/logical_lines.rs +++ b/crates/ruff_linter/src/checkers/logical_lines.rs @@ -1,20 +1,22 @@ -use ruff_diagnostics::Diagnostic; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::{TokenKind, Tokens}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::line_width::IndentWidth; -use crate::registry::{AsRule, Rule}; +use crate::registry::Rule; use crate::rules::pycodestyle::rules::logical_lines::{ - extraneous_whitespace, indentation, missing_whitespace, missing_whitespace_after_keyword, - missing_whitespace_around_operator, redundant_backslash, space_after_comma, - space_around_operator, whitespace_around_keywords, whitespace_around_named_parameter_equals, - whitespace_before_comment, whitespace_before_parameters, LogicalLines, TokenFlags, + LogicalLines, TokenFlags, extraneous_whitespace, indentation, missing_whitespace, + missing_whitespace_after_keyword, missing_whitespace_around_operator, redundant_backslash, + space_after_comma, space_around_operator, whitespace_around_keywords, + whitespace_around_named_parameter_equals, whitespace_before_comment, + whitespace_before_parameters, }; use crate::settings::LinterSettings; -use crate::Locator; + +use super::ast::LintContext; /// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size. pub(crate) fn expand_indent(line: &str, indent_width: IndentWidth) -> usize { @@ -39,56 +41,54 @@ pub(crate) fn check_logical_lines( indexer: &Indexer, stylist: &Stylist, settings: &LinterSettings, -) -> Vec { - let mut context = LogicalLinesContext::new(settings); - + context: &LintContext, +) { let mut prev_line = None; let mut prev_indent_level = None; let indent_char = stylist.indentation().as_char(); - let enforce_space_around_operator = settings.rules.any_enabled(&[ + let enforce_space_around_operator = context.any_rule_enabled(&[ Rule::MultipleSpacesBeforeOperator, Rule::MultipleSpacesAfterOperator, Rule::TabBeforeOperator, Rule::TabAfterOperator, ]); - let enforce_whitespace_around_named_parameter_equals = settings.rules.any_enabled(&[ + let enforce_whitespace_around_named_parameter_equals = context.any_rule_enabled(&[ Rule::UnexpectedSpacesAroundKeywordParameterEquals, Rule::MissingWhitespaceAroundParameterEquals, ]); - let enforce_missing_whitespace_around_operator = settings.rules.any_enabled(&[ + let enforce_missing_whitespace_around_operator = context.any_rule_enabled(&[ Rule::MissingWhitespaceAroundOperator, Rule::MissingWhitespaceAroundArithmeticOperator, Rule::MissingWhitespaceAroundBitwiseOrShiftOperator, Rule::MissingWhitespaceAroundModuloOperator, ]); - let enforce_missing_whitespace = settings.rules.enabled(Rule::MissingWhitespace); - let enforce_space_after_comma = settings - .rules - .any_enabled(&[Rule::MultipleSpacesAfterComma, Rule::TabAfterComma]); - let enforce_extraneous_whitespace = settings.rules.any_enabled(&[ + let enforce_missing_whitespace = context.is_rule_enabled(Rule::MissingWhitespace); + let enforce_space_after_comma = + context.any_rule_enabled(&[Rule::MultipleSpacesAfterComma, Rule::TabAfterComma]); + let enforce_extraneous_whitespace = context.any_rule_enabled(&[ Rule::WhitespaceAfterOpenBracket, Rule::WhitespaceBeforeCloseBracket, Rule::WhitespaceBeforePunctuation, ]); - let enforce_whitespace_around_keywords = settings.rules.any_enabled(&[ + let enforce_whitespace_around_keywords = context.any_rule_enabled(&[ Rule::MultipleSpacesAfterKeyword, Rule::MultipleSpacesBeforeKeyword, Rule::TabAfterKeyword, Rule::TabBeforeKeyword, ]); let enforce_missing_whitespace_after_keyword = - settings.rules.enabled(Rule::MissingWhitespaceAfterKeyword); - let enforce_whitespace_before_comment = settings.rules.any_enabled(&[ + context.is_rule_enabled(Rule::MissingWhitespaceAfterKeyword); + let enforce_whitespace_before_comment = context.any_rule_enabled(&[ Rule::TooFewSpacesBeforeInlineComment, Rule::NoSpaceAfterInlineComment, Rule::NoSpaceAfterBlockComment, Rule::MultipleLeadingHashesForBlockComment, ]); let enforce_whitespace_before_parameters = - settings.rules.enabled(Rule::WhitespaceBeforeParameters); - let enforce_redundant_backslash = settings.rules.enabled(Rule::RedundantBackslash); - let enforce_indentation = settings.rules.any_enabled(&[ + context.is_rule_enabled(Rule::WhitespaceBeforeParameters); + let enforce_redundant_backslash = context.is_rule_enabled(Rule::RedundantBackslash); + let enforce_indentation = context.any_rule_enabled(&[ Rule::IndentationWithInvalidMultiple, Rule::NoIndentedBlock, Rule::UnexpectedIndentation, @@ -101,24 +101,24 @@ pub(crate) fn check_logical_lines( for line in &LogicalLines::from_tokens(tokens, locator) { if line.flags().contains(TokenFlags::OPERATOR) { if enforce_space_around_operator { - space_around_operator(&line, &mut context); + space_around_operator(&line, context); } if enforce_whitespace_around_named_parameter_equals { - whitespace_around_named_parameter_equals(&line, &mut context); + whitespace_around_named_parameter_equals(&line, context); } if enforce_missing_whitespace_around_operator { - missing_whitespace_around_operator(&line, &mut context); + missing_whitespace_around_operator(&line, context); } if enforce_missing_whitespace { - missing_whitespace(&line, &mut context); + missing_whitespace(&line, context); } } if line.flags().contains(TokenFlags::PUNCTUATION) && enforce_space_after_comma { - space_after_comma(&line, &mut context); + space_after_comma(&line, context); } if line @@ -126,30 +126,30 @@ pub(crate) fn check_logical_lines( .intersects(TokenFlags::OPERATOR | TokenFlags::BRACKET | TokenFlags::PUNCTUATION) && enforce_extraneous_whitespace { - extraneous_whitespace(&line, &mut context); + extraneous_whitespace(&line, context); } if line.flags().contains(TokenFlags::KEYWORD) { if enforce_whitespace_around_keywords { - whitespace_around_keywords(&line, &mut context); + whitespace_around_keywords(&line, context); } if enforce_missing_whitespace_after_keyword { - missing_whitespace_after_keyword(&line, &mut context); + missing_whitespace_after_keyword(&line, context); } } if line.flags().contains(TokenFlags::COMMENT) && enforce_whitespace_before_comment { - whitespace_before_comment(&line, locator, &mut context); + whitespace_before_comment(&line, locator, context); } if line.flags().contains(TokenFlags::BRACKET) { if enforce_whitespace_before_parameters { - whitespace_before_parameters(&line, &mut context); + whitespace_before_parameters(&line, context); } if enforce_redundant_backslash { - redundant_backslash(&line, locator, indexer, &mut context); + redundant_backslash(&line, locator, indexer, context); } } @@ -169,18 +169,16 @@ pub(crate) fn check_logical_lines( let indent_size = 4; if enforce_indentation { - for kind in indentation( + indentation( &line, prev_line.as_ref(), indent_char, indent_level, prev_indent_level, indent_size, - ) { - if settings.rules.enabled(kind.rule()) { - context.push_diagnostic(Diagnostic::new(kind, range)); - } - } + range, + context, + ); } if !line.is_comment_only() { @@ -188,26 +186,4 @@ pub(crate) fn check_logical_lines( prev_indent_level = Some(indent_level); } } - context.diagnostics -} - -#[derive(Debug, Clone)] -pub(crate) struct LogicalLinesContext<'a> { - settings: &'a LinterSettings, - diagnostics: Vec, -} - -impl<'a> LogicalLinesContext<'a> { - fn new(settings: &'a LinterSettings) -> Self { - Self { - settings, - diagnostics: Vec::new(), - } - } - - pub(crate) fn push_diagnostic(&mut self, diagnostic: Diagnostic) { - if self.settings.rules.enabled(diagnostic.kind.rule()) { - self.diagnostics.push(diagnostic); - } - } } diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index cb01ac686b687..bf411511ced68 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -5,7 +5,6 @@ use std::path::Path; use itertools::Itertools; use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; @@ -13,62 +12,63 @@ use crate::fix::edits::delete_comment; use crate::noqa::{ Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping, }; -use crate::registry::{AsRule, Rule, RuleSet}; +use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; use crate::rules::pygrep_hooks; use crate::rules::ruff; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; use crate::settings::LinterSettings; -use crate::Locator; +use crate::{Edit, Fix, Locator}; -#[allow(clippy::too_many_arguments)] +use super::ast::LintContext; + +/// RUF100 pub(crate) fn check_noqa( - diagnostics: &mut Vec, + context: &mut LintContext, path: &Path, locator: &Locator, comment_ranges: &CommentRanges, noqa_line_for: &NoqaMapping, analyze_directives: bool, - per_file_ignores: &RuleSet, settings: &LinterSettings, ) -> Vec { // Identify any codes that are globally exempted (within the current file). let file_noqa_directives = FileNoqaDirectives::extract(locator, comment_ranges, &settings.external, path); - let exemption = FileExemption::from(&file_noqa_directives); // Extract all `noqa` directives. let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator); + if file_noqa_directives.is_empty() && noqa_directives.is_empty() { + return Vec::new(); + } + + let exemption = FileExemption::from(&file_noqa_directives); + // Indices of diagnostics that were ignored by a `noqa` directive. let mut ignored_diagnostics = vec![]; // Remove any ignored diagnostics. - 'outer: for (index, diagnostic) in diagnostics.iter().enumerate() { - if matches!(diagnostic.kind.rule(), Rule::BlanketNOQA) { + 'outer: for (index, diagnostic) in context.iter().enumerate() { + // Can't ignore syntax errors. + let Some(code) = diagnostic.secondary_code() else { + continue; + }; + + if *code == Rule::BlanketNOQA.noqa_code() { continue; } - match &exemption { - FileExemption::All(_) => { - // If the file is exempted, ignore all diagnostics. - ignored_diagnostics.push(index); - continue; - } - FileExemption::Codes(codes) => { - // If the diagnostic is ignored by a global exemption, ignore it. - if codes.contains(&&diagnostic.kind.rule().noqa_code()) { - ignored_diagnostics.push(index); - continue; - } - } + if exemption.contains_secondary_code(code) { + ignored_diagnostics.push(index); + continue; } let noqa_offsets = diagnostic - .parent + .parent() .into_iter() - .chain(std::iter::once(diagnostic.start())) + .chain(std::iter::once(diagnostic.expect_range().start())) .map(|position| noqa_line_for.resolve(position)) .unique(); @@ -77,17 +77,21 @@ pub(crate) fn check_noqa( { let suppressed = match &directive_line.directive { Directive::All(_) => { - directive_line - .matches - .push(diagnostic.kind.rule().noqa_code()); + let Ok(rule) = Rule::from_code(code) else { + debug_assert!(false, "Invalid secondary code `{code}`"); + continue; + }; + directive_line.matches.push(rule); ignored_diagnostics.push(index); true } Directive::Codes(directive) => { - if directive.includes(diagnostic.kind.rule()) { - directive_line - .matches - .push(diagnostic.kind.rule().noqa_code()); + if directive.includes(code) { + let Ok(rule) = Rule::from_code(code) else { + debug_assert!(false, "Invalid secondary code `{code}`"); + continue; + }; + directive_line.matches.push(rule); ignored_diagnostics.push(index); true } else { @@ -105,40 +109,28 @@ pub(crate) fn check_noqa( // Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself // suppressed. - if settings.rules.enabled(Rule::UnusedNOQA) + if context.is_rule_enabled(Rule::UnusedNOQA) && analyze_directives && !exemption.includes(Rule::UnusedNOQA) - && !per_file_ignores.contains(Rule::UnusedNOQA) { - let directives: Vec<_> = if settings.preview.is_enabled() { - noqa_directives - .lines() - .iter() - .map(|line| (&line.directive, &line.matches, false)) - .chain( - file_noqa_directives - .lines() - .iter() - .map(|line| (&line.parsed_file_exemption, &line.matches, true)), - ) - .collect() - } else { - noqa_directives - .lines() - .iter() - .map(|line| (&line.directive, &line.matches, false)) - .collect() - }; + let directives = noqa_directives + .lines() + .iter() + .map(|line| (&line.directive, &line.matches, false)) + .chain( + file_noqa_directives + .lines() + .iter() + .map(|line| (&line.parsed_file_exemption, &line.matches, true)), + ); for (directive, matches, is_file_level) in directives { match directive { Directive::All(directive) => { if matches.is_empty() { let edit = delete_comment(directive.range(), locator); - let mut diagnostic = - Diagnostic::new(UnusedNOQA { codes: None }, directive.range()); + let mut diagnostic = context + .report_diagnostic(UnusedNOQA { codes: None }, directive.range()); diagnostic.set_fix(Fix::safe_edit(edit)); - - diagnostics.push(diagnostic); } } Directive::Codes(directive) => { @@ -158,11 +150,11 @@ pub(crate) fn check_noqa( if seen_codes.insert(original_code) { let is_code_used = if is_file_level { - diagnostics - .iter() - .any(|diag| diag.kind.rule().noqa_code() == code) + context.iter().any(|diag| { + diag.secondary_code().is_some_and(|noqa| *noqa == code) + }) } else { - matches.iter().any(|match_| *match_ == code) + matches.iter().any(|match_| match_.noqa_code() == code) } || settings .external .iter() @@ -171,7 +163,7 @@ pub(crate) fn check_noqa( if is_code_used { valid_codes.push(original_code); } else if let Ok(rule) = Rule::from_code(code) { - if settings.rules.enabled(rule) { + if context.is_rule_enabled(rule) { unmatched_codes.push(original_code); } else { disabled_codes.push(original_code); @@ -211,7 +203,7 @@ pub(crate) fn check_noqa( directive.range(), ) }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = context.report_diagnostic( UnusedNOQA { codes: Some(UnusedCodes { disabled: disabled_codes @@ -235,38 +227,30 @@ pub(crate) fn check_noqa( directive.range(), ); diagnostic.set_fix(Fix::safe_edit(edit)); - diagnostics.push(diagnostic); } } } } } - if settings.rules.enabled(Rule::RedirectedNOQA) - && !per_file_ignores.contains(Rule::RedirectedNOQA) - && !exemption.includes(Rule::RedirectedNOQA) - { - ruff::rules::redirected_noqa(diagnostics, &noqa_directives); - ruff::rules::redirected_file_noqa(diagnostics, &file_noqa_directives); + if context.is_rule_enabled(Rule::RedirectedNOQA) && !exemption.includes(Rule::RedirectedNOQA) { + ruff::rules::redirected_noqa(context, &noqa_directives); + ruff::rules::redirected_file_noqa(context, &file_noqa_directives); } - if settings.rules.enabled(Rule::BlanketNOQA) - && !per_file_ignores.contains(Rule::BlanketNOQA) - && !exemption.enumerates(Rule::BlanketNOQA) - { + if context.is_rule_enabled(Rule::BlanketNOQA) && !exemption.enumerates(Rule::BlanketNOQA) { pygrep_hooks::rules::blanket_noqa( - diagnostics, + context, &noqa_directives, locator, &file_noqa_directives, ); } - if settings.rules.enabled(Rule::InvalidRuleCode) - && !per_file_ignores.contains(Rule::InvalidRuleCode) + if context.is_rule_enabled(Rule::InvalidRuleCode) && !exemption.enumerates(Rule::InvalidRuleCode) { - ruff::rules::invalid_noqa_code(diagnostics, &noqa_directives, locator, &settings.external); + ruff::rules::invalid_noqa_code(context, &noqa_directives, locator, &settings.external); } ignored_diagnostics.sort_unstable(); diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index f8c6e1492ae40..5cc8f7a9089c8 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -1,11 +1,11 @@ //! Lint rules based on checking physical lines. -use ruff_diagnostics::Diagnostic; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_source_file::UniversalNewlines; use ruff_text_size::TextSize; +use crate::Locator; use crate::registry::Rule; use crate::rules::flake8_copyright::rules::missing_copyright_notice; use crate::rules::pycodestyle::rules::{ @@ -15,7 +15,8 @@ use crate::rules::pycodestyle::rules::{ use crate::rules::pylint; use crate::rules::ruff::rules::indented_form_feed; use crate::settings::LinterSettings; -use crate::Locator; + +use super::ast::LintContext; pub(crate) fn check_physical_lines( locator: &Locator, @@ -23,18 +24,18 @@ pub(crate) fn check_physical_lines( indexer: &Indexer, doc_lines: &[TextSize], settings: &LinterSettings, -) -> Vec { - let mut diagnostics: Vec = vec![]; - - let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong); - let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong); - let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile); - let enforce_mixed_spaces_and_tabs = settings.rules.enabled(Rule::MixedSpacesAndTabs); - let enforce_bidirectional_unicode = settings.rules.enabled(Rule::BidirectionalUnicode); - let enforce_trailing_whitespace = settings.rules.enabled(Rule::TrailingWhitespace); + context: &LintContext, +) { + let enforce_doc_line_too_long = context.is_rule_enabled(Rule::DocLineTooLong); + let enforce_line_too_long = context.is_rule_enabled(Rule::LineTooLong); + let enforce_no_newline_at_end_of_file = + context.is_rule_enabled(Rule::MissingNewlineAtEndOfFile); + let enforce_mixed_spaces_and_tabs = context.is_rule_enabled(Rule::MixedSpacesAndTabs); + let enforce_bidirectional_unicode = context.is_rule_enabled(Rule::BidirectionalUnicode); + let enforce_trailing_whitespace = context.is_rule_enabled(Rule::TrailingWhitespace); let enforce_blank_line_contains_whitespace = - settings.rules.enabled(Rule::BlankLineWithWhitespace); - let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice); + context.is_rule_enabled(Rule::BlankLineWithWhitespace); + let enforce_copyright_notice = context.is_rule_enabled(Rule::MissingCopyrightNotice); let mut doc_lines_iter = doc_lines.iter().peekable(); let comment_ranges = indexer.comment_ranges(); @@ -45,67 +46,54 @@ pub(crate) fn check_physical_lines( .is_some() { if enforce_doc_line_too_long { - if let Some(diagnostic) = doc_line_too_long(&line, comment_ranges, settings) { - diagnostics.push(diagnostic); - } + doc_line_too_long(&line, comment_ranges, settings, context); } } if enforce_mixed_spaces_and_tabs { - if let Some(diagnostic) = mixed_spaces_and_tabs(&line) { - diagnostics.push(diagnostic); - } + mixed_spaces_and_tabs(&line, context); } if enforce_line_too_long { - if let Some(diagnostic) = line_too_long(&line, comment_ranges, settings) { - diagnostics.push(diagnostic); - } + line_too_long(&line, comment_ranges, settings, context); } if enforce_bidirectional_unicode { - diagnostics.extend(pylint::rules::bidirectional_unicode(&line)); + pylint::rules::bidirectional_unicode(&line, context); } if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace { - if let Some(diagnostic) = trailing_whitespace(&line, locator, indexer, settings) { - diagnostics.push(diagnostic); - } + trailing_whitespace(&line, locator, indexer, context); } - if settings.rules.enabled(Rule::IndentedFormFeed) { - if let Some(diagnostic) = indented_form_feed(&line) { - diagnostics.push(diagnostic); - } + if context.is_rule_enabled(Rule::IndentedFormFeed) { + indented_form_feed(&line, context); } } if enforce_no_newline_at_end_of_file { - if let Some(diagnostic) = no_newline_at_end_of_file(locator, stylist) { - diagnostics.push(diagnostic); - } + no_newline_at_end_of_file(locator, stylist, context); } if enforce_copyright_notice { - if let Some(diagnostic) = missing_copyright_notice(locator, settings) { - diagnostics.push(diagnostic); - } + missing_copyright_notice(locator, settings, context); } - - diagnostics } #[cfg(test)] mod tests { + use std::path::Path; + use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::parse_module; + use crate::Locator; + use crate::checkers::ast::LintContext; use crate::line_width::LineLength; use crate::registry::Rule; use crate::rules::pycodestyle; use crate::settings::LinterSettings; - use crate::Locator; use super::check_physical_lines; @@ -118,19 +106,16 @@ mod tests { let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents()); let check_with_max_line_length = |line_length: LineLength| { - check_physical_lines( - &locator, - &stylist, - &indexer, - &[], - &LinterSettings { - pycodestyle: pycodestyle::settings::Settings { - max_line_length: line_length, - ..pycodestyle::settings::Settings::default() - }, - ..LinterSettings::for_rule(Rule::LineTooLong) + let settings = LinterSettings { + pycodestyle: pycodestyle::settings::Settings { + max_line_length: line_length, + ..pycodestyle::settings::Settings::default() }, - ) + ..LinterSettings::for_rule(Rule::LineTooLong) + }; + let diagnostics = LintContext::new(Path::new(""), line, &settings); + check_physical_lines(&locator, &stylist, &indexer, &[], &settings, &diagnostics); + diagnostics.into_parts().0 }; let line_length = LineLength::try_from(8).unwrap(); assert_eq!(check_with_max_line_length(line_length), vec![]); diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index dc157ba3e0ec7..388bbfea8dd05 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -2,24 +2,25 @@ use std::path::Path; -use ruff_diagnostics::Diagnostic; use ruff_notebook::CellOffsets; use ruff_python_ast::PySourceType; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::Tokens; +use crate::Locator; use crate::directives::TodoComment; -use crate::registry::{AsRule, Rule}; +use crate::registry::Rule; use crate::rules::pycodestyle::rules::BlankLinesChecker; use crate::rules::{ eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff, }; use crate::settings::LinterSettings; -use crate::Locator; -#[allow(clippy::too_many_arguments)] +use super::ast::LintContext; + +#[expect(clippy::too_many_arguments)] pub(crate) fn check_tokens( tokens: &Tokens, path: &Path, @@ -29,11 +30,11 @@ pub(crate) fn check_tokens( settings: &LinterSettings, source_type: PySourceType, cell_offsets: Option<&CellOffsets>, -) -> Vec { - let mut diagnostics: Vec = vec![]; + context: &mut LintContext, +) { let comment_ranges = indexer.comment_ranges(); - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::BlankLineBetweenMethods, Rule::BlankLinesTopLevel, Rule::TooManyBlankLines, @@ -41,45 +42,44 @@ pub(crate) fn check_tokens( Rule::BlankLinesAfterFunctionOrClass, Rule::BlankLinesBeforeNestedDefinition, ]) { - BlankLinesChecker::new(locator, stylist, settings, source_type, cell_offsets) - .check_lines(tokens, &mut diagnostics); + BlankLinesChecker::new( + locator, + stylist, + settings, + source_type, + cell_offsets, + context, + ) + .check_lines(tokens); } - if settings.rules.enabled(Rule::BlanketTypeIgnore) { - pygrep_hooks::rules::blanket_type_ignore(&mut diagnostics, comment_ranges, locator); + if context.is_rule_enabled(Rule::BlanketTypeIgnore) { + pygrep_hooks::rules::blanket_type_ignore(context, comment_ranges, locator); } - if settings.rules.enabled(Rule::EmptyComment) { - pylint::rules::empty_comments(&mut diagnostics, comment_ranges, locator); + if context.is_rule_enabled(Rule::EmptyComment) { + pylint::rules::empty_comments(context, comment_ranges, locator); } - if settings - .rules - .enabled(Rule::AmbiguousUnicodeCharacterComment) - { + if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) { for range in comment_ranges { - ruff::rules::ambiguous_unicode_character_comment( - &mut diagnostics, - locator, - range, - settings, - ); + ruff::rules::ambiguous_unicode_character_comment(context, locator, range, settings); } } - if settings.rules.enabled(Rule::CommentedOutCode) { - eradicate::rules::commented_out_code(&mut diagnostics, locator, comment_ranges, settings); + if context.is_rule_enabled(Rule::CommentedOutCode) { + eradicate::rules::commented_out_code(context, locator, comment_ranges, settings); } - if settings.rules.enabled(Rule::UTF8EncodingDeclaration) { - pyupgrade::rules::unnecessary_coding_comment(&mut diagnostics, locator, comment_ranges); + if context.is_rule_enabled(Rule::UTF8EncodingDeclaration) { + pyupgrade::rules::unnecessary_coding_comment(context, locator, comment_ranges); } - if settings.rules.enabled(Rule::TabIndentation) { - pycodestyle::rules::tab_indentation(&mut diagnostics, locator, indexer); + if context.is_rule_enabled(Rule::TabIndentation) { + pycodestyle::rules::tab_indentation(context, locator, indexer); } - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::InvalidCharacterBackspace, Rule::InvalidCharacterSub, Rule::InvalidCharacterEsc, @@ -87,17 +87,17 @@ pub(crate) fn check_tokens( Rule::InvalidCharacterZeroWidthSpace, ]) { for token in tokens { - pylint::rules::invalid_string_characters(&mut diagnostics, token, locator); + pylint::rules::invalid_string_characters(context, token, locator); } } - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::MultipleStatementsOnOneLineColon, Rule::MultipleStatementsOnOneLineSemicolon, Rule::UselessSemicolon, ]) { pycodestyle::rules::compound_statements( - &mut diagnostics, + context, tokens, locator, indexer, @@ -106,52 +106,40 @@ pub(crate) fn check_tokens( ); } - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::SingleLineImplicitStringConcatenation, Rule::MultiLineImplicitStringConcatenation, ]) { - flake8_implicit_str_concat::rules::implicit( - &mut diagnostics, - tokens, - locator, - indexer, - settings, - ); + flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer, settings); } - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::MissingTrailingComma, Rule::TrailingCommaOnBareTuple, Rule::ProhibitedTrailingComma, ]) { - flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, indexer); + flake8_commas::rules::trailing_commas(context, tokens, locator, indexer); } - if settings.rules.enabled(Rule::ExtraneousParentheses) { - pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator); + if context.is_rule_enabled(Rule::ExtraneousParentheses) { + pyupgrade::rules::extraneous_parentheses(context, tokens, locator); } - if source_type.is_stub() && settings.rules.enabled(Rule::TypeCommentInStub) { - flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, comment_ranges); + if source_type.is_stub() && context.is_rule_enabled(Rule::TypeCommentInStub) { + flake8_pyi::rules::type_comment_in_stub(context, locator, comment_ranges); } - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::ShebangNotExecutable, Rule::ShebangMissingExecutableFile, Rule::ShebangLeadingWhitespace, Rule::ShebangNotFirstLine, Rule::ShebangMissingPython, ]) { - flake8_executable::rules::from_tokens( - &mut diagnostics, - path, - locator, - comment_ranges, - settings, - ); + flake8_executable::rules::from_tokens(context, path, locator, comment_ranges); } - if settings.rules.any_enabled(&[ + if context.any_rule_enabled(&[ Rule::InvalidTodoTag, Rule::MissingTodoAuthor, Rule::MissingTodoLink, @@ -172,19 +160,11 @@ pub(crate) fn check_tokens( TodoComment::from_comment(comment, *comment_range, i) }) .collect(); - flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, comment_ranges); - flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); + flake8_todos::rules::todos(context, &todo_comments, locator, comment_ranges); + flake8_fixme::rules::todos(context, &todo_comments); } - if settings.rules.enabled(Rule::TooManyNewlinesAtEndOfFile) { - pycodestyle::rules::too_many_newlines_at_end_of_file( - &mut diagnostics, - tokens, - cell_offsets, - ); + if context.is_rule_enabled(Rule::TooManyNewlinesAtEndOfFile) { + pycodestyle::rules::too_many_newlines_at_end_of_file(context, tokens, cell_offsets); } - - diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())); - - diagnostics } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index dce86a56a82f8..8813c239dfab7 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -4,13 +4,14 @@ /// `--select`. For pylint this is e.g. C0414 and E0118 but also C and E01. use std::fmt::Formatter; -use strum_macros::{AsRefStr, EnumIter}; +use ruff_db::diagnostic::SecondaryCode; +use strum_macros::EnumIter; -use crate::registry::{AsRule, Linter}; +use crate::registry::Linter; use crate::rule_selector::is_single_rule_selector; use crate::rules; -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NoqaCode(&'static str, &'static str); impl NoqaCode { @@ -46,6 +47,33 @@ impl PartialEq<&str> for NoqaCode { } } +impl PartialEq for &str { + fn eq(&self, other: &NoqaCode) -> bool { + other.eq(self) + } +} + +impl PartialEq for SecondaryCode { + fn eq(&self, other: &NoqaCode) -> bool { + &self.as_str() == other + } +} + +impl PartialEq for NoqaCode { + fn eq(&self, other: &SecondaryCode) -> bool { + other.eq(self) + } +} + +impl serde::Serialize for NoqaCode { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + #[derive(Debug, Copy, Clone)] pub enum RuleGroup { /// The rule is stable. @@ -61,7 +89,7 @@ pub enum RuleGroup { #[ruff_macros::map_codes] pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { - #[allow(clippy::enum_glob_use)] + #[expect(clippy::enum_glob_use)] use Linter::*; #[rustfmt::skip] @@ -189,9 +217,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "C0132") => (RuleGroup::Stable, rules::pylint::rules::TypeParamNameMismatch), (Pylint, "C0205") => (RuleGroup::Stable, rules::pylint::rules::SingleStringSlots), (Pylint, "C0206") => (RuleGroup::Stable, rules::pylint::rules::DictIndexMissingItems), + (Pylint, "C0207") => (RuleGroup::Preview, rules::pylint::rules::MissingMaxsplitArg), (Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet), (Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias), - (Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel), + (Pylint, "C0415") => (RuleGroup::Stable, rules::pylint::rules::ImportOutsideTopLevel), (Pylint, "C1802") => (RuleGroup::Stable, rules::pylint::rules::LenTest), (Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString), (Pylint, "C2401") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiName), @@ -260,7 +289,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias), (Pylint, "R1730") => (RuleGroup::Stable, rules::pylint::rules::IfStmtMinMax), (Pylint, "R1716") => (RuleGroup::Stable, rules::pylint::rules::BooleanChainedComparison), - (Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup), + (Pylint, "R1733") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDictIndexLookup), (Pylint, "R1736") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryListIndexLookup), (Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison), (Pylint, "R2044") => (RuleGroup::Stable, rules::pylint::rules::EmptyComment), @@ -271,7 +300,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { #[cfg(any(feature = "test-rules", test))] (Pylint, "W0101") => (RuleGroup::Preview, rules::pylint::rules::UnreachableCode), (Pylint, "W0108") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryLambda), - (Pylint, "W0177") => (RuleGroup::Preview, rules::pylint::rules::NanComparison), + (Pylint, "W0177") => (RuleGroup::Stable, rules::pylint::rules::NanComparison), (Pylint, "W0120") => (RuleGroup::Stable, rules::pylint::rules::UselessElseOnLoop), (Pylint, "W0127") => (RuleGroup::Stable, rules::pylint::rules::SelfAssigningVariable), (Pylint, "W0128") => (RuleGroup::Stable, rules::pylint::rules::RedeclaredAssignedName), @@ -293,7 +322,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "W1509") => (RuleGroup::Stable, rules::pylint::rules::SubprocessPopenPreexecFn), (Pylint, "W1510") => (RuleGroup::Stable, rules::pylint::rules::SubprocessRunWithoutCheck), (Pylint, "W1514") => (RuleGroup::Preview, rules::pylint::rules::UnspecifiedEncoding), - (Pylint, "W1641") => (RuleGroup::Preview, rules::pylint::rules::EqWithoutHash), + (Pylint, "W1641") => (RuleGroup::Stable, rules::pylint::rules::EqWithoutHash), (Pylint, "W2101") => (RuleGroup::Stable, rules::pylint::rules::UselessWithLock), (Pylint, "W2901") => (RuleGroup::Stable, rules::pylint::rules::RedefinedLoopName), (Pylint, "W3201") => (RuleGroup::Preview, rules::pylint::rules::BadDunderMethodName), @@ -539,10 +568,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum), (Pyupgrade, "043") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs), (Pyupgrade, "044") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP646Unpack), - (Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional), - (Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass), - (Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction), - (Pyupgrade, "049") => (RuleGroup::Preview, rules::pyupgrade::rules::PrivateTypeParameter), + (Pyupgrade, "045") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP604AnnotationOptional), + (Pyupgrade, "046") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericClass), + (Pyupgrade, "047") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericFunction), + (Pyupgrade, "049") => (RuleGroup::Stable, rules::pyupgrade::rules::PrivateTypeParameter), + (Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), @@ -649,7 +679,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bandit, "317") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLSaxUsage), (Flake8Bandit, "318") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLMiniDOMUsage), (Flake8Bandit, "319") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousXMLPullDOMUsage), - (Flake8Bandit, "320") => (RuleGroup::Deprecated, rules::flake8_bandit::rules::SuspiciousXMLETreeUsage), + (Flake8Bandit, "320") => (RuleGroup::Removed, rules::flake8_bandit::rules::SuspiciousXMLETreeUsage), (Flake8Bandit, "321") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousFTPLibUsage), (Flake8Bandit, "323") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousUnverifiedContextUsage), (Flake8Bandit, "324") => (RuleGroup::Stable, rules::flake8_bandit::rules::HashlibInsecureHashFunction), @@ -741,7 +771,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (PandasVet, "013") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotStack), (PandasVet, "015") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfPdMerge), (PandasVet, "101") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck), - (PandasVet, "901") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasDfVariableName), + (PandasVet, "901") => (RuleGroup::Deprecated, rules::pandas_vet::rules::PandasDfVariableName), // flake8-errmsg (Flake8ErrMsg, "101") => (RuleGroup::Stable, rules::flake8_errmsg::rules::RawStringInException), @@ -835,10 +865,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8PytestStyle, "025") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture), (Flake8PytestStyle, "026") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters), (Flake8PytestStyle, "027") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion), - (Flake8PytestStyle, "028") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestParameterWithDefaultArgument), + (Flake8PytestStyle, "028") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestParameterWithDefaultArgument), (Flake8PytestStyle, "029") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsWithoutWarning), - (Flake8PytestStyle, "030") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsTooBroad), - (Flake8PytestStyle, "031") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsWithMultipleStatements), + (Flake8PytestStyle, "030") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestWarnsTooBroad), + (Flake8PytestStyle, "031") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestWarnsWithMultipleStatements), // flake8-pie (Flake8Pie, "790") => (RuleGroup::Stable, rules::flake8_pie::rules::UnnecessaryPlaceholder), @@ -889,27 +919,27 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Tryceratops, "401") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseLogMessage), // flake8-use-pathlib - (Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathAbspath), + (Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath), (Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod), (Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir), (Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs), (Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename), (Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace), - (Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRmdir), - (Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRemove), - (Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsUnlink), + (Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir), + (Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove), + (Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink), (Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsGetcwd), - (Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExists), - (Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExpanduser), - (Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsdir), - (Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsfile), - (Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIslink), - (Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReadlink), + (Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists), + (Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser), + (Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir), + (Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsfile), + (Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIslink), + (Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReadlink), (Flake8UsePathlib, "116") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsStat), - (Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsabs), + (Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsabs), (Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin), - (Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathBasename), - (Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathDirname), + (Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename), + (Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname), (Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile), (Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext), (Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen), @@ -924,6 +954,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob), (Flake8UsePathlib, "208") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsListdir), (Flake8UsePathlib, "210") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix), + (Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::violations::OsSymlink), // flake8-logging-format (Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat), @@ -985,7 +1016,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "024") => (RuleGroup::Stable, rules::ruff::rules::MutableFromkeysValue), (Ruff, "026") => (RuleGroup::Stable, rules::ruff::rules::DefaultFactoryKwarg), (Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax), - (Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment), + (Ruff, "028") => (RuleGroup::Stable, rules::ruff::rules::InvalidFormatterSuppressionComment), (Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync), (Ruff, "030") => (RuleGroup::Stable, rules::ruff::rules::AssertWithPrintMessage), (Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript), @@ -1004,16 +1035,20 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "046") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryCastToInt), (Ruff, "047") => (RuleGroup::Preview, rules::ruff::rules::NeedlessElse), (Ruff, "048") => (RuleGroup::Stable, rules::ruff::rules::MapIntVersionParsing), - (Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum), + (Ruff, "049") => (RuleGroup::Stable, rules::ruff::rules::DataclassEnum), (Ruff, "051") => (RuleGroup::Stable, rules::ruff::rules::IfKeyInDictDel), (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), - (Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars), + (Ruff, "053") => (RuleGroup::Stable, rules::ruff::rules::ClassWithMixedTypeVars), (Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), (Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback), - (Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound), - (Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip), + (Ruff, "057") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryRound), + (Ruff, "058") => (RuleGroup::Stable, rules::ruff::rules::StarmapZip), (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), + (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), + (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), + (Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict), + (Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), @@ -1102,10 +1137,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend), (Refurb, "116") => (RuleGroup::Preview, rules::refurb::rules::FStringNumberFormat), (Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator), - (Refurb, "122") => (RuleGroup::Preview, rules::refurb::rules::ForLoopWrites), + (Refurb, "122") => (RuleGroup::Stable, rules::refurb::rules::ForLoopWrites), (Refurb, "129") => (RuleGroup::Stable, rules::refurb::rules::ReadlinesInFor), (Refurb, "131") => (RuleGroup::Preview, rules::refurb::rules::DeleteFullSlice), - (Refurb, "132") => (RuleGroup::Preview, rules::refurb::rules::CheckAndRemoveFromSet), + (Refurb, "132") => (RuleGroup::Stable, rules::refurb::rules::CheckAndRemoveFromSet), (Refurb, "136") => (RuleGroup::Stable, rules::refurb::rules::IfExprMinMax), (Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap), (Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations), @@ -1114,12 +1149,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant), (Refurb, "154") => (RuleGroup::Preview, rules::refurb::rules::RepeatedGlobal), (Refurb, "156") => (RuleGroup::Preview, rules::refurb::rules::HardcodedStringCharset), - (Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor), + (Refurb, "157") => (RuleGroup::Stable, rules::refurb::rules::VerboseDecimalConstructor), (Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount), - (Refurb, "162") => (RuleGroup::Preview, rules::refurb::rules::FromisoformatReplaceZ), + (Refurb, "162") => (RuleGroup::Stable, rules::refurb::rules::FromisoformatReplaceZ), (Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase), (Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat), - (Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr), + (Refurb, "166") => (RuleGroup::Stable, rules::refurb::rules::IntOnSlicedStr), (Refurb, "167") => (RuleGroup::Stable, rules::refurb::rules::RegexFlagAlias), (Refurb, "168") => (RuleGroup::Stable, rules::refurb::rules::IsinstanceTypeNone), (Refurb, "169") => (RuleGroup::Stable, rules::refurb::rules::TypeNoneComparison), @@ -1138,9 +1173,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Logging, "004") => (RuleGroup::Preview, rules::flake8_logging::rules::LogExceptionOutsideExceptHandler), (Flake8Logging, "007") => (RuleGroup::Stable, rules::flake8_logging::rules::ExceptionWithoutExcInfo), (Flake8Logging, "009") => (RuleGroup::Stable, rules::flake8_logging::rules::UndocumentedWarn), - (Flake8Logging, "014") => (RuleGroup::Preview, rules::flake8_logging::rules::ExcInfoOutsideExceptHandler), + (Flake8Logging, "014") => (RuleGroup::Stable, rules::flake8_logging::rules::ExcInfoOutsideExceptHandler), (Flake8Logging, "015") => (RuleGroup::Stable, rules::flake8_logging::rules::RootLoggerCall), _ => return None, }) } + +impl std::fmt::Display for Rule { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.write_str(self.into()) + } +} diff --git a/crates/ruff_linter/src/cst/helpers.rs b/crates/ruff_linter/src/cst/helpers.rs index b41d6348f8605..4e871e53ec98e 100644 --- a/crates/ruff_linter/src/cst/helpers.rs +++ b/crates/ruff_linter/src/cst/helpers.rs @@ -19,14 +19,14 @@ pub(crate) fn or_space(whitespace: ParenthesizableWhitespace) -> Parenthesizable /// Negate a condition, i.e., `a` => `not a` and `not a` => `a`. pub(crate) fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> { - if let Expression::UnaryOperation(ref expression) = expression { + if let Expression::UnaryOperation(expression) = expression { if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) { return *expression.expression.clone(); } } // If the expression is `True` or `False`, return the opposite. - if let Expression::Name(ref expression) = expression { + if let Expression::Name(expression) = expression { match expression.value { "True" => { return Expression::Name(Box::new(Name { diff --git a/crates/ruff_linter/src/cst/matchers.rs b/crates/ruff_linter/src/cst/matchers.rs index 3bce354cb60b8..9e6ed0f613eb2 100644 --- a/crates/ruff_linter/src/cst/matchers.rs +++ b/crates/ruff_linter/src/cst/matchers.rs @@ -1,10 +1,10 @@ use crate::fix::codemods::CodegenStylist; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use libcst_native::{ Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString, FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import, - ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name, - SmallStatement, Statement, Suite, Tuple, With, + ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, SmallStatement, + Statement, Suite, Tuple, With, }; use ruff_python_codegen::Stylist; @@ -104,14 +104,6 @@ pub(crate) fn match_attribute<'a, 'b>( } } -pub(crate) fn match_name<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a Name<'b>> { - if let Expression::Name(name) = expression { - Ok(name) - } else { - bail!("Expected Expression::Name") - } -} - pub(crate) fn match_arg<'a, 'b>(call: &'a Call<'b>) -> Result<&'a Arg<'b>> { if let Some(arg) = call.args.first() { Ok(arg) diff --git a/crates/ruff_linter/src/directives.rs b/crates/ruff_linter/src/directives.rs index 71278963e9b69..bded1a8f2bedf 100644 --- a/crates/ruff_linter/src/directives.rs +++ b/crates/ruff_linter/src/directives.rs @@ -11,9 +11,9 @@ use ruff_python_trivia::CommentRanges; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use crate::Locator; use crate::noqa::NoqaMapping; use crate::settings::LinterSettings; -use crate::Locator; bitflags! { #[derive(Debug, Copy, Clone)] @@ -366,11 +366,11 @@ mod tests { use ruff_python_trivia::CommentRanges; use ruff_text_size::{TextLen, TextRange, TextSize}; + use crate::Locator; use crate::directives::{ - extract_isort_directives, extract_noqa_line_for, TodoDirective, TodoDirectiveKind, + TodoDirective, TodoDirectiveKind, extract_isort_directives, extract_noqa_line_for, }; use crate::noqa::NoqaMapping; - use crate::Locator; use super::IsortDirectives; diff --git a/crates/ruff_linter/src/doc_lines.rs b/crates/ruff_linter/src/doc_lines.rs index 5996d139ab2ec..c45dc64b4da0c 100644 --- a/crates/ruff_linter/src/doc_lines.rs +++ b/crates/ruff_linter/src/doc_lines.rs @@ -4,7 +4,7 @@ use std::iter::FusedIterator; use std::slice::Iter; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast, Stmt, Suite}; use ruff_python_parser::{Token, TokenKind, Tokens}; use ruff_source_file::UniversalNewlineIterator; @@ -73,6 +73,7 @@ impl StatementVisitor<'_> for StringLinesVisitor<'_> { if let Stmt::Expr(ast::StmtExpr { value: expr, range: _, + node_index: _, }) = stmt { if expr.is_string_literal_expr() { diff --git a/crates/ruff_linter/src/docstrings/extraction.rs b/crates/ruff_linter/src/docstrings/extraction.rs index a0597b06269b6..2a7e70a0b5f6b 100644 --- a/crates/ruff_linter/src/docstrings/extraction.rs +++ b/crates/ruff_linter/src/docstrings/extraction.rs @@ -7,7 +7,12 @@ use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, Member pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&ast::ExprStringLiteral> { let stmt = suite.first()?; // Require the docstring to be a standalone expression. - let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else { + let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = stmt + else { return None; }; // Only match strings. diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index aa42af30ef50f..3151ef45a0357 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -418,7 +418,7 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option } /// Check if the suspected context is really a section header. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn is_docstring_section( line: &Line, indent_size: TextSize, @@ -444,8 +444,7 @@ fn is_docstring_section( if next_line.is_empty() { false } else { - let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '=')); - next_line_is_underline + next_line.chars().all(|char| matches!(char, '-' | '=')) } }); if next_line_is_underline { diff --git a/crates/ruff_linter/src/fix/codemods.rs b/crates/ruff_linter/src/fix/codemods.rs index bcf4ff71826f9..29db53fed164b 100644 --- a/crates/ruff_linter/src/fix/codemods.rs +++ b/crates/ruff_linter/src/fix/codemods.rs @@ -2,21 +2,21 @@ //! and return the modified code snippet as output. use std::borrow::Cow; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use libcst_native::{ Codegen, CodegenState, Expression, ImportNames, NameOrAttribute, ParenthesizableWhitespace, SmallStatement, Statement, }; use rustc_hash::{FxHashMap, FxHashSet}; -use smallvec::{smallvec, SmallVec}; +use smallvec::{SmallVec, smallvec}; use unicode_normalization::UnicodeNormalization; -use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::Stmt; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_codegen::Stylist; -use crate::cst::matchers::match_statement; use crate::Locator; +use crate::cst::matchers::match_statement; /// Glue code to make libcst codegen work with ruff's Stylist pub(crate) trait CodegenStylist<'a>: Codegen<'a> { diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs index 3d9f08d35bd41..e4ac1b5d0f14d 100644 --- a/crates/ruff_linter/src/fix/edits.rs +++ b/crates/ruff_linter/src/fix/edits.rs @@ -2,25 +2,25 @@ use anyhow::{Context, Result}; -use ruff_diagnostics::Edit; +use ruff_python_ast::AnyNodeRef; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Parameters, Stmt}; -use ruff_python_ast::{AnyNodeRef, ArgOrKeyword}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_trivia::textwrap::dedent_to; use ruff_python_trivia::{ - has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind, - SimpleTokenizer, + CommentRanges, PythonWhitespace, SimpleTokenKind, SimpleTokenizer, has_leading_content, + is_python_whitespace, }; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlines}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use crate::Edit; +use crate::Locator; use crate::cst::matchers::{match_function_def, match_indented_block, match_statement}; use crate::fix::codemods; use crate::fix::codemods::CodegenStylist; use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder}; -use crate::Locator; /// Return the [`Edit`] to use when deleting a [`Stmt`]. /// @@ -209,6 +209,7 @@ pub(crate) fn remove_argument( arguments: &Arguments, parentheses: Parentheses, source: &str, + comment_ranges: &CommentRanges, ) -> Result { // Partition into arguments before and after the argument to remove. let (before, after): (Vec<_>, Vec<_>) = arguments @@ -217,6 +218,15 @@ pub(crate) fn remove_argument( .filter(|range| argument.range() != *range) .partition(|range| range.start() < argument.start()); + let arg = arguments + .arguments_source_order() + .find(|arg| arg.range() == argument.range()) + .context("Unable to find argument")?; + + let parenthesized_range = + parenthesized_range(arg.value().into(), arguments.into(), comment_ranges, source) + .unwrap_or(arg.range()); + if !after.is_empty() { // Case 1: argument or keyword is _not_ the last node, so delete from the start of the // argument to the end of the subsequent comma. @@ -234,7 +244,7 @@ pub(crate) fn remove_argument( }) .context("Unable to find next token")?; - Ok(Edit::deletion(argument.start(), next.start())) + Ok(Edit::deletion(parenthesized_range.start(), next.start())) } else if let Some(previous) = before.iter().map(Ranged::end).max() { // Case 2: argument or keyword is the last node, so delete from the start of the // previous comma to the end of the argument. @@ -245,7 +255,7 @@ pub(crate) fn remove_argument( .find(|token| token.kind == SimpleTokenKind::Comma) .context("Unable to find trailing comma")?; - Ok(Edit::deletion(comma.start(), argument.end())) + Ok(Edit::deletion(comma.start(), parenthesized_range.end())) } else { // Case 3: argument or keyword is the only node, so delete the arguments (but preserve // parentheses, if needed). @@ -257,19 +267,23 @@ pub(crate) fn remove_argument( } /// Generic function to add arguments or keyword arguments to function calls. +/// +/// The new argument will be inserted before the first existing keyword argument in `arguments`, if +/// there are any present. Otherwise, the new argument is added to the end of the argument list. pub(crate) fn add_argument( argument: &str, arguments: &Arguments, comment_ranges: &CommentRanges, source: &str, ) -> Edit { - if let Some(last) = arguments.arguments_source_order().last() { + if let Some(ast::Keyword { range, value, .. }) = arguments.keywords.first() { + let keyword = parenthesized_range(value.into(), arguments.into(), comment_ranges, source) + .unwrap_or(*range); + Edit::insertion(format!("{argument}, "), keyword.start()) + } else if let Some(last) = arguments.arguments_source_order().last() { // Case 1: existing arguments, so append after the last argument. let last = parenthesized_range( - match last { - ArgOrKeyword::Arg(arg) => arg.into(), - ArgOrKeyword::Keyword(keyword) => (&keyword.value).into(), - }, + last.value().into(), arguments.into(), comment_ranges, source, @@ -591,11 +605,10 @@ fn all_lines_fit( #[cfg(test)] mod tests { - use anyhow::{anyhow, Result}; + use anyhow::{Result, anyhow}; use ruff_source_file::SourceFileBuilder; use test_case::test_case; - use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::Stmt; use ruff_python_codegen::Stylist; use ruff_python_parser::{parse_expression, parse_module}; @@ -605,8 +618,7 @@ mod tests { use crate::fix::edits::{ add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon, }; - use crate::message::DiagnosticMessage; - use crate::Locator; + use crate::{Edit, Fix, Locator, Violation}; /// Parse the given source using [`Mode::Module`] and return the first statement. fn parse_first_stmt(source: &str) -> Result { @@ -737,22 +749,16 @@ x = 1 \ let diag = { use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; let mut iter = edits.into_iter(); - let diag = Diagnostic::new( - MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary. + // The choice of rule here is arbitrary. + let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic( TextRange::default(), - ) - .with_fix(Fix::safe_edits( + &SourceFileBuilder::new("", "").finish(), + ); + diagnostic.set_fix(Fix::safe_edits( iter.next().ok_or(anyhow!("expected edits nonempty"))?, iter, )); - DiagnosticMessage { - kind: diag.kind, - range: diag.range, - fix: diag.fix, - parent: diag.parent, - file: SourceFileBuilder::new("", "").finish(), - noqa_offset: TextSize::default(), - } + diagnostic }; assert_eq!(apply_fixes([diag].iter(), &locator).code, expect); Ok(()) diff --git a/crates/ruff_linter/src/fix/mod.rs b/crates/ruff_linter/src/fix/mod.rs index 77f78fbddb360..e2672f2f37a97 100644 --- a/crates/ruff_linter/src/fix/mod.rs +++ b/crates/ruff_linter/src/fix/mod.rs @@ -1,16 +1,17 @@ use std::collections::BTreeSet; use itertools::Itertools; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; -use ruff_diagnostics::{Edit, Fix, IsolationLevel, SourceMap}; +use ruff_db::diagnostic::Diagnostic; +use ruff_diagnostics::{IsolationLevel, SourceMap}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use crate::Locator; use crate::linter::FixTable; -use crate::message::{DiagnosticMessage, Message}; -use crate::registry::{AsRule, Rule}; +use crate::registry::Rule; use crate::settings::types::UnsafeFixes; -use crate::Locator; +use crate::{Edit, Fix}; pub(crate) mod codemods; pub(crate) mod edits; @@ -27,19 +28,17 @@ pub(crate) struct FixResult { /// Fix errors in a file, and write the fixed source code to disk. pub(crate) fn fix_file( - messages: &[Message], + diagnostics: &[Diagnostic], locator: &Locator, unsafe_fixes: UnsafeFixes, ) -> Option { let required_applicability = unsafe_fixes.required_applicability(); - let mut with_fixes = messages + let mut with_fixes = diagnostics .iter() - .filter_map(Message::as_diagnostic_message) .filter(|message| { message - .fix - .as_ref() + .fix() .is_some_and(|fix| fix.applies(required_applicability)) }) .peekable(); @@ -53,24 +52,20 @@ pub(crate) fn fix_file( /// Apply a series of fixes. fn apply_fixes<'a>( - diagnostics: impl Iterator, + diagnostics: impl Iterator, locator: &'a Locator<'a>, ) -> FixResult { let mut output = String::with_capacity(locator.len()); let mut last_pos: Option = None; let mut applied: BTreeSet<&Edit> = BTreeSet::default(); let mut isolated: FxHashSet = FxHashSet::default(); - let mut fixed = FxHashMap::default(); + let mut fixed = FixTable::default(); let mut source_map = SourceMap::default(); - for (rule, fix) in diagnostics - .filter_map(|diagnostic| { - diagnostic - .fix - .as_ref() - .map(|fix| (diagnostic.kind.rule(), fix)) - }) - .sorted_by(|(rule1, fix1), (rule2, fix2)| cmp_fix(*rule1, *rule2, fix1, fix2)) + for (code, name, fix) in diagnostics + .filter_map(|msg| msg.secondary_code().map(|code| (code, msg.name(), msg))) + .filter_map(|(code, name, diagnostic)| diagnostic.fix().map(|fix| (code, name, fix))) + .sorted_by(|(_, name1, fix1), (_, name2, fix2)| cmp_fix(name1, name2, fix1, fix2)) { let mut edits = fix .edits() @@ -115,7 +110,7 @@ fn apply_fixes<'a>( } applied.extend(applied_edits.drain(..)); - *fixed.entry(rule).or_default() += 1; + *fixed.entry(code).or_default(name) += 1; } // Add the remaining content. @@ -130,63 +125,73 @@ fn apply_fixes<'a>( } /// Compare two fixes. -fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering { +fn cmp_fix(name1: &str, name2: &str, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering { // Always apply `RedefinedWhileUnused` before `UnusedImport`, as the latter can end up fixing // the former. But we can't apply this just for `RedefinedWhileUnused` and `UnusedImport` because it violates // `< is transitive: a < b and b < c implies a < c. The same must hold for both == and >.` // See https://github.com/astral-sh/ruff/issues/12469#issuecomment-2244392085 - match (rule1, rule2) { - (Rule::RedefinedWhileUnused, Rule::RedefinedWhileUnused) => std::cmp::Ordering::Equal, - (Rule::RedefinedWhileUnused, _) => std::cmp::Ordering::Less, - (_, Rule::RedefinedWhileUnused) => std::cmp::Ordering::Greater, - _ => std::cmp::Ordering::Equal, + let redefined_while_unused = Rule::RedefinedWhileUnused.name().as_str(); + if (name1, name2) == (redefined_while_unused, redefined_while_unused) { + std::cmp::Ordering::Equal + } else if name1 == redefined_while_unused { + std::cmp::Ordering::Less + } else if name2 == redefined_while_unused { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal } // Apply fixes in order of their start position. .then_with(|| fix1.min_start().cmp(&fix2.min_start())) // Break ties in the event of overlapping rules, for some specific combinations. - .then_with(|| match (&rule1, &rule2) { + .then_with(|| { + let rules = (name1, name2); // Apply `MissingTrailingPeriod` fixes before `NewLineAfterLastParagraph` fixes. - (Rule::MissingTrailingPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less, - (Rule::NewLineAfterLastParagraph, Rule::MissingTrailingPeriod) => { + let missing_trailing_period = Rule::MissingTrailingPeriod.name().as_str(); + let newline_after_last_paragraph = Rule::NewLineAfterLastParagraph.name().as_str(); + let if_else_instead_of_dict_get = Rule::IfElseBlockInsteadOfDictGet.name().as_str(); + let if_else_instead_of_if_exp = Rule::IfElseBlockInsteadOfIfExp.name().as_str(); + if rules == (missing_trailing_period, newline_after_last_paragraph) { + std::cmp::Ordering::Less + } else if rules == (newline_after_last_paragraph, missing_trailing_period) { std::cmp::Ordering::Greater } // Apply `IfElseBlockInsteadOfDictGet` fixes before `IfElseBlockInsteadOfIfExp` fixes. - (Rule::IfElseBlockInsteadOfDictGet, Rule::IfElseBlockInsteadOfIfExp) => { + else if rules == (if_else_instead_of_dict_get, if_else_instead_of_if_exp) { std::cmp::Ordering::Less - } - (Rule::IfElseBlockInsteadOfIfExp, Rule::IfElseBlockInsteadOfDictGet) => { + } else if rules == (if_else_instead_of_if_exp, if_else_instead_of_dict_get) { std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal } - _ => std::cmp::Ordering::Equal, }) } #[cfg(test)] mod tests { - use ruff_diagnostics::{Edit, Fix, SourceMarker}; + use ruff_diagnostics::SourceMarker; use ruff_source_file::SourceFileBuilder; use ruff_text_size::{Ranged, TextSize}; - use crate::fix::{apply_fixes, FixResult}; - use crate::message::DiagnosticMessage; + use crate::fix::{FixResult, apply_fixes}; use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; - use crate::Locator; + use crate::{Edit, Fix}; + use crate::{Locator, Violation}; + use ruff_db::diagnostic::Diagnostic; - #[allow(deprecated)] fn create_diagnostics( filename: &str, source: &str, edit: impl IntoIterator, - ) -> Vec { + ) -> Vec { edit.into_iter() - .map(|edit| DiagnosticMessage { + .map(|edit| { // The choice of rule here is arbitrary. - kind: MissingNewlineAtEndOfFile.into(), - range: edit.range(), - fix: Some(Fix::safe_edit(edit)), - parent: None, - file: SourceFileBuilder::new(filename, source).finish(), - noqa_offset: TextSize::default(), + let mut diagnostic = MissingNewlineAtEndOfFile.into_diagnostic( + edit.range(), + &SourceFileBuilder::new(filename, source).finish(), + ); + diagnostic.set_fix(Fix::safe_edit(edit)); + diagnostic }) .collect() } @@ -201,7 +206,7 @@ mod tests { source_map, } = apply_fixes(diagnostics.iter(), &locator); assert_eq!(code, ""); - assert_eq!(fixes.values().sum::(), 0); + assert_eq!(fixes.counts().sum::(), 0); assert!(source_map.markers().is_empty()); } @@ -238,7 +243,7 @@ print("hello world") "# .trim() ); - assert_eq!(fixes.values().sum::(), 1); + assert_eq!(fixes.counts().sum::(), 1); assert_eq!( source_map.markers(), &[ @@ -279,7 +284,7 @@ class A(Bar): " .trim(), ); - assert_eq!(fixes.values().sum::(), 1); + assert_eq!(fixes.counts().sum::(), 1); assert_eq!( source_map.markers(), &[ @@ -316,7 +321,7 @@ class A: " .trim() ); - assert_eq!(fixes.values().sum::(), 1); + assert_eq!(fixes.counts().sum::(), 1); assert_eq!( source_map.markers(), &[ @@ -357,7 +362,7 @@ class A(object): " .trim() ); - assert_eq!(fixes.values().sum::(), 2); + assert_eq!(fixes.counts().sum::(), 2); assert_eq!( source_map.markers(), &[ @@ -399,7 +404,7 @@ class A: " .trim(), ); - assert_eq!(fixes.values().sum::(), 1); + assert_eq!(fixes.counts().sum::(), 1); assert_eq!( source_map.markers(), &[ diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index 983651121a8c0..37fbf36bc6a1a 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -20,6 +20,9 @@ pub fn get_cwd() -> &'static Path { /// Create a set with codes matching the pattern/code pairs. pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnoreList) -> RuleSet { + if ignore_list.is_empty() { + return RuleSet::empty(); + } ignore_list .iter_matches(path, "Adding per-file ignores") .flatten() diff --git a/crates/ruff_linter/src/importer/insertion.rs b/crates/ruff_linter/src/importer/insertion.rs index 5181145e9b357..412fcfaf9377f 100644 --- a/crates/ruff_linter/src/importer/insertion.rs +++ b/crates/ruff_linter/src/importer/insertion.rs @@ -1,15 +1,16 @@ //! Insert statements into Python code. use std::ops::Add; -use ruff_diagnostics::Edit; -use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::Stmt; +use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_codegen::Stylist; use ruff_python_parser::{TokenKind, Tokens}; -use ruff_python_trivia::{textwrap::indent, PythonWhitespace}; +use ruff_python_trivia::is_python_whitespace; +use ruff_python_trivia::{PythonWhitespace, textwrap::indent}; use ruff_source_file::{LineRanges, UniversalNewlineIterator}; use ruff_text_size::{Ranged, TextSize}; +use crate::Edit; use crate::Locator; #[derive(Debug, Clone, PartialEq, Eq)] @@ -306,7 +307,7 @@ fn match_semicolon(s: &str) -> Option { fn match_continuation(s: &str) -> Option { for (offset, c) in s.char_indices() { match c { - ' ' | '\t' => continue, + _ if is_python_whitespace(c) => continue, '\\' => return Some(TextSize::try_from(offset).unwrap()), _ => break, } diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index 82b88f2321c00..31bf7262bced3 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -8,7 +8,6 @@ use std::error::Error; use anyhow::Result; use libcst_native::{ImportAlias, Name as cstName, NameOrAttribute}; -use ruff_diagnostics::Edit; use ruff_python_ast::{self as ast, Expr, ModModule, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_parser::{Parsed, Tokens}; @@ -18,11 +17,12 @@ use ruff_python_semantic::{ use ruff_python_trivia::textwrap::indent; use ruff_text_size::{Ranged, TextSize}; +use crate::Edit; +use crate::Locator; use crate::cst::matchers::{match_aliases, match_import_from, match_statement}; use crate::fix; use crate::fix::codemods::CodegenStylist; use crate::importer::insertion::Insertion; -use crate::Locator; mod insertion; @@ -453,6 +453,7 @@ impl<'a> Importer<'a> { names, level, range: _, + node_index: _, }) = stmt { if *level == 0 diff --git a/crates/ruff_linter/src/lib.rs b/crates/ruff_linter/src/lib.rs index 5c024cb5f87ea..eaafd7a526713 100644 --- a/crates/ruff_linter/src/lib.rs +++ b/crates/ruff_linter/src/lib.rs @@ -9,11 +9,14 @@ pub use locator::Locator; pub use noqa::generate_noqa_edits; #[cfg(feature = "clap")] pub use registry::clap_completion::RuleParser; +pub use rule_selector::RuleSelector; #[cfg(feature = "clap")] pub use rule_selector::clap_completion::RuleSelectorParser; -pub use rule_selector::RuleSelector; pub use rules::pycodestyle::rules::IOError; +pub(crate) use ruff_diagnostics::{Applicability, Edit, Fix}; +pub use violation::{AlwaysFixableViolation, FixAvailability, Violation, ViolationMetadata}; + pub const VERSION: &str = env!("CARGO_PKG_VERSION"); mod checkers; @@ -34,6 +37,7 @@ pub mod message; mod noqa; pub mod package; pub mod packaging; +pub mod preview; pub mod pyproject_toml; pub mod registry; mod renamer; @@ -44,6 +48,7 @@ pub mod settings; pub mod source_kind; mod text_helpers; pub mod upstream_categories; +mod violation; #[cfg(any(test, fuzzing))] pub mod test; diff --git a/crates/ruff_linter/src/line_width.rs b/crates/ruff_linter/src/line_width.rs index 6577ff6ca5465..7525a68cdd832 100644 --- a/crates/ruff_linter/src/line_width.rs +++ b/crates/ruff_linter/src/line_width.rs @@ -1,7 +1,7 @@ use std::error::Error; use std::fmt; use std::hash::Hasher; -use std::num::{NonZeroU16, NonZeroU8, ParseIntError}; +use std::num::{NonZeroU8, NonZeroU16, ParseIntError}; use std::str::FromStr; use serde::{Deserialize, Serialize}; diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index b266a82e54198..dd8543b983125 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1,24 +1,21 @@ use std::borrow::Cow; -use std::cell::LazyCell; -use std::ops::Deref; use std::path::Path; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use colored::Colorize; use itertools::Itertools; use ruff_python_parser::semantic_errors::SemanticSyntaxError; -use rustc_hash::FxHashMap; +use rustc_hash::FxBuildHasher; -use ruff_diagnostics::Diagnostic; +use ruff_db::diagnostic::{Diagnostic, SecondaryCode}; use ruff_notebook::Notebook; use ruff_python_ast::{ModModule, PySourceType, PythonVersion}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::{ParseError, ParseOptions, Parsed, UnsupportedSyntaxError}; -use ruff_source_file::SourceFileBuilder; -use ruff_text_size::Ranged; +use ruff_source_file::SourceFile; -use crate::checkers::ast::check_ast; +use crate::checkers::ast::{LintContext, check_ast}; use crate::checkers::filesystem::check_file_path; use crate::checkers::imports::check_imports; use crate::checkers::noqa::check_noqa; @@ -26,21 +23,24 @@ use crate::checkers::physical_lines::check_physical_lines; use crate::checkers::tokens::check_tokens; use crate::directives::Directives; use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens}; -use crate::fix::{fix_file, FixResult}; -use crate::message::Message; +use crate::fix::{FixResult, fix_file}; +use crate::message::create_syntax_error_diagnostic; use crate::noqa::add_noqa; use crate::package::PackageRoot; -use crate::registry::{AsRule, Rule, RuleSet}; +use crate::preview::is_py314_support_enabled; +use crate::registry::Rule; #[cfg(any(feature = "test-rules", test))] -use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES}; +use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule}; use crate::settings::types::UnsafeFixes; -use crate::settings::{flags, LinterSettings}; +use crate::settings::{LinterSettings, TargetVersion, flags}; use crate::source_kind::SourceKind; -use crate::{directives, fs, Locator}; +use crate::{Locator, directives, fs, warn_user_once}; + +pub(crate) mod float; pub struct LinterResult { /// A collection of diagnostic messages generated by the linter. - pub messages: Vec, + pub diagnostics: Vec, /// Flag indicating that the parsed source code does not contain any /// [`ParseError`]s has_valid_syntax: bool, @@ -85,7 +85,55 @@ impl LinterResult { } } -pub type FixTable = FxHashMap; +#[derive(Debug, Default, PartialEq)] +struct FixCount { + rule_name: &'static str, + count: usize, +} + +/// A mapping from a noqa code to the corresponding lint name and a count of applied fixes. +#[derive(Debug, Default, PartialEq)] +pub struct FixTable(hashbrown::HashMap); + +impl FixTable { + pub fn counts(&self) -> impl Iterator { + self.0.values().map(|fc| fc.count) + } + + pub fn entry<'a>(&'a mut self, code: &'a SecondaryCode) -> FixTableEntry<'a> { + FixTableEntry(self.0.entry_ref(code)) + } + + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .map(|(code, FixCount { rule_name, count })| (code, *rule_name, *count)) + } + + pub fn keys(&self) -> impl Iterator { + self.0.keys() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +pub struct FixTableEntry<'a>( + hashbrown::hash_map::EntryRef<'a, 'a, SecondaryCode, SecondaryCode, FixCount, FxBuildHasher>, +); + +impl<'a> FixTableEntry<'a> { + pub fn or_default(self, rule_name: &'static str) -> &'a mut usize { + &mut (self + .0 + .or_insert(FixCount { + rule_name, + count: 0, + }) + .count) + } +} pub struct FixerResult<'a> { /// The result returned by the linter, after applying any fixes. @@ -96,8 +144,8 @@ pub struct FixerResult<'a> { pub fixed: FixTable, } -/// Generate [`Message`]s from the source code contents at the given `Path`. -#[allow(clippy::too_many_arguments)] +/// Generate [`Diagnostic`]s from the source code contents at the given `Path`. +#[expect(clippy::too_many_arguments)] pub fn check_path( path: &Path, package: Option>, @@ -110,10 +158,10 @@ pub fn check_path( source_kind: &SourceKind, source_type: PySourceType, parsed: &Parsed, - target_version: PythonVersion, -) -> Vec { + target_version: TargetVersion, +) -> Vec { // Aggregate all diagnostics. - let mut diagnostics = vec![]; + let mut context = LintContext::new(path, locator.contents(), settings); // Aggregate all semantic syntax errors. let mut semantic_syntax_errors = vec![]; @@ -123,19 +171,18 @@ pub fn check_path( // Collect doc lines. This requires a rare mix of tokens (for comments) and AST // (for docstrings), which demands special-casing at this level. - let use_doc_lines = settings.rules.enabled(Rule::DocLineTooLong); + let use_doc_lines = context.is_rule_enabled(Rule::DocLineTooLong); let mut doc_lines = vec![]; if use_doc_lines { doc_lines.extend(doc_lines_from_tokens(tokens)); } // Run the token-based rules. - if settings - .rules - .iter_enabled() + if context + .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_tokens()) { - diagnostics.extend(check_tokens( + check_tokens( tokens, path, locator, @@ -144,34 +191,34 @@ pub fn check_path( settings, source_type, source_kind.as_ipy_notebook().map(Notebook::cell_offsets), - )); + &mut context, + ); } // Run the filesystem-based rules. - if settings - .rules - .iter_enabled() + if context + .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_filesystem()) { - diagnostics.extend(check_file_path( + check_file_path( path, package, locator, comment_ranges, settings, - target_version, - )); + target_version.linter_version(), + &context, + ); } // Run the logical line-based rules. - if settings - .rules - .iter_enabled() + if context + .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_logical_lines()) { - diagnostics.extend(crate::checkers::logical_lines::check_logical_lines( - tokens, locator, indexer, stylist, settings, - )); + crate::checkers::logical_lines::check_logical_lines( + tokens, locator, indexer, stylist, settings, &context, + ); } // Run the AST-based rules only if there are no syntax errors. @@ -179,7 +226,7 @@ pub fn check_path( 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( + semantic_syntax_errors.extend(check_ast( parsed, locator, stylist, @@ -193,18 +240,16 @@ pub fn check_path( cell_offsets, notebook_index, target_version, - ); - diagnostics.extend(new_diagnostics); - semantic_syntax_errors.extend(new_semantic_syntax_errors); + &context, + )); let use_imports = !directives.isort.skip_file - && settings - .rules - .iter_enabled() + && context + .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_imports()); if use_imports || use_doc_lines { if use_imports { - let import_diagnostics = check_imports( + check_imports( parsed, locator, indexer, @@ -214,10 +259,9 @@ pub fn check_path( package, source_type, cell_offsets, - target_version, + target_version.linter_version(), + &context, ); - - diagnostics.extend(import_diagnostics); } if use_doc_lines { doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator)); @@ -232,148 +276,124 @@ pub fn check_path( } // Run the lines-based rules. - if settings - .rules - .iter_enabled() + if context + .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_physical_lines()) { - diagnostics.extend(check_physical_lines( - locator, stylist, indexer, &doc_lines, settings, - )); + check_physical_lines(locator, stylist, indexer, &doc_lines, settings, &context); } // Raise violations for internal test rules #[cfg(any(feature = "test-rules", test))] { for test_rule in TEST_RULES { - if !settings.rules.enabled(*test_rule) { + if !context.is_rule_enabled(*test_rule) { continue; } - let diagnostic = match test_rule { + match test_rule { Rule::StableTestRule => { - test_rules::StableTestRule::diagnostic(locator, comment_ranges) + test_rules::StableTestRule::diagnostic(locator, comment_ranges, &context); } Rule::StableTestRuleSafeFix => { - test_rules::StableTestRuleSafeFix::diagnostic(locator, comment_ranges) - } - Rule::StableTestRuleUnsafeFix => { - test_rules::StableTestRuleUnsafeFix::diagnostic(locator, comment_ranges) + test_rules::StableTestRuleSafeFix::diagnostic( + locator, + comment_ranges, + &context, + ); } + Rule::StableTestRuleUnsafeFix => test_rules::StableTestRuleUnsafeFix::diagnostic( + locator, + comment_ranges, + &context, + ), Rule::StableTestRuleDisplayOnlyFix => { - test_rules::StableTestRuleDisplayOnlyFix::diagnostic(locator, comment_ranges) + test_rules::StableTestRuleDisplayOnlyFix::diagnostic( + locator, + comment_ranges, + &context, + ); } Rule::PreviewTestRule => { - test_rules::PreviewTestRule::diagnostic(locator, comment_ranges) + test_rules::PreviewTestRule::diagnostic(locator, comment_ranges, &context); } Rule::DeprecatedTestRule => { - test_rules::DeprecatedTestRule::diagnostic(locator, comment_ranges) + test_rules::DeprecatedTestRule::diagnostic(locator, comment_ranges, &context); } Rule::AnotherDeprecatedTestRule => { - test_rules::AnotherDeprecatedTestRule::diagnostic(locator, comment_ranges) + test_rules::AnotherDeprecatedTestRule::diagnostic( + locator, + comment_ranges, + &context, + ); } Rule::RemovedTestRule => { - test_rules::RemovedTestRule::diagnostic(locator, comment_ranges) - } - Rule::AnotherRemovedTestRule => { - test_rules::AnotherRemovedTestRule::diagnostic(locator, comment_ranges) + test_rules::RemovedTestRule::diagnostic(locator, comment_ranges, &context); } + Rule::AnotherRemovedTestRule => test_rules::AnotherRemovedTestRule::diagnostic( + locator, + comment_ranges, + &context, + ), Rule::RedirectedToTestRule => { - test_rules::RedirectedToTestRule::diagnostic(locator, comment_ranges) - } - Rule::RedirectedFromTestRule => { - test_rules::RedirectedFromTestRule::diagnostic(locator, comment_ranges) + test_rules::RedirectedToTestRule::diagnostic(locator, comment_ranges, &context); } + Rule::RedirectedFromTestRule => test_rules::RedirectedFromTestRule::diagnostic( + locator, + comment_ranges, + &context, + ), Rule::RedirectedFromPrefixTestRule => { - test_rules::RedirectedFromPrefixTestRule::diagnostic(locator, comment_ranges) + test_rules::RedirectedFromPrefixTestRule::diagnostic( + locator, + comment_ranges, + &context, + ); } _ => unreachable!("All test rules must have an implementation"), - }; - if let Some(diagnostic) = diagnostic { - diagnostics.push(diagnostic); } } } - // Ignore diagnostics based on per-file-ignores. - let per_file_ignores = if (!diagnostics.is_empty() - || settings - .rules - .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_noqa())) - && !settings.per_file_ignores.is_empty() - { - fs::ignores_from_path(path, &settings.per_file_ignores) - } else { - RuleSet::empty() - }; - if !per_file_ignores.is_empty() { - diagnostics.retain(|diagnostic| !per_file_ignores.contains(diagnostic.kind.rule())); - } - // Enforce `noqa` directives. if noqa.is_enabled() - || settings - .rules - .iter_enabled() + || context + .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_noqa()) { let ignored = check_noqa( - &mut diagnostics, + &mut context, path, locator, comment_ranges, &directives.noqa_line_for, parsed.has_valid_syntax(), - &per_file_ignores, settings, ); if noqa.is_enabled() { for index in ignored.iter().rev() { - diagnostics.swap_remove(*index); + context.as_mut_vec().swap_remove(*index); } } } - if parsed.has_valid_syntax() { - // Remove fixes for any rules marked as unfixable. - for diagnostic in &mut diagnostics { - if !settings.rules.should_fix(diagnostic.kind.rule()) { - diagnostic.fix = None; - } - } + let (mut diagnostics, source_file) = context.into_parts(); - // Update fix applicability to account for overrides - if !settings.fix_safety.is_empty() { - for diagnostic in &mut diagnostics { - if let Some(fix) = diagnostic.fix.take() { - let fixed_applicability = settings - .fix_safety - .resolve_applicability(diagnostic.kind.rule(), fix.applicability()); - diagnostic.set_fix(fix.with_applicability(fixed_applicability)); - } - } - } - } else { + if !parsed.has_valid_syntax() { // Avoid fixing in case the source code contains syntax errors. for diagnostic in &mut diagnostics { - diagnostic.fix = None; + diagnostic.remove_fix(); } } - let syntax_errors = if settings.preview.is_enabled() { - parsed.unsupported_syntax_errors() - } else { - &[] - }; + let syntax_errors = parsed.unsupported_syntax_errors(); diagnostics_to_messages( diagnostics, parsed.errors(), syntax_errors, &semantic_syntax_errors, - path, - locator, directives, + &source_file, ) } @@ -389,7 +409,7 @@ pub fn add_noqa_to_path( ) -> Result { // Parse once. let target_version = settings.resolve_target_version(path); - let parsed = parse_unchecked_source(source_kind, source_type, target_version); + let parsed = parse_unchecked_source(source_kind, source_type, target_version.parser_version()); // Map row and column locations to byte slices (lazily). let locator = Locator::new(source_kind.source_code()); @@ -409,7 +429,7 @@ pub fn add_noqa_to_path( ); // Generate diagnostics, ignoring any existing `noqa` directives. - let messages = check_path( + let diagnostics = check_path( path, package, &locator, @@ -428,7 +448,7 @@ pub fn add_noqa_to_path( // TODO(dhruvmanila): Add support for Jupyter Notebooks add_noqa( path, - &messages, + &diagnostics, &locator, indexer.comment_ranges(), &settings.external, @@ -437,8 +457,7 @@ pub fn add_noqa_to_path( ) } -/// Generate a [`Message`] for each [`Diagnostic`] triggered by the given source -/// code. +/// Generate a [`Diagnostic`] for each diagnostic triggered by the given source code. pub fn lint_only( path: &Path, package: Option>, @@ -449,7 +468,16 @@ pub fn lint_only( source: ParseSource, ) -> LinterResult { let target_version = settings.resolve_target_version(path); - let parsed = source.into_parsed(source_kind, source_type, target_version); + + if matches!(target_version, TargetVersion(Some(PythonVersion::PY314))) + && !is_py314_support_enabled(settings) + { + warn_user_once!( + "Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning." + ); + } + + let parsed = source.into_parsed(source_kind, source_type, target_version.parser_version()); // Map row and column locations to byte slices (lazily). let locator = Locator::new(source_kind.source_code()); @@ -469,7 +497,7 @@ pub fn lint_only( ); // Generate diagnostics. - let messages = check_path( + let diagnostics = check_path( path, package, &locator, @@ -486,53 +514,47 @@ pub fn lint_only( LinterResult { has_valid_syntax: parsed.has_valid_syntax(), - has_no_syntax_errors: !messages.iter().any(Message::is_syntax_error), - messages, + has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_invalid_syntax), + diagnostics, } } -/// Convert from diagnostics to messages. +/// Convert various error types into a single collection of diagnostics. +/// +/// Also use `directives` to attach noqa offsets to lint diagnostics. fn diagnostics_to_messages( diagnostics: Vec, parse_errors: &[ParseError], unsupported_syntax_errors: &[UnsupportedSyntaxError], semantic_syntax_errors: &[SemanticSyntaxError], - path: &Path, - locator: &Locator, directives: &Directives, -) -> Vec { - let file = LazyCell::new(|| { - let mut builder = - SourceFileBuilder::new(path.to_string_lossy().as_ref(), locator.contents()); - - if let Some(line_index) = locator.line_index() { - builder.set_line_index(line_index.clone()); - } - - builder.finish() - }); - + source_file: &SourceFile, +) -> Vec { parse_errors .iter() - .map(|parse_error| Message::from_parse_error(parse_error, locator, file.deref().clone())) + .map(|parse_error| { + create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error) + }) .chain(unsupported_syntax_errors.iter().map(|syntax_error| { - Message::from_unsupported_syntax_error(syntax_error, file.deref().clone()) + create_syntax_error_diagnostic(source_file.clone(), syntax_error, syntax_error) })) .chain( semantic_syntax_errors .iter() - .map(|error| Message::from_semantic_syntax_error(error, file.deref().clone())), + .map(|error| create_syntax_error_diagnostic(source_file.clone(), error, error)), ) - .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) + .chain(diagnostics.into_iter().map(|mut diagnostic| { + let noqa_offset = directives + .noqa_line_for + .resolve(diagnostic.expect_range().start()); + diagnostic.set_noqa_offset(noqa_offset); + diagnostic })) .collect() } /// Generate `Diagnostic`s from source code content, iteratively fixing /// until stable. -#[allow(clippy::too_many_arguments)] pub fn lint_fix<'a>( path: &Path, package: Option>, @@ -545,7 +567,7 @@ pub fn lint_fix<'a>( let mut transformed = Cow::Borrowed(source_kind); // Track the number of fixed errors across iterations. - let mut fixed = FxHashMap::default(); + let mut fixed = FixTable::default(); // As an escape hatch, bail after 100 iterations. let mut iterations = 0; @@ -558,10 +580,19 @@ pub fn lint_fix<'a>( let target_version = settings.resolve_target_version(path); + if matches!(target_version, TargetVersion(Some(PythonVersion::PY314))) + && !is_py314_support_enabled(settings) + { + warn_user_once!( + "Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning." + ); + } + // Continuously fix until the source code stabilizes. loop { // Parse once. - let parsed = parse_unchecked_source(&transformed, source_type, target_version); + let parsed = + parse_unchecked_source(&transformed, source_type, target_version.parser_version()); // Map row and column locations to byte slices (lazily). let locator = Locator::new(transformed.source_code()); @@ -581,7 +612,7 @@ pub fn lint_fix<'a>( ); // Generate diagnostics. - let messages = check_path( + let diagnostics = check_path( path, package, &locator, @@ -598,19 +629,14 @@ pub fn lint_fix<'a>( if iterations == 0 { has_valid_syntax = parsed.has_valid_syntax(); - has_no_syntax_errors = !messages.iter().any(Message::is_syntax_error); + has_no_syntax_errors = !diagnostics.iter().any(Diagnostic::is_invalid_syntax); } 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_syntax_errors { if let Some(error) = parsed.errors().first() { - report_fix_syntax_error( - path, - transformed.source_code(), - error, - fixed.keys().copied(), - ); + report_fix_syntax_error(path, transformed.source_code(), error, fixed.keys()); return Err(anyhow!("Fix introduced a syntax error")); } } @@ -621,12 +647,12 @@ pub fn lint_fix<'a>( code: fixed_contents, fixes: applied, source_map, - }) = fix_file(&messages, &locator, unsafe_fixes) + }) = fix_file(&diagnostics, &locator, unsafe_fixes) { if iterations < MAX_ITERATIONS { // Count the number of fixed errors. - for (rule, count) in applied { - *fixed.entry(rule).or_default() += count; + for (rule, name, count) in applied.iter() { + *fixed.entry(rule).or_default(name) += count; } transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map)); @@ -638,12 +664,12 @@ pub fn lint_fix<'a>( continue; } - report_failed_to_converge_error(path, transformed.source_code(), &messages); + report_failed_to_converge_error(path, transformed.source_code(), &diagnostics); } return Ok(FixerResult { result: LinterResult { - messages, + diagnostics, has_valid_syntax, has_no_syntax_errors, }, @@ -653,18 +679,16 @@ pub fn lint_fix<'a>( } } -fn collect_rule_codes(rules: impl IntoIterator) -> String { - rules - .into_iter() - .map(|rule| rule.noqa_code().to_string()) - .sorted_unstable() - .dedup() - .join(", ") +fn collect_rule_codes(rules: impl IntoIterator) -> String +where + T: Ord + PartialEq + std::fmt::Display, +{ + rules.into_iter().sorted_unstable().dedup().join(", ") } -#[allow(clippy::print_stderr)] -fn report_failed_to_converge_error(path: &Path, transformed: &str, messages: &[Message]) { - let codes = collect_rule_codes(messages.iter().filter_map(Message::rule)); +#[expect(clippy::print_stderr)] +fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[Diagnostic]) { + let codes = collect_rule_codes(diagnostics.iter().filter_map(Diagnostic::secondary_code)); if cfg!(debug_assertions) { eprintln!( "{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---", @@ -695,12 +719,12 @@ This indicates a bug in Ruff. If you could open an issue at: } } -#[allow(clippy::print_stderr)] -fn report_fix_syntax_error( +#[expect(clippy::print_stderr)] +fn report_fix_syntax_error<'a>( path: &Path, transformed: &str, error: &ParseError, - rules: impl IntoIterator, + rules: impl IntoIterator, ) { let codes = collect_rule_codes(rules); if cfg!(debug_assertions) { @@ -782,17 +806,17 @@ mod tests { use ruff_python_index::Indexer; use ruff_python_parser::ParseOptions; use ruff_python_trivia::textwrap::dedent; - use ruff_text_size::Ranged; use test_case::test_case; + use ruff_db::diagnostic::Diagnostic; use ruff_notebook::{Notebook, NotebookError}; use crate::linter::check_path; - use crate::message::Message; use crate::registry::Rule; + use crate::settings::LinterSettings; use crate::source_kind::SourceKind; - use crate::test::{assert_notebook_path, test_contents, TestedNotebook}; - use crate::{assert_messages, directives, settings, Locator}; + use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet}; + use crate::{Locator, assert_diagnostics, directives, settings}; /// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory. fn notebook_path(path: impl AsRef) -> std::path::PathBuf { @@ -804,15 +828,15 @@ mod tests { let actual = notebook_path("isort.ipynb"); let expected = notebook_path("isort_expected.ipynb"); let TestedNotebook { - messages, + diagnostics, source_notebook, .. } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnsortedImports), + &LinterSettings::for_rule(Rule::UnsortedImports), )?; - assert_messages!(messages, actual, source_notebook); + assert_diagnostics!(diagnostics, actual, source_notebook); Ok(()) } @@ -821,15 +845,15 @@ mod tests { let actual = notebook_path("ipy_escape_command.ipynb"); let expected = notebook_path("ipy_escape_command_expected.ipynb"); let TestedNotebook { - messages, + diagnostics, source_notebook, .. } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; - assert_messages!(messages, actual, source_notebook); + assert_diagnostics!(diagnostics, actual, source_notebook); Ok(()) } @@ -838,15 +862,15 @@ mod tests { let actual = notebook_path("unused_variable.ipynb"); let expected = notebook_path("unused_variable_expected.ipynb"); let TestedNotebook { - messages, + diagnostics, source_notebook, .. } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedVariable), + &LinterSettings::for_rule(Rule::UnusedVariable), )?; - assert_messages!(messages, actual, source_notebook); + assert_diagnostics!(diagnostics, actual, source_notebook); Ok(()) } @@ -855,15 +879,15 @@ mod tests { let actual = notebook_path("undefined_name.ipynb"); let expected = notebook_path("undefined_name.ipynb"); let TestedNotebook { - messages, + diagnostics, source_notebook, .. } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UndefinedName), + &LinterSettings::for_rule(Rule::UndefinedName), )?; - assert_messages!(messages, actual, source_notebook); + assert_diagnostics!(diagnostics, actual, source_notebook); Ok(()) } @@ -878,7 +902,7 @@ mod tests { } = assert_notebook_path( actual_path, &expected_path, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; let mut writer = Vec::new(); fixed_notebook.write(&mut writer)?; @@ -893,15 +917,15 @@ mod tests { let actual = notebook_path("vscode_language_id.ipynb"); let expected = notebook_path("vscode_language_id_expected.ipynb"); let TestedNotebook { - messages, + diagnostics, source_notebook, .. } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; - assert_messages!(messages, actual, source_notebook); + assert_diagnostics!(diagnostics, actual, source_notebook); Ok(()) } @@ -925,11 +949,11 @@ mod tests { #[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")] fn test_cell_id(path: &Path, has_id: bool) -> Result<()> { let source_notebook = Notebook::from_path(¬ebook_path(path))?; - let source_kind = SourceKind::IpyNotebook(source_notebook); + let source_kind = SourceKind::ipy_notebook(source_notebook); let (_, transformed) = test_contents( &source_kind, path, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), ); let linted_notebook = transformed.into_owned().expect_ipy_notebook(); let mut writer = Vec::new(); @@ -945,10 +969,7 @@ mod tests { /// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a /// file. - fn test_snippet_syntax_errors( - contents: &str, - settings: &settings::LinterSettings, - ) -> Vec { + fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec { let contents = dedent(contents); test_contents_syntax_errors( &SourceKind::Python(contents.to_string()), @@ -962,11 +983,12 @@ mod tests { fn test_contents_syntax_errors( source_kind: &SourceKind, path: &Path, - settings: &settings::LinterSettings, - ) -> Vec { + settings: &LinterSettings, + ) -> Vec { let source_type = PySourceType::from(path); + let target_version = settings.resolve_target_version(path); let options = - ParseOptions::from(source_type).with_target_version(settings.unresolved_target_version); + ParseOptions::from(source_type).with_target_version(target_version.parser_version()); let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options) .try_into_module() .expect("PySourceType always parses into a module"); @@ -979,7 +1001,7 @@ mod tests { &locator, &indexer, ); - let mut messages = check_path( + let mut diagnostics = check_path( path, None, &locator, @@ -991,26 +1013,29 @@ mod tests { source_kind, source_type, &parsed, - settings.unresolved_target_version, + target_version, ); - messages.sort_by_key(Ranged::start); - messages + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + diagnostics } #[test_case( - "error_on_310", + "async_in_sync_error_on_310", "async def f(): return [[x async for x in foo(n)] for n in range(3)]", - PythonVersion::PY310 + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" )] #[test_case( - "okay_on_311", + "async_in_sync_okay_on_311", "async def f(): return [[x async for x in foo(n)] for n in range(3)]", - PythonVersion::PY311 + PythonVersion::PY311, + "AsyncComprehensionOutsideAsyncFunction" )] #[test_case( - "okay_on_310", + "async_in_sync_okay_on_310", "async def test(): return [[x async for x in elements(n)] async for n in range(3)]", - PythonVersion::PY310 + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" )] #[test_case( "deferred_function_body", @@ -1020,24 +1045,192 @@ mod tests { def g(): ... [x async for x in foo()] ", - PythonVersion::PY310 + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" + )] + #[test_case( + "async_in_sync_false_positive", + "[x async for x in y]", + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" )] - fn test_async_comprehension_in_sync_comprehension( + #[test_case( + "rebound_comprehension", + "[x:= 2 for x in range(2)]", + PythonVersion::PY310, + "ReboundComprehensionVariable" + )] + #[test_case( + "duplicate_type_param", + "class C[T, T]: pass", + PythonVersion::PY312, + "DuplicateTypeParameter" + )] + #[test_case( + "multiple_case_assignment", + " + match x: + case [a, a]: + pass + case _: + pass + ", + PythonVersion::PY310, + "MultipleCaseAssignment" + )] + #[test_case( + "duplicate_match_key", + " + match x: + case {'key': 1, 'key': 2}: + pass + ", + PythonVersion::PY310, + "DuplicateMatchKey" + )] + #[test_case( + "duplicate_match_class_attribute", + " + match x: + case Point(x=1, x=2): + pass + ", + PythonVersion::PY310, + "DuplicateMatchClassAttribute" + )] + #[test_case( + "invalid_star_expression", + " + def func(): + return *x + ", + PythonVersion::PY310, + "InvalidStarExpression" + )] + #[test_case( + "invalid_star_expression_for", + " + for *x in range(10): + pass + ", + PythonVersion::PY310, + "InvalidStarExpression" + )] + #[test_case( + "invalid_star_expression_yield", + " + def func(): + yield *x + ", + PythonVersion::PY310, + "InvalidStarExpression" + )] + #[test_case( + "irrefutable_case_pattern_wildcard", + " + match value: + case _: + pass + case 1: + pass + ", + PythonVersion::PY310, + "IrrefutableCasePattern" + )] + #[test_case( + "irrefutable_case_pattern_capture", + " + match value: + case irrefutable: + pass + case 1: + pass + ", + PythonVersion::PY310, + "IrrefutableCasePattern" + )] + #[test_case( + "single_starred_assignment", + "*a = [1, 2, 3, 4]", + PythonVersion::PY310, + "SingleStarredAssignment" + )] + #[test_case( + "write_to_debug", + " + __debug__ = False + ", + PythonVersion::PY310, + "WriteToDebug" + )] + #[test_case( + "write_to_debug_in_function_param", + " + def process(__debug__): + pass + ", + PythonVersion::PY310, + "WriteToDebug" + )] + #[test_case( + "write_to_debug_class_type_param", + " + class Generic[__debug__]: + pass + ", + PythonVersion::PY312, + "WriteToDebug" + )] + #[test_case( + "invalid_expression_yield_in_type_param", + " + type X[T: (yield 1)] = int + ", + PythonVersion::PY312, + "InvalidExpression" + )] + #[test_case( + "invalid_expression_yield_in_type_alias", + " + type Y = (yield 1) + ", + PythonVersion::PY312, + "InvalidExpression" + )] + #[test_case( + "invalid_expression_walrus_in_return_annotation", + " + def f[T](x: int) -> (y := 3): return x + ", + PythonVersion::PY312, + "InvalidExpression" + )] + #[test_case( + "invalid_expression_yield_from_in_base_class", + " + class C[T]((yield from [object])): + pass + ", + PythonVersion::PY312, + "InvalidExpression" + )] + fn test_semantic_errors( name: &str, contents: &str, python_version: PythonVersion, + error_type: &str, ) { - let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}"); - let messages = test_snippet_syntax_errors( + let snapshot = format!("semantic_syntax_error_{error_type}_{name}_{python_version}"); + let diagnostics = test_snippet_syntax_errors( contents, - &settings::LinterSettings { + &LinterSettings { rules: settings::rule_table::RuleTable::empty(), - unresolved_target_version: python_version, + unresolved_target_version: python_version.into(), preview: settings::types::PreviewMode::Enabled, ..Default::default() }, ); - assert_messages!(snapshot, messages); + assert_diagnostics!(snapshot, diagnostics); } #[test_case(PythonVersion::PY310)] @@ -1046,33 +1239,40 @@ mod tests { let snapshot = format!("async_comprehension_in_sync_comprehension_notebook_{python_version}"); let path = Path::new("resources/test/fixtures/syntax_errors/async_comprehension.ipynb"); - let messages = test_contents_syntax_errors( - &SourceKind::IpyNotebook(Notebook::from_path(path)?), + let diagnostics = test_contents_syntax_errors( + &SourceKind::ipy_notebook(Notebook::from_path(path)?), path, - &settings::LinterSettings { - unresolved_target_version: python_version, + &LinterSettings { + unresolved_target_version: python_version.into(), rules: settings::rule_table::RuleTable::empty(), preview: settings::types::PreviewMode::Enabled, ..Default::default() }, ); - assert_messages!(snapshot, messages); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } + #[test_case(Rule::LateFutureImport, Path::new("late_future_import.py"))] #[test_case(Rule::YieldOutsideFunction, Path::new("yield_scope.py"))] #[test_case(Rule::ReturnOutsideFunction, Path::new("return_outside_function.py"))] + #[test_case( + Rule::LoadBeforeGlobalDeclaration, + Path::new("load_before_global_declaration.py") + )] + #[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.py"))] + #[test_case(Rule::AwaitOutsideAsync, Path::new("async_comprehension.py"))] fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let path = Path::new("resources/test/fixtures/syntax_errors").join(path); - let messages = test_contents_syntax_errors( + let diagnostics = test_contents_syntax_errors( &SourceKind::Python(std::fs::read_to_string(&path)?), &path, - &settings::LinterSettings::for_rule(rule), + &LinterSettings::for_rule(rule), ); insta::with_settings!({filters => vec![(r"\\", "/")]}, { - assert_messages!(snapshot, messages); + assert_diagnostics!(snapshot, diagnostics); }); Ok(()) @@ -1082,16 +1282,115 @@ mod tests { fn test_await_scope_notebook() -> Result<()> { let path = Path::new("resources/test/fixtures/syntax_errors/await_scope.ipynb"); let TestedNotebook { - messages, + diagnostics, source_notebook, .. } = assert_notebook_path( path, path, - &settings::LinterSettings::for_rule(Rule::YieldOutsideFunction), + &LinterSettings::for_rule(Rule::YieldOutsideFunction), )?; - assert_messages!(messages, path, source_notebook); + assert_diagnostics!(diagnostics, path, source_notebook); Ok(()) } + + const PYI019_EXAMPLE: &str = r#" + from typing import TypeVar + + T = TypeVar("T", bound="_NiceReprEnum") + + class C: + def __new__(cls: type[T]) -> T: + return cls + "#; + + #[test_case( + "pyi019_adds_typing_extensions", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY310.into(), + typing_extensions: true, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_does_not_add_typing_extensions", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY310.into(), + typing_extensions: false, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_adds_typing_without_extensions_disabled", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY311.into(), + typing_extensions: true, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_adds_typing_with_extensions_disabled", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY311.into(), + typing_extensions: false, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi034_disabled", + " + class C: + def __new__(cls) -> C: ... + ", + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 10 }.into(), + typing_extensions: false, + ..LinterSettings::for_rule(Rule::NonSelfReturnType) + } + )] + #[test_case( + "fast002_disabled", + r#" + from fastapi import Depends, FastAPI + + app = FastAPI() + + @app.get("/items/") + async def read_items(commons: dict = Depends(common_parameters)): + return commons + "#, + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 8 }.into(), + typing_extensions: false, + ..LinterSettings::for_rule(Rule::FastApiNonAnnotatedDependency) + } + )] + fn test_disabled_typing_extensions(name: &str, contents: &str, settings: &LinterSettings) { + let snapshot = format!("disabled_typing_extensions_{name}"); + let diagnostics = test_snippet(contents, settings); + assert_diagnostics!(snapshot, diagnostics); + } + + #[test_case( + "pyi026_disabled", + "Vector = list[float]", + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 9 }.into(), + typing_extensions: false, + ..LinterSettings::for_rule(Rule::TypeAliasWithoutAnnotation) + } + )] + fn test_disabled_typing_extensions_pyi(name: &str, contents: &str, settings: &LinterSettings) { + let snapshot = format!("disabled_typing_extensions_pyi_{name}"); + let path = Path::new(".pyi"); + let contents = dedent(contents); + let diagnostics = + test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0; + assert_diagnostics!(snapshot, diagnostics); + } } diff --git a/crates/ruff_linter/src/linter/float.rs b/crates/ruff_linter/src/linter/float.rs new file mode 100644 index 0000000000000..a61da05ea4946 --- /dev/null +++ b/crates/ruff_linter/src/linter/float.rs @@ -0,0 +1,39 @@ +use ruff_python_ast as ast; + +/// Checks if `expr` is a string literal that represents NaN. +/// E.g., `"NaN"`, `"-nAn"`, `"+nan"`, or even `" -NaN \n \t"` +/// Returns `None` if it's not. Else `Some("nan")`, `Some("-nan")`, or `Some("+nan")`. +pub(crate) fn as_nan_float_string_literal(expr: &ast::Expr) -> Option<&'static str> { + find_any_ignore_ascii_case(expr, &["nan", "+nan", "-nan"]) +} + +/// Returns `true` if `expr` is a string literal that represents a non-finite float. +/// E.g., `"NaN"`, "-inf", `"Infinity"`, or even `" +Inf \n \t"`. +/// Return `None` if it's not. Else the lowercased, trimmed string literal, +/// e.g., `Some("nan")`, `Some("-inf")`, or `Some("+infinity")`. +pub(crate) fn as_non_finite_float_string_literal(expr: &ast::Expr) -> Option<&'static str> { + find_any_ignore_ascii_case( + expr, + &[ + "nan", + "+nan", + "-nan", + "inf", + "+inf", + "-inf", + "infinity", + "+infinity", + "-infinity", + ], + ) +} + +fn find_any_ignore_ascii_case(expr: &ast::Expr, patterns: &[&'static str]) -> Option<&'static str> { + let value = &expr.as_string_literal_expr()?.value; + + let value = value.to_str().trim(); + patterns + .iter() + .find(|other| value.eq_ignore_ascii_case(other)) + .copied() +} diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index 5aeaced1b3137..afc212e6a68ca 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -2,7 +2,7 @@ use std::cell::OnceCell; -use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceCode, SourceLocation}; +use ruff_source_file::{LineColumn, LineIndex, LineRanges, OneIndexed, SourceCode}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; #[derive(Debug)] @@ -36,8 +36,8 @@ impl<'a> Locator<'a> { #[deprecated( note = "This is expensive, avoid using outside of the diagnostic phase. Prefer the other `Locator` methods instead." )] - pub fn compute_source_location(&self, offset: TextSize) -> SourceLocation { - self.to_source_code().source_location(offset) + pub fn compute_source_location(&self, offset: TextSize) -> LineColumn { + self.to_source_code().line_column(offset) } pub fn to_index(&self) -> &LineIndex { diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 8e07e0e84f7b2..5b293c797bab1 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -1,4 +1,4 @@ -use std::fmt::{Display, Formatter, Write}; +use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; @@ -6,10 +6,10 @@ use anyhow::Result; use colored::Colorize; use fern; use log::Level; -use ruff_python_parser::{ParseError, ParseErrorType}; +use ruff_python_parser::ParseError; use rustc_hash::FxHashSet; -use ruff_source_file::{LineIndex, OneIndexed, SourceCode, SourceLocation}; +use ruff_source_file::{LineColumn, LineIndex, OneIndexed, SourceCode}; use crate::fs; use crate::source_kind::SourceKind; @@ -109,7 +109,7 @@ pub enum LogLevel { } impl LogLevel { - #[allow(clippy::trivially_copy_pass_by_ref)] + #[expect(clippy::trivially_copy_pass_by_ref)] const fn level_filter(&self) -> log::LevelFilter { match self { LogLevel::Default => log::LevelFilter::Info, @@ -151,7 +151,7 @@ pub fn set_up_logging(level: LogLevel) -> Result<()> { }) .level(level.level_filter()) .level_for("globset", log::LevelFilter::Warn) - .level_for("red_knot_python_semantic", log::LevelFilter::Warn) + .level_for("ty_python_semantic", log::LevelFilter::Warn) .level_for("salsa", log::LevelFilter::Warn) .chain(std::io::stderr()) .apply()?; @@ -195,21 +195,21 @@ impl DisplayParseError { // Translate the byte offset to a location in the originating source. let location = if let Some(jupyter_index) = source_kind.as_ipy_notebook().map(Notebook::index) { - let source_location = source_code.source_location(error.location.start()); + let source_location = source_code.line_column(error.location.start()); ErrorLocation::Cell( jupyter_index - .cell(source_location.row) + .cell(source_location.line) .unwrap_or(OneIndexed::MIN), - SourceLocation { - row: jupyter_index - .cell_row(source_location.row) + LineColumn { + line: jupyter_index + .cell_row(source_location.line) .unwrap_or(OneIndexed::MIN), column: source_location.column, }, ) } else { - ErrorLocation::File(source_code.source_location(error.location.start())) + ErrorLocation::File(source_code.line_column(error.location.start())) }; Self { @@ -245,10 +245,10 @@ impl Display for DisplayParseError { write!( f, "{row}{colon}{column}{colon} {inner}", - row = location.row, + row = location.line, column = location.column, colon = ":".cyan(), - inner = &DisplayParseErrorType(&self.error.error) + inner = self.error.error ) } ErrorLocation::Cell(cell, location) => { @@ -256,74 +256,22 @@ impl Display for DisplayParseError { f, "{cell}{colon}{row}{colon}{column}{colon} {inner}", cell = cell, - row = location.row, + row = location.line, column = location.column, colon = ":".cyan(), - inner = &DisplayParseErrorType(&self.error.error) + inner = self.error.error ) } } } } -pub(crate) struct DisplayParseErrorType<'a>(&'a ParseErrorType); - -impl<'a> DisplayParseErrorType<'a> { - pub(crate) fn new(error: &'a ParseErrorType) -> Self { - Self(error) - } -} - -impl Display for DisplayParseErrorType<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", TruncateAtNewline(&self.0)) - } -} - #[derive(Debug)] enum ErrorLocation { /// The error occurred in a Python file. - File(SourceLocation), + File(LineColumn), /// The error occurred in a Jupyter cell. - Cell(OneIndexed, SourceLocation), -} - -/// Truncates the display text before the first newline character to avoid line breaks. -struct TruncateAtNewline<'a>(&'a dyn Display); - -impl Display for TruncateAtNewline<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - struct TruncateAdapter<'a> { - inner: &'a mut dyn Write, - after_new_line: bool, - } - - impl Write for TruncateAdapter<'_> { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - if self.after_new_line { - Ok(()) - } else { - if let Some(end) = s.find(['\n', '\r']) { - self.inner.write_str(&s[..end])?; - self.inner.write_str("\u{23ce}...")?; - self.after_new_line = true; - Ok(()) - } else { - self.inner.write_str(s) - } - } - } - } - - write!( - TruncateAdapter { - inner: f, - after_new_line: false, - }, - "{}", - self.0 - ) - } + Cell(OneIndexed, LineColumn), } #[cfg(test)] diff --git a/crates/ruff_linter/src/message/azure.rs b/crates/ruff_linter/src/message/azure.rs index c7d6049eac049..96433b61ef269 100644 --- a/crates/ruff_linter/src/message/azure.rs +++ b/crates/ruff_linter/src/message/azure.rs @@ -1,8 +1,9 @@ use std::io::Write; -use ruff_source_file::SourceLocation; +use ruff_db::diagnostic::Diagnostic; +use ruff_source_file::LineColumn; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; /// Generate error logging commands for Azure Pipelines format. /// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning) @@ -13,29 +14,29 @@ impl Emitter for AzureEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - for message in messages { - let location = if context.is_notebook(message.filename()) { + for diagnostic in diagnostics { + let filename = diagnostic.expect_ruff_filename(); + let location = if context.is_notebook(&filename) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - SourceLocation::default() + LineColumn::default() } else { - message.compute_start_location() + diagnostic.expect_ruff_start_location() }; writeln!( writer, "##vso[task.logissue type=error\ ;sourcepath={filename};linenumber={line};columnnumber={col};{code}]{body}", - filename = message.filename(), - line = location.row, + line = location.line, col = location.column, - code = message - .rule() - .map_or_else(String::new, |rule| format!("code={};", rule.noqa_code())), - body = message.body(), + code = diagnostic + .secondary_code() + .map_or_else(String::new, |code| format!("code={code};")), + body = diagnostic.body(), )?; } @@ -47,15 +48,15 @@ impl Emitter for AzureEmitter { mod tests { use insta::assert_snapshot; + use crate::message::AzureEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::AzureEmitter; #[test] fn output() { let mut emitter = AzureEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -63,7 +64,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = AzureEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/diff.rs b/crates/ruff_linter/src/message/diff.rs index 0e56578e177f9..289a4beffeb32 100644 --- a/crates/ruff_linter/src/message/diff.rs +++ b/crates/ruff_linter/src/message/diff.rs @@ -2,15 +2,14 @@ use std::fmt::{Display, Formatter}; use std::num::NonZeroUsize; use colored::{Color, ColoredString, Colorize, Styles}; - -use ruff_text_size::{Ranged, TextRange, TextSize}; use similar::{ChangeTag, TextDiff}; -use ruff_diagnostics::{Applicability, Fix}; +use ruff_db::diagnostic::Diagnostic; use ruff_source_file::{OneIndexed, SourceFile}; +use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::message::Message; use crate::text_helpers::ShowNonprinting; +use crate::{Applicability, Fix}; /// Renders a diff that shows the code fixes. /// @@ -22,13 +21,13 @@ use crate::text_helpers::ShowNonprinting; /// * Compute the diff from the [`Edit`] because diff calculation is expensive. pub(super) struct Diff<'a> { fix: &'a Fix, - source_code: &'a SourceFile, + source_code: SourceFile, } impl<'a> Diff<'a> { - pub(crate) fn from_message(message: &'a Message) -> Option> { + pub(crate) fn from_message(message: &'a Diagnostic) -> Option> { message.fix().map(|fix| Diff { - source_code: message.source_file(), + source_code: message.expect_ruff_source_file(), fix, }) } diff --git a/crates/ruff_linter/src/message/github.rs b/crates/ruff_linter/src/message/github.rs index 9fd0a5ee6b912..37a384d75ecd9 100644 --- a/crates/ruff_linter/src/message/github.rs +++ b/crates/ruff_linter/src/message/github.rs @@ -1,9 +1,10 @@ use std::io::Write; -use ruff_source_file::SourceLocation; +use ruff_db::diagnostic::Diagnostic; +use ruff_source_file::LineColumn; use crate::fs::relativize_path; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; /// Generate error workflow command in GitHub Actions format. /// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message) @@ -14,45 +15,48 @@ impl Emitter for GithubEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - for message in messages { - let source_location = message.compute_start_location(); - let location = if context.is_notebook(message.filename()) { + for diagnostic in diagnostics { + let source_location = diagnostic.expect_ruff_start_location(); + let filename = diagnostic.expect_ruff_filename(); + let location = if context.is_notebook(&filename) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - SourceLocation::default() + LineColumn::default() } else { - source_location.clone() + source_location }; - let end_location = message.compute_end_location(); + let end_location = diagnostic.expect_ruff_end_location(); write!( writer, "::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::", - code = message.rule().map_or_else(String::new, |rule| format!(" ({})", rule.noqa_code())), - file = message.filename(), - row = source_location.row, + code = diagnostic + .secondary_code() + .map_or_else(String::new, |code| format!(" ({code})")), + file = filename, + row = source_location.line, column = source_location.column, - end_row = end_location.row, + end_row = end_location.line, end_column = end_location.column, )?; write!( writer, "{path}:{row}:{column}:", - path = relativize_path(message.filename()), - row = location.row, + path = relativize_path(&filename), + row = location.line, column = location.column, )?; - if let Some(rule) = message.rule() { - write!(writer, " {}", rule.noqa_code())?; + if let Some(code) = diagnostic.secondary_code() { + write!(writer, " {code}")?; } - writeln!(writer, " {}", message.body())?; + writeln!(writer, " {}", diagnostic.body())?; } Ok(()) @@ -63,15 +67,15 @@ impl Emitter for GithubEmitter { mod tests { use insta::assert_snapshot; + use crate::message::GithubEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::GithubEmitter; #[test] fn output() { let mut emitter = GithubEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -79,7 +83,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = GithubEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/gitlab.rs b/crates/ruff_linter/src/message/gitlab.rs index 6433184ac5209..1fe27051b120e 100644 --- a/crates/ruff_linter/src/message/gitlab.rs +++ b/crates/ruff_linter/src/message/gitlab.rs @@ -1,5 +1,5 @@ -use std::collections::hash_map::DefaultHasher; use std::collections::HashSet; +use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::io::Write; @@ -7,8 +7,10 @@ use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; use serde_json::json; +use ruff_db::diagnostic::Diagnostic; + use crate::fs::{relativize_path, relativize_path_to}; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; /// Generate JSON with violations in GitLab CI format // https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool @@ -28,13 +30,13 @@ impl Emitter for GitlabEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { serde_json::to_writer_pretty( writer, &SerializedMessages { - messages, + diagnostics, context, project_dir: self.project_dir.as_deref(), }, @@ -45,7 +47,7 @@ impl Emitter for GitlabEmitter { } struct SerializedMessages<'a> { - messages: &'a [Message], + diagnostics: &'a [Diagnostic], context: &'a EmitterContext<'a>, project_dir: Option<&'a str>, } @@ -55,14 +57,15 @@ impl Serialize for SerializedMessages<'_> { where S: Serializer, { - let mut s = serializer.serialize_seq(Some(self.messages.len()))?; - let mut fingerprints = HashSet::::with_capacity(self.messages.len()); + let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?; + let mut fingerprints = HashSet::::with_capacity(self.diagnostics.len()); - for message in self.messages { - let start_location = message.compute_start_location(); - let end_location = message.compute_end_location(); + for diagnostic in self.diagnostics { + let start_location = diagnostic.expect_ruff_start_location(); + let end_location = diagnostic.expect_ruff_end_location(); - let lines = if self.context.is_notebook(message.filename()) { + let filename = diagnostic.expect_ruff_filename(); + let lines = if self.context.is_notebook(&filename) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback json!({ @@ -71,37 +74,34 @@ impl Serialize for SerializedMessages<'_> { }) } else { json!({ - "begin": start_location.row, - "end": end_location.row + "begin": start_location.line, + "end": end_location.line }) }; let path = self.project_dir.as_ref().map_or_else( - || relativize_path(message.filename()), - |project_dir| relativize_path_to(message.filename(), project_dir), + || relativize_path(&filename), + |project_dir| relativize_path_to(&filename, project_dir), ); - let mut message_fingerprint = fingerprint(message, &path, 0); + let mut message_fingerprint = fingerprint(diagnostic, &path, 0); // Make sure that we do not get a fingerprint that is already in use // by adding in the previously generated one. while fingerprints.contains(&message_fingerprint) { - message_fingerprint = fingerprint(message, &path, message_fingerprint); + message_fingerprint = fingerprint(diagnostic, &path, message_fingerprint); } fingerprints.insert(message_fingerprint); - let (description, check_name) = if let Some(rule) = message.rule() { - (message.body().to_string(), rule.noqa_code().to_string()) + let (description, check_name) = if let Some(code) = diagnostic.secondary_code() { + (diagnostic.body().to_string(), code.as_str()) } else { - let description = message.body(); + let description = diagnostic.body(); let description_without_prefix = description .strip_prefix("SyntaxError: ") .unwrap_or(description); - ( - description_without_prefix.to_string(), - "syntax-error".to_string(), - ) + (description_without_prefix.to_string(), "syntax-error") }; let value = json!({ @@ -123,7 +123,7 @@ impl Serialize for SerializedMessages<'_> { } /// Generate a unique fingerprint to identify a violation. -fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 { +fn fingerprint(message: &Diagnostic, project_path: &str, salt: u64) -> u64 { let mut hasher = DefaultHasher::new(); salt.hash(&mut hasher); @@ -137,15 +137,15 @@ fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 { mod tests { use insta::assert_snapshot; + use crate::message::GitlabEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::GitlabEmitter; #[test] fn output() { let mut emitter = GitlabEmitter::default(); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(redact_fingerprint(&content)); } @@ -153,7 +153,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = GitlabEmitter::default(); - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(redact_fingerprint(&content)); } diff --git a/crates/ruff_linter/src/message/grouped.rs b/crates/ruff_linter/src/message/grouped.rs index 1dfa5d15e6b2b..7e751348912be 100644 --- a/crates/ruff_linter/src/message/grouped.rs +++ b/crates/ruff_linter/src/message/grouped.rs @@ -4,15 +4,14 @@ use std::num::NonZeroUsize; use colored::Colorize; +use ruff_db::diagnostic::Diagnostic; use ruff_notebook::NotebookIndex; use ruff_source_file::OneIndexed; use crate::fs::relativize_path; use crate::message::diff::calculate_print_width; use crate::message::text::{MessageCodeFrame, RuleCodeAndBody}; -use crate::message::{ - group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, -}; +use crate::message::{Emitter, EmitterContext, MessageWithLocation, group_diagnostics_by_filename}; use crate::settings::types::UnsafeFixes; #[derive(Default)] @@ -46,10 +45,10 @@ impl Emitter for GroupedEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - for (filename, messages) in group_messages_by_filename(messages) { + for (filename, messages) in group_diagnostics_by_filename(diagnostics) { // Compute the maximum number of digits in the row and column, for messages in // this file. @@ -57,7 +56,7 @@ impl Emitter for GroupedEmitter { let mut max_column_length = OneIndexed::MIN; for message in &messages { - max_row_length = max_row_length.max(message.start_location.row); + max_row_length = max_row_length.max(message.start_location.line); max_column_length = max_column_length.max(message.start_location.column); } @@ -65,7 +64,7 @@ impl Emitter for GroupedEmitter { let column_length = calculate_print_width(max_column_length); // Print the filename. - writeln!(writer, "{}:", relativize_path(filename).underline())?; + writeln!(writer, "{}:", relativize_path(&*filename).underline())?; // Print each message. for message in messages { @@ -73,7 +72,7 @@ impl Emitter for GroupedEmitter { writer, "{}", DisplayGroupedMessage { - notebook_index: context.notebook_index(message.filename()), + notebook_index: context.notebook_index(&message.expect_ruff_filename()), message, show_fix_status: self.show_fix_status, unsafe_fixes: self.unsafe_fixes, @@ -115,8 +114,8 @@ impl Display for DisplayGroupedMessage<'_> { write!( f, " {row_padding}", - row_padding = - " ".repeat(self.row_length.get() - calculate_print_width(start_location.row).get()) + row_padding = " " + .repeat(self.row_length.get() - calculate_print_width(start_location.line).get()) )?; // Check if we're working on a jupyter notebook and translate positions with cell accordingly @@ -125,18 +124,18 @@ impl Display for DisplayGroupedMessage<'_> { f, "cell {cell}{sep}", cell = jupyter_index - .cell(start_location.row) + .cell(start_location.line) .unwrap_or(OneIndexed::MIN), sep = ":".cyan() )?; ( jupyter_index - .cell_row(start_location.row) + .cell_row(start_location.line) .unwrap_or(OneIndexed::MIN), start_location.column, ) } else { - (start_location.row, start_location.column) + (start_location.line, start_location.column) }; writeln!( @@ -205,16 +204,16 @@ impl std::fmt::Write for PadAdapter<'_> { mod tests { use insta::assert_snapshot; + use crate::message::GroupedEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::GroupedEmitter; use crate::settings::types::UnsafeFixes; #[test] fn default() { let mut emitter = GroupedEmitter::default(); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -222,7 +221,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = GroupedEmitter::default(); - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } @@ -230,7 +229,7 @@ mod tests { #[test] fn show_source() { let mut emitter = GroupedEmitter::default().with_show_source(true); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -240,7 +239,7 @@ mod tests { let mut emitter = GroupedEmitter::default() .with_show_fix_status(true) .with_show_source(true); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -251,7 +250,7 @@ mod tests { .with_show_fix_status(true) .with_show_source(true) .with_unsafe_fixes(UnsafeFixes::Enabled); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/json.rs b/crates/ruff_linter/src/message/json.rs index eaa968c167b5b..8fd2a20003ba5 100644 --- a/crates/ruff_linter/src/message/json.rs +++ b/crates/ruff_linter/src/message/json.rs @@ -2,14 +2,15 @@ use std::io::Write; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; -use ruff_diagnostics::Edit; +use ruff_db::diagnostic::Diagnostic; use ruff_notebook::NotebookIndex; -use ruff_source_file::{OneIndexed, SourceCode, SourceLocation}; +use ruff_source_file::{LineColumn, OneIndexed, SourceCode}; use ruff_text_size::Ranged; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::Edit; +use crate::message::{Emitter, EmitterContext}; #[derive(Default)] pub struct JsonEmitter; @@ -18,17 +19,23 @@ impl Emitter for JsonEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - serde_json::to_writer_pretty(writer, &ExpandedMessages { messages, context })?; + serde_json::to_writer_pretty( + writer, + &ExpandedMessages { + diagnostics, + context, + }, + )?; Ok(()) } } struct ExpandedMessages<'a> { - messages: &'a [Message], + diagnostics: &'a [Diagnostic], context: &'a EmitterContext<'a>, } @@ -37,9 +44,9 @@ impl Serialize for ExpandedMessages<'_> { where S: Serializer, { - let mut s = serializer.serialize_seq(Some(self.messages.len()))?; + let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?; - for message in self.messages { + for message in self.diagnostics { let value = message_to_json_value(message, self.context); s.serialize_element(&value)?; } @@ -48,9 +55,11 @@ impl Serialize for ExpandedMessages<'_> { } } -pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value { - let source_code = message.source_file().to_source_code(); - let notebook_index = context.notebook_index(message.filename()); +pub(crate) fn message_to_json_value(message: &Diagnostic, context: &EmitterContext) -> Value { + let source_file = message.expect_ruff_source_file(); + let source_code = source_file.to_source_code(); + let filename = message.expect_ruff_filename(); + let notebook_index = context.notebook_index(&filename); let fix = message.fix().map(|fix| { json!({ @@ -60,34 +69,42 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) }) }); - let mut start_location = source_code.source_location(message.start()); - let mut end_location = source_code.source_location(message.end()); + let mut start_location = source_code.line_column(message.expect_range().start()); + let mut end_location = source_code.line_column(message.expect_range().end()); let mut noqa_location = message .noqa_offset() - .map(|offset| source_code.source_location(offset)); + .map(|offset| source_code.line_column(offset)); let mut notebook_cell_index = None; if let Some(notebook_index) = notebook_index { notebook_cell_index = Some( notebook_index - .cell(start_location.row) + .cell(start_location.line) .unwrap_or(OneIndexed::MIN), ); - start_location = notebook_index.translate_location(&start_location); - end_location = notebook_index.translate_location(&end_location); - noqa_location = noqa_location.map(|location| notebook_index.translate_location(&location)); + start_location = notebook_index.translate_line_column(&start_location); + end_location = notebook_index.translate_line_column(&end_location); + noqa_location = + noqa_location.map(|location| notebook_index.translate_line_column(&location)); } json!({ - "code": message.rule().map(|rule| rule.noqa_code().to_string()), - "url": message.rule().and_then(|rule| rule.url()), + "code": message.secondary_code(), + "url": message.to_url(), "message": message.body(), "fix": fix, "cell": notebook_cell_index, - "location": start_location, - "end_location": end_location, - "filename": message.filename(), - "noqa_row": noqa_location.map(|location| location.row) + "location": location_to_json(start_location), + "end_location": location_to_json(end_location), + "filename": filename, + "noqa_row": noqa_location.map(|location| location.line) + }) +} + +fn location_to_json(location: LineColumn) -> serde_json::Value { + json!({ + "row": location.line, + "column": location.column }) } @@ -105,8 +122,8 @@ impl Serialize for ExpandedEdits<'_> { let mut s = serializer.serialize_seq(Some(self.edits.len()))?; for edit in self.edits { - let mut location = self.source_code.source_location(edit.start()); - let mut end_location = self.source_code.source_location(edit.end()); + let mut location = self.source_code.line_column(edit.start()); + let mut end_location = self.source_code.line_column(edit.end()); if let Some(notebook_index) = self.notebook_index { // There exists a newline between each cell's source code in the @@ -118,44 +135,44 @@ impl Serialize for ExpandedEdits<'_> { // If it does, we need to translate the end location to the last // character of the previous cell. match ( - notebook_index.cell(location.row), - notebook_index.cell(end_location.row), + notebook_index.cell(location.line), + notebook_index.cell(end_location.line), ) { (Some(start_cell), Some(end_cell)) if start_cell != end_cell => { debug_assert_eq!(end_location.column.get(), 1); - let prev_row = end_location.row.saturating_sub(1); - end_location = SourceLocation { - row: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), + let prev_row = end_location.line.saturating_sub(1); + end_location = LineColumn { + line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), column: self .source_code - .source_location(self.source_code.line_end_exclusive(prev_row)) + .line_column(self.source_code.line_end_exclusive(prev_row)) .column, }; } (Some(_), None) => { debug_assert_eq!(end_location.column.get(), 1); - let prev_row = end_location.row.saturating_sub(1); - end_location = SourceLocation { - row: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), + let prev_row = end_location.line.saturating_sub(1); + end_location = LineColumn { + line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), column: self .source_code - .source_location(self.source_code.line_end_exclusive(prev_row)) + .line_column(self.source_code.line_end_exclusive(prev_row)) .column, }; } _ => { - end_location = notebook_index.translate_location(&end_location); + end_location = notebook_index.translate_line_column(&end_location); } } - location = notebook_index.translate_location(&location); + location = notebook_index.translate_line_column(&location); } let value = json!({ "content": edit.content().unwrap_or_default(), - "location": location, - "end_location": end_location + "location": location_to_json(location), + "end_location": location_to_json(end_location) }); s.serialize_element(&value)?; @@ -169,16 +186,16 @@ impl Serialize for ExpandedEdits<'_> { mod tests { use insta::assert_snapshot; + use crate::message::JsonEmitter; use crate::message::tests::{ - capture_emitter_notebook_output, capture_emitter_output, create_messages, - create_notebook_messages, create_syntax_error_messages, + capture_emitter_notebook_output, capture_emitter_output, create_diagnostics, + create_notebook_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::JsonEmitter; #[test] fn output() { let mut emitter = JsonEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -186,7 +203,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = JsonEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } @@ -194,8 +211,9 @@ mod tests { #[test] fn notebook_output() { let mut emitter = JsonEmitter; - let (messages, notebook_indexes) = create_notebook_messages(); - let content = capture_emitter_notebook_output(&mut emitter, &messages, ¬ebook_indexes); + let (diagnostics, notebook_indexes) = create_notebook_diagnostics(); + let content = + capture_emitter_notebook_output(&mut emitter, &diagnostics, ¬ebook_indexes); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/json_lines.rs b/crates/ruff_linter/src/message/json_lines.rs index 24cf8821703c7..fc9bd8625a39e 100644 --- a/crates/ruff_linter/src/message/json_lines.rs +++ b/crates/ruff_linter/src/message/json_lines.rs @@ -1,7 +1,9 @@ use std::io::Write; +use ruff_db::diagnostic::Diagnostic; + use crate::message::json::message_to_json_value; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; #[derive(Default)] pub struct JsonLinesEmitter; @@ -10,11 +12,11 @@ impl Emitter for JsonLinesEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - for message in messages { - serde_json::to_writer(&mut *writer, &message_to_json_value(message, context))?; + for diagnostic in diagnostics { + serde_json::to_writer(&mut *writer, &message_to_json_value(diagnostic, context))?; writer.write_all(b"\n")?; } Ok(()) @@ -27,14 +29,14 @@ mod tests { use crate::message::json_lines::JsonLinesEmitter; use crate::message::tests::{ - capture_emitter_notebook_output, capture_emitter_output, create_messages, - create_notebook_messages, create_syntax_error_messages, + capture_emitter_notebook_output, capture_emitter_output, create_diagnostics, + create_notebook_diagnostics, create_syntax_error_diagnostics, }; #[test] fn output() { let mut emitter = JsonLinesEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -42,7 +44,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = JsonLinesEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } @@ -50,7 +52,7 @@ mod tests { #[test] fn notebook_output() { let mut emitter = JsonLinesEmitter; - let (messages, notebook_indexes) = create_notebook_messages(); + let (messages, notebook_indexes) = create_notebook_diagnostics(); let content = capture_emitter_notebook_output(&mut emitter, &messages, ¬ebook_indexes); assert_snapshot!(content); diff --git a/crates/ruff_linter/src/message/junit.rs b/crates/ruff_linter/src/message/junit.rs index 7c7e341809e9b..bec60249d07bc 100644 --- a/crates/ruff_linter/src/message/junit.rs +++ b/crates/ruff_linter/src/message/junit.rs @@ -3,11 +3,10 @@ use std::path::Path; use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString}; -use ruff_source_file::SourceLocation; +use ruff_db::diagnostic::Diagnostic; +use ruff_source_file::LineColumn; -use crate::message::{ - group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, -}; +use crate::message::{Emitter, EmitterContext, MessageWithLocation, group_diagnostics_by_filename}; #[derive(Default)] pub struct JunitEmitter; @@ -16,12 +15,12 @@ impl Emitter for JunitEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { let mut report = Report::new("ruff"); - if messages.is_empty() { + if diagnostics.is_empty() { let mut test_suite = TestSuite::new("ruff"); test_suite .extra @@ -31,8 +30,8 @@ impl Emitter for JunitEmitter { test_suite.add_test_case(case); report.add_test_suite(test_suite); } else { - for (filename, messages) in group_messages_by_filename(messages) { - let mut test_suite = TestSuite::new(filename); + for (filename, messages) in group_diagnostics_by_filename(diagnostics) { + let mut test_suite = TestSuite::new(&filename); test_suite .extra .insert(XmlString::new("package"), XmlString::new("org.ruff")); @@ -44,35 +43,35 @@ impl Emitter for JunitEmitter { } = message; let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure); status.set_message(message.body()); - let location = if context.is_notebook(message.filename()) { + let location = if context.is_notebook(&message.expect_ruff_filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - SourceLocation::default() + LineColumn::default() } else { start_location }; status.set_description(format!( "line {row}, col {col}, {body}", - row = location.row, + row = location.line, col = location.column, body = message.body() )); let mut case = TestCase::new( - if let Some(rule) = message.rule() { - format!("org.ruff.{}", rule.noqa_code()) + if let Some(code) = message.secondary_code() { + format!("org.ruff.{code}") } else { "org.ruff".to_string() }, status, ); - let file_path = Path::new(filename); + let file_path = Path::new(&*filename); let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); let classname = file_path.parent().unwrap().join(file_stem); case.set_classname(classname.to_str().unwrap()); case.extra.insert( XmlString::new("line"), - XmlString::new(location.row.to_string()), + XmlString::new(location.line.to_string()), ); case.extra.insert( XmlString::new("column"), @@ -95,15 +94,15 @@ impl Emitter for JunitEmitter { mod tests { use insta::assert_snapshot; + use crate::message::JunitEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::JunitEmitter; #[test] fn output() { let mut emitter = JunitEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -111,7 +110,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = JunitEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 7f16eeac54df2..2dc119e4408d0 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -1,9 +1,11 @@ -use std::cmp::Ordering; use std::collections::BTreeMap; +use std::fmt::Display; use std::io::Write; use std::ops::Deref; -use ruff_python_parser::semantic_errors::SemanticSyntaxError; +use ruff_db::diagnostic::{ + Annotation, Diagnostic, DiagnosticId, LintName, SecondaryCode, Severity, Span, +}; use rustc_hash::FxHashMap; pub use azure::AzureEmitter; @@ -15,17 +17,14 @@ pub use json_lines::JsonLinesEmitter; pub use junit::JunitEmitter; pub use pylint::PylintEmitter; pub use rdjson::RdjsonEmitter; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; use ruff_notebook::NotebookIndex; -use ruff_python_parser::{ParseError, UnsupportedSyntaxError}; -use ruff_source_file::{SourceFile, SourceLocation}; -use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use ruff_source_file::{LineColumn, SourceFile}; +use ruff_text_size::{Ranged, TextRange, TextSize}; pub use sarif::SarifEmitter; pub use text::TextEmitter; -use crate::logging::DisplayParseErrorType; -use crate::registry::{AsRule, Rule}; -use crate::Locator; +use crate::Fix; +use crate::registry::Rule; mod azure; mod diff; @@ -40,284 +39,112 @@ mod rdjson; mod sarif; mod text; -/// Message represents either a diagnostic message corresponding to a rule violation or a syntax -/// error message raised by the parser. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Message { - Diagnostic(DiagnosticMessage), - SyntaxError(SyntaxErrorMessage), -} - -/// A diagnostic message corresponding to a rule violation. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DiagnosticMessage { - pub kind: DiagnosticKind, - pub range: TextRange, - pub fix: Option, - pub parent: Option, - pub file: SourceFile, - pub noqa_offset: TextSize, -} - -/// A syntax error message raised by the parser. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SyntaxErrorMessage { - pub message: String, - pub range: TextRange, - pub file: SourceFile, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum MessageKind { - Diagnostic(Rule), - SyntaxError, -} - -impl MessageKind { - pub fn as_str(&self) -> &str { - match self { - MessageKind::Diagnostic(rule) => rule.as_ref(), - MessageKind::SyntaxError => "syntax-error", - } - } +/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff. +/// +/// This is almost identical to `ruff_db::diagnostic::create_syntax_error_diagnostic`, except the +/// `message` is stored as the primary diagnostic message instead of on the primary annotation, and +/// `SyntaxError: ` is prepended to the message. +/// +/// TODO(brent) These should be unified at some point, but we keep them separate for now to avoid a +/// ton of snapshot changes while combining ruff's diagnostic type with `Diagnostic`. +pub fn create_syntax_error_diagnostic( + span: impl Into, + message: impl std::fmt::Display, + range: impl Ranged, +) -> Diagnostic { + let mut diag = Diagnostic::new( + DiagnosticId::InvalidSyntax, + Severity::Error, + format_args!("SyntaxError: {message}"), + ); + let span = span.into().with_range(range.range()); + diag.annotate(Annotation::primary(span)); + diag } -impl Message { - /// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation. - pub fn from_diagnostic( - diagnostic: Diagnostic, - file: SourceFile, - noqa_offset: TextSize, - ) -> Message { - Message::Diagnostic(DiagnosticMessage { - range: diagnostic.range(), - kind: diagnostic.kind, - fix: diagnostic.fix, - parent: diagnostic.parent, - file, - noqa_offset, - }) - } +#[expect(clippy::too_many_arguments)] +pub fn create_lint_diagnostic( + body: B, + suggestion: Option, + range: TextRange, + fix: Option, + parent: Option, + file: SourceFile, + noqa_offset: Option, + rule: Rule, +) -> Diagnostic +where + B: Display, + S: Display, +{ + let mut diagnostic = Diagnostic::new( + DiagnosticId::Lint(LintName::of(rule.into())), + Severity::Error, + body, + ); - /// Create a [`Message`] from the given [`ParseError`]. - pub fn from_parse_error( - parse_error: &ParseError, - locator: &Locator, - file: SourceFile, - ) -> Message { - // Try to create a non-empty range so that the diagnostic can print a caret at the right - // position. This requires that we retrieve the next character, if any, and take its length - // to maintain char-boundaries. - let len = locator - .after(parse_error.location.start()) - .chars() - .next() - .map_or(TextSize::new(0), TextLen::text_len); - - Message::SyntaxError(SyntaxErrorMessage { - message: format!( - "SyntaxError: {}", - DisplayParseErrorType::new(&parse_error.error) - ), - range: TextRange::at(parse_error.location.start(), len), - file, - }) + if let Some(fix) = fix { + diagnostic.set_fix(fix); } - /// Create a [`Message`] from the given [`UnsupportedSyntaxError`]. - pub fn from_unsupported_syntax_error( - unsupported_syntax_error: &UnsupportedSyntaxError, - file: SourceFile, - ) -> Message { - Message::SyntaxError(SyntaxErrorMessage { - message: format!("SyntaxError: {unsupported_syntax_error}"), - range: unsupported_syntax_error.range, - file, - }) + if let Some(parent) = parent { + diagnostic.set_parent(parent); } - /// 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, - }) + if let Some(noqa_offset) = noqa_offset { + diagnostic.set_noqa_offset(noqa_offset); } - pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> { - match self { - Message::Diagnostic(m) => Some(m), - Message::SyntaxError(_) => None, - } + let span = Span::from(file).with_range(range); + let mut annotation = Annotation::primary(span); + if let Some(suggestion) = suggestion { + annotation = annotation.message(suggestion); } + diagnostic.annotate(annotation); - pub fn into_diagnostic_message(self) -> Option { - match self { - Message::Diagnostic(m) => Some(m), - Message::SyntaxError(_) => None, - } - } - - /// Returns `true` if `self` is a diagnostic message. - pub const fn is_diagnostic_message(&self) -> bool { - matches!(self, Message::Diagnostic(_)) - } - - /// Returns `true` if `self` is a syntax error message. - pub const fn is_syntax_error(&self) -> bool { - matches!(self, Message::SyntaxError(_)) - } + diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string())); - /// Returns a message kind. - pub fn kind(&self) -> MessageKind { - match self { - Message::Diagnostic(m) => MessageKind::Diagnostic(m.kind.rule()), - Message::SyntaxError(_) => MessageKind::SyntaxError, - } - } - - /// Returns the name used to represent the diagnostic. - pub fn name(&self) -> &str { - match self { - Message::Diagnostic(m) => &m.kind.name, - Message::SyntaxError(_) => "SyntaxError", - } - } - - /// Returns the message body to display to the user. - pub fn body(&self) -> &str { - match self { - Message::Diagnostic(m) => &m.kind.body, - Message::SyntaxError(m) => &m.message, - } - } - - /// Returns the fix suggestion for the violation. - pub fn suggestion(&self) -> Option<&str> { - match self { - Message::Diagnostic(m) => m.kind.suggestion.as_deref(), - Message::SyntaxError(_) => None, - } - } - - /// Returns the offset at which the `noqa` comment will be placed if it's a diagnostic message. - pub fn noqa_offset(&self) -> Option { - match self { - Message::Diagnostic(m) => Some(m.noqa_offset), - Message::SyntaxError(_) => None, - } - } - - /// Returns the [`Fix`] for the message, if there is any. - pub fn fix(&self) -> Option<&Fix> { - match self { - Message::Diagnostic(m) => m.fix.as_ref(), - Message::SyntaxError(_) => None, - } - } - - /// Returns `true` if the message contains a [`Fix`]. - pub fn fixable(&self) -> bool { - self.fix().is_some() - } - - /// Returns the [`Rule`] corresponding to the diagnostic message. - pub fn rule(&self) -> Option { - match self { - Message::Diagnostic(m) => Some(m.kind.rule()), - Message::SyntaxError(_) => None, - } - } - - /// Returns the filename for the message. - pub fn filename(&self) -> &str { - self.source_file().name() - } - - /// Computes the start source location for the message. - pub fn compute_start_location(&self) -> SourceLocation { - self.source_file() - .to_source_code() - .source_location(self.start()) - } - - /// Computes the end source location for the message. - pub fn compute_end_location(&self) -> SourceLocation { - self.source_file() - .to_source_code() - .source_location(self.end()) - } - - /// Returns the [`SourceFile`] which the message belongs to. - pub fn source_file(&self) -> &SourceFile { - match self { - Message::Diagnostic(m) => &m.file, - Message::SyntaxError(m) => &m.file, - } - } -} - -impl Ord for Message { - fn cmp(&self, other: &Self) -> Ordering { - (self.source_file(), self.start()).cmp(&(other.source_file(), other.start())) - } -} - -impl PartialOrd for Message { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ranged for Message { - fn range(&self) -> TextRange { - match self { - Message::Diagnostic(m) => m.range, - Message::SyntaxError(m) => m.range, - } - } + diagnostic } struct MessageWithLocation<'a> { - message: &'a Message, - start_location: SourceLocation, + message: &'a Diagnostic, + start_location: LineColumn, } impl Deref for MessageWithLocation<'_> { - type Target = Message; + type Target = Diagnostic; fn deref(&self) -> &Self::Target { self.message } } -fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec> { +fn group_diagnostics_by_filename( + diagnostics: &[Diagnostic], +) -> BTreeMap> { let mut grouped_messages = BTreeMap::default(); - for message in messages { + for diagnostic in diagnostics { grouped_messages - .entry(message.filename()) + .entry(diagnostic.expect_ruff_filename()) .or_insert_with(Vec::new) .push(MessageWithLocation { - message, - start_location: message.compute_start_location(), + message: diagnostic, + start_location: diagnostic.expect_ruff_start_location(), }); } grouped_messages } -/// Display format for a [`Message`]s. +/// Display format for [`Diagnostic`]s. /// -/// The emitter serializes a slice of [`Message`]'s and writes them to a [`Write`]. +/// The emitter serializes a slice of [`Diagnostic`]s and writes them to a [`Write`]. pub trait Emitter { - /// Serializes the `messages` and writes the output to `writer`. + /// Serializes the `diagnostics` and writes the output to `writer`. fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()>; } @@ -346,34 +173,36 @@ impl<'a> EmitterContext<'a> { mod tests { use rustc_hash::FxHashMap; - use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix}; + use ruff_db::diagnostic::Diagnostic; use ruff_notebook::NotebookIndex; - use ruff_python_parser::{parse_unchecked, Mode, ParseOptions}; + use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ruff_source_file::{OneIndexed, SourceFileBuilder}; - use ruff_text_size::{Ranged, TextRange, TextSize}; + use ruff_text_size::{TextRange, TextSize}; - use crate::message::{Emitter, EmitterContext, Message}; - use crate::Locator; + use crate::codes::Rule; + use crate::message::{Emitter, EmitterContext, create_lint_diagnostic}; + use crate::{Edit, Fix}; - pub(super) fn create_syntax_error_messages() -> Vec { + use super::create_syntax_error_diagnostic; + + pub(super) fn create_syntax_error_diagnostics() -> Vec { let source = r"from os import if call(foo def bar(): pass "; - let locator = Locator::new(source); let source_file = SourceFileBuilder::new("syntax_errors.py", source).finish(); parse_unchecked(source, ParseOptions::from(Mode::Module)) .errors() .iter() .map(|parse_error| { - Message::from_parse_error(parse_error, &locator, source_file.clone()) + create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error) }) .collect() } - pub(super) fn create_messages() -> Vec { + pub(super) fn create_diagnostics() -> Vec { let fib = r#"import os @@ -388,58 +217,57 @@ def fibonacci(n): return fibonacci(n - 1) + fibonacci(n - 2) "#; - let unused_import = Diagnostic::new( - DiagnosticKind { - name: "UnusedImport".to_string(), - body: "`os` imported but unused".to_string(), - suggestion: Some("Remove unused import: `os`".to_string()), - }, - TextRange::new(TextSize::from(7), TextSize::from(9)), - ) - .with_fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new( - TextSize::from(0), - TextSize::from(10), - )))); - let fib_source = SourceFileBuilder::new("fib.py", fib).finish(); - let unused_variable = Diagnostic::new( - DiagnosticKind { - name: "UnusedVariable".to_string(), - body: "Local variable `x` is assigned to but never used".to_string(), - suggestion: Some("Remove assignment to unused variable `x`".to_string()), - }, - TextRange::new(TextSize::from(94), TextSize::from(95)), - ) - .with_fix(Fix::unsafe_edit(Edit::deletion( - TextSize::from(94), - TextSize::from(99), - ))); + let unused_import_start = TextSize::from(7); + let unused_import = create_lint_diagnostic( + "`os` imported but unused", + Some("Remove unused import: `os`"), + TextRange::new(unused_import_start, TextSize::from(9)), + Some(Fix::unsafe_edit(Edit::range_deletion(TextRange::new( + TextSize::from(0), + TextSize::from(10), + )))), + None, + fib_source.clone(), + Some(unused_import_start), + Rule::UnusedImport, + ); + + let unused_variable_start = TextSize::from(94); + let unused_variable = create_lint_diagnostic( + "Local variable `x` is assigned to but never used", + Some("Remove assignment to unused variable `x`"), + TextRange::new(unused_variable_start, TextSize::from(95)), + Some(Fix::unsafe_edit(Edit::deletion( + TextSize::from(94), + TextSize::from(99), + ))), + None, + fib_source, + Some(unused_variable_start), + Rule::UnusedVariable, + ); let file_2 = r"if a == 1: pass"; - let undefined_name = Diagnostic::new( - DiagnosticKind { - name: "UndefinedName".to_string(), - body: "Undefined name `a`".to_string(), - suggestion: None, - }, - TextRange::new(TextSize::from(3), TextSize::from(4)), + let undefined_name_start = TextSize::from(3); + let undefined_name = create_lint_diagnostic( + "Undefined name `a`", + Option::<&'static str>::None, + TextRange::new(undefined_name_start, TextSize::from(4)), + None, + None, + SourceFileBuilder::new("undef.py", file_2).finish(), + Some(undefined_name_start), + Rule::UndefinedName, ); - let file_2_source = SourceFileBuilder::new("undef.py", file_2).finish(); - - let unused_import_start = unused_import.start(); - let unused_variable_start = unused_variable.start(); - let undefined_name_start = undefined_name.start(); - vec![ - Message::from_diagnostic(unused_import, fib_source.clone(), unused_import_start), - Message::from_diagnostic(unused_variable, fib_source, unused_variable_start), - Message::from_diagnostic(undefined_name, file_2_source, undefined_name_start), - ] + vec![unused_import, unused_variable, undefined_name] } - pub(super) fn create_notebook_messages() -> (Vec, FxHashMap) { + pub(super) fn create_notebook_diagnostics() + -> (Vec, FxHashMap) { let notebook = r"# cell 1 import os # cell 2 @@ -452,47 +280,53 @@ def foo(): x = 1 "; - let unused_import_os = Diagnostic::new( - DiagnosticKind { - name: "UnusedImport".to_string(), - body: "`os` imported but unused".to_string(), - suggestion: Some("Remove unused import: `os`".to_string()), - }, - TextRange::new(TextSize::from(16), TextSize::from(18)), - ) - .with_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new( - TextSize::from(9), - TextSize::from(19), - )))); - - let unused_import_math = Diagnostic::new( - DiagnosticKind { - name: "UnusedImport".to_string(), - body: "`math` imported but unused".to_string(), - suggestion: Some("Remove unused import: `math`".to_string()), - }, - TextRange::new(TextSize::from(35), TextSize::from(39)), - ) - .with_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new( - TextSize::from(28), - TextSize::from(40), - )))); - - let unused_variable = Diagnostic::new( - DiagnosticKind { - name: "UnusedVariable".to_string(), - body: "Local variable `x` is assigned to but never used".to_string(), - suggestion: Some("Remove assignment to unused variable `x`".to_string()), - }, - TextRange::new(TextSize::from(98), TextSize::from(99)), - ) - .with_fix(Fix::unsafe_edit(Edit::deletion( - TextSize::from(94), - TextSize::from(104), - ))); - let notebook_source = SourceFileBuilder::new("notebook.ipynb", notebook).finish(); + let unused_import_os_start = TextSize::from(16); + let unused_import_os = create_lint_diagnostic( + "`os` imported but unused", + Some("Remove unused import: `os`"), + TextRange::new(unused_import_os_start, TextSize::from(18)), + Some(Fix::safe_edit(Edit::range_deletion(TextRange::new( + TextSize::from(9), + TextSize::from(19), + )))), + None, + notebook_source.clone(), + Some(unused_import_os_start), + Rule::UnusedImport, + ); + + let unused_import_math_start = TextSize::from(35); + let unused_import_math = create_lint_diagnostic( + "`math` imported but unused", + Some("Remove unused import: `math`"), + TextRange::new(unused_import_math_start, TextSize::from(39)), + Some(Fix::safe_edit(Edit::range_deletion(TextRange::new( + TextSize::from(28), + TextSize::from(40), + )))), + None, + notebook_source.clone(), + Some(unused_import_math_start), + Rule::UnusedImport, + ); + + let unused_variable_start = TextSize::from(98); + let unused_variable = create_lint_diagnostic( + "Local variable `x` is assigned to but never used", + Some("Remove assignment to unused variable `x`"), + TextRange::new(unused_variable_start, TextSize::from(99)), + Some(Fix::unsafe_edit(Edit::deletion( + TextSize::from(94), + TextSize::from(104), + ))), + None, + notebook_source, + Some(unused_variable_start), + Rule::UnusedVariable, + ); + let mut notebook_indexes = FxHashMap::default(); notebook_indexes.insert( "notebook.ipynb".to_string(), @@ -524,48 +358,32 @@ def foo(): ), ); - let unused_import_os_start = unused_import_os.start(); - let unused_import_math_start = unused_import_math.start(); - let unused_variable_start = unused_variable.start(); - ( - vec![ - Message::from_diagnostic( - unused_import_os, - notebook_source.clone(), - unused_import_os_start, - ), - Message::from_diagnostic( - unused_import_math, - notebook_source.clone(), - unused_import_math_start, - ), - Message::from_diagnostic(unused_variable, notebook_source, unused_variable_start), - ], + vec![unused_import_os, unused_import_math, unused_variable], notebook_indexes, ) } pub(super) fn capture_emitter_output( emitter: &mut dyn Emitter, - messages: &[Message], + diagnostics: &[Diagnostic], ) -> String { let notebook_indexes = FxHashMap::default(); let context = EmitterContext::new(¬ebook_indexes); let mut output: Vec = Vec::new(); - emitter.emit(&mut output, messages, &context).unwrap(); + emitter.emit(&mut output, diagnostics, &context).unwrap(); String::from_utf8(output).expect("Output to be valid UTF-8") } pub(super) fn capture_emitter_notebook_output( emitter: &mut dyn Emitter, - messages: &[Message], + diagnostics: &[Diagnostic], notebook_indexes: &FxHashMap, ) -> String { let context = EmitterContext::new(notebook_indexes); let mut output: Vec = Vec::new(); - emitter.emit(&mut output, messages, &context).unwrap(); + emitter.emit(&mut output, diagnostics, &context).unwrap(); String::from_utf8(output).expect("Output to be valid UTF-8") } diff --git a/crates/ruff_linter/src/message/pylint.rs b/crates/ruff_linter/src/message/pylint.rs index 10b1f81f1076d..5e6cbe5a6a556 100644 --- a/crates/ruff_linter/src/message/pylint.rs +++ b/crates/ruff_linter/src/message/pylint.rs @@ -1,9 +1,10 @@ use std::io::Write; +use ruff_db::diagnostic::Diagnostic; use ruff_source_file::OneIndexed; use crate::fs::relativize_path; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; /// Generate violations in Pylint format. /// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter) @@ -14,32 +15,29 @@ impl Emitter for PylintEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - for message in messages { - let row = if context.is_notebook(message.filename()) { + for diagnostic in diagnostics { + let filename = diagnostic.expect_ruff_filename(); + let row = if context.is_notebook(&filename) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback OneIndexed::from_zero_indexed(0) } else { - message.compute_start_location().row + diagnostic.expect_ruff_start_location().line }; - let body = if let Some(rule) = message.rule() { - format!( - "[{code}] {body}", - code = rule.noqa_code(), - body = message.body() - ) + let body = if let Some(code) = diagnostic.secondary_code() { + format!("[{code}] {body}", body = diagnostic.body()) } else { - message.body().to_string() + diagnostic.body().to_string() }; writeln!( writer, "{path}:{row}: {body}", - path = relativize_path(message.filename()), + path = relativize_path(&filename), )?; } @@ -51,15 +49,15 @@ impl Emitter for PylintEmitter { mod tests { use insta::assert_snapshot; + use crate::message::PylintEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::PylintEmitter; #[test] fn output() { let mut emitter = PylintEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -67,7 +65,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = PylintEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/rdjson.rs b/crates/ruff_linter/src/message/rdjson.rs index 99b3fc481e257..8be2c56a54a9f 100644 --- a/crates/ruff_linter/src/message/rdjson.rs +++ b/crates/ruff_linter/src/message/rdjson.rs @@ -2,13 +2,14 @@ use std::io::Write; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; -use ruff_diagnostics::Edit; +use ruff_db::diagnostic::Diagnostic; use ruff_source_file::SourceCode; use ruff_text_size::Ranged; -use crate::message::{Emitter, EmitterContext, Message, SourceLocation}; +use crate::Edit; +use crate::message::{Emitter, EmitterContext, LineColumn}; #[derive(Default)] pub struct RdjsonEmitter; @@ -17,7 +18,7 @@ impl Emitter for RdjsonEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], _context: &EmitterContext, ) -> anyhow::Result<()> { serde_json::to_writer_pretty( @@ -28,7 +29,7 @@ impl Emitter for RdjsonEmitter { "url": "https://docs.astral.sh/ruff", }, "severity": "warning", - "diagnostics": &ExpandedMessages{ messages } + "diagnostics": &ExpandedMessages{ diagnostics } }), )?; @@ -37,7 +38,7 @@ impl Emitter for RdjsonEmitter { } struct ExpandedMessages<'a> { - messages: &'a [Message], + diagnostics: &'a [Diagnostic], } impl Serialize for ExpandedMessages<'_> { @@ -45,9 +46,9 @@ impl Serialize for ExpandedMessages<'_> { where S: Serializer, { - let mut s = serializer.serialize_seq(Some(self.messages.len()))?; + let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?; - for message in self.messages { + for message in self.diagnostics { let value = message_to_rdjson_value(message); s.serialize_element(&value)?; } @@ -56,22 +57,23 @@ impl Serialize for ExpandedMessages<'_> { } } -fn message_to_rdjson_value(message: &Message) -> Value { - let source_code = message.source_file().to_source_code(); +fn message_to_rdjson_value(message: &Diagnostic) -> Value { + let source_file = message.expect_ruff_source_file(); + let source_code = source_file.to_source_code(); - let start_location = source_code.source_location(message.start()); - let end_location = source_code.source_location(message.end()); + let start_location = source_code.line_column(message.expect_range().start()); + let end_location = source_code.line_column(message.expect_range().end()); if let Some(fix) = message.fix() { json!({ "message": message.body(), "location": { - "path": message.filename(), - "range": rdjson_range(&start_location, &end_location), + "path": message.expect_ruff_filename(), + "range": rdjson_range(start_location, end_location), }, "code": { - "value": message.rule().map(|rule| rule.noqa_code().to_string()), - "url": message.rule().and_then(|rule| rule.url()), + "value": message.secondary_code(), + "url": message.to_url(), }, "suggestions": rdjson_suggestions(fix.edits(), &source_code), }) @@ -79,12 +81,12 @@ fn message_to_rdjson_value(message: &Message) -> Value { json!({ "message": message.body(), "location": { - "path": message.filename(), - "range": rdjson_range(&start_location, &end_location), + "path": message.expect_ruff_filename(), + "range": rdjson_range(start_location, end_location), }, "code": { - "value": message.rule().map(|rule| rule.noqa_code().to_string()), - "url": message.rule().and_then(|rule| rule.url()), + "value": message.secondary_code(), + "url": message.to_url(), }, }) } @@ -95,11 +97,11 @@ fn rdjson_suggestions(edits: &[Edit], source_code: &SourceCode) -> Value { edits .iter() .map(|edit| { - let location = source_code.source_location(edit.start()); - let end_location = source_code.source_location(edit.end()); + let location = source_code.line_column(edit.start()); + let end_location = source_code.line_column(edit.end()); json!({ - "range": rdjson_range(&location, &end_location), + "range": rdjson_range(location, end_location), "text": edit.content().unwrap_or_default(), }) }) @@ -107,16 +109,10 @@ fn rdjson_suggestions(edits: &[Edit], source_code: &SourceCode) -> Value { ) } -fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value { +fn rdjson_range(start: LineColumn, end: LineColumn) -> Value { json!({ - "start": { - "line": start.row, - "column": start.column, - }, - "end": { - "line": end.row, - "column": end.column, - }, + "start": start, + "end": end, }) } @@ -124,15 +120,15 @@ fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value { mod tests { use insta::assert_snapshot; + use crate::message::RdjsonEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::RdjsonEmitter; #[test] fn output() { let mut emitter = RdjsonEmitter; - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -140,7 +136,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = RdjsonEmitter; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index a04bb441488e9..4b7b80e61ce45 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -5,13 +5,13 @@ use anyhow::Result; use serde::{Serialize, Serializer}; use serde_json::json; +use ruff_db::diagnostic::{Diagnostic, SecondaryCode}; use ruff_source_file::OneIndexed; -use crate::codes::Rule; +use crate::VERSION; use crate::fs::normalize_path; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; use crate::registry::{Linter, RuleNamespace}; -use crate::VERSION; pub struct SarifEmitter; @@ -19,17 +19,17 @@ impl Emitter for SarifEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], _context: &EmitterContext, ) -> Result<()> { - let results = messages + let results = diagnostics .iter() .map(SarifResult::from_message) .collect::>>()?; - let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.rule).collect(); + let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.code).collect(); let mut rules: Vec = unique_rules.into_iter().map(SarifRule::from).collect(); - rules.sort_by(|a, b| a.code.cmp(&b.code)); + rules.sort_by(|a, b| a.code.cmp(b.code)); let output = json!({ "$schema": "https://json.schemastore.org/sarif-2.1.0.json", @@ -54,17 +54,22 @@ impl Emitter for SarifEmitter { #[derive(Debug, Clone)] struct SarifRule<'a> { name: &'a str, - code: String, + code: &'a SecondaryCode, linter: &'a str, summary: &'a str, explanation: Option<&'a str>, url: Option, } -impl From for SarifRule<'_> { - fn from(rule: Rule) -> Self { - let code = rule.noqa_code().to_string(); - let (linter, _) = Linter::parse_code(&code).unwrap(); +impl<'a> From<&'a SecondaryCode> for SarifRule<'a> { + fn from(code: &'a SecondaryCode) -> Self { + // This is a manual re-implementation of Rule::from_code, but we also want the Linter. This + // avoids calling Linter::parse_code twice. + let (linter, suffix) = Linter::parse_code(code).unwrap(); + let rule = linter + .all_rules() + .find(|rule| rule.noqa_code().suffix() == suffix) + .expect("Expected a valid noqa code corresponding to a rule"); Self { name: rule.into(), code, @@ -105,8 +110,8 @@ impl Serialize for SarifRule<'_> { } #[derive(Debug)] -struct SarifResult { - rule: Option, +struct SarifResult<'a> { + code: Option<&'a SecondaryCode>, level: String, message: String, uri: String, @@ -116,46 +121,46 @@ struct SarifResult { end_column: OneIndexed, } -impl SarifResult { +impl<'a> SarifResult<'a> { #[cfg(not(target_arch = "wasm32"))] - fn from_message(message: &Message) -> Result { - let start_location = message.compute_start_location(); - let end_location = message.compute_end_location(); - let path = normalize_path(message.filename()); + fn from_message(message: &'a Diagnostic) -> Result { + let start_location = message.expect_ruff_start_location(); + let end_location = message.expect_ruff_end_location(); + let path = normalize_path(&*message.expect_ruff_filename()); Ok(Self { - rule: message.rule(), + code: message.secondary_code(), level: "error".to_string(), message: message.body().to_string(), uri: url::Url::from_file_path(&path) .map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))? .to_string(), - start_line: start_location.row, + start_line: start_location.line, start_column: start_location.column, - end_line: end_location.row, + end_line: end_location.line, end_column: end_location.column, }) } #[cfg(target_arch = "wasm32")] - #[allow(clippy::unnecessary_wraps)] - fn from_message(message: &Message) -> Result { - let start_location = message.compute_start_location(); - let end_location = message.compute_end_location(); - let path = normalize_path(message.filename()); + #[expect(clippy::unnecessary_wraps)] + fn from_message(message: &'a Diagnostic) -> Result { + let start_location = message.expect_ruff_start_location(); + let end_location = message.expect_ruff_end_location(); + let path = normalize_path(&*message.expect_ruff_filename()); Ok(Self { - rule: message.rule(), + code: message.secondary_code(), level: "error".to_string(), message: message.body().to_string(), uri: path.display().to_string(), - start_line: start_location.row, + start_line: start_location.line, start_column: start_location.column, - end_line: end_location.row, + end_line: end_location.line, end_column: end_location.column, }) } } -impl Serialize for SarifResult { +impl Serialize for SarifResult<'_> { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -178,7 +183,7 @@ impl Serialize for SarifResult { } } }], - "ruleId": self.rule.map(|rule| rule.noqa_code().to_string()), + "ruleId": self.code, }) .serialize(serializer) } @@ -186,14 +191,14 @@ impl Serialize for SarifResult { #[cfg(test)] mod tests { + use crate::message::SarifEmitter; use crate::message::tests::{ - capture_emitter_output, create_messages, create_syntax_error_messages, + capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::SarifEmitter; fn get_output() -> String { let mut emitter = SarifEmitter {}; - capture_emitter_output(&mut emitter, &create_messages()) + capture_emitter_output(&mut emitter, &create_diagnostics()) } #[test] @@ -205,7 +210,7 @@ mod tests { #[test] fn valid_syntax_error_json() { let mut emitter = SarifEmitter {}; - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); serde_json::from_str::(&content).unwrap(); } diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap index 373c7aa2ae589..224cdf75ae6d9 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap @@ -81,7 +81,7 @@ expression: value "rules": [ { "fullDescription": { - "text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n" + "text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n" }, "help": { "text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" @@ -119,7 +119,7 @@ expression: value }, { "fullDescription": { - "text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Options\n- `lint.dummy-variable-rgx`\n" + "text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Fix safety\n\nThis rule's fix is marked as unsafe because removing an unused variable assignment may\ndelete comments that are attached to the assignment.\n\n## Options\n- `lint.dummy-variable-rgx`\n" }, "help": { "text": "Local variable `{name}` is assigned to but never used" diff --git a/crates/ruff_linter/src/message/text.rs b/crates/ruff_linter/src/message/text.rs index 914f12a075268..0578fd7731577 100644 --- a/crates/ruff_linter/src/message/text.rs +++ b/crates/ruff_linter/src/message/text.rs @@ -6,16 +6,17 @@ use bitflags::bitflags; use colored::Colorize; use ruff_annotate_snippets::{Level, Renderer, Snippet}; +use ruff_db::diagnostic::{Diagnostic, SecondaryCode}; use ruff_notebook::NotebookIndex; -use ruff_source_file::{OneIndexed, SourceLocation}; -use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use ruff_source_file::{LineColumn, OneIndexed}; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use crate::Locator; use crate::fs::relativize_path; use crate::line_width::{IndentWidth, LineWidthBuilder}; use crate::message::diff::Diff; -use crate::message::{Emitter, EmitterContext, Message}; +use crate::message::{Emitter, EmitterContext}; use crate::settings::types::UnsafeFixes; -use crate::Locator; bitflags! { #[derive(Default)] @@ -66,19 +67,20 @@ impl Emitter for TextEmitter { fn emit( &mut self, writer: &mut dyn Write, - messages: &[Message], + diagnostics: &[Diagnostic], context: &EmitterContext, ) -> anyhow::Result<()> { - for message in messages { + for message in diagnostics { + let filename = message.expect_ruff_filename(); write!( writer, "{path}{sep}", - path = relativize_path(message.filename()).bold(), + path = relativize_path(&filename).bold(), sep = ":".cyan(), )?; - let start_location = message.compute_start_location(); - let notebook_index = context.notebook_index(message.filename()); + let start_location = message.expect_ruff_start_location(); + let notebook_index = context.notebook_index(&filename); // Check if we're working on a jupyter notebook and translate positions with cell accordingly let diagnostic_location = if let Some(notebook_index) = notebook_index { @@ -86,14 +88,14 @@ impl Emitter for TextEmitter { writer, "cell {cell}{sep}", cell = notebook_index - .cell(start_location.row) + .cell(start_location.line) .unwrap_or(OneIndexed::MIN), sep = ":".cyan(), )?; - SourceLocation { - row: notebook_index - .cell_row(start_location.row) + LineColumn { + line: notebook_index + .cell_row(start_location.line) .unwrap_or(OneIndexed::MIN), column: start_location.column, } @@ -104,7 +106,7 @@ impl Emitter for TextEmitter { writeln!( writer, "{row}{sep}{col}{sep} {code_and_body}", - row = diagnostic_location.row, + row = diagnostic_location.line, col = diagnostic_location.column, sep = ":".cyan(), code_and_body = RuleCodeAndBody { @@ -116,7 +118,7 @@ impl Emitter for TextEmitter { if self.flags.intersects(EmitterFlags::SHOW_SOURCE) { // The `0..0` range is used to highlight file-level diagnostics. - if message.range() != TextRange::default() { + if message.expect_range() != TextRange::default() { writeln!( writer, "{}", @@ -140,7 +142,7 @@ impl Emitter for TextEmitter { } pub(super) struct RuleCodeAndBody<'a> { - pub(crate) message: &'a Message, + pub(crate) message: &'a Diagnostic, pub(crate) show_fix_status: bool, pub(crate) unsafe_fixes: UnsafeFixes, } @@ -151,8 +153,8 @@ impl Display for RuleCodeAndBody<'_> { if let Some(fix) = self.message.fix() { // Do not display an indicator for inapplicable fixes if fix.applies(self.unsafe_fixes.required_applicability()) { - if let Some(rule) = self.message.rule() { - write!(f, "{} ", rule.noqa_code().to_string().red().bold())?; + if let Some(code) = self.message.secondary_code() { + write!(f, "{} ", code.red().bold())?; } return write!( f, @@ -164,11 +166,11 @@ impl Display for RuleCodeAndBody<'_> { } } - if let Some(rule) = self.message.rule() { + if let Some(code) = self.message.secondary_code() { write!( f, "{code} {body}", - code = rule.noqa_code().to_string().red().bold(), + code = code.red().bold(), body = self.message.body(), ) } else { @@ -178,7 +180,7 @@ impl Display for RuleCodeAndBody<'_> { } pub(super) struct MessageCodeFrame<'a> { - pub(crate) message: &'a Message, + pub(crate) message: &'a Diagnostic, pub(crate) notebook_index: Option<&'a NotebookIndex>, } @@ -191,9 +193,10 @@ impl Display for MessageCodeFrame<'_> { Vec::new() }; - let source_code = self.message.source_file().to_source_code(); + let source_file = self.message.expect_ruff_source_file(); + let source_code = source_file.to_source_code(); - let content_start_index = source_code.line_index(self.message.start()); + let content_start_index = source_code.line_index(self.message.expect_range().start()); let mut start_index = content_start_index.saturating_sub(2); // If we're working with a Jupyter Notebook, skip the lines which are @@ -216,7 +219,7 @@ impl Display for MessageCodeFrame<'_> { start_index = start_index.saturating_add(1); } - let content_end_index = source_code.line_index(self.message.end()); + let content_end_index = source_code.line_index(self.message.expect_range().end()); let mut end_index = content_end_index .saturating_add(2) .min(OneIndexed::from_zero_indexed(source_code.line_count())); @@ -247,14 +250,15 @@ impl Display for MessageCodeFrame<'_> { let source = replace_whitespace_and_unprintable( source_code.slice(TextRange::new(start_offset, end_offset)), - self.message.range() - start_offset, + self.message.expect_range() - start_offset, ) .fix_up_empty_spans_after_line_terminator(); let label = self .message - .rule() - .map_or_else(String::new, |rule| rule.noqa_code().to_string()); + .secondary_code() + .map(SecondaryCode::as_str) + .unwrap_or_default(); let line_start = self.notebook_index.map_or_else( || start_index.get(), @@ -268,7 +272,7 @@ impl Display for MessageCodeFrame<'_> { let span = usize::from(source.annotation_range.start()) ..usize::from(source.annotation_range.end()); - let annotation = Level::Error.span(span).label(&label); + let annotation = Level::Error.span(span).label(label); let snippet = Snippet::source(&source.text) .line_start(line_start) .annotation(annotation) @@ -406,17 +410,17 @@ impl<'a> SourceCode<'a> { mod tests { use insta::assert_snapshot; + use crate::message::TextEmitter; use crate::message::tests::{ - capture_emitter_notebook_output, capture_emitter_output, create_messages, - create_notebook_messages, create_syntax_error_messages, + capture_emitter_notebook_output, capture_emitter_output, create_diagnostics, + create_notebook_diagnostics, create_syntax_error_diagnostics, }; - use crate::message::TextEmitter; use crate::settings::types::UnsafeFixes; #[test] fn default() { let mut emitter = TextEmitter::default().with_show_source(true); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -426,7 +430,7 @@ mod tests { let mut emitter = TextEmitter::default() .with_show_fix_status(true) .with_show_source(true); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -437,7 +441,7 @@ mod tests { .with_show_fix_status(true) .with_show_source(true) .with_unsafe_fixes(UnsafeFixes::Enabled); - let content = capture_emitter_output(&mut emitter, &create_messages()); + let content = capture_emitter_output(&mut emitter, &create_diagnostics()); assert_snapshot!(content); } @@ -448,7 +452,7 @@ mod tests { .with_show_fix_status(true) .with_show_source(true) .with_unsafe_fixes(UnsafeFixes::Enabled); - let (messages, notebook_indexes) = create_notebook_messages(); + let (messages, notebook_indexes) = create_notebook_diagnostics(); let content = capture_emitter_notebook_output(&mut emitter, &messages, ¬ebook_indexes); assert_snapshot!(content); @@ -457,7 +461,7 @@ mod tests { #[test] fn syntax_errors() { let mut emitter = TextEmitter::default().with_show_source(true); - let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics()); assert_snapshot!(content); } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index fc00468992a58..7601b6ccb78eb 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -9,17 +9,17 @@ use anyhow::Result; use itertools::Itertools; use log::warn; -use ruff_diagnostics::Edit; -use ruff_python_trivia::{indentation_at_offset, CommentRanges, Cursor}; +use ruff_db::diagnostic::{Diagnostic, SecondaryCode}; +use ruff_python_trivia::{CommentRanges, Cursor, indentation_at_offset}; use ruff_source_file::{LineEnding, LineRanges}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use rustc_hash::FxHashSet; -use crate::codes::NoqaCode; +use crate::Edit; +use crate::Locator; use crate::fs::relativize_path; -use crate::message::Message; -use crate::registry::{AsRule, Rule, RuleSet}; +use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; -use crate::Locator; /// Generates an array of edits that matches the length of `messages`. /// Each potential edit in the array is paired, in order, with the associated diagnostic. @@ -28,7 +28,7 @@ use crate::Locator; /// simultaneously. pub fn generate_noqa_edits( path: &Path, - messages: &[Message], + diagnostics: &[Diagnostic], locator: &Locator, comment_ranges: &CommentRanges, external: &[String], @@ -38,7 +38,7 @@ pub fn generate_noqa_edits( let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let exemption = FileExemption::from(&file_directives); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); - let comments = find_noqa_comments(messages, locator, &exemption, &directives, noqa_line_for); + let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); build_noqa_edits_by_diagnostic(comments, locator, line_ending) } @@ -105,10 +105,9 @@ impl Codes<'_> { /// Returns `true` if the string list of `codes` includes `code` (or an alias /// thereof). - pub(crate) fn includes(&self, needle: Rule) -> bool { - let needle = needle.noqa_code(); + pub(crate) fn includes PartialEq<&'a str>>(&self, needle: &T) -> bool { self.iter() - .any(|code| needle == get_redirect_target(code.as_str()).unwrap_or(code.as_str())) + .any(|code| *needle == get_redirect_target(code.as_str()).unwrap_or(code.as_str())) } } @@ -140,48 +139,55 @@ pub(crate) fn rule_is_ignored( Ok(Some(NoqaLexerOutput { directive: Directive::Codes(codes), .. - })) => codes.includes(code), + })) => codes.includes(&code.noqa_code()), _ => false, } } /// A summary of the file-level exemption as extracted from [`FileNoqaDirectives`]. #[derive(Debug)] -pub(crate) enum FileExemption<'a> { +pub(crate) enum FileExemption { /// The file is exempt from all rules. - All(Vec<&'a NoqaCode>), + All(Vec), /// The file is exempt from the given rules. - Codes(Vec<&'a NoqaCode>), + Codes(Vec), } -impl FileExemption<'_> { +impl FileExemption { + /// Returns `true` if the file is exempt from the given rule, as identified by its noqa code. + pub(crate) fn contains_secondary_code(&self, needle: &SecondaryCode) -> bool { + match self { + FileExemption::All(_) => true, + FileExemption::Codes(codes) => codes.iter().any(|code| *needle == code.noqa_code()), + } + } + /// Returns `true` if the file is exempt from the given rule. pub(crate) fn includes(&self, needle: Rule) -> bool { - let needle = needle.noqa_code(); match self { FileExemption::All(_) => true, - FileExemption::Codes(codes) => codes.iter().any(|code| needle == **code), + FileExemption::Codes(codes) => codes.contains(&needle), } } /// Returns `true` if the file exemption lists the rule directly, rather than via a blanket /// exemption. pub(crate) fn enumerates(&self, needle: Rule) -> bool { - let needle = needle.noqa_code(); let codes = match self { FileExemption::All(codes) => codes, FileExemption::Codes(codes) => codes, }; - codes.iter().any(|code| needle == **code) + codes.contains(&needle) } } -impl<'a> From<&'a FileNoqaDirectives<'a>> for FileExemption<'a> { +impl<'a> From<&'a FileNoqaDirectives<'a>> for FileExemption { fn from(directives: &'a FileNoqaDirectives) -> Self { let codes = directives .lines() .iter() .flat_map(|line| &line.matches) + .copied() .collect(); if directives .lines() @@ -203,7 +209,7 @@ pub(crate) struct FileNoqaDirectiveLine<'a> { /// The blanket noqa directive. pub(crate) parsed_file_exemption: Directive<'a>, /// The codes that are ignored by the parsed exemptions. - pub(crate) matches: Vec, + pub(crate) matches: Vec, } impl Ranged for FileNoqaDirectiveLine<'_> { @@ -238,16 +244,20 @@ impl<'a> FileNoqaDirectives<'a> { let no_indentation_at_offset = indentation_at_offset(range.start(), locator.contents()).is_none(); if !warnings.is_empty() || no_indentation_at_offset { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); for warning in warnings { - warn!("Missing or joined rule code(s) at {path_display}:{line}: {warning}"); + warn!( + "Missing or joined rule code(s) at {path_display}:{line}: {warning}" + ); } if no_indentation_at_offset { - warn!("Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line. For line-level suppression, omit the `ruff:` prefix."); + warn!( + "Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line. For line-level suppression, omit the `ruff:` prefix." + ); continue; } } @@ -266,9 +276,9 @@ impl<'a> FileNoqaDirectives<'a> { if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) { - Some(rule.noqa_code()) + Some(rule) } else { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid rule code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); @@ -285,7 +295,7 @@ impl<'a> FileNoqaDirectives<'a> { }); } Err(err) => { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}"); @@ -299,6 +309,10 @@ impl<'a> FileNoqaDirectives<'a> { pub(crate) fn lines(&self) -> &[FileNoqaDirectiveLine] { &self.0 } + + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } } /// Output of lexing a `noqa` directive. @@ -703,7 +717,7 @@ impl Error for LexicalError {} /// Adds noqa comments to suppress all messages of a file. pub(crate) fn add_noqa( path: &Path, - messages: &[Message], + diagnostics: &[Diagnostic], locator: &Locator, comment_ranges: &CommentRanges, external: &[String], @@ -712,7 +726,7 @@ pub(crate) fn add_noqa( ) -> Result { let (count, output) = add_noqa_inner( path, - messages, + diagnostics, locator, comment_ranges, external, @@ -726,7 +740,7 @@ pub(crate) fn add_noqa( fn add_noqa_inner( path: &Path, - messages: &[Message], + diagnostics: &[Diagnostic], locator: &Locator, comment_ranges: &CommentRanges, external: &[String], @@ -741,7 +755,7 @@ fn add_noqa_inner( let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); - let comments = find_noqa_comments(messages, locator, &exemption, &directives, noqa_line_for); + let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); let edits = build_noqa_edits_by_line(comments, locator, line_ending); @@ -777,7 +791,7 @@ fn build_noqa_edits_by_diagnostic( if let Some(noqa_edit) = generate_noqa_edit( comment.directive, comment.line, - RuleSet::from_rule(comment.rule), + FxHashSet::from_iter([comment.code]), locator, line_ending, ) { @@ -813,7 +827,7 @@ fn build_noqa_edits_by_line<'a>( offset, matches .into_iter() - .map(|NoqaComment { rule, .. }| rule) + .map(|NoqaComment { code, .. }| code) .collect(), locator, line_ending, @@ -826,12 +840,12 @@ fn build_noqa_edits_by_line<'a>( struct NoqaComment<'a> { line: TextSize, - rule: Rule, + code: &'a SecondaryCode, directive: Option<&'a Directive<'a>>, } fn find_noqa_comments<'a>( - messages: &'a [Message], + diagnostics: &'a [Diagnostic], locator: &'a Locator, exemption: &'a FileExemption, directives: &'a NoqaDirectives, @@ -841,29 +855,19 @@ fn find_noqa_comments<'a>( let mut comments_by_line: Vec>> = vec![]; // Mark any non-ignored diagnostics. - for message in messages { - let Message::Diagnostic(diagnostic) = message else { + for message in diagnostics { + let Some(code) = message.secondary_code() else { comments_by_line.push(None); continue; }; - match &exemption { - FileExemption::All(_) => { - // If the file is exempted, don't add any noqa directives. - comments_by_line.push(None); - continue; - } - FileExemption::Codes(codes) => { - // If the diagnostic is ignored by a global exemption, don't add a noqa directive. - if codes.contains(&&diagnostic.kind.rule().noqa_code()) { - comments_by_line.push(None); - continue; - } - } + if exemption.contains_secondary_code(code) { + comments_by_line.push(None); + continue; } // Is the violation ignored by a `noqa` directive on the parent line? - if let Some(parent) = diagnostic.parent { + if let Some(parent) = message.parent() { if let Some(directive_line) = directives.find_line_with_directive(noqa_line_for.resolve(parent)) { @@ -873,7 +877,7 @@ fn find_noqa_comments<'a>( continue; } Directive::Codes(codes) => { - if codes.includes(diagnostic.kind.rule()) { + if codes.includes(code) { comments_by_line.push(None); continue; } @@ -882,9 +886,7 @@ fn find_noqa_comments<'a>( } } - let noqa_offset = noqa_line_for.resolve(diagnostic.range.start()); - - let rule = diagnostic.kind.rule(); + let noqa_offset = noqa_line_for.resolve(message.expect_range().start()); // Or ignored by the directive itself? if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { @@ -894,10 +896,10 @@ fn find_noqa_comments<'a>( continue; } directive @ Directive::Codes(codes) => { - if !codes.includes(rule) { + if !codes.includes(code) { comments_by_line.push(Some(NoqaComment { line: directive_line.start(), - rule, + code, directive: Some(directive), })); } @@ -909,7 +911,7 @@ fn find_noqa_comments<'a>( // There's no existing noqa directive that suppresses the diagnostic. comments_by_line.push(Some(NoqaComment { line: locator.line_start(noqa_offset), - rule, + code, directive: None, })); } @@ -919,7 +921,7 @@ fn find_noqa_comments<'a>( struct NoqaEdit<'a> { edit_range: TextRange, - rules: RuleSet, + noqa_codes: FxHashSet<&'a SecondaryCode>, codes: Option<&'a Codes<'a>>, line_ending: LineEnding, } @@ -938,18 +940,15 @@ impl NoqaEdit<'_> { Some(codes) => { push_codes( writer, - self.rules + self.noqa_codes .iter() - .map(|rule| rule.noqa_code().to_string()) - .chain(codes.iter().map(ToString::to_string)) + .map(|code| code.as_str()) + .chain(codes.iter().map(Code::as_str)) .sorted_unstable(), ); } None => { - push_codes( - writer, - self.rules.iter().map(|rule| rule.noqa_code().to_string()), - ); + push_codes(writer, self.noqa_codes.iter().sorted_unstable()); } } write!(writer, "{}", self.line_ending.as_str()).unwrap(); @@ -965,7 +964,7 @@ impl Ranged for NoqaEdit<'_> { fn generate_noqa_edit<'a>( directive: Option<&'a Directive>, offset: TextSize, - rules: RuleSet, + noqa_codes: FxHashSet<&'a SecondaryCode>, locator: &Locator, line_ending: LineEnding, ) -> Option> { @@ -994,7 +993,7 @@ fn generate_noqa_edit<'a>( Some(NoqaEdit { edit_range, - rules, + noqa_codes, codes, line_ending, }) @@ -1018,7 +1017,7 @@ pub(crate) struct NoqaDirectiveLine<'a> { /// The noqa directive. pub(crate) directive: Directive<'a>, /// The codes that are ignored by the directive. - pub(crate) matches: Vec, + pub(crate) matches: Vec, /// Whether the directive applies to `range.end`. pub(crate) includes_end: bool, } @@ -1053,11 +1052,13 @@ impl<'a> NoqaDirectives<'a> { directive, })) => { if !warnings.is_empty() { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); for warning in warnings { - warn!("Missing or joined rule code(s) at {path_display}:{line}: {warning}"); + warn!( + "Missing or joined rule code(s) at {path_display}:{line}: {warning}" + ); } } if let Directive::Codes(codes) = &directive { @@ -1073,10 +1074,12 @@ impl<'a> NoqaDirectives<'a> { ) .is_err() { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); - warn!("Invalid rule code provided to `# noqa` at {path_display}:{line}: {code}"); + warn!( + "Invalid rule code provided to `# noqa` at {path_display}:{line}: {code}" + ); } } } @@ -1091,7 +1094,7 @@ impl<'a> NoqaDirectives<'a> { }); } Err(err) => { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}"); @@ -1139,6 +1142,10 @@ impl<'a> NoqaDirectives<'a> { pub(crate) fn lines(&self) -> &[NoqaDirectiveLine] { &self.inner } + + pub(crate) fn is_empty(&self) -> bool { + self.inner.is_empty() + } } /// Remaps offsets falling into one of the ranges to instead check for a noqa comment on the @@ -1214,20 +1221,19 @@ mod tests { use insta::assert_debug_snapshot; - use ruff_diagnostics::{Diagnostic, Edit}; use ruff_python_trivia::CommentRanges; use ruff_source_file::{LineEnding, SourceFileBuilder}; - use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; + use ruff_text_size::{TextLen, TextRange, TextSize}; - use crate::message::Message; use crate::noqa::{ - add_noqa_inner, lex_codes, lex_file_exemption, lex_inline_noqa, Directive, LexicalError, - NoqaLexerOutput, NoqaMapping, + Directive, LexicalError, NoqaLexerOutput, NoqaMapping, add_noqa_inner, lex_codes, + lex_file_exemption, lex_inline_noqa, }; use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; - use crate::{generate_noqa_edits, Locator}; + use crate::{Edit, Violation}; + use crate::{Locator, generate_noqa_edits}; fn assert_lexed_ranges_match_slices( directive: Result, LexicalError>, @@ -1244,17 +1250,6 @@ mod tests { } } - /// Create a [`Message`] with a placeholder filename and rule code from `diagnostic`. - fn message_from_diagnostic( - diagnostic: Diagnostic, - path: impl AsRef, - source: &str, - ) -> Message { - let noqa_offset = diagnostic.start(); - let file = SourceFileBuilder::new(path.as_ref().to_string_lossy(), source).finish(); - Message::from_diagnostic(diagnostic, file, noqa_offset) - } - #[test] fn noqa_lex_codes() { let source = " F401,,F402F403 # and so on"; @@ -2835,13 +2830,14 @@ mod tests { assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); - let messages = [Diagnostic::new( - UnusedVariable { - name: "x".to_string(), - }, + let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); + let messages = [UnusedVariable { + name: "x".to_string(), + } + .into_diagnostic( TextRange::new(TextSize::from(0), TextSize::from(0)), - )] - .map(|d| message_from_diagnostic(d, path, contents)); + &source_file, + )]; let contents = "x = 1"; let noqa_line_for = NoqaMapping::default(); @@ -2857,19 +2853,20 @@ mod tests { assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); + let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); let messages = [ - Diagnostic::new( - AmbiguousVariableName("x".to_string()), + AmbiguousVariableName("x".to_string()).into_diagnostic( TextRange::new(TextSize::from(0), TextSize::from(0)), + &source_file, ), - Diagnostic::new( - UnusedVariable { - name: "x".to_string(), - }, + UnusedVariable { + name: "x".to_string(), + } + .into_diagnostic( TextRange::new(TextSize::from(0), TextSize::from(0)), + &source_file, ), - ] - .map(|d| message_from_diagnostic(d, path, contents)); + ]; let contents = "x = 1 # noqa: E741\n"; let noqa_line_for = NoqaMapping::default(); let comment_ranges = @@ -2886,19 +2883,20 @@ mod tests { assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); + let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); let messages = [ - Diagnostic::new( - AmbiguousVariableName("x".to_string()), + AmbiguousVariableName("x".to_string()).into_diagnostic( TextRange::new(TextSize::from(0), TextSize::from(0)), + &source_file, ), - Diagnostic::new( - UnusedVariable { - name: "x".to_string(), - }, + UnusedVariable { + name: "x".to_string(), + } + .into_diagnostic( TextRange::new(TextSize::from(0), TextSize::from(0)), + &source_file, ), - ] - .map(|d| message_from_diagnostic(d, path, contents)); + ]; let contents = "x = 1 # noqa"; let noqa_line_for = NoqaMapping::default(); let comment_ranges = @@ -2929,11 +2927,9 @@ print( ) "#; let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect(); - let messages = [Diagnostic::new( - PrintfStringFormatting, - TextRange::new(12.into(), 79.into()), - )] - .map(|d| message_from_diagnostic(d, path, source)); + let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish(); + let messages = [PrintfStringFormatting + .into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)]; let comment_ranges = CommentRanges::default(); let edits = generate_noqa_edits( path, @@ -2961,11 +2957,9 @@ print( foo; bar = "; - let messages = [Diagnostic::new( - UselessSemicolon, - TextRange::new(4.into(), 5.into()), - )] - .map(|d| message_from_diagnostic(d, path, source)); + let source_file = SourceFileBuilder::new(path.to_string_lossy(), source).finish(); + let messages = + [UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)]; let noqa_line_for = NoqaMapping::default(); let comment_ranges = CommentRanges::default(); let edits = generate_noqa_edits( diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs new file mode 100644 index 0000000000000..a5828127347bb --- /dev/null +++ b/crates/ruff_linter/src/preview.rs @@ -0,0 +1,197 @@ +//! Helpers to test if a specific preview style is enabled or not. +//! +//! The motivation for these functions isn't to avoid code duplication but to ease promoting preview behavior +//! to stable. The challenge with directly checking the `preview` attribute of [`LinterSettings`] is that it is unclear +//! which specific feature this preview check is for. Having named functions simplifies the promotion: +//! Simply delete the function and let Rust tell you which checks you have to remove. + +use crate::settings::LinterSettings; + +pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/16565 +pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// Rule-specific behavior + +// https://github.com/astral-sh/ruff/pull/15541 +pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/10759 +pub(crate) const fn is_comprehension_with_min_max_sum_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/12657 +pub(crate) const fn is_check_comprehensions_in_tuple_call_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/issues/15347 +pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/16719 +pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/13919 +pub(crate) const fn is_fix_manual_list_comprehension_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18763 +pub(crate) const fn is_fix_os_path_getsize_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} +// https://github.com/astral-sh/ruff/pull/18922 +pub(crate) const fn is_fix_os_path_getmtime_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18922 +pub(crate) const fn is_fix_os_path_getatime_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18922 +pub(crate) const fn is_fix_os_path_getctime_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_abspath_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_rmdir_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_unlink_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_remove_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_exists_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_expanduser_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_isdir_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_isfile_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_islink_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_isabs_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_readlink_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_basename_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19213 +pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/11436 +// https://github.com/astral-sh/ruff/pull/11168 +pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/8473 +pub(crate) const fn is_unicode_to_unicode_confusables_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/11370 +pub(crate) const fn is_undefined_export_in_dunder_init_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/14236 +pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18208 +pub(crate) const fn is_multiple_with_statements_fix_safe_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18400 +pub(crate) const fn is_ignore_init_files_in_useless_alias_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18572 +pub(crate) const fn is_optional_as_none_in_union_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18547 +pub(crate) const fn is_invalid_async_mock_access_check_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18867 +pub(crate) const fn is_raise_exception_byte_string_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/18683 +pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/19063 +pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/pyproject_toml.rs b/crates/ruff_linter/src/pyproject_toml.rs index 88b0765c23d66..8b319f595cddd 100644 --- a/crates/ruff_linter/src/pyproject_toml.rs +++ b/crates/ruff_linter/src/pyproject_toml.rs @@ -3,16 +3,16 @@ use log::warn; use pyproject_toml::PyProjectToml; use ruff_text_size::{TextRange, TextSize}; -use ruff_diagnostics::Diagnostic; +use ruff_db::diagnostic::Diagnostic; use ruff_source_file::SourceFile; -use crate::message::Message; use crate::registry::Rule; use crate::rules::ruff::rules::InvalidPyprojectToml; use crate::settings::LinterSettings; -use crate::IOError; +use crate::{IOError, Violation}; -pub fn lint_pyproject_toml(source_file: SourceFile, settings: &LinterSettings) -> Vec { +/// RUF200 +pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) -> Vec { let Some(err) = toml::from_str::(source_file.source_text()).err() else { return Vec::default(); }; @@ -29,12 +29,9 @@ pub fn lint_pyproject_toml(source_file: SourceFile, settings: &LinterSettings) - source_file.name(), ); if settings.rules.enabled(Rule::IOError) { - let diagnostic = Diagnostic::new(IOError { message }, TextRange::default()); - messages.push(Message::from_diagnostic( - diagnostic, - source_file, - TextSize::default(), - )); + let diagnostic = + IOError { message }.into_diagnostic(TextRange::default(), source_file); + messages.push(diagnostic); } else { warn!( "{}{}{} {message}", @@ -55,12 +52,9 @@ pub fn lint_pyproject_toml(source_file: SourceFile, settings: &LinterSettings) - if settings.rules.enabled(Rule::InvalidPyprojectToml) { let toml_err = err.message().to_string(); - let diagnostic = Diagnostic::new(InvalidPyprojectToml { message: toml_err }, range); - messages.push(Message::from_diagnostic( - diagnostic, - source_file, - TextSize::default(), - )); + let diagnostic = + InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file); + messages.push(diagnostic); } messages diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 9b03a8b7f35e0..9351cb5de26a0 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -1,6 +1,7 @@ //! Remnant of the registry of all [`Rule`] implementations, now it's reexporting from codes.rs //! with some helper symbols +use ruff_db::diagnostic::LintName; use strum_macros::EnumIter; pub use codes::Rule; @@ -348,9 +349,18 @@ impl Rule { /// Return the URL for the rule documentation, if it exists. pub fn url(&self) -> Option { - self.explanation() - .is_some() - .then(|| format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.as_ref())) + self.explanation().is_some().then(|| { + format!( + "{}/rules/{name}", + env!("CARGO_PKG_HOMEPAGE"), + name = self.name() + ) + }) + } + + pub fn name(&self) -> LintName { + let name: &'static str = self.into(); + LintName::of(name) } } @@ -421,7 +431,7 @@ pub mod clap_completion { fn possible_values(&self) -> Option + '_>> { Some(Box::new(Rule::iter().map(|rule| { let name = rule.noqa_code().to_string(); - let help = rule.as_ref().to_string(); + let help = rule.name().as_str(); PossibleValue::new(name).help(help) }))) } @@ -443,7 +453,7 @@ mod tests { assert!( rule.explanation().is_some(), "Rule {} is missing documentation", - rule.as_ref() + rule.name() ); } } @@ -460,10 +470,10 @@ mod tests { .collect(); for rule in Rule::iter() { - let rule_name = rule.as_ref(); + let rule_name = rule.name(); for pattern in &patterns { assert!( - !pattern.matches(rule_name), + !pattern.matches(&rule_name), "{rule_name} does not match naming convention, see CONTRIBUTING.md" ); } diff --git a/crates/ruff_linter/src/registry/rule_set.rs b/crates/ruff_linter/src/registry/rule_set.rs index 241cb4020d4b3..62601d7229a1c 100644 --- a/crates/ruff_linter/src/registry/rule_set.rs +++ b/crates/ruff_linter/src/registry/rule_set.rs @@ -16,7 +16,7 @@ pub struct RuleSet([u64; RULESET_SIZE]); impl RuleSet { const EMPTY: [u64; RULESET_SIZE] = [0; RULESET_SIZE]; // 64 fits into a u16 without truncation - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] const SLICE_BITS: u16 = u64::BITS as u16; /// Returns an empty rule set. @@ -302,9 +302,8 @@ impl Display for RuleSet { } else { writeln!(f, "[")?; for rule in self { - let name = rule.as_ref(); let code = rule.noqa_code(); - writeln!(f, "\t{name} ({code}),")?; + writeln!(f, "\t{name} ({code}),", name = rule.name())?; } write!(f, "]")?; } @@ -361,14 +360,14 @@ impl Iterator for RuleSetIterator { loop { let slice = self.set.0.get_mut(self.index as usize)?; // `trailing_zeros` is guaranteed to return a value in [0;64] - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] let bit = slice.trailing_zeros() as u16; if bit < RuleSet::SLICE_BITS { *slice ^= 1 << bit; let rule_value = self.index * RuleSet::SLICE_BITS + bit; // SAFETY: RuleSet guarantees that only valid rules are stored in the set. - #[allow(unsafe_code)] + #[expect(unsafe_code)] return Some(unsafe { std::mem::transmute::(rule_value) }); } diff --git a/crates/ruff_linter/src/renamer.rs b/crates/ruff_linter/src/renamer.rs index abf3434cde76f..d31793dc746e5 100644 --- a/crates/ruff_linter/src/renamer.rs +++ b/crates/ruff_linter/src/renamer.rs @@ -1,15 +1,15 @@ //! Code modification struct to support symbol renaming within a scope. -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use itertools::Itertools; -use ruff_diagnostics::Edit; use ruff_python_ast as ast; use ruff_python_codegen::Stylist; use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel}; use ruff_python_stdlib::{builtins::is_python_builtin, keyword::is_keyword}; use ruff_text_size::Ranged; +use crate::Edit; use crate::checkers::ast::Checker; pub(crate) struct Renamer; @@ -298,11 +298,15 @@ impl Renamer { let name_argument = match qualified_name.segments() { ["collections", "namedtuple"] => arguments.find_argument_value("typename", 0), - ["typing" | "typing_extensions", "TypeVar" | "ParamSpec" | "TypeVarTuple" | "NewType" | "TypeAliasType"] => { - arguments.find_argument_value("name", 0) - } + [ + "typing" | "typing_extensions", + "TypeVar" | "ParamSpec" | "TypeVarTuple" | "NewType" | "TypeAliasType", + ] => arguments.find_argument_value("name", 0), - ["enum", "Enum" | "IntEnum" | "StrEnum" | "ReprEnum" | "Flag" | "IntFlag"] + [ + "enum", + "Enum" | "IntEnum" | "StrEnum" | "ReprEnum" | "Flag" | "IntFlag", + ] | ["typing" | "typing_extensions", "NamedTuple" | "TypedDict"] => { arguments.find_positional(0) } diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index b433cad267bf4..74f069b976497 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -202,7 +202,7 @@ impl RuleSelector { } /// Returns rules matching the selector, taking into account rule groups like preview and deprecated. - pub fn rules<'a>(&'a self, preview: &PreviewOptions) -> impl Iterator + 'a { + pub fn rules<'a>(&'a self, preview: &PreviewOptions) -> impl Iterator + use<'a> { let preview_enabled = preview.mode.is_enabled(); let preview_require_explicit = preview.require_explicit; @@ -259,21 +259,21 @@ pub struct PreviewOptions { #[cfg(feature = "schemars")] mod schema { use itertools::Itertools; - use schemars::JsonSchema; use schemars::_serde_json::Value; + use schemars::JsonSchema; use schemars::schema::{InstanceType, Schema, SchemaObject}; use strum::IntoEnumIterator; + use crate::RuleSelector; use crate::registry::RuleNamespace; use crate::rule_selector::{Linter, RuleCodePrefix}; - use crate::RuleSelector; impl JsonSchema for RuleSelector { fn schema_name() -> String { "RuleSelector".to_string() } - fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema { + fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> Schema { Schema::Object(SchemaObject { instance_type: Some(InstanceType::String.into()), enum_values: Some( @@ -314,7 +314,7 @@ mod schema { .filter(|_rule| { // Filter out all test-only rules #[cfg(any(feature = "test-rules", test))] - #[allow(clippy::used_underscore_binding)] + #[expect(clippy::used_underscore_binding)] if _rule.starts_with("RUF9") || _rule == "PLW0101" { return false; } @@ -346,7 +346,9 @@ impl RuleSelector { 2 => Specificity::Prefix2Chars, 3 => Specificity::Prefix3Chars, 4 => Specificity::Prefix4Chars, - _ => panic!("RuleSelector::specificity doesn't yet support codes with so many characters"), + _ => panic!( + "RuleSelector::specificity doesn't yet support codes with so many characters" + ), } } } @@ -412,10 +414,10 @@ pub mod clap_completion { use strum::IntoEnumIterator; use crate::{ + RuleSelector, codes::RuleCodePrefix, registry::{Linter, RuleNamespace}, rule_selector::is_single_rule_selector, - RuleSelector, }; #[derive(Clone)] @@ -483,8 +485,7 @@ pub mod clap_completion { prefix.linter().common_prefix(), prefix.short_code() ); - let name: &'static str = rule.into(); - return Some(PossibleValue::new(code).help(name)); + return Some(PossibleValue::new(code).help(rule.name().as_str())); } None diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index 23aaaa07d43f2..b5997e916ff94 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -1,34 +1,52 @@ -use crate::rules::numpy::helpers::ImportSearcher; +use crate::checkers::ast::Checker; +use crate::fix::edits::remove_unused_imports; +use crate::importer::ImportRequest; +use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher}; +use ruff_diagnostics::{Edit, Fix}; +use ruff_python_ast::name::QualifiedNameBuilder; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_ast::{Expr, ExprName, StmtTry}; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::{Expr, ExprAttribute, ExprName, StmtTry}; use ruff_python_semantic::Exceptions; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::{MemberNameImport, NameImport}; +use ruff_text_size::Ranged; +use ruff_text_size::TextRange; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum Replacement { + // There's no replacement or suggestion other than removal None, - Name(&'static str), + // The attribute name of a class has been changed. + AttrName(&'static str), + // Additional information. Used when there's replacement but they're not direct mapping. Message(&'static str), + // Symbols updated in Airflow 3 with replacement + // e.g., `airflow.datasets.Dataset` to `airflow.sdk.Asset` AutoImport { module: &'static str, name: &'static str, }, + // Symbols updated in Airflow 3 with only module changed. Used when we want to match multiple names. + // e.g., `airflow.configuration.as_dict | get` to `airflow.configuration.conf.as_dict | get` SourceModuleMoved { module: &'static str, name: String, }, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum ProviderReplacement { - ProviderName { + None, + AutoImport { + module: &'static str, name: &'static str, provider: &'static str, version: &'static str, }, SourceModuleMovedToProvider { - name: String, module: &'static str, + name: String, provider: &'static str, version: &'static str, }, @@ -36,10 +54,27 @@ pub(crate) enum ProviderReplacement { pub(crate) fn is_guarded_by_try_except( expr: &Expr, - replacement: &Replacement, + module: &str, + name: &str, semantic: &SemanticModel, ) -> bool { match expr { + Expr::Attribute(_) => { + if !semantic.in_exception_handler() { + return false; + } + let Some(try_node) = semantic + .current_statements() + .find_map(|stmt| stmt.as_try_stmt()) + else { + return false; + }; + let suspended_exceptions = Exceptions::from_try_stmt(try_node, semantic); + if !suspended_exceptions.contains(Exceptions::ATTRIBUTE_ERROR) { + return false; + } + try_block_contains_undeprecated_attribute(try_node, module, name, semantic) + } Expr::Name(ExprName { id, .. }) => { let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else { return false; @@ -64,19 +99,38 @@ pub(crate) fn is_guarded_by_try_except( { return false; } - try_block_contains_undeprecated_import(try_node, replacement) + try_block_contains_undeprecated_import(try_node, module, name) } _ => false, } } /// Given an [`ast::StmtTry`] node, does the `try` branch of that node -/// contain any [`ast::StmtImportFrom`] nodes that indicate the numpy -/// member is being imported from the non-deprecated location? -fn try_block_contains_undeprecated_import(try_node: &StmtTry, replacement: &Replacement) -> bool { - let Replacement::AutoImport { module, name } = replacement else { - return false; +/// contain any [`ast::ExprAttribute`] nodes that indicate the airflow +/// member is being accessed from the non-deprecated location? +fn try_block_contains_undeprecated_attribute( + try_node: &StmtTry, + module: &str, + name: &str, + semantic: &SemanticModel, +) -> bool { + let undeprecated_qualified_name = { + let mut builder = QualifiedNameBuilder::default(); + for part in module.split('.') { + builder.push(part); + } + builder.push(name); + builder.build() }; + let mut attribute_searcher = AttributeSearcher::new(undeprecated_qualified_name, semantic); + attribute_searcher.visit_body(&try_node.body); + attribute_searcher.found_attribute +} + +/// Given an [`ast::StmtTry`] node, does the `try` branch of that node +/// contain any [`ast::StmtImportFrom`] nodes that indicate the airflow +/// member is being imported from the non-deprecated location? +fn try_block_contains_undeprecated_import(try_node: &StmtTry, module: &str, name: &str) -> bool { let mut import_searcher = ImportSearcher::new(module, name); import_searcher.visit_body(&try_node.body); import_searcher.found_import @@ -123,3 +177,70 @@ pub(crate) fn is_airflow_builtin_or_provider( _ => false, } } + +/// Return the [`ast::ExprName`] at the head of the expression, if any. +pub(crate) fn match_head(value: &Expr) -> Option<&ExprName> { + match value { + Expr::Attribute(ExprAttribute { value, .. }) => value.as_name_expr(), + Expr::Name(name) => Some(name), + _ => None, + } +} + +/// Return the [`Fix`] that imports the new name and updates where the import is referenced. +/// This is used for cases that member name has changed. +/// (e.g., `airflow.datasts.Dataset` to `airflow.sdk.Asset`) +pub(crate) fn generate_import_edit( + expr: &Expr, + checker: &Checker, + module: &str, + name: &str, + ranged: TextRange, +) -> Option { + let (import_edit, _) = checker + .importer() + .get_or_import_symbol( + &ImportRequest::import_from(module, name), + expr.start(), + checker.semantic(), + ) + .ok()?; + let replacement_edit = Edit::range_replacement(name.to_string(), ranged.range()); + Some(Fix::safe_edits(import_edit, [replacement_edit])) +} + +/// Return the [`Fix`] that remove the original import and import the same name with new path. +/// This is used for cases that member name has not changed. +/// (e.g., `airflow.operators.pig_operator.PigOperator` to `airflow.providers.apache.pig.hooks.pig.PigCliHook`) +pub(crate) fn generate_remove_and_runtime_import_edit( + expr: &Expr, + checker: &Checker, + module: &str, + name: &str, +) -> Option { + let head = match_head(expr)?; + let semantic = checker.semantic(); + let binding = semantic + .resolve_name(head) + .or_else(|| checker.semantic().lookup_symbol(&head.id)) + .map(|id| checker.semantic().binding(id))?; + let stmt = binding.statement(semantic)?; + let remove_edit = remove_unused_imports( + std::iter::once(name), + stmt, + None, + checker.locator(), + checker.stylist(), + checker.indexer(), + ) + .ok()?; + let import_edit = checker.importer().add_import( + &NameImport::ImportFrom(MemberNameImport::member( + (*module).to_string(), + name.to_string(), + )), + expr.start(), + ); + + Some(Fix::unsafe_edits(remove_edit, [import_edit])) +} diff --git a/crates/ruff_linter/src/rules/airflow/mod.rs b/crates/ruff_linter/src/rules/airflow/mod.rs index 69a35b5dd3d26..0824b327dde25 100644 --- a/crates/ruff_linter/src/rules/airflow/mod.rs +++ b/crates/ruff_linter/src/rules/airflow/mod.rs @@ -11,27 +11,54 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::AirflowVariableNameTaskIdMismatch, Path::new("AIR001.py"))] #[test_case(Rule::AirflowDagNoScheduleArgument, Path::new("AIR002.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_args.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_names.py"))] + #[test_case(Rule::Airflow3Removal, Path::new("AIR301_names_fix.py"))] + #[test_case(Rule::Airflow3Removal, Path::new("AIR301_provider_names_fix.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_names_try.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_class_attribute.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_airflow_plugin.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_context.py"))] - #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_amazon.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_celery.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_common_sql.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_daskexecutor.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_druid.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_fab.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_hdfs.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_hive.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_http.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_jdbc.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_kubernetes.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_mysql.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_oracle.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_papermill.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_pig.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_postgres.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_presto.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_samba.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_slack.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_smtp.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_sqlite.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_zendesk.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_standard.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_try.py"))] #[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_args.py"))] #[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_names.py"))] + #[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_try.py"))] #[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312.py"))] + #[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312_try.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("airflow").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs b/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs index 79abcf278b0f3..c89071f120839 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/dag_schedule_argument.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -86,6 +86,5 @@ pub(crate) fn dag_no_schedule_argument(checker: &Checker, expr: &Expr) { } // Produce a diagnostic when the `schedule` keyword argument is not found. - let diagnostic = Diagnostic::new(AirflowDagNoScheduleArgument, expr.range()); - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(AirflowDagNoScheduleArgument, expr.range()); } diff --git a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs index 319464903e5a2..e22d6fbd62cf8 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs @@ -1,12 +1,16 @@ -use crate::rules::airflow::helpers::ProviderReplacement; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::Checker; +use crate::rules::airflow::helpers::{ + ProviderReplacement, generate_import_edit, generate_remove_and_runtime_import_edit, + is_guarded_by_try_except, +}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{Expr, ExprAttribute}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use ruff_text_size::TextRange; -use crate::checkers::ast::Checker; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for uses of Airflow functions and values that have been moved to it providers. @@ -19,20 +23,26 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python -/// from airflow.auth.managers.fab.fab_auth_manage import FabAuthManager +/// from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager +/// +/// fab_auth_manager_app = FabAuthManager().get_fastapi_app() /// ``` /// /// Use instead: /// ```python -/// from airflow.providers.fab.auth_manager.fab_auth_manage import FabAuthManager +/// from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager +/// +/// fab_auth_manager_app = FabAuthManager().get_fastapi_app() /// ``` #[derive(ViolationMetadata)] -pub(crate) struct Airflow3MovedToProvider { - deprecated: String, +pub(crate) struct Airflow3MovedToProvider<'a> { + deprecated: QualifiedName<'a>, replacement: ProviderReplacement, } -impl Violation for Airflow3MovedToProvider { +impl Violation for Airflow3MovedToProvider<'_> { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let Airflow3MovedToProvider { @@ -40,8 +50,12 @@ impl Violation for Airflow3MovedToProvider { replacement, } = self; match replacement { - ProviderReplacement::ProviderName { + ProviderReplacement::None => { + format!("`{deprecated}` is removed in Airflow 3.0") + } + ProviderReplacement::AutoImport { name: _, + module: _, provider, version: _, } @@ -58,24 +72,26 @@ impl Violation for Airflow3MovedToProvider { fn fix_title(&self) -> Option { let Airflow3MovedToProvider { replacement, .. } = self; - match replacement { - ProviderReplacement::ProviderName { + if let Some((module, name, provider, version)) = match &replacement { + ProviderReplacement::AutoImport { + module, name, provider, version, - } => { - Some(format!( - "Install `apache-airflow-provider-{provider}>={version}` and use `{name}` instead." - )) - }, + } => Some((module, *name, provider, version)), ProviderReplacement::SourceModuleMovedToProvider { - name, module, + name, provider, version, - } => { - Some(format!("Install `apache-airflow-provider-{provider}>={version}` and use `{module}.{name}` instead.")) - } , + } => Some((module, name.as_str(), provider, version)), + ProviderReplacement::None => None, + } { + Some(format!( + "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` from `{module}` instead." + )) + } else { + None } } } @@ -101,702 +117,1125 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan }; let replacement = match qualified_name.segments() { - // ProviderName: for cases that only one name has been moved // apache-airflow-providers-amazon - ["airflow", "hooks", "S3_hook", rest @ ( - "S3Hook" - | "provide_bucket_name" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "hooks", + "S3_hook", + rest @ ("S3Hook" | "provide_bucket_name"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.amazon.aws.hooks.s3", provider: "amazon", - version: "1.0.0" - }, - ["airflow", "operators", "gcs_to_s3", "GCSToS3Operator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator", - provider: "amazon", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "google_api_to_s3_transfer", "GoogleApiToS3Operator" | "GoogleApiToS3Transfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator", + ["airflow", "operators", "gcs_to_s3", "GCSToS3Operator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.gcs_to_s3", + name: "GCSToS3Operator", + provider: "amazon", + version: "1.0.0", + } + } + [ + "airflow", + "operators", + "google_api_to_s3_transfer", + "GoogleApiToS3Operator" | "GoogleApiToS3Transfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.google_api_to_s3", + name: "GoogleApiToS3Operator", provider: "amazon", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "redshift_to_s3_operator", "RedshiftToS3Operator" | "RedshiftToS3Transfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator", + [ + "airflow", + "operators", + "redshift_to_s3_operator", + "RedshiftToS3Operator" | "RedshiftToS3Transfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.redshift_to_s3", + name: "RedshiftToS3Operator", provider: "amazon", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "s3_file_transform_operator", "S3FileTransformOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.operators.s3_file_transform.S3FileTransformOperator", + [ + "airflow", + "operators", + "s3_file_transform_operator", + "S3FileTransformOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.operators.s3", + name: "S3FileTransformOperator", provider: "amazon", - version: "1.0.0" - }, - ["airflow", "operators", "s3_to_redshift_operator", "S3ToRedshiftOperator" | "S3ToRedshiftTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator", + version: "3.0.0", + }, + [ + "airflow", + "operators", + "s3_to_redshift_operator", + "S3ToRedshiftOperator" | "S3ToRedshiftTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.s3_to_redshift", + name: "S3ToRedshiftOperator", provider: "amazon", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "sensors", "s3_key_sensor", "S3KeySensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.sensors.s3.S3KeySensor", + ["airflow", "sensors", "s3_key_sensor", "S3KeySensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.sensors.s3", + name: "S3KeySensor", provider: "amazon", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-celery - ["airflow", "config_templates", "default_celery", "DEFAULT_CELERY_CONFIG"] => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG", + [ + "airflow", + "config_templates", + "default_celery", + "DEFAULT_CELERY_CONFIG", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.default_celery", + name: "DEFAULT_CELERY_CONFIG", provider: "celery", - version: "3.3.0" + version: "3.3.0", }, - ["airflow", "executors", "celery_executor", rest ] => match *rest { - "app" => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.celery_executor_utils.app", + ["airflow", "executors", "celery_executor", rest] => match *rest { + "app" => ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.celery_executor_utils", + name: "app", provider: "celery", - version: "3.3.0" + version: "3.3.0", }, - "CeleryExecutor" => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.celery_executor.CeleryExecutor", + "CeleryExecutor" => ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.celery_executor", + name: "CeleryExecutor", provider: "celery", - version: "3.3.0" + version: "3.3.0", }, _ => return, }, - ["airflow", "executors", "celery_kubernetes_executor", "CeleryKubernetesExecutor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.celery_kubernetes_executor.CeleryKubernetesExecutor", + [ + "airflow", + "executors", + "celery_kubernetes_executor", + "CeleryKubernetesExecutor", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.celery_kubernetes_executor", + name: "CeleryKubernetesExecutor", provider: "celery", - version: "3.3.0" + version: "3.3.0", }, // apache-airflow-providers-common-sql - ["airflow", "hooks", "dbapi", rest @ ( - "ConnectorProtocol" - | "DbApiHook" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "hooks", + "dbapi", + rest @ ("ConnectorProtocol" | "DbApiHook"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.common.sql.hooks.sql", provider: "common-sql", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "hooks", "dbapi_hook", "DbApiHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.hooks.sql.DbApiHook", + ["airflow", "hooks", "dbapi_hook", "DbApiHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.hooks.sql", + name: "DbApiHook", provider: "common-sql", - version: "1.0.0" - }, - ["airflow", "operators", "check_operator", rest] => match *rest { - "SQLCheckOperator" | "CheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLIntervalCheckOperator" | "IntervalCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLThresholdCheckOperator" | "ThresholdCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLValueCheckOperator" | "ValueCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - _ => return + version: "1.0.0", }, - ["airflow", "operators", "presto_check_operator", rest] => match *rest { - "SQLCheckOperator" | "PrestoCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLIntervalCheckOperator" | "PrestoIntervalCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLValueCheckOperator" | "PrestoValueCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", + [ + "airflow", + "operators", + "check_operator" | "sql", + "SQLCheckOperator", + ] + | [ + "airflow", + "operators", + "check_operator" | "druid_check_operator" | "presto_check_operator", + "CheckOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLCheckOperator", + provider: "common-sql", + version: "1.1.0", + }, + [ + "airflow", + "operators", + "druid_check_operator", + "DruidCheckOperator", + ] + | [ + "airflow", + "operators", + "presto_check_operator", + "PrestoCheckOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLCheckOperator", + provider: "common-sql", + version: "1.1.0", + }, + [ + "airflow", + "operators", + "check_operator", + "IntervalCheckOperator" | "SQLIntervalCheckOperator", + ] + | [ + "airflow", + "operators", + "presto_check_operator", + "IntervalCheckOperator", + ] + | ["airflow", "operators", "sql", "SQLIntervalCheckOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLIntervalCheckOperator", provider: "common-sql", - version: "1.1.0" - }, - _ => return + version: "1.1.0", + } } - ["airflow", "operators", "sql", rest] => match *rest { - "BaseSQLOperator" | - "BranchSQLOperator" | - "SQLCheckOperator" | - "SQLIntervalCheckOperator" | - "SQLTablecheckOperator" | - "SQLThresholdCheckOperator" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + [ + "airflow", + "operators", + "presto_check_operator", + "PrestoIntervalCheckOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLIntervalCheckOperator", + provider: "common-sql", + version: "1.1.0", + }, + [ + "airflow", + "operators", + "check_operator", + "SQLThresholdCheckOperator" | "ThresholdCheckOperator", + ] + | ["airflow", "operators", "sql", "SQLThresholdCheckOperator"] => { + ProviderReplacement::AutoImport { module: "airflow.providers.common.sql.operators.sql", + name: "SQLThresholdCheckOperator", provider: "common-sql", - version: "1.1.0" - }, - "SQLColumnCheckOperator" | - "SQLValueCheckOperator" | - "_convert_to_float_if_possible" | - "parse_boolean" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + version: "1.1.0", + } + } + [ + "airflow", + "operators", + "check_operator", + "SQLValueCheckOperator" | "ValueCheckOperator", + ] + | [ + "airflow", + "operators", + "presto_check_operator", + "ValueCheckOperator", + ] + | ["airflow", "operators", "sql", "SQLValueCheckOperator"] => { + ProviderReplacement::AutoImport { module: "airflow.providers.common.sql.operators.sql", + name: "SQLValueCheckOperator", provider: "common-sql", - version: "1.0.0" - }, - _ => return + version: "1.1.0", + } } - ["airflow", "sensors", "sql" | "sql_sensor", "SqlSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.sensors.sql.SqlSensor", + [ + "airflow", + "operators", + "presto_check_operator", + "PrestoValueCheckOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLValueCheckOperator", provider: "common-sql", - version: "1.0.0" + version: "1.1.0", + }, + ["airflow", "operators", "sql", rest] => match *rest { + "BaseSQLOperator" | "BranchSQLOperator" | "SQLTableCheckOperator" => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.common.sql.operators.sql", + provider: "common-sql", + version: "1.1.0", + } + } + "SQLColumnCheckOperator" | "_convert_to_float_if_possible" | "parse_boolean" => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.common.sql.operators.sql", + provider: "common-sql", + version: "1.0.0", + } + } + _ => return, }, + ["airflow", "sensors", "sql" | "sql_sensor", "SqlSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.sensors.sql", + name: "SqlSensor", + provider: "common-sql", + version: "1.0.0", + } + } + ["airflow", "operators", "jdbc_operator", "JdbcOperator"] + | ["airflow", "operators", "mssql_operator", "MsSqlOperator"] + | ["airflow", "operators", "mysql_operator", "MySqlOperator"] + | ["airflow", "operators", "oracle_operator", "OracleOperator"] + | [ + "airflow", + "operators", + "postgres_operator", + "PostgresOperator", + ] + | ["airflow", "operators", "sqlite_operator", "SqliteOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLExecuteQueryOperator", + provider: "common-sql", + version: "1.3.0", + } + } // apache-airflow-providers-daskexecutor - ["airflow", "executors", "dask_executor", "DaskExecutor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor", - provider: "daskexecutor", - version: "1.0.0" - }, + ["airflow", "executors", "dask_executor", "DaskExecutor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.daskexecutor.executors.dask_executor", + name: "DaskExecutor", + provider: "daskexecutor", + version: "1.0.0", + } + } // apache-airflow-providers-docker - ["airflow", "hooks", "docker_hook", "DockerHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.docker.hooks.docker.DockerHook", + ["airflow", "hooks", "docker_hook", "DockerHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.docker.hooks.docker", + name: "DockerHook", provider: "docker", - version: "1.0.0" - }, - ["airflow", "operators", "docker_operator", "DockerOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.docker.operators.docker.DockerOperator", - provider: "docker", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "docker_operator", "DockerOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.docker.operators.docker", + name: "DockerOperator", + provider: "docker", + version: "1.0.0", + } + } // apache-airflow-providers-apache-druid - ["airflow", "hooks", "druid_hook", rest @ ( - "DruidDbApiHook" - | "DruidHook" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "hooks", + "druid_hook", + rest @ ("DruidDbApiHook" | "DruidHook"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.apache.druid.hooks.druid", provider: "apache-druid", - version: "1.0.0" - }, - ["airflow", "operators", "druid_check_operator", "DruidCheckOperator"] => ProviderReplacement::ProviderName { - name: "DruidCheckOperator", - provider: "apache-druid", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "hive_to_druid", "HiveToDruidOperator" | "HiveToDruidTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator", + [ + "airflow", + "operators", + "hive_to_druid", + "HiveToDruidOperator" | "HiveToDruidTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.druid.transfers.hive_to_druid", + name: "HiveToDruidOperator", provider: "apache-druid", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-fab - ["airflow", "api", "auth", "backend", "basic_auth", rest @ ( - "CLIENT_AUTH" - | "init_app" - | "auth_current_user" - | "requires_authentication" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "api", + "auth", + "backend", + "basic_auth", + rest @ ("CLIENT_AUTH" | "init_app" | "auth_current_user" | "requires_authentication"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth", provider: "fab", - version: "1.0.0" - }, - ["airflow", "api", "auth", "backend", "kerberos_auth", rest @ ( - "log" - | "CLIENT_AUTH" - | "find_user" - | "init_app" - | "requires_authentication" - )] => ProviderReplacement::SourceModuleMovedToProvider { + version: "1.0.0", + }, + [ + "airflow", + "api", + "auth", + "backend", + "kerberos_auth", + rest @ ("log" | "CLIENT_AUTH" | "find_user" | "init_app" | "requires_authentication"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth", provider: "fab", - version: "1.0.0" - }, - ["airflow", "auth", "managers", "fab", "api", "auth", "backend", "kerberos_auth", rest @ ( - "log" - | "CLIENT_AUTH" - | "find_user" - | "init_app" - | "requires_authentication" - )] => ProviderReplacement::SourceModuleMovedToProvider { + version: "1.0.0", + }, + [ + "airflow", + "auth", + "managers", + "fab", + "api", + "auth", + "backend", + "kerberos_auth", + rest @ ("log" | "CLIENT_AUTH" | "find_user" | "init_app" | "requires_authentication"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth", provider: "fab", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "auth", "managers", "fab", "fab_auth_manager", "FabAuthManager"] => ProviderReplacement::ProviderName { - name: "airflow.providers.fab.auth_manager.security_manager.FabAuthManager", + [ + "airflow", + "auth", + "managers", + "fab", + "fab_auth_manager", + "FabAuthManager", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.fab.auth_manager.fab_auth_manager", + name: "FabAuthManager", provider: "fab", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "auth", "managers", "fab", "security_manager", "override", rest @ ( - "MAX_NUM_DATABASE_USER_SESSIONS" - | "FabAirflowSecurityManagerOverride" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + [ + "airflow", + "auth", + "managers", + "fab", + "security_manager", + "override", + "MAX_NUM_DATABASE_USER_SESSIONS", + ] => ProviderReplacement::AutoImport { module: "airflow.providers.fab.auth_manager.security_manager.override", + name: "MAX_NUM_DATABASE_USER_SESSIONS", provider: "fab", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "www", "security", "FabAirflowSecurityManagerOverride"] => ProviderReplacement::ProviderName { - name: "airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride", + [ + "airflow", + "auth", + "managers", + "fab", + "security_manager", + "override", + "FabAirflowSecurityManagerOverride", + ] + | [ + "airflow", + "www", + "security", + "FabAirflowSecurityManagerOverride", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.fab.auth_manager.security_manager.override", + name: "FabAirflowSecurityManagerOverride", provider: "fab", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-apache-hdfs - ["airflow", "hooks", "webhdfs_hook", "WebHDFSHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook", + ["airflow", "hooks", "webhdfs_hook", "WebHDFSHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hdfs.hooks.webhdfs", + name: "WebHDFSHook", provider: "apache-hdfs", - version: "1.0.0" - }, - ["airflow", "sensors", "web_hdfs_sensor", "WebHdfsSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor", - provider: "apache-hdfs", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "sensors", "web_hdfs_sensor", "WebHdfsSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hdfs.sensors.web_hdfs", + name: "WebHdfsSensor", + provider: "apache-hdfs", + version: "1.0.0", + } + } // apache-airflow-providers-apache-hive - ["airflow", "macros", "hive", rest @ ( - "closest_ds_partition" - | "max_partition" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.apache.hive.macros.hive", - provider: "apache-hive", - version: "5.1.0" - }, - ["airflow", "hooks", "hive_hooks", rest @ ( - "HiveCliHook" + [ + "airflow", + "hooks", + "hive_hooks", + rest @ ("HiveCliHook" | "HiveMetastoreHook" | "HiveServer2Hook" - | "HIVE_QUEUE_PRIORITIES" - )] => ProviderReplacement::SourceModuleMovedToProvider { + | "HIVE_QUEUE_PRIORITIES"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.apache.hive.hooks.hive", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "hive_operator", "HiveOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.operators.hive.HiveOperator", + [ + "airflow", + "macros", + "hive", + rest @ ("closest_ds_partition" | "max_partition"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.apache.hive.macros.hive", provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "hive_stats_operator", "HiveStatsCollectionOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator", + version: "5.1.0", + }, + ["airflow", "operators", "hive_operator", "HiveOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.operators.hive", + name: "HiveOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + [ + "airflow", + "operators", + "hive_stats_operator", + "HiveStatsCollectionOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.operators.hive_stats", + name: "HiveStatsCollectionOperator", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "hive_to_mysql", "HiveToMySqlOperator" | "HiveToMySqlTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator", + [ + "airflow", + "operators", + "hive_to_mysql", + "HiveToMySqlOperator" | "HiveToMySqlTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.hive_to_mysql", + name: "HiveToMySqlOperator", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "hive_to_samba_operator", "HiveToSambaOperator"] => ProviderReplacement::ProviderName { + [ + "airflow", + "operators", + "hive_to_samba_operator", + "HiveToSambaOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.hive_to_samba", name: "HiveToSambaOperator", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "mssql_to_hive", "MsSqlToHiveOperator" | "MsSqlToHiveTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator", + [ + "airflow", + "operators", + "mssql_to_hive", + "MsSqlToHiveOperator" | "MsSqlToHiveTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.mssql_to_hive", + name: "MsSqlToHiveOperator", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "mysql_to_hive", "MySqlToHiveOperator" | "MySqlToHiveTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator", + [ + "airflow", + "operators", + "mysql_to_hive", + "MySqlToHiveOperator" | "MySqlToHiveTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.mysql_to_hive", + name: "MySqlToHiveOperator", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "s3_to_hive_operator", "S3ToHiveOperator" | "S3ToHiveTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator", + [ + "airflow", + "operators", + "s3_to_hive_operator", + "S3ToHiveOperator" | "S3ToHiveTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.s3_to_hive", + name: "S3ToHiveOperator", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "sensors", "hive_partition_sensor", "HivePartitionSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor", + [ + "airflow", + "sensors", + "hive_partition_sensor", + "HivePartitionSensor", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.sensors.hive_partition", + name: "HivePartitionSensor", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "sensors", "metastore_partition_sensor", "MetastorePartitionSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor", + [ + "airflow", + "sensors", + "metastore_partition_sensor", + "MetastorePartitionSensor", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.sensors.metastore_partition", + name: "MetastorePartitionSensor", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "sensors", "named_hive_partition_sensor", "NamedHivePartitionSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor", + [ + "airflow", + "sensors", + "named_hive_partition_sensor", + "NamedHivePartitionSensor", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.sensors.named_hive_partition", + name: "NamedHivePartitionSensor", provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-http - ["airflow", "hooks", "http_hook", "HttpHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.hooks.http.HttpHook", + ["airflow", "hooks", "http_hook", "HttpHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.http.hooks.http", + name: "HttpHook", provider: "http", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "http_operator", "SimpleHttpOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.operators.http.SimpleHttpOperator", + [ + "airflow", + "operators", + "http_operator", + "SimpleHttpOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.http.operators.http", + name: "HttpOperator", provider: "http", - version: "1.0.0" + version: "5.0.0", }, - ["airflow", "sensors", "http_sensor", "HttpSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.sensors.http.HttpSensor", + ["airflow", "sensors", "http_sensor", "HttpSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.http.sensors.http", + name: "HttpSensor", provider: "http", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-jdbc - ["airflow", "hooks", "jdbc_hook", rest @ ( - "JdbcHook" - | "jaydebeapi" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "hooks", + "jdbc_hook", + rest @ ("JdbcHook" | "jaydebeapi"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.jdbc.hooks.jdbc", provider: "jdbc", - version: "1.0.0" - }, - ["airflow", "operators", "jdbc_operator", "JdbcOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.jdbc.operators.jdbc.JdbcOperator", - provider: "jdbc", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-cncf-kubernetes - ["airflow", "executors", "kubernetes_executor_types", rest @ ( - "ALL_NAMESPACES" - | "POD_EXECUTOR_DONE_KEY" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "executors", + "kubernetes_executor_types", + rest @ ("ALL_NAMESPACES" | "POD_EXECUTOR_DONE_KEY"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types", provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "k8s_model", rest @ ( - "K8SModel" - | "append_to_pod" - )] => ProviderReplacement::SourceModuleMovedToProvider { + version: "7.4.0", + }, + [ + "airflow", + "kubernetes", + "k8s_model", + rest @ ("K8SModel" | "append_to_pod"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.k8s_model", provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "kube_client", rest @ ( - "_disable_verify_ssl" - | "_enable_tcp_keepalive" - | "get_kube_client" - )] => ProviderReplacement::SourceModuleMovedToProvider { + version: "7.4.0", + }, + [ + "airflow", + "kubernetes", + "kube_client", + rest @ ("_disable_verify_ssl" | "_enable_tcp_keepalive" | "get_kube_client"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client", + module: "airflow.providers.cncf.kubernetes.kube_client", + provider: "cncf-kubernetes", + version: "7.4.0", + }, + [ + "airflow", + "kubernetes", + "kubernetes_helper_functions", + "add_pod_suffix", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "add_unique_suffix", + provider: "cncf-kubernetes", + version: "10.0.0", + }, + [ + "airflow", + "kubernetes", + "kubernetes_helper_functions", + "create_pod_id", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "create_unique_id", provider: "cncf-kubernetes", - version: "7.4.0" + version: "10.0.0", }, - ["airflow", "kubernetes", "kubernetes_helper_functions", rest @ ( - "add_pod_suffix" - | "annotations_for_logging_task_metadata" + [ + "airflow", + "kubernetes", + "kubernetes_helper_functions", + rest @ ("annotations_for_logging_task_metadata" | "annotations_to_key" - | "create_pod_id" | "get_logs_task_metadata" - | "rand_str" - )] => ProviderReplacement::SourceModuleMovedToProvider { + | "rand_str"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, ["airflow", "kubernetes", "pod", rest] => match *rest { - "Port" =>ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1ContainerPort", + "Port" => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1ContainerPort", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - "Resources" => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1ResourceRequirements", + "Resources" => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1ResourceRequirements", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - _ => return - }, - ["airflow", "kubernetes", "pod_generator", rest ] => match *rest { - "datetime_to_label_safe_datestring" | - "extend_object_field" | - "label_safe_datestring_to_datetime" | - "make_safe_label_value" | - "merge_objects" | - "PodGenerator" | - "PodDefaults" => ProviderReplacement::SourceModuleMovedToProvider { + _ => return, + }, + ["airflow", "kubernetes", "pod_generator", rest] => match *rest { + "datetime_to_label_safe_datestring" + | "extend_object_field" + | "label_safe_datestring_to_datetime" + | "make_safe_label_value" + | "merge_objects" + | "PodGenerator" => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.pod_generator", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", + }, + "PodDefaults" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar", + name: "PodDefaults", + provider: "cncf-kubernetes", + version: "7.4.0", + }, + "PodGeneratorDeprecated" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.pod_generator", + name: "PodGenerator", + provider: "cncf-kubernetes", + version: "7.4.0", }, - "PodGeneratorDeprecated" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.pod_generator.PodGenerator", + "add_pod_suffix" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "add_unique_suffix", provider: "cncf-kubernetes", - version: "7.4.0" + version: "10.0.0", }, - "add_pod_suffix" | "rand_str" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "rand_str".to_string(), provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, _ => return, }, - ["airflow", "kubernetes", "pod_generator_deprecated", rest @ ( - "make_safe_label_value" - | "PodDefaults" - | "PodGenerator" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "kubernetes", + "pod_generator_deprecated", + rest @ ("make_safe_label_value" | "PodGenerator"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_generator_deprecated", + module: "airflow.providers.cncf.kubernetes.pod_generator", provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "pod_launcher", rest @( - "PodLauncher" - | "PodStatus" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_launcher_deprecated", + version: "7.4.0", + }, + [ + "airflow", + "kubernetes", + "pod_generator_deprecated" | "pod_launcher_deprecated", + "PodDefaults", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar", + name: "PodDefaults", provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "pod_launcher_deprecated", rest] => match *rest { - "PodLauncher" | "PodStatus" | "PodDefaults" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_launcher_deprecated", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - "get_kube_client" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.kube_client.get_kube_client", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - _ => return, - } - ["airflow", "kubernetes", "pod_runtime_info_env", "PodRuntimeInfoEnv"] => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1EnvVar", + version: "7.4.0", + }, + [ + "airflow", + "kubernetes", + "pod_launcher_deprecated", + "get_kube_client", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kube_client", + name: "get_kube_client", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", + }, + [ + "airflow", + "kubernetes", + "pod_launcher" | "pod_launcher_deprecated", + "PodLauncher", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.utils.pod_manager", + name: "PodManager", + provider: "cncf-kubernetes", + version: "3.0.0", + }, + [ + "airflow", + "kubernetes", + "pod_launcher" | "pod_launcher_deprecated", + "PodStatus", + ] => ProviderReplacement::AutoImport { + module: " airflow.providers.cncf.kubernetes.utils.pod_manager", + name: "PodPhase", + provider: "cncf-kubernetes", + version: "3.0.0", + }, + [ + "airflow", + "kubernetes", + "pod_runtime_info_env", + "PodRuntimeInfoEnv", + ] => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1EnvVar", + provider: "cncf-kubernetes", + version: "7.4.0", }, - ["airflow", "kubernetes", "secret", rest ] => match *rest { - "K8SModel" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.k8s_model.K8SModel", + ["airflow", "kubernetes", "secret", rest] => match *rest { + "K8SModel" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.k8s_model", + name: "K8SModel", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - "Secret" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.secret.Secret", + "Secret" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.secret", + name: "Secret", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, _ => return, }, - ["airflow", "kubernetes", "volume", "Volume"] => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1Volume", + ["airflow", "kubernetes", "volume", "Volume"] => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1Volume", provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "volume_mount", "VolumeMount"] => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1VolumeMount", - provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, + ["airflow", "kubernetes", "volume_mount", "VolumeMount"] => { + ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1VolumeMount", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } // apache-airflow-providers-microsoft-mssql - ["airflow", "hooks", "mssql_hook", "MsSqlHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook", - provider: "microsoft-mssql", - version: "1.0.0" - }, - ["airflow", "operators", "mssql_operator", "MsSqlOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.microsoft.mssql.operators.mssql.MsSqlOperator", + ["airflow", "hooks", "mssql_hook", "MsSqlHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.microsoft.mssql.hooks.mssql", + name: "MsSqlHook", provider: "microsoft-mssql", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-mysql - ["airflow", "hooks", "mysql_hook", "MySqlHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.mysql.hooks.mysql.MySqlHook", - provider: "mysql", - version: "1.0.0" - }, - ["airflow", "operators", "mysql_operator", "MySqlOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.mysql.operators.mysql.MySqlOperator", + ["airflow", "hooks", "mysql_hook", "MySqlHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.mysql.hooks.mysql", + name: "MySqlHook", provider: "mysql", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "presto_to_mysql", "PrestoToMySqlOperator" | "PrestoToMySqlTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator", + [ + "airflow", + "operators", + "presto_to_mysql", + "PrestoToMySqlOperator" | "PrestoToMySqlTransfer", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.mysql.transfers.presto_to_mysql", + name: "PrestoToMySqlOperator", provider: "mysql", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-oracle - ["airflow", "hooks", "oracle_hook", "OracleHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.oracle.hooks.oracle.OracleHook", - provider: "oracle", - version: "1.0.0" - }, - ["airflow", "operators", "oracle_operator", "OracleOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.oracle.operators.oracle.OracleOperator", + ["airflow", "hooks", "oracle_hook", "OracleHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.oracle.hooks.oracle", + name: "OracleHook", provider: "oracle", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-papermill - ["airflow", "operators", "papermill_operator", "PapermillOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.papermill.operators.papermill.PapermillOperator", + [ + "airflow", + "operators", + "papermill_operator", + "PapermillOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.papermill.operators.papermill", + name: "PapermillOperator", provider: "papermill", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-apache-pig - ["airflow", "hooks", "pig_hook", "PigCliHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.pig.hooks.pig.PigCliHook", + ["airflow", "hooks", "pig_hook", "PigCliHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.pig.hooks.pig", + name: "PigCliHook", provider: "apache-pig", - version: "1.0.0" - }, - ["airflow", "operators", "pig_operator", "PigOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.pig.operators.pig.PigOperator", - provider: "apache-pig", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "pig_operator", "PigOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.pig.operators.pig", + name: "PigOperator", + provider: "apache-pig", + version: "1.0.0", + } + } // apache-airflow-providers-postgres - ["airflow", "hooks", "postgres_hook", "PostgresHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.postgres.hooks.postgres.PostgresHook", - provider: "postgres", - version: "1.0.0" - }, - ["airflow", "operators", "postgres_operator", rest @ ( - "Mapping" - | "PostgresOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.postgres.operators.postgres", + ["airflow", "hooks", "postgres_hook", "PostgresHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.postgres.hooks.postgres", + name: "PostgresHook", provider: "postgres", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "postgres_operator", "Mapping"] => ProviderReplacement::None, // apache-airflow-providers-presto - ["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.presto.hooks.presto.PrestoHook", + ["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.presto.hooks.presto", + name: "PrestoHook", provider: "presto", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-samba - ["airflow", "hooks", "samba_hook", "SambaHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.samba.hooks.samba.SambaHook", + ["airflow", "hooks", "samba_hook", "SambaHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.samba.hooks.samba", + name: "SambaHook", provider: "samba", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-slack - ["airflow", "hooks", "slack_hook", "SlackHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.slack.hooks.slack.SlackHook", + ["airflow", "hooks", "slack_hook", "SlackHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.slack.hooks.slack", + name: "SlackHook", provider: "slack", - version: "1.0.0" + version: "1.0.0", }, - ["airflow", "operators", "slack_operator", rest @ ( - "SlackAPIOperator" - | "SlackAPIPostOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "operators", + "slack_operator", + rest @ ("SlackAPIOperator" | "SlackAPIPostOperator"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.slack.operators.slack", provider: "slack", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-smtp - ["airflow", "operators", "email_operator" | "email", "EmailOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.smtp.operators.smtp.EmailOperator", + [ + "airflow", + "operators", + "email_operator" | "email", + "EmailOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.smtp.operators.smtp", + name: "EmailOperator", provider: "smtp", version: "1.0.0", }, // apache-airflow-providers-sqlite - ["airflow", "hooks", "sqlite_hook", "SqliteHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.sqlite.hooks.sqlite.SqliteHook", + ["airflow", "hooks", "sqlite_hook", "SqliteHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.sqlite.hooks.sqlite", + name: "SqliteHook", provider: "sqlite", - version: "1.0.0" - }, - ["airflow", "operators", "sqlite_operator", "SqliteOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.sqlite.operators.sqlite.SqliteOperator", - provider: "sqlite", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-zendesk - ["airflow", "hooks", "zendesk_hook", "ZendeskHook"] => - ProviderReplacement::ProviderName { - name: "airflow.providers.zendesk.hooks.zendesk.ZendeskHook", + ["airflow", "hooks", "zendesk_hook", "ZendeskHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.zendesk.hooks.zendesk", + name: "ZendeskHook", provider: "zendesk", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-standard - ["airflow", "operators", "bash_operator", "BashOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.bash.BashOperator", + [ + "airflow", + "hooks", + "subprocess", + rest @ ("SubprocessResult" | "working_directory"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.hooks.subprocess", provider: "standard", - version: "0.0.1" - }, - ["airflow", "operators", "dagrun_operator", rest @ ( - "TriggerDagRunLink" - | "TriggerDagRunOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { + version: "0.0.3", + }, + ["airflow", "operators", "bash_operator", "BashOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.bash", + name: "BashOperator", + provider: "standard", + version: "0.0.1", + } + } + [ + "airflow", + "operators", + "dagrun_operator", + rest @ ("TriggerDagRunLink" | "TriggerDagRunOperator"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.standard.operators.trigger_dagrun", provider: "standard", - version: "0.0.2" - }, - ["airflow", "operators", "dummy" | "dummy_operator", "EmptyOperator" | "DummyOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.empty.EmptyOperator", + version: "0.0.2", + }, + [ + "airflow", + "operators", + "trigger_dagrun", + "TriggerDagRunLink", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.trigger_dagrun", + name: "TriggerDagRunLink", provider: "standard", - version: "0.0.2" - }, - ["airflow", "operators", "latest_only_operator", "LatestOnlyOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.latest_only.LatestOnlyOperator", + version: "0.0.2", + }, + ["airflow", "operators", "datetime", "target_times_as_dates"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.datetime", + name: "target_times_as_dates", + provider: "standard", + version: "0.0.1", + } + } + [ + "airflow", + "operators", + "dummy" | "dummy_operator", + "EmptyOperator" | "DummyOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.empty", + name: "EmptyOperator", + provider: "standard", + version: "0.0.2", + }, + [ + "airflow", + "operators", + "latest_only_operator", + "LatestOnlyOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.latest_only", + name: "LatestOnlyOperator", provider: "standard", - version: "0.0.3" + version: "0.0.3", }, - ["airflow", "operators", "python_operator", rest @ ( - "BranchPythonOperator" + [ + "airflow", + "operators", + "python_operator", + rest @ ("BranchPythonOperator" | "PythonOperator" | "PythonVirtualenvOperator" - | "ShortCircuitOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { + | "ShortCircuitOperator"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.standard.operators.python", provider: "standard", - version: "0.0.1" - }, - ["airflow", "sensors", "external_task_sensor", rest @ ( - "ExternalTaskMarker" - | "ExternalTaskSensor" - | "ExternalTaskSensorLink" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + version: "0.0.1", + }, + [ + "airflow", + "sensors", + "external_task" | "external_task_sensor", + "ExternalTaskSensorLink", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.sensors.external_task", + name: "ExternalDagLink", + provider: "standard", + version: "0.0.3", + }, + [ + "airflow", + "sensors", + "external_task_sensor", + rest @ ("ExternalTaskMarker" | "ExternalTaskSensor"), + ] => ProviderReplacement::SourceModuleMovedToProvider { module: "airflow.providers.standard.sensors.external_task", + name: (*rest).to_string(), provider: "standard", - version: "0.0.3" + version: "0.0.3", + }, + ["airflow", "sensors", "time_delta", "WaitSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.sensors.time_delta", + name: "WaitSensor", + provider: "standard", + version: "0.0.1", }, _ => return, }; - checker.report_diagnostic(Diagnostic::new( + + let (module, name) = match &replacement { + ProviderReplacement::AutoImport { module, name, .. } => (module, *name), + ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => { + (module, name.as_str()) + } + ProviderReplacement::None => { + checker.report_diagnostic( + Airflow3MovedToProvider { + deprecated: qualified_name, + replacement, + }, + ranged, + ); + return; + } + }; + + if is_guarded_by_try_except(expr, module, name, checker.semantic()) { + return; + } + + let mut diagnostic = checker.report_diagnostic( Airflow3MovedToProvider { - deprecated: qualified_name.to_string(), - replacement, + deprecated: qualified_name, + replacement: replacement.clone(), }, - ranged.range(), - )); + ranged, + ); + + if let Some(fix) = generate_import_edit(expr, checker, module, name, ranged) + .or_else(|| generate_remove_and_runtime_import_edit(expr, checker, module, name)) + { + diagnostic.set_fix(fix); + } } diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index b73bb11e519cc..5d367eb45b25e 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -1,19 +1,19 @@ use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; use crate::rules::airflow::helpers::{ - is_airflow_builtin_or_provider, is_guarded_by_try_except, Replacement, + Replacement, generate_import_edit, generate_remove_and_runtime_import_edit, + is_airflow_builtin_or_provider, is_guarded_by_try_except, }; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::{Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_callable; use ruff_python_ast::{ - name::QualifiedName, Arguments, Expr, ExprAttribute, ExprCall, ExprContext, ExprName, - ExprStringLiteral, ExprSubscript, Stmt, StmtClassDef, StmtFunctionDef, + Arguments, Expr, ExprAttribute, ExprCall, ExprContext, ExprName, ExprStringLiteral, + ExprSubscript, Stmt, StmtClassDef, StmtFunctionDef, name::QualifiedName, }; -use ruff_python_semantic::analyze::typing; use ruff_python_semantic::Modules; use ruff_python_semantic::ScopeKind; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use ruff_text_size::TextRange; @@ -57,28 +57,27 @@ impl Violation for Airflow3Removal { } = self; match replacement { Replacement::None - | Replacement::Name(_) + | Replacement::AttrName(_) + | Replacement::Message(_) | Replacement::AutoImport { module: _, name: _ } | Replacement::SourceModuleMoved { module: _, name: _ } => { format!("`{deprecated}` is removed in Airflow 3.0") } - Replacement::Message(message) => { - format!("`{deprecated}` is removed in Airflow 3.0; {message}") - } } } fn fix_title(&self) -> Option { let Airflow3Removal { replacement, .. } = self; match replacement { - Replacement::Name(name) => Some(format!("Use `{name}` instead")), + Replacement::None => None, + Replacement::AttrName(name) => Some(format!("Use `{name}` instead")), + Replacement::Message(message) => Some((*message).to_string()), Replacement::AutoImport { module, name } => { - Some(format!("Use `{module}.{name}` instead")) + Some(format!("Use `{name}` from `{module}` instead.")) } Replacement::SourceModuleMoved { module, name } => { - Some(format!("Use `{module}.{name}` instead")) + Some(format!("Use `{name}` from `{module}` instead.")) } - _ => None, } } } @@ -101,11 +100,16 @@ pub(crate) fn airflow_3_removal_expr(checker: &Checker, expr: &Expr) { check_method(checker, call_expr); check_context_key_usage_in_call(checker, call_expr); } - Expr::Attribute(attribute_expr @ ExprAttribute { attr, .. }) => { - check_name(checker, expr, attr.range()); + Expr::Attribute(attribute_expr @ ExprAttribute { range, .. }) => { + check_name(checker, expr, *range); check_class_attribute(checker, attribute_expr); } - Expr::Name(ExprName { id, ctx, range }) => { + Expr::Name(ExprName { + id, + ctx, + range, + node_index: _, + }) => { check_name(checker, expr, *range); if matches!(ctx, ExprContext::Store) { if let ScopeKind::Class(class_def) = checker.semantic().current_scope().kind { @@ -167,13 +171,13 @@ fn check_function_parameters(checker: &Checker, function_def: &StmtFunctionDef) for param in function_def.parameters.iter_non_variadic_params() { let param_name = param.name(); if REMOVED_CONTEXT_KEYS.contains(¶m_name.as_str()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( Airflow3Removal { deprecated: param_name.to_string(), replacement: Replacement::None, }, param_name.range(), - )); + ); } } } @@ -191,29 +195,17 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum match qualified_name.segments() { ["airflow", .., "DAG" | "dag"] => { // with replacement - checker.report_diagnostics(diagnostic_for_argument( - arguments, - "fail_stop", - Some("fail_fast"), - )); - checker.report_diagnostics(diagnostic_for_argument( - arguments, - "schedule_interval", - Some("schedule"), - )); - checker.report_diagnostics(diagnostic_for_argument( - arguments, - "timetable", - Some("schedule"), - )); + diagnostic_for_argument(checker, arguments, "fail_stop", Some("fail_fast")); + diagnostic_for_argument(checker, arguments, "schedule_interval", Some("schedule")); + diagnostic_for_argument(checker, arguments, "timetable", Some("schedule")); // without replacement - checker.report_diagnostics(diagnostic_for_argument(arguments, "default_view", None)); - checker.report_diagnostics(diagnostic_for_argument(arguments, "orientation", None)); + diagnostic_for_argument(checker, arguments, "default_view", None); + diagnostic_for_argument(checker, arguments, "orientation", None); } segments => { if is_airflow_auth_manager(segments) { if !arguments.is_empty() { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( Airflow3Removal { deprecated: String::from("appbuilder"), replacement: Replacement::Message( @@ -221,36 +213,53 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum ), }, arguments.range(), - )); + ); } } else if is_airflow_task_handler(segments) { - checker.report_diagnostics(diagnostic_for_argument( - arguments, - "filename_template", - None, - )); + diagnostic_for_argument(checker, arguments, "filename_template", None); } else if is_airflow_builtin_or_provider(segments, "operators", "Operator") { - checker.report_diagnostics(diagnostic_for_argument( + diagnostic_for_argument( + checker, arguments, "task_concurrency", Some("max_active_tis_per_dag"), - )); + ); match segments { - ["airflow", .., "operators", "trigger_dagrun", "TriggerDagRunOperator"] => { - checker.report_diagnostics(diagnostic_for_argument( + [ + "airflow", + .., + "operators", + "trigger_dagrun", + "TriggerDagRunOperator", + ] => { + diagnostic_for_argument( + checker, arguments, "execution_date", Some("logical_date"), - )); + ); } - ["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] - | ["airflow", .., "operators", "weekday", "DayOfWeekSensor" | "BranchDayOfWeekOperator"] => - { - checker.report_diagnostics(diagnostic_for_argument( + [ + "airflow", + .., + "operators", + "datetime", + "BranchDateTimeOperator", + ] + | [ + "airflow", + .., + "operators", + "weekday", + "BranchDayOfWeekOperator", + ] + | ["airflow", .., "sensors", "weekday", "DayOfWeekSensor"] => { + diagnostic_for_argument( + checker, arguments, "use_task_execution_day", Some("use_task_logical_date"), - )); + ); } _ => {} } @@ -278,22 +287,22 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) { let replacement = match *qualname.segments() { ["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() { - "dataset_factories" => Replacement::Name("asset_factories"), - "dataset_uri_handlers" => Replacement::Name("asset_uri_handlers"), + "dataset_factories" => Replacement::AttrName("asset_factories"), + "dataset_uri_handlers" => Replacement::AttrName("asset_uri_handlers"), "dataset_to_openlineage_converters" => { - Replacement::Name("asset_to_openlineage_converters") + Replacement::AttrName("asset_to_openlineage_converters") } _ => return, }, ["airflow", "lineage", "hook", "DatasetLineageInfo"] => match attr.as_str() { - "dataset" => Replacement::Name("asset"), + "dataset" => Replacement::AttrName("asset"), _ => return, }, _ => return, }; // Create the `Fix` first to avoid cloning `Replacement`. - let fix = if let Replacement::Name(name) = replacement { + let fix = if let Replacement::AttrName(name) = replacement { Some(Fix::safe_edit(Edit::range_replacement( name.to_string(), attr.range(), @@ -301,7 +310,7 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) { } else { None }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( Airflow3Removal { deprecated: attr.to_string(), replacement, @@ -311,7 +320,6 @@ fn check_class_attribute(checker: &Checker, attribute_expr: &ExprAttribute) { if let Some(fix) = fix { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } /// Checks whether an Airflow 3.0–removed context key is used in a function decorated with `@task`. @@ -372,19 +380,22 @@ fn check_context_key_usage_in_call(checker: &Checker, call_expr: &ExprCall) { } for removed_key in REMOVED_CONTEXT_KEYS { - let Some(Expr::StringLiteral(ExprStringLiteral { value, range })) = - call_expr.arguments.find_positional(0) + let Some(Expr::StringLiteral(ExprStringLiteral { + value, + range, + node_index: _, + })) = call_expr.arguments.find_positional(0) else { continue; }; if value == removed_key { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( Airflow3Removal { deprecated: removed_key.to_string(), replacement: Replacement::None, }, *range, - )); + ); } } } @@ -419,13 +430,13 @@ fn check_context_key_usage_in_subscript(checker: &Checker, subscript: &ExprSubsc } if REMOVED_CONTEXT_KEYS.contains(&key.to_str()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( Airflow3Removal { deprecated: key.to_string(), replacement: Replacement::None, }, slice.range(), - )); + ); } } @@ -466,61 +477,65 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) { let replacement = match qualname.segments() { ["airflow", "datasets", "manager", "DatasetManager"] => match attr.as_str() { - "register_dataset_change" => Replacement::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"), + "register_dataset_change" => Replacement::AttrName("register_asset_change"), + "create_datasets" => Replacement::AttrName("create_assets"), + "notify_dataset_created" => Replacement::AttrName("notify_asset_created"), + "notify_dataset_changed" => Replacement::AttrName("notify_asset_changed"), + "notify_dataset_alias_created" => Replacement::AttrName("notify_asset_alias_created"), _ => return, }, ["airflow", "lineage", "hook", "HookLineageCollector"] => match attr.as_str() { - "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"), + "create_dataset" => Replacement::AttrName("create_asset"), + "add_input_dataset" => Replacement::AttrName("add_input_asset"), + "add_output_dataset" => Replacement::AttrName("add_output_asset"), + "collected_datasets" => Replacement::AttrName("collected_assets"), _ => return, }, - ["airflow", "providers", "amazon", "auth_manager", "aws_auth_manager", "AwsAuthManager"] => { - match attr.as_str() { - "is_authorized_dataset" => Replacement::Name("is_authorized_asset"), - _ => return, - } - } ["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() { "initialize_providers_dataset_uri_resources" => { - Replacement::Name("initialize_providers_asset_uri_resources") + Replacement::AttrName("initialize_providers_asset_uri_resources") } _ => return, }, - ["airflow", "secrets", "local_filesystem", "LocalFilesystemBackend"] => match attr.as_str() - { - "get_connections" => Replacement::Name("get_connection"), + [ + "airflow", + "secrets", + "local_filesystem", + "LocalFilesystemBackend", + ] => match attr.as_str() { + "get_connections" => Replacement::AttrName("get_connection"), _ => return, }, ["airflow", "datasets", ..] | ["airflow", "Dataset"] => match attr.as_str() { - "iter_datasets" => Replacement::Name("iter_assets"), - "iter_dataset_aliases" => Replacement::Name("iter_asset_aliases"), + "iter_datasets" => Replacement::AttrName("iter_assets"), + "iter_dataset_aliases" => Replacement::AttrName("iter_asset_aliases"), _ => return, }, segments => { if is_airflow_secret_backend(segments) { match attr.as_str() { - "get_conn_uri" => Replacement::Name("get_conn_value"), - "get_connections" => Replacement::Name("get_connection"), + "get_conn_uri" => Replacement::AttrName("get_conn_value"), + "get_connections" => Replacement::AttrName("get_connection"), _ => return, } } else if is_airflow_hook(segments) { match attr.as_str() { - "get_connections" => Replacement::Name("get_connection"), + "get_connections" => Replacement::AttrName("get_connection"), _ => return, } + } else if is_airflow_auth_manager(segments) { + if attr.as_str() == "is_authorized_dataset" { + Replacement::AttrName("is_authorized_asset") + } else { + return; + } } else { return; } } }; // Create the `Fix` first to avoid cloning `Replacement`. - let fix = if let Replacement::Name(name) = replacement { + let fix = if let Replacement::AttrName(name) = replacement { Some(Fix::safe_edit(Edit::range_replacement( name.to_string(), attr.range(), @@ -529,7 +544,7 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) { None }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( Airflow3Removal { deprecated: attr.to_string(), replacement, @@ -539,7 +554,6 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) { if let Some(fix) = fix { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } /// Check whether a removed Airflow name is used. @@ -565,38 +579,57 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { let replacement = match qualified_name.segments() { // airflow.PY\d{1,2} - ["airflow", "PY36" | "PY37" | "PY38" | "PY39" | "PY310" | "PY311" | "PY312"] => { - Replacement::Name("sys.version_info") - } + [ + "airflow", + "PY36" | "PY37" | "PY38" | "PY39" | "PY310" | "PY311" | "PY312", + ] => Replacement::Message("Use `sys.version_info` instead"), // airflow.api_connexion.security - ["airflow", "api_connexion", "security", "requires_access"] => { - Replacement::Name("airflow.api_connexion.security.requires_access_*") - } - ["airflow", "api_connexion", "security", "requires_access_dataset"] => { - Replacement::AutoImport { - module: "airflow.api_connexion.security", - name: "requires_access_asset", - } - } + ["airflow", "api_connexion", "security", "requires_access"] => Replacement::Message( + "Use `airflow.api_fastapi.core_api.security.requires_access_*` instead", + ), + [ + "airflow", + "api_connexion", + "security", + "requires_access_dataset", + ] => Replacement::AutoImport { + module: "airflow.api_fastapi.core_api.security", + name: "requires_access_asset", + }, // airflow.auth.managers - ["airflow", "auth", "managers", "models", "resource_details", "DatasetDetails"] => { - Replacement::Name( - "airflow.api_fastapi.auth.managers.models.resource_details.AssetDetails", - ) - } - ["airflow", "auth", "managers", "base_auth_manager", "is_authorized_dataset"] => { - Replacement::Name( - "airflow.api_fastapi.auth.managers.base_auth_manager.is_authorized_asset", - ) - } + [ + "airflow", + "auth", + "managers", + "base_auth_manager", + "BaseAuthManager", + ] => Replacement::AutoImport { + module: "airflow.api_fastapi.auth.managers.base_auth_manager", + name: "BaseAuthManager", + }, + [ + "airflow", + "auth", + "managers", + "models", + "resource_details", + "DatasetDetails", + ] => Replacement::AutoImport { + module: "airflow.api_fastapi.auth.managers.models.resource_details", + name: "AssetDetails", + }, // airflow.configuration - ["airflow", "configuration", rest @ ("as_dict" | "get" | "getboolean" | "getfloat" | "getint" | "has_option" - | "remove_option" | "set")] => Replacement::SourceModuleMoved { - module: "airflow.configuration.conf", - name: (*rest).to_string(), + [ + "airflow", + "configuration", + rest @ ("as_dict" | "get" | "getboolean" | "getfloat" | "getint" | "has_option" + | "remove_option" | "set"), + ] => Replacement::SourceModuleMoved { + module: "airflow.configuration", + name: format!("conf.{rest}"), }, // airflow.contrib.* @@ -606,76 +639,109 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.datasets.manager ["airflow", "datasets", "manager", rest] => match *rest { - "DatasetManager" => Replacement::Name("airflow.assets.manager.AssetManager"), - "dataset_manager" => Replacement::Name("airflow.assets.manager.asset_manager"), - "resolve_dataset_manager" => Replacement::Name("airflow.assets.resolve_asset_manager"), + "DatasetManager" => Replacement::AutoImport { + module: "airflow.assets.manager", + name: "AssetManager", + }, + "dataset_manager" => Replacement::AutoImport { + module: "airflow.assets.manager", + name: "asset_manager", + }, + "resolve_dataset_manager" => Replacement::AutoImport { + module: "airflow.assets.manager", + name: "resolve_asset_manager", + }, _ => return, }, // airflow.datasets ["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None, // airflow.hooks - ["airflow", "hooks", "base_hook", "BaseHook"] => { - Replacement::Name("airflow.hooks.base.BaseHook") - } + ["airflow", "hooks", "base_hook", "BaseHook"] => Replacement::AutoImport { + module: "airflow.hooks.base", + name: "BaseHook", + }, // airflow.lineage.hook - ["airflow", "lineage", "hook", "DatasetLineageInfo"] => { - Replacement::Name("airflow.lineage.hook.AssetLineageInfo") - } + ["airflow", "lineage", "hook", "DatasetLineageInfo"] => Replacement::AutoImport { + module: "airflow.lineage.hook", + name: "AssetLineageInfo", + }, // airflow.listeners.spec - // TODO: this is removed ["airflow", "listeners", "spec", "dataset", rest] => match *rest { - "on_dataset_created" => { - Replacement::Name("airflow.listeners.spec.asset.on_asset_created") - } - "on_dataset_changed" => { - Replacement::Name("airflow.listeners.spec.asset.on_asset_changed") - } + "on_dataset_created" => Replacement::AutoImport { + module: "airflow.listeners.spec.asset", + name: "on_asset_created", + }, + "on_dataset_changed" => Replacement::AutoImport { + module: "airflow.listeners.spec.asset", + name: "on_asset_changed", + }, _ => return, }, // airflow.metrics.validators ["airflow", "metrics", "validators", rest] => match *rest { - "AllowListValidator" => { - Replacement::Name("airflow.metrics.validators.PatternAllowListValidator") - } - "BlockListValidator" => { - Replacement::Name("airflow.metrics.validators.PatternBlockListValidator") - } + "AllowListValidator" => Replacement::AutoImport { + module: "airflow.metrics.validators", + name: "PatternAllowListValidator", + }, + "BlockListValidator" => Replacement::AutoImport { + module: "airflow.metrics.validators", + name: "PatternBlockListValidator", + }, _ => return, }, // airflow.notifications - ["airflow", "notifications", "basenotifier", "BaseNotifier"] => { - Replacement::Name("airflow.sdk.BaseNotifier") - } + ["airflow", "notifications", "basenotifier", "BaseNotifier"] => Replacement::AutoImport { + module: "airflow.sdk.bases.notifier", + name: "BaseNotifier", + }, // airflow.operators ["airflow", "operators", "subdag", ..] => { Replacement::Message("The whole `airflow.subdag` module has been removed.") } + ["airflow", "operators", "python", "get_current_context"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "get_current_context", + }, // airflow.secrets - ["airflow", "secrets", "local_filesystem", "load_connections"] => { - Replacement::Name("airflow.secrets.local_filesystem.load_connections_dict") - } + ["airflow", "secrets", "local_filesystem", "load_connections"] => Replacement::AutoImport { + module: "airflow.secrets.local_filesystem", + name: "load_connections_dict", + }, // airflow.security - ["airflow", "security", "permissions", "RESOURCE_DATASET"] => { - Replacement::Name("airflow.security.permissions.RESOURCE_ASSET") - } + ["airflow", "security", "permissions", "RESOURCE_DATASET"] => Replacement::AutoImport { + module: "airflow.security.permissions", + name: "RESOURCE_ASSET", + }, // airflow.sensors - ["airflow", "sensors", "base_sensor_operator", "BaseSensorOperator"] => { - Replacement::Name("airflow.sdk.bases.sensor.BaseSensorOperator") - } + [ + "airflow", + "sensors", + "base_sensor_operator", + "BaseSensorOperator", + ] => Replacement::AutoImport { + module: "airflow.sdk.bases.sensor", + name: "BaseSensorOperator", + }, // airflow.timetables - ["airflow", "timetables", "simple", "DatasetTriggeredTimetable"] => { - Replacement::Name("airflow.timetables.simple.AssetTriggeredTimetable") - } + [ + "airflow", + "timetables", + "simple", + "DatasetTriggeredTimetable", + ] => Replacement::AutoImport { + module: "airflow.timetables.simple", + name: "AssetTriggeredTimetable", + }, // airflow.triggers ["airflow", "triggers", "external_task", "TaskStateTrigger"] => Replacement::None, @@ -685,11 +751,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.utils.dag_cycle_tester ["dag_cycle_tester", "test_cycle"] => Replacement::None, - // airflow.utils.dag_parsing_context - ["dag_parsing_context", "get_parsing_context"] => { - Replacement::Name("airflow.sdk.get_parsing_context") - } - // airflow.utils.db ["db", "create_session"] => Replacement::None, @@ -700,142 +761,237 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.utils.dates ["dates", "date_range"] => Replacement::None, - ["dates", "days_ago"] => Replacement::Name("pendulum.today('UTC').add(days=-N, ...)"), - ["dates", "parse_execution_date" | "round_time" | "scale_time_units" | "infer_time_unit"] => { - Replacement::None + ["dates", "days_ago"] => { + Replacement::Message("Use `pendulum.today('UTC').add(days=-N, ...)` instead") } + [ + "dates", + "parse_execution_date" | "round_time" | "scale_time_units" | "infer_time_unit", + ] => Replacement::None, // airflow.utils.file - ["file", "TemporaryDirectory"] => Replacement::Name("tempfile.TemporaryDirectory"), - ["file", "mkdirs"] => Replacement::Name("pathlib.Path({path}).mkdir"), + ["file", "TemporaryDirectory"] => Replacement::AutoImport { + module: "tempfile", + name: "TemporaryDirectory", + }, + ["file", "mkdirs"] => Replacement::Message("Use `pathlib.Path({path}).mkdir` instead"), // airflow.utils.helpers - ["helpers", "chain"] => Replacement::Name("airflow.sdk.chain"), - ["helpers", "cross_downstream"] => Replacement::Name("airflow.sdk.cross_downstream"), + ["helpers", "chain"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "chain", + }, + ["helpers", "cross_downstream"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "cross_downstream", + }, + // TODO: update it as SourceModuleMoved // airflow.utils.log.secrets_masker - ["log", "secrets_masker"] => { - Replacement::Name("airflow.sdk.execution_time.secrets_masker") - } + ["log", "secrets_masker"] => Replacement::AutoImport { + module: "airflow.sdk.execution_time", + name: "secrets_masker", + }, // airflow.utils.state ["state", "SHUTDOWN" | "terminating_states"] => Replacement::None, // airflow.utils.trigger_rule - ["trigger_rule", "TriggerRule", "DUMMY" | "NONE_FAILED_OR_SKIPPED"] => { - Replacement::None - } + [ + "trigger_rule", + "TriggerRule", + "DUMMY" | "NONE_FAILED_OR_SKIPPED", + ] => Replacement::None, _ => return, }, // airflow.www - // TODO: www has been removed - ["airflow", "www", "auth", "has_access"] => { - Replacement::Name("airflow.www.auth.has_access_*") - } - ["airflow", "www", "auth", "has_access_dataset"] => { - Replacement::Name("airflow.www.auth.has_access_dataset") - } - ["airflow", "www", "utils", "get_sensitive_variables_fields"] => { - Replacement::Name("airflow.utils.log.secrets_masker.get_sensitive_variables_fields") - } - ["airflow", "www", "utils", "should_hide_value_for_key"] => { - Replacement::Name("airflow.utils.log.secrets_masker.should_hide_value_for_key") - } + [ + "airflow", + "www", + "auth", + "has_access" | "has_access_dataset", + ] => Replacement::None, + [ + "airflow", + "www", + "utils", + "get_sensitive_variables_fields" | "should_hide_value_for_key", + ] => Replacement::None, // airflow.providers.amazon - ["airflow", "providers", "amazon", "aws", rest @ ..] => match &rest { - ["datasets", "s3", "create_dataset"] => { - Replacement::Name("airflow.providers.amazon.aws.assets.s3.create_asset") - } - ["datasets", "s3", "convert_dataset_to_openlineage"] => Replacement::Name( - "airflow.providers.amazon.aws.assets.s3.convert_asset_to_openlineage", - ), - ["datasets", "s3", "sanitize_uri"] => { - Replacement::Name("airflow.providers.amazon.aws.assets.s3.sanitize_uri") - } - ["auth_manager", "avp", "entities", "AvpEntities", "DATASET"] => Replacement::Name( - "airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.ASSET", - ), + [ + "airflow", + "providers", + "amazon", + "aws", + "datasets", + "s3", + rest, + ] => match *rest { + "create_dataset" => Replacement::AutoImport { + module: "airflow.providers.amazon.aws.assets.s3", + name: "create_asset", + }, + "convert_dataset_to_openlineage" => Replacement::AutoImport { + module: "airflow.providers.amazon.aws.assets.s3", + name: "convert_asset_to_openlineage", + }, + "sanitize_uri" => Replacement::AutoImport { + module: "airflow.providers.amazon.aws.assets.s3", + name: "sanitize_uri", + }, _ => return, }, + [ + "airflow", + "providers", + "amazon", + "aws", + "auth_manager", + "avp", + "entities", + "AvpEntities", + "DATASET", + ] => Replacement::AutoImport { + module: "airflow.providers.amazon.aws.auth_manager.avp.entities", + name: "AvpEntities.ASSET", + }, // airflow.providers.common.io // airflow.providers.common.io.datasets.file - ["airflow", "providers", "common", "io", "datasets", "file", rest] => match *rest { - "create_dataset" => { - Replacement::Name("airflow.providers.common.io.assets.file.create_asset") - } - "convert_dataset_to_openlineage" => Replacement::Name( - "airflow.providers.common.io.assets.file.convert_asset_to_openlineage", - ), - "sanitize_uri" => { - Replacement::Name("airflow.providers.common.io.assets.file.sanitize_uri") - } + [ + "airflow", + "providers", + "common", + "io", + "datasets", + "file", + rest, + ] => match *rest { + "create_dataset" => Replacement::AutoImport { + module: "airflow.providers.common.io.assets.file", + name: "create_asset", + }, + "convert_dataset_to_openlineage" => Replacement::AutoImport { + module: "airflow.providers.common.io.assets.file", + name: "convert_asset_to_openlineage", + }, + "sanitize_uri" => Replacement::AutoImport { + module: "airflow.providers.common.io.assets.file", + name: "sanitize_uri", + }, _ => return, }, - // airflow.providers.fab - ["airflow", "providers", "fab", "auth_manager", "fab_auth_manager", "is_authorized_dataset"] => { - Replacement::Name( - "airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset", - ) - } - // airflow.providers.google // airflow.providers.google.datasets ["airflow", "providers", "google", "datasets", rest @ ..] => match &rest { - ["bigquery", "create_dataset"] => { - Replacement::Name("airflow.providers.google.assets.bigquery.create_asset") - } - ["gcs", "create_dataset"] => { - Replacement::Name("airflow.providers.google.assets.gcs.create_asset") - } - ["gcs", "convert_dataset_to_openlineage"] => Replacement::Name( - "airflow.providers.google.assets.gcs.convert_asset_to_openlineage", - ), - ["gcs", "sanitize_uri"] => { - Replacement::Name("airflow.providers.google.assets.gcs.sanitize_uri") - } + ["bigquery", "create_dataset"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.bigquery", + name: "create_asset", + }, + ["gcs", "create_dataset"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.gcs", + name: "create_asset", + }, + ["gcs", "convert_dataset_to_openlineage"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.gcs", + name: "convert_asset_to_openlineage", + }, + ["gcs", "sanitize_uri"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.gcs", + name: "sanitize_uri", + }, + _ => return, }, // airflow.providers.mysql - ["airflow", "providers", "mysql", "datasets", "mysql", "sanitize_uri"] => { - Replacement::Name("airflow.providers.mysql.assets.mysql.sanitize_uri") - } + [ + "airflow", + "providers", + "mysql", + "datasets", + "mysql", + "sanitize_uri", + ] => Replacement::AutoImport { + module: "airflow.providers.mysql.assets.mysql", + name: "sanitize_uri", + }, // airflow.providers.postgres - ["airflow", "providers", "postgres", "datasets", "postgres", "sanitize_uri"] => { - Replacement::Name("airflow.providers.postgres.assets.postgres.sanitize_uri") - } + [ + "airflow", + "providers", + "postgres", + "datasets", + "postgres", + "sanitize_uri", + ] => Replacement::AutoImport { + module: "airflow.providers.postgres.assets.postgres", + name: "sanitize_uri", + }, // airflow.providers.openlineage // airflow.providers.openlineage.utils.utils - ["airflow", "providers", "openlineage", "utils", "utils", rest] => match *rest { - "DatasetInfo" => { - Replacement::Name("airflow.providers.openlineage.utils.utils.AssetInfo") - } + [ + "airflow", + "providers", + "openlineage", + "utils", + "utils", + rest, + ] => match *rest { + "DatasetInfo" => Replacement::AutoImport { + module: "airflow.providers.openlineage.utils.utils", + name: "AssetInfo", + }, - "translate_airflow_dataset" => Replacement::Name( - "airflow.providers.openlineage.utils.utils.translate_airflow_asset", - ), + "translate_airflow_dataset" => Replacement::AutoImport { + module: "airflow.providers.openlineage.utils.utils", + name: "translate_airflow_asset", + }, _ => return, }, // airflow.providers.trino - ["airflow", "providers", "trino", "datasets", "trino", "sanitize_uri"] => { - Replacement::Name("airflow.providers.trino.assets.trino.sanitize_uri") - } + [ + "airflow", + "providers", + "trino", + "datasets", + "trino", + "sanitize_uri", + ] => Replacement::AutoImport { + module: "airflow.providers.trino.assets.trino", + name: "sanitize_uri", + }, _ => return, }; - if is_guarded_by_try_except(expr, &replacement, semantic) { + let (module, name) = match &replacement { + Replacement::AutoImport { module, name } => (module, *name), + Replacement::SourceModuleMoved { module, name } => (module, name.as_str()), + _ => { + checker.report_diagnostic( + Airflow3Removal { + deprecated: qualified_name.to_string(), + replacement: replacement.clone(), + }, + range, + ); + return; + } + }; + + if is_guarded_by_try_except(expr, module, name, checker.semantic()) { return; } - let mut diagnostic = Diagnostic::new( + let import_target = name.split('.').next().unwrap_or(name); + let mut diagnostic = checker.report_diagnostic( Airflow3Removal { deprecated: qualified_name.to_string(), replacement: replacement.clone(), @@ -843,19 +999,11 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { range, ); - if let Replacement::AutoImport { module, name } = replacement { - diagnostic.try_set_fix(|| { - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from(module, name), - expr.start(), - checker.semantic(), - )?; - let replacement_edit = Edit::range_replacement(binding, range); - Ok(Fix::safe_edits(import_edit, [replacement_edit])) - }); + if let Some(fix) = generate_import_edit(expr, checker, module, import_target, range) + .or_else(|| generate_remove_and_runtime_import_edit(expr, checker, module, name)) + { + diagnostic.set_fix(fix); } - - checker.report_diagnostic(diagnostic); } /// Check whether a customized Airflow plugin contains removed extensions. @@ -887,7 +1035,7 @@ fn check_airflow_plugin_extension( ) }) }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( Airflow3Removal { deprecated: name.to_string(), replacement: Replacement::Message( @@ -895,7 +1043,7 @@ fn check_airflow_plugin_extension( ), }, expr.range(), - )); + ); } } } @@ -903,33 +1051,35 @@ fn check_airflow_plugin_extension( /// Check if the `deprecated` keyword argument is being used and create a diagnostic if so along /// with a possible `replacement`. fn diagnostic_for_argument( + checker: &Checker, arguments: &Arguments, deprecated: &str, replacement: Option<&'static str>, -) -> Option { - let keyword = arguments.find_keyword(deprecated)?; - let mut diagnostic = Diagnostic::new( +) { + let Some(keyword) = arguments.find_keyword(deprecated) else { + return; + }; + let range = keyword + .arg + .as_ref() + .map_or_else(|| keyword.range(), Ranged::range); + let mut diagnostic = checker.report_diagnostic( Airflow3Removal { deprecated: deprecated.to_string(), replacement: match replacement { - Some(name) => Replacement::Name(name), + Some(name) => Replacement::AttrName(name), None => Replacement::None, }, }, - keyword - .arg - .as_ref() - .map_or_else(|| keyword.range(), Ranged::range), + range, ); if let Some(replacement) = replacement { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( replacement.to_string(), - diagnostic.range, + range, ))); } - - Some(diagnostic) } /// Check whether the symbol is coming from the `secrets` builtin or provider module which ends diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index 3fb23f6879d51..9963635c2ef5b 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -1,13 +1,16 @@ -use crate::rules::airflow::helpers::ProviderReplacement; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::Checker; +use crate::rules::airflow::helpers::{ + ProviderReplacement, generate_import_edit, generate_remove_and_runtime_import_edit, + is_guarded_by_try_except, +}; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{Expr, ExprAttribute}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use ruff_text_size::TextRange; -use crate::checkers::ast::Checker; - /// ## What it does /// Checks for uses of Airflow functions and values that have been moved to its providers /// but still have a compatibility layer (e.g., `apache-airflow-providers-standard`). @@ -21,19 +24,40 @@ use crate::checkers::ast::Checker; /// ## Example /// ```python /// from airflow.operators.python import PythonOperator +/// +/// +/// def print_context(ds=None, **kwargs): +/// print(kwargs) +/// print(ds) +/// +/// +/// print_the_context = PythonOperator( +/// task_id="print_the_context", python_callable=print_context +/// ) /// ``` /// /// Use instead: /// ```python /// from airflow.providers.standard.operators.python import PythonOperator +/// +/// +/// def print_context(ds=None, **kwargs): +/// print(kwargs) +/// print(ds) +/// +/// +/// print_the_context = PythonOperator( +/// task_id="print_the_context", python_callable=print_context +/// ) /// ``` #[derive(ViolationMetadata)] -pub(crate) struct Airflow3SuggestedToMoveToProvider { - deprecated: String, +pub(crate) struct Airflow3SuggestedToMoveToProvider<'a> { + deprecated: QualifiedName<'a>, replacement: ProviderReplacement, } -impl Violation for Airflow3SuggestedToMoveToProvider { +impl Violation for Airflow3SuggestedToMoveToProvider<'_> { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { let Airflow3SuggestedToMoveToProvider { @@ -41,8 +65,12 @@ impl Violation for Airflow3SuggestedToMoveToProvider { replacement, } = self; match replacement { - ProviderReplacement::ProviderName { + ProviderReplacement::None => { + format!("`{deprecated}` is removed in Airflow 3.0") + } + ProviderReplacement::AutoImport { name: _, + module: _, provider, version: _, } @@ -52,8 +80,9 @@ impl Violation for Airflow3SuggestedToMoveToProvider { provider, version: _, } => { - format!("`{deprecated}` is deprecated and moved into `{provider}` provider in Airflow 3.0; \ - It still works in Airflow 3.0 but is expected to be removed in a future version." + format!( + "`{deprecated}` is deprecated and moved into `{provider}` provider in Airflow 3.0; \ + It still works in Airflow 3.0 but is expected to be removed in a future version." ) } } @@ -62,23 +91,23 @@ impl Violation for Airflow3SuggestedToMoveToProvider { fn fix_title(&self) -> Option { let Airflow3SuggestedToMoveToProvider { replacement, .. } = self; match replacement { - ProviderReplacement::ProviderName { - name, - provider, - version, - } => { - Some(format!( - "Install `apache-airflow-provider-{provider}>={version}` and use `{name}` instead." - )) - }, - ProviderReplacement::SourceModuleMovedToProvider { + ProviderReplacement::None => None, + ProviderReplacement::AutoImport { + module, name, + provider, + version, + } => Some(format!( + "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` from `{module}` instead." + )), + ProviderReplacement::SourceModuleMovedToProvider { module, + name, provider, version, - } => { - Some(format!("Install `apache-airflow-provider-{provider}>={version}` and use `{module}.{name}` instead.")) - } + } => Some(format!( + "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` from `{module}` instead." + )), } } } @@ -105,145 +134,205 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan let replacement = match qualified_name.segments() { // apache-airflow-providers-standard - ["airflow", "hooks", "filesystem", "FSHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.hooks.filesystem.FSHook", + ["airflow", "hooks", "filesystem", "FSHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.hooks.filesystem", + name: "FSHook", provider: "standard", version: "0.0.1", }, ["airflow", "hooks", "package_index", "PackageIndexHook"] => { - ProviderReplacement::ProviderName { - name: "airflow.providers.standard.hooks.package_index.PackageIndexHook", + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.hooks.package_index", + name: "PackageIndexHook", provider: "standard", version: "0.0.1", } } - ["airflow", "hooks", "subprocess", rest @ ("SubprocessHook" | "SubprocessResult" | "working_directory")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.hooks.subprocess", - provider: "standard", - version: "0.0.3", - } - } - ["airflow", "operators", "bash", "BashOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.bash.BashOperator", + ["airflow", "hooks", "subprocess", "SubprocessHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.hooks.subprocess", + name: "SubprocessHook", + provider: "standard", + version: "0.0.3", + }, + ["airflow", "operators", "bash", "BashOperator"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.bash", + name: "BashOperator", provider: "standard", version: "0.0.1", }, - ["airflow", "operators", "datetime", rest @ ("BranchDateTimeOperator" | "target_times_as_dates")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.time.operators.datetime", + ["airflow", "operators", "datetime", "BranchDateTimeOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.datetime", + name: "BranchDateTimeOperator", provider: "standard", version: "0.0.1", } } - ["airflow", "operators", "trigger_dagrun", rest @ ("TriggerDagRunLink" | "TriggerDagRunOperator")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.operators.trigger_dagrun", - provider: "standard", - version: "0.0.2", - } - } - ["airflow", "operators", "empty", "EmptyOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.empty.EmptyOperator", + [ + "airflow", + "operators", + "trigger_dagrun", + "TriggerDagRunOperator", + ] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.trigger_dagrun", + name: "TriggerDagRunOperator", + provider: "standard", + version: "0.0.2", + }, + ["airflow", "operators", "empty", "EmptyOperator"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.empty", + name: "EmptyOperator", provider: "standard", version: "0.0.2", }, ["airflow", "operators", "latest_only", "LatestOnlyOperator"] => { - ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.latest_only.LatestOnlyOperator", + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.latest_only", + name: "LatestOnlyOperator", provider: "standard", version: "0.0.3", } } - ["airflow", "operators", "python", rest @ ("BranchPythonOperator" - | "PythonOperator" - | "PythonVirtualenvOperator" - | "ShortCircuitOperator")] => ProviderReplacement::SourceModuleMovedToProvider { + [ + "airflow", + "operators", + "python", + rest @ ("BranchPythonOperator" + | "PythonOperator" + | "PythonVirtualenvOperator" + | "ShortCircuitOperator"), + ] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.standard.operators.python", provider: "standard", version: "0.0.1", }, ["airflow", "operators", "weekday", "BranchDayOfWeekOperator"] => { - ProviderReplacement::ProviderName { - name: "airflow.providers.standard.time.operators.weekday.BranchDayOfWeekOperator", + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.weekday", + name: "BranchDayOfWeekOperator", provider: "standard", version: "0.0.1", } } - ["airflow", "sensors", "date_time", rest @ ("DateTimeSensor" | "DateTimeSensorAsync")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.time.sensors.date_time", - provider: "standard", - version: "0.0.1", - } - } - ["airflow", "sensors", "external_task", rest @ ("ExternalTaskMarker" | "ExternalTaskSensor" | "ExternalTaskSensorLink")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.sensors.external_task", - provider: "standard", - version: "0.0.3", - } - } - ["airflow", "sensors", "filesystem", "FileSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.sensors.filesystem.FileSensor", + [ + "airflow", + "sensors", + "date_time", + rest @ ("DateTimeSensor" | "DateTimeSensorAsync"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.sensors.date_time", + provider: "standard", + version: "0.0.1", + }, + [ + "airflow", + "sensors", + "external_task", + rest @ ("ExternalTaskMarker" | "ExternalTaskSensor" | "ExternalTaskSensorLink"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.sensors.external_task", + provider: "standard", + version: "0.0.3", + }, + ["airflow", "sensors", "filesystem", "FileSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.sensors.filesystem", + name: "FileSensor", provider: "standard", version: "0.0.2", }, - ["airflow", "sensors", "time_sensor", rest @ ("TimeSensor" | "TimeSensorAsync")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.time.sensors.time", - provider: "standard", - version: "0.0.1", - } - } - ["airflow", "sensors", "time_delta", rest @ ("TimeDeltaSensor" | "TimeDeltaSensorAsync" | "WaitSensor")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.time.sensors.time_delta", - provider: "standard", - version: "0.0.1", - } - } - ["airflow", "sensors", "weekday", "DayOfWeekSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.time.sensors.weekday.DayOfWeekSensor", + [ + "airflow", + "sensors", + "time_sensor", + rest @ ("TimeSensor" | "TimeSensorAsync"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.sensors.time", provider: "standard", version: "0.0.1", }, - ["airflow", "triggers", "external_task", rest @ ("DagStateTrigger" | "WorkflowTrigger")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.triggers.external_task", - provider: "standard", - version: "0.0.3", - } - } - ["airflow", "triggers", "file", "FileTrigger"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.triggers.file.FileTrigger", + [ + "airflow", + "sensors", + "time_delta", + rest @ ("TimeDeltaSensor" | "TimeDeltaSensorAsync"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.sensors.time_delta", + provider: "standard", + version: "0.0.1", + }, + ["airflow", "sensors", "weekday", "DayOfWeekSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.sensors.weekday", + name: "DayOfWeekSensor", + provider: "standard", + version: "0.0.1", + }, + [ + "airflow", + "triggers", + "external_task", + rest @ ("DagStateTrigger" | "WorkflowTrigger"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.triggers.external_task", + provider: "standard", + version: "0.0.3", + }, + ["airflow", "triggers", "file", "FileTrigger"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.triggers.file", + name: "FileTrigger", + provider: "standard", + version: "0.0.3", + }, + [ + "airflow", + "triggers", + "temporal", + rest @ ("DateTimeTrigger" | "TimeDeltaTrigger"), + ] => ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.triggers.temporal", provider: "standard", version: "0.0.3", }, - ["airflow", "triggers", "temporal", rest @ ("DateTimeTrigger" | "TimeDeltaTrigger")] => { - ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.triggers.temporal", - provider: "standard", - version: "0.0.3", - } - } _ => return, }; - checker.report_diagnostic(Diagnostic::new( + + let (module, name) = match &replacement { + ProviderReplacement::AutoImport { module, name, .. } => (module, *name), + ProviderReplacement::SourceModuleMovedToProvider { module, name, .. } => { + (module, name.as_str()) + } + ProviderReplacement::None => { + checker.report_diagnostic( + Airflow3SuggestedToMoveToProvider { + deprecated: qualified_name, + replacement: replacement.clone(), + }, + ranged.range(), + ); + return; + } + }; + + if is_guarded_by_try_except(expr, module, name, checker.semantic()) { + return; + } + let mut diagnostic = checker.report_diagnostic( Airflow3SuggestedToMoveToProvider { - deprecated: qualified_name.to_string(), - replacement, + deprecated: qualified_name, + replacement: replacement.clone(), }, - ranged.range(), - )); + ranged, + ); + + if let Some(fix) = generate_import_edit(expr, checker, module, name, ranged) + .or_else(|| generate_remove_and_runtime_import_edit(expr, checker, module, name)) + { + diagnostic.set_fix(fix); + } } diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs index 9d500df660111..b5e26ea2aba1b 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs @@ -1,11 +1,11 @@ use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; +use crate::rules::airflow::helpers::{Replacement, is_airflow_builtin_or_provider}; use crate::rules::airflow::helpers::{ - is_airflow_builtin_or_provider, is_guarded_by_try_except, Replacement, + generate_import_edit, generate_remove_and_runtime_import_edit, is_guarded_by_try_except, }; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{name::QualifiedName, Arguments, Expr, ExprAttribute, ExprCall, ExprName}; +use crate::{Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Arguments, Expr, ExprAttribute, ExprCall, ExprName, name::QualifiedName}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use ruff_text_size::TextRange; @@ -53,7 +53,8 @@ impl Violation for Airflow3SuggestedUpdate { } = self; match replacement { Replacement::None - | Replacement::Name(_) + | Replacement::AttrName(_) + | Replacement::Message(_) | Replacement::AutoImport { module: _, name: _ } | Replacement::SourceModuleMoved { module: _, name: _ } => { format!( @@ -61,27 +62,21 @@ impl Violation for Airflow3SuggestedUpdate { It still works in Airflow 3.0 but is expected to be removed in a future version." ) } - Replacement::Message(message) => { - format!( - "`{deprecated}` is removed in Airflow 3.0; \ - It still works in Airflow 3.0 but is expected to be removed in a future version.; \ - {message}" - ) - } } } fn fix_title(&self) -> Option { let Airflow3SuggestedUpdate { replacement, .. } = self; match replacement { - Replacement::Name(name) => Some(format!("Use `{name}` instead")), + Replacement::None => None, + Replacement::AttrName(name) => Some(format!("Use `{name}` instead")), + Replacement::Message(message) => Some((*message).to_string()), Replacement::AutoImport { module, name } => { - Some(format!("Use `{module}.{name}` instead")) + Some(format!("Use `{name}` from `{module}` instead.")) } Replacement::SourceModuleMoved { module, name } => { - Some(format!("Use `{module}.{name}` instead")) + Some(format!("Use `{name}` from `{module}` instead.")) } - _ => None, } } } @@ -107,6 +102,7 @@ pub(crate) fn airflow_3_0_suggested_update_expr(checker: &Checker, expr: &Expr) id: _, ctx: _, range, + node_index: _, }) => { check_name(checker, expr, *range); } @@ -117,33 +113,35 @@ pub(crate) fn airflow_3_0_suggested_update_expr(checker: &Checker, expr: &Expr) /// Check if the `deprecated` keyword argument is being used and create a diagnostic if so along /// with a possible `replacement`. fn diagnostic_for_argument( + checker: &Checker, arguments: &Arguments, deprecated: &str, replacement: Option<&'static str>, -) -> Option { - let keyword = arguments.find_keyword(deprecated)?; - let mut diagnostic = Diagnostic::new( +) { + let Some(keyword) = arguments.find_keyword(deprecated) else { + return; + }; + let range = keyword + .arg + .as_ref() + .map_or_else(|| keyword.range(), Ranged::range); + let mut diagnostic = checker.report_diagnostic( Airflow3SuggestedUpdate { deprecated: deprecated.to_string(), replacement: match replacement { - Some(name) => Replacement::Name(name), + Some(name) => Replacement::AttrName(name), None => Replacement::None, }, }, - keyword - .arg - .as_ref() - .map_or_else(|| keyword.range(), Ranged::range), + range, ); if let Some(replacement) = replacement { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( replacement.to_string(), - diagnostic.range, + range, ))); } - - Some(diagnostic) } /// Check whether a removed Airflow argument is passed. /// @@ -157,15 +155,11 @@ fn diagnostic_for_argument( fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, arguments: &Arguments) { match qualified_name.segments() { ["airflow", .., "DAG" | "dag"] => { - checker.report_diagnostics(diagnostic_for_argument( - arguments, - "sla_miss_callback", - None, - )); + diagnostic_for_argument(checker, arguments, "sla_miss_callback", None); } segments => { if is_airflow_builtin_or_provider(segments, "operators", "Operator") { - checker.report_diagnostics(diagnostic_for_argument(arguments, "sla", None)); + diagnostic_for_argument(checker, arguments, "sla", None); } } } @@ -194,9 +188,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { let replacement = match qualified_name.segments() { // airflow.datasets.metadata - ["airflow", "datasets", "metadata", "Metadata"] => { - Replacement::Name("airflow.sdk.Metadata") - } + ["airflow", "datasets", "metadata", "Metadata"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "Metadata", + }, // airflow.datasets ["airflow", "Dataset"] | ["airflow", "datasets", "Dataset"] => Replacement::AutoImport { module: "airflow.sdk", @@ -204,20 +199,34 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { }, ["airflow", "datasets", rest] => match *rest { "DatasetAliasEvent" => Replacement::None, - "DatasetAlias" => Replacement::Name("airflow.sdk.AssetAlias"), - "DatasetAll" => Replacement::Name("airflow.sdk.AssetAll"), - "DatasetAny" => Replacement::Name("airflow.sdk.AssetAny"), - "expand_alias_to_datasets" => Replacement::Name("airflow.sdk.expand_alias_to_assets"), + "DatasetAlias" => Replacement::AutoImport { + module: "airflow.sdk", + name: "AssetAlias", + }, + "DatasetAll" => Replacement::AutoImport { + module: "airflow.sdk", + name: "AssetAll", + }, + "DatasetAny" => Replacement::AutoImport { + module: "airflow.sdk", + name: "AssetAny", + }, + "expand_alias_to_datasets" => Replacement::AutoImport { + module: "airflow.models.asset", + name: "expand_alias_to_assets", + }, _ => return, }, // airflow.decorators - ["airflow", "decorators", rest @ ("dag" | "task" | "task_group" | "setup" | "teardown")] => { - Replacement::SourceModuleMoved { - module: "airflow.sdk", - name: (*rest).to_string(), - } - } + [ + "airflow", + "decorators", + rest @ ("dag" | "task" | "task_group" | "setup" | "teardown"), + ] => Replacement::SourceModuleMoved { + module: "airflow.sdk", + name: (*rest).to_string(), + }, // airflow.io ["airflow", "io", "path", "ObjectStoragePath"] => Replacement::SourceModuleMoved { @@ -229,16 +238,27 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { name: "attach".to_string(), }, - // airflow.models.baseoperator - ["airflow", "models", "baseoperator", rest] => match *rest { - "chain" | "chain_linear" | "cross_downstream" => Replacement::SourceModuleMoved { + // airflow.models + ["airflow", "models", rest @ ("Connection" | "Variable")] => { + Replacement::SourceModuleMoved { module: "airflow.sdk", name: (*rest).to_string(), - }, - "BaseOperatorLink" => { - Replacement::Name("airflow.sdk.definitions.baseoperatorlink.BaseOperatorLink") } - _ => return, + } + + // airflow.models.baseoperator + [ + "airflow", + "models", + "baseoperator", + rest @ ("chain" | "chain_linear" | "cross_downstream"), + ] => Replacement::SourceModuleMoved { + module: "airflow.sdk", + name: (*rest).to_string(), + }, + ["airflow", "models", "baseoperatorlink", "BaseOperatorLink"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "BaseOperatorLink", }, // airflow.model..DAG ["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved { @@ -246,40 +266,52 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { name: "DAG".to_string(), }, // airflow.timetables - ["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => { - Replacement::Name("airflow.timetables.assets.AssetOrTimeSchedule") - } + ["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => Replacement::AutoImport { + module: "airflow.timetables.assets", + name: "AssetOrTimeSchedule", + }, // airflow.utils - ["airflow", "utils", "dag_parsing_context", "get_parsing_context"] => { - Replacement::Name("airflow.sdk.get_parsing_context") - } + [ + "airflow", + "utils", + "dag_parsing_context", + "get_parsing_context", + ] => Replacement::AutoImport { + module: "airflow.sdk", + name: "get_parsing_context", + }, _ => return, }; - if is_guarded_by_try_except(expr, &replacement, semantic) { + let (module, name) = match &replacement { + Replacement::AutoImport { module, name } => (module, *name), + Replacement::SourceModuleMoved { module, name } => (module, name.as_str()), + _ => { + checker.report_diagnostic( + Airflow3SuggestedUpdate { + deprecated: qualified_name.to_string(), + replacement: replacement.clone(), + }, + range, + ); + return; + } + }; + + if is_guarded_by_try_except(expr, module, name, checker.semantic()) { return; } - - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( Airflow3SuggestedUpdate { deprecated: qualified_name.to_string(), replacement: replacement.clone(), }, range, ); - - if let Replacement::AutoImport { module, name } = replacement { - diagnostic.try_set_fix(|| { - let (import_edit, binding) = checker.importer().get_or_import_symbol( - &ImportRequest::import_from(module, name), - expr.start(), - checker.semantic(), - )?; - let replacement_edit = Edit::range_replacement(binding, range); - Ok(Fix::safe_edits(import_edit, [replacement_edit])) - }); + if let Some(fix) = generate_import_edit(expr, checker, module, name, range) + .or_else(|| generate_remove_and_runtime_import_edit(expr, checker, module, name)) + { + diagnostic.set_fix(fix); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs b/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs index f75418818fa8c..8fcd6da8c167f 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs @@ -1,5 +1,5 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::Violation; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::Expr; use ruff_python_semantic::Modules; @@ -110,11 +110,10 @@ pub(crate) fn variable_name_task_id(checker: &Checker, targets: &[Expr], value: return; } - let diagnostic = Diagnostic::new( + checker.report_diagnostic( AirflowVariableNameTaskIdMismatch { task_id: task_id.to_string(), }, target.range(), ); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_airflow_plugin.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_airflow_plugin.py.snap index dc16e1812bb4e..173877e2d8aa2 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_airflow_plugin.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_airflow_plugin.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- -AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0; This extension should just be imported as a regular python module. +AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0 | 5 | name = "test_plugin" 6 | # --- Invalid extensions start @@ -10,8 +10,9 @@ AIR301_airflow_plugin.py:7:5: AIR301 `operators` is removed in Airflow 3.0; This 8 | sensors = [PluginSensorOperator] 9 | hooks = [PluginHook] | + = help: This extension should just be imported as a regular python module. -AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0; This extension should just be imported as a regular python module. +AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0 | 6 | # --- Invalid extensions start 7 | operators = [PluginOperator] @@ -20,8 +21,9 @@ AIR301_airflow_plugin.py:8:5: AIR301 `sensors` is removed in Airflow 3.0; This e 9 | hooks = [PluginHook] 10 | executors = [PluginExecutor] | + = help: This extension should just be imported as a regular python module. -AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0; This extension should just be imported as a regular python module. +AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0 | 7 | operators = [PluginOperator] 8 | sensors = [PluginSensorOperator] @@ -30,8 +32,9 @@ AIR301_airflow_plugin.py:9:5: AIR301 `hooks` is removed in Airflow 3.0; This ext 10 | executors = [PluginExecutor] 11 | # --- Invalid extensions end | + = help: This extension should just be imported as a regular python module. -AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0; This extension should just be imported as a regular python module. +AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0 | 8 | sensors = [PluginSensorOperator] 9 | hooks = [PluginHook] @@ -40,3 +43,4 @@ AIR301_airflow_plugin.py:10:5: AIR301 `executors` is removed in Airflow 3.0; Thi 11 | # --- Invalid extensions end 12 | macros = [plugin_macro] | + = help: This extension should just be imported as a regular python module. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap index 4731893731237..af858937ab620 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_args.py.snap @@ -1,267 +1,288 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- -AIR301_args.py:20:39: AIR301 [*] `schedule_interval` is removed in Airflow 3.0 +AIR301_args.py:21:39: AIR301 [*] `schedule_interval` is removed in Airflow 3.0 | -18 | DAG(dag_id="class_schedule", schedule="@hourly") -19 | -20 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") +19 | DAG(dag_id="class_schedule", schedule="@hourly") +20 | +21 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") | ^^^^^^^^^^^^^^^^^ AIR301 -21 | -22 | DAG(dag_id="class_timetable", timetable=NullTimetable()) +22 | +23 | DAG(dag_id="class_timetable", timetable=NullTimetable()) | = help: Use `schedule` instead ℹ Safe fix -17 17 | -18 18 | DAG(dag_id="class_schedule", schedule="@hourly") -19 19 | -20 |-DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") - 20 |+DAG(dag_id="class_schedule_interval", schedule="@hourly") -21 21 | -22 22 | DAG(dag_id="class_timetable", timetable=NullTimetable()) -23 23 | +18 18 | +19 19 | DAG(dag_id="class_schedule", schedule="@hourly") +20 20 | +21 |-DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") + 21 |+DAG(dag_id="class_schedule_interval", schedule="@hourly") +22 22 | +23 23 | DAG(dag_id="class_timetable", timetable=NullTimetable()) +24 24 | -AIR301_args.py:22:31: AIR301 [*] `timetable` is removed in Airflow 3.0 +AIR301_args.py:23:31: AIR301 [*] `timetable` is removed in Airflow 3.0 | -20 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") -21 | -22 | DAG(dag_id="class_timetable", timetable=NullTimetable()) +21 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") +22 | +23 | DAG(dag_id="class_timetable", timetable=NullTimetable()) | ^^^^^^^^^ AIR301 | = help: Use `schedule` instead ℹ Safe fix -19 19 | -20 20 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") -21 21 | -22 |-DAG(dag_id="class_timetable", timetable=NullTimetable()) - 22 |+DAG(dag_id="class_timetable", schedule=NullTimetable()) -23 23 | +20 20 | +21 21 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") +22 22 | +23 |-DAG(dag_id="class_timetable", timetable=NullTimetable()) + 23 |+DAG(dag_id="class_timetable", schedule=NullTimetable()) 24 24 | -25 25 | DAG(dag_id="class_fail_stop", fail_stop=True) +25 25 | +26 26 | DAG(dag_id="class_fail_stop", fail_stop=True) -AIR301_args.py:25:31: AIR301 [*] `fail_stop` is removed in Airflow 3.0 +AIR301_args.py:26:31: AIR301 [*] `fail_stop` is removed in Airflow 3.0 | -25 | DAG(dag_id="class_fail_stop", fail_stop=True) +26 | DAG(dag_id="class_fail_stop", fail_stop=True) | ^^^^^^^^^ AIR301 -26 | -27 | DAG(dag_id="class_default_view", default_view="dag_default_view") +27 | +28 | DAG(dag_id="class_default_view", default_view="dag_default_view") | = help: Use `fail_fast` instead ℹ Safe fix -22 22 | DAG(dag_id="class_timetable", timetable=NullTimetable()) -23 23 | +23 23 | DAG(dag_id="class_timetable", timetable=NullTimetable()) 24 24 | -25 |-DAG(dag_id="class_fail_stop", fail_stop=True) - 25 |+DAG(dag_id="class_fail_stop", fail_fast=True) -26 26 | -27 27 | DAG(dag_id="class_default_view", default_view="dag_default_view") -28 28 | +25 25 | +26 |-DAG(dag_id="class_fail_stop", fail_stop=True) + 26 |+DAG(dag_id="class_fail_stop", fail_fast=True) +27 27 | +28 28 | DAG(dag_id="class_default_view", default_view="dag_default_view") +29 29 | -AIR301_args.py:27:34: AIR301 `default_view` is removed in Airflow 3.0 +AIR301_args.py:28:34: AIR301 `default_view` is removed in Airflow 3.0 | -25 | DAG(dag_id="class_fail_stop", fail_stop=True) -26 | -27 | DAG(dag_id="class_default_view", default_view="dag_default_view") +26 | DAG(dag_id="class_fail_stop", fail_stop=True) +27 | +28 | DAG(dag_id="class_default_view", default_view="dag_default_view") | ^^^^^^^^^^^^ AIR301 -28 | -29 | DAG(dag_id="class_orientation", orientation="BT") +29 | +30 | DAG(dag_id="class_orientation", orientation="BT") | -AIR301_args.py:29:33: AIR301 `orientation` is removed in Airflow 3.0 +AIR301_args.py:30:33: AIR301 `orientation` is removed in Airflow 3.0 | -27 | DAG(dag_id="class_default_view", default_view="dag_default_view") -28 | -29 | DAG(dag_id="class_orientation", orientation="BT") +28 | DAG(dag_id="class_default_view", default_view="dag_default_view") +29 | +30 | DAG(dag_id="class_orientation", orientation="BT") | ^^^^^^^^^^^ AIR301 -30 | -31 | allow_future_exec_dates_dag = DAG(dag_id="class_allow_future_exec_dates") +31 | +32 | allow_future_exec_dates_dag = DAG(dag_id="class_allow_future_exec_dates") | -AIR301_args.py:40:6: AIR301 [*] `schedule_interval` is removed in Airflow 3.0 +AIR301_args.py:41:6: AIR301 [*] `schedule_interval` is removed in Airflow 3.0 | -40 | @dag(schedule_interval="0 * * * *") +41 | @dag(schedule_interval="0 * * * *") | ^^^^^^^^^^^^^^^^^ AIR301 -41 | def decorator_schedule_interval(): -42 | pass +42 | def decorator_schedule_interval(): +43 | pass | = help: Use `schedule` instead ℹ Safe fix -37 37 | pass -38 38 | +38 38 | pass 39 39 | -40 |-@dag(schedule_interval="0 * * * *") - 40 |+@dag(schedule="0 * * * *") -41 41 | def decorator_schedule_interval(): -42 42 | pass -43 43 | +40 40 | +41 |-@dag(schedule_interval="0 * * * *") + 41 |+@dag(schedule="0 * * * *") +42 42 | def decorator_schedule_interval(): +43 43 | pass +44 44 | -AIR301_args.py:45:6: AIR301 [*] `timetable` is removed in Airflow 3.0 +AIR301_args.py:46:6: AIR301 [*] `timetable` is removed in Airflow 3.0 | -45 | @dag(timetable=NullTimetable()) +46 | @dag(timetable=NullTimetable()) | ^^^^^^^^^ AIR301 -46 | def decorator_timetable(): -47 | pass +47 | def decorator_timetable(): +48 | pass | = help: Use `schedule` instead ℹ Safe fix -42 42 | pass -43 43 | +43 43 | pass 44 44 | -45 |-@dag(timetable=NullTimetable()) - 45 |+@dag(schedule=NullTimetable()) -46 46 | def decorator_timetable(): -47 47 | pass -48 48 | +45 45 | +46 |-@dag(timetable=NullTimetable()) + 46 |+@dag(schedule=NullTimetable()) +47 47 | def decorator_timetable(): +48 48 | pass +49 49 | -AIR301_args.py:53:39: AIR301 [*] `execution_date` is removed in Airflow 3.0 +AIR301_args.py:54:62: AIR301 [*] `execution_date` is removed in Airflow 3.0 | -51 | def decorator_deprecated_operator_args(): -52 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( -53 | task_id="trigger_dagrun_op1", execution_date="2024-12-04" - | ^^^^^^^^^^^^^^ AIR301 -54 | ) -55 | trigger_dagrun_op2 = TriggerDagRunOperator( +52 | def decorator_deprecated_operator_args(): +53 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( +54 | task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04" + | ^^^^^^^^^^^^^^ AIR301 +55 | ) +56 | trigger_dagrun_op2 = TriggerDagRunOperator( | = help: Use `logical_date` instead ℹ Safe fix -50 50 | @dag() -51 51 | def decorator_deprecated_operator_args(): -52 52 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( -53 |- task_id="trigger_dagrun_op1", execution_date="2024-12-04" - 53 |+ task_id="trigger_dagrun_op1", logical_date="2024-12-04" -54 54 | ) -55 55 | trigger_dagrun_op2 = TriggerDagRunOperator( -56 56 | task_id="trigger_dagrun_op2", execution_date="2024-12-04" +51 51 | @dag() +52 52 | def decorator_deprecated_operator_args(): +53 53 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( +54 |- task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04" + 54 |+ task_id="trigger_dagrun_op1", trigger_dag_id="test", logical_date="2024-12-04" +55 55 | ) +56 56 | trigger_dagrun_op2 = TriggerDagRunOperator( +57 57 | task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04" -AIR301_args.py:56:39: AIR301 [*] `execution_date` is removed in Airflow 3.0 +AIR301_args.py:57:62: AIR301 [*] `execution_date` is removed in Airflow 3.0 | -54 | ) -55 | trigger_dagrun_op2 = TriggerDagRunOperator( -56 | task_id="trigger_dagrun_op2", execution_date="2024-12-04" - | ^^^^^^^^^^^^^^ AIR301 -57 | ) +55 | ) +56 | trigger_dagrun_op2 = TriggerDagRunOperator( +57 | task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04" + | ^^^^^^^^^^^^^^ AIR301 +58 | ) | = help: Use `logical_date` instead ℹ Safe fix -53 53 | task_id="trigger_dagrun_op1", execution_date="2024-12-04" -54 54 | ) -55 55 | trigger_dagrun_op2 = TriggerDagRunOperator( -56 |- task_id="trigger_dagrun_op2", execution_date="2024-12-04" - 56 |+ task_id="trigger_dagrun_op2", logical_date="2024-12-04" -57 57 | ) -58 58 | -59 59 | branch_dt_op = datetime.BranchDateTimeOperator( +54 54 | task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04" +55 55 | ) +56 56 | trigger_dagrun_op2 = TriggerDagRunOperator( +57 |- task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04" + 57 |+ task_id="trigger_dagrun_op2", trigger_dag_id="test", logical_date="2024-12-04" +58 58 | ) +59 59 | +60 60 | branch_dt_op = datetime.BranchDateTimeOperator( -AIR301_args.py:60:33: AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0 +AIR301_args.py:61:33: AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0 | -59 | branch_dt_op = datetime.BranchDateTimeOperator( -60 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 +60 | branch_dt_op = datetime.BranchDateTimeOperator( +61 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 | ^^^^^^^^^^^^^^^^^^^^^^ AIR301 -61 | ) -62 | branch_dt_op2 = BranchDateTimeOperator( +62 | ) +63 | branch_dt_op2 = BranchDateTimeOperator( | = help: Use `use_task_logical_date` instead ℹ Safe fix -57 57 | ) -58 58 | -59 59 | branch_dt_op = datetime.BranchDateTimeOperator( -60 |- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 - 60 |+ task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5 -61 61 | ) -62 62 | branch_dt_op2 = BranchDateTimeOperator( -63 63 | task_id="branch_dt_op2", +58 58 | ) +59 59 | +60 60 | branch_dt_op = datetime.BranchDateTimeOperator( +61 |- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 + 61 |+ task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5 +62 62 | ) +63 63 | branch_dt_op2 = BranchDateTimeOperator( +64 64 | task_id="branch_dt_op2", -AIR301_args.py:60:62: AIR301 [*] `task_concurrency` is removed in Airflow 3.0 +AIR301_args.py:61:62: AIR301 [*] `task_concurrency` is removed in Airflow 3.0 | -59 | branch_dt_op = datetime.BranchDateTimeOperator( -60 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 +60 | branch_dt_op = datetime.BranchDateTimeOperator( +61 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 | ^^^^^^^^^^^^^^^^ AIR301 -61 | ) -62 | branch_dt_op2 = BranchDateTimeOperator( +62 | ) +63 | branch_dt_op2 = BranchDateTimeOperator( | = help: Use `max_active_tis_per_dag` instead ℹ Safe fix -57 57 | ) -58 58 | -59 59 | branch_dt_op = datetime.BranchDateTimeOperator( -60 |- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 - 60 |+ task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5 -61 61 | ) -62 62 | branch_dt_op2 = BranchDateTimeOperator( -63 63 | task_id="branch_dt_op2", +58 58 | ) +59 59 | +60 60 | branch_dt_op = datetime.BranchDateTimeOperator( +61 |- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5 + 61 |+ task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5 +62 62 | ) +63 63 | branch_dt_op2 = BranchDateTimeOperator( +64 64 | task_id="branch_dt_op2", -AIR301_args.py:64:9: AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0 +AIR301_args.py:65:9: AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0 | -62 | branch_dt_op2 = BranchDateTimeOperator( -63 | task_id="branch_dt_op2", -64 | use_task_execution_day=True, +63 | branch_dt_op2 = BranchDateTimeOperator( +64 | task_id="branch_dt_op2", +65 | use_task_execution_day=True, | ^^^^^^^^^^^^^^^^^^^^^^ AIR301 -65 | sla=timedelta(seconds=10), -66 | ) +66 | sla=timedelta(seconds=10), +67 | ) | = help: Use `use_task_logical_date` instead ℹ Safe fix -61 61 | ) -62 62 | branch_dt_op2 = BranchDateTimeOperator( -63 63 | task_id="branch_dt_op2", -64 |- use_task_execution_day=True, - 64 |+ use_task_logical_date=True, -65 65 | sla=timedelta(seconds=10), -66 66 | ) -67 67 | +62 62 | ) +63 63 | branch_dt_op2 = BranchDateTimeOperator( +64 64 | task_id="branch_dt_op2", +65 |- use_task_execution_day=True, + 65 |+ use_task_logical_date=True, +66 66 | sla=timedelta(seconds=10), +67 67 | ) +68 68 | -AIR301_args.py:87:15: AIR301 `filename_template` is removed in Airflow 3.0 +AIR301_args.py:92:9: AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0 | -86 | # deprecated filename_template argument in FileTaskHandler -87 | S3TaskHandler(filename_template="/tmp/test") - | ^^^^^^^^^^^^^^^^^ AIR301 -88 | HdfsTaskHandler(filename_template="/tmp/test") -89 | ElasticsearchTaskHandler(filename_template="/tmp/test") +90 | follow_task_ids_if_true=None, +91 | week_day=1, +92 | use_task_execution_day=True, + | ^^^^^^^^^^^^^^^^^^^^^^ AIR301 +93 | ) | + = help: Use `use_task_logical_date` instead -AIR301_args.py:88:17: AIR301 `filename_template` is removed in Airflow 3.0 - | -86 | # deprecated filename_template argument in FileTaskHandler -87 | S3TaskHandler(filename_template="/tmp/test") -88 | HdfsTaskHandler(filename_template="/tmp/test") - | ^^^^^^^^^^^^^^^^^ AIR301 -89 | ElasticsearchTaskHandler(filename_template="/tmp/test") -90 | GCSTaskHandler(filename_template="/tmp/test") - | +ℹ Safe fix +89 89 | follow_task_ids_if_false=None, +90 90 | follow_task_ids_if_true=None, +91 91 | week_day=1, +92 |- use_task_execution_day=True, + 92 |+ use_task_logical_date=True, +93 93 | ) +94 94 | +95 95 | trigger_dagrun_op >> trigger_dagrun_op2 -AIR301_args.py:89:26: AIR301 `filename_template` is removed in Airflow 3.0 - | -87 | S3TaskHandler(filename_template="/tmp/test") -88 | HdfsTaskHandler(filename_template="/tmp/test") -89 | ElasticsearchTaskHandler(filename_template="/tmp/test") - | ^^^^^^^^^^^^^^^^^ AIR301 -90 | GCSTaskHandler(filename_template="/tmp/test") - | +AIR301_args.py:102:15: AIR301 `filename_template` is removed in Airflow 3.0 + | +101 | # deprecated filename_template argument in FileTaskHandler +102 | S3TaskHandler(filename_template="/tmp/test") + | ^^^^^^^^^^^^^^^^^ AIR301 +103 | HdfsTaskHandler(filename_template="/tmp/test") +104 | ElasticsearchTaskHandler(filename_template="/tmp/test") + | -AIR301_args.py:90:16: AIR301 `filename_template` is removed in Airflow 3.0 - | -88 | HdfsTaskHandler(filename_template="/tmp/test") -89 | ElasticsearchTaskHandler(filename_template="/tmp/test") -90 | GCSTaskHandler(filename_template="/tmp/test") - | ^^^^^^^^^^^^^^^^^ AIR301 -91 | -92 | FabAuthManager(None) - | +AIR301_args.py:103:17: AIR301 `filename_template` is removed in Airflow 3.0 + | +101 | # deprecated filename_template argument in FileTaskHandler +102 | S3TaskHandler(filename_template="/tmp/test") +103 | HdfsTaskHandler(filename_template="/tmp/test") + | ^^^^^^^^^^^^^^^^^ AIR301 +104 | ElasticsearchTaskHandler(filename_template="/tmp/test") +105 | GCSTaskHandler(filename_template="/tmp/test") + | -AIR301_args.py:92:15: AIR301 `appbuilder` is removed in Airflow 3.0; The constructor takes no parameter now - | -90 | GCSTaskHandler(filename_template="/tmp/test") -91 | -92 | FabAuthManager(None) - | ^^^^^^ AIR301 - | +AIR301_args.py:104:26: AIR301 `filename_template` is removed in Airflow 3.0 + | +102 | S3TaskHandler(filename_template="/tmp/test") +103 | HdfsTaskHandler(filename_template="/tmp/test") +104 | ElasticsearchTaskHandler(filename_template="/tmp/test") + | ^^^^^^^^^^^^^^^^^ AIR301 +105 | GCSTaskHandler(filename_template="/tmp/test") + | + +AIR301_args.py:105:16: AIR301 `filename_template` is removed in Airflow 3.0 + | +103 | HdfsTaskHandler(filename_template="/tmp/test") +104 | ElasticsearchTaskHandler(filename_template="/tmp/test") +105 | GCSTaskHandler(filename_template="/tmp/test") + | ^^^^^^^^^^^^^^^^^ AIR301 +106 | +107 | FabAuthManager(None) + | + +AIR301_args.py:107:15: AIR301 `appbuilder` is removed in Airflow 3.0 + | +105 | GCSTaskHandler(filename_template="/tmp/test") +106 | +107 | FabAuthManager(None) + | ^^^^^^ AIR301 + | + = help: The constructor takes no parameter now diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap index 7d69175385d0b..4fdecfe8a15c1 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap @@ -163,7 +163,7 @@ AIR301_class_attribute.py:39:25: AIR301 [*] `iter_dataset_aliases` is removed in 41 41 | # airflow.datasets.manager 42 42 | dm = DatasetManager() -AIR301_class_attribute.py:42:6: AIR301 `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 +AIR301_class_attribute.py:42:6: AIR301 [*] `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 | 41 | # airflow.datasets.manager 42 | dm = DatasetManager() @@ -171,7 +171,25 @@ AIR301_class_attribute.py:42:6: AIR301 `airflow.datasets.manager.DatasetManager` 43 | dm.register_dataset_change() 44 | dm.create_datasets() | - = help: Use `airflow.assets.manager.AssetManager` instead + = help: Use `AssetManager` from `airflow.assets.manager` instead. + +ℹ Safe fix +19 19 | from airflow.providers_manager import ProvidersManager +20 20 | from airflow.secrets.base_secrets import BaseSecretsBackend +21 21 | from airflow.secrets.local_filesystem import LocalFilesystemBackend + 22 |+from airflow.assets.manager import AssetManager +22 23 | +23 24 | # airflow.Dataset +24 25 | dataset_from_root = DatasetFromRoot() +-------------------------------------------------------------------------------- +39 40 | any_to_test_method_call.iter_dataset_aliases() +40 41 | +41 42 | # airflow.datasets.manager +42 |-dm = DatasetManager() + 43 |+dm = AssetManager() +43 44 | dm.register_dataset_change() +44 45 | dm.create_datasets() +45 46 | dm.notify_dataset_created() AIR301_class_attribute.py:43:4: AIR301 [*] `register_dataset_change` is removed in Airflow 3.0 | @@ -277,14 +295,33 @@ AIR301_class_attribute.py:47:4: AIR301 [*] `notify_dataset_alias_created` is rem 49 49 | # airflow.lineage.hook 50 50 | dl_info = DatasetLineageInfo() -AIR301_class_attribute.py:50:11: AIR301 `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 +AIR301_class_attribute.py:50:11: AIR301 [*] `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 | 49 | # airflow.lineage.hook 50 | dl_info = DatasetLineageInfo() | ^^^^^^^^^^^^^^^^^^ AIR301 51 | dl_info.dataset | - = help: Use `airflow.lineage.hook.AssetLineageInfo` instead + = help: Use `AssetLineageInfo` from `airflow.lineage.hook` instead. + +ℹ Safe fix +9 9 | DatasetAny, +10 10 | ) +11 11 | from airflow.datasets.manager import DatasetManager +12 |-from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector + 12 |+from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector, AssetLineageInfo +13 13 | from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager +14 14 | from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError +15 15 | from airflow.providers.google.cloud.secrets.secret_manager import ( +-------------------------------------------------------------------------------- +47 47 | dm.notify_dataset_alias_created() +48 48 | +49 49 | # airflow.lineage.hook +50 |-dl_info = DatasetLineageInfo() + 50 |+dl_info = AssetLineageInfo() +51 51 | dl_info.dataset +52 52 | +53 53 | hlc = HookLineageCollector() AIR301_class_attribute.py:51:9: AIR301 [*] `dataset` is removed in Airflow 3.0 | @@ -492,142 +529,142 @@ AIR301_class_attribute.py:79:15: AIR301 [*] `get_connections` is removed in Airf 81 81 | not_an_error = NotAir302SecretError() 82 82 | not_an_error.get_conn_uri() -AIR301_class_attribute.py:87:4: AIR301 [*] `dataset_factories` is removed in Airflow 3.0 +AIR301_class_attribute.py:86:4: AIR301 [*] `initialize_providers_dataset_uri_resources` is removed in Airflow 3.0 | +84 | # airflow.providers_manager 85 | pm = ProvidersManager() -86 | pm.initialize_providers_asset_uri_resources() +86 | pm.initialize_providers_dataset_uri_resources() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 87 | pm.dataset_factories - | ^^^^^^^^^^^^^^^^^ AIR301 -88 | pm.dataset_factories -89 | pm.dataset_uri_handlers +88 | pm.dataset_uri_handlers | - = help: Use `asset_factories` instead + = help: Use `initialize_providers_asset_uri_resources` instead ℹ Safe fix +83 83 | 84 84 | # airflow.providers_manager 85 85 | pm = ProvidersManager() -86 86 | pm.initialize_providers_asset_uri_resources() -87 |-pm.dataset_factories - 87 |+pm.asset_factories -88 88 | pm.dataset_factories -89 89 | pm.dataset_uri_handlers -90 90 | pm.dataset_to_openlineage_converters +86 |-pm.initialize_providers_dataset_uri_resources() + 86 |+pm.initialize_providers_asset_uri_resources() +87 87 | pm.dataset_factories +88 88 | pm.dataset_uri_handlers +89 89 | pm.dataset_to_openlineage_converters -AIR301_class_attribute.py:88:4: AIR301 [*] `dataset_factories` is removed in Airflow 3.0 +AIR301_class_attribute.py:87:4: AIR301 [*] `dataset_factories` is removed in Airflow 3.0 | -86 | pm.initialize_providers_asset_uri_resources() +85 | pm = ProvidersManager() +86 | pm.initialize_providers_dataset_uri_resources() 87 | pm.dataset_factories -88 | pm.dataset_factories | ^^^^^^^^^^^^^^^^^ AIR301 -89 | pm.dataset_uri_handlers -90 | pm.dataset_to_openlineage_converters +88 | pm.dataset_uri_handlers +89 | pm.dataset_to_openlineage_converters | = help: Use `asset_factories` instead ℹ Safe fix +84 84 | # airflow.providers_manager 85 85 | pm = ProvidersManager() -86 86 | pm.initialize_providers_asset_uri_resources() -87 87 | pm.dataset_factories -88 |-pm.dataset_factories - 88 |+pm.asset_factories -89 89 | pm.dataset_uri_handlers -90 90 | pm.dataset_to_openlineage_converters -91 91 | +86 86 | pm.initialize_providers_dataset_uri_resources() +87 |-pm.dataset_factories + 87 |+pm.asset_factories +88 88 | pm.dataset_uri_handlers +89 89 | pm.dataset_to_openlineage_converters +90 90 | -AIR301_class_attribute.py:89:4: AIR301 [*] `dataset_uri_handlers` is removed in Airflow 3.0 +AIR301_class_attribute.py:88:4: AIR301 [*] `dataset_uri_handlers` is removed in Airflow 3.0 | +86 | pm.initialize_providers_dataset_uri_resources() 87 | pm.dataset_factories -88 | pm.dataset_factories -89 | pm.dataset_uri_handlers +88 | pm.dataset_uri_handlers | ^^^^^^^^^^^^^^^^^^^^ AIR301 -90 | pm.dataset_to_openlineage_converters +89 | pm.dataset_to_openlineage_converters | = help: Use `asset_uri_handlers` instead ℹ Safe fix -86 86 | pm.initialize_providers_asset_uri_resources() +85 85 | pm = ProvidersManager() +86 86 | pm.initialize_providers_dataset_uri_resources() 87 87 | pm.dataset_factories -88 88 | pm.dataset_factories -89 |-pm.dataset_uri_handlers - 89 |+pm.asset_uri_handlers -90 90 | pm.dataset_to_openlineage_converters -91 91 | -92 92 | # airflow.secrets.base_secrets - -AIR301_class_attribute.py:90:4: AIR301 [*] `dataset_to_openlineage_converters` is removed in Airflow 3.0 - | -88 | pm.dataset_factories -89 | pm.dataset_uri_handlers -90 | pm.dataset_to_openlineage_converters +88 |-pm.dataset_uri_handlers + 88 |+pm.asset_uri_handlers +89 89 | pm.dataset_to_openlineage_converters +90 90 | +91 91 | # airflow.secrets.base_secrets + +AIR301_class_attribute.py:89:4: AIR301 [*] `dataset_to_openlineage_converters` is removed in Airflow 3.0 + | +87 | pm.dataset_factories +88 | pm.dataset_uri_handlers +89 | pm.dataset_to_openlineage_converters | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -91 | -92 | # airflow.secrets.base_secrets +90 | +91 | # airflow.secrets.base_secrets | = help: Use `asset_to_openlineage_converters` instead ℹ Safe fix +86 86 | pm.initialize_providers_dataset_uri_resources() 87 87 | pm.dataset_factories -88 88 | pm.dataset_factories -89 89 | pm.dataset_uri_handlers -90 |-pm.dataset_to_openlineage_converters - 90 |+pm.asset_to_openlineage_converters -91 91 | -92 92 | # airflow.secrets.base_secrets -93 93 | base_secret_backend = BaseSecretsBackend() - -AIR301_class_attribute.py:94:21: AIR301 [*] `get_conn_uri` is removed in Airflow 3.0 - | -92 | # airflow.secrets.base_secrets -93 | base_secret_backend = BaseSecretsBackend() -94 | base_secret_backend.get_conn_uri() +88 88 | pm.dataset_uri_handlers +89 |-pm.dataset_to_openlineage_converters + 89 |+pm.asset_to_openlineage_converters +90 90 | +91 91 | # airflow.secrets.base_secrets +92 92 | base_secret_backend = BaseSecretsBackend() + +AIR301_class_attribute.py:93:21: AIR301 [*] `get_conn_uri` is removed in Airflow 3.0 + | +91 | # airflow.secrets.base_secrets +92 | base_secret_backend = BaseSecretsBackend() +93 | base_secret_backend.get_conn_uri() | ^^^^^^^^^^^^ AIR301 -95 | base_secret_backend.get_connections() +94 | base_secret_backend.get_connections() | = help: Use `get_conn_value` instead ℹ Safe fix -91 91 | -92 92 | # airflow.secrets.base_secrets -93 93 | base_secret_backend = BaseSecretsBackend() -94 |-base_secret_backend.get_conn_uri() - 94 |+base_secret_backend.get_conn_value() -95 95 | base_secret_backend.get_connections() -96 96 | -97 97 | # airflow.secrets.local_filesystem - -AIR301_class_attribute.py:95:21: AIR301 [*] `get_connections` is removed in Airflow 3.0 - | -93 | base_secret_backend = BaseSecretsBackend() -94 | base_secret_backend.get_conn_uri() -95 | base_secret_backend.get_connections() +90 90 | +91 91 | # airflow.secrets.base_secrets +92 92 | base_secret_backend = BaseSecretsBackend() +93 |-base_secret_backend.get_conn_uri() + 93 |+base_secret_backend.get_conn_value() +94 94 | base_secret_backend.get_connections() +95 95 | +96 96 | # airflow.secrets.local_filesystem + +AIR301_class_attribute.py:94:21: AIR301 [*] `get_connections` is removed in Airflow 3.0 + | +92 | base_secret_backend = BaseSecretsBackend() +93 | base_secret_backend.get_conn_uri() +94 | base_secret_backend.get_connections() | ^^^^^^^^^^^^^^^ AIR301 -96 | -97 | # airflow.secrets.local_filesystem +95 | +96 | # airflow.secrets.local_filesystem | = help: Use `get_connection` instead ℹ Safe fix -92 92 | # airflow.secrets.base_secrets -93 93 | base_secret_backend = BaseSecretsBackend() -94 94 | base_secret_backend.get_conn_uri() -95 |-base_secret_backend.get_connections() - 95 |+base_secret_backend.get_connection() -96 96 | -97 97 | # airflow.secrets.local_filesystem -98 98 | lfb = LocalFilesystemBackend() - -AIR301_class_attribute.py:99:5: AIR301 [*] `get_connections` is removed in Airflow 3.0 - | -97 | # airflow.secrets.local_filesystem -98 | lfb = LocalFilesystemBackend() -99 | lfb.get_connections() +91 91 | # airflow.secrets.base_secrets +92 92 | base_secret_backend = BaseSecretsBackend() +93 93 | base_secret_backend.get_conn_uri() +94 |-base_secret_backend.get_connections() + 94 |+base_secret_backend.get_connection() +95 95 | +96 96 | # airflow.secrets.local_filesystem +97 97 | lfb = LocalFilesystemBackend() + +AIR301_class_attribute.py:98:5: AIR301 [*] `get_connections` is removed in Airflow 3.0 + | +96 | # airflow.secrets.local_filesystem +97 | lfb = LocalFilesystemBackend() +98 | lfb.get_connections() | ^^^^^^^^^^^^^^^ AIR301 | = help: Use `get_connection` instead ℹ Safe fix -96 96 | -97 97 | # airflow.secrets.local_filesystem -98 98 | lfb = LocalFilesystemBackend() -99 |-lfb.get_connections() - 99 |+lfb.get_connection() +95 95 | +96 96 | # airflow.secrets.local_filesystem +97 97 | lfb = LocalFilesystemBackend() +98 |-lfb.get_connections() + 98 |+lfb.get_connection() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap index 0943b18e5980d..7a55d729e6b7a 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap @@ -1,741 +1,294 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- -AIR301_names.py:77:1: AIR301 `airflow.PY36` is removed in Airflow 3.0 +AIR301_names.py:38:1: AIR301 `airflow.PY36` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:7: AIR301 `airflow.PY37` is removed in Airflow 3.0 +AIR301_names.py:38:7: AIR301 `airflow.PY37` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:13: AIR301 `airflow.PY38` is removed in Airflow 3.0 +AIR301_names.py:38:13: AIR301 `airflow.PY38` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:19: AIR301 `airflow.PY39` is removed in Airflow 3.0 +AIR301_names.py:38:19: AIR301 `airflow.PY39` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:25: AIR301 `airflow.PY310` is removed in Airflow 3.0 +AIR301_names.py:38:25: AIR301 `airflow.PY310` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:32: AIR301 `airflow.PY311` is removed in Airflow 3.0 +AIR301_names.py:38:32: AIR301 `airflow.PY311` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:39: AIR301 `airflow.PY312` is removed in Airflow 3.0 +AIR301_names.py:38:39: AIR301 `airflow.PY312` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +37 | # airflow root +38 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^^ AIR301 -78 | DatasetFromRoot() +39 | +40 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:81:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0 +AIR301_names.py:41:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0 | -80 | # airflow.api_connexion.security -81 | requires_access, requires_access_dataset +40 | # airflow.api_connexion.security +41 | requires_access | ^^^^^^^^^^^^^^^ AIR301 -82 | -83 | # airflow.auth.managers +42 | +43 | # airflow.contrib.* | - = help: Use `airflow.api_connexion.security.requires_access_*` instead + = help: Use `airflow.api_fastapi.core_api.security.requires_access_*` instead -AIR301_names.py:81:18: AIR301 [*] `airflow.api_connexion.security.requires_access_dataset` is removed in Airflow 3.0 +AIR301_names.py:44:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0 | -80 | # airflow.api_connexion.security -81 | requires_access, requires_access_dataset - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -82 | -83 | # airflow.auth.managers +43 | # airflow.contrib.* +44 | AWSAthenaHook() + | ^^^^^^^^^^^^^ AIR301 | - = help: Use `airflow.api_connexion.security.requires_access_asset` instead - -ℹ Safe fix -9 9 | PY311, -10 10 | PY312, -11 11 | ) -12 |-from airflow.api_connexion.security import requires_access, requires_access_dataset - 12 |+from airflow.api_connexion.security import requires_access, requires_access_dataset, requires_access_asset -13 13 | from airflow.auth.managers.base_auth_manager import is_authorized_dataset -14 14 | from airflow.auth.managers.models.resource_details import DatasetDetails -15 15 | from airflow.configuration import ( --------------------------------------------------------------------------------- -78 78 | DatasetFromRoot() -79 79 | -80 80 | # airflow.api_connexion.security -81 |-requires_access, requires_access_dataset - 81 |+requires_access, requires_access_asset -82 82 | -83 83 | # airflow.auth.managers -84 84 | is_authorized_dataset + = help: The whole `airflow.contrib` module has been removed. -AIR301_names.py:84:1: AIR301 `airflow.auth.managers.base_auth_manager.is_authorized_dataset` is removed in Airflow 3.0 +AIR301_names.py:48:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0 | -83 | # airflow.auth.managers -84 | is_authorized_dataset - | ^^^^^^^^^^^^^^^^^^^^^ AIR301 -85 | DatasetDetails() +47 | # airflow.datasets +48 | DatasetAliasEvent() + | ^^^^^^^^^^^^^^^^^ AIR301 | - = help: Use `airflow.api_fastapi.auth.managers.base_auth_manager.is_authorized_asset` instead -AIR301_names.py:85:1: AIR301 `airflow.auth.managers.models.resource_details.DatasetDetails` is removed in Airflow 3.0 +AIR301_names.py:52:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0 | -83 | # airflow.auth.managers -84 | is_authorized_dataset -85 | DatasetDetails() +51 | # airflow.operators.subdag.* +52 | SubDagOperator() | ^^^^^^^^^^^^^^ AIR301 -86 | -87 | # airflow.configuration | - = help: Use `airflow.api_fastapi.auth.managers.models.resource_details.AssetDetails` instead + = help: The whole `airflow.subdag` module has been removed. -AIR301_names.py:88:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0 +AIR301_names.py:61:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^ AIR301 +60 | # airflow.triggers.external_task +61 | TaskStateTrigger() + | ^^^^^^^^^^^^^^^^ AIR301 +62 | +63 | # airflow.utils.date | - = help: Use `airflow.configuration.conf.get` instead -AIR301_names.py:88:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0 +AIR301_names.py:64:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^^^^^^^^ AIR301 +63 | # airflow.utils.date +64 | dates.date_range + | ^^^^^^^^^^^^^^^^ AIR301 +65 | dates.days_ago | - = help: Use `airflow.configuration.conf.getboolean` instead -AIR301_names.py:88:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0 +AIR301_names.py:65:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^^^^^^ AIR301 +63 | # airflow.utils.date +64 | dates.date_range +65 | dates.days_ago + | ^^^^^^^^^^^^^^ AIR301 +66 | +67 | date_range | - = help: Use `airflow.configuration.conf.getfloat` instead + = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead -AIR301_names.py:88:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0 +AIR301_names.py:67:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^^^^ AIR301 +65 | dates.days_ago +66 | +67 | date_range + | ^^^^^^^^^^ AIR301 +68 | days_ago +69 | infer_time_unit | - = help: Use `airflow.configuration.conf.getint` instead -AIR301_names.py:88:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0 +AIR301_names.py:68:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^^^^^^^^ AIR301 +67 | date_range +68 | days_ago + | ^^^^^^^^ AIR301 +69 | infer_time_unit +70 | parse_execution_date | - = help: Use `airflow.configuration.conf.has_option` instead + = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead -AIR301_names.py:88:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0 +AIR301_names.py:69:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^^^^^^^^^^^ AIR301 +67 | date_range +68 | days_ago +69 | infer_time_unit + | ^^^^^^^^^^^^^^^ AIR301 +70 | parse_execution_date +71 | round_time | - = help: Use `airflow.configuration.conf.remove_option` instead -AIR301_names.py:88:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0 +AIR301_names.py:70:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^^^^^ AIR301 +68 | days_ago +69 | infer_time_unit +70 | parse_execution_date + | ^^^^^^^^^^^^^^^^^^^^ AIR301 +71 | round_time +72 | scale_time_units | - = help: Use `airflow.configuration.conf.as_dict` instead -AIR301_names.py:88:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0 +AIR301_names.py:71:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set - | ^^^ AIR301 +69 | infer_time_unit +70 | parse_execution_date +71 | round_time + | ^^^^^^^^^^ AIR301 +72 | scale_time_units | - = help: Use `airflow.configuration.conf.set` instead -AIR301_names.py:92:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0; The whole `airflow.contrib` module has been removed. +AIR301_names.py:72:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0 | -91 | # airflow.contrib.* -92 | AWSAthenaHook() - | ^^^^^^^^^^^^^ AIR301 +70 | parse_execution_date +71 | round_time +72 | scale_time_units + | ^^^^^^^^^^^^^^^^ AIR301 +73 | +74 | # This one was not deprecated. | -AIR301_names.py:96:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0 +AIR301_names.py:79:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0 | -95 | # airflow.datasets -96 | DatasetAliasEvent() - | ^^^^^^^^^^^^^^^^^ AIR301 -97 | -98 | # airflow.datasets.manager +78 | # airflow.utils.dag_cycle_tester +79 | test_cycle + | ^^^^^^^^^^ AIR301 | -AIR301_names.py:99:1: AIR301 `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 - | - 98 | # airflow.datasets.manager - 99 | DatasetManager() - | ^^^^^^^^^^^^^^ AIR301 -100 | dataset_manager -101 | resolve_dataset_manager - | - = help: Use `airflow.assets.manager.AssetManager` instead - -AIR301_names.py:100:1: AIR301 `airflow.datasets.manager.dataset_manager` is removed in Airflow 3.0 - | - 98 | # airflow.datasets.manager - 99 | DatasetManager() -100 | dataset_manager - | ^^^^^^^^^^^^^^^ AIR301 -101 | resolve_dataset_manager - | - = help: Use `airflow.assets.manager.asset_manager` instead - -AIR301_names.py:101:1: AIR301 `airflow.datasets.manager.resolve_dataset_manager` is removed in Airflow 3.0 - | - 99 | DatasetManager() -100 | dataset_manager -101 | resolve_dataset_manager - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -102 | -103 | # airflow.hooks - | - = help: Use `airflow.assets.resolve_asset_manager` instead - -AIR301_names.py:104:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0 - | -103 | # airflow.hooks -104 | BaseHook() - | ^^^^^^^^ AIR301 -105 | -106 | # airflow.lineage.hook - | - = help: Use `airflow.hooks.base.BaseHook` instead - -AIR301_names.py:107:1: AIR301 `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 - | -106 | # airflow.lineage.hook -107 | DatasetLineageInfo() - | ^^^^^^^^^^^^^^^^^^ AIR301 -108 | -109 | # airflow.listeners.spec.dataset - | - = help: Use `airflow.lineage.hook.AssetLineageInfo` instead - -AIR301_names.py:110:1: AIR301 `airflow.listeners.spec.dataset.on_dataset_changed` is removed in Airflow 3.0 - | -109 | # airflow.listeners.spec.dataset -110 | on_dataset_changed - | ^^^^^^^^^^^^^^^^^^ AIR301 -111 | on_dataset_created - | - = help: Use `airflow.listeners.spec.asset.on_asset_changed` instead - -AIR301_names.py:111:1: AIR301 `airflow.listeners.spec.dataset.on_dataset_created` is removed in Airflow 3.0 - | -109 | # airflow.listeners.spec.dataset -110 | on_dataset_changed -111 | on_dataset_created - | ^^^^^^^^^^^^^^^^^^ AIR301 -112 | -113 | # airflow.metrics.validators - | - = help: Use `airflow.listeners.spec.asset.on_asset_created` instead - -AIR301_names.py:114:1: AIR301 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0 - | -113 | # airflow.metrics.validators -114 | AllowListValidator() - | ^^^^^^^^^^^^^^^^^^ AIR301 -115 | BlockListValidator() - | - = help: Use `airflow.metrics.validators.PatternAllowListValidator` instead - -AIR301_names.py:115:1: AIR301 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0 - | -113 | # airflow.metrics.validators -114 | AllowListValidator() -115 | BlockListValidator() - | ^^^^^^^^^^^^^^^^^^ AIR301 - | - = help: Use `airflow.metrics.validators.PatternBlockListValidator` instead - -AIR301_names.py:138:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0; The whole `airflow.subdag` module has been removed. - | -137 | # airflow.operators.subdag.* -138 | SubDagOperator() - | ^^^^^^^^^^^^^^ AIR301 -139 | -140 | # airflow.providers.amazon - | - -AIR301_names.py:141:13: AIR301 `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.DATASET` is removed in Airflow 3.0 - | -140 | # airflow.providers.amazon -141 | AvpEntities.DATASET - | ^^^^^^^ AIR301 -142 | s3.create_dataset -143 | s3.convert_dataset_to_openlineage - | - = help: Use `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.ASSET` instead - -AIR301_names.py:142:4: AIR301 `airflow.providers.amazon.aws.datasets.s3.create_dataset` is removed in Airflow 3.0 - | -140 | # airflow.providers.amazon -141 | AvpEntities.DATASET -142 | s3.create_dataset - | ^^^^^^^^^^^^^^ AIR301 -143 | s3.convert_dataset_to_openlineage -144 | s3.sanitize_uri - | - = help: Use `airflow.providers.amazon.aws.assets.s3.create_asset` instead - -AIR301_names.py:143:4: AIR301 `airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage` is removed in Airflow 3.0 - | -141 | AvpEntities.DATASET -142 | s3.create_dataset -143 | s3.convert_dataset_to_openlineage - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -144 | s3.sanitize_uri - | - = help: Use `airflow.providers.amazon.aws.assets.s3.convert_asset_to_openlineage` instead - -AIR301_names.py:144:4: AIR301 `airflow.providers.amazon.aws.datasets.s3.sanitize_uri` is removed in Airflow 3.0 - | -142 | s3.create_dataset -143 | s3.convert_dataset_to_openlineage -144 | s3.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -145 | -146 | # airflow.providers.common.io - | - = help: Use `airflow.providers.amazon.aws.assets.s3.sanitize_uri` instead - -AIR301_names.py:147:16: AIR301 `airflow.providers.common.io.datasets.file.convert_dataset_to_openlineage` is removed in Airflow 3.0 - | -146 | # airflow.providers.common.io -147 | common_io_file.convert_dataset_to_openlineage - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -148 | common_io_file.create_dataset -149 | common_io_file.sanitize_uri - | - = help: Use `airflow.providers.common.io.assets.file.convert_asset_to_openlineage` instead - -AIR301_names.py:148:16: AIR301 `airflow.providers.common.io.datasets.file.create_dataset` is removed in Airflow 3.0 - | -146 | # airflow.providers.common.io -147 | common_io_file.convert_dataset_to_openlineage -148 | common_io_file.create_dataset - | ^^^^^^^^^^^^^^ AIR301 -149 | common_io_file.sanitize_uri - | - = help: Use `airflow.providers.common.io.assets.file.create_asset` instead - -AIR301_names.py:149:16: AIR301 `airflow.providers.common.io.datasets.file.sanitize_uri` is removed in Airflow 3.0 - | -147 | common_io_file.convert_dataset_to_openlineage -148 | common_io_file.create_dataset -149 | common_io_file.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -150 | -151 | # airflow.providers.fab - | - = help: Use `airflow.providers.common.io.assets.file.sanitize_uri` instead - -AIR301_names.py:152:18: AIR301 `airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_dataset` is removed in Airflow 3.0 - | -151 | # airflow.providers.fab -152 | fab_auth_manager.is_authorized_dataset - | ^^^^^^^^^^^^^^^^^^^^^ AIR301 -153 | -154 | # airflow.providers.google - | - = help: Use `airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset` instead - -AIR301_names.py:157:5: AIR301 `airflow.providers.google.datasets.gcs.create_dataset` is removed in Airflow 3.0 - | -155 | bigquery.sanitize_uri -156 | -157 | gcs.create_dataset - | ^^^^^^^^^^^^^^ AIR301 -158 | gcs.sanitize_uri -159 | gcs.convert_dataset_to_openlineage - | - = help: Use `airflow.providers.google.assets.gcs.create_asset` instead - -AIR301_names.py:158:5: AIR301 `airflow.providers.google.datasets.gcs.sanitize_uri` is removed in Airflow 3.0 - | -157 | gcs.create_dataset -158 | gcs.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -159 | gcs.convert_dataset_to_openlineage - | - = help: Use `airflow.providers.google.assets.gcs.sanitize_uri` instead - -AIR301_names.py:159:5: AIR301 `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage` is removed in Airflow 3.0 - | -157 | gcs.create_dataset -158 | gcs.sanitize_uri -159 | gcs.convert_dataset_to_openlineage - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -160 | -161 | # airflow.providers.mysql - | - = help: Use `airflow.providers.google.assets.gcs.convert_asset_to_openlineage` instead - -AIR301_names.py:162:7: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0 - | -161 | # airflow.providers.mysql -162 | mysql.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -163 | -164 | # airflow.providers.openlineage - | - = help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead - -AIR301_names.py:165:1: AIR301 `airflow.providers.openlineage.utils.utils.DatasetInfo` is removed in Airflow 3.0 - | -164 | # airflow.providers.openlineage -165 | DatasetInfo() - | ^^^^^^^^^^^ AIR301 -166 | translate_airflow_dataset - | - = help: Use `airflow.providers.openlineage.utils.utils.AssetInfo` instead - -AIR301_names.py:166:1: AIR301 `airflow.providers.openlineage.utils.utils.translate_airflow_dataset` is removed in Airflow 3.0 - | -164 | # airflow.providers.openlineage -165 | DatasetInfo() -166 | translate_airflow_dataset - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -167 | -168 | # airflow.providers.postgres - | - = help: Use `airflow.providers.openlineage.utils.utils.translate_airflow_asset` instead - -AIR301_names.py:169:10: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0 - | -168 | # airflow.providers.postgres -169 | postgres.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -170 | -171 | # airflow.providers.trino - | - = help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead - -AIR301_names.py:172:7: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0 - | -171 | # airflow.providers.trino -172 | trino.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -173 | -174 | # airflow.secrets - | - = help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead - -AIR301_names.py:177:1: AIR301 `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0 - | -175 | # get_connection -176 | LocalFilesystemBackend() -177 | load_connections - | ^^^^^^^^^^^^^^^^ AIR301 -178 | -179 | # airflow.security.permissions - | - = help: Use `airflow.secrets.local_filesystem.load_connections_dict` instead - -AIR301_names.py:180:1: AIR301 `airflow.security.permissions.RESOURCE_DATASET` is removed in Airflow 3.0 - | -179 | # airflow.security.permissions -180 | RESOURCE_DATASET - | ^^^^^^^^^^^^^^^^ AIR301 -181 | -182 | # airflow.sensors.base_sensor_operator - | - = help: Use `airflow.security.permissions.RESOURCE_ASSET` instead - -AIR301_names.py:183:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0 - | -182 | # airflow.sensors.base_sensor_operator -183 | BaseSensorOperator() - | ^^^^^^^^^^^^^^^^^^ AIR301 - | - = help: Use `airflow.sdk.bases.sensor.BaseSensorOperator` instead - -AIR301_names.py:187:1: AIR301 `airflow.timetables.simple.DatasetTriggeredTimetable` is removed in Airflow 3.0 - | -186 | # airflow.timetables -187 | DatasetTriggeredTimetable() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -188 | -189 | # airflow.triggers.external_task - | - = help: Use `airflow.timetables.simple.AssetTriggeredTimetable` instead - -AIR301_names.py:190:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0 - | -189 | # airflow.triggers.external_task -190 | TaskStateTrigger() - | ^^^^^^^^^^^^^^^^ AIR301 -191 | -192 | # airflow.utils.date - | - -AIR301_names.py:193:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 - | -192 | # airflow.utils.date -193 | dates.date_range - | ^^^^^^^^^^ AIR301 -194 | dates.days_ago - | - -AIR301_names.py:194:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 - | -192 | # airflow.utils.date -193 | dates.date_range -194 | dates.days_ago - | ^^^^^^^^ AIR301 -195 | -196 | date_range - | - = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead - -AIR301_names.py:196:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 - | -194 | dates.days_ago -195 | -196 | date_range - | ^^^^^^^^^^ AIR301 -197 | days_ago -198 | infer_time_unit - | - -AIR301_names.py:197:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 - | -196 | date_range -197 | days_ago - | ^^^^^^^^ AIR301 -198 | infer_time_unit -199 | parse_execution_date - | - = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead - -AIR301_names.py:198:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0 - | -196 | date_range -197 | days_ago -198 | infer_time_unit - | ^^^^^^^^^^^^^^^ AIR301 -199 | parse_execution_date -200 | round_time - | - -AIR301_names.py:199:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0 - | -197 | days_ago -198 | infer_time_unit -199 | parse_execution_date - | ^^^^^^^^^^^^^^^^^^^^ AIR301 -200 | round_time -201 | scale_time_units - | - -AIR301_names.py:200:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0 - | -198 | infer_time_unit -199 | parse_execution_date -200 | round_time - | ^^^^^^^^^^ AIR301 -201 | scale_time_units - | - -AIR301_names.py:201:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0 - | -199 | parse_execution_date -200 | round_time -201 | scale_time_units - | ^^^^^^^^^^^^^^^^ AIR301 -202 | -203 | # This one was not deprecated. - | - -AIR301_names.py:208:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0 - | -207 | # airflow.utils.dag_cycle_tester -208 | test_cycle - | ^^^^^^^^^^ AIR301 - | - -AIR301_names.py:212:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0 - | -211 | # airflow.utils.db -212 | create_session - | ^^^^^^^^^^^^^^ AIR301 -213 | -214 | # airflow.utils.decorators - | - -AIR301_names.py:215:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed. - | -214 | # airflow.utils.decorators -215 | apply_defaults - | ^^^^^^^^^^^^^^ AIR301 -216 | -217 | # airflow.utils.file - | - -AIR301_names.py:218:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0 - | -217 | # airflow.utils.file -218 | TemporaryDirectory() - | ^^^^^^^^^^^^^^^^^^ AIR301 -219 | mkdirs - | - = help: Use `tempfile.TemporaryDirectory` instead - -AIR301_names.py:219:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 - | -217 | # airflow.utils.file -218 | TemporaryDirectory() -219 | mkdirs - | ^^^^^^ AIR301 -220 | -221 | # airflow.utils.helpers - | - = help: Use `pathlib.Path({path}).mkdir` instead - -AIR301_names.py:222:1: AIR301 `airflow.utils.helpers.chain` is removed in Airflow 3.0 - | -221 | # airflow.utils.helpers -222 | helper_chain - | ^^^^^^^^^^^^ AIR301 -223 | helper_cross_downstream - | - = help: Use `airflow.sdk.chain` instead +AIR301_names.py:83:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0 + | +82 | # airflow.utils.db +83 | create_session + | ^^^^^^^^^^^^^^ AIR301 +84 | +85 | # airflow.utils.decorators + | -AIR301_names.py:223:1: AIR301 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0 - | -221 | # airflow.utils.helpers -222 | helper_chain -223 | helper_cross_downstream - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -224 | -225 | # airflow.utils.log - | - = help: Use `airflow.sdk.cross_downstream` instead +AIR301_names.py:86:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0 + | +85 | # airflow.utils.decorators +86 | apply_defaults + | ^^^^^^^^^^^^^^ AIR301 +87 | +88 | # airflow.utils.file + | + = help: `apply_defaults` is now unconditionally done and can be safely removed. -AIR301_names.py:226:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0 - | -225 | # airflow.utils.log -226 | secrets_masker - | ^^^^^^^^^^^^^^ AIR301 -227 | -228 | # airflow.utils.state - | - = help: Use `airflow.sdk.execution_time.secrets_masker` instead +AIR301_names.py:89:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 + | +88 | # airflow.utils.file +89 | mkdirs + | ^^^^^^ AIR301 + | + = help: Use `pathlib.Path({path}).mkdir` instead -AIR301_names.py:229:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0 - | -228 | # airflow.utils.state -229 | SHUTDOWN - | ^^^^^^^^ AIR301 -230 | terminating_states - | +AIR301_names.py:93:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0 + | +92 | # airflow.utils.state +93 | SHUTDOWN + | ^^^^^^^^ AIR301 +94 | terminating_states + | -AIR301_names.py:230:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0 - | -228 | # airflow.utils.state -229 | SHUTDOWN -230 | terminating_states - | ^^^^^^^^^^^^^^^^^^ AIR301 -231 | -232 | # airflow.utils.trigger_rule - | +AIR301_names.py:94:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0 + | +92 | # airflow.utils.state +93 | SHUTDOWN +94 | terminating_states + | ^^^^^^^^^^^^^^^^^^ AIR301 +95 | +96 | # airflow.utils.trigger_rule + | -AIR301_names.py:233:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0 - | -232 | # airflow.utils.trigger_rule -233 | TriggerRule.DUMMY - | ^^^^^ AIR301 -234 | TriggerRule.NONE_FAILED_OR_SKIPPED - | +AIR301_names.py:97:1: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0 + | +96 | # airflow.utils.trigger_rule +97 | TriggerRule.DUMMY + | ^^^^^^^^^^^^^^^^^ AIR301 +98 | TriggerRule.NONE_FAILED_OR_SKIPPED + | -AIR301_names.py:234:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0 - | -232 | # airflow.utils.trigger_rule -233 | TriggerRule.DUMMY -234 | TriggerRule.NONE_FAILED_OR_SKIPPED - | ^^^^^^^^^^^^^^^^^^^^^^ AIR301 -235 | -236 | # airflow.www.auth - | +AIR301_names.py:98:1: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0 + | +96 | # airflow.utils.trigger_rule +97 | TriggerRule.DUMMY +98 | TriggerRule.NONE_FAILED_OR_SKIPPED + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 + | -AIR301_names.py:237:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0 +AIR301_names.py:102:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0 | -236 | # airflow.www.auth -237 | has_access +101 | # airflow.www.auth +102 | has_access | ^^^^^^^^^^ AIR301 -238 | has_access_dataset +103 | has_access_dataset | - = help: Use `airflow.www.auth.has_access_*` instead -AIR301_names.py:238:1: AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0 +AIR301_names.py:103:1: AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0 | -236 | # airflow.www.auth -237 | has_access -238 | has_access_dataset +101 | # airflow.www.auth +102 | has_access +103 | has_access_dataset | ^^^^^^^^^^^^^^^^^^ AIR301 -239 | -240 | # airflow.www.utils +104 | +105 | # airflow.www.utils | - = help: Use `airflow.www.auth.has_access_dataset` instead -AIR301_names.py:241:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0 +AIR301_names.py:106:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0 | -240 | # airflow.www.utils -241 | get_sensitive_variables_fields +105 | # airflow.www.utils +106 | get_sensitive_variables_fields | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -242 | should_hide_value_for_key +107 | should_hide_value_for_key | - = help: Use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead -AIR301_names.py:242:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0 +AIR301_names.py:107:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0 | -240 | # airflow.www.utils -241 | get_sensitive_variables_fields -242 | should_hide_value_for_key +105 | # airflow.www.utils +106 | get_sensitive_variables_fields +107 | should_hide_value_for_key | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 | - = help: Use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap new file mode 100644 index 0000000000000..e9e738ca15b99 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap @@ -0,0 +1,780 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR301_names_fix.py:17:1: AIR301 [*] `airflow.api_connexion.security.requires_access_dataset` is removed in Airflow 3.0 + | +15 | from airflow.security.permissions import RESOURCE_DATASET +16 | +17 | requires_access_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +18 | +19 | DatasetDetails() + | + = help: Use `requires_access_asset` from `airflow.api_fastapi.core_api.security` instead. + +ℹ Safe fix +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET + 16 |+from airflow.api_fastapi.core_api.security import requires_access_asset +16 17 | +17 |-requires_access_dataset() + 18 |+requires_access_asset() +18 19 | +19 20 | DatasetDetails() +20 21 | + +AIR301_names_fix.py:19:1: AIR301 [*] `airflow.auth.managers.models.resource_details.DatasetDetails` is removed in Airflow 3.0 + | +17 | requires_access_dataset() +18 | +19 | DatasetDetails() + | ^^^^^^^^^^^^^^ AIR301 +20 | +21 | DatasetManager() + | + = help: Use `AssetDetails` from `airflow.api_fastapi.auth.managers.models.resource_details` instead. + +ℹ Safe fix +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET + 16 |+from airflow.api_fastapi.auth.managers.models.resource_details import AssetDetails +16 17 | +17 18 | requires_access_dataset() +18 19 | +19 |-DatasetDetails() + 20 |+AssetDetails() +20 21 | +21 22 | DatasetManager() +22 23 | dataset_manager() + +AIR301_names_fix.py:21:1: AIR301 [*] `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 + | +19 | DatasetDetails() +20 | +21 | DatasetManager() + | ^^^^^^^^^^^^^^ AIR301 +22 | dataset_manager() +23 | resolve_dataset_manager() + | + = help: Use `AssetManager` from `airflow.assets.manager` instead. + +ℹ Safe fix +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET + 16 |+from airflow.assets.manager import AssetManager +16 17 | +17 18 | requires_access_dataset() +18 19 | +19 20 | DatasetDetails() +20 21 | +21 |-DatasetManager() + 22 |+AssetManager() +22 23 | dataset_manager() +23 24 | resolve_dataset_manager() +24 25 | + +AIR301_names_fix.py:22:1: AIR301 [*] `airflow.datasets.manager.dataset_manager` is removed in Airflow 3.0 + | +21 | DatasetManager() +22 | dataset_manager() + | ^^^^^^^^^^^^^^^ AIR301 +23 | resolve_dataset_manager() + | + = help: Use `asset_manager` from `airflow.assets.manager` instead. + +ℹ Safe fix +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET + 16 |+from airflow.assets.manager import asset_manager +16 17 | +17 18 | requires_access_dataset() +18 19 | +19 20 | DatasetDetails() +20 21 | +21 22 | DatasetManager() +22 |-dataset_manager() + 23 |+asset_manager() +23 24 | resolve_dataset_manager() +24 25 | +25 26 | DatasetLineageInfo() + +AIR301_names_fix.py:23:1: AIR301 [*] `airflow.datasets.manager.resolve_dataset_manager` is removed in Airflow 3.0 + | +21 | DatasetManager() +22 | dataset_manager() +23 | resolve_dataset_manager() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +24 | +25 | DatasetLineageInfo() + | + = help: Use `resolve_asset_manager` from `airflow.assets.manager` instead. + +ℹ Safe fix +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET + 16 |+from airflow.assets.manager import resolve_asset_manager +16 17 | +17 18 | requires_access_dataset() +18 19 | +-------------------------------------------------------------------------------- +20 21 | +21 22 | DatasetManager() +22 23 | dataset_manager() +23 |-resolve_dataset_manager() + 24 |+resolve_asset_manager() +24 25 | +25 26 | DatasetLineageInfo() +26 27 | + +AIR301_names_fix.py:25:1: AIR301 [*] `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 + | +23 | resolve_dataset_manager() +24 | +25 | DatasetLineageInfo() + | ^^^^^^^^^^^^^^^^^^ AIR301 +26 | +27 | AllowListValidator() + | + = help: Use `AssetLineageInfo` from `airflow.lineage.hook` instead. + +ℹ Safe fix +9 9 | dataset_manager, +10 10 | resolve_dataset_manager, +11 11 | ) +12 |-from airflow.lineage.hook import DatasetLineageInfo + 12 |+from airflow.lineage.hook import DatasetLineageInfo, AssetLineageInfo +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET +-------------------------------------------------------------------------------- +22 22 | dataset_manager() +23 23 | resolve_dataset_manager() +24 24 | +25 |-DatasetLineageInfo() + 25 |+AssetLineageInfo() +26 26 | +27 27 | AllowListValidator() +28 28 | BlockListValidator() + +AIR301_names_fix.py:27:1: AIR301 [*] `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0 + | +25 | DatasetLineageInfo() +26 | +27 | AllowListValidator() + | ^^^^^^^^^^^^^^^^^^ AIR301 +28 | BlockListValidator() + | + = help: Use `PatternAllowListValidator` from `airflow.metrics.validators` instead. + +ℹ Safe fix +10 10 | resolve_dataset_manager, +11 11 | ) +12 12 | from airflow.lineage.hook import DatasetLineageInfo +13 |-from airflow.metrics.validators import AllowListValidator, BlockListValidator + 13 |+from airflow.metrics.validators import AllowListValidator, BlockListValidator, PatternAllowListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET +16 16 | +-------------------------------------------------------------------------------- +24 24 | +25 25 | DatasetLineageInfo() +26 26 | +27 |-AllowListValidator() + 27 |+PatternAllowListValidator() +28 28 | BlockListValidator() +29 29 | +30 30 | load_connections() + +AIR301_names_fix.py:28:1: AIR301 [*] `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0 + | +27 | AllowListValidator() +28 | BlockListValidator() + | ^^^^^^^^^^^^^^^^^^ AIR301 +29 | +30 | load_connections() + | + = help: Use `PatternBlockListValidator` from `airflow.metrics.validators` instead. + +ℹ Safe fix +10 10 | resolve_dataset_manager, +11 11 | ) +12 12 | from airflow.lineage.hook import DatasetLineageInfo +13 |-from airflow.metrics.validators import AllowListValidator, BlockListValidator + 13 |+from airflow.metrics.validators import AllowListValidator, BlockListValidator, PatternBlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 15 | from airflow.security.permissions import RESOURCE_DATASET +16 16 | +-------------------------------------------------------------------------------- +25 25 | DatasetLineageInfo() +26 26 | +27 27 | AllowListValidator() +28 |-BlockListValidator() + 28 |+PatternBlockListValidator() +29 29 | +30 30 | load_connections() +31 31 | + +AIR301_names_fix.py:30:1: AIR301 [*] `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0 + | +28 | BlockListValidator() +29 | +30 | load_connections() + | ^^^^^^^^^^^^^^^^ AIR301 +31 | +32 | RESOURCE_DATASET + | + = help: Use `load_connections_dict` from `airflow.secrets.local_filesystem` instead. + +ℹ Safe fix +11 11 | ) +12 12 | from airflow.lineage.hook import DatasetLineageInfo +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 |-from airflow.secrets.local_filesystem import load_connections + 14 |+from airflow.secrets.local_filesystem import load_connections, load_connections_dict +15 15 | from airflow.security.permissions import RESOURCE_DATASET +16 16 | +17 17 | requires_access_dataset() +-------------------------------------------------------------------------------- +27 27 | AllowListValidator() +28 28 | BlockListValidator() +29 29 | +30 |-load_connections() + 30 |+load_connections_dict() +31 31 | +32 32 | RESOURCE_DATASET +33 33 | + +AIR301_names_fix.py:32:1: AIR301 [*] `airflow.security.permissions.RESOURCE_DATASET` is removed in Airflow 3.0 + | +30 | load_connections() +31 | +32 | RESOURCE_DATASET + | ^^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `RESOURCE_ASSET` from `airflow.security.permissions` instead. + +ℹ Safe fix +12 12 | from airflow.lineage.hook import DatasetLineageInfo +13 13 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +14 14 | from airflow.secrets.local_filesystem import load_connections +15 |-from airflow.security.permissions import RESOURCE_DATASET + 15 |+from airflow.security.permissions import RESOURCE_DATASET, RESOURCE_ASSET +16 16 | +17 17 | requires_access_dataset() +18 18 | +-------------------------------------------------------------------------------- +29 29 | +30 30 | load_connections() +31 31 | +32 |-RESOURCE_DATASET + 32 |+RESOURCE_ASSET +33 33 | +34 34 | +35 35 | from airflow.listeners.spec.dataset import ( + +AIR301_names_fix.py:40:1: AIR301 [*] `airflow.listeners.spec.dataset.on_dataset_created` is removed in Airflow 3.0 + | +38 | ) +39 | +40 | on_dataset_created() + | ^^^^^^^^^^^^^^^^^^ AIR301 +41 | on_dataset_changed() + | + = help: Use `on_asset_created` from `airflow.listeners.spec.asset` instead. + +ℹ Safe fix +36 36 | on_dataset_changed, +37 37 | on_dataset_created, +38 38 | ) + 39 |+from airflow.listeners.spec.asset import on_asset_created +39 40 | +40 |-on_dataset_created() + 41 |+on_asset_created() +41 42 | on_dataset_changed() +42 43 | +43 44 | + +AIR301_names_fix.py:41:1: AIR301 [*] `airflow.listeners.spec.dataset.on_dataset_changed` is removed in Airflow 3.0 + | +40 | on_dataset_created() +41 | on_dataset_changed() + | ^^^^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `on_asset_changed` from `airflow.listeners.spec.asset` instead. + +ℹ Safe fix +36 36 | on_dataset_changed, +37 37 | on_dataset_created, +38 38 | ) + 39 |+from airflow.listeners.spec.asset import on_asset_changed +39 40 | +40 41 | on_dataset_created() +41 |-on_dataset_changed() + 42 |+on_asset_changed() +42 43 | +43 44 | +44 45 | # airflow.operators.python + +AIR301_names_fix.py:47:1: AIR301 [*] `airflow.operators.python.get_current_context` is removed in Airflow 3.0 + | +45 | from airflow.operators.python import get_current_context +46 | +47 | get_current_context() + | ^^^^^^^^^^^^^^^^^^^ AIR301 +48 | +49 | # airflow.providers.mysql + | + = help: Use `get_current_context` from `airflow.sdk` instead. + +ℹ Unsafe fix +42 42 | +43 43 | +44 44 | # airflow.operators.python +45 |-from airflow.operators.python import get_current_context + 45 |+from airflow.sdk import get_current_context +46 46 | +47 47 | get_current_context() +48 48 | + +AIR301_names_fix.py:52:1: AIR301 [*] `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0 + | +50 | from airflow.providers.mysql.datasets.mysql import sanitize_uri +51 | +52 | sanitize_uri + | ^^^^^^^^^^^^ AIR301 +53 | +54 | # airflow.providers.postgres + | + = help: Use `sanitize_uri` from `airflow.providers.mysql.assets.mysql` instead. + +ℹ Unsafe fix +47 47 | get_current_context() +48 48 | +49 49 | # airflow.providers.mysql +50 |-from airflow.providers.mysql.datasets.mysql import sanitize_uri + 50 |+from airflow.providers.mysql.assets.mysql import sanitize_uri +51 51 | +52 52 | sanitize_uri +53 53 | + +AIR301_names_fix.py:57:1: AIR301 [*] `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0 + | +55 | from airflow.providers.postgres.datasets.postgres import sanitize_uri +56 | +57 | sanitize_uri + | ^^^^^^^^^^^^ AIR301 +58 | +59 | # airflow.providers.trino + | + = help: Use `sanitize_uri` from `airflow.providers.postgres.assets.postgres` instead. + +ℹ Unsafe fix +52 52 | sanitize_uri +53 53 | +54 54 | # airflow.providers.postgres +55 |-from airflow.providers.postgres.datasets.postgres import sanitize_uri + 55 |+from airflow.providers.postgres.assets.postgres import sanitize_uri +56 56 | +57 57 | sanitize_uri +58 58 | + +AIR301_names_fix.py:62:1: AIR301 [*] `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0 + | +60 | from airflow.providers.trino.datasets.trino import sanitize_uri +61 | +62 | sanitize_uri + | ^^^^^^^^^^^^ AIR301 +63 | +64 | # airflow.notifications.basenotifier + | + = help: Use `sanitize_uri` from `airflow.providers.trino.assets.trino` instead. + +ℹ Unsafe fix +57 57 | sanitize_uri +58 58 | +59 59 | # airflow.providers.trino +60 |-from airflow.providers.trino.datasets.trino import sanitize_uri + 60 |+from airflow.providers.trino.assets.trino import sanitize_uri +61 61 | +62 62 | sanitize_uri +63 63 | + +AIR301_names_fix.py:67:1: AIR301 [*] `airflow.notifications.basenotifier.BaseNotifier` is removed in Airflow 3.0 + | +65 | from airflow.notifications.basenotifier import BaseNotifier +66 | +67 | BaseNotifier() + | ^^^^^^^^^^^^ AIR301 +68 | +69 | # airflow.auth.manager + | + = help: Use `BaseNotifier` from `airflow.sdk.bases.notifier` instead. + +ℹ Unsafe fix +62 62 | sanitize_uri +63 63 | +64 64 | # airflow.notifications.basenotifier +65 |-from airflow.notifications.basenotifier import BaseNotifier + 65 |+from airflow.sdk.bases.notifier import BaseNotifier +66 66 | +67 67 | BaseNotifier() +68 68 | + +AIR301_names_fix.py:72:1: AIR301 [*] `airflow.auth.managers.base_auth_manager.BaseAuthManager` is removed in Airflow 3.0 + | +70 | from airflow.auth.managers.base_auth_manager import BaseAuthManager +71 | +72 | BaseAuthManager() + | ^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `BaseAuthManager` from `airflow.api_fastapi.auth.managers.base_auth_manager` instead. + +ℹ Unsafe fix +67 67 | BaseNotifier() +68 68 | +69 69 | # airflow.auth.manager +70 |-from airflow.auth.managers.base_auth_manager import BaseAuthManager + 70 |+from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager +71 71 | +72 72 | BaseAuthManager() +73 73 | + +AIR301_names_fix.py:87:1: AIR301 [*] `airflow.configuration.get` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.get` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+conf, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:6: AIR301 [*] `airflow.configuration.getboolean` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^^^^^^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.getboolean` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, conf, getfloat, getint, has_option, remove_option, as_dict, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:18: AIR301 [*] `airflow.configuration.getfloat` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^^^^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.getfloat` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, getboolean, conf, getint, has_option, remove_option, as_dict, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:28: AIR301 [*] `airflow.configuration.getint` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.getint` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, getboolean, getfloat, conf, has_option, remove_option, as_dict, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:36: AIR301 [*] `airflow.configuration.has_option` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^^^^^^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.has_option` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, getboolean, getfloat, getint, conf, remove_option, as_dict, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:48: AIR301 [*] `airflow.configuration.remove_option` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^^^^^^^^^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.remove_option` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, getboolean, getfloat, getint, has_option, conf, as_dict, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:63: AIR301 [*] `airflow.configuration.as_dict` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^^^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.as_dict` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, getboolean, getfloat, getint, has_option, remove_option, conf, set +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:87:72: AIR301 [*] `airflow.configuration.set` is removed in Airflow 3.0 + | +86 | # airflow.configuration +87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + | ^^^ AIR301 +88 | from airflow.hooks.base_hook import BaseHook + | + = help: Use `conf.set` from `airflow.configuration` instead. + +ℹ Safe fix +81 81 | has_option, +82 82 | remove_option, +83 83 | set, + 84 |+conf, +84 85 | ) +85 86 | +86 87 | # airflow.configuration +87 |-get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set + 88 |+get, getboolean, getfloat, getint, has_option, remove_option, as_dict, conf +88 89 | from airflow.hooks.base_hook import BaseHook +89 90 | +90 91 | # airflow.hooks + +AIR301_names_fix.py:91:1: AIR301 [*] `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0 + | +90 | # airflow.hooks +91 | BaseHook() + | ^^^^^^^^ AIR301 +92 | +93 | from airflow.sensors.base_sensor_operator import BaseSensorOperator + | + = help: Use `BaseHook` from `airflow.hooks.base` instead. + +ℹ Unsafe fix +85 85 | +86 86 | # airflow.configuration +87 87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +88 |-from airflow.hooks.base_hook import BaseHook + 88 |+from airflow.hooks.base import BaseHook +89 89 | +90 90 | # airflow.hooks +91 91 | BaseHook() + +AIR301_names_fix.py:96:1: AIR301 [*] `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0 + | +95 | # airflow.sensors.base_sensor_operator +96 | BaseSensorOperator() + | ^^^^^^^^^^^^^^^^^^ AIR301 +97 | BaseHook() + | + = help: Use `BaseSensorOperator` from `airflow.sdk.bases.sensor` instead. + +ℹ Unsafe fix +90 90 | # airflow.hooks +91 91 | BaseHook() +92 92 | +93 |-from airflow.sensors.base_sensor_operator import BaseSensorOperator + 93 |+from airflow.sdk.bases.sensor import BaseSensorOperator +94 94 | +95 95 | # airflow.sensors.base_sensor_operator +96 96 | BaseSensorOperator() + +AIR301_names_fix.py:97:1: AIR301 [*] `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0 + | +95 | # airflow.sensors.base_sensor_operator +96 | BaseSensorOperator() +97 | BaseHook() + | ^^^^^^^^ AIR301 +98 | +99 | from airflow.utils.helpers import chain as helper_chain + | + = help: Use `BaseHook` from `airflow.hooks.base` instead. + +ℹ Unsafe fix +85 85 | +86 86 | # airflow.configuration +87 87 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +88 |-from airflow.hooks.base_hook import BaseHook +89 88 | +90 89 | # airflow.hooks +91 90 | BaseHook() +92 91 | +93 92 | from airflow.sensors.base_sensor_operator import BaseSensorOperator + 93 |+from airflow.hooks.base import BaseHook +94 94 | +95 95 | # airflow.sensors.base_sensor_operator +96 96 | BaseSensorOperator() + +AIR301_names_fix.py:103:1: AIR301 [*] `airflow.utils.helpers.chain` is removed in Airflow 3.0 + | +102 | # airflow.utils.helpers +103 | helper_chain + | ^^^^^^^^^^^^ AIR301 +104 | helper_cross_downstream + | + = help: Use `chain` from `airflow.sdk` instead. + +ℹ Safe fix +98 98 | +99 99 | from airflow.utils.helpers import chain as helper_chain +100 100 | from airflow.utils.helpers import cross_downstream as helper_cross_downstream + 101 |+from airflow.sdk import chain +101 102 | +102 103 | # airflow.utils.helpers +103 |-helper_chain + 104 |+chain +104 105 | helper_cross_downstream +105 106 | +106 107 | # airflow.utils.file + +AIR301_names_fix.py:104:1: AIR301 [*] `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0 + | +102 | # airflow.utils.helpers +103 | helper_chain +104 | helper_cross_downstream + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +105 | +106 | # airflow.utils.file + | + = help: Use `cross_downstream` from `airflow.sdk` instead. + +ℹ Safe fix +98 98 | +99 99 | from airflow.utils.helpers import chain as helper_chain +100 100 | from airflow.utils.helpers import cross_downstream as helper_cross_downstream + 101 |+from airflow.sdk import cross_downstream +101 102 | +102 103 | # airflow.utils.helpers +103 104 | helper_chain +104 |-helper_cross_downstream + 105 |+cross_downstream +105 106 | +106 107 | # airflow.utils.file +107 108 | from airflow.utils.file import TemporaryDirectory + +AIR301_names_fix.py:109:1: AIR301 [*] `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0 + | +107 | from airflow.utils.file import TemporaryDirectory +108 | +109 | TemporaryDirectory() + | ^^^^^^^^^^^^^^^^^^ AIR301 +110 | +111 | from airflow.utils.log import secrets_masker + | + = help: Use `TemporaryDirectory` from `tempfile` instead. + +ℹ Unsafe fix +104 104 | helper_cross_downstream +105 105 | +106 106 | # airflow.utils.file +107 |-from airflow.utils.file import TemporaryDirectory + 107 |+from tempfile import TemporaryDirectory +108 108 | +109 109 | TemporaryDirectory() +110 110 | + +AIR301_names_fix.py:114:1: AIR301 [*] `airflow.utils.log.secrets_masker` is removed in Airflow 3.0 + | +113 | # airflow.utils.log +114 | secrets_masker + | ^^^^^^^^^^^^^^ AIR301 + | + = help: Use `secrets_masker` from `airflow.sdk.execution_time` instead. + +ℹ Unsafe fix +108 108 | +109 109 | TemporaryDirectory() +110 110 | +111 |-from airflow.utils.log import secrets_masker + 111 |+from airflow.sdk.execution_time import secrets_masker +112 112 | +113 113 | # airflow.utils.log +114 114 | secrets_masker diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap new file mode 100644 index 0000000000000..6c53d5dd6660d --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap @@ -0,0 +1,243 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR301_provider_names_fix.py:11:1: AIR301 [*] `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.DATASET` is removed in Airflow 3.0 + | + 9 | from airflow.security.permissions import RESOURCE_DATASET +10 | +11 | AvpEntities.DATASET + | ^^^^^^^^^^^^^^^^^^^ AIR301 +12 | +13 | # airflow.providers.openlineage.utils.utils + | + = help: Use `AvpEntities.ASSET` from `airflow.providers.amazon.aws.auth_manager.avp.entities` instead. + +ℹ Safe fix +8 8 | from airflow.secrets.local_filesystem import load_connections +9 9 | from airflow.security.permissions import RESOURCE_DATASET +10 10 | +11 |-AvpEntities.DATASET + 11 |+AvpEntities +12 12 | +13 13 | # airflow.providers.openlineage.utils.utils +14 14 | DatasetInfo() + +AIR301_provider_names_fix.py:14:1: AIR301 [*] `airflow.providers.openlineage.utils.utils.DatasetInfo` is removed in Airflow 3.0 + | +13 | # airflow.providers.openlineage.utils.utils +14 | DatasetInfo() + | ^^^^^^^^^^^ AIR301 +15 | translate_airflow_dataset() + | + = help: Use `AssetInfo` from `airflow.providers.openlineage.utils.utils` instead. + +ℹ Safe fix +4 4 | from airflow.providers.openlineage.utils.utils import ( +5 5 | DatasetInfo, +6 6 | translate_airflow_dataset, + 7 |+AssetInfo, +7 8 | ) +8 9 | from airflow.secrets.local_filesystem import load_connections +9 10 | from airflow.security.permissions import RESOURCE_DATASET +-------------------------------------------------------------------------------- +11 12 | AvpEntities.DATASET +12 13 | +13 14 | # airflow.providers.openlineage.utils.utils +14 |-DatasetInfo() + 15 |+AssetInfo() +15 16 | translate_airflow_dataset() +16 17 | +17 18 | # airflow.secrets.local_filesystem + +AIR301_provider_names_fix.py:15:1: AIR301 [*] `airflow.providers.openlineage.utils.utils.translate_airflow_dataset` is removed in Airflow 3.0 + | +13 | # airflow.providers.openlineage.utils.utils +14 | DatasetInfo() +15 | translate_airflow_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +16 | +17 | # airflow.secrets.local_filesystem + | + = help: Use `translate_airflow_asset` from `airflow.providers.openlineage.utils.utils` instead. + +ℹ Safe fix +4 4 | from airflow.providers.openlineage.utils.utils import ( +5 5 | DatasetInfo, +6 6 | translate_airflow_dataset, + 7 |+translate_airflow_asset, +7 8 | ) +8 9 | from airflow.secrets.local_filesystem import load_connections +9 10 | from airflow.security.permissions import RESOURCE_DATASET +-------------------------------------------------------------------------------- +12 13 | +13 14 | # airflow.providers.openlineage.utils.utils +14 15 | DatasetInfo() +15 |-translate_airflow_dataset() + 16 |+translate_airflow_asset() +16 17 | +17 18 | # airflow.secrets.local_filesystem +18 19 | load_connections() + +AIR301_provider_names_fix.py:18:1: AIR301 [*] `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0 + | +17 | # airflow.secrets.local_filesystem +18 | load_connections() + | ^^^^^^^^^^^^^^^^ AIR301 +19 | +20 | # airflow.security.permissions + | + = help: Use `load_connections_dict` from `airflow.secrets.local_filesystem` instead. + +ℹ Safe fix +5 5 | DatasetInfo, +6 6 | translate_airflow_dataset, +7 7 | ) +8 |-from airflow.secrets.local_filesystem import load_connections + 8 |+from airflow.secrets.local_filesystem import load_connections, load_connections_dict +9 9 | from airflow.security.permissions import RESOURCE_DATASET +10 10 | +11 11 | AvpEntities.DATASET +-------------------------------------------------------------------------------- +15 15 | translate_airflow_dataset() +16 16 | +17 17 | # airflow.secrets.local_filesystem +18 |-load_connections() + 18 |+load_connections_dict() +19 19 | +20 20 | # airflow.security.permissions +21 21 | RESOURCE_DATASET + +AIR301_provider_names_fix.py:21:1: AIR301 [*] `airflow.security.permissions.RESOURCE_DATASET` is removed in Airflow 3.0 + | +20 | # airflow.security.permissions +21 | RESOURCE_DATASET + | ^^^^^^^^^^^^^^^^ AIR301 +22 | +23 | from airflow.providers.amazon.aws.datasets.s3 import ( + | + = help: Use `RESOURCE_ASSET` from `airflow.security.permissions` instead. + +ℹ Safe fix +6 6 | translate_airflow_dataset, +7 7 | ) +8 8 | from airflow.secrets.local_filesystem import load_connections +9 |-from airflow.security.permissions import RESOURCE_DATASET + 9 |+from airflow.security.permissions import RESOURCE_DATASET, RESOURCE_ASSET +10 10 | +11 11 | AvpEntities.DATASET +12 12 | +-------------------------------------------------------------------------------- +18 18 | load_connections() +19 19 | +20 20 | # airflow.security.permissions +21 |-RESOURCE_DATASET + 21 |+RESOURCE_ASSET +22 22 | +23 23 | from airflow.providers.amazon.aws.datasets.s3 import ( +24 24 | convert_dataset_to_openlineage as s3_convert_dataset_to_openlineage, + +AIR301_provider_names_fix.py:28:1: AIR301 [*] `airflow.providers.amazon.aws.datasets.s3.create_dataset` is removed in Airflow 3.0 + | +26 | from airflow.providers.amazon.aws.datasets.s3 import create_dataset as s3_create_dataset +27 | +28 | s3_create_dataset() + | ^^^^^^^^^^^^^^^^^ AIR301 +29 | s3_convert_dataset_to_openlineage() + | + = help: Use `create_asset` from `airflow.providers.amazon.aws.assets.s3` instead. + +ℹ Safe fix +24 24 | convert_dataset_to_openlineage as s3_convert_dataset_to_openlineage, +25 25 | ) +26 26 | from airflow.providers.amazon.aws.datasets.s3 import create_dataset as s3_create_dataset + 27 |+from airflow.providers.amazon.aws.assets.s3 import create_asset +27 28 | +28 |-s3_create_dataset() + 29 |+create_asset() +29 30 | s3_convert_dataset_to_openlineage() +30 31 | +31 32 | from airflow.providers.common.io.dataset.file import ( + +AIR301_provider_names_fix.py:29:1: AIR301 [*] `airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage` is removed in Airflow 3.0 + | +28 | s3_create_dataset() +29 | s3_convert_dataset_to_openlineage() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +30 | +31 | from airflow.providers.common.io.dataset.file import ( + | + = help: Use `convert_asset_to_openlineage` from `airflow.providers.amazon.aws.assets.s3` instead. + +ℹ Safe fix +24 24 | convert_dataset_to_openlineage as s3_convert_dataset_to_openlineage, +25 25 | ) +26 26 | from airflow.providers.amazon.aws.datasets.s3 import create_dataset as s3_create_dataset + 27 |+from airflow.providers.amazon.aws.assets.s3 import convert_asset_to_openlineage +27 28 | +28 29 | s3_create_dataset() +29 |-s3_convert_dataset_to_openlineage() + 30 |+convert_asset_to_openlineage() +30 31 | +31 32 | from airflow.providers.common.io.dataset.file import ( +32 33 | convert_dataset_to_openlineage as io_convert_dataset_to_openlineage, + +AIR301_provider_names_fix.py:45:1: AIR301 [*] `airflow.providers.google.datasets.bigquery.create_dataset` is removed in Airflow 3.0 + | +43 | ) +44 | +45 | bigquery_create_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +46 | +47 | # airflow.providers.google.datasets.gcs + | + = help: Use `create_asset` from `airflow.providers.google.assets.bigquery` instead. + +ℹ Safe fix +41 41 | from airflow.providers.google.datasets.bigquery import ( +42 42 | create_dataset as bigquery_create_dataset, +43 43 | ) + 44 |+from airflow.providers.google.assets.bigquery import create_asset +44 45 | +45 |-bigquery_create_dataset() + 46 |+create_asset() +46 47 | +47 48 | # airflow.providers.google.datasets.gcs +48 49 | from airflow.providers.google.datasets.gcs import ( + +AIR301_provider_names_fix.py:53:1: AIR301 [*] `airflow.providers.google.datasets.gcs.create_dataset` is removed in Airflow 3.0 + | +51 | from airflow.providers.google.datasets.gcs import create_dataset as gcs_create_dataset +52 | +53 | gcs_create_dataset() + | ^^^^^^^^^^^^^^^^^^ AIR301 +54 | gcs_convert_dataset_to_openlineage() + | + = help: Use `create_asset` from `airflow.providers.google.assets.gcs` instead. + +ℹ Safe fix +49 49 | convert_dataset_to_openlineage as gcs_convert_dataset_to_openlineage, +50 50 | ) +51 51 | from airflow.providers.google.datasets.gcs import create_dataset as gcs_create_dataset + 52 |+from airflow.providers.google.assets.gcs import create_asset +52 53 | +53 |-gcs_create_dataset() + 54 |+create_asset() +54 55 | gcs_convert_dataset_to_openlineage() + +AIR301_provider_names_fix.py:54:1: AIR301 [*] `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage` is removed in Airflow 3.0 + | +53 | gcs_create_dataset() +54 | gcs_convert_dataset_to_openlineage() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `convert_asset_to_openlineage` from `airflow.providers.google.assets.gcs` instead. + +ℹ Safe fix +49 49 | convert_dataset_to_openlineage as gcs_convert_dataset_to_openlineage, +50 50 | ) +51 51 | from airflow.providers.google.datasets.gcs import create_dataset as gcs_create_dataset + 52 |+from airflow.providers.google.assets.gcs import convert_asset_to_openlineage +52 53 | +53 54 | gcs_create_dataset() +54 |-gcs_convert_dataset_to_openlineage() + 55 |+convert_asset_to_openlineage() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap deleted file mode 100644 index 3019d36239f77..0000000000000 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap +++ /dev/null @@ -1,1647 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/airflow/mod.rs ---- -AIR302.py:226:1: AIR302 `airflow.hooks.S3_hook.provide_bucket_name` is moved into `amazon` provider in Airflow 3.0; - | -225 | # apache-airflow-providers-amazon -226 | provide_bucket_name() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -227 | GCSToS3Operator() -228 | GoogleApiToS3Operator() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.provide_bucket_name` instead. - -AIR302.py:227:1: AIR302 `airflow.operators.gcs_to_s3.GCSToS3Operator` is moved into `amazon` provider in Airflow 3.0; - | -225 | # apache-airflow-providers-amazon -226 | provide_bucket_name() -227 | GCSToS3Operator() - | ^^^^^^^^^^^^^^^ AIR302 -228 | GoogleApiToS3Operator() -229 | GoogleApiToS3Transfer() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator` instead. - -AIR302.py:228:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Operator` is moved into `amazon` provider in Airflow 3.0; - | -226 | provide_bucket_name() -227 | GCSToS3Operator() -228 | GoogleApiToS3Operator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -229 | GoogleApiToS3Transfer() -230 | RedshiftToS3Operator() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. - -AIR302.py:229:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Transfer` is moved into `amazon` provider in Airflow 3.0; - | -227 | GCSToS3Operator() -228 | GoogleApiToS3Operator() -229 | GoogleApiToS3Transfer() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -230 | RedshiftToS3Operator() -231 | RedshiftToS3Transfer() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. - -AIR302.py:230:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Operator` is moved into `amazon` provider in Airflow 3.0; - | -228 | GoogleApiToS3Operator() -229 | GoogleApiToS3Transfer() -230 | RedshiftToS3Operator() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -231 | RedshiftToS3Transfer() -232 | S3FileTransformOperator() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. - -AIR302.py:231:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Transfer` is moved into `amazon` provider in Airflow 3.0; - | -229 | GoogleApiToS3Transfer() -230 | RedshiftToS3Operator() -231 | RedshiftToS3Transfer() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -232 | S3FileTransformOperator() -233 | S3Hook() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. - -AIR302.py:232:1: AIR302 `airflow.operators.s3_file_transform_operator.S3FileTransformOperator` is moved into `amazon` provider in Airflow 3.0; - | -230 | RedshiftToS3Operator() -231 | RedshiftToS3Transfer() -232 | S3FileTransformOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -233 | S3Hook() -234 | SSQLTableCheckOperator3KeySensor() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.operators.s3_file_transform.S3FileTransformOperator` instead. - -AIR302.py:233:1: AIR302 `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` provider in Airflow 3.0; - | -231 | RedshiftToS3Transfer() -232 | S3FileTransformOperator() -233 | S3Hook() - | ^^^^^^ AIR302 -234 | SSQLTableCheckOperator3KeySensor() -235 | S3ToRedshiftOperator() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.S3Hook` instead. - -AIR302.py:235:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftOperator` is moved into `amazon` provider in Airflow 3.0; - | -233 | S3Hook() -234 | SSQLTableCheckOperator3KeySensor() -235 | S3ToRedshiftOperator() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -236 | S3ToRedshiftTransfer() - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. - -AIR302.py:236:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftTransfer` is moved into `amazon` provider in Airflow 3.0; - | -234 | SSQLTableCheckOperator3KeySensor() -235 | S3ToRedshiftOperator() -236 | S3ToRedshiftTransfer() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -237 | -238 | # apache-airflow-providers-celery - | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. - -AIR302.py:239:1: AIR302 `airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG` is moved into `celery` provider in Airflow 3.0; - | -238 | # apache-airflow-providers-celery -239 | DEFAULT_CELERY_CONFIG - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -240 | app -241 | CeleryExecutor() - | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG` instead. - -AIR302.py:240:1: AIR302 `airflow.executors.celery_executor.app` is moved into `celery` provider in Airflow 3.0; - | -238 | # apache-airflow-providers-celery -239 | DEFAULT_CELERY_CONFIG -240 | app - | ^^^ AIR302 -241 | CeleryExecutor() -242 | CeleryKubernetesExecutor() - | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor_utils.app` instead. - -AIR302.py:241:1: AIR302 `airflow.executors.celery_executor.CeleryExecutor` is moved into `celery` provider in Airflow 3.0; - | -239 | DEFAULT_CELERY_CONFIG -240 | app -241 | CeleryExecutor() - | ^^^^^^^^^^^^^^ AIR302 -242 | CeleryKubernetesExecutor() - | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor.CeleryExecutor` instead. - -AIR302.py:242:1: AIR302 `airflow.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` is moved into `celery` provider in Airflow 3.0; - | -240 | app -241 | CeleryExecutor() -242 | CeleryKubernetesExecutor() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -243 | -244 | # apache-airflow-providers-common-sql - | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` instead. - -AIR302.py:245:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0; - | -244 | # apache-airflow-providers-common-sql -245 | _convert_to_float_if_possible() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -246 | parse_boolean() -247 | BaseSQLOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql._convert_to_float_if_possible` instead. - -AIR302.py:246:1: AIR302 `airflow.operators.sql.parse_boolean` is moved into `common-sql` provider in Airflow 3.0; - | -244 | # apache-airflow-providers-common-sql -245 | _convert_to_float_if_possible() -246 | parse_boolean() - | ^^^^^^^^^^^^^ AIR302 -247 | BaseSQLOperator() -248 | BashOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.parse_boolean` instead. - -AIR302.py:247:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is moved into `common-sql` provider in Airflow 3.0; - | -245 | _convert_to_float_if_possible() -246 | parse_boolean() -247 | BaseSQLOperator() - | ^^^^^^^^^^^^^^^ AIR302 -248 | BashOperator() -249 | LegacyBashOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead. - -AIR302.py:249:1: AIR302 `airflow.operators.bash_operator.BashOperator` is moved into `standard` provider in Airflow 3.0; - | -247 | BaseSQLOperator() -248 | BashOperator() -249 | LegacyBashOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -250 | BranchSQLOperator() -251 | CheckOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. - -AIR302.py:250:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is moved into `common-sql` provider in Airflow 3.0; - | -248 | BashOperator() -249 | LegacyBashOperator() -250 | BranchSQLOperator() - | ^^^^^^^^^^^^^^^^^ AIR302 -251 | CheckOperator() -252 | ConnectorProtocol() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead. - -AIR302.py:251:1: AIR302 `airflow.operators.check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -249 | LegacyBashOperator() -250 | BranchSQLOperator() -251 | CheckOperator() - | ^^^^^^^^^^^^^ AIR302 -252 | ConnectorProtocol() -253 | DbApiHook() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:252:1: AIR302 `airflow.hooks.dbapi.ConnectorProtocol` is moved into `common-sql` provider in Airflow 3.0; - | -250 | BranchSQLOperator() -251 | CheckOperator() -252 | ConnectorProtocol() - | ^^^^^^^^^^^^^^^^^ AIR302 -253 | DbApiHook() -254 | DbApiHook2() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.ConnectorProtocol` instead. - -AIR302.py:253:1: AIR302 `airflow.hooks.dbapi.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; - | -251 | CheckOperator() -252 | ConnectorProtocol() -253 | DbApiHook() - | ^^^^^^^^^ AIR302 -254 | DbApiHook2() -255 | IntervalCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. - -AIR302.py:254:1: AIR302 `airflow.hooks.dbapi_hook.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; - | -252 | ConnectorProtocol() -253 | DbApiHook() -254 | DbApiHook2() - | ^^^^^^^^^^ AIR302 -255 | IntervalCheckOperator() -256 | PrestoCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. - -AIR302.py:255:1: AIR302 `airflow.operators.check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -253 | DbApiHook() -254 | DbApiHook2() -255 | IntervalCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -256 | PrestoCheckOperator() -257 | PrestoIntervalCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:256:1: AIR302 `airflow.operators.presto_check_operator.PrestoCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -254 | DbApiHook2() -255 | IntervalCheckOperator() -256 | PrestoCheckOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -257 | PrestoIntervalCheckOperator() -258 | PrestoValueCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:257:1: AIR302 `airflow.operators.presto_check_operator.PrestoIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -255 | IntervalCheckOperator() -256 | PrestoCheckOperator() -257 | PrestoIntervalCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -258 | PrestoValueCheckOperator() -259 | SQLCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:258:1: AIR302 `airflow.operators.presto_check_operator.PrestoValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -256 | PrestoCheckOperator() -257 | PrestoIntervalCheckOperator() -258 | PrestoValueCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -259 | SQLCheckOperator() -260 | SQLCheckOperator2() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:259:1: AIR302 `airflow.operators.check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -257 | PrestoIntervalCheckOperator() -258 | PrestoValueCheckOperator() -259 | SQLCheckOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -260 | SQLCheckOperator2() -261 | SQLCheckOperator3() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:260:1: AIR302 `airflow.operators.presto_check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -258 | PrestoValueCheckOperator() -259 | SQLCheckOperator() -260 | SQLCheckOperator2() - | ^^^^^^^^^^^^^^^^^ AIR302 -261 | SQLCheckOperator3() -262 | SQLColumnCheckOperator2() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:261:1: AIR302 `airflow.operators.sql.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -259 | SQLCheckOperator() -260 | SQLCheckOperator2() -261 | SQLCheckOperator3() - | ^^^^^^^^^^^^^^^^^ AIR302 -262 | SQLColumnCheckOperator2() -263 | SQLIntervalCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:262:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -260 | SQLCheckOperator2() -261 | SQLCheckOperator3() -262 | SQLColumnCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -263 | SQLIntervalCheckOperator() -264 | SQLIntervalCheckOperator2() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLColumnCheckOperator` instead. - -AIR302.py:263:1: AIR302 `airflow.operators.check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -261 | SQLCheckOperator3() -262 | SQLColumnCheckOperator2() -263 | SQLIntervalCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -264 | SQLIntervalCheckOperator2() -265 | SQLIntervalCheckOperator3() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:264:1: AIR302 `airflow.operators.presto_check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -262 | SQLColumnCheckOperator2() -263 | SQLIntervalCheckOperator() -264 | SQLIntervalCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -265 | SQLIntervalCheckOperator3() -266 | SQLTableCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:265:1: AIR302 `airflow.operators.sql.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -263 | SQLIntervalCheckOperator() -264 | SQLIntervalCheckOperator2() -265 | SQLIntervalCheckOperator3() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -266 | SQLTableCheckOperator() -267 | SQLThresholdCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:267:1: AIR302 `airflow.operators.check_operator.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -265 | SQLIntervalCheckOperator3() -266 | SQLTableCheckOperator() -267 | SQLThresholdCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -268 | SQLThresholdCheckOperator2() -269 | SQLValueCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. - -AIR302.py:268:1: AIR302 `airflow.operators.sql.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -266 | SQLTableCheckOperator() -267 | SQLThresholdCheckOperator() -268 | SQLThresholdCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -269 | SQLValueCheckOperator() -270 | SQLValueCheckOperator2() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. - -AIR302.py:269:1: AIR302 `airflow.operators.check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -267 | SQLThresholdCheckOperator() -268 | SQLThresholdCheckOperator2() -269 | SQLValueCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -270 | SQLValueCheckOperator2() -271 | SQLValueCheckOperator3() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:270:1: AIR302 `airflow.operators.presto_check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -268 | SQLThresholdCheckOperator2() -269 | SQLValueCheckOperator() -270 | SQLValueCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -271 | SQLValueCheckOperator3() -272 | SqlSensor() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:271:1: AIR302 `airflow.operators.sql.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -269 | SQLValueCheckOperator() -270 | SQLValueCheckOperator2() -271 | SQLValueCheckOperator3() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -272 | SqlSensor() -273 | SqlSensor2() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:272:1: AIR302 `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; - | -270 | SQLValueCheckOperator2() -271 | SQLValueCheckOperator3() -272 | SqlSensor() - | ^^^^^^^^^ AIR302 -273 | SqlSensor2() -274 | ThresholdCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.sensors.sql.SqlSensor` instead. - -AIR302.py:274:1: AIR302 `airflow.operators.check_operator.ThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -272 | SqlSensor() -273 | SqlSensor2() -274 | ThresholdCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -275 | ValueCheckOperator() - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. - -AIR302.py:275:1: AIR302 `airflow.operators.check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -273 | SqlSensor2() -274 | ThresholdCheckOperator() -275 | ValueCheckOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -276 | -277 | # apache-airflow-providers-daskexecutor - | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:278:1: AIR302 `airflow.executors.dask_executor.DaskExecutor` is moved into `daskexecutor` provider in Airflow 3.0; - | -277 | # apache-airflow-providers-daskexecutor -278 | DaskExecutor() - | ^^^^^^^^^^^^ AIR302 -279 | -280 | # apache-airflow-providers-docker - | - = help: Install `apache-airflow-provider-daskexecutor>=1.0.0` and use `airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor` instead. - -AIR302.py:281:1: AIR302 `airflow.hooks.docker_hook.DockerHook` is moved into `docker` provider in Airflow 3.0; - | -280 | # apache-airflow-providers-docker -281 | DockerHook() - | ^^^^^^^^^^ AIR302 -282 | DockerOperator() - | - = help: Install `apache-airflow-provider-docker>=1.0.0` and use `airflow.providers.docker.hooks.docker.DockerHook` instead. - -AIR302.py:282:1: AIR302 `airflow.operators.docker_operator.DockerOperator` is moved into `docker` provider in Airflow 3.0; - | -280 | # apache-airflow-providers-docker -281 | DockerHook() -282 | DockerOperator() - | ^^^^^^^^^^^^^^ AIR302 -283 | -284 | # apache-airflow-providers-apache-druid - | - = help: Install `apache-airflow-provider-docker>=1.0.0` and use `airflow.providers.docker.operators.docker.DockerOperator` instead. - -AIR302.py:285:1: AIR302 `airflow.hooks.druid_hook.DruidDbApiHook` is moved into `apache-druid` provider in Airflow 3.0; - | -284 | # apache-airflow-providers-apache-druid -285 | DruidDbApiHook() - | ^^^^^^^^^^^^^^ AIR302 -286 | DruidHook() -287 | DruidCheckOperator() - | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidDbApiHook` instead. - -AIR302.py:286:1: AIR302 `airflow.hooks.druid_hook.DruidHook` is moved into `apache-druid` provider in Airflow 3.0; - | -284 | # apache-airflow-providers-apache-druid -285 | DruidDbApiHook() -286 | DruidHook() - | ^^^^^^^^^ AIR302 -287 | DruidCheckOperator() - | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidHook` instead. - -AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `apache-druid` provider in Airflow 3.0; - | -285 | DruidDbApiHook() -286 | DruidHook() -287 | DruidCheckOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -288 | -289 | # apache-airflow-providers-apache-hdfs - | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `DruidCheckOperator` instead. - -AIR302.py:290:1: AIR302 `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into `apache-hdfs` provider in Airflow 3.0; - | -289 | # apache-airflow-providers-apache-hdfs -290 | WebHDFSHook() - | ^^^^^^^^^^^ AIR302 -291 | WebHdfsSensor() - | - = help: Install `apache-airflow-provider-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook` instead. - -AIR302.py:291:1: AIR302 `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved into `apache-hdfs` provider in Airflow 3.0; - | -289 | # apache-airflow-providers-apache-hdfs -290 | WebHDFSHook() -291 | WebHdfsSensor() - | ^^^^^^^^^^^^^ AIR302 -292 | -293 | # apache-airflow-providers-apache-hive - | - = help: Install `apache-airflow-provider-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor` instead. - -AIR302.py:294:1: AIR302 `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is moved into `apache-hive` provider in Airflow 3.0; - | -293 | # apache-airflow-providers-apache-hive -294 | HIVE_QUEUE_PRIORITIES - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -295 | closest_ds_partition() -296 | max_partition() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES` instead. - -AIR302.py:295:1: AIR302 `airflow.macros.hive.closest_ds_partition` is moved into `apache-hive` provider in Airflow 3.0; - | -293 | # apache-airflow-providers-apache-hive -294 | HIVE_QUEUE_PRIORITIES -295 | closest_ds_partition() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -296 | max_partition() -297 | HiveCliHook() - | - = help: Install `apache-airflow-provider-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.closest_ds_partition` instead. - -AIR302.py:296:1: AIR302 `airflow.macros.hive.max_partition` is moved into `apache-hive` provider in Airflow 3.0; - | -294 | HIVE_QUEUE_PRIORITIES -295 | closest_ds_partition() -296 | max_partition() - | ^^^^^^^^^^^^^ AIR302 -297 | HiveCliHook() -298 | HiveMetastoreHook() - | - = help: Install `apache-airflow-provider-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.max_partition` instead. - -AIR302.py:297:1: AIR302 `airflow.hooks.hive_hooks.HiveCliHook` is moved into `apache-hive` provider in Airflow 3.0; - | -295 | closest_ds_partition() -296 | max_partition() -297 | HiveCliHook() - | ^^^^^^^^^^^ AIR302 -298 | HiveMetastoreHook() -299 | HiveOperator() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveCliHook` instead. - -AIR302.py:298:1: AIR302 `airflow.hooks.hive_hooks.HiveMetastoreHook` is moved into `apache-hive` provider in Airflow 3.0; - | -296 | max_partition() -297 | HiveCliHook() -298 | HiveMetastoreHook() - | ^^^^^^^^^^^^^^^^^ AIR302 -299 | HiveOperator() -300 | HivePartitionSensor() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveMetastoreHook` instead. - -AIR302.py:299:1: AIR302 `airflow.operators.hive_operator.HiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -297 | HiveCliHook() -298 | HiveMetastoreHook() -299 | HiveOperator() - | ^^^^^^^^^^^^ AIR302 -300 | HivePartitionSensor() -301 | HiveServer2Hook() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive.HiveOperator` instead. - -AIR302.py:300:1: AIR302 `airflow.sensors.hive_partition_sensor.HivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; - | -298 | HiveMetastoreHook() -299 | HiveOperator() -300 | HivePartitionSensor() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -301 | HiveServer2Hook() -302 | HiveStatsCollectionOperator() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor` instead. - -AIR302.py:301:1: AIR302 `airflow.hooks.hive_hooks.HiveServer2Hook` is moved into `apache-hive` provider in Airflow 3.0; - | -299 | HiveOperator() -300 | HivePartitionSensor() -301 | HiveServer2Hook() - | ^^^^^^^^^^^^^^^ AIR302 -302 | HiveStatsCollectionOperator() -303 | HiveToDruidOperator() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveServer2Hook` instead. - -AIR302.py:302:1: AIR302 `airflow.operators.hive_stats_operator.HiveStatsCollectionOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -300 | HivePartitionSensor() -301 | HiveServer2Hook() -302 | HiveStatsCollectionOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -303 | HiveToDruidOperator() -304 | HiveToDruidTransfer() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator` instead. - -AIR302.py:303:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidOperator` is moved into `apache-druid` provider in Airflow 3.0; - | -301 | HiveServer2Hook() -302 | HiveStatsCollectionOperator() -303 | HiveToDruidOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -304 | HiveToDruidTransfer() -305 | HiveToSambaOperator() - | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. - -AIR302.py:304:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidTransfer` is moved into `apache-druid` provider in Airflow 3.0; - | -302 | HiveStatsCollectionOperator() -303 | HiveToDruidOperator() -304 | HiveToDruidTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -305 | HiveToSambaOperator() -306 | S3ToHiveOperator() - | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. - -AIR302.py:305:1: AIR302 `airflow.operators.hive_to_samba_operator.HiveToSambaOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -303 | HiveToDruidOperator() -304 | HiveToDruidTransfer() -305 | HiveToSambaOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -306 | S3ToHiveOperator() -307 | S3ToHiveTransfer() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `HiveToSambaOperator` instead. - -AIR302.py:306:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -304 | HiveToDruidTransfer() -305 | HiveToSambaOperator() -306 | S3ToHiveOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -307 | S3ToHiveTransfer() -308 | MetastorePartitionSensor() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. - -AIR302.py:307:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -305 | HiveToSambaOperator() -306 | S3ToHiveOperator() -307 | S3ToHiveTransfer() - | ^^^^^^^^^^^^^^^^ AIR302 -308 | MetastorePartitionSensor() -309 | NamedHivePartitionSensor() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. - -AIR302.py:308:1: AIR302 `airflow.sensors.metastore_partition_sensor.MetastorePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; - | -306 | S3ToHiveOperator() -307 | S3ToHiveTransfer() -308 | MetastorePartitionSensor() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -309 | NamedHivePartitionSensor() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor` instead. - -AIR302.py:309:1: AIR302 `airflow.sensors.named_hive_partition_sensor.NamedHivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; - | -307 | S3ToHiveTransfer() -308 | MetastorePartitionSensor() -309 | NamedHivePartitionSensor() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -310 | -311 | # apache-airflow-providers-http - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor` instead. - -AIR302.py:312:1: AIR302 `airflow.hooks.http_hook.HttpHook` is moved into `http` provider in Airflow 3.0; - | -311 | # apache-airflow-providers-http -312 | HttpHook() - | ^^^^^^^^ AIR302 -313 | HttpSensor() -314 | SimpleHttpOperator() - | - = help: Install `apache-airflow-provider-http>=1.0.0` and use `airflow.providers.http.hooks.http.HttpHook` instead. - -AIR302.py:313:1: AIR302 `airflow.sensors.http_sensor.HttpSensor` is moved into `http` provider in Airflow 3.0; - | -311 | # apache-airflow-providers-http -312 | HttpHook() -313 | HttpSensor() - | ^^^^^^^^^^ AIR302 -314 | SimpleHttpOperator() - | - = help: Install `apache-airflow-provider-http>=1.0.0` and use `airflow.providers.http.sensors.http.HttpSensor` instead. - -AIR302.py:314:1: AIR302 `airflow.operators.http_operator.SimpleHttpOperator` is moved into `http` provider in Airflow 3.0; - | -312 | HttpHook() -313 | HttpSensor() -314 | SimpleHttpOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -315 | -316 | # apache-airflow-providers-jdbc - | - = help: Install `apache-airflow-provider-http>=1.0.0` and use `airflow.providers.http.operators.http.SimpleHttpOperator` instead. - -AIR302.py:317:1: AIR302 `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in Airflow 3.0; - | -316 | # apache-airflow-providers-jdbc -317 | jaydebeapi - | ^^^^^^^^^^ AIR302 -318 | JdbcHook() -319 | JdbcOperator() - | - = help: Install `apache-airflow-provider-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.jaydebeapi` instead. - -AIR302.py:318:1: AIR302 `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` provider in Airflow 3.0; - | -316 | # apache-airflow-providers-jdbc -317 | jaydebeapi -318 | JdbcHook() - | ^^^^^^^^ AIR302 -319 | JdbcOperator() - | - = help: Install `apache-airflow-provider-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.JdbcHook` instead. - -AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved into `jdbc` provider in Airflow 3.0; - | -317 | jaydebeapi -318 | JdbcHook() -319 | JdbcOperator() - | ^^^^^^^^^^^^ AIR302 -320 | -321 | # apache-airflow-providers-fab - | - = help: Install `apache-airflow-provider-jdbc>=1.0.0` and use `airflow.providers.jdbc.operators.jdbc.JdbcOperator` instead. - -AIR302.py:322:12: AIR302 `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; - | -321 | # apache-airflow-providers-fab -322 | basic_auth.CLIENT_AUTH - | ^^^^^^^^^^^ AIR302 -323 | basic_auth.init_app -324 | basic_auth.auth_current_user - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.CLIENT_AUTH` instead. - -AIR302.py:323:12: AIR302 `airflow.api.auth.backend.basic_auth.init_app` is moved into `fab` provider in Airflow 3.0; - | -321 | # apache-airflow-providers-fab -322 | basic_auth.CLIENT_AUTH -323 | basic_auth.init_app - | ^^^^^^^^ AIR302 -324 | basic_auth.auth_current_user -325 | basic_auth.requires_authentication - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.init_app` instead. - -AIR302.py:324:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; - | -322 | basic_auth.CLIENT_AUTH -323 | basic_auth.init_app -324 | basic_auth.auth_current_user - | ^^^^^^^^^^^^^^^^^ AIR302 -325 | basic_auth.requires_authentication - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. - -AIR302.py:325:12: AIR302 `airflow.api.auth.backend.basic_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; - | -323 | basic_auth.init_app -324 | basic_auth.auth_current_user -325 | basic_auth.requires_authentication - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -326 | -327 | kerberos_auth.log - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.requires_authentication` instead. - -AIR302.py:327:15: AIR302 `airflow.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; - | -325 | basic_auth.requires_authentication -326 | -327 | kerberos_auth.log - | ^^^ AIR302 -328 | kerberos_auth.CLIENT_AUTH -329 | kerberos_auth.find_user - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.log` instead. - -AIR302.py:328:15: AIR302 `airflow.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; - | -327 | kerberos_auth.log -328 | kerberos_auth.CLIENT_AUTH - | ^^^^^^^^^^^ AIR302 -329 | kerberos_auth.find_user -330 | kerberos_auth.init_app - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.CLIENT_AUTH` instead. - -AIR302.py:329:15: AIR302 `airflow.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; - | -327 | kerberos_auth.log -328 | kerberos_auth.CLIENT_AUTH -329 | kerberos_auth.find_user - | ^^^^^^^^^ AIR302 -330 | kerberos_auth.init_app -331 | kerberos_auth.requires_authentication - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.find_user` instead. - -AIR302.py:330:15: AIR302 `airflow.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; - | -328 | kerberos_auth.CLIENT_AUTH -329 | kerberos_auth.find_user -330 | kerberos_auth.init_app - | ^^^^^^^^ AIR302 -331 | kerberos_auth.requires_authentication -332 | auth_current_user - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.init_app` instead. - -AIR302.py:331:15: AIR302 `airflow.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; - | -329 | kerberos_auth.find_user -330 | kerberos_auth.init_app -331 | kerberos_auth.requires_authentication - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -332 | auth_current_user -333 | backend_kerberos_auth - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.requires_authentication` instead. - -AIR302.py:332:1: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; - | -330 | kerberos_auth.init_app -331 | kerberos_auth.requires_authentication -332 | auth_current_user - | ^^^^^^^^^^^^^^^^^ AIR302 -333 | backend_kerberos_auth -334 | fab_override - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. - -AIR302.py:335:1: AIR302 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0; - | -333 | backend_kerberos_auth -334 | fab_override -335 | FabAuthManager() - | ^^^^^^^^^^^^^^ AIR302 -336 | FabAirflowSecurityManagerOverride() - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.FabAuthManager` instead. - -AIR302.py:336:1: AIR302 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; - | -334 | fab_override -335 | FabAuthManager() -336 | FabAirflowSecurityManagerOverride() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -337 | -338 | # check whether attribute access - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead. - -AIR302.py:339:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; - | -338 | # check whether attribute access -339 | basic_auth.auth_current_user - | ^^^^^^^^^^^^^^^^^ AIR302 -340 | -341 | # apache-airflow-providers-cncf-kubernetes - | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. - -AIR302.py:342:1: AIR302 `airflow.executors.kubernetes_executor_types.ALL_NAMESPACES` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -341 | # apache-airflow-providers-cncf-kubernetes -342 | ALL_NAMESPACES - | ^^^^^^^^^^^^^^ AIR302 -343 | POD_EXECUTOR_DONE_KEY -344 | _disable_verify_ssl() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES` instead. - -AIR302.py:343:1: AIR302 `airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -341 | # apache-airflow-providers-cncf-kubernetes -342 | ALL_NAMESPACES -343 | POD_EXECUTOR_DONE_KEY - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -344 | _disable_verify_ssl() -345 | _enable_tcp_keepalive() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` instead. - -AIR302.py:344:1: AIR302 `airflow.kubernetes.kube_client._disable_verify_ssl` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -342 | ALL_NAMESPACES -343 | POD_EXECUTOR_DONE_KEY -344 | _disable_verify_ssl() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -345 | _enable_tcp_keepalive() -346 | append_to_pod() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. - -AIR302.py:345:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -343 | POD_EXECUTOR_DONE_KEY -344 | _disable_verify_ssl() -345 | _enable_tcp_keepalive() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -346 | append_to_pod() -347 | annotations_for_logging_task_metadata() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. - -AIR302.py:346:1: AIR302 `airflow.kubernetes.k8s_model.append_to_pod` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -344 | _disable_verify_ssl() -345 | _enable_tcp_keepalive() -346 | append_to_pod() - | ^^^^^^^^^^^^^ AIR302 -347 | annotations_for_logging_task_metadata() -348 | annotations_to_key() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.append_to_pod` instead. - -AIR302.py:347:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -345 | _enable_tcp_keepalive() -346 | append_to_pod() -347 | annotations_for_logging_task_metadata() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -348 | annotations_to_key() -349 | create_pod_id() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` instead. - -AIR302.py:348:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_to_key` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -346 | append_to_pod() -347 | annotations_for_logging_task_metadata() -348 | annotations_to_key() - | ^^^^^^^^^^^^^^^^^^ AIR302 -349 | create_pod_id() -350 | datetime_to_label_safe_datestring() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_to_key` instead. - -AIR302.py:349:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -347 | annotations_for_logging_task_metadata() -348 | annotations_to_key() -349 | create_pod_id() - | ^^^^^^^^^^^^^ AIR302 -350 | datetime_to_label_safe_datestring() -351 | extend_object_field() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_pod_id` instead. - -AIR302.py:350:1: AIR302 `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -348 | annotations_to_key() -349 | create_pod_id() -350 | datetime_to_label_safe_datestring() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -351 | extend_object_field() -352 | get_logs_task_metadata() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.datetime_to_label_safe_datestring` instead. - -AIR302.py:351:1: AIR302 `airflow.kubernetes.pod_generator.extend_object_field` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -349 | create_pod_id() -350 | datetime_to_label_safe_datestring() -351 | extend_object_field() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -352 | get_logs_task_metadata() -353 | label_safe_datestring_to_datetime() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.extend_object_field` instead. - -AIR302.py:352:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -350 | datetime_to_label_safe_datestring() -351 | extend_object_field() -352 | get_logs_task_metadata() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -353 | label_safe_datestring_to_datetime() -354 | merge_objects() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` instead. - -AIR302.py:353:1: AIR302 `airflow.kubernetes.pod_generator.label_safe_datestring_to_datetime` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -351 | extend_object_field() -352 | get_logs_task_metadata() -353 | label_safe_datestring_to_datetime() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -354 | merge_objects() -355 | Port() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.label_safe_datestring_to_datetime` instead. - -AIR302.py:354:1: AIR302 `airflow.kubernetes.pod_generator.merge_objects` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -352 | get_logs_task_metadata() -353 | label_safe_datestring_to_datetime() -354 | merge_objects() - | ^^^^^^^^^^^^^ AIR302 -355 | Port() -356 | Resources() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.merge_objects` instead. - -AIR302.py:355:1: AIR302 `airflow.kubernetes.pod.Port` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -353 | label_safe_datestring_to_datetime() -354 | merge_objects() -355 | Port() - | ^^^^ AIR302 -356 | Resources() -357 | PodRuntimeInfoEnv() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ContainerPort` instead. - -AIR302.py:356:1: AIR302 `airflow.kubernetes.pod.Resources` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -354 | merge_objects() -355 | Port() -356 | Resources() - | ^^^^^^^^^ AIR302 -357 | PodRuntimeInfoEnv() -358 | PodGeneratorDeprecated() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ResourceRequirements` instead. - -AIR302.py:357:1: AIR302 `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoEnv` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -355 | Port() -356 | Resources() -357 | PodRuntimeInfoEnv() - | ^^^^^^^^^^^^^^^^^ AIR302 -358 | PodGeneratorDeprecated() -359 | Volume() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1EnvVar` instead. - -AIR302.py:358:1: AIR302 `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -356 | Resources() -357 | PodRuntimeInfoEnv() -358 | PodGeneratorDeprecated() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -359 | Volume() -360 | VolumeMount() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. - -AIR302.py:359:1: AIR302 `airflow.kubernetes.volume.Volume` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -357 | PodRuntimeInfoEnv() -358 | PodGeneratorDeprecated() -359 | Volume() - | ^^^^^^ AIR302 -360 | VolumeMount() -361 | Secret() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1Volume` instead. - -AIR302.py:360:1: AIR302 `airflow.kubernetes.volume_mount.VolumeMount` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -358 | PodGeneratorDeprecated() -359 | Volume() -360 | VolumeMount() - | ^^^^^^^^^^^ AIR302 -361 | Secret() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1VolumeMount` instead. - -AIR302.py:361:1: AIR302 `airflow.kubernetes.secret.Secret` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -359 | Volume() -360 | VolumeMount() -361 | Secret() - | ^^^^^^ AIR302 -362 | -363 | add_pod_suffix() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.secret.Secret` instead. - -AIR302.py:363:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -361 | Secret() -362 | -363 | add_pod_suffix() - | ^^^^^^^^^^^^^^ AIR302 -364 | add_pod_suffix2() -365 | get_kube_client() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. - -AIR302.py:364:1: AIR302 `airflow.kubernetes.pod_generator.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -363 | add_pod_suffix() -364 | add_pod_suffix2() - | ^^^^^^^^^^^^^^^ AIR302 -365 | get_kube_client() -366 | get_kube_client2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. - -AIR302.py:365:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -363 | add_pod_suffix() -364 | add_pod_suffix2() -365 | get_kube_client() - | ^^^^^^^^^^^^^^^ AIR302 -366 | get_kube_client2() -367 | make_safe_label_value() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. - -AIR302.py:366:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -364 | add_pod_suffix2() -365 | get_kube_client() -366 | get_kube_client2() - | ^^^^^^^^^^^^^^^^ AIR302 -367 | make_safe_label_value() -368 | make_safe_label_value2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. - -AIR302.py:367:1: AIR302 `airflow.kubernetes.pod_generator.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -365 | get_kube_client() -366 | get_kube_client2() -367 | make_safe_label_value() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -368 | make_safe_label_value2() -369 | rand_str() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. - -AIR302.py:368:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -366 | get_kube_client2() -367 | make_safe_label_value() -368 | make_safe_label_value2() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -369 | rand_str() -370 | rand_str2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.make_safe_label_value` instead. - -AIR302.py:369:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -367 | make_safe_label_value() -368 | make_safe_label_value2() -369 | rand_str() - | ^^^^^^^^ AIR302 -370 | rand_str2() -371 | K8SModel() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. - -AIR302.py:370:1: AIR302 `airflow.kubernetes.pod_generator.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -368 | make_safe_label_value2() -369 | rand_str() -370 | rand_str2() - | ^^^^^^^^^ AIR302 -371 | K8SModel() -372 | K8SModel2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. - -AIR302.py:371:1: AIR302 `airflow.kubernetes.k8s_model.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -369 | rand_str() -370 | rand_str2() -371 | K8SModel() - | ^^^^^^^^ AIR302 -372 | K8SModel2() -373 | PodLauncher() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.K8SModel` instead. - -AIR302.py:373:1: AIR302 `airflow.kubernetes.pod_launcher.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -371 | K8SModel() -372 | K8SModel2() -373 | PodLauncher() - | ^^^^^^^^^^^ AIR302 -374 | PodLauncher2() -375 | PodStatus() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. - -AIR302.py:374:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -372 | K8SModel2() -373 | PodLauncher() -374 | PodLauncher2() - | ^^^^^^^^^^^^ AIR302 -375 | PodStatus() -376 | PodStatus2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. - -AIR302.py:375:1: AIR302 `airflow.kubernetes.pod_launcher.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -373 | PodLauncher() -374 | PodLauncher2() -375 | PodStatus() - | ^^^^^^^^^ AIR302 -376 | PodStatus2() -377 | PodDefaults() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. - -AIR302.py:376:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -374 | PodLauncher2() -375 | PodStatus() -376 | PodStatus2() - | ^^^^^^^^^^ AIR302 -377 | PodDefaults() -378 | PodDefaults2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. - -AIR302.py:377:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -375 | PodStatus() -376 | PodStatus2() -377 | PodDefaults() - | ^^^^^^^^^^^ AIR302 -378 | PodDefaults2() -379 | PodDefaults3() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodDefaults` instead. - -AIR302.py:378:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -376 | PodStatus2() -377 | PodDefaults() -378 | PodDefaults2() - | ^^^^^^^^^^^^ AIR302 -379 | PodDefaults3() -380 | PodGenerator() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodDefaults` instead. - -AIR302.py:379:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -377 | PodDefaults() -378 | PodDefaults2() -379 | PodDefaults3() - | ^^^^^^^^^^^^ AIR302 -380 | PodGenerator() -381 | PodGenerator2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults` instead. - -AIR302.py:380:1: AIR302 `airflow.kubernetes.pod_generator.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -378 | PodDefaults2() -379 | PodDefaults3() -380 | PodGenerator() - | ^^^^^^^^^^^^ AIR302 -381 | PodGenerator2() - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. - -AIR302.py:381:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -379 | PodDefaults3() -380 | PodGenerator() -381 | PodGenerator2() - | ^^^^^^^^^^^^^ AIR302 - | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodGenerator` instead. - -AIR302.py:385:1: AIR302 `airflow.hooks.mssql_hook.MsSqlHook` is moved into `microsoft-mssql` provider in Airflow 3.0; - | -384 | # apache-airflow-providers-microsoft-mssql -385 | MsSqlHook() - | ^^^^^^^^^ AIR302 -386 | MsSqlOperator() -387 | MsSqlToHiveOperator() - | - = help: Install `apache-airflow-provider-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook` instead. - -AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is moved into `microsoft-mssql` provider in Airflow 3.0; - | -384 | # apache-airflow-providers-microsoft-mssql -385 | MsSqlHook() -386 | MsSqlOperator() - | ^^^^^^^^^^^^^ AIR302 -387 | MsSqlToHiveOperator() -388 | MsSqlToHiveTransfer() - | - = help: Install `apache-airflow-provider-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.operators.mssql.MsSqlOperator` instead. - -AIR302.py:387:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -385 | MsSqlHook() -386 | MsSqlOperator() -387 | MsSqlToHiveOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -388 | MsSqlToHiveTransfer() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. - -AIR302.py:388:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -386 | MsSqlOperator() -387 | MsSqlToHiveOperator() -388 | MsSqlToHiveTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -389 | -390 | # apache-airflow-providers-mysql - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. - -AIR302.py:391:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -390 | # apache-airflow-providers-mysql -391 | HiveToMySqlOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -392 | HiveToMySqlTransfer() -393 | MySqlHook() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. - -AIR302.py:392:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -390 | # apache-airflow-providers-mysql -391 | HiveToMySqlOperator() -392 | HiveToMySqlTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -393 | MySqlHook() -394 | MySqlOperator() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. - -AIR302.py:393:1: AIR302 `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysql` provider in Airflow 3.0; - | -391 | HiveToMySqlOperator() -392 | HiveToMySqlTransfer() -393 | MySqlHook() - | ^^^^^^^^^ AIR302 -394 | MySqlOperator() -395 | MySqlToHiveOperator() - | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.hooks.mysql.MySqlHook` instead. - -AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is moved into `mysql` provider in Airflow 3.0; - | -392 | HiveToMySqlTransfer() -393 | MySqlHook() -394 | MySqlOperator() - | ^^^^^^^^^^^^^ AIR302 -395 | MySqlToHiveOperator() -396 | MySqlToHiveTransfer() - | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.operators.mysql.MySqlOperator` instead. - -AIR302.py:395:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -393 | MySqlHook() -394 | MySqlOperator() -395 | MySqlToHiveOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -396 | MySqlToHiveTransfer() -397 | PrestoToMySqlOperator() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. - -AIR302.py:396:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -394 | MySqlOperator() -395 | MySqlToHiveOperator() -396 | MySqlToHiveTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -397 | PrestoToMySqlOperator() -398 | PrestoToMySqlTransfer() - | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. - -AIR302.py:397:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlOperator` is moved into `mysql` provider in Airflow 3.0; - | -395 | MySqlToHiveOperator() -396 | MySqlToHiveTransfer() -397 | PrestoToMySqlOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -398 | PrestoToMySqlTransfer() - | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. - -AIR302.py:398:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer` is moved into `mysql` provider in Airflow 3.0; - | -396 | MySqlToHiveTransfer() -397 | PrestoToMySqlOperator() -398 | PrestoToMySqlTransfer() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -399 | -400 | # apache-airflow-providers-oracle - | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. - -AIR302.py:401:1: AIR302 `airflow.hooks.oracle_hook.OracleHook` is moved into `oracle` provider in Airflow 3.0; - | -400 | # apache-airflow-providers-oracle -401 | OracleHook() - | ^^^^^^^^^^ AIR302 -402 | OracleOperator() - | - = help: Install `apache-airflow-provider-oracle>=1.0.0` and use `airflow.providers.oracle.hooks.oracle.OracleHook` instead. - -AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is moved into `oracle` provider in Airflow 3.0; - | -400 | # apache-airflow-providers-oracle -401 | OracleHook() -402 | OracleOperator() - | ^^^^^^^^^^^^^^ AIR302 -403 | -404 | # apache-airflow-providers-papermill - | - = help: Install `apache-airflow-provider-oracle>=1.0.0` and use `airflow.providers.oracle.operators.oracle.OracleOperator` instead. - -AIR302.py:405:1: AIR302 `airflow.operators.papermill_operator.PapermillOperator` is moved into `papermill` provider in Airflow 3.0; - | -404 | # apache-airflow-providers-papermill -405 | PapermillOperator() - | ^^^^^^^^^^^^^^^^^ AIR302 -406 | -407 | # apache-airflow-providers-apache-pig - | - = help: Install `apache-airflow-provider-papermill>=1.0.0` and use `airflow.providers.papermill.operators.papermill.PapermillOperator` instead. - -AIR302.py:408:1: AIR302 `airflow.hooks.pig_hook.PigCliHook` is moved into `apache-pig` provider in Airflow 3.0; - | -407 | # apache-airflow-providers-apache-pig -408 | PigCliHook() - | ^^^^^^^^^^ AIR302 -409 | PigOperator() - | - = help: Install `apache-airflow-provider-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.hooks.pig.PigCliHook` instead. - -AIR302.py:409:1: AIR302 `airflow.operators.pig_operator.PigOperator` is moved into `apache-pig` provider in Airflow 3.0; - | -407 | # apache-airflow-providers-apache-pig -408 | PigCliHook() -409 | PigOperator() - | ^^^^^^^^^^^ AIR302 -410 | -411 | # apache-airflow-providers-postgres - | - = help: Install `apache-airflow-provider-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.operators.pig.PigOperator` instead. - -AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is moved into `postgres` provider in Airflow 3.0; - | -411 | # apache-airflow-providers-postgres -412 | Mapping - | ^^^^^^^ AIR302 -413 | PostgresHook() -414 | PostgresOperator() - | - = help: Install `apache-airflow-provider-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.Mapping` instead. - -AIR302.py:413:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into `postgres` provider in Airflow 3.0; - | -411 | # apache-airflow-providers-postgres -412 | Mapping -413 | PostgresHook() - | ^^^^^^^^^^^^ AIR302 -414 | PostgresOperator() - | - = help: Install `apache-airflow-provider-postgres>=1.0.0` and use `airflow.providers.postgres.hooks.postgres.PostgresHook` instead. - -AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` is moved into `postgres` provider in Airflow 3.0; - | -412 | Mapping -413 | PostgresHook() -414 | PostgresOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -415 | -416 | # apache-airflow-providers-presto - | - = help: Install `apache-airflow-provider-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.PostgresOperator` instead. - -AIR302.py:417:1: AIR302 `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provider in Airflow 3.0; - | -416 | # apache-airflow-providers-presto -417 | PrestoHook() - | ^^^^^^^^^^ AIR302 -418 | -419 | # apache-airflow-providers-samba - | - = help: Install `apache-airflow-provider-presto>=1.0.0` and use `airflow.providers.presto.hooks.presto.PrestoHook` instead. - -AIR302.py:420:1: AIR302 `airflow.hooks.samba_hook.SambaHook` is moved into `samba` provider in Airflow 3.0; - | -419 | # apache-airflow-providers-samba -420 | SambaHook() - | ^^^^^^^^^ AIR302 -421 | -422 | # apache-airflow-providers-slack - | - = help: Install `apache-airflow-provider-samba>=1.0.0` and use `airflow.providers.samba.hooks.samba.SambaHook` instead. - -AIR302.py:423:1: AIR302 `airflow.hooks.slack_hook.SlackHook` is moved into `slack` provider in Airflow 3.0; - | -422 | # apache-airflow-providers-slack -423 | SlackHook() - | ^^^^^^^^^ AIR302 -424 | SlackAPIOperator() -425 | SlackAPIPostOperator() - | - = help: Install `apache-airflow-provider-slack>=1.0.0` and use `airflow.providers.slack.hooks.slack.SlackHook` instead. - -AIR302.py:424:1: AIR302 `airflow.operators.slack_operator.SlackAPIOperator` is moved into `slack` provider in Airflow 3.0; - | -422 | # apache-airflow-providers-slack -423 | SlackHook() -424 | SlackAPIOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -425 | SlackAPIPostOperator() - | - = help: Install `apache-airflow-provider-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIOperator` instead. - -AIR302.py:425:1: AIR302 `airflow.operators.slack_operator.SlackAPIPostOperator` is moved into `slack` provider in Airflow 3.0; - | -423 | SlackHook() -424 | SlackAPIOperator() -425 | SlackAPIPostOperator() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -426 | -427 | # apache-airflow-providers-sqlite - | - = help: Install `apache-airflow-provider-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIPostOperator` instead. - -AIR302.py:428:1: AIR302 `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sqlite` provider in Airflow 3.0; - | -427 | # apache-airflow-providers-sqlite -428 | SqliteHook() - | ^^^^^^^^^^ AIR302 -429 | SqliteOperator() - | - = help: Install `apache-airflow-provider-sqlite>=1.0.0` and use `airflow.providers.sqlite.hooks.sqlite.SqliteHook` instead. - -AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is moved into `sqlite` provider in Airflow 3.0; - | -427 | # apache-airflow-providers-sqlite -428 | SqliteHook() -429 | SqliteOperator() - | ^^^^^^^^^^^^^^ AIR302 -430 | -431 | # apache-airflow-providers-zendesk - | - = help: Install `apache-airflow-provider-sqlite>=1.0.0` and use `airflow.providers.sqlite.operators.sqlite.SqliteOperator` instead. - -AIR302.py:432:1: AIR302 `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` provider in Airflow 3.0; - | -431 | # apache-airflow-providers-zendesk -432 | ZendeskHook() - | ^^^^^^^^^^^ AIR302 -433 | -434 | # apache-airflow-providers-smtp - | - = help: Install `apache-airflow-provider-zendesk>=1.0.0` and use `airflow.providers.zendesk.hooks.zendesk.ZendeskHook` instead. - -AIR302.py:435:1: AIR302 `airflow.operators.email_operator.EmailOperator` is moved into `smtp` provider in Airflow 3.0; - | -434 | # apache-airflow-providers-smtp -435 | EmailOperator() - | ^^^^^^^^^^^^^ AIR302 -436 | -437 | # apache-airflow-providers-standard - | - = help: Install `apache-airflow-provider-smtp>=1.0.0` and use `airflow.providers.smtp.operators.smtp.EmailOperator` instead. - -AIR302.py:451:1: AIR302 `airflow.operators.dummy.DummyOperator` is moved into `standard` provider in Airflow 3.0; - | -449 | TimeDeltaSensor() -450 | DayOfWeekSensor() -451 | DummyOperator() - | ^^^^^^^^^^^^^ AIR302 -452 | EmptyOperator() -453 | ExternalTaskMarker() - | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. - -AIR302.py:452:1: AIR302 `airflow.operators.dummy.EmptyOperator` is moved into `standard` provider in Airflow 3.0; - | -450 | DayOfWeekSensor() -451 | DummyOperator() -452 | EmptyOperator() - | ^^^^^^^^^^^^^ AIR302 -453 | ExternalTaskMarker() -454 | ExternalTaskSensor() - | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap new file mode 100644 index 0000000000000..398278fd91bea --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap @@ -0,0 +1,252 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_amazon.py:14:1: AIR302 [*] `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` provider in Airflow 3.0; + | +12 | from airflow.sensors.s3_key_sensor import S3KeySensor +13 | +14 | S3Hook() + | ^^^^^^ AIR302 +15 | provide_bucket_name() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `S3Hook` from `airflow.providers.amazon.aws.hooks.s3` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.S3_hook import ( +4 |- S3Hook, +5 4 | provide_bucket_name, +6 5 | ) +7 6 | from airflow.operators.gcs_to_s3 import GCSToS3Operator +-------------------------------------------------------------------------------- +10 9 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 10 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.hooks.s3 import S3Hook +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:15:1: AIR302 [*] `airflow.hooks.S3_hook.provide_bucket_name` is moved into `amazon` provider in Airflow 3.0; + | +14 | S3Hook() +15 | provide_bucket_name() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +16 | +17 | GCSToS3Operator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `provide_bucket_name` from `airflow.providers.amazon.aws.hooks.s3` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.S3_hook import ( +4 4 | S3Hook, +5 |- provide_bucket_name, +6 5 | ) +7 6 | from airflow.operators.gcs_to_s3 import GCSToS3Operator +8 7 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +-------------------------------------------------------------------------------- +10 9 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 10 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.hooks.s3 import provide_bucket_name +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:17:1: AIR302 [*] `airflow.operators.gcs_to_s3.GCSToS3Operator` is moved into `amazon` provider in Airflow 3.0; + | +15 | provide_bucket_name() +16 | +17 | GCSToS3Operator() + | ^^^^^^^^^^^^^^^ AIR302 +18 | GoogleApiToS3Operator() +19 | RedshiftToS3Operator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `GCSToS3Operator` from `airflow.providers.amazon.aws.transfers.gcs_to_s3` instead. + +ℹ Unsafe fix +4 4 | S3Hook, +5 5 | provide_bucket_name, +6 6 | ) +7 |-from airflow.operators.gcs_to_s3 import GCSToS3Operator +8 7 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +9 8 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +10 9 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 10 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.transfers.gcs_to_s3 import GCSToS3Operator +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:18:1: AIR302 [*] `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Operator` is moved into `amazon` provider in Airflow 3.0; + | +17 | GCSToS3Operator() +18 | GoogleApiToS3Operator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +19 | RedshiftToS3Operator() +20 | S3FileTransformOperator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `GoogleApiToS3Operator` from `airflow.providers.amazon.aws.transfers.google_api_to_s3` instead. + +ℹ Unsafe fix +5 5 | provide_bucket_name, +6 6 | ) +7 7 | from airflow.operators.gcs_to_s3 import GCSToS3Operator +8 |-from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +9 8 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +10 9 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 10 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.transfers.google_api_to_s3 import GoogleApiToS3Operator +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:19:1: AIR302 [*] `airflow.operators.redshift_to_s3_operator.RedshiftToS3Operator` is moved into `amazon` provider in Airflow 3.0; + | +17 | GCSToS3Operator() +18 | GoogleApiToS3Operator() +19 | RedshiftToS3Operator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +20 | S3FileTransformOperator() +21 | S3ToRedshiftOperator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `RedshiftToS3Operator` from `airflow.providers.amazon.aws.transfers.redshift_to_s3` instead. + +ℹ Unsafe fix +6 6 | ) +7 7 | from airflow.operators.gcs_to_s3 import GCSToS3Operator +8 8 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +9 |-from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +10 9 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 10 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.transfers.redshift_to_s3 import RedshiftToS3Operator +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:20:1: AIR302 [*] `airflow.operators.s3_file_transform_operator.S3FileTransformOperator` is moved into `amazon` provider in Airflow 3.0; + | +18 | GoogleApiToS3Operator() +19 | RedshiftToS3Operator() +20 | S3FileTransformOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +21 | S3ToRedshiftOperator() +22 | S3KeySensor() + | + = help: Install `apache-airflow-providers-amazon>=3.0.0` and use `S3FileTransformOperator` from `airflow.providers.amazon.aws.operators.s3` instead. + +ℹ Unsafe fix +7 7 | from airflow.operators.gcs_to_s3 import GCSToS3Operator +8 8 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +9 9 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +10 |-from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 10 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.operators.s3 import S3FileTransformOperator +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:21:1: AIR302 [*] `airflow.operators.s3_to_redshift_operator.S3ToRedshiftOperator` is moved into `amazon` provider in Airflow 3.0; + | +19 | RedshiftToS3Operator() +20 | S3FileTransformOperator() +21 | S3ToRedshiftOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +22 | S3KeySensor() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `S3ToRedshiftOperator` from `airflow.providers.amazon.aws.transfers.s3_to_redshift` instead. + +ℹ Unsafe fix +8 8 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Operator +9 9 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +10 10 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 |-from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 11 | from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.transfers.s3_to_redshift import S3ToRedshiftOperator +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:22:1: AIR302 [*] `airflow.sensors.s3_key_sensor.S3KeySensor` is moved into `amazon` provider in Airflow 3.0; + | +20 | S3FileTransformOperator() +21 | S3ToRedshiftOperator() +22 | S3KeySensor() + | ^^^^^^^^^^^ AIR302 +23 | +24 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Transfer + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `S3KeySensor` from `airflow.providers.amazon.aws.sensors.s3` instead. + +ℹ Unsafe fix +9 9 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Operator +10 10 | from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +11 11 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftOperator +12 |-from airflow.sensors.s3_key_sensor import S3KeySensor + 12 |+from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor +13 13 | +14 14 | S3Hook() +15 15 | provide_bucket_name() + +AIR302_amazon.py:26:1: AIR302 [*] `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Transfer` is moved into `amazon` provider in Airflow 3.0; + | +24 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Transfer +25 | +26 | GoogleApiToS3Transfer() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +27 | +28 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Transfer + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `GoogleApiToS3Operator` from `airflow.providers.amazon.aws.transfers.google_api_to_s3` instead. + +ℹ Unsafe fix +22 22 | S3KeySensor() +23 23 | +24 24 | from airflow.operators.google_api_to_s3_transfer import GoogleApiToS3Transfer + 25 |+from airflow.providers.amazon.aws.transfers.google_api_to_s3 import GoogleApiToS3Operator +25 26 | +26 27 | GoogleApiToS3Transfer() +27 28 | + +AIR302_amazon.py:30:1: AIR302 [*] `airflow.operators.redshift_to_s3_operator.RedshiftToS3Transfer` is moved into `amazon` provider in Airflow 3.0; + | +28 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Transfer +29 | +30 | RedshiftToS3Transfer() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +31 | +32 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftTransfer + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `RedshiftToS3Operator` from `airflow.providers.amazon.aws.transfers.redshift_to_s3` instead. + +ℹ Unsafe fix +26 26 | GoogleApiToS3Transfer() +27 27 | +28 28 | from airflow.operators.redshift_to_s3_operator import RedshiftToS3Transfer + 29 |+from airflow.providers.amazon.aws.transfers.redshift_to_s3 import RedshiftToS3Operator +29 30 | +30 31 | RedshiftToS3Transfer() +31 32 | + +AIR302_amazon.py:34:1: AIR302 [*] `airflow.operators.s3_to_redshift_operator.S3ToRedshiftTransfer` is moved into `amazon` provider in Airflow 3.0; + | +32 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftTransfer +33 | +34 | S3ToRedshiftTransfer() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `S3ToRedshiftOperator` from `airflow.providers.amazon.aws.transfers.s3_to_redshift` instead. + +ℹ Unsafe fix +30 30 | RedshiftToS3Transfer() +31 31 | +32 32 | from airflow.operators.s3_to_redshift_operator import S3ToRedshiftTransfer + 33 |+from airflow.providers.amazon.aws.transfers.s3_to_redshift import S3ToRedshiftOperator +33 34 | +34 35 | S3ToRedshiftTransfer() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap new file mode 100644 index 0000000000000..991c2bcad8118 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap @@ -0,0 +1,67 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_celery.py:9:1: AIR302 [*] `airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG` is moved into `celery` provider in Airflow 3.0; + | + 7 | ) + 8 | + 9 | DEFAULT_CELERY_CONFIG + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +10 | +11 | app + | + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `DEFAULT_CELERY_CONFIG` from `airflow.providers.celery.executors.default_celery` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG +4 3 | from airflow.executors.celery_executor import ( +5 4 | CeleryExecutor, +6 5 | app, +7 6 | ) + 7 |+from airflow.providers.celery.executors.default_celery import DEFAULT_CELERY_CONFIG +8 8 | +9 9 | DEFAULT_CELERY_CONFIG +10 10 | + +AIR302_celery.py:11:1: AIR302 [*] `airflow.executors.celery_executor.app` is moved into `celery` provider in Airflow 3.0; + | + 9 | DEFAULT_CELERY_CONFIG +10 | +11 | app + | ^^^ AIR302 +12 | CeleryExecutor() + | + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `app` from `airflow.providers.celery.executors.celery_executor_utils` instead. + +ℹ Unsafe fix +3 3 | from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG +4 4 | from airflow.executors.celery_executor import ( +5 5 | CeleryExecutor, +6 |- app, +7 6 | ) + 7 |+from airflow.providers.celery.executors.celery_executor_utils import app +8 8 | +9 9 | DEFAULT_CELERY_CONFIG +10 10 | + +AIR302_celery.py:12:1: AIR302 [*] `airflow.executors.celery_executor.CeleryExecutor` is moved into `celery` provider in Airflow 3.0; + | +11 | app +12 | CeleryExecutor() + | ^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `CeleryExecutor` from `airflow.providers.celery.executors.celery_executor` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG +4 4 | from airflow.executors.celery_executor import ( +5 |- CeleryExecutor, +6 5 | app, +7 6 | ) + 7 |+from airflow.providers.celery.executors.celery_executor import CeleryExecutor +8 8 | +9 9 | DEFAULT_CELERY_CONFIG +10 10 | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap new file mode 100644 index 0000000000000..22798b4011578 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap @@ -0,0 +1,639 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_common_sql.py:8:1: AIR302 [*] `airflow.hooks.dbapi.ConnectorProtocol` is moved into `common-sql` provider in Airflow 3.0; + | +6 | ) +7 | +8 | ConnectorProtocol() + | ^^^^^^^^^^^^^^^^^ AIR302 +9 | DbApiHook() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `ConnectorProtocol` from `airflow.providers.common.sql.hooks.sql` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.dbapi import ( +4 |- ConnectorProtocol, +5 4 | DbApiHook, +6 5 | ) + 6 |+from airflow.providers.common.sql.hooks.sql import ConnectorProtocol +7 7 | +8 8 | ConnectorProtocol() +9 9 | DbApiHook() + +AIR302_common_sql.py:9:1: AIR302 [*] `airflow.hooks.dbapi.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; + | + 8 | ConnectorProtocol() + 9 | DbApiHook() + | ^^^^^^^^^ AIR302 +10 | +11 | from airflow.hooks.dbapi_hook import DbApiHook + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `DbApiHook` from `airflow.providers.common.sql.hooks.sql` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.dbapi import ( +4 4 | ConnectorProtocol, +5 |- DbApiHook, +6 5 | ) + 6 |+from airflow.providers.common.sql.hooks.sql import DbApiHook +7 7 | +8 8 | ConnectorProtocol() +9 9 | DbApiHook() + +AIR302_common_sql.py:14:1: AIR302 [*] `airflow.hooks.dbapi_hook.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; + | +12 | from airflow.operators.check_operator import SQLCheckOperator +13 | +14 | DbApiHook() + | ^^^^^^^^^ AIR302 +15 | SQLCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `DbApiHook` from `airflow.providers.common.sql.hooks.sql` instead. + +ℹ Unsafe fix +8 8 | ConnectorProtocol() +9 9 | DbApiHook() +10 10 | +11 |-from airflow.hooks.dbapi_hook import DbApiHook +12 11 | from airflow.operators.check_operator import SQLCheckOperator + 12 |+from airflow.providers.common.sql.hooks.sql import DbApiHook +13 13 | +14 14 | DbApiHook() +15 15 | SQLCheckOperator() + +AIR302_common_sql.py:15:1: AIR302 [*] `airflow.operators.check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +14 | DbApiHook() +15 | SQLCheckOperator() + | ^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +9 9 | DbApiHook() +10 10 | +11 11 | from airflow.hooks.dbapi_hook import DbApiHook +12 |-from airflow.operators.check_operator import SQLCheckOperator + 12 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +13 13 | +14 14 | DbApiHook() +15 15 | SQLCheckOperator() + +AIR302_common_sql.py:21:1: AIR302 [*] `airflow.operators.sql.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +19 | from airflow.operators.sql import SQLCheckOperator +20 | +21 | SQLCheckOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +22 | CheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +16 16 | +17 17 | +18 18 | from airflow.operators.check_operator import CheckOperator +19 |-from airflow.operators.sql import SQLCheckOperator + 19 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +20 20 | +21 21 | SQLCheckOperator() +22 22 | CheckOperator() + +AIR302_common_sql.py:22:1: AIR302 [*] `airflow.operators.check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +21 | SQLCheckOperator() +22 | CheckOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +17 17 | +18 18 | from airflow.operators.check_operator import CheckOperator +19 19 | from airflow.operators.sql import SQLCheckOperator + 20 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +20 21 | +21 22 | SQLCheckOperator() +22 23 | CheckOperator() + +AIR302_common_sql.py:27:1: AIR302 [*] `airflow.operators.druid_check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +25 | from airflow.operators.druid_check_operator import CheckOperator +26 | +27 | CheckOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +23 23 | +24 24 | +25 25 | from airflow.operators.druid_check_operator import CheckOperator + 26 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +26 27 | +27 28 | CheckOperator() +28 29 | + +AIR302_common_sql.py:32:1: AIR302 [*] `airflow.operators.presto_check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +30 | from airflow.operators.presto_check_operator import CheckOperator +31 | +32 | CheckOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +28 28 | +29 29 | +30 30 | from airflow.operators.presto_check_operator import CheckOperator + 31 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +31 32 | +32 33 | CheckOperator() +33 34 | + +AIR302_common_sql.py:42:1: AIR302 [*] `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +40 | from airflow.operators.presto_check_operator import PrestoCheckOperator +41 | +42 | DruidCheckOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +43 | PrestoCheckOperator() +44 | IntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +38 38 | ) +39 39 | from airflow.operators.druid_check_operator import DruidCheckOperator +40 40 | from airflow.operators.presto_check_operator import PrestoCheckOperator + 41 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +41 42 | +42 43 | DruidCheckOperator() +43 44 | PrestoCheckOperator() + +AIR302_common_sql.py:43:1: AIR302 [*] `airflow.operators.presto_check_operator.PrestoCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +42 | DruidCheckOperator() +43 | PrestoCheckOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +44 | IntervalCheckOperator() +45 | SQLIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +38 38 | ) +39 39 | from airflow.operators.druid_check_operator import DruidCheckOperator +40 40 | from airflow.operators.presto_check_operator import PrestoCheckOperator + 41 |+from airflow.providers.common.sql.operators.sql import SQLCheckOperator +41 42 | +42 43 | DruidCheckOperator() +43 44 | PrestoCheckOperator() + +AIR302_common_sql.py:44:1: AIR302 [*] `airflow.operators.check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +42 | DruidCheckOperator() +43 | PrestoCheckOperator() +44 | IntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +45 | SQLIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLIntervalCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +34 34 | +35 35 | from airflow.operators.check_operator import ( +36 36 | IntervalCheckOperator, +37 |- SQLIntervalCheckOperator, +38 37 | ) +39 38 | from airflow.operators.druid_check_operator import DruidCheckOperator +40 39 | from airflow.operators.presto_check_operator import PrestoCheckOperator + 40 |+from airflow.providers.common.sql.operators.sql import SQLIntervalCheckOperator +41 41 | +42 42 | DruidCheckOperator() +43 43 | PrestoCheckOperator() + +AIR302_common_sql.py:45:1: AIR302 [*] `airflow.operators.check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +43 | PrestoCheckOperator() +44 | IntervalCheckOperator() +45 | SQLIntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLIntervalCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +34 34 | +35 35 | from airflow.operators.check_operator import ( +36 36 | IntervalCheckOperator, +37 |- SQLIntervalCheckOperator, +38 37 | ) +39 38 | from airflow.operators.druid_check_operator import DruidCheckOperator +40 39 | from airflow.operators.presto_check_operator import PrestoCheckOperator + 40 |+from airflow.providers.common.sql.operators.sql import SQLIntervalCheckOperator +41 41 | +42 42 | DruidCheckOperator() +43 43 | PrestoCheckOperator() + +AIR302_common_sql.py:54:1: AIR302 [*] `airflow.operators.presto_check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +52 | from airflow.operators.sql import SQLIntervalCheckOperator +53 | +54 | IntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +55 | SQLIntervalCheckOperator() +56 | PrestoIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLIntervalCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +50 50 | PrestoIntervalCheckOperator, +51 51 | ) +52 52 | from airflow.operators.sql import SQLIntervalCheckOperator + 53 |+from airflow.providers.common.sql.operators.sql import SQLIntervalCheckOperator +53 54 | +54 55 | IntervalCheckOperator() +55 56 | SQLIntervalCheckOperator() + +AIR302_common_sql.py:55:1: AIR302 [*] `airflow.operators.sql.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +54 | IntervalCheckOperator() +55 | SQLIntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +56 | PrestoIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLIntervalCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +49 49 | IntervalCheckOperator, +50 50 | PrestoIntervalCheckOperator, +51 51 | ) +52 |-from airflow.operators.sql import SQLIntervalCheckOperator + 52 |+from airflow.providers.common.sql.operators.sql import SQLIntervalCheckOperator +53 53 | +54 54 | IntervalCheckOperator() +55 55 | SQLIntervalCheckOperator() + +AIR302_common_sql.py:56:1: AIR302 [*] `airflow.operators.presto_check_operator.PrestoIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +54 | IntervalCheckOperator() +55 | SQLIntervalCheckOperator() +56 | PrestoIntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLIntervalCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +50 50 | PrestoIntervalCheckOperator, +51 51 | ) +52 52 | from airflow.operators.sql import SQLIntervalCheckOperator + 53 |+from airflow.providers.common.sql.operators.sql import SQLIntervalCheckOperator +53 54 | +54 55 | IntervalCheckOperator() +55 56 | SQLIntervalCheckOperator() + +AIR302_common_sql.py:64:1: AIR302 [*] `airflow.operators.check_operator.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +62 | ) +63 | +64 | SQLThresholdCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +65 | ThresholdCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLThresholdCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +57 57 | +58 58 | +59 59 | from airflow.operators.check_operator import ( +60 |- SQLThresholdCheckOperator, +61 60 | ThresholdCheckOperator, +62 61 | ) + 62 |+from airflow.providers.common.sql.operators.sql import SQLThresholdCheckOperator +63 63 | +64 64 | SQLThresholdCheckOperator() +65 65 | ThresholdCheckOperator() + +AIR302_common_sql.py:65:1: AIR302 [*] `airflow.operators.check_operator.ThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +64 | SQLThresholdCheckOperator() +65 | ThresholdCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLThresholdCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +57 57 | +58 58 | +59 59 | from airflow.operators.check_operator import ( +60 |- SQLThresholdCheckOperator, +61 60 | ThresholdCheckOperator, +62 61 | ) + 62 |+from airflow.providers.common.sql.operators.sql import SQLThresholdCheckOperator +63 63 | +64 64 | SQLThresholdCheckOperator() +65 65 | ThresholdCheckOperator() + +AIR302_common_sql.py:70:1: AIR302 [*] `airflow.operators.sql.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +68 | from airflow.operators.sql import SQLThresholdCheckOperator +69 | +70 | SQLThresholdCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLThresholdCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +65 65 | ThresholdCheckOperator() +66 66 | +67 67 | +68 |-from airflow.operators.sql import SQLThresholdCheckOperator + 68 |+from airflow.providers.common.sql.operators.sql import SQLThresholdCheckOperator +69 69 | +70 70 | SQLThresholdCheckOperator() +71 71 | + +AIR302_common_sql.py:78:1: AIR302 [*] `airflow.operators.check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +76 | ) +77 | +78 | SQLValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +79 | ValueCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLValueCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +71 71 | +72 72 | +73 73 | from airflow.operators.check_operator import ( +74 |- SQLValueCheckOperator, +75 74 | ValueCheckOperator, +76 75 | ) + 76 |+from airflow.providers.common.sql.operators.sql import SQLValueCheckOperator +77 77 | +78 78 | SQLValueCheckOperator() +79 79 | ValueCheckOperator() + +AIR302_common_sql.py:79:1: AIR302 [*] `airflow.operators.check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +78 | SQLValueCheckOperator() +79 | ValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLValueCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +71 71 | +72 72 | +73 73 | from airflow.operators.check_operator import ( +74 |- SQLValueCheckOperator, +75 74 | ValueCheckOperator, +76 75 | ) + 76 |+from airflow.providers.common.sql.operators.sql import SQLValueCheckOperator +77 77 | +78 78 | SQLValueCheckOperator() +79 79 | ValueCheckOperator() + +AIR302_common_sql.py:88:1: AIR302 [*] `airflow.operators.sql.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +86 | from airflow.operators.sql import SQLValueCheckOperator +87 | +88 | SQLValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +89 | ValueCheckOperator() +90 | PrestoValueCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLValueCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +83 83 | PrestoValueCheckOperator, +84 84 | ValueCheckOperator, +85 85 | ) +86 |-from airflow.operators.sql import SQLValueCheckOperator + 86 |+from airflow.providers.common.sql.operators.sql import SQLValueCheckOperator +87 87 | +88 88 | SQLValueCheckOperator() +89 89 | ValueCheckOperator() + +AIR302_common_sql.py:89:1: AIR302 [*] `airflow.operators.presto_check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +88 | SQLValueCheckOperator() +89 | ValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +90 | PrestoValueCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLValueCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +84 84 | ValueCheckOperator, +85 85 | ) +86 86 | from airflow.operators.sql import SQLValueCheckOperator + 87 |+from airflow.providers.common.sql.operators.sql import SQLValueCheckOperator +87 88 | +88 89 | SQLValueCheckOperator() +89 90 | ValueCheckOperator() + +AIR302_common_sql.py:90:1: AIR302 [*] `airflow.operators.presto_check_operator.PrestoValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +88 | SQLValueCheckOperator() +89 | ValueCheckOperator() +90 | PrestoValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLValueCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +84 84 | ValueCheckOperator, +85 85 | ) +86 86 | from airflow.operators.sql import SQLValueCheckOperator + 87 |+from airflow.providers.common.sql.operators.sql import SQLValueCheckOperator +87 88 | +88 89 | SQLValueCheckOperator() +89 90 | ValueCheckOperator() + +AIR302_common_sql.py:102:1: AIR302 [*] `airflow.operators.sql.BaseSQLOperator` is moved into `common-sql` provider in Airflow 3.0; + | +100 | ) +101 | +102 | BaseSQLOperator() + | ^^^^^^^^^^^^^^^ AIR302 +103 | BranchSQLOperator() +104 | SQLTableCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `BaseSQLOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +91 91 | +92 92 | +93 93 | from airflow.operators.sql import ( +94 |- BaseSQLOperator, +95 94 | BranchSQLOperator, +96 95 | SQLColumnCheckOperator, +97 96 | SQLTableCheckOperator, +98 97 | _convert_to_float_if_possible, +99 98 | parse_boolean, +100 99 | ) + 100 |+from airflow.providers.common.sql.operators.sql import BaseSQLOperator +101 101 | +102 102 | BaseSQLOperator() +103 103 | BranchSQLOperator() + +AIR302_common_sql.py:103:1: AIR302 [*] `airflow.operators.sql.BranchSQLOperator` is moved into `common-sql` provider in Airflow 3.0; + | +102 | BaseSQLOperator() +103 | BranchSQLOperator() + | ^^^^^^^^^^^^^^^^^ AIR302 +104 | SQLTableCheckOperator() +105 | SQLColumnCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `BranchSQLOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +92 92 | +93 93 | from airflow.operators.sql import ( +94 94 | BaseSQLOperator, +95 |- BranchSQLOperator, +96 95 | SQLColumnCheckOperator, +97 96 | SQLTableCheckOperator, +98 97 | _convert_to_float_if_possible, +99 98 | parse_boolean, +100 99 | ) + 100 |+from airflow.providers.common.sql.operators.sql import BranchSQLOperator +101 101 | +102 102 | BaseSQLOperator() +103 103 | BranchSQLOperator() + +AIR302_common_sql.py:104:1: AIR302 [*] `airflow.operators.sql.SQLTableCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +102 | BaseSQLOperator() +103 | BranchSQLOperator() +104 | SQLTableCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +105 | SQLColumnCheckOperator() +106 | _convert_to_float_if_possible() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `SQLTableCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +94 94 | BaseSQLOperator, +95 95 | BranchSQLOperator, +96 96 | SQLColumnCheckOperator, +97 |- SQLTableCheckOperator, +98 97 | _convert_to_float_if_possible, +99 98 | parse_boolean, +100 99 | ) + 100 |+from airflow.providers.common.sql.operators.sql import SQLTableCheckOperator +101 101 | +102 102 | BaseSQLOperator() +103 103 | BranchSQLOperator() + +AIR302_common_sql.py:105:1: AIR302 [*] `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +103 | BranchSQLOperator() +104 | SQLTableCheckOperator() +105 | SQLColumnCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +106 | _convert_to_float_if_possible() +107 | parse_boolean() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `SQLColumnCheckOperator` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +93 93 | from airflow.operators.sql import ( +94 94 | BaseSQLOperator, +95 95 | BranchSQLOperator, +96 |- SQLColumnCheckOperator, +97 96 | SQLTableCheckOperator, +98 97 | _convert_to_float_if_possible, +99 98 | parse_boolean, +100 99 | ) + 100 |+from airflow.providers.common.sql.operators.sql import SQLColumnCheckOperator +101 101 | +102 102 | BaseSQLOperator() +103 103 | BranchSQLOperator() + +AIR302_common_sql.py:106:1: AIR302 [*] `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0; + | +104 | SQLTableCheckOperator() +105 | SQLColumnCheckOperator() +106 | _convert_to_float_if_possible() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +107 | parse_boolean() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `_convert_to_float_if_possible` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +95 95 | BranchSQLOperator, +96 96 | SQLColumnCheckOperator, +97 97 | SQLTableCheckOperator, +98 |- _convert_to_float_if_possible, +99 98 | parse_boolean, +100 99 | ) + 100 |+from airflow.providers.common.sql.operators.sql import _convert_to_float_if_possible +101 101 | +102 102 | BaseSQLOperator() +103 103 | BranchSQLOperator() + +AIR302_common_sql.py:107:1: AIR302 [*] `airflow.operators.sql.parse_boolean` is moved into `common-sql` provider in Airflow 3.0; + | +105 | SQLColumnCheckOperator() +106 | _convert_to_float_if_possible() +107 | parse_boolean() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `parse_boolean` from `airflow.providers.common.sql.operators.sql` instead. + +ℹ Unsafe fix +96 96 | SQLColumnCheckOperator, +97 97 | SQLTableCheckOperator, +98 98 | _convert_to_float_if_possible, +99 |- parse_boolean, +100 99 | ) + 100 |+from airflow.providers.common.sql.operators.sql import parse_boolean +101 101 | +102 102 | BaseSQLOperator() +103 103 | BranchSQLOperator() + +AIR302_common_sql.py:112:1: AIR302 [*] `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; + | +110 | from airflow.sensors.sql import SqlSensor +111 | +112 | SqlSensor() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `SqlSensor` from `airflow.providers.common.sql.sensors.sql` instead. + +ℹ Unsafe fix +107 107 | parse_boolean() +108 108 | +109 109 | +110 |-from airflow.sensors.sql import SqlSensor + 110 |+from airflow.providers.common.sql.sensors.sql import SqlSensor +111 111 | +112 112 | SqlSensor() +113 113 | + +AIR302_common_sql.py:117:1: AIR302 [*] `airflow.sensors.sql_sensor.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; + | +115 | from airflow.sensors.sql_sensor import SqlSensor +116 | +117 | SqlSensor() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `SqlSensor` from `airflow.providers.common.sql.sensors.sql` instead. + +ℹ Unsafe fix +112 112 | SqlSensor() +113 113 | +114 114 | +115 |-from airflow.sensors.sql_sensor import SqlSensor + 115 |+from airflow.providers.common.sql.sensors.sql import SqlSensor +116 116 | +117 117 | SqlSensor() +118 118 | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap new file mode 100644 index 0000000000000..5015a44a45113 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_daskexecutor.py:5:1: AIR302 [*] `airflow.executors.dask_executor.DaskExecutor` is moved into `daskexecutor` provider in Airflow 3.0; + | +3 | from airflow.executors.dask_executor import DaskExecutor +4 | +5 | DaskExecutor() + | ^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-daskexecutor>=1.0.0` and use `DaskExecutor` from `airflow.providers.daskexecutor.executors.dask_executor` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.executors.dask_executor import DaskExecutor + 3 |+from airflow.providers.daskexecutor.executors.dask_executor import DaskExecutor +4 4 | +5 5 | DaskExecutor() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap new file mode 100644 index 0000000000000..9dc5ba8ef9f5f --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap @@ -0,0 +1,95 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_druid.py:12:1: AIR302 [*] `airflow.hooks.druid_hook.DruidDbApiHook` is moved into `apache-druid` provider in Airflow 3.0; + | +10 | ) +11 | +12 | DruidDbApiHook() + | ^^^^^^^^^^^^^^ AIR302 +13 | DruidHook() + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `DruidDbApiHook` from `airflow.providers.apache.druid.hooks.druid` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.druid_hook import ( +4 |- DruidDbApiHook, +5 4 | DruidHook, +6 5 | ) +7 6 | from airflow.operators.hive_to_druid import ( +8 7 | HiveToDruidOperator, +9 8 | HiveToDruidTransfer, +10 9 | ) + 10 |+from airflow.providers.apache.druid.hooks.druid import DruidDbApiHook +11 11 | +12 12 | DruidDbApiHook() +13 13 | DruidHook() + +AIR302_druid.py:13:1: AIR302 [*] `airflow.hooks.druid_hook.DruidHook` is moved into `apache-druid` provider in Airflow 3.0; + | +12 | DruidDbApiHook() +13 | DruidHook() + | ^^^^^^^^^ AIR302 +14 | +15 | HiveToDruidOperator() + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `DruidHook` from `airflow.providers.apache.druid.hooks.druid` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.druid_hook import ( +4 4 | DruidDbApiHook, +5 |- DruidHook, +6 5 | ) +7 6 | from airflow.operators.hive_to_druid import ( +8 7 | HiveToDruidOperator, +9 8 | HiveToDruidTransfer, +10 9 | ) + 10 |+from airflow.providers.apache.druid.hooks.druid import DruidHook +11 11 | +12 12 | DruidDbApiHook() +13 13 | DruidHook() + +AIR302_druid.py:15:1: AIR302 [*] `airflow.operators.hive_to_druid.HiveToDruidOperator` is moved into `apache-druid` provider in Airflow 3.0; + | +13 | DruidHook() +14 | +15 | HiveToDruidOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +16 | HiveToDruidTransfer() + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `HiveToDruidOperator` from `airflow.providers.apache.druid.transfers.hive_to_druid` instead. + +ℹ Unsafe fix +5 5 | DruidHook, +6 6 | ) +7 7 | from airflow.operators.hive_to_druid import ( +8 |- HiveToDruidOperator, +9 8 | HiveToDruidTransfer, +10 9 | ) + 10 |+from airflow.providers.apache.druid.transfers.hive_to_druid import HiveToDruidOperator +11 11 | +12 12 | DruidDbApiHook() +13 13 | DruidHook() + +AIR302_druid.py:16:1: AIR302 [*] `airflow.operators.hive_to_druid.HiveToDruidTransfer` is moved into `apache-druid` provider in Airflow 3.0; + | +15 | HiveToDruidOperator() +16 | HiveToDruidTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `HiveToDruidOperator` from `airflow.providers.apache.druid.transfers.hive_to_druid` instead. + +ℹ Unsafe fix +5 5 | DruidHook, +6 6 | ) +7 7 | from airflow.operators.hive_to_druid import ( +8 |- HiveToDruidOperator, +9 8 | HiveToDruidTransfer, +10 9 | ) + 10 |+from airflow.providers.apache.druid.transfers.hive_to_druid import HiveToDruidOperator +11 11 | +12 12 | DruidDbApiHook() +13 13 | DruidHook() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap new file mode 100644 index 0000000000000..1bd1da740f6d9 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap @@ -0,0 +1,416 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_fab.py:10:1: AIR302 [*] `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; + | + 8 | ) + 9 | +10 | CLIENT_AUTH + | ^^^^^^^^^^^ AIR302 +11 | init_app() +12 | auth_current_user() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `CLIENT_AUTH` from `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.api.auth.backend.basic_auth import ( +4 |- CLIENT_AUTH, +5 4 | auth_current_user, +6 5 | init_app, +7 6 | requires_authentication, +8 7 | ) + 8 |+from airflow.providers.fab.auth_manager.api.auth.backend.basic_auth import CLIENT_AUTH +9 9 | +10 10 | CLIENT_AUTH +11 11 | init_app() + +AIR302_fab.py:11:1: AIR302 [*] `airflow.api.auth.backend.basic_auth.init_app` is moved into `fab` provider in Airflow 3.0; + | +10 | CLIENT_AUTH +11 | init_app() + | ^^^^^^^^ AIR302 +12 | auth_current_user() +13 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `init_app` from `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth` instead. + +ℹ Unsafe fix +3 3 | from airflow.api.auth.backend.basic_auth import ( +4 4 | CLIENT_AUTH, +5 5 | auth_current_user, +6 |- init_app, +7 6 | requires_authentication, +8 7 | ) + 8 |+from airflow.providers.fab.auth_manager.api.auth.backend.basic_auth import init_app +9 9 | +10 10 | CLIENT_AUTH +11 11 | init_app() + +AIR302_fab.py:12:1: AIR302 [*] `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; + | +10 | CLIENT_AUTH +11 | init_app() +12 | auth_current_user() + | ^^^^^^^^^^^^^^^^^ AIR302 +13 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `auth_current_user` from `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.api.auth.backend.basic_auth import ( +4 4 | CLIENT_AUTH, +5 |- auth_current_user, +6 5 | init_app, +7 6 | requires_authentication, +8 7 | ) + 8 |+from airflow.providers.fab.auth_manager.api.auth.backend.basic_auth import auth_current_user +9 9 | +10 10 | CLIENT_AUTH +11 11 | init_app() + +AIR302_fab.py:13:1: AIR302 [*] `airflow.api.auth.backend.basic_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; + | +11 | init_app() +12 | auth_current_user() +13 | requires_authentication() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +14 | +15 | from airflow.api.auth.backend.kerberos_auth import ( + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `requires_authentication` from `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth` instead. + +ℹ Unsafe fix +4 4 | CLIENT_AUTH, +5 5 | auth_current_user, +6 6 | init_app, +7 |- requires_authentication, +8 7 | ) + 8 |+from airflow.providers.fab.auth_manager.api.auth.backend.basic_auth import requires_authentication +9 9 | +10 10 | CLIENT_AUTH +11 11 | init_app() + +AIR302_fab.py:23:1: AIR302 [*] `airflow.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; + | +21 | ) +22 | +23 | log() + | ^^^ AIR302 +24 | CLIENT_AUTH +25 | find_user() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `log` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +16 16 | CLIENT_AUTH, +17 17 | find_user, +18 18 | init_app, +19 |- log, +20 19 | requires_authentication, +21 20 | ) + 21 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import log +22 22 | +23 23 | log() +24 24 | CLIENT_AUTH + +AIR302_fab.py:24:1: AIR302 [*] `airflow.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; + | +23 | log() +24 | CLIENT_AUTH + | ^^^^^^^^^^^ AIR302 +25 | find_user() +26 | init_app() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `CLIENT_AUTH` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +13 13 | requires_authentication() +14 14 | +15 15 | from airflow.api.auth.backend.kerberos_auth import ( +16 |- CLIENT_AUTH, +17 16 | find_user, +18 17 | init_app, +19 18 | log, +20 19 | requires_authentication, +21 20 | ) + 21 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import CLIENT_AUTH +22 22 | +23 23 | log() +24 24 | CLIENT_AUTH + +AIR302_fab.py:25:1: AIR302 [*] `airflow.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; + | +23 | log() +24 | CLIENT_AUTH +25 | find_user() + | ^^^^^^^^^ AIR302 +26 | init_app() +27 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `find_user` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +14 14 | +15 15 | from airflow.api.auth.backend.kerberos_auth import ( +16 16 | CLIENT_AUTH, +17 |- find_user, +18 17 | init_app, +19 18 | log, +20 19 | requires_authentication, +21 20 | ) + 21 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import find_user +22 22 | +23 23 | log() +24 24 | CLIENT_AUTH + +AIR302_fab.py:26:1: AIR302 [*] `airflow.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; + | +24 | CLIENT_AUTH +25 | find_user() +26 | init_app() + | ^^^^^^^^ AIR302 +27 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `init_app` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +15 15 | from airflow.api.auth.backend.kerberos_auth import ( +16 16 | CLIENT_AUTH, +17 17 | find_user, +18 |- init_app, +19 18 | log, +20 19 | requires_authentication, +21 20 | ) + 21 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import init_app +22 22 | +23 23 | log() +24 24 | CLIENT_AUTH + +AIR302_fab.py:27:1: AIR302 [*] `airflow.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; + | +25 | find_user() +26 | init_app() +27 | requires_authentication() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +28 | +29 | from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `requires_authentication` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +17 17 | find_user, +18 18 | init_app, +19 19 | log, +20 |- requires_authentication, +21 20 | ) + 21 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import requires_authentication +22 22 | +23 23 | log() +24 24 | CLIENT_AUTH + +AIR302_fab.py:37:1: AIR302 [*] `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; + | +35 | ) +36 | +37 | log() + | ^^^ AIR302 +38 | CLIENT_AUTH +39 | find_user() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `log` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +30 30 | CLIENT_AUTH, +31 31 | find_user, +32 32 | init_app, +33 |- log, +34 33 | requires_authentication, +35 34 | ) + 35 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import log +36 36 | +37 37 | log() +38 38 | CLIENT_AUTH + +AIR302_fab.py:38:1: AIR302 [*] `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; + | +37 | log() +38 | CLIENT_AUTH + | ^^^^^^^^^^^ AIR302 +39 | find_user() +40 | init_app() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `CLIENT_AUTH` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +27 27 | requires_authentication() +28 28 | +29 29 | from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( +30 |- CLIENT_AUTH, +31 30 | find_user, +32 31 | init_app, +33 32 | log, +34 33 | requires_authentication, +35 34 | ) + 35 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import CLIENT_AUTH +36 36 | +37 37 | log() +38 38 | CLIENT_AUTH + +AIR302_fab.py:39:1: AIR302 [*] `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; + | +37 | log() +38 | CLIENT_AUTH +39 | find_user() + | ^^^^^^^^^ AIR302 +40 | init_app() +41 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `find_user` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +28 28 | +29 29 | from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( +30 30 | CLIENT_AUTH, +31 |- find_user, +32 31 | init_app, +33 32 | log, +34 33 | requires_authentication, +35 34 | ) + 35 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import find_user +36 36 | +37 37 | log() +38 38 | CLIENT_AUTH + +AIR302_fab.py:40:1: AIR302 [*] `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; + | +38 | CLIENT_AUTH +39 | find_user() +40 | init_app() + | ^^^^^^^^ AIR302 +41 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `init_app` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +29 29 | from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( +30 30 | CLIENT_AUTH, +31 31 | find_user, +32 |- init_app, +33 32 | log, +34 33 | requires_authentication, +35 34 | ) + 35 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import init_app +36 36 | +37 37 | log() +38 38 | CLIENT_AUTH + +AIR302_fab.py:41:1: AIR302 [*] `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; + | +39 | find_user() +40 | init_app() +41 | requires_authentication() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +42 | +43 | from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `requires_authentication` from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead. + +ℹ Unsafe fix +31 31 | find_user, +32 32 | init_app, +33 33 | log, +34 |- requires_authentication, +35 34 | ) + 35 |+from airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth import requires_authentication +36 36 | +37 37 | log() +38 38 | CLIENT_AUTH + +AIR302_fab.py:49:1: AIR302 [*] `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0; + | +47 | ) +48 | +49 | FabAuthManager() + | ^^^^^^^^^^^^^^ AIR302 +50 | MAX_NUM_DATABASE_USER_SESSIONS +51 | FabAirflowSecurityManagerOverride() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `FabAuthManager` from `airflow.providers.fab.auth_manager.fab_auth_manager` instead. + +ℹ Unsafe fix +40 40 | init_app() +41 41 | requires_authentication() +42 42 | +43 |-from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager +44 43 | from airflow.auth.managers.fab.security_manager.override import ( +45 44 | MAX_NUM_DATABASE_USER_SESSIONS, +46 45 | FabAirflowSecurityManagerOverride, +47 46 | ) + 47 |+from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager +48 48 | +49 49 | FabAuthManager() +50 50 | MAX_NUM_DATABASE_USER_SESSIONS + +AIR302_fab.py:50:1: AIR302 [*] `airflow.auth.managers.fab.security_manager.override.MAX_NUM_DATABASE_USER_SESSIONS` is moved into `fab` provider in Airflow 3.0; + | +49 | FabAuthManager() +50 | MAX_NUM_DATABASE_USER_SESSIONS + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +51 | FabAirflowSecurityManagerOverride() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `MAX_NUM_DATABASE_USER_SESSIONS` from `airflow.providers.fab.auth_manager.security_manager.override` instead. + +ℹ Unsafe fix +42 42 | +43 43 | from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager +44 44 | from airflow.auth.managers.fab.security_manager.override import ( +45 |- MAX_NUM_DATABASE_USER_SESSIONS, +46 45 | FabAirflowSecurityManagerOverride, +47 46 | ) + 47 |+from airflow.providers.fab.auth_manager.security_manager.override import MAX_NUM_DATABASE_USER_SESSIONS +48 48 | +49 49 | FabAuthManager() +50 50 | MAX_NUM_DATABASE_USER_SESSIONS + +AIR302_fab.py:51:1: AIR302 [*] `airflow.auth.managers.fab.security_manager.override.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; + | +49 | FabAuthManager() +50 | MAX_NUM_DATABASE_USER_SESSIONS +51 | FabAirflowSecurityManagerOverride() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +52 | +53 | from airflow.www.security import FabAirflowSecurityManagerOverride + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `FabAirflowSecurityManagerOverride` from `airflow.providers.fab.auth_manager.security_manager.override` instead. + +ℹ Unsafe fix +43 43 | from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager +44 44 | from airflow.auth.managers.fab.security_manager.override import ( +45 45 | MAX_NUM_DATABASE_USER_SESSIONS, +46 |- FabAirflowSecurityManagerOverride, +47 46 | ) + 47 |+from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride +48 48 | +49 49 | FabAuthManager() +50 50 | MAX_NUM_DATABASE_USER_SESSIONS + +AIR302_fab.py:55:1: AIR302 [*] `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; + | +53 | from airflow.www.security import FabAirflowSecurityManagerOverride +54 | +55 | FabAirflowSecurityManagerOverride() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `FabAirflowSecurityManagerOverride` from `airflow.providers.fab.auth_manager.security_manager.override` instead. + +ℹ Unsafe fix +50 50 | MAX_NUM_DATABASE_USER_SESSIONS +51 51 | FabAirflowSecurityManagerOverride() +52 52 | +53 |-from airflow.www.security import FabAirflowSecurityManagerOverride + 53 |+from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride +54 54 | +55 55 | FabAirflowSecurityManagerOverride() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap new file mode 100644 index 0000000000000..a3e75ff1ec465 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_hdfs.py:6:1: AIR302 [*] `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into `apache-hdfs` provider in Airflow 3.0; + | +4 | from airflow.sensors.web_hdfs_sensor import WebHdfsSensor +5 | +6 | WebHDFSHook() + | ^^^^^^^^^^^ AIR302 +7 | WebHdfsSensor() + | + = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `WebHDFSHook` from `airflow.providers.apache.hdfs.hooks.webhdfs` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.webhdfs_hook import WebHDFSHook +4 3 | from airflow.sensors.web_hdfs_sensor import WebHdfsSensor + 4 |+from airflow.providers.apache.hdfs.hooks.webhdfs import WebHDFSHook +5 5 | +6 6 | WebHDFSHook() +7 7 | WebHdfsSensor() + +AIR302_hdfs.py:7:1: AIR302 [*] `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved into `apache-hdfs` provider in Airflow 3.0; + | +6 | WebHDFSHook() +7 | WebHdfsSensor() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `WebHdfsSensor` from `airflow.providers.apache.hdfs.sensors.web_hdfs` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.webhdfs_hook import WebHDFSHook +4 |-from airflow.sensors.web_hdfs_sensor import WebHdfsSensor + 4 |+from airflow.providers.apache.hdfs.sensors.web_hdfs import WebHdfsSensor +5 5 | +6 6 | WebHDFSHook() +7 7 | WebHdfsSensor() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap new file mode 100644 index 0000000000000..b941acb2d7b05 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap @@ -0,0 +1,452 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_hive.py:18:1: AIR302 [*] `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is moved into `apache-hive` provider in Airflow 3.0; + | +16 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator +17 | +18 | HIVE_QUEUE_PRIORITIES + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +19 | HiveCliHook() +20 | HiveMetastoreHook() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HIVE_QUEUE_PRIORITIES` from `airflow.providers.apache.hive.hooks.hive` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.hive_hooks import ( +4 |- HIVE_QUEUE_PRIORITIES, +5 4 | HiveCliHook, +6 5 | HiveMetastoreHook, +7 6 | HiveServer2Hook, +-------------------------------------------------------------------------------- +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.hooks.hive import HIVE_QUEUE_PRIORITIES +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:19:1: AIR302 [*] `airflow.hooks.hive_hooks.HiveCliHook` is moved into `apache-hive` provider in Airflow 3.0; + | +18 | HIVE_QUEUE_PRIORITIES +19 | HiveCliHook() + | ^^^^^^^^^^^ AIR302 +20 | HiveMetastoreHook() +21 | HiveServer2Hook() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveCliHook` from `airflow.providers.apache.hive.hooks.hive` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.hive_hooks import ( +4 4 | HIVE_QUEUE_PRIORITIES, +5 |- HiveCliHook, +6 5 | HiveMetastoreHook, +7 6 | HiveServer2Hook, +8 7 | ) +-------------------------------------------------------------------------------- +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.hooks.hive import HiveCliHook +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:20:1: AIR302 [*] `airflow.hooks.hive_hooks.HiveMetastoreHook` is moved into `apache-hive` provider in Airflow 3.0; + | +18 | HIVE_QUEUE_PRIORITIES +19 | HiveCliHook() +20 | HiveMetastoreHook() + | ^^^^^^^^^^^^^^^^^ AIR302 +21 | HiveServer2Hook() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveMetastoreHook` from `airflow.providers.apache.hive.hooks.hive` instead. + +ℹ Unsafe fix +3 3 | from airflow.hooks.hive_hooks import ( +4 4 | HIVE_QUEUE_PRIORITIES, +5 5 | HiveCliHook, +6 |- HiveMetastoreHook, +7 6 | HiveServer2Hook, +8 7 | ) +9 8 | from airflow.macros.hive import ( +-------------------------------------------------------------------------------- +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.hooks.hive import HiveMetastoreHook +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:21:1: AIR302 [*] `airflow.hooks.hive_hooks.HiveServer2Hook` is moved into `apache-hive` provider in Airflow 3.0; + | +19 | HiveCliHook() +20 | HiveMetastoreHook() +21 | HiveServer2Hook() + | ^^^^^^^^^^^^^^^ AIR302 +22 | +23 | closest_ds_partition() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveServer2Hook` from `airflow.providers.apache.hive.hooks.hive` instead. + +ℹ Unsafe fix +4 4 | HIVE_QUEUE_PRIORITIES, +5 5 | HiveCliHook, +6 6 | HiveMetastoreHook, +7 |- HiveServer2Hook, +8 7 | ) +9 8 | from airflow.macros.hive import ( +10 9 | closest_ds_partition, +-------------------------------------------------------------------------------- +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.hooks.hive import HiveServer2Hook +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:23:1: AIR302 [*] `airflow.macros.hive.closest_ds_partition` is moved into `apache-hive` provider in Airflow 3.0; + | +21 | HiveServer2Hook() +22 | +23 | closest_ds_partition() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +24 | max_partition() + | + = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `closest_ds_partition` from `airflow.providers.apache.hive.macros.hive` instead. + +ℹ Unsafe fix +7 7 | HiveServer2Hook, +8 8 | ) +9 9 | from airflow.macros.hive import ( +10 |- closest_ds_partition, +11 10 | max_partition, +12 11 | ) +13 12 | from airflow.operators.hive_operator import HiveOperator +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.macros.hive import closest_ds_partition +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:24:1: AIR302 [*] `airflow.macros.hive.max_partition` is moved into `apache-hive` provider in Airflow 3.0; + | +23 | closest_ds_partition() +24 | max_partition() + | ^^^^^^^^^^^^^ AIR302 +25 | +26 | HiveOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `max_partition` from `airflow.providers.apache.hive.macros.hive` instead. + +ℹ Unsafe fix +8 8 | ) +9 9 | from airflow.macros.hive import ( +10 10 | closest_ds_partition, +11 |- max_partition, +12 11 | ) +13 12 | from airflow.operators.hive_operator import HiveOperator +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.macros.hive import max_partition +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:26:1: AIR302 [*] `airflow.operators.hive_operator.HiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +24 | max_partition() +25 | +26 | HiveOperator() + | ^^^^^^^^^^^^ AIR302 +27 | HiveStatsCollectionOperator() +28 | HiveToMySqlOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveOperator` from `airflow.providers.apache.hive.operators.hive` instead. + +ℹ Unsafe fix +10 10 | closest_ds_partition, +11 11 | max_partition, +12 12 | ) +13 |-from airflow.operators.hive_operator import HiveOperator +14 13 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.operators.hive import HiveOperator +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:27:1: AIR302 [*] `airflow.operators.hive_stats_operator.HiveStatsCollectionOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +26 | HiveOperator() +27 | HiveStatsCollectionOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +28 | HiveToMySqlOperator() +29 | HiveToSambaOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveStatsCollectionOperator` from `airflow.providers.apache.hive.operators.hive_stats` instead. + +ℹ Unsafe fix +11 11 | max_partition, +12 12 | ) +13 13 | from airflow.operators.hive_operator import HiveOperator +14 |-from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 14 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.operators.hive_stats import HiveStatsCollectionOperator +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:28:1: AIR302 [*] `airflow.operators.hive_to_mysql.HiveToMySqlOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +26 | HiveOperator() +27 | HiveStatsCollectionOperator() +28 | HiveToMySqlOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +29 | HiveToSambaOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveToMySqlOperator` from `airflow.providers.apache.hive.transfers.hive_to_mysql` instead. + +ℹ Unsafe fix +12 12 | ) +13 13 | from airflow.operators.hive_operator import HiveOperator +14 14 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 |-from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 15 | from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.transfers.hive_to_mysql import HiveToMySqlOperator +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:29:1: AIR302 [*] `airflow.operators.hive_to_samba_operator.HiveToSambaOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +27 | HiveStatsCollectionOperator() +28 | HiveToMySqlOperator() +29 | HiveToSambaOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveToSambaOperator` from `airflow.providers.apache.hive.transfers.hive_to_samba` instead. + +ℹ Unsafe fix +13 13 | from airflow.operators.hive_operator import HiveOperator +14 14 | from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +15 15 | from airflow.operators.hive_to_mysql import HiveToMySqlOperator +16 |-from airflow.operators.hive_to_samba_operator import HiveToSambaOperator + 16 |+from airflow.providers.apache.hive.transfers.hive_to_samba import HiveToSambaOperator +17 17 | +18 18 | HIVE_QUEUE_PRIORITIES +19 19 | HiveCliHook() + +AIR302_hive.py:34:1: AIR302 [*] `airflow.operators.hive_to_mysql.HiveToMySqlTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +32 | from airflow.operators.hive_to_mysql import HiveToMySqlTransfer +33 | +34 | HiveToMySqlTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +35 | +36 | from airflow.operators.mysql_to_hive import MySqlToHiveOperator + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveToMySqlOperator` from `airflow.providers.apache.hive.transfers.hive_to_mysql` instead. + +ℹ Unsafe fix +30 30 | +31 31 | +32 32 | from airflow.operators.hive_to_mysql import HiveToMySqlTransfer + 33 |+from airflow.providers.apache.hive.transfers.hive_to_mysql import HiveToMySqlOperator +33 34 | +34 35 | HiveToMySqlTransfer() +35 36 | + +AIR302_hive.py:38:1: AIR302 [*] `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +36 | from airflow.operators.mysql_to_hive import MySqlToHiveOperator +37 | +38 | MySqlToHiveOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +39 | +40 | from airflow.operators.mysql_to_hive import MySqlToHiveTransfer + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `MySqlToHiveOperator` from `airflow.providers.apache.hive.transfers.mysql_to_hive` instead. + +ℹ Unsafe fix +33 33 | +34 34 | HiveToMySqlTransfer() +35 35 | +36 |-from airflow.operators.mysql_to_hive import MySqlToHiveOperator + 36 |+from airflow.providers.apache.hive.transfers.mysql_to_hive import MySqlToHiveOperator +37 37 | +38 38 | MySqlToHiveOperator() +39 39 | + +AIR302_hive.py:42:1: AIR302 [*] `airflow.operators.mysql_to_hive.MySqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +40 | from airflow.operators.mysql_to_hive import MySqlToHiveTransfer +41 | +42 | MySqlToHiveTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +43 | +44 | from airflow.operators.mssql_to_hive import MsSqlToHiveOperator + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `MySqlToHiveOperator` from `airflow.providers.apache.hive.transfers.mysql_to_hive` instead. + +ℹ Unsafe fix +38 38 | MySqlToHiveOperator() +39 39 | +40 40 | from airflow.operators.mysql_to_hive import MySqlToHiveTransfer + 41 |+from airflow.providers.apache.hive.transfers.mysql_to_hive import MySqlToHiveOperator +41 42 | +42 43 | MySqlToHiveTransfer() +43 44 | + +AIR302_hive.py:46:1: AIR302 [*] `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +44 | from airflow.operators.mssql_to_hive import MsSqlToHiveOperator +45 | +46 | MsSqlToHiveOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +47 | +48 | from airflow.operators.mssql_to_hive import MsSqlToHiveTransfer + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `MsSqlToHiveOperator` from `airflow.providers.apache.hive.transfers.mssql_to_hive` instead. + +ℹ Unsafe fix +41 41 | +42 42 | MySqlToHiveTransfer() +43 43 | +44 |-from airflow.operators.mssql_to_hive import MsSqlToHiveOperator + 44 |+from airflow.providers.apache.hive.transfers.mssql_to_hive import MsSqlToHiveOperator +45 45 | +46 46 | MsSqlToHiveOperator() +47 47 | + +AIR302_hive.py:50:1: AIR302 [*] `airflow.operators.mssql_to_hive.MsSqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +48 | from airflow.operators.mssql_to_hive import MsSqlToHiveTransfer +49 | +50 | MsSqlToHiveTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +51 | +52 | from airflow.operators.s3_to_hive_operator import S3ToHiveOperator + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `MsSqlToHiveOperator` from `airflow.providers.apache.hive.transfers.mssql_to_hive` instead. + +ℹ Unsafe fix +46 46 | MsSqlToHiveOperator() +47 47 | +48 48 | from airflow.operators.mssql_to_hive import MsSqlToHiveTransfer + 49 |+from airflow.providers.apache.hive.transfers.mssql_to_hive import MsSqlToHiveOperator +49 50 | +50 51 | MsSqlToHiveTransfer() +51 52 | + +AIR302_hive.py:54:1: AIR302 [*] `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +52 | from airflow.operators.s3_to_hive_operator import S3ToHiveOperator +53 | +54 | S3ToHiveOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +55 | +56 | from airflow.operators.s3_to_hive_operator import S3ToHiveTransfer + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `S3ToHiveOperator` from `airflow.providers.apache.hive.transfers.s3_to_hive` instead. + +ℹ Unsafe fix +49 49 | +50 50 | MsSqlToHiveTransfer() +51 51 | +52 |-from airflow.operators.s3_to_hive_operator import S3ToHiveOperator + 52 |+from airflow.providers.apache.hive.transfers.s3_to_hive import S3ToHiveOperator +53 53 | +54 54 | S3ToHiveOperator() +55 55 | + +AIR302_hive.py:58:1: AIR302 [*] `airflow.operators.s3_to_hive_operator.S3ToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +56 | from airflow.operators.s3_to_hive_operator import S3ToHiveTransfer +57 | +58 | S3ToHiveTransfer() + | ^^^^^^^^^^^^^^^^ AIR302 +59 | +60 | from airflow.sensors.hive_partition_sensor import HivePartitionSensor + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `S3ToHiveOperator` from `airflow.providers.apache.hive.transfers.s3_to_hive` instead. + +ℹ Unsafe fix +54 54 | S3ToHiveOperator() +55 55 | +56 56 | from airflow.operators.s3_to_hive_operator import S3ToHiveTransfer + 57 |+from airflow.providers.apache.hive.transfers.s3_to_hive import S3ToHiveOperator +57 58 | +58 59 | S3ToHiveTransfer() +59 60 | + +AIR302_hive.py:62:1: AIR302 [*] `airflow.sensors.hive_partition_sensor.HivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; + | +60 | from airflow.sensors.hive_partition_sensor import HivePartitionSensor +61 | +62 | HivePartitionSensor() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +63 | +64 | from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HivePartitionSensor` from `airflow.providers.apache.hive.sensors.hive_partition` instead. + +ℹ Unsafe fix +57 57 | +58 58 | S3ToHiveTransfer() +59 59 | +60 |-from airflow.sensors.hive_partition_sensor import HivePartitionSensor + 60 |+from airflow.providers.apache.hive.sensors.hive_partition import HivePartitionSensor +61 61 | +62 62 | HivePartitionSensor() +63 63 | + +AIR302_hive.py:66:1: AIR302 [*] `airflow.sensors.metastore_partition_sensor.MetastorePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; + | +64 | from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor +65 | +66 | MetastorePartitionSensor() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +67 | +68 | from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `MetastorePartitionSensor` from `airflow.providers.apache.hive.sensors.metastore_partition` instead. + +ℹ Unsafe fix +61 61 | +62 62 | HivePartitionSensor() +63 63 | +64 |-from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor + 64 |+from airflow.providers.apache.hive.sensors.metastore_partition import MetastorePartitionSensor +65 65 | +66 66 | MetastorePartitionSensor() +67 67 | + +AIR302_hive.py:70:1: AIR302 [*] `airflow.sensors.named_hive_partition_sensor.NamedHivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; + | +68 | from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor +69 | +70 | NamedHivePartitionSensor() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `NamedHivePartitionSensor` from `airflow.providers.apache.hive.sensors.named_hive_partition` instead. + +ℹ Unsafe fix +65 65 | +66 66 | MetastorePartitionSensor() +67 67 | +68 |-from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor + 68 |+from airflow.providers.apache.hive.sensors.named_hive_partition import NamedHivePartitionSensor +69 69 | +70 70 | NamedHivePartitionSensor() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap new file mode 100644 index 0000000000000..3a8993133ed59 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_http.py:7:1: AIR302 [*] `airflow.hooks.http_hook.HttpHook` is moved into `http` provider in Airflow 3.0; + | +5 | from airflow.sensors.http_sensor import HttpSensor +6 | +7 | HttpHook() + | ^^^^^^^^ AIR302 +8 | SimpleHttpOperator() +9 | HttpSensor() + | + = help: Install `apache-airflow-providers-http>=1.0.0` and use `HttpHook` from `airflow.providers.http.hooks.http` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.http_hook import HttpHook +4 3 | from airflow.operators.http_operator import SimpleHttpOperator +5 4 | from airflow.sensors.http_sensor import HttpSensor + 5 |+from airflow.providers.http.hooks.http import HttpHook +6 6 | +7 7 | HttpHook() +8 8 | SimpleHttpOperator() + +AIR302_http.py:8:1: AIR302 [*] `airflow.operators.http_operator.SimpleHttpOperator` is moved into `http` provider in Airflow 3.0; + | +7 | HttpHook() +8 | SimpleHttpOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +9 | HttpSensor() + | + = help: Install `apache-airflow-providers-http>=5.0.0` and use `HttpOperator` from `airflow.providers.http.operators.http` instead. + +ℹ Safe fix +3 3 | from airflow.hooks.http_hook import HttpHook +4 4 | from airflow.operators.http_operator import SimpleHttpOperator +5 5 | from airflow.sensors.http_sensor import HttpSensor + 6 |+from airflow.providers.http.operators.http import HttpOperator +6 7 | +7 8 | HttpHook() +8 |-SimpleHttpOperator() + 9 |+HttpOperator() +9 10 | HttpSensor() + +AIR302_http.py:9:1: AIR302 [*] `airflow.sensors.http_sensor.HttpSensor` is moved into `http` provider in Airflow 3.0; + | +7 | HttpHook() +8 | SimpleHttpOperator() +9 | HttpSensor() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-http>=1.0.0` and use `HttpSensor` from `airflow.providers.http.sensors.http` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.http_hook import HttpHook +4 4 | from airflow.operators.http_operator import SimpleHttpOperator +5 |-from airflow.sensors.http_sensor import HttpSensor + 5 |+from airflow.providers.http.sensors.http import HttpSensor +6 6 | +7 7 | HttpHook() +8 8 | SimpleHttpOperator() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap new file mode 100644 index 0000000000000..dd6177f543559 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap @@ -0,0 +1,43 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_jdbc.py:8:1: AIR302 [*] `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` provider in Airflow 3.0; + | +6 | ) +7 | +8 | JdbcHook() + | ^^^^^^^^ AIR302 +9 | jaydebeapi() + | + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `JdbcHook` from `airflow.providers.jdbc.hooks.jdbc` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.jdbc_hook import ( +4 |- JdbcHook, +5 4 | jaydebeapi, +6 5 | ) + 6 |+from airflow.providers.jdbc.hooks.jdbc import JdbcHook +7 7 | +8 8 | JdbcHook() +9 9 | jaydebeapi() + +AIR302_jdbc.py:9:1: AIR302 [*] `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in Airflow 3.0; + | +8 | JdbcHook() +9 | jaydebeapi() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `jaydebeapi` from `airflow.providers.jdbc.hooks.jdbc` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.jdbc_hook import ( +4 4 | JdbcHook, +5 |- jaydebeapi, +6 5 | ) + 6 |+from airflow.providers.jdbc.hooks.jdbc import jaydebeapi +7 7 | +8 8 | JdbcHook() +9 9 | jaydebeapi() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap new file mode 100644 index 0000000000000..f46860be1581f --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap @@ -0,0 +1,946 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_kubernetes.py:22:1: AIR302 [*] `airflow.executors.kubernetes_executor_types.ALL_NAMESPACES` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +20 | ) +21 | +22 | ALL_NAMESPACES + | ^^^^^^^^^^^^^^ AIR302 +23 | POD_EXECUTOR_DONE_KEY + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `ALL_NAMESPACES` from `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.executors.kubernetes_executor_types import ( +4 |- ALL_NAMESPACES, +5 4 | POD_EXECUTOR_DONE_KEY, +6 5 | ) +7 6 | from airflow.kubernetes.k8s_model import ( +-------------------------------------------------------------------------------- +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import ALL_NAMESPACES +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:23:1: AIR302 [*] `airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +22 | ALL_NAMESPACES +23 | POD_EXECUTOR_DONE_KEY + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +24 | +25 | K8SModel() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `POD_EXECUTOR_DONE_KEY` from `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.executors.kubernetes_executor_types import ( +4 4 | ALL_NAMESPACES, +5 |- POD_EXECUTOR_DONE_KEY, +6 5 | ) +7 6 | from airflow.kubernetes.k8s_model import ( +8 7 | K8SModel, +-------------------------------------------------------------------------------- +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import POD_EXECUTOR_DONE_KEY +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:25:1: AIR302 [*] `airflow.kubernetes.k8s_model.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +23 | POD_EXECUTOR_DONE_KEY +24 | +25 | K8SModel() + | ^^^^^^^^ AIR302 +26 | append_to_pod() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `K8SModel` from `airflow.providers.cncf.kubernetes.k8s_model` instead. + +ℹ Unsafe fix +5 5 | POD_EXECUTOR_DONE_KEY, +6 6 | ) +7 7 | from airflow.kubernetes.k8s_model import ( +8 |- K8SModel, +9 8 | append_to_pod, +10 9 | ) +11 10 | from airflow.kubernetes.kube_client import ( +-------------------------------------------------------------------------------- +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.k8s_model import K8SModel +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:26:1: AIR302 [*] `airflow.kubernetes.k8s_model.append_to_pod` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +25 | K8SModel() +26 | append_to_pod() + | ^^^^^^^^^^^^^ AIR302 +27 | +28 | _disable_verify_ssl() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `append_to_pod` from `airflow.providers.cncf.kubernetes.k8s_model` instead. + +ℹ Unsafe fix +6 6 | ) +7 7 | from airflow.kubernetes.k8s_model import ( +8 8 | K8SModel, +9 |- append_to_pod, +10 9 | ) +11 10 | from airflow.kubernetes.kube_client import ( +12 11 | _disable_verify_ssl, +-------------------------------------------------------------------------------- +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.k8s_model import append_to_pod +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:28:1: AIR302 [*] `airflow.kubernetes.kube_client._disable_verify_ssl` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +26 | append_to_pod() +27 | +28 | _disable_verify_ssl() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +29 | _enable_tcp_keepalive() +30 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `_disable_verify_ssl` from `airflow.providers.cncf.kubernetes.kube_client` instead. + +ℹ Unsafe fix +9 9 | append_to_pod, +10 10 | ) +11 11 | from airflow.kubernetes.kube_client import ( +12 |- _disable_verify_ssl, +13 12 | _enable_tcp_keepalive, +14 13 | get_kube_client, +15 14 | ) +-------------------------------------------------------------------------------- +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.kube_client import _disable_verify_ssl +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:29:1: AIR302 [*] `airflow.kubernetes.kube_client._enable_tcp_keepalive` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +28 | _disable_verify_ssl() +29 | _enable_tcp_keepalive() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +30 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `_enable_tcp_keepalive` from `airflow.providers.cncf.kubernetes.kube_client` instead. + +ℹ Unsafe fix +10 10 | ) +11 11 | from airflow.kubernetes.kube_client import ( +12 12 | _disable_verify_ssl, +13 |- _enable_tcp_keepalive, +14 13 | get_kube_client, +15 14 | ) +16 15 | from airflow.kubernetes.kubernetes_helper_functions import ( +-------------------------------------------------------------------------------- +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.kube_client import _enable_tcp_keepalive +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:30:1: AIR302 [*] `airflow.kubernetes.kube_client.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +28 | _disable_verify_ssl() +29 | _enable_tcp_keepalive() +30 | get_kube_client() + | ^^^^^^^^^^^^^^^ AIR302 +31 | +32 | add_pod_suffix() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `get_kube_client` from `airflow.providers.cncf.kubernetes.kube_client` instead. + +ℹ Unsafe fix +11 11 | from airflow.kubernetes.kube_client import ( +12 12 | _disable_verify_ssl, +13 13 | _enable_tcp_keepalive, +14 |- get_kube_client, +15 14 | ) +16 15 | from airflow.kubernetes.kubernetes_helper_functions import ( +17 16 | add_pod_suffix, +18 17 | annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.kube_client import get_kube_client +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:32:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +30 | get_kube_client() +31 | +32 | add_pod_suffix() + | ^^^^^^^^^^^^^^ AIR302 +33 | annotations_for_logging_task_metadata() +34 | create_pod_id() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `add_unique_suffix` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Safe fix +18 18 | annotations_for_logging_task_metadata, +19 19 | create_pod_id, +20 20 | ) + 21 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import add_unique_suffix +21 22 | +22 23 | ALL_NAMESPACES +23 24 | POD_EXECUTOR_DONE_KEY +-------------------------------------------------------------------------------- +29 30 | _enable_tcp_keepalive() +30 31 | get_kube_client() +31 32 | +32 |-add_pod_suffix() + 33 |+add_unique_suffix() +33 34 | annotations_for_logging_task_metadata() +34 35 | create_pod_id() +35 36 | + +AIR302_kubernetes.py:33:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +32 | add_pod_suffix() +33 | annotations_for_logging_task_metadata() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +34 | create_pod_id() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `annotations_for_logging_task_metadata` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Unsafe fix +15 15 | ) +16 16 | from airflow.kubernetes.kubernetes_helper_functions import ( +17 17 | add_pod_suffix, +18 |- annotations_for_logging_task_metadata, +19 18 | create_pod_id, +20 19 | ) + 20 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import annotations_for_logging_task_metadata +21 21 | +22 22 | ALL_NAMESPACES +23 23 | POD_EXECUTOR_DONE_KEY + +AIR302_kubernetes.py:34:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +32 | add_pod_suffix() +33 | annotations_for_logging_task_metadata() +34 | create_pod_id() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `create_unique_id` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Safe fix +18 18 | annotations_for_logging_task_metadata, +19 19 | create_pod_id, +20 20 | ) + 21 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import create_unique_id +21 22 | +22 23 | ALL_NAMESPACES +23 24 | POD_EXECUTOR_DONE_KEY +-------------------------------------------------------------------------------- +31 32 | +32 33 | add_pod_suffix() +33 34 | annotations_for_logging_task_metadata() +34 |-create_pod_id() + 35 |+create_unique_id() +35 36 | +36 37 | +37 38 | from airflow.kubernetes.pod_generator import ( + +AIR302_kubernetes.py:49:1: AIR302 [*] `airflow.kubernetes.pod_generator.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +47 | ) +48 | +49 | PodDefaults() + | ^^^^^^^^^^^ AIR302 +50 | PodGenerator() +51 | add_pod_suffix() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodDefaults` from `airflow.providers.cncf.kubernetes.utils.xcom_sidecar` instead. + +ℹ Unsafe fix +35 35 | +36 36 | +37 37 | from airflow.kubernetes.pod_generator import ( +38 |- PodDefaults, +39 38 | PodGenerator, +40 39 | add_pod_suffix, +41 40 | datetime_to_label_safe_datestring, +-------------------------------------------------------------------------------- +45 44 | merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.utils.xcom_sidecar import PodDefaults +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:50:1: AIR302 [*] `airflow.kubernetes.pod_generator.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +49 | PodDefaults() +50 | PodGenerator() + | ^^^^^^^^^^^^ AIR302 +51 | add_pod_suffix() +52 | datetime_to_label_safe_datestring() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodGenerator` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +36 36 | +37 37 | from airflow.kubernetes.pod_generator import ( +38 38 | PodDefaults, +39 |- PodGenerator, +40 39 | add_pod_suffix, +41 40 | datetime_to_label_safe_datestring, +42 41 | extend_object_field, +-------------------------------------------------------------------------------- +45 44 | merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:51:1: AIR302 [*] `airflow.kubernetes.pod_generator.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +49 | PodDefaults() +50 | PodGenerator() +51 | add_pod_suffix() + | ^^^^^^^^^^^^^^ AIR302 +52 | datetime_to_label_safe_datestring() +53 | extend_object_field() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `add_unique_suffix` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Safe fix +45 45 | merge_objects, +46 46 | rand_str, +47 47 | ) + 48 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import add_unique_suffix +48 49 | +49 50 | PodDefaults() +50 51 | PodGenerator() +51 |-add_pod_suffix() + 52 |+add_unique_suffix() +52 53 | datetime_to_label_safe_datestring() +53 54 | extend_object_field() +54 55 | label_safe_datestring_to_datetime() + +AIR302_kubernetes.py:52:1: AIR302 [*] `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +50 | PodGenerator() +51 | add_pod_suffix() +52 | datetime_to_label_safe_datestring() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +53 | extend_object_field() +54 | label_safe_datestring_to_datetime() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `datetime_to_label_safe_datestring` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +38 38 | PodDefaults, +39 39 | PodGenerator, +40 40 | add_pod_suffix, +41 |- datetime_to_label_safe_datestring, +42 41 | extend_object_field, +43 42 | label_safe_datestring_to_datetime, +44 43 | make_safe_label_value, +45 44 | merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.pod_generator import datetime_to_label_safe_datestring +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:53:1: AIR302 [*] `airflow.kubernetes.pod_generator.extend_object_field` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +51 | add_pod_suffix() +52 | datetime_to_label_safe_datestring() +53 | extend_object_field() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +54 | label_safe_datestring_to_datetime() +55 | make_safe_label_value() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `extend_object_field` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +39 39 | PodGenerator, +40 40 | add_pod_suffix, +41 41 | datetime_to_label_safe_datestring, +42 |- extend_object_field, +43 42 | label_safe_datestring_to_datetime, +44 43 | make_safe_label_value, +45 44 | merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.pod_generator import extend_object_field +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:54:1: AIR302 [*] `airflow.kubernetes.pod_generator.label_safe_datestring_to_datetime` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +52 | datetime_to_label_safe_datestring() +53 | extend_object_field() +54 | label_safe_datestring_to_datetime() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +55 | make_safe_label_value() +56 | merge_objects() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `label_safe_datestring_to_datetime` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +40 40 | add_pod_suffix, +41 41 | datetime_to_label_safe_datestring, +42 42 | extend_object_field, +43 |- label_safe_datestring_to_datetime, +44 43 | make_safe_label_value, +45 44 | merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.pod_generator import label_safe_datestring_to_datetime +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:55:1: AIR302 [*] `airflow.kubernetes.pod_generator.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +53 | extend_object_field() +54 | label_safe_datestring_to_datetime() +55 | make_safe_label_value() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +56 | merge_objects() +57 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `make_safe_label_value` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +41 41 | datetime_to_label_safe_datestring, +42 42 | extend_object_field, +43 43 | label_safe_datestring_to_datetime, +44 |- make_safe_label_value, +45 44 | merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.pod_generator import make_safe_label_value +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:56:1: AIR302 [*] `airflow.kubernetes.pod_generator.merge_objects` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +54 | label_safe_datestring_to_datetime() +55 | make_safe_label_value() +56 | merge_objects() + | ^^^^^^^^^^^^^ AIR302 +57 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `merge_objects` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +42 42 | extend_object_field, +43 43 | label_safe_datestring_to_datetime, +44 44 | make_safe_label_value, +45 |- merge_objects, +46 45 | rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.pod_generator import merge_objects +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:57:1: AIR302 [*] `airflow.kubernetes.pod_generator.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +55 | make_safe_label_value() +56 | merge_objects() +57 | rand_str() + | ^^^^^^^^ AIR302 +58 | +59 | from airflow.kubernetes.pod_generator_deprecated import ( + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `rand_str` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Unsafe fix +43 43 | label_safe_datestring_to_datetime, +44 44 | make_safe_label_value, +45 45 | merge_objects, +46 |- rand_str, +47 46 | ) + 47 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import rand_str +48 48 | +49 49 | PodDefaults() +50 50 | PodGenerator() + +AIR302_kubernetes.py:69:1: AIR302 [*] `airflow.kubernetes.pod_generator_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +67 | ) +68 | +69 | PodDefaults() + | ^^^^^^^^^^^ AIR302 +70 | PodGenerator() +71 | make_safe_label_value() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodDefaults` from `airflow.providers.cncf.kubernetes.utils.xcom_sidecar` instead. + +ℹ Unsafe fix +57 57 | rand_str() +58 58 | +59 59 | from airflow.kubernetes.pod_generator_deprecated import ( +60 |- PodDefaults, +61 60 | PodGenerator, +62 61 | make_safe_label_value, +63 62 | ) +-------------------------------------------------------------------------------- +65 64 | PodLauncher, +66 65 | PodStatus, +67 66 | ) + 67 |+from airflow.providers.cncf.kubernetes.utils.xcom_sidecar import PodDefaults +68 68 | +69 69 | PodDefaults() +70 70 | PodGenerator() + +AIR302_kubernetes.py:70:1: AIR302 [*] `airflow.kubernetes.pod_generator_deprecated.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +69 | PodDefaults() +70 | PodGenerator() + | ^^^^^^^^^^^^ AIR302 +71 | make_safe_label_value() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodGenerator` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +58 58 | +59 59 | from airflow.kubernetes.pod_generator_deprecated import ( +60 60 | PodDefaults, +61 |- PodGenerator, +62 61 | make_safe_label_value, +63 62 | ) +64 63 | from airflow.kubernetes.pod_launcher import ( +65 64 | PodLauncher, +66 65 | PodStatus, +67 66 | ) + 67 |+from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator +68 68 | +69 69 | PodDefaults() +70 70 | PodGenerator() + +AIR302_kubernetes.py:71:1: AIR302 [*] `airflow.kubernetes.pod_generator_deprecated.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +69 | PodDefaults() +70 | PodGenerator() +71 | make_safe_label_value() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +72 | +73 | PodLauncher() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `make_safe_label_value` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +59 59 | from airflow.kubernetes.pod_generator_deprecated import ( +60 60 | PodDefaults, +61 61 | PodGenerator, +62 |- make_safe_label_value, +63 62 | ) +64 63 | from airflow.kubernetes.pod_launcher import ( +65 64 | PodLauncher, +66 65 | PodStatus, +67 66 | ) + 67 |+from airflow.providers.cncf.kubernetes.pod_generator import make_safe_label_value +68 68 | +69 69 | PodDefaults() +70 70 | PodGenerator() + +AIR302_kubernetes.py:73:1: AIR302 [*] `airflow.kubernetes.pod_launcher.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +71 | make_safe_label_value() +72 | +73 | PodLauncher() + | ^^^^^^^^^^^ AIR302 +74 | PodStatus() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `PodManager` from `airflow.providers.cncf.kubernetes.utils.pod_manager` instead. + +ℹ Safe fix +65 65 | PodLauncher, +66 66 | PodStatus, +67 67 | ) + 68 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodManager +68 69 | +69 70 | PodDefaults() +70 71 | PodGenerator() +71 72 | make_safe_label_value() +72 73 | +73 |-PodLauncher() + 74 |+PodManager() +74 75 | PodStatus() +75 76 | +76 77 | from airflow.kubernetes.pod_launcher_deprecated import ( + +AIR302_kubernetes.py:74:1: AIR302 [*] `airflow.kubernetes.pod_launcher.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +73 | PodLauncher() +74 | PodStatus() + | ^^^^^^^^^ AIR302 +75 | +76 | from airflow.kubernetes.pod_launcher_deprecated import ( + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `PodPhase` from ` airflow.providers.cncf.kubernetes.utils.pod_manager` instead. + +ℹ Safe fix +65 65 | PodLauncher, +66 66 | PodStatus, +67 67 | ) + 68 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodPhase +68 69 | +69 70 | PodDefaults() +70 71 | PodGenerator() +71 72 | make_safe_label_value() +72 73 | +73 74 | PodLauncher() +74 |-PodStatus() + 75 |+PodPhase() +75 76 | +76 77 | from airflow.kubernetes.pod_launcher_deprecated import ( +77 78 | PodDefaults, + +AIR302_kubernetes.py:90:1: AIR302 [*] `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +88 | from airflow.kubernetes.volume_mount import VolumeMount +89 | +90 | PodDefaults() + | ^^^^^^^^^^^ AIR302 +91 | PodLauncher() +92 | PodStatus() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodDefaults` from `airflow.providers.cncf.kubernetes.utils.xcom_sidecar` instead. + +ℹ Unsafe fix +74 74 | PodStatus() +75 75 | +76 76 | from airflow.kubernetes.pod_launcher_deprecated import ( +77 |- PodDefaults, +78 77 | PodLauncher, +79 78 | PodStatus, +80 79 | get_kube_client, +-------------------------------------------------------------------------------- +86 85 | ) +87 86 | from airflow.kubernetes.volume import Volume +88 87 | from airflow.kubernetes.volume_mount import VolumeMount + 88 |+from airflow.providers.cncf.kubernetes.utils.xcom_sidecar import PodDefaults +89 89 | +90 90 | PodDefaults() +91 91 | PodLauncher() + +AIR302_kubernetes.py:91:1: AIR302 [*] `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +90 | PodDefaults() +91 | PodLauncher() + | ^^^^^^^^^^^ AIR302 +92 | PodStatus() +93 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `PodManager` from `airflow.providers.cncf.kubernetes.utils.pod_manager` instead. + +ℹ Safe fix +86 86 | ) +87 87 | from airflow.kubernetes.volume import Volume +88 88 | from airflow.kubernetes.volume_mount import VolumeMount + 89 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodManager +89 90 | +90 91 | PodDefaults() +91 |-PodLauncher() + 92 |+PodManager() +92 93 | PodStatus() +93 94 | get_kube_client() +94 95 | + +AIR302_kubernetes.py:92:1: AIR302 [*] `airflow.kubernetes.pod_launcher_deprecated.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +90 | PodDefaults() +91 | PodLauncher() +92 | PodStatus() + | ^^^^^^^^^ AIR302 +93 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `PodPhase` from ` airflow.providers.cncf.kubernetes.utils.pod_manager` instead. + +ℹ Safe fix +86 86 | ) +87 87 | from airflow.kubernetes.volume import Volume +88 88 | from airflow.kubernetes.volume_mount import VolumeMount + 89 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodPhase +89 90 | +90 91 | PodDefaults() +91 92 | PodLauncher() +92 |-PodStatus() + 93 |+PodPhase() +93 94 | get_kube_client() +94 95 | +95 96 | PodRuntimeInfoEnv() + +AIR302_kubernetes.py:93:1: AIR302 [*] `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +91 | PodLauncher() +92 | PodStatus() +93 | get_kube_client() + | ^^^^^^^^^^^^^^^ AIR302 +94 | +95 | PodRuntimeInfoEnv() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `get_kube_client` from `airflow.providers.cncf.kubernetes.kube_client` instead. + +ℹ Unsafe fix +77 77 | PodDefaults, +78 78 | PodLauncher, +79 79 | PodStatus, +80 |- get_kube_client, +81 80 | ) +82 81 | from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv +83 82 | from airflow.kubernetes.secret import ( +-------------------------------------------------------------------------------- +86 85 | ) +87 86 | from airflow.kubernetes.volume import Volume +88 87 | from airflow.kubernetes.volume_mount import VolumeMount + 88 |+from airflow.providers.cncf.kubernetes.kube_client import get_kube_client +89 89 | +90 90 | PodDefaults() +91 91 | PodLauncher() + +AIR302_kubernetes.py:95:1: AIR302 [*] `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoEnv` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +93 | get_kube_client() +94 | +95 | PodRuntimeInfoEnv() + | ^^^^^^^^^^^^^^^^^ AIR302 +96 | K8SModel() +97 | Secret() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `V1EnvVar` from `kubernetes.client.models` instead. + +ℹ Safe fix +86 86 | ) +87 87 | from airflow.kubernetes.volume import Volume +88 88 | from airflow.kubernetes.volume_mount import VolumeMount + 89 |+from kubernetes.client.models import V1EnvVar +89 90 | +90 91 | PodDefaults() +91 92 | PodLauncher() +92 93 | PodStatus() +93 94 | get_kube_client() +94 95 | +95 |-PodRuntimeInfoEnv() + 96 |+V1EnvVar() +96 97 | K8SModel() +97 98 | Secret() +98 99 | Volume() + +AIR302_kubernetes.py:96:1: AIR302 [*] `airflow.kubernetes.secret.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +95 | PodRuntimeInfoEnv() +96 | K8SModel() + | ^^^^^^^^ AIR302 +97 | Secret() +98 | Volume() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `K8SModel` from `airflow.providers.cncf.kubernetes.k8s_model` instead. + +ℹ Unsafe fix +81 81 | ) +82 82 | from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv +83 83 | from airflow.kubernetes.secret import ( +84 |- K8SModel, +85 84 | Secret, +86 85 | ) +87 86 | from airflow.kubernetes.volume import Volume +88 87 | from airflow.kubernetes.volume_mount import VolumeMount + 88 |+from airflow.providers.cncf.kubernetes.k8s_model import K8SModel +89 89 | +90 90 | PodDefaults() +91 91 | PodLauncher() + +AIR302_kubernetes.py:97:1: AIR302 [*] `airflow.kubernetes.secret.Secret` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +95 | PodRuntimeInfoEnv() +96 | K8SModel() +97 | Secret() + | ^^^^^^ AIR302 +98 | Volume() +99 | VolumeMount() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `Secret` from `airflow.providers.cncf.kubernetes.secret` instead. + +ℹ Unsafe fix +82 82 | from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv +83 83 | from airflow.kubernetes.secret import ( +84 84 | K8SModel, +85 |- Secret, +86 85 | ) +87 86 | from airflow.kubernetes.volume import Volume +88 87 | from airflow.kubernetes.volume_mount import VolumeMount + 88 |+from airflow.providers.cncf.kubernetes.secret import Secret +89 89 | +90 90 | PodDefaults() +91 91 | PodLauncher() + +AIR302_kubernetes.py:98:1: AIR302 [*] `airflow.kubernetes.volume.Volume` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +96 | K8SModel() +97 | Secret() +98 | Volume() + | ^^^^^^ AIR302 +99 | VolumeMount() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `V1Volume` from `kubernetes.client.models` instead. + +ℹ Safe fix +86 86 | ) +87 87 | from airflow.kubernetes.volume import Volume +88 88 | from airflow.kubernetes.volume_mount import VolumeMount + 89 |+from kubernetes.client.models import V1Volume +89 90 | +90 91 | PodDefaults() +91 92 | PodLauncher() +-------------------------------------------------------------------------------- +95 96 | PodRuntimeInfoEnv() +96 97 | K8SModel() +97 98 | Secret() +98 |-Volume() + 99 |+V1Volume() +99 100 | VolumeMount() +100 101 | +101 102 | from airflow.kubernetes.kubernetes_helper_functions import ( + +AIR302_kubernetes.py:99:1: AIR302 [*] `airflow.kubernetes.volume_mount.VolumeMount` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | + 97 | Secret() + 98 | Volume() + 99 | VolumeMount() + | ^^^^^^^^^^^ AIR302 +100 | +101 | from airflow.kubernetes.kubernetes_helper_functions import ( + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `V1VolumeMount` from `kubernetes.client.models` instead. + +ℹ Safe fix +86 86 | ) +87 87 | from airflow.kubernetes.volume import Volume +88 88 | from airflow.kubernetes.volume_mount import VolumeMount + 89 |+from kubernetes.client.models import V1VolumeMount +89 90 | +90 91 | PodDefaults() +91 92 | PodLauncher() +-------------------------------------------------------------------------------- +96 97 | K8SModel() +97 98 | Secret() +98 99 | Volume() +99 |-VolumeMount() + 100 |+V1VolumeMount() +100 101 | +101 102 | from airflow.kubernetes.kubernetes_helper_functions import ( +102 103 | annotations_to_key, + +AIR302_kubernetes.py:107:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.annotations_to_key` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +105 | ) +106 | +107 | annotations_to_key() + | ^^^^^^^^^^^^^^^^^^ AIR302 +108 | get_logs_task_metadata() +109 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `annotations_to_key` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Unsafe fix +99 99 | VolumeMount() +100 100 | +101 101 | from airflow.kubernetes.kubernetes_helper_functions import ( +102 |- annotations_to_key, +103 102 | get_logs_task_metadata, +104 103 | rand_str, +105 104 | ) + 105 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import annotations_to_key +106 106 | +107 107 | annotations_to_key() +108 108 | get_logs_task_metadata() + +AIR302_kubernetes.py:108:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +107 | annotations_to_key() +108 | get_logs_task_metadata() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +109 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `get_logs_task_metadata` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Unsafe fix +100 100 | +101 101 | from airflow.kubernetes.kubernetes_helper_functions import ( +102 102 | annotations_to_key, +103 |- get_logs_task_metadata, +104 103 | rand_str, +105 104 | ) + 105 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import get_logs_task_metadata +106 106 | +107 107 | annotations_to_key() +108 108 | get_logs_task_metadata() + +AIR302_kubernetes.py:109:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +107 | annotations_to_key() +108 | get_logs_task_metadata() +109 | rand_str() + | ^^^^^^^^ AIR302 +110 | +111 | from airflow.kubernetes.pod_generator import PodGeneratorDeprecated + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `rand_str` from `airflow.providers.cncf.kubernetes.kubernetes_helper_functions` instead. + +ℹ Unsafe fix +101 101 | from airflow.kubernetes.kubernetes_helper_functions import ( +102 102 | annotations_to_key, +103 103 | get_logs_task_metadata, +104 |- rand_str, +105 104 | ) + 105 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import rand_str +106 106 | +107 107 | annotations_to_key() +108 108 | get_logs_task_metadata() + +AIR302_kubernetes.py:113:1: AIR302 [*] `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +111 | from airflow.kubernetes.pod_generator import PodGeneratorDeprecated +112 | +113 | PodGeneratorDeprecated() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `PodGenerator` from `airflow.providers.cncf.kubernetes.pod_generator` instead. + +ℹ Unsafe fix +109 109 | rand_str() +110 110 | +111 111 | from airflow.kubernetes.pod_generator import PodGeneratorDeprecated + 112 |+from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator +112 113 | +113 114 | PodGeneratorDeprecated() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap new file mode 100644 index 0000000000000..7dee3ad223061 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_mysql.py:9:1: AIR302 [*] `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysql` provider in Airflow 3.0; + | + 7 | ) + 8 | + 9 | MySqlHook() + | ^^^^^^^^^ AIR302 +10 | PrestoToMySqlOperator() +11 | PrestoToMySqlTransfer() + | + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `MySqlHook` from `airflow.providers.mysql.hooks.mysql` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.mysql_hook import MySqlHook +4 3 | from airflow.operators.presto_to_mysql import ( +5 4 | PrestoToMySqlOperator, +6 5 | PrestoToMySqlTransfer, +7 6 | ) + 7 |+from airflow.providers.mysql.hooks.mysql import MySqlHook +8 8 | +9 9 | MySqlHook() +10 10 | PrestoToMySqlOperator() + +AIR302_mysql.py:10:1: AIR302 [*] `airflow.operators.presto_to_mysql.PrestoToMySqlOperator` is moved into `mysql` provider in Airflow 3.0; + | + 9 | MySqlHook() +10 | PrestoToMySqlOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +11 | PrestoToMySqlTransfer() + | + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `PrestoToMySqlOperator` from `airflow.providers.mysql.transfers.presto_to_mysql` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.mysql_hook import MySqlHook +4 4 | from airflow.operators.presto_to_mysql import ( +5 |- PrestoToMySqlOperator, +6 5 | PrestoToMySqlTransfer, +7 6 | ) + 7 |+from airflow.providers.mysql.transfers.presto_to_mysql import PrestoToMySqlOperator +8 8 | +9 9 | MySqlHook() +10 10 | PrestoToMySqlOperator() + +AIR302_mysql.py:11:1: AIR302 [*] `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer` is moved into `mysql` provider in Airflow 3.0; + | + 9 | MySqlHook() +10 | PrestoToMySqlOperator() +11 | PrestoToMySqlTransfer() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `PrestoToMySqlOperator` from `airflow.providers.mysql.transfers.presto_to_mysql` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.mysql_hook import MySqlHook +4 4 | from airflow.operators.presto_to_mysql import ( +5 |- PrestoToMySqlOperator, +6 5 | PrestoToMySqlTransfer, +7 6 | ) + 7 |+from airflow.providers.mysql.transfers.presto_to_mysql import PrestoToMySqlOperator +8 8 | +9 9 | MySqlHook() +10 10 | PrestoToMySqlOperator() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap new file mode 100644 index 0000000000000..c29dc82076b18 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_oracle.py:5:1: AIR302 [*] `airflow.hooks.oracle_hook.OracleHook` is moved into `oracle` provider in Airflow 3.0; + | +3 | from airflow.hooks.oracle_hook import OracleHook +4 | +5 | OracleHook() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `OracleHook` from `airflow.providers.oracle.hooks.oracle` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.oracle_hook import OracleHook + 3 |+from airflow.providers.oracle.hooks.oracle import OracleHook +4 4 | +5 5 | OracleHook() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap new file mode 100644 index 0000000000000..99e06729e5924 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_papermill.py:5:1: AIR302 [*] `airflow.operators.papermill_operator.PapermillOperator` is moved into `papermill` provider in Airflow 3.0; + | +3 | from airflow.operators.papermill_operator import PapermillOperator +4 | +5 | PapermillOperator() + | ^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-papermill>=1.0.0` and use `PapermillOperator` from `airflow.providers.papermill.operators.papermill` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.operators.papermill_operator import PapermillOperator + 3 |+from airflow.providers.papermill.operators.papermill import PapermillOperator +4 4 | +5 5 | PapermillOperator() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap new file mode 100644 index 0000000000000..a9e0812af0fc9 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_pig.py:6:1: AIR302 [*] `airflow.hooks.pig_hook.PigCliHook` is moved into `apache-pig` provider in Airflow 3.0; + | +4 | from airflow.operators.pig_operator import PigOperator +5 | +6 | PigCliHook() + | ^^^^^^^^^^ AIR302 +7 | PigOperator() + | + = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `PigCliHook` from `airflow.providers.apache.pig.hooks.pig` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.pig_hook import PigCliHook +4 3 | from airflow.operators.pig_operator import PigOperator + 4 |+from airflow.providers.apache.pig.hooks.pig import PigCliHook +5 5 | +6 6 | PigCliHook() +7 7 | PigOperator() + +AIR302_pig.py:7:1: AIR302 [*] `airflow.operators.pig_operator.PigOperator` is moved into `apache-pig` provider in Airflow 3.0; + | +6 | PigCliHook() +7 | PigOperator() + | ^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `PigOperator` from `airflow.providers.apache.pig.operators.pig` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.pig_hook import PigCliHook +4 |-from airflow.operators.pig_operator import PigOperator + 4 |+from airflow.providers.apache.pig.operators.pig import PigOperator +5 5 | +6 6 | PigCliHook() +7 7 | PigOperator() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap new file mode 100644 index 0000000000000..6f026c04651e4 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_postgres.py:6:1: AIR302 [*] `airflow.hooks.postgres_hook.PostgresHook` is moved into `postgres` provider in Airflow 3.0; + | +4 | from airflow.operators.postgres_operator import Mapping +5 | +6 | PostgresHook() + | ^^^^^^^^^^^^ AIR302 +7 | Mapping() + | + = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `PostgresHook` from `airflow.providers.postgres.hooks.postgres` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.postgres_hook import PostgresHook +4 3 | from airflow.operators.postgres_operator import Mapping + 4 |+from airflow.providers.postgres.hooks.postgres import PostgresHook +5 5 | +6 6 | PostgresHook() +7 7 | Mapping() + +AIR302_postgres.py:7:1: AIR302 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0 + | +6 | PostgresHook() +7 | Mapping() + | ^^^^^^^ AIR302 + | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap new file mode 100644 index 0000000000000..3c0117a196b90 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_presto.py:5:1: AIR302 [*] `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provider in Airflow 3.0; + | +3 | from airflow.hooks.presto_hook import PrestoHook +4 | +5 | PrestoHook() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-presto>=1.0.0` and use `PrestoHook` from `airflow.providers.presto.hooks.presto` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.presto_hook import PrestoHook + 3 |+from airflow.providers.presto.hooks.presto import PrestoHook +4 4 | +5 5 | PrestoHook() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap new file mode 100644 index 0000000000000..02ec8f73138d5 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_samba.py:5:1: AIR302 [*] `airflow.hooks.samba_hook.SambaHook` is moved into `samba` provider in Airflow 3.0; + | +3 | from airflow.hooks.samba_hook import SambaHook +4 | +5 | SambaHook() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-samba>=1.0.0` and use `SambaHook` from `airflow.providers.samba.hooks.samba` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.samba_hook import SambaHook + 3 |+from airflow.providers.samba.hooks.samba import SambaHook +4 4 | +5 5 | SambaHook() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap new file mode 100644 index 0000000000000..fbb1c5033c424 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_slack.py:6:1: AIR302 [*] `airflow.hooks.slack_hook.SlackHook` is moved into `slack` provider in Airflow 3.0; + | +4 | from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator +5 | +6 | SlackHook() + | ^^^^^^^^^ AIR302 +7 | SlackAPIOperator() +8 | SlackAPIPostOperator() + | + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `SlackHook` from `airflow.providers.slack.hooks.slack` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.slack_hook import SlackHook +4 3 | from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator + 4 |+from airflow.providers.slack.hooks.slack import SlackHook +5 5 | +6 6 | SlackHook() +7 7 | SlackAPIOperator() + +AIR302_slack.py:7:1: AIR302 [*] `airflow.operators.slack_operator.SlackAPIOperator` is moved into `slack` provider in Airflow 3.0; + | +6 | SlackHook() +7 | SlackAPIOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +8 | SlackAPIPostOperator() + | + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `SlackAPIOperator` from `airflow.providers.slack.operators.slack` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.slack_hook import SlackHook +4 |-from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator + 4 |+from airflow.operators.slack_operator import SlackAPIPostOperator + 5 |+from airflow.providers.slack.operators.slack import SlackAPIOperator +5 6 | +6 7 | SlackHook() +7 8 | SlackAPIOperator() + +AIR302_slack.py:8:1: AIR302 [*] `airflow.operators.slack_operator.SlackAPIPostOperator` is moved into `slack` provider in Airflow 3.0; + | +6 | SlackHook() +7 | SlackAPIOperator() +8 | SlackAPIPostOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `SlackAPIPostOperator` from `airflow.providers.slack.operators.slack` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.slack_hook import SlackHook +4 |-from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator + 4 |+from airflow.operators.slack_operator import SlackAPIOperator + 5 |+from airflow.providers.slack.operators.slack import SlackAPIPostOperator +5 6 | +6 7 | SlackHook() +7 8 | SlackAPIOperator() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap new file mode 100644 index 0000000000000..ed4184860418b --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_smtp.py:5:1: AIR302 [*] `airflow.operators.email_operator.EmailOperator` is moved into `smtp` provider in Airflow 3.0; + | +3 | from airflow.operators.email_operator import EmailOperator +4 | +5 | EmailOperator() + | ^^^^^^^^^^^^^ AIR302 +6 | +7 | from airflow.operators.email import EmailOperator + | + = help: Install `apache-airflow-providers-smtp>=1.0.0` and use `EmailOperator` from `airflow.providers.smtp.operators.smtp` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.operators.email_operator import EmailOperator + 3 |+from airflow.providers.smtp.operators.smtp import EmailOperator +4 4 | +5 5 | EmailOperator() +6 6 | + +AIR302_smtp.py:9:1: AIR302 [*] `airflow.operators.email.EmailOperator` is moved into `smtp` provider in Airflow 3.0; + | +7 | from airflow.operators.email import EmailOperator +8 | +9 | EmailOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-smtp>=1.0.0` and use `EmailOperator` from `airflow.providers.smtp.operators.smtp` instead. + +ℹ Unsafe fix +4 4 | +5 5 | EmailOperator() +6 6 | +7 |-from airflow.operators.email import EmailOperator + 7 |+from airflow.providers.smtp.operators.smtp import EmailOperator +8 8 | +9 9 | EmailOperator() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap new file mode 100644 index 0000000000000..543d17fc0c2bf --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_sqlite.py:5:1: AIR302 [*] `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sqlite` provider in Airflow 3.0; + | +3 | from airflow.hooks.sqlite_hook import SqliteHook +4 | +5 | SqliteHook() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `SqliteHook` from `airflow.providers.sqlite.hooks.sqlite` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.sqlite_hook import SqliteHook + 3 |+from airflow.providers.sqlite.hooks.sqlite import SqliteHook +4 4 | +5 5 | SqliteHook() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap new file mode 100644 index 0000000000000..4ad415626de33 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap @@ -0,0 +1,490 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_standard.py:20:1: AIR302 [*] `airflow.operators.bash_operator.BashOperator` is moved into `standard` provider in Airflow 3.0; + | +18 | ) +19 | +20 | BashOperator() + | ^^^^^^^^^^^^ AIR302 +21 | +22 | TriggerDagRunLink() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `BashOperator` from `airflow.providers.standard.operators.bash` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.operators.bash_operator import BashOperator +4 3 | from airflow.operators.dagrun_operator import ( +5 4 | TriggerDagRunLink, +6 5 | TriggerDagRunOperator, +-------------------------------------------------------------------------------- +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.bash import BashOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:22:1: AIR302 [*] `airflow.operators.dagrun_operator.TriggerDagRunLink` is moved into `standard` provider in Airflow 3.0; + | +20 | BashOperator() +21 | +22 | TriggerDagRunLink() + | ^^^^^^^^^^^^^^^^^ AIR302 +23 | TriggerDagRunOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `TriggerDagRunLink` from `airflow.providers.standard.operators.trigger_dagrun` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.operators.bash_operator import BashOperator +4 4 | from airflow.operators.dagrun_operator import ( +5 |- TriggerDagRunLink, +6 5 | TriggerDagRunOperator, +7 6 | ) +8 7 | from airflow.operators.latest_only_operator import LatestOnlyOperator +-------------------------------------------------------------------------------- +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunLink +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:23:1: AIR302 [*] `airflow.operators.dagrun_operator.TriggerDagRunOperator` is moved into `standard` provider in Airflow 3.0; + | +22 | TriggerDagRunLink() +23 | TriggerDagRunOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +24 | +25 | LatestOnlyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `TriggerDagRunOperator` from `airflow.providers.standard.operators.trigger_dagrun` instead. + +ℹ Unsafe fix +3 3 | from airflow.operators.bash_operator import BashOperator +4 4 | from airflow.operators.dagrun_operator import ( +5 5 | TriggerDagRunLink, +6 |- TriggerDagRunOperator, +7 6 | ) +8 7 | from airflow.operators.latest_only_operator import LatestOnlyOperator +9 8 | from airflow.operators.python_operator import ( +-------------------------------------------------------------------------------- +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:25:1: AIR302 [*] `airflow.operators.latest_only_operator.LatestOnlyOperator` is moved into `standard` provider in Airflow 3.0; + | +23 | TriggerDagRunOperator() +24 | +25 | LatestOnlyOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +26 | +27 | BranchPythonOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `LatestOnlyOperator` from `airflow.providers.standard.operators.latest_only` instead. + +ℹ Unsafe fix +5 5 | TriggerDagRunLink, +6 6 | TriggerDagRunOperator, +7 7 | ) +8 |-from airflow.operators.latest_only_operator import LatestOnlyOperator +9 8 | from airflow.operators.python_operator import ( +10 9 | BranchPythonOperator, +11 10 | PythonOperator, +-------------------------------------------------------------------------------- +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.latest_only import LatestOnlyOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:27:1: AIR302 [*] `airflow.operators.python_operator.BranchPythonOperator` is moved into `standard` provider in Airflow 3.0; + | +25 | LatestOnlyOperator() +26 | +27 | BranchPythonOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +28 | PythonOperator() +29 | PythonVirtualenvOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `BranchPythonOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +7 7 | ) +8 8 | from airflow.operators.latest_only_operator import LatestOnlyOperator +9 9 | from airflow.operators.python_operator import ( +10 |- BranchPythonOperator, +11 10 | PythonOperator, +12 11 | PythonVirtualenvOperator, +13 12 | ShortCircuitOperator, +-------------------------------------------------------------------------------- +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.python import BranchPythonOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:28:1: AIR302 [*] `airflow.operators.python_operator.PythonOperator` is moved into `standard` provider in Airflow 3.0; + | +27 | BranchPythonOperator() +28 | PythonOperator() + | ^^^^^^^^^^^^^^ AIR302 +29 | PythonVirtualenvOperator() +30 | ShortCircuitOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `PythonOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +8 8 | from airflow.operators.latest_only_operator import LatestOnlyOperator +9 9 | from airflow.operators.python_operator import ( +10 10 | BranchPythonOperator, +11 |- PythonOperator, +12 11 | PythonVirtualenvOperator, +13 12 | ShortCircuitOperator, +14 13 | ) +-------------------------------------------------------------------------------- +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.python import PythonOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:29:1: AIR302 [*] `airflow.operators.python_operator.PythonVirtualenvOperator` is moved into `standard` provider in Airflow 3.0; + | +27 | BranchPythonOperator() +28 | PythonOperator() +29 | PythonVirtualenvOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +30 | ShortCircuitOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `PythonVirtualenvOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +9 9 | from airflow.operators.python_operator import ( +10 10 | BranchPythonOperator, +11 11 | PythonOperator, +12 |- PythonVirtualenvOperator, +13 12 | ShortCircuitOperator, +14 13 | ) +15 14 | from airflow.sensors.external_task_sensor import ( +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.python import PythonVirtualenvOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:30:1: AIR302 [*] `airflow.operators.python_operator.ShortCircuitOperator` is moved into `standard` provider in Airflow 3.0; + | +28 | PythonOperator() +29 | PythonVirtualenvOperator() +30 | ShortCircuitOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +31 | +32 | ExternalTaskMarker() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `ShortCircuitOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +10 10 | BranchPythonOperator, +11 11 | PythonOperator, +12 12 | PythonVirtualenvOperator, +13 |- ShortCircuitOperator, +14 13 | ) +15 14 | from airflow.sensors.external_task_sensor import ( +16 15 | ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.operators.python import ShortCircuitOperator +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:32:1: AIR302 [*] `airflow.sensors.external_task_sensor.ExternalTaskMarker` is moved into `standard` provider in Airflow 3.0; + | +30 | ShortCircuitOperator() +31 | +32 | ExternalTaskMarker() + | ^^^^^^^^^^^^^^^^^^ AIR302 +33 | ExternalTaskSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalTaskMarker` from `airflow.providers.standard.sensors.external_task` instead. + +ℹ Unsafe fix +13 13 | ShortCircuitOperator, +14 14 | ) +15 15 | from airflow.sensors.external_task_sensor import ( +16 |- ExternalTaskMarker, +17 16 | ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.sensors.external_task import ExternalTaskMarker +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:33:1: AIR302 [*] `airflow.sensors.external_task_sensor.ExternalTaskSensor` is moved into `standard` provider in Airflow 3.0; + | +32 | ExternalTaskMarker() +33 | ExternalTaskSensor() + | ^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalTaskSensor` from `airflow.providers.standard.sensors.external_task` instead. + +ℹ Unsafe fix +14 14 | ) +15 15 | from airflow.sensors.external_task_sensor import ( +16 16 | ExternalTaskMarker, +17 |- ExternalTaskSensor, +18 17 | ) + 18 |+from airflow.providers.standard.sensors.external_task import ExternalTaskSensor +19 19 | +20 20 | BashOperator() +21 21 | + +AIR302_standard.py:38:1: AIR302 [*] `airflow.hooks.subprocess.SubprocessResult` is moved into `standard` provider in Airflow 3.0; + | +36 | from airflow.hooks.subprocess import SubprocessResult +37 | +38 | SubprocessResult() + | ^^^^^^^^^^^^^^^^ AIR302 +39 | +40 | from airflow.hooks.subprocess import working_directory + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `SubprocessResult` from `airflow.providers.standard.hooks.subprocess` instead. + +ℹ Unsafe fix +33 33 | ExternalTaskSensor() +34 34 | +35 35 | +36 |-from airflow.hooks.subprocess import SubprocessResult + 36 |+from airflow.providers.standard.hooks.subprocess import SubprocessResult +37 37 | +38 38 | SubprocessResult() +39 39 | + +AIR302_standard.py:42:1: AIR302 [*] `airflow.hooks.subprocess.working_directory` is moved into `standard` provider in Airflow 3.0; + | +40 | from airflow.hooks.subprocess import working_directory +41 | +42 | working_directory() + | ^^^^^^^^^^^^^^^^^ AIR302 +43 | +44 | from airflow.operators.datetime import target_times_as_dates + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `working_directory` from `airflow.providers.standard.hooks.subprocess` instead. + +ℹ Unsafe fix +37 37 | +38 38 | SubprocessResult() +39 39 | +40 |-from airflow.hooks.subprocess import working_directory + 40 |+from airflow.providers.standard.hooks.subprocess import working_directory +41 41 | +42 42 | working_directory() +43 43 | + +AIR302_standard.py:46:1: AIR302 [*] `airflow.operators.datetime.target_times_as_dates` is moved into `standard` provider in Airflow 3.0; + | +44 | from airflow.operators.datetime import target_times_as_dates +45 | +46 | target_times_as_dates() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +47 | +48 | from airflow.operators.trigger_dagrun import TriggerDagRunLink + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `target_times_as_dates` from `airflow.providers.standard.operators.datetime` instead. + +ℹ Unsafe fix +41 41 | +42 42 | working_directory() +43 43 | +44 |-from airflow.operators.datetime import target_times_as_dates + 44 |+from airflow.providers.standard.operators.datetime import target_times_as_dates +45 45 | +46 46 | target_times_as_dates() +47 47 | + +AIR302_standard.py:50:1: AIR302 [*] `airflow.operators.trigger_dagrun.TriggerDagRunLink` is moved into `standard` provider in Airflow 3.0; + | +48 | from airflow.operators.trigger_dagrun import TriggerDagRunLink +49 | +50 | TriggerDagRunLink() + | ^^^^^^^^^^^^^^^^^ AIR302 +51 | +52 | from airflow.sensors.external_task import ExternalTaskSensorLink + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `TriggerDagRunLink` from `airflow.providers.standard.operators.trigger_dagrun` instead. + +ℹ Unsafe fix +45 45 | +46 46 | target_times_as_dates() +47 47 | +48 |-from airflow.operators.trigger_dagrun import TriggerDagRunLink + 48 |+from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunLink +49 49 | +50 50 | TriggerDagRunLink() +51 51 | + +AIR302_standard.py:54:1: AIR302 [*] `airflow.sensors.external_task.ExternalTaskSensorLink` is moved into `standard` provider in Airflow 3.0; + | +52 | from airflow.sensors.external_task import ExternalTaskSensorLink +53 | +54 | ExternalTaskSensorLink() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +55 | +56 | from airflow.sensors.time_delta import WaitSensor + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalDagLink` from `airflow.providers.standard.sensors.external_task` instead. + +ℹ Safe fix +50 50 | TriggerDagRunLink() +51 51 | +52 52 | from airflow.sensors.external_task import ExternalTaskSensorLink + 53 |+from airflow.providers.standard.sensors.external_task import ExternalDagLink +53 54 | +54 |-ExternalTaskSensorLink() + 55 |+ExternalDagLink() +55 56 | +56 57 | from airflow.sensors.time_delta import WaitSensor +57 58 | + +AIR302_standard.py:58:1: AIR302 [*] `airflow.sensors.time_delta.WaitSensor` is moved into `standard` provider in Airflow 3.0; + | +56 | from airflow.sensors.time_delta import WaitSensor +57 | +58 | WaitSensor() + | ^^^^^^^^^^ AIR302 +59 | +60 | from airflow.operators.dummy import DummyOperator + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `WaitSensor` from `airflow.providers.standard.sensors.time_delta` instead. + +ℹ Unsafe fix +53 53 | +54 54 | ExternalTaskSensorLink() +55 55 | +56 |-from airflow.sensors.time_delta import WaitSensor + 56 |+from airflow.providers.standard.sensors.time_delta import WaitSensor +57 57 | +58 58 | WaitSensor() +59 59 | + +AIR302_standard.py:62:1: AIR302 [*] `airflow.operators.dummy.DummyOperator` is moved into `standard` provider in Airflow 3.0; + | +60 | from airflow.operators.dummy import DummyOperator +61 | +62 | DummyOperator() + | ^^^^^^^^^^^^^ AIR302 +63 | +64 | from airflow.operators.dummy import EmptyOperator + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `EmptyOperator` from `airflow.providers.standard.operators.empty` instead. + +ℹ Safe fix +58 58 | WaitSensor() +59 59 | +60 60 | from airflow.operators.dummy import DummyOperator + 61 |+from airflow.providers.standard.operators.empty import EmptyOperator +61 62 | +62 |-DummyOperator() + 63 |+EmptyOperator() +63 64 | +64 65 | from airflow.operators.dummy import EmptyOperator +65 66 | + +AIR302_standard.py:66:1: AIR302 [*] `airflow.operators.dummy.EmptyOperator` is moved into `standard` provider in Airflow 3.0; + | +64 | from airflow.operators.dummy import EmptyOperator +65 | +66 | EmptyOperator() + | ^^^^^^^^^^^^^ AIR302 +67 | +68 | from airflow.operators.dummy_operator import DummyOperator + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `EmptyOperator` from `airflow.providers.standard.operators.empty` instead. + +ℹ Unsafe fix +61 61 | +62 62 | DummyOperator() +63 63 | +64 |-from airflow.operators.dummy import EmptyOperator + 64 |+from airflow.providers.standard.operators.empty import EmptyOperator +65 65 | +66 66 | EmptyOperator() +67 67 | + +AIR302_standard.py:70:1: AIR302 [*] `airflow.operators.dummy_operator.DummyOperator` is moved into `standard` provider in Airflow 3.0; + | +68 | from airflow.operators.dummy_operator import DummyOperator +69 | +70 | DummyOperator() + | ^^^^^^^^^^^^^ AIR302 +71 | +72 | from airflow.operators.dummy_operator import EmptyOperator + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `EmptyOperator` from `airflow.providers.standard.operators.empty` instead. + +ℹ Unsafe fix +66 66 | EmptyOperator() +67 67 | +68 68 | from airflow.operators.dummy_operator import DummyOperator + 69 |+from airflow.providers.standard.operators.empty import EmptyOperator +69 70 | +70 71 | DummyOperator() +71 72 | + +AIR302_standard.py:74:1: AIR302 [*] `airflow.operators.dummy_operator.EmptyOperator` is moved into `standard` provider in Airflow 3.0; + | +72 | from airflow.operators.dummy_operator import EmptyOperator +73 | +74 | EmptyOperator() + | ^^^^^^^^^^^^^ AIR302 +75 | +76 | from airflow.sensors.external_task_sensor import ExternalTaskSensorLink + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `EmptyOperator` from `airflow.providers.standard.operators.empty` instead. + +ℹ Unsafe fix +69 69 | +70 70 | DummyOperator() +71 71 | +72 |-from airflow.operators.dummy_operator import EmptyOperator + 72 |+from airflow.providers.standard.operators.empty import EmptyOperator +73 73 | +74 74 | EmptyOperator() +75 75 | + +AIR302_standard.py:78:1: AIR302 [*] `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is moved into `standard` provider in Airflow 3.0; + | +76 | from airflow.sensors.external_task_sensor import ExternalTaskSensorLink +77 | +78 | ExternalTaskSensorLink() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalDagLink` from `airflow.providers.standard.sensors.external_task` instead. + +ℹ Safe fix +74 74 | EmptyOperator() +75 75 | +76 76 | from airflow.sensors.external_task_sensor import ExternalTaskSensorLink + 77 |+from airflow.providers.standard.sensors.external_task import ExternalDagLink +77 78 | +78 |-ExternalTaskSensorLink() + 79 |+ExternalDagLink() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_try.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_try.py.snap new file mode 100644 index 0000000000000..6d6d3986fe9dd --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_try.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap new file mode 100644 index 0000000000000..e0ee88653f4db --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_zendesk.py:5:1: AIR302 [*] `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` provider in Airflow 3.0; + | +3 | from airflow.hooks.zendesk_hook import ZendeskHook +4 | +5 | ZendeskHook() + | ^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-zendesk>=1.0.0` and use `ZendeskHook` from `airflow.providers.zendesk.hooks.zendesk` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.zendesk_hook import ZendeskHook + 3 |+from airflow.providers.zendesk.hooks.zendesk import ZendeskHook +4 4 | +5 5 | ZendeskHook() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap index 09cf57bee8a54..b34b16ffcc62c 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap @@ -1,248 +1,604 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- -AIR311_names.py:23:1: AIR311 [*] `airflow.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:20:1: AIR311 [*] `airflow.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -22 | # airflow -23 | DatasetFromRoot() +19 | # airflow +20 | DatasetFromRoot() | ^^^^^^^^^^^^^^^ AIR311 -24 | -25 | # airflow.datasets +21 | +22 | # airflow.datasets | - = help: Use `airflow.sdk.Asset` instead + = help: Use `Asset` from `airflow.sdk` instead. ℹ Safe fix -18 18 | from airflow.models.dag import DAG as DAGFromDag -19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule -20 20 | from airflow.utils.dag_parsing_context import get_parsing_context - 21 |+from airflow.sdk import Asset +15 15 | task, +16 16 | task_group, +17 17 | ) + 18 |+from airflow.sdk import Asset +18 19 | +19 20 | # airflow +20 |-DatasetFromRoot() + 21 |+Asset() 21 22 | -22 23 | # airflow -23 |-DatasetFromRoot() - 24 |+Asset() -24 25 | -25 26 | # airflow.datasets -26 27 | Dataset() +22 23 | # airflow.datasets +23 24 | Dataset() -AIR311_names.py:26:1: AIR311 [*] `airflow.datasets.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:23:1: AIR311 [*] `airflow.datasets.Dataset` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -25 | # airflow.datasets -26 | Dataset() +22 | # airflow.datasets +23 | Dataset() | ^^^^^^^ AIR311 -27 | DatasetAlias() -28 | DatasetAll() +24 | DatasetAlias() +25 | DatasetAll() | - = help: Use `airflow.sdk.Asset` instead + = help: Use `Asset` from `airflow.sdk` instead. ℹ Safe fix -18 18 | from airflow.models.dag import DAG as DAGFromDag -19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule -20 20 | from airflow.utils.dag_parsing_context import get_parsing_context - 21 |+from airflow.sdk import Asset +15 15 | task, +16 16 | task_group, +17 17 | ) + 18 |+from airflow.sdk import Asset +18 19 | +19 20 | # airflow +20 21 | DatasetFromRoot() 21 22 | -22 23 | # airflow -23 24 | DatasetFromRoot() -24 25 | -25 26 | # airflow.datasets -26 |-Dataset() - 27 |+Asset() -27 28 | DatasetAlias() -28 29 | DatasetAll() -29 30 | DatasetAny() - -AIR311_names.py:27:1: AIR311 `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -25 | # airflow.datasets -26 | Dataset() -27 | DatasetAlias() +22 23 | # airflow.datasets +23 |-Dataset() + 24 |+Asset() +24 25 | DatasetAlias() +25 26 | DatasetAll() +26 27 | DatasetAny() + +AIR311_names.py:24:1: AIR311 [*] `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +22 | # airflow.datasets +23 | Dataset() +24 | DatasetAlias() | ^^^^^^^^^^^^ AIR311 -28 | DatasetAll() -29 | DatasetAny() +25 | DatasetAll() +26 | DatasetAny() | - = help: Use `airflow.sdk.AssetAlias` instead + = help: Use `AssetAlias` from `airflow.sdk` instead. + +ℹ Safe fix +15 15 | task, +16 16 | task_group, +17 17 | ) + 18 |+from airflow.sdk import AssetAlias +18 19 | +19 20 | # airflow +20 21 | DatasetFromRoot() +21 22 | +22 23 | # airflow.datasets +23 24 | Dataset() +24 |-DatasetAlias() + 25 |+AssetAlias() +25 26 | DatasetAll() +26 27 | DatasetAny() +27 28 | Metadata() -AIR311_names.py:28:1: AIR311 `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:25:1: AIR311 [*] `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -26 | Dataset() -27 | DatasetAlias() -28 | DatasetAll() +23 | Dataset() +24 | DatasetAlias() +25 | DatasetAll() | ^^^^^^^^^^ AIR311 -29 | DatasetAny() -30 | Metadata() +26 | DatasetAny() +27 | Metadata() | - = help: Use `airflow.sdk.AssetAll` instead + = help: Use `AssetAll` from `airflow.sdk` instead. + +ℹ Safe fix +15 15 | task, +16 16 | task_group, +17 17 | ) + 18 |+from airflow.sdk import AssetAll +18 19 | +19 20 | # airflow +20 21 | DatasetFromRoot() +-------------------------------------------------------------------------------- +22 23 | # airflow.datasets +23 24 | Dataset() +24 25 | DatasetAlias() +25 |-DatasetAll() + 26 |+AssetAll() +26 27 | DatasetAny() +27 28 | Metadata() +28 29 | expand_alias_to_datasets() -AIR311_names.py:29:1: AIR311 `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:26:1: AIR311 [*] `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -27 | DatasetAlias() -28 | DatasetAll() -29 | DatasetAny() +24 | DatasetAlias() +25 | DatasetAll() +26 | DatasetAny() | ^^^^^^^^^^ AIR311 -30 | Metadata() -31 | expand_alias_to_datasets() +27 | Metadata() +28 | expand_alias_to_datasets() | - = help: Use `airflow.sdk.AssetAny` instead + = help: Use `AssetAny` from `airflow.sdk` instead. -AIR311_names.py:30:1: AIR311 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Safe fix +15 15 | task, +16 16 | task_group, +17 17 | ) + 18 |+from airflow.sdk import AssetAny +18 19 | +19 20 | # airflow +20 21 | DatasetFromRoot() +-------------------------------------------------------------------------------- +23 24 | Dataset() +24 25 | DatasetAlias() +25 26 | DatasetAll() +26 |-DatasetAny() + 27 |+AssetAny() +27 28 | Metadata() +28 29 | expand_alias_to_datasets() +29 30 | + +AIR311_names.py:27:1: AIR311 [*] `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -28 | DatasetAll() -29 | DatasetAny() -30 | Metadata() +25 | DatasetAll() +26 | DatasetAny() +27 | Metadata() | ^^^^^^^^ AIR311 -31 | expand_alias_to_datasets() +28 | expand_alias_to_datasets() | - = help: Use `airflow.sdk.Metadata` instead + = help: Use `Metadata` from `airflow.sdk` instead. + +ℹ Unsafe fix +8 8 | DatasetAny, +9 9 | expand_alias_to_datasets, +10 10 | ) +11 |-from airflow.datasets.metadata import Metadata +12 11 | from airflow.decorators import ( +13 12 | dag, +14 13 | setup, +15 14 | task, +16 15 | task_group, +17 16 | ) + 17 |+from airflow.sdk import Metadata +18 18 | +19 19 | # airflow +20 20 | DatasetFromRoot() -AIR311_names.py:31:1: AIR311 `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:28:1: AIR311 [*] `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -29 | DatasetAny() -30 | Metadata() -31 | expand_alias_to_datasets() +26 | DatasetAny() +27 | Metadata() +28 | expand_alias_to_datasets() | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR311 -32 | -33 | # airflow.decorators +29 | +30 | # airflow.decorators | - = help: Use `airflow.sdk.expand_alias_to_assets` instead + = help: Use `expand_alias_to_assets` from `airflow.models.asset` instead. -AIR311_names.py:34:1: AIR311 `airflow.decorators.dag` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Safe fix +15 15 | task, +16 16 | task_group, +17 17 | ) + 18 |+from airflow.models.asset import expand_alias_to_assets +18 19 | +19 20 | # airflow +20 21 | DatasetFromRoot() +-------------------------------------------------------------------------------- +25 26 | DatasetAll() +26 27 | DatasetAny() +27 28 | Metadata() +28 |-expand_alias_to_datasets() + 29 |+expand_alias_to_assets() +29 30 | +30 31 | # airflow.decorators +31 32 | dag() + +AIR311_names.py:31:1: AIR311 [*] `airflow.decorators.dag` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -33 | # airflow.decorators -34 | dag() +30 | # airflow.decorators +31 | dag() | ^^^ AIR311 -35 | task() -36 | task_group() +32 | task() +33 | task_group() | - = help: Use `airflow.sdk.dag` instead + = help: Use `dag` from `airflow.sdk` instead. + +ℹ Unsafe fix +10 10 | ) +11 11 | from airflow.datasets.metadata import Metadata +12 12 | from airflow.decorators import ( +13 |- dag, +14 13 | setup, +15 14 | task, +16 15 | task_group, +17 16 | ) + 17 |+from airflow.sdk import dag +18 18 | +19 19 | # airflow +20 20 | DatasetFromRoot() -AIR311_names.py:35:1: AIR311 `airflow.decorators.task` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:32:1: AIR311 [*] `airflow.decorators.task` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -33 | # airflow.decorators -34 | dag() -35 | task() +30 | # airflow.decorators +31 | dag() +32 | task() | ^^^^ AIR311 -36 | task_group() -37 | setup() +33 | task_group() +34 | setup() | - = help: Use `airflow.sdk.task` instead + = help: Use `task` from `airflow.sdk` instead. -AIR311_names.py:36:1: AIR311 `airflow.decorators.task_group` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Unsafe fix +12 12 | from airflow.decorators import ( +13 13 | dag, +14 14 | setup, +15 |- task, +16 15 | task_group, +17 16 | ) + 17 |+from airflow.sdk import task +18 18 | +19 19 | # airflow +20 20 | DatasetFromRoot() + +AIR311_names.py:33:1: AIR311 [*] `airflow.decorators.task_group` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -34 | dag() -35 | task() -36 | task_group() +31 | dag() +32 | task() +33 | task_group() | ^^^^^^^^^^ AIR311 -37 | setup() -38 | teardown() +34 | setup() +35 | from airflow.decorators import teardown | - = help: Use `airflow.sdk.task_group` instead + = help: Use `task_group` from `airflow.sdk` instead. + +ℹ Unsafe fix +13 13 | dag, +14 14 | setup, +15 15 | task, +16 |- task_group, +17 16 | ) + 17 |+from airflow.sdk import task_group +18 18 | +19 19 | # airflow +20 20 | DatasetFromRoot() -AIR311_names.py:37:1: AIR311 `airflow.decorators.setup` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:34:1: AIR311 [*] `airflow.decorators.setup` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -35 | task() -36 | task_group() -37 | setup() +32 | task() +33 | task_group() +34 | setup() | ^^^^^ AIR311 -38 | teardown() +35 | from airflow.decorators import teardown +36 | from airflow.io.path import ObjectStoragePath | - = help: Use `airflow.sdk.setup` instead + = help: Use `setup` from `airflow.sdk` instead. + +ℹ Unsafe fix +11 11 | from airflow.datasets.metadata import Metadata +12 12 | from airflow.decorators import ( +13 13 | dag, +14 |- setup, +15 14 | task, +16 15 | task_group, +17 16 | ) + 17 |+from airflow.sdk import setup +18 18 | +19 19 | # airflow +20 20 | DatasetFromRoot() -AIR311_names.py:38:1: AIR311 `airflow.decorators.teardown` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:48:1: AIR311 [*] `airflow.decorators.teardown` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -36 | task_group() -37 | setup() -38 | teardown() +47 | # airflow.decorators +48 | teardown() | ^^^^^^^^ AIR311 -39 | -40 | # airflow.io +49 | +50 | # # airflow.io | - = help: Use `airflow.sdk.teardown` instead + = help: Use `teardown` from `airflow.sdk` instead. -AIR311_names.py:41:1: AIR311 `airflow.io.path.ObjectStoragePath` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Unsafe fix +32 32 | task() +33 33 | task_group() +34 34 | setup() +35 |-from airflow.decorators import teardown +36 35 | from airflow.io.path import ObjectStoragePath +37 36 | from airflow.io.storage import attach +38 37 | from airflow.models import DAG as DAGFromModel +-------------------------------------------------------------------------------- +43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 43 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 44 | from airflow.models.dag import DAG as DAGFromDag + 45 |+from airflow.sdk import teardown +46 46 | +47 47 | # airflow.decorators +48 48 | teardown() + +AIR311_names.py:51:1: AIR311 [*] `airflow.io.path.ObjectStoragePath` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -40 | # airflow.io -41 | ObjectStoragePath() +50 | # # airflow.io +51 | ObjectStoragePath() | ^^^^^^^^^^^^^^^^^ AIR311 -42 | attach() +52 | attach() | - = help: Use `airflow.sdk.ObjectStoragePath` instead + = help: Use `ObjectStoragePath` from `airflow.sdk` instead. + +ℹ Unsafe fix +33 33 | task_group() +34 34 | setup() +35 35 | from airflow.decorators import teardown +36 |-from airflow.io.path import ObjectStoragePath +37 36 | from airflow.io.storage import attach +38 37 | from airflow.models import DAG as DAGFromModel +39 38 | from airflow.models import ( +-------------------------------------------------------------------------------- +43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 43 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 44 | from airflow.models.dag import DAG as DAGFromDag + 45 |+from airflow.sdk import ObjectStoragePath +46 46 | +47 47 | # airflow.decorators +48 48 | teardown() -AIR311_names.py:42:1: AIR311 `airflow.io.storage.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:52:1: AIR311 [*] `airflow.io.storage.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -40 | # airflow.io -41 | ObjectStoragePath() -42 | attach() +50 | # # airflow.io +51 | ObjectStoragePath() +52 | attach() | ^^^^^^ AIR311 -43 | -44 | # airflow.models +53 | +54 | # airflow.models + | + = help: Use `attach` from `airflow.sdk.io` instead. + +ℹ Unsafe fix +34 34 | setup() +35 35 | from airflow.decorators import teardown +36 36 | from airflow.io.path import ObjectStoragePath +37 |-from airflow.io.storage import attach +38 37 | from airflow.models import DAG as DAGFromModel +39 38 | from airflow.models import ( +40 39 | Connection, +-------------------------------------------------------------------------------- +43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 43 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 44 | from airflow.models.dag import DAG as DAGFromDag + 45 |+from airflow.sdk.io import attach +46 46 | +47 47 | # airflow.decorators +48 48 | teardown() + +AIR311_names.py:55:1: AIR311 [*] `airflow.models.Connection` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | - = help: Use `airflow.sdk.io.attach` instead +54 | # airflow.models +55 | Connection() + | ^^^^^^^^^^ AIR311 +56 | DAGFromModel() +57 | Variable() + | + = help: Use `Connection` from `airflow.sdk` instead. + +ℹ Unsafe fix +37 37 | from airflow.io.storage import attach +38 38 | from airflow.models import DAG as DAGFromModel +39 39 | from airflow.models import ( +40 |- Connection, +41 40 | Variable, +42 41 | ) +43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 43 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 44 | from airflow.models.dag import DAG as DAGFromDag + 45 |+from airflow.sdk import Connection +46 46 | +47 47 | # airflow.decorators +48 48 | teardown() -AIR311_names.py:45:1: AIR311 `airflow.models.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:56:1: AIR311 [*] `airflow.models.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -44 | # airflow.models -45 | DAGFromModel() +54 | # airflow.models +55 | Connection() +56 | DAGFromModel() | ^^^^^^^^^^^^ AIR311 -46 | -47 | # airflow.models.baseoperator +57 | Variable() + | + = help: Use `DAG` from `airflow.sdk` instead. + +ℹ Safe fix +43 43 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 44 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 45 | from airflow.models.dag import DAG as DAGFromDag + 46 |+from airflow.sdk import DAG +46 47 | +47 48 | # airflow.decorators +48 49 | teardown() +-------------------------------------------------------------------------------- +53 54 | +54 55 | # airflow.models +55 56 | Connection() +56 |-DAGFromModel() + 57 |+DAG() +57 58 | Variable() +58 59 | +59 60 | # airflow.models.baseoperator + +AIR311_names.py:57:1: AIR311 [*] `airflow.models.Variable` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +55 | Connection() +56 | DAGFromModel() +57 | Variable() + | ^^^^^^^^ AIR311 +58 | +59 | # airflow.models.baseoperator | - = help: Use `airflow.sdk.DAG` instead + = help: Use `Variable` from `airflow.sdk` instead. -AIR311_names.py:48:1: AIR311 `airflow.models.baseoperator.chain` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Unsafe fix +38 38 | from airflow.models import DAG as DAGFromModel +39 39 | from airflow.models import ( +40 40 | Connection, +41 |- Variable, +42 41 | ) +43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 43 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 44 | from airflow.models.dag import DAG as DAGFromDag + 45 |+from airflow.sdk import Variable +46 46 | +47 47 | # airflow.decorators +48 48 | teardown() + +AIR311_names.py:60:1: AIR311 [*] `airflow.models.baseoperator.chain` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -47 | # airflow.models.baseoperator -48 | chain() +59 | # airflow.models.baseoperator +60 | chain() | ^^^^^ AIR311 -49 | chain_linear() -50 | cross_downstream() +61 | chain_linear() +62 | cross_downstream() | - = help: Use `airflow.sdk.chain` instead + = help: Use `chain` from `airflow.sdk` instead. + +ℹ Unsafe fix +40 40 | Connection, +41 41 | Variable, +42 42 | ) +43 |-from airflow.models.baseoperator import chain, chain_linear, cross_downstream + 43 |+from airflow.models.baseoperator import chain_linear, cross_downstream +44 44 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 45 | from airflow.models.dag import DAG as DAGFromDag + 46 |+from airflow.sdk import chain +46 47 | +47 48 | # airflow.decorators +48 49 | teardown() -AIR311_names.py:49:1: AIR311 `airflow.models.baseoperator.chain_linear` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:61:1: AIR311 [*] `airflow.models.baseoperator.chain_linear` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -47 | # airflow.models.baseoperator -48 | chain() -49 | chain_linear() +59 | # airflow.models.baseoperator +60 | chain() +61 | chain_linear() | ^^^^^^^^^^^^ AIR311 -50 | cross_downstream() +62 | cross_downstream() | - = help: Use `airflow.sdk.chain_linear` instead + = help: Use `chain_linear` from `airflow.sdk` instead. + +ℹ Unsafe fix +40 40 | Connection, +41 41 | Variable, +42 42 | ) +43 |-from airflow.models.baseoperator import chain, chain_linear, cross_downstream + 43 |+from airflow.models.baseoperator import chain, cross_downstream +44 44 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 45 | from airflow.models.dag import DAG as DAGFromDag + 46 |+from airflow.sdk import chain_linear +46 47 | +47 48 | # airflow.decorators +48 49 | teardown() -AIR311_names.py:50:1: AIR311 `airflow.models.baseoperator.cross_downstream` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:62:1: AIR311 [*] `airflow.models.baseoperator.cross_downstream` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -48 | chain() -49 | chain_linear() -50 | cross_downstream() +60 | chain() +61 | chain_linear() +62 | cross_downstream() | ^^^^^^^^^^^^^^^^ AIR311 -51 | -52 | # airflow.models.baseoperatolinker +63 | +64 | # airflow.models.baseoperatolinker | - = help: Use `airflow.sdk.cross_downstream` instead + = help: Use `cross_downstream` from `airflow.sdk` instead. -AIR311_names.py:56:1: AIR311 `airflow.models.dag.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Unsafe fix +40 40 | Connection, +41 41 | Variable, +42 42 | ) +43 |-from airflow.models.baseoperator import chain, chain_linear, cross_downstream + 43 |+from airflow.models.baseoperator import chain, chain_linear +44 44 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 45 | from airflow.models.dag import DAG as DAGFromDag + 46 |+from airflow.sdk import cross_downstream +46 47 | +47 48 | # airflow.decorators +48 49 | teardown() + +AIR311_names.py:65:1: AIR311 [*] `airflow.models.baseoperatorlink.BaseOperatorLink` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +64 | # airflow.models.baseoperatolinker +65 | BaseOperatorLink() + | ^^^^^^^^^^^^^^^^ AIR311 +66 | +67 | # airflow.models.dag + | + = help: Use `BaseOperatorLink` from `airflow.sdk` instead. + +ℹ Unsafe fix +41 41 | Variable, +42 42 | ) +43 43 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 |-from airflow.models.baseoperatorlink import BaseOperatorLink +45 44 | from airflow.models.dag import DAG as DAGFromDag + 45 |+from airflow.sdk import BaseOperatorLink +46 46 | +47 47 | # airflow.decorators +48 48 | teardown() + +AIR311_names.py:68:1: AIR311 [*] `airflow.models.dag.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -55 | # airflow.models.dag -56 | DAGFromDag() +67 | # airflow.models.dag +68 | DAGFromDag() | ^^^^^^^^^^ AIR311 -57 | # airflow.timetables.datasets -58 | DatasetOrTimeSchedule() +69 | from airflow.timetables.datasets import DatasetOrTimeSchedule +70 | from airflow.utils.dag_parsing_context import get_parsing_context | - = help: Use `airflow.sdk.DAG` instead + = help: Use `DAG` from `airflow.sdk` instead. -AIR311_names.py:58:1: AIR311 `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Safe fix +43 43 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream +44 44 | from airflow.models.baseoperatorlink import BaseOperatorLink +45 45 | from airflow.models.dag import DAG as DAGFromDag + 46 |+from airflow.sdk import DAG +46 47 | +47 48 | # airflow.decorators +48 49 | teardown() +-------------------------------------------------------------------------------- +65 66 | BaseOperatorLink() +66 67 | +67 68 | # airflow.models.dag +68 |-DAGFromDag() + 69 |+DAG() +69 70 | from airflow.timetables.datasets import DatasetOrTimeSchedule +70 71 | from airflow.utils.dag_parsing_context import get_parsing_context +71 72 | + +AIR311_names.py:73:1: AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -56 | DAGFromDag() -57 | # airflow.timetables.datasets -58 | DatasetOrTimeSchedule() +72 | # airflow.timetables.datasets +73 | DatasetOrTimeSchedule() | ^^^^^^^^^^^^^^^^^^^^^ AIR311 -59 | -60 | # airflow.utils.dag_parsing_context +74 | +75 | # airflow.utils.dag_parsing_context | - = help: Use `airflow.timetables.assets.AssetOrTimeSchedule` instead + = help: Use `AssetOrTimeSchedule` from `airflow.timetables.assets` instead. -AIR311_names.py:61:1: AIR311 `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Safe fix +68 68 | DAGFromDag() +69 69 | from airflow.timetables.datasets import DatasetOrTimeSchedule +70 70 | from airflow.utils.dag_parsing_context import get_parsing_context + 71 |+from airflow.timetables.assets import AssetOrTimeSchedule +71 72 | +72 73 | # airflow.timetables.datasets +73 |-DatasetOrTimeSchedule() + 74 |+AssetOrTimeSchedule() +74 75 | +75 76 | # airflow.utils.dag_parsing_context +76 77 | get_parsing_context() + +AIR311_names.py:76:1: AIR311 [*] `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -60 | # airflow.utils.dag_parsing_context -61 | get_parsing_context() +75 | # airflow.utils.dag_parsing_context +76 | get_parsing_context() | ^^^^^^^^^^^^^^^^^^^ AIR311 | - = help: Use `airflow.sdk.get_parsing_context` instead + = help: Use `get_parsing_context` from `airflow.sdk` instead. + +ℹ Unsafe fix +67 67 | # airflow.models.dag +68 68 | DAGFromDag() +69 69 | from airflow.timetables.datasets import DatasetOrTimeSchedule +70 |-from airflow.utils.dag_parsing_context import get_parsing_context + 70 |+from airflow.sdk import get_parsing_context +71 71 | +72 72 | # airflow.timetables.datasets +73 73 | DatasetOrTimeSchedule() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_try.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_try.py.snap new file mode 100644 index 0000000000000..6d6d3986fe9dd --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_try.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap index 71877501cdc17..e59605546d9cb 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap @@ -1,370 +1,713 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- -AIR312.py:32:1: AIR312 `airflow.hooks.filesystem.FSHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR312.py:14:1: AIR312 [*] `airflow.hooks.filesystem.FSHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | -30 | from airflow.triggers.temporal import DateTimeTrigger, TimeDeltaTrigger -31 | -32 | FSHook() +12 | from airflow.sensors.date_time import DateTimeSensor +13 | +14 | FSHook() | ^^^^^^ AIR312 -33 | PackageIndexHook() -34 | SubprocessHook(), SubprocessResult(), working_directory() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.hooks.filesystem.FSHook` instead. - -AIR312.py:33:1: AIR312 `airflow.hooks.package_index.PackageIndexHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -32 | FSHook() -33 | PackageIndexHook() +15 | PackageIndexHook() +16 | SubprocessHook() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `FSHook` from `airflow.providers.standard.hooks.filesystem` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.hooks.filesystem import FSHook +4 3 | from airflow.hooks.package_index import PackageIndexHook +5 4 | from airflow.hooks.subprocess import SubprocessHook +6 5 | from airflow.operators.bash import BashOperator +-------------------------------------------------------------------------------- +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.hooks.filesystem import FSHook +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:15:1: AIR312 [*] `airflow.hooks.package_index.PackageIndexHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +14 | FSHook() +15 | PackageIndexHook() | ^^^^^^^^^^^^^^^^ AIR312 -34 | SubprocessHook(), SubprocessResult(), working_directory() -35 | BashOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.hooks.package_index.PackageIndexHook` instead. - -AIR312.py:34:1: AIR312 `airflow.hooks.subprocess.SubprocessHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -32 | FSHook() -33 | PackageIndexHook() -34 | SubprocessHook(), SubprocessResult(), working_directory() +16 | SubprocessHook() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `PackageIndexHook` from `airflow.providers.standard.hooks.package_index` instead. + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from airflow.hooks.filesystem import FSHook +4 |-from airflow.hooks.package_index import PackageIndexHook +5 4 | from airflow.hooks.subprocess import SubprocessHook +6 5 | from airflow.operators.bash import BashOperator +7 6 | from airflow.operators.datetime import BranchDateTimeOperator +-------------------------------------------------------------------------------- +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.hooks.package_index import PackageIndexHook +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:16:1: AIR312 [*] `airflow.hooks.subprocess.SubprocessHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +14 | FSHook() +15 | PackageIndexHook() +16 | SubprocessHook() | ^^^^^^^^^^^^^^ AIR312 -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.SubprocessHook` instead. - -AIR312.py:34:19: AIR312 `airflow.hooks.subprocess.SubprocessResult` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -32 | FSHook() -33 | PackageIndexHook() -34 | SubprocessHook(), SubprocessResult(), working_directory() - | ^^^^^^^^^^^^^^^^ AIR312 -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.SubprocessResult` instead. - -AIR312.py:34:39: AIR312 `airflow.hooks.subprocess.working_directory` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -32 | FSHook() -33 | PackageIndexHook() -34 | SubprocessHook(), SubprocessResult(), working_directory() - | ^^^^^^^^^^^^^^^^^ AIR312 -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.working_directory` instead. - -AIR312.py:35:1: AIR312 `airflow.operators.bash.BashOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -33 | PackageIndexHook() -34 | SubprocessHook(), SubprocessResult(), working_directory() -35 | BashOperator() +17 | +18 | BashOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `SubprocessHook` from `airflow.providers.standard.hooks.subprocess` instead. + +ℹ Unsafe fix +2 2 | +3 3 | from airflow.hooks.filesystem import FSHook +4 4 | from airflow.hooks.package_index import PackageIndexHook +5 |-from airflow.hooks.subprocess import SubprocessHook +6 5 | from airflow.operators.bash import BashOperator +7 6 | from airflow.operators.datetime import BranchDateTimeOperator +8 7 | from airflow.operators.empty import EmptyOperator +-------------------------------------------------------------------------------- +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.hooks.subprocess import SubprocessHook +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:18:1: AIR312 [*] `airflow.operators.bash.BashOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +16 | SubprocessHook() +17 | +18 | BashOperator() | ^^^^^^^^^^^^ AIR312 -36 | BranchDateTimeOperator(), target_times_as_dates() -37 | TriggerDagRunLink(), TriggerDagRunOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. - -AIR312.py:36:1: AIR312 `airflow.operators.datetime.BranchDateTimeOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -34 | SubprocessHook(), SubprocessResult(), working_directory() -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() +19 | BranchDateTimeOperator() +20 | TriggerDagRunOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `BashOperator` from `airflow.providers.standard.operators.bash` instead. + +ℹ Unsafe fix +3 3 | from airflow.hooks.filesystem import FSHook +4 4 | from airflow.hooks.package_index import PackageIndexHook +5 5 | from airflow.hooks.subprocess import SubprocessHook +6 |-from airflow.operators.bash import BashOperator +7 6 | from airflow.operators.datetime import BranchDateTimeOperator +8 7 | from airflow.operators.empty import EmptyOperator +9 8 | from airflow.operators.latest_only import LatestOnlyOperator +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.operators.bash import BashOperator +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:19:1: AIR312 [*] `airflow.operators.datetime.BranchDateTimeOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +18 | BashOperator() +19 | BranchDateTimeOperator() | ^^^^^^^^^^^^^^^^^^^^^^ AIR312 -37 | TriggerDagRunLink(), TriggerDagRunOperator() -38 | EmptyOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.BranchDateTimeOperator` instead. - -AIR312.py:36:27: AIR312 `airflow.operators.datetime.target_times_as_dates` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -34 | SubprocessHook(), SubprocessResult(), working_directory() -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() - | ^^^^^^^^^^^^^^^^^^^^^ AIR312 -37 | TriggerDagRunLink(), TriggerDagRunOperator() -38 | EmptyOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.target_times_as_dates` instead. - -AIR312.py:37:1: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunLink` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() -37 | TriggerDagRunLink(), TriggerDagRunOperator() - | ^^^^^^^^^^^^^^^^^ AIR312 -38 | EmptyOperator() -39 | LatestOnlyOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunLink` instead. - -AIR312.py:37:22: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -35 | BashOperator() -36 | BranchDateTimeOperator(), target_times_as_dates() -37 | TriggerDagRunLink(), TriggerDagRunOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR312 -38 | EmptyOperator() -39 | LatestOnlyOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunOperator` instead. - -AIR312.py:38:1: AIR312 `airflow.operators.empty.EmptyOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -36 | BranchDateTimeOperator(), target_times_as_dates() -37 | TriggerDagRunLink(), TriggerDagRunOperator() -38 | EmptyOperator() +20 | TriggerDagRunOperator() +21 | EmptyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `BranchDateTimeOperator` from `airflow.providers.standard.operators.datetime` instead. + +ℹ Unsafe fix +4 4 | from airflow.hooks.package_index import PackageIndexHook +5 5 | from airflow.hooks.subprocess import SubprocessHook +6 6 | from airflow.operators.bash import BashOperator +7 |-from airflow.operators.datetime import BranchDateTimeOperator +8 7 | from airflow.operators.empty import EmptyOperator +9 8 | from airflow.operators.latest_only import LatestOnlyOperator +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.operators.datetime import BranchDateTimeOperator +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:20:1: AIR312 [*] `airflow.operators.trigger_dagrun.TriggerDagRunOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +18 | BashOperator() +19 | BranchDateTimeOperator() +20 | TriggerDagRunOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR312 +21 | EmptyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `TriggerDagRunOperator` from `airflow.providers.standard.operators.trigger_dagrun` instead. + +ℹ Unsafe fix +7 7 | from airflow.operators.datetime import BranchDateTimeOperator +8 8 | from airflow.operators.empty import EmptyOperator +9 9 | from airflow.operators.latest_only import LatestOnlyOperator +10 |-from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:21:1: AIR312 [*] `airflow.operators.empty.EmptyOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +19 | BranchDateTimeOperator() +20 | TriggerDagRunOperator() +21 | EmptyOperator() | ^^^^^^^^^^^^^ AIR312 -39 | LatestOnlyOperator() -40 | ( - | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. - -AIR312.py:39:1: AIR312 `airflow.operators.latest_only.LatestOnlyOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -37 | TriggerDagRunLink(), TriggerDagRunOperator() -38 | EmptyOperator() -39 | LatestOnlyOperator() +22 | +23 | LatestOnlyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `EmptyOperator` from `airflow.providers.standard.operators.empty` instead. + +ℹ Unsafe fix +5 5 | from airflow.hooks.subprocess import SubprocessHook +6 6 | from airflow.operators.bash import BashOperator +7 7 | from airflow.operators.datetime import BranchDateTimeOperator +8 |-from airflow.operators.empty import EmptyOperator +9 8 | from airflow.operators.latest_only import LatestOnlyOperator +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.operators.empty import EmptyOperator +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:23:1: AIR312 [*] `airflow.operators.latest_only.LatestOnlyOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +21 | EmptyOperator() +22 | +23 | LatestOnlyOperator() | ^^^^^^^^^^^^^^^^^^ AIR312 -40 | ( -41 | BranchPythonOperator(), - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.operators.latest_only.LatestOnlyOperator` instead. - -AIR312.py:41:5: AIR312 `airflow.operators.python.BranchPythonOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -39 | LatestOnlyOperator() -40 | ( -41 | BranchPythonOperator(), - | ^^^^^^^^^^^^^^^^^^^^ AIR312 -42 | PythonOperator(), -43 | PythonVirtualenvOperator(), - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.BranchPythonOperator` instead. - -AIR312.py:42:5: AIR312 `airflow.operators.python.PythonOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -40 | ( -41 | BranchPythonOperator(), -42 | PythonOperator(), - | ^^^^^^^^^^^^^^ AIR312 -43 | PythonVirtualenvOperator(), -44 | ShortCircuitOperator(), - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonOperator` instead. - -AIR312.py:43:5: AIR312 `airflow.operators.python.PythonVirtualenvOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -41 | BranchPythonOperator(), -42 | PythonOperator(), -43 | PythonVirtualenvOperator(), - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR312 -44 | ShortCircuitOperator(), -45 | ) - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonVirtualenvOperator` instead. - -AIR312.py:44:5: AIR312 `airflow.operators.python.ShortCircuitOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -42 | PythonOperator(), -43 | PythonVirtualenvOperator(), -44 | ShortCircuitOperator(), - | ^^^^^^^^^^^^^^^^^^^^ AIR312 -45 | ) -46 | BranchDayOfWeekOperator() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.ShortCircuitOperator` instead. - -AIR312.py:46:1: AIR312 `airflow.operators.weekday.BranchDayOfWeekOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -44 | ShortCircuitOperator(), -45 | ) -46 | BranchDayOfWeekOperator() +24 | BranchDayOfWeekOperator() +25 | DateTimeSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `LatestOnlyOperator` from `airflow.providers.standard.operators.latest_only` instead. + +ℹ Unsafe fix +6 6 | from airflow.operators.bash import BashOperator +7 7 | from airflow.operators.datetime import BranchDateTimeOperator +8 8 | from airflow.operators.empty import EmptyOperator +9 |-from airflow.operators.latest_only import LatestOnlyOperator +10 9 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 10 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.operators.latest_only import LatestOnlyOperator +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:24:1: AIR312 [*] `airflow.operators.weekday.BranchDayOfWeekOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +23 | LatestOnlyOperator() +24 | BranchDayOfWeekOperator() | ^^^^^^^^^^^^^^^^^^^^^^^ AIR312 -47 | DateTimeSensor(), DateTimeSensorAsync() -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.operators.weekday.BranchDayOfWeekOperator` instead. - -AIR312.py:47:1: AIR312 `airflow.sensors.date_time.DateTimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -45 | ) -46 | BranchDayOfWeekOperator() -47 | DateTimeSensor(), DateTimeSensorAsync() +25 | DateTimeSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `BranchDayOfWeekOperator` from `airflow.providers.standard.operators.weekday` instead. + +ℹ Unsafe fix +8 8 | from airflow.operators.empty import EmptyOperator +9 9 | from airflow.operators.latest_only import LatestOnlyOperator +10 10 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 |-from airflow.operators.weekday import BranchDayOfWeekOperator +12 11 | from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.operators.weekday import BranchDayOfWeekOperator +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:25:1: AIR312 [*] `airflow.sensors.date_time.DateTimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +23 | LatestOnlyOperator() +24 | BranchDayOfWeekOperator() +25 | DateTimeSensor() | ^^^^^^^^^^^^^^ AIR312 -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() -49 | FileSensor() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensor` instead. - -AIR312.py:47:19: AIR312 `airflow.sensors.date_time.DateTimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -45 | ) -46 | BranchDayOfWeekOperator() -47 | DateTimeSensor(), DateTimeSensorAsync() - | ^^^^^^^^^^^^^^^^^^^ AIR312 -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() -49 | FileSensor() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensorAsync` instead. - -AIR312.py:48:1: AIR312 `airflow.sensors.external_task.ExternalTaskMarker` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -46 | BranchDayOfWeekOperator() -47 | DateTimeSensor(), DateTimeSensorAsync() -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() +26 | +27 | from airflow.operators.python import ( + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `DateTimeSensor` from `airflow.providers.standard.sensors.date_time` instead. + +ℹ Unsafe fix +9 9 | from airflow.operators.latest_only import LatestOnlyOperator +10 10 | from airflow.operators.trigger_dagrun import TriggerDagRunOperator +11 11 | from airflow.operators.weekday import BranchDayOfWeekOperator +12 |-from airflow.sensors.date_time import DateTimeSensor + 12 |+from airflow.providers.standard.sensors.date_time import DateTimeSensor +13 13 | +14 14 | FSHook() +15 15 | PackageIndexHook() + +AIR312.py:44:1: AIR312 [*] `airflow.operators.python.BranchPythonOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +42 | from airflow.sensors.filesystem import FileSensor +43 | +44 | BranchPythonOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR312 +45 | PythonOperator() +46 | PythonVirtualenvOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `BranchPythonOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +25 25 | DateTimeSensor() +26 26 | +27 27 | from airflow.operators.python import ( +28 |- BranchPythonOperator, +29 28 | PythonOperator, +30 29 | PythonVirtualenvOperator, +31 30 | ShortCircuitOperator, +-------------------------------------------------------------------------------- +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.operators.python import BranchPythonOperator +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:45:1: AIR312 [*] `airflow.operators.python.PythonOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +44 | BranchPythonOperator() +45 | PythonOperator() + | ^^^^^^^^^^^^^^ AIR312 +46 | PythonVirtualenvOperator() +47 | ShortCircuitOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `PythonOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +26 26 | +27 27 | from airflow.operators.python import ( +28 28 | BranchPythonOperator, +29 |- PythonOperator, +30 29 | PythonVirtualenvOperator, +31 30 | ShortCircuitOperator, +32 31 | ) +-------------------------------------------------------------------------------- +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.operators.python import PythonOperator +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:46:1: AIR312 [*] `airflow.operators.python.PythonVirtualenvOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +44 | BranchPythonOperator() +45 | PythonOperator() +46 | PythonVirtualenvOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR312 +47 | ShortCircuitOperator() +48 | DateTimeSensorAsync() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `PythonVirtualenvOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +27 27 | from airflow.operators.python import ( +28 28 | BranchPythonOperator, +29 29 | PythonOperator, +30 |- PythonVirtualenvOperator, +31 30 | ShortCircuitOperator, +32 31 | ) +33 32 | from airflow.sensors.date_time import DateTimeSensorAsync +-------------------------------------------------------------------------------- +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.operators.python import PythonVirtualenvOperator +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:47:1: AIR312 [*] `airflow.operators.python.ShortCircuitOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +45 | PythonOperator() +46 | PythonVirtualenvOperator() +47 | ShortCircuitOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR312 +48 | DateTimeSensorAsync() +49 | ExternalTaskMarker() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `ShortCircuitOperator` from `airflow.providers.standard.operators.python` instead. + +ℹ Unsafe fix +28 28 | BranchPythonOperator, +29 29 | PythonOperator, +30 30 | PythonVirtualenvOperator, +31 |- ShortCircuitOperator, +32 31 | ) +33 32 | from airflow.sensors.date_time import DateTimeSensorAsync +34 33 | from airflow.sensors.external_task import ( +-------------------------------------------------------------------------------- +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.operators.python import ShortCircuitOperator +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:48:1: AIR312 [*] `airflow.sensors.date_time.DateTimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +46 | PythonVirtualenvOperator() +47 | ShortCircuitOperator() +48 | DateTimeSensorAsync() + | ^^^^^^^^^^^^^^^^^^^ AIR312 +49 | ExternalTaskMarker() +50 | ExternalTaskSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `DateTimeSensorAsync` from `airflow.providers.standard.sensors.date_time` instead. + +ℹ Unsafe fix +30 30 | PythonVirtualenvOperator, +31 31 | ShortCircuitOperator, +32 32 | ) +33 |-from airflow.sensors.date_time import DateTimeSensorAsync +34 33 | from airflow.sensors.external_task import ( +35 34 | ExternalTaskMarker, +36 35 | ExternalTaskSensor, +-------------------------------------------------------------------------------- +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.sensors.date_time import DateTimeSensorAsync +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:49:1: AIR312 [*] `airflow.sensors.external_task.ExternalTaskMarker` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +47 | ShortCircuitOperator() +48 | DateTimeSensorAsync() +49 | ExternalTaskMarker() | ^^^^^^^^^^^^^^^^^^ AIR312 -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskMarker` instead. - -AIR312.py:48:23: AIR312 `airflow.sensors.external_task.ExternalTaskSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -46 | BranchDayOfWeekOperator() -47 | DateTimeSensor(), DateTimeSensorAsync() -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() - | ^^^^^^^^^^^^^^^^^^ AIR312 -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensor` instead. - -AIR312.py:48:45: AIR312 `airflow.sensors.external_task.ExternalTaskSensorLink` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -46 | BranchDayOfWeekOperator() -47 | DateTimeSensor(), DateTimeSensorAsync() -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR312 -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensorLink` instead. - -AIR312.py:49:1: AIR312 `airflow.sensors.filesystem.FileSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -47 | DateTimeSensor(), DateTimeSensorAsync() -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() -49 | FileSensor() +50 | ExternalTaskSensor() +51 | FileSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalTaskMarker` from `airflow.providers.standard.sensors.external_task` instead. + +ℹ Unsafe fix +32 32 | ) +33 33 | from airflow.sensors.date_time import DateTimeSensorAsync +34 34 | from airflow.sensors.external_task import ( +35 |- ExternalTaskMarker, +36 35 | ExternalTaskSensor, +37 36 | ) +38 37 | from airflow.sensors.time_sensor import ( +-------------------------------------------------------------------------------- +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.sensors.external_task import ExternalTaskMarker +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:50:1: AIR312 [*] `airflow.sensors.external_task.ExternalTaskSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +48 | DateTimeSensorAsync() +49 | ExternalTaskMarker() +50 | ExternalTaskSensor() + | ^^^^^^^^^^^^^^^^^^ AIR312 +51 | FileSensor() +52 | TimeSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `ExternalTaskSensor` from `airflow.providers.standard.sensors.external_task` instead. + +ℹ Unsafe fix +33 33 | from airflow.sensors.date_time import DateTimeSensorAsync +34 34 | from airflow.sensors.external_task import ( +35 35 | ExternalTaskMarker, +36 |- ExternalTaskSensor, +37 36 | ) +38 37 | from airflow.sensors.time_sensor import ( +39 38 | TimeSensor, +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.sensors.external_task import ExternalTaskSensor +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:51:1: AIR312 [*] `airflow.sensors.filesystem.FileSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +49 | ExternalTaskMarker() +50 | ExternalTaskSensor() +51 | FileSensor() | ^^^^^^^^^^ AIR312 -50 | TimeSensor(), TimeSensorAsync() -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() - | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.sensors.filesystem.FileSensor` instead. - -AIR312.py:50:1: AIR312 `airflow.sensors.time_sensor.TimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() +52 | TimeSensor() +53 | TimeSensorAsync() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `FileSensor` from `airflow.providers.standard.sensors.filesystem` instead. + +ℹ Unsafe fix +39 39 | TimeSensor, +40 40 | TimeSensorAsync, +41 41 | ) +42 |-from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.sensors.filesystem import FileSensor +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:52:1: AIR312 [*] `airflow.sensors.time_sensor.TimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +50 | ExternalTaskSensor() +51 | FileSensor() +52 | TimeSensor() | ^^^^^^^^^^ AIR312 -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() -52 | DayOfWeekSensor() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensor` instead. - -AIR312.py:50:15: AIR312 `airflow.sensors.time_sensor.TimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() - | ^^^^^^^^^^^^^^^ AIR312 -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() -52 | DayOfWeekSensor() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensorAsync` instead. - -AIR312.py:51:1: AIR312 `airflow.sensors.time_delta.TimeDeltaSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() +53 | TimeSensorAsync() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `TimeSensor` from `airflow.providers.standard.sensors.time` instead. + +ℹ Unsafe fix +36 36 | ExternalTaskSensor, +37 37 | ) +38 38 | from airflow.sensors.time_sensor import ( +39 |- TimeSensor, +40 39 | TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.sensors.time import TimeSensor +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:53:1: AIR312 [*] `airflow.sensors.time_sensor.TimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +51 | FileSensor() +52 | TimeSensor() +53 | TimeSensorAsync() | ^^^^^^^^^^^^^^^ AIR312 -52 | DayOfWeekSensor() -53 | DagStateTrigger(), WorkflowTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensor` instead. - -AIR312.py:51:20: AIR312 `airflow.sensors.time_delta.TimeDeltaSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() - | ^^^^^^^^^^^^^^^^^^^^ AIR312 -52 | DayOfWeekSensor() -53 | DagStateTrigger(), WorkflowTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensorAsync` instead. - -AIR312.py:51:44: AIR312 `airflow.sensors.time_delta.WaitSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -49 | FileSensor() -50 | TimeSensor(), TimeSensorAsync() -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() - | ^^^^^^^^^^ AIR312 -52 | DayOfWeekSensor() -53 | DagStateTrigger(), WorkflowTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.WaitSensor` instead. - -AIR312.py:52:1: AIR312 `airflow.sensors.weekday.DayOfWeekSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -50 | TimeSensor(), TimeSensorAsync() -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() -52 | DayOfWeekSensor() +54 | +55 | from airflow.sensors.time_delta import ( + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `TimeSensorAsync` from `airflow.providers.standard.sensors.time` instead. + +ℹ Unsafe fix +37 37 | ) +38 38 | from airflow.sensors.time_sensor import ( +39 39 | TimeSensor, +40 |- TimeSensorAsync, +41 40 | ) +42 41 | from airflow.sensors.filesystem import FileSensor + 42 |+from airflow.providers.standard.sensors.time import TimeSensorAsync +43 43 | +44 44 | BranchPythonOperator() +45 45 | PythonOperator() + +AIR312.py:70:1: AIR312 [*] `airflow.sensors.time_delta.TimeDeltaSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +68 | ) +69 | +70 | TimeDeltaSensor() | ^^^^^^^^^^^^^^^ AIR312 -53 | DagStateTrigger(), WorkflowTrigger() -54 | FileTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.weekday.DayOfWeekSensor` instead. - -AIR312.py:53:1: AIR312 `airflow.triggers.external_task.DagStateTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() -52 | DayOfWeekSensor() -53 | DagStateTrigger(), WorkflowTrigger() +71 | TimeDeltaSensorAsync() +72 | DayOfWeekSensor() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `TimeDeltaSensor` from `airflow.providers.standard.sensors.time_delta` instead. + +ℹ Unsafe fix +53 53 | TimeSensorAsync() +54 54 | +55 55 | from airflow.sensors.time_delta import ( +56 |- TimeDeltaSensor, +57 56 | TimeDeltaSensorAsync, +58 57 | ) +59 58 | from airflow.sensors.weekday import DayOfWeekSensor +-------------------------------------------------------------------------------- +66 65 | DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.sensors.time_delta import TimeDeltaSensor +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:71:1: AIR312 [*] `airflow.sensors.time_delta.TimeDeltaSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +70 | TimeDeltaSensor() +71 | TimeDeltaSensorAsync() + | ^^^^^^^^^^^^^^^^^^^^ AIR312 +72 | DayOfWeekSensor() +73 | DagStateTrigger() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `TimeDeltaSensorAsync` from `airflow.providers.standard.sensors.time_delta` instead. + +ℹ Unsafe fix +54 54 | +55 55 | from airflow.sensors.time_delta import ( +56 56 | TimeDeltaSensor, +57 |- TimeDeltaSensorAsync, +58 57 | ) +59 58 | from airflow.sensors.weekday import DayOfWeekSensor +60 59 | from airflow.triggers.external_task import ( +-------------------------------------------------------------------------------- +66 65 | DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.sensors.time_delta import TimeDeltaSensorAsync +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:72:1: AIR312 [*] `airflow.sensors.weekday.DayOfWeekSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +70 | TimeDeltaSensor() +71 | TimeDeltaSensorAsync() +72 | DayOfWeekSensor() | ^^^^^^^^^^^^^^^ AIR312 -54 | FileTrigger() -55 | DateTimeTrigger(), TimeDeltaTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.external_task.DagStateTrigger` instead. - -AIR312.py:53:20: AIR312 `airflow.triggers.external_task.WorkflowTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() -52 | DayOfWeekSensor() -53 | DagStateTrigger(), WorkflowTrigger() - | ^^^^^^^^^^^^^^^ AIR312 -54 | FileTrigger() -55 | DateTimeTrigger(), TimeDeltaTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.external_task.WorkflowTrigger` instead. - -AIR312.py:54:1: AIR312 `airflow.triggers.file.FileTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -52 | DayOfWeekSensor() -53 | DagStateTrigger(), WorkflowTrigger() -54 | FileTrigger() +73 | DagStateTrigger() +74 | WorkflowTrigger() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `DayOfWeekSensor` from `airflow.providers.standard.sensors.weekday` instead. + +ℹ Unsafe fix +56 56 | TimeDeltaSensor, +57 57 | TimeDeltaSensorAsync, +58 58 | ) +59 |-from airflow.sensors.weekday import DayOfWeekSensor +60 59 | from airflow.triggers.external_task import ( +61 60 | DagStateTrigger, +62 61 | WorkflowTrigger, +-------------------------------------------------------------------------------- +66 65 | DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.sensors.weekday import DayOfWeekSensor +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:73:1: AIR312 [*] `airflow.triggers.external_task.DagStateTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +71 | TimeDeltaSensorAsync() +72 | DayOfWeekSensor() +73 | DagStateTrigger() + | ^^^^^^^^^^^^^^^ AIR312 +74 | WorkflowTrigger() +75 | FileTrigger() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `DagStateTrigger` from `airflow.providers.standard.triggers.external_task` instead. + +ℹ Unsafe fix +58 58 | ) +59 59 | from airflow.sensors.weekday import DayOfWeekSensor +60 60 | from airflow.triggers.external_task import ( +61 |- DagStateTrigger, +62 61 | WorkflowTrigger, +63 62 | ) +64 63 | from airflow.triggers.file import FileTrigger +-------------------------------------------------------------------------------- +66 65 | DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.triggers.external_task import DagStateTrigger +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:74:1: AIR312 [*] `airflow.triggers.external_task.WorkflowTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +72 | DayOfWeekSensor() +73 | DagStateTrigger() +74 | WorkflowTrigger() + | ^^^^^^^^^^^^^^^ AIR312 +75 | FileTrigger() +76 | DateTimeTrigger() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `WorkflowTrigger` from `airflow.providers.standard.triggers.external_task` instead. + +ℹ Unsafe fix +59 59 | from airflow.sensors.weekday import DayOfWeekSensor +60 60 | from airflow.triggers.external_task import ( +61 61 | DagStateTrigger, +62 |- WorkflowTrigger, +63 62 | ) +64 63 | from airflow.triggers.file import FileTrigger +65 64 | from airflow.triggers.temporal import ( +66 65 | DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.triggers.external_task import WorkflowTrigger +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:75:1: AIR312 [*] `airflow.triggers.file.FileTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +73 | DagStateTrigger() +74 | WorkflowTrigger() +75 | FileTrigger() | ^^^^^^^^^^^ AIR312 -55 | DateTimeTrigger(), TimeDeltaTrigger() - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.file.FileTrigger` instead. - -AIR312.py:55:1: AIR312 `airflow.triggers.temporal.DateTimeTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -53 | DagStateTrigger(), WorkflowTrigger() -54 | FileTrigger() -55 | DateTimeTrigger(), TimeDeltaTrigger() +76 | DateTimeTrigger() +77 | TimeDeltaTrigger() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `FileTrigger` from `airflow.providers.standard.triggers.file` instead. + +ℹ Unsafe fix +61 61 | DagStateTrigger, +62 62 | WorkflowTrigger, +63 63 | ) +64 |-from airflow.triggers.file import FileTrigger +65 64 | from airflow.triggers.temporal import ( +66 65 | DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.triggers.file import FileTrigger +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:76:1: AIR312 [*] `airflow.triggers.temporal.DateTimeTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +74 | WorkflowTrigger() +75 | FileTrigger() +76 | DateTimeTrigger() | ^^^^^^^^^^^^^^^ AIR312 +77 | TimeDeltaTrigger() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `DateTimeTrigger` from `airflow.providers.standard.triggers.temporal` instead. + +ℹ Unsafe fix +63 63 | ) +64 64 | from airflow.triggers.file import FileTrigger +65 65 | from airflow.triggers.temporal import ( +66 |- DateTimeTrigger, +67 66 | TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.triggers.temporal import DateTimeTrigger +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() + +AIR312.py:77:1: AIR312 [*] `airflow.triggers.temporal.TimeDeltaTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. + | +75 | FileTrigger() +76 | DateTimeTrigger() +77 | TimeDeltaTrigger() + | ^^^^^^^^^^^^^^^^ AIR312 | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.temporal.DateTimeTrigger` instead. - -AIR312.py:55:20: AIR312 `airflow.triggers.temporal.TimeDeltaTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. - | -53 | DagStateTrigger(), WorkflowTrigger() -54 | FileTrigger() -55 | DateTimeTrigger(), TimeDeltaTrigger() - | ^^^^^^^^^^^^^^^^ AIR312 - | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.temporal.TimeDeltaTrigger` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `TimeDeltaTrigger` from `airflow.providers.standard.triggers.temporal` instead. + +ℹ Unsafe fix +64 64 | from airflow.triggers.file import FileTrigger +65 65 | from airflow.triggers.temporal import ( +66 66 | DateTimeTrigger, +67 |- TimeDeltaTrigger, +68 67 | ) + 68 |+from airflow.providers.standard.triggers.temporal import TimeDeltaTrigger +69 69 | +70 70 | TimeDeltaSensor() +71 71 | TimeDeltaSensorAsync() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312_try.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312_try.py.snap new file mode 100644 index 0000000000000..6d6d3986fe9dd --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312_try.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/eradicate/mod.rs b/crates/ruff_linter/src/rules/eradicate/mod.rs index 7a1a13c8891ff..328868a878c53 100644 --- a/crates/ruff_linter/src/rules/eradicate/mod.rs +++ b/crates/ruff_linter/src/rules/eradicate/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::CommentedOutCode, Path::new("ERA001.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -20,7 +20,7 @@ mod tests { Path::new("eradicate").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs index eb88a2c62537a..77a6f25038fcd 100644 --- a/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff_linter/src/rules/eradicate/rules/commented_out_code.rs @@ -1,13 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_source_file::{LineRanges, UniversalNewlineIterator}; use ruff_text_size::TextRange; -use crate::settings::LinterSettings; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::settings::LinterSettings; +use crate::{Edit, Fix, FixAvailability, Violation}; -use super::super::detection::comment_contains_code; +use crate::rules::eradicate::detection::comment_contains_code; /// ## What it does /// Checks for commented-out Python code. @@ -47,7 +48,7 @@ impl Violation for CommentedOutCode { /// ERA001 pub(crate) fn commented_out_code( - diagnostics: &mut Vec, + context: &LintContext, locator: &Locator, comment_ranges: &CommentRanges, settings: &LinterSettings, @@ -65,11 +66,13 @@ pub(crate) fn commented_out_code( // Verify that the comment is on its own line, and that it contains code. if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) { - let mut diagnostic = Diagnostic::new(CommentedOutCode, range); - diagnostic.set_fix(Fix::display_only_edit(Edit::range_deletion( - locator.full_lines_range(range), - ))); - diagnostics.push(diagnostic); + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(CommentedOutCode, range) + { + diagnostic.set_fix(Fix::display_only_edit(Edit::range_deletion( + locator.full_lines_range(range), + ))); + } } } } @@ -161,8 +164,8 @@ mod tests { use ruff_source_file::LineRanges; use ruff_text_size::TextSize; - use crate::rules::eradicate::rules::commented_out_code::skip_script_comments; use crate::Locator; + use crate::rules::eradicate::rules::commented_out_code::skip_script_comments; #[test] fn script_comment() { diff --git a/crates/ruff_linter/src/rules/fastapi/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 70d22c502016b..f6e48dd4ada0b 100644 --- a/crates/ruff_linter/src/rules/fastapi/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/mod.rs @@ -3,7 +3,6 @@ pub(crate) mod rules; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use anyhow::Result; @@ -11,19 +10,19 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.py"))] #[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))] #[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))] #[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("fastapi").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -32,15 +31,15 @@ mod tests { #[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))] #[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))] fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}_py38", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("fastapi").join(path).as_path(), &settings::LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY38, + unresolved_target_version: ruff_python_ast::PythonVersion::PY38.into(), ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index 9e537d398c8c5..8d0fa8b2809e1 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::helpers::map_callable; use ruff_python_semantic::Modules; @@ -7,6 +6,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::rules::fastapi::rules::is_fastapi_route; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -59,6 +59,22 @@ use ruff_python_ast::PythonVersion; /// return commons /// ``` /// +/// ## Fix safety +/// This fix is always unsafe, as adding/removing/changing a function parameter's +/// default value can change runtime behavior. Additionally, comments inside the +/// deprecated uses might be removed. +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.9, if the target version is < 3.9 and `typing_extensions` imports have been +/// disabled by the [`lint.typing-extensions`] linter option the diagnostic will not be emitted +/// and no fix will be offered. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// [FastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated /// [typing-annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated /// [typing-extensions]: https://typing-extensions.readthedocs.io/en/stable/ @@ -223,7 +239,11 @@ fn create_diagnostic( dependency_call: Option, mut seen_default: bool, ) -> bool { - let mut diagnostic = Diagnostic::new( + let Some(importer) = checker.typing_importer("Annotated", PythonVersion::PY39) else { + return seen_default; + }; + + let mut diagnostic = checker.report_diagnostic( FastApiNonAnnotatedDependency { py_version: checker.target_version(), }, @@ -231,11 +251,7 @@ fn create_diagnostic( ); let try_generate_fix = || { - let (import_edit, binding) = checker.import_from_typing( - "Annotated", - parameter.range.start(), - PythonVersion::PY39, - )?; + let (import_edit, binding) = importer.import(parameter.range.start())?; // Each of these classes takes a single, optional default // argument, followed by kw-only arguments @@ -298,7 +314,5 @@ fn create_diagnostic( } diagnostic.try_set_optional_fix(|| fix); - checker.report_diagnostic(diagnostic); - seen_default } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs index fa5181d34aa4d..a6707d1ea1493 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_redundant_response_model.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Decorator, Expr, ExprCall, Keyword, StmtFunctionDef}; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; use crate::rules::fastapi::rules::is_fastapi_route_decorator; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for FastAPI routes that use the optional `response_model` parameter @@ -58,7 +58,6 @@ use crate::rules::fastapi::rules::is_fastapi_route_decorator; /// async def create_item(item: Item) -> Item: /// return item /// ``` - #[derive(ViolationMetadata)] pub(crate) struct FastApiRedundantResponseModel; @@ -85,17 +84,17 @@ pub(crate) fn fastapi_redundant_response_model(checker: &Checker, function_def: continue; }; let mut diagnostic = - Diagnostic::new(FastApiRedundantResponseModel, response_model_arg.range()); + checker.report_diagnostic(FastApiRedundantResponseModel, response_model_arg.range()); diagnostic.try_set_fix(|| { remove_argument( response_model_arg, &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index 7cf69ee8c40b9..e0d215b45c5da 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -2,18 +2,18 @@ use std::iter::Peekable; use std::ops::Range; use std::str::CharIndices; -use ruff_diagnostics::Fix; -use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault}; use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::{Ranged, TextSize}; +use crate::Fix; use crate::checkers::ast::Checker; use crate::fix::edits::add_parameter; use crate::rules::fastapi::rules::is_fastapi_route_decorator; +use crate::{FixAvailability, Violation}; /// ## What it does /// Identifies FastAPI routes that declare path parameters in the route path @@ -79,9 +79,11 @@ impl Violation for FastApiUnusedPathParameter { function_name, is_positional, } = self; - #[allow(clippy::if_not_else)] + #[expect(clippy::if_not_else)] if !is_positional { - format!("Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature") + format!( + "Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature" + ) } else { format!( "Parameter `{arg_name}` appears in route path, but only as a positional-only argument in `{function_name}` signature" @@ -184,13 +186,13 @@ pub(crate) fn fastapi_unused_path_parameter( .iter() .any(|param| param.name() == path_param); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( FastApiUnusedPathParameter { arg_name: path_param.to_string(), function_name: function_def.name.to_string(), is_positional, }, - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] diagnostic_range .add_start(TextSize::from(range.start as u32 + 1)) .sub_end(TextSize::from((path.len() - range.end + 1) as u32)), @@ -202,7 +204,6 @@ pub(crate) fn fastapi_unused_path_parameter( checker.locator().contents(), ))); } - checker.report_diagnostic(diagnostic); } } @@ -225,6 +226,9 @@ enum Dependency<'a> { /// A function defined in the same file, whose parameter names are as given. Function(Vec<&'a str>), + /// A class defined in the same file, whose constructor parameter names are as given. + Class(Vec<&'a str>), + /// There are multiple `Depends()` calls. /// /// Multiple `Depends` annotations aren't supported by fastapi and the exact behavior is @@ -238,6 +242,7 @@ impl<'a> Dependency<'a> { Self::Unknown => None, Self::Multiple => None, Self::Function(parameter_names) => Some(parameter_names.as_slice()), + Self::Class(parameter_names) => Some(parameter_names.as_slice()), } } } @@ -278,7 +283,14 @@ impl<'a> Dependency<'a> { let mut dependencies = tuple.elts.iter().skip(1).filter_map(|metadata_element| { let arguments = depends_arguments(metadata_element, semantic)?; - Self::from_depends_call(arguments, semantic) + // Arguments to `Depends` can be empty if the dependency is a class + // that FastAPI will call to create an instance of the class itself. + // https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/#shortcut + if arguments.is_empty() { + Self::from_dependency_name(tuple.elts.first()?.as_name_expr()?, semantic) + } else { + Self::from_depends_call(arguments, semantic) + } }); let dependency = dependencies.next()?; @@ -301,25 +313,68 @@ impl<'a> Dependency<'a> { return None; }; - let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { - return Some(Self::Unknown); - }; + Self::from_dependency_name(name, semantic) + } - let BindingKind::FunctionDefinition(scope_id) = binding.kind else { + fn from_dependency_name(name: &'a ast::ExprName, semantic: &SemanticModel<'a>) -> Option { + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { return Some(Self::Unknown); }; - let scope = &semantic.scopes[scope_id]; + match binding.kind { + BindingKind::FunctionDefinition(scope_id) => { + let scope = &semantic.scopes[scope_id]; - let ScopeKind::Function(function_def) = scope.kind else { - return Some(Self::Unknown); - }; + let ScopeKind::Function(function_def) = scope.kind else { + return Some(Self::Unknown); + }; - let parameter_names = non_posonly_non_variadic_parameters(function_def) - .map(|param| param.name().as_str()) - .collect(); + let parameter_names = non_posonly_non_variadic_parameters(function_def) + .map(|param| param.name().as_str()) + .collect(); - Some(Self::Function(parameter_names)) + Some(Self::Function(parameter_names)) + } + BindingKind::ClassDefinition(scope_id) => { + let scope = &semantic.scopes[scope_id]; + + let ScopeKind::Class(class_def) = scope.kind else { + return Some(Self::Unknown); + }; + + let parameter_names = if class_def + .bases() + .iter() + .any(|expr| is_pydantic_base_model(expr, semantic)) + { + class_def + .body + .iter() + .filter_map(|stmt| { + stmt.as_ann_assign_stmt() + .and_then(|ann_assign| ann_assign.target.as_name_expr()) + .map(|name| name.id.as_str()) + }) + .collect() + } else if let Some(init_def) = class_def + .body + .iter() + .filter_map(|stmt| stmt.as_function_def_stmt()) + .find(|func_def| func_def.name.as_str() == "__init__") + { + // Skip `self` parameter + non_posonly_non_variadic_parameters(init_def) + .skip(1) + .map(|param| param.name().as_str()) + .collect() + } else { + return None; + }; + + Some(Self::Class(parameter_names)) + } + _ => Some(Self::Unknown), + } } } @@ -339,11 +394,17 @@ fn depends_arguments<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a } fn is_fastapi_depends(expr: &Expr, semantic: &SemanticModel) -> bool { - let Some(qualified_name) = semantic.resolve_qualified_name(expr) else { - return false; - }; + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["fastapi", "Depends"])) +} - matches!(qualified_name.segments(), ["fastapi", "Depends"]) +fn is_pydantic_base_model(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pydantic", "BaseModel"]) + }) } /// Extract the expected in-route name for a given parameter, if it has an alias. @@ -424,7 +485,7 @@ impl<'a> Iterator for PathParamIterator<'a> { let param_name_end = param_content.find(':').unwrap_or(param_content.len()); let param_name = ¶m_content[..param_name_end].trim(); - #[allow(clippy::range_plus_one)] + #[expect(clippy::range_plus_one)] return Some((param_name, start..end + 1)); } } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/mod.rs b/crates/ruff_linter/src/rules/fastapi/rules/mod.rs index 034737fa12b5c..d6bbe2fdb61bd 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/mod.rs @@ -7,8 +7,8 @@ mod fastapi_redundant_response_model; mod fastapi_unused_path_parameter; use ruff_python_ast as ast; -use ruff_python_semantic::analyze::typing; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing; /// Returns `true` if the function is a FastAPI route. pub(crate) fn is_fastapi_route( diff --git a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap index 9cb210585fd38..6869770ac36dd 100644 --- a/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap +++ b/crates/ruff_linter/src/rules/fastapi/snapshots/ruff_linter__rules__fastapi__tests__fast-api-unused-path-parameter_FAST003.py.snap @@ -380,3 +380,64 @@ FAST003.py:160:19: FAST003 [*] Parameter `thing_id` appears in route path, but n 162 162 | 163 163 | 164 164 | ### No errors + +FAST003.py:197:12: FAST003 [*] Parameter `id` appears in route path, but not in `get_id_pydantic_full` signature + | +196 | # Errors +197 | @app.get("/{id}") + | ^^^^ FAST003 +198 | async def get_id_pydantic_full( +199 | params: Annotated[PydanticParams, Depends(PydanticParams)], + | + = help: Add `id` to function signature + +ℹ Unsafe fix +196 196 | # Errors +197 197 | @app.get("/{id}") +198 198 | async def get_id_pydantic_full( +199 |- params: Annotated[PydanticParams, Depends(PydanticParams)], + 199 |+ params: Annotated[PydanticParams, Depends(PydanticParams)], id, +200 200 | ): ... +201 201 | @app.get("/{id}") +202 202 | async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... + +FAST003.py:201:12: FAST003 [*] Parameter `id` appears in route path, but not in `get_id_pydantic_short` signature + | +199 | params: Annotated[PydanticParams, Depends(PydanticParams)], +200 | ): ... +201 | @app.get("/{id}") + | ^^^^ FAST003 +202 | async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... +203 | @app.get("/{id}") + | + = help: Add `id` to function signature + +ℹ Unsafe fix +199 199 | params: Annotated[PydanticParams, Depends(PydanticParams)], +200 200 | ): ... +201 201 | @app.get("/{id}") +202 |-async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... + 202 |+async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()], id): ... +203 203 | @app.get("/{id}") +204 204 | async def get_id_init_not_annotated(params = Depends(InitParams)): ... +205 205 | + +FAST003.py:203:12: FAST003 [*] Parameter `id` appears in route path, but not in `get_id_init_not_annotated` signature + | +201 | @app.get("/{id}") +202 | async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... +203 | @app.get("/{id}") + | ^^^^ FAST003 +204 | async def get_id_init_not_annotated(params = Depends(InitParams)): ... + | + = help: Add `id` to function signature + +ℹ Unsafe fix +201 201 | @app.get("/{id}") +202 202 | async def get_id_pydantic_short(params: Annotated[PydanticParams, Depends()]): ... +203 203 | @app.get("/{id}") +204 |-async def get_id_init_not_annotated(params = Depends(InitParams)): ... + 204 |+async def get_id_init_not_annotated(id, params = Depends(InitParams)): ... +205 205 | +206 206 | +207 207 | # No errors diff --git a/crates/ruff_linter/src/rules/flake8_2020/mod.rs b/crates/ruff_linter/src/rules/flake8_2020/mod.rs index d7182d699179a..21a0e46ad7478 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::SysVersionSlice3, Path::new("YTT101.py"))] #[test_case(Rule::SysVersion2, Path::new("YTT102.py"))] @@ -29,7 +29,7 @@ mod tests { Path::new("flake8_2020").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs index 1a055108c35c1..12c18a2dd73ce 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/compare.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; -use super::super::helpers::is_sys; +use crate::rules::flake8_2020::helpers::is_sys; /// ## What it does /// Checks for comparisons that test `sys.version` against string literals, @@ -52,17 +52,20 @@ impl Violation for SysVersionCmpStr3 { /// ## What it does /// Checks for equality comparisons against the major version returned by -/// `sys.version_info` (e.g., `sys.version_info[0] == 3`). +/// `sys.version_info` (e.g., `sys.version_info[0] == 3` or `sys.version_info[0] != 3`). /// /// ## Why is this bad? /// Using `sys.version_info[0] == 3` to verify that the major version is /// Python 3 or greater will fail if the major version number is ever /// incremented (e.g., to Python 4). This is likely unintended, as code /// that uses this comparison is likely intended to be run on Python 2, -/// but would now run on Python 4 too. +/// but would now run on Python 4 too. Similarly, using `sys.version_info[0] != 3` +/// to check for Python 2 will also fail if the major version number is +/// incremented. /// /// Instead, use `>=` to check if the major version number is 3 or greater, -/// to future-proof the code. +/// or `<` to check if the major version number is less than 3, to future-proof +/// the code. /// /// ## Example /// ```python @@ -88,12 +91,18 @@ impl Violation for SysVersionCmpStr3 { /// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] -pub(crate) struct SysVersionInfo0Eq3; +pub(crate) struct SysVersionInfo0Eq3 { + eq: bool, +} impl Violation for SysVersionInfo0Eq3 { #[derive_message_formats] fn message(&self) -> String { - "`sys.version_info[0] == 3` referenced (python4), use `>=`".to_string() + if self.eq { + "`sys.version_info[0] == 3` referenced (python4), use `>=`".to_string() + } else { + "`sys.version_info[0] != 3` referenced (python4), use `<`".to_string() + } } } @@ -235,35 +244,36 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators { if *i == 0 { if let ( - [CmpOp::Eq | CmpOp::NotEq], - [Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Int(n), - .. - })], + [operator @ (CmpOp::Eq | CmpOp::NotEq)], + [ + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(n), + .. + }), + ], ) = (ops, comparators) { - if *n == 3 && checker.enabled(Rule::SysVersionInfo0Eq3) { - checker.report_diagnostic(Diagnostic::new( - SysVersionInfo0Eq3, + if *n == 3 && checker.is_rule_enabled(Rule::SysVersionInfo0Eq3) { + checker.report_diagnostic( + SysVersionInfo0Eq3 { + eq: matches!(*operator, CmpOp::Eq), + }, left.range(), - )); + ); } } } else if *i == 1 { if let ( [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], - [Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Int(_), - .. - })], + [ + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(_), + .. + }), + ], ) = (ops, comparators) { - if checker.enabled(Rule::SysVersionInfo1CmpInt) { - checker.report_diagnostic(Diagnostic::new( - SysVersionInfo1CmpInt, - left.range(), - )); - } + checker.report_diagnostic_if_enabled(SysVersionInfo1CmpInt, left.range()); } } } @@ -274,18 +284,15 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators { if let ( [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], - [Expr::NumberLiteral(ast::ExprNumberLiteral { - value: ast::Number::Int(_), - .. - })], + [ + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(_), + .. + }), + ], ) = (ops, comparators) { - if checker.enabled(Rule::SysVersionInfoMinorCmpInt) { - checker.report_diagnostic(Diagnostic::new( - SysVersionInfoMinorCmpInt, - left.range(), - )); - } + checker.report_diagnostic_if_enabled(SysVersionInfoMinorCmpInt, left.range()); } } @@ -299,11 +306,9 @@ pub(crate) fn compare(checker: &Checker, left: &Expr, ops: &[CmpOp], comparators ) = (ops, comparators) { if value.len() == 1 { - if checker.enabled(Rule::SysVersionCmpStr10) { - checker.report_diagnostic(Diagnostic::new(SysVersionCmpStr10, left.range())); - } - } else if checker.enabled(Rule::SysVersionCmpStr3) { - checker.report_diagnostic(Diagnostic::new(SysVersionCmpStr3, left.range())); + checker.report_diagnostic_if_enabled(SysVersionCmpStr10, left.range()); + } else { + checker.report_diagnostic_if_enabled(SysVersionCmpStr3, left.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs index 2e42a4ff78e64..e310dd24b5880 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -56,6 +56,6 @@ pub(crate) fn name_or_attribute(checker: &Checker, expr: &Expr) { .resolve_qualified_name(expr) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["six", "PY3"])) { - checker.report_diagnostic(Diagnostic::new(SixPY3, expr.range())); + checker.report_diagnostic(SixPY3, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs index 3412dcf1f4d58..5b5d4d6e00b2a 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/subscript.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::flake8_2020::helpers::is_sys; @@ -176,16 +176,17 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, slice: &Expr) { upper: Some(upper), step: None, range: _, + node_index: _, }) => { if let Expr::NumberLiteral(ast::ExprNumberLiteral { value: ast::Number::Int(i), .. }) = upper.as_ref() { - if *i == 1 && checker.enabled(Rule::SysVersionSlice1) { - checker.report_diagnostic(Diagnostic::new(SysVersionSlice1, value.range())); - } else if *i == 3 && checker.enabled(Rule::SysVersionSlice3) { - checker.report_diagnostic(Diagnostic::new(SysVersionSlice3, value.range())); + if *i == 1 && checker.is_rule_enabled(Rule::SysVersionSlice1) { + checker.report_diagnostic(SysVersionSlice1, value.range()); + } else if *i == 3 && checker.is_rule_enabled(Rule::SysVersionSlice3) { + checker.report_diagnostic(SysVersionSlice3, value.range()); } } } @@ -194,10 +195,10 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, slice: &Expr) { value: ast::Number::Int(i), .. }) => { - if *i == 2 && checker.enabled(Rule::SysVersion2) { - checker.report_diagnostic(Diagnostic::new(SysVersion2, value.range())); - } else if *i == 0 && checker.enabled(Rule::SysVersion0) { - checker.report_diagnostic(Diagnostic::new(SysVersion0, value.range())); + if *i == 2 && checker.is_rule_enabled(Rule::SysVersion2) { + checker.report_diagnostic(SysVersion2, value.range()); + } else if *i == 0 && checker.is_rule_enabled(Rule::SysVersion0) { + checker.report_diagnostic(SysVersion0, value.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT201_YTT201.py.snap b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT201_YTT201.py.snap index c2ae9a26987b0..f5422cad3de0a 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT201_YTT201.py.snap +++ b/crates/ruff_linter/src/rules/flake8_2020/snapshots/ruff_linter__rules__flake8_2020__tests__YTT201_YTT201.py.snap @@ -20,7 +20,7 @@ YTT201.py:8:7: YTT201 `sys.version_info[0] == 3` referenced (python4), use `>=` 10 | PY2 = version_info[0] != 3 | -YTT201.py:9:7: YTT201 `sys.version_info[0] == 3` referenced (python4), use `>=` +YTT201.py:9:7: YTT201 `sys.version_info[0] != 3` referenced (python4), use `<` | 7 | PY3 = sys.version_info[0] == 3 8 | PY3 = version_info[0] == 3 @@ -29,7 +29,7 @@ YTT201.py:9:7: YTT201 `sys.version_info[0] == 3` referenced (python4), use `>=` 10 | PY2 = version_info[0] != 3 | -YTT201.py:10:7: YTT201 `sys.version_info[0] == 3` referenced (python4), use `>=` +YTT201.py:10:7: YTT201 `sys.version_info[0] != 3` referenced (python4), use `<` | 8 | PY3 = version_info[0] == 3 9 | PY2 = sys.version_info[0] != 3 diff --git a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs index 1adf672cfe85e..c0289225f174d 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs @@ -1,9 +1,8 @@ use itertools::Itertools; use rustc_hash::FxHashSet; -use ruff_diagnostics::Edit; use ruff_python_ast::helpers::{ - pep_604_union, typing_optional, typing_union, ReturnStatementVisitor, + ReturnStatementVisitor, pep_604_union, typing_optional, typing_union, }; use ruff_python_ast::name::Name; use ruff_python_ast::visitor::Visitor; @@ -14,6 +13,7 @@ use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{Definition, SemanticModel}; use ruff_text_size::{TextRange, TextSize}; +use crate::Edit; use crate::checkers::ast::Checker; use ruff_python_ast::PythonVersion; @@ -131,11 +131,13 @@ impl AutoPythonType { "NoReturn" }; let (no_return_edit, binding) = checker - .import_from_typing(member, at, PythonVersion::lowest()) + .typing_importer(member, PythonVersion::lowest())? + .import(at) .ok()?; let expr = Expr::Name(ast::ExprName { id: Name::from(binding), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, }); Some((expr, vec![no_return_edit])) @@ -169,7 +171,8 @@ impl AutoPythonType { // Ex) `Optional[int]` let (optional_edit, binding) = checker - .import_from_typing("Optional", at, PythonVersion::lowest()) + .typing_importer("Optional", PythonVersion::lowest())? + .import(at) .ok()?; let expr = typing_optional(element, Name::from(binding)); Some((expr, vec![optional_edit])) @@ -182,7 +185,8 @@ impl AutoPythonType { // Ex) `Union[int, str]` let (union_edit, binding) = checker - .import_from_typing("Union", at, PythonVersion::lowest()) + .typing_importer("Union", PythonVersion::lowest())? + .import(at) .ok()?; let expr = typing_union(&elements, Name::from(binding)); Some((expr, vec![union_edit])) @@ -200,6 +204,7 @@ fn type_expr(python_type: PythonType) -> Option { Expr::Name(ast::ExprName { id: name.into(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, }) } diff --git a/crates/ruff_linter/src/rules/flake8_annotations/mod.rs b/crates/ruff_linter/src/rules/flake8_annotations/mod.rs index 2b7860d4a8075..7fcd00c78d6b9 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/mod.rs @@ -9,7 +9,7 @@ mod tests { use anyhow::Result; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::test::test_path; @@ -33,7 +33,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -59,7 +59,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -79,7 +79,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -101,7 +101,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -119,7 +119,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -128,7 +128,7 @@ mod tests { let diagnostics = test_path( Path::new("flake8_annotations/auto_return_type.py"), &LinterSettings { - unresolved_target_version: PythonVersion::PY38, + unresolved_target_version: PythonVersion::PY38.into(), ..LinterSettings::for_rules(vec![ Rule::MissingReturnTypeUndocumentedPublicFunction, Rule::MissingReturnTypePrivateFunction, @@ -138,7 +138,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -164,7 +164,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -180,7 +180,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::AnyType]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -198,7 +198,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -216,7 +216,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -226,7 +226,7 @@ mod tests { Path::new("flake8_annotations/simple_magic_methods.py"), &LinterSettings::for_rule(Rule::MissingReturnTypeSpecialMethod), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index 786877146c6e6..b973b47ef83fd 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -1,18 +1,18 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::Definition; +use ruff_python_semantic::analyze::visibility; use ruff_python_stdlib::typing::simple_magic_return_type; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, DiagnosticGuard}; use crate::registry::Rule; use crate::rules::flake8_annotations::helpers::auto_return_type; use crate::rules::ruff::typing::type_hint_resolves_to_any; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks that function arguments have type annotations. @@ -150,7 +150,7 @@ impl Violation for MissingTypeKwargs { #[deprecated(note = "ANN101 has been removed")] pub(crate) struct MissingTypeSelf; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for MissingTypeSelf { fn message(&self) -> String { unreachable!("ANN101 has been removed"); @@ -194,7 +194,7 @@ impl Violation for MissingTypeSelf { #[deprecated(note = "ANN102 has been removed")] pub(crate) struct MissingTypeCls; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for MissingTypeCls { fn message(&self) -> String { unreachable!("ANN102 has been removed") @@ -224,6 +224,16 @@ impl Violation for MissingTypeCls { /// def add(a: int, b: int) -> int: /// return a + b /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for some Python versions, +/// its diagnostic will not be emitted, and no fix will be offered, if `typing_extensions` imports +/// have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct MissingReturnTypeUndocumentedPublicFunction { name: String, @@ -267,6 +277,16 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction { /// def _add(a: int, b: int) -> int: /// return a + b /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for some Python versions, +/// its diagnostic will not be emitted, and no fix will be offered, if `typing_extensions` imports +/// have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct MissingReturnTypePrivateFunction { name: String, @@ -456,12 +476,18 @@ impl Violation for MissingReturnTypeClassMethod { /// ## Example /// /// ```python +/// from typing import Any +/// +/// /// def foo(x: Any): ... /// ``` /// /// Use instead: /// /// ```python +/// from typing import Any +/// +/// /// def foo(x: int): ... /// ``` /// @@ -509,11 +535,11 @@ fn is_none_returning(body: &[Stmt]) -> bool { } /// ANN401 -fn check_dynamically_typed( - checker: &Checker, +fn check_dynamically_typed<'a, 'b, F>( + checker: &'a Checker<'b>, annotation: &Expr, func: F, - diagnostics: &mut Vec, + context: &mut Vec>, ) where F: FnOnce() -> String, { @@ -525,18 +551,13 @@ fn check_dynamically_typed( checker, checker.target_version(), ) { - diagnostics.push(Diagnostic::new( - AnyType { name: func() }, - annotation.range(), - )); + context + .push(checker.report_diagnostic(AnyType { name: func() }, annotation.range())); } } } else { if type_hint_resolves_to_any(annotation, checker, checker.target_version()) { - diagnostics.push(Diagnostic::new( - AnyType { name: func() }, - annotation.range(), - )); + context.push(checker.report_diagnostic(AnyType { name: func() }, annotation.range())); } } } @@ -547,7 +568,11 @@ fn is_stub_function(function_def: &ast::StmtFunctionDef, checker: &Checker) -> b fn is_empty_body(function_def: &ast::StmtFunctionDef) -> bool { function_def.body.iter().all(|stmt| match stmt { Stmt::Pass(_) => true, - Stmt::Expr(ast::StmtExpr { value, range: _ }) => { + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => { matches!( value.as_ref(), Expr::StringLiteral(_) | Expr::EllipsisLiteral(_) @@ -584,13 +609,14 @@ pub(crate) fn definition( checker: &Checker, definition: &Definition, visibility: visibility::Visibility, -) -> Vec { +) { let Some(function) = definition.as_function_def() else { - return vec![]; + return; }; let ast::StmtFunctionDef { range: _, + node_index: _, is_async: _, decorator_list, name, @@ -619,7 +645,7 @@ pub(crate) fn definition( // ANN401 for dynamically typed parameters if let Some(annotation) = parameter.annotation() { has_any_typed_arg = true; - if checker.enabled(Rule::AnyType) && !is_overridden { + if checker.is_rule_enabled(Rule::AnyType) && !is_overridden { check_dynamically_typed( checker, annotation, @@ -628,14 +654,14 @@ pub(crate) fn definition( ); } } else { - if !(checker.settings.flake8_annotations.suppress_dummy_args + if !(checker.settings().flake8_annotations.suppress_dummy_args && checker - .settings + .settings() .dummy_variable_rgx .is_match(parameter.name())) { - if checker.enabled(Rule::MissingTypeFunctionArgument) { - diagnostics.push(Diagnostic::new( + if checker.is_rule_enabled(Rule::MissingTypeFunctionArgument) { + diagnostics.push(checker.report_diagnostic( MissingTypeFunctionArgument { name: parameter.name().to_string(), }, @@ -650,18 +676,18 @@ pub(crate) fn definition( if let Some(arg) = ¶meters.vararg { if let Some(expr) = &arg.annotation { has_any_typed_arg = true; - if !checker.settings.flake8_annotations.allow_star_arg_any { - if checker.enabled(Rule::AnyType) && !is_overridden { + if !checker.settings().flake8_annotations.allow_star_arg_any { + if checker.is_rule_enabled(Rule::AnyType) && !is_overridden { let name = &arg.name; check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics); } } } else { - if !(checker.settings.flake8_annotations.suppress_dummy_args - && checker.settings.dummy_variable_rgx.is_match(&arg.name)) + if !(checker.settings().flake8_annotations.suppress_dummy_args + && checker.settings().dummy_variable_rgx.is_match(&arg.name)) { - if checker.enabled(Rule::MissingTypeArgs) { - diagnostics.push(Diagnostic::new( + if checker.is_rule_enabled(Rule::MissingTypeArgs) { + diagnostics.push(checker.report_diagnostic( MissingTypeArgs { name: arg.name.to_string(), }, @@ -676,8 +702,8 @@ pub(crate) fn definition( if let Some(arg) = ¶meters.kwarg { if let Some(expr) = &arg.annotation { has_any_typed_arg = true; - if !checker.settings.flake8_annotations.allow_star_arg_any { - if checker.enabled(Rule::AnyType) && !is_overridden { + if !checker.settings().flake8_annotations.allow_star_arg_any { + if checker.is_rule_enabled(Rule::AnyType) && !is_overridden { let name = &arg.name; check_dynamically_typed( checker, @@ -688,11 +714,11 @@ pub(crate) fn definition( } } } else { - if !(checker.settings.flake8_annotations.suppress_dummy_args - && checker.settings.dummy_variable_rgx.is_match(&arg.name)) + if !(checker.settings().flake8_annotations.suppress_dummy_args + && checker.settings().dummy_variable_rgx.is_match(&arg.name)) { - if checker.enabled(Rule::MissingTypeKwargs) { - diagnostics.push(Diagnostic::new( + if checker.is_rule_enabled(Rule::MissingTypeKwargs) { + diagnostics.push(checker.report_diagnostic( MissingTypeKwargs { name: arg.name.to_string(), }, @@ -706,16 +732,21 @@ pub(crate) fn definition( // ANN201, ANN202, ANN401 if let Some(expr) = &returns { has_typed_return = true; - if checker.enabled(Rule::AnyType) && !is_overridden { + if checker.is_rule_enabled(Rule::AnyType) && !is_overridden { check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics); } } else if !( // Allow omission of return annotation if the function only returns `None` // (explicitly or implicitly). - checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body) + checker + .settings() + .flake8_annotations + .suppress_none_returning + && is_none_returning(body) ) { + // ANN206 if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) { - if checker.enabled(Rule::MissingReturnTypeClassMethod) { + if checker.is_rule_enabled(Rule::MissingReturnTypeClassMethod) { let return_type = if is_stub_function(function, checker) { None } else { @@ -725,7 +756,7 @@ pub(crate) fn definition( }) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingReturnTypeClassMethod { name: name.to_string(), annotation: return_type.clone().map(|(return_type, ..)| return_type), @@ -741,7 +772,8 @@ pub(crate) fn definition( diagnostics.push(diagnostic); } } else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) { - if checker.enabled(Rule::MissingReturnTypeStaticMethod) { + // ANN205 + if checker.is_rule_enabled(Rule::MissingReturnTypeStaticMethod) { let return_type = if is_stub_function(function, checker) { None } else { @@ -751,7 +783,7 @@ pub(crate) fn definition( }) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingReturnTypeStaticMethod { name: name.to_string(), annotation: return_type.clone().map(|(return_type, ..)| return_type), @@ -767,11 +799,12 @@ pub(crate) fn definition( diagnostics.push(diagnostic); } } else if is_method && visibility::is_init(name) { + // ANN204 // Allow omission of return annotation in `__init__` functions, as long as at // least one argument is typed. - if checker.enabled(Rule::MissingReturnTypeSpecialMethod) { - if !(checker.settings.flake8_annotations.mypy_init_return && has_any_typed_arg) { - let mut diagnostic = Diagnostic::new( + if checker.is_rule_enabled(Rule::MissingReturnTypeSpecialMethod) { + if !(checker.settings().flake8_annotations.mypy_init_return && has_any_typed_arg) { + let mut diagnostic = checker.report_diagnostic( MissingReturnTypeSpecialMethod { name: name.to_string(), annotation: Some("None".to_string()), @@ -786,9 +819,9 @@ pub(crate) fn definition( } } } else if is_method && visibility::is_magic(name) { - if checker.enabled(Rule::MissingReturnTypeSpecialMethod) { + if checker.is_rule_enabled(Rule::MissingReturnTypeSpecialMethod) { let return_type = simple_magic_return_type(name); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingReturnTypeSpecialMethod { name: name.to_string(), annotation: return_type.map(ToString::to_string), @@ -806,7 +839,7 @@ pub(crate) fn definition( } else { match visibility { visibility::Visibility::Public => { - if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) { + if checker.is_rule_enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) { let return_type = if is_stub_function(function, checker) { None } else { @@ -819,7 +852,7 @@ pub(crate) fn definition( (checker.generator().expr(&return_type), edits) }) }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingReturnTypeUndocumentedPublicFunction { name: name.to_string(), annotation: return_type @@ -841,7 +874,7 @@ pub(crate) fn definition( } } visibility::Visibility::Private => { - if checker.enabled(Rule::MissingReturnTypePrivateFunction) { + if checker.is_rule_enabled(Rule::MissingReturnTypePrivateFunction) { let return_type = if is_stub_function(function, checker) { None } else { @@ -854,7 +887,7 @@ pub(crate) fn definition( (checker.generator().expr(&return_type), edits) }) }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingReturnTypePrivateFunction { name: name.to_string(), annotation: return_type @@ -879,13 +912,10 @@ pub(crate) fn definition( } } - if !checker.settings.flake8_annotations.ignore_fully_untyped { - return diagnostics; - } - // If settings say so, don't report any of the // diagnostics gathered here if there were no type annotations at all. - if has_any_typed_arg + let diagnostics_enabled = !checker.settings().flake8_annotations.ignore_fully_untyped + || has_any_typed_arg || has_typed_return || (is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()) @@ -893,10 +923,11 @@ pub(crate) fn definition( .posonlyargs .first() .or_else(|| parameters.args.first()) - .is_some_and(|first_param| first_param.annotation().is_some())) - { - diagnostics - } else { - vec![] + .is_some_and(|first_param| first_param.annotation().is_some())); + + if !diagnostics_enabled { + for diagnostic in diagnostics { + diagnostic.defuse(); + } } } diff --git a/crates/ruff_linter/src/rules/flake8_annotations/settings.rs b/crates/ruff_linter/src/rules/flake8_annotations/settings.rs index 342a56023a0e1..c2b1875100e5d 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/settings.rs @@ -5,7 +5,7 @@ use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, Default, CacheKey)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct Settings { pub mypy_init_return: bool, pub suppress_dummy_args: bool, diff --git a/crates/ruff_linter/src/rules/flake8_async/helpers.rs b/crates/ruff_linter/src/rules/flake8_async/helpers.rs index 65cd7bff273ad..4faf95c40c829 100644 --- a/crates/ruff_linter/src/rules/flake8_async/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_async/helpers.rs @@ -24,8 +24,8 @@ impl AsyncModule { impl std::fmt::Display for AsyncModule { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AsyncModule::AnyIo => write!(f, "asyncio"), - AsyncModule::AsyncIo => write!(f, "anyio"), + AsyncModule::AnyIo => write!(f, "anyio"), + AsyncModule::AsyncIo => write!(f, "asyncio"), AsyncModule::Trio => write!(f, "trio"), } } diff --git a/crates/ruff_linter/src/rules/flake8_async/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index 165db1d7a4c8e..b7346e2429373 100644 --- a/crates/ruff_linter/src/rules/flake8_async/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/mod.rs @@ -9,7 +9,7 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::test::test_path; @@ -34,7 +34,7 @@ mod tests { Path::new("flake8_async").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -44,11 +44,11 @@ mod tests { let diagnostics = test_path( Path::new("flake8_async").join(path), &LinterSettings { - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) }, )?; - assert_messages!(path.file_name().unwrap().to_str().unwrap(), diagnostics); + assert_diagnostics!(path.file_name().unwrap().to_str().unwrap(), diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs index 065f5da16abf0..9b4fab75c4a20 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_busy_wait.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_async::helpers::AsyncModule; @@ -16,6 +16,8 @@ use crate::rules::flake8_async::helpers::AsyncModule; /// /// ## Example /// ```python +/// import asyncio +/// /// DONE = False /// /// @@ -26,6 +28,8 @@ use crate::rules::flake8_async::helpers::AsyncModule; /// /// Use instead: /// ```python +/// import asyncio +/// /// DONE = asyncio.Event() /// /// @@ -74,11 +78,11 @@ pub(crate) fn async_busy_wait(checker: &Checker, while_stmt: &ast::StmtWhile) { qualified_name.segments(), ["trio" | "anyio", "sleep" | "sleep_until"] | ["asyncio", "sleep"] ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( AsyncBusyWait { module: AsyncModule::try_from(&qualified_name).unwrap(), }, while_stmt.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs index 53cbf952b160d..74c19a734a688 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_async::helpers::AsyncModule; use ruff_python_ast::PythonVersion; -#[allow(clippy::doc_link_with_quotes)] +#[expect(clippy::doc_link_with_quotes)] /// ## What it does /// Checks for `async` function definitions with `timeout` parameters. /// @@ -112,8 +112,5 @@ pub(crate) fn async_function_with_timeout(checker: &Checker, function_def: &ast: return; } - checker.report_diagnostic(Diagnostic::new( - AsyncFunctionWithTimeout { module }, - timeout.range(), - )); + checker.report_diagnostic(AsyncFunctionWithTimeout { module }, timeout.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs index fe263f565d510..ec4a70d2669aa 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_zero_sleep.rs @@ -1,5 +1,5 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; @@ -7,6 +7,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::rules::flake8_async::helpers::AsyncModule; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `trio.sleep(0)` or `anyio.sleep(0)`. @@ -32,6 +33,21 @@ use crate::rules::flake8_async::helpers::AsyncModule; /// async def func(): /// await trio.lowlevel.checkpoint() /// ``` +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the +/// `trio.sleep(0)` expression, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// import trio +/// +/// +/// async def func(): +/// await trio.sleep( # comment +/// # comment +/// 0 +/// ) +/// ``` #[derive(ViolationMetadata)] pub(crate) struct AsyncZeroSleep { module: AsyncModule, @@ -62,7 +78,25 @@ pub(crate) fn async_zero_sleep(checker: &Checker, call: &ExprCall) { return; } - let Some(arg) = call.arguments.find_argument_value("seconds", 0) else { + let Some(qualified_name) = checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) + else { + return; + }; + + let Some(module) = AsyncModule::try_from(&qualified_name) else { + return; + }; + + // Determine the correct argument name + let arg_name = match module { + AsyncModule::Trio => "seconds", + AsyncModule::AnyIo => "delay", + AsyncModule::AsyncIo => return, + }; + + let Some(arg) = call.arguments.find_argument_value(arg_name, 0) else { return; }; @@ -91,7 +125,7 @@ pub(crate) fn async_zero_sleep(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new(AsyncZeroSleep { module }, call.range()); + let mut diagnostic = checker.report_diagnostic(AsyncZeroSleep { module }, call.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import_from(&module.to_string(), "lowlevel"), @@ -101,8 +135,15 @@ pub(crate) fn async_zero_sleep(checker: &Checker, call: &ExprCall) { let reference_edit = Edit::range_replacement(format!("{binding}.checkpoint"), call.func.range()); let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range()); - Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit])) + Ok(Fix::applicable_edits( + import_edit, + [reference_edit, arg_edit], + if checker.comment_ranges().intersects(call.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs index 322f49a80f490..b6e9d045ad4dd 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs @@ -1,10 +1,10 @@ use ruff_python_ast::ExprCall; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -20,12 +20,18 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python +/// import urllib +/// +/// /// async def fetch(): /// urllib.request.urlopen("https://example.com/foo/bar").read() /// ``` /// /// Use instead: /// ```python +/// import aiohttp +/// +/// /// async def fetch(): /// async with aiohttp.ClientSession() as session: /// async with session.get("https://example.com/foo/bar") as resp: @@ -70,10 +76,7 @@ pub(crate) fn blocking_http_call(checker: &Checker, call: &ExprCall) { .as_ref() .is_some_and(is_blocking_http_call) { - checker.report_diagnostic(Diagnostic::new( - BlockingHttpCallInAsyncFunction, - call.func.range(), - )); + checker.report_diagnostic(BlockingHttpCallInAsyncFunction, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs index 0752d55146ec2..daf84df349ed1 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::{analyze, SemanticModel}; +use ruff_python_semantic::{SemanticModel, analyze}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -52,10 +52,7 @@ pub(crate) fn blocking_open_call(checker: &Checker, call: &ast::ExprCall) { if is_open_call(&call.func, checker.semantic()) || is_open_call_from_pathlib(call.func.as_ref(), checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( - BlockingOpenCallInAsyncFunction, - call.func.range(), - )); + checker.report_diagnostic(BlockingOpenCallInAsyncFunction, call.func.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs index 693a345ba66ed..2995ab0e3ce55 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_process_invocation.rs @@ -1,12 +1,11 @@ -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::analyze::typing::find_assigned_value; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing::find_assigned_value; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::registry::AsRule; /// ## What it does /// Checks that async functions do not create subprocesses with blocking methods. @@ -22,12 +21,18 @@ use crate::registry::AsRule; /// /// ## Example /// ```python +/// import os +/// +/// /// async def foo(): /// os.popen(cmd) /// ``` /// /// Use instead: /// ```python +/// import asyncio +/// +/// /// async def foo(): /// asyncio.create_subprocess_shell(cmd) /// ``` @@ -55,12 +60,18 @@ impl Violation for CreateSubprocessInAsyncFunction { /// /// ## Example /// ```python +/// import subprocess +/// +/// /// async def foo(): /// subprocess.run(cmd) /// ``` /// /// Use instead: /// ```python +/// import asyncio +/// +/// /// async def foo(): /// asyncio.create_subprocess_shell(cmd) /// ``` @@ -88,12 +99,19 @@ impl Violation for RunProcessInAsyncFunction { /// /// ## Example /// ```python +/// import os +/// +/// /// async def foo(): /// os.waitpid(0) /// ``` /// /// Use instead: /// ```python +/// import asyncio +/// import os +/// +/// /// def wait_for_process(): /// os.waitpid(0) /// @@ -117,37 +135,39 @@ pub(crate) fn blocking_process_invocation(checker: &Checker, call: &ast::ExprCal return; } - let Some(diagnostic_kind) = - checker - .semantic() - .resolve_qualified_name(call.func.as_ref()) - .and_then(|qualified_name| match qualified_name.segments() { - ["subprocess", "Popen"] | ["os", "popen"] => { - Some(CreateSubprocessInAsyncFunction.into()) - } - ["os", "system" | "posix_spawn" | "posix_spawnp"] - | ["subprocess", "run" | "call" | "check_call" | "check_output" | "getoutput" - | "getstatusoutput"] => Some(RunProcessInAsyncFunction.into()), - ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] => { - Some(WaitForProcessInAsyncFunction.into()) - } - ["os", "spawnl" | "spawnle" | "spawnlp" | "spawnlpe" | "spawnv" | "spawnve" - | "spawnvp" | "spawnvpe"] => { - if is_p_wait(call, checker.semantic()) { - Some(RunProcessInAsyncFunction.into()) - } else { - Some(CreateSubprocessInAsyncFunction.into()) - } - } - _ => None, - }) + let Some(qualified_name) = checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) else { return; }; - let diagnostic = Diagnostic::new::(diagnostic_kind, call.func.range()); - if checker.enabled(diagnostic.kind.rule()) { - checker.report_diagnostic(diagnostic); - } + + let range = call.func.range(); + match qualified_name.segments() { + ["subprocess", "Popen"] | ["os", "popen"] => { + checker.report_diagnostic_if_enabled(CreateSubprocessInAsyncFunction, range) + } + ["os", "system" | "posix_spawn" | "posix_spawnp"] + | [ + "subprocess", + "run" | "call" | "check_call" | "check_output" | "getoutput" | "getstatusoutput", + ] => checker.report_diagnostic_if_enabled(RunProcessInAsyncFunction, range), + ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] => { + checker.report_diagnostic_if_enabled(WaitForProcessInAsyncFunction, range) + } + [ + "os", + "spawnl" | "spawnle" | "spawnlp" | "spawnlpe" | "spawnv" | "spawnve" | "spawnvp" + | "spawnvpe", + ] => { + if is_p_wait(call, checker.semantic()) { + checker.report_diagnostic_if_enabled(RunProcessInAsyncFunction, range) + } else { + checker.report_diagnostic_if_enabled(CreateSubprocessInAsyncFunction, range) + } + } + _ => return, + }; } fn is_p_wait(call: &ast::ExprCall, semantic: &SemanticModel) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs index de96e7ba8a121..a3ef530bc0761 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_sleep.rs @@ -1,10 +1,10 @@ use ruff_python_ast::ExprCall; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -19,12 +19,18 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python +/// import time +/// +/// /// async def fetch(): /// time.sleep(1) /// ``` /// /// Use instead: /// ```python +/// import asyncio +/// +/// /// async def fetch(): /// await asyncio.sleep(1) /// ``` @@ -51,10 +57,7 @@ pub(crate) fn blocking_sleep(checker: &Checker, call: &ExprCall) { .as_ref() .is_some_and(is_blocking_sleep) { - checker.report_diagnostic(Diagnostic::new( - BlockingSleepInAsyncFunction, - call.func.range(), - )); + checker.report_diagnostic(BlockingSleepInAsyncFunction, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs b/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs index e8dc02b490b6c..80d91019480e1 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers::{any_over_body, AwaitVisitor}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::helpers::{AwaitVisitor, any_over_body}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{Expr, StmtWith, WithItem}; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_async::helpers::MethodName; @@ -21,6 +21,9 @@ use crate::rules::flake8_async::helpers::MethodName; /// /// ## Example /// ```python +/// import asyncio +/// +/// /// async def func(): /// async with asyncio.timeout(2): /// do_something() @@ -28,6 +31,9 @@ use crate::rules::flake8_async::helpers::MethodName; /// /// Use instead: /// ```python +/// import asyncio +/// +/// /// async def func(): /// async with asyncio.timeout(2): /// do_something() @@ -47,7 +53,9 @@ impl Violation for CancelScopeNoCheckpoint { #[derive_message_formats] fn message(&self) -> String { let Self { method_name } = self; - format!("A `with {method_name}(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.") + format!( + "A `with {method_name}(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint." + ) } } @@ -98,8 +106,5 @@ pub(crate) fn cancel_scope_no_checkpoint( return; } - checker.report_diagnostic(Diagnostic::new( - CancelScopeNoCheckpoint { method_name }, - with_stmt.range, - )); + checker.report_diagnostic(CancelScopeNoCheckpoint { method_name }, with_stmt.range); } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs index ae623a4514867..8680dce16baf9 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprCall, ExprNumberLiteral, Number}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::rules::flake8_async::helpers::AsyncModule; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `trio.sleep()` or `anyio.sleep()` with a delay greater than 24 hours. @@ -34,6 +34,10 @@ use crate::rules::flake8_async::helpers::AsyncModule; /// async def func(): /// await trio.sleep_forever() /// ``` +/// +/// ## Fix safety +/// +/// This fix is marked as unsafe as it changes program behavior. #[derive(ViolationMetadata)] pub(crate) struct LongSleepNotForever { module: AsyncModule, @@ -67,7 +71,25 @@ pub(crate) fn long_sleep_not_forever(checker: &Checker, call: &ExprCall) { return; } - let Some(arg) = call.arguments.find_argument_value("seconds", 0) else { + let Some(qualified_name) = checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) + else { + return; + }; + + let Some(module) = AsyncModule::try_from(&qualified_name) else { + return; + }; + + // Determine the correct argument name + let arg_name = match module { + AsyncModule::Trio => "seconds", + AsyncModule::AnyIo => "delay", + AsyncModule::AsyncIo => return, + }; + + let Some(arg) = call.arguments.find_argument_value(arg_name, 0) else { return; }; @@ -78,17 +100,13 @@ pub(crate) fn long_sleep_not_forever(checker: &Checker, call: &ExprCall) { // TODO(ekohilas): Replace with Duration::from_days(1).as_secs(); when available. let one_day_in_secs = 60 * 60 * 24; match value { - Number::Int(int_value) => { - let Some(int_value) = int_value.as_u64() else { - return; - }; - if int_value <= one_day_in_secs { - return; - } - } + Number::Int(int_value) => match int_value.as_u64() { + Some(int_value) if int_value <= one_day_in_secs => return, + _ => {} // The number is too large, and more than 24 hours + }, Number::Float(float_value) => { - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] if *float_value <= one_day_in_secs as f64 { return; } @@ -115,7 +133,7 @@ pub(crate) fn long_sleep_not_forever(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new(LongSleepNotForever { module }, call.range()); + let mut diagnostic = checker.report_diagnostic(LongSleepNotForever { module }, call.range()); let replacement_function = "sleep_forever"; diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( @@ -127,5 +145,4 @@ pub(crate) fn long_sleep_not_forever(checker: &Checker, call: &ExprCall) { let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range()); Ok(Fix::unsafe_edits(import_edit, [reference_edit, arg_edit])) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs index 58d421316d317..dc4e40e68e0a1 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprCall}; use ruff_python_semantic::Modules; use ruff_text_size::{Ranged, TextRange}; @@ -7,6 +6,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::pad; use crate::rules::flake8_async::helpers::MethodName; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for calls to trio functions that are not immediately awaited. @@ -18,12 +18,18 @@ use crate::rules::flake8_async::helpers::MethodName; /// /// ## Example /// ```python +/// import trio +/// +/// /// async def double_sleep(x): /// trio.sleep(2 * x) /// ``` /// /// Use instead: /// ```python +/// import trio +/// +/// /// async def double_sleep(x): /// await trio.sleep(2 * x) /// ``` @@ -80,7 +86,7 @@ pub(crate) fn sync_call(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new(TrioSyncCall { method_name }, call.range); + let mut diagnostic = checker.report_diagnostic(TrioSyncCall { method_name }, call.range); if checker.semantic().in_async_context() { diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( pad( @@ -91,5 +97,4 @@ pub(crate) fn sync_call(checker: &Checker, call: &ExprCall) { call.func.start(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap index b385e812690fe..13439f55826c2 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap @@ -17,7 +17,7 @@ ASYNC110.py:12:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in | |__________________________________^ ASYNC110 | -ASYNC110.py:22:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop +ASYNC110.py:22:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop | 21 | async def func(): 22 | / while True: @@ -25,7 +25,7 @@ ASYNC110.py:22:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.slee | |_____________________________^ ASYNC110 | -ASYNC110.py:27:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop +ASYNC110.py:27:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop | 26 | async def func(): 27 | / while True: @@ -33,7 +33,7 @@ ASYNC110.py:27:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.slee | |___________________________________^ ASYNC110 | -ASYNC110.py:37:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop +ASYNC110.py:37:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop | 36 | async def func(): 37 | / while True: diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap index ab2185ba56c52..18c6ea8dac5e0 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap @@ -133,7 +133,7 @@ ASYNC115.py:59:11: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `tri 61 61 | 62 62 | def func(): -ASYNC115.py:85:11: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:85:11: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 83 | from anyio import sleep 84 | @@ -142,27 +142,19 @@ ASYNC115.py:85:11: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of ` 86 | await anyio.sleep(1) # OK 87 | await anyio.sleep(0, 1) # OK | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -82 83 | import anyio -83 84 | from anyio import sleep -84 85 | +82 82 | import anyio +83 83 | from anyio import sleep +84 84 | 85 |- await anyio.sleep(0) # ASYNC115 - 86 |+ await lowlevel.checkpoint() # ASYNC115 -86 87 | await anyio.sleep(1) # OK -87 88 | await anyio.sleep(0, 1) # OK -88 89 | await anyio.sleep(...) # OK + 85 |+ await anyio.lowlevel.checkpoint() # ASYNC115 +86 86 | await anyio.sleep(1) # OK +87 87 | await anyio.sleep(0, 1) # OK +88 88 | await anyio.sleep(...) # OK -ASYNC115.py:91:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:91:5: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 89 | await anyio.sleep() # OK 90 | @@ -171,27 +163,19 @@ ASYNC115.py:91:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `a 92 | foo = 0 93 | anyio.sleep(foo) # OK | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -88 89 | await anyio.sleep(...) # OK -89 90 | await anyio.sleep() # OK -90 91 | +88 88 | await anyio.sleep(...) # OK +89 89 | await anyio.sleep() # OK +90 90 | 91 |- anyio.sleep(0) # ASYNC115 - 92 |+ lowlevel.checkpoint() # ASYNC115 -92 93 | foo = 0 -93 94 | anyio.sleep(foo) # OK -94 95 | anyio.sleep(1) # OK + 91 |+ anyio.lowlevel.checkpoint() # ASYNC115 +92 92 | foo = 0 +93 93 | anyio.sleep(foo) # OK +94 94 | anyio.sleep(1) # OK -ASYNC115.py:97:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:97:5: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 95 | time.sleep(0) # OK 96 | @@ -200,49 +184,96 @@ ASYNC115.py:97:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `a 98 | 99 | bar = "bar" | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -94 95 | anyio.sleep(1) # OK -95 96 | time.sleep(0) # OK -96 97 | +94 94 | anyio.sleep(1) # OK +95 95 | time.sleep(0) # OK +96 96 | 97 |- sleep(0) # ASYNC115 - 98 |+ lowlevel.checkpoint() # ASYNC115 -98 99 | -99 100 | bar = "bar" -100 101 | anyio.sleep(bar) + 97 |+ anyio.lowlevel.checkpoint() # ASYNC115 +98 98 | +99 99 | bar = "bar" +100 100 | anyio.sleep(bar) -ASYNC115.py:128:15: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:128:15: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 126 | import anyio 127 | 128 | anyio.run(anyio.sleep(0)) # ASYNC115 | ^^^^^^^^^^^^^^ ASYNC115 | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -125 126 | def func(): -126 127 | import anyio -127 128 | +125 125 | def func(): +126 126 | import anyio +127 127 | 128 |- anyio.run(anyio.sleep(0)) # ASYNC115 - 129 |+ anyio.run(lowlevel.checkpoint()) # ASYNC115 -129 130 | -130 131 | -131 132 | def func(): + 128 |+ anyio.run(anyio.lowlevel.checkpoint()) # ASYNC115 +129 129 | +130 130 | +131 131 | def func(): + +ASYNC115.py:156:11: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` + | +154 | await anyio.sleep(seconds=1) # OK +155 | +156 | await anyio.sleep(delay=0) # ASYNC115 + | ^^^^^^^^^^^^^^^^^^^^ ASYNC115 +157 | await anyio.sleep(seconds=0) # OK + | + = help: Replace with `anyio.lowlevel.checkpoint()` + +ℹ Safe fix +153 153 | await anyio.sleep(delay=1) # OK +154 154 | await anyio.sleep(seconds=1) # OK +155 155 | +156 |- await anyio.sleep(delay=0) # ASYNC115 + 156 |+ await anyio.lowlevel.checkpoint() # ASYNC115 +157 157 | await anyio.sleep(seconds=0) # OK +158 158 | +159 159 | + +ASYNC115.py:166:11: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +164 | await trio.sleep(delay=1) # OK +165 | +166 | await trio.sleep(seconds=0) # ASYNC115 + | ^^^^^^^^^^^^^^^^^^^^^ ASYNC115 +167 | await trio.sleep(delay=0) # OK + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Safe fix +163 163 | await trio.sleep(seconds=1) # OK +164 164 | await trio.sleep(delay=1) # OK +165 165 | +166 |- await trio.sleep(seconds=0) # ASYNC115 + 166 |+ await trio.lowlevel.checkpoint() # ASYNC115 +167 167 | await trio.sleep(delay=0) # OK +168 168 | +169 169 | # https://github.com/astral-sh/ruff/issues/18740 + +ASYNC115.py:175:5: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +174 | await ( +175 | / trio # comment +176 | | .sleep( # comment +177 | | 0 # comment +178 | | ) + | |_____^ ASYNC115 +179 | ) + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Unsafe fix +172 172 | import trio +173 173 | +174 174 | await ( +175 |- trio # comment +176 |- .sleep( # comment +177 |- 0 # comment +178 |- ) + 175 |+ trio.lowlevel.checkpoint() +179 176 | ) diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap index c06800468520e..1e04f8065aae6 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap @@ -147,7 +147,7 @@ ASYNC116.py:57:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usu 59 60 | 60 61 | async def import_anyio(): -ASYNC116.py:64:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:64:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 63 | # These examples are probably not meant to ever wake up: 64 | await anyio.sleep(100000) # error: 116, "async" @@ -155,27 +155,19 @@ ASYNC116.py:64:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 65 | 66 | # 'inf literal' overflow trick | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -61 62 | import anyio -62 63 | -63 64 | # These examples are probably not meant to ever wake up: +61 61 | import anyio +62 62 | +63 63 | # These examples are probably not meant to ever wake up: 64 |- await anyio.sleep(100000) # error: 116, "async" - 65 |+ await sleep_forever() # error: 116, "async" -65 66 | -66 67 | # 'inf literal' overflow trick -67 68 | await anyio.sleep(1e999) # error: 116, "async" + 64 |+ await anyio.sleep_forever() # error: 116, "async" +65 65 | +66 66 | # 'inf literal' overflow trick +67 67 | await anyio.sleep(1e999) # error: 116, "async" -ASYNC116.py:67:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:67:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 66 | # 'inf literal' overflow trick 67 | await anyio.sleep(1e999) # error: 116, "async" @@ -183,27 +175,19 @@ ASYNC116.py:67:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 68 | 69 | await anyio.sleep(86399) | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -64 65 | await anyio.sleep(100000) # error: 116, "async" -65 66 | -66 67 | # 'inf literal' overflow trick +64 64 | await anyio.sleep(100000) # error: 116, "async" +65 65 | +66 66 | # 'inf literal' overflow trick 67 |- await anyio.sleep(1e999) # error: 116, "async" - 68 |+ await sleep_forever() # error: 116, "async" -68 69 | -69 70 | await anyio.sleep(86399) -70 71 | await anyio.sleep(86400) + 67 |+ await anyio.sleep_forever() # error: 116, "async" +68 68 | +69 69 | await anyio.sleep(86399) +70 70 | await anyio.sleep(86400) -ASYNC116.py:71:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:71:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 69 | await anyio.sleep(86399) 70 | await anyio.sleep(86400) @@ -211,27 +195,19 @@ ASYNC116.py:71:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should | ^^^^^^^^^^^^^^^^^^^^^ ASYNC116 72 | await anyio.sleep(86401) # error: 116, "async" | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -68 69 | -69 70 | await anyio.sleep(86399) -70 71 | await anyio.sleep(86400) +68 68 | +69 69 | await anyio.sleep(86399) +70 70 | await anyio.sleep(86400) 71 |- await anyio.sleep(86400.01) # error: 116, "async" - 72 |+ await sleep_forever() # error: 116, "async" -72 73 | await anyio.sleep(86401) # error: 116, "async" -73 74 | -74 75 | await anyio.sleep(-1) # will raise a runtime error + 71 |+ await anyio.sleep_forever() # error: 116, "async" +72 72 | await anyio.sleep(86401) # error: 116, "async" +73 73 | +74 74 | await anyio.sleep(-1) # will raise a runtime error -ASYNC116.py:72:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:72:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 70 | await anyio.sleep(86400) 71 | await anyio.sleep(86400.01) # error: 116, "async" @@ -240,27 +216,19 @@ ASYNC116.py:72:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 73 | 74 | await anyio.sleep(-1) # will raise a runtime error | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -69 70 | await anyio.sleep(86399) -70 71 | await anyio.sleep(86400) -71 72 | await anyio.sleep(86400.01) # error: 116, "async" +69 69 | await anyio.sleep(86399) +70 70 | await anyio.sleep(86400) +71 71 | await anyio.sleep(86400.01) # error: 116, "async" 72 |- await anyio.sleep(86401) # error: 116, "async" - 73 |+ await sleep_forever() # error: 116, "async" -73 74 | -74 75 | await anyio.sleep(-1) # will raise a runtime error -75 76 | await anyio.sleep(0) # handled by different check + 72 |+ await anyio.sleep_forever() # error: 116, "async" +73 73 | +74 74 | await anyio.sleep(-1) # will raise a runtime error +75 75 | await anyio.sleep(0) # handled by different check -ASYNC116.py:101:5: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:101:5: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 100 | # does not require the call to be awaited, nor in an async fun 101 | anyio.sleep(86401) # error: 116, "async" @@ -268,66 +236,50 @@ ASYNC116.py:101:5: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 102 | # also checks that we don't break visit_Call 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -98 99 | import anyio -99 100 | -100 101 | # does not require the call to be awaited, nor in an async fun +98 98 | import anyio +99 99 | +100 100 | # does not require the call to be awaited, nor in an async fun 101 |- anyio.sleep(86401) # error: 116, "async" - 102 |+ sleep_forever() # error: 116, "async" -102 103 | # also checks that we don't break visit_Call -103 104 | anyio.run(anyio.sleep(86401)) # error: 116, "async" -104 105 | + 101 |+ anyio.sleep_forever() # error: 116, "async" +102 102 | # also checks that we don't break visit_Call +103 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" +104 104 | -ASYNC116.py:103:15: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:103:15: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 101 | anyio.sleep(86401) # error: 116, "async" 102 | # also checks that we don't break visit_Call 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" | ^^^^^^^^^^^^^^^^^^ ASYNC116 | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -100 101 | # does not require the call to be awaited, nor in an async fun -101 102 | anyio.sleep(86401) # error: 116, "async" -102 103 | # also checks that we don't break visit_Call +100 100 | # does not require the call to be awaited, nor in an async fun +101 101 | anyio.sleep(86401) # error: 116, "async" +102 102 | # also checks that we don't break visit_Call 103 |- anyio.run(anyio.sleep(86401)) # error: 116, "async" - 104 |+ anyio.run(sleep_forever()) # error: 116, "async" -104 105 | -105 106 | -106 107 | async def import_from_anyio(): + 103 |+ anyio.run(anyio.sleep_forever()) # error: 116, "async" +104 104 | +105 105 | +106 106 | async def import_from_anyio(): -ASYNC116.py:110:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:110:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 109 | # catch from import 110 | await sleep(86401) # error: 116, "async" | ^^^^^^^^^^^^ ASYNC116 | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix 2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. 3 3 | import math 4 4 | from math import inf - 5 |+from asyncio import sleep_forever + 5 |+from anyio import sleep_forever 5 6 | 6 7 | 7 8 | async def import_trio(): @@ -337,3 +289,79 @@ ASYNC116.py:110:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 109 110 | # catch from import 110 |- await sleep(86401) # error: 116, "async" 111 |+ await sleep_forever() # error: 116, "async" +111 112 | +112 113 | +113 114 | async def test_anyio_async116_helpers(): + +ASYNC116.py:119:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` + | +117 | await anyio.sleep(seconds=1) # OK +118 | +119 | await anyio.sleep(delay=86401) # ASYNC116 + | ^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC116 +120 | await anyio.sleep(seconds=86401) # OK + | + = help: Replace with `anyio.sleep_forever()` + +ℹ Unsafe fix +116 116 | await anyio.sleep(delay=1) # OK +117 117 | await anyio.sleep(seconds=1) # OK +118 118 | +119 |- await anyio.sleep(delay=86401) # ASYNC116 + 119 |+ await anyio.sleep_forever() # ASYNC116 +120 120 | await anyio.sleep(seconds=86401) # OK +121 121 | +122 122 | + +ASYNC116.py:129:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()` + | +127 | await trio.sleep(delay=1) # OK +128 | +129 | await trio.sleep(seconds=86401) # ASYNC116 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC116 +130 | await trio.sleep(delay=86401) # OK + | + = help: Replace with `trio.sleep_forever()` + +ℹ Unsafe fix +126 126 | await trio.sleep(seconds=1) # OK +127 127 | await trio.sleep(delay=1) # OK +128 128 | +129 |- await trio.sleep(seconds=86401) # ASYNC116 + 129 |+ await trio.sleep_forever() # ASYNC116 +130 130 | await trio.sleep(delay=86401) # OK +131 131 | +132 132 | + +ASYNC116.py:137:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()` + | +135 | from trio import sleep +136 | +137 | await sleep(18446744073709551616) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC116 +138 | await trio.sleep(99999999999999999999) + | + = help: Replace with `trio.sleep_forever()` + +ℹ Unsafe fix +134 134 | import trio +135 135 | from trio import sleep +136 136 | +137 |- await sleep(18446744073709551616) + 137 |+ await trio.sleep_forever() +138 138 | await trio.sleep(99999999999999999999) + +ASYNC116.py:138:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()` + | +137 | await sleep(18446744073709551616) +138 | await trio.sleep(99999999999999999999) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ASYNC116 + | + = help: Replace with `trio.sleep_forever()` + +ℹ Unsafe fix +135 135 | from trio import sleep +136 136 | +137 137 | await sleep(18446744073709551616) +138 |- await trio.sleep(99999999999999999999) + 138 |+ await trio.sleep_forever() diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index 5f5d7e4bfd311..46e9a53e4538a 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -10,10 +10,10 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; + use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::Assert, Path::new("S101.py"))] @@ -94,7 +94,7 @@ mod tests { Path::new("flake8_bandit").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -104,7 +104,6 @@ mod tests { #[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))] #[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))] #[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))] - #[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", @@ -118,7 +117,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -140,7 +139,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -161,7 +160,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -182,7 +181,7 @@ mod tests { ..LinterSettings::for_rule(Rule::HardcodedTempFile) }, )?; - assert_messages!("S108_extend", diagnostics); + assert_diagnostics!("S108_extend", diagnostics); Ok(()) } @@ -198,7 +197,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TryExceptPass) }, )?; - assert_messages!("S110_typed", diagnostics); + assert_diagnostics!("S110_typed", diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs index 458a8335b30d0..1366cb7ac7981 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs @@ -1,9 +1,11 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; +use ruff_text_size::Ranged; use ruff_text_size::{TextLen, TextRange}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_text_size::Ranged; +use crate::Violation; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks for uses of the `assert` keyword. @@ -41,6 +43,6 @@ impl Violation for Assert { } /// S101 -pub(crate) fn assert_used(stmt: &Stmt) -> Diagnostic { - Diagnostic::new(Assert, TextRange::at(stmt.start(), "assert".text_len())) +pub(crate) fn assert_used(checker: &Checker, stmt: &Stmt) { + checker.report_diagnostic(Assert, TextRange::at(stmt.start(), "assert".text_len())); } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs index b4a768dfc5b88..dab899b43aa01 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -1,12 +1,12 @@ use anyhow::Result; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -78,22 +78,22 @@ pub(crate) fn bad_file_permissions(checker: &Checker, call: &ast::ExprCall) { // The mask is a valid integer value -- check for overly permissive permissions. Ok(Some(mask)) => { if (mask & WRITE_WORLD > 0) || (mask & EXECUTE_GROUP > 0) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadFilePermissions { reason: Reason::Permissive(mask), }, mode_arg.range(), - )); + ); } } // The mask is an invalid integer value (i.e., it's out of range). Err(_) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadFilePermissions { reason: Reason::Invalid, }, mode_arg.range(), - )); + ); } } } @@ -166,6 +166,7 @@ fn parse_mask(expr: &Expr, semantic: &SemanticModel) -> Result> { op, right, range: _, + node_index: _, }) => { let Some(left_value) = parse_mask(left, semantic)? else { return Ok(None); diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs index b1e033a38b365..0ad0de23d54bb 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_extra.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprAttribute}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -54,7 +54,7 @@ pub(crate) fn django_extra(checker: &Checker, call: &ast::ExprCall) { } if is_call_insecure(call) { - checker.report_diagnostic(Diagnostic::new(DjangoExtra, call.arguments.range())); + checker.report_diagnostic(DjangoExtra, call.arguments.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs index 338bcb44a1200..13c3556d23e14 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -55,7 +55,7 @@ pub(crate) fn django_raw_sql(checker: &Checker, call: &ast::ExprCall) { .find_argument_value("sql", 0) .is_some_and(Expr::is_string_literal_expr) { - checker.report_diagnostic(Diagnostic::new(DjangoRawSql, call.func.range())); + checker.report_diagnostic(DjangoRawSql, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs index 4376b4b2ffbc1..f66347d6f4005 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -34,6 +34,6 @@ impl Violation for ExecBuiltin { /// S102 pub(crate) fn exec_used(checker: &Checker, func: &Expr) { if checker.semantic().match_builtin_expr(func, "exec") { - checker.report_diagnostic(Diagnostic::new(ExecBuiltin, func.range())); + checker.report_diagnostic(ExecBuiltin, func.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs index 348b504f7beb9..9f147c6eec50a 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::{Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -18,7 +18,7 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python -/// import flask +/// from flask import Flask /// /// app = Flask() /// @@ -27,7 +27,9 @@ use crate::checkers::ast::Checker; /// /// Use instead: /// ```python -/// import flask +/// import os +/// +/// from flask import Flask /// /// app = Flask() /// @@ -67,6 +69,6 @@ pub(crate) fn flask_debug_true(checker: &Checker, call: &ExprCall) { if typing::resolve_assignment(value, checker.semantic()) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["flask", "Flask"])) { - checker.report_diagnostic(Diagnostic::new(FlaskDebugTrue, debug_argument.range())); + checker.report_diagnostic(FlaskDebugTrue, debug_argument.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index 486428d1ef1c2..843dd5c3b6143 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, StringLike}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -41,8 +41,7 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &Checker, string: StringLik match string { StringLike::String(ast::ExprStringLiteral { value, .. }) => { if value == "0.0.0.0" { - checker - .report_diagnostic(Diagnostic::new(HardcodedBindAllInterfaces, string.range())); + checker.report_diagnostic(HardcodedBindAllInterfaces, string.range()); } } StringLike::FString(ast::ExprFString { value, .. }) => { @@ -50,25 +49,23 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &Checker, string: StringLik match part { ast::FStringPart::Literal(literal) => { if &**literal == "0.0.0.0" { - checker.report_diagnostic(Diagnostic::new( - HardcodedBindAllInterfaces, - literal.range(), - )); + checker.report_diagnostic(HardcodedBindAllInterfaces, literal.range()); } } ast::FStringPart::FString(f_string) => { for literal in f_string.elements.literals() { if &**literal == "0.0.0.0" { - checker.report_diagnostic(Diagnostic::new( - HardcodedBindAllInterfaces, - literal.range(), - )); + checker + .report_diagnostic(HardcodedBindAllInterfaces, literal.range()); } } } } } } + StringLike::Bytes(_) => (), + // TODO(dylan): decide whether to trigger here + StringLike::TString(_) => (), } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index 32d501bb4bd07..872ef4b7a9ae4 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{Expr, Parameter, Parameters}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::{matches_password_name, string_literal}; +use crate::rules::flake8_bandit::helpers::{matches_password_name, string_literal}; /// ## What it does /// Checks for potential uses of hardcoded passwords in function argument @@ -54,18 +54,20 @@ impl Violation for HardcodedPasswordDefault { } } -fn check_password_kwarg(parameter: &Parameter, default: &Expr) -> Option { - string_literal(default).filter(|string| !string.is_empty())?; +fn check_password_kwarg(checker: &Checker, parameter: &Parameter, default: &Expr) { + if string_literal(default).is_none_or(str::is_empty) { + return; + } let kwarg_name = ¶meter.name; if !matches_password_name(kwarg_name) { - return None; + return; } - Some(Diagnostic::new( + checker.report_diagnostic( HardcodedPasswordDefault { name: kwarg_name.to_string(), }, default.range(), - )) + ); } /// S107 @@ -74,8 +76,6 @@ pub(crate) fn hardcoded_password_default(checker: &Checker, parameters: &Paramet let Some(default) = parameter.default() else { continue; }; - if let Some(diagnostic) = check_password_kwarg(¶meter.parameter, default) { - checker.report_diagnostic(diagnostic); - } + check_password_kwarg(checker, ¶meter.parameter, default); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs index e9799c562332b..f3cf884655cb1 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs @@ -1,12 +1,12 @@ use ruff_python_ast::Keyword; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::{matches_password_name, string_literal}; +use crate::rules::flake8_bandit::helpers::{matches_password_name, string_literal}; /// ## What it does /// Checks for potential uses of hardcoded passwords in function calls. @@ -52,17 +52,21 @@ impl Violation for HardcodedPasswordFuncArg { /// S106 pub(crate) fn hardcoded_password_func_arg(checker: &Checker, keywords: &[Keyword]) { - checker.report_diagnostics(keywords.iter().filter_map(|keyword| { - string_literal(&keyword.value).filter(|string| !string.is_empty())?; - let arg = keyword.arg.as_ref()?; + for keyword in keywords { + if string_literal(&keyword.value).is_none_or(str::is_empty) { + continue; + } + let Some(arg) = &keyword.arg else { + continue; + }; if !matches_password_name(arg) { - return None; + continue; } - Some(Diagnostic::new( + checker.report_diagnostic( HardcodedPasswordFuncArg { name: arg.to_string(), }, keyword.range(), - )) - })); + ); + } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs index 082a655c48cc5..3711376801717 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_password_string.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::{matches_password_name, string_literal}; +use crate::rules::flake8_bandit::helpers::{matches_password_name, string_literal}; /// ## What it does /// Checks for potential uses of hardcoded passwords in strings. @@ -76,16 +76,20 @@ pub(crate) fn compare_to_hardcoded_password_string( left: &Expr, comparators: &[Expr], ) { - checker.report_diagnostics(comparators.iter().filter_map(|comp| { - string_literal(comp).filter(|string| !string.is_empty())?; - let name = password_target(left)?; - Some(Diagnostic::new( + for comp in comparators { + if string_literal(comp).is_none_or(str::is_empty) { + continue; + } + let Some(name) = password_target(left) else { + continue; + }; + checker.report_diagnostic( HardcodedPasswordString { name: name.to_string(), }, comp.range(), - )) - })); + ); + } } /// S105 @@ -96,12 +100,12 @@ pub(crate) fn assign_hardcoded_password_string(checker: &Checker, value: &Expr, { for target in targets { if let Some(name) = password_target(target) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( HardcodedPasswordString { name: name.to_string(), }, value.range(), - )); + ); return; } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index f29cd41d30c02..76baa9c47988a 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -2,14 +2,14 @@ use std::sync::LazyLock; use regex::Regex; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::str::raw_contents; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::Checker; static SQL_REGEX: LazyLock = LazyLock::new(|| { Regex::new( @@ -100,12 +100,21 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) { } // f"select * from table where val = {val}" - Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()), + Expr::FString(f_string) + if f_string.value.f_strings().any(|fs| { + fs.elements + .iter() + .any(ast::InterpolatedStringElement::is_interpolation) + }) => + { + concatenated_f_string(f_string, checker.locator()) + } + _ => return, }; if SQL_REGEX.is_match(&content) { - checker.report_diagnostic(Diagnostic::new(HardcodedSQLExpression, expr.range())); + checker.report_diagnostic(HardcodedSQLExpression, expr.range()); } } @@ -167,6 +176,8 @@ fn is_explicit_concatenation(expr: &Expr) -> Option { Expr::DictComp(_) => Some(false), Expr::Compare(_) => Some(false), Expr::FString(_) => Some(true), + // TODO(dylan): decide whether to trigger here + Expr::TString(_) => Some(false), Expr::StringLiteral(_) => Some(true), Expr::BytesLiteral(_) => Some(false), Expr::NoneLiteral(_) => Some(false), @@ -190,7 +201,7 @@ fn is_explicit_concatenation(expr: &Expr) -> Option { .iter() .map(is_explicit_concatenation) .collect::>(); - if values.iter().any(|v| *v == Some(true)) { + if values.contains(&Some(true)) { Some(true) } else if values.iter().all(|v| *v == Some(false)) { Some(false) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index f04a458e00efd..77893289e3f83 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr, StringLike}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -75,13 +75,16 @@ pub(crate) fn hardcoded_tmp_directory(checker: &Checker, string: StringLike) { } } } + // These are not actually strings StringLike::Bytes(_) => (), + // TODO(dylan) - verify that we should skip these + StringLike::TString(_) => (), } } fn check(checker: &Checker, value: &str, range: TextRange) { if !checker - .settings + .settings() .flake8_bandit .hardcoded_tmp_directory .iter() @@ -102,10 +105,10 @@ fn check(checker: &Checker, value: &str, range: TextRange) { } } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( HardcodedTempFile { string: value.to_string(), }, range, - )); + ); } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 3ee3b86c3d37f..1de990e0e57c3 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_false; use ruff_python_ast::{self as ast, Arguments}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::string_literal; +use crate::rules::flake8_bandit::helpers::string_literal; /// ## What it does /// Checks for uses of weak or broken cryptographic hash functions in @@ -141,23 +141,23 @@ fn detect_insecure_hashlib_calls( hash_func_name.to_ascii_lowercase().as_str(), "md4" | "md5" | "sha" | "sha1" ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( HashlibInsecureHashFunction { library: "hashlib".to_string(), string: hash_func_name.to_string(), }, name_arg.range(), - )); + ); } } HashlibCall::WeakHash(func_name) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( HashlibInsecureHashFunction { library: "hashlib".to_string(), string: (*func_name).to_string(), }, call.func.range(), - )); + ); } } } @@ -186,13 +186,13 @@ fn detect_insecure_crypt_calls(checker: &Checker, call: &ast::ExprCall) { qualified_name.segments(), ["crypt", "METHOD_CRYPT" | "METHOD_MD5" | "METHOD_BLOWFISH"] ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( HashlibInsecureHashFunction { library: "crypt".to_string(), string: qualified_name.to_string(), }, method.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 988ad1577449d..475a24dfaea2c 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -70,23 +70,20 @@ pub(crate) fn jinja2_autoescape_false(checker: &Checker, call: &ast::ExprCall) { Expr::Call(ast::ExprCall { func, .. }) => { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if id != "select_autoescape" { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( Jinja2AutoescapeFalse { value: true }, keyword.range(), - )); + ); } } } - _ => checker.report_diagnostic(Diagnostic::new( - Jinja2AutoescapeFalse { value: true }, - keyword.range(), - )), + _ => { + checker + .report_diagnostic(Jinja2AutoescapeFalse { value: true }, keyword.range()); + } } } else { - checker.report_diagnostic(Diagnostic::new( - Jinja2AutoescapeFalse { value: false }, - call.func.range(), - )); + checker.report_diagnostic(Jinja2AutoescapeFalse { value: false }, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index c4f140040f30e..16d9b307b59cb 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -51,9 +51,6 @@ pub(crate) fn logging_config_insecure_listen(checker: &Checker, call: &ast::Expr return; } - checker.report_diagnostic(Diagnostic::new( - LoggingConfigInsecureListen, - call.func.range(), - )); + checker.report_diagnostic(LoggingConfigInsecureListen, call.func.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs index 1698f003e0241..b62a0b39e7411 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs @@ -1,9 +1,10 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; + /// ## What it does /// Checks for uses of the `mako` templates. /// @@ -50,6 +51,6 @@ pub(crate) fn mako_templates(checker: &Checker, call: &ast::ExprCall) { matches!(qualified_name.segments(), ["mako", "template", "Template"]) }) { - checker.report_diagnostic(Diagnostic::new(MakoTemplates, call.func.range())); + checker.report_diagnostic(MakoTemplates, call.func.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs index 11b18dc3c8422..8eb8ec0bfc1ae 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -45,6 +45,6 @@ pub(crate) fn paramiko_call(checker: &Checker, func: &Expr) { matches!(qualified_name.segments(), ["paramiko", "exec_command"]) }) { - checker.report_diagnostic(Diagnostic::new(ParamikoCall, func.range())); + checker.report_diagnostic(ParamikoCall, func.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index 69309cb25bfcc..c22bf55168c37 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::helpers::is_const_false; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -51,22 +51,26 @@ pub(crate) fn request_with_no_cert_validation(checker: &Checker, call: &ast::Exp .semantic() .resolve_qualified_name(&call.func) .and_then(|qualified_name| match qualified_name.segments() { - ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => { - Some("requests") - } - ["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" - | "stream" | "Client" | "AsyncClient"] => Some("httpx"), + [ + "requests", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete", + ] => Some("requests"), + [ + "httpx", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" + | "stream" | "Client" | "AsyncClient", + ] => Some("httpx"), _ => None, }) { if let Some(keyword) = call.arguments.find_keyword("verify") { if is_const_false(&keyword.value) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( RequestWithNoCertValidation { string: target.to_string(), }, keyword.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs index 91c684093da98..b6d3352177c51 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -56,27 +56,36 @@ pub(crate) fn request_without_timeout(checker: &Checker, call: &ast::ExprCall) { .semantic() .resolve_qualified_name(&call.func) .and_then(|qualified_name| match qualified_name.segments() { - ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"] => { - Some("requests") - } - ["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" | "stream" | "Client" | "AsyncClient"] => { - Some("httpx") - } + [ + "requests", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request", + ] => Some("requests"), + [ + "httpx", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" + | "stream" | "Client" | "AsyncClient", + ] => Some("httpx"), _ => None, }) { if let Some(keyword) = call.arguments.find_keyword("timeout") { if keyword.value.is_none_literal_expr() { - checker.report_diagnostic(Diagnostic::new( - RequestWithoutTimeout { implicit: false, module: module.to_string() }, + checker.report_diagnostic( + RequestWithoutTimeout { + implicit: false, + module: module.to_string(), + }, keyword.range(), - )); + ); } } else if module == "requests" { - checker.report_diagnostic(Diagnostic::new( - RequestWithoutTimeout { implicit: true, module: module.to_string() }, + checker.report_diagnostic( + RequestWithoutTimeout { + implicit: true, + module: module.to_string(), + }, call.func.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs index 539b03ad5a39f..1e3a1c6fd1ed2 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs @@ -1,12 +1,12 @@ //! Checks relating to shell injection. -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::{ checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal, }; @@ -108,10 +108,10 @@ impl Violation for SubprocessWithoutShellEqualsTrue { /// /// ## Example /// ```python -/// import subprocess +/// import my_custom_subprocess /// /// user_input = input("Enter a command: ") -/// subprocess.run(user_input, shell=True) +/// my_custom_subprocess.run(user_input, shell=True) /// ``` /// /// ## References @@ -265,14 +265,14 @@ impl Violation for StartProcessWithPartialPath { /// ```python /// import subprocess /// -/// subprocess.Popen(["chmod", "777", "*.py"]) +/// subprocess.Popen(["chmod", "777", "*.py"], shell=True) /// ``` /// /// Use instead: /// ```python /// import subprocess /// -/// subprocess.Popen(["chmod", "777", "main.py"]) +/// subprocess.Popen(["chmod", "777", "main.py"], shell=True) /// ``` /// /// ## References @@ -288,11 +288,11 @@ impl Violation for UnixCommandWildcardInjection { } /// Check if an expression is a trusted input for subprocess.run. -/// We assume that any str or list[str] literal can be trusted. +/// We assume that any str, list[str] or tuple[str] literal can be trusted. fn is_trusted_input(arg: &Expr) -> bool { match arg { Expr::StringLiteral(_) => true, - Expr::List(ast::ExprList { elts, .. }) => { + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { elts.iter().all(|elt| matches!(elt, Expr::StringLiteral(_))) } Expr::Named(named) => is_trusted_input(&named.value), @@ -312,25 +312,21 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) { Some(ShellKeyword { truthiness: truthiness @ (Truthiness::True | Truthiness::Truthy), }) => { - if checker.enabled(Rule::SubprocessPopenWithShellEqualsTrue) { - checker.report_diagnostic(Diagnostic::new( - SubprocessPopenWithShellEqualsTrue { - safety: Safety::from(arg), - is_exact: matches!(truthiness, Truthiness::True), - }, - call.func.range(), - )); - } + checker.report_diagnostic_if_enabled( + SubprocessPopenWithShellEqualsTrue { + safety: Safety::from(arg), + is_exact: matches!(truthiness, Truthiness::True), + }, + call.func.range(), + ); } // S603 _ => { - if !is_trusted_input(arg) || checker.settings.preview.is_disabled() { - if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) { - checker.report_diagnostic(Diagnostic::new( - SubprocessWithoutShellEqualsTrue, - call.func.range(), - )); - } + if !is_trusted_input(arg) { + checker.report_diagnostic_if_enabled( + SubprocessWithoutShellEqualsTrue, + call.func.range(), + ); } } } @@ -340,53 +336,48 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) { }) = shell_keyword { // S604 - if checker.enabled(Rule::CallWithShellEqualsTrue) { - checker.report_diagnostic(Diagnostic::new( - CallWithShellEqualsTrue { - is_exact: matches!(truthiness, Truthiness::True), - }, - call.func.range(), - )); - } + checker.report_diagnostic_if_enabled( + CallWithShellEqualsTrue { + is_exact: matches!(truthiness, Truthiness::True), + }, + call.func.range(), + ); } // S605 - if checker.enabled(Rule::StartProcessWithAShell) { + if checker.is_rule_enabled(Rule::StartProcessWithAShell) { if matches!(call_kind, Some(CallKind::Shell)) { if let Some(arg) = call.arguments.args.first() { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( StartProcessWithAShell { safety: Safety::from(arg), }, call.func.range(), - )); + ); } } } // S606 - if checker.enabled(Rule::StartProcessWithNoShell) { + if checker.is_rule_enabled(Rule::StartProcessWithNoShell) { if matches!(call_kind, Some(CallKind::NoShell)) { - checker.report_diagnostic(Diagnostic::new(StartProcessWithNoShell, call.func.range())); + checker.report_diagnostic(StartProcessWithNoShell, call.func.range()); } } // S607 - if checker.enabled(Rule::StartProcessWithPartialPath) { + if checker.is_rule_enabled(Rule::StartProcessWithPartialPath) { if call_kind.is_some() { if let Some(arg) = call.arguments.args.first() { if is_partial_path(arg) { - checker.report_diagnostic(Diagnostic::new( - StartProcessWithPartialPath, - arg.range(), - )); + checker.report_diagnostic(StartProcessWithPartialPath, arg.range()); } } } } // S609 - if checker.enabled(Rule::UnixCommandWildcardInjection) { + if checker.is_rule_enabled(Rule::UnixCommandWildcardInjection) { if matches!(call_kind, Some(CallKind::Shell)) || matches!( (call_kind, shell_keyword), @@ -400,10 +391,7 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) { { if let Some(arg) = call.arguments.args.first() { if is_wildcard_command(arg) { - checker.report_diagnostic(Diagnostic::new( - UnixCommandWildcardInjection, - arg.range(), - )); + checker.report_diagnostic(UnixCommandWildcardInjection, arg.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 143c6bcd741d1..c7e6affb4837d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Int}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -60,7 +60,7 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) { .. }) ) { - checker.report_diagnostic(Diagnostic::new(SnmpInsecureVersion, keyword.range())); + checker.report_diagnostic(SnmpInsecureVersion, keyword.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index 4f159283724cc..f7453cae1e2ad 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -52,7 +52,7 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) { ) }) { - checker.report_diagnostic(Diagnostic::new(SnmpWeakCryptography, call.func.range())); + checker.report_diagnostic(SnmpWeakCryptography, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs index e3153d271f7eb..b09cd8e4aceb9 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_callable; use ruff_python_ast::{Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -78,9 +78,6 @@ pub(crate) fn ssh_no_host_key_verification(checker: &Checker, call: &ExprCall) { ["paramiko", "client", "SSHClient"] | ["paramiko", "SSHClient"] ) }) { - checker.report_diagnostic(Diagnostic::new( - SSHNoHostKeyVerification, - policy_argument.range(), - )); + checker.report_diagnostic(SSHNoHostKeyVerification, policy_argument.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs index 1782d9097eef7..d022d1711ad19 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprCall}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -68,22 +68,22 @@ pub(crate) fn ssl_insecure_version(checker: &Checker, call: &ExprCall) { match &keyword.value { Expr::Name(ast::ExprName { id, .. }) => { if is_insecure_protocol(id) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( SslInsecureVersion { protocol: id.to_string(), }, keyword.range(), - )); + ); } } Expr::Attribute(ast::ExprAttribute { attr, .. }) => { if is_insecure_protocol(attr) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( SslInsecureVersion { protocol: attr.to_string(), }, keyword.range(), - )); + ); } } _ => {} diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs index a630787f9c248..1acb8d4ef4e02 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_bad_defaults.rs @@ -1,7 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, StmtFunctionDef}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -57,22 +57,22 @@ pub(crate) fn ssl_with_bad_defaults(checker: &Checker, function_def: &StmtFuncti match default { Expr::Name(ast::ExprName { id, range, .. }) => { if is_insecure_protocol(id.as_str()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( SslWithBadDefaults { protocol: id.to_string(), }, *range, - )); + ); } } Expr::Attribute(ast::ExprAttribute { attr, range, .. }) => { if is_insecure_protocol(attr.as_str()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( SslWithBadDefaults { protocol: attr.to_string(), }, *range, - )); + ); } } _ => {} diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs index 9ae61d2a75f29..20cea65ce7a5c 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -43,7 +43,7 @@ pub(crate) fn ssl_with_no_version(checker: &Checker, call: &ExprCall) { .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["ssl", "wrap_socket"])) { if call.arguments.find_keyword("ssl_version").is_none() { - checker.report_diagnostic(Diagnostic::new(SslWithNoVersion, call.range())); + checker.report_diagnostic(SslWithNoVersion, call.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index f47ecc06f4d20..4e2a1dc35c2f8 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -2,13 +2,13 @@ //! //! See: use itertools::Either; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operator}; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::registry::AsRule; +use crate::preview::is_suspicious_function_reference_enabled; /// ## What it does /// Checks for calls to `pickle` functions or modules that wrap them. @@ -789,9 +789,9 @@ impl Violation for SuspiciousXMLPullDOMUsage { } } -/// ## Deprecation +/// ## Removed /// -/// This rule was deprecated as the `lxml` library has been modified to address +/// This rule was removed as the `lxml` library has been modified to address /// known vulnerabilities and unsafe defaults. As such, the `defusedxml` /// library is no longer necessary, `defusedxml` has [deprecated] its `lxml` /// module. @@ -936,7 +936,7 @@ pub(crate) fn suspicious_function_call(checker: &Checker, call: &ExprCall) { } pub(crate) fn suspicious_function_reference(checker: &Checker, func: &Expr) { - if checker.settings.preview.is_disabled() { + if !is_suspicious_function_reference_enabled(checker.settings()) { return; } @@ -1006,9 +1006,9 @@ fn suspicious_function( // Ex) f"foo" Expr::FString(ast::ExprFString { value, .. }) => { value.elements().next().and_then(|element| { - if let ast::FStringElement::Literal(ast::FStringLiteralElement { - value, .. - }) = element + if let ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) = element { Some(Either::Right(value.chars())) } else { @@ -1034,39 +1034,71 @@ fn suspicious_function( return; }; - let diagnostic_kind: DiagnosticKind = match qualified_name.segments() { + match qualified_name.segments() { // Pickle ["pickle" | "dill", "load" | "loads" | "Unpickler"] | ["shelve", "open" | "DbfilenameShelf"] | ["jsonpickle", "decode"] | ["jsonpickle", "unpickler", "decode"] - | ["pandas", "read_pickle"] => SuspiciousPickleUsage.into(), + | ["pandas", "read_pickle"] => { + checker.report_diagnostic_if_enabled(SuspiciousPickleUsage, range) + } // Marshal - ["marshal", "load" | "loads"] => SuspiciousMarshalUsage.into(), + ["marshal", "load" | "loads"] => { + checker.report_diagnostic_if_enabled(SuspiciousMarshalUsage, range) + } // InsecureHash - ["Crypto" | "Cryptodome", "Hash", "SHA" | "MD2" | "MD3" | "MD4" | "MD5", "new"] - | ["cryptography", "hazmat", "primitives", "hashes", "SHA1" | "MD5"] => { - SuspiciousInsecureHashUsage.into() - } + [ + "Crypto" | "Cryptodome", + "Hash", + "SHA" | "MD2" | "MD3" | "MD4" | "MD5", + "new", + ] + | [ + "cryptography", + "hazmat", + "primitives", + "hashes", + "SHA1" | "MD5", + ] => checker.report_diagnostic_if_enabled(SuspiciousInsecureHashUsage, range), // InsecureCipher - ["Crypto" | "Cryptodome", "Cipher", "ARC2" | "Blowfish" | "DES" | "XOR", "new"] - | ["cryptography", "hazmat", "primitives", "ciphers", "algorithms", "ARC4" | "Blowfish" | "IDEA"] => { - SuspiciousInsecureCipherUsage.into() - } + [ + "Crypto" | "Cryptodome", + "Cipher", + "ARC2" | "Blowfish" | "DES" | "XOR", + "new", + ] + | [ + "cryptography", + "hazmat", + "primitives", + "ciphers", + "algorithms", + "ARC4" | "Blowfish" | "IDEA", + ] => checker.report_diagnostic_if_enabled(SuspiciousInsecureCipherUsage, range), // InsecureCipherMode - ["cryptography", "hazmat", "primitives", "ciphers", "modes", "ECB"] => { - SuspiciousInsecureCipherModeUsage.into() - } + [ + "cryptography", + "hazmat", + "primitives", + "ciphers", + "modes", + "ECB", + ] => checker.report_diagnostic_if_enabled(SuspiciousInsecureCipherModeUsage, range), // Mktemp - ["tempfile", "mktemp"] => SuspiciousMktempUsage.into(), + ["tempfile", "mktemp"] => { + checker.report_diagnostic_if_enabled(SuspiciousMktempUsage, range) + } // Eval - ["" | "builtins", "eval"] => SuspiciousEvalUsage.into(), + ["" | "builtins", "eval"] => { + checker.report_diagnostic_if_enabled(SuspiciousEvalUsage, range) + } // MarkSafe ["django", "utils", "safestring" | "html", "mark_safe"] => { @@ -1077,7 +1109,7 @@ fn suspicious_function( } } } - SuspiciousMarkSafeUsage.into() + checker.report_diagnostic_if_enabled(SuspiciousMarkSafeUsage, range) } // URLOpen (`Request`) @@ -1099,12 +1131,18 @@ fn suspicious_function( } } } - SuspiciousURLOpenUsage.into() + checker.report_diagnostic_if_enabled(SuspiciousURLOpenUsage, range) } // URLOpen (`urlopen`, `urlretrieve`) ["urllib", "request", "urlopen" | "urlretrieve"] - | ["six", "moves", "urllib", "request", "urlopen" | "urlretrieve"] => { + | [ + "six", + "moves", + "urllib", + "request", + "urlopen" | "urlretrieve", + ] => { if let Some(arguments) = arguments { if arguments.args.iter().all(|arg| !arg.is_starred_expr()) && arguments @@ -1145,72 +1183,94 @@ fn suspicious_function( } } } - SuspiciousURLOpenUsage.into() + checker.report_diagnostic_if_enabled(SuspiciousURLOpenUsage, range) } // URLOpen (`URLopener`, `FancyURLopener`) ["urllib", "request", "URLopener" | "FancyURLopener"] - | ["six", "moves", "urllib", "request", "URLopener" | "FancyURLopener"] => { - SuspiciousURLOpenUsage.into() - } + | [ + "six", + "moves", + "urllib", + "request", + "URLopener" | "FancyURLopener", + ] => checker.report_diagnostic_if_enabled(SuspiciousURLOpenUsage, range), // NonCryptographicRandom - ["random", "Random" | "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" - | "triangular" | "randbytes"] => SuspiciousNonCryptographicRandomUsage.into(), + [ + "random", + "Random" | "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" + | "triangular" | "randbytes", + ] => checker.report_diagnostic_if_enabled(SuspiciousNonCryptographicRandomUsage, range), // UnverifiedContext - ["ssl", "_create_unverified_context"] => SuspiciousUnverifiedContextUsage.into(), + ["ssl", "_create_unverified_context"] => { + checker.report_diagnostic_if_enabled(SuspiciousUnverifiedContextUsage, range) + } // XMLCElementTree - ["xml", "etree", "cElementTree", "parse" | "iterparse" | "fromstring" | "XMLParser"] => { - SuspiciousXMLCElementTreeUsage.into() - } + [ + "xml", + "etree", + "cElementTree", + "parse" | "iterparse" | "fromstring" | "XMLParser", + ] => checker.report_diagnostic_if_enabled(SuspiciousXMLCElementTreeUsage, range), // XMLElementTree - ["xml", "etree", "ElementTree", "parse" | "iterparse" | "fromstring" | "XMLParser"] => { - SuspiciousXMLElementTreeUsage.into() - } + [ + "xml", + "etree", + "ElementTree", + "parse" | "iterparse" | "fromstring" | "XMLParser", + ] => checker.report_diagnostic_if_enabled(SuspiciousXMLElementTreeUsage, range), // XMLExpatReader - ["xml", "sax", "expatreader", "create_parser"] => SuspiciousXMLExpatReaderUsage.into(), + ["xml", "sax", "expatreader", "create_parser"] => { + checker.report_diagnostic_if_enabled(SuspiciousXMLExpatReaderUsage, range) + } // XMLExpatBuilder ["xml", "dom", "expatbuilder", "parse" | "parseString"] => { - SuspiciousXMLExpatBuilderUsage.into() + checker.report_diagnostic_if_enabled(SuspiciousXMLExpatBuilderUsage, range) } // XMLSax - ["xml", "sax", "parse" | "parseString" | "make_parser"] => SuspiciousXMLSaxUsage.into(), + ["xml", "sax", "parse" | "parseString" | "make_parser"] => { + checker.report_diagnostic_if_enabled(SuspiciousXMLSaxUsage, range) + } // XMLMiniDOM - ["xml", "dom", "minidom", "parse" | "parseString"] => SuspiciousXMLMiniDOMUsage.into(), + ["xml", "dom", "minidom", "parse" | "parseString"] => { + checker.report_diagnostic_if_enabled(SuspiciousXMLMiniDOMUsage, range) + } // XMLPullDOM - ["xml", "dom", "pulldom", "parse" | "parseString"] => SuspiciousXMLPullDOMUsage.into(), + ["xml", "dom", "pulldom", "parse" | "parseString"] => { + checker.report_diagnostic_if_enabled(SuspiciousXMLPullDOMUsage, range) + } // XMLETree - ["lxml", "etree", "parse" | "fromstring" | "RestrictedElement" | "GlobalParserTLS" | "getDefaultParser" - | "check_docinfo"] => SuspiciousXMLETreeUsage.into(), + [ + "lxml", + "etree", + "parse" | "fromstring" | "RestrictedElement" | "GlobalParserTLS" | "getDefaultParser" + | "check_docinfo", + ] => checker.report_diagnostic_if_enabled(SuspiciousXMLETreeUsage, range), // Telnet - ["telnetlib", ..] => SuspiciousTelnetUsage.into(), + ["telnetlib", ..] => checker.report_diagnostic_if_enabled(SuspiciousTelnetUsage, range), // FTPLib - ["ftplib", ..] => SuspiciousFTPLibUsage.into(), + ["ftplib", ..] => checker.report_diagnostic_if_enabled(SuspiciousFTPLibUsage, range), _ => return, }; - - let diagnostic = Diagnostic::new(diagnostic_kind, range); - if checker.enabled(diagnostic.kind.rule()) { - checker.report_diagnostic(diagnostic); - } } /// S308 pub(crate) fn suspicious_function_decorator(checker: &Checker, decorator: &Decorator) { // In preview mode, references are handled collectively by `suspicious_function_reference` - if checker.settings.preview.is_disabled() { + if !is_suspicious_function_reference_enabled(checker.settings()) { suspicious_function(checker, &decorator.expression, None, decorator.range); } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs index deb208863d6a7..f83087c43e08d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs @@ -1,13 +1,12 @@ //! Check for imports of or from suspicious modules. //! //! See: -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Stmt}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::registry::AsRule; /// ## What it does /// Checks for imports of the `telnetlib` module. @@ -284,7 +283,7 @@ impl Violation for SuspiciousXmlrpcImport { /// /// ## Example /// ```python -/// import wsgiref.handlers.CGIHandler +/// from wsgiref.handlers import CGIHandler /// ``` /// /// ## References @@ -361,76 +360,49 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) { Stmt::Import(ast::StmtImport { names, .. }) => { for name in names { match name.name.as_str() { - "telnetlib" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousTelnetlibImport), - name.range, - ), - "ftplib" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousFtplibImport), - name.range, - ), - "pickle" | "cPickle" | "dill" | "shelve" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPickleImport), - name.range, - ), - "subprocess" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousSubprocessImport), - name.range, - ), + "telnetlib" => { + checker.report_diagnostic_if_enabled(SuspiciousTelnetlibImport, name.range); + } + "ftplib" => { + checker.report_diagnostic_if_enabled(SuspiciousFtplibImport, name.range); + } + "pickle" | "cPickle" | "dill" | "shelve" => { + checker.report_diagnostic_if_enabled(SuspiciousPickleImport, name.range); + } + "subprocess" => { + checker + .report_diagnostic_if_enabled(SuspiciousSubprocessImport, name.range); + } "xml.etree.cElementTree" | "xml.etree.ElementTree" => { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlEtreeImport), - name.range, - ); + checker.report_diagnostic_if_enabled(SuspiciousXmlEtreeImport, name.range); + } + "xml.sax" => { + checker.report_diagnostic_if_enabled(SuspiciousXmlSaxImport, name.range); + } + "xml.dom.expatbuilder" => { + checker.report_diagnostic_if_enabled(SuspiciousXmlExpatImport, name.range); + } + "xml.dom.minidom" => { + checker + .report_diagnostic_if_enabled(SuspiciousXmlMinidomImport, name.range); + } + "xml.dom.pulldom" => { + checker + .report_diagnostic_if_enabled(SuspiciousXmlPulldomImport, name.range); + } + "lxml" => { + checker.report_diagnostic_if_enabled(SuspiciousLxmlImport, name.range); + } + "xmlrpc" => { + checker.report_diagnostic_if_enabled(SuspiciousXmlrpcImport, name.range); } - "xml.sax" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlSaxImport), - name.range, - ), - "xml.dom.expatbuilder" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlExpatImport), - name.range, - ), - "xml.dom.minidom" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlMinidomImport), - name.range, - ), - "xml.dom.pulldom" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlPulldomImport), - name.range, - ), - "lxml" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousLxmlImport), - name.range, - ), - "xmlrpc" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlrpcImport), - name.range, - ), "Crypto.Cipher" | "Crypto.Hash" | "Crypto.IO" | "Crypto.Protocol" | "Crypto.PublicKey" | "Crypto.Random" | "Crypto.Signature" | "Crypto.Util" => { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPycryptoImport), - name.range, - ); + checker.report_diagnostic_if_enabled(SuspiciousPycryptoImport, name.range); + } + "pyghmi" => { + checker.report_diagnostic_if_enabled(SuspiciousPyghmiImport, name.range); } - "pyghmi" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPyghmiImport), - name.range, - ), _ => {} } } @@ -438,113 +410,107 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) { Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => { let Some(identifier) = module else { return }; match identifier.as_str() { - "telnetlib" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousTelnetlibImport), - identifier.range(), - ), - "ftplib" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousFtplibImport), - identifier.range(), - ), - "pickle" | "cPickle" | "dill" | "shelve" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPickleImport), - identifier.range(), - ), - "subprocess" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousSubprocessImport), - identifier.range(), - ), + "telnetlib" => { + checker.report_diagnostic_if_enabled( + SuspiciousTelnetlibImport, + identifier.range(), + ); + } + "ftplib" => { + checker + .report_diagnostic_if_enabled(SuspiciousFtplibImport, identifier.range()); + } + "pickle" | "cPickle" | "dill" | "shelve" => { + checker + .report_diagnostic_if_enabled(SuspiciousPickleImport, identifier.range()); + } + "subprocess" => { + checker.report_diagnostic_if_enabled( + SuspiciousSubprocessImport, + identifier.range(), + ); + } "xml.etree" => { for name in names { if matches!(name.name.as_str(), "cElementTree" | "ElementTree") { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlEtreeImport), + checker.report_diagnostic_if_enabled( + SuspiciousXmlEtreeImport, identifier.range(), ); } } } "xml.etree.cElementTree" | "xml.etree.ElementTree" => { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlEtreeImport), - identifier.range(), - ); + checker + .report_diagnostic_if_enabled(SuspiciousXmlEtreeImport, identifier.range()); } "xml" => { for name in names { if name.name.as_str() == "sax" { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlSaxImport), + checker.report_diagnostic_if_enabled( + SuspiciousXmlSaxImport, identifier.range(), ); } } } - "xml.sax" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlSaxImport), - identifier.range(), - ), + "xml.sax" => { + checker + .report_diagnostic_if_enabled(SuspiciousXmlSaxImport, identifier.range()); + } "xml.dom" => { for name in names { match name.name.as_str() { - "expatbuilder" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlExpatImport), - identifier.range(), - ), - "minidom" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlMinidomImport), - identifier.range(), - ), - "pulldom" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlPulldomImport), - identifier.range(), - ), - _ => (), + "expatbuilder" => { + checker.report_diagnostic_if_enabled( + SuspiciousXmlExpatImport, + identifier.range(), + ); + } + "minidom" => { + checker.report_diagnostic_if_enabled( + SuspiciousXmlMinidomImport, + identifier.range(), + ); + } + "pulldom" => { + checker.report_diagnostic_if_enabled( + SuspiciousXmlPulldomImport, + identifier.range(), + ); + } + _ => {} } } } - "xml.dom.expatbuilder" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlExpatImport), - identifier.range(), - ), - "xml.dom.minidom" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlMinidomImport), - identifier.range(), - ), - "xml.dom.pulldom" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlPulldomImport), - identifier.range(), - ), - "lxml" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousLxmlImport), - identifier.range(), - ), - "xmlrpc" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousXmlrpcImport), - identifier.range(), - ), + "xml.dom.expatbuilder" => { + checker + .report_diagnostic_if_enabled(SuspiciousXmlExpatImport, identifier.range()); + } + "xml.dom.minidom" => { + checker.report_diagnostic_if_enabled( + SuspiciousXmlMinidomImport, + identifier.range(), + ); + } + "xml.dom.pulldom" => { + checker.report_diagnostic_if_enabled( + SuspiciousXmlPulldomImport, + identifier.range(), + ); + } + "lxml" => { + checker.report_diagnostic_if_enabled(SuspiciousLxmlImport, identifier.range()); + } + "xmlrpc" => { + checker + .report_diagnostic_if_enabled(SuspiciousXmlrpcImport, identifier.range()); + } "wsgiref.handlers" => { for name in names { if name.name.as_str() == "CGIHandler" { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousHttpoxyImport), + checker.report_diagnostic_if_enabled( + SuspiciousHttpoxyImport, identifier.range(), ); } @@ -553,9 +519,8 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) { "twisted.web.twcgi" => { for name in names { if name.name.as_str() == "CGIScript" { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousHttpoxyImport), + checker.report_diagnostic_if_enabled( + SuspiciousHttpoxyImport, identifier.range(), ); } @@ -574,9 +539,8 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) { | "Signature" | "Util" ) { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPycryptoImport), + checker.report_diagnostic_if_enabled( + SuspiciousPycryptoImport, identifier.range(), ); } @@ -584,27 +548,16 @@ pub(crate) fn suspicious_imports(checker: &Checker, stmt: &Stmt) { } "Crypto.Cipher" | "Crypto.Hash" | "Crypto.IO" | "Crypto.Protocol" | "Crypto.PublicKey" | "Crypto.Random" | "Crypto.Signature" | "Crypto.Util" => { - check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPycryptoImport), - identifier.range(), - ); + checker + .report_diagnostic_if_enabled(SuspiciousPycryptoImport, identifier.range()); + } + "pyghmi" => { + checker + .report_diagnostic_if_enabled(SuspiciousPyghmiImport, identifier.range()); } - "pyghmi" => check_and_push_diagnostic( - checker, - DiagnosticKind::from(SuspiciousPyghmiImport), - identifier.range(), - ), _ => {} } } _ => panic!("Expected Stmt::Import | Stmt::ImportFrom"), } } - -fn check_and_push_diagnostic(checker: &Checker, diagnostic_kind: DiagnosticKind, range: TextRange) { - let diagnostic = Diagnostic::new::(diagnostic_kind, range); - if checker.enabled(diagnostic.kind.rule()) { - checker.report_diagnostic(diagnostic); - } -} diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs index e27409678ec39..8e816ee21d770 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs @@ -1,11 +1,11 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; + /// ## What it does /// Checks for uses of `tarfile.extractall`. /// @@ -70,5 +70,5 @@ pub(crate) fn tarfile_unsafe_members(checker: &Checker, call: &ast::ExprCall) { return; } - checker.report_diagnostic(Diagnostic::new(TarfileUnsafeMembers, call.func.range())); + checker.report_diagnostic(TarfileUnsafeMembers, call.func.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs index fa846af54e1f1..5bd463285315d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{ExceptHandler, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; @@ -38,6 +38,9 @@ use crate::rules::flake8_bandit::helpers::is_untyped_exception; /// logging.exception("Error occurred") /// ``` /// +/// ## Options +/// - `lint.flake8-bandit.check-typed-exception` +/// /// ## References /// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) @@ -61,7 +64,7 @@ pub(crate) fn try_except_continue( ) { if matches!(body, [Stmt::Continue(_)]) { if check_typed_exception || is_untyped_exception(type_, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new(TryExceptContinue, except_handler.range())); + checker.report_diagnostic(TryExceptContinue, except_handler.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs index 693d99ac0a231..56dcaff1e09e7 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{ExceptHandler, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; @@ -34,6 +34,9 @@ use crate::rules::flake8_bandit::helpers::is_untyped_exception; /// logging.exception("Exception occurred") /// ``` /// +/// ## Options +/// - `lint.flake8-bandit.check-typed-exception` +/// /// ## References /// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) @@ -57,7 +60,7 @@ pub(crate) fn try_except_pass( ) { if matches!(body, [Stmt::Pass(_)]) { if check_typed_exception || is_untyped_exception(type_, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new(TryExceptPass, except_handler.range())); + checker.report_diagnostic(TryExceptPass, except_handler.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs index 2d0f383eb7acf..25cd68d58b504 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{Expr, ExprCall}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::{checkers::ast::Checker, settings::LinterSettings}; /// ## What it does @@ -90,7 +90,7 @@ impl Violation for UnsafeMarkupUse { /// S704 pub(crate) fn unsafe_markup_call(checker: &Checker, call: &ExprCall) { if checker - .settings + .settings() .flake8_bandit .extend_markup_names .is_empty() @@ -100,7 +100,7 @@ pub(crate) fn unsafe_markup_call(checker: &Checker, call: &ExprCall) { return; } - if !is_unsafe_call(call, checker.semantic(), checker.settings) { + if !is_unsafe_call(call, checker.semantic(), checker.settings()) { return; } @@ -108,16 +108,16 @@ pub(crate) fn unsafe_markup_call(checker: &Checker, call: &ExprCall) { return; }; - if !is_markup_call(&qualified_name, checker.settings) { + if !is_markup_call(&qualified_name, checker.settings()) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnsafeMarkupUse { name: qualified_name.to_string(), }, call.range(), - )); + ); } fn is_markup_call(qualified_name: &QualifiedName, settings: &LinterSettings) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index f0268ebecf0f2..dae9564053fde 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -82,16 +82,10 @@ pub(crate) fn unsafe_yaml_load(checker: &Checker, call: &ast::ExprCall) { Expr::Name(ast::ExprName { id, .. }) => Some(id.to_string()), _ => None, }; - checker.report_diagnostic(Diagnostic::new( - UnsafeYAMLLoad { loader }, - loader_arg.range(), - )); + checker.report_diagnostic(UnsafeYAMLLoad { loader }, loader_arg.range()); } } else { - checker.report_diagnostic(Diagnostic::new( - UnsafeYAMLLoad { loader: None }, - call.func.range(), - )); + checker.report_diagnostic(UnsafeYAMLLoad { loader: None }, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs index 14ba54b1ee860..63b709fc32b44 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs @@ -1,10 +1,10 @@ use std::fmt::{Display, Formatter}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall}; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -55,10 +55,7 @@ pub(crate) fn weak_cryptographic_key(checker: &Checker, call: &ExprCall) { }; if cryptographic_key.is_vulnerable() { - checker.report_diagnostic(Diagnostic::new( - WeakCryptographicKey { cryptographic_key }, - range, - )); + checker.report_diagnostic(WeakCryptographicKey { cryptographic_key }, range); } } @@ -103,37 +100,42 @@ fn extract_cryptographic_key( ) -> Option<(CryptographicKey, TextRange)> { let qualified_name = checker.semantic().resolve_qualified_name(&call.func)?; match qualified_name.segments() { - ["cryptography", "hazmat", "primitives", "asymmetric", function, "generate_private_key"] => { - match *function { - "dsa" => { - let (key_size, range) = extract_int_argument(call, "key_size", 0)?; - Some((CryptographicKey::Dsa { key_size }, range)) - } - "rsa" => { - let (key_size, range) = extract_int_argument(call, "key_size", 1)?; - Some((CryptographicKey::Rsa { key_size }, range)) - } - "ec" => { - let argument = call.arguments.find_argument_value("curve", 0)?; - let ExprAttribute { attr, value, .. } = argument.as_attribute_expr()?; - let qualified_name = checker.semantic().resolve_qualified_name(value)?; - if matches!( - qualified_name.segments(), - ["cryptography", "hazmat", "primitives", "asymmetric", "ec"] - ) { - Some(( - CryptographicKey::Ec { - algorithm: attr.to_string(), - }, - argument.range(), - )) - } else { - None - } + [ + "cryptography", + "hazmat", + "primitives", + "asymmetric", + function, + "generate_private_key", + ] => match *function { + "dsa" => { + let (key_size, range) = extract_int_argument(call, "key_size", 0)?; + Some((CryptographicKey::Dsa { key_size }, range)) + } + "rsa" => { + let (key_size, range) = extract_int_argument(call, "key_size", 1)?; + Some((CryptographicKey::Rsa { key_size }, range)) + } + "ec" => { + let argument = call.arguments.find_argument_value("curve", 0)?; + let ExprAttribute { attr, value, .. } = argument.as_attribute_expr()?; + let qualified_name = checker.semantic().resolve_qualified_name(value)?; + if matches!( + qualified_name.segments(), + ["cryptography", "hazmat", "primitives", "asymmetric", "ec"] + ) { + Some(( + CryptographicKey::Ec { + algorithm: attr.to_string(), + }, + argument.range(), + )) + } else { + None } - _ => None, } - } + _ => None, + }, ["Crypto" | "Cryptodome", "PublicKey", function, "generate"] => match *function { "DSA" => { let (key_size, range) = extract_int_argument(call, "bits", 0)?; diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap index 15fec6bd70646..bcad262f2fb65 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs -snapshot_kind: text --- S104.py:9:1: S104 Possible binding to all interfaces | @@ -48,6 +47,8 @@ S104.py:24:1: S104 Possible binding to all interfaces 23 | # Implicit string concatenation 24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" | ^^^^^^^^^ S104 +25 | +26 | # t-strings - all ok | S104.py:24:13: S104 Possible binding to all interfaces @@ -55,6 +56,8 @@ S104.py:24:13: S104 Possible binding to all interfaces 23 | # Implicit string concatenation 24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" | ^^^^^^^ S104 +25 | +26 | # t-strings - all ok | S104.py:24:26: S104 Possible binding to all interfaces @@ -62,4 +65,6 @@ S104.py:24:26: S104 Possible binding to all interfaces 23 | # Implicit string concatenation 24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" | ^^^^^^^ S104 +25 | +26 | # t-strings - all ok | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S112_S112.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S112_S112.py.snap index 2df863f5fdcfb..2159815d39b25 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S112_S112.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S112_S112.py.snap @@ -1,46 +1,46 @@ --- source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs --- -S112.py:3:1: S112 `try`-`except`-`continue` detected, consider logging the exception +S112.py:4:5: S112 `try`-`except`-`continue` detected, consider logging the exception | -1 | try: -2 | pass -3 | / except Exception: -4 | | continue - | |____________^ S112 -5 | -6 | try: +2 | try: +3 | pass +4 | / except Exception: +5 | | continue + | |________________^ S112 +6 | +7 | try: | -S112.py:8:1: S112 `try`-`except`-`continue` detected, consider logging the exception +S112.py:9:5: S112 `try`-`except`-`continue` detected, consider logging the exception | - 6 | try: - 7 | pass - 8 | / except: - 9 | | continue - | |____________^ S112 -10 | -11 | try: + 7 | try: + 8 | pass + 9 | / except: +10 | | continue + | |________________^ S112 +11 | +12 | try: | -S112.py:13:1: S112 `try`-`except`-`continue` detected, consider logging the exception +S112.py:14:5: S112 `try`-`except`-`continue` detected, consider logging the exception | -11 | try: -12 | pass -13 | / except (Exception,): -14 | | continue - | |____________^ S112 -15 | -16 | try: +12 | try: +13 | pass +14 | / except (Exception,): +15 | | continue + | |________________^ S112 +16 | +17 | try: | -S112.py:18:1: S112 `try`-`except`-`continue` detected, consider logging the exception +S112.py:19:5: S112 `try`-`except`-`continue` detected, consider logging the exception | -16 | try: -17 | pass -18 | / except (Exception, ValueError): -19 | | continue - | |____________^ S112 -20 | -21 | try: +17 | try: +18 | pass +19 | / except (Exception, ValueError): +20 | | continue + | |________________^ S112 +21 | +22 | try: | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap index e0576d382b0df..2b8d7c974f855 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap @@ -106,74 +106,6 @@ S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input 23 | # Literals are fine, they're trusted. | -S603.py:24:1: S603 `subprocess` call: check for execution of untrusted input - | -23 | # Literals are fine, they're trusted. -24 | run("true") - | ^^^ S603 -25 | Popen(["true"]) -26 | Popen("true", shell=False) - | - -S603.py:25:1: S603 `subprocess` call: check for execution of untrusted input - | -23 | # Literals are fine, they're trusted. -24 | run("true") -25 | Popen(["true"]) - | ^^^^^ S603 -26 | Popen("true", shell=False) -27 | call("true", shell=False) - | - -S603.py:26:1: S603 `subprocess` call: check for execution of untrusted input - | -24 | run("true") -25 | Popen(["true"]) -26 | Popen("true", shell=False) - | ^^^^^ S603 -27 | call("true", shell=False) -28 | check_call("true", shell=False) - | - -S603.py:27:1: S603 `subprocess` call: check for execution of untrusted input - | -25 | Popen(["true"]) -26 | Popen("true", shell=False) -27 | call("true", shell=False) - | ^^^^ S603 -28 | check_call("true", shell=False) -29 | check_output("true", shell=False) - | - -S603.py:28:1: S603 `subprocess` call: check for execution of untrusted input - | -26 | Popen("true", shell=False) -27 | call("true", shell=False) -28 | check_call("true", shell=False) - | ^^^^^^^^^^ S603 -29 | check_output("true", shell=False) -30 | run("true", shell=False) - | - -S603.py:29:1: S603 `subprocess` call: check for execution of untrusted input - | -27 | call("true", shell=False) -28 | check_call("true", shell=False) -29 | check_output("true", shell=False) - | ^^^^^^^^^^^^ S603 -30 | run("true", shell=False) - | - -S603.py:30:1: S603 `subprocess` call: check for execution of untrusted input - | -28 | check_call("true", shell=False) -29 | check_output("true", shell=False) -30 | run("true", shell=False) - | ^^^ S603 -31 | -32 | # Not through assignments though. - | - S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input | 32 | # Not through assignments though. @@ -184,15 +116,6 @@ S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input 36 | # Instant named expressions are fine. | -S603.py:37:1: S603 `subprocess` call: check for execution of untrusted input - | -36 | # Instant named expressions are fine. -37 | run(c := "true") - | ^^^ S603 -38 | -39 | # But non-instant are not. - | - S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input | 39 | # But non-instant are not. diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap index bfd23cbc18eb8..b19b9631f606e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap @@ -601,4 +601,15 @@ S608.py:164:11: S608 Possible SQL injection vector through string-based query co 167 | | FROM ({user_input}) raw 168 | | """ | |___^ S608 +169 | +170 | # https://github.com/astral-sh/ruff/issues/17967 + | + +S608.py:180:11: S608 Possible SQL injection vector through string-based query construction + | +178 | FROM ({user_input}) raw +179 | """ +180 | query64 = f"update {t"{table}"} set var = {t"{var}"}" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608 +181 | query65 = t"update {f"{table}"} set var = {f"{var}"}" | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap index 8ee7eb09b6c35..02a25f90f6ecd 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap @@ -42,7 +42,7 @@ S704.py:11:1: S704 Unsafe use of `flask.Markup` detected S704.py:17:1: S704 Unsafe use of `markupsafe.Markup` detected | -15 | # NOTE: We may be able to get rid of these false positives with red-knot +15 | # NOTE: We may be able to get rid of these false positives with ty 16 | # if it includes comprehensive constant expression detection/evaluation. 17 | Markup("*" * 8) # S704 (false positive) | ^^^^^^^^^^^^^^^ S704 diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S603_S603.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S603_S603.py.snap deleted file mode 100644 index 2b8d7c974f855..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S603_S603.py.snap +++ /dev/null @@ -1,125 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs ---- -S603.py:5:1: S603 `subprocess` call: check for execution of untrusted input - | -3 | # Different Popen wrappers are checked. -4 | a = input() -5 | Popen(a, shell=False) - | ^^^^^ S603 -6 | call(a, shell=False) -7 | check_call(a, shell=False) - | - -S603.py:6:1: S603 `subprocess` call: check for execution of untrusted input - | -4 | a = input() -5 | Popen(a, shell=False) -6 | call(a, shell=False) - | ^^^^ S603 -7 | check_call(a, shell=False) -8 | check_output(a, shell=False) - | - -S603.py:7:1: S603 `subprocess` call: check for execution of untrusted input - | -5 | Popen(a, shell=False) -6 | call(a, shell=False) -7 | check_call(a, shell=False) - | ^^^^^^^^^^ S603 -8 | check_output(a, shell=False) -9 | run(a, shell=False) - | - -S603.py:8:1: S603 `subprocess` call: check for execution of untrusted input - | -6 | call(a, shell=False) -7 | check_call(a, shell=False) -8 | check_output(a, shell=False) - | ^^^^^^^^^^^^ S603 -9 | run(a, shell=False) - | - -S603.py:9:1: S603 `subprocess` call: check for execution of untrusted input - | - 7 | check_call(a, shell=False) - 8 | check_output(a, shell=False) - 9 | run(a, shell=False) - | ^^^ S603 -10 | -11 | # Falsey values are treated as false. - | - -S603.py:12:1: S603 `subprocess` call: check for execution of untrusted input - | -11 | # Falsey values are treated as false. -12 | Popen(a, shell=0) - | ^^^^^ S603 -13 | Popen(a, shell=[]) -14 | Popen(a, shell={}) - | - -S603.py:13:1: S603 `subprocess` call: check for execution of untrusted input - | -11 | # Falsey values are treated as false. -12 | Popen(a, shell=0) -13 | Popen(a, shell=[]) - | ^^^^^ S603 -14 | Popen(a, shell={}) -15 | Popen(a, shell=None) - | - -S603.py:14:1: S603 `subprocess` call: check for execution of untrusted input - | -12 | Popen(a, shell=0) -13 | Popen(a, shell=[]) -14 | Popen(a, shell={}) - | ^^^^^ S603 -15 | Popen(a, shell=None) - | - -S603.py:15:1: S603 `subprocess` call: check for execution of untrusted input - | -13 | Popen(a, shell=[]) -14 | Popen(a, shell={}) -15 | Popen(a, shell=None) - | ^^^^^ S603 -16 | -17 | # Unknown values are treated as falsey. - | - -S603.py:18:1: S603 `subprocess` call: check for execution of untrusted input - | -17 | # Unknown values are treated as falsey. -18 | Popen(a, shell=True if True else False) - | ^^^^^ S603 -19 | -20 | # No value is also caught. - | - -S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input - | -20 | # No value is also caught. -21 | Popen(a) - | ^^^^^ S603 -22 | -23 | # Literals are fine, they're trusted. - | - -S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input - | -32 | # Not through assignments though. -33 | cmd = ["true"] -34 | run(cmd) - | ^^^ S603 -35 | -36 | # Instant named expressions are fine. - | - -S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input - | -39 | # But non-instant are not. -40 | (e := "echo") -41 | run(e) - | ^^^ S603 - | diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs b/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs index 0f21a30396152..658908efb6fbe 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs @@ -10,7 +10,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::BlindExcept, Path::new("BLE.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -19,7 +19,7 @@ mod tests { Path::new("flake8_blind_except").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs index dfe793960e54a..53a69d33e6f35 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::analyze::logging; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::logging; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -101,18 +101,18 @@ pub(crate) fn blind_except( } // If the exception is logged, don't flag an error. - let mut visitor = LogExceptionVisitor::new(semantic, &checker.settings.logger_objects); + let mut visitor = LogExceptionVisitor::new(semantic, &checker.settings().logger_objects); visitor.visit_body(body); if visitor.seen() { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BlindExcept { name: builtin_exception_type.to_string(), }, type_.range(), - )); + ); } /// A visitor to detect whether the exception with the given name was re-raised. diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs index f11b2b12fa89a..b91bdad4a2296 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs @@ -185,7 +185,7 @@ pub(super) fn allow_boolean_trap(call: &ast::ExprCall, checker: &Checker) -> boo } // If the call is explicitly allowed by the user, then the boolean trap is allowed. - if is_user_allowed_func_call(call, checker.semantic(), checker.settings) { + if is_user_allowed_func_call(call, checker.semantic(), checker.settings()) { return true; } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs index 80c81bb3f4654..cd7832181c688 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs @@ -11,10 +11,9 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))] #[test_case(Rule::BooleanDefaultValuePositionalArgument, Path::new("FBT.py"))] @@ -25,25 +24,7 @@ mod tests { Path::new("flake8_boolean_trap").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } - - #[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))] - fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "preview__{}_{}", - rule_code.noqa_code(), - path.to_string_lossy() - ); - let diagnostics = test_path( - Path::new("flake8_boolean_trap").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -61,7 +42,7 @@ mod tests { ..LinterSettings::for_rule(Rule::BooleanPositionalValueInCall) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs index fe9292936a825..84f47e1e961fe 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{Decorator, Expr, Parameters}; use ruff_python_semantic::analyze::visibility; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; @@ -131,10 +131,7 @@ pub(crate) fn boolean_default_value_positional_argument( return; } - checker.report_diagnostic(Diagnostic::new( - BooleanDefaultValuePositionalArgument, - param.identifier(), - )); + checker.report_diagnostic(BooleanDefaultValuePositionalArgument, param.identifier()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs index e758e0d49a647..b3a97c1d9ab22 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_positional_value_in_call.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_boolean_trap::helpers::allow_boolean_trap; @@ -51,6 +51,7 @@ impl Violation for BooleanPositionalValueInCall { } } +/// FBT003 pub(crate) fn boolean_positional_value_in_call(checker: &Checker, call: &ast::ExprCall) { if allow_boolean_trap(call, checker) { return; @@ -61,6 +62,6 @@ pub(crate) fn boolean_positional_value_in_call(checker: &Checker, call: &ast::Ex .iter() .filter(|arg| arg.is_boolean_literal_expr()) { - checker.report_diagnostic(Diagnostic::new(BooleanPositionalValueInCall, arg.range())); + checker.report_diagnostic(BooleanPositionalValueInCall, arg.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs index d7de29798345f..24dff35099b1f 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs @@ -1,18 +1,18 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, Decorator, Expr, Parameters}; -use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::visibility; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## What it does /// Checks for the use of boolean positional arguments in function definitions, -/// as determined by the presence of a `bool` type hint. +/// as determined by the presence of a type hint containing `bool` as an +/// evident subtype - e.g. `bool`, `bool | int`, `typing.Optional[bool]`, etc. /// /// ## Why is this bad? /// Calling a function with boolean positional arguments is confusing as the @@ -30,9 +30,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// Dunder methods that define operators are exempt from this rule, as are /// setters and `@override` definitions. /// -/// In [preview], this rule will also flag annotations that include boolean -/// variants, like `bool | int`. -/// /// ## Example /// /// ```python @@ -96,8 +93,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## References /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) -/// -/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] pub(crate) struct BooleanTypeHintPositionalArgument; @@ -128,14 +123,8 @@ pub(crate) fn boolean_type_hint_positional_argument( let Some(annotation) = parameter.annotation() else { continue; }; - if checker.settings.preview.is_enabled() { - if !match_annotation_to_complex_bool(annotation, checker.semantic()) { - continue; - } - } else { - if !match_annotation_to_literal_bool(annotation) { - continue; - } + if !match_annotation_to_complex_bool(annotation, checker.semantic()) { + continue; } // Allow Boolean type hints in setters. @@ -157,21 +146,7 @@ pub(crate) fn boolean_type_hint_positional_argument( return; } - checker.report_diagnostic(Diagnostic::new( - BooleanTypeHintPositionalArgument, - parameter.identifier(), - )); - } -} - -/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`). -fn match_annotation_to_literal_bool(annotation: &Expr) -> bool { - match annotation { - // Ex) `True` - Expr::Name(name) => &name.id == "bool", - // Ex) `"True"` - Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "bool", - _ => false, + checker.report_diagnostic(BooleanTypeHintPositionalArgument, parameter.identifier()); } } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap index ea925637b517e..ee91fa49f8658 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs -snapshot_kind: text --- FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition | @@ -89,3 +88,17 @@ FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition | ^^^^^ FBT001 91 | pass | + +FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition + | +100 | def func(x: Union[list, Optional[int | str | float | bool]]): + | ^ FBT001 +101 | pass + | + +FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition + | +104 | def func(x: bool | str): + | ^ FBT001 +105 | pass + | diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__preview__FBT001_FBT.py.snap b/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__preview__FBT001_FBT.py.snap deleted file mode 100644 index d3ab33ec5c3df..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/snapshots/ruff_linter__rules__flake8_boolean_trap__tests__preview__FBT001_FBT.py.snap +++ /dev/null @@ -1,105 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs -snapshot_kind: text ---- -FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition - | -2 | posonly_nohint, -3 | posonly_nonboolhint: int, -4 | posonly_boolhint: bool, - | ^^^^^^^^^^^^^^^^ FBT001 -5 | posonly_boolstrhint: "bool", -6 | /, - | - -FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition - | -3 | posonly_nonboolhint: int, -4 | posonly_boolhint: bool, -5 | posonly_boolstrhint: "bool", - | ^^^^^^^^^^^^^^^^^^^ FBT001 -6 | /, -7 | offset, - | - -FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition - | - 8 | posorkw_nonvalued_nohint, - 9 | posorkw_nonvalued_nonboolhint: int, -10 | posorkw_nonvalued_boolhint: bool, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001 -11 | posorkw_nonvalued_boolstrhint: "bool", -12 | posorkw_boolvalued_nohint=True, - | - -FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition - | - 9 | posorkw_nonvalued_nonboolhint: int, -10 | posorkw_nonvalued_boolhint: bool, -11 | posorkw_nonvalued_boolstrhint: "bool", - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001 -12 | posorkw_boolvalued_nohint=True, -13 | posorkw_boolvalued_nonboolhint: int = True, - | - -FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition - | -12 | posorkw_boolvalued_nohint=True, -13 | posorkw_boolvalued_nonboolhint: int = True, -14 | posorkw_boolvalued_boolhint: bool = True, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001 -15 | posorkw_boolvalued_boolstrhint: "bool" = True, -16 | posorkw_nonboolvalued_nohint=1, - | - -FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition - | -13 | posorkw_boolvalued_nonboolhint: int = True, -14 | posorkw_boolvalued_boolhint: bool = True, -15 | posorkw_boolvalued_boolstrhint: "bool" = True, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001 -16 | posorkw_nonboolvalued_nohint=1, -17 | posorkw_nonboolvalued_nonboolhint: int = 2, - | - -FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition - | -16 | posorkw_nonboolvalued_nohint=1, -17 | posorkw_nonboolvalued_nonboolhint: int = 2, -18 | posorkw_nonboolvalued_boolhint: bool = 3, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001 -19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4, -20 | *, - | - -FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition - | -17 | posorkw_nonboolvalued_nonboolhint: int = 2, -18 | posorkw_nonboolvalued_boolhint: bool = 3, -19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001 -20 | *, -21 | kwonly_nonvalued_nohint, - | - -FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition - | -89 | # FBT001: Boolean positional arg in function definition -90 | def foo(self, value: bool) -> None: - | ^^^^^ FBT001 -91 | pass - | - -FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition - | -100 | def func(x: Union[list, Optional[int | str | float | bool]]): - | ^ FBT001 -101 | pass - | - -FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition - | -104 | def func(x: bool | str): - | ^ FBT001 -105 | pass - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index b7c734a589985..53f3ce51e31b4 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -10,17 +10,20 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::test::test_path; + use crate::settings::types::PreviewMode; + use ruff_python_ast::PythonVersion; #[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))] #[test_case(Rule::AssertFalse, Path::new("B011.py"))] - #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] + #[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))] + #[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))] #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] #[test_case(Rule::ClassAsDataStructure, Path::new("class_as_data_structure.py"))] @@ -76,7 +79,7 @@ mod tests { Path::new("flake8_bugbear").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -100,11 +103,11 @@ mod tests { let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), &LinterSettings { - unresolved_target_version: target_version, + unresolved_target_version: target_version.into(), ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -115,7 +118,7 @@ mod tests { Path::new("flake8_bugbear").join(snapshot).as_path(), &LinterSettings::for_rule(Rule::ZipWithoutExplicitStrict), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -134,7 +137,7 @@ mod tests { ..LinterSettings::for_rule(Rule::MutableArgumentDefault) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -155,7 +158,7 @@ mod tests { ..LinterSettings::for_rule(Rule::FunctionCallInDefaultArgument) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -171,7 +174,26 @@ mod tests { ..LinterSettings::for_rule(Rule::MutableContextvarDefault) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))] + #[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))] + fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("flake8_bugbear").join(path).as_path(), + &LinterSettings { + preview: PreviewMode::Enabled, + ..LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs index 6c82b0a4f162e..2d14a917e723d 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; -use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -131,7 +131,11 @@ fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel) fn is_empty_body(body: &[Stmt]) -> bool { body.iter().all(|stmt| match stmt { Stmt::Pass(_) => true, - Stmt::Expr(ast::StmtExpr { value, range: _ }) => { + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => { matches!( value.as_ref(), Expr::StringLiteral(_) | Expr::EllipsisLiteral(_) @@ -188,7 +192,7 @@ pub(crate) fn abstract_base_class( let has_abstract_decorator = is_abstract(decorator_list, checker.semantic()); has_abstract_method |= has_abstract_decorator; - if !checker.enabled(Rule::EmptyMethodWithoutAbstractDecorator) { + if !checker.is_rule_enabled(Rule::EmptyMethodWithoutAbstractDecorator) { continue; } @@ -196,22 +200,22 @@ pub(crate) fn abstract_base_class( && is_empty_body(body) && !is_overload(decorator_list, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( EmptyMethodWithoutAbstractDecorator { name: format!("{name}.{method_name}"), }, stmt.range(), - )); + ); } } - if checker.enabled(Rule::AbstractBaseClassWithoutAbstractMethod) { + if checker.is_rule_enabled(Rule::AbstractBaseClassWithoutAbstractMethod) { if !has_abstract_method { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( AbstractBaseClassWithoutAbstractMethod { name: name.to_string(), }, stmt.identifier(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs index ec1e6db5ff967..8708e4c487973 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Stmt}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_false; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `assert False`. @@ -52,11 +52,13 @@ impl AlwaysFixableViolation for AssertFalse { fn assertion_error(msg: Option<&Expr>) -> Stmt { Stmt::Raise(ast::StmtRaise { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), exc: Some(Box::new(Expr::Call(ast::ExprCall { func: Box::new(Expr::Name(ast::ExprName { id: "AssertionError".into(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), arguments: Arguments { args: if let Some(msg) = msg { @@ -66,8 +68,10 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt { }, keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }))), cause: None, }) @@ -79,10 +83,9 @@ pub(crate) fn assert_false(checker: &Checker, stmt: &Stmt, test: &Expr, msg: Opt return; } - let mut diagnostic = Diagnostic::new(AssertFalse, test.range()); + let mut diagnostic = checker.report_diagnostic(AssertFalse, test.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().stmt(&assertion_error(msg)), stmt.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 37c67831ca4c2..73bc6f131ecbe 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -1,10 +1,10 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, Expr, WithItem}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Arguments, Expr, WithItem}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -56,6 +56,48 @@ impl fmt::Display for ExceptionKind { } } +fn detect_blind_exception( + semantic: &ruff_python_semantic::SemanticModel<'_>, + func: &Expr, + arguments: &Arguments, +) -> Option { + let is_assert_raises = matches!( + func, + &Expr::Attribute(ast::ExprAttribute { ref attr, .. }) if attr.as_str() == "assertRaises" + ); + + let is_pytest_raises = semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"])); + + if !(is_assert_raises || is_pytest_raises) { + return None; + } + + if is_pytest_raises { + if arguments.find_keyword("match").is_some() { + return None; + } + + if arguments + .find_positional(1) + .is_some_and(|arg| matches!(arg, Expr::StringLiteral(_) | Expr::BytesLiteral(_))) + { + return None; + } + } + + let first_arg = arguments.args.first()?; + + let builtin_symbol = semantic.resolve_builtin_symbol(first_arg)?; + + match builtin_symbol { + "Exception" => Some(ExceptionKind::Exception), + "BaseException" => Some(ExceptionKind::BaseException), + _ => None, + } +} + /// B017 pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { for item in items { @@ -63,6 +105,7 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { func, arguments, range: _, + node_index: _, }) = &item.context_expr else { continue; @@ -72,36 +115,31 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { continue; } - let [arg] = &*arguments.args else { - continue; - }; - - let semantic = checker.semantic(); - - let Some(builtin_symbol) = semantic.resolve_builtin_symbol(arg) else { - continue; - }; - - let exception = match builtin_symbol { - "Exception" => ExceptionKind::Exception, - "BaseException" => ExceptionKind::BaseException, - _ => continue, - }; - - if !(matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") - || semantic - .resolve_qualified_name(func) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["pytest", "raises"]) - }) - && arguments.find_keyword("match").is_none()) + if let Some(exception) = + detect_blind_exception(checker.semantic(), func.as_ref(), arguments) { - continue; + checker.report_diagnostic(AssertRaisesException { exception }, item.range()); } + } +} + +/// B017 (call form) +pub(crate) fn assert_raises_exception_call( + checker: &Checker, + ast::ExprCall { + func, + arguments, + range, + node_index: _, + }: &ast::ExprCall, +) { + let semantic = checker.semantic(); + + if arguments.args.len() < 2 && arguments.find_argument("func", 1).is_none() { + return; + } - checker.report_diagnostic(Diagnostic::new( - AssertRaisesException { exception }, - item.range(), - )); + if let Some(exception) = detect_blind_exception(semantic, func.as_ref(), arguments) { + checker.report_diagnostic(AssertRaisesException { exception }, *range); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs index 57c374be1ebec..b96a5d91fad53 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,5 +66,5 @@ pub(crate) fn assignment_to_os_environ(checker: &Checker, targets: &[Expr]) { if id != "os" { return; } - checker.report_diagnostic(Diagnostic::new(AssignmentToOsEnviron, target.range())); + checker.report_diagnostic(AssignmentToOsEnviron, target.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs index 0fcdb5c8d0321..e7c467ca84768 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs @@ -1,10 +1,11 @@ -use crate::checkers::ast::Checker; -use crate::rules::flake8_bugbear::rules::is_infinite_iterable; -use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_python_ast::PythonVersion; +use crate::checkers::ast::Checker; +use crate::rules::flake8_bugbear::rules::is_infinite_iterable; +use crate::{FixAvailability, Violation}; + /// ## What it does /// Checks for `itertools.batched` calls without an explicit `strict` parameter. /// @@ -19,16 +20,22 @@ use ruff_python_ast::PythonVersion; /// /// ## Example /// ```python +/// import itertools +/// /// itertools.batched(iterable, n) /// ``` /// /// Use instead if the batches must be of uniform length: /// ```python +/// import itertools +/// /// itertools.batched(iterable, n, strict=True) /// ``` /// /// Or if the batches can be of non-uniform length: /// ```python +/// import itertools +/// /// itertools.batched(iterable, n, strict=False) /// ``` /// @@ -86,6 +93,5 @@ pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall return; } - let diagnostic = Diagnostic::new(BatchedWithoutExplicitStrict, call.range); - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(BatchedWithoutExplicitStrict, call.range); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs index 8fbfc9a1f3e03..9849cdf81667c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_callable; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::analyze::{class, function_type}; use ruff_python_semantic::{ScopeKind, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -88,8 +88,8 @@ pub(crate) fn cached_instance_method(checker: &Checker, function_def: &ast::Stmt &function_def.decorator_list, scope, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); if !matches!(type_, function_type::FunctionType::Method) { return; @@ -102,7 +102,7 @@ pub(crate) fn cached_instance_method(checker: &Checker, function_def: &ast::Stmt return; } - checker.report_diagnostic(Diagnostic::new(CachedInstanceMethod, decorator.range())); + checker.report_diagnostic(CachedInstanceMethod, decorator.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs index 01bbc446e7283..1ed8395147f70 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::analyze::visibility::{self, Visibility::Public}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use ruff_python_ast::PythonVersion; @@ -105,7 +105,7 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl } if has_dunder_init && public_methods == 1 { - checker.report_diagnostic(Diagnostic::new(ClassAsDataStructure, class_def.range())); + checker.report_diagnostic(ClassAsDataStructure, class_def.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 04e19bddfc68e..f753152e4db45 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -1,9 +1,7 @@ use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, ExceptHandler, Expr, ExprContext}; use ruff_text_size::{Ranged, TextRange}; @@ -11,6 +9,8 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::pad; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Violation}; +use crate::{Edit, Fix}; /// ## What it does /// Checks for `try-except` blocks with duplicate exception handlers. @@ -113,6 +113,7 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr { elts: elts.into_iter().cloned().collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, } .into() @@ -138,10 +139,10 @@ fn duplicate_handler_exceptions<'a>( } } - if checker.enabled(Rule::DuplicateHandlerException) { + if checker.is_rule_enabled(Rule::DuplicateHandlerException) { // TODO(charlie): Handle "BaseException" and redundant exception aliases. if !duplicates.is_empty() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DuplicateHandlerException { names: duplicates .into_iter() @@ -167,7 +168,6 @@ fn duplicate_handler_exceptions<'a>( }, expr.range(), ))); - checker.report_diagnostic(diagnostic); } } @@ -209,7 +209,7 @@ pub(crate) fn duplicate_exceptions(checker: &Checker, handlers: &[ExceptHandler] } } - if checker.enabled(Rule::DuplicateTryBlockException) { + if checker.is_rule_enabled(Rule::DuplicateTryBlockException) { for (name, exprs) in duplicates { for expr in exprs { let is_star = checker @@ -217,13 +217,13 @@ pub(crate) fn duplicate_exceptions(checker: &Checker, handlers: &[ExceptHandler] .current_statement() .as_try_stmt() .is_some_and(|try_stmt| try_stmt.is_star); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( DuplicateTryBlockException { name: name.segments().join("."), is_star, }, expr.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs index 90bed89adb98d..643b2c0c5e015 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_value.rs @@ -1,15 +1,15 @@ use anyhow::{Context, Result}; use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_ast::comparable::HashableExpr; use ruff_python_ast::Expr; +use ruff_python_ast::comparable::HashableExpr; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for set literals that contain duplicate items. @@ -60,7 +60,7 @@ pub(crate) fn duplicate_value(checker: &Checker, set: &ast::ExprSet) { for (index, value) in set.iter().enumerate() { if value.is_literal_expr() { if let Some(existing) = seen_values.insert(HashableExpr::from(value), value) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DuplicateValue { value: checker.generator().expr(value), existing: checker.generator().expr(existing), @@ -71,8 +71,6 @@ pub(crate) fn duplicate_value(checker: &Checker, set: &ast::ExprSet) { diagnostic.try_set_fix(|| { remove_member(set, index, checker.locator().contents()).map(Fix::safe_edit) }); - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index 25f5714ce92a9..d3f5aba2bbdf1 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast}; use ruff_python_ast::{ExceptHandler, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,9 +66,6 @@ pub(crate) fn except_with_empty_tuple(checker: &Checker, except_handler: &Except .current_statement() .as_try_stmt() .is_some_and(|try_stmt| try_stmt.is_star); - checker.report_diagnostic(Diagnostic::new( - ExceptWithEmptyTuple { is_star }, - except_handler.range(), - )); + checker.report_diagnostic(ExceptWithEmptyTuple { is_star }, except_handler.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index 6bb7b13c55d47..66df64700d35d 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -2,10 +2,10 @@ use std::collections::VecDeque; use ruff_python_ast::{self as ast, ExceptHandler, Expr, Operator}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -69,10 +69,7 @@ pub(crate) fn except_with_non_exception_classes(checker: &Checker, except_handle .current_statement() .as_try_stmt() .is_some_and(|try_stmt| try_stmt.is_star); - checker.report_diagnostic(Diagnostic::new( - ExceptWithNonExceptionClasses { is_star }, - expr.range(), - )); + checker.report_diagnostic(ExceptWithNonExceptionClasses { is_star }, expr.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 0e5ef87dda3c0..8b4d9527f9226 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -45,11 +45,16 @@ pub(crate) fn f_string_docstring(checker: &Checker, body: &[Stmt]) { let Some(stmt) = body.first() else { return; }; - let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else { + let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = stmt + else { return; }; if !value.is_f_string_expr() { return; } - checker.report_diagnostic(Diagnostic::new(FStringDocstring, stmt.identifier())); + checker.report_diagnostic(FStringDocstring, stmt.identifier()); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs index 74816eba634a8..54faca1174ffc 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs @@ -1,17 +1,14 @@ -use ruff_python_ast::{self as ast, Expr, Parameters}; -use ruff_text_size::{Ranged, TextRange}; - -use ruff_diagnostics::Violation; -use ruff_diagnostics::{Diagnostic, DiagnosticKind}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::{self as ast, Expr, Parameters}; use ruff_python_semantic::analyze::typing::{ is_immutable_annotation, is_immutable_func, is_immutable_newtype_call, is_mutable_func, }; -use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -73,7 +70,9 @@ impl Violation for FunctionCallInDefaultArgument { #[derive_message_formats] fn message(&self) -> String { if let Some(name) = &self.name { - format!("Do not perform function call `{name}` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable") + format!( + "Do not perform function call `{name}` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable" + ) } else { "Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable".to_string() } @@ -81,20 +80,15 @@ impl Violation for FunctionCallInDefaultArgument { } struct ArgumentDefaultVisitor<'a, 'b> { - semantic: &'a SemanticModel<'b>, + checker: &'a Checker<'b>, extend_immutable_calls: &'a [QualifiedName<'b>], - diagnostics: Vec<(DiagnosticKind, TextRange)>, } impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> { - fn new( - semantic: &'a SemanticModel<'b>, - extend_immutable_calls: &'a [QualifiedName<'b>], - ) -> Self { + fn new(checker: &'a Checker<'b>, extend_immutable_calls: &'a [QualifiedName<'b>]) -> Self { Self { - semantic, + checker, extend_immutable_calls, - diagnostics: Vec::new(), } } } @@ -103,19 +97,26 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> { fn visit_expr(&mut self, expr: &Expr) { match expr { Expr::Call(ast::ExprCall { func, .. }) => { - if !is_mutable_func(func, self.semantic) - && !is_immutable_func(func, self.semantic, self.extend_immutable_calls) + if !is_mutable_func(func, self.checker.semantic()) + && !is_immutable_func( + func, + self.checker.semantic(), + self.extend_immutable_calls, + ) && !func.as_name_expr().is_some_and(|name| { - is_immutable_newtype_call(name, self.semantic, self.extend_immutable_calls) + is_immutable_newtype_call( + name, + self.checker.semantic(), + self.extend_immutable_calls, + ) }) { - self.diagnostics.push(( + self.checker.report_diagnostic( FunctionCallInDefaultArgument { name: UnqualifiedName::from_expr(func).map(|name| name.to_string()), - } - .into(), + }, expr.range(), - )); + ); } visitor::walk_expr(self, expr); } @@ -131,14 +132,14 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> { pub(crate) fn function_call_in_argument_default(checker: &Checker, parameters: &Parameters) { // Map immutable calls to (module, member) format. let extend_immutable_calls: Vec = checker - .settings + .settings() .flake8_bugbear .extend_immutable_calls .iter() .map(|target| QualifiedName::from_dotted_name(target)) .collect(); - let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls); + let mut visitor = ArgumentDefaultVisitor::new(checker, &extend_immutable_calls); for parameter in parameters.iter_non_variadic_params() { if let Some(default) = parameter.default() { if !parameter.annotation().is_some_and(|expr| { @@ -148,8 +149,4 @@ pub(crate) fn function_call_in_argument_default(checker: &Checker, parameters: & } } } - - for (check, range) in visitor.diagnostics { - checker.report_diagnostic(Diagnostic::new(check, range)); - } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index e17d6030853de..a4cbba7cc7ffb 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::types::Node; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Comprehension, Expr, ExprContext, Stmt}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -111,6 +111,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { Stmt::Return(ast::StmtReturn { value: Some(value), range: _, + node_index: _, }) => { // Mark `return lambda: x` as safe. if value.is_lambda_expr() { @@ -128,6 +129,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { func, arguments, range: _, + node_index: _, }) => { match func.as_ref() { Expr::Name(ast::ExprName { id, .. }) => { @@ -167,6 +169,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { parameters, body, range: _, + node_index: _, }) => { if !self.safe_functions.contains(&expr) { // Collect all loaded variable names. @@ -305,12 +308,12 @@ pub(crate) fn function_uses_loop_variable(checker: &Checker, node: &Node) { for name in suspicious_variables { if reassigned_in_loop.contains(&name.id.as_str()) { if checker.insert_flake8_bugbear_range(name.range()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( FunctionUsesLoopVariable { name: name.id.to_string(), }, name.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index 87af7d6400f89..ef108b77f5a18 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use ruff_source_file::LineRanges; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::pad; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `getattr` that take a constant attribute value as an @@ -68,7 +68,7 @@ pub(crate) fn getattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, return; } - let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range()); + let mut diagnostic = checker.report_diagnostic(GetAttrWithConstant, expr.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( pad( if matches!( @@ -88,5 +88,4 @@ pub(crate) fn getattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, ), expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs index e2293a9c3c6c3..87fc151c90431 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -56,7 +56,7 @@ impl Violation for JumpStatementInFinally { fn walk_stmt(checker: &Checker, body: &[Stmt], f: fn(&Stmt) -> bool) { for stmt in body { if f(stmt) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( JumpStatementInFinally { name: match stmt { Stmt::Break(_) => "break", @@ -67,7 +67,7 @@ fn walk_stmt(checker: &Checker, body: &[Stmt], f: fn(&Stmt) -> bool) { .to_owned(), }, stmt.range(), - )); + ); } match stmt { Stmt::While(ast::StmtWhile { body, .. }) | Stmt::For(ast::StmtFor { body, .. }) => { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs index 646e05ec93468..6a617a147de62 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs @@ -1,18 +1,17 @@ use std::collections::HashMap; use std::fmt::Debug; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{ - visitor::{self, Visitor}, Expr, ExprAttribute, ExprCall, ExprSubscript, ExprTuple, Stmt, StmtAssign, StmtAugAssign, StmtDelete, StmtFor, StmtIf, + visitor::{self, Visitor}, }; use ruff_text_size::TextRange; +use crate::Violation; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; @@ -61,6 +60,7 @@ pub(crate) fn loop_iterator_mutation(checker: &Checker, stmt_for: &StmtFor) { orelse: _, is_async: _, range: _, + node_index: _, } = stmt_for; let (index, target, iter) = match iter.as_ref() { @@ -110,7 +110,7 @@ pub(crate) fn loop_iterator_mutation(checker: &Checker, stmt_for: &StmtFor) { let name = UnqualifiedName::from_expr(iter) .map(|name| name.to_string()) .map(SourceCodeSnippet::new); - checker.report_diagnostic(Diagnostic::new(LoopIteratorMutation { name }, *mutation)); + checker.report_diagnostic(LoopIteratorMutation { name }, *mutation); } } @@ -171,6 +171,7 @@ impl<'a> LoopMutationsVisitor<'a> { for target in targets { if let Expr::Subscript(ExprSubscript { range: _, + node_index: _, value, slice: _, ctx: _, @@ -189,6 +190,7 @@ impl<'a> LoopMutationsVisitor<'a> { for target in targets { if let Expr::Subscript(ExprSubscript { range: _, + node_index: _, value, slice, ctx: _, @@ -218,6 +220,7 @@ impl<'a> LoopMutationsVisitor<'a> { fn handle_call(&mut self, func: &Expr) { if let Expr::Attribute(ExprAttribute { range, + node_index: _, value, attr, ctx: _, @@ -238,7 +241,11 @@ impl<'a> Visitor<'a> for LoopMutationsVisitor<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { match stmt { // Ex) `del items[0]` - Stmt::Delete(StmtDelete { range, targets }) => { + Stmt::Delete(StmtDelete { + range, + targets, + node_index: _, + }) => { self.handle_delete(*range, targets); visitor::walk_stmt(self, stmt); } @@ -286,7 +293,6 @@ impl<'a> Visitor<'a> for LoopMutationsVisitor<'a> { if let Some(mutations) = self.mutations.get_mut(&self.branch) { mutations.clear(); } - visitor::walk_stmt(self, stmt); } // Avoid recursion for class and function definitions. diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs index b0247efd0bb21..f0a12e755a4f5 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, Expr}; use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -64,12 +64,12 @@ pub(crate) fn loop_variable_overrides_iterator(checker: &Checker, target: &Expr, for (name, expr) in target_names { if iter_names.contains_key(name) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( LoopVariableOverridesIterator { name: name.to_string(), }, expr.range(), - )); + ); } } } @@ -97,6 +97,7 @@ impl<'a> Visitor<'a> for NameFinder<'a> { parameters, body, range: _, + node_index: _, }) => { visitor::walk_expr(self, body); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index a828364f9c104..960ddbfdfbc92 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -1,21 +1,21 @@ use std::fmt::Write; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, Parameter}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_index::Indexer; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::function_type::is_stub; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; -use ruff_python_semantic::SemanticModel; use ruff_python_trivia::{indentation_at_offset, textwrap}; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of mutable objects as function argument defaults. @@ -68,6 +68,12 @@ use crate::Locator; /// ## Options /// - `lint.flake8-bugbear.extend-immutable-calls` /// +/// ## Fix safety +/// +/// This fix is marked as unsafe because it replaces the mutable default with `None` +/// and initializes it in the function body, which may not be what the user intended, +/// as described above. +/// /// ## References /// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values) #[derive(ViolationMetadata)] @@ -99,7 +105,7 @@ pub(crate) fn mutable_argument_default(checker: &Checker, function_def: &ast::St }; let extend_immutable_calls: Vec = checker - .settings + .settings() .flake8_bugbear .extend_immutable_calls .iter() @@ -111,7 +117,7 @@ pub(crate) fn mutable_argument_default(checker: &Checker, function_def: &ast::St is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice()) }) { - let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range()); + let mut diagnostic = checker.report_diagnostic(MutableArgumentDefault, default.range()); // If the function body is on the same line as the function def, do not fix if let Some(fix) = move_initialization( @@ -126,14 +132,13 @@ pub(crate) fn mutable_argument_default(checker: &Checker, function_def: &ast::St ) { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } } } /// Generate a [`Fix`] to move a mutable argument default initialization /// into the function body. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn move_initialization( function_def: &ast::StmtFunctionDef, parameter: &Parameter, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs index db829a28f40e9..a6d3879d42ec5 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_contextvar_default.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_expr, is_mutable_func}; use ruff_python_semantic::Modules; +use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_expr, is_mutable_func}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -82,7 +82,7 @@ pub(crate) fn mutable_contextvar_default(checker: &Checker, call: &ast::ExprCall }; let extend_immutable_calls: Vec = checker - .settings + .settings() .flake8_bugbear .extend_immutable_calls .iter() @@ -102,6 +102,6 @@ pub(crate) fn mutable_contextvar_default(checker: &Checker, call: &ast::ExprCall matches!(qualified_name.segments(), ["contextvars", "ContextVar"]) }) { - checker.report_diagnostic(Diagnostic::new(MutableContextvarDefault, default.range())); + checker.report_diagnostic(MutableContextvarDefault, default.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index 73c41f4709767..ed2b3e98cdd56 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::{AlwaysFixableViolation, Fix}; use crate::{checkers::ast::Checker, fix::edits::add_argument}; /// ## What it does @@ -20,11 +20,15 @@ use crate::{checkers::ast::Checker, fix::edits::add_argument}; /// /// ## Example /// ```python +/// import warnings +/// /// warnings.warn("This is a warning") /// ``` /// /// Use instead: /// ```python +/// import warnings +/// /// warnings.warn("This is a warning", stacklevel=2) /// ``` /// @@ -60,10 +64,18 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) { return; } + // When prefixes are supplied, stacklevel is implicitly overridden to be `max(2, stacklevel)`. + // + // Signature as of Python 3.13 (https://docs.python.org/3/library/warnings.html#warnings.warn) + // ```text + // 0 1 2 3 4 + // warnings.warn(message, category=None, stacklevel=1, source=None, *, skip_file_prefixes=()) + // ``` if call .arguments .find_argument_value("stacklevel", 2) .is_some() + || is_skip_file_prefixes_param_set(&call.arguments) || call .arguments .args @@ -77,7 +89,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) { { return; } - let mut diagnostic = Diagnostic::new(NoExplicitStacklevel, call.func.range()); + let mut diagnostic = checker.report_diagnostic(NoExplicitStacklevel, call.func.range()); let edit = add_argument( "stacklevel=2", @@ -87,6 +99,15 @@ pub(crate) fn no_explicit_stacklevel(checker: &Checker, call: &ast::ExprCall) { ); diagnostic.set_fix(Fix::unsafe_edit(edit)); +} - checker.report_diagnostic(diagnostic); +/// Returns `true` if `skip_file_prefixes` is set to its non-default value. +/// The default value of `skip_file_prefixes` is an empty tuple. +fn is_skip_file_prefixes_param_set(arguments: &ast::Arguments) -> bool { + arguments + .find_keyword("skip_file_prefixes") + .is_some_and(|keyword| match &keyword.value { + Expr::Tuple(tuple) => !tuple.elts.is_empty(), + _ => true, + }) } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs index 933aac58216e5..bfe131f98e637 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_literal.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -39,6 +39,6 @@ impl Violation for RaiseLiteral { /// B016 pub(crate) fn raise_literal(checker: &Checker, expr: &Expr) { if expr.is_literal_expr() { - checker.report_diagnostic(Diagnostic::new(RaiseLiteral, expr.range())); + checker.report_diagnostic(RaiseLiteral, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs index e33d5ff595ea7..a9b6c674f7278 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs @@ -1,11 +1,11 @@ use ruff_python_ast as ast; use ruff_python_ast::Stmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::RaiseStatementVisitor; use ruff_python_ast::statement_visitor::StatementVisitor; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -106,10 +106,7 @@ pub(crate) fn raise_without_from_inside_except( .as_try_stmt() .is_some_and(|try_stmt| try_stmt.is_star); - checker.report_diagnostic(Diagnostic::new( - RaiseWithoutFromInsideExcept { is_star }, - range, - )); + checker.report_diagnostic(RaiseWithoutFromInsideExcept { is_star }, range); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs index 5dc1ac79f2287..4f38947c7716c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs @@ -2,11 +2,11 @@ use std::fmt; use ruff_python_ast::{self as ast}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -75,10 +75,7 @@ pub(crate) fn re_sub_positional_args(checker: &Checker, call: &ast::ExprCall) { }; if call.arguments.args.len() > method.num_args() { - checker.report_diagnostic(Diagnostic::new( - ReSubPositionalArgs { method }, - call.range(), - )); + checker.report_diagnostic(ReSubPositionalArgs { method }, call.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs index b21e50f896c55..3d5e3834303a5 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, ExceptHandler, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::pad; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for single-element tuples in exception handlers (e.g., @@ -79,7 +79,7 @@ pub(crate) fn redundant_tuple_in_exception_handler(checker: &Checker, handlers: if elt.is_starred_expr() { continue; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RedundantTupleInExceptionHandler { name: checker.generator().expr(elt), }, @@ -100,6 +100,5 @@ pub(crate) fn redundant_tuple_in_exception_handler(checker: &Checker, handlers: ), type_.range(), ))); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs index d5b4e13140ab2..8b8f529df4e63 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/return_in_generator.rs @@ -1,11 +1,10 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::statement_visitor; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::{self as ast, Expr, Stmt, StmtFunctionDef}; use ruff_text_size::TextRange; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -101,7 +100,7 @@ pub(crate) fn return_in_generator(checker: &Checker, function_def: &StmtFunction if visitor.has_yield { if let Some(return_) = visitor.return_ { - checker.report_diagnostic(Diagnostic::new(ReturnInGenerator, return_)); + checker.report_diagnostic(ReturnInGenerator, return_); } } } @@ -127,6 +126,7 @@ impl StatementVisitor<'_> for ReturnInGeneratorVisitor { Stmt::Return(ast::StmtReturn { value: Some(_), range, + node_index: _, }) => { self.return_ = Some(*range); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 0e70f8aa222d0..7643b2846ea31 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Comprehension, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::{self, Visitor}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -153,6 +153,7 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> { body, elif_else_clauses, range: _, + node_index: _, }) => { // base if plus branches let mut if_stack = Vec::with_capacity(1 + elif_else_clauses.len()); @@ -179,6 +180,7 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> { subject, cases, range: _, + node_index: _, }) => { self.counter_stack.push(Vec::with_capacity(cases.len())); self.visit_expr(subject); @@ -210,7 +212,11 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> { Stmt::Continue(_) | Stmt::Break(_) => { self.reset_usage_count(); } - Stmt::Return(ast::StmtReturn { value, range: _ }) => { + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => { if let Some(expr) = value { self.visit_expr(expr); } @@ -250,11 +256,13 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> { elt, generators, range: _, + node_index: _, }) | Expr::SetComp(ast::ExprSetComp { elt, generators, range: _, + node_index: _, }) => { for comprehension in generators { self.visit_comprehension(comprehension); @@ -270,6 +278,7 @@ impl<'a> Visitor<'a> for GroupNameFinder<'a> { value, generators, range: _, + node_index: _, }) => { for comprehension in generators { self.visit_comprehension(comprehension); @@ -339,6 +348,6 @@ pub(crate) fn reuse_of_groupby_generator( finder.visit_stmt(stmt); } for expr in finder.exprs { - checker.report_diagnostic(Diagnostic::new(ReuseOfGroupbyGenerator, expr.range())); + checker.report_diagnostic(ReuseOfGroupbyGenerator, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index 976722f22939f..6244634ffa1fd 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, Expr, ExprContext, Identifier, Stmt}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_codegen::Generator; use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `setattr` that take a constant attribute value as an @@ -53,9 +53,11 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> Str attr: Identifier::new(name.to_string(), TextRange::default()), ctx: ExprContext::Store, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })], value: Box::new(value.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); generator.stmt(&stmt) } @@ -74,6 +76,11 @@ pub(crate) fn setattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, if !is_identifier(name.to_str()) { return; } + // Ignore if the attribute name is `__debug__`. Assigning to the `__debug__` property is a + // `SyntaxError`. + if name.to_str() == "__debug__" { + return; + } if is_mangled_private(name.to_str()) { return; } @@ -87,15 +94,15 @@ pub(crate) fn setattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr, if let Stmt::Expr(ast::StmtExpr { value: child, range: _, + node_index: _, }) = checker.semantic().current_statement() { if expr == child.as_ref() { - let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range()); + let mut diagnostic = checker.report_diagnostic(SetAttrWithConstant, expr.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( assignment(obj, name.to_str(), value, checker.generator()), expr.range(), ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs index 5b9844ec537da..570ac00660c2d 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{Expr, Keyword}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -71,9 +71,6 @@ pub(crate) fn star_arg_unpacking_after_keyword_arg( if arg.start() <= keyword.start() { continue; } - checker.report_diagnostic(Diagnostic::new( - StarArgUnpackingAfterKeywordArg, - arg.range(), - )); + checker.report_diagnostic(StarArgUnpackingAfterKeywordArg, arg.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs index 951f64ce1c6cd..ace25f1f55fb7 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/static_key_dict_comprehension.rs @@ -1,12 +1,12 @@ use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::StoredNameFinder; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; @@ -46,7 +46,7 @@ impl Violation for StaticKeyDictComprehension { } } -/// RUF011 +/// B035, RUF011 pub(crate) fn static_key_dict_comprehension(checker: &Checker, dict_comp: &ast::ExprDictComp) { // Collect the bound names in the comprehension's generators. let names = { @@ -58,12 +58,12 @@ pub(crate) fn static_key_dict_comprehension(checker: &Checker, dict_comp: &ast:: }; if is_constant(&dict_comp.key, &names) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( StaticKeyDictComprehension { key: SourceCodeSnippet::from_str(checker.locator().slice(dict_comp.key.as_ref())), }, dict_comp.key.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs index 78a1c15c2ea96..4c6ef9ef5e2dd 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs @@ -1,10 +1,10 @@ use itertools::Itertools; use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -73,6 +73,6 @@ pub(crate) fn strip_with_multi_characters( }; if value.chars().count() > 1 && !value.chars().all_unique() { - checker.report_diagnostic(Diagnostic::new(StripWithMultiCharacters, expr.range())); + checker.report_diagnostic(StripWithMultiCharacters, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs index bd44c8d4f88b3..f9c868226b299 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unary_prefix_increment_decrement.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr, UnaryOp}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -61,20 +61,20 @@ pub(crate) fn unary_prefix_increment_decrement( }; match (op, nested_op) { (UnaryOp::UAdd, UnaryOp::UAdd) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnaryPrefixIncrementDecrement { operator: UnaryPrefixOperatorType::Increment, }, expr.range(), - )); + ); } (UnaryOp::USub, UnaryOp::USub) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnaryPrefixIncrementDecrement { operator: UnaryPrefixOperatorType::Decrement, }, expr.range(), - )); + ); } _ => {} } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs index 70c66f7a1699e..5588b4c343613 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unintentional_type_annotation.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -46,17 +46,13 @@ pub(crate) fn unintentional_type_annotation( match target { Expr::Subscript(ast::ExprSubscript { value, .. }) => { if value.is_name_expr() { - checker - .report_diagnostic(Diagnostic::new(UnintentionalTypeAnnotation, stmt.range())); + checker.report_diagnostic(UnintentionalTypeAnnotation, stmt.range()); } } Expr::Attribute(ast::ExprAttribute { value, .. }) => { if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { if id != "self" { - checker.report_diagnostic(Diagnostic::new( - UnintentionalTypeAnnotation, - stmt.range(), - )); + checker.report_diagnostic(UnintentionalTypeAnnotation, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index d138540d66c7e..fbeea00d97857 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `hasattr` to test if an object is callable (e.g., @@ -26,6 +27,21 @@ use crate::checkers::ast::Checker; /// callable(obj) /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the `hasattr` call +/// expression, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// hasattr( +/// # comment 1 +/// obj, # comment 2 +/// # comment 3 +/// "__call__", # comment 4 +/// # comment 5 +/// ) +/// ``` +/// /// ## References /// - [Python documentation: `callable`](https://docs.python.org/3/library/functions.html#callable) /// - [Python documentation: `hasattr`](https://docs.python.org/3/library/functions.html#hasattr) @@ -72,7 +88,7 @@ pub(crate) fn unreliable_callable_check( return; } - let mut diagnostic = Diagnostic::new(UnreliableCallableCheck, expr.range()); + let mut diagnostic = checker.report_diagnostic(UnreliableCallableCheck, expr.range()); if builtins_function == "hasattr" { diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( @@ -84,8 +100,15 @@ pub(crate) fn unreliable_callable_check( format!("{binding}({})", checker.locator().slice(obj)), expr.range(), ); - Ok(Fix::safe_edits(binding_edit, import_edit)) + Ok(Fix::applicable_edits( + binding_edit, + import_edit, + if checker.comment_ranges().intersects(expr.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )) }); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index 5b750bc3c829c..4b0a48ac16898 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::helpers; use ruff_python_ast::helpers::{NameFinder, StoredNameFinder}; @@ -8,6 +7,7 @@ use ruff_python_semantic::Binding; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unused variables in loops (e.g., `for` and `while` statements). @@ -93,7 +93,7 @@ pub(crate) fn unused_loop_control_variable(checker: &Checker, stmt_for: &ast::St for (name, expr) in control_names { // Ignore names that are already underscore-prefixed. - if checker.settings.dummy_variable_rgx.is_match(name) { + if checker.settings().dummy_variable_rgx.is_match(name) { continue; } @@ -116,12 +116,12 @@ pub(crate) fn unused_loop_control_variable(checker: &Checker, stmt_for: &ast::St // violation in the next pass. let rename = format!("_{name}"); let rename = checker - .settings + .settings() .dummy_variable_rgx .is_match(rename.as_str()) .then_some(rename); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnusedLoopControlVariable { name: name.to_string(), rename: rename.clone(), @@ -147,7 +147,6 @@ pub(crate) fn unused_loop_control_variable(checker: &Checker, stmt_for: &ast::St } } } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs index 7c8132b902c29..25d373357ce28 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_comparison.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, Stmt}; use ruff_python_semantic::ScopeKind; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::at_last_top_level_expression_in_cell; +use crate::rules::flake8_bugbear::helpers::at_last_top_level_expression_in_cell; /// ## What it does /// Checks for useless comparisons. @@ -78,22 +78,22 @@ pub(crate) fn useless_comparison(checker: &Checker, expr: &Expr) { .and_then(Stmt::as_expr_stmt) .is_some_and(|last_stmt| &*last_stmt.value == expr) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UselessComparison { at: ComparisonLocationAt::EndOfFunction, }, expr.range(), - )); + ); return; } } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UselessComparison { at: ComparisonLocationAt::MiddleBody, }, expr.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index dde13b5636721..4ec3e85d1fd9e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -63,6 +63,6 @@ pub(crate) fn useless_contextlib_suppress( matches!(qualified_name.segments(), ["contextlib", "suppress"]) }) { - checker.report_diagnostic(Diagnostic::new(UselessContextlibSuppress, expr.range())); + checker.report_diagnostic(UselessContextlibSuppress, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs index 4cde035f3f6c3..2c9f74dc75ed4 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_expression.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers::contains_effect; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; +use ruff_python_ast::helpers::contains_effect; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::at_last_top_level_expression_in_cell; +use crate::rules::flake8_bugbear::helpers::at_last_top_level_expression_in_cell; /// ## What it does /// Checks for useless expressions. @@ -100,22 +100,22 @@ pub(crate) fn useless_expression(checker: &Checker, value: &Expr) { // Flag attributes as useless expressions, even if they're attached to calls or other // expressions. if value.is_attribute_expr() { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UselessExpression { kind: Kind::Attribute, }, value.range(), - )); + ); } return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UselessExpression { kind: Kind::Expression, }, value.range(), - )); + ); } #[derive(Debug, PartialEq, Eq, Copy, Clone)] diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 1cfae1b806b8d..73134c65b6a9c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_semantic::SemanticModel; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::add_argument; +use crate::{AlwaysFixableViolation, Applicability, Fix}; /// ## What it does /// Checks for `zip` calls without an explicit `strict` parameter. @@ -63,8 +63,9 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal .iter() .any(|arg| is_infinite_iterable(arg, semantic)) { - checker.report_diagnostic( - Diagnostic::new(ZipWithoutExplicitStrict, call.range()).with_fix(Fix::applicable_edit( + checker + .report_diagnostic(ZipWithoutExplicitStrict, call.range()) + .set_fix(Fix::applicable_edit( add_argument( "strict=False", &call.arguments, @@ -82,8 +83,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal } else { Applicability::Safe }, - )), - ); + )); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap index 2d67212e68d25..1ef53a053bc04 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B004_B004.py.snap @@ -85,4 +85,32 @@ B004.py:24:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable i 25 |+ if builtins.callable(o): 25 26 | print("STILL a bug!") 26 27 | -27 28 | +27 28 | + +B004.py:35:1: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results. + | +33 | # https://github.com/astral-sh/ruff/issues/18741 +34 | # The autofix for this is unsafe due to the comments. +35 | / hasattr( +36 | | # comment 1 +37 | | obj, # comment 2 +38 | | # comment 3 +39 | | "__call__", # comment 4 +40 | | # comment 5 +41 | | ) + | |_^ B004 + | + = help: Replace with `callable()` + +ℹ Unsafe fix +32 32 | +33 33 | # https://github.com/astral-sh/ruff/issues/18741 +34 34 | # The autofix for this is unsafe due to the comments. +35 |-hasattr( +36 |- # comment 1 +37 |- obj, # comment 2 +38 |- # comment 3 +39 |- "__call__", # comment 4 +40 |- # comment 5 +41 |-) + 35 |+callable(obj) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap index 5748ea4eb7612..8bddcba72085f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B009_B009_B010.py.snap @@ -364,6 +364,8 @@ B009_B010.py:69:1: B009 [*] Do not call `getattr` with a constant attribute valu 68 | import builtins 69 | builtins.getattr(foo, "bar") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B009 +70 | +71 | # Regression test for: https://github.com/astral-sh/ruff/issues/18353 | = help: Replace `getattr` with attribute access @@ -373,3 +375,6 @@ B009_B010.py:69:1: B009 [*] Do not call `getattr` with a constant attribute valu 68 68 | import builtins 69 |-builtins.getattr(foo, "bar") 69 |+foo.bar +70 70 | +71 71 | # Regression test for: https://github.com/astral-sh/ruff/issues/18353 +72 72 | setattr(foo, "__debug__", 0) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017.py.snap deleted file mode 100644 index bbee0c1d9158c..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017.py.snap +++ /dev/null @@ -1,45 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs ---- -B017.py:23:14: B017 Do not assert blind exception: `Exception` - | -21 | class Foobar(unittest.TestCase): -22 | def evil_raises(self) -> None: -23 | with self.assertRaises(Exception): - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 -24 | raise Exception("Evil I say!") - | - -B017.py:27:14: B017 Do not assert blind exception: `BaseException` - | -26 | def also_evil_raises(self) -> None: -27 | with self.assertRaises(BaseException): - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 -28 | raise Exception("Evil I say!") - | - -B017.py:45:10: B017 Do not assert blind exception: `Exception` - | -44 | def test_pytest_raises(): -45 | with pytest.raises(Exception): - | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 -46 | raise ValueError("Hello") - | - -B017.py:48:10: B017 Do not assert blind exception: `Exception` - | -46 | raise ValueError("Hello") -47 | -48 | with pytest.raises(Exception), pytest.raises(ValueError): - | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 -49 | raise ValueError("Hello") - | - -B017.py:57:36: B017 Do not assert blind exception: `Exception` - | -55 | raise ValueError("This is also fine") -56 | -57 | with contextlib.nullcontext(), pytest.raises(Exception): - | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 -58 | raise ValueError("Multiple context managers") - | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017_0.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017_0.py.snap new file mode 100644 index 0000000000000..3d23080944d05 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017_0.py.snap @@ -0,0 +1,45 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +--- +B017_0.py:23:14: B017 Do not assert blind exception: `Exception` + | +21 | class Foobar(unittest.TestCase): +22 | def evil_raises(self) -> None: +23 | with self.assertRaises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +24 | raise Exception("Evil I say!") + | + +B017_0.py:27:14: B017 Do not assert blind exception: `BaseException` + | +26 | def also_evil_raises(self) -> None: +27 | with self.assertRaises(BaseException): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +28 | raise Exception("Evil I say!") + | + +B017_0.py:45:10: B017 Do not assert blind exception: `Exception` + | +44 | def test_pytest_raises(): +45 | with pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +46 | raise ValueError("Hello") + | + +B017_0.py:48:10: B017 Do not assert blind exception: `Exception` + | +46 | raise ValueError("Hello") +47 | +48 | with pytest.raises(Exception), pytest.raises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +49 | raise ValueError("Hello") + | + +B017_0.py:57:36: B017 Do not assert blind exception: `Exception` + | +55 | raise ValueError("This is also fine") +56 | +57 | with contextlib.nullcontext(), pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +58 | raise ValueError("Multiple context managers") + | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017_1.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017_1.py.snap new file mode 100644 index 0000000000000..967e60a4f9e9a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B017_B017_1.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap index cf6ec733783d7..ddaf48098f757 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap @@ -37,11 +37,10 @@ B028.py:9:1: B028 [*] No explicit `stacklevel` keyword argument found 7 7 | 8 8 | warnings.warn("test", DeprecationWarning) 9 |-warnings.warn("test", DeprecationWarning, source=None) -10 9 | warnings.warn("test", DeprecationWarning, source=None, stacklevel=2) - 10 |+warnings.warn("test", DeprecationWarning, source=None, stacklevel=2) + 9 |+warnings.warn("test", DeprecationWarning, stacklevel=2, source=None) +10 10 | warnings.warn("test", DeprecationWarning, source=None, stacklevel=2) 11 11 | warnings.warn("test", DeprecationWarning, stacklevel=1) 12 12 | warnings.warn("test", DeprecationWarning, 1) -13 13 | warnings.warn("test", category=DeprecationWarning, stacklevel=1) B028.py:22:1: B028 [*] No explicit `stacklevel` keyword argument found | @@ -59,5 +58,28 @@ B028.py:22:1: B028 [*] No explicit `stacklevel` keyword argument found 24 24 | DeprecationWarning, 25 25 | # some comments here 26 |- source = None # no trailing comma - 26 |+ source = None, stacklevel=2 # no trailing comma + 26 |+ stacklevel=2, source = None # no trailing comma 27 27 | ) +28 28 | +29 29 | # https://github.com/astral-sh/ruff/issues/18011 + +B028.py:32:1: B028 [*] No explicit `stacklevel` keyword argument found + | +30 | warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),)) +31 | # trigger diagnostic if `skip_file_prefixes` is present and set to the default value +32 | warnings.warn("test", skip_file_prefixes=()) + | ^^^^^^^^^^^^^ B028 +33 | +34 | _my_prefixes = ("this","that") + | + = help: Set `stacklevel=2` + +ℹ Unsafe fix +29 29 | # https://github.com/astral-sh/ruff/issues/18011 +30 30 | warnings.warn("test", skip_file_prefixes=(os.path.dirname(__file__),)) +31 31 | # trigger diagnostic if `skip_file_prefixes` is present and set to the default value +32 |-warnings.warn("test", skip_file_prefixes=()) + 32 |+warnings.warn("test", stacklevel=2, skip_file_prefixes=()) +33 33 | +34 34 | _my_prefixes = ("this","that") +35 35 | warnings.warn("test", skip_file_prefixes = _my_prefixes) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B031_B031.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B031_B031.py.snap index b13db50fcda64..7fb26f40a0b0f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B031_B031.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B031_B031.py.snap @@ -195,31 +195,31 @@ B031.py:144:33: B031 Using the generator returned from `itertools.groupby()` mor 146 | for group in groupby(items, key=lambda p: p[1]): | -B031.py:200:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage +B031.py:203:41: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage | -198 | if _section == "greens": -199 | collect_shop_items(shopper, section_items) -200 | collect_shop_items(shopper, section_items) - | ^^^^^^^^^^^^^ B031 -201 | return +201 | if _section == "greens": +202 | collect_shop_items(shopper, section_items) +203 | collect_shop_items(shopper, section_items) + | ^^^^^^^^^^^^^ B031 +204 | return | -B031.py:210:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage +B031.py:215:41: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage | -208 | elif _section == "frozen items": -209 | collect_shop_items(shopper, section_items) -210 | collect_shop_items(shopper, section_items) - | ^^^^^^^^^^^^^ B031 -211 | -212 | # Should trigger, since only one branch has a return statement. +213 | elif _section == "frozen items": +214 | collect_shop_items(shopper, section_items) +215 | collect_shop_items(shopper, section_items) + | ^^^^^^^^^^^^^ B031 +216 | +217 | # Should trigger, since only one branch has a return statement. | -B031.py:219:33: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage +B031.py:226:37: B031 Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage | -217 | elif _section == "frozen items": -218 | collect_shop_items(shopper, section_items) -219 | collect_shop_items(shopper, section_items) # B031 - | ^^^^^^^^^^^^^ B031 -220 | -221 | # Let's redefine the `groupby` function to make sure we pick up the correct one. +224 | elif _section == "frozen items": +225 | collect_shop_items(shopper, section_items) +226 | collect_shop_items(shopper, section_items) # B031 + | ^^^^^^^^^^^^^ B031 +227 | +228 | # Let's redefine the `groupby` function to make sure we pick up the correct one. | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B017_B017_0.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B017_B017_0.py.snap new file mode 100644 index 0000000000000..3d23080944d05 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B017_B017_0.py.snap @@ -0,0 +1,45 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +--- +B017_0.py:23:14: B017 Do not assert blind exception: `Exception` + | +21 | class Foobar(unittest.TestCase): +22 | def evil_raises(self) -> None: +23 | with self.assertRaises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +24 | raise Exception("Evil I say!") + | + +B017_0.py:27:14: B017 Do not assert blind exception: `BaseException` + | +26 | def also_evil_raises(self) -> None: +27 | with self.assertRaises(BaseException): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +28 | raise Exception("Evil I say!") + | + +B017_0.py:45:10: B017 Do not assert blind exception: `Exception` + | +44 | def test_pytest_raises(): +45 | with pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +46 | raise ValueError("Hello") + | + +B017_0.py:48:10: B017 Do not assert blind exception: `Exception` + | +46 | raise ValueError("Hello") +47 | +48 | with pytest.raises(Exception), pytest.raises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +49 | raise ValueError("Hello") + | + +B017_0.py:57:36: B017 Do not assert blind exception: `Exception` + | +55 | raise ValueError("This is also fine") +56 | +57 | with contextlib.nullcontext(), pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +58 | raise ValueError("Multiple context managers") + | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B017_B017_1.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B017_B017_1.py.snap new file mode 100644 index 0000000000000..ee9aadeedd81d --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B017_B017_1.py.snap @@ -0,0 +1,37 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +--- +B017_1.py:20:9: B017 Do not assert blind exception: `Exception` + | +18 | class Foobar(unittest.TestCase): +19 | def call_form_raises(self) -> None: +20 | self.assertRaises(Exception, something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +21 | self.assertRaises(BaseException, something_else) + | + +B017_1.py:21:9: B017 Do not assert blind exception: `BaseException` + | +19 | def call_form_raises(self) -> None: +20 | self.assertRaises(Exception, something_else) +21 | self.assertRaises(BaseException, something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 + | + +B017_1.py:25:5: B017 Do not assert blind exception: `Exception` + | +24 | def test_pytest_call_form() -> None: +25 | pytest.raises(Exception, something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +26 | pytest.raises(BaseException, something_else) + | + +B017_1.py:26:5: B017 Do not assert blind exception: `BaseException` + | +24 | def test_pytest_call_form() -> None: +25 | pytest.raises(Exception, something_else) +26 | pytest.raises(BaseException, something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +27 | +28 | pytest.raises(Exception, something_else, match="hello") + | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index 8d0583d87578a..6f863dc2e3908 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs @@ -10,7 +10,7 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::rules::flake8_builtins; use crate::settings::LinterSettings; @@ -59,7 +59,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -89,7 +89,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -112,7 +112,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -136,7 +136,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -163,7 +163,7 @@ mod tests { }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -207,7 +207,7 @@ mod tests { }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -217,11 +217,11 @@ mod tests { let diagnostics = test_path( Path::new("flake8_builtins").join(path).as_path(), &LinterSettings { - unresolved_target_version: PythonVersion::PY38, + unresolved_target_version: PythonVersion::PY38.into(), ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index 7340489e72fca..c600059c11849 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs @@ -1,13 +1,12 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, Parameter}; use ruff_python_semantic::analyze::visibility::{is_overload, is_override}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::super::helpers::shadows_builtin; +use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## What it does /// Checks for function arguments that use the same names as builtins. @@ -67,7 +66,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &Checker, parameter: &Paramete if shadows_builtin( parameter.name(), checker.source_type, - &checker.settings.flake8_builtins.ignorelist, + &checker.settings().flake8_builtins.ignorelist, checker.target_version(), ) { // Ignore parameters in lambda expressions. @@ -92,11 +91,11 @@ pub(crate) fn builtin_argument_shadowing(checker: &Checker, parameter: &Paramete return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BuiltinArgumentShadowing { name: parameter.name.to_string(), }, parameter.name.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index f337bd214f6f4..b0e5c4979c7d0 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -1,11 +1,10 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::{BindingKind, Scope, ScopeId}; use ruff_source_file::SourceRow; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_builtins::helpers::shadows_builtin; @@ -98,7 +97,7 @@ pub(crate) fn builtin_attribute_shadowing( if shadows_builtin( name, checker.source_type, - &checker.settings.flake8_builtins.ignorelist, + &checker.settings().flake8_builtins.ignorelist, checker.target_version(), ) { // Ignore explicit overrides. @@ -135,14 +134,14 @@ pub(crate) fn builtin_attribute_shadowing( == Some(scope_id) }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BuiltinAttributeShadowing { kind, name: name.to_string(), row: checker.compute_source_row(binding.start()), }, reference.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs index 508ce4f0e6ac5..6dae6ea8bd946 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_import_shadowing.rs @@ -1,7 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Alias; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_builtins::helpers::shadows_builtin; @@ -40,7 +40,6 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## Options /// - `lint.flake8-builtins.ignorelist` /// - `target-version` -/// #[derive(ViolationMetadata)] pub(crate) struct BuiltinImportShadowing { name: String, @@ -60,14 +59,14 @@ pub(crate) fn builtin_import_shadowing(checker: &Checker, alias: &Alias) { if shadows_builtin( name.as_str(), checker.source_type, - &checker.settings.flake8_builtins.ignorelist, + &checker.settings().flake8_builtins.ignorelist, checker.target_version(), ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BuiltinImportShadowing { name: name.to_string(), }, name.range, - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs index 45a4f003f5af6..ef9ef4a5d0a5a 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_lambda_argument_shadowing.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprLambda; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_builtins::helpers::shadows_builtin; @@ -43,15 +43,15 @@ pub(crate) fn builtin_lambda_argument_shadowing(checker: &Checker, lambda: &Expr if shadows_builtin( name, checker.source_type, - &checker.settings.flake8_builtins.ignorelist, + &checker.settings().flake8_builtins.ignorelist, checker.target_version(), ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BuiltinLambdaArgumentShadowing { name: name.to_string(), }, name.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs index a10a0cd3ac53d..31708157dd427 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs @@ -1,8 +1,7 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::TextRange; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_builtins::helpers::shadows_builtin; @@ -71,14 +70,14 @@ pub(crate) fn builtin_variable_shadowing(checker: &Checker, name: &str, range: T if shadows_builtin( name, checker.source_type, - &checker.settings.flake8_builtins.ignorelist, + &checker.settings().flake8_builtins.ignorelist, checker.target_version(), ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BuiltinVariableShadowing { name: name.to_string(), }, range, - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs index 42cf7c5c07074..66f9e6f657c98 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/stdlib_module_shadowing.rs @@ -1,13 +1,14 @@ use std::borrow::Cow; use std::path::{Component, Path, PathBuf}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_stdlib::path::is_module_file; use ruff_python_stdlib::sys::is_known_standard_library; use ruff_text_size::TextRange; +use crate::Violation; +use crate::checkers::ast::LintContext; use crate::settings::LinterSettings; /// ## What it does @@ -69,9 +70,10 @@ pub(crate) fn stdlib_module_shadowing( mut path: &Path, settings: &LinterSettings, target_version: PythonVersion, -) -> Option { + context: &LintContext, +) { if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) { - return None; + return; } // strip src and root prefixes before converting to a fully-qualified module path @@ -83,7 +85,8 @@ pub(crate) fn stdlib_module_shadowing( // for modules like `modname/__init__.py`, use the parent directory name, otherwise just trim // the `.py` extension let path = if is_module_file(path) { - Cow::from(path.parent()?) + let Some(parent) = path.parent() else { return }; + Cow::from(parent) } else { Cow::from(path.with_extension("")) }; @@ -96,23 +99,25 @@ pub(crate) fn stdlib_module_shadowing( .map(|c| c.as_os_str().to_string_lossy()) .rev(); - let module_name = components.next()?; + let Some(module_name) = components.next() else { + return; + }; if is_allowed_module(settings, target_version, &module_name) { - return None; + return; } // not allowed generally, but check for a parent in non-strict mode if !settings.flake8_builtins.strict_checking && components.next().is_some() { - return None; + return; } - Some(Diagnostic::new( + context.report_diagnostic( StdlibModuleShadowing { name: module_name.to_string(), }, TextRange::default(), - )) + ); } /// Return the longest prefix of `path` between `settings.src` and `settings.project_root`. diff --git a/crates/ruff_linter/src/rules/flake8_commas/mod.rs b/crates/ruff_linter/src/rules/flake8_commas/mod.rs index 1e4f88ca35568..54f225de8122c 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_commas/mod.rs @@ -10,7 +10,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Path::new("COM81.py"))] #[test_case(Path::new("COM81_syntax_error.py"))] @@ -24,7 +24,7 @@ mod tests { Rule::ProhibitedTrailingComma, ]), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs b/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs index bc20c966a0f10..74b5e3fb4e36c 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs +++ b/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs @@ -1,11 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_index::Indexer; use ruff_python_parser::{TokenKind, Tokens}; use ruff_text_size::{Ranged, TextRange}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Violation}; +use crate::{Edit, Fix}; /// Simplified token type. #[derive(Copy, Clone, PartialEq, Eq)] @@ -74,7 +75,7 @@ impl From<(TokenKind, TextRange)> for SimpleToken { TokenKind::Import => TokenType::Named, _ => TokenType::Irrelevant, }; - #[allow(clippy::inconsistent_struct_constructor)] + #[expect(clippy::inconsistent_struct_constructor)] Self { range, ty } } } @@ -238,7 +239,7 @@ impl AlwaysFixableViolation for ProhibitedTrailingComma { /// COM812, COM818, COM819 pub(crate) fn trailing_commas( - diagnostics: &mut Vec, + lint_context: &LintContext, tokens: &Tokens, locator: &Locator, indexer: &Indexer, @@ -291,9 +292,7 @@ pub(crate) fn trailing_commas( // Update the comma context stack. let context = update_context(token, prev, prev_prev, &mut stack); - if let Some(diagnostic) = check_token(token, prev, prev_prev, context, locator) { - diagnostics.push(diagnostic); - } + check_token(token, prev, prev_prev, context, locator, lint_context); // Pop the current context if the current token ended it. // The top context is never popped (if unbalanced closing brackets). @@ -319,7 +318,8 @@ fn check_token( prev_prev: SimpleToken, context: Context, locator: &Locator, -) -> Option { + lint_context: &LintContext, +) { // Is it allowed to have a trailing comma before this token? let comma_allowed = token.ty == TokenType::ClosingBracket && match context.ty { @@ -352,20 +352,25 @@ fn check_token( }; if comma_prohibited { - let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, prev.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range()))); - return Some(diagnostic); + if let Some(mut diagnostic) = + lint_context.report_diagnostic_if_enabled(ProhibitedTrailingComma, prev.range()) + { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); + return; + } } // Is prev a prohibited trailing comma on a bare tuple? // Approximation: any comma followed by a statement-ending newline. let bare_comma_prohibited = prev.ty == TokenType::Comma && token.ty == TokenType::Newline; if bare_comma_prohibited { - return Some(Diagnostic::new(TrailingCommaOnBareTuple, prev.range())); + lint_context.report_diagnostic_if_enabled(TrailingCommaOnBareTuple, prev.range()); + return; } if !comma_allowed { - return None; + return; } // Comma is required if: @@ -382,20 +387,19 @@ fn check_token( | TokenType::OpeningCurlyBracket ); if comma_required { - let mut diagnostic = - Diagnostic::new(MissingTrailingComma, TextRange::empty(prev_prev.end())); - // Create a replacement that includes the final bracket (or other token), - // rather than just inserting a comma at the end. This prevents the UP034 fix - // removing any brackets in the same linter pass - doing both at the same time could - // lead to a syntax error. - let contents = locator.slice(prev_prev.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - format!("{contents},"), - prev_prev.range(), - ))); - Some(diagnostic) - } else { - None + if let Some(mut diagnostic) = lint_context + .report_diagnostic_if_enabled(MissingTrailingComma, TextRange::empty(prev_prev.end())) + { + // Create a replacement that includes the final bracket (or other token), + // rather than just inserting a comma at the end. This prevents the UP034 fix + // removing any brackets in the same linter pass - doing both at the same time could + // lead to a syntax error. + let contents = locator.slice(prev_prev.range()); + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + format!("{contents},"), + prev_prev.range(), + ))); + } } } diff --git a/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81_syntax_error.py.snap b/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81_syntax_error.py.snap index 580392a4995cb..8105e635a63dc 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81_syntax_error.py.snap @@ -6,7 +6,7 @@ COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here 1 | # Check for `flake8-commas` violation for a file containing syntax errors. 2 | ( 3 | *args - | ^ + | ^^^^^ 4 | ) | diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs index 62231f7e39529..32d850820b456 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs @@ -1,6 +1,6 @@ use std::iter; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use itertools::Itertools; use libcst_native::{ Arg, AssignEqual, AssignTargetExpression, Call, Comma, Comment, CompFor, Dict, DictComp, @@ -10,17 +10,17 @@ use libcst_native::{ SimpleString, SimpleWhitespace, TrailingWhitespace, Tuple, }; -use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::{self as ast, Expr, ExprCall}; use ruff_python_codegen::Stylist; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::cst::helpers::{negate, space}; use crate::fix::codemods::CodegenStylist; use crate::fix::edits::pad; use crate::rules::flake8_comprehensions::rules::ObjectType; -use crate::Locator; +use crate::{Edit, Fix}; use crate::{ checkers::ast::Checker, cst::matchers::{ @@ -43,7 +43,10 @@ pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) -> // Extract the (k, v) from `(k, v) for ...`. let generator_exp = match_generator_exp(&arg.value)?; let tuple = match_tuple(&generator_exp.elt)?; - let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + let [ + Element::Simple { value: key, .. }, + Element::Simple { value, .. }, + ] = &tuple.elements[..] else { bail!("Expected tuple to contain two elements"); }; @@ -103,7 +106,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict( let tuple = match_tuple(&list_comp.elt)?; - let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + let [ + Element::Simple { value: key, .. }, + Element::Simple { value, .. }, + ] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); }; @@ -230,7 +236,9 @@ pub(crate) fn fix_unnecessary_collection_call( // below. let mut arena: Vec = vec![]; - let quote = checker.f_string_quote_style().unwrap_or(stylist.quote()); + let quote = checker + .interpolated_string_quote_style() + .unwrap_or(stylist.quote()); // Quote each argument. for arg in &call.args { @@ -311,7 +319,7 @@ pub(crate) fn pad_expression( locator: &Locator, semantic: &SemanticModel, ) -> String { - if !semantic.in_f_string() { + if !semantic.in_interpolated_string() { return content; } @@ -343,7 +351,7 @@ pub(crate) fn pad_start( locator: &Locator, semantic: &SemanticModel, ) -> String { - if !semantic.in_f_string() { + if !semantic.in_interpolated_string() { return content.into(); } @@ -364,7 +372,7 @@ pub(crate) fn pad_end( locator: &Locator, semantic: &SemanticModel, ) -> String { - if !semantic.in_f_string() { + if !semantic.in_interpolated_string() { return content.into(); } @@ -792,10 +800,10 @@ pub(crate) fn fix_unnecessary_map( let mut content = tree.codegen_stylist(stylist); - // If the expression is embedded in an f-string, surround it with spaces to avoid + // If the expression is embedded in an interpolated string, surround it with spaces to avoid // syntax errors. if matches!(object_type, ObjectType::Set | ObjectType::Dict) { - if parent.is_some_and(Expr::is_f_string_expr) { + if parent.is_some_and(|expr| expr.is_f_string_expr() || expr.is_t_string_expr()) { content = format!(" {content} "); } } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/helpers.rs similarity index 100% rename from crates/ruff_linter/src/rules/flake8_comprehensions/rules/helpers.rs rename to crates/ruff_linter/src/rules/flake8_comprehensions/helpers.rs diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 4afc9e1d41b3c..dbc7be8d93d13 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -1,5 +1,6 @@ //! Rules from [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/). mod fixes; +mod helpers; pub(crate) mod rules; pub mod settings; @@ -10,10 +11,10 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; + use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))] @@ -23,6 +24,7 @@ mod tests { #[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_2.py"))] #[test_case(Rule::UnnecessaryDictComprehensionForIterable, Path::new("C420.py"))] #[test_case(Rule::UnnecessaryDictComprehensionForIterable, Path::new("C420_1.py"))] + #[test_case(Rule::UnnecessaryDictComprehensionForIterable, Path::new("C420_2.py"))] #[test_case(Rule::UnnecessaryDoubleCastOrProcess, Path::new("C414.py"))] #[test_case(Rule::UnnecessaryGeneratorDict, Path::new("C402.py"))] #[test_case(Rule::UnnecessaryGeneratorList, Path::new("C400.py"))] @@ -44,7 +46,7 @@ mod tests { Path::new("flake8_comprehensions").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -63,7 +65,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -83,7 +85,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs index f939c774dbe82..a81f045403427 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs @@ -18,7 +18,6 @@ pub(crate) use unnecessary_literal_within_tuple_call::*; pub(crate) use unnecessary_map::*; pub(crate) use unnecessary_subscript_reversal::*; -mod helpers; mod unnecessary_call_around_sorted; mod unnecessary_collection_call; mod unnecessary_comprehension; diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs index 13dc4920865aa..08395c5ea3ef2 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Fix}; use crate::rules::flake8_comprehensions::fixes; @@ -79,7 +79,7 @@ pub(crate) fn unnecessary_call_around_sorted( if !semantic.match_builtin_expr(inner_func, "sorted") { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryCallAroundSorted { func: unnecessary_function, }, @@ -94,7 +94,6 @@ pub(crate) fn unnecessary_call_around_sorted( }; Ok(Fix::applicable_edit(edit, applicability)) }); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs index 510d73bddb660..ac9f6664103e8 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextSize}; @@ -7,6 +6,7 @@ use crate::checkers::ast::Checker; use crate::rules::flake8_comprehensions::fixes; use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start}; use crate::rules::flake8_comprehensions::settings::Settings; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for unnecessary `dict()`, `list()` or `tuple()` calls that can be @@ -89,7 +89,7 @@ pub(crate) fn unnecessary_collection_call( }; let mut diagnostic = - Diagnostic::new(UnnecessaryCollectionCall { kind: collection }, call.range()); + checker.report_diagnostic(UnnecessaryCollectionCall { kind: collection }, call.range()); // Convert `dict()` to `{}`. if call.arguments.keywords.is_empty() { @@ -128,8 +128,6 @@ pub(crate) fn unnecessary_collection_call( fixes::fix_unnecessary_collection_call(call, checker).map(Fix::unsafe_edit) }); } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs index 922e00023406f..558f2ace4fd6d 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Comprehension, Expr}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Fix}; use crate::rules::flake8_comprehensions::fixes; @@ -85,7 +85,7 @@ fn add_diagnostic(checker: &Checker, expr: &Expr) { { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryComprehension { kind: comprehension_kind, }, @@ -95,7 +95,6 @@ fn add_diagnostic(checker: &Checker, expr: &Expr) { fixes::fix_unnecessary_comprehension(expr, checker.locator(), checker.stylist()) .map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } /// C416 @@ -115,9 +114,12 @@ pub(crate) fn unnecessary_dict_comprehension( let Expr::Tuple(ast::ExprTuple { elts, .. }) = &generator.target else { return; }; - let [Expr::Name(ast::ExprName { id: target_key, .. }), Expr::Name(ast::ExprName { - id: target_value, .. - })] = elts.as_slice() + let [ + Expr::Name(ast::ExprName { id: target_key, .. }), + Expr::Name(ast::ExprName { + id: target_value, .. + }), + ] = elts.as_slice() else { return; }; @@ -158,14 +160,19 @@ pub(crate) fn unnecessary_list_set_comprehension( }), Expr::Tuple(ast::ExprTuple { elts, .. }), ) => { - let [Expr::Name(ast::ExprName { id: target_key, .. }), Expr::Name(ast::ExprName { - id: target_value, .. - })] = target_elts.as_slice() + let [ + Expr::Name(ast::ExprName { id: target_key, .. }), + Expr::Name(ast::ExprName { + id: target_value, .. + }), + ] = target_elts.as_slice() else { return; }; - let [Expr::Name(ast::ExprName { id: key, .. }), Expr::Name(ast::ExprName { id: value, .. })] = - elts.as_slice() + let [ + Expr::Name(ast::ExprName { id: key, .. }), + Expr::Name(ast::ExprName { id: value, .. }), + ] = elts.as_slice() else { return; }; diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs index cfa611e7deb61..6565131796552 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs @@ -1,12 +1,13 @@ -use ruff_diagnostics::{Diagnostic, FixAvailability}; -use ruff_diagnostics::{Edit, Fix, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::{self as ast, Expr, Keyword}; use ruff_text_size::{Ranged, TextSize}; +use crate::FixAvailability; use crate::checkers::ast::Checker; +use crate::preview::is_comprehension_with_min_max_sum_enabled; use crate::rules::flake8_comprehensions::fixes; +use crate::{Edit, Fix, Violation}; /// ## What it does /// Checks for unnecessary list or set comprehensions passed to builtin functions that take an iterable. @@ -125,7 +126,7 @@ pub(crate) fn unnecessary_comprehension_in_call( if !(matches!( builtin_function, SupportedBuiltins::Any | SupportedBuiltins::All - ) || (checker.settings.preview.is_enabled() + ) || (is_comprehension_with_min_max_sum_enabled(checker.settings()) && matches!( builtin_function, SupportedBuiltins::Sum | SupportedBuiltins::Min | SupportedBuiltins::Max @@ -135,13 +136,13 @@ pub(crate) fn unnecessary_comprehension_in_call( } let mut diagnostic = match (arg, builtin_function.duplication_variance()) { - (Expr::ListComp(_), _) => Diagnostic::new( + (Expr::ListComp(_), _) => checker.report_diagnostic( UnnecessaryComprehensionInCall { comprehension_kind: ComprehensionKind::List, }, arg.range(), ), - (Expr::SetComp(_), DuplicationVariance::Invariant) => Diagnostic::new( + (Expr::SetComp(_), DuplicationVariance::Invariant) => checker.report_diagnostic( UnnecessaryComprehensionInCall { comprehension_kind: ComprehensionKind::Set, }, @@ -174,7 +175,6 @@ pub(crate) fn unnecessary_comprehension_in_call( diagnostic.set_fix(Fix::unsafe_edits(collection_start, [collection_end])); } - checker.report_diagnostic(diagnostic); } /// Return `true` if the [`Expr`] contains an `await` expression. diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs index 7f56ecfaeb03c..b0fddc96dc088 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs @@ -1,12 +1,14 @@ use ast::ExprName; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::{self as ast, Arguments, Comprehension, Expr, ExprCall, ExprContext}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::fix::edits::pad_start; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary dict comprehension when creating a dictionary from @@ -31,6 +33,19 @@ use crate::checkers::ast::Checker; /// dict.fromkeys(iterable, 1) /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments inside the dict comprehension, +/// as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// { # comment 1 +/// a: # comment 2 +/// None # comment 3 +/// for a in iterable # comment 4 +/// } +/// ``` +/// /// ## References /// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys) #[derive(ViolationMetadata)] @@ -113,7 +128,7 @@ pub(crate) fn unnecessary_dict_comprehension_for_iterable( return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryDictComprehensionForIterable { is_value_none_literal: dict_comp.value.is_none_literal_expr(), }, @@ -121,18 +136,28 @@ pub(crate) fn unnecessary_dict_comprehension_for_iterable( ); if checker.semantic().has_builtin_binding("dict") { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - checker - .generator() - .expr(&fix_unnecessary_dict_comprehension( - dict_comp.value.as_ref(), - generator, - )), + let edit = Edit::range_replacement( + pad_start( + checker + .generator() + .expr(&fix_unnecessary_dict_comprehension( + dict_comp.value.as_ref(), + generator, + )), + dict_comp.start(), + checker.locator(), + ), dict_comp.range(), - ))); + ); + diagnostic.set_fix(Fix::applicable_edit( + edit, + if checker.comment_ranges().intersects(dict_comp.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); } - - checker.report_diagnostic(diagnostic); } /// Returns `true` if the expression can be shared across multiple values. @@ -178,14 +203,17 @@ fn fix_unnecessary_dict_comprehension(value: &Expr, generator: &Comprehension) - }, keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; Expr::Call(ExprCall { func: Box::new(Expr::Name(ExprName { id: "dict.fromkeys".into(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), arguments: args, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs index 48a16d57e3d58..c0811cdb06557 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableKeyword; use ruff_python_ast::{self as ast, Arguments, Expr, Keyword}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Fix}; use crate::rules::flake8_comprehensions::fixes; @@ -125,7 +125,7 @@ pub(crate) fn unnecessary_double_cast_or_process( | ("set", "set") | ("list" | "tuple", "list" | "tuple") ) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryDoubleCastOrProcess { inner: inner_func_name.to_string(), outer: outer_func_name.to_string(), @@ -140,6 +140,5 @@ pub(crate) fn unnecessary_double_cast_or_process( ) .map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs index ea7f036eeb8c5..7cf15ff401980 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Keyword}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Fix}; use crate::rules::flake8_comprehensions::fixes; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary generators that can be rewritten as dict @@ -70,8 +70,7 @@ pub(crate) fn unnecessary_generator_dict( if tuple.iter().any(Expr::is_starred_expr) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryGeneratorDict, expr.range()); diagnostic .try_set_fix(|| fixes::fix_unnecessary_generator_dict(expr, checker).map(Fix::unsafe_edit)); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index e2739c5ab719f..eba4c76c603d7 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -1,15 +1,15 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; +use ruff_python_ast::ExprGenerator; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::parenthesize::parenthesized_range; -use ruff_python_ast::ExprGenerator; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary generators that can be rewritten as list @@ -94,22 +94,23 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall if let [generator] = generators.as_slice() { if generator.ifs.is_empty() && !generator.is_async { if ComparableExpr::from(elt) == ComparableExpr::from(&generator.target) { - let diagnostic = Diagnostic::new( - UnnecessaryGeneratorList { - short_circuit: true, - }, - call.range(), - ); let iterator = format!("list({})", checker.locator().slice(generator.iter.range())); let fix = Fix::unsafe_edit(Edit::range_replacement(iterator, call.range())); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic( + UnnecessaryGeneratorList { + short_circuit: true, + }, + call.range(), + ) + .set_fix(fix); return; } } } // Convert `list(f(x) for x in y)` to `[f(x) for x in y]`. - let diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryGeneratorList { short_circuit: false, }, @@ -158,5 +159,5 @@ pub(crate) fn unnecessary_generator_list(checker: &Checker, call: &ast::ExprCall Fix::unsafe_edits(call_start, [call_end]) } }; - checker.report_diagnostic(diagnostic.with_fix(fix)); + diagnostic.set_fix(fix); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 4582a274acf68..0cbad1d806c9a 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -1,16 +1,16 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; +use ruff_python_ast::ExprGenerator; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::parenthesize::parenthesized_range; -use ruff_python_ast::ExprGenerator; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start}; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary generators that can be rewritten as set @@ -94,7 +94,7 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall) if let [generator] = generators.as_slice() { if generator.ifs.is_empty() && !generator.is_async { if ComparableExpr::from(elt) == ComparableExpr::from(&generator.target) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryGeneratorSet { short_circuit: true, }, @@ -105,14 +105,13 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall) iterator, call.range(), ))); - checker.report_diagnostic(diagnostic); return; } } } // Convert `set(f(x) for x in y)` to `{f(x) for x in y}`. - let diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryGeneratorSet { short_circuit: false, }, @@ -165,5 +164,5 @@ pub(crate) fn unnecessary_generator_set(checker: &Checker, call: &ast::ExprCall) Fix::unsafe_edits(call_start, [call_end]) } }; - checker.report_diagnostic(diagnostic.with_fix(fix)); + diagnostic.set_fix(fix); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs index 851c4be16a1df..422081814a5b2 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs @@ -1,14 +1,13 @@ use ruff_python_ast::{Arguments, Expr, ExprCall}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; - use crate::rules::flake8_comprehensions::fixes; +use crate::{AlwaysFixableViolation, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary `list()` calls around list comprehensions. @@ -49,6 +48,7 @@ pub(crate) fn unnecessary_list_call(checker: &Checker, expr: &Expr, call: &ExprC func, arguments, range: _, + node_index: _, } = call; if !arguments.keywords.is_empty() { @@ -61,6 +61,7 @@ pub(crate) fn unnecessary_list_call(checker: &Checker, expr: &Expr, call: &ExprC let Arguments { range: _, + node_index: _, args, keywords: _, } = arguments; @@ -74,10 +75,9 @@ pub(crate) fn unnecessary_list_call(checker: &Checker, expr: &Expr, call: &ExprC if !checker.semantic().has_builtin_binding("list") { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCall, expr.range()); diagnostic.try_set_fix(|| { fixes::fix_unnecessary_list_call(expr, checker.locator(), checker.stylist()) .map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs index 4bad3a17ca486..6ea47477480b3 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs @@ -1,13 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Keyword}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; - use crate::rules::flake8_comprehensions::fixes; +use crate::{AlwaysFixableViolation, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary list comprehensions. @@ -68,9 +67,8 @@ pub(crate) fn unnecessary_list_comprehension_dict( if !checker.semantic().has_builtin_binding("dict") { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListComprehensionDict, expr.range()); diagnostic.try_set_fix(|| { fixes::fix_unnecessary_list_comprehension_dict(expr, checker).map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index be2c56329568e..966b51c388569 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_parser::TokenKind; @@ -7,8 +6,9 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start}; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary list comprehensions. @@ -60,7 +60,7 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast:: if !argument.is_list_comp_expr() { return; } - let diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, call.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListComprehensionSet, call.range()); let one = TextSize::from(1); // Replace `set(` with `{`. @@ -100,5 +100,5 @@ pub(crate) fn unnecessary_list_comprehension_set(checker: &Checker, call: &ast:: let replacement = Edit::range_replacement(checker.source()[span].to_string(), replacement_range); let fix = Fix::unsafe_edits(call_start, [call_end, replacement]); - checker.report_diagnostic(diagnostic.with_fix(fix)); + diagnostic.set_fix(fix); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs index 3ccfccda35e5b..d23a74582bb13 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs @@ -1,13 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Keyword}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; - use crate::rules::flake8_comprehensions::fixes; +use crate::{AlwaysFixableViolation, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for unnecessary list or tuple literals. @@ -78,10 +77,10 @@ pub(crate) fn unnecessary_literal_dict( if !checker.semantic().has_builtin_binding("dict") { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryLiteralDict { obj_type: kind }, expr.range()); + let mut diagnostic = + checker.report_diagnostic(UnnecessaryLiteralDict { obj_type: kind }, expr.range()); diagnostic .try_set_fix(|| fixes::fix_unnecessary_literal_dict(expr, checker).map(Fix::unsafe_edit)); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs index 31f33bf5208c6..e89f3e2fda0b9 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start}; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for `set()` calls that take unnecessary list or tuple literals @@ -67,7 +67,7 @@ pub(crate) fn unnecessary_literal_set(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryLiteralSet { kind }, call.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryLiteralSet { kind }, call.range()); // Convert `set((1, 2))` to `{1, 2}`. diagnostic.set_fix({ @@ -124,8 +124,6 @@ pub(crate) fn unnecessary_literal_set(checker: &Checker, call: &ast::ExprCall) { } } }); - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs index 7b24cb8d9f2c6..1f1c04ccc7832 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs @@ -2,13 +2,13 @@ use std::fmt; use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for `dict()` calls that take unnecessary dict literals or dict @@ -71,7 +71,7 @@ pub(crate) fn unnecessary_literal_within_dict_call(checker: &Checker, call: &ast return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryLiteralWithinDictCall { kind: argument_kind, }, @@ -88,8 +88,6 @@ pub(crate) fn unnecessary_literal_within_dict_call(checker: &Checker, call: &ast Fix::unsafe_edits(call_start, [call_end]) }); - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs index 72bad6b12fc29..5d4a9d7a4ced3 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for `list()` calls that take unnecessary list or tuple literals as @@ -82,7 +82,7 @@ pub(crate) fn unnecessary_literal_within_list_call(checker: &Checker, call: &ast return; } - let diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryLiteralWithinListCall { kind: argument_kind, }, @@ -119,7 +119,7 @@ pub(crate) fn unnecessary_literal_within_list_call(checker: &Checker, call: &ast } }; - checker.report_diagnostic(diagnostic.with_fix(fix)); + diagnostic.set_fix(fix); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index 50a44a31f5d3a..b2d3af263f07c 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -1,14 +1,15 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::{self as ast, Expr}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::preview::is_check_comprehensions_in_tuple_call_enabled; use crate::rules::flake8_comprehensions::fixes; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers; +use crate::rules::flake8_comprehensions::helpers; /// ## What it does /// Checks for `tuple` calls that take unnecessary list or tuple literals as @@ -100,14 +101,16 @@ pub(crate) fn unnecessary_literal_within_tuple_call( let argument_kind = match argument { Expr::Tuple(_) => TupleLiteralKind::Tuple, Expr::List(_) => TupleLiteralKind::List, - Expr::ListComp(_) if checker.settings.preview.is_enabled() => TupleLiteralKind::ListComp, + Expr::ListComp(_) if is_check_comprehensions_in_tuple_call_enabled(checker.settings()) => { + TupleLiteralKind::ListComp + } _ => return, }; if !checker.semantic().has_builtin_binding("tuple") { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryLiteralWithinTupleCall { literal_kind: argument_kind, }, @@ -162,10 +165,8 @@ pub(crate) fn unnecessary_literal_within_tuple_call( }); } - _ => return, + _ => (), } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index 394c045d48671..e31c4b15a097c 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -1,16 +1,16 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Fix}; -use ruff_diagnostics::{FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, ExprContext, Parameters, Stmt}; -use ruff_python_ast::{visitor, ExprLambda}; +use ruff_python_ast::{ExprLambda, visitor}; use ruff_python_semantic::SemanticModel; +use crate::Fix; use crate::checkers::ast::Checker; use crate::rules::flake8_comprehensions::fixes; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary `map()` calls with lambda functions. @@ -138,7 +138,7 @@ pub(crate) fn unnecessary_map(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, call.range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryMap { object_type }, call.range); diagnostic.try_set_fix(|| { fixes::fix_unnecessary_map( call, @@ -149,7 +149,6 @@ pub(crate) fn unnecessary_map(checker: &Checker, call: &ast::ExprCall) { ) .map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } fn is_list_set_or_dict(func: &Expr, semantic: &SemanticModel) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs index a91c38f8c474c..5357daf99fd32 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, UnaryOp}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -52,6 +52,7 @@ pub(crate) fn unnecessary_subscript_reversal(checker: &Checker, call: &ast::Expr upper, step, range: _, + node_index: _, }) = slice.as_ref() else { return; @@ -66,6 +67,7 @@ pub(crate) fn unnecessary_subscript_reversal(checker: &Checker, call: &ast::Expr op: UnaryOp::USub, operand, range: _, + node_index: _, }) = step.as_ref() else { return; @@ -86,10 +88,10 @@ pub(crate) fn unnecessary_subscript_reversal(checker: &Checker, call: &ast::Expr if !matches!(function_name, "reversed" | "set" | "sorted") { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnnecessarySubscriptReversal { func: function_name.to_string(), }, call.range(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401.py.snap index 7ca2ce2725889..c2af13db5e868 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C401_C401.py.snap @@ -346,7 +346,7 @@ C401.py:32:1: C401 [*] Unnecessary generator (rewrite as a set comprehension) 37 | | ) | |_^ C401 38 | -39 | # Not built-in set. +39 | # t-strings | = help: Rewrite as a set comprehension @@ -364,5 +364,166 @@ C401.py:32:1: C401 [*] Unnecessary generator (rewrite as a set comprehension) 37 |-) 35 |+ } 38 36 | -39 37 | # Not built-in set. -40 38 | def set(*args, **kwargs): +39 37 | # t-strings +40 38 | print(t"Hello {set(f(a) for a in 'abc')} World") + +C401.py:40:16: C401 [*] Unnecessary generator (rewrite as a set comprehension) + | +39 | # t-strings +40 | print(t"Hello {set(f(a) for a in 'abc')} World") + | ^^^^^^^^^^^^^^^^^^^^^^^^ C401 +41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +37 37 | ) +38 38 | +39 39 | # t-strings +40 |-print(t"Hello {set(f(a) for a in 'abc')} World") + 40 |+print(t"Hello { {f(a) for a in 'abc'} } World") +41 41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 43 | print(t"Hello {set(a for a in range(3))} World") + +C401.py:41:17: C401 [*] Unnecessary generator (rewrite as a set comprehension) + | +39 | # t-strings +40 | print(t"Hello {set(f(a) for a in 'abc')} World") +41 | print(t"Hello { set(f(a) for a in 'abc') } World") + | ^^^^^^^^^^^^^^^^^^^^^^^^ C401 +42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 | print(t"Hello {set(a for a in range(3))} World") + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +38 38 | +39 39 | # t-strings +40 40 | print(t"Hello {set(f(a) for a in 'abc')} World") +41 |-print(t"Hello { set(f(a) for a in 'abc') } World") + 41 |+print(t"Hello { {f(a) for a in 'abc'} } World") +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 43 | print(t"Hello {set(a for a in range(3))} World") +44 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") + +C401.py:42:17: C401 [*] Unnecessary generator (rewrite as a set comprehension) + | +40 | print(t"Hello {set(f(a) for a in 'abc')} World") +41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401 +43 | print(t"Hello {set(a for a in range(3))} World") +44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +39 39 | # t-strings +40 40 | print(t"Hello {set(f(a) for a in 'abc')} World") +41 41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 |-small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" + 42 |+small_nums = t"{ {a if a < 6 else 0 for a in range(3)} }" +43 43 | print(t"Hello {set(a for a in range(3))} World") +44 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + +C401.py:43:16: C401 [*] Unnecessary generator (rewrite using `set()`) + | +41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 | print(t"Hello {set(a for a in range(3))} World") + | ^^^^^^^^^^^^^^^^^^^^^^^^ C401 +44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + | + = help: Rewrite using `set()` + +ℹ Unsafe fix +40 40 | print(t"Hello {set(f(a) for a in 'abc')} World") +41 41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 |-print(t"Hello {set(a for a in range(3))} World") + 43 |+print(t"Hello {set(range(3))} World") +44 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") +46 46 | + +C401.py:44:10: C401 [*] Unnecessary generator (rewrite using `set()`) + | +42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 | print(t"Hello {set(a for a in range(3))} World") +44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") + | ^^^^^^^^^^^^^^^^^^^^^ C401 +45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + | + = help: Rewrite using `set()` + +ℹ Unsafe fix +41 41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 43 | print(t"Hello {set(a for a in range(3))} World") +44 |-print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") + 44 |+print(t"{set('abc') - set(a for a in 'ab')}") +45 45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") +46 46 | +47 47 | + +C401.py:44:34: C401 [*] Unnecessary generator (rewrite using `set()`) + | +42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 | print(t"Hello {set(a for a in range(3))} World") +44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") + | ^^^^^^^^^^^^^^^^^^^^ C401 +45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + | + = help: Rewrite using `set()` + +ℹ Unsafe fix +41 41 | print(t"Hello { set(f(a) for a in 'abc') } World") +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 43 | print(t"Hello {set(a for a in range(3))} World") +44 |-print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") + 44 |+print(t"{set(a for a in 'abc') - set('ab')}") +45 45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") +46 46 | +47 47 | + +C401.py:45:11: C401 [*] Unnecessary generator (rewrite using `set()`) + | +43 | print(t"Hello {set(a for a in range(3))} World") +44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + | ^^^^^^^^^^^^^^^^^^^^^ C401 + | + = help: Rewrite using `set()` + +ℹ Unsafe fix +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 43 | print(t"Hello {set(a for a in range(3))} World") +44 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 |-print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + 45 |+print(t"{ set('abc') - set(a for a in 'ab') }") +46 46 | +47 47 | +48 48 | # Not built-in set. + +C401.py:45:35: C401 [*] Unnecessary generator (rewrite using `set()`) + | +43 | print(t"Hello {set(a for a in range(3))} World") +44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 | print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + | ^^^^^^^^^^^^^^^^^^^^ C401 + | + = help: Rewrite using `set()` + +ℹ Unsafe fix +42 42 | small_nums = t"{set(a if a < 6 else 0 for a in range(3))}" +43 43 | print(t"Hello {set(a for a in range(3))} World") +44 44 | print(t"{set(a for a in 'abc') - set(a for a in 'ab')}") +45 |-print(t"{ set(a for a in 'abc') - set(a for a in 'ab') }") + 45 |+print(t"{ set(a for a in 'abc') - set('ab') }") +46 46 | +47 47 | +48 48 | # Not built-in set. diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403.py.snap index 57b06b06d6ca7..df5f910c3abaf 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C403_C403.py.snap @@ -319,6 +319,8 @@ C403.py:37:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehe 36 | # Test trailing comma case 37 | s = set([x for x in range(3)],) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ C403 +38 | +39 | s = t"{set([x for x in 'ab'])}" | = help: Rewrite as a set comprehension @@ -328,3 +330,137 @@ C403.py:37:5: C403 [*] Unnecessary list comprehension (rewrite as a set comprehe 36 36 | # Test trailing comma case 37 |-s = set([x for x in range(3)],) 37 |+s = {x for x in range(3)} +38 38 | +39 39 | s = t"{set([x for x in 'ab'])}" +40 40 | s = t'{set([x for x in "ab"])}' + +C403.py:39:8: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +37 | s = set([x for x in range(3)],) +38 | +39 | s = t"{set([x for x in 'ab'])}" + | ^^^^^^^^^^^^^^^^^^^^^^ C403 +40 | s = t'{set([x for x in "ab"])}' + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +36 36 | # Test trailing comma case +37 37 | s = set([x for x in range(3)],) +38 38 | +39 |-s = t"{set([x for x in 'ab'])}" + 39 |+s = t"{ {x for x in 'ab'} }" +40 40 | s = t'{set([x for x in "ab"])}' +41 41 | +42 42 | def f(x): + +C403.py:40:8: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +39 | s = t"{set([x for x in 'ab'])}" +40 | s = t'{set([x for x in "ab"])}' + | ^^^^^^^^^^^^^^^^^^^^^^ C403 +41 | +42 | def f(x): + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +37 37 | s = set([x for x in range(3)],) +38 38 | +39 39 | s = t"{set([x for x in 'ab'])}" +40 |-s = t'{set([x for x in "ab"])}' + 40 |+s = t'{ {x for x in "ab"} }' +41 41 | +42 42 | def f(x): +43 43 | return x + +C403.py:45:8: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +43 | return x +44 | +45 | s = t"{set([f(x) for x in 'ab'])}" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ C403 +46 | +47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +42 42 | def f(x): +43 43 | return x +44 44 | +45 |-s = t"{set([f(x) for x in 'ab'])}" + 45 |+s = t"{ {f(x) for x in 'ab'} }" +46 46 | +47 47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" +48 48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + +C403.py:47:9: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +45 | s = t"{set([f(x) for x in 'ab'])}" +46 | +47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" + | ^^^^^^^^^^^^^^^^^^^^^^ C403 +48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +44 44 | +45 45 | s = t"{set([f(x) for x in 'ab'])}" +46 46 | +47 |-s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" + 47 |+s = t"{ {x for x in 'ab'} | set([x for x in 'ab']) }" +48 48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" +49 49 | + +C403.py:47:34: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +45 | s = t"{set([f(x) for x in 'ab'])}" +46 | +47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" + | ^^^^^^^^^^^^^^^^^^^^^^ C403 +48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +44 44 | +45 45 | s = t"{set([f(x) for x in 'ab'])}" +46 46 | +47 |-s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" + 47 |+s = t"{ set([x for x in 'ab']) | {x for x in 'ab'} }" +48 48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" +49 49 | + +C403.py:48:8: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" +48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + | ^^^^^^^^^^^^^^^^^^^^^^ C403 + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +45 45 | s = t"{set([f(x) for x in 'ab'])}" +46 46 | +47 47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" +48 |-s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + 48 |+s = t"{ {x for x in 'ab'} | set([x for x in 'ab'])}" +49 49 | + +C403.py:48:33: C403 [*] Unnecessary list comprehension (rewrite as a set comprehension) + | +47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" +48 | s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + | ^^^^^^^^^^^^^^^^^^^^^^ C403 + | + = help: Rewrite as a set comprehension + +ℹ Unsafe fix +45 45 | s = t"{set([f(x) for x in 'ab'])}" +46 46 | +47 47 | s = t"{ set([x for x in 'ab']) | set([x for x in 'ab']) }" +48 |-s = t"{set([x for x in 'ab']) | set([x for x in 'ab'])}" + 48 |+s = t"{set([x for x in 'ab']) | {x for x in 'ab'} }" +49 49 | diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C405_C405.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C405_C405.py.snap index b3c7e4b5bef18..b8df7cf5b0978 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C405_C405.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C405_C405.py.snap @@ -322,6 +322,7 @@ C405.py:24:5: C405 [*] Unnecessary list literal (rewrite as a set literal) 24 |+f"{ {'a', 'b'} - set(['a']) }" 25 25 | f"a {set(['a', 'b']) - set(['a'])} b" 26 26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 27 | C405.py:24:23: C405 [*] Unnecessary list literal (rewrite as a set literal) | @@ -341,6 +342,7 @@ C405.py:24:23: C405 [*] Unnecessary list literal (rewrite as a set literal) 24 |+f"{ set(['a', 'b']) - {'a'} }" 25 25 | f"a {set(['a', 'b']) - set(['a'])} b" 26 26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 27 | C405.py:25:6: C405 [*] Unnecessary list literal (rewrite as a set literal) | @@ -359,6 +361,8 @@ C405.py:25:6: C405 [*] Unnecessary list literal (rewrite as a set literal) 25 |-f"a {set(['a', 'b']) - set(['a'])} b" 25 |+f"a { {'a', 'b'} - set(['a'])} b" 26 26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 27 | +28 28 | t"{set([1,2,3])}" C405.py:25:24: C405 [*] Unnecessary list literal (rewrite as a set literal) | @@ -377,6 +381,8 @@ C405.py:25:24: C405 [*] Unnecessary list literal (rewrite as a set literal) 25 |-f"a {set(['a', 'b']) - set(['a'])} b" 25 |+f"a {set(['a', 'b']) - {'a'} } b" 26 26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 27 | +28 28 | t"{set([1,2,3])}" C405.py:26:7: C405 [*] Unnecessary list literal (rewrite as a set literal) | @@ -384,6 +390,8 @@ C405.py:26:7: C405 [*] Unnecessary list literal (rewrite as a set literal) 25 | f"a {set(['a', 'b']) - set(['a'])} b" 26 | f"a { set(['a', 'b']) - set(['a']) } b" | ^^^^^^^^^^^^^^^ C405 +27 | +28 | t"{set([1,2,3])}" | = help: Rewrite as a set literal @@ -393,6 +401,9 @@ C405.py:26:7: C405 [*] Unnecessary list literal (rewrite as a set literal) 25 25 | f"a {set(['a', 'b']) - set(['a'])} b" 26 |-f"a { set(['a', 'b']) - set(['a']) } b" 26 |+f"a { {'a', 'b'} - set(['a']) } b" +27 27 | +28 28 | t"{set([1,2,3])}" +29 29 | t"{set(['a', 'b'])}" C405.py:26:25: C405 [*] Unnecessary list literal (rewrite as a set literal) | @@ -400,6 +411,8 @@ C405.py:26:25: C405 [*] Unnecessary list literal (rewrite as a set literal) 25 | f"a {set(['a', 'b']) - set(['a'])} b" 26 | f"a { set(['a', 'b']) - set(['a']) } b" | ^^^^^^^^^^ C405 +27 | +28 | t"{set([1,2,3])}" | = help: Rewrite as a set literal @@ -409,3 +422,215 @@ C405.py:26:25: C405 [*] Unnecessary list literal (rewrite as a set literal) 25 25 | f"a {set(['a', 'b']) - set(['a'])} b" 26 |-f"a { set(['a', 'b']) - set(['a']) } b" 26 |+f"a { set(['a', 'b']) - {'a'} } b" +27 27 | +28 28 | t"{set([1,2,3])}" +29 29 | t"{set(['a', 'b'])}" + +C405.py:28:4: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 | +28 | t"{set([1,2,3])}" + | ^^^^^^^^^^^^ C405 +29 | t"{set(['a', 'b'])}" +30 | t'{set(["a", "b"])}' + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +25 25 | f"a {set(['a', 'b']) - set(['a'])} b" +26 26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 27 | +28 |-t"{set([1,2,3])}" + 28 |+t"{ {1,2,3} }" +29 29 | t"{set(['a', 'b'])}" +30 30 | t'{set(["a", "b"])}' +31 31 | + +C405.py:29:4: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +28 | t"{set([1,2,3])}" +29 | t"{set(['a', 'b'])}" + | ^^^^^^^^^^^^^^^ C405 +30 | t'{set(["a", "b"])}' + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +26 26 | f"a { set(['a', 'b']) - set(['a']) } b" +27 27 | +28 28 | t"{set([1,2,3])}" +29 |-t"{set(['a', 'b'])}" + 29 |+t"{ {'a', 'b'} }" +30 30 | t'{set(["a", "b"])}' +31 31 | +32 32 | t"{set(['a', 'b']) - set(['a'])}" + +C405.py:30:4: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +28 | t"{set([1,2,3])}" +29 | t"{set(['a', 'b'])}" +30 | t'{set(["a", "b"])}' + | ^^^^^^^^^^^^^^^ C405 +31 | +32 | t"{set(['a', 'b']) - set(['a'])}" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +27 27 | +28 28 | t"{set([1,2,3])}" +29 29 | t"{set(['a', 'b'])}" +30 |-t'{set(["a", "b"])}' + 30 |+t'{ {"a", "b"} }' +31 31 | +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" + +C405.py:32:4: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +30 | t'{set(["a", "b"])}' +31 | +32 | t"{set(['a', 'b']) - set(['a'])}" + | ^^^^^^^^^^^^^^^ C405 +33 | t"{ set(['a', 'b']) - set(['a']) }" +34 | t"a {set(['a', 'b']) - set(['a'])} b" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +29 29 | t"{set(['a', 'b'])}" +30 30 | t'{set(["a", "b"])}' +31 31 | +32 |-t"{set(['a', 'b']) - set(['a'])}" + 32 |+t"{ {'a', 'b'} - set(['a'])}" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" +34 34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 35 | t"a { set(['a', 'b']) - set(['a']) } b" + +C405.py:32:22: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +30 | t'{set(["a", "b"])}' +31 | +32 | t"{set(['a', 'b']) - set(['a'])}" + | ^^^^^^^^^^ C405 +33 | t"{ set(['a', 'b']) - set(['a']) }" +34 | t"a {set(['a', 'b']) - set(['a'])} b" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +29 29 | t"{set(['a', 'b'])}" +30 30 | t'{set(["a", "b"])}' +31 31 | +32 |-t"{set(['a', 'b']) - set(['a'])}" + 32 |+t"{set(['a', 'b']) - {'a'} }" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" +34 34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 35 | t"a { set(['a', 'b']) - set(['a']) } b" + +C405.py:33:5: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +32 | t"{set(['a', 'b']) - set(['a'])}" +33 | t"{ set(['a', 'b']) - set(['a']) }" + | ^^^^^^^^^^^^^^^ C405 +34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 | t"a { set(['a', 'b']) - set(['a']) } b" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +30 30 | t'{set(["a", "b"])}' +31 31 | +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 |-t"{ set(['a', 'b']) - set(['a']) }" + 33 |+t"{ {'a', 'b'} - set(['a']) }" +34 34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 35 | t"a { set(['a', 'b']) - set(['a']) } b" + +C405.py:33:23: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +32 | t"{set(['a', 'b']) - set(['a'])}" +33 | t"{ set(['a', 'b']) - set(['a']) }" + | ^^^^^^^^^^ C405 +34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 | t"a { set(['a', 'b']) - set(['a']) } b" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +30 30 | t'{set(["a", "b"])}' +31 31 | +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 |-t"{ set(['a', 'b']) - set(['a']) }" + 33 |+t"{ set(['a', 'b']) - {'a'} }" +34 34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 35 | t"a { set(['a', 'b']) - set(['a']) } b" + +C405.py:34:6: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +32 | t"{set(['a', 'b']) - set(['a'])}" +33 | t"{ set(['a', 'b']) - set(['a']) }" +34 | t"a {set(['a', 'b']) - set(['a'])} b" + | ^^^^^^^^^^^^^^^ C405 +35 | t"a { set(['a', 'b']) - set(['a']) } b" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +31 31 | +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" +34 |-t"a {set(['a', 'b']) - set(['a'])} b" + 34 |+t"a { {'a', 'b'} - set(['a'])} b" +35 35 | t"a { set(['a', 'b']) - set(['a']) } b" + +C405.py:34:24: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +32 | t"{set(['a', 'b']) - set(['a'])}" +33 | t"{ set(['a', 'b']) - set(['a']) }" +34 | t"a {set(['a', 'b']) - set(['a'])} b" + | ^^^^^^^^^^ C405 +35 | t"a { set(['a', 'b']) - set(['a']) } b" + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +31 31 | +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" +34 |-t"a {set(['a', 'b']) - set(['a'])} b" + 34 |+t"a {set(['a', 'b']) - {'a'} } b" +35 35 | t"a { set(['a', 'b']) - set(['a']) } b" + +C405.py:35:7: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +33 | t"{ set(['a', 'b']) - set(['a']) }" +34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 | t"a { set(['a', 'b']) - set(['a']) } b" + | ^^^^^^^^^^^^^^^ C405 + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" +34 34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 |-t"a { set(['a', 'b']) - set(['a']) } b" + 35 |+t"a { {'a', 'b'} - set(['a']) } b" + +C405.py:35:25: C405 [*] Unnecessary list literal (rewrite as a set literal) + | +33 | t"{ set(['a', 'b']) - set(['a']) }" +34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 | t"a { set(['a', 'b']) - set(['a']) } b" + | ^^^^^^^^^^ C405 + | + = help: Rewrite as a set literal + +ℹ Unsafe fix +32 32 | t"{set(['a', 'b']) - set(['a'])}" +33 33 | t"{ set(['a', 'b']) - set(['a']) }" +34 34 | t"a {set(['a', 'b']) - set(['a'])} b" +35 |-t"a { set(['a', 'b']) - set(['a']) } b" + 35 |+t"a { set(['a', 'b']) - {'a'} } b" diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap index 973aa65bc4076..e9519927ac3a7 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap @@ -354,6 +354,8 @@ C408.py:28:1: C408 [*] Unnecessary `tuple()` call (rewrite as a literal) 28 | / tuple( # comment 29 | | ) | |_^ C408 +30 | +31 | t"{dict(x='y')}" | = help: Rewrite as a literal @@ -364,3 +366,235 @@ C408.py:28:1: C408 [*] Unnecessary `tuple()` call (rewrite as a literal) 28 |-tuple( # comment 28 |+( # comment 29 29 | ) +30 30 | +31 31 | t"{dict(x='y')}" + +C408.py:31:4: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +29 | ) +30 | +31 | t"{dict(x='y')}" + | ^^^^^^^^^^^ C408 +32 | t'{dict(x="y")}' +33 | t"{dict()}" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +28 28 | tuple( # comment +29 29 | ) +30 30 | +31 |-t"{dict(x='y')}" + 31 |+t"{ {'x': 'y'} }" +32 32 | t'{dict(x="y")}' +33 33 | t"{dict()}" +34 34 | t"a {dict()} b" + +C408.py:32:4: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +31 | t"{dict(x='y')}" +32 | t'{dict(x="y")}' + | ^^^^^^^^^^^ C408 +33 | t"{dict()}" +34 | t"a {dict()} b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +29 29 | ) +30 30 | +31 31 | t"{dict(x='y')}" +32 |-t'{dict(x="y")}' + 32 |+t'{ {"x": "y"} }' +33 33 | t"{dict()}" +34 34 | t"a {dict()} b" +35 35 | + +C408.py:33:4: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +31 | t"{dict(x='y')}" +32 | t'{dict(x="y")}' +33 | t"{dict()}" + | ^^^^^^ C408 +34 | t"a {dict()} b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +30 30 | +31 31 | t"{dict(x='y')}" +32 32 | t'{dict(x="y")}' +33 |-t"{dict()}" + 33 |+t"{ {} }" +34 34 | t"a {dict()} b" +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" + +C408.py:34:6: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +32 | t'{dict(x="y")}' +33 | t"{dict()}" +34 | t"a {dict()} b" + | ^^^^^^ C408 +35 | +36 | t"{dict(x='y') | dict(y='z')}" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +31 31 | t"{dict(x='y')}" +32 32 | t'{dict(x="y")}' +33 33 | t"{dict()}" +34 |-t"a {dict()} b" + 34 |+t"a { {} } b" +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" + +C408.py:36:4: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +34 | t"a {dict()} b" +35 | +36 | t"{dict(x='y') | dict(y='z')}" + | ^^^^^^^^^^^ C408 +37 | t"{ dict(x='y') | dict(y='z') }" +38 | t"a {dict(x='y') | dict(y='z')} b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +33 33 | t"{dict()}" +34 34 | t"a {dict()} b" +35 35 | +36 |-t"{dict(x='y') | dict(y='z')}" + 36 |+t"{ {'x': 'y'} | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" +38 38 | t"a {dict(x='y') | dict(y='z')} b" +39 39 | t"a { dict(x='y') | dict(y='z') } b" + +C408.py:36:18: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +34 | t"a {dict()} b" +35 | +36 | t"{dict(x='y') | dict(y='z')}" + | ^^^^^^^^^^^ C408 +37 | t"{ dict(x='y') | dict(y='z') }" +38 | t"a {dict(x='y') | dict(y='z')} b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +33 33 | t"{dict()}" +34 34 | t"a {dict()} b" +35 35 | +36 |-t"{dict(x='y') | dict(y='z')}" + 36 |+t"{dict(x='y') | {'y': 'z'} }" +37 37 | t"{ dict(x='y') | dict(y='z') }" +38 38 | t"a {dict(x='y') | dict(y='z')} b" +39 39 | t"a { dict(x='y') | dict(y='z') } b" + +C408.py:37:5: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +36 | t"{dict(x='y') | dict(y='z')}" +37 | t"{ dict(x='y') | dict(y='z') }" + | ^^^^^^^^^^^ C408 +38 | t"a {dict(x='y') | dict(y='z')} b" +39 | t"a { dict(x='y') | dict(y='z') } b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +34 34 | t"a {dict()} b" +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" +37 |-t"{ dict(x='y') | dict(y='z') }" + 37 |+t"{ {'x': 'y'} | dict(y='z') }" +38 38 | t"a {dict(x='y') | dict(y='z')} b" +39 39 | t"a { dict(x='y') | dict(y='z') } b" + +C408.py:37:19: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +36 | t"{dict(x='y') | dict(y='z')}" +37 | t"{ dict(x='y') | dict(y='z') }" + | ^^^^^^^^^^^ C408 +38 | t"a {dict(x='y') | dict(y='z')} b" +39 | t"a { dict(x='y') | dict(y='z') } b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +34 34 | t"a {dict()} b" +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" +37 |-t"{ dict(x='y') | dict(y='z') }" + 37 |+t"{ dict(x='y') | {'y': 'z'} }" +38 38 | t"a {dict(x='y') | dict(y='z')} b" +39 39 | t"a { dict(x='y') | dict(y='z') } b" + +C408.py:38:6: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +36 | t"{dict(x='y') | dict(y='z')}" +37 | t"{ dict(x='y') | dict(y='z') }" +38 | t"a {dict(x='y') | dict(y='z')} b" + | ^^^^^^^^^^^ C408 +39 | t"a { dict(x='y') | dict(y='z') } b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" +38 |-t"a {dict(x='y') | dict(y='z')} b" + 38 |+t"a { {'x': 'y'} | dict(y='z')} b" +39 39 | t"a { dict(x='y') | dict(y='z') } b" + +C408.py:38:20: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +36 | t"{dict(x='y') | dict(y='z')}" +37 | t"{ dict(x='y') | dict(y='z') }" +38 | t"a {dict(x='y') | dict(y='z')} b" + | ^^^^^^^^^^^ C408 +39 | t"a { dict(x='y') | dict(y='z') } b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" +38 |-t"a {dict(x='y') | dict(y='z')} b" + 38 |+t"a {dict(x='y') | {'y': 'z'} } b" +39 39 | t"a { dict(x='y') | dict(y='z') } b" + +C408.py:39:7: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +37 | t"{ dict(x='y') | dict(y='z') }" +38 | t"a {dict(x='y') | dict(y='z')} b" +39 | t"a { dict(x='y') | dict(y='z') } b" + | ^^^^^^^^^^^ C408 + | + = help: Rewrite as a literal + +ℹ Unsafe fix +36 36 | t"{dict(x='y') | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" +38 38 | t"a {dict(x='y') | dict(y='z')} b" +39 |-t"a { dict(x='y') | dict(y='z') } b" + 39 |+t"a { {'x': 'y'} | dict(y='z') } b" + +C408.py:39:21: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +37 | t"{ dict(x='y') | dict(y='z') }" +38 | t"a {dict(x='y') | dict(y='z')} b" +39 | t"a { dict(x='y') | dict(y='z') } b" + | ^^^^^^^^^^^ C408 + | + = help: Rewrite as a literal + +ℹ Unsafe fix +36 36 | t"{dict(x='y') | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" +38 38 | t"a {dict(x='y') | dict(y='z')} b" +39 |-t"a { dict(x='y') | dict(y='z') } b" + 39 |+t"a { dict(x='y') | {'y': 'z'} } b" diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py_allow_dict_calls_with_keyword_arguments.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py_allow_dict_calls_with_keyword_arguments.snap index 17911683f1723..9e7e51df209af 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py_allow_dict_calls_with_keyword_arguments.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py_allow_dict_calls_with_keyword_arguments.snap @@ -129,6 +129,8 @@ C408.py:28:1: C408 [*] Unnecessary `tuple()` call (rewrite as a literal) 28 | / tuple( # comment 29 | | ) | |_^ C408 +30 | +31 | t"{dict(x='y')}" | = help: Rewrite as a literal @@ -139,3 +141,46 @@ C408.py:28:1: C408 [*] Unnecessary `tuple()` call (rewrite as a literal) 28 |-tuple( # comment 28 |+( # comment 29 29 | ) +30 30 | +31 31 | t"{dict(x='y')}" + +C408.py:33:4: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +31 | t"{dict(x='y')}" +32 | t'{dict(x="y")}' +33 | t"{dict()}" + | ^^^^^^ C408 +34 | t"a {dict()} b" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +30 30 | +31 31 | t"{dict(x='y')}" +32 32 | t'{dict(x="y")}' +33 |-t"{dict()}" + 33 |+t"{ {} }" +34 34 | t"a {dict()} b" +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" + +C408.py:34:6: C408 [*] Unnecessary `dict()` call (rewrite as a literal) + | +32 | t'{dict(x="y")}' +33 | t"{dict()}" +34 | t"a {dict()} b" + | ^^^^^^ C408 +35 | +36 | t"{dict(x='y') | dict(y='z')}" + | + = help: Rewrite as a literal + +ℹ Unsafe fix +31 31 | t"{dict(x='y')}" +32 32 | t'{dict(x="y")}' +33 33 | t"{dict()}" +34 |-t"a {dict()} b" + 34 |+t"a { {} } b" +35 35 | +36 36 | t"{dict(x='y') | dict(y='z')}" +37 37 | t"{ dict(x='y') | dict(y='z') }" diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417.py.snap index 6f7c3e08814fa..3a8148e299931 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C417_C417.py.snap @@ -325,3 +325,38 @@ C417.py:49:1: C417 [*] Unnecessary `map()` usage (rewrite using a generator expr 50 50 | 51 51 | # See https://github.com/astral-sh/ruff/issues/14808 52 52 | # The following should be Ok since + +C417.py:75:8: C417 [*] Unnecessary `map()` usage (rewrite using a set comprehension) + | +74 | # When inside t-string, then the fix should be surrounded by whitespace +75 | _ = t"{set(map(lambda x: x % 2 == 0, nums))}" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 +76 | _ = t"{dict(map(lambda v: (v, v**2), nums))}" + | + = help: Replace `map()` with a set comprehension + +ℹ Unsafe fix +72 72 | list(map(lambda x, y: x, [(1, 2), (3, 4)])) +73 73 | +74 74 | # When inside t-string, then the fix should be surrounded by whitespace +75 |-_ = t"{set(map(lambda x: x % 2 == 0, nums))}" + 75 |+_ = t"{ {x % 2 == 0 for x in nums} }" +76 76 | _ = t"{dict(map(lambda v: (v, v**2), nums))}" +77 77 | + +C417.py:76:8: C417 [*] Unnecessary `map()` usage (rewrite using a dict comprehension) + | +74 | # When inside t-string, then the fix should be surrounded by whitespace +75 | _ = t"{set(map(lambda x: x % 2 == 0, nums))}" +76 | _ = t"{dict(map(lambda v: (v, v**2), nums))}" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 + | + = help: Replace `map()` with a dict comprehension + +ℹ Unsafe fix +73 73 | +74 74 | # When inside t-string, then the fix should be surrounded by whitespace +75 75 | _ = t"{set(map(lambda x: x % 2 == 0, nums))}" +76 |-_ = t"{dict(map(lambda v: (v, v**2), nums))}" + 76 |+_ = t"{ {v: v**2 for v in nums} }" +77 77 | diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420.py.snap index 2a45889f360df..0914f762bffda 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420.py.snap @@ -202,3 +202,34 @@ C420.py:59:6: C420 [*] Unnecessary dict comprehension for iterable; use `dict.fr 60 60 | 61 61 | 62 62 | # Non-violation cases: RUF025 + +C420.py:95:1: C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instead + | + 94 | # https://github.com/astral-sh/ruff/issues/18764 + 95 | / { # 1 + 96 | | a # 2 + 97 | | : # 3 + 98 | | None # 4 + 99 | | for # 5 +100 | | a # 6 +101 | | in # 7 +102 | | iterable # 8 +103 | | } # 9 + | |_^ C420 + | + = help: Replace with `dict.fromkeys(iterable, value)`) + +ℹ Unsafe fix +92 92 | {(a, b): a + b for (a, b) in [(1, 2), (3, 4)]} # OK +93 93 | +94 94 | # https://github.com/astral-sh/ruff/issues/18764 +95 |-{ # 1 +96 |-a # 2 +97 |-: # 3 +98 |-None # 4 +99 |-for # 5 +100 |-a # 6 +101 |-in # 7 +102 |-iterable # 8 +103 |-} # 9 + 95 |+dict.fromkeys(iterable) # 9 diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_2.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_2.py.snap new file mode 100644 index 0000000000000..c0f88083faa31 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C420_C420_2.py.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +--- +C420_2.py:1:7: C420 [*] Unnecessary dict comprehension for iterable; use `dict.fromkeys` instead + | +1 | foo or{x: None for x in bar} + | ^^^^^^^^^^^^^^^^^^^^^^ C420 + | + = help: Replace with `dict.fromkeys(iterable, value)`) + +ℹ Safe fix +1 |-foo or{x: None for x in bar} + 1 |+foo or dict.fromkeys(bar) +2 2 | +3 3 | +4 4 | # C420 fix must make sure to insert a leading space if needed, diff --git a/crates/ruff_linter/src/rules/flake8_copyright/mod.rs b/crates/ruff_linter/src/rules/flake8_copyright/mod.rs index fc422ceddef19..41b04a6d5b4d9 100644 --- a/crates/ruff_linter/src/rules/flake8_copyright/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_copyright/mod.rs @@ -7,7 +7,7 @@ pub mod settings; mod tests { use crate::registry::Rule; use crate::test::test_snippet; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test] fn notice() { @@ -20,7 +20,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -34,7 +34,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -48,7 +48,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -62,7 +62,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -76,7 +76,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -90,7 +90,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -110,7 +110,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -130,7 +130,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -150,7 +150,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -170,7 +170,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -190,7 +190,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -210,7 +210,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -230,7 +230,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -250,7 +250,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -268,7 +268,7 @@ import os ..settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -331,7 +331,7 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test] @@ -342,6 +342,6 @@ import os .trim(), &settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]), ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } } diff --git a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs index f2a3ff49f2202..f2c578036c4f8 100644 --- a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs +++ b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::{TextRange, TextSize}; -use crate::settings::LinterSettings; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; +use crate::settings::LinterSettings; /// ## What it does /// Checks for the absence of copyright notices within Python files. @@ -32,10 +33,11 @@ impl Violation for MissingCopyrightNotice { pub(crate) fn missing_copyright_notice( locator: &Locator, settings: &LinterSettings, -) -> Option { + context: &LintContext, +) { // Ignore files that are too small to contain a copyright notice. if locator.len() < settings.flake8_copyright.min_file_size { - return None; + return; } // Only search the first 4096 bytes in the file. @@ -47,15 +49,12 @@ pub(crate) fn missing_copyright_notice( Some(ref author) => { // Ensure that it's immediately followed by the author. if contents[match_.end()..].trim_start().starts_with(author) { - return None; + return; } } - None => return None, + None => return, } } - Some(Diagnostic::new( - MissingCopyrightNotice, - TextRange::default(), - )) + context.report_diagnostic(MissingCopyrightNotice, TextRange::default()); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_datetimez/helpers.rs similarity index 100% rename from crates/ruff_linter/src/rules/flake8_datetimez/rules/helpers.rs rename to crates/ruff_linter/src/rules/flake8_datetimez/helpers.rs diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/mod.rs b/crates/ruff_linter/src/rules/flake8_datetimez/mod.rs index ef37ee0d73292..34599703fa1c5 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-datetimez](https://pypi.org/project/flake8-datetimez/). +mod helpers; pub(crate) mod rules; #[cfg(test)] @@ -10,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::CallDatetimeWithoutTzinfo, Path::new("DTZ001.py"))] #[test_case(Rule::CallDatetimeToday, Path::new("DTZ002.py"))] @@ -28,7 +29,7 @@ mod tests { Path::new("flake8_datetimez").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs index 2238f8f3464cb..97b76d57fca8b 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -58,6 +58,7 @@ impl Violation for CallDateFromtimestamp { } } +/// DTZ012 pub(crate) fn call_date_fromtimestamp(checker: &Checker, func: &Expr, location: TextRange) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -73,6 +74,6 @@ pub(crate) fn call_date_fromtimestamp(checker: &Checker, func: &Expr, location: ) }) { - checker.report_diagnostic(Diagnostic::new(CallDateFromtimestamp, location)); + checker.report_diagnostic(CallDateFromtimestamp, location); } } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs index af1f1f9797754..533d5bf0e3ce6 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -24,7 +24,7 @@ use crate::checkers::ast::Checker; /// ```python /// import datetime /// -/// datetime.datetime.today() +/// datetime.date.today() /// ``` /// /// Use instead: @@ -57,6 +57,7 @@ impl Violation for CallDateToday { } } +/// DTZ011 pub(crate) fn call_date_today(checker: &Checker, func: &Expr, location: TextRange) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -69,6 +70,6 @@ pub(crate) fn call_date_today(checker: &Checker, func: &Expr, location: TextRang matches!(qualified_name.segments(), ["datetime", "date", "today"]) }) { - checker.report_diagnostic(Diagnostic::new(CallDateToday, location)); + checker.report_diagnostic(CallDateToday, location); } } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index fa28e1ce8ee53..7e443efe74e45 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers::{self, DatetimeModuleAntipattern}; +use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## What it does /// Checks for usage of `datetime.datetime.fromtimestamp()` that do not specify @@ -69,6 +69,7 @@ impl Violation for CallDatetimeFromtimestamp { } } +/// DTZ006 pub(crate) fn call_datetime_fromtimestamp(checker: &Checker, call: &ast::ExprCall) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -97,8 +98,5 @@ pub(crate) fn call_datetime_fromtimestamp(checker: &Checker, call: &ast::ExprCal None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(Diagnostic::new( - CallDatetimeFromtimestamp(antipattern), - call.range, - )); + checker.report_diagnostic(CallDatetimeFromtimestamp(antipattern), call.range); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 566a874e1abf4..ceea7e2d843fe 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers::{self, DatetimeModuleAntipattern}; +use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## What it does /// Checks for usages of `datetime.datetime.now()` that do not specify a timezone. @@ -67,6 +67,7 @@ impl Violation for CallDatetimeNowWithoutTzinfo { } } +/// DTZ005 pub(crate) fn call_datetime_now_without_tzinfo(checker: &Checker, call: &ast::ExprCall) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -92,8 +93,5 @@ pub(crate) fn call_datetime_now_without_tzinfo(checker: &Checker, call: &ast::Ex None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(Diagnostic::new( - CallDatetimeNowWithoutTzinfo(antipattern), - call.range, - )); + checker.report_diagnostic(CallDatetimeNowWithoutTzinfo(antipattern), call.range); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index d4ef7a1203669..93e2a37b9037f 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers::DatetimeModuleAntipattern; +use crate::rules::flake8_datetimez::helpers::DatetimeModuleAntipattern; /// ## What it does /// Checks for uses of `datetime.datetime.strptime()` that lead to naive @@ -139,10 +139,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &Checker, call: &ast: semantic.current_expression_grandparent(), semantic.current_expression_parent(), ) { - checker.report_diagnostic(Diagnostic::new( - CallDatetimeStrptimeWithoutZone(antipattern), - call.range, - )); + checker.report_diagnostic(CallDatetimeStrptimeWithoutZone(antipattern), call.range); } } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs index b4ed2e27be6dd..f748b8d832a48 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -1,13 +1,13 @@ use ruff_python_ast::Expr; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers; +use crate::rules::flake8_datetimez::helpers; /// ## What it does /// Checks for usage of `datetime.datetime.today()`. @@ -56,6 +56,7 @@ impl Violation for CallDatetimeToday { } } +/// DTZ002 pub(crate) fn call_datetime_today(checker: &Checker, func: &Expr, location: TextRange) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -75,5 +76,5 @@ pub(crate) fn call_datetime_today(checker: &Checker, func: &Expr, location: Text return; } - checker.report_diagnostic(Diagnostic::new(CallDatetimeToday, location)); + checker.report_diagnostic(CallDatetimeToday, location); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index 70578afe0d292..4f6210e0266be 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -1,13 +1,13 @@ use ruff_python_ast::Expr; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers; +use crate::rules::flake8_datetimez::helpers; /// ## What it does /// Checks for usage of `datetime.datetime.utcfromtimestamp()`. @@ -60,6 +60,7 @@ impl Violation for CallDatetimeUtcfromtimestamp { } } +/// DTZ004 pub(crate) fn call_datetime_utcfromtimestamp(checker: &Checker, func: &Expr, location: TextRange) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -82,5 +83,5 @@ pub(crate) fn call_datetime_utcfromtimestamp(checker: &Checker, func: &Expr, loc return; } - checker.report_diagnostic(Diagnostic::new(CallDatetimeUtcfromtimestamp, location)); + checker.report_diagnostic(CallDatetimeUtcfromtimestamp, location); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index 8f476873b68ac..552ebbce5fb06 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -1,13 +1,13 @@ use ruff_python_ast::Expr; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers; +use crate::rules::flake8_datetimez::helpers; /// ## What it does /// Checks for usage of `datetime.datetime.utcnow()`. @@ -82,5 +82,5 @@ pub(crate) fn call_datetime_utcnow(checker: &Checker, func: &Expr, location: Tex return; } - checker.report_diagnostic(Diagnostic::new(CallDatetimeUtcnow, location)); + checker.report_diagnostic(CallDatetimeUtcnow, location); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index c2396d971bb0f..d4c53cf324ec2 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers::{self, DatetimeModuleAntipattern}; +use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## What it does /// Checks for `datetime` instantiations that do not specify a timezone. @@ -63,6 +63,7 @@ impl Violation for CallDatetimeWithoutTzinfo { } } +/// DTZ001 pub(crate) fn call_datetime_without_tzinfo(checker: &Checker, call: &ast::ExprCall) { if !checker.semantic().seen_module(Modules::DATETIME) { return; @@ -86,8 +87,5 @@ pub(crate) fn call_datetime_without_tzinfo(checker: &Checker, call: &ast::ExprCa None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(Diagnostic::new( - CallDatetimeWithoutTzinfo(antipattern), - call.range, - )); + checker.report_diagnostic(CallDatetimeWithoutTzinfo(antipattern), call.range); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs index ab4e22830e6f1..a55a91f762247 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/datetime_min_max.rs @@ -1,11 +1,11 @@ use std::fmt::{Display, Formatter}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -18,19 +18,25 @@ use crate::checkers::ast::Checker; /// unexpectedly, as in: /// /// ```python +/// import datetime +/// /// # Timezone: UTC-14 -/// datetime.min.timestamp() # ValueError: year 0 is out of range -/// datetime.max.timestamp() # ValueError: year 10000 is out of range +/// datetime.datetime.min.timestamp() # ValueError: year 0 is out of range +/// datetime.datetime.max.timestamp() # ValueError: year 10000 is out of range /// ``` /// /// ## Example /// ```python -/// datetime.max +/// import datetime +/// +/// datetime.datetime.max /// ``` /// /// Use instead: /// ```python -/// datetime.max.replace(tzinfo=datetime.UTC) +/// import datetime +/// +/// datetime.datetime.max.replace(tzinfo=datetime.UTC) /// ``` #[derive(ViolationMetadata)] pub(crate) struct DatetimeMinMax { @@ -74,7 +80,7 @@ pub(crate) fn datetime_min_max(checker: &Checker, expr: &Expr) { return; } - checker.report_diagnostic(Diagnostic::new(DatetimeMinMax { min_max }, expr.range())); + checker.report_diagnostic(DatetimeMinMax { min_max }, expr.range()); } /// Check if the current expression has the pattern `foo.replace(tzinfo=bar)` or `foo.time()`. diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/mod.rs index c119a97760dc9..346593283c029 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/mod.rs @@ -19,4 +19,3 @@ mod call_datetime_utcfromtimestamp; mod call_datetime_utcnow; mod call_datetime_without_tzinfo; mod datetime_min_max; -mod helpers; diff --git a/crates/ruff_linter/src/rules/flake8_debugger/mod.rs b/crates/ruff_linter/src/rules/flake8_debugger/mod.rs index 293e314a1eee0..56e0dcec7f299 100644 --- a/crates/ruff_linter/src/rules/flake8_debugger/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_debugger/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::Debugger, Path::new("T100.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -20,7 +20,7 @@ mod tests { Path::new("flake8_debugger").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs index b216e69f5ae0b..a068ff4387bf5 100644 --- a/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_debugger::types::DebuggerUsingType; @@ -46,6 +46,7 @@ impl Violation for Debugger { } } +/// T100 /// Checks for the presence of a debugger call. pub(crate) fn debugger_call(checker: &Checker, expr: &Expr, func: &Expr) { if let Some(using_type) = @@ -60,36 +61,36 @@ pub(crate) fn debugger_call(checker: &Checker, expr: &Expr, func: &Expr) { } }) { - checker.report_diagnostic(Diagnostic::new(Debugger { using_type }, expr.range())); + checker.report_diagnostic(Debugger { using_type }, expr.range()); } } +/// T100 /// Checks for the presence of a debugger import. -pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option { +pub(crate) fn debugger_import(checker: &Checker, stmt: &Stmt, module: Option<&str>, name: &str) { if let Some(module) = module { let qualified_name = QualifiedName::user_defined(module).append_member(name); if is_debugger_call(&qualified_name) { - return Some(Diagnostic::new( + checker.report_diagnostic( Debugger { using_type: DebuggerUsingType::Import(qualified_name.to_string()), }, stmt.range(), - )); + ); } } else { let qualified_name = QualifiedName::user_defined(name); if is_debugger_import(&qualified_name) { - return Some(Diagnostic::new( + checker.report_diagnostic( Debugger { using_type: DebuggerUsingType::Import(name.to_string()), }, stmt.range(), - )); + ); } } - None } fn is_debugger_call(qualified_name: &QualifiedName) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_django/helpers.rs b/crates/ruff_linter/src/rules/flake8_django/helpers.rs new file mode 100644 index 0000000000000..4cfedeeec93d7 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_django/helpers.rs @@ -0,0 +1,50 @@ +use ruff_python_ast::{self as ast, Expr}; + +use ruff_python_semantic::{SemanticModel, analyze}; + +/// Return `true` if a Python class appears to be a Django model, based on its base classes. +pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| { + matches!( + qualified_name.segments(), + ["django", "db", "models", "Model"] + ) + }) +} + +/// Return `true` if a Python class appears to be a Django model form, based on its base classes. +pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| { + matches!( + qualified_name.segments(), + ["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"] + ) + }) +} + +/// Return `true` if the expression is constructor for a Django model field. +pub(super) fn is_model_field(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + qualified_name + .segments() + .starts_with(&["django", "db", "models"]) + }) +} + +/// Return the name of the field type, if the expression is constructor for a Django model field. +pub(super) fn get_model_field_name<'a>( + expr: &'a Expr, + semantic: &'a SemanticModel, +) -> Option<&'a str> { + semantic + .resolve_qualified_name(expr) + .and_then(|qualified_name| { + let qualified_name = qualified_name.segments(); + if !qualified_name.starts_with(&["django", "db", "models"]) { + return None; + } + qualified_name.last().copied() + }) +} diff --git a/crates/ruff_linter/src/rules/flake8_django/mod.rs b/crates/ruff_linter/src/rules/flake8_django/mod.rs index fd577ba74f4fb..8be904a8e0981 100644 --- a/crates/ruff_linter/src/rules/flake8_django/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/mod.rs @@ -1,4 +1,5 @@ //! Rules from [django-flake8](https://pypi.org/project/flake8-django/) +mod helpers; pub(crate) mod rules; #[cfg(test)] @@ -10,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::DjangoNullableModelStringField, Path::new("DJ001.py"))] #[test_case(Rule::DjangoLocalsInRenderFunction, Path::new("DJ003.py"))] @@ -25,7 +26,7 @@ mod tests { Path::new("flake8_django").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs index 3343dcd15af0b..13a1209f84498 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::flake8_django::rules::helpers::is_model_form; +use crate::rules::flake8_django::helpers::is_model_form; /// ## What it does /// Checks for the use of `fields = "__all__"` in Django `ModelForm` @@ -78,19 +78,13 @@ pub(crate) fn all_with_model_form(checker: &Checker, class_def: &ast::StmtClassD match value.as_ref() { Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { if value == "__all__" { - checker.report_diagnostic(Diagnostic::new( - DjangoAllWithModelForm, - element.range(), - )); + checker.report_diagnostic(DjangoAllWithModelForm, element.range()); return; } } Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => { if value == "__all__".as_bytes() { - checker.report_diagnostic(Diagnostic::new( - DjangoAllWithModelForm, - element.range(), - )); + checker.report_diagnostic(DjangoAllWithModelForm, element.range()); return; } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs index 8225330146f15..4f5fe15f7a5b9 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::flake8_django::rules::helpers::is_model_form; +use crate::rules::flake8_django::helpers::is_model_form; /// ## What it does /// Checks for the use of `exclude` in Django `ModelForm` classes. @@ -71,10 +71,7 @@ pub(crate) fn exclude_with_model_form(checker: &Checker, class_def: &ast::StmtCl continue; }; if id == "exclude" { - checker.report_diagnostic(Diagnostic::new( - DjangoExcludeWithModelForm, - target.range(), - )); + checker.report_diagnostic(DjangoExcludeWithModelForm, target.range()); return; } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs deleted file mode 100644 index 67c12bbbfb605..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs +++ /dev/null @@ -1,50 +0,0 @@ -use ruff_python_ast::{self as ast, Expr}; - -use ruff_python_semantic::{analyze, SemanticModel}; - -/// Return `true` if a Python class appears to be a Django model, based on its base classes. -pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { - analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| { - matches!( - qualified_name.segments(), - ["django", "db", "models", "Model"] - ) - }) -} - -/// Return `true` if a Python class appears to be a Django model form, based on its base classes. -pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { - analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| { - matches!( - qualified_name.segments(), - ["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"] - ) - }) -} - -/// Return `true` if the expression is constructor for a Django model field. -pub(super) fn is_model_field(expr: &Expr, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(expr) - .is_some_and(|qualified_name| { - qualified_name - .segments() - .starts_with(&["django", "db", "models"]) - }) -} - -/// Return the name of the field type, if the expression is constructor for a Django model field. -pub(super) fn get_model_field_name<'a>( - expr: &'a Expr, - semantic: &'a SemanticModel, -) -> Option<&'a str> { - semantic - .resolve_qualified_name(expr) - .and_then(|qualified_name| { - let qualified_name = qualified_name.segments(); - if !qualified_name.starts_with(&["django", "db", "models"]) { - return None; - } - qualified_name.last().copied() - }) -} diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs index 3df458484dace..4f5ad2ca4218b 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -61,10 +61,7 @@ pub(crate) fn locals_in_render_function(checker: &Checker, call: &ast::ExprCall) if let Some(argument) = call.arguments.find_argument_value("context", 2) { if is_locals_call(argument, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( - DjangoLocalsInRenderFunction, - argument.range(), - )); + checker.report_diagnostic(DjangoLocalsInRenderFunction, argument.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs index 145dbe2149fbb..afdb7bdc18b81 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/mod.rs @@ -8,7 +8,6 @@ pub(crate) use unordered_body_content_in_model::*; mod all_with_model_form; mod exclude_with_model_form; -mod helpers; mod locals_in_render_function; mod model_without_dunder_str; mod non_leading_receiver_decorator; diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs index 80a208def2c98..feee5e4b7e2ca 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::{analyze, Modules, SemanticModel}; +use ruff_python_semantic::{Modules, SemanticModel, analyze}; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers; +use crate::rules::flake8_django::helpers; /// ## What it does /// Checks that a `__str__` method is defined in Django models. @@ -64,10 +64,7 @@ pub(crate) fn model_without_dunder_str(checker: &Checker, class_def: &ast::StmtC return; } - checker.report_diagnostic(Diagnostic::new( - DjangoModelWithoutDunderStr, - class_def.identifier(), - )); + checker.report_diagnostic(DjangoModelWithoutDunderStr, class_def.identifier()); } /// Returns `true` if the class has `__str__` method. diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index d5a2b51d3d603..3a854e9756c85 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Decorator; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -70,10 +70,7 @@ pub(crate) fn non_leading_receiver_decorator(checker: &Checker, decorator_list: }) }); if i > 0 && is_receiver && !seen_receiver { - checker.report_diagnostic(Diagnostic::new( - DjangoNonLeadingReceiverDecorator, - decorator.range(), - )); + checker.report_diagnostic(DjangoNonLeadingReceiverDecorator, decorator.range()); } if !is_receiver && seen_receiver { seen_receiver = false; diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs index 4ddbd80cbf689..3157e6f4fb71a 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -1,14 +1,14 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers; +use crate::rules::flake8_django::helpers; /// ## What it does /// Checks nullable string-based fields (like `CharField` and `TextField`) @@ -64,12 +64,12 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) { continue; }; if let Some(field_name) = is_nullable_field(value, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( DjangoNullableModelStringField { field_name: field_name.to_string(), }, value.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs b/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs index fa4c994c73e7c..b6492136ac8c0 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs @@ -1,15 +1,15 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers; +use crate::rules::flake8_django::helpers; /// ## What it does /// Checks for the order of Model's inner classes, methods, and fields as per @@ -75,7 +75,9 @@ impl Violation for DjangoUnorderedBodyContentInModel { element_type, prev_element_type, } = self; - format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}") + format!( + "Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}" + ) } } @@ -110,14 +112,13 @@ pub(crate) fn unordered_body_content_in_model(checker: &Checker, class_def: &ast .iter() .find(|&&prev_element_type| prev_element_type > element_type) { - let diagnostic = Diagnostic::new( + checker.report_diagnostic( DjangoUnorderedBodyContentInModel { element_type, prev_element_type, }, element.range(), ); - checker.report_diagnostic(diagnostic); } else { element_types.push(element_type); } diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/mod.rs b/crates/ruff_linter/src/rules/flake8_errmsg/mod.rs index 12cd58e650c2d..ffaba369173e2 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_errmsg/mod.rs @@ -9,8 +9,9 @@ mod tests { use anyhow::Result; use crate::registry::Rule; + use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test] fn defaults() -> Result<()> { @@ -22,7 +23,7 @@ mod tests { Rule::DotFormatInException, ]), )?; - assert_messages!("defaults", diagnostics); + assert_diagnostics!("defaults", diagnostics); Ok(()) } @@ -41,7 +42,20 @@ mod tests { ]) }, )?; - assert_messages!("custom", diagnostics); + assert_diagnostics!("custom", diagnostics); + Ok(()) + } + + #[test] + fn preview_string_exception() -> Result<()> { + let diagnostics = test_path( + Path::new("flake8_errmsg/EM101_byte_string.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + ..settings::LinterSettings::for_rule(Rule::RawStringInException) + }, + )?; + assert_diagnostics!("preview", diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs b/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs index 9c55e2fad3602..9f1a2fae3685b 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_errmsg/rules/string_in_exception.rs @@ -1,18 +1,22 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::whitespace; use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; use ruff_python_codegen::Stylist; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; +use crate::preview::is_raise_exception_byte_string_enabled; use crate::registry::Rule; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the use of string literals in exception constructors. /// +/// In [preview], this rule checks for byte string literals in +/// exception constructors. +/// /// ## Why is this bad? /// Python includes the `raise` in the default traceback (and formatters /// like Rich and IPython do too). @@ -47,6 +51,8 @@ use crate::Locator; /// raise RuntimeError(msg) /// RuntimeError: 'Some value' is incorrect /// ``` +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] pub(crate) struct RawStringInException; @@ -185,10 +191,32 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) { match first { // Check for string literals. Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => { - if checker.enabled(Rule::RawStringInException) { - if string.len() >= checker.settings.flake8_errmsg.max_string_length { + if checker.is_rule_enabled(Rule::RawStringInException) { + if string.len() >= checker.settings().flake8_errmsg.max_string_length { + let mut diagnostic = + checker.report_diagnostic(RawStringInException, first.range()); + if let Some(indentation) = + whitespace::indentation(checker.source(), stmt) + { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist(), + checker.locator(), + )); + } + } + } + } + // Check for byte string literals. + Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => { + if checker.settings().rules.enabled(Rule::RawStringInException) { + if bytes.len() >= checker.settings().flake8_errmsg.max_string_length + && is_raise_exception_byte_string_enabled(checker.settings()) + { let mut diagnostic = - Diagnostic::new(RawStringInException, first.range()); + checker.report_diagnostic(RawStringInException, first.range()); if let Some(indentation) = whitespace::indentation(checker.source(), stmt) { @@ -200,14 +228,14 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) { checker.locator(), )); } - checker.report_diagnostic(diagnostic); } } } // Check for f-strings. Expr::FString(_) => { - if checker.enabled(Rule::FStringInException) { - let mut diagnostic = Diagnostic::new(FStringInException, first.range()); + if checker.is_rule_enabled(Rule::FStringInException) { + let mut diagnostic = + checker.report_diagnostic(FStringInException, first.range()); if let Some(indentation) = whitespace::indentation(checker.source(), stmt) { diagnostic.set_fix(generate_fix( stmt, @@ -217,18 +245,17 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) { checker.locator(), )); } - checker.report_diagnostic(diagnostic); } } // Check for .format() calls. Expr::Call(ast::ExprCall { func, .. }) => { - if checker.enabled(Rule::DotFormatInException) { + if checker.is_rule_enabled(Rule::DotFormatInException) { if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { if attr == "format" && value.is_literal_expr() { let mut diagnostic = - Diagnostic::new(DotFormatInException, first.range()); + checker.report_diagnostic(DotFormatInException, first.range()); if let Some(indentation) = whitespace::indentation(checker.source(), stmt) { @@ -240,7 +267,6 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) { checker.locator(), )); } - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__preview.snap b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__preview.snap new file mode 100644 index 0000000000000..f3dbda9bfdbeb --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_errmsg/snapshots/ruff_linter__rules__flake8_errmsg__tests__preview.snap @@ -0,0 +1,35 @@ +--- +source: crates/ruff_linter/src/rules/flake8_errmsg/mod.rs +--- +EM101_byte_string.py:2:24: EM101 [*] Exception must not use a string literal, assign to variable first + | +1 | def f_byte(): +2 | raise RuntimeError(b"This is an example exception") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101 + | + = help: Assign to variable; remove string literal + +ℹ Unsafe fix +1 1 | def f_byte(): +2 |- raise RuntimeError(b"This is an example exception") + 2 |+ msg = b"This is an example exception" + 3 |+ raise RuntimeError(msg) +3 4 | +4 5 | +5 6 | def f_byte_empty(): + +EM101_byte_string.py:6:24: EM101 [*] Exception must not use a string literal, assign to variable first + | +5 | def f_byte_empty(): +6 | raise RuntimeError(b"") + | ^^^ EM101 + | + = help: Assign to variable; remove string literal + +ℹ Unsafe fix +3 3 | +4 4 | +5 5 | def f_byte_empty(): +6 |- raise RuntimeError(b"") + 6 |+ msg = b"" + 7 |+ raise RuntimeError(msg) diff --git a/crates/ruff_linter/src/rules/flake8_executable/mod.rs b/crates/ruff_linter/src/rules/flake8_executable/mod.rs index f127232890320..0e5c2d07af06b 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/mod.rs @@ -12,7 +12,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Path::new("EXE001_1.py"))] #[test_case(Path::new("EXE001_2.py"))] @@ -22,6 +22,8 @@ mod tests { #[test_case(Path::new("EXE002_3.py"))] #[test_case(Path::new("EXE003.py"))] #[test_case(Path::new("EXE003_uv.py"))] + #[test_case(Path::new("EXE003_uv_tool.py"))] + #[test_case(Path::new("EXE003_uvx.py"))] #[test_case(Path::new("EXE004_1.py"))] #[test_case(Path::new("EXE004_2.py"))] #[test_case(Path::new("EXE004_3.py"))] @@ -41,7 +43,7 @@ mod tests { Rule::ShebangMissingPython, ]), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/mod.rs index e8947b596d1ad..99d1ff31f4523 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/mod.rs @@ -1,6 +1,5 @@ use std::path::Path; -use ruff_diagnostics::Diagnostic; use ruff_python_trivia::CommentRanges; pub(crate) use shebang_leading_whitespace::*; pub(crate) use shebang_missing_executable_file::*; @@ -8,10 +7,10 @@ pub(crate) use shebang_missing_python::*; pub(crate) use shebang_not_executable::*; pub(crate) use shebang_not_first_line::*; +use crate::Locator; +use crate::checkers::ast::LintContext; use crate::codes::Rule; use crate::comments::shebang::ShebangDirective; -use crate::settings::LinterSettings; -use crate::Locator; mod shebang_leading_whitespace; mod shebang_missing_executable_file; @@ -20,11 +19,10 @@ mod shebang_not_executable; mod shebang_not_first_line; pub(crate) fn from_tokens( - diagnostics: &mut Vec, + context: &LintContext, path: &Path, locator: &Locator, comment_ranges: &CommentRanges, - settings: &LinterSettings, ) { let mut has_any_shebang = false; for range in comment_ranges { @@ -32,31 +30,21 @@ pub(crate) fn from_tokens( if let Some(shebang) = ShebangDirective::try_extract(comment) { has_any_shebang = true; - if let Some(diagnostic) = shebang_missing_python(range, &shebang) { - diagnostics.push(diagnostic); - } + shebang_missing_python(range, &shebang, context); - if settings.rules.enabled(Rule::ShebangNotExecutable) { - if let Some(diagnostic) = shebang_not_executable(path, range) { - diagnostics.push(diagnostic); - } + if context.is_rule_enabled(Rule::ShebangNotExecutable) { + shebang_not_executable(path, range, context); } - if let Some(diagnostic) = shebang_leading_whitespace(range, locator) { - diagnostics.push(diagnostic); - } + shebang_leading_whitespace(context, range, locator); - if let Some(diagnostic) = shebang_not_first_line(range, locator) { - diagnostics.push(diagnostic); - } + shebang_not_first_line(range, locator, context); } } if !has_any_shebang { - if settings.rules.enabled(Rule::ShebangMissingExecutableFile) { - if let Some(diagnostic) = shebang_missing_executable_file(path) { - diagnostics.push(diagnostic); - } + if context.is_rule_enabled(Rule::ShebangMissingExecutableFile) { + shebang_missing_executable_file(path, context); } } } diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs index a0d3bb8a64763..5fe7a072d2e4c 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_leading_whitespace.rs @@ -1,9 +1,10 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{TextRange, TextSize}; use crate::Locator; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_trivia::is_python_whitespace; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for whitespace before a shebang directive. @@ -45,12 +46,13 @@ impl AlwaysFixableViolation for ShebangLeadingWhitespace { /// EXE004 pub(crate) fn shebang_leading_whitespace( + context: &LintContext, range: TextRange, locator: &Locator, -) -> Option { +) { // If the shebang is at the beginning of the file, abort. if range.start() == TextSize::from(0) { - return None; + return; } // If the entire prefix _isn't_ whitespace, abort (this is handled by EXE005). @@ -59,11 +61,13 @@ pub(crate) fn shebang_leading_whitespace( .chars() .all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n')) { - return None; + return; } let prefix = TextRange::up_to(range.start()); - let mut diagnostic = Diagnostic::new(ShebangLeadingWhitespace, prefix); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(prefix))); - Some(diagnostic) + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(ShebangLeadingWhitespace, prefix) + { + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(prefix))); + } } diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs index 64c35c44b7417..1d13b3815f53e 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_executable_file.rs @@ -1,13 +1,9 @@ -#![allow(unused_imports)] - use std::path::Path; -use ruff_text_size::{Ranged, TextRange}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; -use crate::registry::AsRule; +use crate::Violation; +use crate::checkers::ast::LintContext; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; @@ -49,22 +45,20 @@ impl Violation for ShebangMissingExecutableFile { /// EXE002 #[cfg(target_family = "unix")] -pub(crate) fn shebang_missing_executable_file(filepath: &Path) -> Option { +pub(crate) fn shebang_missing_executable_file(filepath: &Path, context: &LintContext) { // WSL supports Windows file systems, which do not have executable bits. // Instead, everything is executable. Therefore, we skip this rule on WSL. + if is_wsl::is_wsl() { - return None; + return; } if let Ok(true) = is_executable(filepath) { - return Some(Diagnostic::new( + context.report_diagnostic_if_enabled( ShebangMissingExecutableFile, - TextRange::default(), - )); + ruff_text_size::TextRange::default(), + ); } - None } #[cfg(not(target_family = "unix"))] -pub(crate) fn shebang_missing_executable_file(_filepath: &Path) -> Option { - None -} +pub(crate) fn shebang_missing_executable_file(_filepath: &Path, _diagnostics: &LintContext) {} diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs index 8b61aea8a3eb7..13d6b13158747 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_missing_python.rs @@ -1,8 +1,9 @@ use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; +use crate::checkers::ast::LintContext; use crate::comments::shebang::ShebangDirective; /// ## What it does @@ -44,10 +45,16 @@ impl Violation for ShebangMissingPython { pub(crate) fn shebang_missing_python( range: TextRange, shebang: &ShebangDirective, -) -> Option { - if shebang.contains("python") || shebang.contains("pytest") || shebang.contains("uv run") { - return None; + context: &LintContext, +) { + if shebang.contains("python") + || shebang.contains("pytest") + || shebang.contains("uv run") + || shebang.contains("uvx") + || shebang.contains("uv tool run") + { + return; } - Some(Diagnostic::new(ShebangMissingPython, range)) + context.report_diagnostic_if_enabled(ShebangMissingPython, range); } diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs index 4769d0386f79b..afff2a41f3f2a 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -1,12 +1,10 @@ -#![allow(unused_imports)] - use std::path::Path; -use ruff_text_size::{Ranged, TextRange}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_text_size::TextRange; +use crate::Violation; +use crate::checkers::ast::LintContext; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; @@ -51,21 +49,23 @@ impl Violation for ShebangNotExecutable { /// EXE001 #[cfg(target_family = "unix")] -pub(crate) fn shebang_not_executable(filepath: &Path, range: TextRange) -> Option { +pub(crate) fn shebang_not_executable(filepath: &Path, range: TextRange, context: &LintContext) { // WSL supports Windows file systems, which do not have executable bits. // Instead, everything is executable. Therefore, we skip this rule on WSL. + if is_wsl::is_wsl() { - return None; + return; } if let Ok(false) = is_executable(filepath) { - return Some(Diagnostic::new(ShebangNotExecutable, range)); + context.report_diagnostic_if_enabled(ShebangNotExecutable, range); } - - None } #[cfg(not(target_family = "unix"))] -pub(crate) fn shebang_not_executable(_filepath: &Path, _range: TextRange) -> Option { - None +pub(crate) fn shebang_not_executable( + _filepath: &Path, + _range: TextRange, + _diagnostics: &LintContext, +) { } diff --git a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs index 6c1bb58bba566..4f5168e24ddd1 100644 --- a/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs +++ b/crates/ruff_linter/src/rules/flake8_executable/rules/shebang_not_first_line.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{TextRange, TextSize}; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; /// ## What it does /// Checks for a shebang directive that is not at the beginning of the file. @@ -42,10 +43,10 @@ impl Violation for ShebangNotFirstLine { } /// EXE005 -pub(crate) fn shebang_not_first_line(range: TextRange, locator: &Locator) -> Option { +pub(crate) fn shebang_not_first_line(range: TextRange, locator: &Locator, context: &LintContext) { // If the shebang is at the beginning of the file, abort. if range.start() == TextSize::from(0) { - return None; + return; } // If the entire prefix is whitespace, abort (this is handled by EXE004). @@ -54,8 +55,8 @@ pub(crate) fn shebang_not_first_line(range: TextRange, locator: &Locator) -> Opt .chars() .all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n')) { - return None; + return; } - Some(Diagnostic::new(ShebangNotFirstLine, range)) + context.report_diagnostic_if_enabled(ShebangNotFirstLine, range); } diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE003_uv_tool.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE003_uv_tool.py.snap new file mode 100644 index 0000000000000..26e075ae3eb6e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE003_uv_tool.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_executable/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE003_uvx.py.snap b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE003_uvx.py.snap new file mode 100644 index 0000000000000..26e075ae3eb6e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE003_uvx.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_executable/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/flake8_fixme/mod.rs b/crates/ruff_linter/src/rules/flake8_fixme/mod.rs index d2433fc8b0495..aa8ac8a379699 100644 --- a/crates/ruff_linter/src/rules/flake8_fixme/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_fixme/mod.rs @@ -9,19 +9,19 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::LineContainsFixme; "T001")] #[test_case(Rule::LineContainsHack; "T002")] #[test_case(Rule::LineContainsTodo; "T003")] #[test_case(Rule::LineContainsXxx; "T004")] fn rules(rule_code: Rule) -> Result<()> { - let snapshot = format!("{}_T00.py", rule_code.as_ref()); + let snapshot = format!("{}_T00.py", rule_code.name()); let diagnostics = test_path( Path::new("flake8_fixme/T00.py"), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs b/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs index 722ca1543e451..e17deced805ed 100644 --- a/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs +++ b/crates/ruff_linter/src/rules/flake8_fixme/rules/todos.rs @@ -1,6 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; +use crate::checkers::ast::LintContext; use crate::directives::{TodoComment, TodoDirectiveKind}; /// ## What it does @@ -114,19 +115,25 @@ impl Violation for LineContainsHack { } } -pub(crate) fn todos(diagnostics: &mut Vec, directive_ranges: &[TodoComment]) { - diagnostics.extend( - directive_ranges - .iter() - .map(|TodoComment { directive, .. }| match directive.kind { - // FIX001 - TodoDirectiveKind::Fixme => Diagnostic::new(LineContainsFixme, directive.range), - // FIX002 - TodoDirectiveKind::Hack => Diagnostic::new(LineContainsHack, directive.range), - // FIX003 - TodoDirectiveKind::Todo => Diagnostic::new(LineContainsTodo, directive.range), - // FIX004 - TodoDirectiveKind::Xxx => Diagnostic::new(LineContainsXxx, directive.range), - }), - ); +pub(crate) fn todos(context: &LintContext, directive_ranges: &[TodoComment]) { + for TodoComment { directive, .. } in directive_ranges { + match directive.kind { + // FIX001 + TodoDirectiveKind::Fixme => { + context.report_diagnostic_if_enabled(LineContainsFixme, directive.range); + } + // FIX002 + TodoDirectiveKind::Hack => { + context.report_diagnostic_if_enabled(LineContainsHack, directive.range); + } + // FIX003 + TodoDirectiveKind::Todo => { + context.report_diagnostic_if_enabled(LineContainsTodo, directive.range); + } + // FIX004 + TodoDirectiveKind::Xxx => { + context.report_diagnostic_if_enabled(LineContainsXxx, directive.range); + } + } + } } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs index d3e3a7b4eeb7e..1350b0578daaa 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs @@ -10,7 +10,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; use ruff_python_ast::PythonVersion; #[test_case(Path::new("edge_case.py"))] @@ -30,11 +30,11 @@ mod tests { let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37.into(), ..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -49,11 +49,11 @@ mod tests { let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37.into(), ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs index ec6c93b9063be..2305cd5052736 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs @@ -1,12 +1,12 @@ use std::fmt; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::{MemberNameImport, NameImport}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for uses of PEP 585- and PEP 604-style type annotations in Python @@ -85,7 +85,8 @@ impl AlwaysFixableViolation for FutureRequiredTypeAnnotation { /// FA102 pub(crate) fn future_required_type_annotation(checker: &Checker, expr: &Expr, reason: Reason) { - let mut diagnostic = Diagnostic::new(FutureRequiredTypeAnnotation { reason }, expr.range()); + let mut diagnostic = + checker.report_diagnostic(FutureRequiredTypeAnnotation { reason }, expr.range()); let required_import = NameImport::ImportFrom(MemberNameImport::member( "__future__".to_string(), "annotations".to_string(), @@ -95,5 +96,4 @@ pub(crate) fn future_required_type_annotation(checker: &Checker, expr: &Expr, re .importer() .add_import(&required_import, TextSize::default()), )); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index 591a3ea79b132..1447b05322a3a 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -1,9 +1,11 @@ +use ruff_diagnostics::Fix; use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_semantic::{MemberNameImport, NameImport}; use ruff_text_size::Ranged; +use crate::AlwaysFixableViolation; use crate::checkers::ast::Checker; /// ## What it does @@ -61,6 +63,10 @@ use crate::checkers::ast::Checker; /// def func(obj: dict[str, int | None]) -> None: ... /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe, as adding `from __future__ import annotations` +/// may change the semantics of the program. +/// /// ## Options /// - `target-version` #[derive(ViolationMetadata)] @@ -68,12 +74,16 @@ pub(crate) struct FutureRewritableTypeAnnotation { name: String, } -impl Violation for FutureRewritableTypeAnnotation { +impl AlwaysFixableViolation for FutureRewritableTypeAnnotation { #[derive_message_formats] fn message(&self) -> String { let FutureRewritableTypeAnnotation { name } = self; format!("Add `from __future__ import annotations` to simplify `{name}`") } + + fn fix_title(&self) -> String { + "Add `from __future__ import annotations`".to_string() + } } /// FA100 @@ -83,10 +93,17 @@ pub(crate) fn future_rewritable_type_annotation(checker: &Checker, expr: &Expr) .resolve_qualified_name(expr) .map(|binding| binding.to_string()); - if let Some(name) = name { - checker.report_diagnostic(Diagnostic::new( - FutureRewritableTypeAnnotation { name }, - expr.range(), + let Some(name) = name else { return }; + + let import = &NameImport::ImportFrom(MemberNameImport::member( + "__future__".to_string(), + "annotations".to_string(), + )); + checker + .report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range()) + .set_fix(Fix::unsafe_edit( + checker + .importer() + .add_import(import, ruff_text_size::TextSize::default()), )); - } } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap index c3e38a6517da5..5f668c47005a5 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap @@ -1,19 +1,32 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -edge_case.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +edge_case.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 5 | def main(_: List[int]) -> None: | ^^^^ FA100 6 | a_list: t.List[str] = [] 7 | a_list.append("hello") | + = help: Add `from __future__ import annotations` -edge_case.py:6:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import List +2 3 | import typing as t +3 4 | + +edge_case.py:6:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 5 | def main(_: List[int]) -> None: 6 | a_list: t.List[str] = [] | ^^^^^^ FA100 7 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import List +2 3 | import typing as t +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap index d15966d4d210f..9a32b4d1c9a5c 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap @@ -1,11 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +from_typing_import.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: List[str] = [] | ^^^^ FA100 6 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import List +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap index e3426dc72048a..b2ae15f0276ad 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap @@ -1,8 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +from_typing_import_many.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: List[Optional[str]] = [] @@ -10,8 +9,15 @@ from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` 6 | a_list.append("hello") 7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) | + = help: Add `from __future__ import annotations` -from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import Dict, List, Optional, Set, Union, cast +2 3 | +3 4 | + +from_typing_import_many.py:5:18: FA100 [*] Add `from __future__ import annotations` to simplify `typing.Optional` | 4 | def main() -> None: 5 | a_list: List[Optional[str]] = [] @@ -19,3 +25,10 @@ from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` 6 | a_list.append("hello") 7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import Dict, List, Optional, Set, Union, cast +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap index afa5c4aa522fa..b42449261f204 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap @@ -1,11 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -import_typing.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +import_typing.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: typing.List[str] = [] | ^^^^^^^^^^^ FA100 6 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | import typing +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap index d2ec4a38fd5b1..231038c3c4584 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap @@ -1,11 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -import_typing_as.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +import_typing_as.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: t.List[str] = [] | ^^^^^^ FA100 6 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | import typing as t +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs index 09d440526eb06..6f0eb09ea3f7f 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs @@ -23,18 +23,18 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::FStringInGetTextFuncCall, Path::new("INT001.py"))] #[test_case(Rule::FormatInGetTextFuncCall, Path::new("INT002.py"))] #[test_case(Rule::PrintfInGetTextFuncCall, Path::new("INT003.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_gettext").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 3368577ad24bc..13f3544768152 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -54,7 +54,7 @@ impl Violation for FStringInGetTextFuncCall { pub(crate) fn f_string_in_gettext_func_call(checker: &Checker, args: &[Expr]) { if let Some(first) = args.first() { if first.is_f_string_expr() { - checker.report_diagnostic(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); + checker.report_diagnostic(FStringInGetTextFuncCall {}, first.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index 8b436921b77b3..9d53d48abf3cb 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -56,10 +56,7 @@ pub(crate) fn format_in_gettext_func_call(checker: &Checker, args: &[Expr]) { if let Expr::Call(ast::ExprCall { func, .. }) = &first { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { if attr == "format" { - checker.report_diagnostic(Diagnostic::new( - FormatInGetTextFuncCall {}, - first.range(), - )); + checker.report_diagnostic(FormatInGetTextFuncCall {}, first.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 200a093265540..22c8a4b7373cc 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -1,9 +1,9 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Operator}; +use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_text_size::Ranged; /// ## What it does /// Checks for printf-style formatted strings in `gettext` function calls. @@ -60,8 +60,7 @@ pub(crate) fn printf_in_gettext_func_call(checker: &Checker, args: &[Expr]) { }) = &first { if left.is_string_literal_expr() { - checker - .report_diagnostic(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); + checker.report_diagnostic(PrintfInGetTextFuncCall {}, first.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs index dfe2cf6ed1502..2b906f3027a39 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::SingleLineImplicitStringConcatenation, Path::new("ISC.py"))] #[test_case(Rule::MultiLineImplicitStringConcatenation, Path::new("ISC.py"))] @@ -30,7 +30,7 @@ mod tests { Path::new("flake8_implicit_str_concat").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -52,7 +52,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs index 16429da3e793f..ba2a9f61a0594 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/explicit.rs @@ -1,11 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Operator}; +use ruff_python_trivia::is_python_whitespace; use ruff_source_file::LineRanges; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::settings::LinterSettings; -use crate::Locator; +use crate::AlwaysFixableViolation; +use crate::checkers::ast::Checker; +use crate::{Edit, Fix}; /// ## What it does /// Checks for string literals that are explicitly concatenated (using the @@ -34,46 +35,79 @@ use crate::Locator; #[derive(ViolationMetadata)] pub(crate) struct ExplicitStringConcatenation; -impl Violation for ExplicitStringConcatenation { +impl AlwaysFixableViolation for ExplicitStringConcatenation { #[derive_message_formats] fn message(&self) -> String { "Explicitly concatenated string should be implicitly concatenated".to_string() } + + fn fix_title(&self) -> String { + "Remove redundant '+' operator to implicitly concatenate".to_string() + } } /// ISC003 -pub(crate) fn explicit( - expr: &Expr, - locator: &Locator, - settings: &LinterSettings, -) -> Option { +pub(crate) fn explicit(checker: &Checker, expr: &Expr) { // If the user sets `allow-multiline` to `false`, then we should allow explicitly concatenated // strings that span multiple lines even if this rule is enabled. Otherwise, there's no way // for the user to write multiline strings, and that setting is "more explicit" than this rule // being enabled. - if !settings.flake8_implicit_str_concat.allow_multiline { - return None; + if !checker + .settings() + .flake8_implicit_str_concat + .allow_multiline + { + return; } - if let Expr::BinOp(ast::ExprBinOp { - left, - op, - right, - range, - }) = expr - { - if matches!(op, Operator::Add) { - if matches!( - left.as_ref(), - Expr::FString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) - ) && matches!( - right.as_ref(), - Expr::FString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) - ) && locator.contains_line_break(*range) + if let Expr::BinOp(bin_op) = expr { + if let ast::ExprBinOp { + left, + right, + op: Operator::Add, + .. + } = bin_op + { + let concatable = matches!( + (left.as_ref(), right.as_ref()), + ( + Expr::StringLiteral(_) | Expr::FString(_), + Expr::StringLiteral(_) | Expr::FString(_) + ) | (Expr::BytesLiteral(_), Expr::BytesLiteral(_)) + ); + if concatable + && checker + .locator() + .contains_line_break(TextRange::new(left.end(), right.start())) { - return Some(Diagnostic::new(ExplicitStringConcatenation, expr.range())); + checker + .report_diagnostic(ExplicitStringConcatenation, expr.range()) + .set_fix(generate_fix(checker, bin_op)); } } } - None +} + +fn generate_fix(checker: &Checker, expr_bin_op: &ast::ExprBinOp) -> Fix { + let ast::ExprBinOp { left, right, .. } = expr_bin_op; + + let between_operands_range = TextRange::new(left.end(), right.start()); + let between_operands = checker.locator().slice(between_operands_range); + let (before_plus, after_plus) = between_operands.split_once('+').unwrap(); + + let linebreak_before_operator = + before_plus.contains_line_break(TextRange::at(TextSize::new(0), before_plus.text_len())); + + // If removing `+` from first line trim trailing spaces + // Preserve indentation when removing `+` from second line + let before_plus = if linebreak_before_operator { + before_plus + } else { + before_plus.trim_end_matches(is_python_whitespace) + }; + + Fix::safe_edit(Edit::range_replacement( + format!("{before_plus}{after_plus}"), + between_operands_range, + )) } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 20d2e23e076df..491f25da5d163 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -2,16 +2,17 @@ use std::borrow::Cow; use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::str::{leading_quote, trailing_quote}; use ruff_python_index::Indexer; use ruff_python_parser::{TokenKind, Tokens}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; -use crate::settings::LinterSettings; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::settings::LinterSettings; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for implicitly concatenated strings on a single line. @@ -103,7 +104,7 @@ impl Violation for MultiLineImplicitStringConcatenation { /// ISC001, ISC002 pub(crate) fn implicit( - diagnostics: &mut Vec, + context: &LintContext, tokens: &Tokens, locator: &Locator, indexer: &Indexer, @@ -145,21 +146,19 @@ pub(crate) fn implicit( }; if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) { - diagnostics.push(Diagnostic::new( + context.report_diagnostic_if_enabled( MultiLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), - )); + ); } else { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( SingleLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), - ); - - if let Some(fix) = concatenate_strings(a_range, b_range, locator) { - diagnostic.set_fix(fix); + ) { + if let Some(fix) = concatenate_strings(a_range, b_range, locator) { + diagnostic.set_fix(fix); + } } - - diagnostics.push(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap index 73d16d6ebd2b2..e6cfb14078d4b 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap @@ -461,6 +461,7 @@ ISC.py:91:5: ISC001 [*] Implicitly concatenated string literals on one line 91 |+_ = "\128" # fix should be "\128" 92 92 | _ = "\12""foo" # fix should be "\12foo" 93 93 | _ = "\12" "" # fix should be "\12" +94 94 | ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line | @@ -479,6 +480,8 @@ ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line 92 |-_ = "\12""foo" # fix should be "\12foo" 92 |+_ = "\12foo" # fix should be "\12foo" 93 93 | _ = "\12" "" # fix should be "\12" +94 94 | +95 95 | ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line | @@ -495,3 +498,6 @@ ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line 92 92 | _ = "\12""foo" # fix should be "\12foo" 93 |-_ = "\12" "" # fix should be "\12" 93 |+_ = "\12" # fix should be "\12" +94 94 | +95 95 | +96 96 | # Mixed literal + non-literal scenarios diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error.py.snap index a14e63a629ca2..0dab8744b5613 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC001_ISC_syntax_error.py.snap @@ -5,7 +5,7 @@ ISC_syntax_error.py:2:5: SyntaxError: missing closing quote in string literal | 1 | # The lexer doesn't emit a string token if it's unterminated 2 | "a" "b - | ^ + | ^^ 3 | "a" "b" "c 4 | "a" """b | @@ -36,7 +36,7 @@ ISC_syntax_error.py:3:9: SyntaxError: missing closing quote in string literal 1 | # The lexer doesn't emit a string token if it's unterminated 2 | "a" "b 3 | "a" "b" "c - | ^ + | ^^ 4 | "a" """b 5 | c""" "d | @@ -68,7 +68,7 @@ ISC_syntax_error.py:5:6: SyntaxError: missing closing quote in string literal 3 | "a" "b" "c 4 | "a" """b 5 | c""" "d - | ^ + | ^^ 6 | 7 | # For f-strings, the `FStringRanges` won't contain the range for | @@ -153,19 +153,21 @@ ISC_syntax_error.py:16:5: SyntaxError: missing closing quote in string literal 14 | ( 15 | "a" 16 | "b - | ^ + | ^^ 17 | "c" 18 | "d" | ISC_syntax_error.py:26:9: SyntaxError: f-string: unterminated triple-quoted string | -24 | ( -25 | """abc""" -26 | f"""def - | ^ -27 | "g" "h" -28 | "i" "j" +24 | ( +25 | """abc""" +26 | f"""def + | _________^ +27 | | "g" "h" +28 | | "i" "j" +29 | | ) + | |__^ | ISC_syntax_error.py:30:1: SyntaxError: unexpected EOF while parsing diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error.py.snap index 46d99bae675e8..99f0f7d157781 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC002_ISC_syntax_error.py.snap @@ -5,7 +5,7 @@ ISC_syntax_error.py:2:5: SyntaxError: missing closing quote in string literal | 1 | # The lexer doesn't emit a string token if it's unterminated 2 | "a" "b - | ^ + | ^^ 3 | "a" "b" "c 4 | "a" """b | @@ -25,7 +25,7 @@ ISC_syntax_error.py:3:9: SyntaxError: missing closing quote in string literal 1 | # The lexer doesn't emit a string token if it's unterminated 2 | "a" "b 3 | "a" "b" "c - | ^ + | ^^ 4 | "a" """b 5 | c""" "d | @@ -45,7 +45,7 @@ ISC_syntax_error.py:5:6: SyntaxError: missing closing quote in string literal 3 | "a" "b" "c 4 | "a" """b 5 | c""" "d - | ^ + | ^^ 6 | 7 | # For f-strings, the `FStringRanges` won't contain the range for | @@ -107,19 +107,21 @@ ISC_syntax_error.py:16:5: SyntaxError: missing closing quote in string literal 14 | ( 15 | "a" 16 | "b - | ^ + | ^^ 17 | "c" 18 | "d" | ISC_syntax_error.py:26:9: SyntaxError: f-string: unterminated triple-quoted string | -24 | ( -25 | """abc""" -26 | f"""def - | ^ -27 | "g" "h" -28 | "i" "j" +24 | ( +25 | """abc""" +26 | f"""def + | _________^ +27 | | "g" "h" +28 | | "i" "j" +29 | | ) + | |__^ | ISC_syntax_error.py:30:1: SyntaxError: unexpected EOF while parsing diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap index dccaf146a2f4f..6ce1e6eee6089 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC003_ISC.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:9:3: ISC003 Explicitly concatenated string should be implicitly concatenated +ISC.py:9:3: ISC003 [*] Explicitly concatenated string should be implicitly concatenated | 8 | _ = ( 9 | / "abc" + @@ -9,8 +9,19 @@ ISC.py:9:3: ISC003 Explicitly concatenated string should be implicitly concatena | |_______^ ISC003 11 | ) | + = help: Remove redundant '+' operator to implicitly concatenate -ISC.py:14:3: ISC003 Explicitly concatenated string should be implicitly concatenated +ℹ Safe fix +6 6 | "def" +7 7 | +8 8 | _ = ( +9 |- "abc" + + 9 |+ "abc" +10 10 | "def" +11 11 | ) +12 12 | + +ISC.py:14:3: ISC003 [*] Explicitly concatenated string should be implicitly concatenated | 13 | _ = ( 14 | / f"abc" + @@ -18,8 +29,19 @@ ISC.py:14:3: ISC003 Explicitly concatenated string should be implicitly concaten | |_______^ ISC003 16 | ) | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +11 11 | ) +12 12 | +13 13 | _ = ( +14 |- f"abc" + + 14 |+ f"abc" +15 15 | "def" +16 16 | ) +17 17 | -ISC.py:19:3: ISC003 Explicitly concatenated string should be implicitly concatenated +ISC.py:19:3: ISC003 [*] Explicitly concatenated string should be implicitly concatenated | 18 | _ = ( 19 | / b"abc" + @@ -27,8 +49,19 @@ ISC.py:19:3: ISC003 Explicitly concatenated string should be implicitly concaten | |________^ ISC003 21 | ) | + = help: Remove redundant '+' operator to implicitly concatenate -ISC.py:78:10: ISC003 Explicitly concatenated string should be implicitly concatenated +ℹ Safe fix +16 16 | ) +17 17 | +18 18 | _ = ( +19 |- b"abc" + + 19 |+ b"abc" +20 20 | b"def" +21 21 | ) +22 22 | + +ISC.py:78:10: ISC003 [*] Explicitly concatenated string should be implicitly concatenated | 77 | # Explicitly concatenated nested f-strings 78 | _ = f"a {f"first" @@ -38,8 +71,19 @@ ISC.py:78:10: ISC003 Explicitly concatenated string should be implicitly concate 80 | _ = f"a {f"first {f"middle"}" 81 | + f"second"} d" | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +76 76 | +77 77 | # Explicitly concatenated nested f-strings +78 78 | _ = f"a {f"first" +79 |- + f"second"} d" + 79 |+ f"second"} d" +80 80 | _ = f"a {f"first {f"middle"}" +81 81 | + f"second"} d" +82 82 | -ISC.py:80:10: ISC003 Explicitly concatenated string should be implicitly concatenated +ISC.py:80:10: ISC003 [*] Explicitly concatenated string should be implicitly concatenated | 78 | _ = f"a {f"first" 79 | + f"second"} d" @@ -50,3 +94,263 @@ ISC.py:80:10: ISC003 Explicitly concatenated string should be implicitly concate 82 | 83 | # See https://github.com/astral-sh/ruff/issues/12936 | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +78 78 | _ = f"a {f"first" +79 79 | + f"second"} d" +80 80 | _ = f"a {f"first {f"middle"}" +81 |- + f"second"} d" + 81 |+ f"second"} d" +82 82 | +83 83 | # See https://github.com/astral-sh/ruff/issues/12936 +84 84 | _ = "\12""0" # fix should be "\0120" + +ISC.py:110:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +109 | _ = ( +110 | / rf"raw_f{x}" + +111 | | r"raw_normal" + | |_________________^ ISC003 +112 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +107 107 | ) +108 108 | +109 109 | _ = ( +110 |- rf"raw_f{x}" + + 110 |+ rf"raw_f{x}" +111 111 | r"raw_normal" +112 112 | ) +113 113 | + +ISC.py:117:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +115 | # Different prefix combinations +116 | _ = ( +117 | / u"unicode" + +118 | | r"raw" + | |__________^ ISC003 +119 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +114 114 | +115 115 | # Different prefix combinations +116 116 | _ = ( +117 |- u"unicode" + + 117 |+ u"unicode" +118 118 | r"raw" +119 119 | ) +120 120 | + +ISC.py:122:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +121 | _ = ( +122 | / rb"raw_bytes" + +123 | | b"normal_bytes" + | |___________________^ ISC003 +124 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +119 119 | ) +120 120 | +121 121 | _ = ( +122 |- rb"raw_bytes" + + 122 |+ rb"raw_bytes" +123 123 | b"normal_bytes" +124 124 | ) +125 125 | + +ISC.py:127:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +126 | _ = ( +127 | / b"bytes" + +128 | | b"with_bytes" + | |_________________^ ISC003 +129 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +124 124 | ) +125 125 | +126 126 | _ = ( +127 |- b"bytes" + + 127 |+ b"bytes" +128 128 | b"with_bytes" +129 129 | ) +130 130 | + +ISC.py:133:6: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +131 | # Repeated concatenation +132 | +133 | _ = ("a" + + | ______^ +134 | | "b" + + | |_______^ ISC003 +135 | "c" + +136 | "d" + "e" + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +130 130 | +131 131 | # Repeated concatenation +132 132 | +133 |-_ = ("a" + + 133 |+_ = ("a" +134 134 | "b" + +135 135 | "c" + +136 136 | "d" + "e" + +ISC.py:139:6: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +137 | ) +138 | +139 | _ = ("a" + | ______^ +140 | | + "b" + | |_________^ ISC003 +141 | + "c" +142 | + "d" + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +137 137 | ) +138 138 | +139 139 | _ = ("a" +140 |- + "b" + 140 |+ "b" +141 141 | + "c" +142 142 | + "d" +143 143 | + "e" + +ISC.py:160:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +159 | _ = ( +160 | / "first" +161 | | + "second" # extra spaces around + + | |_________________^ ISC003 +162 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +158 158 | +159 159 | _ = ( +160 160 | "first" +161 |- + "second" # extra spaces around + + 161 |+ "second" # extra spaces around + +162 162 | ) +163 163 | +164 164 | _ = ( + +ISC.py:165:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +164 | _ = ( +165 | / "first" + # trailing spaces before + +166 | | "second" + | |____________^ ISC003 +167 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +162 162 | ) +163 163 | +164 164 | _ = ( +165 |- "first" + # trailing spaces before + + 165 |+ "first" # trailing spaces before + +166 166 | "second" +167 167 | ) +168 168 | + +ISC.py:170:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +169 | _ = (( +170 | / "deep" + +171 | | "nesting" + | |_____________^ ISC003 +172 | )) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +167 167 | ) +168 168 | +169 169 | _ = (( +170 |- "deep" + + 170 |+ "deep" +171 171 | "nesting" +172 172 | )) +173 173 | + +ISC.py:175:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +174 | _ = ( +175 | / "contains + plus" + +176 | | "another string" + | |____________________^ ISC003 +177 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +172 172 | )) +173 173 | +174 174 | _ = ( +175 |- "contains + plus" + + 175 |+ "contains + plus" +176 176 | "another string" +177 177 | ) +178 178 | + +ISC.py:180:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +179 | _ = ( +180 | / "start" +181 | | # leading comment +182 | | + "end" + | |___________^ ISC003 +183 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +179 179 | _ = ( +180 180 | "start" +181 181 | # leading comment +182 |- + "end" + 182 |+ "end" +183 183 | ) +184 184 | +185 185 | _ = ( + +ISC.py:186:5: ISC003 [*] Explicitly concatenated string should be implicitly concatenated + | +185 | _ = ( +186 | / "start" + +187 | | # leading comment +188 | | "end" + | |_________^ ISC003 +189 | ) + | + = help: Remove redundant '+' operator to implicitly concatenate + +ℹ Safe fix +183 183 | ) +184 184 | +185 185 | _ = ( +186 |- "start" + + 186 |+ "start" +187 187 | # leading comment +188 188 | "end" +189 189 | ) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap index 73d16d6ebd2b2..e6cfb14078d4b 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap @@ -461,6 +461,7 @@ ISC.py:91:5: ISC001 [*] Implicitly concatenated string literals on one line 91 |+_ = "\128" # fix should be "\128" 92 92 | _ = "\12""foo" # fix should be "\12foo" 93 93 | _ = "\12" "" # fix should be "\12" +94 94 | ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line | @@ -479,6 +480,8 @@ ISC.py:92:5: ISC001 [*] Implicitly concatenated string literals on one line 92 |-_ = "\12""foo" # fix should be "\12foo" 92 |+_ = "\12foo" # fix should be "\12foo" 93 93 | _ = "\12" "" # fix should be "\12" +94 94 | +95 95 | ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line | @@ -495,3 +498,6 @@ ISC.py:93:5: ISC001 [*] Implicitly concatenated string literals on one line 92 92 | _ = "\12""foo" # fix should be "\12foo" 93 |-_ = "\12" "" # fix should be "\12" 93 |+_ = "\12" # fix should be "\12" +94 94 | +95 95 | +96 96 | # Mixed literal + non-literal scenarios diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs index de5d8a89155a8..386e35035cc38 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs @@ -9,9 +9,9 @@ mod tests { use anyhow::Result; use rustc_hash::{FxHashMap, FxHashSet}; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; - use crate::rules::flake8_import_conventions::settings::{default_aliases, BannedAliases}; + use crate::rules::flake8_import_conventions::settings::{BannedAliases, default_aliases}; use crate::settings::LinterSettings; use crate::test::test_path; @@ -21,7 +21,7 @@ mod tests { Path::new("flake8_import_conventions/defaults.py"), &LinterSettings::for_rule(Rule::UnconventionalImportAlias), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -43,7 +43,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnconventionalImportAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -77,7 +77,7 @@ mod tests { ..LinterSettings::for_rule(Rule::BannedImportAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -98,7 +98,7 @@ mod tests { ..LinterSettings::for_rule(Rule::BannedImportFrom) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -120,7 +120,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnconventionalImportAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -143,7 +143,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnconventionalImportAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -169,7 +169,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnconventionalImportAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -179,7 +179,7 @@ mod tests { Path::new("flake8_import_conventions/tricky.py"), &LinterSettings::for_rule(Rule::UnconventionalImportAlias), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -201,7 +201,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnconventionalImportAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs index 7f6788fa25db7..6fc205699af0c 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_alias.rs @@ -1,10 +1,11 @@ use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::flake8_import_conventions::settings::BannedAliases; /// ## What it does @@ -48,24 +49,24 @@ impl Violation for BannedImportAlias { /// ICN002 pub(crate) fn banned_import_alias( + checker: &Checker, stmt: &Stmt, name: &str, asname: &str, banned_conventions: &FxHashMap, -) -> Option { +) { if let Some(banned_aliases) = banned_conventions.get(name) { if banned_aliases .iter() .any(|banned_alias| banned_alias == asname) { - return Some(Diagnostic::new( + checker.report_diagnostic( BannedImportAlias { name: name.to_string(), asname: asname.to_string(), }, stmt.range(), - )); + ); } } - None } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs index 2fa407d54e678..c5a64ea82a2cd 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/banned_import_from.rs @@ -1,10 +1,11 @@ -use ruff_python_ast::Stmt; use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::Stmt; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for member imports that should instead be accessed by importing the /// module. @@ -46,17 +47,17 @@ impl Violation for BannedImportFrom { /// ICN003 pub(crate) fn banned_import_from( + checker: &Checker, stmt: &Stmt, name: &str, banned_conventions: &FxHashSet, -) -> Option { +) { if banned_conventions.contains(name) { - return Some(Diagnostic::new( + checker.report_diagnostic( BannedImportFrom { name: name.to_string(), }, stmt.range(), - )); + ); } - None } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index bdd50e33974be..90970ae343807 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -1,11 +1,11 @@ use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::{Binding, Imported}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Fix, FixAvailability, Violation}; use crate::renamer::Renamer; @@ -60,17 +60,21 @@ pub(crate) fn unconventional_import_alias( checker: &Checker, binding: &Binding, conventions: &FxHashMap, -) -> Option { - let import = binding.as_any_import()?; +) { + let Some(import) = binding.as_any_import() else { + return; + }; let qualified_name = import.qualified_name().to_string(); - let expected_alias = conventions.get(qualified_name.as_str())?; + let Some(expected_alias) = conventions.get(qualified_name.as_str()) else { + return; + }; let name = binding.name(checker.source()); if name == expected_alias { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnconventionalImportAlias { name: qualified_name, asname: expected_alias.to_string(), @@ -92,5 +96,4 @@ pub(crate) fn unconventional_import_alias( }); } } - Some(diagnostic) } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_logging/helpers.rs similarity index 100% rename from crates/ruff_linter/src/rules/flake8_logging/rules/helpers.rs rename to crates/ruff_linter/src/rules/flake8_logging/helpers.rs diff --git a/crates/ruff_linter/src/rules/flake8_logging/mod.rs b/crates/ruff_linter/src/rules/flake8_logging/mod.rs index 0b16662f33e5a..a4ec5974c36a3 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-logging](https://pypi.org/project/flake8-logging/). +mod helpers; pub(crate) mod rules; #[cfg(test)] @@ -8,7 +9,7 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::test::test_path; @@ -28,7 +29,7 @@ mod tests { Path::new("flake8_logging").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs index 8b9f4a1d69206..501953d980d11 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for direct instantiation of `logging.Logger`, as opposed to using @@ -36,6 +36,10 @@ use crate::importer::ImportRequest; /// logger = logging.getLogger(__name__) /// ``` /// +/// ## Fix safety +/// This fix is always unsafe, as changing from `Logger` to `getLogger` +/// changes program behavior by will adding the logger to the logging tree. +/// /// [Logger Objects]: https://docs.python.org/3/library/logging.html#logger-objects #[derive(ViolationMetadata)] pub(crate) struct DirectLoggerInstantiation; @@ -64,7 +68,8 @@ pub(crate) fn direct_logger_instantiation(checker: &Checker, call: &ast::ExprCal .resolve_qualified_name(call.func.as_ref()) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["logging", "Logger"])) { - let mut diagnostic = Diagnostic::new(DirectLoggerInstantiation, call.func.range()); + let mut diagnostic = + checker.report_diagnostic(DirectLoggerInstantiation, call.func.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("logging", "getLogger"), @@ -74,6 +79,5 @@ pub(crate) fn direct_logger_instantiation(checker: &Checker, call: &ast::ExprCal let reference_edit = Edit::range_replacement(binding, call.func.range()); Ok(Fix::unsafe_edits(import_edit, [reference_edit])) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs index 019c4fe42b730..d1768b42541a2 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exc_info_outside_except_handler.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::{Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::analyze::logging::is_logger_candidate; @@ -7,8 +6,9 @@ use ruff_python_stdlib::logging::LoggingLevel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; -use crate::rules::flake8_logging::rules::helpers::outside_handlers; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::rules::flake8_logging::helpers::outside_handlers; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for logging calls with `exc_info=` outside exception handlers. @@ -59,6 +59,7 @@ impl Violation for ExcInfoOutsideExceptHandler { } } +/// LOG014 pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall) { let semantic = checker.semantic(); @@ -68,7 +69,7 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall match &*call.func { func @ Expr::Attribute(ExprAttribute { attr, .. }) => { - if !is_logger_candidate(func, semantic, &checker.settings.logger_objects) { + if !is_logger_candidate(func, semantic, &checker.settings().logger_objects) { return; } @@ -98,6 +99,10 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall return; }; + if !exc_info.value.is_literal_expr() { + return; + } + let truthiness = Truthiness::from_expr(&exc_info.value, |id| semantic.has_builtin_binding(id)); if truthiness.into_bool() != Some(true) { @@ -107,12 +112,16 @@ pub(crate) fn exc_info_outside_except_handler(checker: &Checker, call: &ExprCall let arguments = &call.arguments; let source = checker.source(); - let mut diagnostic = Diagnostic::new(ExcInfoOutsideExceptHandler, exc_info.range); + let mut diagnostic = checker.report_diagnostic(ExcInfoOutsideExceptHandler, exc_info.range); diagnostic.try_set_fix(|| { - let edit = remove_argument(exc_info, arguments, Parentheses::Preserve, source)?; + let edit = remove_argument( + exc_info, + arguments, + Parentheses::Preserve, + source, + checker.comment_ranges(), + )?; Ok(Fix::unsafe_edit(edit)) }); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs index c32a9a5fade94..4724239d56913 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::{self as ast, Expr, ExprCall}; use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -54,7 +54,7 @@ pub(crate) fn exception_without_exc_info(checker: &Checker, call: &ExprCall) { if !logging::is_logger_candidate( &call.func, checker.semantic(), - &checker.settings.logger_objects, + &checker.settings().logger_objects, ) { return; } @@ -74,7 +74,7 @@ pub(crate) fn exception_without_exc_info(checker: &Checker, call: &ExprCall) { } if exc_info_arg_is_falsey(call, checker) { - checker.report_diagnostic(Diagnostic::new(ExceptionWithoutExcInfo, call.range())); + checker.report_diagnostic(ExceptionWithoutExcInfo, call.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs index 60d1d9b69cfe1..368bdfa5d069a 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for any usage of `__cached__` and `__file__` as an argument to @@ -39,6 +39,10 @@ use crate::checkers::ast::Checker; /// logger = logging.getLogger(__name__) /// ``` /// +/// ## Fix safety +/// This fix is always unsafe, as changing the arguments to `getLogger` can change the +/// received logger object, and thus program behavior. +/// /// [logging documentation]: https://docs.python.org/3/library/logging.html#logger-objects #[derive(ViolationMetadata)] pub(crate) struct InvalidGetLoggerArgument; @@ -84,12 +88,11 @@ pub(crate) fn invalid_get_logger_argument(checker: &Checker, call: &ast::ExprCal return; } - let mut diagnostic = Diagnostic::new(InvalidGetLoggerArgument, expr.range()); + let mut diagnostic = checker.report_diagnostic(InvalidGetLoggerArgument, expr.range()); if checker.semantic().has_builtin_binding("__name__") { diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( "__name__".to_string(), expr.range(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs index 4da3294353779..00c099014ed90 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::analyze::logging; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::rules::flake8_logging::rules::helpers::outside_handlers; +use crate::rules::flake8_logging::helpers::outside_handlers; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `.exception()` logging calls outside of exception handlers. @@ -69,7 +69,7 @@ pub(crate) fn log_exception_outside_except_handler(checker: &Checker, call: &Exp let fix = match &*call.func { func @ Expr::Attribute(ExprAttribute { attr, .. }) => { - let logger_objects = &checker.settings.logger_objects; + let logger_objects = &checker.settings().logger_objects; if !logging::is_logger_candidate(func, semantic, logger_objects) { return; @@ -99,11 +99,9 @@ pub(crate) fn log_exception_outside_except_handler(checker: &Checker, call: &Exp _ => return, }; - let mut diagnostic = Diagnostic::new(LogExceptionOutsideExceptHandler, call.range); + let mut diagnostic = checker.report_diagnostic(LogExceptionOutsideExceptHandler, call.range); if let Some(fix) = fix { diagnostic.set_fix(fix); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/mod.rs index 0d0b5e075a818..48f0748c5d821 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/mod.rs @@ -9,7 +9,6 @@ pub(crate) use undocumented_warn::*; mod direct_logger_instantiation; mod exc_info_outside_except_handler; mod exception_without_exc_info; -mod helpers; mod invalid_get_logger_argument; mod log_exception_outside_except_handler; mod root_logger_call; diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs index 8bd3dc8359e41..6c174b39e3e46 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_python_semantic::Modules; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -63,9 +63,7 @@ pub(crate) fn root_logger_call(checker: &Checker, call: &ExprCall) { let kind = RootLoggerCall { attr: (*attr).to_string(), }; - let diagnostic = Diagnostic::new(kind, call.range); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(kind, call.range); } #[inline] diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs index 444d53d1f69d3..2287a217b3357 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs @@ -1,12 +1,12 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `logging.WARN`. @@ -59,7 +59,7 @@ pub(crate) fn undocumented_warn(checker: &Checker, expr: &Expr) { .resolve_qualified_name(expr) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["logging", "WARN"])) { - let mut diagnostic = Diagnostic::new(UndocumentedWarn, expr.range()); + let mut diagnostic = checker.report_diagnostic(UndocumentedWarn, expr.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("logging", "WARNING"), @@ -69,6 +69,5 @@ pub(crate) fn undocumented_warn(checker: &Checker, expr: &Expr) { let reference_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::safe_edits(import_edit, [reference_edit])) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs b/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs index 997a72e9ed376..1dc2b536a629f 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Path::new("G_argparse_parser_error_ok.py"))] #[test_case(Path::new("G_extra_ok.py"))] @@ -45,7 +45,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs index a053b4514fb12..33727411543c2 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs @@ -1,4 +1,3 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator}; use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; @@ -10,6 +9,7 @@ use crate::rules::flake8_logging_format::violations::{ LoggingExcInfo, LoggingExtraAttrClash, LoggingFString, LoggingPercentFormat, LoggingRedundantExcInfo, LoggingStringConcat, LoggingStringFormat, LoggingWarn, }; +use crate::{Edit, Fix}; /// Returns `true` if the attribute is a reserved attribute on the `logging` module's `LogRecord` /// class. @@ -47,30 +47,23 @@ fn check_msg(checker: &Checker, msg: &Expr) { // Check for string concatenation and percent format. Expr::BinOp(ast::ExprBinOp { op, .. }) => match op { Operator::Add => { - if checker.enabled(Rule::LoggingStringConcat) { - checker.report_diagnostic(Diagnostic::new(LoggingStringConcat, msg.range())); - } + checker.report_diagnostic_if_enabled(LoggingStringConcat, msg.range()); } Operator::Mod => { - if checker.enabled(Rule::LoggingPercentFormat) { - checker.report_diagnostic(Diagnostic::new(LoggingPercentFormat, msg.range())); - } + checker.report_diagnostic_if_enabled(LoggingPercentFormat, msg.range()); } _ => {} }, // Check for f-strings. Expr::FString(_) => { - if checker.enabled(Rule::LoggingFString) { - checker.report_diagnostic(Diagnostic::new(LoggingFString, msg.range())); - } + checker.report_diagnostic_if_enabled(LoggingFString, msg.range()); } // Check for .format() calls. Expr::Call(ast::ExprCall { func, .. }) => { - if checker.enabled(Rule::LoggingStringFormat) { + if checker.is_rule_enabled(Rule::LoggingStringFormat) { if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { if attr == "format" && value.is_literal_expr() { - checker - .report_diagnostic(Diagnostic::new(LoggingStringFormat, msg.range())); + checker.report_diagnostic(LoggingStringFormat, msg.range()); } } } @@ -91,10 +84,10 @@ fn check_log_record_attr_clash(checker: &Checker, extra: &Keyword) { None } }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( LoggingExtraAttrClash(invalid_key.value.to_string()), invalid_key.range(), - )); + ); } } Expr::Call(ast::ExprCall { @@ -106,10 +99,10 @@ fn check_log_record_attr_clash(checker: &Checker, extra: &Keyword) { for keyword in keywords { if let Some(attr) = &keyword.arg { if is_reserved_attr(attr) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( LoggingExtraAttrClash(attr.to_string()), keyword.range(), - )); + ); } } } @@ -148,7 +141,7 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) { if !logging::is_logger_candidate( &call.func, checker.semantic(), - &checker.settings.logger_objects, + &checker.settings().logger_objects, ) { return; } @@ -172,36 +165,35 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) { _ => return, }; - // G001 - G004 + // G001, G002, G003, G004 let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall)); if let Some(format_arg) = call.arguments.find_argument_value("msg", msg_pos) { check_msg(checker, format_arg); } // G010 - if checker.enabled(Rule::LoggingWarn) { + if checker.is_rule_enabled(Rule::LoggingWarn) { if matches!( logging_call_type, LoggingCallType::LevelCall(LoggingLevel::Warn) ) { - let mut diagnostic = Diagnostic::new(LoggingWarn, range); + let mut diagnostic = checker.report_diagnostic(LoggingWarn, range); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "warning".to_string(), range, ))); - checker.report_diagnostic(diagnostic); } } // G101 - if checker.enabled(Rule::LoggingExtraAttrClash) { + if checker.is_rule_enabled(Rule::LoggingExtraAttrClash) { if let Some(extra) = call.arguments.find_keyword("extra") { check_log_record_attr_clash(checker, extra); } } // G201, G202 - if checker.any_enabled(&[Rule::LoggingExcInfo, Rule::LoggingRedundantExcInfo]) { + if checker.any_rule_enabled(&[Rule::LoggingExcInfo, Rule::LoggingRedundantExcInfo]) { if !checker.semantic().in_exception_handler() { return; } @@ -211,17 +203,10 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) { if let LoggingCallType::LevelCall(logging_level) = logging_call_type { match logging_level { LoggingLevel::Error => { - if checker.enabled(Rule::LoggingExcInfo) { - checker.report_diagnostic(Diagnostic::new(LoggingExcInfo, range)); - } + checker.report_diagnostic_if_enabled(LoggingExcInfo, range); } LoggingLevel::Exception => { - if checker.enabled(Rule::LoggingRedundantExcInfo) { - checker.report_diagnostic(Diagnostic::new( - LoggingRedundantExcInfo, - exc_info.range(), - )); - } + checker.report_diagnostic_if_enabled(LoggingRedundantExcInfo, exc_info.range()); } _ => {} } diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs b/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs index ad6c399afa6ad..51cc9f436ff6e 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::{AlwaysFixableViolation, Violation}; /// ## What it does /// Checks for uses of `str.format` to format logging messages. diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/mod.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/mod.rs index f02022737ea4d..a05ec60dfff67 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/mod.rs @@ -10,7 +10,7 @@ mod tests { use crate::registry::Rule; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::settings::LinterSettings; use crate::test::{test_path, test_resource_path}; @@ -39,7 +39,9 @@ mod tests { ..LinterSettings::for_rule(Rule::ImplicitNamespacePackage) }, )?; - assert_messages!(snapshot, diagnostics); + insta::with_settings!({filters => vec![(r"\\", "/")]}, { + assert_diagnostics!(snapshot, diagnostics); + }); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs index 4e91d2058263a..0e78057e95e6b 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs @@ -1,17 +1,17 @@ use std::path::{Path, PathBuf}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::script::ScriptTag; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PySourceType; +use ruff_python_ast::script::ScriptTag; use ruff_python_trivia::CommentRanges; use ruff_text_size::{TextRange, TextSize}; +use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; use crate::comments::shebang::ShebangDirective; use crate::fs; use crate::package::PackageRoot; -use crate::settings::types::PreviewMode; -use crate::Locator; /// ## What it does /// Checks for packages that are missing an `__init__.py` file. @@ -43,16 +43,21 @@ impl Violation for ImplicitNamespacePackage { let ImplicitNamespacePackage { filename, parent } = self; match parent { None => { - format!("File `{filename}` is part of an implicit namespace package. Add an `__init__.py`.") + format!( + "File `{filename}` is part of an implicit namespace package. Add an `__init__.py`." + ) } Some(parent) => { - format!("File `{filename}` declares a package, but is nested under an implicit namespace package. Add an `__init__.py` to `{parent}`.") + format!( + "File `{filename}` declares a package, but is nested under an implicit namespace package. Add an `__init__.py` to `{parent}`." + ) } } } } /// INP001 +#[expect(clippy::too_many_arguments)] pub(crate) fn implicit_namespace_package( path: &Path, package: Option>, @@ -60,8 +65,9 @@ pub(crate) fn implicit_namespace_package( comment_ranges: &CommentRanges, project_root: &Path, src: &[PathBuf], - preview: PreviewMode, -) -> Option { + allow_nested_roots: bool, + context: &LintContext, +) { if package.is_none() // Ignore non-`.py` files, which don't require an `__init__.py`. && PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) @@ -80,20 +86,14 @@ pub(crate) fn implicit_namespace_package( // Ignore PEP 723 scripts. && ScriptTag::parse(locator.contents().as_bytes()).is_none() { - #[cfg(all(test, windows))] - let path = path - .to_string_lossy() - .replace(std::path::MAIN_SEPARATOR, "/"); // The snapshot test expects / as the path separator. - return Some(Diagnostic::new( + context.report_diagnostic( ImplicitNamespacePackage { filename: fs::relativize_path(path), parent: None, }, TextRange::default(), - )); - } - - if preview.is_enabled() { + ); + } else if allow_nested_roots { if let Some(PackageRoot::Nested { path: root }) = package.as_ref() { if path.ends_with("__init__.py") { // Identify the intermediary package that's missing the `__init__.py` file. @@ -101,22 +101,15 @@ pub(crate) fn implicit_namespace_package( .ancestors() .find(|parent| !parent.join("__init__.py").exists()) { - #[cfg(all(test, windows))] - let path = path - .to_string_lossy() - .replace(std::path::MAIN_SEPARATOR, "/"); // The snapshot test expects / as the path separator. - - return Some(Diagnostic::new( + context.report_diagnostic( ImplicitNamespacePackage { filename: fs::relativize_path(path), parent: Some(fs::relativize_path(parent)), }, TextRange::default(), - )); + ); } } } } - - None } diff --git a/crates/ruff_linter/src/rules/flake8_pie/mod.rs b/crates/ruff_linter/src/rules/flake8_pie/mod.rs index 8b312f1fb136e..ec1697ba2f624 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/mod.rs @@ -10,7 +10,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::DuplicateClassFieldDefinition, Path::new("PIE794.py"))] #[test_case(Rule::UnnecessaryDictKwargs, Path::new("PIE804.py"))] @@ -27,7 +27,7 @@ mod tests { Path::new("flake8_pie").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs index cd92c32d2ea6d..7049a2814df34 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs @@ -1,14 +1,13 @@ use rustc_hash::FxHashSet; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::{AlwaysFixableViolation, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for duplicate field definitions in classes. @@ -31,6 +30,10 @@ use crate::fix; /// name = Tom /// ... /// ``` +/// +/// ## Fix safety +/// This fix is always marked as unsafe since we cannot know +/// for certain which assignment was intended. #[derive(ViolationMetadata)] pub(crate) struct DuplicateClassFieldDefinition { name: String, @@ -94,7 +97,7 @@ pub(crate) fn duplicate_class_field_definition(checker: &Checker, body: &[Stmt]) } if !seen_targets.insert(target.id.as_str()) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DuplicateClassFieldDefinition { name: target.id.to_string(), }, @@ -105,7 +108,6 @@ pub(crate) fn duplicate_class_field_definition(checker: &Checker, body: &[Stmt]) diagnostic.set_fix(Fix::unsafe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_id(), ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index 763c0584b1fde..27c086833228d 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -3,16 +3,16 @@ use std::iter; use itertools::Either::{Left, Right}; -use ruff_python_semantic::{analyze, SemanticModel}; +use ruff_python_semantic::{SemanticModel, analyze}; use ruff_text_size::{Ranged, TextRange}; use ruff_python_ast::{self as ast, Arguments, BoolOp, Expr, ExprContext, Identifier}; -use ruff_diagnostics::AlwaysFixableViolation; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::AlwaysFixableViolation; use crate::checkers::ast::Checker; +use crate::{Edit, Fix}; /// ## What it does /// Checks for `startswith` or `endswith` calls on the same value with @@ -72,6 +72,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { op: BoolOp::Or, values, range: _, + node_index: _, }) = expr else { return; @@ -86,8 +87,10 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, }) = &call else { continue; @@ -128,7 +131,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { // Generate a `Diagnostic` for each duplicate. for ((attr_name, arg_name), indices) in duplicates { if indices.len() > 1 { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MultipleStartsEndsWith { attr: attr_name.to_string(), }, @@ -145,8 +148,10 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { args, keywords: _, range: _, + node_index: _, }, range: _, + node_index: _, }) = expr else { unreachable!( @@ -173,18 +178,21 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { .collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }); let node1 = Expr::Name(ast::ExprName { id: arg_name.into(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let node2 = Expr::Attribute(ast::ExprAttribute { value: Box::new(node1), attr: Identifier::new(attr_name.to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let node3 = Expr::Call(ast::ExprCall { func: Box::new(node2), @@ -192,8 +200,10 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { args: Box::from([node]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let call = node3; @@ -213,13 +223,13 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { }) .collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let bool_op = node; diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&bool_op), expr.range(), ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs index 5a8783155b6c0..21464f148fe90 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -1,13 +1,12 @@ use ruff_python_semantic::SemanticModel; use rustc_hash::FxHashSet; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::{self as ast, Expr, ExprCall, Stmt}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -91,13 +90,12 @@ pub(crate) fn non_unique_enums(checker: &Checker, parent: &Stmt, body: &[Stmt]) let comparable = ComparableExpr::from(value); if !seen_targets.insert(comparable) { - let diagnostic = Diagnostic::new( + checker.report_diagnostic( NonUniqueEnums { value: checker.generator().expr(value), }, stmt.range(), ); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs index 76e08db4247a3..ad817929c9e5f 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/reimplemented_container_builtin.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{Expr, ExprLambda}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_diagnostics::{FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix}; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for lambdas that can be replaced with the `list` or `dict` builtins. @@ -63,6 +63,7 @@ pub(crate) fn reimplemented_container_builtin(checker: &Checker, expr: &ExprLamb parameters, body, range: _, + node_index: _, } = expr; if parameters.is_some() { @@ -74,7 +75,8 @@ pub(crate) fn reimplemented_container_builtin(checker: &Checker, expr: &ExprLamb Expr::Dict(dict) if dict.is_empty() => Container::Dict, _ => return, }; - let mut diagnostic = Diagnostic::new(ReimplementedContainerBuiltin { container }, expr.range()); + let mut diagnostic = + checker.report_diagnostic(ReimplementedContainerBuiltin { container }, expr.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( container.as_str(), @@ -84,7 +86,6 @@ pub(crate) fn reimplemented_container_builtin(checker: &Checker, expr: &ExprLamb let binding_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::safe_edits(binding_edit, import_edit)) }); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs index f0f9ba7c598c9..09bf37474cd95 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs @@ -1,41 +1,71 @@ use itertools::Itertools; use rustc_hash::{FxBuildHasher, FxHashSet}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary `dict` kwargs. /// /// ## Why is this bad? /// If the `dict` keys are valid identifiers, they can be passed as keyword -/// arguments directly. +/// arguments directly, without constructing unnecessary dictionary. +/// This also makes code more type-safe as type checkers often cannot +/// precisely verify dynamic keyword arguments. /// /// ## Example +/// /// ```python /// def foo(bar): /// return bar + 1 /// /// /// print(foo(**{"bar": 2})) # prints 3 +/// +/// # No typing errors, but results in an exception at runtime. +/// print(foo(**{"bar": 2, "baz": 3})) /// ``` /// /// Use instead: +/// /// ```python /// def foo(bar): /// return bar + 1 /// /// /// print(foo(bar=2)) # prints 3 +/// +/// # Typing error detected: No parameter named "baz". +/// print(foo(bar=2, baz=3)) +/// ``` +/// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe for dictionaries with comments interleaved between +/// the items, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// +/// ```python +/// foo( +/// **{ +/// # comment +/// "x": 1.0, +/// # comment +/// "y": 2.0, +/// } +/// ) /// ``` /// +/// as this is converted to `foo(x=1.0, y=2.0)` without any of the comments. +/// /// ## References /// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) /// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) @@ -70,12 +100,13 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { // Ex) `foo(**{**bar})` if let [ast::DictItem { key: None, value }] = dict.items.as_slice() { - let diagnostic = Diagnostic::new(UnnecessaryDictKwargs, keyword.range()); let edit = Edit::range_replacement( format!("**{}", checker.locator().slice(value)), keyword.range(), ); - checker.report_diagnostic(diagnostic.with_fix(Fix::safe_edit(edit))); + checker + .report_diagnostic(UnnecessaryDictKwargs, keyword.range()) + .set_fix(Fix::safe_edit(edit)); continue; } @@ -89,7 +120,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { continue; } - let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, keyword.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryDictKwargs, keyword.range()); if dict.is_empty() { diagnostic.try_set_fix(|| { @@ -98,6 +129,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); @@ -113,7 +145,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { .iter() .all(|kwarg| !duplicate_keywords.contains(kwarg)) { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + let edit = Edit::range_replacement( kwargs .iter() .zip(dict.iter_values()) @@ -134,12 +166,18 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &Checker, call: &ast::ExprCall) { }) .join(", "), keyword.range(), - ))); + ); + diagnostic.set_fix(Fix::applicable_edit( + edit, + if checker.comment_ranges().intersects(dict.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); } } } - - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs index 71d7e79f89676..1a64d0f971c98 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs @@ -1,6 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::whitespace::trailing_comment_start_offset; use ruff_python_ast::{Expr, ExprStringLiteral, Stmt, StmtExpr}; @@ -9,6 +7,8 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Applicability}; +use crate::{Edit, Fix}; /// ## What it does /// Checks for unnecessary `pass` statements and ellipsis (`...`) literals in @@ -138,14 +138,14 @@ fn add_diagnostic( let isolation_level = Checker::isolation(checker.semantic().current_statement_id()); let fix = Fix::applicable_edit(edit, applicability).isolate(isolation_level); - let diagnostic = Diagnostic::new( - UnnecessaryPlaceholder { - kind: placeholder_kind, - }, - stmt.range(), - ); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic( + UnnecessaryPlaceholder { + kind: placeholder_kind, + }, + stmt.range(), + ) + .set_fix(fix); } #[derive(Debug, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs index f95bf277885e4..5c9d4c4be613e 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_range_start.rs @@ -1,11 +1,10 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::{AlwaysFixableViolation, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for `range` calls with an unnecessary `start` argument. @@ -70,15 +69,15 @@ pub(crate) fn unnecessary_range_start(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryRangeStart, start.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryRangeStart, start.range()); diagnostic.try_set_fix(|| { remove_argument( &start, &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs index 948ce36b25e81..b5aefbfa5e3c9 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_spread.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_parser::{TokenKind, Tokens}; use ruff_text_size::{Ranged, TextLen, TextSize}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary dictionary unpacking operators (`**`). @@ -52,11 +52,10 @@ pub(crate) fn unnecessary_spread(checker: &Checker, dict: &ast::ExprDict) { // We only care about when the key is None which indicates a spread `**` // inside a dict. if let Expr::Dict(inner) = value { - let mut diagnostic = Diagnostic::new(UnnecessarySpread, value.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessarySpread, value.range()); if let Some(fix) = unnecessary_spread_fix(inner, prev_end, checker.tokens()) { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } } prev_end = value.end(); diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE804_PIE804.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE804_PIE804.py.snap index b26502fe8ccf7..f06e91e71af1c 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE804_PIE804.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE804_PIE804.py.snap @@ -190,37 +190,73 @@ PIE804.py:26:22: PIE804 [*] Unnecessary `dict` kwargs 26 |+abc(a=1, **{'a': c}, b=c) # PIE804 27 27 | 28 28 | # Some values need to be parenthesized. -29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 +29 29 | def foo(): -PIE804.py:29:12: PIE804 [*] Unnecessary `dict` kwargs +PIE804.py:30:16: PIE804 [*] Unnecessary `dict` kwargs | 28 | # Some values need to be parenthesized. -29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 - | ^^^^^^^^^^^^^^^^^^^^^ PIE804 -30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804 +29 | def foo(): +30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 + | ^^^^^^^^^^^^^^^^^^^^^ PIE804 +31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804 | = help: Remove unnecessary kwargs ℹ Safe fix -26 26 | abc(a=1, **{'a': c}, **{'b': c}) # PIE804 27 27 | 28 28 | # Some values need to be parenthesized. -29 |-abc(foo=1, **{'bar': (bar := 1)}) # PIE804 - 29 |+abc(foo=1, bar=(bar := 1)) # PIE804 -30 30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804 +29 29 | def foo(): +30 |- abc(foo=1, **{'bar': (bar := 1)}) # PIE804 + 30 |+ abc(foo=1, bar=(bar := 1)) # PIE804 +31 31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804 +32 32 | +33 33 | # https://github.com/astral-sh/ruff/issues/18036 -PIE804.py:30:12: PIE804 [*] Unnecessary `dict` kwargs +PIE804.py:31:16: PIE804 [*] Unnecessary `dict` kwargs | -28 | # Some values need to be parenthesized. -29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 -30 | abc(foo=1, **{'bar': (yield 1)}) # PIE804 - | ^^^^^^^^^^^^^^^^^^^^ PIE804 +29 | def foo(): +30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 +31 | abc(foo=1, **{'bar': (yield 1)}) # PIE804 + | ^^^^^^^^^^^^^^^^^^^^ PIE804 +32 | +33 | # https://github.com/astral-sh/ruff/issues/18036 | = help: Remove unnecessary kwargs ℹ Safe fix -27 27 | 28 28 | # Some values need to be parenthesized. -29 29 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 -30 |-abc(foo=1, **{'bar': (yield 1)}) # PIE804 - 30 |+abc(foo=1, bar=(yield 1)) # PIE804 +29 29 | def foo(): +30 30 | abc(foo=1, **{'bar': (bar := 1)}) # PIE804 +31 |- abc(foo=1, **{'bar': (yield 1)}) # PIE804 + 31 |+ abc(foo=1, bar=(yield 1)) # PIE804 +32 32 | +33 33 | # https://github.com/astral-sh/ruff/issues/18036 +34 34 | # The autofix for this is unsafe due to the comments inside the dictionary. + +PIE804.py:36:5: PIE804 [*] Unnecessary `dict` kwargs + | +34 | # The autofix for this is unsafe due to the comments inside the dictionary. +35 | foo( +36 | / **{ +37 | | # Comment 1 +38 | | "x": 1.0, +39 | | # Comment 2 +40 | | "y": 2.0, +41 | | } + | |_____^ PIE804 +42 | ) + | + = help: Remove unnecessary kwargs + +ℹ Unsafe fix +33 33 | # https://github.com/astral-sh/ruff/issues/18036 +34 34 | # The autofix for this is unsafe due to the comments inside the dictionary. +35 35 | foo( +36 |- **{ +37 |- # Comment 1 +38 |- "x": 1.0, +39 |- # Comment 2 +40 |- "y": 2.0, +41 |- } + 36 |+ x=1.0, y=2.0 +42 37 | ) diff --git a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE808_PIE808.py.snap b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE808_PIE808.py.snap index 86f5cb633258e..7423a0fcc9868 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE808_PIE808.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE808_PIE808.py.snap @@ -38,3 +38,18 @@ PIE808.py:5:16: PIE808 [*] Unnecessary `start` argument in `range` 6 6 | 7 7 | # OK 8 8 | range(x, 10) + +PIE808.py:19:8: PIE808 [*] Unnecessary `start` argument in `range` + | +18 | # regression test for https://github.com/astral-sh/ruff/pull/18805 +19 | range((0), 42) + | ^ PIE808 + | + = help: Remove `start` argument + +ℹ Safe fix +16 16 | range(0, stop=10) +17 17 | +18 18 | # regression test for https://github.com/astral-sh/ruff/pull/18805 +19 |-range((0), 42) + 19 |+range(42) diff --git a/crates/ruff_linter/src/rules/flake8_print/mod.rs b/crates/ruff_linter/src/rules/flake8_print/mod.rs index 8d40e8e1afaf8..2469a484a951e 100644 --- a/crates/ruff_linter/src/rules/flake8_print/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_print/mod.rs @@ -10,7 +10,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::Print, Path::new("T201.py"))] #[test_case(Rule::PPrint, Path::new("T203.py"))] @@ -20,7 +20,7 @@ mod tests { Path::new("flake8_print").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs b/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs index ec895762912ed..c5a851aaf8305 100644 --- a/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs @@ -1,36 +1,51 @@ -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::delete_stmt; -use crate::registry::AsRule; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `print` statements. /// /// ## Why is this bad? -/// `print` statements are useful in some situations (e.g., debugging), but -/// should typically be omitted from production code. `print` statements can -/// lead to the accidental inclusion of sensitive information in logs, and are -/// not configurable by clients, unlike `logging` statements. +/// `print` statements used for debugging should be omitted from production +/// code. They can lead the accidental inclusion of sensitive information in +/// logs, and are not configurable by clients, unlike `logging` statements. +/// +/// `print` statements used to produce output as a part of a command-line +/// interface program are not typically a problem. /// /// ## Example /// ```python -/// def add_numbers(a, b): -/// print(f"The sum of {a} and {b} is {a + b}") -/// return a + b +/// def sum_less_than_four(a, b): +/// print(f"Calling sum_less_than_four") +/// return a + b < 4 /// ``` /// -/// Use instead: +/// The automatic fix will remove the print statement entirely: +/// +/// ```python +/// def sum_less_than_four(a, b): +/// return a + b < 4 +/// ``` +/// +/// To keep the line for logging purposes, instead use something like: +/// /// ```python -/// def add_numbers(a, b): -/// return a + b +/// import logging +/// +/// logging.basicConfig(level=logging.INFO) +/// +/// +/// def sum_less_than_four(a, b): +/// logging.debug("Calling sum_less_than_four") +/// return a + b < 4 /// ``` /// /// ## Fix safety -/// This rule's fix is marked as unsafe, as it may remove `print` statements +/// This rule's fix is marked as unsafe, as it will remove `print` statements /// that are used beyond debugging purposes. #[derive(ViolationMetadata)] pub(crate) struct Print; @@ -52,11 +67,13 @@ impl Violation for Print { /// Checks for `pprint` statements. /// /// ## Why is this bad? -/// Like `print` statements, `pprint` statements are useful in some situations -/// (e.g., debugging), but should typically be omitted from production code. -/// `pprint` statements can lead to the accidental inclusion of sensitive -/// information in logs, and are not configurable by clients, unlike `logging` -/// statements. +/// Like `print` statements, `pprint` statements used for debugging should +/// be omitted from production code. They can lead the accidental inclusion +/// of sensitive information in logs, and are not configurable by clients, +/// unlike `logging` statements. +/// +/// `pprint` statements used to produce output as a part of a command-line +/// interface program are not typically a problem. /// /// ## Example /// ```python @@ -77,7 +94,7 @@ impl Violation for Print { /// ``` /// /// ## Fix safety -/// This rule's fix is marked as unsafe, as it may remove `pprint` statements +/// This rule's fix is marked as unsafe, as it will remove `pprint` statements /// that are used beyond debugging purposes. #[derive(ViolationMetadata)] pub(crate) struct PPrint; @@ -103,7 +120,7 @@ pub(crate) fn print_call(checker: &Checker, call: &ast::ExprCall) { return; }; - let mut diagnostic = match qualified_name.segments() { + let diagnostic = match qualified_name.segments() { ["" | "builtins", "print"] => { // If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`, // or `"sys.stderr"`), don't trigger T201. @@ -118,15 +135,15 @@ pub(crate) fn print_call(checker: &Checker, call: &ast::ExprCall) { } } } - Diagnostic::new(Print, call.func.range()) + checker.report_diagnostic_if_enabled(Print, call.func.range()) } - ["pprint", "pprint"] => Diagnostic::new(PPrint, call.func.range()), + ["pprint", "pprint"] => checker.report_diagnostic_if_enabled(PPrint, call.func.range()), _ => return, }; - if !checker.enabled(diagnostic.kind.rule()) { + let Some(mut diagnostic) = diagnostic else { return; - } + }; // Remove the `print`, if it's a standalone statement. if semantic.current_expression_parent().is_none() { @@ -138,6 +155,4 @@ pub(crate) fn print_call(checker: &Checker, call: &ast::ExprCall) { .isolate(Checker::isolation(semantic.current_statement_parent_id())), ); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs index eef87b404aefc..a065946483ae6 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs @@ -13,7 +13,7 @@ mod tests { use crate::rules::pep8_naming; use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::AnyEqNeAnnotation, Path::new("PYI032.py"))] #[test_case(Rule::AnyEqNeAnnotation, Path::new("PYI032.pyi"))] @@ -73,8 +73,9 @@ mod tests { #[test_case(Rule::RedundantFinalLiteral, Path::new("PYI064.pyi"))] #[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.py"))] #[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.pyi"))] - #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.py"))] - #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.pyi"))] + #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_1.py"))] + #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_1.pyi"))] + #[test_case(Rule::RedundantNumericUnion, Path::new("PYI041_2.py"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))] #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] @@ -133,7 +134,7 @@ mod tests { Path::new("flake8_pyi").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -152,7 +153,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -165,15 +166,16 @@ mod tests { let diagnostics = test_path( Path::new("flake8_pyi").join(path).as_path(), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY38, + unresolved_target_version: PythonVersion::PY38.into(), ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } - #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))] + #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))] + #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", @@ -187,7 +189,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index ba189c2bf7d4d..bebf9aee83aa4 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Parameters; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `__eq__` and `__ne__` implementations that use `typing.Any` as @@ -28,8 +28,10 @@ use crate::checkers::ast::Checker; /// ## Example /// /// ```pyi +/// from typing import Any +/// /// class Foo: -/// def __eq__(self, obj: typing.Any) -> bool: ... +/// def __eq__(self, obj: Any) -> bool: ... /// ``` /// /// Use instead: @@ -84,7 +86,7 @@ pub(crate) fn any_eq_ne_annotation(checker: &Checker, name: &str, parameters: &P return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( AnyEqNeAnnotation { method_name: name.to_string(), }, @@ -100,5 +102,4 @@ pub(crate) fn any_eq_ne_annotation(checker: &Checker, name: &str, parameters: &P let binding_edit = Edit::range_replacement(binding, annotation.range()); Ok(Fix::safe_edits(binding_edit, import_edit)) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs index f10947217063f..0524b69af6098 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::identifier::Identifier; @@ -8,6 +7,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for simple `__iter__` methods that return `Generator`, and for @@ -210,7 +210,7 @@ pub(crate) fn bad_generator_return_type(function_def: &ast::StmtFunctionDef, che } } } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( GeneratorReturnFromIterMethod { return_type: member.to_iter(), method, @@ -228,8 +228,6 @@ pub(crate) fn bad_generator_return_type(function_def: &ast::StmtFunctionDef, che checker, ) }); - - checker.report_diagnostic(diagnostic); } /// Returns `true` if the [`ast::Expr`] is a `None` literal or a `typing.Any` expression. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 81066c0027343..6ac4235e641de 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -1,10 +1,11 @@ use ruff_python_ast::{self as ast, CmpOp, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; +use crate::preview::is_bad_version_info_in_non_stub_enabled; use crate::registry::Rule; /// ## What it does @@ -138,17 +139,15 @@ pub(crate) fn bad_version_info_comparison(checker: &Checker, test: &Expr, has_el } if matches!(op, CmpOp::Lt) { - if checker.enabled(Rule::BadVersionInfoOrder) + if checker.is_rule_enabled(Rule::BadVersionInfoOrder) // See https://github.com/astral-sh/ruff/issues/15347 - && (checker.source_type.is_stub() || checker.settings.preview.is_enabled()) + && (checker.source_type.is_stub() || is_bad_version_info_in_non_stub_enabled(checker.settings())) { if has_else_clause { - checker.report_diagnostic(Diagnostic::new(BadVersionInfoOrder, test.range())); + checker.report_diagnostic(BadVersionInfoOrder, test.range()); } } } else { - if checker.enabled(Rule::BadVersionInfoComparison) { - checker.report_diagnostic(Diagnostic::new(BadVersionInfoComparison, test.range())); - } + checker.report_diagnostic_if_enabled(BadVersionInfoComparison, test.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs index 5fb9f3f82fd7d..264d6505482cf 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bytestring_usage.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for uses of `typing.ByteString` or `collections.abc.ByteString`. @@ -74,10 +74,7 @@ pub(crate) fn bytestring_attribute(checker: &Checker, attribute: &Expr) { ["collections", "abc", "ByteString"] => ByteStringOrigin::CollectionsAbc, _ => return, }; - checker.report_diagnostic(Diagnostic::new( - ByteStringUsage { origin }, - attribute.range(), - )); + checker.report_diagnostic(ByteStringUsage { origin }, attribute.range()); } /// PYI057 @@ -97,7 +94,7 @@ pub(crate) fn bytestring_import(checker: &Checker, import_from: &ast::StmtImport for name in names { if name.name.as_str() == "ByteString" { - checker.report_diagnostic(Diagnostic::new(ByteStringUsage { origin }, name.range())); + checker.report_diagnostic(ByteStringUsage { origin }, name.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs index 78fc8acfae102..5c2a4de600cba 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -24,7 +24,7 @@ use crate::checkers::ast::Checker; /// ```pyi /// from collections import namedtuple /// -/// person = namedtuple("Person", ["name", "age"]) +/// Person = namedtuple("Person", ["name", "age"]) /// ``` /// /// Use instead: @@ -62,6 +62,6 @@ pub(crate) fn collections_named_tuple(checker: &Checker, expr: &Expr) { matches!(qualified_name.segments(), ["collections", "namedtuple"]) }) { - checker.report_diagnostic(Diagnostic::new(CollectionsNamedTuple, expr.range())); + checker.report_diagnostic(CollectionsNamedTuple, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs index 29769a69583a6..0f38dfe8e68a9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_assignment_in_stub.rs @@ -1,8 +1,8 @@ use ruff_python_ast::{Expr, StmtAssign}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -56,5 +56,5 @@ pub(crate) fn complex_assignment_in_stub(checker: &Checker, stmt: &StmtAssign) { if matches!(stmt.targets.as_slice(), [Expr::Name(_)]) { return; } - checker.report_diagnostic(Diagnostic::new(ComplexAssignmentInStub, stmt.range)); + checker.report_diagnostic(ComplexAssignmentInStub, stmt.range); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs index f432eae1f8d4b..010753a0bc275 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -48,12 +48,12 @@ pub(crate) fn complex_if_statement_in_stub(checker: &Checker, test: &Expr) { left, comparators, .. }) = test else { - checker.report_diagnostic(Diagnostic::new(ComplexIfStatementInStub, test.range())); + checker.report_diagnostic(ComplexIfStatementInStub, test.range()); return; }; if comparators.len() != 1 { - checker.report_diagnostic(Diagnostic::new(ComplexIfStatementInStub, test.range())); + checker.report_diagnostic(ComplexIfStatementInStub, test.range()); return; } @@ -74,5 +74,5 @@ pub(crate) fn complex_if_statement_in_stub(checker: &Checker, test: &Expr) { return; } - checker.report_diagnostic(Diagnostic::new(ComplexIfStatementInStub, test.range())); + checker.report_diagnostic(ComplexIfStatementInStub, test.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index e3b84a605a4ce..aaa182b4e07f1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -1,17 +1,17 @@ -use anyhow::{bail, Context}; +use anyhow::{Context, bail}; use itertools::Itertools; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; +use ruff_python_ast::PythonVersion; use ruff_python_semantic::analyze::class::is_metaclass; use ruff_python_semantic::analyze::function_type::{self, FunctionType}; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; -use ruff_python_ast::PythonVersion; +use crate::checkers::ast::{Checker, TypingImporter}; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for methods that use custom [`TypeVar`s][typing_TypeVar] in their @@ -53,7 +53,7 @@ use ruff_python_ast::PythonVersion; /// def bar(cls, arg: int) -> Self: ... /// ``` /// -/// ## Fix behaviour and safety +/// ## Fix behaviour /// The fix replaces all references to the custom type variable in the method's header and body /// with references to `Self`. The fix also adds an import of `Self` if neither `Self` nor `typing` /// is already imported in the module. If your [`target-version`] setting is set to Python 3.11 or @@ -67,9 +67,20 @@ use ruff_python_ast::PythonVersion; /// [`unused-private-type-var`][PYI018] for a rule that will clean up unused private type /// variables. /// +/// ## Fix safety /// The fix is only marked as unsafe if there is the possibility that it might delete a comment /// from your code. /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.11, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// [PEP 673]: https://peps.python.org/pep-0673/#motivation /// [PEP-695]: https://peps.python.org/pep-0695/ /// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/ @@ -102,13 +113,18 @@ impl Violation for CustomTypeVarForSelf { } /// PYI019 -pub(crate) fn custom_type_var_instead_of_self( - checker: &Checker, - binding: &Binding, -) -> Option { +pub(crate) fn custom_type_var_instead_of_self(checker: &Checker, binding: &Binding) { let semantic = checker.semantic(); let current_scope = &semantic.scopes[binding.scope]; - let function_def = binding.statement(semantic)?.as_function_def_stmt()?; + let Some(function_def) = binding + .statement(semantic) + .and_then(|stmt| stmt.as_function_def_stmt()) + else { + return; + }; + let Some(importer) = checker.typing_importer("Self", PythonVersion::PY311) else { + return; + }; let ast::StmtFunctionDef { name: function_name, @@ -122,14 +138,26 @@ pub(crate) fn custom_type_var_instead_of_self( let type_params = type_params.as_deref(); // Given, e.g., `def foo(self: _S, arg: bytes)`, extract `_S`. - let self_or_cls_parameter = parameters - .posonlyargs - .iter() - .chain(¶meters.args) - .next()?; + let Some(self_or_cls_parameter) = parameters.posonlyargs.iter().chain(¶meters.args).next() + else { + return; + }; - let self_or_cls_annotation = self_or_cls_parameter.annotation()?; - let parent_class = current_scope.kind.as_class()?; + let Some(self_or_cls_annotation_unchecked) = self_or_cls_parameter.annotation() else { + return; + }; + let self_or_cls_annotation = match self_or_cls_annotation_unchecked { + ast::Expr::StringLiteral(literal_expr) => { + let Ok(parsed_expr) = checker.parse_type_annotation(literal_expr) else { + return; + }; + parsed_expr.expression() + } + _ => self_or_cls_annotation_unchecked, + }; + let Some(parent_class) = current_scope.kind.as_class() else { + return; + }; // Skip any abstract/static/overloaded methods, // and any methods in metaclasses @@ -137,7 +165,7 @@ pub(crate) fn custom_type_var_instead_of_self( || is_overload(decorator_list, semantic) || is_metaclass(parent_class, semantic).is_yes() { - return None; + return; } let function_kind = function_type::classify( @@ -145,8 +173,8 @@ pub(crate) fn custom_type_var_instead_of_self( decorator_list, current_scope, semantic, - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); let method = match function_kind { @@ -158,17 +186,19 @@ pub(crate) fn custom_type_var_instead_of_self( self_annotation: self_or_cls_annotation, type_params, }), - FunctionType::Function | FunctionType::StaticMethod => return None, + FunctionType::Function | FunctionType::StaticMethod => return, }; - let custom_typevar = method.custom_typevar(semantic, binding.scope)?; + let Some(custom_typevar) = method.custom_typevar(semantic, binding.scope) else { + return; + }; let function_header_end = returns .as_deref() .map(Ranged::end) .unwrap_or_else(|| parameters.end()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CustomTypeVarForSelf { typevar_name: custom_typevar.name(checker.source()).to_string(), }, @@ -178,14 +208,13 @@ pub(crate) fn custom_type_var_instead_of_self( diagnostic.try_set_fix(|| { replace_custom_typevar_with_self( checker, + &importer, function_def, custom_typevar, self_or_cls_parameter, - self_or_cls_annotation, + self_or_cls_annotation_unchecked, ) }); - - Some(diagnostic) } #[derive(Debug)] @@ -310,14 +339,14 @@ fn custom_typevar<'a>( /// * If it was a PEP-695 type variable, removes that `TypeVar` from the PEP-695 type-parameter list fn replace_custom_typevar_with_self( checker: &Checker, + importer: &TypingImporter, function_def: &ast::StmtFunctionDef, custom_typevar: TypeVar, self_or_cls_parameter: &ast::ParameterWithDefault, self_or_cls_annotation: &ast::Expr, ) -> anyhow::Result { // (1) Import `Self` (if necessary) - let (import_edit, self_symbol_binding) = - checker.import_from_typing("Self", function_def.start(), PythonVersion::PY311)?; + let (import_edit, self_symbol_binding) = importer.import(function_def.start())?; // (2) Remove the first parameter's annotation let mut other_edits = vec![Edit::deletion( diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs index 586219b0d7352..5921058fbec35 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/docstring_in_stubs.rs @@ -1,12 +1,10 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprStringLiteral; - -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_text_size::Ranged; - use ruff_python_semantic::Definition; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for the presence of docstrings in stub files. @@ -63,8 +61,6 @@ pub(crate) fn docstring_in_stubs( Edit::range_deletion(docstring_range) }; - let fix = Fix::unsafe_edit(edit); - let diagnostic = Diagnostic::new(DocstringInStub, docstring_range).with_fix(fix); - - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(DocstringInStub, docstring_range); + diagnostic.set_fix(Fix::unsafe_edit(edit)); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs index fe968f534c3fe..fea4985036c4f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_literal_member.rs @@ -2,14 +2,14 @@ use std::collections::HashSet; use rustc_hash::FxHashSet; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::{self as ast, Expr, ExprContext}; use ruff_python_semantic::analyze::typing::traverse_literal; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for duplicate members in a `typing.Literal[]` slice. @@ -19,11 +19,15 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python +/// from typing import Literal +/// /// foo: Literal["a", "b", "a"] /// ``` /// /// Use instead: /// ```python +/// from typing import Literal +/// /// foo: Literal["a", "b"] /// ``` /// @@ -55,7 +59,7 @@ impl AlwaysFixableViolation for DuplicateLiteralMember { pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) { let mut seen_nodes: HashSet, _> = FxHashSet::default(); let mut unique_nodes: Vec<&Expr> = Vec::new(); - let mut diagnostics: Vec = Vec::new(); + let mut diagnostics = Vec::new(); // Adds a member to `literal_exprs` if it is a `Literal` annotation let mut check_for_duplicate_members = |expr: &'a Expr, _: &'a Expr| { @@ -63,7 +67,7 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) { if seen_nodes.insert(expr.into()) { unique_nodes.push(expr); } else { - diagnostics.push(Diagnostic::new( + diagnostics.push(checker.report_diagnostic( DuplicateLiteralMember { duplicate_name: checker.generator().expr(expr), }, @@ -88,12 +92,14 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) { Expr::Tuple(ast::ExprTuple { elts: unique_nodes.into_iter().cloned().collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, parenthesized: false, }) }), value: subscript.value.clone(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, }); let fix = Fix::applicable_edit( @@ -108,6 +114,4 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &Checker, expr: &'a Expr) { diagnostic.set_fix(fix.clone()); } } - - checker.report_diagnostics(diagnostics); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs index 2ddc53ba1e55c..33812cebee09d 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -1,20 +1,16 @@ -use std::collections::HashSet; - -use anyhow::Result; - use rustc_hash::FxHashSet; +use std::collections::HashSet; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::name::Name; -use ruff_python_ast::{ - Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator, PythonVersion, -}; -use ruff_python_semantic::analyze::typing::traverse_union; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::{AtomicNodeIndex, Expr, ExprBinOp, ExprNoneLiteral, Operator, PythonVersion}; +use ruff_python_semantic::analyze::typing::{traverse_union, traverse_union_and_optional}; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use super::generate_union_fix; use crate::checkers::ast::Checker; +use crate::preview::is_optional_as_none_in_union_enabled; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for duplicate union members. @@ -65,7 +61,7 @@ impl Violation for DuplicateUnionMember { pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) { let mut seen_nodes: HashSet, _> = FxHashSet::default(); let mut unique_nodes: Vec<&Expr> = Vec::new(); - let mut diagnostics: Vec = Vec::new(); + let mut diagnostics = Vec::new(); let mut union_type = UnionKind::TypingUnion; // Adds a member to `literal_exprs` if it is a `Literal` annotation @@ -74,21 +70,35 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) { union_type = UnionKind::PEP604; } + let virtual_expr = if is_optional_as_none_in_union_enabled(checker.settings()) + && is_optional_type(checker, expr) + { + // If the union member is an `Optional`, add a virtual `None` literal. + &VIRTUAL_NONE_LITERAL + } else { + expr + }; + // If we've already seen this union member, raise a violation. - if seen_nodes.insert(expr.into()) { - unique_nodes.push(expr); + if seen_nodes.insert(virtual_expr.into()) { + unique_nodes.push(virtual_expr); } else { - diagnostics.push(Diagnostic::new( + diagnostics.push(checker.report_diagnostic( DuplicateUnionMember { - duplicate_name: checker.generator().expr(expr), + duplicate_name: checker.generator().expr(virtual_expr), }, + // Use the real expression's range for diagnostics, expr.range(), )); } }; // Traverse the union, collect all diagnostic members - traverse_union(&mut check_for_duplicate_members, checker.semantic(), expr); + if is_optional_as_none_in_union_enabled(checker.settings()) { + traverse_union_and_optional(&mut check_for_duplicate_members, checker.semantic(), expr); + } else { + traverse_union(&mut check_for_duplicate_members, checker.semantic(), expr); + } if diagnostics.is_empty() { return; @@ -118,7 +128,19 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) { applicability, )), UnionKind::TypingUnion => { - generate_union_fix(checker, unique_nodes, expr, applicability).ok() + // Request `typing.Union` + let Some(importer) = checker.typing_importer("Union", PythonVersion::lowest()) + else { + return; + }; + generate_union_fix( + checker.generator(), + &importer, + unique_nodes, + expr, + applicability, + ) + .ok() } } }; @@ -128,9 +150,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) { diagnostic.set_fix(fix.clone()); } } - - // Add all diagnostics to the checker - checker.report_diagnostics(diagnostics); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -159,6 +178,7 @@ fn generate_pep604_fix( op: Operator::BitOr, right: Box::new(right.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) } else { Some(right.clone()) @@ -172,39 +192,11 @@ fn generate_pep604_fix( ) } -/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. -fn generate_union_fix( - checker: &Checker, - nodes: Vec<&Expr>, - annotation: &Expr, - applicability: Applicability, -) -> Result { - debug_assert!(nodes.len() >= 2, "At least two nodes required"); +static VIRTUAL_NONE_LITERAL: Expr = Expr::NoneLiteral(ExprNoneLiteral { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::new(TextSize::new(0), TextSize::new(0)), +}); - // Request `typing.Union` - let (import_edit, binding) = - checker.import_from_typing("Union", annotation.start(), PythonVersion::lowest())?; - - // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` - let new_expr = Expr::Subscript(ExprSubscript { - range: TextRange::default(), - value: Box::new(Expr::Name(ExprName { - id: Name::new(binding), - ctx: ExprContext::Store, - range: TextRange::default(), - })), - slice: Box::new(Expr::Tuple(ExprTuple { - elts: nodes.into_iter().cloned().collect(), - range: TextRange::default(), - ctx: ExprContext::Load, - parenthesized: false, - })), - ctx: ExprContext::Load, - }); - - Ok(Fix::applicable_edits( - Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()), - [import_edit], - applicability, - )) +fn is_optional_type(checker: &Checker, expr: &Expr) -> bool { + checker.semantic().match_typing_expr(expr, "Optional") } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs index cda0183aeb228..aef6f5639bb49 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Stmt, StmtExpr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Removes ellipses (`...`) in otherwise non-empty class bodies. @@ -55,13 +55,13 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt]) }; if value.is_ellipsis_literal_expr() { - let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range()); + let mut diagnostic = + checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range()); let edit = fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer()); diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_id(), ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs index 881da80687e22..3127f3d411f8b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs @@ -6,13 +6,13 @@ use ruff_python_ast::{ }; use smallvec::SmallVec; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_semantic::{analyze::visibility::is_overload, SemanticModel}; +use ruff_python_semantic::{SemanticModel, analyze::visibility::is_overload}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for incorrect function signatures on `__exit__` and `__aexit__` @@ -59,17 +59,31 @@ impl Violation for BadExitAnnotation { fn message(&self) -> String { let method_name = self.func_kind.to_string(); match self.error_kind { - ErrorKind::StarArgsNotAnnotated => format!("Star-args in `{method_name}` should be annotated with `object`"), - ErrorKind::MissingArgs => format!("If there are no star-args, `{method_name}` should have at least 3 non-keyword-only args (excluding `self`)"), - ErrorKind::ArgsAfterFirstFourMustHaveDefault => format!("All arguments after the first four in `{method_name}` must have a default value"), - ErrorKind::AllKwargsMustHaveDefault => format!("All keyword-only arguments in `{method_name}` must have a default value"), - ErrorKind::FirstArgBadAnnotation => format!("The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`"), - ErrorKind::SecondArgBadAnnotation => format!("The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`"), - ErrorKind::ThirdArgBadAnnotation => format!("The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`"), + ErrorKind::StarArgsNotAnnotated => { + format!("Star-args in `{method_name}` should be annotated with `object`") + } + ErrorKind::MissingArgs => format!( + "If there are no star-args, `{method_name}` should have at least 3 non-keyword-only args (excluding `self`)" + ), + ErrorKind::ArgsAfterFirstFourMustHaveDefault => format!( + "All arguments after the first four in `{method_name}` must have a default value" + ), + ErrorKind::AllKwargsMustHaveDefault => { + format!("All keyword-only arguments in `{method_name}` must have a default value") + } + ErrorKind::FirstArgBadAnnotation => format!( + "The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`" + ), + ErrorKind::SecondArgBadAnnotation => format!( + "The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`" + ), + ErrorKind::ThirdArgBadAnnotation => format!( + "The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`" + ), ErrorKind::UnrecognizedExitOverload => format!( "Annotations for a three-argument `{method_name}` overload (excluding `self`) \ should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType`" - ) + ), } } @@ -167,13 +181,13 @@ pub(crate) fn bad_exit_annotation(checker: &Checker, function: &StmtFunctionDef) .skip(3) .filter(|parameter| parameter.default.is_none()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadExitAnnotation { func_kind, error_kind: ErrorKind::ArgsAfterFirstFourMustHaveDefault, }, parameter.range(), - )); + ); } // ...as should all keyword-only arguments. @@ -182,13 +196,13 @@ pub(crate) fn bad_exit_annotation(checker: &Checker, function: &StmtFunctionDef) .iter() .filter(|arg| arg.default.is_none()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadExitAnnotation { func_kind, error_kind: ErrorKind::AllKwargsMustHaveDefault, }, parameter.range(), - )); + ); } check_positional_args_for_non_overloaded_method(checker, &non_self_positional_args, func_kind); @@ -202,7 +216,7 @@ fn check_short_args_list(checker: &Checker, parameters: &Parameters, func_kind: .annotation() .filter(|ann| !is_object_or_unused(ann, checker.semantic())) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BadExitAnnotation { func_kind, error_kind: ErrorKind::StarArgsNotAnnotated, @@ -219,17 +233,15 @@ fn check_short_args_list(checker: &Checker, parameters: &Parameters, func_kind: let binding_edit = Edit::range_replacement(binding, annotation.range()); Ok(Fix::safe_edits(binding_edit, import_edit)) }); - - checker.report_diagnostic(diagnostic); } } else { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadExitAnnotation { func_kind, error_kind: ErrorKind::MissingArgs, }, parameters.range(), - )); + ); } } @@ -270,13 +282,13 @@ fn check_positional_args_for_non_overloaded_method( continue; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadExitAnnotation { func_kind: kind, error_kind: error_info, }, annotation.range(), - )); + ); } } @@ -335,6 +347,7 @@ fn check_positional_args_for_overloaded_method( // If any overloads have any variadic arguments, don't do any checking let Parameters { range: _, + node_index: _, posonlyargs, args, vararg: None, @@ -409,13 +422,13 @@ fn check_positional_args_for_overloaded_method( } // Okay, neither of them match... - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadExitAnnotation { func_kind: kind, error_kind: ErrorKind::UnrecognizedExitOverload, }, parameters_range, - )); + ); } /// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs index 8949ff40ad0f1..20c70336eb642 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs @@ -1,8 +1,8 @@ use ruff_python_ast::StmtImportFrom; -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::{Fix, FixAvailability, Violation}; use crate::{checkers::ast::Checker, fix}; /// ## What it does @@ -53,24 +53,20 @@ pub(crate) fn from_future_import(checker: &Checker, target: &StmtImportFrom) { return; } - let mut diagnostic = Diagnostic::new(FutureAnnotationsInStub, *range); + let mut diagnostic = checker.report_diagnostic(FutureAnnotationsInStub, *range); - if checker.settings.preview.is_enabled() { - let stmt = checker.semantic().current_statement(); + let stmt = checker.semantic().current_statement(); - diagnostic.try_set_fix(|| { - let edit = fix::edits::remove_unused_imports( - std::iter::once("annotations"), - stmt, - None, - checker.locator(), - checker.stylist(), - checker.indexer(), - )?; + diagnostic.try_set_fix(|| { + let edit = fix::edits::remove_unused_imports( + std::iter::once("annotations"), + stmt, + None, + checker.locator(), + checker.stylist(), + checker.indexer(), + )?; - Ok(Fix::safe_edit(edit)) - }); - } - - checker.report_diagnostic(diagnostic); + Ok(Fix::safe_edit(edit)) + }); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs index 7d5ef5ca81f44..19565abd5655c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/generic_not_last_base_class.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, helpers::map_subscript}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{add_argument, remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, add_argument, remove_argument}; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for classes inheriting from `typing.Generic[]` where `Generic[]` is @@ -13,40 +13,76 @@ use crate::fix::edits::{add_argument, remove_argument, Parentheses}; /// ## Why is this bad? /// If `Generic[]` is not the final class in the bases tuple, unexpected /// behaviour can occur at runtime (See [this CPython issue][1] for an example). -/// The rule is also applied to stub files, but, unlike at runtime, -/// in stubs it is purely enforced for stylistic consistency. +/// +/// The rule is also applied to stub files, where it won't cause issues at +/// runtime. This is because type checkers may not be able to infer an +/// accurate [MRO] for the class, which could lead to unexpected or +/// inaccurate results when they analyze your code. /// /// For example: /// ```python +/// from collections.abc import Container, Iterable, Sized +/// from typing import Generic, TypeVar +/// +/// +/// T = TypeVar("T") +/// K = TypeVar("K") +/// V = TypeVar("V") +/// +/// /// class LinkedList(Generic[T], Sized): /// def push(self, item: T) -> None: /// self._items.append(item) /// +/// /// class MyMapping( /// Generic[K, V], -/// Iterable[Tuple[K, V]], -/// Container[Tuple[K, V]], +/// Iterable[tuple[K, V]], +/// Container[tuple[K, V]], /// ): /// ... /// ``` /// /// Use instead: /// ```python +/// from collections.abc import Container, Iterable, Sized +/// from typing import Generic, TypeVar +/// +/// +/// T = TypeVar("T") +/// K = TypeVar("K") +/// V = TypeVar("V") +/// +/// /// class LinkedList(Sized, Generic[T]): /// def push(self, item: T) -> None: /// self._items.append(item) /// +/// /// class MyMapping( -/// Iterable[Tuple[K, V]], -/// Container[Tuple[K, V]], +/// Iterable[tuple[K, V]], +/// Container[tuple[K, V]], /// Generic[K, V], /// ): /// ... /// ``` +/// +/// ## Fix safety +/// +/// This rule's fix is always unsafe because reordering base classes can change +/// the behavior of the code by modifying the class's MRO. The fix will also +/// delete trailing comments after the `Generic` base class in multi-line base +/// class lists, if any are present. +/// +/// ## Fix availability +/// +/// This rule's fix is only available when there are no `*args` present in the base class list. +/// /// ## References /// - [`typing.Generic` documentation](https://docs.python.org/3/library/typing.html#typing.Generic) /// /// [1]: https://github.com/python/cpython/issues/106102 +/// [MRO]: https://docs.python.org/3/glossary.html#term-method-resolution-order #[derive(ViolationMetadata)] pub(crate) struct GenericNotLastBaseClass; @@ -92,14 +128,28 @@ pub(crate) fn generic_not_last_base_class(checker: &Checker, class_def: &ast::St return; } - let mut diagnostic = Diagnostic::new(GenericNotLastBaseClass, bases.range()); + let mut diagnostic = checker.report_diagnostic(GenericNotLastBaseClass, bases.range()); + + // Avoid suggesting a fix if any of the arguments is starred. This avoids tricky syntax errors + // in cases like + // + // ```python + // class C3(Generic[T], metaclass=type, *[str]): ... + // ``` + // + // where we would naively try to put `Generic[T]` after `*[str]`, which is also after a keyword + // argument, causing the error. + if bases + .arguments_source_order() + .any(|arg| arg.value().is_starred_expr()) + { + return; + } // No fix if multiple `Generic[]`s are seen in the class bases. if generic_base_iter.next().is_none() { diagnostic.try_set_fix(|| generate_fix(generic_base, bases, checker)); } - - checker.report_diagnostic(diagnostic); } fn generate_fix( @@ -110,7 +160,13 @@ fn generate_fix( let locator = checker.locator(); let source = locator.contents(); - let deletion = remove_argument(generic_base, arguments, Parentheses::Preserve, source)?; + let deletion = remove_argument( + generic_base, + arguments, + Parentheses::Preserve, + source, + checker.comment_ranges(), + )?; let insertion = add_argument( locator.slice(generic_base), arguments, @@ -118,5 +174,5 @@ fn generate_fix( source, ); - Ok(Fix::safe_edits(deletion, [insertion])) + Ok(Fix::unsafe_edits(deletion, [insertion])) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs index 398cc349bc63b..b72a0ce0cc31f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; use ruff_text_size::Ranged; use ruff_python_semantic::{Definition, Member, MemberKind}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -125,9 +125,6 @@ pub(crate) fn iter_method_return_iterable(checker: &Checker, definition: &Defini } }) { - checker.report_diagnostic(Diagnostic::new( - IterMethodReturnIterable { is_async }, - returns.range(), - )); + checker.report_diagnostic(IterMethodReturnIterable { is_async }, returns.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs index 25be942f197c6..77e902da12476 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs @@ -1,5 +1,14 @@ use std::fmt; +use anyhow::Result; + +use ruff_python_ast::{Expr, ExprContext, ExprName, ExprSubscript, ExprTuple, name::Name}; +use ruff_python_codegen::Generator; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::TypingImporter; +use crate::{Applicability, Edit, Fix}; + pub(crate) use any_eq_ne_annotation::*; pub(crate) use bad_generator_return_type::*; pub(crate) use bad_version_info_comparison::*; @@ -108,3 +117,42 @@ impl fmt::Display for TypingModule { fmt.write_str(self.as_str()) } } + +/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. +fn generate_union_fix( + generator: Generator, + importer: &TypingImporter, + nodes: Vec<&Expr>, + annotation: &Expr, + applicability: Applicability, +) -> Result { + debug_assert!(nodes.len() >= 2, "At least two nodes required"); + + let (import_edit, binding) = importer.import(annotation.start())?; + + // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` + let new_expr = Expr::Subscript(ExprSubscript { + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + value: Box::new(Expr::Name(ExprName { + id: Name::new(binding), + ctx: ExprContext::Store, + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + })), + slice: Box::new(Expr::Tuple(ExprTuple { + elts: nodes.into_iter().cloned().collect(), + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + ctx: ExprContext::Load, + parenthesized: false, + })), + ctx: ExprContext::Load, + }); + + Ok(Fix::applicable_edits( + Edit::range_replacement(generator.expr(&new_expr), annotation.range()), + [import_edit], + applicability, + )) +} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index 5500f706a39eb..c129ae33f7953 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -1,10 +1,10 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use ruff_python_ast::PythonVersion; @@ -65,7 +65,7 @@ pub(crate) fn no_return_argument_annotation(checker: &Checker, parameters: &ast: .filter_map(ast::AnyParameterRef::annotation) { if is_no_return(annotation, checker) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NoReturnArgumentAnnotationInStub { module: if checker.target_version() >= PythonVersion::PY311 { TypingModule::Typing @@ -74,7 +74,7 @@ pub(crate) fn no_return_argument_annotation(checker: &Checker, parameters: &ast: }, }, annotation.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs index b99413d5993b6..3e3efd8c661fe 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_empty_stub_body.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::{self as ast, Stmt}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for non-empty function stub bodies. @@ -59,16 +59,20 @@ pub(crate) fn non_empty_stub_body(checker: &Checker, body: &[Stmt]) { } // Ignore `...` (the desired case). - if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { + if let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = stmt + { if value.is_ellipsis_literal_expr() { return; } } - let mut diagnostic = Diagnostic::new(NonEmptyStubBody, stmt.range()); + let mut diagnostic = checker.report_diagnostic(NonEmptyStubBody, stmt.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), stmt.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 1eda92b7ee43b..9e823d11d5d39 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -1,10 +1,10 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::{Checker, TypingImporter}; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; +use ruff_python_ast::PythonVersion; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::PythonVersion; use ruff_python_semantic::analyze; use ruff_python_semantic::analyze::class::might_be_generic; use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload}; @@ -75,6 +75,16 @@ use ruff_text_size::Ranged; /// ## Fix safety /// This rule's fix is marked as unsafe as it changes the meaning of your type annotations. /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.11, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// ## References /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) #[derive(ViolationMetadata)] @@ -96,7 +106,9 @@ impl Violation for NonSelfReturnType { if matches!(class_name.as_str(), "__new__") { "`__new__` methods usually return `self` at runtime".to_string() } else { - format!("`{method_name}` methods in classes like `{class_name}` usually return `self` at runtime") + format!( + "`{method_name}` methods in classes like `{class_name}` usually return `self` at runtime" + ) } } @@ -192,7 +204,11 @@ fn add_diagnostic( class_def: &ast::StmtClassDef, method_name: &str, ) { - let mut diagnostic = Diagnostic::new( + let Some(importer) = checker.typing_importer("Self", PythonVersion::PY311) else { + return; + }; + + let mut diagnostic = checker.report_diagnostic( NonSelfReturnType { class_name: class_def.name.to_string(), method_name: method_name.to_string(), @@ -200,21 +216,19 @@ fn add_diagnostic( stmt.identifier(), ); - diagnostic.try_set_fix(|| replace_with_self_fix(checker, stmt, returns, class_def)); - - checker.report_diagnostic(diagnostic); + diagnostic.try_set_fix(|| { + replace_with_self_fix(checker.semantic(), &importer, stmt, returns, class_def) + }); } fn replace_with_self_fix( - checker: &Checker, + semantic: &SemanticModel, + importer: &TypingImporter, stmt: &ast::Stmt, returns: &ast::Expr, class_def: &ast::StmtClassDef, ) -> anyhow::Result { - let semantic = checker.semantic(); - - let (self_import, self_binding) = - checker.import_from_typing("Self", returns.start(), PythonVersion::PY311)?; + let (self_import, self_binding) = importer.import(returns.start())?; let mut others = Vec::with_capacity(2); @@ -230,7 +244,7 @@ fn replace_with_self_fix( others.extend(remove_first_argument_type_hint()); others.push(Edit::range_replacement(self_binding, returns.range())); - let applicability = if might_be_generic(class_def, checker.semantic()) { + let applicability = if might_be_generic(class_def, semantic) { Applicability::DisplayOnly } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs index fdf3afe311a2e..739778f4cf714 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/numeric_literal_too_long.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; use ruff_text_size::{Ranged, TextSize}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for numeric literals with a string representation longer than ten @@ -50,10 +50,9 @@ pub(crate) fn numeric_literal_too_long(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new(NumericLiteralTooLong, expr.range()); + let mut diagnostic = checker.report_diagnostic(NumericLiteralTooLong, expr.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs index 5c1363d17a093..2177f92e78dee 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_in_class_body.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for the presence of the `pass` statement in non-empty class bodies @@ -52,11 +52,10 @@ pub(crate) fn pass_in_class_body(checker: &Checker, class_def: &ast::StmtClassDe continue; } - let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range()); + let mut diagnostic = checker.report_diagnostic(PassInClassBody, stmt.range()); let edit = fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer()); diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_id(), ))); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs index f530b70259ea6..f4a7f64538408 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pass_statement_stub_body.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `pass` statements in empty stub bodies. @@ -44,10 +44,9 @@ pub(crate) fn pass_statement_stub_body(checker: &Checker, body: &[Stmt]) { return; }; - let mut diagnostic = Diagnostic::new(PassStatementStubBody, pass.range()); + let mut diagnostic = checker.report_diagnostic(PassStatementStubBody, pass.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), pass.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs index 46970f64f8a13..ec5fd871bb216 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/pre_pep570_positional_argument.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, ParameterWithDefault}; use ruff_python_semantic::analyze::function_type; +use crate::Violation; use crate::checkers::ast::Checker; use ruff_python_ast::PythonVersion; @@ -75,8 +75,8 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast &function_def.decorator_list, scope, semantic, - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); // If the method has a `self` or `cls` argument, skip it. @@ -87,10 +87,7 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast if let Some(arg) = function_def.parameters.args.get(skip) { if is_old_style_positional_only(arg) { - checker.report_diagnostic(Diagnostic::new( - Pep484StylePositionalOnlyParameter, - arg.identifier(), - )); + checker.report_diagnostic(Pep484StylePositionalOnlyParameter, arg.identifier()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs index 96fb143c21752..152c54671bce0 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -1,10 +1,10 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq, Copy, Clone)] @@ -106,5 +106,5 @@ pub(crate) fn prefix_type_params(checker: &Checker, value: &Expr, targets: &[Exp return; }; - checker.report_diagnostic(Diagnostic::new(UnprefixedTypeParam { kind }, value.range())); + checker.report_diagnostic(UnprefixedTypeParam { kind }, value.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs index 8b8390c20c935..63b610408b09a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs @@ -1,9 +1,9 @@ use ruff_text_size::TextRange; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for quoted type annotations in stub (`.pyi`) files, which should be avoided. @@ -44,10 +44,9 @@ impl AlwaysFixableViolation for QuotedAnnotationInStub { /// PYI020 pub(crate) fn quoted_annotation_in_stub(checker: &Checker, annotation: &str, range: TextRange) { - let mut diagnostic = Diagnostic::new(QuotedAnnotationInStub, range); + let mut diagnostic = checker.report_diagnostic(QuotedAnnotationInStub, range); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( annotation.to_string(), range, ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs index 47016c98b3da1..d710b4823f827 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_final_literal.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, comparable::ComparableExpr}; use ruff_text_size::{Ranged, TextSize}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for redundant `Final[Literal[...]]` annotations. @@ -97,7 +97,7 @@ pub(crate) fn redundant_final_literal(checker: &Checker, ann_assign: &ast::StmtA return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RedundantFinalLiteral { literal: SourceCodeSnippet::from_str(checker.locator().slice(literal.range())), }, @@ -113,8 +113,6 @@ pub(crate) fn redundant_final_literal(checker: &Checker, ann_assign: &ast::StmtA } else { diagnostic.set_fix(generate_fix(annotation, Some(literal), checker.locator())); } - - checker.report_diagnostic(diagnostic); } /// Generate a fix to convert a `Final[Literal[...]]` annotation to a `Final` annotation. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs index bfcfcef7ca2ac..512849b70d06a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_literal_union.rs @@ -2,13 +2,13 @@ use std::fmt; use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef}; -use ruff_python_semantic::analyze::typing::traverse_union; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; @@ -90,7 +90,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &Checker, union: &'a Expr) { }; if builtin_types_in_union.contains(&literal_type) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( RedundantLiteralUnion { literal: SourceCodeSnippet::from_str( checker.locator().slice(typing_literal_expr), @@ -98,7 +98,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &Checker, union: &'a Expr) { builtin_type: literal_type, }, typing_literal_expr.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 16a48bc5f5717..12c4b01fb954e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -1,11 +1,9 @@ use anyhow::Result; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ - self as ast, + self as ast, Expr, ExprBinOp, ExprContext, ExprNoneLiteral, Operator, PythonVersion, helpers::{pep_604_union, typing_optional}, name::Name, - Expr, ExprBinOp, ExprContext, ExprNoneLiteral, ExprSubscript, Operator, PythonVersion, }; use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union}; use ruff_text_size::{Ranged, TextRange}; @@ -13,6 +11,7 @@ use ruff_text_size::{Ranged, TextRange}; use smallvec::SmallVec; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for redundant `Literal[None]` annotations. @@ -121,7 +120,7 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex // N.B. Applying the fix can leave an unused import to be fixed by the `unused-import` rule. for none_expr in none_exprs { let mut diagnostic = - Diagnostic::new(RedundantNoneLiteral { union_kind }, none_expr.range()); + checker.report_diagnostic(RedundantNoneLiteral { union_kind }, none_expr.range()); diagnostic.try_set_optional_fix(|| { create_fix( checker, @@ -130,8 +129,13 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex literal_elements.clone(), union_kind, ) + // Isolate the fix to ensure multiple fixes on the same expression (like + // `Literal[None,] | Literal[None,]` -> `None | None`) happen across separate passes, + // preventing the production of invalid code. + .map(|fix| { + fix.map(|fix| fix.isolate(Checker::isolation(semantic.current_statement_id()))) + }) }); - checker.report_diagnostic(diagnostic); } } @@ -172,18 +176,9 @@ fn create_fix( traverse_union( &mut |expr, _| { - if matches!(expr, Expr::NoneLiteral(_)) { + if expr.is_none_literal_expr() { is_fixable = false; } - if expr != literal_expr { - if let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr { - if semantic.match_typing_expr(value, "Literal") - && matches!(**slice, Expr::NoneLiteral(_)) - { - is_fixable = false; - } - } - } }, semantic, enclosing_pep604_union, @@ -210,11 +205,13 @@ fn create_fix( let new_literal_expr = Expr::Subscript(ast::ExprSubscript { value: Box::new(literal_subscript.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, slice: Box::new(if literal_elements.len() > 1 { Expr::Tuple(ast::ExprTuple { elts: literal_elements.into_iter().cloned().collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, parenthesized: true, }) @@ -225,11 +222,11 @@ fn create_fix( let fix = match union_kind { UnionKind::TypingOptional => { - let (import_edit, bound_name) = checker.import_from_typing( - "Optional", - literal_expr.start(), - PythonVersion::lowest(), - )?; + let Some(importer) = checker.typing_importer("Optional", PythonVersion::lowest()) + else { + return Ok(None); + }; + let (import_edit, bound_name) = importer.import(literal_expr.start())?; let optional_expr = typing_optional(new_literal_expr, Name::from(bound_name)); let content = checker.generator().expr(&optional_expr); let optional_edit = Edit::range_replacement(content, literal_expr.range()); @@ -238,6 +235,7 @@ fn create_fix( UnionKind::BitOr => { let none_expr = Expr::NoneLiteral(ExprNoneLiteral { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let union_expr = pep_604_union(&[new_literal_expr, none_expr]); let content = checker.generator().expr(&union_expr); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 2f196c2cafb37..b493474929525 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -1,17 +1,14 @@ use bitflags::bitflags; -use anyhow::Result; - -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{ - name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, - Operator, Parameters, PythonVersion, -}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, Parameters, PythonVersion}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; + +use super::generate_union_fix; /// ## What it does /// Checks for parameter annotations that contain redundant unions between @@ -132,7 +129,17 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) { // Traverse the union a second time to construct a [`Fix`]. traverse_union(&mut remove_numeric_type, checker.semantic(), annotation); - let mut diagnostic = Diagnostic::new(RedundantNumericUnion { redundancy }, annotation.range()); + let mut diagnostic = + checker.report_diagnostic(RedundantNumericUnion { redundancy }, annotation.range()); + + if !checker.semantic().execution_context().is_typing() + && !checker.source_type.is_stub() + && fix_starts_with_none_none(&necessary_nodes) + { + // If there are multiple `None` literals, we cannot apply the fix in a runtime context. + // E.g., `None | None | int` will cause a `RuntimeError`. + return; + } // Mark [`Fix`] as unsafe when comments are in range. let applicability = if checker.comment_ranges().intersects(annotation.range()) { @@ -157,7 +164,18 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) { applicability, )), UnionKind::TypingUnion => { - generate_union_fix(checker, necessary_nodes, annotation, applicability).ok() + let Some(importer) = checker.typing_importer("Union", PythonVersion::lowest()) + else { + return; + }; + generate_union_fix( + checker.generator(), + &importer, + necessary_nodes, + annotation, + applicability, + ) + .ok() } } }; @@ -165,8 +183,6 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) { if let Some(fix) = fix { diagnostic.set_fix(fix); } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -245,6 +261,7 @@ fn generate_pep604_fix( op: Operator::BitOr, right: Box::new(right.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) } else { Some(right.clone()) @@ -258,39 +275,7 @@ fn generate_pep604_fix( ) } -/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. -fn generate_union_fix( - checker: &Checker, - nodes: Vec<&Expr>, - annotation: &Expr, - applicability: Applicability, -) -> Result { - debug_assert!(nodes.len() >= 2, "At least two nodes required"); - - // Request `typing.Union` - let (import_edit, binding) = - checker.import_from_typing("Optional", annotation.start(), PythonVersion::lowest())?; - - // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` - let new_expr = Expr::Subscript(ExprSubscript { - range: TextRange::default(), - value: Box::new(Expr::Name(ExprName { - id: Name::new(binding), - ctx: ExprContext::Store, - range: TextRange::default(), - })), - slice: Box::new(Expr::Tuple(ExprTuple { - elts: nodes.into_iter().cloned().collect(), - range: TextRange::default(), - ctx: ExprContext::Load, - parenthesized: false, - })), - ctx: ExprContext::Load, - }); - - Ok(Fix::applicable_edits( - Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()), - [import_edit], - applicability, - )) +/// Check whether the proposed fix starts with two `None` literals. +fn fix_starts_with_none_none(nodes: &[&Expr]) -> bool { + nodes.len() >= 2 && nodes.iter().take(2).all(|node| node.is_none_literal_expr()) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 3a2c05e4d1626..32059594db8be 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::PythonVersion; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, Operator, Parameters, Stmt, UnaryOp}; -use ruff_python_semantic::{analyze::class::is_enumeration, ScopeKind, SemanticModel}; +use ruff_python_semantic::{ScopeKind, SemanticModel, analyze::class::is_enumeration}; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::flake8_pyi::rules::TypingModule; -use crate::Locator; -use ruff_python_ast::PythonVersion; +use crate::{AlwaysFixableViolation, Edit, Fix, Violation}; /// ## What it does /// Checks for typed function arguments in stubs with complex default values. @@ -75,7 +75,7 @@ impl AlwaysFixableViolation for TypedArgumentDefaultInStub { /// ## Example /// /// ```pyi -/// def foo(arg=[]) -> None: ... +/// def foo(arg=bar()) -> None: ... /// ``` /// /// Use instead: @@ -120,7 +120,7 @@ impl AlwaysFixableViolation for ArgumentDefaultInStub { /// /// ## Example /// ```pyi -/// foo: str = "..." +/// foo: str = bar() /// ``` /// /// Use instead: @@ -190,7 +190,9 @@ impl Violation for UnassignedSpecialVariableInStub { #[derive_message_formats] fn message(&self) -> String { let UnassignedSpecialVariableInStub { name } = self; - format!("`{name}` in a stub file must have a value, as it has the same semantics as `{name}` at runtime") + format!( + "`{name}` in a stub file must have a value, as it has the same semantics as `{name}` at runtime" + ) } } @@ -217,6 +219,16 @@ impl Violation for UnassignedSpecialVariableInStub { /// /// Vector: TypeAlias = list[float] /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.10, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct TypeAliasWithoutAnnotation { module: TypingModule, @@ -288,7 +300,11 @@ fn is_valid_default_value_with_annotation( } Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) - | Expr::Set(ast::ExprSet { elts, range: _ }) => { + | Expr::Set(ast::ExprSet { + elts, + range: _, + node_index: _, + }) => { return allow_container && elts.len() <= 10 && elts @@ -308,6 +324,7 @@ fn is_valid_default_value_with_annotation( op: UnaryOp::USub, operand, range: _, + node_index: _, }) => { match operand.as_ref() { // Ex) `-1`, `-3.14`, `2j` @@ -330,6 +347,7 @@ fn is_valid_default_value_with_annotation( op: Operator::Add | Operator::Sub, right, range: _, + node_index: _, }) => { // Ex) `1 + 2j`, `1 - 2j`, `-1 - 2j`, `-1 + 2j` if let Expr::NumberLiteral(ast::ExprNumberLiteral { @@ -348,6 +366,7 @@ fn is_valid_default_value_with_annotation( op: UnaryOp::USub, operand, range: _, + node_index: _, }) = left.as_ref() { // Ex) `-1 + 2j`, `-1 - 2j` @@ -386,6 +405,7 @@ fn is_valid_pep_604_union(annotation: &Expr) -> bool { op: Operator::BitOr, right, range: _, + node_index: _, }) => is_valid_pep_604_union_member(left) && is_valid_pep_604_union_member(right), Expr::Name(_) | Expr::Subscript(_) | Expr::Attribute(_) | Expr::NoneLiteral(_) => true, _ => false, @@ -398,6 +418,7 @@ fn is_valid_pep_604_union(annotation: &Expr) -> bool { op: Operator::BitOr, right, range: _, + node_index: _, }) = annotation else { return false; @@ -497,14 +518,13 @@ pub(crate) fn typed_argument_simple_defaults(checker: &Checker, parameters: &Par checker.locator(), checker.semantic(), ) { - let mut diagnostic = Diagnostic::new(TypedArgumentDefaultInStub, default.range()); + let mut diagnostic = + checker.report_diagnostic(TypedArgumentDefaultInStub, default.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), default.range(), ))); - - checker.report_diagnostic(diagnostic); } } } @@ -523,14 +543,13 @@ pub(crate) fn argument_simple_defaults(checker: &Checker, parameters: &Parameter checker.locator(), checker.semantic(), ) { - let mut diagnostic = Diagnostic::new(ArgumentDefaultInStub, default.range()); + let mut diagnostic = + checker.report_diagnostic(ArgumentDefaultInStub, default.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), default.range(), ))); - - checker.report_diagnostic(diagnostic); } } } @@ -557,12 +576,11 @@ pub(crate) fn assignment_default_in_stub(checker: &Checker, targets: &[Expr], va return; } - let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, value.range()); + let mut diagnostic = checker.report_diagnostic(AssignmentDefaultInStub, value.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), value.range(), ))); - checker.report_diagnostic(diagnostic); } /// PYI015 @@ -591,12 +609,11 @@ pub(crate) fn annotated_assignment_default_in_stub( return; } - let mut diagnostic = Diagnostic::new(AssignmentDefaultInStub, value.range()); + let mut diagnostic = checker.report_diagnostic(AssignmentDefaultInStub, value.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), value.range(), ))); - checker.report_diagnostic(diagnostic); } /// PYI052 @@ -626,12 +643,12 @@ pub(crate) fn unannotated_assignment_in_stub(checker: &Checker, targets: &[Expr] return; } } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnannotatedAssignmentInStub { name: id.to_string(), }, value.range(), - )); + ); } /// PYI035 @@ -644,12 +661,12 @@ pub(crate) fn unassigned_special_variable_in_stub(checker: &Checker, target: &Ex return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnassignedSpecialVariableInStub { name: id.to_string(), }, stmt.range(), - )); + ); } /// PYI026 @@ -672,7 +689,11 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar TypingModule::TypingExtensions }; - let mut diagnostic = Diagnostic::new( + let Some(importer) = checker.typing_importer("TypeAlias", PythonVersion::PY310) else { + return; + }; + + let mut diagnostic = checker.report_diagnostic( TypeAliasWithoutAnnotation { module, name: id.to_string(), @@ -681,12 +702,10 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar target.range(), ); diagnostic.try_set_fix(|| { - let (import_edit, binding) = - checker.import_from_typing("TypeAlias", target.start(), PythonVersion::PY310)?; + let (import_edit, binding) = importer.import(target.start())?; Ok(Fix::safe_edits( Edit::range_replacement(format!("{id}: {binding}"), target.range()), [import_edit], )) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index b4cbe3d21285a..6cb7ceaa2e2aa 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -1,13 +1,13 @@ use ruff_python_ast as ast; use ruff_python_ast::Stmt; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_abstract; use crate::checkers::ast::Checker; use crate::fix::edits::delete_stmt; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for redundant definitions of `__str__` or `__repr__` in stubs. @@ -82,7 +82,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &Checker, stmt: &Stmt) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( StrOrReprDefinedInStub { name: name.to_string(), }, @@ -94,5 +94,4 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &Checker, stmt: &Stmt) { diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_parent_id(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index 1a9ad3a325d69..a739f91d36193 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::{self as ast, StringLike}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for the use of string and bytes literals longer than 50 characters @@ -67,17 +67,21 @@ pub(crate) fn string_or_bytes_too_long(checker: &Checker, string: StringLike) { StringLike::String(ast::ExprStringLiteral { value, .. }) => value.chars().count(), StringLike::Bytes(ast::ExprBytesLiteral { value, .. }) => value.len(), StringLike::FString(node) => count_f_string_chars(node), + // TODO(dylan): decide how to count chars, especially + // if interpolations are of different type than `str` + StringLike::TString(_) => { + return; + } }; if length <= 50 { return; } - let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, string.range()); + let mut diagnostic = checker.report_diagnostic(StringOrBytesTooLong, string.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "...".to_string(), string.range(), ))); - checker.report_diagnostic(diagnostic); } /// Count the number of visible characters in an f-string. This accounts for @@ -92,8 +96,10 @@ fn count_f_string_chars(f_string: &ast::ExprFString) -> usize { .elements .iter() .map(|element| match element { - ast::FStringElement::Literal(string) => string.chars().count(), - ast::FStringElement::Expression(expr) => expr.range().len().to_usize(), + ast::InterpolatedStringElement::Literal(string) => string.chars().count(), + ast::InterpolatedStringElement::Interpolation(expr) => { + expr.range().len().to_usize() + } }) .sum(), }) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs index 680f215cc14de..06b428d7d3a27 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::identifier::Identifier; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; +use ruff_python_ast::identifier::Identifier; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -41,9 +41,6 @@ impl Violation for StubBodyMultipleStatements { /// PYI048 pub(crate) fn stub_body_multiple_statements(checker: &Checker, stmt: &Stmt, body: &[Stmt]) { if body.len() > 1 { - checker.report_diagnostic(Diagnostic::new( - StubBodyMultipleStatements, - stmt.identifier(), - )); + checker.report_diagnostic(StubBodyMultipleStatements, stmt.identifier()); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs index d9646910c004d..7a2aba860ba76 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_alias_naming.rs @@ -1,8 +1,8 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -14,11 +14,15 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```pyi +/// from typing import TypeAlias +/// /// type_alias_name: TypeAlias = int /// ``` /// /// Use instead: /// ```pyi +/// from typing import TypeAlias +/// /// TypeAliasName: TypeAlias = int /// ``` #[derive(ViolationMetadata)] @@ -69,7 +73,9 @@ impl Violation for TSuffixedTypeAlias { #[derive_message_formats] fn message(&self) -> String { let Self { name } = self; - format!("Private type alias `{name}` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)") + format!( + "Private type alias `{name}` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)" + ) } } @@ -107,12 +113,12 @@ pub(crate) fn snake_case_type_alias(checker: &Checker, target: &Expr) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( SnakeCaseTypeAlias { name: id.to_string(), }, *range, - )); + ); } } @@ -123,11 +129,11 @@ pub(crate) fn t_suffixed_type_alias(checker: &Checker, target: &Expr) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TSuffixedTypeAlias { name: id.to_string(), }, *range, - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index 2000edeb950d1..2fe79461ed1af 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -2,11 +2,12 @@ use std::sync::LazyLock; use regex::Regex; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; /// ## What it does /// Checks for the use of type comments (e.g., `x = 1 # type: int`) in stub @@ -38,7 +39,7 @@ impl Violation for TypeCommentInStub { /// PYI033 pub(crate) fn type_comment_in_stub( - diagnostics: &mut Vec, + context: &LintContext, locator: &Locator, comment_ranges: &CommentRanges, ) { @@ -46,7 +47,7 @@ pub(crate) fn type_comment_in_stub( let comment = locator.slice(range); if TYPE_COMMENT_REGEX.is_match(comment) && !TYPE_IGNORE_REGEX.is_match(comment) { - diagnostics.push(Diagnostic::new(TypeCommentInStub, range)); + context.report_diagnostic_if_enabled(TypeCommentInStub, range); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index cc1cc6b942e47..ba3c63ac2f2ef 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -1,12 +1,11 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Imported; use ruff_python_semantic::{Binding, BindingKind, Scope}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; - use crate::renamer::Renamer; +use crate::{Applicability, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `from collections.abc import Set` imports that do not alias @@ -57,26 +56,24 @@ impl Violation for UnaliasedCollectionsAbcSetImport { } /// PYI025 -pub(crate) fn unaliased_collections_abc_set_import( - checker: &Checker, - binding: &Binding, -) -> Option { +pub(crate) fn unaliased_collections_abc_set_import(checker: &Checker, binding: &Binding) { let BindingKind::FromImport(import) = &binding.kind else { - return None; + return; }; if !matches!( import.qualified_name().segments(), ["collections", "abc", "Set"] ) { - return None; + return; } let name = binding.name(checker.source()); if name == "AbstractSet" { - return None; + return; } - let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range()); + let mut diagnostic = + checker.report_diagnostic(UnaliasedCollectionsAbcSetImport, binding.range()); if checker.semantic().is_available("AbstractSet") { diagnostic.try_set_fix(|| { let semantic = checker.semantic(); @@ -87,7 +84,6 @@ pub(crate) fn unaliased_collections_abc_set_import( Ok(Fix::applicable_edits(edit, rest, applicability)) }); } - Some(diagnostic) } fn determine_applicability(binding: &Binding, scope: &Scope, checker: &Checker) -> Applicability { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs index 10db113bb3077..0765eeb48d0ff 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::pep_604_union; use ruff_python_ast::{self as ast, Expr, ExprContext}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the presence of multiple literal types in a union. @@ -124,7 +124,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryLiteralUnion { members: literal_exprs .iter() @@ -140,10 +140,12 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) { slice: Box::new(Expr::Tuple(ast::ExprTuple { elts: literal_exprs.into_iter().cloned().collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, parenthesized: true, })), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, }); @@ -162,10 +164,12 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) { slice: Box::new(Expr::Tuple(ast::ExprTuple { elts, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, parenthesized: true, })), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, })) } else { @@ -180,6 +184,4 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) { Fix::safe_edit(edit) } }); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs index 4ad46eb85b1f3..56d838b950af2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs @@ -1,6 +1,5 @@ use ast::ExprContext; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::pep_604_union; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr}; @@ -8,6 +7,7 @@ use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the presence of multiple `type`s in a union. @@ -116,7 +116,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) { .map(|type_expr| Name::new(checker.locator().slice(type_expr))) .collect(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryTypeUnion { members: type_members.clone(), union_kind, @@ -134,10 +134,12 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) { id: Name::new_static("type"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), slice: Box::new(pep_604_union(&elts)), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); if other_exprs.is_empty() { @@ -157,6 +159,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) { id: Name::new_static("type"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), slice: Box::new(Expr::Subscript(ast::ExprSubscript { value: subscript.value.clone(), @@ -168,18 +171,22 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) { id: type_member, ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) }) .collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, })), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); if other_exprs.is_empty() { @@ -195,10 +202,12 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) { elts: exprs.into_iter().cloned().collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, })), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); checker.generator().expr(&union) @@ -218,8 +227,6 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &Checker, union: &'a Expr) { applicability, )); } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs index c5f2dafd9d643..d9183c09668b7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, CmpOp, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -21,7 +21,9 @@ use crate::registry::Rule; /// /// ## Example /// ```pyi -/// if sys.platform.startswith("linux"): +/// import sys +/// +/// if sys.platform == "xunil"[::-1]: /// # Linux specific definitions /// ... /// else: @@ -31,6 +33,8 @@ use crate::registry::Rule; /// /// Instead, use a simple string comparison, such as `==` or `!=`: /// ```pyi +/// import sys +/// /// if sys.platform == "linux": /// # Linux specific definitions /// ... @@ -65,11 +69,15 @@ impl Violation for UnrecognizedPlatformCheck { /// /// ## Example /// ```pyi +/// import sys +/// /// if sys.platform == "linus": ... /// ``` /// /// Use instead: /// ```pyi +/// import sys +/// /// if sys.platform == "linux": ... /// ``` /// @@ -114,28 +122,24 @@ pub(crate) fn unrecognized_platform(checker: &Checker, test: &Expr) { // "in" might also make sense but we don't currently have one. if !matches!(op, CmpOp::Eq | CmpOp::NotEq) { - if checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker.report_diagnostic(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); - } + checker.report_diagnostic_if_enabled(UnrecognizedPlatformCheck, test.range()); return; } if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = right { // Other values are possible but we don't need them right now. // This protects against typos. - if checker.enabled(Rule::UnrecognizedPlatformName) { + if checker.is_rule_enabled(Rule::UnrecognizedPlatformName) { if !matches!(value.to_str(), "linux" | "win32" | "cygwin" | "darwin") { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnrecognizedPlatformName { platform: value.to_string(), }, right.range(), - )); + ); } } } else { - if checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker.report_diagnostic(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); - } + checker.report_diagnostic_if_enabled(UnrecognizedPlatformCheck, test.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs index 72a20ed3c2caf..9188e865a8c05 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::{self as ast, CmpOp, Expr, Int}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -147,9 +147,7 @@ pub(crate) fn unrecognized_version_info(checker: &Checker, test: &Expr) { if let Some(expected) = ExpectedComparator::try_from(left) { version_check(checker, expected, test, *op, comparator); } else { - if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { - checker.report_diagnostic(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); - } + checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range()); } } @@ -163,37 +161,28 @@ fn version_check( // Single digit comparison, e.g., `sys.version_info[0] == 2`. if expected == ExpectedComparator::MajorDigit { if !is_int_constant(comparator) { - if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { - checker - .report_diagnostic(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); - } + checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range()); } return; } // Tuple comparison, e.g., `sys.version_info == (3, 4)`. let Expr::Tuple(tuple) = comparator else { - if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { - checker.report_diagnostic(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); - } + checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range()); return; }; if !tuple.iter().all(is_int_constant) { // All tuple elements must be integers, e.g., `sys.version_info == (3, 4)` instead of // `sys.version_info == (3.0, 4)`. - if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { - checker.report_diagnostic(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); - } + checker.report_diagnostic_if_enabled(UnrecognizedVersionInfoCheck, test.range()); } else if tuple.len() > 2 { // Must compare against major and minor version only, e.g., `sys.version_info == (3, 4)` // instead of `sys.version_info == (3, 4, 0)`. - if checker.enabled(Rule::PatchVersionComparison) { - checker.report_diagnostic(Diagnostic::new(PatchVersionComparison, test.range())); - } + checker.report_diagnostic_if_enabled(PatchVersionComparison, test.range()); } - if checker.enabled(Rule::WrongTupleLengthVersionComparison) { + if checker.is_rule_enabled(Rule::WrongTupleLengthVersionComparison) { if op == CmpOp::Eq || op == CmpOp::NotEq { let expected_length = match expected { ExpectedComparator::MajorTuple => 1, @@ -202,10 +191,10 @@ fn version_check( }; if tuple.len() != expected_length { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( WrongTupleLengthVersionComparison { expected_length }, test.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs index 625c9803fc49f..e745f67210a71 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unsupported_method_call_on_all.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -49,7 +49,9 @@ impl Violation for UnsupportedMethodCallOnAll { #[derive_message_formats] fn message(&self) -> String { let UnsupportedMethodCallOnAll { name } = self; - format!("Calling `.{name}()` on `__all__` may not be supported by all type checkers (use `+=` instead)") + format!( + "Calling `.{name}()` on `__all__` may not be supported by all type checkers (use `+=` instead)" + ) } } @@ -67,12 +69,12 @@ pub(crate) fn unsupported_method_call_on_all(checker: &Checker, func: &Expr) { if !is_unsupported_method(attr.as_str()) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnsupportedMethodCallOnAll { name: attr.to_string(), }, func.range(), - )); + ); } fn is_unsupported_method(name: &str) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs index 4b99c3998d71f..d5e6645642058 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_semantic::{Scope, SemanticModel}; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the presence of unused private `TypeVar`, `ParamSpec` or @@ -224,21 +224,20 @@ pub(crate) fn unused_private_type_var(checker: &Checker, scope: &Scope) { continue; }; - let diagnostic = Diagnostic::new( - UnusedPrivateTypeVar { - type_var_like_name: id.to_string(), - type_var_like_kind: type_var_like_kind.to_string(), - }, - binding.range(), - ) - .with_fix(Fix::unsafe_edit(fix::edits::delete_stmt( - stmt, - None, - checker.locator(), - checker.indexer(), - ))); - - checker.report_diagnostic(diagnostic); + checker + .report_diagnostic( + UnusedPrivateTypeVar { + type_var_like_name: id.to_string(), + type_var_like_kind: type_var_like_kind.to_string(), + }, + binding.range(), + ) + .set_fix(Fix::unsafe_edit(fix::edits::delete_stmt( + stmt, + None, + checker.locator(), + checker.indexer(), + ))); } } @@ -271,12 +270,12 @@ pub(crate) fn unused_private_protocol(checker: &Checker, scope: &Scope) { continue; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnusedPrivateProtocol { name: class_def.name.to_string(), }, binding.range(), - )); + ); } } @@ -303,12 +302,12 @@ pub(crate) fn unused_private_type_alias(checker: &Checker, scope: &Scope) { continue; }; - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnusedPrivateTypeAlias { name: alias_name.to_string(), }, binding.range(), - )); + ); } } @@ -358,12 +357,12 @@ pub(crate) fn unused_private_typed_dict(checker: &Checker, scope: &Scope) { continue; }; - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnusedPrivateTypedDict { name: class_name.to_string(), }, binding.range(), - )); + ); } } @@ -405,11 +404,7 @@ fn extract_typeddict_name<'a>(stmt: &'a Stmt, semantic: &SemanticModel) -> Optio }; let ast::ExprName { id, .. } = target.as_name_expr()?; let ast::ExprCall { func, .. } = value.as_call_expr()?; - if is_typeddict(func) { - Some(id) - } else { - None - } + if is_typeddict(func) { Some(id) } else { None } } _ => None, } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap index 3d671d859c533..8a51694b4a9b7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap @@ -914,4 +914,79 @@ PYI016.py:115:23: PYI016 [*] Duplicate union member `int` 115 |+field35: "int | str" # Error 116 116 | 117 117 | -118 118 | +118 118 | + +PYI016.py:134:45: PYI016 [*] Duplicate union member `typing.Optional[int]` + | +132 | field40: typing.Union[typing.Optional[int], None] +133 | field41: typing.Optional[typing.Union[int, None]] +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | ^^^^^^^^^^^^^^^^^^^^ PYI016 +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] + | + = help: Remove duplicate union member `typing.Optional[int]` + +ℹ Safe fix +131 131 | # equivalent to int | None +132 132 | field40: typing.Union[typing.Optional[int], None] +133 133 | field41: typing.Optional[typing.Union[int, None]] +134 |-field42: typing.Union[typing.Optional[int], typing.Optional[int]] + 134 |+field42: typing.Optional[int] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] +137 137 | field45: typing.Optional[int] | typing.Optional[int] + +PYI016.py:137:33: PYI016 [*] Duplicate union member `typing.Optional[int]` + | +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] +137 | field45: typing.Optional[int] | typing.Optional[int] + | ^^^^^^^^^^^^^^^^^^^^ PYI016 +138 | # equivalent to int | dict | None +139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | + = help: Remove duplicate union member `typing.Optional[int]` + +ℹ Safe fix +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] +137 |-field45: typing.Optional[int] | typing.Optional[int] + 137 |+field45: typing.Optional[int] +138 138 | # equivalent to int | dict | None +139 139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +140 140 | field47: typing.Optional[int] | typing.Optional[dict] + +PYI016.py:143:61: PYI016 [*] Duplicate union member `complex` + | +142 | # avoid reporting twice +143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + | ^^^^^^^ PYI016 +144 | field49: typing.Optional[complex | complex] | complex + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +140 140 | field47: typing.Optional[int] | typing.Optional[dict] +141 141 | +142 142 | # avoid reporting twice +143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + 143 |+field48: typing.Union[typing.Optional[complex], complex] +144 144 | field49: typing.Optional[complex | complex] | complex + +PYI016.py:144:36: PYI016 [*] Duplicate union member `complex` + | +142 | # avoid reporting twice +143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +144 | field49: typing.Optional[complex | complex] | complex + | ^^^^^^^ PYI016 + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +141 141 | +142 142 | # avoid reporting twice +143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +144 |-field49: typing.Optional[complex | complex] | complex + 144 |+field49: typing.Optional[complex] | complex diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap index 0f14b06acb8d6..cd373340682cc 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs -snapshot_kind: text --- PYI016.pyi:7:15: PYI016 [*] Duplicate union member `str` | @@ -883,6 +882,8 @@ PYI016.pyi:113:61: PYI016 [*] Duplicate union member `list[int]` 112 | # Test case for mixed union type 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error | ^^^^^^^^^ PYI016 +114 | +115 | # https://github.com/astral-sh/ruff/issues/18546 | = help: Remove duplicate union member `list[int]` @@ -892,3 +893,81 @@ PYI016.pyi:113:61: PYI016 [*] Duplicate union member `list[int]` 112 112 | # Test case for mixed union type 113 |-field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error 113 |+field34: typing.Union[list[int], str, bytes] # Error +114 114 | +115 115 | # https://github.com/astral-sh/ruff/issues/18546 +116 116 | # Expand Optional[T] to Union[T, None] + +PYI016.pyi:125:45: PYI016 [*] Duplicate union member `typing.Optional[int]` + | +123 | field40: typing.Union[typing.Optional[int], None] +124 | field41: typing.Optional[typing.Union[int, None]] +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | ^^^^^^^^^^^^^^^^^^^^ PYI016 +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] + | + = help: Remove duplicate union member `typing.Optional[int]` + +ℹ Safe fix +122 122 | # equivalent to int | None +123 123 | field40: typing.Union[typing.Optional[int], None] +124 124 | field41: typing.Optional[typing.Union[int, None]] +125 |-field42: typing.Union[typing.Optional[int], typing.Optional[int]] + 125 |+field42: typing.Optional[int] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] +128 128 | field45: typing.Optional[int] | typing.Optional[int] + +PYI016.pyi:128:33: PYI016 [*] Duplicate union member `typing.Optional[int]` + | +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] +128 | field45: typing.Optional[int] | typing.Optional[int] + | ^^^^^^^^^^^^^^^^^^^^ PYI016 +129 | # equivalent to int | dict | None +130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | + = help: Remove duplicate union member `typing.Optional[int]` + +ℹ Safe fix +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] +128 |-field45: typing.Optional[int] | typing.Optional[int] + 128 |+field45: typing.Optional[int] +129 129 | # equivalent to int | dict | None +130 130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +131 131 | field47: typing.Optional[int] | typing.Optional[dict] + +PYI016.pyi:134:61: PYI016 [*] Duplicate union member `complex` + | +133 | # avoid reporting twice +134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + | ^^^^^^^ PYI016 +135 | field49: typing.Optional[complex | complex] | complex + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +131 131 | field47: typing.Optional[int] | typing.Optional[dict] +132 132 | +133 133 | # avoid reporting twice +134 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + 134 |+field48: typing.Union[typing.Optional[complex], complex] +135 135 | field49: typing.Optional[complex | complex] | complex + +PYI016.pyi:135:36: PYI016 [*] Duplicate union member `complex` + | +133 | # avoid reporting twice +134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +135 | field49: typing.Optional[complex | complex] | complex + | ^^^^^^^ PYI016 + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +132 132 | +133 133 | # avoid reporting twice +134 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +135 |-field49: typing.Optional[complex | complex] | complex + 135 |+field49: typing.Optional[complex] | complex diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap index 1bbfbef8e2c36..4071c9595fbb8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.py.snap @@ -690,3 +690,96 @@ PYI019_0.py:173:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` 174 174 | type S = int 175 175 | print(S) # not a reference to the type variable, so not touched by the autofix 176 176 | return 42 + +PYI019_0.py:189:52: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +188 | class BadClassWithStringTypeHints: +189 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 +190 | +191 | @classmethod + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +186 186 | from __future__ import annotations +187 187 | +188 188 | class BadClassWithStringTypeHints: +189 |- def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + 189 |+ def bad_instance_method_with_string_annotations(self, arg: str) -> "Self": ... # PYI019 +190 190 | +191 191 | @classmethod +192 192 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + +PYI019_0.py:192:49: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +191 | @classmethod +192 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +189 189 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 +190 190 | +191 191 | @classmethod +192 |- def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + 192 |+ def bad_class_method_with_string_annotations(cls) -> "Self": ... # PYI019 +193 193 | +194 194 | +195 195 | @classmethod + +PYI019_0.py:196:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +195 | @classmethod +196 | def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +193 193 | +194 194 | +195 195 | @classmethod +196 |- def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + 196 |+ def bad_class_method_with_mixed_annotations_1(cls) -> Self: ... # PYI019 +197 197 | +198 198 | +199 199 | @classmethod + +PYI019_0.py:200:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +199 | @classmethod +200 | def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +197 197 | +198 198 | +199 199 | @classmethod +200 |- def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + 200 |+ def bad_class_method_with_mixed_annotations_1(cls) -> "Self": ... # PYI019 +201 201 | +202 202 | +203 203 | class BadSubscriptReturnTypeWithStringTypeHints: + +PYI019_0.py:205:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` + | +203 | class BadSubscriptReturnTypeWithStringTypeHints: +204 | @classmethod +205 | def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `S` with `Self` + +ℹ Safe fix +202 202 | +203 203 | class BadSubscriptReturnTypeWithStringTypeHints: +204 204 | @classmethod +205 |- def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + 205 |+ def m(cls) -> "type[Self]": ... # PYI019 +206 206 | +207 207 | +208 208 | class GoodClassWiStringTypeHints: diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap index 7c5b794bd5621..9b4647f3fe363 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI019_PYI019_0.pyi.snap @@ -682,3 +682,96 @@ PYI019_0.pyi:167:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` 168 168 | 169 169 | 170 170 | MetaType = TypeVar("MetaType") + +PYI019_0.pyi:181:52: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +180 | class BadClassWithStringTypeHints: +181 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 +182 | +183 | @classmethod + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +178 178 | +179 179 | +180 180 | class BadClassWithStringTypeHints: +181 |- def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 + 181 |+ def bad_instance_method_with_string_annotations(self, arg: str) -> "Self": ... # PYI019 +182 182 | +183 183 | @classmethod +184 184 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + +PYI019_0.pyi:184:49: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +183 | @classmethod +184 | def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +181 181 | def bad_instance_method_with_string_annotations(self: "_S", arg: str) -> "_S": ... # PYI019 +182 182 | +183 183 | @classmethod +184 |- def bad_class_method_with_string_annotations(cls: "type[_S]") -> "_S": ... # PYI019 + 184 |+ def bad_class_method_with_string_annotations(cls) -> "Self": ... # PYI019 +185 185 | +186 186 | +187 187 | @classmethod + +PYI019_0.pyi:188:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +187 | @classmethod +188 | def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +185 185 | +186 186 | +187 187 | @classmethod +188 |- def bad_class_method_with_mixed_annotations_1(cls: "type[_S]") -> _S: ... # PYI019 + 188 |+ def bad_class_method_with_mixed_annotations_1(cls) -> Self: ... # PYI019 +189 189 | +190 190 | +191 191 | @classmethod + +PYI019_0.pyi:192:50: PYI019 [*] Use `Self` instead of custom TypeVar `_S` + | +191 | @classmethod +192 | def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `_S` with `Self` + +ℹ Safe fix +189 189 | +190 190 | +191 191 | @classmethod +192 |- def bad_class_method_with_mixed_annotations_1(cls: type[_S]) -> "_S": ... # PYI019 + 192 |+ def bad_class_method_with_mixed_annotations_1(cls) -> "Self": ... # PYI019 +193 193 | +194 194 | +195 195 | class BadSubscriptReturnTypeWithStringTypeHints: + +PYI019_0.pyi:197:10: PYI019 [*] Use `Self` instead of custom TypeVar `S` + | +195 | class BadSubscriptReturnTypeWithStringTypeHints: +196 | @classmethod +197 | def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019 + | + = help: Replace TypeVar `S` with `Self` + +ℹ Safe fix +194 194 | +195 195 | class BadSubscriptReturnTypeWithStringTypeHints: +196 196 | @classmethod +197 |- def m[S](cls: "type[S]") -> "type[S]": ... # PYI019 + 197 |+ def m(cls) -> "type[Self]": ... # PYI019 +198 198 | +199 199 | +200 200 | class GoodClassWithStringTypeHints: diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap deleted file mode 100644 index b4680e7da885a..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.py.snap +++ /dev/null @@ -1,311 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs -snapshot_kind: text ---- -PYI041.py:22:14: PYI041 [*] Use `float` instead of `int | float` - | -22 | def f0(arg1: float | int) -> None: - | ^^^^^^^^^^^ PYI041 -23 | ... - | - = help: Remove redundant type - -ℹ Safe fix -19 19 | ... -20 20 | -21 21 | -22 |-def f0(arg1: float | int) -> None: - 22 |+def f0(arg1: float) -> None: -23 23 | ... -24 24 | -25 25 | - -PYI041.py:26:30: PYI041 [*] Use `complex` instead of `float | complex` - | -26 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -27 | ... - | - = help: Remove redundant type - -ℹ Safe fix -23 23 | ... -24 24 | -25 25 | -26 |-def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: - 26 |+def f1(arg1: float, *, arg2: list[str] | type[bool] | complex) -> None: -27 27 | ... -28 28 | -29 29 | - -PYI041.py:30:28: PYI041 [*] Use `float` instead of `int | float` - | -30 | def f2(arg1: int, /, arg2: int | int | float) -> None: - | ^^^^^^^^^^^^^^^^^ PYI041 -31 | ... - | - = help: Remove redundant type - -ℹ Safe fix -27 27 | ... -28 28 | -29 29 | -30 |-def f2(arg1: int, /, arg2: int | int | float) -> None: - 30 |+def f2(arg1: int, /, arg2: float) -> None: -31 31 | ... -32 32 | -33 33 | - -PYI041.py:34:26: PYI041 [*] Use `float` instead of `int | float` - | -34 | def f3(arg1: int, *args: Union[int | int | float]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -35 | ... - | - = help: Remove redundant type - -ℹ Safe fix -31 31 | ... -32 32 | -33 33 | -34 |-def f3(arg1: int, *args: Union[int | int | float]) -> None: - 34 |+def f3(arg1: int, *args: float) -> None: -35 35 | ... -36 36 | -37 37 | - -PYI041.py:38:24: PYI041 [*] Use `float` instead of `int | float` - | -38 | async def f4(**kwargs: int | int | float) -> None: - | ^^^^^^^^^^^^^^^^^ PYI041 -39 | ... - | - = help: Remove redundant type - -ℹ Safe fix -35 35 | ... -36 36 | -37 37 | -38 |-async def f4(**kwargs: int | int | float) -> None: - 38 |+async def f4(**kwargs: float) -> None: -39 39 | ... -40 40 | -41 41 | - -PYI041.py:42:26: PYI041 [*] Use `float` instead of `int | float` - | -42 | def f5(arg1: int, *args: Union[int, int, float]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ PYI041 -43 | ... - | - = help: Remove redundant type - -ℹ Safe fix -39 39 | ... -40 40 | -41 41 | -42 |-def f5(arg1: int, *args: Union[int, int, float]) -> None: - 42 |+def f5(arg1: int, *args: float) -> None: -43 43 | ... -44 44 | -45 45 | - -PYI041.py:46:26: PYI041 [*] Use `float` instead of `int | float` - | -46 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -47 | ... - | - = help: Remove redundant type - -ℹ Safe fix -43 43 | ... -44 44 | -45 45 | -46 |-def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: - 46 |+def f6(arg1: int, *args: float) -> None: -47 47 | ... -48 48 | -49 49 | - -PYI041.py:50:26: PYI041 [*] Use `float` instead of `int | float` - | -50 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -51 | ... - | - = help: Remove redundant type - -ℹ Safe fix -47 47 | ... -48 48 | -49 49 | -50 |-def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: - 50 |+def f7(arg1: int, *args: float) -> None: -51 51 | ... -52 52 | -53 53 | - -PYI041.py:54:26: PYI041 [*] Use `float` instead of `int | float` - | -54 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -55 | ... - | - = help: Remove redundant type - -ℹ Safe fix -51 51 | ... -52 52 | -53 53 | -54 |-def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: - 54 |+def f8(arg1: int, *args: float) -> None: -55 55 | ... -56 56 | -57 57 | - -PYI041.py:59:10: PYI041 [*] Use `complex` instead of `int | float | complex` - | -58 | def f9( -59 | arg: Union[ # comment - | __________^ -60 | | float, # another -61 | | complex, int] - | |_____________________^ PYI041 -62 | ) -> None: -63 | ... - | - = help: Remove redundant type - -ℹ Unsafe fix -56 56 | -57 57 | -58 58 | def f9( -59 |- arg: Union[ # comment -60 |- float, # another -61 |- complex, int] - 59 |+ arg: complex -62 60 | ) -> None: -63 61 | ... -64 62 | - -PYI041.py:67:9: PYI041 [*] Use `complex` instead of `int | float | complex` - | -65 | def f10( -66 | arg: ( -67 | / int | # comment -68 | | float | # another -69 | | complex - | |_______________^ PYI041 -70 | ) -71 | ) -> None: - | - = help: Remove redundant type - -ℹ Unsafe fix -64 64 | -65 65 | def f10( -66 66 | arg: ( -67 |- int | # comment -68 |- float | # another -69 67 | complex -70 68 | ) -71 69 | ) -> None: - -PYI041.py:79:24: PYI041 [*] Use `complex` instead of `int | float | complex` - | -77 | ... -78 | -79 | def bad(self, arg: int | float | complex) -> None: - | ^^^^^^^^^^^^^^^^^^^^^ PYI041 -80 | ... - | - = help: Remove redundant type - -ℹ Safe fix -76 76 | def good(self, arg: int) -> None: -77 77 | ... -78 78 | -79 |- def bad(self, arg: int | float | complex) -> None: - 79 |+ def bad(self, arg: complex) -> None: -80 80 | ... -81 81 | -82 82 | def bad2(self, arg: int | Union[float, complex]) -> None: - -PYI041.py:82:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -80 | ... -81 | -82 | def bad2(self, arg: int | Union[float, complex]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -83 | ... - | - = help: Remove redundant type - -ℹ Safe fix -79 79 | def bad(self, arg: int | float | complex) -> None: -80 80 | ... -81 81 | -82 |- def bad2(self, arg: int | Union[float, complex]) -> None: - 82 |+ def bad2(self, arg: complex) -> None: -83 83 | ... -84 84 | -85 85 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: - -PYI041.py:85:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -83 | ... -84 | -85 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -86 | ... - | - = help: Remove redundant type - -ℹ Safe fix -82 82 | def bad2(self, arg: int | Union[float, complex]) -> None: -83 83 | ... -84 84 | -85 |- def bad3(self, arg: Union[Union[float, complex], int]) -> None: - 85 |+ def bad3(self, arg: complex) -> None: -86 86 | ... -87 87 | -88 88 | def bad4(self, arg: Union[float | complex, int]) -> None: - -PYI041.py:88:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -86 | ... -87 | -88 | def bad4(self, arg: Union[float | complex, int]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -89 | ... - | - = help: Remove redundant type - -ℹ Safe fix -85 85 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: -86 86 | ... -87 87 | -88 |- def bad4(self, arg: Union[float | complex, int]) -> None: - 88 |+ def bad4(self, arg: complex) -> None: -89 89 | ... -90 90 | -91 91 | def bad5(self, arg: int | (float | complex)) -> None: - -PYI041.py:91:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -89 | ... -90 | -91 | def bad5(self, arg: int | (float | complex)) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -92 | ... - | - = help: Remove redundant type - -ℹ Safe fix -88 88 | def bad4(self, arg: Union[float | complex, int]) -> None: -89 89 | ... -90 90 | -91 |- def bad5(self, arg: int | (float | complex)) -> None: - 91 |+ def bad5(self, arg: complex) -> None: -92 92 | ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap deleted file mode 100644 index dbf365cb5bdc7..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041.pyi.snap +++ /dev/null @@ -1,306 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs -snapshot_kind: text ---- -PYI041.pyi:21:14: PYI041 [*] Use `float` instead of `int | float` - | -21 | def f0(arg1: float | int) -> None: ... # PYI041 - | ^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -18 18 | def good2(arg: int, arg2: int | bool) -> None: ... -19 19 | -20 20 | -21 |-def f0(arg1: float | int) -> None: ... # PYI041 - 21 |+def f0(arg1: float) -> None: ... # PYI041 -22 22 | -23 23 | -24 24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 - -PYI041.pyi:24:30: PYI041 [*] Use `complex` instead of `float | complex` - | -24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -21 21 | def f0(arg1: float | int) -> None: ... # PYI041 -22 22 | -23 23 | -24 |-def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 - 24 |+def f1(arg1: float, *, arg2: list[str] | type[bool] | complex) -> None: ... # PYI041 -25 25 | -26 26 | -27 27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 - -PYI041.pyi:27:28: PYI041 [*] Use `float` instead of `int | float` - | -27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -24 24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 -25 25 | -26 26 | -27 |-def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 - 27 |+def f2(arg1: int, /, arg2: float) -> None: ... # PYI041 -28 28 | -29 29 | -30 30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 - -PYI041.pyi:30:26: PYI041 [*] Use `float` instead of `int | float` - | -30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -27 27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 -28 28 | -29 29 | -30 |-def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 - 30 |+def f3(arg1: int, *args: float) -> None: ... # PYI041 -31 31 | -32 32 | -33 33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 - -PYI041.pyi:33:24: PYI041 [*] Use `float` instead of `int | float` - | -33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^ PYI041 -34 | -35 | def f5( - | - = help: Remove redundant type - -ℹ Safe fix -30 30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 -31 31 | -32 32 | -33 |-async def f4(**kwargs: int | int | float) -> None: ... # PYI041 - 33 |+async def f4(**kwargs: float) -> None: ... # PYI041 -34 34 | -35 35 | def f5( -36 36 | arg: Union[ # comment - -PYI041.pyi:36:10: PYI041 [*] Use `complex` instead of `int | float | complex` - | -35 | def f5( -36 | arg: Union[ # comment - | __________^ -37 | | float, # another -38 | | complex, int] - | |_____________________^ PYI041 -39 | ) -> None: ... # PYI041 - | - = help: Remove redundant type - -ℹ Unsafe fix -33 33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 -34 34 | -35 35 | def f5( -36 |- arg: Union[ # comment -37 |- float, # another -38 |- complex, int] - 36 |+ arg: complex -39 37 | ) -> None: ... # PYI041 -40 38 | -41 39 | def f6( - -PYI041.pyi:43:9: PYI041 [*] Use `complex` instead of `int | float | complex` - | -41 | def f6( -42 | arg: ( -43 | / int | # comment -44 | | float | # another -45 | | complex - | |_______________^ PYI041 -46 | ) -47 | ) -> None: ... # PYI041 - | - = help: Remove redundant type - -ℹ Unsafe fix -40 40 | -41 41 | def f6( -42 42 | arg: ( -43 |- int | # comment -44 |- float | # another -45 43 | complex -46 44 | ) -47 45 | ) -> None: ... # PYI041 - -PYI041.pyi:49:26: PYI041 [*] Use `float` instead of `int | float` - | -47 | ) -> None: ... # PYI041 -48 | -49 | def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -46 46 | ) -47 47 | ) -> None: ... # PYI041 -48 48 | -49 |-def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 - 49 |+def f5(arg1: int, *args: float) -> None: ... # PYI041 -50 50 | -51 51 | -52 52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 - -PYI041.pyi:52:26: PYI041 [*] Use `float` instead of `int | float` - | -52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -49 49 | def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 -50 50 | -51 51 | -52 |-def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 - 52 |+def f6(arg1: int, *args: float) -> None: ... # PYI041 -53 53 | -54 54 | -55 55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 - -PYI041.pyi:55:26: PYI041 [*] Use `float` instead of `int | float` - | -55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -52 52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 -53 53 | -54 54 | -55 |-def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 - 55 |+def f7(arg1: int, *args: float) -> None: ... # PYI041 -56 56 | -57 57 | -58 58 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 - -PYI041.pyi:58:26: PYI041 [*] Use `float` instead of `int | float` - | -58 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -55 55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 -56 56 | -57 57 | -58 |-def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 - 58 |+def f8(arg1: int, *args: float) -> None: ... # PYI041 -59 59 | -60 60 | -61 61 | class Foo: - -PYI041.pyi:64:24: PYI041 [*] Use `complex` instead of `int | float | complex` - | -62 | def good(self, arg: int) -> None: ... -63 | -64 | def bad(self, arg: int | float | complex) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^ PYI041 -65 | -66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -61 61 | class Foo: -62 62 | def good(self, arg: int) -> None: ... -63 63 | -64 |- def bad(self, arg: int | float | complex) -> None: ... # PYI041 - 64 |+ def bad(self, arg: complex) -> None: ... # PYI041 -65 65 | -66 66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 -67 67 | - -PYI041.pyi:66:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -64 | def bad(self, arg: int | float | complex) -> None: ... # PYI041 -65 | -66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -67 | -68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -63 63 | -64 64 | def bad(self, arg: int | float | complex) -> None: ... # PYI041 -65 65 | -66 |- def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 - 66 |+ def bad2(self, arg: complex) -> None: ... # PYI041 -67 67 | -68 68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 -69 69 | - -PYI041.pyi:68:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 -67 | -68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -69 | -70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -65 65 | -66 66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 -67 67 | -68 |- def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 - 68 |+ def bad3(self, arg: complex) -> None: ... # PYI041 -69 69 | -70 70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 -71 71 | - -PYI041.pyi:70:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 -69 | -70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 -71 | -72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -67 67 | -68 68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 -69 69 | -70 |- def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 - 70 |+ def bad4(self, arg: complex) -> None: ... # PYI041 -71 71 | -72 72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 - -PYI041.pyi:72:25: PYI041 [*] Use `complex` instead of `int | float | complex` - | -70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 -71 | -72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 - | ^^^^^^^^^^^^^^^^^^^^^^^ PYI041 - | - = help: Remove redundant type - -ℹ Safe fix -69 69 | -70 70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 -71 71 | -72 |- def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 - 72 |+ def bad5(self, arg: complex) -> None: ... # PYI041 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_1.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_1.py.snap new file mode 100644 index 0000000000000..a161e8d6f8ad3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_1.py.snap @@ -0,0 +1,361 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs +--- +PYI041_1.py:23:14: PYI041 [*] Use `float` instead of `int | float` + | +23 | def f0(arg1: float | int) -> None: + | ^^^^^^^^^^^ PYI041 +24 | ... + | + = help: Remove redundant type + +ℹ Safe fix +20 20 | ... +21 21 | +22 22 | +23 |-def f0(arg1: float | int) -> None: + 23 |+def f0(arg1: float) -> None: +24 24 | ... +25 25 | +26 26 | + +PYI041_1.py:27:30: PYI041 [*] Use `complex` instead of `float | complex` + | +27 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +28 | ... + | + = help: Remove redundant type + +ℹ Safe fix +24 24 | ... +25 25 | +26 26 | +27 |-def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: + 27 |+def f1(arg1: float, *, arg2: list[str] | type[bool] | complex) -> None: +28 28 | ... +29 29 | +30 30 | + +PYI041_1.py:31:28: PYI041 [*] Use `float` instead of `int | float` + | +31 | def f2(arg1: int, /, arg2: int | int | float) -> None: + | ^^^^^^^^^^^^^^^^^ PYI041 +32 | ... + | + = help: Remove redundant type + +ℹ Safe fix +28 28 | ... +29 29 | +30 30 | +31 |-def f2(arg1: int, /, arg2: int | int | float) -> None: + 31 |+def f2(arg1: int, /, arg2: float) -> None: +32 32 | ... +33 33 | +34 34 | + +PYI041_1.py:35:26: PYI041 [*] Use `float` instead of `int | float` + | +35 | def f3(arg1: int, *args: Union[int | int | float]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +36 | ... + | + = help: Remove redundant type + +ℹ Safe fix +32 32 | ... +33 33 | +34 34 | +35 |-def f3(arg1: int, *args: Union[int | int | float]) -> None: + 35 |+def f3(arg1: int, *args: float) -> None: +36 36 | ... +37 37 | +38 38 | + +PYI041_1.py:39:24: PYI041 [*] Use `float` instead of `int | float` + | +39 | async def f4(**kwargs: int | int | float) -> None: + | ^^^^^^^^^^^^^^^^^ PYI041 +40 | ... + | + = help: Remove redundant type + +ℹ Safe fix +36 36 | ... +37 37 | +38 38 | +39 |-async def f4(**kwargs: int | int | float) -> None: + 39 |+async def f4(**kwargs: float) -> None: +40 40 | ... +41 41 | +42 42 | + +PYI041_1.py:43:26: PYI041 [*] Use `float` instead of `int | float` + | +43 | def f5(arg1: int, *args: Union[int, int, float]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ PYI041 +44 | ... + | + = help: Remove redundant type + +ℹ Safe fix +40 40 | ... +41 41 | +42 42 | +43 |-def f5(arg1: int, *args: Union[int, int, float]) -> None: + 43 |+def f5(arg1: int, *args: float) -> None: +44 44 | ... +45 45 | +46 46 | + +PYI041_1.py:47:26: PYI041 [*] Use `float` instead of `int | float` + | +47 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +48 | ... + | + = help: Remove redundant type + +ℹ Safe fix +44 44 | ... +45 45 | +46 46 | +47 |-def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: + 47 |+def f6(arg1: int, *args: float) -> None: +48 48 | ... +49 49 | +50 50 | + +PYI041_1.py:51:26: PYI041 [*] Use `float` instead of `int | float` + | +51 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +52 | ... + | + = help: Remove redundant type + +ℹ Safe fix +48 48 | ... +49 49 | +50 50 | +51 |-def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: + 51 |+def f7(arg1: int, *args: float) -> None: +52 52 | ... +53 53 | +54 54 | + +PYI041_1.py:55:26: PYI041 [*] Use `float` instead of `int | float` + | +55 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +56 | ... + | + = help: Remove redundant type + +ℹ Safe fix +52 52 | ... +53 53 | +54 54 | +55 |-def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: + 55 |+def f8(arg1: int, *args: float) -> None: +56 56 | ... +57 57 | +58 58 | + +PYI041_1.py:60:10: PYI041 [*] Use `complex` instead of `int | float | complex` + | +59 | def f9( +60 | arg: Union[ # comment + | __________^ +61 | | float, # another +62 | | complex, int] + | |_____________________^ PYI041 +63 | ) -> None: +64 | ... + | + = help: Remove redundant type + +ℹ Unsafe fix +57 57 | +58 58 | +59 59 | def f9( +60 |- arg: Union[ # comment +61 |- float, # another +62 |- complex, int] + 60 |+ arg: complex +63 61 | ) -> None: +64 62 | ... +65 63 | + +PYI041_1.py:68:9: PYI041 [*] Use `complex` instead of `int | float | complex` + | +66 | def f10( +67 | arg: ( +68 | / int | # comment +69 | | float | # another +70 | | complex + | |_______________^ PYI041 +71 | ) +72 | ) -> None: + | + = help: Remove redundant type + +ℹ Unsafe fix +65 65 | +66 66 | def f10( +67 67 | arg: ( +68 |- int | # comment +69 |- float | # another +70 68 | complex +71 69 | ) +72 70 | ) -> None: + +PYI041_1.py:80:24: PYI041 [*] Use `complex` instead of `int | float | complex` + | +78 | ... +79 | +80 | def bad(self, arg: int | float | complex) -> None: + | ^^^^^^^^^^^^^^^^^^^^^ PYI041 +81 | ... + | + = help: Remove redundant type + +ℹ Safe fix +77 77 | def good(self, arg: int) -> None: +78 78 | ... +79 79 | +80 |- def bad(self, arg: int | float | complex) -> None: + 80 |+ def bad(self, arg: complex) -> None: +81 81 | ... +82 82 | +83 83 | def bad2(self, arg: int | Union[float, complex]) -> None: + +PYI041_1.py:83:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +81 | ... +82 | +83 | def bad2(self, arg: int | Union[float, complex]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +84 | ... + | + = help: Remove redundant type + +ℹ Safe fix +80 80 | def bad(self, arg: int | float | complex) -> None: +81 81 | ... +82 82 | +83 |- def bad2(self, arg: int | Union[float, complex]) -> None: + 83 |+ def bad2(self, arg: complex) -> None: +84 84 | ... +85 85 | +86 86 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: + +PYI041_1.py:86:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +84 | ... +85 | +86 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +87 | ... + | + = help: Remove redundant type + +ℹ Safe fix +83 83 | def bad2(self, arg: int | Union[float, complex]) -> None: +84 84 | ... +85 85 | +86 |- def bad3(self, arg: Union[Union[float, complex], int]) -> None: + 86 |+ def bad3(self, arg: complex) -> None: +87 87 | ... +88 88 | +89 89 | def bad4(self, arg: Union[float | complex, int]) -> None: + +PYI041_1.py:89:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +87 | ... +88 | +89 | def bad4(self, arg: Union[float | complex, int]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +90 | ... + | + = help: Remove redundant type + +ℹ Safe fix +86 86 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: +87 87 | ... +88 88 | +89 |- def bad4(self, arg: Union[float | complex, int]) -> None: + 89 |+ def bad4(self, arg: complex) -> None: +90 90 | ... +91 91 | +92 92 | def bad5(self, arg: int | (float | complex)) -> None: + +PYI041_1.py:92:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +90 | ... +91 | +92 | def bad5(self, arg: int | (float | complex)) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +93 | ... + | + = help: Remove redundant type + +ℹ Safe fix +89 89 | def bad4(self, arg: Union[float | complex, int]) -> None: +90 90 | ... +91 91 | +92 |- def bad5(self, arg: int | (float | complex)) -> None: + 92 |+ def bad5(self, arg: complex) -> None: +93 93 | ... +94 94 | +95 95 | + +PYI041_1.py:99:23: PYI041 Use `float` instead of `int | float` + | + 97 | # fix must not yield runtime `None | None | ...` (TypeError) + 98 | class Issue18298: + 99 | def f1(self, arg: None | int | None | float = None) -> None: # PYI041 - no fix + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +100 | pass + | + = help: Remove redundant type + +PYI041_1.py:104:27: PYI041 [*] Use `float` instead of `int | float` + | +102 | if TYPE_CHECKING: +103 | +104 | def f2(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +105 | +106 | else: + | + = help: Remove redundant type + +ℹ Safe fix +101 101 | +102 102 | if TYPE_CHECKING: +103 103 | +104 |- def f2(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix + 104 |+ def f2(self, arg: None | None | float = None) -> None: ... # PYI041 - with fix +105 105 | +106 106 | else: +107 107 | + +PYI041_1.py:111:23: PYI041 [*] Use `float` instead of `int | float` + | +109 | pass +110 | +111 | def f3(self, arg: None | float | None | int | None = None) -> None: # PYI041 - with fix + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +112 | pass + | + = help: Remove redundant type + +ℹ Safe fix +108 108 | def f2(self, arg=None) -> None: +109 109 | pass +110 110 | +111 |- def f3(self, arg: None | float | None | int | None = None) -> None: # PYI041 - with fix + 111 |+ def f3(self, arg: None | float | None | None = None) -> None: # PYI041 - with fix +112 112 | pass diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_1.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_1.pyi.snap new file mode 100644 index 0000000000000..69f8f6fd36eb6 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_1.pyi.snap @@ -0,0 +1,345 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs +--- +PYI041_1.pyi:21:14: PYI041 [*] Use `float` instead of `int | float` + | +21 | def f0(arg1: float | int) -> None: ... # PYI041 + | ^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +18 18 | def good2(arg: int, arg2: int | bool) -> None: ... +19 19 | +20 20 | +21 |-def f0(arg1: float | int) -> None: ... # PYI041 + 21 |+def f0(arg1: float) -> None: ... # PYI041 +22 22 | +23 23 | +24 24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 + +PYI041_1.pyi:24:30: PYI041 [*] Use `complex` instead of `float | complex` + | +24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +21 21 | def f0(arg1: float | int) -> None: ... # PYI041 +22 22 | +23 23 | +24 |-def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 + 24 |+def f1(arg1: float, *, arg2: list[str] | type[bool] | complex) -> None: ... # PYI041 +25 25 | +26 26 | +27 27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 + +PYI041_1.pyi:27:28: PYI041 [*] Use `float` instead of `int | float` + | +27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +24 24 | def f1(arg1: float, *, arg2: float | list[str] | type[bool] | complex) -> None: ... # PYI041 +25 25 | +26 26 | +27 |-def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 + 27 |+def f2(arg1: int, /, arg2: float) -> None: ... # PYI041 +28 28 | +29 29 | +30 30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 + +PYI041_1.pyi:30:26: PYI041 [*] Use `float` instead of `int | float` + | +30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +27 27 | def f2(arg1: int, /, arg2: int | int | float) -> None: ... # PYI041 +28 28 | +29 29 | +30 |-def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 + 30 |+def f3(arg1: int, *args: float) -> None: ... # PYI041 +31 31 | +32 32 | +33 33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 + +PYI041_1.pyi:33:24: PYI041 [*] Use `float` instead of `int | float` + | +33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^ PYI041 +34 | +35 | def f5( + | + = help: Remove redundant type + +ℹ Safe fix +30 30 | def f3(arg1: int, *args: Union[int | int | float]) -> None: ... # PYI041 +31 31 | +32 32 | +33 |-async def f4(**kwargs: int | int | float) -> None: ... # PYI041 + 33 |+async def f4(**kwargs: float) -> None: ... # PYI041 +34 34 | +35 35 | def f5( +36 36 | arg: Union[ # comment + +PYI041_1.pyi:36:10: PYI041 [*] Use `complex` instead of `int | float | complex` + | +35 | def f5( +36 | arg: Union[ # comment + | __________^ +37 | | float, # another +38 | | complex, int] + | |_____________________^ PYI041 +39 | ) -> None: ... # PYI041 + | + = help: Remove redundant type + +ℹ Unsafe fix +33 33 | async def f4(**kwargs: int | int | float) -> None: ... # PYI041 +34 34 | +35 35 | def f5( +36 |- arg: Union[ # comment +37 |- float, # another +38 |- complex, int] + 36 |+ arg: complex +39 37 | ) -> None: ... # PYI041 +40 38 | +41 39 | def f6( + +PYI041_1.pyi:43:9: PYI041 [*] Use `complex` instead of `int | float | complex` + | +41 | def f6( +42 | arg: ( +43 | / int | # comment +44 | | float | # another +45 | | complex + | |_______________^ PYI041 +46 | ) +47 | ) -> None: ... # PYI041 + | + = help: Remove redundant type + +ℹ Unsafe fix +40 40 | +41 41 | def f6( +42 42 | arg: ( +43 |- int | # comment +44 |- float | # another +45 43 | complex +46 44 | ) +47 45 | ) -> None: ... # PYI041 + +PYI041_1.pyi:49:26: PYI041 [*] Use `float` instead of `int | float` + | +47 | ) -> None: ... # PYI041 +48 | +49 | def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +46 46 | ) +47 47 | ) -> None: ... # PYI041 +48 48 | +49 |-def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 + 49 |+def f5(arg1: int, *args: float) -> None: ... # PYI041 +50 50 | +51 51 | +52 52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 + +PYI041_1.pyi:52:26: PYI041 [*] Use `float` instead of `int | float` + | +52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +49 49 | def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041 +50 50 | +51 51 | +52 |-def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 + 52 |+def f6(arg1: int, *args: float) -> None: ... # PYI041 +53 53 | +54 54 | +55 55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 + +PYI041_1.pyi:55:26: PYI041 [*] Use `float` instead of `int | float` + | +55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +52 52 | def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041 +53 53 | +54 54 | +55 |-def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 + 55 |+def f7(arg1: int, *args: float) -> None: ... # PYI041 +56 56 | +57 57 | +58 58 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 + +PYI041_1.pyi:58:26: PYI041 [*] Use `float` instead of `int | float` + | +58 | def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +55 55 | def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041 +56 56 | +57 57 | +58 |-def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041 + 58 |+def f8(arg1: int, *args: float) -> None: ... # PYI041 +59 59 | +60 60 | +61 61 | class Foo: + +PYI041_1.pyi:64:24: PYI041 [*] Use `complex` instead of `int | float | complex` + | +62 | def good(self, arg: int) -> None: ... +63 | +64 | def bad(self, arg: int | float | complex) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^ PYI041 +65 | +66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +61 61 | class Foo: +62 62 | def good(self, arg: int) -> None: ... +63 63 | +64 |- def bad(self, arg: int | float | complex) -> None: ... # PYI041 + 64 |+ def bad(self, arg: complex) -> None: ... # PYI041 +65 65 | +66 66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 +67 67 | + +PYI041_1.pyi:66:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +64 | def bad(self, arg: int | float | complex) -> None: ... # PYI041 +65 | +66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +67 | +68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +63 63 | +64 64 | def bad(self, arg: int | float | complex) -> None: ... # PYI041 +65 65 | +66 |- def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 + 66 |+ def bad2(self, arg: complex) -> None: ... # PYI041 +67 67 | +68 68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 +69 69 | + +PYI041_1.pyi:68:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 +67 | +68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +69 | +70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +65 65 | +66 66 | def bad2(self, arg: int | Union[float, complex]) -> None: ... # PYI041 +67 67 | +68 |- def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 + 68 |+ def bad3(self, arg: complex) -> None: ... # PYI041 +69 69 | +70 70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 +71 71 | + +PYI041_1.pyi:70:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 +69 | +70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +71 | +72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +67 67 | +68 68 | def bad3(self, arg: Union[Union[float, complex], int]) -> None: ... # PYI041 +69 69 | +70 |- def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 + 70 |+ def bad4(self, arg: complex) -> None: ... # PYI041 +71 71 | +72 72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 +73 73 | + +PYI041_1.pyi:72:25: PYI041 [*] Use `complex` instead of `int | float | complex` + | +70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 +71 | +72 | def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +69 69 | +70 70 | def bad4(self, arg: Union[float | complex, int]) -> None: ... # PYI041 +71 71 | +72 |- def bad5(self, arg: int | (float | complex)) -> None: ... # PYI041 + 72 |+ def bad5(self, arg: complex) -> None: ... # PYI041 +73 73 | +74 74 | +75 75 | # https://github.com/astral-sh/ruff/issues/18298 + +PYI041_1.pyi:78:23: PYI041 [*] Use `float` instead of `int | float` + | +76 | # fix must not yield runtime `None | None | ...` (TypeError) +77 | class Issue18298: +78 | def f1(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +79 | +80 | def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix + | + = help: Remove redundant type + +ℹ Safe fix +75 75 | # https://github.com/astral-sh/ruff/issues/18298 +76 76 | # fix must not yield runtime `None | None | ...` (TypeError) +77 77 | class Issue18298: +78 |- def f1(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix + 78 |+ def f1(self, arg: None | None | float = None) -> None: ... # PYI041 - with fix +79 79 | +80 80 | def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix + +PYI041_1.pyi:80:23: PYI041 [*] Use `float` instead of `int | float` + | +78 | def f1(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix +79 | +80 | def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 + | + = help: Remove redundant type + +ℹ Safe fix +77 77 | class Issue18298: +78 78 | def f1(self, arg: None | int | None | float = None) -> None: ... # PYI041 - with fix +79 79 | +80 |- def f3(self, arg: None | float | None | int | None = None) -> None: ... # PYI041 - with fix + 80 |+ def f3(self, arg: None | float | None | None = None) -> None: ... # PYI041 - with fix diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_2.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_2.py.snap new file mode 100644 index 0000000000000..10c39077a244d --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI041_PYI041_2.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs +--- +PYI041_2.py:7:23: PYI041 Use `float` instead of `int | float` + | +5 | # fix must not yield runtime `None | None | ...` (TypeError) +6 | class Issue18298: +7 | def f1(self, arg: None | int | None | float = None) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI041 +8 | pass + | + = help: Remove redundant type diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap index bb018c27edb74..4ae21763be417 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs --- -PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics +PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics | 1 | # Bad import. 2 | from __future__ import annotations # PYI044. @@ -10,7 +10,14 @@ PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stu | = help: Remove `from __future__ import annotations` -PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics +ℹ Safe fix +1 1 | # Bad import. +2 |-from __future__ import annotations # PYI044. +3 2 | from __future__ import annotations, with_statement # PYI044. +4 3 | +5 4 | # Good imports. + +PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics | 1 | # Bad import. 2 | from __future__ import annotations # PYI044. @@ -20,3 +27,12 @@ PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stu 5 | # Good imports. | = help: Remove `from __future__ import annotations` + +ℹ Safe fix +1 1 | # Bad import. +2 2 | from __future__ import annotations # PYI044. +3 |-from __future__ import annotations, with_statement # PYI044. + 3 |+from __future__ import with_statement # PYI044. +4 4 | +5 5 | # Good imports. +6 6 | from __future__ import with_statement diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.py.snap index bc91fb867cef4..d5f7860983c0c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.py.snap @@ -12,7 +12,7 @@ PYI059.py:8:17: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 5 5 | K = TypeVar('K') 6 6 | V = TypeVar('V') 7 7 | @@ -37,7 +37,7 @@ PYI059.py:15:16: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 13 13 | self._items.append(item) 14 14 | 15 15 | class MyMapping( # PYI059 @@ -59,7 +59,7 @@ PYI059.py:26:10: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 23 23 | # Inheriting from just `Generic` is a TypeError, but it's probably fine 24 24 | # to flag this issue in this case as well, since after fixing the error 25 25 | # the Generic's position issue persists. @@ -88,7 +88,7 @@ PYI059.py:30:10: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 30 30 | class Foo( # comment about the bracket 31 31 | # Part 1 of multiline comment 1 32 32 | # Part 2 of multiline comment 1 @@ -111,3 +111,53 @@ PYI059.py:45:8: PYI059 `Generic[]` should always be the last base class | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI059 | = help: Move `Generic[]` to the end + +PYI059.py:59:9: PYI059 [*] `Generic[]` should always be the last base class + | +57 | # syntax errors with starred and keyword arguments from +58 | # https://github.com/astral-sh/ruff/issues/18602 +59 | class C1(Generic[T], str, **{"metaclass": type}): # PYI059 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI059 +60 | ... + | + = help: Move `Generic[]` to the end + +ℹ Unsafe fix +56 56 | +57 57 | # syntax errors with starred and keyword arguments from +58 58 | # https://github.com/astral-sh/ruff/issues/18602 +59 |-class C1(Generic[T], str, **{"metaclass": type}): # PYI059 + 59 |+class C1(str, Generic[T], **{"metaclass": type}): # PYI059 +60 60 | ... +61 61 | +62 62 | class C2(Generic[T], str, metaclass=type): # PYI059 + +PYI059.py:62:9: PYI059 [*] `Generic[]` should always be the last base class + | +60 | ... +61 | +62 | class C2(Generic[T], str, metaclass=type): # PYI059 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI059 +63 | ... + | + = help: Move `Generic[]` to the end + +ℹ Unsafe fix +59 59 | class C1(Generic[T], str, **{"metaclass": type}): # PYI059 +60 60 | ... +61 61 | +62 |-class C2(Generic[T], str, metaclass=type): # PYI059 + 62 |+class C2(str, Generic[T], metaclass=type): # PYI059 +63 63 | ... +64 64 | +65 65 | class C3(Generic[T], metaclass=type, *[str]): # PYI059 but no fix + +PYI059.py:65:9: PYI059 `Generic[]` should always be the last base class + | +63 | ... +64 | +65 | class C3(Generic[T], metaclass=type, *[str]): # PYI059 but no fix + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI059 +66 | ... + | + = help: Move `Generic[]` to the end diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.pyi.snap index 275dd20dbaa08..16c5c72d08705 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI059_PYI059.pyi.snap @@ -12,7 +12,7 @@ PYI059.pyi:8:17: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 5 5 | K = TypeVar('K') 6 6 | V = TypeVar('V') 7 7 | @@ -37,7 +37,7 @@ PYI059.pyi:12:16: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 10 10 | def push(self, item: T) -> None: ... 11 11 | 12 12 | class MyMapping( # PYI059 @@ -58,7 +58,7 @@ PYI059.pyi:22:10: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 19 19 | # Inheriting from just `Generic` is a TypeError, but it's probably fine 20 20 | # to flag this issue in this case as well, since after fixing the error 21 21 | # the Generic's position issue persists. @@ -87,7 +87,7 @@ PYI059.pyi:25:10: PYI059 [*] `Generic[]` should always be the last base class | = help: Move `Generic[]` to the end -ℹ Safe fix +ℹ Unsafe fix 25 25 | class Foo( # comment about the bracket 26 26 | # Part 1 of multiline comment 1 27 27 | # Part 2 of multiline comment 1 diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap index 33bf166e8b796..32cfb3f84c2e4 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap @@ -422,7 +422,6 @@ PYI061.py:79:20: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) | ^^^^ PYI061 80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -432,24 +431,5 @@ PYI061.py:80:28: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) 80 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -81 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.py:81:12: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.py:81:28: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap index a2c845c0649b8..6a48501c8dad8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap @@ -291,7 +291,6 @@ PYI061.pyi:54:20: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) | ^^^^ PYI061 55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -301,24 +300,5 @@ PYI061.pyi:55:28: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) 55 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -56 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.pyi:56:12: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.pyi:56:28: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI016_PYI016.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI016_PYI016.py.snap new file mode 100644 index 0000000000000..8b49997fcc5f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI016_PYI016.py.snap @@ -0,0 +1,1213 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs +--- +PYI016.py:7:15: PYI016 [*] Duplicate union member `str` + | +6 | # Should emit for duplicate field types. +7 | field2: str | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +8 | +9 | # Should emit for union types in arguments. + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +4 4 | field1: str +5 5 | +6 6 | # Should emit for duplicate field types. +7 |-field2: str | str # PYI016: Duplicate union member `str` + 7 |+field2: str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` + +PYI016.py:10:23: PYI016 [*] Duplicate union member `int` + | + 9 | # Should emit for union types in arguments. +10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` + | ^^^ PYI016 +11 | print(arg1) + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +7 7 | field2: str | str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int` + 10 |+def func1(arg1: int): # PYI016: Duplicate union member `int` +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. + +PYI016.py:14:22: PYI016 [*] Duplicate union member `str` + | +13 | # Should emit for unions in return types. +14 | def func2() -> str | str: # PYI016: Duplicate union member `str` + | ^^^ PYI016 +15 | return "my string" + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. +14 |-def func2() -> str | str: # PYI016: Duplicate union member `str` + 14 |+def func2() -> str: # PYI016: Duplicate union member `str` +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. + +PYI016.py:18:15: PYI016 [*] Duplicate union member `str` + | +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` + | ^^^ PYI016 +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 |-field3: str | str | int # PYI016: Duplicate union member `str` + 18 |+field3: str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + +PYI016.py:19:15: PYI016 [*] Duplicate union member `int` + | +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` + | ^^^ PYI016 +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 |-field4: int | int | str # PYI016: Duplicate union member `int` + 19 |+field4: int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | + +PYI016.py:20:21: PYI016 [*] Duplicate union member `str` + | +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 |-field5: str | int | str # PYI016: Duplicate union member `str` + 20 |+field5: str | int # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. + +PYI016.py:21:28: PYI016 [*] Duplicate union member `int` + | +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | ^^^ PYI016 +22 | +23 | # Shouldn't emit for non-type unions. + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 |-field6: int | bool | str | int # PYI016: Duplicate union member `int` + 21 |+field6: int | bool | str # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. +24 24 | field7 = str | str + +PYI016.py:27:22: PYI016 [*] Duplicate union member `int` + | +26 | # Should emit for strangely-bracketed unions. +27 | field8: int | (str | int) # PYI016: Duplicate union member `int` + | ^^^ PYI016 +28 | +29 | # Should handle user brackets when fixing. + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +24 24 | field7 = str | str +25 25 | +26 26 | # Should emit for strangely-bracketed unions. +27 |-field8: int | (str | int) # PYI016: Duplicate union member `int` + 27 |+field8: int | str # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` + +PYI016.py:30:16: PYI016 [*] Duplicate union member `int` + | +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` + | ^^^ PYI016 +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +27 27 | field8: int | (str | int) # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 |-field9: int | (int | str) # PYI016: Duplicate union member `int` + 30 |+field9: int | str # PYI016: Duplicate union member `int` +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. + +PYI016.py:31:24: PYI016 [*] Duplicate union member `str` + | +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +32 | +33 | # Should emit for nested unions. + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 |-field10: (str | int) | str # PYI016: Duplicate union member `str` + 31 |+field10: str | int # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 34 | field11: dict[int | int, str] + +PYI016.py:34:21: PYI016 [*] Duplicate union member `int` + | +33 | # Should emit for nested unions. +34 | field11: dict[int | int, str] + | ^^^ PYI016 +35 | +36 | # Should emit for unions with more than two cases + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 |-field11: dict[int | int, str] + 34 |+field11: dict[int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error + +PYI016.py:37:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.py:37:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.py:38:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.py:38:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.py:38:28: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.py:41:16: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | str # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.py:41:28: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | str # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.py:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]` + | +43 | # Should emit for duplicate literal types; also covered by PYI030 +44 | field15: typing.Literal[1] | typing.Literal[1] # Error + | ^^^^^^^^^^^^^^^^^ PYI016 +45 | +46 | # Shouldn't emit if in new parent type + | + = help: Remove duplicate union member `typing.Literal[1]` + +ℹ Safe fix +41 41 | field14: int | int | str | int # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 |-field15: typing.Literal[1] | typing.Literal[1] # Error + 44 |+field15: typing.Literal[1] # Error +45 45 | +46 46 | # Shouldn't emit if in new parent type +47 47 | field16: int | dict[int, str] # OK + +PYI016.py:57:5: PYI016 [*] Duplicate union member `set[int]` + | +55 | int # foo +56 | ], +57 | / set[ +58 | | int # bar +59 | | ], + | |_____^ PYI016 +60 | ] # Error, newline and comment will not be emitted in message + | + = help: Remove duplicate union member `set[int]` + +ℹ Unsafe fix +50 50 | field17: dict[int, int] # OK +51 51 | +52 52 | # Should emit in cases with newlines +53 |-field18: typing.Union[ +54 |- set[ +55 |- int # foo +56 |- ], +57 |- set[ +58 |- int # bar +59 |- ], +60 |-] # Error, newline and comment will not be emitted in message + 53 |+field18: set[int] # Error, newline and comment will not be emitted in message +61 54 | +62 55 | # Should emit in cases with `typing.Union` instead of `|` +63 56 | field19: typing.Union[int, int] # Error + +PYI016.py:63:28: PYI016 [*] Duplicate union member `int` + | +62 | # Should emit in cases with `typing.Union` instead of `|` +63 | field19: typing.Union[int, int] # Error + | ^^^ PYI016 +64 | +65 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +60 60 | ] # Error, newline and comment will not be emitted in message +61 61 | +62 62 | # Should emit in cases with `typing.Union` instead of `|` +63 |-field19: typing.Union[int, int] # Error + 63 |+field19: int # Error +64 64 | +65 65 | # Should emit in cases with nested `typing.Union` +66 66 | field20: typing.Union[int, typing.Union[int, str]] # Error + +PYI016.py:66:41: PYI016 [*] Duplicate union member `int` + | +65 | # Should emit in cases with nested `typing.Union` +66 | field20: typing.Union[int, typing.Union[int, str]] # Error + | ^^^ PYI016 +67 | +68 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +63 63 | field19: typing.Union[int, int] # Error +64 64 | +65 65 | # Should emit in cases with nested `typing.Union` +66 |-field20: typing.Union[int, typing.Union[int, str]] # Error + 66 |+field20: typing.Union[int, str] # Error +67 67 | +68 68 | # Should emit in cases with mixed `typing.Union` and `|` +69 69 | field21: typing.Union[int, int | str] # Error + +PYI016.py:69:28: PYI016 [*] Duplicate union member `int` + | +68 | # Should emit in cases with mixed `typing.Union` and `|` +69 | field21: typing.Union[int, int | str] # Error + | ^^^ PYI016 +70 | +71 | # Should emit only once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +66 66 | field20: typing.Union[int, typing.Union[int, str]] # Error +67 67 | +68 68 | # Should emit in cases with mixed `typing.Union` and `|` +69 |-field21: typing.Union[int, int | str] # Error + 69 |+field21: int | str # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + +PYI016.py:72:41: PYI016 [*] Duplicate union member `int` + | +71 | # Should emit only once in cases with multiple nested `typing.Union` +72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 +73 | +74 | # Should emit in cases with newlines + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +69 69 | field21: typing.Union[int, int | str] # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 |-field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + 72 |+field22: int # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 75 | field23: set[ # foo + +PYI016.py:72:59: PYI016 [*] Duplicate union member `int` + | +71 | # Should emit only once in cases with multiple nested `typing.Union` +72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 +73 | +74 | # Should emit in cases with newlines + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +69 69 | field21: typing.Union[int, int | str] # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 |-field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + 72 |+field22: int # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 75 | field23: set[ # foo + +PYI016.py:72:64: PYI016 [*] Duplicate union member `int` + | +71 | # Should emit only once in cases with multiple nested `typing.Union` +72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 +73 | +74 | # Should emit in cases with newlines + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +69 69 | field21: typing.Union[int, int | str] # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 |-field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + 72 |+field22: int # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 75 | field23: set[ # foo + +PYI016.py:76:12: PYI016 [*] Duplicate union member `set[int]` + | +74 | # Should emit in cases with newlines +75 | field23: set[ # foo +76 | int] | set[int] + | ^^^^^^^^ PYI016 +77 | +78 | # Should emit twice (once for each `int` in the nested union, both of which are + | + = help: Remove duplicate union member `set[int]` + +ℹ Unsafe fix +72 72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 |-field23: set[ # foo +76 |- int] | set[int] + 75 |+field23: set[int] +77 76 | +78 77 | # Should emit twice (once for each `int` in the nested union, both of which are +79 78 | # duplicates of the outer `int`), but not three times (which would indicate that + +PYI016.py:81:41: PYI016 [*] Duplicate union member `int` + | +79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 | # we incorrectly re-checked the nested union). +81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +82 | +83 | # Should emit twice (once for each `int` in the nested union, both of which are + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +78 78 | # Should emit twice (once for each `int` in the nested union, both of which are +79 79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 80 | # we incorrectly re-checked the nested union). +81 |-field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + 81 |+field24: int # PYI016: Duplicate union member `int` +82 82 | +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that + +PYI016.py:81:46: PYI016 [*] Duplicate union member `int` + | +79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 | # we incorrectly re-checked the nested union). +81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +82 | +83 | # Should emit twice (once for each `int` in the nested union, both of which are + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +78 78 | # Should emit twice (once for each `int` in the nested union, both of which are +79 79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 80 | # we incorrectly re-checked the nested union). +81 |-field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + 81 |+field24: int # PYI016: Duplicate union member `int` +82 82 | +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that + +PYI016.py:86:28: PYI016 [*] Duplicate union member `int` + | +84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 | # we incorrectly re-checked the nested union). +86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +87 | +88 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 85 | # we incorrectly re-checked the nested union). +86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + 86 |+field25: int # PYI016: Duplicate union member `int` +87 87 | +88 88 | # Should emit in cases with nested `typing.Union` +89 89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + +PYI016.py:86:34: PYI016 [*] Duplicate union member `int` + | +84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 | # we incorrectly re-checked the nested union). +86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +87 | +88 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 85 | # we incorrectly re-checked the nested union). +86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + 86 |+field25: int # PYI016: Duplicate union member `int` +87 87 | +88 88 | # Should emit in cases with nested `typing.Union` +89 89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + +PYI016.py:89:41: PYI016 [*] Duplicate union member `int` + | +88 | # Should emit in cases with nested `typing.Union` +89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +90 | +91 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +86 86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` +87 87 | +88 88 | # Should emit in cases with nested `typing.Union` +89 |-field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + 89 |+field26: int # PYI016: Duplicate union member `int` +90 90 | +91 91 | # Should emit in cases with nested `typing.Union` +92 92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + +PYI016.py:92:54: PYI016 [*] Duplicate union member `int` + | +91 | # Should emit in cases with nested `typing.Union` +92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +93 | +94 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +89 89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` +90 90 | +91 91 | # Should emit in cases with nested `typing.Union` +92 |-field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + 92 |+field27: int # PYI016: Duplicate union member `int` +93 93 | +94 94 | # Should emit in cases with mixed `typing.Union` and `|` +95 95 | field28: typing.Union[int | int] # Error + +PYI016.py:95:29: PYI016 [*] Duplicate union member `int` + | +94 | # Should emit in cases with mixed `typing.Union` and `|` +95 | field28: typing.Union[int | int] # Error + | ^^^ PYI016 +96 | +97 | # Should emit twice in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +92 92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` +93 93 | +94 94 | # Should emit in cases with mixed `typing.Union` and `|` +95 |-field28: typing.Union[int | int] # Error + 95 |+field28: int # Error +96 96 | +97 97 | # Should emit twice in cases with multiple nested `typing.Union` +98 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + +PYI016.py:98:54: PYI016 [*] Duplicate union member `int` + | + 97 | # Should emit twice in cases with multiple nested `typing.Union` + 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + | ^^^ PYI016 + 99 | +100 | # Should emit once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +95 95 | field28: typing.Union[int | int] # Error +96 96 | +97 97 | # Should emit twice in cases with multiple nested `typing.Union` +98 |-field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + 98 |+field29: int # Error +99 99 | +100 100 | # Should emit once in cases with multiple nested `typing.Union` +101 101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + +PYI016.py:98:59: PYI016 [*] Duplicate union member `int` + | + 97 | # Should emit twice in cases with multiple nested `typing.Union` + 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + | ^^^ PYI016 + 99 | +100 | # Should emit once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +95 95 | field28: typing.Union[int | int] # Error +96 96 | +97 97 | # Should emit twice in cases with multiple nested `typing.Union` +98 |-field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + 98 |+field29: int # Error +99 99 | +100 100 | # Should emit once in cases with multiple nested `typing.Union` +101 101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + +PYI016.py:101:54: PYI016 [*] Duplicate union member `int` + | +100 | # Should emit once in cases with multiple nested `typing.Union` +101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + | ^^^ PYI016 +102 | +103 | # Should emit once, and fix to `typing.Union[float, int]` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +98 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error +99 99 | +100 100 | # Should emit once in cases with multiple nested `typing.Union` +101 |-field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + 101 |+field30: typing.Union[int, str] # Error +102 102 | +103 103 | # Should emit once, and fix to `typing.Union[float, int]` +104 104 | field31: typing.Union[float, typing.Union[int | int]] # Error + +PYI016.py:104:49: PYI016 [*] Duplicate union member `int` + | +103 | # Should emit once, and fix to `typing.Union[float, int]` +104 | field31: typing.Union[float, typing.Union[int | int]] # Error + | ^^^ PYI016 +105 | +106 | # Should emit once, and fix to `typing.Union[float, int]` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +101 101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error +102 102 | +103 103 | # Should emit once, and fix to `typing.Union[float, int]` +104 |-field31: typing.Union[float, typing.Union[int | int]] # Error + 104 |+field31: float | int # Error +105 105 | +106 106 | # Should emit once, and fix to `typing.Union[float, int]` +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error + +PYI016.py:107:49: PYI016 [*] Duplicate union member `int` + | +106 | # Should emit once, and fix to `typing.Union[float, int]` +107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error + | ^^^ PYI016 +108 | +109 | # Test case for mixed union type fix + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +104 104 | field31: typing.Union[float, typing.Union[int | int]] # Error +105 105 | +106 106 | # Should emit once, and fix to `typing.Union[float, int]` +107 |-field32: typing.Union[float, typing.Union[int | int | int]] # Error + 107 |+field32: float | int # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + +PYI016.py:107:55: PYI016 [*] Duplicate union member `int` + | +106 | # Should emit once, and fix to `typing.Union[float, int]` +107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error + | ^^^ PYI016 +108 | +109 | # Test case for mixed union type fix + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +104 104 | field31: typing.Union[float, typing.Union[int | int]] # Error +105 105 | +106 106 | # Should emit once, and fix to `typing.Union[float, int]` +107 |-field32: typing.Union[float, typing.Union[int | int | int]] # Error + 107 |+field32: float | int # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + +PYI016.py:110:42: PYI016 [*] Duplicate union member `int` + | +109 | # Test case for mixed union type fix +110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 |-field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + 110 |+field33: int # Error +111 111 | +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +PYI016.py:110:62: PYI016 [*] Duplicate union member `int` + | +109 | # Test case for mixed union type fix +110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 |-field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + 110 |+field33: int # Error +111 111 | +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +PYI016.py:110:68: PYI016 [*] Duplicate union member `int` + | +109 | # Test case for mixed union type fix +110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 |-field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + 110 |+field33: int # Error +111 111 | +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +PYI016.py:113:61: PYI016 [*] Duplicate union member `list[int]` + | +112 | # Test case for mixed union type +113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + | ^^^^^^^^^ PYI016 +114 | +115 | field35: "int | str | int" # Error + | + = help: Remove duplicate union member `list[int]` + +ℹ Safe fix +110 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error +111 111 | +112 112 | # Test case for mixed union type +113 |-field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + 113 |+field34: typing.Union[list[int], str, bytes] # Error +114 114 | +115 115 | field35: "int | str | int" # Error +116 116 | + +PYI016.py:115:23: PYI016 [*] Duplicate union member `int` + | +113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error +114 | +115 | field35: "int | str | int" # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error +114 114 | +115 |-field35: "int | str | int" # Error + 115 |+field35: "int | str" # Error +116 116 | +117 117 | +118 118 | + +PYI016.py:130:26: PYI016 [*] Duplicate union member `None` + | +128 | field38: typing.Union[int, None] +129 | # equivalent to None +130 | field39: typing.Optional[None] + | ^^^^ PYI016 +131 | # equivalent to int | None +132 | field40: typing.Union[typing.Optional[int], None] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +127 127 | field37: typing.Optional[int] +128 128 | field38: typing.Union[int, None] +129 129 | # equivalent to None +130 |-field39: typing.Optional[None] + 130 |+field39: None +131 131 | # equivalent to int | None +132 132 | field40: typing.Union[typing.Optional[int], None] +133 133 | field41: typing.Optional[typing.Union[int, None]] + +PYI016.py:132:45: PYI016 [*] Duplicate union member `None` + | +130 | field39: typing.Optional[None] +131 | # equivalent to int | None +132 | field40: typing.Union[typing.Optional[int], None] + | ^^^^ PYI016 +133 | field41: typing.Optional[typing.Union[int, None]] +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +129 129 | # equivalent to None +130 130 | field39: typing.Optional[None] +131 131 | # equivalent to int | None +132 |-field40: typing.Union[typing.Optional[int], None] + 132 |+field40: typing.Union[None, int] +133 133 | field41: typing.Optional[typing.Union[int, None]] +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 135 | field43: typing.Optional[int] | None + +PYI016.py:133:44: PYI016 [*] Duplicate union member `None` + | +131 | # equivalent to int | None +132 | field40: typing.Union[typing.Optional[int], None] +133 | field41: typing.Optional[typing.Union[int, None]] + | ^^^^ PYI016 +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 | field43: typing.Optional[int] | None + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +130 130 | field39: typing.Optional[None] +131 131 | # equivalent to int | None +132 132 | field40: typing.Union[typing.Optional[int], None] +133 |-field41: typing.Optional[typing.Union[int, None]] + 133 |+field41: typing.Union[None, int] +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] + +PYI016.py:134:45: PYI016 [*] Duplicate union member `None` + | +132 | field40: typing.Union[typing.Optional[int], None] +133 | field41: typing.Optional[typing.Union[int, None]] +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | ^^^^^^^^^^^^^^^ PYI016 +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +131 131 | # equivalent to int | None +132 132 | field40: typing.Union[typing.Optional[int], None] +133 133 | field41: typing.Optional[typing.Union[int, None]] +134 |-field42: typing.Union[typing.Optional[int], typing.Optional[int]] + 134 |+field42: typing.Union[None, int] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] +137 137 | field45: typing.Optional[int] | typing.Optional[int] + +PYI016.py:134:61: PYI016 [*] Duplicate union member `int` + | +132 | field40: typing.Union[typing.Optional[int], None] +133 | field41: typing.Optional[typing.Union[int, None]] +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | ^^^ PYI016 +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +131 131 | # equivalent to int | None +132 132 | field40: typing.Union[typing.Optional[int], None] +133 133 | field41: typing.Optional[typing.Union[int, None]] +134 |-field42: typing.Union[typing.Optional[int], typing.Optional[int]] + 134 |+field42: typing.Union[None, int] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] +137 137 | field45: typing.Optional[int] | typing.Optional[int] + +PYI016.py:135:33: PYI016 [*] Duplicate union member `None` + | +133 | field41: typing.Optional[typing.Union[int, None]] +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 | field43: typing.Optional[int] | None + | ^^^^ PYI016 +136 | field44: typing.Optional[int | None] +137 | field45: typing.Optional[int] | typing.Optional[int] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +132 132 | field40: typing.Union[typing.Optional[int], None] +133 133 | field41: typing.Optional[typing.Union[int, None]] +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 |-field43: typing.Optional[int] | None + 135 |+field43: None | int +136 136 | field44: typing.Optional[int | None] +137 137 | field45: typing.Optional[int] | typing.Optional[int] +138 138 | # equivalent to int | dict | None + +PYI016.py:136:32: PYI016 [*] Duplicate union member `None` + | +134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] + | ^^^^ PYI016 +137 | field45: typing.Optional[int] | typing.Optional[int] +138 | # equivalent to int | dict | None + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +133 133 | field41: typing.Optional[typing.Union[int, None]] +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 135 | field43: typing.Optional[int] | None +136 |-field44: typing.Optional[int | None] + 136 |+field44: None | int +137 137 | field45: typing.Optional[int] | typing.Optional[int] +138 138 | # equivalent to int | dict | None +139 139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + +PYI016.py:137:33: PYI016 [*] Duplicate union member `None` + | +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] +137 | field45: typing.Optional[int] | typing.Optional[int] + | ^^^^^^^^^^^^^^^ PYI016 +138 | # equivalent to int | dict | None +139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] +137 |-field45: typing.Optional[int] | typing.Optional[int] + 137 |+field45: typing.Union[None, int] +138 138 | # equivalent to int | dict | None +139 139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +140 140 | field47: typing.Optional[int] | typing.Optional[dict] + +PYI016.py:137:49: PYI016 [*] Duplicate union member `int` + | +135 | field43: typing.Optional[int] | None +136 | field44: typing.Optional[int | None] +137 | field45: typing.Optional[int] | typing.Optional[int] + | ^^^ PYI016 +138 | # equivalent to int | dict | None +139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +134 134 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +135 135 | field43: typing.Optional[int] | None +136 136 | field44: typing.Optional[int | None] +137 |-field45: typing.Optional[int] | typing.Optional[int] + 137 |+field45: typing.Union[None, int] +138 138 | # equivalent to int | dict | None +139 139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +140 140 | field47: typing.Optional[int] | typing.Optional[dict] + +PYI016.py:139:45: PYI016 [*] Duplicate union member `None` + | +137 | field45: typing.Optional[int] | typing.Optional[int] +138 | # equivalent to int | dict | None +139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | ^^^^^^^^^^^^^^^ PYI016 +140 | field47: typing.Optional[int] | typing.Optional[dict] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +136 136 | field44: typing.Optional[int | None] +137 137 | field45: typing.Optional[int] | typing.Optional[int] +138 138 | # equivalent to int | dict | None +139 |-field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + 139 |+field46: typing.Union[None, int, dict] +140 140 | field47: typing.Optional[int] | typing.Optional[dict] +141 141 | +142 142 | # avoid reporting twice + +PYI016.py:140:33: PYI016 [*] Duplicate union member `None` + | +138 | # equivalent to int | dict | None +139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +140 | field47: typing.Optional[int] | typing.Optional[dict] + | ^^^^^^^^^^^^^^^ PYI016 +141 | +142 | # avoid reporting twice + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +137 137 | field45: typing.Optional[int] | typing.Optional[int] +138 138 | # equivalent to int | dict | None +139 139 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +140 |-field47: typing.Optional[int] | typing.Optional[dict] + 140 |+field47: typing.Union[None, int, dict] +141 141 | +142 142 | # avoid reporting twice +143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + +PYI016.py:143:61: PYI016 [*] Duplicate union member `complex` + | +142 | # avoid reporting twice +143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + | ^^^^^^^ PYI016 +144 | field49: typing.Optional[complex | complex] | complex + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +140 140 | field47: typing.Optional[int] | typing.Optional[dict] +141 141 | +142 142 | # avoid reporting twice +143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + 143 |+field48: typing.Union[None, complex] +144 144 | field49: typing.Optional[complex | complex] | complex + +PYI016.py:143:72: PYI016 [*] Duplicate union member `complex` + | +142 | # avoid reporting twice +143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + | ^^^^^^^ PYI016 +144 | field49: typing.Optional[complex | complex] | complex + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +140 140 | field47: typing.Optional[int] | typing.Optional[dict] +141 141 | +142 142 | # avoid reporting twice +143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + 143 |+field48: typing.Union[None, complex] +144 144 | field49: typing.Optional[complex | complex] | complex + +PYI016.py:144:36: PYI016 [*] Duplicate union member `complex` + | +142 | # avoid reporting twice +143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +144 | field49: typing.Optional[complex | complex] | complex + | ^^^^^^^ PYI016 + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +141 141 | +142 142 | # avoid reporting twice +143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +144 |-field49: typing.Optional[complex | complex] | complex + 144 |+field49: None | complex + +PYI016.py:144:47: PYI016 [*] Duplicate union member `complex` + | +142 | # avoid reporting twice +143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +144 | field49: typing.Optional[complex | complex] | complex + | ^^^^^^^ PYI016 + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +141 141 | +142 142 | # avoid reporting twice +143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +144 |-field49: typing.Optional[complex | complex] | complex + 144 |+field49: None | complex diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI016_PYI016.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI016_PYI016.pyi.snap new file mode 100644 index 0000000000000..4af0da5a50f36 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI016_PYI016.pyi.snap @@ -0,0 +1,1194 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs +--- +PYI016.pyi:7:15: PYI016 [*] Duplicate union member `str` + | +6 | # Should emit for duplicate field types. +7 | field2: str | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +8 | +9 | # Should emit for union types in arguments. + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +4 4 | field1: str +5 5 | +6 6 | # Should emit for duplicate field types. +7 |-field2: str | str # PYI016: Duplicate union member `str` + 7 |+field2: str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` + +PYI016.pyi:10:23: PYI016 [*] Duplicate union member `int` + | + 9 | # Should emit for union types in arguments. +10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` + | ^^^ PYI016 +11 | print(arg1) + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +7 7 | field2: str | str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int` + 10 |+def func1(arg1: int): # PYI016: Duplicate union member `int` +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. + +PYI016.pyi:14:22: PYI016 [*] Duplicate union member `str` + | +13 | # Should emit for unions in return types. +14 | def func2() -> str | str: # PYI016: Duplicate union member `str` + | ^^^ PYI016 +15 | return "my string" + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. +14 |-def func2() -> str | str: # PYI016: Duplicate union member `str` + 14 |+def func2() -> str: # PYI016: Duplicate union member `str` +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. + +PYI016.pyi:18:15: PYI016 [*] Duplicate union member `str` + | +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` + | ^^^ PYI016 +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 |-field3: str | str | int # PYI016: Duplicate union member `str` + 18 |+field3: str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + +PYI016.pyi:19:15: PYI016 [*] Duplicate union member `int` + | +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` + | ^^^ PYI016 +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 |-field4: int | int | str # PYI016: Duplicate union member `int` + 19 |+field4: int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | + +PYI016.pyi:20:21: PYI016 [*] Duplicate union member `str` + | +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 |-field5: str | int | str # PYI016: Duplicate union member `str` + 20 |+field5: str | int # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. + +PYI016.pyi:21:28: PYI016 [*] Duplicate union member `int` + | +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | ^^^ PYI016 +22 | +23 | # Shouldn't emit for non-type unions. + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 |-field6: int | bool | str | int # PYI016: Duplicate union member `int` + 21 |+field6: int | bool | str # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. +24 24 | field7 = str | str + +PYI016.pyi:27:22: PYI016 [*] Duplicate union member `int` + | +26 | # Should emit for strangely-bracketed unions. +27 | field8: int | (str | int) # PYI016: Duplicate union member `int` + | ^^^ PYI016 +28 | +29 | # Should handle user brackets when fixing. + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +24 24 | field7 = str | str +25 25 | +26 26 | # Should emit for strangely-bracketed unions. +27 |-field8: int | (str | int) # PYI016: Duplicate union member `int` + 27 |+field8: int | str # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` + +PYI016.pyi:30:16: PYI016 [*] Duplicate union member `int` + | +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` + | ^^^ PYI016 +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +27 27 | field8: int | (str | int) # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 |-field9: int | (int | str) # PYI016: Duplicate union member `int` + 30 |+field9: int | str # PYI016: Duplicate union member `int` +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. + +PYI016.pyi:31:24: PYI016 [*] Duplicate union member `str` + | +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +32 | +33 | # Should emit for nested unions. + | + = help: Remove duplicate union member `str` + +ℹ Safe fix +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 |-field10: (str | int) | str # PYI016: Duplicate union member `str` + 31 |+field10: str | int # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 34 | field11: dict[int | int, str] + +PYI016.pyi:34:21: PYI016 [*] Duplicate union member `int` + | +33 | # Should emit for nested unions. +34 | field11: dict[int | int, str] + | ^^^ PYI016 +35 | +36 | # Should emit for unions with more than two cases + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 |-field11: dict[int | int, str] + 34 |+field11: dict[int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error + +PYI016.pyi:37:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.pyi:37:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.pyi:38:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:38:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:38:28: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:41:16: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | str # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.pyi:41:28: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | str # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.pyi:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]` + | +43 | # Should emit for duplicate literal types; also covered by PYI030 +44 | field15: typing.Literal[1] | typing.Literal[1] # Error + | ^^^^^^^^^^^^^^^^^ PYI016 +45 | +46 | # Shouldn't emit if in new parent type + | + = help: Remove duplicate union member `typing.Literal[1]` + +ℹ Safe fix +41 41 | field14: int | int | str | int # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 |-field15: typing.Literal[1] | typing.Literal[1] # Error + 44 |+field15: typing.Literal[1] # Error +45 45 | +46 46 | # Shouldn't emit if in new parent type +47 47 | field16: int | dict[int, str] # OK + +PYI016.pyi:57:5: PYI016 [*] Duplicate union member `set[int]` + | +55 | int # foo +56 | ], +57 | / set[ +58 | | int # bar +59 | | ], + | |_____^ PYI016 +60 | ] # Error, newline and comment will not be emitted in message + | + = help: Remove duplicate union member `set[int]` + +ℹ Unsafe fix +50 50 | field17: dict[int, int] # OK +51 51 | +52 52 | # Should emit in cases with newlines +53 |-field18: typing.Union[ +54 |- set[ +55 |- int # foo +56 |- ], +57 |- set[ +58 |- int # bar +59 |- ], +60 |-] # Error, newline and comment will not be emitted in message + 53 |+field18: set[int] # Error, newline and comment will not be emitted in message +61 54 | +62 55 | # Should emit in cases with `typing.Union` instead of `|` +63 56 | field19: typing.Union[int, int] # Error + +PYI016.pyi:63:28: PYI016 [*] Duplicate union member `int` + | +62 | # Should emit in cases with `typing.Union` instead of `|` +63 | field19: typing.Union[int, int] # Error + | ^^^ PYI016 +64 | +65 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +60 60 | ] # Error, newline and comment will not be emitted in message +61 61 | +62 62 | # Should emit in cases with `typing.Union` instead of `|` +63 |-field19: typing.Union[int, int] # Error + 63 |+field19: int # Error +64 64 | +65 65 | # Should emit in cases with nested `typing.Union` +66 66 | field20: typing.Union[int, typing.Union[int, str]] # Error + +PYI016.pyi:66:41: PYI016 [*] Duplicate union member `int` + | +65 | # Should emit in cases with nested `typing.Union` +66 | field20: typing.Union[int, typing.Union[int, str]] # Error + | ^^^ PYI016 +67 | +68 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +63 63 | field19: typing.Union[int, int] # Error +64 64 | +65 65 | # Should emit in cases with nested `typing.Union` +66 |-field20: typing.Union[int, typing.Union[int, str]] # Error + 66 |+field20: typing.Union[int, str] # Error +67 67 | +68 68 | # Should emit in cases with mixed `typing.Union` and `|` +69 69 | field21: typing.Union[int, int | str] # Error + +PYI016.pyi:69:28: PYI016 [*] Duplicate union member `int` + | +68 | # Should emit in cases with mixed `typing.Union` and `|` +69 | field21: typing.Union[int, int | str] # Error + | ^^^ PYI016 +70 | +71 | # Should emit only once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +66 66 | field20: typing.Union[int, typing.Union[int, str]] # Error +67 67 | +68 68 | # Should emit in cases with mixed `typing.Union` and `|` +69 |-field21: typing.Union[int, int | str] # Error + 69 |+field21: int | str # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + +PYI016.pyi:72:41: PYI016 [*] Duplicate union member `int` + | +71 | # Should emit only once in cases with multiple nested `typing.Union` +72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 +73 | +74 | # Should emit in cases with newlines + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +69 69 | field21: typing.Union[int, int | str] # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 |-field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + 72 |+field22: int # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 75 | field23: set[ # foo + +PYI016.pyi:72:59: PYI016 [*] Duplicate union member `int` + | +71 | # Should emit only once in cases with multiple nested `typing.Union` +72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 +73 | +74 | # Should emit in cases with newlines + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +69 69 | field21: typing.Union[int, int | str] # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 |-field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + 72 |+field22: int # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 75 | field23: set[ # foo + +PYI016.pyi:72:64: PYI016 [*] Duplicate union member `int` + | +71 | # Should emit only once in cases with multiple nested `typing.Union` +72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 +73 | +74 | # Should emit in cases with newlines + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +69 69 | field21: typing.Union[int, int | str] # Error +70 70 | +71 71 | # Should emit only once in cases with multiple nested `typing.Union` +72 |-field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + 72 |+field22: int # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 75 | field23: set[ # foo + +PYI016.pyi:76:12: PYI016 [*] Duplicate union member `set[int]` + | +74 | # Should emit in cases with newlines +75 | field23: set[ # foo +76 | int] | set[int] + | ^^^^^^^^ PYI016 +77 | +78 | # Should emit twice (once for each `int` in the nested union, both of which are + | + = help: Remove duplicate union member `set[int]` + +ℹ Unsafe fix +72 72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error +73 73 | +74 74 | # Should emit in cases with newlines +75 |-field23: set[ # foo +76 |- int] | set[int] + 75 |+field23: set[int] +77 76 | +78 77 | # Should emit twice (once for each `int` in the nested union, both of which are +79 78 | # duplicates of the outer `int`), but not three times (which would indicate that + +PYI016.pyi:81:41: PYI016 [*] Duplicate union member `int` + | +79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 | # we incorrectly re-checked the nested union). +81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +82 | +83 | # Should emit twice (once for each `int` in the nested union, both of which are + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +78 78 | # Should emit twice (once for each `int` in the nested union, both of which are +79 79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 80 | # we incorrectly re-checked the nested union). +81 |-field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + 81 |+field24: int # PYI016: Duplicate union member `int` +82 82 | +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that + +PYI016.pyi:81:46: PYI016 [*] Duplicate union member `int` + | +79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 | # we incorrectly re-checked the nested union). +81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +82 | +83 | # Should emit twice (once for each `int` in the nested union, both of which are + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +78 78 | # Should emit twice (once for each `int` in the nested union, both of which are +79 79 | # duplicates of the outer `int`), but not three times (which would indicate that +80 80 | # we incorrectly re-checked the nested union). +81 |-field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int` + 81 |+field24: int # PYI016: Duplicate union member `int` +82 82 | +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that + +PYI016.pyi:86:28: PYI016 [*] Duplicate union member `int` + | +84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 | # we incorrectly re-checked the nested union). +86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +87 | +88 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 85 | # we incorrectly re-checked the nested union). +86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + 86 |+field25: int # PYI016: Duplicate union member `int` +87 87 | +88 88 | # Should emit in cases with nested `typing.Union` +89 89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + +PYI016.pyi:86:34: PYI016 [*] Duplicate union member `int` + | +84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 | # we incorrectly re-checked the nested union). +86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +87 | +88 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +83 83 | # Should emit twice (once for each `int` in the nested union, both of which are +84 84 | # duplicates of the outer `int`), but not three times (which would indicate that +85 85 | # we incorrectly re-checked the nested union). +86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` + 86 |+field25: int # PYI016: Duplicate union member `int` +87 87 | +88 88 | # Should emit in cases with nested `typing.Union` +89 89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + +PYI016.pyi:89:41: PYI016 [*] Duplicate union member `int` + | +88 | # Should emit in cases with nested `typing.Union` +89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +90 | +91 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +86 86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int` +87 87 | +88 88 | # Should emit in cases with nested `typing.Union` +89 |-field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` + 89 |+field26: int # PYI016: Duplicate union member `int` +90 90 | +91 91 | # Should emit in cases with nested `typing.Union` +92 92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + +PYI016.pyi:92:54: PYI016 [*] Duplicate union member `int` + | +91 | # Should emit in cases with nested `typing.Union` +92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + | ^^^ PYI016 +93 | +94 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +89 89 | field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int` +90 90 | +91 91 | # Should emit in cases with nested `typing.Union` +92 |-field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` + 92 |+field27: int # PYI016: Duplicate union member `int` +93 93 | +94 94 | # Should emit in cases with mixed `typing.Union` and `|` +95 95 | field28: typing.Union[int | int] # Error + +PYI016.pyi:95:29: PYI016 [*] Duplicate union member `int` + | +94 | # Should emit in cases with mixed `typing.Union` and `|` +95 | field28: typing.Union[int | int] # Error + | ^^^ PYI016 +96 | +97 | # Should emit twice in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +92 92 | field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int` +93 93 | +94 94 | # Should emit in cases with mixed `typing.Union` and `|` +95 |-field28: typing.Union[int | int] # Error + 95 |+field28: int # Error +96 96 | +97 97 | # Should emit twice in cases with multiple nested `typing.Union` +98 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + +PYI016.pyi:98:54: PYI016 [*] Duplicate union member `int` + | + 97 | # Should emit twice in cases with multiple nested `typing.Union` + 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + | ^^^ PYI016 + 99 | +100 | # Should emit once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +95 95 | field28: typing.Union[int | int] # Error +96 96 | +97 97 | # Should emit twice in cases with multiple nested `typing.Union` +98 |-field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + 98 |+field29: int # Error +99 99 | +100 100 | # Should emit once in cases with multiple nested `typing.Union` +101 101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + +PYI016.pyi:98:59: PYI016 [*] Duplicate union member `int` + | + 97 | # Should emit twice in cases with multiple nested `typing.Union` + 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + | ^^^ PYI016 + 99 | +100 | # Should emit once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +95 95 | field28: typing.Union[int | int] # Error +96 96 | +97 97 | # Should emit twice in cases with multiple nested `typing.Union` +98 |-field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error + 98 |+field29: int # Error +99 99 | +100 100 | # Should emit once in cases with multiple nested `typing.Union` +101 101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + +PYI016.pyi:101:54: PYI016 [*] Duplicate union member `int` + | +100 | # Should emit once in cases with multiple nested `typing.Union` +101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + | ^^^ PYI016 +102 | +103 | # Should emit once, and fix to `typing.Union[float, int]` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +98 98 | field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error +99 99 | +100 100 | # Should emit once in cases with multiple nested `typing.Union` +101 |-field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error + 101 |+field30: typing.Union[int, str] # Error +102 102 | +103 103 | # Should emit once, and fix to `typing.Union[float, int]` +104 104 | field31: typing.Union[float, typing.Union[int | int]] # Error + +PYI016.pyi:104:49: PYI016 [*] Duplicate union member `int` + | +103 | # Should emit once, and fix to `typing.Union[float, int]` +104 | field31: typing.Union[float, typing.Union[int | int]] # Error + | ^^^ PYI016 +105 | +106 | # Should emit once, and fix to `typing.Union[float, int]` + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +101 101 | field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error +102 102 | +103 103 | # Should emit once, and fix to `typing.Union[float, int]` +104 |-field31: typing.Union[float, typing.Union[int | int]] # Error + 104 |+field31: float | int # Error +105 105 | +106 106 | # Should emit once, and fix to `typing.Union[float, int]` +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error + +PYI016.pyi:107:49: PYI016 [*] Duplicate union member `int` + | +106 | # Should emit once, and fix to `typing.Union[float, int]` +107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error + | ^^^ PYI016 +108 | +109 | # Test case for mixed union type fix + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +104 104 | field31: typing.Union[float, typing.Union[int | int]] # Error +105 105 | +106 106 | # Should emit once, and fix to `typing.Union[float, int]` +107 |-field32: typing.Union[float, typing.Union[int | int | int]] # Error + 107 |+field32: float | int # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + +PYI016.pyi:107:55: PYI016 [*] Duplicate union member `int` + | +106 | # Should emit once, and fix to `typing.Union[float, int]` +107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error + | ^^^ PYI016 +108 | +109 | # Test case for mixed union type fix + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +104 104 | field31: typing.Union[float, typing.Union[int | int]] # Error +105 105 | +106 106 | # Should emit once, and fix to `typing.Union[float, int]` +107 |-field32: typing.Union[float, typing.Union[int | int | int]] # Error + 107 |+field32: float | int # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + +PYI016.pyi:110:42: PYI016 [*] Duplicate union member `int` + | +109 | # Test case for mixed union type fix +110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 |-field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + 110 |+field33: int # Error +111 111 | +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +PYI016.pyi:110:62: PYI016 [*] Duplicate union member `int` + | +109 | # Test case for mixed union type fix +110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 |-field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + 110 |+field33: int # Error +111 111 | +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +PYI016.pyi:110:68: PYI016 [*] Duplicate union member `int` + | +109 | # Test case for mixed union type fix +110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + | ^^^ PYI016 +111 | +112 | # Test case for mixed union type + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +107 107 | field32: typing.Union[float, typing.Union[int | int | int]] # Error +108 108 | +109 109 | # Test case for mixed union type fix +110 |-field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error + 110 |+field33: int # Error +111 111 | +112 112 | # Test case for mixed union type +113 113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + +PYI016.pyi:113:61: PYI016 [*] Duplicate union member `list[int]` + | +112 | # Test case for mixed union type +113 | field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + | ^^^^^^^^^ PYI016 +114 | +115 | # https://github.com/astral-sh/ruff/issues/18546 + | + = help: Remove duplicate union member `list[int]` + +ℹ Safe fix +110 110 | field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error +111 111 | +112 112 | # Test case for mixed union type +113 |-field34: typing.Union[list[int], str] | typing.Union[bytes, list[int]] # Error + 113 |+field34: typing.Union[list[int], str, bytes] # Error +114 114 | +115 115 | # https://github.com/astral-sh/ruff/issues/18546 +116 116 | # Expand Optional[T] to Union[T, None] + +PYI016.pyi:121:26: PYI016 [*] Duplicate union member `None` + | +119 | field38: typing.Union[int, None] +120 | # equivalent to None +121 | field39: typing.Optional[None] + | ^^^^ PYI016 +122 | # equivalent to int | None +123 | field40: typing.Union[typing.Optional[int], None] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +118 118 | field37: typing.Optional[int] +119 119 | field38: typing.Union[int, None] +120 120 | # equivalent to None +121 |-field39: typing.Optional[None] + 121 |+field39: None +122 122 | # equivalent to int | None +123 123 | field40: typing.Union[typing.Optional[int], None] +124 124 | field41: typing.Optional[typing.Union[int, None]] + +PYI016.pyi:123:45: PYI016 [*] Duplicate union member `None` + | +121 | field39: typing.Optional[None] +122 | # equivalent to int | None +123 | field40: typing.Union[typing.Optional[int], None] + | ^^^^ PYI016 +124 | field41: typing.Optional[typing.Union[int, None]] +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +120 120 | # equivalent to None +121 121 | field39: typing.Optional[None] +122 122 | # equivalent to int | None +123 |-field40: typing.Union[typing.Optional[int], None] + 123 |+field40: typing.Union[None, int] +124 124 | field41: typing.Optional[typing.Union[int, None]] +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 126 | field43: typing.Optional[int] | None + +PYI016.pyi:124:44: PYI016 [*] Duplicate union member `None` + | +122 | # equivalent to int | None +123 | field40: typing.Union[typing.Optional[int], None] +124 | field41: typing.Optional[typing.Union[int, None]] + | ^^^^ PYI016 +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 | field43: typing.Optional[int] | None + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +121 121 | field39: typing.Optional[None] +122 122 | # equivalent to int | None +123 123 | field40: typing.Union[typing.Optional[int], None] +124 |-field41: typing.Optional[typing.Union[int, None]] + 124 |+field41: typing.Union[None, int] +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] + +PYI016.pyi:125:45: PYI016 [*] Duplicate union member `None` + | +123 | field40: typing.Union[typing.Optional[int], None] +124 | field41: typing.Optional[typing.Union[int, None]] +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | ^^^^^^^^^^^^^^^ PYI016 +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +122 122 | # equivalent to int | None +123 123 | field40: typing.Union[typing.Optional[int], None] +124 124 | field41: typing.Optional[typing.Union[int, None]] +125 |-field42: typing.Union[typing.Optional[int], typing.Optional[int]] + 125 |+field42: typing.Union[None, int] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] +128 128 | field45: typing.Optional[int] | typing.Optional[int] + +PYI016.pyi:125:61: PYI016 [*] Duplicate union member `int` + | +123 | field40: typing.Union[typing.Optional[int], None] +124 | field41: typing.Optional[typing.Union[int, None]] +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] + | ^^^ PYI016 +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +122 122 | # equivalent to int | None +123 123 | field40: typing.Union[typing.Optional[int], None] +124 124 | field41: typing.Optional[typing.Union[int, None]] +125 |-field42: typing.Union[typing.Optional[int], typing.Optional[int]] + 125 |+field42: typing.Union[None, int] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] +128 128 | field45: typing.Optional[int] | typing.Optional[int] + +PYI016.pyi:126:33: PYI016 [*] Duplicate union member `None` + | +124 | field41: typing.Optional[typing.Union[int, None]] +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 | field43: typing.Optional[int] | None + | ^^^^ PYI016 +127 | field44: typing.Optional[int | None] +128 | field45: typing.Optional[int] | typing.Optional[int] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +123 123 | field40: typing.Union[typing.Optional[int], None] +124 124 | field41: typing.Optional[typing.Union[int, None]] +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 |-field43: typing.Optional[int] | None + 126 |+field43: None | int +127 127 | field44: typing.Optional[int | None] +128 128 | field45: typing.Optional[int] | typing.Optional[int] +129 129 | # equivalent to int | dict | None + +PYI016.pyi:127:32: PYI016 [*] Duplicate union member `None` + | +125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] + | ^^^^ PYI016 +128 | field45: typing.Optional[int] | typing.Optional[int] +129 | # equivalent to int | dict | None + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +124 124 | field41: typing.Optional[typing.Union[int, None]] +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 126 | field43: typing.Optional[int] | None +127 |-field44: typing.Optional[int | None] + 127 |+field44: None | int +128 128 | field45: typing.Optional[int] | typing.Optional[int] +129 129 | # equivalent to int | dict | None +130 130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + +PYI016.pyi:128:33: PYI016 [*] Duplicate union member `None` + | +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] +128 | field45: typing.Optional[int] | typing.Optional[int] + | ^^^^^^^^^^^^^^^ PYI016 +129 | # equivalent to int | dict | None +130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] +128 |-field45: typing.Optional[int] | typing.Optional[int] + 128 |+field45: typing.Union[None, int] +129 129 | # equivalent to int | dict | None +130 130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +131 131 | field47: typing.Optional[int] | typing.Optional[dict] + +PYI016.pyi:128:49: PYI016 [*] Duplicate union member `int` + | +126 | field43: typing.Optional[int] | None +127 | field44: typing.Optional[int | None] +128 | field45: typing.Optional[int] | typing.Optional[int] + | ^^^ PYI016 +129 | # equivalent to int | dict | None +130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | + = help: Remove duplicate union member `int` + +ℹ Safe fix +125 125 | field42: typing.Union[typing.Optional[int], typing.Optional[int]] +126 126 | field43: typing.Optional[int] | None +127 127 | field44: typing.Optional[int | None] +128 |-field45: typing.Optional[int] | typing.Optional[int] + 128 |+field45: typing.Union[None, int] +129 129 | # equivalent to int | dict | None +130 130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +131 131 | field47: typing.Optional[int] | typing.Optional[dict] + +PYI016.pyi:130:45: PYI016 [*] Duplicate union member `None` + | +128 | field45: typing.Optional[int] | typing.Optional[int] +129 | # equivalent to int | dict | None +130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + | ^^^^^^^^^^^^^^^ PYI016 +131 | field47: typing.Optional[int] | typing.Optional[dict] + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +127 127 | field44: typing.Optional[int | None] +128 128 | field45: typing.Optional[int] | typing.Optional[int] +129 129 | # equivalent to int | dict | None +130 |-field46: typing.Union[typing.Optional[int], typing.Optional[dict]] + 130 |+field46: typing.Union[None, int, dict] +131 131 | field47: typing.Optional[int] | typing.Optional[dict] +132 132 | +133 133 | # avoid reporting twice + +PYI016.pyi:131:33: PYI016 [*] Duplicate union member `None` + | +129 | # equivalent to int | dict | None +130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +131 | field47: typing.Optional[int] | typing.Optional[dict] + | ^^^^^^^^^^^^^^^ PYI016 +132 | +133 | # avoid reporting twice + | + = help: Remove duplicate union member `None` + +ℹ Safe fix +128 128 | field45: typing.Optional[int] | typing.Optional[int] +129 129 | # equivalent to int | dict | None +130 130 | field46: typing.Union[typing.Optional[int], typing.Optional[dict]] +131 |-field47: typing.Optional[int] | typing.Optional[dict] + 131 |+field47: typing.Union[None, int, dict] +132 132 | +133 133 | # avoid reporting twice +134 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + +PYI016.pyi:134:61: PYI016 [*] Duplicate union member `complex` + | +133 | # avoid reporting twice +134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + | ^^^^^^^ PYI016 +135 | field49: typing.Optional[complex | complex] | complex + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +131 131 | field47: typing.Optional[int] | typing.Optional[dict] +132 132 | +133 133 | # avoid reporting twice +134 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + 134 |+field48: typing.Union[None, complex] +135 135 | field49: typing.Optional[complex | complex] | complex + +PYI016.pyi:134:72: PYI016 [*] Duplicate union member `complex` + | +133 | # avoid reporting twice +134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + | ^^^^^^^ PYI016 +135 | field49: typing.Optional[complex | complex] | complex + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +131 131 | field47: typing.Optional[int] | typing.Optional[dict] +132 132 | +133 133 | # avoid reporting twice +134 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] + 134 |+field48: typing.Union[None, complex] +135 135 | field49: typing.Optional[complex | complex] | complex + +PYI016.pyi:135:36: PYI016 [*] Duplicate union member `complex` + | +133 | # avoid reporting twice +134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +135 | field49: typing.Optional[complex | complex] | complex + | ^^^^^^^ PYI016 + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +132 132 | +133 133 | # avoid reporting twice +134 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +135 |-field49: typing.Optional[complex | complex] | complex + 135 |+field49: None | complex + +PYI016.pyi:135:47: PYI016 [*] Duplicate union member `complex` + | +133 | # avoid reporting twice +134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +135 | field49: typing.Optional[complex | complex] | complex + | ^^^^^^^ PYI016 + | + = help: Remove duplicate union member `complex` + +ℹ Safe fix +132 132 | +133 133 | # avoid reporting twice +134 134 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex] +135 |-field49: typing.Optional[complex | complex] | complex + 135 |+field49: None | complex diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI044_PYI044.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI044_PYI044.pyi.snap deleted file mode 100644 index 4ae21763be417..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__preview__PYI044_PYI044.pyi.snap +++ /dev/null @@ -1,38 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs ---- -PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics - | -1 | # Bad import. -2 | from __future__ import annotations # PYI044. - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044 -3 | from __future__ import annotations, with_statement # PYI044. - | - = help: Remove `from __future__ import annotations` - -ℹ Safe fix -1 1 | # Bad import. -2 |-from __future__ import annotations # PYI044. -3 2 | from __future__ import annotations, with_statement # PYI044. -4 3 | -5 4 | # Good imports. - -PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics - | -1 | # Bad import. -2 | from __future__ import annotations # PYI044. -3 | from __future__ import annotations, with_statement # PYI044. - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044 -4 | -5 | # Good imports. - | - = help: Remove `from __future__ import annotations` - -ℹ Safe fix -1 1 | # Bad import. -2 2 | from __future__ import annotations # PYI044. -3 |-from __future__ import annotations, with_statement # PYI044. - 3 |+from __future__ import with_statement # PYI044. -4 4 | -5 5 | # Good imports. -6 6 | from __future__ import with_statement diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap index 99f4969598fa9..bbee6e9da58cd 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap @@ -464,7 +464,6 @@ PYI061.py:79:20: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) | ^^^^ PYI061 80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -474,24 +473,5 @@ PYI061.py:80:28: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) 80 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -81 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.py:81:12: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.py:81:28: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap index a2c845c0649b8..6a48501c8dad8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap @@ -291,7 +291,6 @@ PYI061.pyi:54:20: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) | ^^^^ PYI061 55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -301,24 +300,5 @@ PYI061.pyi:55:28: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) 55 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -56 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.pyi:56:12: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.pyi:56:28: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs new file mode 100644 index 0000000000000..43429c8c4ff93 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/helpers.rs @@ -0,0 +1,161 @@ +use std::fmt; + +use ruff_python_ast::helpers::map_callable; +use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall, Keyword, Stmt, StmtFunctionDef}; +use ruff_python_semantic::analyze::visibility; +use ruff_python_semantic::{ScopeKind, SemanticModel}; +use ruff_python_trivia::PythonWhitespace; + +use crate::checkers::ast::Checker; + +pub(super) fn get_mark_decorators<'a>( + decorators: &'a [Decorator], + semantic: &'a SemanticModel, +) -> impl Iterator + 'a { + decorators.iter().filter_map(move |decorator| { + let expr = map_callable(&decorator.expression); + let qualified_name = semantic.resolve_qualified_name(expr)?; + match qualified_name.segments() { + ["pytest", "mark", marker] => Some((decorator, *marker)), + _ => None, + } + }) +} + +pub(super) fn is_pytest_fail(call: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(call) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "fail"])) +} + +pub(super) fn is_pytest_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "fixture"])) +} + +pub(super) fn is_pytest_yield_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pytest", "yield_fixture"]) + }) +} + +pub(super) fn is_pytest_parametrize(call: &ExprCall, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pytest", "mark", "parametrize"]) + }) +} + +/// Whether the currently checked `func` is likely to be a Pytest test. +/// +/// A normal Pytest test function is one whose name starts with `test` and is either: +/// +/// * Placed at module-level, or +/// * Placed within a class whose name starts with `Test` and does not have an `__init__` method. +/// +/// During test discovery, Pytest respects a few settings which we do not have access to. +/// This function is thus prone to both false positives and false negatives. +/// +/// References: +/// - [`pytest` documentation: Conventions for Python test discovery](https://docs.pytest.org/en/stable/explanation/goodpractices.html#conventions-for-python-test-discovery) +/// - [`pytest` documentation: Changing naming conventions](https://docs.pytest.org/en/stable/example/pythoncollection.html#changing-naming-conventions) +pub(crate) fn is_likely_pytest_test(func: &StmtFunctionDef, checker: &Checker) -> bool { + let semantic = checker.semantic(); + + if !func.name.starts_with("test") { + return false; + } + + if semantic.scope_id.is_global() { + return true; + } + + let ScopeKind::Class(class) = semantic.current_scope().kind else { + return false; + }; + + if !class.name.starts_with("Test") { + return false; + } + + class.body.iter().all(|stmt| { + let Stmt::FunctionDef(function) = stmt else { + return true; + }; + + !visibility::is_init(&function.name) + }) +} + +pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool { + if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &keyword.value { + value == literal + } else { + false + } +} + +pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool { + match expr { + Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(), + Expr::NoneLiteral(_) => true, + Expr::FString(ast::ExprFString { value, .. }) => { + value.iter().all(|f_string_part| match f_string_part { + ast::FStringPart::Literal(literal) => literal.is_empty(), + ast::FStringPart::FString(f_string) => f_string + .elements + .iter() + .all(is_empty_or_null_interpolated_string_element), + }) + } + _ => false, + } +} + +fn is_empty_or_null_interpolated_string_element(element: &ast::InterpolatedStringElement) -> bool { + match element { + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { + value, + .. + }) => value.is_empty(), + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { + expression, + .. + }) => is_empty_or_null_string(expression), + } +} + +pub(super) fn split_names(names: &str) -> Vec<&str> { + // Match the following pytest code: + // [x.strip() for x in argnames.split(",") if x.strip()] + names + .split(',') + .filter_map(|s| { + let trimmed = s.trim_whitespace(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } + }) + .collect::>() +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(super) enum Parentheses { + None, + Empty, +} + +impl fmt::Display for Parentheses { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Parentheses::None => fmt.write_str(""), + Parentheses::Empty => fmt.write_str("()"), + } + } +} diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs index 7cb3486d0141d..5923d978ec7a1 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/). +mod helpers; pub(crate) mod rules; pub mod settings; pub mod types; @@ -13,7 +14,7 @@ mod tests { use crate::registry::Rule; use crate::settings::types::IdentifierPattern; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; use super::settings::Settings; use super::types; @@ -354,7 +355,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(name, diagnostics); + assert_diagnostics!(name, diagnostics); Ok(()) } @@ -373,7 +374,7 @@ mod tests { ]) }, )?; - assert_messages!("PT006_and_PT007", diagnostics); + assert_diagnostics!("PT006_and_PT007", diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs index 2460db7492e11..599decd57b18c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs @@ -2,14 +2,13 @@ use std::borrow::Cow; use std::iter; use anyhow::Result; -use anyhow::{bail, Context}; +use anyhow::{Context, bail}; use libcst_native::{ self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, }; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::visitor::Visitor; @@ -22,13 +21,14 @@ use ruff_python_semantic::{Binding, BindingKind}; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; use crate::cst::helpers::negate; use crate::cst::matchers::match_indented_block; use crate::cst::matchers::match_module; use crate::fix::codemods::CodegenStylist; use crate::importer::ImportRequest; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; use super::unittest_assert::UnittestAssert; @@ -187,7 +187,6 @@ impl Violation for PytestAssertAlwaysFalse { /// /// ## References /// - [`pytest` documentation: Assertion introspection details](https://docs.pytest.org/en/7.1.x/how-to/assert.html#assertion-introspection-details) - #[derive(ViolationMetadata)] pub(crate) struct PytestUnittestAssertion { assertion: String, @@ -210,23 +209,23 @@ impl Violation for PytestUnittestAssertion { /// Visitor that tracks assert statements and checks if they reference /// the exception name. -struct ExceptionHandlerVisitor<'a> { +struct ExceptionHandlerVisitor<'a, 'b> { exception_name: &'a str, current_assert: Option<&'a Stmt>, - errors: Vec, + checker: &'a Checker<'b>, } -impl<'a> ExceptionHandlerVisitor<'a> { - const fn new(exception_name: &'a str) -> Self { +impl<'a, 'b> ExceptionHandlerVisitor<'a, 'b> { + const fn new(checker: &'a Checker<'b>, exception_name: &'a str) -> Self { Self { exception_name, current_assert: None, - errors: Vec::new(), + checker, } } } -impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> { +impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a, '_> { fn visit_stmt(&mut self, stmt: &'a Stmt) { match stmt { Stmt::Assert(_) => { @@ -243,12 +242,12 @@ impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> { Expr::Name(ast::ExprName { id, .. }) => { if let Some(current_assert) = self.current_assert { if id.as_str() == self.exception_name { - self.errors.push(Diagnostic::new( + self.checker.report_diagnostic( PytestAssertInExcept { name: id.to_string(), }, current_assert.range(), - )); + ); } } } @@ -257,13 +256,12 @@ impl<'a> Visitor<'a> for ExceptionHandlerVisitor<'a> { } } -fn check_assert_in_except(name: &str, body: &[Stmt]) -> Vec { +fn check_assert_in_except(checker: &Checker, name: &str, body: &[Stmt]) { // Walk body to find assert statements that reference the exception name - let mut visitor = ExceptionHandlerVisitor::new(name); + let mut visitor = ExceptionHandlerVisitor::new(checker, name); for stmt in body { visitor.visit_stmt(stmt); } - visitor.errors } /// PT009 @@ -282,7 +280,7 @@ pub(crate) fn unittest_assertion( return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestUnittestAssertion { assertion: unittest_assert.to_string(), }, @@ -308,8 +306,6 @@ pub(crate) fn unittest_assertion( ))); } } - - checker.report_diagnostic(diagnostic); } /// ## What it does @@ -379,28 +375,23 @@ pub(crate) fn unittest_raises_assertion_call(checker: &Checker, call: &ast::Expr } } - if let Some(diagnostic) = unittest_raises_assertion(call, vec![], checker) { - checker.report_diagnostic(diagnostic); - } + unittest_raises_assertion(call, vec![], checker); } /// PT027 -pub(crate) fn unittest_raises_assertion_binding( - checker: &Checker, - binding: &Binding, -) -> Option { +pub(crate) fn unittest_raises_assertion_binding(checker: &Checker, binding: &Binding) { if !matches!(binding.kind, BindingKind::WithItemVar) { - return None; + return; } let semantic = checker.semantic(); - let Stmt::With(with) = binding.statement(semantic)? else { - return None; + let Some(Stmt::With(with)) = binding.statement(semantic) else { + return; }; - let Expr::Call(call) = corresponding_context_expr(binding, with)? else { - return None; + let Some(Expr::Call(call)) = corresponding_context_expr(binding, with) else { + return; }; let mut edits = vec![]; @@ -419,11 +410,13 @@ pub(crate) fn unittest_raises_assertion_binding( // ``` for reference_id in binding.references() { let reference = semantic.reference(reference_id); - let node_id = reference.expression_id()?; + let Some(node_id) = reference.expression_id() else { + return; + }; let mut ancestors = semantic.expressions(node_id).skip(1); - let Expr::Attribute(ast::ExprAttribute { attr, .. }) = ancestors.next()? else { + let Some(Expr::Attribute(ast::ExprAttribute { attr, .. })) = ancestors.next() else { continue; }; @@ -432,7 +425,7 @@ pub(crate) fn unittest_raises_assertion_binding( } } - unittest_raises_assertion(call, edits, checker) + unittest_raises_assertion(call, edits, checker); } fn corresponding_context_expr<'a>(binding: &Binding, with: &'a ast::StmtWith) -> Option<&'a Expr> { @@ -453,23 +446,19 @@ fn corresponding_context_expr<'a>(binding: &Binding, with: &'a ast::StmtWith) -> }) } -fn unittest_raises_assertion( - call: &ast::ExprCall, - extra_edits: Vec, - checker: &Checker, -) -> Option { +fn unittest_raises_assertion(call: &ast::ExprCall, extra_edits: Vec, checker: &Checker) { let Expr::Attribute(ast::ExprAttribute { attr, .. }) = call.func.as_ref() else { - return None; + return; }; if !matches!( attr.as_str(), "assertRaises" | "failUnlessRaises" | "assertRaisesRegex" | "assertRaisesRegexp" ) { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestUnittestRaisesAssertion { assertion: attr.to_string(), }, @@ -497,8 +486,6 @@ fn unittest_raises_assertion( }); } } - - Some(diagnostic) } fn to_pytest_raises_args<'a>( @@ -590,21 +577,19 @@ fn to_pytest_raises_args<'a>( pub(crate) fn assert_falsy(checker: &Checker, stmt: &Stmt, test: &Expr) { let truthiness = Truthiness::from_expr(test, |id| checker.semantic().has_builtin_binding(id)); if truthiness.into_bool() == Some(false) { - checker.report_diagnostic(Diagnostic::new(PytestAssertAlwaysFalse, stmt.range())); + checker.report_diagnostic(PytestAssertAlwaysFalse, stmt.range()); } } /// PT017 pub(crate) fn assert_in_exception_handler(checker: &Checker, handlers: &[ExceptHandler]) { - checker.report_diagnostics(handlers.iter().flat_map(|handler| match handler { - ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => { - if let Some(name) = name { - check_assert_in_except(name, body) - } else { - Vec::new() - } + for handler in handlers { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) = + handler; + if let Some(name) = name { + check_assert_in_except(checker, name, body); } - })); + } } #[derive(Copy, Clone)] @@ -633,11 +618,13 @@ fn is_composite_condition(test: &Expr) -> CompositionKind { op: UnaryOp::Not, operand, range: _, + node_index: _, }) => { if let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _, + node_index: _, }) = operand.as_ref() { // Only split cases without mixed `and` and `or`. @@ -827,7 +814,7 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> pub(crate) fn composite_condition(checker: &Checker, stmt: &Stmt, test: &Expr, msg: Option<&Expr>) { let composite = is_composite_condition(test); if matches!(composite, CompositionKind::Simple | CompositionKind::Mixed) { - let mut diagnostic = Diagnostic::new(PytestCompositeAssertion, stmt.range()); + let mut diagnostic = checker.report_diagnostic(PytestCompositeAssertion, stmt.range()); if matches!(composite, CompositionKind::Simple) && msg.is_none() && !checker.comment_ranges().intersects(stmt.range()) @@ -840,6 +827,5 @@ pub(crate) fn composite_condition(checker: &Checker, stmt: &Stmt, test: &Expr, m .map(Fix::unsafe_edit) }); } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs index 998ad78014ed0..cc95dd837a4c7 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fail.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use super::helpers::{is_empty_or_null_string, is_pytest_fail}; +use crate::rules::flake8_pytest_style::helpers::{is_empty_or_null_string, is_pytest_fail}; /// ## What it does /// Checks for `pytest.fail` calls without a message. @@ -55,6 +55,7 @@ impl Violation for PytestFailWithoutMessage { } } +/// PT016 pub(crate) fn fail_call(checker: &Checker, call: &ast::ExprCall) { if is_pytest_fail(&call.func, checker.semantic()) { // Allow either `pytest.fail(reason="...")` (introduced in pytest 7.0) or @@ -65,7 +66,7 @@ pub(crate) fn fail_call(checker: &Checker, call: &ast::ExprCall) { .or_else(|| call.arguments.find_argument_value("msg", 0)) .is_none_or(is_empty_or_null_string) { - checker.report_diagnostic(Diagnostic::new(PytestFailWithoutMessage, call.func.range())); + checker.report_diagnostic(PytestFailWithoutMessage, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index 4b3f038f3e8c9..128b3b06e33ea 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -1,24 +1,27 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::Decorator; +use ruff_python_ast::helpers::map_callable; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::Decorator; use ruff_python_ast::{self as ast, Expr, Parameters, Stmt}; -use ruff_python_semantic::analyze::visibility::is_abstract; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::visibility::is_abstract; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; use ruff_text_size::{TextLen, TextRange}; +use rustc_hash::FxHashSet; use crate::checkers::ast::Checker; use crate::fix::edits; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Violation}; +use crate::{Edit, Fix}; -use super::helpers::{ - get_mark_decorators, is_pytest_fixture, is_pytest_yield_fixture, keyword_is_literal, - Parentheses, +use crate::rules::flake8_pytest_style::helpers::{ + Parentheses, get_mark_decorators, is_pytest_fixture, is_pytest_yield_fixture, + keyword_is_literal, }; /// ## What it does @@ -55,6 +58,22 @@ use super::helpers::{ /// def my_fixture(): ... /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the +/// `pytest.fixture` decorator. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// import pytest +/// +/// +/// @pytest.fixture( +/// # comment +/// # scope = "module" +/// ) +/// def my_fixture(): ... +/// ``` +/// /// ## Options /// - `lint.flake8-pytest-style.fixture-parentheses` /// @@ -218,7 +237,7 @@ impl AlwaysFixableViolation for PytestExtraneousScopeFunction { #[deprecated(note = "PT004 has been removed")] pub(crate) struct PytestMissingFixtureNameUnderscore; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for PytestMissingFixtureNameUnderscore { fn message(&self) -> String { unreachable!("PT004 has been removed"); @@ -283,7 +302,7 @@ impl Violation for PytestMissingFixtureNameUnderscore { #[deprecated(note = "PT005 has been removed")] pub(crate) struct PytestIncorrectFixtureNameUnderscore; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for PytestIncorrectFixtureNameUnderscore { fn message(&self) -> String { unreachable!("PT005 has been removed"); @@ -620,7 +639,11 @@ struct SkipFunctionsVisitor<'a> { impl<'a> Visitor<'a> for SkipFunctionsVisitor<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { match stmt { - Stmt::Return(ast::StmtReturn { value, range: _ }) => { + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => { if value.is_some() { self.has_return_with_value = true; } @@ -635,7 +658,11 @@ impl<'a> Visitor<'a> for SkipFunctionsVisitor<'a> { Expr::YieldFrom(_) => { self.has_yield_from = true; } - Expr::Yield(ast::ExprYield { value, range: _ }) => { + Expr::Yield(ast::ExprYield { + value, + range: _, + node_index: _, + }) => { self.yield_statements.push(expr); if value.is_some() { self.has_return_with_value = true; @@ -670,28 +697,35 @@ fn pytest_fixture_parentheses( expected: Parentheses, actual: Parentheses, ) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestFixtureIncorrectParenthesesStyle { expected, actual }, decorator.range(), ); diagnostic.set_fix(fix); - checker.report_diagnostic(diagnostic); } /// PT001, PT002, PT003 fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decorator) { match &decorator.expression { Expr::Call(ast::ExprCall { - func, + func: _, arguments, range: _, + node_index: _, }) => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { - if !checker.settings.flake8_pytest_style.fixture_parentheses + if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if !checker.settings().flake8_pytest_style.fixture_parentheses && arguments.args.is_empty() && arguments.keywords.is_empty() { - let fix = Fix::safe_edit(Edit::deletion(func.end(), decorator.end())); + let fix = Fix::applicable_edit( + Edit::range_deletion(arguments.range()), + if checker.comment_ranges().intersects(arguments.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + ); pytest_fixture_parentheses( checker, decorator, @@ -702,39 +736,39 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor } } - if checker.enabled(Rule::PytestFixturePositionalArgs) { + if checker.is_rule_enabled(Rule::PytestFixturePositionalArgs) { if !arguments.args.is_empty() { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( PytestFixturePositionalArgs { function: func_name.to_string(), }, decorator.range(), - )); + ); } } - if checker.enabled(Rule::PytestExtraneousScopeFunction) { + if checker.is_rule_enabled(Rule::PytestExtraneousScopeFunction) { if let Some(keyword) = arguments.find_keyword("scope") { if keyword_is_literal(keyword, "function") { - let mut diagnostic = - Diagnostic::new(PytestExtraneousScopeFunction, keyword.range()); + let mut diagnostic = checker + .report_diagnostic(PytestExtraneousScopeFunction, keyword.range()); diagnostic.try_set_fix(|| { edits::remove_argument( keyword, arguments, edits::Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::unsafe_edit) }); - checker.report_diagnostic(diagnostic); } } } } _ => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { - if checker.settings.flake8_pytest_style.fixture_parentheses { + if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if checker.settings().flake8_pytest_style.fixture_parentheses { let fix = Fix::safe_edit(Edit::insertion( Parentheses::Empty.to_string(), decorator.end(), @@ -760,7 +794,7 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns: visitor.visit_stmt(stmt); } - if checker.enabled(Rule::PytestUselessYieldFixture) { + if checker.is_rule_enabled(Rule::PytestUselessYieldFixture) { let Some(stmt) = body.last() else { return; }; @@ -773,7 +807,7 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns: if visitor.yield_statements.len() != 1 { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestUselessYieldFixture { name: name.to_string(), }, @@ -802,21 +836,61 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns: } else { diagnostic.set_fix(Fix::safe_edit(yield_edit)); } - checker.report_diagnostic(diagnostic); } } /// PT019 -fn check_test_function_args(checker: &Checker, parameters: &Parameters) { +fn check_test_function_args(checker: &Checker, parameters: &Parameters, decorators: &[Decorator]) { + let mut named_parametrize = FxHashSet::default(); + for decorator in decorators.iter().filter(|decorator| { + UnqualifiedName::from_expr(map_callable(&decorator.expression)) + .is_some_and(|name| matches!(name.segments(), ["pytest", "mark", "parametrize"])) + }) { + let Some(call_expr) = decorator.expression.as_call_expr() else { + continue; + }; + let Some(first_arg) = call_expr.arguments.find_argument_value("argnames", 0) else { + continue; + }; + + match first_arg { + Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { + named_parametrize.extend( + value + .to_str() + .split(',') + .map(str::trim) + .filter(|param| !param.is_empty() && param.starts_with('_')), + ); + } + + Expr::Name(_) => return, + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) + if elts.iter().any(Expr::is_name_expr) => + { + return; + } + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + named_parametrize.extend( + elts.iter() + .filter_map(Expr::as_string_literal_expr) + .map(|param| param.value.to_str().trim()) + .filter(|param| !param.is_empty() && param.starts_with('_')), + ); + } + _ => {} + } + } + for parameter in parameters.iter_non_variadic_params() { let name = parameter.name(); - if name.starts_with('_') { - checker.report_diagnostic(Diagnostic::new( + if name.starts_with('_') && !named_parametrize.contains(name.as_str()) { + checker.report_diagnostic( PytestFixtureParamWithoutValue { name: name.to_string(), }, parameter.range(), - )); + ); } } } @@ -824,10 +898,7 @@ fn check_test_function_args(checker: &Checker, parameters: &Parameters) { /// PT020 fn check_fixture_decorator_name(checker: &Checker, decorator: &Decorator) { if is_pytest_yield_fixture(decorator, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( - PytestDeprecatedYieldFixture, - decorator.range(), - )); + checker.report_diagnostic(PytestDeprecatedYieldFixture, decorator.range()); } } @@ -844,33 +915,28 @@ fn check_fixture_addfinalizer(checker: &Checker, parameters: &Parameters, body: } if let Some(addfinalizer) = visitor.addfinalizer_call { - checker.report_diagnostic(Diagnostic::new( - PytestFixtureFinalizerCallback, - addfinalizer.range(), - )); + checker.report_diagnostic(PytestFixtureFinalizerCallback, addfinalizer.range()); } } /// PT024, PT025 fn check_fixture_marks(checker: &Checker, decorators: &[Decorator]) { - for (expr, marker) in get_mark_decorators(decorators) { - if checker.enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) { + for (expr, marker) in get_mark_decorators(decorators, checker.semantic()) { + if checker.is_rule_enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) { if marker == "asyncio" { let mut diagnostic = - Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, expr.range()); + checker.report_diagnostic(PytestUnnecessaryAsyncioMarkOnFixture, expr.range()); let range = checker.locator().full_lines_range(expr.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); - checker.report_diagnostic(diagnostic); } } - if checker.enabled(Rule::PytestErroneousUseFixturesOnFixture) { + if checker.is_rule_enabled(Rule::PytestErroneousUseFixturesOnFixture) { if marker == "usefixtures" { let mut diagnostic = - Diagnostic::new(PytestErroneousUseFixturesOnFixture, expr.range()); + checker.report_diagnostic(PytestErroneousUseFixturesOnFixture, expr.range()); let line_range = checker.locator().full_lines_range(expr.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(line_range))); - checker.report_diagnostic(diagnostic); } } } @@ -886,35 +952,35 @@ pub(crate) fn fixture( ) { let decorator = fixture_decorator(decorators, checker.semantic()); if let Some(decorator) = decorator { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - || checker.enabled(Rule::PytestFixturePositionalArgs) - || checker.enabled(Rule::PytestExtraneousScopeFunction) + if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle) + || checker.is_rule_enabled(Rule::PytestFixturePositionalArgs) + || checker.is_rule_enabled(Rule::PytestExtraneousScopeFunction) { check_fixture_decorator(checker, name, decorator); } - if checker.enabled(Rule::PytestDeprecatedYieldFixture) { + if checker.is_rule_enabled(Rule::PytestDeprecatedYieldFixture) { check_fixture_decorator_name(checker, decorator); } - if checker.enabled(Rule::PytestUselessYieldFixture) + if checker.is_rule_enabled(Rule::PytestUselessYieldFixture) && !is_abstract(decorators, checker.semantic()) { check_fixture_returns(checker, name, body, returns); } - if checker.enabled(Rule::PytestFixtureFinalizerCallback) { + if checker.is_rule_enabled(Rule::PytestFixtureFinalizerCallback) { check_fixture_addfinalizer(checker, parameters, body); } - if checker.enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) - || checker.enabled(Rule::PytestErroneousUseFixturesOnFixture) + if checker.is_rule_enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) + || checker.is_rule_enabled(Rule::PytestErroneousUseFixturesOnFixture) { check_fixture_marks(checker, decorators); } } - if checker.enabled(Rule::PytestFixtureParamWithoutValue) && name.starts_with("test_") { - check_test_function_args(checker, parameters); + if checker.is_rule_enabled(Rule::PytestFixtureParamWithoutValue) && name.starts_with("test_") { + check_test_function_args(checker, parameters, decorators); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs deleted file mode 100644 index 7b886dd5cc653..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::checkers::ast::Checker; -use ruff_python_ast::helpers::map_callable; -use ruff_python_ast::name::UnqualifiedName; -use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall, Keyword, Stmt, StmtFunctionDef}; -use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::{ScopeKind, SemanticModel}; -use ruff_python_trivia::PythonWhitespace; -use std::fmt; - -pub(super) fn get_mark_decorators( - decorators: &[Decorator], -) -> impl Iterator { - decorators.iter().filter_map(|decorator| { - let name = UnqualifiedName::from_expr(map_callable(&decorator.expression))?; - let ["pytest", "mark", marker] = name.segments() else { - return None; - }; - Some((decorator, *marker)) - }) -} - -pub(super) fn is_pytest_fail(call: &Expr, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(call) - .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "fail"])) -} - -pub(super) fn is_pytest_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(map_callable(&decorator.expression)) - .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "fixture"])) -} - -pub(super) fn is_pytest_yield_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(map_callable(&decorator.expression)) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["pytest", "yield_fixture"]) - }) -} - -pub(super) fn is_pytest_parametrize(call: &ExprCall, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(&call.func) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["pytest", "mark", "parametrize"]) - }) -} - -/// Whether the currently checked `func` is likely to be a Pytest test. -/// -/// A normal Pytest test function is one whose name starts with `test` and is either: -/// -/// * Placed at module-level, or -/// * Placed within a class whose name starts with `Test` and does not have an `__init__` method. -/// -/// During test discovery, Pytest respects a few settings which we do not have access to. -/// This function is thus prone to both false positives and false negatives. -/// -/// References: -/// - [`pytest` documentation: Conventions for Python test discovery](https://docs.pytest.org/en/stable/explanation/goodpractices.html#conventions-for-python-test-discovery) -/// - [`pytest` documentation: Changing naming conventions](https://docs.pytest.org/en/stable/example/pythoncollection.html#changing-naming-conventions) -pub(crate) fn is_likely_pytest_test(func: &StmtFunctionDef, checker: &Checker) -> bool { - let semantic = checker.semantic(); - - if !func.name.starts_with("test") { - return false; - } - - if semantic.scope_id.is_global() { - return true; - } - - let ScopeKind::Class(class) = semantic.current_scope().kind else { - return false; - }; - - if !class.name.starts_with("Test") { - return false; - } - - class.body.iter().all(|stmt| { - let Stmt::FunctionDef(function) = stmt else { - return true; - }; - - !visibility::is_init(&function.name) - }) -} - -pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool { - if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &keyword.value { - value == literal - } else { - false - } -} - -pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool { - match expr { - Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(), - Expr::NoneLiteral(_) => true, - Expr::FString(ast::ExprFString { value, .. }) => { - value.iter().all(|f_string_part| match f_string_part { - ast::FStringPart::Literal(literal) => literal.is_empty(), - ast::FStringPart::FString(f_string) => f_string - .elements - .iter() - .all(is_empty_or_null_fstring_element), - }) - } - _ => false, - } -} - -fn is_empty_or_null_fstring_element(element: &ast::FStringElement) -> bool { - match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => value.is_empty(), - ast::FStringElement::Expression(ast::FStringExpressionElement { expression, .. }) => { - is_empty_or_null_string(expression) - } - } -} - -pub(super) fn split_names(names: &str) -> Vec<&str> { - // Match the following pytest code: - // [x.strip() for x in argnames.split(",") if x.strip()] - names - .split(',') - .filter_map(|s| { - let trimmed = s.trim_whitespace(); - if trimmed.is_empty() { - None - } else { - Some(trimmed) - } - }) - .collect::>() -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(super) enum Parentheses { - None, - Empty, -} - -impl fmt::Display for Parentheses { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Parentheses::None => fmt.write_str(""), - Parentheses::Empty => fmt.write_str("()"), - } - } -} diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs index 830dd882bd937..2b6c1261852be 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/imports.rs @@ -1,9 +1,10 @@ use ruff_python_ast::Stmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for incorrect import of pytest. /// @@ -36,39 +37,26 @@ fn is_pytest_or_subpackage(imported_name: &str) -> bool { } /// PT013 -pub(crate) fn import(import_from: &Stmt, name: &str, asname: Option<&str>) -> Option { +pub(crate) fn import(checker: &Checker, import_from: &Stmt, name: &str, asname: Option<&str>) { if is_pytest_or_subpackage(name) { if let Some(alias) = asname { if alias != name { - return Some(Diagnostic::new( - PytestIncorrectPytestImport, - import_from.range(), - )); + checker.report_diagnostic(PytestIncorrectPytestImport, import_from.range()); } } } - None } /// PT013 -pub(crate) fn import_from( - import_from: &Stmt, - module: Option<&str>, - level: u32, -) -> Option { +pub(crate) fn import_from(checker: &Checker, import_from: &Stmt, module: Option<&str>, level: u32) { // If level is not zero or module is none, return if level != 0 { - return None; + return; } if let Some(module) = module { if is_pytest_or_subpackage(module) { - return Some(Diagnostic::new( - PytestIncorrectPytestImport, - import_from.range(), - )); + checker.report_diagnostic(PytestIncorrectPytestImport, import_from.range()); } } - - None } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs index e538db57da29f..4931a24c9de1c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs @@ -1,13 +1,14 @@ +use ruff_diagnostics::Applicability; use ruff_python_ast::{self as ast, Arguments, Decorator, Expr}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::helpers::{get_mark_decorators, Parentheses}; +use crate::rules::flake8_pytest_style::helpers::{Parentheses, get_mark_decorators}; /// ## What it does /// Checks for argument-free `@pytest.mark.()` decorators with or @@ -30,7 +31,7 @@ use super::helpers::{get_mark_decorators, Parentheses}; /// import pytest /// /// -/// @pytest.mark.foo +/// @pytest.mark.foo() /// def test_something(): ... /// ``` /// @@ -40,7 +41,20 @@ use super::helpers::{get_mark_decorators, Parentheses}; /// import pytest /// /// -/// @pytest.mark.foo() +/// @pytest.mark.foo +/// def test_something(): ... +/// ``` +/// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the +/// `pytest.mark.` decorator. +/// ```python +/// import pytest +/// +/// +/// @pytest.mark.foo( +/// # comment +/// ) /// def test_something(): ... /// ``` /// @@ -126,7 +140,7 @@ fn pytest_mark_parentheses( preferred: Parentheses, actual: Parentheses, ) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestIncorrectMarkParenthesesStyle { mark_name: marker.to_string(), expected_parens: preferred, @@ -135,26 +149,28 @@ fn pytest_mark_parentheses( decorator.range(), ); diagnostic.set_fix(fix); - checker.report_diagnostic(diagnostic); } fn check_mark_parentheses(checker: &Checker, decorator: &Decorator, marker: &str) { match &decorator.expression { Expr::Call(ast::ExprCall { - func, - arguments: - Arguments { - args, - keywords, - range: _, - }, + func: _, + arguments, range: _, + node_index: _, }) => { - if !checker.settings.flake8_pytest_style.mark_parentheses - && args.is_empty() - && keywords.is_empty() + if !checker.settings().flake8_pytest_style.mark_parentheses + && arguments.args.is_empty() + && arguments.keywords.is_empty() { - let fix = Fix::safe_edit(Edit::deletion(func.end(), decorator.end())); + let fix = Fix::applicable_edit( + Edit::range_deletion(arguments.range()), + if checker.comment_ranges().intersects(arguments.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + ); pytest_mark_parentheses( checker, decorator, @@ -166,7 +182,7 @@ fn check_mark_parentheses(checker: &Checker, decorator: &Decorator, marker: &str } } _ => { - if checker.settings.flake8_pytest_style.mark_parentheses { + if checker.settings().flake8_pytest_style.mark_parentheses { let fix = Fix::safe_edit(Edit::insertion( Parentheses::Empty.to_string(), decorator.end(), @@ -204,16 +220,18 @@ fn check_useless_usefixtures(checker: &Checker, decorator: &Decorator, marker: & _ => return, } - let mut diagnostic = Diagnostic::new(PytestUseFixturesWithoutParameters, decorator.range()); + let mut diagnostic = + checker.report_diagnostic(PytestUseFixturesWithoutParameters, decorator.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(decorator.range()))); - checker.report_diagnostic(diagnostic); } +/// PT023, PT026 pub(crate) fn marks(checker: &Checker, decorators: &[Decorator]) { - let enforce_parentheses = checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle); - let enforce_useless_usefixtures = checker.enabled(Rule::PytestUseFixturesWithoutParameters); + let enforce_parentheses = checker.is_rule_enabled(Rule::PytestIncorrectMarkParenthesesStyle); + let enforce_useless_usefixtures = + checker.is_rule_enabled(Rule::PytestUseFixturesWithoutParameters); - for (decorator, marker) in get_mark_decorators(decorators) { + for (decorator, marker) in get_mark_decorators(decorators, checker.semantic()) { if enforce_parentheses { check_mark_parentheses(checker, decorator, marker); } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/mod.rs index 128a1c5b4ff4b..f9ba74a16f726 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/mod.rs @@ -12,7 +12,6 @@ pub(crate) use warns::*; mod assertion; mod fail; mod fixture; -mod helpers; mod imports; mod marks; mod parametrize; diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs index 534f851bdd36f..bd3059e430a25 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -1,7 +1,6 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags}; @@ -12,9 +11,10 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::registry::Rule; +use crate::{Edit, Fix, FixAvailability, Violation}; -use super::super::types; -use super::helpers::{is_pytest_parametrize, split_names}; +use crate::rules::flake8_pytest_style::helpers::{is_pytest_parametrize, split_names}; +use crate::rules::flake8_pytest_style::types; /// ## What it does /// Checks for the type of parameter names passed to `pytest.mark.parametrize`. @@ -92,7 +92,9 @@ impl Violation for PytestParametrizeNamesWrongType { } } }; - format!("Wrong type passed to first argument of `pytest.mark.parametrize`; expected {expected_string}") + format!( + "Wrong type passed to first argument of `pytest.mark.parametrize`; expected {expected_string}" + ) } fn fix_title(&self) -> Option { @@ -299,6 +301,7 @@ fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) - }) .into_boxed_str(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), flags, }); Some(generator.expr(&node)) @@ -332,7 +335,10 @@ fn get_parametrize_name_range( /// PT006 fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr) { - let names_type = checker.settings.flake8_pytest_style.parametrize_names_type; + let names_type = checker + .settings() + .flake8_pytest_style + .parametrize_names_type; match expr { Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { @@ -347,7 +353,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr checker.locator().contents(), ) .unwrap_or(expr.range()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, expected: names_type, @@ -361,19 +367,20 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr Expr::from(ast::StringLiteral { value: Box::from(*name), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), flags: checker.default_string_flags(), }) }) .collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( format!("({})", checker.generator().expr(&node)), name_range, ))); - checker.report_diagnostic(diagnostic); } types::ParametrizeNameType::List => { let name_range = get_parametrize_name_range( @@ -383,7 +390,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr checker.locator().contents(), ) .unwrap_or(expr.range()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, expected: names_type, @@ -397,18 +404,19 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr Expr::from(ast::StringLiteral { value: Box::from(*name), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), flags: checker.default_string_flags(), }) }) .collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&node), name_range, ))); - checker.report_diagnostic(diagnostic); } types::ParametrizeNameType::Csv => {} } @@ -421,7 +429,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr match names_type { types::ParametrizeNameType::Tuple => {} types::ParametrizeNameType::List => { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, expected: names_type, @@ -432,15 +440,15 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr elts: elts.clone(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&node), expr.range(), ))); - checker.report_diagnostic(diagnostic); } types::ParametrizeNameType::Csv => { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, expected: names_type, @@ -455,7 +463,6 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr expr.range(), ))); } - checker.report_diagnostic(diagnostic); } } } @@ -467,7 +474,7 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr match names_type { types::ParametrizeNameType::List => {} types::ParametrizeNameType::Tuple => { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, expected: names_type, @@ -478,16 +485,16 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr elts: elts.clone(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( format!("({})", checker.generator().expr(&node)), expr.range(), ))); - checker.report_diagnostic(diagnostic); } types::ParametrizeNameType::Csv => { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: false, expected: names_type, @@ -502,7 +509,6 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr expr.range(), ))); } - checker.report_diagnostic(diagnostic); } } } @@ -513,10 +519,13 @@ fn check_names(checker: &Checker, call: &ExprCall, expr: &Expr, argvalues: &Expr /// PT007 fn check_values(checker: &Checker, names: &Expr, values: &Expr) { - let values_type = checker.settings.flake8_pytest_style.parametrize_values_type; + let values_type = checker + .settings() + .flake8_pytest_style + .parametrize_values_type; let values_row_type = checker - .settings + .settings() .flake8_pytest_style .parametrize_values_row_type; @@ -529,7 +538,7 @@ fn check_values(checker: &Checker, names: &Expr, values: &Expr) { match values { Expr::List(ast::ExprList { elts, .. }) => { if values_type != types::ParametrizeValuesType::List { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeValuesWrongType { values: values_type, row: values_row_type, @@ -568,7 +577,6 @@ fn check_values(checker: &Checker, names: &Expr, values: &Expr) { ); Fix::unsafe_edits(values_start, [values_end]) }); - checker.report_diagnostic(diagnostic); } if is_multi_named { @@ -577,7 +585,7 @@ fn check_values(checker: &Checker, names: &Expr, values: &Expr) { } Expr::Tuple(ast::ExprTuple { elts, .. }) => { if values_type != types::ParametrizeValuesType::Tuple { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeValuesWrongType { values: values_type, row: values_row_type, @@ -616,7 +624,6 @@ fn check_values(checker: &Checker, names: &Expr, values: &Expr) { Fix::unsafe_edits(values_start, [values_end]) }); - checker.report_diagnostic(diagnostic); } if is_multi_named { @@ -662,7 +669,7 @@ fn check_duplicates(checker: &Checker, values: &Expr) { let expr = ComparableExpr::from(element); seen.entry(expr) .and_modify(|index| { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestDuplicateParametrizeTestCases { index: *index }, element.range(), ); @@ -677,7 +684,6 @@ fn check_duplicates(checker: &Checker, values: &Expr) { diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(deletion_range))); } } - checker.report_diagnostic(diagnostic); }) .or_insert(index); prev = Some(element); @@ -685,7 +691,7 @@ fn check_duplicates(checker: &Checker, values: &Expr) { } fn handle_single_name(checker: &Checker, argnames: &Expr, value: &Expr, argvalues: &Expr) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeNamesWrongType { single_argument: true, expected: types::ParametrizeNameType::Csv, @@ -728,7 +734,6 @@ fn handle_single_name(checker: &Checker, argnames: &Expr, value: &Expr, argvalue Fix::safe_edits(argnames_edit, argvalues_edits) }; diagnostic.set_fix(fix); - checker.report_diagnostic(diagnostic); } /// Generate [`Edit`]s to unpack single-element lists or tuples in the given [`Expr`]. @@ -773,7 +778,7 @@ fn handle_value_rows( match elt { Expr::Tuple(ast::ExprTuple { elts, .. }) => { if values_row_type != types::ParametrizeValuesRowType::Tuple { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeValuesWrongType { values: values_type, row: values_row_type, @@ -811,12 +816,11 @@ fn handle_value_rows( let elt_end = Edit::replacement("]".into(), start, elt.end()); Fix::unsafe_edits(elt_start, [elt_end]) }); - checker.report_diagnostic(diagnostic); } } Expr::List(ast::ExprList { elts, .. }) => { if values_row_type != types::ParametrizeValuesRowType::List { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PytestParametrizeValuesWrongType { values: values_type, row: values_row_type, @@ -855,7 +859,6 @@ fn handle_value_rows( ); Fix::unsafe_edits(elt_start, [elt_end]) }); - checker.report_diagnostic(diagnostic); } } _ => {} @@ -868,7 +871,7 @@ pub(crate) fn parametrize(checker: &Checker, call: &ExprCall) { return; } - if checker.enabled(Rule::PytestParametrizeNamesWrongType) { + if checker.is_rule_enabled(Rule::PytestParametrizeNamesWrongType) { let names = call.arguments.find_argument_value("argnames", 0); let values = call.arguments.find_argument_value("argvalues", 1); @@ -876,7 +879,7 @@ pub(crate) fn parametrize(checker: &Checker, call: &ExprCall) { check_names(checker, call, names, values); } } - if checker.enabled(Rule::PytestParametrizeValuesWrongType) { + if checker.is_rule_enabled(Rule::PytestParametrizeValuesWrongType) { let names = call.arguments.find_argument_value("argnames", 0); let values = call.arguments.find_argument_value("argvalues", 1); @@ -884,7 +887,7 @@ pub(crate) fn parametrize(checker: &Checker, call: &ExprCall) { check_values(checker, names, values); } } - if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) { + if checker.is_rule_enabled(Rule::PytestDuplicateParametrizeTestCases) { if let Some(values) = call.arguments.find_argument_value("argvalues", 1) { check_duplicates(checker, values); } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs index b245d8724cd6c..fef3f695eb00c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs @@ -1,11 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, Parameters}; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; + /// ## What it does /// Checks for mocked calls that use a dummy `lambda` function instead of /// `return_value`. @@ -73,19 +75,23 @@ impl<'a> Visitor<'a> for LambdaBodyVisitor<'a> { } } -fn check_patch_call(call: &ast::ExprCall, index: usize) -> Option { +fn check_patch_call(checker: &Checker, call: &ast::ExprCall, index: usize) { if call.arguments.find_keyword("return_value").is_some() { - return None; + return; } - let ast::ExprLambda { + let Some(ast::ExprLambda { parameters, body, range: _, - } = call + node_index: _, + }) = call .arguments - .find_argument_value("new", index)? - .as_lambda_expr()?; + .find_argument_value("new", index) + .and_then(|expr| expr.as_lambda_expr()) + else { + return; + }; // Walk the lambda body. If the lambda uses the arguments, then it's valid. if let Some(parameters) = parameters { @@ -95,16 +101,18 @@ fn check_patch_call(call: &ast::ExprCall, index: usize) -> Option { }; visitor.visit_expr(body); if visitor.uses_args { - return None; + return; } } - Some(Diagnostic::new(PytestPatchWithLambda, call.func.range())) + checker.report_diagnostic(PytestPatchWithLambda, call.func.range()); } /// PT008 -pub(crate) fn patch_with_lambda(call: &ast::ExprCall) -> Option { - let name = UnqualifiedName::from_expr(&call.func)?; +pub(crate) fn patch_with_lambda(checker: &Checker, call: &ast::ExprCall) { + let Some(name) = UnqualifiedName::from_expr(&call.func) else { + return; + }; if matches!( name.segments(), @@ -118,7 +126,7 @@ pub(crate) fn patch_with_lambda(call: &ast::ExprCall) -> Option { "patch" ] | ["unittest", "mock", "patch"] ) { - check_patch_call(call, 1) + check_patch_call(checker, call, 1); } else if matches!( name.segments(), [ @@ -132,8 +140,6 @@ pub(crate) fn patch_with_lambda(call: &ast::ExprCall) -> Option { "object" ] | ["unittest", "mock", "patch", "object"] ) { - check_patch_call(call, 2) - } else { - None + check_patch_call(checker, call, 2); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs index 8cd0ea6335b55..feacfbe937728 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_compound_statement; use ruff_python_ast::{self as ast, Expr, Stmt, WithItem}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; -use super::helpers::is_empty_or_null_string; +use crate::rules::flake8_pytest_style::helpers::is_empty_or_null_string; /// ## What it does /// Checks for `pytest.raises` context managers with multiple statements. @@ -170,22 +170,20 @@ const fn is_non_trivial_with_body(body: &[Stmt]) -> bool { } } +/// PT010 pub(crate) fn raises_call(checker: &Checker, call: &ast::ExprCall) { if is_pytest_raises(&call.func, checker.semantic()) { - if checker.enabled(Rule::PytestRaisesWithoutException) { + if checker.is_rule_enabled(Rule::PytestRaisesWithoutException) { if call .arguments .find_argument("expected_exception", 0) .is_none() { - checker.report_diagnostic(Diagnostic::new( - PytestRaisesWithoutException, - call.func.range(), - )); + checker.report_diagnostic(PytestRaisesWithoutException, call.func.range()); } } - if checker.enabled(Rule::PytestRaisesTooBroad) { + if checker.is_rule_enabled(Rule::PytestRaisesTooBroad) { // Pytest.raises has two overloads // ```py // with raises(expected_exception: type[E] | tuple[type[E], ...], *, match: str | Pattern[str] | None = ...) → RaisesContext[E] as excinfo @@ -208,6 +206,7 @@ pub(crate) fn raises_call(checker: &Checker, call: &ast::ExprCall) { } } +/// PT012 pub(crate) fn complex_raises(checker: &Checker, stmt: &Stmt, items: &[WithItem], body: &[Stmt]) { let raises_called = items.iter().any(|item| match &item.context_expr { Expr::Call(ast::ExprCall { func, .. }) => is_pytest_raises(func, checker.semantic()), @@ -234,10 +233,7 @@ pub(crate) fn complex_raises(checker: &Checker, stmt: &Stmt, items: &[WithItem], }; if is_too_complex { - checker.report_diagnostic(Diagnostic::new( - PytestRaisesWithMultipleStatements, - stmt.range(), - )); + checker.report_diagnostic(PytestRaisesWithMultipleStatements, stmt.range()); } } } @@ -250,13 +246,13 @@ fn exception_needs_match(checker: &Checker, exception: &Expr) { .and_then(|qualified_name| { let qualified_name = qualified_name.to_string(); checker - .settings + .settings() .flake8_pytest_style .raises_require_match_for .iter() .chain( &checker - .settings + .settings() .flake8_pytest_style .raises_extend_require_match_for, ) @@ -264,11 +260,11 @@ fn exception_needs_match(checker: &Checker, exception: &Expr) { .then_some(qualified_name) }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( PytestRaisesTooBroad { exception: qualified_name, }, exception.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs index 39ae6e247c749..810e783a2cc6f 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/test_functions.rs @@ -1,7 +1,7 @@ use crate::checkers::ast::Checker; -use crate::rules::flake8_pytest_style::rules::helpers::is_likely_pytest_test; -use ruff_diagnostics::{Diagnostic, Edit, Fix, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::rules::flake8_pytest_style::helpers::is_likely_pytest_test; +use crate::{Edit, Fix, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::StmtFunctionDef; use ruff_text_size::Ranged; @@ -61,9 +61,10 @@ pub(crate) fn parameter_with_default_argument(checker: &Checker, function_def: & }; let parameter_name = parameter.name().to_string(); let kind = PytestParameterWithDefaultArgument { parameter_name }; - let diagnostic = Diagnostic::new(kind, default.range()); let edit = Edit::deletion(parameter.parameter.end(), parameter.end()); let fix = Fix::display_only_edit(edit); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(kind, default.range()) + .set_fix(fix); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs index fce0ea72420b0..650452c6a0f86 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; use ruff_python_ast::name::Name; use ruff_python_ast::{ self as ast, Arguments, CmpOp, Expr, ExprContext, Identifier, Keyword, Stmt, UnaryOp, @@ -166,6 +166,7 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt { test: Box::new(expr.clone()), msg: msg.map(|msg| Box::new(msg.clone())), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } @@ -175,6 +176,7 @@ fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr { ops: Box::from([cmp_op]), comparators: Box::from([right.clone()]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } @@ -294,6 +296,7 @@ impl UnittestAssert { op: UnaryOp::Not, operand: Box::new(expr.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }), msg, ) @@ -367,6 +370,7 @@ impl UnittestAssert { }; let node = Expr::NoneLiteral(ast::ExprNoneLiteral { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let expr = compare(expr, cmp_op, &node); Ok(assert(&expr, msg)) @@ -383,6 +387,7 @@ impl UnittestAssert { id: Name::new_static("isinstance"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node1 = ast::ExprCall { func: Box::new(node.into()), @@ -390,8 +395,10 @@ impl UnittestAssert { args: Box::from([(**obj).clone(), (**cls).clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let isinstance = node1.into(); if matches!(self, UnittestAssert::IsInstance) { @@ -401,6 +408,7 @@ impl UnittestAssert { op: UnaryOp::Not, operand: Box::new(isinstance), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let expr = node.into(); Ok(assert(&expr, msg)) @@ -421,12 +429,14 @@ impl UnittestAssert { id: Name::new_static("re"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node1 = ast::ExprAttribute { value: Box::new(node.into()), attr: Identifier::new("search".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node2 = ast::ExprCall { func: Box::new(node1.into()), @@ -434,8 +444,10 @@ impl UnittestAssert { args: Box::from([(**regex).clone(), (**text).clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let re_search = node2.into(); if matches!(self, UnittestAssert::Regex | UnittestAssert::RegexpMatches) { @@ -445,6 +457,7 @@ impl UnittestAssert { op: UnaryOp::Not, operand: Box::new(re_search), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; Ok(assert(&node.into(), msg)) } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs index 32594a1c24e4c..55e1a7bb4827a 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_compound_statement; use ruff_python_ast::{self as ast, Expr, Stmt, WithItem}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; -use super::helpers::is_empty_or_null_string; +use crate::rules::flake8_pytest_style::helpers::is_empty_or_null_string; /// ## What it does /// Checks for `pytest.warns` context managers with multiple statements. @@ -76,11 +76,11 @@ impl Violation for PytestWarnsWithMultipleStatements { /// /// /// def test_foo(): -/// with pytest.warns(RuntimeWarning): +/// with pytest.warns(Warning): /// ... /// /// # empty string is also an error -/// with pytest.warns(RuntimeWarning, match=""): +/// with pytest.warns(Warning, match=""): /// ... /// ``` /// @@ -90,7 +90,7 @@ impl Violation for PytestWarnsWithMultipleStatements { /// /// /// def test_foo(): -/// with pytest.warns(RuntimeWarning, match="expected message"): +/// with pytest.warns(Warning, match="expected message"): /// ... /// ``` /// @@ -172,16 +172,13 @@ const fn is_non_trivial_with_body(body: &[Stmt]) -> bool { /// PT029, PT030 pub(crate) fn warns_call(checker: &Checker, call: &ast::ExprCall) { if is_pytest_warns(&call.func, checker.semantic()) { - if checker.enabled(Rule::PytestWarnsWithoutWarning) { + if checker.is_rule_enabled(Rule::PytestWarnsWithoutWarning) { if call.arguments.is_empty() { - checker.report_diagnostic(Diagnostic::new( - PytestWarnsWithoutWarning, - call.func.range(), - )); + checker.report_diagnostic(PytestWarnsWithoutWarning, call.func.range()); } } - if checker.enabled(Rule::PytestWarnsTooBroad) { + if checker.is_rule_enabled(Rule::PytestWarnsTooBroad) { if let Some(warning) = call.arguments.find_argument_value("expected_warning", 0) { if call .arguments @@ -222,10 +219,7 @@ pub(crate) fn complex_warns(checker: &Checker, stmt: &Stmt, items: &[WithItem], }; if is_too_complex { - checker.report_diagnostic(Diagnostic::new( - PytestWarnsWithMultipleStatements, - stmt.range(), - )); + checker.report_diagnostic(PytestWarnsWithMultipleStatements, stmt.range()); } } } @@ -239,13 +233,13 @@ fn warning_needs_match(checker: &Checker, warning: &Expr) { .and_then(|qualified_name| { let qualified_name = qualified_name.to_string(); checker - .settings + .settings() .flake8_pytest_style .warns_require_match_for .iter() .chain( &checker - .settings + .settings() .flake8_pytest_style .warns_extend_require_match_for, ) @@ -253,11 +247,11 @@ fn warning_needs_match(checker: &Checker, warning: &Expr) { .then_some(qualified_name) }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( PytestWarnsTooBroad { warning: qualified_name, }, warning.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT001_default.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT001_default.snap index 8bcf1ce60d196..659e307c745ca 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT001_default.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT001_default.snap @@ -125,3 +125,66 @@ PT001.py:74:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` 74 |+@aliased 77 75 | def aliased_parentheses_no_params_multiline(): 78 76 | return 42 +79 77 | + +PT001.py:81:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` + | +80 | # https://github.com/astral-sh/ruff/issues/18770 +81 | / @pytest.fixture( +82 | | # TODO: use module scope +83 | | # scope="module" +84 | | ) + | |_^ PT001 +85 | def my_fixture(): ... + | + = help: Remove parentheses + +ℹ Unsafe fix +78 78 | return 42 +79 79 | +80 80 | # https://github.com/astral-sh/ruff/issues/18770 +81 |-@pytest.fixture( +82 |- # TODO: use module scope +83 |- # scope="module" +84 |-) + 81 |+@pytest.fixture +85 82 | def my_fixture(): ... +86 83 | +87 84 | + +PT001.py:88:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` + | +88 | @(pytest.fixture()) + | ^^^^^^^^^^^^^^^^^^^ PT001 +89 | def outer_paren_fixture_no_params(): +90 | return 42 + | + = help: Remove parentheses + +ℹ Safe fix +85 85 | def my_fixture(): ... +86 86 | +87 87 | +88 |-@(pytest.fixture()) + 88 |+@(pytest.fixture) +89 89 | def outer_paren_fixture_no_params(): +90 90 | return 42 +91 91 | + +PT001.py:93:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` + | +93 | @(fixture()) + | ^^^^^^^^^^^^ PT001 +94 | def outer_paren_imported_fixture_no_params(): +95 | return 42 + | + = help: Remove parentheses + +ℹ Safe fix +90 90 | return 42 +91 91 | +92 92 | +93 |-@(fixture()) + 93 |+@(fixture) +94 94 | def outer_paren_imported_fixture_no_params(): +95 95 | return 42 diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT019.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT019.snap index 309e4a19ec029..2c2abc05e472f 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT019.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT019.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs -snapshot_kind: text --- PT019.py:9:14: PT019 Fixture `_fixture` without value is injected as parameter, use `@pytest.mark.usefixtures` instead | @@ -15,3 +14,19 @@ PT019.py:13:17: PT019 Fixture `_fixture` without value is injected as parameter, | ^^^^^^^^ PT019 14 | pass | + +PT019.py:31:24: PT019 Fixture `_bar` without value is injected as parameter, use `@pytest.mark.usefixtures` instead + | +30 | @pytest.mark.parametrize("_foo", [1, 2, 3]) +31 | def test_thingy2(_foo, _bar): # Error _bar is not defined in parametrize + | ^^^^ PT019 +32 | pass + | + +PT019.py:39:24: PT019 Fixture `_bar` without value is injected as parameter, use `@pytest.mark.usefixtures` instead + | +38 | @pytest.mark.parametrize(("_foo"), [1, 2, 3]) +39 | def test_thingy4(_foo, _bar): # Error _bar is not defined in parametrize + | ^^^^ PT019 +40 | pass + | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap index 8240fc440b392..b28b6da12647d 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap @@ -1,101 +1,172 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs -snapshot_kind: text --- -PT023.py:46:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` - | -46 | @pytest.mark.foo() - | ^^^^^^^^^^^^^^^^^^ PT023 -47 | def test_something(): -48 | pass - | - = help: Remove parentheses - -ℹ Safe fix -43 43 | # With parentheses -44 44 | -45 45 | -46 |-@pytest.mark.foo() - 46 |+@pytest.mark.foo -47 47 | def test_something(): -48 48 | pass -49 49 | - PT023.py:51:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | 51 | @pytest.mark.foo() | ^^^^^^^^^^^^^^^^^^ PT023 -52 | class TestClass: -53 | def test_something(): +52 | def test_something(): +53 | pass | = help: Remove parentheses ℹ Safe fix -48 48 | pass +48 48 | # With parentheses 49 49 | 50 50 | 51 |-@pytest.mark.foo() 51 |+@pytest.mark.foo -52 52 | class TestClass: -53 53 | def test_something(): -54 54 | pass +52 52 | def test_something(): +53 53 | pass +54 54 | -PT023.py:58:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` +PT023.py:56:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | +56 | @pytest.mark.foo() + | ^^^^^^^^^^^^^^^^^^ PT023 57 | class TestClass: -58 | @pytest.mark.foo() - | ^^^^^^^^^^^^^^^^^^ PT023 -59 | def test_something(): -60 | pass +58 | def test_something(): | = help: Remove parentheses ℹ Safe fix +53 53 | pass +54 54 | 55 55 | -56 56 | +56 |-@pytest.mark.foo() + 56 |+@pytest.mark.foo 57 57 | class TestClass: -58 |- @pytest.mark.foo() - 58 |+ @pytest.mark.foo -59 59 | def test_something(): -60 60 | pass -61 61 | +58 58 | def test_something(): +59 59 | pass -PT023.py:64:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` +PT023.py:63:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | -63 | class TestClass: -64 | @pytest.mark.foo() +62 | class TestClass: +63 | @pytest.mark.foo() | ^^^^^^^^^^^^^^^^^^ PT023 -65 | class TestNestedClass: -66 | def test_something(): +64 | def test_something(): +65 | pass | = help: Remove parentheses ℹ Safe fix +60 60 | 61 61 | -62 62 | -63 63 | class TestClass: -64 |- @pytest.mark.foo() - 64 |+ @pytest.mark.foo -65 65 | class TestNestedClass: -66 66 | def test_something(): -67 67 | pass +62 62 | class TestClass: +63 |- @pytest.mark.foo() + 63 |+ @pytest.mark.foo +64 64 | def test_something(): +65 65 | pass +66 66 | + +PT023.py:69:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` + | +68 | class TestClass: +69 | @pytest.mark.foo() + | ^^^^^^^^^^^^^^^^^^ PT023 +70 | class TestNestedClass: +71 | def test_something(): + | + = help: Remove parentheses -PT023.py:72:9: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` +ℹ Safe fix +66 66 | +67 67 | +68 68 | class TestClass: +69 |- @pytest.mark.foo() + 69 |+ @pytest.mark.foo +70 70 | class TestNestedClass: +71 71 | def test_something(): +72 72 | pass + +PT023.py:77:9: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | -70 | class TestClass: -71 | class TestNestedClass: -72 | @pytest.mark.foo() +75 | class TestClass: +76 | class TestNestedClass: +77 | @pytest.mark.foo() | ^^^^^^^^^^^^^^^^^^ PT023 -73 | def test_something(): -74 | pass +78 | def test_something(): +79 | pass + | + = help: Remove parentheses + +ℹ Safe fix +74 74 | +75 75 | class TestClass: +76 76 | class TestNestedClass: +77 |- @pytest.mark.foo() + 77 |+ @pytest.mark.foo +78 78 | def test_something(): +79 79 | pass +80 80 | + +PT023.py:82:1: PT023 [*] Use `@pytest.mark.parametrize` over `@pytest.mark.parametrize()` + | +81 | # https://github.com/astral-sh/ruff/issues/18770 +82 | / @pytest.mark.parametrize( +83 | | # TODO: fix later +84 | | # ("param1", "param2"), +85 | | # ( +86 | | # (1, 2), +87 | | # (3, 4), +88 | | # ), +89 | | ) + | |_^ PT023 +90 | def test_bar(param1, param2): ... | = help: Remove parentheses +ℹ Unsafe fix +79 79 | pass +80 80 | +81 81 | # https://github.com/astral-sh/ruff/issues/18770 +82 |-@pytest.mark.parametrize( +83 |- # TODO: fix later +84 |- # ("param1", "param2"), +85 |- # ( +86 |- # (1, 2), +87 |- # (3, 4), +88 |- # ), +89 |-) + 82 |+@pytest.mark.parametrize +90 83 | def test_bar(param1, param2): ... +91 84 | +92 85 | + +PT023.py:93:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` + | +93 | @(pytest.mark.foo()) + | ^^^^^^^^^^^^^^^^^^^^ PT023 +94 | def test_outer_paren_mark_function(): +95 | pass + | + = help: Remove parentheses + +ℹ Safe fix +90 90 | def test_bar(param1, param2): ... +91 91 | +92 92 | +93 |-@(pytest.mark.foo()) + 93 |+@(pytest.mark.foo) +94 94 | def test_outer_paren_mark_function(): +95 95 | pass +96 96 | + +PT023.py:99:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` + | + 98 | class TestClass: + 99 | @(pytest.mark.foo()) + | ^^^^^^^^^^^^^^^^^^^^ PT023 +100 | def test_method_outer_paren(): +101 | pass + | + = help: Remove parentheses + ℹ Safe fix -69 69 | -70 70 | class TestClass: -71 71 | class TestNestedClass: -72 |- @pytest.mark.foo() - 72 |+ @pytest.mark.foo -73 73 | def test_something(): -74 74 | pass +96 96 | +97 97 | +98 98 | class TestClass: +99 |- @(pytest.mark.foo()) + 99 |+ @(pytest.mark.foo) +100 100 | def test_method_outer_paren(): +101 101 | pass diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap index 4021157c87f2c..a992327458490 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap @@ -1,102 +1,101 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs -snapshot_kind: text --- -PT023.py:12:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` - | -12 | @pytest.mark.foo - | ^^^^^^^^^^^^^^^^ PT023 -13 | def test_something(): -14 | pass - | - = help: Add parentheses - -ℹ Safe fix -9 9 | # Without parentheses -10 10 | -11 11 | -12 |-@pytest.mark.foo - 12 |+@pytest.mark.foo() -13 13 | def test_something(): -14 14 | pass -15 15 | - PT023.py:17:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | 17 | @pytest.mark.foo | ^^^^^^^^^^^^^^^^ PT023 -18 | class TestClass: -19 | def test_something(): +18 | def test_something(): +19 | pass | = help: Add parentheses ℹ Safe fix -14 14 | pass +14 14 | # Without parentheses 15 15 | 16 16 | 17 |-@pytest.mark.foo 17 |+@pytest.mark.foo() -18 18 | class TestClass: -19 19 | def test_something(): -20 20 | pass +18 18 | def test_something(): +19 19 | pass +20 20 | -PT023.py:24:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` +PT023.py:22:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | +22 | @pytest.mark.foo + | ^^^^^^^^^^^^^^^^ PT023 23 | class TestClass: -24 | @pytest.mark.foo - | ^^^^^^^^^^^^^^^^ PT023 -25 | def test_something(): -26 | pass +24 | def test_something(): | = help: Add parentheses ℹ Safe fix +19 19 | pass +20 20 | 21 21 | -22 22 | +22 |-@pytest.mark.foo + 22 |+@pytest.mark.foo() 23 23 | class TestClass: -24 |- @pytest.mark.foo - 24 |+ @pytest.mark.foo() -25 25 | def test_something(): -26 26 | pass -27 27 | +24 24 | def test_something(): +25 25 | pass -PT023.py:30:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` +PT023.py:29:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | -29 | class TestClass: -30 | @pytest.mark.foo +28 | class TestClass: +29 | @pytest.mark.foo | ^^^^^^^^^^^^^^^^ PT023 -31 | class TestNestedClass: -32 | def test_something(): +30 | def test_something(): +31 | pass | = help: Add parentheses ℹ Safe fix +26 26 | 27 27 | -28 28 | -29 29 | class TestClass: -30 |- @pytest.mark.foo - 30 |+ @pytest.mark.foo() -31 31 | class TestNestedClass: -32 32 | def test_something(): -33 33 | pass +28 28 | class TestClass: +29 |- @pytest.mark.foo + 29 |+ @pytest.mark.foo() +30 30 | def test_something(): +31 31 | pass +32 32 | + +PT023.py:35:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` + | +34 | class TestClass: +35 | @pytest.mark.foo + | ^^^^^^^^^^^^^^^^ PT023 +36 | class TestNestedClass: +37 | def test_something(): + | + = help: Add parentheses + +ℹ Safe fix +32 32 | +33 33 | +34 34 | class TestClass: +35 |- @pytest.mark.foo + 35 |+ @pytest.mark.foo() +36 36 | class TestNestedClass: +37 37 | def test_something(): +38 38 | pass -PT023.py:38:9: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` +PT023.py:43:9: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | -36 | class TestClass: -37 | class TestNestedClass: -38 | @pytest.mark.foo +41 | class TestClass: +42 | class TestNestedClass: +43 | @pytest.mark.foo | ^^^^^^^^^^^^^^^^ PT023 -39 | def test_something(): -40 | pass +44 | def test_something(): +45 | pass | = help: Add parentheses ℹ Safe fix -35 35 | -36 36 | class TestClass: -37 37 | class TestNestedClass: -38 |- @pytest.mark.foo - 38 |+ @pytest.mark.foo() -39 39 | def test_something(): -40 40 | pass -41 41 | +40 40 | +41 41 | class TestClass: +42 42 | class TestNestedClass: +43 |- @pytest.mark.foo + 43 |+ @pytest.mark.foo() +44 44 | def test_something(): +45 45 | pass +46 46 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index a684391bcdf23..b342d92f1c26f 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -10,7 +10,7 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::test::test_path; @@ -46,7 +46,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -65,7 +65,7 @@ mod tests { .with_target_version(PythonVersion::PY311) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -84,7 +84,7 @@ mod tests { .with_target_version(PythonVersion::PY311) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -116,7 +116,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -154,7 +154,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -192,7 +192,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -211,7 +211,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::BadQuotesInlineString]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -230,7 +230,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::BadQuotesMultilineString]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -249,7 +249,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::BadQuotesDocstring]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index 91ec9c16b2d9e..3a15f45154fcc 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -1,15 +1,13 @@ use flake8_quotes::helpers::{contains_escaped_quote, raw_contents, unescape_string}; use flake8_quotes::settings::Quote; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::visitor::{walk_f_string, Visitor}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::visitor::{Visitor, walk_f_string, walk_t_string}; use ruff_python_ast::{self as ast, AnyStringFlags, PythonVersion, StringFlags, StringLike}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::rules::flake8_quotes; -use crate::settings::LinterSettings; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for strings that include escaped quotes, and suggests changing @@ -21,12 +19,12 @@ use crate::Locator; /// /// ## Example /// ```python -/// foo = 'bar\'s' +/// foo = "bar\"s" /// ``` /// /// Use instead: /// ```python -/// foo = "bar's" +/// foo = 'bar"s' /// ``` /// /// ## Formatter compatibility @@ -56,16 +54,12 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike // This rule has support for strings nested inside another f-strings but they're checked // via the outermost f-string. This means that we shouldn't be checking any nested string // or f-string. - || checker.semantic().in_f_string_replacement_field() + || checker.semantic().in_interpolated_string_replacement_field() { return; } - let mut rule_checker = AvoidableEscapedQuoteChecker::new( - checker.locator(), - checker.settings, - checker.target_version(), - ); + let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker, checker.target_version()); for part in string_like.parts() { match part { @@ -76,62 +70,45 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike rule_checker.visit_bytes_literal(bytes_literal); } ast::StringLikePart::FString(f_string) => rule_checker.visit_f_string(f_string), + ast::StringLikePart::TString(t_string) => rule_checker.visit_t_string(t_string), } } - - checker.report_diagnostics(rule_checker.into_diagnostics()); } /// Checks for `Q003` violations using the [`Visitor`] implementation. -#[derive(Debug)] -struct AvoidableEscapedQuoteChecker<'a> { - locator: &'a Locator<'a>, +struct AvoidableEscapedQuoteChecker<'a, 'b> { + checker: &'a Checker<'b>, quotes_settings: &'a flake8_quotes::settings::Settings, supports_pep701: bool, - diagnostics: Vec, } -impl<'a> AvoidableEscapedQuoteChecker<'a> { - fn new( - locator: &'a Locator<'a>, - settings: &'a LinterSettings, - target_version: PythonVersion, - ) -> Self { +impl<'a, 'b> AvoidableEscapedQuoteChecker<'a, 'b> { + fn new(checker: &'a Checker<'b>, target_version: PythonVersion) -> Self { Self { - locator, - quotes_settings: &settings.flake8_quotes, + checker, + quotes_settings: &checker.settings().flake8_quotes, supports_pep701: target_version.supports_pep_701(), - diagnostics: vec![], } } - - /// Consumes the checker and returns a vector of [`Diagnostic`] found during the visit. - fn into_diagnostics(self) -> Vec { - self.diagnostics - } } -impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> { - fn visit_string_literal(&mut self, string_literal: &'_ ast::StringLiteral) { - if let Some(diagnostic) = check_string_or_bytes( - self.locator, +impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_, '_> { + fn visit_string_literal(&mut self, string_literal: &ast::StringLiteral) { + check_string_or_bytes( + self.checker, self.quotes_settings, string_literal.range(), AnyStringFlags::from(string_literal.flags), - ) { - self.diagnostics.push(diagnostic); - } + ); } - fn visit_bytes_literal(&mut self, bytes_literal: &'_ ast::BytesLiteral) { - if let Some(diagnostic) = check_string_or_bytes( - self.locator, + fn visit_bytes_literal(&mut self, bytes_literal: &ast::BytesLiteral) { + check_string_or_bytes( + self.checker, self.quotes_settings, bytes_literal.range(), AnyStringFlags::from(bytes_literal.flags), - ) { - self.diagnostics.push(diagnostic); - } + ); } fn visit_f_string(&mut self, f_string: &'_ ast::FString) { @@ -203,35 +180,80 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_> { .literals() .any(|literal| contains_quote(literal, opposite_quote_char)) { - if let Some(diagnostic) = check_f_string(self.locator, self.quotes_settings, f_string) { - self.diagnostics.push(diagnostic); - } + check_interpolated_string( + self.checker, + self.quotes_settings, + AnyStringFlags::from(f_string.flags), + &f_string.elements, + f_string.range, + ); } walk_f_string(self, f_string); } + + fn visit_t_string(&mut self, t_string: &'_ ast::TString) { + let opposite_quote_char = self.quotes_settings.inline_quotes.opposite().as_char(); + + // If any literal part of this t-string contains the quote character which is opposite to + // the configured inline quotes, we can't change the quote style for this t-string. For + // example: + // + // ```py + // t"\"hello\" {x} 'world'" + // ``` + // + // If we try to fix the above example, the t-string will end in the middle and "world" will + // be considered as a variable which is outside this t-string: + // + // ```py + // t'"hello" {x} 'world'' + // # ^ + // # t-string ends here now + // ``` + // + // The check is local to this t-string and it shouldn't check for any literal parts of any + // nested t-string. + if !t_string + .elements + .literals() + .any(|literal| contains_quote(literal, opposite_quote_char)) + { + check_interpolated_string( + self.checker, + self.quotes_settings, + AnyStringFlags::from(t_string.flags), + &t_string.elements, + t_string.range, + ); + } + + walk_t_string(self, t_string); + } } /// Checks for unnecessary escaped quotes in a string or bytes literal. /// /// # Panics /// -/// If the string kind is an f-string. +/// If the string kind is an f-string or a t-string. fn check_string_or_bytes( - locator: &Locator, + checker: &Checker, quotes_settings: &flake8_quotes::settings::Settings, range: TextRange, flags: AnyStringFlags, -) -> Option { - assert!(!flags.is_f_string()); +) { + assert!(!flags.is_interpolated_string()); + + let locator = checker.locator(); if flags.is_triple_quoted() || flags.is_raw_string() { - return None; + return; } // Check if we're using the preferred quotation style. if Quote::from(flags.quote_style()) != quotes_settings.inline_quotes { - return None; + return; } let contents = raw_contents(locator.slice(range), flags); @@ -239,10 +261,10 @@ fn check_string_or_bytes( if !contains_escaped_quote(contents, quotes_settings.inline_quotes.as_char()) || contains_quote(contents, quotes_settings.inline_quotes.opposite().as_char()) { - return None; + return; } - let mut diagnostic = Diagnostic::new(AvoidableEscapedQuote, range); + let mut diagnostic = checker.report_diagnostic(AvoidableEscapedQuote, range); let fixed_contents = format!( "{prefix}{quote}{value}{quote}", prefix = flags.prefix(), @@ -253,32 +275,31 @@ fn check_string_or_bytes( fixed_contents, range, ))); - Some(diagnostic) } -/// Checks for unnecessary escaped quotes in an f-string. -fn check_f_string( - locator: &Locator, +/// Checks for unnecessary escaped quotes in an f-string or t-string. +fn check_interpolated_string( + checker: &Checker, quotes_settings: &flake8_quotes::settings::Settings, - f_string: &ast::FString, -) -> Option { - let ast::FString { flags, range, .. } = f_string; - + flags: ast::AnyStringFlags, + elements: &ast::InterpolatedStringElements, + range: TextRange, +) { if flags.is_triple_quoted() || flags.prefix().is_raw() { - return None; + return; } // Check if we're using the preferred quotation style. if Quote::from(flags.quote_style()) != quotes_settings.inline_quotes { - return None; + return; } let quote_char = quotes_settings.inline_quotes.as_char(); let opposite_quote_char = quotes_settings.inline_quotes.opposite().as_char(); let mut edits = vec![]; - for literal in f_string.elements.literals() { - let content = locator.slice(literal); + for literal in elements.literals() { + let content = checker.locator().slice(literal); if !contains_escaped_quote(content, quote_char) { continue; } @@ -289,13 +310,13 @@ fn check_f_string( } if edits.is_empty() { - return None; + return; } - // Replacement for the f-string opening quote. We don't perform the check for raw and + // Replacement for the f/t-string opening quote. We don't perform the check for raw and // triple-quoted f-strings, so no need to account for them. let start_edit = Edit::range_replacement( - format!("f{opposite_quote_char}"), + format!("{}{opposite_quote_char}", flags.prefix()), TextRange::at( range.start(), // Prefix + quote char @@ -303,16 +324,15 @@ fn check_f_string( ), ); - // Replacement for the f-string ending quote. We don't perform the check for triple-quoted + // Replacement for the f/t-string ending quote. We don't perform the check for triple-quoted // f-string, so no need to account for them. edits.push(Edit::range_replacement( opposite_quote_char.to_string(), TextRange::at( // Offset would either be the end offset of the start edit in case there are no - // elements in the f-string (e.g., `f""`) or the end offset of the last f-string + // elements in the f/t-string (e.g., `f""`) or the end offset of the last f/t-string // element (e.g., `f"hello"`). - f_string - .elements + elements .last() .map_or_else(|| start_edit.end(), Ranged::end), // Quote char @@ -320,9 +340,9 @@ fn check_f_string( ), )); - Some( - Diagnostic::new(AvoidableEscapedQuote, *range).with_fix(Fix::safe_edits(start_edit, edits)), - ) + checker + .report_diagnostic(AvoidableEscapedQuote, range) + .set_fix(Fix::safe_edits(start_edit, edits)); } #[derive(Debug, Default)] @@ -343,6 +363,11 @@ impl Visitor<'_> for ContainsAnyString { self.result = true; // We don't need to recurse into this f-string now that we already know the result. } + + fn visit_t_string(&mut self, _: &'_ ast::TString) { + self.result = true; + // We don't need to recurse into this t-string now that we already know the result. + } } /// Return `true` if the haystack contains the quote. diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index 764df33109724..aae3f18480ebc 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::StringLike; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::registry::Rule; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; -use super::super::settings::Quote; +use crate::rules::flake8_quotes::settings::Quote; /// ## What it does /// Checks for inline strings that use single quotes or double quotes, @@ -251,7 +251,7 @@ fn text_ends_at_quote(locator: &Locator, range: TextRange, quote: Quote) -> bool /// Q002 fn docstring(checker: &Checker, range: TextRange) { - let quotes_settings = &checker.settings.flake8_quotes; + let quotes_settings = &checker.settings().flake8_quotes; let locator = checker.locator(); let text = locator.slice(range); @@ -261,12 +261,12 @@ fn docstring(checker: &Checker, range: TextRange) { { // Fixing this would result in a one-sided multi-line docstring, which would // introduce a syntax error. - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadQuotesDocstring { preferred_quote: quotes_settings.docstring_quotes, }, range, - )); + ); return; } @@ -277,7 +277,7 @@ fn docstring(checker: &Checker, range: TextRange) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BadQuotesDocstring { preferred_quote: quotes_settings.docstring_quotes, }, @@ -298,12 +298,11 @@ fn docstring(checker: &Checker, range: TextRange) { fixed_contents, range, ))); - checker.report_diagnostic(diagnostic); } /// Q000, Q001 fn strings(checker: &Checker, sequence: &[TextRange]) { - let quotes_settings = &checker.settings.flake8_quotes; + let quotes_settings = &checker.settings().flake8_quotes; let locator = checker.locator(); let trivia = sequence @@ -333,7 +332,7 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { for (range, trivia) in sequence.iter().zip(trivia) { if trivia.is_multiline { // If multiline strings aren't enforced, ignore it. - if !checker.enabled(Rule::BadQuotesMultilineString) { + if !checker.is_rule_enabled(Rule::BadQuotesMultilineString) { continue; } @@ -353,7 +352,7 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { continue; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BadQuotesMultilineString { preferred_quote: quotes_settings.multiline_quotes, }, @@ -373,13 +372,12 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { fixed_contents, *range, ))); - checker.report_diagnostic(diagnostic); } else if trivia.last_quote_char != quotes_settings.inline_quotes.as_char() // If we're not using the preferred type, only allow use to avoid escapes. && !relax_quote { // If inline strings aren't enforced, ignore it. - if !checker.enabled(Rule::BadQuotesInlineString) { + if !checker.is_rule_enabled(Rule::BadQuotesInlineString) { continue; } @@ -391,12 +389,12 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { // ```python // ''"assert" ' SAM macro definitions ''' // ``` - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadQuotesInlineString { preferred_quote: quotes_settings.inline_quotes, }, *range, - )); + ); continue; } @@ -406,16 +404,16 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { // ```python // ''"assert" ' SAM macro definitions ''' // ``` - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadQuotesInlineString { preferred_quote: quotes_settings.inline_quotes, }, *range, - )); + ); continue; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BadQuotesInlineString { preferred_quote: quotes_settings.inline_quotes, }, @@ -433,7 +431,6 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { fixed_contents, *range, ))); - checker.report_diagnostic(diagnostic); } } } @@ -447,20 +444,24 @@ pub(crate) fn check_string_quotes(checker: &Checker, string_like: StringLike) { } // TODO(dhruvmanila): Support checking for escaped quotes in f-strings. - if checker.semantic().in_f_string_replacement_field() { + if checker + .semantic() + .in_interpolated_string_replacement_field() + { return; } let ranges: Vec<_> = string_like.parts().map(|part| part.range()).collect(); if checker.semantic().in_pep_257_docstring() { - if checker.enabled(Rule::BadQuotesDocstring) { + if checker.is_rule_enabled(Rule::BadQuotesDocstring) { for range in ranges { docstring(checker, range); } } } else { - if checker.any_enabled(&[Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString]) { + if checker.any_rule_enabled(&[Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString]) + { strings(checker, &ranges); } } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs index 5789d77a644b9..4100097e03d36 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs @@ -1,12 +1,13 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ + self as ast, AnyStringFlags, InterpolatedStringElements, StringFlags, StringLike, +}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix}; -use super::super::helpers::{contains_escaped_quote, raw_contents, unescape_string}; +use crate::rules::flake8_quotes::helpers::{contains_escaped_quote, raw_contents, unescape_string}; /// ## What it does /// Checks for strings that include unnecessarily escaped quotes. @@ -51,32 +52,33 @@ pub(crate) fn unnecessary_escaped_quote(checker: &Checker, string_like: StringLi return; } - let locator = checker.locator(); - for part in string_like.parts() { match part { - ast::StringLikePart::String(string_literal) => { - if let Some(diagnostic) = check_string_or_bytes( - locator, - string_literal.range(), - AnyStringFlags::from(string_literal.flags), - ) { - checker.report_diagnostic(diagnostic); - } - } - ast::StringLikePart::Bytes(bytes_literal) => { - if let Some(diagnostic) = check_string_or_bytes( - locator, - bytes_literal.range(), - AnyStringFlags::from(bytes_literal.flags), - ) { - checker.report_diagnostic(diagnostic); - } + ast::StringLikePart::String(string_literal) => check_string_or_bytes( + checker, + string_literal.range(), + AnyStringFlags::from(string_literal.flags), + ), + ast::StringLikePart::Bytes(bytes_literal) => check_string_or_bytes( + checker, + bytes_literal.range(), + AnyStringFlags::from(bytes_literal.flags), + ), + ast::StringLikePart::FString(ast::FString { + elements, + range, + node_index: _, + flags, + }) => { + check_interpolated_string(checker, AnyStringFlags::from(*flags), *range, elements); } - ast::StringLikePart::FString(f_string) => { - if let Some(diagnostic) = check_f_string(locator, f_string) { - checker.report_diagnostic(diagnostic); - } + ast::StringLikePart::TString(ast::TString { + elements, + range, + node_index: _, + flags, + }) => { + check_interpolated_string(checker, AnyStringFlags::from(*flags), *range, elements); } } } @@ -87,47 +89,46 @@ pub(crate) fn unnecessary_escaped_quote(checker: &Checker, string_like: StringLi /// # Panics /// /// If the string kind is an f-string. -fn check_string_or_bytes( - locator: &Locator, - range: TextRange, - flags: AnyStringFlags, -) -> Option { - assert!(!flags.is_f_string()); +fn check_string_or_bytes(checker: &Checker, range: TextRange, flags: AnyStringFlags) { + assert!(!flags.is_interpolated_string()); if flags.is_triple_quoted() || flags.is_raw_string() { - return None; + return; } - let contents = raw_contents(locator.slice(range), flags); + let contents = raw_contents(checker.locator().slice(range), flags); let quote = flags.quote_style(); let opposite_quote_char = quote.opposite().as_char(); if !contains_escaped_quote(contents, opposite_quote_char) { - return None; + return; } - let mut diagnostic = Diagnostic::new(UnnecessaryEscapedQuote, range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryEscapedQuote, range); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( flags .display_contents(&unescape_string(contents, opposite_quote_char)) .to_string(), range, ))); - Some(diagnostic) } -/// Checks for unnecessary escaped quotes in an f-string. -fn check_f_string(locator: &Locator, f_string: &ast::FString) -> Option { - let ast::FString { flags, range, .. } = f_string; +/// Checks for unnecessary escaped quotes in an f-string or t-string. +fn check_interpolated_string( + checker: &Checker, + flags: AnyStringFlags, + range: TextRange, + elements: &InterpolatedStringElements, +) { if flags.is_triple_quoted() || flags.prefix().is_raw() { - return None; + return; } let opposite_quote_char = flags.quote_style().opposite().as_char(); let mut edits = vec![]; - for literal in f_string.elements.literals() { - let content = locator.slice(literal); + for literal in elements.literals() { + let content = checker.locator().slice(literal); if !contains_escaped_quote(content, opposite_quote_char) { continue; } @@ -138,9 +139,10 @@ fn check_f_string(locator: &Locator, f_string: &ast::FString) -> Option Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_raise").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index 07390d0350561..e3a66cf35b123 100644 --- a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::BindingKind; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for unnecessary parentheses on raised exceptions. @@ -36,6 +36,10 @@ use crate::checkers::ast::Checker; /// raise TypeError /// ``` /// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if removing the parentheses would also remove comments +/// or if it’s unclear whether the expression is a class or a function call. +/// /// ## References /// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[derive(ViolationMetadata)] @@ -58,6 +62,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &Checker, expr: &Exp func, arguments, range: _, + node_index: _, }) = expr else { return; @@ -108,7 +113,8 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &Checker, expr: &Exp } } - let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, arguments.range()); + let mut diagnostic = + checker.report_diagnostic(UnnecessaryParenOnRaiseException, arguments.range()); // If the arguments are immediately followed by a `from`, insert whitespace to avoid // a syntax error, as in: @@ -131,17 +137,19 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &Checker, expr: &Exp }, )); } else { + let applicability = if exception_type.is_some() + && !checker.comment_ranges().intersects(arguments.range()) + { + Applicability::Safe + } else { + Applicability::Unsafe + }; + diagnostic.set_fix(Fix::applicable_edit( Edit::range_deletion(arguments.range()), - if exception_type.is_some() { - Applicability::Safe - } else { - Applicability::Unsafe - }, + applicability, )); } - - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_raise/snapshots/ruff_linter__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap b/crates/ruff_linter/src/rules/flake8_raise/snapshots/ruff_linter__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap index 80d7448df1fca..380796814716e 100644 --- a/crates/ruff_linter/src/rules/flake8_raise/snapshots/ruff_linter__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap +++ b/crates/ruff_linter/src/rules/flake8_raise/snapshots/ruff_linter__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap @@ -167,7 +167,7 @@ RSE102.py:37:16: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Safe fix +ℹ Unsafe fix 34 34 | ) 35 35 | 36 36 | # RSE102 @@ -296,3 +296,25 @@ RSE102.py:107:27: RSE102 [*] Unnecessary parentheses on raised exception 106 106 | if future.exception(): 107 |- raise future.Exception() 107 |+ raise future.Exception +108 108 | +109 109 | +110 110 | raise TypeError( + +RSE102.py:110:16: RSE102 [*] Unnecessary parentheses on raised exception + | +110 | raise TypeError( + | ________________^ +111 | | # comment +112 | | ) + | |_^ RSE102 + | + = help: Remove unnecessary parentheses + +ℹ Unsafe fix +107 107 | raise future.Exception() +108 108 | +109 109 | +110 |-raise TypeError( +111 |- # comment +112 |-) + 110 |+raise TypeError diff --git a/crates/ruff_linter/src/rules/flake8_return/mod.rs b/crates/ruff_linter/src/rules/flake8_return/mod.rs index 42eb65ff28043..871e3af3c2690 100644 --- a/crates/ruff_linter/src/rules/flake8_return/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_return/mod.rs @@ -11,11 +11,10 @@ mod tests { use anyhow::Result; use test_case::test_case; + use crate::assert_diagnostics; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; use crate::test::test_path; - use crate::{assert_messages, settings}; #[test_case(Rule::UnnecessaryReturnNone, Path::new("RET501.py"))] #[test_case(Rule::ImplicitReturnValue, Path::new("RET502.py"))] @@ -31,25 +30,7 @@ mod tests { Path::new("flake8_return").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } - - #[test_case(Rule::ImplicitReturn, Path::new("RET503.py"))] - fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "preview__{}_{}", - rule_code.noqa_code(), - path.to_string_lossy() - ); - let diagnostics = test_path( - Path::new("flake8_return").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs index a28597949e85e..8fd3ae0b44af9 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -1,33 +1,30 @@ -use std::ops::Add; - use anyhow::Result; -use ruff_diagnostics::{AlwaysFixableViolation, FixAvailability, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::{is_const_false, is_const_true}; use ruff_python_ast::stmt_if::elif_else_range; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{self as ast, Decorator, ElifElseClause, Expr, Stmt}; -use ruff_python_codegen::Stylist; -use ruff_python_index::Indexer; -use ruff_python_semantic::analyze::visibility::is_property; +use ruff_python_parser::TokenKind; use ruff_python_semantic::SemanticModel; -use ruff_python_trivia::{is_python_whitespace, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_semantic::analyze::visibility::is_property; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, is_python_whitespace}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::fix::edits; use crate::fix::edits::adjust_indentation; -use crate::registry::{AsRule, Rule}; +use crate::registry::Rule; use crate::rules::flake8_return::helpers::end_of_last_statement; -use crate::Locator; +use crate::{AlwaysFixableViolation, FixAvailability, Violation}; +use crate::{Edit, Fix}; -use super::super::branch::Branch; -use super::super::helpers::result_exists; -use super::super::visitor::{ReturnVisitor, Stack}; +use crate::rules::flake8_return::branch::Branch; +use crate::rules::flake8_return::helpers::result_exists; +use crate::rules::flake8_return::visitor::{ReturnVisitor, Stack}; /// ## What it does /// Checks for the presence of a `return None` statement when `None` is the only @@ -54,6 +51,10 @@ use super::super::visitor::{ReturnVisitor, Stack}; /// return /// return /// ``` +/// +/// ## Fix safety +/// This rule's fix is marked as unsafe for cases in which comments would be +/// dropped from the `return` statement. #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryReturnNone; @@ -378,18 +379,22 @@ fn unnecessary_return_none(checker: &Checker, decorator_list: &[Decorator], stac // Skip property functions if is_property( decorator_list, - checker.settings.pydocstyle.property_decorators(), + checker.settings().pydocstyle.property_decorators(), checker.semantic(), ) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryReturnNone, stmt.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - "return".to_string(), - stmt.range(), - ))); - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(UnnecessaryReturnNone, stmt.range()); + let edit = Edit::range_replacement("return".to_string(), stmt.range()); + diagnostic.set_fix(Fix::applicable_edit( + edit, + if checker.comment_ranges().intersects(stmt.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); } } @@ -399,12 +404,11 @@ fn implicit_return_value(checker: &Checker, stack: &Stack) { if stmt.value.is_some() { continue; } - let mut diagnostic = Diagnostic::new(ImplicitReturnValue, stmt.range()); + let mut diagnostic = checker.report_diagnostic(ImplicitReturnValue, stmt.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "return None".to_string(), stmt.range(), ))); - checker.report_diagnostic(diagnostic); } } @@ -454,7 +458,7 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { } fn add_return_none(checker: &Checker, stmt: &Stmt, range: TextRange) { - let mut diagnostic = Diagnostic::new(ImplicitReturn, range); + let mut diagnostic = checker.report_diagnostic(ImplicitReturn, range); if let Some(indent) = indentation(checker.source(), stmt) { let mut content = String::new(); content.push_str(checker.stylist().line_ending().as_str()); @@ -465,70 +469,55 @@ fn add_return_none(checker: &Checker, stmt: &Stmt, range: TextRange) { end_of_last_statement(stmt, checker.locator()), ))); } - checker.report_diagnostic(diagnostic); } -/// Returns a list of all implicit returns in the given statement. -/// -/// Note: The function should be refactored to `has_implicit_return` with an early return (when seeing the first implicit return) -/// when removing the preview gating. -fn implicit_returns<'a>(checker: &Checker, stmt: &'a Stmt) -> Vec<&'a Stmt> { +fn has_implicit_return(checker: &Checker, stmt: &Stmt) -> bool { match stmt { Stmt::If(ast::StmtIf { body, elif_else_clauses, .. }) => { - let mut implicit_stmts = body + if body .last() - .map(|last| implicit_returns(checker, last)) - .unwrap_or_default(); - - for clause in elif_else_clauses { - implicit_stmts.extend( - clause - .body - .last() - .iter() - .flat_map(|last| implicit_returns(checker, last)), - ); + .is_some_and(|last| has_implicit_return(checker, last)) + { + return true; + } + + if elif_else_clauses.iter().any(|clause| { + clause + .body + .last() + .is_some_and(|last| has_implicit_return(checker, last)) + }) { + return true; } // Check if we don't have an else clause - if matches!( + matches!( elif_else_clauses.last(), None | Some(ast::ElifElseClause { test: Some(_), .. }) - ) { - implicit_stmts.push(stmt); - } - implicit_stmts + ) } - Stmt::Assert(ast::StmtAssert { test, .. }) if is_const_false(test) => vec![], - Stmt::While(ast::StmtWhile { test, .. }) if is_const_true(test) => vec![], + Stmt::Assert(ast::StmtAssert { test, .. }) if is_const_false(test) => false, + Stmt::While(ast::StmtWhile { test, .. }) if is_const_true(test) => false, Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => { if let Some(last_stmt) = orelse.last() { - implicit_returns(checker, last_stmt) + has_implicit_return(checker, last_stmt) } else { - vec![stmt] - } - } - Stmt::Match(ast::StmtMatch { cases, .. }) => { - let mut implicit_stmts = vec![]; - for case in cases { - implicit_stmts.extend( - case.body - .last() - .into_iter() - .flat_map(|last_stmt| implicit_returns(checker, last_stmt)), - ); + true } - implicit_stmts } + Stmt::Match(ast::StmtMatch { cases, .. }) => cases.iter().any(|case| { + case.body + .last() + .is_some_and(|last| has_implicit_return(checker, last)) + }), Stmt::With(ast::StmtWith { body, .. }) => body .last() - .map(|last_stmt| implicit_returns(checker, last_stmt)) - .unwrap_or_default(), - Stmt::Return(_) | Stmt::Raise(_) | Stmt::Try(_) => vec![], + .is_some_and(|last_stmt| has_implicit_return(checker, last_stmt)), + Stmt::Return(_) | Stmt::Raise(_) | Stmt::Try(_) => false, Stmt::Expr(ast::StmtExpr { value, .. }) if matches!( value.as_ref(), @@ -536,33 +525,35 @@ fn implicit_returns<'a>(checker: &Checker, stmt: &'a Stmt) -> Vec<&'a Stmt> { if is_noreturn_func(func, checker.semantic()) ) => { - vec![] - } - _ => { - vec![stmt] + false } + _ => true, } } /// RET503 fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt: &Stmt) { - let implicit_stmts = implicit_returns(checker, stmt); - - if implicit_stmts.is_empty() { - return; - } - - if checker.settings.preview.is_enabled() { + if has_implicit_return(checker, stmt) { add_return_none(checker, stmt, function_def.range()); - } else { - for implicit_stmt in implicit_stmts { - add_return_none(checker, implicit_stmt, implicit_stmt.range()); - } } } /// RET504 -fn unnecessary_assign(checker: &Checker, stack: &Stack) { +pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) { + let Stmt::FunctionDef(function_def) = function_stmt else { + return; + }; + let Some(stack) = create_stack(checker, function_def) else { + return; + }; + + if !result_exists(&stack.returns) { + return; + } + + let Some(function_scope) = checker.semantic().function_scope(function_def) else { + return; + }; for (assign, return_, stmt) in &stack.assignment_return { // Identify, e.g., `return x`. let Some(value) = return_.value.as_ref() else { @@ -606,7 +597,23 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) { continue; } - let mut diagnostic = Diagnostic::new( + let Some(assigned_binding) = function_scope + .get(assigned_id) + .map(|binding_id| checker.semantic().binding(binding_id)) + else { + continue; + }; + // Check if there's any reference made to `assigned_binding` in another scope, e.g, nested + // functions. If there is, ignore them. + if assigned_binding + .references() + .map(|reference_id| checker.semantic().reference(reference_id)) + .any(|reference| reference.scope_id() != assigned_binding.scope) + { + continue; + } + + let mut diagnostic = checker.report_diagnostic( UnnecessaryAssign { name: assigned_id.to_string(), }, @@ -619,17 +626,17 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) { let delete_return = edits::delete_stmt(stmt, None, checker.locator(), checker.indexer()); - // Replace the `x = 1` statement with `return 1`. - let content = checker.locator().slice(assign); - let equals_index = content - .find('=') - .ok_or(anyhow::anyhow!("expected '=' in assignment statement"))?; - let after_equals = equals_index + 1; + let eq_token = checker + .tokens() + .before(assign.value.start()) + .iter() + .rfind(|token| token.kind() == TokenKind::Equal) + .unwrap(); + let content = checker.source(); + // Replace the `x = 1` statement with `return 1`. let replace_assign = Edit::range_replacement( - // If necessary, add whitespace after the `return` keyword. - // Ex) Convert `x=y` to `return y` (instead of `returny`). - if content[after_equals..] + if content[eq_token.end().to_usize()..] .chars() .next() .is_some_and(is_python_whitespace) @@ -640,18 +647,11 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) { }, // Replace from the start of the assignment statement to the end of the equals // sign. - TextRange::new( - assign.start(), - assign - .range() - .start() - .add(TextSize::try_from(after_equals)?), - ), + TextRange::new(assign.start(), eq_token.range().end()), ); Ok(Fix::unsafe_edits(replace_assign, [delete_return])) }); - checker.report_diagnostic(diagnostic); } } @@ -666,83 +666,24 @@ fn superfluous_else_node( } else { Branch::Else }; + let range = elif_else_range(elif_else, checker.locator().contents()) + .unwrap_or_else(|| elif_else.range()); for child in if_elif_body { - if child.is_return_stmt() { - let mut diagnostic = Diagnostic::new( - SuperfluousElseReturn { branch }, - elif_else_range(elif_else, checker.locator().contents()) - .unwrap_or_else(|| elif_else.range()), - ); - if checker.enabled(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_else( - elif_else, - checker.locator(), - checker.indexer(), - checker.stylist(), - ) - }); - checker.report_diagnostic(diagnostic); - } - return true; + let diagnostic = if child.is_return_stmt() { + checker.report_diagnostic_if_enabled(SuperfluousElseReturn { branch }, range) } else if child.is_break_stmt() { - let mut diagnostic = Diagnostic::new( - SuperfluousElseBreak { branch }, - elif_else_range(elif_else, checker.locator().contents()) - .unwrap_or_else(|| elif_else.range()), - ); - if checker.enabled(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_else( - elif_else, - checker.locator(), - checker.indexer(), - checker.stylist(), - ) - }); - - checker.report_diagnostic(diagnostic); - } - return true; + checker.report_diagnostic_if_enabled(SuperfluousElseBreak { branch }, range) } else if child.is_raise_stmt() { - let mut diagnostic = Diagnostic::new( - SuperfluousElseRaise { branch }, - elif_else_range(elif_else, checker.locator().contents()) - .unwrap_or_else(|| elif_else.range()), - ); - if checker.enabled(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_else( - elif_else, - checker.locator(), - checker.indexer(), - checker.stylist(), - ) - }); - - checker.report_diagnostic(diagnostic); - } - return true; + checker.report_diagnostic_if_enabled(SuperfluousElseRaise { branch }, range) } else if child.is_continue_stmt() { - let mut diagnostic = Diagnostic::new( - SuperfluousElseContinue { branch }, - elif_else_range(elif_else, checker.locator().contents()) - .unwrap_or_else(|| elif_else.range()), - ); - if checker.enabled(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_else( - elif_else, - checker.locator(), - checker.indexer(), - checker.stylist(), - ) - }); - - checker.report_diagnostic(diagnostic); - } - return true; + checker.report_diagnostic_if_enabled(SuperfluousElseContinue { branch }, range) + } else { + continue; + }; + if let Some(mut d) = diagnostic { + d.try_set_fix(|| remove_else(checker, elif_else)); } + return true; } false } @@ -754,24 +695,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) { } } -/// Run all checks from the `flake8-return` plugin. -pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { - let ast::StmtFunctionDef { - decorator_list, - returns, - body, - .. - } = function_def; +fn create_stack<'a>( + checker: &'a Checker, + function_def: &'a ast::StmtFunctionDef, +) -> Option> { + let ast::StmtFunctionDef { body, .. } = function_def; // Find the last statement in the function. let Some(last_stmt) = body.last() else { // Skip empty functions. - return; + return None; }; // Skip functions that consist of a single return statement. if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) { - return; + return None; } // Traverse the function body, to collect the stack. @@ -785,10 +723,30 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { // Avoid false positives for generators. if stack.is_generator { - return; + return None; } - if checker.any_enabled(&[ + Some(stack) +} + +/// Run all checks from the `flake8-return` plugin, but `RET504` which is ran +/// after the semantic model is fully built. +pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { + let ast::StmtFunctionDef { + decorator_list, + returns, + body, + .. + } = function_def; + + let Some(stack) = create_stack(checker, function_def) else { + return; + }; + let Some(last_stmt) = body.last() else { + return; + }; + + if checker.any_rule_enabled(&[ Rule::SuperfluousElseReturn, Rule::SuperfluousElseRaise, Rule::SuperfluousElseContinue, @@ -804,18 +762,14 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { // If we have at least one non-`None` return... if result_exists(&stack.returns) { - if checker.enabled(Rule::ImplicitReturnValue) { + if checker.is_rule_enabled(Rule::ImplicitReturnValue) { implicit_return_value(checker, &stack); } - if checker.enabled(Rule::ImplicitReturn) { + if checker.is_rule_enabled(Rule::ImplicitReturn) { implicit_return(checker, function_def, last_stmt); } - - if checker.enabled(Rule::UnnecessaryAssign) { - unnecessary_assign(checker, &stack); - } } else { - if checker.enabled(Rule::UnnecessaryReturnNone) { + if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) { // Skip functions that have a return annotation that is not `None`. if returns.as_deref().is_none_or(Expr::is_none_literal_expr) { unnecessary_return_none(checker, decorator_list, &stack); @@ -825,12 +779,11 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) { } /// Generate a [`Fix`] to remove an `else` or `elif` clause. -fn remove_else( - elif_else: &ElifElseClause, - locator: &Locator, - indexer: &Indexer, - stylist: &Stylist, -) -> Result { +fn remove_else(checker: &Checker, elif_else: &ElifElseClause) -> Result { + let locator = checker.locator(); + let indexer = checker.indexer(); + let stylist = checker.stylist(); + if elif_else.test.is_some() { // Ex) `elif` -> `if` Ok(Fix::safe_edit(Edit::deletion( diff --git a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET501_RET501.py.snap b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET501_RET501.py.snap index 0dcc8a8491d50..6afa398d94101 100644 --- a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET501_RET501.py.snap +++ b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET501_RET501.py.snap @@ -40,3 +40,23 @@ RET501.py:14:9: RET501 [*] Do not explicitly `return None` in function if it is 15 15 | 16 16 | @property 17 17 | def prop(self) -> None: + +RET501.py:59:9: RET501 [*] Do not explicitly `return None` in function if it is the only possible return value + | +57 | if not bar: +58 | return +59 | / return ( +60 | | None # comment +61 | | ) + | |_________^ RET501 + | + = help: Remove explicit `return None` + +ℹ Unsafe fix +56 56 | def foo(bar): +57 57 | if not bar: +58 58 | return +59 |- return ( +60 |- None # comment +61 |- ) + 59 |+ return diff --git a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET503_RET503.py.snap b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET503_RET503.py.snap index af2cbb50470d3..91c961e50974d 100644 --- a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET503_RET503.py.snap +++ b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET503_RET503.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff_linter/src/rules/flake8_return/mod.rs --- -RET503.py:21:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:20:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | 19 | # if/elif/else -20 | def x(y): -21 | / if not y: +20 | / def x(y): +21 | | if not y: 22 | | return 1 | |________________^ RET503 23 | # error @@ -21,32 +21,34 @@ RET503.py:21:5: RET503 [*] Missing explicit `return` at the end of function able 24 25 | 25 26 | -RET503.py:28:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:26:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -26 | def x(y): -27 | if not y: -28 | print() # error - | ^^^^^^^ RET503 -29 | else: -30 | return 2 +26 | / def x(y): +27 | | if not y: +28 | | print() # error +29 | | else: +30 | | return 2 + | |________________^ RET503 | = help: Add explicit `return` statement ℹ Unsafe fix -26 26 | def x(y): -27 27 | if not y: 28 28 | print() # error - 29 |+ return None -29 30 | else: -30 31 | return 2 +29 29 | else: +30 30 | return 2 + 31 |+ return None 31 32 | +32 33 | +33 34 | def x(y): -RET503.py:37:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:33:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -35 | return 1 -36 | -37 | print() # error - | ^^^^^^^ RET503 +33 | / def x(y): +34 | | if not y: +35 | | return 1 +36 | | +37 | | print() # error + | |___________^ RET503 | = help: Add explicit `return` statement @@ -59,11 +61,11 @@ RET503.py:37:5: RET503 [*] Missing explicit `return` at the end of function able 39 40 | 40 41 | # for -RET503.py:42:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:41:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | 40 | # for -41 | def x(y): -42 | / for i in range(10): +41 | / def x(y): +42 | | for i in range(10): 43 | | if i > 10: 44 | | return i | |____________________^ RET503 @@ -80,12 +82,15 @@ RET503.py:42:5: RET503 [*] Missing explicit `return` at the end of function able 46 47 | 47 48 | -RET503.py:53:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:48:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -51 | return i -52 | else: -53 | print() # error - | ^^^^^^^ RET503 +48 | / def x(y): +49 | | for i in range(10): +50 | | if i > 10: +51 | | return i +52 | | else: +53 | | print() # error + | |_______________^ RET503 | = help: Add explicit `return` statement @@ -93,17 +98,19 @@ RET503.py:53:9: RET503 [*] Missing explicit `return` at the end of function able 51 51 | return i 52 52 | else: 53 53 | print() # error - 54 |+ return None + 54 |+ return None 54 55 | 55 56 | 56 57 | # A nonexistent function -RET503.py:60:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:57:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -58 | if x > 0: -59 | return False -60 | no_such_function() # error - | ^^^^^^^^^^^^^^^^^^ RET503 +56 | # A nonexistent function +57 | / def func_unknown(x): +58 | | if x > 0: +59 | | return False +60 | | no_such_function() # error + | |______________________^ RET503 | = help: Add explicit `return` statement @@ -116,12 +123,14 @@ RET503.py:60:5: RET503 [*] Missing explicit `return` at the end of function able 62 63 | 63 64 | # A function that does return the control -RET503.py:67:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:64:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -65 | if x > 0: -66 | return False -67 | print("", end="") # error - | ^^^^^^^^^^^^^^^^^ RET503 +63 | # A function that does return the control +64 | / def func_no_noreturn(x): +65 | | if x > 0: +66 | | return False +67 | | print("", end="") # error + | |_____________________^ RET503 | = help: Add explicit `return` statement @@ -134,11 +143,11 @@ RET503.py:67:5: RET503 [*] Missing explicit `return` at the end of function able 69 70 | 70 71 | ### -RET503.py:83:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:82:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | 81 | # last line in while loop -82 | def x(y): -83 | / while i > 0: +82 | / def x(y): +83 | | while i > 0: 84 | | if y > 0: 85 | | return 1 86 | | y += 1 @@ -155,11 +164,11 @@ RET503.py:83:5: RET503 [*] Missing explicit `return` at the end of function able 88 89 | 89 90 | # exclude empty functions -RET503.py:114:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:113:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | 112 | # return value within loop -113 | def bar1(x, y, z): -114 | / for i in x: +113 | / def bar1(x, y, z): +114 | | for i in x: 115 | | if i > y: 116 | | break 117 | | return z @@ -176,10 +185,10 @@ RET503.py:114:5: RET503 [*] Missing explicit `return` at the end of function abl 119 120 | 120 121 | def bar3(x, y, z): -RET503.py:121:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:120:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -120 | def bar3(x, y, z): -121 | / for i in x: +120 | / def bar3(x, y, z): +121 | | for i in x: 122 | | if i > y: 123 | | if z: 124 | | break @@ -199,10 +208,10 @@ RET503.py:121:5: RET503 [*] Missing explicit `return` at the end of function abl 129 130 | 130 131 | def bar1(x, y, z): -RET503.py:131:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:130:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -130 | def bar1(x, y, z): -131 | / for i in x: +130 | / def bar1(x, y, z): +131 | | for i in x: 132 | | if i < y: 133 | | continue 134 | | return z @@ -219,10 +228,10 @@ RET503.py:131:5: RET503 [*] Missing explicit `return` at the end of function abl 136 137 | 137 138 | def bar3(x, y, z): -RET503.py:138:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:137:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -137 | def bar3(x, y, z): -138 | / for i in x: +137 | / def bar3(x, y, z): +138 | | for i in x: 139 | | if i < y: 140 | | if z: 141 | | continue @@ -242,11 +251,13 @@ RET503.py:138:5: RET503 [*] Missing explicit `return` at the end of function abl 146 147 | 147 148 | def prompts(self, foo): -RET503.py:275:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:271:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -273 | return False -274 | -275 | / for value in values: +271 | / def nested(values): +272 | | if not values: +273 | | return False +274 | | +275 | | for value in values: 276 | | print(value) | |____________________^ RET503 | @@ -261,12 +272,16 @@ RET503.py:275:5: RET503 [*] Missing explicit `return` at the end of function abl 278 279 | 279 280 | def while_true(): -RET503.py:292:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:287:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -290 | return 1 -291 | case 1: -292 | print() # error - | ^^^^^^^ RET503 +286 | # match +287 | / def x(y): +288 | | match y: +289 | | case 0: +290 | | return 1 +291 | | case 1: +292 | | print() # error + | |___________________^ RET503 | = help: Add explicit `return` statement @@ -274,16 +289,16 @@ RET503.py:292:13: RET503 [*] Missing explicit `return` at the end of function ab 290 290 | return 1 291 291 | case 1: 292 292 | print() # error - 293 |+ return None + 293 |+ return None 293 294 | 294 295 | 295 296 | def foo(baz: str) -> str: -RET503.py:301:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:300:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | 299 | def end_of_statement(): -300 | def example(): -301 | / if True: +300 | / def example(): +301 | | if True: 302 | | return "" | |_____________________^ RET503 | @@ -298,10 +313,10 @@ RET503.py:301:9: RET503 [*] Missing explicit `return` at the end of function abl 304 305 | 305 306 | def example(): -RET503.py:306:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:305:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -305 | def example(): -306 | / if True: +305 | / def example(): +306 | | if True: 307 | | return "" | |_____________________^ RET503 | @@ -316,10 +331,10 @@ RET503.py:306:9: RET503 [*] Missing explicit `return` at the end of function abl 309 310 | 310 311 | def example(): -RET503.py:311:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:310:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -310 | def example(): -311 | / if True: +310 | / def example(): +311 | | if True: 312 | | return "" # type: ignore | |_____________________^ RET503 | @@ -334,10 +349,10 @@ RET503.py:311:9: RET503 [*] Missing explicit `return` at the end of function abl 314 315 | 315 316 | def example(): -RET503.py:316:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:315:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -315 | def example(): -316 | / if True: +315 | / def example(): +316 | | if True: 317 | | return "" ; | |_____________________^ RET503 | @@ -352,10 +367,10 @@ RET503.py:316:9: RET503 [*] Missing explicit `return` at the end of function abl 319 320 | 320 321 | def example(): -RET503.py:321:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:320:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -320 | def example(): -321 | / if True: +320 | / def example(): +321 | | if True: 322 | | return "" \ | |_____________________^ RET503 323 | ; # type: ignore @@ -371,12 +386,13 @@ RET503.py:321:9: RET503 [*] Missing explicit `return` at the end of function abl 325 326 | 326 327 | def end_of_file(): -RET503.py:329:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:326:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -327 | if False: -328 | return 1 -329 | x = 2 \ - | ^^^^^ RET503 +326 | / def end_of_file(): +327 | | if False: +328 | | return 1 +329 | | x = 2 \ + | |_________^ RET503 | = help: Add explicit `return` statement @@ -389,12 +405,15 @@ RET503.py:329:5: RET503 [*] Missing explicit `return` at the end of function abl 332 333 | 333 334 | # function return type annotation NoReturn -RET503.py:403:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:398:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -401 | else: -402 | with c: -403 | d - | ^ RET503 +398 | / def f(): +399 | | if a: +400 | | return b +401 | | else: +402 | | with c: +403 | | d + | |_____________^ RET503 | = help: Add explicit `return` statement @@ -402,17 +421,22 @@ RET503.py:403:13: RET503 [*] Missing explicit `return` at the end of function ab 401 401 | else: 402 402 | with c: 403 403 | d - 404 |+ return None + 404 |+ return None 404 405 | 405 406 | 406 407 | -RET503.py:418:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value +RET503.py:413:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value | -416 | if x == 5: -417 | return 5 -418 | bar() - | ^^^^^ RET503 +411 | # the semantic model hasn't yet seen `bar`'s declaration. +412 | # Supporting nested functions requires making this a deferred rule. +413 | / def foo(x: int) -> int: +414 | | def bar() -> NoReturn: +415 | | abort() +416 | | if x == 5: +417 | | return 5 +418 | | bar() + | |_________^ RET503 | = help: Add explicit `return` statement diff --git a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap index 8cf3328508b5d..afad18563c2d9 100644 --- a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap +++ b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_return/mod.rs -snapshot_kind: text --- RET504.py:6:12: RET504 [*] Unnecessary assignment to `a` before `return` statement | @@ -258,3 +257,47 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return 422 |- services = a["services"] 423 |- return services 422 |+ return a["services"] +424 423 | +425 424 | +426 425 | # See: https://github.com/astral-sh/ruff/issues/14052 + +RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement + | +456 | (#= +457 | x) = 1 +458 | return x + | ^ RET504 +459 | +460 | def f(): + | + = help: Remove unnecessary assignment + +ℹ Unsafe fix +453 453 | +454 454 | # See: https://github.com/astral-sh/ruff/issues/18411 +455 455 | def f(): +456 |- (#= +457 |- x) = 1 +458 |- return x + 456 |+ return 1 +459 457 | +460 458 | def f(): +461 459 | x = (1 + +RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement + | +461 | x = (1 +462 | ) +463 | return x + | ^ RET504 + | + = help: Remove unnecessary assignment + +ℹ Unsafe fix +458 458 | return x +459 459 | +460 460 | def f(): +461 |- x = (1 + 461 |+ return (1 +462 462 | ) +463 |- return x diff --git a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__preview__RET503_RET503.py.snap b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__preview__RET503_RET503.py.snap deleted file mode 100644 index 91c961e50974d..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__preview__RET503_RET503.py.snap +++ /dev/null @@ -1,449 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_return/mod.rs ---- -RET503.py:20:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -19 | # if/elif/else -20 | / def x(y): -21 | | if not y: -22 | | return 1 - | |________________^ RET503 -23 | # error - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -20 20 | def x(y): -21 21 | if not y: -22 22 | return 1 - 23 |+ return None -23 24 | # error -24 25 | -25 26 | - -RET503.py:26:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -26 | / def x(y): -27 | | if not y: -28 | | print() # error -29 | | else: -30 | | return 2 - | |________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -28 28 | print() # error -29 29 | else: -30 30 | return 2 - 31 |+ return None -31 32 | -32 33 | -33 34 | def x(y): - -RET503.py:33:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -33 | / def x(y): -34 | | if not y: -35 | | return 1 -36 | | -37 | | print() # error - | |___________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -35 35 | return 1 -36 36 | -37 37 | print() # error - 38 |+ return None -38 39 | -39 40 | -40 41 | # for - -RET503.py:41:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -40 | # for -41 | / def x(y): -42 | | for i in range(10): -43 | | if i > 10: -44 | | return i - | |____________________^ RET503 -45 | # error - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -42 42 | for i in range(10): -43 43 | if i > 10: -44 44 | return i - 45 |+ return None -45 46 | # error -46 47 | -47 48 | - -RET503.py:48:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -48 | / def x(y): -49 | | for i in range(10): -50 | | if i > 10: -51 | | return i -52 | | else: -53 | | print() # error - | |_______________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -51 51 | return i -52 52 | else: -53 53 | print() # error - 54 |+ return None -54 55 | -55 56 | -56 57 | # A nonexistent function - -RET503.py:57:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -56 | # A nonexistent function -57 | / def func_unknown(x): -58 | | if x > 0: -59 | | return False -60 | | no_such_function() # error - | |______________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -58 58 | if x > 0: -59 59 | return False -60 60 | no_such_function() # error - 61 |+ return None -61 62 | -62 63 | -63 64 | # A function that does return the control - -RET503.py:64:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -63 | # A function that does return the control -64 | / def func_no_noreturn(x): -65 | | if x > 0: -66 | | return False -67 | | print("", end="") # error - | |_____________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -65 65 | if x > 0: -66 66 | return False -67 67 | print("", end="") # error - 68 |+ return None -68 69 | -69 70 | -70 71 | ### - -RET503.py:82:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -81 | # last line in while loop -82 | / def x(y): -83 | | while i > 0: -84 | | if y > 0: -85 | | return 1 -86 | | y += 1 - | |______________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -84 84 | if y > 0: -85 85 | return 1 -86 86 | y += 1 - 87 |+ return None -87 88 | -88 89 | -89 90 | # exclude empty functions - -RET503.py:113:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -112 | # return value within loop -113 | / def bar1(x, y, z): -114 | | for i in x: -115 | | if i > y: -116 | | break -117 | | return z - | |________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -115 115 | if i > y: -116 116 | break -117 117 | return z - 118 |+ return None -118 119 | -119 120 | -120 121 | def bar3(x, y, z): - -RET503.py:120:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -120 | / def bar3(x, y, z): -121 | | for i in x: -122 | | if i > y: -123 | | if z: -124 | | break -125 | | else: -126 | | return z -127 | | return None - | |___________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -125 125 | else: -126 126 | return z -127 127 | return None - 128 |+ return None -128 129 | -129 130 | -130 131 | def bar1(x, y, z): - -RET503.py:130:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -130 | / def bar1(x, y, z): -131 | | for i in x: -132 | | if i < y: -133 | | continue -134 | | return z - | |________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -132 132 | if i < y: -133 133 | continue -134 134 | return z - 135 |+ return None -135 136 | -136 137 | -137 138 | def bar3(x, y, z): - -RET503.py:137:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -137 | / def bar3(x, y, z): -138 | | for i in x: -139 | | if i < y: -140 | | if z: -141 | | continue -142 | | else: -143 | | return z -144 | | return None - | |___________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -142 142 | else: -143 143 | return z -144 144 | return None - 145 |+ return None -145 146 | -146 147 | -147 148 | def prompts(self, foo): - -RET503.py:271:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -271 | / def nested(values): -272 | | if not values: -273 | | return False -274 | | -275 | | for value in values: -276 | | print(value) - | |____________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -274 274 | -275 275 | for value in values: -276 276 | print(value) - 277 |+ return None -277 278 | -278 279 | -279 280 | def while_true(): - -RET503.py:287:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -286 | # match -287 | / def x(y): -288 | | match y: -289 | | case 0: -290 | | return 1 -291 | | case 1: -292 | | print() # error - | |___________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -290 290 | return 1 -291 291 | case 1: -292 292 | print() # error - 293 |+ return None -293 294 | -294 295 | -295 296 | def foo(baz: str) -> str: - -RET503.py:300:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -299 | def end_of_statement(): -300 | / def example(): -301 | | if True: -302 | | return "" - | |_____________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -300 300 | def example(): -301 301 | if True: -302 302 | return "" - 303 |+ return None -303 304 | -304 305 | -305 306 | def example(): - -RET503.py:305:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -305 | / def example(): -306 | | if True: -307 | | return "" - | |_____________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -305 305 | def example(): -306 306 | if True: -307 307 | return "" - 308 |+ return None -308 309 | -309 310 | -310 311 | def example(): - -RET503.py:310:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -310 | / def example(): -311 | | if True: -312 | | return "" # type: ignore - | |_____________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -310 310 | def example(): -311 311 | if True: -312 312 | return "" # type: ignore - 313 |+ return None -313 314 | -314 315 | -315 316 | def example(): - -RET503.py:315:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -315 | / def example(): -316 | | if True: -317 | | return "" ; - | |_____________________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -315 315 | def example(): -316 316 | if True: -317 317 | return "" ; - 318 |+ return None -318 319 | -319 320 | -320 321 | def example(): - -RET503.py:320:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -320 | / def example(): -321 | | if True: -322 | | return "" \ - | |_____________________^ RET503 -323 | ; # type: ignore - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -321 321 | if True: -322 322 | return "" \ -323 323 | ; # type: ignore - 324 |+ return None -324 325 | -325 326 | -326 327 | def end_of_file(): - -RET503.py:326:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -326 | / def end_of_file(): -327 | | if False: -328 | | return 1 -329 | | x = 2 \ - | |_________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -328 328 | return 1 -329 329 | x = 2 \ -330 330 | - 331 |+ return None -331 332 | -332 333 | -333 334 | # function return type annotation NoReturn - -RET503.py:398:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -398 | / def f(): -399 | | if a: -400 | | return b -401 | | else: -402 | | with c: -403 | | d - | |_____________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -401 401 | else: -402 402 | with c: -403 403 | d - 404 |+ return None -404 405 | -405 406 | -406 407 | - -RET503.py:413:1: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value - | -411 | # the semantic model hasn't yet seen `bar`'s declaration. -412 | # Supporting nested functions requires making this a deferred rule. -413 | / def foo(x: int) -> int: -414 | | def bar() -> NoReturn: -415 | | abort() -416 | | if x == 5: -417 | | return 5 -418 | | bar() - | |_________^ RET503 - | - = help: Add explicit `return` statement - -ℹ Unsafe fix -415 415 | abort() -416 416 | if x == 5: -417 417 | return 5 -418 |- bar() - 418 |+ bar() - 419 |+ return None diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index 38ddae2af4ede..e939209f53665 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -95,8 +95,16 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { // But don't recurse into the body. return; } - Stmt::Global(ast::StmtGlobal { names, range: _ }) - | Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { + Stmt::Global(ast::StmtGlobal { + names, + range: _, + node_index: _, + }) + | Stmt::Nonlocal(ast::StmtNonlocal { + names, + range: _, + node_index: _, + }) => { self.stack .non_locals .extend(names.iter().map(Identifier::as_str)); diff --git a/crates/ruff_linter/src/rules/flake8_self/mod.rs b/crates/ruff_linter/src/rules/flake8_self/mod.rs index a445b40bafaf1..8c0752fda1572 100644 --- a/crates/ruff_linter/src/rules/flake8_self/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_self/mod.rs @@ -4,13 +4,12 @@ pub mod settings; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use crate::registry::Rule; use crate::rules::flake8_self; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; use anyhow::Result; use ruff_python_ast::name::Name; use test_case::test_case; @@ -18,12 +17,12 @@ mod tests { #[test_case(Rule::PrivateMemberAccess, Path::new("SLF001.py"))] #[test_case(Rule::PrivateMemberAccess, Path::new("SLF001_1.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_self").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -38,7 +37,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::PrivateMemberAccess) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs index 60ca1c8bdf1cb..2326e24ea3591 100644 --- a/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::{is_dunder, is_sunder}; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, Expr}; @@ -8,6 +7,7 @@ use ruff_python_semantic::analyze::typing::TypeChecker; use ruff_python_semantic::{BindingKind, ScopeKind, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::is_dunder_operator_method; @@ -85,7 +85,7 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) { } if checker - .settings + .settings() .flake8_self .ignore_names .contains(attr.id()) @@ -144,12 +144,12 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) { } } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( PrivateMemberAccess { access: attr.to_string(), }, expr.range(), - )); + ); } /// Check for the following cases: diff --git a/crates/ruff_linter/src/rules/flake8_simplify/mod.rs b/crates/ruff_linter/src/rules/flake8_simplify/mod.rs index 6fb706df1709e..1fd42b3450245 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/mod.rs @@ -9,10 +9,10 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; + use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::DuplicateIsinstanceCall, Path::new("SIM101.py"))] #[test_case(Rule::CollapsibleIf, Path::new("SIM102.py"))] @@ -48,17 +48,18 @@ mod tests { #[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))] #[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))] #[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))] + #[test_case(Rule::ZipDictKeysAndValues, Path::new("SIM911.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_simplify").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } - #[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))] + #[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", @@ -72,7 +73,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs index bc63d55994120..7576c71fc11ba 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -6,10 +6,9 @@ use itertools::Itertools; use ruff_python_ast::{self as ast, Arguments, BoolOp, CmpOp, Expr, ExprContext, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::helpers::{contains_effect, Truthiness}; +use ruff_python_ast::helpers::{Truthiness, contains_effect}; use ruff_python_ast::name::Name; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_codegen::Generator; @@ -17,6 +16,7 @@ use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::fix::edits::pad; +use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for multiple `isinstance` calls on the same target. @@ -307,8 +307,10 @@ fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option< args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, } = call.as_call_expr()?; if args.len() != 2 { return None; @@ -330,6 +332,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { op: BoolOp::Or, values, range: _, + node_index: _, }) = expr else { return; @@ -374,7 +377,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { } else { unreachable!("Indices should only contain `isinstance` calls") }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DuplicateIsinstanceCall { name: if let Expr::Name(ast::ExprName { id, .. }) = target { Some(id.to_string()) @@ -418,6 +421,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { .collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }; let isinstance_call = ast::ExprCall { @@ -426,6 +430,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { id: Name::new_static("isinstance"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), @@ -433,8 +438,10 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { args: Box::from([target.clone(), tuple.into()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(); @@ -451,6 +458,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { .chain(after) .collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(); let fixed_source = checker.generator().expr(&bool_op); @@ -462,7 +470,6 @@ pub(crate) fn duplicate_isinstance_call(checker: &Checker, expr: &Expr) { expr.range(), ))); } - checker.report_diagnostic(diagnostic); } } } @@ -473,6 +480,7 @@ fn match_eq_target(expr: &Expr) -> Option<(&Name, &Expr)> { ops, comparators, range: _, + node_index: _, }) = expr else { return None; @@ -498,6 +506,7 @@ pub(crate) fn compare_with_tuple(checker: &Checker, expr: &Expr) { op: BoolOp::Or, values, range: _, + node_index: _, }) = expr else { return; @@ -543,21 +552,24 @@ pub(crate) fn compare_with_tuple(checker: &Checker, expr: &Expr) { elts: comparators.into_iter().cloned().collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }; let node1 = ast::ExprName { id: id.clone(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node2 = ast::ExprCompare { left: Box::new(node1.into()), ops: Box::from([CmpOp::In]), comparators: Box::from([node.into()]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let in_expr = node2.into(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CompareWithTuple { replacement: checker.generator().expr(&in_expr), }, @@ -577,6 +589,7 @@ pub(crate) fn compare_with_tuple(checker: &Checker, expr: &Expr) { op: BoolOp::Or, values: iter::once(in_expr).chain(unmatched).collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; node.into() }; @@ -584,7 +597,6 @@ pub(crate) fn compare_with_tuple(checker: &Checker, expr: &Expr) { checker.generator().expr(&in_expr), expr.range(), ))); - checker.report_diagnostic(diagnostic); } } @@ -594,6 +606,7 @@ pub(crate) fn expr_and_not_expr(checker: &Checker, expr: &Expr) { op: BoolOp::And, values, range: _, + node_index: _, }) = expr else { return; @@ -610,6 +623,7 @@ pub(crate) fn expr_and_not_expr(checker: &Checker, expr: &Expr) { op: UnaryOp::Not, operand, range: _, + node_index: _, }) = expr { negated_expr.push(operand); @@ -629,7 +643,7 @@ pub(crate) fn expr_and_not_expr(checker: &Checker, expr: &Expr) { for negate_expr in negated_expr { for non_negate_expr in &non_negated_expr { if let Some(id) = is_same_expr(negate_expr, non_negate_expr) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ExprAndNotExpr { name: id.to_string(), }, @@ -639,7 +653,6 @@ pub(crate) fn expr_and_not_expr(checker: &Checker, expr: &Expr) { "False".to_string(), expr.range(), ))); - checker.report_diagnostic(diagnostic); } } } @@ -651,6 +664,7 @@ pub(crate) fn expr_or_not_expr(checker: &Checker, expr: &Expr) { op: BoolOp::Or, values, range: _, + node_index: _, }) = expr else { return; @@ -667,6 +681,7 @@ pub(crate) fn expr_or_not_expr(checker: &Checker, expr: &Expr) { op: UnaryOp::Not, operand, range: _, + node_index: _, }) = expr { negated_expr.push(operand); @@ -686,7 +701,7 @@ pub(crate) fn expr_or_not_expr(checker: &Checker, expr: &Expr) { for negate_expr in negated_expr { for non_negate_expr in &non_negated_expr { if let Some(id) = is_same_expr(negate_expr, non_negate_expr) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ExprOrNotExpr { name: id.to_string(), }, @@ -696,7 +711,6 @@ pub(crate) fn expr_or_not_expr(checker: &Checker, expr: &Expr) { "True".to_string(), expr.range(), ))); - checker.report_diagnostic(diagnostic); } } } @@ -737,6 +751,7 @@ fn is_short_circuit( op, values, range: _, + node_index: _, }) = expr else { return None; @@ -838,7 +853,7 @@ pub(crate) fn expr_or_true(checker: &Checker, expr: &Expr) { } if let Some((edit, remove)) = is_short_circuit(expr, BoolOp::Or, checker) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ExprOrTrue { expr: edit.content().unwrap_or_default().to_string(), remove, @@ -846,7 +861,6 @@ pub(crate) fn expr_or_true(checker: &Checker, expr: &Expr) { edit.range(), ); diagnostic.set_fix(Fix::unsafe_edit(edit)); - checker.report_diagnostic(diagnostic); } } @@ -857,7 +871,7 @@ pub(crate) fn expr_and_false(checker: &Checker, expr: &Expr) { } if let Some((edit, remove)) = is_short_circuit(expr, BoolOp::And, checker) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ExprAndFalse { expr: edit.content().unwrap_or_default().to_string(), remove, @@ -865,6 +879,5 @@ pub(crate) fn expr_and_false(checker: &Checker, expr: &Expr) { edit.range(), ); diagnostic.set_fix(Fix::unsafe_edit(edit)); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs index 5131b6ad0fd55..afc2e88b04169 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs @@ -1,13 +1,13 @@ -use ruff_python_ast::{self as ast, str_prefix::StringLiteralPrefix, Arguments, Expr}; +use ruff_python_ast::{self as ast, Arguments, Expr, str_prefix::StringLiteralPrefix}; use ruff_text_size::{Ranged, TextRange}; -use crate::fix::snippet::SourceCodeSnippet; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_semantic::analyze::typing::is_dict; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; +use ruff_python_semantic::analyze::typing::is_dict; use crate::checkers::ast::Checker; +use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Check for environment variables that are not capitalized. @@ -32,6 +32,12 @@ use crate::checkers::ast::Checker; /// os.environ["FOO"] /// ``` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because automatically capitalizing environment variable names +/// can change program behavior in environments where the variable names are case-sensitive, such as most +/// Unix-like systems. +/// /// ## References /// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ) #[derive(ViolationMetadata)] @@ -169,13 +175,13 @@ pub(crate) fn use_capital_environment_variables(checker: &Checker, expr: &Expr) return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UncapitalizedEnvironmentVariables { expected: SourceCodeSnippet::new(capital_env_var), actual: SourceCodeSnippet::new(env_var.to_string()), }, arg.range(), - )); + ); } fn check_os_environ_subscript(checker: &Checker, expr: &Expr) { @@ -209,7 +215,7 @@ fn check_os_environ_subscript(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UncapitalizedEnvironmentVariables { expected: SourceCodeSnippet::new(capital_env_var.clone()), actual: SourceCodeSnippet::new(env_var.to_string()), @@ -226,13 +232,13 @@ fn check_os_environ_subscript(checker: &Checker, expr: &Expr) { } }), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let new_env_var = node.into(); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&new_env_var), slice.range(), ))); - checker.report_diagnostic(diagnostic); } /// SIM910 @@ -241,6 +247,7 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) { func, arguments: Arguments { args, keywords, .. }, range: _, + node_index: _, }) = expr else { return; @@ -273,7 +280,7 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) { Expr::Name(name) => { let Some(binding) = checker .semantic() - .only_binding(name) + .resolve_name(name) .map(|id| checker.semantic().binding(id)) else { return; @@ -292,7 +299,7 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) { ); let actual = checker.locator().slice(expr); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DictGetWithNoneDefault { expected: SourceCodeSnippet::new(expected.clone()), actual: SourceCodeSnippet::from_str(actual), @@ -303,5 +310,4 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) { expected, expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs index 9f054ef51e004..7ba5d6410ac72 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -1,13 +1,13 @@ use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::{is_const_false, is_const_true}; use ruff_python_ast::name::Name; use ruff_python_ast::parenthesize::parenthesized_range; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `if` expressions that can be replaced with `bool()` calls. @@ -27,6 +27,13 @@ use crate::checkers::ast::Checker; /// bool(a) /// ``` /// +/// ## Fix safety +/// +/// This fix is marked as unsafe because it may change the program’s behavior if the condition does not +/// return a proper Boolean. While the fix will try to wrap non-boolean values in a call to bool, +/// custom implementations of comparison functions like `__eq__` can avoid the bool call and still +/// lead to altered behavior. Moreover, the fix may remove comments. +/// /// ## References /// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[derive(ViolationMetadata)] @@ -150,7 +157,7 @@ pub(crate) fn if_expr_with_true_false( return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfExprWithTrueFalse { is_compare: test.is_compare_expr(), }, @@ -181,6 +188,7 @@ pub(crate) fn if_expr_with_true_false( id: Name::new_static("bool"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), @@ -188,15 +196,16 @@ pub(crate) fn if_expr_with_true_false( args: Box::from([test.clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), expr.range(), ))); } - checker.report_diagnostic(diagnostic); } /// SIM211 @@ -211,19 +220,19 @@ pub(crate) fn if_expr_with_false_true( return; } - let mut diagnostic = Diagnostic::new(IfExprWithFalseTrue, expr.range()); + let mut diagnostic = checker.report_diagnostic(IfExprWithFalseTrue, expr.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr( &ast::ExprUnaryOp { op: UnaryOp::Not, operand: Box::new(test.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), expr.range(), ))); - checker.report_diagnostic(diagnostic); } /// SIM212 @@ -238,6 +247,7 @@ pub(crate) fn twisted_arms_in_ifexpr( op, operand, range: _, + node_index: _, }) = &test else { return; @@ -257,7 +267,7 @@ pub(crate) fn twisted_arms_in_ifexpr( return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfExprWithTwistedArms { expr_body: checker.generator().expr(body), expr_else: checker.generator().expr(orelse), @@ -272,10 +282,10 @@ pub(crate) fn twisted_arms_in_ifexpr( body: Box::new(node1), orelse: Box::new(node), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&node3.into()), expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs index 45e163fb285ca..37f3c16a8e8de 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, Arguments, CmpOp, Expr, ExprContext, Stmt, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for negated `==` operators. @@ -153,6 +153,7 @@ pub(crate) fn negation_with_equal_op(checker: &Checker, expr: &Expr, op: UnaryOp ops, comparators, range: _, + node_index: _, }) = operand else { return; @@ -173,7 +174,7 @@ pub(crate) fn negation_with_equal_op(checker: &Checker, expr: &Expr, op: UnaryOp } } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NegateEqualOp { left: checker.generator().expr(left), right: checker.generator().expr(&comparators[0]), @@ -185,12 +186,12 @@ pub(crate) fn negation_with_equal_op(checker: &Checker, expr: &Expr, op: UnaryOp ops: Box::from([CmpOp::NotEq]), comparators: comparators.clone(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&node.into()), expr.range(), ))); - checker.report_diagnostic(diagnostic); } /// SIM202 @@ -208,6 +209,7 @@ pub(crate) fn negation_with_not_equal_op( ops, comparators, range: _, + node_index: _, }) = operand else { return; @@ -228,7 +230,7 @@ pub(crate) fn negation_with_not_equal_op( } } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NegateNotEqualOp { left: checker.generator().expr(left), right: checker.generator().expr(&comparators[0]), @@ -240,12 +242,12 @@ pub(crate) fn negation_with_not_equal_op( ops: Box::from([CmpOp::Eq]), comparators: comparators.clone(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().expr(&node.into()), expr.range(), ))); - checker.report_diagnostic(diagnostic); } /// SIM208 @@ -257,6 +259,7 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera op: operand_op, operand, range: _, + node_index: _, }) = operand else { return; @@ -265,7 +268,7 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DoubleNegation { expr: checker.generator().expr(operand), }, @@ -281,6 +284,7 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera id: Name::new_static("bool"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node1 = ast::ExprCall { func: Box::new(node.into()), @@ -288,13 +292,14 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera args: Box::from([*operand.clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs index b86626984eab7..248a7250e7d93 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs @@ -1,17 +1,17 @@ use anyhow::bail; use ast::Expr; -use ruff_diagnostics::{Diagnostic, Fix}; -use ruff_diagnostics::{FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Stmt, WithItem}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange}; +use super::fix_with; +use crate::Fix; use crate::checkers::ast::Checker; use crate::fix::edits::fits; - -use super::fix_with; +use crate::preview::is_multiple_with_statements_fix_safe_enabled; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for the unnecessary nesting of multiple consecutive context @@ -45,8 +45,15 @@ use super::fix_with; /// pass /// ``` /// +/// ## Fix safety +/// +/// This fix is marked as always unsafe unless [preview] mode is enabled, in which case it is always +/// marked as safe. Note that the fix is unavailable if it would remove comments (in either case). +/// /// ## References /// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] pub(crate) struct MultipleWithStatements; @@ -68,12 +75,14 @@ impl Violation for MultipleWithStatements { /// Returns a boolean indicating whether it's an async with statement, the items /// and body. fn next_with(body: &[Stmt]) -> Option<(bool, &[WithItem], &[Stmt])> { - let [Stmt::With(ast::StmtWith { - is_async, - items, - body, - .. - })] = body + let [ + Stmt::With(ast::StmtWith { + is_async, + items, + body, + .. + }), + ] = body else { return None; }; @@ -162,7 +171,7 @@ pub(crate) fn multiple_with_statements( return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MultipleWithStatements, TextRange::new(with_stmt.start(), colon.end()), ); @@ -182,11 +191,15 @@ pub(crate) fn multiple_with_statements( content, with_stmt.into(), checker.locator(), - checker.settings.pycodestyle.max_line_length, - checker.settings.tab_size, + checker.settings().pycodestyle.max_line_length, + checker.settings().tab_size, ) }) { - Ok(Some(Fix::unsafe_edit(edit))) + if is_multiple_with_statements_fix_safe_enabled(checker.settings()) { + Ok(Some(Fix::safe_edit(edit))) + } else { + Ok(Some(Fix::unsafe_edit(edit))) + } } else { Ok(None) } @@ -195,6 +208,5 @@ pub(crate) fn multiple_with_statements( } }); } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs index 43f8facc3c549..da4721b875923 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs @@ -1,24 +1,24 @@ use std::borrow::Cow; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use libcst_native::ParenthesizedNode; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::AnyNodeRef; -use ruff_python_ast::{self as ast, whitespace, ElifElseClause, Expr, Stmt}; +use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt, whitespace}; use ruff_python_codegen::Stylist; use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::cst::helpers::space; use crate::cst::matchers::{match_function_def, match_if, match_indented_block, match_statement}; use crate::fix::codemods::CodegenStylist; use crate::fix::edits::fits; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for nested `if` statements that can be collapsed into a single `if` @@ -107,7 +107,7 @@ pub(crate) fn nested_if_statements( return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CollapsibleIf, TextRange::new(nested_if.start(), colon.end()), ); @@ -125,8 +125,8 @@ pub(crate) fn nested_if_statements( content, (&nested_if).into(), checker.locator(), - checker.settings.pycodestyle.max_line_length, - checker.settings.tab_size, + checker.settings().pycodestyle.max_line_length, + checker.settings().tab_size, ) }) { Ok(Some(Fix::unsafe_edit(edit))) @@ -138,7 +138,6 @@ pub(crate) fn nested_if_statements( } }); } - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy)] @@ -230,12 +229,14 @@ fn nested_if_body(stmt_if: &ast::StmtIf) -> Option { /// ... /// ``` fn find_last_nested_if(body: &[Stmt]) -> Option<&Expr> { - let [Stmt::If(ast::StmtIf { - test, - body: inner_body, - elif_else_clauses, - .. - })] = body + let [ + Stmt::If(ast::StmtIf { + test, + body: inner_body, + elif_else_clauses, + .. + }), + ] = body else { return None; }; @@ -343,7 +344,7 @@ pub(super) fn collapse_nested_if( let outer_if = match_if(statement)?; let libcst_native::If { - body: libcst_native::Suite::IndentedBlock(ref mut outer_body), + body: libcst_native::Suite::IndentedBlock(outer_body), orelse: None, .. } = outer_if @@ -351,9 +352,11 @@ pub(super) fn collapse_nested_if( bail!("Expected outer if to have indented body and no else") }; - let [libcst_native::Statement::Compound(libcst_native::CompoundStatement::If( - inner_if @ libcst_native::If { orelse: None, .. }, - ))] = &mut *outer_body.body + let [ + libcst_native::Statement::Compound(libcst_native::CompoundStatement::If( + inner_if @ libcst_native::If { orelse: None, .. }, + )), + ] = &mut *outer_body.body else { bail!("Expected one inner if statement"); }; diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs index b056845bfe9f3..4f4d0c39687a8 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/enumerate_for_loop.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast, Expr, Int, Number, Operator, Stmt}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -20,6 +20,7 @@ use crate::checkers::ast::Checker; /// ## Example /// ```python /// fruits = ["apple", "banana", "cherry"] +/// i = 0 /// for fruit in fruits: /// print(f"{i + 1}. {fruit}") /// i += 1 @@ -139,13 +140,12 @@ pub(crate) fn enumerate_for_loop(checker: &Checker, for_stmt: &ast::StmtFor) { continue; } - let diagnostic = Diagnostic::new( + checker.report_diagnostic( EnumerateForLoop { index: index.id.to_string(), }, stmt.range(), ); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/fix_with.rs index 008e9f1f2df1f..e518753f40deb 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/fix_with.rs @@ -1,16 +1,16 @@ -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use libcst_native::{CompoundStatement, Statement, Suite, With}; -use ruff_diagnostics::Edit; use ruff_python_ast as ast; use ruff_python_ast::whitespace; use ruff_python_codegen::Stylist; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; +use crate::Edit; +use crate::Locator; use crate::cst::matchers::{match_function_def, match_indented_block, match_statement, match_with}; use crate::fix::codemods::CodegenStylist; -use crate::Locator; /// (SIM117) Convert `with a: with b:` to `with a, b:`. pub(crate) fn fix_multiple_with_statements( @@ -55,7 +55,7 @@ pub(crate) fn fix_multiple_with_statements( let outer_with = match_with(statement)?; let With { - body: Suite::IndentedBlock(ref mut outer_body), + body: Suite::IndentedBlock(outer_body), .. } = outer_with else { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs index d7a43a37ba9cf..505bfe58f75c8 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::{ @@ -12,6 +11,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::fits; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `if` statements that can be replaced with `dict.get` calls. @@ -27,6 +27,7 @@ use crate::fix::edits::fits; /// /// ## Example /// ```python +/// foo = {} /// if "bar" in foo: /// value = foo["bar"] /// else: @@ -35,6 +36,7 @@ use crate::fix::edits::fits; /// /// Use instead: /// ```python +/// foo = {} /// value = foo.get("bar", 0) /// ``` /// @@ -82,11 +84,13 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast let [body_stmt] = body.as_slice() else { return; }; - let [ElifElseClause { - body: else_body, - test: None, - .. - }] = elif_else_clauses.as_slice() + let [ + ElifElseClause { + body: else_body, + test: None, + .. + }, + ] = elif_else_clauses.as_slice() else { return; }; @@ -121,6 +125,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast ops, comparators: test_dict, range: _, + node_index: _, }) = &**test else { return; @@ -185,6 +190,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast attr: Identifier::new("get".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node3 = ast::ExprCall { func: Box::new(node2.into()), @@ -192,14 +198,17 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast args: Box::from([node1, node]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node4 = expected_var.clone(); let node5 = ast::StmtAssign { targets: vec![node4], value: Box::new(node3.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let contents = checker.generator().stmt(&node5.into()); @@ -208,13 +217,13 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast &contents, stmt_if.into(), checker.locator(), - checker.settings.pycodestyle.max_line_length, - checker.settings.tab_size, + checker.settings().pycodestyle.max_line_length, + checker.settings().tab_size, ) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfElseBlockInsteadOfDictGet { contents: contents.clone(), }, @@ -229,7 +238,6 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast stmt_if.range(), ))); } - checker.report_diagnostic(diagnostic); } /// SIM401 @@ -245,6 +253,7 @@ pub(crate) fn if_exp_instead_of_dict_get( ops, comparators: test_dict, range: _, + node_index: _, }) = test else { return; @@ -290,6 +299,7 @@ pub(crate) fn if_exp_instead_of_dict_get( attr: Identifier::new("get".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let fixed_node = ast::ExprCall { func: Box::new(dict_get_node.into()), @@ -297,13 +307,15 @@ pub(crate) fn if_exp_instead_of_dict_get( args: Box::from([dict_key_node, default_value_node]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let contents = checker.generator().expr(&fixed_node.into()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfElseBlockInsteadOfDictGet { contents: contents.clone(), }, @@ -318,5 +330,4 @@ pub(crate) fn if_exp_instead_of_dict_get( expr.range(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs index 144bb43d4c8c6..e3e7a41b63136 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs @@ -1,13 +1,13 @@ use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableLiteral; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::{self as ast, CmpOp, ElifElseClause, Expr, Stmt}; use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -18,17 +18,22 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python -/// if x == 1: -/// return "Hello" -/// elif x == 2: -/// return "Goodbye" -/// else: -/// return "Goodnight" +/// def find_phrase(x): +/// if x == 1: +/// return "Hello" +/// elif x == 2: +/// return "Goodbye" +/// elif x == 3: +/// return "Good morning" +/// else: +/// return "Goodnight" /// ``` /// /// Use instead: /// ```python -/// return {1: "Hello", 2: "Goodbye"}.get(x, "Goodnight") +/// def find_phrase(x): +/// phrases = {1: "Hello", 2: "Goodye", 3: "Good morning"} +/// return phrases.get(x, "Goodnight") /// ``` #[derive(ViolationMetadata)] pub(crate) struct IfElseBlockInsteadOfDictLookup; @@ -57,6 +62,7 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &Checker, stmt_if: & ops, comparators, range: _, + node_index: _, }) = test.as_ref() else { return; @@ -73,7 +79,14 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &Checker, stmt_if: & let Some(literal_expr) = expr.as_literal_expr() else { return; }; - let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else { + let [ + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }), + ] = body.as_slice() + else { return; }; @@ -99,7 +112,14 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &Checker, stmt_if: & for clause in elif_else_clauses { let ElifElseClause { test, body, .. } = clause; - let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else { + let [ + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }), + ] = body.as_slice() + else { return; }; @@ -107,7 +127,14 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &Checker, stmt_if: & // `else` None => { // The else must also be a single effect-free return statement - let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else { + let [ + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }), + ] = body.as_slice() + else { return; }; if value.as_ref().is_some_and(|value| { @@ -122,6 +149,7 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &Checker, stmt_if: & ops, comparators, range: _, + node_index: _, })) => { let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { return; @@ -156,8 +184,5 @@ pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &Checker, stmt_if: & return; } - checker.report_diagnostic(Diagnostic::new( - IfElseBlockInsteadOfDictLookup, - stmt_if.range(), - )); + checker.report_diagnostic(IfElseBlockInsteadOfDictLookup, stmt_if.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs index d96d11df9da8d..97cbcb76ac3c0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::{self as ast, BoolOp, ElifElseClause, Expr, Stmt}; @@ -8,11 +7,14 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::fits; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does -/// Check for `if`-`else`-blocks that can be replaced with a ternary operator. -/// Moreover, in [preview], check if these ternary expressions can be -/// further simplified to binary expressions. +/// Check for `if`-`else`-blocks that can be replaced with a ternary +/// or binary operator. +/// +/// The lint is suppressed if the suggested replacement would exceed +/// the maximum line length configured in [pycodestyle.max-line-length]. /// /// ## Why is this bad? /// `if`-`else`-blocks that assign a value to a variable in both branches can @@ -32,7 +34,7 @@ use crate::fix::edits::fits; /// bar = x if foo else y /// ``` /// -/// Or, in [preview]: +/// Or: /// /// ```python /// if cond: @@ -56,8 +58,8 @@ use crate::fix::edits::fits; /// ## References /// - [Python documentation: Conditional expressions](https://docs.python.org/3/reference/expressions.html#conditional-expressions) /// -/// [preview]: https://docs.astral.sh/ruff/preview/ /// [code coverage]: https://github.com/nedbat/coveragepy/issues/509 +/// [pycodestyle.max-line-length]: https://docs.astral.sh/ruff/settings/#lint_pycodestyle_max-line-length #[derive(ViolationMetadata)] pub(crate) struct IfElseBlockInsteadOfIfExp { /// The ternary or binary expression to replace the `if`-`else`-block. @@ -95,30 +97,37 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast:: body, elif_else_clauses, range: _, + node_index: _, } = stmt_if; // `test: None` to only match an `else` clause - let [ElifElseClause { - body: else_body, - test: None, - .. - }] = elif_else_clauses.as_slice() + let [ + ElifElseClause { + body: else_body, + test: None, + .. + }, + ] = elif_else_clauses.as_slice() else { return; }; - let [Stmt::Assign(ast::StmtAssign { - targets: body_targets, - value: body_value, - .. - })] = body.as_slice() + let [ + Stmt::Assign(ast::StmtAssign { + targets: body_targets, + value: body_value, + .. + }), + ] = body.as_slice() else { return; }; - let [Stmt::Assign(ast::StmtAssign { - targets: else_targets, - value: else_value, - .. - })] = else_body.as_slice() + let [ + Stmt::Assign(ast::StmtAssign { + targets: else_targets, + value: else_value, + .. + }), + ] = else_body.as_slice() else { return; }; @@ -176,56 +185,53 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast:: // // The match statement below implements the following // logic: - // - If `test == body_value` and preview enabled, replace with `target_var = test or else_value` - // - If `test == not body_value` and preview enabled, replace with `target_var = body_value and else_value` - // - If `not test == body_value` and preview enabled, replace with `target_var = body_value and else_value` + // - If `test == body_value`, replace with `target_var = test or else_value` + // - If `test == not body_value`, replace with `target_var = body_value and else_value` + // - If `not test == body_value`, replace with `target_var = body_value and else_value` // - Otherwise, replace with `target_var = body_value if test else else_value` - let (contents, assignment_kind) = - match (checker.settings.preview.is_enabled(), test, body_value) { - (true, test_node, body_node) - if ComparableExpr::from(test_node) == ComparableExpr::from(body_node) - && !contains_effect(test_node, |id| { - checker.semantic().has_builtin_binding(id) - }) => - { - let target_var = &body_target; - let binary = assignment_binary_or(target_var, body_value, else_value); - (checker.generator().stmt(&binary), AssignmentKind::Binary) - } - (true, test_node, body_node) - if (test_node.as_unary_op_expr().is_some_and(|op_expr| { - op_expr.op.is_not() - && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node) - }) || body_node.as_unary_op_expr().is_some_and(|op_expr| { - op_expr.op.is_not() - && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(test_node) - })) && !contains_effect(test_node, |id| { - checker.semantic().has_builtin_binding(id) - }) => - { - let target_var = &body_target; - let binary = assignment_binary_and(target_var, body_value, else_value); - (checker.generator().stmt(&binary), AssignmentKind::Binary) - } - _ => { - let target_var = &body_target; - let ternary = assignment_ternary(target_var, body_value, test, else_value); - (checker.generator().stmt(&ternary), AssignmentKind::Ternary) - } - }; + let (contents, assignment_kind) = match (test, body_value) { + (test_node, body_node) + if ComparableExpr::from(test_node) == ComparableExpr::from(body_node) + && !contains_effect(test_node, |id| checker.semantic().has_builtin_binding(id)) => + { + let target_var = &body_target; + let binary = assignment_binary_or(target_var, body_value, else_value); + (checker.generator().stmt(&binary), AssignmentKind::Binary) + } + (test_node, body_node) + if (test_node.as_unary_op_expr().is_some_and(|op_expr| { + op_expr.op.is_not() + && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node) + }) || body_node.as_unary_op_expr().is_some_and(|op_expr| { + op_expr.op.is_not() + && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(test_node) + })) && !contains_effect(test_node, |id| { + checker.semantic().has_builtin_binding(id) + }) => + { + let target_var = &body_target; + let binary = assignment_binary_and(target_var, body_value, else_value); + (checker.generator().stmt(&binary), AssignmentKind::Binary) + } + _ => { + let target_var = &body_target; + let ternary = assignment_ternary(target_var, body_value, test, else_value); + (checker.generator().stmt(&ternary), AssignmentKind::Ternary) + } + }; // Don't flag if the resulting expression would exceed the maximum line length. if !fits( &contents, stmt_if.into(), checker.locator(), - checker.settings.pycodestyle.max_line_length, - checker.settings.tab_size, + checker.settings().pycodestyle.max_line_length, + checker.settings().tab_size, ) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfElseBlockInsteadOfIfExp { contents: contents.clone(), kind: assignment_kind, @@ -241,7 +247,6 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast:: stmt_if.range(), ))); } - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -261,11 +266,13 @@ fn assignment_ternary( body: Box::new(body_value.clone()), orelse: Box::new(orelse_value.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node1 = ast::StmtAssign { targets: vec![target_var.clone()], value: Box::new(node.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; node1.into() } @@ -275,11 +282,13 @@ fn assignment_binary_and(target_var: &Expr, left_value: &Expr, right_value: &Exp op: BoolOp::And, values: vec![left_value.clone(), right_value.clone()], range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let node1 = ast::StmtAssign { targets: vec![target_var.clone()], value: Box::new(node.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; node1.into() } @@ -287,10 +296,12 @@ fn assignment_binary_and(target_var: &Expr, left_value: &Expr, right_value: &Exp fn assignment_binary_or(target_var: &Expr, left_value: &Expr, right_value: &Expr) -> Stmt { (ast::StmtAssign { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), targets: vec![target_var.clone()], value: Box::new( (ast::ExprBoolOp { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), op: BoolOp::Or, values: vec![left_value.clone(), right_value.clone()], }) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs index df402fc2cd74d..404c78e260475 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_with_same_arms.rs @@ -2,18 +2,18 @@ use std::borrow::Cow; use anyhow::Result; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableStmt; use ruff_python_ast::parenthesize::parenthesized_range; -use ruff_python_ast::stmt_if::{if_elif_branches, IfElifBranch}; +use ruff_python_ast::stmt_if::{IfElifBranch, if_elif_branches}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `if` branches with identical arm bodies. @@ -87,7 +87,7 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) { continue; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfWithSameArms, TextRange::new(current_branch.start(), following_branch.end()), ); @@ -101,8 +101,6 @@ pub(crate) fn if_with_same_arms(checker: &Checker, stmt_if: &ast::StmtIf) { checker.comment_ranges(), ) }); - - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs index e1e1e069b3a9e..da647235e19de 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_diagnostics::{Applicability, Edit}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::parenthesize::parenthesized_range; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::AnyNodeRef; +use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr}; use ruff_python_semantic::analyze::typing; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Fix}; +use crate::{Applicability, Edit}; /// ## What it does /// Checks for key-existence checks against `dict.keys()` calls. @@ -60,6 +60,7 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa func, arguments: Arguments { args, keywords, .. }, range: _, + node_index: _, }) = &right else { return; @@ -103,7 +104,7 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa ) .unwrap_or(right.range()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( InDictKeys { operator: operator.as_str().to_string(), }, @@ -158,7 +159,6 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa )); } } - checker.report_diagnostic(diagnostic); } /// SIM118 in a `for` loop. diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs index 9b73053450e2c..679797a258301 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::traversal; use ruff_python_ast::{self as ast, Arguments, ElifElseClause, Expr, ExprContext, Stmt}; @@ -8,6 +7,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `if` statements that can be replaced with `bool`. @@ -19,29 +19,40 @@ use crate::fix::snippet::SourceCodeSnippet; /// ## Example /// Given: /// ```python -/// if x > 0: -/// return True -/// else: -/// return False +/// def foo(x: int) -> bool: +/// if x > 0: +/// return True +/// else: +/// return False /// ``` /// /// Use instead: /// ```python -/// return x > 0 +/// def foo(x: int) -> bool: +/// return x > 0 /// ``` /// /// Or, given: /// ```python -/// if x > 0: -/// return True -/// return False +/// def foo(x: int) -> bool: +/// if x > 0: +/// return True +/// return False /// ``` /// /// Use instead: /// ```python -/// return x > 0 +/// def foo(x: int) -> bool: +/// return x > 0 /// ``` /// +/// ## Fix safety +/// +/// This fix is marked as unsafe because it may change the program’s behavior if the condition does not +/// return a proper Boolean. While the fix will try to wrap non-boolean values in a call to bool, +/// custom implementations of comparison functions like `__eq__` can avoid the bool call and still +/// lead to altered behavior. +/// /// ## References /// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[derive(ViolationMetadata)] @@ -96,11 +107,13 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { // else: // return False // ``` - [ElifElseClause { - body: else_body, - test: None, - .. - }] => ( + [ + ElifElseClause { + body: else_body, + test: None, + .. + }, + ] => ( if_test.as_ref(), if_body, else_body.as_slice(), @@ -113,15 +126,21 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { // elif x < 0: // return False // ``` - [.., ElifElseClause { - body: elif_body, - test: Some(elif_test), - range: elif_range, - }, ElifElseClause { - body: else_body, - test: None, - range: else_range, - }] => ( + [ + .., + ElifElseClause { + body: elif_body, + test: Some(elif_test), + range: elif_range, + node_index: _, + }, + ElifElseClause { + body: else_body, + test: None, + range: else_range, + node_index: _, + }, + ] => ( elif_test, elif_body, else_body.as_slice(), @@ -237,6 +256,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { left: left.clone(), comparators: Box::new([right.clone()]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) } @@ -244,6 +264,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { op: ast::UnaryOp::Not, operand: Box::new(if_test.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), } } else if if_test.is_compare_expr() { @@ -256,6 +277,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { id: Name::new_static("bool"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let call_node = ast::ExprCall { func: Box::new(func_node.into()), @@ -263,8 +285,10 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { args: Box::from([if_test.clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; Some(Expr::Call(call_node)) } else { @@ -277,6 +301,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { Stmt::Return(ast::StmtReturn { value: Some(Box::new(expr.clone())), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) }); @@ -288,7 +313,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { .as_ref() .map(|expr| checker.generator().expr(expr)); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NeedlessBool { condition: condition.map(SourceCodeSnippet::new), negate: inverted, @@ -301,7 +326,6 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { range, ))); } - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -312,11 +336,7 @@ enum Bool { impl From for Bool { fn from(value: bool) -> Self { - if value { - Bool::True - } else { - Bool::False - } + if value { Bool::True } else { Bool::False } } } @@ -324,7 +344,12 @@ fn is_one_line_return_bool(stmts: &[Stmt]) -> Option { let [stmt] = stmts else { return None; }; - let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt else { + let Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) = stmt + else { return None; }; let Some(Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. })) = value.as_deref() else { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index fc36a1acfc32f..33c789455fbd8 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::{ScopeKind, SemanticModel}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -49,7 +49,12 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool { let Some(expr) = semantic.current_expression_grandparent() else { return false; }; - let Expr::Await(ast::ExprAwait { value, range: _ }) = expr else { + let Expr::Await(ast::ExprAwait { + value, + range: _, + node_index: _, + }) = expr + else { return false; }; let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { @@ -265,8 +270,5 @@ pub(crate) fn open_file_with_context_handler(checker: &Checker, call: &ast::Expr } } - checker.report_diagnostic(Diagnostic::new( - OpenFileWithContextHandler, - call.func.range(), - )); + checker.report_diagnostic(OpenFileWithContextHandler, call.func.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index f2da4443ff69c..04837424637df 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::name::Name; use ruff_python_ast::traversal; @@ -13,6 +12,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::fits; use crate::line_width::LineWidthBuilder; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `for` loops that can be replaced with a builtin function, like @@ -23,17 +23,23 @@ use crate::line_width::LineWidthBuilder; /// /// ## Example /// ```python -/// for item in iterable: -/// if predicate(item): -/// return True -/// return False +/// def foo(): +/// for item in iterable: +/// if predicate(item): +/// return True +/// return False /// ``` /// /// Use instead: /// ```python -/// return any(predicate(item) for item in iterable) +/// def foo(): +/// return any(predicate(item) for item in iterable) /// ``` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because it might remove comments. +/// /// ## References /// - [Python documentation: `any`](https://docs.python.org/3/library/functions.html#any) /// - [Python documentation: `all`](https://docs.python.org/3/library/functions.html#all) @@ -101,13 +107,13 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { &contents, stmt.into(), checker.locator(), - checker.settings.pycodestyle.max_line_length, - checker.settings.tab_size, + checker.settings().pycodestyle.max_line_length, + checker.settings().tab_size, ) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ReimplementedBuiltin { replacement: contents.to_string(), }, @@ -120,7 +126,6 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { terminal.stmt.end(), ))); } - checker.report_diagnostic(diagnostic); } // Replace with `all`. (false, true) => { @@ -130,6 +135,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { op: UnaryOp::Not, operand, range: _, + node_index: _, }) = &loop_.test { *operand.clone() @@ -138,6 +144,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { ops, comparators, range: _, + node_index: _, }) = &loop_.test { if let ([op], [comparator]) = (&**ops, &**comparators) { @@ -158,6 +165,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { ops: Box::from([op]), comparators: Box::from([comparator.clone()]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; node.into() } else { @@ -165,6 +173,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { op: UnaryOp::Not, operand: Box::new(loop_.test.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; node.into() } @@ -173,6 +182,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { op: UnaryOp::Not, operand: Box::new(loop_.test.clone()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; node.into() } @@ -187,19 +197,19 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { // Don't flag if the resulting expression would exceed the maximum line length. let line_start = checker.locator().line_start(stmt.start()); - if LineWidthBuilder::new(checker.settings.tab_size) + if LineWidthBuilder::new(checker.settings().tab_size) .add_str( checker .locator() .slice(TextRange::new(line_start, stmt.start())), ) .add_str(&contents) - > checker.settings.pycodestyle.max_line_length + > checker.settings().pycodestyle.max_line_length { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ReimplementedBuiltin { replacement: contents.to_string(), }, @@ -212,7 +222,6 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) { terminal.stmt.end(), ))); } - checker.report_diagnostic(diagnostic); } _ => {} } @@ -268,22 +277,28 @@ fn match_loop(stmt: &Stmt) -> Option { // The loop itself should contain a single `if` statement, with a single `return` statement in // the body. - let [Stmt::If(ast::StmtIf { - body: nested_body, - test: nested_test, - elif_else_clauses: nested_elif_else_clauses, - range: _, - })] = body.as_slice() + let [ + Stmt::If(ast::StmtIf { + body: nested_body, + test: nested_test, + elif_else_clauses: nested_elif_else_clauses, + range: _, + node_index: _, + }), + ] = body.as_slice() else { return None; }; if !nested_elif_else_clauses.is_empty() { return None; } - let [Stmt::Return(ast::StmtReturn { - value: Some(value), - range: _, - })] = nested_body.as_slice() + let [ + Stmt::Return(ast::StmtReturn { + value: Some(value), + range: _, + node_index: _, + }), + ] = nested_body.as_slice() else { return None; }; @@ -315,10 +330,13 @@ fn match_else_return(stmt: &Stmt) -> Option { }; // The `else` block has to contain a single `return True` or `return False`. - let [Stmt::Return(ast::StmtReturn { - value: Some(next_value), - range: _, - })] = orelse.as_slice() + let [ + Stmt::Return(ast::StmtReturn { + value: Some(next_value), + range: _, + node_index: _, + }), + ] = orelse.as_slice() else { return None; }; @@ -360,6 +378,7 @@ fn match_sibling_return<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option split_default(str_value, maxsplit_value), + Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value, direction), Expr::StringLiteral(sep_value) => { let sep_value_str = sep_value.value.to_str(); Some(split_sep( @@ -99,10 +99,10 @@ pub(crate) fn split_static_string( } } } else { - split_default(str_value, maxsplit_value) + split_default(str_value, maxsplit_value, direction) }; - let mut diagnostic = Diagnostic::new(SplitStaticString, call.range()); + let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range()); if let Some(ref replacement_expr) = split_replacement { diagnostic.set_fix(Fix::applicable_edit( Edit::range_replacement(checker.generator().expr(replacement_expr), call.range()), @@ -114,7 +114,6 @@ pub(crate) fn split_static_string( }, )); } - checker.report_diagnostic(diagnostic); } fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr { @@ -125,6 +124,7 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr { Expr::from(StringLiteral { value: Box::from(*elt), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), // intentionally omit the triple quote flag, if set, to avoid strange // replacements like // @@ -141,10 +141,15 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr { .collect(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } -fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option { +fn split_default( + str_value: &StringLiteralValue, + max_split: i32, + direction: Direction, +) -> Option { // From the Python documentation: // > If sep is not specified or is None, a different splitting algorithm is applied: runs of // > consecutive whitespace are regarded as a single separator, and the result will contain @@ -152,6 +157,7 @@ fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option // > Consequently, splitting an empty string or a string consisting of just whitespace with // > a None separator returns []. // https://docs.python.org/3/library/stdtypes.html#str.split + let string_val = str_value.to_str(); match max_split.cmp(&0) { Ordering::Greater => { // Autofix for `maxsplit` without separator not yet implemented, as @@ -160,14 +166,30 @@ fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option None } Ordering::Equal => { - let list_items: Vec<&str> = vec![str_value.to_str()]; + // Behavior for maxsplit = 0 when sep is None: + // - If the string is empty or all whitespace, result is []. + // - Otherwise: + // - " x ".split(maxsplit=0) -> ['x '] + // - " x ".rsplit(maxsplit=0) -> [' x'] + // - "".split(maxsplit=0) -> [] + // - " ".split(maxsplit=0) -> [] + let processed_str = if direction == Direction::Left { + string_val.trim_start() + } else { + string_val.trim_end() + }; + let list_items: &[_] = if processed_str.is_empty() { + &[] + } else { + &[processed_str] + }; Some(construct_replacement( - &list_items, + list_items, str_value.first_literal_flags(), )) } Ordering::Less => { - let list_items: Vec<&str> = str_value.to_str().split_whitespace().collect(); + let list_items: Vec<&str> = string_val.split_whitespace().collect(); Some(construct_replacement( &list_items, str_value.first_literal_flags(), @@ -186,12 +208,20 @@ fn split_sep( let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) { match direction { Direction::Left => value.splitn(split_n + 1, sep_value).collect(), - Direction::Right => value.rsplitn(split_n + 1, sep_value).collect(), + Direction::Right => { + let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect(); + items.reverse(); + items + } } } else { match direction { Direction::Left => value.split(sep_value).collect(), - Direction::Right => value.rsplit(sep_value).collect(), + Direction::Right => { + let mut items: Vec<&str> = value.rsplit(sep_value).collect(); + items.reverse(); + items + } } }; diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs index 49cfabae59521..34c4915f3bc46 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; @@ -9,6 +8,7 @@ use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `try`-`except`-`pass` blocks that can be replaced with the @@ -65,7 +65,13 @@ impl Violation for SuppressibleException { fn is_empty(body: &[Stmt]) -> bool { match body { [Stmt::Pass(_)] => true, - [Stmt::Expr(ast::StmtExpr { value, range: _ })] => value.is_ellipsis_literal_expr(), + [ + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }), + ] => value.is_ellipsis_literal_expr(), _ => false, } } @@ -96,8 +102,11 @@ pub(crate) fn suppressible_exception( return; } - let [ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, range, .. })] = - handlers + let [ + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + body, range, type_, .. + }), + ] = handlers else { return; }; @@ -115,12 +124,18 @@ pub(crate) fn suppressible_exception( }; let exception = if handler_names.is_empty() { - "Exception".to_string() + if type_.is_none() { + // case where there are no handler names provided at all + "BaseException".to_string() + } else { + // case where handler names is an empty tuple + String::new() + } } else { handler_names.join(", ") }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( SuppressibleException { exception: exception.clone(), }, @@ -136,16 +151,29 @@ pub(crate) fn suppressible_exception( stmt.start(), checker.semantic(), )?; - let replace_try = Edit::range_replacement( - format!("with {binding}({exception})"), + let mut rest: Vec = Vec::new(); + let content: String; + if exception == "BaseException" && handler_names.is_empty() { + let (import_exception, binding_exception) = + checker.importer().get_or_import_symbol( + &ImportRequest::import("builtins", &exception), + stmt.start(), + checker.semantic(), + )?; + content = format!("with {binding}({binding_exception})"); + rest.push(import_exception); + } else { + content = format!("with {binding}({exception})"); + } + rest.push(Edit::range_deletion( + checker.locator().full_lines_range(*range), + )); + rest.push(Edit::range_replacement( + content, TextRange::at(stmt.start(), "try".text_len()), - ); - let remove_handler = Edit::range_deletion(checker.locator().full_lines_range(*range)); - Ok(Fix::unsafe_edits( - import_edit, - [replace_try, remove_handler], - )) + )); + + Ok(Fix::unsafe_edits(import_edit, rest)) }); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs index 9afd4812dfa98..a21b960af07d4 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -3,19 +3,19 @@ use std::cmp; use anyhow::Result; use libcst_native::CompOp; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, CmpOp, Expr, UnaryOp}; use ruff_python_codegen::Stylist; use ruff_python_stdlib::str::{self}; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; use crate::cst::helpers::or_space; use crate::cst::matchers::{match_comparison, transform_expression}; use crate::fix::edits::pad; use crate::fix::snippet::SourceCodeSnippet; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for conditions that position a constant on the left-hand side of the @@ -116,6 +116,7 @@ impl From<&Expr> for ConstantLikelihood { op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert, operand, range: _, + node_index: _, }) => ConstantLikelihood::from(&**operand), _ => ConstantLikelihood::Unlikely, } @@ -225,7 +226,7 @@ pub(crate) fn yoda_conditions( } if let Ok(suggestion) = reverse_comparison(expr, checker.locator(), checker.stylist()) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( YodaConditions { suggestion: Some(SourceCodeSnippet::new(suggestion.clone())), }, @@ -235,11 +236,7 @@ pub(crate) fn yoda_conditions( pad(suggestion, expr.range(), checker.locator()), expr.range(), ))); - checker.report_diagnostic(diagnostic); } else { - checker.report_diagnostic(Diagnostic::new( - YodaConditions { suggestion: None }, - expr.range(), - )); + checker.report_diagnostic(YodaConditions { suggestion: None }, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs index 6639392f19a4a..58f3a01a8a0ac 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/zip_dict_keys_and_values.rs @@ -1,11 +1,12 @@ use ast::{ExprAttribute, ExprName, Identifier}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr}; +use ruff_python_semantic::analyze::typing::is_dict; use ruff_text_size::Ranged; +use crate::fix::edits; +use crate::{AlwaysFixableViolation, Edit, Fix}; use crate::{checkers::ast::Checker, fix::snippet::SourceCodeSnippet}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_python_semantic::analyze::typing::is_dict; /// ## What it does /// Checks for use of `zip()` to iterate over keys and values of a dictionary at once. @@ -67,9 +68,11 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall) } = expr; match &keywords[..] { [] => {} - [ast::Keyword { - arg: Some(name), .. - }] if name.as_str() == "strict" => {} + [ + ast::Keyword { + arg: Some(name), .. + }, + ] if name.as_str() == "strict" => {} _ => return, } let [arg1, arg2] = &args[..] else { @@ -90,7 +93,7 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall) let Some(binding) = checker .semantic() - .only_binding(var1) + .resolve_name(var1) .map(|id| checker.semantic().binding(id)) else { return; @@ -99,10 +102,14 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall) return; } - let expected = format!("{}.items()", checker.locator().slice(var1)); + let expected = edits::pad( + format!("{}.items()", checker.locator().slice(var1)), + expr.range(), + checker.locator(), + ); let actual = checker.locator().slice(expr); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ZipDictKeysAndValues { expected: SourceCodeSnippet::new(expected.clone()), actual: SourceCodeSnippet::from_str(actual), @@ -113,7 +120,6 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall) expected, expr.range(), ))); - checker.report_diagnostic(diagnostic); } fn get_var_attr(expr: &Expr) -> Option<(&ExprName, &Identifier)> { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_0.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_0.py.snap index da95917f8a763..c4fe31837ce89 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_0.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM105_SIM105_0.py.snap @@ -90,7 +90,7 @@ SIM105_0.py:19:1: SIM105 [*] Use `contextlib.suppress(ValueError, OSError)` inst 24 23 | # SIM105 25 24 | try: -SIM105_0.py:25:1: SIM105 [*] Use `contextlib.suppress(Exception)` instead of `try`-`except`-`pass` +SIM105_0.py:25:1: SIM105 [*] Use `contextlib.suppress(BaseException)` instead of `try`-`except`-`pass` | 24 | # SIM105 25 | / try: @@ -101,25 +101,26 @@ SIM105_0.py:25:1: SIM105 [*] Use `contextlib.suppress(Exception)` instead of `tr 29 | 30 | # SIM105 | - = help: Replace with `contextlib.suppress(Exception)` + = help: Replace with `contextlib.suppress(BaseException)` ℹ Unsafe fix 1 |+import contextlib -1 2 | def foo(): -2 3 | pass -3 4 | + 2 |+import builtins +1 3 | def foo(): +2 4 | pass +3 5 | -------------------------------------------------------------------------------- -22 23 | pass -23 24 | -24 25 | # SIM105 +22 24 | pass +23 25 | +24 26 | # SIM105 25 |-try: - 26 |+with contextlib.suppress(Exception): -26 27 | foo() + 27 |+with contextlib.suppress(builtins.BaseException): +26 28 | foo() 27 |-except: 28 |- pass -29 28 | -30 29 | # SIM105 -31 30 | try: +29 29 | +30 30 | # SIM105 +31 31 | try: SIM105_0.py:31:1: SIM105 [*] Use `contextlib.suppress(a.Error, b.Error)` instead of `try`-`except`-`pass` | @@ -285,3 +286,59 @@ SIM105_0.py:126:5: SIM105 [*] Use `contextlib.suppress(OSError)` instead of `try 127 |+ with contextlib.suppress(OSError): os.makedirs(model_dir); 129 128 | \ 130 129 | # +131 130 | + +SIM105_0.py:133:1: SIM105 [*] Use `contextlib.suppress()` instead of `try`-`except`-`pass` + | +132 | # Regression tests for: https://github.com/astral-sh/ruff/issues/18209 +133 | / try: +134 | | 1 / 0 +135 | | except (): +136 | | pass + | |________^ SIM105 + | + = help: Replace with `contextlib.suppress()` + +ℹ Unsafe fix + 1 |+import contextlib +1 2 | def foo(): +2 3 | pass +3 4 | +-------------------------------------------------------------------------------- +130 131 | # +131 132 | +132 133 | # Regression tests for: https://github.com/astral-sh/ruff/issues/18209 +133 |-try: + 134 |+with contextlib.suppress(): +134 135 | 1 / 0 +135 |-except (): +136 |- pass +137 136 | +138 137 | +139 138 | BaseException = ValueError + +SIM105_0.py:140:1: SIM105 [*] Use `contextlib.suppress(BaseException)` instead of `try`-`except`-`pass` + | +139 | BaseException = ValueError +140 | / try: +141 | | int("a") +142 | | except BaseException: +143 | | pass + | |________^ SIM105 + | + = help: Replace with `contextlib.suppress(BaseException)` + +ℹ Unsafe fix + 1 |+import contextlib +1 2 | def foo(): +2 3 | pass +3 4 | +-------------------------------------------------------------------------------- +137 138 | +138 139 | +139 140 | BaseException = ValueError +140 |-try: + 141 |+with contextlib.suppress(BaseException): +141 142 | int("a") +142 |-except BaseException: +143 |- pass diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM108_SIM108.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM108_SIM108.py.snap index a18c5fdac5b1b..b4d70317ad555 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM108_SIM108.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM108_SIM108.py.snap @@ -118,7 +118,7 @@ SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of ` | = help: Replace `if`-`else`-block with `x = 3 if True else 5` -SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_cond` instead of `if`-`else`-block +SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block | 139 | # SIM108 - should suggest 140 | # z = cond or other_cond @@ -130,7 +130,7 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co 145 | 146 | # SIM108 - should suggest | - = help: Replace `if`-`else`-block with `z = cond if cond else other_cond` + = help: Replace `if`-`else`-block with `z = cond or other_cond` ℹ Unsafe fix 138 138 | @@ -140,12 +140,12 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co 142 |- z = cond 143 |-else: 144 |- z = other_cond - 141 |+z = cond if cond else other_cond + 141 |+z = cond or other_cond 145 142 | 146 143 | # SIM108 - should suggest 147 144 | # z = cond and other_cond -SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else other_cond` instead of `if`-`else`-block +SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block | 146 | # SIM108 - should suggest 147 | # z = cond and other_cond @@ -157,7 +157,7 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe 152 | 153 | # SIM108 - should suggest | - = help: Replace `if`-`else`-block with `z = cond if not cond else other_cond` + = help: Replace `if`-`else`-block with `z = cond and other_cond` ℹ Unsafe fix 145 145 | @@ -167,12 +167,12 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe 149 |- z = cond 150 |-else: 151 |- z = other_cond - 148 |+z = cond if not cond else other_cond + 148 |+z = cond and other_cond 152 149 | 153 150 | # SIM108 - should suggest 154 151 | # z = not cond and other_cond -SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else other_cond` instead of `if`-`else`-block +SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block | 153 | # SIM108 - should suggest 154 | # z = not cond and other_cond @@ -184,7 +184,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe 159 | 160 | # SIM108 does not suggest | - = help: Replace `if`-`else`-block with `z = not cond if cond else other_cond` + = help: Replace `if`-`else`-block with `z = not cond and other_cond` ℹ Unsafe fix 152 152 | @@ -194,7 +194,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe 156 |- z = not cond 157 |-else: 158 |- z = other_cond - 155 |+z = not cond if cond else other_cond + 155 |+z = not cond and other_cond 159 156 | 160 157 | # SIM108 does not suggest 161 158 | # a binary option in these cases, diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM115_SIM115.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM115_SIM115.py.snap index 551ddd03158b6..5e448f3e6d871 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM115_SIM115.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM115_SIM115.py.snap @@ -50,285 +50,285 @@ SIM115.py:12:5: SIM115 Use a context manager for opening files 14 | f.close() | -SIM115.py:39:9: SIM115 Use a context manager for opening files +SIM115.py:40:9: SIM115 Use a context manager for opening files | -37 | # SIM115 -38 | with contextlib.ExitStack(): -39 | f = open("filename") +38 | # SIM115 +39 | with contextlib.ExitStack(): +40 | f = open("filename") | ^^^^ SIM115 -40 | -41 | # OK - | - -SIM115.py:80:5: SIM115 Use a context manager for opening files - | -78 | import fileinput -79 | -80 | f = tempfile.NamedTemporaryFile() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 -81 | f = tempfile.TemporaryFile() -82 | f = tempfile.SpooledTemporaryFile() +41 | +42 | # OK | SIM115.py:81:5: SIM115 Use a context manager for opening files | -80 | f = tempfile.NamedTemporaryFile() -81 | f = tempfile.TemporaryFile() - | ^^^^^^^^^^^^^^^^^^^^^^ SIM115 -82 | f = tempfile.SpooledTemporaryFile() -83 | f = tarfile.open("foo.tar") +79 | import fileinput +80 | +81 | f = tempfile.NamedTemporaryFile() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 +82 | f = tempfile.TemporaryFile() +83 | f = tempfile.SpooledTemporaryFile() | SIM115.py:82:5: SIM115 Use a context manager for opening files | -80 | f = tempfile.NamedTemporaryFile() -81 | f = tempfile.TemporaryFile() -82 | f = tempfile.SpooledTemporaryFile() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 -83 | f = tarfile.open("foo.tar") -84 | f = TarFile("foo.tar").open() +81 | f = tempfile.NamedTemporaryFile() +82 | f = tempfile.TemporaryFile() + | ^^^^^^^^^^^^^^^^^^^^^^ SIM115 +83 | f = tempfile.SpooledTemporaryFile() +84 | f = tarfile.open("foo.tar") | SIM115.py:83:5: SIM115 Use a context manager for opening files | -81 | f = tempfile.TemporaryFile() -82 | f = tempfile.SpooledTemporaryFile() -83 | f = tarfile.open("foo.tar") - | ^^^^^^^^^^^^ SIM115 -84 | f = TarFile("foo.tar").open() -85 | f = tarfile.TarFile("foo.tar").open() +81 | f = tempfile.NamedTemporaryFile() +82 | f = tempfile.TemporaryFile() +83 | f = tempfile.SpooledTemporaryFile() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 +84 | f = tarfile.open("foo.tar") +85 | f = TarFile("foo.tar").open() | SIM115.py:84:5: SIM115 Use a context manager for opening files | -82 | f = tempfile.SpooledTemporaryFile() -83 | f = tarfile.open("foo.tar") -84 | f = TarFile("foo.tar").open() - | ^^^^^^^^^^^^^^^^^^^^^^^ SIM115 -85 | f = tarfile.TarFile("foo.tar").open() -86 | f = tarfile.TarFile().open() +82 | f = tempfile.TemporaryFile() +83 | f = tempfile.SpooledTemporaryFile() +84 | f = tarfile.open("foo.tar") + | ^^^^^^^^^^^^ SIM115 +85 | f = TarFile("foo.tar").open() +86 | f = tarfile.TarFile("foo.tar").open() | SIM115.py:85:5: SIM115 Use a context manager for opening files | -83 | f = tarfile.open("foo.tar") -84 | f = TarFile("foo.tar").open() -85 | f = tarfile.TarFile("foo.tar").open() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 -86 | f = tarfile.TarFile().open() -87 | f = zipfile.ZipFile("foo.zip").open("foo.txt") +83 | f = tempfile.SpooledTemporaryFile() +84 | f = tarfile.open("foo.tar") +85 | f = TarFile("foo.tar").open() + | ^^^^^^^^^^^^^^^^^^^^^^^ SIM115 +86 | f = tarfile.TarFile("foo.tar").open() +87 | f = tarfile.TarFile().open() | SIM115.py:86:5: SIM115 Use a context manager for opening files | -84 | f = TarFile("foo.tar").open() -85 | f = tarfile.TarFile("foo.tar").open() -86 | f = tarfile.TarFile().open() - | ^^^^^^^^^^^^^^^^^^^^^^ SIM115 -87 | f = zipfile.ZipFile("foo.zip").open("foo.txt") -88 | f = io.open("foo.txt") +84 | f = tarfile.open("foo.tar") +85 | f = TarFile("foo.tar").open() +86 | f = tarfile.TarFile("foo.tar").open() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 +87 | f = tarfile.TarFile().open() +88 | f = zipfile.ZipFile("foo.zip").open("foo.txt") | SIM115.py:87:5: SIM115 Use a context manager for opening files | -85 | f = tarfile.TarFile("foo.tar").open() -86 | f = tarfile.TarFile().open() -87 | f = zipfile.ZipFile("foo.zip").open("foo.txt") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 -88 | f = io.open("foo.txt") -89 | f = io.open_code("foo.txt") +85 | f = TarFile("foo.tar").open() +86 | f = tarfile.TarFile("foo.tar").open() +87 | f = tarfile.TarFile().open() + | ^^^^^^^^^^^^^^^^^^^^^^ SIM115 +88 | f = zipfile.ZipFile("foo.zip").open("foo.txt") +89 | f = io.open("foo.txt") | SIM115.py:88:5: SIM115 Use a context manager for opening files | -86 | f = tarfile.TarFile().open() -87 | f = zipfile.ZipFile("foo.zip").open("foo.txt") -88 | f = io.open("foo.txt") - | ^^^^^^^ SIM115 -89 | f = io.open_code("foo.txt") -90 | f = codecs.open("foo.txt") +86 | f = tarfile.TarFile("foo.tar").open() +87 | f = tarfile.TarFile().open() +88 | f = zipfile.ZipFile("foo.zip").open("foo.txt") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115 +89 | f = io.open("foo.txt") +90 | f = io.open_code("foo.txt") | SIM115.py:89:5: SIM115 Use a context manager for opening files | -87 | f = zipfile.ZipFile("foo.zip").open("foo.txt") -88 | f = io.open("foo.txt") -89 | f = io.open_code("foo.txt") - | ^^^^^^^^^^^^ SIM115 -90 | f = codecs.open("foo.txt") -91 | f = bz2.open("foo.txt") +87 | f = tarfile.TarFile().open() +88 | f = zipfile.ZipFile("foo.zip").open("foo.txt") +89 | f = io.open("foo.txt") + | ^^^^^^^ SIM115 +90 | f = io.open_code("foo.txt") +91 | f = codecs.open("foo.txt") | SIM115.py:90:5: SIM115 Use a context manager for opening files | -88 | f = io.open("foo.txt") -89 | f = io.open_code("foo.txt") -90 | f = codecs.open("foo.txt") - | ^^^^^^^^^^^ SIM115 -91 | f = bz2.open("foo.txt") -92 | f = gzip.open("foo.txt") +88 | f = zipfile.ZipFile("foo.zip").open("foo.txt") +89 | f = io.open("foo.txt") +90 | f = io.open_code("foo.txt") + | ^^^^^^^^^^^^ SIM115 +91 | f = codecs.open("foo.txt") +92 | f = bz2.open("foo.txt") | SIM115.py:91:5: SIM115 Use a context manager for opening files | -89 | f = io.open_code("foo.txt") -90 | f = codecs.open("foo.txt") -91 | f = bz2.open("foo.txt") - | ^^^^^^^^ SIM115 -92 | f = gzip.open("foo.txt") -93 | f = dbm.open("foo.db") +89 | f = io.open("foo.txt") +90 | f = io.open_code("foo.txt") +91 | f = codecs.open("foo.txt") + | ^^^^^^^^^^^ SIM115 +92 | f = bz2.open("foo.txt") +93 | f = gzip.open("foo.txt") | SIM115.py:92:5: SIM115 Use a context manager for opening files | -90 | f = codecs.open("foo.txt") -91 | f = bz2.open("foo.txt") -92 | f = gzip.open("foo.txt") - | ^^^^^^^^^ SIM115 -93 | f = dbm.open("foo.db") -94 | f = dbm.gnu.open("foo.db") +90 | f = io.open_code("foo.txt") +91 | f = codecs.open("foo.txt") +92 | f = bz2.open("foo.txt") + | ^^^^^^^^ SIM115 +93 | f = gzip.open("foo.txt") +94 | f = dbm.open("foo.db") | SIM115.py:93:5: SIM115 Use a context manager for opening files | -91 | f = bz2.open("foo.txt") -92 | f = gzip.open("foo.txt") -93 | f = dbm.open("foo.db") - | ^^^^^^^^ SIM115 -94 | f = dbm.gnu.open("foo.db") -95 | f = dbm.ndbm.open("foo.db") +91 | f = codecs.open("foo.txt") +92 | f = bz2.open("foo.txt") +93 | f = gzip.open("foo.txt") + | ^^^^^^^^^ SIM115 +94 | f = dbm.open("foo.db") +95 | f = dbm.gnu.open("foo.db") | SIM115.py:94:5: SIM115 Use a context manager for opening files | -92 | f = gzip.open("foo.txt") -93 | f = dbm.open("foo.db") -94 | f = dbm.gnu.open("foo.db") - | ^^^^^^^^^^^^ SIM115 -95 | f = dbm.ndbm.open("foo.db") -96 | f = dbm.dumb.open("foo.db") +92 | f = bz2.open("foo.txt") +93 | f = gzip.open("foo.txt") +94 | f = dbm.open("foo.db") + | ^^^^^^^^ SIM115 +95 | f = dbm.gnu.open("foo.db") +96 | f = dbm.ndbm.open("foo.db") | SIM115.py:95:5: SIM115 Use a context manager for opening files | -93 | f = dbm.open("foo.db") -94 | f = dbm.gnu.open("foo.db") -95 | f = dbm.ndbm.open("foo.db") - | ^^^^^^^^^^^^^ SIM115 -96 | f = dbm.dumb.open("foo.db") -97 | f = lzma.open("foo.xz") +93 | f = gzip.open("foo.txt") +94 | f = dbm.open("foo.db") +95 | f = dbm.gnu.open("foo.db") + | ^^^^^^^^^^^^ SIM115 +96 | f = dbm.ndbm.open("foo.db") +97 | f = dbm.dumb.open("foo.db") | SIM115.py:96:5: SIM115 Use a context manager for opening files | -94 | f = dbm.gnu.open("foo.db") -95 | f = dbm.ndbm.open("foo.db") -96 | f = dbm.dumb.open("foo.db") +94 | f = dbm.open("foo.db") +95 | f = dbm.gnu.open("foo.db") +96 | f = dbm.ndbm.open("foo.db") | ^^^^^^^^^^^^^ SIM115 -97 | f = lzma.open("foo.xz") -98 | f = lzma.LZMAFile("foo.xz") +97 | f = dbm.dumb.open("foo.db") +98 | f = lzma.open("foo.xz") | SIM115.py:97:5: SIM115 Use a context manager for opening files | -95 | f = dbm.ndbm.open("foo.db") -96 | f = dbm.dumb.open("foo.db") -97 | f = lzma.open("foo.xz") - | ^^^^^^^^^ SIM115 -98 | f = lzma.LZMAFile("foo.xz") -99 | f = shelve.open("foo.db") +95 | f = dbm.gnu.open("foo.db") +96 | f = dbm.ndbm.open("foo.db") +97 | f = dbm.dumb.open("foo.db") + | ^^^^^^^^^^^^^ SIM115 +98 | f = lzma.open("foo.xz") +99 | f = lzma.LZMAFile("foo.xz") | SIM115.py:98:5: SIM115 Use a context manager for opening files | - 96 | f = dbm.dumb.open("foo.db") - 97 | f = lzma.open("foo.xz") - 98 | f = lzma.LZMAFile("foo.xz") - | ^^^^^^^^^^^^^ SIM115 - 99 | f = shelve.open("foo.db") -100 | f = tokenize.open("foo.py") + 96 | f = dbm.ndbm.open("foo.db") + 97 | f = dbm.dumb.open("foo.db") + 98 | f = lzma.open("foo.xz") + | ^^^^^^^^^ SIM115 + 99 | f = lzma.LZMAFile("foo.xz") +100 | f = shelve.open("foo.db") | SIM115.py:99:5: SIM115 Use a context manager for opening files | - 97 | f = lzma.open("foo.xz") - 98 | f = lzma.LZMAFile("foo.xz") - 99 | f = shelve.open("foo.db") - | ^^^^^^^^^^^ SIM115 -100 | f = tokenize.open("foo.py") -101 | f = wave.open("foo.wav") + 97 | f = dbm.dumb.open("foo.db") + 98 | f = lzma.open("foo.xz") + 99 | f = lzma.LZMAFile("foo.xz") + | ^^^^^^^^^^^^^ SIM115 +100 | f = shelve.open("foo.db") +101 | f = tokenize.open("foo.py") | SIM115.py:100:5: SIM115 Use a context manager for opening files | - 98 | f = lzma.LZMAFile("foo.xz") - 99 | f = shelve.open("foo.db") -100 | f = tokenize.open("foo.py") - | ^^^^^^^^^^^^^ SIM115 -101 | f = wave.open("foo.wav") -102 | f = tarfile.TarFile.taropen("foo.tar") + 98 | f = lzma.open("foo.xz") + 99 | f = lzma.LZMAFile("foo.xz") +100 | f = shelve.open("foo.db") + | ^^^^^^^^^^^ SIM115 +101 | f = tokenize.open("foo.py") +102 | f = wave.open("foo.wav") | SIM115.py:101:5: SIM115 Use a context manager for opening files | - 99 | f = shelve.open("foo.db") -100 | f = tokenize.open("foo.py") -101 | f = wave.open("foo.wav") - | ^^^^^^^^^ SIM115 -102 | f = tarfile.TarFile.taropen("foo.tar") -103 | f = fileinput.input("foo.txt") + 99 | f = lzma.LZMAFile("foo.xz") +100 | f = shelve.open("foo.db") +101 | f = tokenize.open("foo.py") + | ^^^^^^^^^^^^^ SIM115 +102 | f = wave.open("foo.wav") +103 | f = tarfile.TarFile.taropen("foo.tar") | SIM115.py:102:5: SIM115 Use a context manager for opening files | -100 | f = tokenize.open("foo.py") -101 | f = wave.open("foo.wav") -102 | f = tarfile.TarFile.taropen("foo.tar") - | ^^^^^^^^^^^^^^^^^^^^^^^ SIM115 -103 | f = fileinput.input("foo.txt") -104 | f = fileinput.FileInput("foo.txt") +100 | f = shelve.open("foo.db") +101 | f = tokenize.open("foo.py") +102 | f = wave.open("foo.wav") + | ^^^^^^^^^ SIM115 +103 | f = tarfile.TarFile.taropen("foo.tar") +104 | f = fileinput.input("foo.txt") | SIM115.py:103:5: SIM115 Use a context manager for opening files | -101 | f = wave.open("foo.wav") -102 | f = tarfile.TarFile.taropen("foo.tar") -103 | f = fileinput.input("foo.txt") - | ^^^^^^^^^^^^^^^ SIM115 -104 | f = fileinput.FileInput("foo.txt") +101 | f = tokenize.open("foo.py") +102 | f = wave.open("foo.wav") +103 | f = tarfile.TarFile.taropen("foo.tar") + | ^^^^^^^^^^^^^^^^^^^^^^^ SIM115 +104 | f = fileinput.input("foo.txt") +105 | f = fileinput.FileInput("foo.txt") | SIM115.py:104:5: SIM115 Use a context manager for opening files | -102 | f = tarfile.TarFile.taropen("foo.tar") -103 | f = fileinput.input("foo.txt") -104 | f = fileinput.FileInput("foo.txt") +102 | f = wave.open("foo.wav") +103 | f = tarfile.TarFile.taropen("foo.tar") +104 | f = fileinput.input("foo.txt") + | ^^^^^^^^^^^^^^^ SIM115 +105 | f = fileinput.FileInput("foo.txt") + | + +SIM115.py:105:5: SIM115 Use a context manager for opening files + | +103 | f = tarfile.TarFile.taropen("foo.tar") +104 | f = fileinput.input("foo.txt") +105 | f = fileinput.FileInput("foo.txt") | ^^^^^^^^^^^^^^^^^^^ SIM115 -105 | -106 | with contextlib.suppress(Exception): +106 | +107 | with contextlib.suppress(Exception): | -SIM115.py:240:9: SIM115 Use a context manager for opening files +SIM115.py:241:9: SIM115 Use a context manager for opening files | -238 | def aliased(): -239 | from shelve import open as open_shelf -240 | x = open_shelf("foo.dbm") +239 | def aliased(): +240 | from shelve import open as open_shelf +241 | x = open_shelf("foo.dbm") | ^^^^^^^^^^ SIM115 -241 | x.close() +242 | x.close() | -SIM115.py:244:9: SIM115 Use a context manager for opening files +SIM115.py:245:9: SIM115 Use a context manager for opening files | -243 | from tarfile import TarFile as TF -244 | f = TF("foo").open() +244 | from tarfile import TarFile as TF +245 | f = TF("foo").open() | ^^^^^^^^^^^^^^ SIM115 -245 | f.close() +246 | f.close() | -SIM115.py:257:5: SIM115 Use a context manager for opening files +SIM115.py:258:5: SIM115 Use a context manager for opening files | -256 | # SIM115 -257 | f = dbm.sqlite3.open("foo.db") +257 | # SIM115 +258 | f = dbm.sqlite3.open("foo.db") | ^^^^^^^^^^^^^^^^ SIM115 -258 | f.close() +259 | f.close() | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM116_SIM116.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM116_SIM116.py.snap index 844e1906b4b95..459f1a98d4d80 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM116_SIM116.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM116_SIM116.py.snap @@ -1,110 +1,110 @@ --- source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs --- -SIM116.py:5:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:6:5: SIM116 Use a dictionary instead of consecutive `if` statements | - 4 | # SIM116 - 5 | / if a == "foo": - 6 | | return "bar" - 7 | | elif a == "bar": - 8 | | return "baz" - 9 | | elif a == "boo": -10 | | return "ooh" -11 | | else: -12 | | return 42 - | |_____________^ SIM116 -13 | -14 | # SIM116 + 5 | # SIM116 + 6 | / if a == "foo": + 7 | | return "bar" + 8 | | elif a == "bar": + 9 | | return "baz" +10 | | elif a == "boo": +11 | | return "ooh" +12 | | else: +13 | | return 42 + | |_________________^ SIM116 +14 | +15 | # SIM116 | -SIM116.py:15:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:16:5: SIM116 Use a dictionary instead of consecutive `if` statements | -14 | # SIM116 -15 | / if a == 1: -16 | | return (1, 2, 3) -17 | | elif a == 2: -18 | | return (4, 5, 6) -19 | | elif a == 3: -20 | | return (7, 8, 9) -21 | | else: -22 | | return (10, 11, 12) - | |_______________________^ SIM116 -23 | -24 | # SIM116 +15 | # SIM116 +16 | / if a == 1: +17 | | return (1, 2, 3) +18 | | elif a == 2: +19 | | return (4, 5, 6) +20 | | elif a == 3: +21 | | return (7, 8, 9) +22 | | else: +23 | | return (10, 11, 12) + | |___________________________^ SIM116 +24 | +25 | # SIM116 | -SIM116.py:25:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:26:5: SIM116 Use a dictionary instead of consecutive `if` statements | -24 | # SIM116 -25 | / if a == 1: -26 | | return (1, 2, 3) -27 | | elif a == 2: -28 | | return (4, 5, 6) -29 | | elif a == 3: -30 | | return (7, 8, 9) - | |____________________^ SIM116 -31 | -32 | # SIM116 +25 | # SIM116 +26 | / if a == 1: +27 | | return (1, 2, 3) +28 | | elif a == 2: +29 | | return (4, 5, 6) +30 | | elif a == 3: +31 | | return (7, 8, 9) + | |________________________^ SIM116 +32 | +33 | # SIM116 | -SIM116.py:33:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:34:5: SIM116 Use a dictionary instead of consecutive `if` statements | -32 | # SIM116 -33 | / if a == "hello 'sir'": -34 | | return (1, 2, 3) -35 | | elif a == 'goodbye "mam"': -36 | | return (4, 5, 6) -37 | | elif a == """Fairwell 'mister'""": -38 | | return (7, 8, 9) -39 | | else: -40 | | return (10, 11, 12) - | |_______________________^ SIM116 -41 | -42 | # SIM116 +33 | # SIM116 +34 | / if a == "hello 'sir'": +35 | | return (1, 2, 3) +36 | | elif a == 'goodbye "mam"': +37 | | return (4, 5, 6) +38 | | elif a == """Fairwell 'mister'""": +39 | | return (7, 8, 9) +40 | | else: +41 | | return (10, 11, 12) + | |___________________________^ SIM116 +42 | +43 | # SIM116 | -SIM116.py:43:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:44:5: SIM116 Use a dictionary instead of consecutive `if` statements | -42 | # SIM116 -43 | / if a == b"one": -44 | | return 1 -45 | | elif a == b"two": -46 | | return 2 -47 | | elif a == b"three": -48 | | return 3 - | |____________^ SIM116 -49 | -50 | # SIM116 +43 | # SIM116 +44 | / if a == b"one": +45 | | return 1 +46 | | elif a == b"two": +47 | | return 2 +48 | | elif a == b"three": +49 | | return 3 + | |________________^ SIM116 +50 | +51 | # SIM116 | -SIM116.py:51:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:52:5: SIM116 Use a dictionary instead of consecutive `if` statements | -50 | # SIM116 -51 | / if a == "hello 'sir'": -52 | | return ("hello'", 'hi"', 3) -53 | | elif a == 'goodbye "mam"': -54 | | return (4, 5, 6) -55 | | elif a == """Fairwell 'mister'""": -56 | | return (7, 8, 9) -57 | | else: -58 | | return (10, 11, 12) - | |_______________________^ SIM116 -59 | -60 | # OK +51 | # SIM116 +52 | / if a == "hello 'sir'": +53 | | return ("hello'", 'hi"', 3) +54 | | elif a == 'goodbye "mam"': +55 | | return (4, 5, 6) +56 | | elif a == """Fairwell 'mister'""": +57 | | return (7, 8, 9) +58 | | else: +59 | | return (10, 11, 12) + | |___________________________^ SIM116 +60 | +61 | # OK | -SIM116.py:79:1: SIM116 Use a dictionary instead of consecutive `if` statements +SIM116.py:80:5: SIM116 Use a dictionary instead of consecutive `if` statements | -78 | # SIM116 -79 | / if func_name == "create": -80 | | return "A" -81 | | elif func_name == "modify": -82 | | return "M" -83 | | elif func_name == "remove": -84 | | return "D" -85 | | elif func_name == "move": -86 | | return "MV" - | |_______________^ SIM116 -87 | -88 | # OK +79 | # SIM116 +80 | / if func_name == "create": +81 | | return "A" +82 | | elif func_name == "modify": +83 | | return "M" +84 | | elif func_name == "remove": +85 | | return "D" +86 | | elif func_name == "move": +87 | | return "MV" + | |___________________^ SIM116 +88 | +89 | # OK | diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap index 62db77198bc15..ae93115007f43 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap @@ -352,7 +352,7 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split` 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 33 | "".split() # [] -34 | """ +34 | """ | = help: Replace with list literal @@ -363,7 +363,7 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split` 32 |-" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] 32 |+[" a", "a a", "a a "] # [" a", "a a", "a a "] 33 33 | "".split() # [] -34 34 | """ +34 34 | """ 35 35 | """.split() # [] SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split` @@ -371,7 +371,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split` 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] 33 | "".split() # [] | ^^^^^^^^^^ SIM905 -34 | """ +34 | """ 35 | """.split() # [] | = help: Replace with list literal @@ -382,7 +382,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split` 32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] 33 |-"".split() # [] 33 |+[] # [] -34 34 | """ +34 34 | """ 35 35 | """.split() # [] 36 36 | " ".split() # [] @@ -390,7 +390,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split` | 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] 33 | "".split() # [] -34 | / """ +34 | / """ 35 | | """.split() # [] | |___________^ SIM905 36 | " ".split() # [] @@ -402,7 +402,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split` 31 31 | 32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "] 33 33 | "".split() # [] -34 |-""" +34 |-""" 35 |-""".split() # [] 34 |+[] # [] 36 35 | " ".split() # [] @@ -411,7 +411,7 @@ SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split` SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split` | -34 | """ +34 | """ 35 | """.split() # [] 36 | " ".split() # [] | ^^^^^^^^^^^^^^^^^ SIM905 @@ -422,7 +422,7 @@ SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split` ℹ Safe fix 33 33 | "".split() # [] -34 34 | """ +34 34 | """ 35 35 | """.split() # [] 36 |-" ".split() # [] 36 |+[] # [] @@ -442,7 +442,7 @@ SIM905.py:37:1: SIM905 [*] Consider using a list literal instead of `str.split` = help: Replace with list literal ℹ Safe fix -34 34 | """ +34 34 | """ 35 35 | """.split() # [] 36 36 | " ".split() # [] 37 |-"/abc/".split() # ["/abc/"] @@ -854,6 +854,8 @@ SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split` 107 | | "'itemD'" 108 | | """.split() | |___________^ SIM905 +109 | +110 | # https://github.com/astral-sh/ruff/issues/18042 | = help: Replace with list literal @@ -868,3 +870,393 @@ SIM905.py:103:1: SIM905 [*] Consider using a list literal instead of `str.split` 107 |-"'itemD'" 108 |-""".split() 103 |+['"itemA"', "'itemB'", "'''itemC'''", "\"'itemD'\""] +109 104 | +110 105 | # https://github.com/astral-sh/ruff/issues/18042 +111 106 | print("a,b".rsplit(",")) + +SIM905.py:111:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +110 | # https://github.com/astral-sh/ruff/issues/18042 +111 | print("a,b".rsplit(",")) + | ^^^^^^^^^^^^^^^^^ SIM905 +112 | print("a,b,c".rsplit(",", 1)) + | + = help: Replace with list literal + +ℹ Safe fix +108 108 | """.split() +109 109 | +110 110 | # https://github.com/astral-sh/ruff/issues/18042 +111 |-print("a,b".rsplit(",")) + 111 |+print(["a", "b"]) +112 112 | print("a,b,c".rsplit(",", 1)) +113 113 | +114 114 | # https://github.com/astral-sh/ruff/issues/18069 + +SIM905.py:112:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +110 | # https://github.com/astral-sh/ruff/issues/18042 +111 | print("a,b".rsplit(",")) +112 | print("a,b,c".rsplit(",", 1)) + | ^^^^^^^^^^^^^^^^^^^^^^ SIM905 +113 | +114 | # https://github.com/astral-sh/ruff/issues/18069 + | + = help: Replace with list literal + +ℹ Safe fix +109 109 | +110 110 | # https://github.com/astral-sh/ruff/issues/18042 +111 111 | print("a,b".rsplit(",")) +112 |-print("a,b,c".rsplit(",", 1)) + 112 |+print(["a,b", "c"]) +113 113 | +114 114 | # https://github.com/astral-sh/ruff/issues/18069 +115 115 | + +SIM905.py:116:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +114 | # https://github.com/astral-sh/ruff/issues/18069 +115 | +116 | print("".split(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^ SIM905 +117 | print("".split(sep=None, maxsplit=0)) +118 | print(" ".split(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +113 113 | +114 114 | # https://github.com/astral-sh/ruff/issues/18069 +115 115 | +116 |-print("".split(maxsplit=0)) + 116 |+print([]) +117 117 | print("".split(sep=None, maxsplit=0)) +118 118 | print(" ".split(maxsplit=0)) +119 119 | print(" ".split(sep=None, maxsplit=0)) + +SIM905.py:117:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +116 | print("".split(maxsplit=0)) +117 | print("".split(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +118 | print(" ".split(maxsplit=0)) +119 | print(" ".split(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +114 114 | # https://github.com/astral-sh/ruff/issues/18069 +115 115 | +116 116 | print("".split(maxsplit=0)) +117 |-print("".split(sep=None, maxsplit=0)) + 117 |+print([]) +118 118 | print(" ".split(maxsplit=0)) +119 119 | print(" ".split(sep=None, maxsplit=0)) +120 120 | print(" x ".split(maxsplit=0)) + +SIM905.py:118:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +116 | print("".split(maxsplit=0)) +117 | print("".split(sep=None, maxsplit=0)) +118 | print(" ".split(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^ SIM905 +119 | print(" ".split(sep=None, maxsplit=0)) +120 | print(" x ".split(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +115 115 | +116 116 | print("".split(maxsplit=0)) +117 117 | print("".split(sep=None, maxsplit=0)) +118 |-print(" ".split(maxsplit=0)) + 118 |+print([]) +119 119 | print(" ".split(sep=None, maxsplit=0)) +120 120 | print(" x ".split(maxsplit=0)) +121 121 | print(" x ".split(sep=None, maxsplit=0)) + +SIM905.py:119:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +117 | print("".split(sep=None, maxsplit=0)) +118 | print(" ".split(maxsplit=0)) +119 | print(" ".split(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +120 | print(" x ".split(maxsplit=0)) +121 | print(" x ".split(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +116 116 | print("".split(maxsplit=0)) +117 117 | print("".split(sep=None, maxsplit=0)) +118 118 | print(" ".split(maxsplit=0)) +119 |-print(" ".split(sep=None, maxsplit=0)) + 119 |+print([]) +120 120 | print(" x ".split(maxsplit=0)) +121 121 | print(" x ".split(sep=None, maxsplit=0)) +122 122 | print(" x ".split(maxsplit=0)) + +SIM905.py:120:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +118 | print(" ".split(maxsplit=0)) +119 | print(" ".split(sep=None, maxsplit=0)) +120 | print(" x ".split(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +121 | print(" x ".split(sep=None, maxsplit=0)) +122 | print(" x ".split(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +117 117 | print("".split(sep=None, maxsplit=0)) +118 118 | print(" ".split(maxsplit=0)) +119 119 | print(" ".split(sep=None, maxsplit=0)) +120 |-print(" x ".split(maxsplit=0)) + 120 |+print(["x "]) +121 121 | print(" x ".split(sep=None, maxsplit=0)) +122 122 | print(" x ".split(maxsplit=0)) +123 123 | print(" x ".split(sep=None, maxsplit=0)) + +SIM905.py:121:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +119 | print(" ".split(sep=None, maxsplit=0)) +120 | print(" x ".split(maxsplit=0)) +121 | print(" x ".split(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +122 | print(" x ".split(maxsplit=0)) +123 | print(" x ".split(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +118 118 | print(" ".split(maxsplit=0)) +119 119 | print(" ".split(sep=None, maxsplit=0)) +120 120 | print(" x ".split(maxsplit=0)) +121 |-print(" x ".split(sep=None, maxsplit=0)) + 121 |+print(["x "]) +122 122 | print(" x ".split(maxsplit=0)) +123 123 | print(" x ".split(sep=None, maxsplit=0)) +124 124 | print("".rsplit(maxsplit=0)) + +SIM905.py:122:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +120 | print(" x ".split(maxsplit=0)) +121 | print(" x ".split(sep=None, maxsplit=0)) +122 | print(" x ".split(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +123 | print(" x ".split(sep=None, maxsplit=0)) +124 | print("".rsplit(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +119 119 | print(" ".split(sep=None, maxsplit=0)) +120 120 | print(" x ".split(maxsplit=0)) +121 121 | print(" x ".split(sep=None, maxsplit=0)) +122 |-print(" x ".split(maxsplit=0)) + 122 |+print(["x "]) +123 123 | print(" x ".split(sep=None, maxsplit=0)) +124 124 | print("".rsplit(maxsplit=0)) +125 125 | print("".rsplit(sep=None, maxsplit=0)) + +SIM905.py:123:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +121 | print(" x ".split(sep=None, maxsplit=0)) +122 | print(" x ".split(maxsplit=0)) +123 | print(" x ".split(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +124 | print("".rsplit(maxsplit=0)) +125 | print("".rsplit(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +120 120 | print(" x ".split(maxsplit=0)) +121 121 | print(" x ".split(sep=None, maxsplit=0)) +122 122 | print(" x ".split(maxsplit=0)) +123 |-print(" x ".split(sep=None, maxsplit=0)) + 123 |+print(["x "]) +124 124 | print("".rsplit(maxsplit=0)) +125 125 | print("".rsplit(sep=None, maxsplit=0)) +126 126 | print(" ".rsplit(maxsplit=0)) + +SIM905.py:124:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +122 | print(" x ".split(maxsplit=0)) +123 | print(" x ".split(sep=None, maxsplit=0)) +124 | print("".rsplit(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^ SIM905 +125 | print("".rsplit(sep=None, maxsplit=0)) +126 | print(" ".rsplit(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +121 121 | print(" x ".split(sep=None, maxsplit=0)) +122 122 | print(" x ".split(maxsplit=0)) +123 123 | print(" x ".split(sep=None, maxsplit=0)) +124 |-print("".rsplit(maxsplit=0)) + 124 |+print([]) +125 125 | print("".rsplit(sep=None, maxsplit=0)) +126 126 | print(" ".rsplit(maxsplit=0)) +127 127 | print(" ".rsplit(sep=None, maxsplit=0)) + +SIM905.py:125:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +123 | print(" x ".split(sep=None, maxsplit=0)) +124 | print("".rsplit(maxsplit=0)) +125 | print("".rsplit(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +126 | print(" ".rsplit(maxsplit=0)) +127 | print(" ".rsplit(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +122 122 | print(" x ".split(maxsplit=0)) +123 123 | print(" x ".split(sep=None, maxsplit=0)) +124 124 | print("".rsplit(maxsplit=0)) +125 |-print("".rsplit(sep=None, maxsplit=0)) + 125 |+print([]) +126 126 | print(" ".rsplit(maxsplit=0)) +127 127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 128 | print(" x ".rsplit(maxsplit=0)) + +SIM905.py:126:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +124 | print("".rsplit(maxsplit=0)) +125 | print("".rsplit(sep=None, maxsplit=0)) +126 | print(" ".rsplit(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^ SIM905 +127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 | print(" x ".rsplit(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +123 123 | print(" x ".split(sep=None, maxsplit=0)) +124 124 | print("".rsplit(maxsplit=0)) +125 125 | print("".rsplit(sep=None, maxsplit=0)) +126 |-print(" ".rsplit(maxsplit=0)) + 126 |+print([]) +127 127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 128 | print(" x ".rsplit(maxsplit=0)) +129 129 | print(" x ".rsplit(maxsplit=0)) + +SIM905.py:127:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +125 | print("".rsplit(sep=None, maxsplit=0)) +126 | print(" ".rsplit(maxsplit=0)) +127 | print(" ".rsplit(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +128 | print(" x ".rsplit(maxsplit=0)) +129 | print(" x ".rsplit(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +124 124 | print("".rsplit(maxsplit=0)) +125 125 | print("".rsplit(sep=None, maxsplit=0)) +126 126 | print(" ".rsplit(maxsplit=0)) +127 |-print(" ".rsplit(sep=None, maxsplit=0)) + 127 |+print([]) +128 128 | print(" x ".rsplit(maxsplit=0)) +129 129 | print(" x ".rsplit(maxsplit=0)) +130 130 | print(" x ".rsplit(sep=None, maxsplit=0)) + +SIM905.py:128:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +126 | print(" ".rsplit(maxsplit=0)) +127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 | print(" x ".rsplit(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +129 | print(" x ".rsplit(maxsplit=0)) +130 | print(" x ".rsplit(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +125 125 | print("".rsplit(sep=None, maxsplit=0)) +126 126 | print(" ".rsplit(maxsplit=0)) +127 127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 |-print(" x ".rsplit(maxsplit=0)) + 128 |+print([" x"]) +129 129 | print(" x ".rsplit(maxsplit=0)) +130 130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 131 | print(" x ".rsplit(maxsplit=0)) + +SIM905.py:129:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 | print(" x ".rsplit(maxsplit=0)) +129 | print(" x ".rsplit(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 | print(" x ".rsplit(maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +126 126 | print(" ".rsplit(maxsplit=0)) +127 127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 128 | print(" x ".rsplit(maxsplit=0)) +129 |-print(" x ".rsplit(maxsplit=0)) + 129 |+print([" x"]) +130 130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 131 | print(" x ".rsplit(maxsplit=0)) +132 132 | print(" x ".rsplit(sep=None, maxsplit=0)) + +SIM905.py:130:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +128 | print(" x ".rsplit(maxsplit=0)) +129 | print(" x ".rsplit(maxsplit=0)) +130 | print(" x ".rsplit(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +131 | print(" x ".rsplit(maxsplit=0)) +132 | print(" x ".rsplit(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +127 127 | print(" ".rsplit(sep=None, maxsplit=0)) +128 128 | print(" x ".rsplit(maxsplit=0)) +129 129 | print(" x ".rsplit(maxsplit=0)) +130 |-print(" x ".rsplit(sep=None, maxsplit=0)) + 130 |+print([" x"]) +131 131 | print(" x ".rsplit(maxsplit=0)) +132 132 | print(" x ".rsplit(sep=None, maxsplit=0)) + +SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +129 | print(" x ".rsplit(maxsplit=0)) +130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 | print(" x ".rsplit(maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 +132 | print(" x ".rsplit(sep=None, maxsplit=0)) + | + = help: Replace with list literal + +ℹ Safe fix +128 128 | print(" x ".rsplit(maxsplit=0)) +129 129 | print(" x ".rsplit(maxsplit=0)) +130 130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 |-print(" x ".rsplit(maxsplit=0)) + 131 |+print([" x"]) +132 132 | print(" x ".rsplit(sep=None, maxsplit=0)) + +SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split` + | +130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 | print(" x ".rsplit(maxsplit=0)) +132 | print(" x ".rsplit(sep=None, maxsplit=0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905 + | + = help: Replace with list literal + +ℹ Safe fix +129 129 | print(" x ".rsplit(maxsplit=0)) +130 130 | print(" x ".rsplit(sep=None, maxsplit=0)) +131 131 | print(" x ".rsplit(maxsplit=0)) +132 |-print(" x ".rsplit(sep=None, maxsplit=0)) + 132 |+print([" x"]) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM910_SIM910.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM910_SIM910.py.snap index 262e131eecd44..8bbbdc5fd567f 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM910_SIM910.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM910_SIM910.py.snap @@ -160,3 +160,19 @@ SIM910.py:43:9: SIM910 [*] Use `some_dict.get('a')` instead of `some_dict.get('a 44 44 | 45 45 | # OK 46 46 | def foo(some_other: object): + +SIM910.py:57:11: SIM910 [*] Use `dict.get("Cat")` instead of `dict.get("Cat", None)` + | +55 | def foo(): +56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11} +57 | age = dict.get("Cat", None) + | ^^^^^^^^^^^^^^^^^^^^^ SIM910 + | + = help: Replace `dict.get("Cat", None)` with `dict.get("Cat")` + +ℹ Safe fix +54 54 | # https://github.com/astral-sh/ruff/issues/18777 +55 55 | def foo(): +56 56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11} +57 |- age = dict.get("Cat", None) + 57 |+ age = dict.get("Cat") diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap new file mode 100644 index 0000000000000..261332a8be5ef --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM911_SIM911.py.snap @@ -0,0 +1,95 @@ +--- +source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs +--- +SIM911.py:2:17: SIM911 [*] Use `d.items()` instead of `zip(d.keys(), d.values())` + | +1 | def foo(d: dict[str, str]) -> None: +2 | for k, v in zip(d.keys(), d.values()): # SIM911 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM911 +3 | ... + | + = help: Replace `zip(d.keys(), d.values())` with `d.items()` + +ℹ Safe fix +1 1 | def foo(d: dict[str, str]) -> None: +2 |- for k, v in zip(d.keys(), d.values()): # SIM911 + 2 |+ for k, v in d.items(): # SIM911 +3 3 | ... +4 4 | +5 5 | for k, v in zip(d.keys(), d.values(), strict=True): # SIM911 + +SIM911.py:5:17: SIM911 [*] Use `d.items()` instead of `zip(d.keys(), d.values(), strict=True)` + | +3 | ... +4 | +5 | for k, v in zip(d.keys(), d.values(), strict=True): # SIM911 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM911 +6 | ... + | + = help: Replace `zip(d.keys(), d.values(), strict=True)` with `d.items()` + +ℹ Safe fix +2 2 | for k, v in zip(d.keys(), d.values()): # SIM911 +3 3 | ... +4 4 | +5 |- for k, v in zip(d.keys(), d.values(), strict=True): # SIM911 + 5 |+ for k, v in d.items(): # SIM911 +6 6 | ... +7 7 | +8 8 | for k, v in zip(d.keys(), d.values(), struct=True): # OK + +SIM911.py:20:13: SIM911 [*] Use `d2.items()` instead of `zip(d2.keys(), d2.values())` + | +18 | ... +19 | +20 | for k, v in zip(d2.keys(), d2.values()): # SIM911 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM911 +21 | ... + | + = help: Replace `zip(d2.keys(), d2.values())` with `d2.items()` + +ℹ Safe fix +17 17 | for k, v in zip(d1.items(), d2.values()): # OK +18 18 | ... +19 19 | +20 |-for k, v in zip(d2.keys(), d2.values()): # SIM911 + 20 |+for k, v in d2.items(): # SIM911 +21 21 | ... +22 22 | +23 23 | items = zip(x.keys(), x.values()) # OK + +SIM911.py:30:27: SIM911 [*] Use `dict.items()` instead of `zip(dict.keys(), dict.values())` + | +28 | def foo(): +29 | dict = {} +30 | for country, stars in zip(dict.keys(), dict.values()): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM911 +31 | ... + | + = help: Replace `zip(dict.keys(), dict.values())` with `dict.items()` + +ℹ Safe fix +27 27 | # https://github.com/astral-sh/ruff/issues/18777 +28 28 | def foo(): +29 29 | dict = {} +30 |- for country, stars in zip(dict.keys(), dict.values()): + 30 |+ for country, stars in dict.items(): +31 31 | ... +32 32 | +33 33 | + +SIM911.py:36:22: SIM911 [*] Use ` flag_stars.items()` instead of `(zip)(flag_stars.keys(), flag_stars.values())` + | +34 | # https://github.com/astral-sh/ruff/issues/18776 +35 | flag_stars = {} +36 | for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM911 + | + = help: Replace `(zip)(flag_stars.keys(), flag_stars.values())` with ` flag_stars.items()` + +ℹ Safe fix +33 33 | +34 34 | # https://github.com/astral-sh/ruff/issues/18776 +35 35 | flag_stars = {} +36 |-for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):... + 36 |+for country, stars in flag_stars.items():... diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM108_SIM108.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM108_SIM108.py.snap deleted file mode 100644 index b4d70317ad555..0000000000000 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM108_SIM108.py.snap +++ /dev/null @@ -1,382 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs ---- -SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `if`-`else`-block - | -1 | # SIM108 -2 | / if a: -3 | | b = c -4 | | else: -5 | | b = d - | |_________^ SIM108 -6 | -7 | # OK - | - = help: Replace `if`-`else`-block with `b = c if a else d` - -ℹ Unsafe fix -1 1 | # SIM108 -2 |-if a: -3 |- b = c -4 |-else: -5 |- b = d - 2 |+b = c if a else d -6 3 | -7 4 | # OK -8 5 | b = c if a else d - -SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block - | -28 | pass -29 | else: -30 | / if a: -31 | | b = 1 -32 | | else: -33 | | b = 2 - | |_____________^ SIM108 - | - = help: Replace `if`-`else`-block with `b = 1 if a else 2` - -ℹ Unsafe fix -27 27 | if True: -28 28 | pass -29 29 | else: -30 |- if a: -31 |- b = 1 -32 |- else: -33 |- b = 2 - 30 |+ b = 1 if a else 2 -34 31 | -35 32 | -36 33 | import sys - -SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block - | -57 | # SIM108 (without fix due to comments) -58 | / if x > 0: -59 | | # test test -60 | | abc = x -61 | | else: -62 | | # test test test -63 | | abc = -x - | |____________^ SIM108 - | - = help: Replace `if`-`else`-block with `abc = x if x > 0 else -x` - -SIM108.py:82:1: SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` instead of `if`-`else`-block - | -81 | # SIM108 -82 | / if a: -83 | | b = "cccccccccccccccccccccccccccccccccß" -84 | | else: -85 | | b = "ddddddddddddddddddddddddddddddddd💣" - | |_____________________________________________^ SIM108 - | - = help: Replace `if`-`else`-block with `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` - -ℹ Unsafe fix -79 79 | -80 80 | -81 81 | # SIM108 -82 |-if a: -83 |- b = "cccccccccccccccccccccccccccccccccß" -84 |-else: -85 |- b = "ddddddddddddddddddddddddddddddddd💣" - 82 |+b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣" -86 83 | -87 84 | -88 85 | # OK (too long) - -SIM108.py:105:1: SIM108 Use ternary operator `exitcode = 0 if True else 1` instead of `if`-`else`-block - | -104 | # SIM108 (without fix due to trailing comment) -105 | / if True: -106 | | exitcode = 0 -107 | | else: -108 | | exitcode = 1 # Trailing comment - | |________________^ SIM108 - | - = help: Replace `if`-`else`-block with `exitcode = 0 if True else 1` - -SIM108.py:112:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block - | -111 | # SIM108 -112 | / if True: x = 3 # Foo -113 | | else: x = 5 - | |___________^ SIM108 - | - = help: Replace `if`-`else`-block with `x = 3 if True else 5` - -SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block - | -116 | # SIM108 -117 | / if True: # Foo -118 | | x = 3 -119 | | else: -120 | | x = 5 - | |_________^ SIM108 - | - = help: Replace `if`-`else`-block with `x = 3 if True else 5` - -SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block - | -139 | # SIM108 - should suggest -140 | # z = cond or other_cond -141 | / if cond: -142 | | z = cond -143 | | else: -144 | | z = other_cond - | |__________________^ SIM108 -145 | -146 | # SIM108 - should suggest - | - = help: Replace `if`-`else`-block with `z = cond or other_cond` - -ℹ Unsafe fix -138 138 | -139 139 | # SIM108 - should suggest -140 140 | # z = cond or other_cond -141 |-if cond: -142 |- z = cond -143 |-else: -144 |- z = other_cond - 141 |+z = cond or other_cond -145 142 | -146 143 | # SIM108 - should suggest -147 144 | # z = cond and other_cond - -SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block - | -146 | # SIM108 - should suggest -147 | # z = cond and other_cond -148 | / if not cond: -149 | | z = cond -150 | | else: -151 | | z = other_cond - | |__________________^ SIM108 -152 | -153 | # SIM108 - should suggest - | - = help: Replace `if`-`else`-block with `z = cond and other_cond` - -ℹ Unsafe fix -145 145 | -146 146 | # SIM108 - should suggest -147 147 | # z = cond and other_cond -148 |-if not cond: -149 |- z = cond -150 |-else: -151 |- z = other_cond - 148 |+z = cond and other_cond -152 149 | -153 150 | # SIM108 - should suggest -154 151 | # z = not cond and other_cond - -SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block - | -153 | # SIM108 - should suggest -154 | # z = not cond and other_cond -155 | / if cond: -156 | | z = not cond -157 | | else: -158 | | z = other_cond - | |__________________^ SIM108 -159 | -160 | # SIM108 does not suggest - | - = help: Replace `if`-`else`-block with `z = not cond and other_cond` - -ℹ Unsafe fix -152 152 | -153 153 | # SIM108 - should suggest -154 154 | # z = not cond and other_cond -155 |-if cond: -156 |- z = not cond -157 |-else: -158 |- z = other_cond - 155 |+z = not cond and other_cond -159 156 | -160 157 | # SIM108 does not suggest -161 158 | # a binary option in these cases, - -SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` instead of `if`-`else`-block - | -165 | # (Of course, these specific expressions -166 | # should be simplified for other reasons...) -167 | / if True: -168 | | z = 1 -169 | | else: -170 | | z = other - | |_____________^ SIM108 -171 | -172 | if False: - | - = help: Replace `if`-`else`-block with `z = 1 if True else other` - -ℹ Unsafe fix -164 164 | # so, e.g. `True == 1`. -165 165 | # (Of course, these specific expressions -166 166 | # should be simplified for other reasons...) -167 |-if True: -168 |- z = 1 -169 |-else: -170 |- z = other - 167 |+z = 1 if True else other -171 168 | -172 169 | if False: -173 170 | z = 1 - -SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block - | -170 | z = other -171 | -172 | / if False: -173 | | z = 1 -174 | | else: -175 | | z = other - | |_____________^ SIM108 -176 | -177 | if 1: - | - = help: Replace `if`-`else`-block with `z = 1 if False else other` - -ℹ Unsafe fix -169 169 | else: -170 170 | z = other -171 171 | -172 |-if False: -173 |- z = 1 -174 |-else: -175 |- z = other - 172 |+z = 1 if False else other -176 173 | -177 174 | if 1: -178 175 | z = True - -SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block - | -175 | z = other -176 | -177 | / if 1: -178 | | z = True -179 | | else: -180 | | z = other - | |_____________^ SIM108 -181 | -182 | # SIM108 does not suggest a binary option in this - | - = help: Replace `if`-`else`-block with `z = True if 1 else other` - -ℹ Unsafe fix -174 174 | else: -175 175 | z = other -176 176 | -177 |-if 1: -178 |- z = True -179 |-else: -180 |- z = other - 177 |+z = True if 1 else other -181 178 | -182 179 | # SIM108 does not suggest a binary option in this -183 180 | # case, since we'd be reducing the number of calls - -SIM108.py:185:1: SIM108 [*] Use ternary operator `z = foo() if foo() else other` instead of `if`-`else`-block - | -183 | # case, since we'd be reducing the number of calls -184 | # from Two to one. -185 | / if foo(): -186 | | z = foo() -187 | | else: -188 | | z = other - | |_____________^ SIM108 -189 | -190 | # SIM108 does not suggest a binary option in this - | - = help: Replace `if`-`else`-block with `z = foo() if foo() else other` - -ℹ Unsafe fix -182 182 | # SIM108 does not suggest a binary option in this -183 183 | # case, since we'd be reducing the number of calls -184 184 | # from Two to one. -185 |-if foo(): -186 |- z = foo() -187 |-else: -188 |- z = other - 185 |+z = foo() if foo() else other -189 186 | -190 187 | # SIM108 does not suggest a binary option in this -191 188 | # case, since we'd be reducing the number of calls - -SIM108.py:193:1: SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of `if`-`else`-block - | -191 | # case, since we'd be reducing the number of calls -192 | # from Two to one. -193 | / if foo(): -194 | | z = not foo() -195 | | else: -196 | | z = other - | |_____________^ SIM108 - | - = help: Replace `if`-`else`-block with `z = not foo() if foo() else other` - -ℹ Unsafe fix -190 190 | # SIM108 does not suggest a binary option in this -191 191 | # case, since we'd be reducing the number of calls -192 192 | # from Two to one. -193 |-if foo(): -194 |- z = not foo() -195 |-else: -196 |- z = other - 193 |+z = not foo() if foo() else other -197 194 | -198 195 | -199 196 | # These two cases double as tests for f-string quote preservation. The first - -SIM108.py:202:1: SIM108 [*] Use ternary operator `var = "str" if cond else f"{first}-{second}"` instead of `if`-`else`-block - | -200 | # f-string should preserve its double quotes, and the second should preserve -201 | # single quotes -202 | / if cond: -203 | | var = "str" -204 | | else: -205 | | var = f"{first}-{second}" - | |_____________________________^ SIM108 -206 | -207 | if cond: - | - = help: Replace `if`-`else`-block with `var = "str" if cond else f"{first}-{second}"` - -ℹ Unsafe fix -199 199 | # These two cases double as tests for f-string quote preservation. The first -200 200 | # f-string should preserve its double quotes, and the second should preserve -201 201 | # single quotes -202 |-if cond: -203 |- var = "str" -204 |-else: -205 |- var = f"{first}-{second}" - 202 |+var = "str" if cond else f"{first}-{second}" -206 203 | -207 204 | if cond: -208 205 | var = "str" - -SIM108.py:207:1: SIM108 [*] Use ternary operator `var = "str" if cond else f'{first}-{second}'` instead of `if`-`else`-block - | -205 | var = f"{first}-{second}" -206 | -207 | / if cond: -208 | | var = "str" -209 | | else: -210 | | var = f'{first}-{second}' - | |_____________________________^ SIM108 - | - = help: Replace `if`-`else`-block with `var = "str" if cond else f'{first}-{second}'` - -ℹ Unsafe fix -204 204 | else: -205 205 | var = f"{first}-{second}" -206 206 | -207 |-if cond: -208 |- var = "str" -209 |-else: -210 |- var = f'{first}-{second}' - 207 |+var = "str" if cond else f'{first}-{second}' diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM117_SIM117.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM117_SIM117.py.snap new file mode 100644 index 0000000000000..4928d2db7e51e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__preview__SIM117_SIM117.py.snap @@ -0,0 +1,339 @@ +--- +source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs +--- +SIM117.py:2:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +1 | # SIM117 +2 | / with A() as a: +3 | | with B() as b: + | |__________________^ SIM117 +4 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +1 1 | # SIM117 +2 |-with A() as a: +3 |- with B() as b: +4 |- print("hello") + 2 |+with A() as a, B() as b: + 3 |+ print("hello") +5 4 | +6 5 | # SIM117 +7 6 | with A(): + +SIM117.py:7:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | + 6 | # SIM117 + 7 | / with A(): + 8 | | with B(): + | |_____________^ SIM117 + 9 | with C(): +10 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +4 4 | print("hello") +5 5 | +6 6 | # SIM117 +7 |-with A(): +8 |- with B(): +9 |- with C(): +10 |- print("hello") + 7 |+with A(), B(): + 8 |+ with C(): + 9 |+ print("hello") +11 10 | +12 11 | # SIM117 +13 12 | with A() as a: + +SIM117.py:13:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements + | +12 | # SIM117 +13 | / with A() as a: +14 | | # Unfixable due to placement of this comment. +15 | | with B() as b: + | |__________________^ SIM117 +16 | print("hello") + | + = help: Combine `with` statements + +SIM117.py:19:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +18 | # SIM117 +19 | / with A() as a: +20 | | with B() as b: + | |__________________^ SIM117 +21 | # Fixable due to placement of this comment. +22 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +16 16 | print("hello") +17 17 | +18 18 | # SIM117 +19 |-with A() as a: +20 |- with B() as b: +21 |- # Fixable due to placement of this comment. +22 |- print("hello") + 19 |+with A() as a, B() as b: + 20 |+ # Fixable due to placement of this comment. + 21 |+ print("hello") +23 22 | +24 23 | # OK +25 24 | with A() as a: + +SIM117.py:47:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +46 | # SIM117 +47 | / async with A() as a: +48 | | async with B() as b: + | |________________________^ SIM117 +49 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +44 44 | print("hello") +45 45 | +46 46 | # SIM117 +47 |-async with A() as a: +48 |- async with B() as b: +49 |- print("hello") + 47 |+async with A() as a, B() as b: + 48 |+ print("hello") +50 49 | +51 50 | while True: +52 51 | # SIM117 + +SIM117.py:53:5: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +51 | while True: +52 | # SIM117 +53 | / with A() as a: +54 | | with B() as b: + | |______________________^ SIM117 +55 | """this +56 | is valid""" + | + = help: Combine `with` statements + +ℹ Safe fix +50 50 | +51 51 | while True: +52 52 | # SIM117 +53 |- with A() as a: +54 |- with B() as b: +55 |- """this + 53 |+ with A() as a, B() as b: + 54 |+ """this +56 55 | is valid""" +57 56 | +58 |- """the indentation on + 57 |+ """the indentation on +59 58 | this line is significant""" +60 59 | +61 |- "this is" \ + 60 |+ "this is" \ +62 61 | "allowed too" +63 62 | +64 |- ("so is" + 63 |+ ("so is" +65 64 | "this for some reason") +66 65 | +67 66 | # SIM117 + +SIM117.py:68:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +67 | # SIM117 +68 | / with ( +69 | | A() as a, +70 | | B() as b, +71 | | ): +72 | | with C() as c: + | |__________________^ SIM117 +73 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +67 67 | # SIM117 +68 68 | with ( +69 69 | A() as a, +70 |- B() as b, + 70 |+ B() as b,C() as c +71 71 | ): +72 |- with C() as c: +73 |- print("hello") + 72 |+ print("hello") +74 73 | +75 74 | # SIM117 +76 75 | with A() as a: + +SIM117.py:76:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +75 | # SIM117 +76 | / with A() as a: +77 | | with ( +78 | | B() as b, +79 | | C() as c, +80 | | ): + | |______^ SIM117 +81 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +73 73 | print("hello") +74 74 | +75 75 | # SIM117 +76 |-with A() as a: +77 |- with ( +78 |- B() as b, +79 |- C() as c, +80 |- ): +81 |- print("hello") + 76 |+with ( + 77 |+ A() as a, B() as b, + 78 |+ C() as c, + 79 |+): + 80 |+ print("hello") +82 81 | +83 82 | # SIM117 +84 83 | with ( + +SIM117.py:84:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +83 | # SIM117 +84 | / with ( +85 | | A() as a, +86 | | B() as b, +87 | | ): +88 | | with ( +89 | | C() as c, +90 | | D() as d, +91 | | ): + | |______^ SIM117 +92 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +83 83 | # SIM117 +84 84 | with ( +85 85 | A() as a, +86 |- B() as b, + 86 |+ B() as b,C() as c, + 87 |+ D() as d, +87 88 | ): +88 |- with ( +89 |- C() as c, +90 |- D() as d, +91 |- ): +92 |- print("hello") + 89 |+ print("hello") +93 90 | +94 91 | # SIM117 (auto-fixable) +95 92 | with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a: + +SIM117.py:95:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +94 | # SIM117 (auto-fixable) +95 | / with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a: +96 | | with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b: + | |__________________________________________________^ SIM117 +97 | print("hello") + | + = help: Combine `with` statements + +ℹ Safe fix +92 92 | print("hello") +93 93 | +94 94 | # SIM117 (auto-fixable) +95 |-with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a: +96 |- with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b: +97 |- print("hello") + 95 |+with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a, B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b: + 96 |+ print("hello") +98 97 | +99 98 | # SIM117 (not auto-fixable too long) +100 99 | with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a: + +SIM117.py:100:1: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements + | + 99 | # SIM117 (not auto-fixable too long) +100 | / with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a: +101 | | with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b: + | |__________________________________________________^ SIM117 +102 | print("hello") + | + = help: Combine `with` statements + +SIM117.py:106:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements + | +104 | # From issue #3025. +105 | async def main(): +106 | / async with A() as a: # SIM117. +107 | | async with B() as b: + | |____________________________^ SIM117 +108 | print("async-inside!") + | + = help: Combine `with` statements + +SIM117.py:126:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +125 | # SIM117 +126 | / with A() as a: +127 | | with B() as b: + | |__________________^ SIM117 +128 | type ListOrSet[T] = list[T] | set[T] + | + = help: Combine `with` statements + +ℹ Safe fix +123 123 | f(b2, c2, d2) +124 124 | +125 125 | # SIM117 +126 |-with A() as a: +127 |- with B() as b: +128 |- type ListOrSet[T] = list[T] | set[T] + 126 |+with A() as a, B() as b: + 127 |+ type ListOrSet[T] = list[T] | set[T] +129 128 | +130 |- class ClassA[T: str]: +131 |- def method1(self) -> T: +132 |- ... + 129 |+ class ClassA[T: str]: + 130 |+ def method1(self) -> T: + 131 |+ ... +133 132 | +134 |- f" something { my_dict["key"] } something else " + 133 |+ f" something { my_dict["key"] } something else " +135 134 | +136 |- f"foo {f"bar {x}"} baz" + 135 |+ f"foo {f"bar {x}"} baz" +137 136 | +138 137 | # Allow cascading for some statements. +139 138 | import anyio + +SIM117.py:163:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +162 | # Do not suppress combination, if a context manager is already combined with another. +163 | / async with asyncio.timeout(1), A(): +164 | | async with B(): + | |___________________^ SIM117 +165 | pass + | + = help: Combine `with` statements + +ℹ Safe fix +160 160 | pass +161 161 | +162 162 | # Do not suppress combination, if a context manager is already combined with another. +163 |-async with asyncio.timeout(1), A(): +164 |- async with B(): +165 |- pass + 163 |+async with asyncio.timeout(1), A(), B(): + 164 |+ pass diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_slots/helpers.rs similarity index 100% rename from crates/ruff_linter/src/rules/flake8_slots/rules/helpers.rs rename to crates/ruff_linter/src/rules/flake8_slots/helpers.rs diff --git a/crates/ruff_linter/src/rules/flake8_slots/mod.rs b/crates/ruff_linter/src/rules/flake8_slots/mod.rs index 422ff4f7be2f8..99bd9daa8b35b 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-slots](https://pypi.org/project/flake8-slots/). +mod helpers; pub(crate) mod rules; #[cfg(test)] @@ -10,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::NoSlotsInStrSubclass, Path::new("SLOT000.py"))] #[test_case(Rule::NoSlotsInTupleSubclass, Path::new("SLOT001.py"))] @@ -21,7 +22,7 @@ mod tests { Path::new("flake8_slots").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/mod.rs index abbed6b9e4e12..bfea7a6cf17a8 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/mod.rs @@ -2,7 +2,6 @@ pub(crate) use no_slots_in_namedtuple_subclass::*; pub(crate) use no_slots_in_str_subclass::*; pub(crate) use no_slots_in_tuple_subclass::*; -mod helpers; mod no_slots_in_namedtuple_subclass; mod no_slots_in_str_subclass; mod no_slots_in_tuple_subclass; diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index f02e9e5b84b6c..37d2084c96531 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -1,12 +1,12 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, identifier::Identifier, Arguments, Expr, Stmt, StmtClassDef}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Arguments, Expr, Stmt, StmtClassDef, identifier::Identifier}; use ruff_python_semantic::SemanticModel; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::flake8_slots::rules::helpers::has_slots; +use crate::rules::flake8_slots::helpers::has_slots; /// ## What it does /// Checks for subclasses of `collections.namedtuple` or `typing.NamedTuple` @@ -88,10 +88,10 @@ pub(crate) fn no_slots_in_namedtuple_subclass( if let Some(namedtuple_kind) = namedtuple_base(bases, checker.semantic()) { if !has_slots(&class.body) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NoSlotsInNamedtupleSubclass(namedtuple_kind), stmt.identifier(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 7df55a4b009c1..198071aaa4b57 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{Arguments, Expr, Stmt, StmtClassDef}; -use ruff_python_semantic::{analyze::class::is_enumeration, SemanticModel}; +use ruff_python_semantic::{SemanticModel, analyze::class::is_enumeration}; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::flake8_slots::rules::helpers::has_slots; +use crate::rules::flake8_slots::helpers::has_slots; /// ## What it does /// Checks for subclasses of `str` that lack a `__slots__` definition. @@ -73,7 +73,7 @@ pub(crate) fn no_slots_in_str_subclass(checker: &Checker, stmt: &Stmt, class: &S return; } - checker.report_diagnostic(Diagnostic::new(NoSlotsInStrSubclass, stmt.identifier())); + checker.report_diagnostic(NoSlotsInStrSubclass, stmt.identifier()); } /// Return `true` if the class is a subclass of `str`. diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index 639cdc128961f..dd9cea0ad34ab 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{Arguments, Stmt, StmtClassDef}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::identifier::Identifier; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::flake8_slots::rules::helpers::has_slots; +use crate::rules::flake8_slots::helpers::has_slots; /// ## What it does /// Checks for subclasses of `tuple` that lack a `__slots__` definition. @@ -66,7 +66,7 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &Checker, stmt: &Stmt, class: semantic.match_builtin_expr(base, "tuple") || semantic.match_typing_expr(base, "Tuple") }) { if !has_slots(&class.body) { - checker.report_diagnostic(Diagnostic::new(NoSlotsInTupleSubclass, stmt.identifier())); + checker.report_diagnostic(NoSlotsInTupleSubclass, stmt.identifier()); } } } diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/mod.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/mod.rs index c037ae78842ef..b5b06f7d1abb4 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/mod.rs @@ -10,7 +10,7 @@ mod tests { use anyhow::Result; use rustc_hash::FxHashMap; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::rules::flake8_tidy_imports; use crate::rules::flake8_tidy_imports::settings::{ApiBan, Strictness}; @@ -42,7 +42,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::BannedApi]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -72,7 +72,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::BannedApi]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -88,7 +88,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::RelativeImports]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -104,7 +104,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::RelativeImports]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -121,7 +121,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::RelativeImports]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -140,7 +140,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::BannedModuleLevelImports]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs index f92c0c684f971..dc352e3e4ecb9 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_tidy_imports::matchers::NameMatchPolicy; @@ -40,23 +40,23 @@ impl Violation for BannedApi { /// TID251 pub(crate) fn banned_api(checker: &Checker, policy: &NameMatchPolicy, node: &T) { - let banned_api = &checker.settings.flake8_tidy_imports.banned_api; + let banned_api = &checker.settings().flake8_tidy_imports.banned_api; if let Some(banned_module) = policy.find(banned_api.keys().map(AsRef::as_ref)) { if let Some(reason) = banned_api.get(&banned_module) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BannedApi { name: banned_module, message: reason.msg.to_string(), }, node.range(), - )); + ); } } } /// TID251 pub(crate) fn banned_attribute_access(checker: &Checker, expr: &Expr) { - let banned_api = &checker.settings.flake8_tidy_imports.banned_api; + let banned_api = &checker.settings().flake8_tidy_imports.banned_api; if banned_api.is_empty() { return; } @@ -71,12 +71,12 @@ pub(crate) fn banned_attribute_access(checker: &Checker, expr: &Expr) { }) }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BannedApi { name: banned_path.to_string(), message: ban.msg.to_string(), }, expr.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs index 3b27c63e9c85f..6527d8d522dba 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_module_level_imports.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::resolve_imported_module_path; use ruff_python_ast::{Alias, AnyNodeRef, Stmt, StmtImport, StmtImportFrom}; use ruff_text_size::Ranged; use std::borrow::Cow; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_tidy_imports::matchers::{MatchName, MatchNameOrParent, NameMatchPolicy}; @@ -64,16 +64,16 @@ pub(crate) fn banned_module_level_imports(checker: &Checker, stmt: &Stmt) { for (policy, node) in &BannedModuleImportPolicies::new(stmt, checker) { if let Some(banned_module) = policy.find( checker - .settings + .settings() .flake8_tidy_imports .banned_module_level_imports(), ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BannedModuleLevelImports { name: banned_module, }, node.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs index 011678ee112a7..1e6ac5e4ca8a5 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/relative_imports.rs @@ -1,13 +1,13 @@ use ruff_python_ast::{self as ast, Identifier, Stmt}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::resolve_imported_module_path; use ruff_python_codegen::Generator; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::flake8_tidy_imports::settings::Strictness; @@ -101,6 +101,7 @@ fn fix_banned_relative_import( names: names.clone(), level: 0, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let content = generator.stmt(&node.into()); Some(Fix::unsafe_edit(Edit::range_replacement( @@ -117,20 +118,18 @@ pub(crate) fn banned_relative_import( module: Option<&str>, module_path: Option<&[String]>, strictness: Strictness, -) -> Option { +) { let strictness_level = match strictness { Strictness::All => 0, Strictness::Parents => 1, }; if level > strictness_level { - let mut diagnostic = Diagnostic::new(RelativeImports { strictness }, stmt.range()); + let mut diagnostic = + checker.report_diagnostic(RelativeImports { strictness }, stmt.range()); if let Some(fix) = fix_banned_relative_import(stmt, level, module, module_path, checker.generator()) { diagnostic.set_fix(fix); } - Some(diagnostic) - } else { - None } } diff --git a/crates/ruff_linter/src/rules/flake8_todos/mod.rs b/crates/ruff_linter/src/rules/flake8_todos/mod.rs index 7a4129a78b682..1fe42ae613dd9 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_todos/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod rules; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use anyhow::Result; @@ -10,7 +9,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::InvalidTodoTag, Path::new("TD001.py"))] #[test_case(Rule::MissingTodoAuthor, Path::new("TD002.py"))] @@ -20,12 +19,12 @@ mod tests { #[test_case(Rule::InvalidTodoCapitalization, Path::new("TD006.py"))] #[test_case(Rule::MissingSpaceAfterTodoColon, Path::new("TD007.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_todos").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs index 9b4698d36ac98..24125023a6930 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs @@ -2,13 +2,14 @@ use std::sync::LazyLock; use regex::RegexSet; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_text_size::{TextLen, TextRange, TextSize}; -use crate::directives::{TodoComment, TodoDirective, TodoDirectiveKind}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::directives::{TodoComment, TodoDirective, TodoDirectiveKind}; +use crate::{AlwaysFixableViolation, Edit, Fix, Violation}; /// ## What it does /// Checks that a TODO comment is labelled with "TODO". @@ -248,7 +249,7 @@ static ISSUE_LINK_TODO_LINE_REGEX_SET: LazyLock = LazyLock::new(|| { }); pub(crate) fn todos( - diagnostics: &mut Vec, + context: &LintContext, todo_comments: &[TodoComment], locator: &Locator, comment_ranges: &CommentRanges, @@ -267,8 +268,8 @@ pub(crate) fn todos( continue; } - directive_errors(diagnostics, directive); - static_errors(diagnostics, content, range, directive); + directive_errors(context, directive); + static_errors(context, content, range, directive); let mut has_issue_link = false; // VSCode recommended links on same line are ok: @@ -307,46 +308,44 @@ pub(crate) fn todos( if !has_issue_link { // TD003 - diagnostics.push(Diagnostic::new(MissingTodoLink, directive.range)); + context.report_diagnostic_if_enabled(MissingTodoLink, directive.range); } } } /// Check that the directive itself is valid. This function modifies `diagnostics` in-place. -fn directive_errors(diagnostics: &mut Vec, directive: &TodoDirective) { +fn directive_errors(context: &LintContext, directive: &TodoDirective) { if directive.content == "TODO" { return; } if directive.content.to_uppercase() == "TODO" { // TD006 - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( InvalidTodoCapitalization { tag: directive.content.to_string(), }, directive.range, - ); - - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - "TODO".to_string(), - directive.range, - ))); - - diagnostics.push(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + "TODO".to_string(), + directive.range, + ))); + } } else { // TD001 - diagnostics.push(Diagnostic::new( + context.report_diagnostic_if_enabled( InvalidTodoTag { tag: directive.content.to_string(), }, directive.range, - )); + ); } } /// Checks for "static" errors in the comment: missing colon, missing author, etc. -fn static_errors( - diagnostics: &mut Vec, +pub(crate) fn static_errors( + context: &LintContext, comment: &str, comment_range: TextRange, directive: &TodoDirective, @@ -367,13 +366,13 @@ fn static_errors( TextSize::try_from(end_index).unwrap() } else { // TD002 - diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range)); + context.report_diagnostic_if_enabled(MissingTodoAuthor, directive.range); TextSize::new(0) } } else { // TD002 - diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range)); + context.report_diagnostic_if_enabled(MissingTodoAuthor, directive.range); TextSize::new(0) }; @@ -382,18 +381,18 @@ fn static_errors( if let Some(after_colon) = after_author.strip_prefix(':') { if after_colon.is_empty() { // TD005 - diagnostics.push(Diagnostic::new(MissingTodoDescription, directive.range)); + context.report_diagnostic_if_enabled(MissingTodoDescription, directive.range); } else if !after_colon.starts_with(char::is_whitespace) { // TD007 - diagnostics.push(Diagnostic::new(MissingSpaceAfterTodoColon, directive.range)); + context.report_diagnostic_if_enabled(MissingSpaceAfterTodoColon, directive.range); } } else { // TD004 - diagnostics.push(Diagnostic::new(MissingTodoColon, directive.range)); + context.report_diagnostic_if_enabled(MissingTodoColon, directive.range); if after_author.is_empty() { // TD005 - diagnostics.push(Diagnostic::new(MissingTodoDescription, directive.range)); + context.report_diagnostic_if_enabled(MissingTodoDescription, directive.range); } } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index 259342e2b9a48..21da4fb514573 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -1,20 +1,20 @@ use std::cmp::Reverse; -use ruff_diagnostics::Edit; use ruff_python_ast::helpers::{map_callable, map_subscript}; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::str::Quote; -use ruff_python_ast::visitor::transformer::{walk_expr, Transformer}; +use ruff_python_ast::visitor::transformer::{Transformer, walk_expr}; use ruff_python_ast::{self as ast, Decorator, Expr, StringLiteralFlags}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_parser::typing::parse_type_annotation; use ruff_python_semantic::{ - analyze, Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel, + Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel, analyze, }; use ruff_text_size::{Ranged, TextRange}; -use crate::rules::flake8_type_checking::settings::Settings; +use crate::Edit; use crate::Locator; +use crate::rules::flake8_type_checking::settings::Settings; /// Returns `true` if the [`ResolvedReference`] is in a typing-only context _or_ a runtime-evaluated /// context (with quoting enabled). @@ -367,6 +367,7 @@ impl<'a> QuoteAnnotator<'a> { let annotation = subgenerator.expr(&expr_without_forward_references); generator.expr(&Expr::from(ast::StringLiteral { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), value: annotation.into_boxed_str(), flags: self.flags, })) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index dfb6ce81ddad9..45511cdd8ca90 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -6,7 +6,6 @@ pub mod settings; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use anyhow::Result; @@ -15,7 +14,7 @@ mod tests { use crate::registry::{Linter, Rule}; use crate::test::{test_path, test_snippet}; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TC005.py"))] #[test_case(Rule::RuntimeCastValue, Path::new("TC006.py"))] @@ -37,6 +36,7 @@ mod tests { #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_8.py"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_9.py"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))] + #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("whitespace.py"))] #[test_case(Rule::RuntimeStringUnion, Path::new("TC010_1.py"))] #[test_case(Rule::RuntimeStringUnion, Path::new("TC010_2.py"))] #[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TC001.py"))] @@ -55,12 +55,12 @@ mod tests { #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))] #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_2.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -70,7 +70,7 @@ mod tests { #[test_case(Rule::QuotedTypeAlias, Path::new("TC008.py"))] #[test_case(Rule::QuotedTypeAlias, Path::new("TC008_typing_execution_context.py"))] fn type_alias_rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings::for_rules(vec![ @@ -78,25 +78,21 @@ mod tests { Rule::QuotedTypeAlias, ]), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } #[test_case(Rule::QuotedTypeAlias, Path::new("TC008_union_syntax_pre_py310.py"))] fn type_alias_rules_pre_py310(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "pre_py310_{}_{}", - rule_code.as_ref(), - path.to_string_lossy() - ); + let snapshot = format!("pre_py310_{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY39, + unresolved_target_version: PythonVersion::PY39.into(), ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -107,7 +103,7 @@ mod tests { #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote3.py"))] #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote3.py"))] fn quote(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("quote_{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("quote_{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -118,7 +114,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -126,7 +122,7 @@ mod tests { #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))] #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("kw_only.py"))] fn strict(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("strict_{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -137,7 +133,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -153,7 +149,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -170,7 +166,7 @@ mod tests { Path::new("exempt_type_checking_3.py") )] fn exempt_type_checking(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -182,7 +178,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -207,7 +203,7 @@ mod tests { Path::new("runtime_evaluated_base_classes_5.py") )] fn runtime_evaluated_base_classes(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -221,7 +217,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -238,7 +234,7 @@ mod tests { Path::new("runtime_evaluated_decorators_3.py") )] fn runtime_evaluated_decorators(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -253,7 +249,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -264,7 +260,7 @@ mod tests { Path::new("module/undefined.py") )] fn base_class_same_file(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -275,14 +271,14 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("module/app.py"))] #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/routes.py"))] fn decorator_same_file(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), &settings::LinterSettings { @@ -302,7 +298,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -523,7 +519,7 @@ mod tests { contents, &settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules()), ); - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); } #[test_case( @@ -575,6 +571,6 @@ mod tests { ..settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules()) }, ); - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index 303114a9a3435..c6b935929d5e8 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -1,12 +1,12 @@ use ruff_python_ast as ast; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for an empty type-checking block. @@ -63,7 +63,7 @@ pub(crate) fn empty_type_checking_block(checker: &Checker, stmt: &ast::StmtIf) { return; } - let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, stmt.range()); + let mut diagnostic = checker.report_diagnostic(EmptyTypeCheckingBlock, stmt.range()); // Delete the entire type-checking block. let stmt = checker.semantic().current_statement(); let parent = checker.semantic().current_statement_parent(); @@ -71,5 +71,4 @@ pub(crate) fn empty_type_checking_block(checker: &Checker, stmt: &ast::StmtIf) { diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_parent_id(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs index b3b94e6e88270..1db267f67703b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs @@ -1,11 +1,11 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::flake8_type_checking::helpers::quote_type_expression; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for unquoted type expressions in `typing.cast()` calls. @@ -62,7 +62,7 @@ pub(crate) fn runtime_cast_value(checker: &Checker, type_expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new(RuntimeCastValue, type_expr.range()); + let mut diagnostic = checker.report_diagnostic(RuntimeCastValue, type_expr.range()); let edit = quote_type_expression( type_expr, checker.semantic(), @@ -75,5 +75,4 @@ pub(crate) fn runtime_cast_value(checker: &Checker, type_expr: &Expr) { } else { diagnostic.set_fix(Fix::safe_edit(edit)); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 469c6500c6b23..10a1580388aaa 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -3,8 +3,7 @@ use std::borrow::Cow; use anyhow::Result; use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::{Imported, NodeId, Scope, ScopeId}; use ruff_text_size::Ranged; @@ -14,6 +13,7 @@ use crate::fix; use crate::importer::ImportedMembers; use crate::rules::flake8_type_checking::helpers::{filter_contained, quote_annotation}; use crate::rules::flake8_type_checking::imports::ImportBinding; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for imports that are required at runtime but are only defined in @@ -164,7 +164,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S // since some people will consistently use their // type aliases at runtimes, while others won't, so // the best solution is unclear. - if checker.settings.flake8_type_checking.quote_annotations + if checker.settings().flake8_type_checking.quote_annotations && binding.references().all(|reference_id| { let reference = checker.semantic().reference(reference_id); reference.in_typing_context() || reference.in_runtime_evaluated_annotation() @@ -198,7 +198,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S .. } in imports { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RuntimeImportInTypeCheckingBlock { qualified_name: import.qualified_name().to_string(), strategy: Strategy::MoveImport, @@ -211,7 +211,6 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S if let Some(fix) = fix.as_ref() { diagnostic.set_fix(fix.clone()); } - checker.report_diagnostic(diagnostic); } } @@ -227,7 +226,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S .. } in imports { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RuntimeImportInTypeCheckingBlock { qualified_name: import.qualified_name().to_string(), strategy: Strategy::QuoteUsages, @@ -238,7 +237,6 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S diagnostic.set_parent(range.start()); } diagnostic.set_fix(fix.clone()); - checker.report_diagnostic(diagnostic); } } @@ -252,7 +250,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S .. } in imports { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RuntimeImportInTypeCheckingBlock { qualified_name: import.qualified_name().to_string(), strategy: Strategy::MoveImport, @@ -262,7 +260,6 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S if let Some(range) = parent_range { diagnostic.set_parent(range.start()); } - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs index b9379e9c16e1e..2391cbf1576cc 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_string_union.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::{Expr, Operator}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,7 +66,7 @@ pub(crate) fn runtime_string_union(checker: &Checker, expr: &Expr) { traverse_op(expr, &mut strings); for string in strings { - checker.report_diagnostic(Diagnostic::new(RuntimeStringUnion, string.range())); + checker.report_diagnostic(RuntimeStringUnion, string.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 245741b9e9efb..53c1d004c300c 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -1,6 +1,5 @@ use ast::{ExprContext, Operator}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::{Expr, Stmt}; use ruff_python_semantic::{Binding, SemanticModel, TypingOnlyBindingsStatus}; @@ -10,7 +9,9 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::flake8_type_checking::helpers::quote_type_expression; +use crate::{AlwaysFixableViolation, Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; +use ruff_python_ast::parenthesize::parenthesized_range; /// ## What it does /// Checks if [PEP 613] explicit type aliases contain references to @@ -87,11 +88,15 @@ impl Violation for UnquotedTypeAlias { /// ## Example /// Given: /// ```python +/// from typing import TypeAlias +/// /// OptInt: TypeAlias = "int | None" /// ``` /// /// Use instead: /// ```python +/// from typing import TypeAlias +/// /// OptInt: TypeAlias = int | None /// ``` /// @@ -142,26 +147,26 @@ impl AlwaysFixableViolation for QuotedTypeAlias { } /// TC007 -pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Option> { +pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) { if binding.context.is_typing() { - return None; + return; } if !binding.is_annotated_type_alias() { - return None; + return; } let Some(Stmt::AnnAssign(ast::StmtAnnAssign { value: Some(expr), .. })) = binding.statement(checker.semantic()) else { - return None; + return; }; let mut names = Vec::new(); collect_typing_references(checker, expr, &mut names); if names.is_empty() { - return None; + return; } // We generate a diagnostic for every name that needs to be quoted @@ -178,14 +183,11 @@ pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Optio checker.locator(), checker.default_string_flags(), ); - let mut diagnostics = Vec::with_capacity(names.len()); for name in names { - let mut diagnostic = Diagnostic::new(UnquotedTypeAlias, name.range()); + let mut diagnostic = checker.report_diagnostic(UnquotedTypeAlias, name.range()); diagnostic.set_parent(parent); diagnostic.set_fix(Fix::unsafe_edit(edit.clone())); - diagnostics.push(diagnostic); } - Some(diagnostics) } /// Traverses the type expression and collects `[Expr::Name]` nodes that are @@ -249,7 +251,7 @@ fn collect_typing_references<'a>( // if TC004 is enabled we shouldn't emit a TC007 for a reference to // a binding that would emit a TC004, otherwise the fixes will never // stabilize and keep going in circles - if checker.enabled(Rule::RuntimeImportInTypeCheckingBlock) + if checker.is_rule_enabled(Rule::RuntimeImportInTypeCheckingBlock) && checker .semantic() .binding(binding_id) @@ -270,7 +272,7 @@ pub(crate) fn quoted_type_alias( expr: &Expr, annotation_expr: &ast::ExprStringLiteral, ) { - if checker.enabled(Rule::RuntimeStringUnion) { + if checker.is_rule_enabled(Rule::RuntimeStringUnion) { // this should return a TC010 error instead if let Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, @@ -289,14 +291,36 @@ pub(crate) fn quoted_type_alias( } let range = annotation_expr.range(); - let mut diagnostic = Diagnostic::new(QuotedTypeAlias, range); - let edit = Edit::range_replacement(annotation_expr.value.to_string(), range); + let mut diagnostic = checker.report_diagnostic(QuotedTypeAlias, range); + let fix_string = annotation_expr.value.to_string(); + let fix_string = if (fix_string.contains('\n') || fix_string.contains('\r')) + && parenthesized_range( + // Check for parenthesis outside string ("""...""") + annotation_expr.into(), + checker.semantic().current_statement().into(), + checker.comment_ranges(), + checker.locator().contents(), + ) + .is_none() + && parenthesized_range( + // Check for parenthesis inside string """(...)""" + expr.into(), + annotation_expr.into(), + checker.comment_ranges(), + checker.locator().contents(), + ) + .is_none() + { + format!("({fix_string})") + } else { + fix_string + }; + let edit = Edit::range_replacement(fix_string, range); if checker.comment_ranges().intersects(range) { diagnostic.set_fix(Fix::unsafe_edit(edit)); } else { diagnostic.set_fix(Fix::safe_edit(edit)); } - checker.report_diagnostic(diagnostic); } /// Traverses the type expression and checks if the expression can safely diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 777a7b123713b..dd0f1abe80a42 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -3,20 +3,22 @@ use std::borrow::Cow; use anyhow::Result; use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::{Binding, Imported, NodeId, Scope}; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, DiagnosticGuard}; use crate::codes::Rule; use crate::fix; use crate::importer::ImportedMembers; +use crate::preview::is_full_path_match_source_strategy_enabled; use crate::rules::flake8_type_checking::helpers::{ filter_contained, is_typing_reference, quote_annotation, }; use crate::rules::flake8_type_checking::imports::ImportBinding; -use crate::rules::isort::{categorize, ImportSection, ImportType}; +use crate::rules::isort::categorize::MatchSourceStrategy; +use crate::rules::isort::{ImportSection, ImportType, categorize}; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for first-party imports that are only used for type annotations, but @@ -42,7 +44,7 @@ use crate::rules::isort::{categorize, ImportSection, ImportType}; /// ```python /// from __future__ import annotations /// -/// import local_module +/// from . import local_module /// /// /// def func(sized: local_module.Container) -> int: @@ -56,13 +58,19 @@ use crate::rules::isort::{categorize, ImportSection, ImportType}; /// from typing import TYPE_CHECKING /// /// if TYPE_CHECKING: -/// import local_module +/// from . import local_module /// /// /// def func(sized: local_module.Container) -> int: /// return len(sized) /// ``` /// +/// +/// ## Preview +/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +/// the criterion for determining whether an import is first-party +/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-third-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. +/// /// ## Options /// - `lint.flake8-type-checking.quote-annotations` /// - `lint.flake8-type-checking.runtime-evaluated-base-classes` @@ -138,6 +146,11 @@ impl Violation for TypingOnlyFirstPartyImport { /// return len(df) /// ``` /// +/// ## Preview +/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +/// the criterion for determining whether an import is first-party +/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. +/// /// ## Options /// - `lint.flake8-type-checking.quote-annotations` /// - `lint.flake8-type-checking.runtime-evaluated-base-classes` @@ -260,7 +273,7 @@ pub(crate) fn typing_only_runtime_import( // If we're in un-strict mode, don't flag typing-only imports that are // implicitly loaded by way of a valid runtime import. - if !checker.settings.flake8_type_checking.strict + if !checker.settings().flake8_type_checking.strict && runtime_imports .iter() .any(|import| is_implicit_import(binding, import)) @@ -281,7 +294,7 @@ pub(crate) fn typing_only_runtime_import( .references() .map(|reference_id| checker.semantic().reference(reference_id)) .all(|reference| { - is_typing_reference(reference, &checker.settings.flake8_type_checking) + is_typing_reference(reference, &checker.settings().flake8_type_checking) }) { let qualified_name = import.qualified_name(); @@ -289,7 +302,7 @@ pub(crate) fn typing_only_runtime_import( if is_exempt( &qualified_name.to_string(), &checker - .settings + .settings() .flake8_type_checking .exempt_modules .iter() @@ -299,18 +312,28 @@ pub(crate) fn typing_only_runtime_import( continue; } + let source_name = import.source_name().join("."); + // Categorize the import, using coarse-grained categorization. + let match_source_strategy = + if is_full_path_match_source_strategy_enabled(checker.settings()) { + MatchSourceStrategy::FullPath + } else { + MatchSourceStrategy::Root + }; + let import_type = match categorize( - &qualified_name.to_string(), + &source_name, qualified_name.is_unresolved_import(), - &checker.settings.src, + &checker.settings().src, checker.package(), - checker.settings.isort.detect_same_package, - &checker.settings.isort.known_modules, + checker.settings().isort.detect_same_package, + &checker.settings().isort.known_modules, checker.target_version(), - checker.settings.isort.no_sections, - &checker.settings.isort.section_order, - &checker.settings.isort.default_section, + checker.settings().isort.no_sections, + &checker.settings().isort.section_order, + &checker.settings().isort.default_section, + match_source_strategy, ) { ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { ImportType::FirstParty @@ -324,7 +347,7 @@ pub(crate) fn typing_only_runtime_import( } }; - if !checker.enabled(rule_for(import_type)) { + if !checker.is_rule_enabled(rule_for(import_type)) { continue; } @@ -370,8 +393,10 @@ pub(crate) fn typing_only_runtime_import( .. } in imports { - let mut diagnostic = Diagnostic::new( - diagnostic_for(import_type, import.qualified_name().to_string()), + let mut diagnostic = diagnostic_for( + checker, + import_type, + import.qualified_name().to_string(), range, ); if let Some(range) = parent_range { @@ -380,7 +405,6 @@ pub(crate) fn typing_only_runtime_import( if let Some(fix) = fix.as_ref() { diagnostic.set_fix(fix.clone()); } - checker.report_diagnostic(diagnostic); } } @@ -394,14 +418,15 @@ pub(crate) fn typing_only_runtime_import( .. } in imports { - let mut diagnostic = Diagnostic::new( - diagnostic_for(import_type, import.qualified_name().to_string()), + let mut diagnostic = diagnostic_for( + checker, + import_type, + import.qualified_name().to_string(), range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); } - checker.report_diagnostic(diagnostic); } } } @@ -417,11 +442,22 @@ fn rule_for(import_type: ImportType) -> Rule { } /// Return the [`Diagnostic`] for the given import type. -fn diagnostic_for(import_type: ImportType, qualified_name: String) -> DiagnosticKind { +fn diagnostic_for<'a, 'b>( + checker: &'a Checker<'b>, + import_type: ImportType, + qualified_name: String, + range: TextRange, +) -> DiagnosticGuard<'a, 'b> { match import_type { - ImportType::StandardLibrary => TypingOnlyStandardLibraryImport { qualified_name }.into(), - ImportType::ThirdParty => TypingOnlyThirdPartyImport { qualified_name }.into(), - ImportType::FirstParty => TypingOnlyFirstPartyImport { qualified_name }.into(), + ImportType::StandardLibrary => { + checker.report_diagnostic(TypingOnlyStandardLibraryImport { qualified_name }, range) + } + ImportType::ThirdParty => { + checker.report_diagnostic(TypingOnlyThirdPartyImport { qualified_name }, range) + } + ImportType::FirstParty => { + checker.report_diagnostic(TypingOnlyFirstPartyImport { qualified_name }, range) + } _ => unreachable!("Unexpected import type"), } } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.py.snap index 626c985dbe4e9..b55796e6075d1 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008.py.snap @@ -409,6 +409,8 @@ TC008.py:52:18: TC008 [*] Remove quotes from type alias 51 | a: TypeAlias = 'Baz' # OK 52 | type A = 'Baz' # TC008 | ^^^^^ TC008 +53 | +54 | # O should have parenthesis added | = help: Remove quotes @@ -418,3 +420,187 @@ TC008.py:52:18: TC008 [*] Remove quotes from type alias 51 51 | a: TypeAlias = 'Baz' # OK 52 |- type A = 'Baz' # TC008 52 |+ type A = Baz # TC008 +53 53 | +54 54 | # O should have parenthesis added +55 55 | o: TypeAlias = """int + +TC008.py:55:16: TC008 [*] Remove quotes from type alias + | +54 | # O should have parenthesis added +55 | o: TypeAlias = """int + | ________________^ +56 | | | None""" + | |_________^ TC008 +57 | type O = """int +58 | | None""" + | + = help: Remove quotes + +ℹ Safe fix +52 52 | type A = 'Baz' # TC008 +53 53 | +54 54 | # O should have parenthesis added +55 |-o: TypeAlias = """int +56 |-| None""" + 55 |+o: TypeAlias = (int + 56 |+| None) +57 57 | type O = """int +58 58 | | None""" +59 59 | + +TC008.py:57:10: TC008 [*] Remove quotes from type alias + | +55 | o: TypeAlias = """int +56 | | None""" +57 | type O = """int + | __________^ +58 | | | None""" + | |_________^ TC008 +59 | +60 | # P, Q, and R should not have parenthesis added + | + = help: Remove quotes + +ℹ Safe fix +54 54 | # O should have parenthesis added +55 55 | o: TypeAlias = """int +56 56 | | None""" +57 |-type O = """int +58 |-| None""" + 57 |+type O = (int + 58 |+| None) +59 59 | +60 60 | # P, Q, and R should not have parenthesis added +61 61 | p: TypeAlias = ("""int + +TC008.py:61:17: TC008 [*] Remove quotes from type alias + | +60 | # P, Q, and R should not have parenthesis added +61 | p: TypeAlias = ("""int + | _________________^ +62 | | | None""") + | |_________^ TC008 +63 | type P = ("""int +64 | | None""") + | + = help: Remove quotes + +ℹ Safe fix +58 58 | | None""" +59 59 | +60 60 | # P, Q, and R should not have parenthesis added +61 |-p: TypeAlias = ("""int +62 |-| None""") + 61 |+p: TypeAlias = (int + 62 |+| None) +63 63 | type P = ("""int +64 64 | | None""") +65 65 | + +TC008.py:63:11: TC008 [*] Remove quotes from type alias + | +61 | p: TypeAlias = ("""int +62 | | None""") +63 | type P = ("""int + | ___________^ +64 | | | None""") + | |_________^ TC008 +65 | +66 | q: TypeAlias = """(int + | + = help: Remove quotes + +ℹ Safe fix +60 60 | # P, Q, and R should not have parenthesis added +61 61 | p: TypeAlias = ("""int +62 62 | | None""") +63 |-type P = ("""int +64 |-| None""") + 63 |+type P = (int + 64 |+| None) +65 65 | +66 66 | q: TypeAlias = """(int +67 67 | | None)""" + +TC008.py:66:16: TC008 [*] Remove quotes from type alias + | +64 | | None""") +65 | +66 | q: TypeAlias = """(int + | ________________^ +67 | | | None)""" + | |__________^ TC008 +68 | type Q = """(int +69 | | None)""" + | + = help: Remove quotes + +ℹ Safe fix +63 63 | type P = ("""int +64 64 | | None""") +65 65 | +66 |-q: TypeAlias = """(int +67 |-| None)""" + 66 |+q: TypeAlias = (int + 67 |+| None) +68 68 | type Q = """(int +69 69 | | None)""" +70 70 | + +TC008.py:68:10: TC008 [*] Remove quotes from type alias + | +66 | q: TypeAlias = """(int +67 | | None)""" +68 | type Q = """(int + | __________^ +69 | | | None)""" + | |__________^ TC008 +70 | +71 | r: TypeAlias = """int | None""" + | + = help: Remove quotes + +ℹ Safe fix +65 65 | +66 66 | q: TypeAlias = """(int +67 67 | | None)""" +68 |-type Q = """(int +69 |-| None)""" + 68 |+type Q = (int + 69 |+| None) +70 70 | +71 71 | r: TypeAlias = """int | None""" +72 72 | type R = """int | None""" + +TC008.py:71:16: TC008 [*] Remove quotes from type alias + | +69 | | None)""" +70 | +71 | r: TypeAlias = """int | None""" + | ^^^^^^^^^^^^^^^^ TC008 +72 | type R = """int | None""" + | + = help: Remove quotes + +ℹ Safe fix +68 68 | type Q = """(int +69 69 | | None)""" +70 70 | +71 |-r: TypeAlias = """int | None""" + 71 |+r: TypeAlias = int | None +72 72 | type R = """int | None""" + +TC008.py:72:10: TC008 [*] Remove quotes from type alias + | +71 | r: TypeAlias = """int | None""" +72 | type R = """int | None""" + | ^^^^^^^^^^^^^^^^ TC008 + | + = help: Remove quotes + +ℹ Safe fix +69 69 | | None)""" +70 70 | +71 71 | r: TypeAlias = """int | None""" +72 |-type R = """int | None""" + 72 |+type R = int | None diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap index 25348b34ab633..f117c364f7eb5 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap @@ -212,3 +212,37 @@ TC006.py:89:9: TC006 [*] Add quotes to type expression in `typing.cast()` 89 |+ "int" # TC006 90 90 | , 6.0 91 91 | ) +92 92 | + +TC006.py:98:14: TC006 [*] Add quotes to type expression in `typing.cast()` + | +96 | from typing import cast +97 | +98 | cast(typ=int, val=3.0) # TC006 + | ^^^ TC006 +99 | cast(val=3.0, typ=int) # TC006 + | + = help: Add quotes + +ℹ Safe fix +95 95 | # Keyword arguments +96 96 | from typing import cast +97 97 | +98 |- cast(typ=int, val=3.0) # TC006 + 98 |+ cast(typ="int", val=3.0) # TC006 +99 99 | cast(val=3.0, typ=int) # TC006 + +TC006.py:99:23: TC006 [*] Add quotes to type expression in `typing.cast()` + | +98 | cast(typ=int, val=3.0) # TC006 +99 | cast(val=3.0, typ=int) # TC006 + | ^^^ TC006 + | + = help: Add quotes + +ℹ Safe fix +96 96 | from typing import cast +97 97 | +98 98 | cast(typ=int, val=3.0) # TC006 +99 |- cast(val=3.0, typ=int) # TC006 + 99 |+ cast(val=3.0, typ="int") # TC006 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_whitespace.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_whitespace.py.snap new file mode 100644 index 0000000000000..b4dd04e5ba174 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_whitespace.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +whitespace.py:5:26: TC004 [*] Move import `builtins` out of type-checking block. Import is used for more than type hinting. + | +3 | from typing import TYPE_CHECKING \ +4 | +5 | if TYPE_CHECKING: import builtins + | ^^^^^^^^ TC004 +6 | builtins.print("!") + | + = help: Move out of type-checking block + +ℹ Unsafe fix +1 1 | # Regression test for: https://github.com/astral-sh/ruff/issues/19175 +2 2 | # there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash +3 |-from typing import TYPE_CHECKING \ + 3 |+from typing import TYPE_CHECKING; import builtins \ +4 4 | +5 |-if TYPE_CHECKING: import builtins + 5 |+if TYPE_CHECKING: pass +6 6 | builtins.print("!") diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/mod.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/mod.rs index 8f0eaaed3a48e..2deca6b3f12cf 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::UnusedFunctionArgument, Path::new("ARG.py"))] #[test_case(Rule::UnusedMethodArgument, Path::new("ARG.py"))] @@ -24,7 +24,7 @@ mod tests { Path::new("flake8_unused_arguments").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -45,7 +45,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -66,7 +66,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index a8ceee48b8881..fb0ea5a0042be 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -1,13 +1,12 @@ use ruff_python_ast as ast; use ruff_python_ast::{Parameter, Parameters, Stmt, StmtExpr, StmtFunctionDef, StmtRaise}; -use ruff_diagnostics::DiagnosticKind; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::{function_type, visibility}; use ruff_python_semantic::{Scope, ScopeKind, SemanticModel}; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -223,14 +222,18 @@ enum Argumentable { } impl Argumentable { - fn check_for(self, name: String) -> DiagnosticKind { + fn check_for(self, checker: &Checker, name: String, range: TextRange) { match self { - Self::Function => UnusedFunctionArgument { name }.into(), - Self::Method => UnusedMethodArgument { name }.into(), - Self::ClassMethod => UnusedClassMethodArgument { name }.into(), - Self::StaticMethod => UnusedStaticMethodArgument { name }.into(), - Self::Lambda => UnusedLambdaArgument { name }.into(), - } + Self::Function => checker.report_diagnostic(UnusedFunctionArgument { name }, range), + Self::Method => checker.report_diagnostic(UnusedMethodArgument { name }, range), + Self::ClassMethod => { + checker.report_diagnostic(UnusedClassMethodArgument { name }, range) + } + Self::StaticMethod => { + checker.report_diagnostic(UnusedStaticMethodArgument { name }, range) + } + Self::Lambda => checker.report_diagnostic(UnusedLambdaArgument { name }, range), + }; } const fn rule_code(self) -> Rule { @@ -247,7 +250,7 @@ impl Argumentable { /// Check a plain function for unused arguments. fn function(argumentable: Argumentable, parameters: &Parameters, scope: &Scope, checker: &Checker) { let ignore_variadic_names = checker - .settings + .settings() .flake8_unused_arguments .ignore_variadic_names; let args = parameters @@ -273,7 +276,7 @@ fn function(argumentable: Argumentable, parameters: &Parameters, scope: &Scope, /// Check a method for unused arguments. fn method(argumentable: Argumentable, parameters: &Parameters, scope: &Scope, checker: &Checker) { let ignore_variadic_names = checker - .settings + .settings() .flake8_unused_arguments .ignore_variadic_names; let args = parameters @@ -304,23 +307,21 @@ fn call<'a>( checker: &Checker, ) { let semantic = checker.semantic(); - let dummy_variable_rgx = &checker.settings.dummy_variable_rgx; - checker.report_diagnostics(parameters.filter_map(|arg| { - let binding = scope + let dummy_variable_rgx = &checker.settings().dummy_variable_rgx; + for arg in parameters { + let Some(binding) = scope .get(arg.name()) - .map(|binding_id| semantic.binding(binding_id))?; + .map(|binding_id| semantic.binding(binding_id)) + else { + continue; + }; if binding.kind.is_argument() && binding.is_unused() && !dummy_variable_rgx.is_match(arg.name()) { - Some(Diagnostic::new( - argumentable.check_for(arg.name.to_string()), - binding.range(), - )) - } else { - None + argumentable.check_for(checker, arg.name.to_string(), binding.range()); } - })); + } } /// Returns `true` if a function appears to be a base class stub. In other @@ -346,10 +347,13 @@ pub(crate) fn is_not_implemented_stub_with_variable( _ => &function_def.body, }; - let [Stmt::Assign(ast::StmtAssign { targets, value, .. }), Stmt::Raise(StmtRaise { - exc: Some(exception), - .. - })] = statements + let [ + Stmt::Assign(ast::StmtAssign { targets, value, .. }), + Stmt::Raise(StmtRaise { + exc: Some(exception), + .. + }), + ] = statements else { return false; }; @@ -404,11 +408,11 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { decorator_list, parent, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ) { function_type::FunctionType::Function => { - if checker.enabled(Argumentable::Function.rule_code()) + if checker.is_rule_enabled(Argumentable::Function.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && !visibility::is_overload(decorator_list, checker.semantic()) @@ -417,7 +421,7 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { } } function_type::FunctionType::Method => { - if checker.enabled(Argumentable::Method.rule_code()) + if checker.is_rule_enabled(Argumentable::Method.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) @@ -431,7 +435,7 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { } } function_type::FunctionType::ClassMethod => { - if checker.enabled(Argumentable::ClassMethod.rule_code()) + if checker.is_rule_enabled(Argumentable::ClassMethod.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) @@ -445,7 +449,7 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { } } function_type::FunctionType::StaticMethod => { - if checker.enabled(Argumentable::StaticMethod.rule_code()) + if checker.is_rule_enabled(Argumentable::StaticMethod.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && (!visibility::is_magic(name) @@ -459,7 +463,7 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { } } function_type::FunctionType::NewMethod => { - if checker.enabled(Argumentable::StaticMethod.rule_code()) + if checker.is_rule_enabled(Argumentable::StaticMethod.rule_code()) && !function_type::is_stub(function_def, checker.semantic()) && !is_not_implemented_stub_with_variable(function_def, checker.semantic()) && !visibility::is_abstract(decorator_list, checker.semantic()) @@ -475,7 +479,7 @@ pub(crate) fn unused_arguments(checker: &Checker, scope: &Scope) { } ScopeKind::Lambda(ast::ExprLambda { parameters, .. }) => { if let Some(parameters) = parameters { - if checker.enabled(Argumentable::Lambda.rule_code()) { + if checker.is_rule_enabled(Argumentable::Lambda.rule_code()) { function(Argumentable::Lambda, parameters, scope, checker); } } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs new file mode 100644 index 0000000000000..d8e1fbe0d760c --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs @@ -0,0 +1,74 @@ +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use crate::{Applicability, Edit, Fix, Violation}; +use ruff_python_ast::{self as ast}; +use ruff_python_ast::{Expr, ExprCall}; +use ruff_text_size::Ranged; + +pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool { + arguments + .find_keyword(name) + .is_some_and(|keyword| !keyword.value.is_none_literal_expr()) +} + +pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool { + expr.as_call_expr().is_some_and(|expr_call| { + checker + .semantic() + .resolve_qualified_name(&expr_call.func) + .is_some_and(|name| matches!(name.segments(), ["pathlib", "Path"])) + }) +} + +/// We check functions that take only 1 argument, this does not apply to functions +/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib, +/// so check if it's set to non-default values +pub(crate) fn check_os_pathlib_single_arg_calls( + checker: &Checker, + call: &ExprCall, + attr: &str, + fn_argument: &str, + fix_enabled: bool, + violation: impl Violation, +) { + if call.arguments.len() != 1 { + return; + } + + let Some(arg) = call.arguments.find_argument_value(fn_argument, 0) else { + return; + }; + + let arg_code = checker.locator().slice(arg.range()); + let range = call.range(); + + let mut diagnostic = checker.report_diagnostic(violation, call.func.range()); + + if fix_enabled { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("pathlib", "Path"), + call.start(), + checker.semantic(), + )?; + + let applicability = if checker.comment_ranges().intersects(range) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + let replacement = if is_pathlib_path_call(checker, arg) { + format!("{arg_code}.{attr}") + } else { + format!("{binding}({arg_code}).{attr}") + }; + + Ok(Fix::applicable_edits( + Edit::range_replacement(replacement, range), + [import_edit], + applicability, + )) + }); + } +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs index fcb05b3f0923f..91765ce8413cc 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/). +mod helpers; pub(crate) mod rules; pub(crate) mod violations; @@ -9,9 +10,10 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings; + use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Path::new("full_name.py"))] @@ -50,7 +52,7 @@ mod tests { Rule::BuiltinOpen, ]), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -58,6 +60,7 @@ mod tests { #[test_case(Rule::PyPath, Path::new("py_path_2.py"))] #[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))] + #[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))] #[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))] #[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))] #[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))] @@ -66,13 +69,78 @@ mod tests { #[test_case(Rule::OsListdir, Path::new("PTH208.py"))] #[test_case(Rule::InvalidPathlibWithSuffix, Path::new("PTH210.py"))] #[test_case(Rule::InvalidPathlibWithSuffix, Path::new("PTH210_1.py"))] + #[test_case(Rule::OsSymlink, Path::new("PTH211.py"))] fn rules_pypath(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_use_pathlib").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("full_name.py"))] + #[test_case(Path::new("import_as.py"))] + #[test_case(Path::new("import_from_as.py"))] + #[test_case(Path::new("import_from.py"))] + fn preview_rules(path: &Path) -> Result<()> { + let snapshot = format!("preview_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("flake8_use_pathlib").join(path).as_path(), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + ..settings::LinterSettings::for_rules(vec![ + Rule::OsPathAbspath, + Rule::OsChmod, + Rule::OsMkdir, + Rule::OsMakedirs, + Rule::OsRename, + Rule::OsReplace, + Rule::OsRmdir, + Rule::OsRemove, + Rule::OsUnlink, + Rule::OsGetcwd, + Rule::OsPathExists, + Rule::OsPathExpanduser, + Rule::OsPathIsdir, + Rule::OsPathIsfile, + Rule::OsPathIslink, + Rule::OsReadlink, + Rule::OsStat, + Rule::OsPathIsabs, + Rule::OsPathJoin, + Rule::OsPathBasename, + Rule::OsPathDirname, + Rule::OsPathSamefile, + Rule::OsPathSplitext, + Rule::BuiltinOpen, + ]) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))] + #[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))] + #[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))] + #[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))] + #[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))] + fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("flake8_use_pathlib").join(path).as_path(), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs index be4fb8988e361..3c329bf20d769 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/glob_rule.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// Checks for the use of `glob.glob()` and `glob.iglob()`. diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs index d927d150f20c3..6b8242226e50f 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs @@ -1,9 +1,9 @@ use crate::checkers::ast::Checker; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::{Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, StringFlags}; -use ruff_python_semantic::analyze::typing; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; /// ## What it does @@ -19,12 +19,20 @@ use ruff_text_size::Ranged; /// ## Example /// /// ```python +/// from pathlib import Path +/// +/// path = Path() +/// /// path.with_suffix("py") /// ``` /// /// Use instead: /// /// ```python +/// from pathlib import Path +/// +/// path = Path() +/// /// path.with_suffix(".py") /// ``` /// @@ -108,7 +116,8 @@ pub(crate) fn invalid_pathlib_with_suffix(checker: &Checker, call: &ast::ExprCal }; let single_dot = string_value == "."; - let mut diagnostic = Diagnostic::new(InvalidPathlibWithSuffix { single_dot }, call.range); + let mut diagnostic = + checker.report_diagnostic(InvalidPathlibWithSuffix { single_dot }, call.range); if !single_dot { let after_leading_quote = string.start() + first_part.flags.opener_len(); diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( @@ -116,8 +125,6 @@ pub(crate) fn invalid_pathlib_with_suffix(checker: &Checker, call: &ast::ExprCal after_leading_quote, ))); } - - checker.report_diagnostic(diagnostic); } fn is_path_with_suffix_call(semantic: &SemanticModel, func: &ast::Expr) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs index f009855154a44..a5dd3f69f3a76 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs @@ -1,19 +1,45 @@ pub(crate) use glob_rule::*; pub(crate) use invalid_pathlib_with_suffix::*; +pub(crate) use os_path_abspath::*; +pub(crate) use os_path_basename::*; +pub(crate) use os_path_dirname::*; +pub(crate) use os_path_exists::*; +pub(crate) use os_path_expanduser::*; pub(crate) use os_path_getatime::*; pub(crate) use os_path_getctime::*; pub(crate) use os_path_getmtime::*; pub(crate) use os_path_getsize::*; +pub(crate) use os_path_isabs::*; +pub(crate) use os_path_isdir::*; +pub(crate) use os_path_isfile::*; +pub(crate) use os_path_islink::*; +pub(crate) use os_readlink::*; +pub(crate) use os_remove::*; +pub(crate) use os_rmdir::*; pub(crate) use os_sep_split::*; +pub(crate) use os_unlink::*; pub(crate) use path_constructor_current_directory::*; pub(crate) use replaceable_by_pathlib::*; mod glob_rule; mod invalid_pathlib_with_suffix; +mod os_path_abspath; +mod os_path_basename; +mod os_path_dirname; +mod os_path_exists; +mod os_path_expanduser; mod os_path_getatime; mod os_path_getctime; mod os_path_getmtime; mod os_path_getsize; +mod os_path_isabs; +mod os_path_isdir; +mod os_path_isfile; +mod os_path_islink; +mod os_readlink; +mod os_remove; +mod os_rmdir; mod os_sep_split; +mod os_unlink; mod path_constructor_current_directory; mod replaceable_by_pathlib; diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs new file mode 100644 index 0000000000000..a8961da959999 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_abspath.rs @@ -0,0 +1,74 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_abspath_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.abspath`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.resolve()` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.abspath()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// file_path = os.path.abspath("../path/to/file") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// file_path = Path("../path/to/file").resolve() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve) +/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathAbspath; + +impl Violation for OsPathAbspath { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.abspath()` should be replaced by `Path.resolve()`".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).resolve()`".to_string()) + } +} + +/// PTH100 +pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "abspath"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "resolve()", + "path", + is_fix_os_path_abspath_enabled(checker.settings()), + OsPathAbspath, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs new file mode 100644 index 0000000000000..f69526dd7abe3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_basename.rs @@ -0,0 +1,73 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_basename_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.basename`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.name` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.basename()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.basename(__file__) +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path(__file__).name +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name) +/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathBasename; + +impl Violation for OsPathBasename { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.basename()` should be replaced by `Path.name`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).name`".to_string()) + } +} + +/// PTH119 +pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "basename"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "name", + "p", + is_fix_os_path_basename_enabled(checker.settings()), + OsPathBasename, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs new file mode 100644 index 0000000000000..a39ebc2814a27 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_dirname.rs @@ -0,0 +1,73 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_dirname_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.dirname`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.parent` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.dirname()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.dirname(__file__) +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path(__file__).parent +/// ``` +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## References +/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent) +/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathDirname; + +impl Violation for OsPathDirname { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.dirname()` should be replaced by `Path.parent`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).parent`".to_string()) + } +} + +/// PTH120 +pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "dirname"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "parent", + "p", + is_fix_os_path_dirname_enabled(checker.settings()), + OsPathDirname, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs new file mode 100644 index 0000000000000..d56697b7b1852 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_exists.rs @@ -0,0 +1,73 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_exists_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.exists`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.exists()` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.exists()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.exists("file.py") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("file.py").exists() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists) +/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathExists; + +impl Violation for OsPathExists { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.exists()` should be replaced by `Path.exists()`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).exists()`".to_string()) + } +} + +/// PTH110 +pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "exists"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "exists()", + "path", + is_fix_os_path_exists_enabled(checker.settings()), + OsPathExists, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs new file mode 100644 index 0000000000000..6c25e30a5fcd3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_expanduser.rs @@ -0,0 +1,73 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_expanduser_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.expanduser`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.expanduser()` can improve readability over the `os.path` +/// module's counterparts (e.g., as `os.path.expanduser()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.expanduser("~/films/Monty Python") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("~/films/Monty Python").expanduser() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser) +/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathExpanduser; + +impl Violation for OsPathExpanduser { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.expanduser()` should be replaced by `Path.expanduser()`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).expanduser()`".to_string()) + } +} + +/// PTH111 +pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "expanduser"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "expanduser()", + "path", + is_fix_os_path_expanduser_enabled(checker.settings()), + OsPathExpanduser, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs index d4645dcbcea05..435ec1a9393ad 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getatime.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_getatime_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; /// ## What it does /// Checks for uses of `os.path.getatime`. @@ -31,6 +35,9 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// it can be less performant than the lower-level alternatives that work directly with strings, /// especially on older versions of Python. /// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime) @@ -42,8 +49,28 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; pub(crate) struct OsPathGetatime; impl Violation for OsPathGetatime { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { "`os.path.getatime` should be replaced by `Path.stat().st_atime`".to_string() } + + fn fix_title(&self) -> Option { + Some("Replace with `Path.stat(...).st_atime`".to_string()) + } +} + +/// PTH203 +pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "getatime"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "stat().st_atime", + "filename", + is_fix_os_path_getatime_enabled(checker.settings()), + OsPathGetatime, + ); } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs index 8c645f2994797..7b01da2234802 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getctime.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_getctime_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; /// ## What it does /// Checks for uses of `os.path.getctime`. @@ -31,6 +35,9 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// it can be less performant than the lower-level alternatives that work directly with strings, /// especially on older versions of Python. /// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime) @@ -42,8 +49,29 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; pub(crate) struct OsPathGetctime; impl Violation for OsPathGetctime { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "`os.path.getctime` should be replaced by `Path.stat().st_ctime`".to_string() } + + fn fix_title(&self) -> Option { + Some("Replace with `Path.stat(...).st_ctime`".to_string()) + } +} + +/// PTH205 +pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "getctime"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "stat().st_ctime", + "filename", + is_fix_os_path_getctime_enabled(checker.settings()), + OsPathGetctime, + ); } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs index 9237a307b0384..dbcc1d44b9535 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getmtime.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_getmtime_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; /// ## What it does /// Checks for uses of `os.path.getmtime`. @@ -31,6 +35,9 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// it can be less performant than the lower-level alternatives that work directly with strings, /// especially on older versions of Python. /// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime) @@ -42,8 +49,29 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; pub(crate) struct OsPathGetmtime; impl Violation for OsPathGetmtime { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "`os.path.getmtime` should be replaced by `Path.stat().st_mtime`".to_string() } + + fn fix_title(&self) -> Option { + Some("Replace with `Path.stat(...).st_mtime`".to_string()) + } +} + +/// PTH204 +pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "getmtime"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "stat().st_mtime", + "filename", + is_fix_os_path_getmtime_enabled(checker.settings()), + OsPathGetmtime, + ); } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs index b642783023469..9dc2606d7f2a0 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_getsize.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_getsize_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; /// ## What it does /// Checks for uses of `os.path.getsize`. @@ -31,6 +35,9 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// it can be less performant than the lower-level alternatives that work directly with strings, /// especially on older versions of Python. /// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// /// ## References /// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat) /// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize) @@ -42,8 +49,29 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; pub(crate) struct OsPathGetsize; impl Violation for OsPathGetsize { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "`os.path.getsize` should be replaced by `Path.stat().st_size`".to_string() } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).stat().st_size`".to_string()) + } +} + +/// PTH202 +pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "getsize"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "stat().st_size", + "filename", + is_fix_os_path_getsize_enabled(checker.settings()), + OsPathGetsize, + ); } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs new file mode 100644 index 0000000000000..482ec95f24484 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isabs.rs @@ -0,0 +1,72 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_isabs_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.isabs`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.is_absolute()` can improve readability over the `os.path` +/// module's counterparts (e.g., as `os.path.isabs()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// if os.path.isabs(file_name): +/// print("Absolute path!") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// if Path(file_name).is_absolute(): +/// print("Absolute path!") +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## References +/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute) +/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathIsabs; + +impl Violation for OsPathIsabs { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.isabs()` should be replaced by `Path.is_absolute()`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).is_absolute()`".to_string()) + } +} + +/// PTH117 +pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "isabs"] { + return; + } + check_os_pathlib_single_arg_calls( + checker, + call, + "is_absolute()", + "s", + is_fix_os_path_isabs_enabled(checker.settings()), + OsPathIsabs, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs new file mode 100644 index 0000000000000..4c33745e521c1 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isdir.rs @@ -0,0 +1,75 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_isdir_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.isdir`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.is_dir()` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.isdir()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.isdir("docs") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("docs").is_dir() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir) +/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathIsdir; + +impl Violation for OsPathIsdir { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.isdir()` should be replaced by `Path.is_dir()`".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).is_dir()`".to_string()) + } +} + +/// PTH112 +pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "isdir"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "is_dir()", + "s", + is_fix_os_path_isdir_enabled(checker.settings()), + OsPathIsdir, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs new file mode 100644 index 0000000000000..0fa2bd4fd8cbb --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_isfile.rs @@ -0,0 +1,75 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_isfile_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.isfile`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.is_file()` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.isfile()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.isfile("docs") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("docs").is_file() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file) +/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathIsfile; + +impl Violation for OsPathIsfile { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.isfile()` should be replaced by `Path.is_file()`".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).is_file()`".to_string()) + } +} + +/// PTH113 +pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "isfile"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "is_file()", + "path", + is_fix_os_path_isfile_enabled(checker.settings()), + OsPathIsfile, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs new file mode 100644 index 0000000000000..db9aa3d5e93ff --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_islink.rs @@ -0,0 +1,75 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_path_islink_enabled; +use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.path.islink`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.path`. When possible, using `Path` object +/// methods such as `Path.is_symlink()` can improve readability over the `os.path` +/// module's counterparts (e.g., `os.path.islink()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.path.islink("docs") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("docs").is_symlink() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink) +/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsPathIslink; + +impl Violation for OsPathIslink { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.path.islink()` should be replaced by `Path.is_symlink()`".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).is_symlink()`".to_string()) + } +} + +/// PTH114 +pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&str]) { + if segments != ["os", "path", "islink"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "is_symlink()", + "path", + is_fix_os_path_islink_enabled(checker.settings()), + OsPathIslink, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs new file mode 100644 index 0000000000000..62526f6666a8e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_readlink.rs @@ -0,0 +1,91 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_readlink_enabled; +use crate::rules::flake8_use_pathlib::helpers::{ + check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default, +}; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ExprCall, PythonVersion}; + +/// ## What it does +/// Checks for uses of `os.readlink`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os`. When possible, using `Path` object +/// methods such as `Path.readlink()` can improve readability over the `os` +/// module's counterparts (e.g., `os.readlink()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.readlink(file_name) +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path(file_name).readlink() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline) +/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsReadlink; + +impl Violation for OsReadlink { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.readlink()` should be replaced by `Path.readlink()`".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).readlink()`".to_string()) + } +} + +/// PTH115 +pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str]) { + // Python 3.9+ + if checker.target_version() < PythonVersion::PY39 { + return; + } + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink) + // ```text + // 0 1 + // os.readlink(path, *, dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + + if segments != ["os", "readlink"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "readlink()", + "path", + is_fix_os_readlink_enabled(checker.settings()), + OsReadlink, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs new file mode 100644 index 0000000000000..3ec99469eae42 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_remove.rs @@ -0,0 +1,86 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_remove_enabled; +use crate::rules::flake8_use_pathlib::helpers::{ + check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default, +}; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.remove`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os`. When possible, using `Path` object +/// methods such as `Path.unlink()` can improve readability over the `os` +/// module's counterparts (e.g., `os.remove()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.remove("file.py") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("file.py").unlink() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink) +/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsRemove; + +impl Violation for OsRemove { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.remove()` should be replaced by `Path.unlink()`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).unlink()`".to_string()) + } +} + +/// PTH107 +pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove) + // ```text + // 0 1 + // os.remove(path, *, dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + + if segments != ["os", "remove"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "unlink()", + "path", + is_fix_os_remove_enabled(checker.settings()), + OsRemove, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs new file mode 100644 index 0000000000000..bb85284f4e15f --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rmdir.rs @@ -0,0 +1,86 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_rmdir_enabled; +use crate::rules::flake8_use_pathlib::helpers::{ + check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default, +}; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.rmdir`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os`. When possible, using `Path` object +/// methods such as `Path.rmdir()` can improve readability over the `os` +/// module's counterparts (e.g., `os.rmdir()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.rmdir("folder/") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("folder/").rmdir() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir) +/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsRmdir; + +impl Violation for OsRmdir { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.rmdir()` should be replaced by `Path.rmdir()`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).rmdir()`".to_string()) + } +} + +/// PTH106 +pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir) + // ```text + // 0 1 + // os.rmdir(path, *, dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + + if segments != ["os", "rmdir"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "rmdir()", + "path", + is_fix_os_rmdir_enabled(checker.settings()), + OsRmdir, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs index e8ab662f15451..f6e01d037cb67 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprAttribute}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -94,5 +94,5 @@ pub(crate) fn os_sep_split(checker: &Checker, call: &ast::ExprCall) { return; } - checker.report_diagnostic(Diagnostic::new(OsSepSplit, attr.range())); + checker.report_diagnostic(OsSepSplit, attr.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs new file mode 100644 index 0000000000000..5d97338e880d5 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_unlink.rs @@ -0,0 +1,86 @@ +use crate::checkers::ast::Checker; +use crate::preview::is_fix_os_unlink_enabled; +use crate::rules::flake8_use_pathlib::helpers::{ + check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default, +}; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for uses of `os.unlink`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os`. When possible, using `Path` object +/// methods such as `Path.unlink()` can improve readability over the `os` +/// module's counterparts (e.g., `os.unlink()`). +/// +/// ## Examples +/// ```python +/// import os +/// +/// os.unlink("file.py") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("file.py").unlink() +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// +/// ## References +/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink) +/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsUnlink; + +impl Violation for OsUnlink { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] + fn message(&self) -> String { + "`os.unlink()` should be replaced by `Path.unlink()`".to_string() + } + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).unlink()`".to_string()) + } +} + +/// PTH108 +pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink) + // ```text + // 0 1 + // os.unlink(path, *, dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + + if segments != ["os", "unlink"] { + return; + } + + check_os_pathlib_single_arg_calls( + checker, + call, + "unlink()", + "path", + is_fix_os_unlink_enabled(checker.settings()), + OsUnlink, + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs index 5f9c89bbb6240..5b448ebe96e38 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -1,7 +1,6 @@ use std::ops::Range; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{Expr, ExprBinOp, ExprCall, Operator}; use ruff_python_semantic::SemanticModel; @@ -9,7 +8,8 @@ use ruff_python_trivia::CommentRanges; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for `pathlib.Path` objects that are initialized with the current @@ -33,6 +33,10 @@ use crate::fix::edits::{remove_argument, Parentheses}; /// _ = Path() /// ``` /// +/// ## Fix safety +/// This fix is marked unsafe if there are comments inside the parentheses, as applying +/// the fix will delete them. +/// /// ## References /// - [Python documentation: `Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) #[derive(ViolationMetadata)] @@ -50,7 +54,11 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory { } /// PTH201 -pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprCall) { +pub(crate) fn path_constructor_current_directory( + checker: &Checker, + call: &ExprCall, + segments: &[&str], +) { let applicability = |range| { if checker.comment_ranges().intersects(range) { Applicability::Unsafe @@ -59,15 +67,9 @@ pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprC } }; - let (func, arguments) = (&call.func, &call.arguments); + let arguments = &call.arguments; - if !checker - .semantic() - .resolve_qualified_name(func) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["pathlib", "Path" | "PurePath"]) - }) - { + if !matches!(segments, ["pathlib", "Path" | "PurePath"]) { return; } @@ -83,7 +85,7 @@ pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprC return; } - let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, arg.range()); + let mut diagnostic = checker.report_diagnostic(PathConstructorCurrentDirectory, arg.range()); match parent_and_next_path_fragment_range( checker.semantic(), @@ -107,12 +109,16 @@ pub(crate) fn path_constructor_current_directory(checker: &Checker, call: &ExprC diagnostic.set_fix(Fix::applicable_edit(edit, applicability(parent_range))); } None => diagnostic.try_set_fix(|| { - let edit = remove_argument(arg, arguments, Parentheses::Preserve, checker.source())?; + let edit = remove_argument( + arg, + arguments, + Parentheses::Preserve, + checker.source(), + checker.comment_ranges(), + )?; Ok(Fix::applicable_edit(edit, applicability(call.range()))) }), } - - checker.report_diagnostic(diagnostic); } fn parent_and_next_path_fragment_range( diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 7d44916c89007..28ce248e2f7a6 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -1,171 +1,241 @@ -use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall}; -use ruff_python_semantic::analyze::typing; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::registry::AsRule; -use crate::rules::flake8_use_pathlib::rules::{ - Glob, OsPathGetatime, OsPathGetctime, OsPathGetmtime, OsPathGetsize, -}; +use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default; +use crate::rules::flake8_use_pathlib::rules::Glob; use crate::rules::flake8_use_pathlib::violations::{ - BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathAbspath, - OsPathBasename, OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir, - OsPathIsfile, OsPathIslink, OsPathJoin, OsPathSamefile, OsPathSplitext, OsReadlink, OsRemove, - OsRename, OsReplace, OsRmdir, OsStat, OsUnlink, PyPath, + BuiltinOpen, Joiner, OsChmod, OsGetcwd, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, + OsPathSamefile, OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath, }; -use ruff_python_ast::PythonVersion; pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { - if let Some(diagnostic_kind) = checker - .semantic() - .resolve_qualified_name(&call.func) - .and_then(|qualified_name| match qualified_name.segments() { - // PTH100 - ["os", "path", "abspath"] => Some(OsPathAbspath.into()), - // PTH101 - ["os", "chmod"] => Some(OsChmod.into()), - // PTH102 - ["os", "makedirs"] => Some(OsMakedirs.into()), - // PTH103 - ["os", "mkdir"] => Some(OsMkdir.into()), - // PTH104 - ["os", "rename"] => Some(OsRename.into()), - // PTH105 - ["os", "replace"] => Some(OsReplace.into()), - // PTH106 - ["os", "rmdir"] => Some(OsRmdir.into()), - // PTH107 - ["os", "remove"] => Some(OsRemove.into()), - // PTH108 - ["os", "unlink"] => Some(OsUnlink.into()), - // PTH109 - ["os", "getcwd"] => Some(OsGetcwd.into()), - ["os", "getcwdb"] => Some(OsGetcwd.into()), - // PTH110 - ["os", "path", "exists"] => Some(OsPathExists.into()), - // PTH111 - ["os", "path", "expanduser"] => Some(OsPathExpanduser.into()), - // PTH112 - ["os", "path", "isdir"] => Some(OsPathIsdir.into()), - // PTH113 - ["os", "path", "isfile"] => Some(OsPathIsfile.into()), - // PTH114 - ["os", "path", "islink"] => Some(OsPathIslink.into()), - // PTH116 - ["os", "stat"] => Some(OsStat.into()), - // PTH117 - ["os", "path", "isabs"] => Some(OsPathIsabs.into()), - // PTH118 - ["os", "path", "join"] => Some( - OsPathJoin { - module: "path".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash - }, - } - .into(), - ), - ["os", "sep", "join"] => Some( - OsPathJoin { - module: "sep".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash - }, - } - .into(), - ), - // PTH119 - ["os", "path", "basename"] => Some(OsPathBasename.into()), - // PTH120 - ["os", "path", "dirname"] => Some(OsPathDirname.into()), - // PTH121 - ["os", "path", "samefile"] => Some(OsPathSamefile.into()), - // PTH122 - ["os", "path", "splitext"] => Some(OsPathSplitext.into()), - // PTH202 - ["os", "path", "getsize"] => Some(OsPathGetsize.into()), - // PTH203 - ["os", "path", "getatime"] => Some(OsPathGetatime.into()), - // PTH204 - ["os", "path", "getmtime"] => Some(OsPathGetmtime.into()), - // PTH205 - ["os", "path", "getctime"] => Some(OsPathGetctime.into()), - // PTH123 - ["" | "builtins", "open"] => { - // `closefd` and `opener` are not supported by pathlib, so check if they are - // are set to non-default values. - // https://github.com/astral-sh/ruff/issues/7620 - // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): - // ```text - // 0 1 2 3 4 5 - // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, - // 6 7 - // closefd=True, opener=None) - // ^^^^ ^^^^ - // ``` - // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): - // ```text - // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) - // ``` - if call + let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) else { + return; + }; + + let range = call.func.range(); + match qualified_name.segments() { + // PTH101 + ["os", "chmod"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod) + // ```text + // 0 1 2 3 + // os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True) + // ``` + if call + .arguments + .find_argument_value("path", 0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + || is_keyword_only_argument_non_default(&call.arguments, "dir_fd") + { + return; + } + checker.report_diagnostic_if_enabled(OsChmod, range) + } + // PTH102 + ["os", "makedirs"] => checker.report_diagnostic_if_enabled(OsMakedirs, range), + // PTH103 + ["os", "mkdir"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.mkdir) + // ```text + // 0 1 2 + // os.mkdir(path, mode=0o777, *, dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + checker.report_diagnostic_if_enabled(OsMkdir, range) + } + // PTH104 + ["os", "rename"] => { + // `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are + // set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename) + // ```text + // 0 1 2 3 + // os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd") + || is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd") + { + return; + } + checker.report_diagnostic_if_enabled(OsRename, range) + } + // PTH105 + ["os", "replace"] => { + // `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are + // set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace) + // ```text + // 0 1 2 3 + // os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd") + || is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd") + { + return; + } + checker.report_diagnostic_if_enabled(OsReplace, range) + } + // PTH109 + ["os", "getcwd"] => checker.report_diagnostic_if_enabled(OsGetcwd, range), + ["os", "getcwdb"] => checker.report_diagnostic_if_enabled(OsGetcwd, range), + + // PTH116 + ["os", "stat"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.stat) + // ```text + // 0 1 2 + // os.stat(path, *, dir_fd=None, follow_symlinks=True) + // ``` + if call + .arguments + .find_argument_value("path", 0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + || is_keyword_only_argument_non_default(&call.arguments, "dir_fd") + { + return; + } + checker.report_diagnostic_if_enabled(OsStat, range) + } + // PTH118 + ["os", "path", "join"] => checker.report_diagnostic_if_enabled( + OsPathJoin { + module: "path".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, + }, + range, + ), + ["os", "sep", "join"] => checker.report_diagnostic_if_enabled( + OsPathJoin { + module: "sep".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, + }, + range, + ), + // PTH121 + ["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range), + // PTH122 + ["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range), + // PTH211 + ["os", "symlink"] => { + // `dir_fd` is not supported by pathlib, so check if there are non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.symlink) + // ```text + // 0 1 2 3 + // os.symlink(src, dst, target_is_directory=False, *, dir_fd=None) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + checker.report_diagnostic_if_enabled(OsSymlink, range) + } + + // PTH123 + ["" | "builtins", "open"] => { + // `closefd` and `opener` are not supported by pathlib, so check if they + // are set to non-default values. + // https://github.com/astral-sh/ruff/issues/7620 + // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): + // ```text + // 0 1 2 3 4 5 + // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, + // 6 7 + // closefd=True, opener=None) + // ^^^^ ^^^^ + // ``` + // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): + // ```text + // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) + // ``` + if call + .arguments + .find_argument_value("closefd", 6) + .is_some_and(|expr| { + !matches!( + expr, + Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) + ) + }) + || is_argument_non_default(&call.arguments, "opener", 7) + || call .arguments - .find_argument_value("closefd", 6) - .is_some_and(|expr| { - !matches!( - expr, - Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) - ) - }) - || call - .arguments - .find_argument_value("opener", 7) - .is_some_and(|expr| !expr.is_none_literal_expr()) - || call - .arguments - .find_positional(0) - .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) - { - return None; - } - Some(BuiltinOpen.into()) + .find_argument_value("file", 0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + { + return; } - // PTH124 - ["py", "path", "local"] => Some(PyPath.into()), - // PTH207 - ["glob", "glob"] => Some( + checker.report_diagnostic_if_enabled(BuiltinOpen, range) + } + // PTH124 + ["py", "path", "local"] => checker.report_diagnostic_if_enabled(PyPath, range), + // PTH207 + ["glob", "glob"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.glob) + // ```text + // 0 1 2 3 4 + // glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + + checker.report_diagnostic_if_enabled( Glob { function: "glob".to_string(), - } - .into(), - ), - ["glob", "iglob"] => Some( + }, + range, + ) + } + + ["glob", "iglob"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.iglob) + // ```text + // 0 1 2 3 4 + // glob.iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False) + // ``` + if is_keyword_only_argument_non_default(&call.arguments, "dir_fd") { + return; + } + + checker.report_diagnostic_if_enabled( Glob { function: "iglob".to_string(), - } - .into(), - ), - // PTH115 - // Python 3.9+ - ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => { - Some(OsReadlink.into()) + }, + range, + ) + } + // PTH208 + ["os", "listdir"] => { + if call + .arguments + .find_argument_value("path", 0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + { + return; } - // PTH208, - ["os", "listdir"] => Some(OsListdir.into()), - _ => None, - }) - { - let diagnostic = Diagnostic::new::(diagnostic_kind, call.func.range()); - - if checker.enabled(diagnostic.kind.rule()) { - checker.report_diagnostic(diagnostic); + checker.report_diagnostic_if_enabled(OsListdir, range) } - } + + _ => return, + }; } /// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer. @@ -180,7 +250,7 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { return true; } - let Some(name) = expr.as_name_expr() else { + let Some(name) = get_name_expr(expr) else { return false; }; @@ -190,3 +260,18 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { typing::is_int(binding, semantic) } + +fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { + match expr { + Expr::Name(name) => Some(name), + Expr::Call(ExprCall { func, .. }) => get_name_expr(func), + _ => None, + } +} + +/// Returns `true` if argument `name` is set to a non-default `None` value. +fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool { + arguments + .find_argument_value(name, position) + .is_some_and(|expr| !expr.is_none_literal_expr()) +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202.py.snap index 4f2fd0ce76387..4ae823b9e130d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202.py.snap @@ -1,74 +1,333 @@ --- source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs --- -PTH202.py:6:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` - | -6 | os.path.getsize("filename") - | ^^^^^^^^^^^^^^^ PTH202 -7 | os.path.getsize(b"filename") -8 | os.path.getsize(Path("filename")) - | - -PTH202.py:7:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` - | -6 | os.path.getsize("filename") -7 | os.path.getsize(b"filename") - | ^^^^^^^^^^^^^^^ PTH202 -8 | os.path.getsize(Path("filename")) -9 | os.path.getsize(__file__) - | - -PTH202.py:8:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` - | -6 | os.path.getsize("filename") -7 | os.path.getsize(b"filename") -8 | os.path.getsize(Path("filename")) - | ^^^^^^^^^^^^^^^ PTH202 -9 | os.path.getsize(__file__) - | - -PTH202.py:9:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` - | - 7 | os.path.getsize(b"filename") - 8 | os.path.getsize(Path("filename")) - 9 | os.path.getsize(__file__) - | ^^^^^^^^^^^^^^^ PTH202 -10 | -11 | getsize("filename") +PTH202.py:10:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` | +10 | os.path.getsize("filename") + | ^^^^^^^^^^^^^^^ PTH202 +11 | os.path.getsize(b"filename") +12 | os.path.getsize(Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` PTH202.py:11:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` | - 9 | os.path.getsize(__file__) -10 | -11 | getsize("filename") - | ^^^^^^^ PTH202 -12 | getsize(b"filename") -13 | getsize(Path("filename")) +10 | os.path.getsize("filename") +11 | os.path.getsize(b"filename") + | ^^^^^^^^^^^^^^^ PTH202 +12 | os.path.getsize(Path("filename")) +13 | os.path.getsize(__file__) | + = help: Replace with `Path(...).stat().st_size` PTH202.py:12:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` | -11 | getsize("filename") -12 | getsize(b"filename") - | ^^^^^^^ PTH202 -13 | getsize(Path("filename")) -14 | getsize(__file__) +10 | os.path.getsize("filename") +11 | os.path.getsize(b"filename") +12 | os.path.getsize(Path("filename")) + | ^^^^^^^^^^^^^^^ PTH202 +13 | os.path.getsize(__file__) | + = help: Replace with `Path(...).stat().st_size` PTH202.py:13:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` | -11 | getsize("filename") -12 | getsize(b"filename") -13 | getsize(Path("filename")) +11 | os.path.getsize(b"filename") +12 | os.path.getsize(Path("filename")) +13 | os.path.getsize(__file__) + | ^^^^^^^^^^^^^^^ PTH202 +14 | +15 | os.path.getsize(filename) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:15:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +13 | os.path.getsize(__file__) +14 | +15 | os.path.getsize(filename) + | ^^^^^^^^^^^^^^^ PTH202 +16 | os.path.getsize(filename1) +17 | os.path.getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:16:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +15 | os.path.getsize(filename) +16 | os.path.getsize(filename1) + | ^^^^^^^^^^^^^^^ PTH202 +17 | os.path.getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:17:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +15 | os.path.getsize(filename) +16 | os.path.getsize(filename1) +17 | os.path.getsize(filename2) + | ^^^^^^^^^^^^^^^ PTH202 +18 | +19 | os.path.getsize(filename="filename") + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:19:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +17 | os.path.getsize(filename2) +18 | +19 | os.path.getsize(filename="filename") + | ^^^^^^^^^^^^^^^ PTH202 +20 | os.path.getsize(filename=b"filename") +21 | os.path.getsize(filename=Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:20:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +19 | os.path.getsize(filename="filename") +20 | os.path.getsize(filename=b"filename") + | ^^^^^^^^^^^^^^^ PTH202 +21 | os.path.getsize(filename=Path("filename")) +22 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:21:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +19 | os.path.getsize(filename="filename") +20 | os.path.getsize(filename=b"filename") +21 | os.path.getsize(filename=Path("filename")) + | ^^^^^^^^^^^^^^^ PTH202 +22 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:22:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +20 | os.path.getsize(filename=b"filename") +21 | os.path.getsize(filename=Path("filename")) +22 | os.path.getsize(filename=__file__) + | ^^^^^^^^^^^^^^^ PTH202 +23 | +24 | getsize("filename") + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:24:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +22 | os.path.getsize(filename=__file__) +23 | +24 | getsize("filename") + | ^^^^^^^ PTH202 +25 | getsize(b"filename") +26 | getsize(Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:25:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +24 | getsize("filename") +25 | getsize(b"filename") + | ^^^^^^^ PTH202 +26 | getsize(Path("filename")) +27 | getsize(__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:26:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +24 | getsize("filename") +25 | getsize(b"filename") +26 | getsize(Path("filename")) + | ^^^^^^^ PTH202 +27 | getsize(__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:27:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +25 | getsize(b"filename") +26 | getsize(Path("filename")) +27 | getsize(__file__) + | ^^^^^^^ PTH202 +28 | +29 | getsize(filename="filename") + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:29:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +27 | getsize(__file__) +28 | +29 | getsize(filename="filename") + | ^^^^^^^ PTH202 +30 | getsize(filename=b"filename") +31 | getsize(filename=Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:30:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +29 | getsize(filename="filename") +30 | getsize(filename=b"filename") + | ^^^^^^^ PTH202 +31 | getsize(filename=Path("filename")) +32 | getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:31:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +29 | getsize(filename="filename") +30 | getsize(filename=b"filename") +31 | getsize(filename=Path("filename")) | ^^^^^^^ PTH202 -14 | getsize(__file__) +32 | getsize(filename=__file__) | + = help: Replace with `Path(...).stat().st_size` -PTH202.py:14:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` +PTH202.py:32:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` | -12 | getsize(b"filename") -13 | getsize(Path("filename")) -14 | getsize(__file__) +30 | getsize(filename=b"filename") +31 | getsize(filename=Path("filename")) +32 | getsize(filename=__file__) | ^^^^^^^ PTH202 +33 | +34 | getsize(filename) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:34:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +32 | getsize(filename=__file__) +33 | +34 | getsize(filename) + | ^^^^^^^ PTH202 +35 | getsize(filename1) +36 | getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:35:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +34 | getsize(filename) +35 | getsize(filename1) + | ^^^^^^^ PTH202 +36 | getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:36:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +34 | getsize(filename) +35 | getsize(filename1) +36 | getsize(filename2) + | ^^^^^^^ PTH202 + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:39:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +39 | os.path.getsize( + | ^^^^^^^^^^^^^^^ PTH202 +40 | "filename", # comment +41 | ) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:43:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +41 | ) +42 | +43 | os.path.getsize( + | ^^^^^^^^^^^^^^^ PTH202 +44 | # comment +45 | "filename" + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:50:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +48 | ) +49 | +50 | os.path.getsize( + | ^^^^^^^^^^^^^^^ PTH202 +51 | # comment +52 | b"filename" + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:56:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +54 | ) +55 | +56 | os.path.getsize( # comment + | ^^^^^^^^^^^^^^^ PTH202 +57 | Path(__file__) +58 | # comment + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:61:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +59 | ) # comment +60 | +61 | getsize( # comment + | ^^^^^^^ PTH202 +62 | "filename") + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:64:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +62 | "filename") +63 | +64 | getsize( # comment + | ^^^^^^^ PTH202 +65 | b"filename", +66 | #comment + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:69:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +67 | ) +68 | +69 | os.path.getsize("file" + "name") + | ^^^^^^^^^^^^^^^ PTH202 +70 | +71 | getsize \ + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:71:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +69 | os.path.getsize("file" + "name") +70 | +71 | getsize \ + | ^^^^^^^ PTH202 +72 | \ +73 | \ + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:78:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +76 | ) +77 | +78 | getsize(Path("filename").resolve()) + | ^^^^^^^ PTH202 +79 | +80 | import pathlib + | + = help: Replace with `Path(...).stat().st_size` + +PTH202.py:82:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +80 | import pathlib +81 | +82 | os.path.getsize(pathlib.Path("filename")) + | ^^^^^^^^^^^^^^^ PTH202 | + = help: Replace with `Path(...).stat().st_size` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202_2.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202_2.py.snap new file mode 100644 index 0000000000000..a5c3c8b7c2aa2 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH202_PTH202_2.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH202_2.py:3:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +1 | import os +2 | +3 | os.path.getsize(filename="filename") + | ^^^^^^^^^^^^^^^ PTH202 +4 | os.path.getsize(filename=b"filename") +5 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202_2.py:4:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +3 | os.path.getsize(filename="filename") +4 | os.path.getsize(filename=b"filename") + | ^^^^^^^^^^^^^^^ PTH202 +5 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +PTH202_2.py:5:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size` + | +3 | os.path.getsize(filename="filename") +4 | os.path.getsize(filename=b"filename") +5 | os.path.getsize(filename=__file__) + | ^^^^^^^^^^^^^^^ PTH202 + | + = help: Replace with `Path(...).stat().st_size` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH203_PTH203.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH203_PTH203.py.snap index 8851d9d32ab15..112cb7933a48e 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH203_PTH203.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH203_PTH203.py.snap @@ -10,6 +10,7 @@ PTH203.py:5:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a 6 | os.path.getatime(b"filename") 7 | os.path.getatime(Path("filename")) | + = help: Replace with `Path.stat(...).st_atime` PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` | @@ -18,6 +19,7 @@ PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a | ^^^^^^^^^^^^^^^^ PTH203 7 | os.path.getatime(Path("filename")) | + = help: Replace with `Path.stat(...).st_atime` PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` | @@ -26,6 +28,7 @@ PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_a 7 | os.path.getatime(Path("filename")) | ^^^^^^^^^^^^^^^^ PTH203 | + = help: Replace with `Path.stat(...).st_atime` PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` | @@ -34,6 +37,7 @@ PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_ 11 | getatime(b"filename") 12 | getatime(Path("filename")) | + = help: Replace with `Path.stat(...).st_atime` PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` | @@ -42,6 +46,7 @@ PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_ | ^^^^^^^^ PTH203 12 | getatime(Path("filename")) | + = help: Replace with `Path.stat(...).st_atime` PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` | @@ -50,3 +55,88 @@ PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_ 12 | getatime(Path("filename")) | ^^^^^^^^ PTH203 | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:17:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +15 | file = __file__ +16 | +17 | os.path.getatime(file) + | ^^^^^^^^^^^^^^^^ PTH203 +18 | os.path.getatime(filename="filename") +19 | os.path.getatime(filename=Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:18:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +17 | os.path.getatime(file) +18 | os.path.getatime(filename="filename") + | ^^^^^^^^^^^^^^^^ PTH203 +19 | os.path.getatime(filename=Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:19:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +17 | os.path.getatime(file) +18 | os.path.getatime(filename="filename") +19 | os.path.getatime(filename=Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH203 +20 | +21 | os.path.getatime( # comment 1 + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:21:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +19 | os.path.getatime(filename=Path("filename")) +20 | +21 | os.path.getatime( # comment 1 + | ^^^^^^^^^^^^^^^^ PTH203 +22 | # comment 2 +23 | "filename" # comment 3 + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:29:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +27 | ) # comment 7 +28 | +29 | os.path.getatime("file" + "name") + | ^^^^^^^^^^^^^^^^ PTH203 +30 | +31 | getatime(Path("filename").resolve()) + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:31:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +29 | os.path.getatime("file" + "name") +30 | +31 | getatime(Path("filename").resolve()) + | ^^^^^^^^ PTH203 +32 | +33 | os.path.getatime(pathlib.Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:33:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +31 | getatime(Path("filename").resolve()) +32 | +33 | os.path.getatime(pathlib.Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH203 +34 | +35 | getatime(Path("dir") / "file.txt") + | + = help: Replace with `Path.stat(...).st_atime` + +PTH203.py:35:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +33 | os.path.getatime(pathlib.Path("filename")) +34 | +35 | getatime(Path("dir") / "file.txt") + | ^^^^^^^^ PTH203 + | + = help: Replace with `Path.stat(...).st_atime` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH204_PTH204.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH204_PTH204.py.snap index b137cbd772ff7..7e3b24248654b 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH204_PTH204.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH204_PTH204.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs -snapshot_kind: text --- PTH204.py:6:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` | @@ -9,6 +8,7 @@ PTH204.py:6:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m 7 | os.path.getmtime(b"filename") 8 | os.path.getmtime(Path("filename")) | + = help: Replace with `Path.stat(...).st_mtime` PTH204.py:7:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` | @@ -17,6 +17,7 @@ PTH204.py:7:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m | ^^^^^^^^^^^^^^^^ PTH204 8 | os.path.getmtime(Path("filename")) | + = help: Replace with `Path.stat(...).st_mtime` PTH204.py:8:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` | @@ -25,6 +26,7 @@ PTH204.py:8:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_m 8 | os.path.getmtime(Path("filename")) | ^^^^^^^^^^^^^^^^ PTH204 | + = help: Replace with `Path.stat(...).st_mtime` PTH204.py:11:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` | @@ -33,6 +35,7 @@ PTH204.py:11:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_ 12 | getmtime(b"filename") 13 | getmtime(Path("filename")) | + = help: Replace with `Path.stat(...).st_mtime` PTH204.py:12:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` | @@ -41,6 +44,7 @@ PTH204.py:12:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_ | ^^^^^^^^ PTH204 13 | getmtime(Path("filename")) | + = help: Replace with `Path.stat(...).st_mtime` PTH204.py:13:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_mtime` | @@ -49,3 +53,4 @@ PTH204.py:13:1: PTH204 `os.path.getmtime` should be replaced by `Path.stat().st_ 13 | getmtime(Path("filename")) | ^^^^^^^^ PTH204 | + = help: Replace with `Path.stat(...).st_mtime` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH205_PTH205.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH205_PTH205.py.snap index 3edeac694e549..a8b523d0ce245 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH205_PTH205.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH205_PTH205.py.snap @@ -8,6 +8,7 @@ PTH205.py:6:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c 7 | os.path.getctime(b"filename") 8 | os.path.getctime(Path("filename")) | + = help: Replace with `Path.stat(...).st_ctime` PTH205.py:7:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime` | @@ -16,6 +17,7 @@ PTH205.py:7:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c | ^^^^^^^^^^^^^^^^ PTH205 8 | os.path.getctime(Path("filename")) | + = help: Replace with `Path.stat(...).st_ctime` PTH205.py:8:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime` | @@ -26,6 +28,7 @@ PTH205.py:8:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_c 9 | 10 | getctime("filename") | + = help: Replace with `Path.stat(...).st_ctime` PTH205.py:10:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime` | @@ -36,6 +39,7 @@ PTH205.py:10:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ 11 | getctime(b"filename") 12 | getctime(Path("filename")) | + = help: Replace with `Path.stat(...).st_ctime` PTH205.py:11:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime` | @@ -44,6 +48,7 @@ PTH205.py:11:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ | ^^^^^^^^ PTH205 12 | getctime(Path("filename")) | + = help: Replace with `Path.stat(...).st_ctime` PTH205.py:12:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ctime` | @@ -52,3 +57,4 @@ PTH205.py:12:1: PTH205 `os.path.getctime` should be replaced by `Path.stat().st_ 12 | getctime(Path("filename")) | ^^^^^^^^ PTH205 | + = help: Replace with `Path.stat(...).st_ctime` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap index 6d30e0fb5e555..ddcba2ba597ee 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs -snapshot_kind: text --- PTH207.py:9:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob` | @@ -26,4 +25,6 @@ PTH207.py:11:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob` 10 | list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))) 11 | search("*.png") | ^^^^^^ PTH207 +12 | +13 | # if `dir_fd` is set, suppress the diagnostic | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH211_PTH211.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH211_PTH211.py.snap new file mode 100644 index 0000000000000..76ac48db2cd47 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH211_PTH211.py.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH211.py:5:1: PTH211 `os.symlink` should be replaced by `Path.symlink_to` + | +5 | os.symlink("usr/bin/python", "tmp/python") + | ^^^^^^^^^^ PTH211 +6 | os.symlink(b"usr/bin/python", b"tmp/python") +7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok + | + +PTH211.py:6:1: PTH211 `os.symlink` should be replaced by `Path.symlink_to` + | +5 | os.symlink("usr/bin/python", "tmp/python") +6 | os.symlink(b"usr/bin/python", b"tmp/python") + | ^^^^^^^^^^ PTH211 +7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok + | + +PTH211.py:9:1: PTH211 `os.symlink` should be replaced by `Path.symlink_to` + | + 7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok + 8 | + 9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True) + | ^^^^^^^^^^ PTH211 +10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True) +11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok + | + +PTH211.py:10:1: PTH211 `os.symlink` should be replaced by `Path.symlink_to` + | + 9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True) +10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True) + | ^^^^^^^^^^ PTH211 +11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok + | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap index e0d29b06be0f9..f02d3de37eec6 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap @@ -10,6 +10,7 @@ full_name.py:7:5: PTH100 `os.path.abspath()` should be replaced by `Path.resolve 8 | aa = os.chmod(p) 9 | aaa = os.mkdir(p) | + = help: Replace with `Path(...).resolve()` full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` | @@ -69,6 +70,7 @@ full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` 14 | os.remove(p) 15 | os.unlink(p) | + = help: Replace with `Path(...).rmdir()` full_name.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` | @@ -79,6 +81,7 @@ full_name.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` 15 | os.unlink(p) 16 | os.getcwd(p) | + = help: Replace with `Path(...).unlink()` full_name.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` | @@ -89,6 +92,7 @@ full_name.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` 16 | os.getcwd(p) 17 | b = os.path.exists(p) | + = help: Replace with `Path(...).unlink()` full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` | @@ -109,6 +113,7 @@ full_name.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists( 18 | bb = os.path.expanduser(p) 19 | bbb = os.path.isdir(p) | + = help: Replace with `Path(...).exists()` full_name.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()` | @@ -119,6 +124,7 @@ full_name.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.exp 19 | bbb = os.path.isdir(p) 20 | bbbb = os.path.isfile(p) | + = help: Replace with `Path(...).expanduser()` full_name.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()` | @@ -129,6 +135,7 @@ full_name.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir() 20 | bbbb = os.path.isfile(p) 21 | bbbbb = os.path.islink(p) | + = help: Replace with `Path(...).is_dir()` full_name.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()` | @@ -139,6 +146,7 @@ full_name.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file 21 | bbbbb = os.path.islink(p) 22 | os.readlink(p) | + = help: Replace with `Path(...).is_file()` full_name.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()` | @@ -149,6 +157,7 @@ full_name.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_syml 22 | os.readlink(p) 23 | os.stat(p) | + = help: Replace with `Path(...).is_symlink()` full_name.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()` | @@ -159,6 +168,7 @@ full_name.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink() 23 | os.stat(p) 24 | os.path.isabs(p) | + = help: Replace with `Path(...).readlink()` full_name.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` | @@ -179,6 +189,7 @@ full_name.py:24:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_absol 25 | os.path.join(p, q) 26 | os.sep.join([p, q]) | + = help: Replace with `Path(...).is_absolute()` full_name.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator | @@ -219,6 +230,7 @@ full_name.py:28:1: PTH119 `os.path.basename()` should be replaced by `Path.name` 29 | os.path.dirname(p) 30 | os.path.samefile(p) | + = help: Replace with `Path(...).name` full_name.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent` | @@ -229,6 +241,7 @@ full_name.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent 30 | os.path.samefile(p) 31 | os.path.splitext(p) | + = help: Replace with `Path(...).parent` full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` | @@ -317,3 +330,33 @@ full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()` | ^^^^ PTH123 48 | open(p, 'r', - 1, None, None, None, False, opener) | + +full_name.py:65:1: PTH123 `open()` should be replaced by `Path.open()` + | +63 | open(f()) +64 | +65 | open(b"foo") + | ^^^^ PTH123 +66 | byte_str = b"bar" +67 | open(byte_str) + | + +full_name.py:67:1: PTH123 `open()` should be replaced by `Path.open()` + | +65 | open(b"foo") +66 | byte_str = b"bar" +67 | open(byte_str) + | ^^^^ PTH123 +68 | +69 | def bytes_str_func() -> bytes: + | + +full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()` + | +69 | def bytes_str_func() -> bytes: +70 | return b"foo" +71 | open(bytes_str_func()) + | ^^^^ PTH123 +72 | +73 | # https://github.com/astral-sh/ruff/issues/17693 + | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_as.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_as.py.snap index dcf8d8a659931..aedda0b51deda 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_as.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_as.py.snap @@ -10,6 +10,7 @@ import_as.py:7:5: PTH100 `os.path.abspath()` should be replaced by `Path.resolve 8 | aa = foo.chmod(p) 9 | aaa = foo.mkdir(p) | + = help: Replace with `Path(...).resolve()` import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` | @@ -69,6 +70,7 @@ import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` 14 | foo.remove(p) 15 | foo.unlink(p) | + = help: Replace with `Path(...).rmdir()` import_as.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` | @@ -79,6 +81,7 @@ import_as.py:14:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` 15 | foo.unlink(p) 16 | foo.getcwd(p) | + = help: Replace with `Path(...).unlink()` import_as.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` | @@ -89,6 +92,7 @@ import_as.py:15:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` 16 | foo.getcwd(p) 17 | b = foo_p.exists(p) | + = help: Replace with `Path(...).unlink()` import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` | @@ -109,6 +113,7 @@ import_as.py:17:5: PTH110 `os.path.exists()` should be replaced by `Path.exists( 18 | bb = foo_p.expanduser(p) 19 | bbb = foo_p.isdir(p) | + = help: Replace with `Path(...).exists()` import_as.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()` | @@ -119,6 +124,7 @@ import_as.py:18:6: PTH111 `os.path.expanduser()` should be replaced by `Path.exp 19 | bbb = foo_p.isdir(p) 20 | bbbb = foo_p.isfile(p) | + = help: Replace with `Path(...).expanduser()` import_as.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()` | @@ -129,6 +135,7 @@ import_as.py:19:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir() 20 | bbbb = foo_p.isfile(p) 21 | bbbbb = foo_p.islink(p) | + = help: Replace with `Path(...).is_dir()` import_as.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()` | @@ -139,6 +146,7 @@ import_as.py:20:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file 21 | bbbbb = foo_p.islink(p) 22 | foo.readlink(p) | + = help: Replace with `Path(...).is_file()` import_as.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()` | @@ -149,6 +157,7 @@ import_as.py:21:9: PTH114 `os.path.islink()` should be replaced by `Path.is_syml 22 | foo.readlink(p) 23 | foo.stat(p) | + = help: Replace with `Path(...).is_symlink()` import_as.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()` | @@ -159,6 +168,7 @@ import_as.py:22:1: PTH115 `os.readlink()` should be replaced by `Path.readlink() 23 | foo.stat(p) 24 | foo_p.isabs(p) | + = help: Replace with `Path(...).readlink()` import_as.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` | @@ -179,6 +189,7 @@ import_as.py:24:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_absol 25 | foo_p.join(p, q) 26 | foo.sep.join([p, q]) | + = help: Replace with `Path(...).is_absolute()` import_as.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator | @@ -219,6 +230,7 @@ import_as.py:28:1: PTH119 `os.path.basename()` should be replaced by `Path.name` 29 | foo_p.dirname(p) 30 | foo_p.samefile(p) | + = help: Replace with `Path(...).name` import_as.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent` | @@ -229,6 +241,7 @@ import_as.py:29:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent 30 | foo_p.samefile(p) 31 | foo_p.splitext(p) | + = help: Replace with `Path(...).parent` import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap index 0cacb90179cd8..18a2370618df7 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap @@ -10,6 +10,7 @@ import_from.py:9:5: PTH100 `os.path.abspath()` should be replaced by `Path.resol 10 | aa = chmod(p) 11 | aaa = mkdir(p) | + = help: Replace with `Path(...).resolve()` import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` | @@ -69,6 +70,7 @@ import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` 16 | remove(p) 17 | unlink(p) | + = help: Replace with `Path(...).rmdir()` import_from.py:16:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` | @@ -79,6 +81,7 @@ import_from.py:16:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` 17 | unlink(p) 18 | getcwd(p) | + = help: Replace with `Path(...).unlink()` import_from.py:17:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` | @@ -89,6 +92,7 @@ import_from.py:17:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` 18 | getcwd(p) 19 | b = exists(p) | + = help: Replace with `Path(...).unlink()` import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` | @@ -109,6 +113,7 @@ import_from.py:19:5: PTH110 `os.path.exists()` should be replaced by `Path.exist 20 | bb = expanduser(p) 21 | bbb = isdir(p) | + = help: Replace with `Path(...).exists()` import_from.py:20:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()` | @@ -119,6 +124,7 @@ import_from.py:20:6: PTH111 `os.path.expanduser()` should be replaced by `Path.e 21 | bbb = isdir(p) 22 | bbbb = isfile(p) | + = help: Replace with `Path(...).expanduser()` import_from.py:21:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()` | @@ -129,6 +135,7 @@ import_from.py:21:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir 22 | bbbb = isfile(p) 23 | bbbbb = islink(p) | + = help: Replace with `Path(...).is_dir()` import_from.py:22:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()` | @@ -139,6 +146,7 @@ import_from.py:22:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_fi 23 | bbbbb = islink(p) 24 | readlink(p) | + = help: Replace with `Path(...).is_file()` import_from.py:23:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()` | @@ -149,6 +157,7 @@ import_from.py:23:9: PTH114 `os.path.islink()` should be replaced by `Path.is_sy 24 | readlink(p) 25 | stat(p) | + = help: Replace with `Path(...).is_symlink()` import_from.py:24:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()` | @@ -159,6 +168,7 @@ import_from.py:24:1: PTH115 `os.readlink()` should be replaced by `Path.readlink 25 | stat(p) 26 | isabs(p) | + = help: Replace with `Path(...).readlink()` import_from.py:25:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` | @@ -179,6 +189,7 @@ import_from.py:26:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_abs 27 | join(p, q) 28 | sep.join((p, q)) | + = help: Replace with `Path(...).is_absolute()` import_from.py:27:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator | @@ -219,6 +230,7 @@ import_from.py:30:1: PTH119 `os.path.basename()` should be replaced by `Path.nam 31 | dirname(p) 32 | samefile(p) | + = help: Replace with `Path(...).name` import_from.py:31:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent` | @@ -229,6 +241,7 @@ import_from.py:31:1: PTH120 `os.path.dirname()` should be replaced by `Path.pare 32 | samefile(p) 33 | splitext(p) | + = help: Replace with `Path(...).parent` import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from_as.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from_as.py.snap index 7f9fc380f4b97..f3b946a72c0d4 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from_as.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from_as.py.snap @@ -10,6 +10,7 @@ import_from_as.py:14:5: PTH100 `os.path.abspath()` should be replaced by `Path.r 15 | aa = xchmod(p) 16 | aaa = xmkdir(p) | + = help: Replace with `Path(...).resolve()` import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` | @@ -69,6 +70,7 @@ import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` 21 | xremove(p) 22 | xunlink(p) | + = help: Replace with `Path(...).rmdir()` import_from_as.py:21:1: PTH107 `os.remove()` should be replaced by `Path.unlink()` | @@ -79,6 +81,7 @@ import_from_as.py:21:1: PTH107 `os.remove()` should be replaced by `Path.unlink( 22 | xunlink(p) 23 | xgetcwd(p) | + = help: Replace with `Path(...).unlink()` import_from_as.py:22:1: PTH108 `os.unlink()` should be replaced by `Path.unlink()` | @@ -89,6 +92,7 @@ import_from_as.py:22:1: PTH108 `os.unlink()` should be replaced by `Path.unlink( 23 | xgetcwd(p) 24 | b = xexists(p) | + = help: Replace with `Path(...).unlink()` import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` | @@ -109,6 +113,7 @@ import_from_as.py:24:5: PTH110 `os.path.exists()` should be replaced by `Path.ex 25 | bb = xexpanduser(p) 26 | bbb = xisdir(p) | + = help: Replace with `Path(...).exists()` import_from_as.py:25:6: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()` | @@ -119,6 +124,7 @@ import_from_as.py:25:6: PTH111 `os.path.expanduser()` should be replaced by `Pat 26 | bbb = xisdir(p) 27 | bbbb = xisfile(p) | + = help: Replace with `Path(...).expanduser()` import_from_as.py:26:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_dir()` | @@ -129,6 +135,7 @@ import_from_as.py:26:7: PTH112 `os.path.isdir()` should be replaced by `Path.is_ 27 | bbbb = xisfile(p) 28 | bbbbb = xislink(p) | + = help: Replace with `Path(...).is_dir()` import_from_as.py:27:8: PTH113 `os.path.isfile()` should be replaced by `Path.is_file()` | @@ -139,6 +146,7 @@ import_from_as.py:27:8: PTH113 `os.path.isfile()` should be replaced by `Path.is 28 | bbbbb = xislink(p) 29 | xreadlink(p) | + = help: Replace with `Path(...).is_file()` import_from_as.py:28:9: PTH114 `os.path.islink()` should be replaced by `Path.is_symlink()` | @@ -149,6 +157,7 @@ import_from_as.py:28:9: PTH114 `os.path.islink()` should be replaced by `Path.is 29 | xreadlink(p) 30 | xstat(p) | + = help: Replace with `Path(...).is_symlink()` import_from_as.py:29:1: PTH115 `os.readlink()` should be replaced by `Path.readlink()` | @@ -159,6 +168,7 @@ import_from_as.py:29:1: PTH115 `os.readlink()` should be replaced by `Path.readl 30 | xstat(p) 31 | xisabs(p) | + = help: Replace with `Path(...).readlink()` import_from_as.py:30:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` | @@ -179,6 +189,7 @@ import_from_as.py:31:1: PTH117 `os.path.isabs()` should be replaced by `Path.is_ 32 | xjoin(p, q) 33 | s.join((p, q)) | + = help: Replace with `Path(...).is_absolute()` import_from_as.py:32:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator | @@ -219,6 +230,7 @@ import_from_as.py:35:1: PTH119 `os.path.basename()` should be replaced by `Path. 36 | xdirname(p) 37 | xsamefile(p) | + = help: Replace with `Path(...).name` import_from_as.py:36:1: PTH120 `os.path.dirname()` should be replaced by `Path.parent` | @@ -229,6 +241,7 @@ import_from_as.py:36:1: PTH120 `os.path.dirname()` should be replaced by `Path.p 37 | xsamefile(p) 38 | xsplitext(p) | + = help: Replace with `Path(...).parent` import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH202_PTH202.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH202_PTH202.py.snap new file mode 100644 index 0000000000000..35a650e9db118 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH202_PTH202.py.snap @@ -0,0 +1,673 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH202.py:10:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +10 | os.path.getsize("filename") + | ^^^^^^^^^^^^^^^ PTH202 +11 | os.path.getsize(b"filename") +12 | os.path.getsize(Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +7 7 | filename2 = Path("filename") +8 8 | +9 9 | +10 |-os.path.getsize("filename") + 10 |+Path("filename").stat().st_size +11 11 | os.path.getsize(b"filename") +12 12 | os.path.getsize(Path("filename")) +13 13 | os.path.getsize(__file__) + +PTH202.py:11:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +10 | os.path.getsize("filename") +11 | os.path.getsize(b"filename") + | ^^^^^^^^^^^^^^^ PTH202 +12 | os.path.getsize(Path("filename")) +13 | os.path.getsize(__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +8 8 | +9 9 | +10 10 | os.path.getsize("filename") +11 |-os.path.getsize(b"filename") + 11 |+Path(b"filename").stat().st_size +12 12 | os.path.getsize(Path("filename")) +13 13 | os.path.getsize(__file__) +14 14 | + +PTH202.py:12:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +10 | os.path.getsize("filename") +11 | os.path.getsize(b"filename") +12 | os.path.getsize(Path("filename")) + | ^^^^^^^^^^^^^^^ PTH202 +13 | os.path.getsize(__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +9 9 | +10 10 | os.path.getsize("filename") +11 11 | os.path.getsize(b"filename") +12 |-os.path.getsize(Path("filename")) + 12 |+Path("filename").stat().st_size +13 13 | os.path.getsize(__file__) +14 14 | +15 15 | os.path.getsize(filename) + +PTH202.py:13:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +11 | os.path.getsize(b"filename") +12 | os.path.getsize(Path("filename")) +13 | os.path.getsize(__file__) + | ^^^^^^^^^^^^^^^ PTH202 +14 | +15 | os.path.getsize(filename) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +10 10 | os.path.getsize("filename") +11 11 | os.path.getsize(b"filename") +12 12 | os.path.getsize(Path("filename")) +13 |-os.path.getsize(__file__) + 13 |+Path(__file__).stat().st_size +14 14 | +15 15 | os.path.getsize(filename) +16 16 | os.path.getsize(filename1) + +PTH202.py:15:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +13 | os.path.getsize(__file__) +14 | +15 | os.path.getsize(filename) + | ^^^^^^^^^^^^^^^ PTH202 +16 | os.path.getsize(filename1) +17 | os.path.getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +12 12 | os.path.getsize(Path("filename")) +13 13 | os.path.getsize(__file__) +14 14 | +15 |-os.path.getsize(filename) + 15 |+Path(filename).stat().st_size +16 16 | os.path.getsize(filename1) +17 17 | os.path.getsize(filename2) +18 18 | + +PTH202.py:16:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +15 | os.path.getsize(filename) +16 | os.path.getsize(filename1) + | ^^^^^^^^^^^^^^^ PTH202 +17 | os.path.getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +13 13 | os.path.getsize(__file__) +14 14 | +15 15 | os.path.getsize(filename) +16 |-os.path.getsize(filename1) + 16 |+Path(filename1).stat().st_size +17 17 | os.path.getsize(filename2) +18 18 | +19 19 | os.path.getsize(filename="filename") + +PTH202.py:17:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +15 | os.path.getsize(filename) +16 | os.path.getsize(filename1) +17 | os.path.getsize(filename2) + | ^^^^^^^^^^^^^^^ PTH202 +18 | +19 | os.path.getsize(filename="filename") + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +14 14 | +15 15 | os.path.getsize(filename) +16 16 | os.path.getsize(filename1) +17 |-os.path.getsize(filename2) + 17 |+Path(filename2).stat().st_size +18 18 | +19 19 | os.path.getsize(filename="filename") +20 20 | os.path.getsize(filename=b"filename") + +PTH202.py:19:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +17 | os.path.getsize(filename2) +18 | +19 | os.path.getsize(filename="filename") + | ^^^^^^^^^^^^^^^ PTH202 +20 | os.path.getsize(filename=b"filename") +21 | os.path.getsize(filename=Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +16 16 | os.path.getsize(filename1) +17 17 | os.path.getsize(filename2) +18 18 | +19 |-os.path.getsize(filename="filename") + 19 |+Path("filename").stat().st_size +20 20 | os.path.getsize(filename=b"filename") +21 21 | os.path.getsize(filename=Path("filename")) +22 22 | os.path.getsize(filename=__file__) + +PTH202.py:20:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +19 | os.path.getsize(filename="filename") +20 | os.path.getsize(filename=b"filename") + | ^^^^^^^^^^^^^^^ PTH202 +21 | os.path.getsize(filename=Path("filename")) +22 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +17 17 | os.path.getsize(filename2) +18 18 | +19 19 | os.path.getsize(filename="filename") +20 |-os.path.getsize(filename=b"filename") + 20 |+Path(b"filename").stat().st_size +21 21 | os.path.getsize(filename=Path("filename")) +22 22 | os.path.getsize(filename=__file__) +23 23 | + +PTH202.py:21:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +19 | os.path.getsize(filename="filename") +20 | os.path.getsize(filename=b"filename") +21 | os.path.getsize(filename=Path("filename")) + | ^^^^^^^^^^^^^^^ PTH202 +22 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +18 18 | +19 19 | os.path.getsize(filename="filename") +20 20 | os.path.getsize(filename=b"filename") +21 |-os.path.getsize(filename=Path("filename")) + 21 |+Path("filename").stat().st_size +22 22 | os.path.getsize(filename=__file__) +23 23 | +24 24 | getsize("filename") + +PTH202.py:22:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +20 | os.path.getsize(filename=b"filename") +21 | os.path.getsize(filename=Path("filename")) +22 | os.path.getsize(filename=__file__) + | ^^^^^^^^^^^^^^^ PTH202 +23 | +24 | getsize("filename") + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +19 19 | os.path.getsize(filename="filename") +20 20 | os.path.getsize(filename=b"filename") +21 21 | os.path.getsize(filename=Path("filename")) +22 |-os.path.getsize(filename=__file__) + 22 |+Path(__file__).stat().st_size +23 23 | +24 24 | getsize("filename") +25 25 | getsize(b"filename") + +PTH202.py:24:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +22 | os.path.getsize(filename=__file__) +23 | +24 | getsize("filename") + | ^^^^^^^ PTH202 +25 | getsize(b"filename") +26 | getsize(Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +21 21 | os.path.getsize(filename=Path("filename")) +22 22 | os.path.getsize(filename=__file__) +23 23 | +24 |-getsize("filename") + 24 |+Path("filename").stat().st_size +25 25 | getsize(b"filename") +26 26 | getsize(Path("filename")) +27 27 | getsize(__file__) + +PTH202.py:25:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +24 | getsize("filename") +25 | getsize(b"filename") + | ^^^^^^^ PTH202 +26 | getsize(Path("filename")) +27 | getsize(__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +22 22 | os.path.getsize(filename=__file__) +23 23 | +24 24 | getsize("filename") +25 |-getsize(b"filename") + 25 |+Path(b"filename").stat().st_size +26 26 | getsize(Path("filename")) +27 27 | getsize(__file__) +28 28 | + +PTH202.py:26:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +24 | getsize("filename") +25 | getsize(b"filename") +26 | getsize(Path("filename")) + | ^^^^^^^ PTH202 +27 | getsize(__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +23 23 | +24 24 | getsize("filename") +25 25 | getsize(b"filename") +26 |-getsize(Path("filename")) + 26 |+Path("filename").stat().st_size +27 27 | getsize(__file__) +28 28 | +29 29 | getsize(filename="filename") + +PTH202.py:27:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +25 | getsize(b"filename") +26 | getsize(Path("filename")) +27 | getsize(__file__) + | ^^^^^^^ PTH202 +28 | +29 | getsize(filename="filename") + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +24 24 | getsize("filename") +25 25 | getsize(b"filename") +26 26 | getsize(Path("filename")) +27 |-getsize(__file__) + 27 |+Path(__file__).stat().st_size +28 28 | +29 29 | getsize(filename="filename") +30 30 | getsize(filename=b"filename") + +PTH202.py:29:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +27 | getsize(__file__) +28 | +29 | getsize(filename="filename") + | ^^^^^^^ PTH202 +30 | getsize(filename=b"filename") +31 | getsize(filename=Path("filename")) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +26 26 | getsize(Path("filename")) +27 27 | getsize(__file__) +28 28 | +29 |-getsize(filename="filename") + 29 |+Path("filename").stat().st_size +30 30 | getsize(filename=b"filename") +31 31 | getsize(filename=Path("filename")) +32 32 | getsize(filename=__file__) + +PTH202.py:30:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +29 | getsize(filename="filename") +30 | getsize(filename=b"filename") + | ^^^^^^^ PTH202 +31 | getsize(filename=Path("filename")) +32 | getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +27 27 | getsize(__file__) +28 28 | +29 29 | getsize(filename="filename") +30 |-getsize(filename=b"filename") + 30 |+Path(b"filename").stat().st_size +31 31 | getsize(filename=Path("filename")) +32 32 | getsize(filename=__file__) +33 33 | + +PTH202.py:31:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +29 | getsize(filename="filename") +30 | getsize(filename=b"filename") +31 | getsize(filename=Path("filename")) + | ^^^^^^^ PTH202 +32 | getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +28 28 | +29 29 | getsize(filename="filename") +30 30 | getsize(filename=b"filename") +31 |-getsize(filename=Path("filename")) + 31 |+Path("filename").stat().st_size +32 32 | getsize(filename=__file__) +33 33 | +34 34 | getsize(filename) + +PTH202.py:32:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +30 | getsize(filename=b"filename") +31 | getsize(filename=Path("filename")) +32 | getsize(filename=__file__) + | ^^^^^^^ PTH202 +33 | +34 | getsize(filename) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +29 29 | getsize(filename="filename") +30 30 | getsize(filename=b"filename") +31 31 | getsize(filename=Path("filename")) +32 |-getsize(filename=__file__) + 32 |+Path(__file__).stat().st_size +33 33 | +34 34 | getsize(filename) +35 35 | getsize(filename1) + +PTH202.py:34:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +32 | getsize(filename=__file__) +33 | +34 | getsize(filename) + | ^^^^^^^ PTH202 +35 | getsize(filename1) +36 | getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +31 31 | getsize(filename=Path("filename")) +32 32 | getsize(filename=__file__) +33 33 | +34 |-getsize(filename) + 34 |+Path(filename).stat().st_size +35 35 | getsize(filename1) +36 36 | getsize(filename2) +37 37 | + +PTH202.py:35:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +34 | getsize(filename) +35 | getsize(filename1) + | ^^^^^^^ PTH202 +36 | getsize(filename2) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +32 32 | getsize(filename=__file__) +33 33 | +34 34 | getsize(filename) +35 |-getsize(filename1) + 35 |+Path(filename1).stat().st_size +36 36 | getsize(filename2) +37 37 | +38 38 | + +PTH202.py:36:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +34 | getsize(filename) +35 | getsize(filename1) +36 | getsize(filename2) + | ^^^^^^^ PTH202 + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +33 33 | +34 34 | getsize(filename) +35 35 | getsize(filename1) +36 |-getsize(filename2) + 36 |+Path(filename2).stat().st_size +37 37 | +38 38 | +39 39 | os.path.getsize( + +PTH202.py:39:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +39 | os.path.getsize( + | ^^^^^^^^^^^^^^^ PTH202 +40 | "filename", # comment +41 | ) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +36 36 | getsize(filename2) +37 37 | +38 38 | +39 |-os.path.getsize( +40 |- "filename", # comment +41 |-) + 39 |+Path("filename").stat().st_size +42 40 | +43 41 | os.path.getsize( +44 42 | # comment + +PTH202.py:43:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +41 | ) +42 | +43 | os.path.getsize( + | ^^^^^^^^^^^^^^^ PTH202 +44 | # comment +45 | "filename" + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +40 40 | "filename", # comment +41 41 | ) +42 42 | +43 |-os.path.getsize( +44 |- # comment +45 |- "filename" +46 |- , +47 |- # comment +48 |-) + 43 |+Path("filename").stat().st_size +49 44 | +50 45 | os.path.getsize( +51 46 | # comment + +PTH202.py:50:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +48 | ) +49 | +50 | os.path.getsize( + | ^^^^^^^^^^^^^^^ PTH202 +51 | # comment +52 | b"filename" + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +47 47 | # comment +48 48 | ) +49 49 | +50 |-os.path.getsize( +51 |- # comment +52 |- b"filename" +53 |- # comment +54 |-) + 50 |+Path(b"filename").stat().st_size +55 51 | +56 52 | os.path.getsize( # comment +57 53 | Path(__file__) + +PTH202.py:56:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +54 | ) +55 | +56 | os.path.getsize( # comment + | ^^^^^^^^^^^^^^^ PTH202 +57 | Path(__file__) +58 | # comment + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +53 53 | # comment +54 54 | ) +55 55 | +56 |-os.path.getsize( # comment +57 |- Path(__file__) +58 |- # comment +59 |-) # comment + 56 |+Path(__file__).stat().st_size # comment +60 57 | +61 58 | getsize( # comment +62 59 | "filename") + +PTH202.py:61:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +59 | ) # comment +60 | +61 | getsize( # comment + | ^^^^^^^ PTH202 +62 | "filename") + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +58 58 | # comment +59 59 | ) # comment +60 60 | +61 |-getsize( # comment +62 |- "filename") + 61 |+Path("filename").stat().st_size +63 62 | +64 63 | getsize( # comment +65 64 | b"filename", + +PTH202.py:64:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +62 | "filename") +63 | +64 | getsize( # comment + | ^^^^^^^ PTH202 +65 | b"filename", +66 | #comment + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +61 61 | getsize( # comment +62 62 | "filename") +63 63 | +64 |-getsize( # comment +65 |- b"filename", +66 |- #comment +67 |-) + 64 |+Path(b"filename").stat().st_size +68 65 | +69 66 | os.path.getsize("file" + "name") +70 67 | + +PTH202.py:69:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +67 | ) +68 | +69 | os.path.getsize("file" + "name") + | ^^^^^^^^^^^^^^^ PTH202 +70 | +71 | getsize \ + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +66 66 | #comment +67 67 | ) +68 68 | +69 |-os.path.getsize("file" + "name") + 69 |+Path("file" + "name").stat().st_size +70 70 | +71 71 | getsize \ +72 72 | \ + +PTH202.py:71:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +69 | os.path.getsize("file" + "name") +70 | +71 | getsize \ + | ^^^^^^^ PTH202 +72 | \ +73 | \ + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Unsafe fix +68 68 | +69 69 | os.path.getsize("file" + "name") +70 70 | +71 |-getsize \ +72 |-\ +73 |-\ +74 |- ( # comment +75 |- "filename", +76 |- ) + 71 |+Path("filename").stat().st_size +77 72 | +78 73 | getsize(Path("filename").resolve()) +79 74 | + +PTH202.py:78:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +76 | ) +77 | +78 | getsize(Path("filename").resolve()) + | ^^^^^^^ PTH202 +79 | +80 | import pathlib + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +75 75 | "filename", +76 76 | ) +77 77 | +78 |-getsize(Path("filename").resolve()) + 78 |+Path(Path("filename").resolve()).stat().st_size +79 79 | +80 80 | import pathlib +81 81 | + +PTH202.py:82:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +80 | import pathlib +81 | +82 | os.path.getsize(pathlib.Path("filename")) + | ^^^^^^^^^^^^^^^ PTH202 + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +79 79 | +80 80 | import pathlib +81 81 | +82 |-os.path.getsize(pathlib.Path("filename")) + 82 |+pathlib.Path("filename").stat().st_size diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH202_PTH202_2.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH202_PTH202_2.py.snap new file mode 100644 index 0000000000000..a61e87ae2d18a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH202_PTH202_2.py.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH202_2.py:3:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +1 | import os +2 | +3 | os.path.getsize(filename="filename") + | ^^^^^^^^^^^^^^^ PTH202 +4 | os.path.getsize(filename=b"filename") +5 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +1 1 | import os + 2 |+import pathlib +2 3 | +3 |-os.path.getsize(filename="filename") + 4 |+pathlib.Path("filename").stat().st_size +4 5 | os.path.getsize(filename=b"filename") +5 6 | os.path.getsize(filename=__file__) + +PTH202_2.py:4:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +3 | os.path.getsize(filename="filename") +4 | os.path.getsize(filename=b"filename") + | ^^^^^^^^^^^^^^^ PTH202 +5 | os.path.getsize(filename=__file__) + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +1 1 | import os + 2 |+import pathlib +2 3 | +3 4 | os.path.getsize(filename="filename") +4 |-os.path.getsize(filename=b"filename") + 5 |+pathlib.Path(b"filename").stat().st_size +5 6 | os.path.getsize(filename=__file__) + +PTH202_2.py:5:1: PTH202 [*] `os.path.getsize` should be replaced by `Path.stat().st_size` + | +3 | os.path.getsize(filename="filename") +4 | os.path.getsize(filename=b"filename") +5 | os.path.getsize(filename=__file__) + | ^^^^^^^^^^^^^^^ PTH202 + | + = help: Replace with `Path(...).stat().st_size` + +ℹ Safe fix +1 1 | import os + 2 |+import pathlib +2 3 | +3 4 | os.path.getsize(filename="filename") +4 5 | os.path.getsize(filename=b"filename") +5 |-os.path.getsize(filename=__file__) + 6 |+pathlib.Path(__file__).stat().st_size diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH203_PTH203.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH203_PTH203.py.snap new file mode 100644 index 0000000000000..8a02cfe4ffba2 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH203_PTH203.py.snap @@ -0,0 +1,284 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH203.py:5:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +3 | from os.path import getatime +4 | +5 | os.path.getatime("filename") + | ^^^^^^^^^^^^^^^^ PTH203 +6 | os.path.getatime(b"filename") +7 | os.path.getatime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +2 2 | from pathlib import Path +3 3 | from os.path import getatime +4 4 | +5 |-os.path.getatime("filename") + 5 |+Path("filename").stat().st_atime +6 6 | os.path.getatime(b"filename") +7 7 | os.path.getatime(Path("filename")) +8 8 | + +PTH203.py:6:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +5 | os.path.getatime("filename") +6 | os.path.getatime(b"filename") + | ^^^^^^^^^^^^^^^^ PTH203 +7 | os.path.getatime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +3 3 | from os.path import getatime +4 4 | +5 5 | os.path.getatime("filename") +6 |-os.path.getatime(b"filename") + 6 |+Path(b"filename").stat().st_atime +7 7 | os.path.getatime(Path("filename")) +8 8 | +9 9 | + +PTH203.py:7:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +5 | os.path.getatime("filename") +6 | os.path.getatime(b"filename") +7 | os.path.getatime(Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH203 + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +4 4 | +5 5 | os.path.getatime("filename") +6 6 | os.path.getatime(b"filename") +7 |-os.path.getatime(Path("filename")) + 7 |+Path("filename").stat().st_atime +8 8 | +9 9 | +10 10 | getatime("filename") + +PTH203.py:10:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +10 | getatime("filename") + | ^^^^^^^^ PTH203 +11 | getatime(b"filename") +12 | getatime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +7 7 | os.path.getatime(Path("filename")) +8 8 | +9 9 | +10 |-getatime("filename") + 10 |+Path("filename").stat().st_atime +11 11 | getatime(b"filename") +12 12 | getatime(Path("filename")) +13 13 | + +PTH203.py:11:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +10 | getatime("filename") +11 | getatime(b"filename") + | ^^^^^^^^ PTH203 +12 | getatime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +8 8 | +9 9 | +10 10 | getatime("filename") +11 |-getatime(b"filename") + 11 |+Path(b"filename").stat().st_atime +12 12 | getatime(Path("filename")) +13 13 | +14 14 | + +PTH203.py:12:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +10 | getatime("filename") +11 | getatime(b"filename") +12 | getatime(Path("filename")) + | ^^^^^^^^ PTH203 + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +9 9 | +10 10 | getatime("filename") +11 11 | getatime(b"filename") +12 |-getatime(Path("filename")) + 12 |+Path("filename").stat().st_atime +13 13 | +14 14 | +15 15 | file = __file__ + +PTH203.py:17:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +15 | file = __file__ +16 | +17 | os.path.getatime(file) + | ^^^^^^^^^^^^^^^^ PTH203 +18 | os.path.getatime(filename="filename") +19 | os.path.getatime(filename=Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +14 14 | +15 15 | file = __file__ +16 16 | +17 |-os.path.getatime(file) + 17 |+Path(file).stat().st_atime +18 18 | os.path.getatime(filename="filename") +19 19 | os.path.getatime(filename=Path("filename")) +20 20 | + +PTH203.py:18:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +17 | os.path.getatime(file) +18 | os.path.getatime(filename="filename") + | ^^^^^^^^^^^^^^^^ PTH203 +19 | os.path.getatime(filename=Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +15 15 | file = __file__ +16 16 | +17 17 | os.path.getatime(file) +18 |-os.path.getatime(filename="filename") + 18 |+Path("filename").stat().st_atime +19 19 | os.path.getatime(filename=Path("filename")) +20 20 | +21 21 | os.path.getatime( # comment 1 + +PTH203.py:19:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +17 | os.path.getatime(file) +18 | os.path.getatime(filename="filename") +19 | os.path.getatime(filename=Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH203 +20 | +21 | os.path.getatime( # comment 1 + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +16 16 | +17 17 | os.path.getatime(file) +18 18 | os.path.getatime(filename="filename") +19 |-os.path.getatime(filename=Path("filename")) + 19 |+Path("filename").stat().st_atime +20 20 | +21 21 | os.path.getatime( # comment 1 +22 22 | # comment 2 + +PTH203.py:21:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +19 | os.path.getatime(filename=Path("filename")) +20 | +21 | os.path.getatime( # comment 1 + | ^^^^^^^^^^^^^^^^ PTH203 +22 | # comment 2 +23 | "filename" # comment 3 + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Unsafe fix +18 18 | os.path.getatime(filename="filename") +19 19 | os.path.getatime(filename=Path("filename")) +20 20 | +21 |-os.path.getatime( # comment 1 +22 |- # comment 2 +23 |- "filename" # comment 3 +24 |- # comment 4 +25 |- , # comment 5 +26 |- # comment 6 +27 |-) # comment 7 + 21 |+Path("filename").stat().st_atime # comment 7 +28 22 | +29 23 | os.path.getatime("file" + "name") +30 24 | + +PTH203.py:29:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +27 | ) # comment 7 +28 | +29 | os.path.getatime("file" + "name") + | ^^^^^^^^^^^^^^^^ PTH203 +30 | +31 | getatime(Path("filename").resolve()) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +26 26 | # comment 6 +27 27 | ) # comment 7 +28 28 | +29 |-os.path.getatime("file" + "name") + 29 |+Path("file" + "name").stat().st_atime +30 30 | +31 31 | getatime(Path("filename").resolve()) +32 32 | + +PTH203.py:31:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +29 | os.path.getatime("file" + "name") +30 | +31 | getatime(Path("filename").resolve()) + | ^^^^^^^^ PTH203 +32 | +33 | os.path.getatime(pathlib.Path("filename")) + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +28 28 | +29 29 | os.path.getatime("file" + "name") +30 30 | +31 |-getatime(Path("filename").resolve()) + 31 |+Path(Path("filename").resolve()).stat().st_atime +32 32 | +33 33 | os.path.getatime(pathlib.Path("filename")) +34 34 | + +PTH203.py:33:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +31 | getatime(Path("filename").resolve()) +32 | +33 | os.path.getatime(pathlib.Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH203 +34 | +35 | getatime(Path("dir") / "file.txt") + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +30 30 | +31 31 | getatime(Path("filename").resolve()) +32 32 | +33 |-os.path.getatime(pathlib.Path("filename")) + 33 |+pathlib.Path("filename").stat().st_atime +34 34 | +35 35 | getatime(Path("dir") / "file.txt") + +PTH203.py:35:1: PTH203 [*] `os.path.getatime` should be replaced by `Path.stat().st_atime` + | +33 | os.path.getatime(pathlib.Path("filename")) +34 | +35 | getatime(Path("dir") / "file.txt") + | ^^^^^^^^ PTH203 + | + = help: Replace with `Path.stat(...).st_atime` + +ℹ Safe fix +32 32 | +33 33 | os.path.getatime(pathlib.Path("filename")) +34 34 | +35 |-getatime(Path("dir") / "file.txt") + 35 |+Path(Path("dir") / "file.txt").stat().st_atime diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH204_PTH204.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH204_PTH204.py.snap new file mode 100644 index 0000000000000..b63229a19b7e2 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH204_PTH204.py.snap @@ -0,0 +1,110 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH204.py:6:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` + | +6 | os.path.getmtime("filename") + | ^^^^^^^^^^^^^^^^ PTH204 +7 | os.path.getmtime(b"filename") +8 | os.path.getmtime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_mtime` + +ℹ Safe fix +3 3 | from os.path import getmtime +4 4 | +5 5 | +6 |-os.path.getmtime("filename") + 6 |+Path("filename").stat().st_mtime +7 7 | os.path.getmtime(b"filename") +8 8 | os.path.getmtime(Path("filename")) +9 9 | + +PTH204.py:7:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` + | +6 | os.path.getmtime("filename") +7 | os.path.getmtime(b"filename") + | ^^^^^^^^^^^^^^^^ PTH204 +8 | os.path.getmtime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_mtime` + +ℹ Safe fix +4 4 | +5 5 | +6 6 | os.path.getmtime("filename") +7 |-os.path.getmtime(b"filename") + 7 |+Path(b"filename").stat().st_mtime +8 8 | os.path.getmtime(Path("filename")) +9 9 | +10 10 | + +PTH204.py:8:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` + | +6 | os.path.getmtime("filename") +7 | os.path.getmtime(b"filename") +8 | os.path.getmtime(Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH204 + | + = help: Replace with `Path.stat(...).st_mtime` + +ℹ Safe fix +5 5 | +6 6 | os.path.getmtime("filename") +7 7 | os.path.getmtime(b"filename") +8 |-os.path.getmtime(Path("filename")) + 8 |+Path("filename").stat().st_mtime +9 9 | +10 10 | +11 11 | getmtime("filename") + +PTH204.py:11:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` + | +11 | getmtime("filename") + | ^^^^^^^^ PTH204 +12 | getmtime(b"filename") +13 | getmtime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_mtime` + +ℹ Safe fix +8 8 | os.path.getmtime(Path("filename")) +9 9 | +10 10 | +11 |-getmtime("filename") + 11 |+Path("filename").stat().st_mtime +12 12 | getmtime(b"filename") +13 13 | getmtime(Path("filename")) + +PTH204.py:12:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` + | +11 | getmtime("filename") +12 | getmtime(b"filename") + | ^^^^^^^^ PTH204 +13 | getmtime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_mtime` + +ℹ Safe fix +9 9 | +10 10 | +11 11 | getmtime("filename") +12 |-getmtime(b"filename") + 12 |+Path(b"filename").stat().st_mtime +13 13 | getmtime(Path("filename")) + +PTH204.py:13:1: PTH204 [*] `os.path.getmtime` should be replaced by `Path.stat().st_mtime` + | +11 | getmtime("filename") +12 | getmtime(b"filename") +13 | getmtime(Path("filename")) + | ^^^^^^^^ PTH204 + | + = help: Replace with `Path.stat(...).st_mtime` + +ℹ Safe fix +10 10 | +11 11 | getmtime("filename") +12 12 | getmtime(b"filename") +13 |-getmtime(Path("filename")) + 13 |+Path("filename").stat().st_mtime diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH205_PTH205.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH205_PTH205.py.snap new file mode 100644 index 0000000000000..3d675e924184f --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH205_PTH205.py.snap @@ -0,0 +1,114 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH205.py:6:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` + | +6 | os.path.getctime("filename") + | ^^^^^^^^^^^^^^^^ PTH205 +7 | os.path.getctime(b"filename") +8 | os.path.getctime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_ctime` + +ℹ Safe fix +3 3 | from os.path import getctime +4 4 | +5 5 | +6 |-os.path.getctime("filename") + 6 |+Path("filename").stat().st_ctime +7 7 | os.path.getctime(b"filename") +8 8 | os.path.getctime(Path("filename")) +9 9 | + +PTH205.py:7:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` + | +6 | os.path.getctime("filename") +7 | os.path.getctime(b"filename") + | ^^^^^^^^^^^^^^^^ PTH205 +8 | os.path.getctime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_ctime` + +ℹ Safe fix +4 4 | +5 5 | +6 6 | os.path.getctime("filename") +7 |-os.path.getctime(b"filename") + 7 |+Path(b"filename").stat().st_ctime +8 8 | os.path.getctime(Path("filename")) +9 9 | +10 10 | getctime("filename") + +PTH205.py:8:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` + | + 6 | os.path.getctime("filename") + 7 | os.path.getctime(b"filename") + 8 | os.path.getctime(Path("filename")) + | ^^^^^^^^^^^^^^^^ PTH205 + 9 | +10 | getctime("filename") + | + = help: Replace with `Path.stat(...).st_ctime` + +ℹ Safe fix +5 5 | +6 6 | os.path.getctime("filename") +7 7 | os.path.getctime(b"filename") +8 |-os.path.getctime(Path("filename")) + 8 |+Path("filename").stat().st_ctime +9 9 | +10 10 | getctime("filename") +11 11 | getctime(b"filename") + +PTH205.py:10:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` + | + 8 | os.path.getctime(Path("filename")) + 9 | +10 | getctime("filename") + | ^^^^^^^^ PTH205 +11 | getctime(b"filename") +12 | getctime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_ctime` + +ℹ Safe fix +7 7 | os.path.getctime(b"filename") +8 8 | os.path.getctime(Path("filename")) +9 9 | +10 |-getctime("filename") + 10 |+Path("filename").stat().st_ctime +11 11 | getctime(b"filename") +12 12 | getctime(Path("filename")) + +PTH205.py:11:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` + | +10 | getctime("filename") +11 | getctime(b"filename") + | ^^^^^^^^ PTH205 +12 | getctime(Path("filename")) + | + = help: Replace with `Path.stat(...).st_ctime` + +ℹ Safe fix +8 8 | os.path.getctime(Path("filename")) +9 9 | +10 10 | getctime("filename") +11 |-getctime(b"filename") + 11 |+Path(b"filename").stat().st_ctime +12 12 | getctime(Path("filename")) + +PTH205.py:12:1: PTH205 [*] `os.path.getctime` should be replaced by `Path.stat().st_ctime` + | +10 | getctime("filename") +11 | getctime(b"filename") +12 | getctime(Path("filename")) + | ^^^^^^^^ PTH205 + | + = help: Replace with `Path.stat(...).st_ctime` + +ℹ Safe fix +9 9 | +10 10 | getctime("filename") +11 11 | getctime(b"filename") +12 |-getctime(Path("filename")) + 12 |+Path("filename").stat().st_ctime diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap new file mode 100644 index 0000000000000..2018e0bafb44f --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap @@ -0,0 +1,580 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +full_name.py:7:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()` + | +5 | q = "bar" +6 | +7 | a = os.path.abspath(p) + | ^^^^^^^^^^^^^^^ PTH100 +8 | aa = os.chmod(p) +9 | aaa = os.mkdir(p) + | + = help: Replace with `Path(...).resolve()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +6 7 | +7 |-a = os.path.abspath(p) + 8 |+a = pathlib.Path(p).resolve() +8 9 | aa = os.chmod(p) +9 10 | aaa = os.mkdir(p) +10 11 | os.makedirs(p) + +full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` + | + 7 | a = os.path.abspath(p) + 8 | aa = os.chmod(p) + | ^^^^^^^^ PTH101 + 9 | aaa = os.mkdir(p) +10 | os.makedirs(p) + | + +full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` + | + 7 | a = os.path.abspath(p) + 8 | aa = os.chmod(p) + 9 | aaa = os.mkdir(p) + | ^^^^^^^^ PTH102 +10 | os.makedirs(p) +11 | os.rename(p) + | + +full_name.py:10:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)` + | + 8 | aa = os.chmod(p) + 9 | aaa = os.mkdir(p) +10 | os.makedirs(p) + | ^^^^^^^^^^^ PTH103 +11 | os.rename(p) +12 | os.replace(p) + | + +full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()` + | + 9 | aaa = os.mkdir(p) +10 | os.makedirs(p) +11 | os.rename(p) + | ^^^^^^^^^ PTH104 +12 | os.replace(p) +13 | os.rmdir(p) + | + +full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()` + | +10 | os.makedirs(p) +11 | os.rename(p) +12 | os.replace(p) + | ^^^^^^^^^^ PTH105 +13 | os.rmdir(p) +14 | os.remove(p) + | + +full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` + | +11 | os.rename(p) +12 | os.replace(p) +13 | os.rmdir(p) + | ^^^^^^^^ PTH106 +14 | os.remove(p) +15 | os.unlink(p) + | + = help: Replace with `Path(...).rmdir()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +10 11 | os.makedirs(p) +11 12 | os.rename(p) +12 13 | os.replace(p) +13 |-os.rmdir(p) + 14 |+pathlib.Path(p).rmdir() +14 15 | os.remove(p) +15 16 | os.unlink(p) +16 17 | os.getcwd(p) + +full_name.py:14:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()` + | +12 | os.replace(p) +13 | os.rmdir(p) +14 | os.remove(p) + | ^^^^^^^^^ PTH107 +15 | os.unlink(p) +16 | os.getcwd(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +11 12 | os.rename(p) +12 13 | os.replace(p) +13 14 | os.rmdir(p) +14 |-os.remove(p) + 15 |+pathlib.Path(p).unlink() +15 16 | os.unlink(p) +16 17 | os.getcwd(p) +17 18 | b = os.path.exists(p) + +full_name.py:15:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()` + | +13 | os.rmdir(p) +14 | os.remove(p) +15 | os.unlink(p) + | ^^^^^^^^^ PTH108 +16 | os.getcwd(p) +17 | b = os.path.exists(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +12 13 | os.replace(p) +13 14 | os.rmdir(p) +14 15 | os.remove(p) +15 |-os.unlink(p) + 16 |+pathlib.Path(p).unlink() +16 17 | os.getcwd(p) +17 18 | b = os.path.exists(p) +18 19 | bb = os.path.expanduser(p) + +full_name.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` + | +14 | os.remove(p) +15 | os.unlink(p) +16 | os.getcwd(p) + | ^^^^^^^^^ PTH109 +17 | b = os.path.exists(p) +18 | bb = os.path.expanduser(p) + | + +full_name.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()` + | +15 | os.unlink(p) +16 | os.getcwd(p) +17 | b = os.path.exists(p) + | ^^^^^^^^^^^^^^ PTH110 +18 | bb = os.path.expanduser(p) +19 | bbb = os.path.isdir(p) + | + = help: Replace with `Path(...).exists()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +14 15 | os.remove(p) +15 16 | os.unlink(p) +16 17 | os.getcwd(p) +17 |-b = os.path.exists(p) + 18 |+b = pathlib.Path(p).exists() +18 19 | bb = os.path.expanduser(p) +19 20 | bbb = os.path.isdir(p) +20 21 | bbbb = os.path.isfile(p) + +full_name.py:18:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()` + | +16 | os.getcwd(p) +17 | b = os.path.exists(p) +18 | bb = os.path.expanduser(p) + | ^^^^^^^^^^^^^^^^^^ PTH111 +19 | bbb = os.path.isdir(p) +20 | bbbb = os.path.isfile(p) + | + = help: Replace with `Path(...).expanduser()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +15 16 | os.unlink(p) +16 17 | os.getcwd(p) +17 18 | b = os.path.exists(p) +18 |-bb = os.path.expanduser(p) + 19 |+bb = pathlib.Path(p).expanduser() +19 20 | bbb = os.path.isdir(p) +20 21 | bbbb = os.path.isfile(p) +21 22 | bbbbb = os.path.islink(p) + +full_name.py:19:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()` + | +17 | b = os.path.exists(p) +18 | bb = os.path.expanduser(p) +19 | bbb = os.path.isdir(p) + | ^^^^^^^^^^^^^ PTH112 +20 | bbbb = os.path.isfile(p) +21 | bbbbb = os.path.islink(p) + | + = help: Replace with `Path(...).is_dir()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +16 17 | os.getcwd(p) +17 18 | b = os.path.exists(p) +18 19 | bb = os.path.expanduser(p) +19 |-bbb = os.path.isdir(p) + 20 |+bbb = pathlib.Path(p).is_dir() +20 21 | bbbb = os.path.isfile(p) +21 22 | bbbbb = os.path.islink(p) +22 23 | os.readlink(p) + +full_name.py:20:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()` + | +18 | bb = os.path.expanduser(p) +19 | bbb = os.path.isdir(p) +20 | bbbb = os.path.isfile(p) + | ^^^^^^^^^^^^^^ PTH113 +21 | bbbbb = os.path.islink(p) +22 | os.readlink(p) + | + = help: Replace with `Path(...).is_file()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +17 18 | b = os.path.exists(p) +18 19 | bb = os.path.expanduser(p) +19 20 | bbb = os.path.isdir(p) +20 |-bbbb = os.path.isfile(p) + 21 |+bbbb = pathlib.Path(p).is_file() +21 22 | bbbbb = os.path.islink(p) +22 23 | os.readlink(p) +23 24 | os.stat(p) + +full_name.py:21:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()` + | +19 | bbb = os.path.isdir(p) +20 | bbbb = os.path.isfile(p) +21 | bbbbb = os.path.islink(p) + | ^^^^^^^^^^^^^^ PTH114 +22 | os.readlink(p) +23 | os.stat(p) + | + = help: Replace with `Path(...).is_symlink()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +18 19 | bb = os.path.expanduser(p) +19 20 | bbb = os.path.isdir(p) +20 21 | bbbb = os.path.isfile(p) +21 |-bbbbb = os.path.islink(p) + 22 |+bbbbb = pathlib.Path(p).is_symlink() +22 23 | os.readlink(p) +23 24 | os.stat(p) +24 25 | os.path.isabs(p) + +full_name.py:22:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()` + | +20 | bbbb = os.path.isfile(p) +21 | bbbbb = os.path.islink(p) +22 | os.readlink(p) + | ^^^^^^^^^^^ PTH115 +23 | os.stat(p) +24 | os.path.isabs(p) + | + = help: Replace with `Path(...).readlink()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +19 20 | bbb = os.path.isdir(p) +20 21 | bbbb = os.path.isfile(p) +21 22 | bbbbb = os.path.islink(p) +22 |-os.readlink(p) + 23 |+pathlib.Path(p).readlink() +23 24 | os.stat(p) +24 25 | os.path.isabs(p) +25 26 | os.path.join(p, q) + +full_name.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` + | +21 | bbbbb = os.path.islink(p) +22 | os.readlink(p) +23 | os.stat(p) + | ^^^^^^^ PTH116 +24 | os.path.isabs(p) +25 | os.path.join(p, q) + | + +full_name.py:24:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()` + | +22 | os.readlink(p) +23 | os.stat(p) +24 | os.path.isabs(p) + | ^^^^^^^^^^^^^ PTH117 +25 | os.path.join(p, q) +26 | os.sep.join([p, q]) + | + = help: Replace with `Path(...).is_absolute()` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +21 22 | bbbbb = os.path.islink(p) +22 23 | os.readlink(p) +23 24 | os.stat(p) +24 |-os.path.isabs(p) + 25 |+pathlib.Path(p).is_absolute() +25 26 | os.path.join(p, q) +26 27 | os.sep.join([p, q]) +27 28 | os.sep.join((p, q)) + +full_name.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator + | +23 | os.stat(p) +24 | os.path.isabs(p) +25 | os.path.join(p, q) + | ^^^^^^^^^^^^ PTH118 +26 | os.sep.join([p, q]) +27 | os.sep.join((p, q)) + | + +full_name.py:26:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +24 | os.path.isabs(p) +25 | os.path.join(p, q) +26 | os.sep.join([p, q]) + | ^^^^^^^^^^^ PTH118 +27 | os.sep.join((p, q)) +28 | os.path.basename(p) + | + +full_name.py:27:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +25 | os.path.join(p, q) +26 | os.sep.join([p, q]) +27 | os.sep.join((p, q)) + | ^^^^^^^^^^^ PTH118 +28 | os.path.basename(p) +29 | os.path.dirname(p) + | + +full_name.py:28:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name` + | +26 | os.sep.join([p, q]) +27 | os.sep.join((p, q)) +28 | os.path.basename(p) + | ^^^^^^^^^^^^^^^^ PTH119 +29 | os.path.dirname(p) +30 | os.path.samefile(p) + | + = help: Replace with `Path(...).name` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +25 26 | os.path.join(p, q) +26 27 | os.sep.join([p, q]) +27 28 | os.sep.join((p, q)) +28 |-os.path.basename(p) + 29 |+pathlib.Path(p).name +29 30 | os.path.dirname(p) +30 31 | os.path.samefile(p) +31 32 | os.path.splitext(p) + +full_name.py:29:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent` + | +27 | os.sep.join((p, q)) +28 | os.path.basename(p) +29 | os.path.dirname(p) + | ^^^^^^^^^^^^^^^ PTH120 +30 | os.path.samefile(p) +31 | os.path.splitext(p) + | + = help: Replace with `Path(...).parent` + +ℹ Safe fix +1 1 | import os +2 2 | import os.path + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +26 27 | os.sep.join([p, q]) +27 28 | os.sep.join((p, q)) +28 29 | os.path.basename(p) +29 |-os.path.dirname(p) + 30 |+pathlib.Path(p).parent +30 31 | os.path.samefile(p) +31 32 | os.path.splitext(p) +32 33 | with open(p) as fp: + +full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` + | +28 | os.path.basename(p) +29 | os.path.dirname(p) +30 | os.path.samefile(p) + | ^^^^^^^^^^^^^^^^ PTH121 +31 | os.path.splitext(p) +32 | with open(p) as fp: + | + +full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` + | +29 | os.path.dirname(p) +30 | os.path.samefile(p) +31 | os.path.splitext(p) + | ^^^^^^^^^^^^^^^^ PTH122 +32 | with open(p) as fp: +33 | fp.read() + | + +full_name.py:32:6: PTH123 `open()` should be replaced by `Path.open()` + | +30 | os.path.samefile(p) +31 | os.path.splitext(p) +32 | with open(p) as fp: + | ^^^^ PTH123 +33 | fp.read() +34 | open(p).close() + | + +full_name.py:34:1: PTH123 `open()` should be replaced by `Path.open()` + | +32 | with open(p) as fp: +33 | fp.read() +34 | open(p).close() + | ^^^^ PTH123 +35 | os.getcwdb(p) +36 | os.path.join(p, *q) + | + +full_name.py:35:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` + | +33 | fp.read() +34 | open(p).close() +35 | os.getcwdb(p) + | ^^^^^^^^^^ PTH109 +36 | os.path.join(p, *q) +37 | os.sep.join(p, *q) + | + +full_name.py:36:1: PTH118 `os.path.join()` should be replaced by `Path.joinpath()` + | +34 | open(p).close() +35 | os.getcwdb(p) +36 | os.path.join(p, *q) + | ^^^^^^^^^^^^ PTH118 +37 | os.sep.join(p, *q) + | + +full_name.py:37:1: PTH118 `os.sep.join()` should be replaced by `Path.joinpath()` + | +35 | os.getcwdb(p) +36 | os.path.join(p, *q) +37 | os.sep.join(p, *q) + | ^^^^^^^^^^^ PTH118 +38 | +39 | # https://github.com/astral-sh/ruff/issues/7620 + | + +full_name.py:46:1: PTH123 `open()` should be replaced by `Path.open()` + | +44 | open(p, closefd=False) +45 | open(p, opener=opener) +46 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) + | ^^^^ PTH123 +47 | open(p, 'r', - 1, None, None, None, True, None) +48 | open(p, 'r', - 1, None, None, None, False, opener) + | + +full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()` + | +45 | open(p, opener=opener) +46 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) +47 | open(p, 'r', - 1, None, None, None, True, None) + | ^^^^ PTH123 +48 | open(p, 'r', - 1, None, None, None, False, opener) + | + +full_name.py:65:1: PTH123 `open()` should be replaced by `Path.open()` + | +63 | open(f()) +64 | +65 | open(b"foo") + | ^^^^ PTH123 +66 | byte_str = b"bar" +67 | open(byte_str) + | + +full_name.py:67:1: PTH123 `open()` should be replaced by `Path.open()` + | +65 | open(b"foo") +66 | byte_str = b"bar" +67 | open(byte_str) + | ^^^^ PTH123 +68 | +69 | def bytes_str_func() -> bytes: + | + +full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()` + | +69 | def bytes_str_func() -> bytes: +70 | return b"foo" +71 | open(bytes_str_func()) + | ^^^^ PTH123 +72 | +73 | # https://github.com/astral-sh/ruff/issues/17693 + | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_as.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_as.py.snap new file mode 100644 index 0000000000000..bca1ad2b3201d --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_as.py.snap @@ -0,0 +1,478 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +import_as.py:7:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()` + | +5 | q = "bar" +6 | +7 | a = foo_p.abspath(p) + | ^^^^^^^^^^^^^ PTH100 +8 | aa = foo.chmod(p) +9 | aaa = foo.mkdir(p) + | + = help: Replace with `Path(...).resolve()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +6 7 | +7 |-a = foo_p.abspath(p) + 8 |+a = pathlib.Path(p).resolve() +8 9 | aa = foo.chmod(p) +9 10 | aaa = foo.mkdir(p) +10 11 | foo.makedirs(p) + +import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` + | + 7 | a = foo_p.abspath(p) + 8 | aa = foo.chmod(p) + | ^^^^^^^^^ PTH101 + 9 | aaa = foo.mkdir(p) +10 | foo.makedirs(p) + | + +import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` + | + 7 | a = foo_p.abspath(p) + 8 | aa = foo.chmod(p) + 9 | aaa = foo.mkdir(p) + | ^^^^^^^^^ PTH102 +10 | foo.makedirs(p) +11 | foo.rename(p) + | + +import_as.py:10:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)` + | + 8 | aa = foo.chmod(p) + 9 | aaa = foo.mkdir(p) +10 | foo.makedirs(p) + | ^^^^^^^^^^^^ PTH103 +11 | foo.rename(p) +12 | foo.replace(p) + | + +import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()` + | + 9 | aaa = foo.mkdir(p) +10 | foo.makedirs(p) +11 | foo.rename(p) + | ^^^^^^^^^^ PTH104 +12 | foo.replace(p) +13 | foo.rmdir(p) + | + +import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()` + | +10 | foo.makedirs(p) +11 | foo.rename(p) +12 | foo.replace(p) + | ^^^^^^^^^^^ PTH105 +13 | foo.rmdir(p) +14 | foo.remove(p) + | + +import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` + | +11 | foo.rename(p) +12 | foo.replace(p) +13 | foo.rmdir(p) + | ^^^^^^^^^ PTH106 +14 | foo.remove(p) +15 | foo.unlink(p) + | + = help: Replace with `Path(...).rmdir()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +10 11 | foo.makedirs(p) +11 12 | foo.rename(p) +12 13 | foo.replace(p) +13 |-foo.rmdir(p) + 14 |+pathlib.Path(p).rmdir() +14 15 | foo.remove(p) +15 16 | foo.unlink(p) +16 17 | foo.getcwd(p) + +import_as.py:14:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()` + | +12 | foo.replace(p) +13 | foo.rmdir(p) +14 | foo.remove(p) + | ^^^^^^^^^^ PTH107 +15 | foo.unlink(p) +16 | foo.getcwd(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +11 12 | foo.rename(p) +12 13 | foo.replace(p) +13 14 | foo.rmdir(p) +14 |-foo.remove(p) + 15 |+pathlib.Path(p).unlink() +15 16 | foo.unlink(p) +16 17 | foo.getcwd(p) +17 18 | b = foo_p.exists(p) + +import_as.py:15:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()` + | +13 | foo.rmdir(p) +14 | foo.remove(p) +15 | foo.unlink(p) + | ^^^^^^^^^^ PTH108 +16 | foo.getcwd(p) +17 | b = foo_p.exists(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +12 13 | foo.replace(p) +13 14 | foo.rmdir(p) +14 15 | foo.remove(p) +15 |-foo.unlink(p) + 16 |+pathlib.Path(p).unlink() +16 17 | foo.getcwd(p) +17 18 | b = foo_p.exists(p) +18 19 | bb = foo_p.expanduser(p) + +import_as.py:16:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` + | +14 | foo.remove(p) +15 | foo.unlink(p) +16 | foo.getcwd(p) + | ^^^^^^^^^^ PTH109 +17 | b = foo_p.exists(p) +18 | bb = foo_p.expanduser(p) + | + +import_as.py:17:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()` + | +15 | foo.unlink(p) +16 | foo.getcwd(p) +17 | b = foo_p.exists(p) + | ^^^^^^^^^^^^ PTH110 +18 | bb = foo_p.expanduser(p) +19 | bbb = foo_p.isdir(p) + | + = help: Replace with `Path(...).exists()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +14 15 | foo.remove(p) +15 16 | foo.unlink(p) +16 17 | foo.getcwd(p) +17 |-b = foo_p.exists(p) + 18 |+b = pathlib.Path(p).exists() +18 19 | bb = foo_p.expanduser(p) +19 20 | bbb = foo_p.isdir(p) +20 21 | bbbb = foo_p.isfile(p) + +import_as.py:18:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()` + | +16 | foo.getcwd(p) +17 | b = foo_p.exists(p) +18 | bb = foo_p.expanduser(p) + | ^^^^^^^^^^^^^^^^ PTH111 +19 | bbb = foo_p.isdir(p) +20 | bbbb = foo_p.isfile(p) + | + = help: Replace with `Path(...).expanduser()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +15 16 | foo.unlink(p) +16 17 | foo.getcwd(p) +17 18 | b = foo_p.exists(p) +18 |-bb = foo_p.expanduser(p) + 19 |+bb = pathlib.Path(p).expanduser() +19 20 | bbb = foo_p.isdir(p) +20 21 | bbbb = foo_p.isfile(p) +21 22 | bbbbb = foo_p.islink(p) + +import_as.py:19:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()` + | +17 | b = foo_p.exists(p) +18 | bb = foo_p.expanduser(p) +19 | bbb = foo_p.isdir(p) + | ^^^^^^^^^^^ PTH112 +20 | bbbb = foo_p.isfile(p) +21 | bbbbb = foo_p.islink(p) + | + = help: Replace with `Path(...).is_dir()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +16 17 | foo.getcwd(p) +17 18 | b = foo_p.exists(p) +18 19 | bb = foo_p.expanduser(p) +19 |-bbb = foo_p.isdir(p) + 20 |+bbb = pathlib.Path(p).is_dir() +20 21 | bbbb = foo_p.isfile(p) +21 22 | bbbbb = foo_p.islink(p) +22 23 | foo.readlink(p) + +import_as.py:20:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()` + | +18 | bb = foo_p.expanduser(p) +19 | bbb = foo_p.isdir(p) +20 | bbbb = foo_p.isfile(p) + | ^^^^^^^^^^^^ PTH113 +21 | bbbbb = foo_p.islink(p) +22 | foo.readlink(p) + | + = help: Replace with `Path(...).is_file()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +17 18 | b = foo_p.exists(p) +18 19 | bb = foo_p.expanduser(p) +19 20 | bbb = foo_p.isdir(p) +20 |-bbbb = foo_p.isfile(p) + 21 |+bbbb = pathlib.Path(p).is_file() +21 22 | bbbbb = foo_p.islink(p) +22 23 | foo.readlink(p) +23 24 | foo.stat(p) + +import_as.py:21:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()` + | +19 | bbb = foo_p.isdir(p) +20 | bbbb = foo_p.isfile(p) +21 | bbbbb = foo_p.islink(p) + | ^^^^^^^^^^^^ PTH114 +22 | foo.readlink(p) +23 | foo.stat(p) + | + = help: Replace with `Path(...).is_symlink()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +18 19 | bb = foo_p.expanduser(p) +19 20 | bbb = foo_p.isdir(p) +20 21 | bbbb = foo_p.isfile(p) +21 |-bbbbb = foo_p.islink(p) + 22 |+bbbbb = pathlib.Path(p).is_symlink() +22 23 | foo.readlink(p) +23 24 | foo.stat(p) +24 25 | foo_p.isabs(p) + +import_as.py:22:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()` + | +20 | bbbb = foo_p.isfile(p) +21 | bbbbb = foo_p.islink(p) +22 | foo.readlink(p) + | ^^^^^^^^^^^^ PTH115 +23 | foo.stat(p) +24 | foo_p.isabs(p) + | + = help: Replace with `Path(...).readlink()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +19 20 | bbb = foo_p.isdir(p) +20 21 | bbbb = foo_p.isfile(p) +21 22 | bbbbb = foo_p.islink(p) +22 |-foo.readlink(p) + 23 |+pathlib.Path(p).readlink() +23 24 | foo.stat(p) +24 25 | foo_p.isabs(p) +25 26 | foo_p.join(p, q) + +import_as.py:23:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` + | +21 | bbbbb = foo_p.islink(p) +22 | foo.readlink(p) +23 | foo.stat(p) + | ^^^^^^^^ PTH116 +24 | foo_p.isabs(p) +25 | foo_p.join(p, q) + | + +import_as.py:24:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()` + | +22 | foo.readlink(p) +23 | foo.stat(p) +24 | foo_p.isabs(p) + | ^^^^^^^^^^^ PTH117 +25 | foo_p.join(p, q) +26 | foo.sep.join([p, q]) + | + = help: Replace with `Path(...).is_absolute()` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +21 22 | bbbbb = foo_p.islink(p) +22 23 | foo.readlink(p) +23 24 | foo.stat(p) +24 |-foo_p.isabs(p) + 25 |+pathlib.Path(p).is_absolute() +25 26 | foo_p.join(p, q) +26 27 | foo.sep.join([p, q]) +27 28 | foo.sep.join((p, q)) + +import_as.py:25:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator + | +23 | foo.stat(p) +24 | foo_p.isabs(p) +25 | foo_p.join(p, q) + | ^^^^^^^^^^ PTH118 +26 | foo.sep.join([p, q]) +27 | foo.sep.join((p, q)) + | + +import_as.py:26:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +24 | foo_p.isabs(p) +25 | foo_p.join(p, q) +26 | foo.sep.join([p, q]) + | ^^^^^^^^^^^^ PTH118 +27 | foo.sep.join((p, q)) +28 | foo_p.basename(p) + | + +import_as.py:27:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +25 | foo_p.join(p, q) +26 | foo.sep.join([p, q]) +27 | foo.sep.join((p, q)) + | ^^^^^^^^^^^^ PTH118 +28 | foo_p.basename(p) +29 | foo_p.dirname(p) + | + +import_as.py:28:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name` + | +26 | foo.sep.join([p, q]) +27 | foo.sep.join((p, q)) +28 | foo_p.basename(p) + | ^^^^^^^^^^^^^^ PTH119 +29 | foo_p.dirname(p) +30 | foo_p.samefile(p) + | + = help: Replace with `Path(...).name` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +25 26 | foo_p.join(p, q) +26 27 | foo.sep.join([p, q]) +27 28 | foo.sep.join((p, q)) +28 |-foo_p.basename(p) + 29 |+pathlib.Path(p).name +29 30 | foo_p.dirname(p) +30 31 | foo_p.samefile(p) +31 32 | foo_p.splitext(p) + +import_as.py:29:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent` + | +27 | foo.sep.join((p, q)) +28 | foo_p.basename(p) +29 | foo_p.dirname(p) + | ^^^^^^^^^^^^^ PTH120 +30 | foo_p.samefile(p) +31 | foo_p.splitext(p) + | + = help: Replace with `Path(...).parent` + +ℹ Safe fix +1 1 | import os as foo +2 2 | import os.path as foo_p + 3 |+import pathlib +3 4 | +4 5 | p = "/foo" +5 6 | q = "bar" +-------------------------------------------------------------------------------- +26 27 | foo.sep.join([p, q]) +27 28 | foo.sep.join((p, q)) +28 29 | foo_p.basename(p) +29 |-foo_p.dirname(p) + 30 |+pathlib.Path(p).parent +30 31 | foo_p.samefile(p) +31 32 | foo_p.splitext(p) + +import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` + | +28 | foo_p.basename(p) +29 | foo_p.dirname(p) +30 | foo_p.samefile(p) + | ^^^^^^^^^^^^^^ PTH121 +31 | foo_p.splitext(p) + | + +import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` + | +29 | foo_p.dirname(p) +30 | foo_p.samefile(p) +31 | foo_p.splitext(p) + | ^^^^^^^^^^^^^^ PTH122 + | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap new file mode 100644 index 0000000000000..c56f5fc7d9063 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap @@ -0,0 +1,521 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +import_from.py:9:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()` + | + 7 | q = "bar" + 8 | + 9 | a = abspath(p) + | ^^^^^^^ PTH100 +10 | aa = chmod(p) +11 | aaa = mkdir(p) + | + = help: Replace with `Path(...).resolve()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +8 9 | +9 |-a = abspath(p) + 10 |+a = pathlib.Path(p).resolve() +10 11 | aa = chmod(p) +11 12 | aaa = mkdir(p) +12 13 | makedirs(p) + +import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` + | + 9 | a = abspath(p) +10 | aa = chmod(p) + | ^^^^^ PTH101 +11 | aaa = mkdir(p) +12 | makedirs(p) + | + +import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` + | + 9 | a = abspath(p) +10 | aa = chmod(p) +11 | aaa = mkdir(p) + | ^^^^^ PTH102 +12 | makedirs(p) +13 | rename(p) + | + +import_from.py:12:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)` + | +10 | aa = chmod(p) +11 | aaa = mkdir(p) +12 | makedirs(p) + | ^^^^^^^^ PTH103 +13 | rename(p) +14 | replace(p) + | + +import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()` + | +11 | aaa = mkdir(p) +12 | makedirs(p) +13 | rename(p) + | ^^^^^^ PTH104 +14 | replace(p) +15 | rmdir(p) + | + +import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()` + | +12 | makedirs(p) +13 | rename(p) +14 | replace(p) + | ^^^^^^^ PTH105 +15 | rmdir(p) +16 | remove(p) + | + +import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` + | +13 | rename(p) +14 | replace(p) +15 | rmdir(p) + | ^^^^^ PTH106 +16 | remove(p) +17 | unlink(p) + | + = help: Replace with `Path(...).rmdir()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +12 13 | makedirs(p) +13 14 | rename(p) +14 15 | replace(p) +15 |-rmdir(p) + 16 |+pathlib.Path(p).rmdir() +16 17 | remove(p) +17 18 | unlink(p) +18 19 | getcwd(p) + +import_from.py:16:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()` + | +14 | replace(p) +15 | rmdir(p) +16 | remove(p) + | ^^^^^^ PTH107 +17 | unlink(p) +18 | getcwd(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +13 14 | rename(p) +14 15 | replace(p) +15 16 | rmdir(p) +16 |-remove(p) + 17 |+pathlib.Path(p).unlink() +17 18 | unlink(p) +18 19 | getcwd(p) +19 20 | b = exists(p) + +import_from.py:17:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()` + | +15 | rmdir(p) +16 | remove(p) +17 | unlink(p) + | ^^^^^^ PTH108 +18 | getcwd(p) +19 | b = exists(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +14 15 | replace(p) +15 16 | rmdir(p) +16 17 | remove(p) +17 |-unlink(p) + 18 |+pathlib.Path(p).unlink() +18 19 | getcwd(p) +19 20 | b = exists(p) +20 21 | bb = expanduser(p) + +import_from.py:18:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` + | +16 | remove(p) +17 | unlink(p) +18 | getcwd(p) + | ^^^^^^ PTH109 +19 | b = exists(p) +20 | bb = expanduser(p) + | + +import_from.py:19:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()` + | +17 | unlink(p) +18 | getcwd(p) +19 | b = exists(p) + | ^^^^^^ PTH110 +20 | bb = expanduser(p) +21 | bbb = isdir(p) + | + = help: Replace with `Path(...).exists()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +16 17 | remove(p) +17 18 | unlink(p) +18 19 | getcwd(p) +19 |-b = exists(p) + 20 |+b = pathlib.Path(p).exists() +20 21 | bb = expanduser(p) +21 22 | bbb = isdir(p) +22 23 | bbbb = isfile(p) + +import_from.py:20:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()` + | +18 | getcwd(p) +19 | b = exists(p) +20 | bb = expanduser(p) + | ^^^^^^^^^^ PTH111 +21 | bbb = isdir(p) +22 | bbbb = isfile(p) + | + = help: Replace with `Path(...).expanduser()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +17 18 | unlink(p) +18 19 | getcwd(p) +19 20 | b = exists(p) +20 |-bb = expanduser(p) + 21 |+bb = pathlib.Path(p).expanduser() +21 22 | bbb = isdir(p) +22 23 | bbbb = isfile(p) +23 24 | bbbbb = islink(p) + +import_from.py:21:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()` + | +19 | b = exists(p) +20 | bb = expanduser(p) +21 | bbb = isdir(p) + | ^^^^^ PTH112 +22 | bbbb = isfile(p) +23 | bbbbb = islink(p) + | + = help: Replace with `Path(...).is_dir()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +18 19 | getcwd(p) +19 20 | b = exists(p) +20 21 | bb = expanduser(p) +21 |-bbb = isdir(p) + 22 |+bbb = pathlib.Path(p).is_dir() +22 23 | bbbb = isfile(p) +23 24 | bbbbb = islink(p) +24 25 | readlink(p) + +import_from.py:22:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()` + | +20 | bb = expanduser(p) +21 | bbb = isdir(p) +22 | bbbb = isfile(p) + | ^^^^^^ PTH113 +23 | bbbbb = islink(p) +24 | readlink(p) + | + = help: Replace with `Path(...).is_file()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +19 20 | b = exists(p) +20 21 | bb = expanduser(p) +21 22 | bbb = isdir(p) +22 |-bbbb = isfile(p) + 23 |+bbbb = pathlib.Path(p).is_file() +23 24 | bbbbb = islink(p) +24 25 | readlink(p) +25 26 | stat(p) + +import_from.py:23:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()` + | +21 | bbb = isdir(p) +22 | bbbb = isfile(p) +23 | bbbbb = islink(p) + | ^^^^^^ PTH114 +24 | readlink(p) +25 | stat(p) + | + = help: Replace with `Path(...).is_symlink()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +20 21 | bb = expanduser(p) +21 22 | bbb = isdir(p) +22 23 | bbbb = isfile(p) +23 |-bbbbb = islink(p) + 24 |+bbbbb = pathlib.Path(p).is_symlink() +24 25 | readlink(p) +25 26 | stat(p) +26 27 | isabs(p) + +import_from.py:24:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()` + | +22 | bbbb = isfile(p) +23 | bbbbb = islink(p) +24 | readlink(p) + | ^^^^^^^^ PTH115 +25 | stat(p) +26 | isabs(p) + | + = help: Replace with `Path(...).readlink()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +21 22 | bbb = isdir(p) +22 23 | bbbb = isfile(p) +23 24 | bbbbb = islink(p) +24 |-readlink(p) + 25 |+pathlib.Path(p).readlink() +25 26 | stat(p) +26 27 | isabs(p) +27 28 | join(p, q) + +import_from.py:25:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` + | +23 | bbbbb = islink(p) +24 | readlink(p) +25 | stat(p) + | ^^^^ PTH116 +26 | isabs(p) +27 | join(p, q) + | + +import_from.py:26:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()` + | +24 | readlink(p) +25 | stat(p) +26 | isabs(p) + | ^^^^^ PTH117 +27 | join(p, q) +28 | sep.join((p, q)) + | + = help: Replace with `Path(...).is_absolute()` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +23 24 | bbbbb = islink(p) +24 25 | readlink(p) +25 26 | stat(p) +26 |-isabs(p) + 27 |+pathlib.Path(p).is_absolute() +27 28 | join(p, q) +28 29 | sep.join((p, q)) +29 30 | sep.join([p, q]) + +import_from.py:27:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator + | +25 | stat(p) +26 | isabs(p) +27 | join(p, q) + | ^^^^ PTH118 +28 | sep.join((p, q)) +29 | sep.join([p, q]) + | + +import_from.py:28:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +26 | isabs(p) +27 | join(p, q) +28 | sep.join((p, q)) + | ^^^^^^^^ PTH118 +29 | sep.join([p, q]) +30 | basename(p) + | + +import_from.py:29:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +27 | join(p, q) +28 | sep.join((p, q)) +29 | sep.join([p, q]) + | ^^^^^^^^ PTH118 +30 | basename(p) +31 | dirname(p) + | + +import_from.py:30:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name` + | +28 | sep.join((p, q)) +29 | sep.join([p, q]) +30 | basename(p) + | ^^^^^^^^ PTH119 +31 | dirname(p) +32 | samefile(p) + | + = help: Replace with `Path(...).name` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +27 28 | join(p, q) +28 29 | sep.join((p, q)) +29 30 | sep.join([p, q]) +30 |-basename(p) + 31 |+pathlib.Path(p).name +31 32 | dirname(p) +32 33 | samefile(p) +33 34 | splitext(p) + +import_from.py:31:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent` + | +29 | sep.join([p, q]) +30 | basename(p) +31 | dirname(p) + | ^^^^^^^ PTH120 +32 | samefile(p) +33 | splitext(p) + | + = help: Replace with `Path(...).parent` + +ℹ Safe fix +2 2 | from os import remove, unlink, getcwd, readlink, stat +3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext + 5 |+import pathlib +5 6 | +6 7 | p = "/foo" +7 8 | q = "bar" +-------------------------------------------------------------------------------- +28 29 | sep.join((p, q)) +29 30 | sep.join([p, q]) +30 31 | basename(p) +31 |-dirname(p) + 32 |+pathlib.Path(p).parent +32 33 | samefile(p) +33 34 | splitext(p) +34 35 | with open(p) as fp: + +import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` + | +30 | basename(p) +31 | dirname(p) +32 | samefile(p) + | ^^^^^^^^ PTH121 +33 | splitext(p) +34 | with open(p) as fp: + | + +import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` + | +31 | dirname(p) +32 | samefile(p) +33 | splitext(p) + | ^^^^^^^^ PTH122 +34 | with open(p) as fp: +35 | fp.read() + | + +import_from.py:34:6: PTH123 `open()` should be replaced by `Path.open()` + | +32 | samefile(p) +33 | splitext(p) +34 | with open(p) as fp: + | ^^^^ PTH123 +35 | fp.read() +36 | open(p).close() + | + +import_from.py:36:1: PTH123 `open()` should be replaced by `Path.open()` + | +34 | with open(p) as fp: +35 | fp.read() +36 | open(p).close() + | ^^^^ PTH123 + | + +import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()` + | +41 | from builtins import open +42 | +43 | with open(p) as _: ... # Error + | ^^^^ PTH123 + | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from_as.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from_as.py.snap new file mode 100644 index 0000000000000..2728b6caf4ae3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from_as.py.snap @@ -0,0 +1,491 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +import_from_as.py:14:5: PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()` + | +12 | q = "bar" +13 | +14 | a = xabspath(p) + | ^^^^^^^^ PTH100 +15 | aa = xchmod(p) +16 | aaa = xmkdir(p) + | + = help: Replace with `Path(...).resolve()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +13 14 | +14 |-a = xabspath(p) + 15 |+a = pathlib.Path(p).resolve() +15 16 | aa = xchmod(p) +16 17 | aaa = xmkdir(p) +17 18 | xmakedirs(p) + +import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()` + | +14 | a = xabspath(p) +15 | aa = xchmod(p) + | ^^^^^^ PTH101 +16 | aaa = xmkdir(p) +17 | xmakedirs(p) + | + +import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` + | +14 | a = xabspath(p) +15 | aa = xchmod(p) +16 | aaa = xmkdir(p) + | ^^^^^^ PTH102 +17 | xmakedirs(p) +18 | xrename(p) + | + +import_from_as.py:17:1: PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)` + | +15 | aa = xchmod(p) +16 | aaa = xmkdir(p) +17 | xmakedirs(p) + | ^^^^^^^^^ PTH103 +18 | xrename(p) +19 | xreplace(p) + | + +import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename()` + | +16 | aaa = xmkdir(p) +17 | xmakedirs(p) +18 | xrename(p) + | ^^^^^^^ PTH104 +19 | xreplace(p) +20 | xrmdir(p) + | + +import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()` + | +17 | xmakedirs(p) +18 | xrename(p) +19 | xreplace(p) + | ^^^^^^^^ PTH105 +20 | xrmdir(p) +21 | xremove(p) + | + +import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` + | +18 | xrename(p) +19 | xreplace(p) +20 | xrmdir(p) + | ^^^^^^ PTH106 +21 | xremove(p) +22 | xunlink(p) + | + = help: Replace with `Path(...).rmdir()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +17 18 | xmakedirs(p) +18 19 | xrename(p) +19 20 | xreplace(p) +20 |-xrmdir(p) + 21 |+pathlib.Path(p).rmdir() +21 22 | xremove(p) +22 23 | xunlink(p) +23 24 | xgetcwd(p) + +import_from_as.py:21:1: PTH107 [*] `os.remove()` should be replaced by `Path.unlink()` + | +19 | xreplace(p) +20 | xrmdir(p) +21 | xremove(p) + | ^^^^^^^ PTH107 +22 | xunlink(p) +23 | xgetcwd(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +18 19 | xrename(p) +19 20 | xreplace(p) +20 21 | xrmdir(p) +21 |-xremove(p) + 22 |+pathlib.Path(p).unlink() +22 23 | xunlink(p) +23 24 | xgetcwd(p) +24 25 | b = xexists(p) + +import_from_as.py:22:1: PTH108 [*] `os.unlink()` should be replaced by `Path.unlink()` + | +20 | xrmdir(p) +21 | xremove(p) +22 | xunlink(p) + | ^^^^^^^ PTH108 +23 | xgetcwd(p) +24 | b = xexists(p) + | + = help: Replace with `Path(...).unlink()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +19 20 | xreplace(p) +20 21 | xrmdir(p) +21 22 | xremove(p) +22 |-xunlink(p) + 23 |+pathlib.Path(p).unlink() +23 24 | xgetcwd(p) +24 25 | b = xexists(p) +25 26 | bb = xexpanduser(p) + +import_from_as.py:23:1: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` + | +21 | xremove(p) +22 | xunlink(p) +23 | xgetcwd(p) + | ^^^^^^^ PTH109 +24 | b = xexists(p) +25 | bb = xexpanduser(p) + | + +import_from_as.py:24:5: PTH110 [*] `os.path.exists()` should be replaced by `Path.exists()` + | +22 | xunlink(p) +23 | xgetcwd(p) +24 | b = xexists(p) + | ^^^^^^^ PTH110 +25 | bb = xexpanduser(p) +26 | bbb = xisdir(p) + | + = help: Replace with `Path(...).exists()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +21 22 | xremove(p) +22 23 | xunlink(p) +23 24 | xgetcwd(p) +24 |-b = xexists(p) + 25 |+b = pathlib.Path(p).exists() +25 26 | bb = xexpanduser(p) +26 27 | bbb = xisdir(p) +27 28 | bbbb = xisfile(p) + +import_from_as.py:25:6: PTH111 [*] `os.path.expanduser()` should be replaced by `Path.expanduser()` + | +23 | xgetcwd(p) +24 | b = xexists(p) +25 | bb = xexpanduser(p) + | ^^^^^^^^^^^ PTH111 +26 | bbb = xisdir(p) +27 | bbbb = xisfile(p) + | + = help: Replace with `Path(...).expanduser()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +22 23 | xunlink(p) +23 24 | xgetcwd(p) +24 25 | b = xexists(p) +25 |-bb = xexpanduser(p) + 26 |+bb = pathlib.Path(p).expanduser() +26 27 | bbb = xisdir(p) +27 28 | bbbb = xisfile(p) +28 29 | bbbbb = xislink(p) + +import_from_as.py:26:7: PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()` + | +24 | b = xexists(p) +25 | bb = xexpanduser(p) +26 | bbb = xisdir(p) + | ^^^^^^ PTH112 +27 | bbbb = xisfile(p) +28 | bbbbb = xislink(p) + | + = help: Replace with `Path(...).is_dir()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +23 24 | xgetcwd(p) +24 25 | b = xexists(p) +25 26 | bb = xexpanduser(p) +26 |-bbb = xisdir(p) + 27 |+bbb = pathlib.Path(p).is_dir() +27 28 | bbbb = xisfile(p) +28 29 | bbbbb = xislink(p) +29 30 | xreadlink(p) + +import_from_as.py:27:8: PTH113 [*] `os.path.isfile()` should be replaced by `Path.is_file()` + | +25 | bb = xexpanduser(p) +26 | bbb = xisdir(p) +27 | bbbb = xisfile(p) + | ^^^^^^^ PTH113 +28 | bbbbb = xislink(p) +29 | xreadlink(p) + | + = help: Replace with `Path(...).is_file()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +24 25 | b = xexists(p) +25 26 | bb = xexpanduser(p) +26 27 | bbb = xisdir(p) +27 |-bbbb = xisfile(p) + 28 |+bbbb = pathlib.Path(p).is_file() +28 29 | bbbbb = xislink(p) +29 30 | xreadlink(p) +30 31 | xstat(p) + +import_from_as.py:28:9: PTH114 [*] `os.path.islink()` should be replaced by `Path.is_symlink()` + | +26 | bbb = xisdir(p) +27 | bbbb = xisfile(p) +28 | bbbbb = xislink(p) + | ^^^^^^^ PTH114 +29 | xreadlink(p) +30 | xstat(p) + | + = help: Replace with `Path(...).is_symlink()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +25 26 | bb = xexpanduser(p) +26 27 | bbb = xisdir(p) +27 28 | bbbb = xisfile(p) +28 |-bbbbb = xislink(p) + 29 |+bbbbb = pathlib.Path(p).is_symlink() +29 30 | xreadlink(p) +30 31 | xstat(p) +31 32 | xisabs(p) + +import_from_as.py:29:1: PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()` + | +27 | bbbb = xisfile(p) +28 | bbbbb = xislink(p) +29 | xreadlink(p) + | ^^^^^^^^^ PTH115 +30 | xstat(p) +31 | xisabs(p) + | + = help: Replace with `Path(...).readlink()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +26 27 | bbb = xisdir(p) +27 28 | bbbb = xisfile(p) +28 29 | bbbbb = xislink(p) +29 |-xreadlink(p) + 30 |+pathlib.Path(p).readlink() +30 31 | xstat(p) +31 32 | xisabs(p) +32 33 | xjoin(p, q) + +import_from_as.py:30:1: PTH116 `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` + | +28 | bbbbb = xislink(p) +29 | xreadlink(p) +30 | xstat(p) + | ^^^^^ PTH116 +31 | xisabs(p) +32 | xjoin(p, q) + | + +import_from_as.py:31:1: PTH117 [*] `os.path.isabs()` should be replaced by `Path.is_absolute()` + | +29 | xreadlink(p) +30 | xstat(p) +31 | xisabs(p) + | ^^^^^^ PTH117 +32 | xjoin(p, q) +33 | s.join((p, q)) + | + = help: Replace with `Path(...).is_absolute()` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +28 29 | bbbbb = xislink(p) +29 30 | xreadlink(p) +30 31 | xstat(p) +31 |-xisabs(p) + 32 |+pathlib.Path(p).is_absolute() +32 33 | xjoin(p, q) +33 34 | s.join((p, q)) +34 35 | s.join([p, q]) + +import_from_as.py:32:1: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator + | +30 | xstat(p) +31 | xisabs(p) +32 | xjoin(p, q) + | ^^^^^ PTH118 +33 | s.join((p, q)) +34 | s.join([p, q]) + | + +import_from_as.py:33:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +31 | xisabs(p) +32 | xjoin(p, q) +33 | s.join((p, q)) + | ^^^^^^ PTH118 +34 | s.join([p, q]) +35 | xbasename(p) + | + +import_from_as.py:34:1: PTH118 `os.sep.join()` should be replaced by `Path` with `/` operator + | +32 | xjoin(p, q) +33 | s.join((p, q)) +34 | s.join([p, q]) + | ^^^^^^ PTH118 +35 | xbasename(p) +36 | xdirname(p) + | + +import_from_as.py:35:1: PTH119 [*] `os.path.basename()` should be replaced by `Path.name` + | +33 | s.join((p, q)) +34 | s.join([p, q]) +35 | xbasename(p) + | ^^^^^^^^^ PTH119 +36 | xdirname(p) +37 | xsamefile(p) + | + = help: Replace with `Path(...).name` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +32 33 | xjoin(p, q) +33 34 | s.join((p, q)) +34 35 | s.join([p, q]) +35 |-xbasename(p) + 36 |+pathlib.Path(p).name +36 37 | xdirname(p) +37 38 | xsamefile(p) +38 39 | xsplitext(p) + +import_from_as.py:36:1: PTH120 [*] `os.path.dirname()` should be replaced by `Path.parent` + | +34 | s.join([p, q]) +35 | xbasename(p) +36 | xdirname(p) + | ^^^^^^^^ PTH120 +37 | xsamefile(p) +38 | xsplitext(p) + | + = help: Replace with `Path(...).parent` + +ℹ Safe fix +7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs +8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname +9 9 | from os.path import samefile as xsamefile, splitext as xsplitext + 10 |+import pathlib +10 11 | +11 12 | p = "/foo" +12 13 | q = "bar" +-------------------------------------------------------------------------------- +33 34 | s.join((p, q)) +34 35 | s.join([p, q]) +35 36 | xbasename(p) +36 |-xdirname(p) + 37 |+pathlib.Path(p).parent +37 38 | xsamefile(p) +38 39 | xsplitext(p) + +import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.samefile()` + | +35 | xbasename(p) +36 | xdirname(p) +37 | xsamefile(p) + | ^^^^^^^^^ PTH121 +38 | xsplitext(p) + | + +import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` + | +36 | xdirname(p) +37 | xsamefile(p) +38 | xsplitext(p) + | ^^^^^^^^^ PTH122 + | diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs index 28b9aa6bccd0b..2a36ae6f552b9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs @@ -1,50 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; -/// ## What it does -/// Checks for uses of `os.path.abspath`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.resolve()` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.abspath()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// file_path = os.path.abspath("../path/to/file") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// file_path = Path("../path/to/file").resolve() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve) -/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathAbspath; - -impl Violation for OsPathAbspath { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.abspath()` should be replaced by `Path.resolve()`".to_string() - } -} +use crate::Violation; /// ## What it does /// Checks for uses of `os.chmod`. @@ -274,141 +230,6 @@ impl Violation for OsReplace { } } -/// ## What it does -/// Checks for uses of `os.rmdir`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os`. When possible, using `Path` object -/// methods such as `Path.rmdir()` can improve readability over the `os` -/// module's counterparts (e.g., `os.rmdir()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.rmdir("folder/") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("folder/").rmdir() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir) -/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsRmdir; - -impl Violation for OsRmdir { - #[derive_message_formats] - fn message(&self) -> String { - "`os.rmdir()` should be replaced by `Path.rmdir()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.remove`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os`. When possible, using `Path` object -/// methods such as `Path.unlink()` can improve readability over the `os` -/// module's counterparts (e.g., `os.remove()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.remove("file.py") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("file.py").unlink() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink) -/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsRemove; - -impl Violation for OsRemove { - #[derive_message_formats] - fn message(&self) -> String { - "`os.remove()` should be replaced by `Path.unlink()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.unlink`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os`. When possible, using `Path` object -/// methods such as `Path.unlink()` can improve readability over the `os` -/// module's counterparts (e.g., `os.unlink()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.unlink("file.py") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("file.py").unlink() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink) -/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsUnlink; - -impl Violation for OsUnlink { - #[derive_message_formats] - fn message(&self) -> String { - "`os.unlink()` should be replaced by `Path.unlink()`".to_string() - } -} - /// ## What it does /// Checks for uses of `os.getcwd` and `os.getcwdb`. /// @@ -455,276 +276,6 @@ impl Violation for OsGetcwd { } } -/// ## What it does -/// Checks for uses of `os.path.exists`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.exists()` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.exists()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.exists("file.py") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("file.py").exists() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists) -/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathExists; - -impl Violation for OsPathExists { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.exists()` should be replaced by `Path.exists()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.path.expanduser`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.expanduser()` can improve readability over the `os.path` -/// module's counterparts (e.g., as `os.path.expanduser()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.expanduser("~/films/Monty Python") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("~/films/Monty Python").expanduser() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser) -/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathExpanduser; - -impl Violation for OsPathExpanduser { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.expanduser()` should be replaced by `Path.expanduser()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.path.isdir`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.is_dir()` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.isdir()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.isdir("docs") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("docs").is_dir() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir) -/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathIsdir; - -impl Violation for OsPathIsdir { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.isdir()` should be replaced by `Path.is_dir()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.path.isfile`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.is_file()` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.isfile()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.isfile("docs") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("docs").is_file() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file) -/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathIsfile; - -impl Violation for OsPathIsfile { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.isfile()` should be replaced by `Path.is_file()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.path.islink`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.is_symlink()` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.islink()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.islink("docs") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path("docs").is_symlink() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink) -/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathIslink; - -impl Violation for OsPathIslink { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.islink()` should be replaced by `Path.is_symlink()`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.readlink`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os`. When possible, using `Path` object -/// methods such as `Path.readlink()` can improve readability over the `os` -/// module's counterparts (e.g., `os.readlink()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.readlink(file_name) -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path(file_name).readlink() -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline) -/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsReadlink; - -impl Violation for OsReadlink { - #[derive_message_formats] - fn message(&self) -> String { - "`os.readlink()` should be replaced by `Path.readlink()`".to_string() - } -} - /// ## What it does /// Checks for uses of `os.stat`. /// @@ -780,53 +331,6 @@ impl Violation for OsStat { } } -/// ## What it does -/// Checks for uses of `os.path.isabs`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.is_absolute()` can improve readability over the `os.path` -/// module's counterparts (e.g., as `os.path.isabs()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// if os.path.isabs(file_name): -/// print("Absolute path!") -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// if Path(file_name).is_absolute(): -/// print("Absolute path!") -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute) -/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathIsabs; - -impl Violation for OsPathIsabs { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.isabs()` should be replaced by `Path.is_absolute()`".to_string() - } -} - /// ## What it does /// Checks for uses of `os.path.join`. /// @@ -889,96 +393,6 @@ pub(crate) enum Joiner { Joinpath, } -/// ## What it does -/// Checks for uses of `os.path.basename`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.name` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.basename()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.basename(__file__) -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path(__file__).name -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name) -/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathBasename; - -impl Violation for OsPathBasename { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.basename()` should be replaced by `Path.name`".to_string() - } -} - -/// ## What it does -/// Checks for uses of `os.path.dirname`. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation, as compared to -/// the lower-level API offered by `os.path`. When possible, using `Path` object -/// methods such as `Path.parent` can improve readability over the `os.path` -/// module's counterparts (e.g., `os.path.dirname()`). -/// -/// ## Examples -/// ```python -/// import os -/// -/// os.path.dirname(__file__) -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// Path(__file__).parent -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than the lower-level alternatives that work directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent) -/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname) -/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) -/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) -/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) -/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) -#[derive(ViolationMetadata)] -pub(crate) struct OsPathDirname; - -impl Violation for OsPathDirname { - #[derive_message_formats] - fn message(&self) -> String { - "`os.path.dirname()` should be replaced by `Path.parent`".to_string() - } -} - /// ## What it does /// Checks for uses of `os.path.samefile`. /// @@ -1215,3 +629,45 @@ impl Violation for OsListdir { "Use `pathlib.Path.iterdir()` instead.".to_string() } } + +/// ## What it does +/// Checks for uses of `os.symlink`. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation, as compared to +/// the lower-level API offered by `os.symlink`. +/// +/// ## Example +/// ```python +/// import os +/// +/// os.symlink("usr/bin/python", "tmp/python", target_is_directory=False) +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// Path("tmp/python").symlink_to("usr/bin/python") +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than the lower-level alternatives that work directly with strings, +/// especially on older versions of Python. +/// +/// ## References +/// - [Python documentation: `Path.symlink_to`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.symlink_to) +/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/) +/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) +/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/) +/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/) +#[derive(ViolationMetadata)] +pub(crate) struct OsSymlink; + +impl Violation for OsSymlink { + #[derive_message_formats] + fn message(&self) -> String { + "`os.symlink` should be replaced by `Path.symlink_to`".to_string() + } +} diff --git a/crates/ruff_linter/src/rules/flynt/helpers.rs b/crates/ruff_linter/src/rules/flynt/helpers.rs index a71b369b6987f..58137a38165d8 100644 --- a/crates/ruff_linter/src/rules/flynt/helpers.rs +++ b/crates/ruff_linter/src/rules/flynt/helpers.rs @@ -2,21 +2,23 @@ use ruff_python_ast::{self as ast, Arguments, ConversionFlag, Expr}; use ruff_text_size::TextRange; /// Wrap an expression in a [`ast::FStringElement::Expression`] with no special formatting. -fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement { - ast::FStringElement::Expression(ast::FStringExpressionElement { +fn to_interpolated_string_interpolation_element(inner: &Expr) -> ast::InterpolatedStringElement { + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression: Box::new(inner.clone()), debug_text: None, conversion: ConversionFlag::None, format_spec: None, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } -/// Convert a string to a [`ast::FStringElement::Literal`]. -pub(super) fn to_f_string_literal_element(s: &str) -> ast::FStringElement { - ast::FStringElement::Literal(ast::FStringLiteralElement { +/// Convert a string to a [`ast::InterpolatedStringLiteralElement `]. +pub(super) fn to_interpolated_string_literal_element(s: &str) -> ast::InterpolatedStringElement { + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { value: Box::from(s), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } @@ -31,8 +33,10 @@ fn is_simple_call(expr: &Expr) -> bool { args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, }) => args.is_empty() && keywords.is_empty() && is_simple_callee(func), _ => false, } @@ -48,20 +52,29 @@ fn is_simple_callee(func: &Expr) -> bool { } } -/// Convert an expression to a f-string element (if it looks like a good idea). -pub(super) fn to_f_string_element(expr: &Expr) -> Option { +/// Convert an expression to an f-string or t-string element (if it looks like a good idea). +pub(super) fn to_interpolated_string_element( + expr: &Expr, +) -> Option { match expr { - Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => { - Some(ast::FStringElement::Literal(ast::FStringLiteralElement { + Expr::StringLiteral(ast::ExprStringLiteral { + value, + range, + node_index, + }) => Some(ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value: value.to_string().into_boxed_str(), range: *range, - })) - } + node_index: node_index.clone(), + }, + )), // These should be pretty safe to wrap in a formatted value. Expr::NumberLiteral(_) | Expr::BooleanLiteral(_) | Expr::Name(_) | Expr::Attribute(_) => { - Some(to_f_string_expression_element(expr)) + Some(to_interpolated_string_interpolation_element(expr)) + } + Expr::Call(_) if is_simple_call(expr) => { + Some(to_interpolated_string_interpolation_element(expr)) } - Expr::Call(_) if is_simple_call(expr) => Some(to_f_string_expression_element(expr)), _ => None, } } diff --git a/crates/ruff_linter/src/rules/flynt/mod.rs b/crates/ruff_linter/src/rules/flynt/mod.rs index 28d49c212518a..373e6cfc4cb93 100644 --- a/crates/ruff_linter/src/rules/flynt/mod.rs +++ b/crates/ruff_linter/src/rules/flynt/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::StaticJoinToFString, Path::new("FLY002.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -20,7 +20,7 @@ mod tests { Path::new("flynt").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index 309ec0ccc70a1..6ac651fb66563 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -1,14 +1,14 @@ use ast::FStringFlags; use itertools::Itertools; -use crate::fix::edits::pad; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::fix::edits::pad; use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Edit, Fix}; use crate::rules::flynt::helpers; @@ -28,6 +28,14 @@ use crate::rules::flynt::helpers; /// f"{foo} {bar}" /// ``` /// +/// ## Fix safety +/// The fix is always marked unsafe because the evaluation of the f-string +/// expressions will default to calling the `__format__` method of each +/// object, whereas `str.join` expects each object to be an instance of +/// `str` and uses the corresponding string. Therefore it is possible for +/// the values of the resulting strings to differ, or for one expression +/// to raise an exception while the other does not. +/// /// ## References /// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[derive(ViolationMetadata)] @@ -83,6 +91,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< .into_boxed_str(), flags: flags?, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; return Some(node.into()); } @@ -97,14 +106,15 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< return None; } if !std::mem::take(&mut first) { - f_string_elements.push(helpers::to_f_string_literal_element(joiner)); + f_string_elements.push(helpers::to_interpolated_string_literal_element(joiner)); } - f_string_elements.push(helpers::to_f_string_element(expr)?); + f_string_elements.push(helpers::to_interpolated_string_element(expr)?); } let node = ast::FString { elements: f_string_elements.into(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), flags, }; Some(node.into()) @@ -144,7 +154,7 @@ pub(crate) fn static_join_to_fstring(checker: &Checker, expr: &Expr, joiner: &st let contents = checker.generator().expr(&new_expr); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( StaticJoinToFString { expression: SourceCodeSnippet::new(contents.clone()), }, @@ -154,5 +164,4 @@ pub(crate) fn static_join_to_fstring(checker: &Checker, expr: &Expr, joiner: &st pad(contents, expr.range(), checker.locator()), expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/isort/annotate.rs b/crates/ruff_linter/src/rules/isort/annotate.rs index f3f1a247b2d4f..ccbcc84b194f5 100644 --- a/crates/ruff_linter/src/rules/isort/annotate.rs +++ b/crates/ruff_linter/src/rules/isort/annotate.rs @@ -23,7 +23,11 @@ pub(crate) fn annotate_imports<'a>( .iter() .map(|import| { match import { - Stmt::Import(ast::StmtImport { names, range }) => { + Stmt::Import(ast::StmtImport { + names, + range, + node_index: _, + }) => { // Find comments above. let mut atop = vec![]; while let Some(comment) = @@ -59,6 +63,7 @@ pub(crate) fn annotate_imports<'a>( names, level, range: _, + node_index: _, }) => { // Find comments above. let mut atop = vec![]; diff --git a/crates/ruff_linter/src/rules/isort/block.rs b/crates/ruff_linter/src/rules/isort/block.rs index c982c01e1195f..1dfe96dc5172f 100644 --- a/crates/ruff_linter/src/rules/isort/block.rs +++ b/crates/ruff_linter/src/rules/isort/block.rs @@ -6,9 +6,9 @@ use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use crate::Locator; use crate::directives::IsortDirectives; use crate::rules::isort::helpers; -use crate::Locator; /// A block of imports within a Python module. #[derive(Debug, Default)] diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index 1e5d39b3fcf42..aee53af6579a4 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use std::fmt; +use std::fs; +use std::iter; use std::path::{Path, PathBuf}; -use std::{fs, iter}; use log::debug; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; @@ -79,16 +80,16 @@ enum Reason<'a> { Future, KnownStandardLibrary, SamePackage, - #[allow(dead_code)] + #[expect(dead_code)] SourceMatch(&'a Path), NoMatch, UserDefinedSection, NoSections, - #[allow(dead_code)] + #[expect(dead_code)] DisabledSection(&'a ImportSection), } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn categorize<'a>( module_name: &str, is_relative: bool, @@ -100,6 +101,7 @@ pub(crate) fn categorize<'a>( no_sections: bool, section_order: &'a [ImportSection], default_section: &'a ImportSection, + match_source_strategy: MatchSourceStrategy, ) -> &'a ImportSection { let module_base = module_name.split('.').next().unwrap(); let (mut import_type, mut reason) = { @@ -127,7 +129,7 @@ pub(crate) fn categorize<'a>( &ImportSection::Known(ImportType::FirstParty), Reason::SamePackage, ) - } else if let Some(src) = match_sources(src, module_base) { + } else if let Some(src) = match_sources(src, module_name, match_source_strategy) { ( &ImportSection::Known(ImportType::FirstParty), Reason::SourceMatch(src), @@ -156,23 +158,67 @@ fn same_package(package: Option>, module_base: &str) -> bool { .is_some_and(|package| package.ends_with(module_base)) } -fn match_sources<'a>(paths: &'a [PathBuf], base: &str) -> Option<&'a Path> { - for path in paths { - if let Ok(metadata) = fs::metadata(path.join(base)) { - if metadata.is_dir() { - return Some(path); +/// Returns the source path with respect to which the module `name` +/// should be considered first party, or `None` if no path is found. +/// +/// The [`MatchSourceStrategy`] is the criterion used to decide whether +/// the module path matches a given source directory. +/// +/// # Examples +/// +/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory, +/// no matter the strategy. +/// +/// - With `match_source_strategy == MatchSourceStrategy::Root`, the module +/// named `foo.baz` will match `[SRC]` if `[SRC]/foo` is a +/// directory or `[SRC]/foo.py` exists. +/// +/// - With `match_source_stratgy == MatchSourceStrategy::FullPath`, the module +/// named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz` is a directory, +/// or `[SRC]/foo/baz.py` exists or `[SRC]/foo/baz.pyi` exists. +fn match_sources<'a>( + paths: &'a [PathBuf], + name: &str, + match_source_strategy: MatchSourceStrategy, +) -> Option<&'a Path> { + match match_source_strategy { + MatchSourceStrategy::Root => { + let base = name.split('.').next()?; + for path in paths { + if let Ok(metadata) = fs::metadata(path.join(base)) { + if metadata.is_dir() { + return Some(path); + } + } + if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) { + if metadata.is_file() { + return Some(path); + } + } } + None } - if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) { - if metadata.is_file() { - return Some(path); + MatchSourceStrategy::FullPath => { + let relative_path: PathBuf = name.split('.').collect(); + relative_path.components().next()?; + for root in paths { + let candidate = root.join(&relative_path); + if candidate.is_dir() { + return Some(root); + } + if ["py", "pyi"] + .into_iter() + .any(|extension| candidate.with_extension(extension).is_file()) + { + return Some(root); + } } + None } } - None } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn categorize_imports<'a>( block: ImportBlock<'a>, src: &[PathBuf], @@ -183,6 +229,7 @@ pub(crate) fn categorize_imports<'a>( no_sections: bool, section_order: &'a [ImportSection], default_section: &'a ImportSection, + match_source_strategy: MatchSourceStrategy, ) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> { let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default(); // Categorize `Stmt::Import`. @@ -198,6 +245,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(import_type) @@ -218,6 +266,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(classification) @@ -238,6 +287,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(classification) @@ -258,6 +308,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(classification) @@ -317,7 +368,9 @@ impl KnownModules { let mut seen = FxHashSet::with_capacity_and_hasher(known.len(), FxBuildHasher); for (module, _) in &known { if !seen.insert(module) { - warn_user_once!("One or more modules are part of multiple import sections, including: `{module}`"); + warn_user_once!( + "One or more modules are part of multiple import sections, including: `{module}`" + ); break; } } @@ -409,3 +462,463 @@ impl fmt::Display for KnownModules { Ok(()) } } + +/// Rule to determine whether a module path matches +/// a relative path from a source directory. +#[derive(Debug, Clone, Copy)] +pub(crate) enum MatchSourceStrategy { + /// Matches if first term in module path is found in file system + /// + /// # Example + /// Module is `foo.bar.baz` and `[SRC]/foo` exists + Root, + /// Matches only if full module path is reflected in file system + /// + /// # Example + /// Module is `foo.bar.baz` and `[SRC]/foo/bar/baz` exists + FullPath, +} + +#[cfg(test)] +mod tests { + use crate::rules::isort::categorize::{MatchSourceStrategy, match_sources}; + + use std::fs; + use std::path::{Path, PathBuf}; + use tempfile::tempdir; + + /// Helper function to create a file with parent directories + fn create_file>(path: P) { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, "").unwrap(); + } + + /// Helper function to create a directory and all parent directories + fn create_dir>(path: P) { + fs::create_dir_all(path).unwrap(); + } + + /// Tests a traditional Python package layout: + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// ├── module1.py + /// └── module2.py + /// ``` + #[test] + fn test_traditional_layout() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create traditional layout + create_dir(project_dir.join("mypackage")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/module1.py")); + create_file(project_dir.join("mypackage/module2.py")); + + let paths = vec![project_dir.clone()]; + + // Test with Root strategy + + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "nonexistent", MatchSourceStrategy::Root), + None + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "mypackage.nonexistent", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a src-based Python package layout: + /// ``` + /// project/ + /// └── src/ + /// └── mypackage/ + /// ├── __init__.py + /// └── module1.py + /// ``` + #[test] + fn test_src_layout() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + let src_dir = project_dir.join("src"); + + // Create src layout + create_dir(src_dir.join("mypackage")); + create_file(src_dir.join("mypackage/__init__.py")); + create_file(src_dir.join("mypackage/module1.py")); + + let paths = vec![src_dir.clone()]; + + // Test with Root strategy + + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + Some(src_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root), + Some(src_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root), + Some(src_dir.as_path()) + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath), + Some(src_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "mypackage.nonexistent", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a nested package layout: + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// ├── module1.py + /// └── subpackage/ + /// ├── __init__.py + /// └── module2.py + /// ``` + #[test] + fn test_nested_packages() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create nested package layout + create_dir(project_dir.join("mypackage/subpackage")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/module1.py")); + create_file(project_dir.join("mypackage/subpackage/__init__.py")); + create_file(project_dir.join("mypackage/subpackage/module2.py")); + + let paths = vec![project_dir.clone()]; + + // Test with Root strategy + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.subpackage", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources( + &paths, + "mypackage.subpackage.module2", + MatchSourceStrategy::FullPath + ), + Some(project_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "mypackage.subpackage.nonexistent", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a namespace package layout (PEP 420): + /// ``` + /// project/ + /// └── namespace/ # No __init__.py (namespace package) + /// └── package1/ + /// ├── __init__.py + /// └── module1.py + /// ``` + #[test] + fn test_namespace_packages() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create namespace package layout + create_dir(project_dir.join("namespace/package1")); + create_file(project_dir.join("namespace/package1/__init__.py")); + create_file(project_dir.join("namespace/package1/module1.py")); + + let paths = vec![project_dir.clone()]; + // Test with Root strategy + + assert_eq!( + match_sources(&paths, "namespace", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "namespace.package1", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources( + &paths, + "namespace.package2.module1", + MatchSourceStrategy::Root + ), + Some(project_dir.as_path()) + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources(&paths, "namespace.package1", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources( + &paths, + "namespace.package1.module1", + MatchSourceStrategy::FullPath + ), + Some(project_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "namespace.package2.module1", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a package with type stubs (.pyi files): + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// └── module1.pyi # Only .pyi file, no .py + /// ``` + #[test] + fn test_type_stubs() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create package with type stub + create_dir(project_dir.join("mypackage")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/module1.pyi")); // Only create .pyi file, not .py + + // Test with FullPath strategy + let paths = vec![project_dir.clone()]; + + // Module "mypackage.module1" should match project_dir using .pyi file + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + } + + /// Tests a package with both a module and a directory having the same name: + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// ├── feature.py # Module with same name as directory + /// └── feature/ # Directory with same name as module + /// ├── __init__.py + /// └── submodule.py + /// ``` + #[test] + fn test_same_name_module_and_directory() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create package with module and directory of the same name + create_dir(project_dir.join("mypackage/feature")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/feature.py")); // Module with same name as directory + create_file(project_dir.join("mypackage/feature/__init__.py")); + create_file(project_dir.join("mypackage/feature/submodule.py")); + + // Test with Root strategy + let paths = vec![project_dir.clone()]; + + // Module "mypackage.feature" should match project_dir (matches the file first) + assert_eq!( + match_sources(&paths, "mypackage.feature", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + // Test with FullPath strategy + + // Module "mypackage.feature" should match project_dir + assert_eq!( + match_sources(&paths, "mypackage.feature", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + // Module "mypackage.feature.submodule" should match project_dir + assert_eq!( + match_sources( + &paths, + "mypackage.feature.submodule", + MatchSourceStrategy::FullPath + ), + Some(project_dir.as_path()) + ); + } + + /// Tests multiple source directories with different packages: + /// ``` + /// project1/ + /// └── package1/ + /// ├── __init__.py + /// └── module1.py + /// + /// project2/ + /// └── package2/ + /// ├── __init__.py + /// └── module2.py + /// ``` + #[test] + fn test_multiple_source_paths() { + let temp_dir = tempdir().unwrap(); + let project1_dir = temp_dir.path().join("project1"); + let project2_dir = temp_dir.path().join("project2"); + + // Create files in project1 + create_dir(project1_dir.join("package1")); + create_file(project1_dir.join("package1/__init__.py")); + create_file(project1_dir.join("package1/module1.py")); + + // Create files in project2 + create_dir(project2_dir.join("package2")); + create_file(project2_dir.join("package2/__init__.py")); + create_file(project2_dir.join("package2/module2.py")); + + // Test with multiple paths in search order + let paths = vec![project1_dir.clone(), project2_dir.clone()]; + + // Module "package1" should match project1_dir + assert_eq!( + match_sources(&paths, "package1", MatchSourceStrategy::Root), + Some(project1_dir.as_path()) + ); + + // Module "package2" should match project2_dir + assert_eq!( + match_sources(&paths, "package2", MatchSourceStrategy::Root), + Some(project2_dir.as_path()) + ); + + // Try with reversed order to check search order + let paths_reversed = vec![project2_dir, project1_dir.clone()]; + + // Module "package1" should still match project1_dir + assert_eq!( + match_sources(&paths_reversed, "package1", MatchSourceStrategy::Root), + Some(project1_dir.as_path()) + ); + } + + /// Tests behavior with an empty module name + /// ``` + /// project/ + /// └── mypackage/ + /// ``` + /// + /// In theory this should never happen since we expect + /// module names to have been normalized by the time we + /// call `match_sources`. But it is worth noting that the + /// behavior is different depending on the [`MatchSourceStrategy`] + #[test] + fn test_empty_module_name() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + create_dir(project_dir.join("mypackage")); + + let paths = vec![project_dir.clone()]; + + assert_eq!( + match_sources(&paths, "", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + assert_eq!( + match_sources(&paths, "", MatchSourceStrategy::FullPath), + None + ); + } + + /// Tests behavior with an empty list of source paths + #[test] + fn test_empty_paths() { + let paths: Vec = vec![]; + + // Empty paths should return None + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + None + ); + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath), + None + ); + } +} diff --git a/crates/ruff_linter/src/rules/isort/format.rs b/crates/ruff_linter/src/rules/isort/format.rs index 29639d27d5600..e7210cd8dabee 100644 --- a/crates/ruff_linter/src/rules/isort/format.rs +++ b/crates/ruff_linter/src/rules/isort/format.rs @@ -40,7 +40,7 @@ pub(crate) fn format_import( } /// Add an import-from statement to the [`RopeBuilder`]. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn format_import_from( import_from: &ImportFromData, comments: &ImportFromCommentSet, diff --git a/crates/ruff_linter/src/rules/isort/helpers.rs b/crates/ruff_linter/src/rules/isort/helpers.rs index 1d2a93a65d882..f144a56993282 100644 --- a/crates/ruff_linter/src/rules/isort/helpers.rs +++ b/crates/ruff_linter/src/rules/isort/helpers.rs @@ -4,8 +4,8 @@ use ruff_python_trivia::PythonWhitespace; use ruff_source_file::UniversalNewlines; use ruff_text_size::Ranged; -use crate::rules::isort::types::TrailingComma; use crate::Locator; +use crate::rules::isort::types::TrailingComma; /// Return `true` if a `Stmt::ImportFrom` statement ends with a magic /// trailing comma. diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 40e0d6f9da112..5b822e825d6a8 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -5,8 +5,8 @@ use std::path::PathBuf; use annotate::annotate_imports; use block::{Block, Trailer}; pub(crate) use categorize::categorize; -use categorize::categorize_imports; pub use categorize::{ImportSection, ImportType}; +use categorize::{MatchSourceStrategy, categorize_imports}; use comments::Comment; use normalize::normalize_imports; use order::order_imports; @@ -17,9 +17,9 @@ use settings::Settings; use types::EitherImport::{Import, ImportFrom}; use types::{AliasData, ImportBlock, TrailingComma}; +use crate::Locator; use crate::line_width::{LineLength, LineWidthBuilder}; use crate::package::PackageRoot; -use crate::Locator; use ruff_python_ast::PythonVersion; mod annotate; @@ -63,7 +63,7 @@ pub(crate) enum AnnotatedImport<'a> { }, } -#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] +#[expect(clippy::too_many_arguments)] pub(crate) fn format_imports( block: &Block, comments: Vec, @@ -76,6 +76,7 @@ pub(crate) fn format_imports( source_type: PySourceType, target_version: PythonVersion, settings: &Settings, + match_source_strategy: MatchSourceStrategy, tokens: &Tokens, ) -> String { let trailer = &block.trailer; @@ -103,6 +104,7 @@ pub(crate) fn format_imports( package, target_version, settings, + match_source_strategy, ); if !block_output.is_empty() && !output.is_empty() { @@ -149,7 +151,7 @@ pub(crate) fn format_imports( output } -#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] +#[expect(clippy::too_many_arguments)] fn format_import_block( block: ImportBlock, line_length: LineLength, @@ -159,6 +161,7 @@ fn format_import_block( package: Option>, target_version: PythonVersion, settings: &Settings, + match_source_strategy: MatchSourceStrategy, ) -> String { #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum LineInsertion { @@ -169,7 +172,6 @@ fn format_import_block( Inserted, } - // Categorize by type (e.g., first-party vs. third-party). let mut block_by_type = categorize_imports( block, src, @@ -180,6 +182,7 @@ fn format_import_block( settings.no_sections, &settings.section_order, &settings.default_section, + match_source_strategy, ); let mut output = String::new(); @@ -287,9 +290,8 @@ mod tests { use test_case::test_case; use ruff_python_semantic::{MemberNameImport, ModuleNameImport, NameImport}; - use ruff_text_size::Ranged; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::rules::isort::categorize::{ImportSection, KnownModules}; use crate::settings::LinterSettings; @@ -360,7 +362,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -388,7 +390,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -412,7 +414,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -436,7 +438,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -451,7 +453,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - crate::assert_messages!(snapshot, diagnostics); + crate::assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -475,7 +477,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -500,7 +502,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -518,7 +520,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -542,7 +544,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -560,7 +562,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -579,7 +581,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -597,7 +599,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -619,7 +621,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -637,7 +639,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -655,8 +657,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -683,8 +685,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -713,8 +715,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -741,8 +743,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -763,8 +765,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -783,8 +785,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -817,7 +819,7 @@ mod tests { ..LinterSettings::for_rule(Rule::MissingRequiredImport) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -851,7 +853,7 @@ mod tests { ..LinterSettings::for_rule(Rule::MissingRequiredImport) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -879,7 +881,7 @@ mod tests { }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -902,7 +904,7 @@ mod tests { ..LinterSettings::for_rules([Rule::MissingRequiredImport, Rule::UselessImportAlias]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -932,7 +934,7 @@ mod tests { ..LinterSettings::for_rule(Rule::MissingRequiredImport) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -955,7 +957,7 @@ mod tests { ..LinterSettings::for_rule(Rule::MissingRequiredImport) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -989,7 +991,7 @@ mod tests { ..LinterSettings::for_rules([Rule::MissingRequiredImport, Rule::UnusedImport]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1008,7 +1010,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1026,7 +1028,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1044,7 +1046,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1079,7 +1081,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1103,7 +1105,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1127,8 +1129,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1152,8 +1154,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1174,8 +1176,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1195,8 +1197,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(&*snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(&*snapshot, diagnostics); Ok(()) } @@ -1214,8 +1216,8 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - diagnostics.sort_by_key(Ranged::start); - assert_messages!(snapshot, diagnostics); + diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start()); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1238,7 +1240,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1271,7 +1273,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1303,7 +1305,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1327,7 +1329,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1344,7 +1346,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -1361,7 +1363,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -1384,7 +1386,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -1404,7 +1406,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnsortedImports) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/isort/normalize.rs b/crates/ruff_linter/src/rules/isort/normalize.rs index 9feb16456634e..b59113a11d052 100644 --- a/crates/ruff_linter/src/rules/isort/normalize.rs +++ b/crates/ruff_linter/src/rules/isort/normalize.rs @@ -1,6 +1,6 @@ +use super::AnnotatedImport; use super::settings::Settings; use super::types::{AliasData, ImportBlock, ImportFromData, TrailingComma}; -use super::AnnotatedImport; pub(crate) fn normalize_imports<'a>( imports: Vec>, diff --git a/crates/ruff_linter/src/rules/isort/order.rs b/crates/ruff_linter/src/rules/isort/order.rs index 6555101aad251..fca47f8775d24 100644 --- a/crates/ruff_linter/src/rules/isort/order.rs +++ b/crates/ruff_linter/src/rules/isort/order.rs @@ -54,7 +54,7 @@ pub(crate) fn order_imports<'a>( }, ); - let ordered_imports = if matches!(section, ImportSection::Known(ImportType::Future)) { + if matches!(section, ImportSection::Known(ImportType::Future)) { from_imports .sorted_by_cached_key(|(import_from, _, _, aliases)| { ModuleKey::from_module( @@ -140,7 +140,5 @@ pub(crate) fn order_imports<'a>( .chain(ordered_from_imports.into_iter().map(ImportFrom)) .collect() } - }; - - ordered_imports + } } diff --git a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs index b3c8d6cd1c04c..bff7e42137ee0 100644 --- a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_docstring_stmt; use ruff_python_ast::{self as ast, ModModule, PySourceType, Stmt}; use ruff_python_codegen::Stylist; @@ -7,9 +6,11 @@ use ruff_python_parser::Parsed; use ruff_python_semantic::{FutureImport, NameImport}; use ruff_text_size::{TextRange, TextSize}; +use crate::Locator; +use crate::checkers::ast::LintContext; use crate::importer::Importer; use crate::settings::LinterSettings; -use crate::Locator; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Adds any required imports, as specified by the user, to the top of the @@ -57,7 +58,12 @@ impl AlwaysFixableViolation for MissingRequiredImport { fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { match target { NameImport::Import(target) => { - let Stmt::Import(ast::StmtImport { names, range: _ }) = &stmt else { + let Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) = &stmt + else { return false; }; names.iter().any(|alias| { @@ -71,6 +77,7 @@ fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { names, level, range: _, + node_index: _, }) = &stmt else { return false; @@ -85,22 +92,22 @@ fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { } } -#[allow(clippy::too_many_arguments)] fn add_required_import( required_import: &NameImport, parsed: &Parsed, locator: &Locator, stylist: &Stylist, source_type: PySourceType, -) -> Option { + context: &LintContext, +) { // Don't add imports to semantically-empty files. if parsed.suite().iter().all(is_docstring_stmt) { - return None; + return; } // We don't need to add `__future__` imports to stubs. if source_type.is_stub() && required_import.is_future_import() { - return None; + return; } // If the import is already present in a top-level block, don't add it. @@ -109,18 +116,17 @@ fn add_required_import( .iter() .any(|stmt| includes_import(stmt, required_import)) { - return None; + return; } // Always insert the diagnostic at top-of-file. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = context.report_diagnostic( MissingRequiredImport(required_import.to_string()), TextRange::default(), ); diagnostic.set_fix(Fix::safe_edit( Importer::new(parsed, locator, stylist).add_import(required_import, TextSize::default()), )); - Some(diagnostic) } /// I002 @@ -130,13 +136,16 @@ pub(crate) fn add_required_imports( stylist: &Stylist, settings: &LinterSettings, source_type: PySourceType, -) -> Vec { - settings - .isort - .required_imports - .iter() - .filter_map(|required_import| { - add_required_import(required_import, parsed, locator, stylist, source_type) - }) - .collect() + context: &LintContext, +) { + for required_import in &settings.isort.required_imports { + add_required_import( + required_import, + parsed, + locator, + stylist, + source_type, + context, + ); + } } diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index 4ea44ee823924..4f379b503acd4 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -1,22 +1,25 @@ use itertools::{EitherOrBoth, Itertools}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::whitespace::trailing_lines_end; use ruff_python_ast::{PySourceType, PythonVersion, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::Tokens; -use ruff_python_trivia::{leading_indentation, textwrap::indent, PythonWhitespace}; +use ruff_python_trivia::{PythonWhitespace, leading_indentation, textwrap::indent}; use ruff_source_file::{LineRanges, UniversalNewlines}; use ruff_text_size::{Ranged, TextRange}; -use super::super::block::Block; -use super::super::{comments, format_imports}; +use crate::Locator; +use crate::checkers::ast::LintContext; use crate::line_width::LineWidthBuilder; use crate::package::PackageRoot; +use crate::preview::is_full_path_match_source_strategy_enabled; +use crate::rules::isort::block::Block; +use crate::rules::isort::categorize::MatchSourceStrategy; +use crate::rules::isort::{comments, format_imports}; use crate::settings::LinterSettings; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// De-duplicates, groups, and sorts imports based on the provided `isort` settings. @@ -36,6 +39,13 @@ use crate::Locator; /// import numpy as np /// import pandas /// ``` +/// +/// ## Preview +/// When [`preview`](https://docs.astral.sh/ruff/preview/) mode is enabled, Ruff applies a stricter criterion +/// for determining whether an import should be classified as first-party. +/// Specifically, for an import of the form `import foo.bar.baz`, Ruff will +/// check that `foo/bar`, relative to a [user-specified `src`](https://docs.astral.sh/ruff/settings/#src) directory, contains either +/// the directory `baz` or else a file with the name `baz.py` or `baz.pyi`. #[derive(ViolationMetadata)] pub(crate) struct UnsortedImports; @@ -77,7 +87,7 @@ fn matches_ignoring_indentation(val1: &str, val2: &str) -> bool { }) } -#[allow(clippy::cast_sign_loss, clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] /// I001 pub(crate) fn organize_imports( block: &Block, @@ -89,7 +99,8 @@ pub(crate) fn organize_imports( source_type: PySourceType, tokens: &Tokens, target_version: PythonVersion, -) -> Option { + context: &LintContext, +) { let indentation = locator.slice(extract_indentation_range(&block.imports, locator)); let indentation = leading_indentation(indentation); @@ -101,7 +112,8 @@ pub(crate) fn organize_imports( || indexer .followed_by_multi_statement_line(block.imports.last().unwrap(), locator.contents()) { - return Some(Diagnostic::new(UnsortedImports, range)); + context.report_diagnostic(UnsortedImports, range); + return; } // Extract comments. Take care to grab any inline comments from the last line. @@ -117,6 +129,12 @@ pub(crate) fn organize_imports( trailing_lines_end(block.imports.last().unwrap(), locator.contents()) }; + let match_source_strategy = if is_full_path_match_source_strategy_enabled(settings) { + MatchSourceStrategy::FullPath + } else { + MatchSourceStrategy::Root + }; + // Generate the sorted import block. let expected = format_imports( block, @@ -130,6 +148,7 @@ pub(crate) fn organize_imports( source_type, target_version, &settings.isort, + match_source_strategy, tokens, ); @@ -137,12 +156,11 @@ pub(crate) fn organize_imports( let fix_range = TextRange::new(locator.line_start(range.start()), trailing_line_end); let actual = locator.slice(fix_range); if matches_ignoring_indentation(actual, &expected) { - return None; + return; } - let mut diagnostic = Diagnostic::new(UnsortedImports, range); + let mut diagnostic = context.report_diagnostic(UnsortedImports, range); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( indent(&expected, indentation).to_string(), fix_range, ))); - Some(diagnostic) } diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index efed45bce84bb..05a4dddf081ae 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use crate::display_settings; -use crate::rules::isort::categorize::KnownModules; use crate::rules::isort::ImportType; +use crate::rules::isort::categorize::KnownModules; use ruff_macros::CacheKey; use ruff_python_semantic::{Alias, MemberNameImport, ModuleNameImport, NameImport}; @@ -45,7 +45,7 @@ impl Display for RelativeImportsOrder { } #[derive(Debug, Clone, CacheKey)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct Settings { pub required_imports: BTreeSet, pub combine_as_imports: bool, diff --git a/crates/ruff_linter/src/rules/mccabe/mod.rs b/crates/ruff_linter/src/rules/mccabe/mod.rs index e8d1d3313f163..f90cb4065ba57 100644 --- a/crates/ruff_linter/src/rules/mccabe/mod.rs +++ b/crates/ruff_linter/src/rules/mccabe/mod.rs @@ -9,7 +9,7 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; use crate::test::test_path; @@ -26,7 +26,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::ComplexStructure]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs index e81dfcb02e147..3648c48239c29 100644 --- a/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff_linter/src/rules/mccabe/rules/function_is_too_complex.rs @@ -1,8 +1,10 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::identifier::Identifier; +use crate::Violation; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks for functions with a high `McCabe` complexity. @@ -152,24 +154,24 @@ fn get_complexity_number(stmts: &[Stmt]) -> usize { complexity } +/// C901 pub(crate) fn function_is_too_complex( + checker: &Checker, stmt: &Stmt, name: &str, body: &[Stmt], max_complexity: usize, -) -> Option { +) { let complexity = get_complexity_number(body) + 1; if complexity > max_complexity { - Some(Diagnostic::new( + checker.report_diagnostic( ComplexStructure { name: name.to_string(), complexity, max_complexity, }, stmt.identifier(), - )) - } else { - None + ); } } diff --git a/crates/ruff_linter/src/rules/numpy/helpers.rs b/crates/ruff_linter/src/rules/numpy/helpers.rs index 39ee1c1767a4b..a2e5eb6a1502f 100644 --- a/crates/ruff_linter/src/rules/numpy/helpers.rs +++ b/crates/ruff_linter/src/rules/numpy/helpers.rs @@ -1,5 +1,10 @@ +use ruff_python_ast::Expr; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_ast::{statement_visitor, Alias, Stmt, StmtImportFrom}; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::visitor::{walk_expr, walk_stmt}; +use ruff_python_ast::{Alias, Stmt, StmtImportFrom, statement_visitor}; +use ruff_python_semantic::SemanticModel; /// AST visitor that searches an AST tree for [`ast::StmtImportFrom`] nodes /// that match a certain [`QualifiedName`]. @@ -43,3 +48,57 @@ impl StatementVisitor<'_> for ImportSearcher<'_> { } } } + +/// AST visitor that searches an AST tree for [`ast::ExprAttribute`] nodes +/// that match a certain [`QualifiedName`]. +pub(crate) struct AttributeSearcher<'a> { + attribute_to_find: QualifiedName<'a>, + semantic: &'a SemanticModel<'a>, + pub found_attribute: bool, +} + +impl<'a> AttributeSearcher<'a> { + pub(crate) fn new( + attribute_to_find: QualifiedName<'a>, + semantic: &'a SemanticModel<'a>, + ) -> Self { + Self { + attribute_to_find, + semantic, + found_attribute: false, + } + } +} + +impl Visitor<'_> for AttributeSearcher<'_> { + fn visit_expr(&mut self, expr: &'_ Expr) { + if self.found_attribute { + return; + } + if expr.is_attribute_expr() + && self + .semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| qualified_name == self.attribute_to_find) + { + self.found_attribute = true; + return; + } + walk_expr(self, expr); + } + + fn visit_stmt(&mut self, stmt: &ruff_python_ast::Stmt) { + if !self.found_attribute { + walk_stmt(self, stmt); + } + } + + fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) { + for stmt in body { + self.visit_stmt(stmt); + if self.found_attribute { + return; + } + } + } +} diff --git a/crates/ruff_linter/src/rules/numpy/mod.rs b/crates/ruff_linter/src/rules/numpy/mod.rs index 7a313eac2ecdf..3d3d50d44184c 100644 --- a/crates/ruff_linter/src/rules/numpy/mod.rs +++ b/crates/ruff_linter/src/rules/numpy/mod.rs @@ -4,7 +4,6 @@ pub(crate) mod rules; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use anyhow::Result; @@ -12,7 +11,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::NumpyDeprecatedTypeAlias, Path::new("NPY001.py"))] #[test_case(Rule::NumpyLegacyRandom, Path::new("NPY002.py"))] @@ -22,12 +21,12 @@ mod tests { #[test_case(Rule::Numpy2Deprecation, Path::new("NPY201_2.py"))] #[test_case(Rule::Numpy2Deprecation, Path::new("NPY201_3.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("numpy").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs index caf6afc133ab2..b34f4b917d38c 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of deprecated NumPy functions. @@ -73,7 +73,7 @@ pub(crate) fn deprecated_function(checker: &Checker, expr: &Expr) { _ => None, }) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NumpyDeprecatedFunction { existing: existing.to_string(), replacement: replacement.to_string(), @@ -89,6 +89,5 @@ pub(crate) fn deprecated_function(checker: &Checker, expr: &Expr) { let replacement_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::safe_edits(import_edit, [replacement_edit])) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs index 3e5ee760a090a..daa445642ad06 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for deprecated NumPy type aliases. @@ -74,7 +74,7 @@ pub(crate) fn deprecated_type_alias(checker: &Checker, expr: &Expr) { } }) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NumpyDeprecatedTypeAlias { type_name: type_name.to_string(), }, @@ -93,6 +93,5 @@ pub(crate) fn deprecated_type_alias(checker: &Checker, expr: &Expr) { let binding_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::safe_edits(binding_edit, import_edit)) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs index 9be089f24a8d9..cbe58e2122729 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -137,11 +137,11 @@ pub(crate) fn legacy_random(checker: &Checker, expr: &Expr) { } }) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NumpyLegacyRandom { method_name: method_name.to_string(), }, expr.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs index 5fca95a4f7700..bcc712b18dcf2 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs @@ -1,7 +1,5 @@ -use crate::rules::numpy::helpers::ImportSearcher; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::name::QualifiedNameBuilder; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr}; @@ -10,6 +8,8 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher}; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of NumPy functions and constants that were removed from @@ -269,13 +269,17 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { ["numpy", "deprecate"] => Replacement { existing: "deprecate", details: Details::Manual { - guideline: Some("Emit `DeprecationWarning` with `warnings.warn` directly, or use `typing.deprecated`."), + guideline: Some( + "Emit `DeprecationWarning` with `warnings.warn` directly, or use `typing.deprecated`.", + ), }, }, ["numpy", "deprecate_with_doc"] => Replacement { existing: "deprecate_with_doc", details: Details::Manual { - guideline: Some("Emit `DeprecationWarning` with `warnings.warn` directly, or use `typing.deprecated`."), + guideline: Some( + "Emit `DeprecationWarning` with `warnings.warn` directly, or use `typing.deprecated`.", + ), }, }, ["numpy", "disp"] => Replacement { @@ -293,14 +297,14 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { ["numpy", "find_common_type"] => Replacement { existing: "find_common_type", details: Details::Manual { - guideline: Some("Use `numpy.promote_types` or `numpy.result_type` instead. To achieve semantics for the `scalar_types` argument, use `numpy.result_type` and pass the Python values `0`, `0.0`, or `0j`."), + guideline: Some( + "Use `numpy.promote_types` or `numpy.result_type` instead. To achieve semantics for the `scalar_types` argument, use `numpy.result_type` and pass the Python values `0`, `0.0`, or `0j`.", + ), }, }, ["numpy", "get_array_wrap"] => Replacement { existing: "get_array_wrap", - details: Details::Manual { - guideline: None, - }, + details: Details::Manual { guideline: None }, }, ["numpy", "float_"] => Replacement { existing: "float_", @@ -358,9 +362,7 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { }, ["numpy", "issctype"] => Replacement { existing: "issctype", - details: Details::Manual { - guideline: None, - }, + details: Details::Manual { guideline: None }, }, ["numpy", "issubclass_"] => Replacement { existing: "issubclass_", @@ -386,9 +388,7 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { }, ["numpy", "maximum_sctype"] => Replacement { existing: "maximum_sctype", - details: Details::Manual { - guideline: None, - }, + details: Details::Manual { guideline: None }, }, ["numpy", existing @ ("NaN" | "NAN")] => Replacement { existing, @@ -440,9 +440,7 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { }, ["numpy", "obj2sctype"] => Replacement { existing: "obj2sctype", - details: Details::Manual { - guideline: None, - }, + details: Details::Manual { guideline: None }, }, ["numpy", "PINF"] => Replacement { existing: "PINF", @@ -494,15 +492,11 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { }, ["numpy", "sctype2char"] => Replacement { existing: "sctype2char", - details: Details::Manual { - guideline: None, - }, + details: Details::Manual { guideline: None }, }, ["numpy", "sctypes"] => Replacement { existing: "sctypes", - details: Details::Manual { - guideline: None, - }, + details: Details::Manual { guideline: None }, }, ["numpy", "seterrobj"] => Replacement { existing: "seterrobj", @@ -673,7 +667,7 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( Numpy2Deprecation { existing: replacement.existing.to_string(), migration_guide: replacement.details.guideline(), @@ -707,7 +701,6 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { )), Details::Manual { guideline: _ } => {} } - checker.report_diagnostic(diagnostic); } /// Ignore attempts to access a `numpy` member via its deprecated name @@ -823,57 +816,6 @@ fn try_block_contains_undeprecated_attribute( attribute_searcher.found_attribute } -/// AST visitor that searches an AST tree for [`ast::ExprAttribute`] nodes -/// that match a certain [`QualifiedName`]. -struct AttributeSearcher<'a> { - attribute_to_find: QualifiedName<'a>, - semantic: &'a SemanticModel<'a>, - found_attribute: bool, -} - -impl<'a> AttributeSearcher<'a> { - fn new(attribute_to_find: QualifiedName<'a>, semantic: &'a SemanticModel<'a>) -> Self { - Self { - attribute_to_find, - semantic, - found_attribute: false, - } - } -} - -impl Visitor<'_> for AttributeSearcher<'_> { - fn visit_expr(&mut self, expr: &'_ Expr) { - if self.found_attribute { - return; - } - if expr.is_attribute_expr() - && self - .semantic - .resolve_qualified_name(expr) - .is_some_and(|qualified_name| qualified_name == self.attribute_to_find) - { - self.found_attribute = true; - return; - } - ast::visitor::walk_expr(self, expr); - } - - fn visit_stmt(&mut self, stmt: &ruff_python_ast::Stmt) { - if !self.found_attribute { - ast::visitor::walk_stmt(self, stmt); - } - } - - fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) { - for stmt in body { - self.visit_stmt(stmt); - if self.found_attribute { - return; - } - } - } -} - /// Given an [`ast::StmtTry`] node, does the `try` branch of that node /// contain any [`ast::StmtImportFrom`] nodes that indicate the numpy /// member is being imported from the non-deprecated location? diff --git a/crates/ruff_linter/src/rules/pandas_vet/mod.rs b/crates/ruff_linter/src/rules/pandas_vet/mod.rs index dbe4a18c2dedf..982eedbde5086 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/mod.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/mod.rs @@ -11,7 +11,7 @@ mod tests { use crate::registry::{Linter, Rule}; use crate::test::{test_path, test_snippet}; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case( r#" @@ -29,6 +29,21 @@ mod tests { "#, "PD002_fail" )] + #[test_case( + r" + import polars as pl + x = pl.DataFrame() + x.drop(['a'], inplace=True) + ", + "PD002_pass_polars" + )] + #[test_case( + r" + x = DataFrame() + x.drop(['a'], inplace=True) + ", + "PD002_pass_no_import" + )] #[test_case( r" import pandas as pd @@ -355,7 +370,7 @@ mod tests { contents, &settings::LinterSettings::for_rules(Linter::PandasVet.rules()), ); - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); } #[test_case( @@ -370,7 +385,7 @@ mod tests { Path::new("pandas_vet").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs index a28a37a9e6ea7..3c0617b7087ef 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/assignment_to_df.rs @@ -1,9 +1,13 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + +/// ## Deprecated +/// +/// This rule has been deprecated as it's highly opinionated and overly strict in most cases. +/// /// ## What it does /// Checks for assignments to the variable `df`. /// @@ -38,15 +42,15 @@ impl Violation for PandasDfVariableName { } /// PD901 -pub(crate) fn assignment_to_df(targets: &[Expr]) -> Option { +pub(crate) fn assignment_to_df(checker: &Checker, targets: &[Expr]) { let [target] = targets else { - return None; + return; }; let Expr::Name(ast::ExprName { id, .. }) = target else { - return None; + return; }; if id != "df" { - return None; + return; } - Some(Diagnostic::new(PandasDfVariableName, target.range())) + checker.report_diagnostic(PandasDfVariableName, target.range()); } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs index 7fec3e4decff4..ab6b55104bce1 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/attr.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// ## What it does /// Checks for uses of `.values` on Pandas Series and Index objects. @@ -77,5 +77,5 @@ pub(crate) fn attr(checker: &Checker, attribute: &ast::ExprAttribute) { return; } - checker.report_diagnostic(Diagnostic::new(PandasUseOfDotValues, attribute.range())); + checker.report_diagnostic(PandasUseOfDotValues, attribute.range()); } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs index ce69acc79067c..8b7619cf3e5d2 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/call.rs @@ -1,14 +1,13 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::Violation; -use ruff_diagnostics::{Diagnostic, DiagnosticKind}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; -use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// ## What it does /// Checks for uses of `.isnull` on Pandas objects. @@ -171,26 +170,6 @@ pub(crate) fn call(checker: &Checker, func: &Expr) { let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func else { return; }; - let violation: DiagnosticKind = match attr.as_str() { - "isnull" if checker.settings.rules.enabled(Rule::PandasUseOfDotIsNull) => { - PandasUseOfDotIsNull.into() - } - "notnull" if checker.settings.rules.enabled(Rule::PandasUseOfDotNotNull) => { - PandasUseOfDotNotNull.into() - } - "pivot" | "unstack" - if checker - .settings - .rules - .enabled(Rule::PandasUseOfDotPivotOrUnstack) => - { - PandasUseOfDotPivotOrUnstack.into() - } - "stack" if checker.settings.rules.enabled(Rule::PandasUseOfDotStack) => { - PandasUseOfDotStack.into() - } - _ => return, - }; // Ignore irrelevant bindings (like imports). if !matches!( @@ -200,5 +179,24 @@ pub(crate) fn call(checker: &Checker, func: &Expr) { return; } - checker.report_diagnostic(Diagnostic::new(violation, func.range())); + let range = func.range(); + match attr.as_str() { + // PD003 + "isnull" if checker.is_rule_enabled(Rule::PandasUseOfDotIsNull) => { + checker.report_diagnostic(PandasUseOfDotIsNull, range); + } + // PD004 + "notnull" if checker.is_rule_enabled(Rule::PandasUseOfDotNotNull) => { + checker.report_diagnostic(PandasUseOfDotNotNull, range); + } + // PD010 + "pivot" | "unstack" if checker.is_rule_enabled(Rule::PandasUseOfDotPivotOrUnstack) => { + checker.report_diagnostic(PandasUseOfDotPivotOrUnstack, range); + } + // PD013 + "stack" if checker.is_rule_enabled(Rule::PandasUseOfDotStack) => { + checker.report_diagnostic(PandasUseOfDotStack, range); + } + _ => {} + } } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs index 487a23e4a700a..100a688b794b9 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs @@ -1,14 +1,15 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Keyword, Stmt}; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Edit, Fix, FixAvailability, Violation}; +use ruff_python_semantic::Modules; /// ## What it does /// Checks for `inplace=True` usages in `pandas` function and method @@ -52,12 +53,7 @@ impl Violation for PandasUseOfInplaceArgument { /// PD002 pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) { - // If the function was imported from another module, and it's _not_ Pandas, abort. - if checker - .semantic() - .resolve_qualified_name(&call.func) - .is_some_and(|qualified_name| !matches!(qualified_name.segments(), ["pandas", ..])) - { + if !checker.semantic().seen_module(Modules::PANDAS) { return; } @@ -78,7 +74,8 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) { }; if arg == "inplace" { if is_const_true(&keyword.value) { - let mut diagnostic = Diagnostic::new(PandasUseOfInplaceArgument, keyword.range()); + let mut diagnostic = + checker.report_diagnostic(PandasUseOfInplaceArgument, keyword.range()); // Avoid applying the fix if: // 1. The keyword argument is followed by a star argument (we can't be certain that // the star argument _doesn't_ contain an override). @@ -99,8 +96,6 @@ pub(crate) fn inplace_argument(checker: &Checker, call: &ast::ExprCall) { diagnostic.set_fix(fix); } } - - checker.report_diagnostic(diagnostic); } // Duplicate keywords is a syntax error, so we can stop here. @@ -138,6 +133,7 @@ fn convert_inplace_argument_to_assignment( &call.arguments, Parentheses::Preserve, locator.contents(), + comment_ranges, ) .ok()?; diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs index 8daed47a7c780..2d7ceba14bb33 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/nunique_constant_series_check.rs @@ -1,12 +1,11 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, CmpOp, Expr, Int}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// ## What it does /// Check for uses of `.nunique()` to check if a Pandas Series is constant @@ -88,6 +87,7 @@ pub(crate) fn nunique_constant_series_check( Expr::NumberLiteral(ast::ExprNumberLiteral { value: ast::Number::Int(Int::ONE), range: _, + node_index: _, }) ) { return; @@ -114,8 +114,5 @@ pub(crate) fn nunique_constant_series_check( return; } - checker.report_diagnostic(Diagnostic::new( - PandasNuniqueConstantSeriesCheck, - expr.range(), - )); + checker.report_diagnostic(PandasNuniqueConstantSeriesCheck, expr.range()); } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs index 224ba9dbf9ae6..d8c90b74ac509 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/pd_merge.rs @@ -1,11 +1,12 @@ use ruff_python_ast::{self as ast, Expr}; -use crate::checkers::ast::Checker; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; + /// ## What it does /// Checks for uses of `pd.merge` on Pandas objects. /// @@ -63,7 +64,7 @@ pub(crate) fn use_of_pd_merge(checker: &Checker, func: &Expr) { if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func { if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { if id == "pd" && attr == "merge" { - checker.report_diagnostic(Diagnostic::new(PandasUseOfPdMerge, func.range())); + checker.report_diagnostic(PandasUseOfPdMerge, func.range()); } } } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs index 3a41259c4960e..776e738bbb247 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::Expr; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -62,8 +62,7 @@ pub(crate) fn use_of_read_table(checker: &Checker, call: &ast::ExprCall) { .map(|keyword| &keyword.value) { if value == "," { - checker - .report_diagnostic(Diagnostic::new(PandasUseOfDotReadTable, call.func.range())); + checker.report_diagnostic(PandasUseOfDotReadTable, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs index 4831569b1f172..30fc9209524dc 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/subscript.rs @@ -1,14 +1,13 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::Violation; -use ruff_diagnostics::{Diagnostic, DiagnosticKind}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; -use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +use crate::rules::pandas_vet::helpers::{Resolution, test_expression}; /// ## What it does /// Checks for uses of `.ix` on Pandas objects. @@ -152,15 +151,6 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, expr: &Expr) { return; }; - let violation: DiagnosticKind = match attr.as_str() { - "ix" if checker.settings.rules.enabled(Rule::PandasUseOfDotIx) => PandasUseOfDotIx.into(), - "at" if checker.settings.rules.enabled(Rule::PandasUseOfDotAt) => PandasUseOfDotAt.into(), - "iat" if checker.settings.rules.enabled(Rule::PandasUseOfDotIat) => { - PandasUseOfDotIat.into() - } - _ => return, - }; - // Avoid flagging on non-DataFrames (e.g., `{"a": 1}.at[0]`), and on irrelevant bindings // (like imports). if !matches!( @@ -170,5 +160,21 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, expr: &Expr) { return; } - checker.report_diagnostic(Diagnostic::new(violation, expr.range())); + let range = expr.range(); + + match attr.as_str() { + // PD007 + "ix" if checker.is_rule_enabled(Rule::PandasUseOfDotIx) => { + checker.report_diagnostic(PandasUseOfDotIx, range) + } + // PD008 + "at" if checker.is_rule_enabled(Rule::PandasUseOfDotAt) => { + checker.report_diagnostic(PandasUseOfDotAt, range) + } + // PD009 + "iat" if checker.is_rule_enabled(Rule::PandasUseOfDotIat) => { + checker.report_diagnostic(PandasUseOfDotIat, range) + } + _ => return, + }; } diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_pass_no_import.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_pass_no_import.snap new file mode 100644 index 0000000000000..07173f8f1bf87 --- /dev/null +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_pass_no_import.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pandas_vet/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_pass_polars.snap b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_pass_polars.snap new file mode 100644 index 0000000000000..07173f8f1bf87 --- /dev/null +++ b/crates/ruff_linter/src/rules/pandas_vet/snapshots/ruff_linter__rules__pandas_vet__tests__PD002_pass_polars.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pandas_vet/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs index 2b69eddd7cdea..5f291697592dd 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::{analyze, SemanticModel}; +use ruff_python_semantic::{SemanticModel, analyze}; use ruff_python_stdlib::str::{is_cased_lowercase, is_cased_uppercase}; pub(super) fn is_camelcase(name: &str) -> bool { diff --git a/crates/ruff_linter/src/rules/pep8_naming/mod.rs b/crates/ruff_linter/src/rules/pep8_naming/mod.rs index 8e24e4492afd7..a646946fe995b 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/mod.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/mod.rs @@ -15,7 +15,7 @@ mod tests { use crate::rules::pep8_naming::settings::IgnoreNames; use crate::rules::{flake8_import_conventions, pep8_naming}; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::InvalidClassName, Path::new("N801.py"))] #[test_case(Rule::InvalidFunctionName, Path::new("N802.py"))] @@ -85,7 +85,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -104,7 +104,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::CamelcaseImportedAsAcronym) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -124,7 +124,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::InvalidFirstArgumentNameForMethod) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -143,7 +143,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::InvalidFirstArgumentNameForMethod) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -181,7 +181,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index 14400d905f1bc..6c547ff5c8016 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Alias, Stmt}; use ruff_python_stdlib::str::{self}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; @@ -62,25 +62,25 @@ pub(crate) fn camelcase_imported_as_acronym( alias: &Alias, stmt: &Stmt, checker: &Checker, -) -> Option { +) { if helpers::is_camelcase(name) && !str::is_cased_lowercase(asname) && str::is_cased_uppercase(asname) && helpers::is_acronym(name, asname) { - let ignore_names = &checker.settings.pep8_naming.ignore_names; + let ignore_names = &checker.settings().pep8_naming.ignore_names; // Ignore any explicitly-allowed names. if ignore_names.matches(name) || ignore_names.matches(asname) { - return None; + return; } // Ignore names that follow a community-agreed import convention. if is_ignored_because_of_import_convention(asname, stmt, alias, checker) { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CamelcaseImportedAsAcronym { name: name.to_string(), asname: asname.to_string(), @@ -88,9 +88,7 @@ pub(crate) fn camelcase_imported_as_acronym( alias.range(), ); diagnostic.set_parent(stmt.start()); - return Some(diagnostic); } - None } fn is_ignored_because_of_import_convention( @@ -116,7 +114,7 @@ fn is_ignored_because_of_import_convention( // Ignore names that follow a community-agreed import convention. checker - .settings + .settings() .flake8_import_conventions .aliases .get(&*full_name) diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs index c2b27f2c8cd90..609cd61fc0938 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs @@ -1,10 +1,11 @@ use ruff_python_ast::{Alias, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_stdlib::str::{self}; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; use crate::rules::pep8_naming::settings::IgnoreNames; @@ -65,14 +66,17 @@ impl Violation for CamelcaseImportedAsConstant { /// N814 pub(crate) fn camelcase_imported_as_constant( + checker: &Checker, name: &str, asname: &str, alias: &Alias, stmt: &Stmt, ignore_names: &IgnoreNames, -) -> Option { +) { // Single-character names are ambiguous. It could be a class or a constant. - asname.chars().nth(1)?; + if asname.chars().nth(1).is_none() { + return; + } if helpers::is_camelcase(name) && !str::is_cased_lowercase(asname) @@ -81,9 +85,9 @@ pub(crate) fn camelcase_imported_as_constant( { // Ignore any explicitly-allowed names. if ignore_names.matches(name) || ignore_names.matches(asname) { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CamelcaseImportedAsConstant { name: name.to_string(), asname: asname.to_string(), @@ -91,7 +95,5 @@ pub(crate) fn camelcase_imported_as_constant( alias.range(), ); diagnostic.set_parent(stmt.start()); - return Some(diagnostic); } - None } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs index e883c1ed5755f..c81884c0458a0 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs @@ -1,9 +1,10 @@ use ruff_python_ast::{Alias, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; use crate::rules::pep8_naming::settings::IgnoreNames; @@ -50,18 +51,19 @@ impl Violation for CamelcaseImportedAsLowercase { /// N813 pub(crate) fn camelcase_imported_as_lowercase( + checker: &Checker, name: &str, asname: &str, alias: &Alias, stmt: &Stmt, ignore_names: &IgnoreNames, -) -> Option { +) { if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_cased_lowercase(asname) { // Ignore any explicitly-allowed names. if ignore_names.matches(name) || ignore_names.matches(asname) { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CamelcaseImportedAsLowercase { name: name.to_string(), asname: asname.to_string(), @@ -69,7 +71,5 @@ pub(crate) fn camelcase_imported_as_lowercase( alias.range(), ); diagnostic.set_parent(stmt.start()); - return Some(diagnostic); } - None } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index d2f8adf0f481e..26e72de928934 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Alias, Stmt}; use ruff_python_stdlib::str; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::{helpers, settings::IgnoreNames}; /// ## What it does @@ -63,12 +64,13 @@ impl Violation for ConstantImportedAsNonConstant { /// N811 pub(crate) fn constant_imported_as_non_constant( + checker: &Checker, name: &str, asname: &str, alias: &Alias, stmt: &Stmt, ignore_names: &IgnoreNames, -) -> Option { +) { if str::is_cased_uppercase(name) && !(str::is_cased_uppercase(asname) // Single-character names are ambiguous. @@ -78,9 +80,9 @@ pub(crate) fn constant_imported_as_non_constant( { // Ignore any explicitly-allowed names. if ignore_names.matches(name) || ignore_names.matches(asname) { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ConstantImportedAsNonConstant { name: name.to_string(), asname: asname.to_string(), @@ -88,7 +90,5 @@ pub(crate) fn constant_imported_as_non_constant( alias.range(), ); diagnostic.set_parent(stmt.start()); - return Some(diagnostic); } - None } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs index 2b5ae74493993..c4b8f86299ced 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -1,11 +1,12 @@ use ruff_python_ast::Stmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{Scope, ScopeKind}; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::settings::IgnoreNames; /// ## What it does @@ -48,24 +49,25 @@ impl Violation for DunderFunctionName { /// N807 pub(crate) fn dunder_function_name( + checker: &Checker, scope: &Scope, stmt: &Stmt, name: &str, ignore_names: &IgnoreNames, -) -> Option { +) { if matches!(scope.kind, ScopeKind::Class(_)) { - return None; + return; } if !visibility::is_magic(name) { - return None; + return; } // Allowed under PEP 562 (https://peps.python.org/pep-0562/). if matches!(scope.kind, ScopeKind::Module) && (name == "__getattr__" || name == "__dir__") { - return None; + return; } // Ignore any explicitly-allowed names. if ignore_names.matches(name) { - return None; + return; } - Some(Diagnostic::new(DunderFunctionName, stmt.identifier())) + checker.report_diagnostic(DunderFunctionName, stmt.identifier()); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index fcb85ba6cf6bf..ecaa71ee208b7 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -1,9 +1,10 @@ use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::settings::IgnoreNames; /// ## What it does @@ -48,13 +49,14 @@ impl Violation for ErrorSuffixOnExceptionName { /// N818 pub(crate) fn error_suffix_on_exception_name( + checker: &Checker, class_def: &Stmt, arguments: Option<&Arguments>, name: &str, ignore_names: &IgnoreNames, -) -> Option { +) { if name.ends_with("Error") { - return None; + return; } if !arguments.is_some_and(|arguments| { @@ -66,18 +68,18 @@ pub(crate) fn error_suffix_on_exception_name( } }) }) { - return None; + return; } // Ignore any explicitly-allowed names. if ignore_names.matches(name) { - return None; + return; } - Some(Diagnostic::new( + checker.report_diagnostic( ErrorSuffixOnExceptionName { name: name.to_string(), }, class_def.identifier(), - )) + ); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs index d2445e8c39959..5bab2edfc7166 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ExprLambda, Parameters, StmtFunctionDef}; -use ruff_python_semantic::analyze::visibility::is_override; use ruff_python_semantic::ScopeKind; +use ruff_python_semantic::analyze::visibility::is_override; use ruff_python_stdlib::str; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -81,7 +81,7 @@ pub(crate) fn invalid_argument_name_lambda(checker: &Checker, lambda: &ExprLambd /// N803 fn invalid_argument_name(checker: &Checker, parameters: &Parameters) { - let ignore_names = &checker.settings.pep8_naming.ignore_names; + let ignore_names = &checker.settings().pep8_naming.ignore_names; for parameter in parameters { let name = parameter.name().as_str(); @@ -94,13 +94,11 @@ fn invalid_argument_name(checker: &Checker, parameters: &Parameters) { continue; } - let diagnostic = Diagnostic::new( + checker.report_diagnostic( InvalidArgumentName { name: name.to_string(), }, parameter.range(), ); - - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs index b755667918917..c220d4d41abfc 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -1,9 +1,10 @@ use ruff_python_ast::Stmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::settings::IgnoreNames; /// ## What it does @@ -54,22 +55,22 @@ impl Violation for InvalidClassName { /// N801 pub(crate) fn invalid_class_name( + checker: &Checker, class_def: &Stmt, name: &str, ignore_names: &IgnoreNames, -) -> Option { +) { let stripped = name.trim_start_matches('_'); if !stripped.chars().next().is_some_and(char::is_uppercase) || stripped.contains('_') { // Ignore any explicitly-allowed names. if ignore_names.matches(name) { - return None; + return; } - return Some(Diagnostic::new( + checker.report_diagnostic( InvalidClassName { name: name.to_string(), }, class_def.identifier(), - )); + ); } - None } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs index 21bd9172a7134..790f1a8b40f87 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs @@ -1,18 +1,18 @@ use anyhow::Result; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::ParameterWithDefault; use ruff_python_codegen::Stylist; -use ruff_python_semantic::analyze::class::{is_metaclass, IsMetaclass}; +use ruff_python_semantic::analyze::class::{IsMetaclass, is_metaclass}; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::{Scope, ScopeKind, SemanticModel}; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, DiagnosticGuard}; use crate::registry::Rule; -use crate::renamer::Renamer; +use crate::renamer::{Renamer, ShadowedKind}; +use crate::{Fix, Violation}; /// ## What it does /// Checks for instance methods that use a name other than `self` for their @@ -64,8 +64,7 @@ pub(crate) struct InvalidFirstArgumentNameForMethod { } impl Violation for InvalidFirstArgumentNameForMethod { - const FIX_AVAILABILITY: ruff_diagnostics::FixAvailability = - ruff_diagnostics::FixAvailability::Sometimes; + const FIX_AVAILABILITY: crate::FixAvailability = crate::FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { @@ -137,13 +136,12 @@ pub(crate) struct InvalidFirstArgumentNameForClassMethod { } impl Violation for InvalidFirstArgumentNameForClassMethod { - const FIX_AVAILABILITY: ruff_diagnostics::FixAvailability = - ruff_diagnostics::FixAvailability::Sometimes; + const FIX_AVAILABILITY: crate::FixAvailability = crate::FixAvailability::Sometimes; #[derive_message_formats] // The first string below is what shows up in the documentation // in the rule table, and it is the more common case. - #[allow(clippy::if_not_else)] + #[expect(clippy::if_not_else)] fn message(&self) -> String { if !self.is_new { "First argument of a class method should be named `cls`".to_string() @@ -167,14 +165,22 @@ enum FunctionType { } impl FunctionType { - fn diagnostic_kind(self, argument_name: String) -> DiagnosticKind { + fn diagnostic_kind<'a, 'b>( + self, + checker: &'a Checker<'b>, + argument_name: String, + range: TextRange, + ) -> DiagnosticGuard<'a, 'b> { match self { - Self::Method => InvalidFirstArgumentNameForMethod { argument_name }.into(), - Self::ClassMethod => InvalidFirstArgumentNameForClassMethod { - argument_name, - is_new: false, - } - .into(), + Self::Method => checker + .report_diagnostic(InvalidFirstArgumentNameForMethod { argument_name }, range), + Self::ClassMethod => checker.report_diagnostic( + InvalidFirstArgumentNameForClassMethod { + argument_name, + is_new: false, + }, + range, + ), } } @@ -220,8 +226,8 @@ pub(crate) fn invalid_first_argument_name(checker: &Checker, scope: &Scope) { decorator_list, parent_scope, semantic, - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ) { function_type::FunctionType::Function | function_type::FunctionType::StaticMethod => { return; @@ -237,7 +243,7 @@ pub(crate) fn invalid_first_argument_name(checker: &Checker, scope: &Scope) { return; } }; - if !checker.enabled(function_type.rule()) { + if !checker.is_rule_enabled(function_type.rule()) { return; } @@ -254,7 +260,7 @@ pub(crate) fn invalid_first_argument_name(checker: &Checker, scope: &Scope) { if &self_or_cls.name == function_type.valid_first_argument_name() || checker - .settings + .settings() .pep8_naming .ignore_names .matches(&self_or_cls.name) @@ -262,12 +268,11 @@ pub(crate) fn invalid_first_argument_name(checker: &Checker, scope: &Scope) { return; } - let mut diagnostic = Diagnostic::new( - function_type.diagnostic_kind(self_or_cls.name.to_string()), - self_or_cls.range(), - ); + let mut diagnostic = + function_type.diagnostic_kind(checker, self_or_cls.name.to_string(), self_or_cls.range()); diagnostic.try_set_optional_fix(|| { rename_parameter( + checker, scope, self_or_cls, parameters, @@ -276,11 +281,11 @@ pub(crate) fn invalid_first_argument_name(checker: &Checker, scope: &Scope) { checker.stylist(), ) }); - checker.report_diagnostic(diagnostic); } /// Rename the first parameter to `self` or `cls`, if no other parameter has the target name. fn rename_parameter( + checker: &Checker, scope: &Scope<'_>, self_or_cls: &ast::Parameter, parameters: &ast::Parameters, @@ -296,6 +301,16 @@ fn rename_parameter( { return Ok(None); } + let binding = scope + .get(&self_or_cls.name) + .map(|binding_id| semantic.binding(binding_id)) + .unwrap(); + + // Don't provide autofix if `self` or `cls` is already defined in the scope. + if ShadowedKind::new(binding, function_type.valid_first_argument_name(), checker).shadows_any() + { + return Ok(None); + } let (edit, rest) = Renamer::rename( &self_or_cls.name, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs index 508aeb689eaf1..566c55292983e 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -1,12 +1,13 @@ use ruff_python_ast::{Decorator, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; -use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::visibility; use ruff_python_stdlib::str; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::settings::IgnoreNames; /// ## What it does @@ -57,15 +58,16 @@ impl Violation for InvalidFunctionName { /// N802 pub(crate) fn invalid_function_name( + checker: &Checker, stmt: &Stmt, name: &str, decorator_list: &[Decorator], ignore_names: &IgnoreNames, semantic: &SemanticModel, -) -> Option { +) { // Ignore any function names that are already lowercase. if str::is_lowercase(name) { - return None; + return; } // Ignore any functions that are explicitly `@override` or `@overload`. @@ -74,18 +76,18 @@ pub(crate) fn invalid_function_name( if visibility::is_override(decorator_list, semantic) || visibility::is_overload(decorator_list, semantic) { - return None; + return; } // Ignore any explicitly-allowed names. if ignore_names.matches(name) { - return None; + return; } - Some(Diagnostic::new( + checker.report_diagnostic( InvalidFunctionName { name: name.to_string(), }, stmt.identifier(), - )) + ); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs index 702efa3962f31..6e7e699282659 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_module_name.rs @@ -1,15 +1,17 @@ use std::ffi::OsStr; use std::path::Path; -use crate::package::PackageRoot; -use crate::rules::pep8_naming::settings::IgnoreNames; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PySourceType; use ruff_python_stdlib::identifiers::{is_migration_name, is_module_name}; use ruff_python_stdlib::path::is_module_file; use ruff_text_size::TextRange; +use crate::Violation; +use crate::checkers::ast::LintContext; +use crate::package::PackageRoot; +use crate::rules::pep8_naming::settings::IgnoreNames; + /// ## What it does /// Checks for module names that do not follow the `snake_case` naming /// convention or are otherwise invalid. @@ -53,9 +55,10 @@ pub(crate) fn invalid_module_name( path: &Path, package: Option>, ignore_names: &IgnoreNames, -) -> Option { + context: &LintContext, +) { if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file_or_stub) { - return None; + return; } if let Some(package) = package { @@ -77,18 +80,16 @@ pub(crate) fn invalid_module_name( if !is_valid_module_name { // Ignore any explicitly-allowed names. if ignore_names.matches(&module_name) { - return None; + return; } - return Some(Diagnostic::new( + context.report_diagnostic( InvalidModuleName { name: module_name.to_string(), }, TextRange::default(), - )); + ); } } - - None } /// Return `true` if a [`Path`] refers to a migration file. diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs index 83262dac4ab89..6d2df2e8f14df 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Alias, Stmt}; use ruff_python_stdlib::str; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pep8_naming::settings::IgnoreNames; /// ## What it does @@ -49,19 +50,20 @@ impl Violation for LowercaseImportedAsNonLowercase { /// N812 pub(crate) fn lowercase_imported_as_non_lowercase( + checker: &Checker, name: &str, asname: &str, alias: &Alias, stmt: &Stmt, ignore_names: &IgnoreNames, -) -> Option { +) { if !str::is_cased_uppercase(name) && str::is_cased_lowercase(name) && !str::is_lowercase(asname) { // Ignore any explicitly-allowed names. if ignore_names.matches(name) || ignore_names.matches(asname) { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( LowercaseImportedAsNonLowercase { name: name.to_string(), asname: asname.to_string(), @@ -69,7 +71,5 @@ pub(crate) fn lowercase_imported_as_non_lowercase( alias.range(), ); diagnostic.set_parent(stmt.start()); - return Some(diagnostic); } - None } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 905bcb47d46f6..1ad5f88381877 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; @@ -72,14 +72,14 @@ pub(crate) fn mixed_case_variable_in_class_scope( return; } - if checker.settings.pep8_naming.ignore_names.matches(name) { + if checker.settings().pep8_naming.ignore_names.matches(name) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( MixedCaseVariableInClassScope { name: name.to_string(), }, expr.range(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index ddaff4e510fe4..84e955033ed49 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; @@ -75,14 +75,14 @@ pub(crate) fn mixed_case_variable_in_global_scope(checker: &Checker, expr: &Expr return; } - if checker.settings.pep8_naming.ignore_names.matches(name) { + if checker.settings().pep8_naming.ignore_names.matches(name) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( MixedCaseVariableInGlobalScope { name: name.to_string(), }, expr.range(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index c580d2149cb6f..d066363122717 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_stdlib::str; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; @@ -77,14 +77,14 @@ pub(crate) fn non_lowercase_variable_in_function(checker: &Checker, expr: &Expr, } // Ignore explicitly-allowed names. - if checker.settings.pep8_naming.ignore_names.matches(name) { + if checker.settings().pep8_naming.ignore_names.matches(name) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NonLowercaseVariableInFunction { name: name.to_string(), }, expr.range(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N804_N804.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N804_N804.py.snap index 140631d9ff4c2..79cd708650e39 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N804_N804.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N804_N804.py.snap @@ -153,3 +153,13 @@ N804.py:74:20: N804 [*] First argument of a class method should be named `cls` 76 76 | 77 77 | def func(x): 78 78 | return x + +N804.py:88:18: N804 First argument of a class method should be named `cls` + | +86 | class Example: +87 | @classmethod +88 | def function(this): + | ^^^^ N804 +89 | cls = 1234 + | + = help: Rename `this` to `cls` diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap index 89d578bc7f186..27e3b3262c526 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__N805_N805.py.snap @@ -247,7 +247,7 @@ N805.py:115:20: N805 [*] First argument of a method should be named `self` 119 119 | def bad_method(this): 120 120 | self = this -N805.py:119:20: N805 [*] First argument of a method should be named `self` +N805.py:119:20: N805 First argument of a method should be named `self` | 117 | this 118 | @@ -257,18 +257,6 @@ N805.py:119:20: N805 [*] First argument of a method should be named `self` | = help: Rename `this` to `self` -ℹ Unsafe fix -116 116 | this = this -117 117 | this -118 118 | -119 |- def bad_method(this): -120 |- self = this - 119 |+ def bad_method(self): - 120 |+ self = self -121 121 | -122 122 | -123 123 | class RenamingWithNFKC: - N805.py:124:17: N805 [*] First argument of a method should be named `self` | 123 | class RenamingWithNFKC: @@ -289,3 +277,13 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self` 126 126 | 127 127 | 128 128 | from typing import Protocol + +N805.py:141:11: N805 First argument of a method should be named `self` + | +139 | # https://github.com/astral-sh/ruff/issues/18459 +140 | class C: +141 | def f(this): + | ^^^^ N805 +142 | self = 123 + | + = help: Rename `this` to `self` diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap index 6d28da75c6806..94befdac59454 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__classmethod_decorators.snap @@ -190,7 +190,7 @@ N805.py:115:20: N805 [*] First argument of a method should be named `self` 119 119 | def bad_method(this): 120 120 | self = this -N805.py:119:20: N805 [*] First argument of a method should be named `self` +N805.py:119:20: N805 First argument of a method should be named `self` | 117 | this 118 | @@ -200,18 +200,6 @@ N805.py:119:20: N805 [*] First argument of a method should be named `self` | = help: Rename `this` to `self` -ℹ Unsafe fix -116 116 | this = this -117 117 | this -118 118 | -119 |- def bad_method(this): -120 |- self = this - 119 |+ def bad_method(self): - 120 |+ self = self -121 121 | -122 122 | -123 123 | class RenamingWithNFKC: - N805.py:124:17: N805 [*] First argument of a method should be named `self` | 123 | class RenamingWithNFKC: @@ -232,3 +220,13 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self` 126 126 | 127 127 | 128 128 | from typing import Protocol + +N805.py:141:11: N805 First argument of a method should be named `self` + | +139 | # https://github.com/astral-sh/ruff/issues/18459 +140 | class C: +141 | def f(this): + | ^^^^ N805 +142 | self = 123 + | + = help: Rename `this` to `self` diff --git a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap index c1a6d0298a115..7ba83b01fd852 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap +++ b/crates/ruff_linter/src/rules/pep8_naming/snapshots/ruff_linter__rules__pep8_naming__tests__staticmethod_decorators.snap @@ -228,7 +228,7 @@ N805.py:115:20: N805 [*] First argument of a method should be named `self` 119 119 | def bad_method(this): 120 120 | self = this -N805.py:119:20: N805 [*] First argument of a method should be named `self` +N805.py:119:20: N805 First argument of a method should be named `self` | 117 | this 118 | @@ -238,18 +238,6 @@ N805.py:119:20: N805 [*] First argument of a method should be named `self` | = help: Rename `this` to `self` -ℹ Unsafe fix -116 116 | this = this -117 117 | this -118 118 | -119 |- def bad_method(this): -120 |- self = this - 119 |+ def bad_method(self): - 120 |+ self = self -121 121 | -122 122 | -123 123 | class RenamingWithNFKC: - N805.py:124:17: N805 [*] First argument of a method should be named `self` | 123 | class RenamingWithNFKC: @@ -270,3 +258,13 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self` 126 126 | 127 127 | 128 128 | from typing import Protocol + +N805.py:141:11: N805 First argument of a method should be named `self` + | +139 | # https://github.com/astral-sh/ruff/issues/18459 +140 | class C: +141 | def f(this): + | ^^^^ N805 +142 | self = 123 + | + = help: Rename `this` to `self` diff --git a/crates/ruff_linter/src/rules/perflint/helpers.rs b/crates/ruff_linter/src/rules/perflint/helpers.rs new file mode 100644 index 0000000000000..271975246c14e --- /dev/null +++ b/crates/ruff_linter/src/rules/perflint/helpers.rs @@ -0,0 +1,98 @@ +use ruff_python_trivia::{ + BackwardsTokenizer, PythonWhitespace, SimpleToken, SimpleTokenKind, SimpleTokenizer, +}; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; + +pub(super) fn comment_strings_in_range<'a>( + checker: &'a Checker, + range: TextRange, + ranges_to_ignore: &[TextRange], +) -> Vec<&'a str> { + checker + .comment_ranges() + .comments_in_range(range) + .iter() + // Ignore comments inside of the append or iterator, since these are preserved + .filter(|comment| { + !ranges_to_ignore + .iter() + .any(|to_ignore| to_ignore.contains_range(**comment)) + }) + .map(|range| checker.locator().slice(range).trim_whitespace_start()) + .collect() +} + +fn semicolon_before_and_after( + checker: &Checker, + statement: TextRange, +) -> (Option, Option) { + // determine whether there's a semicolon either before or after the binding statement. + // Since it's a binding statement, we can just check whether there's a semicolon immediately + // after the whitespace in front of or behind it + let mut after_tokenizer = + SimpleTokenizer::starts_at(statement.end(), checker.locator().contents()).skip_trivia(); + + let after_semicolon = if after_tokenizer + .next() + .is_some_and(|token| token.kind() == SimpleTokenKind::Semi) + { + after_tokenizer.next() + } else { + None + }; + + let semicolon_before = BackwardsTokenizer::up_to( + statement.start(), + checker.locator().contents(), + checker.comment_ranges(), + ) + .skip_trivia() + .next() + .filter(|token| token.kind() == SimpleTokenKind::Semi); + + (semicolon_before, after_semicolon) +} + +/// Finds the range necessary to delete a statement (including any semicolons around it). +/// Returns the range and whether there were multiple statements on the line +pub(super) fn statement_deletion_range( + checker: &Checker, + statement_range: TextRange, +) -> (TextRange, bool) { + let locator = checker.locator(); + // If the binding has multiple statements on its line, the fix would be substantially more complicated + let (semicolon_before, after_semicolon) = semicolon_before_and_after(checker, statement_range); + + // If there are multiple binding statements in one line, we don't want to accidentally delete them + // Instead, we just delete the binding statement and leave any comments where they are + + match (semicolon_before, after_semicolon) { + // ```python + // a = [] + // ``` + (None, None) => (locator.full_lines_range(statement_range), false), + + // ```python + // a = 1; b = [] + // ^^^^^^^^ + // a = 1; b = []; c = 3 + // ^^^^^^^^ + // ``` + (Some(semicolon_before), Some(_) | None) => ( + TextRange::new(semicolon_before.start(), statement_range.end()), + true, + ), + + // ```python + // a = []; b = 3 + // ^^^^^^^ + // ``` + (None, Some(after_semicolon)) => ( + TextRange::new(statement_range.start(), after_semicolon.start()), + true, + ), + } +} diff --git a/crates/ruff_linter/src/rules/perflint/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index 07d67d8363bfd..35a7d31f6278f 100644 --- a/crates/ruff_linter/src/rules/perflint/mod.rs +++ b/crates/ruff_linter/src/rules/perflint/mod.rs @@ -1,6 +1,6 @@ //! Rules from [perflint](https://pypi.org/project/perflint/). +mod helpers; pub(crate) mod rules; - #[cfg(test)] mod tests { use std::path::Path; @@ -9,10 +9,10 @@ mod tests { use ruff_python_ast::PythonVersion; use test_case::test_case; - use crate::assert_messages; + use crate::assert_diagnostics; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; + use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] @@ -27,11 +27,12 @@ mod tests { Path::new("perflint").join(path).as_path(), &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY310), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } - // TODO: remove this test case when the fix for `perf401` is stabilized + // TODO: remove this test case when the fixes for `perf401` and `perf403` are stabilized + #[test_case(Rule::ManualDictComprehension, Path::new("PERF403.py"))] #[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( @@ -43,11 +44,11 @@ mod tests { Path::new("perflint").join(path).as_path(), &LinterSettings { preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs index c6f4a4d4230e8..b26e7b309b89b 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -1,13 +1,13 @@ use std::fmt; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::{Arguments, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::pad; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `dict.items()` that discard either the key or the value @@ -99,7 +99,7 @@ pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor } (true, false) => { // The key is unused, so replace with `dict.values()`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IncorrectDictIterator { subset: DictSubset::Values, }, @@ -115,11 +115,10 @@ pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor stmt_for.target.range(), ); diagnostic.set_fix(Fix::unsafe_edits(replace_attribute, [replace_target])); - checker.report_diagnostic(diagnostic); } (false, true) => { // The value is unused, so replace with `dict.keys()`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IncorrectDictIterator { subset: DictSubset::Keys, }, @@ -135,7 +134,6 @@ pub(crate) fn incorrect_dict_iterator(checker: &Checker, stmt_for: &ast::StmtFor stmt_for.target.range(), ); diagnostic.set_fix(Fix::unsafe_edits(replace_attribute, [replace_target])); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs index 97f53cb55ce7a..d67b3803d8e4c 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs @@ -1,11 +1,15 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::helpers::any_over_expr; -use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::analyze::typing::is_dict; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ + self as ast, Expr, Stmt, comparable::ComparableExpr, helpers::any_over_expr, +}; +use ruff_python_semantic::{Binding, analyze::typing::is_dict}; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::preview::is_fix_manual_dict_comprehension_enabled; +use crate::rules::perflint::helpers::{comment_strings_in_range, statement_deletion_range}; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `for` loops that can be replaced by a dictionary comprehension. @@ -42,29 +46,59 @@ use crate::checkers::ast::Checker; /// result.update({x: y for x, y in pairs if y % 2}) /// ``` #[derive(ViolationMetadata)] -pub(crate) struct ManualDictComprehension; +pub(crate) struct ManualDictComprehension { + fix_type: DictComprehensionType, + is_async: bool, +} impl Violation for ManualDictComprehension { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { - "Use a dictionary comprehension instead of a for-loop".to_string() + let modifier = if self.is_async { "an async" } else { "a" }; + + match self.fix_type { + DictComprehensionType::Comprehension => { + format!("Use a dictionary comprehension instead of {modifier} for-loop") + } + DictComprehensionType::Update => { + format!("Use `dict.update` instead of {modifier} for-loop") + } + } + } + fn fix_title(&self) -> Option { + let modifier = if self.is_async { "async " } else { "" }; + match self.fix_type { + DictComprehensionType::Comprehension => Some(format!( + "Replace {modifier}for loop with dict comprehension" + )), + DictComprehensionType::Update => { + Some(format!("Replace {modifier}for loop with `dict.update`")) + } + } } } /// PERF403 -pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: &[Stmt]) { +pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtFor) { + let ast::StmtFor { body, target, .. } = for_stmt; + let body = body.as_slice(); + let target = target.as_ref(); let (stmt, if_test) = match body { // ```python // for idx, name in enumerate(names): // if idx % 2 == 0: // result[name] = idx // ``` - [Stmt::If(ast::StmtIf { - body, - elif_else_clauses, - test, - .. - })] => { + [ + Stmt::If(ast::StmtIf { + body, + elif_else_clauses, + test, + .. + }), + ] => { // TODO(charlie): If there's an `else` clause, verify that the `else` has the // same structure. if !elif_else_clauses.is_empty() { @@ -87,25 +121,35 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: targets, value, range, + node_index: _, }) = stmt else { return; }; - let [Expr::Subscript(ast::ExprSubscript { - value: subscript_value, - slice, - .. - })] = targets.as_slice() + let [ + Expr::Subscript(ast::ExprSubscript { + value: subscript_value, + slice: key, + .. + }), + ] = targets.as_slice() else { return; }; + // If any references to a target variable are after the loop, + // then removing the loop would cause a NameError. Make sure none + // of the variables are used outside the for loop. + if has_post_loop_references(checker, target, for_stmt.end()) { + return; + } + match target { Expr::Tuple(tuple) => { if !tuple .iter() - .any(|element| ComparableExpr::from(slice) == ComparableExpr::from(element)) + .any(|element| ComparableExpr::from(key) == ComparableExpr::from(element)) { return; } @@ -117,7 +161,7 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: } } Expr::Name(_) => { - if ComparableExpr::from(slice) != ComparableExpr::from(target) { + if ComparableExpr::from(key) != ComparableExpr::from(target) { return; } if ComparableExpr::from(value) != ComparableExpr::from(target) { @@ -164,5 +208,276 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: return; } - checker.report_diagnostic(Diagnostic::new(ManualDictComprehension, *range)); + if is_fix_manual_dict_comprehension_enabled(checker.settings()) { + let binding_stmt = binding.statement(checker.semantic()); + let binding_value = binding_stmt.and_then(|binding_stmt| match binding_stmt { + ast::Stmt::AnnAssign(assign) => assign.value.as_deref(), + ast::Stmt::Assign(assign) => Some(&assign.value), + _ => None, + }); + + // If the variable is an empty dict literal, then we might be able to replace it with a full dict comprehension. + // otherwise, it has to be replaced with a `dict.update` + let binding_is_empty_dict = + binding_value.is_some_and(|binding_value| match binding_value { + // value = {} + Expr::Dict(dict_expr) => dict_expr.is_empty(), + // value = dict() + Expr::Call(call) => { + checker + .semantic() + .resolve_builtin_symbol(&call.func) + .is_some_and(|name| name == "dict") + && call.arguments.is_empty() + } + _ => false, + }); + + let assignment_in_same_statement = binding.source.is_some_and(|binding_source| { + let for_loop_parent = checker.semantic().current_statement_parent_id(); + let binding_parent = checker.semantic().parent_statement_id(binding_source); + for_loop_parent == binding_parent + }); + // If the binding is not a single name expression, it could be replaced with a dict comprehension, + // but not necessarily, so this needs to be manually fixed. This does not apply when using an update. + let binding_has_one_target = binding_stmt.is_some_and(|binding_stmt| match binding_stmt { + ast::Stmt::AnnAssign(_) => true, + ast::Stmt::Assign(assign) => assign.targets.len() == 1, + _ => false, + }); + // If the binding gets used in between the assignment and the for loop, a comprehension is no longer safe + + // If the binding is after the for loop, then it can't be fixed, and this check would panic, + // so we check that they are in the same statement first + let binding_unused_between = assignment_in_same_statement + && binding_stmt.is_some_and(|binding_stmt| { + let from_assign_to_loop = TextRange::new(binding_stmt.end(), for_stmt.start()); + // Test if there's any reference to the result dictionary between its definition and the for loop. + // If there's at least one, then it's been accessed in the middle somewhere, so it's not safe to change into a comprehension + !binding + .references() + .map(|ref_id| checker.semantic().reference(ref_id).range()) + .any(|text_range| from_assign_to_loop.contains_range(text_range)) + }); + // A dict update works in every context, while a dict comprehension only works when all the criteria are true + let fix_type = if binding_is_empty_dict + && assignment_in_same_statement + && binding_has_one_target + && binding_unused_between + { + DictComprehensionType::Comprehension + } else { + DictComprehensionType::Update + }; + + let mut diagnostic = checker.report_diagnostic( + ManualDictComprehension { + fix_type, + is_async: for_stmt.is_async, + }, + *range, + ); + diagnostic.try_set_optional_fix(|| { + Ok(convert_to_dict_comprehension( + fix_type, + binding, + for_stmt, + if_test.map(std::convert::AsRef::as_ref), + key.as_ref(), + value.as_ref(), + checker, + )) + }); + } else { + checker.report_diagnostic( + ManualDictComprehension { + fix_type: DictComprehensionType::Comprehension, + is_async: for_stmt.is_async, + }, + *range, + ); + } +} + +fn convert_to_dict_comprehension( + fix_type: DictComprehensionType, + binding: &Binding, + for_stmt: &ast::StmtFor, + if_test: Option<&ast::Expr>, + key: &Expr, + value: &Expr, + checker: &Checker, +) -> Option { + let locator = checker.locator(); + + let if_str = match if_test { + Some(test) => { + // If the test is an assignment expression, + // we must parenthesize it when it appears + // inside the comprehension to avoid a syntax error. + // + // Notice that we do not need `any_over_expr` here, + // since if the assignment expression appears + // internally (e.g. as an operand in a boolean + // operation) then it will already be parenthesized. + match test { + Expr::Named(_) | Expr::If(_) | Expr::Lambda(_) => { + format!(" if ({})", locator.slice(test.range())) + } + _ => format!(" if {}", locator.slice(test.range())), + } + } + None => String::new(), + }; + + // if the loop target was an implicit tuple, add parentheses around it + // ```python + // for i in a, b: + // ... + // ``` + // becomes + // {... for i in (a, b)} + let iter_str = if let Expr::Tuple(ast::ExprTuple { + parenthesized: false, + .. + }) = &*for_stmt.iter + { + format!("({})", locator.slice(for_stmt.iter.range())) + } else { + locator.slice(for_stmt.iter.range()).to_string() + }; + + let target_str = locator.slice(for_stmt.target.range()); + let for_type = if for_stmt.is_async { + "async for" + } else { + "for" + }; + // Handles the case where `key` has a trailing comma, e.g, `dict[x,] = y` + let key_range = if let Expr::Tuple(ast::ExprTuple { elts, .. }) = key { + let [expr] = elts.as_slice() else { + return None; + }; + expr.range() + } else { + key.range() + }; + let elt_str = format!( + "{}: {}", + locator.slice(key_range), + locator.slice(value.range()) + ); + + let comprehension_str = format!("{{{elt_str} {for_type} {target_str} in {iter_str}{if_str}}}"); + + let for_loop_inline_comments = comment_strings_in_range( + checker, + for_stmt.range, + &[key.range(), value.range(), for_stmt.iter.range()], + ); + + let newline = checker.stylist().line_ending().as_str(); + + let indent = locator.slice(TextRange::new( + locator.line_start(for_stmt.range.start()), + for_stmt.range.start(), + )); + + let variable_name = locator.slice(binding); + match fix_type { + DictComprehensionType::Update => { + let indentation = if for_loop_inline_comments.is_empty() { + String::new() + } else { + format!("{newline}{indent}") + }; + + let comprehension_body = format!("{variable_name}.update({comprehension_str})"); + + let text_to_replace = format!( + "{}{indentation}{comprehension_body}", + for_loop_inline_comments.join(&indentation) + ); + + Some(Fix::unsafe_edit(Edit::range_replacement( + text_to_replace, + for_stmt.range, + ))) + } + DictComprehensionType::Comprehension => { + let binding_stmt = binding.statement(checker.semantic()); + debug_assert!( + binding_stmt.is_some(), + "must be passed a binding with a statement" + ); + let binding_stmt = binding_stmt?; + + let binding_stmt_range = binding_stmt.range(); + + let annotations = match binding_stmt.as_ann_assign_stmt() { + Some(assign) => format!(": {}", locator.slice(assign.annotation.range())), + None => String::new(), + }; + + // If there are multiple binding statements in one line, we don't want to accidentally delete them + // Instead, we just delete the binding statement and leave any comments where they are + let (binding_stmt_deletion_range, binding_is_multiple_stmts) = + statement_deletion_range(checker, binding_stmt_range); + + let comments_to_move = if binding_is_multiple_stmts { + for_loop_inline_comments + } else { + let mut new_comments = + comment_strings_in_range(checker, binding_stmt_deletion_range, &[]); + new_comments.extend(for_loop_inline_comments); + new_comments + }; + + let indentation = if comments_to_move.is_empty() { + String::new() + } else { + format!("{newline}{indent}") + }; + let leading_comments = format!("{}{indentation}", comments_to_move.join(&indentation)); + + let comprehension_body = + format!("{leading_comments}{variable_name}{annotations} = {comprehension_str}"); + Some(Fix::unsafe_edits( + Edit::range_deletion(binding_stmt_deletion_range), + [Edit::range_replacement(comprehension_body, for_stmt.range)], + )) + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DictComprehensionType { + Update, + Comprehension, +} + +fn has_post_loop_references(checker: &Checker, expr: &Expr, loop_end: TextSize) -> bool { + any_over_expr(expr, &|expr| match expr { + Expr::Tuple(ast::ExprTuple { elts, .. }) => elts + .iter() + .any(|expr| has_post_loop_references(checker, expr, loop_end)), + Expr::Name(name) => { + let Some(target_binding) = checker + .semantic() + .bindings + .iter() + .find(|binding| name.range() == binding.range) + else { + // no binding in for statement => err on the safe side and make the checker skip + // e.g., `for foo[0] in bar:` or `for foo.bar in baz:` + return true; + }; + + target_binding + .references() + .map(|reference| checker.semantic().reference(reference)) + .any(|other_reference| other_reference.start() > loop_end) + } + _ => false, + }) } diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs index 752678546b4d6..058de22939bf2 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs @@ -1,16 +1,18 @@ use ruff_python_ast::{self as ast, Arguments, Expr}; -use crate::checkers::ast::Checker; -use anyhow::{anyhow, Result}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; - -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use crate::{Edit, Fix, FixAvailability, Violation}; +use crate::{ + checkers::ast::Checker, preview::is_fix_manual_list_comprehension_enabled, + rules::perflint::helpers::statement_deletion_range, +}; +use anyhow::{Result, anyhow}; + +use crate::rules::perflint::helpers::comment_strings_in_range; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; -use ruff_python_semantic::{analyze::typing::is_list, Binding}; -use ruff_python_trivia::{BackwardsTokenizer, PythonWhitespace, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_semantic::{Binding, analyze::typing::is_list}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; - /// ## What it does /// Checks for `for` loops that can be replaced by a list comprehension. /// @@ -46,11 +48,6 @@ use ruff_text_size::{Ranged, TextRange}; /// original = list(range(10000)) /// filtered.extend(x for x in original if x % 2) /// ``` -/// -/// Take care that if the original for-loop uses an assignment expression -/// as a conditional, such as `if match:=re.match("\d+","123")`, then -/// the corresponding comprehension must wrap the assignment -/// expression in parentheses to avoid a syntax error. #[derive(ViolationMetadata)] pub(crate) struct ManualListComprehension { is_async: bool, @@ -111,12 +108,14 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF // if z: // filtered.append(x) // ``` - [ast::Stmt::If(ast::StmtIf { - body, - elif_else_clauses, - test, - .. - })] => { + [ + ast::Stmt::If(ast::StmtIf { + body, + elif_else_clauses, + test, + .. + }), + ] => { if !elif_else_clauses.is_empty() { return; } @@ -144,8 +143,10 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF args, keywords, range: _, + node_index: _, }, range, + node_index: _, }) = value.as_ref() else { return; @@ -264,12 +265,23 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF ast::Stmt::Assign(assign) => Some(&assign.value), _ => None, }); + // If the variable is an empty list literal, then we might be able to replace it with a full list comprehension - // otherwise, it has to be replaced with a `list.extend` + // otherwise, it has to be replaced with a `list.extend`. let binding_is_empty_list = - list_binding_value.is_some_and(|binding_value| match binding_value.as_list_expr() { - Some(list_expr) => list_expr.elts.is_empty(), - None => false, + list_binding_value.is_some_and(|binding_value| match binding_value { + // `value = []` + Expr::List(list_expr) => list_expr.is_empty(), + // `value = list()` + // This might be linted against, but turning it into a list comprehension will also remove it + Expr::Call(call) => { + checker + .semantic() + .resolve_builtin_symbol(&call.func) + .is_some_and(|name| name == "list") + && call.arguments.is_empty() + } + _ => false, }); // If the for loop does not have the same parent element as the binding, then it cannot always be @@ -316,7 +328,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF ComprehensionType::Extend }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ManualListComprehension { is_async: for_stmt.is_async, comprehension_type: Some(comprehension_type), @@ -325,7 +337,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF ); // TODO: once this fix is stabilized, change the rule to always fixable - if checker.settings.preview.is_enabled() { + if is_fix_manual_list_comprehension_enabled(checker.settings()) { diagnostic.try_set_fix(|| { convert_to_list_extend( comprehension_type, @@ -337,15 +349,13 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF ) }); } - - checker.report_diagnostic(diagnostic); } fn convert_to_list_extend( fix_type: ComprehensionType, binding: &Binding, for_stmt: &ast::StmtFor, - if_test: Option<&ast::Expr>, + if_test: Option<&Expr>, to_append: &Expr, checker: &Checker, ) -> Result { @@ -361,10 +371,11 @@ fn convert_to_list_extend( // since if the assignment expression appears // internally (e.g. as an operand in a boolean // operation) then it will already be parenthesized. - if test.is_named_expr() { - format!(" if ({})", locator.slice(test.range())) - } else { - format!(" if {}", locator.slice(test.range())) + match test { + Expr::Named(_) | Expr::If(_) | Expr::Lambda(_) => { + format!(" if ({})", locator.slice(test.range())) + } + _ => format!(" if {}", locator.slice(test.range())), } } None => String::new(), @@ -397,22 +408,12 @@ fn convert_to_list_extend( let elt_str = locator.slice(to_append); let generator_str = format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}"); - let comment_strings_in_range = |range| { - checker - .comment_ranges() - .comments_in_range(range) - .iter() - // Ignore comments inside of the append or iterator, since these are preserved - .filter(|comment| { - !to_append.range().contains_range(**comment) - && !for_stmt.iter.range().contains_range(**comment) - }) - .map(|range| locator.slice(range).trim_whitespace_start()) - .collect() - }; - let variable_name = locator.slice(binding); - let for_loop_inline_comments: Vec<&str> = comment_strings_in_range(for_stmt.range); + let for_loop_inline_comments = comment_strings_in_range( + checker, + for_stmt.range, + &[to_append.range(), for_stmt.iter.range()], + ); let newline = checker.stylist().line_ending().as_str(); @@ -457,74 +458,25 @@ fn convert_to_list_extend( .ok_or(anyhow!( "Binding must have a statement to convert into a list comprehension" ))?; - // If the binding has multiple statements on its line, the fix would be substantially more complicated - let (semicolon_before, after_semicolon) = { - // determine whether there's a semicolon either before or after the binding statement. - // Since it's a binding statement, we can just check whether there's a semicolon immediately - // after the whitespace in front of or behind it - let mut after_tokenizer = - SimpleTokenizer::starts_at(binding_stmt_range.end(), locator.contents()) - .skip_trivia(); - - let after_semicolon = if after_tokenizer - .next() - .is_some_and(|token| token.kind() == SimpleTokenKind::Semi) - { - after_tokenizer.next() - } else { - None - }; - - let semicolon_before = BackwardsTokenizer::up_to( - binding_stmt_range.start(), - locator.contents(), - checker.comment_ranges(), - ) - .skip_trivia() - .next() - .filter(|token| token.kind() == SimpleTokenKind::Semi); - - (semicolon_before, after_semicolon) - }; + // If there are multiple binding statements in one line, we don't want to accidentally delete them // Instead, we just delete the binding statement and leave any comments where they are let (binding_stmt_deletion_range, binding_is_multiple_stmts) = - match (semicolon_before, after_semicolon) { - // ```python - // a = [] - // ``` - (None, None) => (locator.full_lines_range(binding_stmt_range), false), - - // ```python - // a = 1; b = [] - // ^^^^^^^^ - // a = 1; b = []; c = 3 - // ^^^^^^^^ - // ``` - (Some(semicolon_before), Some(_) | None) => ( - TextRange::new(semicolon_before.start(), binding_stmt_range.end()), - true, - ), - - // ```python - // a = []; b = 3 - // ^^^^^^^ - // ``` - (None, Some(after_semicolon)) => ( - TextRange::new(binding_stmt_range.start(), after_semicolon.start()), - true, - ), - }; + statement_deletion_range(checker, binding_stmt_range); let annotations = match binding_stmt.and_then(|stmt| stmt.as_ann_assign_stmt()) { Some(assign) => format!(": {}", locator.slice(assign.annotation.range())), None => String::new(), }; - let mut comments_to_move = for_loop_inline_comments; - if !binding_is_multiple_stmts { - comments_to_move.extend(comment_strings_in_range(binding_stmt_deletion_range)); - } + let comments_to_move = if binding_is_multiple_stmts { + for_loop_inline_comments + } else { + let mut new_comments = + comment_strings_in_range(checker, binding_stmt_deletion_range, &[]); + new_comments.extend(for_loop_inline_comments); + new_comments + }; let indentation = if comments_to_move.is_empty() { String::new() diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs index 24ee2ae9c188b..3322830aca515 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; use ruff_python_semantic::analyze::typing::is_list; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -69,8 +69,10 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { args, keywords, range: _, + node_index: _, }, range, + node_index: _, }) = value.as_ref() else { return; @@ -119,5 +121,5 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { return; } - checker.report_diagnostic(Diagnostic::new(ManualListCopy, *range)); + checker.report_diagnostic(ManualListCopy, *range); } diff --git a/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs index aa53fb5c8ed0d..87f89bf18ef37 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/try_except_in_loop.rs @@ -1,11 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; -use ruff_python_ast::{self as ast, Stmt}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; +use ruff_python_ast::{self as ast, PythonVersion, Stmt}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use ruff_python_ast::PythonVersion; /// ## What it does /// Checks for uses of except handling via `try`-`except` within `for` and @@ -107,7 +106,7 @@ pub(crate) fn try_except_in_loop(checker: &Checker, body: &[Stmt]) { return; } - checker.report_diagnostic(Diagnostic::new(TryExceptInLoop, handler.range())); + checker.report_diagnostic(TryExceptInLoop, handler.range()); } /// Returns `true` if a `break` or `continue` statement is present in `body`. diff --git a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs index 2662e44e87e6e..8949c0997d9d4 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -1,11 +1,13 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; use ruff_python_semantic::analyze::typing::find_assigned_value; use ruff_text_size::TextRange; use crate::checkers::ast::Checker; +use crate::fix::edits; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for explicit casts to `list` on for-loop iterables. @@ -35,6 +37,19 @@ use crate::checkers::ast::Checker; /// for i in items: /// print(i) /// ``` +/// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the +/// `list()` call, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// items = (1, 2, 3) +/// for i in list( # comment +/// items +/// ): +/// print(i) +/// ``` #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryListCast; @@ -58,8 +73,10 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt args, keywords: _, range: _, + node_index: _, }, range: list_range, + node_index: _, }) = iter else { return; @@ -86,9 +103,8 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt range: iterable_range, .. }) => { - let mut diagnostic = Diagnostic::new(UnnecessaryListCast, *list_range); - diagnostic.set_fix(remove_cast(*list_range, *iterable_range)); - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range); + diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range)); } Expr::Name(ast::ExprName { id, @@ -114,9 +130,8 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt return; } - let mut diagnostic = Diagnostic::new(UnnecessaryListCast, *list_range); - diagnostic.set_fix(remove_cast(*list_range, *iterable_range)); - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range); + diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range)); } } _ => {} @@ -124,10 +139,19 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt } /// Generate a [`Fix`] to remove a `list` cast from an expression. -fn remove_cast(list_range: TextRange, iterable_range: TextRange) -> Fix { - Fix::safe_edits( - Edit::deletion(list_range.start(), iterable_range.start()), - [Edit::deletion(iterable_range.end(), list_range.end())], +fn remove_cast(checker: &Checker, list_range: TextRange, iterable_range: TextRange) -> Fix { + let content = edits::pad( + checker.locator().slice(iterable_range).to_string(), + list_range, + checker.locator(), + ); + Fix::applicable_edit( + Edit::range_replacement(content, list_range), + if checker.comment_ranges().intersects(list_range) { + Applicability::Unsafe + } else { + Applicability::Safe + }, ) } diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF101_PERF101.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF101_PERF101.py.snap index fd545e136452c..25fc58e80c7f1 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF101_PERF101.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF101_PERF101.py.snap @@ -168,7 +168,7 @@ PERF101.py:34:10: PERF101 [*] Do not cast an iterable to `list` before iterating | = help: Remove `list()` cast -ℹ Safe fix +ℹ Unsafe fix 31 31 | ): 32 32 | pass 33 33 | @@ -238,3 +238,56 @@ PERF101.py:86:10: PERF101 [*] Do not cast an iterable to `list` before iterating 86 |-for i in builtins.list(nested_tuple): # PERF101 86 |+for i in nested_tuple: # PERF101 87 87 | pass +88 88 | +89 89 | # https://github.com/astral-sh/ruff/issues/18783 + +PERF101.py:91:9: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +89 | # https://github.com/astral-sh/ruff/issues/18783 +90 | items = (1, 2, 3) +91 | for i in(list)(items): + | ^^^^^^^^^^^^^ PERF101 +92 | print(i) + | + = help: Remove `list()` cast + +ℹ Safe fix +88 88 | +89 89 | # https://github.com/astral-sh/ruff/issues/18783 +90 90 | items = (1, 2, 3) +91 |-for i in(list)(items): + 91 |+for i in items: +92 92 | print(i) +93 93 | +94 94 | # https://github.com/astral-sh/ruff/issues/18784 + +PERF101.py:96:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | + 94 | # https://github.com/astral-sh/ruff/issues/18784 + 95 | items = (1, 2, 3) + 96 | for i in ( # 1 + | __________^ + 97 | | list # 2 + 98 | | # 3 + 99 | | )( # 4 +100 | | items # 5 +101 | | # 6 +102 | | ): + | |_^ PERF101 +103 | print(i) + | + = help: Remove `list()` cast + +ℹ Unsafe fix +93 93 | +94 94 | # https://github.com/astral-sh/ruff/issues/18784 +95 95 | items = (1, 2, 3) +96 |-for i in ( # 1 +97 |- list # 2 +98 |- # 3 +99 |-)( # 4 +100 |- items # 5 +101 |- # 6 +102 |-): + 96 |+for i in items: +103 97 | print(i) diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap index d21c46905a8f2..d451379bcf5ec 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap @@ -93,7 +93,7 @@ PERF401.py:142:9: PERF401 Use a list comprehension to create a transformed list PERF401.py:149:9: PERF401 Use a list comprehension to create a transformed list | -147 | tmp = 1; result = [] # commment should be protected +147 | tmp = 1; result = [] # comment should be protected 148 | for i in range(10): 149 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ PERF401 @@ -102,7 +102,7 @@ PERF401.py:149:9: PERF401 Use a list comprehension to create a transformed list PERF401.py:156:9: PERF401 Use a list comprehension to create a transformed list | -154 | result = []; tmp = 1 # commment should be protected +154 | result = []; tmp = 1 # comment should be protected 155 | for i in range(10): 156 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ PERF401 @@ -208,5 +208,38 @@ PERF401.py:262:13: PERF401 Use a list comprehension to create a transformed list 261 | if j := i: 262 | items.append(j) | ^^^^^^^^^^^^^^^ PERF401 +263 | +264 | def f(): | = help: Replace for loop with list comprehension + +PERF401.py:268:9: PERF401 Use a list comprehension to create a transformed list + | +266 | result = list() # this should be replaced with a comprehension +267 | for i in values: +268 | result.append(i + 1) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 +269 | +270 | def f(): + | + = help: Replace for loop with list comprehension + +PERF401.py:276:13: PERF401 Use a list comprehension to create a transformed list + | +274 | for i in src: +275 | if True if True else False: +276 | dst.append(i) + | ^^^^^^^^^^^^^ PERF401 +277 | +278 | for i in src: + | + = help: Replace for loop with list comprehension + +PERF401.py:280:13: PERF401 Use `list.extend` to create a transformed list + | +278 | for i in src: +279 | if lambda: 0: +280 | dst.append(i) + | ^^^^^^^^^^^^^ PERF401 + | + = help: Replace for loop with list.extend diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap index 6f26d4eeeed09..6cfe2b486df80 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/perflint/mod.rs -snapshot_kind: text --- PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop | @@ -9,6 +8,7 @@ PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop 5 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop | @@ -17,43 +17,145 @@ PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop 13 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:31:13: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:33:13: PERF403 Use a dictionary comprehension instead of a for-loop | -29 | for idx, name in enumerate(fruit): -30 | if idx % 2: -31 | result[idx] = name # PERF403 +31 | for idx, name in enumerate(fruit): +32 | if idx % 2: +33 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:61:13: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:63:13: PERF403 Use a dictionary comprehension instead of a for-loop | -59 | for idx, name in enumerate(fruit): -60 | if idx % 2: -61 | result[idx] = name # PERF403 +61 | for idx, name in enumerate(fruit): +62 | if idx % 2: +63 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:76:9: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:78:9: PERF403 Use a dictionary comprehension instead of a for-loop | -74 | result = {} -75 | for name in fruit: -76 | result[name] = name # PERF403 +76 | result = {} +77 | for name in fruit: +78 | result[name] = name # PERF403 | ^^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:83:9: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:85:9: PERF403 Use a dictionary comprehension instead of a for-loop | -81 | result = {} -82 | for idx, name in enumerate(fruit): -83 | result[name] = idx # PERF403 +83 | result = {} +84 | for idx, name in enumerate(fruit): +85 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:91:9: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:94:9: PERF403 Use a dictionary comprehension instead of a for-loop | -89 | result = SneakyDict() -90 | for idx, name in enumerate(fruit): -91 | result[name] = idx # PERF403 +92 | result = SneakyDict() +93 | for idx, name in enumerate(fruit): +94 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension + +PERF403.py:106:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +104 | ): +105 | # comment 3 +106 | / result[ +107 | | name # comment 4 +108 | | ] = idx # PERF403 + | |_______________^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:115:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +113 | a = 1; result = {}; b = 2 +114 | for idx, name in enumerate(fruit): +115 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:122:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +120 | result = {"kiwi": 3} +121 | for idx, name in enumerate(fruit): +122 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:129:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +127 | (_, result) = (None, {"kiwi": 3}) +128 | for idx, name in enumerate(fruit): +129 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:137:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +135 | print(len(result)) +136 | for idx, name in enumerate(fruit): +137 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:145:13: PERF403 Use a dictionary comprehension instead of a for-loop + | +143 | for idx, name in enumerate(fruit): +144 | if last_idx := idx % 3: +145 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:153:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +151 | result = {} +152 | for idx, name in indices, fruit: +153 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:162:13: PERF403 Use a dictionary comprehension instead of a for-loop + | +160 | for k, v in src: +161 | if True if True else False: +162 | dst[k] = v + | ^^^^^^^^^^ PERF403 +163 | +164 | for k, v in src: + | + = help: Replace for loop with dict comprehension + +PERF403.py:166:13: PERF403 Use a dictionary comprehension instead of a for-loop + | +164 | for k, v in src: +165 | if lambda: 0: +166 | dst[k] = v + | ^^^^^^^^^^ PERF403 +167 | +168 | # https://github.com/astral-sh/ruff/issues/18859 + | + = help: Replace for loop with dict comprehension + +PERF403.py:172:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +170 | v = {} +171 | for o,(x,)in(): +172 | v[x,]=o + | ^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap index ff568d18219eb..d5b614bfe98bb 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap @@ -169,10 +169,10 @@ PERF401.py:119:13: PERF401 [*] Use a list comprehension to create a transformed 117 |- # single-line comment 2 should be protected 118 |- if i % 2: # single-line comment 3 should be protected 119 |- result.append(i) # PERF401 - 115 |+ # single-line comment 1 should be protected - 116 |+ # single-line comment 2 should be protected - 117 |+ # single-line comment 3 should be protected - 118 |+ # comment after assignment should be protected + 115 |+ # comment after assignment should be protected + 116 |+ # single-line comment 1 should be protected + 117 |+ # single-line comment 2 should be protected + 118 |+ # single-line comment 3 should be protected 119 |+ result = [i for i in range(10) if i % 2] # PERF401 120 120 | 121 121 | @@ -223,7 +223,7 @@ PERF401.py:142:9: PERF401 [*] Use a list comprehension to create a transformed l PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed list | -147 | tmp = 1; result = [] # commment should be protected +147 | tmp = 1; result = [] # comment should be protected 148 | for i in range(10): 149 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ PERF401 @@ -234,10 +234,10 @@ PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed l 144 144 | 145 145 | def f(): 146 146 | # make sure that `tmp` is not deleted -147 |- tmp = 1; result = [] # commment should be protected +147 |- tmp = 1; result = [] # comment should be protected 148 |- for i in range(10): 149 |- result.append(i + 1) # PERF401 - 147 |+ tmp = 1 # commment should be protected + 147 |+ tmp = 1 # comment should be protected 148 |+ result = [i + 1 for i in range(10)] # PERF401 150 149 | 151 150 | @@ -245,7 +245,7 @@ PERF401.py:149:9: PERF401 [*] Use a list comprehension to create a transformed l PERF401.py:156:9: PERF401 [*] Use a list comprehension to create a transformed list | -154 | result = []; tmp = 1 # commment should be protected +154 | result = []; tmp = 1 # comment should be protected 155 | for i in range(10): 156 | result.append(i + 1) # PERF401 | ^^^^^^^^^^^^^^^^^^^^ PERF401 @@ -256,10 +256,10 @@ PERF401.py:156:9: PERF401 [*] Use a list comprehension to create a transformed l 151 151 | 152 152 | def f(): 153 153 | # make sure that `tmp` is not deleted -154 |- result = []; tmp = 1 # commment should be protected +154 |- result = []; tmp = 1 # comment should be protected 155 |- for i in range(10): 156 |- result.append(i + 1) # PERF401 - 154 |+ tmp = 1 # commment should be protected + 154 |+ tmp = 1 # comment should be protected 155 |+ result = [i + 1 for i in range(10)] # PERF401 157 156 | 158 157 | @@ -492,6 +492,8 @@ PERF401.py:262:13: PERF401 [*] Use a list comprehension to create a transformed 261 | if j := i: 262 | items.append(j) | ^^^^^^^^^^^^^^^ PERF401 +263 | +264 | def f(): | = help: Replace for loop with list comprehension @@ -505,3 +507,73 @@ PERF401.py:262:13: PERF401 [*] Use a list comprehension to create a transformed 261 |- if j := i: 262 |- items.append(j) 259 |+ items = [j for i in range(5) if (j := i)] +263 260 | +264 261 | def f(): +265 262 | values = [1, 2, 3] + +PERF401.py:268:9: PERF401 [*] Use a list comprehension to create a transformed list + | +266 | result = list() # this should be replaced with a comprehension +267 | for i in values: +268 | result.append(i + 1) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 +269 | +270 | def f(): + | + = help: Replace for loop with list comprehension + +ℹ Unsafe fix +263 263 | +264 264 | def f(): +265 265 | values = [1, 2, 3] +266 |- result = list() # this should be replaced with a comprehension +267 |- for i in values: +268 |- result.append(i + 1) # PERF401 + 266 |+ # this should be replaced with a comprehension + 267 |+ result = [i + 1 for i in values] # PERF401 +269 268 | +270 269 | def f(): +271 270 | src = [1] + +PERF401.py:276:13: PERF401 [*] Use a list comprehension to create a transformed list + | +274 | for i in src: +275 | if True if True else False: +276 | dst.append(i) + | ^^^^^^^^^^^^^ PERF401 +277 | +278 | for i in src: + | + = help: Replace for loop with list comprehension + +ℹ Unsafe fix +269 269 | +270 270 | def f(): +271 271 | src = [1] +272 |- dst = [] +273 272 | +274 |- for i in src: +275 |- if True if True else False: +276 |- dst.append(i) + 273 |+ dst = [i for i in src if (True if True else False)] +277 274 | +278 275 | for i in src: +279 276 | if lambda: 0: + +PERF401.py:280:13: PERF401 [*] Use `list.extend` to create a transformed list + | +278 | for i in src: +279 | if lambda: 0: +280 | dst.append(i) + | ^^^^^^^^^^^^^ PERF401 + | + = help: Replace for loop with list.extend + +ℹ Unsafe fix +275 275 | if True if True else False: +276 276 | dst.append(i) +277 277 | +278 |- for i in src: +279 |- if lambda: 0: +280 |- dst.append(i) + 278 |+ dst.extend(i for i in src if (lambda: 0)) diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap new file mode 100644 index 0000000000000..4c3a99b81f479 --- /dev/null +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap @@ -0,0 +1,379 @@ +--- +source: crates/ruff_linter/src/rules/perflint/mod.rs +--- +PERF403.py:5:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +3 | result = {} +4 | for idx, name in enumerate(fruit): +5 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +1 1 | def foo(): +2 2 | fruit = ["apple", "pear", "orange"] +3 |- result = {} +4 |- for idx, name in enumerate(fruit): +5 |- result[idx] = name # PERF403 + 3 |+ result = {idx: name for idx, name in enumerate(fruit)} # PERF403 +6 4 | +7 5 | +8 6 | def foo(): + +PERF403.py:13:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +11 | for idx, name in enumerate(fruit): +12 | if idx % 2: +13 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +7 7 | +8 8 | def foo(): +9 9 | fruit = ["apple", "pear", "orange"] +10 |- result = {} +11 |- for idx, name in enumerate(fruit): +12 |- if idx % 2: +13 |- result[idx] = name # PERF403 + 10 |+ result = {idx: name for idx, name in enumerate(fruit) if idx % 2} # PERF403 +14 11 | +15 12 | +16 13 | def foo(): + +PERF403.py:33:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +31 | for idx, name in enumerate(fruit): +32 | if idx % 2: +33 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +26 26 | +27 27 | +28 28 | def foo(): +29 |- result = {} +30 29 | fruit = ["apple", "pear", "orange"] +31 |- for idx, name in enumerate(fruit): +32 |- if idx % 2: +33 |- result[idx] = name # PERF403 + 30 |+ result = {idx: name for idx, name in enumerate(fruit) if idx % 2} # PERF403 +34 31 | +35 32 | +36 33 | def foo(): + +PERF403.py:63:13: PERF403 [*] Use `dict.update` instead of a for-loop + | +61 | for idx, name in enumerate(fruit): +62 | if idx % 2: +63 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +58 58 | def foo(): +59 59 | result = {1: "banana"} +60 60 | fruit = ["apple", "pear", "orange"] +61 |- for idx, name in enumerate(fruit): +62 |- if idx % 2: +63 |- result[idx] = name # PERF403 + 61 |+ result.update({idx: name for idx, name in enumerate(fruit) if idx % 2}) # PERF403 +64 62 | +65 63 | +66 64 | def foo(): + +PERF403.py:78:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +76 | result = {} +77 | for name in fruit: +78 | result[name] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +73 73 | +74 74 | def foo(): +75 75 | fruit = ["apple", "pear", "orange"] +76 |- result = {} +77 |- for name in fruit: +78 |- result[name] = name # PERF403 + 76 |+ result = {name: name for name in fruit} # PERF403 +79 77 | +80 78 | +81 79 | def foo(): + +PERF403.py:85:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +83 | result = {} +84 | for idx, name in enumerate(fruit): +85 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +80 80 | +81 81 | def foo(): +82 82 | fruit = ["apple", "pear", "orange"] +83 |- result = {} +84 |- for idx, name in enumerate(fruit): +85 |- result[name] = idx # PERF403 + 83 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403 +86 84 | +87 85 | +88 86 | def foo(): + +PERF403.py:94:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +92 | result = SneakyDict() +93 | for idx, name in enumerate(fruit): +94 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +89 89 | from builtins import dict as SneakyDict +90 90 | +91 91 | fruit = ["apple", "pear", "orange"] +92 |- result = SneakyDict() +93 |- for idx, name in enumerate(fruit): +94 |- result[name] = idx # PERF403 + 92 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403 +95 93 | +96 94 | +97 95 | def foo(): + +PERF403.py:106:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +104 | ): +105 | # comment 3 +106 | / result[ +107 | | name # comment 4 +108 | | ] = idx # PERF403 + | |_______________^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +96 96 | +97 97 | def foo(): +98 98 | fruit = ["apple", "pear", "orange"] +99 |- result: dict[str, int] = { +100 |- # comment 1 +101 |- } +102 |- for idx, name in enumerate( + 99 |+ # comment 1 + 100 |+ # comment 3 + 101 |+ # comment 4 + 102 |+ result: dict[str, int] = {name: idx for idx, name in enumerate( +103 103 | fruit # comment 2 +104 |- ): +105 |- # comment 3 +106 |- result[ +107 |- name # comment 4 +108 |- ] = idx # PERF403 + 104 |+ )} # PERF403 +109 105 | +110 106 | +111 107 | def foo(): + +PERF403.py:115:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +113 | a = 1; result = {}; b = 2 +114 | for idx, name in enumerate(fruit): +115 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +110 110 | +111 111 | def foo(): +112 112 | fruit = ["apple", "pear", "orange"] +113 |- a = 1; result = {}; b = 2 +114 |- for idx, name in enumerate(fruit): +115 |- result[name] = idx # PERF403 + 113 |+ a = 1; b = 2 + 114 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403 +116 115 | +117 116 | +118 117 | def foo(): + +PERF403.py:122:9: PERF403 [*] Use `dict.update` instead of a for-loop + | +120 | result = {"kiwi": 3} +121 | for idx, name in enumerate(fruit): +122 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +118 118 | def foo(): +119 119 | fruit = ["apple", "pear", "orange"] +120 120 | result = {"kiwi": 3} +121 |- for idx, name in enumerate(fruit): +122 |- result[name] = idx # PERF403 + 121 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403 +123 122 | +124 123 | +125 124 | def foo(): + +PERF403.py:129:9: PERF403 [*] Use `dict.update` instead of a for-loop + | +127 | (_, result) = (None, {"kiwi": 3}) +128 | for idx, name in enumerate(fruit): +129 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +125 125 | def foo(): +126 126 | fruit = ["apple", "pear", "orange"] +127 127 | (_, result) = (None, {"kiwi": 3}) +128 |- for idx, name in enumerate(fruit): +129 |- result[name] = idx # PERF403 + 128 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403 +130 129 | +131 130 | +132 131 | def foo(): + +PERF403.py:137:9: PERF403 [*] Use `dict.update` instead of a for-loop + | +135 | print(len(result)) +136 | for idx, name in enumerate(fruit): +137 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +133 133 | fruit = ["apple", "pear", "orange"] +134 134 | result = {} +135 135 | print(len(result)) +136 |- for idx, name in enumerate(fruit): +137 |- result[name] = idx # PERF403 + 136 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403 +138 137 | +139 138 | +140 139 | def foo(): + +PERF403.py:145:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +143 | for idx, name in enumerate(fruit): +144 | if last_idx := idx % 3: +145 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +139 139 | +140 140 | def foo(): +141 141 | fruit = ["apple", "pear", "orange"] +142 |- result = {} +143 |- for idx, name in enumerate(fruit): +144 |- if last_idx := idx % 3: +145 |- result[name] = idx # PERF403 + 142 |+ result = {name: idx for idx, name in enumerate(fruit) if (last_idx := idx % 3)} # PERF403 +146 143 | +147 144 | +148 145 | def foo(): + +PERF403.py:153:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +151 | result = {} +152 | for idx, name in indices, fruit: +153 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +148 148 | def foo(): +149 149 | fruit = ["apple", "pear", "orange"] +150 150 | indices = [0, 1, 2] +151 |- result = {} +152 |- for idx, name in indices, fruit: +153 |- result[name] = idx # PERF403 + 151 |+ result = {name: idx for idx, name in (indices, fruit)} # PERF403 +154 152 | +155 153 | +156 154 | def foo(): + +PERF403.py:162:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +160 | for k, v in src: +161 | if True if True else False: +162 | dst[k] = v + | ^^^^^^^^^^ PERF403 +163 | +164 | for k, v in src: + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +155 155 | +156 156 | def foo(): +157 157 | src = (("x", 1),) +158 |- dst = {} +159 158 | +160 |- for k, v in src: +161 |- if True if True else False: +162 |- dst[k] = v + 159 |+ dst = {k: v for k, v in src if (True if True else False)} +163 160 | +164 161 | for k, v in src: +165 162 | if lambda: 0: + +PERF403.py:166:13: PERF403 [*] Use `dict.update` instead of a for-loop + | +164 | for k, v in src: +165 | if lambda: 0: +166 | dst[k] = v + | ^^^^^^^^^^ PERF403 +167 | +168 | # https://github.com/astral-sh/ruff/issues/18859 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +161 161 | if True if True else False: +162 162 | dst[k] = v +163 163 | +164 |- for k, v in src: +165 |- if lambda: 0: +166 |- dst[k] = v + 164 |+ dst.update({k: v for k, v in src if (lambda: 0)}) +167 165 | +168 166 | # https://github.com/astral-sh/ruff/issues/18859 +169 167 | def foo(): + +PERF403.py:172:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +170 | v = {} +171 | for o,(x,)in(): +172 | v[x,]=o + | ^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +167 167 | +168 168 | # https://github.com/astral-sh/ruff/issues/18859 +169 169 | def foo(): +170 |- v = {} +171 |- for o,(x,)in(): +172 |- v[x,]=o + 170 |+ v = {x: o for o,(x,) in ()} +173 171 | +174 172 | +175 173 | # https://github.com/astral-sh/ruff/issues/19005 diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index a6a2180bbb6c5..d7253a68dbeeb 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -19,7 +19,7 @@ mod tests { use crate::rules::{isort, pycodestyle}; use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; use super::settings::Settings; @@ -71,7 +71,7 @@ mod tests { Path::new("pycodestyle").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -95,7 +95,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -106,7 +106,7 @@ mod tests { &settings::LinterSettings::for_rule(Rule::MissingNewlineAtEndOfFile), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -155,7 +155,7 @@ mod tests { Path::new("pycodestyle").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -172,7 +172,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -190,7 +190,7 @@ mod tests { Path::new("pycodestyle").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -214,7 +214,7 @@ mod tests { Path::new("pycodestyle").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -244,7 +244,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -257,7 +257,9 @@ mod tests { lines_after_imports: isize, lines_between_types: usize, ) -> Result<()> { - let snapshot = format!("too_many_blank_lines_isort_compatibility-lines-after({lines_after_imports})-between({lines_between_types})"); + let snapshot = format!( + "too_many_blank_lines_isort_compatibility-lines-after({lines_after_imports})-between({lines_between_types})" + ); let diagnostics = test_path( Path::new("pycodestyle").join("E30_isort.py"), &settings::LinterSettings { @@ -272,7 +274,7 @@ mod tests { ]) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -288,7 +290,7 @@ mod tests { Path::new("pycodestyle").join("E30.pyi"), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -304,7 +306,7 @@ mod tests { Path::new("pycodestyle").join("E30.ipynb"), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -320,7 +322,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -334,7 +336,7 @@ mod tests { Rule::IsLiteral, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -349,7 +351,7 @@ mod tests { Rule::MultipleLeadingHashesForBlockComment, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -367,7 +369,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::LineTooLong) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -383,7 +385,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::DocLineTooLong) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -399,7 +401,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::DocLineTooLong) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -420,7 +422,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::LineTooLong) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/overlong.rs b/crates/ruff_linter/src/rules/pycodestyle/overlong.rs index 691ea9dd231aa..56cfb25df9d28 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/overlong.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/overlong.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use ruff_python_trivia::{is_pragma_comment, CommentRanges}; +use ruff_python_trivia::{CommentRanges, is_pragma_comment}; use ruff_source_file::Line; use ruff_text_size::{TextLen, TextRange}; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs index 44d1236c95150..6bf061ede5676 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_class_name.rs @@ -1,9 +1,10 @@ use ruff_python_ast::Identifier; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pycodestyle::helpers::is_ambiguous_name; /// ## What it does @@ -36,13 +37,8 @@ impl Violation for AmbiguousClassName { } /// E742 -pub(crate) fn ambiguous_class_name(name: &Identifier) -> Option { +pub(crate) fn ambiguous_class_name(checker: &Checker, name: &Identifier) { if is_ambiguous_name(name) { - Some(Diagnostic::new( - AmbiguousClassName(name.to_string()), - name.range(), - )) - } else { - None + checker.report_diagnostic(AmbiguousClassName(name.to_string()), name.range()); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs index 6c1b88f8d9e59..1cd8770c86ab0 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_function_name.rs @@ -1,9 +1,10 @@ use ruff_python_ast::Identifier; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; +use crate::checkers::ast::Checker; use crate::rules::pycodestyle::helpers::is_ambiguous_name; /// ## What it does @@ -36,13 +37,8 @@ impl Violation for AmbiguousFunctionName { } /// E743 -pub(crate) fn ambiguous_function_name(name: &Identifier) -> Option { +pub(crate) fn ambiguous_function_name(checker: &Checker, name: &Identifier) { if is_ambiguous_name(name) { - Some(Diagnostic::new( - AmbiguousFunctionName(name.to_string()), - name.range(), - )) - } else { - None + checker.report_diagnostic(AmbiguousFunctionName(name.to_string()), name.range()); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs index bf306bf390f5e..156b336a47a78 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/ambiguous_variable_name.rs @@ -1,8 +1,8 @@ use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pycodestyle::helpers::is_ambiguous_name; @@ -32,7 +32,6 @@ use crate::rules::pycodestyle::helpers::is_ambiguous_name; /// o = 123 /// i = 42 /// ``` -/// #[derive(ViolationMetadata)] pub(crate) struct AmbiguousVariableName(pub String); @@ -50,9 +49,6 @@ pub(crate) fn ambiguous_variable_name(checker: &Checker, name: &str, range: Text return; } if is_ambiguous_name(name) { - checker.report_diagnostic(Diagnostic::new( - AmbiguousVariableName(name.to_string()), - range, - )); + checker.report_diagnostic(AmbiguousVariableName(name.to_string()), range); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs index ebf49db0d6e65..3040240166149 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/bare_except.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::except; use ruff_python_ast::{self as ast, ExceptHandler, Expr, Stmt}; -use crate::Locator; +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for bare `except` catches in `try`-`except` statements. @@ -56,21 +56,16 @@ impl Violation for BareExcept { /// E722 pub(crate) fn bare_except( + checker: &Checker, type_: Option<&Expr>, body: &[Stmt], handler: &ExceptHandler, - locator: &Locator, -) -> Option { +) { if type_.is_none() && !body .iter() .any(|stmt| matches!(stmt, Stmt::Raise(ast::StmtRaise { exc: None, .. }))) { - Some(Diagnostic::new( - BareExcept, - except(handler, locator.contents()), - )) - } else { - None + checker.report_diagnostic(BareExcept, except(handler, checker.locator().contents())); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs index 87400d64bd366..8b44df73a3412 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -5,11 +5,7 @@ use std::slice::Iter; use itertools::Itertools; -use ruff_diagnostics::AlwaysFixableViolation; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Edit; -use ruff_diagnostics::Fix; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_notebook::CellOffsets; use ruff_python_ast::PySourceType; use ruff_python_codegen::Stylist; @@ -21,10 +17,12 @@ use ruff_source_file::{LineRanges, UniversalNewlines}; use ruff_text_size::TextRange; use ruff_text_size::TextSize; +use crate::checkers::ast::{DiagnosticGuard, LintContext}; use crate::checkers::logical_lines::expand_indent; use crate::line_width::IndentWidth; use crate::rules::pycodestyle::helpers::is_non_logical_token; -use crate::Locator; +use crate::settings::LinterSettings; +use crate::{AlwaysFixableViolation, Edit, Fix, Locator, Violation}; /// Number of blank lines around top level classes and functions. const BLANK_LINES_TOP_LEVEL: u32 = 2; @@ -690,43 +688,54 @@ impl Status { } /// Contains variables used for the linting of blank lines. -#[derive(Debug)] -pub(crate) struct BlankLinesChecker<'a> { +pub(crate) struct BlankLinesChecker<'a, 'b> { stylist: &'a Stylist<'a>, locator: &'a Locator<'a>, - indent_width: IndentWidth, - lines_after_imports: isize, - lines_between_types: usize, source_type: PySourceType, cell_offsets: Option<&'a CellOffsets>, + context: &'a LintContext<'b>, + settings: &'a LinterSettings, } -impl<'a> BlankLinesChecker<'a> { +impl<'a, 'b> BlankLinesChecker<'a, 'b> { pub(crate) fn new( locator: &'a Locator<'a>, stylist: &'a Stylist<'a>, - settings: &crate::settings::LinterSettings, + settings: &'a LinterSettings, source_type: PySourceType, cell_offsets: Option<&'a CellOffsets>, - ) -> BlankLinesChecker<'a> { + context: &'a LintContext<'b>, + ) -> BlankLinesChecker<'a, 'b> { BlankLinesChecker { stylist, locator, - indent_width: settings.tab_size, - lines_after_imports: settings.isort.lines_after_imports, - lines_between_types: settings.isort.lines_between_types, source_type, cell_offsets, + context, + settings, } } + /// Report a diagnostic if the associated rule is enabled. + fn report_diagnostic( + &self, + kind: T, + range: TextRange, + ) -> Option> { + self.context.report_diagnostic_if_enabled(kind, range) + } + /// E301, E302, E303, E304, E305, E306 - pub(crate) fn check_lines(&self, tokens: &Tokens, diagnostics: &mut Vec) { + pub(crate) fn check_lines(&self, tokens: &Tokens) { let mut prev_indent_length: Option = None; let mut prev_logical_line: Option = None; let mut state = BlankLinesState::default(); - let line_preprocessor = - LinePreprocessor::new(tokens, self.locator, self.indent_width, self.cell_offsets); + let line_preprocessor = LinePreprocessor::new( + tokens, + self.locator, + self.settings.tab_size, + self.cell_offsets, + ); for logical_line in line_preprocessor { // Reset `follows` after a dedent: @@ -762,7 +771,7 @@ impl<'a> BlankLinesChecker<'a> { state.class_status.update(&logical_line); state.fn_status.update(&logical_line); - self.check_line(&logical_line, &state, prev_indent_length, diagnostics); + self.check_line(&logical_line, &state, prev_indent_length); match logical_line.kind { LogicalLineKind::Class => { @@ -818,13 +827,12 @@ impl<'a> BlankLinesChecker<'a> { } } - #[allow(clippy::nonminimal_bool)] + #[expect(clippy::nonminimal_bool)] fn check_line( &self, line: &LogicalLineInfo, state: &BlankLinesState, prev_indent_length: Option, - diagnostics: &mut Vec, ) { if line.preceding_blank_lines == 0 // Only applies to methods. @@ -842,13 +850,14 @@ impl<'a> BlankLinesChecker<'a> { && !self.source_type.is_stub() { // E301 - let mut diagnostic = Diagnostic::new(BlankLineBetweenMethods, line.first_token_range); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - self.stylist.line_ending().to_string(), - self.locator.line_start(state.last_non_comment_line_end), - ))); - - diagnostics.push(diagnostic); + if let Some(mut diagnostic) = + self.report_diagnostic(BlankLineBetweenMethods, line.first_token_range) + { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist.line_ending().to_string(), + self.locator.line_start(state.last_non_comment_line_end), + ))); + } } // Blank lines in stub files are used to group definitions. Don't enforce blank lines. @@ -870,7 +879,7 @@ impl<'a> BlankLinesChecker<'a> { // `isort` defaults to 2 if before a class or function definition (except in stubs where it is one) and 1 otherwise. // Defaulting to 2 (or 1 in stubs) here is correct because the variable is only used when testing the // blank lines before a class or function definition. - u32::try_from(self.lines_after_imports).unwrap_or(max_lines_level) + u32::try_from(self.settings.isort.lines_after_imports).unwrap_or(max_lines_level) } else { max_lines_level } @@ -896,32 +905,31 @@ impl<'a> BlankLinesChecker<'a> { && !line.is_beginning_of_cell { // E302 - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = self.report_diagnostic( BlankLinesTopLevel { actual_blank_lines: line.preceding_blank_lines.count(), expected_blank_lines: expected_blank_lines_before_definition, }, line.first_token_range, - ); - - if let Some(blank_lines_range) = line.blank_lines.range() { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - self.stylist - .line_ending() - .repeat(expected_blank_lines_before_definition as usize), - blank_lines_range, - ))); - } else { - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - self.stylist.line_ending().repeat( - (expected_blank_lines_before_definition - - line.preceding_blank_lines.count()) as usize, - ), - self.locator.line_start(state.last_non_comment_line_end), - ))); + ) { + if let Some(blank_lines_range) = line.blank_lines.range() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + self.stylist + .line_ending() + .repeat(expected_blank_lines_before_definition as usize), + blank_lines_range, + ))); + } else { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist.line_ending().repeat( + (expected_blank_lines_before_definition + - line.preceding_blank_lines.count()) + as usize, + ), + self.locator.line_start(state.last_non_comment_line_end), + ))); + } } - - diagnostics.push(diagnostic); } // If between `import` and `from .. import ..` or the other way round, @@ -933,32 +941,31 @@ impl<'a> BlankLinesChecker<'a> { (LogicalLineKind::Import, Follows::FromImport) | (LogicalLineKind::FromImport, Follows::Import) ) { - max_lines_level.max(u32::try_from(self.lines_between_types).unwrap_or(u32::MAX)) + max_lines_level + .max(u32::try_from(self.settings.isort.lines_between_types).unwrap_or(u32::MAX)) } else { expected_blank_lines_before_definition }; if line.blank_lines > max_blank_lines { // E303 - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = self.report_diagnostic( TooManyBlankLines { actual_blank_lines: line.blank_lines.count(), }, line.first_token_range, - ); - - if let Some(blank_lines_range) = line.blank_lines.range() { - if max_blank_lines == 0 { - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(blank_lines_range))); - } else { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - self.stylist.line_ending().repeat(max_blank_lines as usize), - blank_lines_range, - ))); + ) { + if let Some(blank_lines_range) = line.blank_lines.range() { + if max_blank_lines == 0 { + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(blank_lines_range))); + } else { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + self.stylist.line_ending().repeat(max_blank_lines as usize), + blank_lines_range, + ))); + } } } - - diagnostics.push(diagnostic); } if matches!(state.follows, Follows::Decorator) @@ -966,38 +973,38 @@ impl<'a> BlankLinesChecker<'a> { && line.preceding_blank_lines > 0 { // E304 - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = self.report_diagnostic( BlankLineAfterDecorator { actual_blank_lines: line.preceding_blank_lines.count(), }, line.first_token_range, - ); - - // Get all the lines between the last decorator line (included) and the current line (included). - // Then remove all blank lines. - let trivia_range = TextRange::new( - state.last_non_comment_line_end, - self.locator.line_start(line.first_token_range.start()), - ); - let trivia_text = self.locator.slice(trivia_range); - let mut trivia_without_blank_lines = trivia_text - .universal_newlines() - .filter_map(|line| (!line.trim_whitespace().is_empty()).then_some(line.as_str())) - .join(&self.stylist.line_ending()); - - let fix = if trivia_without_blank_lines.is_empty() { - Fix::safe_edit(Edit::range_deletion(trivia_range)) - } else { - trivia_without_blank_lines.push_str(&self.stylist.line_ending()); - Fix::safe_edit(Edit::range_replacement( - trivia_without_blank_lines, - trivia_range, - )) - }; - - diagnostic.set_fix(fix); + ) { + // Get all the lines between the last decorator line (included) and the current line (included). + // Then remove all blank lines. + let trivia_range = TextRange::new( + state.last_non_comment_line_end, + self.locator.line_start(line.first_token_range.start()), + ); + let trivia_text = self.locator.slice(trivia_range); + let mut trivia_without_blank_lines = trivia_text + .universal_newlines() + .filter_map(|line| { + (!line.trim_whitespace().is_empty()).then_some(line.as_str()) + }) + .join(&self.stylist.line_ending()); + + let fix = if trivia_without_blank_lines.is_empty() { + Fix::safe_edit(Edit::range_deletion(trivia_range)) + } else { + trivia_without_blank_lines.push_str(&self.stylist.line_ending()); + Fix::safe_edit(Edit::range_replacement( + trivia_without_blank_lines, + trivia_range, + )) + }; - diagnostics.push(diagnostic); + diagnostic.set_fix(fix); + } } if line.preceding_blank_lines < BLANK_LINES_TOP_LEVEL @@ -1013,30 +1020,28 @@ impl<'a> BlankLinesChecker<'a> { && !line.is_beginning_of_cell { // E305 - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = self.report_diagnostic( BlankLinesAfterFunctionOrClass { actual_blank_lines: line.preceding_blank_lines.count(), }, line.first_token_range, - ); - - if let Some(blank_lines_range) = line.blank_lines.range() { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - self.stylist - .line_ending() - .repeat(BLANK_LINES_TOP_LEVEL as usize), - blank_lines_range, - ))); - } else { - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - self.stylist.line_ending().repeat( - (BLANK_LINES_TOP_LEVEL - line.preceding_blank_lines.count()) as usize, - ), - self.locator.line_start(state.last_non_comment_line_end), - ))); + ) { + if let Some(blank_lines_range) = line.blank_lines.range() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + self.stylist + .line_ending() + .repeat(BLANK_LINES_TOP_LEVEL as usize), + blank_lines_range, + ))); + } else { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist.line_ending().repeat( + (BLANK_LINES_TOP_LEVEL - line.preceding_blank_lines.count()) as usize, + ), + self.locator.line_start(state.last_non_comment_line_end), + ))); + } } - - diagnostics.push(diagnostic); } if line.preceding_blank_lines == 0 @@ -1056,15 +1061,14 @@ impl<'a> BlankLinesChecker<'a> { && !self.source_type.is_stub() { // E306 - let mut diagnostic = - Diagnostic::new(BlankLinesBeforeNestedDefinition, line.first_token_range); - - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - self.stylist.line_ending().to_string(), - self.locator.line_start(line.first_token_range.start()), - ))); - - diagnostics.push(diagnostic); + if let Some(mut diagnostic) = + self.report_diagnostic(BlankLinesBeforeNestedDefinition, line.first_token_range) + { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + self.stylist.line_ending().to_string(), + self.locator.line_start(line.first_token_range.start()), + ))); + } } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs index 396f50fdd3eb9..49a2fb1db58ec 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/compound_statements.rs @@ -1,6 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_notebook::CellOffsets; use ruff_python_ast::PySourceType; use ruff_python_index::Indexer; @@ -8,6 +6,9 @@ use ruff_python_parser::{TokenIterWithContext, TokenKind, Tokens}; use ruff_text_size::{Ranged, TextSize}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Violation}; +use crate::{Edit, Fix}; /// ## What it does /// Checks for compound statements (multiple statements on the same line). @@ -98,7 +99,7 @@ impl AlwaysFixableViolation for UselessSemicolon { /// E701, E702, E703 pub(crate) fn compound_statements( - diagnostics: &mut Vec, + context: &LintContext, tokens: &Tokens, locator: &Locator, indexer: &Indexer, @@ -167,14 +168,16 @@ pub(crate) fn compound_statements( !has_non_trivia_tokens_till(token_iter.clone(), cell_range.end()) })) { - let mut diagnostic = Diagnostic::new(UselessSemicolon, range); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion( - indexer - .preceded_by_continuations(range.start(), locator.contents()) - .unwrap_or(range.start()), - range.end(), - ))); - diagnostics.push(diagnostic); + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(UselessSemicolon, range) + { + diagnostic.set_fix(Fix::safe_edit(Edit::deletion( + indexer + .preceded_by_continuations(range.start(), locator.contents()) + .unwrap_or(range.start()), + range.end(), + ))); + } } } @@ -224,7 +227,8 @@ pub(crate) fn compound_statements( | TokenKind::NonLogicalNewline => {} _ => { if let Some(range) = semi { - diagnostics.push(Diagnostic::new(MultipleStatementsOnOneLineSemicolon, range)); + context + .report_diagnostic_if_enabled(MultipleStatementsOnOneLineSemicolon, range); // Reset. semi = None; @@ -232,7 +236,7 @@ pub(crate) fn compound_statements( } if let Some(range) = colon { - diagnostics.push(Diagnostic::new(MultipleStatementsOnOneLineColon, range)); + context.report_diagnostic_if_enabled(MultipleStatementsOnOneLineColon, range); // Reset. colon = None; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs index cc11ee34957f8..c9f0ae0753ab7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs @@ -1,8 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_source_file::Line; +use crate::Violation; +use crate::checkers::ast::LintContext; use crate::rules::pycodestyle::overlong::Overlong; use crate::settings::LinterSettings; @@ -86,9 +87,13 @@ pub(crate) fn doc_line_too_long( line: &Line, comment_ranges: &CommentRanges, settings: &LinterSettings, -) -> Option { - let limit = settings.pycodestyle.max_doc_length?; - Overlong::try_from_line( + context: &LintContext, +) { + let Some(limit) = settings.pycodestyle.max_doc_length else { + return; + }; + + if let Some(overlong) = Overlong::try_from_line( line, comment_ranges, limit, @@ -98,11 +103,10 @@ pub(crate) fn doc_line_too_long( &[] }, settings.tab_size, - ) - .map(|overlong| { - Diagnostic::new( + ) { + context.report_diagnostic( DocLineTooLong(overlong.width(), limit.value() as usize), overlong.range(), - ) - }) + ); + } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs index 25eefaeb673cd..58baf660bc9c9 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// This is not a regular diagnostic; instead, it's raised when a file cannot be read @@ -66,7 +67,7 @@ impl Violation for IOError { #[deprecated(note = "E999 has been removed")] pub(crate) struct SyntaxError; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for SyntaxError { fn message(&self) -> String { unreachable!("E999 has been removed") diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 9844aa6ad934b..1e0fe8507a993 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -1,13 +1,16 @@ use memchr::memchr_iter; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{AnyStringFlags, FStringElement, StringLike, StringLikePart}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ + AnyStringFlags, InterpolatedStringElement, InterpolatedStringElements, StringLike, + StringLikePart, +}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::fix::edits::pad_start; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for invalid escape sequences. @@ -70,39 +73,16 @@ pub(crate) fn invalid_escape_sequence(checker: &Checker, string_like: StringLike StringLikePart::String(_) | StringLikePart::Bytes(_) => { analyze_escape_chars(locator, part.range(), part.flags()) } - StringLikePart::FString(f_string) => { - let flags = AnyStringFlags::from(f_string.flags); - let mut escape_chars_state = EscapeCharsState::default(); - // Whether we suggest converting to a raw string or - // adding backslashes depends on the presence of valid - // escape characters in the entire f-string. Therefore, - // we must analyze escape characters in each f-string - // element before pushing a diagnostic and fix. - for element in &f_string.elements { - match element { - FStringElement::Literal(literal) => { - escape_chars_state.update(analyze_escape_chars( - locator, - literal.range(), - flags, - )); - } - FStringElement::Expression(expression) => { - let Some(format_spec) = expression.format_spec.as_ref() else { - continue; - }; - for literal in format_spec.elements.literals() { - escape_chars_state.update(analyze_escape_chars( - locator, - literal.range(), - flags, - )); - } - } - } - } - escape_chars_state - } + StringLikePart::FString(f_string) => analyze_escape_chars_in_interpolated_string( + AnyStringFlags::from(f_string.flags), + &f_string.elements, + locator, + ), + StringLikePart::TString(t_string) => analyze_escape_chars_in_interpolated_string( + AnyStringFlags::from(t_string.flags), + &t_string.elements, + locator, + ), }; check(checker, locator, part.start(), part.flags(), state); } @@ -146,7 +126,7 @@ fn analyze_escape_chars( let next_char = match source[i + 1..].chars().next() { Some(next_char) => next_char, - None if flags.is_f_string() => { + None if flags.is_interpolated_string() => { // If we're at the end of a f-string middle token, the next character // is actually emitted as a different token. For example, // @@ -230,6 +210,39 @@ fn analyze_escape_chars( } } +fn analyze_escape_chars_in_interpolated_string( + flags: AnyStringFlags, + elements: &InterpolatedStringElements, + locator: &Locator, +) -> EscapeCharsState { + let mut escape_chars_state = EscapeCharsState::default(); + // Whether we suggest converting to a raw string or + // adding backslashes depends on the presence of valid + // escape characters in the entire f/t-string. Therefore, + // we must analyze escape characters in each f/t-string + // element before pushing a diagnostic and fix. + for element in elements { + match element { + InterpolatedStringElement::Literal(literal) => { + escape_chars_state.update(analyze_escape_chars(locator, literal.range(), flags)); + } + InterpolatedStringElement::Interpolation(interpolation) => { + let Some(format_spec) = interpolation.format_spec.as_ref() else { + continue; + }; + for literal in format_spec.elements.literals() { + escape_chars_state.update(analyze_escape_chars( + locator, + literal.range(), + flags, + )); + } + } + } + } + escape_chars_state +} + /// Pushes a diagnostic and fix depending on escape characters seen so far. /// /// If we have not seen any valid escape characters, we convert to @@ -252,7 +265,7 @@ fn check( if contains_valid_escape_sequence { // Escape with backslash. for invalid_escape_char in &invalid_escape_chars { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( InvalidEscapeSequence { ch: invalid_escape_char.ch, fix_title: FixTitle::AddBackslash, @@ -263,12 +276,11 @@ fn check( r"\".to_string(), invalid_escape_char.start() + TextSize::from(1), ))); - checker.report_diagnostic(diagnostic); } } else { // Turn into raw string. for invalid_escape_char in &invalid_escape_chars { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( InvalidEscapeSequence { ch: invalid_escape_char.ch, fix_title: FixTitle::UseRawStringLiteral, @@ -295,8 +307,6 @@ fn check( )), ); } - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index 3bf860dd5a8d9..c8cbc21ce6aa7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{ self as ast, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter, @@ -11,6 +10,7 @@ use ruff_source_file::UniversalNewlines; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for lambda expressions which are assigned to a variable. @@ -70,7 +70,16 @@ pub(crate) fn lambda_assignment( return; }; - let mut diagnostic = Diagnostic::new( + // If the assignment is a class attribute (with an annotation), ignore it. + // + // This is most common for, e.g., dataclasses and Pydantic models. Those libraries will + // treat the lambda as an assignable field, and the use of a lambda is almost certainly + // intentional. + if annotation.is_some() && checker.semantic().current_scope().kind.is_class() { + return; + } + + let mut diagnostic = checker.report_diagnostic( LambdaAssignment { name: id.to_string(), }, @@ -83,7 +92,7 @@ pub(crate) fn lambda_assignment( let first_line = checker.locator().line_str(stmt.start()); let indentation = leading_indentation(first_line); let mut indented = String::new(); - for (idx, line) in function(id, lambda, annotation, checker) + for (idx, line) in function(id, lambda, annotation, stmt, checker) .universal_newlines() .enumerate() { @@ -96,15 +105,6 @@ pub(crate) fn lambda_assignment( } } - // If the assignment is a class attribute (with an annotation), ignore it. - // - // This is most common for, e.g., dataclasses and Pydantic models. Those libraries will - // treat the lambda as an assignable field, and the use of a lambda is almost certainly - // intentional. - if annotation.is_some() && checker.semantic().current_scope().kind.is_class() { - return; - } - // Otherwise, if the assignment is in a class body, flag it, but use a display-only fix. // Rewriting safely would require making this a static method. // @@ -129,8 +129,6 @@ pub(crate) fn lambda_assignment( ))); } } - - checker.report_diagnostic(diagnostic); } /// Extract the argument types and return type from a `Callable` annotation. @@ -179,6 +177,7 @@ fn function( name: &str, lambda: &ExprLambda, annotation: Option<&Expr>, + stmt: &Stmt, checker: &Checker, ) -> String { // Use a dummy body. It gets replaced at the end with the actual body. @@ -188,6 +187,7 @@ fn function( ExprEllipsisLiteral::default(), ))), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let parameters = lambda.parameters.as_deref().cloned().unwrap_or_default(); if let Some(annotation) = annotation { @@ -235,10 +235,11 @@ fn function( returns: Some(Box::new(return_type)), type_params: None, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let generated = checker.generator().stmt(&func); - return replace_trailing_ellipsis_with_original_expr(generated, lambda, checker); + return replace_trailing_ellipsis_with_original_expr(generated, lambda, stmt, checker); } } let function = Stmt::FunctionDef(ast::StmtFunctionDef { @@ -250,15 +251,17 @@ fn function( returns: None, type_params: None, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let generated = checker.generator().stmt(&function); - replace_trailing_ellipsis_with_original_expr(generated, lambda, checker) + replace_trailing_ellipsis_with_original_expr(generated, lambda, stmt, checker) } fn replace_trailing_ellipsis_with_original_expr( mut generated: String, lambda: &ExprLambda, + stmt: &Stmt, checker: &Checker, ) -> String { let original_expr_range = parenthesized_range( @@ -269,14 +272,28 @@ fn replace_trailing_ellipsis_with_original_expr( ) .unwrap_or(lambda.body.range()); - let original_expr_in_source = checker.locator().slice(original_expr_range); + // This prevents the autofix of introducing a syntax error if the lambda's body is an + // expression spanned across multiple lines. To avoid the syntax error we preserve + // the parenthesis around the body. + let original_expr_in_source = if parenthesized_range( + lambda.into(), + stmt.into(), + checker.comment_ranges(), + checker.source(), + ) + .is_some() + { + format!("({})", checker.locator().slice(original_expr_range)) + } else { + checker.locator().slice(original_expr_range).to_string() + }; let placeholder_ellipsis_start = generated.rfind("...").unwrap(); let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len(); generated.replace_range( placeholder_ellipsis_start..placeholder_ellipsis_end, - original_expr_in_source, + &original_expr_in_source, ); generated } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs index 599c01a32c692..9a03e0f98a2e5 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs @@ -1,8 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_source_file::Line; +use crate::Violation; +use crate::checkers::ast::LintContext; use crate::rules::pycodestyle::overlong::Overlong; use crate::settings::LinterSettings; @@ -84,10 +85,11 @@ pub(crate) fn line_too_long( line: &Line, comment_ranges: &CommentRanges, settings: &LinterSettings, -) -> Option { + context: &LintContext, +) { let limit = settings.pycodestyle.max_line_length; - Overlong::try_from_line( + if let Some(overlong) = Overlong::try_from_line( line, comment_ranges, limit, @@ -97,11 +99,10 @@ pub(crate) fn line_too_long( &[] }, settings.tab_size, - ) - .map(|overlong| { - Diagnostic::new( + ) { + context.report_diagnostic( LineTooLong(overlong.width(), limit.value() as usize), overlong.range(), - ) - }) + ); + } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index 265d0c7046a6c..baf229ada8546 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -1,15 +1,15 @@ +use ruff_python_ast::parenthesize::parenthesized_range; use rustc_hash::FxHashMap; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers; -use ruff_python_ast::helpers::generate_comparison; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::helpers::{self, generate_comparison}; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Edit, Fix}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum EqCmpOp { @@ -136,22 +136,18 @@ impl AlwaysFixableViolation for TrueFalseComparison { let cond = cond.truncated_display(); match (value, op) { (true, EqCmpOp::Eq) => { - format!("Avoid equality comparisons to `True`; use `if {cond}:` for truth checks") + format!("Avoid equality comparisons to `True`; use `{cond}:` for truth checks") } (true, EqCmpOp::NotEq) => { format!( - "Avoid inequality comparisons to `True`; use `if not {cond}:` for false checks" + "Avoid inequality comparisons to `True`; use `not {cond}:` for false checks" ) } (false, EqCmpOp::Eq) => { - format!( - "Avoid equality comparisons to `False`; use `if not {cond}:` for false checks" - ) + format!("Avoid equality comparisons to `False`; use `not {cond}:` for false checks") } (false, EqCmpOp::NotEq) => { - format!( - "Avoid inequality comparisons to `False`; use `if {cond}:` for truth checks" - ) + format!("Avoid inequality comparisons to `False`; use `{cond}:` for truth checks") } } } @@ -170,6 +166,42 @@ impl AlwaysFixableViolation for TrueFalseComparison { } } +fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option { + let value = comparator.as_boolean_literal_expr()?.value; + match op { + CmpOp::Is | CmpOp::Eq => Some(value), + CmpOp::IsNot | CmpOp::NotEq => Some(!value), + _ => None, + } +} + +fn generate_redundant_comparison( + compare: &ast::ExprCompare, + comment_ranges: &ruff_python_trivia::CommentRanges, + source: &str, + comparator: &Expr, + kind: bool, + needs_wrap: bool, +) -> String { + let comparator_range = + parenthesized_range(comparator.into(), compare.into(), comment_ranges, source) + .unwrap_or(comparator.range()); + + let comparator_str = &source[comparator_range]; + + let result = if kind { + comparator_str.to_string() + } else { + format!("not {comparator_str}") + }; + + if needs_wrap { + format!("({result})") + } else { + result + } +} + /// E711, E712 pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) { // Mapping from (bad operator index) to (replacement operator). As we iterate @@ -177,7 +209,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) // then replace the entire expression at the end with one "real" fix, to // avoid conflicts. let mut bad_ops: FxHashMap = FxHashMap::default(); - let mut diagnostics: Vec = vec![]; + let mut diagnostics = vec![]; // Check `left`. let mut comparator = compare.left.as_ref(); @@ -190,22 +222,24 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) if !helpers::is_constant_non_singleton(next) { if let Some(op) = EqCmpOp::try_from(*op) { - if checker.enabled(Rule::NoneComparison) && comparator.is_none_literal_expr() { + if checker.is_rule_enabled(Rule::NoneComparison) && comparator.is_none_literal_expr() { match op { EqCmpOp::Eq => { - let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); + let diagnostic = + checker.report_diagnostic(NoneComparison(op), comparator.range()); bad_ops.insert(0, CmpOp::Is); diagnostics.push(diagnostic); } EqCmpOp::NotEq => { - let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); + let diagnostic = + checker.report_diagnostic(NoneComparison(op), comparator.range()); bad_ops.insert(0, CmpOp::IsNot); diagnostics.push(diagnostic); } } } - if checker.enabled(Rule::TrueFalseComparison) { + if checker.is_rule_enabled(Rule::TrueFalseComparison) { if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) = comparator { match op { EqCmpOp::Eq => { @@ -214,7 +248,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) } else { None }; - let diagnostic = Diagnostic::new( + let diagnostic = checker.report_diagnostic( TrueFalseComparison { value: *value, op, @@ -231,7 +265,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) } else { None }; - let diagnostic = Diagnostic::new( + let diagnostic = checker.report_diagnostic( TrueFalseComparison { value: *value, op, @@ -256,25 +290,37 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) } if let Some(op) = EqCmpOp::try_from(*op) { - if checker.enabled(Rule::NoneComparison) && next.is_none_literal_expr() { + if checker.is_rule_enabled(Rule::NoneComparison) && next.is_none_literal_expr() { match op { EqCmpOp::Eq => { - let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); + let diagnostic = + checker.report_diagnostic(NoneComparison(op), next.range()); bad_ops.insert(index, CmpOp::Is); diagnostics.push(diagnostic); } EqCmpOp::NotEq => { - let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); + let diagnostic = + checker.report_diagnostic(NoneComparison(op), next.range()); bad_ops.insert(index, CmpOp::IsNot); diagnostics.push(diagnostic); } } } - if checker.enabled(Rule::TrueFalseComparison) { + if checker.is_rule_enabled(Rule::TrueFalseComparison) { if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) = next { match op { EqCmpOp::Eq => { + if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: comparator_value, + .. + }) = comparator + { + if value == comparator_value { + continue; + } + } + let cond = if compare.ops.len() == 1 { Some(SourceCodeSnippet::from_str( checker.locator().slice(comparator), @@ -282,7 +328,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) } else { None }; - let diagnostic = Diagnostic::new( + let diagnostic = checker.report_diagnostic( TrueFalseComparison { value: *value, op, @@ -301,7 +347,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) } else { None }; - let diagnostic = Diagnostic::new( + let diagnostic = checker.report_diagnostic( TrueFalseComparison { value: *value, op, @@ -323,7 +369,6 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) // TODO(charlie): Respect `noqa` directives. If one of the operators has a // `noqa`, but another doesn't, both will be removed here. if !bad_ops.is_empty() { - // Replace the entire comparison expression. let ops = compare .ops .iter() @@ -331,14 +376,53 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) .map(|(idx, op)| bad_ops.get(&idx).unwrap_or(op)) .copied() .collect::>(); - let content = generate_comparison( - &compare.left, - &ops, - &compare.comparators, - compare.into(), - checker.comment_ranges(), - checker.source(), - ); + + let comment_ranges = checker.comment_ranges(); + let source = checker.source(); + + let content = match (&*compare.ops, &*compare.comparators) { + ([op], [comparator]) => { + if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) { + let needs_wrap = compare.left.range().start() != compare.range().start(); + generate_redundant_comparison( + compare, + comment_ranges, + source, + comparator, + kind, + needs_wrap, + ) + } else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) { + let needs_wrap = comparator.range().end() != compare.range().end(); + generate_redundant_comparison( + compare, + comment_ranges, + source, + &compare.left, + kind, + needs_wrap, + ) + } else { + generate_comparison( + &compare.left, + &ops, + &compare.comparators, + compare.into(), + comment_ranges, + source, + ) + } + } + _ => generate_comparison( + &compare.left, + &ops, + &compare.comparators, + compare.into(), + comment_ranges, + source, + ), + }; + for diagnostic in &mut diagnostics { diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( content.to_string(), @@ -346,6 +430,4 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) ))); } } - - checker.report_diagnostics(diagnostics); } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs index a40931bcf9b5a..2044af0ba5627 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs @@ -1,12 +1,11 @@ -use ruff_diagnostics::AlwaysFixableViolation; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Edit; -use ruff_diagnostics::Fix; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::AlwaysFixableViolation; +use crate::Edit; +use crate::Fix; +use crate::checkers::ast::LintContext; use super::{LogicalLine, Whitespace}; @@ -126,7 +125,7 @@ impl AlwaysFixableViolation for WhitespaceBeforePunctuation { } /// E201, E202, E203 -pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLinesContext) { +pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &LintContext) { let mut fstrings = 0u32; let mut brackets = vec![]; let mut prev_token = None; @@ -165,13 +164,13 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin BracketOrPunctuation::OpenBracket(symbol) if symbol != '{' || fstrings == 0 => { let (trailing, trailing_len) = line.trailing_whitespace(token); if !matches!(trailing, Whitespace::None) { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( WhitespaceAfterOpenBracket { symbol }, TextRange::at(token.end(), trailing_len), - ); - diagnostic - .set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range()))); - context.push_diagnostic(diagnostic); + ) { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); + } } } BracketOrPunctuation::CloseBracket(symbol) if symbol != '}' || fstrings == 0 => { @@ -179,13 +178,13 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) = line.leading_whitespace(token) { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( WhitespaceBeforeCloseBracket { symbol }, TextRange::at(token.start() - offset, offset), - ); - diagnostic - .set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range()))); - context.push_diagnostic(diagnostic); + ) { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); + } } } } @@ -205,14 +204,16 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin // If we're in the second half of a double colon, disallow // any whitespace (e.g., `foo[1: :2]` or `foo[1 : : 2]`). if matches!(prev_token, Some(TokenKind::Colon)) { - let mut diagnostic = Diagnostic::new( - WhitespaceBeforePunctuation { symbol }, - TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion( - diagnostic.range(), - ))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = context + .report_diagnostic_if_enabled( + WhitespaceBeforePunctuation { symbol }, + TextRange::at(token.start() - offset, offset), + ) + { + let range = diagnostic.expect_range(); + diagnostic + .set_fix(Fix::safe_edit(Edit::range_deletion(range))); + } } else if iter.peek().is_some_and(|token| { matches!(token.kind(), TokenKind::Rsqb | TokenKind::Comma) }) { @@ -220,14 +221,17 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin // Or `foo[index :, 2]`, but not `foo[index :, 2]`. if let (Whitespace::Many | Whitespace::Tab, offset) = whitespace { - let mut diagnostic = Diagnostic::new( - WhitespaceBeforePunctuation { symbol }, - TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion( - diagnostic.range(), - ))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = context + .report_diagnostic_if_enabled( + WhitespaceBeforePunctuation { symbol }, + TextRange::at(token.start() - offset, offset), + ) + { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edit( + Edit::range_deletion(range), + )); + } } } else if iter.peek().is_some_and(|token| { matches!( @@ -245,15 +249,21 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin // whitespace before the colon and so should the fix if let (Whitespace::Many | Whitespace::Tab, offset) = whitespace { - let mut diagnostic = Diagnostic::new( - WhitespaceBeforePunctuation { symbol }, - TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edits( - Edit::range_deletion(diagnostic.range()), - [Edit::insertion(" ".into(), token.start() - offset)], - )); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = context + .report_diagnostic_if_enabled( + WhitespaceBeforePunctuation { symbol }, + TextRange::at(token.start() - offset, offset), + ) + { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edits( + Edit::range_deletion(range), + [Edit::insertion( + " ".into(), + token.start() - offset, + )], + )); + } } } else { // Allow, e.g., `foo[1:2]` or `foo[1 : 2]` or `foo[1 :: 2]`. @@ -262,14 +272,17 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin .filter(|next| matches!(next.kind(), TokenKind::Colon)) .unwrap_or(&token); if line.trailing_whitespace(token) != whitespace { - let mut diagnostic = Diagnostic::new( - WhitespaceBeforePunctuation { symbol }, - TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion( - diagnostic.range(), - ))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = context + .report_diagnostic_if_enabled( + WhitespaceBeforePunctuation { symbol }, + TextRange::at(token.start() - offset, offset), + ) + { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edit( + Edit::range_deletion(range), + )); + } } } } else { @@ -280,14 +293,13 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin // Avoid removing any whitespace for f-string debug expressions. continue; } - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( WhitespaceBeforePunctuation { symbol }, TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion( - diagnostic.range(), - ))); - context.push_diagnostic(diagnostic); + ) { + let range = diagnostic.expect_range(); + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); + } } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs index c5f89257842d2..b6a6bbe434a53 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/indentation.rs @@ -1,7 +1,9 @@ -use ruff_diagnostics::DiagnosticKind; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; +use ruff_text_size::TextRange; + +use crate::Violation; +use crate::checkers::ast::LintContext; use super::LogicalLine; @@ -58,12 +60,14 @@ impl Violation for IndentationWithInvalidMultiple { /// ```python /// if True: /// # a = 1 +/// ... /// ``` /// /// Use instead: /// ```python /// if True: /// # a = 1 +/// ... /// ``` /// /// ## Formatter compatibility @@ -254,6 +258,7 @@ impl Violation for OverIndented { } /// E111, E112, E113, E114, E115, E116, E117 +#[expect(clippy::too_many_arguments)] pub(crate) fn indentation( logical_line: &LogicalLine, prev_logical_line: Option<&LogicalLine>, @@ -261,51 +266,55 @@ pub(crate) fn indentation( indent_level: usize, prev_indent_level: Option, indent_size: usize, -) -> Vec { - let mut diagnostics = vec![]; - + range: TextRange, + context: &LintContext, +) { if indent_level % indent_size != 0 { - diagnostics.push(if logical_line.is_comment_only() { - DiagnosticKind::from(IndentationWithInvalidMultipleComment { - indent_width: indent_size, - }) + if logical_line.is_comment_only() { + context.report_diagnostic_if_enabled( + IndentationWithInvalidMultipleComment { + indent_width: indent_size, + }, + range, + ); } else { - DiagnosticKind::from(IndentationWithInvalidMultiple { - indent_width: indent_size, - }) - }); + context.report_diagnostic_if_enabled( + IndentationWithInvalidMultiple { + indent_width: indent_size, + }, + range, + ); + } } let indent_expect = prev_logical_line .and_then(|prev_logical_line| prev_logical_line.tokens_trimmed().last()) .is_some_and(|t| t.kind() == TokenKind::Colon); if indent_expect && indent_level <= prev_indent_level.unwrap_or(0) { - diagnostics.push(if logical_line.is_comment_only() { - DiagnosticKind::from(NoIndentedBlockComment) + if logical_line.is_comment_only() { + context.report_diagnostic_if_enabled(NoIndentedBlockComment, range); } else { - DiagnosticKind::from(NoIndentedBlock) - }); + context.report_diagnostic_if_enabled(NoIndentedBlock, range); + } } else if !indent_expect && prev_indent_level.is_some_and(|prev_indent_level| indent_level > prev_indent_level) { - diagnostics.push(if logical_line.is_comment_only() { - DiagnosticKind::from(UnexpectedIndentationComment) + if logical_line.is_comment_only() { + context.report_diagnostic_if_enabled(UnexpectedIndentationComment, range); } else { - DiagnosticKind::from(UnexpectedIndentation) - }); + context.report_diagnostic_if_enabled(UnexpectedIndentation, range); + } } if indent_expect { let expected_indent_amount = if indent_char == '\t' { 8 } else { 4 }; let expected_indent_level = prev_indent_level.unwrap_or(0) + expected_indent_amount; if indent_level > expected_indent_level { - diagnostics.push( + context.report_diagnostic_if_enabled( OverIndented { is_comment: logical_line.is_comment_only(), - } - .into(), + }, + range, ); } } - - diagnostics } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs index 547ab4859c785..19d5d7ca01e25 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::Edit; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_text_size::Ranged; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::Edit; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Fix}; use super::{DefinitionState, LogicalLine}; @@ -40,7 +40,7 @@ impl AlwaysFixableViolation for MissingWhitespace { } /// E231 -pub(crate) fn missing_whitespace(line: &LogicalLine, context: &mut LogicalLinesContext) { +pub(crate) fn missing_whitespace(line: &LogicalLine, context: &LintContext) { let mut fstrings = 0u32; let mut definition_state = DefinitionState::from_tokens(line.tokens()); let mut brackets = Vec::new(); @@ -103,10 +103,15 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, context: &mut LogicalLinesC } } - let diagnostic = - Diagnostic::new(MissingWhitespace { token: kind }, token.range()); - let fix = Fix::safe_edit(Edit::insertion(" ".to_string(), token.end())); - context.push_diagnostic(diagnostic.with_fix(fix)); + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + MissingWhitespace { token: kind }, + token.range(), + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + " ".to_string(), + token.end(), + ))); + } } } _ => {} diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs index dfa9a4ece7caa..f0d4600038044 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_after_keyword.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_text_size::Ranged; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::checkers::ast::LintContext; use crate::rules::pycodestyle::rules::logical_lines::LogicalLine; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for missing whitespace after keywords. @@ -41,10 +41,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAfterKeyword { } /// E275 -pub(crate) fn missing_whitespace_after_keyword( - line: &LogicalLine, - context: &mut LogicalLinesContext, -) { +pub(crate) fn missing_whitespace_after_keyword(line: &LogicalLine, context: &LintContext) { for window in line.tokens().windows(2) { let tok0 = &window[0]; let tok1 = &window[1]; @@ -71,9 +68,11 @@ pub(crate) fn missing_whitespace_after_keyword( )) && tok0.end() == tok1.start() { - let mut diagnostic = Diagnostic::new(MissingWhitespaceAfterKeyword, tok0.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion(" ".to_string(), tok0.end()))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(MissingWhitespaceAfterKeyword, tok0.range()) + { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion(" ".to_string(), tok0.end()))); + } } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index 03b8e16c4dab7..2b7fdadb106d4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -1,18 +1,19 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, DiagnosticKind, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::checkers::ast::LintContext; use crate::rules::pycodestyle::helpers::is_non_logical_token; use crate::rules::pycodestyle::rules::logical_lines::{DefinitionState, LogicalLine}; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for missing whitespace around all operators. /// /// ## Why is this bad? /// According to [PEP 8], there should be one space before and after all -/// operators. +/// assignment (`=`), augmented assignment (`+=`, `-=`, etc.), comparison, +/// and Booleans operators. /// /// ## Example /// ```python @@ -46,8 +47,14 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundOperator { /// Checks for missing whitespace arithmetic operators. /// /// ## Why is this bad? -/// According to [PEP 8], there should be one space before and after an -/// arithmetic operator (+, -, /, and *). +/// [PEP 8] recommends never using more than one space, and always having the +/// same amount of whitespace on both sides of a binary operator. +/// +/// For consistency, this rule enforces one space before and after an +/// arithmetic operator (`+`, `-`, `/`, and `*`). +/// +/// (Note that [PEP 8] suggests only adding whitespace around the operator with +/// the lowest precedence, but that authors should "use [their] own judgment".) /// /// ## Example /// ```python @@ -59,7 +66,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundOperator { /// number = 40 + 2 /// ``` /// -/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations // E226 #[derive(ViolationMetadata)] pub(crate) struct MissingWhitespaceAroundArithmeticOperator; @@ -79,8 +86,14 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundArithmeticOperator { /// Checks for missing whitespace around bitwise and shift operators. /// /// ## Why is this bad? -/// According to [PEP 8], there should be one space before and after bitwise and -/// shift operators (<<, >>, &, |, ^). +/// [PEP 8] recommends never using more than one space, and always having the +/// same amount of whitespace on both sides of a binary operator. +/// +/// For consistency, this rule enforces one space before and after bitwise and +/// shift operators (`<<`, `>>`, `&`, `|`, `^`). +/// +/// (Note that [PEP 8] suggests only adding whitespace around the operator with +/// the lowest precedence, but that authors should "use [their] own judgment".) /// /// ## Example /// ```python @@ -92,7 +105,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundArithmeticOperator { /// x = 128 << 1 /// ``` /// -/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations // E227 #[derive(ViolationMetadata)] pub(crate) struct MissingWhitespaceAroundBitwiseOrShiftOperator; @@ -112,8 +125,14 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundBitwiseOrShiftOperator { /// Checks for missing whitespace around the modulo operator. /// /// ## Why is this bad? -/// According to [PEP 8], the modulo operator (%) should have whitespace on -/// either side of it. +/// [PEP 8] recommends never using more than one space, and always having the +/// same amount of whitespace on both sides of a binary operator. +/// +/// For consistency, this rule enforces one space before and after a modulo +/// operator (`%`). +/// +/// (Note that [PEP 8] suggests only adding whitespace around the operator with +/// the lowest precedence, but that authors should "use [their] own judgment".) /// /// ## Example /// ```python @@ -142,10 +161,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundModuloOperator { } /// E225, E226, E227, E228 -pub(crate) fn missing_whitespace_around_operator( - line: &LogicalLine, - context: &mut LogicalLinesContext, -) { +pub(crate) fn missing_whitespace_around_operator(line: &LogicalLine, context: &LintContext) { let mut definition_state = DefinitionState::from_tokens(line.tokens()); let mut tokens = line.tokens().iter().peekable(); let first_token = tokens @@ -252,34 +268,37 @@ pub(crate) fn missing_whitespace_around_operator( match (has_leading_trivia, has_trailing_trivia) { // Operator with trailing but no leading space, enforce consistent spacing. (false, true) => { - let mut diagnostic = - Diagnostic::new(diagnostic_kind_for_operator(kind), token.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - " ".to_string(), - token.start(), - ))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = + diagnostic_kind_for_operator(kind, token.range(), context) + { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + " ".to_string(), + token.start(), + ))); + } } // Operator with leading but no trailing space, enforce consistent spacing. (true, false) => { - let mut diagnostic = - Diagnostic::new(diagnostic_kind_for_operator(kind), token.range()); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - " ".to_string(), - token.end(), - ))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = + diagnostic_kind_for_operator(kind, token.range(), context) + { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + " ".to_string(), + token.end(), + ))); + } } // Operator with no space, require spaces if it is required by the operator. (false, false) => { if needs_space == NeedsSpace::Yes { - let mut diagnostic = - Diagnostic::new(diagnostic_kind_for_operator(kind), token.range()); - diagnostic.set_fix(Fix::safe_edits( - Edit::insertion(" ".to_string(), token.start()), - [Edit::insertion(" ".to_string(), token.end())], - )); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = + diagnostic_kind_for_operator(kind, token.range(), context) + { + diagnostic.set_fix(Fix::safe_edits( + Edit::insertion(" ".to_string(), token.start()), + [Edit::insertion(" ".to_string(), token.end())], + )); + } } } (true, true) => { @@ -317,15 +336,19 @@ impl From for NeedsSpace { } } -fn diagnostic_kind_for_operator(operator: TokenKind) -> DiagnosticKind { +fn diagnostic_kind_for_operator<'a>( + operator: TokenKind, + range: TextRange, + context: &'a LintContext<'a>, +) -> Option> { if operator == TokenKind::Percent { - DiagnosticKind::from(MissingWhitespaceAroundModuloOperator) + context.report_diagnostic_if_enabled(MissingWhitespaceAroundModuloOperator, range) } else if operator.is_bitwise_or_shift() { - DiagnosticKind::from(MissingWhitespaceAroundBitwiseOrShiftOperator) + context.report_diagnostic_if_enabled(MissingWhitespaceAroundBitwiseOrShiftOperator, range) } else if operator.is_arithmetic() { - DiagnosticKind::from(MissingWhitespaceAroundArithmeticOperator) + context.report_diagnostic_if_enabled(MissingWhitespaceAroundArithmeticOperator, range) } else { - DiagnosticKind::from(MissingWhitespaceAroundOperator) + context.report_diagnostic_if_enabled(MissingWhitespaceAroundOperator, range) } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs index 167c3456df6aa..9f5c033b9f0c3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -18,8 +18,8 @@ pub(crate) use whitespace_around_named_parameter_equals::*; pub(crate) use whitespace_before_comment::*; pub(crate) use whitespace_before_parameters::*; -use crate::rules::pycodestyle::helpers::is_non_logical_token; use crate::Locator; +use crate::rules::pycodestyle::helpers::is_non_logical_token; mod extraneous_whitespace; mod indentation; @@ -392,7 +392,6 @@ impl LogicalLinesBuilder { } // SAFETY: `LogicalLines::from_tokens` asserts that the file has less than `u32::MAX` tokens and each tokens is at least one character long - #[allow(clippy::cast_possible_truncation)] fn push_token(&mut self, kind: TokenKind, range: TextRange) { let line = &mut self.current_line; @@ -428,7 +427,7 @@ impl LogicalLinesBuilder { } // SAFETY: `LogicalLines::from_tokens` asserts that the file has less than `u32::MAX` tokens and each tokens is at least one character long - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn finish_line(&mut self) { let end = self.tokens.len() as u32; if self.current_line.tokens_start < end { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs index a6c348b780244..b8f8439bec8a6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/redundant_backslash.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_index::Indexer; use ruff_python_parser::TokenKind; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::checkers::logical_lines::LogicalLinesContext; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; use super::LogicalLine; @@ -48,7 +48,7 @@ pub(crate) fn redundant_backslash( line: &LogicalLine, locator: &Locator, indexer: &Indexer, - context: &mut LogicalLinesContext, + context: &LintContext, ) { let mut parens = 0; let continuation_lines = indexer.continuation_line_starts(); @@ -75,15 +75,15 @@ pub(crate) fn redundant_backslash( for continuation_line in &continuation_lines[start_index..end_index] { let backslash_end = locator.line_end(*continuation_line); let backslash_start = backslash_end - TextSize::new(1); - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( RedundantBackslash, TextRange::new(backslash_start, backslash_end), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion( - backslash_start, - backslash_end, - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::deletion( + backslash_start, + backslash_end, + ))); + } } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index 7069ede17a3fc..7a73fc7795ef6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; use super::{LogicalLine, Whitespace}; @@ -196,7 +196,7 @@ impl AlwaysFixableViolation for MultipleSpacesAfterComma { } /// E221, E222, E223, E224 -pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLinesContext) { +pub(crate) fn space_around_operator(line: &LogicalLine, context: &LintContext) { let mut after_operator = false; for token in line.tokens() { @@ -206,26 +206,26 @@ pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLin if !after_operator { match line.leading_whitespace(token) { (Whitespace::Tab, offset) => { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( TabBeforeOperator, TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::at(token.start() - offset, offset), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.start() - offset, offset), + ))); + } } (Whitespace::Many, offset) => { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( MultipleSpacesBeforeOperator, TextRange::at(token.start() - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::at(token.start() - offset, offset), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.start() - offset, offset), + ))); + } } _ => {} } @@ -233,24 +233,26 @@ pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLin match line.trailing_whitespace(token) { (Whitespace::Tab, len) => { - let mut diagnostic = - Diagnostic::new(TabAfterOperator, TextRange::at(token.end(), len)); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + TabAfterOperator, TextRange::at(token.end(), len), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.end(), len), + ))); + } } (Whitespace::Many, len) => { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( MultipleSpacesAfterOperator, TextRange::at(token.end(), len), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::at(token.end(), len), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.end(), len), + ))); + } } _ => {} } @@ -261,27 +263,31 @@ pub(crate) fn space_around_operator(line: &LogicalLine, context: &mut LogicalLin } /// E241, E242 -pub(crate) fn space_after_comma(line: &LogicalLine, context: &mut LogicalLinesContext) { +pub(crate) fn space_after_comma(line: &LogicalLine, context: &LintContext) { for token in line.tokens() { if matches!(token.kind(), TokenKind::Comma) { match line.trailing_whitespace(token) { (Whitespace::Tab, len) => { - let mut diagnostic = - Diagnostic::new(TabAfterComma, TextRange::at(token.end(), len)); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + TabAfterComma, TextRange::at(token.end(), len), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.end(), len), + ))); + } } (Whitespace::Many, len) => { - let mut diagnostic = - Diagnostic::new(MultipleSpacesAfterComma, TextRange::at(token.end(), len)); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + MultipleSpacesAfterComma, TextRange::at(token.end(), len), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.end(), len), + ))); + } } _ => {} } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs index aa9b7eaf52b04..af7a415275fe0 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; use super::{LogicalLine, Whitespace}; @@ -43,12 +43,12 @@ impl AlwaysFixableViolation for MultipleSpacesAfterKeyword { /// /// ## Example /// ```python -/// True and False +/// x and y /// ``` /// /// Use instead: /// ```python -/// True and False +/// x and y /// ``` #[derive(ViolationMetadata)] pub(crate) struct MultipleSpacesBeforeKeyword; @@ -123,7 +123,7 @@ impl AlwaysFixableViolation for TabBeforeKeyword { } /// E271, E272, E273, E274 -pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut LogicalLinesContext) { +pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &LintContext) { let mut after_keyword = false; for token in line.tokens() { @@ -133,27 +133,27 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic match line.leading_whitespace(token) { (Whitespace::Tab, offset) => { let start = token.start(); - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( TabBeforeKeyword, TextRange::at(start - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::at(start - offset, offset), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(start - offset, offset), + ))); + } } (Whitespace::Many, offset) => { let start = token.start(); - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( MultipleSpacesBeforeKeyword, TextRange::at(start - offset, offset), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::at(start - offset, offset), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(start - offset, offset), + ))); + } } _ => {} } @@ -161,24 +161,26 @@ pub(crate) fn whitespace_around_keywords(line: &LogicalLine, context: &mut Logic match line.trailing_whitespace(token) { (Whitespace::Tab, len) => { - let mut diagnostic = - Diagnostic::new(TabAfterKeyword, TextRange::at(token.end(), len)); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + TabAfterKeyword, TextRange::at(token.end(), len), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.end(), len), + ))); + } } (Whitespace::Many, len) => { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( MultipleSpacesAfterKeyword, TextRange::at(token.end(), len), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::at(token.end(), len), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::at(token.end(), len), + ))); + } } _ => {} } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs index b4d6d4bacfc34..3e11ca5a8c531 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_around_named_parameter_equals.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::checkers::ast::LintContext; use crate::rules::pycodestyle::rules::logical_lines::{DefinitionState, LogicalLine}; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for missing whitespace around the equals sign in an unannotated @@ -85,10 +85,7 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundParameterEquals { } /// E251, E252 -pub(crate) fn whitespace_around_named_parameter_equals( - line: &LogicalLine, - context: &mut LogicalLinesContext, -) { +pub(crate) fn whitespace_around_named_parameter_equals(line: &LogicalLine, context: &LintContext) { let mut parens = 0u32; let mut fstrings = 0u32; let mut annotated_func_arg = false; @@ -125,13 +122,15 @@ pub(crate) fn whitespace_around_named_parameter_equals( if definition_state.in_type_params() || (annotated_func_arg && parens == 1) { let start = token.start(); if start == prev_end && prev_end != TextSize::new(0) { - let mut diagnostic = - Diagnostic::new(MissingWhitespaceAroundParameterEquals, token.range); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - " ".to_string(), - token.start(), - ))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + MissingWhitespaceAroundParameterEquals, + token.range, + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + " ".to_string(), + token.start(), + ))); + } } while let Some(next) = iter.peek() { @@ -141,15 +140,15 @@ pub(crate) fn whitespace_around_named_parameter_equals( let next_start = next.start(); if next_start == token.end() { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( MissingWhitespaceAroundParameterEquals, token.range, - ); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( - " ".to_string(), - token.end(), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::insertion( + " ".to_string(), + token.end(), + ))); + } } break; } @@ -157,12 +156,13 @@ pub(crate) fn whitespace_around_named_parameter_equals( } else { // If there's space between the preceding token and the equals sign, report it. if token.start() != prev_end { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( UnexpectedSpacesAroundKeywordParameterEquals, TextRange::new(prev_end, token.start()), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion(prev_end, token.start()))); - context.push_diagnostic(diagnostic); + ) { + diagnostic + .set_fix(Fix::safe_edit(Edit::deletion(prev_end, token.start()))); + } } // If there's space between the equals sign and the following token, report it. @@ -171,15 +171,15 @@ pub(crate) fn whitespace_around_named_parameter_equals( iter.next(); } else { if next.start() != token.end() { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( UnexpectedSpacesAroundKeywordParameterEquals, TextRange::new(token.end(), next.start()), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion( - token.end(), - next.start(), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::deletion( + token.end(), + next.start(), + ))); + } } break; } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs index 90cc6bb182c23..49f85613b2000 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_python_trivia::PythonWhitespace; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::checkers::logical_lines::LogicalLinesContext; -use crate::rules::pycodestyle::rules::logical_lines::LogicalLine; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::rules::pycodestyle::rules::logical_lines::LogicalLine; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks if inline comments are separated by at least two spaces. @@ -167,7 +167,7 @@ impl AlwaysFixableViolation for MultipleLeadingHashesForBlockComment { pub(crate) fn whitespace_before_comment( line: &LogicalLine, locator: &Locator, - context: &mut LogicalLinesContext, + context: &LintContext, ) { let mut prev_end = TextSize::default(); for token in line.tokens() { @@ -185,15 +185,15 @@ pub(crate) fn whitespace_before_comment( let is_inline_comment = !line_text.trim_whitespace().is_empty(); if is_inline_comment { if range.start() - prev_end < " ".text_len() { - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( TooFewSpacesBeforeInlineComment, TextRange::new(prev_end, range.start()), - ); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - " ".to_string(), - TextRange::new(prev_end, range.start()), - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + " ".to_string(), + TextRange::new(prev_end, range.start()), + ))); + } } } @@ -210,30 +210,36 @@ pub(crate) fn whitespace_before_comment( if is_inline_comment { if bad_prefix.is_some() || comment.chars().next().is_some_and(char::is_whitespace) { - let mut diagnostic = Diagnostic::new(NoSpaceAfterInlineComment, range); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - format_leading_space(token_text), - range, - ))); - context.push_diagnostic(diagnostic); - } - } else if let Some(bad_prefix) = bad_prefix { - if bad_prefix != '!' || !line.is_start_of_file() { - if bad_prefix != '#' { - let mut diagnostic = Diagnostic::new(NoSpaceAfterBlockComment, range); + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(NoSpaceAfterInlineComment, range) + { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( format_leading_space(token_text), range, ))); - context.push_diagnostic(diagnostic); + } + } + } else if let Some(bad_prefix) = bad_prefix { + if bad_prefix != '!' || !line.is_start_of_file() { + if bad_prefix != '#' { + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(NoSpaceAfterBlockComment, range) + { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + format_leading_space(token_text), + range, + ))); + } } else if !comment.is_empty() { - let mut diagnostic = - Diagnostic::new(MultipleLeadingHashesForBlockComment, range); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - format_leading_hashes(token_text), + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + MultipleLeadingHashesForBlockComment, range, - ))); - context.push_diagnostic(diagnostic); + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + format_leading_hashes(token_text), + range, + ))); + } } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs index 227a20c67ca0f..4f180f2632192 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::checkers::logical_lines::LogicalLinesContext; +use crate::checkers::ast::LintContext; use crate::rules::pycodestyle::rules::logical_lines::LogicalLine; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for extraneous whitespace immediately preceding an open parenthesis @@ -54,7 +54,7 @@ impl AlwaysFixableViolation for WhitespaceBeforeParameters { } /// E211 -pub(crate) fn whitespace_before_parameters(line: &LogicalLine, context: &mut LogicalLinesContext) { +pub(crate) fn whitespace_before_parameters(line: &LogicalLine, context: &LintContext) { let previous = line.tokens().first().unwrap(); let mut pre_pre_kind: Option = None; @@ -76,9 +76,11 @@ pub(crate) fn whitespace_before_parameters(line: &LogicalLine, context: &mut Log let end = token.end() - TextSize::from(1); let kind: WhitespaceBeforeParameters = WhitespaceBeforeParameters { bracket: kind }; - let mut diagnostic = Diagnostic::new(kind, TextRange::new(start, end)); - diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start, end))); - context.push_diagnostic(diagnostic); + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(kind, TextRange::new(start, end)) + { + diagnostic.set_fix(Fix::safe_edit(Edit::deletion(start, end))); + } } pre_pre_kind = Some(prev_token); prev_token = kind; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs index e5cd7c49eaae4..f8301cbc94a6b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_codegen::Stylist; use ruff_text_size::{TextLen, TextRange}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for files missing a new line at the end of the file. @@ -40,24 +41,22 @@ impl AlwaysFixableViolation for MissingNewlineAtEndOfFile { pub(crate) fn no_newline_at_end_of_file( locator: &Locator, stylist: &Stylist, -) -> Option { + context: &LintContext, +) { let source = locator.contents(); // Ignore empty and BOM only files. if source.is_empty() || source == "\u{feff}" { - return None; + return; } if !source.ends_with(['\n', '\r']) { let range = TextRange::empty(locator.contents().text_len()); - let mut diagnostic = Diagnostic::new(MissingNewlineAtEndOfFile, range); + let mut diagnostic = context.report_diagnostic(MissingNewlineAtEndOfFile, range); diagnostic.set_fix(Fix::safe_edit(Edit::insertion( stylist.line_ending().to_string(), range.start(), ))); - return Some(diagnostic); } - - None } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs index fb50e2d12a188..649fc972fcd93 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/mixed_spaces_and_tabs.rs @@ -1,10 +1,11 @@ use ruff_text_size::{TextLen, TextRange}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::leading_indentation; use ruff_source_file::Line; +use crate::{Violation, checkers::ast::LintContext}; + /// ## What it does /// Checks for mixed tabs and spaces in indentation. /// @@ -36,15 +37,13 @@ impl Violation for MixedSpacesAndTabs { } /// E101 -pub(crate) fn mixed_spaces_and_tabs(line: &Line) -> Option { +pub(crate) fn mixed_spaces_and_tabs(line: &Line, context: &LintContext) { let indent = leading_indentation(line.as_str()); if indent.contains(' ') && indent.contains('\t') { - Some(Diagnostic::new( + context.report_diagnostic( MixedSpacesAndTabs, TextRange::at(line.start(), indent.text_len()), - )) - } else { - None + ); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs index d8b752cef356c..066de7316fbad 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/module_import_not_at_top_of_file.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{PySourceType, Stmt}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -58,11 +58,11 @@ impl Violation for ModuleImportNotAtTopOfFile { /// E402 pub(crate) fn module_import_not_at_top_of_file(checker: &Checker, stmt: &Stmt) { if checker.semantic().seen_import_boundary() && checker.semantic().at_top_level() { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( ModuleImportNotAtTopOfFile { source_type: checker.source_type, }, stmt.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs index deadfbbc0c974..dd7dc69b5b933 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/multiple_imports_on_one_line.rs @@ -1,7 +1,6 @@ use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Alias, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; @@ -9,8 +8,9 @@ use ruff_python_trivia::indentation_at_offset; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Check for multiple imports on one line. @@ -49,7 +49,7 @@ impl Violation for MultipleImportsOnOneLine { /// E401 pub(crate) fn multiple_imports_on_one_line(checker: &Checker, stmt: &Stmt, names: &[Alias]) { if names.len() > 1 { - let mut diagnostic = Diagnostic::new(MultipleImportsOnOneLine, stmt.range()); + let mut diagnostic = checker.report_diagnostic(MultipleImportsOnOneLine, stmt.range()); diagnostic.set_fix(split_imports( stmt, names, @@ -57,7 +57,6 @@ pub(crate) fn multiple_imports_on_one_line(checker: &Checker, stmt: &Stmt, names checker.indexer(), checker.stylist(), )); - checker.report_diagnostic(diagnostic); } } @@ -76,6 +75,7 @@ fn split_imports( .map(|alias| { let Alias { range: _, + node_index: _, name, asname, } = alias; @@ -100,6 +100,7 @@ fn split_imports( .map(|alias| { let Alias { range: _, + node_index: _, name, asname, } = alias; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs index efe474a2d31af..a431cb036862f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/not_tests.rs @@ -1,12 +1,12 @@ -use crate::fix::edits::pad; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::generate_comparison; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::fix::edits::pad; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for membership tests using `not {element} in {collection}`. @@ -88,6 +88,7 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { ops, comparators, range: _, + node_index: _, }) = unary_op.operand.as_ref() else { return; @@ -95,8 +96,8 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { match &**ops { [CmpOp::In] => { - if checker.enabled(Rule::NotInTest) { - let mut diagnostic = Diagnostic::new(NotInTest, unary_op.operand.range()); + if checker.is_rule_enabled(Rule::NotInTest) { + let mut diagnostic = checker.report_diagnostic(NotInTest, unary_op.operand.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( pad( generate_comparison( @@ -112,12 +113,11 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { ), unary_op.range(), ))); - checker.report_diagnostic(diagnostic); } } [CmpOp::Is] => { - if checker.enabled(Rule::NotIsTest) { - let mut diagnostic = Diagnostic::new(NotIsTest, unary_op.operand.range()); + if checker.is_rule_enabled(Rule::NotIsTest) { + let mut diagnostic = checker.report_diagnostic(NotIsTest, unary_op.operand.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( pad( generate_comparison( @@ -133,7 +133,6 @@ pub(crate) fn not_tests(checker: &Checker, unary_op: &ast::ExprUnaryOp) { ), unary_op.range(), ))); - checker.report_diagnostic(diagnostic); } } _ => {} diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs index 9945f7f8f3b32..40d272336dccc 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/tab_indentation.rs @@ -1,10 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_index::Indexer; use ruff_source_file::LineRanges; use ruff_text_size::{TextRange, TextSize}; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; /// ## What it does /// Checks for indentation that uses tabs. @@ -33,11 +34,7 @@ impl Violation for TabIndentation { } /// W191 -pub(crate) fn tab_indentation( - diagnostics: &mut Vec, - locator: &Locator, - indexer: &Indexer, -) { +pub(crate) fn tab_indentation(context: &LintContext, locator: &Locator, indexer: &Indexer) { let contents = locator.contents().as_bytes(); let mut offset = 0; while let Some(index) = memchr::memchr(b'\t', &contents[offset..]) { @@ -46,7 +43,7 @@ pub(crate) fn tab_indentation( // Determine whether the tab is part of the line's indentation. if let Some(indent) = tab_indentation_at_line_start(range.start(), locator, indexer) { - diagnostics.push(Diagnostic::new(TabIndentation, indent)); + context.report_diagnostic_if_enabled(TabIndentation, indent); } // Advance to the next line. diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs index fb7ad77c476c3..79ccdc69788ff 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/too_many_newlines_at_end_of_file.rs @@ -1,12 +1,13 @@ use std::iter::Peekable; use itertools::Itertools; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_notebook::CellOffsets; use ruff_python_parser::{Token, TokenKind, Tokens}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use crate::{AlwaysFixableViolation, Edit, Fix, checkers::ast::LintContext}; + /// ## What it does /// Checks for files with multiple trailing blank lines. /// @@ -58,16 +59,16 @@ impl AlwaysFixableViolation for TooManyNewlinesAtEndOfFile { /// W391 pub(crate) fn too_many_newlines_at_end_of_file( - diagnostics: &mut Vec, + context: &LintContext, tokens: &Tokens, cell_offsets: Option<&CellOffsets>, ) { let mut tokens_iter = tokens.iter().rev().peekable(); if let Some(cell_offsets) = cell_offsets { - diagnostics.extend(notebook_newline_diagnostics(tokens_iter, cell_offsets)); - } else if let Some(diagnostic) = newline_diagnostic(&mut tokens_iter, false) { - diagnostics.push(diagnostic); + notebook_newline_diagnostics(tokens_iter, cell_offsets, context); + } else { + newline_diagnostic(&mut tokens_iter, false, context); } } @@ -75,8 +76,8 @@ pub(crate) fn too_many_newlines_at_end_of_file( fn notebook_newline_diagnostics<'a>( mut tokens_iter: Peekable>, cell_offsets: &CellOffsets, -) -> Vec { - let mut results = Vec::new(); + context: &LintContext, +) { let offset_iter = cell_offsets.iter().rev(); // NB: When interpreting the below, recall that the iterators @@ -87,20 +88,16 @@ fn notebook_newline_diagnostics<'a>( .peeking_take_while(|tok| tok.end() >= offset) .for_each(drop); - let Some(diagnostic) = newline_diagnostic(&mut tokens_iter, true) else { - continue; - }; - - results.push(diagnostic); + newline_diagnostic(&mut tokens_iter, true, context); } - results } /// Possible diagnostic, with fix, for too many newlines in cell or source file fn newline_diagnostic<'a>( tokens_iter: &mut Peekable>, in_notebook: bool, -) -> Option { + context: &LintContext, +) { let mut num_trailing_newlines: u32 = 0; let mut newline_range_start: Option = None; let mut newline_range_end: Option = None; @@ -126,23 +123,24 @@ fn newline_diagnostic<'a>( } if num_trailing_newlines == 0 || num_trailing_newlines == 1 { - return None; + return; } - let (start, end) = (match (newline_range_start, newline_range_end) { + let Some((start, end)) = (match (newline_range_start, newline_range_end) { (Some(s), Some(e)) => Some((s, e)), _ => None, - })?; + }) else { + return; + }; let diagnostic_range = TextRange::new(start, end); - Some( - Diagnostic::new( - TooManyNewlinesAtEndOfFile { - num_trailing_newlines, - in_notebook, - }, - diagnostic_range, - ) - .with_fix(Fix::safe_edit(Edit::range_deletion(diagnostic_range))), - ) + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( + TooManyNewlinesAtEndOfFile { + num_trailing_newlines, + in_notebook, + }, + diagnostic_range, + ) { + diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic_range))); + } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs index 215dc880a8d51..78dabff4dfa37 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_index::Indexer; use ruff_source_file::Line; use ruff_text_size::{TextLen, TextRange, TextSize}; -use crate::registry::Rule; -use crate::settings::LinterSettings; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for superfluous trailing whitespace. @@ -25,6 +25,11 @@ use crate::Locator; /// spam(1)\n# /// ``` /// +/// ## Fix safety +/// +/// This fix is marked unsafe if the whitespace is inside a multiline string, +/// as removing it changes the string's content. +/// /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[derive(ViolationMetadata)] pub(crate) struct TrailingWhitespace; @@ -57,6 +62,11 @@ impl AlwaysFixableViolation for TrailingWhitespace { /// class Foo(object):\n\n bang = 12 /// ``` /// +/// ## Fix safety +/// +/// This fix is marked unsafe if the whitespace is inside a multiline string, +/// as removing it changes the string's content. +/// /// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[derive(ViolationMetadata)] pub(crate) struct BlankLineWithWhitespace; @@ -77,8 +87,8 @@ pub(crate) fn trailing_whitespace( line: &Line, locator: &Locator, indexer: &Indexer, - settings: &LinterSettings, -) -> Option { + context: &LintContext, +) { let whitespace_len: TextSize = line .chars() .rev() @@ -94,8 +104,8 @@ pub(crate) fn trailing_whitespace( Applicability::Safe }; if range == line.range() { - if settings.rules.enabled(Rule::BlankLineWithWhitespace) { - let mut diagnostic = Diagnostic::new(BlankLineWithWhitespace, range); + if context.is_rule_enabled(Rule::BlankLineWithWhitespace) { + let mut diagnostic = context.report_diagnostic(BlankLineWithWhitespace, range); // Remove any preceding continuations, to avoid introducing a potential // syntax error. diagnostic.set_fix(Fix::applicable_edit( @@ -107,16 +117,13 @@ pub(crate) fn trailing_whitespace( )), applicability, )); - return Some(diagnostic); } - } else if settings.rules.enabled(Rule::TrailingWhitespace) { - let mut diagnostic = Diagnostic::new(TrailingWhitespace, range); + } else if context.is_rule_enabled(Rule::TrailingWhitespace) { + let mut diagnostic = context.report_diagnostic(TrailingWhitespace, range); diagnostic.set_fix(Fix::applicable_edit( Edit::range_deletion(range), applicability, )); - return Some(diagnostic); } } - None } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs index 2b596320a11de..b7a0c0ca86c00 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/type_comparison.rs @@ -1,11 +1,11 @@ use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -76,7 +76,7 @@ pub(crate) fn type_comparison(checker: &Checker, compare: &ast::ExprCompare) { } // Disallow the comparison. - checker.report_diagnostic(Diagnostic::new(TypeComparison, compare.range())); + checker.report_diagnostic(TypeComparison, compare.range()); } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs index d3126e32ea8b8..63517ae80306a 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/whitespace_after_decorator.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Decorator; use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for trailing whitespace after a decorator's opening `@`. @@ -29,7 +29,6 @@ use crate::checkers::ast::Checker; /// ``` /// /// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length - #[derive(ViolationMetadata)] pub(crate) struct WhitespaceAfterDecorator; @@ -62,9 +61,8 @@ pub(crate) fn whitespace_after_decorator(checker: &Checker, decorator_list: &[De let end = start + TextSize::try_from(end).unwrap(); let range = TextRange::new(start, end); - let mut diagnostic = Diagnostic::new(WhitespaceAfterDecorator, range); + let mut diagnostic = checker.report_diagnostic(WhitespaceAfterDecorator, range); diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap index 7410af0c31557..4b92dd68235d4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- E11.py:3:1: E111 Indentation is not a multiple of 4 | @@ -27,7 +26,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -37,7 +36,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -57,7 +56,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap index 032451e9ba548..5ade4346e026f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap @@ -16,7 +16,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -26,7 +26,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -56,7 +56,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap index 09150ce64f11a..533d2ee8d1e03 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap @@ -1,13 +1,12 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- E11.py:9:1: SyntaxError: Expected an indented block after `if` statement | 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -27,7 +26,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -47,7 +46,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap index 2d9342697289c..0ce72700a4d44 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap @@ -1,13 +1,12 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- E11.py:9:1: SyntaxError: Expected an indented block after `if` statement | 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -17,7 +16,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -47,7 +46,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap index e0ce2a827cbe9..a8b570fb33493 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap @@ -6,7 +6,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -16,7 +16,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -96,7 +96,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap index e968a2526dc31..01db13d269d61 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap @@ -1,13 +1,12 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- E11.py:9:1: SyntaxError: Expected an indented block after `if` statement | 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -17,7 +16,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -77,7 +76,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap index 73be2664e6191..65c7dd99c84e2 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- E11.py:6:1: E117 Over-indented | @@ -17,7 +16,7 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement 7 | #: E112 8 | if False: 9 | print() - | ^ + | ^^^^^ 10 | #: E113 11 | print() | @@ -27,7 +26,7 @@ E11.py:12:1: SyntaxError: Unexpected indentation 10 | #: E113 11 | print() 12 | print() - | ^ + | ^^^^ 13 | #: E114 E116 14 | mimetype = 'application/x-directory' | @@ -67,7 +66,7 @@ E11.py:45:1: SyntaxError: Expected an indented block after `if` statement 43 | #: E112 44 | if False: # 45 | print() - | ^ + | ^^^^^ 46 | #: 47 | if False: | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_4.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_4.py.snap index 8af342273e06a..4893167012931 100644 Binary files a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_4.py.snap and b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E501_E501_4.py.snap differ diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap index 02f2c26951687..b540850623816 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- -E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `if res:` for truth checks +E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `res:` for truth checks | 1 | #: E712 2 | if res == True: @@ -14,12 +14,12 @@ E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `if res:` for tr ℹ Unsafe fix 1 1 | #: E712 2 |-if res == True: - 2 |+if res is True: + 2 |+if res: 3 3 | pass 4 4 | #: E712 5 5 | if res != False: -E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `if res:` for truth checks +E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `res:` for truth checks | 3 | pass 4 | #: E712 @@ -35,12 +35,12 @@ E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `if res:` for 3 3 | pass 4 4 | #: E712 5 |-if res != False: - 5 |+if res is not False: + 5 |+if res: 6 6 | pass 7 7 | #: E712 8 8 | if True != res: -E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `if not res:` for false checks +E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `not res:` for false checks | 6 | pass 7 | #: E712 @@ -56,12 +56,12 @@ E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `if not res:` 6 6 | pass 7 7 | #: E712 8 |-if True != res: - 8 |+if True is not res: + 8 |+if not res: 9 9 | pass 10 10 | #: E712 11 11 | if False == res: -E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `if not res:` for false checks +E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `not res:` for false checks | 9 | pass 10 | #: E712 @@ -77,12 +77,12 @@ E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `if not res:` 9 9 | pass 10 10 | #: E712 11 |-if False == res: - 11 |+if False is res: + 11 |+if not res: 12 12 | pass 13 13 | #: E712 14 14 | if res[1] == True: -E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `if res[1]:` for truth checks +E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `res[1]:` for truth checks | 12 | pass 13 | #: E712 @@ -98,12 +98,12 @@ E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `if res[1]:` fo 12 12 | pass 13 13 | #: E712 14 |-if res[1] == True: - 14 |+if res[1] is True: + 14 |+if res[1]: 15 15 | pass 16 16 | #: E712 17 17 | if res[1] != False: -E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `if res[1]:` for truth checks +E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `res[1]:` for truth checks | 15 | pass 16 | #: E712 @@ -119,12 +119,12 @@ E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `if res[1]:` 15 15 | pass 16 16 | #: E712 17 |-if res[1] != False: - 17 |+if res[1] is not False: + 17 |+if res[1]: 18 18 | pass 19 19 | #: E712 20 20 | var = 1 if cond == True else -1 if cond == False else cond -E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `if cond:` for truth checks +E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `cond:` for truth checks | 18 | pass 19 | #: E712 @@ -140,12 +140,12 @@ E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `if cond:` for 18 18 | pass 19 19 | #: E712 20 |-var = 1 if cond == True else -1 if cond == False else cond - 20 |+var = 1 if cond is True else -1 if cond == False else cond + 20 |+var = 1 if cond else -1 if cond == False else cond 21 21 | #: E712 22 22 | if (True) == TrueElement or x == TrueElement: 23 23 | pass -E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `if not cond:` for false checks +E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `not cond:` for false checks | 18 | pass 19 | #: E712 @@ -161,12 +161,12 @@ E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `if not cond: 18 18 | pass 19 19 | #: E712 20 |-var = 1 if cond == True else -1 if cond == False else cond - 20 |+var = 1 if cond == True else -1 if cond is False else cond + 20 |+var = 1 if cond == True else -1 if not cond else cond 21 21 | #: E712 22 22 | if (True) == TrueElement or x == TrueElement: 23 23 | pass -E712.py:22:4: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement:` for truth checks +E712.py:22:4: E712 [*] Avoid equality comparisons to `True`; use `TrueElement:` for truth checks | 20 | var = 1 if cond == True else -1 if cond == False else cond 21 | #: E712 @@ -181,7 +181,7 @@ E712.py:22:4: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement 20 20 | var = 1 if cond == True else -1 if cond == False else cond 21 21 | #: E712 22 |-if (True) == TrueElement or x == TrueElement: - 22 |+if (True) is TrueElement or x == TrueElement: + 22 |+if (TrueElement) or x == TrueElement: 23 23 | pass 24 24 | 25 25 | if res == True != False: @@ -226,7 +226,7 @@ E712.py:25:4: E712 [*] Avoid equality comparisons to `True` or `False` 27 27 | 28 28 | if(True) == TrueElement or x == TrueElement: -E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement:` for truth checks +E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `TrueElement:` for truth checks | 26 | pass 27 | @@ -241,12 +241,12 @@ E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement 26 26 | pass 27 27 | 28 |-if(True) == TrueElement or x == TrueElement: - 28 |+if(True) is TrueElement or x == TrueElement: + 28 |+if(TrueElement) or x == TrueElement: 29 29 | pass 30 30 | 31 31 | if (yield i) == True: -E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `if yield i:` for truth checks +E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `yield i:` for truth checks | 29 | pass 30 | @@ -261,7 +261,24 @@ E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `if yield i:` f 29 29 | pass 30 30 | 31 |-if (yield i) == True: - 31 |+if (yield i) is True: + 31 |+if (yield i): 32 32 | print("even") 33 33 | 34 34 | #: Okay + +E712.py:58:4: E712 [*] Avoid equality comparisons to `True`; use `True:` for truth checks + | +57 | # https://github.com/astral-sh/ruff/issues/17582 +58 | if True == True: # No duplicated diagnostic + | ^^^^^^^^^^^^ E712 +59 | pass + | + = help: Replace with `True` + +ℹ Unsafe fix +55 55 | assert [42, not foo] in bar +56 56 | +57 57 | # https://github.com/astral-sh/ruff/issues/17582 +58 |-if True == True: # No duplicated diagnostic + 58 |+if True: # No duplicated diagnostic +59 59 | pass diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap index 775ca06ecbaab..d80f8fca9619d 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap @@ -318,7 +318,7 @@ E731.py:139:5: E731 Do not assign a `lambda` expression, use a `def` 138 138 | class TemperatureScales(Enum): 139 |- CELSIUS = (lambda deg_c: deg_c) 139 |+ def CELSIUS(deg_c): - 140 |+ return deg_c + 140 |+ return (deg_c) 140 141 | FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32) 141 142 | 142 143 | @@ -338,7 +338,7 @@ E731.py:140:5: E731 Do not assign a `lambda` expression, use a `def` 139 139 | CELSIUS = (lambda deg_c: deg_c) 140 |- FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32) 140 |+ def FAHRENHEIT(deg_c): - 141 |+ return deg_c * 9 / 5 + 32 + 141 |+ return (deg_c * 9 / 5 + 32) 141 142 | 142 143 | 143 144 | # Regression test for: https://github.com/astral-sh/ruff/issues/7141 @@ -449,6 +449,8 @@ E731.py:176:1: E731 [*] Do not assign a `lambda` expression, use a `def` 178 | | y := 10 179 | | ) | |_^ E731 +180 | +181 | # https://github.com/astral-sh/ruff/issues/18475 | = help: Rewrite `x` as a `def` @@ -462,3 +464,59 @@ E731.py:176:1: E731 [*] Do not assign a `lambda` expression, use a `def` 177 178 | # comment 178 179 | y := 10 179 180 | ) + +E731.py:182:1: E731 [*] Do not assign a `lambda` expression, use a `def` + | +181 | # https://github.com/astral-sh/ruff/issues/18475 +182 | / foo_tooltip = ( +183 | | lambda x, data: f"\nfoo: {data['foo'][int(x)]}" +184 | | if data["foo"] is not None +185 | | else "" +186 | | ) + | |_^ E731 +187 | +188 | foo_tooltip = ( + | + = help: Rewrite `foo_tooltip` as a `def` + +ℹ Unsafe fix +179 179 | ) +180 180 | +181 181 | # https://github.com/astral-sh/ruff/issues/18475 +182 |-foo_tooltip = ( +183 |- lambda x, data: f"\nfoo: {data['foo'][int(x)]}" + 182 |+def foo_tooltip(x, data): + 183 |+ return (f"\nfoo: {data['foo'][int(x)]}" +184 184 | if data["foo"] is not None +185 |- else "" +186 |-) + 185 |+ else "") +187 186 | +188 187 | foo_tooltip = ( +189 188 | lambda x, data: f"\nfoo: {data['foo'][int(x)]}" + + +E731.py:188:1: E731 [*] Do not assign a `lambda` expression, use a `def` + | +186 | ) +187 | +188 | / foo_tooltip = ( +189 | | lambda x, data: f"\nfoo: {data['foo'][int(x)]}" + +190 | | more +191 | | +192 | | ) + | |_^ E731 + | + = help: Rewrite `foo_tooltip` as a `def` + +ℹ Unsafe fix +185 185 | else "" +186 186 | ) +187 187 | +188 |-foo_tooltip = ( +189 |- lambda x, data: f"\nfoo: {data['foo'][int(x)]}" + +190 |- more +191 |- +192 |-) + 188 |+def foo_tooltip(x, data): + 189 |+ return (f"\nfoo: {data['foo'][int(x)]}" + + 190 |+ more) diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap index 8d763a16a3516..4ca58ba4ec2e8 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -12,7 +12,7 @@ W605_1.py:4:11: W605 [*] Invalid escape sequence: `\.` = help: Use a raw string literal ℹ Safe fix -1 1 | # Same as `W605_0.py` but using f-strings instead. +1 1 | # Same as `W605_0.py` but using f-strings and t-strings instead. 2 2 | 3 3 | #: W605:1:10 4 |-regex = f'\.png$' @@ -320,3 +320,346 @@ W605_1.py:68:9: W605 [*] Invalid escape sequence: `\I` 67 67 | # Debug text (should trigger) 68 |-t = f"{'\InHere'=}" 68 |+t = f"{r'\InHere'=}" +69 69 | +70 70 | +71 71 | + +W605_1.py:73:11: W605 [*] Invalid escape sequence: `\.` + | +72 | #: W605:1:10 +73 | regex = t'\.png$' + | ^^ W605 +74 | +75 | #: W605:2:1 + | + = help: Use a raw string literal + +ℹ Safe fix +70 70 | +71 71 | +72 72 | #: W605:1:10 +73 |-regex = t'\.png$' + 73 |+regex = rt'\.png$' +74 74 | +75 75 | #: W605:2:1 +76 76 | regex = t''' + +W605_1.py:77:1: W605 [*] Invalid escape sequence: `\.` + | +75 | #: W605:2:1 +76 | regex = t''' +77 | \.png$ + | ^^ W605 +78 | ''' + | + = help: Use a raw string literal + +ℹ Safe fix +73 73 | regex = t'\.png$' +74 74 | +75 75 | #: W605:2:1 +76 |-regex = t''' + 76 |+regex = rt''' +77 77 | \.png$ +78 78 | ''' +79 79 | + +W605_1.py:82:7: W605 [*] Invalid escape sequence: `\_` + | +80 | #: W605:2:6 +81 | f( +82 | t'\_' + | ^^ W605 +83 | ) + | + = help: Use a raw string literal + +ℹ Safe fix +79 79 | +80 80 | #: W605:2:6 +81 81 | f( +82 |- t'\_' + 82 |+ rt'\_' +83 83 | ) +84 84 | +85 85 | #: W605:4:6 + +W605_1.py:89:6: W605 [*] Invalid escape sequence: `\_` + | +87 | multi-line +88 | literal +89 | with \_ somewhere + | ^^ W605 +90 | in the middle +91 | """ + | + = help: Use a raw string literal + +ℹ Safe fix +83 83 | ) +84 84 | +85 85 | #: W605:4:6 +86 |-t""" + 86 |+rt""" +87 87 | multi-line +88 88 | literal +89 89 | with \_ somewhere + +W605_1.py:94:40: W605 [*] Invalid escape sequence: `\_` + | +93 | #: W605:1:38 +94 | value = t'new line\nand invalid escape \_ here' + | ^^ W605 + | + = help: Add backslash to escape sequence + +ℹ Safe fix +91 91 | """ +92 92 | +93 93 | #: W605:1:38 +94 |-value = t'new line\nand invalid escape \_ here' + 94 |+value = t'new line\nand invalid escape \\_ here' +95 95 | +96 96 | +97 97 | #: Okay + +W605_1.py:109:1: W605 [*] Invalid escape sequence: `\w` + | +107 | regex = t'\w' # noqa +108 | regex = t''' +109 | \w + | ^^ W605 +110 | ''' # noqa + | + = help: Use a raw string literal + +ℹ Safe fix +105 105 | ''' +106 106 | s = t'\\' +107 107 | regex = t'\w' # noqa +108 |-regex = t''' + 108 |+regex = rt''' +109 109 | \w +110 110 | ''' # noqa +111 111 | + +W605_1.py:112:13: W605 [*] Invalid escape sequence: `\_` + | +110 | ''' # noqa +111 | +112 | regex = t'\\\_' + | ^^ W605 +113 | value = t'\{{1}}' +114 | value = t'\{1}' + | + = help: Add backslash to escape sequence + +ℹ Safe fix +109 109 | \w +110 110 | ''' # noqa +111 111 | +112 |-regex = t'\\\_' + 112 |+regex = t'\\\\_' +113 113 | value = t'\{{1}}' +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' + +W605_1.py:113:11: W605 [*] Invalid escape sequence: `\{` + | +112 | regex = t'\\\_' +113 | value = t'\{{1}}' + | ^^ W605 +114 | value = t'\{1}' +115 | value = t'{1:\}' + | + = help: Use a raw string literal + +ℹ Safe fix +110 110 | ''' # noqa +111 111 | +112 112 | regex = t'\\\_' +113 |-value = t'\{{1}}' + 113 |+value = rt'\{{1}}' +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' +116 116 | value = t"{t"\{1}"}" + +W605_1.py:114:11: W605 [*] Invalid escape sequence: `\{` + | +112 | regex = t'\\\_' +113 | value = t'\{{1}}' +114 | value = t'\{1}' + | ^^ W605 +115 | value = t'{1:\}' +116 | value = t"{t"\{1}"}" + | + = help: Use a raw string literal + +ℹ Safe fix +111 111 | +112 112 | regex = t'\\\_' +113 113 | value = t'\{{1}}' +114 |-value = t'\{1}' + 114 |+value = rt'\{1}' +115 115 | value = t'{1:\}' +116 116 | value = t"{t"\{1}"}" +117 117 | value = rt"{t"\{1}"}" + +W605_1.py:115:14: W605 [*] Invalid escape sequence: `\}` + | +113 | value = t'\{{1}}' +114 | value = t'\{1}' +115 | value = t'{1:\}' + | ^^ W605 +116 | value = t"{t"\{1}"}" +117 | value = rt"{t"\{1}"}" + | + = help: Use a raw string literal + +ℹ Safe fix +112 112 | regex = t'\\\_' +113 113 | value = t'\{{1}}' +114 114 | value = t'\{1}' +115 |-value = t'{1:\}' + 115 |+value = rt'{1:\}' +116 116 | value = t"{t"\{1}"}" +117 117 | value = rt"{t"\{1}"}" +118 118 | + +W605_1.py:116:14: W605 [*] Invalid escape sequence: `\{` + | +114 | value = t'\{1}' +115 | value = t'{1:\}' +116 | value = t"{t"\{1}"}" + | ^^ W605 +117 | value = rt"{t"\{1}"}" + | + = help: Use a raw string literal + +ℹ Safe fix +113 113 | value = t'\{{1}}' +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' +116 |-value = t"{t"\{1}"}" + 116 |+value = t"{rt"\{1}"}" +117 117 | value = rt"{t"\{1}"}" +118 118 | +119 119 | # Okay + +W605_1.py:117:15: W605 [*] Invalid escape sequence: `\{` + | +115 | value = t'{1:\}' +116 | value = t"{t"\{1}"}" +117 | value = rt"{t"\{1}"}" + | ^^ W605 +118 | +119 | # Okay + | + = help: Use a raw string literal + +ℹ Safe fix +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' +116 116 | value = t"{t"\{1}"}" +117 |-value = rt"{t"\{1}"}" + 117 |+value = rt"{rt"\{1}"}" +118 118 | +119 119 | # Okay +120 120 | value = rt'\{{1}}' + +W605_1.py:126:9: W605 [*] Invalid escape sequence: `\d` + | +125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 | t"{{}}+-\d" + | ^^ W605 +127 | t"\n{{}}+-\d+" +128 | t"\n{{}}�+-\d+" + | + = help: Use a raw string literal + +ℹ Safe fix +123 123 | value = t"{rt"\{1}"}" +124 124 | +125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 |-t"{{}}+-\d" + 126 |+rt"{{}}+-\d" +127 127 | t"\n{{}}+-\d+" +128 128 | t"\n{{}}�+-\d+" +129 129 | + +W605_1.py:127:11: W605 [*] Invalid escape sequence: `\d` + | +125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 | t"{{}}+-\d" +127 | t"\n{{}}+-\d+" + | ^^ W605 +128 | t"\n{{}}�+-\d+" + | + = help: Add backslash to escape sequence + +ℹ Safe fix +124 124 | +125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 126 | t"{{}}+-\d" +127 |-t"\n{{}}+-\d+" + 127 |+t"\n{{}}+-\\d+" +128 128 | t"\n{{}}�+-\d+" +129 129 | +130 130 | # See https://github.com/astral-sh/ruff/issues/11491 + +W605_1.py:128:12: W605 [*] Invalid escape sequence: `\d` + | +126 | t"{{}}+-\d" +127 | t"\n{{}}+-\d+" +128 | t"\n{{}}�+-\d+" + | ^^ W605 +129 | +130 | # See https://github.com/astral-sh/ruff/issues/11491 + | + = help: Add backslash to escape sequence + +ℹ Safe fix +125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 126 | t"{{}}+-\d" +127 127 | t"\n{{}}+-\d+" +128 |-t"\n{{}}�+-\d+" + 128 |+t"\n{{}}�+-\\d+" +129 129 | +130 130 | # See https://github.com/astral-sh/ruff/issues/11491 +131 131 | total = 10 + +W605_1.py:134:31: W605 [*] Invalid escape sequence: `\I` + | +132 | ok = 7 +133 | incomplete = 3 +134 | s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" + | ^^ W605 +135 | +136 | # Debug text (should trigger) + | + = help: Add backslash to escape sequence + +ℹ Safe fix +131 131 | total = 10 +132 132 | ok = 7 +133 133 | incomplete = 3 +134 |-s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" + 134 |+s = t"TOTAL: {total}\nOK: {ok}\\INCOMPLETE: {incomplete}\n" +135 135 | +136 136 | # Debug text (should trigger) +137 137 | t = t"{'\InHere'=}" + +W605_1.py:137:9: W605 [*] Invalid escape sequence: `\I` + | +136 | # Debug text (should trigger) +137 | t = t"{'\InHere'=}" + | ^^ W605 + | + = help: Use a raw string literal + +ℹ Safe fix +134 134 | s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" +135 135 | +136 136 | # Debug text (should trigger) +137 |-t = t"{'\InHere'=}" + 137 |+t = t"{r'\InHere'=}" diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap index 04831e37b3bd6..89296988de3cb 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- constant_literals.py:4:4: F632 [*] Use `==` to compare constant literals | @@ -107,7 +106,7 @@ constant_literals.py:12:4: F632 [*] Use `==` to compare constant literals 14 14 | if False == None: # E711, E712 (fix) 15 15 | pass -constant_literals.py:14:4: E712 [*] Avoid equality comparisons to `False`; use `if not None:` for false checks +constant_literals.py:14:4: E712 [*] Avoid equality comparisons to `False`; use `not None:` for false checks | 12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) 13 | pass @@ -123,7 +122,7 @@ constant_literals.py:14:4: E712 [*] Avoid equality comparisons to `False`; use ` 12 12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) 13 13 | pass 14 |-if False == None: # E711, E712 (fix) - 14 |+if False is None: # E711, E712 (fix) + 14 |+if not None: # E711, E712 (fix) 15 15 | pass 16 16 | if None == False: # E711, E712 (fix) 17 17 | pass @@ -144,7 +143,7 @@ constant_literals.py:14:13: E711 [*] Comparison to `None` should be `cond is Non 12 12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) 13 13 | pass 14 |-if False == None: # E711, E712 (fix) - 14 |+if False is None: # E711, E712 (fix) + 14 |+if not None: # E711, E712 (fix) 15 15 | pass 16 16 | if None == False: # E711, E712 (fix) 17 17 | pass @@ -164,12 +163,12 @@ constant_literals.py:16:4: E711 [*] Comparison to `None` should be `cond is None 14 14 | if False == None: # E711, E712 (fix) 15 15 | pass 16 |-if None == False: # E711, E712 (fix) - 16 |+if None is False: # E711, E712 (fix) + 16 |+if not None: # E711, E712 (fix) 17 17 | pass 18 18 | 19 19 | named_var = [] -constant_literals.py:16:4: E712 [*] Avoid equality comparisons to `False`; use `if not None:` for false checks +constant_literals.py:16:4: E712 [*] Avoid equality comparisons to `False`; use `not None:` for false checks | 14 | if False == None: # E711, E712 (fix) 15 | pass @@ -184,7 +183,7 @@ constant_literals.py:16:4: E712 [*] Avoid equality comparisons to `False`; use ` 14 14 | if False == None: # E711, E712 (fix) 15 15 | pass 16 |-if None == False: # E711, E712 (fix) - 16 |+if None is False: # E711, E712 (fix) + 16 |+if not None: # E711, E712 (fix) 17 17 | pass 18 18 | 19 19 | named_var = [] diff --git a/crates/ruff_linter/src/rules/pydoclint/mod.rs b/crates/ruff_linter/src/rules/pydoclint/mod.rs index af4131e427425..9fae2230a6807 100644 --- a/crates/ruff_linter/src/rules/pydoclint/mod.rs +++ b/crates/ruff_linter/src/rules/pydoclint/mod.rs @@ -4,7 +4,6 @@ pub mod settings; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use anyhow::Result; @@ -14,18 +13,18 @@ mod tests { use crate::rules::pydocstyle; use crate::rules::pydocstyle::settings::Convention; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; use super::settings::Settings; #[test_case(Rule::DocstringMissingException, Path::new("DOC501.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("pydoclint").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -36,7 +35,7 @@ mod tests { #[test_case(Rule::DocstringMissingException, Path::new("DOC501_google.py"))] #[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_google.py"))] fn rules_google_style(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("pydoclint").join(path).as_path(), &settings::LinterSettings { @@ -47,7 +46,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -58,7 +57,7 @@ mod tests { #[test_case(Rule::DocstringMissingException, Path::new("DOC501_numpy.py"))] #[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_numpy.py"))] fn rules_numpy_style(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("pydoclint").join(path).as_path(), &settings::LinterSettings { @@ -69,7 +68,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -79,7 +78,7 @@ mod tests { fn rules_google_style_ignore_one_line(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "{}_{}_ignore_one_line", - rule_code.as_ref(), + rule_code.name(), path.to_string_lossy() ); let diagnostics = test_path( @@ -95,7 +94,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 826d71761501d..1f5f5a0763bf4 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1,21 +1,20 @@ use itertools::Itertools; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_callable; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::{self as ast, visitor, Expr, Stmt}; +use ruff_python_ast::{self as ast, Expr, Stmt, visitor}; use ruff_python_semantic::analyze::{function_type, visibility}; use ruff_python_semantic::{Definition, SemanticModel}; use ruff_source_file::NewlineWithTrailingNewline; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; +use crate::docstrings::Docstring; use crate::docstrings::sections::{SectionContext, SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; -use crate::docstrings::Docstring; use crate::registry::Rule; use crate::rules::pydocstyle::settings::Convention; @@ -239,6 +238,9 @@ impl Violation for DocstringExtraneousYields { /// /// ## Example /// ```python +/// class FasterThanLightError(ArithmeticError): ... +/// +/// /// def calculate_speed(distance: float, time: float) -> float: /// """Calculate speed as distance divided by time. /// @@ -257,6 +259,9 @@ impl Violation for DocstringExtraneousYields { /// /// Use instead: /// ```python +/// class FasterThanLightError(ArithmeticError): ... +/// +/// /// def calculate_speed(distance: float, time: float) -> float: /// """Calculate speed as distance divided by time. /// @@ -523,7 +528,7 @@ impl Ranged for YieldEntry { } } -#[allow(clippy::enum_variant_names)] +#[expect(clippy::enum_variant_names)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ReturnEntryKind { NotNone, @@ -674,6 +679,7 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { } Stmt::Return(ast::StmtReturn { range, + node_index: _, value: Some(value), }) => { self.returns.push(ReturnEntry { @@ -685,7 +691,11 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { }, }); } - Stmt::Return(ast::StmtReturn { range, value: None }) => { + Stmt::Return(ast::StmtReturn { + range, + node_index: _, + value: None, + }) => { self.returns.push(ReturnEntry { range: *range, kind: ReturnEntryKind::ImplicitNone, @@ -702,6 +712,7 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { match expr { Expr::Yield(ast::ExprYield { range, + node_index: _, value: Some(value), }) => { self.yields.push(YieldEntry { @@ -709,7 +720,11 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { is_none_yield: value.is_none_literal_expr(), }); } - Expr::Yield(ast::ExprYield { range, value: None }) => { + Expr::Yield(ast::ExprYield { + range, + node_index: _, + value: None, + }) => { self.yields.push(YieldEntry { range: *range, is_none_yield: true, @@ -806,16 +821,24 @@ fn generator_annotation_arguments<'a>( ) -> Option> { let qualified_name = semantic.resolve_qualified_name(map_subscript(expr))?; match qualified_name.segments() { - ["typing" | "typing_extensions", "Iterable" | "AsyncIterable" | "Iterator" | "AsyncIterator"] - | ["collections", "abc", "Iterable" | "AsyncIterable" | "Iterator" | "AsyncIterator"] => { - match expr { - Expr::Subscript(ast::ExprSubscript { slice, .. }) => { - Some(GeneratorOrIteratorArguments::Single(slice)) - } - _ => Some(GeneratorOrIteratorArguments::Unparameterized), + [ + "typing" | "typing_extensions", + "Iterable" | "AsyncIterable" | "Iterator" | "AsyncIterator", + ] + | [ + "collections", + "abc", + "Iterable" | "AsyncIterable" | "Iterator" | "AsyncIterator", + ] => match expr { + Expr::Subscript(ast::ExprSubscript { slice, .. }) => { + Some(GeneratorOrIteratorArguments::Single(slice)) } - } - ["typing" | "typing_extensions", "Generator" | "AsyncGenerator"] + _ => Some(GeneratorOrIteratorArguments::Unparameterized), + }, + [ + "typing" | "typing_extensions", + "Generator" | "AsyncGenerator", + ] | ["collections", "abc", "Generator" | "AsyncGenerator"] => match expr { Expr::Subscript(ast::ExprSubscript { slice, .. }) => { if let Expr::Tuple(tuple) = &**slice { @@ -865,14 +888,12 @@ pub(crate) fn check_docstring( section_contexts: &SectionContexts, convention: Option, ) { - let mut diagnostics = Vec::new(); - // Only check function docstrings. let Some(function_def) = definition.as_function_def() else { return; }; - if checker.settings.pydoclint.ignore_one_line_docstrings && is_one_line(docstring) { + if checker.settings().pydoclint.ignore_one_line_docstrings && is_one_line(docstring) { return; } @@ -900,11 +921,11 @@ pub(crate) fn check_docstring( }; // DOC201 - if checker.enabled(Rule::DocstringMissingReturns) { + if checker.is_rule_enabled(Rule::DocstringMissingReturns) { if should_document_returns(function_def) && !returns_documented(docstring, &docstring_sections, convention) { - let extra_property_decorators = checker.settings.pydocstyle.property_decorators(); + let extra_property_decorators = checker.settings().pydocstyle.property_decorators(); if !definition.is_property(extra_property_decorators, semantic) { if !body_entries.returns.is_empty() { match function_def.returns.as_deref() { @@ -919,10 +940,8 @@ pub(crate) fn check_docstring( semantic, ) { - diagnostics.push(Diagnostic::new( - DocstringMissingReturns, - docstring.range(), - )); + checker + .report_diagnostic(DocstringMissingReturns, docstring.range()); } } None if body_entries @@ -930,8 +949,7 @@ pub(crate) fn check_docstring( .iter() .any(|entry| !entry.is_none_return()) => { - diagnostics - .push(Diagnostic::new(DocstringMissingReturns, docstring.range())); + checker.report_diagnostic(DocstringMissingReturns, docstring.range()); } _ => {} } @@ -941,7 +959,7 @@ pub(crate) fn check_docstring( } // DOC402 - if checker.enabled(Rule::DocstringMissingYields) { + if checker.is_rule_enabled(Rule::DocstringMissingYields) { if !yields_documented(docstring, &docstring_sections, convention) { if !body_entries.yields.is_empty() { match function_def.returns.as_deref() { @@ -950,12 +968,10 @@ pub(crate) fn check_docstring( |arguments| arguments.first().is_none_or(Expr::is_none_literal_expr), ) => { - diagnostics - .push(Diagnostic::new(DocstringMissingYields, docstring.range())); + checker.report_diagnostic(DocstringMissingYields, docstring.range()); } None if body_entries.yields.iter().any(|entry| !entry.is_none_yield) => { - diagnostics - .push(Diagnostic::new(DocstringMissingYields, docstring.range())); + checker.report_diagnostic(DocstringMissingYields, docstring.range()); } _ => {} } @@ -964,7 +980,7 @@ pub(crate) fn check_docstring( } // DOC501 - if checker.enabled(Rule::DocstringMissingException) { + if checker.is_rule_enabled(Rule::DocstringMissingException) { for body_raise in &body_entries.raised_exceptions { let Some(name) = body_raise.qualified_name.segments().last() else { continue; @@ -982,13 +998,12 @@ pub(crate) fn check_docstring( .ends_with(exception.segments()) }) }) { - let diagnostic = Diagnostic::new( + checker.report_diagnostic( DocstringMissingException { id: (*name).to_string(), }, docstring.range(), ); - diagnostics.push(diagnostic); } } } @@ -997,29 +1012,27 @@ pub(crate) fn check_docstring( // document that it raises an exception without including the exception in the implementation. if !visibility::is_abstract(&function_def.decorator_list, semantic) { // DOC202 - if checker.enabled(Rule::DocstringExtraneousReturns) { + if checker.is_rule_enabled(Rule::DocstringExtraneousReturns) { if docstring_sections.returns.is_some() { if body_entries.returns.is_empty() || body_entries.returns.iter().all(ReturnEntry::is_implicit) { - let diagnostic = Diagnostic::new(DocstringExtraneousReturns, docstring.range()); - diagnostics.push(diagnostic); + checker.report_diagnostic(DocstringExtraneousReturns, docstring.range()); } } } // DOC403 - if checker.enabled(Rule::DocstringExtraneousYields) { + if checker.is_rule_enabled(Rule::DocstringExtraneousYields) { if docstring_sections.yields.is_some() { if body_entries.yields.is_empty() { - let diagnostic = Diagnostic::new(DocstringExtraneousYields, docstring.range()); - diagnostics.push(diagnostic); + checker.report_diagnostic(DocstringExtraneousYields, docstring.range()); } } } // DOC502 - if checker.enabled(Rule::DocstringExtraneousException) { + if checker.is_rule_enabled(Rule::DocstringExtraneousException) { if let Some(docstring_raises) = docstring_sections.raises { let mut extraneous_exceptions = Vec::new(); for docstring_raise in &docstring_raises.raised_exceptions { @@ -1033,17 +1046,14 @@ pub(crate) fn check_docstring( } } if !extraneous_exceptions.is_empty() { - let diagnostic = Diagnostic::new( + checker.report_diagnostic( DocstringExtraneousException { ids: extraneous_exceptions, }, docstring.range(), ); - diagnostics.push(diagnostic); } } } } - - checker.report_diagnostics(diagnostics); } diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 93b47cfdad13f..fc785ac14eb65 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -6,9 +6,9 @@ use ruff_python_trivia::Cursor; use ruff_source_file::{Line, UniversalNewlines}; use ruff_text_size::{TextRange, TextSize}; +use crate::docstrings::Docstring; use crate::docstrings::sections::{SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; -use crate::docstrings::Docstring; use crate::rules::pydocstyle::settings::{Convention, Settings}; /// Return the index of the first logical line in a string. diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index 08731c7868c55..2df67b2320146 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -14,7 +14,7 @@ mod tests { use super::settings::{Convention, Settings}; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::MissingBlankLineAfterLastSection, Path::new("sections.py"))] #[test_case(Rule::NoBlankLineAfterSection, Path::new("sections.py"))] @@ -109,7 +109,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -119,7 +119,7 @@ mod tests { Path::new("pydocstyle/bom.py"), &settings::LinterSettings::for_rule(Rule::TripleSingleQuotes), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -134,7 +134,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -147,7 +147,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -164,7 +164,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -181,7 +181,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -198,7 +198,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::UndocumentedParam) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -211,7 +211,7 @@ mod tests { Rule::MissingTrailingPeriod, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -230,7 +230,7 @@ mod tests { Rule::UndocumentedPublicInit, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs index 86793435f710f..a062d173bd18b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/backslashes.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for docstrings that include backslashes, but are not defined as @@ -96,7 +96,8 @@ pub(crate) fn backslashes(checker: &Checker, docstring: &Docstring) { // Only allow continuations (backslashes followed by newlines) and Unicode escapes. if !matches!(*escaped_char, '\r' | '\n' | 'u' | 'U' | 'N') { - let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range()); + let mut diagnostic = + checker.report_diagnostic(EscapeSequenceInDocstring, docstring.range()); if !docstring.is_u_string() { diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( @@ -105,7 +106,6 @@ pub(crate) fn backslashes(checker: &Checker, docstring: &Docstring) { ))); } - checker.report_diagnostic(diagnostic); break; } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs index bd65d138fc493..50e6a75d3d219 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_after_summary.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for docstring summary lines that are not separated from the docstring @@ -84,7 +84,7 @@ pub(crate) fn blank_after_summary(checker: &Checker, docstring: &Docstring) { } } if lines_count > 1 && blanks_count != 1 { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingBlankLineAfterSummary { num_lines: blanks_count, }, @@ -118,6 +118,5 @@ pub(crate) fn blank_after_summary(checker: &Checker, docstring: &Docstring) { blank_end, ))); } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs index 1b562b9ae533a..1196a42a75fcd 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_class.rs @@ -1,6 +1,5 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_trivia::{indentation_at_offset, PythonWhitespace}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_trivia::{PythonWhitespace, indentation_at_offset}; use ruff_source_file::{Line, LineRanges, UniversalNewlineIterator}; use ruff_text_size::Ranged; use ruff_text_size::TextRange; @@ -8,6 +7,7 @@ use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for docstrings on class definitions that are not preceded by a @@ -170,8 +170,8 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring) return; } - if checker.enabled(Rule::IncorrectBlankLineBeforeClass) - || checker.enabled(Rule::BlankLineBeforeClass) + if checker.is_rule_enabled(Rule::IncorrectBlankLineBeforeClass) + || checker.is_rule_enabled(Rule::BlankLineBeforeClass) { let mut lines = UniversalNewlineIterator::with_offset( checker.locator().slice(between_range), @@ -191,33 +191,32 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring) } } - if checker.enabled(Rule::BlankLineBeforeClass) { + if checker.is_rule_enabled(Rule::BlankLineBeforeClass) { if blank_lines_before != 0 { - let mut diagnostic = Diagnostic::new(BlankLineBeforeClass, docstring.range()); + let mut diagnostic = + checker.report_diagnostic(BlankLineBeforeClass, docstring.range()); // Delete the blank line before the class. diagnostic.set_fix(Fix::safe_edit(Edit::deletion( blank_lines_start, docstring.line_start(), ))); - checker.report_diagnostic(diagnostic); } } - if checker.enabled(Rule::IncorrectBlankLineBeforeClass) { + if checker.is_rule_enabled(Rule::IncorrectBlankLineBeforeClass) { if blank_lines_before != 1 { let mut diagnostic = - Diagnostic::new(IncorrectBlankLineBeforeClass, docstring.range()); + checker.report_diagnostic(IncorrectBlankLineBeforeClass, docstring.range()); // Insert one blank line before the class. diagnostic.set_fix(Fix::safe_edit(Edit::replacement( checker.stylist().line_ending().to_string(), blank_lines_start, docstring.line_start(), ))); - checker.report_diagnostic(diagnostic); } } } - if checker.enabled(Rule::IncorrectBlankLineAfterClass) { + if checker.is_rule_enabled(Rule::IncorrectBlankLineAfterClass) { let class_after_docstring_range = TextRange::new(docstring.end(), class.end()); let class_after_docstring = checker.locator().slice(class_after_docstring_range); let mut lines = UniversalNewlineIterator::with_offset( @@ -245,7 +244,7 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring) let indentation = indentation_at_offset(docstring.start(), checker.source()) .expect("Own line docstring must have indentation"); let mut diagnostic = - Diagnostic::new(IncorrectBlankLineAfterClass, docstring.range()); + checker.report_diagnostic(IncorrectBlankLineAfterClass, docstring.range()); let line_ending = checker.stylist().line_ending().as_str(); // We have to trim the whitespace twice, once before the semicolon above and // once after the semicolon here, or we get invalid indents: @@ -259,7 +258,7 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring) replacement_start, first_line.end(), ))); - checker.report_diagnostic(diagnostic); + return; } else if trailing.starts_with('#') { // Keep the end-of-line comment, start counting empty lines after it @@ -280,14 +279,14 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring) } if blank_lines_after != 1 { - let mut diagnostic = Diagnostic::new(IncorrectBlankLineAfterClass, docstring.range()); + let mut diagnostic = + checker.report_diagnostic(IncorrectBlankLineAfterClass, docstring.range()); // Insert a blank line before the class (replacing any existing lines). diagnostic.set_fix(Fix::safe_edit(Edit::replacement( checker.stylist().line_ending().to_string(), replacement_start, blank_lines_end, ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs index ecd2df8762ca1..a3f1ab0efd6ee 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -1,8 +1,7 @@ use regex::Regex; use std::sync::LazyLock; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::PythonWhitespace; use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines}; use ruff_text_size::Ranged; @@ -11,6 +10,7 @@ use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for docstrings on functions that are separated by one or more blank @@ -107,7 +107,7 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri return; }; - if checker.enabled(Rule::BlankLineBeforeFunction) { + if checker.is_rule_enabled(Rule::BlankLineBeforeFunction) { let before = checker .locator() .slice(TextRange::new(function.start(), docstring.start())); @@ -126,7 +126,7 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri } if blank_lines_before != 0 { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BlankLineBeforeFunction { num_lines: blank_lines_before, }, @@ -137,11 +137,10 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri blank_lines_start, docstring.line_start(), ))); - checker.report_diagnostic(diagnostic); } } - if checker.enabled(Rule::BlankLineAfterFunction) { + if checker.is_rule_enabled(Rule::BlankLineAfterFunction) { let after = checker .locator() .slice(TextRange::new(docstring.end(), function.end())); @@ -180,7 +179,7 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri } if blank_lines_after != 0 { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BlankLineAfterFunction { num_lines: blank_lines_after, }, @@ -191,7 +190,6 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri first_line_end, blank_lines_end, ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs index 577e9c2f2dd88..ccc082ba38b4a 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for docstrings that do not start with a capital letter. @@ -90,7 +90,7 @@ pub(crate) fn capitalized(checker: &Checker, docstring: &Docstring) { let leading_whitespace_len = body.text_len() - trim_start_body.text_len(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( FirstWordUncapitalized { first_word: first_word.to_string(), capitalized_word: capitalized_word.to_string(), @@ -102,6 +102,4 @@ pub(crate) fn capitalized(checker: &Checker, docstring: &Docstring) { capitalized_word, TextRange::at(body.start() + leading_whitespace_len, first_word.text_len()), ))); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs index d9368797647ac..11f7915c338ef 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_period.rs @@ -1,14 +1,14 @@ use ruff_text_size::TextLen; use strum::IntoEnumIterator; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::docstrings::sections::SectionKind; use crate::docstrings::Docstring; +use crate::docstrings::sections::SectionKind; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::pydocstyle::helpers::logical_line; @@ -106,7 +106,8 @@ pub(crate) fn ends_with_period(checker: &Checker, docstring: &Docstring) { } if !trimmed.ends_with('.') { - let mut diagnostic = Diagnostic::new(MissingTrailingPeriod, docstring.range()); + let mut diagnostic = + checker.report_diagnostic(MissingTrailingPeriod, docstring.range()); // Best-effort fix: avoid adding a period after other punctuation marks. if !trimmed.ends_with([':', ';', '?', '!']) { diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( @@ -114,7 +115,6 @@ pub(crate) fn ends_with_period(checker: &Checker, docstring: &Docstring) { line.start() + trimmed.text_len(), ))); } - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs index b34f3c99feab1..6b4d25fa56f00 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/ends_with_punctuation.rs @@ -1,14 +1,14 @@ use ruff_text_size::TextLen; use strum::IntoEnumIterator; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::docstrings::sections::SectionKind; use crate::docstrings::Docstring; +use crate::docstrings::sections::SectionKind; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::pydocstyle::helpers::logical_line; @@ -105,7 +105,8 @@ pub(crate) fn ends_with_punctuation(checker: &Checker, docstring: &Docstring) { } if !trimmed.ends_with(['.', '!', '?']) { - let mut diagnostic = Diagnostic::new(MissingTerminalPunctuation, docstring.range()); + let mut diagnostic = + checker.report_diagnostic(MissingTerminalPunctuation, docstring.range()); // Best-effort fix: avoid adding a period after other punctuation marks. if !trimmed.ends_with([':', ';']) { diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( @@ -113,7 +114,6 @@ pub(crate) fn ends_with_punctuation(checker: &Checker, docstring: &Docstring) { line.start() + trimmed.text_len(), ))); } - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs index d739d28283acb..0ad276b81616f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/if_needed.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_overload; +use crate::Violation; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; @@ -82,9 +82,6 @@ pub(crate) fn if_needed(checker: &Checker, docstring: &Docstring) { return; }; if is_overload(&function.decorator_list, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( - OverloadWithDocstring, - function.identifier(), - )); + checker.report_diagnostic(OverloadWithDocstring, function.identifier()); } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs index 597380a369e14..2a8f9b721f526 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs @@ -1,6 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::docstrings::{clean_space, leading_space}; use ruff_source_file::{Line, NewlineWithTrailingNewline}; use ruff_text_size::{Ranged, TextSize}; @@ -9,8 +7,10 @@ use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Violation}; +use crate::{Edit, Fix}; -#[allow(clippy::tabs_in_doc_comments)] +#[expect(clippy::tabs_in_doc_comments)] /// ## What it does /// Checks for docstrings that are indented with tabs. /// @@ -220,17 +220,16 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) { // yet. has_seen_tab = has_seen_tab || line_indent.contains('\t'); - if checker.enabled(Rule::UnderIndentation) { + if checker.is_rule_enabled(Rule::UnderIndentation) { // We report under-indentation on every line. This isn't great, but enables // fix. if (is_last || !is_blank) && line_indent_size < docstring_indent_size { let mut diagnostic = - Diagnostic::new(UnderIndentation, TextRange::empty(line.start())); + checker.report_diagnostic(UnderIndentation, TextRange::empty(line.start())); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( clean_space(docstring_indentation), TextRange::at(line.start(), line_indent.text_len()), ))); - checker.report_diagnostic(diagnostic); } } @@ -265,13 +264,13 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) { current = lines.next(); } - if checker.enabled(Rule::DocstringTabIndentation) { + if checker.is_rule_enabled(Rule::DocstringTabIndentation) { if has_seen_tab { - checker.report_diagnostic(Diagnostic::new(DocstringTabIndentation, docstring.range())); + checker.report_diagnostic(DocstringTabIndentation, docstring.range()); } } - if checker.enabled(Rule::OverIndentation) { + if checker.is_rule_enabled(Rule::OverIndentation) { // If every line (except the last) is over-indented... if let Some(smallest_over_indent_size) = smallest_over_indent_size { for line in over_indented_lines { @@ -281,7 +280,7 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) { // We report over-indentation on every line. This isn't great, but // enables the fix capability. let mut diagnostic = - Diagnostic::new(OverIndentation, TextRange::empty(line.start())); + checker.report_diagnostic(OverIndentation, TextRange::empty(line.start())); let edit = if indent.is_empty() { // Delete the entire indent. @@ -311,7 +310,6 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) { Edit::range_replacement(indent, range) }; diagnostic.set_fix(Fix::safe_edit(edit)); - checker.report_diagnostic(diagnostic); } } @@ -324,7 +322,7 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) { let is_indent_only = line_indent.len() == last.len(); if last_line_over_indent > 0 && is_indent_only { let mut diagnostic = - Diagnostic::new(OverIndentation, TextRange::empty(last.start())); + checker.report_diagnostic(OverIndentation, TextRange::empty(last.start())); let indent = clean_space(docstring_indentation); let range = TextRange::at(last.start(), line_indent.text_len()); let edit = if indent.is_empty() { @@ -333,7 +331,6 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) { Edit::range_replacement(indent, range) }; diagnostic.set_fix(Fix::safe_edit(edit)); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs index f5f1829ea7bf9..f9e1348e7cdc7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/multi_line_summary_start.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::str::is_triple_quote; use ruff_python_semantic::Definition; use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlineIterator}; @@ -10,6 +9,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for docstring summary lines that are not positioned on the first @@ -155,8 +155,9 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring) }; if is_triple_quote(&first_line) { - if checker.enabled(Rule::MultiLineSummaryFirstLine) { - let mut diagnostic = Diagnostic::new(MultiLineSummaryFirstLine, docstring.range()); + if checker.is_rule_enabled(Rule::MultiLineSummaryFirstLine) { + let mut diagnostic = + checker.report_diagnostic(MultiLineSummaryFirstLine, docstring.range()); // Delete until first non-whitespace char. for line in content_lines { if let Some(end_column) = line.find(|c: char| !c.is_whitespace()) { @@ -167,7 +168,6 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring) break; } } - checker.report_diagnostic(diagnostic); } } else if first_line.as_str().ends_with('\\') { // Ignore the edge case whether a single quoted string is multiple lines through an @@ -179,8 +179,9 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring) // ``` return; } else { - if checker.enabled(Rule::MultiLineSummarySecondLine) { - let mut diagnostic = Diagnostic::new(MultiLineSummarySecondLine, docstring.range()); + if checker.is_rule_enabled(Rule::MultiLineSummarySecondLine) { + let mut diagnostic = + checker.report_diagnostic(MultiLineSummarySecondLine, docstring.range()); let mut indentation = Cow::Borrowed(docstring.compute_indentation()); let mut fixable = true; if !indentation.chars().all(char::is_whitespace) { @@ -223,7 +224,6 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring) first_line.end(), ))); } - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs index 5f1c0ee6502ee..afdaf68348ea7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs @@ -1,13 +1,13 @@ use ruff_text_size::{TextLen, TextSize}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::docstrings::clean_space; use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlines}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for multi-line docstrings whose closing quotes are not on their @@ -79,7 +79,7 @@ pub(crate) fn newline_after_last_paragraph(checker: &Checker, docstring: &Docstr { if last_line != "\"\"\"" && last_line != "'''" { let mut diagnostic = - Diagnostic::new(NewLineAfterLastParagraph, docstring.range()); + checker.report_diagnostic(NewLineAfterLastParagraph, docstring.range()); // Insert a newline just before the end-quote(s). let num_trailing_quotes = "'''".text_len(); let num_trailing_spaces: TextSize = last_line @@ -99,7 +99,6 @@ pub(crate) fn newline_after_last_paragraph(checker: &Checker, docstring: &Docstr docstring.end() - num_trailing_quotes - num_trailing_spaces, docstring.end() - num_trailing_quotes, ))); - checker.report_diagnostic(diagnostic); } } return; diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs index 2addede6a4388..5a06ae6022d9f 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/no_signature.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::UniversalNewlines; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; @@ -86,6 +86,6 @@ pub(crate) fn no_signature(checker: &Checker, docstring: &Docstring) { true }) { - checker.report_diagnostic(Diagnostic::new(SignatureInDocstring, docstring.range())); + checker.report_diagnostic(SignatureInDocstring, docstring.range()); } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs index 0b3410f1fb426..fd9ee2798e751 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::NewlineWithTrailingNewline; use ruff_text_size::Ranged; use ruff_text_size::{TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::pydocstyle::helpers::ends_with_backslash; @@ -62,7 +62,7 @@ pub(crate) fn no_surrounding_whitespace(checker: &Checker, docstring: &Docstring if line == trimmed { return; } - let mut diagnostic = Diagnostic::new(SurroundingWhitespace, docstring.range()); + let mut diagnostic = checker.report_diagnostic(SurroundingWhitespace, docstring.range()); let quote = docstring.quote_style().as_char(); // If removing whitespace would lead to an invalid string of quote // characters, avoid applying the fix. @@ -72,5 +72,4 @@ pub(crate) fn no_surrounding_whitespace(checker: &Checker, docstring: &Docstring TextRange::at(body.start(), line.text_len()), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs index 22b552bf32936..6369e355de29c 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -2,12 +2,12 @@ use std::sync::LazyLock; use imperative::Mood; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::visibility::{is_property, is_test}; use ruff_source_file::UniversalNewlines; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::rules::pydocstyle::helpers::normalize_word; @@ -99,11 +99,11 @@ pub(crate) fn non_imperative_mood(checker: &Checker, docstring: &Docstring, sett } if matches!(MOOD.is_imperative(&first_word_norm), Some(false)) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NonImperativeMood { first_line: first_line.to_string(), }, docstring.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs index b00392a246c42..4c69d9bd4f90c 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/not_empty.rs @@ -1,10 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; -use crate::registry::Rule; /// ## What it does /// Checks for empty docstrings. @@ -44,9 +43,6 @@ pub(crate) fn not_empty(checker: &Checker, docstring: &Docstring) -> bool { if !docstring.body().trim().is_empty() { return true; } - - if checker.enabled(Rule::EmptyDocstring) { - checker.report_diagnostic(Diagnostic::new(EmptyDocstring, docstring.range())); - } + checker.report_diagnostic_if_enabled(EmptyDocstring, docstring.range()); false } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs index 6e1e208a02bae..87d4f876aada3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/not_missing.rs @@ -1,14 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{ - is_call, is_init, is_magic, is_new, is_overload, is_override, Visibility, + Visibility, is_call, is_init, is_magic, is_new, is_overload, is_override, }; use ruff_python_semantic::{Definition, Member, MemberKind, Module, ModuleKind}; use ruff_text_size::TextRange; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::registry::Rule; /// ## What it does /// Checks for undocumented public module definitions. @@ -551,48 +550,29 @@ pub(crate) fn not_missing( if checker.source_type.is_ipynb() { return true; } - if checker.enabled(Rule::UndocumentedPublicModule) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicModule, - TextRange::default(), - )); - } + checker.report_diagnostic_if_enabled(UndocumentedPublicModule, TextRange::default()); false } Definition::Module(Module { kind: ModuleKind::Package, .. }) => { - if checker.enabled(Rule::UndocumentedPublicPackage) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicPackage, - TextRange::default(), - )); - } + checker.report_diagnostic_if_enabled(UndocumentedPublicPackage, TextRange::default()); false } Definition::Member(Member { kind: MemberKind::Class(class), .. }) => { - if checker.enabled(Rule::UndocumentedPublicClass) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicClass, - class.identifier(), - )); - } + checker.report_diagnostic_if_enabled(UndocumentedPublicClass, class.identifier()); false } Definition::Member(Member { kind: MemberKind::NestedClass(function), .. }) => { - if checker.enabled(Rule::UndocumentedPublicNestedClass) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicNestedClass, - function.identifier(), - )); - } + checker + .report_diagnostic_if_enabled(UndocumentedPublicNestedClass, function.identifier()); false } Definition::Member(Member { @@ -602,12 +582,10 @@ pub(crate) fn not_missing( if is_overload(&function.decorator_list, checker.semantic()) { true } else { - if checker.enabled(Rule::UndocumentedPublicFunction) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicFunction, - function.identifier(), - )); - } + checker.report_diagnostic_if_enabled( + UndocumentedPublicFunction, + function.identifier(), + ); false } } @@ -620,36 +598,19 @@ pub(crate) fn not_missing( { true } else if is_init(&function.name) { - if checker.enabled(Rule::UndocumentedPublicInit) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicInit, - function.identifier(), - )); - } + checker.report_diagnostic_if_enabled(UndocumentedPublicInit, function.identifier()); true } else if is_new(&function.name) || is_call(&function.name) { - if checker.enabled(Rule::UndocumentedPublicMethod) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicMethod, - function.identifier(), - )); - } + checker + .report_diagnostic_if_enabled(UndocumentedPublicMethod, function.identifier()); true } else if is_magic(&function.name) { - if checker.enabled(Rule::UndocumentedMagicMethod) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedMagicMethod, - function.identifier(), - )); - } + checker + .report_diagnostic_if_enabled(UndocumentedMagicMethod, function.identifier()); true } else { - if checker.enabled(Rule::UndocumentedPublicMethod) { - checker.report_diagnostic(Diagnostic::new( - UndocumentedPublicMethod, - function.identifier(), - )); - } + checker + .report_diagnostic_if_enabled(UndocumentedPublicMethod, function.identifier()); true } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs index 7c5ca77e19e38..158092f110792 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::NewlineWithTrailingNewline; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for single-line docstrings that are broken across multiple lines. @@ -27,6 +27,11 @@ use crate::docstrings::Docstring; /// """Return the mean of the given values.""" /// ``` /// +/// ## Fix safety +/// The fix is marked as unsafe because it could affect tools that parse docstrings, +/// documentation generators, or custom introspection utilities that rely on +/// specific docstring formatting. +/// /// ## References /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// @@ -62,7 +67,8 @@ pub(crate) fn one_liner(checker: &Checker, docstring: &Docstring) { } if non_empty_line_count == 1 && line_count > 1 { - let mut diagnostic = Diagnostic::new(UnnecessaryMultilineDocstring, docstring.range()); + let mut diagnostic = + checker.report_diagnostic(UnnecessaryMultilineDocstring, docstring.range()); // If removing whitespace would lead to an invalid string of quote // characters, avoid applying the fix. @@ -82,7 +88,5 @@ pub(crate) fn one_liner(checker: &Checker, docstring: &Docstring) { docstring.range(), ))); } - - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index ca93a71c8db65..0f4e51717a8f9 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -3,9 +3,7 @@ use regex::Regex; use rustc_hash::FxHashSet; use std::sync::LazyLock; -use ruff_diagnostics::{AlwaysFixableViolation, Violation}; -use ruff_diagnostics::{Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::docstrings::{clean_space, leading_space}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; @@ -14,12 +12,14 @@ use ruff_source_file::NewlineWithTrailingNewline; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::docstrings::Docstring; use crate::docstrings::sections::{SectionContext, SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; -use crate::docstrings::Docstring; use crate::registry::Rule; use crate::rules::pydocstyle::helpers::find_underline; use crate::rules::pydocstyle::settings::Convention; +use crate::{AlwaysFixableViolation, Violation}; +use crate::{Edit, Fix}; /// ## What it does /// Checks for over-indented sections in docstrings. @@ -1361,8 +1361,8 @@ fn blanks_and_section_underline( if let Some(non_blank_line) = following_lines.next() { if let Some(dashed_line) = find_underline(&non_blank_line, '-') { if num_blank_lines_after_header > 0 { - if checker.enabled(Rule::MissingSectionUnderlineAfterName) { - let mut diagnostic = Diagnostic::new( + if checker.is_rule_enabled(Rule::MissingSectionUnderlineAfterName) { + let mut diagnostic = checker.report_diagnostic( MissingSectionUnderlineAfterName { name: context.section_name().to_string(), }, @@ -1374,14 +1374,12 @@ fn blanks_and_section_underline( context.following_range().start(), blank_lines_end, ))); - - checker.report_diagnostic(diagnostic); } } if dashed_line.len().to_usize() != context.section_name().len() { - if checker.enabled(Rule::MismatchedSectionUnderlineLength) { - let mut diagnostic = Diagnostic::new( + if checker.is_rule_enabled(Rule::MismatchedSectionUnderlineLength) { + let mut diagnostic = checker.report_diagnostic( MismatchedSectionUnderlineLength { name: context.section_name().to_string(), }, @@ -1393,16 +1391,14 @@ fn blanks_and_section_underline( "-".repeat(context.section_name().len()), dashed_line, ))); - - checker.report_diagnostic(diagnostic); } } - if checker.enabled(Rule::OverindentedSectionUnderline) { + if checker.is_rule_enabled(Rule::OverindentedSectionUnderline) { let leading_space = leading_space(&non_blank_line); let docstring_indentation = docstring.compute_indentation(); if leading_space.len() > docstring_indentation.len() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( OverindentedSectionUnderline { name: context.section_name().to_string(), }, @@ -1420,8 +1416,6 @@ fn blanks_and_section_underline( } else { Edit::range_replacement(contents, range) })); - - checker.report_diagnostic(diagnostic); } } @@ -1440,15 +1434,13 @@ fn blanks_and_section_underline( } if following_lines.peek().is_none() { - if checker.enabled(Rule::EmptyDocstringSection) { - checker.report_diagnostic(Diagnostic::new( - EmptyDocstringSection { - name: context.section_name().to_string(), - }, - context.section_name_range(), - )); - } - } else if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) { + checker.report_diagnostic_if_enabled( + EmptyDocstringSection { + name: context.section_name().to_string(), + }, + context.section_name_range(), + ); + } else if checker.is_rule_enabled(Rule::BlankLinesBetweenHeaderAndContent) { // If the section is followed by exactly one line, and then a // reStructuredText directive, the blank lines should be preserved, as in: // @@ -1472,7 +1464,7 @@ fn blanks_and_section_underline( if is_sphinx { if num_blank_lines_dashes_end > 1 { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BlankLinesBetweenHeaderAndContent { name: context.section_name().to_string(), }, @@ -1484,10 +1476,9 @@ fn blanks_and_section_underline( line_after_dashes.start(), blank_lines_after_dashes_end, ))); - checker.report_diagnostic(diagnostic); } } else { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BlankLinesBetweenHeaderAndContent { name: context.section_name().to_string(), }, @@ -1498,24 +1489,22 @@ fn blanks_and_section_underline( line_after_dashes.start(), blank_lines_after_dashes_end, ))); - checker.report_diagnostic(diagnostic); } } } } else { - if checker.enabled(Rule::EmptyDocstringSection) { - checker.report_diagnostic(Diagnostic::new( - EmptyDocstringSection { - name: context.section_name().to_string(), - }, - context.section_name_range(), - )); - } + checker.report_diagnostic_if_enabled( + EmptyDocstringSection { + name: context.section_name().to_string(), + }, + context.section_name_range(), + ); } } else { - if style.is_numpy() && checker.enabled(Rule::MissingDashedUnderlineAfterSection) { + if style.is_numpy() && checker.is_rule_enabled(Rule::MissingDashedUnderlineAfterSection) + { if let Some(equal_line) = find_underline(&non_blank_line, '=') { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingDashedUnderlineAfterSection { name: context.section_name().to_string(), }, @@ -1528,10 +1517,8 @@ fn blanks_and_section_underline( "-".repeat(context.section_name().len()), equal_line, ))); - - checker.report_diagnostic(diagnostic); } else { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingDashedUnderlineAfterSection { name: context.section_name().to_string(), }, @@ -1549,12 +1536,10 @@ fn blanks_and_section_underline( content, context.summary_range().end(), ))); - - checker.report_diagnostic(diagnostic); } } if num_blank_lines_after_header > 0 { - if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) { + if checker.is_rule_enabled(Rule::BlankLinesBetweenHeaderAndContent) { // If the section is followed by exactly one line, and then a // reStructuredText directive, the blank lines should be preserved, as in: // @@ -1577,7 +1562,7 @@ fn blanks_and_section_underline( if is_sphinx { if num_blank_lines_after_header > 1 { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BlankLinesBetweenHeaderAndContent { name: context.section_name().to_string(), }, @@ -1590,10 +1575,9 @@ fn blanks_and_section_underline( context.following_range().start(), blank_lines_end, ))); - checker.report_diagnostic(diagnostic); } } else { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BlankLinesBetweenHeaderAndContent { name: context.section_name().to_string(), }, @@ -1604,15 +1588,14 @@ fn blanks_and_section_underline( TextRange::new(context.following_range().start(), blank_lines_end); // Delete any blank lines between the header and content. diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); - checker.report_diagnostic(diagnostic); } } } } } else { // Nothing but blank lines after the section header. - if style.is_numpy() && checker.enabled(Rule::MissingDashedUnderlineAfterSection) { - let mut diagnostic = Diagnostic::new( + if style.is_numpy() && checker.is_rule_enabled(Rule::MissingDashedUnderlineAfterSection) { + let mut diagnostic = checker.report_diagnostic( MissingDashedUnderlineAfterSection { name: context.section_name().to_string(), }, @@ -1630,17 +1613,13 @@ fn blanks_and_section_underline( content, context.summary_range().end(), ))); - - checker.report_diagnostic(diagnostic); - } - if checker.enabled(Rule::EmptyDocstringSection) { - checker.report_diagnostic(Diagnostic::new( - EmptyDocstringSection { - name: context.section_name().to_string(), - }, - context.section_name_range(), - )); } + checker.report_diagnostic_if_enabled( + EmptyDocstringSection { + name: context.section_name().to_string(), + }, + context.section_name_range(), + ); } } @@ -1651,11 +1630,11 @@ fn common_section( next: Option<&SectionContext>, style: SectionStyle, ) { - if checker.enabled(Rule::NonCapitalizedSectionName) { + if checker.is_rule_enabled(Rule::NonCapitalizedSectionName) { let capitalized_section_name = context.kind().as_str(); if context.section_name() != capitalized_section_name { let section_range = context.section_name_range(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NonCapitalizedSectionName { name: context.section_name().to_string(), }, @@ -1667,16 +1646,15 @@ fn common_section( capitalized_section_name.to_string(), section_range, ))); - checker.report_diagnostic(diagnostic); } } - if checker.enabled(Rule::OverindentedSection) { + if checker.is_rule_enabled(Rule::OverindentedSection) { let leading_space = leading_space(context.summary_line()); let docstring_indentation = docstring.compute_indentation(); if leading_space.len() > docstring_indentation.len() { let section_range = context.section_name_range(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( OverindentedSection { name: context.section_name().to_string(), }, @@ -1691,14 +1669,13 @@ fn common_section( } else { Edit::range_replacement(content, fix_range) })); - checker.report_diagnostic(diagnostic); } } let line_end = checker.stylist().line_ending().as_str(); if let Some(next) = next { - if checker.enabled(Rule::NoBlankLineAfterSection) { + if checker.is_rule_enabled(Rule::NoBlankLineAfterSection) { let num_blank_lines = context .following_lines() .rev() @@ -1706,7 +1683,7 @@ fn common_section( .count(); if num_blank_lines < 2 { let section_range = context.section_name_range(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NoBlankLineAfterSection { name: context.section_name().to_string(), }, @@ -1717,13 +1694,12 @@ fn common_section( line_end.to_string(), next.start(), ))); - checker.report_diagnostic(diagnostic); } } } else { // The first blank line is the line containing the closing triple quotes, so we need at // least two. - if checker.enabled(Rule::MissingBlankLineAfterLastSection) { + if checker.is_rule_enabled(Rule::MissingBlankLineAfterLastSection) { let num_blank_lines = context .following_lines() .rev() @@ -1741,32 +1717,31 @@ fn common_section( format!( "{}{}", line_end.repeat(2 - num_blank_lines), - docstring.compute_indentation() + leading_space(docstring.compute_indentation()) ), context.end() - del_len, context.end(), ); let section_range = context.section_name_range(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingBlankLineAfterLastSection { name: context.section_name().to_string(), }, section_range, ); diagnostic.set_fix(Fix::safe_edit(edit)); - checker.report_diagnostic(diagnostic); } } } - if checker.enabled(Rule::NoBlankLineBeforeSection) { + if checker.is_rule_enabled(Rule::NoBlankLineBeforeSection) { if !context .previous_line() .is_some_and(|line| line.trim().is_empty()) { let section_range = context.section_name_range(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NoBlankLineBeforeSection { name: context.section_name().to_string(), }, @@ -1777,7 +1752,6 @@ fn common_section( line_end.to_string(), context.start(), ))); - checker.report_diagnostic(diagnostic); } } @@ -1809,7 +1783,7 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa // Check specifically for `vararg` and `kwarg`, which can be prefixed with a // single or double star, respectively. - if !checker.settings.pydocstyle.ignore_var_parameters() { + if !checker.settings().pydocstyle.ignore_var_parameters() { if let Some(arg) = function.parameters.vararg.as_ref() { let arg_name = arg.name.as_str(); let starred_arg_name = format!("*{arg_name}"); @@ -1835,13 +1809,13 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa if !missing_arg_names.is_empty() { if let Some(definition) = docstring.definition.name() { let names = missing_arg_names.into_iter().sorted().collect(); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UndocumentedParam { definition: definition.to_string(), names, }, function.identifier(), - )); + ); } } } @@ -1951,11 +1925,11 @@ fn numpy_section( ) { common_section(checker, docstring, context, next, SectionStyle::Numpy); - if checker.enabled(Rule::MissingNewLineAfterSectionName) { + if checker.is_rule_enabled(Rule::MissingNewLineAfterSectionName) { let suffix = context.summary_after_section_name(); if !suffix.is_empty() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingNewLineAfterSectionName { name: context.section_name().to_string(), }, @@ -1966,12 +1940,10 @@ fn numpy_section( section_range.end(), suffix.text_len(), )))); - - checker.report_diagnostic(diagnostic); } } - if checker.enabled(Rule::UndocumentedParam) { + if checker.is_rule_enabled(Rule::UndocumentedParam) { if matches!(context.kind(), SectionKind::Parameters) { parameters_section(checker, docstring, context); } @@ -1986,10 +1958,10 @@ fn google_section( ) { common_section(checker, docstring, context, next, SectionStyle::Google); - if checker.enabled(Rule::MissingSectionNameColon) { + if checker.is_rule_enabled(Rule::MissingSectionNameColon) { let suffix = context.summary_after_section_name(); if suffix != ":" { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MissingSectionNameColon { name: context.section_name().to_string(), }, @@ -2001,7 +1973,6 @@ fn google_section( ":".to_string(), TextRange::at(section_name_range.end(), suffix.text_len()), ))); - checker.report_diagnostic(diagnostic); } } } @@ -2027,7 +1998,7 @@ fn parse_google_sections( google_section(checker, docstring, &context, iterator.peek()); } - if checker.enabled(Rule::UndocumentedParam) { + if checker.is_rule_enabled(Rule::UndocumentedParam) { let mut has_args = false; let mut documented_args: FxHashSet = FxHashSet::default(); for section_context in section_contexts { diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs index e0c37ed291c8b..1bbcc9ab4ca1a 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/starts_with_this.rs @@ -1,7 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::rules::pydocstyle::helpers::normalize_word; @@ -64,5 +64,5 @@ pub(crate) fn starts_with_this(checker: &Checker, docstring: &Docstring) { if normalize_word(first_word) != "this" { return; } - checker.report_diagnostic(Diagnostic::new(DocstringStartsWithThis, docstring.range())); + checker.report_diagnostic(DocstringStartsWithThis, docstring.range()); } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs index 42cab0c652237..374ac69369758 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/triple_quotes.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::str::Quote; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for docstrings that use `'''triple single quotes'''` instead of @@ -79,8 +79,8 @@ pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) { match expected_quote { Quote::Single => { if !opener.ends_with("'''") { - let mut diagnostic = - Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range()); + let mut diagnostic = checker + .report_diagnostic(TripleSingleQuotes { expected_quote }, docstring.range()); let body = docstring.body().as_str(); if !body.ends_with('\'') { @@ -89,14 +89,12 @@ pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) { docstring.range(), ))); } - - checker.report_diagnostic(diagnostic); } } Quote::Double => { if !opener.ends_with("\"\"\"") { - let mut diagnostic = - Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range()); + let mut diagnostic = checker + .report_diagnostic(TripleSingleQuotes { expected_quote }, docstring.range()); let body = docstring.body().as_str(); if !body.ends_with('"') { @@ -105,8 +103,6 @@ pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) { docstring.range(), ))); } - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap index d1339c1a8a46c..b8e6594eeeb18 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap @@ -79,5 +79,27 @@ D413.py:69:5: D413 [*] Missing blank line after last section ("Returns") 69 69 | Returns: 70 70 | the value 71 |- """ - 71 |+ +72 71 | 72 |+ """ + 73 |+ +73 74 | +74 75 | def func(): +75 76 | ("""Docstring. + +D413.py:77:5: D413 [*] Missing blank line after last section ("Raises") + | +75 | ("""Docstring. +76 | +77 | Raises: + | ^^^^^^ D413 +78 | ValueError: An error. +79 | """) + | + = help: Add blank line after "Raises" + +ℹ Safe fix +76 76 | +77 77 | Raises: +78 78 | ValueError: An error. + 79 |+ +79 80 | """) diff --git a/crates/ruff_linter/src/rules/pyflakes/fixes.rs b/crates/ruff_linter/src/rules/pyflakes/fixes.rs index e1cc9e975bec2..f2d6e64af3d8b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/fixes.rs +++ b/crates/ruff_linter/src/rules/pyflakes/fixes.rs @@ -1,6 +1,5 @@ use anyhow::{Context, Ok, Result}; -use ruff_diagnostics::Edit; use ruff_python_ast as ast; use ruff_python_ast::Expr; use ruff_python_codegen::Stylist; @@ -8,8 +7,9 @@ use ruff_python_semantic::Binding; use ruff_python_trivia::{BackwardsTokenizer, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::Ranged; -use crate::cst::matchers::{match_call_mut, match_dict, transform_expression}; +use crate::Edit; use crate::Locator; +use crate::cst::matchers::{match_call_mut, match_dict, transform_expression}; /// Generate a [`Edit`] to remove unused keys from format dict. pub(super) fn remove_unused_format_arguments_from_dict( diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 273e10e9da989..2aa8104d6b4f6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -11,6 +11,7 @@ mod tests { use anyhow::Result; use regex::Regex; + use ruff_db::diagnostic::Diagnostic; use ruff_python_parser::ParseOptions; use rustc_hash::FxHashMap; use test_case::test_case; @@ -19,19 +20,16 @@ mod tests { use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_trivia::textwrap::dedent; - use ruff_text_size::Ranged; use crate::linter::check_path; - use crate::message::Message; - use crate::registry::{AsRule, Linter, Rule}; + use crate::registry::{Linter, Rule}; use crate::rules::isort; use crate::rules::pyflakes; use crate::settings::types::PreviewMode; - use crate::settings::{flags, LinterSettings}; + use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; use crate::test::{test_contents, test_path, test_snippet}; - use crate::Locator; - use crate::{assert_messages, directives}; + use crate::{Locator, assert_diagnostics, directives}; #[test_case(Rule::UnusedImport, Path::new("F401_0.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_1.py"))] @@ -187,7 +185,7 @@ mod tests { Path::new("pyflakes").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -210,7 +208,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -219,11 +217,11 @@ mod tests { let diagnostics = test_snippet( "PythonFinalizationError", &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY312, + unresolved_target_version: ruff_python_ast::PythonVersion::PY312.into(), ..LinterSettings::for_rule(Rule::UndefinedName) }, ); - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); } #[test_case(Rule::UnusedImport, Path::new("__init__.py"))] @@ -247,7 +245,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -286,7 +284,7 @@ mod tests { }, ) .0; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); } // Regression test for https://github.com/astral-sh/ruff/issues/12897 @@ -314,7 +312,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }; let diagnostics = test_path(Path::new("pyflakes").join(path).as_path(), &settings)?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -334,7 +332,7 @@ mod tests { Path::new("pyflakes").join(path).as_path(), &LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -358,7 +356,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -374,7 +372,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -387,7 +385,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnusedVariable) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -401,7 +399,7 @@ mod tests { Rule::UnusedImport, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -411,7 +409,7 @@ mod tests { Path::new("pyflakes/builtins.py"), &LinterSettings::for_rules(vec![Rule::UndefinedName]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -424,7 +422,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::UndefinedName]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -434,7 +432,7 @@ mod tests { Path::new("pyflakes/typing_modules.py"), &LinterSettings::for_rules(vec![Rule::UndefinedName]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -447,7 +445,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::UndefinedName]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -457,7 +455,7 @@ mod tests { Path::new("pyflakes/future_annotations.py"), &LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UndefinedName]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -467,7 +465,7 @@ mod tests { Path::new("pyflakes/multi_statement_lines.py"), &LinterSettings::for_rule(Rule::UnusedImport), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -480,7 +478,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UndefinedName) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -493,7 +491,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UndefinedName) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -510,7 +508,7 @@ mod tests { ..LinterSettings::for_rule(Rule::UnusedImport) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -734,7 +732,7 @@ mod tests { contents, &LinterSettings::for_rules(Linter::Pyflakes.rules()), ); - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); } /// A re-implementation of the Pyflakes test runner. @@ -744,8 +742,9 @@ mod tests { let source_type = PySourceType::default(); let source_kind = SourceKind::Python(contents.to_string()); let settings = LinterSettings::for_rules(Linter::Pyflakes.rules()); + let target_version = settings.unresolved_target_version; let options = - ParseOptions::from(source_type).with_target_version(settings.unresolved_target_version); + ParseOptions::from(source_type).with_target_version(target_version.parser_version()); let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options) .try_into_module() .expect("PySourceType always parses into a module"); @@ -770,14 +769,15 @@ mod tests { &source_kind, source_type, &parsed, - settings.unresolved_target_version, + target_version, ); - messages.sort_by_key(Ranged::start); + messages.sort_by_key(|diagnostic| diagnostic.expect_range().start()); let actual = messages .iter() - .filter_map(Message::as_diagnostic_message) - .map(|diagnostic| diagnostic.kind.rule()) + .filter(|msg| !msg.is_invalid_syntax()) + .map(Diagnostic::name) .collect::>(); + let expected: Vec<_> = expected.iter().map(|rule| rule.name().as_str()).collect(); assert_eq!(actual, expected); } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs b/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs index 90ea3250e32ad..798f9bb990b43 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/assert_tuple.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -41,7 +41,7 @@ impl Violation for AssertTuple { pub(crate) fn assert_tuple(checker: &Checker, stmt: &Stmt, test: &Expr) { if let Expr::Tuple(tuple) = &test { if !tuple.is_empty() { - checker.report_diagnostic(Diagnostic::new(AssertTuple, stmt.range())); + checker.report_diagnostic(AssertTuple, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs b/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs index 957791cd6c820..88b77c2cb48ed 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/break_outside_loop.rs @@ -1,9 +1,10 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for `break` statements outside of loops. /// @@ -31,15 +32,16 @@ impl Violation for BreakOutsideLoop { /// F701 pub(crate) fn break_outside_loop<'a>( + checker: &Checker, stmt: &'a Stmt, parents: &mut impl Iterator, -) -> Option { +) { let mut child = stmt; for parent in parents { match parent { Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => { if !orelse.contains(child) { - return None; + return; } } Stmt::FunctionDef(_) | Stmt::ClassDef(_) => { @@ -50,5 +52,5 @@ pub(crate) fn break_outside_loop<'a>( child = parent; } - Some(Diagnostic::new(BreakOutsideLoop, stmt.range())) + checker.report_diagnostic(BreakOutsideLoop, stmt.range()); } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs b/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs index b09dfc5db677f..a6e426b9d5653 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/continue_outside_loop.rs @@ -1,9 +1,10 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for `continue` statements outside of loops. /// @@ -31,15 +32,16 @@ impl Violation for ContinueOutsideLoop { /// F702 pub(crate) fn continue_outside_loop<'a>( + checker: &Checker, stmt: &'a Stmt, parents: &mut impl Iterator, -) -> Option { +) { let mut child = stmt; for parent in parents { match parent { Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => { if !orelse.contains(child) { - return None; + return; } } Stmt::FunctionDef(_) | Stmt::ClassDef(_) => { @@ -50,5 +52,5 @@ pub(crate) fn continue_outside_loop<'a>( child = parent; } - Some(Diagnostic::new(ContinueOutsideLoop, stmt.range())) + checker.report_diagnostic(ContinueOutsideLoop, stmt.range()); } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs index 0b84aa6fc3fc7..42a45f3bf4200 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/default_except_not_last.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::except; use ruff_python_ast::{self as ast, ExceptHandler}; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for `except` blocks that handle all exceptions, but are not the last @@ -55,18 +56,14 @@ impl Violation for DefaultExceptNotLast { /// F707 pub(crate) fn default_except_not_last( + checker: &Checker, handlers: &[ExceptHandler], locator: &Locator, -) -> Option { +) { for (idx, handler) in handlers.iter().enumerate() { let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = handler; if type_.is_none() && idx < handlers.len() - 1 { - return Some(Diagnostic::new( - DefaultExceptNotLast, - except(handler, locator.contents()), - )); + checker.report_diagnostic(DefaultExceptNotLast, except(handler, locator.contents())); } } - - None } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index b9fd68f673814..3e15f4cfe334a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for f-strings that do not contain any placeholder expressions. @@ -73,7 +73,7 @@ pub(crate) fn f_string_missing_placeholders(checker: &Checker, expr: &ast::ExprF f_string .elements .iter() - .any(ast::FStringElement::is_expression) + .any(ast::InterpolatedStringElement::is_interpolation) }) { return; } @@ -91,13 +91,13 @@ pub(crate) fn f_string_missing_placeholders(checker: &Checker, expr: &ast::ExprF TextSize::new(1), ); - let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, f_string.range()); + let mut diagnostic = + checker.report_diagnostic(FStringMissingPlaceholders, f_string.range()); diagnostic.set_fix(convert_f_string_to_regular_string( prefix_range, f_string.range(), checker.locator(), )); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs b/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs index dd4a2a59a5ce6..41c3796563e2d 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/forward_annotation_syntax_error.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// Checks for forward annotations that include invalid syntax. diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs index 3f66ca3bf554f..f30347c0d638e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Alias; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_stdlib::future::is_feature_name; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -30,15 +30,16 @@ impl Violation for FutureFeatureNotDefined { } } +/// F407 pub(crate) fn future_feature_not_defined(checker: &Checker, alias: &Alias) { if is_feature_name(&alias.name) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( FutureFeatureNotDefined { name: alias.name.to_string(), }, alias.range(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs b/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs index 8ff228b3e543f..75fb7ce65a19f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/if_tuple.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{Expr, StmtIf}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::stmt_if::if_elif_branches; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -47,6 +47,6 @@ pub(crate) fn if_tuple(checker: &Checker, stmt_if: &StmtIf) { if tuple.is_empty() { continue; } - checker.report_diagnostic(Diagnostic::new(IfTuple, branch.test.range())); + checker.report_diagnostic(IfTuple, branch.test.range()); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs index 102f812d9e7e9..3b3c33d0fe669 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs @@ -1,6 +1,10 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_semantic::{BindingKind, Scope, ScopeId}; use ruff_source_file::SourceRow; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for import bindings that are shadowed by loop variables. @@ -43,6 +47,48 @@ impl Violation for ImportShadowedByLoopVar { } } +/// F402 +pub(crate) fn import_shadowed_by_loop_var(checker: &Checker, scope_id: ScopeId, scope: &Scope) { + for (name, binding_id) in scope.bindings() { + for shadow in checker.semantic().shadowed_bindings(scope_id, binding_id) { + // If the shadowing binding isn't a loop variable, abort. + let binding = &checker.semantic().bindings[shadow.binding_id()]; + if !binding.kind.is_loop_var() { + continue; + } + + // If the shadowed binding isn't an import, abort. + let shadowed = &checker.semantic().bindings[shadow.shadowed_id()]; + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) { + continue; + } + + // If the bindings are in different forks, abort. + if shadowed.source.is_none_or(|left| { + binding + .source + .is_none_or(|right| !checker.semantic().same_branch(left, right)) + }) { + continue; + } + + checker.report_diagnostic( + ImportShadowedByLoopVar { + name: name.to_string(), + row: checker.compute_source_row(shadowed.start()), + }, + binding.range(), + ); + } + } +} + /// ## What it does /// Checks for the use of wildcard imports. /// diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 7fae459a4cc21..6a6fa72517546 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -1,13 +1,13 @@ -use anyhow::{bail, Error}; +use anyhow::{Error, bail}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers; use ruff_python_ast::{CmpOp, Expr}; use ruff_python_parser::{TokenKind, Tokens}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `is` and `is not` comparisons against literals, like integers, @@ -90,7 +90,8 @@ pub(crate) fn invalid_literal_comparison( || helpers::is_mutable_iterable_initializer(left) || helpers::is_mutable_iterable_initializer(right)) { - let mut diagnostic = Diagnostic::new(IsLiteral { cmp_op: op.into() }, expr.range()); + let mut diagnostic = + checker.report_diagnostic(IsLiteral { cmp_op: op.into() }, expr.range()); if lazy_located.is_none() { lazy_located = Some(locate_cmp_ops(expr, checker.tokens())); } @@ -117,7 +118,6 @@ pub(crate) fn invalid_literal_comparison( bail!("Failed to fix invalid comparison due to missing op") } }); - checker.report_diagnostic(diagnostic); } left = right; } @@ -246,7 +246,7 @@ mod tests { use ruff_python_parser::parse_expression; use ruff_text_size::TextSize; - use super::{locate_cmp_ops, LocatedCmpOp}; + use super::{LocatedCmpOp, locate_cmp_ops}; fn extract_cmp_op_locations(source: &str) -> Result> { let parsed = parse_expression(source)?; diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs index 2ffe38b801fcc..146917dd455cc 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -59,6 +59,6 @@ impl Violation for InvalidPrintSyntax { /// F633 pub(crate) fn invalid_print_syntax(checker: &Checker, left: &Expr) { if checker.semantic().match_builtin_expr(left, "print") { - checker.report_diagnostic(Diagnostic::new(InvalidPrintSyntax, left.range())); + checker.report_diagnostic(InvalidPrintSyntax, left.range()); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs b/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs index 8db8537b63597..e19fd97b5d7e3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/raise_not_implemented.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `raise` statements that raise `NotImplemented`. @@ -74,7 +74,7 @@ pub(crate) fn raise_not_implemented(checker: &Checker, expr: &Expr) { let Some(expr) = match_not_implemented(expr) else { return; }; - let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range()); + let mut diagnostic = checker.report_diagnostic(RaiseNotImplemented, expr.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( "NotImplementedError", @@ -86,5 +86,4 @@ pub(crate) fn raise_not_implemented(checker: &Checker, expr: &Expr) { import_edit, )) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs index b48b120d208aa..a60b67f82d064 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs @@ -1,6 +1,14 @@ -use ruff_diagnostics::{FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_semantic::analyze::visibility; +use ruff_python_semantic::{BindingKind, Imported, Scope, ScopeId}; use ruff_source_file::SourceRow; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::fix::edits; +use crate::{Fix, FixAvailability, Violation}; + +use rustc_hash::FxHashMap; /// ## What it does /// Checks for variable definitions that redefine (or "shadow") unused @@ -42,3 +50,154 @@ impl Violation for RedefinedWhileUnused { Some(format!("Remove definition: `{name}`")) } } + +/// F811 +pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope: &Scope) { + // Index the redefined bindings by statement. + let mut redefinitions = FxHashMap::default(); + + for (name, binding_id) in scope.bindings() { + for shadow in checker.semantic().shadowed_bindings(scope_id, binding_id) { + // If the shadowing binding is a loop variable, abort, to avoid overlap + // with F402. + let binding = &checker.semantic().bindings[shadow.binding_id()]; + if binding.kind.is_loop_var() { + continue; + } + + // If the shadowed binding is used, abort. + let shadowed = &checker.semantic().bindings[shadow.shadowed_id()]; + if shadowed.is_used() { + continue; + } + + // If the shadowing binding isn't considered a "redefinition" of the + // shadowed binding, abort. + if !binding.redefines(shadowed) { + continue; + } + + if shadow.same_scope() { + // If the symbol is a dummy variable, abort, unless the shadowed + // binding is an import. + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) && checker.settings().dummy_variable_rgx.is_match(name) + { + continue; + } + + let Some(node_id) = shadowed.source else { + continue; + }; + + // If this is an overloaded function, abort. + if shadowed.kind.is_function_definition() { + if checker + .semantic() + .statement(node_id) + .as_function_def_stmt() + .is_some_and(|function| { + visibility::is_overload(&function.decorator_list, checker.semantic()) + }) + { + continue; + } + } + } else { + // Only enforce cross-scope shadowing for imports. + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) { + continue; + } + } + + // If the bindings are in different forks, abort. + if shadowed.source.is_none_or(|left| { + binding + .source + .is_none_or(|right| !checker.semantic().same_branch(left, right)) + }) { + continue; + } + + redefinitions + .entry(binding.source) + .or_insert_with(Vec::new) + .push((shadowed, binding)); + } + } + + // Create a fix for each source statement. + let mut fixes = FxHashMap::default(); + for (source, entries) in &redefinitions { + let Some(source) = source else { + continue; + }; + + let member_names = entries + .iter() + .filter_map(|(shadowed, binding)| { + if let Some(shadowed_import) = shadowed.as_any_import() { + if let Some(import) = binding.as_any_import() { + if shadowed_import.qualified_name() == import.qualified_name() { + return Some(import.member_name()); + } + } + } + None + }) + .collect::>(); + + if !member_names.is_empty() { + let statement = checker.semantic().statement(*source); + let parent = checker.semantic().parent_statement(*source); + let Ok(edit) = edits::remove_unused_imports( + member_names.iter().map(std::convert::AsRef::as_ref), + statement, + parent, + checker.locator(), + checker.stylist(), + checker.indexer(), + ) else { + continue; + }; + fixes.insert( + *source, + Fix::safe_edit(edit).isolate(Checker::isolation( + checker.semantic().parent_statement_id(*source), + )), + ); + } + } + + // Create diagnostics for each statement. + for (source, entries) in &redefinitions { + for (shadowed, binding) in entries { + let mut diagnostic = checker.report_diagnostic( + RedefinedWhileUnused { + name: binding.name(checker.source()).to_string(), + row: checker.compute_source_row(shadowed.start()), + }, + binding.range(), + ); + + if let Some(range) = binding.parent_range(checker.semantic()) { + diagnostic.set_parent(range.start()); + } + + if let Some(fix) = source.as_ref().and_then(|source| fixes.get(source)) { + diagnostic.set_fix(fix.clone()); + } + } + } +} diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index f29d1dd3399cb..0771b2ec721bb 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -1,8 +1,7 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use std::collections::hash_map::Entry; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::{ComparableExpr, HashableExpr}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; @@ -11,6 +10,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; use crate::registry::Rule; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for dictionary literals that associate multiple values with the @@ -40,6 +40,12 @@ use crate::registry::Rule; /// foo["baz"] # 2 /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing a repeated dictionary key +/// may delete comments that are attached to the removed key-value pair. This can also change +/// the program's behavior if the value expressions have side effects. +/// /// ## References /// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[derive(ViolationMetadata)] @@ -106,6 +112,12 @@ impl Violation for MultiValueRepeatedKeyLiteral { /// foo[baz] # 2 /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing a repeated dictionary key +/// may delete comments that are attached to the removed key-value pair. This can also change +/// the program's behavior if the value expressions have side effects. +/// /// ## References /// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[derive(ViolationMetadata)] @@ -164,8 +176,8 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { | Expr::EllipsisLiteral(_) | Expr::Tuple(_) | Expr::FString(_) => { - if checker.enabled(Rule::MultiValueRepeatedKeyLiteral) { - let mut diagnostic = Diagnostic::new( + if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) { + let mut diagnostic = checker.report_diagnostic( MultiValueRepeatedKeyLiteral { name: SourceCodeSnippet::from_str(checker.locator().slice(key)), existing: SourceCodeSnippet::from_str( @@ -194,12 +206,11 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { .end(), ))); } - checker.report_diagnostic(diagnostic); } } Expr::Name(_) => { - if checker.enabled(Rule::MultiValueRepeatedKeyVariable) { - let mut diagnostic = Diagnostic::new( + if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) { + let mut diagnostic = checker.report_diagnostic( MultiValueRepeatedKeyVariable { name: SourceCodeSnippet::from_str(checker.locator().slice(key)), }, @@ -226,7 +237,6 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { .end(), ))); } - checker.report_diagnostic(diagnostic); } } _ => {} diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs index ce7affefdd6db..bd004db4543e0 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/return_outside_function.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// Checks for `return` statements outside of functions. diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs index 9f99355fd02aa..b7566b73113fe 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/starred_expressions.rs @@ -1,8 +1,9 @@ use ruff_python_ast::Expr; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::{Violation, checkers::ast::Checker}; /// ## What it does /// Checks for the use of too many expressions in starred assignment statements. @@ -10,8 +11,8 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// ## Why is this bad? /// In assignment statements, starred expressions can be used to unpack iterables. /// -/// In Python 3, no more than 1 << 8 assignments are allowed before a starred -/// expression, and no more than 1 << 24 expressions are allowed after a starred +/// In Python 3, no more than `1 << 8` assignments are allowed before a starred +/// expression, and no more than `1 << 24` expressions are allowed after a starred /// expression. /// /// ## References @@ -54,17 +55,19 @@ impl Violation for MultipleStarredExpressions { /// F621, F622 pub(crate) fn starred_expressions( + checker: &Checker, elts: &[Expr], check_too_many_expressions: bool, check_two_starred_expressions: bool, location: TextRange, -) -> Option { +) { let mut has_starred: bool = false; let mut starred_index: Option = None; for (index, elt) in elts.iter().enumerate() { if elt.is_starred_expr() { if has_starred && check_two_starred_expressions { - return Some(Diagnostic::new(MultipleStarredExpressions, location)); + checker.report_diagnostic(MultipleStarredExpressions, location); + return; } has_starred = true; starred_index = Some(index); @@ -74,10 +77,8 @@ pub(crate) fn starred_expressions( if check_too_many_expressions { if let Some(starred_index) = starred_index { if starred_index >= 1 << 8 || elts.len() - starred_index > 1 << 24 { - return Some(Diagnostic::new(ExpressionsInStarAssignment, location)); + checker.report_diagnostic(ExpressionsInStarAssignment, location); } } } - - None } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs b/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs index 827a8596ea86f..785e7266b3b1f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/strings.rs @@ -1,21 +1,23 @@ use std::string::ToString; +use ruff_diagnostics::Applicability; +use ruff_python_ast::helpers::contains_effect; use rustc_hash::FxHashSet; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr, Keyword}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Fix, FixAvailability, Violation}; -use super::super::cformat::CFormatSummary; -use super::super::fixes::{ +use crate::rules::pyflakes::cformat::CFormatSummary; +use crate::rules::pyflakes::fixes::{ remove_unused_format_arguments_from_dict, remove_unused_keyword_arguments_from_format_call, remove_unused_positional_arguments_from_format_call, }; -use super::super::format::FormatSummary; +use crate::rules::pyflakes::format::FormatSummary; /// ## What it does /// Checks for invalid `printf`-style format strings. @@ -138,6 +140,16 @@ impl Violation for PercentFormatExpectedSequence { /// "Hello, %(name)s" % {"name": "World"} /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe for mapping key +/// containing function calls with potential side effects, +/// because removing such arguments could change the behavior of the code. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// "Hello, %(name)s" % {"greeting": print(1), "name": "World"} +/// ``` +/// /// ## References /// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[derive(ViolationMetadata)] @@ -379,6 +391,16 @@ impl Violation for StringDotFormatInvalidFormat { /// "Hello, {name}".format(name="World") /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if the unused keyword argument +/// contains a function call with potential side effects, +/// because removing such arguments could change the behavior of the code. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// "Hello, {name}".format(greeting=print(1), name="World") +/// ``` +/// /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] @@ -420,6 +442,16 @@ impl Violation for StringDotFormatExtraNamedArguments { /// "Hello, {0}".format("world") /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if the unused positional argument +/// contains a function call with potential side effects, +/// because removing such arguments could change the behavior of the code. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// "Hello, {0}".format("world", print(1)) +/// ``` +/// /// ## References /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[derive(ViolationMetadata)] @@ -539,7 +571,7 @@ pub(crate) fn percent_format_expected_mapping( | Expr::ListComp(_) | Expr::SetComp(_) | Expr::Generator(_) => { - checker.report_diagnostic(Diagnostic::new(PercentFormatExpectedMapping, location)); + checker.report_diagnostic(PercentFormatExpectedMapping, location); } _ => {} } @@ -554,7 +586,7 @@ pub(crate) fn percent_format_expected_sequence( location: TextRange, ) { if summary.num_positional > 1 && matches!(right, Expr::Dict(_) | Expr::DictComp(_)) { - checker.report_diagnostic(Diagnostic::new(PercentFormatExpectedSequence, location)); + checker.report_diagnostic(PercentFormatExpectedSequence, location); } } @@ -599,21 +631,29 @@ pub(crate) fn percent_format_extra_named_arguments( .iter() .map(|(_, name)| (*name).to_string()) .collect(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PercentFormatExtraNamedArguments { missing: names }, location, ); - let indexes: Vec = missing.iter().map(|(index, _)| *index).collect(); + diagnostic.try_set_fix(|| { + let indexes: Vec = missing.iter().map(|(index, _)| *index).collect(); let edit = remove_unused_format_arguments_from_dict( &indexes, dict, checker.locator(), checker.stylist(), )?; - Ok(Fix::safe_edit(edit)) + Ok(Fix::applicable_edit( + edit, + // Mark fix as unsafe if `dict` contains a call with side effect + if contains_effect(right, |id| checker.semantic().has_builtin_binding(id)) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )) }); - checker.report_diagnostic(diagnostic); } /// F505 @@ -654,12 +694,12 @@ pub(crate) fn percent_format_missing_arguments( .collect(); if !missing.is_empty() { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( PercentFormatMissingArgument { missing: missing.iter().map(|&s| s.clone()).collect(), }, location, - )); + ); } } @@ -670,10 +710,7 @@ pub(crate) fn percent_format_mixed_positional_and_named( location: TextRange, ) { if !(summary.num_positional == 0 || summary.keywords.is_empty()) { - checker.report_diagnostic(Diagnostic::new( - PercentFormatMixedPositionalAndNamed, - location, - )); + checker.report_diagnostic(PercentFormatMixedPositionalAndNamed, location); } } @@ -698,13 +735,13 @@ pub(crate) fn percent_format_positional_count_mismatch( } if found != summary.num_positional { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( PercentFormatPositionalCountMismatch { wanted: summary.num_positional, got: found, }, location, - )); + ); } } } @@ -718,8 +755,9 @@ pub(crate) fn percent_format_star_requires_sequence( ) { if summary.starred { match right { - Expr::Dict(_) | Expr::DictComp(_) => checker - .report_diagnostic(Diagnostic::new(PercentFormatStarRequiresSequence, location)), + Expr::Dict(_) | Expr::DictComp(_) => { + checker.report_diagnostic(PercentFormatStarRequiresSequence, location); + } _ => {} } } @@ -737,16 +775,19 @@ pub(crate) fn string_dot_format_extra_named_arguments( return; } - let keywords = keywords + let keyword_names = keywords .iter() - .filter_map(|Keyword { arg, .. }| arg.as_ref()); + .filter_map(|Keyword { arg, value, .. }| Some((arg.as_ref()?, value))); - let missing: Vec<(usize, &Name)> = keywords + let mut side_effects = false; + let missing: Vec<(usize, &Name)> = keyword_names .enumerate() - .filter_map(|(index, keyword)| { + .filter_map(|(index, (keyword, value))| { if summary.keywords.contains(keyword.id()) { None } else { + side_effects |= + contains_effect(value, |id| checker.semantic().has_builtin_binding(id)); Some((index, &keyword.id)) } }) @@ -757,11 +798,11 @@ pub(crate) fn string_dot_format_extra_named_arguments( } let names: Vec = missing.iter().map(|(_, name)| (*name).clone()).collect(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( StringDotFormatExtraNamedArguments { missing: names }, call.range(), ); - let indexes: Vec = missing.iter().map(|(index, _)| *index).collect(); + let indexes: Vec = missing.into_iter().map(|(index, _)| index).collect(); diagnostic.try_set_fix(|| { let edit = remove_unused_keyword_arguments_from_format_call( &indexes, @@ -769,9 +810,17 @@ pub(crate) fn string_dot_format_extra_named_arguments( checker.locator(), checker.stylist(), )?; - Ok(Fix::safe_edit(edit)) + + Ok(Fix::applicable_edit( + edit, + // Mark fix as unsafe if the `format` call contains an argument with side effect + if side_effects { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )) }); - checker.report_diagnostic(diagnostic); } /// F523 @@ -806,20 +855,24 @@ pub(crate) fn string_dot_format_extra_positional_arguments( true } + let mut side_effects = false; let missing: Vec = args .iter() .enumerate() .filter(|(i, arg)| { !(arg.is_starred_expr() || summary.autos.contains(i) || summary.indices.contains(i)) }) - .map(|(i, _)| i) + .map(|(i, arg)| { + side_effects |= contains_effect(arg, |id| checker.semantic().has_builtin_binding(id)); + i + }) .collect(); if missing.is_empty() { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( StringDotFormatExtraPositionalArguments { missing: missing .iter() @@ -837,11 +890,17 @@ pub(crate) fn string_dot_format_extra_positional_arguments( checker.locator(), checker.stylist(), )?; - Ok(Fix::safe_edit(edit)) + Ok(Fix::applicable_edit( + edit, + // Mark fix as unsafe if the `format` call contains an argument with side effect + if side_effects { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )) }); } - - checker.report_diagnostic(diagnostic); } /// F524 @@ -880,10 +939,7 @@ pub(crate) fn string_dot_format_missing_argument( .collect(); if !missing.is_empty() { - checker.report_diagnostic(Diagnostic::new( - StringDotFormatMissingArguments { missing }, - call.range(), - )); + checker.report_diagnostic(StringDotFormatMissingArguments { missing }, call.range()); } } @@ -894,9 +950,6 @@ pub(crate) fn string_dot_format_mixing_automatic( summary: &FormatSummary, ) { if !(summary.autos.is_empty() || summary.indices.is_empty()) { - checker.report_diagnostic(Diagnostic::new( - StringDotFormatMixingAutomatic, - call.range(), - )); + checker.report_diagnostic(StringDotFormatMixingAutomatic, call.range()); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs index 06f8b390e8802..18c9ddc1d8d30 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_export.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// Checks for undefined names in `__all__`. diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs index 3fef265e5d7ce..0af1d5f4fb6dc 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_local.rs @@ -1,10 +1,10 @@ use std::string::ToString; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::{Scope, ScopeId}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -62,12 +62,12 @@ pub(crate) fn undefined_local(checker: &Checker, scope_id: ScopeId, scope: &Scop } }) { // Then it's probably an error. - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UndefinedLocal { name: name.to_string(), }, range, - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs index cdfffe1f463b8..f1666bf9b3f5b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/undefined_name.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// Checks for uses of undefined names. diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs index 7ba0a158759dd..041a5a02110c6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_annotation.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Scope; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -39,13 +39,13 @@ pub(crate) fn unused_annotation(checker: &Checker, scope: &Scope) { let binding = checker.semantic().binding(binding_id); if binding.kind.is_annotation() && binding.is_unused() - && !checker.settings.dummy_variable_rgx.is_match(name) + && !checker.settings().dummy_variable_rgx.is_match(name) { Some((name.to_string(), binding.range())) } else { None } }) { - checker.report_diagnostic(Diagnostic::new(UnusedAnnotation { name }, range)); + checker.report_diagnostic(UnusedAnnotation { name }, range); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 1a9af61beb3a9..a12b6a98066a3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -1,11 +1,10 @@ use std::borrow::Cow; use std::iter; -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; use std::collections::BTreeMap; -use ruff_diagnostics::{Applicability, Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Stmt}; use ruff_python_semantic::{ @@ -16,8 +15,13 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix; +use crate::preview::{ + is_dunder_init_fix_unused_import_enabled, is_full_path_match_source_strategy_enabled, +}; use crate::registry::Rule; +use crate::rules::isort::categorize::MatchSourceStrategy; use crate::rules::{isort, isort::ImportSection, isort::ImportType}; +use crate::{Applicability, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unused imports. @@ -87,6 +91,11 @@ use crate::rules::{isort, isort::ImportSection, isort::ImportType}; /// print("numpy is not installed") /// ``` /// +/// ## Preview +/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +/// the criterion for determining whether an import is first-party +/// is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. +/// /// ## Options /// - `lint.ignore-init-module-imports` /// - `lint.pyflakes.allowed-unused-imports` @@ -149,12 +158,12 @@ impl Violation for UnusedImport { submodule_import: true, } => { return Some(format!( - "Use an explicit re-export: `import {parent} as {parent}; import {binding}`", - parent = binding - .split('.') - .next() - .expect("Expected all submodule imports to contain a '.'") - )) + "Use an explicit re-export: `import {parent} as {parent}; import {binding}`", + parent = binding + .split('.') + .next() + .expect("Expected all submodule imports to contain a '.'") + )); } UnusedImportContext::DunderInitFirstParty { dunder_all_count: DunderAllCount::One, @@ -170,7 +179,7 @@ impl Violation for UnusedImport { .split('.') .next() .expect("Expected all submodule imports to contain a '.'") - )) + )); } UnusedImportContext::DunderInitFirstParty { dunder_all_count: DunderAllCount::Many, @@ -221,18 +230,24 @@ enum UnusedImportContext { } fn is_first_party(import: &AnyImport, checker: &Checker) -> bool { - let qualified_name = import.qualified_name(); + let source_name = import.source_name().join("."); + let match_source_strategy = if is_full_path_match_source_strategy_enabled(checker.settings()) { + MatchSourceStrategy::FullPath + } else { + MatchSourceStrategy::Root + }; let category = isort::categorize( - &qualified_name.to_string(), - qualified_name.is_unresolved_import(), - &checker.settings.src, + &source_name, + import.qualified_name().is_unresolved_import(), + &checker.settings().src, checker.package(), - checker.settings.isort.detect_same_package, - &checker.settings.isort.known_modules, + checker.settings().isort.detect_same_package, + &checker.settings().isort.known_modules, checker.target_version(), - checker.settings.isort.no_sections, - &checker.settings.isort.section_order, - &checker.settings.isort.default_section, + checker.settings().isort.no_sections, + &checker.settings().isort.section_order, + &checker.settings().isort.default_section, + match_source_strategy, ); matches! { category, @@ -261,6 +276,7 @@ fn find_dunder_all_exprs<'a>(semantic: &'a SemanticModel) -> Vec<&'a ast::Expr> .collect() } +/// F401 /// For some unused binding in an import statement... /// /// __init__.py ∧ 1stpty → safe, if one __all__, add to __all__ @@ -302,7 +318,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { // If an import is marked as required, avoid treating it as unused, regardless of whether // it was _actually_ used. if checker - .settings + .settings() .isort .required_imports .iter() @@ -313,7 +329,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { // If an import was marked as allowed, avoid treating it as unused. if checker - .settings + .settings() .pyflakes .allowed_unused_imports .iter() @@ -351,8 +367,8 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { } let in_init = checker.path().ends_with("__init__.py"); - let fix_init = !checker.settings.ignore_init_module_imports; - let preview_mode = checker.settings.preview.is_enabled(); + let fix_init = !checker.settings().ignore_init_module_imports; + let preview_mode = is_dunder_init_fix_unused_import_enabled(checker.settings()); let dunder_all_exprs = find_dunder_all_exprs(checker.semantic()); // Generate a diagnostic for every import, but share fixes across all imports within the same @@ -410,7 +426,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { iter::zip(to_remove, iter::repeat(fix_remove)), iter::zip(to_reexport, iter::repeat(fix_reexport)), ) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnusedImport { name: binding.import.qualified_name().to_string(), module: binding.import.member_name().to_string(), @@ -429,14 +445,13 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { diagnostic.set_fix(fix.clone()); } } - checker.report_diagnostic(diagnostic); } } // Separately, generate a diagnostic for every _ignored_ import, to ensure that the // suppression comments aren't marked as unused. for binding in ignored.into_values().flatten() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnusedImport { name: binding.import.qualified_name().to_string(), module: binding.import.member_name().to_string(), @@ -450,7 +465,6 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { if let Some(range) = binding.parent_range { diagnostic.set_parent(range.start()); } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs index dc7d7e208cef8..7dde1bd468d15 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs @@ -1,7 +1,6 @@ use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Stmt}; @@ -11,6 +10,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::fix::edits::delete_stmt; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the presence of unused variables in function scopes. @@ -38,6 +38,11 @@ use crate::fix::edits::delete_stmt; /// return x /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing an unused variable assignment may +/// delete comments that are attached to the assignment. +/// /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] @@ -182,7 +187,7 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option { } else { let name = binding.name(checker.source()); let renamed = format!("_{name}"); - if checker.settings.dummy_variable_rgx.is_match(&renamed) { + if checker.settings().dummy_variable_rgx.is_match(&renamed) { let edit = Edit::range_replacement(renamed, binding.range()); return Some(Fix::unsafe_edit(edit).isolate(isolation)); @@ -251,7 +256,7 @@ pub(crate) fn unused_variable(checker: &Checker, name: &str, binding: &Binding) return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnusedVariable { name: name.to_string(), }, @@ -260,5 +265,4 @@ pub(crate) fn unused_variable(checker: &Checker, name: &str, binding: &Binding) if let Some(fix) = remove_unused_variable(binding, checker) { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs b/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs index 5e6960d145a17..7838c267db37f 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/yield_outside_function.rs @@ -1,9 +1,10 @@ use std::fmt; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::semantic_errors::YieldOutsideFunctionKind; +use crate::Violation; + #[derive(Debug, PartialEq, Eq)] pub(crate) enum DeferralKeyword { Yield, diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F504_F504.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F504_F504.py.snap index 270e0e25a5957..175af6e93b74d 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F504_F504.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F504_F504.py.snap @@ -89,6 +89,8 @@ F504.py:14:1: F504 [*] `%`-format string has unused named argument(s): test1, te 15 | | 'test1': '', 'test2': '', 16 | | } | |_^ F504 +17 | +18 | # https://github.com/astral-sh/ruff/issues/18806 | = help: Remove extra named arguments: test1, test2 @@ -99,3 +101,20 @@ F504.py:14:1: F504 [*] `%`-format string has unused named argument(s): test1, te 15 |- 'test1': '', 16 |- 'test2': '', 15 |+ 17 16 | } +18 17 | +19 18 | # https://github.com/astral-sh/ruff/issues/18806 + +F504.py:20:1: F504 [*] `%`-format string has unused named argument(s): greeting + | +19 | # https://github.com/astral-sh/ruff/issues/18806 +20 | "Hello, %(name)s" % {"greeting": print(1), "name": "World"} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F504 + | + = help: Remove extra named arguments: greeting + +ℹ Unsafe fix +17 17 | } +18 18 | +19 19 | # https://github.com/astral-sh/ruff/issues/18806 +20 |-"Hello, %(name)s" % {"greeting": print(1), "name": "World"} + 20 |+"Hello, %(name)s" % {"name": "World"} diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F522_F522.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F522_F522.py.snap index 4548b23df1866..f6a9f22629fb0 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F522_F522.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F522_F522.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pyflakes/mod.rs -snapshot_kind: text --- F522.py:1:1: F522 [*] `.format` call has unused named argument(s): bar | @@ -55,6 +54,7 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham 4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522 5 5 | ('' 6 6 | .format(x=2)) # F522 +7 7 | F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x | @@ -64,6 +64,8 @@ F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x | __^ 6 | | .format(x=2)) # F522 | |_____________^ F522 +7 | +8 | # https://github.com/astral-sh/ruff/issues/18806 | = help: Remove extra named arguments: x @@ -73,3 +75,43 @@ F522.py:5:2: F522 [*] `.format` call has unused named argument(s): x 5 5 | ('' 6 |- .format(x=2)) # F522 6 |+ .format()) # F522 +7 7 | +8 8 | # https://github.com/astral-sh/ruff/issues/18806 +9 9 | # The fix here is unsafe because the unused argument has side effect + +F522.py:10:1: F522 [*] `.format` call has unused named argument(s): greeting + | + 8 | # https://github.com/astral-sh/ruff/issues/18806 + 9 | # The fix here is unsafe because the unused argument has side effect +10 | "Hello, {name}".format(greeting=print(1), name="World") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522 +11 | +12 | # The fix here is safe because the unused argument has no side effect, + | + = help: Remove extra named arguments: greeting + +ℹ Unsafe fix +7 7 | +8 8 | # https://github.com/astral-sh/ruff/issues/18806 +9 9 | # The fix here is unsafe because the unused argument has side effect +10 |-"Hello, {name}".format(greeting=print(1), name="World") + 10 |+"Hello, {name}".format(name="World") +11 11 | +12 12 | # The fix here is safe because the unused argument has no side effect, +13 13 | # even though the used argument has a side effect + +F522.py:14:1: F522 [*] `.format` call has unused named argument(s): greeting + | +12 | # The fix here is safe because the unused argument has no side effect, +13 | # even though the used argument has a side effect +14 | "Hello, {name}".format(greeting="Pikachu", name=print(1)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522 + | + = help: Remove extra named arguments: greeting + +ℹ Safe fix +11 11 | +12 12 | # The fix here is safe because the unused argument has no side effect, +13 13 | # even though the used argument has a side effect +14 |-"Hello, {name}".format(greeting="Pikachu", name=print(1)) + 14 |+"Hello, {name}".format(name=print(1)) diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F523_F523.py.snap index 5b882fb4c6dfe..373bf431e6a41 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F523_F523.py.snap @@ -286,6 +286,8 @@ F523.py:36:1: F523 [*] `.format` call has unused arguments at position(s): 0 36 |-"Hello".format("world") 36 |+"Hello" 37 37 | "Hello".format("world", key="value") +38 38 | +39 39 | # https://github.com/astral-sh/ruff/issues/18806 F523.py:37:1: F523 [*] `.format` call has unused arguments at position(s): 0 | @@ -293,6 +295,8 @@ F523.py:37:1: F523 [*] `.format` call has unused arguments at position(s): 0 36 | "Hello".format("world") 37 | "Hello".format("world", key="value") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +38 | +39 | # https://github.com/astral-sh/ruff/issues/18806 | = help: Remove extra positional arguments at position(s): 0 @@ -302,3 +306,43 @@ F523.py:37:1: F523 [*] `.format` call has unused arguments at position(s): 0 36 36 | "Hello".format("world") 37 |-"Hello".format("world", key="value") 37 |+"Hello".format(key="value") +38 38 | +39 39 | # https://github.com/astral-sh/ruff/issues/18806 +40 40 | # The fix here is unsafe because the unused argument has side effect + +F523.py:41:1: F523 [*] `.format` call has unused arguments at position(s): 1 + | +39 | # https://github.com/astral-sh/ruff/issues/18806 +40 | # The fix here is unsafe because the unused argument has side effect +41 | "Hello, {0}".format("world", print(1)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +42 | +43 | # The fix here is safe because the unused argument has no side effect, + | + = help: Remove extra positional arguments at position(s): 1 + +ℹ Unsafe fix +38 38 | +39 39 | # https://github.com/astral-sh/ruff/issues/18806 +40 40 | # The fix here is unsafe because the unused argument has side effect +41 |-"Hello, {0}".format("world", print(1)) + 41 |+"Hello, {0}".format("world", ) +42 42 | +43 43 | # The fix here is safe because the unused argument has no side effect, +44 44 | # even though the used argument has a side effect + +F523.py:45:1: F523 [*] `.format` call has unused arguments at position(s): 1 + | +43 | # The fix here is safe because the unused argument has no side effect, +44 | # even though the used argument has a side effect +45 | "Hello, {0}".format(print(1), "Pikachu") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 + | + = help: Remove extra positional arguments at position(s): 1 + +ℹ Safe fix +42 42 | +43 43 | # The fix here is safe because the unused argument has no side effect, +44 44 | # even though the used argument has a side effect +45 |-"Hello, {0}".format(print(1), "Pikachu") + 45 |+"Hello, {0}".format(print(1), ) diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap index b871b1e78f2bb..4afdfc8bb6de6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F704_F704.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pyflakes/mod.rs -snapshot_kind: text --- F704.py:6:5: F704 `yield` statement outside of a function | @@ -31,4 +30,6 @@ F704.py:11:1: F704 `await` statement outside of a function 10 | yield from 3 11 | await f() | ^^^^^^^^^ F704 +12 | +13 | def _(): | diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs b/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs index adc4e91fa6367..1117bb516aecb 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs @@ -10,8 +10,9 @@ mod tests { use crate::registry::Rule; + use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::BlanketTypeIgnore, Path::new("PGH003_0.py"))] #[test_case(Rule::BlanketTypeIgnore, Path::new("PGH003_1.py"))] @@ -26,7 +27,25 @@ mod tests { Path::new("pygrep_hooks").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Rule::InvalidMockAccess, Path::new("PGH005_0.py"))] + fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("pygrep_hooks").join(path).as_path(), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs index b0fe9532d8961..e60fd420721c9 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs @@ -1,10 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::Cursor; use ruff_text_size::{Ranged, TextRange}; -use crate::noqa::{self, Directive, FileNoqaDirectives, NoqaDirectives}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::noqa::{self, Directive, FileNoqaDirectives, NoqaDirectives}; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Check for `noqa` annotations that suppress all diagnostics, as opposed to @@ -74,20 +75,20 @@ impl Violation for BlanketNOQA { /// PGH004 pub(crate) fn blanket_noqa( - diagnostics: &mut Vec, + context: &LintContext, noqa_directives: &NoqaDirectives, locator: &Locator, file_noqa_directives: &FileNoqaDirectives, ) { for line in file_noqa_directives.lines() { if let Directive::All(_) = line.parsed_file_exemption { - diagnostics.push(Diagnostic::new( + context.report_diagnostic( BlanketNOQA { missing_colon: false, file_exemption: true, }, line.range(), - )); + ); } } @@ -105,7 +106,7 @@ pub(crate) fn blanket_noqa( // Ex) `# noqa F401` let start = all.end(); let end = start + cursor.token_len(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = context.report_diagnostic( BlanketNOQA { missing_colon: true, file_exemption: false, @@ -113,16 +114,15 @@ pub(crate) fn blanket_noqa( TextRange::new(all.start(), end), ); diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(':'.to_string(), start))); - diagnostics.push(diagnostic); } else { // Otherwise, it looks like an intentional blanket `noqa` annotation. - diagnostics.push(Diagnostic::new( + context.report_diagnostic( BlanketNOQA { missing_colon: false, file_exemption: false, }, all.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs index a520720e2098d..efd12c7b3b9d9 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_type_ignore.rs @@ -1,15 +1,16 @@ use std::sync::LazyLock; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use memchr::memchr_iter; use regex::Regex; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_text_size::TextSize; use crate::Locator; +use crate::Violation; +use crate::checkers::ast::LintContext; /// ## What it does /// Check for `type: ignore` annotations that suppress all type warnings, as @@ -52,7 +53,7 @@ impl Violation for BlanketTypeIgnore { /// PGH003 pub(crate) fn blanket_type_ignore( - diagnostics: &mut Vec, + context: &LintContext, comment_ranges: &CommentRanges, locator: &Locator, ) { @@ -92,10 +93,10 @@ pub(crate) fn blanket_type_ignore( // Match the optional `[...]` tag. if let Ok(codes) = parse_type_ignore_tag(comment) { if codes.is_empty() { - diagnostics.push(Diagnostic::new( + context.report_diagnostic_if_enabled( BlanketTypeIgnore, range.add_start(TextSize::try_from(start).unwrap()), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index 7f4faef4a0879..648702121c43c 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::{FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::{FixAvailability, Violation}; /// ## Removed /// This rule is identical to [G010] which should be used instead. diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs index 102c9666bc921..9ff519116d36d 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/invalid_mock_access.rs @@ -1,10 +1,11 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; +use crate::preview::is_invalid_async_mock_access_check_enabled; #[derive(Debug, PartialEq, Eq)] enum Reason { @@ -51,7 +52,7 @@ impl Violation for InvalidMockAccess { /// PGH005 pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = expr { - if matches!( + let is_uncalled_mock_method = matches!( attr.as_str(), "assert_any_call" | "assert_called" @@ -60,13 +61,26 @@ pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) { | "assert_called_with" | "assert_has_calls" | "assert_not_called" - ) { - checker.report_diagnostic(Diagnostic::new( + ); + let is_uncalled_async_mock_method = + is_invalid_async_mock_access_check_enabled(checker.settings()) + && matches!( + attr.as_str(), + "assert_awaited" + | "assert_awaited_once" + | "assert_awaited_with" + | "assert_awaited_once_with" + | "assert_any_await" + | "assert_has_awaits" + | "assert_not_awaited" + ); + if is_uncalled_mock_method || is_uncalled_async_mock_method { + checker.report_diagnostic( InvalidMockAccess { reason: Reason::UncalledMethod(attr.to_string()), }, expr.range(), - )); + ); } } } @@ -81,7 +95,7 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) { }, _ => return, }; - if matches!( + let is_missing_mock_method = matches!( attr.as_str(), "any_call" | "called_once" @@ -89,12 +103,25 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) { | "called_with" | "has_calls" | "not_called" - ) { - checker.report_diagnostic(Diagnostic::new( + ); + let is_missing_async_mock_method = + is_invalid_async_mock_access_check_enabled(checker.settings()) + && matches!( + attr.as_str(), + "awaited" + | "awaited_once" + | "awaited_with" + | "awaited_once_with" + | "any_await" + | "has_awaits" + | "not_awaited" + ); + if is_missing_mock_method || is_missing_async_mock_method { + checker.report_diagnostic( InvalidMockAccess { reason: Reason::NonExistentMethod(attr.to_string()), }, test.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs index 65e2ec28b61e9..8615e4ae5102f 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/rules/no_eval.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## Removed /// This rule is identical to [S307] which should be used instead. diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH005_PGH005_0.py.snap b/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH005_PGH005_0.py.snap index 1d8c3f41fff02..f95f64df91936 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH005_PGH005_0.py.snap +++ b/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__PGH005_PGH005_0.py.snap @@ -1,90 +1,91 @@ --- source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs --- -PGH005_0.py:2:8: PGH005 Non-existent mock method: `not_called` +PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called` | -1 | # Errors -2 | assert my_mock.not_called() +2 | # ============ +3 | # Errors +4 | assert my_mock.not_called() | ^^^^^^^^^^^^^^^^^^^^ PGH005 -3 | assert my_mock.called_once_with() -4 | assert my_mock.not_called +5 | assert my_mock.called_once_with() +6 | assert my_mock.not_called | -PGH005_0.py:3:8: PGH005 Non-existent mock method: `called_once_with` +PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with` | -1 | # Errors -2 | assert my_mock.not_called() -3 | assert my_mock.called_once_with() +3 | # Errors +4 | assert my_mock.not_called() +5 | assert my_mock.called_once_with() | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 -4 | assert my_mock.not_called -5 | assert my_mock.called_once_with +6 | assert my_mock.not_called +7 | assert my_mock.called_once_with | -PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called` +PGH005_0.py:6:8: PGH005 Non-existent mock method: `not_called` | -2 | assert my_mock.not_called() -3 | assert my_mock.called_once_with() -4 | assert my_mock.not_called +4 | assert my_mock.not_called() +5 | assert my_mock.called_once_with() +6 | assert my_mock.not_called | ^^^^^^^^^^^^^^^^^^ PGH005 -5 | assert my_mock.called_once_with -6 | my_mock.assert_not_called +7 | assert my_mock.called_once_with +8 | my_mock.assert_not_called | -PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with` +PGH005_0.py:7:8: PGH005 Non-existent mock method: `called_once_with` | -3 | assert my_mock.called_once_with() -4 | assert my_mock.not_called -5 | assert my_mock.called_once_with +5 | assert my_mock.called_once_with() +6 | assert my_mock.not_called +7 | assert my_mock.called_once_with | ^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 -6 | my_mock.assert_not_called -7 | my_mock.assert_called +8 | my_mock.assert_not_called +9 | my_mock.assert_called | -PGH005_0.py:6:1: PGH005 Mock method should be called: `assert_not_called` - | -4 | assert my_mock.not_called -5 | assert my_mock.called_once_with -6 | my_mock.assert_not_called - | ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 -7 | my_mock.assert_called -8 | my_mock.assert_called_once_with - | +PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_not_called` + | + 6 | assert my_mock.not_called + 7 | assert my_mock.called_once_with + 8 | my_mock.assert_not_called + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 + 9 | my_mock.assert_called +10 | my_mock.assert_called_once_with + | -PGH005_0.py:7:1: PGH005 Mock method should be called: `assert_called` - | -5 | assert my_mock.called_once_with -6 | my_mock.assert_not_called -7 | my_mock.assert_called - | ^^^^^^^^^^^^^^^^^^^^^ PGH005 -8 | my_mock.assert_called_once_with -9 | my_mock.assert_called_once_with - | +PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called` + | + 7 | assert my_mock.called_once_with + 8 | my_mock.assert_not_called + 9 | my_mock.assert_called + | ^^^^^^^^^^^^^^^^^^^^^ PGH005 +10 | my_mock.assert_called_once_with +11 | my_mock.assert_called_once_with + | -PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_called_once_with` +PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with` | - 6 | my_mock.assert_not_called - 7 | my_mock.assert_called - 8 | my_mock.assert_called_once_with + 8 | my_mock.assert_not_called + 9 | my_mock.assert_called +10 | my_mock.assert_called_once_with | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 - 9 | my_mock.assert_called_once_with -10 | MyMock.assert_called_once_with +11 | my_mock.assert_called_once_with +12 | MyMock.assert_called_once_with | -PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called_once_with` +PGH005_0.py:11:1: PGH005 Mock method should be called: `assert_called_once_with` | - 7 | my_mock.assert_called - 8 | my_mock.assert_called_once_with - 9 | my_mock.assert_called_once_with + 9 | my_mock.assert_called +10 | my_mock.assert_called_once_with +11 | my_mock.assert_called_once_with | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 -10 | MyMock.assert_called_once_with +12 | MyMock.assert_called_once_with | -PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with` +PGH005_0.py:12:1: PGH005 Mock method should be called: `assert_called_once_with` | - 8 | my_mock.assert_called_once_with - 9 | my_mock.assert_called_once_with -10 | MyMock.assert_called_once_with +10 | my_mock.assert_called_once_with +11 | my_mock.assert_called_once_with +12 | MyMock.assert_called_once_with | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 -11 | -12 | # OK +13 | +14 | # OK | diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__preview__PGH005_PGH005_0.py.snap b/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__preview__PGH005_PGH005_0.py.snap new file mode 100644 index 0000000000000..e61728110cb4a --- /dev/null +++ b/crates/ruff_linter/src/rules/pygrep_hooks/snapshots/ruff_linter__rules__pygrep_hooks__tests__preview__PGH005_PGH005_0.py.snap @@ -0,0 +1,190 @@ +--- +source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs +--- +PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called` + | +2 | # ============ +3 | # Errors +4 | assert my_mock.not_called() + | ^^^^^^^^^^^^^^^^^^^^ PGH005 +5 | assert my_mock.called_once_with() +6 | assert my_mock.not_called + | + +PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with` + | +3 | # Errors +4 | assert my_mock.not_called() +5 | assert my_mock.called_once_with() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +6 | assert my_mock.not_called +7 | assert my_mock.called_once_with + | + +PGH005_0.py:6:8: PGH005 Non-existent mock method: `not_called` + | +4 | assert my_mock.not_called() +5 | assert my_mock.called_once_with() +6 | assert my_mock.not_called + | ^^^^^^^^^^^^^^^^^^ PGH005 +7 | assert my_mock.called_once_with +8 | my_mock.assert_not_called + | + +PGH005_0.py:7:8: PGH005 Non-existent mock method: `called_once_with` + | +5 | assert my_mock.called_once_with() +6 | assert my_mock.not_called +7 | assert my_mock.called_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +8 | my_mock.assert_not_called +9 | my_mock.assert_called + | + +PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_not_called` + | + 6 | assert my_mock.not_called + 7 | assert my_mock.called_once_with + 8 | my_mock.assert_not_called + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 + 9 | my_mock.assert_called +10 | my_mock.assert_called_once_with + | + +PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called` + | + 7 | assert my_mock.called_once_with + 8 | my_mock.assert_not_called + 9 | my_mock.assert_called + | ^^^^^^^^^^^^^^^^^^^^^ PGH005 +10 | my_mock.assert_called_once_with +11 | my_mock.assert_called_once_with + | + +PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with` + | + 8 | my_mock.assert_not_called + 9 | my_mock.assert_called +10 | my_mock.assert_called_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +11 | my_mock.assert_called_once_with +12 | MyMock.assert_called_once_with + | + +PGH005_0.py:11:1: PGH005 Mock method should be called: `assert_called_once_with` + | + 9 | my_mock.assert_called +10 | my_mock.assert_called_once_with +11 | my_mock.assert_called_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +12 | MyMock.assert_called_once_with + | + +PGH005_0.py:12:1: PGH005 Mock method should be called: `assert_called_once_with` + | +10 | my_mock.assert_called_once_with +11 | my_mock.assert_called_once_with +12 | MyMock.assert_called_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +13 | +14 | # OK + | + +PGH005_0.py:26:8: PGH005 Non-existent mock method: `not_awaited` + | +24 | # ================= +25 | # Errors +26 | assert my_mock.not_awaited() + | ^^^^^^^^^^^^^^^^^^^^^ PGH005 +27 | assert my_mock.awaited_once_with() +28 | assert my_mock.not_awaited + | + +PGH005_0.py:27:8: PGH005 Non-existent mock method: `awaited_once_with` + | +25 | # Errors +26 | assert my_mock.not_awaited() +27 | assert my_mock.awaited_once_with() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +28 | assert my_mock.not_awaited +29 | assert my_mock.awaited_once_with + | + +PGH005_0.py:28:8: PGH005 Non-existent mock method: `not_awaited` + | +26 | assert my_mock.not_awaited() +27 | assert my_mock.awaited_once_with() +28 | assert my_mock.not_awaited + | ^^^^^^^^^^^^^^^^^^^ PGH005 +29 | assert my_mock.awaited_once_with +30 | my_mock.assert_not_awaited + | + +PGH005_0.py:29:8: PGH005 Non-existent mock method: `awaited_once_with` + | +27 | assert my_mock.awaited_once_with() +28 | assert my_mock.not_awaited +29 | assert my_mock.awaited_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +30 | my_mock.assert_not_awaited +31 | my_mock.assert_awaited + | + +PGH005_0.py:30:1: PGH005 Mock method should be called: `assert_not_awaited` + | +28 | assert my_mock.not_awaited +29 | assert my_mock.awaited_once_with +30 | my_mock.assert_not_awaited + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +31 | my_mock.assert_awaited +32 | my_mock.assert_awaited_once_with + | + +PGH005_0.py:31:1: PGH005 Mock method should be called: `assert_awaited` + | +29 | assert my_mock.awaited_once_with +30 | my_mock.assert_not_awaited +31 | my_mock.assert_awaited + | ^^^^^^^^^^^^^^^^^^^^^^ PGH005 +32 | my_mock.assert_awaited_once_with +33 | my_mock.assert_awaited_once_with + | + +PGH005_0.py:32:1: PGH005 Mock method should be called: `assert_awaited_once_with` + | +30 | my_mock.assert_not_awaited +31 | my_mock.assert_awaited +32 | my_mock.assert_awaited_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +33 | my_mock.assert_awaited_once_with +34 | MyMock.assert_awaited_once_with + | + +PGH005_0.py:33:1: PGH005 Mock method should be called: `assert_awaited_once_with` + | +31 | my_mock.assert_awaited +32 | my_mock.assert_awaited_once_with +33 | my_mock.assert_awaited_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +34 | MyMock.assert_awaited_once_with +35 | assert my_mock.awaited + | + +PGH005_0.py:34:1: PGH005 Mock method should be called: `assert_awaited_once_with` + | +32 | my_mock.assert_awaited_once_with +33 | my_mock.assert_awaited_once_with +34 | MyMock.assert_awaited_once_with + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005 +35 | assert my_mock.awaited + | + +PGH005_0.py:35:8: PGH005 Non-existent mock method: `awaited` + | +33 | my_mock.assert_awaited_once_with +34 | MyMock.assert_awaited_once_with +35 | assert my_mock.awaited + | ^^^^^^^^^^^^^^^ PGH005 +36 | +37 | # OK + | diff --git a/crates/ruff_linter/src/rules/pylint/helpers.rs b/crates/ruff_linter/src/rules/pylint/helpers.rs index d2b04e6cf4b81..056d27b6024f4 100644 --- a/crates/ruff_linter/src/rules/pylint/helpers.rs +++ b/crates/ruff_linter/src/rules/pylint/helpers.rs @@ -1,6 +1,6 @@ use ruff_python_ast as ast; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::{visitor, Arguments, Expr, Stmt}; +use ruff_python_ast::{Arguments, Expr, Stmt, visitor}; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::{ScopeKind, SemanticModel}; use ruff_text_size::TextRange; diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 4c19774b04568..dd4c11f67c166 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -16,9 +16,9 @@ mod tests { use crate::registry::Rule; use crate::rules::{flake8_tidy_imports, pylint}; - use crate::assert_messages; - use crate::settings::types::PreviewMode; + use crate::assert_diagnostics; use crate::settings::LinterSettings; + use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))] @@ -168,6 +168,7 @@ mod tests { )] #[test_case(Rule::UselessElseOnLoop, Path::new("useless_else_on_loop.py"))] #[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"))] + #[test_case(Rule::UselessImportAlias, Path::new("import_aliasing_2/__init__.py"))] #[test_case(Rule::UselessReturn, Path::new("useless_return.py"))] #[test_case(Rule::UselessWithLock, Path::new("useless_with_lock.py"))] #[test_case(Rule::UnreachableCode, Path::new("unreachable.py"))] @@ -231,6 +232,7 @@ mod tests { Path::new("bad_staticmethod_argument.py") )] #[test_case(Rule::LenTest, Path::new("len_as_condition.py"))] + #[test_case(Rule::MissingMaxsplitArg, Path::new("missing_maxsplit_arg.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( @@ -245,7 +247,7 @@ mod tests { ..LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -256,7 +258,7 @@ mod tests { &LinterSettings::for_rule(Rule::ContinueInFinally) .with_target_version(PythonVersion::PY37), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -272,7 +274,7 @@ mod tests { ..LinterSettings::for_rule(Rule::MagicValueComparison) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -288,7 +290,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyArguments) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -301,7 +303,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyArguments) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -317,7 +319,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyPositionalArguments) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -333,7 +335,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyBranches) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -349,7 +351,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyBooleanExpressions) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -365,7 +367,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyStatements) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -381,7 +383,7 @@ mod tests { ..LinterSettings::for_rule(Rule::TooManyReturnStatements) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -397,7 +399,7 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::TooManyPublicMethods]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -413,7 +415,20 @@ mod tests { ..LinterSettings::for_rules(vec![Rule::TooManyLocals]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn preview_useless_import_alias() -> Result<()> { + let diagnostics = test_path( + Path::new("pylint/import_aliasing_2/__init__.py"), + &LinterSettings { + preview: PreviewMode::Enabled, + ..LinterSettings::for_rule(Rule::UselessImportAlias) + }, + )?; + assert_diagnostics!(diagnostics); Ok(()) } @@ -437,7 +452,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs b/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs index 8baf775f86bc8..bc8bed77c7767 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/and_or_ternary.rs @@ -1,6 +1,7 @@ -use ruff_diagnostics::Violation; use ruff_macros::ViolationMetadata; +use crate::Violation; + /// ## Removal /// This rule was removed from Ruff because it was common for it to introduce behavioral changes. /// See [#9007](https://github.com/astral-sh/ruff/issues/9007) for more information. diff --git a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs index fea14ffb4af14..64db3f1e400bc 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq, Copy, Clone)] @@ -49,7 +49,7 @@ impl Violation for AssertOnStringLiteral { pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { match test { Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( AssertOnStringLiteral { kind: if value.is_empty() { Kind::Empty @@ -58,10 +58,10 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { }, }, test.range(), - )); + ); } Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( AssertOnStringLiteral { kind: if value.is_empty() { Kind::Empty @@ -70,17 +70,17 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { }, }, test.range(), - )); + ); } Expr::FString(ast::ExprFString { value, .. }) => { let kind = if value.iter().all(|f_string_part| match f_string_part { ast::FStringPart::Literal(literal) => literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().all(|element| match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { - value, .. - }) => value.is_empty(), - ast::FStringElement::Expression(_) => false, + ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) => value.is_empty(), + ast::InterpolatedStringElement::Interpolation(_) => false, }) } }) { @@ -89,10 +89,10 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { ast::FStringPart::Literal(literal) => !literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().any(|element| match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { - value, .. - }) => !value.is_empty(), - ast::FStringElement::Expression(_) => false, + ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) => !value.is_empty(), + ast::InterpolatedStringElement::Interpolation(_) => false, }) } }) { @@ -100,10 +100,7 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { } else { Kind::Unknown }; - checker.report_diagnostic(Diagnostic::new( - AssertOnStringLiteral { kind }, - test.range(), - )); + checker.report_diagnostic(AssertOnStringLiteral { kind }, test.range()); } _ => {} } diff --git a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs index fd73d2ab69ad3..e41275a1cab92 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## What it does /// Checks for uses of `await` outside `async` functions. diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs index 468d0afd4bd62..b4ee0a43328ef 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_dunder_method_name.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::is_known_dunder_method; @@ -69,7 +69,7 @@ pub(crate) fn bad_dunder_method_name(checker: &Checker, method: &ast::StmtFuncti // If the name is explicitly allowed, skip it. if is_known_dunder_method(&method.name) || checker - .settings + .settings() .pylint .allow_dunder_method_names .contains(method.name.as_str()) @@ -82,10 +82,10 @@ pub(crate) fn bad_dunder_method_name(checker: &Checker, method: &ast::StmtFuncti return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadDunderMethodName { name: method.name.to_string(), }, method.identifier(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs index 3d0aa801ba01a..b89cbf142565d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::open_mode::OpenMode; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -24,13 +24,14 @@ use crate::checkers::ast::Checker; /// ## Example /// ```python /// with open("file", "rwx") as f: -/// return f.read() +/// content = f.read() /// ``` /// /// Use instead: +/// /// ```python /// with open("file", "r") as f: -/// return f.read() +/// content = f.read() /// ``` /// /// ## References @@ -66,12 +67,12 @@ pub(crate) fn bad_open_mode(checker: &Checker, call: &ast::ExprCall) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadOpenMode { mode: value.to_string(), }, mode.range(), - )); + ); } #[derive(Debug, Copy, Clone)] diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs index 51b7ca0b7f481..4108cd8c74c2a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::ParameterWithDefault; +use ruff_python_semantic::Scope; use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::analyze::function_type::FunctionType; -use ruff_python_semantic::Scope; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -71,8 +71,8 @@ pub(crate) fn bad_staticmethod_argument(checker: &Checker, scope: &Scope) { decorator_list, parent, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); match type_ { @@ -101,10 +101,10 @@ pub(crate) fn bad_staticmethod_argument(checker: &Checker, scope: &Scope) { _ => return, } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadStaticmethodArgument { argument_name: self_or_cls.name.to_string(), }, self_or_cls.range(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs index dc1f0269e126f..b251bab3236a6 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs @@ -3,12 +3,12 @@ use std::fmt; use ruff_python_ast::{self as ast, Expr}; use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_semantic::analyze::typing; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use ruff_python_ast::PythonVersion; @@ -211,7 +211,5 @@ pub(crate) fn bad_str_strip_call(checker: &Checker, call: &ast::ExprCall) { None }; - let diagnostic = Diagnostic::new(BadStrStripCall { strip, removal }, arg.range()); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(BadStrStripCall { strip, removal }, arg.range()); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs index 8a3ee38847330..a94e8290e9302 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs @@ -1,7 +1,6 @@ use std::str::FromStr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprStringLiteral, StringFlags, StringLiteral}; use ruff_python_literal::{ cformat::{CFormatErrorType, CFormatString}, @@ -11,6 +10,7 @@ use ruff_python_literal::{ }; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -50,7 +50,7 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { match FormatSpec::parse(format_spec) { Err(FormatSpecError::InvalidFormatType) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadStringFormatCharacter { // The format type character is always the last one. // More info in the official spec: @@ -58,7 +58,7 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { format_char: format_spec.chars().last().unwrap(), }, range, - )); + ); } Err(_) => {} Ok(FormatSpec::Static(_)) => {} @@ -70,7 +70,7 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { if let Err(FormatSpecError::InvalidFormatType) = FormatSpec::parse(&format_spec) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( BadStringFormatCharacter { // The format type character is always the last one. // More info in the official spec: @@ -78,7 +78,7 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { format_char: format_spec.chars().last().unwrap(), }, range, - )); + ); } } } @@ -92,6 +92,7 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { pub(crate) fn percent(checker: &Checker, expr: &Expr, format_string: &ExprStringLiteral) { for StringLiteral { value: _, + node_index: _, range, flags, } in &format_string.value @@ -103,10 +104,7 @@ pub(crate) fn percent(checker: &Checker, expr: &Expr, format_string: &ExprString // Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`. if let Err(format_error) = CFormatString::from_str(string) { if let CFormatErrorType::UnsupportedFormatChar(format_char) = format_error.typ { - checker.report_diagnostic(Diagnostic::new( - BadStringFormatCharacter { format_char }, - expr.range(), - )); + checker.report_diagnostic(BadStringFormatCharacter { format_char }, expr.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs index 70e22b2e61bd4..a747b9b12ae72 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs @@ -5,10 +5,10 @@ use ruff_python_literal::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, use ruff_text_size::Ranged; use rustc_hash::FxHashMap; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -219,6 +219,7 @@ pub(crate) fn bad_string_format_type( let mut format_strings = vec![]; for StringLiteral { value: _, + node_index: _, range, flags, } in &format_string.value @@ -236,10 +237,14 @@ pub(crate) fn bad_string_format_type( // Parse the parameters. let is_valid = match &*bin_op.right { Expr::Tuple(ast::ExprTuple { elts, .. }) => is_valid_tuple(&format_strings, elts), - Expr::Dict(ast::ExprDict { items, range: _ }) => is_valid_dict(&format_strings, items), + Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) => is_valid_dict(&format_strings, items), _ => is_valid_constant(&format_strings, &bin_op.right), }; if !is_valid { - checker.report_diagnostic(Diagnostic::new(BadStringFormatType, bin_op.range())); + checker.report_diagnostic(BadStringFormatType, bin_op.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs index 09d775b1e9513..e5ed24bc189af 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bidirectional_unicode.rs @@ -1,7 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::Line; +use crate::{Violation, checkers::ast::LintContext}; + const BIDI_UNICODE: [char; 10] = [ '\u{202A}', //{LEFT-TO-RIGHT EMBEDDING} '\u{202B}', //{RIGHT-TO-LEFT EMBEDDING} @@ -52,10 +53,8 @@ impl Violation for BidirectionalUnicode { } /// PLE2502 -pub(crate) fn bidirectional_unicode(line: &Line) -> Vec { - let mut diagnostics = Vec::new(); +pub(crate) fn bidirectional_unicode(line: &Line, context: &LintContext) { if line.contains(BIDI_UNICODE) { - diagnostics.push(Diagnostic::new(BidirectionalUnicode, line.full_range())); + context.report_diagnostic(BidirectionalUnicode, line.full_range()); } - diagnostics } diff --git a/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs b/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs index f7de7044f307a..eefae02792e89 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/binary_op_exception.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, ExceptHandler, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq, Copy, Clone)] @@ -76,8 +76,5 @@ pub(crate) fn binary_op_exception(checker: &Checker, except_handler: &ExceptHand return; }; - checker.report_diagnostic(Diagnostic::new( - BinaryOpException { op: op.into() }, - type_.range(), - )); + checker.report_diagnostic(BinaryOpException { op: op.into() }, type_.range()); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs index 545c62fe4ac8f..1795e7ed57f02 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/boolean_chained_comparison.rs @@ -1,13 +1,13 @@ use itertools::Itertools; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ - parenthesize::{parentheses_iterator, parenthesized_range}, BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, + parenthesize::{parentheses_iterator, parenthesized_range}, }; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Check for chained boolean operations that can be simplified. @@ -69,96 +69,93 @@ pub(crate) fn boolean_chained_comparison(checker: &Checker, expr_bool_op: &ExprB .iter() .map(|expr| expr.as_compare_expr().unwrap()); - let diagnostics = compare_expressions - .tuple_windows() - .filter(|(left_compare, right_compare)| { - are_compare_expr_simplifiable(left_compare, right_compare) - }) - .filter_map(|(left_compare, right_compare)| { - let Expr::Name(left_compare_right) = left_compare.comparators.last()? else { - return None; - }; - - let Expr::Name(right_compare_left) = &*right_compare.left else { - return None; - }; - - if left_compare_right.id() != right_compare_left.id() { - return None; + for (left_compare, right_compare) in + compare_expressions + .tuple_windows() + .filter(|(left_compare, right_compare)| { + are_compare_expr_simplifiable(left_compare, right_compare) + }) + { + let Some(Expr::Name(left_compare_right)) = left_compare.comparators.last() else { + continue; + }; + + let Expr::Name(right_compare_left) = &*right_compare.left else { + continue; + }; + + if left_compare_right.id() != right_compare_left.id() { + continue; + } + + let left_paren_count = parentheses_iterator( + left_compare.into(), + Some(expr_bool_op.into()), + comment_ranges, + locator.contents(), + ) + .count(); + + let right_paren_count = parentheses_iterator( + right_compare.into(), + Some(expr_bool_op.into()), + comment_ranges, + locator.contents(), + ) + .count(); + + // Create the edit that removes the comparison operator + + // In `a<(b) and ((b)) { + let balance_parens_edit = Edit::insertion( + "(".repeat(right_paren_count - left_paren_count), + left_compare.start(), + ); + Fix::safe_edits(edit, [balance_parens_edit]) } + std::cmp::Ordering::Equal => Fix::safe_edit(edit), + std::cmp::Ordering::Greater => { + let balance_parens_edit = Edit::insertion( + ")".repeat(left_paren_count - right_paren_count), + right_compare.end(), + ); + Fix::safe_edits(edit, [balance_parens_edit]) + } + }; - let left_paren_count = parentheses_iterator( - left_compare.into(), - Some(expr_bool_op.into()), - comment_ranges, - locator.contents(), - ) - .count(); - - let right_paren_count = parentheses_iterator( - right_compare.into(), - Some(expr_bool_op.into()), - comment_ranges, - locator.contents(), - ) - .count(); - - // Create the edit that removes the comparison operator + let mut diagnostic = checker.report_diagnostic( + BooleanChainedComparison, + TextRange::new(left_compare.start(), right_compare.end()), + ); - // In `a<(b) and ((b)) { - let balance_parens_edit = Edit::insertion( - "(".repeat(right_paren_count - left_paren_count), - left_compare.start(), - ); - Fix::safe_edits(edit, [balance_parens_edit]) - } - std::cmp::Ordering::Equal => Fix::safe_edit(edit), - std::cmp::Ordering::Greater => { - let balance_parens_edit = Edit::insertion( - ")".repeat(left_paren_count - right_paren_count), - right_compare.end(), - ); - Fix::safe_edits(edit, [balance_parens_edit]) - } - }; - - let mut diagnostic = Diagnostic::new( - BooleanChainedComparison, - TextRange::new(left_compare.start(), right_compare.end()), - ); - - diagnostic.set_fix(fix); - - Some(diagnostic) - }); - - checker.report_diagnostics(diagnostics); + diagnostic.set_fix(fix); + } } /// Checks whether two compare expressions are simplifiable diff --git a/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs b/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs index 8fba531329778..72b15426a7774 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/collapsible_else_if.rs @@ -1,17 +1,17 @@ use anyhow::Result; use ast::whitespace::indentation; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, ElifElseClause, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::fix::edits::adjust_indentation; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `else` blocks that consist of a single `if` statement. @@ -82,7 +82,7 @@ pub(crate) fn collapsible_else_if(checker: &Checker, stmt: &Stmt) { return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CollapsibleElseIf, TextRange::new(else_clause.start(), first.start()), ); @@ -95,7 +95,6 @@ pub(crate) fn collapsible_else_if(checker: &Checker, stmt: &Stmt) { checker.stylist(), ) }); - checker.report_diagnostic(diagnostic); } /// Generate [`Fix`] to convert an `else` block to an `elif` block. diff --git a/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs index 51a74aa06144e..94217d05ced7d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/compare_to_empty_string.rs @@ -2,10 +2,10 @@ use anyhow::bail; use itertools::Itertools; use ruff_python_ast::{self as ast, CmpOp, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -89,13 +89,13 @@ pub(crate) fn compare_to_empty_string( let expr = checker.generator().expr(rhs); let existing = format!("{literal} {op} {expr}"); let replacement = format!("{}{expr}", op.into_unary()); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( CompareToEmptyString { existing, replacement, }, lhs.range(), - )); + ); } } } @@ -107,13 +107,13 @@ pub(crate) fn compare_to_empty_string( let literal = checker.generator().expr(rhs); let existing = format!("{expr} {op} {literal}"); let replacement = format!("{}{expr}", op.into_unary()); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( CompareToEmptyString { existing, replacement, }, rhs.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs b/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs index c42f7b8c5da95..8eab156203d44 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/comparison_of_constant.rs @@ -1,10 +1,10 @@ use itertools::Itertools; use ruff_python_ast::{CmpOp, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -62,7 +62,7 @@ pub(crate) fn comparison_of_constant( .zip(ops) { if left.is_literal_expr() && right.is_literal_expr() { - let diagnostic = Diagnostic::new( + checker.report_diagnostic( ComparisonOfConstant { left_constant: checker.generator().expr(left), op: *op, @@ -70,8 +70,6 @@ pub(crate) fn comparison_of_constant( }, left.range(), ); - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs b/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs index dbc2f76868c76..60697e17cd99e 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/comparison_with_itself.rs @@ -1,12 +1,12 @@ use itertools::Itertools; -use crate::fix::snippet::SourceCodeSnippet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{CmpOp, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; +use crate::fix::snippet::SourceCodeSnippet; /// ## What it does /// Checks for operations that compare a name to itself. @@ -67,12 +67,12 @@ pub(crate) fn comparison_with_itself( op, checker.locator().slice(right) ); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( ComparisonWithItself { actual: SourceCodeSnippet::new(actual), }, left_name.range(), - )); + ); } // Ex) `id(foo) == id(foo)` (Expr::Call(left_call), Expr::Call(right_call)) => { @@ -115,12 +115,12 @@ pub(crate) fn comparison_with_itself( op, checker.locator().slice(right) ); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( ComparisonWithItself { actual: SourceCodeSnippet::new(actual), }, left_call.range(), - )); + ); } } _ => {} diff --git a/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs b/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs index ea2ed3a14a128..a89baf379edd2 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/continue_in_finally.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -49,7 +49,7 @@ impl Violation for ContinueInFinally { fn traverse_body(checker: &Checker, body: &[Stmt]) { for stmt in body { if stmt.is_continue_stmt() { - checker.report_diagnostic(Diagnostic::new(ContinueInFinally, stmt.range())); + checker.report_diagnostic(ContinueInFinally, stmt.range()); } match stmt { diff --git a/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs b/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs index 61430227e64df..c30ac6883e6e7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs @@ -1,16 +1,15 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::{ - self as ast, + self as ast, Expr, ExprContext, visitor::{self, Visitor}, - Expr, ExprContext, }; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_python_semantic::analyze::typing::is_dict; -use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -46,7 +45,6 @@ use crate::checkers::ast::Checker; /// for instrument, section in ORCHESTRA.items(): /// print(f"{instrument}: {section}") /// ``` - #[derive(ViolationMetadata)] pub(crate) struct DictIndexMissingItems; @@ -87,8 +85,7 @@ pub(crate) fn dict_index_missing_items(checker: &Checker, stmt_for: &ast::StmtFo }; if has_violation { - let diagnostic = Diagnostic::new(DictIndexMissingItems, stmt_for.range()); - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(DictIndexMissingItems, stmt_for.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs b/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs index f19d7e7eb71de..60c7b91d048c0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/dict_iter_missing_items.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{Expr, Stmt}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::typing::is_dict; use ruff_python_semantic::{Binding, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for dictionary unpacking in a for loop without calling `.items()`. @@ -94,12 +94,11 @@ pub(crate) fn dict_iter_missing_items(checker: &Checker, target: &Expr, iter: &E return; } - let mut diagnostic = Diagnostic::new(DictIterMissingItems, iter.range()); + let mut diagnostic = checker.report_diagnostic(DictIterMissingItems, iter.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( format!("{}.items()", name.id), iter.range(), ))); - checker.report_diagnostic(diagnostic); } /// Returns true if the binding is a dictionary where each key is a tuple with two elements. diff --git a/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs b/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs index dcefe934508e7..e4a5192df2930 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/duplicate_bases.rs @@ -1,12 +1,13 @@ +use ruff_diagnostics::Applicability; use ruff_python_ast::{self as ast, Arguments, Expr}; use rustc_hash::{FxBuildHasher, FxHashSet}; -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Fix, FixAvailability, Violation}; /// ## What it does /// Checks for duplicate base classes in class definitions. @@ -34,6 +35,23 @@ use crate::fix::edits::{remove_argument, Parentheses}; /// pass /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the +/// base classes, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// class Foo: +/// pass +/// +/// +/// class Bar( +/// Foo, # comment +/// Foo, +/// ): +/// pass +/// ``` +/// /// ## References /// - [Python documentation: Class definitions](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) #[derive(ViolationMetadata)] @@ -67,7 +85,7 @@ pub(crate) fn duplicate_bases(checker: &Checker, name: &str, arguments: Option<& for base in bases { if let Expr::Name(ast::ExprName { id, .. }) = base { if !seen.insert(id) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DuplicateBases { base: id.to_string(), class: name.to_string(), @@ -80,10 +98,19 @@ pub(crate) fn duplicate_bases(checker: &Checker, name: &str, arguments: Option<& arguments, Parentheses::Remove, checker.locator().contents(), + checker.comment_ranges(), ) - .map(Fix::safe_edit) + .map(|edit| { + Fix::applicable_edit( + edit, + if checker.comment_ranges().intersects(arguments.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + ) + }) }); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs b/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs index f36265fb4ae4f..5d09f2e8a731f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/empty_comment.rs @@ -1,10 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_trivia::{is_python_whitespace, CommentRanges}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_trivia::{CommentRanges, is_python_whitespace}; use ruff_source_file::LineRanges; use ruff_text_size::{TextRange, TextSize}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for a # symbol appearing on a line not followed by an actual comment. @@ -45,7 +46,7 @@ impl Violation for EmptyComment { /// PLR2044 pub(crate) fn empty_comments( - diagnostics: &mut Vec, + context: &LintContext, comment_ranges: &CommentRanges, locator: &Locator, ) { @@ -58,14 +59,12 @@ pub(crate) fn empty_comments( } // If the line contains an empty comment, add a diagnostic. - if let Some(diagnostic) = empty_comment(range, locator) { - diagnostics.push(diagnostic); - } + empty_comment(context, range, locator); } } /// Return a [`Diagnostic`] if the comment at the given [`TextRange`] is empty. -fn empty_comment(range: TextRange, locator: &Locator) -> Option { +fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator) { // Check: is the comment empty? if !locator .slice(range) @@ -73,7 +72,7 @@ fn empty_comment(range: TextRange, locator: &Locator) -> Option { .skip(1) .all(is_python_whitespace) { - return None; + return; } // Find the location of the `#`. @@ -96,13 +95,15 @@ fn empty_comment(range: TextRange, locator: &Locator) -> Option { } }); - Some( - Diagnostic::new(EmptyComment, TextRange::new(first_hash_col, line.end())).with_fix( - Fix::safe_edit(if let Some(deletion_start_col) = deletion_start_col { + if let Some(mut diagnostic) = context + .report_diagnostic_if_enabled(EmptyComment, TextRange::new(first_hash_col, line.end())) + { + diagnostic.set_fix(Fix::safe_edit( + if let Some(deletion_start_col) = deletion_start_col { Edit::deletion(line.start() + deletion_start_col, line.end()) } else { Edit::range_deletion(locator.full_line_range(first_hash_col)) - }), - ), - ) + }, + )); + } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs b/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs index 695429683de6d..69401e854d7bf 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/eq_without_hash.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ Expr, ExprName, Identifier, StmtAnnAssign, StmtAssign, StmtClassDef, StmtFunctionDef, }; use ruff_python_semantic::analyze::class::{ - any_member_declaration, ClassMemberBoundness, ClassMemberKind, + ClassMemberBoundness, ClassMemberKind, any_member_declaration, }; use ruff_text_size::Ranged; use std::ops::BitOr; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -16,10 +16,10 @@ use crate::checkers::ast::Checker; /// /// ## Why is this bad? /// A class that implements `__eq__` but not `__hash__` will have its hash -/// method implicitly set to `None`, regardless of if a super class defines -/// `__hash__`. This will cause the class to be unhashable, will in turn -/// cause issues when using the class as a key in a dictionary or a member -/// of a set. +/// method implicitly set to `None`, regardless of if a superclass defines +/// `__hash__`. This will cause the class to be unhashable, which will in turn +/// cause issues when using instances of the class as keys in a dictionary or +/// members of a set. /// /// ## Example /// @@ -46,52 +46,16 @@ use crate::checkers::ast::Checker; /// return hash(self.name) /// ``` /// -/// This issue is particularly tricky with inheritance. Even if a parent class correctly implements -/// both `__eq__` and `__hash__`, overriding `__eq__` in a child class without also implementing -/// `__hash__` will make the child class unhashable: +/// In general, it is unsound to inherit a `__hash__` implementation from a parent class while +/// overriding the `__eq__` implementation because the two must be kept in sync. However, an easy +/// way to resolve this error in cases where it _is_ sound is to explicitly set `__hash__` to the +/// parent class's implementation: /// /// ```python -/// class Person: -/// def __init__(self): -/// self.name = "monty" -/// -/// def __eq__(self, other): -/// return isinstance(other, Person) and other.name == self.name -/// -/// def __hash__(self): -/// return hash(self.name) -/// -/// /// class Developer(Person): -/// def __init__(self): -/// super().__init__() -/// self.language = "python" +/// def __init__(self): ... /// -/// def __eq__(self, other): -/// return ( -/// super().__eq__(other) -/// and isinstance(other, Developer) -/// and self.language == other.language -/// ) -/// -/// -/// hash(Developer()) # TypeError: unhashable type: 'Developer' -/// ``` -/// -/// One way to fix this is to retain the implementation of `__hash__` from the parent class: -/// -/// ```python -/// class Developer(Person): -/// def __init__(self): -/// super().__init__() -/// self.language = "python" -/// -/// def __eq__(self, other): -/// return ( -/// super().__eq__(other) -/// and isinstance(other, Developer) -/// and self.language == other.language -/// ) +/// def __eq__(self, other): ... /// /// __hash__ = Person.__hash__ /// ``` @@ -109,7 +73,7 @@ impl Violation for EqWithoutHash { } } -/// W1641 +/// PLW1641 pub(crate) fn object_without_hash_method(checker: &Checker, class: &StmtClassDef) { if checker.source_type.is_stub() { return; @@ -122,8 +86,7 @@ pub(crate) fn object_without_hash_method(checker: &Checker, class: &StmtClassDef hash: HasMethod::No } ) { - let diagnostic = Diagnostic::new(EqWithoutHash, class.name.range()); - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(EqWithoutHash, class.name.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs b/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs index 5237f47c01541..c92593740fc5b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/global_at_module_level.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -27,6 +27,6 @@ impl Violation for GlobalAtModuleLevel { /// PLW0604 pub(crate) fn global_at_module_level(checker: &Checker, stmt: &Stmt) { if checker.semantic().current_scope().kind.is_module() { - checker.report_diagnostic(Diagnostic::new(GlobalAtModuleLevel, stmt.range())); + checker.report_diagnostic(GlobalAtModuleLevel, stmt.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs b/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs index 6fe46484ddeea..dfa63cd427270 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/global_statement.rs @@ -1,6 +1,6 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -31,8 +31,9 @@ use crate::checkers::ast::Checker; /// /// /// def foo(): +/// var = 10 /// print(var) -/// return 10 +/// return var /// /// /// var = foo() @@ -54,13 +55,13 @@ impl Violation for GlobalStatement { /// PLW0603 pub(crate) fn global_statement(checker: &Checker, name: &str) { if let Some(range) = checker.semantic().global(name) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( GlobalStatement { name: name.to_string(), }, // Match Pylint's behavior by reporting on the `global` statement`, rather // than the variable usage. range, - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs b/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs index 17a9f828b7494..a5421541f3df9 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/global_variable_not_assigned.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_semantic::{ResolvedReference, Scope}; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for `global` variables that are not assigned a value in the current @@ -37,7 +41,7 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[derive(ViolationMetadata)] pub(crate) struct GlobalVariableNotAssigned { - pub name: String, + name: String, } impl Violation for GlobalVariableNotAssigned { @@ -47,3 +51,34 @@ impl Violation for GlobalVariableNotAssigned { format!("Using global for `{name}` but no assignment is done") } } + +/// PLW0602 +pub(crate) fn global_variable_not_assigned(checker: &Checker, scope: &Scope) { + for (name, binding_id) in scope.bindings() { + let binding = checker.semantic().binding(binding_id); + // If the binding is a `global`, then it's a top-level `global` that was never + // assigned in the current scope. If it were assigned, the `global` would be + // shadowed by the assignment. + if binding.kind.is_global() { + // If the binding was conditionally deleted, it will include a reference within + // a `Del` context, but won't be shadowed by a `BindingKind::Deletion`, as in: + // ```python + // if condition: + // del var + // ``` + if binding + .references + .iter() + .map(|id| checker.semantic().reference(*id)) + .all(ResolvedReference::is_load) + { + checker.report_diagnostic( + GlobalVariableNotAssigned { + name: (*name).to_string(), + }, + binding.range(), + ); + } + } + } +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs index 4e64179d1e373..4fc2bf484e75e 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, CmpOp, Stmt}; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `if` statements that can be replaced with `min()` or `max()` @@ -29,6 +29,19 @@ use crate::fix::snippet::SourceCodeSnippet; /// highest_score = max(highest_score, score) /// ``` /// +/// ## Fix safety +/// This fix is marked unsafe if it would delete any comments within the replacement range. +/// +/// An example to illustrate where comments are preserved and where they are not: +/// +/// ```py +/// a, b = 0, 10 +/// +/// if a >= b: # deleted comment +/// # deleted comment +/// a = b # preserved comment +/// ``` +/// /// ## References /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) @@ -67,24 +80,27 @@ impl Violation for IfStmtMinMax { } } -/// R1730, R1731 +/// PLR1730, PLR1731 pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) { let ast::StmtIf { test, body, elif_else_clauses, range: _, + node_index: _, } = stmt_if; if !elif_else_clauses.is_empty() { return; } - let [body @ Stmt::Assign(ast::StmtAssign { - targets: body_targets, - value: body_value, - .. - })] = body.as_slice() + let [ + body @ Stmt::Assign(ast::StmtAssign { + targets: body_targets, + value: body_value, + .. + }), + ] = body.as_slice() else { return; }; @@ -161,7 +177,7 @@ pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) { checker.locator().slice(arg2), ); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfStmtMinMax { min_max, replacement: SourceCodeSnippet::from_str(replacement.as_str()), @@ -169,14 +185,19 @@ pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) { stmt_if.range(), ); + let range_replacement = stmt_if.range(); + let applicability = if checker.comment_ranges().intersects(range_replacement) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + if checker.semantic().has_builtin_binding(min_max.as_str()) { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - replacement, - stmt_if.range(), - ))); + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement(replacement, range_replacement), + applicability, + )); } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs b/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs index 1ebe84b6e5c63..57b7fd2b27c23 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_outside_top_level.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; use ruff_text_size::Ranged; +use crate::Violation; use crate::rules::flake8_tidy_imports::rules::BannedModuleImportPolicies; use crate::{ checkers::ast::Checker, codes::Rule, rules::flake8_tidy_imports::matchers::NameMatchPolicy, @@ -32,7 +32,7 @@ use crate::{ /// def print_python_version(): /// import platform /// -/// print(python.python_version()) +/// print(platform.python_version()) /// ``` /// /// Use instead: @@ -41,9 +41,16 @@ use crate::{ /// /// /// def print_python_version(): -/// print(python.python_version()) +/// print(platform.python_version()) /// ``` /// +/// ## See also +/// This rule will ignore import statements configured in +/// [`lint.flake8-tidy-imports.banned-module-level-imports`][banned-module-level-imports] +/// if the rule [`banned-module-level-imports`][TID253] is enabled. +/// +/// [banned-module-level-imports]: https://docs.astral.sh/ruff/settings/#lint_flake8-tidy-imports_banned-module-level-imports +/// [TID253]: https://docs.astral.sh/ruff/rules/banned-module-level-imports/ /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[derive(ViolationMetadata)] pub(crate) struct ImportOutsideTopLevel; @@ -55,7 +62,7 @@ impl Violation for ImportOutsideTopLevel { } } -/// C0415 +/// PLC0415 pub(crate) fn import_outside_top_level(checker: &Checker, stmt: &Stmt) { if checker.semantic().current_scope().kind.is_module() { // "Top-level" imports are allowed @@ -64,7 +71,7 @@ pub(crate) fn import_outside_top_level(checker: &Checker, stmt: &Stmt) { // Check if any of the non-top-level imports are banned by TID253 // before emitting the diagnostic to avoid conflicts. - if checker.enabled(Rule::BannedModuleLevelImports) { + if checker.is_rule_enabled(Rule::BannedModuleLevelImports) { let mut all_aliases_banned = true; let mut has_alias = false; for (policy, node) in &BannedModuleImportPolicies::new(stmt, checker) { @@ -84,14 +91,14 @@ pub(crate) fn import_outside_top_level(checker: &Checker, stmt: &Stmt) { } // Emit the diagnostic - checker.report_diagnostic(Diagnostic::new(ImportOutsideTopLevel, stmt.range())); + checker.report_diagnostic(ImportOutsideTopLevel, stmt.range()); } fn is_banned_module_level_import(policy: &NameMatchPolicy, checker: &Checker) -> bool { policy .find( checker - .settings + .settings() .flake8_tidy_imports .banned_module_level_imports(), ) diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs index 78beec5e92c96..dab7b51793158 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs @@ -2,12 +2,12 @@ use std::borrow::Cow; use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_python_semantic::{FromImport, Import, Imported, ResolvedReference, Scope}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::package::PackageRoot; @@ -136,10 +136,7 @@ pub(crate) fn import_private_name(checker: &Checker, scope: &Scope) { } else { None }; - checker.report_diagnostic(Diagnostic::new( - ImportPrivateName { name, module }, - binding.range(), - )); + checker.report_diagnostic(ImportPrivateName { name, module }, binding.range()); } } @@ -151,7 +148,7 @@ fn is_typing(reference: &ResolvedReference) -> bool { || reference.in_runtime_evaluated_annotation() } -#[allow(clippy::struct_field_names)] +#[expect(clippy::struct_field_names)] struct ImportInfo<'a> { module_name: &'a [&'a str], member_name: Cow<'a, str>, diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_self.rs b/crates/ruff_linter/src/rules/pylint/rules/import_self.rs index 62b9c1e54f6cd..03ba035f3e3f9 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_self.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_self.rs @@ -1,10 +1,11 @@ use ruff_python_ast::Alias; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::resolve_imported_module_path; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for import statements that import the current module. /// @@ -35,30 +36,36 @@ impl Violation for ImportSelf { } /// PLW0406 -pub(crate) fn import_self(alias: &Alias, module_path: Option<&[String]>) -> Option { - let module_path = module_path?; +pub(crate) fn import_self(checker: &Checker, alias: &Alias, module_path: Option<&[String]>) { + let Some(module_path) = module_path else { + return; + }; if alias.name.split('.').eq(module_path) { - return Some(Diagnostic::new( + checker.report_diagnostic( ImportSelf { name: alias.name.to_string(), }, alias.range(), - )); + ); } - - None } /// PLW0406 pub(crate) fn import_from_self( + checker: &Checker, level: u32, module: Option<&str>, names: &[Alias], module_path: Option<&[String]>, -) -> Option { - let module_path = module_path?; - let imported_module_path = resolve_imported_module_path(level, module, Some(module_path))?; +) { + let Some(module_path) = module_path else { + return; + }; + let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) + else { + return; + }; if imported_module_path .split('.') @@ -68,14 +75,12 @@ pub(crate) fn import_from_self( .iter() .find(|alias| alias.name == module_path[module_path.len() - 1]) { - return Some(Diagnostic::new( + checker.report_diagnostic( ImportSelf { name: format!("{}.{}", imported_module_path, alias.name), }, alias.range(), - )); + ); } } - - None } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs index 2cf8ed1688c8b..e338280462d0b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_format.rs @@ -1,8 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Binding; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for invalid assignments to `__all__`. /// @@ -36,10 +37,8 @@ impl Violation for InvalidAllFormat { } /// PLE0605 -pub(crate) fn invalid_all_format(binding: &Binding) -> Option { +pub(crate) fn invalid_all_format(checker: &Checker, binding: &Binding) { if binding.is_invalid_all_format() { - Some(Diagnostic::new(InvalidAllFormat, binding.range())) - } else { - None + checker.report_diagnostic(InvalidAllFormat, binding.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs index e3d17caa28dd8..99d83fe98b6e1 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_all_object.rs @@ -1,8 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Binding; use ruff_text_size::Ranged; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for the inclusion of invalid objects in `__all__`. /// @@ -36,10 +37,8 @@ impl Violation for InvalidAllObject { } /// PLE0604 -pub(crate) fn invalid_all_object(binding: &Binding) -> Option { +pub(crate) fn invalid_all_object(checker: &Checker, binding: &Binding) { if binding.is_invalid_all_object() { - Some(Diagnostic::new(InvalidAllObject, binding.range())) - } else { - None + checker.report_diagnostic(InvalidAllObject, binding.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs index 7f36077b9af9c..cb099e399aa72 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; @@ -9,6 +8,7 @@ use ruff_python_semantic::analyze::terminal::Terminal; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -68,10 +68,7 @@ pub(crate) fn invalid_bool_return(checker: &Checker, function_def: &ast::StmtFun // If there are no return statements, add a diagnostic. if terminal == Terminal::Implicit { - checker.report_diagnostic(Diagnostic::new( - InvalidBoolReturnType, - function_def.identifier(), - )); + checker.report_diagnostic(InvalidBoolReturnType, function_def.identifier()); return; } @@ -88,11 +85,11 @@ pub(crate) fn invalid_bool_return(checker: &Checker, function_def: &ast::StmtFun ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::Number(NumberLike::Bool)) ) { - checker.report_diagnostic(Diagnostic::new(InvalidBoolReturnType, value.range())); + checker.report_diagnostic(InvalidBoolReturnType, value.range()); } } else { // Disallow implicit `None`. - checker.report_diagnostic(Diagnostic::new(InvalidBoolReturnType, stmt.range())); + checker.report_diagnostic(InvalidBoolReturnType, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs index 86f41eff893c9..9b5004e6bbafe 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_bytes_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; @@ -9,6 +8,7 @@ use ruff_python_semantic::analyze::terminal::Terminal; use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -68,10 +68,7 @@ pub(crate) fn invalid_bytes_return(checker: &Checker, function_def: &ast::StmtFu // If there are no return statements, add a diagnostic. if terminal == Terminal::Implicit { - checker.report_diagnostic(Diagnostic::new( - InvalidBytesReturnType, - function_def.identifier(), - )); + checker.report_diagnostic(InvalidBytesReturnType, function_def.identifier()); return; } @@ -87,11 +84,11 @@ pub(crate) fn invalid_bytes_return(checker: &Checker, function_def: &ast::StmtFu ResolvedPythonType::from(value), ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::Bytes) ) { - checker.report_diagnostic(Diagnostic::new(InvalidBytesReturnType, value.range())); + checker.report_diagnostic(InvalidBytesReturnType, value.range()); } } else { // Disallow implicit `None`. - checker.report_diagnostic(Diagnostic::new(InvalidBytesReturnType, stmt.range())); + checker.report_diagnostic(InvalidBytesReturnType, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs index 7a4e935b3d5eb..37d899b653d76 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_python_semantic::Modules; +use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -70,6 +70,6 @@ pub(crate) fn invalid_envvar_default(checker: &Checker, call: &ast::ExprCall) { ) { return; } - checker.report_diagnostic(Diagnostic::new(InvalidEnvvarDefault, expr.range())); + checker.report_diagnostic(InvalidEnvvarDefault, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs index bee484546f8c8..7a2e1e3e0fb57 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_python_semantic::Modules; +use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -58,6 +58,6 @@ pub(crate) fn invalid_envvar_value(checker: &Checker, call: &ast::ExprCall) { return; } - checker.report_diagnostic(Diagnostic::new(InvalidEnvvarValue, expr.range())); + checker.report_diagnostic(InvalidEnvvarValue, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs index 5fe3b0ca56949..a1ecd71cf1aea 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; @@ -9,6 +8,7 @@ use ruff_python_semantic::analyze::terminal::Terminal; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -48,7 +48,7 @@ impl Violation for InvalidHashReturnType { } } -/// E0309 +/// PLE0309 pub(crate) fn invalid_hash_return(checker: &Checker, function_def: &ast::StmtFunctionDef) { if function_def.name.as_str() != "__hash__" { return; @@ -72,10 +72,7 @@ pub(crate) fn invalid_hash_return(checker: &Checker, function_def: &ast::StmtFun // If there are no return statements, add a diagnostic. if terminal == Terminal::Implicit { - checker.report_diagnostic(Diagnostic::new( - InvalidHashReturnType, - function_def.identifier(), - )); + checker.report_diagnostic(InvalidHashReturnType, function_def.identifier()); return; } @@ -92,11 +89,11 @@ pub(crate) fn invalid_hash_return(checker: &Checker, function_def: &ast::StmtFun ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer)) ) { - checker.report_diagnostic(Diagnostic::new(InvalidHashReturnType, value.range())); + checker.report_diagnostic(InvalidHashReturnType, value.range()); } } else { // Disallow implicit `None`. - checker.report_diagnostic(Diagnostic::new(InvalidHashReturnType, stmt.range())); + checker.report_diagnostic(InvalidHashReturnType, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs index 1ecc5de3c5f29..dca29deb76133 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_index_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; @@ -9,6 +8,7 @@ use ruff_python_semantic::analyze::terminal::Terminal; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -50,7 +50,7 @@ impl Violation for InvalidIndexReturnType { } } -/// E0305 +/// PLE0305 pub(crate) fn invalid_index_return(checker: &Checker, function_def: &ast::StmtFunctionDef) { if function_def.name.as_str() != "__index__" { return; @@ -74,10 +74,7 @@ pub(crate) fn invalid_index_return(checker: &Checker, function_def: &ast::StmtFu // If there are no return statements, add a diagnostic. if terminal == Terminal::Implicit { - checker.report_diagnostic(Diagnostic::new( - InvalidIndexReturnType, - function_def.identifier(), - )); + checker.report_diagnostic(InvalidIndexReturnType, function_def.identifier()); return; } @@ -94,11 +91,11 @@ pub(crate) fn invalid_index_return(checker: &Checker, function_def: &ast::StmtFu ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer)) ) { - checker.report_diagnostic(Diagnostic::new(InvalidIndexReturnType, value.range())); + checker.report_diagnostic(InvalidIndexReturnType, value.range()); } } else { // Disallow implicit `None`. - checker.report_diagnostic(Diagnostic::new(InvalidIndexReturnType, stmt.range())); + checker.report_diagnostic(InvalidIndexReturnType, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs index 30918afb568bf..4e155a4aabbcc 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_length_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; @@ -9,6 +8,7 @@ use ruff_python_semantic::analyze::terminal::Terminal; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -49,7 +49,7 @@ impl Violation for InvalidLengthReturnType { } } -/// E0303 +/// PLE0303 pub(crate) fn invalid_length_return(checker: &Checker, function_def: &ast::StmtFunctionDef) { if function_def.name.as_str() != "__len__" { return; @@ -73,10 +73,7 @@ pub(crate) fn invalid_length_return(checker: &Checker, function_def: &ast::StmtF // If there are no return statements, add a diagnostic. if terminal == Terminal::Implicit { - checker.report_diagnostic(Diagnostic::new( - InvalidLengthReturnType, - function_def.identifier(), - )); + checker.report_diagnostic(InvalidLengthReturnType, function_def.identifier()); return; } @@ -95,11 +92,11 @@ pub(crate) fn invalid_length_return(checker: &Checker, function_def: &ast::StmtF | ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer)) ) { - checker.report_diagnostic(Diagnostic::new(InvalidLengthReturnType, value.range())); + checker.report_diagnostic(InvalidLengthReturnType, value.range()); } } else { // Disallow implicit `None`. - checker.report_diagnostic(Diagnostic::new(InvalidLengthReturnType, stmt.range())); + checker.report_diagnostic(InvalidLengthReturnType, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs index 47c103ed3480a..2484b2f74fcfa 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_str_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; @@ -9,6 +8,7 @@ use ruff_python_semantic::analyze::terminal::Terminal; use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -44,7 +44,7 @@ impl Violation for InvalidStrReturnType { } } -/// E0307 +/// PLE0307 pub(crate) fn invalid_str_return(checker: &Checker, function_def: &ast::StmtFunctionDef) { if function_def.name.as_str() != "__str__" { return; @@ -68,10 +68,7 @@ pub(crate) fn invalid_str_return(checker: &Checker, function_def: &ast::StmtFunc // If there are no return statements, add a diagnostic. if terminal == Terminal::Implicit { - checker.report_diagnostic(Diagnostic::new( - InvalidStrReturnType, - function_def.identifier(), - )); + checker.report_diagnostic(InvalidStrReturnType, function_def.identifier()); return; } @@ -87,11 +84,11 @@ pub(crate) fn invalid_str_return(checker: &Checker, function_def: &ast::StmtFunc ResolvedPythonType::from(value), ResolvedPythonType::Unknown | ResolvedPythonType::Atom(PythonType::String) ) { - checker.report_diagnostic(Diagnostic::new(InvalidStrReturnType, value.range())); + checker.report_diagnostic(InvalidStrReturnType, value.range()); } } else { // Disallow implicit `None`. - checker.report_diagnostic(Diagnostic::new(InvalidStrReturnType, stmt.range())); + checker.report_diagnostic(InvalidStrReturnType, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs index 7a30ee0763226..067387189e288 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::{Token, TokenKind}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for strings that contain the control character `BS`. @@ -180,11 +181,7 @@ impl Violation for InvalidCharacterZeroWidthSpace { } /// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515 -pub(crate) fn invalid_string_characters( - diagnostics: &mut Vec, - token: &Token, - locator: &Locator, -) { +pub(crate) fn invalid_string_characters(context: &LintContext, token: &Token, locator: &Locator) { let text = match token.kind() { // We can't use the `value` field since it's decoded and e.g. for f-strings removed a curly // brace that escaped another curly brace, which would gives us wrong column information. @@ -193,28 +190,51 @@ pub(crate) fn invalid_string_characters( }; for (column, match_) in text.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) { + let location = token.start() + TextSize::try_from(column).unwrap(); let c = match_.chars().next().unwrap(); - let (replacement, rule): (&str, DiagnosticKind) = match c { - '\x08' => ("\\b", InvalidCharacterBackspace.into()), - '\x1A' => ("\\x1A", InvalidCharacterSub.into()), - '\x1B' => ("\\x1B", InvalidCharacterEsc.into()), - '\0' => ("\\0", InvalidCharacterNul.into()), - '\u{200b}' => ("\\u200b", InvalidCharacterZeroWidthSpace.into()), + let range = TextRange::at(location, c.text_len()); + + let is_escaped = &text[..column] + .chars() + .rev() + .take_while(|c| *c == '\\') + .count() + % 2 + == 1; + + let (replacement, diagnostic) = match c { + '\x08' => ( + "\\b", + context.report_diagnostic_if_enabled(InvalidCharacterBackspace, range), + ), + '\x1A' => ( + "\\x1A", + context.report_diagnostic_if_enabled(InvalidCharacterSub, range), + ), + '\x1B' => ( + "\\x1B", + context.report_diagnostic_if_enabled(InvalidCharacterEsc, range), + ), + '\0' => ( + "\\0", + context.report_diagnostic_if_enabled(InvalidCharacterNul, range), + ), + '\u{200b}' => ( + "\\u200b", + context.report_diagnostic_if_enabled(InvalidCharacterZeroWidthSpace, range), + ), _ => { continue; } }; - let location = token.start() + TextSize::try_from(column).unwrap(); - let range = TextRange::at(location, c.text_len()); - - let mut diagnostic = Diagnostic::new(rule, range); + let Some(mut diagnostic) = diagnostic else { + continue; + }; - if !token.unwrap_string_flags().is_raw_string() { + if !token.unwrap_string_flags().is_raw_string() && !is_escaped { let edit = Edit::range_replacement(replacement.to_string(), range); diagnostic.set_fix(Fix::safe_edit(edit)); } - - diagnostics.push(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs b/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs index b38acef7f0262..92d18485ae425 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/iteration_over_set.rs @@ -1,12 +1,12 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::comparable::HashableExpr; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; +use ruff_python_ast::comparable::HashableExpr; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for iteration over a `set` literal where each element in the set is @@ -63,7 +63,7 @@ pub(crate) fn iteration_over_set(checker: &Checker, expr: &Expr) { } } - let mut diagnostic = Diagnostic::new(IterationOverSet, expr.range()); + let mut diagnostic = checker.report_diagnostic(IterationOverSet, expr.range()); let tuple = if let [elt] = set.elts.as_slice() { let elt = checker.locator().slice(elt); @@ -73,6 +73,4 @@ pub(crate) fn iteration_over_set(checker: &Checker, expr: &Expr) { format!("({})", &set[1..set.len() - 1]) }; diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(tuple, expr.range()))); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/len_test.rs b/crates/ruff_linter/src/rules/pylint/rules/len_test.rs index c9a85a6285a16..1a664eadda12c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/len_test.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/len_test.rs @@ -1,13 +1,16 @@ -use crate::checkers::ast::Checker; -use crate::fix::snippet::SourceCodeSnippet; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprCall}; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; use ruff_python_semantic::analyze::typing::find_binding_value; -use ruff_python_semantic::{BindingId, SemanticModel}; use ruff_text_size::Ranged; +use crate::checkers::ast::Checker; +use crate::fix::edits; +use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Edit, Fix}; + /// ## What it does /// Checks for `len` calls on sequences in a boolean test context. /// @@ -40,6 +43,19 @@ use ruff_text_size::Ranged; /// print(vegetables) /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe when the `len` call includes a comment, +/// as the comment would be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// fruits = [] +/// if len( +/// fruits # comment +/// ): +/// ... +/// ``` +/// /// ## References /// [PEP 8: Programming Recommendations](https://peps.python.org/pep-0008/#programming-recommendations) #[derive(ViolationMetadata)] @@ -90,18 +106,24 @@ pub(crate) fn len_test(checker: &Checker, call: &ExprCall) { let replacement = checker.locator().slice(argument.range()).to_string(); - checker.report_diagnostic( - Diagnostic::new( + checker + .report_diagnostic( LenTest { expression: SourceCodeSnippet::new(replacement.clone()), }, call.range(), ) - .with_fix(Fix::safe_edit(Edit::range_replacement( - replacement, - call.range(), - ))), - ); + .set_fix(Fix::applicable_edit( + Edit::range_replacement( + edits::pad(replacement, call.range(), checker.locator()), + call.range(), + ), + if checker.comment_ranges().intersects(call.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); } fn is_indirect_sequence(expr: &Expr, semantic: &SemanticModel) -> bool { @@ -110,12 +132,11 @@ fn is_indirect_sequence(expr: &Expr, semantic: &SemanticModel) -> bool { }; let scope = semantic.current_scope(); - let bindings: Vec = scope.get_all(name).collect(); - let [binding_id] = bindings.as_slice() else { + let Some(binding_id) = scope.get(name) else { return false; }; - let binding = semantic.binding(*binding_id); + let binding = semantic.binding(binding_id); // Attempt to find the binding's value let Some(binding_value) = find_binding_value(binding, semantic) else { diff --git a/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs b/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs index 9c1a6984c19a8..2130cd29cf919 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/literal_membership.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for membership tests on `list` and `tuple` literals. @@ -101,7 +101,7 @@ pub(crate) fn literal_membership(checker: &Checker, compare: &ast::ExprCompare) return; } - let mut diagnostic = Diagnostic::new(LiteralMembership, right.range()); + let mut diagnostic = checker.report_diagnostic(LiteralMembership, right.range()); let literal = checker.locator().slice(right); let set = format!("{{{}}}", &literal[1..literal.len() - 1]); @@ -109,6 +109,4 @@ pub(crate) fn literal_membership(checker: &Checker, compare: &ast::ExprCompare) set, right.range(), ))); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs index a946d51276d71..5d93d51e50a5d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/load_before_global_declaration.rs @@ -1,7 +1,8 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::SourceRow; +use crate::Violation; + /// ## What it does /// Checks for uses of names that are declared as `global` prior to the /// relevant `global` declaration. diff --git a/crates/ruff_linter/src/rules/pylint/rules/logging.rs b/crates/ruff_linter/src/rules/pylint/rules/logging.rs index 359b0de4ec7df..d8a13cfdf3c01 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/logging.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/logging.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pyflakes::cformat::CFormatSummary; @@ -109,7 +109,7 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) { if !logging::is_logger_candidate( &call.func, checker.semantic(), - &checker.settings.logger_objects, + &checker.settings().logger_objects, ) { return; } @@ -152,15 +152,15 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) { let num_message_args = call.arguments.args.len() - 1; let num_keywords = call.arguments.keywords.len(); - if checker.enabled(Rule::LoggingTooManyArgs) { + if checker.is_rule_enabled(Rule::LoggingTooManyArgs) { if summary.num_positional < num_message_args { - checker.report_diagnostic(Diagnostic::new(LoggingTooManyArgs, call.func.range())); + checker.report_diagnostic(LoggingTooManyArgs, call.func.range()); } } - if checker.enabled(Rule::LoggingTooFewArgs) { + if checker.is_rule_enabled(Rule::LoggingTooFewArgs) { if num_message_args > 0 && num_keywords == 0 && summary.num_positional > num_message_args { - checker.report_diagnostic(Diagnostic::new(LoggingTooFewArgs, call.func.range())); + checker.report_diagnostic(LoggingTooFewArgs, call.func.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs index ff34e25dfcbcf..a469e7b40c064 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs @@ -1,10 +1,10 @@ use itertools::Itertools; use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, UnaryOp}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::settings::ConstantType; @@ -89,7 +89,7 @@ fn is_magic_value(literal_expr: LiteralExpressionRef, allowed_types: &[ConstantT !matches!(value.to_str(), "" | "__main__") } LiteralExpressionRef::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] ast::Number::Float(value) => !(*value == 0.0 || *value == 1.0), ast::Number::Int(value) => !matches!(*value, Int::ZERO | Int::ONE), ast::Number::Complex { .. } => true, @@ -110,13 +110,13 @@ pub(crate) fn magic_value_comparison(checker: &Checker, left: &Expr, comparators for comparison_expr in std::iter::once(left).chain(comparators) { if let Some(value) = as_literal(comparison_expr) { - if is_magic_value(value, &checker.settings.pylint.allow_magic_value_types) { - checker.report_diagnostic(Diagnostic::new( + if is_magic_value(value, &checker.settings().pylint.allow_magic_value_types) { + checker.report_diagnostic( MagicValueComparison { value: checker.locator().slice(comparison_expr).to_string(), }, comparison_expr.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs index e1237e4b1e934..5f5cc9ccc9e17 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/manual_import_from.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Alias, Identifier, Stmt}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for submodule imports that are aliased to the submodule name. @@ -58,7 +58,7 @@ pub(crate) fn manual_from_import(checker: &Checker, stmt: &Stmt, alias: &Alias, return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ManualFromImport { module: module.to_string(), name: name.to_string(), @@ -72,14 +72,15 @@ pub(crate) fn manual_from_import(checker: &Checker, stmt: &Stmt, alias: &Alias, name: asname.clone(), asname: None, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }], level: 0, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( checker.generator().stmt(&node.into()), stmt.range(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs b/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs index 88f29b9769adc..3856d2e649b3f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/misplaced_bare_raise.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_method; @@ -60,9 +60,9 @@ pub(crate) fn misplaced_bare_raise(checker: &Checker, raise: &ast::StmtRaise) { return; } - if in_dunder_method("__exit__", checker.semantic(), checker.settings) { + if in_dunder_method("__exit__", checker.semantic(), checker.settings()) { return; } - checker.report_diagnostic(Diagnostic::new(MisplacedBareRaise, raise.range())); + checker.report_diagnostic(MisplacedBareRaise, raise.range()); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs b/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs new file mode 100644 index 0000000000000..f615ed6cefe98 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/missing_maxsplit_arg.rs @@ -0,0 +1,171 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ + DictItem, Expr, ExprAttribute, ExprCall, ExprDict, ExprNumberLiteral, ExprStringLiteral, + ExprSubscript, ExprUnaryOp, Keyword, Number, UnaryOp, +}; +use ruff_python_semantic::{SemanticModel, analyze::typing}; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for access to the first or last element of `str.split()` or `str.rsplit()` without +/// `maxsplit=1` +/// +/// ## Why is this bad? +/// Calling `str.split()` or `str.rsplit()` without passing `maxsplit=1` splits on every delimiter in the +/// string. When accessing only the first or last element of the result, it +/// would be more efficient to only split once. +/// +/// ## Example +/// ```python +/// url = "www.example.com" +/// prefix = url.split(".")[0] +/// ``` +/// +/// Use instead: +/// ```python +/// url = "www.example.com" +/// prefix = url.split(".", maxsplit=1)[0] +/// ``` +/// +/// To access the last element, use `str.rsplit()` instead of `str.split()`: +/// ```python +/// url = "www.example.com" +/// suffix = url.rsplit(".", maxsplit=1)[-1] +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct MissingMaxsplitArg { + index: SliceBoundary, + actual_split_type: String, +} + +/// Represents the index of the slice used for this rule (which can only be 0 or -1) +enum SliceBoundary { + First, + Last, +} + +impl Violation for MissingMaxsplitArg { + #[derive_message_formats] + fn message(&self) -> String { + let MissingMaxsplitArg { + index, + actual_split_type, + } = self; + + let suggested_split_type = match index { + SliceBoundary::First => "split", + SliceBoundary::Last => "rsplit", + }; + + if actual_split_type == suggested_split_type { + format!("Pass `maxsplit=1` into `str.{actual_split_type}()`") + } else { + format!( + "Instead of `str.{actual_split_type}()`, call `str.{suggested_split_type}()` and pass `maxsplit=1`", + ) + } + } +} + +fn is_string(expr: &Expr, semantic: &SemanticModel) -> bool { + if let Expr::Name(name) = expr { + semantic + .only_binding(name) + .is_some_and(|binding_id| typing::is_string(semantic.binding(binding_id), semantic)) + } else if let Some(binding_id) = semantic.lookup_attribute(expr) { + typing::is_string(semantic.binding(binding_id), semantic) + } else { + expr.is_string_literal_expr() + } +} + +/// PLC0207 +pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr, expr: &Expr) { + // Check the sliced expression is a function + let Expr::Call(ExprCall { + func, arguments, .. + }) = value + else { + return; + }; + + // Check the slice index is either 0 or -1 (first or last value) + let index = match slice { + Expr::NumberLiteral(ExprNumberLiteral { + value: Number::Int(number_value), + .. + }) => number_value.as_i64(), + Expr::UnaryOp(ExprUnaryOp { + op: UnaryOp::USub, + operand, + .. + }) => match operand.as_ref() { + Expr::NumberLiteral(ExprNumberLiteral { + value: Number::Int(number_value), + .. + }) => number_value.as_i64().map(|number| -number), + _ => return, + }, + _ => return, + }; + + let slice_boundary = match index { + Some(0) => SliceBoundary::First, + Some(-1) => SliceBoundary::Last, + _ => return, + }; + + let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + + // Check the function is "split" or "rsplit" + let attr = attr.as_str(); + if !matches!(attr, "split" | "rsplit") { + return; + } + + let mut target_instance = value; + // a subscripted value could technically be subscripted further ad infinitum, so we + // recurse into the subscript expressions until we find the value being subscripted + while let Expr::Subscript(ExprSubscript { value, .. }) = target_instance.as_ref() { + target_instance = value; + } + + // Check the function is called on a string + if !is_string(target_instance, checker.semantic()) { + return; + } + + // Check the function does not have maxsplit set + if arguments.find_argument_value("maxsplit", 1).is_some() { + return; + } + + // Check maxsplit kwarg not set via unpacked dict literal + for keyword in &*arguments.keywords { + let Keyword { value, .. } = keyword; + + if let Expr::Dict(ExprDict { items, .. }) = value { + for item in items { + let DictItem { key, .. } = item; + if let Some(Expr::StringLiteral(ExprStringLiteral { value, .. })) = key { + if value.to_str() == "maxsplit" { + return; + } + } + } + } + } + + checker.report_diagnostic( + MissingMaxsplitArg { + index: slice_boundary, + actual_split_type: attr.to_string(), + }, + expr.range(), + ); +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 3ddc469656fc4..891691f21b7ec 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -46,6 +46,7 @@ pub(crate) use logging::*; pub(crate) use magic_value_comparison::*; pub(crate) use manual_import_from::*; pub(crate) use misplaced_bare_raise::*; +pub(crate) use missing_maxsplit_arg::*; pub(crate) use modified_iterating_set::*; pub(crate) use named_expr_without_context::*; pub(crate) use nan_comparison::*; @@ -155,6 +156,7 @@ mod logging; mod magic_value_comparison; mod manual_import_from; mod misplaced_bare_raise; +mod missing_maxsplit_arg; mod modified_iterating_set; mod named_expr_without_context; mod nan_comparison; diff --git a/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs b/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs index 84bb50f0734a1..6d99e420697f0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/modified_iterating_set.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_body; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr, StmtFor}; @@ -7,6 +6,7 @@ use ruff_python_semantic::analyze::typing::is_set; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for loops in which a `set` is modified during iteration. @@ -38,6 +38,11 @@ use crate::checkers::ast::Checker; /// nums.add(num + 5) /// ``` /// +/// ## Fix safety +/// This fix is always unsafe because it changes the program’s behavior. Replacing the +/// original set with a copy during iteration allows code that would previously raise a +/// `RuntimeError` to run without error. +/// /// ## References /// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) #[derive(ViolationMetadata)] @@ -92,7 +97,7 @@ pub(crate) fn modified_iterating_set(checker: &Checker, for_stmt: &StmtFor) { }); if is_modified { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ModifiedIteratingSet { name: name.id.clone(), }, @@ -102,7 +107,6 @@ pub(crate) fn modified_iterating_set(checker: &Checker, for_stmt: &StmtFor) { format!("{}.copy()", checker.locator().slice(name)), name.range(), ))); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs b/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs index 87a46a20b0b43..5b306bd547a39 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/named_expr_without_context.rs @@ -1,8 +1,8 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -37,6 +37,6 @@ impl Violation for NamedExprWithoutContext { /// PLW0131 pub(crate) fn named_expr_without_context(checker: &Checker, value: &Expr) { if let Expr::Named(ast::ExprNamed { range, .. }) = value { - checker.report_diagnostic(Diagnostic::new(NamedExprWithoutContext, *range)); + checker.report_diagnostic(NamedExprWithoutContext, *range); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs index aca8fe15fcfd9..3ff711e0fd24b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nan_comparison.rs @@ -1,10 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; +use crate::linter::float::as_nan_float_string_literal; /// ## What it does /// Checks for comparisons against NaN values. @@ -30,7 +32,6 @@ use crate::checkers::ast::Checker; /// if math.isnan(x): /// pass /// ``` -/// #[derive(ViolationMetadata)] pub(crate) struct NanComparison { nan: Nan, @@ -66,26 +67,17 @@ fn nan_comparison_impl<'a>(checker: &Checker, comparators: impl Iterator { - checker.report_diagnostic(Diagnostic::new( - NanComparison { nan: Nan::NumPy }, - expr.range(), - )); + checker.report_diagnostic(NanComparison { nan: Nan::NumPy }, expr.range()); } ["math", "nan"] => { - checker.report_diagnostic(Diagnostic::new( - NanComparison { nan: Nan::Math }, - expr.range(), - )); + checker.report_diagnostic(NanComparison { nan: Nan::Math }, expr.range()); } _ => continue, } } if is_nan_float(expr, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new( - NanComparison { nan: Nan::Math }, - expr.range(), - )); + checker.report_diagnostic(NanComparison { nan: Nan::Math }, expr.range()); } } } @@ -122,14 +114,10 @@ fn is_nan_float(expr: &Expr, semantic: &SemanticModel) -> bool { return false; } - let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**args else { + let [expr] = &**args else { return false; }; - - if !matches!( - value.to_str(), - "nan" | "NaN" | "NAN" | "Nan" | "nAn" | "naN" | "nAN" | "NAn" - ) { + if as_nan_float_string_literal(expr).is_none() { return false; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs index 5c7290b932159..b9be519243e0b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{self as ast, Arguments, Expr, Keyword}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum MinMax { @@ -21,6 +21,7 @@ pub(crate) enum MinMax { /// readability. /// /// ## Example +/// /// ```python /// minimum = min(1, 2, min(3, 4, 5)) /// maximum = max(1, 2, max(3, 4, 5)) @@ -28,12 +29,48 @@ pub(crate) enum MinMax { /// ``` /// /// Use instead: +/// /// ```python /// minimum = min(1, 2, 3, 4, 5) /// maximum = max(1, 2, 3, 4, 5) /// diff = maximum - minimum /// ``` /// +/// ## Known issues +/// +/// The resulting code may be slower and use more memory, especially for nested iterables. For +/// example, this code: +/// +/// ```python +/// iterable = range(3) +/// min(1, min(iterable)) +/// ``` +/// +/// will be fixed to: +/// +/// ```python +/// iterable = range(3) +/// min(1, *iterable) +/// ``` +/// +/// At least on current versions of CPython, this allocates a collection for the whole iterable +/// before calling `min` and could cause performance regressions, at least for large iterables. +/// +/// ## Fix safety +/// +/// This fix is always unsafe and may change the program's behavior for types without full +/// equivalence relations, such as float comparisons involving `NaN`. +/// +/// ```python +/// print(min(2.0, min(float("nan"), 1.0))) # before fix: 2.0 +/// print(min(2.0, float("nan"), 1.0)) # after fix: 1.0 +/// +/// print(max(1.0, max(float("nan"), 2.0))) # before fix: 1.0 +/// print(max(1.0, float("nan"), 2.0)) # after fix: 2.0 +/// ``` +/// +/// The fix will also remove any comments within the outer call. +/// /// ## References /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) @@ -97,8 +134,10 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel) args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, }) = arg { if MinMax::try_from_call(func, keywords, semantic) == Some(min_max) { @@ -108,6 +147,7 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel) value: Box::new(arg.clone()), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); new_args.push(new_arg); continue; @@ -137,9 +177,11 @@ pub(crate) fn nested_min_max( let Some(min_max) = MinMax::try_from_call(func, keywords, checker.semantic()) else { return; }; - - if matches!(&args, [Expr::Call(ast::ExprCall { arguments: Arguments {args, .. }, .. })] if args.len() == 1) - { + // It's only safe to flatten nested calls if the outer call has more than one argument. + // When the outer call has a single argument, flattening would change the semantics by + // changing the shape of the call from treating the inner result as an iterable (or a scalar) + // to passing multiple arguments directly, which can lead to behavioral changes. + if args.len() < 2 { return; } @@ -154,25 +196,22 @@ pub(crate) fn nested_min_max( }; MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic()) == Some(min_max) }) { - let mut diagnostic = Diagnostic::new(NestedMinMax { func: min_max }, expr.range()); - if !checker - .comment_ranges() - .has_comments(expr, checker.source()) - { - let flattened_expr = Expr::Call(ast::ExprCall { - func: Box::new(func.clone()), - arguments: Arguments { - args: collect_nested_args(min_max, args, checker.semantic()).into_boxed_slice(), - keywords: Box::from(keywords), - range: TextRange::default(), - }, + let mut diagnostic = + checker.report_diagnostic(NestedMinMax { func: min_max }, expr.range()); + let flattened_expr = Expr::Call(ast::ExprCall { + func: Box::new(func.clone()), + arguments: Arguments { + args: collect_nested_args(min_max, args, checker.semantic()).into_boxed_slice(), + keywords: Box::from(keywords), range: TextRange::default(), - }); - diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( - checker.generator().expr(&flattened_expr), - expr.range(), - ))); - } - checker.report_diagnostic(diagnostic); + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + }, + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + }); + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + checker.generator().expr(&flattened_expr), + expr.range(), + ))); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs b/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs index 8f6cd66d2041e..99016e24c26df 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_method_decorator.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, DiagnosticKind, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_trivia::indentation_at_offset; @@ -9,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for the use of a classmethod being made without the decorator. @@ -104,9 +104,9 @@ fn get_undecorated_methods(checker: &Checker, class_stmt: &Stmt, method_type: &M let mut explicit_decorator_calls: HashMap = HashMap::default(); - let (method_name, diagnostic_type): (&str, DiagnosticKind) = match method_type { - MethodType::Classmethod => ("classmethod", NoClassmethodDecorator.into()), - MethodType::Staticmethod => ("staticmethod", NoStaticmethodDecorator.into()), + let method_name = match method_type { + MethodType::Classmethod => "classmethod", + MethodType::Staticmethod => "staticmethod", }; // gather all explicit *method calls @@ -170,10 +170,13 @@ fn get_undecorated_methods(checker: &Checker, class_stmt: &Stmt, method_type: &M continue; } - let mut diagnostic = Diagnostic::new( - diagnostic_type.clone(), - TextRange::new(stmt.range().start(), stmt.range().start()), - ); + let range = TextRange::new(stmt.range().start(), stmt.range().start()); + let mut diagnostic = match method_type { + MethodType::Classmethod => checker.report_diagnostic(NoClassmethodDecorator, range), + MethodType::Staticmethod => { + checker.report_diagnostic(NoStaticmethodDecorator, range) + } + }; let indentation = indentation_at_offset(stmt.range().start(), checker.source()); @@ -191,7 +194,6 @@ fn get_undecorated_methods(checker: &Checker, class_stmt: &Stmt, method_type: &M checker.indexer(), )], )); - checker.report_diagnostic(diagnostic); } None => { continue; diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs index 53327e050da2e..c46a2680054d4 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::{ - analyze::{function_type, visibility}, Scope, ScopeId, ScopeKind, + analyze::{function_type, visibility}, }; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_variable; @@ -76,15 +76,15 @@ pub(crate) fn no_self_use(checker: &Checker, scope_id: ScopeId, scope: &Scope) { decorator_list, parent, semantic, - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ), function_type::FunctionType::Method ) { return; } - let extra_property_decorators = checker.settings.pydocstyle.property_decorators(); + let extra_property_decorators = checker.settings().pydocstyle.property_decorators(); if function_type::is_stub(func, semantic) || visibility::is_magic(name) @@ -126,11 +126,11 @@ pub(crate) fn no_self_use(checker: &Checker, scope_id: ScopeId, scope: &Scope) { .map(|binding_id| semantic.binding(binding_id)) .is_some_and(|binding| binding.kind.is_argument() && binding.is_unused()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NoSelfUse { method_name: name.to_string(), }, func.identifier(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs index 148825e3870e7..75046c8ffec3c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_module_import.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Alias; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -69,24 +69,24 @@ pub(crate) fn non_ascii_module_import(checker: &Checker, alias: &Alias) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NonAsciiImportName { name: asname.to_string(), kind: Kind::Aliased, }, asname.range(), - )); + ); } else { if alias.name.as_str().is_ascii() { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NonAsciiImportName { name: alias.name.to_string(), kind: Kind::Unaliased, }, alias.name.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs index 4bd54b3c035d2..2458f418a4b16 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_ascii_name.rs @@ -1,11 +1,11 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::{Binding, BindingKind}; use ruff_text_size::Ranged; -use crate::Locator; +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for the use of non-ASCII characters in variable names. @@ -44,10 +44,11 @@ impl Violation for NonAsciiName { } /// PLC2401 -pub(crate) fn non_ascii_name(binding: &Binding, locator: &Locator) -> Option { +pub(crate) fn non_ascii_name(checker: &Checker, binding: &Binding) { + let locator = checker.locator(); let name = binding.name(locator.contents()); if name.is_ascii() { - return None; + return; } let kind = match binding.kind { @@ -73,17 +74,17 @@ pub(crate) fn non_ascii_name(binding: &Binding, locator: &Locator) -> Option { - return None; + return; } }; - Some(Diagnostic::new( + checker.report_diagnostic( NonAsciiName { name: name.to_string(), kind, }, binding.range(), - )) + ); } #[derive(Debug, PartialEq, Eq, Copy, Clone)] diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs index 6bc86a582da7f..1562783af2d07 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs @@ -1,6 +1,5 @@ use ast::Expr; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::parenthesize::parenthesized_range; @@ -8,6 +7,7 @@ use ruff_python_ast::{ExprBinOp, ExprRef, Operator}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for assignments that can be replaced with augmented assignment @@ -101,7 +101,8 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi // Match, e.g., `x = x + 1`. if ComparableExpr::from(target) == ComparableExpr::from(&value.left) { - let mut diagnostic = Diagnostic::new(NonAugmentedAssignment { operator }, assign.range()); + let mut diagnostic = + checker.report_diagnostic(NonAugmentedAssignment { operator }, assign.range()); diagnostic.set_fix(Fix::unsafe_edit(augmented_assignment( checker, target, @@ -110,7 +111,7 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi value, assign.range, ))); - checker.report_diagnostic(diagnostic); + return; } @@ -120,7 +121,8 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi && (value.left.is_number_literal_expr() || value.left.is_boolean_literal_expr()) && ComparableExpr::from(target) == ComparableExpr::from(&value.right) { - let mut diagnostic = Diagnostic::new(NonAugmentedAssignment { operator }, assign.range()); + let mut diagnostic = + checker.report_diagnostic(NonAugmentedAssignment { operator }, assign.range()); diagnostic.set_fix(Fix::unsafe_edit(augmented_assignment( checker, target, @@ -129,7 +131,6 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi value, assign.range, ))); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs index af2a211cda64f..01a237c1685ec 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs @@ -1,10 +1,10 @@ use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -74,12 +74,12 @@ pub(crate) fn non_slot_assignment(checker: &Checker, class_def: &ast::StmtClassD } for attribute in is_attributes_not_in_slots(&class_def.body) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NonSlotAssignment { name: attribute.name.to_string(), }, attribute.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs index a45d9770fbbda..bb8b10a2eaaa8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs @@ -1,7 +1,7 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -53,18 +53,18 @@ impl Violation for NonlocalAndGlobal { } } -/// E115 +/// PLE0115 pub(crate) fn nonlocal_and_global(checker: &Checker, nonlocal: &ast::StmtNonlocal) { // Determine whether any of the newly declared `nonlocal` variables are already declared as // `global`. for name in &nonlocal.names { if let Some(global) = checker.semantic().global(name) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( NonlocalAndGlobal { name: name.to_string(), }, global, - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs index 4206262102276..ff2bd1a2ab0de 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_without_binding.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast as ast; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for `nonlocal` names without bindings. @@ -41,3 +45,19 @@ impl Violation for NonlocalWithoutBinding { format!("Nonlocal name `{name}` found without binding") } } + +/// PLE0117 +pub(crate) fn nonlocal_without_binding(checker: &Checker, nonlocal: &ast::StmtNonlocal) { + if !checker.semantic().scope_id.is_global() { + for name in &nonlocal.names { + if checker.semantic().nonlocal(name).is_none() { + checker.report_diagnostic( + NonlocalWithoutBinding { + name: name.to_string(), + }, + name.range(), + ); + } + } + } +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs index 5d1081836345b..4a3f29e0904ac 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,6 +66,6 @@ pub(crate) fn potential_index_error(checker: &Checker, value: &Expr, slice: &Exp // Emit a diagnostic if the index is out of bounds. If the index can't be represented as an // `i64`, but the length _can_, then the index is definitely out of bounds. if index.is_none_or(|index| index >= length || index < -length) { - checker.report_diagnostic(Diagnostic::new(PotentialIndexError, slice.range())); + checker.report_diagnostic(PotentialIndexError, slice.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs index 1061849c1848f..a0c2c9db75738 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/property_with_parameters.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{identifier::Identifier, Decorator, Parameters, Stmt}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Decorator, Parameters, Stmt, identifier::Identifier}; use ruff_python_semantic::analyze::visibility::is_property; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -55,8 +55,8 @@ pub(crate) fn property_with_parameters( return; } let semantic = checker.semantic(); - let extra_property_decorators = checker.settings.pydocstyle.property_decorators(); + let extra_property_decorators = checker.settings().pydocstyle.property_decorators(); if is_property(decorator_list, extra_property_decorators, semantic) { - checker.report_diagnostic(Diagnostic::new(PropertyWithParameters, stmt.identifier())); + checker.report_diagnostic(PropertyWithParameters, stmt.identifier()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs b/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs index e4f5774ebdef4..b85a58b1da23a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redeclared_assigned_name.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -27,7 +27,6 @@ use crate::checkers::ast::Checker; /// _, b, a = (1, 2, 3) /// print(a) # 3 /// ``` -/// #[derive(ViolationMetadata)] pub(crate) struct RedeclaredAssignedName { name: String, @@ -57,18 +56,26 @@ fn check_expr(checker: &Checker, expr: &Expr, names: &mut Vec) { check_expr(checker, target, names); } } + Expr::List(list) => { + for target in list { + check_expr(checker, target, names); + } + } + Expr::Starred(starred) => { + check_expr(checker, &starred.value, names); + } Expr::Name(ast::ExprName { id, .. }) => { - if checker.settings.dummy_variable_rgx.is_match(id) { + if checker.settings().dummy_variable_rgx.is_match(id) { // Ignore dummy variable assignments return; } if names.contains(id) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( RedeclaredAssignedName { name: id.to_string(), }, expr.range(), - )); + ); } names.push(id.clone()); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs index 3462305e7c559..856447d810545 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs @@ -1,5 +1,9 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_semantic::{BindingKind, Scope, ScopeId}; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; /// ## What it does /// Checks for variables defined in `for`, `try`, `with` statements @@ -28,7 +32,6 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; /// /// ## References /// - [Pylint documentation](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/redefined-argument-from-local.html) - #[derive(ViolationMetadata)] pub(crate) struct RedefinedArgumentFromLocal { pub(crate) name: String, @@ -41,3 +44,35 @@ impl Violation for RedefinedArgumentFromLocal { format!("Redefining argument with the local name `{name}`") } } + +/// PLR1704 +pub(crate) fn redefined_argument_from_local(checker: &Checker, scope_id: ScopeId, scope: &Scope) { + for (name, binding_id) in scope.bindings() { + for shadow in checker.semantic().shadowed_bindings(scope_id, binding_id) { + let binding = &checker.semantic().bindings[shadow.binding_id()]; + if !matches!( + binding.kind, + BindingKind::LoopVar | BindingKind::BoundException | BindingKind::WithItemVar + ) { + continue; + } + let shadowed = &checker.semantic().bindings[shadow.shadowed_id()]; + if !shadowed.kind.is_argument() { + continue; + } + if checker.settings().dummy_variable_rgx.is_match(name) { + continue; + } + let scope = &checker.semantic().scopes[binding.scope]; + if scope.kind.is_generator() { + continue; + } + checker.report_diagnostic( + RedefinedArgumentFromLocal { + name: name.to_string(), + }, + binding.range(), + ); + } + } +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs index 39111a10dc6c8..bbdf17e8e7365 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs @@ -3,13 +3,13 @@ use std::{fmt, iter}; use regex::Regex; use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Stmt, WithItem}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -280,11 +280,13 @@ fn assignment_targets_from_expr<'a>( ctx: ExprContext::Store, value, range: _, + node_index: _, }) => Box::new(iter::once(value.as_ref())), Expr::Name(ast::ExprName { ctx: ExprContext::Store, id, range: _, + node_index: _, }) => { // Ignore dummy variables. if dummy_variable_rgx.is_match(id) { @@ -297,6 +299,7 @@ fn assignment_targets_from_expr<'a>( ctx: ExprContext::Store, elts, range: _, + node_index: _, }) => Box::new( elts.iter() .flat_map(|elt| assignment_targets_from_expr(elt, dummy_variable_rgx)), @@ -305,6 +308,7 @@ fn assignment_targets_from_expr<'a>( ctx: ExprContext::Store, elts, range: _, + node_index: _, parenthesized: _, }) => Box::new( elts.iter() @@ -342,7 +346,7 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) { let (outer_assignment_targets, inner_assignment_targets) = match stmt { Stmt::With(ast::StmtWith { items, body, .. }) => { let outer_assignment_targets: Vec = - assignment_targets_from_with_items(items, &checker.settings.dummy_variable_rgx) + assignment_targets_from_with_items(items, &checker.settings().dummy_variable_rgx) .map(|expr| ExprWithOuterBindingKind { expr, binding_kind: OuterBindingKind::With, @@ -350,7 +354,7 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) { .collect(); let mut visitor = InnerForWithAssignTargetsVisitor { context: checker.semantic(), - dummy_variable_rgx: &checker.settings.dummy_variable_rgx, + dummy_variable_rgx: &checker.settings().dummy_variable_rgx, assignment_targets: vec![], }; for stmt in body { @@ -360,7 +364,7 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) { } Stmt::For(ast::StmtFor { target, body, .. }) => { let outer_assignment_targets: Vec = - assignment_targets_from_expr(target, &checker.settings.dummy_variable_rgx) + assignment_targets_from_expr(target, &checker.settings().dummy_variable_rgx) .map(|expr| ExprWithOuterBindingKind { expr, binding_kind: OuterBindingKind::For, @@ -368,7 +372,7 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) { .collect(); let mut visitor = InnerForWithAssignTargetsVisitor { context: checker.semantic(), - dummy_variable_rgx: &checker.settings.dummy_variable_rgx, + dummy_variable_rgx: &checker.settings().dummy_variable_rgx, assignment_targets: vec![], }; for stmt in body { @@ -379,25 +383,21 @@ pub(crate) fn redefined_loop_name(checker: &Checker, stmt: &Stmt) { _ => panic!("redefined_loop_name called on Statement that is not a `With` or `For`"), }; - let mut diagnostics = Vec::new(); - for outer_assignment_target in &outer_assignment_targets { for inner_assignment_target in &inner_assignment_targets { // Compare the targets structurally. if ComparableExpr::from(outer_assignment_target.expr) .eq(&(ComparableExpr::from(inner_assignment_target.expr))) { - diagnostics.push(Diagnostic::new( + checker.report_diagnostic( RedefinedLoopName { name: checker.generator().expr(outer_assignment_target.expr), outer_kind: outer_assignment_target.binding_kind, inner_kind: inner_assignment_target.binding_kind, }, inner_assignment_target.expr.range(), - )); + ); } } } - - checker.report_diagnostics(diagnostics); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs index bdeec02ce85c7..a449a3054db52 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs @@ -1,13 +1,13 @@ use std::hash::Hash; -use ruff_python_semantic::{analyze::class::iter_super_class, SemanticModel}; +use ruff_python_semantic::analyze::class::iter_super_class; use rustc_hash::FxHashSet; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,11 +66,9 @@ pub(crate) fn redefined_slots_in_subclass(checker: &Checker, class_def: &ast::St return; } - let semantic = checker.semantic(); - let diagnostics = class_slots - .iter() - .filter_map(|slot| check_super_slots(class_def, semantic, slot)); - checker.report_diagnostics(diagnostics); + for slot in class_slots { + check_super_slots(checker, class_def, &slot); + } } #[derive(Clone, Debug)] @@ -103,25 +101,18 @@ impl Ranged for Slot<'_> { } } -fn check_super_slots( - class_def: &ast::StmtClassDef, - semantic: &SemanticModel, - slot: &Slot, -) -> Option { - iter_super_class(class_def, semantic) - .skip(1) - .find_map(&|super_class: &ast::StmtClassDef| { - if slots_members(&super_class.body).contains(slot) { - return Some(Diagnostic::new( - RedefinedSlotsInSubclass { - base: super_class.name.to_string(), - slot_name: slot.name.to_string(), - }, - slot.range(), - )); - } - None - }) +fn check_super_slots(checker: &Checker, class_def: &ast::StmtClassDef, slot: &Slot) { + for super_class in iter_super_class(class_def, checker.semantic()).skip(1) { + if slots_members(&super_class.body).contains(slot) { + checker.report_diagnostic( + RedefinedSlotsInSubclass { + base: super_class.name.to_string(), + slot_name: slot.name.to_string(), + }, + slot.range(), + ); + } + } } fn slots_members(body: &[Stmt]) -> FxHashSet { @@ -176,7 +167,11 @@ fn slots_attributes(expr: &Expr) -> impl Iterator { Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) | Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().filter_map(|elt| match elt { - Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => Some(Slot { + Expr::StringLiteral(ast::ExprStringLiteral { + value, + range, + node_index: _, + }) => Some(Slot { name: value.to_str(), range: *range, }), @@ -192,12 +187,14 @@ fn slots_attributes(expr: &Expr) -> impl Iterator { .unwrap() .iter_keys() .filter_map(|key| match key { - Some(Expr::StringLiteral(ast::ExprStringLiteral { value, range })) => { - Some(Slot { - name: value.to_str(), - range: *range, - }) - } + Some(Expr::StringLiteral(ast::ExprStringLiteral { + value, + range, + node_index: _, + })) => Some(Slot { + name: value.to_str(), + range: *range, + }), _ => None, }), ), diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs index 29967c6aef371..4ac8f3f79f43b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs @@ -2,17 +2,17 @@ use itertools::Itertools; use rustc_hash::{FxBuildHasher, FxHashMap}; use ast::ExprContext; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::{any_over_expr, contains_effect}; -use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr}; +use ruff_python_ast::{self as ast, AtomicNodeIndex, BoolOp, CmpOp, Expr}; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for repeated equality comparisons that can be rewritten as a membership @@ -32,6 +32,12 @@ use crate::Locator; /// If the items are hashable, use a `set` for efficiency; otherwise, use a /// `tuple`. /// +/// ## Fix safety +/// This rule is always unsafe since literal sets and tuples +/// evaluate their members eagerly whereas `or` comparisons +/// are short-circuited. It is therefore possible that a fix +/// will change behavior in the presence of side-effects. +/// /// ## Example /// ```python /// foo == "bar" or foo == "baz" or foo == "qux" @@ -57,7 +63,9 @@ impl AlwaysFixableViolation for RepeatedEqualityComparison { fn message(&self) -> String { match (self.expression.full_display(), self.all_hashable) { (Some(expression), false) => { - format!("Consider merging multiple comparisons: `{expression}`. Use a `set` if the elements are hashable.") + format!( + "Consider merging multiple comparisons: `{expression}`. Use a `set` if the elements are hashable." + ) } (Some(expression), true) => { format!("Consider merging multiple comparisons: `{expression}`.") @@ -137,7 +145,7 @@ pub(crate) fn repeated_equality_comparison(checker: &Checker, bool_op: &ast::Exp .iter() .all(|comparator| comparator.is_literal_expr()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RepeatedEqualityComparison { expression: SourceCodeSnippet::new(merged_membership_test( expr, @@ -162,11 +170,13 @@ pub(crate) fn repeated_equality_comparison(checker: &Checker, bool_op: &ast::Exp Expr::Set(ast::ExprSet { elts: comparators.iter().copied().cloned().collect(), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }) } else { Expr::Tuple(ast::ExprTuple { elts: comparators.iter().copied().cloned().collect(), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), ctx: ExprContext::Load, parenthesized: true, }) @@ -184,15 +194,15 @@ pub(crate) fn repeated_equality_comparison(checker: &Checker, bool_op: &ast::Exp }, comparators: Box::from([comparator]), range: bool_op.range(), + node_index: AtomicNodeIndex::dummy(), }))) .chain(after) .collect(), range: bool_op.range(), + node_index: AtomicNodeIndex::dummy(), })), bool_op.range(), ))); - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs index c319e4b7f2779..9c71a3246956d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -1,6 +1,6 @@ -use ruff_diagnostics::AlwaysFixableViolation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::AlwaysFixableViolation; use crate::fix::snippet::SourceCodeSnippet; /// ## Removed diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs index 18054602f1178..b2033a5e8f76a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_keyword_argument.rs @@ -1,10 +1,10 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprCall, ExprStringLiteral}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -35,6 +35,7 @@ impl Violation for RepeatedKeywordArgument { } } +/// PLE1132 pub(crate) fn repeated_keyword_argument(checker: &Checker, call: &ExprCall) { let ExprCall { arguments, .. } = call; @@ -44,24 +45,24 @@ pub(crate) fn repeated_keyword_argument(checker: &Checker, call: &ExprCall) { if let Some(id) = &keyword.arg { // Ex) `func(a=1, a=2)` if !seen.insert(id.as_str()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( RepeatedKeywordArgument { duplicate_keyword: id.to_string(), }, keyword.range(), - )); + ); } } else if let Expr::Dict(dict) = &keyword.value { // Ex) `func(**{"a": 1, "a": 2})` for key in dict.iter_keys().flatten() { if let Expr::StringLiteral(ExprStringLiteral { value, .. }) = key { if !seen.insert(value.to_str()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( RepeatedKeywordArgument { duplicate_keyword: value.to_string(), }, key.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs b/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs index e311673226e9a..a80913635c4d3 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/return_in_init.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_method; @@ -46,7 +46,12 @@ impl Violation for ReturnInInit { /// PLE0101 pub(crate) fn return_in_init(checker: &Checker, stmt: &Stmt) { - if let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt { + if let Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) = stmt + { if let Some(expr) = value { if expr.is_none_literal_expr() { // Explicit `return None`. @@ -58,7 +63,7 @@ pub(crate) fn return_in_init(checker: &Checker, stmt: &Stmt) { } } - if in_dunder_method("__init__", checker.semantic(), checker.settings) { - checker.report_diagnostic(Diagnostic::new(ReturnInInit, stmt.range())); + if in_dunder_method("__init__", checker.semantic(), checker.settings()) { + checker.report_diagnostic(ReturnInInit, stmt.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs b/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs index 11c2d04852efa..2d9d194c6e1e0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/self_assigning_variable.rs @@ -1,10 +1,10 @@ use itertools::Itertools; use ruff_python_ast::{self as ast, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -43,7 +43,6 @@ pub(crate) fn self_assignment(checker: &Checker, assign: &ast::StmtAssign) { if checker.semantic().current_scope().kind.is_class() { return; } - let mut diagnostics = Vec::new(); for (left, right) in assign .targets @@ -51,9 +50,8 @@ pub(crate) fn self_assignment(checker: &Checker, assign: &ast::StmtAssign) { .chain(std::iter::once(assign.value.as_ref())) .tuple_combinations() { - visit_assignments(left, right, &mut diagnostics); + visit_assignments(checker, left, right); } - checker.report_diagnostics(diagnostics); } /// PLW0127 @@ -67,28 +65,26 @@ pub(crate) fn self_annotated_assignment(checker: &Checker, assign: &ast::StmtAnn if checker.semantic().current_scope().kind.is_class() { return; } - let mut diagnostics = Vec::new(); - visit_assignments(&assign.target, value, &mut diagnostics); - checker.report_diagnostics(diagnostics); + visit_assignments(checker, &assign.target, value); } -fn visit_assignments(left: &Expr, right: &Expr, diagnostics: &mut Vec) { +fn visit_assignments(checker: &Checker, left: &Expr, right: &Expr) { match (left, right) { (Expr::Tuple(lhs), Expr::Tuple(rhs)) if lhs.len() == rhs.len() => lhs .iter() .zip(rhs) - .for_each(|(lhs_elem, rhs_elem)| visit_assignments(lhs_elem, rhs_elem, diagnostics)), + .for_each(|(lhs_elem, rhs_elem)| visit_assignments(checker, lhs_elem, rhs_elem)), ( Expr::Name(ast::ExprName { id: lhs_name, .. }), Expr::Name(ast::ExprName { id: rhs_name, .. }), ) if lhs_name == rhs_name => { - diagnostics.push(Diagnostic::new( + checker.report_diagnostic( SelfAssigningVariable { name: lhs_name.to_string(), }, left.range(), - )); + ); } _ => {} } diff --git a/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs index d9e7647e30201..d6611acd21ff0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/self_or_cls_assignment.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::analyze::function_type::{self as function_type, FunctionType}; use ruff_python_semantic::ScopeKind; +use ruff_python_semantic::analyze::function_type::{self as function_type, FunctionType}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,7 +66,7 @@ impl Violation for SelfOrClsAssignment { } } -/// PLW0127 +/// PLW0642 pub(crate) fn self_or_cls_assignment(checker: &Checker, target: &Expr) { let ScopeKind::Function(ast::StmtFunctionDef { name, @@ -98,8 +98,8 @@ pub(crate) fn self_or_cls_assignment(checker: &Checker, target: &Expr) { decorator_list, parent, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); let method_type = match (function_type, self_or_cls.name().as_str()) { @@ -117,10 +117,7 @@ fn check_expr(checker: &Checker, target: &Expr, method_type: MethodType) { Expr::Name(_) => { if let Expr::Name(ast::ExprName { id, .. }) = target { if id.as_str() == method_type.arg_name() { - checker.report_diagnostic(Diagnostic::new( - SelfOrClsAssignment { method_type }, - target.range(), - )); + checker.report_diagnostic(SelfOrClsAssignment { method_type }, target.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs b/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs index 246766925b54a..2cfe984f86237 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/shallow_copy_environ.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Check for shallow `os.environ` copies. @@ -88,10 +88,9 @@ pub(crate) fn shallow_copy_environ(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(ShallowCopyEnviron, call.range()); + let mut diagnostic = checker.report_diagnostic(ShallowCopyEnviron, call.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( format!("{}.copy()", checker.locator().slice(arg)), call.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs b/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs index 75699aa9bfc72..3cf41a00db4f6 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/single_string_slots.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, Expr, Stmt, StmtClassDef}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -66,10 +66,7 @@ pub(crate) fn single_string_slots(checker: &Checker, class: &StmtClassDef) { if let Expr::Name(ast::ExprName { id, .. }) = target { if id.as_str() == "__slots__" { if matches!(value.as_ref(), Expr::StringLiteral(_) | Expr::FString(_)) { - checker.report_diagnostic(Diagnostic::new( - SingleStringSlots, - stmt.identifier(), - )); + checker.report_diagnostic(SingleStringSlots, stmt.identifier()); } } } @@ -83,10 +80,7 @@ pub(crate) fn single_string_slots(checker: &Checker, class: &StmtClassDef) { if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { if id.as_str() == "__slots__" { if matches!(value.as_ref(), Expr::StringLiteral(_) | Expr::FString(_)) { - checker.report_diagnostic(Diagnostic::new( - SingleStringSlots, - stmt.identifier(), - )); + checker.report_diagnostic(SingleStringSlots, stmt.identifier()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs index 7b1588abe57d8..91d280fb73358 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::Scope; +use ruff_python_semantic::analyze::function_type; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for methods decorated with `@singledispatch`. @@ -58,7 +58,7 @@ impl Violation for SingledispatchMethod { } } -/// E1519 +/// PLE1519 pub(crate) fn singledispatch_method(checker: &Checker, scope: &Scope) { let Some(func) = scope.kind.as_function() else { return; @@ -79,8 +79,8 @@ pub(crate) fn singledispatch_method(checker: &Checker, scope: &Scope) { decorator_list, parent, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); if !matches!( type_, @@ -99,7 +99,7 @@ pub(crate) fn singledispatch_method(checker: &Checker, scope: &Scope) { matches!(qualified_name.segments(), ["functools", "singledispatch"]) }) { - let mut diagnostic = Diagnostic::new(SingledispatchMethod, decorator.range()); + let mut diagnostic = checker.report_diagnostic(SingledispatchMethod, decorator.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("functools", "singledispatchmethod"), @@ -111,7 +111,6 @@ pub(crate) fn singledispatch_method(checker: &Checker, scope: &Scope) { [import_edit], )) }); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs index 554b4d461b5e2..eb2e82c25853c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs @@ -1,12 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; -use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::Scope; +use ruff_python_semantic::analyze::function_type; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for non-method functions decorated with `@singledispatchmethod`. @@ -56,7 +56,7 @@ impl Violation for SingledispatchmethodFunction { } } -/// E1520 +/// PLE1520 pub(crate) fn singledispatchmethod_function(checker: &Checker, scope: &Scope) { let Some(func) = scope.kind.as_function() else { return; @@ -77,8 +77,8 @@ pub(crate) fn singledispatchmethod_function(checker: &Checker, scope: &Scope) { decorator_list, parent, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); if !matches!(type_, function_type::FunctionType::Function) { return; @@ -95,7 +95,8 @@ pub(crate) fn singledispatchmethod_function(checker: &Checker, scope: &Scope) { ) }) { - let mut diagnostic = Diagnostic::new(SingledispatchmethodFunction, decorator.range()); + let mut diagnostic = + checker.report_diagnostic(SingledispatchmethodFunction, decorator.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("functools", "singledispatch"), @@ -107,7 +108,6 @@ pub(crate) fn singledispatchmethod_function(checker: &Checker, scope: &Scope) { [import_edit], )) }); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs index 5420981619c44..b07d0bef0df98 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -65,7 +65,7 @@ pub(crate) fn subprocess_popen_preexec_fn(checker: &Checker, call: &ast::ExprCal .find_keyword("preexec_fn") .filter(|keyword| !keyword.value.is_none_literal_expr()) { - checker.report_diagnostic(Diagnostic::new(SubprocessPopenPreexecFn, keyword.range())); + checker.report_diagnostic(SubprocessPopenPreexecFn, keyword.range()); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs index 150056b7e49ef..9d4848ff67eb0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::add_argument; +use crate::{AlwaysFixableViolation, Applicability, Fix}; /// ## What it does /// Checks for uses of `subprocess.run` without an explicit `check` argument. @@ -71,7 +71,8 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["subprocess", "run"])) { if call.arguments.find_keyword("check").is_none() { - let mut diagnostic = Diagnostic::new(SubprocessRunWithoutCheck, call.func.range()); + let mut diagnostic = + checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range()); diagnostic.set_fix(Fix::applicable_edit( add_argument( "check=False", @@ -91,7 +92,6 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa Applicability::Safe }, )); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs b/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs index 7aeb5bbff898e..510fc836c53ce 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/super_without_brackets.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::{analyze::function_type, ScopeKind}; +use ruff_python_semantic::{ScopeKind, analyze::function_type}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Detects attempts to use `super` without parentheses. @@ -96,8 +96,8 @@ pub(crate) fn super_without_brackets(checker: &Checker, func: &Expr) { &function_def.decorator_list, parent, checker.semantic(), - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ); if !matches!( classification, @@ -108,12 +108,10 @@ pub(crate) fn super_without_brackets(checker: &Checker, func: &Expr) { return; } - let mut diagnostic = Diagnostic::new(SuperWithoutBrackets, value.range()); + let mut diagnostic = checker.report_diagnostic(SuperWithoutBrackets, value.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "super()".to_string(), value.range(), ))); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs index be59c46c5b771..c0a1a0445a6de 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs @@ -1,11 +1,11 @@ -use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; - +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_text_size::Ranged; +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; + /// ## What it does /// Checks for uses of the `exit()` and `quit()`. /// @@ -19,6 +19,20 @@ use ruff_text_size::Ranged; /// Prefer `sys.exit()`, as the `sys` module is guaranteed to exist in all /// contexts. /// +/// ## Fix safety +/// This fix is always unsafe. When replacing `exit` or `quit` with `sys.exit`, +/// the behavior can change in the following ways: +/// +/// 1. If the code runs in an environment where the `site` module is not imported +/// (e.g., with `python -S`), the original code would raise a `NameError`, while +/// the fixed code would execute normally. +/// +/// 2. `site.exit` and `sys.exit` handle tuple arguments differently. `site.exit` +/// treats tuples as regular objects and always returns exit code 1, while `sys.exit` +/// interprets tuple contents to determine the exit code: an empty tuple () results in +/// exit code 0, and a single-element tuple like (2,) uses that element's value (2) as +/// the exit code. +/// /// ## Example /// ```python /// if __name__ == "__main__": @@ -63,7 +77,7 @@ pub(crate) fn sys_exit_alias(checker: &Checker, call: &ExprCall) { if !matches!(builtin, "exit" | "quit") { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( SysExitAlias { name: builtin.to_string(), }, @@ -77,7 +91,6 @@ pub(crate) fn sys_exit_alias(checker: &Checker, call: &ExprCall) { .any(|kwarg| kwarg.arg.is_none()); // only one optional argument allowed, and we can't convert **kwargs if call.arguments.len() > 1 || has_star_kwargs { - checker.report_diagnostic(diagnostic); return; } @@ -97,5 +110,4 @@ pub(crate) fn sys_exit_alias(checker: &Checker, call: &ExprCall) { } Ok(Fix::unsafe_edits(import_edit, edits)) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs index f64be8688d325..7fb946ea8334b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_arguments.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::{function_type, visibility}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -68,10 +68,10 @@ pub(crate) fn too_many_arguments(checker: &Checker, function_def: &ast::StmtFunc let num_arguments = function_def .parameters .iter_non_variadic_params() - .filter(|param| !checker.settings.dummy_variable_rgx.is_match(param.name())) + .filter(|param| !checker.settings().dummy_variable_rgx.is_match(param.name())) .count(); - if num_arguments <= checker.settings.pylint.max_args { + if num_arguments <= checker.settings().pylint.max_args { return; } @@ -90,8 +90,8 @@ pub(crate) fn too_many_arguments(checker: &Checker, function_def: &ast::StmtFunc &function_def.decorator_list, semantic.current_scope(), semantic, - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ), function_type::FunctionType::Method | function_type::FunctionType::ClassMethod @@ -104,15 +104,15 @@ pub(crate) fn too_many_arguments(checker: &Checker, function_def: &ast::StmtFunc num_arguments }; - if num_arguments <= checker.settings.pylint.max_args { + if num_arguments <= checker.settings().pylint.max_args { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TooManyArguments { c_args: num_arguments, - max_args: checker.settings.pylint.max_args, + max_args: checker.settings().pylint.max_args, }, function_def.identifier(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs index ddb86458d5e69..76bf947411b25 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_boolean_expressions.rs @@ -1,9 +1,9 @@ use ast::{Expr, StmtIf}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -46,28 +46,28 @@ impl Violation for TooManyBooleanExpressions { pub(crate) fn too_many_boolean_expressions(checker: &Checker, stmt: &StmtIf) { if let Some(bool_op) = stmt.test.as_bool_op_expr() { let expressions = count_bools(bool_op); - if expressions > checker.settings.pylint.max_bool_expr { - checker.report_diagnostic(Diagnostic::new( + if expressions > checker.settings().pylint.max_bool_expr { + checker.report_diagnostic( TooManyBooleanExpressions { expressions, - max_expressions: checker.settings.pylint.max_bool_expr, + max_expressions: checker.settings().pylint.max_bool_expr, }, bool_op.range(), - )); + ); } } for elif in &stmt.elif_else_clauses { if let Some(bool_op) = elif.test.as_ref().and_then(Expr::as_bool_op_expr) { let expressions = count_bools(bool_op); - if expressions > checker.settings.pylint.max_bool_expr { - checker.report_diagnostic(Diagnostic::new( + if expressions > checker.settings().pylint.max_bool_expr { + checker.report_diagnostic( TooManyBooleanExpressions { expressions, - max_expressions: checker.settings.pylint.max_bool_expr, + max_expressions: checker.settings().pylint.max_bool_expr, }, bool_op.range(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs index 15bfed361f412..d5c82b95eb8aa 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_branches.rs @@ -1,8 +1,10 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::identifier::Identifier; +use crate::Violation; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks for functions or methods with too many branches, including (nested) @@ -233,21 +235,20 @@ fn num_branches(stmts: &[Stmt]) -> usize { /// PLR0912 pub(crate) fn too_many_branches( + checker: &Checker, stmt: &Stmt, body: &[Stmt], max_branches: usize, -) -> Option { +) { let branches = num_branches(body); if branches > max_branches { - Some(Diagnostic::new( + checker.report_diagnostic( TooManyBranches { branches, max_branches, }, stmt.identifier(), - )) - } else { - None + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs index 3c87cf02df4b2..56ac5cf986b4c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_locals.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::{Scope, ScopeKind}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -45,15 +45,15 @@ pub(crate) fn too_many_locals(checker: &Checker, scope: &Scope) { binding.kind.is_assignment() }) .count(); - if num_locals > checker.settings.pylint.max_locals { + if num_locals > checker.settings().pylint.max_locals { if let ScopeKind::Function(func) = scope.kind { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TooManyLocals { current_amount: num_locals, - max_amount: checker.settings.pylint.max_locals, + max_amount: checker.settings().pylint.max_locals, }, func.identifier(), - )); + ); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs index 63ea268e8462a..0e4ee0dea3ed0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_nested_blocks.rs @@ -1,9 +1,9 @@ use ast::ExceptHandler; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Stmt}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -48,7 +48,7 @@ pub(crate) fn too_many_nested_blocks(checker: &Checker, stmt: &Stmt) { return; } - let max_nested_blocks = checker.settings.pylint.max_nested_blocks; + let max_nested_blocks = checker.settings().pylint.max_nested_blocks; // Traverse up the hierarchy, identifying the root node and counting the number of nested // blocks between the root and this leaf. @@ -74,13 +74,13 @@ pub(crate) fn too_many_nested_blocks(checker: &Checker, stmt: &Stmt) { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TooManyNestedBlocks { nested_blocks: count, max_nested_blocks, }, checker.semantic().statement(root_id).range(), - )); + ); } /// Returns `true` if the given statement is a nested block. diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs index 90a047c355bc4..2e0e02908c53a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, identifier::Identifier}; use ruff_python_semantic::analyze::{function_type, visibility}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -72,10 +72,10 @@ pub(crate) fn too_many_positional_arguments( .posonlyargs .iter() .chain(&function_def.parameters.args) - .filter(|param| !checker.settings.dummy_variable_rgx.is_match(param.name())) + .filter(|param| !checker.settings().dummy_variable_rgx.is_match(param.name())) .count(); - if num_positional_args <= checker.settings.pylint.max_positional_args { + if num_positional_args <= checker.settings().pylint.max_positional_args { return; } @@ -94,8 +94,8 @@ pub(crate) fn too_many_positional_arguments( &function_def.decorator_list, semantic.current_scope(), semantic, - &checker.settings.pep8_naming.classmethod_decorators, - &checker.settings.pep8_naming.staticmethod_decorators, + &checker.settings().pep8_naming.classmethod_decorators, + &checker.settings().pep8_naming.staticmethod_decorators, ), function_type::FunctionType::Method | function_type::FunctionType::ClassMethod @@ -108,15 +108,15 @@ pub(crate) fn too_many_positional_arguments( num_positional_args }; - if num_positional_args <= checker.settings.pylint.max_positional_args { + if num_positional_args <= checker.settings().pylint.max_positional_args { return; } - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TooManyPositionalArguments { c_pos: num_positional_args, - max_pos: checker.settings.pylint.max_positional_args, + max_pos: checker.settings().pylint.max_positional_args, }, function_def.identifier(), - )); + ); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs index a5e7d9883f33e..53decdc547ccb 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_public_methods.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::analyze::visibility::{self, Visibility::Public}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -99,7 +99,7 @@ impl Violation for TooManyPublicMethods { } } -/// R0904 +/// PLR0904 pub(crate) fn too_many_public_methods( checker: &Checker, class_def: &ast::StmtClassDef, @@ -121,12 +121,12 @@ pub(crate) fn too_many_public_methods( .count(); if methods > max_methods { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TooManyPublicMethods { methods, max_methods, }, class_def.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs index 6680f64efcf3a..f4fb169653137 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_return_statements.rs @@ -1,11 +1,11 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::Visitor; +use crate::{Violation, checkers::ast::Checker}; + /// ## What it does /// Checks for functions or methods with too many return statements. /// @@ -77,21 +77,20 @@ fn num_returns(body: &[Stmt]) -> usize { /// PLR0911 pub(crate) fn too_many_return_statements( + checker: &Checker, stmt: &Stmt, body: &[Stmt], max_returns: usize, -) -> Option { +) { let returns = num_returns(body); if returns > max_returns { - Some(Diagnostic::new( + checker.report_diagnostic( TooManyReturnStatements { returns, max_returns, }, stmt.identifier(), - )) - } else { - None + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs index 7ce15e254e03c..7033a71d83fc8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_statements.rs @@ -1,8 +1,10 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::identifier::Identifier; +use crate::Violation; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks for functions or methods with too many statements. @@ -137,21 +139,20 @@ fn num_statements(stmts: &[Stmt]) -> usize { /// PLR0915 pub(crate) fn too_many_statements( + checker: &Checker, stmt: &Stmt, body: &[Stmt], max_statements: usize, -) -> Option { +) { let statements = num_statements(body); if statements > max_statements { - Some(Diagnostic::new( + checker.report_diagnostic( TooManyStatements { statements, max_statements, }, stmt.identifier(), - )) - } else { - None + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs index 0180c3cda5587..b4cd0a11f4c69 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs @@ -1,11 +1,11 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::type_param_name; @@ -124,13 +124,13 @@ pub(crate) fn type_bivariance(checker: &Checker, value: &Expr) { return; }; - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TypeBivariance { kind, param_name: type_param_name(arguments).map(ToString::to_string), }, func.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs index d4b0b7ab16335..f73b12fdadeb7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -1,11 +1,11 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::type_param_name; @@ -59,7 +59,9 @@ impl Violation for TypeNameIncorrectVariance { variance, replacement_name, } = self; - format!("`{kind}` name \"{param_name}\" does not reflect its {variance}; consider renaming it to \"{replacement_name}\"") + format!( + "`{kind}` name \"{param_name}\" does not reflect its {variance}; consider renaming it to \"{replacement_name}\"" + ) } } @@ -126,7 +128,7 @@ pub(crate) fn type_name_incorrect_variance(checker: &Checker, value: &Expr) { VarVariance::Invariance => name_root.to_string(), }; - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TypeNameIncorrectVariance { kind, param_name: param_name.to_string(), @@ -134,7 +136,7 @@ pub(crate) fn type_name_incorrect_variance(checker: &Checker, value: &Expr) { replacement_name, }, func.range(), - )); + ); } /// Returns `true` if the parameter name does not match its type variance. diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs index ca08d84e79813..2d1b33bd89fa0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -1,10 +1,10 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::type_param_name; @@ -119,14 +119,14 @@ pub(crate) fn type_param_name_mismatch(checker: &Checker, value: &Expr, targets: return; }; - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( TypeParamNameMismatch { kind, var_name: var_name.to_string(), param_name: param_name.to_string(), }, value.range(), - )); + ); } #[derive(Debug, PartialEq, Eq, Copy, Clone)] diff --git a/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs index de689aee8cdbe..b6bc8c255c45a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -2,11 +2,11 @@ use std::cmp::Ordering; use ruff_python_ast::{Decorator, Parameters, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; +use crate::Violation; use crate::checkers::ast::Checker; #[derive(Debug, Eq, PartialEq)] @@ -186,13 +186,13 @@ pub(crate) fn unexpected_special_method_signature( }; if !valid_signature { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnexpectedSpecialMethodSignature { method_name: name.to_owned(), expected_params, actual_params, }, stmt.identifier(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs index 2173f718ee234..9e1be524b24d2 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dict_index_lookup.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, StmtFor}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::SequenceIndexVisitor; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for key-based dict accesses during `.items()` iterations. @@ -59,12 +59,11 @@ pub(crate) fn unnecessary_dict_index_lookup(checker: &Checker, stmt_for: &StmtFo }; for range in ranges { - let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryDictIndexLookup, range); diagnostic.set_fix(Fix::safe_edits( Edit::range_replacement(value_name.id.to_string(), range), [noop(index_name), noop(value_name)], )); - checker.report_diagnostic(diagnostic); } } @@ -104,12 +103,11 @@ pub(crate) fn unnecessary_dict_index_lookup_comprehension(checker: &Checker, exp }; for range in ranges { - let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryDictIndexLookup, range); diagnostic.set_fix(Fix::safe_edits( Edit::range_replacement(value_name.id.to_string(), range), [noop(index_name), noop(value_name)], )); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs index 1b8775fd3ad91..17d0ab7663247 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -38,6 +38,6 @@ impl Violation for UnnecessaryDirectLambdaCall { /// PLC3002 pub(crate) fn unnecessary_direct_lambda_call(checker: &Checker, expr: &Expr, func: &Expr) { if let Expr::Lambda(_) = func { - checker.report_diagnostic(Diagnostic::new(UnnecessaryDirectLambdaCall, expr.range())); + checker.report_diagnostic(UnnecessaryDirectLambdaCall, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs index 28163c94fc367..62f69fad7bc69 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs @@ -1,11 +1,12 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, OperatorPrecedence, Stmt}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::fix::edits; use crate::rules::pylint::helpers::is_known_dunder_method; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -15,6 +16,34 @@ use ruff_python_ast::PythonVersion; /// Dunder names are not meant to be called explicitly and, in most cases, can /// be replaced with builtins or operators. /// +/// ## Fix safety +/// This fix is always unsafe. When replacing dunder method calls with operators +/// or builtins, the behavior can change in the following ways: +/// +/// 1. Types may implement only a subset of related dunder methods. Calling a +/// missing dunder method directly returns `NotImplemented`, but using the +/// equivalent operator raises a `TypeError`. +/// ```python +/// class C: pass +/// c = C() +/// c.__gt__(1) # before fix: NotImplemented +/// c > 1 # after fix: raises TypeError +/// ``` +/// 2. Instance-assigned dunder methods are ignored by operators and builtins. +/// ```python +/// class C: pass +/// c = C() +/// c.__bool__ = lambda: False +/// c.__bool__() # before fix: False +/// bool(c) # after fix: True +/// ``` +/// +/// 3. Even with built-in types, behavior can differ. +/// ```python +/// (1).__gt__(1.0) # before fix: NotImplemented +/// 1 > 1.0 # after fix: False +/// ``` +/// /// ## Example /// ```python /// three = (3.0).__str__() @@ -34,7 +63,6 @@ use ruff_python_ast::PythonVersion; /// def is_greater_than_two(x: int) -> bool: /// return x > 2 /// ``` -/// #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryDunderCall { method: String, @@ -174,7 +202,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) { } } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryDunderCall { method: attr.to_string(), replacement: title, @@ -205,12 +233,10 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) { } diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( - fixed, + edits::pad(fixed, call.range(), checker.locator()), call.range(), ))); } - - checker.report_diagnostic(diagnostic); } /// Return `true` if this is a dunder method that is allowed to be called explicitly. diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs index 9b6483b31b02c..783063ccda57b 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_lambda.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::{self as ast, visitor, Expr, ExprLambda, Parameter, ParameterWithDefault}; +use ruff_python_ast::{self as ast, Expr, ExprLambda, Parameter, ParameterWithDefault, visitor}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `lambda` definitions that consist of a single function call @@ -46,14 +46,16 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryLambda; -impl AlwaysFixableViolation for UnnecessaryLambda { +impl Violation for UnnecessaryLambda { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Lambda may be unnecessary; consider inlining inner function".to_string() } - fn fix_title(&self) -> String { - "Inline function call".to_string() + fn fix_title(&self) -> Option { + Some("Inline function call".to_string()) } } @@ -63,6 +65,7 @@ pub(crate) fn unnecessary_lambda(checker: &Checker, lambda: &ExprLambda) { parameters, body, range: _, + node_index: _, } = lambda; // The lambda should consist of a single function call. @@ -198,7 +201,7 @@ pub(crate) fn unnecessary_lambda(checker: &Checker, lambda: &ExprLambda) { finder.names }; - for name in names { + for name in &names { if let Some(binding_id) = checker.semantic().resolve_name(name) { let binding = checker.semantic().binding(binding_id); if checker.semantic().is_current_scope(binding.scope) { @@ -207,15 +210,34 @@ pub(crate) fn unnecessary_lambda(checker: &Checker, lambda: &ExprLambda) { } } - let mut diagnostic = Diagnostic::new(UnnecessaryLambda, lambda.range()); - diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement( - checker.locator().slice(func.as_ref()).to_string(), - lambda.range(), - ), - Applicability::Unsafe, - )); - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(UnnecessaryLambda, lambda.range()); + // Suppress the fix if the assignment expression target shadows one of the lambda's parameters. + // This is necessary to avoid introducing a change in the behavior of the program. + for name in names { + if let Some(binding_id) = checker.semantic().lookup_symbol(name.id()) { + let binding = checker.semantic().binding(binding_id); + if checker + .semantic() + .current_scope() + .shadowed_binding(binding_id) + .is_some() + && binding + .expression(checker.semantic()) + .is_some_and(Expr::is_named_expr) + { + return; + } + } + } + + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + if func.is_named_expr() { + format!("({})", checker.locator().slice(func.as_ref())) + } else { + checker.locator().slice(func.as_ref()).to_string() + }, + lambda.range(), + ))); } /// Identify all `Expr::Name` nodes in an AST. diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs index c0874d0d3deb7..991d25bac698a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, Int, Number, StmtFor}; use ruff_python_semantic::SemanticModel; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::SequenceIndexVisitor; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for index-based list accesses during `enumerate` iterations. @@ -61,12 +61,11 @@ pub(crate) fn unnecessary_list_index_lookup(checker: &Checker, stmt_for: &StmtFo }; for range in ranges { - let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListIndexLookup, range); diagnostic.set_fix(Fix::safe_edits( Edit::range_replacement(value_name.id.to_string(), range), [noop(index_name), noop(value_name)], )); - checker.report_diagnostic(diagnostic); } } @@ -105,12 +104,11 @@ pub(crate) fn unnecessary_list_index_lookup_comprehension(checker: &Checker, exp }; for range in ranges { - let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListIndexLookup, range); diagnostic.set_fix(Fix::safe_edits( Edit::range_replacement(value_name.id.to_string(), range), [noop(index_name), noop(value_name)], )); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs b/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs index b7541f5521430..b590573b4c8ad 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs @@ -2,12 +2,12 @@ use std::collections::HashSet; use itertools::Itertools; use ruff_python_ast::{Identifier, Stmt}; -use ruff_python_semantic::cfg::graph::{build_cfg, BlockId, Condition, ControlFlowGraph}; +use ruff_python_semantic::cfg::graph::{BlockId, Condition, ControlFlowGraph, build_cfg}; use ruff_text_size::TextRange; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -64,12 +64,12 @@ pub(crate) fn in_function(checker: &Checker, name: &Identifier, body: &[Stmt]) { let start = cfg.range(start_block).start(); let end = cfg.range(end_block).end(); - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnreachableCode { name: name.to_string(), }, TextRange::new(start, end), - )); + ); } } @@ -98,7 +98,7 @@ fn reachable(cfg: &ControlFlowGraph) -> HashSet { /// Returns `Some(true)` if the condition is always true, e.g. `if True`, same /// with `Some(false)` if it's never taken. If it can't be determined it returns /// `None`, e.g. `if i == 100`. -#[allow(clippy::unnecessary_wraps)] +#[expect(clippy::unnecessary_wraps)] fn taken(condition: &Condition) -> Option { match condition { Condition::Always => Some(true), diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index 0ebee95e794e3..cc9ff1cb29c13 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -1,7 +1,6 @@ use std::fmt::{Display, Formatter}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::SemanticModel; @@ -9,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::add_argument; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for uses of `open` and related calls without an explicit `encoding` @@ -28,6 +28,17 @@ use crate::fix::edits::add_argument; /// Python 3.10 and later, or `locale.getpreferredencoding()` on earlier versions, /// to make the encoding explicit. /// +/// ## Fix safety +/// This fix is always unsafe and may change the program's behavior. It forces +/// `encoding="utf-8"` as the default, regardless of the platform’s actual default +/// encoding, which may cause `UnicodeDecodeError` on non-UTF-8 systems. +/// ```python +/// with open("test.txt") as f: +/// print(f.read()) # before fix (on UTF-8 systems): 你好,世界! +/// with open("test.txt", encoding="utf-8") as f: +/// print(f.read()) # after fix (on Windows): UnicodeDecodeError +/// ``` +/// /// ## Example /// ```python /// open("file.txt") @@ -80,7 +91,7 @@ pub(crate) fn unspecified_encoding(checker: &Checker, call: &ast::ExprCall) { return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnspecifiedEncoding { function_name, mode, @@ -88,7 +99,6 @@ pub(crate) fn unspecified_encoding(checker: &Checker, call: &ast::ExprCall) { call.func.range(), ); diagnostic.set_fix(generate_keyword_fix(checker, call)); - checker.report_diagnostic(diagnostic); } /// Represents the path of the function or method being called. @@ -130,9 +140,10 @@ impl<'a> Callee<'a> { match self { Callee::Qualified(qualified_name) => match qualified_name.segments() { ["" | "codecs" | "_io", "open"] => ModeArgument::Supported, - ["tempfile", "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile"] => { - ModeArgument::Supported - } + [ + "tempfile", + "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile", + ] => ModeArgument::Supported, ["io" | "_io", "TextIOWrapper"] => ModeArgument::Unsupported, _ => ModeArgument::Unsupported, }, @@ -162,6 +173,7 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix { value: Box::from("utf-8"), flags: checker.default_string_flags(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) ), &call.arguments, @@ -204,11 +216,23 @@ fn is_violation(call: &ast::ExprCall, qualified_name: &Callee) -> bool { return false; } } + + let encoding_param_pos = match qualified_name.segments() { + // The `encoding` parameter position for `codecs.open` + ["codecs", _] => 2, + // The `encoding` parameter position for `_io.open` and the builtin `open` + _ => 3, + }; + // else mode not specified, defaults to text mode - call.arguments.find_argument_value("encoding", 3).is_none() + call.arguments + .find_argument_value("encoding", encoding_param_pos) + .is_none() } - ["tempfile", tempfile_class @ ("TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile")] => - { + [ + "tempfile", + tempfile_class @ ("TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile"), + ] => { let mode_pos = usize::from(*tempfile_class == "SpooledTemporaryFile"); if let Some(mode_arg) = call.arguments.find_argument_value("mode", mode_pos) { if is_binary_mode(mode_arg).unwrap_or(true) { diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs index 0161b2e4f0087..5dc74c156a891 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs @@ -1,8 +1,7 @@ use anyhow::Result; use ast::whitespace::indentation; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier; use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt}; use ruff_python_codegen::Stylist; @@ -10,9 +9,10 @@ use ruff_python_index::Indexer; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::fix::edits::adjust_indentation; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `else` clauses on loops without a `break` statement. @@ -70,7 +70,7 @@ pub(crate) fn useless_else_on_loop(checker: &Checker, stmt: &Stmt, body: &[Stmt] let else_range = identifier::else_(stmt, checker.locator().contents()).expect("else clause"); - let mut diagnostic = Diagnostic::new(UselessElseOnLoop, else_range); + let mut diagnostic = checker.report_diagnostic(UselessElseOnLoop, else_range); diagnostic.try_set_fix(|| { remove_else( stmt, @@ -81,7 +81,6 @@ pub(crate) fn useless_else_on_loop(checker: &Checker, stmt: &Stmt, body: &[Stmt] checker.stylist(), ) }); - checker.report_diagnostic(diagnostic); } /// Returns `true` if the given body contains a `break` statement. diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs index 8ed8d8b51511a..4d7be33a71255 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::builtins; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -56,12 +56,11 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp }; if is_builtin_exception(func, checker.semantic(), checker.target_version()) { - let mut diagnostic = Diagnostic::new(UselessExceptionStatement, expr.range()); + let mut diagnostic = checker.report_diagnostic(UselessExceptionStatement, expr.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( "raise ".to_string(), expr.start(), ))); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs index 292f26f8f6ba3..bc9101b646678 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs @@ -1,17 +1,25 @@ use ruff_python_ast::Alias; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::preview::is_ignore_init_files_in_useless_alias_enabled; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for import aliases that do not rename the original package. /// +/// In [preview] this rule does not apply in `__init__.py` files. +/// /// ## Why is this bad? /// The import alias is redundant and should be removed to avoid confusion. /// +/// ## Fix safety +/// This fix is marked as always unsafe because the user may be intentionally +/// re-exporting the import. While statements like `import numpy as numpy` +/// appear redundant, they can have semantic meaning in certain contexts. +/// /// ## Example /// ```python /// import numpy as numpy @@ -27,6 +35,8 @@ use crate::checkers::ast::Checker; /// ```python /// import numpy /// ``` +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] pub(crate) struct UselessImportAlias { required_import_conflict: bool, @@ -37,7 +47,7 @@ impl Violation for UselessImportAlias { #[derive_message_formats] fn message(&self) -> String { - #[allow(clippy::if_not_else)] + #[expect(clippy::if_not_else)] if !self.required_import_conflict { "Import alias does not rename original package".to_string() } else { @@ -62,13 +72,21 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) { if alias.name.as_str() != asname.as_str() { return; } + + // A re-export in __init__.py is probably intentional. + if checker.path().ends_with("__init__.py") + && is_ignore_init_files_in_useless_alias_enabled(checker.settings()) + { + return; + } + // A required import with a useless alias causes an infinite loop. // See https://github.com/astral-sh/ruff/issues/14283 let required_import_conflict = checker - .settings + .settings() .isort .requires_module_import(alias.name.to_string(), Some(asname.to_string())); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UselessImportAlias { required_import_conflict, }, @@ -80,8 +98,6 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) { alias.range(), ))); } - - checker.report_diagnostic(diagnostic); } /// PLC0414 @@ -97,15 +113,21 @@ pub(crate) fn useless_import_from_alias( if alias.name.as_str() != asname.as_str() { return; } + + // A re-export in __init__.py is probably intentional. + if checker.path().ends_with("__init__.py") { + return; + } + // A required import with a useless alias causes an infinite loop. // See https://github.com/astral-sh/ruff/issues/14283 - let required_import_conflict = checker.settings.isort.requires_member_import( + let required_import_conflict = checker.settings().isort.requires_member_import( module.map(str::to_string), alias.name.to_string(), Some(asname.to_string()), level, ); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UselessImportAlias { required_import_conflict, }, @@ -118,6 +140,4 @@ pub(crate) fn useless_import_from_alias( alias.range(), ))); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs index 0143a026f58e6..c33acea2d4720 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_return.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, Stmt}; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for functions that end with an unnecessary `return` or @@ -61,7 +61,12 @@ pub(crate) fn useless_return( }; // Verify that the last statement is a return statement. - let Stmt::Return(ast::StmtReturn { value, range: _ }) = &last_stmt else { + let Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) = &last_stmt + else { return; }; @@ -72,7 +77,12 @@ pub(crate) fn useless_return( // Skip functions that consist of a docstring and a return statement. if body.len() == 2 { - if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = &body[0] { + if let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = &body[0] + { if value.is_string_literal_expr() { return; } @@ -94,10 +104,9 @@ pub(crate) fn useless_return( return; } - let mut diagnostic = Diagnostic::new(UselessReturn, last_stmt.range()); + let mut diagnostic = checker.report_diagnostic(UselessReturn, last_stmt.range()); let edit = fix::edits::delete_stmt(last_stmt, Some(stmt), checker.locator(), checker.indexer()); diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_id(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs index 02d0b1df4ab9a..77931841f0016 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -80,6 +80,6 @@ pub(crate) fn useless_with_lock(checker: &Checker, with: &ast::StmtWith) { return; } - checker.report_diagnostic(Diagnostic::new(UselessWithLock, call.range())); + checker.report_diagnostic(UselessWithLock, call.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs b/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs index 7a8c31e744cf7..60fc10bc5237f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/yield_from_in_async_function.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::ScopeKind; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -43,6 +43,6 @@ pub(crate) fn yield_from_in_async_function(checker: &Checker, expr: &ast::ExprYi checker.semantic().current_scope().kind, ScopeKind::Function(ast::StmtFunctionDef { is_async: true, .. }) ) { - checker.report_diagnostic(Diagnostic::new(YieldFromInAsyncFunction, expr.range())); + checker.report_diagnostic(YieldFromInAsyncFunction, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs b/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs index 60a0c349b16d3..67236eed6bad9 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/yield_in_init.rs @@ -1,9 +1,9 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_method; @@ -40,7 +40,7 @@ impl Violation for YieldInInit { /// PLE0100 pub(crate) fn yield_in_init(checker: &Checker, expr: &Expr) { - if in_dunder_method("__init__", checker.semantic(), checker.settings) { - checker.report_diagnostic(Diagnostic::new(YieldInInit, expr.range())); + if in_dunder_method("__init__", checker.semantic(), checker.settings()) { + checker.report_diagnostic(YieldInInit, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0207_missing_maxsplit_arg.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0207_missing_maxsplit_arg.py.snap new file mode 100644 index 0000000000000..f1b01001f6291 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0207_missing_maxsplit_arg.py.snap @@ -0,0 +1,324 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +12 | # Errors +13 | ## Test split called directly on string literal +14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^ PLC0207 +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +13 | ## Test split called directly on string literal +14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg] +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] +17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg] +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()` + | +15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg] +16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg] +17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +18 | +19 | ## Test split called on string variable + | + +missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +19 | ## Test split called on string variable +20 | SEQ.split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^ PLC0207 +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +19 | ## Test split called on string variable +20 | SEQ.split(",")[0] # [missing-maxsplit-arg] +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^ PLC0207 +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] +23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +20 | SEQ.split(",")[0] # [missing-maxsplit-arg] +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^ PLC0207 +23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()` + | +21 | SEQ.split(",")[-1] # [missing-maxsplit-arg] +22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg] +23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^ PLC0207 +24 | +25 | ## Test split called on class attribute + | + +missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +25 | ## Test split called on class attribute +26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +25 | ## Test split called on class attribute +26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg] +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] +29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg] +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()` + | +27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg] +28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg] +29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +30 | +31 | ## Test split called on sliced string + | + +missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +31 | ## Test split called on sliced string +32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +31 | ## Test split called on sliced string +32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg] +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^ PLC0207 +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg] +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg] +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] +38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg] +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()` + | +36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg] +37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg] +38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +39 | +40 | ## Test sep given as named argument + | + +missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +40 | ## Test sep given as named argument +41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +40 | ## Test sep given as named argument +41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] +44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg] +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()` + | +42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg] +43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg] +44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +45 | +46 | ## Special cases + | + +missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +46 | ## Special cases +47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg] +49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +46 | ## Special cases +47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg] +48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg] +48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg] +49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +50 | +51 | ## Test class attribute named split + | + +missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +51 | ## Test class attribute named split +52 | Bar.split.split(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1` + | +51 | ## Test class attribute named split +52 | Bar.split.split(",")[0] # [missing-maxsplit-arg] +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] +55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1` + | +52 | Bar.split.split(",")[0] # [missing-maxsplit-arg] +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + | + +missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()` + | +53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg] +54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg] +55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +56 | +57 | ## Test unpacked dict literal kwargs + | + +missing_maxsplit_arg.py:58:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +57 | ## Test unpacked dict literal kwargs +58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 + | + +missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +177 | # Errors +178 | kwargs_without_maxsplit = {"seq": ","} +179 | "1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +180 | # OK +181 | kwargs_with_maxsplit = {"maxsplit": 1} + | + +missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +180 | # OK +181 | kwargs_with_maxsplit = {"maxsplit": 1} +182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 +183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} +184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive + | + +missing_maxsplit_arg.py:184:1: PLC0207 Pass `maxsplit=1` into `str.split()` + | +182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive +183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1} +184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207 + | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0414_import_aliasing_2____init__.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0414_import_aliasing_2____init__.py.snap new file mode 100644 index 0000000000000..101902f31102b --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0414_import_aliasing_2____init__.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +__init__.py:1:8: PLC0414 [*] Import alias does not rename original package + | +1 | import collections as collections + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0414 +2 | from collections import OrderedDict as OrderedDict +3 | from . import foo as foo + | + = help: Remove import alias + +ℹ Unsafe fix +1 |-import collections as collections + 1 |+import collections +2 2 | from collections import OrderedDict as OrderedDict +3 3 | from . import foo as foo +4 4 | from .foo import bar as bar diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC1802_len_as_condition.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC1802_len_as_condition.py.snap index 2b64669ae4f89..93d27a16054f8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC1802_len_as_condition.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC1802_len_as_condition.py.snap @@ -396,6 +396,47 @@ len_as_condition.py:130:12: PLC1802 [*] `len(set((w + 1) for w in set()))` used 132 132 | 133 133 | import numpy +len_as_condition.py:193:8: PLC1802 [*] `len(x)` used as condition without comparison + | +191 | if cond: +192 | x = [4,5,6] +193 | if len(x): # this should be addressed + | ^^^^^^ PLC1802 +194 | print(x) + | + = help: Remove `len` + +ℹ Safe fix +190 190 | x = [1,2,3] +191 191 | if cond: +192 192 | x = [4,5,6] +193 |- if len(x): # this should be addressed + 193 |+ if x: # this should be addressed +194 194 | print(x) +195 195 | +196 196 | def g(cond:bool): + +len_as_condition.py:200:8: PLC1802 [*] `len(x)` used as condition without comparison + | +198 | if cond: +199 | x = [4,5,6] +200 | if len(x): # this should be addressed + | ^^^^^^ PLC1802 +201 | print(x) +202 | del x + | + = help: Remove `len` + +ℹ Safe fix +197 197 | x = [1,2,3] +198 198 | if cond: +199 199 | x = [4,5,6] +200 |- if len(x): # this should be addressed + 200 |+ if x: # this should be addressed +201 201 | print(x) +202 202 | del x +203 203 | + len_as_condition.py:214:8: PLC1802 [*] `len(x)` used as condition without comparison | 212 | def inner(x:int): @@ -416,6 +457,26 @@ len_as_condition.py:214:8: PLC1802 [*] `len(x)` used as condition without compar 216 216 | 217 217 | def redefined(): +len_as_condition.py:220:8: PLC1802 [*] `len(x)` used as condition without comparison + | +218 | x = 123 +219 | x = [1, 2, 3] +220 | if len(x): # this should be addressed + | ^^^^^^ PLC1802 +221 | print(x) + | + = help: Remove `len` + +ℹ Safe fix +217 217 | def redefined(): +218 218 | x = 123 +219 219 | x = [1, 2, 3] +220 |- if len(x): # this should be addressed + 220 |+ if x: # this should be addressed +221 221 | print(x) +222 222 | +223 223 | global_seq = [1, 2, 3] + len_as_condition.py:233:8: PLC1802 [*] `len(x)` used as condition without comparison | 231 | if False: @@ -452,6 +513,8 @@ len_as_condition.py:237:6: PLC1802 [*] `len(ascii(1))` used as condition without 237 |-bool(len(ascii(1))) 237 |+bool(ascii(1)) 238 238 | bool(len(sorted(""))) +239 239 | +240 240 | # regression tests for https://github.com/astral-sh/ruff/issues/18811 len_as_condition.py:238:6: PLC1802 [*] `len(sorted(""))` used as condition without comparison | @@ -459,6 +522,8 @@ len_as_condition.py:238:6: PLC1802 [*] `len(sorted(""))` used as condition witho 237 | bool(len(ascii(1))) 238 | bool(len(sorted(""))) | ^^^^^^^^^^^^^^^ PLC1802 +239 | +240 | # regression tests for https://github.com/astral-sh/ruff/issues/18811 | = help: Remove `len` @@ -468,3 +533,49 @@ len_as_condition.py:238:6: PLC1802 [*] `len(sorted(""))` used as condition witho 237 237 | bool(len(ascii(1))) 238 |-bool(len(sorted(""))) 238 |+bool(sorted("")) +239 239 | +240 240 | # regression tests for https://github.com/astral-sh/ruff/issues/18811 +241 241 | fruits = [] + +len_as_condition.py:242:3: PLC1802 [*] `len(fruits)` used as condition without comparison + | +240 | # regression tests for https://github.com/astral-sh/ruff/issues/18811 +241 | fruits = [] +242 | if(len)(fruits): + | ^^^^^^^^^^^^^ PLC1802 +243 | ... + | + = help: Remove `len` + +ℹ Safe fix +239 239 | +240 240 | # regression tests for https://github.com/astral-sh/ruff/issues/18811 +241 241 | fruits = [] +242 |-if(len)(fruits): + 242 |+if fruits: +243 243 | ... +244 244 | +245 245 | # regression tests for https://github.com/astral-sh/ruff/issues/18812 + +len_as_condition.py:247:4: PLC1802 [*] `len(fruits)` used as condition without comparison + | +245 | # regression tests for https://github.com/astral-sh/ruff/issues/18812 +246 | fruits = [] +247 | if len( + | ____^ +248 | | fruits # comment +249 | | ): + | |_^ PLC1802 +250 | ... + | + = help: Remove `len` + +ℹ Unsafe fix +244 244 | +245 245 | # regression tests for https://github.com/astral-sh/ruff/issues/18812 +246 246 | fruits = [] +247 |-if len( +248 |- fruits # comment +249 |-): + 247 |+if fruits: +250 248 | ... diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap index 4d897e73d97b6..971cc51c39cb8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC2801_unnecessary_dunder_call.py.snap @@ -1193,6 +1193,8 @@ unnecessary_dunder_call.py:131:8: PLC2801 [*] Unnecessary dunder call to `__str_ 130 | # https://github.com/astral-sh/ruff/issues/14597 131 | assert "abc".__str__() == "abc" | ^^^^^^^^^^^^^^^ PLC2801 +132 | +133 | # https://github.com/astral-sh/ruff/issues/18813 | = help: Use `str()` builtin @@ -1202,3 +1204,21 @@ unnecessary_dunder_call.py:131:8: PLC2801 [*] Unnecessary dunder call to `__str_ 130 130 | # https://github.com/astral-sh/ruff/issues/14597 131 |-assert "abc".__str__() == "abc" 131 |+assert str("abc") == "abc" +132 132 | +133 133 | # https://github.com/astral-sh/ruff/issues/18813 +134 134 | three = 1 if 1 else(3.0).__str__() + +unnecessary_dunder_call.py:134:20: PLC2801 [*] Unnecessary dunder call to `__str__`. Use `str()` builtin. + | +133 | # https://github.com/astral-sh/ruff/issues/18813 +134 | three = 1 if 1 else(3.0).__str__() + | ^^^^^^^^^^^^^^^ PLC2801 + | + = help: Use `str()` builtin + +ℹ Unsafe fix +131 131 | assert "abc".__str__() == "abc" +132 132 | +133 133 | # https://github.com/astral-sh/ruff/issues/18813 +134 |-three = 1 if 1 else(3.0).__str__() + 134 |+three = 1 if 1 else str(3.0) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap index 5d1994780df2a..adabada987380 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap @@ -123,3 +123,11 @@ load_before_global_declaration.py:113:14: PLE0118 Name `x` is used prior to glob | ^ PLE0118 114 | global x | + +load_before_global_declaration.py:162:1: PLE0118 Name `x` is used prior to global declaration on line 163 + | +161 | # surprisingly still an error, global in module scope +162 | x = None + | ^ PLE0118 +163 | global x + | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0241_duplicate_bases.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0241_duplicate_bases.py.snap index ca3062908ba7d..7848ac3fb60de 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0241_duplicate_bases.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0241_duplicate_bases.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pylint/mod.rs -snapshot_kind: text --- duplicate_bases.py:13:13: PLE0241 [*] Duplicate base `A` for class `F1` | @@ -155,3 +154,24 @@ duplicate_bases.py:54:5: PLE0241 [*] Duplicate base `A` for class `G4` 55 54 | B, 56 55 | ): 57 56 | ... + +duplicate_bases.py:76:5: PLE0241 [*] Duplicate base `Foo` for class `Bar` + | +74 | # https://github.com/astral-sh/ruff/issues/18814 +75 | class Bar(Foo, # 1 +76 | Foo # 2 + | ^^^ PLE0241 +77 | ): +78 | pass + | + = help: Remove duplicate base + +ℹ Unsafe fix +72 72 | ... +73 73 | +74 74 | # https://github.com/astral-sh/ruff/issues/18814 +75 |-class Bar(Foo, # 1 +76 |- Foo # 2 + 75 |+class Bar(Foo # 2 +77 76 | ): +78 77 | pass diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap index 46b9cb6a3fcc5..408e2ababfc70 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters_syntax_error.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters_syntax_error.py.snap index 55449038ed237..191bff373de70 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters_syntax_error.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters_syntax_error.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pylint/mod.rs -snapshot_kind: text --- invalid_characters_syntax_error.py:5:6: PLE2510 Invalid unescaped character backspace, use "\b" instead | @@ -17,7 +16,7 @@ invalid_characters_syntax_error.py:7:5: SyntaxError: missing closing quote in st 5 | b = '␈' 6 | # Unterminated string 7 | b = '␈ - | ^ + | ^^ 8 | b = '␈' 9 | # Unterminated f-string | @@ -99,7 +98,7 @@ invalid_characters_syntax_error.py:13:14: SyntaxError: missing closing quote in 11 | b = f'␈' 12 | # Implicitly concatenated 13 | b = '␈' f'␈' '␈ - | ^ + | ^^ | invalid_characters_syntax_error.py:13:16: SyntaxError: Expected a statement diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap index 88630ccb7a5a9..2f7825c655c9c 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap index eefd991446f13..f07f158d08a38 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap index a06d6f645f090..f6e122931c66b 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap index 5b50a43e0589e..833b37b2e5593 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap index 94b7385d0a67e..89aec8775ee6c 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap @@ -359,7 +359,7 @@ if_stmt_min_max.py:119:1: PLR1730 [*] Replace `if` statement with `A2 = max(A2, | = help: Replace with `A2 = max(A2, A1)` -ℹ Safe fix +ℹ Unsafe fix 116 116 | A1 = AA(0) 117 117 | A2 = AA(3) 118 118 | @@ -382,7 +382,7 @@ if_stmt_min_max.py:122:1: PLR1730 [*] Replace `if` statement with `A2 = max(A1, | = help: Replace with `A2 = max(A1, A2)` -ℹ Safe fix +ℹ Unsafe fix 119 119 | if A2 < A1: # [max-instead-of-if] 120 120 | A2 = A1 121 121 | @@ -405,7 +405,7 @@ if_stmt_min_max.py:125:1: PLR1730 [*] Replace `if` statement with `A2 = min(A2, | = help: Replace with `A2 = min(A2, A1)` -ℹ Safe fix +ℹ Unsafe fix 122 122 | if A2 <= A1: # [max-instead-of-if] 123 123 | A2 = A1 124 124 | @@ -428,7 +428,7 @@ if_stmt_min_max.py:128:1: PLR1730 [*] Replace `if` statement with `A2 = min(A1, | = help: Replace with `A2 = min(A1, A2)` -ℹ Safe fix +ℹ Unsafe fix 125 125 | if A2 > A1: # [min-instead-of-if] 126 126 | A2 = A1 127 127 | @@ -721,6 +721,8 @@ if_stmt_min_max.py:238:1: PLR1730 [*] Replace `if` statement with `counter["b"] 238 | / if counter["a"] > counter["b"]: 239 | | counter["b"] = counter["a"] | |_______________________________^ PLR1730 +240 | +241 | # https://github.com/astral-sh/ruff/issues/17311 | = help: Replace with `counter["b"] = max(counter["b"], counter["a"])` @@ -731,3 +733,48 @@ if_stmt_min_max.py:238:1: PLR1730 [*] Replace `if` statement with `counter["b"] 238 |-if counter["a"] > counter["b"]: 239 |- counter["b"] = counter["a"] 238 |+counter["b"] = max(counter["b"], counter["a"]) +240 239 | +241 240 | # https://github.com/astral-sh/ruff/issues/17311 +242 241 | + +if_stmt_min_max.py:245:1: PLR1730 [*] Replace `if` statement with `a = min(b, a)` + | +243 | # fix marked unsafe as delete comments +244 | a, b = [], [] +245 | / if a >= b: +246 | | # very important comment +247 | | a = b + | |_________^ PLR1730 +248 | +249 | # fix marked safe as preserve comments + | + = help: Replace with `a = min(b, a)` + +ℹ Unsafe fix +242 242 | +243 243 | # fix marked unsafe as delete comments +244 244 | a, b = [], [] +245 |-if a >= b: +246 |- # very important comment +247 |- a = b + 245 |+a = min(b, a) +248 246 | +249 247 | # fix marked safe as preserve comments +250 248 | if a >= b: + +if_stmt_min_max.py:250:1: PLR1730 [*] Replace `if` statement with `a = min(b, a)` + | +249 | # fix marked safe as preserve comments +250 | / if a >= b: +251 | | a = b # very important comment + | |_________^ PLR1730 + | + = help: Replace with `a = min(b, a)` + +ℹ Safe fix +247 247 | a = b +248 248 | +249 249 | # fix marked safe as preserve comments +250 |-if a >= b: +251 |- a = b # very important comment + 250 |+a = min(b, a) # very important comment diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap index e4e25f27e649d..73dd687d2fe5f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap @@ -155,3 +155,29 @@ unnecessary_lambda.py:10:5: PLW0108 [*] Lambda may be unnecessary; consider inli 11 11 | 12 12 | # default value in lambda parameters 13 13 | _ = lambda x=42: print(x) + +unnecessary_lambda.py:62:5: PLW0108 [*] Lambda may be unnecessary; consider inlining inner function + | +61 | # https://github.com/astral-sh/ruff/issues/18675 +62 | _ = lambda x: (string := str)(x) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW0108 +63 | _ = lambda x: ((x := 1) and str)(x) + | + = help: Inline function call + +ℹ Unsafe fix +59 59 | _ = lambda *args: f(*args, y=x) +60 60 | +61 61 | # https://github.com/astral-sh/ruff/issues/18675 +62 |-_ = lambda x: (string := str)(x) + 62 |+_ = (string := str) +63 63 | _ = lambda x: ((x := 1) and str)(x) + +unnecessary_lambda.py:63:5: PLW0108 Lambda may be unnecessary; consider inlining inner function + | +61 | # https://github.com/astral-sh/ruff/issues/18675 +62 | _ = lambda x: (string := str)(x) +63 | _ = lambda x: ((x := 1) and str)(x) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW0108 + | + = help: Inline function call diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0128_redeclared_assigned_name.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0128_redeclared_assigned_name.py.snap index c3f54bdf9b68d..13c70ddf96eac 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0128_redeclared_assigned_name.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0128_redeclared_assigned_name.py.snap @@ -25,6 +25,7 @@ redeclared_assigned_name.py:3:9: PLW0128 Redeclared variable `FIRST` in assignme 3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128 | ^^^^^ PLW0128 4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 | redeclared_assigned_name.py:3:32: PLW0128 Redeclared variable `FIRST` in assignment @@ -34,6 +35,7 @@ redeclared_assigned_name.py:3:32: PLW0128 Redeclared variable `FIRST` in assignm 3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128 | ^^^^^ PLW0128 4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 | redeclared_assigned_name.py:4:23: PLW0128 Redeclared variable `FIRST` in assignment @@ -42,8 +44,8 @@ redeclared_assigned_name.py:4:23: PLW0128 Redeclared variable `FIRST` in assignm 3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128 4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 | ^^^^^ PLW0128 -5 | -6 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 +6 | FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 | redeclared_assigned_name.py:4:30: PLW0128 Redeclared variable `SECOND` in assignment @@ -52,6 +54,44 @@ redeclared_assigned_name.py:4:30: PLW0128 Redeclared variable `SECOND` in assign 3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128 4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 | ^^^^^^ PLW0128 -5 | -6 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 +6 | FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 + | + +redeclared_assigned_name.py:5:9: PLW0128 Redeclared variable `FIRST` in assignment + | +3 | FIRST, (FIRST, SECOND, (THIRD, FIRST)) = (1, (1, 2)) # PLW0128 +4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 + | ^^^^^ PLW0128 +6 | FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 +7 | FIRST, *FIRST = (1, 2) # PLW0128 + | + +redeclared_assigned_name.py:6:9: PLW0128 Redeclared variable `FIRST` in assignment + | +4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 +6 | FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 + | ^^^^^ PLW0128 +7 | FIRST, *FIRST = (1, 2) # PLW0128 + | + +redeclared_assigned_name.py:6:32: PLW0128 Redeclared variable `FIRST` in assignment + | +4 | FIRST, SECOND, THIRD, FIRST, SECOND = (1, 2, 3, 4) # PLW0128 +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 +6 | FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 + | ^^^^^ PLW0128 +7 | FIRST, *FIRST = (1, 2) # PLW0128 + | + +redeclared_assigned_name.py:7:9: PLW0128 Redeclared variable `FIRST` in assignment + | +5 | FIRST, [FIRST, SECOND] = (1, (1, 2)) # PLW0128 +6 | FIRST, [FIRST, SECOND, [THIRD, FIRST]] = (1, (1, 2)) # PLW0128 +7 | FIRST, *FIRST = (1, 2) # PLW0128 + | ^^^^^ PLW0128 +8 | +9 | FIRST, SECOND, _, _, _ignored = (1, 2, 3, 4, 5) # OK | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap index a9c4292f14771..3cc8d69737b6f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap @@ -107,3 +107,20 @@ nan_comparison.py:60:10: PLW0177 Comparing against a NaN value; use `math.isnan` 61 | 62 | # No errors | + +nan_comparison.py:98:13: PLW0177 Comparing against a NaN value; use `math.isnan` instead + | +96 | # PLW0117 +97 | # https://github.com/astral-sh/ruff/issues/18596 +98 | assert x == float("-NaN ") + | ^^^^^^^^^^^^^^ PLW0177 +99 | assert x == float(" \n+nan \t") + | + +nan_comparison.py:99:13: PLW0177 Comparing against a NaN value; use `math.isnan` instead + | +97 | # https://github.com/astral-sh/ruff/issues/18596 +98 | assert x == float("-NaN ") +99 | assert x == float(" \n+nan \t") + | ^^^^^^^^^^^^^^^^^^^^^ PLW0177 + | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1510_subprocess_run_without_check.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1510_subprocess_run_without_check.py.snap index 7e6539ec04c0b..b0d1645a5d07f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1510_subprocess_run_without_check.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1510_subprocess_run_without_check.py.snap @@ -37,7 +37,7 @@ subprocess_run_without_check.py:5:1: PLW1510 [*] `subprocess.run` without explic 3 3 | # Errors. 4 4 | subprocess.run("ls") 5 |-subprocess.run("ls", shell=True) - 5 |+subprocess.run("ls", shell=True, check=False) + 5 |+subprocess.run("ls", check=False, shell=True) 6 6 | subprocess.run( 7 7 | ["ls"], 8 8 | shell=False, @@ -58,7 +58,7 @@ subprocess_run_without_check.py:6:1: PLW1510 [*] `subprocess.run` without explic 6 6 | subprocess.run( 7 7 | ["ls"], 8 |- shell=False, - 8 |+ shell=False, check=False, + 8 |+ check=False, shell=False, 9 9 | ) 10 10 | subprocess.run(["ls"], **kwargs) 11 11 | @@ -79,7 +79,7 @@ subprocess_run_without_check.py:10:1: PLW1510 [*] `subprocess.run` without expli 8 8 | shell=False, 9 9 | ) 10 |-subprocess.run(["ls"], **kwargs) - 10 |+subprocess.run(["ls"], **kwargs, check=False) + 10 |+subprocess.run(["ls"], check=False, **kwargs) 11 11 | 12 12 | # Non-errors. 13 13 | subprocess.run("ls", check=True) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap index af86b45d1d4c9..0afee2e28623c 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap @@ -169,7 +169,7 @@ nested_min_max.py:15:1: PLW3301 [*] Nested `min` calls can be flattened 15 | min(1, min(2, 3, key=test), min(4, 5)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301 16 | -17 | # Don't provide a fix if there are comments within the call. +17 | # The fix is already unsafe, so deleting comments is okay. | = help: Flatten nested `min` calls @@ -180,12 +180,12 @@ nested_min_max.py:15:1: PLW3301 [*] Nested `min` calls can be flattened 15 |-min(1, min(2, 3, key=test), min(4, 5)) 15 |+min(1, min(2, 3, key=test), 4, 5) 16 16 | -17 17 | # Don't provide a fix if there are comments within the call. +17 17 | # The fix is already unsafe, so deleting comments is okay. 18 18 | min( -nested_min_max.py:18:1: PLW3301 Nested `min` calls can be flattened +nested_min_max.py:18:1: PLW3301 [*] Nested `min` calls can be flattened | -17 | # Don't provide a fix if there are comments within the call. +17 | # The fix is already unsafe, so deleting comments is okay. 18 | / min( 19 | | 1, # This is a comment. 20 | | min(2, 3), @@ -196,6 +196,19 @@ nested_min_max.py:18:1: PLW3301 Nested `min` calls can be flattened | = help: Flatten nested `min` calls +ℹ Unsafe fix +15 15 | min(1, min(2, 3, key=test), min(4, 5)) +16 16 | +17 17 | # The fix is already unsafe, so deleting comments is okay. +18 |-min( +19 |- 1, # This is a comment. +20 |- min(2, 3), +21 |-) + 18 |+min(1, 2, 3) +22 19 | +23 20 | # Handle iterable expressions. +24 21 | min(1, min(a)) + nested_min_max.py:24:1: PLW3301 [*] Nested `min` calls can be flattened | 23 | # Handle iterable expressions. diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview_useless_import_alias.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview_useless_import_alias.snap new file mode 100644 index 0000000000000..6c123427ab2b8 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview_useless_import_alias.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 822e680acfceb..76ff4b09835b2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -17,7 +17,7 @@ mod tests { use crate::rules::pyupgrade; use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] #[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))] @@ -36,12 +36,14 @@ mod tests { #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))] #[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))] #[test_case(Rule::NativeLiterals, Path::new("UP018.py"))] + #[test_case(Rule::NativeLiterals, Path::new("UP018_CR.py"))] + #[test_case(Rule::NativeLiterals, Path::new("UP018_LF.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_1.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_3.py"))] #[test_case(Rule::NonPEP604AnnotationUnion, Path::new("UP007.py"))] - #[test_case(Rule::NonPEP604AnnotationUnion, Path::new("UP045.py"))] + #[test_case(Rule::NonPEP604AnnotationOptional, Path::new("UP045.py"))] #[test_case(Rule::NonPEP604Isinstance, Path::new("UP038.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_1.py"))] @@ -109,26 +111,28 @@ mod tests { #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))] #[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))] #[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))] + #[test_case(Rule::UselessClassMetaclassType, Path::new("UP050.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } - #[test] - fn up007_preview() -> Result<()> { + #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] + fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}__preview", path.to_string_lossy()); let diagnostics = test_path( - Path::new("pyupgrade/UP045.py"), + Path::new("pyupgrade").join(path).as_path(), &settings::LinterSettings { preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationUnion) + ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -137,11 +141,11 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP041.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -150,11 +154,11 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP040.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311, + unresolved_target_version: PythonVersion::PY311.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -166,11 +170,11 @@ mod tests { pyupgrade: pyupgrade::settings::Settings { keep_runtime_typing: true, }, - unresolved_target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -182,11 +186,11 @@ mod tests { pyupgrade: pyupgrade::settings::Settings { keep_runtime_typing: true, }, - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -195,11 +199,11 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -208,11 +212,11 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -221,14 +225,14 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37, + unresolved_target_version: PythonVersion::PY37.into(), ..settings::LinterSettings::for_rules([ Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationOptional, ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -237,14 +241,14 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..settings::LinterSettings::for_rules([ Rule::NonPEP604AnnotationUnion, Rule::NonPEP604AnnotationOptional, ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -253,11 +257,11 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP017.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311, + unresolved_target_version: PythonVersion::PY311.into(), ..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -266,11 +270,11 @@ mod tests { let diagnostics = test_path( Path::new("pyupgrade/UP044.py"), &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311, + unresolved_target_version: PythonVersion::PY311.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 0c4299ba3b3cd..92f163a57559e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -1,7 +1,6 @@ use log::debug; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt}; @@ -13,6 +12,7 @@ use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `NamedTuple` declarations that use functional syntax. @@ -87,6 +87,7 @@ pub(crate) fn convert_named_tuple_functional_to_class( // Ex) `NamedTuple("MyType")` ([_typename], []) => vec![Stmt::Pass(ast::StmtPass { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })], // Ex) `NamedTuple("MyType", [("a", int), ("b", str)])` ([_typename, fields], []) => { @@ -113,7 +114,7 @@ pub(crate) fn convert_named_tuple_functional_to_class( } }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ConvertNamedTupleFunctionalToClass { name: typename.to_string(), }, @@ -130,7 +131,6 @@ pub(crate) fn convert_named_tuple_functional_to_class( checker.comment_ranges(), )); } - checker.report_diagnostic(diagnostic); } /// Return the typename, args, keywords, and base class. @@ -146,6 +146,7 @@ fn match_named_tuple_assign<'a>( func, arguments: Arguments { args, keywords, .. }, range: _, + node_index: _, }) = value else { return None; @@ -164,6 +165,7 @@ fn create_field_assignment_stmt(field: Name, annotation: &Expr) -> Stmt { id: field, ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), @@ -171,6 +173,7 @@ fn create_field_assignment_stmt(field: Name, annotation: &Expr) -> Stmt { value: None, simple: true, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into() } @@ -181,6 +184,7 @@ fn create_fields_from_fields_arg(fields: &Expr) -> Option> { if fields.is_empty() { let node = Stmt::Pass(ast::StmtPass { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Some(vec![node]) } else { @@ -232,11 +236,13 @@ fn create_class_def_stmt(typename: &str, body: Vec, base_class: &Expr) -> args: Box::from([base_class.clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), body, type_params: None, decorator_list: vec![], range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into() } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 09ace2c627306..97f16190782ec 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; @@ -9,6 +8,7 @@ use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `TypedDict` declarations that use functional syntax. @@ -97,7 +97,7 @@ pub(crate) fn convert_typed_dict_functional_to_class( return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ConvertTypedDictFunctionalToClass { name: class_name.to_string(), }, @@ -115,7 +115,6 @@ pub(crate) fn convert_typed_dict_functional_to_class( checker.comment_ranges(), )); } - checker.report_diagnostic(diagnostic); } /// Return the class name, arguments, keywords and base class for a `TypedDict` @@ -132,6 +131,7 @@ fn match_typed_dict_assign<'a>( func, arguments, range: _, + node_index: _, }) = value else { return None; @@ -150,6 +150,7 @@ fn create_field_assignment_stmt(field: &str, annotation: &Expr) -> Stmt { id: field.into(), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), @@ -157,6 +158,7 @@ fn create_field_assignment_stmt(field: &str, annotation: &Expr) -> Stmt { value: None, simple: true, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into() } @@ -177,11 +179,13 @@ fn create_class_def_stmt( None => Box::from([]), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), body, type_params: None, decorator_list: vec![], range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into() } @@ -190,6 +194,7 @@ fn fields_from_dict_literal(items: &[ast::DictItem]) -> Option> { if items.is_empty() { let node = Stmt::Pass(ast::StmtPass { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Some(vec![node]) } else { @@ -223,6 +228,7 @@ fn fields_from_dict_call(func: &Expr, keywords: &[Keyword]) -> Option> if keywords.is_empty() { let node = Stmt::Pass(ast::StmtPass { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Some(vec![node]) } else { @@ -235,6 +241,7 @@ fn fields_from_keywords(keywords: &[Keyword]) -> Option> { if keywords.is_empty() { let node = Stmt::Pass(ast::StmtPass { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); return Some(vec![node]); } @@ -257,13 +264,16 @@ fn match_fields_and_total(arguments: &Arguments) -> Option<(Vec, Option<&K ([_typename, fields], [..]) => { let total = arguments.find_keyword("total"); match fields { - Expr::Dict(ast::ExprDict { items, range: _ }) => { - Some((fields_from_dict_literal(items)?, total)) - } + Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) => Some((fields_from_dict_literal(items)?, total)), Expr::Call(ast::ExprCall { func, arguments: Arguments { keywords, .. }, range: _, + node_index: _, }) => Some((fields_from_dict_call(func, keywords)?, total)), _ => None, } @@ -272,6 +282,7 @@ fn match_fields_and_total(arguments: &Arguments) -> Option<(Vec, Option<&K ([_typename], []) => { let node = Stmt::Pass(ast::StmtPass { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Some((vec![node], None)) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 77c899c752f7b..b67f312699739 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -1,11 +1,11 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `datetime.timezone.utc`. @@ -58,7 +58,7 @@ pub(crate) fn datetime_utc_alias(checker: &Checker, expr: &Expr) { matches!(qualified_name.segments(), ["datetime", "timezone", "utc"]) }) { - let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC, expr.range()); + let mut diagnostic = checker.report_diagnostic(DatetimeTimezoneUTC, expr.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import_from("datetime", "UTC"), @@ -68,6 +68,5 @@ pub(crate) fn datetime_utc_alias(checker: &Checker, expr: &Expr) { let reference_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::safe_edits(import_edit, [reference_edit])) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs index 177bb99d9a89d..ab1bd978f739a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Stmt}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of the `xml.etree.cElementTree` module. @@ -42,19 +42,22 @@ fn add_check_for_node(checker: &Checker, node: &T) where T: Ranged, { - let mut diagnostic = Diagnostic::new(DeprecatedCElementTree, node.range()); + let mut diagnostic = checker.report_diagnostic(DeprecatedCElementTree, node.range()); let contents = checker.locator().slice(node); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( contents.replacen("cElementTree", "ElementTree", 1), node.range(), ))); - checker.report_diagnostic(diagnostic); } /// UP023 pub(crate) fn deprecated_c_element_tree(checker: &Checker, stmt: &Stmt) { match stmt { - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { // Ex) `import xml.etree.cElementTree as ET` for name in names { if &name.name == "xml.etree.cElementTree" && name.asname.is_some() { @@ -67,6 +70,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &Checker, stmt: &Stmt) { names, level, range: _, + node_index: _, }) => { if *level > 0 { // Ex) `import .xml.etree.cElementTree as ET` diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs index 05d00bd6f6e0d..022db3909737c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs @@ -1,16 +1,16 @@ use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{Alias, StmtImportFrom}; use ruff_python_codegen::Stylist; use ruff_python_parser::Tokens; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::pyupgrade::fixes; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; /// An import was moved and renamed as part of a deprecation. @@ -271,7 +271,7 @@ const TYPING_TO_RE_39: &[&str] = &["Match", "Pattern"]; const TYPING_RE_TO_RE_39: &[&str] = &["Match", "Pattern"]; // Members of `typing_extensions` that were moved to `typing`. -const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated", "get_type_hints"]; +const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated"]; // Members of `typing` that were moved _and_ renamed (and thus cannot be // automatically fixed). @@ -373,6 +373,9 @@ const TYPING_EXTENSIONS_TO_TYPING_313: &[&str] = &[ "NoDefault", "ReadOnly", "TypeIs", + // Introduced in Python 3.5, + // but typing_extensions backports features from py313: + "get_type_hints", // Introduced in Python 3.6, // but typing_extensions backports features from py313: "ContextManager", @@ -726,7 +729,7 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport ); for (operation, fix) in fixer.without_renames() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DeprecatedImport { deprecation: Deprecation::WithoutRename(operation), }, @@ -738,16 +741,14 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport import_from_stmt.range(), ))); } - checker.report_diagnostic(diagnostic); } for operation in fixer.with_renames() { - let diagnostic = Diagnostic::new( + checker.report_diagnostic( DeprecatedImport { deprecation: Deprecation::WithRename(operation), }, import_from_stmt.range(), ); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs index f0edbc51a4a65..0ef84304fc41a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -5,8 +5,7 @@ use libcst_native::{ }; use log::debug; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{self as ast, Stmt}; @@ -14,10 +13,11 @@ use ruff_python_codegen::Stylist; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; use crate::cst::matchers::{match_import, match_import_from, match_statement}; use crate::fix::codemods::CodegenStylist; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub(crate) enum MockReference { @@ -258,7 +258,7 @@ pub(crate) fn deprecated_mock_attribute(checker: &Checker, attribute: &ast::Expr if UnqualifiedName::from_expr(&attribute.value) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["mock", "mock"])) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DeprecatedMockImport { reference_type: MockReference::Attribute, }, @@ -268,14 +268,17 @@ pub(crate) fn deprecated_mock_attribute(checker: &Checker, attribute: &ast::Expr "mock".to_string(), attribute.value.range(), ))); - checker.report_diagnostic(diagnostic); } } /// UP026 pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { match stmt { - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { // Find all `mock` imports. if names .iter() @@ -297,7 +300,7 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { // Add a `Diagnostic` for each `mock` import. for name in names { if &name.name == "mock" || &name.name == "mock.mock" { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DeprecatedMockImport { reference_type: MockReference::Import, }, @@ -309,7 +312,6 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { stmt.range(), ))); } - checker.report_diagnostic(diagnostic); } } } @@ -324,7 +326,7 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { } if module == "mock" { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DeprecatedMockImport { reference_type: MockReference::Import, }, @@ -337,7 +339,6 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { .map(Fix::safe_edit) }); } - checker.report_diagnostic(diagnostic); } } _ => (), diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs index 77087ce14bf53..519f8d9d397b9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs @@ -2,11 +2,11 @@ use ruff_python_ast::{self as ast, Expr}; use rustc_hash::FxHashMap; use std::sync::LazyLock; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of deprecated methods from the `unittest` module. @@ -91,7 +91,7 @@ pub(crate) fn deprecated_unittest_alias(checker: &Checker, expr: &Expr) { if id != "self" { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DeprecatedUnittestAlias { alias: attr.to_string(), target: (*target).to_string(), @@ -102,5 +102,4 @@ pub(crate) fn deprecated_unittest_alias(checker: &Checker, expr: &Expr) { format!("self.{target}"), expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 630fccae242e1..c6ba676d8232c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -1,11 +1,12 @@ use std::slice::Iter; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_parser::{Token, TokenKind, Tokens}; use ruff_text_size::{Ranged, TextRange}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for extraneous parentheses. @@ -114,11 +115,7 @@ fn match_extraneous_parentheses(tokens: &mut Iter<'_, Token>) -> Option<(TextRan } /// UP034 -pub(crate) fn extraneous_parentheses( - diagnostics: &mut Vec, - tokens: &Tokens, - locator: &Locator, -) { +pub(crate) fn extraneous_parentheses(context: &LintContext, tokens: &Tokens, locator: &Locator) { let mut token_iter = tokens.iter(); while let Some(token) = token_iter.next() { if !matches!(token.kind(), TokenKind::Lpar) { @@ -129,16 +126,16 @@ pub(crate) fn extraneous_parentheses( continue; }; - let mut diagnostic = Diagnostic::new( + if let Some(mut diagnostic) = context.report_diagnostic_if_enabled( ExtraneousParentheses, TextRange::new(start_range.start(), end_range.end()), - ); - let contents = locator.slice(TextRange::new(start_range.start(), end_range.end())); - diagnostic.set_fix(Fix::safe_edit(Edit::replacement( - contents[1..contents.len() - 1].to_string(), - start_range.start(), - end_range.end(), - ))); - diagnostics.push(diagnostic); + ) { + let contents = locator.slice(TextRange::new(start_range.start(), end_range.end())); + diagnostic.set_fix(Fix::safe_edit(Edit::replacement( + contents[1..contents.len() - 1].to_string(), + start_range.start(), + end_range.end(), + ))); + } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs index 001c0407ee473..f34751ce9308f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -3,8 +3,7 @@ use std::borrow::Cow; use anyhow::{Context, Result}; use rustc_hash::{FxHashMap, FxHashSet}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::str::{leading_quote, trailing_quote}; use ruff_python_ast::{self as ast, Expr, Keyword}; @@ -15,10 +14,11 @@ use ruff_python_parser::TokenKind; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::pyflakes::format::FormatSummary; use crate::rules::pyupgrade::helpers::{curly_escape, curly_unescape}; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `str.format` calls that can be replaced with f-strings. @@ -85,6 +85,7 @@ impl<'a> FormatSummaryValues<'a> { arg, value, range: _, + node_index: _, } = keyword; let key = arg.as_ref()?; if contains_quotes(locator.slice(value)) || locator.contains_line_break(value.range()) { @@ -504,7 +505,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma return; } - let mut diagnostic = Diagnostic::new(FString, call.range()); + let mut diagnostic = checker.report_diagnostic(FString, call.range()); // Avoid fix if there are comments within the call: // ``` @@ -528,5 +529,4 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma ))); } } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs index b14f9ad181ceb..6865dc4e2367c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs @@ -1,22 +1,22 @@ use std::sync::LazyLock; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use libcst_native::{Arg, Expression}; use regex::Regex; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_codegen::Stylist; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; use crate::cst::matchers::{ match_attribute, match_call_mut, match_expression, transform_expression_text, }; use crate::fix::codemods::CodegenStylist; use crate::rules::pyflakes::format::FormatSummary; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary positional indices in format strings. @@ -39,6 +39,10 @@ use crate::Locator; /// "{}, {}".format("Hello", "World") # "Hello, World" /// ``` /// +/// This fix is marked as unsafe because: +/// - Comments attached to arguments are not moved, which can cause comments to mismatch the actual arguments. +/// - If arguments have side effects (e.g., print), reordering may change program behavior. +/// /// ## References /// - [Python documentation: Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax) /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) @@ -108,12 +112,11 @@ pub(crate) fn format_literals(checker: &Checker, call: &ast::ExprCall, summary: Arguments::Reorder(&summary.indices) }; - let mut diagnostic = Diagnostic::new(FormatLiterals, call.range()); + let mut diagnostic = checker.report_diagnostic(FormatLiterals, call.range()); diagnostic.try_set_fix(|| { generate_call(call, arguments, checker.locator(), checker.stylist()) .map(|suggestion| Fix::unsafe_edit(Edit::range_replacement(suggestion, call.range()))) }); - checker.report_diagnostic(diagnostic); } /// Returns true if the indices are sequential. diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index aa4e142a5a094..9b1fd268019b4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, Keyword}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `functools.lru_cache` that set `maxsize=None`. @@ -64,8 +64,10 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &Checker, decorator_list: &[D args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, }) = &decorator.expression else { continue; @@ -85,9 +87,10 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &Checker, decorator_list: &[D arg, value, range: _, + node_index: _, } = &keywords[0]; if arg.as_ref().is_some_and(|arg| arg == "maxsize") && value.is_none_literal_expr() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( LRUCacheWithMaxsizeNone, TextRange::new(func.end(), decorator.end()), ); @@ -101,7 +104,6 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &Checker, decorator_list: &[D Edit::range_replacement(binding, decorator.expression.range()); Ok(Fix::safe_edits(import_edit, [reference_edit])) }); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 80c404813c454..89416772198d6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Decorator, Expr}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for unnecessary parentheses on `functools.lru_cache` decorators. @@ -59,6 +59,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &Checker, decorator_list: &[ func, arguments, range: _, + node_index: _, }) = &decorator.expression else { continue; @@ -74,12 +75,11 @@ pub(crate) fn lru_cache_without_parameters(checker: &Checker, decorator_list: &[ matches!(qualified_name.segments(), ["functools", "lru_cache"]) }) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( LRUCacheWithoutParameters, TextRange::new(func.end(), decorator.end()), ); diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(arguments.range()))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs index c8f5f1b04b496..aef1d1d16858e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs @@ -37,6 +37,7 @@ pub(crate) use unpacked_list_comprehension::*; pub(crate) use use_pep585_annotation::*; pub(crate) use use_pep604_annotation::*; pub(crate) use use_pep604_isinstance::*; +pub(crate) use useless_class_metaclass_type::*; pub(crate) use useless_metaclass_type::*; pub(crate) use useless_object_inheritance::*; pub(crate) use yield_in_for_loop::*; @@ -80,6 +81,7 @@ mod unpacked_list_comprehension; mod use_pep585_annotation; mod use_pep604_annotation; mod use_pep604_isinstance; +mod useless_class_metaclass_type; mod useless_metaclass_type; mod useless_object_inheritance; mod yield_in_for_loop; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs index a1d8bf31deb37..4b2bbb1068d2d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -1,12 +1,13 @@ use std::fmt; use std::str::FromStr; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, OperatorPrecedence, UnaryOp}; +use ruff_source_file::find_newline; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum LiteralType { @@ -38,23 +39,27 @@ impl LiteralType { LiteralType::Str => ast::StringLiteral { value: Box::default(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), flags: checker.default_string_flags(), } .into(), LiteralType::Bytes => ast::BytesLiteral { value: Box::default(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), flags: checker.default_bytes_flags(), } .into(), LiteralType::Int => ast::ExprNumberLiteral { value: ast::Number::Int(Int::from(0u8)), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), LiteralType::Float => ast::ExprNumberLiteral { value: ast::Number::Float(0.0), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), LiteralType::Bool => ast::ExprBooleanLiteral::default().into(), @@ -159,14 +164,17 @@ pub(crate) fn native_literals( args, keywords, range: _, + node_index: _, }, - range: _, + range: call_range, + node_index: _, } = call; if !keywords.is_empty() || args.len() > 1 { return; } + let tokens = checker.tokens(); let semantic = checker.semantic(); let Some(builtin) = semantic.resolve_builtin_symbol(func) else { @@ -191,21 +199,21 @@ pub(crate) fn native_literals( match args.first() { None => { - let mut diagnostic = Diagnostic::new(NativeLiterals { literal_type }, call.range()); - // Do not suggest fix for attribute access on an int like `int().attribute` // Ex) `int().denominator` is valid but `0.denominator` is not if literal_type == LiteralType::Int && matches!(parent_expr, Some(Expr::Attribute(_))) { return; } + let mut diagnostic = + checker.report_diagnostic(NativeLiterals { literal_type }, call.range()); + let expr = literal_type.as_zero_value_expr(checker); let content = checker.generator().expr(&expr); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( content, call.range(), ))); - checker.report_diagnostic(diagnostic); } Some(arg) => { let (has_unary_op, literal_expr) = if let Some(literal_expr) = arg.as_literal_expr() { @@ -243,7 +251,23 @@ pub(crate) fn native_literals( let arg_code = checker.locator().slice(arg); - let content = match (parent_expr, literal_type, has_unary_op) { + let mut needs_space = false; + // Look for the `Rpar` token of the call expression and check if there is a keyword token right + // next to it without any space separating them. Without this check, the fix for this + // rule would create a syntax error. + // Ex) `bool(True)and None` no space between `)` and the keyword `and`. + // + // Subtract 1 from the end of the range to include `Rpar` token in the slice. + if let [paren_token, next_token, ..] = tokens.after(call_range.sub_end(1.into()).end()) + { + needs_space = next_token.kind().is_keyword() + && paren_token.range().end() == next_token.range().start(); + } + + let mut content = match (parent_expr, literal_type, has_unary_op) { + // Expressions including newlines must be parenthesised to be valid syntax + (_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"), + // Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float // Ex) `(7).denominator` is valid but `7.denominator` is not // Note that floats do not have this problem @@ -261,6 +285,10 @@ pub(crate) fn native_literals( _ => arg_code.to_string(), }; + if needs_space { + content.push(' '); + } + let applicability = if checker.comment_ranges().intersects(call.range) { Applicability::Unsafe } else { @@ -269,8 +297,9 @@ pub(crate) fn native_literals( let edit = Edit::range_replacement(content, call.range()); let fix = Fix::applicable_edit(edit, applicability); - let diagnostic = Diagnostic::new(NativeLiterals { literal_type }, call.range()); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(NativeLiterals { literal_type }, call.range()) + .set_fix(fix); } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs index 50728df17353b..79ab523ad3da1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/non_pep646_unpack.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprSubscript, PythonVersion}; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `Unpack[]` on Python 3.11 and above, and suggests @@ -88,12 +88,11 @@ pub(crate) fn use_pep646_unpack(checker: &Checker, expr: &ExprSubscript) { return; } - let mut diagnostic = Diagnostic::new(NonPEP646Unpack, *range); + let mut diagnostic = checker.report_diagnostic(NonPEP646Unpack, *range); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( format!("*{}", checker.locator().slice(slice.as_ref())), *range, ))); - checker.report_diagnostic(diagnostic); } /// Determine whether the [`ExprSubscript`] is in a subscript index (e.g., `Generic[Unpack[int]]`). diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs index 1c3a62c8ed18e..4d74273a28e84 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs @@ -1,10 +1,10 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `io.open`. @@ -52,7 +52,7 @@ pub(crate) fn open_alias(checker: &Checker, expr: &Expr, func: &Expr) { .resolve_qualified_name(func) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["io", "open"])) { - let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); + let mut diagnostic = checker.report_diagnostic(OpenAlias, expr.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( "open", @@ -64,6 +64,5 @@ pub(crate) fn open_alias(checker: &Checker, expr: &Expr, func: &Expr) { import_edit, )) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs index a9c65a92a06f6..c08a49462f141 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs @@ -1,13 +1,13 @@ use ruff_python_ast::{self as ast, ExceptHandler, Expr, ExprContext}; use ruff_text_size::{Ranged, TextRange}; -use crate::fix::edits::pad; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::{Name, UnqualifiedName}; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +use crate::fix::edits::pad; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of exceptions that alias `OSError`. @@ -64,14 +64,14 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool { [ "" | "builtins", "EnvironmentError" | "IOError" | "WindowsError" - ] | ["mmap" | "select" | "socket" | "os", "error"] + ] | ["mmap" | "resource" | "select" | "socket" | "os", "error"] ) }) } /// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`]. fn atom_diagnostic(checker: &Checker, target: &Expr) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( OSErrorAlias { name: UnqualifiedName::from_expr(target).map(|name| name.to_string()), }, @@ -88,12 +88,11 @@ fn atom_diagnostic(checker: &Checker, target: &Expr) { import_edit, )) }); - checker.report_diagnostic(diagnostic); } /// Create a [`Diagnostic`] for a tuple of expressions. fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) { - let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, tuple.range()); + let mut diagnostic = checker.report_diagnostic(OSErrorAlias { name: None }, tuple.range()); let semantic = checker.semantic(); if semantic.has_builtin_binding("OSError") { // Filter out any `OSErrors` aliases. @@ -117,6 +116,7 @@ fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr] id: Name::new_static("OSError"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; remaining.insert(0, node.into()); } @@ -128,6 +128,7 @@ fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr] elts: remaining, ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }; format!("({})", checker.generator().expr(&node.into())) @@ -138,7 +139,6 @@ fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr] tuple.range(), ))); } - checker.report_diagnostic(diagnostic); } /// UP024 diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index 969220eb4481e..52e4cd9771ba0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -2,10 +2,9 @@ use std::cmp::Ordering; use anyhow::Result; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_subscript; -use ruff_python_ast::stmt_if::{if_elif_branches, BranchKind, IfElifBranch}; +use ruff_python_ast::stmt_if::{BranchKind, IfElifBranch, if_elif_branches}; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{self as ast, CmpOp, ElifElseClause, Expr, Int, StmtIf}; use ruff_source_file::LineRanges; @@ -13,7 +12,9 @@ use ruff_text_size::{Ranged, TextLen, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::{adjust_indentation, delete_stmt}; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; +use ruff_python_semantic::SemanticModel; /// ## What it does /// Checks for conditional blocks gated on `sys.version_info` comparisons @@ -44,6 +45,10 @@ use ruff_python_ast::PythonVersion; /// ## Options /// - `target-version` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe because it will remove all code, +/// comments, and annotations within unreachable version blocks. +/// /// ## References /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] @@ -89,6 +94,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { ops, comparators, range: _, + node_index: _, }) = &branch.test else { continue; @@ -98,14 +104,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { continue; }; - // Detect `sys.version_info`, along with slices (like `sys.version_info[:2]`). - if !checker - .semantic() - .resolve_qualified_name(map_subscript(left)) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["sys", "version_info"]) - }) - { + if !is_valid_version_info(checker.semantic(), left) { continue; } @@ -125,7 +124,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { ) { Ok(false) => {} Ok(true) => { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( OutdatedVersionBlock { reason: if op.is_lt() || op.is_lt_e() { Reason::AlwaysFalse @@ -142,15 +141,14 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { } { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } Err(_) => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( OutdatedVersionBlock { reason: Reason::Invalid, }, comparison.range(), - )); + ); } } } @@ -178,28 +176,30 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { }; match reason { Reason::AlwaysTrue => { - let mut diagnostic = - Diagnostic::new(OutdatedVersionBlock { reason }, branch.test.range()); + let mut diagnostic = checker.report_diagnostic( + OutdatedVersionBlock { reason }, + branch.test.range(), + ); if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch) { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } Reason::AlwaysFalse => { - let mut diagnostic = - Diagnostic::new(OutdatedVersionBlock { reason }, branch.test.range()); + let mut diagnostic = checker.report_diagnostic( + OutdatedVersionBlock { reason }, + branch.test.range(), + ); if let Some(fix) = fix_always_false_branch(checker, stmt_if, &branch) { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } Reason::Invalid => { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( OutdatedVersionBlock { reason: Reason::Invalid, }, comparison.range(), - )); + ); } } } @@ -450,6 +450,21 @@ fn extract_version(elts: &[Expr]) -> Option> { Some(version) } +/// Returns `true` if the expression is related to `sys.version_info`. +/// +/// This includes: +/// - Direct access: `sys.version_info` +/// - Subscript access: `sys.version_info[:2]`, `sys.version_info[0]` +/// - Major version attribute: `sys.version_info.major` +fn is_valid_version_info(semantic: &SemanticModel, left: &Expr) -> bool { + semantic + .resolve_qualified_name(map_subscript(left)) + .is_some_and(|name| matches!(name.segments(), ["sys", "version_info"])) + || semantic + .resolve_qualified_name(left) + .is_some_and(|name| matches!(name.segments(), ["sys", "version_info", "major"])) +} + #[cfg(test)] mod tests { use test_case::test_case; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index ec36bfe599711..e364056de37d6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -6,11 +6,10 @@ use std::fmt::Display; use itertools::Itertools; use ruff_python_ast::{ - self as ast, + self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, + TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, name::Name, visitor::{self, Visitor}, - Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam, - TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, }; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; @@ -141,12 +140,14 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { TypeParamKind::TypeVar => { TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), name: Identifier::new(*name, TextRange::default()), bound: match restriction { Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())), Some(TypeVarRestriction::Constraint(constraints)) => { Some(Box::new(Expr::Tuple(ast::ExprTuple { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), elts: constraints.iter().map(|expr| (*expr).clone()).collect(), ctx: ast::ExprContext::Load, parenthesized: true, @@ -155,14 +156,17 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { Some(TypeVarRestriction::AnyStr) => { Some(Box::new(Expr::Tuple(ast::ExprTuple { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), elts: vec![ Expr::Name(ExprName { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), id: Name::from("str"), ctx: ast::ExprContext::Load, }), Expr::Name(ExprName { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), id: Name::from("bytes"), ctx: ast::ExprContext::Load, }), @@ -180,11 +184,13 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam { } TypeParamKind::TypeVarTuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), name: Identifier::new(*name, TextRange::default()), default: None, }), TypeParamKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), name: Identifier::new(*name, TextRange::default()), default: None, }), @@ -282,7 +288,7 @@ pub(crate) fn expr_name_to_type_var<'a>( match value.as_ref() { Expr::Subscript(ExprSubscript { - value: ref subscript_value, + value: subscript_value, .. }) => { if semantic.match_typing_expr(subscript_value, "TypeVar") { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs index 24f365a2b66d3..bafcb37c970ac 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs @@ -1,15 +1,15 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{ExprSubscript, StmtClassDef}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; use super::{ - check_type_vars, find_generic, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor, + DisplayTypeVars, TypeVarReferenceVisitor, check_type_vars, find_generic, in_nested_context, }; /// ## What it does @@ -32,7 +32,7 @@ use super::{ /// fix. /// /// Not all type checkers fully support PEP 695 yet, so even valid fixes suggested by this rule may -/// cause type checking to fail. +/// cause type checking to [fail]. /// /// ## Fix safety /// @@ -84,6 +84,7 @@ use super::{ /// [PYI059]: https://docs.astral.sh/ruff/rules/generic-not-last-base-class/ /// [UP047]: https://docs.astral.sh/ruff/rules/non-pep695-generic-function/ /// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/ +/// [fail]: https://github.com/python/mypy/issues/18507 #[derive(ViolationMetadata)] pub(crate) struct NonPEP695GenericClass { name: String, @@ -138,7 +139,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NonPEP695GenericClass { name: name.to_string(), }, @@ -154,7 +155,6 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD // because `find_generic` also finds the *first* Generic argument, this has the additional // benefit of bailing out with a diagnostic if multiple Generic arguments are present if generic_idx != arguments.len() - 1 { - checker.report_diagnostic(diagnostic); return; } @@ -187,6 +187,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD // just because we can't confirm that `SomethingElse` is a `TypeVar` if !visitor.any_skipped { let Some(type_vars) = check_type_vars(visitor.vars) else { + diagnostic.defuse(); return; }; @@ -202,6 +203,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD arguments, Parentheses::Remove, checker.source(), + checker.comment_ranges(), )?; Ok(Fix::unsafe_edits( Edit::insertion(type_params.to_string(), name.end()), @@ -209,6 +211,4 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD )) }); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs index b7d0c99de8dbb..645fbcad61d98 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_function.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::visitor::Visitor; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::StmtFunctionDef; +use ruff_python_ast::visitor::Visitor; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; -use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor}; +use super::{DisplayTypeVars, TypeVarReferenceVisitor, check_type_vars, in_nested_context}; /// ## What it does /// @@ -25,7 +25,7 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// in Python 3.13. /// /// Not all type checkers fully support PEP 695 yet, so even valid fixes suggested by this rule may -/// cause type checking to fail. +/// cause type checking to [fail]. /// /// ## Fix safety /// @@ -76,6 +76,7 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc /// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/ /// [UP046]: https://docs.astral.sh/ruff/rules/non-pep695-generic-class/ /// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/ +/// [fail]: https://github.com/python/mypy/issues/18507 #[derive(ViolationMetadata)] pub(crate) struct NonPEP695GenericFunction { name: String, @@ -163,16 +164,15 @@ pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &Stmt source: checker.source(), }; - checker.report_diagnostic( - Diagnostic::new( + checker + .report_diagnostic( NonPEP695GenericFunction { name: name.to_string(), }, TextRange::new(name.start(), parameters.end()), ) - .with_fix(Fix::unsafe_edit(Edit::insertion( + .set_fix(Fix::unsafe_edit(Edit::insertion( type_params.to_string(), name.end(), - ))), - ); + ))); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs index 5895b3e8e9d16..5fb8937abf6ca 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_type_alias.rs @@ -1,7 +1,6 @@ use itertools::Itertools; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::visitor::Visitor; @@ -9,10 +8,11 @@ use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssi use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; use super::{ - expr_name_to_type_var, DisplayTypeVars, TypeParamKind, TypeVar, TypeVarReferenceVisitor, + DisplayTypeVars, TypeParamKind, TypeVar, TypeVarReferenceVisitor, expr_name_to_type_var, }; /// ## What it does @@ -138,11 +138,13 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) { let type_params = match arguments.keywords.as_ref() { [] => &[], - [Keyword { - arg: Some(name), - value: Expr::Tuple(type_params), - .. - }] if name.as_str() == "type_params" => type_params.elts.as_slice(), + [ + Keyword { + arg: Some(name), + value: Expr::Tuple(type_params), + .. + }, + ] if name.as_str() == "type_params" => type_params.elts.as_slice(), _ => return, }; @@ -170,14 +172,14 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) { return; }; - checker.report_diagnostic(create_diagnostic( + create_diagnostic( checker, stmt.into(), &target_name.id, value, &vars, TypeAliasKind::TypeAliasType, - )); + ); } /// UP040 @@ -229,14 +231,14 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) { return; } - checker.report_diagnostic(create_diagnostic( + create_diagnostic( checker, stmt.into(), name, value, &vars, TypeAliasKind::TypeAlias, - )); + ); } /// Generate a [`Diagnostic`] for a non-PEP 695 type alias or type alias type. @@ -247,7 +249,7 @@ fn create_diagnostic( value: &Expr, type_vars: &[TypeVar], type_alias_kind: TypeAliasKind, -) -> Diagnostic { +) { let source = checker.source(); let comment_ranges = checker.comment_ranges(); @@ -285,12 +287,13 @@ fn create_diagnostic( } }; - Diagnostic::new( - NonPEP695TypeAlias { - name: name.to_string(), - type_alias_kind, - }, - stmt.range(), - ) - .with_fix(Fix::applicable_edit(edit, applicability)) + checker + .report_diagnostic( + NonPEP695TypeAlias { + name: name.to_string(), + type_alias_kind, + }, + stmt.range(), + ) + .set_fix(Fix::applicable_edit(edit, applicability)); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs index ae2ea2ff88690..d581cc64dcad3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/private_type_parameter.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; use ruff_python_semantic::Binding; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::Ranged; +use crate::{Applicability, Fix, FixAvailability, Violation}; use crate::{ checkers::ast::Checker, renamer::{Renamer, ShadowedKind}, @@ -101,43 +101,45 @@ impl Violation for PrivateTypeParameter { } /// UP049 -pub(crate) fn private_type_parameter(checker: &Checker, binding: &Binding) -> Option { +pub(crate) fn private_type_parameter(checker: &Checker, binding: &Binding) { let semantic = checker.semantic(); - let stmt = binding.statement(semantic)?; + let Some(stmt) = binding.statement(semantic) else { + return; + }; if !binding.kind.is_type_param() { - return None; + return; } let kind = match stmt { Stmt::FunctionDef(_) => ParamKind::Function, Stmt::ClassDef(_) => ParamKind::Class, - _ => return None, + _ => return, }; let old_name = binding.name(checker.source()); if !old_name.starts_with('_') { - return None; + return; } // Sunder `_T_`, dunder `__T__`, and all all-under `_` or `__` cases should all be skipped, as // these are not "private" names if old_name.ends_with('_') { - return None; + return; } - let mut diagnostic = Diagnostic::new(PrivateTypeParameter { kind }, binding.range); + let mut diagnostic = checker.report_diagnostic(PrivateTypeParameter { kind }, binding.range); let new_name = old_name.trim_start_matches('_'); // if the new name would shadow another variable, keyword, or builtin, emit a diagnostic without // a suggested fix if ShadowedKind::new(binding, new_name, checker).shadows_any() { - return Some(diagnostic); + return; } if !is_identifier(new_name) { - return Some(diagnostic); + return; } let source = checker.source(); @@ -163,6 +165,4 @@ pub(crate) fn private_type_parameter(checker: &Checker, binding: &Binding) -> Op let fix_isolation = Checker::isolation(binding.source); Ok(Fix::applicable_edits(first, rest, applicability).isolate(fix_isolation)) }); - - Some(diagnostic) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs index 986ce240c6205..1faffd5a71616 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -2,9 +2,8 @@ use std::borrow::Cow; use std::fmt::Write; use std::str::FromStr; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, whitespace::indentation, AnyStringFlags, Expr, StringFlags}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, AnyStringFlags, Expr, StringFlags, whitespace::indentation}; use ruff_python_codegen::Stylist; use ruff_python_literal::cformat::{ CConversionFlags, CFormatPart, CFormatPrecision, CFormatQuantity, CFormatString, @@ -14,9 +13,10 @@ use ruff_python_stdlib::identifiers::is_identifier; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::pyupgrade::helpers::curly_escape; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `printf`-style string formatting, and offers to replace it with @@ -231,7 +231,12 @@ fn clean_params_tuple<'a>(right: &Expr, locator: &Locator<'a>) -> Cow<'a, str> { fn clean_params_dictionary(right: &Expr, locator: &Locator, stylist: &Stylist) -> Option { let is_multi_line = locator.contains_line_break(right.range()); let mut contents = String::new(); - if let Expr::Dict(ast::ExprDict { items, range: _ }) = &right { + if let Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) = &right + { let mut arguments: Vec = vec![]; let mut seen: Vec<&str> = vec![]; let mut indent = None; @@ -303,7 +308,7 @@ fn clean_params_dictionary(right: &Expr, locator: &Locator, stylist: &Stylist) - /// [`Expr`] can be converted. fn convertible(format_string: &CFormatString, params: &Expr) -> bool { for (.., format_part) in format_string.iter() { - let CFormatPart::Spec(ref fmt) = format_part else { + let CFormatPart::Spec(fmt) = format_part else { continue; }; @@ -385,13 +390,13 @@ pub(crate) fn printf_string_formatting( return; }; if !convertible(&format_string, right) { - checker.report_diagnostic(Diagnostic::new(PrintfStringFormatting, string_expr.range())); + checker.report_diagnostic(PrintfStringFormatting, string_expr.range()); return; } // Count the number of positional and keyword arguments. for (.., format_part) in format_string.iter() { - let CFormatPart::Spec(ref fmt) = format_part else { + let CFormatPart::Spec(fmt) = format_part else { continue; }; if fmt.mapping_key.is_none() { @@ -446,10 +451,7 @@ pub(crate) fn printf_string_formatting( let Some(params_string) = clean_params_dictionary(right, checker.locator(), checker.stylist()) else { - checker.report_diagnostic(Diagnostic::new( - PrintfStringFormatting, - string_expr.range(), - )); + checker.report_diagnostic(PrintfStringFormatting, string_expr.range()); return; }; Cow::Owned(params_string) @@ -504,12 +506,11 @@ pub(crate) fn printf_string_formatting( // Add the `.format` call. let _ = write!(&mut contents, ".format{params_string}"); - let mut diagnostic = Diagnostic::new(PrintfStringFormatting, bin_op.range()); + let mut diagnostic = checker.report_diagnostic(PrintfStringFormatting, bin_op.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( contents, bin_op.range(), ))); - checker.report_diagnostic(diagnostic); } #[cfg(test)] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs index 9afaa5104f8df..f890b83372448 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -1,12 +1,12 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::Stmt; use ruff_python_parser::TokenKind; +use ruff_python_semantic::SemanticModel; +use ruff_source_file::LineRanges; use ruff_text_size::{TextLen, TextRange, TextSize}; use crate::checkers::ast::Checker; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::Stmt; -use ruff_python_semantic::SemanticModel; -use ruff_source_file::LineRanges; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for the presence of unnecessary quotes in type annotations. @@ -85,8 +85,6 @@ impl AlwaysFixableViolation for QuotedAnnotation { /// UP037 pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: TextRange) { - let diagnostic = Diagnostic::new(QuotedAnnotation, range); - let placeholder_range = TextRange::up_to(annotation.text_len()); let spans_multiple_lines = annotation.contains_line_break(placeholder_range); @@ -108,7 +106,9 @@ pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: Text let edit = Edit::range_replacement(new_content, range); let fix = Fix::safe_edit(edit); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(QuotedAnnotation, range) + .set_fix(fix); } fn in_parameter_annotation(offset: TextSize, semantic: &SemanticModel) -> bool { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs index 43403c01630cb..13deff49417bc 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -1,12 +1,12 @@ use anyhow::Result; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_parser::{TokenKind, Tokens}; use ruff_python_stdlib::open_mode::OpenMode; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for redundant `open` mode arguments. @@ -81,17 +81,12 @@ pub(crate) fn redundant_open_modes(checker: &Checker, call: &ast::ExprCall) { }; let reduced = mode.reduce(); if reduced != mode { - checker.report_diagnostic(create_diagnostic(call, mode_arg, reduced, checker)); + create_diagnostic(call, mode_arg, reduced, checker); } } -fn create_diagnostic( - call: &ast::ExprCall, - mode_arg: &Expr, - mode: OpenMode, - checker: &Checker, -) -> Diagnostic { - let mut diagnostic = Diagnostic::new( +fn create_diagnostic(call: &ast::ExprCall, mode_arg: &Expr, mode: OpenMode, checker: &Checker) { + let mut diagnostic = checker.report_diagnostic( RedundantOpenModes { replacement: mode.to_string(), }, @@ -109,8 +104,6 @@ fn create_diagnostic( mode_arg.range(), ))); } - - diagnostic } fn create_remove_argument_fix( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 9a752241d0000..17de6c99fa3b3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -1,13 +1,14 @@ use anyhow::Result; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Keyword}; use ruff_python_semantic::Modules; +use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `subprocess.run` that send `stdout` and `stderr` to a @@ -33,6 +34,12 @@ use crate::fix::edits::{remove_argument, Parentheses}; /// subprocess.run(["foo"], capture_output=True) /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because replacing `stdout=subprocess.PIPE` and +/// `stderr=subprocess.PIPE` with `capture_output=True` may delete comments attached +/// to the original arguments. +/// /// ## References /// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) /// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) @@ -88,12 +95,18 @@ pub(crate) fn replace_stdout_stderr(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(ReplaceStdoutStderr, call.range()); + let mut diagnostic = checker.report_diagnostic(ReplaceStdoutStderr, call.range()); if call.arguments.find_keyword("capture_output").is_none() { - diagnostic - .try_set_fix(|| generate_fix(stdout, stderr, call, checker.locator().contents())); + diagnostic.try_set_fix(|| { + generate_fix( + stdout, + stderr, + call, + checker.locator().contents(), + checker.comment_ranges(), + ) + }); } - checker.report_diagnostic(diagnostic); } } @@ -103,6 +116,7 @@ fn generate_fix( stderr: &Keyword, call: &ast::ExprCall, source: &str, + comment_ranges: &CommentRanges, ) -> Result { let (first, second) = if stdout.start() < stderr.start() { (stdout, stderr) @@ -117,6 +131,7 @@ fn generate_fix( &call.arguments, Parentheses::Preserve, source, + comment_ranges, )?], )) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs index 05216a7107582..7bdcdad798977 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::identifier::Identifier; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for classes that inherit from both `str` and `enum.Enum`. @@ -76,7 +76,6 @@ use crate::importer::ImportRequest; /// - [enum.StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum) /// /// [breaking change]: https://blog.pecar.me/python-enum - #[derive(ViolationMetadata)] pub(crate) struct ReplaceStrEnum { name: String, @@ -126,7 +125,7 @@ pub(crate) fn replace_str_enum(checker: &Checker, class_def: &ast::StmtClassDef) return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ReplaceStrEnum { name: class_def.name.to_string(), }, @@ -153,6 +152,4 @@ pub(crate) fn replace_str_enum(checker: &Checker, class_def: &ast::StmtClassDef) )) }); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 3f31a36d9ae80..c9a2a8b530dd9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `subprocess.run` that set the `universal_newlines` @@ -67,7 +67,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall return; }; - let mut diagnostic = Diagnostic::new(ReplaceUniversalNewlines, arg.range()); + let mut diagnostic = checker.report_diagnostic(ReplaceUniversalNewlines, arg.range()); if call.arguments.find_keyword("text").is_some() { diagnostic.try_set_fix(|| { @@ -76,6 +76,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); @@ -85,6 +86,5 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall arg.range(), ))); } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs index ba11eb64c5505..ddb5ee034ebbe 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -1,9 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; +use crate::preview::is_safe_super_call_with_parameters_fix_enabled; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `super` calls that pass redundant arguments. @@ -40,20 +42,31 @@ use crate::checkers::ast::Checker; /// super().foo() /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing the arguments from a call +/// may delete comments that are attached to the arguments. +/// +/// In [preview], the fix is marked safe if no comments are present. +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ +/// /// ## References /// - [Python documentation: `super`](https://docs.python.org/3/library/functions.html#super) /// - [super/MRO, Python's most misunderstood feature.](https://www.youtube.com/watch?v=X1PQ7zzltz4) #[derive(ViolationMetadata)] pub(crate) struct SuperCallWithParameters; -impl AlwaysFixableViolation for SuperCallWithParameters { +impl Violation for SuperCallWithParameters { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Use `super()` instead of `super(__class__, self)`".to_string() } - fn fix_title(&self) -> String { - "Remove `__super__` parameters".to_string() + fn fix_title(&self) -> Option { + Some("Remove `super()` parameters".to_string()) } } @@ -61,7 +74,7 @@ impl AlwaysFixableViolation for SuperCallWithParameters { pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall) { // Only bother going through the super check at all if we're in a `super` call. // (We check this in `super_args` too, so this is just an optimization.) - if !is_super_call_with_arguments(call) { + if !is_super_call_with_arguments(call, checker) { return; } let scope = checker.semantic().current_scope(); @@ -116,7 +129,9 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall return; }; - if !(first_arg_id == parent_name.as_str() && second_arg_id == parent_arg.name().as_str()) { + if !((first_arg_id == "__class__" || first_arg_id == parent_name.as_str()) + && second_arg_id == parent_arg.name().as_str()) + { return; } @@ -152,19 +167,29 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall return; } - let mut diagnostic = Diagnostic::new(SuperCallWithParameters, call.arguments.range()); - diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( - call.arguments.start() + TextSize::new(1), - call.arguments.end() - TextSize::new(1), - ))); - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(SuperCallWithParameters, call.arguments.range()); + + // Only provide a fix if there are no keyword arguments, since super() doesn't accept keyword arguments + if call.arguments.keywords.is_empty() { + let applicability = if !checker.comment_ranges().intersects(call.arguments.range()) + && is_safe_super_call_with_parameters_fix_enabled(checker.settings()) + { + Applicability::Safe + } else { + Applicability::Unsafe + }; + + diagnostic.set_fix(Fix::applicable_edit( + Edit::deletion( + call.arguments.start() + TextSize::new(1), + call.arguments.end() - TextSize::new(1), + ), + applicability, + )); + } } /// Returns `true` if a call is an argumented `super` invocation. -fn is_super_call_with_arguments(call: &ast::ExprCall) -> bool { - if let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() { - id == "super" && !call.arguments.is_empty() - } else { - false - } +fn is_super_call_with_arguments(call: &ast::ExprCall, checker: &Checker) -> bool { + checker.semantic().match_builtin_expr(&call.func, "super") && !call.arguments.is_empty() } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs index b1e030f77ec87..63dc4a363fff1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs @@ -1,14 +1,14 @@ use ruff_python_ast::{self as ast, ExceptHandler, Expr, ExprContext}; use ruff_text_size::{Ranged, TextRange}; -use crate::fix::edits::pad; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::PythonVersion; use ruff_python_ast::name::{Name, UnqualifiedName}; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; -use ruff_python_ast::PythonVersion; +use crate::fix::edits::pad; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of exceptions that alias `TimeoutError`. @@ -83,7 +83,7 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel, target_version: PythonVersion /// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`]. fn atom_diagnostic(checker: &Checker, target: &Expr) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( TimeoutErrorAlias { name: UnqualifiedName::from_expr(target).map(|name| name.to_string()), }, @@ -100,12 +100,11 @@ fn atom_diagnostic(checker: &Checker, target: &Expr) { import_edit, )) }); - checker.report_diagnostic(diagnostic); } /// Create a [`Diagnostic`] for a tuple of expressions. fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) { - let mut diagnostic = Diagnostic::new(TimeoutErrorAlias { name: None }, tuple.range()); + let mut diagnostic = checker.report_diagnostic(TimeoutErrorAlias { name: None }, tuple.range()); let semantic = checker.semantic(); if semantic.has_builtin_binding("TimeoutError") { // Filter out any `TimeoutErrors` aliases. @@ -129,6 +128,7 @@ fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr] id: Name::new_static("TimeoutError"), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; remaining.insert(0, node.into()); } @@ -140,6 +140,7 @@ fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr] elts: remaining, ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }; format!("({})", checker.generator().expr(&node.into())) @@ -150,7 +151,6 @@ fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr] tuple.range(), ))); } - checker.report_diagnostic(diagnostic); } /// UP041 diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs index 3308350af4506..251cd37dbc62c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -1,13 +1,12 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; - -use crate::fix::edits::pad; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::fix::edits::pad; +use crate::{Edit, Fix, FixAvailability, Violation}; -use super::super::types::Primitive; +use crate::rules::pyupgrade::types::Primitive; /// ## What it does /// Checks for uses of `type` that take a primitive as an argument. @@ -65,7 +64,7 @@ pub(crate) fn type_of_primitive(checker: &Checker, expr: &Expr, func: &Expr, arg if !semantic.match_builtin_expr(func, "type") { return; } - let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range()); + let mut diagnostic = checker.report_diagnostic(TypeOfPrimitive { primitive }, expr.range()); let builtin = primitive.builtin(); if semantic.has_builtin_binding(&builtin) { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( @@ -73,5 +72,4 @@ pub(crate) fn type_of_primitive(checker: &Checker, expr: &Expr, func: &Expr, arg expr.range(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 4a4165b878b4e..9e8adda647a30 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -1,11 +1,11 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `typing.Text`. @@ -56,7 +56,7 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { .resolve_qualified_name(expr) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"])) { - let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range()); + let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( "str", @@ -68,6 +68,5 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { import_edit, )) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs index 1447896ab50cc..7d62237e1f0d0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unicode_kind_prefix.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::StringLiteral; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of the Unicode kind prefix (`u`) in strings. @@ -41,11 +41,29 @@ impl AlwaysFixableViolation for UnicodeKindPrefix { /// UP025 pub(crate) fn unicode_kind_prefix(checker: &Checker, string: &StringLiteral) { if string.flags.prefix().is_unicode() { - let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, string.range); - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::at( - string.start(), - TextSize::from(1), - )))); - checker.report_diagnostic(diagnostic); + let mut diagnostic = checker.report_diagnostic(UnicodeKindPrefix, string.range); + + let prefix_range = TextRange::at(string.start(), TextSize::new(1)); + let locator = checker.locator(); + let content = locator + .slice(TextRange::new(prefix_range.end(), string.end())) + .to_owned(); + + // If the preceding character is equivalent to the quote character, insert a space to avoid a + // syntax error. For example, when removing the `u` prefix in `""u""`, rewrite to `"" ""` + // instead of `""""`. + // see https://github.com/astral-sh/ruff/issues/18895 + let edit = if locator + .slice(TextRange::up_to(prefix_range.start())) + .chars() + .last() + .is_some_and(|char| content.starts_with(char)) + { + Edit::range_replacement(" ".to_string(), prefix_range) + } else { + Edit::range_deletion(prefix_range) + }; + + diagnostic.set_fix(Fix::safe_edit(edit)); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index f89671027777a..bef5438882f42 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -1,12 +1,12 @@ use itertools::Itertools; use ruff_python_ast::{Alias, Stmt}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for unnecessary imports of builtins. @@ -110,7 +110,7 @@ pub(crate) fn unnecessary_builtin_import( return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryBuiltinImport { names: unused_imports .iter() @@ -138,5 +138,4 @@ pub(crate) fn unnecessary_builtin_import( checker.semantic().current_statement_parent_id(), ))) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs index 832272d5f5334..10649d493ea4e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for class definitions that include unnecessary parentheses after @@ -48,10 +48,9 @@ pub(crate) fn unnecessary_class_parentheses(checker: &Checker, class_def: &ast:: return; } - let mut diagnostic = Diagnostic::new(UnnecessaryClassParentheses, arguments.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryClassParentheses, arguments.range()); diagnostic.set_fix(Fix::safe_edit(Edit::deletion( arguments.start(), arguments.end(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs index 7972641e290e8..e9d7b90777df6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs @@ -3,13 +3,14 @@ use std::sync::LazyLock; use regex::Regex; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for unnecessary UTF-8 encoding declarations. @@ -66,7 +67,7 @@ struct CodingCommentRange { /// UP009 pub(crate) fn unnecessary_coding_comment( - diagnostics: &mut Vec, + context: &LintContext, locator: &Locator, comment_ranges: &CommentRanges, ) { @@ -106,9 +107,11 @@ pub(crate) fn unnecessary_coding_comment( } let fix = Fix::safe_edit(Edit::range_deletion(range.line)); - let diagnostic = Diagnostic::new(UTF8EncodingDeclaration, range.comment); - - diagnostics.push(diagnostic.with_fix(fix)); + if let Some(mut diagnostic) = + context.report_diagnostic_if_enabled(UTF8EncodingDeclaration, range.comment) + { + diagnostic.set_fix(fix); + } } struct CodingCommentIterator<'a> { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs index b95125b5158bc..ed1cba9f53df7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_default_type_args.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for unnecessary default type arguments for `Generator` and @@ -83,6 +83,7 @@ pub(crate) fn unnecessary_default_type_args(checker: &Checker, expr: &Expr) { elts, ctx: _, range: _, + node_index: _, parenthesized: _, }) = slice.as_ref() else { @@ -102,7 +103,7 @@ pub(crate) fn unnecessary_default_type_args(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryDefaultTypeArgs, expr.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryDefaultTypeArgs, expr.range()); let applicability = if checker .comment_ranges() @@ -126,17 +127,18 @@ pub(crate) fn unnecessary_default_type_args(checker: &Checker, expr: &Expr) { elts: valid_elts, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }) }), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), expr.range(), ), applicability, )); - checker.report_diagnostic(diagnostic); } /// Trim trailing `None` literals from the given elements. diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index 438e5055d8c3f..4ce5e8492f7ea 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -1,14 +1,14 @@ use std::fmt::Write as _; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr, Keyword}; use ruff_python_parser::{TokenKind, Tokens}; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; -use crate::fix::edits::{pad, remove_argument, Parentheses}; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::fix::edits::{Parentheses, pad, remove_argument}; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for unnecessary calls to `encode` as UTF-8. @@ -161,7 +161,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { if let Some(encoding_arg) = match_encoding_arg(&call.arguments) { if literal.to_str().is_ascii() { // Ex) Convert `"foo".encode()` to `b"foo"`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEncodeUTF8 { reason: Reason::BytesLiteral, }, @@ -172,11 +172,10 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { call, checker.tokens(), )); - checker.report_diagnostic(diagnostic); } else if let EncodingArg::Keyword(kwarg) = encoding_arg { // Ex) Convert `"unicode text©".encode(encoding="utf-8")` to // `"unicode text©".encode()`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, @@ -188,13 +187,13 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); - checker.report_diagnostic(diagnostic); } else if let EncodingArg::Positional(arg) = encoding_arg { // Ex) Convert `"unicode text©".encode("utf-8")` to `"unicode text©".encode()`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, @@ -206,10 +205,10 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); - checker.report_diagnostic(diagnostic); } } } @@ -219,7 +218,7 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { if let EncodingArg::Keyword(kwarg) = encoding_arg { // Ex) Convert `f"unicode text©".encode(encoding="utf-8")` to // `f"unicode text©".encode()`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, @@ -231,13 +230,13 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); - checker.report_diagnostic(diagnostic); } else if let EncodingArg::Positional(arg) = encoding_arg { // Ex) Convert `f"unicode text©".encode("utf-8")` to `f"unicode text©".encode()`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEncodeUTF8 { reason: Reason::DefaultArgument, }, @@ -249,10 +248,10 @@ pub(crate) fn unnecessary_encode_utf8(checker: &Checker, call: &ast::ExprCall) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(Fix::safe_edit) }); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs index d04d11580adc8..c713bdf718935 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -1,12 +1,12 @@ use itertools::Itertools; use ruff_python_ast::{Alias, Stmt}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Applicability, Fix}; /// ## What it does /// Checks for unnecessary `__future__` imports. @@ -30,6 +30,9 @@ use crate::fix; /// print("Hello, world!") /// ``` /// +/// ## Fix safety +/// This fix is marked unsafe if applying it would delete a comment. +/// /// ## Options /// - `target-version` /// @@ -98,7 +101,7 @@ pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: & if unused_imports.is_empty() { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryFutureImport { names: unused_imports .iter() @@ -123,9 +126,18 @@ pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: & checker.stylist(), checker.indexer(), )?; - Ok(Fix::safe_edit(edit).isolate(Checker::isolation( - checker.semantic().current_statement_parent_id(), - ))) + + let range = edit.range(); + let applicability = if checker.comment_ranges().intersects(range) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + Ok( + Fix::applicable_edit(edit, applicability).isolate(Checker::isolation( + checker.semantic().current_statement_parent_id(), + )), + ) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs index 5c8d28ec462f2..1958a23120b91 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -1,6 +1,7 @@ -use ruff_diagnostics::Violation; use ruff_macros::ViolationMetadata; +use crate::Violation; + /// ## Removed /// There's no [evidence](https://github.com/astral-sh/ruff/issues/12754) that generators are /// meaningfully faster than list comprehensions when combined with unpacking. diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index c9d9af485fdbd..044c21f258193 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -1,13 +1,13 @@ use ruff_python_ast::Expr; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::UnqualifiedName; use ruff_python_semantic::analyze::typing::ModuleMember; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -80,7 +80,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: let Some(from) = UnqualifiedName::from_expr(expr) else { return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NonPEP585Annotation { from: from.to_string(), to: replacement.to_string(), @@ -132,5 +132,4 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: } } } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 22d27551dfdee..a6a5b7187a953 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -1,9 +1,6 @@ -use ruff_diagnostics::{ - Applicability, Diagnostic, DiagnosticKind, Edit, Fix, FixAvailability, Violation, -}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers::{pep_604_optional, pep_604_union}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PythonVersion; +use ruff_python_ast::helpers::{pep_604_optional, pep_604_union}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::analyze::typing::Pep604Operator; use ruff_text_size::Ranged; @@ -11,7 +8,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::fix::edits::pad; -use crate::settings::types::PreviewMode; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Check for type annotations that can be rewritten based on [PEP 604] syntax. @@ -41,8 +38,7 @@ use crate::settings::types::PreviewMode; /// foo: int | str = 1 /// ``` /// -/// ## Preview -/// In preview mode, this rule only checks for usages of `typing.Union`, +/// Note that this rule only checks for usages of `typing.Union`, /// while `UP045` checks for `typing.Optional`. /// /// ## Fix safety @@ -102,8 +98,8 @@ impl Violation for NonPEP604AnnotationUnion { /// ``` /// /// ## Fix safety -/// This rule's fix is marked as unsafe, as it may lead to runtime errors when -/// alongside libraries that rely on runtime type annotations, like Pydantic, +/// This rule's fix is marked as unsafe, as it may lead to runtime errors +/// using libraries that rely on runtime type annotations, like Pydantic, /// on Python versions prior to Python 3.10. It may also lead to runtime errors /// in unusual and likely incorrect type annotations where the type does not /// support the `|` operator. @@ -136,11 +132,23 @@ pub(crate) fn non_pep604_annotation( slice: &Expr, operator: Pep604Operator, ) { + // `NamedTuple` is not a type; it's a type constructor. Using it in a type annotation doesn't + // make much sense. But since type checkers will currently (incorrectly) _not_ complain about it + // being used in a type annotation, we just ignore `Optional[typing.NamedTuple]` and + // `Union[...]` containing `NamedTuple`. + // + if is_optional_named_tuple(checker, operator, slice) + || is_union_with_named_tuple(checker, operator, slice) + { + return; + } + // Avoid fixing forward references, types not in an annotation, and expressions that would // lead to invalid syntax. let fixable = checker.semantic().in_type_definition() && !checker.semantic().in_complex_string_type_definition() - && is_allowed_value(slice); + && is_allowed_value(slice) + && !is_optional_none(operator, slice); let applicability = if checker.target_version() >= PythonVersion::PY310 { Applicability::Safe @@ -150,22 +158,12 @@ pub(crate) fn non_pep604_annotation( match operator { Pep604Operator::Optional => { - let (rule, diagnostic_kind) = match checker.settings.preview { - PreviewMode::Disabled => ( - Rule::NonPEP604AnnotationUnion, - DiagnosticKind::from(NonPEP604AnnotationUnion), - ), - PreviewMode::Enabled => ( - Rule::NonPEP604AnnotationOptional, - DiagnosticKind::from(NonPEP604AnnotationOptional), - ), - }; + let guard = + checker.report_diagnostic_if_enabled(NonPEP604AnnotationOptional, expr.range()); - if !checker.enabled(rule) { + let Some(mut diagnostic) = guard else { return; - } - - let mut diagnostic = Diagnostic::new(diagnostic_kind, expr.range()); + }; if fixable { match slice { @@ -187,14 +185,13 @@ pub(crate) fn non_pep604_annotation( } } } - checker.report_diagnostic(diagnostic); } Pep604Operator::Union => { - if !checker.enabled(Rule::NonPEP604AnnotationUnion) { + if !checker.is_rule_enabled(Rule::NonPEP604AnnotationUnion) { return; } - let mut diagnostic = Diagnostic::new(NonPEP604AnnotationUnion, expr.range()); + let mut diagnostic = checker.report_diagnostic(NonPEP604AnnotationUnion, expr.range()); if fixable { match slice { Expr::Slice(_) => { @@ -229,7 +226,6 @@ pub(crate) fn non_pep604_annotation( } } } - checker.report_diagnostic(diagnostic); } } } @@ -263,6 +259,7 @@ fn is_allowed_value(expr: &Expr) -> bool { | Expr::Compare(_) | Expr::Call(_) | Expr::FString(_) + | Expr::TString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) @@ -286,3 +283,27 @@ fn is_allowed_value(expr: &Expr) -> bool { | Expr::IpyEscapeCommand(_) => false, } } + +/// Return `true` if this is an `Optional[typing.NamedTuple]` annotation. +fn is_optional_named_tuple(checker: &Checker, operator: Pep604Operator, slice: &Expr) -> bool { + matches!(operator, Pep604Operator::Optional) && is_named_tuple(checker, slice) +} + +/// Return `true` if this is a `Union[...]` annotation containing `typing.NamedTuple`. +fn is_union_with_named_tuple(checker: &Checker, operator: Pep604Operator, slice: &Expr) -> bool { + matches!(operator, Pep604Operator::Union) + && (is_named_tuple(checker, slice) + || slice + .as_tuple_expr() + .is_some_and(|tuple| tuple.elts.iter().any(|elt| is_named_tuple(checker, elt)))) +} + +/// Return `true` if this is a `typing.NamedTuple` annotation. +fn is_named_tuple(checker: &Checker, expr: &Expr) -> bool { + checker.semantic().match_typing_expr(expr, "NamedTuple") +} + +/// Return `true` if this is an `Optional[None]` annotation. +fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool { + matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_)) +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index 4da657f4731b9..2e0f08638b277 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -1,12 +1,12 @@ use std::fmt; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers::pep_604_union; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; +use ruff_python_ast::helpers::pep_604_union; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub(crate) enum CallKind { @@ -109,12 +109,10 @@ pub(crate) fn use_pep604_isinstance(checker: &Checker, expr: &Expr, func: &Expr, let Some(kind) = CallKind::from_name(builtin_function_name) else { return; }; - checker.report_diagnostic( - Diagnostic::new(NonPEP604Isinstance { kind }, expr.range()).with_fix(Fix::unsafe_edit( - Edit::range_replacement( - checker.generator().expr(&pep_604_union(&tuple.elts)), - types.range(), - ), - )), - ); + checker + .report_diagnostic(NonPEP604Isinstance { kind }, expr.range()) + .set_fix(Fix::unsafe_edit(Edit::range_replacement( + checker.generator().expr(&pep_604_union(&tuple.elts)), + types.range(), + ))); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs new file mode 100644 index 0000000000000..09e993b2fbdd0 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs @@ -0,0 +1,87 @@ +use crate::checkers::ast::Checker; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Fix, FixAvailability, Violation}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::StmtClassDef; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for `metaclass=type` in class definitions. +/// +/// ## Why is this bad? +/// Since Python 3, the default metaclass is `type`, so specifying it explicitly is redundant. +/// +/// Even though `__prepare__` is not required, the default metaclass (`type`) implements it, +/// for the convenience of subclasses calling it via `super()`. +/// ## Example +/// +/// ```python +/// class Foo(metaclass=type): ... +/// ``` +/// +/// Use instead: +/// +/// ```python +/// class Foo: ... +/// ``` +/// +/// ## References +/// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) +#[derive(ViolationMetadata)] +pub(crate) struct UselessClassMetaclassType { + name: String, +} + +impl Violation for UselessClassMetaclassType { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let UselessClassMetaclassType { name } = self; + format!("Class `{name}` uses `metaclass=type`, which is redundant") + } + + fn fix_title(&self) -> Option { + Some("Remove `metaclass=type`".to_string()) + } +} + +/// UP050 +pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtClassDef) { + let Some(arguments) = class_def.arguments.as_deref() else { + return; + }; + + for keyword in &arguments.keywords { + if let (Some("metaclass"), expr) = (keyword.arg.as_deref(), &keyword.value) { + if checker.semantic().match_builtin_expr(expr, "type") { + let mut diagnostic = checker.report_diagnostic( + UselessClassMetaclassType { + name: class_def.name.to_string(), + }, + keyword.range(), + ); + + diagnostic.try_set_fix(|| { + let edit = remove_argument( + keyword, + arguments, + Parentheses::Remove, + checker.locator().contents(), + checker.comment_ranges(), + )?; + + let range = edit.range(); + let applicability = if checker.comment_ranges().intersects(range) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + Ok(Fix::applicable_edit(edit, applicability)) + }); + } + } + } +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs index 1ca18d6fa06c6..b221b2e7cd8e0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for the use of `__metaclass__ = type` in class definitions. @@ -62,12 +62,11 @@ pub(crate) fn useless_metaclass_type( return; } - let mut diagnostic = Diagnostic::new(UselessMetaclassType, stmt.range()); + let mut diagnostic = checker.report_diagnostic(UselessMetaclassType, stmt.range()); let stmt = checker.semantic().current_statement(); let parent = checker.semantic().current_statement_parent(); let edit = fix::edits::delete_stmt(stmt, parent, checker.locator(), checker.indexer()); diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation( checker.semantic().current_statement_parent_id(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs index 53b7e0f7dbc41..cb1fe0152e0c8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -1,10 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{AlwaysFixableViolation, Fix}; /// ## What it does /// Checks for classes that inherit from `object`. @@ -25,6 +26,9 @@ use crate::fix::edits::{remove_argument, Parentheses}; /// class Foo: ... /// ``` /// +/// ## Fix safety +/// This fix is unsafe if it would cause comments to be deleted. +/// /// ## References /// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) #[derive(ViolationMetadata)] @@ -55,21 +59,30 @@ pub(crate) fn useless_object_inheritance(checker: &Checker, class_def: &ast::Stm continue; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UselessObjectInheritance { name: class_def.name.to_string(), }, base.range(), ); + diagnostic.try_set_fix(|| { - remove_argument( + let edit = remove_argument( base, arguments, Parentheses::Remove, checker.locator().contents(), - ) - .map(Fix::safe_edit) + checker.comment_ranges(), + )?; + + let range = edit.range(); + let applicability = if checker.comment_ranges().intersects(range) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + Ok(Fix::applicable_edit(edit, applicability)) }); - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs index b289f23c79c9f..8fa6118e4cdcb 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `for` loops that can be replaced with `yield from` expressions. @@ -15,21 +15,23 @@ use crate::checkers::ast::Checker; /// /// ## Example /// ```python -/// for x in foo: -/// yield x +/// def bar(): +/// for x in foo: +/// yield x /// -/// global y -/// for y in foo: -/// yield y +/// global y +/// for y in foo: +/// yield y /// ``` /// /// Use instead: /// ```python -/// yield from foo +/// def bar(): +/// yield from foo /// -/// for _element in foo: -/// y = _element -/// yield y +/// for _element in foo: +/// y = _element +/// yield y /// ``` /// /// ## Fix safety @@ -78,6 +80,7 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) { orelse, is_async: _, range: _, + node_index: _, } = stmt_for; // If there is an else statement, don't rewrite. @@ -91,12 +94,18 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) { }; // If the body is not a yield, don't rewrite. - let Stmt::Expr(ast::StmtExpr { value, range: _ }) = &body else { + let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = &body + else { return; }; let Expr::Yield(ast::ExprYield { value: Some(value), range: _, + node_index: _, }) = value.as_ref() else { return; @@ -126,7 +135,7 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) { return; } - let mut diagnostic = Diagnostic::new(YieldInForLoop, stmt_for.range()); + let mut diagnostic = checker.report_diagnostic(YieldInForLoop, stmt_for.range()); let contents = checker.locator().slice( parenthesized_range( @@ -158,8 +167,6 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) { stmt_for.range(), ))); } - - checker.report_diagnostic(diagnostic); } /// Return `true` if the two expressions are equivalent, and both consistent solely diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP004.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP004.py.snap index 7c4eabfa7bd7d..07d46783e26ad 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP004.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP004.py.snap @@ -51,7 +51,7 @@ UP004.py:16:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 12 12 | ... 13 13 | 14 14 | @@ -75,7 +75,7 @@ UP004.py:24:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 19 19 | ... 20 20 | 21 21 | @@ -99,7 +99,7 @@ UP004.py:31:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 26 26 | ... 27 27 | 28 28 | @@ -122,7 +122,7 @@ UP004.py:37:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 33 33 | ... 34 34 | 35 35 | @@ -146,7 +146,7 @@ UP004.py:45:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 40 40 | ... 41 41 | 42 42 | @@ -171,7 +171,7 @@ UP004.py:53:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 48 48 | ... 49 49 | 50 50 | @@ -196,7 +196,7 @@ UP004.py:61:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 56 56 | ... 57 57 | 58 58 | @@ -221,7 +221,7 @@ UP004.py:69:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 64 64 | ... 65 65 | 66 66 | @@ -320,7 +320,7 @@ UP004.py:98:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 95 95 | 96 96 | 97 97 | class B( @@ -381,7 +381,7 @@ UP004.py:125:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 121 121 | ... 122 122 | 123 123 | @@ -403,7 +403,7 @@ UP004.py:131:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Safe fix +ℹ Unsafe fix 127 127 | ... 128 128 | 129 129 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap index a2b50cedc1115..d61828d4c4a7a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap @@ -314,3 +314,5 @@ UP007.py:91:26: UP007 [*] Use `X | Y` for type annotations 91 |-def myfunc(param: "tuple[Union[int, 'AClass', None], str]"): 91 |+def myfunc(param: "tuple[int | 'AClass' | None, str]"): 92 92 | print(param) +93 93 | +94 94 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap index 45c029d2d08aa..f6070b8ded0cd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs -snapshot_kind: text --- UP008.py:17:23: UP008 [*] Use `super()` instead of `super(__class__, self)` | @@ -10,7 +9,7 @@ UP008.py:17:23: UP008 [*] Use `super()` instead of `super(__class__, self)` 18 | super(Child, self).method # wrong 19 | super( | - = help: Remove `__super__` parameters + = help: Remove `super()` parameters ℹ Unsafe fix 14 14 | Parent.super(1, 2) # ok @@ -31,7 +30,7 @@ UP008.py:18:14: UP008 [*] Use `super()` instead of `super(__class__, self)` 19 | super( 20 | Child, | - = help: Remove `__super__` parameters + = help: Remove `super()` parameters ℹ Unsafe fix 15 15 | @@ -54,7 +53,7 @@ UP008.py:19:14: UP008 [*] Use `super()` instead of `super(__class__, self)` 22 | | ).method() # wrong | |_________^ UP008 | - = help: Remove `__super__` parameters + = help: Remove `super()` parameters ℹ Unsafe fix 16 16 | def wrong(self): @@ -77,7 +76,7 @@ UP008.py:36:14: UP008 [*] Use `super()` instead of `super(__class__, self)` | ^^^^^^^^^^^^^^^ UP008 37 | super().f() | - = help: Remove `__super__` parameters + = help: Remove `super()` parameters ℹ Unsafe fix 33 33 | @@ -96,7 +95,7 @@ UP008.py:50:18: UP008 [*] Use `super()` instead of `super(__class__, self)` | ^^^^^^^^^^^^^^^ UP008 51 | super().f() | - = help: Remove `__super__` parameters + = help: Remove `super()` parameters ℹ Unsafe fix 47 47 | super(MyClass, self).f() # CANNOT use super() @@ -116,7 +115,7 @@ UP008.py:74:14: UP008 [*] Use `super()` instead of `super(__class__, self)` | ^^^^^^^^^^^^^^^^^ UP008 75 | super().f() # OK | - = help: Remove `__super__` parameters + = help: Remove `super()` parameters ℹ Unsafe fix 71 71 | @dataclass @@ -126,4 +125,177 @@ UP008.py:74:14: UP008 [*] Use `super()` instead of `super(__class__, self)` 74 |+ super().f() # Error 75 75 | super().f() # OK 76 76 | -77 77 | +77 77 | + +UP008.py:92:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +90 | class B(A): +91 | def bar(self): +92 | super(__class__, self).foo() + | ^^^^^^^^^^^^^^^^^ UP008 + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +89 89 | +90 90 | class B(A): +91 91 | def bar(self): +92 |- super(__class__, self).foo() + 92 |+ super().foo() +93 93 | +94 94 | +95 95 | # see: https://github.com/astral-sh/ruff/issues/18684 + +UP008.py:107:23: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +105 | class C: +106 | def f(self): +107 | builtins.super(C, self) + | ^^^^^^^^^ UP008 + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +104 104 | +105 105 | class C: +106 106 | def f(self): +107 |- builtins.super(C, self) + 107 |+ builtins.super() +108 108 | +109 109 | +110 110 | # see: https://github.com/astral-sh/ruff/issues/18533 + +UP008.py:113:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +111 | class ClassForCommentEnthusiasts(BaseClass): +112 | def with_comments(self): +113 | super( + | ______________^ +114 | | # super helpful comment +115 | | ClassForCommentEnthusiasts, +116 | | self +117 | | ).f() + | |_________^ UP008 +118 | super( +119 | ClassForCommentEnthusiasts, + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +110 110 | # see: https://github.com/astral-sh/ruff/issues/18533 +111 111 | class ClassForCommentEnthusiasts(BaseClass): +112 112 | def with_comments(self): +113 |- super( +114 |- # super helpful comment +115 |- ClassForCommentEnthusiasts, +116 |- self +117 |- ).f() + 113 |+ super().f() +118 114 | super( +119 115 | ClassForCommentEnthusiasts, +120 116 | # even more helpful comment + +UP008.py:118:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +116 | self +117 | ).f() +118 | super( + | ______________^ +119 | | ClassForCommentEnthusiasts, +120 | | # even more helpful comment +121 | | self +122 | | ).f() + | |_________^ UP008 +123 | super( +124 | ClassForCommentEnthusiasts, + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +115 115 | ClassForCommentEnthusiasts, +116 116 | self +117 117 | ).f() +118 |- super( +119 |- ClassForCommentEnthusiasts, +120 |- # even more helpful comment +121 |- self +122 |- ).f() + 118 |+ super().f() +123 119 | super( +124 120 | ClassForCommentEnthusiasts, +125 121 | self + +UP008.py:123:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +121 | self +122 | ).f() +123 | super( + | ______________^ +124 | | ClassForCommentEnthusiasts, +125 | | self +126 | | # also a comment +127 | | ).f() + | |_________^ UP008 + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +120 120 | # even more helpful comment +121 121 | self +122 122 | ).f() +123 |- super( +124 |- ClassForCommentEnthusiasts, +125 |- self +126 |- # also a comment +127 |- ).f() + 123 |+ super().f() +128 124 | +129 125 | +130 126 | # Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed + +UP008.py:133:21: UP008 Use `super()` instead of `super(__class__, self)` + | +131 | class Ord(int): +132 | def __len__(self): +133 | return super(Ord, self, uhoh=True, **{"error": True}).bit_length() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 +134 | +135 | class ExampleWithKeywords: + | + = help: Remove `super()` parameters + +UP008.py:137:14: UP008 Use `super()` instead of `super(__class__, self)` + | +135 | class ExampleWithKeywords: +136 | def method1(self): +137 | super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 +138 | +139 | def method2(self): + | + = help: Remove `super()` parameters + +UP008.py:140:14: UP008 Use `super()` instead of `super(__class__, self)` + | +139 | def method2(self): +140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 +141 | +142 | def method3(self): + | + = help: Remove `super()` parameters + +UP008.py:143:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +142 | def method3(self): +143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +140 140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed +141 141 | +142 142 | def method3(self): +143 |- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords + 143 |+ super().some_method() # Should be fixed - no keywords diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py__preview.snap new file mode 100644 index 0000000000000..c6c4bf37f16ad --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py__preview.snap @@ -0,0 +1,301 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP008.py:17:23: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +16 | def wrong(self): +17 | parent = super(Child, self) # wrong + | ^^^^^^^^^^^^^ UP008 +18 | super(Child, self).method # wrong +19 | super( + | + = help: Remove `super()` parameters + +ℹ Safe fix +14 14 | Parent.super(1, 2) # ok +15 15 | +16 16 | def wrong(self): +17 |- parent = super(Child, self) # wrong + 17 |+ parent = super() # wrong +18 18 | super(Child, self).method # wrong +19 19 | super( +20 20 | Child, + +UP008.py:18:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +16 | def wrong(self): +17 | parent = super(Child, self) # wrong +18 | super(Child, self).method # wrong + | ^^^^^^^^^^^^^ UP008 +19 | super( +20 | Child, + | + = help: Remove `super()` parameters + +ℹ Safe fix +15 15 | +16 16 | def wrong(self): +17 17 | parent = super(Child, self) # wrong +18 |- super(Child, self).method # wrong + 18 |+ super().method # wrong +19 19 | super( +20 20 | Child, +21 21 | self, + +UP008.py:19:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +17 | parent = super(Child, self) # wrong +18 | super(Child, self).method # wrong +19 | super( + | ______________^ +20 | | Child, +21 | | self, +22 | | ).method() # wrong + | |_________^ UP008 + | + = help: Remove `super()` parameters + +ℹ Safe fix +16 16 | def wrong(self): +17 17 | parent = super(Child, self) # wrong +18 18 | super(Child, self).method # wrong +19 |- super( +20 |- Child, +21 |- self, +22 |- ).method() # wrong + 19 |+ super().method() # wrong +23 20 | +24 21 | +25 22 | class BaseClass: + +UP008.py:36:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +34 | class MyClass(BaseClass): +35 | def normal(self): +36 | super(MyClass, self).f() # can use super() + | ^^^^^^^^^^^^^^^ UP008 +37 | super().f() + | + = help: Remove `super()` parameters + +ℹ Safe fix +33 33 | +34 34 | class MyClass(BaseClass): +35 35 | def normal(self): +36 |- super(MyClass, self).f() # can use super() + 36 |+ super().f() # can use super() +37 37 | super().f() +38 38 | +39 39 | def different_argument(self, other): + +UP008.py:50:18: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +49 | def inner_argument(self): +50 | super(MyClass, self).f() # can use super() + | ^^^^^^^^^^^^^^^ UP008 +51 | super().f() + | + = help: Remove `super()` parameters + +ℹ Safe fix +47 47 | super(MyClass, self).f() # CANNOT use super() +48 48 | +49 49 | def inner_argument(self): +50 |- super(MyClass, self).f() # can use super() + 50 |+ super().f() # can use super() +51 51 | super().f() +52 52 | +53 53 | outer_argument() + +UP008.py:74:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +72 | class DataClass: +73 | def normal(self): +74 | super(DataClass, self).f() # Error + | ^^^^^^^^^^^^^^^^^ UP008 +75 | super().f() # OK + | + = help: Remove `super()` parameters + +ℹ Safe fix +71 71 | @dataclass +72 72 | class DataClass: +73 73 | def normal(self): +74 |- super(DataClass, self).f() # Error + 74 |+ super().f() # Error +75 75 | super().f() # OK +76 76 | +77 77 | + +UP008.py:92:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +90 | class B(A): +91 | def bar(self): +92 | super(__class__, self).foo() + | ^^^^^^^^^^^^^^^^^ UP008 + | + = help: Remove `super()` parameters + +ℹ Safe fix +89 89 | +90 90 | class B(A): +91 91 | def bar(self): +92 |- super(__class__, self).foo() + 92 |+ super().foo() +93 93 | +94 94 | +95 95 | # see: https://github.com/astral-sh/ruff/issues/18684 + +UP008.py:107:23: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +105 | class C: +106 | def f(self): +107 | builtins.super(C, self) + | ^^^^^^^^^ UP008 + | + = help: Remove `super()` parameters + +ℹ Safe fix +104 104 | +105 105 | class C: +106 106 | def f(self): +107 |- builtins.super(C, self) + 107 |+ builtins.super() +108 108 | +109 109 | +110 110 | # see: https://github.com/astral-sh/ruff/issues/18533 + +UP008.py:113:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +111 | class ClassForCommentEnthusiasts(BaseClass): +112 | def with_comments(self): +113 | super( + | ______________^ +114 | | # super helpful comment +115 | | ClassForCommentEnthusiasts, +116 | | self +117 | | ).f() + | |_________^ UP008 +118 | super( +119 | ClassForCommentEnthusiasts, + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +110 110 | # see: https://github.com/astral-sh/ruff/issues/18533 +111 111 | class ClassForCommentEnthusiasts(BaseClass): +112 112 | def with_comments(self): +113 |- super( +114 |- # super helpful comment +115 |- ClassForCommentEnthusiasts, +116 |- self +117 |- ).f() + 113 |+ super().f() +118 114 | super( +119 115 | ClassForCommentEnthusiasts, +120 116 | # even more helpful comment + +UP008.py:118:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +116 | self +117 | ).f() +118 | super( + | ______________^ +119 | | ClassForCommentEnthusiasts, +120 | | # even more helpful comment +121 | | self +122 | | ).f() + | |_________^ UP008 +123 | super( +124 | ClassForCommentEnthusiasts, + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +115 115 | ClassForCommentEnthusiasts, +116 116 | self +117 117 | ).f() +118 |- super( +119 |- ClassForCommentEnthusiasts, +120 |- # even more helpful comment +121 |- self +122 |- ).f() + 118 |+ super().f() +123 119 | super( +124 120 | ClassForCommentEnthusiasts, +125 121 | self + +UP008.py:123:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +121 | self +122 | ).f() +123 | super( + | ______________^ +124 | | ClassForCommentEnthusiasts, +125 | | self +126 | | # also a comment +127 | | ).f() + | |_________^ UP008 + | + = help: Remove `super()` parameters + +ℹ Unsafe fix +120 120 | # even more helpful comment +121 121 | self +122 122 | ).f() +123 |- super( +124 |- ClassForCommentEnthusiasts, +125 |- self +126 |- # also a comment +127 |- ).f() + 123 |+ super().f() +128 124 | +129 125 | +130 126 | # Issue #19096: super calls with keyword arguments should emit diagnostic but not be fixed + +UP008.py:133:21: UP008 Use `super()` instead of `super(__class__, self)` + | +131 | class Ord(int): +132 | def __len__(self): +133 | return super(Ord, self, uhoh=True, **{"error": True}).bit_length() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 +134 | +135 | class ExampleWithKeywords: + | + = help: Remove `super()` parameters + +UP008.py:137:14: UP008 Use `super()` instead of `super(__class__, self)` + | +135 | class ExampleWithKeywords: +136 | def method1(self): +137 | super(ExampleWithKeywords, self, invalid=True).some_method() # Should emit diagnostic but NOT be fixed + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 +138 | +139 | def method2(self): + | + = help: Remove `super()` parameters + +UP008.py:140:14: UP008 Use `super()` instead of `super(__class__, self)` + | +139 | def method2(self): +140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 +141 | +142 | def method3(self): + | + = help: Remove `super()` parameters + +UP008.py:143:14: UP008 [*] Use `super()` instead of `super(__class__, self)` + | +142 | def method3(self): +143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP008 + | + = help: Remove `super()` parameters + +ℹ Safe fix +140 140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed +141 141 | +142 142 | def method3(self): +143 |- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords + 143 |+ super().some_method() # Should be fixed - no keywords diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010.py.snap index 89d50cf2f49f8..4696822fe94e7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010.py.snap @@ -156,6 +156,7 @@ UP010.py:13:5: UP010 [*] Unnecessary `__future__` import `generator_stop` for ta 13 | from __future__ import generator_stop | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP010 14 | from __future__ import invalid_module, generators +15 | from __future__ import generators # comment | = help: Remove unnecessary `__future__` import @@ -165,6 +166,7 @@ UP010.py:13:5: UP010 [*] Unnecessary `__future__` import `generator_stop` for ta 12 12 | if True: 13 |- from __future__ import generator_stop 14 13 | from __future__ import invalid_module, generators +15 14 | from __future__ import generators # comment UP010.py:14:5: UP010 [*] Unnecessary `__future__` import `generators` for target Python version | @@ -172,6 +174,7 @@ UP010.py:14:5: UP010 [*] Unnecessary `__future__` import `generators` for target 13 | from __future__ import generator_stop 14 | from __future__ import invalid_module, generators | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP010 +15 | from __future__ import generators # comment | = help: Remove unnecessary `__future__` import @@ -181,3 +184,19 @@ UP010.py:14:5: UP010 [*] Unnecessary `__future__` import `generators` for target 13 13 | from __future__ import generator_stop 14 |- from __future__ import invalid_module, generators 14 |+ from __future__ import invalid_module +15 15 | from __future__ import generators # comment + +UP010.py:15:5: UP010 [*] Unnecessary `__future__` import `generators` for target Python version + | +13 | from __future__ import generator_stop +14 | from __future__ import invalid_module, generators +15 | from __future__ import generators # comment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP010 + | + = help: Remove unnecessary `__future__` import + +ℹ Unsafe fix +12 12 | if True: +13 13 | from __future__ import generator_stop +14 14 | from __future__ import invalid_module, generators +15 |- from __future__ import generators # comment diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap index 1c04500c3ded3..d57eac6e12bae 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap @@ -602,6 +602,8 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal) 85 | | ipsum''' # Comment 86 | | ).foo | |_^ UP018 +87 | +88 | # https://github.com/astral-sh/ruff/issues/17606 | = help: Replace with string literal @@ -615,3 +617,80 @@ UP018.py:83:1: UP018 [*] Unnecessary `str` call (rewrite as a literal) 86 |-).foo 83 |+'''Lorem 84 |+ ipsum'''.foo +87 85 | +88 86 | # https://github.com/astral-sh/ruff/issues/17606 +89 87 | bool(True)and None + +UP018.py:89:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal) + | +88 | # https://github.com/astral-sh/ruff/issues/17606 +89 | bool(True)and None + | ^^^^^^^^^^ UP018 +90 | int(1)and None +91 | float(1.)and None + | + = help: Replace with boolean literal + +ℹ Safe fix +86 86 | ).foo +87 87 | +88 88 | # https://github.com/astral-sh/ruff/issues/17606 +89 |-bool(True)and None + 89 |+True and None +90 90 | int(1)and None +91 91 | float(1.)and None +92 92 | bool(True)and() + +UP018.py:90:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +88 | # https://github.com/astral-sh/ruff/issues/17606 +89 | bool(True)and None +90 | int(1)and None + | ^^^^^^ UP018 +91 | float(1.)and None +92 | bool(True)and() + | + = help: Replace with integer literal + +ℹ Safe fix +87 87 | +88 88 | # https://github.com/astral-sh/ruff/issues/17606 +89 89 | bool(True)and None +90 |-int(1)and None + 90 |+1 and None +91 91 | float(1.)and None +92 92 | bool(True)and() + +UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal) + | +89 | bool(True)and None +90 | int(1)and None +91 | float(1.)and None + | ^^^^^^^^^ UP018 +92 | bool(True)and() + | + = help: Replace with float literal + +ℹ Safe fix +88 88 | # https://github.com/astral-sh/ruff/issues/17606 +89 89 | bool(True)and None +90 90 | int(1)and None +91 |-float(1.)and None + 91 |+1. and None +92 92 | bool(True)and() + +UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal) + | +90 | int(1)and None +91 | float(1.)and None +92 | bool(True)and() + | ^^^^^^^^^^ UP018 + | + = help: Replace with boolean literal + +ℹ Safe fix +89 89 | bool(True)and None +90 90 | int(1)and None +91 91 | float(1.)and None +92 |-bool(True)and() + 92 |+True and() diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap new file mode 100644 index 0000000000000..6dae74bfe53b6 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP018_CR.py:2:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +1 | # Keep parenthesis around preserved CR int(- 1) int(+ 1) + | ^^^^^^^^^^^ UP018 + | + = help: Replace with integer literal + +ℹ Safe fix +1 1 | # Keep parenthesis around preserved CR 2 |-int(- 2 |+(- 3 3 | 1) 4 4 | int(+ 5 5 | 1) + +UP018_CR.py:4:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +2 | int(- 1) int(+ 1) + | ^^^^^^^^^^^ UP018 + | + = help: Replace with integer literal + +ℹ Safe fix +1 1 | # Keep parenthesis around preserved CR 2 2 | int(- 3 3 | 1) 4 |-int(+ 4 |+(+ 5 5 | 1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap new file mode 100644 index 0000000000000..d3a530d273c71 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP018_LF.py:3:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +1 | # Keep parentheses around preserved \n +2 | +3 | / int(- +4 | | 1) + | |______^ UP018 +5 | +6 | int(+ + | + = help: Replace with integer literal + +ℹ Safe fix +1 1 | # Keep parentheses around preserved \n +2 2 | +3 |-int(- + 3 |+(- +4 4 | 1) +5 5 | +6 6 | int(+ + +UP018_LF.py:6:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +4 | 1) +5 | +6 | / int(+ +7 | | 1) + | |______^ UP018 + | + = help: Replace with integer literal + +ℹ Safe fix +3 3 | int(- +4 4 | 1) +5 5 | +6 |-int(+ + 6 |+(+ +7 7 | 1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap index b6e358f62f226..7a05ae711be00 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap @@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError` | 8 | # Testing the modules - 9 | import socket, mmap, select + 9 | import socket, mmap, select, resource 10 | raise socket.error | ^^^^^^^^^^^^ UP024 11 | raise mmap.error @@ -15,32 +15,33 @@ UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError` ℹ Safe fix 7 7 | 8 8 | # Testing the modules -9 9 | import socket, mmap, select +9 9 | import socket, mmap, select, resource 10 |-raise socket.error 10 |+raise OSError 11 11 | raise mmap.error 12 12 | raise select.error -13 13 | +13 13 | raise resource.error UP024_2.py:11:7: UP024 [*] Replace aliased errors with `OSError` | - 9 | import socket, mmap, select + 9 | import socket, mmap, select, resource 10 | raise socket.error 11 | raise mmap.error | ^^^^^^^^^^ UP024 12 | raise select.error +13 | raise resource.error | = help: Replace `mmap.error` with builtin `OSError` ℹ Safe fix 8 8 | # Testing the modules -9 9 | import socket, mmap, select +9 9 | import socket, mmap, select, resource 10 10 | raise socket.error 11 |-raise mmap.error 11 |+raise OSError 12 12 | raise select.error -13 13 | -14 14 | raise socket.error() +13 13 | raise resource.error +14 14 | UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError` | @@ -48,355 +49,416 @@ UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError` 11 | raise mmap.error 12 | raise select.error | ^^^^^^^^^^^^ UP024 -13 | -14 | raise socket.error() +13 | raise resource.error | = help: Replace `select.error` with builtin `OSError` ℹ Safe fix -9 9 | import socket, mmap, select +9 9 | import socket, mmap, select, resource 10 10 | raise socket.error 11 11 | raise mmap.error 12 |-raise select.error 12 |+raise OSError -13 13 | -14 14 | raise socket.error() -15 15 | raise mmap.error(1) +13 13 | raise resource.error +14 14 | +15 15 | raise socket.error() -UP024_2.py:14:7: UP024 [*] Replace aliased errors with `OSError` +UP024_2.py:13:7: UP024 [*] Replace aliased errors with `OSError` | +11 | raise mmap.error 12 | raise select.error -13 | -14 | raise socket.error() - | ^^^^^^^^^^^^ UP024 -15 | raise mmap.error(1) -16 | raise select.error(1, 2) +13 | raise resource.error + | ^^^^^^^^^^^^^^ UP024 +14 | +15 | raise socket.error() | - = help: Replace `socket.error` with builtin `OSError` + = help: Replace `resource.error` with builtin `OSError` ℹ Safe fix +10 10 | raise socket.error 11 11 | raise mmap.error 12 12 | raise select.error -13 13 | -14 |-raise socket.error() - 14 |+raise OSError() -15 15 | raise mmap.error(1) -16 16 | raise select.error(1, 2) -17 17 | +13 |-raise resource.error + 13 |+raise OSError +14 14 | +15 15 | raise socket.error() +16 16 | raise mmap.error(1) UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError` | -14 | raise socket.error() -15 | raise mmap.error(1) - | ^^^^^^^^^^ UP024 -16 | raise select.error(1, 2) +13 | raise resource.error +14 | +15 | raise socket.error() + | ^^^^^^^^^^^^ UP024 +16 | raise mmap.error(1) +17 | raise select.error(1, 2) | - = help: Replace `mmap.error` with builtin `OSError` + = help: Replace `socket.error` with builtin `OSError` ℹ Safe fix 12 12 | raise select.error -13 13 | -14 14 | raise socket.error() -15 |-raise mmap.error(1) - 15 |+raise OSError(1) -16 16 | raise select.error(1, 2) -17 17 | -18 18 | raise socket.error( +13 13 | raise resource.error +14 14 | +15 |-raise socket.error() + 15 |+raise OSError() +16 16 | raise mmap.error(1) +17 17 | raise select.error(1, 2) +18 18 | raise resource.error(1, "strerror", "filename") UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError` | -14 | raise socket.error() -15 | raise mmap.error(1) -16 | raise select.error(1, 2) +15 | raise socket.error() +16 | raise mmap.error(1) + | ^^^^^^^^^^ UP024 +17 | raise select.error(1, 2) +18 | raise resource.error(1, "strerror", "filename") + | + = help: Replace `mmap.error` with builtin `OSError` + +ℹ Safe fix +13 13 | raise resource.error +14 14 | +15 15 | raise socket.error() +16 |-raise mmap.error(1) + 16 |+raise OSError(1) +17 17 | raise select.error(1, 2) +18 18 | raise resource.error(1, "strerror", "filename") +19 19 | + +UP024_2.py:17:7: UP024 [*] Replace aliased errors with `OSError` + | +15 | raise socket.error() +16 | raise mmap.error(1) +17 | raise select.error(1, 2) | ^^^^^^^^^^^^ UP024 -17 | -18 | raise socket.error( +18 | raise resource.error(1, "strerror", "filename") | = help: Replace `select.error` with builtin `OSError` ℹ Safe fix -13 13 | -14 14 | raise socket.error() -15 15 | raise mmap.error(1) -16 |-raise select.error(1, 2) - 16 |+raise OSError(1, 2) -17 17 | -18 18 | raise socket.error( -19 19 | 1, +14 14 | +15 15 | raise socket.error() +16 16 | raise mmap.error(1) +17 |-raise select.error(1, 2) + 17 |+raise OSError(1, 2) +18 18 | raise resource.error(1, "strerror", "filename") +19 19 | +20 20 | raise socket.error( UP024_2.py:18:7: UP024 [*] Replace aliased errors with `OSError` | -16 | raise select.error(1, 2) -17 | -18 | raise socket.error( +16 | raise mmap.error(1) +17 | raise select.error(1, 2) +18 | raise resource.error(1, "strerror", "filename") + | ^^^^^^^^^^^^^^ UP024 +19 | +20 | raise socket.error( + | + = help: Replace `resource.error` with builtin `OSError` + +ℹ Safe fix +15 15 | raise socket.error() +16 16 | raise mmap.error(1) +17 17 | raise select.error(1, 2) +18 |-raise resource.error(1, "strerror", "filename") + 18 |+raise OSError(1, "strerror", "filename") +19 19 | +20 20 | raise socket.error( +21 21 | 1, + +UP024_2.py:20:7: UP024 [*] Replace aliased errors with `OSError` + | +18 | raise resource.error(1, "strerror", "filename") +19 | +20 | raise socket.error( | ^^^^^^^^^^^^ UP024 -19 | 1, -20 | 2, +21 | 1, +22 | 2, | = help: Replace `socket.error` with builtin `OSError` ℹ Safe fix -15 15 | raise mmap.error(1) -16 16 | raise select.error(1, 2) -17 17 | -18 |-raise socket.error( - 18 |+raise OSError( -19 19 | 1, -20 20 | 2, -21 21 | 3, - -UP024_2.py:25:7: UP024 [*] Replace aliased errors with `OSError` - | -24 | from mmap import error -25 | raise error +17 17 | raise select.error(1, 2) +18 18 | raise resource.error(1, "strerror", "filename") +19 19 | +20 |-raise socket.error( + 20 |+raise OSError( +21 21 | 1, +22 22 | 2, +23 23 | 3, + +UP024_2.py:27:7: UP024 [*] Replace aliased errors with `OSError` + | +26 | from mmap import error +27 | raise error | ^^^^^ UP024 -26 | -27 | from socket import error +28 | +29 | from socket import error | = help: Replace `error` with builtin `OSError` ℹ Safe fix -22 22 | ) -23 23 | -24 24 | from mmap import error -25 |-raise error - 25 |+raise OSError -26 26 | -27 27 | from socket import error -28 28 | raise error(1) - -UP024_2.py:28:7: UP024 [*] Replace aliased errors with `OSError` - | -27 | from socket import error -28 | raise error(1) +24 24 | ) +25 25 | +26 26 | from mmap import error +27 |-raise error + 27 |+raise OSError +28 28 | +29 29 | from socket import error +30 30 | raise error(1) + +UP024_2.py:30:7: UP024 [*] Replace aliased errors with `OSError` + | +29 | from socket import error +30 | raise error(1) | ^^^^^ UP024 -29 | -30 | from select import error +31 | +32 | from select import error | = help: Replace `error` with builtin `OSError` ℹ Safe fix -25 25 | raise error -26 26 | -27 27 | from socket import error -28 |-raise error(1) - 28 |+raise OSError(1) -29 29 | -30 30 | from select import error -31 31 | raise error(1, 2) - -UP024_2.py:31:7: UP024 [*] Replace aliased errors with `OSError` - | -30 | from select import error -31 | raise error(1, 2) +27 27 | raise error +28 28 | +29 29 | from socket import error +30 |-raise error(1) + 30 |+raise OSError(1) +31 31 | +32 32 | from select import error +33 33 | raise error(1, 2) + +UP024_2.py:33:7: UP024 [*] Replace aliased errors with `OSError` + | +32 | from select import error +33 | raise error(1, 2) | ^^^^^ UP024 -32 | -33 | # Testing the names +34 | +35 | from resource import error | = help: Replace `error` with builtin `OSError` ℹ Safe fix -28 28 | raise error(1) -29 29 | -30 30 | from select import error -31 |-raise error(1, 2) - 31 |+raise OSError(1, 2) -32 32 | -33 33 | # Testing the names -34 34 | raise EnvironmentError - -UP024_2.py:34:7: UP024 [*] Replace aliased errors with `OSError` - | -33 | # Testing the names -34 | raise EnvironmentError +30 30 | raise error(1) +31 31 | +32 32 | from select import error +33 |-raise error(1, 2) + 33 |+raise OSError(1, 2) +34 34 | +35 35 | from resource import error +36 36 | raise error(1, "strerror", "filename") + +UP024_2.py:36:7: UP024 [*] Replace aliased errors with `OSError` + | +35 | from resource import error +36 | raise error(1, "strerror", "filename") + | ^^^^^ UP024 +37 | +38 | # Testing the names + | + = help: Replace `error` with builtin `OSError` + +ℹ Safe fix +33 33 | raise error(1, 2) +34 34 | +35 35 | from resource import error +36 |-raise error(1, "strerror", "filename") + 36 |+raise OSError(1, "strerror", "filename") +37 37 | +38 38 | # Testing the names +39 39 | raise EnvironmentError + +UP024_2.py:39:7: UP024 [*] Replace aliased errors with `OSError` + | +38 | # Testing the names +39 | raise EnvironmentError | ^^^^^^^^^^^^^^^^ UP024 -35 | raise IOError -36 | raise WindowsError +40 | raise IOError +41 | raise WindowsError | = help: Replace `EnvironmentError` with builtin `OSError` ℹ Safe fix -31 31 | raise error(1, 2) -32 32 | -33 33 | # Testing the names -34 |-raise EnvironmentError - 34 |+raise OSError -35 35 | raise IOError -36 36 | raise WindowsError +36 36 | raise error(1, "strerror", "filename") 37 37 | +38 38 | # Testing the names +39 |-raise EnvironmentError + 39 |+raise OSError +40 40 | raise IOError +41 41 | raise WindowsError +42 42 | -UP024_2.py:35:7: UP024 [*] Replace aliased errors with `OSError` +UP024_2.py:40:7: UP024 [*] Replace aliased errors with `OSError` | -33 | # Testing the names -34 | raise EnvironmentError -35 | raise IOError +38 | # Testing the names +39 | raise EnvironmentError +40 | raise IOError | ^^^^^^^ UP024 -36 | raise WindowsError +41 | raise WindowsError | = help: Replace `IOError` with builtin `OSError` ℹ Safe fix -32 32 | -33 33 | # Testing the names -34 34 | raise EnvironmentError -35 |-raise IOError - 35 |+raise OSError -36 36 | raise WindowsError 37 37 | -38 38 | raise EnvironmentError() - -UP024_2.py:36:7: UP024 [*] Replace aliased errors with `OSError` - | -34 | raise EnvironmentError -35 | raise IOError -36 | raise WindowsError +38 38 | # Testing the names +39 39 | raise EnvironmentError +40 |-raise IOError + 40 |+raise OSError +41 41 | raise WindowsError +42 42 | +43 43 | raise EnvironmentError() + +UP024_2.py:41:7: UP024 [*] Replace aliased errors with `OSError` + | +39 | raise EnvironmentError +40 | raise IOError +41 | raise WindowsError | ^^^^^^^^^^^^ UP024 -37 | -38 | raise EnvironmentError() +42 | +43 | raise EnvironmentError() | = help: Replace `WindowsError` with builtin `OSError` ℹ Safe fix -33 33 | # Testing the names -34 34 | raise EnvironmentError -35 35 | raise IOError -36 |-raise WindowsError - 36 |+raise OSError -37 37 | -38 38 | raise EnvironmentError() -39 39 | raise IOError(1) - -UP024_2.py:38:7: UP024 [*] Replace aliased errors with `OSError` - | -36 | raise WindowsError -37 | -38 | raise EnvironmentError() +38 38 | # Testing the names +39 39 | raise EnvironmentError +40 40 | raise IOError +41 |-raise WindowsError + 41 |+raise OSError +42 42 | +43 43 | raise EnvironmentError() +44 44 | raise IOError(1) + +UP024_2.py:43:7: UP024 [*] Replace aliased errors with `OSError` + | +41 | raise WindowsError +42 | +43 | raise EnvironmentError() | ^^^^^^^^^^^^^^^^ UP024 -39 | raise IOError(1) -40 | raise WindowsError(1, 2) +44 | raise IOError(1) +45 | raise WindowsError(1, 2) | = help: Replace `EnvironmentError` with builtin `OSError` ℹ Safe fix -35 35 | raise IOError -36 36 | raise WindowsError -37 37 | -38 |-raise EnvironmentError() - 38 |+raise OSError() -39 39 | raise IOError(1) -40 40 | raise WindowsError(1, 2) -41 41 | - -UP024_2.py:39:7: UP024 [*] Replace aliased errors with `OSError` - | -38 | raise EnvironmentError() -39 | raise IOError(1) +40 40 | raise IOError +41 41 | raise WindowsError +42 42 | +43 |-raise EnvironmentError() + 43 |+raise OSError() +44 44 | raise IOError(1) +45 45 | raise WindowsError(1, 2) +46 46 | + +UP024_2.py:44:7: UP024 [*] Replace aliased errors with `OSError` + | +43 | raise EnvironmentError() +44 | raise IOError(1) | ^^^^^^^ UP024 -40 | raise WindowsError(1, 2) +45 | raise WindowsError(1, 2) | = help: Replace `IOError` with builtin `OSError` ℹ Safe fix -36 36 | raise WindowsError -37 37 | -38 38 | raise EnvironmentError() -39 |-raise IOError(1) - 39 |+raise OSError(1) -40 40 | raise WindowsError(1, 2) -41 41 | -42 42 | raise EnvironmentError( - -UP024_2.py:40:7: UP024 [*] Replace aliased errors with `OSError` - | -38 | raise EnvironmentError() -39 | raise IOError(1) -40 | raise WindowsError(1, 2) +41 41 | raise WindowsError +42 42 | +43 43 | raise EnvironmentError() +44 |-raise IOError(1) + 44 |+raise OSError(1) +45 45 | raise WindowsError(1, 2) +46 46 | +47 47 | raise EnvironmentError( + +UP024_2.py:45:7: UP024 [*] Replace aliased errors with `OSError` + | +43 | raise EnvironmentError() +44 | raise IOError(1) +45 | raise WindowsError(1, 2) | ^^^^^^^^^^^^ UP024 -41 | -42 | raise EnvironmentError( +46 | +47 | raise EnvironmentError( | = help: Replace `WindowsError` with builtin `OSError` ℹ Safe fix -37 37 | -38 38 | raise EnvironmentError() -39 39 | raise IOError(1) -40 |-raise WindowsError(1, 2) - 40 |+raise OSError(1, 2) -41 41 | -42 42 | raise EnvironmentError( -43 43 | 1, - -UP024_2.py:42:7: UP024 [*] Replace aliased errors with `OSError` - | -40 | raise WindowsError(1, 2) -41 | -42 | raise EnvironmentError( +42 42 | +43 43 | raise EnvironmentError() +44 44 | raise IOError(1) +45 |-raise WindowsError(1, 2) + 45 |+raise OSError(1, 2) +46 46 | +47 47 | raise EnvironmentError( +48 48 | 1, + +UP024_2.py:47:7: UP024 [*] Replace aliased errors with `OSError` + | +45 | raise WindowsError(1, 2) +46 | +47 | raise EnvironmentError( | ^^^^^^^^^^^^^^^^ UP024 -43 | 1, -44 | 2, +48 | 1, +49 | 2, | = help: Replace `EnvironmentError` with builtin `OSError` ℹ Safe fix -39 39 | raise IOError(1) -40 40 | raise WindowsError(1, 2) -41 41 | -42 |-raise EnvironmentError( - 42 |+raise OSError( -43 43 | 1, -44 44 | 2, -45 45 | 3, - -UP024_2.py:48:7: UP024 [*] Replace aliased errors with `OSError` - | -46 | ) -47 | -48 | raise WindowsError +44 44 | raise IOError(1) +45 45 | raise WindowsError(1, 2) +46 46 | +47 |-raise EnvironmentError( + 47 |+raise OSError( +48 48 | 1, +49 49 | 2, +50 50 | 3, + +UP024_2.py:53:7: UP024 [*] Replace aliased errors with `OSError` + | +51 | ) +52 | +53 | raise WindowsError | ^^^^^^^^^^^^ UP024 -49 | raise EnvironmentError(1) -50 | raise IOError(1, 2) +54 | raise EnvironmentError(1) +55 | raise IOError(1, 2) | = help: Replace `WindowsError` with builtin `OSError` ℹ Safe fix -45 45 | 3, -46 46 | ) -47 47 | -48 |-raise WindowsError - 48 |+raise OSError -49 49 | raise EnvironmentError(1) -50 50 | raise IOError(1, 2) - -UP024_2.py:49:7: UP024 [*] Replace aliased errors with `OSError` - | -48 | raise WindowsError -49 | raise EnvironmentError(1) +50 50 | 3, +51 51 | ) +52 52 | +53 |-raise WindowsError + 53 |+raise OSError +54 54 | raise EnvironmentError(1) +55 55 | raise IOError(1, 2) + +UP024_2.py:54:7: UP024 [*] Replace aliased errors with `OSError` + | +53 | raise WindowsError +54 | raise EnvironmentError(1) | ^^^^^^^^^^^^^^^^ UP024 -50 | raise IOError(1, 2) +55 | raise IOError(1, 2) | = help: Replace `EnvironmentError` with builtin `OSError` ℹ Safe fix -46 46 | ) -47 47 | -48 48 | raise WindowsError -49 |-raise EnvironmentError(1) - 49 |+raise OSError(1) -50 50 | raise IOError(1, 2) - -UP024_2.py:50:7: UP024 [*] Replace aliased errors with `OSError` - | -48 | raise WindowsError -49 | raise EnvironmentError(1) -50 | raise IOError(1, 2) +51 51 | ) +52 52 | +53 53 | raise WindowsError +54 |-raise EnvironmentError(1) + 54 |+raise OSError(1) +55 55 | raise IOError(1, 2) + +UP024_2.py:55:7: UP024 [*] Replace aliased errors with `OSError` + | +53 | raise WindowsError +54 | raise EnvironmentError(1) +55 | raise IOError(1, 2) | ^^^^^^^ UP024 | = help: Replace `IOError` with builtin `OSError` ℹ Safe fix -47 47 | -48 48 | raise WindowsError -49 49 | raise EnvironmentError(1) -50 |-raise IOError(1, 2) - 50 |+raise OSError(1, 2) +52 52 | +53 53 | raise WindowsError +54 54 | raise EnvironmentError(1) +55 |-raise IOError(1, 2) + 55 |+raise OSError(1, 2) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap index 916977bfed2f4..b9b6bc4da1353 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap @@ -281,14 +281,18 @@ UP025.py:27:7: UP025 [*] Remove unicode literals from strings 25 25 | return"Hello" # OK 26 26 | 27 |-f"foo"u"bar" # OK - 27 |+f"foo""bar" # OK + 27 |+f"foo" "bar" # OK 28 28 | f"foo" u"bar" # OK +29 29 | +30 30 | # https://github.com/astral-sh/ruff/issues/18895 UP025.py:28:8: UP025 [*] Remove unicode literals from strings | 27 | f"foo"u"bar" # OK 28 | f"foo" u"bar" # OK | ^^^^^^ UP025 +29 | +30 | # https://github.com/astral-sh/ruff/issues/18895 | = help: Remove unicode prefix @@ -298,3 +302,80 @@ UP025.py:28:8: UP025 [*] Remove unicode literals from strings 27 27 | f"foo"u"bar" # OK 28 |-f"foo" u"bar" # OK 28 |+f"foo" "bar" # OK +29 29 | +30 30 | # https://github.com/astral-sh/ruff/issues/18895 +31 31 | ""u"" + +UP025.py:31:3: UP025 [*] Remove unicode literals from strings + | +30 | # https://github.com/astral-sh/ruff/issues/18895 +31 | ""u"" + | ^^^ UP025 +32 | ""u"hi" +33 | """"""""""""""""""""u"hi" + | + = help: Remove unicode prefix + +ℹ Safe fix +28 28 | f"foo" u"bar" # OK +29 29 | +30 30 | # https://github.com/astral-sh/ruff/issues/18895 +31 |-""u"" + 31 |+"" "" +32 32 | ""u"hi" +33 33 | """"""""""""""""""""u"hi" +34 34 | ""U"helloooo" + +UP025.py:32:3: UP025 [*] Remove unicode literals from strings + | +30 | # https://github.com/astral-sh/ruff/issues/18895 +31 | ""u"" +32 | ""u"hi" + | ^^^^^ UP025 +33 | """"""""""""""""""""u"hi" +34 | ""U"helloooo" + | + = help: Remove unicode prefix + +ℹ Safe fix +29 29 | +30 30 | # https://github.com/astral-sh/ruff/issues/18895 +31 31 | ""u"" +32 |-""u"hi" + 32 |+"" "hi" +33 33 | """"""""""""""""""""u"hi" +34 34 | ""U"helloooo" + +UP025.py:33:21: UP025 [*] Remove unicode literals from strings + | +31 | ""u"" +32 | ""u"hi" +33 | """"""""""""""""""""u"hi" + | ^^^^^ UP025 +34 | ""U"helloooo" + | + = help: Remove unicode prefix + +ℹ Safe fix +30 30 | # https://github.com/astral-sh/ruff/issues/18895 +31 31 | ""u"" +32 32 | ""u"hi" +33 |-""""""""""""""""""""u"hi" + 33 |+"""""""""""""""""""" "hi" +34 34 | ""U"helloooo" + +UP025.py:34:3: UP025 [*] Remove unicode literals from strings + | +32 | ""u"hi" +33 | """"""""""""""""""""u"hi" +34 | ""U"helloooo" + | ^^^^^^^^^^^ UP025 + | + = help: Remove unicode prefix + +ℹ Safe fix +31 31 | ""u"" +32 32 | ""u"hi" +33 33 | """"""""""""""""""""u"hi" +34 |-""U"helloooo" + 34 |+"" "helloooo" diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap index fb3b19b94aef7..95c0bee046365 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap @@ -1179,6 +1179,8 @@ UP035.py:111:1: UP035 [*] Import from `warnings` instead: `deprecated` 110 | # UP035 on py313+ only 111 | from typing_extensions import deprecated | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035 +112 | +113 | # UP035 on py313+ only | = help: Import from `warnings` @@ -1189,5 +1191,25 @@ UP035.py:111:1: UP035 [*] Import from `warnings` instead: `deprecated` 111 |-from typing_extensions import deprecated 111 |+from warnings import deprecated 112 112 | -113 113 | -114 114 | # https://github.com/astral-sh/ruff/issues/15780 +113 113 | # UP035 on py313+ only +114 114 | from typing_extensions import get_type_hints + +UP035.py:114:1: UP035 [*] Import from `typing` instead: `get_type_hints` + | +113 | # UP035 on py313+ only +114 | from typing_extensions import get_type_hints + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035 +115 | +116 | # https://github.com/astral-sh/ruff/issues/15780 + | + = help: Import from `typing` + +ℹ Safe fix +111 111 | from typing_extensions import deprecated +112 112 | +113 113 | # UP035 on py313+ only +114 |-from typing_extensions import get_type_hints + 114 |+from typing import get_type_hints +115 115 | +116 116 | # https://github.com/astral-sh/ruff/issues/15780 +117 117 | from typing_extensions import is_typeddict diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap index efd113261f901..b1c24a6ba9901 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap @@ -157,3 +157,123 @@ UP036_5.py:48:24: UP036 Version specifier is invalid | ^^^^^^^^^^^^^^^ UP036 49 | print() | + +UP036_5.py:77:4: UP036 [*] Version block is outdated for minimum Python version + | +75 | # https://github.com/astral-sh/ruff/issues/18165 +76 | +77 | if sys.version_info.major >= 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +78 | print("3") +79 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +74 74 | +75 75 | # https://github.com/astral-sh/ruff/issues/18165 +76 76 | +77 |-if sys.version_info.major >= 3: +78 |- print("3") +79 |-else: +80 |- print("2") + 77 |+print("3") +81 78 | +82 79 | if sys.version_info.major > 3: +83 80 | print("3") + +UP036_5.py:82:4: UP036 [*] Version block is outdated for minimum Python version + | +80 | print("2") +81 | +82 | if sys.version_info.major > 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +83 | print("3") +84 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +79 79 | else: +80 80 | print("2") +81 81 | +82 |-if sys.version_info.major > 3: +83 |- print("3") +84 |-else: +85 |- print("2") + 82 |+print("2") +86 83 | +87 84 | if sys.version_info.major <= 3: +88 85 | print("3") + +UP036_5.py:87:4: UP036 [*] Version block is outdated for minimum Python version + | +85 | print("2") +86 | +87 | if sys.version_info.major <= 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +88 | print("3") +89 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +84 84 | else: +85 85 | print("2") +86 86 | +87 |-if sys.version_info.major <= 3: +88 |- print("3") +89 |-else: +90 |- print("2") + 87 |+print("3") +91 88 | +92 89 | if sys.version_info.major < 3: +93 90 | print("3") + +UP036_5.py:92:4: UP036 [*] Version block is outdated for minimum Python version + | +90 | print("2") +91 | +92 | if sys.version_info.major < 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +93 | print("3") +94 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +89 89 | else: +90 90 | print("2") +91 91 | +92 |-if sys.version_info.major < 3: +93 |- print("3") +94 |-else: +95 |- print("2") + 92 |+print("2") +96 93 | +97 94 | if sys.version_info.major == 3: +98 95 | print("3") + +UP036_5.py:97:4: UP036 [*] Version block is outdated for minimum Python version + | +95 | print("2") +96 | +97 | if sys.version_info.major == 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +98 | print("3") +99 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +94 94 | else: +95 95 | print("2") +96 96 | +97 |-if sys.version_info.major == 3: +98 |- print("3") +99 |-else: +100 |- print("2") + 97 |+print("3") +101 98 | +102 99 | # Semantically incorrect, skip fixing +103 100 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap index 357987139526a..bc50bc86883d8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap @@ -1,14 +1,13 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs -snapshot_kind: text --- -UP045.py:5:10: UP007 [*] Use `X | Y` for type annotations +UP045.py:5:10: UP045 [*] Use `X | None` for type annotations | 5 | def f(x: Optional[str]) -> None: - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 6 | ... | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 2 2 | from typing import Optional @@ -20,13 +19,13 @@ UP045.py:5:10: UP007 [*] Use `X | Y` for type annotations 7 7 | 8 8 | -UP045.py:9:10: UP007 [*] Use `X | Y` for type annotations +UP045.py:9:10: UP045 [*] Use `X | None` for type annotations | 9 | def f(x: typing.Optional[str]) -> None: - | ^^^^^^^^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^^^^^^^^ UP045 10 | ... | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 6 6 | ... @@ -38,14 +37,14 @@ UP045.py:9:10: UP007 [*] Use `X | Y` for type annotations 11 11 | 12 12 | -UP045.py:14:8: UP007 [*] Use `X | Y` for type annotations +UP045.py:14:8: UP045 [*] Use `X | None` for type annotations | 13 | def f() -> None: 14 | x: Optional[str] - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 15 | x = Optional[str] | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 11 11 | @@ -57,22 +56,22 @@ UP045.py:14:8: UP007 [*] Use `X | Y` for type annotations 16 16 | 17 17 | -UP045.py:15:9: UP007 Use `X | Y` for type annotations +UP045.py:15:9: UP045 Use `X | None` for type annotations | 13 | def f() -> None: 14 | x: Optional[str] 15 | x = Optional[str] - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 | - = help: Convert to `X | Y` + = help: Convert to `X | None` -UP045.py:18:15: UP007 [*] Use `X | Y` for type annotations +UP045.py:18:15: UP045 [*] Use `X | None` for type annotations | 18 | def f(x: list[Optional[int]]) -> None: - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 19 | ... | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 15 15 | x = Optional[str] @@ -84,31 +83,31 @@ UP045.py:18:15: UP007 [*] Use `X | Y` for type annotations 20 20 | 21 21 | -UP045.py:22:10: UP007 Use `X | Y` for type annotations +UP045.py:22:10: UP045 Use `X | None` for type annotations | 22 | def f(x: Optional[int : float]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^^^^^^^^^ UP045 23 | ... | - = help: Convert to `X | Y` + = help: Convert to `X | None` -UP045.py:26:10: UP007 Use `X | Y` for type annotations +UP045.py:26:10: UP045 Use `X | None` for type annotations | 26 | def f(x: Optional[str, int : float]) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP045 27 | ... | - = help: Convert to `X | Y` + = help: Convert to `X | None` -UP045.py:30:10: UP007 Use `X | Y` for type annotations +UP045.py:30:10: UP045 Use `X | None` for type annotations | 30 | def f(x: Optional[int, float]) -> None: - | ^^^^^^^^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^^^^^^^^ UP045 31 | ... | - = help: Convert to `X | Y` + = help: Convert to `X | None` -UP045.py:36:28: UP007 [*] Use `X | Y` for type annotations +UP045.py:36:28: UP045 [*] Use `X | None` for type annotations | 34 | # Regression test for: https://github.com/astral-sh/ruff/issues/7131 35 | class ServiceRefOrValue: @@ -117,9 +116,9 @@ UP045.py:36:28: UP007 [*] Use `X | Y` for type annotations 37 | | list[ServiceSpecificationRef] 38 | | | list[ServiceSpecification] 39 | | ] = None - | |_____^ UP007 + | |_____^ UP045 | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 33 33 | @@ -134,14 +133,14 @@ UP045.py:36:28: UP007 [*] Use `X | Y` for type annotations 41 38 | 42 39 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201 -UP045.py:44:28: UP007 [*] Use `X | Y` for type annotations +UP045.py:44:28: UP045 [*] Use `X | None` for type annotations | 42 | # Regression test for: https://github.com/astral-sh/ruff/issues/7201 43 | class ServiceRefOrValue: 44 | service_specification: Optional[str]is not True = None - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 41 41 | @@ -149,3 +148,15 @@ UP045.py:44:28: UP007 [*] Use `X | Y` for type annotations 43 43 | class ServiceRefOrValue: 44 |- service_specification: Optional[str]is not True = None 44 |+ service_specification: str | None is not True = None +45 45 | +46 46 | +47 47 | # Test for: https://github.com/astral-sh/ruff/issues/18508 + +UP045.py:49:6: UP045 Use `X | None` for type annotations + | +47 | # Test for: https://github.com/astral-sh/ruff/issues/18508 +48 | # Optional[None] should not be offered a fix +49 | foo: Optional[None] = None + | ^^^^^^^^^^^^^^ UP045 + | + = help: Convert to `X | None` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap new file mode 100644 index 0000000000000..1d4e1af6df855 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap @@ -0,0 +1,237 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP050.py:5:9: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +5 | class A(metaclass=type): + | ^^^^^^^^^^^^^^ UP050 +6 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +2 2 | ... +3 3 | +4 4 | +5 |-class A(metaclass=type): + 5 |+class A: +6 6 | ... +7 7 | +8 8 | + +UP050.py:10:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | + 9 | class A( +10 | metaclass=type + | ^^^^^^^^^^^^^^ UP050 +11 | ): +12 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +6 6 | ... +7 7 | +8 8 | +9 |-class A( +10 |- metaclass=type +11 |-): + 9 |+class A: +12 10 | ... +13 11 | +14 12 | + +UP050.py:16:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +15 | class A( +16 | metaclass=type + | ^^^^^^^^^^^^^^ UP050 +17 | # +18 | ): + | + = help: Remove `metaclass=type` + +ℹ Unsafe fix +12 12 | ... +13 13 | +14 14 | +15 |-class A( +16 |- metaclass=type +17 |- # +18 |-): + 15 |+class A: +19 16 | ... +20 17 | +21 18 | + +UP050.py:24:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +22 | class A( +23 | # +24 | metaclass=type + | ^^^^^^^^^^^^^^ UP050 +25 | ): +26 | ... + | + = help: Remove `metaclass=type` + +ℹ Unsafe fix +19 19 | ... +20 20 | +21 21 | +22 |-class A( +23 |- # +24 |- metaclass=type +25 |-): + 22 |+class A: +26 23 | ... +27 24 | +28 25 | + +UP050.py:30:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +29 | class A( +30 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +31 | # +32 | ): + | + = help: Remove `metaclass=type` + +ℹ Unsafe fix +26 26 | ... +27 27 | +28 28 | +29 |-class A( +30 |- metaclass=type, +31 |- # +32 |-): + 29 |+class A: +33 30 | ... +34 31 | +35 32 | + +UP050.py:38:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +36 | class A( +37 | # +38 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +39 | # +40 | ): + | + = help: Remove `metaclass=type` + +ℹ Unsafe fix +33 33 | ... +34 34 | +35 35 | +36 |-class A( +37 |- # +38 |- metaclass=type, +39 |- # +40 |-): + 36 |+class A: +41 37 | ... +42 38 | +43 39 | + +UP050.py:44:12: UP050 [*] Class `B` uses `metaclass=type`, which is redundant + | +44 | class B(A, metaclass=type): + | ^^^^^^^^^^^^^^ UP050 +45 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +41 41 | ... +42 42 | +43 43 | +44 |-class B(A, metaclass=type): + 44 |+class B(A): +45 45 | ... +46 46 | +47 47 | + +UP050.py:50:5: UP050 [*] Class `B` uses `metaclass=type`, which is redundant + | +48 | class B( +49 | A, +50 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +51 | ): +52 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +47 47 | +48 48 | class B( +49 49 | A, +50 |- metaclass=type, +51 50 | ): +52 51 | ... +53 52 | + +UP050.py:58:5: UP050 [*] Class `B` uses `metaclass=type`, which is redundant + | +56 | A, +57 | # comment +58 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +59 | ): +60 | ... + | + = help: Remove `metaclass=type` + +ℹ Unsafe fix +54 54 | +55 55 | class B( +56 56 | A, +57 |- # comment +58 |- metaclass=type, +59 57 | ): +60 58 | ... +61 59 | + +UP050.py:69:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +68 | class A( +69 | metaclass=type # comment + | ^^^^^^^^^^^^^^ UP050 +70 | , +71 | ): + | + = help: Remove `metaclass=type` + +ℹ Unsafe fix +65 65 | ... +66 66 | +67 67 | +68 |-class A( +69 |- metaclass=type # comment +70 |- , +71 |-): + 68 |+class A: +72 69 | ... +73 70 | +74 71 | + +UP050.py:83:9: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +81 | import builtins +82 | +83 | class A(metaclass=builtins.type): + | ^^^^^^^^^^^^^^^^^^^^^^^ UP050 +84 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +80 80 | +81 81 | import builtins +82 82 | +83 |-class A(metaclass=builtins.type): + 83 |+class A: +84 84 | ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap index 525b392e7c156..29df4b48bb879 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_p37.snap @@ -1,14 +1,14 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- -future_annotations.py:40:4: UP007 [*] Use `X | Y` for type annotations +future_annotations.py:40:4: UP045 [*] Use `X | None` for type annotations | 40 | x: Optional[int] = None - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Unsafe fix 37 37 | return y diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap index f88a3b32fb327..163b0a8cb5606 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap @@ -1,14 +1,14 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- -future_annotations.py:40:4: UP007 [*] Use `X | Y` for type annotations +future_annotations.py:40:4: UP045 [*] Use `X | None` for type annotations | 40 | x: Optional[int] = None - | ^^^^^^^^^^^^^ UP007 + | ^^^^^^^^^^^^^ UP045 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | - = help: Convert to `X | Y` + = help: Convert to `X | None` ℹ Safe fix 37 37 | return y diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up007_preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up007_preview.snap deleted file mode 100644 index c996a51d3844d..0000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up007_preview.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs -snapshot_kind: text ---- - diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index 54ca98a586e2f..0c6749af8ce0b 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -1,11 +1,13 @@ -use ruff_diagnostics::{Applicability, Edit, Fix}; +use std::borrow::Cow; + use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, Expr}; +use ruff_python_ast::{self as ast, Expr, parenthesize::parenthesized_range}; use ruff_python_codegen::Generator; use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix}; use ruff_python_ast::PythonVersion; /// Format a code snippet to call `name.method()`. @@ -15,6 +17,7 @@ pub(super) fn generate_method_call(name: Name, method: &str, generator: Generato id: name, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Construct `name.method`. let attr = ast::ExprAttribute { @@ -22,6 +25,7 @@ pub(super) fn generate_method_call(name: Name, method: &str, generator: Generato attr: ast::Identifier::new(method.to_string(), TextRange::default()), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Make it into a call `name.method()` let call = ast::ExprCall { @@ -30,13 +34,16 @@ pub(super) fn generate_method_call(name: Name, method: &str, generator: Generato args: Box::from([]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // And finally, turn it into a statement. let stmt = ast::StmtExpr { value: Box::new(call.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; generator.stmt(&stmt.into()) } @@ -62,6 +69,7 @@ pub(super) fn replace_with_identity_check( ops: [op].into(), comparators: [ast::ExprNoneLiteral::default().into()].into(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let new_content = generator.expr(&new_expr); @@ -300,3 +308,57 @@ fn match_open_mode(mode: &Expr) -> Option { _ => None, } } + +/// A helper function that extracts the `iter` from a [`ast::StmtFor`] node and +/// adds parentheses if needed. +/// +/// These cases are okay and will not be modified: +/// +/// - `for x in z: ...` -> `"z"` +/// - `for x in (y, z): ...` -> `"(y, z)"` +/// - `for x in [y, z]: ...` -> `"[y, z]"` +/// +/// While these cases require parentheses: +/// +/// - `for x in y, z: ...` -> `"(y, z)"` +/// - `for x in lambda: 0: ...` -> `"(lambda: 0)"` +/// - `for x in (1,) if True else (2,): ...` -> `"((1,) if True else (2,))"` +pub(super) fn parenthesize_loop_iter_if_necessary<'a>( + for_stmt: &'a ast::StmtFor, + checker: &'a Checker, + location: IterLocation, +) -> Cow<'a, str> { + let locator = checker.locator(); + let iter = for_stmt.iter.as_ref(); + + let original_parenthesized_range = parenthesized_range( + iter.into(), + for_stmt.into(), + checker.comment_ranges(), + checker.source(), + ); + + if let Some(range) = original_parenthesized_range { + return Cow::Borrowed(locator.slice(range)); + } + + let iter_in_source = locator.slice(iter); + + match iter { + ast::Expr::Tuple(tuple) if !tuple.parenthesized => { + Cow::Owned(format!("({iter_in_source})")) + } + ast::Expr::Lambda(_) | ast::Expr::If(_) + if matches!(location, IterLocation::Comprehension) => + { + Cow::Owned(format!("({iter_in_source})")) + } + _ => Cow::Borrowed(iter_in_source), + } +} + +#[derive(Copy, Clone)] +pub(super) enum IterLocation { + Call, + Comprehension, +} diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 830581d49f74a..918785314138d 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -13,7 +13,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::ReadWholeFile, Path::new("FURB101.py"))] #[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))] @@ -35,7 +35,8 @@ mod tests { #[test_case(Rule::UnnecessaryFromFloat, Path::new("FURB164.py"))] #[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))] #[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))] - #[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))] + #[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171_0.py"))] + #[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171_1.py"))] #[test_case(Rule::BitCount, Path::new("FURB161.py"))] #[test_case(Rule::IntOnSlicedStr, Path::new("FURB166.py"))] #[test_case(Rule::RegexFlagAlias, Path::new("FURB167.py"))] @@ -57,7 +58,7 @@ mod tests { Path::new("refurb").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -68,7 +69,18 @@ mod tests { &settings::LinterSettings::for_rule(Rule::WriteWholeFile) .with_target_version(PythonVersion::PY39), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn fstring_number_format_python_311() -> Result<()> { + let diagnostics = test_path( + Path::new("refurb/FURB116.py"), + &settings::LinterSettings::for_rule(Rule::FStringNumberFormat) + .with_target_version(PythonVersion::PY311), + )?; + assert_diagnostics!(diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index 863ea3cdd4c08..1dd8d9c89e8c2 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -1,11 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::PythonVersion; use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall}; +use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for uses of `bin(...).count("1")` to perform a population count. @@ -27,6 +28,11 @@ use crate::fix::snippet::SourceCodeSnippet; /// y = 0b1111011.bit_count() /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe unless the argument to `bin` can be inferred as +/// an instance of a type that implements the `__index__` and `bit_count` methods because this can +/// change the exception raised at runtime for an invalid argument. +/// /// ## Options /// - `target-version` /// @@ -131,6 +137,7 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { Expr::StringLiteral(inner) => inner.value.is_implicit_concatenated(), Expr::BytesLiteral(inner) => inner.value.is_implicit_concatenated(), Expr::FString(inner) => inner.value.is_implicit_concatenated(), + Expr::TString(inner) => inner.value.is_implicit_concatenated(), Expr::Await(_) | Expr::Starred(_) @@ -163,24 +170,29 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { | Expr::Subscript(_) => false, }; + // check if the fix is safe or not + let applicability: Applicability = match ResolvedPythonType::from(arg) { + ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer | NumberLike::Bool)) => { + Applicability::Safe + } + _ => Applicability::Unsafe, + }; + let replacement = if parenthesize { format!("({literal_text}).bit_count()") } else { format!("{literal_text}.bit_count()") }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( BitCount { existing: SourceCodeSnippet::from_str(literal_text), replacement: SourceCodeSnippet::new(replacement.to_string()), }, call.range(), ); - - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - replacement, - call.range(), - ))); - - checker.report_diagnostic(diagnostic); + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement(replacement, call.range()), + applicability, + )); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs index 446ec88a2543a..ed2494f24bfcd 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::{self as ast, CmpOp, Expr, Stmt}; @@ -9,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `set.remove` that can be replaced with `set.discard`. @@ -104,7 +104,7 @@ pub(crate) fn check_and_remove_from_set(checker: &Checker, if_stmt: &ast::StmtIf return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CheckAndRemoveFromSet { element: SourceCodeSnippet::from_str(checker.locator().slice(check_element)), set: check_set.id.to_string(), @@ -116,7 +116,6 @@ pub(crate) fn check_and_remove_from_set(checker: &Checker, if_stmt: &ast::StmtIf if_stmt.start(), if_stmt.end(), ))); - checker.report_diagnostic(diagnostic); } fn compare(lhs: &ComparableExpr, rhs: &ComparableExpr) -> bool { @@ -161,7 +160,7 @@ fn match_remove(if_stmt: &ast::StmtIf) -> Option<(&Expr, &ast::ExprName)> { .. } = attr.as_attribute_expr()?; - let Expr::Name(ref set @ ast::ExprName { .. }) = receiver.as_ref() else { + let Expr::Name(set @ ast::ExprName { .. }) = receiver.as_ref() else { return None; }; @@ -186,6 +185,7 @@ fn make_suggestion(set: &ast::ExprName, element: &Expr, generator: Generator) -> attr: ast::Identifier::new("discard".to_string(), TextRange::default()), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Make the actual call `set.discard(element)` let call = ast::ExprCall { @@ -194,13 +194,16 @@ fn make_suggestion(set: &ast::ExprName, element: &Expr, generator: Generator) -> args: Box::from([element.clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // And finally, turn it into a statement. let stmt = ast::StmtExpr { value: Box::new(call.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; generator.stmt(&stmt.into()) } diff --git a/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs b/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs index e07864fbf95b2..2fce83de338af 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/delete_full_slice.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::analyze::typing::{is_dict, is_list}; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing::{is_dict, is_list}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::refurb::helpers::generate_method_call; @@ -66,7 +66,7 @@ pub(crate) fn delete_full_slice(checker: &Checker, delete: &ast::StmtDelete) { continue; }; - let mut diagnostic = Diagnostic::new(DeleteFullSlice, delete.range); + let mut diagnostic = checker.report_diagnostic(DeleteFullSlice, delete.range); // Fix is only supported for single-target deletions. if delete.targets.len() == 1 { @@ -77,8 +77,6 @@ pub(crate) fn delete_full_slice(checker: &Checker, delete: &ast::StmtDelete) { delete.end(), ))); } - - checker.report_diagnostic(diagnostic); } } @@ -95,6 +93,7 @@ fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a upper: None, step: None, range: _, + node_index: _, }) ) { return None; diff --git a/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs b/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs index ceb0fab87bd8c..609c0dd260926 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/for_loop_set_mutations.rs @@ -1,11 +1,12 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, Stmt, StmtFor}; use ruff_python_semantic::analyze::typing; use crate::checkers::ast::Checker; +use crate::rules::refurb::helpers::IterLocation; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; -use super::helpers::parenthesize_loop_iter_if_necessary; +use crate::rules::refurb::helpers::parenthesize_loop_iter_if_necessary; /// ## What it does /// Checks for code that updates a set with the contents of an iterable by @@ -106,7 +107,7 @@ pub(crate) fn for_loop_set_mutations(checker: &Checker, for_stmt: &StmtFor) { format!( "{}.{batch_method_name}({})", set.id, - parenthesize_loop_iter_if_necessary(for_stmt, checker), + parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Call), ) } (for_target, arg) => format!( @@ -114,7 +115,7 @@ pub(crate) fn for_loop_set_mutations(checker: &Checker, for_stmt: &StmtFor) { set.id, locator.slice(arg), locator.slice(for_target), - parenthesize_loop_iter_if_necessary(for_stmt, checker), + parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Comprehension), ), }; @@ -128,13 +129,13 @@ pub(crate) fn for_loop_set_mutations(checker: &Checker, for_stmt: &StmtFor) { applicability, ); - let diagnostic = Diagnostic::new( - ForLoopSetMutations { - method_name, - batch_method_name, - }, - for_stmt.range, - ); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic( + ForLoopSetMutations { + method_name, + batch_method_name, + }, + for_stmt.range, + ) + .set_fix(fix); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs b/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs index e3b5765593df2..58dadc3e908e1 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/for_loop_writes.rs @@ -1,13 +1,14 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprList, ExprName, ExprTuple, Stmt, StmtFor}; use ruff_python_semantic::analyze::typing; use ruff_python_semantic::{Binding, ScopeId, SemanticModel, TypingOnlyBindingsStatus}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::rules::refurb::helpers::IterLocation; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; -use super::helpers::parenthesize_loop_iter_if_necessary; +use crate::rules::refurb::helpers::parenthesize_loop_iter_if_necessary; /// ## What it does /// Checks for the use of `IOBase.write` in a for loop. @@ -36,6 +37,9 @@ use super::helpers::parenthesize_loop_iter_if_necessary; /// f.writelines(line.encode() for line in lines) /// ``` /// +/// ## Fix safety +/// This fix is marked as unsafe if it would cause comments to be deleted. +/// /// ## References /// - [Python documentation: `io.IOBase.writelines`](https://docs.python.org/3/library/io.html#io.IOBase.writelines) #[derive(ViolationMetadata)] @@ -55,26 +59,34 @@ impl AlwaysFixableViolation for ForLoopWrites { } /// FURB122 -pub(crate) fn for_loop_writes_binding(checker: &Checker, binding: &Binding) -> Option { +pub(crate) fn for_loop_writes_binding(checker: &Checker, binding: &Binding) { if !binding.kind.is_loop_var() { - return None; + return; } let semantic = checker.semantic(); - let for_stmt = binding.statement(semantic)?.as_for_stmt()?; + let Some(for_stmt) = binding + .statement(semantic) + .and_then(|stmt| stmt.as_for_stmt()) + else { + return; + }; if for_stmt.is_async { - return None; + return; } let binding_names = binding_names(&for_stmt.target); - if !binding_names.first()?.range().contains_range(binding.range) { - return None; + if !binding_names + .first() + .is_some_and(|name| name.range().contains_range(binding.range)) + { + return; } - for_loop_writes(checker, for_stmt, binding.scope, &binding_names) + for_loop_writes(checker, for_stmt, binding.scope, &binding_names); } /// FURB122 @@ -86,9 +98,7 @@ pub(crate) fn for_loop_writes_stmt(checker: &Checker, for_stmt: &StmtFor) { let scope_id = checker.semantic().scope_id; - if let Some(diagnostic) = for_loop_writes(checker, for_stmt, scope_id, &[]) { - checker.report_diagnostic(diagnostic); - } + for_loop_writes(checker, for_stmt, scope_id, &[]); } /// Find the names in a `for` loop target @@ -125,40 +135,49 @@ fn for_loop_writes( for_stmt: &StmtFor, scope_id: ScopeId, binding_names: &[&ExprName], -) -> Option { +) { if !for_stmt.orelse.is_empty() { - return None; + return; } let [Stmt::Expr(stmt_expr)] = for_stmt.body.as_slice() else { - return None; + return; }; - let call_expr = stmt_expr.value.as_call_expr()?; - let expr_attr = call_expr.func.as_attribute_expr()?; + let Some(call_expr) = stmt_expr.value.as_call_expr() else { + return; + }; + let Some(expr_attr) = call_expr.func.as_attribute_expr() else { + return; + }; if &expr_attr.attr != "write" { - return None; + return; } if !call_expr.arguments.keywords.is_empty() { - return None; + return; } let [write_arg] = call_expr.arguments.args.as_ref() else { - return None; + return; }; - let io_object_name = expr_attr.value.as_name_expr()?; + let Some(io_object_name) = expr_attr.value.as_name_expr() else { + return; + }; let semantic = checker.semantic(); // Determine whether `f` in `f.write()` was bound to a file object. - let binding = semantic.binding(semantic.resolve_name(io_object_name)?); + let Some(name) = semantic.resolve_name(io_object_name) else { + return; + }; + let binding = semantic.binding(name); if !typing::is_io_base(binding, semantic) { - return None; + return; } if loop_variables_are_used_outside_loop(binding_names, for_stmt.range, semantic, scope_id) { - return None; + return; } let locator = checker.locator(); @@ -167,7 +186,7 @@ fn for_loop_writes( format!( "{}.writelines({})", locator.slice(io_object_name), - parenthesize_loop_iter_if_necessary(for_stmt, checker), + parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Call), ) } (for_target, write_arg) => { @@ -176,7 +195,7 @@ fn for_loop_writes( locator.slice(io_object_name), locator.slice(write_arg), locator.slice(for_target), - parenthesize_loop_iter_if_necessary(for_stmt, checker), + parenthesize_loop_iter_if_necessary(for_stmt, checker, IterLocation::Comprehension), ) } }; @@ -191,14 +210,14 @@ fn for_loop_writes( applicability, ); - let diagnostic = Diagnostic::new( - ForLoopWrites { - name: io_object_name.id.to_string(), - }, - for_stmt.range, - ); - - Some(diagnostic.with_fix(fix)) + checker + .report_diagnostic( + ForLoopWrites { + name: io_object_name.id.to_string(), + }, + for_stmt.range, + ) + .set_fix(fix); } fn loop_variables_are_used_outside_loop( diff --git a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs index f5b5e6e790e5b..def4bf832e068 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fromisoformat_replace_z.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{ Expr, ExprAttribute, ExprBinOp, ExprCall, ExprStringLiteral, ExprSubscript, ExprUnaryOp, @@ -9,6 +8,7 @@ use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `datetime.fromisoformat()` calls @@ -118,10 +118,11 @@ pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) { let range_to_remove = TextRange::new(value_full_range.end(), argument.end()); - let diagnostic = Diagnostic::new(FromisoformatReplaceZ, argument.range()); let fix = Fix::unsafe_edit(Edit::range_deletion(range_to_remove)); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(FromisoformatReplaceZ, argument.range()) + .set_fix(fix); } fn func_is_fromisoformat(func: &Expr, semantic: &SemanticModel) -> bool { diff --git a/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs b/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs index d71772e6ded69..c06fe722b0a8a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs @@ -1,10 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, Expr, ExprCall}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Expr, ExprCall, Number, PythonVersion, UnaryOp}; +use ruff_source_file::find_newline; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `bin(...)[2:]` (or `hex`, or `oct`) to convert @@ -24,6 +25,11 @@ use crate::fix::snippet::SourceCodeSnippet; /// ```python /// print(f"{1337:b}") /// ``` +/// +/// ## Fix safety +/// The fix is only marked as safe for integer literals, all other cases +/// are display-only, as they may change the runtime behaviour of the program +/// or introduce syntax errors. #[derive(ViolationMetadata)] pub(crate) struct FStringNumberFormat { replacement: Option, @@ -110,22 +116,36 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs return; }; - // Generate a replacement, if possible. - let replacement = if matches!( + // float and complex numbers are false positives, ignore them. + if matches!( arg, - Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_) + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Float(_) | Number::Complex { .. }, + .. + }) ) { - let inner_source = checker.locator().slice(arg); + return; + } - let quote = checker.stylist().quote(); - let shorthand = base.shorthand(); + let maybe_number = if let Some(maybe_number) = arg + .as_unary_op_expr() + .filter(|unary_expr| unary_expr.op == UnaryOp::UAdd) + .map(|unary_expr| &unary_expr.operand) + { + maybe_number + } else { + arg + }; - Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}")) + let applicability = if matches!(maybe_number, Expr::NumberLiteral(_)) { + Applicability::Safe } else { - None + Applicability::DisplayOnly }; - let mut diagnostic = Diagnostic::new( + let replacement = try_create_replacement(checker, arg, base); + + let mut diagnostic = checker.report_diagnostic( FStringNumberFormat { replacement: replacement.as_deref().map(SourceCodeSnippet::from_str), base, @@ -134,13 +154,50 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs ); if let Some(replacement) = replacement { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - replacement, - subscript.range(), - ))); + let edit = Edit::range_replacement(replacement, subscript.range()); + diagnostic.set_fix(Fix::applicable_edit(edit, applicability)); + } +} + +/// Generate a replacement, if possible. +fn try_create_replacement(checker: &Checker, arg: &Expr, base: Base) -> Option { + if !matches!( + arg, + Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_) | Expr::UnaryOp(_) + ) { + return None; } - checker.report_diagnostic(diagnostic); + let inner_source = checker.locator().slice(arg); + + // On Python 3.11 and earlier, trying to replace an `arg` that contains a backslash + // would create a `SyntaxError` in the f-string. + if checker.target_version() <= PythonVersion::PY311 && inner_source.contains('\\') { + return None; + } + + // On Python 3.11 and earlier, trying to replace an `arg` that spans multiple lines + // would create a `SyntaxError` in the f-string. + if checker.target_version() <= PythonVersion::PY311 && find_newline(inner_source).is_some() { + return None; + } + + let quote = checker.stylist().quote(); + let shorthand = base.shorthand(); + + // If the `arg` contains double quotes we need to create the f-string with single quotes + // to avoid a `SyntaxError` in Python 3.11 and earlier. + if checker.target_version() <= PythonVersion::PY311 && inner_source.contains(quote.as_str()) { + return None; + } + + // If the `arg` contains a brace add an space before it to avoid a `SyntaxError` + // in the f-string. + if inner_source.starts_with('{') { + Some(format!("f{quote}{{ {inner_source}:{shorthand}}}{quote}")) + } else { + Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}")) + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs b/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs index 0c0d0a4a422cf..d29d89244be5f 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/hardcoded_string_charset.rs @@ -1,10 +1,11 @@ -use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprStringLiteral; use ruff_text_size::TextRange; +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use crate::{AlwaysFixableViolation, Edit, Fix}; + /// ## What it does /// Checks for uses of hardcoded charsets, which are defined in Python string module. /// @@ -129,7 +130,7 @@ fn check_charset_exact(bytes: &[u8]) -> Option<&NamedCharset> { fn push_diagnostic(checker: &Checker, range: TextRange, charset: &NamedCharset) { let name = charset.name; - let mut diagnostic = Diagnostic::new(HardcodedStringCharset { name }, range); + let mut diagnostic = checker.report_diagnostic(HardcodedStringCharset { name }, range); diagnostic.try_set_fix(|| { let (edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("string", name), @@ -141,5 +142,4 @@ fn push_diagnostic(checker: &Checker, range: TextRange, charset: &NamedCharset) [edit], )) }); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs b/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs index 617173613773a..837e5ed79f548 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::Modules; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the use of `.digest().hex()` on a hashlib hash, like `sha512`. @@ -109,13 +109,12 @@ pub(crate) fn hashlib_digest_hex(checker: &Checker, call: &ExprCall) { ) }) { - let mut diagnostic = Diagnostic::new(HashlibDigestHex, call.range()); + let mut diagnostic = checker.report_diagnostic(HashlibDigestHex, call.range()); if arguments.is_empty() { diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( ".hexdigest".to_string(), TextRange::new(value.end(), call.func.end()), ))); } - checker.report_diagnostic(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/refurb/rules/helpers.rs b/crates/ruff_linter/src/rules/refurb/rules/helpers.rs deleted file mode 100644 index 8f66b92c46d78..0000000000000 --- a/crates/ruff_linter/src/rules/refurb/rules/helpers.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::borrow::Cow; - -use ruff_python_ast::{self as ast, parenthesize::parenthesized_range}; - -use crate::checkers::ast::Checker; - -/// A helper function that extracts the `iter` from a [`ast::StmtFor`] node and, -/// if the `iter` is an unparenthesized tuple, adds parentheses: -/// -/// - `for x in z: ...` -> `"x"` -/// - `for (x, y) in z: ...` -> `"(x, y)"` -/// - `for [x, y] in z: ...` -> `"[x, y]"` -/// - `for x, y in z: ...` -> `"(x, y)"` # <-- Parentheses added only for this example -pub(super) fn parenthesize_loop_iter_if_necessary<'a>( - for_stmt: &'a ast::StmtFor, - checker: &'a Checker, -) -> Cow<'a, str> { - let locator = checker.locator(); - let iter = for_stmt.iter.as_ref(); - - let original_parenthesized_range = parenthesized_range( - iter.into(), - for_stmt.into(), - checker.comment_ranges(), - checker.source(), - ); - - if let Some(range) = original_parenthesized_range { - return Cow::Borrowed(locator.slice(range)); - } - - let iter_in_source = locator.slice(iter); - - match iter { - ast::Expr::Tuple(tuple) if !tuple.parenthesized => { - Cow::Owned(format!("({iter_in_source})")) - } - _ => Cow::Borrowed(iter_in_source), - } -} diff --git a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs index 53265e57f5765..51047f15bba73 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/if_exp_instead_of_or_operator.rs @@ -1,17 +1,17 @@ use std::borrow::Cow; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; +use ruff_python_ast::Expr; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::parenthesize::parenthesized_range; -use ruff_python_ast::Expr; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for ternary `if` expressions that can be replaced with the `or` @@ -61,13 +61,14 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex body, orelse, range, + node_index: _, } = if_expr; if ComparableExpr::from(test) != ComparableExpr::from(body) { return; } - let mut diagnostic = Diagnostic::new(IfExpInsteadOfOrOperator, *range); + let mut diagnostic = checker.report_diagnostic(IfExpInsteadOfOrOperator, *range); // Replace with `{test} or {orelse}`. diagnostic.set_fix(Fix::applicable_edit( @@ -85,8 +86,6 @@ pub(crate) fn if_exp_instead_of_or_operator(checker: &Checker, if_expr: &ast::Ex Applicability::Safe }, )); - - checker.report_diagnostic(diagnostic); } /// Parenthesize an expression for use in an `or` operator (e.g., parenthesize `x` in `x or y`), diff --git a/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs index 3036feef1add1..a514b8704e0c4 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `if` expressions that can be replaced with `min()` or `max()` @@ -130,7 +130,7 @@ pub(crate) fn if_expr_min_max(checker: &Checker, if_exp: &ast::ExprIf) { checker.generator().expr(arg2), ); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( IfExprMinMax { min_max, expression: SourceCodeSnippet::from_str(checker.locator().slice(if_exp)), @@ -145,8 +145,6 @@ pub(crate) fn if_expr_min_max(checker: &Checker, if_exp: &ast::ExprIf) { if_exp.range(), ))); } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs b/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs index a3da7c6513c21..c9c6891ee7aca 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall}; use ruff_text_size::Ranged; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::{checkers::ast::Checker, importer::ImportRequest}; /// ## What it does @@ -24,7 +24,6 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// /// ## References /// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd) - #[derive(ViolationMetadata)] pub(crate) struct ImplicitCwd; @@ -88,7 +87,7 @@ pub(crate) fn no_implicit_cwd(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new(ImplicitCwd, call.range()); + let mut diagnostic = checker.report_diagnostic(ImplicitCwd, call.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( @@ -101,6 +100,4 @@ pub(crate) fn no_implicit_cwd(checker: &Checker, call: &ExprCall) { [import_edit], )) }); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs b/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs index b2da47d6be6cd..588569f39723c 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprCall, Identifier}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `int` with an explicit base in which a string expression @@ -64,6 +64,7 @@ impl AlwaysFixableViolation for IntOnSlicedStr { } } +/// FURB166 pub(crate) fn int_on_sliced_str(checker: &Checker, call: &ExprCall) { // Verify that the function is `int`. if !checker.semantic().match_builtin_expr(&call.func, "int") { @@ -116,7 +117,7 @@ pub(crate) fn int_on_sliced_str(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new(IntOnSlicedStr { base: base_u8 }, call.range()); + let mut diagnostic = checker.report_diagnostic(IntOnSlicedStr { base: base_u8 }, call.range()); diagnostic.set_fix(Fix::unsafe_edits( Edit::range_replacement( checker.locator().slice(&*expr_subscript.value).to_string(), @@ -124,5 +125,4 @@ pub(crate) fn int_on_sliced_str(checker: &Checker, call: &ExprCall) { ), [Edit::range_replacement("0".to_string(), base.range())], )); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs b/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs index d051a8704d119..4ff1f1c4af877 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::rules::refurb::helpers::replace_with_identity_check; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for uses of `isinstance` that check if an object is of type `None`. @@ -69,9 +69,9 @@ pub(crate) fn isinstance_type_none(checker: &Checker, call: &ast::ExprCall) { } let fix = replace_with_identity_check(expr, call.range, false, checker); - let diagnostic = Diagnostic::new(IsinstanceTypeNone, call.range); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(IsinstanceTypeNone, call.range) + .set_fix(fix); } /// Returns `true` if the given expression is equivalent to checking if the @@ -100,7 +100,9 @@ fn is_none(expr: &Expr, semantic: &SemanticModel) -> bool { } // Ex) `(type(None),)` - Expr::Tuple(tuple) => tuple.iter().all(|element| inner(element, false, semantic)), + Expr::Tuple(tuple) => { + !tuple.is_empty() && tuple.iter().all(|element| inner(element, false, semantic)) + } // Ex) `type(None) | type(None)` Expr::BinOp(ast::ExprBinOp { @@ -125,7 +127,8 @@ fn is_none(expr: &Expr, semantic: &SemanticModel) -> bool { match slice.as_ref() { Expr::Tuple(ast::ExprTuple { elts, .. }) => { - elts.iter().all(|element| inner(element, true, semantic)) + !elts.is_empty() + && elts.iter().all(|element| inner(element, true, semantic)) } slice => inner(slice, true, semantic), } diff --git a/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs b/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs index b6dcaec72dfdf..bf8e6c6207f97 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/list_reverse_copy.rs @@ -1,13 +1,13 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ Expr, ExprCall, ExprName, ExprSlice, ExprSubscript, ExprUnaryOp, Int, StmtAssign, UnaryOp, }; -use ruff_python_semantic::analyze::typing; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for list reversals that can be performed in-place in lieu of @@ -89,18 +89,17 @@ pub(crate) fn list_assign_reversed(checker: &Checker, assign: &StmtAssign) { return; } - checker.report_diagnostic( - Diagnostic::new( + checker + .report_diagnostic( ListReverseCopy { name: target_expr.id.to_string(), }, assign.range(), ) - .with_fix(Fix::unsafe_edit(Edit::range_replacement( + .set_fix(Fix::unsafe_edit(Edit::range_replacement( format!("{}.reverse()", target_expr.id), assign.range(), - ))), - ); + ))); } /// Recursively removes any `list` wrappers from the expression. diff --git a/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs b/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs index af468f75368ba..c2db8c2c7ece6 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs @@ -1,12 +1,12 @@ use anyhow::Result; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Number}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for literals that are similar to constants in `math` module. @@ -55,7 +55,7 @@ pub(crate) fn math_constant(checker: &Checker, literal: &ast::ExprNumberLiteral) }; if let Some(constant) = Constant::from_value(value) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( MathConstant { literal: checker.locator().slice(literal).into(), constant: constant.name(), @@ -63,7 +63,6 @@ pub(crate) fn math_constant(checker: &Checker, literal: &ast::ExprNumberLiteral) literal.range(), ); diagnostic.try_set_fix(|| convert_to_constant(literal, constant.name(), checker)); - checker.report_diagnostic(diagnostic); } } @@ -105,7 +104,7 @@ enum Constant { } impl Constant { - #[allow(clippy::approx_constant)] + #[expect(clippy::approx_constant)] fn from_value(value: f64) -> Option { if (3.14..3.15).contains(&value) { matches_constant(std::f64::consts::PI, value).then_some(Self::Pi) diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index 9fff62560c938..96016b03514ec 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -1,12 +1,13 @@ use itertools::Itertools; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::StmtClassDef; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `metaclass=abc.ABCMeta` to define abstract base classes @@ -31,6 +32,11 @@ use crate::importer::ImportRequest; /// pass /// ``` /// +/// ## Fix safety +/// The rule's fix is unsafe if the class has base classes. This is because the base classes might +/// be validating the class's other base classes (e.g., `typing.Protocol` does this) or otherwise +/// alter runtime behavior if more base classes are added. +/// /// ## References /// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) /// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta) @@ -69,7 +75,12 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { return; } - let mut diagnostic = Diagnostic::new(MetaClassABCMeta, keyword.range); + let applicability = if class_def.bases().is_empty() { + Applicability::Safe + } else { + Applicability::Unsafe + }; + let mut diagnostic = checker.report_diagnostic(MetaClassABCMeta, keyword.range); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( @@ -80,7 +91,7 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { Ok(if position > 0 { // When the `abc.ABCMeta` is not the first keyword, put `abc.ABC` before the first // keyword. - Fix::safe_edits( + Fix::applicable_edits( // Delete from the previous argument, to the end of the `metaclass` argument. Edit::range_deletion(TextRange::new( class_def.keywords()[position - 1].end(), @@ -91,14 +102,14 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) { Edit::insertion(format!("{binding}, "), class_def.keywords()[0].start()), import_edit, ], + applicability, ) } else { - Fix::safe_edits( + Fix::applicable_edits( Edit::range_replacement(binding, keyword.range), [import_edit], + applicability, ) }) }); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/mod.rs b/crates/ruff_linter/src/rules/refurb/rules/mod.rs index a0c573dc6c8b9..88e58fd583d17 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/mod.rs @@ -44,7 +44,6 @@ mod fromisoformat_replace_z; mod fstring_number_format; mod hardcoded_string_charset; mod hashlib_digest_hex; -mod helpers; mod if_exp_instead_of_or_operator; mod if_expr_min_max; mod implicit_cwd; diff --git a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs index 31ec1632ec7b9..b0b17456ab0e8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::{self as ast, Expr}; use ruff_python_codegen::Generator; @@ -7,6 +6,7 @@ use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `print` calls with unnecessary empty strings as positional @@ -84,7 +84,8 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { Reason::EmptyArgument }; - let mut diagnostic = Diagnostic::new(PrintEmptyString { reason }, call.range()); + let mut diagnostic = + checker.report_diagnostic(PrintEmptyString { reason }, call.range()); diagnostic.set_fix( EmptyStringFix::from_call( @@ -95,8 +96,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { ) .into_fix(), ); - - checker.report_diagnostic(diagnostic); } [arg] if arg.is_starred_expr() => { @@ -107,7 +106,7 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { [] | [_] => { // If there's a `sep` argument, remove it, regardless of what it is. if call.arguments.find_keyword("sep").is_some() { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PrintEmptyString { reason: Reason::UselessSeparator, }, @@ -123,8 +122,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { ) .into_fix(), ); - - checker.report_diagnostic(diagnostic); } } @@ -170,7 +167,7 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { Separator::Remove }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( PrintEmptyString { reason: if separator == Separator::Retain { Reason::EmptyArgument @@ -185,8 +182,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call(call, separator, checker.semantic(), checker.generator()) .into_fix(), ); - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs index ee28e60391e80..cb18f0ff90a1b 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::{self, Visitor}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_codegen::Generator; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; -use super::super::helpers::{find_file_opens, FileOpen}; +use crate::rules::refurb::helpers::{FileOpen, find_file_opens}; /// ## What it does /// Checks for uses of `open` and `read` that can be replaced by `pathlib` @@ -64,49 +64,26 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) { } // Then we need to match each `open` operation with exactly one `read` call. - let matches = { - let mut matcher = ReadMatcher::new(candidates); - visitor::walk_body(&mut matcher, &with.body); - matcher.into_matches() - }; - - // All the matched operations should be reported. - let diagnostics: Vec = matches - .iter() - .map(|open| { - Diagnostic::new( - ReadWholeFile { - filename: SourceCodeSnippet::from_str(&checker.generator().expr(open.filename)), - suggestion: make_suggestion(open, checker.generator()), - }, - open.item.range(), - ) - }) - .collect(); - checker.report_diagnostics(diagnostics); + let mut matcher = ReadMatcher::new(checker, candidates); + visitor::walk_body(&mut matcher, &with.body); } /// AST visitor that matches `open` operations with the corresponding `read` calls. -#[derive(Debug)] -struct ReadMatcher<'a> { +struct ReadMatcher<'a, 'b> { + checker: &'a Checker<'b>, candidates: Vec>, - matches: Vec>, } -impl<'a> ReadMatcher<'a> { - fn new(candidates: Vec>) -> Self { +impl<'a, 'b> ReadMatcher<'a, 'b> { + fn new(checker: &'a Checker<'b>, candidates: Vec>) -> Self { Self { + checker, candidates, - matches: vec![], } } - - fn into_matches(self) -> Vec> { - self.matches - } } -impl<'a> Visitor<'a> for ReadMatcher<'a> { +impl<'a> Visitor<'a> for ReadMatcher<'a, '_> { fn visit_expr(&mut self, expr: &'a Expr) { if let Some(read_from) = match_read_call(expr) { if let Some(open) = self @@ -114,7 +91,16 @@ impl<'a> Visitor<'a> for ReadMatcher<'a> { .iter() .position(|open| open.is_ref(read_from)) { - self.matches.push(self.candidates.remove(open)); + let open = self.candidates.remove(open); + self.checker.report_diagnostic( + ReadWholeFile { + filename: SourceCodeSnippet::from_str( + &self.checker.generator().expr(open.filename), + ), + suggestion: make_suggestion(&open, self.checker.generator()), + }, + open.item.range(), + ); } return; } @@ -144,6 +130,7 @@ fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> SourceCodeSnipp id: open.mode.pathlib_method(), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let call = ast::ExprCall { func: Box::new(name.into()), @@ -151,8 +138,10 @@ fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> SourceCodeSnipp args: Box::from([]), keywords: open.keywords.iter().copied().cloned().collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; SourceCodeSnippet::from_str(&generator.expr(&call.into())) } diff --git a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs index e06626da9c438..6bdbf94184d5d 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs @@ -1,11 +1,14 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{Comprehension, Expr, StmtFor}; use ruff_python_semantic::analyze::typing; use ruff_python_semantic::analyze::typing::is_io_base_expr; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::fix::edits::pad_end; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `readlines()` when iterating over a file line-by-line. @@ -29,6 +32,19 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there's comments in the +/// `readlines()` call, as comments may be removed. +/// +/// For example, the fix would be marked as unsafe in the following case: +/// ```python +/// with open("file.txt") as fp: +/// for line in ( # comment +/// fp.readlines() # comment +/// ): +/// ... +/// ``` +/// /// ## References /// - [Python documentation: `io.IOBase.readlines`](https://docs.python.org/3/library/io.html#io.IOBase.readlines) #[derive(ViolationMetadata)] @@ -84,9 +100,31 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) { } } - let mut diagnostic = Diagnostic::new(ReadlinesInFor, expr_call.range()); - diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion( - expr_call.range().add_start(expr_attr.value.range().len()), - ))); - checker.report_diagnostic(diagnostic); + let deletion_range = if let Some(parenthesized_range) = parenthesized_range( + expr_attr.value.as_ref().into(), + expr_attr.into(), + checker.comment_ranges(), + checker.source(), + ) { + expr_call.range().add_start(parenthesized_range.len()) + } else { + expr_call.range().add_start(expr_attr.value.range().len()) + }; + + let padded = pad_end(String::new(), deletion_range.end(), checker.locator()); + let edit = if padded.is_empty() { + Edit::range_deletion(deletion_range) + } else { + Edit::range_replacement(padded, deletion_range) + }; + + let mut diagnostic = checker.report_diagnostic(ReadlinesInFor, expr_call.range()); + diagnostic.set_fix(Fix::applicable_edit( + edit, + if checker.comment_ranges().intersects(iter_expr.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs index a506aa17b834c..dc8081cc639bc 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs @@ -1,11 +1,13 @@ use anyhow::Result; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::Applicability; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Expr, Number}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `math.log` calls with a redundant base. @@ -36,6 +38,13 @@ use crate::importer::ImportRequest; /// math.log10(4) /// ``` /// +/// ## Fix safety +/// This fix is marked unsafe when the argument is a starred expression, as this changes +/// the call semantics and may raise runtime errors. It is also unsafe if comments are +/// present within the call, as they will be removed. Additionally, `math.log(x, base)` +/// and `math.log2(x)` / `math.log10(x)` may differ due to floating-point rounding, so +/// the fix is also unsafe when making this transformation. +/// /// ## References /// - [Python documentation: `math.log`](https://docs.python.org/3/library/math.html#math.log) /// - [Python documentation: `math.log2`](https://docs.python.org/3/library/math.html#math.log2) @@ -96,7 +105,7 @@ pub(crate) fn redundant_log_base(checker: &Checker, call: &ast::ExprCall) { return; }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( RedundantLogBase { base, arg: checker.locator().slice(arg).into(), @@ -104,7 +113,6 @@ pub(crate) fn redundant_log_base(checker: &Checker, call: &ast::ExprCall) { call.range(), ); diagnostic.try_set_fix(|| generate_fix(checker, call, base, arg)); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -129,7 +137,7 @@ fn is_number_literal(expr: &Expr, value: i8) -> bool { if let Number::Int(number) = &number_literal.value { return number.as_i8().is_some_and(|number| number == value); } else if let Number::Float(number) = number_literal.value { - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] return number == f64::from(value); } } @@ -142,9 +150,26 @@ fn generate_fix(checker: &Checker, call: &ast::ExprCall, base: Base, arg: &Expr) call.start(), checker.semantic(), )?; - let number = checker.locator().slice(arg); - Ok(Fix::safe_edits( - Edit::range_replacement(format!("{binding}({number})"), call.range()), + + let arg_range = parenthesized_range( + arg.into(), + call.into(), + checker.comment_ranges(), + checker.source(), + ) + .unwrap_or(arg.range()); + let arg_str = checker.locator().slice(arg_range); + + Ok(Fix::applicable_edits( + Edit::range_replacement(format!("{binding}({arg_str})"), call.range()), [edit], + if (matches!(base, Base::Two | Base::Ten)) + || arg.is_starred_expr() + || checker.comment_ranges().intersects(call.range()) + { + Applicability::Unsafe + } else { + Applicability::Safe + }, )) } diff --git a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs index d7e0fee51e8cf..3ea5561cd8b62 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for the use of shorthand aliases for regular expression flags @@ -31,7 +31,6 @@ use crate::importer::ImportRequest; /// if re.match("^hello", "hello world", re.IGNORECASE): /// ... /// ``` -/// #[derive(ViolationMetadata)] pub(crate) struct RegexFlagAlias { flag: RegexFlag, @@ -74,7 +73,7 @@ pub(crate) fn regex_flag_alias(checker: &Checker, expr: &Expr) { return; }; - let mut diagnostic = Diagnostic::new(RegexFlagAlias { flag }, expr.range()); + let mut diagnostic = checker.report_diagnostic(RegexFlagAlias { flag }, expr.range()); diagnostic.try_set_fix(|| { let (edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("re", flag.full_name()), @@ -86,7 +85,6 @@ pub(crate) fn regex_flag_alias(checker: &Checker, expr: &Expr) { [edit], )) }); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs index d7d327a561185..29d64e7218607 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_operator.rs @@ -4,17 +4,17 @@ use std::fmt::{Debug, Display, Formatter}; use anyhow::Result; use itertools::Itertools; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, Expr, ExprSlice, ExprSubscript, ExprTuple, Parameters, Stmt}; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::importer::{ImportRequest, Importer}; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for lambda expressions and function definitions that can be replaced with a function from @@ -112,7 +112,7 @@ pub(crate) fn reimplemented_operator(checker: &Checker, target: &FunctionLike) { return; }; let fix = target.try_fix(&operator, checker.importer(), checker.semantic()); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( ReimplementedOperator { operator, target: target.kind(), @@ -120,7 +120,6 @@ pub(crate) fn reimplemented_operator(checker: &Checker, target: &FunctionLike) { target.range(), ); diagnostic.try_set_optional_fix(|| fix); - checker.report_diagnostic(diagnostic); } /// Candidate for lambda expression or function definition consisting of a return statement. diff --git a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs index 1d4cee5154c92..a45d768f5346e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs @@ -1,6 +1,5 @@ -use anyhow::{bail, Result}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use anyhow::{Result, bail}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::name::Name; @@ -9,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for generator expressions, list and set comprehensions that can @@ -133,7 +133,7 @@ pub(crate) fn reimplemented_starmap(checker: &Checker, target: &StarmapCandidate } } - let mut diagnostic = Diagnostic::new(ReimplementedStarmap, target.range()); + let mut diagnostic = checker.report_diagnostic(ReimplementedStarmap, target.range()); diagnostic.try_set_fix(|| { // Import `starmap` from `itertools`. let (import_edit, starmap_name) = checker.importer().get_or_import_symbol( @@ -156,7 +156,6 @@ pub(crate) fn reimplemented_starmap(checker: &Checker, target: &StarmapCandidate ); Ok(Fix::safe_edits(import_edit, [main_edit])) }); - checker.report_diagnostic(diagnostic); } /// An enum for a node that can be considered a candidate for replacement with `starmap`. @@ -299,6 +298,7 @@ fn construct_starmap_call(starmap_binding: Name, iter: &Expr, func: &Expr) -> as id: starmap_binding, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; ast::ExprCall { func: Box::new(starmap.into()), @@ -306,8 +306,10 @@ fn construct_starmap_call(starmap_binding: Name, iter: &Expr, func: &Expr) -> as args: Box::from([func.clone(), iter.clone()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } } @@ -317,6 +319,7 @@ fn wrap_with_call_to(call: ast::ExprCall, func_name: Name) -> ast::ExprCall { id: func_name, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; ast::ExprCall { func: Box::new(name.into()), @@ -324,8 +327,10 @@ fn wrap_with_call_to(call: ast::ExprCall, func_name: Name) -> ast::ExprCall { args: Box::from([call.into()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } } diff --git a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs index 6c82f94e5b34a..d3c8259ec9d96 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs @@ -1,8 +1,7 @@ use rustc_hash::FxHashMap; use ast::traversal; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::traversal::EnclosingSuite; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_codegen::Generator; @@ -12,6 +11,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for consecutive calls to `append`. @@ -86,48 +86,40 @@ pub(crate) fn repeated_append(checker: &Checker, stmt: &Stmt) { return; } - // group borrows from checker, so we can't directly push into checker.diagnostics - let diagnostics: Vec = group_appends(appends) - .iter() - .filter_map(|group| { - // Groups with just one element are fine, and shouldn't be replaced by `extend`. - if group.appends.len() <= 1 { - return None; - } - - let replacement = make_suggestion(group, checker.generator()); - - let mut diagnostic = Diagnostic::new( - RepeatedAppend { - name: group.name().to_string(), - replacement: SourceCodeSnippet::new(replacement.clone()), - }, - group.range(), - ); - - // We only suggest a fix when all appends in a group are clumped together. If they're - // non-consecutive, fixing them is much more difficult. - // - // Avoid fixing if there are comments in between the appends: - // - // ```python - // a.append(1) - // # comment - // a.append(2) - // ``` - if group.is_consecutive && !checker.comment_ranges().intersects(group.range()) { - diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement( - replacement, - group.start(), - group.end(), - ))); - } - - Some(diagnostic) - }) - .collect(); + for group in group_appends(appends) { + // Groups with just one element are fine, and shouldn't be replaced by `extend`. + if group.appends.len() <= 1 { + continue; + } - checker.report_diagnostics(diagnostics); + let replacement = make_suggestion(&group, checker.generator()); + + let mut diagnostic = checker.report_diagnostic( + RepeatedAppend { + name: group.name().to_string(), + replacement: SourceCodeSnippet::new(replacement.clone()), + }, + group.range(), + ); + + // We only suggest a fix when all appends in a group are clumped together. If they're + // non-consecutive, fixing them is much more difficult. + // + // Avoid fixing if there are comments in between the appends: + // + // ```python + // a.append(1) + // # comment + // a.append(2) + // ``` + if group.is_consecutive && !checker.comment_ranges().intersects(group.range()) { + diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement( + replacement, + group.start(), + group.end(), + ))); + } + } } #[derive(Debug, Clone)] @@ -332,9 +324,11 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String { assert!(!appends.is_empty()); let first = appends.first().unwrap(); - assert!(appends - .iter() - .all(|append| append.binding.source == first.binding.source)); + assert!( + appends + .iter() + .all(|append| append.binding.source == first.binding.source) + ); // Here we construct `var.extend((elt1, elt2, ..., eltN)) // @@ -348,6 +342,7 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String { elts, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, }; // Make `var.extend`. @@ -357,6 +352,7 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String { attr: ast::Identifier::new("extend".to_string(), TextRange::default()), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Make the actual call `var.extend((elt1, elt2, ..., eltN))` let call = ast::ExprCall { @@ -365,13 +361,16 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String { args: Box::from([tuple.into()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // And finally, turn it into a statement. let stmt = ast::StmtExpr { value: Box::new(call.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; generator.stmt(&stmt.into()) } diff --git a/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs b/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs index 7fa845806a4b5..685f7124cc339 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/repeated_global.rs @@ -1,11 +1,11 @@ use itertools::Itertools; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Stmt; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for consecutive `global` (or `nonlocal`) statements. @@ -74,25 +74,23 @@ pub(crate) fn repeated_global(checker: &Checker, mut suite: &[Stmt]) { // diagnostic. if let [first, .., last] = globals_sequence { let range = first.range().cover(last.range()); - checker.report_diagnostic( - Diagnostic::new(RepeatedGlobal { global_kind }, range).with_fix(Fix::safe_edit( - Edit::range_replacement( - format!( - "{global_kind} {}", - globals_sequence - .iter() - .flat_map(|stmt| match stmt { - Stmt::Global(stmt) => &stmt.names, - Stmt::Nonlocal(stmt) => &stmt.names, - _ => unreachable!(), - }) - .map(ruff_python_ast::Identifier::id) - .format(", ") - ), - range, + checker + .report_diagnostic(RepeatedGlobal { global_kind }, range) + .set_fix(Fix::safe_edit(Edit::range_replacement( + format!( + "{global_kind} {}", + globals_sequence + .iter() + .flat_map(|stmt| match stmt { + Stmt::Global(stmt) => &stmt.names, + Stmt::Nonlocal(stmt) => &stmt.names, + _ => unreachable!(), + }) + .map(ruff_python_ast::Identifier::id) + .format(", ") ), - )), - ); + range, + ))); } suite = next_suite; diff --git a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs index d77e2b4f81e64..e581bc7a1269d 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs @@ -1,11 +1,12 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::generate_comparison; use ruff_python_ast::{self as ast, CmpOp, Expr, ExprStringLiteral}; +use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::edits::pad; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for membership tests against single-item containers. @@ -78,13 +79,11 @@ pub(crate) fn single_item_membership_test( _ => return, }; - // Check if the right-hand side is a single-item object. - let Some(item) = single_item(right) else { + // Check if the right-hand side is a single-item object + let Some(item) = single_item(right, checker.semantic()) else { return; }; - let diagnostic = Diagnostic::new(SingleItemMembershipTest { membership_test }, expr.range()); - let edit = Edit::range_replacement( pad( generate_comparison( @@ -110,12 +109,14 @@ pub(crate) fn single_item_membership_test( let fix = Fix::applicable_edit(edit, applicability); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(SingleItemMembershipTest { membership_test }, expr.range()) + .set_fix(fix); } /// Return the single item wrapped in `Some` if the expression contains a single /// item, otherwise return `None`. -fn single_item(expr: &Expr) -> Option<&Expr> { +fn single_item<'a>(expr: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Expr> { match expr { Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) @@ -124,6 +125,20 @@ fn single_item(expr: &Expr) -> Option<&Expr> { [item] => Some(item), _ => None, }, + Expr::Call(ast::ExprCall { + func, + arguments, + range: _, + node_index: _, + }) => { + if arguments.len() != 1 || !is_set_method(func, semantic) { + return None; + } + + arguments + .find_positional(0) + .and_then(|arg| single_item(arg, semantic)) + } string_expr @ Expr::StringLiteral(ExprStringLiteral { value: string, .. }) if string.chars().count() == 1 => { @@ -133,6 +148,12 @@ fn single_item(expr: &Expr) -> Option<&Expr> { } } +fn is_set_method(func: &Expr, semantic: &SemanticModel) -> bool { + ["set", "frozenset"] + .iter() + .any(|s| semantic.match_builtin_expr(func, s)) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MembershipTest { /// Ex) `1 in [1]` diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs index b35e58ed84aed..91b7903889833 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_copy.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::analyze::typing::is_list; @@ -7,6 +6,7 @@ use ruff_python_semantic::{Binding, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::rules::refurb::helpers::generate_method_call; @@ -61,14 +61,13 @@ pub(crate) fn slice_copy(checker: &Checker, subscript: &ast::ExprSubscript) { let Some(name) = match_list_full_slice(subscript, checker.semantic()) else { return; }; - let mut diagnostic = Diagnostic::new(SliceCopy, subscript.range()); + let mut diagnostic = checker.report_diagnostic(SliceCopy, subscript.range()); let replacement = generate_method_call(name.clone(), "copy", checker.generator()); diagnostic.set_fix(Fix::safe_edit(Edit::replacement( replacement, subscript.start(), subscript.end(), ))); - checker.report_diagnostic(diagnostic); } /// Matches `obj[:]` where `obj` is a list. @@ -84,6 +83,7 @@ fn match_list_full_slice<'a>( upper: None, step: None, range: _, + node_index: _, }) ) { return None; diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs index 14de99b09be6c..427af68ccf93c 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for code that could be written more idiomatically using @@ -78,7 +78,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI let kind = removal_data.affix_query.kind; let text = removal_data.text; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( SliceToRemovePrefixOrSuffix { affix_kind: kind, stmt_or_expression: StmtOrExpr::Expression, @@ -93,7 +93,6 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI if_expr.start(), if_expr.end(), ))); - checker.report_diagnostic(diagnostic); } } } @@ -108,7 +107,7 @@ pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtI let kind = removal_data.affix_query.kind; let text = removal_data.text; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( SliceToRemovePrefixOrSuffix { affix_kind: kind, stmt_or_expression: StmtOrExpr::Statement, @@ -127,7 +126,6 @@ pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtI if_stmt.start(), if_stmt.end(), ))); - checker.report_diagnostic(diagnostic); } } } @@ -147,6 +145,7 @@ fn affix_removal_data_expr(if_expr: &ast::ExprIf) -> Option { body, orelse, range: _, + node_index: _, } = if_expr; let ast::ExprSubscript { value, slice, .. } = body.as_subscript_expr()?; @@ -173,6 +172,7 @@ fn affix_removal_data_stmt(if_stmt: &ast::StmtIf) -> Option { body, elif_else_clauses, range: _, + node_index: _, } = if_stmt; // Cannot safely transform, e.g., @@ -205,6 +205,7 @@ fn affix_removal_data_stmt(if_stmt: &ast::StmtIf) -> Option { value, targets, range: _, + node_index: _, } = statement.as_assign_stmt()?; let [target] = targets.as_slice() else { return None; @@ -327,9 +328,11 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value: num, range: _, + node_index: _, }), ast::Expr::StringLiteral(ast::ExprStringLiteral { range: _, + node_index: _, value: string_val, }), ) => num @@ -341,6 +344,7 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - AffixKind::StartsWith, ast::Expr::Call(ast::ExprCall { range: _, + node_index: _, func, arguments, }), @@ -360,9 +364,11 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - op: ast::UnaryOp::USub, operand, range: _, + node_index: _, }), ast::Expr::StringLiteral(ast::ExprStringLiteral { range: _, + node_index: _, value: string_val, }), ) if operand.is_number_literal_expr() => operand.as_number_literal_expr().is_some_and( @@ -380,11 +386,13 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - op: ast::UnaryOp::USub, operand, range: _, + node_index: _, }), _, ) => operand.as_call_expr().is_some_and( |ast::ExprCall { range: _, + node_index: _, func, arguments, }| { diff --git a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs index 85df541faa684..41965dc04f1f5 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs @@ -1,13 +1,12 @@ -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::Edit; -use ruff_diagnostics::Fix; -use ruff_diagnostics::FixAvailability; -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Number; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::Edit; +use crate::Fix; +use crate::FixAvailability; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -178,7 +177,7 @@ pub(crate) fn sorted_min_max(checker: &Checker, subscript: &ast::ExprSubscript) (Index::Last, true) => MinMax::Min, }; - let mut diagnostic = Diagnostic::new(SortedMinMax { min_max }, subscript.range()); + let mut diagnostic = checker.report_diagnostic(SortedMinMax { min_max }, subscript.range()); if checker.semantic().has_builtin_binding(min_max.as_str()) { diagnostic.set_fix({ @@ -200,8 +199,6 @@ pub(crate) fn sorted_min_max(checker: &Checker, subscript: &ast::ExprSubscript) } }); } - - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs index fa8805e5a2600..7d92379b36d07 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/subclass_builtin.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{helpers::map_subscript, Arguments, StmtClassDef}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Arguments, StmtClassDef, helpers::map_subscript}; use ruff_text_size::Ranged; +use crate::{AlwaysFixableViolation, Edit, Fix}; use crate::{checkers::ast::Checker, importer::ImportRequest}; /// ## What it does @@ -105,7 +105,7 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) { let user_symbol = supported_builtin.user_symbol(); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( SubclassBuiltin { subclass: symbol.to_string(), replacement: user_symbol.to_string(), @@ -121,7 +121,6 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) { let other_edit = Edit::range_replacement(binding, base_expr.range()); Ok(Fix::unsafe_edits(import_edit, [other_edit])) }); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs b/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs index c9a539918e01d..b498a5ee491b9 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/type_none_comparison.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_python_semantic::SemanticModel; +use crate::AlwaysFixableViolation; use crate::checkers::ast::Checker; use crate::rules::refurb::helpers::replace_with_identity_check; @@ -75,12 +75,12 @@ pub(crate) fn type_none_comparison(checker: &Checker, compare: &ast::ExprCompare _ => return, }; - let diagnostic = Diagnostic::new(TypeNoneComparison { replacement }, compare.range); - let negate = replacement == IdentityCheck::IsNot; let fix = replace_with_identity_check(other_arg, compare.range, negate, checker); - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(TypeNoneComparison { replacement }, compare.range) + .set_fix(fix); } /// Returns the object passed to the function, if the expression is a call to diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs index 4dceca05bcf47..db8e3fb1c3bc0 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs @@ -1,7 +1,6 @@ use std::fmt; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ruff_python_ast::{Arguments, Expr, Int}; @@ -12,6 +11,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::pad; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `enumerate` that discard either the index or the value @@ -124,7 +124,7 @@ pub(crate) fn unnecessary_enumerate(checker: &Checker, stmt_for: &ast::StmtFor) // Both the index and the value are used. } (true, false) => { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEnumerate { subset: EnumerateSubset::Values, }, @@ -143,8 +143,6 @@ pub(crate) fn unnecessary_enumerate(checker: &Checker, stmt_for: &ast::StmtFor) stmt_for.target.range(), ); diagnostic.set_fix(Fix::unsafe_edits(replace_iter, [replace_target])); - - checker.report_diagnostic(diagnostic); } (false, true) => { // Ensure the sequence object works with `len`. If it doesn't, the @@ -167,7 +165,7 @@ pub(crate) fn unnecessary_enumerate(checker: &Checker, stmt_for: &ast::StmtFor) } // The value is unused, so replace with `for index in range(len(sequence))`. - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEnumerate { subset: EnumerateSubset::Indices, }, @@ -205,7 +203,6 @@ pub(crate) fn unnecessary_enumerate(checker: &Checker, stmt_for: &ast::StmtFor) diagnostic.set_fix(Fix::unsafe_edits(replace_iter, [replace_target])); } } - checker.report_diagnostic(diagnostic); } } } @@ -235,6 +232,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { id: name, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Construct `len(name)`. let len = ast::ExprCall { @@ -243,6 +241,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { id: Name::new_static("len"), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), @@ -250,8 +249,10 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { args: Box::from([var.into()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Construct `range(len(name))`. let range = ast::ExprCall { @@ -260,6 +261,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { id: Name::new_static("range"), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), ), @@ -267,13 +269,16 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { args: Box::from([len.into()]), keywords: Box::from([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // And finally, turn it into a statement. let stmt = ast::StmtExpr { value: Box::new(range.into()), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; generator.stmt(&stmt.into()) } diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs index 02f0de2c32fd5..897a8bbc72103 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprCall}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::linter::float::as_non_finite_float_string_literal; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary `from_float` and `from_decimal` usages to construct @@ -92,7 +93,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryFromFloat { method_name, constructor, @@ -137,13 +138,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { let [float] = arguments.args.as_ref() else { break 'short_circuit; }; - let Some(float) = float.as_string_literal_expr() else { - break 'short_circuit; - }; - if !matches!( - float.value.to_str().to_lowercase().as_str(), - "inf" | "-inf" | "infinity" | "-infinity" | "nan" - ) { + if as_non_finite_float_string_literal(float).is_none() { break 'short_circuit; } @@ -157,13 +152,11 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { edit, [Edit::range_replacement(replacement, call.range())], )); - checker.report_diagnostic(diagnostic); return; } diagnostic.set_fix(Fix::safe_edit(edit)); - checker.report_diagnostic(diagnostic); } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs index 1b9a92801992a..a08489d2d1346 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs @@ -1,11 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + use ruff_python_ast::{self as ast, Expr}; use ruff_python_trivia::PythonWhitespace; use ruff_text_size::Ranged; use std::borrow::Cow; use crate::checkers::ast::Checker; +use crate::linter::float::as_non_finite_float_string_literal; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary string literal or float casts in `Decimal` @@ -23,6 +25,9 @@ use crate::checkers::ast::Checker; /// Prefer the more concise form of argument passing for `Decimal` /// constructors, as it's more readable and idiomatic. /// +/// Note that this rule does not flag quoted float literals such as `Decimal("0.1")`, which will +/// produce a more precise `Decimal` value than the unquoted `Decimal(0.1)`. +/// /// ## Example /// ```python /// Decimal("0") @@ -71,7 +76,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal return; }; - let diagnostic = match value { + match value { Expr::StringLiteral(ast::ExprStringLiteral { value: str_literal, .. }) => { @@ -120,7 +125,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal }; let replacement = format!("{unary}{rest}"); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( VerboseDecimalConstructor { replacement: replacement.clone(), }, @@ -131,8 +136,6 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal replacement, value.range(), ))); - - diagnostic } Expr::Call(ast::ExprCall { func, arguments, .. @@ -149,42 +152,20 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal let [float] = arguments.args.as_ref() else { return; }; - let Some(float) = float.as_string_literal_expr() else { + let Some(float_str) = as_non_finite_float_string_literal(float) else { return; }; - let trimmed = float.value.to_str().trim(); - let mut matches_non_finite_keyword = false; - for non_finite_keyword in [ - "inf", - "+inf", - "-inf", - "infinity", - "+infinity", - "-infinity", - "nan", - "+nan", - "-nan", - ] { - if trimmed.eq_ignore_ascii_case(non_finite_keyword) { - matches_non_finite_keyword = true; - break; - } - } - if !matches_non_finite_keyword { - return; - } - let mut replacement = checker.locator().slice(float).to_string(); // `Decimal(float("-nan")) == Decimal("nan")` - if trimmed.eq_ignore_ascii_case("-nan") { + if float_str == "-nan" { // Here we do not attempt to remove just the '-' character. // It may have been encoded (e.g. as '\N{hyphen-minus}') // in the original source slice, and the added complexity // does not make sense for this edge case. replacement = "\"nan\"".to_string(); } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( VerboseDecimalConstructor { replacement: replacement.clone(), }, @@ -195,15 +176,9 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal replacement, value.range(), ))); - - diagnostic - } - _ => { - return; } - }; - - checker.report_diagnostic(diagnostic); + _ => {} + } } // ```console diff --git a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs index fa6ac1580ccae..2440f4236c93e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs @@ -1,15 +1,15 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::relocate::relocate_expr; use ruff_python_ast::visitor::{self, Visitor}; use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_codegen::Generator; use ruff_text_size::{Ranged, TextRange}; +use crate::Violation; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; -use super::super::helpers::{find_file_opens, FileOpen}; +use crate::rules::refurb::helpers::{FileOpen, find_file_opens}; /// ## What it does /// Checks for uses of `open` and `write` that can be replaced by `pathlib` @@ -65,54 +65,28 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) { } // Then we need to match each `open` operation with exactly one `write` call. - let (matches, contents) = { - let mut matcher = WriteMatcher::new(candidates); - visitor::walk_body(&mut matcher, &with.body); - matcher.finish() - }; - - // All the matched operations should be reported. - let diagnostics: Vec = matches - .iter() - .zip(contents) - .map(|(open, content)| { - Diagnostic::new( - WriteWholeFile { - filename: SourceCodeSnippet::from_str(&checker.generator().expr(open.filename)), - suggestion: make_suggestion(open, content, checker.generator()), - }, - open.item.range(), - ) - }) - .collect(); - checker.report_diagnostics(diagnostics); + let mut matcher = WriteMatcher::new(checker, candidates); + visitor::walk_body(&mut matcher, &with.body); } /// AST visitor that matches `open` operations with the corresponding `write` calls. -#[derive(Debug)] -struct WriteMatcher<'a> { +struct WriteMatcher<'a, 'b> { + checker: &'a Checker<'b>, candidates: Vec>, - matches: Vec>, - contents: Vec<&'a Expr>, loop_counter: u32, } -impl<'a> WriteMatcher<'a> { - fn new(candidates: Vec>) -> Self { +impl<'a, 'b> WriteMatcher<'a, 'b> { + fn new(checker: &'a Checker<'b>, candidates: Vec>) -> Self { Self { + checker, candidates, - matches: vec![], - contents: vec![], loop_counter: 0, } } - - fn finish(self) -> (Vec>, Vec<&'a Expr>) { - (self.matches, self.contents) - } } -impl<'a> Visitor<'a> for WriteMatcher<'a> { +impl<'a> Visitor<'a> for WriteMatcher<'a, '_> { fn visit_stmt(&mut self, stmt: &'a Stmt) { if matches!(stmt, ast::Stmt::While(_) | ast::Stmt::For(_)) { self.loop_counter += 1; @@ -131,8 +105,16 @@ impl<'a> Visitor<'a> for WriteMatcher<'a> { .position(|open| open.is_ref(write_to)) { if self.loop_counter == 0 { - self.matches.push(self.candidates.remove(open)); - self.contents.push(content); + let open = self.candidates.remove(open); + self.checker.report_diagnostic( + WriteWholeFile { + filename: SourceCodeSnippet::from_str( + &self.checker.generator().expr(open.filename), + ), + suggestion: make_suggestion(&open, content, self.checker.generator()), + }, + open.item.range(), + ); } else { self.candidates.remove(open); } @@ -166,6 +148,7 @@ fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> Sou id: open.mode.pathlib_method(), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let mut arg = arg.clone(); relocate_expr(&mut arg, TextRange::default()); @@ -175,8 +158,10 @@ fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> Sou args: Box::new([arg]), keywords: open.keywords.iter().copied().cloned().collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; SourceCodeSnippet::from_str(&generator.expr(&call.into())) } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap index a19b474227e6b..c39547d551468 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap @@ -1,144 +1,288 @@ --- source: crates/ruff_linter/src/rules/refurb/mod.rs --- -FURB116.py:6:7: FURB116 [*] Replace `oct` call with `f"{num:o}"` - | -4 | return num -5 | -6 | print(oct(num)[2:]) # FURB116 - | ^^^^^^^^^^^^ FURB116 -7 | print(hex(num)[2:]) # FURB116 -8 | print(bin(num)[2:]) # FURB116 - | - = help: Replace with `f"{num:o}"` +FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"` + | + 7 | return num + 8 | + 9 | print(oct(num)[2:]) # FURB116 + | ^^^^^^^^^^^^ FURB116 +10 | print(hex(num)[2:]) # FURB116 +11 | print(bin(num)[2:]) # FURB116 + | + = help: Replace with `f"{num:o}"` -ℹ Safe fix -3 3 | def return_num() -> int: -4 4 | return num -5 5 | -6 |-print(oct(num)[2:]) # FURB116 - 6 |+print(f"{num:o}") # FURB116 -7 7 | print(hex(num)[2:]) # FURB116 -8 8 | print(bin(num)[2:]) # FURB116 -9 9 | - -FURB116.py:7:7: FURB116 [*] Replace `hex` call with `f"{num:x}"` - | -6 | print(oct(num)[2:]) # FURB116 -7 | print(hex(num)[2:]) # FURB116 - | ^^^^^^^^^^^^ FURB116 -8 | print(bin(num)[2:]) # FURB116 - | - = help: Replace with `f"{num:x}"` +ℹ Display-only fix +6 6 | def return_num() -> int: +7 7 | return num +8 8 | +9 |-print(oct(num)[2:]) # FURB116 + 9 |+print(f"{num:o}") # FURB116 +10 10 | print(hex(num)[2:]) # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | -ℹ Safe fix -4 4 | return num -5 5 | -6 6 | print(oct(num)[2:]) # FURB116 -7 |-print(hex(num)[2:]) # FURB116 - 7 |+print(f"{num:x}") # FURB116 -8 8 | print(bin(num)[2:]) # FURB116 -9 9 | -10 10 | print(oct(1337)[2:]) # FURB116 - -FURB116.py:8:7: FURB116 [*] Replace `bin` call with `f"{num:b}"` - | - 6 | print(oct(num)[2:]) # FURB116 - 7 | print(hex(num)[2:]) # FURB116 - 8 | print(bin(num)[2:]) # FURB116 +FURB116.py:10:7: FURB116 Replace `hex` call with `f"{num:x}"` + | + 9 | print(oct(num)[2:]) # FURB116 +10 | print(hex(num)[2:]) # FURB116 | ^^^^^^^^^^^^ FURB116 - 9 | -10 | print(oct(1337)[2:]) # FURB116 +11 | print(bin(num)[2:]) # FURB116 + | + = help: Replace with `f"{num:x}"` + +ℹ Display-only fix +7 7 | return num +8 8 | +9 9 | print(oct(num)[2:]) # FURB116 +10 |-print(hex(num)[2:]) # FURB116 + 10 |+print(f"{num:x}") # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 + +FURB116.py:11:7: FURB116 Replace `bin` call with `f"{num:b}"` + | + 9 | print(oct(num)[2:]) # FURB116 +10 | print(hex(num)[2:]) # FURB116 +11 | print(bin(num)[2:]) # FURB116 + | ^^^^^^^^^^^^ FURB116 +12 | +13 | print(oct(1337)[2:]) # FURB116 | = help: Replace with `f"{num:b}"` -ℹ Safe fix -5 5 | -6 6 | print(oct(num)[2:]) # FURB116 -7 7 | print(hex(num)[2:]) # FURB116 -8 |-print(bin(num)[2:]) # FURB116 - 8 |+print(f"{num:b}") # FURB116 -9 9 | -10 10 | print(oct(1337)[2:]) # FURB116 -11 11 | print(hex(1337)[2:]) # FURB116 - -FURB116.py:10:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"` - | - 8 | print(bin(num)[2:]) # FURB116 - 9 | -10 | print(oct(1337)[2:]) # FURB116 +ℹ Display-only fix +8 8 | +9 9 | print(oct(num)[2:]) # FURB116 +10 10 | print(hex(num)[2:]) # FURB116 +11 |-print(bin(num)[2:]) # FURB116 + 11 |+print(f"{num:b}") # FURB116 +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 + +FURB116.py:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"` + | +11 | print(bin(num)[2:]) # FURB116 +12 | +13 | print(oct(1337)[2:]) # FURB116 | ^^^^^^^^^^^^^ FURB116 -11 | print(hex(1337)[2:]) # FURB116 -12 | print(bin(1337)[2:]) # FURB116 +14 | print(hex(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 | = help: Replace with `f"{1337:o}"` ℹ Safe fix -7 7 | print(hex(num)[2:]) # FURB116 -8 8 | print(bin(num)[2:]) # FURB116 -9 9 | -10 |-print(oct(1337)[2:]) # FURB116 - 10 |+print(f"{1337:o}") # FURB116 -11 11 | print(hex(1337)[2:]) # FURB116 -12 12 | print(bin(1337)[2:]) # FURB116 -13 13 | - -FURB116.py:11:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"` - | -10 | print(oct(1337)[2:]) # FURB116 -11 | print(hex(1337)[2:]) # FURB116 +10 10 | print(hex(num)[2:]) # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | +13 |-print(oct(1337)[2:]) # FURB116 + 13 |+print(f"{1337:o}") # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 +15 15 | print(bin(1337)[2:]) # FURB116 +16 16 | print(bin(+1337)[2:]) # FURB116 + +FURB116.py:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"` + | +13 | print(oct(1337)[2:]) # FURB116 +14 | print(hex(1337)[2:]) # FURB116 | ^^^^^^^^^^^^^ FURB116 -12 | print(bin(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 +16 | print(bin(+1337)[2:]) # FURB116 | = help: Replace with `f"{1337:x}"` ℹ Safe fix -8 8 | print(bin(num)[2:]) # FURB116 -9 9 | -10 10 | print(oct(1337)[2:]) # FURB116 -11 |-print(hex(1337)[2:]) # FURB116 - 11 |+print(f"{1337:x}") # FURB116 -12 12 | print(bin(1337)[2:]) # FURB116 -13 13 | -14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix) - -FURB116.py:12:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"` - | -10 | print(oct(1337)[2:]) # FURB116 -11 | print(hex(1337)[2:]) # FURB116 -12 | print(bin(1337)[2:]) # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 +14 |-print(hex(1337)[2:]) # FURB116 + 14 |+print(f"{1337:x}") # FURB116 +15 15 | print(bin(1337)[2:]) # FURB116 +16 16 | print(bin(+1337)[2:]) # FURB116 +17 17 | + +FURB116.py:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"` + | +13 | print(oct(1337)[2:]) # FURB116 +14 | print(hex(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 | ^^^^^^^^^^^^^ FURB116 -13 | -14 | print(bin(return_num())[2:]) # FURB116 (no autofix) +16 | print(bin(+1337)[2:]) # FURB116 | = help: Replace with `f"{1337:b}"` ℹ Safe fix -9 9 | -10 10 | print(oct(1337)[2:]) # FURB116 -11 11 | print(hex(1337)[2:]) # FURB116 -12 |-print(bin(1337)[2:]) # FURB116 - 12 |+print(f"{1337:b}") # FURB116 -13 13 | -14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix) -15 15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) - -FURB116.py:14:7: FURB116 Replace `bin` call with f-string - | -12 | print(bin(1337)[2:]) # FURB116 -13 | -14 | print(bin(return_num())[2:]) # FURB116 (no autofix) +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 +15 |-print(bin(1337)[2:]) # FURB116 + 15 |+print(f"{1337:b}") # FURB116 +16 16 | print(bin(+1337)[2:]) # FURB116 +17 17 | +18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix) + +FURB116.py:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"` + | +14 | print(hex(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 +16 | print(bin(+1337)[2:]) # FURB116 + | ^^^^^^^^^^^^^^ FURB116 +17 | +18 | print(bin(return_num())[2:]) # FURB116 (no autofix) + | + = help: Replace with `f"{+1337:b}"` + +ℹ Safe fix +13 13 | print(oct(1337)[2:]) # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 +15 15 | print(bin(1337)[2:]) # FURB116 +16 |-print(bin(+1337)[2:]) # FURB116 + 16 |+print(f"{+1337:b}") # FURB116 +17 17 | +18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix) +19 19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) + +FURB116.py:18:7: FURB116 Replace `bin` call with f-string + | +16 | print(bin(+1337)[2:]) # FURB116 +17 | +18 | print(bin(return_num())[2:]) # FURB116 (no autofix) | ^^^^^^^^^^^^^^^^^^^^^ FURB116 -15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) +19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) | = help: Replace with f-string -FURB116.py:15:7: FURB116 Replace `bin` call with f-string +FURB116.py:19:7: FURB116 Replace `bin` call with f-string | -14 | print(bin(return_num())[2:]) # FURB116 (no autofix) -15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) +18 | print(bin(return_num())[2:]) # FURB116 (no autofix) +19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) | ^^^^^^^^^^^^^^^^^^^^^^ FURB116 -16 | -17 | ## invalid +20 | +21 | ## invalid | = help: Replace with f-string + +FURB116.py:32:7: FURB116 Replace `bin` call with `f"{d:b}"` + | +30 | d = datetime.datetime.now(tz=datetime.UTC) +31 | # autofix is display-only +32 | print(bin(d)[2:]) + | ^^^^^^^^^^ FURB116 +33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 | print(bin(len("xyz").numerator)[2:]) + | + = help: Replace with `f"{d:b}"` + +ℹ Display-only fix +29 29 | +30 30 | d = datetime.datetime.now(tz=datetime.UTC) +31 31 | # autofix is display-only +32 |-print(bin(d)[2:]) + 32 |+print(f"{d:b}") +33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 34 | print(bin(len("xyz").numerator)[2:]) +35 35 | + +FURB116.py:34:7: FURB116 Replace `bin` call with `f"{len("xyz").numerator:b}"` + | +32 | print(bin(d)[2:]) +33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 | print(bin(len("xyz").numerator)[2:]) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116 +35 | +36 | # autofix is display-only + | + = help: Replace with `f"{len("xyz").numerator:b}"` + +ℹ Display-only fix +31 31 | # autofix is display-only +32 32 | print(bin(d)[2:]) +33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 |-print(bin(len("xyz").numerator)[2:]) + 34 |+print(f"{len("xyz").numerator:b}") +35 35 | +36 36 | # autofix is display-only +37 37 | print(bin({0: 1}[0].numerator)[2:]) + +FURB116.py:37:7: FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"` + | +36 | # autofix is display-only +37 | print(bin({0: 1}[0].numerator)[2:]) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116 +38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 | print(bin(ord("\\").numerator)[2:]) + | + = help: Replace with `f"{ {0: 1}[0].numerator:b}"` + +ℹ Display-only fix +34 34 | print(bin(len("xyz").numerator)[2:]) +35 35 | +36 36 | # autofix is display-only +37 |-print(bin({0: 1}[0].numerator)[2:]) + 37 |+print(f"{ {0: 1}[0].numerator:b}") +38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 39 | print(bin(ord("\\").numerator)[2:]) +40 40 | print(hex(sys + +FURB116.py:39:7: FURB116 Replace `bin` call with `f"{ord("\\").numerator:b}"` + | +37 | print(bin({0: 1}[0].numerator)[2:]) +38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 | print(bin(ord("\\").numerator)[2:]) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116 +40 | print(hex(sys +41 | .maxunicode)[2:]) + | + = help: Replace with `f"{ord("\\").numerator:b}"` + +ℹ Display-only fix +36 36 | # autofix is display-only +37 37 | print(bin({0: 1}[0].numerator)[2:]) +38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 |-print(bin(ord("\\").numerator)[2:]) + 39 |+print(f"{ord("\\").numerator:b}") +40 40 | print(hex(sys +41 41 | .maxunicode)[2:]) +42 42 | + +FURB116.py:40:7: FURB116 Replace `hex` call with f-string + | +38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 | print(bin(ord("\\").numerator)[2:]) +40 | print(hex(sys + | _______^ +41 | | .maxunicode)[2:]) + | |________________^ FURB116 +42 | +43 | # for negatives numbers autofix is display-only + | + = help: Replace with f-string + +ℹ Display-only fix +37 37 | print(bin({0: 1}[0].numerator)[2:]) +38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 39 | print(bin(ord("\\").numerator)[2:]) +40 |-print(hex(sys +41 |-.maxunicode)[2:]) + 40 |+print(f"{sys + 41 |+.maxunicode:x}") +42 42 | +43 43 | # for negatives numbers autofix is display-only +44 44 | print(bin(-1)[2:]) + +FURB116.py:44:7: FURB116 Replace `bin` call with `f"{-1:b}"` + | +43 | # for negatives numbers autofix is display-only +44 | print(bin(-1)[2:]) + | ^^^^^^^^^^^ FURB116 + | + = help: Replace with `f"{-1:b}"` + +ℹ Display-only fix +41 41 | .maxunicode)[2:]) +42 42 | +43 43 | # for negatives numbers autofix is display-only +44 |-print(bin(-1)[2:]) + 44 |+print(f"{-1:b}") diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap index 693030678e3d0..3f90c115644e4 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap @@ -307,3 +307,126 @@ FURB122.py:93:9: FURB122 [*] Use of `f.write` in a for loop 98 97 | 99 98 | 100 99 | # OK + +FURB122.py:183:9: FURB122 [*] Use of `f.write` in a for loop + | +181 | def _(): +182 | with Path("file.txt").open("w", encoding="utf-8") as f: +183 | / for l in lambda: 0: +184 | | f.write(f"[{l}]") + | |_____________________________^ FURB122 + | + = help: Replace with `f.writelines` + +ℹ Safe fix +180 180 | +181 181 | def _(): +182 182 | with Path("file.txt").open("w", encoding="utf-8") as f: +183 |- for l in lambda: 0: +184 |- f.write(f"[{l}]") + 183 |+ f.writelines(f"[{l}]" for l in (lambda: 0)) +185 184 | +186 185 | +187 186 | def _(): + +FURB122.py:189:9: FURB122 [*] Use of `f.write` in a for loop + | +187 | def _(): +188 | with Path("file.txt").open("w", encoding="utf-8") as f: +189 | / for l in (1,) if True else (2,): +190 | | f.write(f"[{l}]") + | |_____________________________^ FURB122 + | + = help: Replace with `f.writelines` + +ℹ Safe fix +186 186 | +187 187 | def _(): +188 188 | with Path("file.txt").open("w", encoding="utf-8") as f: +189 |- for l in (1,) if True else (2,): +190 |- f.write(f"[{l}]") + 189 |+ f.writelines(f"[{l}]" for l in ((1,) if True else (2,))) +191 190 | +192 191 | +193 192 | # don't need to add parentheses when making a function argument + +FURB122.py:196:9: FURB122 [*] Use of `f.write` in a for loop + | +194 | def _(): +195 | with open("file", "w") as f: +196 | / for line in lambda: 0: +197 | | f.write(line) + | |_________________________^ FURB122 + | + = help: Replace with `f.writelines` + +ℹ Safe fix +193 193 | # don't need to add parentheses when making a function argument +194 194 | def _(): +195 195 | with open("file", "w") as f: +196 |- for line in lambda: 0: +197 |- f.write(line) + 196 |+ f.writelines(lambda: 0) +198 197 | +199 198 | +200 199 | def _(): + +FURB122.py:202:9: FURB122 [*] Use of `f.write` in a for loop + | +200 | def _(): +201 | with open("file", "w") as f: +202 | / for line in (1,) if True else (2,): +203 | | f.write(line) + | |_________________________^ FURB122 + | + = help: Replace with `f.writelines` + +ℹ Safe fix +199 199 | +200 200 | def _(): +201 201 | with open("file", "w") as f: +202 |- for line in (1,) if True else (2,): +203 |- f.write(line) + 202 |+ f.writelines((1,) if True else (2,)) +204 203 | +205 204 | +206 205 | # don't add extra parentheses if they're already parenthesized + +FURB122.py:209:9: FURB122 [*] Use of `f.write` in a for loop + | +207 | def _(): +208 | with open("file", "w") as f: +209 | / for line in (lambda: 0): +210 | | f.write(f"{line}") + | |______________________________^ FURB122 + | + = help: Replace with `f.writelines` + +ℹ Safe fix +206 206 | # don't add extra parentheses if they're already parenthesized +207 207 | def _(): +208 208 | with open("file", "w") as f: +209 |- for line in (lambda: 0): +210 |- f.write(f"{line}") + 209 |+ f.writelines(f"{line}" for line in (lambda: 0)) +211 210 | +212 211 | +213 212 | def _(): + +FURB122.py:215:9: FURB122 [*] Use of `f.write` in a for loop + | +213 | def _(): +214 | with open("file", "w") as f: +215 | / for line in ((1,) if True else (2,)): +216 | | f.write(f"{line}") + | |______________________________^ FURB122 + | + = help: Replace with `f.writelines` + +ℹ Safe fix +212 212 | +213 213 | def _(): +214 214 | with open("file", "w") as f: +215 |- for line in ((1,) if True else (2,)): +216 |- f.write(f"{line}") + 215 |+ f.writelines(f"{line}" for line in ((1,) if True else (2,))) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap index c11b60c272766..6b67b28958c1e 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap @@ -12,7 +12,7 @@ FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 4 4 | 5 5 | # Errors 6 6 | with open("FURB129.py") as f: @@ -33,7 +33,7 @@ FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 6 6 | with open("FURB129.py") as f: 7 7 | for _line in f.readlines(): 8 8 | pass @@ -53,7 +53,7 @@ FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 7 7 | for _line in f.readlines(): 8 8 | pass 9 9 | a = [line.lower() for line in f.readlines()] @@ -74,7 +74,7 @@ FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 8 8 | pass 9 9 | a = [line.lower() for line in f.readlines()] 10 10 | b = {line.upper() for line in f.readlines()} @@ -93,7 +93,7 @@ FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 11 11 | c = {line.lower(): line.upper() for line in f.readlines()} 12 12 | 13 13 | with Path("FURB129.py").open() as f: @@ -113,7 +113,7 @@ FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 14 14 | for _line in f.readlines(): 15 15 | pass 16 16 | @@ -133,7 +133,7 @@ FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 17 17 | for _line in open("FURB129.py").readlines(): 18 18 | pass 19 19 | @@ -154,7 +154,7 @@ FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 23 23 | 24 24 | def func(): 25 25 | f = Path("FURB129.py").open() @@ -173,7 +173,7 @@ FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 29 29 | 30 30 | 31 31 | def func(f: io.BytesIO): @@ -194,7 +194,7 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix 35 35 | 36 36 | def func(): 37 37 | with (open("FURB129.py") as f, foo as bar): @@ -204,40 +204,162 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil 40 40 | for _line in bar.readlines(): 41 41 | pass -FURB129.py:48:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly +FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly | -47 | with builtins.open("FURB129.py") as f: -48 | for line in f.readlines(): +46 | with builtins.open("FURB129.py") as f: +47 | for line in f.readlines(): | ^^^^^^^^^^^^^ FURB129 -49 | pass +48 | pass | = help: Remove `readlines()` -ℹ Unsafe fix +ℹ Safe fix +44 44 | import builtins 45 45 | -46 46 | -47 47 | with builtins.open("FURB129.py") as f: -48 |- for line in f.readlines(): - 48 |+ for line in f: -49 49 | pass +46 46 | with builtins.open("FURB129.py") as f: +47 |- for line in f.readlines(): + 47 |+ for line in f: +48 48 | pass +49 49 | 50 50 | -51 51 | -FURB129.py:56:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly +FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly | -55 | with o("FURB129.py") as f: -56 | for line in f.readlines(): +53 | with o("FURB129.py") as f: +54 | for line in f.readlines(): | ^^^^^^^^^^^^^ FURB129 -57 | pass +55 | pass | = help: Remove `readlines()` +ℹ Safe fix +51 51 | from builtins import open as o +52 52 | +53 53 | with o("FURB129.py") as f: +54 |- for line in f.readlines(): + 54 |+ for line in f: +55 55 | pass +56 56 | +57 57 | + +FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +91 | # https://github.com/astral-sh/ruff/issues/18231 +92 | with open("furb129.py") as f: +93 | for line in (f).readlines(): + | ^^^^^^^^^^^^^^^ FURB129 +94 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +90 90 | +91 91 | # https://github.com/astral-sh/ruff/issues/18231 +92 92 | with open("furb129.py") as f: +93 |- for line in (f).readlines(): + 93 |+ for line in (f): +94 94 | pass +95 95 | +96 96 | with open("furb129.py") as f: + +FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +96 | with open("furb129.py") as f: +97 | [line for line in (f).readlines()] + | ^^^^^^^^^^^^^^^ FURB129 + | + = help: Remove `readlines()` + +ℹ Safe fix +94 94 | pass +95 95 | +96 96 | with open("furb129.py") as f: +97 |- [line for line in (f).readlines()] + 97 |+ [line for line in (f)] +98 98 | +99 99 | +100 100 | with open("furb129.py") as f: + +FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +100 | with open("furb129.py") as f: +101 | for line in (((f))).readlines(): + | ^^^^^^^^^^^^^^^^^^^ FURB129 +102 | pass +103 | for line in(f).readlines(): + | + = help: Remove `readlines()` + +ℹ Safe fix +98 98 | +99 99 | +100 100 | with open("furb129.py") as f: +101 |- for line in (((f))).readlines(): + 101 |+ for line in (((f))): +102 102 | pass +103 103 | for line in(f).readlines(): +104 104 | pass + +FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +101 | for line in (((f))).readlines(): +102 | pass +103 | for line in(f).readlines(): + | ^^^^^^^^^^^^^^^ FURB129 +104 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +100 100 | with open("furb129.py") as f: +101 101 | for line in (((f))).readlines(): +102 102 | pass +103 |- for line in(f).readlines(): + 103 |+ for line in(f): +104 104 | pass +105 105 | +106 106 | # Test case for issue #17683 (missing space before keyword) + +FURB129.py:107:29: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +106 | # Test case for issue #17683 (missing space before keyword) +107 | print([line for line in f.readlines()if True]) + | ^^^^^^^^^^^^^ FURB129 +108 | +109 | # https://github.com/astral-sh/ruff/issues/18843 + | + = help: Remove `readlines()` + +ℹ Safe fix +104 104 | pass +105 105 | +106 106 | # Test case for issue #17683 (missing space before keyword) +107 |- print([line for line in f.readlines()if True]) + 107 |+ print([line for line in f if True]) +108 108 | +109 109 | # https://github.com/astral-sh/ruff/issues/18843 +110 110 | with open("file.txt") as fp: + +FURB129.py:112:9: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +110 | with open("file.txt") as fp: +111 | for line in ( # 1 +112 | / fp. # 3 # 2 +113 | | readlines( # 4 +114 | | ) # 5 + | |_________^ FURB129 +115 | ): +116 | ... + | + = help: Remove `readlines()` + ℹ Unsafe fix -53 53 | -54 54 | -55 55 | with o("FURB129.py") as f: -56 |- for line in f.readlines(): - 56 |+ for line in f: -57 57 | pass -58 58 | -59 59 | +109 109 | # https://github.com/astral-sh/ruff/issues/18843 +110 110 | with open("file.txt") as fp: +111 111 | for line in ( # 1 +112 |- fp. # 3 # 2 +113 |- readlines( # 4 +114 |- ) # 5 + 112 |+ fp # 5 +115 113 | ): +116 114 | ... diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap index 8f655fa8ee54d..be96dc7fcd5f8 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap @@ -280,3 +280,134 @@ FURB142.py:41:1: FURB142 [*] Use of `set.add()` in a for loop 46 45 | 47 46 | 48 47 | # False negative + +FURB142.py:83:1: FURB142 [*] Use of `set.discard()` in a for loop + | +81 | s = set() +82 | +83 | / for x in lambda: 0: +84 | | s.discard(-x) + | |_________________^ FURB142 +85 | +86 | for x in (1,) if True else (2,): + | + = help: Replace with `.difference_update()` + +ℹ Safe fix +80 80 | +81 81 | s = set() +82 82 | +83 |-for x in lambda: 0: +84 |- s.discard(-x) + 83 |+s.difference_update(-x for x in (lambda: 0)) +85 84 | +86 85 | for x in (1,) if True else (2,): +87 86 | s.add(-x) + +FURB142.py:86:1: FURB142 [*] Use of `set.add()` in a for loop + | +84 | s.discard(-x) +85 | +86 | / for x in (1,) if True else (2,): +87 | | s.add(-x) + | |_____________^ FURB142 +88 | +89 | # don't add extra parens + | + = help: Replace with `.update()` + +ℹ Safe fix +83 83 | for x in lambda: 0: +84 84 | s.discard(-x) +85 85 | +86 |-for x in (1,) if True else (2,): +87 |- s.add(-x) + 86 |+s.update(-x for x in ((1,) if True else (2,))) +88 87 | +89 88 | # don't add extra parens +90 89 | for x in (lambda: 0): + +FURB142.py:90:1: FURB142 [*] Use of `set.discard()` in a for loop + | +89 | # don't add extra parens +90 | / for x in (lambda: 0): +91 | | s.discard(-x) + | |_________________^ FURB142 +92 | +93 | for x in ((1,) if True else (2,)): + | + = help: Replace with `.difference_update()` + +ℹ Safe fix +87 87 | s.add(-x) +88 88 | +89 89 | # don't add extra parens +90 |-for x in (lambda: 0): +91 |- s.discard(-x) + 90 |+s.difference_update(-x for x in (lambda: 0)) +92 91 | +93 92 | for x in ((1,) if True else (2,)): +94 93 | s.add(-x) + +FURB142.py:93:1: FURB142 [*] Use of `set.add()` in a for loop + | +91 | s.discard(-x) +92 | +93 | / for x in ((1,) if True else (2,)): +94 | | s.add(-x) + | |_____________^ FURB142 +95 | +96 | # don't add parens directly in function call + | + = help: Replace with `.update()` + +ℹ Safe fix +90 90 | for x in (lambda: 0): +91 91 | s.discard(-x) +92 92 | +93 |-for x in ((1,) if True else (2,)): +94 |- s.add(-x) + 93 |+s.update(-x for x in ((1,) if True else (2,))) +95 94 | +96 95 | # don't add parens directly in function call +97 96 | for x in lambda: 0: + +FURB142.py:97:1: FURB142 [*] Use of `set.discard()` in a for loop + | + 96 | # don't add parens directly in function call + 97 | / for x in lambda: 0: + 98 | | s.discard(x) + | |________________^ FURB142 + 99 | +100 | for x in (1,) if True else (2,): + | + = help: Replace with `.difference_update()` + +ℹ Safe fix +94 94 | s.add(-x) +95 95 | +96 96 | # don't add parens directly in function call +97 |-for x in lambda: 0: +98 |- s.discard(x) + 97 |+s.difference_update(lambda: 0) +99 98 | +100 99 | for x in (1,) if True else (2,): +101 100 | s.add(x) + +FURB142.py:100:1: FURB142 [*] Use of `set.add()` in a for loop + | + 98 | s.discard(x) + 99 | +100 | / for x in (1,) if True else (2,): +101 | | s.add(x) + | |____________^ FURB142 + | + = help: Replace with `.update()` + +ℹ Safe fix +97 97 | for x in lambda: 0: +98 98 | s.discard(x) +99 99 | +100 |-for x in (1,) if True else (2,): +101 |- s.add(x) + 100 |+s.update((1,) if True else (2,)) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap index 7888f39f7dee0..2bff822228477 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap @@ -12,7 +12,7 @@ FURB161.py:6:9: FURB161 [*] Use of `bin(x).count('1')` | = help: Replace with `(x).bit_count()` -ℹ Safe fix +ℹ Unsafe fix 3 3 | def ten() -> int: 4 4 | return 10 5 5 | @@ -137,7 +137,7 @@ FURB161.py:12:9: FURB161 [*] Use of `bin(ten()).count('1')` | = help: Replace with `ten().bit_count()` -ℹ Safe fix +ℹ Unsafe fix 9 9 | count = bin(0xA).count("1") # FURB161 10 10 | count = bin(0o12).count("1") # FURB161 11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161 @@ -145,7 +145,7 @@ FURB161.py:12:9: FURB161 [*] Use of `bin(ten()).count('1')` 12 |+count = ten().bit_count() # FURB161 13 13 | count = bin((10)).count("1") # FURB161 14 14 | count = bin("10" "15").count("1") # FURB161 -15 15 | +15 15 | count = bin("123").count("1") # FURB161 FURB161.py:13:9: FURB161 [*] Use of `bin(10).count('1')` | @@ -154,6 +154,7 @@ FURB161.py:13:9: FURB161 [*] Use of `bin(10).count('1')` 13 | count = bin((10)).count("1") # FURB161 | ^^^^^^^^^^^^^^^^^^^^ FURB161 14 | count = bin("10" "15").count("1") # FURB161 +15 | count = bin("123").count("1") # FURB161 | = help: Replace with `(10).bit_count()` @@ -164,8 +165,8 @@ FURB161.py:13:9: FURB161 [*] Use of `bin(10).count('1')` 13 |-count = bin((10)).count("1") # FURB161 13 |+count = (10).bit_count() # FURB161 14 14 | count = bin("10" "15").count("1") # FURB161 -15 15 | -16 16 | count = x.bit_count() # OK +15 15 | count = bin("123").count("1") # FURB161 +16 16 | FURB161.py:14:9: FURB161 [*] Use of `bin("10" "15").count('1')` | @@ -173,17 +174,37 @@ FURB161.py:14:9: FURB161 [*] Use of `bin("10" "15").count('1')` 13 | count = bin((10)).count("1") # FURB161 14 | count = bin("10" "15").count("1") # FURB161 | ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB161 -15 | -16 | count = x.bit_count() # OK +15 | count = bin("123").count("1") # FURB161 | = help: Replace with `("10" "15").bit_count()` -ℹ Safe fix +ℹ Unsafe fix 11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161 12 12 | count = bin(ten()).count("1") # FURB161 13 13 | count = bin((10)).count("1") # FURB161 14 |-count = bin("10" "15").count("1") # FURB161 14 |+count = ("10" "15").bit_count() # FURB161 -15 15 | -16 16 | count = x.bit_count() # OK -17 17 | count = (10).bit_count() # OK +15 15 | count = bin("123").count("1") # FURB161 +16 16 | +17 17 | count = x.bit_count() # OK + +FURB161.py:15:9: FURB161 [*] Use of `bin("123").count('1')` + | +13 | count = bin((10)).count("1") # FURB161 +14 | count = bin("10" "15").count("1") # FURB161 +15 | count = bin("123").count("1") # FURB161 + | ^^^^^^^^^^^^^^^^^^^^^ FURB161 +16 | +17 | count = x.bit_count() # OK + | + = help: Replace with `"123".bit_count()` + +ℹ Unsafe fix +12 12 | count = bin(ten()).count("1") # FURB161 +13 13 | count = bin((10)).count("1") # FURB161 +14 14 | count = bin("10" "15").count("1") # FURB161 +15 |-count = bin("123").count("1") # FURB161 + 15 |+count = "123".bit_count() # FURB161 +16 16 | +17 17 | count = x.bit_count() # OK +18 18 | count = (10).bit_count() # OK diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap index 0985c7f1c9117..14c40084bacc9 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap @@ -11,7 +11,7 @@ FURB163.py:4:1: FURB163 [*] Prefer `math.log2(1)` over `math.log` with a redunda | = help: Replace with `math.log2(1)` -ℹ Safe fix +ℹ Unsafe fix 1 1 | import math 2 2 | 3 3 | # Errors @@ -32,7 +32,7 @@ FURB163.py:5:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redund | = help: Replace with `math.log10(1)` -ℹ Safe fix +ℹ Unsafe fix 2 2 | 3 3 | # Errors 4 4 | math.log(1, 2) @@ -74,7 +74,7 @@ FURB163.py:8:1: FURB163 [*] Prefer `math.log2(foo)` over `math.log` with a redun | = help: Replace with `math.log2(foo)` -ℹ Safe fix +ℹ Unsafe fix 5 5 | math.log(1, 10) 6 6 | math.log(1, math.e) 7 7 | foo = ... @@ -95,7 +95,7 @@ FURB163.py:9:1: FURB163 [*] Prefer `math.log10(foo)` over `math.log` with a redu | = help: Replace with `math.log10(foo)` -ℹ Safe fix +ℹ Unsafe fix 6 6 | math.log(1, math.e) 7 7 | foo = ... 8 8 | math.log(foo, 2) @@ -136,7 +136,7 @@ FURB163.py:11:1: FURB163 [*] Prefer `math.log2(1)` over `math.log` with a redund | = help: Replace with `math.log2(1)` -ℹ Safe fix +ℹ Unsafe fix 8 8 | math.log(foo, 2) 9 9 | math.log(foo, 10) 10 10 | math.log(foo, math.e) @@ -157,7 +157,7 @@ FURB163.py:12:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redun | = help: Replace with `math.log10(1)` -ℹ Safe fix +ℹ Unsafe fix 9 9 | math.log(foo, 10) 10 10 | math.log(foo, math.e) 11 11 | math.log(1, 2.0) @@ -166,3 +166,203 @@ FURB163.py:12:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redun 13 13 | 14 14 | # OK 15 15 | math.log2(1) + +FURB163.py:49:11: FURB163 [*] Prefer `math.log(yield)` over `math.log` with a redundant base + | +47 | # https://github.com/astral-sh/ruff/issues/18747 +48 | def log(): +49 | yield math.log((yield), math.e) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB163 + | + = help: Replace with `math.log(yield)` + +ℹ Safe fix +46 46 | +47 47 | # https://github.com/astral-sh/ruff/issues/18747 +48 48 | def log(): +49 |- yield math.log((yield), math.e) + 49 |+ yield math.log((yield)) +50 50 | +51 51 | +52 52 | def log(): + +FURB163.py:53:11: FURB163 [*] Prefer `math.log(yield from x)` over `math.log` with a redundant base + | +52 | def log(): +53 | yield math.log((yield from x), math.e) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB163 +54 | +55 | # see: https://github.com/astral-sh/ruff/issues/18639 + | + = help: Replace with `math.log(yield from x)` + +ℹ Safe fix +50 50 | +51 51 | +52 52 | def log(): +53 |- yield math.log((yield from x), math.e) + 53 |+ yield math.log((yield from x)) +54 54 | +55 55 | # see: https://github.com/astral-sh/ruff/issues/18639 +56 56 | math.log(1, 10 # comment + +FURB163.py:56:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redundant base + | +55 | # see: https://github.com/astral-sh/ruff/issues/18639 +56 | / math.log(1, 10 # comment +57 | | ) + | |__________^ FURB163 +58 | +59 | math.log(1, + | + = help: Replace with `math.log10(1)` + +ℹ Unsafe fix +53 53 | yield math.log((yield from x), math.e) +54 54 | +55 55 | # see: https://github.com/astral-sh/ruff/issues/18639 +56 |-math.log(1, 10 # comment +57 |- ) + 56 |+math.log10(1) +58 57 | +59 58 | math.log(1, +60 59 | 10 # comment + +FURB163.py:59:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redundant base + | +57 | ) +58 | +59 | / math.log(1, +60 | | 10 # comment +61 | | ) + | |__________^ FURB163 +62 | +63 | math.log(1 # comment + | + = help: Replace with `math.log10(1)` + +ℹ Unsafe fix +56 56 | math.log(1, 10 # comment +57 57 | ) +58 58 | +59 |-math.log(1, +60 |- 10 # comment +61 |- ) + 59 |+math.log10(1) +62 60 | +63 61 | math.log(1 # comment +64 62 | , # comment + +FURB163.py:63:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redundant base + | +61 | ) +62 | +63 | / math.log(1 # comment +64 | | , # comment +65 | | 10 # comment +66 | | ) + | |__________^ FURB163 +67 | +68 | math.log( + | + = help: Replace with `math.log10(1)` + +ℹ Unsafe fix +60 60 | 10 # comment +61 61 | ) +62 62 | +63 |-math.log(1 # comment +64 |- , # comment +65 |- 10 # comment +66 |- ) + 63 |+math.log10(1) +67 64 | +68 65 | math.log( +69 66 | 1 # comment + +FURB163.py:68:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redundant base + | +66 | ) +67 | +68 | / math.log( +69 | | 1 # comment +70 | | , +71 | | 10 # comment +72 | | ) + | |_^ FURB163 +73 | +74 | math.log(4.13e223, 2) + | + = help: Replace with `math.log10(1)` + +ℹ Unsafe fix +65 65 | 10 # comment +66 66 | ) +67 67 | +68 |-math.log( +69 |- 1 # comment +70 |- , +71 |- 10 # comment +72 |-) + 68 |+math.log10(1) +73 69 | +74 70 | math.log(4.13e223, 2) +75 71 | math.log(4.14e223, 10) + +FURB163.py:74:1: FURB163 [*] Prefer `math.log2(4.13e223)` over `math.log` with a redundant base + | +72 | ) +73 | +74 | math.log(4.13e223, 2) + | ^^^^^^^^^^^^^^^^^^^^^ FURB163 +75 | math.log(4.14e223, 10) + | + = help: Replace with `math.log2(4.13e223)` + +ℹ Unsafe fix +71 71 | 10 # comment +72 72 | ) +73 73 | +74 |-math.log(4.13e223, 2) + 74 |+math.log2(4.13e223) +75 75 | math.log(4.14e223, 10) +76 76 | +77 77 | + +FURB163.py:75:1: FURB163 [*] Prefer `math.log10(4.14e223)` over `math.log` with a redundant base + | +74 | math.log(4.13e223, 2) +75 | math.log(4.14e223, 10) + | ^^^^^^^^^^^^^^^^^^^^^^ FURB163 + | + = help: Replace with `math.log10(4.14e223)` + +ℹ Unsafe fix +72 72 | ) +73 73 | +74 74 | math.log(4.13e223, 2) +75 |-math.log(4.14e223, 10) + 75 |+math.log10(4.14e223) +76 76 | +77 77 | +78 78 | def print_log(*args): + +FURB163.py:80:15: FURB163 [*] Prefer `math.log(*args)` over `math.log` with a redundant base + | +78 | def print_log(*args): +79 | try: +80 | print(math.log(*args, math.e)) + | ^^^^^^^^^^^^^^^^^^^^^^^ FURB163 +81 | except TypeError as e: +82 | print(repr(e)) + | + = help: Replace with `math.log(*args)` + +ℹ Unsafe fix +77 77 | +78 78 | def print_log(*args): +79 79 | try: +80 |- print(math.log(*args, math.e)) + 80 |+ print(math.log(*args)) +81 81 | except TypeError as e: +82 82 | print(repr(e)) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap index e15af106a498f..5f72efdfcd186 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap @@ -313,7 +313,7 @@ FURB164.py:20:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi 20 |+_ = Decimal("Infinity") 21 21 | _ = Decimal.from_float(float("-Infinity")) 22 22 | _ = Decimal.from_float(float("nan")) -23 23 | +23 23 | _ = Decimal.from_float(float("-NaN ")) FURB164.py:21:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction | @@ -322,6 +322,7 @@ FURB164.py:21:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi 21 | _ = Decimal.from_float(float("-Infinity")) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 22 | _ = Decimal.from_float(float("nan")) +23 | _ = Decimal.from_float(float("-NaN ")) | = help: Replace with `Decimal` constructor @@ -332,8 +333,8 @@ FURB164.py:21:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi 21 |-_ = Decimal.from_float(float("-Infinity")) 21 |+_ = Decimal("-Infinity") 22 22 | _ = Decimal.from_float(float("nan")) -23 23 | -24 24 | # OK +23 23 | _ = Decimal.from_float(float("-NaN ")) +24 24 | _ = Decimal.from_float(float(" \n+nan \t")) FURB164.py:22:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction | @@ -341,8 +342,8 @@ FURB164.py:22:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi 21 | _ = Decimal.from_float(float("-Infinity")) 22 | _ = Decimal.from_float(float("nan")) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 -23 | -24 | # OK +23 | _ = Decimal.from_float(float("-NaN ")) +24 | _ = Decimal.from_float(float(" \n+nan \t")) | = help: Replace with `Decimal` constructor @@ -352,6 +353,131 @@ FURB164.py:22:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi 21 21 | _ = Decimal.from_float(float("-Infinity")) 22 |-_ = Decimal.from_float(float("nan")) 22 |+_ = Decimal("nan") -23 23 | -24 24 | # OK -25 25 | _ = Fraction(0.1) +23 23 | _ = Decimal.from_float(float("-NaN ")) +24 24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 25 | _ = Decimal.from_float(float(" iNf \n\t ")) + +FURB164.py:23:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction + | +21 | _ = Decimal.from_float(float("-Infinity")) +22 | _ = Decimal.from_float(float("nan")) +23 | _ = Decimal.from_float(float("-NaN ")) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 +24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 | _ = Decimal.from_float(float(" iNf \n\t ")) + | + = help: Replace with `Decimal` constructor + +ℹ Safe fix +20 20 | _ = Decimal.from_float(float("Infinity")) +21 21 | _ = Decimal.from_float(float("-Infinity")) +22 22 | _ = Decimal.from_float(float("nan")) +23 |-_ = Decimal.from_float(float("-NaN ")) + 23 |+_ = Decimal("-NaN ") +24 24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 26 | _ = Decimal.from_float(float("  -inF\n \t")) + +FURB164.py:24:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction + | +22 | _ = Decimal.from_float(float("nan")) +23 | _ = Decimal.from_float(float("-NaN ")) +24 | _ = Decimal.from_float(float(" \n+nan \t")) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 +25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 | _ = Decimal.from_float(float("  -inF\n \t")) + | + = help: Replace with `Decimal` constructor + +ℹ Safe fix +21 21 | _ = Decimal.from_float(float("-Infinity")) +22 22 | _ = Decimal.from_float(float("nan")) +23 23 | _ = Decimal.from_float(float("-NaN ")) +24 |-_ = Decimal.from_float(float(" \n+nan \t")) + 24 |+_ = Decimal(" \n+nan \t") +25 25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) + +FURB164.py:25:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction + | +23 | _ = Decimal.from_float(float("-NaN ")) +24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 | _ = Decimal.from_float(float(" iNf \n\t ")) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 +26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) + | + = help: Replace with `Decimal` constructor + +ℹ Safe fix +22 22 | _ = Decimal.from_float(float("nan")) +23 23 | _ = Decimal.from_float(float("-NaN ")) +24 24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 |-_ = Decimal.from_float(float(" iNf \n\t ")) + 25 |+_ = Decimal(" iNf \n\t ") +26 26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) +28 28 | _ = Decimal.from_float(float("  -InfinIty\n \t")) + +FURB164.py:26:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction + | +24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 | _ = Decimal.from_float(float("  -inF\n \t")) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 +27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) +28 | _ = Decimal.from_float(float("  -InfinIty\n \t")) + | + = help: Replace with `Decimal` constructor + +ℹ Safe fix +23 23 | _ = Decimal.from_float(float("-NaN ")) +24 24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 |-_ = Decimal.from_float(float("  -inF\n \t")) + 26 |+_ = Decimal("  -inF\n \t") +27 27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) +28 28 | _ = Decimal.from_float(float("  -InfinIty\n \t")) +29 29 | + +FURB164.py:27:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction + | +25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 +28 | _ = Decimal.from_float(float("  -InfinIty\n \t")) + | + = help: Replace with `Decimal` constructor + +ℹ Safe fix +24 24 | _ = Decimal.from_float(float(" \n+nan \t")) +25 25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 |-_ = Decimal.from_float(float(" InfinIty \n\t ")) + 27 |+_ = Decimal(" InfinIty \n\t ") +28 28 | _ = Decimal.from_float(float("  -InfinIty\n \t")) +29 29 | +30 30 | # OK + +FURB164.py:28:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction + | +26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) +28 | _ = Decimal.from_float(float("  -InfinIty\n \t")) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164 +29 | +30 | # OK + | + = help: Replace with `Decimal` constructor + +ℹ Safe fix +25 25 | _ = Decimal.from_float(float(" iNf \n\t ")) +26 26 | _ = Decimal.from_float(float("  -inF\n \t")) +27 27 | _ = Decimal.from_float(float(" InfinIty \n\t ")) +28 |-_ = Decimal.from_float(float("  -InfinIty\n \t")) + 28 |+_ = Decimal("  -InfinIty\n \t") +29 29 | +30 30 | # OK +31 31 | _ = Fraction(0.1) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171.py.snap deleted file mode 100644 index e4af1c0427817..0000000000000 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171.py.snap +++ /dev/null @@ -1,352 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/refurb/mod.rs ---- -FURB171.py:3:4: FURB171 [*] Membership test against single-item container - | -1 | # Errors. -2 | -3 | if 1 in (1,): - | ^^^^^^^^^ FURB171 -4 | print("Single-element tuple") - | - = help: Convert to equality test - -ℹ Safe fix -1 1 | # Errors. -2 2 | -3 |-if 1 in (1,): - 3 |+if 1 == 1: -4 4 | print("Single-element tuple") -5 5 | -6 6 | if 1 in [1]: - -FURB171.py:6:4: FURB171 [*] Membership test against single-item container - | -4 | print("Single-element tuple") -5 | -6 | if 1 in [1]: - | ^^^^^^^^ FURB171 -7 | print("Single-element list") - | - = help: Convert to equality test - -ℹ Safe fix -3 3 | if 1 in (1,): -4 4 | print("Single-element tuple") -5 5 | -6 |-if 1 in [1]: - 6 |+if 1 == 1: -7 7 | print("Single-element list") -8 8 | -9 9 | if 1 in {1}: - -FURB171.py:9:4: FURB171 [*] Membership test against single-item container - | - 7 | print("Single-element list") - 8 | - 9 | if 1 in {1}: - | ^^^^^^^^ FURB171 -10 | print("Single-element set") - | - = help: Convert to equality test - -ℹ Safe fix -6 6 | if 1 in [1]: -7 7 | print("Single-element list") -8 8 | -9 |-if 1 in {1}: - 9 |+if 1 == 1: -10 10 | print("Single-element set") -11 11 | -12 12 | if "a" in "a": - -FURB171.py:12:4: FURB171 [*] Membership test against single-item container - | -10 | print("Single-element set") -11 | -12 | if "a" in "a": - | ^^^^^^^^^^ FURB171 -13 | print("Single-element string") - | - = help: Convert to equality test - -ℹ Unsafe fix -9 9 | if 1 in {1}: -10 10 | print("Single-element set") -11 11 | -12 |-if "a" in "a": - 12 |+if "a" == "a": -13 13 | print("Single-element string") -14 14 | -15 15 | if 1 not in (1,): - -FURB171.py:15:4: FURB171 [*] Membership test against single-item container - | -13 | print("Single-element string") -14 | -15 | if 1 not in (1,): - | ^^^^^^^^^^^^^ FURB171 -16 | print("Check `not in` membership test") - | - = help: Convert to inequality test - -ℹ Safe fix -12 12 | if "a" in "a": -13 13 | print("Single-element string") -14 14 | -15 |-if 1 not in (1,): - 15 |+if 1 != 1: -16 16 | print("Check `not in` membership test") -17 17 | -18 18 | if not 1 in (1,): - -FURB171.py:18:8: FURB171 [*] Membership test against single-item container - | -16 | print("Check `not in` membership test") -17 | -18 | if not 1 in (1,): - | ^^^^^^^^^ FURB171 -19 | print("Check the negated membership test") - | - = help: Convert to equality test - -ℹ Safe fix -15 15 | if 1 not in (1,): -16 16 | print("Check `not in` membership test") -17 17 | -18 |-if not 1 in (1,): - 18 |+if not 1 == 1: -19 19 | print("Check the negated membership test") -20 20 | -21 21 | # Non-errors. - -FURB171.py:52:5: FURB171 [*] Membership test against single-item container - | -51 | # https://github.com/astral-sh/ruff/issues/10063 -52 | _ = a in ( - | _____^ -53 | | # Foo -54 | | b, -55 | | ) - | |_^ FURB171 -56 | -57 | _ = a in ( # Foo1 - | - = help: Convert to equality test - -ℹ Unsafe fix -49 49 | -50 50 | -51 51 | # https://github.com/astral-sh/ruff/issues/10063 -52 |-_ = a in ( -53 |- # Foo -54 |- b, -55 |-) - 52 |+_ = a == b -56 53 | -57 54 | _ = a in ( # Foo1 -58 55 | ( # Foo2 - -FURB171.py:57:5: FURB171 [*] Membership test against single-item container - | -55 | ) -56 | -57 | _ = a in ( # Foo1 - | _____^ -58 | | ( # Foo2 -59 | | # Foo3 -60 | | ( # Tuple -61 | | ( # Bar -62 | | (b -63 | | # Bar -64 | | ) -65 | | ) -66 | | # Foo4 -67 | | # Foo5 -68 | | , -69 | | ) -70 | | # Foo6 -71 | | ) -72 | | ) - | |_^ FURB171 -73 | -74 | foo = ( - | - = help: Convert to equality test - -ℹ Unsafe fix -54 54 | b, -55 55 | ) -56 56 | -57 |-_ = a in ( # Foo1 -58 |- ( # Foo2 -59 |- # Foo3 -60 |- ( # Tuple -61 |- ( # Bar - 57 |+_ = a == ( # Bar -62 58 | (b -63 59 | # Bar -64 60 | ) -65 61 | ) -66 |- # Foo4 -67 |- # Foo5 -68 |- , -69 |- ) -70 |- # Foo6 -71 |- ) -72 |-) -73 62 | -74 63 | foo = ( -75 64 | lorem() - -FURB171.py:77:28: FURB171 [*] Membership test against single-item container - | -75 | lorem() -76 | .ipsum() -77 | .dolor(lambda sit: sit in ( - | ____________________________^ -78 | | # Foo1 -79 | | # Foo2 -80 | | amet, -81 | | )) - | |_________^ FURB171 -82 | ) - | - = help: Convert to equality test - -ℹ Unsafe fix -74 74 | foo = ( -75 75 | lorem() -76 76 | .ipsum() -77 |- .dolor(lambda sit: sit in ( -78 |- # Foo1 -79 |- # Foo2 -80 |- amet, -81 |- )) - 77 |+ .dolor(lambda sit: sit == amet) -82 78 | ) -83 79 | -84 80 | foo = ( - -FURB171.py:87:28: FURB171 [*] Membership test against single-item container - | -85 | lorem() -86 | .ipsum() -87 | .dolor(lambda sit: sit in ( - | ____________________________^ -88 | | ( -89 | | # Foo1 -90 | | # Foo2 -91 | | amet -92 | | ), -93 | | )) - | |_________^ FURB171 -94 | ) - | - = help: Convert to equality test - -ℹ Unsafe fix -84 84 | foo = ( -85 85 | lorem() -86 86 | .ipsum() -87 |- .dolor(lambda sit: sit in ( -88 |- ( - 87 |+ .dolor(lambda sit: sit == ( -89 88 | # Foo1 -90 89 | # Foo2 -91 90 | amet -92 |- ), -93 |- )) - 91 |+ )) -94 92 | ) -95 93 | -96 94 | foo = lorem() \ - -FURB171.py:98:24: FURB171 [*] Membership test against single-item container - | - 96 | foo = lorem() \ - 97 | .ipsum() \ - 98 | .dolor(lambda sit: sit in ( - | ________________________^ - 99 | | # Foo1 -100 | | # Foo2 -101 | | amet, -102 | | )) - | |_____^ FURB171 -103 | -104 | def _(): - | - = help: Convert to equality test - -ℹ Unsafe fix -95 95 | -96 96 | foo = lorem() \ -97 97 | .ipsum() \ -98 |- .dolor(lambda sit: sit in ( -99 |- # Foo1 -100 |- # Foo2 -101 |- amet, -102 |- )) - 98 |+ .dolor(lambda sit: sit == amet) -103 99 | -104 100 | def _(): -105 101 | if foo not \ - -FURB171.py:105:8: FURB171 [*] Membership test against single-item container - | -104 | def _(): -105 | if foo not \ - | ________^ -106 | | in [ -107 | | # Before -108 | | bar -109 | | # After -110 | | ]: ... - | |_____^ FURB171 -111 | -112 | def _(): - | - = help: Convert to inequality test - -ℹ Unsafe fix -102 102 | )) -103 103 | -104 104 | def _(): -105 |- if foo not \ -106 |- in [ -107 |- # Before -108 |- bar -109 |- # After -110 |- ]: ... - 105 |+ if foo != bar: ... -111 106 | -112 107 | def _(): -113 108 | if foo not \ - -FURB171.py:113:8: FURB171 [*] Membership test against single-item container - | -112 | def _(): -113 | if foo not \ - | ________^ -114 | | in [ -115 | | # Before -116 | | bar -117 | | # After -118 | | ] and \ - | |_____^ FURB171 -119 | 0 < 1: ... - | - = help: Convert to inequality test - -ℹ Unsafe fix -110 110 | ]: ... -111 111 | -112 112 | def _(): -113 |- if foo not \ -114 |- in [ -115 |- # Before -116 |- bar -117 |- # After -118 |- ] and \ - 113 |+ if foo != bar and \ -119 114 | 0 < 1: ... diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171_0.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171_0.py.snap new file mode 100644 index 0000000000000..8bc2c8a6afa10 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171_0.py.snap @@ -0,0 +1,352 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB171_0.py:3:4: FURB171 [*] Membership test against single-item container + | +1 | # Errors. +2 | +3 | if 1 in (1,): + | ^^^^^^^^^ FURB171 +4 | print("Single-element tuple") + | + = help: Convert to equality test + +ℹ Safe fix +1 1 | # Errors. +2 2 | +3 |-if 1 in (1,): + 3 |+if 1 == 1: +4 4 | print("Single-element tuple") +5 5 | +6 6 | if 1 in [1]: + +FURB171_0.py:6:4: FURB171 [*] Membership test against single-item container + | +4 | print("Single-element tuple") +5 | +6 | if 1 in [1]: + | ^^^^^^^^ FURB171 +7 | print("Single-element list") + | + = help: Convert to equality test + +ℹ Safe fix +3 3 | if 1 in (1,): +4 4 | print("Single-element tuple") +5 5 | +6 |-if 1 in [1]: + 6 |+if 1 == 1: +7 7 | print("Single-element list") +8 8 | +9 9 | if 1 in {1}: + +FURB171_0.py:9:4: FURB171 [*] Membership test against single-item container + | + 7 | print("Single-element list") + 8 | + 9 | if 1 in {1}: + | ^^^^^^^^ FURB171 +10 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +6 6 | if 1 in [1]: +7 7 | print("Single-element list") +8 8 | +9 |-if 1 in {1}: + 9 |+if 1 == 1: +10 10 | print("Single-element set") +11 11 | +12 12 | if "a" in "a": + +FURB171_0.py:12:4: FURB171 [*] Membership test against single-item container + | +10 | print("Single-element set") +11 | +12 | if "a" in "a": + | ^^^^^^^^^^ FURB171 +13 | print("Single-element string") + | + = help: Convert to equality test + +ℹ Unsafe fix +9 9 | if 1 in {1}: +10 10 | print("Single-element set") +11 11 | +12 |-if "a" in "a": + 12 |+if "a" == "a": +13 13 | print("Single-element string") +14 14 | +15 15 | if 1 not in (1,): + +FURB171_0.py:15:4: FURB171 [*] Membership test against single-item container + | +13 | print("Single-element string") +14 | +15 | if 1 not in (1,): + | ^^^^^^^^^^^^^ FURB171 +16 | print("Check `not in` membership test") + | + = help: Convert to inequality test + +ℹ Safe fix +12 12 | if "a" in "a": +13 13 | print("Single-element string") +14 14 | +15 |-if 1 not in (1,): + 15 |+if 1 != 1: +16 16 | print("Check `not in` membership test") +17 17 | +18 18 | if not 1 in (1,): + +FURB171_0.py:18:8: FURB171 [*] Membership test against single-item container + | +16 | print("Check `not in` membership test") +17 | +18 | if not 1 in (1,): + | ^^^^^^^^^ FURB171 +19 | print("Check the negated membership test") + | + = help: Convert to equality test + +ℹ Safe fix +15 15 | if 1 not in (1,): +16 16 | print("Check `not in` membership test") +17 17 | +18 |-if not 1 in (1,): + 18 |+if not 1 == 1: +19 19 | print("Check the negated membership test") +20 20 | +21 21 | # Non-errors. + +FURB171_0.py:52:5: FURB171 [*] Membership test against single-item container + | +51 | # https://github.com/astral-sh/ruff/issues/10063 +52 | _ = a in ( + | _____^ +53 | | # Foo +54 | | b, +55 | | ) + | |_^ FURB171 +56 | +57 | _ = a in ( # Foo1 + | + = help: Convert to equality test + +ℹ Unsafe fix +49 49 | +50 50 | +51 51 | # https://github.com/astral-sh/ruff/issues/10063 +52 |-_ = a in ( +53 |- # Foo +54 |- b, +55 |-) + 52 |+_ = a == b +56 53 | +57 54 | _ = a in ( # Foo1 +58 55 | ( # Foo2 + +FURB171_0.py:57:5: FURB171 [*] Membership test against single-item container + | +55 | ) +56 | +57 | _ = a in ( # Foo1 + | _____^ +58 | | ( # Foo2 +59 | | # Foo3 +60 | | ( # Tuple +61 | | ( # Bar +62 | | (b +63 | | # Bar +64 | | ) +65 | | ) +66 | | # Foo4 +67 | | # Foo5 +68 | | , +69 | | ) +70 | | # Foo6 +71 | | ) +72 | | ) + | |_^ FURB171 +73 | +74 | foo = ( + | + = help: Convert to equality test + +ℹ Unsafe fix +54 54 | b, +55 55 | ) +56 56 | +57 |-_ = a in ( # Foo1 +58 |- ( # Foo2 +59 |- # Foo3 +60 |- ( # Tuple +61 |- ( # Bar + 57 |+_ = a == ( # Bar +62 58 | (b +63 59 | # Bar +64 60 | ) +65 61 | ) +66 |- # Foo4 +67 |- # Foo5 +68 |- , +69 |- ) +70 |- # Foo6 +71 |- ) +72 |-) +73 62 | +74 63 | foo = ( +75 64 | lorem() + +FURB171_0.py:77:28: FURB171 [*] Membership test against single-item container + | +75 | lorem() +76 | .ipsum() +77 | .dolor(lambda sit: sit in ( + | ____________________________^ +78 | | # Foo1 +79 | | # Foo2 +80 | | amet, +81 | | )) + | |_________^ FURB171 +82 | ) + | + = help: Convert to equality test + +ℹ Unsafe fix +74 74 | foo = ( +75 75 | lorem() +76 76 | .ipsum() +77 |- .dolor(lambda sit: sit in ( +78 |- # Foo1 +79 |- # Foo2 +80 |- amet, +81 |- )) + 77 |+ .dolor(lambda sit: sit == amet) +82 78 | ) +83 79 | +84 80 | foo = ( + +FURB171_0.py:87:28: FURB171 [*] Membership test against single-item container + | +85 | lorem() +86 | .ipsum() +87 | .dolor(lambda sit: sit in ( + | ____________________________^ +88 | | ( +89 | | # Foo1 +90 | | # Foo2 +91 | | amet +92 | | ), +93 | | )) + | |_________^ FURB171 +94 | ) + | + = help: Convert to equality test + +ℹ Unsafe fix +84 84 | foo = ( +85 85 | lorem() +86 86 | .ipsum() +87 |- .dolor(lambda sit: sit in ( +88 |- ( + 87 |+ .dolor(lambda sit: sit == ( +89 88 | # Foo1 +90 89 | # Foo2 +91 90 | amet +92 |- ), +93 |- )) + 91 |+ )) +94 92 | ) +95 93 | +96 94 | foo = lorem() \ + +FURB171_0.py:98:24: FURB171 [*] Membership test against single-item container + | + 96 | foo = lorem() \ + 97 | .ipsum() \ + 98 | .dolor(lambda sit: sit in ( + | ________________________^ + 99 | | # Foo1 +100 | | # Foo2 +101 | | amet, +102 | | )) + | |_____^ FURB171 +103 | +104 | def _(): + | + = help: Convert to equality test + +ℹ Unsafe fix +95 95 | +96 96 | foo = lorem() \ +97 97 | .ipsum() \ +98 |- .dolor(lambda sit: sit in ( +99 |- # Foo1 +100 |- # Foo2 +101 |- amet, +102 |- )) + 98 |+ .dolor(lambda sit: sit == amet) +103 99 | +104 100 | def _(): +105 101 | if foo not \ + +FURB171_0.py:105:8: FURB171 [*] Membership test against single-item container + | +104 | def _(): +105 | if foo not \ + | ________^ +106 | | in [ +107 | | # Before +108 | | bar +109 | | # After +110 | | ]: ... + | |_____^ FURB171 +111 | +112 | def _(): + | + = help: Convert to inequality test + +ℹ Unsafe fix +102 102 | )) +103 103 | +104 104 | def _(): +105 |- if foo not \ +106 |- in [ +107 |- # Before +108 |- bar +109 |- # After +110 |- ]: ... + 105 |+ if foo != bar: ... +111 106 | +112 107 | def _(): +113 108 | if foo not \ + +FURB171_0.py:113:8: FURB171 [*] Membership test against single-item container + | +112 | def _(): +113 | if foo not \ + | ________^ +114 | | in [ +115 | | # Before +116 | | bar +117 | | # After +118 | | ] and \ + | |_____^ FURB171 +119 | 0 < 1: ... + | + = help: Convert to inequality test + +ℹ Unsafe fix +110 110 | ]: ... +111 111 | +112 112 | def _(): +113 |- if foo not \ +114 |- in [ +115 |- # Before +116 |- bar +117 |- # After +118 |- ] and \ + 113 |+ if foo != bar and \ +119 114 | 0 < 1: ... diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171_1.py.snap new file mode 100644 index 0000000000000..01e249adf5ba1 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB171_FURB171_1.py.snap @@ -0,0 +1,141 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB171_1.py:3:4: FURB171 [*] Membership test against single-item container + | +1 | # Errors. +2 | +3 | if 1 in set([1]): + | ^^^^^^^^^^^^^ FURB171 +4 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +1 1 | # Errors. +2 2 | +3 |-if 1 in set([1]): + 3 |+if 1 == 1: +4 4 | print("Single-element set") +5 5 | +6 6 | if 1 in set((1,)): + +FURB171_1.py:6:4: FURB171 [*] Membership test against single-item container + | +4 | print("Single-element set") +5 | +6 | if 1 in set((1,)): + | ^^^^^^^^^^^^^^ FURB171 +7 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +3 3 | if 1 in set([1]): +4 4 | print("Single-element set") +5 5 | +6 |-if 1 in set((1,)): + 6 |+if 1 == 1: +7 7 | print("Single-element set") +8 8 | +9 9 | if 1 in set({1}): + +FURB171_1.py:9:4: FURB171 [*] Membership test against single-item container + | + 7 | print("Single-element set") + 8 | + 9 | if 1 in set({1}): + | ^^^^^^^^^^^^^ FURB171 +10 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +6 6 | if 1 in set((1,)): +7 7 | print("Single-element set") +8 8 | +9 |-if 1 in set({1}): + 9 |+if 1 == 1: +10 10 | print("Single-element set") +11 11 | +12 12 | if 1 in frozenset([1]): + +FURB171_1.py:12:4: FURB171 [*] Membership test against single-item container + | +10 | print("Single-element set") +11 | +12 | if 1 in frozenset([1]): + | ^^^^^^^^^^^^^^^^^^^ FURB171 +13 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +9 9 | if 1 in set({1}): +10 10 | print("Single-element set") +11 11 | +12 |-if 1 in frozenset([1]): + 12 |+if 1 == 1: +13 13 | print("Single-element set") +14 14 | +15 15 | if 1 in frozenset((1,)): + +FURB171_1.py:15:4: FURB171 [*] Membership test against single-item container + | +13 | print("Single-element set") +14 | +15 | if 1 in frozenset((1,)): + | ^^^^^^^^^^^^^^^^^^^^ FURB171 +16 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +12 12 | if 1 in frozenset([1]): +13 13 | print("Single-element set") +14 14 | +15 |-if 1 in frozenset((1,)): + 15 |+if 1 == 1: +16 16 | print("Single-element set") +17 17 | +18 18 | if 1 in frozenset({1}): + +FURB171_1.py:18:4: FURB171 [*] Membership test against single-item container + | +16 | print("Single-element set") +17 | +18 | if 1 in frozenset({1}): + | ^^^^^^^^^^^^^^^^^^^ FURB171 +19 | print("Single-element set") + | + = help: Convert to equality test + +ℹ Safe fix +15 15 | if 1 in frozenset((1,)): +16 16 | print("Single-element set") +17 17 | +18 |-if 1 in frozenset({1}): + 18 |+if 1 == 1: +19 19 | print("Single-element set") +20 20 | +21 21 | if 1 in set(set([1])): + +FURB171_1.py:21:4: FURB171 [*] Membership test against single-item container + | +19 | print("Single-element set") +20 | +21 | if 1 in set(set([1])): + | ^^^^^^^^^^^^^^^^^^ FURB171 +22 | print('Recursive solution') + | + = help: Convert to equality test + +ℹ Safe fix +18 18 | if 1 in frozenset({1}): +19 19 | print("Single-element set") +20 20 | +21 |-if 1 in set(set([1])): + 21 |+if 1 == 1: +22 22 | print('Recursive solution') +23 23 | +24 24 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap index 45df8c8e53cea..d351d92f93732 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap @@ -50,7 +50,7 @@ FURB180.py:26:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract | = help: Replace with `abc.ABC` -ℹ Safe fix +ℹ Unsafe fix 23 23 | pass 24 24 | 25 25 | @@ -68,7 +68,7 @@ FURB180.py:31:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract | = help: Replace with `abc.ABC` -ℹ Safe fix +ℹ Unsafe fix 28 28 | def foo(self): pass 29 29 | 30 30 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap new file mode 100644 index 0000000000000..37b1e3aea601a --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap @@ -0,0 +1,256 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB116.py:9:7: FURB116 Replace `oct` call with `f"{num:o}"` + | + 7 | return num + 8 | + 9 | print(oct(num)[2:]) # FURB116 + | ^^^^^^^^^^^^ FURB116 +10 | print(hex(num)[2:]) # FURB116 +11 | print(bin(num)[2:]) # FURB116 + | + = help: Replace with `f"{num:o}"` + +ℹ Display-only fix +6 6 | def return_num() -> int: +7 7 | return num +8 8 | +9 |-print(oct(num)[2:]) # FURB116 + 9 |+print(f"{num:o}") # FURB116 +10 10 | print(hex(num)[2:]) # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | + +FURB116.py:10:7: FURB116 Replace `hex` call with `f"{num:x}"` + | + 9 | print(oct(num)[2:]) # FURB116 +10 | print(hex(num)[2:]) # FURB116 + | ^^^^^^^^^^^^ FURB116 +11 | print(bin(num)[2:]) # FURB116 + | + = help: Replace with `f"{num:x}"` + +ℹ Display-only fix +7 7 | return num +8 8 | +9 9 | print(oct(num)[2:]) # FURB116 +10 |-print(hex(num)[2:]) # FURB116 + 10 |+print(f"{num:x}") # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 + +FURB116.py:11:7: FURB116 Replace `bin` call with `f"{num:b}"` + | + 9 | print(oct(num)[2:]) # FURB116 +10 | print(hex(num)[2:]) # FURB116 +11 | print(bin(num)[2:]) # FURB116 + | ^^^^^^^^^^^^ FURB116 +12 | +13 | print(oct(1337)[2:]) # FURB116 + | + = help: Replace with `f"{num:b}"` + +ℹ Display-only fix +8 8 | +9 9 | print(oct(num)[2:]) # FURB116 +10 10 | print(hex(num)[2:]) # FURB116 +11 |-print(bin(num)[2:]) # FURB116 + 11 |+print(f"{num:b}") # FURB116 +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 + +FURB116.py:13:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"` + | +11 | print(bin(num)[2:]) # FURB116 +12 | +13 | print(oct(1337)[2:]) # FURB116 + | ^^^^^^^^^^^^^ FURB116 +14 | print(hex(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 + | + = help: Replace with `f"{1337:o}"` + +ℹ Safe fix +10 10 | print(hex(num)[2:]) # FURB116 +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | +13 |-print(oct(1337)[2:]) # FURB116 + 13 |+print(f"{1337:o}") # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 +15 15 | print(bin(1337)[2:]) # FURB116 +16 16 | print(bin(+1337)[2:]) # FURB116 + +FURB116.py:14:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"` + | +13 | print(oct(1337)[2:]) # FURB116 +14 | print(hex(1337)[2:]) # FURB116 + | ^^^^^^^^^^^^^ FURB116 +15 | print(bin(1337)[2:]) # FURB116 +16 | print(bin(+1337)[2:]) # FURB116 + | + = help: Replace with `f"{1337:x}"` + +ℹ Safe fix +11 11 | print(bin(num)[2:]) # FURB116 +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 +14 |-print(hex(1337)[2:]) # FURB116 + 14 |+print(f"{1337:x}") # FURB116 +15 15 | print(bin(1337)[2:]) # FURB116 +16 16 | print(bin(+1337)[2:]) # FURB116 +17 17 | + +FURB116.py:15:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"` + | +13 | print(oct(1337)[2:]) # FURB116 +14 | print(hex(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 + | ^^^^^^^^^^^^^ FURB116 +16 | print(bin(+1337)[2:]) # FURB116 + | + = help: Replace with `f"{1337:b}"` + +ℹ Safe fix +12 12 | +13 13 | print(oct(1337)[2:]) # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 +15 |-print(bin(1337)[2:]) # FURB116 + 15 |+print(f"{1337:b}") # FURB116 +16 16 | print(bin(+1337)[2:]) # FURB116 +17 17 | +18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix) + +FURB116.py:16:7: FURB116 [*] Replace `bin` call with `f"{+1337:b}"` + | +14 | print(hex(1337)[2:]) # FURB116 +15 | print(bin(1337)[2:]) # FURB116 +16 | print(bin(+1337)[2:]) # FURB116 + | ^^^^^^^^^^^^^^ FURB116 +17 | +18 | print(bin(return_num())[2:]) # FURB116 (no autofix) + | + = help: Replace with `f"{+1337:b}"` + +ℹ Safe fix +13 13 | print(oct(1337)[2:]) # FURB116 +14 14 | print(hex(1337)[2:]) # FURB116 +15 15 | print(bin(1337)[2:]) # FURB116 +16 |-print(bin(+1337)[2:]) # FURB116 + 16 |+print(f"{+1337:b}") # FURB116 +17 17 | +18 18 | print(bin(return_num())[2:]) # FURB116 (no autofix) +19 19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) + +FURB116.py:18:7: FURB116 Replace `bin` call with f-string + | +16 | print(bin(+1337)[2:]) # FURB116 +17 | +18 | print(bin(return_num())[2:]) # FURB116 (no autofix) + | ^^^^^^^^^^^^^^^^^^^^^ FURB116 +19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) + | + = help: Replace with f-string + +FURB116.py:19:7: FURB116 Replace `bin` call with f-string + | +18 | print(bin(return_num())[2:]) # FURB116 (no autofix) +19 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix) + | ^^^^^^^^^^^^^^^^^^^^^^ FURB116 +20 | +21 | ## invalid + | + = help: Replace with f-string + +FURB116.py:32:7: FURB116 Replace `bin` call with `f"{d:b}"` + | +30 | d = datetime.datetime.now(tz=datetime.UTC) +31 | # autofix is display-only +32 | print(bin(d)[2:]) + | ^^^^^^^^^^ FURB116 +33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 | print(bin(len("xyz").numerator)[2:]) + | + = help: Replace with `f"{d:b}"` + +ℹ Display-only fix +29 29 | +30 30 | d = datetime.datetime.now(tz=datetime.UTC) +31 31 | # autofix is display-only +32 |-print(bin(d)[2:]) + 32 |+print(f"{d:b}") +33 33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 34 | print(bin(len("xyz").numerator)[2:]) +35 35 | + +FURB116.py:34:7: FURB116 Replace `bin` call with f-string + | +32 | print(bin(d)[2:]) +33 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +34 | print(bin(len("xyz").numerator)[2:]) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116 +35 | +36 | # autofix is display-only + | + = help: Replace with f-string + +FURB116.py:37:7: FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"` + | +36 | # autofix is display-only +37 | print(bin({0: 1}[0].numerator)[2:]) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116 +38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 | print(bin(ord("\\").numerator)[2:]) + | + = help: Replace with `f"{ {0: 1}[0].numerator:b}"` + +ℹ Display-only fix +34 34 | print(bin(len("xyz").numerator)[2:]) +35 35 | +36 36 | # autofix is display-only +37 |-print(bin({0: 1}[0].numerator)[2:]) + 37 |+print(f"{ {0: 1}[0].numerator:b}") +38 38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 39 | print(bin(ord("\\").numerator)[2:]) +40 40 | print(hex(sys + +FURB116.py:39:7: FURB116 Replace `bin` call with f-string + | +37 | print(bin({0: 1}[0].numerator)[2:]) +38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 | print(bin(ord("\\").numerator)[2:]) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB116 +40 | print(hex(sys +41 | .maxunicode)[2:]) + | + = help: Replace with f-string + +FURB116.py:40:7: FURB116 Replace `hex` call with f-string + | +38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error +39 | print(bin(ord("\\").numerator)[2:]) +40 | print(hex(sys + | _______^ +41 | | .maxunicode)[2:]) + | |________________^ FURB116 +42 | +43 | # for negatives numbers autofix is display-only + | + = help: Replace with f-string + +FURB116.py:44:7: FURB116 Replace `bin` call with `f"{-1:b}"` + | +43 | # for negatives numbers autofix is display-only +44 | print(bin(-1)[2:]) + | ^^^^^^^^^^^ FURB116 + | + = help: Replace with `f"{-1:b}"` + +ℹ Display-only fix +41 41 | .maxunicode)[2:]) +42 42 | +43 43 | # for negatives numbers autofix is display-only +44 |-print(bin(-1)[2:]) + 44 |+print(f"{-1:b}") diff --git a/crates/ruff_linter/src/rules/ruff/helpers.rs b/crates/ruff_linter/src/rules/ruff/helpers.rs new file mode 100644 index 0000000000000..535492816bb4a --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/helpers.rs @@ -0,0 +1,244 @@ +use ruff_python_ast::helpers::{Truthiness, map_callable, map_subscript}; +use ruff_python_ast::{self as ast, Expr, ExprCall}; +use ruff_python_semantic::{BindingKind, Modules, SemanticModel, analyze}; + +/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`. +/// +/// While `__slots__` is typically defined via a tuple, Python accepts any iterable and, in +/// particular, allows the use of a dictionary to define the attribute names (as keys) and +/// docstrings (as values). +pub(super) fn is_special_attribute(value: &Expr) -> bool { + if let Expr::Name(ast::ExprName { id, .. }) = value { + matches!( + id.as_str(), + "__slots__" | "__dict__" | "__weakref__" | "__annotations__" + ) + } else { + false + } +} + +/// Returns `true` if the given [`Expr`] is a stdlib `dataclasses.field` call. +fn is_stdlib_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"])) +} + +/// Returns `true` if the given [`Expr`] is a call to an `attrs` field function. +fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["attrs", "field" | "Factory"] + // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L33 + | ["attr", "ib" | "attr" | "attrib" | "field" | "Factory"] + ) + }) +} + +/// Return `true` if `func` represents a `field()` call corresponding to the `dataclass_kind` variant passed in. +/// +/// I.e., if `DataclassKind::Attrs` is passed in, +/// return `true` if `func` represents a call to `attr.ib()` or `attrs.field()`; +/// if `DataclassKind::Stdlib` is passed in, +/// return `true` if `func` represents a call to `dataclasse.field()`. +pub(super) fn is_dataclass_field( + func: &Expr, + semantic: &SemanticModel, + dataclass_kind: DataclassKind, +) -> bool { + match dataclass_kind { + DataclassKind::Attrs(..) => is_attrs_field(func, semantic), + DataclassKind::Stdlib => is_stdlib_dataclass_field(func, semantic), + } +} + +/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. +pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + + // ClassVar can be used either with a subscript `ClassVar[...]` or without (the type is + // inferred). + semantic.match_typing_expr(map_subscript(annotation), "ClassVar") +} + +/// Returns `true` if the given [`Expr`] is a `typing.Final` annotation. +pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + + // Final can be used either with a subscript `Final[...]` or without (the type is + // inferred). + semantic.match_typing_expr(map_subscript(annotation), "Final") +} + +/// Values that [`attrs`'s `auto_attribs`][1] accept. +/// +/// [1]: https://www.attrs.org/en/stable/api.html#attrs.define +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum AttrsAutoAttribs { + /// `a: str = ...` are automatically converted to fields. + True, + /// Only `attrs.field()`/`attr.ib()` calls are considered fields. + False, + /// `True` if any attributes are annotated (and no unannotated `attrs.field`s are found). + /// `False` otherwise. + None, + /// The provided value is not a literal. + Unknown, +} + +/// Enumeration of various kinds of dataclasses recognised by Ruff +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum DataclassKind { + /// dataclasses created by the stdlib `dataclasses` module + Stdlib, + /// dataclasses created by the third-party `attrs` library + Attrs(AttrsAutoAttribs), +} + +/// Return the kind of dataclass this class definition is (stdlib or `attrs`), +/// or `None` if the class is not a dataclass. +pub(super) fn dataclass_kind<'a>( + class_def: &'a ast::StmtClassDef, + semantic: &SemanticModel, +) -> Option<(DataclassKind, &'a ast::Decorator)> { + if !(semantic.seen_module(Modules::DATACLASSES) || semantic.seen_module(Modules::ATTRS)) { + return None; + } + + for decorator in &class_def.decorator_list { + let Some(qualified_name) = + semantic.resolve_qualified_name(map_callable(&decorator.expression)) + else { + continue; + }; + + match qualified_name.segments() { + ["attrs" | "attr", func @ ("define" | "frozen" | "mutable")] + // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L32 + | ["attr", func @ ("s" | "attributes" | "attrs")] => { + // `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`, + // whereas `@attr.s` implicitly sets `auto_attribs=False`. + // https://www.attrs.org/en/stable/api.html#attrs.define + // https://www.attrs.org/en/stable/api-attr.html#attr.s + let Expr::Call(ExprCall { arguments, .. }) = &decorator.expression else { + let auto_attribs = if *func == "s" { + AttrsAutoAttribs::False + } else { + AttrsAutoAttribs::None + }; + + return Some((DataclassKind::Attrs(auto_attribs), decorator)); + }; + + let Some(auto_attribs) = arguments.find_keyword("auto_attribs") else { + return Some((DataclassKind::Attrs(AttrsAutoAttribs::None), decorator)); + }; + + let auto_attribs = match Truthiness::from_expr(&auto_attribs.value, |id| { + semantic.has_builtin_binding(id) + }) { + // `auto_attribs` requires an exact `True` to be true + Truthiness::True => AttrsAutoAttribs::True, + // Or an exact `None` to auto-detect. + Truthiness::None => AttrsAutoAttribs::None, + // Otherwise, anything else (even a truthy value, like `1`) is considered `False`. + Truthiness::Truthy | Truthiness::False | Truthiness::Falsey => { + AttrsAutoAttribs::False + } + // Unless, of course, we can't determine the value. + Truthiness::Unknown => AttrsAutoAttribs::Unknown, + }; + + return Some((DataclassKind::Attrs(auto_attribs), decorator)); + } + ["dataclasses", "dataclass"] => return Some((DataclassKind::Stdlib, decorator)), + _ => continue, + } + } + + None +} + +/// Return true if dataclass (stdlib or `attrs`) is frozen +pub(super) fn is_frozen_dataclass( + dataclass_decorator: &ast::Decorator, + semantic: &SemanticModel, +) -> bool { + let Some(qualified_name) = + semantic.resolve_qualified_name(map_callable(&dataclass_decorator.expression)) + else { + return false; + }; + + match qualified_name.segments() { + ["dataclasses", "dataclass"] => { + let Expr::Call(ExprCall { arguments, .. }) = &dataclass_decorator.expression else { + return false; + }; + + let Some(keyword) = arguments.find_keyword("frozen") else { + return false; + }; + Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id)) + .into_bool() + .unwrap_or_default() + } + ["attrs" | "attr", "frozen"] => true, + _ => false, + } +} + +/// Returns `true` if the given class has "default copy" semantics. +/// +/// For example, Pydantic `BaseModel` and `BaseSettings` subclasses copy attribute defaults on +/// instance creation. As such, the use of mutable default values is safe for such classes. +pub(super) fn has_default_copy_semantics( + class_def: &ast::StmtClassDef, + semantic: &SemanticModel, +) -> bool { + analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| { + matches!( + qualified_name.segments(), + [ + "pydantic", + "BaseModel" | "RootModel" | "BaseSettings" | "BaseConfig" + ] | ["pydantic", "generics", "GenericModel"] + | [ + "pydantic", + "v1", + "BaseModel" | "BaseSettings" | "BaseConfig" + ] + | ["pydantic", "v1", "generics", "GenericModel"] + | ["pydantic_settings", "BaseSettings"] + | ["msgspec", "Struct"] + | ["sqlmodel", "SQLModel"] + ) + }) +} + +/// Returns `true` if the given function is an instantiation of a class that implements the +/// descriptor protocol. +/// +/// See: +pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.lookup_attribute(func).is_some_and(|id| { + let BindingKind::ClassDefinition(scope_id) = semantic.binding(id).kind else { + return false; + }; + + // Look for `__get__`, `__set__`, and `__delete__` methods. + ["__get__", "__set__", "__delete__"].iter().any(|method| { + semantic.scopes[scope_id] + .get(method) + .is_some_and(|id| semantic.binding(id).kind.is_function_definition()) + }) + }) +} diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index c3543aeb90d83..f712219503213 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -1,5 +1,6 @@ //! Ruff-specific rules. +mod helpers; pub(crate) mod rules; pub mod settings; pub(crate) mod typing; @@ -18,12 +19,13 @@ mod tests { use crate::pyproject_toml::lint_pyproject_toml; use crate::registry::Rule; - use crate::settings::types::{CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode}; use crate::settings::LinterSettings; + use crate::settings::types::{CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode}; use crate::test::{test_path, test_resource_path}; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] + #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))] #[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))] #[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))] #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] @@ -84,19 +86,31 @@ mod tests { #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))] #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))] #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))] + #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_CR.py"))] + #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_LF.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_if.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_for.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_while.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_try.py"))] #[test_case(Rule::MapIntVersionParsing, Path::new("RUF048.py"))] #[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))] + #[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))] #[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))] #[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))] + #[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))] #[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))] + #[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))] + #[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))] + #[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))] #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_0.py"))] #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))] #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_2.py"))] #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_3.py"))] + #[test_case(Rule::InEmptyCollection, Path::new("RUF060.py"))] + #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_raises.py"))] + #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))] + #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))] + #[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))] #[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))] @@ -106,7 +120,7 @@ mod tests { Path::new("ruff").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -121,7 +135,7 @@ mod tests { ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -133,11 +147,11 @@ mod tests { ruff: super::settings::Settings { parenthesize_tuple_in_subscript: false, }, - unresolved_target_version: PythonVersion::PY310, + unresolved_target_version: PythonVersion::PY310.into(), ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -154,7 +168,61 @@ mod tests { &settings::LinterSettings::for_rule(Rule::ImplicitOptional) .with_target_version(PythonVersion::PY39), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test] + fn access_annotations_from_class_dict_py39_no_typing_extensions() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF063.py"), + &LinterSettings { + typing_extensions: false, + unresolved_target_version: PythonVersion::PY39.into(), + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn access_annotations_from_class_dict_py39_with_typing_extensions() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF063.py"), + &LinterSettings { + typing_extensions: true, + unresolved_target_version: PythonVersion::PY39.into(), + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn access_annotations_from_class_dict_py310() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF063.py"), + &LinterSettings { + unresolved_target_version: PythonVersion::PY310.into(), + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + + #[test] + fn access_annotations_from_class_dict_py314() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF063.py"), + &LinterSettings { + unresolved_target_version: PythonVersion::PY314.into(), + ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + }, + )?; + assert_diagnostics!(diagnostics); Ok(()) } @@ -171,7 +239,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -189,7 +257,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -202,7 +270,7 @@ mod tests { Rule::AmbiguousVariableName, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -223,7 +291,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -244,7 +312,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -254,7 +322,7 @@ mod tests { Path::new("ruff/RUF100_1.py"), &settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA, Rule::UnusedImport]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -271,7 +339,7 @@ mod tests { .unwrap(); let diagnostics = test_path(Path::new("ruff/RUF100_2.py"), &settings)?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -285,7 +353,7 @@ mod tests { Rule::UndefinedName, ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -295,7 +363,7 @@ mod tests { Path::new("ruff/RUF100_4.py"), &settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA, Rule::UnusedImport]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -311,7 +379,7 @@ mod tests { ]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -319,12 +387,9 @@ mod tests { fn ruff_noqa_filedirective_unused() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF100_6.py"), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA]) - }, + &settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -332,17 +397,14 @@ mod tests { fn ruff_noqa_filedirective_unused_last_of_many() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF100_7.py"), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rules(vec![ - Rule::UnusedNOQA, - Rule::FStringMissingPlaceholders, - Rule::LineTooLong, - Rule::UnusedVariable, - ]) - }, + &settings::LinterSettings::for_rules(vec![ + Rule::UnusedNOQA, + Rule::FStringMissingPlaceholders, + Rule::LineTooLong, + Rule::UnusedVariable, + ]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -355,7 +417,7 @@ mod tests { ..settings::LinterSettings::for_rule(Rule::InvalidRuleCode) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -373,7 +435,7 @@ mod tests { ..settings::LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UnusedNOQA]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -391,7 +453,7 @@ mod tests { ..settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA]) }, )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -401,7 +463,7 @@ mod tests { Path::new("ruff/flake8_noqa.py"), &settings::LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -411,7 +473,7 @@ mod tests { Path::new("ruff/ruff_noqa_all.py"), &settings::LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -421,7 +483,7 @@ mod tests { Path::new("ruff/ruff_noqa_codes.py"), &settings::LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -431,7 +493,7 @@ mod tests { Path::new("ruff/ruff_noqa_invalid.py"), &settings::LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -441,7 +503,7 @@ mod tests { Path::new("ruff/redirects.py"), &settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationUnion), )?; - assert_messages!(diagnostics); + assert_diagnostics!(diagnostics); Ok(()) } @@ -460,10 +522,10 @@ mod tests { let contents = fs::read_to_string(path)?; let source_file = SourceFileBuilder::new("pyproject.toml", contents).finish(); let messages = lint_pyproject_toml( - source_file, + &source_file, &settings::LinterSettings::for_rule(Rule::InvalidPyprojectToml), ); - assert_messages!(snapshot, messages); + assert_diagnostics!(snapshot, messages); Ok(()) } @@ -473,14 +535,8 @@ mod tests { #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_2.py"))] #[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))] - #[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))] - #[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))] - #[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))] - #[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))] - #[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))] #[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))] #[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))] - #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", @@ -494,7 +550,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } @@ -522,7 +578,7 @@ mod tests { ..settings::LinterSettings::for_rule(rule_code) }, )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs new file mode 100644 index 0000000000000..f314d38c1a907 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs @@ -0,0 +1,185 @@ +use crate::checkers::ast::Checker; +use crate::{FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Expr, ExprCall, ExprSubscript, PythonVersion}; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for uses of `foo.__dict__.get("__annotations__")` or +/// `foo.__dict__["__annotations__"]` on Python 3.10+ and Python < 3.10 when +/// [typing-extensions](https://docs.astral.sh/ruff/settings/#lint_typing-extensions) +/// is enabled. +/// +/// ## Why is this bad? +/// Starting with Python 3.14, directly accessing `__annotations__` via +/// `foo.__dict__.get("__annotations__")` or `foo.__dict__["__annotations__"]` +/// will only return annotations if the class is defined under +/// `from __future__ import annotations`. +/// +/// Therefore, it is better to use dedicated library functions like +/// `annotationlib.get_annotations` (Python 3.14+), `inspect.get_annotations` +/// (Python 3.10+), or `typing_extensions.get_annotations` (for Python < 3.10 if +/// [typing-extensions](https://pypi.org/project/typing-extensions/) is +/// available). +/// +/// The benefits of using these functions include: +/// 1. **Avoiding Undocumented Internals:** They provide a stable, public API, +/// unlike direct `__dict__` access which relies on implementation details. +/// 2. **Forward-Compatibility:** They are designed to handle changes in +/// Python's annotation system across versions, ensuring your code remains +/// robust (e.g., correctly handling the Python 3.14 behavior mentioned +/// above). +/// +/// See [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) +/// for alternatives. +/// +/// ## Example +/// +/// ```python +/// foo.__dict__.get("__annotations__", {}) +/// # or +/// foo.__dict__["__annotations__"] +/// ``` +/// +/// On Python 3.14+, use instead: +/// ```python +/// import annotationlib +/// +/// annotationlib.get_annotations(foo) +/// ``` +/// +/// On Python 3.10+, use instead: +/// ```python +/// import inspect +/// +/// inspect.get_annotations(foo) +/// ``` +/// +/// On Python < 3.10 with [typing-extensions](https://pypi.org/project/typing-extensions/) +/// installed, use instead: +/// ```python +/// import typing_extensions +/// +/// typing_extensions.get_annotations(foo) +/// ``` +/// +/// ## Fix safety +/// +/// No autofix is currently provided for this rule. +/// +/// ## Fix availability +/// +/// No autofix is currently provided for this rule. +/// +/// ## References +/// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) +#[derive(ViolationMetadata)] +pub(crate) struct AccessAnnotationsFromClassDict { + python_version: PythonVersion, +} + +impl Violation for AccessAnnotationsFromClassDict { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; + + #[derive_message_formats] + fn message(&self) -> String { + let suggestion = if self.python_version >= PythonVersion::PY314 { + "annotationlib.get_annotations" + } else if self.python_version >= PythonVersion::PY310 { + "inspect.get_annotations" + } else { + "typing_extensions.get_annotations" + }; + format!("Use `{suggestion}` instead of `__dict__` access") + } +} + +/// RUF063 +pub(crate) fn access_annotations_from_class_dict_with_get(checker: &Checker, call: &ExprCall) { + let python_version = checker.target_version(); + let typing_extensions = checker.settings().typing_extensions; + + // Only apply this rule for Python 3.10 and newer unless `typing-extensions` is enabled. + if python_version < PythonVersion::PY310 && !typing_extensions { + return; + } + + // Expected pattern: foo.__dict__.get("__annotations__" [, ]) + // Here, `call` is the `.get(...)` part. + + // 1. Check that the `call.func` is `get` + let get_attribute = match call.func.as_ref() { + Expr::Attribute(attr) if attr.attr.as_str() == "get" => attr, + _ => return, + }; + + // 2. Check that the `get_attribute.value` is `__dict__` + match get_attribute.value.as_ref() { + Expr::Attribute(attr) if attr.attr.as_str() == "__dict__" => {} + _ => return, + } + + // At this point, we have `foo.__dict__.get`. + + // 3. Check arguments to `.get()`: + // - No keyword arguments. + // - One or two positional arguments. + // - First positional argument must be the string literal "__annotations__". + // - The value of the second positional argument (if present) does not affect the match. + if !call.arguments.keywords.is_empty() || call.arguments.len() > 2 { + return; + } + + let Some(first_arg) = &call.arguments.find_positional(0) else { + return; + }; + + let is_first_arg_correct = first_arg + .as_string_literal_expr() + .is_some_and(|s| s.value.to_str() == "__annotations__"); + + if is_first_arg_correct { + checker.report_diagnostic( + AccessAnnotationsFromClassDict { python_version }, + call.range(), + ); + } +} + +/// RUF063 +pub(crate) fn access_annotations_from_class_dict_by_key( + checker: &Checker, + subscript: &ExprSubscript, +) { + let python_version = checker.target_version(); + let typing_extensions = checker.settings().typing_extensions; + + // Only apply this rule for Python 3.10 and newer unless `typing-extensions` is enabled. + if python_version < PythonVersion::PY310 && !typing_extensions { + return; + } + + // Expected pattern: foo.__dict__["__annotations__"] + + // 1. Check that the slice is a string literal "__annotations__" + if subscript + .slice + .as_string_literal_expr() + .is_none_or(|s| s.value.to_str() != "__annotations__") + { + return; + } + + // 2. Check that the `subscript.value` is `__dict__` + let is_value_correct = subscript + .value + .as_attribute_expr() + .is_some_and(|attr| attr.attr.as_str() == "__dict__"); + + if is_value_correct { + checker.report_diagnostic( + AccessAnnotationsFromClassDict { python_version }, + subscript.range(), + ); + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs index 3457a55d7e31e..1af8f6fcaa9a6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -2,17 +2,17 @@ use std::fmt; use bitflags::bitflags; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, StringLike}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, FString, StringLike, TString}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::checkers::ast::Checker; -use crate::registry::AsRule; -use crate::rules::ruff::rules::confusables::confusable; +use crate::Locator; +use crate::Violation; +use crate::checkers::ast::{Checker, LintContext}; +use crate::preview::is_unicode_to_unicode_confusables_enabled; use crate::rules::ruff::rules::Context; +use crate::rules::ruff::rules::confusables::confusable; use crate::settings::LinterSettings; -use crate::Locator; /// ## What it does /// Checks for ambiguous Unicode characters in strings. @@ -175,13 +175,15 @@ impl Violation for AmbiguousUnicodeCharacterComment { /// RUF003 pub(crate) fn ambiguous_unicode_character_comment( - diagnostics: &mut Vec, + context: &LintContext, locator: &Locator, range: TextRange, settings: &LinterSettings, ) { let text = locator.slice(range); - ambiguous_unicode_character(diagnostics, text, range, Context::Comment, settings); + for candidate in ambiguous_unicode_character(text, range, settings) { + candidate.into_diagnostic(Context::Comment, settings, context); + } } /// RUF001, RUF002 @@ -202,29 +204,22 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like: match part { ast::StringLikePart::String(string_literal) => { let text = checker.locator().slice(string_literal); - let mut diagnostics = Vec::new(); - ambiguous_unicode_character( - &mut diagnostics, - text, - string_literal.range(), - context, - checker.settings, - ); - checker.report_diagnostics(diagnostics); + for candidate in + ambiguous_unicode_character(text, string_literal.range(), checker.settings()) + { + candidate.report_diagnostic(checker, context); + } } ast::StringLikePart::Bytes(_) => {} - ast::StringLikePart::FString(f_string) => { - for literal in f_string.elements.literals() { + ast::StringLikePart::FString(FString { elements, .. }) + | ast::StringLikePart::TString(TString { elements, .. }) => { + for literal in elements.literals() { let text = checker.locator().slice(literal); - let mut diagnostics = Vec::new(); - ambiguous_unicode_character( - &mut diagnostics, - text, - literal.range(), - context, - checker.settings, - ); - checker.report_diagnostics(diagnostics); + for candidate in + ambiguous_unicode_character(text, literal.range(), checker.settings()) + { + candidate.report_diagnostic(checker, context); + } } } } @@ -232,15 +227,15 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like: } fn ambiguous_unicode_character( - diagnostics: &mut Vec, text: &str, range: TextRange, - context: Context, settings: &LinterSettings, -) { +) -> Vec { + let mut candidates = Vec::new(); + // Most of the time, we don't need to check for ambiguous unicode characters at all. if text.is_ascii() { - return; + return candidates; } // Iterate over the "words" in the text. @@ -252,9 +247,7 @@ fn ambiguous_unicode_character( if !word_candidates.is_empty() { if word_flags.is_candidate_word() { for candidate in word_candidates.drain(..) { - if let Some(diagnostic) = candidate.into_diagnostic(context, settings) { - diagnostics.push(diagnostic); - } + candidates.push(candidate); } } word_candidates.clear(); @@ -264,25 +257,23 @@ fn ambiguous_unicode_character( // Check if the boundary character is itself an ambiguous unicode character, in which // case, it's always included as a diagnostic. if !current_char.is_ascii() { - if let Some(representant) = confusable(current_char as u32) - .filter(|representant| settings.preview.is_enabled() || representant.is_ascii()) - { + if let Some(representant) = confusable(current_char as u32).filter(|representant| { + is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii() + }) { let candidate = Candidate::new( TextSize::try_from(relative_offset).unwrap() + range.start(), current_char, representant, ); - if let Some(diagnostic) = candidate.into_diagnostic(context, settings) { - diagnostics.push(diagnostic); - } + candidates.push(candidate); } } } else if current_char.is_ascii() { // The current word contains at least one ASCII character. word_flags |= WordFlags::ASCII; - } else if let Some(representant) = confusable(current_char as u32) - .filter(|representant| settings.preview.is_enabled() || representant.is_ascii()) - { + } else if let Some(representant) = confusable(current_char as u32).filter(|representant| { + is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii() + }) { // The current word contains an ambiguous unicode character. word_candidates.push(Candidate::new( TextSize::try_from(relative_offset).unwrap() + range.start(), @@ -299,13 +290,13 @@ fn ambiguous_unicode_character( if !word_candidates.is_empty() { if word_flags.is_candidate_word() { for candidate in word_candidates.drain(..) { - if let Some(diagnostic) = candidate.into_diagnostic(context, settings) { - diagnostics.push(diagnostic); - } + candidates.push(candidate); } } word_candidates.clear(); } + + candidates } bitflags! { @@ -351,34 +342,71 @@ impl Candidate { } } - fn into_diagnostic(self, context: Context, settings: &LinterSettings) -> Option { + fn into_diagnostic( + self, + context: Context, + settings: &LinterSettings, + lint_context: &LintContext, + ) { if !settings.allowed_confusables.contains(&self.confusable) { let char_range = TextRange::at(self.offset, self.confusable.text_len()); - let diagnostic = Diagnostic::new::( - match context { - Context::String => AmbiguousUnicodeCharacterString { + match context { + Context::String => lint_context.report_diagnostic_if_enabled( + AmbiguousUnicodeCharacterString { confusable: self.confusable, representant: self.representant, - } - .into(), - Context::Docstring => AmbiguousUnicodeCharacterDocstring { + }, + char_range, + ), + Context::Docstring => lint_context.report_diagnostic_if_enabled( + AmbiguousUnicodeCharacterDocstring { confusable: self.confusable, representant: self.representant, - } - .into(), - Context::Comment => AmbiguousUnicodeCharacterComment { + }, + char_range, + ), + Context::Comment => lint_context.report_diagnostic_if_enabled( + AmbiguousUnicodeCharacterComment { confusable: self.confusable, representant: self.representant, - } - .into(), - }, - char_range, - ); - if settings.rules.enabled(diagnostic.kind.rule()) { - return Some(diagnostic); - } + }, + char_range, + ), + }; + } + } + + fn report_diagnostic(self, checker: &Checker, context: Context) { + if !checker + .settings() + .allowed_confusables + .contains(&self.confusable) + { + let char_range = TextRange::at(self.offset, self.confusable.text_len()); + match context { + Context::String => checker.report_diagnostic_if_enabled( + AmbiguousUnicodeCharacterString { + confusable: self.confusable, + representant: self.representant, + }, + char_range, + ), + Context::Docstring => checker.report_diagnostic_if_enabled( + AmbiguousUnicodeCharacterDocstring { + confusable: self.confusable, + representant: self.representant, + }, + char_range, + ), + Context::Comment => checker.report_diagnostic_if_enabled( + AmbiguousUnicodeCharacterComment { + confusable: self.confusable, + representant: self.representant, + }, + char_range, + ), + }; } - None } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs index cea4a135333fa..41f72bbdc5ea7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for uses of `assert expression, print(message)`. @@ -62,19 +62,19 @@ pub(crate) fn assert_with_print_message(checker: &Checker, stmt: &ast::StmtAsser if semantic.match_builtin_expr(&call.func, "print") { // This is the confirmed rule condition - let mut diagnostic = Diagnostic::new(AssertWithPrintMessage, call.range()); + let mut diagnostic = checker.report_diagnostic(AssertWithPrintMessage, call.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( checker.generator().stmt(&Stmt::Assert(ast::StmtAssert { test: stmt.test.clone(), msg: print_arguments::to_expr(&call.arguments, checker).map(Box::new), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), // We have to replace the entire statement, // as the `print` could be empty and thus `call.range()` // will cease to exist. stmt.range(), ))); - checker.report_diagnostic(diagnostic); } } } @@ -89,9 +89,9 @@ pub(crate) fn assert_with_print_message(checker: &Checker, stmt: &ast::StmtAsser mod print_arguments { use itertools::Itertools; use ruff_python_ast::{ - Arguments, ConversionFlag, Expr, ExprFString, FString, FStringElement, FStringElements, - FStringExpressionElement, FStringFlags, FStringLiteralElement, FStringValue, StringLiteral, - StringLiteralFlags, + Arguments, ConversionFlag, Expr, ExprFString, FString, FStringFlags, FStringValue, + InterpolatedElement, InterpolatedStringElement, InterpolatedStringElements, + InterpolatedStringLiteralElement, StringLiteral, StringLiteralFlags, }; use ruff_text_size::TextRange; @@ -104,16 +104,17 @@ mod print_arguments { /// `FStringLiteralElement`. /// - if the expression is an f-string, the elements will be returned as-is. /// - otherwise, the expression will be wrapped in a `FStringExpressionElement`. - fn expr_to_fstring_elements(expr: &Expr) -> Vec { + fn expr_to_fstring_elements(expr: &Expr) -> Vec { match expr { // If the expression is a string literal, convert each part to a `FStringLiteralElement`. Expr::StringLiteral(string) => string .value .iter() .map(|part| { - FStringElement::Literal(FStringLiteralElement { + InterpolatedStringElement::Literal(InterpolatedStringLiteralElement { value: part.value.clone(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) }) .collect(), @@ -123,13 +124,16 @@ mod print_arguments { // Otherwise, return the expression as a single `FStringExpressionElement` wrapping // the expression. - expr => vec![FStringElement::Expression(FStringExpressionElement { - expression: Box::new(expr.clone()), - debug_text: None, - conversion: ConversionFlag::None, - format_spec: None, - range: TextRange::default(), - })], + expr => vec![InterpolatedStringElement::Interpolation( + InterpolatedElement { + expression: Box::new(expr.clone()), + debug_text: None, + conversion: ConversionFlag::None, + format_spec: None, + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + }, + )], } } @@ -141,15 +145,16 @@ mod print_arguments { /// checking if the `sep` and `args` arguments to `print` are all string /// literals. fn fstring_elements_to_string_literals<'a>( - mut elements: impl ExactSizeIterator, + mut elements: impl ExactSizeIterator, flags: StringLiteralFlags, ) -> Option> { elements.try_fold(Vec::with_capacity(elements.len()), |mut acc, element| { - if let FStringElement::Literal(literal) = element { + if let InterpolatedStringElement::Literal(literal) = element { acc.push(StringLiteral { value: literal.value.clone(), flags, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Some(acc) } else { @@ -163,8 +168,8 @@ mod print_arguments { /// This function will return [`None`] if any of the arguments are not string literals, /// or if there are no arguments at all. fn args_to_string_literal_expr<'a>( - args: impl ExactSizeIterator>, - sep: impl ExactSizeIterator, + args: impl ExactSizeIterator>, + sep: impl ExactSizeIterator, flags: StringLiteralFlags, ) -> Option { // If there are no arguments, short-circuit and return `None` @@ -207,6 +212,7 @@ mod print_arguments { value: combined_string.into(), flags, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) } @@ -221,8 +227,8 @@ mod print_arguments { /// Also note that the iterator arguments of this function are consumed, /// as opposed to the references taken by [`args_to_string_literal_expr`]. fn args_to_fstring_expr( - mut args: impl ExactSizeIterator>, - sep: impl ExactSizeIterator, + mut args: impl ExactSizeIterator>, + sep: impl ExactSizeIterator, flags: FStringFlags, ) -> Option { // If there are no arguments, short-circuit and return `None` @@ -237,11 +243,13 @@ mod print_arguments { Some(Expr::FString(ExprFString { value: FStringValue::single(FString { - elements: FStringElements::from(fstring_elements), + elements: InterpolatedStringElements::from(fstring_elements), flags, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) } @@ -274,10 +282,13 @@ mod print_arguments { ) .map(expr_to_fstring_elements) .unwrap_or_else(|| { - vec![FStringElement::Literal(FStringLiteralElement { - range: TextRange::default(), - value: " ".into(), - })] + vec![InterpolatedStringElement::Literal( + InterpolatedStringLiteralElement { + range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), + value: " ".into(), + }, + )] }); let args = arguments diff --git a/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs b/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs index 69d098457af49..1de48b6454a9b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assignment_in_assert.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Binding; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -58,24 +58,26 @@ impl Violation for AssignmentInAssert { } /// RUF018 -pub(crate) fn assignment_in_assert(checker: &Checker, binding: &Binding) -> Option { +pub(crate) fn assignment_in_assert(checker: &Checker, binding: &Binding) { if !binding.in_assert_statement() { - return None; + return; } let semantic = checker.semantic(); - let parent_expression = binding.expression(semantic)?.as_named_expr()?; + let Some(parent_expression) = binding + .expression(semantic) + .and_then(|expr| expr.as_named_expr()) + else { + return; + }; if binding .references() .all(|reference| semantic.reference(reference).in_assert_statement()) { - return None; + return; } - Some(Diagnostic::new( - AssignmentInAssert, - parent_expression.range(), - )) + checker.report_diagnostic(AssignmentInAssert, parent_expression.range()); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs index ce96afc093c0b..17657e6f9cd39 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs @@ -1,12 +1,12 @@ use std::fmt; use ast::Stmt; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::{analyze::typing, Scope, SemanticModel}; +use ruff_python_semantic::{Scope, SemanticModel, analyze::typing}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -67,9 +67,9 @@ impl Violation for AsyncioDanglingTask { } /// RUF006 -pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Option { +pub(crate) fn asyncio_dangling_task(checker: &Checker, expr: &Expr, semantic: &SemanticModel) { let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return None; + return; }; // Ex) `asyncio.create_task(...)` @@ -81,15 +81,14 @@ pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Op _ => None, }) { - return Some(Diagnostic::new( + checker.report_diagnostic( AsyncioDanglingTask { expr: "asyncio".to_string(), method, }, expr.range(), - )); - } - + ); + } else // Ex) `loop = ...; loop.create_task(...)` if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() { if attr == "create_task" { @@ -103,18 +102,17 @@ pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Op ] ) }) { - return Some(Diagnostic::new( + checker.report_diagnostic( AsyncioDanglingTask { expr: name.id.to_string(), method: Method::CreateTask, }, expr.range(), - )); + ); } } } } - None } /// RUF006 @@ -153,18 +151,14 @@ pub(crate) fn asyncio_dangling_binding(scope: &Scope, checker: &Checker) { continue; }; - let diagnostic = match semantic.statement(source) { + match semantic.statement(source) { Stmt::Assign(ast::StmtAssign { value, targets, .. }) if targets.len() == 1 => { - asyncio_dangling_task(value, semantic) + asyncio_dangling_task(checker, value, semantic); } Stmt::AnnAssign(ast::StmtAnnAssign { value: Some(value), .. - }) => asyncio_dangling_task(value, semantic), - _ => None, - }; - - if let Some(diagnostic) = diagnostic { - checker.report_diagnostic(diagnostic); + }) => asyncio_dangling_task(checker, value, semantic), + _ => {} } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs index cce3d70a0ae90..1f4a553714f3a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/class_with_mixed_type_vars.rs @@ -1,18 +1,18 @@ use rustc_hash::FxHashSet; use std::iter; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ Arguments, Expr, ExprStarred, ExprSubscript, ExprTuple, StmtClassDef, TypeParams, }; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; use crate::rules::pyupgrade::rules::pep695::{ - expr_name_to_type_var, find_generic, DisplayTypeVars, TypeParamKind, TypeVar, + DisplayTypeVars, TypeParamKind, TypeVar, expr_name_to_type_var, find_generic, }; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -103,7 +103,7 @@ pub(crate) fn class_with_mixed_type_vars(checker: &Checker, class_def: &StmtClas return; }; - let mut diagnostic = Diagnostic::new(ClassWithMixedTypeVars, generic_base.range); + let mut diagnostic = checker.report_diagnostic(ClassWithMixedTypeVars, generic_base.range); diagnostic.try_set_optional_fix(|| { convert_type_vars( @@ -114,8 +114,6 @@ pub(crate) fn class_with_mixed_type_vars(checker: &Checker, class_def: &StmtClas checker, ) }); - - checker.report_diagnostic(diagnostic); } fn typing_generic_base_and_arguments<'a>( @@ -159,8 +157,13 @@ fn convert_type_vars( source, }; - let remove_generic_base = - remove_argument(generic_base, class_arguments, Parentheses::Remove, source)?; + let remove_generic_base = remove_argument( + generic_base, + class_arguments, + Parentheses::Remove, + source, + checker.comment_ranges(), + )?; let replace_type_params = Edit::range_replacement(new_type_params.to_string(), type_params.range); diff --git a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs index 6c9239ee47cc0..f8cd2b9c36238 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprContext, Operator}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of the `+` operator to concatenate collections. @@ -33,6 +33,12 @@ use crate::fix::snippet::SourceCodeSnippet; /// bar = [1, *foo, 5, 6] /// ``` /// +/// ## Fix safety +/// +/// The fix is always marked as unsafe because the `+` operator uses the `__add__` magic method and +/// `*`-unpacking uses the `__iter__` magic method. Both of these could have custom +/// implementations, causing the fix to change program behaviour. +/// /// ## References /// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) /// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) @@ -72,6 +78,7 @@ fn make_splat_elts( value: Box::from(splat_element.clone()), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; let splat = node.into(); if splat_at_left { @@ -89,34 +96,31 @@ enum Type { } /// Recursively merge all the tuples and lists in the expression. -fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(Expr, Type)> { +fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> { let Expr::BinOp(ast::ExprBinOp { left, op: Operator::Add, right, range: _, + node_index: _, }) = expr else { return None; }; let new_left = match left.as_ref() { - Expr::BinOp(ast::ExprBinOp { .. }) => { - match concatenate_expressions(left, should_support_slices) { - Some((new_left, _)) => new_left, - None => *left.clone(), - } - } + Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(left) { + Some((new_left, _)) => new_left, + None => *left.clone(), + }, _ => *left.clone(), }; let new_right = match right.as_ref() { - Expr::BinOp(ast::ExprBinOp { .. }) => { - match concatenate_expressions(right, should_support_slices) { - Some((new_right, _)) => new_right, - None => *right.clone(), - } - } + Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(right) { + Some((new_right, _)) => new_right, + None => *right.clone(), + }, _ => *right.clone(), }; @@ -144,9 +148,7 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<( make_splat_elts(splat_element, other_elements, splat_at_left) } // Subscripts are also considered safe-ish to splat if the indexer is a slice. - Expr::Subscript(ast::ExprSubscript { slice, .. }) - if should_support_slices && matches!(&**slice, Expr::Slice(_)) => - { + Expr::Subscript(ast::ExprSubscript { slice, .. }) if matches!(&**slice, Expr::Slice(_)) => { make_splat_elts(splat_element, other_elements, splat_at_left) } // If the splat element is itself a list/tuple, insert them in the other list/tuple. @@ -164,12 +166,14 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<( elts: new_elts, ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), } .into(), Type::Tuple => ast::ExprTuple { elts: new_elts, ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, } .into(), @@ -191,9 +195,7 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) { return; } - let should_support_slices = checker.settings.preview.is_enabled(); - - let Some((new_expr, type_)) = concatenate_expressions(expr, should_support_slices) else { + let Some((new_expr, type_)) = concatenate_expressions(expr) else { return; }; @@ -202,7 +204,7 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) { Type::Tuple => format!("({})", checker.generator().expr(&new_expr)), Type::List => checker.generator().expr(&new_expr), }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( CollectionLiteralConcatenation { expression: SourceCodeSnippet::new(contents.clone()), }, @@ -219,5 +221,4 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) { expr.range(), ))); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs b/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs index 0f4e6797d02c2..9744cff0ae8d5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/dataclass_enum.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::StmtClassDef; use ruff_python_semantic::analyze::class::is_enumeration; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{dataclass_kind, DataclassKind}; +use crate::rules::ruff::helpers::{DataclassKind, dataclass_kind}; /// ## What it does /// Checks for enum classes which are also decorated with `@dataclass`. @@ -70,7 +70,5 @@ pub(crate) fn dataclass_enum(checker: &Checker, class_def: &StmtClassDef) { return; } - let diagnostic = Diagnostic::new(DataclassEnum, decorator.range); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(DataclassEnum, decorator.range); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs index a8d29a9741288..a3bbaa608efb3 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/decimal_from_float_literal.rs @@ -1,13 +1,13 @@ use std::fmt; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_codegen::Stylist; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `Decimal` calls passing a float literal. @@ -28,7 +28,7 @@ use crate::Locator; /// num = Decimal("1.2345") /// ``` /// -/// ## Fix Safety +/// ## Fix safety /// This rule's fix is marked as unsafe because it changes the underlying value /// of the `Decimal` instance that is constructed. This can lead to unexpected /// behavior if your program relies on the previous value (whether deliberately or not). @@ -60,10 +60,14 @@ pub(crate) fn decimal_from_float_literal_syntax(checker: &Checker, call: &ast::E matches!(qualified_name.segments(), ["decimal", "Decimal"]) }) { - let diagnostic = Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix( - fix_float_literal(arg.range(), float, checker.locator(), checker.stylist()), - ); - checker.report_diagnostic(diagnostic); + checker + .report_diagnostic(DecimalFromFloatLiteral, arg.range()) + .set_fix(fix_float_literal( + arg.range(), + float, + checker.locator(), + checker.stylist(), + )); } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs index fc5cc0aaa64ba..355aa47519e14 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs @@ -1,16 +1,17 @@ use anyhow::Result; use ast::Keyword; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_constant; use ruff_python_ast::{self as ast, Expr}; +use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; +use crate::Locator; use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; +use crate::fix::edits::{Parentheses, remove_argument}; use crate::fix::snippet::SourceCodeSnippet; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for incorrect usages of `default_factory` as a keyword argument when @@ -100,14 +101,15 @@ pub(crate) fn default_factory_kwarg(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( DefaultFactoryKwarg { default_factory: SourceCodeSnippet::from_str(checker.locator().slice(keyword)), }, call.range(), ); - diagnostic.try_set_fix(|| convert_to_positional(call, keyword, checker.locator())); - checker.report_diagnostic(diagnostic); + diagnostic.try_set_fix(|| { + convert_to_positional(call, keyword, checker.locator(), checker.comment_ranges()) + }); } /// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`). @@ -132,6 +134,7 @@ fn convert_to_positional( call: &ast::ExprCall, default_factory: &Keyword, locator: &Locator, + comment_ranges: &CommentRanges, ) -> Result { if call.arguments.len() == 1 { // Ex) `defaultdict(default_factory=list)` @@ -148,6 +151,7 @@ fn convert_to_positional( &call.arguments, Parentheses::Preserve, locator.contents(), + comment_ranges, )?; // Second, insert the value as the first positional argument. diff --git a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index 00f7417aab974..e3af51eee2367 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -1,17 +1,19 @@ -use anyhow::{bail, Result}; +use std::fmt::Display; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, Arguments, Expr}; -use ruff_python_codegen::Stylist; +use anyhow::Result; + +use libcst_native::{LeftParen, ParenthesizedNode, RightParen}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Expr, OperatorPrecedence}; +use ruff_python_parser::TokenKind; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::cst::helpers::space; use crate::cst::matchers::{ - match_call_mut, match_formatted_string, match_formatted_string_expression, match_name, - transform_expression, + match_call_mut, match_formatted_string, match_formatted_string_expression, transform_expression, }; -use crate::Locator; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `str()`, `repr()`, and `ascii()` as explicit type @@ -40,25 +42,27 @@ use crate::Locator; #[derive(ViolationMetadata)] pub(crate) struct ExplicitFStringTypeConversion; -impl AlwaysFixableViolation for ExplicitFStringTypeConversion { +impl Violation for ExplicitFStringTypeConversion { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Use explicit conversion flag".to_string() } - fn fix_title(&self) -> String { - "Replace with conversion flag".to_string() + fn fix_title(&self) -> Option { + Some("Replace with conversion flag".to_string()) } } /// RUF010 pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &ast::FString) { for (index, element) in f_string.elements.iter().enumerate() { - let Some(ast::FStringExpressionElement { + let Some(ast::InterpolatedElement { expression, conversion, .. - }) = element.as_expression() + }) = element.as_interpolation() else { continue; }; @@ -68,83 +72,142 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &as continue; } - let Expr::Call(ast::ExprCall { - func, - arguments: - Arguments { - args, - keywords, - range: _, - }, - .. - }) = expression.as_ref() - else { + let Expr::Call(call) = expression.as_ref() else { continue; }; - // Can't be a conversion otherwise. - if !keywords.is_empty() { + let Some(conversion) = checker + .semantic() + .resolve_builtin_symbol(&call.func) + .and_then(Conversion::from_str) + else { continue; - } + }; + let arg = match conversion { + // Handles the cases: `f"{str(object=arg)}"` and `f"{str(arg)}"` + Conversion::Str if call.arguments.len() == 1 => { + let Some(arg) = call.arguments.find_argument_value("object", 0) else { + continue; + }; + arg + } + Conversion::Str | Conversion::Repr | Conversion::Ascii => { + // Can't be a conversion otherwise. + if !call.arguments.keywords.is_empty() { + continue; + } - // Can't be a conversion otherwise. - let [arg] = &**args else { - continue; + // Can't be a conversion otherwise. + let [arg] = call.arguments.args.as_ref() else { + continue; + }; + arg + } }; - // Avoid attempting to rewrite, e.g., `f"{str({})}"`; the curly braces are problematic. - if matches!( - arg, - Expr::Dict(_) | Expr::Set(_) | Expr::DictComp(_) | Expr::SetComp(_) - ) { - continue; + // Suppress lint for starred expressions. + if arg.is_starred_expr() { + return; } - if !checker - .semantic() - .resolve_builtin_symbol(func) - .is_some_and(|builtin| matches!(builtin, "str" | "repr" | "ascii")) + let mut diagnostic = + checker.report_diagnostic(ExplicitFStringTypeConversion, expression.range()); + + // Don't support fixing f-string with debug text. + if element + .as_interpolation() + .is_some_and(|interpolation| interpolation.debug_text.is_some()) { - continue; + return; } - let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, expression.range()); diagnostic.try_set_fix(|| { - convert_call_to_conversion_flag(f_string, index, checker.locator(), checker.stylist()) + convert_call_to_conversion_flag(checker, conversion, f_string, index, arg) }); - checker.report_diagnostic(diagnostic); } } /// Generate a [`Fix`] to replace an explicit type conversion with a conversion flag. fn convert_call_to_conversion_flag( + checker: &Checker, + conversion: Conversion, f_string: &ast::FString, index: usize, - locator: &Locator, - stylist: &Stylist, + arg: &Expr, ) -> Result { - let source_code = locator.slice(f_string); - transform_expression(source_code, stylist, |mut expression| { + let source_code = checker.locator().slice(f_string); + transform_expression(source_code, checker.stylist(), |mut expression| { let formatted_string = match_formatted_string(&mut expression)?; // Replace the formatted call expression at `index` with a conversion flag. let formatted_string_expression = match_formatted_string_expression(&mut formatted_string.parts[index])?; let call = match_call_mut(&mut formatted_string_expression.expression)?; - let name = match_name(&call.func)?; - match name.value { - "str" => { - formatted_string_expression.conversion = Some("s"); - } - "repr" => { - formatted_string_expression.conversion = Some("r"); - } - "ascii" => { - formatted_string_expression.conversion = Some("a"); - } - _ => bail!("Unexpected function call: `{:?}`", name.value), + + formatted_string_expression.conversion = Some(conversion.as_str()); + + if starts_with_brace(checker, arg) { + formatted_string_expression.whitespace_before_expression = space(); } - formatted_string_expression.expression = call.args[0].value.clone(); + + formatted_string_expression.expression = if needs_paren(OperatorPrecedence::from_expr(arg)) + { + call.args[0] + .value + .clone() + .with_parens(LeftParen::default(), RightParen::default()) + } else { + call.args[0].value.clone() + }; + Ok(expression) }) .map(|output| Fix::safe_edit(Edit::range_replacement(output, f_string.range()))) } + +fn starts_with_brace(checker: &Checker, arg: &Expr) -> bool { + checker + .tokens() + .in_range(arg.range()) + .iter() + // Skip the trivia tokens + .find(|token| !token.kind().is_trivia()) + .is_some_and(|token| matches!(token.kind(), TokenKind::Lbrace)) +} + +fn needs_paren(precedence: OperatorPrecedence) -> bool { + precedence <= OperatorPrecedence::Lambda +} + +/// Represents the three built-in Python conversion functions that can be replaced +/// with f-string conversion flags. +#[derive(Copy, Clone)] +enum Conversion { + Ascii, + Str, + Repr, +} + +impl Conversion { + fn from_str(value: &str) -> Option { + Some(match value { + "ascii" => Self::Ascii, + "str" => Self::Str, + "repr" => Self::Repr, + _ => return None, + }) + } + + fn as_str(self) -> &'static str { + match self { + Conversion::Ascii => "a", + Conversion::Str => "s", + Conversion::Repr => "r", + } + } +} + +impl Display for Conversion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs index 8dddb24d8c1bb..8e5596569440b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs @@ -1,11 +1,12 @@ -use crate::checkers::ast::Checker; -use crate::fix::edits::{remove_argument, Parentheses}; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{helpers::Truthiness, Expr, ExprAttribute}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Expr, ExprAttribute, helpers::Truthiness}; use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; +use crate::checkers::ast::Checker; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Applicability, Fix, FixAvailability, Violation}; + /// ## What it does /// Checks for `dict.get(key, falsy_value)` calls in boolean test positions. /// @@ -27,21 +28,34 @@ use ruff_text_size::Ranged; /// ``` /// /// ## Fix safety -/// This rule's fix is marked as safe, unless the `dict.get()` call contains comments between arguments. +/// +/// This rule's fix is marked as safe, unless the `dict.get()` call contains comments between +/// arguments that will be deleted. +/// +/// ## Fix availability +/// +/// This rule's fix is unavailable in cases where invalid arguments are provided to `dict.get`. As +/// shown in the [documentation], `dict.get` takes two positional-only arguments, so invalid cases +/// are identified by the presence of more than two arguments or any keyword arguments. +/// +/// [documentation]: https://docs.python.org/3.13/library/stdtypes.html#dict.get #[derive(ViolationMetadata)] pub(crate) struct FalsyDictGetFallback; -impl AlwaysFixableViolation for FalsyDictGetFallback { +impl Violation for FalsyDictGetFallback { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.".to_string() } - fn fix_title(&self) -> String { - "Remove falsy fallback from `dict.get()`".to_string() + fn fix_title(&self) -> Option { + Some("Remove falsy fallback from `dict.get()`".to_string()) } } +/// RUF056 pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) { let semantic = checker.semantic(); @@ -86,7 +100,17 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new(FalsyDictGetFallback, fallback_arg.range()); + let mut diagnostic = checker.report_diagnostic(FalsyDictGetFallback, fallback_arg.range()); + + // All arguments to `dict.get` are positional-only. + if !call.arguments.keywords.is_empty() { + return; + } + + // And there are only two of them, at most. + if call.arguments.args.len() > 2 { + return; + } let comment_ranges = checker.comment_ranges(); @@ -103,9 +127,8 @@ pub(crate) fn falsy_dict_get_fallback(checker: &Checker, expr: &Expr) { &call.arguments, Parentheses::Preserve, checker.locator().contents(), + checker.comment_ranges(), ) .map(|edit| Fix::applicable_edit(edit, applicability)) }); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 71e6331d655bd..2e1045ba7662e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -1,17 +1,18 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::typing::{ is_immutable_annotation, is_immutable_func, is_immutable_newtype_call, }; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{ - dataclass_kind, is_class_var_annotation, is_dataclass_field, is_descriptor_class, - AttrsAutoAttribs, DataclassKind, +use crate::rules::ruff::helpers::{ + AttrsAutoAttribs, DataclassKind, dataclass_kind, is_class_var_annotation, is_dataclass_field, + is_descriptor_class, is_frozen_dataclass, }; /// ## What it does @@ -107,7 +108,7 @@ pub(crate) fn function_call_in_dataclass_default(checker: &Checker, class_def: & }; let extend_immutable_calls: Vec = checker - .settings + .settings() .flake8_bugbear .extend_immutable_calls .iter() @@ -143,6 +144,7 @@ pub(crate) fn function_call_in_dataclass_default(checker: &Checker, class_def: & || func.as_name_expr().is_some_and(|name| { is_immutable_newtype_call(name, checker.semantic(), &extend_immutable_calls) }) + || is_frozen_dataclass_instantiation(func, semantic) { continue; } @@ -150,9 +152,7 @@ pub(crate) fn function_call_in_dataclass_default(checker: &Checker, class_def: & let kind = FunctionCallInDataclassDefaultArgument { name: UnqualifiedName::from_expr(func).map(|name| name.to_string()), }; - let diagnostic = Diagnostic::new(kind, expr.range()); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(kind, expr.range()); } } @@ -162,3 +162,19 @@ fn any_annotated(class_body: &[Stmt]) -> bool { .iter() .any(|stmt| matches!(stmt, Stmt::AnnAssign(..))) } + +/// Checks that the passed function is an instantiation of the class, +/// retrieves the ``StmtClassDef`` and verifies that it is a frozen dataclass +fn is_frozen_dataclass_instantiation(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.lookup_attribute(func).is_some_and(|id| { + let binding = &semantic.binding(id); + let Some(Stmt::ClassDef(class_def)) = binding.statement(semantic) else { + return false; + }; + + let Some((_, dataclass_decorator)) = dataclass_kind(class_def, semantic) else { + return false; + }; + is_frozen_dataclass(dataclass_decorator, semantic) + }) +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs deleted file mode 100644 index b3c813186a03d..0000000000000 --- a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs +++ /dev/null @@ -1,211 +0,0 @@ -use ruff_python_ast::helpers::{map_callable, map_subscript, Truthiness}; -use ruff_python_ast::{self as ast, Expr, ExprCall}; -use ruff_python_semantic::{analyze, BindingKind, Modules, SemanticModel}; - -/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`. -/// -/// While `__slots__` is typically defined via a tuple, Python accepts any iterable and, in -/// particular, allows the use of a dictionary to define the attribute names (as keys) and -/// docstrings (as values). -pub(super) fn is_special_attribute(value: &Expr) -> bool { - if let Expr::Name(ast::ExprName { id, .. }) = value { - matches!( - id.as_str(), - "__slots__" | "__dict__" | "__weakref__" | "__annotations__" - ) - } else { - false - } -} - -/// Returns `true` if the given [`Expr`] is a stdlib `dataclasses.field` call. -fn is_stdlib_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(func) - .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"])) -} - -/// Returns `true` if the given [`Expr`] is a call to `attr.ib()` or `attrs.field()`. -fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool { - semantic - .resolve_qualified_name(func) - .is_some_and(|qualified_name| { - matches!( - qualified_name.segments(), - ["attrs", "field" | "Factory"] | ["attr", "ib"] - ) - }) -} - -/// Return `true` if `func` represents a `field()` call corresponding to the `dataclass_kind` variant passed in. -/// -/// I.e., if `DataclassKind::Attrs` is passed in, -/// return `true` if `func` represents a call to `attr.ib()` or `attrs.field()`; -/// if `DataclassKind::Stdlib` is passed in, -/// return `true` if `func` represents a call to `dataclasse.field()`. -pub(super) fn is_dataclass_field( - func: &Expr, - semantic: &SemanticModel, - dataclass_kind: DataclassKind, -) -> bool { - match dataclass_kind { - DataclassKind::Attrs(..) => is_attrs_field(func, semantic), - DataclassKind::Stdlib => is_stdlib_dataclass_field(func, semantic), - } -} - -/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. -pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { - if !semantic.seen_typing() { - return false; - } - - // ClassVar can be used either with a subscript `ClassVar[...]` or without (the type is - // inferred). - semantic.match_typing_expr(map_subscript(annotation), "ClassVar") -} - -/// Returns `true` if the given [`Expr`] is a `typing.Final` annotation. -pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { - if !semantic.seen_typing() { - return false; - } - - // Final can be used either with a subscript `Final[...]` or without (the type is - // inferred). - semantic.match_typing_expr(map_subscript(annotation), "Final") -} - -/// Values that [`attrs`'s `auto_attribs`][1] accept. -/// -/// [1]: https://www.attrs.org/en/stable/api.html#attrs.define -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum AttrsAutoAttribs { - /// `a: str = ...` are automatically converted to fields. - True, - /// Only `attrs.field()`/`attr.ib()` calls are considered fields. - False, - /// `True` if any attributes are annotated (and no unannotated `attrs.field`s are found). - /// `False` otherwise. - None, - /// The provided value is not a literal. - Unknown, -} - -/// Enumeration of various kinds of dataclasses recognised by Ruff -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) enum DataclassKind { - /// dataclasses created by the stdlib `dataclasses` module - Stdlib, - /// dataclasses created by the third-party `attrs` library - Attrs(AttrsAutoAttribs), -} - -/// Return the kind of dataclass this class definition is (stdlib or `attrs`), -/// or `None` if the class is not a dataclass. -pub(super) fn dataclass_kind<'a>( - class_def: &'a ast::StmtClassDef, - semantic: &SemanticModel, -) -> Option<(DataclassKind, &'a ast::Decorator)> { - if !(semantic.seen_module(Modules::DATACLASSES) || semantic.seen_module(Modules::ATTRS)) { - return None; - } - - for decorator in &class_def.decorator_list { - let Some(qualified_name) = - semantic.resolve_qualified_name(map_callable(&decorator.expression)) - else { - continue; - }; - - match qualified_name.segments() { - ["attrs", func @ ("define" | "frozen" | "mutable")] | ["attr", func @ "s"] => { - // `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`, - // whereas `@attr.s` implicitly sets `auto_attribs=False`. - // https://www.attrs.org/en/stable/api.html#attrs.define - // https://www.attrs.org/en/stable/api-attr.html#attr.s - let Expr::Call(ExprCall { arguments, .. }) = &decorator.expression else { - let auto_attribs = if *func == "s" { - AttrsAutoAttribs::False - } else { - AttrsAutoAttribs::None - }; - - return Some((DataclassKind::Attrs(auto_attribs), decorator)); - }; - - let Some(auto_attribs) = arguments.find_keyword("auto_attribs") else { - return Some((DataclassKind::Attrs(AttrsAutoAttribs::None), decorator)); - }; - - let auto_attribs = match Truthiness::from_expr(&auto_attribs.value, |id| { - semantic.has_builtin_binding(id) - }) { - // `auto_attribs` requires an exact `True` to be true - Truthiness::True => AttrsAutoAttribs::True, - // Or an exact `None` to auto-detect. - Truthiness::None => AttrsAutoAttribs::None, - // Otherwise, anything else (even a truthy value, like `1`) is considered `False`. - Truthiness::Truthy | Truthiness::False | Truthiness::Falsey => { - AttrsAutoAttribs::False - } - // Unless, of course, we can't determine the value. - Truthiness::Unknown => AttrsAutoAttribs::Unknown, - }; - - return Some((DataclassKind::Attrs(auto_attribs), decorator)); - } - ["dataclasses", "dataclass"] => return Some((DataclassKind::Stdlib, decorator)), - _ => continue, - } - } - - None -} - -/// Returns `true` if the given class has "default copy" semantics. -/// -/// For example, Pydantic `BaseModel` and `BaseSettings` subclasses copy attribute defaults on -/// instance creation. As such, the use of mutable default values is safe for such classes. -pub(super) fn has_default_copy_semantics( - class_def: &ast::StmtClassDef, - semantic: &SemanticModel, -) -> bool { - analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| { - matches!( - qualified_name.segments(), - [ - "pydantic", - "BaseModel" | "RootModel" | "BaseSettings" | "BaseConfig" - ] | ["pydantic", "generics", "GenericModel"] - | [ - "pydantic", - "v1", - "BaseModel" | "BaseSettings" | "BaseConfig" - ] - | ["pydantic", "v1", "generics", "GenericModel"] - | ["pydantic_settings", "BaseSettings"] - | ["msgspec", "Struct"] - | ["sqlmodel", "SQLModel"] - ) - }) -} - -/// Returns `true` if the given function is an instantiation of a class that implements the -/// descriptor protocol. -/// -/// See: -pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool { - semantic.lookup_attribute(func).is_some_and(|id| { - let BindingKind::ClassDefinition(scope_id) = semantic.binding(id).kind else { - return false; - }; - - // Look for `__get__`, `__set__`, and `__delete__` methods. - ["__get__", "__set__", "__delete__"].iter().any(|method| { - semantic.scopes[scope_id] - .get(method) - .is_some_and(|id| semantic.binding(id).kind.is_function_definition()) - }) - }) -} diff --git a/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs b/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs index 6243c11dad6a1..969aff55b24a6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/if_key_in_dict_del.rs @@ -1,9 +1,10 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{CmpOp, Expr, ExprName, ExprSubscript, Stmt, StmtIf}; use ruff_python_semantic::analyze::typing; +use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; + type Key = Expr; type Dict = ExprName; @@ -65,9 +66,9 @@ pub(crate) fn if_key_in_dict_del(checker: &Checker, stmt: &StmtIf) { let fix = replace_with_dict_pop_fix(checker, stmt, test_dict, test_key); - let diagnostic = Diagnostic::new(IfKeyInDictDel, delete.range); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(IfKeyInDictDel, delete.range) + .set_fix(fix); } fn extract_dict_and_key_from_test(test: &Expr) -> Option<(&Dict, &Key)> { diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs index 722086e96e399..47e3fc4d8e4b7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_classvar_in_dataclass.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::{Expr, ExprName, Stmt, StmtAssign, StmtClassDef}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{dataclass_kind, DataclassKind}; +use crate::rules::ruff::helpers::{DataclassKind, dataclass_kind}; /// ## What it does /// Checks for implicit class variables in dataclasses. @@ -87,7 +87,7 @@ pub(crate) fn implicit_class_var_in_dataclass(checker: &mut Checker, class_def: continue; }; - if checker.settings.dummy_variable_rgx.is_match(id.as_str()) { + if checker.settings().dummy_variable_rgx.is_match(id.as_str()) { continue; } @@ -95,8 +95,6 @@ pub(crate) fn implicit_class_var_in_dataclass(checker: &mut Checker, class_def: continue; } - let diagnostic = Diagnostic::new(ImplicitClassVarInDataclass, target.range()); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(ImplicitClassVarInDataclass, target.range()); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index c59a94b1a198f..1a4653c429ea2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -1,19 +1,19 @@ use std::fmt; -use anyhow::Result; +use anyhow::{Context, Result}; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr, Operator, Parameters}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; -use super::super::typing::type_hint_explicitly_allows_none; +use crate::rules::ruff::typing::type_hint_explicitly_allows_none; /// ## What it does /// Checks for the use of implicit `Optional` in type annotations when the @@ -72,6 +72,11 @@ use super::super::typing::type_hint_explicitly_allows_none; /// ## Options /// - `target-version` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because it can change the behavior of code that relies on +/// type hints, and it assumes the default value is always appropriate—which might not be the case. +/// /// [PEP 484]: https://peps.python.org/pep-0484/#union-types #[derive(ViolationMetadata)] pub(crate) struct ImplicitOptional { @@ -128,6 +133,7 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) op: Operator::BitOr, right: Box::new(Expr::NoneLiteral(ast::ExprNoneLiteral::default())), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); let content = checker.generator().expr(&new_expr); Ok(Fix::unsafe_edit(Edit::range_replacement( @@ -136,14 +142,18 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) ))) } ConversionType::Optional => { - let (import_edit, binding) = - checker.import_from_typing("Optional", expr.start(), PythonVersion::lowest())?; + let importer = checker + .typing_importer("Optional", PythonVersion::lowest()) + .context("Optional should be available on all supported Python versions")?; + let (import_edit, binding) = importer.import(expr.start())?; let new_expr = Expr::Subscript(ast::ExprSubscript { range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), value: Box::new(Expr::Name(ast::ExprName { id: Name::new(binding), ctx: ast::ExprContext::Store, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })), slice: Box::new(expr.clone()), ctx: ast::ExprContext::Load, @@ -180,11 +190,10 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) { let conversion_type = checker.target_version().into(); let mut diagnostic = - Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + checker.report_diagnostic(ImplicitOptional { conversion_type }, expr.range()); if parsed_annotation.kind().is_simple() { diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); } - checker.report_diagnostic(diagnostic); } } else { // Unquoted annotation. @@ -196,9 +205,8 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) { let conversion_type = checker.target_version().into(); let mut diagnostic = - Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + checker.report_diagnostic(ImplicitOptional { conversion_type }, expr.range()); diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs new file mode 100644 index 0000000000000..30f4ff8bc71f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs @@ -0,0 +1,103 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, CmpOp, Expr}; +use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for membership tests on empty collections (such as `list`, `tuple`, `set` or `dict`). +/// +/// ## Why is this bad? +/// If the collection is always empty, the check is unnecessary, and can be removed. +/// +/// ## Example +/// +/// ```python +/// if 1 not in set(): +/// print("got it!") +/// ``` +/// +/// Use instead: +/// +/// ```python +/// print("got it!") +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct InEmptyCollection; + +impl Violation for InEmptyCollection { + #[derive_message_formats] + fn message(&self) -> String { + "Unnecessary membership test on empty collection".to_string() + } +} + +/// RUF060 +pub(crate) fn in_empty_collection(checker: &Checker, compare: &ast::ExprCompare) { + let [op] = &*compare.ops else { + return; + }; + + if !matches!(op, CmpOp::In | CmpOp::NotIn) { + return; + } + + let [right] = &*compare.comparators else { + return; + }; + + let semantic = checker.semantic(); + + if is_empty(right, semantic) { + checker.report_diagnostic(InEmptyCollection, compare.range()); + } +} + +fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool { + let set_methods = ["set", "frozenset"]; + let collection_methods = [ + "list", + "tuple", + "set", + "frozenset", + "dict", + "bytes", + "bytearray", + "str", + ]; + + match expr { + Expr::List(ast::ExprList { elts, .. }) => elts.is_empty(), + Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.is_empty(), + Expr::Set(ast::ExprSet { elts, .. }) => elts.is_empty(), + Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(), + Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(), + Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(), + Expr::FString(s) => s + .value + .elements() + .all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())), + Expr::Call(ast::ExprCall { + func, + arguments, + range: _, + node_index: _, + }) => { + if arguments.is_empty() { + collection_methods + .iter() + .any(|s| semantic.match_builtin_expr(func, s)) + } else if let Some(arg) = arguments.find_positional(0) { + set_methods + .iter() + .any(|s| semantic.match_builtin_expr(func, s)) + && is_empty(arg, semantic) + } else { + false + } + } + _ => false, + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs index a582cd3c4bbba..e0c537f0b0cc1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, ExprSubscript, PythonVersion}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for consistent style regarding whether nonempty tuples in subscripts @@ -63,7 +63,7 @@ impl AlwaysFixableViolation for IncorrectlyParenthesizedTupleInSubscript { /// RUF031 pub(crate) fn subscript_with_parenthesized_tuple(checker: &Checker, subscript: &ExprSubscript) { - let prefer_parentheses = checker.settings.ruff.parenthesize_tuple_in_subscript; + let prefer_parentheses = checker.settings().ruff.parenthesize_tuple_in_subscript; let Expr::Tuple(tuple_subscript) = &*subscript.slice else { return; @@ -111,11 +111,10 @@ pub(crate) fn subscript_with_parenthesized_tuple(checker: &Checker, subscript: & }; let edit = Edit::range_replacement(new_source, source_range); - checker.report_diagnostic( - Diagnostic::new( + checker + .report_diagnostic( IncorrectlyParenthesizedTupleInSubscript { prefer_parentheses }, source_range, ) - .with_fix(Fix::safe_edit(edit)), - ); + .set_fix(Fix::safe_edit(edit)); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs b/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs index f79ad2a46967c..b943c6553604d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs @@ -1,10 +1,11 @@ use memchr::memchr; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_source_file::Line; use ruff_text_size::{TextRange, TextSize}; +use crate::{Violation, checkers::ast::LintContext}; + /// ## What it does /// Checks for form feed characters preceded by either a space or a tab. /// @@ -48,11 +49,13 @@ const SPACE: u8 = b' '; const TAB: u8 = b'\t'; /// RUF054 -pub(crate) fn indented_form_feed(line: &Line) -> Option { - let index_relative_to_line = memchr(FORM_FEED, line.as_bytes())?; +pub(crate) fn indented_form_feed(line: &Line, context: &LintContext) { + let Some(index_relative_to_line) = memchr(FORM_FEED, line.as_bytes()) else { + return; + }; if index_relative_to_line == 0 { - return None; + return; } if line[..index_relative_to_line] @@ -60,12 +63,14 @@ pub(crate) fn indented_form_feed(line: &Line) -> Option { .iter() .any(|byte| *byte != SPACE && *byte != TAB) { - return None; + return; } - let relative_index = u32::try_from(index_relative_to_line).ok()?; + let Ok(relative_index) = u32::try_from(index_relative_to_line) else { + return; + }; let absolute_index = line.start() + TextSize::new(relative_index); let range = TextRange::at(absolute_index, 1.into()); - Some(Diagnostic::new(IndentedFormFeed, range)) + context.report_diagnostic(IndentedFormFeed, range); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs index c343e6a97eedb..3d2bbbaa83070 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_assert_message_literal_argument.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Expr, StmtAssert}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -52,8 +52,5 @@ pub(crate) fn invalid_assert_message_literal_argument(checker: &Checker, stmt: & return; } - checker.report_diagnostic(Diagnostic::new( - InvalidAssertMessageLiteralArgument, - message.range(), - )); + checker.report_diagnostic(InvalidAssertMessageLiteralArgument, message.range()); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs index b3fac6e6ca292..81d821522599e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs @@ -3,15 +3,15 @@ use std::fmt::Display; use smallvec::SmallVec; use ast::{StmtClassDef, StmtFunctionDef}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, helpers::comment_indentation_after, AnyNodeRef}; -use ruff_python_trivia::{indentation_at_offset, SuppressionKind}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, AnyNodeRef, helpers::comment_indentation_after}; +use ruff_python_trivia::{SuppressionKind, indentation_at_offset}; use ruff_text_size::{Ranged, TextLen, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::fix::edits::delete_comment; -use crate::Locator; +use crate::{AlwaysFixableViolation, Fix}; use super::suppression_comment_visitor::{ CaptureSuppressionComment, SuppressionComment, SuppressionCommentData, @@ -49,6 +49,11 @@ use super::suppression_comment_visitor::{ /// # fmt: on /// # yapf: enable /// ``` +/// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because it deletes the invalid suppression comment, +/// rather than trying to move it to a valid position, which the user more likely intended. #[derive(ViolationMetadata)] pub(crate) struct InvalidFormatterSuppressionComment { reason: IgnoredReason, @@ -99,10 +104,9 @@ pub(crate) fn ignored_formatter_suppression_comment(checker: &Checker, suite: &a comments.sort(); for (range, reason) in comments.ignored_comments() { - checker.report_diagnostic( - Diagnostic::new(InvalidFormatterSuppressionComment { reason }, range) - .with_fix(Fix::unsafe_edit(delete_comment(range, checker.locator()))), - ); + checker + .report_diagnostic(InvalidFormatterSuppressionComment { reason }, range) + .set_fix(Fix::unsafe_edit(delete_comment(range, checker.locator()))); } } @@ -298,10 +302,11 @@ const fn is_valid_enclosing_node(node: AnyNodeRef) -> bool { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::FStringExpressionElement(_) - | AnyNodeRef::FStringLiteralElement(_) - | AnyNodeRef::FStringFormatSpec(_) + | AnyNodeRef::InterpolatedElement(_) + | AnyNodeRef::InterpolatedStringLiteralElement(_) + | AnyNodeRef::InterpolatedStringFormatSpec(_) | AnyNodeRef::ExprFString(_) + | AnyNodeRef::ExprTString(_) | AnyNodeRef::ExprStringLiteral(_) | AnyNodeRef::ExprBytesLiteral(_) | AnyNodeRef::ExprNumberLiteral(_) @@ -339,6 +344,7 @@ const fn is_valid_enclosing_node(node: AnyNodeRef) -> bool { | AnyNodeRef::TypeParamTypeVarTuple(_) | AnyNodeRef::TypeParamParamSpec(_) | AnyNodeRef::FString(_) + | AnyNodeRef::TString(_) | AnyNodeRef::StringLiteral(_) | AnyNodeRef::BytesLiteral(_) | AnyNodeRef::Identifier(_) => false, diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs index bd7fd69ccc17c..067a074718a36 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_index_type.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{Expr, ExprNumberLiteral, ExprSlice, ExprSubscript, Number}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use std::fmt; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -41,7 +41,9 @@ impl Violation for InvalidIndexType { is_slice, } = self; if *is_slice { - format!("Slice in indexed access to type `{value_type}` uses type `{index_type}` instead of an integer") + format!( + "Slice in indexed access to type `{value_type}` uses type `{index_type}` instead of an integer" + ) } else { format!( "Indexed access to type `{value_type}` uses type `{index_type}` instead of an integer or slice" @@ -88,14 +90,14 @@ pub(crate) fn invalid_index_type(checker: &Checker, expr: &ExprSubscript) { if index_type.is_literal() { // If the index is a literal, require an integer if index_type != CheckableExprType::IntLiteral { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( InvalidIndexType { value_type: value_type.to_string(), index_type: index_type.to_string(), is_slice: false, }, index.range(), - )); + ); } } else if let Expr::Slice(ExprSlice { lower, upper, step, .. @@ -111,36 +113,36 @@ pub(crate) fn invalid_index_type(checker: &Checker, expr: &ExprSubscript) { is_slice_type, CheckableExprType::IntLiteral | CheckableExprType::NoneLiteral ) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( InvalidIndexType { value_type: value_type.to_string(), index_type: is_slice_type.to_string(), is_slice: true, }, is_slice.range(), - )); + ); } } else if let Some(is_slice_type) = CheckableExprType::try_from(is_slice.as_ref()) { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( InvalidIndexType { value_type: value_type.to_string(), index_type: is_slice_type.to_string(), is_slice: true, }, is_slice.range(), - )); + ); } } } else { // If it's some other checkable data type, it's a violation - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( InvalidIndexType { value_type: value_type.to_string(), index_type: index_type.to_string(), is_slice: false, }, index.range(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs index 290f2da3e9079..bc91f9cf54444 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::{FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for any pyproject.toml that does not conform to the schema from the relevant PEPs. diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs index 997d311f109a0..734a544b726d7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs @@ -1,11 +1,12 @@ -use crate::noqa::{Code, Directive}; -use crate::registry::Rule; -use crate::Locator; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::noqa::{Code, Directive}; use crate::noqa::{Codes, NoqaDirectives}; +use crate::registry::Rule; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `noqa` codes that are invalid. @@ -48,7 +49,7 @@ impl AlwaysFixableViolation for InvalidRuleCode { /// RUF102 for invalid noqa codes pub(crate) fn invalid_noqa_code( - diagnostics: &mut Vec, + context: &LintContext, noqa_directives: &NoqaDirectives, locator: &Locator, external: &[String], @@ -69,11 +70,11 @@ pub(crate) fn invalid_noqa_code( .partition(|&code| code_is_valid(code, external)); if valid_codes.is_empty() { - diagnostics.push(all_codes_invalid_diagnostic(directive, invalid_codes)); + all_codes_invalid_diagnostic(directive, invalid_codes, context); } else { - diagnostics.extend(invalid_codes.into_iter().map(|invalid_code| { - some_codes_are_invalid_diagnostic(directive, invalid_code, locator) - })); + for invalid_code in invalid_codes { + some_codes_are_invalid_diagnostic(directive, invalid_code, locator, context); + } } } } @@ -86,36 +87,40 @@ fn code_is_valid(code: &Code, external: &[String]) -> bool { fn all_codes_invalid_diagnostic( directive: &Codes<'_>, invalid_codes: Vec<&Code<'_>>, -) -> Diagnostic { - Diagnostic::new( - InvalidRuleCode { - rule_code: invalid_codes - .into_iter() - .map(Code::as_str) - .collect::>() - .join(", "), - }, - directive.range(), - ) - .with_fix(Fix::safe_edit(Edit::range_deletion(directive.range()))) + context: &LintContext, +) { + context + .report_diagnostic( + InvalidRuleCode { + rule_code: invalid_codes + .into_iter() + .map(Code::as_str) + .collect::>() + .join(", "), + }, + directive.range(), + ) + .set_fix(Fix::safe_edit(Edit::range_deletion(directive.range()))); } fn some_codes_are_invalid_diagnostic( codes: &Codes, invalid_code: &Code, locator: &Locator, -) -> Diagnostic { - let diagnostic = Diagnostic::new( - InvalidRuleCode { - rule_code: invalid_code.to_string(), - }, - invalid_code.range(), - ); - diagnostic.with_fix(Fix::safe_edit(remove_invalid_noqa( - codes, - invalid_code, - locator, - ))) + context: &LintContext, +) { + context + .report_diagnostic( + InvalidRuleCode { + rule_code: invalid_code.to_string(), + }, + invalid_code.range(), + ) + .set_fix(Fix::safe_edit(remove_invalid_noqa( + codes, + invalid_code, + locator, + ))); } fn remove_invalid_noqa(codes: &Codes, invalid_code: &Code, locator: &Locator) -> Edit { diff --git a/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs new file mode 100644 index 0000000000000..5dc94542dab0d --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs @@ -0,0 +1,308 @@ +use itertools::{Either, Itertools}; +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, AtomicNodeIndex, Expr, Stmt, StmtExpr, StmtWith, WithItem}; +use ruff_python_semantic::SemanticModel; +use ruff_python_trivia::{has_leading_content, has_trailing_content, leading_indentation}; +use ruff_source_file::UniversalNewlines; +use ruff_text_size::{Ranged, TextRange}; +use std::fmt; + +use crate::{FixAvailability, Violation, checkers::ast::Checker}; + +/// ## What it does +/// Checks for non-contextmanager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call`. +/// +/// ## Why is this bad? +/// The context-manager form is more readable, easier to extend, and supports additional kwargs. +/// +/// ## Example +/// ```python +/// import pytest +/// +/// +/// excinfo = pytest.raises(ValueError, int, "hello") +/// pytest.warns(UserWarning, my_function, arg) +/// pytest.deprecated_call(my_deprecated_function, arg1, arg2) +/// ``` +/// +/// Use instead: +/// ```python +/// import pytest +/// +/// +/// with pytest.raises(ValueError) as excinfo: +/// int("hello") +/// with pytest.warns(UserWarning): +/// my_function(arg) +/// with pytest.deprecated_call(): +/// my_deprecated_function(arg1, arg2) +/// ``` +/// +/// ## References +/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) +/// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns) +/// - [`pytest` documentation: `pytest.deprecated_call`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-deprecated-call) +#[derive(ViolationMetadata)] +pub(crate) struct LegacyFormPytestRaises { + context_type: PytestContextType, +} + +impl Violation for LegacyFormPytestRaises { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Use context-manager form of `pytest.{}()`", + self.context_type + ) + } + + fn fix_title(&self) -> Option { + Some(format!( + "Use `pytest.{}()` as a context-manager", + self.context_type + )) + } +} + +/// Enum representing the type of pytest context manager +#[derive(PartialEq, Clone, Copy)] +enum PytestContextType { + Raises, + Warns, + DeprecatedCall, +} + +impl fmt::Display for PytestContextType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Self::Raises => "raises", + Self::Warns => "warns", + Self::DeprecatedCall => "deprecated_call", + }; + write!(f, "{name}") + } +} + +impl PytestContextType { + fn from_expr_name(func: &Expr, semantic: &SemanticModel) -> Option { + semantic + .resolve_qualified_name(func) + .and_then(|qualified_name| match qualified_name.segments() { + ["pytest", "raises"] => Some(Self::Raises), + ["pytest", "warns"] => Some(Self::Warns), + ["pytest", "deprecated_call"] => Some(Self::DeprecatedCall), + _ => None, + }) + } + + fn expected_arg(self) -> Option<(&'static str, usize)> { + match self { + Self::Raises => Some(("expected_exception", 0)), + Self::Warns => Some(("expected_warning", 0)), + Self::DeprecatedCall => None, + } + } + + fn func_arg(self) -> (&'static str, usize) { + match self { + Self::Raises | Self::Warns => ("func", 1), + Self::DeprecatedCall => ("func", 0), + } + } +} + +/// RUF061 +pub(crate) fn legacy_raises_warns_deprecated_call(checker: &Checker, call: &ast::ExprCall) { + let semantic = checker.semantic(); + let Some(context_type) = PytestContextType::from_expr_name(&call.func, semantic) else { + return; + }; + + let (func_arg_name, func_arg_position) = context_type.func_arg(); + if call + .arguments + .find_argument(func_arg_name, func_arg_position) + .is_none() + { + return; + } + + let mut diagnostic = + checker.report_diagnostic(LegacyFormPytestRaises { context_type }, call.range()); + + let stmt = semantic.current_statement(); + if !has_leading_content(stmt.start(), checker.source()) + && !has_trailing_content(stmt.end(), checker.source()) + { + if let Some(with_stmt) = try_fix_legacy_call(context_type, stmt, semantic) { + let generated = checker.generator().stmt(&Stmt::With(with_stmt)); + let first_line = checker.locator().line_str(stmt.start()); + let indentation = leading_indentation(first_line); + let mut indented = String::new(); + for (idx, line) in generated.universal_newlines().enumerate() { + if idx == 0 { + indented.push_str(&line); + } else { + indented.push_str(checker.stylist().line_ending().as_str()); + indented.push_str(indentation); + indented.push_str(&line); + } + } + + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + indented, + stmt.range(), + ))); + } + } +} + +fn try_fix_legacy_call( + context_type: PytestContextType, + stmt: &Stmt, + semantic: &SemanticModel, +) -> Option { + match stmt { + Stmt::Expr(StmtExpr { value, .. }) => { + let call = value.as_call_expr()?; + + // Handle two patterns for legacy calls: + // 1. Direct usage: `pytest.raises(ZeroDivisionError, func, 1, b=0)` + // 2. With match method: `pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero")` + // + // The second branch specifically looks for raises().match() pattern which only exists for + // `raises` (not `warns`/`deprecated_call`) since only `raises` returns an ExceptionInfo + // object with a .match() method. We need to preserve this match condition when converting + // to context manager form. + if PytestContextType::from_expr_name(&call.func, semantic) == Some(context_type) { + generate_with_statement(context_type, call, None, None, None) + } else if let PytestContextType::Raises = context_type { + let inner_raises_call = call + .func + .as_attribute_expr() + .filter(|expr_attribute| &expr_attribute.attr == "match") + .and_then(|expr_attribute| expr_attribute.value.as_call_expr()) + .filter(|inner_call| { + PytestContextType::from_expr_name(&inner_call.func, semantic) + == Some(PytestContextType::Raises) + })?; + let match_arg = call.arguments.args.first(); + generate_with_statement(context_type, inner_raises_call, match_arg, None, None) + } else { + None + } + } + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { + let call = value.as_call_expr().filter(|call| { + PytestContextType::from_expr_name(&call.func, semantic) == Some(context_type) + })?; + let (optional_vars, assign_targets) = match context_type { + PytestContextType::Raises => { + let [target] = targets.as_slice() else { + return None; + }; + (Some(target), None) + } + PytestContextType::Warns | PytestContextType::DeprecatedCall => { + (None, Some(targets.as_slice())) + } + }; + + generate_with_statement(context_type, call, None, optional_vars, assign_targets) + } + _ => None, + } +} + +fn generate_with_statement( + context_type: PytestContextType, + legacy_call: &ast::ExprCall, + match_arg: Option<&Expr>, + optional_vars: Option<&Expr>, + assign_targets: Option<&[Expr]>, +) -> Option { + let expected = if let Some((name, position)) = context_type.expected_arg() { + Some(legacy_call.arguments.find_argument_value(name, position)?) + } else { + None + }; + + let (func_arg_name, func_arg_position) = context_type.func_arg(); + let func = legacy_call + .arguments + .find_argument_value(func_arg_name, func_arg_position)?; + + let (func_args, func_keywords): (Vec<_>, Vec<_>) = legacy_call + .arguments + .arguments_source_order() + .skip(if expected.is_some() { 2 } else { 1 }) + .partition_map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(expr) => Either::Left(expr.clone()), + ast::ArgOrKeyword::Keyword(keyword) => Either::Right(keyword.clone()), + }); + + let context_call = ast::ExprCall { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + func: legacy_call.func.clone(), + arguments: ast::Arguments { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + args: expected.cloned().as_slice().into(), + keywords: match_arg + .map(|expr| ast::Keyword { + node_index: AtomicNodeIndex::dummy(), + // Take range from the original expression so that the keyword + // argument is generated after positional arguments + range: expr.range(), + arg: Some(ast::Identifier::new("match", TextRange::default())), + value: expr.clone(), + }) + .as_slice() + .into(), + }, + }; + + let func_call = ast::ExprCall { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + func: Box::new(func.clone()), + arguments: ast::Arguments { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + args: func_args.into(), + keywords: func_keywords.into(), + }, + }; + + let body = if let Some(assign_targets) = assign_targets { + Stmt::Assign(ast::StmtAssign { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + targets: assign_targets.to_vec(), + value: Box::new(func_call.into()), + }) + } else { + Stmt::Expr(StmtExpr { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + value: Box::new(func_call.into()), + }) + }; + + Some(StmtWith { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + is_async: false, + items: vec![WithItem { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + context_expr: context_call.into(), + optional_vars: optional_vars.map(|var| Box::new(var.clone())), + }], + body: vec![body], + }) +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs b/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs index 3942fa6c5edf1..4b7fa7e69fcd5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs @@ -1,9 +1,9 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -53,7 +53,7 @@ pub(crate) fn map_int_version_parsing(checker: &Checker, call: &ast::ExprCall) { }; if is_dunder_version_split_dot(second) && semantic.match_builtin_expr(first, "int") { - checker.report_diagnostic(Diagnostic::new(MapIntVersionParsing, call.range())); + checker.report_diagnostic(MapIntVersionParsing, call.range()); } } @@ -68,8 +68,10 @@ fn map_call_with_two_arguments<'a>( args, keywords, range: _, + node_index: _, }, range: _, + node_index: _, } = call; if !keywords.is_empty() { @@ -100,8 +102,11 @@ fn is_dunder_version_split_dot(expr: &ast::Expr) -> bool { return false; } - let Some(ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ })) = - arguments.find_argument_value("sep", 0) + let Some(ast::Expr::StringLiteral(ast::ExprStringLiteral { + value, + range: _, + node_index: _, + })) = arguments.find_argument_value("sep", 0) else { return false; }; diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index c44bd2cb8a214..9194d758da90f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -1,8 +1,7 @@ use memchr::memchr2_iter; use rustc_hash::FxHashSet; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_literal::format::FormatSpec; use ruff_python_parser::parse_expression; @@ -10,9 +9,10 @@ use ruff_python_semantic::analyze::logging::is_logger_candidate; use ruff_python_semantic::{Modules, SemanticModel, TypingOnlyBindingsStatus}; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::fastapi::rules::is_fastapi_route_call; -use crate::Locator; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Searches for strings that look like they were meant to be f-strings, but are missing an `f` prefix. @@ -53,6 +53,11 @@ use crate::Locator; /// print(f"Hello {name}! It is {day_of_week} today!") /// ``` /// +/// ## Fix safety +/// +/// This fix will always change the behavior of the program and, despite the precautions detailed +/// above, this may be undesired. As such the fix is always marked as unsafe. +/// /// [logging]: https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application /// [gettext]: https://docs.python.org/3/library/gettext.html /// [FastAPI path]: https://fastapi.tiangolo.com/tutorial/path-params/ @@ -88,7 +93,7 @@ pub(crate) fn missing_fstring_syntax(checker: &Checker, literal: &ast::StringLit } } - let logger_objects = &checker.settings.logger_objects; + let logger_objects = &checker.settings().logger_objects; let fastapi_seen = semantic.seen_module(Modules::FASTAPI); // We also want to avoid: @@ -111,9 +116,9 @@ pub(crate) fn missing_fstring_syntax(checker: &Checker, literal: &ast::StringLit } if should_be_fstring(literal, checker.locator(), semantic) { - let diagnostic = Diagnostic::new(MissingFStringSyntax, literal.range()) - .with_fix(fix_fstring_syntax(literal.range())); - checker.report_diagnostic(diagnostic); + checker + .report_diagnostic(MissingFStringSyntax, literal.range()) + .set_fix(fix_fstring_syntax(literal.range())); } } @@ -209,7 +214,7 @@ fn should_be_fstring( for f_string in value.f_strings() { let mut has_name = false; - for element in f_string.elements.expressions() { + for element in f_string.elements.interpolations() { if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() { if arg_names.contains(id) { return false; diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index b0aff0016e519..420b8c310a585 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -1,3 +1,4 @@ +pub(crate) use access_annotations_from_class_dict::*; pub(crate) use ambiguous_unicode_character::*; pub(crate) use assert_with_print_message::*; pub(crate) use assignment_in_assert::*; @@ -13,6 +14,7 @@ pub(crate) use function_call_in_dataclass_default::*; pub(crate) use if_key_in_dict_del::*; pub(crate) use implicit_classvar_in_dataclass::*; pub(crate) use implicit_optional::*; +pub(crate) use in_empty_collection::*; pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*; pub(crate) use indented_form_feed::*; pub(crate) use invalid_assert_message_literal_argument::*; @@ -20,6 +22,7 @@ pub(crate) use invalid_formatter_suppression_comment::*; pub(crate) use invalid_index_type::*; pub(crate) use invalid_pyproject_toml::*; pub(crate) use invalid_rule_code::*; +pub(crate) use legacy_form_pytest_raises::*; pub(crate) use map_int_version_parsing::*; pub(crate) use missing_fstring_syntax::*; pub(crate) use mutable_class_default::*; @@ -27,6 +30,7 @@ pub(crate) use mutable_dataclass_default::*; pub(crate) use mutable_fromkeys_value::*; pub(crate) use needless_else::*; pub(crate) use never_union::*; +pub(crate) use non_octal_permissions::*; pub(crate) use none_not_at_end_of_union::*; pub(crate) use parenthesize_chained_operators::*; pub(crate) use post_init_default::*; @@ -56,6 +60,7 @@ pub(crate) use used_dummy_variable::*; pub(crate) use useless_if_else::*; pub(crate) use zip_instead_of_pairwise::*; +mod access_annotations_from_class_dict; mod ambiguous_unicode_character; mod assert_with_print_message; mod assignment_in_assert; @@ -69,10 +74,10 @@ mod default_factory_kwarg; mod explicit_f_string_type_conversion; mod falsy_dict_get_fallback; mod function_call_in_dataclass_default; -mod helpers; mod if_key_in_dict_del; mod implicit_classvar_in_dataclass; mod implicit_optional; +mod in_empty_collection; mod incorrectly_parenthesized_tuple_in_subscript; mod indented_form_feed; mod invalid_assert_message_literal_argument; @@ -80,6 +85,7 @@ mod invalid_formatter_suppression_comment; mod invalid_index_type; mod invalid_pyproject_toml; mod invalid_rule_code; +mod legacy_form_pytest_raises; mod map_int_version_parsing; mod missing_fstring_syntax; mod mutable_class_default; @@ -87,6 +93,7 @@ mod mutable_dataclass_default; mod mutable_fromkeys_value; mod needless_else; mod never_union; +mod non_octal_permissions; mod none_not_at_end_of_union; mod parenthesize_chained_operators; mod post_init_default; diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs index 2b86c5cda5a7b..4114c553f13b8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_class_default.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{ +use crate::rules::ruff::helpers::{ dataclass_kind, has_default_copy_semantics, is_class_var_annotation, is_final_annotation, is_special_attribute, }; @@ -118,7 +118,7 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas return; } - checker.report_diagnostic(Diagnostic::new(MutableClassDefault, value.range())); + checker.report_diagnostic(MutableClassDefault, value.range()); } } Stmt::Assign(ast::StmtAssign { value, targets, .. }) => { @@ -130,7 +130,7 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas return; } - checker.report_diagnostic(Diagnostic::new(MutableClassDefault, value.range())); + checker.report_diagnostic(MutableClassDefault, value.range()); } } _ => (), diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs index 9cb5dba688e44..736ab510ce9b3 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::ruff::rules::helpers::{dataclass_kind, is_class_var_annotation}; +use crate::rules::ruff::helpers::{dataclass_kind, is_class_var_annotation}; /// ## What it does /// Checks for mutable default values in dataclass attributes. @@ -86,9 +86,7 @@ pub(crate) fn mutable_dataclass_default(checker: &Checker, class_def: &ast::Stmt && !is_class_var_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic(), &[]) { - let diagnostic = Diagnostic::new(MutableDataclassDefault, value.range()); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(MutableDataclassDefault, value.range()); } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs b/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs index 88e2bb5803ad0..5b60d38c1a7ff 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mutable_fromkeys_value.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::analyze::typing::is_mutable_expr; @@ -9,6 +8,7 @@ use ruff_text_size::Ranged; use ruff_text_size::TextRange; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for mutable objects passed as a value argument to `dict.fromkeys`. @@ -87,12 +87,11 @@ pub(crate) fn mutable_fromkeys_value(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(MutableFromkeysValue, call.range()); + let mut diagnostic = checker.report_diagnostic(MutableFromkeysValue, call.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( generate_dict_comprehension(keys, value, checker.generator()), call.range(), ))); - checker.report_diagnostic(diagnostic); } /// Format a code snippet to expression `{key: value for key in keys}`, where @@ -103,6 +102,7 @@ fn generate_dict_comprehension(keys: &Expr, value: &Expr, generator: Generator) id: Name::new_static("key"), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; // Construct `key in keys`. let comp = ast::Comprehension { @@ -110,6 +110,7 @@ fn generate_dict_comprehension(keys: &Expr, value: &Expr, generator: Generator) iter: keys.clone(), ifs: vec![], range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), is_async: false, }; // Construct the dict comprehension. @@ -118,6 +119,7 @@ fn generate_dict_comprehension(keys: &Expr, value: &Expr, generator: Generator) value: Box::new(value.clone()), generators: vec![comp], range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }; generator.expr(&dict_comp.into()) } diff --git a/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs b/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs index 61cc964fddd9e..ca510970d6b21 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/needless_else.rs @@ -1,7 +1,6 @@ use std::cmp::Ordering; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::comment_indentation_after; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{Stmt, StmtExpr, StmtFor, StmtIf, StmtTry, StmtWhile}; @@ -10,6 +9,7 @@ use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `else` clauses that only contains `pass` and `...` statements. @@ -70,9 +70,9 @@ pub(crate) fn needless_else(checker: &Checker, stmt: AnyNodeWithOrElse) { let edit = Edit::range_deletion(remove_range); let fix = Fix::safe_edit(edit); - let diagnostic = Diagnostic::new(NeedlessElse, else_range); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(NeedlessElse, else_range) + .set_fix(fix); } /// Whether `body` contains only one `pass` or `...` statement. diff --git a/crates/ruff_linter/src/rules/ruff/rules/never_union.rs b/crates/ruff_linter/src/rules/ruff/rules/never_union.rs index fdfcd56e260b3..8be1156a0a19c 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/never_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/never_union.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, ExprBinOp, Operator}; -use ruff_python_semantic::{analyze::typing::traverse_union, SemanticModel}; +use ruff_python_semantic::{SemanticModel, analyze::typing::traverse_union}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `typing.NoReturn` and `typing.Never` in union types. @@ -74,10 +74,11 @@ pub(crate) fn never_union(checker: &Checker, expr: &Expr) { left, right, range: _, + node_index: _, }) => { // Analyze the left-hand side of the `|` operator. if let Some(never_like) = NeverLike::from_expr(left, checker.semantic()) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NeverUnion { never_like, union_like: UnionLike::PEP604, @@ -95,12 +96,11 @@ pub(crate) fn never_union(checker: &Checker, expr: &Expr) { expr.range(), ))); } - checker.report_diagnostic(diagnostic); } // Analyze the right-hand side of the `|` operator. if let Some(never_like) = NeverLike::from_expr(right, checker.semantic()) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NeverUnion { never_like, union_like: UnionLike::PEP604, @@ -113,7 +113,6 @@ pub(crate) fn never_union(checker: &Checker, expr: &Expr) { expr.range(), ))); } - checker.report_diagnostic(diagnostic); } } @@ -123,6 +122,7 @@ pub(crate) fn never_union(checker: &Checker, expr: &Expr) { slice, ctx: _, range: _, + node_index: _, }) if checker.semantic().match_typing_expr(value, "Union") => { let Expr::Tuple(tuple_slice) = &**slice else { return; @@ -143,7 +143,7 @@ pub(crate) fn never_union(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( NeverUnion { never_like, union_like: UnionLike::TypingUnion, @@ -164,15 +164,16 @@ pub(crate) fn never_union(checker: &Checker, expr: &Expr) { elts: rest, ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), parenthesized: true, })), ctx: ast::ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), })) }, expr.range(), ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs b/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs new file mode 100644 index 0000000000000..90e70f9201587 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/non_octal_permissions.rs @@ -0,0 +1,213 @@ +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::name::QualifiedName; +use ruff_python_ast::{self as ast, Expr, ExprCall}; +use ruff_python_semantic::{SemanticModel, analyze}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::{FixAvailability, Violation}; + +/// ## What it does +/// Checks for standard library functions which take a numeric `mode` argument +/// where a non-octal integer literal is passed. +/// +/// ## Why is this bad? +/// +/// Numeric modes are made up of one to four octal digits. Converting a non-octal +/// integer to octal may not be the mode the author intended. +/// +/// ## Example +/// +/// ```python +/// os.chmod("foo", 644) +/// ``` +/// +/// Use instead: +/// +/// ```python +/// os.chmod("foo", 0o644) +/// ``` +/// +/// ## Fix safety +/// +/// There are two categories of fix, the first of which is where it looks like +/// the author intended to use an octal literal but the `0o` prefix is missing: +/// +/// ```python +/// os.chmod("foo", 400) +/// os.chmod("foo", 644) +/// ``` +/// +/// This class of fix changes runtime behaviour. In the first case, `400` +/// corresponds to `0o620` (`u=rw,g=w,o=`). As this mode is not deemed likely, +/// it is changed to `0o400` (`u=r,go=`). Similarly, `644` corresponds to +/// `0o1204` (`u=ws,g=,o=r`) and is changed to `0o644` (`u=rw,go=r`). +/// +/// The second category is decimal literals which are recognised as likely valid +/// but in decimal form: +/// +/// ```python +/// os.chmod("foo", 256) +/// os.chmod("foo", 493) +/// ``` +/// +/// `256` corresponds to `0o400` (`u=r,go=`) and `493` corresponds to `0o755` +/// (`u=rwx,go=rx`). Both of these fixes keep runtime behavior unchanged. If the +/// original code really intended to use `0o256` (`u=w,g=rx,o=rw`) instead of +/// `256`, this fix should not be accepted. +/// +/// ## Fix availability +/// +/// A fix is only available if the integer literal matches a set of common modes. +#[derive(ViolationMetadata)] +pub(crate) struct NonOctalPermissions; + +impl Violation for NonOctalPermissions { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + "Non-octal mode".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with octal literal".to_string()) + } +} + +/// RUF064 +pub(crate) fn non_octal_permissions(checker: &Checker, call: &ExprCall) { + let mode_arg = find_func_mode_arg(call, checker.semantic()) + .or_else(|| find_method_mode_arg(call, checker.semantic())); + + let Some(mode_arg) = mode_arg else { + return; + }; + + let Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(int), + .. + }) = mode_arg + else { + return; + }; + + let mode_literal = &checker.locator().contents()[mode_arg.range()]; + + if mode_literal.starts_with("0o") || mode_literal.starts_with("0O") || mode_literal == "0" { + return; + } + + let mut diagnostic = checker.report_diagnostic(NonOctalPermissions, mode_arg.range()); + + // Don't suggest a fix for 0x or 0b literals. + if mode_literal.starts_with('0') { + return; + } + + let Some(suggested) = int.as_u16().and_then(suggest_fix) else { + return; + }; + + let edit = Edit::range_replacement(format!("{suggested:#o}"), mode_arg.range()); + diagnostic.set_fix(Fix::unsafe_edit(edit)); +} + +fn find_func_mode_arg<'a>(call: &'a ExprCall, semantic: &SemanticModel) -> Option<&'a Expr> { + let qualified_name = semantic.resolve_qualified_name(&call.func)?; + + match qualified_name.segments() { + ["os", "umask"] => call.arguments.find_argument_value("mode", 0), + [ + "os", + "chmod" | "fchmod" | "lchmod" | "mkdir" | "makedirs" | "mkfifo" | "mknod", + ] => call.arguments.find_argument_value("mode", 1), + ["os", "open"] => call.arguments.find_argument_value("mode", 2), + ["dbm", "open"] | ["dbm", "gnu" | "ndbm", "open"] => { + call.arguments.find_argument_value("mode", 2) + } + _ => None, + } +} + +fn find_method_mode_arg<'a>(call: &'a ExprCall, semantic: &SemanticModel) -> Option<&'a Expr> { + let (type_name, attr_name) = resolve_method_call(&call.func, semantic)?; + + match (type_name.segments(), attr_name) { + ( + ["pathlib", "Path" | "PosixPath" | "WindowsPath"], + "chmod" | "lchmod" | "mkdir" | "touch", + ) => call.arguments.find_argument_value("mode", 0), + _ => None, + } +} + +fn resolve_method_call<'a>( + func: &'a Expr, + semantic: &'a SemanticModel, +) -> Option<(QualifiedName<'a>, &'a str)> { + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else { + return None; + }; + + // First: is this an inlined call like `pathlib.Path.chmod`? + // ```python + // from pathlib import Path + // Path("foo").chmod(0o644) + // ``` + if let Expr::Call(call) = value.as_ref() { + let qualified_name = semantic.resolve_qualified_name(&call.func)?; + return Some((qualified_name, attr)); + } + + // Second, is this a call like `pathlib.Path.chmod` via a variable? + // ```python + // from pathlib import Path + // path = Path("foo") + // path.chmod() + // ``` + let Expr::Name(name) = value.as_ref() else { + return None; + }; + + let binding_id = semantic.resolve_name(name)?; + + let binding = semantic.binding(binding_id); + + let Some(Expr::Call(call)) = analyze::typing::find_binding_value(binding, semantic) else { + return None; + }; + + let qualified_name = semantic.resolve_qualified_name(&call.func)?; + + Some((qualified_name, attr)) +} + +/// Try to determine whether the integer literal +fn suggest_fix(mode: u16) -> Option { + // These suggestions are in the form of + // | => + // If could theoretically be a valid octal literal, the + // comment explains why it's deemed unlikely to be intentional. + match mode { + 400 | 256 => Some(0o400), // -w-r-xrw-, group/other > user unlikely + 440 | 288 => Some(0o440), + 444 | 292 => Some(0o444), + 600 | 384 => Some(0o600), + 640 | 416 => Some(0o640), // r----xrw-, other > user unlikely + 644 | 420 => Some(0o644), // r---w----, group write but not read unlikely + 660 | 432 => Some(0o660), // r---wx-w-, write but not read unlikely + 664 | 436 => Some(0o664), // r---wxrw-, other > user unlikely + 666 | 438 => Some(0o666), + 700 | 448 => Some(0o700), + 744 | 484 => Some(0o744), + 750 | 488 => Some(0o750), + 755 | 493 => Some(0o755), + 770 | 504 => Some(0o770), // r-x---r--, other > group unlikely + 775 | 509 => Some(0o775), + 776 | 510 => Some(0o776), // r-x--x---, seems unlikely + 777 | 511 => Some(0o777), // r-x--x--x, seems unlikely + _ => None, + } +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs index 07a72cd30c8fd..a040bc96185ca 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::Ranged; use smallvec::SmallVec; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -70,6 +70,6 @@ pub(crate) fn none_not_at_end_of_union<'a>(checker: &Checker, union: &'a Expr) { } for none_expr in none_exprs { - checker.report_diagnostic(Diagnostic::new(NoneNotAtEndOfUnion, none_expr.range())); + checker.report_diagnostic(NoneNotAtEndOfUnion, none_expr.range()); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs index 1aef814d3a9a0..e7ad588a8bcf4 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for chained operators where adding parentheses could improve the @@ -86,10 +86,9 @@ pub(crate) fn parenthesize_chained_logical_operators(checker: &Checker, expr: &a { let new_source = format!("({})", locator.slice(source_range)); let edit = Edit::range_replacement(new_source, source_range); - checker.report_diagnostic( - Diagnostic::new(ParenthesizeChainedOperators, source_range) - .with_fix(Fix::safe_edit(edit)), - ); + checker + .report_diagnostic(ParenthesizeChainedOperators, source_range) + .set_fix(Fix::safe_edit(edit)); } } _ => continue, diff --git a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs index c587a0f7a2e0a..390c6bb067ce7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs @@ -1,16 +1,16 @@ use anyhow::Context; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::{Scope, ScopeKind}; use ruff_python_trivia::{indentation_at_offset, textwrap}; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::{checkers::ast::Checker, importer::ImportRequest}; -use super::helpers::{dataclass_kind, DataclassKind}; +use crate::rules::ruff::helpers::{DataclassKind, dataclass_kind}; /// ## What it does /// Checks for `__post_init__` dataclass methods with parameter defaults. @@ -61,6 +61,12 @@ use super::helpers::{dataclass_kind, DataclassKind}; /// foo = Foo() # Prints '1 2'. /// ``` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because, although switching to `InitVar` is usually correct, +/// it is incorrect when the parameter is not intended to be part of the public API or when the value +/// is meant to be shared across all instances. +/// /// ## References /// - [Python documentation: Post-init processing](https://docs.python.org/3/library/dataclasses.html#post-init-processing) /// - [Python documentation: Init-only variables](https://docs.python.org/3/library/dataclasses.html#init-only-variables) @@ -102,13 +108,12 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct } let mut stopped_fixes = false; - let mut diagnostics = vec![]; for parameter in function_def.parameters.iter_non_variadic_params() { let Some(default) = parameter.default() else { continue; }; - let mut diagnostic = Diagnostic::new(PostInitDefault, default.range()); + let mut diagnostic = checker.report_diagnostic(PostInitDefault, default.range()); if !stopped_fixes { diagnostic.try_set_fix(|| { @@ -123,13 +128,9 @@ pub(crate) fn post_init_default(checker: &Checker, function_def: &ast::StmtFunct // Need to stop fixes as soon as there is a parameter we cannot fix. // Otherwise, we risk a syntax error (a parameter without a default // following parameter with a default). - stopped_fixes |= diagnostic.fix.is_none(); + stopped_fixes |= diagnostic.fix().is_none(); } - - diagnostics.push(diagnostic); } - - checker.report_diagnostics(diagnostics); } /// Generate a [`Fix`] to transform a `__post_init__` default argument into a diff --git a/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs b/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs index 067ed63c22649..5199c0e966ea5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs @@ -1,8 +1,9 @@ +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast as ast; + +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::flake8_pytest_style::rules::is_pytest_raises; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast as ast; /// ## What it does /// Checks for non-raw literal string arguments passed to the `match` parameter @@ -98,9 +99,7 @@ pub(crate) fn pytest_raises_ambiguous_pattern(checker: &Checker, call: &ast::Exp return; } - let diagnostic = Diagnostic::new(PytestRaisesAmbiguousPattern, string.range); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(PytestRaisesAmbiguousPattern, string.range); } fn string_has_unescaped_metacharacters(value: &ast::StringLiteralValue) -> bool { diff --git a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs index 53d4bf44cecd5..4321e004c3b24 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs @@ -1,8 +1,7 @@ use anyhow::Result; use itertools::Itertools; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_python_semantic::SemanticModel; @@ -10,6 +9,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for the use of `sum()` to flatten lists of lists, which has @@ -46,6 +46,14 @@ use crate::importer::ImportRequest; /// functools.reduce(operator.iadd, lists, []) /// ``` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because `sum` uses the `__add__` magic method while +/// `operator.iadd` uses the `__iadd__` magic method, and these behave differently on lists. +/// The former requires the right summand to be a list, whereas the latter allows for any iterable. +/// Therefore, the fix could inadvertently cause code that previously raised an error to silently +/// succeed. Moreover, the fix could remove comments from the original code. +/// /// ## References /// - [_How Not to Flatten a List of Lists in Python_](https://mathieularose.com/how-not-to-flatten-a-list-of-lists-in-python) /// - [_How do I make a flat list out of a list of lists?_](https://stackoverflow.com/questions/952914/how-do-i-make-a-flat-list-out-of-a-list-of-lists/953097#953097) @@ -71,6 +79,7 @@ pub(crate) fn quadratic_list_summation(checker: &Checker, call: &ast::ExprCall) func, arguments, range, + node_index: _, } = call; let Some(iterable) = arguments.args.first() else { @@ -87,9 +96,8 @@ pub(crate) fn quadratic_list_summation(checker: &Checker, call: &ast::ExprCall) return; } - let mut diagnostic = Diagnostic::new(QuadraticListSummation, *range); + let mut diagnostic = checker.report_diagnostic(QuadraticListSummation, *range); diagnostic.try_set_fix(|| convert_to_reduce(iterable, call, checker)); - checker.report_diagnostic(diagnostic); } /// Generate a [`Fix`] to convert a `sum()` call to a `functools.reduce()` call. diff --git a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs index ac2bcf90dc0af..e11a9a7ad1935 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::checkers::ast::LintContext; use crate::noqa::{Codes, Directive, FileNoqaDirectives, NoqaDirectives}; use crate::rule_redirects::get_redirect_target; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for `noqa` directives that use redirected rule codes. @@ -43,35 +44,32 @@ impl AlwaysFixableViolation for RedirectedNOQA { } /// RUF101 for in-line noqa directives -pub(crate) fn redirected_noqa(diagnostics: &mut Vec, noqa_directives: &NoqaDirectives) { +pub(crate) fn redirected_noqa(context: &LintContext, noqa_directives: &NoqaDirectives) { for line in noqa_directives.lines() { let Directive::Codes(directive) = &line.directive else { continue; }; - build_diagnostics(diagnostics, directive); + build_diagnostics(context, directive); } } /// RUF101 for file noqa directives -pub(crate) fn redirected_file_noqa( - diagnostics: &mut Vec, - noqa_directives: &FileNoqaDirectives, -) { +pub(crate) fn redirected_file_noqa(context: &LintContext, noqa_directives: &FileNoqaDirectives) { for line in noqa_directives.lines() { let Directive::Codes(codes) = &line.parsed_file_exemption else { continue; }; - build_diagnostics(diagnostics, codes); + build_diagnostics(context, codes); } } /// Convert a sequence of [Codes] into [Diagnostic]s and append them to `diagnostics`. -fn build_diagnostics(diagnostics: &mut Vec, codes: &Codes<'_>) { +pub(crate) fn build_diagnostics(context: &LintContext, codes: &Codes<'_>) { for code in codes.iter() { if let Some(redirected) = get_redirect_target(code.as_str()) { - let mut diagnostic = Diagnostic::new( + let mut diagnostic = context.report_diagnostic( RedirectedNOQA { original: code.to_string(), target: redirected.to_string(), @@ -82,7 +80,6 @@ fn build_diagnostics(diagnostics: &mut Vec, codes: &Codes<'_>) { redirected.to_string(), code.range(), ))); - diagnostics.push(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs index 3ab76f994b5b7..3c0fafc623a8e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/redundant_bool_literal.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Expr; use ruff_python_semantic::analyze::typing::traverse_literal; use ruff_text_size::Ranged; @@ -7,6 +6,7 @@ use ruff_text_size::Ranged; use bitflags::bitflags; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `Literal[True, False]` type annotations. @@ -111,7 +111,7 @@ pub(crate) fn redundant_bool_literal<'a>(checker: &Checker, literal_expr: &'a Ex let seen_others = seen_expr.contains(BooleanLiteral::OTHER); let mut diagnostic = - Diagnostic::new(RedundantBoolLiteral { seen_others }, literal_expr.range()); + checker.report_diagnostic(RedundantBoolLiteral { seen_others }, literal_expr.range()); // Provide a [`Fix`] when the complete `Literal` can be replaced. Applying the fix // can leave an unused import to be fixed by the `unused-import` rule. @@ -123,8 +123,6 @@ pub(crate) fn redundant_bool_literal<'a>(checker: &Checker, literal_expr: &'a Ex ))); } } - - checker.report_diagnostic(diagnostic); } bitflags! { diff --git a/crates/ruff_linter/src/rules/ruff/rules/sequence_sorting.rs b/crates/ruff_linter/src/rules/ruff/rules/sequence_sorting.rs index 35715e75aa495..e5881bc0edde1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sequence_sorting.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sequence_sorting.rs @@ -12,7 +12,7 @@ use ruff_python_ast as ast; use ruff_python_codegen::Stylist; use ruff_python_parser::{TokenKind, Tokens}; use ruff_python_stdlib::str::is_cased_uppercase; -use ruff_python_trivia::{first_non_trivia_token, leading_indentation, SimpleTokenKind}; +use ruff_python_trivia::{SimpleTokenKind, first_non_trivia_token, leading_indentation}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -587,7 +587,9 @@ fn collect_string_sequence_lines<'a>( } TokenKind::String => { let Some(string_value) = string_items_iter.next() else { - unreachable!("Expected the number of string tokens to be equal to the number of string items in the sequence"); + unreachable!( + "Expected the number of string tokens to be equal to the number of string items in the sequence" + ); }; line_state.visit_string_token(string_value, token.range()); ends_with_trailing_comma = false; diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs index d1d2b479588e8..70fd5acd6cc74 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_all.rs @@ -1,14 +1,14 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_source_file::LineRanges; use ruff_text_size::TextRange; use crate::checkers::ast::Checker; use crate::rules::ruff::rules::sequence_sorting::{ - sort_single_line_elements_sequence, MultilineStringSequenceValue, SequenceKind, - SortClassification, SortingStyle, + MultilineStringSequenceValue, SequenceKind, SortClassification, SortingStyle, + sort_single_line_elements_sequence, }; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `__all__` definitions that are not ordered @@ -158,6 +158,7 @@ pub(crate) fn sort_dunder_all_ann_assign(checker: &Checker, node: &ast::StmtAnnA } } +/// RUF022 /// Sort a tuple or list that defines or mutates the global variable `__all__`. /// /// This routine checks whether the tuple or list is sorted, and emits a @@ -199,15 +200,13 @@ fn sort_dunder_all(checker: &Checker, target: &ast::Expr, node: &ast::Expr) { return; } - let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range); + let mut diagnostic = checker.report_diagnostic(UnsortedDunderAll, range); if let SortClassification::UnsortedAndMaybeFixable { items } = elts_analysis { if let Some(fix) = create_fix(range, elts, &items, kind, checker) { diagnostic.set_fix(fix); } } - - checker.report_diagnostic(diagnostic); } /// Attempt to return `Some(fix)`, where `fix` is a `Fix` diff --git a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs index fd588bf82f82c..e626ac8535533 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/sort_dunder_slots.rs @@ -2,19 +2,19 @@ use std::borrow::Cow; use itertools::izip; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Binding; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::ruff::rules::sequence_sorting::{ - sort_single_line_elements_sequence, CommentComplexity, MultilineStringSequenceValue, - SequenceKind, SortClassification, SortingStyle, + CommentComplexity, MultilineStringSequenceValue, SequenceKind, SortClassification, + SortingStyle, sort_single_line_elements_sequence, }; -use crate::Locator; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for `__slots__` definitions that are not ordered according to a @@ -105,43 +105,56 @@ impl Violation for UnsortedDunderSlots { const SORTING_STYLE: SortingStyle = SortingStyle::Natural; +/// RUF023 /// Sort a tuple, list, dict or set that defines `__slots__` in a class scope. /// /// This routine checks whether the display is sorted, and emits a /// violation if it is not sorted. If the tuple/list/set was not sorted, /// it attempts to set a `Fix` on the violation. -pub(crate) fn sort_dunder_slots(checker: &Checker, binding: &Binding) -> Option { +pub(crate) fn sort_dunder_slots(checker: &Checker, binding: &Binding) { let semantic = checker.semantic(); - let (target, value) = match binding.statement(semantic)? { + let Some(stmt) = binding.statement(semantic) else { + return; + }; + + let (target, value) = match stmt { ast::Stmt::Assign(ast::StmtAssign { targets, value, .. }) => match targets.as_slice() { [target] => (target, &**value), - _ => return None, + _ => return, }, - ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => { - (&**target, value.as_deref()?) - } - _ => return None, + ast::Stmt::AnnAssign(ast::StmtAnnAssign { + target, + value: Some(value), + .. + }) => (&**target, &**value), + _ => return, }; - let ast::ExprName { id, .. } = target.as_name_expr()?; + let Some(ast::ExprName { id, .. }) = target.as_name_expr() else { + return; + }; if id != "__slots__" { - return None; + return; } // We're only interested in `__slots__` in the class scope - let enclosing_class = semantic.scopes[binding.scope].kind.as_class()?; + let Some(enclosing_class) = semantic.scopes[binding.scope].kind.as_class() else { + return; + }; // and it has to be an assignment to a "display literal" (a literal dict/set/tuple/list) - let display = StringLiteralDisplay::new(value)?; + let Some(display) = StringLiteralDisplay::new(value) else { + return; + }; let sort_classification = SortClassification::of_elements(&display.elts, SORTING_STYLE); if sort_classification.is_not_a_list_of_string_literals() || sort_classification.is_sorted() { - return None; + return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnsortedDunderSlots { class_name: enclosing_class.name.id.clone(), }, @@ -163,8 +176,6 @@ pub(crate) fn sort_dunder_slots(checker: &Checker, binding: &Binding) -> Option< diagnostic.set_fix(Fix::applicable_edit(edit, applicability)); } } - - Some(diagnostic) } /// Struct representing a [display](https://docs.python.org/3/reference/expressions.html#displays-for-lists-sets-and-dictionaries) @@ -174,7 +185,7 @@ struct StringLiteralDisplay<'a> { /// The elts from the original AST node representing the display. /// Each elt is the AST representation of a single string literal /// element in the display - elts: Cow<'a, Vec>, + elts: Cow<'a, [ast::Expr]>, /// The source-code range of the display as a whole range: TextRange, /// What kind of a display is it? A dict, set, list or tuple? @@ -213,7 +224,11 @@ impl<'a> StringLiteralDisplay<'a> { kind, } } - ast::Expr::Set(ast::ExprSet { elts, range }) => { + ast::Expr::Set(ast::ExprSet { + elts, + range, + node_index: _, + }) => { let kind = DisplayKind::Sequence(SequenceKind::Set); Self { elts: Cow::Borrowed(elts), diff --git a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs index d47d5dad00d0f..c5ecfabbe4ff3 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs @@ -1,10 +1,11 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{parenthesize::parenthesized_range, Expr, ExprCall}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Expr, ExprCall, parenthesize::parenthesized_range}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange}; +use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; + /// ## What it does /// Checks for `itertools.starmap` calls where the second argument is a `zip` call. /// @@ -89,13 +90,11 @@ pub(crate) fn starmap_zip(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = Diagnostic::new(StarmapZip, call.range); + let mut diagnostic = checker.report_diagnostic(StarmapZip, call.range); if let Some(fix) = replace_with_map(call, iterable_call, checker) { diagnostic.set_fix(fix); } - - checker.report_diagnostic(diagnostic); } /// Replace the `starmap` call with a call to the `map` builtin, if `map` has not been shadowed. diff --git a/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs b/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs index a5b512013cb76..63fd8e361446d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/static_key_dict_comprehension.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## Removed /// This rule was implemented in `flake8-bugbear` and has been remapped to [B035] diff --git a/crates/ruff_linter/src/rules/ruff/rules/suppression_comment_visitor.rs b/crates/ruff_linter/src/rules/ruff/rules/suppression_comment_visitor.rs index 19ee1372d1e63..c99dec47ca6cf 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/suppression_comment_visitor.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/suppression_comment_visitor.rs @@ -1,12 +1,12 @@ use std::iter::Peekable; use ruff_python_ast::{ + AnyNodeRef, Suite, helpers::comment_indentation_after, visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}, - AnyNodeRef, Suite, }; use ruff_python_trivia::{ - indentation_at_offset, CommentLinePosition, SimpleTokenizer, SuppressionKind, + CommentLinePosition, SimpleTokenizer, SuppressionKind, indentation_at_offset, }; use ruff_text_size::{Ranged, TextRange, TextSize}; diff --git a/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs b/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs index 5faf5242cad1b..3fc282ad3a09a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/test_rules.rs @@ -13,13 +13,14 @@ /// /// Rules that provide a fix _must_ not raise unconditionally or the linter /// will not converge. -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_trivia::CommentRanges; use ruff_text_size::TextSize; -use crate::registry::Rule; use crate::Locator; +use crate::checkers::ast::LintContext; +use crate::registry::Rule; +use crate::{Edit, Fix, FixAvailability, Violation}; /// Check if a comment exists anywhere in a given file fn comment_exists(text: &str, locator: &Locator, comment_ranges: &CommentRanges) -> bool { @@ -48,7 +49,7 @@ pub(crate) const TEST_RULES: &[Rule] = &[ ]; pub(crate) trait TestRule { - fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges) -> Option; + fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges, context: &LintContext); } /// ## What it does @@ -79,11 +80,8 @@ impl Violation for StableTestRule { } impl TestRule for StableTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - StableTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(StableTestRule, ruff_text_size::TextRange::default()); } } @@ -115,15 +113,12 @@ impl Violation for StableTestRuleSafeFix { } impl TestRule for StableTestRuleSafeFix { - fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges) -> Option { + fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges, context: &LintContext) { let comment = "# fix from stable-test-rule-safe-fix\n".to_string(); - if comment_exists(&comment, locator, comment_ranges) { - None - } else { - Some( - Diagnostic::new(StableTestRuleSafeFix, ruff_text_size::TextRange::default()) - .with_fix(Fix::safe_edit(Edit::insertion(comment, TextSize::new(0)))), - ) + if !comment_exists(&comment, locator, comment_ranges) { + context + .report_diagnostic(StableTestRuleSafeFix, ruff_text_size::TextRange::default()) + .set_fix(Fix::safe_edit(Edit::insertion(comment, TextSize::new(0)))); } } } @@ -156,18 +151,15 @@ impl Violation for StableTestRuleUnsafeFix { } impl TestRule for StableTestRuleUnsafeFix { - fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges) -> Option { + fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges, context: &LintContext) { let comment = "# fix from stable-test-rule-unsafe-fix\n".to_string(); - if comment_exists(&comment, locator, comment_ranges) { - None - } else { - Some( - Diagnostic::new( + if !comment_exists(&comment, locator, comment_ranges) { + context + .report_diagnostic( StableTestRuleUnsafeFix, ruff_text_size::TextRange::default(), ) - .with_fix(Fix::unsafe_edit(Edit::insertion(comment, TextSize::new(0)))), - ) + .set_fix(Fix::unsafe_edit(Edit::insertion(comment, TextSize::new(0)))); } } } @@ -200,21 +192,18 @@ impl Violation for StableTestRuleDisplayOnlyFix { } impl TestRule for StableTestRuleDisplayOnlyFix { - fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges) -> Option { + fn diagnostic(locator: &Locator, comment_ranges: &CommentRanges, context: &LintContext) { let comment = "# fix from stable-test-rule-display-only-fix\n".to_string(); - if comment_exists(&comment, locator, comment_ranges) { - None - } else { - Some( - Diagnostic::new( + if !comment_exists(&comment, locator, comment_ranges) { + context + .report_diagnostic( StableTestRuleDisplayOnlyFix, ruff_text_size::TextRange::default(), ) - .with_fix(Fix::display_only_edit(Edit::insertion( + .set_fix(Fix::display_only_edit(Edit::insertion( comment, TextSize::new(0), - ))), - ) + ))); } } } @@ -247,11 +236,8 @@ impl Violation for PreviewTestRule { } impl TestRule for PreviewTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - PreviewTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(PreviewTestRule, ruff_text_size::TextRange::default()); } } @@ -283,11 +269,8 @@ impl Violation for DeprecatedTestRule { } impl TestRule for DeprecatedTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - DeprecatedTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(DeprecatedTestRule, ruff_text_size::TextRange::default()); } } @@ -319,11 +302,11 @@ impl Violation for AnotherDeprecatedTestRule { } impl TestRule for AnotherDeprecatedTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic( AnotherDeprecatedTestRule, ruff_text_size::TextRange::default(), - )) + ); } } @@ -355,11 +338,8 @@ impl Violation for RemovedTestRule { } impl TestRule for RemovedTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - RemovedTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(RemovedTestRule, ruff_text_size::TextRange::default()); } } @@ -391,11 +371,8 @@ impl Violation for AnotherRemovedTestRule { } impl TestRule for AnotherRemovedTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - AnotherRemovedTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(AnotherRemovedTestRule, ruff_text_size::TextRange::default()); } } @@ -427,11 +404,8 @@ impl Violation for RedirectedFromTestRule { } impl TestRule for RedirectedFromTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - RedirectedFromTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(RedirectedFromTestRule, ruff_text_size::TextRange::default()); } } @@ -463,11 +437,8 @@ impl Violation for RedirectedToTestRule { } impl TestRule for RedirectedToTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( - RedirectedToTestRule, - ruff_text_size::TextRange::default(), - )) + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic(RedirectedToTestRule, ruff_text_size::TextRange::default()); } } @@ -499,10 +470,10 @@ impl Violation for RedirectedFromPrefixTestRule { } impl TestRule for RedirectedFromPrefixTestRule { - fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges) -> Option { - Some(Diagnostic::new( + fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) { + context.report_diagnostic( RedirectedFromPrefixTestRule, ruff_text_size::TextRange::default(), - )) + ); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index 6a2d318a08c78..39c45b19e80f5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -1,18 +1,18 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{Arguments, Expr, ExprCall}; -use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_python_semantic::SemanticModel; -use ruff_python_trivia::{lines_after_ignoring_trivia, CommentRanges}; +use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; +use ruff_python_trivia::{CommentRanges, lines_after_ignoring_trivia}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; +use crate::Locator; use crate::checkers::ast::Checker; use crate::rules::ruff::rules::unnecessary_round::{ - rounded_and_ndigits, InferredType, NdigitsValue, RoundedValue, + InferredType, NdigitsValue, RoundedValue, rounded_and_ndigits, }; -use crate::Locator; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does /// Checks for `int` conversions of values that are already integers. @@ -88,9 +88,9 @@ pub(crate) fn unnecessary_cast_to_int(checker: &Checker, call: &ExprCall) { checker.comment_ranges(), checker.source(), ); - let diagnostic = Diagnostic::new(UnnecessaryCastToInt, call.range()); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(UnnecessaryCastToInt, call.range()) + .set_fix(fix); } /// Creates a fix that replaces `int(expression)` with `expression`. @@ -158,9 +158,10 @@ fn call_applicability(checker: &Checker, inner_call: &ExprCall) -> Option { - Some(Applicability::Safe) - } + | [ + "math", + "comb" | "factorial" | "gcd" | "lcm" | "isqrt" | "perm", + ] => Some(Applicability::Safe), // Depends on `ndigits` and `number.__round__` ["" | "builtins", "round"] => round_applicability(arguments, checker.semantic()), diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs index 8f0069b7cd5cf..ae09b7f544a65 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Comprehension, Expr, Int}; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::builtins::is_iterator; @@ -9,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks the following constructs, all of which can be replaced by @@ -115,7 +115,7 @@ pub(crate) fn unnecessary_iterable_allocation_for_first_element(checker: &Checke Cow::Owned(format!("iter({iterable})")) }; - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryIterableAllocationForFirstElement { iterable: SourceCodeSnippet::new(iterable.to_string()), }, @@ -126,8 +126,6 @@ pub(crate) fn unnecessary_iterable_allocation_for_first_element(checker: &Checke format!("next({iterable})"), expr.range(), ))); - - checker.report_diagnostic(diagnostic); } /// Check that the slice [`Expr`] is a slice of the first element (e.g., `x[0]`). @@ -263,9 +261,11 @@ fn match_iteration_target(expr: &Expr, semantic: &SemanticModel) -> Option Option { - let [generator @ Comprehension { - is_async: false, .. - }] = generators + let [ + generator @ Comprehension { + is_async: false, .. + }, + ] = generators else { return None; }; diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs index 4a443fa1997f0..72887978b3b6d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_key_check.rs @@ -1,13 +1,13 @@ use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::parenthesize::parenthesized_range; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for unnecessary key checks prior to accessing a dictionary. @@ -102,7 +102,7 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) { return; } - let mut diagnostic = Diagnostic::new(UnnecessaryKeyCheck, expr.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryKeyCheck, expr.range()); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( format!( "{}.get({})", @@ -127,5 +127,4 @@ pub(crate) fn unnecessary_key_check(checker: &Checker, expr: &Expr) { ), expr.range(), ))); - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs index 47b3f712adda0..51561e4e1f701 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs @@ -1,9 +1,13 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_diagnostics::{Applicability, Edit}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; +use crate::checkers::ast::Checker; +use crate::fix::edits::{Parentheses, remove_argument}; +use crate::{Fix, FixAvailability, Violation}; + /// ## What it does /// Checks for usages of `collections.deque` that have an empty iterable as the first argument. /// @@ -28,6 +32,15 @@ use ruff_text_size::Ranged; /// queue = deque(maxlen=10) /// ``` /// +/// ## Fix safety +/// +/// The fix is marked as unsafe whenever it would delete comments present in the `deque` call or if +/// there are unrecognized arguments other than `iterable` and `maxlen`. +/// +/// ## Fix availability +/// +/// This rule's fix is unavailable if any starred arguments are present after the initial iterable. +/// /// ## References /// - [Python documentation: `collections.deque`](https://docs.python.org/3/library/collections.html#collections.deque) #[derive(ViolationMetadata)] @@ -35,19 +48,21 @@ pub(crate) struct UnnecessaryEmptyIterableWithinDequeCall { has_maxlen: bool, } -impl AlwaysFixableViolation for UnnecessaryEmptyIterableWithinDequeCall { +impl Violation for UnnecessaryEmptyIterableWithinDequeCall { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Unnecessary empty iterable within a deque call".to_string() } - fn fix_title(&self) -> String { + fn fix_title(&self) -> Option { let title = if self.has_maxlen { "Replace with `deque(maxlen=...)`" } else { "Replace with `deque()`" }; - title.to_string() + Some(title.to_string()) } } @@ -64,13 +79,13 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a return; } - let Some(iterable) = arguments.find_argument_value("iterable", 0) else { + let Some(iterable) = arguments.find_argument("iterable", 0) else { return; }; let maxlen = arguments.find_argument_value("maxlen", 1); - let is_empty_literal = match iterable { + let is_empty_literal = match iterable.value() { Expr::Dict(dict) => dict.is_empty(), Expr::List(list) => list.is_empty(), Expr::Tuple(tuple) => tuple.is_empty(), @@ -85,36 +100,78 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a }) && call.arguments.is_empty() } + Expr::StringLiteral(string) => string.value.is_empty(), + Expr::BytesLiteral(bytes) => bytes.value.is_empty(), + Expr::FString(fstring) => fstring.value.is_empty_literal(), _ => false, }; if !is_empty_literal { return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryEmptyIterableWithinDequeCall { has_maxlen: maxlen.is_some(), }, deque.range, ); - diagnostic.set_fix(fix_unnecessary_literal_in_deque(checker, deque, maxlen)); + // Return without a fix in the presence of a starred argument because we can't accurately + // generate the fix. If all of the arguments are unpacked (e.g. `deque(*([], 10))`), we will + // have already returned after the first `find_argument_value` call. + if deque.arguments.args.iter().any(Expr::is_starred_expr) { + return; + } - checker.report_diagnostic(diagnostic); + diagnostic.try_set_fix(|| fix_unnecessary_literal_in_deque(checker, iterable, deque, maxlen)); } fn fix_unnecessary_literal_in_deque( checker: &Checker, + iterable: ast::ArgOrKeyword, deque: &ast::ExprCall, maxlen: Option<&Expr>, -) -> Fix { - let deque_name = checker.locator().slice(deque.func.range()); - let deque_str = match maxlen { - Some(maxlen) => { - let len_str = checker.locator().slice(maxlen); - format!("{deque_name}(maxlen={len_str})") - } - None => format!("{deque_name}()"), +) -> anyhow::Result { + // if `maxlen` is `Some`, we know there were exactly two arguments, and we can replace the whole + // call. otherwise, we only delete the `iterable` argument and leave the others untouched. + let edit = if let Some(maxlen) = maxlen { + let deque_name = checker.locator().slice( + parenthesized_range( + deque.func.as_ref().into(), + deque.into(), + checker.comment_ranges(), + checker.source(), + ) + .unwrap_or(deque.func.range()), + ); + let len_str = checker.locator().slice(maxlen); + let deque_str = format!("{deque_name}(maxlen={len_str})"); + Edit::range_replacement(deque_str, deque.range) + } else { + let range = parenthesized_range( + iterable.value().into(), + (&deque.arguments).into(), + checker.comment_ranges(), + checker.source(), + ) + .unwrap_or(iterable.range()); + remove_argument( + &range, + &deque.arguments, + Parentheses::Preserve, + checker.source(), + checker.comment_ranges(), + )? + }; + let has_comments = checker.comment_ranges().intersects(edit.range()); + // we've already checked maxlen.is_some() && args != 2 above, so this is the only problematic + // case left + let unknown_arguments = maxlen.is_none() && deque.arguments.len() != 1; + let applicability = if has_comments || unknown_arguments { + Applicability::Unsafe + } else { + Applicability::Safe }; - Fix::safe_edit(Edit::range_replacement(deque_str, deque.range)) + + Ok(Fix::applicable_edit(edit, applicability)) } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs index 772f65ded5548..8e815c601b6b1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_nested_literal.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{AnyNodeRef, Expr, ExprContext, ExprSubscript, ExprTuple}; use ruff_python_semantic::analyze::typing::traverse_literal; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for unnecessary nested `Literal`. @@ -74,7 +74,7 @@ impl Violation for UnnecessaryNestedLiteral { } } -/// RUF039 +/// RUF041 pub(crate) fn unnecessary_nested_literal<'a>(checker: &Checker, literal_expr: &'a Expr) { let mut is_nested = false; @@ -105,7 +105,7 @@ pub(crate) fn unnecessary_nested_literal<'a>(checker: &Checker, literal_expr: &' literal_expr, ); - let mut diagnostic = Diagnostic::new(UnnecessaryNestedLiteral, literal_expr.range()); + let mut diagnostic = checker.report_diagnostic(UnnecessaryNestedLiteral, literal_expr.range()); // Create a [`Fix`] that flattens all nodes. if let Expr::Subscript(subscript) = literal_expr { @@ -116,12 +116,14 @@ pub(crate) fn unnecessary_nested_literal<'a>(checker: &Checker, literal_expr: &' Expr::Tuple(ExprTuple { elts: nodes.into_iter().cloned().collect(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, parenthesized: false, }) }), value: subscript.value.clone(), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), ctx: ExprContext::Load, }); let fix = Fix::applicable_edit( @@ -134,6 +136,4 @@ pub(crate) fn unnecessary_nested_literal<'a>(checker: &Checker, literal_expr: &' ); diagnostic.set_fix(fix); } - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs index 343c3cdec05a8..7656a494fa738 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs @@ -1,6 +1,5 @@ use itertools::Itertools; -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral, ExprUnaryOp, Identifier, UnaryOp, @@ -10,6 +9,7 @@ use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::TextRange; use crate::checkers::ast::Checker; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// @@ -116,7 +116,7 @@ pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall) let new_expr = re_func.replacement(); let repl = new_expr.map(|expr| checker.generator().expr(&expr)); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnnecessaryRegularExpression { replacement: repl.clone(), }, @@ -133,8 +133,6 @@ pub(crate) fn unnecessary_regular_expression(checker: &Checker, call: &ExprCall) }, )); } - - checker.report_diagnostic(diagnostic); } /// The `re` functions supported by this rule. @@ -279,6 +277,7 @@ impl<'a> ReFunc<'a> { op: UnaryOp::Not, operand: Box::new(expr), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Some(negated_expr) } @@ -302,6 +301,7 @@ impl<'a> ReFunc<'a> { ops: Box::new([op]), comparators: Box::new([right.clone()]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } @@ -313,6 +313,7 @@ impl<'a> ReFunc<'a> { attr: Identifier::new(method, TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }); Expr::Call(ExprCall { func: Box::new(method), @@ -320,8 +321,10 @@ impl<'a> ReFunc<'a> { args: args.into_boxed_slice(), keywords: Box::new([]), range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }, range: TextRange::default(), + node_index: ruff_python_ast::AtomicNodeIndex::dummy(), }) } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs index 6edf01cc1ba7c..baf5ea38a5a4d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs @@ -1,14 +1,15 @@ -use crate::checkers::ast::Checker; -use crate::Locator; -use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{Arguments, Expr, ExprCall, ExprNumberLiteral, Number}; +use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_python_semantic::analyze::typing; -use ruff_python_semantic::SemanticModel; use ruff_source_file::find_newline; use ruff_text_size::Ranged; +use crate::Locator; +use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; + /// ## What it does /// Checks for `round()` calls that have no effect on the input. /// @@ -27,6 +28,11 @@ use ruff_text_size::Ranged; /// ```python /// a = 1 /// ``` +/// +/// ## Fix safety +/// +/// The fix is marked unsafe if it is not possible to guarantee that the first argument of +/// `round()` is of type `int`, or if the fix deletes comments. #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryRound; @@ -94,9 +100,9 @@ pub(crate) fn unnecessary_round(checker: &Checker, call: &ExprCall) { let edit = unwrap_round_call(call, rounded, checker.semantic(), checker.locator()); let fix = Fix::applicable_edit(edit, applicability); - let diagnostic = Diagnostic::new(UnnecessaryRound, call.range()); - - checker.report_diagnostic(diagnostic.with_fix(fix)); + checker + .report_diagnostic(UnnecessaryRound, call.range()) + .set_fix(fix); } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs b/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs index 3b1fedbae2722..557cab426998f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unraw_re_pattern.rs @@ -1,8 +1,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ BytesLiteral, Expr, ExprBytesLiteral, ExprCall, ExprStringLiteral, StringLiteral, }; @@ -11,6 +10,7 @@ use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Reports the following `re` and `regex` calls when @@ -161,7 +161,7 @@ fn check_string(checker: &Checker, literal: &StringLiteral, module: RegexModule, let kind = PatternKind::String; let func = func.to_string(); let range = literal.range; - let mut diagnostic = Diagnostic::new(UnrawRePattern { module, func, kind }, range); + let mut diagnostic = checker.report_diagnostic(UnrawRePattern { module, func, kind }, range); if // The (no-op) `u` prefix is a syntax error when combined with `r` @@ -177,7 +177,6 @@ fn check_string(checker: &Checker, literal: &StringLiteral, module: RegexModule, literal.range().start(), ))); } - checker.report_diagnostic(diagnostic); } fn check_bytes(checker: &Checker, literal: &BytesLiteral, module: RegexModule, func: &str) { @@ -188,7 +187,5 @@ fn check_bytes(checker: &Checker, literal: &BytesLiteral, module: RegexModule, f let kind = PatternKind::Bytes; let func = func.to_string(); let range = literal.range; - let diagnostic = Diagnostic::new(UnrawRePattern { module, func, kind }, range); - - checker.report_diagnostic(diagnostic); + checker.report_diagnostic(UnrawRePattern { module, func, kind }, range); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs b/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs index 25769b88c759c..2cc04e602b513 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## Removed /// This rule was implemented in `bandit` and has been remapped to diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs index e3fb4cb8d960c..b7c55dc0b0e43 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_async.rs @@ -1,12 +1,13 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor::source_order; use ruff_python_ast::{self as ast, AnyNodeRef, Expr, Stmt}; -use ruff_python_semantic::analyze::function_type::is_stub; use ruff_python_semantic::Modules; +use ruff_python_semantic::analyze::function_type::is_stub; +use crate::Violation; use crate::checkers::ast::Checker; + use crate::rules::fastapi::rules::is_fastapi_route; /// ## What it does @@ -188,11 +189,11 @@ pub(crate) fn unused_async( }; if !found_await_or_async { - checker.report_diagnostic(Diagnostic::new( + checker.report_diagnostic( UnusedAsync { name: name.to_string(), }, function_def.identifier(), - )); + ); } } diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs index 02f4bceb444b5..52e367500b030 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs @@ -1,7 +1,8 @@ use itertools::Itertools; -use ruff_diagnostics::AlwaysFixableViolation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::AlwaysFixableViolation; #[derive(Debug, PartialEq, Eq)] pub(crate) struct UnusedCodes { diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs index 3229e33665923..16a893d1ea06a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_unpacked_variable.rs @@ -1,9 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Binding; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::renamer::ShadowedKind; +use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for the presence of unused variables in unpacked assignments. @@ -63,7 +64,12 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option { let name = binding.name(checker.source()); let renamed = format!("_{name}"); - if checker.settings.dummy_variable_rgx.is_match(&renamed) { + + if ShadowedKind::new(binding, &renamed, checker).shadows_any() { + return None; + } + + if checker.settings().dummy_variable_rgx.is_match(&renamed) { let edit = Edit::range_replacement(renamed, binding.range()); return Some(Fix::unsafe_edit(edit).isolate(isolation)); @@ -78,7 +84,7 @@ pub(crate) fn unused_unpacked_variable(checker: &Checker, name: &str, binding: & return; } - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UnusedUnpackedVariable { name: name.to_string(), }, @@ -87,5 +93,4 @@ pub(crate) fn unused_unpacked_variable(checker: &Checker, name: &str, binding: & if let Some(fix) = remove_unused_variable(binding, checker) { diagnostic.set_fix(fix); } - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs index 938c24b48c8e4..a87fc40273fae 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs @@ -1,10 +1,10 @@ -use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::is_dunder; use ruff_python_semantic::{Binding, BindingId}; use ruff_python_stdlib::identifiers::is_identifier; use ruff_text_size::Ranged; +use crate::{Fix, FixAvailability, Violation}; use crate::{ checkers::ast::Checker, renamer::{Renamer, ShadowedKind}, @@ -98,20 +98,16 @@ impl Violation for UsedDummyVariable { } /// RUF052 -pub(crate) fn used_dummy_variable( - checker: &Checker, - binding: &Binding, - binding_id: BindingId, -) -> Option { +pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_id: BindingId) { let name = binding.name(checker.source()); // Ignore `_` and dunder variables if name == "_" || is_dunder(name) { - return None; + return; } // only used variables if binding.is_unused() { - return None; + return; } // We only emit the lint on variables defined via assignments. @@ -131,12 +127,12 @@ pub(crate) fn used_dummy_variable( // - // - if !binding.kind.is_assignment() { - return None; + return; } // This excludes `global` and `nonlocal` variables. if binding.is_global() || binding.is_nonlocal() { - return None; + return; } let semantic = checker.semantic(); @@ -144,7 +140,7 @@ pub(crate) fn used_dummy_variable( // Only variables defined in function scopes let scope = &semantic.scopes[binding.scope]; if !scope.kind.is_function() { - return None; + return; } // Recall from above that we do not wish to flag "private" @@ -159,21 +155,22 @@ pub(crate) fn used_dummy_variable( .shadowed_bindings(binding_id) .any(|shadow_id| semantic.binding(shadow_id).kind.is_argument()) { - return None; + return; } - if !checker.settings.dummy_variable_rgx.is_match(name) { - return None; + if !checker.settings().dummy_variable_rgx.is_match(name) { + return; } // If the name doesn't start with an underscore, we don't consider it for a fix if !name.starts_with('_') { - return Some(Diagnostic::new( + checker.report_diagnostic( UsedDummyVariable { name: name.to_string(), shadowed_kind: None, }, binding.range(), - )); + ); + return; } // Trim the leading underscores for further checks @@ -181,7 +178,7 @@ pub(crate) fn used_dummy_variable( let shadowed_kind = ShadowedKind::new(binding, trimmed_name, checker); - let mut diagnostic = Diagnostic::new( + let mut diagnostic = checker.report_diagnostic( UsedDummyVariable { name: name.to_string(), shadowed_kind: Some(shadowed_kind), @@ -196,8 +193,6 @@ pub(crate) fn used_dummy_variable( .map(|(edit, rest)| Fix::unsafe_edits(edit, rest)) }); } - - Some(diagnostic) } /// Suggests a potential alternative name to resolve a shadowing conflict. @@ -216,7 +211,7 @@ fn get_possible_new_name( }; // Check if the fix name is again dummy identifier - if checker.settings.dummy_variable_rgx.is_match(&fix_name) { + if checker.settings().dummy_variable_rgx.is_match(&fix_name) { return None; } diff --git a/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs b/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs index 9543d9762283a..6d7502dd56e50 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/useless_if_else.rs @@ -1,9 +1,10 @@ -use crate::checkers::ast::Checker; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_ast::comparable::ComparableExpr; +use crate::Violation; +use crate::checkers::ast::Checker; + /// ## What it does /// Checks for useless `if`-`else` conditions with identical arms. /// @@ -44,5 +45,5 @@ pub(crate) fn useless_if_else(checker: &Checker, if_expr: &ast::ExprIf) { return; } - checker.report_diagnostic(Diagnostic::new(UselessIfElse, *range)); + checker.report_diagnostic(UselessIfElse, *range); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs b/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs index 9a473eb32b7c6..2c5396f8b5e32 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/zip_instead_of_pairwise.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr, Int}; use ruff_text_size::Ranged; +use crate::{Edit, Fix, FixAvailability, Violation}; use crate::{checkers::ast::Checker, importer::ImportRequest}; /// ## What it does @@ -29,6 +29,14 @@ use crate::{checkers::ast::Checker, importer::ImportRequest}; /// pairwise(letters) # ("A", "B"), ("B", "C"), ("C", "D") /// ``` /// +/// ## Fix safety +/// +/// The fix is always marked unsafe because it assumes that slicing an object +/// (e.g., `obj[1:]`) produces a value with the same type and iteration behavior +/// as the original object, which is not guaranteed for user-defined types that +/// override `__getitem__` without properly handling slices. Moreover, the fix +/// could delete comments. +/// /// ## References /// - [Python documentation: `itertools.pairwise`](https://docs.python.org/3/library/itertools.html#itertools.pairwise) #[derive(ViolationMetadata)] @@ -86,6 +94,7 @@ fn match_slice_info(expr: &Expr) -> Option { let Expr::NumberLiteral(ast::ExprNumberLiteral { value: ast::Number::Int(int), range: _, + node_index: _, }) = lower.as_ref() else { return None; @@ -152,7 +161,7 @@ pub(crate) fn zip_instead_of_pairwise(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = Diagnostic::new(ZipInsteadOfPairwise, func.range()); + let mut diagnostic = checker.report_diagnostic(ZipInsteadOfPairwise, func.range()); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( @@ -164,6 +173,4 @@ pub(crate) fn zip_instead_of_pairwise(checker: &Checker, call: &ast::ExprCall) { Edit::range_replacement(format!("{binding}({})", first_arg_info.id), call.range()); Ok(Fix::unsafe_edits(import_edit, [reference_edit])) }); - - checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF005_RUF005_slices.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005_slices.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF005_RUF005_slices.py.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005_slices.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap index 5ed5adb316ebb..08faee3866e73 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs -snapshot_kind: text --- RUF009_attrs.py:46:41: RUF009 Do not perform function call `default_function` in dataclass defaults | @@ -31,3 +30,79 @@ RUF009_attrs.py:48:34: RUF009 Do not perform function call `ImmutableType` in da 49 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES 50 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES | + +RUF009_attrs.py:108:12: RUF009 Do not perform function call `F` in dataclass defaults + | +106 | @attr.attrs +107 | class H: +108 | f: F = F() + | ^^^ RUF009 +109 | g: G = G() + | + +RUF009_attrs.py:109:12: RUF009 Do not perform function call `G` in dataclass defaults + | +107 | class H: +108 | f: F = F() +109 | g: G = G() + | ^^^ RUF009 + | + +RUF009_attrs.py:114:12: RUF009 Do not perform function call `F` in dataclass defaults + | +112 | @attr.define +113 | class I: +114 | f: F = F() + | ^^^ RUF009 +115 | g: G = G() + | + +RUF009_attrs.py:115:12: RUF009 Do not perform function call `G` in dataclass defaults + | +113 | class I: +114 | f: F = F() +115 | g: G = G() + | ^^^ RUF009 + | + +RUF009_attrs.py:120:12: RUF009 Do not perform function call `F` in dataclass defaults + | +118 | @attr.frozen +119 | class J: +120 | f: F = F() + | ^^^ RUF009 +121 | g: G = G() + | + +RUF009_attrs.py:121:12: RUF009 Do not perform function call `G` in dataclass defaults + | +119 | class J: +120 | f: F = F() +121 | g: G = G() + | ^^^ RUF009 + | + +RUF009_attrs.py:126:12: RUF009 Do not perform function call `F` in dataclass defaults + | +124 | @attr.mutable +125 | class K: +126 | f: F = F() + | ^^^ RUF009 +127 | g: G = G() + | + +RUF009_attrs.py:127:12: RUF009 Do not perform function call `G` in dataclass defaults + | +125 | class K: +126 | f: F = F() +127 | g: G = G() + | ^^^ RUF009 + | + +RUF009_attrs.py:144:20: RUF009 Do not perform function call `list` in dataclass defaults + | +142 | @attr.attributes +143 | class TestAttrAttributes: +144 | x: list[int] = list() # RUF009 + | ^^^^^^ RUF009 + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap index 1910bdf6f9d0e..7c3cdf80a69e6 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap @@ -244,4 +244,134 @@ RUF010.py:35:20: RUF010 [*] Use explicit conversion flag 35 |+ f" that flows {obj!r} of type {type(obj)}.{additional_message}" # RUF010 36 36 | ) 37 37 | -38 38 | +38 38 | + +RUF010.py:40:4: RUF010 [*] Use explicit conversion flag + | +39 | # https://github.com/astral-sh/ruff/issues/16325 +40 | f"{str({})}" + | ^^^^^^^ RUF010 +41 | +42 | f"{str({} | {})}" + | + = help: Replace with conversion flag + +ℹ Safe fix +37 37 | +38 38 | +39 39 | # https://github.com/astral-sh/ruff/issues/16325 +40 |-f"{str({})}" + 40 |+f"{ {}!s}" +41 41 | +42 42 | f"{str({} | {})}" +43 43 | + +RUF010.py:42:4: RUF010 [*] Use explicit conversion flag + | +40 | f"{str({})}" +41 | +42 | f"{str({} | {})}" + | ^^^^^^^^^^^^ RUF010 +43 | +44 | import builtins + | + = help: Replace with conversion flag + +ℹ Safe fix +39 39 | # https://github.com/astral-sh/ruff/issues/16325 +40 40 | f"{str({})}" +41 41 | +42 |-f"{str({} | {})}" + 42 |+f"{ {} | {}!s}" +43 43 | +44 44 | import builtins +45 45 | + +RUF010.py:46:4: RUF010 [*] Use explicit conversion flag + | +44 | import builtins +45 | +46 | f"{builtins.repr(1)}" + | ^^^^^^^^^^^^^^^^ RUF010 +47 | +48 | f"{repr(1)=}" + | + = help: Replace with conversion flag + +ℹ Safe fix +43 43 | +44 44 | import builtins +45 45 | +46 |-f"{builtins.repr(1)}" + 46 |+f"{1!r}" +47 47 | +48 48 | f"{repr(1)=}" +49 49 | + +RUF010.py:48:4: RUF010 Use explicit conversion flag + | +46 | f"{builtins.repr(1)}" +47 | +48 | f"{repr(1)=}" + | ^^^^^^^ RUF010 +49 | +50 | f"{repr(lambda: 1)}" + | + = help: Replace with conversion flag + +RUF010.py:50:4: RUF010 [*] Use explicit conversion flag + | +48 | f"{repr(1)=}" +49 | +50 | f"{repr(lambda: 1)}" + | ^^^^^^^^^^^^^^^ RUF010 +51 | +52 | f"{repr(x := 2)}" + | + = help: Replace with conversion flag + +ℹ Safe fix +47 47 | +48 48 | f"{repr(1)=}" +49 49 | +50 |-f"{repr(lambda: 1)}" + 50 |+f"{(lambda: 1)!r}" +51 51 | +52 52 | f"{repr(x := 2)}" +53 53 | + +RUF010.py:52:4: RUF010 [*] Use explicit conversion flag + | +50 | f"{repr(lambda: 1)}" +51 | +52 | f"{repr(x := 2)}" + | ^^^^^^^^^^^^ RUF010 +53 | +54 | f"{str(object=3)}" + | + = help: Replace with conversion flag + +ℹ Safe fix +49 49 | +50 50 | f"{repr(lambda: 1)}" +51 51 | +52 |-f"{repr(x := 2)}" + 52 |+f"{(x := 2)!r}" +53 53 | +54 54 | f"{str(object=3)}" + +RUF010.py:54:4: RUF010 [*] Use explicit conversion flag + | +52 | f"{repr(x := 2)}" +53 | +54 | f"{str(object=3)}" + | ^^^^^^^^^^^^^ RUF010 + | + = help: Replace with conversion flag + +ℹ Safe fix +51 51 | +52 52 | f"{repr(x := 2)}" +53 53 | +54 |-f"{str(object=3)}" + 54 |+f"{3!s}" diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap index 288b5bded4d30..caf3f33b3db13 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs -snapshot_kind: text --- RUF037.py:6:13: RUF037 [*] Unnecessary empty iterable within a deque call | @@ -127,3 +126,241 @@ RUF037.py:30:13: RUF037 [*] Unnecessary empty iterable within a deque call 31 31 | 32 32 | 33 33 | def f(): + +RUF037.py:61:13: RUF037 [*] Unnecessary empty iterable within a deque call + | +60 | def f(): +61 | x = 0 or(deque)([]) + | ^^^^^^^^^^^ RUF037 + | + = help: Replace with `deque()` + +ℹ Safe fix +58 58 | queue = deque() # Ok +59 59 | +60 60 | def f(): +61 |- x = 0 or(deque)([]) + 61 |+ x = 0 or(deque)() +62 62 | +63 63 | +64 64 | # regression tests for https://github.com/astral-sh/ruff/issues/18612 + +RUF037.py:66:5: RUF037 Unnecessary empty iterable within a deque call + | +64 | # regression tests for https://github.com/astral-sh/ruff/issues/18612 +65 | def f(): +66 | deque([], *[10]) # RUF037 but no fix + | ^^^^^^^^^^^^^^^^ RUF037 +67 | deque([], **{"maxlen": 10}) # RUF037 +68 | deque([], foo=1) # RUF037 + | + = help: Replace with `deque()` + +RUF037.py:67:5: RUF037 [*] Unnecessary empty iterable within a deque call + | +65 | def f(): +66 | deque([], *[10]) # RUF037 but no fix +67 | deque([], **{"maxlen": 10}) # RUF037 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF037 +68 | deque([], foo=1) # RUF037 + | + = help: Replace with `deque()` + +ℹ Unsafe fix +64 64 | # regression tests for https://github.com/astral-sh/ruff/issues/18612 +65 65 | def f(): +66 66 | deque([], *[10]) # RUF037 but no fix +67 |- deque([], **{"maxlen": 10}) # RUF037 + 67 |+ deque(**{"maxlen": 10}) # RUF037 +68 68 | deque([], foo=1) # RUF037 +69 69 | +70 70 | + +RUF037.py:68:5: RUF037 [*] Unnecessary empty iterable within a deque call + | +66 | deque([], *[10]) # RUF037 but no fix +67 | deque([], **{"maxlen": 10}) # RUF037 +68 | deque([], foo=1) # RUF037 + | ^^^^^^^^^^^^^^^^ RUF037 + | + = help: Replace with `deque()` + +ℹ Unsafe fix +65 65 | def f(): +66 66 | deque([], *[10]) # RUF037 but no fix +67 67 | deque([], **{"maxlen": 10}) # RUF037 +68 |- deque([], foo=1) # RUF037 + 68 |+ deque(foo=1) # RUF037 +69 69 | +70 70 | +71 71 | # Somewhat related to the issue, both okay because we can't generally look + +RUF037.py:80:5: RUF037 [*] Unnecessary empty iterable within a deque call + | +78 | # are deleted +79 | def f(): +80 | / deque( # a comment in deque, deleted +81 | | [ # a comment _in_ the list, deleted +82 | | ], # a comment after the list, deleted +83 | | maxlen=10, # a comment on maxlen, deleted +84 | | ) # only this is preserved + | |_________^ RUF037 + | + = help: Replace with `deque(maxlen=...)` + +ℹ Unsafe fix +77 77 | # The fix was actually always unsafe in the presence of comments. all of these +78 78 | # are deleted +79 79 | def f(): +80 |- deque( # a comment in deque, deleted +81 |- [ # a comment _in_ the list, deleted +82 |- ], # a comment after the list, deleted +83 |- maxlen=10, # a comment on maxlen, deleted +84 |- ) # only this is preserved + 80 |+ deque(maxlen=10) # only this is preserved +85 81 | +86 82 | +87 83 | # `maxlen` can also be passed positionally + +RUF037.py:89:5: RUF037 [*] Unnecessary empty iterable within a deque call + | +87 | # `maxlen` can also be passed positionally +88 | def f(): +89 | deque([], 10) + | ^^^^^^^^^^^^^ RUF037 + | + = help: Replace with `deque(maxlen=...)` + +ℹ Safe fix +86 86 | +87 87 | # `maxlen` can also be passed positionally +88 88 | def f(): +89 |- deque([], 10) + 89 |+ deque(maxlen=10) +90 90 | +91 91 | +92 92 | def f(): + +RUF037.py:93:5: RUF037 [*] Unnecessary empty iterable within a deque call + | +92 | def f(): +93 | deque([], iterable=[]) + | ^^^^^^^^^^^^^^^^^^^^^^ RUF037 +94 | +95 | # https://github.com/astral-sh/ruff/issues/18854 + | + = help: Replace with `deque()` + +ℹ Unsafe fix +90 90 | +91 91 | +92 92 | def f(): +93 |- deque([], iterable=[]) + 93 |+ deque([]) +94 94 | +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 96 | deque("") + +RUF037.py:96:1: RUF037 [*] Unnecessary empty iterable within a deque call + | +95 | # https://github.com/astral-sh/ruff/issues/18854 +96 | deque("") + | ^^^^^^^^^ RUF037 +97 | deque(b"") +98 | deque(f"") + | + = help: Replace with `deque()` + +ℹ Safe fix +93 93 | deque([], iterable=[]) +94 94 | +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 |-deque("") + 96 |+deque() +97 97 | deque(b"") +98 98 | deque(f"") +99 99 | deque(f"" "") + +RUF037.py:97:1: RUF037 [*] Unnecessary empty iterable within a deque call + | +95 | # https://github.com/astral-sh/ruff/issues/18854 +96 | deque("") +97 | deque(b"") + | ^^^^^^^^^^ RUF037 +98 | deque(f"") +99 | deque(f"" "") + | + = help: Replace with `deque()` + +ℹ Safe fix +94 94 | +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 96 | deque("") +97 |-deque(b"") + 97 |+deque() +98 98 | deque(f"") +99 99 | deque(f"" "") +100 100 | deque(f"" f"") + +RUF037.py:98:1: RUF037 [*] Unnecessary empty iterable within a deque call + | + 96 | deque("") + 97 | deque(b"") + 98 | deque(f"") + | ^^^^^^^^^^ RUF037 + 99 | deque(f"" "") +100 | deque(f"" f"") + | + = help: Replace with `deque()` + +ℹ Safe fix +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 96 | deque("") +97 97 | deque(b"") +98 |-deque(f"") + 98 |+deque() +99 99 | deque(f"" "") +100 100 | deque(f"" f"") +101 101 | deque("abc") # OK + +RUF037.py:99:1: RUF037 [*] Unnecessary empty iterable within a deque call + | + 97 | deque(b"") + 98 | deque(f"") + 99 | deque(f"" "") + | ^^^^^^^^^^^^^ RUF037 +100 | deque(f"" f"") +101 | deque("abc") # OK + | + = help: Replace with `deque()` + +ℹ Safe fix +96 96 | deque("") +97 97 | deque(b"") +98 98 | deque(f"") +99 |-deque(f"" "") + 99 |+deque() +100 100 | deque(f"" f"") +101 101 | deque("abc") # OK +102 102 | deque(b"abc") # OK + +RUF037.py:100:1: RUF037 [*] Unnecessary empty iterable within a deque call + | + 98 | deque(f"") + 99 | deque(f"" "") +100 | deque(f"" f"") + | ^^^^^^^^^^^^^^ RUF037 +101 | deque("abc") # OK +102 | deque(b"abc") # OK + | + = help: Replace with `deque()` + +ℹ Safe fix +97 97 | deque(b"") +98 98 | deque(f"") +99 99 | deque(f"" "") +100 |-deque(f"" f"") + 100 |+deque() +101 101 | deque("abc") # OK +102 102 | deque(b"abc") # OK +103 103 | deque(f"" "a") # OK diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap new file mode 100644 index 0000000000000..61b9115357d0a --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF046_CR.py:1:1: RUF046 [*] Value being cast to `int` is already an integer + | +1 | int(- 1) # Carriage return as newline + | ^^^^^^^^^^^ RUF046 + | + = help: Remove unnecessary `int` call + +ℹ Safe fix +1 |-int(- 1 |+(- 2 2 | 1) # Carriage return as newline diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap new file mode 100644 index 0000000000000..97d44c191d4df --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF046_LF.py:2:1: RUF046 [*] Value being cast to `int` is already an integer + | +1 | # \n as newline +2 | / int(- +3 | | 1) + | |______^ RUF046 + | + = help: Remove unnecessary `int` call + +ℹ Safe fix +1 1 | # \n as newline +2 |-int(- + 2 |+(- +3 3 | 1) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF049_RUF049.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF049_RUF049.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF049_RUF049.py.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF049_RUF049.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF053_RUF053.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF053_RUF053.py.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap index c269084fef655..4d6d820f87b40 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap @@ -323,7 +323,7 @@ RUF056.py:149:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in 149 |+value = not my_dict.get("key") # [RUF056] 150 150 | value = not my_dict.get("key", "") # [RUF056] 151 151 | -152 152 | # testing dict.get call using kwargs +152 152 | # testing invalid dict.get call with inline comment RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. | @@ -332,7 +332,7 @@ RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in 150 | value = not my_dict.get("key", "") # [RUF056] | ^^ RUF056 151 | -152 | # testing dict.get call using kwargs +152 | # testing invalid dict.get call with inline comment | = help: Remove falsy fallback from `dict.get()` @@ -343,142 +343,149 @@ RUF056.py:150:32: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in 150 |-value = not my_dict.get("key", "") # [RUF056] 150 |+value = not my_dict.get("key") # [RUF056] 151 151 | -152 152 | # testing dict.get call using kwargs -153 153 | value = not my_dict.get(key="key", default=False) # [RUF056] +152 152 | # testing invalid dict.get call with inline comment +153 153 | value = not my_dict.get("key", # comment1 -RUF056.py:153:36: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. +RUF056.py:154:22: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. | -152 | # testing dict.get call using kwargs -153 | value = not my_dict.get(key="key", default=False) # [RUF056] - | ^^^^^^^^^^^^^ RUF056 -154 | value = not my_dict.get(default=[], key="key") # [RUF056] +152 | # testing invalid dict.get call with inline comment +153 | value = not my_dict.get("key", # comment1 +154 | [] # comment2 + | ^^ RUF056 +155 | ) # [RUF056] | = help: Remove falsy fallback from `dict.get()` -ℹ Safe fix +ℹ Unsafe fix 150 150 | value = not my_dict.get("key", "") # [RUF056] 151 151 | -152 152 | # testing dict.get call using kwargs -153 |-value = not my_dict.get(key="key", default=False) # [RUF056] - 153 |+value = not my_dict.get(key="key") # [RUF056] -154 154 | value = not my_dict.get(default=[], key="key") # [RUF056] -155 155 | -156 156 | # testing invalid dict.get call with inline comment - -RUF056.py:154:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. - | -152 | # testing dict.get call using kwargs -153 | value = not my_dict.get(key="key", default=False) # [RUF056] -154 | value = not my_dict.get(default=[], key="key") # [RUF056] - | ^^^^^^^^^^ RUF056 -155 | -156 | # testing invalid dict.get call with inline comment +152 152 | # testing invalid dict.get call with inline comment +153 |-value = not my_dict.get("key", # comment1 +154 |- [] # comment2 + 153 |+value = not my_dict.get("key" # comment2 +155 154 | ) # [RUF056] +156 155 | +157 156 | # regression tests for https://github.com/astral-sh/ruff/issues/18628 + +RUF056.py:163:24: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +162 | # extra positional +163 | not my_dict.get("key", False, "?!") + | ^^^^^ RUF056 +164 | +165 | # `default` is positional-only, so these are invalid | = help: Remove falsy fallback from `dict.get()` -ℹ Safe fix -151 151 | -152 152 | # testing dict.get call using kwargs -153 153 | value = not my_dict.get(key="key", default=False) # [RUF056] -154 |-value = not my_dict.get(default=[], key="key") # [RUF056] - 154 |+value = not my_dict.get(key="key") # [RUF056] -155 155 | -156 156 | # testing invalid dict.get call with inline comment -157 157 | value = not my_dict.get("key", # comment1 - -RUF056.py:158:22: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. - | -156 | # testing invalid dict.get call with inline comment -157 | value = not my_dict.get("key", # comment1 -158 | [] # comment2 - | ^^ RUF056 -159 | ) # [RUF056] +RUF056.py:166:24: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +165 | # `default` is positional-only, so these are invalid +166 | not my_dict.get("key", default=False) + | ^^^^^^^^^^^^^ RUF056 +167 | not my_dict.get(key="key", default=False) +168 | not my_dict.get(default=[], key="key") | = help: Remove falsy fallback from `dict.get()` -ℹ Unsafe fix -154 154 | value = not my_dict.get(default=[], key="key") # [RUF056] -155 155 | -156 156 | # testing invalid dict.get call with inline comment -157 |-value = not my_dict.get("key", # comment1 -158 |- [] # comment2 - 157 |+value = not my_dict.get("key" # comment2 -159 158 | ) # [RUF056] -160 159 | -161 160 | # testing invalid dict.get call with kwargs and inline comment - -RUF056.py:163:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. - | -161 | # testing invalid dict.get call with kwargs and inline comment -162 | value = not my_dict.get(key="key", # comment1 -163 | default=False # comment2 - | ^^^^^^^^^^^^^ RUF056 -164 | ) # [RUF056] -165 | value = not my_dict.get(default=[], # comment1 +RUF056.py:167:28: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +165 | # `default` is positional-only, so these are invalid +166 | not my_dict.get("key", default=False) +167 | not my_dict.get(key="key", default=False) + | ^^^^^^^^^^^^^ RUF056 +168 | not my_dict.get(default=[], key="key") +169 | not my_dict.get(default=False) | = help: Remove falsy fallback from `dict.get()` -ℹ Unsafe fix -159 159 | ) # [RUF056] -160 160 | -161 161 | # testing invalid dict.get call with kwargs and inline comment -162 |-value = not my_dict.get(key="key", # comment1 -163 |- default=False # comment2 - 162 |+value = not my_dict.get(key="key" # comment2 -164 163 | ) # [RUF056] -165 164 | value = not my_dict.get(default=[], # comment1 -166 165 | key="key" # comment2 - -RUF056.py:165:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. - | -163 | default=False # comment2 -164 | ) # [RUF056] -165 | value = not my_dict.get(default=[], # comment1 - | ^^^^^^^^^^ RUF056 -166 | key="key" # comment2 -167 | ) # [RUF056] +RUF056.py:168:17: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +166 | not my_dict.get("key", default=False) +167 | not my_dict.get(key="key", default=False) +168 | not my_dict.get(default=[], key="key") + | ^^^^^^^^^^ RUF056 +169 | not my_dict.get(default=False) +170 | not my_dict.get(key="key", other="something", default=False) | = help: Remove falsy fallback from `dict.get()` -ℹ Unsafe fix -162 162 | value = not my_dict.get(key="key", # comment1 -163 163 | default=False # comment2 -164 164 | ) # [RUF056] -165 |-value = not my_dict.get(default=[], # comment1 - 165 |+value = not my_dict.get(# comment1 -166 166 | key="key" # comment2 -167 167 | ) # [RUF056] -168 168 | - -RUF056.py:170:55: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. - | -169 | # testing invalid dict.get calls -170 | value = not my_dict.get(key="key", other="something", default=False) - | ^^^^^^^^^^^^^ RUF056 -171 | value = not my_dict.get(default=False, other="something", key="test") +RUF056.py:169:17: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +167 | not my_dict.get(key="key", default=False) +168 | not my_dict.get(default=[], key="key") +169 | not my_dict.get(default=False) + | ^^^^^^^^^^^^^ RUF056 +170 | not my_dict.get(key="key", other="something", default=False) +171 | not my_dict.get(default=False, other="something", key="test") | = help: Remove falsy fallback from `dict.get()` -ℹ Safe fix -167 167 | ) # [RUF056] -168 168 | -169 169 | # testing invalid dict.get calls -170 |-value = not my_dict.get(key="key", other="something", default=False) - 170 |+value = not my_dict.get(key="key", other="something") -171 171 | value = not my_dict.get(default=False, other="something", key="test") +RUF056.py:170:47: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +168 | not my_dict.get(default=[], key="key") +169 | not my_dict.get(default=False) +170 | not my_dict.get(key="key", other="something", default=False) + | ^^^^^^^^^^^^^ RUF056 +171 | not my_dict.get(default=False, other="something", key="test") + | + = help: Remove falsy fallback from `dict.get()` + +RUF056.py:171:17: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +169 | not my_dict.get(default=False) +170 | not my_dict.get(key="key", other="something", default=False) +171 | not my_dict.get(default=False, other="something", key="test") + | ^^^^^^^^^^^^^ RUF056 +172 | +173 | # comments don't really matter here because of the kwargs but include them for + | + = help: Remove falsy fallback from `dict.get()` + +RUF056.py:177:5: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +175 | not my_dict.get( +176 | key="key", # comment1 +177 | default=False, # comment2 + | ^^^^^^^^^^^^^ RUF056 +178 | ) # comment 3 +179 | not my_dict.get( + | + = help: Remove falsy fallback from `dict.get()` + +RUF056.py:180:5: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +178 | ) # comment 3 +179 | not my_dict.get( +180 | default=[], # comment1 + | ^^^^^^^^^^ RUF056 +181 | key="key", # comment2 +182 | ) # comment 3 + | + = help: Remove falsy fallback from `dict.get()` + +RUF056.py:187:24: RUF056 Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. + | +185 | # TypeError is raised at runtime before and after the fix, but we still bail +186 | # out for having an unrecognized number of arguments +187 | not my_dict.get("key", False, foo=...) + | ^^^^^ RUF056 +188 | +189 | # https://github.com/astral-sh/ruff/issues/18798 + | + = help: Remove falsy fallback from `dict.get()` -RUF056.py:171:25: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. +RUF056.py:191:19: RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy. | -169 | # testing invalid dict.get calls -170 | value = not my_dict.get(key="key", other="something", default=False) -171 | value = not my_dict.get(default=False, other="something", key="test") - | ^^^^^^^^^^^^^ RUF056 +189 | # https://github.com/astral-sh/ruff/issues/18798 +190 | d = {} +191 | not d.get("key", (False)) + | ^^^^^ RUF056 | = help: Remove falsy fallback from `dict.get()` ℹ Safe fix -168 168 | -169 169 | # testing invalid dict.get calls -170 170 | value = not my_dict.get(key="key", other="something", default=False) -171 |-value = not my_dict.get(default=False, other="something", key="test") - 171 |+value = not my_dict.get(other="something", key="test") +188 188 | +189 189 | # https://github.com/astral-sh/ruff/issues/18798 +190 190 | d = {} +191 |-not d.get("key", (False)) + 191 |+not d.get("key") diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF057_RUF057.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF057_RUF057.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF057_RUF057.py.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF057_RUF057.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF058_RUF058_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF058_RUF058_0.py.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF058_RUF058_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_1.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF058_RUF058_1.py.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_1.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap index 03d9674ce00d8..0f7d1505c3c3e 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap @@ -197,4 +197,14 @@ RUF059_0.py:86:29: RUF059 [*] Unpacked variable `that` is never used 86 |+ open("") as ((this, _that)), 87 87 | ): 88 88 | print("hello") -89 89 | +89 89 | + +RUF059_0.py:101:5: RUF059 Unpacked variable `x` is never used + | + 99 | # see: https://github.com/astral-sh/ruff/issues/18507 +100 | def f(_x): +101 | x, = "1" + | ^ RUF059 +102 | print(_x) + | + = help: Prefix it with an underscore or any other dummy variable pattern diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap new file mode 100644 index 0000000000000..ac92e967a9792 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF060_RUF060.py.snap @@ -0,0 +1,230 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF060.py:2:1: RUF060 Unnecessary membership test on empty collection + | +1 | # Errors +2 | 1 in [] + | ^^^^^^^ RUF060 +3 | 1 not in [] +4 | 2 in list() + | + +RUF060.py:3:1: RUF060 Unnecessary membership test on empty collection + | +1 | # Errors +2 | 1 in [] +3 | 1 not in [] + | ^^^^^^^^^^^ RUF060 +4 | 2 in list() +5 | 2 not in list() + | + +RUF060.py:4:1: RUF060 Unnecessary membership test on empty collection + | +2 | 1 in [] +3 | 1 not in [] +4 | 2 in list() + | ^^^^^^^^^^^ RUF060 +5 | 2 not in list() +6 | _ in () + | + +RUF060.py:5:1: RUF060 Unnecessary membership test on empty collection + | +3 | 1 not in [] +4 | 2 in list() +5 | 2 not in list() + | ^^^^^^^^^^^^^^^ RUF060 +6 | _ in () +7 | _ not in () + | + +RUF060.py:6:1: RUF060 Unnecessary membership test on empty collection + | +4 | 2 in list() +5 | 2 not in list() +6 | _ in () + | ^^^^^^^ RUF060 +7 | _ not in () +8 | 'x' in tuple() + | + +RUF060.py:7:1: RUF060 Unnecessary membership test on empty collection + | +5 | 2 not in list() +6 | _ in () +7 | _ not in () + | ^^^^^^^^^^^ RUF060 +8 | 'x' in tuple() +9 | 'y' not in tuple() + | + +RUF060.py:8:1: RUF060 Unnecessary membership test on empty collection + | + 6 | _ in () + 7 | _ not in () + 8 | 'x' in tuple() + | ^^^^^^^^^^^^^^ RUF060 + 9 | 'y' not in tuple() +10 | 'a' in set() + | + +RUF060.py:9:1: RUF060 Unnecessary membership test on empty collection + | + 7 | _ not in () + 8 | 'x' in tuple() + 9 | 'y' not in tuple() + | ^^^^^^^^^^^^^^^^^^ RUF060 +10 | 'a' in set() +11 | 'a' not in set() + | + +RUF060.py:10:1: RUF060 Unnecessary membership test on empty collection + | + 8 | 'x' in tuple() + 9 | 'y' not in tuple() +10 | 'a' in set() + | ^^^^^^^^^^^^ RUF060 +11 | 'a' not in set() +12 | 'b' in {} + | + +RUF060.py:11:1: RUF060 Unnecessary membership test on empty collection + | + 9 | 'y' not in tuple() +10 | 'a' in set() +11 | 'a' not in set() + | ^^^^^^^^^^^^^^^^ RUF060 +12 | 'b' in {} +13 | 'b' not in {} + | + +RUF060.py:12:1: RUF060 Unnecessary membership test on empty collection + | +10 | 'a' in set() +11 | 'a' not in set() +12 | 'b' in {} + | ^^^^^^^^^ RUF060 +13 | 'b' not in {} +14 | 1 in dict() + | + +RUF060.py:13:1: RUF060 Unnecessary membership test on empty collection + | +11 | 'a' not in set() +12 | 'b' in {} +13 | 'b' not in {} + | ^^^^^^^^^^^^^ RUF060 +14 | 1 in dict() +15 | 2 not in dict() + | + +RUF060.py:14:1: RUF060 Unnecessary membership test on empty collection + | +12 | 'b' in {} +13 | 'b' not in {} +14 | 1 in dict() + | ^^^^^^^^^^^ RUF060 +15 | 2 not in dict() +16 | "a" in "" + | + +RUF060.py:15:1: RUF060 Unnecessary membership test on empty collection + | +13 | 'b' not in {} +14 | 1 in dict() +15 | 2 not in dict() + | ^^^^^^^^^^^^^^^ RUF060 +16 | "a" in "" +17 | b'c' in b"" + | + +RUF060.py:16:1: RUF060 Unnecessary membership test on empty collection + | +14 | 1 in dict() +15 | 2 not in dict() +16 | "a" in "" + | ^^^^^^^^^ RUF060 +17 | b'c' in b"" +18 | "b" in f"" + | + +RUF060.py:17:1: RUF060 Unnecessary membership test on empty collection + | +15 | 2 not in dict() +16 | "a" in "" +17 | b'c' in b"" + | ^^^^^^^^^^^ RUF060 +18 | "b" in f"" +19 | b"a" in bytearray() + | + +RUF060.py:18:1: RUF060 Unnecessary membership test on empty collection + | +16 | "a" in "" +17 | b'c' in b"" +18 | "b" in f"" + | ^^^^^^^^^^ RUF060 +19 | b"a" in bytearray() +20 | b"a" in bytes() + | + +RUF060.py:19:1: RUF060 Unnecessary membership test on empty collection + | +17 | b'c' in b"" +18 | "b" in f"" +19 | b"a" in bytearray() + | ^^^^^^^^^^^^^^^^^^^ RUF060 +20 | b"a" in bytes() +21 | 1 in frozenset() + | + +RUF060.py:20:1: RUF060 Unnecessary membership test on empty collection + | +18 | "b" in f"" +19 | b"a" in bytearray() +20 | b"a" in bytes() + | ^^^^^^^^^^^^^^^ RUF060 +21 | 1 in frozenset() +22 | 1 in set(set()) + | + +RUF060.py:21:1: RUF060 Unnecessary membership test on empty collection + | +19 | b"a" in bytearray() +20 | b"a" in bytes() +21 | 1 in frozenset() + | ^^^^^^^^^^^^^^^^ RUF060 +22 | 1 in set(set()) +23 | 2 in frozenset([]) + | + +RUF060.py:22:1: RUF060 Unnecessary membership test on empty collection + | +20 | b"a" in bytes() +21 | 1 in frozenset() +22 | 1 in set(set()) + | ^^^^^^^^^^^^^^^ RUF060 +23 | 2 in frozenset([]) +24 | '' in set("") + | + +RUF060.py:23:1: RUF060 Unnecessary membership test on empty collection + | +21 | 1 in frozenset() +22 | 1 in set(set()) +23 | 2 in frozenset([]) + | ^^^^^^^^^^^^^^^^^^ RUF060 +24 | '' in set("") + | + +RUF060.py:24:1: RUF060 Unnecessary membership test on empty collection + | +22 | 1 in set(set()) +23 | 2 in frozenset([]) +24 | '' in set("") + | ^^^^^^^^^^^^^ RUF060 +25 | +26 | # OK + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap new file mode 100644 index 0000000000000..57f0c51fe18bb --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061_deprecated_call.py:16:5: RUF061 [*] Use context-manager form of `pytest.deprecated_call()` + | +15 | def test_error_trivial(): +16 | pytest.deprecated_call(raise_deprecation_warning, "deprecated") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.deprecated_call()` as a context-manager + +ℹ Unsafe fix +13 13 | +14 14 | +15 15 | def test_error_trivial(): +16 |- pytest.deprecated_call(raise_deprecation_warning, "deprecated") + 16 |+ with pytest.deprecated_call(): + 17 |+ raise_deprecation_warning("deprecated") +17 18 | +18 19 | +19 20 | def test_error_assign(): + +RUF061_deprecated_call.py:20:9: RUF061 [*] Use context-manager form of `pytest.deprecated_call()` + | +19 | def test_error_assign(): +20 | s = pytest.deprecated_call(raise_deprecation_warning, "deprecated") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +21 | print(s) + | + = help: Use `pytest.deprecated_call()` as a context-manager + +ℹ Unsafe fix +17 17 | +18 18 | +19 19 | def test_error_assign(): +20 |- s = pytest.deprecated_call(raise_deprecation_warning, "deprecated") + 20 |+ with pytest.deprecated_call(): + 21 |+ s = raise_deprecation_warning("deprecated") +21 22 | print(s) +22 23 | +23 24 | + +RUF061_deprecated_call.py:25:5: RUF061 [*] Use context-manager form of `pytest.deprecated_call()` + | +24 | def test_error_lambda(): +25 | pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.deprecated_call()` as a context-manager + +ℹ Unsafe fix +22 22 | +23 23 | +24 24 | def test_error_lambda(): +25 |- pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) + 25 |+ with pytest.deprecated_call(): + 26 |+ (lambda: warnings.warn("", DeprecationWarning))() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap new file mode 100644 index 0000000000000..be2efdaee9261 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap @@ -0,0 +1,114 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061_raises.py:19:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +18 | def test_error_trivial(): +19 | pytest.raises(ZeroDivisionError, func, 1, b=0) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +16 16 | +17 17 | +18 18 | def test_error_trivial(): +19 |- pytest.raises(ZeroDivisionError, func, 1, b=0) + 19 |+ with pytest.raises(ZeroDivisionError): + 20 |+ func(1, b=0) +20 21 | +21 22 | +22 23 | def test_error_match(): + +RUF061_raises.py:23:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +22 | def test_error_match(): +23 | pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +20 20 | +21 21 | +22 22 | def test_error_match(): +23 |- pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") + 23 |+ with pytest.raises(ZeroDivisionError, match="division by zero"): + 24 |+ func(1, b=0) +24 25 | +25 26 | +26 27 | def test_error_assign(): + +RUF061_raises.py:27:15: RUF061 [*] Use context-manager form of `pytest.raises()` + | +26 | def test_error_assign(): +27 | excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +24 24 | +25 25 | +26 26 | def test_error_assign(): +27 |- excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) + 27 |+ with pytest.raises(ZeroDivisionError) as excinfo: + 28 |+ func(1, b=0) +28 29 | +29 30 | +30 31 | def test_error_kwargs(): + +RUF061_raises.py:31:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +30 | def test_error_kwargs(): +31 | pytest.raises(func=func, expected_exception=ZeroDivisionError) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +28 28 | +29 29 | +30 30 | def test_error_kwargs(): +31 |- pytest.raises(func=func, expected_exception=ZeroDivisionError) + 31 |+ with pytest.raises(ZeroDivisionError): + 32 |+ func() +32 33 | +33 34 | +34 35 | def test_error_multi_statement(): + +RUF061_raises.py:35:15: RUF061 [*] Use context-manager form of `pytest.raises()` + | +34 | def test_error_multi_statement(): +35 | excinfo = pytest.raises(ValueError, int, "hello") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +36 | assert excinfo.match("^invalid literal") + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +32 32 | +33 33 | +34 34 | def test_error_multi_statement(): +35 |- excinfo = pytest.raises(ValueError, int, "hello") + 35 |+ with pytest.raises(ValueError) as excinfo: + 36 |+ int("hello") +36 37 | assert excinfo.match("^invalid literal") +37 38 | +38 39 | + +RUF061_raises.py:40:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +39 | def test_error_lambda(): +40 | pytest.raises(ZeroDivisionError, lambda: 1 / 0) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +37 37 | +38 38 | +39 39 | def test_error_lambda(): +40 |- pytest.raises(ZeroDivisionError, lambda: 1 / 0) + 40 |+ with pytest.raises(ZeroDivisionError): + 41 |+ (lambda: 1 / 0)() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap new file mode 100644 index 0000000000000..e47eb6307e3e8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061_warns.py:16:5: RUF061 [*] Use context-manager form of `pytest.warns()` + | +15 | def test_error_trivial(): +16 | pytest.warns(UserWarning, raise_user_warning, "warning") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.warns()` as a context-manager + +ℹ Unsafe fix +13 13 | +14 14 | +15 15 | def test_error_trivial(): +16 |- pytest.warns(UserWarning, raise_user_warning, "warning") + 16 |+ with pytest.warns(UserWarning): + 17 |+ raise_user_warning("warning") +17 18 | +18 19 | +19 20 | def test_error_assign(): + +RUF061_warns.py:20:9: RUF061 [*] Use context-manager form of `pytest.warns()` + | +19 | def test_error_assign(): +20 | s = pytest.warns(UserWarning, raise_user_warning, "warning") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +21 | print(s) + | + = help: Use `pytest.warns()` as a context-manager + +ℹ Unsafe fix +17 17 | +18 18 | +19 19 | def test_error_assign(): +20 |- s = pytest.warns(UserWarning, raise_user_warning, "warning") + 20 |+ with pytest.warns(UserWarning): + 21 |+ s = raise_user_warning("warning") +21 22 | print(s) +22 23 | +23 24 | + +RUF061_warns.py:25:5: RUF061 [*] Use context-manager form of `pytest.warns()` + | +24 | def test_error_lambda(): +25 | pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.warns()` as a context-manager + +ℹ Unsafe fix +22 22 | +23 23 | +24 24 | def test_error_lambda(): +25 |- pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) + 25 |+ with pytest.warns(UserWarning): + 26 |+ (lambda: warnings.warn("", UserWarning))() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap new file mode 100644 index 0000000000000..e42b87b266f15 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap @@ -0,0 +1,347 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF064.py:6:17: RUF064 [*] Non-octal mode + | +4 | from pathlib import Path +5 | +6 | os.chmod("foo", 444) # Error + | ^^^ RUF064 +7 | os.chmod("foo", 0o444) # OK +8 | os.chmod("foo", 7777) # Error + | + = help: Replace with octal literal + +ℹ Unsafe fix +3 3 | import os +4 4 | from pathlib import Path +5 5 | +6 |-os.chmod("foo", 444) # Error + 6 |+os.chmod("foo", 0o444) # Error +7 7 | os.chmod("foo", 0o444) # OK +8 8 | os.chmod("foo", 7777) # Error +9 9 | os.chmod("foo", 10000) # Error + +RUF064.py:8:17: RUF064 Non-octal mode + | + 6 | os.chmod("foo", 444) # Error + 7 | os.chmod("foo", 0o444) # OK + 8 | os.chmod("foo", 7777) # Error + | ^^^^ RUF064 + 9 | os.chmod("foo", 10000) # Error +10 | os.chmod("foo", 99999) # Error + | + = help: Replace with octal literal + +RUF064.py:9:17: RUF064 Non-octal mode + | + 7 | os.chmod("foo", 0o444) # OK + 8 | os.chmod("foo", 7777) # Error + 9 | os.chmod("foo", 10000) # Error + | ^^^^^ RUF064 +10 | os.chmod("foo", 99999) # Error + | + = help: Replace with octal literal + +RUF064.py:10:17: RUF064 Non-octal mode + | + 8 | os.chmod("foo", 7777) # Error + 9 | os.chmod("foo", 10000) # Error +10 | os.chmod("foo", 99999) # Error + | ^^^^^ RUF064 +11 | +12 | os.umask(777) # Error + | + = help: Replace with octal literal + +RUF064.py:12:10: RUF064 [*] Non-octal mode + | +10 | os.chmod("foo", 99999) # Error +11 | +12 | os.umask(777) # Error + | ^^^ RUF064 +13 | os.umask(0o777) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +9 9 | os.chmod("foo", 10000) # Error +10 10 | os.chmod("foo", 99999) # Error +11 11 | +12 |-os.umask(777) # Error + 12 |+os.umask(0o777) # Error +13 13 | os.umask(0o777) # OK +14 14 | +15 15 | os.fchmod(0, 400) # Error + +RUF064.py:15:14: RUF064 [*] Non-octal mode + | +13 | os.umask(0o777) # OK +14 | +15 | os.fchmod(0, 400) # Error + | ^^^ RUF064 +16 | os.fchmod(0, 0o400) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +12 12 | os.umask(777) # Error +13 13 | os.umask(0o777) # OK +14 14 | +15 |-os.fchmod(0, 400) # Error + 15 |+os.fchmod(0, 0o400) # Error +16 16 | os.fchmod(0, 0o400) # OK +17 17 | +18 18 | os.lchmod("foo", 755) # Error + +RUF064.py:18:18: RUF064 [*] Non-octal mode + | +16 | os.fchmod(0, 0o400) # OK +17 | +18 | os.lchmod("foo", 755) # Error + | ^^^ RUF064 +19 | os.lchmod("foo", 0o755) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +15 15 | os.fchmod(0, 400) # Error +16 16 | os.fchmod(0, 0o400) # OK +17 17 | +18 |-os.lchmod("foo", 755) # Error + 18 |+os.lchmod("foo", 0o755) # Error +19 19 | os.lchmod("foo", 0o755) # OK +20 20 | +21 21 | os.mkdir("foo", 600) # Error + +RUF064.py:21:17: RUF064 [*] Non-octal mode + | +19 | os.lchmod("foo", 0o755) # OK +20 | +21 | os.mkdir("foo", 600) # Error + | ^^^ RUF064 +22 | os.mkdir("foo", 0o600) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +18 18 | os.lchmod("foo", 755) # Error +19 19 | os.lchmod("foo", 0o755) # OK +20 20 | +21 |-os.mkdir("foo", 600) # Error + 21 |+os.mkdir("foo", 0o600) # Error +22 22 | os.mkdir("foo", 0o600) # OK +23 23 | +24 24 | os.makedirs("foo", 644) # Error + +RUF064.py:24:20: RUF064 [*] Non-octal mode + | +22 | os.mkdir("foo", 0o600) # OK +23 | +24 | os.makedirs("foo", 644) # Error + | ^^^ RUF064 +25 | os.makedirs("foo", 0o644) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +21 21 | os.mkdir("foo", 600) # Error +22 22 | os.mkdir("foo", 0o600) # OK +23 23 | +24 |-os.makedirs("foo", 644) # Error + 24 |+os.makedirs("foo", 0o644) # Error +25 25 | os.makedirs("foo", 0o644) # OK +26 26 | +27 27 | os.mkfifo("foo", 640) # Error + +RUF064.py:27:18: RUF064 [*] Non-octal mode + | +25 | os.makedirs("foo", 0o644) # OK +26 | +27 | os.mkfifo("foo", 640) # Error + | ^^^ RUF064 +28 | os.mkfifo("foo", 0o640) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +24 24 | os.makedirs("foo", 644) # Error +25 25 | os.makedirs("foo", 0o644) # OK +26 26 | +27 |-os.mkfifo("foo", 640) # Error + 27 |+os.mkfifo("foo", 0o640) # Error +28 28 | os.mkfifo("foo", 0o640) # OK +29 29 | +30 30 | os.mknod("foo", 660) # Error + +RUF064.py:30:17: RUF064 [*] Non-octal mode + | +28 | os.mkfifo("foo", 0o640) # OK +29 | +30 | os.mknod("foo", 660) # Error + | ^^^ RUF064 +31 | os.mknod("foo", 0o660) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +27 27 | os.mkfifo("foo", 640) # Error +28 28 | os.mkfifo("foo", 0o640) # OK +29 29 | +30 |-os.mknod("foo", 660) # Error + 30 |+os.mknod("foo", 0o660) # Error +31 31 | os.mknod("foo", 0o660) # OK +32 32 | +33 33 | os.open("foo", os.O_CREAT, 644) # Error + +RUF064.py:33:28: RUF064 [*] Non-octal mode + | +31 | os.mknod("foo", 0o660) # OK +32 | +33 | os.open("foo", os.O_CREAT, 644) # Error + | ^^^ RUF064 +34 | os.open("foo", os.O_CREAT, 0o644) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +30 30 | os.mknod("foo", 660) # Error +31 31 | os.mknod("foo", 0o660) # OK +32 32 | +33 |-os.open("foo", os.O_CREAT, 644) # Error + 33 |+os.open("foo", os.O_CREAT, 0o644) # Error +34 34 | os.open("foo", os.O_CREAT, 0o644) # OK +35 35 | +36 36 | Path("bar").chmod(755) # Error + +RUF064.py:36:19: RUF064 [*] Non-octal mode + | +34 | os.open("foo", os.O_CREAT, 0o644) # OK +35 | +36 | Path("bar").chmod(755) # Error + | ^^^ RUF064 +37 | Path("bar").chmod(0o755) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +33 33 | os.open("foo", os.O_CREAT, 644) # Error +34 34 | os.open("foo", os.O_CREAT, 0o644) # OK +35 35 | +36 |-Path("bar").chmod(755) # Error + 36 |+Path("bar").chmod(0o755) # Error +37 37 | Path("bar").chmod(0o755) # OK +38 38 | +39 39 | path = Path("bar") + +RUF064.py:40:12: RUF064 [*] Non-octal mode + | +39 | path = Path("bar") +40 | path.chmod(755) # Error + | ^^^ RUF064 +41 | path.chmod(0o755) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +37 37 | Path("bar").chmod(0o755) # OK +38 38 | +39 39 | path = Path("bar") +40 |-path.chmod(755) # Error + 40 |+path.chmod(0o755) # Error +41 41 | path.chmod(0o755) # OK +42 42 | +43 43 | dbm.open("db", "r", 600) # Error + +RUF064.py:43:21: RUF064 [*] Non-octal mode + | +41 | path.chmod(0o755) # OK +42 | +43 | dbm.open("db", "r", 600) # Error + | ^^^ RUF064 +44 | dbm.open("db", "r", 0o600) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +40 40 | path.chmod(755) # Error +41 41 | path.chmod(0o755) # OK +42 42 | +43 |-dbm.open("db", "r", 600) # Error + 43 |+dbm.open("db", "r", 0o600) # Error +44 44 | dbm.open("db", "r", 0o600) # OK +45 45 | +46 46 | dbm.gnu.open("db", "r", 600) # Error + +RUF064.py:46:25: RUF064 [*] Non-octal mode + | +44 | dbm.open("db", "r", 0o600) # OK +45 | +46 | dbm.gnu.open("db", "r", 600) # Error + | ^^^ RUF064 +47 | dbm.gnu.open("db", "r", 0o600) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +43 43 | dbm.open("db", "r", 600) # Error +44 44 | dbm.open("db", "r", 0o600) # OK +45 45 | +46 |-dbm.gnu.open("db", "r", 600) # Error + 46 |+dbm.gnu.open("db", "r", 0o600) # Error +47 47 | dbm.gnu.open("db", "r", 0o600) # OK +48 48 | +49 49 | dbm.ndbm.open("db", "r", 600) # Error + +RUF064.py:49:26: RUF064 [*] Non-octal mode + | +47 | dbm.gnu.open("db", "r", 0o600) # OK +48 | +49 | dbm.ndbm.open("db", "r", 600) # Error + | ^^^ RUF064 +50 | dbm.ndbm.open("db", "r", 0o600) # OK + | + = help: Replace with octal literal + +ℹ Unsafe fix +46 46 | dbm.gnu.open("db", "r", 600) # Error +47 47 | dbm.gnu.open("db", "r", 0o600) # OK +48 48 | +49 |-dbm.ndbm.open("db", "r", 600) # Error + 49 |+dbm.ndbm.open("db", "r", 0o600) # Error +50 50 | dbm.ndbm.open("db", "r", 0o600) # OK +51 51 | +52 52 | os.fchmod(0, 256) # 0o400 + +RUF064.py:52:14: RUF064 [*] Non-octal mode + | +50 | dbm.ndbm.open("db", "r", 0o600) # OK +51 | +52 | os.fchmod(0, 256) # 0o400 + | ^^^ RUF064 +53 | os.fchmod(0, 493) # 0o755 + | + = help: Replace with octal literal + +ℹ Unsafe fix +49 49 | dbm.ndbm.open("db", "r", 600) # Error +50 50 | dbm.ndbm.open("db", "r", 0o600) # OK +51 51 | +52 |-os.fchmod(0, 256) # 0o400 + 52 |+os.fchmod(0, 0o400) # 0o400 +53 53 | os.fchmod(0, 493) # 0o755 + +RUF064.py:53:14: RUF064 [*] Non-octal mode + | +52 | os.fchmod(0, 256) # 0o400 +53 | os.fchmod(0, 493) # 0o755 + | ^^^ RUF064 + | + = help: Replace with octal literal + +ℹ Unsafe fix +50 50 | dbm.ndbm.open("db", "r", 0o600) # OK +51 51 | +52 52 | os.fchmod(0, 256) # 0o400 +53 |-os.fchmod(0, 493) # 0o755 + 53 |+os.fchmod(0, 0o755) # 0o755 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap new file mode 100644 index 0000000000000..06274f1af201b --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py310.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF063.py:4:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access + | +2 | # Cases that should trigger the violation +3 | +4 | foo.__dict__.get("__annotations__") # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | + +RUF063.py:5:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:6:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:7:1: RUF063 Use `inspect.get_annotations` instead of `__dict__` access + | +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +8 | +9 | # Cases that should NOT trigger the violation + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap new file mode 100644 index 0000000000000..0f07c9a8e954d --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py314.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF063.py:4:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access + | +2 | # Cases that should trigger the violation +3 | +4 | foo.__dict__.get("__annotations__") # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | + +RUF063.py:5:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:6:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:7:1: RUF063 Use `annotationlib.get_annotations` instead of `__dict__` access + | +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +8 | +9 | # Cases that should NOT trigger the violation + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_no_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_no_typing_extensions.snap new file mode 100644 index 0000000000000..7f58cfd7246a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_no_typing_extensions.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap new file mode 100644 index 0000000000000..c5ef0419bad28 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__access_annotations_from_class_dict_py39_with_typing_extensions.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF063.py:4:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access + | +2 | # Cases that should trigger the violation +3 | +4 | foo.__dict__.get("__annotations__") # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | + +RUF063.py:5:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:6:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access + | +4 | foo.__dict__.get("__annotations__") # RUF063 +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | + +RUF063.py:7:1: RUF063 Use `typing_extensions.get_annotations` instead of `__dict__` access + | +5 | foo.__dict__.get("__annotations__", None) # RUF063 +6 | foo.__dict__.get("__annotations__", {}) # RUF063 +7 | foo.__dict__["__annotations__"] # RUF063 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF063 +8 | +9 | # Cases that should NOT trigger the violation + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap index d4ea19a5df52a..b698180305396 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap @@ -1,8 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs -snapshot_kind: text --- -RUF100_2.py:1:19: RUF100 [*] Unused `noqa` directive (unused: `F401`) +RUF100_2.py:1:19: RUF100 [*] Unused `noqa` directive (non-enabled: `F401`) | 1 | import itertools # noqa: F401 | ^^^^^^^^^^^^ RUF100 diff --git a/crates/ruff_linter/src/rules/tryceratops/helpers.rs b/crates/ruff_linter/src/rules/tryceratops/helpers.rs index da30bfe23c25d..ed18c29fdc2f7 100644 --- a/crates/ruff_linter/src/rules/tryceratops/helpers.rs +++ b/crates/ruff_linter/src/rules/tryceratops/helpers.rs @@ -1,8 +1,8 @@ use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, ExceptHandler, Expr}; -use ruff_python_semantic::analyze::logging; use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; /// Collect `logging`-like calls from an AST. diff --git a/crates/ruff_linter/src/rules/tryceratops/mod.rs b/crates/ruff_linter/src/rules/tryceratops/mod.rs index 21f3a01382ddc..9cc6838cdd455 100644 --- a/crates/ruff_linter/src/rules/tryceratops/mod.rs +++ b/crates/ruff_linter/src/rules/tryceratops/mod.rs @@ -4,7 +4,6 @@ pub(crate) mod rules; #[cfg(test)] mod tests { - use std::convert::AsRef; use std::path::Path; use anyhow::Result; @@ -13,7 +12,7 @@ mod tests { use crate::registry::Rule; use crate::test::test_path; - use crate::{assert_messages, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::RaiseVanillaClass, Path::new("TRY002.py"))] #[test_case(Rule::RaiseVanillaArgs, Path::new("TRY003.py"))] @@ -25,12 +24,12 @@ mod tests { #[test_case(Rule::ErrorInsteadOfException, Path::new("TRY400.py"))] #[test_case(Rule::VerboseLogMessage, Path::new("TRY401.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("tryceratops").join(path).as_path(), &settings::LinterSettings::for_rule(rule_code), )?; - assert_messages!(snapshot, diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs index 4093e4ea3d7eb..ebc2d40a75e87 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -1,5 +1,4 @@ -use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, ExceptHandler, Expr}; use ruff_python_semantic::analyze::logging::exc_info; @@ -9,6 +8,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::rules::tryceratops::helpers::LoggerCandidateVisitor; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `logging.error` instead of `logging.exception` when @@ -73,14 +73,15 @@ pub(crate) fn error_instead_of_exception(checker: &Checker, handlers: &[ExceptHa let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; let calls = { let mut visitor = - LoggerCandidateVisitor::new(checker.semantic(), &checker.settings.logger_objects); + LoggerCandidateVisitor::new(checker.semantic(), &checker.settings().logger_objects); visitor.visit_body(body); visitor.calls }; for (expr, logging_level) in calls { if matches!(logging_level, LoggingLevel::Error) { if exc_info(&expr.arguments, checker.semantic()).is_none() { - let mut diagnostic = Diagnostic::new(ErrorInsteadOfException, expr.range()); + let mut diagnostic = + checker.report_diagnostic(ErrorInsteadOfException, expr.range()); match expr.func.as_ref() { Expr::Attribute(ast::ExprAttribute { attr, .. }) => { @@ -134,8 +135,6 @@ pub(crate) fn error_instead_of_exception(checker: &Checker, handlers: &[ExceptHa } _ => {} } - - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs index 8351e52350105..c359367ded101 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs @@ -1,8 +1,8 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Arguments, Expr}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -78,7 +78,7 @@ pub(crate) fn raise_vanilla_args(checker: &Checker, expr: &Expr) { } if contains_message(arg) { - checker.report_diagnostic(Diagnostic::new(RaiseVanillaArgs, expr.range())); + checker.report_diagnostic(RaiseVanillaArgs, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs index f06029be2add7..6549f3caad079 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -1,10 +1,10 @@ -use ruff_python_ast::helpers::map_callable; use ruff_python_ast::Expr; +use ruff_python_ast::helpers::map_callable; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -74,6 +74,6 @@ pub(crate) fn raise_vanilla_class(checker: &Checker, expr: &Expr) { ) }) { - checker.report_diagnostic(Diagnostic::new(RaiseVanillaClass, expr.range())); + checker.report_diagnostic(RaiseVanillaClass, expr.range()); } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs index 59d28065e5ff0..ce1489637781f 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs @@ -1,14 +1,14 @@ use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{ comparable::ComparableExpr, helpers::{self, map_callable}, - statement_visitor::{walk_stmt, StatementVisitor}, + statement_visitor::{StatementVisitor, walk_stmt}, }; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -115,7 +115,7 @@ pub(crate) fn raise_within_try(checker: &Checker, body: &[Stmt], handlers: &[Exc .is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException")) }) { - checker.report_diagnostic(Diagnostic::new(RaiseWithinTry, stmt.range())); + checker.report_diagnostic(RaiseWithinTry, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs b/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs index f208306d2480f..a50d64136e3f3 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/reraise_no_cause.rs @@ -1,5 +1,6 @@ -use ruff_diagnostics::Violation; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; + +use crate::Violation; /// ## Removed /// This rule is identical to [B904] which should be used instead. diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs index a8aeb115df550..feb5b7eb6c52e 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/try_consider_else.rs @@ -1,10 +1,10 @@ use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::contains_effect; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -67,13 +67,18 @@ pub(crate) fn try_consider_else( ) { if body.len() > 1 && orelse.is_empty() && !handler.is_empty() { if let Some(stmt) = body.last() { - if let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt { + if let Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) = stmt + { if let Some(value) = value { if contains_effect(value, |id| checker.semantic().has_builtin_binding(id)) { return; } } - checker.report_diagnostic(Diagnostic::new(TryConsiderElse, stmt.range())); + checker.report_diagnostic(TryConsiderElse, stmt.range()); } } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs index a4c238bf57f51..59eedace937a5 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -1,11 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::map_callable; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast, Expr, Stmt, StmtIf}; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -94,7 +94,7 @@ fn check_type_check_test(semantic: &SemanticModel, test: &Expr) -> bool { fn check_raise(checker: &Checker, exc: &Expr, item: &Stmt) { if is_builtin_exception(exc, checker.semantic()) { - checker.report_diagnostic(Diagnostic::new(TypeCheckWithoutTypeError, item.range())); + checker.report_diagnostic(TypeCheckWithoutTypeError, item.range()); } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs b/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs index 3dcb96d1c87f9..f52a3e95c21cc 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/useless_try_except.rs @@ -1,9 +1,9 @@ use ruff_python_ast::{self as ast, ExceptHandler, ExceptHandlerExceptHandler, Expr, Stmt}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does @@ -40,33 +40,30 @@ impl Violation for UselessTryExcept { /// TRY203 (previously TRY302) pub(crate) fn useless_try_except(checker: &Checker, handlers: &[ExceptHandler]) { - if let Some(diagnostics) = handlers - .iter() - .map(|handler| { - let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = - handler; - let Some(Stmt::Raise(ast::StmtRaise { - exc, cause: None, .. - })) = &body.first() - else { - return None; - }; - if let Some(expr) = exc { - // E.g., `except ... as e: raise e` - if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() { - if name.as_ref().is_some_and(|name| name.as_str() == id) { - return Some(Diagnostic::new(UselessTryExcept, handler.range())); - } + if handlers.iter().all(|handler| { + let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = handler; + let Some(Stmt::Raise(ast::StmtRaise { + exc, cause: None, .. + })) = &body.first() + else { + return false; + }; + if let Some(expr) = exc { + // E.g., `except ... as e: raise e` + if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() { + if name.as_ref().is_some_and(|name| name.as_str() == id) { + return true; } - None - } else { - // E.g., `except ...: raise` - Some(Diagnostic::new(UselessTryExcept, handler.range())) } - }) - .collect::>>() - { + false + } else { + // E.g., `except ...: raise` + true + } + }) { // Require that all handlers are useless, but create one diagnostic per handler. - checker.report_diagnostics(diagnostics); + for handler in handlers { + checker.report_diagnostic(UselessTryExcept, handler.range()); + } } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs index 8d46cbb4493b6..9b8c6619c825f 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_log_message.rs @@ -1,12 +1,12 @@ use ruff_python_ast::{self as ast, ExceptHandler, Expr}; -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_stdlib::logging::LoggingLevel; use ruff_text_size::Ranged; +use crate::Violation; use crate::checkers::ast::Checker; use crate::rules::tryceratops::helpers::LoggerCandidateVisitor; @@ -51,7 +51,7 @@ pub(crate) fn verbose_log_message(checker: &Checker, handlers: &[ExceptHandler]) // Find all calls to `logging.exception`. let calls = { let mut visitor = - LoggerCandidateVisitor::new(checker.semantic(), &checker.settings.logger_objects); + LoggerCandidateVisitor::new(checker.semantic(), &checker.settings().logger_objects); visitor.visit_body(body); visitor.calls }; @@ -78,7 +78,7 @@ pub(crate) fn verbose_log_message(checker: &Checker, handlers: &[ExceptHandler]) }; let binding = checker.semantic().binding(id); if binding.kind.is_bound_exception() { - checker.report_diagnostic(Diagnostic::new(VerboseLogMessage, expr.range())); + checker.report_diagnostic(VerboseLogMessage, expr.range()); } } } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs index 4f41f7a5d8c5e..c330632c9776d 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/verbose_raise.rs @@ -1,11 +1,11 @@ use ruff_python_ast::{self as ast, ExceptHandler, Expr, Stmt}; -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::{AlwaysFixableViolation, Edit, Fix}; /// ## What it does /// Checks for needless exception names in `raise` statements. @@ -72,12 +72,12 @@ pub(crate) fn verbose_raise(checker: &Checker, handlers: &[ExceptHandler]) { // ...and the raised object is bound to the same name... if let Expr::Name(ast::ExprName { id, .. }) = exc.as_ref() { if id == exception_name.as_str() { - let mut diagnostic = Diagnostic::new(VerboseRaise, exc.range()); + let mut diagnostic = + checker.report_diagnostic(VerboseRaise, exc.range()); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( "raise".to_string(), raise.range(), ))); - checker.report_diagnostic(diagnostic); } } } diff --git a/crates/ruff_linter/src/settings/fix_safety_table.rs b/crates/ruff_linter/src/settings/fix_safety_table.rs index 8f4382452f3c0..bff38bbe5241f 100644 --- a/crates/ruff_linter/src/settings/fix_safety_table.rs +++ b/crates/ruff_linter/src/settings/fix_safety_table.rs @@ -1,15 +1,14 @@ use std::fmt::{Debug, Display, Formatter}; -use ruff_diagnostics::Applicability; use ruff_macros::CacheKey; use rustc_hash::FxHashMap; use strum::IntoEnumIterator; +use crate::Applicability; use crate::{ - display_settings, + RuleSelector, display_settings, registry::{Rule, RuleSet}, rule_selector::{PreviewOptions, Specificity}, - RuleSelector, }; /// A table to keep track of which rules fixes should have diff --git a/crates/ruff_linter/src/settings/flags.rs b/crates/ruff_linter/src/settings/flags.rs index 5815e19b4939f..dd558ed52b0aa 100644 --- a/crates/ruff_linter/src/settings/flags.rs +++ b/crates/ruff_linter/src/settings/flags.rs @@ -19,11 +19,7 @@ impl Noqa { impl From for Noqa { fn from(value: bool) -> Self { - if value { - Noqa::Enabled - } else { - Noqa::Disabled - } + if value { Noqa::Enabled } else { Noqa::Disabled } } } diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index e4aff6e74908f..bfec035a594cd 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -23,7 +23,7 @@ use crate::rules::{ pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use crate::settings::types::{CompiledPerFileIgnoreList, ExtensionMapping, FilePatternSet}; -use crate::{codes, fs, RuleSelector}; +use crate::{RuleSelector, codes, fs}; use super::line_width::IndentWidth; @@ -225,7 +225,7 @@ pub struct LinterSettings { /// /// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to obtain the Python /// version for a given file, while respecting the overrides in `per_file_target_version`. - pub unresolved_target_version: PythonVersion, + pub unresolved_target_version: TargetVersion, /// Path-specific overrides to `unresolved_target_version`. /// /// If you have a `Checker` available, see its `target_version` method instead. @@ -250,6 +250,7 @@ pub struct LinterSettings { pub line_length: LineLength, pub task_tags: Vec, pub typing_modules: Vec, + pub typing_extensions: bool, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, @@ -313,6 +314,7 @@ impl Display for LinterSettings { self.line_length, self.task_tags | array, self.typing_modules | array, + self.typing_extensions, ] } writeln!(f, "\n# Linter Plugins")?; @@ -376,7 +378,7 @@ impl LinterSettings { pub fn for_rule(rule_code: Rule) -> Self { Self { rules: RuleTable::from_iter([rule_code]), - unresolved_target_version: PythonVersion::latest(), + unresolved_target_version: PythonVersion::latest().into(), ..Self::default() } } @@ -384,7 +386,7 @@ impl LinterSettings { pub fn for_rules(rules: impl IntoIterator) -> Self { Self { rules: RuleTable::from_iter(rules), - unresolved_target_version: PythonVersion::latest(), + unresolved_target_version: PythonVersion::latest().into(), ..Self::default() } } @@ -392,7 +394,7 @@ impl LinterSettings { pub fn new(project_root: &Path) -> Self { Self { exclude: FilePatternSet::default(), - unresolved_target_version: PythonVersion::default(), + unresolved_target_version: TargetVersion(None), per_file_target_version: CompiledPerFileTargetVersionList::default(), project_root: project_root.to_path_buf(), rules: DEFAULT_SELECTORS @@ -450,24 +452,25 @@ impl LinterSettings { preview: PreviewMode::default(), explicit_preview_rules: false, extension: ExtensionMapping::default(), + typing_extensions: true, } } #[must_use] pub fn with_target_version(mut self, target_version: PythonVersion) -> Self { - self.unresolved_target_version = target_version; + self.unresolved_target_version = target_version.into(); self } - /// Resolve the [`PythonVersion`] to use for linting. + /// Resolve the [`TargetVersion`] to use for linting. /// /// This method respects the per-file version overrides in /// [`LinterSettings::per_file_target_version`] and falls back on /// [`LinterSettings::unresolved_target_version`] if none of the override patterns match. - pub fn resolve_target_version(&self, path: &Path) -> PythonVersion { + pub fn resolve_target_version(&self, path: &Path) -> TargetVersion { self.per_file_target_version .is_match(path) - .unwrap_or(self.unresolved_target_version) + .map_or(self.unresolved_target_version, TargetVersion::from) } } @@ -476,3 +479,48 @@ impl Default for LinterSettings { Self::new(fs::get_cwd()) } } + +/// A thin wrapper around `Option` to clarify the reason for different `unwrap` +/// calls in various places. +/// +/// For example, we want to default to `PythonVersion::latest()` for parsing and detecting semantic +/// syntax errors because this will minimize version-related diagnostics when the Python version is +/// unset. In contrast, we want to default to `PythonVersion::default()` for lint rules. These +/// correspond to the [`TargetVersion::parser_version`] and [`TargetVersion::linter_version`] +/// methods, respectively. +#[derive(Debug, Clone, Copy, CacheKey)] +pub struct TargetVersion(pub Option); + +impl TargetVersion { + /// Return the [`PythonVersion`] to use for parsing. + /// + /// This will be either the Python version specified by the user or the latest supported + /// version if unset. + pub fn parser_version(&self) -> PythonVersion { + self.0.unwrap_or_else(PythonVersion::latest) + } + + /// Return the [`PythonVersion`] to use for version-dependent lint rules. + /// + /// This will either be the Python version specified by the user or the default Python version + /// if unset. + pub fn linter_version(&self) -> PythonVersion { + self.0.unwrap_or_default() + } +} + +impl From for TargetVersion { + fn from(value: PythonVersion) -> Self { + Self(Some(value)) + } +} + +impl Display for TargetVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // manual inlining of display_settings! + match self.0 { + Some(value) => write!(f, "{value}"), + None => f.write_str("none"), + } + } +} diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 7087d9856d512..a72e80284ad40 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -5,19 +5,19 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::string::ToString; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use log::debug; use pep440_rs::{VersionSpecifier, VersionSpecifiers}; use rustc_hash::FxHashMap; -use serde::{de, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, de}; use strum_macros::EnumIter; use ruff_cache::{CacheKey, CacheKeyHasher}; -use ruff_diagnostics::Applicability; use ruff_macros::CacheKey; use ruff_python_ast::{self as ast, PySourceType}; +use crate::Applicability; use crate::registry::RuleSet; use crate::rule_selector::RuleSelector; use crate::{display_settings, fs}; @@ -34,6 +34,7 @@ pub enum PythonVersion { Py311, Py312, Py313, + Py314, } impl Default for PythonVersion { @@ -55,6 +56,7 @@ impl TryFrom for PythonVersion { ast::PythonVersion::PY311 => Ok(Self::Py311), ast::PythonVersion::PY312 => Ok(Self::Py312), ast::PythonVersion::PY313 => Ok(Self::Py313), + ast::PythonVersion::PY314 => Ok(Self::Py314), _ => Err(format!("unrecognized python version {value}")), } } @@ -84,6 +86,7 @@ impl PythonVersion { Self::Py311 => (3, 11), Self::Py312 => (3, 12), Self::Py313 => (3, 13), + Self::Py314 => (3, 14), } } } @@ -246,12 +249,12 @@ pub struct FilePatternSet { cache_key: u64, // This field is only for displaying the internals // of `set`. - #[allow(clippy::used_underscore_binding)] + #[expect(clippy::used_underscore_binding)] _set_internals: Vec, } impl FilePatternSet { - #[allow(clippy::used_underscore_binding)] + #[expect(clippy::used_underscore_binding)] pub fn try_from_iter(patterns: I) -> Result where I: IntoIterator, @@ -575,8 +578,8 @@ impl schemars::JsonSchema for RequiredVersion { "RequiredVersion".to_string() } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - gen.subschema_for::() + fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + generator.subschema_for::() } } diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap new file mode 100644 index 0000000000000..9e570b889cc0a --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/async_comprehension.py:5:8: PLE1142 `await` should be used within an async function + | +4 | def regular_function(): +5 | [x async for x in elements(1)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1142 +6 | +7 | async with elements(1) as x: + | + +resources/test/fixtures/syntax_errors/async_comprehension.py:7:5: PLE1142 `await` should be used within an async function + | + 5 | [x async for x in elements(1)] + 6 | + 7 | / async with elements(1) as x: + 8 | | pass + | |____________^ PLE1142 + 9 | +10 | async for _ in elements(1): + | + +resources/test/fixtures/syntax_errors/async_comprehension.py:10:5: PLE1142 `await` should be used within an async function + | + 8 | pass + 9 | +10 | / async for _ in elements(1): +11 | | pass + | |____________^ PLE1142 + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap deleted file mode 100644 index 86380f014a57f..0000000000000 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/ruff_linter/src/linter.rs ---- -:1:27: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) - | -1 | async def f(): return [[x async for x in foo(n)] for n in range(3)] - | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap index d55d10cfc6594..c573573f7063c 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/linter.rs --- -resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) +resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) | 1 | async def elements(n): yield n 2 | [x async for x in elements(5)] # okay, async at top level diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap new file mode 100644 index 0000000000000..fb45718a9ab9e --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/await_outside_async_function.py:2:5: PLE1142 `await` should be used within an async function + | +1 | def func(): +2 | await 1 + | ^^^^^^^ PLE1142 +3 | +4 | # Top-level await + | + +resources/test/fixtures/syntax_errors/await_outside_async_function.py:5:1: PLE1142 `await` should be used within an async function + | +4 | # Top-level await +5 | await 1 + | ^^^^^^^ PLE1142 + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_deferred_function_body_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_deferred_function_body_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap new file mode 100644 index 0000000000000..b2537598f04a1 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 2 | from typing import TypeVar + 3 |+from typing_extensions import Self +3 4 | +4 5 | T = TypeVar("T", bound="_NiceReprEnum") +5 6 | +6 7 | class C: +7 |- def __new__(cls: type[T]) -> T: + 8 |+ def __new__(cls) -> Self: +8 9 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap new file mode 100644 index 0000000000000..0dee5cdff1f70 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 |-from typing import TypeVar + 2 |+from typing import TypeVar, Self +3 3 | +4 4 | T = TypeVar("T", bound="_NiceReprEnum") +5 5 | +6 6 | class C: +7 |- def __new__(cls: type[T]) -> T: + 7 |+ def __new__(cls) -> Self: +8 8 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap new file mode 100644 index 0000000000000..0dee5cdff1f70 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 |-from typing import TypeVar + 2 |+from typing import TypeVar, Self +3 3 | +4 4 | T = TypeVar("T", bound="_NiceReprEnum") +5 5 | +6 6 | class C: +7 |- def __new__(cls: type[T]) -> T: + 7 |+ def __new__(cls) -> Self: +8 8 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_310_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_311_3.11.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_311_3.11.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap new file mode 100644 index 0000000000000..4ba33c756c494 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap new file mode 100644 index 0000000000000..6140d4cf408ca --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/late_future_import.py:2:1: F404 `from __future__` imports must occur at the beginning of the file + | +1 | import random +2 | from __future__ import annotations # Error; not at top of file + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F404 + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap new file mode 100644 index 0000000000000..a917491f4125a --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/load_before_global_declaration.py:9:5: PLE0118 Name `x` is used prior to global declaration on line 10 + | + 7 | x = 10 + 8 | def test_2(): + 9 | x += 1 # error + | ^ PLE0118 +10 | global x + | + +resources/test/fixtures/syntax_errors/load_before_global_declaration.py:13:11: PLE0118 Name `x` is used prior to global declaration on line 14 + | +12 | def test_3(): +13 | print(x) # error + | ^ PLE0118 +14 | global x +15 | x = 5 + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap new file mode 100644 index 0000000000000..9fd781f021060 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:1:27: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) + | +1 | async def f(): return [[x async for x in foo(n)] for n in range(3)] + | ^^^^^^^^^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap new file mode 100644 index 0000000000000..4ba33c756c494 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap new file mode 100644 index 0000000000000..4ba33c756c494 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap new file mode 100644 index 0000000000000..4ba33c756c494 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap new file mode 100644 index 0000000000000..4ba33c756c494 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap new file mode 100644 index 0000000000000..bac3111de0219 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:21: SyntaxError: attribute name `x` repeated in class pattern + | +2 | match x: +3 | case Point(x=1, x=2): + | ^ +4 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap new file mode 100644 index 0000000000000..f877e86e31013 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:21: SyntaxError: mapping pattern checks duplicate key `'key'` + | +2 | match x: +3 | case {'key': 1, 'key': 2}: + | ^^^^^ +4 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap new file mode 100644 index 0000000000000..5ede497ec6771 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:1:12: SyntaxError: duplicate type parameter + | +1 | class C[T, T]: pass + | ^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap new file mode 100644 index 0000000000000..a09dda153fa57 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_walrus_in_return_annotation_3.12.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:22: SyntaxError: named expression cannot be used within a generic definition + | +2 | def f[T](x: int) -> (y := 3): return x + | ^^^^^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap new file mode 100644 index 0000000000000..6f71b926a1c69 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_from_in_base_class_3.12.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:13: SyntaxError: yield expression cannot be used within a generic definition + | +2 | class C[T]((yield from [object])): + | ^^^^^^^^^^^^^^^^^^^ +3 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap new file mode 100644 index 0000000000000..39c220e6a1534 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_alias_3.12.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:11: SyntaxError: yield expression cannot be used within a type alias + | +2 | type Y = (yield 1) + | ^^^^^^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap new file mode 100644 index 0000000000000..9f0c12aaacc42 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidExpression_invalid_expression_yield_in_type_param_3.12.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:12: SyntaxError: yield expression cannot be used within a TypeVar bound + | +2 | type X[T: (yield 1)] = int + | ^^^^^^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap new file mode 100644 index 0000000000000..81bc057071a74 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:12: SyntaxError: Starred expression cannot be used here + | +2 | def func(): +3 | return *x + | ^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap new file mode 100644 index 0000000000000..811caf59bb708 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:5: SyntaxError: Starred expression cannot be used here + | +2 | for *x in range(10): + | ^^ +3 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap new file mode 100644 index 0000000000000..ee1549b0cddd9 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:11: SyntaxError: Starred expression cannot be used here + | +2 | def func(): +3 | yield *x + | ^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap new file mode 100644 index 0000000000000..502f151558ad4 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_capture_3.10.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:10: SyntaxError: name capture `irrefutable` makes remaining patterns unreachable + | +2 | match value: +3 | case irrefutable: + | ^^^^^^^^^^^ +4 | pass +5 | case 1: + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap new file mode 100644 index 0000000000000..cc54b4776b6bb --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_IrrefutableCasePattern_irrefutable_case_pattern_wildcard_3.10.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:10: SyntaxError: wildcard makes remaining patterns unreachable + | +2 | match value: +3 | case _: + | ^ +4 | pass +5 | case 1: + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap new file mode 100644 index 0000000000000..6cee83489af49 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:14: SyntaxError: multiple assignments to name `a` in pattern + | +2 | match x: +3 | case [a, a]: + | ^ +4 | pass +5 | case _: + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap new file mode 100644 index 0000000000000..80fb65620a4c3 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:1:2: SyntaxError: assignment expression cannot rebind comprehension variable + | +1 | [x:= 2 for x in range(2)] + | ^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap new file mode 100644 index 0000000000000..ecc8f955d8fff --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_SingleStarredAssignment_single_starred_assignment_3.10.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:1:1: SyntaxError: starred assignment target must be in a list or tuple + | +1 | *a = [1, 2, 3, 4] + | ^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap new file mode 100644 index 0000000000000..baebbdf41f123 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_3.10.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:1: SyntaxError: cannot assign to `__debug__` + | +2 | __debug__ = False + | ^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap new file mode 100644 index 0000000000000..44139dc6cf0cb --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_class_type_param_3.12.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:15: SyntaxError: cannot assign to `__debug__` + | +2 | class Generic[__debug__]: + | ^^^^^^^^^ +3 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap new file mode 100644 index 0000000000000..935ad560543ce --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_WriteToDebug_write_to_debug_in_function_param_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:13: SyntaxError: cannot assign to `__debug__` + | +2 | def process(__debug__): + | ^^^^^^^^^ +3 | pass + | diff --git a/crates/ruff_linter/src/source_kind.rs b/crates/ruff_linter/src/source_kind.rs index f067fe49dbb4c..a146d2db13710 100644 --- a/crates/ruff_linter/src/source_kind.rs +++ b/crates/ruff_linter/src/source_kind.rs @@ -16,15 +16,47 @@ use colored::Colorize; use crate::fs; use crate::text_helpers::ShowNonprinting; -#[derive(Clone, Debug, PartialEq, is_macro::Is)] +#[derive(Clone, Debug, PartialEq)] pub enum SourceKind { /// The source contains Python source code. Python(String), /// The source contains a Jupyter notebook. - IpyNotebook(Notebook), + IpyNotebook(Box), } impl SourceKind { + pub fn ipy_notebook(notebook: Notebook) -> Self { + SourceKind::IpyNotebook(Box::new(notebook)) + } + + pub fn as_ipy_notebook(&self) -> Option<&Notebook> { + match self { + SourceKind::IpyNotebook(notebook) => Some(notebook), + SourceKind::Python(_) => None, + } + } + + pub fn as_python(&self) -> Option<&str> { + match self { + SourceKind::Python(code) => Some(code), + SourceKind::IpyNotebook(_) => None, + } + } + + pub fn expect_python(self) -> String { + match self { + SourceKind::Python(code) => code, + SourceKind::IpyNotebook(_) => panic!("expected python code"), + } + } + + pub fn expect_ipy_notebook(self) -> Notebook { + match self { + SourceKind::IpyNotebook(notebook) => *notebook, + SourceKind::Python(_) => panic!("expected ipy notebook"), + } + } + #[must_use] pub(crate) fn updated(&self, new_source: String, source_map: &SourceMap) -> Self { match self { @@ -52,7 +84,7 @@ impl SourceKind { let notebook = Notebook::from_path(path)?; Ok(notebook .is_python_notebook() - .then_some(Self::IpyNotebook(notebook))) + .then_some(Self::IpyNotebook(Box::new(notebook)))) } else { let contents = std::fs::read_to_string(path)?; Ok(Some(Self::Python(contents))) @@ -69,7 +101,7 @@ impl SourceKind { let notebook = Notebook::from_source_code(&source_code)?; Ok(notebook .is_python_notebook() - .then_some(Self::IpyNotebook(notebook))) + .then_some(Self::IpyNotebook(Box::new(notebook)))) } else { Ok(Some(Self::Python(source_code))) } diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 09b077334b648..bab73eb86ec3f 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -9,7 +9,7 @@ use anyhow::Result; use itertools::Itertools; use rustc_hash::FxHashMap; -use ruff_diagnostics::{Applicability, FixAvailability}; +use ruff_db::diagnostic::Diagnostic; use ruff_notebook::Notebook; #[cfg(not(fuzzing))] use ruff_notebook::NotebookError; @@ -20,16 +20,17 @@ use ruff_python_parser::{ParseError, ParseOptions}; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::SourceFileBuilder; -use crate::fix::{fix_file, FixResult}; +use crate::codes::Rule; +use crate::fix::{FixResult, fix_file}; use crate::linter::check_path; -use crate::message::{Emitter, EmitterContext, Message, TextEmitter}; +use crate::message::{Emitter, EmitterContext, TextEmitter, create_syntax_error_diagnostic}; use crate::package::PackageRoot; use crate::packaging::detect_package_root; -use crate::registry::AsRule; use crate::settings::types::UnsafeFixes; -use crate::settings::{flags, LinterSettings}; +use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; -use crate::{directives, Locator}; +use crate::{Applicability, FixAvailability}; +use crate::{Locator, directives}; #[cfg(not(fuzzing))] pub(crate) fn test_resource_path(path: impl AsRef) -> std::path::PathBuf { @@ -38,7 +39,10 @@ pub(crate) fn test_resource_path(path: impl AsRef) -> std::path::PathBuf { /// Run [`check_path`] on a Python file in the `resources/test/fixtures` directory. #[cfg(not(fuzzing))] -pub(crate) fn test_path(path: impl AsRef, settings: &LinterSettings) -> Result> { +pub(crate) fn test_path( + path: impl AsRef, + settings: &LinterSettings, +) -> Result> { let path = test_resource_path("fixtures").join(path); let source_type = PySourceType::from(&path); let source_kind = SourceKind::from_path(path.as_ref(), source_type)?.expect("valid source"); @@ -47,7 +51,7 @@ pub(crate) fn test_path(path: impl AsRef, settings: &LinterSettings) -> Re #[cfg(not(fuzzing))] pub(crate) struct TestedNotebook { - pub(crate) messages: Vec, + pub(crate) diagnostics: Vec, pub(crate) source_notebook: Notebook, pub(crate) linted_notebook: Notebook, } @@ -60,7 +64,7 @@ pub(crate) fn assert_notebook_path( ) -> Result { let source_notebook = Notebook::from_path(path.as_ref())?; - let source_kind = SourceKind::IpyNotebook(source_notebook); + let source_kind = SourceKind::ipy_notebook(source_notebook); let (messages, transformed) = test_contents(&source_kind, path.as_ref(), settings); let expected_notebook = Notebook::from_path(expected.as_ref())?; let linted_notebook = transformed.into_owned().expect_ipy_notebook(); @@ -76,14 +80,14 @@ pub(crate) fn assert_notebook_path( ); Ok(TestedNotebook { - messages, + diagnostics: messages, source_notebook: source_kind.expect_ipy_notebook(), linted_notebook, }) } /// Run [`check_path`] on a snippet of Python code. -pub fn test_snippet(contents: &str, settings: &LinterSettings) -> Vec { +pub fn test_snippet(contents: &str, settings: &LinterSettings) -> Vec { let path = Path::new(""); let contents = dedent(contents); test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0 @@ -107,10 +111,11 @@ pub(crate) fn test_contents<'a>( source_kind: &'a SourceKind, path: &Path, settings: &LinterSettings, -) -> (Vec, Cow<'a, SourceKind>) { +) -> (Vec, Cow<'a, SourceKind>) { let source_type = PySourceType::from(path); let target_version = settings.resolve_target_version(path); - let options = ParseOptions::from(source_type).with_target_version(target_version); + let options = + ParseOptions::from(source_type).with_target_version(target_version.parser_version()); let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options.clone()) .try_into_module() .expect("PySourceType always parses into a module"); @@ -206,8 +211,7 @@ pub(crate) fn test_contents<'a>( if parsed.has_invalid_syntax() && !source_has_errors { // Previous fix introduced a syntax error, abort let fixes = print_diagnostics(messages, path, source_kind); - let syntax_errors = - print_syntax_errors(parsed.errors(), path, &locator, &transformed); + let syntax_errors = print_syntax_errors(parsed.errors(), path, &transformed); panic!( "Fixed source has a syntax error where the source document does not. This is a bug in one of the generated fixes: @@ -232,10 +236,10 @@ Source with applied fixes: let messages = messages .into_iter() - .filter_map(Message::into_diagnostic_message) - .map(|mut diagnostic| { - let rule = diagnostic.kind.rule(); - let fixable = diagnostic.fix.as_ref().is_some_and(|fix| { + .filter_map(|msg| Some((msg.secondary_code()?.to_string(), msg))) + .map(|(code, mut diagnostic)| { + let rule = Rule::from_code(&code).unwrap(); + let fixable = diagnostic.fix().is_some_and(|fix| { matches!( fix.applicability(), Applicability::Safe | Applicability::Unsafe @@ -268,37 +272,40 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e } assert!( - !(fixable && diagnostic.kind.suggestion.is_none()), + !(fixable && diagnostic.suggestion().is_none()), "Diagnostic emitted by {rule:?} is fixable but \ `Violation::fix_title` returns `None`" ); - // Not strictly necessary but adds some coverage for this code path - diagnostic.noqa_offset = directives.noqa_line_for.resolve(diagnostic.range.start()); - diagnostic.file = source_code.clone(); + // Not strictly necessary but adds some coverage for this code path by overriding the + // noqa offset and the source file + let range = diagnostic.expect_range(); + diagnostic.set_noqa_offset(directives.noqa_line_for.resolve(range.start())); + if let Some(annotation) = diagnostic.primary_annotation_mut() { + annotation.set_span( + ruff_db::diagnostic::Span::from(source_code.clone()).with_range(range), + ); + } - Message::Diagnostic(diagnostic) + diagnostic }) .chain(parsed.errors().iter().map(|parse_error| { - Message::from_parse_error(parse_error, &locator, source_code.clone()) + create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error) })) - .sorted() + .sorted_by(Diagnostic::ruff_start_ordering) .collect(); (messages, transformed) } -fn print_syntax_errors( - errors: &[ParseError], - path: &Path, - locator: &Locator, - source: &SourceKind, -) -> String { +fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind) -> String { let filename = path.file_name().unwrap().to_string_lossy(); let source_file = SourceFileBuilder::new(filename.as_ref(), source.source_code()).finish(); let messages: Vec<_> = errors .iter() - .map(|parse_error| Message::from_parse_error(parse_error, locator, source_file.clone())) + .map(|parse_error| { + create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error) + }) .collect(); if let Some(notebook) = source.as_ipy_notebook() { @@ -308,19 +315,19 @@ fn print_syntax_errors( } } -/// Print the [`Message::Diagnostic`]s in `messages`. -fn print_diagnostics(mut messages: Vec, path: &Path, source: &SourceKind) -> String { - messages.retain(Message::is_diagnostic_message); +/// Print the lint diagnostics in `diagnostics`. +fn print_diagnostics(mut diagnostics: Vec, path: &Path, source: &SourceKind) -> String { + diagnostics.retain(|msg| !msg.is_invalid_syntax()); if let Some(notebook) = source.as_ipy_notebook() { - print_jupyter_messages(&messages, path, notebook) + print_jupyter_messages(&diagnostics, path, notebook) } else { - print_messages(&messages) + print_messages(&diagnostics) } } pub(crate) fn print_jupyter_messages( - messages: &[Message], + diagnostics: &[Diagnostic], path: &Path, notebook: &Notebook, ) -> String { @@ -333,7 +340,7 @@ pub(crate) fn print_jupyter_messages( .with_unsafe_fixes(UnsafeFixes::Enabled) .emit( &mut output, - messages, + diagnostics, &EmitterContext::new(&FxHashMap::from_iter([( path.file_name().unwrap().to_string_lossy().to_string(), notebook.index().clone(), @@ -344,7 +351,7 @@ pub(crate) fn print_jupyter_messages( String::from_utf8(output).unwrap() } -pub(crate) fn print_messages(messages: &[Message]) -> String { +pub(crate) fn print_messages(diagnostics: &[Diagnostic]) -> String { let mut output = Vec::new(); TextEmitter::default() @@ -354,7 +361,7 @@ pub(crate) fn print_messages(messages: &[Message]) -> String { .with_unsafe_fixes(UnsafeFixes::Enabled) .emit( &mut output, - messages, + diagnostics, &EmitterContext::new(&FxHashMap::default()), ) .unwrap(); @@ -363,7 +370,7 @@ pub(crate) fn print_messages(messages: &[Message]) -> String { } #[macro_export] -macro_rules! assert_messages { +macro_rules! assert_diagnostics { ($value:expr, $path:expr, $notebook:expr) => {{ insta::with_settings!({ omit_expression => true }, { insta::assert_snapshot!( diff --git a/crates/ruff_diagnostics/src/violation.rs b/crates/ruff_linter/src/violation.rs similarity index 80% rename from crates/ruff_diagnostics/src/violation.rs rename to crates/ruff_linter/src/violation.rs index bf6690c5b28e4..f32fa40e9adcb 100644 --- a/crates/ruff_diagnostics/src/violation.rs +++ b/crates/ruff_linter/src/violation.rs @@ -1,6 +1,11 @@ -use crate::DiagnosticKind; use std::fmt::{Debug, Display}; +use ruff_db::diagnostic::Diagnostic; +use ruff_source_file::SourceFile; +use ruff_text_size::TextRange; + +use crate::{codes::Rule, message::create_lint_diagnostic}; + #[derive(Debug, Copy, Clone)] pub enum FixAvailability { Sometimes, @@ -19,15 +24,15 @@ impl Display for FixAvailability { } pub trait ViolationMetadata { - /// Returns the rule name of this violation - fn rule_name() -> &'static str; + /// Returns the rule for this violation + fn rule() -> Rule; /// Returns an explanation of what this violation catches, /// why it's bad, and what users should do instead. fn explain() -> Option<&'static str>; } -pub trait Violation: ViolationMetadata { +pub trait Violation: ViolationMetadata + Sized { /// `None` in the case a fix is never available or otherwise Some /// [`FixAvailability`] describing the available fix. const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; @@ -47,6 +52,20 @@ pub trait Violation: ViolationMetadata { /// Returns the format strings used by [`message`](Violation::message). fn message_formats() -> &'static [&'static str]; + + /// Convert the violation into a [`Diagnostic`]. + fn into_diagnostic(self, range: TextRange, file: &SourceFile) -> Diagnostic { + create_lint_diagnostic( + self.message(), + self.fix_title(), + range, + None, + None, + file.clone(), + None, + Self::rule(), + ) + } } /// This trait exists just to make implementing the [`Violation`] trait more @@ -79,16 +98,3 @@ impl Violation for V { ::message_formats() } } - -impl From for DiagnosticKind -where - T: Violation, -{ - fn from(value: T) -> Self { - Self { - body: Violation::message(&value), - suggestion: Violation::fix_title(&value), - name: T::rule_name().to_string(), - } - } -} diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index c0215053c5393..7e72d59855f72 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -17,6 +17,7 @@ doctest = false [dependencies] ruff_python_trivia = { workspace = true } +heck = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true, features = ["derive", "parsing", "extra-traits", "full"] } diff --git a/crates/ruff_macros/src/cache_key.rs b/crates/ruff_macros/src/cache_key.rs index 65418ac4aa1d1..747fd3726061d 100644 --- a/crates/ruff_macros/src/cache_key.rs +++ b/crates/ruff_macros/src/cache_key.rs @@ -92,7 +92,7 @@ pub(crate) fn derive_cache_key(item: &DeriveInput) -> syn::Result { return Err(Error::new( item.span(), "CacheKey does not support unions. Only structs and enums are supported", - )) + )); } }; @@ -143,7 +143,7 @@ impl Parse for CacheKeyFieldAttributes { return Err(Error::new( arg.span(), format!("Unknown `cache_field` argument {name}"), - )) + )); } } } diff --git a/crates/ruff_macros/src/combine.rs b/crates/ruff_macros/src/combine.rs index 3d755d8ac09d9..f18ecd60ee199 100644 --- a/crates/ruff_macros/src/combine.rs +++ b/crates/ruff_macros/src/combine.rs @@ -20,6 +20,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result syn::Result Self { - #[allow(deprecated)] + #[expect(deprecated)] Self { #( #output diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 0df56a2256426..6d4f6e4d90c41 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -86,8 +86,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { Ok(quote! { #[automatically_derived] - impl crate::options_base::OptionsMetadata for #ident { - fn record(visit: &mut dyn crate::options_base::Visit) { + impl ruff_options_metadata::OptionsMetadata for #ident { + fn record(visit: &mut dyn ruff_options_metadata::Visit) { #(#output);* } @@ -125,7 +125,7 @@ fn handle_option_group(field: &Field) -> syn::Result { let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); Ok(quote_spanned!( - ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>())) + ident.span() => (visit.record_set(#kebab_name, ruff_options_metadata::OptionSet::of::<#path>())) )) } _ => Err(syn::Error::new( @@ -214,14 +214,14 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { - visit.record_field(#kebab_name, crate::options_base::OptionField{ + visit.record_field(#kebab_name, ruff_options_metadata::OptionField{ doc: &#doc, default: &#default, value_type: &#value_type, @@ -271,15 +271,24 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result })?; let Some(default) = default else { - return Err(syn::Error::new(attribute.span(), "Mandatory `default` field is missing in `#[option]` attribute. Specify the default using `#[option(default=\"..\")]`.")); + return Err(syn::Error::new( + attribute.span(), + "Mandatory `default` field is missing in `#[option]` attribute. Specify the default using `#[option(default=\"..\")]`.", + )); }; let Some(value_type) = value_type else { - return Err(syn::Error::new(attribute.span(), "Mandatory `value_type` field is missing in `#[option]` attribute. Specify the value type using `#[option(value_type=\"..\")]`.")); + return Err(syn::Error::new( + attribute.span(), + "Mandatory `value_type` field is missing in `#[option]` attribute. Specify the value type using `#[option(value_type=\"..\")]`.", + )); }; let Some(example) = example else { - return Err(syn::Error::new(attribute.span(), "Mandatory `example` field is missing in `#[option]` attribute. Add an example using `#[option(example=\"..\")]`.")); + return Err(syn::Error::new( + attribute.span(), + "Mandatory `example` field is missing in `#[option]` attribute. Add an example using `#[option(example=\"..\")]`.", + )); }; Ok(FieldAttributes { diff --git a/crates/ruff_macros/src/derive_message_formats.rs b/crates/ruff_macros/src/derive_message_formats.rs index 3ee3826edf809..67d522f9b76f5 100644 --- a/crates/ruff_macros/src/derive_message_formats.rs +++ b/crates/ruff_macros/src/derive_message_formats.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use quote::{quote, quote_spanned, ToTokens}; +use quote::{ToTokens, quote, quote_spanned}; use syn::spanned::Spanned; use syn::token::{Dot, Paren}; use syn::{Block, Expr, ExprLit, ExprMethodCall, ItemFn, Lit, Stmt}; diff --git a/crates/ruff_macros/src/env_vars.rs b/crates/ruff_macros/src/env_vars.rs new file mode 100644 index 0000000000000..da59c679962b9 --- /dev/null +++ b/crates/ruff_macros/src/env_vars.rs @@ -0,0 +1,95 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ImplItem, ItemImpl}; + +pub(crate) fn attribute_env_vars_metadata(mut input: ItemImpl) -> TokenStream { + // Verify that this is an impl for EnvVars + let impl_type = &input.self_ty; + + let mut env_var_entries = Vec::new(); + let mut hidden_vars = Vec::new(); + + // Process each item in the impl block + for item in &mut input.items { + if let ImplItem::Const(const_item) = item { + // Extract the const name and value + let const_name = &const_item.ident; + let const_expr = &const_item.expr; + + // Check if the const has the #[attr_hidden] attribute + let is_hidden = const_item + .attrs + .iter() + .any(|attr| attr.path().is_ident("attr_hidden")); + + // Remove our custom attributes + const_item.attrs.retain(|attr| { + !attr.path().is_ident("attr_hidden") + && !attr.path().is_ident("attr_env_var_pattern") + }); + + if is_hidden { + hidden_vars.push(const_name.clone()); + } else { + // Extract documentation from doc comments + let doc_attrs: Vec<_> = const_item + .attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .collect(); + + if !doc_attrs.is_empty() { + // Convert doc attributes to a single string + let doc_string = extract_doc_string(&doc_attrs); + env_var_entries.push((const_name.clone(), const_expr.clone(), doc_string)); + } + } + } + } + + // Generate the metadata method. + let metadata_entries: Vec<_> = env_var_entries + .iter() + .map(|(_name, expr, doc)| { + quote! { + (#expr, #doc) + } + }) + .collect(); + + let metadata_impl = quote! { + impl #impl_type { + /// Returns metadata for all non-hidden environment variables. + pub fn metadata() -> Vec<(&'static str, &'static str)> { + vec![ + #(#metadata_entries),* + ] + } + } + }; + + quote! { + #input + #metadata_impl + } +} + +/// Extract documentation from doc attributes into a single string +fn extract_doc_string(attrs: &[&syn::Attribute]) -> String { + attrs + .iter() + .filter_map(|attr| { + if let syn::Meta::NameValue(meta) = &attr.meta { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit_str), + .. + }) = &meta.value + { + return Some(lit_str.value().trim().to_string()); + } + } + None + }) + .collect::>() + .join("\n") +} diff --git a/crates/ruff_macros/src/kebab_case.rs b/crates/ruff_macros/src/kebab_case.rs index 1f6dcf42fd387..3b7ef3f8db0a0 100644 --- a/crates/ruff_macros/src/kebab_case.rs +++ b/crates/ruff_macros/src/kebab_case.rs @@ -1,19 +1,10 @@ +use heck::ToKebabCase; use proc_macro2::TokenStream; pub(crate) fn kebab_case(input: &syn::Ident) -> TokenStream { - let screaming_snake_case = input.to_string(); + let s = input.to_string(); - let mut kebab_case = String::with_capacity(screaming_snake_case.len()); - - for (i, word) in screaming_snake_case.split('_').enumerate() { - if i > 0 { - kebab_case.push('-'); - } - - kebab_case.push_str(&word.to_lowercase()); - } - - let kebab_case_lit = syn::LitStr::new(&kebab_case, input.span()); + let kebab_case_lit = syn::LitStr::new(&s.to_kebab_case(), input.span()); quote::quote!(#kebab_case_lit) } diff --git a/crates/ruff_macros/src/lib.rs b/crates/ruff_macros/src/lib.rs index 05913ddee843b..ef72240409678 100644 --- a/crates/ruff_macros/src/lib.rs +++ b/crates/ruff_macros/src/lib.rs @@ -1,21 +1,23 @@ -//! This crate implements internal macros for the `ruff` library. +//! This crate implements internal macros for the `ruff` and `ty` libraries. use crate::cache_key::derive_cache_key; use crate::newtype_index::generate_newtype_index; use crate::violation_metadata::violation_metadata; use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, Error, ItemFn, ItemStruct}; +use syn::{DeriveInput, Error, ItemFn, ItemStruct, parse_macro_input}; mod cache_key; mod combine; mod combine_options; mod config; mod derive_message_formats; +mod env_vars; mod kebab_case; mod map_codes; mod newtype_index; mod rule_code_prefix; mod rule_namespace; +mod rust_doc; mod violation_metadata; #[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))] @@ -27,6 +29,15 @@ pub fn derive_options_metadata(input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(RustDoc)] +pub fn derive_rust_doc(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + rust_doc::derive_impl(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + #[proc_macro_derive(CombineOptions)] pub fn derive_combine_options(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -36,8 +47,8 @@ pub fn derive_combine_options(input: TokenStream) -> TokenStream { .into() } -/// Automatically derives a `red_knot_project::project::Combine` implementation for the attributed type -/// that calls `red_knot_project::project::Combine::combine` for each field. +/// Automatically derives a `ty_project::project::Combine` implementation for the attributed type +/// that calls `ty_project::project::Combine::combine` for each field. /// /// The derive macro can only be used on structs. Enums aren't yet supported. #[proc_macro_derive(Combine)] @@ -49,7 +60,7 @@ pub fn derive_combine(input: TokenStream) -> TokenStream { .into() } -/// Converts a screaming snake case identifier to a kebab case string. +/// Converts an identifier to a kebab case string. #[proc_macro] pub fn kebab_case(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::Ident); @@ -134,3 +145,15 @@ pub fn newtype_index(_metadata: TokenStream, input: TokenStream) -> TokenStream TokenStream::from(output) } + +/// Generates metadata for environment variables declared in the impl block. +/// +/// This attribute macro should be applied to an `impl EnvVars` block. +/// It will generate a `metadata()` method that returns all non-hidden +/// environment variables with their documentation. +#[proc_macro_attribute] +pub fn attribute_env_vars_metadata(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as syn::ItemImpl); + + env_vars::attribute_env_vars_metadata(input).into() +} diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index a38f7cc543b99..39993af6afb01 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -2,10 +2,10 @@ use std::collections::{BTreeMap, HashMap}; use itertools::Itertools; use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; +use quote::{ToTokens, quote}; use syn::{ - parenthesized, parse::Parse, spanned::Spanned, Attribute, Error, Expr, ExprCall, ExprMatch, - Ident, ItemFn, LitStr, Pat, Path, Stmt, Token, + Attribute, Error, Expr, ExprCall, ExprMatch, Ident, ItemFn, LitStr, Pat, Path, Stmt, Token, + parenthesized, parse::Parse, spanned::Spanned, }; use crate::rule_code_prefix::{get_prefix_ident, intersection_all}; @@ -174,7 +174,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { output.extend(quote! { impl #linter { - pub fn rules(&self) -> ::std::vec::IntoIter { + pub(crate) fn rules(&self) -> ::std::vec::IntoIter { match self { #prefix_into_iter_match_arms } } } @@ -182,7 +182,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { } output.extend(quote! { impl RuleCodePrefix { - pub fn parse(linter: &Linter, code: &str) -> Result { + pub(crate) fn parse(linter: &Linter, code: &str) -> Result { use std::str::FromStr; Ok(match linter { @@ -190,7 +190,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { }) } - pub fn rules(&self) -> ::std::vec::IntoIter { + pub(crate) fn rules(&self) -> ::std::vec::IntoIter { match self { #(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)* } @@ -319,7 +319,7 @@ See also https://github.com/astral-sh/ruff/issues/2186. matches!(self.group(), RuleGroup::Preview) } - pub fn is_stable(&self) -> bool { + pub(crate) fn is_stable(&self) -> bool { matches!(self.group(), RuleGroup::Stable) } @@ -371,7 +371,7 @@ fn generate_iter_impl( quote! { impl Linter { /// Rules not in the preview. - pub fn rules(self: &Linter) -> ::std::vec::IntoIter { + pub(crate) fn rules(self: &Linter) -> ::std::vec::IntoIter { match self { #linter_rules_match_arms } @@ -385,7 +385,7 @@ fn generate_iter_impl( } impl RuleCodePrefix { - pub fn iter() -> impl Iterator { + pub(crate) fn iter() -> impl Iterator { use strum::IntoEnumIterator; let mut prefixes = Vec::new(); @@ -404,8 +404,6 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { let mut rule_fixable_match_arms = quote!(); let mut rule_explanation_match_arms = quote!(); - let mut from_impls_for_diagnostic_kind = quote!(); - for Rule { name, attrs, path, .. } in input @@ -415,20 +413,17 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { #name, }); // Apply the `attrs` to each arm, like `[cfg(feature = "foo")]`. - rule_message_formats_match_arms - .extend(quote! {#(#attrs)* Self::#name => <#path as ruff_diagnostics::Violation>::message_formats(),}); + rule_message_formats_match_arms.extend( + quote! {#(#attrs)* Self::#name => <#path as crate::Violation>::message_formats(),}, + ); rule_fixable_match_arms.extend( - quote! {#(#attrs)* Self::#name => <#path as ruff_diagnostics::Violation>::FIX_AVAILABILITY,}, + quote! {#(#attrs)* Self::#name => <#path as crate::Violation>::FIX_AVAILABILITY,}, ); rule_explanation_match_arms.extend(quote! {#(#attrs)* Self::#name => #path::explain(),}); - - // Enable conversion from `DiagnosticKind` to `Rule`. - from_impls_for_diagnostic_kind - .extend(quote! {#(#attrs)* stringify!(#name) => Rule::#name,}); } quote! { - use ruff_diagnostics::Violation; + use crate::Violation; #[derive( EnumIter, @@ -441,8 +436,10 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { PartialOrd, Ord, ::ruff_macros::CacheKey, - AsRefStr, ::strum_macros::IntoStaticStr, + ::strum_macros::EnumString, + ::serde::Serialize, + ::serde::Deserialize, )] #[repr(u16)] #[strum(serialize_all = "kebab-case")] @@ -456,25 +453,15 @@ fn register_rules<'a>(input: impl Iterator) -> TokenStream { /// Returns the documentation for this rule. pub fn explanation(&self) -> Option<&'static str> { - use ruff_diagnostics::ViolationMetadata; + use crate::ViolationMetadata; match self { #rule_explanation_match_arms } } /// Returns the fix status of this rule. - pub const fn fixable(&self) -> ruff_diagnostics::FixAvailability { + pub const fn fixable(&self) -> crate::FixAvailability { match self { #rule_fixable_match_arms } } } - - - impl AsRule for ruff_diagnostics::DiagnosticKind { - fn rule(&self) -> Rule { - match self.name.as_str() { - #from_impls_for_diagnostic_kind - _ => unreachable!("invalid rule name: {}", self.name), - } - } - } } } diff --git a/crates/ruff_macros/src/newtype_index.rs b/crates/ruff_macros/src/newtype_index.rs index ad3638d21f7b6..99663542d7a7e 100644 --- a/crates/ruff_macros/src/newtype_index.rs +++ b/crates/ruff_macros/src/newtype_index.rs @@ -48,7 +48,7 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result syn::Result syn::Result { + let docs = get_docs(&input.attrs)?; + + let name = input.ident; + + let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics ruff_db::RustDoc for #name #ty_generics #where_clause { + fn rust_doc() -> &'static str { + #docs + } + } + }) +} +/// Collect all doc comment attributes into a string +fn get_docs(attrs: &[Attribute]) -> syn::Result { + let mut explanation = String::new(); + for attr in attrs { + if attr.path().is_ident("doc") { + if let Some(lit) = parse_attr(["doc"], attr) { + let value = lit.value(); + // `/// ` adds + let line = value.strip_prefix(' ').unwrap_or(&value); + explanation.push_str(line); + explanation.push('\n'); + } else { + return Err(Error::new_spanned(attr, "unimplemented doc comment style")); + } + } + } + Ok(explanation) +} + +fn parse_attr<'a, const LEN: usize>( + path: [&'static str; LEN], + attr: &'a Attribute, +) -> Option<&'a LitStr> { + if let Meta::NameValue(name_value) = &attr.meta { + let path_idents = name_value + .path + .segments + .iter() + .map(|segment| &segment.ident); + + if path_idents.eq(path) { + if let syn::Expr::Lit(syn::ExprLit { + lit: Lit::Str(lit), .. + }) = &name_value.value + { + return Some(lit); + } + } + } + + None +} diff --git a/crates/ruff_macros/src/violation_metadata.rs b/crates/ruff_macros/src/violation_metadata.rs index c64ee30b211bb..3bdceb40790e6 100644 --- a/crates/ruff_macros/src/violation_metadata.rs +++ b/crates/ruff_macros/src/violation_metadata.rs @@ -7,12 +7,14 @@ pub(crate) fn violation_metadata(input: DeriveInput) -> syn::Result let name = input.ident; + let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl(); + Ok(quote! { #[automatically_derived] - #[allow(deprecated)] - impl ruff_diagnostics::ViolationMetadata for #name { - fn rule_name() -> &'static str { - stringify!(#name) + #[expect(deprecated)] + impl #impl_generics crate::ViolationMetadata for #name #ty_generics #where_clause { + fn rule() -> crate::registry::Rule { + crate::registry::Rule::#name } fn explain() -> Option<&'static str> { diff --git a/crates/ruff_notebook/src/cell.rs b/crates/ruff_notebook/src/cell.rs index 949f55726a1f6..56c1822341f79 100644 --- a/crates/ruff_notebook/src/cell.rs +++ b/crates/ruff_notebook/src/cell.rs @@ -5,8 +5,8 @@ use itertools::Itertools; use ruff_text_size::{TextRange, TextSize}; -use crate::schema::{Cell, SourceValue}; use crate::CellMetadata; +use crate::schema::{Cell, SourceValue}; impl fmt::Display for SourceValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/crates/ruff_notebook/src/index.rs b/crates/ruff_notebook/src/index.rs index 9912b94e6225e..35e4e07fcbe52 100644 --- a/crates/ruff_notebook/src/index.rs +++ b/crates/ruff_notebook/src/index.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use ruff_source_file::{OneIndexed, SourceLocation}; +use ruff_source_file::{LineColumn, OneIndexed, SourceLocation}; /// Jupyter Notebook indexing table /// @@ -33,16 +33,29 @@ impl NotebookIndex { self.row_to_row_in_cell.get(row.to_zero_indexed()).copied() } - /// Translates the given source location based on the indexing table. + /// Translates the given [`LineColumn`] based on the indexing table. /// /// This will translate the row/column in the concatenated source code /// to the row/column in the Jupyter Notebook. - pub fn translate_location(&self, source_location: &SourceLocation) -> SourceLocation { - SourceLocation { - row: self - .cell_row(source_location.row) + pub fn translate_line_column(&self, source_location: &LineColumn) -> LineColumn { + LineColumn { + line: self + .cell_row(source_location.line) .unwrap_or(OneIndexed::MIN), column: source_location.column, } } + + /// Translates the given [`SourceLocation`] based on the indexing table. + /// + /// This will translate the line/character in the concatenated source code + /// to the line/character in the Jupyter Notebook. + pub fn translate_source_location(&self, source_location: &SourceLocation) -> SourceLocation { + SourceLocation { + line: self + .cell_row(source_location.line) + .unwrap_or(OneIndexed::MIN), + character_offset: source_location.character_offset, + } + } } diff --git a/crates/ruff_notebook/src/notebook.rs b/crates/ruff_notebook/src/notebook.rs index 3c2855f6ffb29..124202e9274cc 100644 --- a/crates/ruff_notebook/src/notebook.rs +++ b/crates/ruff_notebook/src/notebook.rs @@ -18,7 +18,7 @@ use ruff_text_size::TextSize; use crate::cell::CellOffsets; use crate::index::NotebookIndex; use crate::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue}; -use crate::{schema, CellMetadata, RawNotebookMetadata}; +use crate::{CellMetadata, RawNotebookMetadata, schema}; /// Run round-trip source code generation on a given Jupyter notebook file path. pub fn round_trip(path: &Path) -> anyhow::Result { @@ -43,7 +43,9 @@ pub enum NotebookError { Io(#[from] io::Error), #[error(transparent)] Json(serde_json::Error), - #[error("Expected a Jupyter Notebook, which must be internally stored as JSON, but this file isn't valid JSON: {0}")] + #[error( + "Expected a Jupyter Notebook, which must be internally stored as JSON, but this file isn't valid JSON: {0}" + )] InvalidJson(serde_json::Error), #[error("This file does not match the schema expected of Jupyter Notebooks: {0}")] InvalidSchema(serde_json::Error), diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml new file mode 100644 index 0000000000000..55f3f4ad19df2 --- /dev/null +++ b/crates/ruff_options_metadata/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ruff_options_metadata" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +serde = { workspace = true, optional = true } + +[dev-dependencies] + +[lints] +workspace = true diff --git a/crates/ruff_options_metadata/src/lib.rs b/crates/ruff_options_metadata/src/lib.rs new file mode 100644 index 0000000000000..7f7332c3c2429 --- /dev/null +++ b/crates/ruff_options_metadata/src/lib.rs @@ -0,0 +1,440 @@ +use std::fmt::{Debug, Display, Formatter}; + +/// Visits [`OptionsMetadata`]. +/// +/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata. +pub trait Visit { + /// Visits an [`OptionField`] value named `name`. + fn record_field(&mut self, name: &str, field: OptionField); + + /// Visits an [`OptionSet`] value named `name`. + fn record_set(&mut self, name: &str, group: OptionSet); +} + +/// Returns metadata for its options. +pub trait OptionsMetadata { + /// Visits the options metadata of this object by calling `visit` for each option. + fn record(visit: &mut dyn Visit); + + fn documentation() -> Option<&'static str> { + None + } + + /// Returns the extracted metadata. + fn metadata() -> OptionSet + where + Self: Sized + 'static, + { + OptionSet::of::() + } +} + +impl OptionsMetadata for Option +where + T: OptionsMetadata, +{ + fn record(visit: &mut dyn Visit) { + T::record(visit); + } +} + +/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`]. +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize), serde(untagged))] +pub enum OptionEntry { + /// A single option. + Field(OptionField), + + /// A set of options. + Set(OptionSet), +} + +impl Display for OptionEntry { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + OptionEntry::Set(set) => std::fmt::Display::fmt(set, f), + OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f), + } + } +} + +/// A set of options. +/// +/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing +/// [`OptionsMetadata`]. +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct OptionSet { + record: fn(&mut dyn Visit), + doc: fn() -> Option<&'static str>, +} + +impl OptionSet { + pub fn of() -> Self + where + T: OptionsMetadata + 'static, + { + Self { + record: T::record, + doc: T::documentation, + } + } + + /// Visits the options in this set by calling `visit` for each option. + pub fn record(&self, visit: &mut dyn Visit) { + let record = self.record; + record(visit); + } + + pub fn documentation(&self) -> Option<&'static str> { + let documentation = self.doc; + documentation() + } + + /// Returns `true` if this set has an option that resolves to `name`. + /// + /// The name can be separated by `.` to find a nested option. + /// + /// ## Examples + /// + /// ### Test for the existence of a child option + /// + /// ```rust + /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit}; + /// + /// struct WithOptions; + /// + /// impl OptionsMetadata for WithOptions { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None, + /// }); + /// } + /// } + /// + /// assert!(WithOptions::metadata().has("ignore-git-ignore")); + /// assert!(!WithOptions::metadata().has("does-not-exist")); + /// ``` + /// ### Test for the existence of a nested option + /// + /// ```rust + /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit}; + /// + /// struct Root; + /// + /// impl OptionsMetadata for Root { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }); + /// + /// visit.record_set("format", Nested::metadata()); + /// } + /// } + /// + /// struct Nested; + /// + /// impl OptionsMetadata for Nested { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("hard-tabs", OptionField { + /// doc: "Use hard tabs for indentation and spaces for alignment.", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }); + /// } + /// } + /// + /// assert!(Root::metadata().has("format.hard-tabs")); + /// assert!(!Root::metadata().has("format.spaces")); + /// assert!(!Root::metadata().has("lint.hard-tabs")); + /// ``` + pub fn has(&self, name: &str) -> bool { + self.find(name).is_some() + } + + /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise. + /// + /// The name can be separated by `.` to find a nested option. + /// + /// ## Examples + /// + /// ### Find a child option + /// + /// ```rust + /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// + /// struct WithOptions; + /// + /// static IGNORE_GIT_IGNORE: OptionField = OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }; + /// + /// impl OptionsMetadata for WithOptions { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone()); + /// } + /// } + /// + /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore"), Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))); + /// assert_eq!(WithOptions::metadata().find("does-not-exist"), None); + /// ``` + /// ### Find a nested option + /// + /// ```rust + /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// + /// static HARD_TABS: OptionField = OptionField { + /// doc: "Use hard tabs for indentation and spaces for alignment.", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }; + /// + /// struct Root; + /// + /// impl OptionsMetadata for Root { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }); + /// + /// visit.record_set("format", Nested::metadata()); + /// } + /// } + /// + /// struct Nested; + /// + /// impl OptionsMetadata for Nested { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("hard-tabs", HARD_TABS.clone()); + /// } + /// } + /// + /// assert_eq!(Root::metadata().find("format.hard-tabs"), Some(OptionEntry::Field(HARD_TABS.clone()))); + /// assert_eq!(Root::metadata().find("format"), Some(OptionEntry::Set(Nested::metadata()))); + /// assert_eq!(Root::metadata().find("format.spaces"), None); + /// assert_eq!(Root::metadata().find("lint.hard-tabs"), None); + /// ``` + pub fn find(&self, name: &str) -> Option { + struct FindOptionVisitor<'a> { + option: Option, + parts: std::str::Split<'a, char>, + needle: &'a str, + } + + impl Visit for FindOptionVisitor<'_> { + fn record_set(&mut self, name: &str, set: OptionSet) { + if self.option.is_none() && name == self.needle { + if let Some(next) = self.parts.next() { + self.needle = next; + set.record(self); + } else { + self.option = Some(OptionEntry::Set(set)); + } + } + } + + fn record_field(&mut self, name: &str, field: OptionField) { + if self.option.is_none() && name == self.needle { + if self.parts.next().is_none() { + self.option = Some(OptionEntry::Field(field)); + } + } + } + } + + let mut parts = name.split('.'); + + if let Some(first) = parts.next() { + let mut visitor = FindOptionVisitor { + parts, + needle: first, + option: None, + }; + + self.record(&mut visitor); + visitor.option + } else { + None + } + } + + pub fn collect_fields(&self) -> Vec<(String, OptionField)> { + struct FieldsCollector(Vec<(String, OptionField)>); + + impl Visit for FieldsCollector { + fn record_field(&mut self, name: &str, field: OptionField) { + self.0.push((name.to_string(), field)); + } + + fn record_set(&mut self, _name: &str, _group: OptionSet) {} + } + + let mut visitor = FieldsCollector(vec![]); + self.record(&mut visitor); + visitor.0 + } +} + +/// Visitor that writes out the names of all fields and sets. +struct DisplayVisitor<'fmt, 'buf> { + f: &'fmt mut Formatter<'buf>, + result: std::fmt::Result, +} + +impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> { + fn new(f: &'fmt mut Formatter<'buf>) -> Self { + Self { f, result: Ok(()) } + } + + fn finish(self) -> std::fmt::Result { + self.result + } +} + +impl Visit for DisplayVisitor<'_, '_> { + fn record_set(&mut self, name: &str, _: OptionSet) { + self.result = self.result.and_then(|()| writeln!(self.f, "{name}")); + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.result = self.result.and_then(|()| { + write!(self.f, "{name}")?; + + if field.deprecated.is_some() { + write!(self.f, " (deprecated)")?; + } + + writeln!(self.f) + }); + } +} + +impl Display for OptionSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut visitor = DisplayVisitor::new(f); + self.record(&mut visitor); + visitor.finish() + } +} + +impl Debug for OptionSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize))] +pub struct OptionField { + pub doc: &'static str, + /// Ex) `"false"` + pub default: &'static str, + /// Ex) `"bool"` + pub value_type: &'static str, + /// Ex) `"per-file-ignores"` + pub scope: Option<&'static str>, + pub example: &'static str, + pub deprecated: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize))] +pub struct Deprecated { + pub since: Option<&'static str>, + pub message: Option<&'static str>, +} + +impl Display for OptionField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.doc)?; + writeln!(f)?; + writeln!(f, "Default value: {}", self.default)?; + writeln!(f, "Type: {}", self.value_type)?; + + if let Some(deprecated) = &self.deprecated { + write!(f, "Deprecated")?; + + if let Some(since) = deprecated.since { + write!(f, " (since {since})")?; + } + + if let Some(message) = deprecated.message { + write!(f, ": {message}")?; + } + + writeln!(f)?; + } + + writeln!(f, "Example usage:\n```toml\n{}\n```", self.example) + } +} + +#[cfg(feature = "serde")] +mod serde { + use super::{OptionField, OptionSet, Visit}; + use serde::{Serialize, Serializer}; + use std::collections::BTreeMap; + + impl Serialize for OptionSet { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut entries = BTreeMap::new(); + let mut visitor = SerializeVisitor { + entries: &mut entries, + }; + self.record(&mut visitor); + entries.serialize(serializer) + } + } + + struct SerializeVisitor<'a> { + entries: &'a mut BTreeMap, + } + + impl Visit for SerializeVisitor<'_> { + fn record_set(&mut self, name: &str, set: OptionSet) { + // Collect the entries of the set. + let mut entries = BTreeMap::new(); + let mut visitor = SerializeVisitor { + entries: &mut entries, + }; + set.record(&mut visitor); + + // Insert the set into the entries. + for (key, value) in entries { + self.entries.insert(format!("{name}.{key}"), value); + } + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.entries.insert(name.to_string(), field); + } + } +} diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 22f484fdbc0d1..5a50f4ee74c02 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -22,6 +22,7 @@ ruff_text_size = { workspace = true } aho-corasick = { workspace = true } bitflags = { workspace = true } compact_str = { workspace = true } +get-size2 = { workspace = true, optional = true } is-macro = { workspace = true } itertools = { workspace = true } memchr = { workspace = true } @@ -29,6 +30,7 @@ rustc-hash = { workspace = true } salsa = { workspace = true, optional = true } schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } +thiserror = { workspace = true } [features] schemars = ["dep:schemars"] @@ -39,6 +41,10 @@ serde = [ "dep:ruff_cache", "compact_str/serde", ] +get-size = [ + "dep:get-size2", + "ruff_text_size/get-size" +] [lints] workspace = true diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 8d2b9e067d4d0..38a9415515d25 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -433,6 +433,18 @@ See also [JoinedStr](https://docs.python.org/3/library/ast.html#ast.JoinedStr)"" fields = [{ name = "value", type = "FStringValue" }] custom_source_order = true +[Expr.nodes.ExprTString] +doc = """An AST node that represents either a single-part t-string literal +or an implicitly concatenated t-string literal. + +This type differs from the original Python AST `TemplateStr` in that it +doesn't join the implicitly concatenated parts into a single string. Instead, +it keeps them separate and provide various methods to access the parts. + +See also [TemplateStr](https://docs.python.org/3/library/ast.html#ast.TemplateStr)""" +fields = [{ name = "value", type = "TStringValue" }] +custom_source_order = true + [Expr.nodes.ExprStringLiteral] doc = """An AST node that represents either a single-part string literal or an implicitly concatenated string literal.""" @@ -539,9 +551,10 @@ doc = "See also [excepthandler](https://docs.python.org/3/library/ast.html#ast.e [ExceptHandler.nodes] ExceptHandlerExceptHandler = {} -[FStringElement.nodes] -FStringExpressionElement = { variant = "Expression" } -FStringLiteralElement = { variant = "Literal" } +[InterpolatedStringElement.nodes] +InterpolatedElement = { variant = "Interpolation" } +InterpolatedStringLiteralElement = { variant = "Literal" } + [Pattern] doc = "See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern)" @@ -565,7 +578,7 @@ TypeParamTypeVarTuple = {} TypeParamParamSpec = {} [ungrouped.nodes] -FStringFormatSpec = {} +InterpolatedStringFormatSpec = {} PatternArguments = {} PatternKeyword = {} Comprehension = {} @@ -581,6 +594,7 @@ Decorator = {} ElifElseClause = {} TypeParams = {} FString = {} +TString = {} StringLiteral = {} BytesLiteral = {} Identifier = {} diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index 24e344908a9e0..2b8687b23334b 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -15,7 +15,7 @@ import tomllib # Types that require `crate::`. We can slowly remove these types as we move them to generate scripts. -types_requiring_create_prefix = { +types_requiring_crate_prefix = { "IpyEscapeKind", "ExprContext", "Identifier", @@ -23,6 +23,7 @@ "BytesLiteralValue", "StringLiteralValue", "FStringValue", + "TStringValue", "Arguments", "CmpOp", "Comprehension", @@ -40,6 +41,23 @@ } +@dataclass +class VisitorInfo: + name: str + accepts_sequence: bool = False + + +# Map of AST node types to their corresponding visitor information. +# Only visitors that are different from the default `visit_*` method are included. +# These visitors either have a different name or accept a sequence of items. +type_to_visitor_function: dict[str, VisitorInfo] = { + "TypeParams": VisitorInfo("visit_type_params", True), + "Parameters": VisitorInfo("visit_parameters", True), + "Stmt": VisitorInfo("visit_body", True), + "Arguments": VisitorInfo("visit_arguments", True), +} + + def rustfmt(code: str) -> str: return check_output(["rustfmt", "--emit=stdout"], input=code, text=True) @@ -202,6 +220,7 @@ def extract_type_argument(rust_type_str: str) -> str: if close_bracket_index == -1 or close_bracket_index <= open_bracket_index: raise ValueError(f"Brackets are not balanced for type {rust_type_str}") inner_type = rust_type_str[open_bracket_index + 1 : close_bracket_index].strip() + inner_type = inner_type.replace("crate::", "") return inner_type @@ -281,9 +300,11 @@ def write_owned_enum(out: list[str], ast: Ast) -> None: Also creates: - `impl Ranged for TypeParam` + - `impl HasNodeIndex for TypeParam` - `TypeParam::visit_source_order` - `impl From for TypeParam` - `impl Ranged for TypeParamTypeVar` + - `impl HasNodeIndex for TypeParamTypeVar` - `fn TypeParam::is_type_var() -> bool` If the `add_suffix_to_is_methods` group option is true, then the @@ -295,6 +316,7 @@ def write_owned_enum(out: list[str], ast: Ast) -> None: if group.doc is not None: write_rustdoc(out, group.doc) out.append("#[derive(Clone, Debug, PartialEq)]") + out.append('#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]') out.append(f"pub enum {group.owned_enum_ty} {{") for node in group.nodes: out.append(f"{node.variant}({node.ty}),") @@ -322,6 +344,19 @@ def write_owned_enum(out: list[str], ast: Ast) -> None: } """) + out.append(f""" + impl crate::HasNodeIndex for {group.owned_enum_ty} {{ + fn node_index(&self) -> &crate::AtomicNodeIndex {{ + match self {{ + """) + for node in group.nodes: + out.append(f"Self::{node.variant}(node) => node.node_index(),") + out.append(""" + } + } + } + """) + out.append( "#[allow(dead_code, clippy::match_wildcard_for_single_variants)]" ) # Not all is_methods are used @@ -418,6 +453,15 @@ def write_owned_enum(out: list[str], ast: Ast) -> None: }} """) + for node in ast.all_nodes: + out.append(f""" + impl crate::HasNodeIndex for {node.ty} {{ + fn node_index(&self) -> &crate::AtomicNodeIndex {{ + &self.node_index + }} + }} + """) + for group in ast.groups: out.append(f""" impl {group.owned_enum_ty} {{ @@ -459,6 +503,7 @@ def write_ref_enum(out: list[str], ast: Ast) -> None: - `impl<'a> From<&'a TypeParam> for TypeParamRef<'a>` - `impl<'a> From<&'a TypeParamTypeVar> for TypeParamRef<'a>` - `impl Ranged for TypeParamRef<'_>` + - `impl HasNodeIndex for TypeParamRef<'_>` - `fn TypeParamRef::is_type_var() -> bool` The name of each variant can be customized via the `variant` node option. If @@ -471,6 +516,7 @@ def write_ref_enum(out: list[str], ast: Ast) -> None: if group.doc is not None: write_rustdoc(out, group.doc) out.append("""#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)]""") + out.append('#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]') out.append(f"""pub enum {group.ref_enum_ty}<'a> {{""") for node in group.nodes: if group.add_suffix_to_is_methods: @@ -516,6 +562,19 @@ def write_ref_enum(out: list[str], ast: Ast) -> None: } """) + out.append(f""" + impl crate::HasNodeIndex for {group.ref_enum_ty}<'_> {{ + fn node_index(&self) -> &crate::AtomicNodeIndex {{ + match self {{ + """) + for node in group.nodes: + out.append(f"Self::{node.variant}(node) => node.node_index(),") + out.append(""" + } + } + } + """) + # ------------------------------------------------------------------------------ # AnyNodeRef @@ -539,12 +598,15 @@ def write_anynoderef(out: list[str], ast: Ast) -> None: - `impl<'a> From> for AnyNodeRef<'a>` - `impl<'a> From<&'a TypeParamTypeVarTuple> for AnyNodeRef<'a>` - `impl Ranged for AnyNodeRef<'_>` + - `impl HasNodeIndex for AnyNodeRef<'_>` - `fn AnyNodeRef::as_ptr(&self) -> std::ptr::NonNull<()>` - `fn AnyNodeRef::visit_source_order(self, visitor &mut impl SourceOrderVisitor)` """ out.append(""" + /// A flattened enumeration of all AST nodes. #[derive(Copy, Clone, Debug, is_macro::Is, PartialEq)] + #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum AnyNodeRef<'a> { """) for node in ast.all_nodes: @@ -623,6 +685,19 @@ def write_anynoderef(out: list[str], ast: Ast) -> None: } """) + out.append(""" + impl crate::HasNodeIndex for AnyNodeRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + """) + for node in ast.all_nodes: + out.append(f"""AnyNodeRef::{node.name}(node) => node.node_index(),""") + out.append(""" + } + } + } + """) + out.append(""" impl AnyNodeRef<'_> { pub fn as_ptr(&self) -> std::ptr::NonNull<()> { @@ -674,6 +749,162 @@ def write_anynoderef(out: list[str], ast: Ast) -> None: """) +# ------------------------------------------------------------------------------ +# AnyRootNodeRef + + +def write_root_anynoderef(out: list[str], ast: Ast) -> None: + """ + Create the AnyRootNodeRef type. + + ```rust + pub enum AnyRootNodeRef<'a> { + ... + TypeParam(&'a TypeParam), + ... + } + ``` + + Also creates: + - `impl<'a> From<&'a TypeParam> for AnyRootNodeRef<'a>` + - `impl<'a> TryFrom> for &'a TypeParam` + - `impl<'a> TryFrom> for &'a TypeParamVarTuple` + - `impl Ranged for AnyRootNodeRef<'_>` + - `impl HasNodeIndex for AnyRootNodeRef<'_>` + - `fn AnyRootNodeRef::visit_source_order(self, visitor &mut impl SourceOrderVisitor)` + """ + + out.append(""" + /// An enumeration of all AST nodes. + /// + /// Unlike `AnyNodeRef`, this type does not flatten nested enums, so its variants only + /// consist of the "root" AST node types. This is useful as it exposes references to the + /// original enums, not just references to their inner values. + /// + /// For example, `AnyRootNodeRef::Mod` contains a reference to the `Mod` enum, while + /// `AnyNodeRef` has top-level `AnyNodeRef::ModModule` and `AnyNodeRef::ModExpression` + /// variants. + #[derive(Copy, Clone, Debug, PartialEq)] + #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] + pub enum AnyRootNodeRef<'a> { + """) + for group in ast.groups: + out.append(f"""{group.name}(&'a {group.owned_enum_ty}),""") + for node in ast.ungrouped_nodes: + out.append(f"""{node.name}(&'a {node.ty}),""") + out.append(""" + } + """) + + for group in ast.groups: + out.append(f""" + impl<'a> From<&'a {group.owned_enum_ty}> for AnyRootNodeRef<'a> {{ + fn from(node: &'a {group.owned_enum_ty}) -> AnyRootNodeRef<'a> {{ + AnyRootNodeRef::{group.name}(node) + }} + }} + """) + + out.append(f""" + impl<'a> TryFrom> for &'a {group.owned_enum_ty} {{ + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a {group.owned_enum_ty}, ()> {{ + match node {{ + AnyRootNodeRef::{group.name}(node) => Ok(node), + _ => Err(()) + }} + }} + }} + """) + + for node in group.nodes: + out.append(f""" + impl<'a> TryFrom> for &'a {node.ty} {{ + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a {node.ty}, ()> {{ + match node {{ + AnyRootNodeRef::{group.name}({group.owned_enum_ty}::{node.variant}(node)) => Ok(node), + _ => Err(()) + }} + }} + }} + """) + + for node in ast.ungrouped_nodes: + out.append(f""" + impl<'a> From<&'a {node.ty}> for AnyRootNodeRef<'a> {{ + fn from(node: &'a {node.ty}) -> AnyRootNodeRef<'a> {{ + AnyRootNodeRef::{node.name}(node) + }} + }} + """) + + out.append(f""" + impl<'a> TryFrom> for &'a {node.ty} {{ + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a {node.ty}, ()> {{ + match node {{ + AnyRootNodeRef::{node.name}(node) => Ok(node), + _ => Err(()) + }} + }} + }} + """) + + out.append(""" + impl ruff_text_size::Ranged for AnyRootNodeRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + """) + for group in ast.groups: + out.append(f"""AnyRootNodeRef::{group.name}(node) => node.range(),""") + for node in ast.ungrouped_nodes: + out.append(f"""AnyRootNodeRef::{node.name}(node) => node.range(),""") + out.append(""" + } + } + } + """) + + out.append(""" + impl crate::HasNodeIndex for AnyRootNodeRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + """) + for group in ast.groups: + out.append(f"""AnyRootNodeRef::{group.name}(node) => node.node_index(),""") + for node in ast.ungrouped_nodes: + out.append(f"""AnyRootNodeRef::{node.name}(node) => node.node_index(),""") + out.append(""" + } + } + } + """) + + out.append(""" + impl<'a> AnyRootNodeRef<'a> { + pub fn visit_source_order<'b, V>(self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'b> + ?Sized, + 'a: 'b, + { + match self { + """) + for group in ast.groups: + out.append( + f"""AnyRootNodeRef::{group.name}(node) => node.visit_source_order(visitor),""" + ) + for node in ast.ungrouped_nodes: + out.append( + f"""AnyRootNodeRef::{node.name}(node) => node.visit_source_order(visitor),""" + ) + out.append(""" + } + } + } + """) + + # ------------------------------------------------------------------------------ # NodeKind @@ -736,15 +967,17 @@ def write_node(out: list[str], ast: Ast) -> None: + "".join(f", {derive}" for derive in node.derives) + ")]" ) + out.append('#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]') name = node.name out.append(f"pub struct {name} {{") + out.append("pub node_index: crate::AtomicNodeIndex,") out.append("pub range: ruff_text_size::TextRange,") for field in node.fields: field_str = f"pub {field.name}: " ty = field.parsed_ty rust_ty = f"{field.parsed_ty.name}" - if ty.name in types_requiring_create_prefix: + if ty.name in types_requiring_crate_prefix: rust_ty = f"crate::{rust_ty}" if ty.slice_: rust_ty = f"[{rust_ty}]" @@ -766,39 +999,6 @@ def write_node(out: list[str], ast: Ast) -> None: # Source order visitor -@dataclass -class VisitorInfo: - name: str - accepts_sequence: bool = False - - -# Map of AST node types to their corresponding visitor information -type_to_visitor_function: dict[str, VisitorInfo] = { - "Decorator": VisitorInfo("visit_decorator"), - "Identifier": VisitorInfo("visit_identifier"), - "crate::TypeParams": VisitorInfo("visit_type_params", True), - "crate::Parameters": VisitorInfo("visit_parameters", True), - "Expr": VisitorInfo("visit_expr"), - "Stmt": VisitorInfo("visit_body", True), - "Arguments": VisitorInfo("visit_arguments", True), - "crate::Arguments": VisitorInfo("visit_arguments", True), - "Operator": VisitorInfo("visit_operator"), - "ElifElseClause": VisitorInfo("visit_elif_else_clause"), - "WithItem": VisitorInfo("visit_with_item"), - "MatchCase": VisitorInfo("visit_match_case"), - "ExceptHandler": VisitorInfo("visit_except_handler"), - "Alias": VisitorInfo("visit_alias"), - "UnaryOp": VisitorInfo("visit_unary_op"), - "DictItem": VisitorInfo("visit_dict_item"), - "Comprehension": VisitorInfo("visit_comprehension"), - "CmpOp": VisitorInfo("visit_cmp_op"), - "FStringValue": VisitorInfo("visit_f_string_value"), - "StringLiteralValue": VisitorInfo("visit_string_literal"), - "BytesLiteralValue": VisitorInfo("visit_bytes_literal"), -} -annotation_visitor_function = VisitorInfo("visit_annotation") - - def write_source_order(out: list[str], ast: Ast) -> None: for group in ast.groups: for node in group.nodes: @@ -814,26 +1014,36 @@ def write_source_order(out: list[str], ast: Ast) -> None: else: fields_list += f"{field.name},\n" fields_list += "range: _,\n" + fields_list += "node_index: _,\n" for field in node.fields_in_source_order(): - visitor = type_to_visitor_function[field.parsed_ty.inner] + visitor_name = ( + type_to_visitor_function.get( + field.parsed_ty.inner, VisitorInfo("") + ).name + or f"visit_{to_snake_case(field.parsed_ty.inner)}" + ) + visits_sequence = type_to_visitor_function.get( + field.parsed_ty.inner, VisitorInfo("") + ).accepts_sequence + if field.is_annotation: - visitor = annotation_visitor_function + visitor_name = "visit_annotation" if field.parsed_ty.optional: body += f""" if let Some({field.name}) = {field.name} {{ - visitor.{visitor.name}({field.name}); + visitor.{visitor_name}({field.name}); }}\n """ - elif not visitor.accepts_sequence and field.parsed_ty.seq: + elif not visits_sequence and field.parsed_ty.seq: body += f""" for elm in {field.name} {{ - visitor.{visitor.name}(elm); + visitor.{visitor_name}(elm); }} """ else: - body += f"visitor.{visitor.name}({field.name});\n" + body += f"visitor.{visitor_name}({field.name});\n" visitor_arg_name = "visitor" if len(node.fields_in_source_order()) == 0: @@ -864,6 +1074,7 @@ def generate(ast: Ast) -> list[str]: write_owned_enum(out, ast) write_ref_enum(out, ast) write_anynoderef(out, ast) + write_root_anynoderef(out, ast) write_nodekind(out, ast) write_node(out, ast) write_source_order(out, ast) diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 8dd4c0dd85a9c..170e20ebc75ea 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -226,7 +226,6 @@ pub struct PatternMatchOr<'a> { patterns: Vec>, } -#[allow(clippy::enum_variant_names)] #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparablePattern<'a> { MatchValue(PatternMatchValue<'a>), @@ -513,48 +512,58 @@ impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ComparableFStringElement<'a> { +pub enum ComparableInterpolatedStringElement<'a> { Literal(Cow<'a, str>), - FStringExpressionElement(FStringExpressionElement<'a>), + InterpolatedElement(InterpolatedElement<'a>), } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct FStringExpressionElement<'a> { +pub struct InterpolatedElement<'a> { expression: ComparableExpr<'a>, debug_text: Option<&'a ast::DebugText>, conversion: ast::ConversionFlag, - format_spec: Option>>, + format_spec: Option>>, } -impl<'a> From<&'a ast::FStringElement> for ComparableFStringElement<'a> { - fn from(fstring_element: &'a ast::FStringElement) -> Self { - match fstring_element { - ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => { - Self::Literal(value.as_ref().into()) +impl<'a> From<&'a ast::InterpolatedStringElement> for ComparableInterpolatedStringElement<'a> { + fn from(interpolated_string_element: &'a ast::InterpolatedStringElement) -> Self { + match interpolated_string_element { + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { + value, + .. + }) => Self::Literal(value.as_ref().into()), + ast::InterpolatedStringElement::Interpolation(formatted_value) => { + formatted_value.into() } - ast::FStringElement::Expression(formatted_value) => formatted_value.into(), } } } -impl<'a> From<&'a ast::FStringExpressionElement> for ComparableFStringElement<'a> { - fn from(fstring_expression_element: &'a ast::FStringExpressionElement) -> Self { - let ast::FStringExpressionElement { +impl<'a> From<&'a ast::InterpolatedElement> for InterpolatedElement<'a> { + fn from(interpolated_element: &'a ast::InterpolatedElement) -> Self { + let ast::InterpolatedElement { expression, debug_text, conversion, format_spec, range: _, - } = fstring_expression_element; + node_index: _, + } = interpolated_element; - Self::FStringExpressionElement(FStringExpressionElement { + Self { expression: (expression).into(), debug_text: debug_text.as_ref(), conversion: *conversion, format_spec: format_spec .as_ref() .map(|spec| spec.elements.iter().map(Into::into).collect()), - }) + } + } +} + +impl<'a> From<&'a ast::InterpolatedElement> for ComparableInterpolatedStringElement<'a> { + fn from(interpolated_element: &'a ast::InterpolatedElement) -> Self { + Self::InterpolatedElement(interpolated_element.into()) } } @@ -568,6 +577,7 @@ impl<'a> From<&'a ast::ElifElseClause> for ComparableElifElseClause<'a> { fn from(elif_else_clause: &'a ast::ElifElseClause) -> Self { let ast::ElifElseClause { range: _, + node_index: _, test, body, } = elif_else_clause; @@ -611,7 +621,7 @@ impl<'a> From> for ComparableLiteral<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableFString<'a> { - elements: Box<[ComparableFStringElement<'a>]>, + elements: Box<[ComparableInterpolatedStringElement<'a>]>, } impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { @@ -638,7 +648,7 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { fn from(value: &'a ast::FStringValue) -> Self { #[derive(Default)] struct Collector<'a> { - elements: Vec>, + elements: Vec>, } impl<'a> Collector<'a> { @@ -648,17 +658,17 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { // `elements` vector, while subsequent strings // are concatenated onto this top string. fn push_literal(&mut self, literal: &'a str) { - if let Some(ComparableFStringElement::Literal(existing_literal)) = + if let Some(ComparableInterpolatedStringElement::Literal(existing_literal)) = self.elements.last_mut() { existing_literal.to_mut().push_str(literal); } else { self.elements - .push(ComparableFStringElement::Literal(literal.into())); + .push(ComparableInterpolatedStringElement::Literal(literal.into())); } } - fn push_expression(&mut self, expression: &'a ast::FStringExpressionElement) { + fn push_expression(&mut self, expression: &'a ast::InterpolatedElement) { self.elements.push(expression.into()); } } @@ -673,10 +683,10 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { ast::FStringPart::FString(fstring) => { for element in &fstring.elements { match element { - ast::FStringElement::Literal(literal) => { + ast::InterpolatedStringElement::Literal(literal) => { collector.push_literal(&literal.value); } - ast::FStringElement::Expression(expression) => { + ast::InterpolatedStringElement::Interpolation(expression) => { collector.push_expression(expression); } } @@ -691,6 +701,133 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ComparableTString<'a> { + strings: Box<[ComparableInterpolatedStringElement<'a>]>, + interpolations: Box<[InterpolatedElement<'a>]>, +} + +impl<'a> From<&'a ast::TStringValue> for ComparableTString<'a> { + // The approach taken below necessarily deviates from the + // corresponding implementation for [`ast::FStringValue`]. + // The reason is that a t-string value is composed of _three_ + // non-comparable parts: literals, f-string expressions, and + // t-string interpolations. Since we have merged the AST nodes + // that capture f-string expressions and t-string interpolations + // into the shared [`ast::InterpolatedElement`], we must + // be careful to distinguish between them here. + // + // Consequently, we model a [`ComparableTString`] on the actual + // [CPython implementation] of a `string.templatelib.Template` object: + // it is composed of `strings` and `interpolations`. In CPython, + // the `strings` field is a tuple of honest strings (since f-strings + // are evaluated). Our `strings` field will house both f-string + // expressions and string literals. + // + // Finally, as in CPython, we must be careful to ensure that the length + // of `strings` is always one more than the length of `interpolations` - + // that way we can recover the original reading order by interleaving + // starting with `strings`. This is how we can tell the + // difference between, e.g. `t"{foo}bar"` and `t"bar{foo}"`. + // + // - [CPython implementation](https://github.com/python/cpython/blob/c91ad5da9d92eac4718e4da8d53689c3cc24535e/Python/codegen.c#L4052-L4103) + fn from(value: &'a ast::TStringValue) -> Self { + struct Collector<'a> { + strings: Vec>, + interpolations: Vec>, + } + + impl Default for Collector<'_> { + fn default() -> Self { + Self { + strings: vec![ComparableInterpolatedStringElement::Literal("".into())], + interpolations: vec![], + } + } + } + + impl<'a> Collector<'a> { + // The logic for concatenating adjacent string literals + // occurs here, implicitly: when we encounter a sequence + // of string literals, the first gets pushed to the + // `strings` vector, while subsequent strings + // are concatenated onto this top string. + fn push_literal(&mut self, literal: &'a str) { + if let Some(ComparableInterpolatedStringElement::Literal(existing_literal)) = + self.strings.last_mut() + { + existing_literal.to_mut().push_str(literal); + } else { + self.strings + .push(ComparableInterpolatedStringElement::Literal(literal.into())); + } + } + + fn start_new_literal(&mut self) { + self.strings + .push(ComparableInterpolatedStringElement::Literal("".into())); + } + + fn push_fstring_expression(&mut self, expression: &'a ast::InterpolatedElement) { + if let Some(ComparableInterpolatedStringElement::Literal(last_literal)) = + self.strings.last() + { + // Recall that we insert empty strings after + // each interpolation. If we encounter an f-string + // expression, we replace the empty string with it. + if last_literal.is_empty() { + self.strings.pop(); + } + } + self.strings.push(expression.into()); + } + fn push_tstring_interpolation(&mut self, expression: &'a ast::InterpolatedElement) { + self.interpolations.push(expression.into()); + self.start_new_literal(); + } + } + + let mut collector = Collector::default(); + + for part in value { + match part { + ast::TStringPart::Literal(string_literal) => { + collector.push_literal(&string_literal.value); + } + ast::TStringPart::TString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Literal(literal) => { + collector.push_literal(&literal.value); + } + ast::InterpolatedStringElement::Interpolation(interpolation) => { + collector.push_tstring_interpolation(interpolation); + } + } + } + } + ast::TStringPart::FString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Literal(literal) => { + collector.push_literal(&literal.value); + } + ast::InterpolatedStringElement::Interpolation(expression) => { + collector.push_fstring_expression(expression); + } + } + } + } + } + } + + Self { + strings: collector.strings.into_boxed_slice(), + interpolations: collector.interpolations.into_boxed_slice(), + } + } +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableStringLiteral<'a> { value: &'a str, @@ -834,11 +971,11 @@ pub struct ExprCall<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ExprFStringExpressionElement<'a> { +pub struct ExprInterpolatedElement<'a> { value: Box>, debug_text: Option<&'a ast::DebugText>, conversion: ast::ConversionFlag, - format_spec: Vec>, + format_spec: Vec>, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -846,6 +983,11 @@ pub struct ExprFString<'a> { value: ComparableFString<'a>, } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprTString<'a> { + value: ComparableTString<'a>, +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprStringLiteral<'a> { value: ComparableStringLiteral<'a>, @@ -930,8 +1072,10 @@ pub enum ComparableExpr<'a> { YieldFrom(ExprYieldFrom<'a>), Compare(ExprCompare<'a>), Call(ExprCall<'a>), - FStringExpressionElement(ExprFStringExpressionElement<'a>), + FStringExpressionElement(ExprInterpolatedElement<'a>), FString(ExprFString<'a>), + TStringInterpolationElement(ExprInterpolatedElement<'a>), + TString(ExprTString<'a>), StringLiteral(ExprStringLiteral<'a>), BytesLiteral(ExprBytesLiteral<'a>), NumberLiteral(ExprNumberLiteral<'a>), @@ -967,6 +1111,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { op, values, range: _, + node_index: _, }) => Self::BoolOp(ExprBoolOp { op: (*op).into(), values: values.iter().map(Into::into).collect(), @@ -975,6 +1120,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { target, value, range: _, + node_index: _, }) => Self::NamedExpr(ExprNamed { target: target.into(), value: value.into(), @@ -984,6 +1130,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { op, right, range: _, + node_index: _, }) => Self::BinOp(ExprBinOp { left: left.into(), op: (*op).into(), @@ -993,6 +1140,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { op, operand, range: _, + node_index: _, }) => Self::UnaryOp(ExprUnaryOp { op: (*op).into(), operand: operand.into(), @@ -1001,6 +1149,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { parameters, body, range: _, + node_index: _, }) => Self::Lambda(ExprLambda { parameters: parameters.as_ref().map(Into::into), body: body.into(), @@ -1010,21 +1159,31 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { body, orelse, range: _, + node_index: _, }) => Self::IfExp(ExprIf { test: test.into(), body: body.into(), orelse: orelse.into(), }), - ast::Expr::Dict(ast::ExprDict { items, range: _ }) => Self::Dict(ExprDict { + ast::Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) => Self::Dict(ExprDict { items: items.iter().map(ComparableDictItem::from).collect(), }), - ast::Expr::Set(ast::ExprSet { elts, range: _ }) => Self::Set(ExprSet { + ast::Expr::Set(ast::ExprSet { + elts, + range: _, + node_index: _, + }) => Self::Set(ExprSet { elts: elts.iter().map(Into::into).collect(), }), ast::Expr::ListComp(ast::ExprListComp { elt, generators, range: _, + node_index: _, }) => Self::ListComp(ExprListComp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), @@ -1033,6 +1192,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { elt, generators, range: _, + node_index: _, }) => Self::SetComp(ExprSetComp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), @@ -1042,6 +1202,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { value, generators, range: _, + node_index: _, }) => Self::DictComp(ExprDictComp { key: key.into(), value: value.into(), @@ -1051,27 +1212,39 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { elt, generators, range: _, + node_index: _, parenthesized: _, }) => Self::GeneratorExp(ExprGenerator { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), }), - ast::Expr::Await(ast::ExprAwait { value, range: _ }) => Self::Await(ExprAwait { + ast::Expr::Await(ast::ExprAwait { + value, + range: _, + node_index: _, + }) => Self::Await(ExprAwait { value: value.into(), }), - ast::Expr::Yield(ast::ExprYield { value, range: _ }) => Self::Yield(ExprYield { + ast::Expr::Yield(ast::ExprYield { + value, + range: _, + node_index: _, + }) => Self::Yield(ExprYield { value: value.as_ref().map(Into::into), }), - ast::Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) => { - Self::YieldFrom(ExprYieldFrom { - value: value.into(), - }) - } + ast::Expr::YieldFrom(ast::ExprYieldFrom { + value, + range: _, + node_index: _, + }) => Self::YieldFrom(ExprYieldFrom { + value: value.into(), + }), ast::Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _, + node_index: _, }) => Self::Compare(ExprCompare { left: left.into(), ops: ops.iter().copied().map(Into::into).collect(), @@ -1081,37 +1254,55 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { func, arguments, range: _, + node_index: _, }) => Self::Call(ExprCall { func: func.into(), arguments: arguments.into(), }), - ast::Expr::FString(ast::ExprFString { value, range: _ }) => { - Self::FString(ExprFString { - value: value.into(), - }) - } - ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => { - Self::StringLiteral(ExprStringLiteral { - value: ComparableStringLiteral { - value: value.to_str(), - }, - }) - } - ast::Expr::BytesLiteral(ast::ExprBytesLiteral { value, range: _ }) => { - Self::BytesLiteral(ExprBytesLiteral { - value: ComparableBytesLiteral { - value: Cow::from(value), - }, - }) - } - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, range: _ }) => { - Self::NumberLiteral(ExprNumberLiteral { - value: value.into(), - }) - } - ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, range: _ }) => { - Self::BoolLiteral(ExprBoolLiteral { value: *value }) - } + ast::Expr::FString(ast::ExprFString { + value, + range: _, + node_index: _, + }) => Self::FString(ExprFString { + value: value.into(), + }), + ast::Expr::TString(ast::ExprTString { + value, + range: _, + node_index: _, + }) => Self::TString(ExprTString { + value: value.into(), + }), + ast::Expr::StringLiteral(ast::ExprStringLiteral { + value, + range: _, + node_index: _, + }) => Self::StringLiteral(ExprStringLiteral { + value: ComparableStringLiteral { + value: value.to_str(), + }, + }), + ast::Expr::BytesLiteral(ast::ExprBytesLiteral { + value, + range: _, + node_index: _, + }) => Self::BytesLiteral(ExprBytesLiteral { + value: ComparableBytesLiteral { + value: Cow::from(value), + }, + }), + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value, + range: _, + node_index: _, + }) => Self::NumberLiteral(ExprNumberLiteral { + value: value.into(), + }), + ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value, + range: _, + node_index: _, + }) => Self::BoolLiteral(ExprBoolLiteral { value: *value }), ast::Expr::NoneLiteral(_) => Self::NoneLiteral, ast::Expr::EllipsisLiteral(_) => Self::EllipsisLiteral, ast::Expr::Attribute(ast::ExprAttribute { @@ -1119,6 +1310,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { attr, ctx: _, range: _, + node_index: _, }) => Self::Attribute(ExprAttribute { value: value.into(), attr: attr.as_str(), @@ -1128,6 +1320,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { slice, ctx: _, range: _, + node_index: _, }) => Self::Subscript(ExprSubscript { value: value.into(), slice: slice.into(), @@ -1136,6 +1329,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { value, ctx: _, range: _, + node_index: _, }) => Self::Starred(ExprStarred { value: value.into(), }), @@ -1144,6 +1338,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { elts, ctx: _, range: _, + node_index: _, }) => Self::List(ExprList { elts: elts.iter().map(Into::into).collect(), }), @@ -1151,6 +1346,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { elts, ctx: _, range: _, + node_index: _, parenthesized: _, }) => Self::Tuple(ExprTuple { elts: elts.iter().map(Into::into).collect(), @@ -1160,6 +1356,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { upper, step, range: _, + node_index: _, }) => Self::Slice(ExprSlice { lower: lower.as_ref().map(Into::into), upper: upper.as_ref().map(Into::into), @@ -1169,6 +1366,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { kind, value, range: _, + node_index: _, }) => Self::IpyEscapeCommand(ExprIpyEscapeCommand { kind: *kind, value }), } } @@ -1253,6 +1451,7 @@ impl<'a> From<&'a ast::TypeParam> for ComparableTypeParam<'a> { bound, default, range: _, + node_index: _, }) => Self::TypeVar(TypeParamTypeVar { name: name.as_str(), bound: bound.as_ref().map(Into::into), @@ -1262,6 +1461,7 @@ impl<'a> From<&'a ast::TypeParam> for ComparableTypeParam<'a> { name, default, range: _, + node_index: _, }) => Self::TypeVarTuple(TypeParamTypeVarTuple { name: name.as_str(), default: default.as_ref().map(Into::into), @@ -1270,6 +1470,7 @@ impl<'a> From<&'a ast::TypeParam> for ComparableTypeParam<'a> { name, default, range: _, + node_index: _, }) => Self::ParamSpec(TypeParamParamSpec { name: name.as_str(), default: default.as_ref().map(Into::into), @@ -1449,6 +1650,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { returns, type_params, range: _, + node_index: _, }) => Self::FunctionDef(StmtFunctionDef { is_async: *is_async, name: name.as_str(), @@ -1465,6 +1667,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { decorator_list, type_params, range: _, + node_index: _, }) => Self::ClassDef(StmtClassDef { name: name.as_str(), arguments: arguments.as_ref().map(Into::into).unwrap_or_default(), @@ -1472,14 +1675,23 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { decorator_list: decorator_list.iter().map(Into::into).collect(), type_params: type_params.as_ref().map(Into::into), }), - ast::Stmt::Return(ast::StmtReturn { value, range: _ }) => Self::Return(StmtReturn { + ast::Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => Self::Return(StmtReturn { value: value.as_ref().map(Into::into), }), - ast::Stmt::Delete(ast::StmtDelete { targets, range: _ }) => Self::Delete(StmtDelete { + ast::Stmt::Delete(ast::StmtDelete { + targets, + range: _, + node_index: _, + }) => Self::Delete(StmtDelete { targets: targets.iter().map(Into::into).collect(), }), ast::Stmt::TypeAlias(ast::StmtTypeAlias { range: _, + node_index: _, name, type_params, value, @@ -1492,6 +1704,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { targets, value, range: _, + node_index: _, }) => Self::Assign(StmtAssign { targets: targets.iter().map(Into::into).collect(), value: value.into(), @@ -1501,6 +1714,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { op, value, range: _, + node_index: _, }) => Self::AugAssign(StmtAugAssign { target: target.into(), op: (*op).into(), @@ -1512,6 +1726,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { value, simple, range: _, + node_index: _, }) => Self::AnnAssign(StmtAnnAssign { target: target.into(), annotation: annotation.into(), @@ -1525,6 +1740,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { body, orelse, range: _, + node_index: _, }) => Self::For(StmtFor { is_async: *is_async, target: target.into(), @@ -1537,6 +1753,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { body, orelse, range: _, + node_index: _, }) => Self::While(StmtWhile { test: test.into(), body: body.iter().map(Into::into).collect(), @@ -1547,6 +1764,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { body, elif_else_clauses, range: _, + node_index: _, }) => Self::If(StmtIf { test: test.into(), body: body.iter().map(Into::into).collect(), @@ -1557,6 +1775,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { items, body, range: _, + node_index: _, }) => Self::With(StmtWith { is_async: *is_async, items: items.iter().map(Into::into).collect(), @@ -1566,6 +1785,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { subject, cases, range: _, + node_index: _, }) => Self::Match(StmtMatch { subject: subject.into(), cases: cases.iter().map(Into::into).collect(), @@ -1574,6 +1794,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { exc, cause, range: _, + node_index: _, }) => Self::Raise(StmtRaise { exc: exc.as_ref().map(Into::into), cause: cause.as_ref().map(Into::into), @@ -1585,6 +1806,7 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { finalbody, is_star, range: _, + node_index: _, }) => Self::Try(StmtTry { body: body.iter().map(Into::into).collect(), handlers: handlers.iter().map(Into::into).collect(), @@ -1596,11 +1818,16 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { test, msg, range: _, + node_index: _, }) => Self::Assert(StmtAssert { test: test.into(), msg: msg.as_ref().map(Into::into), }), - ast::Stmt::Import(ast::StmtImport { names, range: _ }) => Self::Import(StmtImport { + ast::Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => Self::Import(StmtImport { names: names.iter().map(Into::into).collect(), }), ast::Stmt::ImportFrom(ast::StmtImportFrom { @@ -1608,25 +1835,37 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { names, level, range: _, + node_index: _, }) => Self::ImportFrom(StmtImportFrom { module: module.as_deref(), names: names.iter().map(Into::into).collect(), level: *level, }), - ast::Stmt::Global(ast::StmtGlobal { names, range: _ }) => Self::Global(StmtGlobal { + ast::Stmt::Global(ast::StmtGlobal { + names, + range: _, + node_index: _, + }) => Self::Global(StmtGlobal { + names: names.iter().map(ast::Identifier::as_str).collect(), + }), + ast::Stmt::Nonlocal(ast::StmtNonlocal { + names, + range: _, + node_index: _, + }) => Self::Nonlocal(StmtNonlocal { names: names.iter().map(ast::Identifier::as_str).collect(), }), - ast::Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { - Self::Nonlocal(StmtNonlocal { - names: names.iter().map(ast::Identifier::as_str).collect(), - }) - } ast::Stmt::IpyEscapeCommand(ast::StmtIpyEscapeCommand { kind, value, range: _, + node_index: _, }) => Self::IpyEscapeCommand(StmtIpyEscapeCommand { kind: *kind, value }), - ast::Stmt::Expr(ast::StmtExpr { value, range: _ }) => Self::Expr(StmtExpr { + ast::Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => Self::Expr(StmtExpr { value: value.into(), }), ast::Stmt::Pass(_) => Self::Pass, diff --git a/crates/ruff_python_ast/src/expression.rs b/crates/ruff_python_ast/src/expression.rs index 48a0342971e5b..14fe4e1170103 100644 --- a/crates/ruff_python_ast/src/expression.rs +++ b/crates/ruff_python_ast/src/expression.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::{ self as ast, AnyNodeRef, AnyStringFlags, Expr, ExprBytesLiteral, ExprFString, ExprRef, - ExprStringLiteral, StringFlags, + ExprStringLiteral, ExprTString, StringFlags, }; impl<'a> From<&'a Box> for ExprRef<'a> { @@ -80,17 +80,18 @@ impl LiteralExpressionRef<'_> { } /// An enum that holds a reference to a string-like expression from the AST. This includes string -/// literals, bytes literals, and f-strings. +/// literals, bytes literals, f-strings, and t-strings. #[derive(Copy, Clone, Debug, PartialEq)] pub enum StringLike<'a> { String(&'a ast::ExprStringLiteral), Bytes(&'a ast::ExprBytesLiteral), FString(&'a ast::ExprFString), + TString(&'a ast::ExprTString), } impl<'a> StringLike<'a> { - pub const fn is_fstring(self) -> bool { - matches!(self, Self::FString(_)) + pub const fn is_interpolated_string(self) -> bool { + matches!(self, Self::TString(_) | Self::FString(_)) } /// Returns an iterator over the [`StringLikePart`] contained in this string-like expression. @@ -99,6 +100,7 @@ impl<'a> StringLike<'a> { StringLike::String(expr) => StringLikePartIter::String(expr.value.iter()), StringLike::Bytes(expr) => StringLikePartIter::Bytes(expr.value.iter()), StringLike::FString(expr) => StringLikePartIter::FString(expr.value.iter()), + StringLike::TString(expr) => StringLikePartIter::TString(expr.value.iter()), } } @@ -108,6 +110,7 @@ impl<'a> StringLike<'a> { Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(), Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(), Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(), + Self::TString(ExprTString { value, .. }) => value.is_implicit_concatenated(), } } @@ -116,6 +119,7 @@ impl<'a> StringLike<'a> { StringLike::String(expr) => ExprRef::StringLiteral(expr), StringLike::Bytes(expr) => ExprRef::BytesLiteral(expr), StringLike::FString(expr) => ExprRef::FString(expr), + StringLike::TString(expr) => ExprRef::TString(expr), } } } @@ -138,12 +142,19 @@ impl<'a> From<&'a ast::ExprFString> for StringLike<'a> { } } +impl<'a> From<&'a ast::ExprTString> for StringLike<'a> { + fn from(value: &'a ast::ExprTString) -> Self { + StringLike::TString(value) + } +} + impl<'a> From<&StringLike<'a>> for ExprRef<'a> { fn from(value: &StringLike<'a>) -> Self { match value { StringLike::String(expr) => ExprRef::StringLiteral(expr), StringLike::Bytes(expr) => ExprRef::BytesLiteral(expr), StringLike::FString(expr) => ExprRef::FString(expr), + StringLike::TString(expr) => ExprRef::TString(expr), } } } @@ -160,6 +171,7 @@ impl<'a> From<&StringLike<'a>> for AnyNodeRef<'a> { StringLike::String(expr) => AnyNodeRef::ExprStringLiteral(expr), StringLike::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr), StringLike::FString(expr) => AnyNodeRef::ExprFString(expr), + StringLike::TString(expr) => AnyNodeRef::ExprTString(expr), } } } @@ -172,6 +184,7 @@ impl<'a> TryFrom<&'a Expr> for StringLike<'a> { Expr::StringLiteral(value) => Ok(Self::String(value)), Expr::BytesLiteral(value) => Ok(Self::Bytes(value)), Expr::FString(value) => Ok(Self::FString(value)), + Expr::TString(value) => Ok(Self::TString(value)), _ => Err(()), } } @@ -185,6 +198,7 @@ impl<'a> TryFrom> for StringLike<'a> { AnyNodeRef::ExprStringLiteral(value) => Ok(Self::String(value)), AnyNodeRef::ExprBytesLiteral(value) => Ok(Self::Bytes(value)), AnyNodeRef::ExprFString(value) => Ok(Self::FString(value)), + AnyNodeRef::ExprTString(value) => Ok(Self::TString(value)), _ => Err(()), } } @@ -196,6 +210,7 @@ impl Ranged for StringLike<'_> { StringLike::String(literal) => literal.range(), StringLike::Bytes(literal) => literal.range(), StringLike::FString(literal) => literal.range(), + StringLike::TString(literal) => literal.range(), } } } @@ -206,6 +221,7 @@ pub enum StringLikePart<'a> { String(&'a ast::StringLiteral), Bytes(&'a ast::BytesLiteral), FString(&'a ast::FString), + TString(&'a ast::TString), } impl<'a> StringLikePart<'a> { @@ -215,6 +231,7 @@ impl<'a> StringLikePart<'a> { StringLikePart::String(string) => AnyStringFlags::from(string.flags), StringLikePart::Bytes(bytes) => AnyStringFlags::from(bytes.flags), StringLikePart::FString(f_string) => AnyStringFlags::from(f_string.flags), + StringLikePart::TString(t_string) => AnyStringFlags::from(t_string.flags), } } @@ -238,8 +255,8 @@ impl<'a> StringLikePart<'a> { } } - pub const fn is_fstring(self) -> bool { - matches!(self, Self::FString(_)) + pub const fn is_interpolated_string(self) -> bool { + matches!(self, Self::FString(_) | Self::TString(_)) } } @@ -261,6 +278,12 @@ impl<'a> From<&'a ast::FString> for StringLikePart<'a> { } } +impl<'a> From<&'a ast::TString> for StringLikePart<'a> { + fn from(value: &'a ast::TString) -> Self { + StringLikePart::TString(value) + } +} + impl<'a> From<&StringLikePart<'a>> for AnyNodeRef<'a> { fn from(value: &StringLikePart<'a>) -> Self { AnyNodeRef::from(*value) @@ -273,6 +296,7 @@ impl<'a> From> for AnyNodeRef<'a> { StringLikePart::String(part) => AnyNodeRef::StringLiteral(part), StringLikePart::Bytes(part) => AnyNodeRef::BytesLiteral(part), StringLikePart::FString(part) => AnyNodeRef::FString(part), + StringLikePart::TString(part) => AnyNodeRef::TString(part), } } } @@ -283,6 +307,7 @@ impl Ranged for StringLikePart<'_> { StringLikePart::String(part) => part.range(), StringLikePart::Bytes(part) => part.range(), StringLikePart::FString(part) => part.range(), + StringLikePart::TString(part) => part.range(), } } } @@ -295,6 +320,7 @@ pub enum StringLikePartIter<'a> { String(std::slice::Iter<'a, ast::StringLiteral>), Bytes(std::slice::Iter<'a, ast::BytesLiteral>), FString(std::slice::Iter<'a, ast::FStringPart>), + TString(std::slice::Iter<'a, ast::TStringPart>), } impl<'a> Iterator for StringLikePartIter<'a> { @@ -313,6 +339,16 @@ impl<'a> Iterator for StringLikePartIter<'a> { ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string), } } + StringLikePartIter::TString(inner) => { + let part = inner.next()?; + match part { + ast::TStringPart::Literal(string_literal) => { + StringLikePart::String(string_literal) + } + ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string), + ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string), + } + } }; Some(part) @@ -323,6 +359,7 @@ impl<'a> Iterator for StringLikePartIter<'a> { StringLikePartIter::String(inner) => inner.size_hint(), StringLikePartIter::Bytes(inner) => inner.size_hint(), StringLikePartIter::FString(inner) => inner.size_hint(), + StringLikePartIter::TString(inner) => inner.size_hint(), } } } @@ -341,6 +378,16 @@ impl DoubleEndedIterator for StringLikePartIter<'_> { ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string), } } + StringLikePartIter::TString(inner) => { + let part = inner.next_back()?; + match part { + ast::TStringPart::Literal(string_literal) => { + StringLikePart::String(string_literal) + } + ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string), + ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string), + } + } }; Some(part) diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 166878d973d32..935595705dd9c 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -6,6 +6,7 @@ use crate::visitor::source_order::SourceOrderVisitor; /// See also [mod](https://docs.python.org/3/library/ast.html#ast.mod) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Mod { Module(crate::ModModule), Expression(crate::ModExpression), @@ -32,6 +33,15 @@ impl ruff_text_size::Ranged for Mod { } } +impl crate::HasNodeIndex for Mod { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::Module(node) => node.node_index(), + Self::Expression(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] impl Mod { #[inline] @@ -111,6 +121,7 @@ impl Mod { /// See also [stmt](https://docs.python.org/3/library/ast.html#ast.stmt) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Stmt { FunctionDef(crate::StmtFunctionDef), ClassDef(crate::StmtClassDef), @@ -321,6 +332,38 @@ impl ruff_text_size::Ranged for Stmt { } } +impl crate::HasNodeIndex for Stmt { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::FunctionDef(node) => node.node_index(), + Self::ClassDef(node) => node.node_index(), + Self::Return(node) => node.node_index(), + Self::Delete(node) => node.node_index(), + Self::TypeAlias(node) => node.node_index(), + Self::Assign(node) => node.node_index(), + Self::AugAssign(node) => node.node_index(), + Self::AnnAssign(node) => node.node_index(), + Self::For(node) => node.node_index(), + Self::While(node) => node.node_index(), + Self::If(node) => node.node_index(), + Self::With(node) => node.node_index(), + Self::Match(node) => node.node_index(), + Self::Raise(node) => node.node_index(), + Self::Try(node) => node.node_index(), + Self::Assert(node) => node.node_index(), + Self::Import(node) => node.node_index(), + Self::ImportFrom(node) => node.node_index(), + Self::Global(node) => node.node_index(), + Self::Nonlocal(node) => node.node_index(), + Self::Expr(node) => node.node_index(), + Self::Pass(node) => node.node_index(), + Self::Break(node) => node.node_index(), + Self::Continue(node) => node.node_index(), + Self::IpyEscapeCommand(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] impl Stmt { #[inline] @@ -1251,6 +1294,7 @@ impl Stmt { /// See also [expr](https://docs.python.org/3/library/ast.html#ast.expr) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Expr { BoolOp(crate::ExprBoolOp), Named(crate::ExprNamed), @@ -1270,6 +1314,7 @@ pub enum Expr { Compare(crate::ExprCompare), Call(crate::ExprCall), FString(crate::ExprFString), + TString(crate::ExprTString), StringLiteral(crate::ExprStringLiteral), BytesLiteral(crate::ExprBytesLiteral), NumberLiteral(crate::ExprNumberLiteral), @@ -1394,6 +1439,12 @@ impl From for Expr { } } +impl From for Expr { + fn from(node: crate::ExprTString) -> Self { + Self::TString(node) + } +} + impl From for Expr { fn from(node: crate::ExprStringLiteral) -> Self { Self::StringLiteral(node) @@ -1499,6 +1550,7 @@ impl ruff_text_size::Ranged for Expr { Self::Compare(node) => node.range(), Self::Call(node) => node.range(), Self::FString(node) => node.range(), + Self::TString(node) => node.range(), Self::StringLiteral(node) => node.range(), Self::BytesLiteral(node) => node.range(), Self::NumberLiteral(node) => node.range(), @@ -1517,6 +1569,46 @@ impl ruff_text_size::Ranged for Expr { } } +impl crate::HasNodeIndex for Expr { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::BoolOp(node) => node.node_index(), + Self::Named(node) => node.node_index(), + Self::BinOp(node) => node.node_index(), + Self::UnaryOp(node) => node.node_index(), + Self::Lambda(node) => node.node_index(), + Self::If(node) => node.node_index(), + Self::Dict(node) => node.node_index(), + Self::Set(node) => node.node_index(), + Self::ListComp(node) => node.node_index(), + Self::SetComp(node) => node.node_index(), + Self::DictComp(node) => node.node_index(), + Self::Generator(node) => node.node_index(), + Self::Await(node) => node.node_index(), + Self::Yield(node) => node.node_index(), + Self::YieldFrom(node) => node.node_index(), + Self::Compare(node) => node.node_index(), + Self::Call(node) => node.node_index(), + Self::FString(node) => node.node_index(), + Self::TString(node) => node.node_index(), + Self::StringLiteral(node) => node.node_index(), + Self::BytesLiteral(node) => node.node_index(), + Self::NumberLiteral(node) => node.node_index(), + Self::BooleanLiteral(node) => node.node_index(), + Self::NoneLiteral(node) => node.node_index(), + Self::EllipsisLiteral(node) => node.node_index(), + Self::Attribute(node) => node.node_index(), + Self::Subscript(node) => node.node_index(), + Self::Starred(node) => node.node_index(), + Self::Name(node) => node.node_index(), + Self::List(node) => node.node_index(), + Self::Tuple(node) => node.node_index(), + Self::Slice(node) => node.node_index(), + Self::IpyEscapeCommand(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] impl Expr { #[inline] @@ -2185,6 +2277,43 @@ impl Expr { } } + #[inline] + pub const fn is_t_string_expr(&self) -> bool { + matches!(self, Self::TString(_)) + } + + #[inline] + pub fn t_string_expr(self) -> Option { + match self { + Self::TString(val) => Some(val), + _ => None, + } + } + + #[inline] + pub fn expect_t_string_expr(self) -> crate::ExprTString { + match self { + Self::TString(val) => val, + _ => panic!("called expect on {self:?}"), + } + } + + #[inline] + pub fn as_t_string_expr_mut(&mut self) -> Option<&mut crate::ExprTString> { + match self { + Self::TString(val) => Some(val), + _ => None, + } + } + + #[inline] + pub fn as_t_string_expr(&self) -> Option<&crate::ExprTString> { + match self { + Self::TString(val) => Some(val), + _ => None, + } + } + #[inline] pub const fn is_string_literal_expr(&self) -> bool { matches!(self, Self::StringLiteral(_)) @@ -2706,6 +2835,7 @@ impl Expr { /// See also [excepthandler](https://docs.python.org/3/library/ast.html#ast.excepthandler) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum ExceptHandler { ExceptHandler(crate::ExceptHandlerExceptHandler), } @@ -2724,6 +2854,14 @@ impl ruff_text_size::Ranged for ExceptHandler { } } +impl crate::HasNodeIndex for ExceptHandler { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::ExceptHandler(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] impl ExceptHandler { #[inline] @@ -2761,67 +2899,77 @@ impl ExceptHandler { } #[derive(Clone, Debug, PartialEq)] -pub enum FStringElement { - Expression(crate::FStringExpressionElement), - Literal(crate::FStringLiteralElement), +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum InterpolatedStringElement { + Interpolation(crate::InterpolatedElement), + Literal(crate::InterpolatedStringLiteralElement), } -impl From for FStringElement { - fn from(node: crate::FStringExpressionElement) -> Self { - Self::Expression(node) +impl From for InterpolatedStringElement { + fn from(node: crate::InterpolatedElement) -> Self { + Self::Interpolation(node) } } -impl From for FStringElement { - fn from(node: crate::FStringLiteralElement) -> Self { +impl From for InterpolatedStringElement { + fn from(node: crate::InterpolatedStringLiteralElement) -> Self { Self::Literal(node) } } -impl ruff_text_size::Ranged for FStringElement { +impl ruff_text_size::Ranged for InterpolatedStringElement { fn range(&self) -> ruff_text_size::TextRange { match self { - Self::Expression(node) => node.range(), + Self::Interpolation(node) => node.range(), Self::Literal(node) => node.range(), } } } +impl crate::HasNodeIndex for InterpolatedStringElement { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::Interpolation(node) => node.node_index(), + Self::Literal(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] -impl FStringElement { +impl InterpolatedStringElement { #[inline] - pub const fn is_expression(&self) -> bool { - matches!(self, Self::Expression(_)) + pub const fn is_interpolation(&self) -> bool { + matches!(self, Self::Interpolation(_)) } #[inline] - pub fn expression(self) -> Option { + pub fn interpolation(self) -> Option { match self { - Self::Expression(val) => Some(val), + Self::Interpolation(val) => Some(val), _ => None, } } #[inline] - pub fn expect_expression(self) -> crate::FStringExpressionElement { + pub fn expect_interpolation(self) -> crate::InterpolatedElement { match self { - Self::Expression(val) => val, + Self::Interpolation(val) => val, _ => panic!("called expect on {self:?}"), } } #[inline] - pub fn as_expression_mut(&mut self) -> Option<&mut crate::FStringExpressionElement> { + pub fn as_interpolation_mut(&mut self) -> Option<&mut crate::InterpolatedElement> { match self { - Self::Expression(val) => Some(val), + Self::Interpolation(val) => Some(val), _ => None, } } #[inline] - pub fn as_expression(&self) -> Option<&crate::FStringExpressionElement> { + pub fn as_interpolation(&self) -> Option<&crate::InterpolatedElement> { match self { - Self::Expression(val) => Some(val), + Self::Interpolation(val) => Some(val), _ => None, } } @@ -2832,7 +2980,7 @@ impl FStringElement { } #[inline] - pub fn literal(self) -> Option { + pub fn literal(self) -> Option { match self { Self::Literal(val) => Some(val), _ => None, @@ -2840,7 +2988,7 @@ impl FStringElement { } #[inline] - pub fn expect_literal(self) -> crate::FStringLiteralElement { + pub fn expect_literal(self) -> crate::InterpolatedStringLiteralElement { match self { Self::Literal(val) => val, _ => panic!("called expect on {self:?}"), @@ -2848,7 +2996,7 @@ impl FStringElement { } #[inline] - pub fn as_literal_mut(&mut self) -> Option<&mut crate::FStringLiteralElement> { + pub fn as_literal_mut(&mut self) -> Option<&mut crate::InterpolatedStringLiteralElement> { match self { Self::Literal(val) => Some(val), _ => None, @@ -2856,7 +3004,7 @@ impl FStringElement { } #[inline] - pub fn as_literal(&self) -> Option<&crate::FStringLiteralElement> { + pub fn as_literal(&self) -> Option<&crate::InterpolatedStringLiteralElement> { match self { Self::Literal(val) => Some(val), _ => None, @@ -2866,6 +3014,7 @@ impl FStringElement { /// See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Pattern { MatchValue(crate::PatternMatchValue), MatchSingleton(crate::PatternMatchSingleton), @@ -2940,6 +3089,21 @@ impl ruff_text_size::Ranged for Pattern { } } +impl crate::HasNodeIndex for Pattern { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::MatchValue(node) => node.node_index(), + Self::MatchSingleton(node) => node.node_index(), + Self::MatchSequence(node) => node.node_index(), + Self::MatchMapping(node) => node.node_index(), + Self::MatchClass(node) => node.node_index(), + Self::MatchStar(node) => node.node_index(), + Self::MatchAs(node) => node.node_index(), + Self::MatchOr(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] impl Pattern { #[inline] @@ -3241,6 +3405,7 @@ impl Pattern { /// See also [type_param](https://docs.python.org/3/library/ast.html#ast.type_param) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum TypeParam { TypeVar(crate::TypeParamTypeVar), TypeVarTuple(crate::TypeParamTypeVarTuple), @@ -3275,6 +3440,16 @@ impl ruff_text_size::Ranged for TypeParam { } } +impl crate::HasNodeIndex for TypeParam { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::TypeVar(node) => node.node_index(), + Self::TypeVarTuple(node) => node.node_index(), + Self::ParamSpec(node) => node.node_index(), + } + } +} + #[allow(dead_code, clippy::match_wildcard_for_single_variants)] impl TypeParam { #[inline] @@ -3659,6 +3834,12 @@ impl ruff_text_size::Ranged for crate::ExprFString { } } +impl ruff_text_size::Ranged for crate::ExprTString { + fn range(&self) -> ruff_text_size::TextRange { + self.range + } +} + impl ruff_text_size::Ranged for crate::ExprStringLiteral { fn range(&self) -> ruff_text_size::TextRange { self.range @@ -3749,13 +3930,13 @@ impl ruff_text_size::Ranged for crate::ExceptHandlerExceptHandler { } } -impl ruff_text_size::Ranged for crate::FStringExpressionElement { +impl ruff_text_size::Ranged for crate::InterpolatedElement { fn range(&self) -> ruff_text_size::TextRange { self.range } } -impl ruff_text_size::Ranged for crate::FStringLiteralElement { +impl ruff_text_size::Ranged for crate::InterpolatedStringLiteralElement { fn range(&self) -> ruff_text_size::TextRange { self.range } @@ -3827,7 +4008,7 @@ impl ruff_text_size::Ranged for crate::TypeParamParamSpec { } } -impl ruff_text_size::Ranged for crate::FStringFormatSpec { +impl ruff_text_size::Ranged for crate::InterpolatedStringFormatSpec { fn range(&self) -> ruff_text_size::TextRange { self.range } @@ -3923,6 +4104,12 @@ impl ruff_text_size::Ranged for crate::FString { } } +impl ruff_text_size::Ranged for crate::TString { + fn range(&self) -> ruff_text_size::TextRange { + self.range + } +} + impl ruff_text_size::Ranged for crate::StringLiteral { fn range(&self) -> ruff_text_size::TextRange { self.range @@ -3941,2452 +4128,4623 @@ impl ruff_text_size::Ranged for crate::Identifier { } } -impl Mod { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - Mod::Module(node) => node.visit_source_order(visitor), - Mod::Expression(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::ModModule { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl Stmt { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - Stmt::FunctionDef(node) => node.visit_source_order(visitor), - Stmt::ClassDef(node) => node.visit_source_order(visitor), - Stmt::Return(node) => node.visit_source_order(visitor), - Stmt::Delete(node) => node.visit_source_order(visitor), - Stmt::TypeAlias(node) => node.visit_source_order(visitor), - Stmt::Assign(node) => node.visit_source_order(visitor), - Stmt::AugAssign(node) => node.visit_source_order(visitor), - Stmt::AnnAssign(node) => node.visit_source_order(visitor), - Stmt::For(node) => node.visit_source_order(visitor), - Stmt::While(node) => node.visit_source_order(visitor), - Stmt::If(node) => node.visit_source_order(visitor), - Stmt::With(node) => node.visit_source_order(visitor), - Stmt::Match(node) => node.visit_source_order(visitor), - Stmt::Raise(node) => node.visit_source_order(visitor), - Stmt::Try(node) => node.visit_source_order(visitor), - Stmt::Assert(node) => node.visit_source_order(visitor), - Stmt::Import(node) => node.visit_source_order(visitor), - Stmt::ImportFrom(node) => node.visit_source_order(visitor), - Stmt::Global(node) => node.visit_source_order(visitor), - Stmt::Nonlocal(node) => node.visit_source_order(visitor), - Stmt::Expr(node) => node.visit_source_order(visitor), - Stmt::Pass(node) => node.visit_source_order(visitor), - Stmt::Break(node) => node.visit_source_order(visitor), - Stmt::Continue(node) => node.visit_source_order(visitor), - Stmt::IpyEscapeCommand(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::ModExpression { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl Expr { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - Expr::BoolOp(node) => node.visit_source_order(visitor), - Expr::Named(node) => node.visit_source_order(visitor), - Expr::BinOp(node) => node.visit_source_order(visitor), - Expr::UnaryOp(node) => node.visit_source_order(visitor), - Expr::Lambda(node) => node.visit_source_order(visitor), - Expr::If(node) => node.visit_source_order(visitor), - Expr::Dict(node) => node.visit_source_order(visitor), - Expr::Set(node) => node.visit_source_order(visitor), - Expr::ListComp(node) => node.visit_source_order(visitor), - Expr::SetComp(node) => node.visit_source_order(visitor), - Expr::DictComp(node) => node.visit_source_order(visitor), - Expr::Generator(node) => node.visit_source_order(visitor), - Expr::Await(node) => node.visit_source_order(visitor), - Expr::Yield(node) => node.visit_source_order(visitor), - Expr::YieldFrom(node) => node.visit_source_order(visitor), - Expr::Compare(node) => node.visit_source_order(visitor), - Expr::Call(node) => node.visit_source_order(visitor), - Expr::FString(node) => node.visit_source_order(visitor), - Expr::StringLiteral(node) => node.visit_source_order(visitor), - Expr::BytesLiteral(node) => node.visit_source_order(visitor), - Expr::NumberLiteral(node) => node.visit_source_order(visitor), - Expr::BooleanLiteral(node) => node.visit_source_order(visitor), - Expr::NoneLiteral(node) => node.visit_source_order(visitor), - Expr::EllipsisLiteral(node) => node.visit_source_order(visitor), - Expr::Attribute(node) => node.visit_source_order(visitor), - Expr::Subscript(node) => node.visit_source_order(visitor), - Expr::Starred(node) => node.visit_source_order(visitor), - Expr::Name(node) => node.visit_source_order(visitor), - Expr::List(node) => node.visit_source_order(visitor), - Expr::Tuple(node) => node.visit_source_order(visitor), - Expr::Slice(node) => node.visit_source_order(visitor), - Expr::IpyEscapeCommand(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::StmtFunctionDef { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ExceptHandler { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - ExceptHandler::ExceptHandler(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::StmtClassDef { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl FStringElement { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - FStringElement::Expression(node) => node.visit_source_order(visitor), - FStringElement::Literal(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::StmtReturn { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl Pattern { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - Pattern::MatchValue(node) => node.visit_source_order(visitor), - Pattern::MatchSingleton(node) => node.visit_source_order(visitor), - Pattern::MatchSequence(node) => node.visit_source_order(visitor), - Pattern::MatchMapping(node) => node.visit_source_order(visitor), - Pattern::MatchClass(node) => node.visit_source_order(visitor), - Pattern::MatchStar(node) => node.visit_source_order(visitor), - Pattern::MatchAs(node) => node.visit_source_order(visitor), - Pattern::MatchOr(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::StmtDelete { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl TypeParam { - #[allow(unused)] - pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, - { - match self { - TypeParam::TypeVar(node) => node.visit_source_order(visitor), - TypeParam::TypeVarTuple(node) => node.visit_source_order(visitor), - TypeParam::ParamSpec(node) => node.visit_source_order(visitor), - } +impl crate::HasNodeIndex for crate::StmtTypeAlias { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -/// See also [mod](https://docs.python.org/3/library/ast.html#ast.mod) -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum ModRef<'a> { - Module(&'a crate::ModModule), - Expression(&'a crate::ModExpression), +impl crate::HasNodeIndex for crate::StmtAssign { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index + } } -impl<'a> From<&'a Mod> for ModRef<'a> { - fn from(node: &'a Mod) -> Self { - match node { - Mod::Module(node) => ModRef::Module(node), - Mod::Expression(node) => ModRef::Expression(node), - } +impl crate::HasNodeIndex for crate::StmtAugAssign { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ModModule> for ModRef<'a> { - fn from(node: &'a crate::ModModule) -> Self { - Self::Module(node) +impl crate::HasNodeIndex for crate::StmtAnnAssign { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ModExpression> for ModRef<'a> { - fn from(node: &'a crate::ModExpression) -> Self { - Self::Expression(node) +impl crate::HasNodeIndex for crate::StmtFor { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ruff_text_size::Ranged for ModRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - Self::Module(node) => node.range(), - Self::Expression(node) => node.range(), - } +impl crate::HasNodeIndex for crate::StmtWhile { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -/// See also [stmt](https://docs.python.org/3/library/ast.html#ast.stmt) -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum StmtRef<'a> { - #[is(name = "function_def_stmt")] - FunctionDef(&'a crate::StmtFunctionDef), - #[is(name = "class_def_stmt")] - ClassDef(&'a crate::StmtClassDef), - #[is(name = "return_stmt")] - Return(&'a crate::StmtReturn), - #[is(name = "delete_stmt")] - Delete(&'a crate::StmtDelete), - #[is(name = "type_alias_stmt")] - TypeAlias(&'a crate::StmtTypeAlias), - #[is(name = "assign_stmt")] - Assign(&'a crate::StmtAssign), - #[is(name = "aug_assign_stmt")] - AugAssign(&'a crate::StmtAugAssign), - #[is(name = "ann_assign_stmt")] - AnnAssign(&'a crate::StmtAnnAssign), - #[is(name = "for_stmt")] - For(&'a crate::StmtFor), - #[is(name = "while_stmt")] - While(&'a crate::StmtWhile), - #[is(name = "if_stmt")] - If(&'a crate::StmtIf), - #[is(name = "with_stmt")] - With(&'a crate::StmtWith), - #[is(name = "match_stmt")] - Match(&'a crate::StmtMatch), - #[is(name = "raise_stmt")] - Raise(&'a crate::StmtRaise), - #[is(name = "try_stmt")] - Try(&'a crate::StmtTry), - #[is(name = "assert_stmt")] - Assert(&'a crate::StmtAssert), - #[is(name = "import_stmt")] - Import(&'a crate::StmtImport), - #[is(name = "import_from_stmt")] - ImportFrom(&'a crate::StmtImportFrom), - #[is(name = "global_stmt")] - Global(&'a crate::StmtGlobal), - #[is(name = "nonlocal_stmt")] - Nonlocal(&'a crate::StmtNonlocal), - #[is(name = "expr_stmt")] - Expr(&'a crate::StmtExpr), - #[is(name = "pass_stmt")] - Pass(&'a crate::StmtPass), - #[is(name = "break_stmt")] - Break(&'a crate::StmtBreak), - #[is(name = "continue_stmt")] - Continue(&'a crate::StmtContinue), - #[is(name = "ipy_escape_command_stmt")] - IpyEscapeCommand(&'a crate::StmtIpyEscapeCommand), +impl crate::HasNodeIndex for crate::StmtIf { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index + } } -impl<'a> From<&'a Stmt> for StmtRef<'a> { - fn from(node: &'a Stmt) -> Self { - match node { - Stmt::FunctionDef(node) => StmtRef::FunctionDef(node), - Stmt::ClassDef(node) => StmtRef::ClassDef(node), - Stmt::Return(node) => StmtRef::Return(node), - Stmt::Delete(node) => StmtRef::Delete(node), - Stmt::TypeAlias(node) => StmtRef::TypeAlias(node), - Stmt::Assign(node) => StmtRef::Assign(node), - Stmt::AugAssign(node) => StmtRef::AugAssign(node), - Stmt::AnnAssign(node) => StmtRef::AnnAssign(node), - Stmt::For(node) => StmtRef::For(node), - Stmt::While(node) => StmtRef::While(node), - Stmt::If(node) => StmtRef::If(node), - Stmt::With(node) => StmtRef::With(node), - Stmt::Match(node) => StmtRef::Match(node), - Stmt::Raise(node) => StmtRef::Raise(node), - Stmt::Try(node) => StmtRef::Try(node), - Stmt::Assert(node) => StmtRef::Assert(node), - Stmt::Import(node) => StmtRef::Import(node), - Stmt::ImportFrom(node) => StmtRef::ImportFrom(node), - Stmt::Global(node) => StmtRef::Global(node), - Stmt::Nonlocal(node) => StmtRef::Nonlocal(node), - Stmt::Expr(node) => StmtRef::Expr(node), - Stmt::Pass(node) => StmtRef::Pass(node), - Stmt::Break(node) => StmtRef::Break(node), - Stmt::Continue(node) => StmtRef::Continue(node), - Stmt::IpyEscapeCommand(node) => StmtRef::IpyEscapeCommand(node), - } +impl crate::HasNodeIndex for crate::StmtWith { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtFunctionDef> for StmtRef<'a> { - fn from(node: &'a crate::StmtFunctionDef) -> Self { - Self::FunctionDef(node) +impl crate::HasNodeIndex for crate::StmtMatch { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtClassDef> for StmtRef<'a> { - fn from(node: &'a crate::StmtClassDef) -> Self { - Self::ClassDef(node) +impl crate::HasNodeIndex for crate::StmtRaise { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtReturn> for StmtRef<'a> { - fn from(node: &'a crate::StmtReturn) -> Self { - Self::Return(node) +impl crate::HasNodeIndex for crate::StmtTry { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtDelete> for StmtRef<'a> { - fn from(node: &'a crate::StmtDelete) -> Self { - Self::Delete(node) +impl crate::HasNodeIndex for crate::StmtAssert { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtTypeAlias> for StmtRef<'a> { - fn from(node: &'a crate::StmtTypeAlias) -> Self { - Self::TypeAlias(node) +impl crate::HasNodeIndex for crate::StmtImport { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtAssign> for StmtRef<'a> { - fn from(node: &'a crate::StmtAssign) -> Self { - Self::Assign(node) +impl crate::HasNodeIndex for crate::StmtImportFrom { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtAugAssign> for StmtRef<'a> { - fn from(node: &'a crate::StmtAugAssign) -> Self { - Self::AugAssign(node) +impl crate::HasNodeIndex for crate::StmtGlobal { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtAnnAssign> for StmtRef<'a> { - fn from(node: &'a crate::StmtAnnAssign) -> Self { - Self::AnnAssign(node) +impl crate::HasNodeIndex for crate::StmtNonlocal { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtFor> for StmtRef<'a> { - fn from(node: &'a crate::StmtFor) -> Self { - Self::For(node) +impl crate::HasNodeIndex for crate::StmtExpr { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtWhile> for StmtRef<'a> { - fn from(node: &'a crate::StmtWhile) -> Self { - Self::While(node) +impl crate::HasNodeIndex for crate::StmtPass { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtIf> for StmtRef<'a> { - fn from(node: &'a crate::StmtIf) -> Self { - Self::If(node) +impl crate::HasNodeIndex for crate::StmtBreak { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtWith> for StmtRef<'a> { - fn from(node: &'a crate::StmtWith) -> Self { - Self::With(node) +impl crate::HasNodeIndex for crate::StmtContinue { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtMatch> for StmtRef<'a> { - fn from(node: &'a crate::StmtMatch) -> Self { - Self::Match(node) +impl crate::HasNodeIndex for crate::StmtIpyEscapeCommand { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtRaise> for StmtRef<'a> { - fn from(node: &'a crate::StmtRaise) -> Self { - Self::Raise(node) +impl crate::HasNodeIndex for crate::ExprBoolOp { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtTry> for StmtRef<'a> { - fn from(node: &'a crate::StmtTry) -> Self { - Self::Try(node) +impl crate::HasNodeIndex for crate::ExprNamed { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtAssert> for StmtRef<'a> { - fn from(node: &'a crate::StmtAssert) -> Self { - Self::Assert(node) +impl crate::HasNodeIndex for crate::ExprBinOp { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtImport> for StmtRef<'a> { - fn from(node: &'a crate::StmtImport) -> Self { - Self::Import(node) +impl crate::HasNodeIndex for crate::ExprUnaryOp { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtImportFrom> for StmtRef<'a> { - fn from(node: &'a crate::StmtImportFrom) -> Self { - Self::ImportFrom(node) +impl crate::HasNodeIndex for crate::ExprLambda { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtGlobal> for StmtRef<'a> { - fn from(node: &'a crate::StmtGlobal) -> Self { - Self::Global(node) +impl crate::HasNodeIndex for crate::ExprIf { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtNonlocal> for StmtRef<'a> { - fn from(node: &'a crate::StmtNonlocal) -> Self { - Self::Nonlocal(node) +impl crate::HasNodeIndex for crate::ExprDict { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtExpr> for StmtRef<'a> { - fn from(node: &'a crate::StmtExpr) -> Self { - Self::Expr(node) +impl crate::HasNodeIndex for crate::ExprSet { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtPass> for StmtRef<'a> { - fn from(node: &'a crate::StmtPass) -> Self { - Self::Pass(node) +impl crate::HasNodeIndex for crate::ExprListComp { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtBreak> for StmtRef<'a> { - fn from(node: &'a crate::StmtBreak) -> Self { - Self::Break(node) +impl crate::HasNodeIndex for crate::ExprSetComp { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtContinue> for StmtRef<'a> { - fn from(node: &'a crate::StmtContinue) -> Self { - Self::Continue(node) +impl crate::HasNodeIndex for crate::ExprDictComp { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::StmtIpyEscapeCommand> for StmtRef<'a> { - fn from(node: &'a crate::StmtIpyEscapeCommand) -> Self { - Self::IpyEscapeCommand(node) +impl crate::HasNodeIndex for crate::ExprGenerator { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ruff_text_size::Ranged for StmtRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - Self::FunctionDef(node) => node.range(), - Self::ClassDef(node) => node.range(), - Self::Return(node) => node.range(), - Self::Delete(node) => node.range(), - Self::TypeAlias(node) => node.range(), - Self::Assign(node) => node.range(), - Self::AugAssign(node) => node.range(), - Self::AnnAssign(node) => node.range(), - Self::For(node) => node.range(), - Self::While(node) => node.range(), - Self::If(node) => node.range(), - Self::With(node) => node.range(), - Self::Match(node) => node.range(), - Self::Raise(node) => node.range(), - Self::Try(node) => node.range(), - Self::Assert(node) => node.range(), - Self::Import(node) => node.range(), - Self::ImportFrom(node) => node.range(), - Self::Global(node) => node.range(), - Self::Nonlocal(node) => node.range(), - Self::Expr(node) => node.range(), - Self::Pass(node) => node.range(), - Self::Break(node) => node.range(), - Self::Continue(node) => node.range(), - Self::IpyEscapeCommand(node) => node.range(), - } +impl crate::HasNodeIndex for crate::ExprAwait { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -/// See also [expr](https://docs.python.org/3/library/ast.html#ast.expr) -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum ExprRef<'a> { - #[is(name = "bool_op_expr")] - BoolOp(&'a crate::ExprBoolOp), - #[is(name = "named_expr")] - Named(&'a crate::ExprNamed), - #[is(name = "bin_op_expr")] - BinOp(&'a crate::ExprBinOp), - #[is(name = "unary_op_expr")] - UnaryOp(&'a crate::ExprUnaryOp), - #[is(name = "lambda_expr")] - Lambda(&'a crate::ExprLambda), - #[is(name = "if_expr")] - If(&'a crate::ExprIf), - #[is(name = "dict_expr")] - Dict(&'a crate::ExprDict), - #[is(name = "set_expr")] - Set(&'a crate::ExprSet), - #[is(name = "list_comp_expr")] - ListComp(&'a crate::ExprListComp), - #[is(name = "set_comp_expr")] - SetComp(&'a crate::ExprSetComp), - #[is(name = "dict_comp_expr")] - DictComp(&'a crate::ExprDictComp), - #[is(name = "generator_expr")] - Generator(&'a crate::ExprGenerator), - #[is(name = "await_expr")] - Await(&'a crate::ExprAwait), - #[is(name = "yield_expr")] - Yield(&'a crate::ExprYield), - #[is(name = "yield_from_expr")] - YieldFrom(&'a crate::ExprYieldFrom), - #[is(name = "compare_expr")] - Compare(&'a crate::ExprCompare), - #[is(name = "call_expr")] - Call(&'a crate::ExprCall), - #[is(name = "f_string_expr")] - FString(&'a crate::ExprFString), - #[is(name = "string_literal_expr")] - StringLiteral(&'a crate::ExprStringLiteral), - #[is(name = "bytes_literal_expr")] - BytesLiteral(&'a crate::ExprBytesLiteral), - #[is(name = "number_literal_expr")] - NumberLiteral(&'a crate::ExprNumberLiteral), - #[is(name = "boolean_literal_expr")] - BooleanLiteral(&'a crate::ExprBooleanLiteral), - #[is(name = "none_literal_expr")] - NoneLiteral(&'a crate::ExprNoneLiteral), - #[is(name = "ellipsis_literal_expr")] - EllipsisLiteral(&'a crate::ExprEllipsisLiteral), - #[is(name = "attribute_expr")] - Attribute(&'a crate::ExprAttribute), - #[is(name = "subscript_expr")] - Subscript(&'a crate::ExprSubscript), - #[is(name = "starred_expr")] - Starred(&'a crate::ExprStarred), - #[is(name = "name_expr")] - Name(&'a crate::ExprName), - #[is(name = "list_expr")] - List(&'a crate::ExprList), - #[is(name = "tuple_expr")] - Tuple(&'a crate::ExprTuple), - #[is(name = "slice_expr")] - Slice(&'a crate::ExprSlice), - #[is(name = "ipy_escape_command_expr")] - IpyEscapeCommand(&'a crate::ExprIpyEscapeCommand), +impl crate::HasNodeIndex for crate::ExprYield { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index + } } -impl<'a> From<&'a Expr> for ExprRef<'a> { - fn from(node: &'a Expr) -> Self { - match node { - Expr::BoolOp(node) => ExprRef::BoolOp(node), - Expr::Named(node) => ExprRef::Named(node), - Expr::BinOp(node) => ExprRef::BinOp(node), - Expr::UnaryOp(node) => ExprRef::UnaryOp(node), - Expr::Lambda(node) => ExprRef::Lambda(node), - Expr::If(node) => ExprRef::If(node), - Expr::Dict(node) => ExprRef::Dict(node), - Expr::Set(node) => ExprRef::Set(node), - Expr::ListComp(node) => ExprRef::ListComp(node), - Expr::SetComp(node) => ExprRef::SetComp(node), - Expr::DictComp(node) => ExprRef::DictComp(node), - Expr::Generator(node) => ExprRef::Generator(node), - Expr::Await(node) => ExprRef::Await(node), - Expr::Yield(node) => ExprRef::Yield(node), - Expr::YieldFrom(node) => ExprRef::YieldFrom(node), - Expr::Compare(node) => ExprRef::Compare(node), - Expr::Call(node) => ExprRef::Call(node), - Expr::FString(node) => ExprRef::FString(node), - Expr::StringLiteral(node) => ExprRef::StringLiteral(node), - Expr::BytesLiteral(node) => ExprRef::BytesLiteral(node), - Expr::NumberLiteral(node) => ExprRef::NumberLiteral(node), - Expr::BooleanLiteral(node) => ExprRef::BooleanLiteral(node), - Expr::NoneLiteral(node) => ExprRef::NoneLiteral(node), - Expr::EllipsisLiteral(node) => ExprRef::EllipsisLiteral(node), - Expr::Attribute(node) => ExprRef::Attribute(node), - Expr::Subscript(node) => ExprRef::Subscript(node), - Expr::Starred(node) => ExprRef::Starred(node), - Expr::Name(node) => ExprRef::Name(node), - Expr::List(node) => ExprRef::List(node), - Expr::Tuple(node) => ExprRef::Tuple(node), - Expr::Slice(node) => ExprRef::Slice(node), - Expr::IpyEscapeCommand(node) => ExprRef::IpyEscapeCommand(node), - } +impl crate::HasNodeIndex for crate::ExprYieldFrom { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprBoolOp> for ExprRef<'a> { - fn from(node: &'a crate::ExprBoolOp) -> Self { - Self::BoolOp(node) +impl crate::HasNodeIndex for crate::ExprCompare { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprNamed> for ExprRef<'a> { - fn from(node: &'a crate::ExprNamed) -> Self { - Self::Named(node) +impl crate::HasNodeIndex for crate::ExprCall { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprBinOp> for ExprRef<'a> { - fn from(node: &'a crate::ExprBinOp) -> Self { - Self::BinOp(node) +impl crate::HasNodeIndex for crate::ExprFString { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprUnaryOp> for ExprRef<'a> { - fn from(node: &'a crate::ExprUnaryOp) -> Self { - Self::UnaryOp(node) +impl crate::HasNodeIndex for crate::ExprTString { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprLambda> for ExprRef<'a> { - fn from(node: &'a crate::ExprLambda) -> Self { - Self::Lambda(node) +impl crate::HasNodeIndex for crate::ExprStringLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprIf> for ExprRef<'a> { - fn from(node: &'a crate::ExprIf) -> Self { - Self::If(node) +impl crate::HasNodeIndex for crate::ExprBytesLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprDict> for ExprRef<'a> { - fn from(node: &'a crate::ExprDict) -> Self { - Self::Dict(node) +impl crate::HasNodeIndex for crate::ExprNumberLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprSet> for ExprRef<'a> { - fn from(node: &'a crate::ExprSet) -> Self { - Self::Set(node) +impl crate::HasNodeIndex for crate::ExprBooleanLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprListComp> for ExprRef<'a> { - fn from(node: &'a crate::ExprListComp) -> Self { - Self::ListComp(node) +impl crate::HasNodeIndex for crate::ExprNoneLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprSetComp> for ExprRef<'a> { - fn from(node: &'a crate::ExprSetComp) -> Self { - Self::SetComp(node) +impl crate::HasNodeIndex for crate::ExprEllipsisLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprDictComp> for ExprRef<'a> { - fn from(node: &'a crate::ExprDictComp) -> Self { - Self::DictComp(node) +impl crate::HasNodeIndex for crate::ExprAttribute { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprGenerator> for ExprRef<'a> { - fn from(node: &'a crate::ExprGenerator) -> Self { - Self::Generator(node) +impl crate::HasNodeIndex for crate::ExprSubscript { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprAwait> for ExprRef<'a> { - fn from(node: &'a crate::ExprAwait) -> Self { - Self::Await(node) +impl crate::HasNodeIndex for crate::ExprStarred { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprYield> for ExprRef<'a> { - fn from(node: &'a crate::ExprYield) -> Self { - Self::Yield(node) +impl crate::HasNodeIndex for crate::ExprName { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprYieldFrom> for ExprRef<'a> { - fn from(node: &'a crate::ExprYieldFrom) -> Self { - Self::YieldFrom(node) +impl crate::HasNodeIndex for crate::ExprList { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprCompare> for ExprRef<'a> { - fn from(node: &'a crate::ExprCompare) -> Self { - Self::Compare(node) +impl crate::HasNodeIndex for crate::ExprTuple { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprCall> for ExprRef<'a> { - fn from(node: &'a crate::ExprCall) -> Self { - Self::Call(node) +impl crate::HasNodeIndex for crate::ExprSlice { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprFString> for ExprRef<'a> { - fn from(node: &'a crate::ExprFString) -> Self { - Self::FString(node) +impl crate::HasNodeIndex for crate::ExprIpyEscapeCommand { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprStringLiteral> for ExprRef<'a> { - fn from(node: &'a crate::ExprStringLiteral) -> Self { - Self::StringLiteral(node) +impl crate::HasNodeIndex for crate::ExceptHandlerExceptHandler { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprBytesLiteral> for ExprRef<'a> { - fn from(node: &'a crate::ExprBytesLiteral) -> Self { - Self::BytesLiteral(node) +impl crate::HasNodeIndex for crate::InterpolatedElement { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprNumberLiteral> for ExprRef<'a> { - fn from(node: &'a crate::ExprNumberLiteral) -> Self { - Self::NumberLiteral(node) +impl crate::HasNodeIndex for crate::InterpolatedStringLiteralElement { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprBooleanLiteral> for ExprRef<'a> { - fn from(node: &'a crate::ExprBooleanLiteral) -> Self { - Self::BooleanLiteral(node) +impl crate::HasNodeIndex for crate::PatternMatchValue { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprNoneLiteral> for ExprRef<'a> { - fn from(node: &'a crate::ExprNoneLiteral) -> Self { - Self::NoneLiteral(node) +impl crate::HasNodeIndex for crate::PatternMatchSingleton { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprEllipsisLiteral> for ExprRef<'a> { - fn from(node: &'a crate::ExprEllipsisLiteral) -> Self { - Self::EllipsisLiteral(node) +impl crate::HasNodeIndex for crate::PatternMatchSequence { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprAttribute> for ExprRef<'a> { - fn from(node: &'a crate::ExprAttribute) -> Self { - Self::Attribute(node) +impl crate::HasNodeIndex for crate::PatternMatchMapping { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprSubscript> for ExprRef<'a> { - fn from(node: &'a crate::ExprSubscript) -> Self { - Self::Subscript(node) +impl crate::HasNodeIndex for crate::PatternMatchClass { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprStarred> for ExprRef<'a> { - fn from(node: &'a crate::ExprStarred) -> Self { - Self::Starred(node) +impl crate::HasNodeIndex for crate::PatternMatchStar { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprName> for ExprRef<'a> { - fn from(node: &'a crate::ExprName) -> Self { - Self::Name(node) +impl crate::HasNodeIndex for crate::PatternMatchAs { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprList> for ExprRef<'a> { - fn from(node: &'a crate::ExprList) -> Self { - Self::List(node) +impl crate::HasNodeIndex for crate::PatternMatchOr { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprTuple> for ExprRef<'a> { - fn from(node: &'a crate::ExprTuple) -> Self { - Self::Tuple(node) +impl crate::HasNodeIndex for crate::TypeParamTypeVar { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprSlice> for ExprRef<'a> { - fn from(node: &'a crate::ExprSlice) -> Self { - Self::Slice(node) +impl crate::HasNodeIndex for crate::TypeParamTypeVarTuple { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExprIpyEscapeCommand> for ExprRef<'a> { - fn from(node: &'a crate::ExprIpyEscapeCommand) -> Self { - Self::IpyEscapeCommand(node) +impl crate::HasNodeIndex for crate::TypeParamParamSpec { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ruff_text_size::Ranged for ExprRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - Self::BoolOp(node) => node.range(), - Self::Named(node) => node.range(), - Self::BinOp(node) => node.range(), - Self::UnaryOp(node) => node.range(), - Self::Lambda(node) => node.range(), - Self::If(node) => node.range(), - Self::Dict(node) => node.range(), - Self::Set(node) => node.range(), - Self::ListComp(node) => node.range(), - Self::SetComp(node) => node.range(), - Self::DictComp(node) => node.range(), - Self::Generator(node) => node.range(), - Self::Await(node) => node.range(), - Self::Yield(node) => node.range(), - Self::YieldFrom(node) => node.range(), - Self::Compare(node) => node.range(), - Self::Call(node) => node.range(), - Self::FString(node) => node.range(), - Self::StringLiteral(node) => node.range(), - Self::BytesLiteral(node) => node.range(), - Self::NumberLiteral(node) => node.range(), - Self::BooleanLiteral(node) => node.range(), - Self::NoneLiteral(node) => node.range(), - Self::EllipsisLiteral(node) => node.range(), - Self::Attribute(node) => node.range(), - Self::Subscript(node) => node.range(), - Self::Starred(node) => node.range(), - Self::Name(node) => node.range(), - Self::List(node) => node.range(), - Self::Tuple(node) => node.range(), - Self::Slice(node) => node.range(), - Self::IpyEscapeCommand(node) => node.range(), - } +impl crate::HasNodeIndex for crate::InterpolatedStringFormatSpec { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -/// See also [excepthandler](https://docs.python.org/3/library/ast.html#ast.excepthandler) -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum ExceptHandlerRef<'a> { - ExceptHandler(&'a crate::ExceptHandlerExceptHandler), -} - -impl<'a> From<&'a ExceptHandler> for ExceptHandlerRef<'a> { - fn from(node: &'a ExceptHandler) -> Self { - match node { - ExceptHandler::ExceptHandler(node) => ExceptHandlerRef::ExceptHandler(node), - } +impl crate::HasNodeIndex for crate::PatternArguments { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::ExceptHandlerExceptHandler> for ExceptHandlerRef<'a> { - fn from(node: &'a crate::ExceptHandlerExceptHandler) -> Self { - Self::ExceptHandler(node) +impl crate::HasNodeIndex for crate::PatternKeyword { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ruff_text_size::Ranged for ExceptHandlerRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - Self::ExceptHandler(node) => node.range(), - } +impl crate::HasNodeIndex for crate::Comprehension { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum FStringElementRef<'a> { - Expression(&'a crate::FStringExpressionElement), - Literal(&'a crate::FStringLiteralElement), -} - -impl<'a> From<&'a FStringElement> for FStringElementRef<'a> { - fn from(node: &'a FStringElement) -> Self { - match node { - FStringElement::Expression(node) => FStringElementRef::Expression(node), - FStringElement::Literal(node) => FStringElementRef::Literal(node), - } +impl crate::HasNodeIndex for crate::Arguments { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::FStringExpressionElement> for FStringElementRef<'a> { - fn from(node: &'a crate::FStringExpressionElement) -> Self { - Self::Expression(node) +impl crate::HasNodeIndex for crate::Parameters { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::FStringLiteralElement> for FStringElementRef<'a> { - fn from(node: &'a crate::FStringLiteralElement) -> Self { - Self::Literal(node) +impl crate::HasNodeIndex for crate::Parameter { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ruff_text_size::Ranged for FStringElementRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - Self::Expression(node) => node.range(), - Self::Literal(node) => node.range(), - } +impl crate::HasNodeIndex for crate::ParameterWithDefault { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -/// See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern) -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum PatternRef<'a> { - MatchValue(&'a crate::PatternMatchValue), - MatchSingleton(&'a crate::PatternMatchSingleton), - MatchSequence(&'a crate::PatternMatchSequence), - MatchMapping(&'a crate::PatternMatchMapping), - MatchClass(&'a crate::PatternMatchClass), - MatchStar(&'a crate::PatternMatchStar), - MatchAs(&'a crate::PatternMatchAs), - MatchOr(&'a crate::PatternMatchOr), +impl crate::HasNodeIndex for crate::Keyword { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index + } } -impl<'a> From<&'a Pattern> for PatternRef<'a> { - fn from(node: &'a Pattern) -> Self { - match node { - Pattern::MatchValue(node) => PatternRef::MatchValue(node), - Pattern::MatchSingleton(node) => PatternRef::MatchSingleton(node), - Pattern::MatchSequence(node) => PatternRef::MatchSequence(node), - Pattern::MatchMapping(node) => PatternRef::MatchMapping(node), - Pattern::MatchClass(node) => PatternRef::MatchClass(node), - Pattern::MatchStar(node) => PatternRef::MatchStar(node), - Pattern::MatchAs(node) => PatternRef::MatchAs(node), - Pattern::MatchOr(node) => PatternRef::MatchOr(node), - } +impl crate::HasNodeIndex for crate::Alias { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchValue> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchValue) -> Self { - Self::MatchValue(node) +impl crate::HasNodeIndex for crate::WithItem { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchSingleton> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchSingleton) -> Self { - Self::MatchSingleton(node) +impl crate::HasNodeIndex for crate::MatchCase { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchSequence> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchSequence) -> Self { - Self::MatchSequence(node) +impl crate::HasNodeIndex for crate::Decorator { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchMapping> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchMapping) -> Self { - Self::MatchMapping(node) +impl crate::HasNodeIndex for crate::ElifElseClause { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchClass> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchClass) -> Self { - Self::MatchClass(node) +impl crate::HasNodeIndex for crate::TypeParams { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchStar> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchStar) -> Self { - Self::MatchStar(node) +impl crate::HasNodeIndex for crate::FString { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchAs> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchAs) -> Self { - Self::MatchAs(node) +impl crate::HasNodeIndex for crate::TString { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl<'a> From<&'a crate::PatternMatchOr> for PatternRef<'a> { - fn from(node: &'a crate::PatternMatchOr) -> Self { - Self::MatchOr(node) +impl crate::HasNodeIndex for crate::StringLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -impl ruff_text_size::Ranged for PatternRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - Self::MatchValue(node) => node.range(), - Self::MatchSingleton(node) => node.range(), - Self::MatchSequence(node) => node.range(), - Self::MatchMapping(node) => node.range(), - Self::MatchClass(node) => node.range(), - Self::MatchStar(node) => node.range(), - Self::MatchAs(node) => node.range(), - Self::MatchOr(node) => node.range(), - } +impl crate::HasNodeIndex for crate::BytesLiteral { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index } } -/// See also [type_param](https://docs.python.org/3/library/ast.html#ast.type_param) -#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum TypeParamRef<'a> { - TypeVar(&'a crate::TypeParamTypeVar), - TypeVarTuple(&'a crate::TypeParamTypeVarTuple), - ParamSpec(&'a crate::TypeParamParamSpec), +impl crate::HasNodeIndex for crate::Identifier { + fn node_index(&self) -> &crate::AtomicNodeIndex { + &self.node_index + } } -impl<'a> From<&'a TypeParam> for TypeParamRef<'a> { - fn from(node: &'a TypeParam) -> Self { - match node { - TypeParam::TypeVar(node) => TypeParamRef::TypeVar(node), - TypeParam::TypeVarTuple(node) => TypeParamRef::TypeVarTuple(node), - TypeParam::ParamSpec(node) => TypeParamRef::ParamSpec(node), +impl Mod { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { + match self { + Mod::Module(node) => node.visit_source_order(visitor), + Mod::Expression(node) => node.visit_source_order(visitor), } } } -impl<'a> From<&'a crate::TypeParamTypeVar> for TypeParamRef<'a> { - fn from(node: &'a crate::TypeParamTypeVar) -> Self { - Self::TypeVar(node) +impl Stmt { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { + match self { + Stmt::FunctionDef(node) => node.visit_source_order(visitor), + Stmt::ClassDef(node) => node.visit_source_order(visitor), + Stmt::Return(node) => node.visit_source_order(visitor), + Stmt::Delete(node) => node.visit_source_order(visitor), + Stmt::TypeAlias(node) => node.visit_source_order(visitor), + Stmt::Assign(node) => node.visit_source_order(visitor), + Stmt::AugAssign(node) => node.visit_source_order(visitor), + Stmt::AnnAssign(node) => node.visit_source_order(visitor), + Stmt::For(node) => node.visit_source_order(visitor), + Stmt::While(node) => node.visit_source_order(visitor), + Stmt::If(node) => node.visit_source_order(visitor), + Stmt::With(node) => node.visit_source_order(visitor), + Stmt::Match(node) => node.visit_source_order(visitor), + Stmt::Raise(node) => node.visit_source_order(visitor), + Stmt::Try(node) => node.visit_source_order(visitor), + Stmt::Assert(node) => node.visit_source_order(visitor), + Stmt::Import(node) => node.visit_source_order(visitor), + Stmt::ImportFrom(node) => node.visit_source_order(visitor), + Stmt::Global(node) => node.visit_source_order(visitor), + Stmt::Nonlocal(node) => node.visit_source_order(visitor), + Stmt::Expr(node) => node.visit_source_order(visitor), + Stmt::Pass(node) => node.visit_source_order(visitor), + Stmt::Break(node) => node.visit_source_order(visitor), + Stmt::Continue(node) => node.visit_source_order(visitor), + Stmt::IpyEscapeCommand(node) => node.visit_source_order(visitor), + } } } -impl<'a> From<&'a crate::TypeParamTypeVarTuple> for TypeParamRef<'a> { - fn from(node: &'a crate::TypeParamTypeVarTuple) -> Self { - Self::TypeVarTuple(node) +impl Expr { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { + match self { + Expr::BoolOp(node) => node.visit_source_order(visitor), + Expr::Named(node) => node.visit_source_order(visitor), + Expr::BinOp(node) => node.visit_source_order(visitor), + Expr::UnaryOp(node) => node.visit_source_order(visitor), + Expr::Lambda(node) => node.visit_source_order(visitor), + Expr::If(node) => node.visit_source_order(visitor), + Expr::Dict(node) => node.visit_source_order(visitor), + Expr::Set(node) => node.visit_source_order(visitor), + Expr::ListComp(node) => node.visit_source_order(visitor), + Expr::SetComp(node) => node.visit_source_order(visitor), + Expr::DictComp(node) => node.visit_source_order(visitor), + Expr::Generator(node) => node.visit_source_order(visitor), + Expr::Await(node) => node.visit_source_order(visitor), + Expr::Yield(node) => node.visit_source_order(visitor), + Expr::YieldFrom(node) => node.visit_source_order(visitor), + Expr::Compare(node) => node.visit_source_order(visitor), + Expr::Call(node) => node.visit_source_order(visitor), + Expr::FString(node) => node.visit_source_order(visitor), + Expr::TString(node) => node.visit_source_order(visitor), + Expr::StringLiteral(node) => node.visit_source_order(visitor), + Expr::BytesLiteral(node) => node.visit_source_order(visitor), + Expr::NumberLiteral(node) => node.visit_source_order(visitor), + Expr::BooleanLiteral(node) => node.visit_source_order(visitor), + Expr::NoneLiteral(node) => node.visit_source_order(visitor), + Expr::EllipsisLiteral(node) => node.visit_source_order(visitor), + Expr::Attribute(node) => node.visit_source_order(visitor), + Expr::Subscript(node) => node.visit_source_order(visitor), + Expr::Starred(node) => node.visit_source_order(visitor), + Expr::Name(node) => node.visit_source_order(visitor), + Expr::List(node) => node.visit_source_order(visitor), + Expr::Tuple(node) => node.visit_source_order(visitor), + Expr::Slice(node) => node.visit_source_order(visitor), + Expr::IpyEscapeCommand(node) => node.visit_source_order(visitor), + } } } -impl<'a> From<&'a crate::TypeParamParamSpec> for TypeParamRef<'a> { - fn from(node: &'a crate::TypeParamParamSpec) -> Self { - Self::ParamSpec(node) +impl ExceptHandler { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { + match self { + ExceptHandler::ExceptHandler(node) => node.visit_source_order(visitor), + } } } -impl ruff_text_size::Ranged for TypeParamRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { +impl InterpolatedStringElement { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { match self { - Self::TypeVar(node) => node.range(), - Self::TypeVarTuple(node) => node.range(), - Self::ParamSpec(node) => node.range(), + InterpolatedStringElement::Interpolation(node) => node.visit_source_order(visitor), + InterpolatedStringElement::Literal(node) => node.visit_source_order(visitor), } } } -#[derive(Copy, Clone, Debug, is_macro::Is, PartialEq)] -pub enum AnyNodeRef<'a> { - ModModule(&'a crate::ModModule), - ModExpression(&'a crate::ModExpression), - StmtFunctionDef(&'a crate::StmtFunctionDef), - StmtClassDef(&'a crate::StmtClassDef), - StmtReturn(&'a crate::StmtReturn), - StmtDelete(&'a crate::StmtDelete), - StmtTypeAlias(&'a crate::StmtTypeAlias), - StmtAssign(&'a crate::StmtAssign), - StmtAugAssign(&'a crate::StmtAugAssign), - StmtAnnAssign(&'a crate::StmtAnnAssign), - StmtFor(&'a crate::StmtFor), - StmtWhile(&'a crate::StmtWhile), - StmtIf(&'a crate::StmtIf), - StmtWith(&'a crate::StmtWith), - StmtMatch(&'a crate::StmtMatch), - StmtRaise(&'a crate::StmtRaise), - StmtTry(&'a crate::StmtTry), - StmtAssert(&'a crate::StmtAssert), - StmtImport(&'a crate::StmtImport), - StmtImportFrom(&'a crate::StmtImportFrom), - StmtGlobal(&'a crate::StmtGlobal), - StmtNonlocal(&'a crate::StmtNonlocal), - StmtExpr(&'a crate::StmtExpr), - StmtPass(&'a crate::StmtPass), - StmtBreak(&'a crate::StmtBreak), - StmtContinue(&'a crate::StmtContinue), - StmtIpyEscapeCommand(&'a crate::StmtIpyEscapeCommand), - ExprBoolOp(&'a crate::ExprBoolOp), - ExprNamed(&'a crate::ExprNamed), - ExprBinOp(&'a crate::ExprBinOp), - ExprUnaryOp(&'a crate::ExprUnaryOp), - ExprLambda(&'a crate::ExprLambda), - ExprIf(&'a crate::ExprIf), - ExprDict(&'a crate::ExprDict), - ExprSet(&'a crate::ExprSet), - ExprListComp(&'a crate::ExprListComp), - ExprSetComp(&'a crate::ExprSetComp), - ExprDictComp(&'a crate::ExprDictComp), - ExprGenerator(&'a crate::ExprGenerator), - ExprAwait(&'a crate::ExprAwait), - ExprYield(&'a crate::ExprYield), - ExprYieldFrom(&'a crate::ExprYieldFrom), - ExprCompare(&'a crate::ExprCompare), - ExprCall(&'a crate::ExprCall), - ExprFString(&'a crate::ExprFString), - ExprStringLiteral(&'a crate::ExprStringLiteral), - ExprBytesLiteral(&'a crate::ExprBytesLiteral), - ExprNumberLiteral(&'a crate::ExprNumberLiteral), - ExprBooleanLiteral(&'a crate::ExprBooleanLiteral), - ExprNoneLiteral(&'a crate::ExprNoneLiteral), - ExprEllipsisLiteral(&'a crate::ExprEllipsisLiteral), - ExprAttribute(&'a crate::ExprAttribute), - ExprSubscript(&'a crate::ExprSubscript), - ExprStarred(&'a crate::ExprStarred), - ExprName(&'a crate::ExprName), - ExprList(&'a crate::ExprList), - ExprTuple(&'a crate::ExprTuple), - ExprSlice(&'a crate::ExprSlice), - ExprIpyEscapeCommand(&'a crate::ExprIpyEscapeCommand), - ExceptHandlerExceptHandler(&'a crate::ExceptHandlerExceptHandler), - FStringExpressionElement(&'a crate::FStringExpressionElement), - FStringLiteralElement(&'a crate::FStringLiteralElement), - PatternMatchValue(&'a crate::PatternMatchValue), - PatternMatchSingleton(&'a crate::PatternMatchSingleton), - PatternMatchSequence(&'a crate::PatternMatchSequence), - PatternMatchMapping(&'a crate::PatternMatchMapping), - PatternMatchClass(&'a crate::PatternMatchClass), - PatternMatchStar(&'a crate::PatternMatchStar), - PatternMatchAs(&'a crate::PatternMatchAs), - PatternMatchOr(&'a crate::PatternMatchOr), - TypeParamTypeVar(&'a crate::TypeParamTypeVar), - TypeParamTypeVarTuple(&'a crate::TypeParamTypeVarTuple), - TypeParamParamSpec(&'a crate::TypeParamParamSpec), - FStringFormatSpec(&'a crate::FStringFormatSpec), - PatternArguments(&'a crate::PatternArguments), - PatternKeyword(&'a crate::PatternKeyword), - Comprehension(&'a crate::Comprehension), - Arguments(&'a crate::Arguments), - Parameters(&'a crate::Parameters), - Parameter(&'a crate::Parameter), - ParameterWithDefault(&'a crate::ParameterWithDefault), - Keyword(&'a crate::Keyword), - Alias(&'a crate::Alias), - WithItem(&'a crate::WithItem), - MatchCase(&'a crate::MatchCase), - Decorator(&'a crate::Decorator), - ElifElseClause(&'a crate::ElifElseClause), - TypeParams(&'a crate::TypeParams), - FString(&'a crate::FString), - StringLiteral(&'a crate::StringLiteral), - BytesLiteral(&'a crate::BytesLiteral), - Identifier(&'a crate::Identifier), +impl Pattern { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { + match self { + Pattern::MatchValue(node) => node.visit_source_order(visitor), + Pattern::MatchSingleton(node) => node.visit_source_order(visitor), + Pattern::MatchSequence(node) => node.visit_source_order(visitor), + Pattern::MatchMapping(node) => node.visit_source_order(visitor), + Pattern::MatchClass(node) => node.visit_source_order(visitor), + Pattern::MatchStar(node) => node.visit_source_order(visitor), + Pattern::MatchAs(node) => node.visit_source_order(visitor), + Pattern::MatchOr(node) => node.visit_source_order(visitor), + } + } } -impl<'a> From<&'a Mod> for AnyNodeRef<'a> { - fn from(node: &'a Mod) -> AnyNodeRef<'a> { - match node { - Mod::Module(node) => AnyNodeRef::ModModule(node), - Mod::Expression(node) => AnyNodeRef::ModExpression(node), +impl TypeParam { + #[allow(unused)] + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, + { + match self { + TypeParam::TypeVar(node) => node.visit_source_order(visitor), + TypeParam::TypeVarTuple(node) => node.visit_source_order(visitor), + TypeParam::ParamSpec(node) => node.visit_source_order(visitor), } } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: ModRef<'a>) -> AnyNodeRef<'a> { +/// See also [mod](https://docs.python.org/3/library/ast.html#ast.mod) +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum ModRef<'a> { + Module(&'a crate::ModModule), + Expression(&'a crate::ModExpression), +} + +impl<'a> From<&'a Mod> for ModRef<'a> { + fn from(node: &'a Mod) -> Self { match node { - ModRef::Module(node) => AnyNodeRef::ModModule(node), - ModRef::Expression(node) => AnyNodeRef::ModExpression(node), + Mod::Module(node) => ModRef::Module(node), + Mod::Expression(node) => ModRef::Expression(node), } } } -impl<'a> AnyNodeRef<'a> { - pub fn as_mod_ref(self) -> Option> { - match self { - Self::ModModule(node) => Some(ModRef::Module(node)), - Self::ModExpression(node) => Some(ModRef::Expression(node)), +impl<'a> From<&'a crate::ModModule> for ModRef<'a> { + fn from(node: &'a crate::ModModule) -> Self { + Self::Module(node) + } +} - _ => None, - } +impl<'a> From<&'a crate::ModExpression> for ModRef<'a> { + fn from(node: &'a crate::ModExpression) -> Self { + Self::Expression(node) } } -impl<'a> From<&'a Stmt> for AnyNodeRef<'a> { - fn from(node: &'a Stmt) -> AnyNodeRef<'a> { - match node { - Stmt::FunctionDef(node) => AnyNodeRef::StmtFunctionDef(node), - Stmt::ClassDef(node) => AnyNodeRef::StmtClassDef(node), - Stmt::Return(node) => AnyNodeRef::StmtReturn(node), - Stmt::Delete(node) => AnyNodeRef::StmtDelete(node), - Stmt::TypeAlias(node) => AnyNodeRef::StmtTypeAlias(node), - Stmt::Assign(node) => AnyNodeRef::StmtAssign(node), - Stmt::AugAssign(node) => AnyNodeRef::StmtAugAssign(node), - Stmt::AnnAssign(node) => AnyNodeRef::StmtAnnAssign(node), - Stmt::For(node) => AnyNodeRef::StmtFor(node), - Stmt::While(node) => AnyNodeRef::StmtWhile(node), - Stmt::If(node) => AnyNodeRef::StmtIf(node), - Stmt::With(node) => AnyNodeRef::StmtWith(node), - Stmt::Match(node) => AnyNodeRef::StmtMatch(node), - Stmt::Raise(node) => AnyNodeRef::StmtRaise(node), - Stmt::Try(node) => AnyNodeRef::StmtTry(node), - Stmt::Assert(node) => AnyNodeRef::StmtAssert(node), - Stmt::Import(node) => AnyNodeRef::StmtImport(node), - Stmt::ImportFrom(node) => AnyNodeRef::StmtImportFrom(node), - Stmt::Global(node) => AnyNodeRef::StmtGlobal(node), - Stmt::Nonlocal(node) => AnyNodeRef::StmtNonlocal(node), - Stmt::Expr(node) => AnyNodeRef::StmtExpr(node), - Stmt::Pass(node) => AnyNodeRef::StmtPass(node), - Stmt::Break(node) => AnyNodeRef::StmtBreak(node), - Stmt::Continue(node) => AnyNodeRef::StmtContinue(node), - Stmt::IpyEscapeCommand(node) => AnyNodeRef::StmtIpyEscapeCommand(node), +impl ruff_text_size::Ranged for ModRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::Module(node) => node.range(), + Self::Expression(node) => node.range(), } } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: StmtRef<'a>) -> AnyNodeRef<'a> { - match node { - StmtRef::FunctionDef(node) => AnyNodeRef::StmtFunctionDef(node), - StmtRef::ClassDef(node) => AnyNodeRef::StmtClassDef(node), - StmtRef::Return(node) => AnyNodeRef::StmtReturn(node), - StmtRef::Delete(node) => AnyNodeRef::StmtDelete(node), - StmtRef::TypeAlias(node) => AnyNodeRef::StmtTypeAlias(node), - StmtRef::Assign(node) => AnyNodeRef::StmtAssign(node), - StmtRef::AugAssign(node) => AnyNodeRef::StmtAugAssign(node), - StmtRef::AnnAssign(node) => AnyNodeRef::StmtAnnAssign(node), - StmtRef::For(node) => AnyNodeRef::StmtFor(node), - StmtRef::While(node) => AnyNodeRef::StmtWhile(node), - StmtRef::If(node) => AnyNodeRef::StmtIf(node), - StmtRef::With(node) => AnyNodeRef::StmtWith(node), - StmtRef::Match(node) => AnyNodeRef::StmtMatch(node), - StmtRef::Raise(node) => AnyNodeRef::StmtRaise(node), - StmtRef::Try(node) => AnyNodeRef::StmtTry(node), - StmtRef::Assert(node) => AnyNodeRef::StmtAssert(node), - StmtRef::Import(node) => AnyNodeRef::StmtImport(node), - StmtRef::ImportFrom(node) => AnyNodeRef::StmtImportFrom(node), - StmtRef::Global(node) => AnyNodeRef::StmtGlobal(node), - StmtRef::Nonlocal(node) => AnyNodeRef::StmtNonlocal(node), - StmtRef::Expr(node) => AnyNodeRef::StmtExpr(node), - StmtRef::Pass(node) => AnyNodeRef::StmtPass(node), - StmtRef::Break(node) => AnyNodeRef::StmtBreak(node), - StmtRef::Continue(node) => AnyNodeRef::StmtContinue(node), - StmtRef::IpyEscapeCommand(node) => AnyNodeRef::StmtIpyEscapeCommand(node), +impl crate::HasNodeIndex for ModRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::Module(node) => node.node_index(), + Self::Expression(node) => node.node_index(), } } } -impl<'a> AnyNodeRef<'a> { - pub fn as_stmt_ref(self) -> Option> { - match self { - Self::StmtFunctionDef(node) => Some(StmtRef::FunctionDef(node)), - Self::StmtClassDef(node) => Some(StmtRef::ClassDef(node)), - Self::StmtReturn(node) => Some(StmtRef::Return(node)), - Self::StmtDelete(node) => Some(StmtRef::Delete(node)), - Self::StmtTypeAlias(node) => Some(StmtRef::TypeAlias(node)), - Self::StmtAssign(node) => Some(StmtRef::Assign(node)), - Self::StmtAugAssign(node) => Some(StmtRef::AugAssign(node)), - Self::StmtAnnAssign(node) => Some(StmtRef::AnnAssign(node)), - Self::StmtFor(node) => Some(StmtRef::For(node)), - Self::StmtWhile(node) => Some(StmtRef::While(node)), - Self::StmtIf(node) => Some(StmtRef::If(node)), - Self::StmtWith(node) => Some(StmtRef::With(node)), - Self::StmtMatch(node) => Some(StmtRef::Match(node)), - Self::StmtRaise(node) => Some(StmtRef::Raise(node)), - Self::StmtTry(node) => Some(StmtRef::Try(node)), - Self::StmtAssert(node) => Some(StmtRef::Assert(node)), - Self::StmtImport(node) => Some(StmtRef::Import(node)), - Self::StmtImportFrom(node) => Some(StmtRef::ImportFrom(node)), - Self::StmtGlobal(node) => Some(StmtRef::Global(node)), - Self::StmtNonlocal(node) => Some(StmtRef::Nonlocal(node)), - Self::StmtExpr(node) => Some(StmtRef::Expr(node)), - Self::StmtPass(node) => Some(StmtRef::Pass(node)), - Self::StmtBreak(node) => Some(StmtRef::Break(node)), - Self::StmtContinue(node) => Some(StmtRef::Continue(node)), - Self::StmtIpyEscapeCommand(node) => Some(StmtRef::IpyEscapeCommand(node)), +/// See also [stmt](https://docs.python.org/3/library/ast.html#ast.stmt) +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum StmtRef<'a> { + #[is(name = "function_def_stmt")] + FunctionDef(&'a crate::StmtFunctionDef), + #[is(name = "class_def_stmt")] + ClassDef(&'a crate::StmtClassDef), + #[is(name = "return_stmt")] + Return(&'a crate::StmtReturn), + #[is(name = "delete_stmt")] + Delete(&'a crate::StmtDelete), + #[is(name = "type_alias_stmt")] + TypeAlias(&'a crate::StmtTypeAlias), + #[is(name = "assign_stmt")] + Assign(&'a crate::StmtAssign), + #[is(name = "aug_assign_stmt")] + AugAssign(&'a crate::StmtAugAssign), + #[is(name = "ann_assign_stmt")] + AnnAssign(&'a crate::StmtAnnAssign), + #[is(name = "for_stmt")] + For(&'a crate::StmtFor), + #[is(name = "while_stmt")] + While(&'a crate::StmtWhile), + #[is(name = "if_stmt")] + If(&'a crate::StmtIf), + #[is(name = "with_stmt")] + With(&'a crate::StmtWith), + #[is(name = "match_stmt")] + Match(&'a crate::StmtMatch), + #[is(name = "raise_stmt")] + Raise(&'a crate::StmtRaise), + #[is(name = "try_stmt")] + Try(&'a crate::StmtTry), + #[is(name = "assert_stmt")] + Assert(&'a crate::StmtAssert), + #[is(name = "import_stmt")] + Import(&'a crate::StmtImport), + #[is(name = "import_from_stmt")] + ImportFrom(&'a crate::StmtImportFrom), + #[is(name = "global_stmt")] + Global(&'a crate::StmtGlobal), + #[is(name = "nonlocal_stmt")] + Nonlocal(&'a crate::StmtNonlocal), + #[is(name = "expr_stmt")] + Expr(&'a crate::StmtExpr), + #[is(name = "pass_stmt")] + Pass(&'a crate::StmtPass), + #[is(name = "break_stmt")] + Break(&'a crate::StmtBreak), + #[is(name = "continue_stmt")] + Continue(&'a crate::StmtContinue), + #[is(name = "ipy_escape_command_stmt")] + IpyEscapeCommand(&'a crate::StmtIpyEscapeCommand), +} - _ => None, +impl<'a> From<&'a Stmt> for StmtRef<'a> { + fn from(node: &'a Stmt) -> Self { + match node { + Stmt::FunctionDef(node) => StmtRef::FunctionDef(node), + Stmt::ClassDef(node) => StmtRef::ClassDef(node), + Stmt::Return(node) => StmtRef::Return(node), + Stmt::Delete(node) => StmtRef::Delete(node), + Stmt::TypeAlias(node) => StmtRef::TypeAlias(node), + Stmt::Assign(node) => StmtRef::Assign(node), + Stmt::AugAssign(node) => StmtRef::AugAssign(node), + Stmt::AnnAssign(node) => StmtRef::AnnAssign(node), + Stmt::For(node) => StmtRef::For(node), + Stmt::While(node) => StmtRef::While(node), + Stmt::If(node) => StmtRef::If(node), + Stmt::With(node) => StmtRef::With(node), + Stmt::Match(node) => StmtRef::Match(node), + Stmt::Raise(node) => StmtRef::Raise(node), + Stmt::Try(node) => StmtRef::Try(node), + Stmt::Assert(node) => StmtRef::Assert(node), + Stmt::Import(node) => StmtRef::Import(node), + Stmt::ImportFrom(node) => StmtRef::ImportFrom(node), + Stmt::Global(node) => StmtRef::Global(node), + Stmt::Nonlocal(node) => StmtRef::Nonlocal(node), + Stmt::Expr(node) => StmtRef::Expr(node), + Stmt::Pass(node) => StmtRef::Pass(node), + Stmt::Break(node) => StmtRef::Break(node), + Stmt::Continue(node) => StmtRef::Continue(node), + Stmt::IpyEscapeCommand(node) => StmtRef::IpyEscapeCommand(node), } } } -impl<'a> From<&'a Expr> for AnyNodeRef<'a> { - fn from(node: &'a Expr) -> AnyNodeRef<'a> { - match node { - Expr::BoolOp(node) => AnyNodeRef::ExprBoolOp(node), - Expr::Named(node) => AnyNodeRef::ExprNamed(node), - Expr::BinOp(node) => AnyNodeRef::ExprBinOp(node), - Expr::UnaryOp(node) => AnyNodeRef::ExprUnaryOp(node), - Expr::Lambda(node) => AnyNodeRef::ExprLambda(node), - Expr::If(node) => AnyNodeRef::ExprIf(node), - Expr::Dict(node) => AnyNodeRef::ExprDict(node), - Expr::Set(node) => AnyNodeRef::ExprSet(node), - Expr::ListComp(node) => AnyNodeRef::ExprListComp(node), - Expr::SetComp(node) => AnyNodeRef::ExprSetComp(node), - Expr::DictComp(node) => AnyNodeRef::ExprDictComp(node), - Expr::Generator(node) => AnyNodeRef::ExprGenerator(node), - Expr::Await(node) => AnyNodeRef::ExprAwait(node), - Expr::Yield(node) => AnyNodeRef::ExprYield(node), - Expr::YieldFrom(node) => AnyNodeRef::ExprYieldFrom(node), - Expr::Compare(node) => AnyNodeRef::ExprCompare(node), - Expr::Call(node) => AnyNodeRef::ExprCall(node), - Expr::FString(node) => AnyNodeRef::ExprFString(node), - Expr::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node), - Expr::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node), - Expr::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node), - Expr::BooleanLiteral(node) => AnyNodeRef::ExprBooleanLiteral(node), - Expr::NoneLiteral(node) => AnyNodeRef::ExprNoneLiteral(node), - Expr::EllipsisLiteral(node) => AnyNodeRef::ExprEllipsisLiteral(node), - Expr::Attribute(node) => AnyNodeRef::ExprAttribute(node), - Expr::Subscript(node) => AnyNodeRef::ExprSubscript(node), - Expr::Starred(node) => AnyNodeRef::ExprStarred(node), - Expr::Name(node) => AnyNodeRef::ExprName(node), - Expr::List(node) => AnyNodeRef::ExprList(node), - Expr::Tuple(node) => AnyNodeRef::ExprTuple(node), - Expr::Slice(node) => AnyNodeRef::ExprSlice(node), - Expr::IpyEscapeCommand(node) => AnyNodeRef::ExprIpyEscapeCommand(node), - } +impl<'a> From<&'a crate::StmtFunctionDef> for StmtRef<'a> { + fn from(node: &'a crate::StmtFunctionDef) -> Self { + Self::FunctionDef(node) } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: ExprRef<'a>) -> AnyNodeRef<'a> { - match node { - ExprRef::BoolOp(node) => AnyNodeRef::ExprBoolOp(node), - ExprRef::Named(node) => AnyNodeRef::ExprNamed(node), - ExprRef::BinOp(node) => AnyNodeRef::ExprBinOp(node), - ExprRef::UnaryOp(node) => AnyNodeRef::ExprUnaryOp(node), - ExprRef::Lambda(node) => AnyNodeRef::ExprLambda(node), - ExprRef::If(node) => AnyNodeRef::ExprIf(node), - ExprRef::Dict(node) => AnyNodeRef::ExprDict(node), - ExprRef::Set(node) => AnyNodeRef::ExprSet(node), - ExprRef::ListComp(node) => AnyNodeRef::ExprListComp(node), - ExprRef::SetComp(node) => AnyNodeRef::ExprSetComp(node), - ExprRef::DictComp(node) => AnyNodeRef::ExprDictComp(node), - ExprRef::Generator(node) => AnyNodeRef::ExprGenerator(node), - ExprRef::Await(node) => AnyNodeRef::ExprAwait(node), - ExprRef::Yield(node) => AnyNodeRef::ExprYield(node), - ExprRef::YieldFrom(node) => AnyNodeRef::ExprYieldFrom(node), - ExprRef::Compare(node) => AnyNodeRef::ExprCompare(node), - ExprRef::Call(node) => AnyNodeRef::ExprCall(node), - ExprRef::FString(node) => AnyNodeRef::ExprFString(node), - ExprRef::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node), - ExprRef::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node), - ExprRef::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node), - ExprRef::BooleanLiteral(node) => AnyNodeRef::ExprBooleanLiteral(node), - ExprRef::NoneLiteral(node) => AnyNodeRef::ExprNoneLiteral(node), - ExprRef::EllipsisLiteral(node) => AnyNodeRef::ExprEllipsisLiteral(node), - ExprRef::Attribute(node) => AnyNodeRef::ExprAttribute(node), - ExprRef::Subscript(node) => AnyNodeRef::ExprSubscript(node), - ExprRef::Starred(node) => AnyNodeRef::ExprStarred(node), - ExprRef::Name(node) => AnyNodeRef::ExprName(node), - ExprRef::List(node) => AnyNodeRef::ExprList(node), - ExprRef::Tuple(node) => AnyNodeRef::ExprTuple(node), - ExprRef::Slice(node) => AnyNodeRef::ExprSlice(node), - ExprRef::IpyEscapeCommand(node) => AnyNodeRef::ExprIpyEscapeCommand(node), - } +impl<'a> From<&'a crate::StmtClassDef> for StmtRef<'a> { + fn from(node: &'a crate::StmtClassDef) -> Self { + Self::ClassDef(node) } } -impl<'a> AnyNodeRef<'a> { - pub fn as_expr_ref(self) -> Option> { - match self { - Self::ExprBoolOp(node) => Some(ExprRef::BoolOp(node)), - Self::ExprNamed(node) => Some(ExprRef::Named(node)), - Self::ExprBinOp(node) => Some(ExprRef::BinOp(node)), - Self::ExprUnaryOp(node) => Some(ExprRef::UnaryOp(node)), - Self::ExprLambda(node) => Some(ExprRef::Lambda(node)), - Self::ExprIf(node) => Some(ExprRef::If(node)), - Self::ExprDict(node) => Some(ExprRef::Dict(node)), - Self::ExprSet(node) => Some(ExprRef::Set(node)), - Self::ExprListComp(node) => Some(ExprRef::ListComp(node)), - Self::ExprSetComp(node) => Some(ExprRef::SetComp(node)), - Self::ExprDictComp(node) => Some(ExprRef::DictComp(node)), - Self::ExprGenerator(node) => Some(ExprRef::Generator(node)), - Self::ExprAwait(node) => Some(ExprRef::Await(node)), - Self::ExprYield(node) => Some(ExprRef::Yield(node)), - Self::ExprYieldFrom(node) => Some(ExprRef::YieldFrom(node)), - Self::ExprCompare(node) => Some(ExprRef::Compare(node)), - Self::ExprCall(node) => Some(ExprRef::Call(node)), - Self::ExprFString(node) => Some(ExprRef::FString(node)), - Self::ExprStringLiteral(node) => Some(ExprRef::StringLiteral(node)), - Self::ExprBytesLiteral(node) => Some(ExprRef::BytesLiteral(node)), - Self::ExprNumberLiteral(node) => Some(ExprRef::NumberLiteral(node)), - Self::ExprBooleanLiteral(node) => Some(ExprRef::BooleanLiteral(node)), - Self::ExprNoneLiteral(node) => Some(ExprRef::NoneLiteral(node)), - Self::ExprEllipsisLiteral(node) => Some(ExprRef::EllipsisLiteral(node)), - Self::ExprAttribute(node) => Some(ExprRef::Attribute(node)), - Self::ExprSubscript(node) => Some(ExprRef::Subscript(node)), - Self::ExprStarred(node) => Some(ExprRef::Starred(node)), - Self::ExprName(node) => Some(ExprRef::Name(node)), - Self::ExprList(node) => Some(ExprRef::List(node)), - Self::ExprTuple(node) => Some(ExprRef::Tuple(node)), - Self::ExprSlice(node) => Some(ExprRef::Slice(node)), - Self::ExprIpyEscapeCommand(node) => Some(ExprRef::IpyEscapeCommand(node)), - - _ => None, - } +impl<'a> From<&'a crate::StmtReturn> for StmtRef<'a> { + fn from(node: &'a crate::StmtReturn) -> Self { + Self::Return(node) } } -impl<'a> From<&'a ExceptHandler> for AnyNodeRef<'a> { - fn from(node: &'a ExceptHandler) -> AnyNodeRef<'a> { - match node { - ExceptHandler::ExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node), - } +impl<'a> From<&'a crate::StmtDelete> for StmtRef<'a> { + fn from(node: &'a crate::StmtDelete) -> Self { + Self::Delete(node) } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: ExceptHandlerRef<'a>) -> AnyNodeRef<'a> { - match node { - ExceptHandlerRef::ExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node), - } +impl<'a> From<&'a crate::StmtTypeAlias> for StmtRef<'a> { + fn from(node: &'a crate::StmtTypeAlias) -> Self { + Self::TypeAlias(node) } } -impl<'a> AnyNodeRef<'a> { - pub fn as_except_handler_ref(self) -> Option> { - match self { - Self::ExceptHandlerExceptHandler(node) => Some(ExceptHandlerRef::ExceptHandler(node)), - - _ => None, - } +impl<'a> From<&'a crate::StmtAssign> for StmtRef<'a> { + fn from(node: &'a crate::StmtAssign) -> Self { + Self::Assign(node) } } -impl<'a> From<&'a FStringElement> for AnyNodeRef<'a> { - fn from(node: &'a FStringElement) -> AnyNodeRef<'a> { - match node { - FStringElement::Expression(node) => AnyNodeRef::FStringExpressionElement(node), - FStringElement::Literal(node) => AnyNodeRef::FStringLiteralElement(node), - } +impl<'a> From<&'a crate::StmtAugAssign> for StmtRef<'a> { + fn from(node: &'a crate::StmtAugAssign) -> Self { + Self::AugAssign(node) } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: FStringElementRef<'a>) -> AnyNodeRef<'a> { - match node { - FStringElementRef::Expression(node) => AnyNodeRef::FStringExpressionElement(node), - FStringElementRef::Literal(node) => AnyNodeRef::FStringLiteralElement(node), - } +impl<'a> From<&'a crate::StmtAnnAssign> for StmtRef<'a> { + fn from(node: &'a crate::StmtAnnAssign) -> Self { + Self::AnnAssign(node) } } -impl<'a> AnyNodeRef<'a> { - pub fn as_f_string_element_ref(self) -> Option> { - match self { - Self::FStringExpressionElement(node) => Some(FStringElementRef::Expression(node)), - Self::FStringLiteralElement(node) => Some(FStringElementRef::Literal(node)), - - _ => None, - } +impl<'a> From<&'a crate::StmtFor> for StmtRef<'a> { + fn from(node: &'a crate::StmtFor) -> Self { + Self::For(node) } } -impl<'a> From<&'a Pattern> for AnyNodeRef<'a> { - fn from(node: &'a Pattern) -> AnyNodeRef<'a> { - match node { - Pattern::MatchValue(node) => AnyNodeRef::PatternMatchValue(node), - Pattern::MatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node), - Pattern::MatchSequence(node) => AnyNodeRef::PatternMatchSequence(node), - Pattern::MatchMapping(node) => AnyNodeRef::PatternMatchMapping(node), - Pattern::MatchClass(node) => AnyNodeRef::PatternMatchClass(node), - Pattern::MatchStar(node) => AnyNodeRef::PatternMatchStar(node), - Pattern::MatchAs(node) => AnyNodeRef::PatternMatchAs(node), - Pattern::MatchOr(node) => AnyNodeRef::PatternMatchOr(node), - } +impl<'a> From<&'a crate::StmtWhile> for StmtRef<'a> { + fn from(node: &'a crate::StmtWhile) -> Self { + Self::While(node) } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: PatternRef<'a>) -> AnyNodeRef<'a> { - match node { - PatternRef::MatchValue(node) => AnyNodeRef::PatternMatchValue(node), - PatternRef::MatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node), - PatternRef::MatchSequence(node) => AnyNodeRef::PatternMatchSequence(node), - PatternRef::MatchMapping(node) => AnyNodeRef::PatternMatchMapping(node), - PatternRef::MatchClass(node) => AnyNodeRef::PatternMatchClass(node), - PatternRef::MatchStar(node) => AnyNodeRef::PatternMatchStar(node), - PatternRef::MatchAs(node) => AnyNodeRef::PatternMatchAs(node), - PatternRef::MatchOr(node) => AnyNodeRef::PatternMatchOr(node), - } +impl<'a> From<&'a crate::StmtIf> for StmtRef<'a> { + fn from(node: &'a crate::StmtIf) -> Self { + Self::If(node) } } -impl<'a> AnyNodeRef<'a> { - pub fn as_pattern_ref(self) -> Option> { - match self { - Self::PatternMatchValue(node) => Some(PatternRef::MatchValue(node)), - Self::PatternMatchSingleton(node) => Some(PatternRef::MatchSingleton(node)), - Self::PatternMatchSequence(node) => Some(PatternRef::MatchSequence(node)), - Self::PatternMatchMapping(node) => Some(PatternRef::MatchMapping(node)), - Self::PatternMatchClass(node) => Some(PatternRef::MatchClass(node)), - Self::PatternMatchStar(node) => Some(PatternRef::MatchStar(node)), - Self::PatternMatchAs(node) => Some(PatternRef::MatchAs(node)), - Self::PatternMatchOr(node) => Some(PatternRef::MatchOr(node)), - - _ => None, - } +impl<'a> From<&'a crate::StmtWith> for StmtRef<'a> { + fn from(node: &'a crate::StmtWith) -> Self { + Self::With(node) } } -impl<'a> From<&'a TypeParam> for AnyNodeRef<'a> { - fn from(node: &'a TypeParam) -> AnyNodeRef<'a> { - match node { - TypeParam::TypeVar(node) => AnyNodeRef::TypeParamTypeVar(node), - TypeParam::TypeVarTuple(node) => AnyNodeRef::TypeParamTypeVarTuple(node), - TypeParam::ParamSpec(node) => AnyNodeRef::TypeParamParamSpec(node), - } +impl<'a> From<&'a crate::StmtMatch> for StmtRef<'a> { + fn from(node: &'a crate::StmtMatch) -> Self { + Self::Match(node) } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: TypeParamRef<'a>) -> AnyNodeRef<'a> { - match node { - TypeParamRef::TypeVar(node) => AnyNodeRef::TypeParamTypeVar(node), - TypeParamRef::TypeVarTuple(node) => AnyNodeRef::TypeParamTypeVarTuple(node), - TypeParamRef::ParamSpec(node) => AnyNodeRef::TypeParamParamSpec(node), - } +impl<'a> From<&'a crate::StmtRaise> for StmtRef<'a> { + fn from(node: &'a crate::StmtRaise) -> Self { + Self::Raise(node) } } -impl<'a> AnyNodeRef<'a> { - pub fn as_type_param_ref(self) -> Option> { - match self { - Self::TypeParamTypeVar(node) => Some(TypeParamRef::TypeVar(node)), - Self::TypeParamTypeVarTuple(node) => Some(TypeParamRef::TypeVarTuple(node)), - Self::TypeParamParamSpec(node) => Some(TypeParamRef::ParamSpec(node)), - - _ => None, - } +impl<'a> From<&'a crate::StmtTry> for StmtRef<'a> { + fn from(node: &'a crate::StmtTry) -> Self { + Self::Try(node) } } -impl<'a> From<&'a crate::ModModule> for AnyNodeRef<'a> { - fn from(node: &'a crate::ModModule) -> AnyNodeRef<'a> { - AnyNodeRef::ModModule(node) +impl<'a> From<&'a crate::StmtAssert> for StmtRef<'a> { + fn from(node: &'a crate::StmtAssert) -> Self { + Self::Assert(node) } } -impl<'a> From<&'a crate::ModExpression> for AnyNodeRef<'a> { - fn from(node: &'a crate::ModExpression) -> AnyNodeRef<'a> { - AnyNodeRef::ModExpression(node) +impl<'a> From<&'a crate::StmtImport> for StmtRef<'a> { + fn from(node: &'a crate::StmtImport) -> Self { + Self::Import(node) } } -impl<'a> From<&'a crate::StmtFunctionDef> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtFunctionDef) -> AnyNodeRef<'a> { - AnyNodeRef::StmtFunctionDef(node) +impl<'a> From<&'a crate::StmtImportFrom> for StmtRef<'a> { + fn from(node: &'a crate::StmtImportFrom) -> Self { + Self::ImportFrom(node) } } -impl<'a> From<&'a crate::StmtClassDef> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtClassDef) -> AnyNodeRef<'a> { - AnyNodeRef::StmtClassDef(node) +impl<'a> From<&'a crate::StmtGlobal> for StmtRef<'a> { + fn from(node: &'a crate::StmtGlobal) -> Self { + Self::Global(node) } } -impl<'a> From<&'a crate::StmtReturn> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtReturn) -> AnyNodeRef<'a> { - AnyNodeRef::StmtReturn(node) +impl<'a> From<&'a crate::StmtNonlocal> for StmtRef<'a> { + fn from(node: &'a crate::StmtNonlocal) -> Self { + Self::Nonlocal(node) } } -impl<'a> From<&'a crate::StmtDelete> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtDelete) -> AnyNodeRef<'a> { - AnyNodeRef::StmtDelete(node) +impl<'a> From<&'a crate::StmtExpr> for StmtRef<'a> { + fn from(node: &'a crate::StmtExpr) -> Self { + Self::Expr(node) } } -impl<'a> From<&'a crate::StmtTypeAlias> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtTypeAlias) -> AnyNodeRef<'a> { - AnyNodeRef::StmtTypeAlias(node) +impl<'a> From<&'a crate::StmtPass> for StmtRef<'a> { + fn from(node: &'a crate::StmtPass) -> Self { + Self::Pass(node) } } -impl<'a> From<&'a crate::StmtAssign> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtAssign) -> AnyNodeRef<'a> { - AnyNodeRef::StmtAssign(node) +impl<'a> From<&'a crate::StmtBreak> for StmtRef<'a> { + fn from(node: &'a crate::StmtBreak) -> Self { + Self::Break(node) } } -impl<'a> From<&'a crate::StmtAugAssign> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtAugAssign) -> AnyNodeRef<'a> { - AnyNodeRef::StmtAugAssign(node) +impl<'a> From<&'a crate::StmtContinue> for StmtRef<'a> { + fn from(node: &'a crate::StmtContinue) -> Self { + Self::Continue(node) } } -impl<'a> From<&'a crate::StmtAnnAssign> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtAnnAssign) -> AnyNodeRef<'a> { - AnyNodeRef::StmtAnnAssign(node) +impl<'a> From<&'a crate::StmtIpyEscapeCommand> for StmtRef<'a> { + fn from(node: &'a crate::StmtIpyEscapeCommand) -> Self { + Self::IpyEscapeCommand(node) } } -impl<'a> From<&'a crate::StmtFor> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtFor) -> AnyNodeRef<'a> { - AnyNodeRef::StmtFor(node) +impl ruff_text_size::Ranged for StmtRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::FunctionDef(node) => node.range(), + Self::ClassDef(node) => node.range(), + Self::Return(node) => node.range(), + Self::Delete(node) => node.range(), + Self::TypeAlias(node) => node.range(), + Self::Assign(node) => node.range(), + Self::AugAssign(node) => node.range(), + Self::AnnAssign(node) => node.range(), + Self::For(node) => node.range(), + Self::While(node) => node.range(), + Self::If(node) => node.range(), + Self::With(node) => node.range(), + Self::Match(node) => node.range(), + Self::Raise(node) => node.range(), + Self::Try(node) => node.range(), + Self::Assert(node) => node.range(), + Self::Import(node) => node.range(), + Self::ImportFrom(node) => node.range(), + Self::Global(node) => node.range(), + Self::Nonlocal(node) => node.range(), + Self::Expr(node) => node.range(), + Self::Pass(node) => node.range(), + Self::Break(node) => node.range(), + Self::Continue(node) => node.range(), + Self::IpyEscapeCommand(node) => node.range(), + } } } -impl<'a> From<&'a crate::StmtWhile> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtWhile) -> AnyNodeRef<'a> { - AnyNodeRef::StmtWhile(node) +impl crate::HasNodeIndex for StmtRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::FunctionDef(node) => node.node_index(), + Self::ClassDef(node) => node.node_index(), + Self::Return(node) => node.node_index(), + Self::Delete(node) => node.node_index(), + Self::TypeAlias(node) => node.node_index(), + Self::Assign(node) => node.node_index(), + Self::AugAssign(node) => node.node_index(), + Self::AnnAssign(node) => node.node_index(), + Self::For(node) => node.node_index(), + Self::While(node) => node.node_index(), + Self::If(node) => node.node_index(), + Self::With(node) => node.node_index(), + Self::Match(node) => node.node_index(), + Self::Raise(node) => node.node_index(), + Self::Try(node) => node.node_index(), + Self::Assert(node) => node.node_index(), + Self::Import(node) => node.node_index(), + Self::ImportFrom(node) => node.node_index(), + Self::Global(node) => node.node_index(), + Self::Nonlocal(node) => node.node_index(), + Self::Expr(node) => node.node_index(), + Self::Pass(node) => node.node_index(), + Self::Break(node) => node.node_index(), + Self::Continue(node) => node.node_index(), + Self::IpyEscapeCommand(node) => node.node_index(), + } } } -impl<'a> From<&'a crate::StmtIf> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtIf) -> AnyNodeRef<'a> { - AnyNodeRef::StmtIf(node) - } +/// See also [expr](https://docs.python.org/3/library/ast.html#ast.expr) +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum ExprRef<'a> { + #[is(name = "bool_op_expr")] + BoolOp(&'a crate::ExprBoolOp), + #[is(name = "named_expr")] + Named(&'a crate::ExprNamed), + #[is(name = "bin_op_expr")] + BinOp(&'a crate::ExprBinOp), + #[is(name = "unary_op_expr")] + UnaryOp(&'a crate::ExprUnaryOp), + #[is(name = "lambda_expr")] + Lambda(&'a crate::ExprLambda), + #[is(name = "if_expr")] + If(&'a crate::ExprIf), + #[is(name = "dict_expr")] + Dict(&'a crate::ExprDict), + #[is(name = "set_expr")] + Set(&'a crate::ExprSet), + #[is(name = "list_comp_expr")] + ListComp(&'a crate::ExprListComp), + #[is(name = "set_comp_expr")] + SetComp(&'a crate::ExprSetComp), + #[is(name = "dict_comp_expr")] + DictComp(&'a crate::ExprDictComp), + #[is(name = "generator_expr")] + Generator(&'a crate::ExprGenerator), + #[is(name = "await_expr")] + Await(&'a crate::ExprAwait), + #[is(name = "yield_expr")] + Yield(&'a crate::ExprYield), + #[is(name = "yield_from_expr")] + YieldFrom(&'a crate::ExprYieldFrom), + #[is(name = "compare_expr")] + Compare(&'a crate::ExprCompare), + #[is(name = "call_expr")] + Call(&'a crate::ExprCall), + #[is(name = "f_string_expr")] + FString(&'a crate::ExprFString), + #[is(name = "t_string_expr")] + TString(&'a crate::ExprTString), + #[is(name = "string_literal_expr")] + StringLiteral(&'a crate::ExprStringLiteral), + #[is(name = "bytes_literal_expr")] + BytesLiteral(&'a crate::ExprBytesLiteral), + #[is(name = "number_literal_expr")] + NumberLiteral(&'a crate::ExprNumberLiteral), + #[is(name = "boolean_literal_expr")] + BooleanLiteral(&'a crate::ExprBooleanLiteral), + #[is(name = "none_literal_expr")] + NoneLiteral(&'a crate::ExprNoneLiteral), + #[is(name = "ellipsis_literal_expr")] + EllipsisLiteral(&'a crate::ExprEllipsisLiteral), + #[is(name = "attribute_expr")] + Attribute(&'a crate::ExprAttribute), + #[is(name = "subscript_expr")] + Subscript(&'a crate::ExprSubscript), + #[is(name = "starred_expr")] + Starred(&'a crate::ExprStarred), + #[is(name = "name_expr")] + Name(&'a crate::ExprName), + #[is(name = "list_expr")] + List(&'a crate::ExprList), + #[is(name = "tuple_expr")] + Tuple(&'a crate::ExprTuple), + #[is(name = "slice_expr")] + Slice(&'a crate::ExprSlice), + #[is(name = "ipy_escape_command_expr")] + IpyEscapeCommand(&'a crate::ExprIpyEscapeCommand), } -impl<'a> From<&'a crate::StmtWith> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtWith) -> AnyNodeRef<'a> { - AnyNodeRef::StmtWith(node) +impl<'a> From<&'a Expr> for ExprRef<'a> { + fn from(node: &'a Expr) -> Self { + match node { + Expr::BoolOp(node) => ExprRef::BoolOp(node), + Expr::Named(node) => ExprRef::Named(node), + Expr::BinOp(node) => ExprRef::BinOp(node), + Expr::UnaryOp(node) => ExprRef::UnaryOp(node), + Expr::Lambda(node) => ExprRef::Lambda(node), + Expr::If(node) => ExprRef::If(node), + Expr::Dict(node) => ExprRef::Dict(node), + Expr::Set(node) => ExprRef::Set(node), + Expr::ListComp(node) => ExprRef::ListComp(node), + Expr::SetComp(node) => ExprRef::SetComp(node), + Expr::DictComp(node) => ExprRef::DictComp(node), + Expr::Generator(node) => ExprRef::Generator(node), + Expr::Await(node) => ExprRef::Await(node), + Expr::Yield(node) => ExprRef::Yield(node), + Expr::YieldFrom(node) => ExprRef::YieldFrom(node), + Expr::Compare(node) => ExprRef::Compare(node), + Expr::Call(node) => ExprRef::Call(node), + Expr::FString(node) => ExprRef::FString(node), + Expr::TString(node) => ExprRef::TString(node), + Expr::StringLiteral(node) => ExprRef::StringLiteral(node), + Expr::BytesLiteral(node) => ExprRef::BytesLiteral(node), + Expr::NumberLiteral(node) => ExprRef::NumberLiteral(node), + Expr::BooleanLiteral(node) => ExprRef::BooleanLiteral(node), + Expr::NoneLiteral(node) => ExprRef::NoneLiteral(node), + Expr::EllipsisLiteral(node) => ExprRef::EllipsisLiteral(node), + Expr::Attribute(node) => ExprRef::Attribute(node), + Expr::Subscript(node) => ExprRef::Subscript(node), + Expr::Starred(node) => ExprRef::Starred(node), + Expr::Name(node) => ExprRef::Name(node), + Expr::List(node) => ExprRef::List(node), + Expr::Tuple(node) => ExprRef::Tuple(node), + Expr::Slice(node) => ExprRef::Slice(node), + Expr::IpyEscapeCommand(node) => ExprRef::IpyEscapeCommand(node), + } } } -impl<'a> From<&'a crate::StmtMatch> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtMatch) -> AnyNodeRef<'a> { - AnyNodeRef::StmtMatch(node) +impl<'a> From<&'a crate::ExprBoolOp> for ExprRef<'a> { + fn from(node: &'a crate::ExprBoolOp) -> Self { + Self::BoolOp(node) } } -impl<'a> From<&'a crate::StmtRaise> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtRaise) -> AnyNodeRef<'a> { - AnyNodeRef::StmtRaise(node) +impl<'a> From<&'a crate::ExprNamed> for ExprRef<'a> { + fn from(node: &'a crate::ExprNamed) -> Self { + Self::Named(node) } } -impl<'a> From<&'a crate::StmtTry> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtTry) -> AnyNodeRef<'a> { - AnyNodeRef::StmtTry(node) +impl<'a> From<&'a crate::ExprBinOp> for ExprRef<'a> { + fn from(node: &'a crate::ExprBinOp) -> Self { + Self::BinOp(node) } } -impl<'a> From<&'a crate::StmtAssert> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtAssert) -> AnyNodeRef<'a> { - AnyNodeRef::StmtAssert(node) +impl<'a> From<&'a crate::ExprUnaryOp> for ExprRef<'a> { + fn from(node: &'a crate::ExprUnaryOp) -> Self { + Self::UnaryOp(node) } } -impl<'a> From<&'a crate::StmtImport> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtImport) -> AnyNodeRef<'a> { - AnyNodeRef::StmtImport(node) +impl<'a> From<&'a crate::ExprLambda> for ExprRef<'a> { + fn from(node: &'a crate::ExprLambda) -> Self { + Self::Lambda(node) } } -impl<'a> From<&'a crate::StmtImportFrom> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtImportFrom) -> AnyNodeRef<'a> { - AnyNodeRef::StmtImportFrom(node) +impl<'a> From<&'a crate::ExprIf> for ExprRef<'a> { + fn from(node: &'a crate::ExprIf) -> Self { + Self::If(node) } } -impl<'a> From<&'a crate::StmtGlobal> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtGlobal) -> AnyNodeRef<'a> { - AnyNodeRef::StmtGlobal(node) +impl<'a> From<&'a crate::ExprDict> for ExprRef<'a> { + fn from(node: &'a crate::ExprDict) -> Self { + Self::Dict(node) } } -impl<'a> From<&'a crate::StmtNonlocal> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtNonlocal) -> AnyNodeRef<'a> { - AnyNodeRef::StmtNonlocal(node) +impl<'a> From<&'a crate::ExprSet> for ExprRef<'a> { + fn from(node: &'a crate::ExprSet) -> Self { + Self::Set(node) } } -impl<'a> From<&'a crate::StmtExpr> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtExpr) -> AnyNodeRef<'a> { - AnyNodeRef::StmtExpr(node) +impl<'a> From<&'a crate::ExprListComp> for ExprRef<'a> { + fn from(node: &'a crate::ExprListComp) -> Self { + Self::ListComp(node) } } -impl<'a> From<&'a crate::StmtPass> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtPass) -> AnyNodeRef<'a> { - AnyNodeRef::StmtPass(node) +impl<'a> From<&'a crate::ExprSetComp> for ExprRef<'a> { + fn from(node: &'a crate::ExprSetComp) -> Self { + Self::SetComp(node) } } -impl<'a> From<&'a crate::StmtBreak> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtBreak) -> AnyNodeRef<'a> { - AnyNodeRef::StmtBreak(node) +impl<'a> From<&'a crate::ExprDictComp> for ExprRef<'a> { + fn from(node: &'a crate::ExprDictComp) -> Self { + Self::DictComp(node) } } -impl<'a> From<&'a crate::StmtContinue> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtContinue) -> AnyNodeRef<'a> { - AnyNodeRef::StmtContinue(node) +impl<'a> From<&'a crate::ExprGenerator> for ExprRef<'a> { + fn from(node: &'a crate::ExprGenerator) -> Self { + Self::Generator(node) } } -impl<'a> From<&'a crate::StmtIpyEscapeCommand> for AnyNodeRef<'a> { - fn from(node: &'a crate::StmtIpyEscapeCommand) -> AnyNodeRef<'a> { - AnyNodeRef::StmtIpyEscapeCommand(node) +impl<'a> From<&'a crate::ExprAwait> for ExprRef<'a> { + fn from(node: &'a crate::ExprAwait) -> Self { + Self::Await(node) } } -impl<'a> From<&'a crate::ExprBoolOp> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprBoolOp) -> AnyNodeRef<'a> { - AnyNodeRef::ExprBoolOp(node) +impl<'a> From<&'a crate::ExprYield> for ExprRef<'a> { + fn from(node: &'a crate::ExprYield) -> Self { + Self::Yield(node) } } -impl<'a> From<&'a crate::ExprNamed> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprNamed) -> AnyNodeRef<'a> { - AnyNodeRef::ExprNamed(node) +impl<'a> From<&'a crate::ExprYieldFrom> for ExprRef<'a> { + fn from(node: &'a crate::ExprYieldFrom) -> Self { + Self::YieldFrom(node) } } -impl<'a> From<&'a crate::ExprBinOp> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprBinOp) -> AnyNodeRef<'a> { - AnyNodeRef::ExprBinOp(node) +impl<'a> From<&'a crate::ExprCompare> for ExprRef<'a> { + fn from(node: &'a crate::ExprCompare) -> Self { + Self::Compare(node) } } -impl<'a> From<&'a crate::ExprUnaryOp> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprUnaryOp) -> AnyNodeRef<'a> { - AnyNodeRef::ExprUnaryOp(node) +impl<'a> From<&'a crate::ExprCall> for ExprRef<'a> { + fn from(node: &'a crate::ExprCall) -> Self { + Self::Call(node) } } -impl<'a> From<&'a crate::ExprLambda> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprLambda) -> AnyNodeRef<'a> { - AnyNodeRef::ExprLambda(node) +impl<'a> From<&'a crate::ExprFString> for ExprRef<'a> { + fn from(node: &'a crate::ExprFString) -> Self { + Self::FString(node) } } -impl<'a> From<&'a crate::ExprIf> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprIf) -> AnyNodeRef<'a> { - AnyNodeRef::ExprIf(node) +impl<'a> From<&'a crate::ExprTString> for ExprRef<'a> { + fn from(node: &'a crate::ExprTString) -> Self { + Self::TString(node) } } -impl<'a> From<&'a crate::ExprDict> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprDict) -> AnyNodeRef<'a> { - AnyNodeRef::ExprDict(node) +impl<'a> From<&'a crate::ExprStringLiteral> for ExprRef<'a> { + fn from(node: &'a crate::ExprStringLiteral) -> Self { + Self::StringLiteral(node) } } -impl<'a> From<&'a crate::ExprSet> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprSet) -> AnyNodeRef<'a> { - AnyNodeRef::ExprSet(node) +impl<'a> From<&'a crate::ExprBytesLiteral> for ExprRef<'a> { + fn from(node: &'a crate::ExprBytesLiteral) -> Self { + Self::BytesLiteral(node) } } -impl<'a> From<&'a crate::ExprListComp> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprListComp) -> AnyNodeRef<'a> { - AnyNodeRef::ExprListComp(node) +impl<'a> From<&'a crate::ExprNumberLiteral> for ExprRef<'a> { + fn from(node: &'a crate::ExprNumberLiteral) -> Self { + Self::NumberLiteral(node) } } -impl<'a> From<&'a crate::ExprSetComp> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprSetComp) -> AnyNodeRef<'a> { - AnyNodeRef::ExprSetComp(node) +impl<'a> From<&'a crate::ExprBooleanLiteral> for ExprRef<'a> { + fn from(node: &'a crate::ExprBooleanLiteral) -> Self { + Self::BooleanLiteral(node) } } -impl<'a> From<&'a crate::ExprDictComp> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprDictComp) -> AnyNodeRef<'a> { - AnyNodeRef::ExprDictComp(node) +impl<'a> From<&'a crate::ExprNoneLiteral> for ExprRef<'a> { + fn from(node: &'a crate::ExprNoneLiteral) -> Self { + Self::NoneLiteral(node) } } -impl<'a> From<&'a crate::ExprGenerator> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprGenerator) -> AnyNodeRef<'a> { - AnyNodeRef::ExprGenerator(node) +impl<'a> From<&'a crate::ExprEllipsisLiteral> for ExprRef<'a> { + fn from(node: &'a crate::ExprEllipsisLiteral) -> Self { + Self::EllipsisLiteral(node) } } -impl<'a> From<&'a crate::ExprAwait> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprAwait) -> AnyNodeRef<'a> { - AnyNodeRef::ExprAwait(node) +impl<'a> From<&'a crate::ExprAttribute> for ExprRef<'a> { + fn from(node: &'a crate::ExprAttribute) -> Self { + Self::Attribute(node) } } -impl<'a> From<&'a crate::ExprYield> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprYield) -> AnyNodeRef<'a> { - AnyNodeRef::ExprYield(node) +impl<'a> From<&'a crate::ExprSubscript> for ExprRef<'a> { + fn from(node: &'a crate::ExprSubscript) -> Self { + Self::Subscript(node) } } -impl<'a> From<&'a crate::ExprYieldFrom> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprYieldFrom) -> AnyNodeRef<'a> { - AnyNodeRef::ExprYieldFrom(node) +impl<'a> From<&'a crate::ExprStarred> for ExprRef<'a> { + fn from(node: &'a crate::ExprStarred) -> Self { + Self::Starred(node) } } -impl<'a> From<&'a crate::ExprCompare> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprCompare) -> AnyNodeRef<'a> { - AnyNodeRef::ExprCompare(node) +impl<'a> From<&'a crate::ExprName> for ExprRef<'a> { + fn from(node: &'a crate::ExprName) -> Self { + Self::Name(node) } } -impl<'a> From<&'a crate::ExprCall> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprCall) -> AnyNodeRef<'a> { - AnyNodeRef::ExprCall(node) +impl<'a> From<&'a crate::ExprList> for ExprRef<'a> { + fn from(node: &'a crate::ExprList) -> Self { + Self::List(node) } } -impl<'a> From<&'a crate::ExprFString> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprFString) -> AnyNodeRef<'a> { - AnyNodeRef::ExprFString(node) +impl<'a> From<&'a crate::ExprTuple> for ExprRef<'a> { + fn from(node: &'a crate::ExprTuple) -> Self { + Self::Tuple(node) } } -impl<'a> From<&'a crate::ExprStringLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprStringLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::ExprStringLiteral(node) +impl<'a> From<&'a crate::ExprSlice> for ExprRef<'a> { + fn from(node: &'a crate::ExprSlice) -> Self { + Self::Slice(node) } } -impl<'a> From<&'a crate::ExprBytesLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprBytesLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::ExprBytesLiteral(node) +impl<'a> From<&'a crate::ExprIpyEscapeCommand> for ExprRef<'a> { + fn from(node: &'a crate::ExprIpyEscapeCommand) -> Self { + Self::IpyEscapeCommand(node) } } -impl<'a> From<&'a crate::ExprNumberLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprNumberLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::ExprNumberLiteral(node) +impl ruff_text_size::Ranged for ExprRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::BoolOp(node) => node.range(), + Self::Named(node) => node.range(), + Self::BinOp(node) => node.range(), + Self::UnaryOp(node) => node.range(), + Self::Lambda(node) => node.range(), + Self::If(node) => node.range(), + Self::Dict(node) => node.range(), + Self::Set(node) => node.range(), + Self::ListComp(node) => node.range(), + Self::SetComp(node) => node.range(), + Self::DictComp(node) => node.range(), + Self::Generator(node) => node.range(), + Self::Await(node) => node.range(), + Self::Yield(node) => node.range(), + Self::YieldFrom(node) => node.range(), + Self::Compare(node) => node.range(), + Self::Call(node) => node.range(), + Self::FString(node) => node.range(), + Self::TString(node) => node.range(), + Self::StringLiteral(node) => node.range(), + Self::BytesLiteral(node) => node.range(), + Self::NumberLiteral(node) => node.range(), + Self::BooleanLiteral(node) => node.range(), + Self::NoneLiteral(node) => node.range(), + Self::EllipsisLiteral(node) => node.range(), + Self::Attribute(node) => node.range(), + Self::Subscript(node) => node.range(), + Self::Starred(node) => node.range(), + Self::Name(node) => node.range(), + Self::List(node) => node.range(), + Self::Tuple(node) => node.range(), + Self::Slice(node) => node.range(), + Self::IpyEscapeCommand(node) => node.range(), + } } } -impl<'a> From<&'a crate::ExprBooleanLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprBooleanLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::ExprBooleanLiteral(node) +impl crate::HasNodeIndex for ExprRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::BoolOp(node) => node.node_index(), + Self::Named(node) => node.node_index(), + Self::BinOp(node) => node.node_index(), + Self::UnaryOp(node) => node.node_index(), + Self::Lambda(node) => node.node_index(), + Self::If(node) => node.node_index(), + Self::Dict(node) => node.node_index(), + Self::Set(node) => node.node_index(), + Self::ListComp(node) => node.node_index(), + Self::SetComp(node) => node.node_index(), + Self::DictComp(node) => node.node_index(), + Self::Generator(node) => node.node_index(), + Self::Await(node) => node.node_index(), + Self::Yield(node) => node.node_index(), + Self::YieldFrom(node) => node.node_index(), + Self::Compare(node) => node.node_index(), + Self::Call(node) => node.node_index(), + Self::FString(node) => node.node_index(), + Self::TString(node) => node.node_index(), + Self::StringLiteral(node) => node.node_index(), + Self::BytesLiteral(node) => node.node_index(), + Self::NumberLiteral(node) => node.node_index(), + Self::BooleanLiteral(node) => node.node_index(), + Self::NoneLiteral(node) => node.node_index(), + Self::EllipsisLiteral(node) => node.node_index(), + Self::Attribute(node) => node.node_index(), + Self::Subscript(node) => node.node_index(), + Self::Starred(node) => node.node_index(), + Self::Name(node) => node.node_index(), + Self::List(node) => node.node_index(), + Self::Tuple(node) => node.node_index(), + Self::Slice(node) => node.node_index(), + Self::IpyEscapeCommand(node) => node.node_index(), + } } } -impl<'a> From<&'a crate::ExprNoneLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprNoneLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::ExprNoneLiteral(node) - } +/// See also [excepthandler](https://docs.python.org/3/library/ast.html#ast.excepthandler) +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum ExceptHandlerRef<'a> { + ExceptHandler(&'a crate::ExceptHandlerExceptHandler), } -impl<'a> From<&'a crate::ExprEllipsisLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprEllipsisLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::ExprEllipsisLiteral(node) +impl<'a> From<&'a ExceptHandler> for ExceptHandlerRef<'a> { + fn from(node: &'a ExceptHandler) -> Self { + match node { + ExceptHandler::ExceptHandler(node) => ExceptHandlerRef::ExceptHandler(node), + } } } -impl<'a> From<&'a crate::ExprAttribute> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprAttribute) -> AnyNodeRef<'a> { - AnyNodeRef::ExprAttribute(node) +impl<'a> From<&'a crate::ExceptHandlerExceptHandler> for ExceptHandlerRef<'a> { + fn from(node: &'a crate::ExceptHandlerExceptHandler) -> Self { + Self::ExceptHandler(node) } } -impl<'a> From<&'a crate::ExprSubscript> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprSubscript) -> AnyNodeRef<'a> { - AnyNodeRef::ExprSubscript(node) +impl ruff_text_size::Ranged for ExceptHandlerRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::ExceptHandler(node) => node.range(), + } } } -impl<'a> From<&'a crate::ExprStarred> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprStarred) -> AnyNodeRef<'a> { - AnyNodeRef::ExprStarred(node) +impl crate::HasNodeIndex for ExceptHandlerRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::ExceptHandler(node) => node.node_index(), + } } } -impl<'a> From<&'a crate::ExprName> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprName) -> AnyNodeRef<'a> { - AnyNodeRef::ExprName(node) - } +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum InterpolatedStringElementRef<'a> { + Interpolation(&'a crate::InterpolatedElement), + Literal(&'a crate::InterpolatedStringLiteralElement), } -impl<'a> From<&'a crate::ExprList> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprList) -> AnyNodeRef<'a> { - AnyNodeRef::ExprList(node) +impl<'a> From<&'a InterpolatedStringElement> for InterpolatedStringElementRef<'a> { + fn from(node: &'a InterpolatedStringElement) -> Self { + match node { + InterpolatedStringElement::Interpolation(node) => { + InterpolatedStringElementRef::Interpolation(node) + } + InterpolatedStringElement::Literal(node) => InterpolatedStringElementRef::Literal(node), + } } } -impl<'a> From<&'a crate::ExprTuple> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprTuple) -> AnyNodeRef<'a> { - AnyNodeRef::ExprTuple(node) +impl<'a> From<&'a crate::InterpolatedElement> for InterpolatedStringElementRef<'a> { + fn from(node: &'a crate::InterpolatedElement) -> Self { + Self::Interpolation(node) } } -impl<'a> From<&'a crate::ExprSlice> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprSlice) -> AnyNodeRef<'a> { - AnyNodeRef::ExprSlice(node) +impl<'a> From<&'a crate::InterpolatedStringLiteralElement> for InterpolatedStringElementRef<'a> { + fn from(node: &'a crate::InterpolatedStringLiteralElement) -> Self { + Self::Literal(node) } } -impl<'a> From<&'a crate::ExprIpyEscapeCommand> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExprIpyEscapeCommand) -> AnyNodeRef<'a> { - AnyNodeRef::ExprIpyEscapeCommand(node) +impl ruff_text_size::Ranged for InterpolatedStringElementRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::Interpolation(node) => node.range(), + Self::Literal(node) => node.range(), + } } } -impl<'a> From<&'a crate::ExceptHandlerExceptHandler> for AnyNodeRef<'a> { - fn from(node: &'a crate::ExceptHandlerExceptHandler) -> AnyNodeRef<'a> { - AnyNodeRef::ExceptHandlerExceptHandler(node) +impl crate::HasNodeIndex for InterpolatedStringElementRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::Interpolation(node) => node.node_index(), + Self::Literal(node) => node.node_index(), + } } } -impl<'a> From<&'a crate::FStringExpressionElement> for AnyNodeRef<'a> { - fn from(node: &'a crate::FStringExpressionElement) -> AnyNodeRef<'a> { - AnyNodeRef::FStringExpressionElement(node) - } +/// See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern) +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum PatternRef<'a> { + MatchValue(&'a crate::PatternMatchValue), + MatchSingleton(&'a crate::PatternMatchSingleton), + MatchSequence(&'a crate::PatternMatchSequence), + MatchMapping(&'a crate::PatternMatchMapping), + MatchClass(&'a crate::PatternMatchClass), + MatchStar(&'a crate::PatternMatchStar), + MatchAs(&'a crate::PatternMatchAs), + MatchOr(&'a crate::PatternMatchOr), } -impl<'a> From<&'a crate::FStringLiteralElement> for AnyNodeRef<'a> { - fn from(node: &'a crate::FStringLiteralElement) -> AnyNodeRef<'a> { - AnyNodeRef::FStringLiteralElement(node) +impl<'a> From<&'a Pattern> for PatternRef<'a> { + fn from(node: &'a Pattern) -> Self { + match node { + Pattern::MatchValue(node) => PatternRef::MatchValue(node), + Pattern::MatchSingleton(node) => PatternRef::MatchSingleton(node), + Pattern::MatchSequence(node) => PatternRef::MatchSequence(node), + Pattern::MatchMapping(node) => PatternRef::MatchMapping(node), + Pattern::MatchClass(node) => PatternRef::MatchClass(node), + Pattern::MatchStar(node) => PatternRef::MatchStar(node), + Pattern::MatchAs(node) => PatternRef::MatchAs(node), + Pattern::MatchOr(node) => PatternRef::MatchOr(node), + } } } -impl<'a> From<&'a crate::PatternMatchValue> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchValue) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchValue(node) +impl<'a> From<&'a crate::PatternMatchValue> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchValue) -> Self { + Self::MatchValue(node) } } -impl<'a> From<&'a crate::PatternMatchSingleton> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchSingleton) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchSingleton(node) +impl<'a> From<&'a crate::PatternMatchSingleton> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchSingleton) -> Self { + Self::MatchSingleton(node) } } -impl<'a> From<&'a crate::PatternMatchSequence> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchSequence) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchSequence(node) +impl<'a> From<&'a crate::PatternMatchSequence> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchSequence) -> Self { + Self::MatchSequence(node) } } -impl<'a> From<&'a crate::PatternMatchMapping> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchMapping) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchMapping(node) +impl<'a> From<&'a crate::PatternMatchMapping> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchMapping) -> Self { + Self::MatchMapping(node) } } -impl<'a> From<&'a crate::PatternMatchClass> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchClass) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchClass(node) +impl<'a> From<&'a crate::PatternMatchClass> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchClass) -> Self { + Self::MatchClass(node) } } -impl<'a> From<&'a crate::PatternMatchStar> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchStar) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchStar(node) +impl<'a> From<&'a crate::PatternMatchStar> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchStar) -> Self { + Self::MatchStar(node) } } -impl<'a> From<&'a crate::PatternMatchAs> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchAs) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchAs(node) +impl<'a> From<&'a crate::PatternMatchAs> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchAs) -> Self { + Self::MatchAs(node) } } -impl<'a> From<&'a crate::PatternMatchOr> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternMatchOr) -> AnyNodeRef<'a> { - AnyNodeRef::PatternMatchOr(node) +impl<'a> From<&'a crate::PatternMatchOr> for PatternRef<'a> { + fn from(node: &'a crate::PatternMatchOr) -> Self { + Self::MatchOr(node) } } -impl<'a> From<&'a crate::TypeParamTypeVar> for AnyNodeRef<'a> { - fn from(node: &'a crate::TypeParamTypeVar) -> AnyNodeRef<'a> { - AnyNodeRef::TypeParamTypeVar(node) +impl ruff_text_size::Ranged for PatternRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::MatchValue(node) => node.range(), + Self::MatchSingleton(node) => node.range(), + Self::MatchSequence(node) => node.range(), + Self::MatchMapping(node) => node.range(), + Self::MatchClass(node) => node.range(), + Self::MatchStar(node) => node.range(), + Self::MatchAs(node) => node.range(), + Self::MatchOr(node) => node.range(), + } } } -impl<'a> From<&'a crate::TypeParamTypeVarTuple> for AnyNodeRef<'a> { - fn from(node: &'a crate::TypeParamTypeVarTuple) -> AnyNodeRef<'a> { - AnyNodeRef::TypeParamTypeVarTuple(node) +impl crate::HasNodeIndex for PatternRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::MatchValue(node) => node.node_index(), + Self::MatchSingleton(node) => node.node_index(), + Self::MatchSequence(node) => node.node_index(), + Self::MatchMapping(node) => node.node_index(), + Self::MatchClass(node) => node.node_index(), + Self::MatchStar(node) => node.node_index(), + Self::MatchAs(node) => node.node_index(), + Self::MatchOr(node) => node.node_index(), + } } } -impl<'a> From<&'a crate::TypeParamParamSpec> for AnyNodeRef<'a> { - fn from(node: &'a crate::TypeParamParamSpec) -> AnyNodeRef<'a> { - AnyNodeRef::TypeParamParamSpec(node) - } +/// See also [type_param](https://docs.python.org/3/library/ast.html#ast.type_param) +#[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum TypeParamRef<'a> { + TypeVar(&'a crate::TypeParamTypeVar), + TypeVarTuple(&'a crate::TypeParamTypeVarTuple), + ParamSpec(&'a crate::TypeParamParamSpec), } -impl<'a> From<&'a crate::FStringFormatSpec> for AnyNodeRef<'a> { - fn from(node: &'a crate::FStringFormatSpec) -> AnyNodeRef<'a> { - AnyNodeRef::FStringFormatSpec(node) - } -} - -impl<'a> From<&'a crate::PatternArguments> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternArguments) -> AnyNodeRef<'a> { - AnyNodeRef::PatternArguments(node) - } -} - -impl<'a> From<&'a crate::PatternKeyword> for AnyNodeRef<'a> { - fn from(node: &'a crate::PatternKeyword) -> AnyNodeRef<'a> { - AnyNodeRef::PatternKeyword(node) - } -} - -impl<'a> From<&'a crate::Comprehension> for AnyNodeRef<'a> { - fn from(node: &'a crate::Comprehension) -> AnyNodeRef<'a> { - AnyNodeRef::Comprehension(node) - } -} - -impl<'a> From<&'a crate::Arguments> for AnyNodeRef<'a> { - fn from(node: &'a crate::Arguments) -> AnyNodeRef<'a> { - AnyNodeRef::Arguments(node) +impl<'a> From<&'a TypeParam> for TypeParamRef<'a> { + fn from(node: &'a TypeParam) -> Self { + match node { + TypeParam::TypeVar(node) => TypeParamRef::TypeVar(node), + TypeParam::TypeVarTuple(node) => TypeParamRef::TypeVarTuple(node), + TypeParam::ParamSpec(node) => TypeParamRef::ParamSpec(node), + } } } -impl<'a> From<&'a crate::Parameters> for AnyNodeRef<'a> { - fn from(node: &'a crate::Parameters) -> AnyNodeRef<'a> { - AnyNodeRef::Parameters(node) +impl<'a> From<&'a crate::TypeParamTypeVar> for TypeParamRef<'a> { + fn from(node: &'a crate::TypeParamTypeVar) -> Self { + Self::TypeVar(node) } } -impl<'a> From<&'a crate::Parameter> for AnyNodeRef<'a> { - fn from(node: &'a crate::Parameter) -> AnyNodeRef<'a> { - AnyNodeRef::Parameter(node) +impl<'a> From<&'a crate::TypeParamTypeVarTuple> for TypeParamRef<'a> { + fn from(node: &'a crate::TypeParamTypeVarTuple) -> Self { + Self::TypeVarTuple(node) } } -impl<'a> From<&'a crate::ParameterWithDefault> for AnyNodeRef<'a> { - fn from(node: &'a crate::ParameterWithDefault) -> AnyNodeRef<'a> { - AnyNodeRef::ParameterWithDefault(node) +impl<'a> From<&'a crate::TypeParamParamSpec> for TypeParamRef<'a> { + fn from(node: &'a crate::TypeParamParamSpec) -> Self { + Self::ParamSpec(node) } } -impl<'a> From<&'a crate::Keyword> for AnyNodeRef<'a> { - fn from(node: &'a crate::Keyword) -> AnyNodeRef<'a> { - AnyNodeRef::Keyword(node) +impl ruff_text_size::Ranged for TypeParamRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + Self::TypeVar(node) => node.range(), + Self::TypeVarTuple(node) => node.range(), + Self::ParamSpec(node) => node.range(), + } } } -impl<'a> From<&'a crate::Alias> for AnyNodeRef<'a> { - fn from(node: &'a crate::Alias) -> AnyNodeRef<'a> { - AnyNodeRef::Alias(node) +impl crate::HasNodeIndex for TypeParamRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + Self::TypeVar(node) => node.node_index(), + Self::TypeVarTuple(node) => node.node_index(), + Self::ParamSpec(node) => node.node_index(), + } } } -impl<'a> From<&'a crate::WithItem> for AnyNodeRef<'a> { - fn from(node: &'a crate::WithItem) -> AnyNodeRef<'a> { - AnyNodeRef::WithItem(node) - } +/// A flattened enumeration of all AST nodes. +#[derive(Copy, Clone, Debug, is_macro::Is, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum AnyNodeRef<'a> { + ModModule(&'a crate::ModModule), + ModExpression(&'a crate::ModExpression), + StmtFunctionDef(&'a crate::StmtFunctionDef), + StmtClassDef(&'a crate::StmtClassDef), + StmtReturn(&'a crate::StmtReturn), + StmtDelete(&'a crate::StmtDelete), + StmtTypeAlias(&'a crate::StmtTypeAlias), + StmtAssign(&'a crate::StmtAssign), + StmtAugAssign(&'a crate::StmtAugAssign), + StmtAnnAssign(&'a crate::StmtAnnAssign), + StmtFor(&'a crate::StmtFor), + StmtWhile(&'a crate::StmtWhile), + StmtIf(&'a crate::StmtIf), + StmtWith(&'a crate::StmtWith), + StmtMatch(&'a crate::StmtMatch), + StmtRaise(&'a crate::StmtRaise), + StmtTry(&'a crate::StmtTry), + StmtAssert(&'a crate::StmtAssert), + StmtImport(&'a crate::StmtImport), + StmtImportFrom(&'a crate::StmtImportFrom), + StmtGlobal(&'a crate::StmtGlobal), + StmtNonlocal(&'a crate::StmtNonlocal), + StmtExpr(&'a crate::StmtExpr), + StmtPass(&'a crate::StmtPass), + StmtBreak(&'a crate::StmtBreak), + StmtContinue(&'a crate::StmtContinue), + StmtIpyEscapeCommand(&'a crate::StmtIpyEscapeCommand), + ExprBoolOp(&'a crate::ExprBoolOp), + ExprNamed(&'a crate::ExprNamed), + ExprBinOp(&'a crate::ExprBinOp), + ExprUnaryOp(&'a crate::ExprUnaryOp), + ExprLambda(&'a crate::ExprLambda), + ExprIf(&'a crate::ExprIf), + ExprDict(&'a crate::ExprDict), + ExprSet(&'a crate::ExprSet), + ExprListComp(&'a crate::ExprListComp), + ExprSetComp(&'a crate::ExprSetComp), + ExprDictComp(&'a crate::ExprDictComp), + ExprGenerator(&'a crate::ExprGenerator), + ExprAwait(&'a crate::ExprAwait), + ExprYield(&'a crate::ExprYield), + ExprYieldFrom(&'a crate::ExprYieldFrom), + ExprCompare(&'a crate::ExprCompare), + ExprCall(&'a crate::ExprCall), + ExprFString(&'a crate::ExprFString), + ExprTString(&'a crate::ExprTString), + ExprStringLiteral(&'a crate::ExprStringLiteral), + ExprBytesLiteral(&'a crate::ExprBytesLiteral), + ExprNumberLiteral(&'a crate::ExprNumberLiteral), + ExprBooleanLiteral(&'a crate::ExprBooleanLiteral), + ExprNoneLiteral(&'a crate::ExprNoneLiteral), + ExprEllipsisLiteral(&'a crate::ExprEllipsisLiteral), + ExprAttribute(&'a crate::ExprAttribute), + ExprSubscript(&'a crate::ExprSubscript), + ExprStarred(&'a crate::ExprStarred), + ExprName(&'a crate::ExprName), + ExprList(&'a crate::ExprList), + ExprTuple(&'a crate::ExprTuple), + ExprSlice(&'a crate::ExprSlice), + ExprIpyEscapeCommand(&'a crate::ExprIpyEscapeCommand), + ExceptHandlerExceptHandler(&'a crate::ExceptHandlerExceptHandler), + InterpolatedElement(&'a crate::InterpolatedElement), + InterpolatedStringLiteralElement(&'a crate::InterpolatedStringLiteralElement), + PatternMatchValue(&'a crate::PatternMatchValue), + PatternMatchSingleton(&'a crate::PatternMatchSingleton), + PatternMatchSequence(&'a crate::PatternMatchSequence), + PatternMatchMapping(&'a crate::PatternMatchMapping), + PatternMatchClass(&'a crate::PatternMatchClass), + PatternMatchStar(&'a crate::PatternMatchStar), + PatternMatchAs(&'a crate::PatternMatchAs), + PatternMatchOr(&'a crate::PatternMatchOr), + TypeParamTypeVar(&'a crate::TypeParamTypeVar), + TypeParamTypeVarTuple(&'a crate::TypeParamTypeVarTuple), + TypeParamParamSpec(&'a crate::TypeParamParamSpec), + InterpolatedStringFormatSpec(&'a crate::InterpolatedStringFormatSpec), + PatternArguments(&'a crate::PatternArguments), + PatternKeyword(&'a crate::PatternKeyword), + Comprehension(&'a crate::Comprehension), + Arguments(&'a crate::Arguments), + Parameters(&'a crate::Parameters), + Parameter(&'a crate::Parameter), + ParameterWithDefault(&'a crate::ParameterWithDefault), + Keyword(&'a crate::Keyword), + Alias(&'a crate::Alias), + WithItem(&'a crate::WithItem), + MatchCase(&'a crate::MatchCase), + Decorator(&'a crate::Decorator), + ElifElseClause(&'a crate::ElifElseClause), + TypeParams(&'a crate::TypeParams), + FString(&'a crate::FString), + TString(&'a crate::TString), + StringLiteral(&'a crate::StringLiteral), + BytesLiteral(&'a crate::BytesLiteral), + Identifier(&'a crate::Identifier), } -impl<'a> From<&'a crate::MatchCase> for AnyNodeRef<'a> { - fn from(node: &'a crate::MatchCase) -> AnyNodeRef<'a> { - AnyNodeRef::MatchCase(node) +impl<'a> From<&'a Mod> for AnyNodeRef<'a> { + fn from(node: &'a Mod) -> AnyNodeRef<'a> { + match node { + Mod::Module(node) => AnyNodeRef::ModModule(node), + Mod::Expression(node) => AnyNodeRef::ModExpression(node), + } } } -impl<'a> From<&'a crate::Decorator> for AnyNodeRef<'a> { - fn from(node: &'a crate::Decorator) -> AnyNodeRef<'a> { - AnyNodeRef::Decorator(node) +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: ModRef<'a>) -> AnyNodeRef<'a> { + match node { + ModRef::Module(node) => AnyNodeRef::ModModule(node), + ModRef::Expression(node) => AnyNodeRef::ModExpression(node), + } } } -impl<'a> From<&'a crate::ElifElseClause> for AnyNodeRef<'a> { - fn from(node: &'a crate::ElifElseClause) -> AnyNodeRef<'a> { - AnyNodeRef::ElifElseClause(node) - } -} +impl<'a> AnyNodeRef<'a> { + pub fn as_mod_ref(self) -> Option> { + match self { + Self::ModModule(node) => Some(ModRef::Module(node)), + Self::ModExpression(node) => Some(ModRef::Expression(node)), -impl<'a> From<&'a crate::TypeParams> for AnyNodeRef<'a> { - fn from(node: &'a crate::TypeParams) -> AnyNodeRef<'a> { - AnyNodeRef::TypeParams(node) + _ => None, + } } } -impl<'a> From<&'a crate::FString> for AnyNodeRef<'a> { - fn from(node: &'a crate::FString) -> AnyNodeRef<'a> { - AnyNodeRef::FString(node) +impl<'a> From<&'a Stmt> for AnyNodeRef<'a> { + fn from(node: &'a Stmt) -> AnyNodeRef<'a> { + match node { + Stmt::FunctionDef(node) => AnyNodeRef::StmtFunctionDef(node), + Stmt::ClassDef(node) => AnyNodeRef::StmtClassDef(node), + Stmt::Return(node) => AnyNodeRef::StmtReturn(node), + Stmt::Delete(node) => AnyNodeRef::StmtDelete(node), + Stmt::TypeAlias(node) => AnyNodeRef::StmtTypeAlias(node), + Stmt::Assign(node) => AnyNodeRef::StmtAssign(node), + Stmt::AugAssign(node) => AnyNodeRef::StmtAugAssign(node), + Stmt::AnnAssign(node) => AnyNodeRef::StmtAnnAssign(node), + Stmt::For(node) => AnyNodeRef::StmtFor(node), + Stmt::While(node) => AnyNodeRef::StmtWhile(node), + Stmt::If(node) => AnyNodeRef::StmtIf(node), + Stmt::With(node) => AnyNodeRef::StmtWith(node), + Stmt::Match(node) => AnyNodeRef::StmtMatch(node), + Stmt::Raise(node) => AnyNodeRef::StmtRaise(node), + Stmt::Try(node) => AnyNodeRef::StmtTry(node), + Stmt::Assert(node) => AnyNodeRef::StmtAssert(node), + Stmt::Import(node) => AnyNodeRef::StmtImport(node), + Stmt::ImportFrom(node) => AnyNodeRef::StmtImportFrom(node), + Stmt::Global(node) => AnyNodeRef::StmtGlobal(node), + Stmt::Nonlocal(node) => AnyNodeRef::StmtNonlocal(node), + Stmt::Expr(node) => AnyNodeRef::StmtExpr(node), + Stmt::Pass(node) => AnyNodeRef::StmtPass(node), + Stmt::Break(node) => AnyNodeRef::StmtBreak(node), + Stmt::Continue(node) => AnyNodeRef::StmtContinue(node), + Stmt::IpyEscapeCommand(node) => AnyNodeRef::StmtIpyEscapeCommand(node), + } } } -impl<'a> From<&'a crate::StringLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::StringLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::StringLiteral(node) +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: StmtRef<'a>) -> AnyNodeRef<'a> { + match node { + StmtRef::FunctionDef(node) => AnyNodeRef::StmtFunctionDef(node), + StmtRef::ClassDef(node) => AnyNodeRef::StmtClassDef(node), + StmtRef::Return(node) => AnyNodeRef::StmtReturn(node), + StmtRef::Delete(node) => AnyNodeRef::StmtDelete(node), + StmtRef::TypeAlias(node) => AnyNodeRef::StmtTypeAlias(node), + StmtRef::Assign(node) => AnyNodeRef::StmtAssign(node), + StmtRef::AugAssign(node) => AnyNodeRef::StmtAugAssign(node), + StmtRef::AnnAssign(node) => AnyNodeRef::StmtAnnAssign(node), + StmtRef::For(node) => AnyNodeRef::StmtFor(node), + StmtRef::While(node) => AnyNodeRef::StmtWhile(node), + StmtRef::If(node) => AnyNodeRef::StmtIf(node), + StmtRef::With(node) => AnyNodeRef::StmtWith(node), + StmtRef::Match(node) => AnyNodeRef::StmtMatch(node), + StmtRef::Raise(node) => AnyNodeRef::StmtRaise(node), + StmtRef::Try(node) => AnyNodeRef::StmtTry(node), + StmtRef::Assert(node) => AnyNodeRef::StmtAssert(node), + StmtRef::Import(node) => AnyNodeRef::StmtImport(node), + StmtRef::ImportFrom(node) => AnyNodeRef::StmtImportFrom(node), + StmtRef::Global(node) => AnyNodeRef::StmtGlobal(node), + StmtRef::Nonlocal(node) => AnyNodeRef::StmtNonlocal(node), + StmtRef::Expr(node) => AnyNodeRef::StmtExpr(node), + StmtRef::Pass(node) => AnyNodeRef::StmtPass(node), + StmtRef::Break(node) => AnyNodeRef::StmtBreak(node), + StmtRef::Continue(node) => AnyNodeRef::StmtContinue(node), + StmtRef::IpyEscapeCommand(node) => AnyNodeRef::StmtIpyEscapeCommand(node), + } } } -impl<'a> From<&'a crate::BytesLiteral> for AnyNodeRef<'a> { - fn from(node: &'a crate::BytesLiteral) -> AnyNodeRef<'a> { - AnyNodeRef::BytesLiteral(node) +impl<'a> AnyNodeRef<'a> { + pub fn as_stmt_ref(self) -> Option> { + match self { + Self::StmtFunctionDef(node) => Some(StmtRef::FunctionDef(node)), + Self::StmtClassDef(node) => Some(StmtRef::ClassDef(node)), + Self::StmtReturn(node) => Some(StmtRef::Return(node)), + Self::StmtDelete(node) => Some(StmtRef::Delete(node)), + Self::StmtTypeAlias(node) => Some(StmtRef::TypeAlias(node)), + Self::StmtAssign(node) => Some(StmtRef::Assign(node)), + Self::StmtAugAssign(node) => Some(StmtRef::AugAssign(node)), + Self::StmtAnnAssign(node) => Some(StmtRef::AnnAssign(node)), + Self::StmtFor(node) => Some(StmtRef::For(node)), + Self::StmtWhile(node) => Some(StmtRef::While(node)), + Self::StmtIf(node) => Some(StmtRef::If(node)), + Self::StmtWith(node) => Some(StmtRef::With(node)), + Self::StmtMatch(node) => Some(StmtRef::Match(node)), + Self::StmtRaise(node) => Some(StmtRef::Raise(node)), + Self::StmtTry(node) => Some(StmtRef::Try(node)), + Self::StmtAssert(node) => Some(StmtRef::Assert(node)), + Self::StmtImport(node) => Some(StmtRef::Import(node)), + Self::StmtImportFrom(node) => Some(StmtRef::ImportFrom(node)), + Self::StmtGlobal(node) => Some(StmtRef::Global(node)), + Self::StmtNonlocal(node) => Some(StmtRef::Nonlocal(node)), + Self::StmtExpr(node) => Some(StmtRef::Expr(node)), + Self::StmtPass(node) => Some(StmtRef::Pass(node)), + Self::StmtBreak(node) => Some(StmtRef::Break(node)), + Self::StmtContinue(node) => Some(StmtRef::Continue(node)), + Self::StmtIpyEscapeCommand(node) => Some(StmtRef::IpyEscapeCommand(node)), + + _ => None, + } } } -impl<'a> From<&'a crate::Identifier> for AnyNodeRef<'a> { - fn from(node: &'a crate::Identifier) -> AnyNodeRef<'a> { - AnyNodeRef::Identifier(node) +impl<'a> From<&'a Expr> for AnyNodeRef<'a> { + fn from(node: &'a Expr) -> AnyNodeRef<'a> { + match node { + Expr::BoolOp(node) => AnyNodeRef::ExprBoolOp(node), + Expr::Named(node) => AnyNodeRef::ExprNamed(node), + Expr::BinOp(node) => AnyNodeRef::ExprBinOp(node), + Expr::UnaryOp(node) => AnyNodeRef::ExprUnaryOp(node), + Expr::Lambda(node) => AnyNodeRef::ExprLambda(node), + Expr::If(node) => AnyNodeRef::ExprIf(node), + Expr::Dict(node) => AnyNodeRef::ExprDict(node), + Expr::Set(node) => AnyNodeRef::ExprSet(node), + Expr::ListComp(node) => AnyNodeRef::ExprListComp(node), + Expr::SetComp(node) => AnyNodeRef::ExprSetComp(node), + Expr::DictComp(node) => AnyNodeRef::ExprDictComp(node), + Expr::Generator(node) => AnyNodeRef::ExprGenerator(node), + Expr::Await(node) => AnyNodeRef::ExprAwait(node), + Expr::Yield(node) => AnyNodeRef::ExprYield(node), + Expr::YieldFrom(node) => AnyNodeRef::ExprYieldFrom(node), + Expr::Compare(node) => AnyNodeRef::ExprCompare(node), + Expr::Call(node) => AnyNodeRef::ExprCall(node), + Expr::FString(node) => AnyNodeRef::ExprFString(node), + Expr::TString(node) => AnyNodeRef::ExprTString(node), + Expr::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node), + Expr::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node), + Expr::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node), + Expr::BooleanLiteral(node) => AnyNodeRef::ExprBooleanLiteral(node), + Expr::NoneLiteral(node) => AnyNodeRef::ExprNoneLiteral(node), + Expr::EllipsisLiteral(node) => AnyNodeRef::ExprEllipsisLiteral(node), + Expr::Attribute(node) => AnyNodeRef::ExprAttribute(node), + Expr::Subscript(node) => AnyNodeRef::ExprSubscript(node), + Expr::Starred(node) => AnyNodeRef::ExprStarred(node), + Expr::Name(node) => AnyNodeRef::ExprName(node), + Expr::List(node) => AnyNodeRef::ExprList(node), + Expr::Tuple(node) => AnyNodeRef::ExprTuple(node), + Expr::Slice(node) => AnyNodeRef::ExprSlice(node), + Expr::IpyEscapeCommand(node) => AnyNodeRef::ExprIpyEscapeCommand(node), + } } } -impl ruff_text_size::Ranged for AnyNodeRef<'_> { - fn range(&self) -> ruff_text_size::TextRange { - match self { - AnyNodeRef::ModModule(node) => node.range(), - AnyNodeRef::ModExpression(node) => node.range(), - AnyNodeRef::StmtFunctionDef(node) => node.range(), - AnyNodeRef::StmtClassDef(node) => node.range(), - AnyNodeRef::StmtReturn(node) => node.range(), - AnyNodeRef::StmtDelete(node) => node.range(), - AnyNodeRef::StmtTypeAlias(node) => node.range(), - AnyNodeRef::StmtAssign(node) => node.range(), - AnyNodeRef::StmtAugAssign(node) => node.range(), - AnyNodeRef::StmtAnnAssign(node) => node.range(), - AnyNodeRef::StmtFor(node) => node.range(), - AnyNodeRef::StmtWhile(node) => node.range(), - AnyNodeRef::StmtIf(node) => node.range(), - AnyNodeRef::StmtWith(node) => node.range(), - AnyNodeRef::StmtMatch(node) => node.range(), - AnyNodeRef::StmtRaise(node) => node.range(), - AnyNodeRef::StmtTry(node) => node.range(), - AnyNodeRef::StmtAssert(node) => node.range(), - AnyNodeRef::StmtImport(node) => node.range(), - AnyNodeRef::StmtImportFrom(node) => node.range(), - AnyNodeRef::StmtGlobal(node) => node.range(), - AnyNodeRef::StmtNonlocal(node) => node.range(), - AnyNodeRef::StmtExpr(node) => node.range(), - AnyNodeRef::StmtPass(node) => node.range(), - AnyNodeRef::StmtBreak(node) => node.range(), - AnyNodeRef::StmtContinue(node) => node.range(), - AnyNodeRef::StmtIpyEscapeCommand(node) => node.range(), - AnyNodeRef::ExprBoolOp(node) => node.range(), - AnyNodeRef::ExprNamed(node) => node.range(), - AnyNodeRef::ExprBinOp(node) => node.range(), - AnyNodeRef::ExprUnaryOp(node) => node.range(), - AnyNodeRef::ExprLambda(node) => node.range(), - AnyNodeRef::ExprIf(node) => node.range(), - AnyNodeRef::ExprDict(node) => node.range(), - AnyNodeRef::ExprSet(node) => node.range(), - AnyNodeRef::ExprListComp(node) => node.range(), - AnyNodeRef::ExprSetComp(node) => node.range(), - AnyNodeRef::ExprDictComp(node) => node.range(), - AnyNodeRef::ExprGenerator(node) => node.range(), - AnyNodeRef::ExprAwait(node) => node.range(), - AnyNodeRef::ExprYield(node) => node.range(), - AnyNodeRef::ExprYieldFrom(node) => node.range(), - AnyNodeRef::ExprCompare(node) => node.range(), - AnyNodeRef::ExprCall(node) => node.range(), - AnyNodeRef::ExprFString(node) => node.range(), - AnyNodeRef::ExprStringLiteral(node) => node.range(), - AnyNodeRef::ExprBytesLiteral(node) => node.range(), - AnyNodeRef::ExprNumberLiteral(node) => node.range(), - AnyNodeRef::ExprBooleanLiteral(node) => node.range(), - AnyNodeRef::ExprNoneLiteral(node) => node.range(), - AnyNodeRef::ExprEllipsisLiteral(node) => node.range(), - AnyNodeRef::ExprAttribute(node) => node.range(), - AnyNodeRef::ExprSubscript(node) => node.range(), - AnyNodeRef::ExprStarred(node) => node.range(), - AnyNodeRef::ExprName(node) => node.range(), - AnyNodeRef::ExprList(node) => node.range(), - AnyNodeRef::ExprTuple(node) => node.range(), - AnyNodeRef::ExprSlice(node) => node.range(), - AnyNodeRef::ExprIpyEscapeCommand(node) => node.range(), - AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(), - AnyNodeRef::FStringExpressionElement(node) => node.range(), - AnyNodeRef::FStringLiteralElement(node) => node.range(), - AnyNodeRef::PatternMatchValue(node) => node.range(), - AnyNodeRef::PatternMatchSingleton(node) => node.range(), - AnyNodeRef::PatternMatchSequence(node) => node.range(), - AnyNodeRef::PatternMatchMapping(node) => node.range(), - AnyNodeRef::PatternMatchClass(node) => node.range(), - AnyNodeRef::PatternMatchStar(node) => node.range(), - AnyNodeRef::PatternMatchAs(node) => node.range(), - AnyNodeRef::PatternMatchOr(node) => node.range(), - AnyNodeRef::TypeParamTypeVar(node) => node.range(), - AnyNodeRef::TypeParamTypeVarTuple(node) => node.range(), - AnyNodeRef::TypeParamParamSpec(node) => node.range(), - AnyNodeRef::FStringFormatSpec(node) => node.range(), - AnyNodeRef::PatternArguments(node) => node.range(), - AnyNodeRef::PatternKeyword(node) => node.range(), - AnyNodeRef::Comprehension(node) => node.range(), - AnyNodeRef::Arguments(node) => node.range(), - AnyNodeRef::Parameters(node) => node.range(), - AnyNodeRef::Parameter(node) => node.range(), - AnyNodeRef::ParameterWithDefault(node) => node.range(), - AnyNodeRef::Keyword(node) => node.range(), - AnyNodeRef::Alias(node) => node.range(), - AnyNodeRef::WithItem(node) => node.range(), - AnyNodeRef::MatchCase(node) => node.range(), - AnyNodeRef::Decorator(node) => node.range(), - AnyNodeRef::ElifElseClause(node) => node.range(), - AnyNodeRef::TypeParams(node) => node.range(), - AnyNodeRef::FString(node) => node.range(), - AnyNodeRef::StringLiteral(node) => node.range(), - AnyNodeRef::BytesLiteral(node) => node.range(), - AnyNodeRef::Identifier(node) => node.range(), +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: ExprRef<'a>) -> AnyNodeRef<'a> { + match node { + ExprRef::BoolOp(node) => AnyNodeRef::ExprBoolOp(node), + ExprRef::Named(node) => AnyNodeRef::ExprNamed(node), + ExprRef::BinOp(node) => AnyNodeRef::ExprBinOp(node), + ExprRef::UnaryOp(node) => AnyNodeRef::ExprUnaryOp(node), + ExprRef::Lambda(node) => AnyNodeRef::ExprLambda(node), + ExprRef::If(node) => AnyNodeRef::ExprIf(node), + ExprRef::Dict(node) => AnyNodeRef::ExprDict(node), + ExprRef::Set(node) => AnyNodeRef::ExprSet(node), + ExprRef::ListComp(node) => AnyNodeRef::ExprListComp(node), + ExprRef::SetComp(node) => AnyNodeRef::ExprSetComp(node), + ExprRef::DictComp(node) => AnyNodeRef::ExprDictComp(node), + ExprRef::Generator(node) => AnyNodeRef::ExprGenerator(node), + ExprRef::Await(node) => AnyNodeRef::ExprAwait(node), + ExprRef::Yield(node) => AnyNodeRef::ExprYield(node), + ExprRef::YieldFrom(node) => AnyNodeRef::ExprYieldFrom(node), + ExprRef::Compare(node) => AnyNodeRef::ExprCompare(node), + ExprRef::Call(node) => AnyNodeRef::ExprCall(node), + ExprRef::FString(node) => AnyNodeRef::ExprFString(node), + ExprRef::TString(node) => AnyNodeRef::ExprTString(node), + ExprRef::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node), + ExprRef::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node), + ExprRef::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node), + ExprRef::BooleanLiteral(node) => AnyNodeRef::ExprBooleanLiteral(node), + ExprRef::NoneLiteral(node) => AnyNodeRef::ExprNoneLiteral(node), + ExprRef::EllipsisLiteral(node) => AnyNodeRef::ExprEllipsisLiteral(node), + ExprRef::Attribute(node) => AnyNodeRef::ExprAttribute(node), + ExprRef::Subscript(node) => AnyNodeRef::ExprSubscript(node), + ExprRef::Starred(node) => AnyNodeRef::ExprStarred(node), + ExprRef::Name(node) => AnyNodeRef::ExprName(node), + ExprRef::List(node) => AnyNodeRef::ExprList(node), + ExprRef::Tuple(node) => AnyNodeRef::ExprTuple(node), + ExprRef::Slice(node) => AnyNodeRef::ExprSlice(node), + ExprRef::IpyEscapeCommand(node) => AnyNodeRef::ExprIpyEscapeCommand(node), } } } -impl AnyNodeRef<'_> { - pub fn as_ptr(&self) -> std::ptr::NonNull<()> { +impl<'a> AnyNodeRef<'a> { + pub fn as_expr_ref(self) -> Option> { match self { - AnyNodeRef::ModModule(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ModExpression(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtFunctionDef(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtClassDef(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtReturn(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtDelete(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtTypeAlias(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtAssign(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtAugAssign(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtAnnAssign(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtFor(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtWhile(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtIf(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtWith(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtMatch(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtRaise(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtTry(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtAssert(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtImport(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtImportFrom(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtGlobal(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtNonlocal(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtExpr(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtPass(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtBreak(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtContinue(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StmtIpyEscapeCommand(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprBoolOp(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprNamed(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprBinOp(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprUnaryOp(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprLambda(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprIf(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprDict(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprSet(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprListComp(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprSetComp(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprDictComp(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprGenerator(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprAwait(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprYield(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprYieldFrom(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprCompare(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprCall(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprFString(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprStringLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprBytesLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprNumberLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprBooleanLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprNoneLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprEllipsisLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprAttribute(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprSubscript(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprStarred(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprName(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprList(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprTuple(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprSlice(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExprIpyEscapeCommand(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ExceptHandlerExceptHandler(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FStringExpressionElement(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FStringLiteralElement(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchValue(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchSingleton(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchSequence(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchMapping(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchClass(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchStar(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchAs(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternMatchOr(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::TypeParamTypeVar(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::TypeParamTypeVarTuple(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::TypeParamParamSpec(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FStringFormatSpec(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternArguments(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::PatternKeyword(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Comprehension(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Arguments(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Parameters(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Parameter(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ParameterWithDefault(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Keyword(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Alias(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::WithItem(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::MatchCase(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Decorator(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::ElifElseClause(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::TypeParams(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FString(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::StringLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::BytesLiteral(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::Identifier(node) => std::ptr::NonNull::from(*node).cast(), + Self::ExprBoolOp(node) => Some(ExprRef::BoolOp(node)), + Self::ExprNamed(node) => Some(ExprRef::Named(node)), + Self::ExprBinOp(node) => Some(ExprRef::BinOp(node)), + Self::ExprUnaryOp(node) => Some(ExprRef::UnaryOp(node)), + Self::ExprLambda(node) => Some(ExprRef::Lambda(node)), + Self::ExprIf(node) => Some(ExprRef::If(node)), + Self::ExprDict(node) => Some(ExprRef::Dict(node)), + Self::ExprSet(node) => Some(ExprRef::Set(node)), + Self::ExprListComp(node) => Some(ExprRef::ListComp(node)), + Self::ExprSetComp(node) => Some(ExprRef::SetComp(node)), + Self::ExprDictComp(node) => Some(ExprRef::DictComp(node)), + Self::ExprGenerator(node) => Some(ExprRef::Generator(node)), + Self::ExprAwait(node) => Some(ExprRef::Await(node)), + Self::ExprYield(node) => Some(ExprRef::Yield(node)), + Self::ExprYieldFrom(node) => Some(ExprRef::YieldFrom(node)), + Self::ExprCompare(node) => Some(ExprRef::Compare(node)), + Self::ExprCall(node) => Some(ExprRef::Call(node)), + Self::ExprFString(node) => Some(ExprRef::FString(node)), + Self::ExprTString(node) => Some(ExprRef::TString(node)), + Self::ExprStringLiteral(node) => Some(ExprRef::StringLiteral(node)), + Self::ExprBytesLiteral(node) => Some(ExprRef::BytesLiteral(node)), + Self::ExprNumberLiteral(node) => Some(ExprRef::NumberLiteral(node)), + Self::ExprBooleanLiteral(node) => Some(ExprRef::BooleanLiteral(node)), + Self::ExprNoneLiteral(node) => Some(ExprRef::NoneLiteral(node)), + Self::ExprEllipsisLiteral(node) => Some(ExprRef::EllipsisLiteral(node)), + Self::ExprAttribute(node) => Some(ExprRef::Attribute(node)), + Self::ExprSubscript(node) => Some(ExprRef::Subscript(node)), + Self::ExprStarred(node) => Some(ExprRef::Starred(node)), + Self::ExprName(node) => Some(ExprRef::Name(node)), + Self::ExprList(node) => Some(ExprRef::List(node)), + Self::ExprTuple(node) => Some(ExprRef::Tuple(node)), + Self::ExprSlice(node) => Some(ExprRef::Slice(node)), + Self::ExprIpyEscapeCommand(node) => Some(ExprRef::IpyEscapeCommand(node)), + + _ => None, + } + } +} + +impl<'a> From<&'a ExceptHandler> for AnyNodeRef<'a> { + fn from(node: &'a ExceptHandler) -> AnyNodeRef<'a> { + match node { + ExceptHandler::ExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node), + } + } +} + +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: ExceptHandlerRef<'a>) -> AnyNodeRef<'a> { + match node { + ExceptHandlerRef::ExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node), } } } impl<'a> AnyNodeRef<'a> { - pub fn visit_source_order<'b, V>(self, visitor: &mut V) - where - V: crate::visitor::source_order::SourceOrderVisitor<'b> + ?Sized, - 'a: 'b, - { + pub fn as_except_handler_ref(self) -> Option> { match self { - AnyNodeRef::ModModule(node) => node.visit_source_order(visitor), - AnyNodeRef::ModExpression(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtFunctionDef(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtClassDef(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtReturn(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtDelete(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtTypeAlias(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtAssign(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtAugAssign(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtAnnAssign(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtFor(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtWhile(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtIf(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtWith(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtMatch(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtRaise(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtTry(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtAssert(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtImport(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtImportFrom(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtGlobal(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtNonlocal(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtExpr(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtPass(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtBreak(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtContinue(node) => node.visit_source_order(visitor), - AnyNodeRef::StmtIpyEscapeCommand(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprBoolOp(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprNamed(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprBinOp(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprUnaryOp(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprLambda(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprIf(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprDict(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprSet(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprListComp(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprSetComp(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprDictComp(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprGenerator(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprAwait(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprYield(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprYieldFrom(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprCompare(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprCall(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprFString(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprStringLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprBytesLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprNumberLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprBooleanLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprNoneLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprEllipsisLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprAttribute(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprSubscript(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprStarred(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprName(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprList(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprTuple(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprSlice(node) => node.visit_source_order(visitor), - AnyNodeRef::ExprIpyEscapeCommand(node) => node.visit_source_order(visitor), - AnyNodeRef::ExceptHandlerExceptHandler(node) => node.visit_source_order(visitor), - AnyNodeRef::FStringExpressionElement(node) => node.visit_source_order(visitor), - AnyNodeRef::FStringLiteralElement(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchValue(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchSingleton(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchSequence(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchMapping(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchClass(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchStar(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchAs(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternMatchOr(node) => node.visit_source_order(visitor), - AnyNodeRef::TypeParamTypeVar(node) => node.visit_source_order(visitor), - AnyNodeRef::TypeParamTypeVarTuple(node) => node.visit_source_order(visitor), - AnyNodeRef::TypeParamParamSpec(node) => node.visit_source_order(visitor), - AnyNodeRef::FStringFormatSpec(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternArguments(node) => node.visit_source_order(visitor), - AnyNodeRef::PatternKeyword(node) => node.visit_source_order(visitor), - AnyNodeRef::Comprehension(node) => node.visit_source_order(visitor), - AnyNodeRef::Arguments(node) => node.visit_source_order(visitor), - AnyNodeRef::Parameters(node) => node.visit_source_order(visitor), - AnyNodeRef::Parameter(node) => node.visit_source_order(visitor), - AnyNodeRef::ParameterWithDefault(node) => node.visit_source_order(visitor), - AnyNodeRef::Keyword(node) => node.visit_source_order(visitor), - AnyNodeRef::Alias(node) => node.visit_source_order(visitor), - AnyNodeRef::WithItem(node) => node.visit_source_order(visitor), - AnyNodeRef::MatchCase(node) => node.visit_source_order(visitor), - AnyNodeRef::Decorator(node) => node.visit_source_order(visitor), - AnyNodeRef::ElifElseClause(node) => node.visit_source_order(visitor), - AnyNodeRef::TypeParams(node) => node.visit_source_order(visitor), - AnyNodeRef::FString(node) => node.visit_source_order(visitor), - AnyNodeRef::StringLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::BytesLiteral(node) => node.visit_source_order(visitor), - AnyNodeRef::Identifier(node) => node.visit_source_order(visitor), + Self::ExceptHandlerExceptHandler(node) => Some(ExceptHandlerRef::ExceptHandler(node)), + + _ => None, + } + } +} + +impl<'a> From<&'a InterpolatedStringElement> for AnyNodeRef<'a> { + fn from(node: &'a InterpolatedStringElement) -> AnyNodeRef<'a> { + match node { + InterpolatedStringElement::Interpolation(node) => AnyNodeRef::InterpolatedElement(node), + InterpolatedStringElement::Literal(node) => { + AnyNodeRef::InterpolatedStringLiteralElement(node) + } + } + } +} + +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: InterpolatedStringElementRef<'a>) -> AnyNodeRef<'a> { + match node { + InterpolatedStringElementRef::Interpolation(node) => { + AnyNodeRef::InterpolatedElement(node) + } + InterpolatedStringElementRef::Literal(node) => { + AnyNodeRef::InterpolatedStringLiteralElement(node) + } + } + } +} + +impl<'a> AnyNodeRef<'a> { + pub fn as_interpolated_string_element_ref(self) -> Option> { + match self { + Self::InterpolatedElement(node) => { + Some(InterpolatedStringElementRef::Interpolation(node)) + } + Self::InterpolatedStringLiteralElement(node) => { + Some(InterpolatedStringElementRef::Literal(node)) + } + + _ => None, + } + } +} + +impl<'a> From<&'a Pattern> for AnyNodeRef<'a> { + fn from(node: &'a Pattern) -> AnyNodeRef<'a> { + match node { + Pattern::MatchValue(node) => AnyNodeRef::PatternMatchValue(node), + Pattern::MatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node), + Pattern::MatchSequence(node) => AnyNodeRef::PatternMatchSequence(node), + Pattern::MatchMapping(node) => AnyNodeRef::PatternMatchMapping(node), + Pattern::MatchClass(node) => AnyNodeRef::PatternMatchClass(node), + Pattern::MatchStar(node) => AnyNodeRef::PatternMatchStar(node), + Pattern::MatchAs(node) => AnyNodeRef::PatternMatchAs(node), + Pattern::MatchOr(node) => AnyNodeRef::PatternMatchOr(node), + } + } +} + +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: PatternRef<'a>) -> AnyNodeRef<'a> { + match node { + PatternRef::MatchValue(node) => AnyNodeRef::PatternMatchValue(node), + PatternRef::MatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node), + PatternRef::MatchSequence(node) => AnyNodeRef::PatternMatchSequence(node), + PatternRef::MatchMapping(node) => AnyNodeRef::PatternMatchMapping(node), + PatternRef::MatchClass(node) => AnyNodeRef::PatternMatchClass(node), + PatternRef::MatchStar(node) => AnyNodeRef::PatternMatchStar(node), + PatternRef::MatchAs(node) => AnyNodeRef::PatternMatchAs(node), + PatternRef::MatchOr(node) => AnyNodeRef::PatternMatchOr(node), + } + } +} + +impl<'a> AnyNodeRef<'a> { + pub fn as_pattern_ref(self) -> Option> { + match self { + Self::PatternMatchValue(node) => Some(PatternRef::MatchValue(node)), + Self::PatternMatchSingleton(node) => Some(PatternRef::MatchSingleton(node)), + Self::PatternMatchSequence(node) => Some(PatternRef::MatchSequence(node)), + Self::PatternMatchMapping(node) => Some(PatternRef::MatchMapping(node)), + Self::PatternMatchClass(node) => Some(PatternRef::MatchClass(node)), + Self::PatternMatchStar(node) => Some(PatternRef::MatchStar(node)), + Self::PatternMatchAs(node) => Some(PatternRef::MatchAs(node)), + Self::PatternMatchOr(node) => Some(PatternRef::MatchOr(node)), + + _ => None, + } + } +} + +impl<'a> From<&'a TypeParam> for AnyNodeRef<'a> { + fn from(node: &'a TypeParam) -> AnyNodeRef<'a> { + match node { + TypeParam::TypeVar(node) => AnyNodeRef::TypeParamTypeVar(node), + TypeParam::TypeVarTuple(node) => AnyNodeRef::TypeParamTypeVarTuple(node), + TypeParam::ParamSpec(node) => AnyNodeRef::TypeParamParamSpec(node), + } + } +} + +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: TypeParamRef<'a>) -> AnyNodeRef<'a> { + match node { + TypeParamRef::TypeVar(node) => AnyNodeRef::TypeParamTypeVar(node), + TypeParamRef::TypeVarTuple(node) => AnyNodeRef::TypeParamTypeVarTuple(node), + TypeParamRef::ParamSpec(node) => AnyNodeRef::TypeParamParamSpec(node), + } + } +} + +impl<'a> AnyNodeRef<'a> { + pub fn as_type_param_ref(self) -> Option> { + match self { + Self::TypeParamTypeVar(node) => Some(TypeParamRef::TypeVar(node)), + Self::TypeParamTypeVarTuple(node) => Some(TypeParamRef::TypeVarTuple(node)), + Self::TypeParamParamSpec(node) => Some(TypeParamRef::ParamSpec(node)), + + _ => None, + } + } +} + +impl<'a> From<&'a crate::ModModule> for AnyNodeRef<'a> { + fn from(node: &'a crate::ModModule) -> AnyNodeRef<'a> { + AnyNodeRef::ModModule(node) + } +} + +impl<'a> From<&'a crate::ModExpression> for AnyNodeRef<'a> { + fn from(node: &'a crate::ModExpression) -> AnyNodeRef<'a> { + AnyNodeRef::ModExpression(node) + } +} + +impl<'a> From<&'a crate::StmtFunctionDef> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtFunctionDef) -> AnyNodeRef<'a> { + AnyNodeRef::StmtFunctionDef(node) + } +} + +impl<'a> From<&'a crate::StmtClassDef> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtClassDef) -> AnyNodeRef<'a> { + AnyNodeRef::StmtClassDef(node) + } +} + +impl<'a> From<&'a crate::StmtReturn> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtReturn) -> AnyNodeRef<'a> { + AnyNodeRef::StmtReturn(node) + } +} + +impl<'a> From<&'a crate::StmtDelete> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtDelete) -> AnyNodeRef<'a> { + AnyNodeRef::StmtDelete(node) + } +} + +impl<'a> From<&'a crate::StmtTypeAlias> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtTypeAlias) -> AnyNodeRef<'a> { + AnyNodeRef::StmtTypeAlias(node) + } +} + +impl<'a> From<&'a crate::StmtAssign> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtAssign) -> AnyNodeRef<'a> { + AnyNodeRef::StmtAssign(node) + } +} + +impl<'a> From<&'a crate::StmtAugAssign> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtAugAssign) -> AnyNodeRef<'a> { + AnyNodeRef::StmtAugAssign(node) + } +} + +impl<'a> From<&'a crate::StmtAnnAssign> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtAnnAssign) -> AnyNodeRef<'a> { + AnyNodeRef::StmtAnnAssign(node) + } +} + +impl<'a> From<&'a crate::StmtFor> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtFor) -> AnyNodeRef<'a> { + AnyNodeRef::StmtFor(node) + } +} + +impl<'a> From<&'a crate::StmtWhile> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtWhile) -> AnyNodeRef<'a> { + AnyNodeRef::StmtWhile(node) + } +} + +impl<'a> From<&'a crate::StmtIf> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtIf) -> AnyNodeRef<'a> { + AnyNodeRef::StmtIf(node) + } +} + +impl<'a> From<&'a crate::StmtWith> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtWith) -> AnyNodeRef<'a> { + AnyNodeRef::StmtWith(node) + } +} + +impl<'a> From<&'a crate::StmtMatch> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtMatch) -> AnyNodeRef<'a> { + AnyNodeRef::StmtMatch(node) + } +} + +impl<'a> From<&'a crate::StmtRaise> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtRaise) -> AnyNodeRef<'a> { + AnyNodeRef::StmtRaise(node) + } +} + +impl<'a> From<&'a crate::StmtTry> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtTry) -> AnyNodeRef<'a> { + AnyNodeRef::StmtTry(node) + } +} + +impl<'a> From<&'a crate::StmtAssert> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtAssert) -> AnyNodeRef<'a> { + AnyNodeRef::StmtAssert(node) + } +} + +impl<'a> From<&'a crate::StmtImport> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtImport) -> AnyNodeRef<'a> { + AnyNodeRef::StmtImport(node) + } +} + +impl<'a> From<&'a crate::StmtImportFrom> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtImportFrom) -> AnyNodeRef<'a> { + AnyNodeRef::StmtImportFrom(node) + } +} + +impl<'a> From<&'a crate::StmtGlobal> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtGlobal) -> AnyNodeRef<'a> { + AnyNodeRef::StmtGlobal(node) + } +} + +impl<'a> From<&'a crate::StmtNonlocal> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtNonlocal) -> AnyNodeRef<'a> { + AnyNodeRef::StmtNonlocal(node) + } +} + +impl<'a> From<&'a crate::StmtExpr> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtExpr) -> AnyNodeRef<'a> { + AnyNodeRef::StmtExpr(node) + } +} + +impl<'a> From<&'a crate::StmtPass> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtPass) -> AnyNodeRef<'a> { + AnyNodeRef::StmtPass(node) + } +} + +impl<'a> From<&'a crate::StmtBreak> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtBreak) -> AnyNodeRef<'a> { + AnyNodeRef::StmtBreak(node) + } +} + +impl<'a> From<&'a crate::StmtContinue> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtContinue) -> AnyNodeRef<'a> { + AnyNodeRef::StmtContinue(node) + } +} + +impl<'a> From<&'a crate::StmtIpyEscapeCommand> for AnyNodeRef<'a> { + fn from(node: &'a crate::StmtIpyEscapeCommand) -> AnyNodeRef<'a> { + AnyNodeRef::StmtIpyEscapeCommand(node) + } +} + +impl<'a> From<&'a crate::ExprBoolOp> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprBoolOp) -> AnyNodeRef<'a> { + AnyNodeRef::ExprBoolOp(node) + } +} + +impl<'a> From<&'a crate::ExprNamed> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprNamed) -> AnyNodeRef<'a> { + AnyNodeRef::ExprNamed(node) + } +} + +impl<'a> From<&'a crate::ExprBinOp> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprBinOp) -> AnyNodeRef<'a> { + AnyNodeRef::ExprBinOp(node) + } +} + +impl<'a> From<&'a crate::ExprUnaryOp> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprUnaryOp) -> AnyNodeRef<'a> { + AnyNodeRef::ExprUnaryOp(node) + } +} + +impl<'a> From<&'a crate::ExprLambda> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprLambda) -> AnyNodeRef<'a> { + AnyNodeRef::ExprLambda(node) + } +} + +impl<'a> From<&'a crate::ExprIf> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprIf) -> AnyNodeRef<'a> { + AnyNodeRef::ExprIf(node) + } +} + +impl<'a> From<&'a crate::ExprDict> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprDict) -> AnyNodeRef<'a> { + AnyNodeRef::ExprDict(node) + } +} + +impl<'a> From<&'a crate::ExprSet> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprSet) -> AnyNodeRef<'a> { + AnyNodeRef::ExprSet(node) + } +} + +impl<'a> From<&'a crate::ExprListComp> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprListComp) -> AnyNodeRef<'a> { + AnyNodeRef::ExprListComp(node) + } +} + +impl<'a> From<&'a crate::ExprSetComp> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprSetComp) -> AnyNodeRef<'a> { + AnyNodeRef::ExprSetComp(node) + } +} + +impl<'a> From<&'a crate::ExprDictComp> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprDictComp) -> AnyNodeRef<'a> { + AnyNodeRef::ExprDictComp(node) + } +} + +impl<'a> From<&'a crate::ExprGenerator> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprGenerator) -> AnyNodeRef<'a> { + AnyNodeRef::ExprGenerator(node) + } +} + +impl<'a> From<&'a crate::ExprAwait> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprAwait) -> AnyNodeRef<'a> { + AnyNodeRef::ExprAwait(node) + } +} + +impl<'a> From<&'a crate::ExprYield> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprYield) -> AnyNodeRef<'a> { + AnyNodeRef::ExprYield(node) + } +} + +impl<'a> From<&'a crate::ExprYieldFrom> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprYieldFrom) -> AnyNodeRef<'a> { + AnyNodeRef::ExprYieldFrom(node) + } +} + +impl<'a> From<&'a crate::ExprCompare> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprCompare) -> AnyNodeRef<'a> { + AnyNodeRef::ExprCompare(node) + } +} + +impl<'a> From<&'a crate::ExprCall> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprCall) -> AnyNodeRef<'a> { + AnyNodeRef::ExprCall(node) + } +} + +impl<'a> From<&'a crate::ExprFString> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprFString) -> AnyNodeRef<'a> { + AnyNodeRef::ExprFString(node) + } +} + +impl<'a> From<&'a crate::ExprTString> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprTString) -> AnyNodeRef<'a> { + AnyNodeRef::ExprTString(node) + } +} + +impl<'a> From<&'a crate::ExprStringLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprStringLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::ExprStringLiteral(node) + } +} + +impl<'a> From<&'a crate::ExprBytesLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprBytesLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::ExprBytesLiteral(node) + } +} + +impl<'a> From<&'a crate::ExprNumberLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprNumberLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::ExprNumberLiteral(node) + } +} + +impl<'a> From<&'a crate::ExprBooleanLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprBooleanLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::ExprBooleanLiteral(node) + } +} + +impl<'a> From<&'a crate::ExprNoneLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprNoneLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::ExprNoneLiteral(node) + } +} + +impl<'a> From<&'a crate::ExprEllipsisLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprEllipsisLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::ExprEllipsisLiteral(node) + } +} + +impl<'a> From<&'a crate::ExprAttribute> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprAttribute) -> AnyNodeRef<'a> { + AnyNodeRef::ExprAttribute(node) + } +} + +impl<'a> From<&'a crate::ExprSubscript> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprSubscript) -> AnyNodeRef<'a> { + AnyNodeRef::ExprSubscript(node) + } +} + +impl<'a> From<&'a crate::ExprStarred> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprStarred) -> AnyNodeRef<'a> { + AnyNodeRef::ExprStarred(node) + } +} + +impl<'a> From<&'a crate::ExprName> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprName) -> AnyNodeRef<'a> { + AnyNodeRef::ExprName(node) + } +} + +impl<'a> From<&'a crate::ExprList> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprList) -> AnyNodeRef<'a> { + AnyNodeRef::ExprList(node) + } +} + +impl<'a> From<&'a crate::ExprTuple> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprTuple) -> AnyNodeRef<'a> { + AnyNodeRef::ExprTuple(node) + } +} + +impl<'a> From<&'a crate::ExprSlice> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprSlice) -> AnyNodeRef<'a> { + AnyNodeRef::ExprSlice(node) + } +} + +impl<'a> From<&'a crate::ExprIpyEscapeCommand> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprIpyEscapeCommand) -> AnyNodeRef<'a> { + AnyNodeRef::ExprIpyEscapeCommand(node) + } +} + +impl<'a> From<&'a crate::ExceptHandlerExceptHandler> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExceptHandlerExceptHandler) -> AnyNodeRef<'a> { + AnyNodeRef::ExceptHandlerExceptHandler(node) + } +} + +impl<'a> From<&'a crate::InterpolatedElement> for AnyNodeRef<'a> { + fn from(node: &'a crate::InterpolatedElement) -> AnyNodeRef<'a> { + AnyNodeRef::InterpolatedElement(node) + } +} + +impl<'a> From<&'a crate::InterpolatedStringLiteralElement> for AnyNodeRef<'a> { + fn from(node: &'a crate::InterpolatedStringLiteralElement) -> AnyNodeRef<'a> { + AnyNodeRef::InterpolatedStringLiteralElement(node) + } +} + +impl<'a> From<&'a crate::PatternMatchValue> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchValue) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchValue(node) + } +} + +impl<'a> From<&'a crate::PatternMatchSingleton> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchSingleton) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchSingleton(node) + } +} + +impl<'a> From<&'a crate::PatternMatchSequence> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchSequence) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchSequence(node) + } +} + +impl<'a> From<&'a crate::PatternMatchMapping> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchMapping) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchMapping(node) + } +} + +impl<'a> From<&'a crate::PatternMatchClass> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchClass) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchClass(node) + } +} + +impl<'a> From<&'a crate::PatternMatchStar> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchStar) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchStar(node) + } +} + +impl<'a> From<&'a crate::PatternMatchAs> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchAs) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchAs(node) + } +} + +impl<'a> From<&'a crate::PatternMatchOr> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternMatchOr) -> AnyNodeRef<'a> { + AnyNodeRef::PatternMatchOr(node) + } +} + +impl<'a> From<&'a crate::TypeParamTypeVar> for AnyNodeRef<'a> { + fn from(node: &'a crate::TypeParamTypeVar) -> AnyNodeRef<'a> { + AnyNodeRef::TypeParamTypeVar(node) + } +} + +impl<'a> From<&'a crate::TypeParamTypeVarTuple> for AnyNodeRef<'a> { + fn from(node: &'a crate::TypeParamTypeVarTuple) -> AnyNodeRef<'a> { + AnyNodeRef::TypeParamTypeVarTuple(node) + } +} + +impl<'a> From<&'a crate::TypeParamParamSpec> for AnyNodeRef<'a> { + fn from(node: &'a crate::TypeParamParamSpec) -> AnyNodeRef<'a> { + AnyNodeRef::TypeParamParamSpec(node) + } +} + +impl<'a> From<&'a crate::InterpolatedStringFormatSpec> for AnyNodeRef<'a> { + fn from(node: &'a crate::InterpolatedStringFormatSpec) -> AnyNodeRef<'a> { + AnyNodeRef::InterpolatedStringFormatSpec(node) + } +} + +impl<'a> From<&'a crate::PatternArguments> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternArguments) -> AnyNodeRef<'a> { + AnyNodeRef::PatternArguments(node) + } +} + +impl<'a> From<&'a crate::PatternKeyword> for AnyNodeRef<'a> { + fn from(node: &'a crate::PatternKeyword) -> AnyNodeRef<'a> { + AnyNodeRef::PatternKeyword(node) + } +} + +impl<'a> From<&'a crate::Comprehension> for AnyNodeRef<'a> { + fn from(node: &'a crate::Comprehension) -> AnyNodeRef<'a> { + AnyNodeRef::Comprehension(node) + } +} + +impl<'a> From<&'a crate::Arguments> for AnyNodeRef<'a> { + fn from(node: &'a crate::Arguments) -> AnyNodeRef<'a> { + AnyNodeRef::Arguments(node) + } +} + +impl<'a> From<&'a crate::Parameters> for AnyNodeRef<'a> { + fn from(node: &'a crate::Parameters) -> AnyNodeRef<'a> { + AnyNodeRef::Parameters(node) + } +} + +impl<'a> From<&'a crate::Parameter> for AnyNodeRef<'a> { + fn from(node: &'a crate::Parameter) -> AnyNodeRef<'a> { + AnyNodeRef::Parameter(node) + } +} + +impl<'a> From<&'a crate::ParameterWithDefault> for AnyNodeRef<'a> { + fn from(node: &'a crate::ParameterWithDefault) -> AnyNodeRef<'a> { + AnyNodeRef::ParameterWithDefault(node) + } +} + +impl<'a> From<&'a crate::Keyword> for AnyNodeRef<'a> { + fn from(node: &'a crate::Keyword) -> AnyNodeRef<'a> { + AnyNodeRef::Keyword(node) + } +} + +impl<'a> From<&'a crate::Alias> for AnyNodeRef<'a> { + fn from(node: &'a crate::Alias) -> AnyNodeRef<'a> { + AnyNodeRef::Alias(node) + } +} + +impl<'a> From<&'a crate::WithItem> for AnyNodeRef<'a> { + fn from(node: &'a crate::WithItem) -> AnyNodeRef<'a> { + AnyNodeRef::WithItem(node) + } +} + +impl<'a> From<&'a crate::MatchCase> for AnyNodeRef<'a> { + fn from(node: &'a crate::MatchCase) -> AnyNodeRef<'a> { + AnyNodeRef::MatchCase(node) + } +} + +impl<'a> From<&'a crate::Decorator> for AnyNodeRef<'a> { + fn from(node: &'a crate::Decorator) -> AnyNodeRef<'a> { + AnyNodeRef::Decorator(node) + } +} + +impl<'a> From<&'a crate::ElifElseClause> for AnyNodeRef<'a> { + fn from(node: &'a crate::ElifElseClause) -> AnyNodeRef<'a> { + AnyNodeRef::ElifElseClause(node) + } +} + +impl<'a> From<&'a crate::TypeParams> for AnyNodeRef<'a> { + fn from(node: &'a crate::TypeParams) -> AnyNodeRef<'a> { + AnyNodeRef::TypeParams(node) + } +} + +impl<'a> From<&'a crate::FString> for AnyNodeRef<'a> { + fn from(node: &'a crate::FString) -> AnyNodeRef<'a> { + AnyNodeRef::FString(node) + } +} + +impl<'a> From<&'a crate::TString> for AnyNodeRef<'a> { + fn from(node: &'a crate::TString) -> AnyNodeRef<'a> { + AnyNodeRef::TString(node) + } +} + +impl<'a> From<&'a crate::StringLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::StringLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::StringLiteral(node) + } +} + +impl<'a> From<&'a crate::BytesLiteral> for AnyNodeRef<'a> { + fn from(node: &'a crate::BytesLiteral) -> AnyNodeRef<'a> { + AnyNodeRef::BytesLiteral(node) + } +} + +impl<'a> From<&'a crate::Identifier> for AnyNodeRef<'a> { + fn from(node: &'a crate::Identifier) -> AnyNodeRef<'a> { + AnyNodeRef::Identifier(node) + } +} + +impl ruff_text_size::Ranged for AnyNodeRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + AnyNodeRef::ModModule(node) => node.range(), + AnyNodeRef::ModExpression(node) => node.range(), + AnyNodeRef::StmtFunctionDef(node) => node.range(), + AnyNodeRef::StmtClassDef(node) => node.range(), + AnyNodeRef::StmtReturn(node) => node.range(), + AnyNodeRef::StmtDelete(node) => node.range(), + AnyNodeRef::StmtTypeAlias(node) => node.range(), + AnyNodeRef::StmtAssign(node) => node.range(), + AnyNodeRef::StmtAugAssign(node) => node.range(), + AnyNodeRef::StmtAnnAssign(node) => node.range(), + AnyNodeRef::StmtFor(node) => node.range(), + AnyNodeRef::StmtWhile(node) => node.range(), + AnyNodeRef::StmtIf(node) => node.range(), + AnyNodeRef::StmtWith(node) => node.range(), + AnyNodeRef::StmtMatch(node) => node.range(), + AnyNodeRef::StmtRaise(node) => node.range(), + AnyNodeRef::StmtTry(node) => node.range(), + AnyNodeRef::StmtAssert(node) => node.range(), + AnyNodeRef::StmtImport(node) => node.range(), + AnyNodeRef::StmtImportFrom(node) => node.range(), + AnyNodeRef::StmtGlobal(node) => node.range(), + AnyNodeRef::StmtNonlocal(node) => node.range(), + AnyNodeRef::StmtExpr(node) => node.range(), + AnyNodeRef::StmtPass(node) => node.range(), + AnyNodeRef::StmtBreak(node) => node.range(), + AnyNodeRef::StmtContinue(node) => node.range(), + AnyNodeRef::StmtIpyEscapeCommand(node) => node.range(), + AnyNodeRef::ExprBoolOp(node) => node.range(), + AnyNodeRef::ExprNamed(node) => node.range(), + AnyNodeRef::ExprBinOp(node) => node.range(), + AnyNodeRef::ExprUnaryOp(node) => node.range(), + AnyNodeRef::ExprLambda(node) => node.range(), + AnyNodeRef::ExprIf(node) => node.range(), + AnyNodeRef::ExprDict(node) => node.range(), + AnyNodeRef::ExprSet(node) => node.range(), + AnyNodeRef::ExprListComp(node) => node.range(), + AnyNodeRef::ExprSetComp(node) => node.range(), + AnyNodeRef::ExprDictComp(node) => node.range(), + AnyNodeRef::ExprGenerator(node) => node.range(), + AnyNodeRef::ExprAwait(node) => node.range(), + AnyNodeRef::ExprYield(node) => node.range(), + AnyNodeRef::ExprYieldFrom(node) => node.range(), + AnyNodeRef::ExprCompare(node) => node.range(), + AnyNodeRef::ExprCall(node) => node.range(), + AnyNodeRef::ExprFString(node) => node.range(), + AnyNodeRef::ExprTString(node) => node.range(), + AnyNodeRef::ExprStringLiteral(node) => node.range(), + AnyNodeRef::ExprBytesLiteral(node) => node.range(), + AnyNodeRef::ExprNumberLiteral(node) => node.range(), + AnyNodeRef::ExprBooleanLiteral(node) => node.range(), + AnyNodeRef::ExprNoneLiteral(node) => node.range(), + AnyNodeRef::ExprEllipsisLiteral(node) => node.range(), + AnyNodeRef::ExprAttribute(node) => node.range(), + AnyNodeRef::ExprSubscript(node) => node.range(), + AnyNodeRef::ExprStarred(node) => node.range(), + AnyNodeRef::ExprName(node) => node.range(), + AnyNodeRef::ExprList(node) => node.range(), + AnyNodeRef::ExprTuple(node) => node.range(), + AnyNodeRef::ExprSlice(node) => node.range(), + AnyNodeRef::ExprIpyEscapeCommand(node) => node.range(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(), + AnyNodeRef::InterpolatedElement(node) => node.range(), + AnyNodeRef::InterpolatedStringLiteralElement(node) => node.range(), + AnyNodeRef::PatternMatchValue(node) => node.range(), + AnyNodeRef::PatternMatchSingleton(node) => node.range(), + AnyNodeRef::PatternMatchSequence(node) => node.range(), + AnyNodeRef::PatternMatchMapping(node) => node.range(), + AnyNodeRef::PatternMatchClass(node) => node.range(), + AnyNodeRef::PatternMatchStar(node) => node.range(), + AnyNodeRef::PatternMatchAs(node) => node.range(), + AnyNodeRef::PatternMatchOr(node) => node.range(), + AnyNodeRef::TypeParamTypeVar(node) => node.range(), + AnyNodeRef::TypeParamTypeVarTuple(node) => node.range(), + AnyNodeRef::TypeParamParamSpec(node) => node.range(), + AnyNodeRef::InterpolatedStringFormatSpec(node) => node.range(), + AnyNodeRef::PatternArguments(node) => node.range(), + AnyNodeRef::PatternKeyword(node) => node.range(), + AnyNodeRef::Comprehension(node) => node.range(), + AnyNodeRef::Arguments(node) => node.range(), + AnyNodeRef::Parameters(node) => node.range(), + AnyNodeRef::Parameter(node) => node.range(), + AnyNodeRef::ParameterWithDefault(node) => node.range(), + AnyNodeRef::Keyword(node) => node.range(), + AnyNodeRef::Alias(node) => node.range(), + AnyNodeRef::WithItem(node) => node.range(), + AnyNodeRef::MatchCase(node) => node.range(), + AnyNodeRef::Decorator(node) => node.range(), + AnyNodeRef::ElifElseClause(node) => node.range(), + AnyNodeRef::TypeParams(node) => node.range(), + AnyNodeRef::FString(node) => node.range(), + AnyNodeRef::TString(node) => node.range(), + AnyNodeRef::StringLiteral(node) => node.range(), + AnyNodeRef::BytesLiteral(node) => node.range(), + AnyNodeRef::Identifier(node) => node.range(), + } + } +} + +impl crate::HasNodeIndex for AnyNodeRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + AnyNodeRef::ModModule(node) => node.node_index(), + AnyNodeRef::ModExpression(node) => node.node_index(), + AnyNodeRef::StmtFunctionDef(node) => node.node_index(), + AnyNodeRef::StmtClassDef(node) => node.node_index(), + AnyNodeRef::StmtReturn(node) => node.node_index(), + AnyNodeRef::StmtDelete(node) => node.node_index(), + AnyNodeRef::StmtTypeAlias(node) => node.node_index(), + AnyNodeRef::StmtAssign(node) => node.node_index(), + AnyNodeRef::StmtAugAssign(node) => node.node_index(), + AnyNodeRef::StmtAnnAssign(node) => node.node_index(), + AnyNodeRef::StmtFor(node) => node.node_index(), + AnyNodeRef::StmtWhile(node) => node.node_index(), + AnyNodeRef::StmtIf(node) => node.node_index(), + AnyNodeRef::StmtWith(node) => node.node_index(), + AnyNodeRef::StmtMatch(node) => node.node_index(), + AnyNodeRef::StmtRaise(node) => node.node_index(), + AnyNodeRef::StmtTry(node) => node.node_index(), + AnyNodeRef::StmtAssert(node) => node.node_index(), + AnyNodeRef::StmtImport(node) => node.node_index(), + AnyNodeRef::StmtImportFrom(node) => node.node_index(), + AnyNodeRef::StmtGlobal(node) => node.node_index(), + AnyNodeRef::StmtNonlocal(node) => node.node_index(), + AnyNodeRef::StmtExpr(node) => node.node_index(), + AnyNodeRef::StmtPass(node) => node.node_index(), + AnyNodeRef::StmtBreak(node) => node.node_index(), + AnyNodeRef::StmtContinue(node) => node.node_index(), + AnyNodeRef::StmtIpyEscapeCommand(node) => node.node_index(), + AnyNodeRef::ExprBoolOp(node) => node.node_index(), + AnyNodeRef::ExprNamed(node) => node.node_index(), + AnyNodeRef::ExprBinOp(node) => node.node_index(), + AnyNodeRef::ExprUnaryOp(node) => node.node_index(), + AnyNodeRef::ExprLambda(node) => node.node_index(), + AnyNodeRef::ExprIf(node) => node.node_index(), + AnyNodeRef::ExprDict(node) => node.node_index(), + AnyNodeRef::ExprSet(node) => node.node_index(), + AnyNodeRef::ExprListComp(node) => node.node_index(), + AnyNodeRef::ExprSetComp(node) => node.node_index(), + AnyNodeRef::ExprDictComp(node) => node.node_index(), + AnyNodeRef::ExprGenerator(node) => node.node_index(), + AnyNodeRef::ExprAwait(node) => node.node_index(), + AnyNodeRef::ExprYield(node) => node.node_index(), + AnyNodeRef::ExprYieldFrom(node) => node.node_index(), + AnyNodeRef::ExprCompare(node) => node.node_index(), + AnyNodeRef::ExprCall(node) => node.node_index(), + AnyNodeRef::ExprFString(node) => node.node_index(), + AnyNodeRef::ExprTString(node) => node.node_index(), + AnyNodeRef::ExprStringLiteral(node) => node.node_index(), + AnyNodeRef::ExprBytesLiteral(node) => node.node_index(), + AnyNodeRef::ExprNumberLiteral(node) => node.node_index(), + AnyNodeRef::ExprBooleanLiteral(node) => node.node_index(), + AnyNodeRef::ExprNoneLiteral(node) => node.node_index(), + AnyNodeRef::ExprEllipsisLiteral(node) => node.node_index(), + AnyNodeRef::ExprAttribute(node) => node.node_index(), + AnyNodeRef::ExprSubscript(node) => node.node_index(), + AnyNodeRef::ExprStarred(node) => node.node_index(), + AnyNodeRef::ExprName(node) => node.node_index(), + AnyNodeRef::ExprList(node) => node.node_index(), + AnyNodeRef::ExprTuple(node) => node.node_index(), + AnyNodeRef::ExprSlice(node) => node.node_index(), + AnyNodeRef::ExprIpyEscapeCommand(node) => node.node_index(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => node.node_index(), + AnyNodeRef::InterpolatedElement(node) => node.node_index(), + AnyNodeRef::InterpolatedStringLiteralElement(node) => node.node_index(), + AnyNodeRef::PatternMatchValue(node) => node.node_index(), + AnyNodeRef::PatternMatchSingleton(node) => node.node_index(), + AnyNodeRef::PatternMatchSequence(node) => node.node_index(), + AnyNodeRef::PatternMatchMapping(node) => node.node_index(), + AnyNodeRef::PatternMatchClass(node) => node.node_index(), + AnyNodeRef::PatternMatchStar(node) => node.node_index(), + AnyNodeRef::PatternMatchAs(node) => node.node_index(), + AnyNodeRef::PatternMatchOr(node) => node.node_index(), + AnyNodeRef::TypeParamTypeVar(node) => node.node_index(), + AnyNodeRef::TypeParamTypeVarTuple(node) => node.node_index(), + AnyNodeRef::TypeParamParamSpec(node) => node.node_index(), + AnyNodeRef::InterpolatedStringFormatSpec(node) => node.node_index(), + AnyNodeRef::PatternArguments(node) => node.node_index(), + AnyNodeRef::PatternKeyword(node) => node.node_index(), + AnyNodeRef::Comprehension(node) => node.node_index(), + AnyNodeRef::Arguments(node) => node.node_index(), + AnyNodeRef::Parameters(node) => node.node_index(), + AnyNodeRef::Parameter(node) => node.node_index(), + AnyNodeRef::ParameterWithDefault(node) => node.node_index(), + AnyNodeRef::Keyword(node) => node.node_index(), + AnyNodeRef::Alias(node) => node.node_index(), + AnyNodeRef::WithItem(node) => node.node_index(), + AnyNodeRef::MatchCase(node) => node.node_index(), + AnyNodeRef::Decorator(node) => node.node_index(), + AnyNodeRef::ElifElseClause(node) => node.node_index(), + AnyNodeRef::TypeParams(node) => node.node_index(), + AnyNodeRef::FString(node) => node.node_index(), + AnyNodeRef::TString(node) => node.node_index(), + AnyNodeRef::StringLiteral(node) => node.node_index(), + AnyNodeRef::BytesLiteral(node) => node.node_index(), + AnyNodeRef::Identifier(node) => node.node_index(), + } + } +} + +impl AnyNodeRef<'_> { + pub fn as_ptr(&self) -> std::ptr::NonNull<()> { + match self { + AnyNodeRef::ModModule(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ModExpression(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtFunctionDef(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtClassDef(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtReturn(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtDelete(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtTypeAlias(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtAssign(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtAugAssign(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtAnnAssign(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtFor(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtWhile(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtIf(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtWith(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtMatch(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtRaise(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtTry(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtAssert(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtImport(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtImportFrom(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtGlobal(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtNonlocal(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtExpr(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtPass(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtBreak(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtContinue(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StmtIpyEscapeCommand(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprBoolOp(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprNamed(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprBinOp(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprUnaryOp(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprLambda(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprIf(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprDict(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprSet(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprListComp(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprSetComp(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprDictComp(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprGenerator(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprAwait(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprYield(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprYieldFrom(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprCompare(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprCall(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprFString(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprTString(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprStringLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprBytesLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprNumberLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprBooleanLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprNoneLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprEllipsisLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprAttribute(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprSubscript(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprStarred(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprName(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprList(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprTuple(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprSlice(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprIpyEscapeCommand(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::InterpolatedElement(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::InterpolatedStringLiteralElement(node) => { + std::ptr::NonNull::from(*node).cast() + } + AnyNodeRef::PatternMatchValue(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchSingleton(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchSequence(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchMapping(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchClass(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchStar(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchAs(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternMatchOr(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::TypeParamTypeVar(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::TypeParamTypeVarTuple(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::TypeParamParamSpec(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::InterpolatedStringFormatSpec(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternArguments(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::PatternKeyword(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Comprehension(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Arguments(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Parameters(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Parameter(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ParameterWithDefault(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Keyword(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Alias(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::WithItem(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::MatchCase(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Decorator(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ElifElseClause(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::TypeParams(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::FString(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::TString(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::StringLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::BytesLiteral(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::Identifier(node) => std::ptr::NonNull::from(*node).cast(), + } + } +} + +impl<'a> AnyNodeRef<'a> { + pub fn visit_source_order<'b, V>(self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'b> + ?Sized, + 'a: 'b, + { + match self { + AnyNodeRef::ModModule(node) => node.visit_source_order(visitor), + AnyNodeRef::ModExpression(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtFunctionDef(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtClassDef(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtReturn(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtDelete(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtTypeAlias(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtAssign(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtAugAssign(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtAnnAssign(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtFor(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtWhile(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtIf(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtWith(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtMatch(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtRaise(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtTry(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtAssert(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtImport(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtImportFrom(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtGlobal(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtNonlocal(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtExpr(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtPass(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtBreak(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtContinue(node) => node.visit_source_order(visitor), + AnyNodeRef::StmtIpyEscapeCommand(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprBoolOp(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprNamed(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprBinOp(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprUnaryOp(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprLambda(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprIf(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprDict(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprSet(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprListComp(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprSetComp(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprDictComp(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprGenerator(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprAwait(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprYield(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprYieldFrom(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprCompare(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprCall(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprFString(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprTString(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprStringLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprBytesLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprNumberLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprBooleanLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprNoneLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprEllipsisLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprAttribute(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprSubscript(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprStarred(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprName(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprList(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprTuple(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprSlice(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprIpyEscapeCommand(node) => node.visit_source_order(visitor), + AnyNodeRef::ExceptHandlerExceptHandler(node) => node.visit_source_order(visitor), + AnyNodeRef::InterpolatedElement(node) => node.visit_source_order(visitor), + AnyNodeRef::InterpolatedStringLiteralElement(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchValue(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchSingleton(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchSequence(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchMapping(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchClass(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchStar(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchAs(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternMatchOr(node) => node.visit_source_order(visitor), + AnyNodeRef::TypeParamTypeVar(node) => node.visit_source_order(visitor), + AnyNodeRef::TypeParamTypeVarTuple(node) => node.visit_source_order(visitor), + AnyNodeRef::TypeParamParamSpec(node) => node.visit_source_order(visitor), + AnyNodeRef::InterpolatedStringFormatSpec(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternArguments(node) => node.visit_source_order(visitor), + AnyNodeRef::PatternKeyword(node) => node.visit_source_order(visitor), + AnyNodeRef::Comprehension(node) => node.visit_source_order(visitor), + AnyNodeRef::Arguments(node) => node.visit_source_order(visitor), + AnyNodeRef::Parameters(node) => node.visit_source_order(visitor), + AnyNodeRef::Parameter(node) => node.visit_source_order(visitor), + AnyNodeRef::ParameterWithDefault(node) => node.visit_source_order(visitor), + AnyNodeRef::Keyword(node) => node.visit_source_order(visitor), + AnyNodeRef::Alias(node) => node.visit_source_order(visitor), + AnyNodeRef::WithItem(node) => node.visit_source_order(visitor), + AnyNodeRef::MatchCase(node) => node.visit_source_order(visitor), + AnyNodeRef::Decorator(node) => node.visit_source_order(visitor), + AnyNodeRef::ElifElseClause(node) => node.visit_source_order(visitor), + AnyNodeRef::TypeParams(node) => node.visit_source_order(visitor), + AnyNodeRef::FString(node) => node.visit_source_order(visitor), + AnyNodeRef::TString(node) => node.visit_source_order(visitor), + AnyNodeRef::StringLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::BytesLiteral(node) => node.visit_source_order(visitor), + AnyNodeRef::Identifier(node) => node.visit_source_order(visitor), + } + } +} + +impl AnyNodeRef<'_> { + pub const fn is_module(self) -> bool { + matches!( + self, + AnyNodeRef::ModModule(_) | AnyNodeRef::ModExpression(_) + ) + } +} + +impl AnyNodeRef<'_> { + pub const fn is_statement(self) -> bool { + matches!( + self, + AnyNodeRef::StmtFunctionDef(_) + | AnyNodeRef::StmtClassDef(_) + | AnyNodeRef::StmtReturn(_) + | AnyNodeRef::StmtDelete(_) + | AnyNodeRef::StmtTypeAlias(_) + | AnyNodeRef::StmtAssign(_) + | AnyNodeRef::StmtAugAssign(_) + | AnyNodeRef::StmtAnnAssign(_) + | AnyNodeRef::StmtFor(_) + | AnyNodeRef::StmtWhile(_) + | AnyNodeRef::StmtIf(_) + | AnyNodeRef::StmtWith(_) + | AnyNodeRef::StmtMatch(_) + | AnyNodeRef::StmtRaise(_) + | AnyNodeRef::StmtTry(_) + | AnyNodeRef::StmtAssert(_) + | AnyNodeRef::StmtImport(_) + | AnyNodeRef::StmtImportFrom(_) + | AnyNodeRef::StmtGlobal(_) + | AnyNodeRef::StmtNonlocal(_) + | AnyNodeRef::StmtExpr(_) + | AnyNodeRef::StmtPass(_) + | AnyNodeRef::StmtBreak(_) + | AnyNodeRef::StmtContinue(_) + | AnyNodeRef::StmtIpyEscapeCommand(_) + ) + } +} + +impl AnyNodeRef<'_> { + pub const fn is_expression(self) -> bool { + matches!( + self, + AnyNodeRef::ExprBoolOp(_) + | AnyNodeRef::ExprNamed(_) + | AnyNodeRef::ExprBinOp(_) + | AnyNodeRef::ExprUnaryOp(_) + | AnyNodeRef::ExprLambda(_) + | AnyNodeRef::ExprIf(_) + | AnyNodeRef::ExprDict(_) + | AnyNodeRef::ExprSet(_) + | AnyNodeRef::ExprListComp(_) + | AnyNodeRef::ExprSetComp(_) + | AnyNodeRef::ExprDictComp(_) + | AnyNodeRef::ExprGenerator(_) + | AnyNodeRef::ExprAwait(_) + | AnyNodeRef::ExprYield(_) + | AnyNodeRef::ExprYieldFrom(_) + | AnyNodeRef::ExprCompare(_) + | AnyNodeRef::ExprCall(_) + | AnyNodeRef::ExprFString(_) + | AnyNodeRef::ExprTString(_) + | AnyNodeRef::ExprStringLiteral(_) + | AnyNodeRef::ExprBytesLiteral(_) + | AnyNodeRef::ExprNumberLiteral(_) + | AnyNodeRef::ExprBooleanLiteral(_) + | AnyNodeRef::ExprNoneLiteral(_) + | AnyNodeRef::ExprEllipsisLiteral(_) + | AnyNodeRef::ExprAttribute(_) + | AnyNodeRef::ExprSubscript(_) + | AnyNodeRef::ExprStarred(_) + | AnyNodeRef::ExprName(_) + | AnyNodeRef::ExprList(_) + | AnyNodeRef::ExprTuple(_) + | AnyNodeRef::ExprSlice(_) + | AnyNodeRef::ExprIpyEscapeCommand(_) + ) + } +} + +impl AnyNodeRef<'_> { + pub const fn is_except_handler(self) -> bool { + matches!(self, AnyNodeRef::ExceptHandlerExceptHandler(_)) + } +} + +impl AnyNodeRef<'_> { + pub const fn is_interpolated_string_element(self) -> bool { + matches!( + self, + AnyNodeRef::InterpolatedElement(_) | AnyNodeRef::InterpolatedStringLiteralElement(_) + ) + } +} + +impl AnyNodeRef<'_> { + pub const fn is_pattern(self) -> bool { + matches!( + self, + AnyNodeRef::PatternMatchValue(_) + | AnyNodeRef::PatternMatchSingleton(_) + | AnyNodeRef::PatternMatchSequence(_) + | AnyNodeRef::PatternMatchMapping(_) + | AnyNodeRef::PatternMatchClass(_) + | AnyNodeRef::PatternMatchStar(_) + | AnyNodeRef::PatternMatchAs(_) + | AnyNodeRef::PatternMatchOr(_) + ) + } +} + +impl AnyNodeRef<'_> { + pub const fn is_type_param(self) -> bool { + matches!( + self, + AnyNodeRef::TypeParamTypeVar(_) + | AnyNodeRef::TypeParamTypeVarTuple(_) + | AnyNodeRef::TypeParamParamSpec(_) + ) + } +} + +/// An enumeration of all AST nodes. +/// +/// Unlike `AnyNodeRef`, this type does not flatten nested enums, so its variants only +/// consist of the "root" AST node types. This is useful as it exposes references to the +/// original enums, not just references to their inner values. +/// +/// For example, `AnyRootNodeRef::Mod` contains a reference to the `Mod` enum, while +/// `AnyNodeRef` has top-level `AnyNodeRef::ModModule` and `AnyNodeRef::ModExpression` +/// variants. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum AnyRootNodeRef<'a> { + Mod(&'a Mod), + Stmt(&'a Stmt), + Expr(&'a Expr), + ExceptHandler(&'a ExceptHandler), + InterpolatedStringElement(&'a InterpolatedStringElement), + Pattern(&'a Pattern), + TypeParam(&'a TypeParam), + InterpolatedStringFormatSpec(&'a crate::InterpolatedStringFormatSpec), + PatternArguments(&'a crate::PatternArguments), + PatternKeyword(&'a crate::PatternKeyword), + Comprehension(&'a crate::Comprehension), + Arguments(&'a crate::Arguments), + Parameters(&'a crate::Parameters), + Parameter(&'a crate::Parameter), + ParameterWithDefault(&'a crate::ParameterWithDefault), + Keyword(&'a crate::Keyword), + Alias(&'a crate::Alias), + WithItem(&'a crate::WithItem), + MatchCase(&'a crate::MatchCase), + Decorator(&'a crate::Decorator), + ElifElseClause(&'a crate::ElifElseClause), + TypeParams(&'a crate::TypeParams), + FString(&'a crate::FString), + TString(&'a crate::TString), + StringLiteral(&'a crate::StringLiteral), + BytesLiteral(&'a crate::BytesLiteral), + Identifier(&'a crate::Identifier), +} + +impl<'a> From<&'a Mod> for AnyRootNodeRef<'a> { + fn from(node: &'a Mod) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Mod(node) + } +} + +impl<'a> TryFrom> for &'a Mod { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a Mod, ()> { + match node { + AnyRootNodeRef::Mod(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ModModule { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ModModule, ()> { + match node { + AnyRootNodeRef::Mod(Mod::Module(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ModExpression { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ModExpression, ()> { + match node { + AnyRootNodeRef::Mod(Mod::Expression(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a Stmt> for AnyRootNodeRef<'a> { + fn from(node: &'a Stmt) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Stmt(node) + } +} + +impl<'a> TryFrom> for &'a Stmt { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a Stmt, ()> { + match node { + AnyRootNodeRef::Stmt(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtFunctionDef { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtFunctionDef, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::FunctionDef(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtClassDef { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtClassDef, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::ClassDef(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtReturn { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtReturn, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Return(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtDelete { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtDelete, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Delete(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtTypeAlias { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtTypeAlias, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::TypeAlias(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtAssign { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtAssign, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Assign(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtAugAssign { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtAugAssign, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::AugAssign(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtAnnAssign { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtAnnAssign, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::AnnAssign(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtFor { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtFor, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::For(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtWhile { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtWhile, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::While(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtIf { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtIf, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::If(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtWith { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtWith, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::With(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtMatch { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtMatch, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Match(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtRaise { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtRaise, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Raise(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtTry { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtTry, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Try(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtAssert { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtAssert, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Assert(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtImport { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtImport, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Import(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtImportFrom { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtImportFrom, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::ImportFrom(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtGlobal { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtGlobal, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Global(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtNonlocal { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtNonlocal, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Nonlocal(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtExpr { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtExpr, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Expr(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtPass { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtPass, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Pass(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtBreak { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtBreak, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Break(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtContinue { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtContinue, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::Continue(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::StmtIpyEscapeCommand { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StmtIpyEscapeCommand, ()> { + match node { + AnyRootNodeRef::Stmt(Stmt::IpyEscapeCommand(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a Expr> for AnyRootNodeRef<'a> { + fn from(node: &'a Expr) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Expr(node) + } +} + +impl<'a> TryFrom> for &'a Expr { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a Expr, ()> { + match node { + AnyRootNodeRef::Expr(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprBoolOp { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprBoolOp, ()> { + match node { + AnyRootNodeRef::Expr(Expr::BoolOp(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprNamed { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprNamed, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Named(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprBinOp { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprBinOp, ()> { + match node { + AnyRootNodeRef::Expr(Expr::BinOp(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprUnaryOp { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprUnaryOp, ()> { + match node { + AnyRootNodeRef::Expr(Expr::UnaryOp(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprLambda { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprLambda, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Lambda(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprIf { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprIf, ()> { + match node { + AnyRootNodeRef::Expr(Expr::If(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprDict { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprDict, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Dict(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprSet { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprSet, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Set(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprListComp { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprListComp, ()> { + match node { + AnyRootNodeRef::Expr(Expr::ListComp(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprSetComp { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprSetComp, ()> { + match node { + AnyRootNodeRef::Expr(Expr::SetComp(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprDictComp { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprDictComp, ()> { + match node { + AnyRootNodeRef::Expr(Expr::DictComp(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprGenerator { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprGenerator, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Generator(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprAwait { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprAwait, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Await(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprYield { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprYield, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Yield(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprYieldFrom { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprYieldFrom, ()> { + match node { + AnyRootNodeRef::Expr(Expr::YieldFrom(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprCompare { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprCompare, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Compare(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprCall { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprCall, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Call(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprFString { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprFString, ()> { + match node { + AnyRootNodeRef::Expr(Expr::FString(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprTString { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprTString, ()> { + match node { + AnyRootNodeRef::Expr(Expr::TString(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprStringLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprStringLiteral, ()> { + match node { + AnyRootNodeRef::Expr(Expr::StringLiteral(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprBytesLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprBytesLiteral, ()> { + match node { + AnyRootNodeRef::Expr(Expr::BytesLiteral(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprNumberLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprNumberLiteral, ()> { + match node { + AnyRootNodeRef::Expr(Expr::NumberLiteral(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprBooleanLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprBooleanLiteral, ()> { + match node { + AnyRootNodeRef::Expr(Expr::BooleanLiteral(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprNoneLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprNoneLiteral, ()> { + match node { + AnyRootNodeRef::Expr(Expr::NoneLiteral(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprEllipsisLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprEllipsisLiteral, ()> { + match node { + AnyRootNodeRef::Expr(Expr::EllipsisLiteral(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprAttribute { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprAttribute, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Attribute(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprSubscript { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprSubscript, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Subscript(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprStarred { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprStarred, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Starred(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprName { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprName, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Name(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprList { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprList, ()> { + match node { + AnyRootNodeRef::Expr(Expr::List(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprTuple { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprTuple, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Tuple(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprSlice { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprSlice, ()> { + match node { + AnyRootNodeRef::Expr(Expr::Slice(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExprIpyEscapeCommand { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExprIpyEscapeCommand, ()> { + match node { + AnyRootNodeRef::Expr(Expr::IpyEscapeCommand(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a ExceptHandler> for AnyRootNodeRef<'a> { + fn from(node: &'a ExceptHandler) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::ExceptHandler(node) + } +} + +impl<'a> TryFrom> for &'a ExceptHandler { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a ExceptHandler, ()> { + match node { + AnyRootNodeRef::ExceptHandler(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::ExceptHandlerExceptHandler { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ExceptHandlerExceptHandler, ()> { + match node { + AnyRootNodeRef::ExceptHandler(ExceptHandler::ExceptHandler(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a InterpolatedStringElement> for AnyRootNodeRef<'a> { + fn from(node: &'a InterpolatedStringElement) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::InterpolatedStringElement(node) + } +} + +impl<'a> TryFrom> for &'a InterpolatedStringElement { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a InterpolatedStringElement, ()> { + match node { + AnyRootNodeRef::InterpolatedStringElement(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::InterpolatedElement { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::InterpolatedElement, ()> { + match node { + AnyRootNodeRef::InterpolatedStringElement( + InterpolatedStringElement::Interpolation(node), + ) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::InterpolatedStringLiteralElement { + type Error = (); + fn try_from( + node: AnyRootNodeRef<'a>, + ) -> Result<&'a crate::InterpolatedStringLiteralElement, ()> { + match node { + AnyRootNodeRef::InterpolatedStringElement(InterpolatedStringElement::Literal(node)) => { + Ok(node) + } + _ => Err(()), + } + } +} + +impl<'a> From<&'a Pattern> for AnyRootNodeRef<'a> { + fn from(node: &'a Pattern) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Pattern(node) + } +} + +impl<'a> TryFrom> for &'a Pattern { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a Pattern, ()> { + match node { + AnyRootNodeRef::Pattern(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchValue { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchValue, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchValue(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchSingleton { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchSingleton, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchSingleton(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchSequence { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchSequence, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchSequence(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchMapping { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchMapping, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchMapping(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchClass { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchClass, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchClass(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchStar { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchStar, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchStar(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchAs { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchAs, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchAs(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::PatternMatchOr { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternMatchOr, ()> { + match node { + AnyRootNodeRef::Pattern(Pattern::MatchOr(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a TypeParam> for AnyRootNodeRef<'a> { + fn from(node: &'a TypeParam) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::TypeParam(node) + } +} + +impl<'a> TryFrom> for &'a TypeParam { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a TypeParam, ()> { + match node { + AnyRootNodeRef::TypeParam(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::TypeParamTypeVar { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::TypeParamTypeVar, ()> { + match node { + AnyRootNodeRef::TypeParam(TypeParam::TypeVar(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::TypeParamTypeVarTuple { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::TypeParamTypeVarTuple, ()> { + match node { + AnyRootNodeRef::TypeParam(TypeParam::TypeVarTuple(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> TryFrom> for &'a crate::TypeParamParamSpec { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::TypeParamParamSpec, ()> { + match node { + AnyRootNodeRef::TypeParam(TypeParam::ParamSpec(node)) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::InterpolatedStringFormatSpec> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::InterpolatedStringFormatSpec) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::InterpolatedStringFormatSpec(node) + } +} + +impl<'a> TryFrom> for &'a crate::InterpolatedStringFormatSpec { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::InterpolatedStringFormatSpec, ()> { + match node { + AnyRootNodeRef::InterpolatedStringFormatSpec(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::PatternArguments> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::PatternArguments) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::PatternArguments(node) + } +} + +impl<'a> TryFrom> for &'a crate::PatternArguments { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternArguments, ()> { + match node { + AnyRootNodeRef::PatternArguments(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::PatternKeyword> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::PatternKeyword) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::PatternKeyword(node) + } +} + +impl<'a> TryFrom> for &'a crate::PatternKeyword { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::PatternKeyword, ()> { + match node { + AnyRootNodeRef::PatternKeyword(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::Comprehension> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Comprehension) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Comprehension(node) + } +} + +impl<'a> TryFrom> for &'a crate::Comprehension { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Comprehension, ()> { + match node { + AnyRootNodeRef::Comprehension(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::Arguments> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Arguments) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Arguments(node) + } +} + +impl<'a> TryFrom> for &'a crate::Arguments { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Arguments, ()> { + match node { + AnyRootNodeRef::Arguments(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::Parameters> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Parameters) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Parameters(node) + } +} + +impl<'a> TryFrom> for &'a crate::Parameters { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Parameters, ()> { + match node { + AnyRootNodeRef::Parameters(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::Parameter> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Parameter) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Parameter(node) + } +} + +impl<'a> TryFrom> for &'a crate::Parameter { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Parameter, ()> { + match node { + AnyRootNodeRef::Parameter(node) => Ok(node), + _ => Err(()), } } } -impl AnyNodeRef<'_> { - pub const fn is_module(self) -> bool { - matches!( - self, - AnyNodeRef::ModModule(_) | AnyNodeRef::ModExpression(_) - ) +impl<'a> From<&'a crate::ParameterWithDefault> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::ParameterWithDefault) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::ParameterWithDefault(node) } } -impl AnyNodeRef<'_> { - pub const fn is_statement(self) -> bool { - matches!( - self, - AnyNodeRef::StmtFunctionDef(_) - | AnyNodeRef::StmtClassDef(_) - | AnyNodeRef::StmtReturn(_) - | AnyNodeRef::StmtDelete(_) - | AnyNodeRef::StmtTypeAlias(_) - | AnyNodeRef::StmtAssign(_) - | AnyNodeRef::StmtAugAssign(_) - | AnyNodeRef::StmtAnnAssign(_) - | AnyNodeRef::StmtFor(_) - | AnyNodeRef::StmtWhile(_) - | AnyNodeRef::StmtIf(_) - | AnyNodeRef::StmtWith(_) - | AnyNodeRef::StmtMatch(_) - | AnyNodeRef::StmtRaise(_) - | AnyNodeRef::StmtTry(_) - | AnyNodeRef::StmtAssert(_) - | AnyNodeRef::StmtImport(_) - | AnyNodeRef::StmtImportFrom(_) - | AnyNodeRef::StmtGlobal(_) - | AnyNodeRef::StmtNonlocal(_) - | AnyNodeRef::StmtExpr(_) - | AnyNodeRef::StmtPass(_) - | AnyNodeRef::StmtBreak(_) - | AnyNodeRef::StmtContinue(_) - | AnyNodeRef::StmtIpyEscapeCommand(_) - ) +impl<'a> TryFrom> for &'a crate::ParameterWithDefault { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ParameterWithDefault, ()> { + match node { + AnyRootNodeRef::ParameterWithDefault(node) => Ok(node), + _ => Err(()), + } } } -impl AnyNodeRef<'_> { - pub const fn is_expression(self) -> bool { - matches!( - self, - AnyNodeRef::ExprBoolOp(_) - | AnyNodeRef::ExprNamed(_) - | AnyNodeRef::ExprBinOp(_) - | AnyNodeRef::ExprUnaryOp(_) - | AnyNodeRef::ExprLambda(_) - | AnyNodeRef::ExprIf(_) - | AnyNodeRef::ExprDict(_) - | AnyNodeRef::ExprSet(_) - | AnyNodeRef::ExprListComp(_) - | AnyNodeRef::ExprSetComp(_) - | AnyNodeRef::ExprDictComp(_) - | AnyNodeRef::ExprGenerator(_) - | AnyNodeRef::ExprAwait(_) - | AnyNodeRef::ExprYield(_) - | AnyNodeRef::ExprYieldFrom(_) - | AnyNodeRef::ExprCompare(_) - | AnyNodeRef::ExprCall(_) - | AnyNodeRef::ExprFString(_) - | AnyNodeRef::ExprStringLiteral(_) - | AnyNodeRef::ExprBytesLiteral(_) - | AnyNodeRef::ExprNumberLiteral(_) - | AnyNodeRef::ExprBooleanLiteral(_) - | AnyNodeRef::ExprNoneLiteral(_) - | AnyNodeRef::ExprEllipsisLiteral(_) - | AnyNodeRef::ExprAttribute(_) - | AnyNodeRef::ExprSubscript(_) - | AnyNodeRef::ExprStarred(_) - | AnyNodeRef::ExprName(_) - | AnyNodeRef::ExprList(_) - | AnyNodeRef::ExprTuple(_) - | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExprIpyEscapeCommand(_) - ) +impl<'a> From<&'a crate::Keyword> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Keyword) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Keyword(node) } } -impl AnyNodeRef<'_> { - pub const fn is_except_handler(self) -> bool { - matches!(self, AnyNodeRef::ExceptHandlerExceptHandler(_)) +impl<'a> TryFrom> for &'a crate::Keyword { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Keyword, ()> { + match node { + AnyRootNodeRef::Keyword(node) => Ok(node), + _ => Err(()), + } } } -impl AnyNodeRef<'_> { - pub const fn is_f_string_element(self) -> bool { - matches!( - self, - AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_) - ) +impl<'a> From<&'a crate::Alias> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Alias) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Alias(node) } } -impl AnyNodeRef<'_> { - pub const fn is_pattern(self) -> bool { - matches!( - self, - AnyNodeRef::PatternMatchValue(_) - | AnyNodeRef::PatternMatchSingleton(_) - | AnyNodeRef::PatternMatchSequence(_) - | AnyNodeRef::PatternMatchMapping(_) - | AnyNodeRef::PatternMatchClass(_) - | AnyNodeRef::PatternMatchStar(_) - | AnyNodeRef::PatternMatchAs(_) - | AnyNodeRef::PatternMatchOr(_) - ) +impl<'a> TryFrom> for &'a crate::Alias { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Alias, ()> { + match node { + AnyRootNodeRef::Alias(node) => Ok(node), + _ => Err(()), + } } } -impl AnyNodeRef<'_> { - pub const fn is_type_param(self) -> bool { - matches!( - self, - AnyNodeRef::TypeParamTypeVar(_) - | AnyNodeRef::TypeParamTypeVarTuple(_) - | AnyNodeRef::TypeParamParamSpec(_) - ) +impl<'a> From<&'a crate::WithItem> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::WithItem) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::WithItem(node) + } +} + +impl<'a> TryFrom> for &'a crate::WithItem { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::WithItem, ()> { + match node { + AnyRootNodeRef::WithItem(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::MatchCase> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::MatchCase) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::MatchCase(node) + } +} + +impl<'a> TryFrom> for &'a crate::MatchCase { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::MatchCase, ()> { + match node { + AnyRootNodeRef::MatchCase(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::Decorator> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Decorator) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Decorator(node) + } +} + +impl<'a> TryFrom> for &'a crate::Decorator { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Decorator, ()> { + match node { + AnyRootNodeRef::Decorator(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::ElifElseClause> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::ElifElseClause) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::ElifElseClause(node) + } +} + +impl<'a> TryFrom> for &'a crate::ElifElseClause { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::ElifElseClause, ()> { + match node { + AnyRootNodeRef::ElifElseClause(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::TypeParams> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::TypeParams) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::TypeParams(node) + } +} + +impl<'a> TryFrom> for &'a crate::TypeParams { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::TypeParams, ()> { + match node { + AnyRootNodeRef::TypeParams(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::FString> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::FString) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::FString(node) + } +} + +impl<'a> TryFrom> for &'a crate::FString { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::FString, ()> { + match node { + AnyRootNodeRef::FString(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::TString> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::TString) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::TString(node) + } +} + +impl<'a> TryFrom> for &'a crate::TString { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::TString, ()> { + match node { + AnyRootNodeRef::TString(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::StringLiteral> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::StringLiteral) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::StringLiteral(node) + } +} + +impl<'a> TryFrom> for &'a crate::StringLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::StringLiteral, ()> { + match node { + AnyRootNodeRef::StringLiteral(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::BytesLiteral> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::BytesLiteral) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::BytesLiteral(node) + } +} + +impl<'a> TryFrom> for &'a crate::BytesLiteral { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::BytesLiteral, ()> { + match node { + AnyRootNodeRef::BytesLiteral(node) => Ok(node), + _ => Err(()), + } + } +} + +impl<'a> From<&'a crate::Identifier> for AnyRootNodeRef<'a> { + fn from(node: &'a crate::Identifier) -> AnyRootNodeRef<'a> { + AnyRootNodeRef::Identifier(node) + } +} + +impl<'a> TryFrom> for &'a crate::Identifier { + type Error = (); + fn try_from(node: AnyRootNodeRef<'a>) -> Result<&'a crate::Identifier, ()> { + match node { + AnyRootNodeRef::Identifier(node) => Ok(node), + _ => Err(()), + } + } +} + +impl ruff_text_size::Ranged for AnyRootNodeRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + AnyRootNodeRef::Mod(node) => node.range(), + AnyRootNodeRef::Stmt(node) => node.range(), + AnyRootNodeRef::Expr(node) => node.range(), + AnyRootNodeRef::ExceptHandler(node) => node.range(), + AnyRootNodeRef::InterpolatedStringElement(node) => node.range(), + AnyRootNodeRef::Pattern(node) => node.range(), + AnyRootNodeRef::TypeParam(node) => node.range(), + AnyRootNodeRef::InterpolatedStringFormatSpec(node) => node.range(), + AnyRootNodeRef::PatternArguments(node) => node.range(), + AnyRootNodeRef::PatternKeyword(node) => node.range(), + AnyRootNodeRef::Comprehension(node) => node.range(), + AnyRootNodeRef::Arguments(node) => node.range(), + AnyRootNodeRef::Parameters(node) => node.range(), + AnyRootNodeRef::Parameter(node) => node.range(), + AnyRootNodeRef::ParameterWithDefault(node) => node.range(), + AnyRootNodeRef::Keyword(node) => node.range(), + AnyRootNodeRef::Alias(node) => node.range(), + AnyRootNodeRef::WithItem(node) => node.range(), + AnyRootNodeRef::MatchCase(node) => node.range(), + AnyRootNodeRef::Decorator(node) => node.range(), + AnyRootNodeRef::ElifElseClause(node) => node.range(), + AnyRootNodeRef::TypeParams(node) => node.range(), + AnyRootNodeRef::FString(node) => node.range(), + AnyRootNodeRef::TString(node) => node.range(), + AnyRootNodeRef::StringLiteral(node) => node.range(), + AnyRootNodeRef::BytesLiteral(node) => node.range(), + AnyRootNodeRef::Identifier(node) => node.range(), + } + } +} + +impl crate::HasNodeIndex for AnyRootNodeRef<'_> { + fn node_index(&self) -> &crate::AtomicNodeIndex { + match self { + AnyRootNodeRef::Mod(node) => node.node_index(), + AnyRootNodeRef::Stmt(node) => node.node_index(), + AnyRootNodeRef::Expr(node) => node.node_index(), + AnyRootNodeRef::ExceptHandler(node) => node.node_index(), + AnyRootNodeRef::InterpolatedStringElement(node) => node.node_index(), + AnyRootNodeRef::Pattern(node) => node.node_index(), + AnyRootNodeRef::TypeParam(node) => node.node_index(), + AnyRootNodeRef::InterpolatedStringFormatSpec(node) => node.node_index(), + AnyRootNodeRef::PatternArguments(node) => node.node_index(), + AnyRootNodeRef::PatternKeyword(node) => node.node_index(), + AnyRootNodeRef::Comprehension(node) => node.node_index(), + AnyRootNodeRef::Arguments(node) => node.node_index(), + AnyRootNodeRef::Parameters(node) => node.node_index(), + AnyRootNodeRef::Parameter(node) => node.node_index(), + AnyRootNodeRef::ParameterWithDefault(node) => node.node_index(), + AnyRootNodeRef::Keyword(node) => node.node_index(), + AnyRootNodeRef::Alias(node) => node.node_index(), + AnyRootNodeRef::WithItem(node) => node.node_index(), + AnyRootNodeRef::MatchCase(node) => node.node_index(), + AnyRootNodeRef::Decorator(node) => node.node_index(), + AnyRootNodeRef::ElifElseClause(node) => node.node_index(), + AnyRootNodeRef::TypeParams(node) => node.node_index(), + AnyRootNodeRef::FString(node) => node.node_index(), + AnyRootNodeRef::TString(node) => node.node_index(), + AnyRootNodeRef::StringLiteral(node) => node.node_index(), + AnyRootNodeRef::BytesLiteral(node) => node.node_index(), + AnyRootNodeRef::Identifier(node) => node.node_index(), + } + } +} + +impl<'a> AnyRootNodeRef<'a> { + pub fn visit_source_order<'b, V>(self, visitor: &mut V) + where + V: crate::visitor::source_order::SourceOrderVisitor<'b> + ?Sized, + 'a: 'b, + { + match self { + AnyRootNodeRef::Mod(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Stmt(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Expr(node) => node.visit_source_order(visitor), + AnyRootNodeRef::ExceptHandler(node) => node.visit_source_order(visitor), + AnyRootNodeRef::InterpolatedStringElement(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Pattern(node) => node.visit_source_order(visitor), + AnyRootNodeRef::TypeParam(node) => node.visit_source_order(visitor), + AnyRootNodeRef::InterpolatedStringFormatSpec(node) => node.visit_source_order(visitor), + AnyRootNodeRef::PatternArguments(node) => node.visit_source_order(visitor), + AnyRootNodeRef::PatternKeyword(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Comprehension(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Arguments(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Parameters(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Parameter(node) => node.visit_source_order(visitor), + AnyRootNodeRef::ParameterWithDefault(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Keyword(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Alias(node) => node.visit_source_order(visitor), + AnyRootNodeRef::WithItem(node) => node.visit_source_order(visitor), + AnyRootNodeRef::MatchCase(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Decorator(node) => node.visit_source_order(visitor), + AnyRootNodeRef::ElifElseClause(node) => node.visit_source_order(visitor), + AnyRootNodeRef::TypeParams(node) => node.visit_source_order(visitor), + AnyRootNodeRef::FString(node) => node.visit_source_order(visitor), + AnyRootNodeRef::TString(node) => node.visit_source_order(visitor), + AnyRootNodeRef::StringLiteral(node) => node.visit_source_order(visitor), + AnyRootNodeRef::BytesLiteral(node) => node.visit_source_order(visitor), + AnyRootNodeRef::Identifier(node) => node.visit_source_order(visitor), + } } } @@ -6437,6 +8795,7 @@ pub enum NodeKind { ExprCompare, ExprCall, ExprFString, + ExprTString, ExprStringLiteral, ExprBytesLiteral, ExprNumberLiteral, @@ -6452,8 +8811,8 @@ pub enum NodeKind { ExprSlice, ExprIpyEscapeCommand, ExceptHandlerExceptHandler, - FStringExpressionElement, - FStringLiteralElement, + InterpolatedElement, + InterpolatedStringLiteralElement, PatternMatchValue, PatternMatchSingleton, PatternMatchSequence, @@ -6465,7 +8824,7 @@ pub enum NodeKind { TypeParamTypeVar, TypeParamTypeVarTuple, TypeParamParamSpec, - FStringFormatSpec, + InterpolatedStringFormatSpec, PatternArguments, PatternKeyword, Comprehension, @@ -6481,6 +8840,7 @@ pub enum NodeKind { ElifElseClause, TypeParams, FString, + TString, StringLiteral, BytesLiteral, Identifier, @@ -6534,6 +8894,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprCompare(_) => NodeKind::ExprCompare, AnyNodeRef::ExprCall(_) => NodeKind::ExprCall, AnyNodeRef::ExprFString(_) => NodeKind::ExprFString, + AnyNodeRef::ExprTString(_) => NodeKind::ExprTString, AnyNodeRef::ExprStringLiteral(_) => NodeKind::ExprStringLiteral, AnyNodeRef::ExprBytesLiteral(_) => NodeKind::ExprBytesLiteral, AnyNodeRef::ExprNumberLiteral(_) => NodeKind::ExprNumberLiteral, @@ -6549,8 +8910,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprSlice(_) => NodeKind::ExprSlice, AnyNodeRef::ExprIpyEscapeCommand(_) => NodeKind::ExprIpyEscapeCommand, AnyNodeRef::ExceptHandlerExceptHandler(_) => NodeKind::ExceptHandlerExceptHandler, - AnyNodeRef::FStringExpressionElement(_) => NodeKind::FStringExpressionElement, - AnyNodeRef::FStringLiteralElement(_) => NodeKind::FStringLiteralElement, + AnyNodeRef::InterpolatedElement(_) => NodeKind::InterpolatedElement, + AnyNodeRef::InterpolatedStringLiteralElement(_) => { + NodeKind::InterpolatedStringLiteralElement + } AnyNodeRef::PatternMatchValue(_) => NodeKind::PatternMatchValue, AnyNodeRef::PatternMatchSingleton(_) => NodeKind::PatternMatchSingleton, AnyNodeRef::PatternMatchSequence(_) => NodeKind::PatternMatchSequence, @@ -6562,7 +8925,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::TypeParamTypeVar(_) => NodeKind::TypeParamTypeVar, AnyNodeRef::TypeParamTypeVarTuple(_) => NodeKind::TypeParamTypeVarTuple, AnyNodeRef::TypeParamParamSpec(_) => NodeKind::TypeParamParamSpec, - AnyNodeRef::FStringFormatSpec(_) => NodeKind::FStringFormatSpec, + AnyNodeRef::InterpolatedStringFormatSpec(_) => NodeKind::InterpolatedStringFormatSpec, AnyNodeRef::PatternArguments(_) => NodeKind::PatternArguments, AnyNodeRef::PatternKeyword(_) => NodeKind::PatternKeyword, AnyNodeRef::Comprehension(_) => NodeKind::Comprehension, @@ -6578,6 +8941,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ElifElseClause(_) => NodeKind::ElifElseClause, AnyNodeRef::TypeParams(_) => NodeKind::TypeParams, AnyNodeRef::FString(_) => NodeKind::FString, + AnyNodeRef::TString(_) => NodeKind::TString, AnyNodeRef::StringLiteral(_) => NodeKind::StringLiteral, AnyNodeRef::BytesLiteral(_) => NodeKind::BytesLiteral, AnyNodeRef::Identifier(_) => NodeKind::Identifier, @@ -6587,14 +8951,18 @@ impl AnyNodeRef<'_> { /// See also [Module](https://docs.python.org/3/library/ast.html#ast.Module) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ModModule { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub body: Vec, } /// See also [Module](https://docs.python.org/3/library/ast.html#ast.Module) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ModExpression { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub body: Box, } @@ -6604,7 +8972,9 @@ pub struct ModExpression { /// /// This type differs from the original Python AST, as it collapses the synchronous and asynchronous variants into a single type. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtFunctionDef { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub is_async: bool, pub decorator_list: Vec, @@ -6617,7 +8987,9 @@ pub struct StmtFunctionDef { /// See also [ClassDef](https://docs.python.org/3/library/ast.html#ast.ClassDef) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtClassDef { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub decorator_list: Vec, pub name: crate::Identifier, @@ -6628,21 +9000,27 @@ pub struct StmtClassDef { /// See also [Return](https://docs.python.org/3/library/ast.html#ast.Return) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtReturn { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Option>, } /// See also [Delete](https://docs.python.org/3/library/ast.html#ast.Delete) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtDelete { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub targets: Vec, } /// See also [TypeAlias](https://docs.python.org/3/library/ast.html#ast.TypeAlias) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtTypeAlias { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub name: Box, pub type_params: Option>, @@ -6651,7 +9029,9 @@ pub struct StmtTypeAlias { /// See also [Assign](https://docs.python.org/3/library/ast.html#ast.Assign) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtAssign { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub targets: Vec, pub value: Box, @@ -6659,7 +9039,9 @@ pub struct StmtAssign { /// See also [AugAssign](https://docs.python.org/3/library/ast.html#ast.AugAssign) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtAugAssign { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub target: Box, pub op: crate::Operator, @@ -6668,7 +9050,9 @@ pub struct StmtAugAssign { /// See also [AnnAssign](https://docs.python.org/3/library/ast.html#ast.AnnAssign) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtAnnAssign { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub target: Box, pub annotation: Box, @@ -6681,7 +9065,9 @@ pub struct StmtAnnAssign { /// /// This type differs from the original Python AST, as it collapses the synchronous and asynchronous variants into a single type. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtFor { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub is_async: bool, pub target: Box, @@ -6693,7 +9079,9 @@ pub struct StmtFor { /// See also [While](https://docs.python.org/3/library/ast.html#ast.While) /// and [AsyncWhile](https://docs.python.org/3/library/ast.html#ast.AsyncWhile). #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtWhile { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub test: Box, pub body: Vec, @@ -6702,7 +9090,9 @@ pub struct StmtWhile { /// See also [If](https://docs.python.org/3/library/ast.html#ast.If) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtIf { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub test: Box, pub body: Vec, @@ -6714,7 +9104,9 @@ pub struct StmtIf { /// /// This type differs from the original Python AST, as it collapses the synchronous and asynchronous variants into a single type. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtWith { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub is_async: bool, pub items: Vec, @@ -6723,7 +9115,9 @@ pub struct StmtWith { /// See also [Match](https://docs.python.org/3/library/ast.html#ast.Match) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtMatch { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub subject: Box, pub cases: Vec, @@ -6731,7 +9125,9 @@ pub struct StmtMatch { /// See also [Raise](https://docs.python.org/3/library/ast.html#ast.Raise) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtRaise { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub exc: Option>, pub cause: Option>, @@ -6740,7 +9136,9 @@ pub struct StmtRaise { /// See also [Try](https://docs.python.org/3/library/ast.html#ast.Try) /// and [TryStar](https://docs.python.org/3/library/ast.html#ast.TryStar) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtTry { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub body: Vec, pub handlers: Vec, @@ -6751,7 +9149,9 @@ pub struct StmtTry { /// See also [Assert](https://docs.python.org/3/library/ast.html#ast.Assert) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtAssert { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub test: Box, pub msg: Option>, @@ -6759,14 +9159,18 @@ pub struct StmtAssert { /// See also [Import](https://docs.python.org/3/library/ast.html#ast.Import) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtImport { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub names: Vec, } /// See also [ImportFrom](https://docs.python.org/3/library/ast.html#ast.ImportFrom) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtImportFrom { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub module: Option, pub names: Vec, @@ -6775,40 +9179,52 @@ pub struct StmtImportFrom { /// See also [Global](https://docs.python.org/3/library/ast.html#ast.Global) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtGlobal { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub names: Vec, } /// See also [Nonlocal](https://docs.python.org/3/library/ast.html#ast.Nonlocal) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtNonlocal { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub names: Vec, } /// See also [Expr](https://docs.python.org/3/library/ast.html#ast.Expr) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtExpr { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Box, } /// See also [Pass](https://docs.python.org/3/library/ast.html#ast.Pass) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtPass { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, } /// See also [Break](https://docs.python.org/3/library/ast.html#ast.Break) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtBreak { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, } /// See also [Continue](https://docs.python.org/3/library/ast.html#ast.Continue) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtContinue { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, } @@ -6867,7 +9283,9 @@ pub struct StmtContinue { /// [Escape kind]: crate::IpyEscapeKind /// #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StmtIpyEscapeCommand { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub kind: crate::IpyEscapeKind, pub value: Box, @@ -6875,7 +9293,9 @@ pub struct StmtIpyEscapeCommand { /// See also [BoolOp](https://docs.python.org/3/library/ast.html#ast.BoolOp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprBoolOp { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub op: crate::BoolOp, pub values: Vec, @@ -6883,7 +9303,9 @@ pub struct ExprBoolOp { /// See also [NamedExpr](https://docs.python.org/3/library/ast.html#ast.NamedExpr) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprNamed { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub target: Box, pub value: Box, @@ -6891,7 +9313,9 @@ pub struct ExprNamed { /// See also [BinOp](https://docs.python.org/3/library/ast.html#ast.BinOp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprBinOp { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub left: Box, pub op: crate::Operator, @@ -6900,7 +9324,9 @@ pub struct ExprBinOp { /// See also [UnaryOp](https://docs.python.org/3/library/ast.html#ast.UnaryOp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprUnaryOp { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub op: crate::UnaryOp, pub operand: Box, @@ -6908,7 +9334,9 @@ pub struct ExprUnaryOp { /// See also [Lambda](https://docs.python.org/3/library/ast.html#ast.Lambda) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprLambda { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub parameters: Option>, pub body: Box, @@ -6916,7 +9344,9 @@ pub struct ExprLambda { /// See also [IfExp](https://docs.python.org/3/library/ast.html#ast.IfExp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprIf { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub test: Box, pub body: Box, @@ -6925,21 +9355,27 @@ pub struct ExprIf { /// See also [Dict](https://docs.python.org/3/library/ast.html#ast.Dict) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprDict { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub items: Vec, } /// See also [Set](https://docs.python.org/3/library/ast.html#ast.Set) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprSet { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub elts: Vec, } /// See also [ListComp](https://docs.python.org/3/library/ast.html#ast.ListComp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprListComp { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub elt: Box, pub generators: Vec, @@ -6947,7 +9383,9 @@ pub struct ExprListComp { /// See also [SetComp](https://docs.python.org/3/library/ast.html#ast.SetComp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprSetComp { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub elt: Box, pub generators: Vec, @@ -6955,7 +9393,9 @@ pub struct ExprSetComp { /// See also [DictComp](https://docs.python.org/3/library/ast.html#ast.DictComp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprDictComp { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub key: Box, pub value: Box, @@ -6964,7 +9404,9 @@ pub struct ExprDictComp { /// See also [GeneratorExp](https://docs.python.org/3/library/ast.html#ast.GeneratorExp) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprGenerator { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub elt: Box, pub generators: Vec, @@ -6973,28 +9415,36 @@ pub struct ExprGenerator { /// See also [Await](https://docs.python.org/3/library/ast.html#ast.Await) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprAwait { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Box, } /// See also [Yield](https://docs.python.org/3/library/ast.html#ast.Yield) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprYield { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Option>, } /// See also [YieldFrom](https://docs.python.org/3/library/ast.html#ast.YieldFrom) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprYieldFrom { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Box, } /// See also [Compare](https://docs.python.org/3/library/ast.html#ast.Compare) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprCompare { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub left: Box, pub ops: Box<[crate::CmpOp]>, @@ -7003,7 +9453,9 @@ pub struct ExprCompare { /// See also [Call](https://docs.python.org/3/library/ast.html#ast.Call) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprCall { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub func: Box, pub arguments: crate::Arguments, @@ -7018,15 +9470,35 @@ pub struct ExprCall { /// /// See also [JoinedStr](https://docs.python.org/3/library/ast.html#ast.JoinedStr) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprFString { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: crate::FStringValue, } +/// An AST node that represents either a single-part t-string literal +/// or an implicitly concatenated t-string literal. +/// +/// This type differs from the original Python AST `TemplateStr` in that it +/// doesn't join the implicitly concatenated parts into a single string. Instead, +/// it keeps them separate and provide various methods to access the parts. +/// +/// See also [TemplateStr](https://docs.python.org/3/library/ast.html#ast.TemplateStr) +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct ExprTString { + pub node_index: crate::AtomicNodeIndex, + pub range: ruff_text_size::TextRange, + pub value: crate::TStringValue, +} + /// An AST node that represents either a single-part string literal /// or an implicitly concatenated string literal. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprStringLiteral { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: crate::StringLiteralValue, } @@ -7034,36 +9506,48 @@ pub struct ExprStringLiteral { /// An AST node that represents either a single-part bytestring literal /// or an implicitly concatenated bytestring literal. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprBytesLiteral { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: crate::BytesLiteralValue, } #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprNumberLiteral { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: crate::Number, } #[derive(Clone, Debug, PartialEq, Default)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprBooleanLiteral { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: bool, } #[derive(Clone, Debug, PartialEq, Default)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprNoneLiteral { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, } #[derive(Clone, Debug, PartialEq, Default)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprEllipsisLiteral { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, } /// See also [Attribute](https://docs.python.org/3/library/ast.html#ast.Attribute) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprAttribute { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Box, pub attr: crate::Identifier, @@ -7072,7 +9556,9 @@ pub struct ExprAttribute { /// See also [Subscript](https://docs.python.org/3/library/ast.html#ast.Subscript) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprSubscript { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Box, pub slice: Box, @@ -7081,7 +9567,9 @@ pub struct ExprSubscript { /// See also [Starred](https://docs.python.org/3/library/ast.html#ast.Starred) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprStarred { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub value: Box, pub ctx: crate::ExprContext, @@ -7089,7 +9577,9 @@ pub struct ExprStarred { /// See also [Name](https://docs.python.org/3/library/ast.html#ast.Name) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprName { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub id: Name, pub ctx: crate::ExprContext, @@ -7097,7 +9587,9 @@ pub struct ExprName { /// See also [List](https://docs.python.org/3/library/ast.html#ast.List) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprList { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub elts: Vec, pub ctx: crate::ExprContext, @@ -7105,7 +9597,9 @@ pub struct ExprList { /// See also [Tuple](https://docs.python.org/3/library/ast.html#ast.Tuple) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprTuple { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub elts: Vec, pub ctx: crate::ExprContext, @@ -7114,7 +9608,9 @@ pub struct ExprTuple { /// See also [Slice](https://docs.python.org/3/library/ast.html#ast.Slice) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprSlice { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub lower: Option>, pub upper: Option>, @@ -7133,7 +9629,9 @@ pub struct ExprSlice { /// For more information related to terminology and syntax of escape commands, /// see [`StmtIpyEscapeCommand`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprIpyEscapeCommand { + pub node_index: crate::AtomicNodeIndex, pub range: ruff_text_size::TextRange, pub kind: crate::IpyEscapeKind, pub value: Box, @@ -7144,7 +9642,11 @@ impl ModModule { where V: SourceOrderVisitor<'a> + ?Sized, { - let ModModule { body, range: _ } = self; + let ModModule { + body, + range: _, + node_index: _, + } = self; visitor.visit_body(body); } } @@ -7154,7 +9656,11 @@ impl ModExpression { where V: SourceOrderVisitor<'a> + ?Sized, { - let ModExpression { body, range: _ } = self; + let ModExpression { + body, + range: _, + node_index: _, + } = self; visitor.visit_expr(body); } } @@ -7173,6 +9679,7 @@ impl StmtFunctionDef { returns, body, range: _, + node_index: _, } = self; for elm in decorator_list { @@ -7206,6 +9713,7 @@ impl StmtClassDef { arguments, body, range: _, + node_index: _, } = self; for elm in decorator_list { @@ -7230,7 +9738,11 @@ impl StmtReturn { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtReturn { value, range: _ } = self; + let StmtReturn { + value, + range: _, + node_index: _, + } = self; if let Some(value) = value { visitor.visit_expr(value); @@ -7243,7 +9755,11 @@ impl StmtDelete { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtDelete { targets, range: _ } = self; + let StmtDelete { + targets, + range: _, + node_index: _, + } = self; for elm in targets { visitor.visit_expr(elm); @@ -7261,6 +9777,7 @@ impl StmtTypeAlias { type_params, value, range: _, + node_index: _, } = self; visitor.visit_expr(name); @@ -7281,6 +9798,7 @@ impl StmtAssign { targets, value, range: _, + node_index: _, } = self; for elm in targets { @@ -7300,6 +9818,7 @@ impl StmtAugAssign { op, value, range: _, + node_index: _, } = self; visitor.visit_expr(target); visitor.visit_operator(op); @@ -7318,6 +9837,7 @@ impl StmtAnnAssign { value, simple: _, range: _, + node_index: _, } = self; visitor.visit_expr(target); visitor.visit_annotation(annotation); @@ -7340,6 +9860,7 @@ impl StmtFor { body, orelse, range: _, + node_index: _, } = self; visitor.visit_expr(target); visitor.visit_expr(iter); @@ -7358,6 +9879,7 @@ impl StmtWhile { body, orelse, range: _, + node_index: _, } = self; visitor.visit_expr(test); visitor.visit_body(body); @@ -7375,6 +9897,7 @@ impl StmtIf { body, elif_else_clauses, range: _, + node_index: _, } = self; visitor.visit_expr(test); visitor.visit_body(body); @@ -7395,6 +9918,7 @@ impl StmtWith { items, body, range: _, + node_index: _, } = self; for elm in items { @@ -7413,6 +9937,7 @@ impl StmtMatch { subject, cases, range: _, + node_index: _, } = self; visitor.visit_expr(subject); @@ -7431,6 +9956,7 @@ impl StmtRaise { exc, cause, range: _, + node_index: _, } = self; if let Some(exc) = exc { @@ -7455,6 +9981,7 @@ impl StmtTry { finalbody, is_star: _, range: _, + node_index: _, } = self; visitor.visit_body(body); @@ -7475,6 +10002,7 @@ impl StmtAssert { test, msg, range: _, + node_index: _, } = self; visitor.visit_expr(test); @@ -7489,7 +10017,11 @@ impl StmtImport { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtImport { names, range: _ } = self; + let StmtImport { + names, + range: _, + node_index: _, + } = self; for elm in names { visitor.visit_alias(elm); @@ -7507,6 +10039,7 @@ impl StmtImportFrom { names, level: _, range: _, + node_index: _, } = self; if let Some(module) = module { @@ -7524,7 +10057,11 @@ impl StmtGlobal { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtGlobal { names, range: _ } = self; + let StmtGlobal { + names, + range: _, + node_index: _, + } = self; for elm in names { visitor.visit_identifier(elm); @@ -7537,7 +10074,11 @@ impl StmtNonlocal { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtNonlocal { names, range: _ } = self; + let StmtNonlocal { + names, + range: _, + node_index: _, + } = self; for elm in names { visitor.visit_identifier(elm); @@ -7550,7 +10091,11 @@ impl StmtExpr { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtExpr { value, range: _ } = self; + let StmtExpr { + value, + range: _, + node_index: _, + } = self; visitor.visit_expr(value); } } @@ -7560,7 +10105,10 @@ impl StmtPass { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtPass { range: _ } = self; + let StmtPass { + range: _, + node_index: _, + } = self; } } @@ -7569,7 +10117,10 @@ impl StmtBreak { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtBreak { range: _ } = self; + let StmtBreak { + range: _, + node_index: _, + } = self; } } @@ -7578,7 +10129,10 @@ impl StmtContinue { where V: SourceOrderVisitor<'a> + ?Sized, { - let StmtContinue { range: _ } = self; + let StmtContinue { + range: _, + node_index: _, + } = self; } } @@ -7591,6 +10145,7 @@ impl StmtIpyEscapeCommand { kind: _, value: _, range: _, + node_index: _, } = self; } } @@ -7604,6 +10159,7 @@ impl ExprNamed { target, value, range: _, + node_index: _, } = self; visitor.visit_expr(target); visitor.visit_expr(value); @@ -7620,6 +10176,7 @@ impl ExprBinOp { op, right, range: _, + node_index: _, } = self; visitor.visit_expr(left); visitor.visit_operator(op); @@ -7636,6 +10193,7 @@ impl ExprUnaryOp { op, operand, range: _, + node_index: _, } = self; visitor.visit_unary_op(op); visitor.visit_expr(operand); @@ -7651,6 +10209,7 @@ impl ExprLambda { parameters, body, range: _, + node_index: _, } = self; if let Some(parameters) = parameters { @@ -7671,6 +10230,7 @@ impl ExprIf { body, orelse, range: _, + node_index: _, } = self; visitor.visit_expr(body); visitor.visit_expr(test); @@ -7683,7 +10243,11 @@ impl ExprSet { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprSet { elts, range: _ } = self; + let ExprSet { + elts, + range: _, + node_index: _, + } = self; for elm in elts { visitor.visit_expr(elm); @@ -7700,6 +10264,7 @@ impl ExprListComp { elt, generators, range: _, + node_index: _, } = self; visitor.visit_expr(elt); @@ -7718,6 +10283,7 @@ impl ExprSetComp { elt, generators, range: _, + node_index: _, } = self; visitor.visit_expr(elt); @@ -7737,6 +10303,7 @@ impl ExprDictComp { value, generators, range: _, + node_index: _, } = self; visitor.visit_expr(key); visitor.visit_expr(value); @@ -7757,6 +10324,7 @@ impl ExprGenerator { generators, parenthesized: _, range: _, + node_index: _, } = self; visitor.visit_expr(elt); @@ -7771,7 +10339,11 @@ impl ExprAwait { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprAwait { value, range: _ } = self; + let ExprAwait { + value, + range: _, + node_index: _, + } = self; visitor.visit_expr(value); } } @@ -7781,7 +10353,11 @@ impl ExprYield { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprYield { value, range: _ } = self; + let ExprYield { + value, + range: _, + node_index: _, + } = self; if let Some(value) = value { visitor.visit_expr(value); @@ -7794,7 +10370,11 @@ impl ExprYieldFrom { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprYieldFrom { value, range: _ } = self; + let ExprYieldFrom { + value, + range: _, + node_index: _, + } = self; visitor.visit_expr(value); } } @@ -7808,6 +10388,7 @@ impl ExprCall { func, arguments, range: _, + node_index: _, } = self; visitor.visit_expr(func); visitor.visit_arguments(arguments); @@ -7819,7 +10400,11 @@ impl ExprNumberLiteral { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprNumberLiteral { value: _, range: _ } = self; + let ExprNumberLiteral { + value: _, + range: _, + node_index: _, + } = self; } } @@ -7828,7 +10413,11 @@ impl ExprBooleanLiteral { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprBooleanLiteral { value: _, range: _ } = self; + let ExprBooleanLiteral { + value: _, + range: _, + node_index: _, + } = self; } } @@ -7837,7 +10426,10 @@ impl ExprNoneLiteral { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprNoneLiteral { range: _ } = self; + let ExprNoneLiteral { + range: _, + node_index: _, + } = self; } } @@ -7846,7 +10438,10 @@ impl ExprEllipsisLiteral { where V: SourceOrderVisitor<'a> + ?Sized, { - let ExprEllipsisLiteral { range: _ } = self; + let ExprEllipsisLiteral { + range: _, + node_index: _, + } = self; } } @@ -7860,6 +10455,7 @@ impl ExprAttribute { attr, ctx: _, range: _, + node_index: _, } = self; visitor.visit_expr(value); visitor.visit_identifier(attr); @@ -7876,6 +10472,7 @@ impl ExprSubscript { slice, ctx: _, range: _, + node_index: _, } = self; visitor.visit_expr(value); visitor.visit_expr(slice); @@ -7891,6 +10488,7 @@ impl ExprStarred { value, ctx: _, range: _, + node_index: _, } = self; visitor.visit_expr(value); } @@ -7905,6 +10503,7 @@ impl ExprName { id: _, ctx: _, range: _, + node_index: _, } = self; } } @@ -7918,6 +10517,7 @@ impl ExprList { elts, ctx: _, range: _, + node_index: _, } = self; for elm in elts { @@ -7936,6 +10536,7 @@ impl ExprTuple { ctx: _, parenthesized: _, range: _, + node_index: _, } = self; for elm in elts { @@ -7954,6 +10555,7 @@ impl ExprSlice { upper, step, range: _, + node_index: _, } = self; if let Some(lower) = lower { @@ -7979,6 +10581,7 @@ impl ExprIpyEscapeCommand { kind: _, value: _, range: _, + node_index: _, } = self; } } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 4d207ee547bcd..de17f50869130 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -3,7 +3,7 @@ use std::path::Path; use rustc_hash::FxHashMap; -use ruff_python_trivia::{indentation_at_offset, CommentRanges, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_trivia::{CommentRanges, SimpleTokenKind, SimpleTokenizer, indentation_at_offset}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -12,8 +12,8 @@ use crate::parenthesize::parenthesized_range; use crate::statement_visitor::StatementVisitor; use crate::visitor::Visitor; use crate::{ - self as ast, Arguments, CmpOp, DictItem, ExceptHandler, Expr, FStringElement, MatchCase, - Operator, Pattern, Stmt, TypeParam, + self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, + InterpolatedStringElement, MatchCase, Operator, Pattern, Stmt, TypeParam, }; use crate::{AnyNodeRef, ExprContext}; @@ -53,6 +53,7 @@ where func, arguments, range: _, + node_index: _, }) = expr { // Ex) `list()` @@ -138,11 +139,15 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { } Expr::FString(ast::ExprFString { value, .. }) => value .elements() - .any(|expr| any_over_f_string_element(expr, func)), + .any(|expr| any_over_interpolated_string_element(expr, func)), + Expr::TString(ast::ExprTString { value, .. }) => value + .elements() + .any(|expr| any_over_interpolated_string_element(expr, func)), Expr::Named(ast::ExprNamed { target, value, range: _, + node_index: _, }) => any_over_expr(target, func) || any_over_expr(value, func), Expr::BinOp(ast::ExprBinOp { left, right, .. }) => { any_over_expr(left, func) || any_over_expr(right, func) @@ -154,32 +159,49 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { body, orelse, range: _, + node_index: _, }) => any_over_expr(test, func) || any_over_expr(body, func) || any_over_expr(orelse, func), - Expr::Dict(ast::ExprDict { items, range: _ }) => { - items.iter().any(|ast::DictItem { key, value }| { - any_over_expr(value, func) - || key.as_ref().is_some_and(|key| any_over_expr(key, func)) - }) - } - Expr::Set(ast::ExprSet { elts, range: _ }) - | Expr::List(ast::ExprList { elts, range: _, .. }) - | Expr::Tuple(ast::ExprTuple { elts, range: _, .. }) => { - elts.iter().any(|expr| any_over_expr(expr, func)) - } + Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) => items.iter().any(|ast::DictItem { key, value }| { + any_over_expr(value, func) || key.as_ref().is_some_and(|key| any_over_expr(key, func)) + }), + Expr::Set(ast::ExprSet { + elts, + range: _, + node_index: _, + }) + | Expr::List(ast::ExprList { + elts, + range: _, + node_index: _, + .. + }) + | Expr::Tuple(ast::ExprTuple { + elts, + range: _, + node_index: _, + .. + }) => elts.iter().any(|expr| any_over_expr(expr, func)), Expr::ListComp(ast::ExprListComp { elt, generators, range: _, + node_index: _, }) | Expr::SetComp(ast::ExprSetComp { elt, generators, range: _, + node_index: _, }) | Expr::Generator(ast::ExprGenerator { elt, generators, range: _, + node_index: _, parenthesized: _, }) => { any_over_expr(elt, func) @@ -194,6 +216,7 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { value, generators, range: _, + node_index: _, }) => { any_over_expr(key, func) || any_over_expr(value, func) @@ -203,15 +226,33 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { || generator.ifs.iter().any(|expr| any_over_expr(expr, func)) }) } - Expr::Await(ast::ExprAwait { value, range: _ }) - | Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) + Expr::Await(ast::ExprAwait { + value, + range: _, + node_index: _, + }) + | Expr::YieldFrom(ast::ExprYieldFrom { + value, + range: _, + node_index: _, + }) | Expr::Attribute(ast::ExprAttribute { - value, range: _, .. + value, + range: _, + node_index: _, + .. }) | Expr::Starred(ast::ExprStarred { - value, range: _, .. + value, + range: _, + node_index: _, + .. }) => any_over_expr(value, func), - Expr::Yield(ast::ExprYield { value, range: _ }) => value + Expr::Yield(ast::ExprYield { + value, + range: _, + node_index: _, + }) => value .as_ref() .is_some_and(|value| any_over_expr(value, func)), Expr::Compare(ast::ExprCompare { @@ -221,6 +262,7 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { func: call_func, arguments, range: _, + node_index: _, }) => { any_over_expr(call_func, func) // Note that this is the evaluation order but not necessarily the declaration order @@ -238,6 +280,7 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { upper, step, range: _, + node_index: _, }) => { lower .as_ref() @@ -281,11 +324,17 @@ pub fn any_over_type_param(type_param: &TypeParam, func: &dyn Fn(&Expr) -> bool) pub fn any_over_pattern(pattern: &Pattern, func: &dyn Fn(&Expr) -> bool) -> bool { match pattern { - Pattern::MatchValue(ast::PatternMatchValue { value, range: _ }) => { - any_over_expr(value, func) - } + Pattern::MatchValue(ast::PatternMatchValue { + value, + range: _, + node_index: _, + }) => any_over_expr(value, func), Pattern::MatchSingleton(_) => false, - Pattern::MatchSequence(ast::PatternMatchSequence { patterns, range: _ }) => patterns + Pattern::MatchSequence(ast::PatternMatchSequence { + patterns, + range: _, + node_index: _, + }) => patterns .iter() .any(|pattern| any_over_pattern(pattern, func)), Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, .. }) => { @@ -309,28 +358,32 @@ pub fn any_over_pattern(pattern: &Pattern, func: &dyn Fn(&Expr) -> bool) -> bool Pattern::MatchAs(ast::PatternMatchAs { pattern, .. }) => pattern .as_ref() .is_some_and(|pattern| any_over_pattern(pattern, func)), - Pattern::MatchOr(ast::PatternMatchOr { patterns, range: _ }) => patterns + Pattern::MatchOr(ast::PatternMatchOr { + patterns, + range: _, + node_index: _, + }) => patterns .iter() .any(|pattern| any_over_pattern(pattern, func)), } } -pub fn any_over_f_string_element( - element: &ast::FStringElement, +pub fn any_over_interpolated_string_element( + element: &ast::InterpolatedStringElement, func: &dyn Fn(&Expr) -> bool, ) -> bool { match element { - ast::FStringElement::Literal(_) => false, - ast::FStringElement::Expression(ast::FStringExpressionElement { + ast::InterpolatedStringElement::Literal(_) => false, + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, format_spec, .. }) => { any_over_expr(expression, func) || format_spec.as_ref().is_some_and(|spec| { - spec.elements - .iter() - .any(|spec_element| any_over_f_string_element(spec_element, func)) + spec.elements.iter().any(|spec_element| { + any_over_interpolated_string_element(spec_element, func) + }) }) } } @@ -392,12 +445,18 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { .iter() .any(|decorator| any_over_expr(&decorator.expression, func)) } - Stmt::Return(ast::StmtReturn { value, range: _ }) => value + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => value .as_ref() .is_some_and(|value| any_over_expr(value, func)), - Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { - targets.iter().any(|expr| any_over_expr(expr, func)) - } + Stmt::Delete(ast::StmtDelete { + targets, + range: _, + node_index: _, + }) => targets.iter().any(|expr| any_over_expr(expr, func)), Stmt::TypeAlias(ast::StmtTypeAlias { name, type_params, @@ -447,12 +506,14 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { body, orelse, range: _, + node_index: _, }) => any_over_expr(test, func) || any_over_body(body, func) || any_over_body(orelse, func), Stmt::If(ast::StmtIf { test, body, elif_else_clauses, range: _, + node_index: _, }) => { any_over_expr(test, func) || any_over_body(body, func) @@ -477,6 +538,7 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { exc, cause, range: _, + node_index: _, }) => { exc.as_ref().is_some_and(|value| any_over_expr(value, func)) || cause @@ -490,6 +552,7 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { finalbody, is_star: _, range: _, + node_index: _, }) => { any_over_body(body, func) || handlers.iter().any(|handler| { @@ -508,6 +571,7 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { test, msg, range: _, + node_index: _, }) => { any_over_expr(test, func) || msg.as_ref().is_some_and(|value| any_over_expr(value, func)) @@ -516,6 +580,7 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { subject, cases, range: _, + node_index: _, }) => { any_over_expr(subject, func) || cases.iter().any(|case| { @@ -524,6 +589,7 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { guard, body, range: _, + node_index: _, } = case; any_over_pattern(pattern, func) || guard.as_ref().is_some_and(|expr| any_over_expr(expr, func)) @@ -534,7 +600,11 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool { Stmt::ImportFrom(_) => false, Stmt::Global(_) => false, Stmt::Nonlocal(_) => false, - Stmt::Expr(ast::StmtExpr { value, range: _ }) => any_over_expr(value, func), + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => any_over_expr(value, func), Stmt::Pass(_) | Stmt::Break(_) | Stmt::Continue(_) => false, Stmt::IpyEscapeCommand(_) => false, } @@ -954,6 +1024,7 @@ impl<'a> StatementVisitor<'a> for RaiseStatementVisitor<'a> { exc, cause, range: _, + node_index: _, }) => { self.raises .push((stmt.range(), exc.as_deref(), cause.as_deref())); @@ -1023,7 +1094,12 @@ impl Visitor<'_> for AwaitVisitor { /// Return `true` if a `Stmt` is a docstring. pub fn is_docstring_stmt(stmt: &Stmt) -> bool { - if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { + if let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = stmt + { value.is_string_literal_expr() } else { false @@ -1036,7 +1112,12 @@ pub fn on_conditional_branch<'a>(parents: &mut impl Iterator) - if matches!(parent, Stmt::If(_) | Stmt::While(_) | Stmt::Match(_)) { return true; } - if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = parent { + if let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = parent + { if value.is_if_expr() { return true; } @@ -1304,6 +1385,8 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool { // These literals may or may not be empty. Expr::FString(f_string) => is_non_empty_f_string(f_string), + // These literals may or may not be empty. + Expr::TString(f_string) => is_non_empty_t_string(f_string), Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(), Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(), } @@ -1313,8 +1396,78 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool { ast::FStringPart::Literal(string_literal) => !string_literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().all(|element| match element { - FStringElement::Literal(string_literal) => !string_literal.is_empty(), - FStringElement::Expression(f_string) => inner(&f_string.expression), + InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(), + InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression), + }) + } + }) +} + +/// Returns `true` if the expression definitely resolves to a non-empty string, when used as an +/// f-string expression, or `false` if the expression may resolve to an empty string. +fn is_non_empty_t_string(expr: &ast::ExprTString) -> bool { + fn inner(expr: &Expr) -> bool { + match expr { + // When stringified, these expressions are always non-empty. + Expr::Lambda(_) => true, + Expr::Dict(_) => true, + Expr::Set(_) => true, + Expr::ListComp(_) => true, + Expr::SetComp(_) => true, + Expr::DictComp(_) => true, + Expr::Compare(_) => true, + Expr::NumberLiteral(_) => true, + Expr::BooleanLiteral(_) => true, + Expr::NoneLiteral(_) => true, + Expr::EllipsisLiteral(_) => true, + Expr::List(_) => true, + Expr::Tuple(_) => true, + + // These expressions must resolve to the inner expression. + Expr::If(ast::ExprIf { body, orelse, .. }) => inner(body) && inner(orelse), + Expr::Named(ast::ExprNamed { value, .. }) => inner(value), + + // These expressions are complex. We can't determine whether they're empty or not. + Expr::BoolOp(ast::ExprBoolOp { .. }) => false, + Expr::BinOp(ast::ExprBinOp { .. }) => false, + Expr::UnaryOp(ast::ExprUnaryOp { .. }) => false, + Expr::Generator(_) => false, + Expr::Await(_) => false, + Expr::Yield(_) => false, + Expr::YieldFrom(_) => false, + Expr::Call(_) => false, + Expr::Attribute(_) => false, + Expr::Subscript(_) => false, + Expr::Starred(_) => false, + Expr::Name(_) => false, + Expr::Slice(_) => false, + Expr::IpyEscapeCommand(_) => false, + + // These literals may or may not be empty. + Expr::FString(f_string) => is_non_empty_f_string(f_string), + // These literals may or may not be empty. + Expr::TString(t_string) => is_non_empty_t_string(t_string), + Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(), + Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(), + } + } + + expr.value.iter().any(|part| match part { + ast::TStringPart::Literal(string_literal) => !string_literal.is_empty(), + ast::TStringPart::TString(t_string) => { + t_string.elements.iter().all(|element| match element { + ast::InterpolatedStringElement::Literal(string_literal) => { + !string_literal.is_empty() + } + ast::InterpolatedStringElement::Interpolation(t_string) => { + inner(&t_string.expression) + } + }) + } + ast::TStringPart::FString(f_string) => { + f_string.elements.iter().all(|element| match element { + InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(), + InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression), }) } }) @@ -1331,10 +1484,10 @@ fn is_empty_f_string(expr: &ast::ExprFString) -> bool { value .elements() .all(|f_string_element| match f_string_element { - FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => { - value.is_empty() - } - FStringElement::Expression(ast::FStringExpressionElement { + InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) => value.is_empty(), + InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, .. }) => inner(expression), @@ -1348,8 +1501,8 @@ fn is_empty_f_string(expr: &ast::ExprFString) -> bool { ast::FStringPart::Literal(string_literal) => string_literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().all(|element| match element { - FStringElement::Literal(string_literal) => string_literal.is_empty(), - FStringElement::Expression(f_string) => inner(&f_string.expression), + InterpolatedStringElement::Literal(string_literal) => string_literal.is_empty(), + InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression), }) } }) @@ -1405,6 +1558,7 @@ pub fn pep_604_optional(expr: &Expr) -> Expr { op: Operator::BitOr, right: Box::new(Expr::NoneLiteral(ast::ExprNoneLiteral::default())), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), } .into() } @@ -1416,6 +1570,7 @@ pub fn pep_604_union(elts: &[Expr]) -> Expr { elts: vec![], ctx: ExprContext::Load, range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), parenthesized: true, }), [Expr::Tuple(ast::ExprTuple { elts, .. })] => pep_604_union(elts), @@ -1425,6 +1580,7 @@ pub fn pep_604_union(elts: &[Expr]) -> Expr { op: Operator::BitOr, right: Box::new(pep_604_union(&[elt.clone()])), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }), } } @@ -1435,11 +1591,13 @@ pub fn typing_optional(elt: Expr, binding: Name) -> Expr { value: Box::new(Expr::Name(ast::ExprName { id: binding, range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), ctx: ExprContext::Load, })), slice: Box::new(elt), ctx: ExprContext::Load, range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }) } @@ -1452,16 +1610,19 @@ pub fn typing_union(elts: &[Expr], binding: Name) -> Expr { value: Box::new(Expr::Name(ast::ExprName { id: binding, range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), ctx: ExprContext::Load, })), slice: Box::new(Expr::Tuple(ast::ExprTuple { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), elts: elts.to_vec(), ctx: ExprContext::Load, parenthesized: false, })), ctx: ExprContext::Load, range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }) } @@ -1549,9 +1710,9 @@ mod tests { use crate::helpers::{any_over_stmt, any_over_type_param, resolve_imported_module_path}; use crate::{ - Expr, ExprContext, ExprName, ExprNumberLiteral, Identifier, Int, Number, Stmt, - StmtTypeAlias, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, - TypeParams, + AtomicNodeIndex, Expr, ExprContext, ExprName, ExprNumberLiteral, Identifier, Int, Number, + Stmt, StmtTypeAlias, TypeParam, TypeParamParamSpec, TypeParamTypeVar, + TypeParamTypeVarTuple, TypeParams, }; #[test] @@ -1594,28 +1755,34 @@ mod tests { let name = Expr::Name(ExprName { id: "x".into(), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), ctx: ExprContext::Load, }); let constant_one = Expr::NumberLiteral(ExprNumberLiteral { value: Number::Int(Int::from(1u8)), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); let constant_two = Expr::NumberLiteral(ExprNumberLiteral { value: Number::Int(Int::from(2u8)), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); let constant_three = Expr::NumberLiteral(ExprNumberLiteral { value: Number::Int(Int::from(3u8)), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); let type_var_one = TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), bound: Some(Box::new(constant_one.clone())), default: None, name: Identifier::new("x", TextRange::default()), }); let type_var_two = TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), bound: None, default: Some(Box::new(constant_two.clone())), name: Identifier::new("x", TextRange::default()), @@ -1625,9 +1792,11 @@ mod tests { type_params: Some(Box::new(TypeParams { type_params: vec![type_var_one, type_var_two], range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), })), value: Box::new(constant_three.clone()), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); assert!(!any_over_stmt(&type_alias, &|expr| { seen.borrow_mut().push(expr.clone()); @@ -1643,6 +1812,7 @@ mod tests { fn any_over_type_param_type_var() { let type_var_no_bound = TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), bound: None, default: None, name: Identifier::new("x", TextRange::default()), @@ -1652,10 +1822,12 @@ mod tests { let constant = Expr::NumberLiteral(ExprNumberLiteral { value: Number::Int(Int::ONE), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); let type_var_with_bound = TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), bound: Some(Box::new(constant.clone())), default: None, name: Identifier::new("x", TextRange::default()), @@ -1673,6 +1845,7 @@ mod tests { let type_var_with_default = TypeParam::TypeVar(TypeParamTypeVar { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), default: Some(Box::new(constant.clone())), bound: None, name: Identifier::new("x", TextRange::default()), @@ -1693,6 +1866,7 @@ mod tests { fn any_over_type_param_type_var_tuple() { let type_var_tuple = TypeParam::TypeVarTuple(TypeParamTypeVarTuple { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), name: Identifier::new("x", TextRange::default()), default: None, }); @@ -1704,10 +1878,12 @@ mod tests { let constant = Expr::NumberLiteral(ExprNumberLiteral { value: Number::Int(Int::ONE), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); let type_var_tuple_with_default = TypeParam::TypeVarTuple(TypeParamTypeVarTuple { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), default: Some(Box::new(constant.clone())), name: Identifier::new("x", TextRange::default()), }); @@ -1727,6 +1903,7 @@ mod tests { fn any_over_type_param_param_spec() { let type_param_spec = TypeParam::ParamSpec(TypeParamParamSpec { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), name: Identifier::new("x", TextRange::default()), default: None, }); @@ -1738,10 +1915,12 @@ mod tests { let constant = Expr::NumberLiteral(ExprNumberLiteral { value: Number::Int(Int::ONE), range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }); let param_spec_with_default = TypeParam::TypeVarTuple(TypeParamTypeVarTuple { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), default: Some(Box::new(constant.clone())), name: Identifier::new("x", TextRange::default()), }); diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index 8a4478c1f274a..c8a54cbadf264 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -13,7 +13,7 @@ use crate::{self as ast, Alias, ExceptHandler, Parameter, ParameterWithDefault, Stmt}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use ruff_python_trivia::{is_python_whitespace, Cursor}; +use ruff_python_trivia::{Cursor, is_python_whitespace}; pub trait Identifier { /// Return the [`TextRange`] of the identifier in the given AST node. diff --git a/crates/ruff_python_ast/src/int.rs b/crates/ruff_python_ast/src/int.rs index 4d918f5574354..eacfd8b54a6b1 100644 --- a/crates/ruff_python_ast/src/int.rs +++ b/crates/ruff_python_ast/src/int.rs @@ -3,6 +3,7 @@ use std::str::FromStr; /// A Python integer literal. Represents both small (fits in an `i64`) and large integers. #[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Int(Number); impl FromStr for Int { @@ -216,6 +217,7 @@ impl From for Int { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] enum Number { /// A "small" number that can be represented as an `u64`. Small(u64), diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 7983ccee82190..eddcb953cec14 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -4,6 +4,7 @@ use std::path::Path; pub use expression::*; pub use generated::*; pub use int::*; +pub use node_index::*; pub use nodes::*; pub use operator_precedence::*; pub use python_version::*; @@ -17,6 +18,7 @@ pub mod identifier; mod int; pub mod name; mod node; +mod node_index; mod nodes; pub mod operator_precedence; pub mod parenthesize; diff --git a/crates/ruff_python_ast/src/name.rs b/crates/ruff_python_ast/src/name.rs index 6765f2aacfaf6..598257529bdd4 100644 --- a/crates/ruff_python_ast/src/name.rs +++ b/crates/ruff_python_ast/src/name.rs @@ -3,13 +3,14 @@ use std::fmt::{Debug, Display, Formatter, Write}; use std::hash::{Hash, Hasher}; use std::ops::Deref; -use crate::generated::ExprName; use crate::Expr; +use crate::generated::ExprName; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))] #[cfg_attr(feature = "salsa", derive(salsa::Update))] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Name(compact_str::CompactString); impl Name { @@ -111,6 +112,13 @@ impl From for compact_str::CompactString { } } +impl From for String { + #[inline] + fn from(name: Name) -> Self { + name.as_str().into() + } +} + impl FromIterator for Name { fn from_iter>(iter: I) -> Self { Self(iter.into_iter().collect()) @@ -192,14 +200,14 @@ impl schemars::JsonSchema for Name { String::schema_id() } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - String::json_schema(gen) + fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(generator) } fn _schemars_private_non_optional_json_schema( - gen: &mut schemars::gen::SchemaGenerator, + generator: &mut schemars::r#gen::SchemaGenerator, ) -> schemars::schema::Schema { - String::_schemars_private_non_optional_json_schema(gen) + String::_schemars_private_non_optional_json_schema(generator) } fn _schemars_private_is_option() -> bool { @@ -396,7 +404,7 @@ impl<'a> UnqualifiedName<'a> { Expr::Attribute(attr2) => attr2, // Ex) `foo.bar` Expr::Name(ExprName { id, .. }) => { - return Some(Self::from_slice(&[id.as_str(), attr1.attr.as_str()])) + return Some(Self::from_slice(&[id.as_str(), attr1.attr.as_str()])); } _ => return None, }; diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 1880a3d615049..b9b637b2b4b4d 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -13,6 +13,7 @@ impl ast::ElifElseClause { { let ast::ElifElseClause { range: _, + node_index: _, test, body, } = self; @@ -28,7 +29,11 @@ impl ast::ExprDict { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::ExprDict { items, range: _ } = self; + let ast::ExprDict { + items, + range: _, + node_index: _, + } = self; for ast::DictItem { key, value } in items { if let Some(key) = key { @@ -48,6 +53,7 @@ impl ast::ExprBoolOp { op, values, range: _, + node_index: _, } = self; match values.as_slice() { [left, rest @ ..] => { @@ -74,6 +80,7 @@ impl ast::ExprCompare { ops, comparators, range: _, + node_index: _, } = self; visitor.visit_expr(left); @@ -85,23 +92,23 @@ impl ast::ExprCompare { } } -impl ast::FStringFormatSpec { +impl ast::InterpolatedStringFormatSpec { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { for element in &self.elements { - visitor.visit_f_string_element(element); + visitor.visit_interpolated_string_element(element); } } } -impl ast::FStringExpressionElement { +impl ast::InterpolatedElement { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::FStringExpressionElement { + let ast::InterpolatedElement { expression, format_spec, .. @@ -110,18 +117,22 @@ impl ast::FStringExpressionElement { if let Some(format_spec) = format_spec { for spec_part in &format_spec.elements { - visitor.visit_f_string_element(spec_part); + visitor.visit_interpolated_string_element(spec_part); } } } } -impl ast::FStringLiteralElement { +impl ast::InterpolatedStringLiteralElement { pub(crate) fn visit_source_order<'a, V>(&'a self, _visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::FStringLiteralElement { range: _, value: _ } = self; + let ast::InterpolatedStringLiteralElement { + range: _, + node_index: _, + value: _, + } = self; } } @@ -130,7 +141,11 @@ impl ast::ExprFString { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::ExprFString { value, range: _ } = self; + let ast::ExprFString { + value, + range: _, + node_index: _, + } = self; for f_string_part in value { match f_string_part { @@ -145,12 +160,43 @@ impl ast::ExprFString { } } +impl ast::ExprTString { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let ast::ExprTString { + value, + range: _, + node_index: _, + } = self; + + for t_string_part in value { + match t_string_part { + ast::TStringPart::Literal(string_literal) => { + visitor.visit_string_literal(string_literal); + } + ast::TStringPart::FString(f_string) => { + visitor.visit_f_string(f_string); + } + ast::TStringPart::TString(t_string) => { + visitor.visit_t_string(t_string); + } + } + } + } +} + impl ast::ExprStringLiteral { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::ExprStringLiteral { value, range: _ } = self; + let ast::ExprStringLiteral { + value, + range: _, + node_index: _, + } = self; for string_literal in value { visitor.visit_string_literal(string_literal); @@ -163,7 +209,11 @@ impl ast::ExprBytesLiteral { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::ExprBytesLiteral { value, range: _ } = self; + let ast::ExprBytesLiteral { + value, + range: _, + node_index: _, + } = self; for bytes_literal in value { visitor.visit_bytes_literal(bytes_literal); @@ -178,6 +228,7 @@ impl ast::ExceptHandlerExceptHandler { { let ast::ExceptHandlerExceptHandler { range: _, + node_index: _, type_, name, body, @@ -199,7 +250,11 @@ impl ast::PatternMatchValue { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::PatternMatchValue { value, range: _ } = self; + let ast::PatternMatchValue { + value, + range: _, + node_index: _, + } = self; visitor.visit_expr(value); } } @@ -209,7 +264,11 @@ impl ast::PatternMatchSingleton { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::PatternMatchSingleton { value, range: _ } = self; + let ast::PatternMatchSingleton { + value, + range: _, + node_index: _, + } = self; visitor.visit_singleton(value); } } @@ -219,7 +278,11 @@ impl ast::PatternMatchSequence { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::PatternMatchSequence { patterns, range: _ } = self; + let ast::PatternMatchSequence { + patterns, + range: _, + node_index: _, + } = self; for pattern in patterns { visitor.visit_pattern(pattern); } @@ -236,6 +299,7 @@ impl ast::PatternMatchMapping { patterns, rest, range: _, + node_index: _, } = self; let mut rest = rest.as_ref(); @@ -266,6 +330,7 @@ impl ast::PatternMatchClass { cls, arguments: parameters, range: _, + node_index: _, } = self; visitor.visit_expr(cls); visitor.visit_pattern_arguments(parameters); @@ -277,7 +342,11 @@ impl ast::PatternMatchStar { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::PatternMatchStar { range: _, name } = self; + let ast::PatternMatchStar { + range: _, + node_index: _, + name, + } = self; if let Some(name) = name { visitor.visit_identifier(name); @@ -293,6 +362,7 @@ impl ast::PatternMatchAs { let ast::PatternMatchAs { pattern, range: _, + node_index: _, name, } = self; if let Some(pattern) = pattern { @@ -310,7 +380,11 @@ impl ast::PatternMatchOr { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::PatternMatchOr { patterns, range: _ } = self; + let ast::PatternMatchOr { + patterns, + range: _, + node_index: _, + } = self; for pattern in patterns { visitor.visit_pattern(pattern); } @@ -324,6 +398,7 @@ impl ast::PatternArguments { { let PatternArguments { range: _, + node_index: _, patterns, keywords, } = self; @@ -345,6 +420,7 @@ impl ast::PatternKeyword { { let PatternKeyword { range: _, + node_index: _, attr, pattern, } = self; @@ -361,6 +437,7 @@ impl ast::Comprehension { { let ast::Comprehension { range: _, + node_index: _, target, iter, ifs, @@ -412,6 +489,7 @@ impl ast::Parameter { { let ast::Parameter { range: _, + node_index: _, name, annotation, } = self; @@ -430,6 +508,7 @@ impl ast::ParameterWithDefault { { let ast::ParameterWithDefault { range: _, + node_index: _, parameter, default, } = self; @@ -447,6 +526,7 @@ impl ast::Keyword { { let ast::Keyword { range: _, + node_index: _, arg, value, } = self; @@ -465,6 +545,7 @@ impl Alias { { let ast::Alias { range: _, + node_index: _, name, asname, } = self; @@ -483,6 +564,7 @@ impl ast::WithItem { { let ast::WithItem { range: _, + node_index: _, context_expr, optional_vars, } = self; @@ -502,6 +584,7 @@ impl ast::MatchCase { { let ast::MatchCase { range: _, + node_index: _, pattern, guard, body, @@ -522,6 +605,7 @@ impl ast::Decorator { { let ast::Decorator { range: _, + node_index: _, expression, } = self; @@ -536,6 +620,7 @@ impl ast::TypeParams { { let ast::TypeParams { range: _, + node_index: _, type_params, } = self; @@ -555,6 +640,7 @@ impl ast::TypeParamTypeVar { default, name, range: _, + node_index: _, } = self; visitor.visit_identifier(name); @@ -575,6 +661,7 @@ impl ast::TypeParamTypeVarTuple { { let ast::TypeParamTypeVarTuple { range: _, + node_index: _, name, default, } = self; @@ -593,6 +680,7 @@ impl ast::TypeParamParamSpec { { let ast::TypeParamParamSpec { range: _, + node_index: _, name, default, } = self; @@ -611,11 +699,30 @@ impl ast::FString { let ast::FString { elements, range: _, + node_index: _, flags: _, } = self; for fstring_element in elements { - visitor.visit_f_string_element(fstring_element); + visitor.visit_interpolated_string_element(fstring_element); + } + } +} + +impl ast::TString { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let ast::TString { + elements, + range: _, + node_index: _, + flags: _, + } = self; + + for tstring_element in elements { + visitor.visit_interpolated_string_element(tstring_element); } } } @@ -628,6 +735,7 @@ impl ast::StringLiteral { { let ast::StringLiteral { range: _, + node_index: _, value: _, flags: _, } = self; @@ -642,6 +750,7 @@ impl ast::BytesLiteral { { let ast::BytesLiteral { range: _, + node_index: _, value: _, flags: _, } = self; @@ -654,7 +763,11 @@ impl ast::Identifier { where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::Identifier { range: _, id: _ } = self; + let ast::Identifier { + range: _, + node_index: _, + id: _, + } = self; } } @@ -676,60 +789,57 @@ impl<'a> AnyNodeRef<'a> { /// The last child of the last branch, if the node has multiple branches. pub fn last_child_in_body(&self) -> Option> { - let body = match self { - AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. }) - | AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) - | AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) - | AnyNodeRef::MatchCase(MatchCase { body, .. }) - | AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler { - body, - .. - }) - | AnyNodeRef::ElifElseClause(ast::ElifElseClause { body, .. }) => body, - AnyNodeRef::StmtIf(ast::StmtIf { - body, - elif_else_clauses, - .. - }) => elif_else_clauses.last().map_or(body, |clause| &clause.body), - - AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. }) - | AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => { - if orelse.is_empty() { - body - } else { - orelse + let body = + match self { + AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. }) + | AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) + | AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) + | AnyNodeRef::MatchCase(MatchCase { body, .. }) + | AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler { + body, + .. + }) + | AnyNodeRef::ElifElseClause(ast::ElifElseClause { body, .. }) => body, + AnyNodeRef::StmtIf(ast::StmtIf { + body, + elif_else_clauses, + .. + }) => elif_else_clauses.last().map_or(body, |clause| &clause.body), + + AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => { + if orelse.is_empty() { body } else { orelse } } - } - AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => { - return cases.last().map(AnyNodeRef::from); - } + AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => { + return cases.last().map(AnyNodeRef::from); + } - AnyNodeRef::StmtTry(ast::StmtTry { - body, - handlers, - orelse, - finalbody, - .. - }) => { - if finalbody.is_empty() { - if orelse.is_empty() { - if handlers.is_empty() { - body + AnyNodeRef::StmtTry(ast::StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) => { + if finalbody.is_empty() { + if orelse.is_empty() { + if handlers.is_empty() { + body + } else { + return handlers.last().map(AnyNodeRef::from); + } } else { - return handlers.last().map(AnyNodeRef::from); + orelse } } else { - orelse + finalbody } - } else { - finalbody } - } - // Not a node that contains an indented child node. - _ => return None, - }; + // Not a node that contains an indented child node. + _ => return None, + }; body.last().map(AnyNodeRef::from) } diff --git a/crates/ruff_python_ast/src/node_index.rs b/crates/ruff_python_ast/src/node_index.rs new file mode 100644 index 0000000000000..094a5460d74a9 --- /dev/null +++ b/crates/ruff_python_ast/src/node_index.rs @@ -0,0 +1,100 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + +/// An AST node that has an index. +pub trait HasNodeIndex { + /// Returns the [`AtomicNodeIndex`] for this node. + fn node_index(&self) -> &AtomicNodeIndex; +} + +impl HasNodeIndex for &T +where + T: HasNodeIndex, +{ + fn node_index(&self) -> &AtomicNodeIndex { + T::node_index(*self) + } +} + +/// A unique index for a node within an AST. +/// +/// This type is interiorly mutable to allow assigning node indices +/// on-demand after parsing. +#[derive(Default)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct AtomicNodeIndex(AtomicU32); + +impl AtomicNodeIndex { + /// Returns a placeholder `AtomicNodeIndex`. + pub const fn dummy() -> AtomicNodeIndex { + AtomicNodeIndex(AtomicU32::new(u32::MAX)) + } + + /// Load the current value of the `AtomicNodeIndex`. + pub fn load(&self) -> NodeIndex { + NodeIndex(self.0.load(Ordering::Relaxed)) + } + + /// Set the value of the `AtomicNodeIndex`. + pub fn set(&self, value: u32) { + self.0.store(value, Ordering::Relaxed); + } +} + +/// A unique index for a node within an AST. +#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Clone, Copy, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct NodeIndex(u32); + +impl NodeIndex { + pub fn as_usize(self) -> usize { + self.0 as _ + } +} + +impl From for AtomicNodeIndex { + fn from(value: u32) -> Self { + AtomicNodeIndex(AtomicU32::from(value)) + } +} + +impl std::fmt::Debug for AtomicNodeIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if *self == AtomicNodeIndex::dummy() { + f.debug_tuple("AtomicNodeIndex").finish_non_exhaustive() + } else { + f.debug_tuple("AtomicNodeIndex").field(&self.0).finish() + } + } +} + +impl std::hash::Hash for AtomicNodeIndex { + fn hash(&self, state: &mut H) { + self.load().hash(state); + } +} + +impl PartialOrd for AtomicNodeIndex { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AtomicNodeIndex { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.load().cmp(&other.load()) + } +} + +impl Eq for AtomicNodeIndex {} + +impl PartialEq for AtomicNodeIndex { + fn eq(&self, other: &Self) -> bool { + self.load() == other.load() + } +} + +impl Clone for AtomicNodeIndex { + fn clone(&self) -> Self { + Self(AtomicU32::from(self.0.load(Ordering::Relaxed))) + } +} diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index be28519a0f6fe..fc9c3f885131c 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -1,8 +1,9 @@ #![allow(clippy::derive_partial_eq_without_eq)] +use crate::AtomicNodeIndex; use crate::generated::{ ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprSet, ExprStringLiteral, - ExprTuple, StmtClassDef, + ExprTString, ExprTuple, StmtClassDef, }; use std::borrow::Cow; use std::fmt; @@ -17,13 +18,14 @@ use itertools::Itertools; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix}; +use crate::str_prefix::{ + AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix, +}; use crate::{ - int, + Expr, ExprRef, InterpolatedStringElement, LiteralExpressionRef, OperatorPrecedence, Pattern, + Stmt, TypeParam, int, name::Name, str::{Quote, TripleQuotes}, - Expr, ExprRef, FStringElement, LiteralExpressionRef, OperatorPrecedence, Pattern, Stmt, - TypeParam, }; impl StmtClassDef { @@ -45,8 +47,10 @@ impl StmtClassDef { } #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ElifElseClause { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub test: Option, pub body: Vec, } @@ -130,6 +134,7 @@ impl ExprRef<'_> { /// /// [1]: https://docs.python.org/3/reference/expressions.html#displays-for-lists-sets-and-dictionaries #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct DictItem { pub key: Option, pub value: Expr, @@ -313,35 +318,41 @@ impl<'a> IntoIterator for &'a ExprSet { } #[derive(Clone, Debug, PartialEq)] -pub struct FStringFormatSpec { +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct InterpolatedStringFormatSpec { pub range: TextRange, - pub elements: FStringElements, + pub node_index: AtomicNodeIndex, + pub elements: InterpolatedStringElements, } /// See also [FormattedValue](https://docs.python.org/3/library/ast.html#ast.FormattedValue) #[derive(Clone, Debug, PartialEq)] -pub struct FStringExpressionElement { +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct InterpolatedElement { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub expression: Box, pub debug_text: Option, pub conversion: ConversionFlag, - pub format_spec: Option>, + pub format_spec: Option>, } /// An `FStringLiteralElement` with an empty `value` is an invalid f-string element. #[derive(Clone, Debug, PartialEq)] -pub struct FStringLiteralElement { +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct InterpolatedStringLiteralElement { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub value: Box, } -impl FStringLiteralElement { +impl InterpolatedStringLiteralElement { pub fn is_valid(&self) -> bool { !self.value.is_empty() } } -impl Deref for FStringLiteralElement { +impl Deref for InterpolatedStringLiteralElement { type Target = str; fn deref(&self) -> &Self::Target { @@ -351,8 +362,9 @@ impl Deref for FStringLiteralElement { /// Transforms a value prior to formatting it. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] #[repr(i8)] -#[allow(clippy::cast_possible_wrap)] +#[expect(clippy::cast_possible_wrap)] pub enum ConversionFlag { /// No conversion None = -1, // CPython uses -1 @@ -377,6 +389,7 @@ impl ConversionFlag { } #[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct DebugText { /// The text between the `{` and the expression node. pub leading: String, @@ -397,6 +410,7 @@ impl ExprFString { /// The value representing an [`ExprFString`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct FStringValue { inner: FStringValueInner, } @@ -484,7 +498,7 @@ impl FStringValue { self.iter().filter_map(|part| part.as_f_string()) } - /// Returns an iterator over all the [`FStringElement`] contained in this value. + /// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value. /// /// An f-string element is what makes up an [`FString`] i.e., it is either a /// string literal or an expression. In the following example, @@ -495,9 +509,23 @@ impl FStringValue { /// /// The f-string elements returned would be string literal (`"bar "`), /// expression (`x`) and string literal (`"qux"`). - pub fn elements(&self) -> impl Iterator { + pub fn elements(&self) -> impl Iterator { self.f_strings().flat_map(|fstring| fstring.elements.iter()) } + + /// Returns `true` if the node represents an empty f-string literal. + /// + /// Noteh that a [`FStringValue`] node will always have >= 1 [`FStringPart`]s inside it. + /// This method checks whether the value of the concatenated parts is equal to the empty + /// f-string, not whether the f-string has 0 parts inside it. + pub fn is_empty_literal(&self) -> bool { + match &self.inner { + FStringValueInner::Single(fstring_part) => fstring_part.is_empty_literal(), + FStringValueInner::Concatenated(fstring_parts) => { + fstring_parts.iter().all(FStringPart::is_empty_literal) + } + } + } } impl<'a> IntoIterator for &'a FStringValue { @@ -519,6 +547,7 @@ impl<'a> IntoIterator for &'a mut FStringValue { /// An internal representation of [`FStringValue`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] enum FStringValueInner { /// A single f-string i.e., `f"foo"`. /// @@ -532,6 +561,7 @@ enum FStringValueInner { /// An f-string part which is either a string literal or an f-string. #[derive(Clone, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum FStringPart { Literal(StringLiteral), FString(FString), @@ -544,6 +574,13 @@ impl FStringPart { Self::FString(f_string) => f_string.flags.quote_style(), } } + + pub fn is_empty_literal(&self) -> bool { + match &self { + FStringPart::Literal(string_literal) => string_literal.value.is_empty(), + FStringPart::FString(f_string) => f_string.elements.is_empty(), + } + } } impl Ranged for FStringPart { @@ -555,6 +592,184 @@ impl Ranged for FStringPart { } } +impl ExprTString { + /// Returns the single [`TString`] if the t-string isn't implicitly concatenated, [`None`] + /// otherwise. + pub const fn as_single_part_tstring(&self) -> Option<&TString> { + match &self.value.inner { + TStringValueInner::Single(TStringPart::TString(tstring)) => Some(tstring), + _ => None, + } + } +} + +/// The value representing an [`ExprTString`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct TStringValue { + inner: TStringValueInner, +} + +impl TStringValue { + /// Creates a new t-string literal with a single [`TString`] part. + pub fn single(value: TString) -> Self { + Self { + inner: TStringValueInner::Single(TStringPart::TString(value)), + } + } + + /// Creates a new t-string with the given values that represents an implicitly + /// concatenated t-string. + /// + /// # Panics + /// + /// Panics if `values` has less than 2 elements. + /// Use [`TStringValue::single`] instead. + pub fn concatenated(values: Vec) -> Self { + assert!( + values.len() > 1, + "Use `TStringValue::single` to create single-part t-strings" + ); + Self { + inner: TStringValueInner::Concatenated(values), + } + } + + /// Returns `true` if the t-string is implicitly concatenated, `false` otherwise. + pub fn is_implicit_concatenated(&self) -> bool { + matches!(self.inner, TStringValueInner::Concatenated(_)) + } + + /// Returns a slice of all the [`TStringPart`]s contained in this value. + pub fn as_slice(&self) -> &[TStringPart] { + match &self.inner { + TStringValueInner::Single(part) => std::slice::from_ref(part), + TStringValueInner::Concatenated(parts) => parts, + } + } + + /// Returns a mutable slice of all the [`TStringPart`]s contained in this value. + fn as_mut_slice(&mut self) -> &mut [TStringPart] { + match &mut self.inner { + TStringValueInner::Single(part) => std::slice::from_mut(part), + TStringValueInner::Concatenated(parts) => parts, + } + } + + /// Returns an iterator over all the [`TStringPart`]s contained in this value. + pub fn iter(&self) -> Iter { + self.as_slice().iter() + } + + /// Returns an iterator over all the [`TStringPart`]s contained in this value + /// that allows modification. + pub fn iter_mut(&mut self) -> IterMut { + self.as_mut_slice().iter_mut() + } + + /// Returns an iterator over the [`StringLiteral`] parts contained in this value. + /// + /// Note that this doesn't recurse into the t-string parts. For example, + /// + /// ```python + /// "foo" t"bar {x}" "baz" t"qux" + /// ``` + /// + /// Here, the string literal parts returned would be `"foo"` and `"baz"`. + pub fn literals(&self) -> impl Iterator { + self.iter().filter_map(|part| part.as_literal()) + } + + /// Returns an iterator over the [`TString`] parts contained in this value. + /// + /// Note that this doesn't recurse into the t-string parts. For example, + /// + /// ```python + /// "foo" t"bar {x}" "baz" t"qux" + /// ``` + /// + /// Here, the t-string parts returned would be `f"bar {x}"` and `f"qux"`. + pub fn t_strings(&self) -> impl Iterator { + self.iter().filter_map(|part| part.as_t_string()) + } + + /// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value. + /// + /// An t-string element is what makes up an [`TString`] i.e., it is either a + /// string literal or an interpolation. In the following example, + /// + /// ```python + /// "foo" t"bar {x}" "baz" t"qux" + /// ``` + /// + /// The t-string elements returned would be string literal (`"bar "`), + /// interpolation (`x`) and string literal (`"qux"`). + pub fn elements(&self) -> impl Iterator { + self.t_strings().flat_map(|fstring| fstring.elements.iter()) + } +} + +impl<'a> IntoIterator for &'a TStringValue { + type Item = &'a TStringPart; + type IntoIter = Iter<'a, TStringPart>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut TStringValue { + type Item = &'a mut TStringPart; + type IntoIter = IterMut<'a, TStringPart>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +/// An internal representation of [`TStringValue`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +enum TStringValueInner { + /// A single t-string i.e., `t"foo"`. + /// + /// This is always going to be `TStringPart::TString` variant which is + /// maintained by the `TStringValue::single` constructor. + Single(TStringPart), + + /// An implicitly concatenated t-string i.e., `"foo" t"bar {x}"`. + Concatenated(Vec), +} + +/// An t-string part which is either a string literal, an f-string, +/// or a t-string. +#[derive(Clone, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub enum TStringPart { + Literal(StringLiteral), + FString(FString), + TString(TString), +} + +impl TStringPart { + pub fn quote_style(&self) -> Quote { + match self { + Self::Literal(string_literal) => string_literal.flags.quote_style(), + Self::FString(f_string) => f_string.flags.quote_style(), + Self::TString(t_string) => t_string.flags.quote_style(), + } + } +} + +impl Ranged for TStringPart { + fn range(&self) -> TextRange { + match self { + TStringPart::Literal(string_literal) => string_literal.range(), + TStringPart::FString(f_string) => f_string.range(), + TStringPart::TString(t_string) => t_string.range(), + } + } +} + pub trait StringFlags: Copy { /// Does the string use single or double quotes in its opener and closer? fn quote_style(self) -> Quote; @@ -636,7 +851,7 @@ impl std::fmt::Display for DisplayFlags<'_> { bitflags! { #[derive(Default, Copy, Clone, PartialEq, Eq, Hash)] - struct FStringFlagsInner: u8 { + struct InterpolatedStringFlagsInner: u8 { /// The f-string uses double quotes (`"`) for its opener and closer. /// If this flag is not set, the f-string uses single quotes (`'`) /// for its opener and closer. @@ -660,9 +875,17 @@ bitflags! { } } +#[cfg(feature = "get-size")] +impl get_size2::GetSize for InterpolatedStringFlagsInner {} + /// Flags that can be queried to obtain information /// regarding the prefixes and quotes used for an f-string. /// +/// Note: This is identical to [`TStringFlags`] except that +/// the implementation of the `prefix` method of the +/// [`StringFlags`] trait returns a variant of +/// `AnyStringPrefix::Format`. +/// /// ## Notes on usage /// /// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix @@ -672,7 +895,8 @@ bitflags! { /// will properly handle nested f-strings. For usage that doesn't fit into one of these categories, /// the public constructor [`FStringFlags::empty`] can be used. #[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct FStringFlags(FStringFlagsInner); +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct FStringFlags(InterpolatedStringFlagsInner); impl FStringFlags { /// Construct a new [`FStringFlags`] with **no flags set**. @@ -685,42 +909,60 @@ impl FStringFlags { /// situations in which alternative ways to construct this struct should be used, especially /// when writing lint rules. pub fn empty() -> Self { - Self(FStringFlagsInner::empty()) + Self(InterpolatedStringFlagsInner::empty()) } #[must_use] pub fn with_quote_style(mut self, quote_style: Quote) -> Self { - self.0 - .set(FStringFlagsInner::DOUBLE, quote_style.is_double()); + self.0.set( + InterpolatedStringFlagsInner::DOUBLE, + quote_style.is_double(), + ); self } #[must_use] pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self { - self.0 - .set(FStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes()); + self.0.set( + InterpolatedStringFlagsInner::TRIPLE_QUOTED, + triple_quotes.is_yes(), + ); self } #[must_use] pub fn with_prefix(mut self, prefix: FStringPrefix) -> Self { match prefix { - FStringPrefix::Regular => { - Self(self.0 - FStringFlagsInner::R_PREFIX_LOWER - FStringFlagsInner::R_PREFIX_UPPER) - } + FStringPrefix::Regular => Self( + self.0 + - InterpolatedStringFlagsInner::R_PREFIX_LOWER + - InterpolatedStringFlagsInner::R_PREFIX_UPPER, + ), FStringPrefix::Raw { uppercase_r } => { - self.0.set(FStringFlagsInner::R_PREFIX_UPPER, uppercase_r); - self.0.set(FStringFlagsInner::R_PREFIX_LOWER, !uppercase_r); + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_UPPER, uppercase_r); + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_LOWER, !uppercase_r); self } } } pub const fn prefix(self) -> FStringPrefix { - if self.0.contains(FStringFlagsInner::R_PREFIX_LOWER) { - debug_assert!(!self.0.contains(FStringFlagsInner::R_PREFIX_UPPER)); + if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_LOWER) + { + debug_assert!( + !self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + ); FStringPrefix::Raw { uppercase_r: false } - } else if self.0.contains(FStringFlagsInner::R_PREFIX_UPPER) { + } else if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + { FStringPrefix::Raw { uppercase_r: true } } else { FStringPrefix::Regular @@ -728,12 +970,109 @@ impl FStringFlags { } } +// TODO(dylan): the documentation about using +// `Checker::default_tstring_flags` is not yet +// correct. This method does not yet exist because +// introducing it would emit a dead code warning +// until we call it in lint rules. +/// Flags that can be queried to obtain information +/// regarding the prefixes and quotes used for an f-string. +/// +/// Note: This is identical to [`FStringFlags`] except that +/// the implementation of the `prefix` method of the +/// [`StringFlags`] trait returns a variant of +/// `AnyStringPrefix::Template`. +/// +/// ## Notes on usage +/// +/// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix +/// from an existing t-string literal, consider passing along the [`FString::flags`] field. If you +/// don't have an existing literal but have a `Checker` from the `ruff_linter` crate available, +/// consider using `Checker::default_tstring_flags` to create instances of this struct; this method +/// will properly handle nested t-strings. For usage that doesn't fit into one of these categories, +/// the public constructor [`TStringFlags::empty`] can be used. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct TStringFlags(InterpolatedStringFlagsInner); + +impl TStringFlags { + /// Construct a new [`TStringFlags`] with **no flags set**. + /// + /// See [`TStringFlags::with_quote_style`], [`TStringFlags::with_triple_quotes`], and + /// [`TStringFlags::with_prefix`] for ways of setting the quote style (single or double), + /// enabling triple quotes, and adding prefixes (such as `r`), respectively. + /// + /// See the documentation for [`TStringFlags`] for additional caveats on this constructor, and + /// situations in which alternative ways to construct this struct should be used, especially + /// when writing lint rules. + pub fn empty() -> Self { + Self(InterpolatedStringFlagsInner::empty()) + } + + #[must_use] + pub fn with_quote_style(mut self, quote_style: Quote) -> Self { + self.0.set( + InterpolatedStringFlagsInner::DOUBLE, + quote_style.is_double(), + ); + self + } + + #[must_use] + pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self { + self.0.set( + InterpolatedStringFlagsInner::TRIPLE_QUOTED, + triple_quotes.is_yes(), + ); + self + } + + #[must_use] + pub fn with_prefix(mut self, prefix: TStringPrefix) -> Self { + match prefix { + TStringPrefix::Regular => Self( + self.0 + - InterpolatedStringFlagsInner::R_PREFIX_LOWER + - InterpolatedStringFlagsInner::R_PREFIX_UPPER, + ), + TStringPrefix::Raw { uppercase_r } => { + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_UPPER, uppercase_r); + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_LOWER, !uppercase_r); + self + } + } + } + + pub const fn prefix(self) -> TStringPrefix { + if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_LOWER) + { + debug_assert!( + !self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + ); + TStringPrefix::Raw { uppercase_r: false } + } else if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + { + TStringPrefix::Raw { uppercase_r: true } + } else { + TStringPrefix::Regular + } + } +} + impl StringFlags for FStringFlags { /// Return `true` if the f-string is triple-quoted, i.e., /// it begins and ends with three consecutive quote characters. /// For example: `f"""{bar}"""` fn triple_quotes(self) -> TripleQuotes { - if self.0.contains(FStringFlagsInner::TRIPLE_QUOTED) { + if self.0.contains(InterpolatedStringFlagsInner::TRIPLE_QUOTED) { TripleQuotes::Yes } else { TripleQuotes::No @@ -745,7 +1084,7 @@ impl StringFlags for FStringFlags { /// - `f"{"a"}"` -> `QuoteStyle::Double` /// - `f'{"a"}'` -> `QuoteStyle::Single` fn quote_style(self) -> Quote { - if self.0.contains(FStringFlagsInner::DOUBLE) { + if self.0.contains(InterpolatedStringFlagsInner::DOUBLE) { Quote::Double } else { Quote::Single @@ -767,17 +1106,59 @@ impl fmt::Debug for FStringFlags { } } +impl StringFlags for TStringFlags { + /// Return `true` if the t-string is triple-quoted, i.e., + /// it begins and ends with three consecutive quote characters. + /// For example: `t"""{bar}"""` + fn triple_quotes(self) -> TripleQuotes { + if self.0.contains(InterpolatedStringFlagsInner::TRIPLE_QUOTED) { + TripleQuotes::Yes + } else { + TripleQuotes::No + } + } + + /// Return the quoting style (single or double quotes) + /// used by the t-string's opener and closer: + /// - `t"{"a"}"` -> `QuoteStyle::Double` + /// - `t'{"a"}'` -> `QuoteStyle::Single` + fn quote_style(self) -> Quote { + if self.0.contains(InterpolatedStringFlagsInner::DOUBLE) { + Quote::Double + } else { + Quote::Single + } + } + + fn prefix(self) -> AnyStringPrefix { + AnyStringPrefix::Template(self.prefix()) + } +} + +impl fmt::Debug for TStringFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TStringFlags") + .field("quote_style", &self.quote_style()) + .field("prefix", &self.prefix()) + .field("triple_quoted", &self.is_triple_quoted()) + .finish() + } +} + /// An AST node that represents a single f-string which is part of an [`ExprFString`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct FString { pub range: TextRange, - pub elements: FStringElements, + pub node_index: AtomicNodeIndex, + pub elements: InterpolatedStringElements, pub flags: FStringFlags, } impl From for Expr { fn from(payload: FString) -> Self { ExprFString { + node_index: payload.node_index.clone(), range: payload.range, value: FStringValue::single(payload), } @@ -785,66 +1166,88 @@ impl From for Expr { } } -/// A newtype wrapper around a list of [`FStringElement`]. +/// A newtype wrapper around a list of [`InterpolatedStringElement`]. #[derive(Clone, Default, PartialEq)] -pub struct FStringElements(Vec); +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct InterpolatedStringElements(Vec); -impl FStringElements { - /// Returns an iterator over all the [`FStringLiteralElement`] nodes contained in this f-string. - pub fn literals(&self) -> impl Iterator { +impl InterpolatedStringElements { + /// Returns an iterator over all the [`InterpolatedStringLiteralElement`] nodes contained in this f-string. + pub fn literals(&self) -> impl Iterator { self.iter().filter_map(|element| element.as_literal()) } - /// Returns an iterator over all the [`FStringExpressionElement`] nodes contained in this f-string. - pub fn expressions(&self) -> impl Iterator { - self.iter().filter_map(|element| element.as_expression()) + /// Returns an iterator over all the [`InterpolatedElement`] nodes contained in this f-string. + pub fn interpolations(&self) -> impl Iterator { + self.iter().filter_map(|element| element.as_interpolation()) } } -impl From> for FStringElements { - fn from(elements: Vec) -> Self { - FStringElements(elements) +impl From> for InterpolatedStringElements { + fn from(elements: Vec) -> Self { + InterpolatedStringElements(elements) } } -impl<'a> IntoIterator for &'a FStringElements { - type IntoIter = Iter<'a, FStringElement>; - type Item = &'a FStringElement; +impl<'a> IntoIterator for &'a InterpolatedStringElements { + type IntoIter = Iter<'a, InterpolatedStringElement>; + type Item = &'a InterpolatedStringElement; fn into_iter(self) -> Self::IntoIter { self.iter() } } -impl<'a> IntoIterator for &'a mut FStringElements { - type IntoIter = IterMut<'a, FStringElement>; - type Item = &'a mut FStringElement; +impl<'a> IntoIterator for &'a mut InterpolatedStringElements { + type IntoIter = IterMut<'a, InterpolatedStringElement>; + type Item = &'a mut InterpolatedStringElement; fn into_iter(self) -> Self::IntoIter { self.iter_mut() } } -impl Deref for FStringElements { - type Target = [FStringElement]; +impl Deref for InterpolatedStringElements { + type Target = [InterpolatedStringElement]; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for FStringElements { +impl DerefMut for InterpolatedStringElements { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl fmt::Debug for FStringElements { +impl fmt::Debug for InterpolatedStringElements { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } +/// An AST node that represents a single t-string which is part of an [`ExprTString`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] +pub struct TString { + pub range: TextRange, + pub node_index: AtomicNodeIndex, + pub elements: InterpolatedStringElements, + pub flags: TStringFlags, +} + +impl From for Expr { + fn from(payload: TString) -> Self { + ExprTString { + node_index: payload.node_index.clone(), + range: payload.range, + value: TStringValue::single(payload), + } + .into() + } +} + impl ExprStringLiteral { /// Return `Some(literal)` if the string only consists of a single `StringLiteral` part /// (indicating that it is not implicitly concatenated). Otherwise, return `None`. @@ -858,6 +1261,7 @@ impl ExprStringLiteral { /// The value representing a [`ExprStringLiteral`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StringLiteralValue { inner: StringLiteralValueInner, } @@ -1015,6 +1419,7 @@ impl fmt::Display for StringLiteralValue { /// An internal representation of [`StringLiteralValue`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] enum StringLiteralValueInner { /// A single string literal i.e., `"foo"`. Single(StringLiteral), @@ -1058,6 +1463,9 @@ bitflags! { } } +#[cfg(feature = "get-size")] +impl get_size2::GetSize for StringLiteralFlagsInner {} + /// Flags that can be queried to obtain information /// regarding the prefixes and quotes used for a string literal. /// @@ -1071,6 +1479,7 @@ bitflags! { /// handle surrounding f-strings. For usage that doesn't fit into one of these categories, the /// public constructor [`StringLiteralFlags::empty`] can be used. #[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StringLiteralFlags(StringLiteralFlagsInner); impl StringLiteralFlags { @@ -1139,10 +1548,12 @@ impl StringLiteralFlags { pub const fn prefix(self) -> StringLiteralPrefix { if self.0.contains(StringLiteralFlagsInner::U_PREFIX) { - debug_assert!(!self.0.intersects( - StringLiteralFlagsInner::R_PREFIX_LOWER - .union(StringLiteralFlagsInner::R_PREFIX_UPPER) - )); + debug_assert!( + !self.0.intersects( + StringLiteralFlagsInner::R_PREFIX_LOWER + .union(StringLiteralFlagsInner::R_PREFIX_UPPER) + ) + ); StringLiteralPrefix::Unicode } else if self.0.contains(StringLiteralFlagsInner::R_PREFIX_LOWER) { debug_assert!(!self.0.contains(StringLiteralFlagsInner::R_PREFIX_UPPER)); @@ -1197,8 +1608,10 @@ impl fmt::Debug for StringLiteralFlags { /// An AST node that represents a single string literal which is part of an /// [`ExprStringLiteral`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct StringLiteral { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub value: Box, pub flags: StringLiteralFlags, } @@ -1222,6 +1635,7 @@ impl StringLiteral { Self { range, value: "".into(), + node_index: AtomicNodeIndex::dummy(), flags: StringLiteralFlags::empty().with_invalid(), } } @@ -1241,6 +1655,7 @@ impl From for Expr { fn from(payload: StringLiteral) -> Self { ExprStringLiteral { range: payload.range, + node_index: AtomicNodeIndex::dummy(), value: StringLiteralValue::single(payload), } .into() @@ -1250,6 +1665,7 @@ impl From for Expr { /// An internal representation of [`StringLiteral`] that represents an /// implicitly concatenated string. #[derive(Clone)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] struct ConcatenatedStringLiteral { /// The individual [`StringLiteral`] parts that make up the concatenated string. strings: Vec, @@ -1302,6 +1718,7 @@ impl ExprBytesLiteral { /// The value representing a [`ExprBytesLiteral`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct BytesLiteralValue { inner: BytesLiteralValueInner, } @@ -1430,6 +1847,7 @@ impl<'a> From<&'a BytesLiteralValue> for Cow<'a, [u8]> { /// An internal representation of [`BytesLiteralValue`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] enum BytesLiteralValueInner { /// A single-part bytestring literal i.e., `b"foo"`. Single(BytesLiteral), @@ -1464,6 +1882,9 @@ bitflags! { } } +#[cfg(feature = "get-size")] +impl get_size2::GetSize for BytesLiteralFlagsInner {} + /// Flags that can be queried to obtain information /// regarding the prefixes and quotes used for a bytes literal. /// @@ -1476,6 +1897,7 @@ bitflags! { /// will properly handle surrounding f-strings. For usage that doesn't fit into one of these /// categories, the public constructor [`BytesLiteralFlags::empty`] can be used. #[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct BytesLiteralFlags(BytesLiteralFlagsInner); impl BytesLiteralFlags { @@ -1585,8 +2007,10 @@ impl fmt::Debug for BytesLiteralFlags { /// An AST node that represents a single bytes literal which is part of an /// [`ExprBytesLiteral`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct BytesLiteral { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub value: Box<[u8]>, pub flags: BytesLiteralFlags, } @@ -1610,6 +2034,7 @@ impl BytesLiteral { Self { range, value: Box::new([]), + node_index: AtomicNodeIndex::dummy(), flags: BytesLiteralFlags::empty().with_invalid(), } } @@ -1619,6 +2044,7 @@ impl From for Expr { fn from(payload: BytesLiteral) -> Self { ExprBytesLiteral { range: payload.range, + node_index: AtomicNodeIndex::dummy(), value: BytesLiteralValue::single(payload), } .into() @@ -1661,18 +2087,23 @@ bitflags! { /// but can have no other prefixes. const F_PREFIX = 1 << 4; + /// The string has a `t` or `T` prefix, meaning it is a t-string. + /// T-strings can also be raw strings, + /// but can have no other prefixes. + const T_PREFIX = 1 << 5; + /// The string has an `r` prefix, meaning it is a raw string. /// F-strings and byte-strings can be raw, /// as can strings with no other prefixes. /// U-strings cannot be raw. - const R_PREFIX_LOWER = 1 << 5; + const R_PREFIX_LOWER = 1 << 6; /// The string has an `R` prefix, meaning it is a raw string. /// The casing of the `r`/`R` has no semantic significance at runtime; /// see https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#r-strings-and-r-strings /// for why we track the casing of the `r` prefix, /// but not for any other prefix - const R_PREFIX_UPPER = 1 << 6; + const R_PREFIX_UPPER = 1 << 7; } } @@ -1710,6 +2141,15 @@ impl AnyStringFlags { AnyStringPrefix::Format(FStringPrefix::Raw { uppercase_r: true }) => { AnyStringFlagsInner::F_PREFIX.union(AnyStringFlagsInner::R_PREFIX_UPPER) } + + // t-strings + AnyStringPrefix::Template(TStringPrefix::Regular) => AnyStringFlagsInner::T_PREFIX, + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }) => { + AnyStringFlagsInner::T_PREFIX.union(AnyStringFlagsInner::R_PREFIX_LOWER) + } + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }) => { + AnyStringFlagsInner::T_PREFIX.union(AnyStringFlagsInner::R_PREFIX_UPPER) + } }; self } @@ -1733,9 +2173,10 @@ impl AnyStringFlags { ) } - /// Does the string have an `f` or `F` prefix? - pub const fn is_f_string(self) -> bool { - self.0.contains(AnyStringFlagsInner::F_PREFIX) + /// Does the string have an `f`,`F`,`t`, or `T` prefix? + pub const fn is_interpolated_string(self) -> bool { + self.0 + .intersects(AnyStringFlagsInner::F_PREFIX.union(AnyStringFlagsInner::T_PREFIX)) } /// Does the string have a `b` or `B` prefix? @@ -1792,6 +2233,17 @@ impl StringFlags for AnyStringFlags { return AnyStringPrefix::Format(FStringPrefix::Regular); } + // t-strings + if flags.contains(AnyStringFlagsInner::T_PREFIX) { + if flags.contains(AnyStringFlagsInner::R_PREFIX_LOWER) { + return AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }); + } + if flags.contains(AnyStringFlagsInner::R_PREFIX_UPPER) { + return AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }); + } + return AnyStringPrefix::Template(TStringPrefix::Regular); + } + // bytestrings if flags.contains(AnyStringFlagsInner::B_PREFIX) { if flags.contains(AnyStringFlagsInner::R_PREFIX_LOWER) { @@ -1871,7 +2323,7 @@ impl From for AnyStringFlags { impl From for FStringFlags { fn from(value: AnyStringFlags) -> FStringFlags { - let AnyStringPrefix::Format(fstring_prefix) = value.prefix() else { + let AnyStringPrefix::Format(prefix) = value.prefix() else { unreachable!( "Should never attempt to convert {} into an f-string", value.prefix() @@ -1879,7 +2331,7 @@ impl From for FStringFlags { }; FStringFlags::empty() .with_quote_style(value.quote_style()) - .with_prefix(fstring_prefix) + .with_prefix(prefix) .with_triple_quotes(value.triple_quotes()) } } @@ -1890,7 +2342,29 @@ impl From for AnyStringFlags { } } +impl From for TStringFlags { + fn from(value: AnyStringFlags) -> TStringFlags { + let AnyStringPrefix::Template(prefix) = value.prefix() else { + unreachable!( + "Should never attempt to convert {} into a t-string", + value.prefix() + ) + }; + TStringFlags::empty() + .with_quote_style(value.quote_style()) + .with_prefix(prefix) + .with_triple_quotes(value.triple_quotes()) + } +} + +impl From for AnyStringFlags { + fn from(value: TStringFlags) -> Self { + value.as_any_string_flags() + } +} + #[derive(Clone, Debug, PartialEq, is_macro::Is)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Number { Int(int::Int), Float(f64), @@ -1958,6 +2432,7 @@ impl<'a> IntoIterator for &'a ExprTuple { /// See also [expr_context](https://docs.python.org/3/library/ast.html#ast.expr_context) #[derive(Clone, Debug, PartialEq, is_macro::Is, Copy, Hash, Eq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum ExprContext { Load, Store, @@ -1967,6 +2442,7 @@ pub enum ExprContext { /// See also [boolop](https://docs.python.org/3/library/ast.html#ast.BoolOp) #[derive(Clone, Debug, PartialEq, is_macro::Is, Copy, Hash, Eq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum BoolOp { And, Or, @@ -1989,6 +2465,7 @@ impl fmt::Display for BoolOp { /// See also [operator](https://docs.python.org/3/library/ast.html#ast.operator) #[derive(Clone, Debug, PartialEq, is_macro::Is, Copy, Hash, Eq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Operator { Add, Sub, @@ -2090,6 +2567,7 @@ impl fmt::Display for Operator { /// See also [unaryop](https://docs.python.org/3/library/ast.html#ast.unaryop) #[derive(Clone, Debug, PartialEq, is_macro::Is, Copy, Hash, Eq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum UnaryOp { Invert, Not, @@ -2116,6 +2594,7 @@ impl fmt::Display for UnaryOp { /// See also [cmpop](https://docs.python.org/3/library/ast.html#ast.cmpop) #[derive(Clone, Debug, PartialEq, is_macro::Is, Copy, Hash, Eq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum CmpOp { Eq, NotEq, @@ -2170,8 +2649,10 @@ impl fmt::Display for CmpOp { /// See also [comprehension](https://docs.python.org/3/library/ast.html#ast.comprehension) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Comprehension { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub target: Expr, pub iter: Expr, pub ifs: Vec, @@ -2180,8 +2661,10 @@ pub struct Comprehension { /// See also [ExceptHandler](https://docs.python.org/3/library/ast.html#ast.ExceptHandler) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExceptHandlerExceptHandler { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub type_: Option>, pub name: Option, pub body: Vec, @@ -2189,8 +2672,10 @@ pub struct ExceptHandlerExceptHandler { /// See also [arg](https://docs.python.org/3/library/ast.html#ast.arg) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Parameter { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub name: Identifier, pub annotation: Option>, } @@ -2207,32 +2692,40 @@ impl Parameter { /// See also [keyword](https://docs.python.org/3/library/ast.html#ast.keyword) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Keyword { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub arg: Option, pub value: Expr, } /// See also [alias](https://docs.python.org/3/library/ast.html#ast.alias) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Alias { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub name: Identifier, pub asname: Option, } /// See also [withitem](https://docs.python.org/3/library/ast.html#ast.withitem) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct WithItem { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub context_expr: Expr, pub optional_vars: Option>, } /// See also [match_case](https://docs.python.org/3/library/ast.html#ast.match_case) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct MatchCase { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub pattern: Pattern, pub guard: Option>, pub body: Vec, @@ -2253,16 +2746,19 @@ impl Pattern { pattern, name, range, + node_index, }) => match pattern { Some(pattern) => pattern.irrefutable_pattern(), None => match name { Some(name) => Some(IrrefutablePattern { kind: IrrefutablePatternKind::Name(name.id.clone()), range: *range, + node_index: node_index.clone(), }), None => Some(IrrefutablePattern { kind: IrrefutablePatternKind::Wildcard, range: *range, + node_index: node_index.clone(), }), }, }, @@ -2300,9 +2796,11 @@ impl Pattern { pub struct IrrefutablePattern { pub kind: IrrefutablePatternKind, pub range: TextRange, + pub node_index: AtomicNodeIndex, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum IrrefutablePatternKind { Name(Name), Wildcard, @@ -2310,29 +2808,37 @@ pub enum IrrefutablePatternKind { /// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchValue { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub value: Box, } /// See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchSingleton { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub value: Singleton, } /// See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchSequence { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub patterns: Vec, } /// See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchMapping { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub keys: Vec, pub patterns: Vec, pub rest: Option, @@ -2340,8 +2846,10 @@ pub struct PatternMatchMapping { /// See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchClass { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub cls: Box, pub arguments: PatternArguments, } @@ -2351,8 +2859,10 @@ pub struct PatternMatchClass { /// /// Like [`Arguments`], but for [`PatternMatchClass`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternArguments { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub patterns: Vec, pub keywords: Vec, } @@ -2362,31 +2872,39 @@ pub struct PatternArguments { /// /// Like [`Keyword`], but for [`PatternMatchClass`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternKeyword { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub attr: Identifier, pub pattern: Pattern, } /// See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchStar { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub name: Option, } /// See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchAs { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub pattern: Option>, pub name: Option, } /// See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PatternMatchOr { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub patterns: Vec, } @@ -2410,8 +2928,10 @@ impl TypeParam { /// See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct TypeParamTypeVar { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub name: Identifier, pub bound: Option>, pub default: Option>, @@ -2419,24 +2939,30 @@ pub struct TypeParamTypeVar { /// See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct TypeParamParamSpec { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub name: Identifier, pub default: Option>, } /// See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct TypeParamTypeVarTuple { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub name: Identifier, pub default: Option>, } /// See also [decorator](https://docs.python.org/3/library/ast.html#ast.decorator) #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Decorator { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub expression: Expr, } @@ -2508,8 +3034,10 @@ impl Ranged for AnyParameterRef<'_> { /// NOTE: This type differs from the original Python AST. See: [arguments](https://docs.python.org/3/library/ast.html#ast.arguments). #[derive(Clone, Debug, PartialEq, Default)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Parameters { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub posonlyargs: Vec, pub args: Vec, pub vararg: Option>, @@ -2544,6 +3072,7 @@ impl Parameters { pub fn len(&self) -> usize { let Parameters { range: _, + node_index: _, posonlyargs, args, vararg, @@ -2592,6 +3121,7 @@ impl<'a> ParametersIterator<'a> { fn new(parameters: &'a Parameters) -> Self { let Parameters { range: _, + node_index: _, posonlyargs, args, vararg, @@ -2724,8 +3254,10 @@ impl<'a> IntoIterator for &'a Box { /// NOTE: This type is different from original Python AST. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ParameterWithDefault { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub parameter: Parameter, pub default: Option>, } @@ -2767,8 +3299,10 @@ impl ParameterWithDefault { /// typically used for `metaclass`, with any additional arguments being passed to the `metaclass`. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Arguments { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub args: Box<[Expr]>, pub keywords: Box<[Keyword]>, } @@ -2844,7 +3378,7 @@ impl Arguments { self.find_argument(name, position).map(ArgOrKeyword::value) } - /// Return the the argument with the given name or at the given position, or `None` if no such + /// Return the argument with the given name or at the given position, or `None` if no such /// argument exists. Used to retrieve arguments that can be provided _either_ as keyword or /// positional arguments. pub fn find_argument(&self, name: &str, position: usize) -> Option { @@ -2916,8 +3450,10 @@ impl Arguments { /// the `T`, `U`, and `V` type parameters in the order they appear in the source code. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct TypeParams { pub range: TextRange, + pub node_index: AtomicNodeIndex, pub type_params: Vec, } @@ -2938,6 +3474,7 @@ pub type Suite = Vec; /// /// [IPython Syntax]: https://github.com/ipython/ipython/blob/635815e8f1ded5b764d66cacc80bbe25e9e2587f/IPython/core/inputtransformer2.py#L335-L343 #[derive(PartialEq, Eq, Debug, Clone, Hash, Copy)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum IpyEscapeKind { /// Send line to underlying system shell (`!`). Shell, @@ -3029,9 +3566,11 @@ impl IpyEscapeKind { /// ... /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct Identifier { pub id: Name, pub range: TextRange, + pub node_index: AtomicNodeIndex, } impl Identifier { @@ -3039,6 +3578,7 @@ impl Identifier { pub fn new(id: impl Into, range: TextRange) -> Self { Self { id: id.into(), + node_index: AtomicNodeIndex::dummy(), range, } } @@ -3102,6 +3642,7 @@ impl From for Name { } #[derive(Clone, Copy, Debug, Hash, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub enum Singleton { None, True, @@ -3120,51 +3661,50 @@ impl From for Singleton { #[cfg(test)] mod tests { - use crate::generated::*; use crate::Mod; + use crate::generated::*; #[test] #[cfg(target_pointer_width = "64")] fn size() { - assert!(std::mem::size_of::() <= 120); - assert!(std::mem::size_of::() <= 120); - assert!(std::mem::size_of::() <= 104); - assert!(std::mem::size_of::() <= 112); - assert!(std::mem::size_of::() <= 32); - assert!(matches!(std::mem::size_of::(), 88)); - - assert_eq!(std::mem::size_of::(), 64); - assert_eq!(std::mem::size_of::(), 56); - assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::size_of::(), 128); + assert_eq!(std::mem::size_of::(), 128); + assert_eq!(std::mem::size_of::(), 120); + assert_eq!(std::mem::size_of::(), 112); + assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 104); + assert_eq!(std::mem::size_of::(), 80); + assert_eq!(std::mem::size_of::(), 64); + assert_eq!(std::mem::size_of::(), 24); assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 40); - assert_eq!(std::mem::size_of::(), 12); - assert_eq!(std::mem::size_of::(), 40); - assert_eq!(std::mem::size_of::(), 56); - assert_eq!(std::mem::size_of::(), 48); - assert_eq!(std::mem::size_of::(), 32); - assert_eq!(std::mem::size_of::(), 48); - assert_eq!(std::mem::size_of::(), 8); - assert!(matches!(std::mem::size_of::(), 48)); + assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::size_of::(), 48); + assert_eq!(std::mem::size_of::(), 72); + assert_eq!(std::mem::size_of::(), 56); + assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 56); + assert_eq!(std::mem::size_of::(), 12); + assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 48); - assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 32); - assert_eq!(std::mem::size_of::(), 24); + assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 40); - assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 48); assert_eq!(std::mem::size_of::(), 40); - assert_eq!(std::mem::size_of::(), 24); - assert_eq!(std::mem::size_of::(), 8); - assert_eq!(std::mem::size_of::(), 32); - assert_eq!(std::mem::size_of::(), 32); - assert_eq!(std::mem::size_of::(), 40); - assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 12); + assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 48); + assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 24); - assert_eq!(std::mem::size_of::(), 56); + assert_eq!(std::mem::size_of::(), 64); assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 24); - assert_eq!(std::mem::size_of::(), 16); - assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::size_of::(), 24); + assert_eq!(std::mem::size_of::(), 24); } } diff --git a/crates/ruff_python_ast/src/operator_precedence.rs b/crates/ruff_python_ast/src/operator_precedence.rs index 750ef7f719d3c..6b652847e9a57 100644 --- a/crates/ruff_python_ast/src/operator_precedence.rs +++ b/crates/ruff_python_ast/src/operator_precedence.rs @@ -72,7 +72,8 @@ impl OperatorPrecedence { | ExprRef::BooleanLiteral(_) | ExprRef::NoneLiteral(_) | ExprRef::EllipsisLiteral(_) - | ExprRef::FString(_) => Self::Atomic, + | ExprRef::FString(_) + | ExprRef::TString(_) => Self::Atomic, // Subscription, slicing, call, attribute reference ExprRef::Attribute(_) | ExprRef::Subscript(_) diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index e5d1406ded277..7eab154ad1317 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -1,10 +1,11 @@ -use std::fmt; +use std::{fmt, str::FromStr}; /// Representation of a Python version. /// /// N.B. This does not necessarily represent a Python version that we actually support. #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct PythonVersion { pub major: u8, pub minor: u8, @@ -30,6 +31,10 @@ impl PythonVersion { major: 3, minor: 13, }; + pub const PY314: PythonVersion = PythonVersion { + major: 3, + minor: 14, + }; pub fn iter() -> impl Iterator { [ @@ -40,6 +45,7 @@ impl PythonVersion { PythonVersion::PY311, PythonVersion::PY312, PythonVersion::PY313, + PythonVersion::PY314, ] .into_iter() } @@ -49,10 +55,23 @@ impl PythonVersion { Self::PY37 } + // TODO: change this to 314 when it is released pub const fn latest() -> Self { Self::PY313 } + /// The latest Python version supported in preview + pub fn latest_preview() -> Self { + let latest_preview = Self::PY314; + debug_assert!(latest_preview >= Self::latest()); + latest_preview + } + + pub const fn latest_ty() -> Self { + // Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version. + Self::PY313 + } + pub const fn as_tuple(self) -> (u8, u8) { (self.major, self.minor) } @@ -67,6 +86,10 @@ impl PythonVersion { pub fn supports_pep_701(self) -> bool { self >= Self::PY312 } + + pub fn defers_annotations(self) -> bool { + self >= Self::PY314 + } } impl Default for PythonVersion { @@ -75,18 +98,6 @@ impl Default for PythonVersion { } } -impl TryFrom<(&str, &str)> for PythonVersion { - type Error = std::num::ParseIntError; - - fn try_from(value: (&str, &str)) -> Result { - let (major, minor) = value; - Ok(Self { - major: major.parse()?, - minor: minor.parse()?, - }) - } -} - impl From<(u8, u8)> for PythonVersion { fn from(value: (u8, u8)) -> Self { let (major, minor) = value; @@ -101,6 +112,55 @@ impl fmt::Display for PythonVersion { } } +#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)] +pub enum PythonVersionDeserializationError { + #[error("Invalid python version `{0}`: expected `major.minor`")] + WrongPeriodNumber(Box), + #[error("Invalid major version `{0}`: {1}")] + InvalidMajorVersion(Box, #[source] std::num::ParseIntError), + #[error("Invalid minor version `{0}`: {1}")] + InvalidMinorVersion(Box, #[source] std::num::ParseIntError), +} + +impl TryFrom<(&str, &str)> for PythonVersion { + type Error = PythonVersionDeserializationError; + + fn try_from(value: (&str, &str)) -> Result { + let (major, minor) = value; + Ok(Self { + major: major.parse().map_err(|err| { + PythonVersionDeserializationError::InvalidMajorVersion(Box::from(major), err) + })?, + minor: minor.parse().map_err(|err| { + PythonVersionDeserializationError::InvalidMinorVersion(Box::from(minor), err) + })?, + }) + } +} + +impl FromStr for PythonVersion { + type Err = PythonVersionDeserializationError; + + fn from_str(s: &str) -> Result { + let (major, minor) = s + .split_once('.') + .ok_or_else(|| PythonVersionDeserializationError::WrongPeriodNumber(Box::from(s)))?; + + Self::try_from((major, minor)).map_err(|err| { + // Give a better error message for something like `3.8.5` or `3..8` + if matches!( + err, + PythonVersionDeserializationError::InvalidMinorVersion(_, _) + ) && minor.contains('.') + { + PythonVersionDeserializationError::WrongPeriodNumber(Box::from(s)) + } else { + err + } + }) + } +} + #[cfg(feature = "serde")] mod serde { use super::PythonVersion; @@ -110,26 +170,9 @@ mod serde { where D: serde::Deserializer<'de>, { - let as_str = String::deserialize(deserializer)?; - - if let Some((major, minor)) = as_str.split_once('.') { - let major = major.parse().map_err(|err| { - serde::de::Error::custom(format!("invalid major version: {err}")) - })?; - let minor = minor.parse().map_err(|err| { - serde::de::Error::custom(format!("invalid minor version: {err}")) - })?; - - Ok((major, minor).into()) - } else { - let major = as_str.parse().map_err(|err| { - serde::de::Error::custom(format!( - "invalid python-version: {err}, expected: `major.minor`" - )) - })?; - - Ok((major, 0).into()) - } + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) } } @@ -146,16 +189,16 @@ mod serde { #[cfg(feature = "schemars")] mod schemars { use super::PythonVersion; - use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation}; - use schemars::JsonSchema; use schemars::_serde_json::Value; + use schemars::JsonSchema; + use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation}; impl JsonSchema for PythonVersion { fn schema_name() -> String { "PythonVersion".to_string() } - fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema { + fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> Schema { let sub_schemas = std::iter::once(Schema::Object(SchemaObject { instance_type: Some(schemars::schema::InstanceType::String.into()), string: Some(Box::new(schemars::schema::StringValidation { diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index 803aea06f029b..eea26d7a37c6c 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -1,6 +1,6 @@ use ruff_text_size::TextRange; -use crate::visitor::transformer::{walk_expr, walk_keyword, Transformer}; +use crate::visitor::transformer::{Transformer, walk_expr, walk_keyword}; use crate::{self as ast}; use crate::{Expr, Keyword}; @@ -72,6 +72,9 @@ impl Transformer for Relocator { Expr::FString(ast::ExprFString { range, .. }) => { *range = self.range; } + Expr::TString(ast::ExprTString { range, .. }) => { + *range = self.range; + } Expr::StringLiteral(ast::ExprStringLiteral { range, .. }) => { *range = self.range; } @@ -84,10 +87,10 @@ impl Transformer for Relocator { Expr::BooleanLiteral(ast::ExprBooleanLiteral { range, .. }) => { *range = self.range; } - Expr::NoneLiteral(ast::ExprNoneLiteral { range }) => { + Expr::NoneLiteral(ast::ExprNoneLiteral { range, .. }) => { *range = self.range; } - Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { range }) => { + Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { range, .. }) => { *range = self.range; } Expr::Attribute(ast::ExprAttribute { range, .. }) => { diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 5a8dd1093e3b5..a9096a5218a00 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -5,7 +5,7 @@ use std::sync::LazyLock; use ruff_text_size::{TextLen, TextRange}; /// Enumeration of the two kinds of quotes that can be used -/// for Python string/f-string/bytestring literals +/// for Python string/f/t-string/bytestring literals #[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq, is_macro::Is)] pub enum Quote { /// E.g. `'` diff --git a/crates/ruff_python_ast/src/str_prefix.rs b/crates/ruff_python_ast/src/str_prefix.rs index 37f8421711da8..a00b02fb46c28 100644 --- a/crates/ruff_python_ast/src/str_prefix.rs +++ b/crates/ruff_python_ast/src/str_prefix.rs @@ -91,6 +91,47 @@ impl fmt::Display for FStringPrefix { } } +/// Enumeration of the valid prefixes a t-string literal can have. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum TStringPrefix { + /// Just a regular t-string with no other prefixes, e.g. t"{bar}" + Regular, + + /// A "raw" template string, that has an `r` or `R` prefix, + /// e.g. `rt"{bar}"` or `Rt"{bar}"` + Raw { uppercase_r: bool }, +} + +impl TStringPrefix { + /// Return a `str` representation of the prefix + pub const fn as_str(self) -> &'static str { + match self { + Self::Regular => "t", + Self::Raw { uppercase_r: true } => "Rt", + Self::Raw { uppercase_r: false } => "rt", + } + } + + pub const fn text_len(self) -> TextSize { + match self { + Self::Regular => TextSize::new(1), + Self::Raw { .. } => TextSize::new(2), + } + } + + /// Return true if this prefix indicates a "raw t-string", + /// e.g. `rt"{bar}"` or `Rt"{bar}"` + pub const fn is_raw(self) -> bool { + matches!(self, Self::Raw { .. }) + } +} + +impl fmt::Display for TStringPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + /// Enumeration of the valid prefixes a bytestring literal can have. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum ByteStringPrefix { @@ -151,6 +192,9 @@ pub enum AnyStringPrefix { /// Prefixes that indicate the string is an f-string Format(FStringPrefix), + /// Prefixes that indicate the string is a t-string + Template(TStringPrefix), + /// All other prefixes Regular(StringLiteralPrefix), } @@ -161,6 +205,7 @@ impl AnyStringPrefix { Self::Regular(regular_prefix) => regular_prefix.as_str(), Self::Bytes(bytestring_prefix) => bytestring_prefix.as_str(), Self::Format(fstring_prefix) => fstring_prefix.as_str(), + Self::Template(tstring_prefix) => tstring_prefix.as_str(), } } @@ -169,6 +214,7 @@ impl AnyStringPrefix { Self::Regular(regular_prefix) => regular_prefix.text_len(), Self::Bytes(bytestring_prefix) => bytestring_prefix.text_len(), Self::Format(fstring_prefix) => fstring_prefix.text_len(), + Self::Template(tstring_prefix) => tstring_prefix.text_len(), } } @@ -177,6 +223,7 @@ impl AnyStringPrefix { Self::Regular(regular_prefix) => regular_prefix.is_raw(), Self::Bytes(bytestring_prefix) => bytestring_prefix.is_raw(), Self::Format(fstring_prefix) => fstring_prefix.is_raw(), + Self::Template(tstring_prefix) => tstring_prefix.is_raw(), } } } diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 6d9bb92cef968..786a7c643f61c 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -5,10 +5,10 @@ pub mod transformer; use crate::{ self as ast, Alias, AnyParameterRef, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, - Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, - FStringPart, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments, - PatternKeyword, Stmt, StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, - TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem, + Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringPart, + InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, + PatternArguments, PatternKeyword, Stmt, StringLiteral, TString, TStringPart, TypeParam, + TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem, }; /// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order. @@ -99,8 +99,14 @@ pub trait Visitor<'a> { fn visit_f_string(&mut self, f_string: &'a FString) { walk_f_string(self, f_string); } - fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { - walk_f_string_element(self, f_string_element); + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { + walk_interpolated_string_element(self, interpolated_string_element); + } + fn visit_t_string(&mut self, t_string: &'a TString) { + walk_t_string(self, t_string); } fn visit_string_literal(&mut self, string_literal: &'a StringLiteral) { walk_string_literal(self, string_literal); @@ -166,18 +172,27 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { } visitor.visit_body(body); } - Stmt::Return(ast::StmtReturn { value, range: _ }) => { + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => { if let Some(expr) = value { visitor.visit_expr(expr); } } - Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { + Stmt::Delete(ast::StmtDelete { + targets, + range: _, + node_index: _, + }) => { for expr in targets { visitor.visit_expr(expr); } } Stmt::TypeAlias(ast::StmtTypeAlias { range: _, + node_index: _, name, type_params, value, @@ -199,6 +214,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { op, value, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_operator(op); @@ -233,6 +249,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { body, orelse, range: _, + node_index: _, }) => { visitor.visit_expr(test); visitor.visit_body(body); @@ -243,6 +260,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { body, elif_else_clauses, range: _, + node_index: _, }) => { visitor.visit_expr(test); visitor.visit_body(body); @@ -263,6 +281,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { subject, cases, range: _, + node_index: _, }) => { visitor.visit_expr(subject); for match_case in cases { @@ -273,6 +292,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { exc, cause, range: _, + node_index: _, }) => { if let Some(expr) = exc { visitor.visit_expr(expr); @@ -288,6 +308,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { finalbody, is_star: _, range: _, + node_index: _, }) => { visitor.visit_body(body); for except_handler in handlers { @@ -300,13 +321,18 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { test, msg, range: _, + node_index: _, }) => { visitor.visit_expr(test); if let Some(expr) = msg { visitor.visit_expr(expr); } } - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { for alias in names { visitor.visit_alias(alias); } @@ -318,7 +344,11 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { } Stmt::Global(_) => {} Stmt::Nonlocal(_) => {} - Stmt::Expr(ast::StmtExpr { value, range: _ }) => visitor.visit_expr(value), + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => visitor.visit_expr(value), Stmt::Pass(_) | Stmt::Break(_) | Stmt::Continue(_) | Stmt::IpyEscapeCommand(_) => {} } } @@ -337,6 +367,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { op, values, range: _, + node_index: _, }) => { visitor.visit_bool_op(op); for expr in values { @@ -347,6 +378,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { target, value, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_expr(target); @@ -356,6 +388,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { op, right, range: _, + node_index: _, }) => { visitor.visit_expr(left); visitor.visit_operator(op); @@ -365,6 +398,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { op, operand, range: _, + node_index: _, }) => { visitor.visit_unary_op(op); visitor.visit_expr(operand); @@ -373,6 +407,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { parameters, body, range: _, + node_index: _, }) => { if let Some(parameters) = parameters { visitor.visit_parameters(parameters); @@ -384,12 +419,17 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { body, orelse, range: _, + node_index: _, }) => { visitor.visit_expr(test); visitor.visit_expr(body); visitor.visit_expr(orelse); } - Expr::Dict(ast::ExprDict { items, range: _ }) => { + Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) => { for ast::DictItem { key, value } in items { if let Some(key) = key { visitor.visit_expr(key); @@ -397,7 +437,11 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { visitor.visit_expr(value); } } - Expr::Set(ast::ExprSet { elts, range: _ }) => { + Expr::Set(ast::ExprSet { + elts, + range: _, + node_index: _, + }) => { for expr in elts { visitor.visit_expr(expr); } @@ -406,6 +450,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { elt, generators, range: _, + node_index: _, }) => { for comprehension in generators { visitor.visit_comprehension(comprehension); @@ -416,6 +461,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { elt, generators, range: _, + node_index: _, }) => { for comprehension in generators { visitor.visit_comprehension(comprehension); @@ -427,6 +473,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { value, generators, range: _, + node_index: _, }) => { for comprehension in generators { visitor.visit_comprehension(comprehension); @@ -438,6 +485,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { elt, generators, range: _, + node_index: _, parenthesized: _, }) => { for comprehension in generators { @@ -445,18 +493,31 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { } visitor.visit_expr(elt); } - Expr::Await(ast::ExprAwait { value, range: _ }) => visitor.visit_expr(value), - Expr::Yield(ast::ExprYield { value, range: _ }) => { + Expr::Await(ast::ExprAwait { + value, + range: _, + node_index: _, + }) => visitor.visit_expr(value), + Expr::Yield(ast::ExprYield { + value, + range: _, + node_index: _, + }) => { if let Some(expr) = value { visitor.visit_expr(expr); } } - Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) => visitor.visit_expr(value), + Expr::YieldFrom(ast::ExprYieldFrom { + value, + range: _, + node_index: _, + }) => visitor.visit_expr(value), Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _, + node_index: _, }) => { visitor.visit_expr(left); for cmp_op in ops { @@ -470,6 +531,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { func, arguments, range: _, + node_index: _, }) => { visitor.visit_expr(func); visitor.visit_arguments(arguments); @@ -484,6 +546,17 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { } } } + Expr::TString(ast::ExprTString { value, .. }) => { + for part in value { + match part { + TStringPart::Literal(string_literal) => { + visitor.visit_string_literal(string_literal); + } + TStringPart::FString(f_string) => visitor.visit_f_string(f_string), + TStringPart::TString(t_string) => visitor.visit_t_string(t_string), + } + } + } Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { for string_literal in value { visitor.visit_string_literal(string_literal); @@ -507,6 +580,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { slice, ctx, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_expr(slice); @@ -516,6 +590,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { value, ctx, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_expr_context(ctx); @@ -527,6 +602,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { elts, ctx, range: _, + node_index: _, }) => { for expr in elts { visitor.visit_expr(expr); @@ -537,6 +613,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { elts, ctx, range: _, + node_index: _, parenthesized: _, }) => { for expr in elts { @@ -549,6 +626,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { upper, step, range: _, + node_index: _, }) => { if let Some(expr) = lower { visitor.visit_expr(expr); @@ -590,7 +668,7 @@ pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>( } pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) { - // Note that the there might be keywords before the last arg, e.g. in + // Note that there might be keywords before the last arg, e.g. in // f(*args, a=2, *args2, **kwargs)`, but we follow Python in evaluating first `args` and then // `keywords`. See also [Arguments::arguments_source_order`]. for arg in &*arguments.args { @@ -645,6 +723,7 @@ pub fn walk_type_param<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, type_param: default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = bound { visitor.visit_expr(expr); @@ -657,6 +736,7 @@ pub fn walk_type_param<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, type_param: default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = default { visitor.visit_expr(expr); @@ -666,6 +746,7 @@ pub fn walk_type_param<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, type_param: default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = default { visitor.visit_expr(expr); @@ -739,30 +820,36 @@ pub fn walk_pattern_keyword<'a, V: Visitor<'a> + ?Sized>( } pub fn walk_f_string<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, f_string: &'a FString) { - for f_string_element in &f_string.elements { - visitor.visit_f_string_element(f_string_element); + for interpolated_string_element in &f_string.elements { + visitor.visit_interpolated_string_element(interpolated_string_element); } } -pub fn walk_f_string_element<'a, V: Visitor<'a> + ?Sized>( +pub fn walk_interpolated_string_element<'a, V: Visitor<'a> + ?Sized>( visitor: &mut V, - f_string_element: &'a FStringElement, + interpolated_string_element: &'a InterpolatedStringElement, ) { - if let ast::FStringElement::Expression(ast::FStringExpressionElement { + if let ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, format_spec, .. - }) = f_string_element + }) = interpolated_string_element { visitor.visit_expr(expression); if let Some(format_spec) = format_spec { for spec_element in &format_spec.elements { - visitor.visit_f_string_element(spec_element); + visitor.visit_interpolated_string_element(spec_element); } } } } +pub fn walk_t_string<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, t_string: &'a TString) { + for t_string_element in &t_string.elements { + visitor.visit_interpolated_string_element(t_string_element); + } +} + pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>( _visitor: &V, _expr_context: &'a ExprContext, diff --git a/crates/ruff_python_ast/src/visitor/source_order.rs b/crates/ruff_python_ast/src/visitor/source_order.rs index 5e6ca022a0600..9e42766f0d4ac 100644 --- a/crates/ruff_python_ast/src/visitor/source_order.rs +++ b/crates/ruff_python_ast/src/visitor/source_order.rs @@ -1,8 +1,8 @@ use crate::{ Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator, ElifElseClause, - ExceptHandler, Expr, FString, FStringElement, Keyword, MatchCase, Mod, Operator, Parameter, - ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword, Singleton, Stmt, - StringLiteral, TypeParam, TypeParams, UnaryOp, WithItem, + ExceptHandler, Expr, FString, InterpolatedStringElement, Keyword, MatchCase, Mod, Operator, + Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword, + Singleton, Stmt, StringLiteral, TString, TypeParam, TypeParams, UnaryOp, WithItem, }; use crate::{AnyNodeRef, Identifier}; @@ -157,8 +157,16 @@ pub trait SourceOrderVisitor<'a> { } #[inline] - fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { - walk_f_string_element(self, f_string_element); + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { + walk_interpolated_string_element(self, interpolated_string_element); + } + + #[inline] + fn visit_t_string(&mut self, t_string: &'a TString) { + walk_t_string(self, t_string); } #[inline] @@ -272,6 +280,7 @@ where Expr::Compare(expr) => expr.visit_source_order(visitor), Expr::Call(expr) => expr.visit_source_order(visitor), Expr::FString(expr) => expr.visit_source_order(visitor), + Expr::TString(expr) => expr.visit_source_order(visitor), Expr::StringLiteral(expr) => expr.visit_source_order(visitor), Expr::BytesLiteral(expr) => expr.visit_source_order(visitor), Expr::NumberLiteral(expr) => expr.visit_source_order(visitor), @@ -492,20 +501,22 @@ where { let node = AnyNodeRef::from(pattern_keyword); if visitor.enter_node(node).is_traverse() { - visitor.visit_pattern(&pattern_keyword.pattern); + pattern_keyword.visit_source_order(visitor); } visitor.leave_node(node); } -pub fn walk_f_string_element<'a, V: SourceOrderVisitor<'a> + ?Sized>( +pub fn walk_interpolated_string_element<'a, V: SourceOrderVisitor<'a> + ?Sized>( visitor: &mut V, - f_string_element: &'a FStringElement, + f_string_element: &'a InterpolatedStringElement, ) { let node = AnyNodeRef::from(f_string_element); if visitor.enter_node(node).is_traverse() { match f_string_element { - FStringElement::Expression(element) => element.visit_source_order(visitor), - FStringElement::Literal(element) => element.visit_source_order(visitor), + InterpolatedStringElement::Interpolation(element) => { + element.visit_source_order(visitor); + } + InterpolatedStringElement::Literal(element) => element.visit_source_order(visitor), } } visitor.leave_node(node); @@ -550,6 +561,18 @@ where visitor.leave_node(node); } +#[inline] +pub fn walk_t_string<'a, V>(visitor: &mut V, t_string: &'a TString) +where + V: SourceOrderVisitor<'a> + ?Sized, +{ + let node = AnyNodeRef::from(t_string); + if visitor.enter_node(node).is_traverse() { + t_string.visit_source_order(visitor); + } + visitor.leave_node(node); +} + #[inline] pub fn walk_string_literal<'a, V>(visitor: &mut V, string_literal: &'a StringLiteral) where diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index dad507c53e34f..4ceebf619a43a 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -1,8 +1,8 @@ use crate::{ self as ast, Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator, - ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, Keyword, MatchCase, - Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, - StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, + ElifElseClause, ExceptHandler, Expr, ExprContext, FString, InterpolatedStringElement, Keyword, + MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, + StringLiteral, TString, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem, }; @@ -86,8 +86,14 @@ pub trait Transformer { fn visit_f_string(&self, f_string: &mut FString) { walk_f_string(self, f_string); } - fn visit_f_string_element(&self, f_string_element: &mut FStringElement) { - walk_f_string_element(self, f_string_element); + fn visit_interpolated_string_element( + &self, + interpolated_string_element: &mut InterpolatedStringElement, + ) { + walk_interpolated_string_element(self, interpolated_string_element); + } + fn visit_t_string(&self, t_string: &mut TString) { + walk_t_string(self, t_string); } fn visit_string_literal(&self, string_literal: &mut StringLiteral) { walk_string_literal(self, string_literal); @@ -153,18 +159,27 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { } visitor.visit_body(body); } - Stmt::Return(ast::StmtReturn { value, range: _ }) => { + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => { if let Some(expr) = value { visitor.visit_expr(expr); } } - Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { + Stmt::Delete(ast::StmtDelete { + targets, + range: _, + node_index: _, + }) => { for expr in targets { visitor.visit_expr(expr); } } Stmt::TypeAlias(ast::StmtTypeAlias { range: _, + node_index: _, name, type_params, value, @@ -186,6 +201,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { op, value, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_operator(op); @@ -220,6 +236,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { body, orelse, range: _, + node_index: _, }) => { visitor.visit_expr(test); visitor.visit_body(body); @@ -230,13 +247,11 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { body, elif_else_clauses, range: _, + node_index: _, }) => { visitor.visit_expr(test); visitor.visit_body(body); for clause in elif_else_clauses { - if let Some(test) = &mut clause.test { - visitor.visit_expr(test); - } walk_elif_else_clause(visitor, clause); } } @@ -250,6 +265,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { subject, cases, range: _, + node_index: _, }) => { visitor.visit_expr(subject); for match_case in cases { @@ -260,6 +276,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { exc, cause, range: _, + node_index: _, }) => { if let Some(expr) = exc { visitor.visit_expr(expr); @@ -275,6 +292,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { finalbody, is_star: _, range: _, + node_index: _, }) => { visitor.visit_body(body); for except_handler in handlers { @@ -287,13 +305,18 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { test, msg, range: _, + node_index: _, }) => { visitor.visit_expr(test); if let Some(expr) = msg { visitor.visit_expr(expr); } } - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { for alias in names { visitor.visit_alias(alias); } @@ -305,7 +328,11 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { } Stmt::Global(_) => {} Stmt::Nonlocal(_) => {} - Stmt::Expr(ast::StmtExpr { value, range: _ }) => visitor.visit_expr(value), + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => visitor.visit_expr(value), Stmt::Pass(_) | Stmt::Break(_) | Stmt::Continue(_) | Stmt::IpyEscapeCommand(_) => {} } } @@ -324,6 +351,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { op, values, range: _, + node_index: _, }) => { visitor.visit_bool_op(op); for expr in values { @@ -334,6 +362,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { target, value, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_expr(target); @@ -343,6 +372,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { op, right, range: _, + node_index: _, }) => { visitor.visit_expr(left); visitor.visit_operator(op); @@ -352,6 +382,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { op, operand, range: _, + node_index: _, }) => { visitor.visit_unary_op(op); visitor.visit_expr(operand); @@ -360,6 +391,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { parameters, body, range: _, + node_index: _, }) => { if let Some(parameters) = parameters { visitor.visit_parameters(parameters); @@ -371,12 +403,17 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { body, orelse, range: _, + node_index: _, }) => { visitor.visit_expr(test); visitor.visit_expr(body); visitor.visit_expr(orelse); } - Expr::Dict(ast::ExprDict { items, range: _ }) => { + Expr::Dict(ast::ExprDict { + items, + range: _, + node_index: _, + }) => { for ast::DictItem { key, value } in items { if let Some(key) = key { visitor.visit_expr(key); @@ -384,7 +421,11 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { visitor.visit_expr(value); } } - Expr::Set(ast::ExprSet { elts, range: _ }) => { + Expr::Set(ast::ExprSet { + elts, + range: _, + node_index: _, + }) => { for expr in elts { visitor.visit_expr(expr); } @@ -393,6 +434,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { elt, generators, range: _, + node_index: _, }) => { for comprehension in generators { visitor.visit_comprehension(comprehension); @@ -403,6 +445,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { elt, generators, range: _, + node_index: _, }) => { for comprehension in generators { visitor.visit_comprehension(comprehension); @@ -414,6 +457,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { value, generators, range: _, + node_index: _, }) => { for comprehension in generators { visitor.visit_comprehension(comprehension); @@ -425,6 +469,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { elt, generators, range: _, + node_index: _, parenthesized: _, }) => { for comprehension in generators { @@ -432,18 +477,31 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { } visitor.visit_expr(elt); } - Expr::Await(ast::ExprAwait { value, range: _ }) => visitor.visit_expr(value), - Expr::Yield(ast::ExprYield { value, range: _ }) => { + Expr::Await(ast::ExprAwait { + value, + range: _, + node_index: _, + }) => visitor.visit_expr(value), + Expr::Yield(ast::ExprYield { + value, + range: _, + node_index: _, + }) => { if let Some(expr) = value { visitor.visit_expr(expr); } } - Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) => visitor.visit_expr(value), + Expr::YieldFrom(ast::ExprYieldFrom { + value, + range: _, + node_index: _, + }) => visitor.visit_expr(value), Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _, + node_index: _, }) => { visitor.visit_expr(left); for cmp_op in &mut **ops { @@ -457,6 +515,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { func, arguments, range: _, + node_index: _, }) => { visitor.visit_expr(func); visitor.visit_arguments(arguments); @@ -473,6 +532,21 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { } } } + Expr::TString(ast::ExprTString { value, .. }) => { + for t_string_part in value.iter_mut() { + match t_string_part { + ast::TStringPart::Literal(string_literal) => { + visitor.visit_string_literal(string_literal); + } + ast::TStringPart::FString(f_string) => { + visitor.visit_f_string(f_string); + } + ast::TStringPart::TString(t_string) => { + visitor.visit_t_string(t_string); + } + } + } + } Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { for string_literal in value.iter_mut() { visitor.visit_string_literal(string_literal); @@ -496,6 +570,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { slice, ctx, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_expr(slice); @@ -505,6 +580,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { value, ctx, range: _, + node_index: _, }) => { visitor.visit_expr(value); visitor.visit_expr_context(ctx); @@ -516,6 +592,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { elts, ctx, range: _, + node_index: _, }) => { for expr in elts { visitor.visit_expr(expr); @@ -526,6 +603,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { elts, ctx, range: _, + node_index: _, parenthesized: _, }) => { for expr in elts { @@ -538,6 +616,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { upper, step, range: _, + node_index: _, }) => { if let Some(expr) = lower { visitor.visit_expr(expr); @@ -576,7 +655,7 @@ pub fn walk_except_handler( } pub fn walk_arguments(visitor: &V, arguments: &mut Arguments) { - // Note that the there might be keywords before the last arg, e.g. in + // Note that there might be keywords before the last arg, e.g. in // f(*args, a=2, *args2, **kwargs)`, but we follow Python in evaluating first `args` and then // `keywords`. See also [Arguments::arguments_source_order`]. for arg in &mut *arguments.args { @@ -652,6 +731,7 @@ pub fn walk_type_param(visitor: &V, type_param: &mut Ty default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = bound { visitor.visit_expr(expr); @@ -664,6 +744,7 @@ pub fn walk_type_param(visitor: &V, type_param: &mut Ty default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = default { visitor.visit_expr(expr); @@ -673,6 +754,7 @@ pub fn walk_type_param(visitor: &V, type_param: &mut Ty default, name: _, range: _, + node_index: _, }) => { if let Some(expr) = default { visitor.visit_expr(expr); @@ -747,29 +829,35 @@ pub fn walk_pattern_keyword( pub fn walk_f_string(visitor: &V, f_string: &mut FString) { for element in &mut f_string.elements { - visitor.visit_f_string_element(element); + visitor.visit_interpolated_string_element(element); } } -pub fn walk_f_string_element( +pub fn walk_interpolated_string_element( visitor: &V, - f_string_element: &mut FStringElement, + interpolated_string_element: &mut InterpolatedStringElement, ) { - if let ast::FStringElement::Expression(ast::FStringExpressionElement { + if let ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, format_spec, .. - }) = f_string_element + }) = interpolated_string_element { visitor.visit_expr(expression); if let Some(format_spec) = format_spec { for spec_element in &mut format_spec.elements { - visitor.visit_f_string_element(spec_element); + visitor.visit_interpolated_string_element(spec_element); } } } } +pub fn walk_t_string(visitor: &V, t_string: &mut TString) { + for element in &mut t_string.elements { + visitor.visit_interpolated_string_element(element); + } +} + pub fn walk_expr_context(_visitor: &V, _expr_context: &mut ExprContext) {} pub fn walk_bool_op(_visitor: &V, _bool_op: &mut BoolOp) {} diff --git a/crates/ruff_python_ast/src/whitespace.rs b/crates/ruff_python_ast/src/whitespace.rs index 014f18ce56474..78a77bb49b574 100644 --- a/crates/ruff_python_ast/src/whitespace.rs +++ b/crates/ruff_python_ast/src/whitespace.rs @@ -1,4 +1,4 @@ -use ruff_python_trivia::{indentation_at_offset, is_python_whitespace, PythonWhitespace}; +use ruff_python_trivia::{PythonWhitespace, indentation_at_offset, is_python_whitespace}; use ruff_source_file::{LineRanges, UniversalNewlineIterator}; use ruff_text_size::{Ranged, TextRange, TextSize}; diff --git a/crates/ruff_python_ast_integration_tests/tests/comparable.rs b/crates/ruff_python_ast_integration_tests/tests/comparable.rs index b45b226168a2e..8f792f1bddb01 100644 --- a/crates/ruff_python_ast_integration_tests/tests/comparable.rs +++ b/crates/ruff_python_ast_integration_tests/tests/comparable.rs @@ -1,19 +1,36 @@ use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_parser::{parse_expression, ParseError}; +use ruff_python_parser::{ParseError, parse_expression}; + +#[track_caller] +fn assert_comparable(left: &str, right: &str) -> Result<(), ParseError> { + let left_parsed = parse_expression(left)?; + let right_parsed = parse_expression(right)?; + + let left_compr = ComparableExpr::from(left_parsed.expr()); + let right_compr = ComparableExpr::from(right_parsed.expr()); + + assert_eq!(left_compr, right_compr); + Ok(()) +} + +#[track_caller] +fn assert_noncomparable(left: &str, right: &str) -> Result<(), ParseError> { + let left_parsed = parse_expression(left)?; + let right_parsed = parse_expression(right)?; + + let left_compr = ComparableExpr::from(left_parsed.expr()); + let right_compr = ComparableExpr::from(right_parsed.expr()); + + assert_ne!(left_compr, right_compr); + Ok(()) +} #[test] fn concatenated_strings_compare_equal() -> Result<(), ParseError> { let split_contents = r#"'a' 'b' r'\n raw'"#; let value_contents = r#"'ab\\n raw'"#; - let split_parsed = parse_expression(split_contents)?; - let value_parsed = parse_expression(value_contents)?; - - let split_compr = ComparableExpr::from(split_parsed.expr()); - let value_compr = ComparableExpr::from(value_parsed.expr()); - - assert_eq!(split_compr, value_compr); - Ok(()) + assert_comparable(split_contents, value_contents) } #[test] @@ -21,14 +38,7 @@ fn concatenated_bytes_compare_equal() -> Result<(), ParseError> { let split_contents = r#"b'a' b'b'"#; let value_contents = r#"b'ab'"#; - let split_parsed = parse_expression(split_contents)?; - let value_parsed = parse_expression(value_contents)?; - - let split_compr = ComparableExpr::from(split_parsed.expr()); - let value_compr = ComparableExpr::from(value_parsed.expr()); - - assert_eq!(split_compr, value_compr); - Ok(()) + assert_comparable(split_contents, value_contents) } #[test] @@ -36,12 +46,45 @@ fn concatenated_fstrings_compare_equal() -> Result<(), ParseError> { let split_contents = r#"f"{foo!r} this" r"\n raw" f" and {bar!s} that""#; let value_contents = r#"f"{foo!r} this\\n raw and {bar!s} that""#; - let split_parsed = parse_expression(split_contents)?; - let value_parsed = parse_expression(value_contents)?; + assert_comparable(split_contents, value_contents) +} - let split_compr = ComparableExpr::from(split_parsed.expr()); - let value_compr = ComparableExpr::from(value_parsed.expr()); +#[test] +fn concatenated_tstrings_compare_equal() -> Result<(), ParseError> { + let split_contents = r#"t"{foo!r} this" r"\n raw" t" and {bar!s} that""#; + let value_contents = r#"t"{foo!r} this\\n raw and {bar!s} that""#; - assert_eq!(split_compr, value_compr); - Ok(()) + assert_comparable(split_contents, value_contents) +} + +#[test] +fn concatenated_f_and_t_strings_interwoven_compare_equal() -> Result<(), ParseError> { + let split_contents = r#"f"{foo} this " t"{bar}" "baz""#; + let value_contents = r#"f"{foo}" t" this {bar}" "baz""#; + + assert_comparable(split_contents, value_contents) +} + +#[test] +fn concatenated_f_and_t_strings_compare_unequal_when_swapped() -> Result<(), ParseError> { + let f_then_t_contents = r#"f"{foo!r} this" r"\n raw" t" and {bar!s} that""#; + let t_then_f_contents = r#"t"{foo!r} this" r"\n raw" f" and {bar!s} that""#; + + assert_noncomparable(f_then_t_contents, t_then_f_contents) +} + +#[test] +fn t_strings_literal_order_matters_compare_unequal() -> Result<(), ParseError> { + let interp_then_literal_contents = r#"t"{foo}bar""#; + let literal_then_interp_contents = r#"t"bar{foo}""#; + + assert_noncomparable(interp_then_literal_contents, literal_then_interp_contents) +} + +#[test] +fn t_strings_empty_concat_equal() -> Result<(), ParseError> { + let empty_literal = r#""" t"hey{foo}""#; + let empty_f_string = r#"f""t"hey{foo}""#; + + assert_comparable(empty_literal, empty_f_string) } diff --git a/crates/ruff_python_ast_integration_tests/tests/identifier.rs b/crates/ruff_python_ast_integration_tests/tests/identifier.rs index 324390b8454c9..080eb6d059baf 100644 --- a/crates/ruff_python_ast_integration_tests/tests/identifier.rs +++ b/crates/ruff_python_ast_integration_tests/tests/identifier.rs @@ -1,5 +1,5 @@ use ruff_python_ast::identifier; -use ruff_python_parser::{parse_module, ParseError}; +use ruff_python_parser::{ParseError, parse_module}; use ruff_text_size::{TextRange, TextSize}; #[test] diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap index 84266e3745e02..87e9af87a6b99 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap @@ -1,18 +1,17 @@ --- source: crates/ruff_python_ast_integration_tests/tests/source_order.rs expression: trace -snapshot_kind: text --- - ModModule - StmtExpr - ExprFString - StringLiteral - FString - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringLiteralElement + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__match_class_pattern.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__match_class_pattern.snap index ddd8b0c3d1079..6a2e1f07f5f87 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__match_class_pattern.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__match_class_pattern.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_ast_integration_tests/tests/source_order.rs expression: trace -snapshot_kind: text --- - ModModule - StmtMatch @@ -21,12 +20,15 @@ snapshot_kind: text - ExprName - PatternArguments - PatternKeyword + - Identifier - PatternMatchValue - ExprNumberLiteral - PatternKeyword + - Identifier - PatternMatchValue - ExprNumberLiteral - PatternKeyword + - Identifier - PatternMatchValue - ExprNumberLiteral - StmtExpr diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap new file mode 100644 index 0000000000000..75e4f537b2f2a --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_python_ast_integration_tests/tests/source_order.rs +expression: trace +--- +- ModModule + - StmtExpr + - ExprTString + - StringLiteral + - TString + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap index f379b791d7186..4d81357f9ea62 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap @@ -1,17 +1,16 @@ --- source: crates/ruff_python_ast_integration_tests/tests/visitor.rs expression: trace -snapshot_kind: text --- - StmtExpr - ExprFString - StringLiteral - FString - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringLiteralElement + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap new file mode 100644 index 0000000000000..58def387aa8cd --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff_python_ast_integration_tests/tests/visitor.rs +expression: trace +--- +- StmtExpr + - ExprTString + - StringLiteral + - TString + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/source_order.rs b/crates/ruff_python_ast_integration_tests/tests/source_order.rs index a2f7574d7e4a2..ea257eb2794e7 100644 --- a/crates/ruff_python_ast_integration_tests/tests/source_order.rs +++ b/crates/ruff_python_ast_integration_tests/tests/source_order.rs @@ -4,7 +4,7 @@ use insta::assert_snapshot; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::{AnyNodeRef, BoolOp, CmpOp, Operator, Singleton, UnaryOp}; -use ruff_python_parser::{parse, Mode, ParseOptions}; +use ruff_python_parser::{Mode, ParseOptions, parse}; #[test] fn function_arguments() { @@ -146,6 +146,15 @@ fn f_strings() { assert_snapshot!(trace); } +#[test] +fn t_strings() { + let source = r"'pre' t'foo {bar:.{x}f} baz'"; + + let trace = trace_source_order_visitation(source); + + assert_snapshot!(trace); +} + fn trace_source_order_visitation(source: &str) -> String { let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap(); diff --git a/crates/ruff_python_ast_integration_tests/tests/stmt_if.rs b/crates/ruff_python_ast_integration_tests/tests/stmt_if.rs index 240d01187efc8..249e7de738efc 100644 --- a/crates/ruff_python_ast_integration_tests/tests/stmt_if.rs +++ b/crates/ruff_python_ast_integration_tests/tests/stmt_if.rs @@ -1,5 +1,5 @@ use ruff_python_ast::stmt_if::elif_else_range; -use ruff_python_parser::{parse_module, ParseError}; +use ruff_python_parser::{ParseError, parse_module}; use ruff_text_size::TextSize; #[test] diff --git a/crates/ruff_python_ast_integration_tests/tests/visitor.rs b/crates/ruff_python_ast_integration_tests/tests/visitor.rs index 8bfe4851f9279..b05655d9ba477 100644 --- a/crates/ruff_python_ast_integration_tests/tests/visitor.rs +++ b/crates/ruff_python_ast_integration_tests/tests/visitor.rs @@ -3,17 +3,17 @@ use std::fmt::{Debug, Write}; use insta::assert_snapshot; use ruff_python_ast::visitor::{ - walk_alias, walk_bytes_literal, walk_comprehension, walk_except_handler, walk_expr, - walk_f_string, walk_f_string_element, walk_keyword, walk_match_case, walk_parameter, - walk_parameters, walk_pattern, walk_stmt, walk_string_literal, walk_type_param, walk_with_item, - Visitor, + Visitor, walk_alias, walk_bytes_literal, walk_comprehension, walk_except_handler, walk_expr, + walk_f_string, walk_interpolated_string_element, walk_keyword, walk_match_case, walk_parameter, + walk_parameters, walk_pattern, walk_stmt, walk_string_literal, walk_t_string, walk_type_param, + walk_with_item, }; use ruff_python_ast::{ self as ast, Alias, AnyNodeRef, BoolOp, BytesLiteral, CmpOp, Comprehension, ExceptHandler, - Expr, FString, FStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, - Stmt, StringLiteral, TypeParam, UnaryOp, WithItem, + Expr, FString, InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, + Pattern, Stmt, StringLiteral, TString, TypeParam, UnaryOp, WithItem, }; -use ruff_python_parser::{parse, Mode, ParseOptions}; +use ruff_python_parser::{Mode, ParseOptions, parse}; #[test] fn function_arguments() { @@ -155,6 +155,15 @@ fn f_strings() { assert_snapshot!(trace); } +#[test] +fn t_strings() { + let source = r"'pre' t'foo {bar:.{x}f} baz'"; + + let trace = trace_visitation(source); + + assert_snapshot!(trace); +} + fn trace_visitation(source: &str) -> String { let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap(); @@ -169,10 +178,18 @@ where V: Visitor<'a> + ?Sized, { match module { - ast::Mod::Module(ast::ModModule { body, range: _ }) => { + ast::Mod::Module(ast::ModModule { + body, + range: _, + node_index: _, + }) => { visitor.visit_body(body); } - ast::Mod::Expression(ast::ModExpression { body, range: _ }) => visitor.visit_expr(body), + ast::Mod::Expression(ast::ModExpression { + body, + range: _, + node_index: _, + }) => visitor.visit_expr(body), } } @@ -319,9 +336,15 @@ impl Visitor<'_> for RecordVisitor { self.exit_node(); } - fn visit_f_string_element(&mut self, f_string_element: &FStringElement) { + fn visit_interpolated_string_element(&mut self, f_string_element: &InterpolatedStringElement) { self.enter_node(f_string_element); - walk_f_string_element(self, f_string_element); + walk_interpolated_string_element(self, f_string_element); + self.exit_node(); + } + + fn visit_t_string(&mut self, t_string: &TString) { + self.enter_node(t_string); + walk_t_string(self, t_string); self.exit_node(); } } diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 11217c7ff3528..db6bbd3eec932 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -5,9 +5,9 @@ use std::ops::Deref; use ruff_python_ast::{ self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp, - Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, FStringFlags, Identifier, - MatchCase, Operator, Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite, - TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem, + Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator, + Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite, TypeParam, + TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem, }; use ruff_python_ast::{ParameterWithDefault, TypeParams}; use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape}; @@ -264,6 +264,7 @@ impl<'a> Generator<'a> { decorator_list, type_params, range: _, + node_index: _, }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); for decorator in decorator_list { @@ -308,7 +309,11 @@ impl<'a> Generator<'a> { self.newlines(2); } } - Stmt::Return(ast::StmtReturn { value, range: _ }) => { + Stmt::Return(ast::StmtReturn { + value, + range: _, + node_index: _, + }) => { statement!({ if let Some(expr) = value { self.p("return "); @@ -318,7 +323,11 @@ impl<'a> Generator<'a> { } }); } - Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { + Stmt::Delete(ast::StmtDelete { + targets, + range: _, + node_index: _, + }) => { statement!({ self.p("del "); let mut first = true; @@ -342,6 +351,7 @@ impl<'a> Generator<'a> { op, value, range: _, + node_index: _, }) => { statement!({ self.unparse_expr(target, precedence::AUG_ASSIGN); @@ -371,6 +381,7 @@ impl<'a> Generator<'a> { value, simple, range: _, + node_index: _, }) => { statement!({ let need_parens = matches!(target.as_ref(), Expr::Name(_)) && !simple; @@ -416,6 +427,7 @@ impl<'a> Generator<'a> { body, orelse, range: _, + node_index: _, }) => { statement!({ self.p("while "); @@ -435,6 +447,7 @@ impl<'a> Generator<'a> { body, elif_else_clauses, range: _, + node_index: _, }) => { statement!({ self.p("if "); @@ -482,6 +495,7 @@ impl<'a> Generator<'a> { subject, cases, range: _, + node_index: _, }) => { statement!({ self.p("match "); @@ -499,6 +513,7 @@ impl<'a> Generator<'a> { Stmt::TypeAlias(ast::StmtTypeAlias { name, range: _, + node_index: _, type_params, value, }) => { @@ -516,6 +531,7 @@ impl<'a> Generator<'a> { exc, cause, range: _, + node_index: _, }) => { statement!({ self.p("raise"); @@ -536,6 +552,7 @@ impl<'a> Generator<'a> { finalbody, is_star, range: _, + node_index: _, }) => { statement!({ self.p("try:"); @@ -565,6 +582,7 @@ impl<'a> Generator<'a> { test, msg, range: _, + node_index: _, }) => { statement!({ self.p("assert "); @@ -575,7 +593,11 @@ impl<'a> Generator<'a> { } }); } - Stmt::Import(ast::StmtImport { names, range: _ }) => { + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => { statement!({ self.p("import "); let mut first = true; @@ -590,6 +612,7 @@ impl<'a> Generator<'a> { names, level, range: _, + node_index: _, }) => { statement!({ self.p("from "); @@ -609,7 +632,11 @@ impl<'a> Generator<'a> { } }); } - Stmt::Global(ast::StmtGlobal { names, range: _ }) => { + Stmt::Global(ast::StmtGlobal { + names, + range: _, + node_index: _, + }) => { statement!({ self.p("global "); let mut first = true; @@ -619,7 +646,11 @@ impl<'a> Generator<'a> { } }); } - Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { + Stmt::Nonlocal(ast::StmtNonlocal { + names, + range: _, + node_index: _, + }) => { statement!({ self.p("nonlocal "); let mut first = true; @@ -629,7 +660,11 @@ impl<'a> Generator<'a> { } }); } - Stmt::Expr(ast::StmtExpr { value, range: _ }) => { + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => { statement!({ self.unparse_expr(value, precedence::EXPR); }); @@ -664,6 +699,7 @@ impl<'a> Generator<'a> { name, body, range: _, + node_index: _, }) => { self.p("except"); if star { @@ -685,13 +721,25 @@ impl<'a> Generator<'a> { fn unparse_pattern(&mut self, ast: &Pattern) { match ast { - Pattern::MatchValue(ast::PatternMatchValue { value, range: _ }) => { + Pattern::MatchValue(ast::PatternMatchValue { + value, + range: _, + node_index: _, + }) => { self.unparse_expr(value, precedence::MAX); } - Pattern::MatchSingleton(ast::PatternMatchSingleton { value, range: _ }) => { + Pattern::MatchSingleton(ast::PatternMatchSingleton { + value, + range: _, + node_index: _, + }) => { self.unparse_singleton(*value); } - Pattern::MatchSequence(ast::PatternMatchSequence { patterns, range: _ }) => { + Pattern::MatchSequence(ast::PatternMatchSequence { + patterns, + range: _, + node_index: _, + }) => { self.p("["); let mut first = true; for pattern in patterns { @@ -705,6 +753,7 @@ impl<'a> Generator<'a> { patterns, rest, range: _, + node_index: _, }) => { self.p("{"); let mut first = true; @@ -722,7 +771,11 @@ impl<'a> Generator<'a> { self.p("}"); } Pattern::MatchClass(_) => {} - Pattern::MatchStar(ast::PatternMatchStar { name, range: _ }) => { + Pattern::MatchStar(ast::PatternMatchStar { + name, + range: _, + node_index: _, + }) => { self.p("*"); if let Some(name) = name { self.p_id(name); @@ -734,6 +787,7 @@ impl<'a> Generator<'a> { pattern, name, range: _, + node_index: _, }) => { if let Some(pattern) = pattern { self.unparse_pattern(pattern); @@ -745,7 +799,11 @@ impl<'a> Generator<'a> { self.p("_"); } } - Pattern::MatchOr(ast::PatternMatchOr { patterns, range: _ }) => { + Pattern::MatchOr(ast::PatternMatchOr { + patterns, + range: _, + node_index: _, + }) => { let mut first = true; for pattern in patterns { self.p_delim(&mut first, " | "); @@ -841,6 +899,7 @@ impl<'a> Generator<'a> { op, values, range: _, + node_index: _, }) => { let (op, prec) = opprec!(bin, op, BoolOp, And("and", AND), Or("or", OR)); group_if!(prec, { @@ -855,6 +914,7 @@ impl<'a> Generator<'a> { target, value, range: _, + node_index: _, }) => { group_if!(precedence::NAMED_EXPR, { self.unparse_expr(target, precedence::NAMED_EXPR); @@ -867,6 +927,7 @@ impl<'a> Generator<'a> { op, right, range: _, + node_index: _, }) => { let rassoc = matches!(op, Operator::Pow); let (op, prec) = opprec!( @@ -897,6 +958,7 @@ impl<'a> Generator<'a> { op, operand, range: _, + node_index: _, }) => { let (op, prec) = opprec!( un, @@ -916,6 +978,7 @@ impl<'a> Generator<'a> { parameters, body, range: _, + node_index: _, }) => { group_if!(precedence::LAMBDA, { self.p("lambda"); @@ -932,6 +995,7 @@ impl<'a> Generator<'a> { body, orelse, range: _, + node_index: _, }) => { group_if!(precedence::IF_EXP, { self.unparse_expr(body, precedence::IF_EXP + 1); @@ -974,6 +1038,7 @@ impl<'a> Generator<'a> { elt, generators, range: _, + node_index: _, }) => { self.p("["); self.unparse_expr(elt, precedence::COMPREHENSION_ELEMENT); @@ -984,6 +1049,7 @@ impl<'a> Generator<'a> { elt, generators, range: _, + node_index: _, }) => { self.p("{"); self.unparse_expr(elt, precedence::COMPREHENSION_ELEMENT); @@ -995,6 +1061,7 @@ impl<'a> Generator<'a> { value, generators, range: _, + node_index: _, }) => { self.p("{"); self.unparse_expr(key, precedence::COMPREHENSION_ELEMENT); @@ -1008,19 +1075,28 @@ impl<'a> Generator<'a> { generators, parenthesized: _, range: _, + node_index: _, }) => { self.p("("); self.unparse_expr(elt, precedence::COMPREHENSION_ELEMENT); self.unparse_comp(generators); self.p(")"); } - Expr::Await(ast::ExprAwait { value, range: _ }) => { + Expr::Await(ast::ExprAwait { + value, + range: _, + node_index: _, + }) => { group_if!(precedence::AWAIT, { self.p("await "); self.unparse_expr(value, precedence::MAX); }); } - Expr::Yield(ast::ExprYield { value, range: _ }) => { + Expr::Yield(ast::ExprYield { + value, + range: _, + node_index: _, + }) => { group_if!(precedence::YIELD, { self.p("yield"); if let Some(value) = value { @@ -1029,7 +1105,11 @@ impl<'a> Generator<'a> { } }); } - Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) => { + Expr::YieldFrom(ast::ExprYieldFrom { + value, + range: _, + node_index: _, + }) => { group_if!(precedence::YIELD_FROM, { self.p("yield from "); self.unparse_expr(value, precedence::MAX); @@ -1040,6 +1120,7 @@ impl<'a> Generator<'a> { ops, comparators, range: _, + node_index: _, }) => { group_if!(precedence::CMP, { let new_lvl = precedence::CMP + 1; @@ -1066,16 +1147,20 @@ impl<'a> Generator<'a> { func, arguments, range: _, + node_index: _, }) => { self.unparse_expr(func, precedence::MAX); self.p("("); if let ( - [Expr::Generator(ast::ExprGenerator { - elt, - generators, - range: _, - parenthesized: _, - })], + [ + Expr::Generator(ast::ExprGenerator { + elt, + generators, + range: _, + node_index: _, + parenthesized: _, + }), + ], [], ) = (arguments.args.as_ref(), arguments.keywords.as_ref()) { @@ -1110,6 +1195,9 @@ impl<'a> Generator<'a> { Expr::FString(ast::ExprFString { value, .. }) => { self.unparse_f_string_value(value); } + Expr::TString(ast::ExprTString { value, .. }) => { + self.unparse_t_string_value(value); + } Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { self.unparse_string_literal_value(value); } @@ -1212,6 +1300,7 @@ impl<'a> Generator<'a> { upper, step, range: _, + node_index: _, }) => { if let Some(lower) = lower { self.unparse_expr(lower, precedence::SLICE); @@ -1324,24 +1413,29 @@ impl<'a> Generator<'a> { self.unparse_string_literal(string_literal); } ast::FStringPart::FString(f_string) => { - self.unparse_f_string(&f_string.elements, f_string.flags); + self.unparse_interpolated_string(&f_string.elements, f_string.flags.into()); } } } } - fn unparse_f_string_body(&mut self, values: &[ast::FStringElement]) { + fn unparse_interpolated_string_body( + &mut self, + values: &[ast::InterpolatedStringElement], + flags: AnyStringFlags, + ) { for value in values { - self.unparse_f_string_element(value); + self.unparse_interpolated_string_element(value, flags); } } - fn unparse_f_string_expression_element( + fn unparse_interpolated_element( &mut self, val: &Expr, debug_text: Option<&DebugText>, conversion: ConversionFlag, - spec: Option<&ast::FStringFormatSpec>, + spec: Option<&ast::InterpolatedStringFormatSpec>, + flags: AnyStringFlags, ) { let mut generator = Generator::new(self.indent, self.line_ending); generator.unparse_expr(val, precedence::FORMATTED_VALUE); @@ -1365,54 +1459,99 @@ impl<'a> Generator<'a> { if !conversion.is_none() { self.p("!"); - #[allow(clippy::cast_possible_truncation)] + self.p(&format!("{}", conversion as u8 as char)); } if let Some(spec) = spec { self.p(":"); - self.unparse_f_string_specifier(&spec.elements); + self.unparse_f_string_specifier(&spec.elements, flags); } self.p("}"); } - fn unparse_f_string_element(&mut self, element: &ast::FStringElement) { + fn unparse_interpolated_string_element( + &mut self, + element: &ast::InterpolatedStringElement, + flags: AnyStringFlags, + ) { match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => { - self.unparse_f_string_literal_element(value); + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { + value, + .. + }) => { + self.unparse_interpolated_string_literal_element(value, flags); } - ast::FStringElement::Expression(ast::FStringExpressionElement { + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, debug_text, conversion, format_spec, range: _, - }) => self.unparse_f_string_expression_element( + node_index: _, + }) => self.unparse_interpolated_element( expression, debug_text.as_ref(), *conversion, format_spec.as_deref(), + flags, ), } } - fn unparse_f_string_literal_element(&mut self, s: &str) { + fn unparse_interpolated_string_literal_element(&mut self, s: &str, flags: AnyStringFlags) { let s = s.replace('{', "{{").replace('}', "}}"); - self.p(&s); + if flags.prefix().is_raw() { + self.buffer += &s; + return; + } + let escape = UnicodeEscape::with_preferred_quote(&s, flags.quote_style()); + if let Some(len) = escape.layout().len { + self.buffer.reserve(len); + } + escape + .write_body(&mut self.buffer) + .expect("Writing to a String buffer should never fail"); } - fn unparse_f_string_specifier(&mut self, values: &[ast::FStringElement]) { - self.unparse_f_string_body(values); + fn unparse_f_string_specifier( + &mut self, + values: &[ast::InterpolatedStringElement], + flags: AnyStringFlags, + ) { + self.unparse_interpolated_string_body(values, flags); } /// Unparse `values` with [`Generator::unparse_f_string_body`], using `quote` as the preferred /// surrounding quote style. - fn unparse_f_string(&mut self, values: &[ast::FStringElement], flags: FStringFlags) { - let mut generator = Generator::new(self.indent, self.line_ending); - generator.unparse_f_string_body(values); - let body = &generator.buffer; - self.p_str_repr(body, flags); + fn unparse_interpolated_string( + &mut self, + values: &[ast::InterpolatedStringElement], + flags: AnyStringFlags, + ) { + self.p(flags.prefix().as_str()); + self.p(flags.quote_str()); + self.unparse_interpolated_string_body(values, flags); + self.p(flags.quote_str()); + } + + fn unparse_t_string_value(&mut self, value: &ast::TStringValue) { + let mut first = true; + for t_string_part in value { + self.p_delim(&mut first, " "); + match t_string_part { + ast::TStringPart::Literal(string_literal) => { + self.unparse_string_literal(string_literal); + } + ast::TStringPart::FString(f_string) => { + self.unparse_interpolated_string(&f_string.elements, f_string.flags.into()); + } + ast::TStringPart::TString(t_string) => { + self.unparse_interpolated_string(&t_string.elements, t_string.flags.into()); + } + } + } } fn unparse_alias(&mut self, alias: &Alias) { @@ -1435,7 +1574,7 @@ impl<'a> Generator<'a> { #[cfg(test)] mod tests { use ruff_python_ast::{Mod, ModModule}; - use ruff_python_parser::{self, parse_module, Mode, ParseOptions}; + use ruff_python_parser::{self, Mode, ParseOptions, parse_module}; use ruff_source_file::LineEnding; use crate::stylist::Indentation; @@ -1797,6 +1936,20 @@ class Foo: assert_round_trip!(r#"f"{ chr(65) = :#x}""#); assert_round_trip!(r#"f"{ ( chr(65) ) = }""#); assert_round_trip!(r#"f"{a=!r:0.05f}""#); + // https://github.com/astral-sh/ruff/issues/18742 + assert_eq!( + round_trip( + r#" +f"{1= +}" +"# + ), + r#" +f"{1= +}" +"# + .trim() + ); } #[test] diff --git a/crates/ruff_python_codegen/src/lib.rs b/crates/ruff_python_codegen/src/lib.rs index b1525cce3f48c..81a25cef1c13a 100644 --- a/crates/ruff_python_codegen/src/lib.rs +++ b/crates/ruff_python_codegen/src/lib.rs @@ -1,5 +1,5 @@ pub use generator::Generator; -use ruff_python_parser::{parse_module, ParseError}; +use ruff_python_parser::{ParseError, parse_module}; pub use stylist::Stylist; mod generator; diff --git a/crates/ruff_python_codegen/src/stylist.rs b/crates/ruff_python_codegen/src/stylist.rs index 582f1e5ff8b97..d8165941bf471 100644 --- a/crates/ruff_python_codegen/src/stylist.rs +++ b/crates/ruff_python_codegen/src/stylist.rs @@ -5,7 +5,7 @@ use std::ops::Deref; use ruff_python_ast::str::Quote; use ruff_python_parser::{Token, TokenKind, Tokens}; -use ruff_source_file::{find_newline, LineEnding, LineRanges}; +use ruff_source_file::{LineEnding, LineRanges, find_newline}; use ruff_text_size::Ranged; #[derive(Debug, Clone)] @@ -49,7 +49,7 @@ fn detect_quote(tokens: &[Token]) -> Quote { for token in tokens { match token.kind() { TokenKind::String if !token.is_triple_quoted_string() => { - return token.string_quote_style() + return token.string_quote_style(); } TokenKind::FStringStart => return token.string_quote_style(), _ => continue, @@ -148,8 +148,8 @@ impl Deref for Indentation { #[cfg(test)] mod tests { - use ruff_python_parser::{parse_module, parse_unchecked, Mode, ParseOptions}; - use ruff_source_file::{find_newline, LineEnding}; + use ruff_python_parser::{Mode, ParseOptions, parse_module, parse_unchecked}; + use ruff_source_file::{LineEnding, find_newline}; use super::{Indentation, Quote, Stylist}; diff --git a/crates/ruff_python_formatter/CONTRIBUTING.md b/crates/ruff_python_formatter/CONTRIBUTING.md index 3084ec3607dcc..f0e60974a49c1 100644 --- a/crates/ruff_python_formatter/CONTRIBUTING.md +++ b/crates/ruff_python_formatter/CONTRIBUTING.md @@ -204,7 +204,7 @@ impl FormatNodeRule for FormatStmtReturn { fn fmt_fields(&self, item: &StmtReturn, f: &mut PyFormatter) -> FormatResult<()> { // Here we destructure item and make sure each field is listed. // We generally don't need range if it's underscore-ignored - let StmtReturn { range: _, value } = item; + let StmtReturn { range: _, node_index: _, value } = item; // Implement some formatting logic, in this case no space (and no value) after a return with // no value if let Some(value) = value { diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index ba8354c2aab6b..ded6ae5b00718 100755 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -38,9 +38,9 @@ def rustfmt(code: str) -> str: # `FStringLiteralElement`, `FStringFormatSpec` and `FStringExpressionElement` are # handled by the `FString` implementation. if node in ( - "FStringLiteralElement", - "FStringExpressionElement", - "FStringFormatSpec", + "InterpolatedStringLiteralElement", + "InterpolatedElement", + "InterpolatedStringFormatSpec", "Identifier", ): continue diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py b/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py index ba6a1b208c7af..8224f38ea20cd 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py @@ -74,8 +74,7 @@ f'{(abc:=10)}' f"This is a really long string, but just make sure that you reflow fstrings { - 2+2:d -}" + 2+2:d}" f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}" f"{2+2=}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig b/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig index 762b7f0d533d3..fd6eec1af8d06 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/.editorconfig @@ -8,4 +8,8 @@ ij_formatter_enabled = false [docstring_tab_indentation.py] generated_code = true -ij_formatter_enabled = false \ No newline at end of file +ij_formatter_enabled = false + +[f-string-carriage-return-newline.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index 8b27e3cd9f1a4..439d0f1cc1459 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -431,3 +431,16 @@ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) aaaaaaaaaaa = f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) + +# This t-string should be flattened +xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) + +# This is not a multiline t-string, but the expression is too long so it should be +# wrapped in parentheses. +t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +aaaaaaaaaaa = t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py index d3a19d24e8238..fd658cc5ce8ec 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py @@ -242,18 +242,22 @@ }" # comment 19 # comment 20 -# Single-quoted f-strings with a format specificer can be multiline -f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - variable:.3f} ddddddddddddddd eeeeeeee" +# The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. +f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable +:.3f} ddddddddddddddd eeeeeeee" + +# The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable + :.3f} ddddddddddddddd eeeeeeee""" +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f +} ddddddddddddddd eeeeeeee""" -# But, if it's triple-quoted then we can't or the format specificer will have a -# trailing newline -f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - variable:.3f} ddddddddddddddd eeeeeeee""" - -# But, we can break the ones which don't have a format specifier -f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr { - xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb""" +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd + # comment + :.3f} cccccccccc""" # Throw in a random comment in it but surprise, this is not a comment but just a text # which is part of the format specifier @@ -267,27 +271,28 @@ # Conversion flags # -# This is not a valid Python code because of the additional whitespace between the `!` -# and conversion type. But, our parser isn't strict about this. This should probably be -# removed once we have a strict parser. -x = f"aaaaaaaaa { x ! r }" # Even in the case of debug expressions, we only need to preserve the whitespace within # the expression part of the replacement field. -x = f"aaaaaaaaa { x = ! r }" +x = f"aaaaaaaaa { x = !r }" # Combine conversion flags with format specifiers -x = f"{x = ! s - :>0 - - }" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = f"{x !s - :>0 - # comment 21 - }" +x = f"{x = !s + :>0}" + +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + x = f""" { # comment 22 @@ -304,14 +309,28 @@ """ # Mix of various features. -f"{ # comment 26 +f"""{ # comment 26 foo # after foo :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" + + +f"""{foo + :a{ + a # comment 29 + # comment 30 + } +}""" + +# Regression test for https://github.com/astral-sh/ruff/issues/18672 +f"{ + # comment 31 + foo + :>}" # Assignment statement @@ -465,13 +484,11 @@ # This is not a multiline f-string even though it has a newline after the format specifier. aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment aaaaaaaaaaaaaaaaaa = ( f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ) # The newline is only considered when it's a tripled-quoted f-string. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py index ede789e997978..e945211becb3b 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py @@ -100,6 +100,55 @@ f"{10 + len('bar')=}" f'{10 + len("bar")=}' +############################################################################## +# T-strings +############################################################################## + +# Escape `{` and `}` when merging a t-string with a string +"a {not_a_variable}" t"b {10}" "c" + +# Join, and break expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{ +expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +# Join, but don't break the expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +t"test{ +expression +}flat" t"can be { +joined +} together" + +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style +t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes + +# Different triple quoted strings +t"{'''test'''}" t'{"""other"""}' + +# Now with inner quotes +t"{'''test ' '''}" t'{"""other " """}' +t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}' +t"{b'''test ' '''}" t'{b"""other " """}' +t"{t'''test ' '''}" t'{t"""other " """}' + +# debug expressions containing quotes +t"{10 + len('bar')=}" t"{10 + len('bar')=}" +t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}" + +# We can't safely merge this pre Python 3.12 without altering the debug expression. +t"{10 + len('bar')=}" t'{10 + len("bar")=}' + + ############################################################################## # Don't join raw strings ############################################################################## @@ -110,6 +159,9 @@ f"test" fr"test" f"test" fR"test" +t"test" tr"test" +t"test" tR"test" + ############################################################################## # Don't join triple quoted strings @@ -119,9 +171,22 @@ "single" f""""single""" +"single" t""""single""" + b"single" b"""triple""" +############################################################################## +# Don't join t-strings and f-strings +############################################################################## + +t"{interp}" f"{expr}" + +f"{expr}" t"{interp}" + +f"{expr}" "string" t"{interp}" + + ############################################################################## # Join strings in with statements ############################################################################## diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py index 642ad83c27444..c70233ea52fe1 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py @@ -293,6 +293,155 @@ ) +############################################################# +# T-Strings +############################################################# + +# Flatten and join the t-string +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline + +# Parenthesize the value and join it, inline the comment +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +# The target splits because of a magic trailing comma +# The string is joined and not parenthesized because it just fits into the line length (including comment). +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Same but starting with a joined string. They should both result in the same formatting. +[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + +# The target splits because of the magic trailing comma +# The string is **not** joined because it with the inlined comment exceeds the line length limit. +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# The target should be flat +# The string should be joined because it fits into the line length +a[ + aaaaaaa, + b +] = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccc" # comment +) + +# Same but starting with a joined string. They should both result in the same formatting. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccc" # comment + +# The target should be flat +# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Split an overlong target, but join the string if it fits +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment +) + +# Split both if necessary and keep multiline +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment +) + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions: +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + + # Trailing last-part comments a = ( @@ -374,4 +523,4 @@ return ( f"Exception in {call_back_name} when handling msg on " f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe] -) \ No newline at end of file +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json new file mode 100644 index 0000000000000..c485014b9b9bd --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json @@ -0,0 +1 @@ +[{"target_version": "3.14"}] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py new file mode 100644 index 0000000000000..8e1aaf635bf59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py @@ -0,0 +1,731 @@ +( + t'{one}' + t'{two}' +) + + +rt"Not-so-tricky \"quote" + +# Regression test for tstrings dropping comments +result_f = ( + 'Traceback (most recent call last):\n' + t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + # It also varies depending on the platform (stack size) + # Fortunately, we don't care about exactness here, so we use regex + r' \[Previous line repeated (\d+) more times\]' '\n' + 'RecursionError: maximum recursion depth exceeded\n' +) + + +# Regression for tstring dropping comments that were accidentally attached to +# an expression inside a formatted value +( + t'{1}' + # comment 1 + '' +) + +( + t'{1}' # comment 2 + t'{2}' +) + +( + t'{1}' + t'{2}' # comment 3 +) + +( + 1, ( # comment 4 + t'{2}' + ) +) + +( + ( + t'{1}' + # comment 5 + ), + 2 +) + +# https://github.com/astral-sh/ruff/issues/6841 +x = t'''a{""}b''' +y = t'''c{1}d"""e''' +z = t'''a{""}b''' t'''c{1}d"""e''' + +# T-String formatting test cases (Preview) + +# Simple expression with a mix of debug expression and comments. +x = t"{a}" +x = t"{ + a = }" +x = t"{ # comment 6 + a }" +x = t"{ # comment 7 + a = }" + +# Remove the parentheses as adding them doesn't make then fit within the line length limit. +# This is similar to how we format it before t-string formatting. +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc" +) +# Here, we would use the best fit layout to put the t-string indented on the next line +# similar to the next example. +aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +) + +# This should never add the optional parentheses because even after adding them, the +# t-string exceeds the line length limit. +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc" + +# Multiple larger expressions which exceeds the line length limit. Here, we need to decide +# whether to split at the first or second expression. This should work similarly to the +# assignment statement formatting where we split from right to left in preview mode. +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" + +# The above example won't split but when we start introducing line breaks: +x = t"aaaaaaaaaaaa { + bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { + ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd + } eeeeeeeeeeeeee" + +# But, in case comments are present, we would split at the expression containing the +# comments: +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10 + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { # comment 11 + ddddddddddddddd } eeeeeeeeeeeeee" + +# Here, the expression part itself starts with a curly brace so we need to add an extra +# space between the opening curly brace and the expression. +x = t"{ {'x': 1, 'y': 2} }" +# Although the extra space isn't required before the ending curly brace, we add it for +# consistency. +x = t"{ {'x': 1, 'y': 2}}" +x = t"{ {'x': 1, 'y': 2} = }" +x = t"{ # comment 12 + {'x': 1, 'y': 2} }" +x = t"{ # comment 13 + {'x': 1, 'y': 2} = }" +# But, if there's a format specifier or a conversion flag then we don't need to add +# any whitespace at the end +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb" +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" + +# But, in this case, we would split the expression itself because it exceeds the line +# length limit so we need not add the extra space. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} +}" +# And, split the expression itself because it exceeds the line length. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +############################################################################################# +# Quotes +############################################################################################# +t"foo 'bar' {x}" +t"foo \"bar\" {x}" +t'foo "bar" {x}' +t'foo \'bar\' {x}' +t"foo {"bar"}" + +t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes +t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes + +fr"single quotes ' {x}" # Keep double because `'` can't be escaped +fr'double quotes " {x}' # Keep single because `"` can't be escaped +fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes. + +# Here, the formatter will remove the escapes +t"foo {'\'bar\''}" +t"foo {'\"bar\"'}" + +# Quotes inside the expressions have no impact on the quote selection of the outer string. +# Required so that the following two examples result in the same formatting. +t'foo {10 + len("bar")}' +t"foo {10 + len('bar')}" + +# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression +t'foo {10 + len("bar")=}' +t'''foo {10 + len('''bar''')=}''' +t'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes + +# Triple-quoted strings +# It's ok to use the same quote char for the inner string if it's single-quoted. +t"""test {'inner'}""" +t"""test {"inner"}""" +# But if the inner string is also triple-quoted then we should preserve the existing quotes. +t"""test {'''inner'''}""" + +# It's not okay to change the quote style if the inner string is triple quoted and contains a quote. +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + +t"""test {t'inner {'''inner inner'''}'}""" +t"""test {t'''inner {"""inner inner"""}'''}""" + +# Magic trailing comma +# +# The expression formatting will result in breaking it across multiple lines with a +# trailing comma but as the expression isn't already broken, we will remove all the line +# breaks which results in the trailing comma being present. This test case makes sure +# that the trailing comma is removed as well. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# And, if the trailing comma is already present, we still need to remove it. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa" + +# Keep this Multiline by breaking it at the square brackets. +t"""aaaaaa {[ + xxxxxxxx, + yyyyyyyy, +]} ccc""" + +# Add the magic trailing comma because the elements don't fit within the line length limit +# when collapsed. +t"aaaaaa {[ + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + yyyyyyyyyyyy +]} ccccccc" + +# Remove the parentheses because they aren't required +xxxxxxxxxxxxxxx = ( + t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb { + xxxxxxxxxxx # comment 14 + + yyyyyyyyyy + } dddddddddd" +) + +# Comments + +# No comments should be dropped! +t"{ # comment 15 + # comment 16 + foo # comment 17 + # comment 18 +}" # comment 19 +# comment 20 + +# The specifier of a t-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. +t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable + :.3f} ddddddddddddddd eeeeeeee" + +# The same applies for triple quoted t-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable + :.3f} ddddddddddddddd eeeeeeee""" +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable + :.3f +} ddddddddddddddd eeeeeeee""" + + +# Throw in a random comment in it but surprise, this is not a comment but just a text +# which is part of the format specifier +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment +} cccccccccc""" +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment} cccccccccc""" + +# Conversion flags +# + +# Even in the case of debug expressions, we only need to preserve the whitespace within +# the expression part of the replacement field. +x = t"aaaaaaaaa { x = !r }" + +# Combine conversion flags with format specifiers +x = t"{x = !s + :>0}" + +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + +x = t""" +{ # comment 22 + x = :.0{y # comment 23 + }f}""" + +# Here, the debug expression is in a nested t-string so we should start preserving +# whitespaces from that point onwards. This means we should format the outer t-string. +x = t"""{"foo " + # comment 24 + t"{ x = + + }" # comment 25 + } + """ + +# Mix of various features. +t"""{ # comment 26 + foo # after foo + :>{ + x # after x + } + # comment 27 + # comment 28 +} woah {x}""" + +# Assignment statement + +# Even though this t-string has multiline expression, thus allowing us to break it at the +# curly braces, the t-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the t-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The t-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this t-string. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the t-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong +) + +# The following t-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = t"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = t"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the t-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + +# But, the following t-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" +) + +# This is an implicitly concatenated t-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two t-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Similar to the above example but this fits within the line length limit. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single t-string instead. + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# This is not a multiline t-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted t-string. +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment +) + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +) +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# t-strings in other positions + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if ( + t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if ( + t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): + pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert t"aaaaaaaaa{ + expression}bbbbbbbbbbbb", t"cccccccccc{ + expression}dddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{expression}dddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# t-strings as a single argument to a call expression to test whether it's huggable or not. +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call(t"{ + aaaaaa + + '''test + more''' +}") + +# Indentation + +# What should be the indentation? +# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590 +if indent0: + if indent1: + if indent2: + foo = t"""hello world +hello { + t"aaaaaaa { + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } bbbbbbbb" + + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } -------- +""" + + +# Implicit concatenated t-string containing quotes +_ = ( + 'This string should change its quotes to double quotes' + t'This string uses double quotes in an expression {"it's a quote"}' + t'This t-string does not use any quotes.' +) + +# Regression test for https://github.com/astral-sh/ruff/issues/14487 +t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc" + +# Regression test for https://github.com/astral-sh/ruff/issues/14778 +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" + +# Quotes reuse +t"{'a'}" + +# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes +t'foo {10 + len("bar")=}' +t'''foo {10 + len("""bar""")=}''' + +# 312+, it's okay to change the quotes here without creating an invalid t-string +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + + +# Regression tests for https://github.com/astral-sh/ruff/issues/13935 +t'{1: hy "user"}' +t'{1:hy "user"}' +t'{1: abcd "{1}" }' +t'{1: abcd "{'aa'}" }' +t'{1=: "abcd {'aa'}}' +t'{x:a{z:hy "user"}} \'\'\'' + +# Changing the outer quotes is fine because the format-spec is in a nested expression. +t'{t'{z=:hy "user"}'} \'\'\'' + + +# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim. +t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error +t'{1=: abcd \'\'}' # Changing the quotes here is fine because the inner quotes aren't the opposite quotes +t'{1=: abcd \"\"}' # Changing the quotes here is fine because the inner quotes are escaped +# Don't change the quotes in the following cases: +t'{x=:hy "user"} \'\'\'' +t'{x=:a{y:hy "user"}} \'\'\'' +t'{x=:a{y:{z:hy "user"}}} \'\'\'' +t'{x:a{y=:{z:hy "user"}}} \'\'\'' + +# This is fine because the debug expression and format spec are in a nested expression + +t"""{1=: "this" is fine}""" +t'''{1=: "this" is fine}''' # Change quotes to double quotes because they're preferred +t'{1=: {'ab"cd"'}}' # It's okay if the quotes are in an expression part. + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15459 +print(t"{ {1, 2, 3} - {2} }") +print(t"{ {1: 2}.keys() }") +print(t"{({1, 2, 3}) - ({2})}") +print(t"{1, 2, {3} }") +print(t"{(1, 2, {3})}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15535 +print(t"{ {}, }") # A single item tuple gets parenthesized +print(t"{ {}.values(), }") +print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized +print(t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized + {}, 1, +}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15536 +print(t"{ {}, 1, }") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py new file mode 100644 index 0000000000000..1ddc0eee67393 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py @@ -0,0 +1,8 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/18667 +f"{ +1= +}" + +t"{ +1= +}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py index b898fad81bbba..fe1a4a30507f5 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py @@ -288,5 +288,13 @@ ]: pass - +match a, b: + case [], []: + ... + case [], _: + ... + case _, []: + ... + case _, _: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern_match_regression_brackets.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern_match_regression_brackets.py new file mode 100644 index 0000000000000..5f94a64eb294a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern_match_regression_brackets.py @@ -0,0 +1,8 @@ +# Ruff in some cases added brackets around tuples in some cases; deviating from Black. +# Ensure we don't revert already-applied formats with the fix. +# See https://github.com/astral-sh/ruff/pull/18147 +match a, b: + case [[], []]: + ... + case [[], _]: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/decorated_class_after_function.pyi b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/decorated_class_after_function.pyi new file mode 100644 index 0000000000000..ed41c20d24cb8 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/decorated_class_after_function.pyi @@ -0,0 +1,9 @@ +# Issue #18865: Decorated classes below functions should be separated with blank lines +def hello(): ... +@lambda _, /: _ +class A: ... + +def world(): ... + +@final +class B: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py index 3e7002456cf6f..dfc5e61fd1e07 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py @@ -2,6 +2,7 @@ i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break +i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 86cabaea091e2..8d7aeb502b230 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,4 +1,4 @@ -use ruff_formatter::{write, Argument, Arguments}; +use ruff_formatter::{Argument, Arguments, write}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::context::{NodeLevel, WithNodeLevel}; @@ -178,7 +178,6 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { self } - #[allow(unused)] pub(crate) fn entries(&mut self, entries: I) -> &mut Self where T: Ranged, @@ -206,14 +205,14 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { pub(crate) fn finish(&mut self) -> FormatResult<()> { self.result.and_then(|()| { - // Don't add a magic trailing comma when formatting an f-string expression + // Don't add a magic trailing comma when formatting an f-string or t-string expression // that always must be flat because the `expand_parent` forces enclosing // groups to expand, e.g. `print(f"{(a,)} ")` would format the f-string in // flat mode but the `print` call gets expanded because of the `expand_parent`. if self .fmt .context() - .f_string_state() + .interpolated_string_state() .can_contain_line_breaks() == Some(false) { diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index b88d8e20bbef4..60f7cd413c1e1 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -3,16 +3,16 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use clap::{command, Parser, ValueEnum}; +use clap::{Parser, ValueEnum, command}; use ruff_formatter::SourceCode; use ruff_python_ast::PySourceType; -use ruff_python_parser::{parse, ParseOptions}; +use ruff_python_parser::{ParseOptions, parse}; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::comments::collect_comments; -use crate::{format_module_ast, MagicTrailingComma, PreviewMode, PyFormatOptions}; +use crate::{MagicTrailingComma, PreviewMode, PyFormatOptions, format_module_ast}; #[derive(ValueEnum, Clone, Debug)] pub enum Emit { @@ -24,7 +24,7 @@ pub enum Emit { #[derive(Parser)] #[command(author, version, about, long_about = None)] -#[allow(clippy::struct_excessive_bools)] // It's only the dev cli anyways +#[expect(clippy::struct_excessive_bools)] // It's only the dev cli anyways pub struct Cli { /// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported pub files: Vec, diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index a2038669b4282..8cd374464f4a8 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -184,7 +184,7 @@ mod tests { use insta::assert_debug_snapshot; use ruff_formatter::SourceCode; - use ruff_python_ast::AnyNodeRef; + use ruff_python_ast::{AnyNodeRef, AtomicNodeIndex}; use ruff_python_ast::{StmtBreak, StmtContinue}; use ruff_python_trivia::{CommentLinePosition, CommentRanges}; use ruff_text_size::{TextRange, TextSize}; @@ -196,10 +196,12 @@ mod tests { fn debug() { let continue_statement = StmtContinue { range: TextRange::new(TextSize::new(18), TextSize::new(26)), + node_index: AtomicNodeIndex::dummy(), }; let break_statement = StmtBreak { range: TextRange::new(TextSize::new(55), TextSize::new(60)), + node_index: AtomicNodeIndex::dummy(), }; let source = r"# leading comment diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index aa7f31ac278a2..af1868681a56e 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; -use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode}; +use ruff_formatter::{FormatError, FormatOptions, SourceCode, format_args, write}; use ruff_python_ast::{AnyNodeRef, NodeKind, PySourceType}; use ruff_python_trivia::{ - is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before, CommentLinePosition, + CommentLinePosition, is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before, }; use ruff_text_size::{Ranged, TextLen, TextRange}; @@ -381,14 +381,13 @@ impl Format> for FormatTrailingEndOfLineComment<'_> { 0 } else { // Start with 2 because of the two leading spaces. - let width = 2u32.saturating_add( + + 2u32.saturating_add( TextWidth::from_text(&normalized_comment, f.options().indent_width()) .width() .expect("Expected comment not to contain any newlines") .value(), - ); - - width + ) }; write!( diff --git a/crates/ruff_python_formatter/src/comments/map.rs b/crates/ruff_python_formatter/src/comments/map.rs index e7be3d7a5f66f..0d727fa212067 100644 --- a/crates/ruff_python_formatter/src/comments/map.rs +++ b/crates/ruff_python_formatter/src/comments/map.rs @@ -244,7 +244,6 @@ impl MultiMap { } /// Returns `true` if `key` has any *leading*, *dangling*, or *trailing* parts. - #[allow(unused)] pub(super) fn has(&self, key: &K) -> bool { self.index.contains_key(key) } @@ -542,7 +541,7 @@ impl PartIndex { // OK because: // * The `value < u32::MAX` guarantees that the add doesn't overflow. // * The `+ 1` guarantees that the index is not zero - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Self(std::num::NonZeroU32::new((value as u32) + 1).expect("valid value")) } diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 2bfd6b375a11d..af65795e1b2ab 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -514,7 +514,7 @@ mod tests { use ruff_formatter::SourceCode; use ruff_python_ast::{Mod, PySourceType}; - use ruff_python_parser::{parse, ParseOptions, Parsed}; + use ruff_python_parser::{ParseOptions, Parsed, parse}; use ruff_python_trivia::CommentRanges; use crate::comments::Comments; diff --git a/crates/ruff_python_formatter/src/comments/node_key.rs b/crates/ruff_python_formatter/src/comments/node_key.rs index 5c38ac10e5f9f..a0c28ea286606 100644 --- a/crates/ruff_python_formatter/src/comments/node_key.rs +++ b/crates/ruff_python_formatter/src/comments/node_key.rs @@ -53,6 +53,7 @@ impl<'a> From> for NodeRefEqualityKey<'a> { mod tests { use crate::comments::node_key::NodeRefEqualityKey; use ruff_python_ast::AnyNodeRef; + use ruff_python_ast::AtomicNodeIndex; use ruff_python_ast::StmtContinue; use ruff_text_size::TextRange; use std::collections::hash_map::DefaultHasher; @@ -68,6 +69,7 @@ mod tests { fn equality() { let continue_statement = StmtContinue { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }; let ref_a = NodeRefEqualityKey::from_ref(AnyNodeRef::from(&continue_statement)); @@ -81,6 +83,7 @@ mod tests { fn inequality() { let continue_statement = StmtContinue { range: TextRange::default(), + node_index: AtomicNodeIndex::dummy(), }; let boxed = Box::new(continue_statement.clone()); diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index db58d1402c0ee..0ec2aa6d2fbe3 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -4,15 +4,15 @@ use ruff_python_ast::{ self as ast, AnyNodeRef, Comprehension, Expr, ModModule, Parameter, Parameters, StringLike, }; use ruff_python_trivia::{ - find_only_token_in_range, first_non_trivia_token, indentation_at_offset, BackwardsTokenizer, - CommentRanges, SimpleToken, SimpleTokenKind, SimpleTokenizer, + BackwardsTokenizer, CommentRanges, SimpleToken, SimpleTokenKind, SimpleTokenizer, + find_only_token_in_range, first_non_trivia_token, indentation_at_offset, }; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextLen, TextRange}; use std::cmp::Ordering; use crate::comments::visitor::{CommentPlacement, DecoratedComment}; -use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSection}; +use crate::expression::expr_slice::{ExprSliceCommentSection, assign_comment_in_slice}; use crate::expression::parentheses::is_expression_parenthesized; use crate::other::parameters::{ assign_argument_separator_comment_placement, find_parameter_separators, @@ -314,36 +314,33 @@ fn handle_enclosed_comment<'a>( AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from), AnyNodeRef::StmtWith(with_) => handle_with_comment(comment, with_), AnyNodeRef::ExprCall(_) => handle_call_comment(comment), - AnyNodeRef::ExprStringLiteral(_) => { - if let Some(AnyNodeRef::FString(fstring)) = comment.enclosing_parent() { - CommentPlacement::dangling(fstring, comment) - } else { - CommentPlacement::Default(comment) - } - } + AnyNodeRef::ExprStringLiteral(_) => match comment.enclosing_parent() { + Some(AnyNodeRef::FString(fstring)) => CommentPlacement::dangling(fstring, comment), + Some(AnyNodeRef::TString(tstring)) => CommentPlacement::dangling(tstring, comment), + _ => CommentPlacement::Default(comment), + }, AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment), - AnyNodeRef::FStringExpressionElement(_) => { - // Handle comments after the format specifier (should be rare): - // - // ```python - // f"literal { - // expr:.3f - // # comment - // }" - // ``` - // - // This is a valid comment placement. - if matches!( - comment.preceding_node(), - Some( - AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_) - ) - ) { - CommentPlacement::trailing(comment.enclosing_node(), comment) - } else { - handle_bracketed_end_of_line_comment(comment, source) + AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment), + AnyNodeRef::InterpolatedElement(element) => { + if let Some(preceding) = comment.preceding_node() { + // Own line comment before format specifier + // ```py + // aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + // aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd + // # comment + // :.3f} cccccccccc""" + // ``` + if comment.line_position().is_own_line() + && element.format_spec.is_some() + && comment.following_node().is_some() + { + return CommentPlacement::trailing(preceding, comment); + } } + + handle_bracketed_end_of_line_comment(comment, source) } + AnyNodeRef::ExprList(_) | AnyNodeRef::ExprSet(_) | AnyNodeRef::ExprListComp(_) @@ -1066,6 +1063,7 @@ fn handle_slice_comments<'a>( ) -> CommentPlacement<'a> { let ast::ExprSlice { range: _, + node_index: _, lower, upper, step, @@ -1449,6 +1447,7 @@ fn handle_expr_if_comment<'a>( ) -> CommentPlacement<'a> { let ast::ExprIf { range: _, + node_index: _, test, body, orelse, @@ -2256,7 +2255,9 @@ mod tests { ); assert_eq!( - max_empty_lines("# trailing comment\n\n# own line comment\n\n\n# an other own line comment\n# block"), + max_empty_lines( + "# trailing comment\n\n# own line comment\n\n\n# an other own line comment\n# block" + ), 2 ); diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index c1293758019cd..bddd6d84e08b1 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -5,9 +5,9 @@ use ruff_formatter::{Buffer, FormatContext, GroupId, IndentWidth, SourceCode}; use ruff_python_ast::str::Quote; use ruff_python_parser::Tokens; -use crate::comments::Comments; -use crate::other::f_string_element::FStringExpressionElementContext; use crate::PyFormatOptions; +use crate::comments::Comments; +use crate::other::interpolated_string::InterpolatedStringContext; pub struct PyFormatContext<'a> { options: PyFormatOptions, @@ -25,8 +25,8 @@ pub struct PyFormatContext<'a> { /// quote style that is inverted from the one here in order to ensure that /// the formatted Python code will be valid. docstring: Option, - /// The state of the formatter with respect to f-strings. - f_string_state: FStringState, + /// The state of the formatter with respect to f-strings and t-strings. + interpolated_string_state: InterpolatedStringState, } impl<'a> PyFormatContext<'a> { @@ -44,7 +44,7 @@ impl<'a> PyFormatContext<'a> { node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other), indent_level: IndentLevel::new(0), docstring: None, - f_string_state: FStringState::Outside, + interpolated_string_state: InterpolatedStringState::Outside, } } @@ -97,16 +97,18 @@ impl<'a> PyFormatContext<'a> { } } - pub(crate) fn f_string_state(&self) -> FStringState { - self.f_string_state + pub(crate) fn interpolated_string_state(&self) -> InterpolatedStringState { + self.interpolated_string_state } - pub(crate) fn set_f_string_state(&mut self, f_string_state: FStringState) { - self.f_string_state = f_string_state; + pub(crate) fn set_interpolated_string_state( + &mut self, + interpolated_string_state: InterpolatedStringState, + ) { + self.interpolated_string_state = interpolated_string_state; } /// Returns `true` if preview mode is enabled. - #[allow(unused)] pub(crate) const fn is_preview(&self) -> bool { self.options.preview().is_enabled() } @@ -136,24 +138,24 @@ impl Debug for PyFormatContext<'_> { } #[derive(Clone, Copy, Debug, Default)] -pub(crate) enum FStringState { +pub(crate) enum InterpolatedStringState { /// The formatter is inside an f-string expression element i.e., between the /// curly brace in `f"foo {x}"`. /// /// The containing `FStringContext` is the surrounding f-string context. - InsideExpressionElement(FStringExpressionElementContext), + InsideInterpolatedElement(InterpolatedStringContext), /// The formatter is outside an f-string. #[default] Outside, } -impl FStringState { +impl InterpolatedStringState { pub(crate) fn can_contain_line_breaks(self) -> Option { match self { - FStringState::InsideExpressionElement(context) => { - Some(context.can_contain_line_breaks()) + InterpolatedStringState::InsideInterpolatedElement(context) => { + Some(context.is_multiline()) } - FStringState::Outside => None, + InterpolatedStringState::Outside => None, } } } @@ -376,25 +378,25 @@ where } } -pub(crate) struct WithFStringState<'a, B, D> +pub(crate) struct WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, { buffer: D, - saved_location: FStringState, + saved_location: InterpolatedStringState, } -impl<'a, B, D> WithFStringState<'a, B, D> +impl<'a, B, D> WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, { - pub(crate) fn new(expr_location: FStringState, mut buffer: D) -> Self { + pub(crate) fn new(expr_location: InterpolatedStringState, mut buffer: D) -> Self { let context = buffer.state_mut().context_mut(); - let saved_location = context.f_string_state(); + let saved_location = context.interpolated_string_state(); - context.set_f_string_state(expr_location); + context.set_interpolated_string_state(expr_location); Self { buffer, @@ -403,7 +405,7 @@ where } } -impl<'a, B, D> Deref for WithFStringState<'a, B, D> +impl<'a, B, D> Deref for WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, @@ -415,7 +417,7 @@ where } } -impl<'a, B, D> DerefMut for WithFStringState<'a, B, D> +impl<'a, B, D> DerefMut for WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, @@ -425,7 +427,7 @@ where } } -impl<'a, B, D> Drop for WithFStringState<'a, B, D> +impl<'a, B, D> Drop for WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, @@ -434,6 +436,6 @@ where self.buffer .state_mut() .context_mut() - .set_f_string_state(self.saved_location); + .set_interpolated_string_state(self.saved_location); } } diff --git a/crates/ruff_python_formatter/src/db.rs b/crates/ruff_python_formatter/src/db.rs index 3cbb20341063c..cec8833d31cda 100644 --- a/crates/ruff_python_formatter/src/db.rs +++ b/crates/ruff_python_formatter/src/db.rs @@ -1,9 +1,9 @@ -use ruff_db::{files::File, Db as SourceDb, Upcast}; +use ruff_db::{Db as SourceDb, files::File}; use crate::PyFormatOptions; #[salsa::db] -pub trait Db: SourceDb + Upcast { +pub trait Db: SourceDb { /// Returns the formatting options fn format_options(&self, file: File) -> PyFormatOptions; } diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index 429740c0b327a..5d8660845224c 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -11,14 +11,14 @@ use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange}; -use crate::comments::{leading_comments, trailing_comments, Comments, SourceComment}; +use crate::comments::{Comments, SourceComment, leading_comments, trailing_comments}; +use crate::expression::OperatorPrecedence; use crate::expression::parentheses::{ - in_parentheses_only_group, in_parentheses_only_if_group_breaks, + Parentheses, in_parentheses_only_group, in_parentheses_only_if_group_breaks, in_parentheses_only_soft_line_break, in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized, write_in_parentheses_only_group_end_tag, - write_in_parentheses_only_group_start_tag, Parentheses, + write_in_parentheses_only_group_start_tag, }; -use crate::expression::OperatorPrecedence; use crate::prelude::*; use crate::string::implicit::FormatImplicitConcatenatedString; @@ -571,7 +571,7 @@ impl<'a> FlatBinaryExpressionSlice<'a> { "Operand slice must contain at least one operand" ); - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { // SAFETY: `BinaryChainSlice` has the same layout as a slice because it uses `repr(transparent)` &*(std::ptr::from_ref::<[OperandOrOperator<'a>]>(slice) diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 6d3555f7eb996..ebb0093ccb28e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -1,14 +1,14 @@ -use ruff_formatter::{write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{Expr, ExprAttribute, ExprNumberLiteral, Number}; -use ruff_python_trivia::{find_only_token_in_range, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer, find_only_token_in_range}; use ruff_text_size::{Ranged, TextRange}; use crate::comments::dangling_comments; +use crate::expression::CallChainLayout; use crate::expression::parentheses::{ - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, + NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized, }; -use crate::expression::CallChainLayout; use crate::prelude::*; #[derive(Default)] @@ -30,6 +30,7 @@ impl FormatNodeRule for FormatExprAttribute { let ExprAttribute { value, range: _, + node_index: _, attr, ctx: _, } = item; @@ -188,7 +189,12 @@ impl NeedsParentheses for ExprAttribute { // Non Hex, octal or binary number literals need parentheses to disambiguate the attribute `.` from // a decimal point. Floating point numbers don't strictly need parentheses but it reads better (rather than 0.0.test()). fn is_base_ten_number_literal(expr: &Expr, source: &str) -> bool { - if let Some(ExprNumberLiteral { value, range }) = expr.as_number_literal_expr() { + if let Some(ExprNumberLiteral { + value, + range, + node_index: _, + }) = expr.as_number_literal_expr() + { match value { Number::Float(_) => true, Number::Int(_) => { diff --git a/crates/ruff_python_formatter/src/expression/expr_await.rs b/crates/ruff_python_formatter/src/expression/expr_await.rs index a7073312988c7..cd862f2f9afe6 100644 --- a/crates/ruff_python_formatter/src/expression/expr_await.rs +++ b/crates/ruff_python_formatter/src/expression/expr_await.rs @@ -4,7 +4,7 @@ use ruff_python_ast::ExprAwait; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{ - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parenthesize, + NeedsParentheses, OptionalParentheses, Parenthesize, is_expression_parenthesized, }; use crate::prelude::*; @@ -13,7 +13,11 @@ pub struct FormatExprAwait; impl FormatNodeRule for FormatExprAwait { fn fmt_fields(&self, item: &ExprAwait, f: &mut PyFormatter) -> FormatResult<()> { - let ExprAwait { range: _, value } = item; + let ExprAwait { + range: _, + node_index: _, + value, + } = item; write!( f, diff --git a/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs b/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs index a60b46f9ae825..4243af72eaa0e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs @@ -2,11 +2,11 @@ use ruff_python_ast::ExprBytesLiteral; use ruff_python_ast::{AnyNodeRef, StringLike}; use crate::expression::parentheses::{ - in_parentheses_only_group, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, }; use crate::prelude::*; use crate::string::implicit::FormatImplicitConcatenatedStringFlat; -use crate::string::{implicit::FormatImplicitConcatenatedString, StringLikeExtensions}; +use crate::string::{StringLikeExtensions, implicit::FormatImplicitConcatenatedString}; #[derive(Default)] pub struct FormatExprBytesLiteral; diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index e1dc7a5ae61c3..a5c3227a7d3ba 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -3,10 +3,10 @@ use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{Expr, ExprCall}; use crate::comments::dangling_comments; +use crate::expression::CallChainLayout; use crate::expression::parentheses::{ - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, + NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized, }; -use crate::expression::CallChainLayout; use crate::prelude::*; #[derive(Default)] @@ -27,6 +27,7 @@ impl FormatNodeRule for FormatExprCall { fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { let ExprCall { range: _, + node_index: _, func, arguments, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 4e0e3abb5e548..9e5b4f111225b 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -2,9 +2,9 @@ use ruff_formatter::{format_args, write}; use ruff_python_ast::{AnyNodeRef, DictItem, Expr, ExprDict}; use ruff_text_size::{Ranged, TextRange}; -use crate::comments::{dangling_comments, leading_comments, SourceComment}; +use crate::comments::{SourceComment, dangling_comments, leading_comments}; use crate::expression::parentheses::{ - empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, empty_parenthesized, parenthesized, }; use crate::prelude::*; @@ -13,7 +13,11 @@ pub struct FormatExprDict; impl FormatNodeRule for FormatExprDict { fn fmt_fields(&self, item: &ExprDict, f: &mut PyFormatter) -> FormatResult<()> { - let ExprDict { range: _, items } = item; + let ExprDict { + range: _, + node_index: _, + items, + } = item; let comments = f.context().comments().clone(); let dangling = comments.dangling(item); diff --git a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs index 223a982c6d6a6..91eb3183633ab 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs @@ -4,7 +4,7 @@ use ruff_python_ast::ExprDictComp; use ruff_text_size::Ranged; use crate::comments::dangling_comments; -use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, parenthesized}; use crate::prelude::*; #[derive(Default)] @@ -14,6 +14,7 @@ impl FormatNodeRule for FormatExprDictComp { fn fmt_fields(&self, item: &ExprDictComp, f: &mut PyFormatter) -> FormatResult<()> { let ExprDictComp { range: _, + node_index: _, key, value, generators, diff --git a/crates/ruff_python_formatter/src/expression/expr_f_string.rs b/crates/ruff_python_formatter/src/expression/expr_f_string.rs index 045df2cdd897e..ad559e102aced 100644 --- a/crates/ruff_python_formatter/src/expression/expr_f_string.rs +++ b/crates/ruff_python_formatter/src/expression/expr_f_string.rs @@ -1,14 +1,14 @@ use ruff_python_ast::{AnyNodeRef, ExprFString, StringLike}; use crate::expression::parentheses::{ - in_parentheses_only_group, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, }; -use crate::other::f_string::FStringLayout; +use crate::other::interpolated_string::InterpolatedStringLayout; use crate::prelude::*; +use crate::string::StringLikeExtensions; use crate::string::implicit::{ FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat, }; -use crate::string::StringLikeExtensions; #[derive(Default)] pub struct FormatExprFString; @@ -41,7 +41,11 @@ impl NeedsParentheses for ExprFString { if let Some(fstring_part) = self.as_single_part_fstring() { // The f-string is not implicitly concatenated if StringLike::FString(self).is_multiline(context) - || FStringLayout::from_f_string(fstring_part, context.source()).is_multiline() + || InterpolatedStringLayout::from_interpolated_string_elements( + &fstring_part.elements, + context.source(), + ) + .is_multiline() { OptionalParentheses::Never } else { diff --git a/crates/ruff_python_formatter/src/expression/expr_generator.rs b/crates/ruff_python_formatter/src/expression/expr_generator.rs index 566f81d5defc7..db0cd28ef20ca 100644 --- a/crates/ruff_python_formatter/src/expression/expr_generator.rs +++ b/crates/ruff_python_formatter/src/expression/expr_generator.rs @@ -1,8 +1,8 @@ -use ruff_formatter::{format_args, write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, format_args, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprGenerator; -use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, parenthesized}; use crate::prelude::*; #[derive(Eq, PartialEq, Debug, Default)] @@ -37,6 +37,7 @@ impl FormatNodeRule for FormatExprGenerator { fn fmt_fields(&self, item: &ExprGenerator, f: &mut PyFormatter) -> FormatResult<()> { let ExprGenerator { range: _, + node_index: _, elt, generators, parenthesized: is_parenthesized, diff --git a/crates/ruff_python_formatter/src/expression/expr_if.rs b/crates/ruff_python_formatter/src/expression/expr_if.rs index 9d417531fcd04..8dfa92d885115 100644 --- a/crates/ruff_python_formatter/src/expression/expr_if.rs +++ b/crates/ruff_python_formatter/src/expression/expr_if.rs @@ -1,11 +1,11 @@ -use ruff_formatter::{write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{Expr, ExprIf}; use crate::comments::leading_comments; use crate::expression::parentheses::{ - in_parentheses_only_group, in_parentheses_only_soft_line_break_or_space, - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, + in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized, }; use crate::prelude::*; @@ -46,6 +46,7 @@ impl FormatNodeRule for FormatExprIf { fn fmt_fields(&self, item: &ExprIf, f: &mut PyFormatter) -> FormatResult<()> { let ExprIf { range: _, + node_index: _, test, body, orelse, diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index 6d4c684585a71..c5890fba248c0 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -15,6 +15,7 @@ impl FormatNodeRule for FormatExprLambda { fn fmt_fields(&self, item: &ExprLambda, f: &mut PyFormatter) -> FormatResult<()> { let ExprLambda { range: _, + node_index: _, parameters, body, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index a0c16248fa8fe..4e295001d3865 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -4,7 +4,7 @@ use ruff_python_ast::ExprList; use ruff_text_size::Ranged; use crate::expression::parentheses::{ - empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, empty_parenthesized, parenthesized, }; use crate::prelude::*; @@ -15,6 +15,7 @@ impl FormatNodeRule for FormatExprList { fn fmt_fields(&self, item: &ExprList, f: &mut PyFormatter) -> FormatResult<()> { let ExprList { range: _, + node_index: _, elts, ctx: _, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs index 3ae851aefadad..ba36b4b66eb22 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -1,8 +1,8 @@ -use ruff_formatter::{format_args, write, FormatResult}; +use ruff_formatter::{FormatResult, format_args, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprListComp; -use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, parenthesized}; use crate::prelude::*; #[derive(Default)] @@ -12,6 +12,7 @@ impl FormatNodeRule for FormatExprListComp { fn fmt_fields(&self, item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { let ExprListComp { range: _, + node_index: _, elt, generators, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index 5a8b6b2665089..f9c4534722ce5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -14,6 +14,7 @@ impl FormatNodeRule for FormatExprName { id: _, range, ctx: _, + node_index: _, } = item; write!(f, [source_text_slice(*range)]) } diff --git a/crates/ruff_python_formatter/src/expression/expr_named.rs b/crates/ruff_python_formatter/src/expression/expr_named.rs index 6815759efb1ff..117e42825ebb5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named.rs @@ -4,7 +4,7 @@ use ruff_python_ast::ExprNamed; use crate::comments::dangling_comments; use crate::expression::parentheses::{ - in_parentheses_only_soft_line_break_or_space, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, in_parentheses_only_soft_line_break_or_space, }; use crate::prelude::*; @@ -17,6 +17,7 @@ impl FormatNodeRule for FormatExprNamed { target, value, range: _, + node_index: _, } = item; // This context, a dangling comment is a comment between the `:=` and the value. diff --git a/crates/ruff_python_formatter/src/expression/expr_set.rs b/crates/ruff_python_formatter/src/expression/expr_set.rs index b137342e30a3b..45d7f2f170187 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set.rs @@ -2,7 +2,7 @@ use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprSet; use ruff_text_size::Ranged; -use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, parenthesized}; use crate::prelude::*; #[derive(Default)] @@ -10,7 +10,11 @@ pub struct FormatExprSet; impl FormatNodeRule for FormatExprSet { fn fmt_fields(&self, item: &ExprSet, f: &mut PyFormatter) -> FormatResult<()> { - let ExprSet { range: _, elts } = item; + let ExprSet { + range: _, + node_index: _, + elts, + } = item; // That would be a dict expression assert!(!elts.is_empty()); // Avoid second mutable borrow of f diff --git a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs index 18f20dbcee9d9..adbe2d9671f2c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs @@ -1,8 +1,8 @@ -use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use ruff_formatter::{Buffer, FormatResult, format_args, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprSetComp; -use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, parenthesized}; use crate::prelude::*; #[derive(Default)] @@ -12,6 +12,7 @@ impl FormatNodeRule for FormatExprSetComp { fn fmt_fields(&self, item: &ExprSetComp, f: &mut PyFormatter) -> FormatResult<()> { let ExprSetComp { range: _, + node_index: _, elt, generators, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 6f5f844342b7d..4f4b0f001dc41 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -1,10 +1,10 @@ -use ruff_formatter::{write, FormatError}; +use ruff_formatter::{FormatError, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{Expr, ExprSlice, ExprUnaryOp, UnaryOp}; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange}; -use crate::comments::{dangling_comments, SourceComment}; +use crate::comments::{SourceComment, dangling_comments}; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; @@ -21,6 +21,7 @@ impl FormatNodeRule for FormatExprSlice { upper, step, range, + node_index: _, } = item; let (first_colon, second_colon) = find_colons( @@ -232,6 +233,7 @@ pub(crate) fn assign_comment_in_slice( upper, step: _, range, + node_index: _, } = expr_slice; let (first_colon, second_colon) = diff --git a/crates/ruff_python_formatter/src/expression/expr_starred.rs b/crates/ruff_python_formatter/src/expression/expr_starred.rs index 07fa030260572..57dd3c7affb8e 100644 --- a/crates/ruff_python_formatter/src/expression/expr_starred.rs +++ b/crates/ruff_python_formatter/src/expression/expr_starred.rs @@ -14,6 +14,7 @@ impl FormatNodeRule for FormatExprStarred { fn fmt_fields(&self, item: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { let ExprStarred { range: _, + node_index: _, value, ctx: _, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs index c6f0890731ad2..217aeea20f3bc 100644 --- a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs @@ -1,6 +1,6 @@ use crate::builders::parenthesize_if_expands; use crate::expression::parentheses::{ - in_parentheses_only_group, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, }; use crate::other::string_literal::StringLiteralKind; use crate::prelude::*; @@ -8,7 +8,7 @@ use crate::string::implicit::{ FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat, ImplicitConcatenatedLayout, }; -use crate::string::{implicit::FormatImplicitConcatenatedString, StringLikeExtensions}; +use crate::string::{StringLikeExtensions, implicit::FormatImplicitConcatenatedString}; use ruff_formatter::FormatRuleWithOptions; use ruff_python_ast::{AnyNodeRef, ExprStringLiteral, StringLike}; diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 221b43ab1bd7d..ce3aaf1f69c90 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -1,12 +1,12 @@ -use ruff_formatter::{write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{Expr, ExprSubscript}; +use crate::expression::CallChainLayout; use crate::expression::expr_tuple::TupleParentheses; use crate::expression::parentheses::{ - is_expression_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, + NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized, parenthesized, }; -use crate::expression::CallChainLayout; use crate::prelude::*; #[derive(Default)] @@ -27,6 +27,7 @@ impl FormatNodeRule for FormatExprSubscript { fn fmt_fields(&self, item: &ExprSubscript, f: &mut PyFormatter) -> FormatResult<()> { let ExprSubscript { range: _, + node_index: _, value, slice, ctx: _, diff --git a/crates/ruff_python_formatter/src/expression/expr_t_string.rs b/crates/ruff_python_formatter/src/expression/expr_t_string.rs new file mode 100644 index 0000000000000..d937338baf6f4 --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/expr_t_string.rs @@ -0,0 +1,59 @@ +use ruff_python_ast::{AnyNodeRef, ExprTString, StringLike}; + +use crate::expression::parentheses::{ + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, +}; +use crate::other::interpolated_string::InterpolatedStringLayout; +use crate::prelude::*; +use crate::string::StringLikeExtensions; +use crate::string::implicit::{ + FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat, +}; + +#[derive(Default)] +pub struct FormatExprTString; + +impl FormatNodeRule for FormatExprTString { + fn fmt_fields(&self, item: &ExprTString, f: &mut PyFormatter) -> FormatResult<()> { + if let Some(t_string) = item.as_single_part_tstring() { + t_string.format().fmt(f) + } else { + // Always join tstrings that aren't parenthesized and thus, are always on a single line. + if !f.context().node_level().is_parenthesized() { + if let Some(format_flat) = + FormatImplicitConcatenatedStringFlat::new(item.into(), f.context()) + { + return format_flat.fmt(f); + } + } + + in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f) + } + } +} + +impl NeedsParentheses for ExprTString { + fn needs_parentheses( + &self, + _parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { + if let Some(tstring_part) = self.as_single_part_tstring() { + // The t-string is not implicitly concatenated + if StringLike::TString(self).is_multiline(context) + || InterpolatedStringLayout::from_interpolated_string_elements( + &tstring_part.elements, + context.source(), + ) + .is_multiline() + { + OptionalParentheses::Never + } else { + OptionalParentheses::BestFit + } + } else { + // The t-string is implicitly concatenated + OptionalParentheses::Multiline + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index 1caacc10a89b3..6d9504867329d 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,11 +1,11 @@ -use ruff_formatter::{format_args, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, format_args}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprTuple; use ruff_text_size::{Ranged, TextRange}; use crate::builders::parenthesize_if_expands; use crate::expression::parentheses::{ - empty_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, empty_parenthesized, optional_parentheses, parenthesized, }; use crate::other::commas::has_trailing_comma; use crate::prelude::*; @@ -116,6 +116,7 @@ impl FormatNodeRule for FormatExprTuple { elts, ctx: _, range: _, + node_index: _, parenthesized: is_parenthesized, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 56b2ec198b1fd..a8454cda1eb41 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -4,7 +4,7 @@ use ruff_python_ast::UnaryOp; use crate::comments::trailing_comments; use crate::expression::parentheses::{ - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, + NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized, }; use crate::prelude::*; @@ -15,6 +15,7 @@ impl FormatNodeRule for FormatExprUnaryOp { fn fmt_fields(&self, item: &ExprUnaryOp, f: &mut PyFormatter) -> FormatResult<()> { let ExprUnaryOp { range: _, + node_index: _, op, operand, } = item; diff --git a/crates/ruff_python_formatter/src/expression/expr_yield.rs b/crates/ruff_python_formatter/src/expression/expr_yield.rs index 8d8d13c726a26..4b335e208ef12 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield.rs @@ -5,7 +5,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{ - is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parenthesize, + NeedsParentheses, OptionalParentheses, Parenthesize, is_expression_parenthesized, }; use crate::prelude::*; diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index d1a1777362f29..a320a1edf54b9 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -2,21 +2,21 @@ use std::cmp::Ordering; use std::slice; use ruff_formatter::{ - write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, + FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, write, }; use ruff_python_ast::parenthesize::parentheses_iterator; -use ruff_python_ast::visitor::source_order::{walk_expr, SourceOrderVisitor}; +use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, walk_expr}; use ruff_python_ast::{self as ast}; use ruff_python_ast::{AnyNodeRef, Expr, ExprRef, Operator}; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::builders::parenthesize_if_expands; -use crate::comments::{leading_comments, trailing_comments, LeadingDanglingTrailingComments}; +use crate::comments::{LeadingDanglingTrailingComments, leading_comments, trailing_comments}; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{ - is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, - OptionalParentheses, Parentheses, Parenthesize, + NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, is_expression_parenthesized, + optional_parentheses, parenthesized, }; use crate::prelude::*; use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled; @@ -50,6 +50,7 @@ pub(crate) mod expr_slice; pub(crate) mod expr_starred; pub(crate) mod expr_string_literal; pub(crate) mod expr_subscript; +pub(crate) mod expr_t_string; pub(crate) mod expr_tuple; pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; @@ -94,6 +95,7 @@ impl FormatRule> for FormatExpr { Expr::Compare(expr) => expr.format().fmt(f), Expr::Call(expr) => expr.format().fmt(f), Expr::FString(expr) => expr.format().fmt(f), + Expr::TString(expr) => expr.format().fmt(f), Expr::StringLiteral(expr) => expr.format().fmt(f), Expr::BytesLiteral(expr) => expr.format().fmt(f), Expr::NumberLiteral(expr) => expr.format().fmt(f), @@ -282,6 +284,7 @@ fn format_with_parentheses_comments( Expr::Compare(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::Call(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::FString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), + Expr::TString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::StringLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::BytesLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::NumberLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), @@ -391,7 +394,7 @@ impl Format> for MaybeParenthesizeExpression<'_> { .fmt(f) } else { expression.format().with_options(Parentheses::Never).fmt(f) - } + }; } needs_parentheses => needs_parentheses, }; @@ -480,6 +483,7 @@ impl NeedsParentheses for Expr { Expr::Compare(expr) => expr.needs_parentheses(parent, context), Expr::Call(expr) => expr.needs_parentheses(parent, context), Expr::FString(expr) => expr.needs_parentheses(parent, context), + Expr::TString(expr) => expr.needs_parentheses(parent, context), Expr::StringLiteral(expr) => expr.needs_parentheses(parent, context), Expr::BytesLiteral(expr) => expr.needs_parentheses(parent, context), Expr::NumberLiteral(expr) => expr.needs_parentheses(parent, context), @@ -523,7 +527,6 @@ impl<'ast> IntoFormat> for Expr { /// * The expression contains at least one parenthesized sub expression (optimization to avoid unnecessary work) /// /// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) -#[allow(clippy::if_same_then_else)] pub(crate) fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { let mut visitor = CanOmitOptionalParenthesesVisitor::new(context); visitor.visit_subexpression(expr); @@ -679,9 +682,10 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons // because each comparison requires a left operand, and `n` `operands` and right sides. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Expr::BoolOp(ast::ExprBoolOp { range: _, + node_index: _, op: _, values, }) => self.update_max_precedence_with_count( @@ -693,6 +697,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { left: _, right: _, range: _, + node_index: _, }) => self.update_max_precedence(OperatorPrecedence::from(*op)), Expr::If(_) => { @@ -702,9 +707,10 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons // because each comparison requires a left operand, and `n` `operands` and right sides. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Expr::Compare(ast::ExprCompare { range: _, + node_index: _, left: _, ops, comparators: _, @@ -716,6 +722,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { } Expr::Call(ast::ExprCall { range: _, + node_index: _, func, arguments: _, }) => { @@ -737,6 +744,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // `[a, b].test.test[300].dot` Expr::Attribute(ast::ExprAttribute { range: _, + node_index: _, value, attr: _, ctx: _, @@ -757,6 +765,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // Visit the sub-expressions because the sub expressions may be the end of the entire expression. Expr::UnaryOp(ast::ExprUnaryOp { range: _, + node_index: _, op, operand: _, }) => { @@ -776,6 +785,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // Terminal nodes or nodes that wrap a sub-expression (where the sub expression can never be at the end). Expr::FString(_) + | Expr::TString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) @@ -1127,6 +1137,7 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) -> | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::FString(_) + | Expr::TString(_) | Expr::EllipsisLiteral(_) => false, } } @@ -1222,6 +1233,7 @@ pub(crate) fn is_splittable_expression(expr: &Expr, context: &PyFormatContext) - // String like literals can expand if they are implicit concatenated. Expr::FString(fstring) => fstring.value.is_implicit_concatenated(), + Expr::TString(tstring) => tstring.value.is_implicit_concatenated(), Expr::StringLiteral(string) => string.value.is_implicit_concatenated(), Expr::BytesLiteral(bytes) => bytes.value.is_implicit_concatenated(), @@ -1279,6 +1291,7 @@ pub(crate) fn left_most<'expr>( | Expr::Name(_) | Expr::Starred(_) | Expr::FString(_) + | Expr::TString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 8fa13546b3245..a76f8a0aec7c7 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -1,15 +1,15 @@ use ruff_formatter::prelude::tag::Condition; -use ruff_formatter::{format_args, write, Argument, Arguments}; +use ruff_formatter::{Argument, Arguments, format_args, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprRef; use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{ - first_non_trivia_token, BackwardsTokenizer, SimpleToken, SimpleTokenKind, + BackwardsTokenizer, SimpleToken, SimpleTokenKind, first_non_trivia_token, }; use ruff_text_size::Ranged; use crate::comments::{ - dangling_comments, dangling_open_parenthesis_comments, trailing_comments, SourceComment, + SourceComment, dangling_comments, dangling_open_parenthesis_comments, trailing_comments, }; use crate::context::{NodeLevel, WithNodeLevel}; use crate::prelude::*; @@ -255,7 +255,7 @@ impl<'ast> Format> for FormatOptionalParentheses<'_, 'ast> soft_line_break(), if_group_breaks(&token(")")) ]) - .with_group_id(Some(parens_id))] + .with_id(Some(parens_id))] ) } } @@ -422,9 +422,11 @@ impl Format> for FormatEmptyParenthesized<'_> { let end_of_line_split = self .comments .partition_point(|comment| comment.line_position().is_end_of_line()); - debug_assert!(self.comments[end_of_line_split..] - .iter() - .all(|comment| comment.line_position().is_own_line())); + debug_assert!( + self.comments[end_of_line_split..] + .iter() + .all(|comment| comment.line_position().is_own_line()) + ); group(&format_args![ token(self.left), // end-of-line comments diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index 9bb79d80accaf..3abc77a538819 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -1562,6 +1562,42 @@ impl<'ast> IntoFormat> for ast::ExprFString { } } +impl FormatRule> + for crate::expression::expr_t_string::FormatExprTString +{ + #[inline] + fn fmt(&self, node: &ast::ExprTString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::ExprTString { + type Format<'a> = FormatRefWithRule< + 'a, + ast::ExprTString, + crate::expression::expr_t_string::FormatExprTString, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::expression::expr_t_string::FormatExprTString::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::ExprTString { + type Format = FormatOwnedWithRule< + ast::ExprTString, + crate::expression::expr_t_string::FormatExprTString, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::expression::expr_t_string::FormatExprTString::default(), + ) + } +} + impl FormatRule> for crate::expression::expr_string_literal::FormatExprStringLiteral { @@ -2963,6 +2999,34 @@ impl<'ast> IntoFormat> for ast::FString { } } +impl FormatRule> for crate::other::t_string::FormatTString { + #[inline] + fn fmt(&self, node: &ast::TString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::TString { + type Format<'a> = FormatRefWithRule< + 'a, + ast::TString, + crate::other::t_string::FormatTString, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, crate::other::t_string::FormatTString::default()) + } +} +impl<'ast> IntoFormat> for ast::TString { + type Format = FormatOwnedWithRule< + ast::TString, + crate::other::t_string::FormatTString, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, crate::other::t_string::FormatTString::default()) + } +} + impl FormatRule> for crate::other::string_literal::FormatStringLiteral { diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index c991f2520b308..3dbef73807680 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -6,14 +6,14 @@ use tracing::Level; pub use range::format_range; use ruff_formatter::prelude::*; -use ruff_formatter::{format, write, FormatError, Formatted, PrintError, Printed, SourceCode}; +use ruff_formatter::{FormatError, Formatted, PrintError, Printed, SourceCode, format, write}; use ruff_python_ast::{AnyNodeRef, Mod}; -use ruff_python_parser::{parse, ParseError, ParseOptions, Parsed}; +use ruff_python_parser::{ParseError, ParseOptions, Parsed, parse}; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; use crate::comments::{ - has_skip_comment, leading_comments, trailing_comments, Comments, SourceComment, + Comments, SourceComment, has_skip_comment, leading_comments, trailing_comments, }; pub use crate::context::PyFormatContext; pub use crate::db::Db; @@ -77,7 +77,13 @@ where self.fmt_fields(node, f)?; - debug_assert!(node_comments.dangling.iter().all(SourceComment::is_formatted), "The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."); + debug_assert!( + node_comments + .dangling + .iter() + .all(SourceComment::is_formatted), + "The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`." + ); write!( f, @@ -159,16 +165,16 @@ where pub fn formatted_file(db: &dyn Db, file: File) -> Result, FormatModuleError> { let options = db.format_options(file); - let parsed = parsed_module(db.upcast(), file); + let parsed = parsed_module(db, file).load(db); if let Some(first) = parsed.errors().first() { return Err(FormatModuleError::ParseError(first.clone())); } let comment_ranges = CommentRanges::from(parsed.tokens()); - let source = source_text(db.upcast(), file); + let source = source_text(db, file); - let formatted = format_node(parsed, &comment_ranges, &source, options)?; + let formatted = format_node(&parsed, &comment_ranges, &source, options)?; let printed = formatted.print()?; if printed.as_code() == &*source { @@ -194,11 +200,11 @@ mod tests { use insta::assert_snapshot; use ruff_python_ast::PySourceType; - use ruff_python_parser::{parse, ParseOptions}; + use ruff_python_parser::{ParseOptions, parse}; use ruff_python_trivia::CommentRanges; use ruff_text_size::{TextRange, TextSize}; - use crate::{format_module_ast, format_module_source, format_range, PyFormatOptions}; + use crate::{PyFormatOptions, format_module_ast, format_module_source, format_range}; /// Very basic test intentionally kept very similar to the CLI #[test] @@ -226,14 +232,10 @@ if True: #[test] fn quick_test() { let source = r#" -def main() -> None: - if True: - some_very_long_variable_name_abcdefghijk = Foo() - some_very_long_variable_name_abcdefghijk = some_very_long_variable_name_abcdefghijk[ - some_very_long_variable_name_abcdefghijk.some_very_long_attribute_name - == "This is a very long string abcdefghijk" - ] +def hello(): ... +@lambda _, /: _ +class A: ... "#; let source_type = PySourceType::Python; diff --git a/crates/ruff_python_formatter/src/main.rs b/crates/ruff_python_formatter/src/main.rs index 248ee5fa54a7b..09e5ea2ae6740 100644 --- a/crates/ruff_python_formatter/src/main.rs +++ b/crates/ruff_python_formatter/src/main.rs @@ -1,11 +1,11 @@ -use std::io::{stdout, Read, Write}; +use std::io::{Read, Write, stdout}; use std::path::Path; use std::{fs, io}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use clap::Parser as ClapParser; -use ruff_python_formatter::cli::{format_and_debug_print, Cli, Emit}; +use ruff_python_formatter::cli::{Cli, Emit, format_and_debug_print}; /// Read a `String` from `stdin`. pub(crate) fn read_from_stdin() -> Result { @@ -14,7 +14,6 @@ pub(crate) fn read_from_stdin() -> Result { Ok(buffer) } -#[allow(clippy::print_stdout)] fn main() -> Result<()> { let cli: Cli = Cli::parse(); diff --git a/crates/ruff_python_formatter/src/module/mod_expression.rs b/crates/ruff_python_formatter/src/module/mod_expression.rs index 79822a2dbb017..7a0664f4f8cfa 100644 --- a/crates/ruff_python_formatter/src/module/mod_expression.rs +++ b/crates/ruff_python_formatter/src/module/mod_expression.rs @@ -7,7 +7,11 @@ pub struct FormatModExpression; impl FormatNodeRule for FormatModExpression { fn fmt_fields(&self, item: &ModExpression, f: &mut PyFormatter) -> FormatResult<()> { - let ModExpression { body, range: _ } = item; + let ModExpression { + body, + range: _, + node_index: _, + } = item; body.format().fmt(f) } } diff --git a/crates/ruff_python_formatter/src/module/mod_module.rs b/crates/ruff_python_formatter/src/module/mod_module.rs index 6eb3a1d5e846f..f24eb90429159 100644 --- a/crates/ruff_python_formatter/src/module/mod_module.rs +++ b/crates/ruff_python_formatter/src/module/mod_module.rs @@ -2,16 +2,20 @@ use ruff_formatter::write; use ruff_python_ast::ModModule; use ruff_python_trivia::lines_after; +use crate::FormatNodeRule; use crate::prelude::*; use crate::statement::suite::SuiteKind; -use crate::FormatNodeRule; #[derive(Default)] pub struct FormatModModule; impl FormatNodeRule for FormatModModule { fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> { - let ModModule { range, body } = item; + let ModModule { + range, + body, + node_index: _, + } = item; if body.is_empty() { // Only preserve an empty line if the source contains an empty line too. diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index 1357d53590d35..26916d66ea2ae 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -398,7 +398,7 @@ pub enum DocstringCodeLineWidth { #[cfg(feature = "schemars")] mod schema { use ruff_formatter::LineWidth; - use schemars::gen::SchemaGenerator; + use schemars::r#gen::SchemaGenerator; use schemars::schema::{Metadata, Schema, SubschemaValidation}; /// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`. @@ -415,8 +415,8 @@ mod schema { // // The only difference to the automatically derived schema is that we use `oneOf` instead of // `allOf`. There's no semantic difference between `allOf` and `oneOf` for single element lists. - pub(super) fn fixed(gen: &mut SchemaGenerator) -> Schema { - let schema = gen.subschema_for::(); + pub(super) fn fixed(generator: &mut SchemaGenerator) -> Schema { + let schema = generator.subschema_for::(); Schema::Object(schemars::schema::SchemaObject { metadata: Some(Box::new(Metadata { description: Some( @@ -457,7 +457,7 @@ fn deserialize_docstring_code_line_width_dynamic<'de, D>(d: D) -> Result<(), D:: where D: serde::Deserializer<'de>, { - use serde::{de::Error, Deserialize}; + use serde::{Deserialize, de::Error}; let value = String::deserialize(d)?; match &*value { diff --git a/crates/ruff_python_formatter/src/other/alias.rs b/crates/ruff_python_formatter/src/other/alias.rs index f8532d22d4b20..bce6485e12492 100644 --- a/crates/ruff_python_formatter/src/other/alias.rs +++ b/crates/ruff_python_formatter/src/other/alias.rs @@ -11,6 +11,7 @@ impl FormatNodeRule for FormatAlias { fn fmt_fields(&self, item: &Alias, f: &mut PyFormatter) -> FormatResult<()> { let Alias { range: _, + node_index: _, name, asname, } = item; diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 8dc079fce97ff..758deaeeb7d91 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,11 +1,11 @@ -use ruff_formatter::{write, FormatContext}; +use ruff_formatter::{FormatContext, write}; use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, StringFlags, StringLike}; use ruff_python_trivia::{PythonWhitespace, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::expression::expr_generator::GeneratorExpParentheses; use crate::expression::is_expression_huggable; -use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses}; +use crate::expression::parentheses::{Parentheses, empty_parenthesized, parenthesized}; use crate::other::commas; use crate::prelude::*; use crate::string::StringLikeExtensions; @@ -19,6 +19,7 @@ impl FormatNodeRule for FormatArguments { range, args, keywords, + node_index: _, } = item; // We have a case with `f()` without any argument, which is a special case because we can // have a comment with no node attachment inside: diff --git a/crates/ruff_python_formatter/src/other/commas.rs b/crates/ruff_python_formatter/src/other/commas.rs index 50a0fff111130..f96397abcb92f 100644 --- a/crates/ruff_python_formatter/src/other/commas.rs +++ b/crates/ruff_python_formatter/src/other/commas.rs @@ -2,8 +2,8 @@ use ruff_formatter::FormatContext; use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::TextRange; -use crate::prelude::*; use crate::MagicTrailingComma; +use crate::prelude::*; /// Returns `true` if the range ends with a magic trailing comma (and the magic trailing comma /// should be respected). diff --git a/crates/ruff_python_formatter/src/other/comprehension.rs b/crates/ruff_python_formatter/src/other/comprehension.rs index c15c2ca3e693d..cfa75b3e881a5 100644 --- a/crates/ruff_python_formatter/src/other/comprehension.rs +++ b/crates/ruff_python_formatter/src/other/comprehension.rs @@ -1,6 +1,6 @@ -use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use ruff_formatter::{Buffer, FormatResult, format_args, write}; use ruff_python_ast::{Comprehension, Expr}; -use ruff_python_trivia::{find_only_token_in_range, SimpleTokenKind}; +use ruff_python_trivia::{SimpleTokenKind, find_only_token_in_range}; use ruff_text_size::{Ranged, TextRange}; use crate::comments::{leading_comments, trailing_comments}; @@ -52,6 +52,7 @@ impl FormatNodeRule for FormatComprehension { let Comprehension { range: _, + node_index: _, target, iter, ifs, diff --git a/crates/ruff_python_formatter/src/other/decorator.rs b/crates/ruff_python_formatter/src/other/decorator.rs index 38e534d9048bb..0c1719b99166f 100644 --- a/crates/ruff_python_formatter/src/other/decorator.rs +++ b/crates/ruff_python_formatter/src/other/decorator.rs @@ -14,6 +14,7 @@ impl FormatNodeRule for FormatDecorator { let Decorator { expression, range: _, + node_index: _, } = item; write!( diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs index 11fdf640ad9dd..4f2f93a3e934a 100644 --- a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -1,11 +1,11 @@ -use ruff_formatter::write; use ruff_formatter::FormatRuleWithOptions; +use ruff_formatter::write; use ruff_python_ast::ExceptHandlerExceptHandler; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Copy, Clone, Default)] @@ -42,6 +42,7 @@ impl FormatNodeRule for FormatExceptHandlerExceptHan let except_handler_kind = self.except_handler_kind; let ExceptHandlerExceptHandler { range: _, + node_index: _, type_, name, body, diff --git a/crates/ruff_python_formatter/src/other/f_string.rs b/crates/ruff_python_formatter/src/other/f_string.rs index c423f39f34b0d..a02cd4a0867a9 100644 --- a/crates/ruff_python_formatter/src/other/f_string.rs +++ b/crates/ruff_python_formatter/src/other/f_string.rs @@ -1,12 +1,9 @@ -use ruff_formatter::write; -use ruff_python_ast::{AnyStringFlags, FString, StringFlags}; -use ruff_source_file::LineRanges; -use ruff_text_size::Ranged; - +use super::interpolated_string_element::FormatInterpolatedStringElement; +use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout}; use crate::prelude::*; use crate::string::{StringNormalizer, StringQuotes}; - -use super::f_string_element::FormatFStringElement; +use ruff_formatter::write; +use ruff_python_ast::{FString, StringFlags}; /// Formats an f-string which is part of a larger f-string expression. /// @@ -21,9 +18,12 @@ impl FormatNodeRule for FormatFString { let string_kind = normalizer.choose_quotes(item.into()).flags(); - let context = FStringContext::new( + let context = InterpolatedStringContext::new( string_kind, - FStringLayout::from_f_string(item, f.context().source()), + InterpolatedStringLayout::from_interpolated_string_elements( + &item.elements, + f.context().source(), + ), ); // Starting prefix and quote @@ -31,78 +31,10 @@ impl FormatNodeRule for FormatFString { write!(f, [string_kind.prefix(), quotes])?; for element in &item.elements { - FormatFStringElement::new(element, context).fmt(f)?; + FormatInterpolatedStringElement::new(element, context).fmt(f)?; } // Ending quote quotes.fmt(f) } } - -#[derive(Clone, Copy, Debug)] -pub(crate) struct FStringContext { - /// The string flags of the enclosing f-string part. - enclosing_flags: AnyStringFlags, - layout: FStringLayout, -} - -impl FStringContext { - pub(crate) const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self { - Self { - enclosing_flags: flags, - layout, - } - } - - pub(crate) fn flags(self) -> AnyStringFlags { - self.enclosing_flags - } - - pub(crate) const fn layout(self) -> FStringLayout { - self.layout - } -} - -#[derive(Copy, Clone, Debug)] -pub(crate) enum FStringLayout { - /// Original f-string is flat. - /// Don't break expressions to keep the string flat. - Flat, - /// Original f-string has multiline expressions in the replacement fields. - /// Allow breaking expressions across multiple lines. - Multiline, -} - -impl FStringLayout { - pub(crate) fn from_f_string(f_string: &FString, source: &str) -> Self { - // Heuristic: Allow breaking the f-string expressions across multiple lines - // only if there already is at least one multiline expression. This puts the - // control in the hands of the user to decide if they want to break the - // f-string expressions across multiple lines or not. This is similar to - // how Prettier does it for template literals in JavaScript. - // - // If it's single quoted f-string and it contains a multiline expression, then we - // assume that the target version of Python supports it (3.12+). If there are comments - // used in any of the expression of the f-string, then it's always going to be multiline - // and we assume that the target version of Python supports it (3.12+). - // - // Reference: https://prettier.io/docs/en/next/rationale.html#template-literals - if f_string - .elements - .expressions() - .any(|expr| source.contains_line_break(expr.range())) - { - Self::Multiline - } else { - Self::Flat - } - } - - pub(crate) const fn is_flat(self) -> bool { - matches!(self, FStringLayout::Flat) - } - - pub(crate) const fn is_multiline(self) -> bool { - matches!(self, FStringLayout::Multiline) - } -} diff --git a/crates/ruff_python_formatter/src/other/f_string_element.rs b/crates/ruff_python_formatter/src/other/f_string_element.rs deleted file mode 100644 index 36193eaf96865..0000000000000 --- a/crates/ruff_python_formatter/src/other/f_string_element.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std::borrow::Cow; - -use ruff_formatter::{format_args, write, Buffer, RemoveSoftLinesBuffer}; -use ruff_python_ast::{ - AnyStringFlags, ConversionFlag, Expr, FStringElement, FStringExpressionElement, - FStringLiteralElement, StringFlags, -}; -use ruff_text_size::{Ranged, TextSlice}; - -use crate::comments::{dangling_open_parenthesis_comments, trailing_comments}; -use crate::context::{FStringState, NodeLevel, WithFStringState, WithNodeLevel}; -use crate::expression::left_most; -use crate::prelude::*; -use crate::string::normalize_string; -use crate::verbatim::verbatim_text; - -use super::f_string::FStringContext; - -/// Formats an f-string element which is either a literal or a formatted expression. -/// -/// This delegates the actual formatting to the appropriate formatter. -pub(crate) struct FormatFStringElement<'a> { - element: &'a FStringElement, - context: FStringContext, -} - -impl<'a> FormatFStringElement<'a> { - pub(crate) fn new(element: &'a FStringElement, context: FStringContext) -> Self { - Self { element, context } - } -} - -impl Format> for FormatFStringElement<'_> { - fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - match self.element { - FStringElement::Literal(string_literal) => { - FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f) - } - FStringElement::Expression(expression) => { - FormatFStringExpressionElement::new(expression, self.context).fmt(f) - } - } - } -} - -/// Formats an f-string literal element. -pub(crate) struct FormatFStringLiteralElement<'a> { - element: &'a FStringLiteralElement, - /// Flags of the enclosing F-string part - fstring_flags: AnyStringFlags, -} - -impl<'a> FormatFStringLiteralElement<'a> { - pub(crate) fn new(element: &'a FStringLiteralElement, fstring_flags: AnyStringFlags) -> Self { - Self { - element, - fstring_flags, - } - } -} - -impl Format> for FormatFStringLiteralElement<'_> { - fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let literal_content = f.context().source().slice(self.element); - let normalized = normalize_string(literal_content, 0, self.fstring_flags, false); - match &normalized { - Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f), - Cow::Owned(normalized) => text(normalized).fmt(f), - } - } -} - -/// Context representing an f-string expression element. -#[derive(Clone, Copy, Debug)] -pub(crate) struct FStringExpressionElementContext { - /// The context of the parent f-string containing this expression element. - parent_context: FStringContext, - /// Indicates whether this expression element has format specifier or not. - has_format_spec: bool, -} - -impl FStringExpressionElementContext { - /// Returns the [`FStringContext`] containing this expression element. - pub(crate) fn f_string(self) -> FStringContext { - self.parent_context - } - - /// Returns `true` if the expression element can contain line breaks. - pub(crate) fn can_contain_line_breaks(self) -> bool { - self.parent_context.layout().is_multiline() - // For a triple-quoted f-string, the element can't be formatted into multiline if it - // has a format specifier because otherwise the newline would be treated as part of the - // format specifier. - // - // Given the following f-string: - // ```python - // f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee""" - // ``` - // - // We can't format it as: - // ```python - // f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - // variable:.3f - // } ddddddddddddddd eeeeeeee""" - // ``` - // - // Here, the format specifier string would become ".3f\n", which is not what we want. - // But, if the original source code already contained a newline, they'll be preserved. - // - // The Python version is irrelevant in this case. - && !(self.parent_context.flags().is_triple_quoted() && self.has_format_spec) - } -} - -/// Formats an f-string expression element. -pub(crate) struct FormatFStringExpressionElement<'a> { - element: &'a FStringExpressionElement, - context: FStringExpressionElementContext, -} - -impl<'a> FormatFStringExpressionElement<'a> { - pub(crate) fn new(element: &'a FStringExpressionElement, context: FStringContext) -> Self { - Self { - element, - context: FStringExpressionElementContext { - parent_context: context, - has_format_spec: element.format_spec.is_some(), - }, - } - } -} - -impl Format> for FormatFStringExpressionElement<'_> { - fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let FStringExpressionElement { - expression, - debug_text, - conversion, - format_spec, - .. - } = self.element; - - if let Some(debug_text) = debug_text { - token("{").fmt(f)?; - - let comments = f.context().comments(); - - // If the element has a debug text, preserve the same formatting as - // in the source code (`verbatim`). This requires us to mark all of - // the surrounding comments as formatted. - comments.mark_verbatim_node_comments_formatted(self.element.into()); - - // Above method doesn't mark the leading and trailing comments of the element. - // There can't be any leading comments for an expression element, but there - // can be trailing comments. For example, - // - // ```python - // f"""foo { - // x:.3f - // # trailing comment - // }""" - // ``` - for trailing_comment in comments.trailing(self.element) { - trailing_comment.mark_formatted(); - } - - write!( - f, - [ - text(&debug_text.leading), - verbatim_text(&**expression), - text(&debug_text.trailing), - ] - )?; - - // Even if debug text is present, any whitespace between the - // conversion flag and the format spec doesn't need to be preserved. - match conversion { - ConversionFlag::Str => text("!s").fmt(f)?, - ConversionFlag::Ascii => text("!a").fmt(f)?, - ConversionFlag::Repr => text("!r").fmt(f)?, - ConversionFlag::None => (), - } - - if let Some(format_spec) = format_spec.as_deref() { - write!(f, [token(":"), verbatim_text(format_spec)])?; - } - - token("}").fmt(f) - } else { - let comments = f.context().comments().clone(); - let dangling_item_comments = comments.dangling(self.element); - - // If an expression starts with a `{`, we need to add a space before the - // curly brace to avoid turning it into a literal curly with `{{`. - // - // For example, - // ```python - // f"{ {'x': 1, 'y': 2} }" - // # ^ ^ - // ``` - // - // We need to preserve the space highlighted by `^`. The whitespace - // before the closing curly brace is not strictly necessary, but it's - // added to maintain consistency. - let bracket_spacing = - needs_bracket_spacing(expression, f.context()).then_some(format_with(|f| { - if self.context.can_contain_line_breaks() { - soft_line_break_or_space().fmt(f) - } else { - space().fmt(f) - } - })); - - let item = format_with(|f: &mut PyFormatter| { - // Update the context to be inside the f-string expression element. - let f = &mut WithFStringState::new( - FStringState::InsideExpressionElement(self.context), - f, - ); - - write!(f, [bracket_spacing, expression.format()])?; - - // Conversion comes first, then the format spec. - match conversion { - ConversionFlag::Str => text("!s").fmt(f)?, - ConversionFlag::Ascii => text("!a").fmt(f)?, - ConversionFlag::Repr => text("!r").fmt(f)?, - ConversionFlag::None => (), - } - - if let Some(format_spec) = format_spec.as_deref() { - token(":").fmt(f)?; - - for element in &format_spec.elements { - FormatFStringElement::new(element, self.context.f_string()).fmt(f)?; - } - - // These trailing comments can only occur if the format specifier is - // present. For example, - // - // ```python - // f"{ - // x:.3f - // # comment - // }" - // ``` - // - // Any other trailing comments are attached to the expression itself. - trailing_comments(comments.trailing(self.element)).fmt(f)?; - } - - if conversion.is_none() && format_spec.is_none() { - bracket_spacing.fmt(f)?; - } - - Ok(()) - }); - - let open_parenthesis_comments = if dangling_item_comments.is_empty() { - None - } else { - Some(dangling_open_parenthesis_comments(dangling_item_comments)) - }; - - token("{").fmt(f)?; - - { - let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); - - if self.context.can_contain_line_breaks() { - group(&format_args![ - open_parenthesis_comments, - soft_block_indent(&item) - ]) - .fmt(&mut f)?; - } else { - let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); - - write!(buffer, [open_parenthesis_comments, item])?; - } - } - - token("}").fmt(f) - } - } -} - -fn needs_bracket_spacing(expr: &Expr, context: &PyFormatContext) -> bool { - // Ruff parenthesizes single element tuples, that's why we shouldn't insert - // a space around the curly braces for those. - if expr - .as_tuple_expr() - .is_some_and(|tuple| !tuple.parenthesized && tuple.elts.len() == 1) - { - return false; - } - - matches!( - left_most(expr, context.comments().ranges(), context.source()), - Expr::Dict(_) | Expr::DictComp(_) | Expr::Set(_) | Expr::SetComp(_) - ) -} diff --git a/crates/ruff_python_formatter/src/other/interpolated_string.rs b/crates/ruff_python_formatter/src/other/interpolated_string.rs new file mode 100644 index 0000000000000..c146395587500 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/interpolated_string.rs @@ -0,0 +1,73 @@ +use ruff_python_ast::{AnyStringFlags, InterpolatedStringElements}; +use ruff_source_file::LineRanges; +use ruff_text_size::Ranged; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct InterpolatedStringContext { + /// The string flags of the enclosing f/t-string part. + enclosing_flags: AnyStringFlags, + layout: InterpolatedStringLayout, +} + +impl InterpolatedStringContext { + pub(crate) const fn new(flags: AnyStringFlags, layout: InterpolatedStringLayout) -> Self { + Self { + enclosing_flags: flags, + layout, + } + } + + pub(crate) fn flags(self) -> AnyStringFlags { + self.enclosing_flags + } + + pub(crate) const fn is_multiline(self) -> bool { + matches!(self.layout, InterpolatedStringLayout::Multiline) + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) enum InterpolatedStringLayout { + /// Original f/t-string is flat. + /// Don't break expressions to keep the string flat. + Flat, + /// Original f/t-string has multiline expressions in the replacement fields. + /// Allow breaking expressions across multiple lines. + Multiline, +} + +impl InterpolatedStringLayout { + // Heuristic: Allow breaking the f/t-string expressions across multiple lines + // only if there already is at least one multiline expression. This puts the + // control in the hands of the user to decide if they want to break the + // f/t-string expressions across multiple lines or not. This is similar to + // how Prettier does it for template literals in JavaScript. + // + // If it's single quoted f-string and it contains a multiline expression, then we + // assume that the target version of Python supports it (3.12+). If there are comments + // used in any of the expression of the f-string, then it's always going to be multiline + // and we assume that the target version of Python supports it (3.12+). + // + // Reference: https://prettier.io/docs/en/next/rationale.html#template-literals + pub(crate) fn from_interpolated_string_elements( + elements: &InterpolatedStringElements, + source: &str, + ) -> Self { + if elements + .interpolations() + .any(|expr| source.contains_line_break(expr.range())) + { + Self::Multiline + } else { + Self::Flat + } + } + + pub(crate) const fn is_flat(self) -> bool { + matches!(self, InterpolatedStringLayout::Flat) + } + + pub(crate) const fn is_multiline(self) -> bool { + matches!(self, InterpolatedStringLayout::Multiline) + } +} diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs new file mode 100644 index 0000000000000..13526c218f493 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -0,0 +1,294 @@ +use std::borrow::Cow; + +use ruff_formatter::{Buffer, FormatOptions as _, RemoveSoftLinesBuffer, format_args, write}; +use ruff_python_ast::{ + AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement, + InterpolatedStringLiteralElement, +}; +use ruff_text_size::{Ranged, TextSlice}; + +use crate::comments::dangling_open_parenthesis_comments; +use crate::context::{ + InterpolatedStringState, NodeLevel, WithInterpolatedStringState, WithNodeLevel, +}; +use crate::expression::left_most; +use crate::prelude::*; +use crate::string::normalize_string; +use crate::verbatim::verbatim_text; + +use super::interpolated_string::InterpolatedStringContext; + +/// Formats an f-string element which is either a literal or a formatted expression. +/// +/// This delegates the actual formatting to the appropriate formatter. +pub(crate) struct FormatInterpolatedStringElement<'a> { + element: &'a InterpolatedStringElement, + context: InterpolatedStringContext, +} + +impl<'a> FormatInterpolatedStringElement<'a> { + pub(crate) fn new( + element: &'a InterpolatedStringElement, + context: InterpolatedStringContext, + ) -> Self { + Self { element, context } + } +} + +impl Format> for FormatInterpolatedStringElement<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + match self.element { + InterpolatedStringElement::Literal(string_literal) => { + FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f) + } + InterpolatedStringElement::Interpolation(expression) => { + FormatInterpolatedElement::new(expression, self.context).fmt(f) + } + } + } +} + +/// Formats an f-string literal element. +pub(crate) struct FormatFStringLiteralElement<'a> { + element: &'a InterpolatedStringLiteralElement, + /// Flags of the enclosing F-string part + fstring_flags: AnyStringFlags, +} + +impl<'a> FormatFStringLiteralElement<'a> { + pub(crate) fn new( + element: &'a InterpolatedStringLiteralElement, + fstring_flags: AnyStringFlags, + ) -> Self { + Self { + element, + fstring_flags, + } + } +} + +impl Format> for FormatFStringLiteralElement<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let literal_content = f.context().source().slice(self.element); + let normalized = normalize_string(literal_content, 0, self.fstring_flags, false); + match &normalized { + Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f), + Cow::Owned(normalized) => text(normalized).fmt(f), + } + } +} + +/// Formats an f-string expression element. +pub(crate) struct FormatInterpolatedElement<'a> { + element: &'a InterpolatedElement, + context: InterpolatedStringContext, +} + +impl<'a> FormatInterpolatedElement<'a> { + pub(crate) fn new( + element: &'a InterpolatedElement, + context: InterpolatedStringContext, + ) -> Self { + Self { element, context } + } +} + +impl Format> for FormatInterpolatedElement<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let InterpolatedElement { + expression, + debug_text, + conversion, + format_spec, + .. + } = self.element; + + let expression = &**expression; + + if let Some(debug_text) = debug_text { + token("{").fmt(f)?; + + let comments = f.context().comments(); + + // If the element has a debug text, preserve the same formatting as + // in the source code (`verbatim`). This requires us to mark all of + // the surrounding comments as formatted. + comments.mark_verbatim_node_comments_formatted(self.element.into()); + + // Above method doesn't mark the leading and trailing comments of the element. + // There can't be any leading comments for an expression element, but there + // can be trailing comments. For example, + // + // ```python + // f"""foo { + // x:.3f + // # trailing comment + // }""" + // ``` + for trailing_comment in comments.trailing(self.element) { + trailing_comment.mark_formatted(); + } + + write!( + f, + [ + NormalizedDebugText(&debug_text.leading), + verbatim_text(expression), + NormalizedDebugText(&debug_text.trailing), + ] + )?; + + // Even if debug text is present, any whitespace between the + // conversion flag and the format spec doesn't need to be preserved. + match conversion { + ConversionFlag::Str => text("!s").fmt(f)?, + ConversionFlag::Ascii => text("!a").fmt(f)?, + ConversionFlag::Repr => text("!r").fmt(f)?, + ConversionFlag::None => (), + } + + if let Some(format_spec) = format_spec.as_deref() { + write!(f, [token(":"), verbatim_text(format_spec)])?; + } + + token("}").fmt(f) + } else { + let comments = f.context().comments().clone(); + let dangling_item_comments = comments.dangling(self.element); + + let multiline = self.context.is_multiline(); + + // If an expression starts with a `{`, we need to add a space before the + // curly brace to avoid turning it into a literal curly with `{{`. + // + // For example, + // ```python + // f"{ {'x': 1, 'y': 2} }" + // # ^ ^ + // ``` + // + // We need to preserve the space highlighted by `^`. The whitespace + // before the closing curly brace is not strictly necessary, but it's + // added to maintain consistency. + let bracket_spacing = + needs_bracket_spacing(expression, f.context()).then_some(format_with(|f| { + if multiline { + soft_line_break_or_space().fmt(f) + } else { + space().fmt(f) + } + })); + + let item = format_with(|f: &mut PyFormatter| { + // Update the context to be inside the f-string expression element. + let f = &mut WithInterpolatedStringState::new( + InterpolatedStringState::InsideInterpolatedElement(self.context), + f, + ); + + write!(f, [bracket_spacing, expression.format()])?; + + // Conversion comes first, then the format spec. + match conversion { + ConversionFlag::Str => text("!s").fmt(f)?, + ConversionFlag::Ascii => text("!a").fmt(f)?, + ConversionFlag::Repr => text("!r").fmt(f)?, + ConversionFlag::None => (), + } + + if let Some(format_spec) = format_spec.as_deref() { + // ```py + // f"{ + // foo + // # comment 27 + // :test}" + // ``` + if comments.has_trailing(expression) { + soft_line_break().fmt(f)?; + } + + token(":").fmt(f)?; + + for element in &format_spec.elements { + FormatInterpolatedStringElement::new(element, self.context).fmt(f)?; + } + } + + if conversion.is_none() && format_spec.is_none() { + bracket_spacing.fmt(f)?; + } + + Ok(()) + }); + + let open_parenthesis_comments = if dangling_item_comments.is_empty() { + None + } else { + Some(dangling_open_parenthesis_comments(dangling_item_comments)) + }; + + token("{").fmt(f)?; + + { + let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); + + if self.context.is_multiline() { + if format_spec.is_none() { + group(&format_args![ + open_parenthesis_comments, + soft_block_indent(&item) + ]) + .fmt(&mut f)?; + } else { + // For strings ending with a format spec, don't add a newline between the end of the format spec + // and closing curly brace because that is invalid syntax for single quoted strings and + // the newline is preserved as part of the format spec for triple quoted strings. + + group(&format_args![ + open_parenthesis_comments, + indent(&format_args![soft_line_break(), item]) + ]) + .fmt(&mut f)?; + } + } else { + let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); + + write!(buffer, [open_parenthesis_comments, item])?; + } + } + + token("}").fmt(f) + } + } +} + +fn needs_bracket_spacing(expr: &Expr, context: &PyFormatContext) -> bool { + // Ruff parenthesizes single element tuples, that's why we shouldn't insert + // a space around the curly braces for those. + if expr + .as_tuple_expr() + .is_some_and(|tuple| !tuple.parenthesized && tuple.elts.len() == 1) + { + return false; + } + + matches!( + left_most(expr, context.comments().ranges(), context.source()), + Expr::Dict(_) | Expr::DictComp(_) | Expr::Set(_) | Expr::SetComp(_) + ) +} + +struct NormalizedDebugText<'a>(&'a str); + +impl Format> for NormalizedDebugText<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let normalized = normalize_newlines(self.0, ['\r']); + + f.write_element(FormatElement::Text { + text_width: TextWidth::from_text(&normalized, f.options().indent_width()), + text: normalized.into_owned().into_boxed_str(), + }); + + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/src/other/keyword.rs b/crates/ruff_python_formatter/src/other/keyword.rs index 6c568832423fb..df00ed94ccb58 100644 --- a/crates/ruff_python_formatter/src/other/keyword.rs +++ b/crates/ruff_python_formatter/src/other/keyword.rs @@ -10,6 +10,7 @@ impl FormatNodeRule for FormatKeyword { fn fmt_fields(&self, item: &Keyword, f: &mut PyFormatter) -> FormatResult<()> { let Keyword { range: _, + node_index: _, arg, value, } = item; diff --git a/crates/ruff_python_formatter/src/other/match_case.rs b/crates/ruff_python_formatter/src/other/match_case.rs index 5964af3244e79..130efd01cdc53 100644 --- a/crates/ruff_python_formatter/src/other/match_case.rs +++ b/crates/ruff_python_formatter/src/other/match_case.rs @@ -1,11 +1,11 @@ -use ruff_formatter::{format_args, write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, format_args, write}; use ruff_python_ast::MatchCase; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::pattern::maybe_parenthesize_pattern; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -26,6 +26,7 @@ impl FormatNodeRule for FormatMatchCase { fn fmt_fields(&self, item: &MatchCase, f: &mut PyFormatter) -> FormatResult<()> { let MatchCase { range: _, + node_index: _, pattern, guard, body, diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index b55b1a70f6e59..33f14c4f7bd41 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -7,12 +7,14 @@ pub(crate) mod decorator; pub(crate) mod elif_else_clause; pub(crate) mod except_handler_except_handler; pub(crate) mod f_string; -pub(crate) mod f_string_element; pub(crate) mod identifier; +pub(crate) mod interpolated_string; +pub(crate) mod interpolated_string_element; pub(crate) mod keyword; pub(crate) mod match_case; pub(crate) mod parameter; pub(crate) mod parameter_with_default; pub(crate) mod parameters; pub(crate) mod string_literal; +pub(crate) mod t_string; pub(crate) mod with_item; diff --git a/crates/ruff_python_formatter/src/other/parameter.rs b/crates/ruff_python_formatter/src/other/parameter.rs index 8a634928fcf6d..483280082eb6c 100644 --- a/crates/ruff_python_formatter/src/other/parameter.rs +++ b/crates/ruff_python_formatter/src/other/parameter.rs @@ -9,6 +9,7 @@ impl FormatNodeRule for FormatParameter { fn fmt_fields(&self, item: &Parameter, f: &mut PyFormatter) -> FormatResult<()> { let Parameter { range: _, + node_index: _, name, annotation, } = item; diff --git a/crates/ruff_python_formatter/src/other/parameter_with_default.rs b/crates/ruff_python_formatter/src/other/parameter_with_default.rs index a4d09671dfb31..a78c082a00dd2 100644 --- a/crates/ruff_python_formatter/src/other/parameter_with_default.rs +++ b/crates/ruff_python_formatter/src/other/parameter_with_default.rs @@ -12,6 +12,7 @@ impl FormatNodeRule for FormatParameterWithDefault { fn fmt_fields(&self, item: &ParameterWithDefault, f: &mut PyFormatter) -> FormatResult<()> { let ParameterWithDefault { range: _, + node_index: _, parameter, default, } = item; @@ -38,20 +39,26 @@ impl FormatNodeRule for FormatParameterWithDefault { // ``` let needs_line_break_trailing = f.context().comments().has_trailing(parameter); let default_first_comment = f.context().comments().leading(default.as_ref()).first(); - let needs_line_break_leading = default_first_comment.is_some_and(|default_leading_comment| { - let mut tokenizer = SimpleTokenizer::new( - f.context().source(), - TextRange::new(parameter.end(), default_leading_comment.start()), - ) - .skip_trivia() - .skip_while(|token| token.kind == SimpleTokenKind::RParen); - let equals = tokenizer.next(); - debug_assert!(equals.is_some_and(|token| token.kind == SimpleTokenKind::Equals)); - let lparens = tokenizer.next(); - debug_assert!(lparens - .as_ref().is_none_or(|token| token.kind == SimpleTokenKind::LParen)); - lparens.is_none() - }); + let needs_line_break_leading = + default_first_comment.is_some_and(|default_leading_comment| { + let mut tokenizer = SimpleTokenizer::new( + f.context().source(), + TextRange::new(parameter.end(), default_leading_comment.start()), + ) + .skip_trivia() + .skip_while(|token| token.kind == SimpleTokenKind::RParen); + let equals = tokenizer.next(); + debug_assert!( + equals.is_some_and(|token| token.kind == SimpleTokenKind::Equals) + ); + let lparens = tokenizer.next(); + debug_assert!( + lparens + .as_ref() + .is_none_or(|token| token.kind == SimpleTokenKind::LParen) + ); + lparens.is_none() + }); let needs_line_break = needs_line_break_trailing || needs_line_break_leading; write!( diff --git a/crates/ruff_python_formatter/src/other/parameters.rs b/crates/ruff_python_formatter/src/other/parameters.rs index 617233a467f04..1c6682bab15cf 100644 --- a/crates/ruff_python_formatter/src/other/parameters.rs +++ b/crates/ruff_python_formatter/src/other/parameters.rs @@ -1,11 +1,11 @@ -use ruff_formatter::{format_args, write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, format_args, write}; use ruff_python_ast::{AnyNodeRef, Parameters}; use ruff_python_trivia::{CommentLinePosition, SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::comments::{ - dangling_comments, dangling_open_parenthesis_comments, leading_comments, leading_node_comments, - trailing_comments, SourceComment, + SourceComment, dangling_comments, dangling_open_parenthesis_comments, leading_comments, + leading_node_comments, trailing_comments, }; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::empty_parenthesized; @@ -49,6 +49,7 @@ impl FormatNodeRule for FormatParameters { fn fmt_fields(&self, item: &Parameters, f: &mut PyFormatter) -> FormatResult<()> { let Parameters { range: _, + node_index: _, posonlyargs, args, vararg, @@ -670,10 +671,28 @@ fn has_trailing_comma( // The slash lacks its own node if ends_with_pos_only_argument_separator { let comma = tokens.next(); - assert!(matches!(comma, Some(SimpleToken { kind: SimpleTokenKind::Comma, .. })), "The last positional only argument must be separated by a `,` from the positional only parameters separator `/` but found '{comma:?}'."); + assert!( + matches!( + comma, + Some(SimpleToken { + kind: SimpleTokenKind::Comma, + .. + }) + ), + "The last positional only argument must be separated by a `,` from the positional only parameters separator `/` but found '{comma:?}'." + ); let slash = tokens.next(); - assert!(matches!(slash, Some(SimpleToken { kind: SimpleTokenKind::Slash, .. })), "The positional argument separator must be present for a function that has positional only parameters but found '{slash:?}'."); + assert!( + matches!( + slash, + Some(SimpleToken { + kind: SimpleTokenKind::Slash, + .. + }) + ), + "The positional argument separator must be present for a function that has positional only parameters but found '{slash:?}'." + ); } tokens diff --git a/crates/ruff_python_formatter/src/other/string_literal.rs b/crates/ruff_python_formatter/src/other/string_literal.rs index b1fbe18df33ad..0d5510417403a 100644 --- a/crates/ruff_python_formatter/src/other/string_literal.rs +++ b/crates/ruff_python_formatter/src/other/string_literal.rs @@ -1,9 +1,9 @@ use ruff_formatter::FormatRuleWithOptions; use ruff_python_ast::StringLiteral; -use crate::prelude::*; -use crate::string::{docstring, StringNormalizer}; use crate::QuoteStyle; +use crate::prelude::*; +use crate::string::{StringNormalizer, docstring}; #[derive(Default)] pub struct FormatStringLiteral { diff --git a/crates/ruff_python_formatter/src/other/t_string.rs b/crates/ruff_python_formatter/src/other/t_string.rs new file mode 100644 index 0000000000000..098c668c51ad8 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/t_string.rs @@ -0,0 +1,40 @@ +use super::interpolated_string_element::FormatInterpolatedStringElement; +use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout}; +use crate::prelude::*; +use crate::string::{StringNormalizer, StringQuotes}; +use ruff_formatter::write; +use ruff_python_ast::{StringFlags, TString}; + +/// Formats a t-string which is part of a larger t-string expression. +/// +/// For example, this would be used to format the t-string part in `"foo" t"bar {x}"` +/// or the standalone t-string in `t"foo {x} bar"`. +#[derive(Default)] +pub struct FormatTString; + +impl FormatNodeRule for FormatTString { + fn fmt_fields(&self, item: &TString, f: &mut PyFormatter) -> FormatResult<()> { + let normalizer = StringNormalizer::from_context(f.context()); + + let string_kind = normalizer.choose_quotes(item.into()).flags(); + + let context = InterpolatedStringContext::new( + string_kind, + InterpolatedStringLayout::from_interpolated_string_elements( + &item.elements, + f.context().source(), + ), + ); + + // Starting prefix and quote + let quotes = StringQuotes::from(string_kind); + write!(f, [string_kind.prefix(), quotes])?; + + for element in &item.elements { + FormatInterpolatedStringElement::new(element, context).fmt(f)?; + } + + // Ending quote + quotes.fmt(f) + } +} diff --git a/crates/ruff_python_formatter/src/other/with_item.rs b/crates/ruff_python_formatter/src/other/with_item.rs index c81fb0fa77ceb..a309edd6e1641 100644 --- a/crates/ruff_python_formatter/src/other/with_item.rs +++ b/crates/ruff_python_formatter/src/other/with_item.rs @@ -1,9 +1,9 @@ -use ruff_formatter::{write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, write}; use ruff_python_ast::WithItem; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{ - is_expression_parenthesized, parenthesized, Parentheses, Parenthesize, + Parentheses, Parenthesize, is_expression_parenthesized, parenthesized, }; use crate::prelude::*; @@ -94,6 +94,7 @@ impl FormatNodeRule for FormatWithItem { fn fmt_fields(&self, item: &WithItem, f: &mut PyFormatter) -> FormatResult<()> { let WithItem { range: _, + node_index: _, context_expr, optional_vars, } = item; diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index a670a3e3c2c58..e255d59359552 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{AnyNodeRef, Expr}; use ruff_python_ast::{MatchCase, Pattern}; use ruff_python_trivia::CommentRanges; use ruff_python_trivia::{ - first_non_trivia_token, BackwardsTokenizer, SimpleToken, SimpleTokenKind, + BackwardsTokenizer, SimpleToken, SimpleTokenKind, first_non_trivia_token, }; use ruff_text_size::Ranged; use std::cmp::Ordering; @@ -11,7 +11,7 @@ use std::cmp::Ordering; use crate::builders::parenthesize_if_expands; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{ - optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, + NeedsParentheses, OptionalParentheses, Parentheses, optional_parentheses, parenthesized, }; use crate::prelude::*; @@ -293,6 +293,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> { // F-strings are allowed according to python's grammar but fail with a syntax error at runtime. // That's why we need to support them for formatting. Expr::FString(_) | + Expr::TString(_)| Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => { // require no state update other than visit_pattern does. } @@ -306,7 +307,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> { _ => { debug_assert!( false, - "Unsupported expression in pattern mach value: {:?}", + "Unsupported expression in pattern match value: {:?}", value.value ); } diff --git a/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs b/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs index 6b3a82c7223c3..94d7448226e15 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_arguments.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{Pattern, PatternArguments}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses}; +use crate::expression::parentheses::{Parentheses, empty_parenthesized, parenthesized}; use crate::prelude::*; #[derive(Default)] diff --git a/crates/ruff_python_formatter/src/pattern/pattern_keyword.rs b/crates/ruff_python_formatter/src/pattern/pattern_keyword.rs index 974e6534743cd..3f3aa6efa9c50 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_keyword.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_keyword.rs @@ -9,6 +9,7 @@ impl FormatNodeRule for FormatPatternKeyword { fn fmt_fields(&self, item: &PatternKeyword, f: &mut PyFormatter) -> FormatResult<()> { let PatternKeyword { range: _, + node_index: _, attr, pattern, } = item; diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs index 68958cf02dbd7..f313bf0309afc 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_as.rs @@ -13,6 +13,7 @@ impl FormatNodeRule for FormatPatternMatchAs { fn fmt_fields(&self, item: &PatternMatchAs, f: &mut PyFormatter) -> FormatResult<()> { let PatternMatchAs { range: _, + node_index: _, pattern, name, } = item; diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs index d516940302435..ce1583049406b 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_class.rs @@ -13,6 +13,7 @@ impl FormatNodeRule for FormatPatternMatchClass { fn fmt_fields(&self, item: &PatternMatchClass, f: &mut PyFormatter) -> FormatResult<()> { let PatternMatchClass { range: _, + node_index: _, cls, arguments, } = item; diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_mapping.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_mapping.rs index e9943ab72bfcd..cb5568f707035 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_mapping.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_mapping.rs @@ -5,9 +5,9 @@ use ruff_python_ast::{Expr, Identifier, Pattern}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange}; -use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::comments::{SourceComment, leading_comments, trailing_comments}; use crate::expression::parentheses::{ - empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, empty_parenthesized, parenthesized, }; use crate::prelude::*; @@ -21,6 +21,7 @@ impl FormatNodeRule for FormatPatternMatchMapping { patterns, rest, range: _, + node_index: _, } = item; debug_assert_eq!(keys.len(), patterns.len()); @@ -163,6 +164,7 @@ fn find_double_star(pattern: &PatternMatchMapping, source: &str) -> Option<(Text patterns, rest, range: _, + node_index: _, } = pattern; // If there's no `rest` element, there's no `**`. diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs index c3119a1ca52c9..b0b21f49e902a 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_or.rs @@ -4,8 +4,8 @@ use ruff_python_ast::PatternMatchOr; use crate::comments::leading_comments; use crate::expression::parentheses::{ - in_parentheses_only_group, in_parentheses_only_soft_line_break_or_space, NeedsParentheses, - OptionalParentheses, + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, + in_parentheses_only_soft_line_break_or_space, }; use crate::prelude::*; @@ -14,7 +14,11 @@ pub struct FormatPatternMatchOr; impl FormatNodeRule for FormatPatternMatchOr { fn fmt_fields(&self, item: &PatternMatchOr, f: &mut PyFormatter) -> FormatResult<()> { - let PatternMatchOr { range: _, patterns } = item; + let PatternMatchOr { + range: _, + node_index: _, + patterns, + } = item; let inner = format_with(|f: &mut PyFormatter| { let mut patterns = patterns.iter(); let comments = f.context().comments().clone(); diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_sequence.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_sequence.rs index c3f4d0e65bf3c..8753760ec6912 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_sequence.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_sequence.rs @@ -1,11 +1,11 @@ -use ruff_formatter::{format_args, Format, FormatResult}; +use ruff_formatter::{Format, FormatResult, format_args}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::PatternMatchSequence; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange}; use crate::expression::parentheses::{ - empty_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, OptionalParentheses, + NeedsParentheses, OptionalParentheses, empty_parenthesized, optional_parentheses, parenthesized, }; use crate::prelude::*; @@ -14,7 +14,11 @@ pub struct FormatPatternMatchSequence; impl FormatNodeRule for FormatPatternMatchSequence { fn fmt_fields(&self, item: &PatternMatchSequence, f: &mut PyFormatter) -> FormatResult<()> { - let PatternMatchSequence { patterns, range } = item; + let PatternMatchSequence { + patterns, + range, + node_index: _, + } = item; let comments = f.context().comments().clone(); let dangling = comments.dangling(item); @@ -25,7 +29,7 @@ impl FormatNodeRule for FormatPatternMatchSequence { // If the sequence is empty, format the empty parentheses, along with any dangling // comments. ([], SequenceType::Tuple | SequenceType::TupleNoParens) => { - return empty_parenthesized("(", dangling, ")").fmt(f) + return empty_parenthesized("(", dangling, ")").fmt(f); } ([], SequenceType::List) => return empty_parenthesized("[", dangling, "]").fmt(f), @@ -34,7 +38,7 @@ impl FormatNodeRule for FormatPatternMatchSequence { ([elt], SequenceType::Tuple | SequenceType::TupleNoParens) => { return parenthesized("(", &format_args![elt.format(), token(",")], ")") .with_dangling_comments(dangling) - .fmt(f) + .fmt(f); } _ => {} @@ -79,9 +83,26 @@ pub(crate) enum SequenceType { impl SequenceType { pub(crate) fn from_pattern(pattern: &PatternMatchSequence, source: &str) -> SequenceType { - if source[pattern.range()].starts_with('[') { + let before_first_pattern = &source[TextRange::new( + pattern.start(), + pattern + .patterns + .first() + .map(Ranged::start) + .unwrap_or(pattern.end()), + )]; + let after_last_pattern = &source[TextRange::new( + pattern.start(), + pattern + .patterns + .first() + .map(Ranged::end) + .unwrap_or(pattern.end()), + )]; + + if before_first_pattern.starts_with('[') && !after_last_pattern.ends_with(',') { SequenceType::List - } else if source[pattern.range()].starts_with('(') { + } else if before_first_pattern.starts_with('(') { // If the pattern is empty, it must be a parenthesized tuple with no members. (This // branch exists to differentiate between a tuple with and without its own parentheses, // but a tuple without its own parentheses must have at least one member.) diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs index 5f8efadd9e960..848b7c4a5d533 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs @@ -9,7 +9,11 @@ pub struct FormatPatternMatchValue; impl FormatNodeRule for FormatPatternMatchValue { fn fmt_fields(&self, item: &PatternMatchValue, f: &mut PyFormatter) -> FormatResult<()> { - let PatternMatchValue { value, range: _ } = item; + let PatternMatchValue { + value, + range: _, + node_index: _, + } = item; value.format().with_options(Parentheses::Never).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/prelude.rs b/crates/ruff_python_formatter/src/prelude.rs index f3f88f6145a8d..be96d7a44fbff 100644 --- a/crates/ruff_python_formatter/src/prelude.rs +++ b/crates/ruff_python_formatter/src/prelude.rs @@ -1,7 +1,5 @@ -#[allow(unused_imports)] pub(crate) use crate::{ - builders::PyFormatterExtensions, AsFormat, FormatNodeRule, FormattedIterExt as _, IntoFormat, - PyFormatContext, PyFormatter, + AsFormat, FormatNodeRule, FormattedIterExt as _, IntoFormat, PyFormatContext, PyFormatter, + builders::PyFormatterExtensions, }; -#[allow(unused_imports)] pub(crate) use ruff_formatter::prelude::*; diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 52e0881f3db8d..ee9b378cb893d 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -20,3 +20,10 @@ pub(crate) const fn is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring ) -> bool { context.is_preview() } + +/// Returns `true` if the [`blank_line_before_decorated_class_in_stub`](https://github.com/astral-sh/ruff/issues/18865) preview style is enabled. +pub(crate) const fn is_blank_line_before_decorated_class_in_stub_enabled( + context: &PyFormatContext, +) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 03b16b4467f52..436c1a12c2f94 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -2,13 +2,13 @@ use tracing::Level; use ruff_formatter::printer::SourceMapGeneration; use ruff_formatter::{ - format, FormatContext, FormatError, FormatOptions, IndentStyle, PrintedRange, SourceCode, + FormatContext, FormatError, FormatOptions, IndentStyle, PrintedRange, SourceCode, format, }; -use ruff_python_ast::visitor::source_order::{walk_body, SourceOrderVisitor, TraversalSignal}; +use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_body}; use ruff_python_ast::{AnyNodeRef, Stmt, StmtMatch, StmtTry}; -use ruff_python_parser::{parse, ParseOptions}; +use ruff_python_parser::{ParseOptions, parse}; use ruff_python_trivia::{ - indentation_at_offset, BackwardsTokenizer, CommentRanges, SimpleToken, SimpleTokenKind, + BackwardsTokenizer, CommentRanges, SimpleToken, SimpleTokenKind, indentation_at_offset, }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -17,7 +17,7 @@ use crate::context::{IndentLevel, NodeLevel}; use crate::prelude::*; use crate::statement::suite::DocstringStmt; use crate::verbatim::{ends_suppression, starts_suppression}; -use crate::{format_module_source, FormatModuleError, PyFormatOptions}; +use crate::{FormatModuleError, PyFormatOptions, format_module_source}; /// Formats the given `range` in source rather than the entire file. /// @@ -369,6 +369,7 @@ impl SourceOrderVisitor<'_> for NarrowRange<'_> { subject: _, cases, range: _, + node_index: _, }) => { if let Some(saved_state) = self.enter_level(cases.first().map(AnyNodeRef::from)) { for match_case in cases { @@ -387,6 +388,7 @@ impl SourceOrderVisitor<'_> for NarrowRange<'_> { finalbody, is_star: _, range: _, + node_index: _, }) => { self.visit_body(body); if let Some(except_handler_saved) = @@ -546,7 +548,7 @@ impl NarrowRange<'_> { Some(SavedLevel { level: saved_level }) } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn leave_level(&mut self, saved_state: SavedLevel) { self.level = saved_state.level; } @@ -659,10 +661,11 @@ impl Format> for FormatEnclosingNode<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::FStringExpressionElement(_) - | AnyNodeRef::FStringLiteralElement(_) - | AnyNodeRef::FStringFormatSpec(_) + | AnyNodeRef::InterpolatedElement(_) + | AnyNodeRef::InterpolatedStringLiteralElement(_) + | AnyNodeRef::InterpolatedStringFormatSpec(_) | AnyNodeRef::ExprFString(_) + | AnyNodeRef::ExprTString(_) | AnyNodeRef::ExprStringLiteral(_) | AnyNodeRef::ExprBytesLiteral(_) | AnyNodeRef::ExprNumberLiteral(_) @@ -679,6 +682,7 @@ impl Format> for FormatEnclosingNode<'_> { | AnyNodeRef::ExprIpyEscapeCommand(_) | AnyNodeRef::FString(_) | AnyNodeRef::StringLiteral(_) + | AnyNodeRef::TString(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index 0bfef48438014..7cc82ca923b08 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -1,4 +1,4 @@ -use ruff_formatter::{write, Argument, Arguments, FormatError}; +use ruff_formatter::{Argument, Arguments, FormatError, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{ ElifElseClause, ExceptHandlerExceptHandler, MatchCase, StmtClassDef, StmtFor, StmtFunctionDef, @@ -7,8 +7,8 @@ use ruff_python_ast::{ use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; -use crate::statement::suite::{contains_only_an_ellipsis, SuiteKind}; +use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments}; +use crate::statement::suite::{SuiteKind, contains_only_an_ellipsis}; use crate::verbatim::write_suppressed_clause_header; use crate::{has_skip_comment, prelude::*}; @@ -87,6 +87,7 @@ impl ClauseHeader<'_> { type_params, arguments, range: _, + node_index: _, decorator_list: _, name: _, body: _, @@ -103,6 +104,7 @@ impl ClauseHeader<'_> { type_params, parameters, range: _, + node_index: _, is_async: _, decorator_list: _, name: _, @@ -121,6 +123,7 @@ impl ClauseHeader<'_> { ClauseHeader::If(StmtIf { test, range: _, + node_index: _, body: _, elif_else_clauses: _, }) => { @@ -129,6 +132,7 @@ impl ClauseHeader<'_> { ClauseHeader::ElifElse(ElifElseClause { test, range: _, + node_index: _, body: _, }) => { if let Some(test) = test.as_ref() { @@ -139,6 +143,7 @@ impl ClauseHeader<'_> { ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { type_: type_expr, range: _, + node_index: _, name: _, body: _, }) => { @@ -149,6 +154,7 @@ impl ClauseHeader<'_> { ClauseHeader::Match(StmtMatch { subject, range: _, + node_index: _, cases: _, }) => { visit(subject.as_ref(), visitor); @@ -157,6 +163,7 @@ impl ClauseHeader<'_> { guard, pattern, range: _, + node_index: _, body: _, }) => { visit(pattern, visitor); @@ -169,6 +176,7 @@ impl ClauseHeader<'_> { target, iter, range: _, + node_index: _, is_async: _, body: _, orelse: _, @@ -179,6 +187,7 @@ impl ClauseHeader<'_> { ClauseHeader::While(StmtWhile { test, range: _, + node_index: _, body: _, orelse: _, }) => { @@ -187,6 +196,7 @@ impl ClauseHeader<'_> { ClauseHeader::With(StmtWith { items, range: _, + node_index: _, is_async: _, body: _, }) => { @@ -473,12 +483,22 @@ fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResu range, }) => Ok(range), Some(token) => { - debug_assert!(false, "Expected the colon marking the end of the case header but found {token:?} instead."); - Err(FormatError::syntax_error("Expected colon marking the end of the case header but found another token instead.")) + debug_assert!( + false, + "Expected the colon marking the end of the case header but found {token:?} instead." + ); + Err(FormatError::syntax_error( + "Expected colon marking the end of the case header but found another token instead.", + )) } None => { - debug_assert!(false, "Expected the colon marking the end of the case header but found the end of the range."); - Err(FormatError::syntax_error("Expected the colon marking the end of the case header but found the end of the range.")) + debug_assert!( + false, + "Expected the colon marking the end of the case header but found the end of the range." + ); + Err(FormatError::syntax_error( + "Expected the colon marking the end of the case header but found the end of the range.", + )) } } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index a51e668f3e7d8..1de79a6b07eab 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -17,6 +17,7 @@ impl FormatNodeRule for FormatStmtAnnAssign { fn fmt_fields(&self, item: &StmtAnnAssign, f: &mut PyFormatter) -> FormatResult<()> { let StmtAnnAssign { range: _, + node_index: _, target, annotation, value, diff --git a/crates/ruff_python_formatter/src/statement/stmt_assert.rs b/crates/ruff_python_formatter/src/statement/stmt_assert.rs index 099ce3c24d391..fc2aba97015f3 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assert.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assert.rs @@ -14,6 +14,7 @@ impl FormatNodeRule for FormatStmtAssert { fn fmt_fields(&self, item: &StmtAssert, f: &mut PyFormatter) -> FormatResult<()> { let StmtAssert { range: _, + node_index: _, test, msg, } = item; diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 80dfc81884bdb..b9fbe6b7a3e85 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,29 +1,29 @@ -use ruff_formatter::{format_args, write, FormatError, RemoveSoftLinesBuffer}; +use ruff_formatter::{FormatError, RemoveSoftLinesBuffer, format_args, write}; use ruff_python_ast::{ - AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike, + AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike, TString, TypeParams, }; use crate::builders::parenthesize_if_expands; use crate::comments::{ - trailing_comments, Comments, LeadingDanglingTrailingComments, SourceComment, + Comments, LeadingDanglingTrailingComments, SourceComment, trailing_comments, }; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{ - is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses, - Parentheses, Parenthesize, + NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, is_expression_parenthesized, + optional_parentheses, }; use crate::expression::{ can_omit_optional_parentheses, has_own_parentheses, has_parentheses, maybe_parenthesize_expression, }; -use crate::other::f_string::FStringLayout; +use crate::other::interpolated_string::InterpolatedStringLayout; use crate::statement::trailing_semicolon; +use crate::string::StringLikeExtensions; use crate::string::implicit::{ FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat, ImplicitConcatenatedLayout, }; -use crate::string::StringLikeExtensions; use crate::{has_skip_comment, prelude::*}; #[derive(Default)] @@ -33,6 +33,7 @@ impl FormatNodeRule for FormatStmtAssign { fn fmt_fields(&self, item: &StmtAssign, f: &mut PyFormatter) -> FormatResult<()> { let StmtAssign { range: _, + node_index: _, targets, value, } = item; @@ -291,15 +292,16 @@ impl Format> for FormatStatementsLastExpression<'_> { let can_inline_comment = should_inline_comments(value, *statement, f.context()); let string_like = StringLike::try_from(*value).ok(); - let format_f_string = - string_like.and_then(|string| format_f_string_assignment(string, f.context())); + let format_interpolated_string = string_like + .and_then(|string| format_interpolated_string_assignment(string, f.context())); + let format_implicit_flat = string_like.and_then(|string| { FormatImplicitConcatenatedStringFlat::new(string, f.context()) }); if !can_inline_comment && format_implicit_flat.is_none() - && format_f_string.is_none() + && format_interpolated_string.is_none() { return maybe_parenthesize_expression( value, @@ -351,7 +353,7 @@ impl Format> for FormatStatementsLastExpression<'_> { let string = flat.string(); let flat = format_with(|f| { - if string.is_fstring() { + if string.is_interpolated_string() { let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); write!(buffer, [flat]) @@ -361,7 +363,7 @@ impl Format> for FormatStatementsLastExpression<'_> { }) .memoized(); - // F-String containing an expression with a magic trailing comma, a comment, or a + // F-string or T-string containing an expression with a magic trailing comma, a comment, or a // multiline debug expression should never be joined. Use the default layout. // ```python // aaaa = f"abcd{[ @@ -369,7 +371,7 @@ impl Format> for FormatStatementsLastExpression<'_> { // 2, // ]}" "more" // ``` - if string.is_fstring() && flat.inspect(f)?.will_break() { + if string.is_interpolated_string() && flat.inspect(f)?.will_break() { inline_comments.mark_unformatted(); return write!( @@ -413,7 +415,7 @@ impl Format> for FormatStatementsLastExpression<'_> { soft_block_indent(&format_args![flat, inline_comments]), token(")"), ]) - .with_group_id(Some(group_id)) + .with_id(Some(group_id)) .should_expand(true) .fmt(f) }); @@ -433,7 +435,7 @@ impl Format> for FormatStatementsLastExpression<'_> { token(")"), inline_comments, ]) - .with_group_id(Some(group_id)) + .with_id(Some(group_id)) .should_expand(true) .fmt(f) }); @@ -446,24 +448,23 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![single_line, joined_parenthesized, implicit_expanded] .with_mode(BestFittingMode::AllLines) .fmt(f)?; - } else if let Some(format_f_string) = format_f_string { + } else if let Some(format_interpolated_string) = format_interpolated_string { inline_comments.mark_formatted(); - let f_string_flat = format_with(|f| { + let interpolated_string_flat = format_with(|f| { let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); - write!(buffer, [format_f_string.format()]) + write!(buffer, [format_interpolated_string]) }) .memoized(); - - // F-String containing an expression with a magic trailing comma, a comment, or a - // multiline debug expression should never be joined. Use the default layout. + // F/T-String containing an interpolation with a magic trailing comma, a comment, or a + // multiline debug interpolation should never be joined. Use the default layout. // ```python // aaaa = f"aaaa {[ // 1, 2, // ]} bbbb" // ``` - if f_string_flat.inspect(f)?.will_break() { + if interpolated_string_flat.inspect(f)?.will_break() { inline_comments.mark_unformatted(); return write!( @@ -482,43 +483,51 @@ impl Format> for FormatStatementsLastExpression<'_> { // expression}moreeeeeeeeeeeeeeeee" // ``` - // Flatten the f-string. + // Flatten the f/t-string. // ```python // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" // ``` let single_line = - format_with(|f| write!(f, [f_string_flat, inline_comments])); + format_with(|f| write!(f, [interpolated_string_flat, inline_comments])); - // Parenthesize the f-string and flatten the f-string. + // Parenthesize the t-string and flatten the t-string. // ```python // aaaaaaaaaaaaaaaaaa = ( - // f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" + // t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" // ) // ``` let joined_parenthesized = format_with(|f| { group(&format_args![ token("("), - soft_block_indent(&format_args![f_string_flat, inline_comments]), + soft_block_indent(&format_args![ + interpolated_string_flat, + inline_comments + ]), token(")"), ]) - .with_group_id(Some(group_id)) + .with_id(Some(group_id)) .should_expand(true) .fmt(f) }); - // Avoid flattening or parenthesizing the f-string, keep the original - // f-string formatting. + // Avoid flattening or parenthesizing the f/t-string, keep the original + // f/t-string formatting. // ```python - // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + // aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ // expression // }moreeeeeeeeeeeeeeeee" // ``` - let format_f_string = - format_with(|f| write!(f, [format_f_string.format(), inline_comments])); + let format_interpolated_string = format_with(|f| { + write!(f, [format_interpolated_string, inline_comments]) + }); - best_fitting![single_line, joined_parenthesized, format_f_string] - .with_mode(BestFittingMode::AllLines) - .fmt(f)?; + best_fitting![ + single_line, + joined_parenthesized, + format_interpolated_string + ] + .with_mode(BestFittingMode::AllLines) + .fmt(f)?; } else { best_fit_parenthesize(&format_once(|f| { inline_comments.mark_formatted(); @@ -559,17 +568,16 @@ impl Format> for FormatStatementsLastExpression<'_> { let should_inline_comments = should_inline_comments(value, *statement, f.context()); let string_like = StringLike::try_from(*value).ok(); - let format_f_string = - string_like.and_then(|string| format_f_string_assignment(string, f.context())); + let format_interpolated_string = string_like + .and_then(|string| format_interpolated_string_assignment(string, f.context())); let format_implicit_flat = string_like.and_then(|string| { FormatImplicitConcatenatedStringFlat::new(string, f.context()) }); - // Use the normal `maybe_parenthesize_layout` for splittable `value`s. if !should_inline_comments && !should_non_inlineable_use_best_fit(value, *statement, f.context()) && format_implicit_flat.is_none() - && format_f_string.is_none() + && format_interpolated_string.is_none() { return write!( f, @@ -593,7 +601,7 @@ impl Format> for FormatStatementsLastExpression<'_> { // Don't inline comments for attribute and call expressions for black compatibility let inline_comments = if should_inline_comments || format_implicit_flat.is_some() - || format_f_string.is_some() + || format_interpolated_string.is_some() { OptionalParenthesesInlinedComments::new( &expression_comments, @@ -633,7 +641,9 @@ impl Format> for FormatStatementsLastExpression<'_> { // This is mainly a performance optimisation that avoids unnecessary memoization // and using the costly `BestFitting` layout if it is already known that only the last variant // can ever fit because the left breaks. - if format_implicit_flat.is_none() && format_f_string.is_none() && last_target_breaks + if format_implicit_flat.is_none() + && format_interpolated_string.is_none() + && last_target_breaks { return write!( f, @@ -650,7 +660,7 @@ impl Format> for FormatStatementsLastExpression<'_> { let format_value = format_with(|f| { if let Some(format_implicit_flat) = format_implicit_flat.as_ref() { - if format_implicit_flat.string().is_fstring() { + if format_implicit_flat.string().is_interpolated_string() { // Remove any soft line breaks emitted by the f-string formatting. // This is important when formatting f-strings as part of an assignment right side // because `best_fit_parenthesize` will otherwise still try to break inner @@ -660,11 +670,13 @@ impl Format> for FormatStatementsLastExpression<'_> { } else { format_implicit_flat.fmt(f) } - } else if let Some(format_f_string) = format_f_string.as_ref() { + } else if let Some(format_interpolated_string) = + format_interpolated_string.as_ref() + { // Similar to above, remove any soft line breaks emitted by the f-string // formatting. let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); - write!(buffer, [format_f_string.format()]) + write!(buffer, [format_interpolated_string]) } else { value.format().with_options(Parentheses::Never).fmt(f) } @@ -766,7 +778,7 @@ impl Format> for FormatStatementsLastExpression<'_> { // 2, // ]}" "more" // ``` - if format_implicit_flat.string().is_fstring() + if format_implicit_flat.string().is_interpolated_string() && format_value.inspect(f)?.will_break() { inline_comments.mark_unformatted(); @@ -817,7 +829,7 @@ impl Format> for FormatStatementsLastExpression<'_> { space(), token("("), group(&soft_block_indent(&format_expanded)) - .with_group_id(Some(group_id)) + .with_id(Some(group_id)) .should_expand(true), token(")"), inline_comments @@ -875,7 +887,7 @@ impl Format> for FormatStatementsLastExpression<'_> { space(), token("("), group(&soft_block_indent(&format_expanded)) - .with_group_id(Some(group_id)) + .with_id(Some(group_id)) .should_expand(true), token(")"), inline_comments @@ -905,12 +917,12 @@ impl Format> for FormatStatementsLastExpression<'_> { .with_mode(BestFittingMode::AllLines) .fmt(f) } - } else if let Some(format_f_string) = &format_f_string { - // F-String containing an expression with a magic trailing comma, a comment, or a + } else if let Some(format_interpolated_string) = &format_interpolated_string { + // F/T-String containing an interpolation with a magic trailing comma, a comment, or a // multiline debug expression should never be joined. Use the default layout. // // ```python - // aaaa, bbbb = f"aaaa {[ + // aaaa, bbbb = t"aaaa {[ // 1, 2, // ]} bbbb" // ``` @@ -933,40 +945,46 @@ impl Format> for FormatStatementsLastExpression<'_> { ); } - let format_f_string = - format_with(|f| write!(f, [format_f_string.format(), inline_comments])) + let format_interpolated_string = + format_with(|f| write!(f, [format_interpolated_string, inline_comments])) .memoized(); // Considering the following initial source: // // ```python // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( - // f"aaaaaaaaaaaaaaaaaaa { + // t"aaaaaaaaaaaaaaaaaaa { // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" // ) // ``` // - // Keep the target flat, and use the regular f-string formatting. + // Keep the target flat, and use the regular f/t-string formatting. // // ```python - // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa { // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc // } ddddddddddddddddddd" // ``` - let flat_target_regular_f_string = format_with(|f| { + let flat_target_regular_interpolated_string = format_with(|f| { write!( f, - [last_target, space(), operator, space(), format_f_string] + [ + last_target, + space(), + operator, + space(), + format_interpolated_string + ] ) }); - // Expand the parent and parenthesize the flattened f-string. + // Expand the parent and parenthesize the flattened f/t-string. // // ```python // aaaaaaaaaaaa[ // "bbbbbbbbbbbbbbbb" // ] = ( - // f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + // t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" // ) // ``` let split_target_value_parenthesized_flat = format_with(|f| { @@ -988,16 +1006,16 @@ impl Format> for FormatStatementsLastExpression<'_> { ) }); - // Expand the parent, and use the regular f-string formatting. + // Expand the parent, and use the regular f/t-string formatting. // // ```python // aaaaaaaaaaaa[ // "bbbbbbbbbbbbbbbb" - // ] = f"aaaaaaaaaaaaaaaaaaa { + // ] = t"aaaaaaaaaaaaaaaaaaa { // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc // } ddddddddddddddddddd" // ``` - let split_target_regular_f_string = format_with(|f| { + let split_target_regular_interpolated_string = format_with(|f| { write!( f, [ @@ -1005,7 +1023,7 @@ impl Format> for FormatStatementsLastExpression<'_> { space(), operator, space(), - format_f_string, + format_interpolated_string, ] ) }); @@ -1016,7 +1034,7 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![ split_target_flat_value, split_target_value_parenthesized_flat, - split_target_regular_f_string, + split_target_regular_interpolated_string, ] .with_mode(BestFittingMode::AllLines) .fmt(f) @@ -1024,10 +1042,10 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![ single_line, flat_target_parenthesize_value, - flat_target_regular_f_string, + flat_target_regular_interpolated_string, split_target_flat_value, split_target_value_parenthesized_flat, - split_target_regular_f_string, + split_target_regular_interpolated_string, ] .with_mode(BestFittingMode::AllLines) .fmt(f) @@ -1045,13 +1063,31 @@ impl Format> for FormatStatementsLastExpression<'_> { } } -/// Formats an f-string that is at the value position of an assignment statement. +#[derive(Debug, Copy, Clone)] +enum InterpolatedString<'a> { + FString(&'a FString), + TString(&'a TString), +} + +impl Format> for InterpolatedString<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + match self { + InterpolatedString::FString(string) => string.format().fmt(f), + InterpolatedString::TString(string) => string.format().fmt(f), + } + } +} + +/// Formats an f/t-string that is at the value position of an assignment statement. /// -/// This is just a wrapper around [`FormatFString`] while considering a special case when the -/// f-string is at an assignment statement's value position. +/// For legibility, we discuss only the case of f-strings below, but the +/// same comments apply to t-strings. /// -/// This is necessary to prevent an instability where an f-string contains a multiline expression -/// and the f-string fits on the line, but only when it's surrounded by parentheses. +/// This is just a wrapper around [`FormatFString`] while considering a special +/// case when the f-string is at an assignment statement's value position. +/// This is necessary to prevent an instability where an f-string contains a +/// multiline expression and the f-string fits on the line, but only when it's +/// surrounded by parentheses. /// /// ```python /// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ @@ -1099,30 +1135,40 @@ impl Format> for FormatStatementsLastExpression<'_> { /// The reason for this is because (a) f-string already has a multiline expression thus it tries to /// break the expression and (b) the `BestFit` layout doesn't considers the layout where the /// multiline f-string isn't surrounded by parentheses. -fn format_f_string_assignment<'a>( +fn format_interpolated_string_assignment<'a>( string: StringLike<'a>, context: &PyFormatContext, -) -> Option<&'a FString> { - let StringLike::FString(expr) = string else { - return None; +) -> Option> { + let (interpolated_string, elements) = match string { + StringLike::TString(expr) => { + let t_string = expr.as_single_part_tstring()?; + (InterpolatedString::TString(t_string), &t_string.elements) + } + StringLike::FString(expr) => { + let f_string = expr.as_single_part_fstring()?; + (InterpolatedString::FString(f_string), &f_string.elements) + } + _ => { + return None; + } }; - let f_string = expr.as_single_part_fstring()?; - - // If the f-string is flat, there are no breakpoints from which it can be made multiline. - // This is the case when the f-string has no expressions or if it does then the expressions + // If the f/t-string is flat, there are no breakpoints from which it can be made multiline. + // This is the case when the f/t-string has no expressions or if it does then the expressions // are flat (no newlines). - if FStringLayout::from_f_string(f_string, context.source()).is_flat() { + if InterpolatedStringLayout::from_interpolated_string_elements(elements, context.source()) + .is_flat() + { return None; } - // This checks whether the f-string is multi-line and it can *never* be flattened. Thus, + // This checks whether the f/t-string is multi-line and it can *never* be flattened. Thus, // it's useless to try the flattened layout. if string.is_multiline(context) { return None; } - Some(f_string) + Some(interpolated_string) } #[derive(Debug, Default)] @@ -1153,7 +1199,10 @@ impl<'a> OptionalParenthesesInlinedComments<'a> { let (expression_inline_comments, trailing_own_line_comments) = expression_comments.trailing.split_at(after_end_of_line); - debug_assert!(trailing_own_line_comments.is_empty(), "The method should have returned early if the expression has trailing own line comments"); + debug_assert!( + trailing_own_line_comments.is_empty(), + "The method should have returned early if the expression has trailing own line comments" + ); Some(OptionalParenthesesInlinedComments { expression: expression_inline_comments, @@ -1274,6 +1323,9 @@ fn should_inline_comments( Expr::FString(fstring) => { fstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit } + Expr::TString(tstring) => { + tstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } _ => false, } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs index 4a8a3b382d85c..58ac4b34e5068 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs @@ -4,12 +4,12 @@ use ruff_python_ast::StmtAugAssign; use crate::comments::SourceComment; use crate::expression::parentheses::is_expression_parenthesized; use crate::statement::stmt_assign::{ - has_target_own_parentheses, AnyAssignmentOperator, AnyBeforeOperator, - FormatStatementsLastExpression, + AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression, + has_target_own_parentheses, }; use crate::statement::trailing_semicolon; -use crate::{has_skip_comment, prelude::*}; use crate::{AsFormat, FormatNodeRule}; +use crate::{has_skip_comment, prelude::*}; #[derive(Default)] pub struct FormatStmtAugAssign; @@ -21,6 +21,7 @@ impl FormatNodeRule for FormatStmtAugAssign { op, value, range: _, + node_index: _, } = item; if has_target_own_parentheses(target, f.context()) diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 6077720412371..9e84c76580e40 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -6,9 +6,9 @@ use ruff_text_size::Ranged; use crate::comments::format::{ empty_lines_after_leading_comments, empty_lines_before_trailing_comments, }; -use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::comments::{SourceComment, leading_comments, trailing_comments}; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -18,6 +18,7 @@ impl FormatNodeRule for FormatStmtClassDef { fn fmt_fields(&self, item: &StmtClassDef, f: &mut PyFormatter) -> FormatResult<()> { let StmtClassDef { range: _, + node_index: _, name, arguments, body, diff --git a/crates/ruff_python_formatter/src/statement/stmt_delete.rs b/crates/ruff_python_formatter/src/statement/stmt_delete.rs index 17b1cbe911237..fa3a27bb6f4d1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_delete.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_delete.rs @@ -2,8 +2,8 @@ use ruff_formatter::write; use ruff_python_ast::StmtDelete; use ruff_text_size::Ranged; -use crate::builders::{parenthesize_if_expands, PyFormatterExtensions}; -use crate::comments::{dangling_node_comments, SourceComment}; +use crate::builders::{PyFormatterExtensions, parenthesize_if_expands}; +use crate::comments::{SourceComment, dangling_node_comments}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::{has_skip_comment, prelude::*}; @@ -13,7 +13,11 @@ pub struct FormatStmtDelete; impl FormatNodeRule for FormatStmtDelete { fn fmt_fields(&self, item: &StmtDelete, f: &mut PyFormatter) -> FormatResult<()> { - let StmtDelete { range: _, targets } = item; + let StmtDelete { + range: _, + node_index: _, + targets, + } = item; write!(f, [token("del"), space()])?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_for.rs b/crates/ruff_python_formatter/src/statement/stmt_for.rs index 7d9d334d95fcf..17517fc20ae7a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -6,7 +6,7 @@ use crate::expression::expr_tuple::TupleParentheses; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader, ElseClause}; +use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Debug)] @@ -36,6 +36,7 @@ impl FormatNodeRule for FormatStmtFor { body, orelse, range: _, + node_index: _, } = item; let comments = f.context().comments().clone(); diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index ffd70bec6baf2..86e2003efdc26 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -4,7 +4,7 @@ use crate::comments::format::{ use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::stmt_class_def::FormatDecorators; use crate::statement::suite::SuiteKind; use ruff_formatter::write; @@ -93,6 +93,7 @@ impl FormatNodeRule for FormatStmtFunctionDef { fn format_function_header(f: &mut PyFormatter, item: &StmtFunctionDef) -> FormatResult<()> { let StmtFunctionDef { range: _, + node_index: _, is_async, decorator_list: _, name, diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index f58333dc67b46..9b080ddc6e87a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -5,7 +5,7 @@ use ruff_text_size::Ranged; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -15,6 +15,7 @@ impl FormatNodeRule for FormatStmtIf { fn fmt_fields(&self, item: &StmtIf, f: &mut PyFormatter) -> FormatResult<()> { let StmtIf { range: _, + node_index: _, test, body, elif_else_clauses, @@ -68,6 +69,7 @@ pub(crate) fn format_elif_else_clause( ) -> FormatResult<()> { let ElifElseClause { range: _, + node_index: _, test, body, } = item; diff --git a/crates/ruff_python_formatter/src/statement/stmt_import.rs b/crates/ruff_python_formatter/src/statement/stmt_import.rs index 8045ae7864db5..c44b8ffd494d5 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import.rs @@ -9,7 +9,11 @@ pub struct FormatStmtImport; impl FormatNodeRule for FormatStmtImport { fn fmt_fields(&self, item: &StmtImport, f: &mut PyFormatter) -> FormatResult<()> { - let StmtImport { names, range: _ } = item; + let StmtImport { + names, + range: _, + node_index: _, + } = item; let names = format_with(|f| { f.join_with(&format_args![token(","), space()]) .entries(names.iter().formatted()) diff --git a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs index eb91c452798fa..341adc64d227e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -2,7 +2,7 @@ use ruff_formatter::write; use ruff_python_ast::StmtImportFrom; use ruff_text_size::Ranged; -use crate::builders::{parenthesize_if_expands, PyFormatterExtensions, TrailingComma}; +use crate::builders::{PyFormatterExtensions, TrailingComma, parenthesize_if_expands}; use crate::comments::SourceComment; use crate::expression::parentheses::parenthesized; use crate::has_skip_comment; @@ -19,6 +19,7 @@ impl FormatNodeRule for FormatStmtImportFrom { names, level, range: _, + node_index: _, } = item; write!( diff --git a/crates/ruff_python_formatter/src/statement/stmt_match.rs b/crates/ruff_python_formatter/src/statement/stmt_match.rs index 441c881c7a0f1..45e0f9238659a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_match.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_match.rs @@ -6,7 +6,7 @@ use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_header}; #[derive(Default)] pub struct FormatStmtMatch; @@ -15,6 +15,7 @@ impl FormatNodeRule for FormatStmtMatch { fn fmt_fields(&self, item: &StmtMatch, f: &mut PyFormatter) -> FormatResult<()> { let StmtMatch { range: _, + node_index: _, subject, cases, } = item; diff --git a/crates/ruff_python_formatter/src/statement/stmt_raise.rs b/crates/ruff_python_formatter/src/statement/stmt_raise.rs index 6a855e4418586..bfb73d00afaae 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_raise.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_raise.rs @@ -13,6 +13,7 @@ impl FormatNodeRule for FormatStmtRaise { fn fmt_fields(&self, item: &StmtRaise, f: &mut PyFormatter) -> FormatResult<()> { let StmtRaise { range: _, + node_index: _, exc, cause, } = item; diff --git a/crates/ruff_python_formatter/src/statement/stmt_return.rs b/crates/ruff_python_formatter/src/statement/stmt_return.rs index ab782c02edbf2..fbeb9488e441e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_return.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_return.rs @@ -11,7 +11,11 @@ pub struct FormatStmtReturn; impl FormatNodeRule for FormatStmtReturn { fn fmt_fields(&self, item: &StmtReturn, f: &mut PyFormatter) -> FormatResult<()> { - let StmtReturn { range: _, value } = item; + let StmtReturn { + range: _, + node_index: _, + value, + } = item; token("return").fmt(f)?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index a3964ad214cfa..411be5b33984a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -1,15 +1,15 @@ -use ruff_formatter::{write, FormatRuleWithOptions}; +use ruff_formatter::{FormatRuleWithOptions, write}; use ruff_python_ast::{ExceptHandler, StmtTry}; use ruff_text_size::Ranged; use crate::comments; -use crate::comments::leading_alternate_branch_comments; use crate::comments::SourceComment; +use crate::comments::leading_alternate_branch_comments; use crate::other::except_handler_except_handler::{ ExceptHandlerKind, FormatExceptHandlerExceptHandler, }; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader, ElseClause}; +use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; use crate::statement::suite::SuiteKind; use crate::statement::{FormatRefWithRule, Stmt}; @@ -66,6 +66,7 @@ impl FormatNodeRule for FormatStmtTry { finalbody, is_star, range: _, + node_index: _, } = item; let comments_info = f.context().comments().clone(); diff --git a/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs b/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs index e503d413668b2..a66b4f64be9c1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs @@ -17,6 +17,7 @@ impl FormatNodeRule for FormatStmtTypeAlias { type_params, value, range: _, + node_index: _, } = item; write!(f, [token("type"), space(), name.as_ref().format()])?; diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index 538a18692032c..dc5593ded0265 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -5,7 +5,7 @@ use ruff_text_size::Ranged; use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader, ElseClause}; +use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Default)] @@ -15,6 +15,7 @@ impl FormatNodeRule for FormatStmtWhile { fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> { let StmtWhile { range: _, + node_index: _, test, body, orelse, diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index 5098251850f84..4c9cbc0b3f529 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -1,4 +1,4 @@ -use ruff_formatter::{format_args, write, FormatContext, FormatError}; +use ruff_formatter::{FormatContext, FormatError, format_args, write}; use ruff_python_ast::PythonVersion; use ruff_python_ast::{StmtWith, WithItem}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; @@ -13,7 +13,7 @@ use crate::expression::parentheses::{ use crate::other::commas; use crate::other::with_item::WithItemLayout; use crate::prelude::*; -use crate::statement::clause::{clause_body, clause_header, ClauseHeader}; +use crate::statement::clause::{ClauseHeader, clause_body, clause_header}; use crate::statement::suite::SuiteKind; #[derive(Default)] diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 7b275677ef288..4071b4ba1fc52 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -1,5 +1,5 @@ use ruff_formatter::{ - write, FormatContext, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, + FormatContext, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, write, }; use ruff_python_ast::helpers::is_compound_statement; use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite}; @@ -8,11 +8,12 @@ use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, l use ruff_text_size::{Ranged, TextRange}; use crate::comments::{ - leading_comments, trailing_comments, Comments, LeadingDanglingTrailingComments, + Comments, LeadingDanglingTrailingComments, leading_comments, trailing_comments, }; use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel}; use crate::other::string_literal::StringLiteralKind; use crate::prelude::*; +use crate::preview::is_blank_line_before_decorated_class_in_stub_enabled; use crate::statement::stmt_expr::FormatStmtExpr; use crate::verbatim::{ suppressed_node, write_suppressed_statements_starting_with_leading_comment, @@ -170,7 +171,6 @@ impl FormatRule> for FormatSuite { } else { first.fmt(f)?; - #[allow(clippy::if_same_then_else)] let empty_line_after_docstring = if matches!(first, SuiteChildStatement::Docstring(_)) && self.kind == SuiteKind::Class { @@ -701,10 +701,15 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm // // class LockType2: ... // ``` - let class_decorator_instead_of_empty_line = preceding.is_function_def_stmt() - && following - .as_class_def_stmt() - .is_some_and(|class| !class.decorator_list.is_empty()); + // + // However, this behavior is incorrect and should not be replicated in preview mode. + // See: https://github.com/astral-sh/ruff/issues/18865 + let class_decorator_instead_of_empty_line = + !is_blank_line_before_decorated_class_in_stub_enabled(f.context()) + && preceding.is_function_def_stmt() + && following + .as_class_def_stmt() + .is_some_and(|class| !class.decorator_list.is_empty()); // A function definition following a stub function definition // ```python @@ -917,10 +922,10 @@ mod tests { use ruff_python_parser::parse_module; use ruff_python_trivia::CommentRanges; + use crate::PyFormatOptions; use crate::comments::Comments; use crate::prelude::*; use crate::statement::suite::SuiteKind; - use crate::PyFormatOptions; fn format_suite(level: SuiteKind) -> String { let source = r" diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index 58b1c98b00393..5ca55677749f5 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -10,19 +10,19 @@ use itertools::Itertools; use regex::Regex; use ruff_formatter::printer::SourceMapGeneration; -use ruff_python_ast::{str::Quote, AnyStringFlags, StringFlags}; +use ruff_python_ast::{AnyStringFlags, StringFlags, str::Quote}; use ruff_python_parser::ParseOptions; use ruff_python_trivia::CommentRanges; use { - ruff_formatter::{write, FormatOptions, IndentStyle, LineWidth, Printed}, - ruff_python_trivia::{is_python_whitespace, PythonWhitespace}, + ruff_formatter::{FormatOptions, IndentStyle, LineWidth, Printed, write}, + ruff_python_trivia::{PythonWhitespace, is_python_whitespace}, ruff_text_size::{Ranged, TextLen, TextRange, TextSize}, }; use super::NormalizedString; use crate::preview::is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled; use crate::string::StringQuotes; -use crate::{prelude::*, DocstringCodeLineWidth, FormatModuleError}; +use crate::{DocstringCodeLineWidth, FormatModuleError, prelude::*}; /// Format a docstring by trimming whitespace and adjusting the indentation. /// @@ -1796,7 +1796,7 @@ impl Indentation { }), _ => None, - } + }; } Self::Mixed { .. } => return None, }; diff --git a/crates/ruff_python_formatter/src/string/implicit.rs b/crates/ruff_python_formatter/src/string/implicit.rs index 9aeeffd4844e8..fc25d030aa8c2 100644 --- a/crates/ruff_python_formatter/src/string/implicit.rs +++ b/crates/ruff_python_formatter/src/string/implicit.rs @@ -1,28 +1,32 @@ use itertools::Itertools; -use ruff_formatter::{format_args, write, FormatContext}; +use ruff_formatter::{FormatContext, format_args, write}; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_ast::str_prefix::{ - AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, + AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix, +}; +use ruff_python_ast::{ + AnyStringFlags, FString, InterpolatedStringElement, StringFlags, StringLike, StringLikePart, + TString, }; -use ruff_python_ast::{AnyStringFlags, FStringElement, StringFlags, StringLike, StringLikePart}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; use std::borrow::Cow; use crate::comments::{leading_comments, trailing_comments}; use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space; -use crate::other::f_string::{FStringContext, FStringLayout}; -use crate::other::f_string_element::FormatFStringExpressionElement; +use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout}; +use crate::other::interpolated_string_element::FormatInterpolatedElement; use crate::prelude::*; use crate::string::docstring::needs_chaperone_space; use crate::string::normalize::{ - is_fstring_with_quoted_debug_expression, is_fstring_with_quoted_format_spec_and_debug, - is_fstring_with_triple_quoted_literal_expression_containing_quotes, QuoteMetadata, + QuoteMetadata, is_fstring_with_quoted_debug_expression, + is_fstring_with_triple_quoted_literal_expression_containing_quotes, + is_interpolated_string_with_quoted_format_spec_and_debug, }; -use crate::string::{normalize_string, StringLikeExtensions, StringNormalizer, StringQuotes}; +use crate::string::{StringLikeExtensions, StringNormalizer, StringQuotes, normalize_string}; /// Formats any implicitly concatenated string. This could be any valid combination -/// of string, bytes or f-string literals. +/// of string, bytes, f-string, or t-string literals. pub(crate) struct FormatImplicitConcatenatedString<'a> { string: StringLike<'a>, } @@ -97,6 +101,7 @@ impl Format> for FormatImplicitConcatenatedStringExpanded<'_ StringLikePart::String(part) => part.format().fmt(f), StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f), StringLikePart::FString(part) => part.format().fmt(f), + StringLikePart::TString(part) => part.format().fmt(f), }); let part_comments = comments.leading_dangling_trailing(part); @@ -137,7 +142,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { let first_part = string.parts().next()?; - // The string is either a regular string, f-string, or bytes string. + // The string is either a regular string, f-string, t-string, or bytes string. let normalizer = StringNormalizer::from_context(context); // Some if a part requires preserving its quotes. @@ -163,9 +168,34 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { return None; } - if let StringLikePart::FString(fstring) = part { - if context.options().target_version().supports_pep_701() { - if is_fstring_with_quoted_format_spec_and_debug(fstring, context) { + match part { + StringLikePart::FString(fstring) => { + if matches!(string, StringLike::TString(_)) { + // Don't concatenate t-strings and f-strings + return None; + } + if context.options().target_version().supports_pep_701() { + if is_interpolated_string_with_quoted_format_spec_and_debug( + &fstring.elements, + fstring.flags.into(), + context, + ) { + if preserve_quotes_requirement + .is_some_and(|quote| quote != part.flags().quote_style()) + { + return None; + } + preserve_quotes_requirement = Some(part.flags().quote_style()); + } + } + // Avoid invalid syntax for pre Python 312: + // * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}' + // * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'` + else if is_fstring_with_quoted_debug_expression(fstring, context) + || is_fstring_with_triple_quoted_literal_expression_containing_quotes( + fstring, context, + ) + { if preserve_quotes_requirement .is_some_and(|quote| quote != part.flags().quote_style()) { @@ -174,21 +204,21 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { preserve_quotes_requirement = Some(part.flags().quote_style()); } } - // Avoid invalid syntax for pre Python 312: - // * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}' - // * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'` - else if is_fstring_with_quoted_debug_expression(fstring, context) - || is_fstring_with_triple_quoted_literal_expression_containing_quotes( - fstring, context, - ) - { - if preserve_quotes_requirement - .is_some_and(|quote| quote != part.flags().quote_style()) - { - return None; + StringLikePart::TString(tstring) => { + if is_interpolated_string_with_quoted_format_spec_and_debug( + &tstring.elements, + tstring.flags.into(), + context, + ) { + if preserve_quotes_requirement + .is_some_and(|quote| quote != part.flags().quote_style()) + { + return None; + } + preserve_quotes_requirement = Some(part.flags().quote_style()); } - preserve_quotes_requirement = Some(part.flags().quote_style()); } + StringLikePart::Bytes(_) | StringLikePart::String(_) => {} } } @@ -202,6 +232,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { StringLike::String(_) => AnyStringPrefix::Regular(StringLiteralPrefix::Empty), StringLike::Bytes(_) => AnyStringPrefix::Bytes(ByteStringPrefix::Regular), StringLike::FString(_) => AnyStringPrefix::Format(FStringPrefix::Regular), + StringLike::TString(_) => AnyStringPrefix::Template(TStringPrefix::Regular), }; let quote = if let Some(quote) = preserve_quotes_requirement { @@ -286,7 +317,7 @@ impl Format> for FormatImplicitConcatenatedStringFlat<'_> { FormatLiteralContent { range: part.content_range(), flags: self.flags, - is_fstring: false, + is_interpolated_string: false, trim_start: first_non_empty && self.docstring, trim_end: self.docstring && parts.peek().is_none(), } @@ -299,28 +330,32 @@ impl Format> for FormatImplicitConcatenatedStringFlat<'_> { } } - StringLikePart::FString(f_string) => { - for element in &f_string.elements { + StringLikePart::FString(FString { elements, .. }) + | StringLikePart::TString(TString { elements, .. }) => { + for element in elements { match element { - FStringElement::Literal(literal) => { + InterpolatedStringElement::Literal(literal) => { FormatLiteralContent { range: literal.range(), flags: self.flags, - is_fstring: true, + is_interpolated_string: true, trim_end: false, trim_start: false, } .fmt(f)?; } // Formatting the expression here and in the expanded version is safe **only** - // because we assert that the f-string never contains any comments. - FStringElement::Expression(expression) => { - let context = FStringContext::new( + // because we assert that the f/t-string never contains any comments. + InterpolatedStringElement::Interpolation(expression) => { + let context = InterpolatedStringContext::new( self.flags, - FStringLayout::from_f_string(f_string, f.context().source()), + InterpolatedStringLayout::from_interpolated_string_elements( + elements, + f.context().source(), + ), ); - FormatFStringExpressionElement::new(expression, context).fmt(f)?; + FormatInterpolatedElement::new(expression, context).fmt(f)?; } } } @@ -335,7 +370,7 @@ impl Format> for FormatImplicitConcatenatedStringFlat<'_> { struct FormatLiteralContent { range: TextRange, flags: AnyStringFlags, - is_fstring: bool, + is_interpolated_string: bool, trim_start: bool, trim_end: bool, } @@ -347,7 +382,7 @@ impl Format> for FormatLiteralContent { content, 0, self.flags, - self.flags.is_f_string() && !self.is_fstring, + self.flags.is_interpolated_string() && !self.is_interpolated_string, ); // Trim the start and end of the string if it's the first or last part of a docstring. diff --git a/crates/ruff_python_formatter/src/string/mod.rs b/crates/ruff_python_formatter/src/string/mod.rs index 5e2183be3a864..6f9fc5b33e1da 100644 --- a/crates/ruff_python_formatter/src/string/mod.rs +++ b/crates/ruff_python_formatter/src/string/mod.rs @@ -1,17 +1,16 @@ use memchr::memchr2; -pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer}; -use ruff_python_ast::str::{Quote, TripleQuotes}; +pub(crate) use normalize::{NormalizedString, StringNormalizer, normalize_string}; use ruff_python_ast::StringLikePart; +use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_ast::{ - self as ast, + self as ast, AnyStringFlags, StringFlags, str_prefix::{AnyStringPrefix, StringLiteralPrefix}, - AnyStringFlags, StringFlags, }; use ruff_source_file::LineRanges; use ruff_text_size::Ranged; -use crate::prelude::*; use crate::QuoteStyle; +use crate::prelude::*; pub(crate) mod docstring; pub(crate) mod implicit; @@ -86,57 +85,55 @@ pub(crate) trait StringLikeExtensions { impl StringLikeExtensions for ast::StringLike<'_> { fn is_multiline(&self, context: &PyFormatContext) -> bool { + // Helper for f-string and t-string parts + fn contains_line_break_or_comments( + elements: &ast::InterpolatedStringElements, + context: &PyFormatContext, + triple_quotes: TripleQuotes, + ) -> bool { + elements.iter().any(|element| match element { + ast::InterpolatedStringElement::Literal(literal) => { + triple_quotes.is_yes() && context.source().contains_line_break(literal.range()) + } + ast::InterpolatedStringElement::Interpolation(expression) => { + // Expressions containing comments can't be joined. + // + // Format specifiers needs to be checked as well. For example, the + // following should be considered multiline because the literal + // part of the format specifier contains a newline at the end + // (`.3f\n`): + // + // ```py + // x = f"hello {a + b + c + d:.3f + // } world" + // ``` + context.comments().contains_comments(expression.into()) + || expression.format_spec.as_deref().is_some_and(|spec| { + contains_line_break_or_comments(&spec.elements, context, triple_quotes) + }) + || expression.debug_text.as_ref().is_some_and(|debug_text| { + memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some() + || memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()).is_some() + }) + } + }) + } + self.parts().any(|part| match part { StringLikePart::String(_) | StringLikePart::Bytes(_) => { part.flags().is_triple_quoted() && context.source().contains_line_break(part.range()) } - StringLikePart::FString(f_string) => { - fn contains_line_break_or_comments( - elements: &ast::FStringElements, - context: &PyFormatContext, - triple_quotes: TripleQuotes, - ) -> bool { - elements.iter().any(|element| match element { - ast::FStringElement::Literal(literal) => { - triple_quotes.is_yes() - && context.source().contains_line_break(literal.range()) - } - ast::FStringElement::Expression(expression) => { - // Expressions containing comments can't be joined. - // - // Format specifiers needs to be checked as well. For example, the - // following should be considered multiline because the literal - // part of the format specifier contains a newline at the end - // (`.3f\n`): - // - // ```py - // x = f"hello {a + b + c + d:.3f - // } world" - // ``` - context.comments().contains_comments(expression.into()) - || expression.format_spec.as_deref().is_some_and(|spec| { - contains_line_break_or_comments( - &spec.elements, - context, - triple_quotes, - ) - }) - || expression.debug_text.as_ref().is_some_and(|debug_text| { - memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some() - || memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()) - .is_some() - }) - } - }) - } - - contains_line_break_or_comments( - &f_string.elements, - context, - f_string.flags.triple_quotes(), - ) - } + StringLikePart::FString(f_string) => contains_line_break_or_comments( + &f_string.elements, + context, + f_string.flags.triple_quotes(), + ), + StringLikePart::TString(t_string) => contains_line_break_or_comments( + &t_string.elements, + context, + t_string.flags.triple_quotes(), + ), }) } } diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index 7ebc3929053dd..b7aa0605d78d6 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -5,16 +5,15 @@ use std::iter::FusedIterator; use ruff_formatter::FormatContext; use ruff_python_ast::visitor::source_order::SourceOrderVisitor; use ruff_python_ast::{ - str::{Quote, TripleQuotes}, - AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements, FStringFlags, + AnyStringFlags, BytesLiteral, FString, InterpolatedStringElement, InterpolatedStringElements, StringFlags, StringLikePart, StringLiteral, }; use ruff_text_size::{Ranged, TextRange, TextSlice}; -use crate::context::FStringState; -use crate::prelude::*; -use crate::string::StringQuotes; use crate::QuoteStyle; +use crate::context::InterpolatedStringState; +use crate::prelude::*; +use crate::string::{Quote, StringQuotes, TripleQuotes}; pub(crate) struct StringNormalizer<'a, 'src> { preferred_quote_style: Option, @@ -47,11 +46,11 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { .unwrap_or(self.context.options().quote_style()); let supports_pep_701 = self.context.options().target_version().supports_pep_701(); - // For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't. - if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state() + // For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't. + if let InterpolatedStringState::InsideInterpolatedElement(parent_context) = + self.context.interpolated_string_state() { - let parent_flags = parent_context.f_string().flags(); - + let parent_flags = parent_context.flags(); if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() { // This logic is even necessary when using preserve and the target python version doesn't support PEP701 because // we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes @@ -67,33 +66,49 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { return QuoteStyle::Preserve; } - // There are cases where it is necessary to preserve the quotes to prevent an invalid f-string. - if let StringLikePart::FString(fstring) = string { - // There are two cases where it's necessary to preserve the quotes if the - // target version is pre 3.12 and the part is an f-string. - if !supports_pep_701 { - // An f-string expression contains a debug text with a quote character - // because the formatter will emit the debug expression **exactly** the - // same as in the source text. - if is_fstring_with_quoted_debug_expression(fstring, self.context) { - return QuoteStyle::Preserve; + // There are cases where it is necessary to preserve the quotes to prevent an invalid f-string or t-string. + match string { + StringLikePart::FString(fstring) => { + // There are two cases where it's necessary to preserve the quotes if the + // target version is pre 3.12 and the part is an f-string. + if !supports_pep_701 { + // An f-string expression contains a debug text with a quote character + // because the formatter will emit the debug expression **exactly** the + // same as in the source text. + if is_fstring_with_quoted_debug_expression(fstring, self.context) { + return QuoteStyle::Preserve; + } + + // An f-string expression that contains a triple quoted string literal + // expression that contains a quote. + if is_fstring_with_triple_quoted_literal_expression_containing_quotes( + fstring, + self.context, + ) { + return QuoteStyle::Preserve; + } } - // An f-string expression that contains a triple quoted string literal - // expression that contains a quote. - if is_fstring_with_triple_quoted_literal_expression_containing_quotes( - fstring, + // An f-string expression element contains a debug text and the corresponding + // format specifier has a literal element with a quote character. + if is_interpolated_string_with_quoted_format_spec_and_debug( + &fstring.elements, + fstring.flags.into(), self.context, ) { return QuoteStyle::Preserve; } } - - // An f-string expression element contains a debug text and the corresponding - // format specifier has a literal element with a quote character. - if is_fstring_with_quoted_format_spec_and_debug(fstring, self.context) { - return QuoteStyle::Preserve; + StringLikePart::TString(tstring) => { + if is_interpolated_string_with_quoted_format_spec_and_debug( + &tstring.elements, + tstring.flags.into(), + self.context, + ) { + return QuoteStyle::Preserve; + } } + _ => {} } // Per PEP 8, always prefer double quotes for triple-quoted strings. @@ -172,7 +187,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { // The preferred quote style is single or double quotes, and the string contains a quote or // another character that may require escaping (Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => { - let metadata = if string.is_fstring() { + let metadata = if string.is_interpolated_string() { QuoteMetadata::from_part(string, self.context, preferred_quote) } else { QuoteMetadata::from_str( @@ -262,9 +277,19 @@ impl QuoteMetadata { StringLikePart::FString(fstring) => { let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote); - metadata.merge_fstring_elements( + metadata.merge_interpolated_string_elements( &fstring.elements, - fstring.flags, + fstring.flags.into(), + context, + preferred_quote, + ) + } + StringLikePart::TString(tstring) => { + let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote); + + metadata.merge_interpolated_string_elements( + &tstring.elements, + tstring.flags.into(), context, preferred_quote, ) @@ -369,7 +394,7 @@ impl QuoteMetadata { }) } - /// For f-strings, only consider the quotes inside string-literals but ignore + /// For f-strings and t-strings, only consider the quotes inside string-literals but ignore /// quotes inside expressions (except inside the format spec). This allows both the outer and the nested literals /// to make the optimal local-choice to reduce the total number of quotes necessary. /// This doesn't require any pre 312 special handling because an expression @@ -377,10 +402,10 @@ impl QuoteMetadata { /// ```python /// f"{'escaping a quote like this \" is a syntax error pre 312'}" /// ``` - fn merge_fstring_elements( + fn merge_interpolated_string_elements( self, - elements: &FStringElements, - flags: FStringFlags, + elements: &InterpolatedStringElements, + flags: AnyStringFlags, context: &PyFormatContext, preferred_quote: Quote, ) -> Self { @@ -388,19 +413,19 @@ impl QuoteMetadata { for element in elements { match element { - FStringElement::Literal(literal) => { + InterpolatedStringElement::Literal(literal) => { merged = merged .merge(&QuoteMetadata::from_str( context.source().slice(literal), - flags.into(), + flags, preferred_quote, )) .expect("Merge to succeed because all parts have the same flags"); } - FStringElement::Expression(expression) => { + InterpolatedStringElement::Interpolation(expression) => { if let Some(spec) = expression.format_spec.as_deref() { if expression.debug_text.is_none() { - merged = merged.merge_fstring_elements( + merged = merged.merge_interpolated_string_elements( &spec.elements, flags, context, @@ -691,7 +716,6 @@ pub(crate) fn normalize_string( } if !new_flags.is_triple_quoted() { - #[allow(clippy::if_same_then_else)] if next == opposite_quote { // Remove the escape by ending before the backslash and starting again with the quote chars.next(); @@ -880,7 +904,7 @@ pub(super) fn is_fstring_with_quoted_debug_expression( fstring: &FString, context: &PyFormatContext, ) -> bool { - fstring.elements.expressions().any(|expression| { + fstring.elements.interpolations().any(|expression| { if expression.debug_text.is_some() { let content = context.source().slice(expression); contains_opposite_quote(content, fstring.flags.into()) @@ -890,58 +914,6 @@ pub(super) fn is_fstring_with_quoted_debug_expression( }) } -/// Returns `true` if `string` has any f-string expression element (direct or nested) with a debug expression and a format spec -/// that contains the opposite quote. It's important to preserve the quote style for those f-strings -/// because changing the quote style would result in invalid syntax. -/// -/// ```python -/// f'{1=: "abcd \'\'}' -/// f'{x=:a{y:"abcd"}}' -/// f'{x=:a{y:{z:"abcd"}}}' -/// ``` -pub(super) fn is_fstring_with_quoted_format_spec_and_debug( - fstring: &FString, - context: &PyFormatContext, -) -> bool { - fn has_format_spec_with_opposite_quote( - elements: &FStringElements, - flags: FStringFlags, - context: &PyFormatContext, - in_debug: bool, - ) -> bool { - elements.iter().any(|element| match element { - FStringElement::Literal(literal) => { - let content = context.source().slice(literal); - - in_debug && contains_opposite_quote(content, flags.into()) - } - FStringElement::Expression(expression) => { - expression.format_spec.as_deref().is_some_and(|spec| { - has_format_spec_with_opposite_quote( - &spec.elements, - flags, - context, - in_debug || expression.debug_text.is_some(), - ) - }) - } - }) - } - - fstring.elements.expressions().any(|expression| { - if let Some(spec) = expression.format_spec.as_deref() { - return has_format_spec_with_opposite_quote( - &spec.elements, - fstring.flags, - context, - expression.debug_text.is_some(), - ); - } - - false - }) -} - /// Tests if the `fstring` contains any triple quoted string, byte, or f-string literal that /// contains a quote character opposite to its own quote character. /// @@ -981,6 +953,17 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes } } + contains_quotes + } + StringLikePart::TString(tstring) => { + let mut contains_quotes = false; + for literal in tstring.elements.literals() { + if self.contains_quote(literal.range(), tstring.flags.into()) { + contains_quotes = true; + break; + } + } + contains_quotes } }; @@ -1019,6 +1002,59 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes visitor.found } +/// Returns `true` if `string` has any f/t-string interpolation element (direct or nested) with a debug expression and a format spec +/// that contains the opposite quote. It's important to preserve the quote style for those f/t-strings +/// because changing the quote style would result in invalid syntax. +/// +/// ```python +/// t'{1=: "abcd \'\'}' +/// t'{x=:a{y:"abcd"}}' +/// t'{x=:a{y:{z:"abcd"}}}' +/// ``` +pub(super) fn is_interpolated_string_with_quoted_format_spec_and_debug( + elements: &InterpolatedStringElements, + flags: AnyStringFlags, + context: &PyFormatContext, +) -> bool { + fn has_format_spec_with_opposite_quote( + elements: &InterpolatedStringElements, + flags: AnyStringFlags, + context: &PyFormatContext, + in_debug: bool, + ) -> bool { + elements.iter().any(|element| match element { + InterpolatedStringElement::Literal(literal) => { + let content = context.source().slice(literal); + + in_debug && contains_opposite_quote(content, flags) + } + InterpolatedStringElement::Interpolation(expression) => { + expression.format_spec.as_deref().is_some_and(|spec| { + has_format_spec_with_opposite_quote( + &spec.elements, + flags, + context, + in_debug || expression.debug_text.is_some(), + ) + }) + } + }) + } + + elements.interpolations().any(|expression| { + if let Some(spec) = expression.format_spec.as_deref() { + return has_format_spec_with_opposite_quote( + &spec.elements, + flags, + context, + expression.debug_text.is_some(), + ); + } + + false + }) +} + fn contains_opposite_quote(content: &str, flags: AnyStringFlags) -> bool { if flags.is_triple_quoted() { match flags.quote_style() { @@ -1058,9 +1094,9 @@ mod tests { use std::borrow::Cow; use ruff_python_ast::{ + AnyStringFlags, str::{Quote, TripleQuotes}, str_prefix::{AnyStringPrefix, ByteStringPrefix}, - AnyStringFlags, }; use crate::string::normalize_string; diff --git a/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs b/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs index 6f9f5e5a72005..4a33ed0684056 100644 --- a/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs +++ b/crates/ruff_python_formatter/src/type_param/type_param_param_spec.rs @@ -10,6 +10,7 @@ impl FormatNodeRule for FormatTypeParamParamSpec { fn fmt_fields(&self, item: &TypeParamParamSpec, f: &mut PyFormatter) -> FormatResult<()> { let TypeParamParamSpec { range: _, + node_index: _, name, default, } = item; diff --git a/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs b/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs index 294498326714c..c18032b795933 100644 --- a/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs +++ b/crates/ruff_python_formatter/src/type_param/type_param_type_var.rs @@ -10,6 +10,7 @@ impl FormatNodeRule for FormatTypeParamTypeVar { fn fmt_fields(&self, item: &TypeParamTypeVar, f: &mut PyFormatter) -> FormatResult<()> { let TypeParamTypeVar { range: _, + node_index: _, name, bound, default, diff --git a/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs b/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs index 30ab85e9001ff..4b50da7704eb0 100644 --- a/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs +++ b/crates/ruff_python_formatter/src/type_param/type_param_type_var_tuple.rs @@ -10,6 +10,7 @@ impl FormatNodeRule for FormatTypeParamTypeVarTuple { fn fmt_fields(&self, item: &TypeParamTypeVarTuple, f: &mut PyFormatter) -> FormatResult<()> { let TypeParamTypeVarTuple { range: _, + node_index: _, name, default, } = item; diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index 2f60a7abc81b5..98022572485d9 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::iter::FusedIterator; use std::slice::Iter; -use ruff_formatter::{write, FormatError}; +use ruff_formatter::{FormatError, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::Stmt; use ruff_python_parser::{self as parser, TokenKind}; @@ -11,7 +11,7 @@ use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::comments::format::{empty_lines, format_comment}; -use crate::comments::{leading_comments, trailing_comments, SourceComment}; +use crate::comments::{SourceComment, leading_comments, trailing_comments}; use crate::prelude::*; use crate::statement::clause::ClauseHeader; use crate::statement::suite::SuiteChildStatement; @@ -487,7 +487,7 @@ enum SuppressionComments<'a> { /// Comments that all fall into the formatted range. Formatted { - #[allow(unused)] + #[expect(unused)] comments: &'a [SourceComment], }, } @@ -798,13 +798,13 @@ impl Iterator for LogicalLinesIter<'_> { Some(token) if token.kind() == TokenKind::Unknown => { return Some(Err(FormatError::syntax_error( "Unexpected token when lexing verbatim statement range.", - ))) + ))); } Some(token) => match token.kind() { TokenKind::Newline => break (token.start(), token.end()), // Ignore if inside an expression TokenKind::NonLogicalNewline if parens == 0 => { - break (token.start(), token.end()) + break (token.start(), token.end()); } TokenKind::Lbrace | TokenKind::Lpar | TokenKind::Lsqb => { parens = parens.saturating_add(1); diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 7e83d969cb9c0..49b207621f685 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -2,8 +2,8 @@ use crate::normalizer::Normalizer; use itertools::Itertools; use ruff_formatter::FormatOptions; use ruff_python_ast::comparable::ComparableMod; -use ruff_python_formatter::{format_module_source, format_range, PreviewMode, PyFormatOptions}; -use ruff_python_parser::{parse, ParseOptions, UnsupportedSyntaxError}; +use ruff_python_formatter::{PreviewMode, PyFormatOptions, format_module_source, format_range}; +use ruff_python_parser::{ParseOptions, UnsupportedSyntaxError, parse}; use ruff_source_file::{LineIndex, OneIndexed}; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashMap; @@ -324,7 +324,12 @@ fn format_file(source: &str, options: &PyFormatOptions, input_path: &Path) -> St (Cow::Owned(without_markers), content) } else { - let printed = format_module_source(source, options.clone()).expect("Formatting to succeed"); + let printed = format_module_source(source, options.clone()).unwrap_or_else(|err| { + panic!( + "Formatting `{input_path} was expected to succeed but it failed: {err}", + input_path = input_path.display() + ) + }); let formatted_code = printed.into_code(); ensure_stability_when_formatting_twice(&formatted_code, options, input_path); @@ -430,10 +435,10 @@ fn ensure_unchanged_ast( formatted_unsupported_syntax_errors .into_values() .map(|error| { - let location = index.source_location(error.start(), formatted_code); + let location = index.line_column(error.start(), formatted_code); format!( "{row}:{col} {error}", - row = location.row, + row = location.line, col = location.column ) }) diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index a2fac515cf33d..e5237f8c5f285 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -4,12 +4,12 @@ use { regex::Regex, }; -use ruff_python_ast::visitor::transformer::Transformer; use ruff_python_ast::{ - self as ast, BytesLiteralFlags, Expr, FStringElement, FStringFlags, FStringLiteralElement, - FStringPart, Stmt, StringFlags, + self as ast, BytesLiteralFlags, Expr, FStringFlags, FStringPart, InterpolatedStringElement, + InterpolatedStringLiteralElement, Stmt, StringFlags, }; -use ruff_python_ast::{visitor::transformer, StringLiteralFlags}; +use ruff_python_ast::{AtomicNodeIndex, visitor::transformer::Transformer}; +use ruff_python_ast::{StringLiteralFlags, visitor::transformer}; use ruff_text_size::{Ranged, TextRange}; /// A struct to normalize AST nodes for the purpose of comparing formatted representations for @@ -82,6 +82,7 @@ impl Transformer for Normalizer { value: Box::from(string.value.to_str()), range: string.range, flags: StringLiteralFlags::empty(), + node_index: AtomicNodeIndex::dummy(), }); } } @@ -98,6 +99,7 @@ impl Transformer for Normalizer { value: bytes.value.bytes().collect(), range: bytes.range, flags: BytesLiteralFlags::empty(), + node_index: AtomicNodeIndex::dummy(), }); } } @@ -117,7 +119,7 @@ impl Transformer for Normalizer { if can_join { #[derive(Default)] struct Collector { - elements: Vec, + elements: Vec, } impl Collector { @@ -127,7 +129,7 @@ impl Transformer for Normalizer { // `elements` vector, while subsequent strings // are concatenated onto this top string. fn push_literal(&mut self, literal: &str, range: TextRange) { - if let Some(FStringElement::Literal(existing_literal)) = + if let Some(InterpolatedStringElement::Literal(existing_literal)) = self.elements.last_mut() { let value = std::mem::take(&mut existing_literal.value); @@ -137,20 +139,19 @@ impl Transformer for Normalizer { existing_literal.range = TextRange::new(existing_literal.start(), range.end()); } else { - self.elements.push(FStringElement::Literal( - FStringLiteralElement { + self.elements.push(InterpolatedStringElement::Literal( + InterpolatedStringLiteralElement { range, value: literal.into(), + node_index: AtomicNodeIndex::dummy(), }, )); } } - fn push_expression( - &mut self, - expression: ast::FStringExpressionElement, - ) { - self.elements.push(FStringElement::Expression(expression)); + fn push_expression(&mut self, expression: ast::InterpolatedElement) { + self.elements + .push(InterpolatedStringElement::Interpolation(expression)); } } @@ -165,11 +166,13 @@ impl Transformer for Normalizer { ast::FStringPart::FString(fstring) => { for element in &fstring.elements { match element { - ast::FStringElement::Literal(literal) => { + ast::InterpolatedStringElement::Literal(literal) => { collector .push_literal(&literal.value, literal.range); } - ast::FStringElement::Expression(expression) => { + ast::InterpolatedStringElement::Interpolation( + expression, + ) => { collector.push_expression(expression.clone()); } } @@ -182,6 +185,7 @@ impl Transformer for Normalizer { elements: collector.elements.into(), range: fstring.range, flags: FStringFlags::empty(), + node_index: AtomicNodeIndex::dummy(), }); } } @@ -192,6 +196,24 @@ impl Transformer for Normalizer { transformer::walk_expr(self, expr); } + fn visit_interpolated_string_element( + &self, + interpolated_string_element: &mut InterpolatedStringElement, + ) { + let InterpolatedStringElement::Interpolation(interpolation) = interpolated_string_element + else { + return; + }; + + let Some(debug) = &mut interpolation.debug_text else { + return; + }; + + // Changing the newlines to the configured newline is okay because Python normalizes all newlines to `\n` + debug.leading = debug.leading.replace("\r\n", "\n").replace('\r', "\n"); + debug.trailing = debug.trailing.replace("\r\n", "\n").replace('\r', "\n"); + } + fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) { static STRIP_DOC_TESTS: LazyLock = LazyLock::new(|| { Regex::new( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pattern_matching_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pattern_matching_trailing_comma.py.snap index fa13412b560d1..79f1cf71b49fe 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pattern_matching_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pattern_matching_trailing_comma.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/pattern_matching_trailing_comma.py -snapshot_kind: text --- ## Input @@ -27,7 +26,7 @@ match more := (than, one), indeed,: ```diff --- Black +++ Ruff -@@ -8,13 +8,16 @@ +@@ -8,7 +8,10 @@ pass @@ -38,15 +37,7 @@ match more := (than, one), indeed,: +): case _, (5, 6): pass -- case ( -+ case [ - [[5], (6)], - [7], -- ): -+ ]: - pass - case _: - pass + case ( ``` ## Ruff Output @@ -68,10 +59,10 @@ match ( ): case _, (5, 6): pass - case [ + case ( [[5], (6)], [7], - ]: + ): pass case _: pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap index 1b074e6b45c67..63d6544faaf4b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py -snapshot_kind: text --- ## Input @@ -82,8 +81,7 @@ x = f"a{2+2:=^{foo(x+y**2):something else}one more}b" f'{(abc:=10)}' f"This is a really long string, but just make sure that you reflow fstrings { - 2+2:d -}" + 2+2:d}" f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}" f"{2+2=}" @@ -170,7 +168,7 @@ rf"\{"a"}" x = """foo {{ {2 + 2}bar baz""" -@@ -28,74 +26,62 @@ +@@ -28,55 +26,48 @@ x = f"""foo {{ {2 + 2}bar {{ baz""" @@ -242,12 +240,7 @@ rf"\{"a"}" f"{2+2=}" f"{2+2 = }" - f"{ 2 + 2 = }" - --f"""foo { -- datetime.datetime.now():%Y -+f"""foo {datetime.datetime.now():%Y - %m +@@ -88,14 +79,10 @@ %d }""" @@ -264,7 +257,7 @@ rf"\{"a"}" ) f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \ -@@ -105,8 +91,10 @@ +@@ -105,8 +92,10 @@ rf"\{{\}}" f""" @@ -277,7 +270,7 @@ rf"\{"a"}" """ value: str = f"""foo -@@ -124,13 +112,15 @@ +@@ -124,13 +113,15 @@ f'{{\\"kind\\":\\"ConfigMap\\",\\"metadata\\":{{\\"annotations\\":{{}},\\"name\\":\\"cluster-info\\",\\"namespace\\":\\"amazon-cloudwatch\\"}}}}' @@ -378,7 +371,8 @@ f"{2+2=}" f"{2+2 = }" f"{ 2 + 2 = }" -f"""foo {datetime.datetime.now():%Y +f"""foo { + datetime.datetime.now():%Y %m %d }""" diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 9a5b9d1882001..a5f34628115c6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -437,6 +437,19 @@ f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) aaaaaaaaaaa = f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) + +# This t-string should be flattened +xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) + +# This is not a multiline t-string, but the expression is too long so it should be +# wrapped in parentheses. +t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +aaaaaaaaaaa = t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) ``` ## Output @@ -927,4 +940,22 @@ aaaaaaaaaaa = ( worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) ) + +# This t-string should be flattened +xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {expression} bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) + +# This is not a multiline t-string, but the expression is too long so it should be +# wrapped in parentheses. +( + t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +) +aaaaaaaaaaa = ( + t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap index b357a01b9f6cb..c929f160dff59 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap @@ -248,18 +248,22 @@ f"{ # comment 15 }" # comment 19 # comment 20 -# Single-quoted f-strings with a format specificer can be multiline -f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - variable:.3f} ddddddddddddddd eeeeeeee" - -# But, if it's triple-quoted then we can't or the format specificer will have a -# trailing newline -f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - variable:.3f} ddddddddddddddd eeeeeeee""" +# The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. +f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable +:.3f} ddddddddddddddd eeeeeeee" + +# The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable + :.3f} ddddddddddddddd eeeeeeee""" +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f +} ddddddddddddddd eeeeeeee""" -# But, we can break the ones which don't have a format specifier -f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr { - xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb""" +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd + # comment + :.3f} cccccccccc""" # Throw in a random comment in it but surprise, this is not a comment but just a text # which is part of the format specifier @@ -273,27 +277,28 @@ aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { # Conversion flags # -# This is not a valid Python code because of the additional whitespace between the `!` -# and conversion type. But, our parser isn't strict about this. This should probably be -# removed once we have a strict parser. -x = f"aaaaaaaaa { x ! r }" # Even in the case of debug expressions, we only need to preserve the whitespace within # the expression part of the replacement field. -x = f"aaaaaaaaa { x = ! r }" +x = f"aaaaaaaaa { x = !r }" # Combine conversion flags with format specifiers -x = f"{x = ! s - :>0 - - }" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = f"{x !s - :>0 - # comment 21 - }" +x = f"{x = !s + :>0}" + +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + x = f""" { # comment 22 @@ -310,14 +315,28 @@ x = f"""{"foo " + # comment 24 """ # Mix of various features. -f"{ # comment 26 +f"""{ # comment 26 foo # after foo :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" + + +f"""{foo + :a{ + a # comment 29 + # comment 30 + } +}""" + +# Regression test for https://github.com/astral-sh/ruff/issues/18672 +f"{ + # comment 31 + foo + :>}" # Assignment statement @@ -471,13 +490,11 @@ aaaaa[aaaaaaaaaaa] = ( # This is not a multiline f-string even though it has a newline after the format specifier. aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment aaaaaaaaaaaaaaaaaa = ( f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ) # The newline is only considered when it's a tripled-quoted f-string. @@ -1012,34 +1029,36 @@ f"{ # comment 15 }" # comment 19 # comment 20 -# Single-quoted f-strings with a format specificer can be multiline +# The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - variable:.3f -} ddddddddddddddd eeeeeeee" + variable:.3f} ddddddddddddddd eeeeeeee" -# But, if it's triple-quoted then we can't or the format specificer will have a -# trailing newline -f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee""" +# The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee""" +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f +} ddddddddddddddd eeeeeeee""" -# But, we can break the ones which don't have a format specifier -f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { - xxxxxxxxxxxxxxxxxxxx -} bbbbbbbbbbbb""" +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd + # comment + :.3f} cccccccccc""" # Throw in a random comment in it but surprise, this is not a comment but just a text # which is part of the format specifier -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment } cccccccccc""" -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment} cccccccccc""" # Conversion flags # -# This is not a valid Python code because of the additional whitespace between the `!` -# and conversion type. But, our parser isn't strict about this. This should probably be -# removed once we have a strict parser. -x = f"aaaaaaaaa {x!r}" # Even in the case of debug expressions, we only need to preserve the whitespace within # the expression part of the replacement field. @@ -1047,13 +1066,22 @@ x = f"aaaaaaaaa { x = !r}" # Combine conversion flags with format specifiers x = f"{x = !s:>0}" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. + x = f"{ - x!s:>0 - # comment 21 -}" + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{ + 1 + # comment 21-3 + :}" + +f"{ + 1 # comment 21-4 + :} a" + x = f""" { # comment 22 @@ -1071,13 +1099,27 @@ x = f"""{ """ # Mix of various features. -f"{ # comment 26 - foo:>{ # after foo +f"""{ # comment 26 + foo # after foo + :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" + + +f"""{ + foo:a{ + a # comment 29 + # comment 30 + } +}""" + +# Regression test for https://github.com/astral-sh/ruff/issues/18672 +f"{ + # comment 31 + foo:>}" # Assignment statement @@ -1244,10 +1286,12 @@ aaaaaaaaaaaaaaaaaa = ( ) # The newline is only considered when it's a tripled-quoted f-string. -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment # Remove the parentheses here @@ -1812,34 +1856,36 @@ f"{ # comment 15 }" # comment 19 # comment 20 -# Single-quoted f-strings with a format specificer can be multiline +# The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { - variable:.3f -} ddddddddddddddd eeeeeeee" + variable:.3f} ddddddddddddddd eeeeeeee" -# But, if it's triple-quoted then we can't or the format specificer will have a -# trailing newline -f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee""" +# The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee""" +f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f +} ddddddddddddddd eeeeeeee""" -# But, we can break the ones which don't have a format specifier -f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { - xxxxxxxxxxxxxxxxxxxx -} bbbbbbbbbbbb""" +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd + # comment + :.3f} cccccccccc""" # Throw in a random comment in it but surprise, this is not a comment but just a text # which is part of the format specifier -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment } cccccccccc""" -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment} cccccccccc""" # Conversion flags # -# This is not a valid Python code because of the additional whitespace between the `!` -# and conversion type. But, our parser isn't strict about this. This should probably be -# removed once we have a strict parser. -x = f"aaaaaaaaa {x!r}" # Even in the case of debug expressions, we only need to preserve the whitespace within # the expression part of the replacement field. @@ -1847,13 +1893,22 @@ x = f"aaaaaaaaa { x = !r}" # Combine conversion flags with format specifiers x = f"{x = !s:>0}" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. + x = f"{ - x!s:>0 - # comment 21 -}" + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{ + 1 + # comment 21-3 + :}" + +f"{ + 1 # comment 21-4 + :} a" + x = f""" { # comment 22 @@ -1871,13 +1926,27 @@ x = f"""{ """ # Mix of various features. -f"{ # comment 26 - foo:>{ # after foo +f"""{ # comment 26 + foo # after foo + :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" + + +f"""{ + foo:a{ + a # comment 29 + # comment 30 + } +}""" + +# Regression test for https://github.com/astral-sh/ruff/issues/18672 +f"{ + # comment 31 + foo:>}" # Assignment statement @@ -2044,10 +2113,12 @@ aaaaaaaaaaaaaaaaaa = ( ) # The newline is only considered when it's a tripled-quoted f-string. -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment # Remove the parentheses here diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap index c2e7f51ca16cc..6c577df6bb73e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap @@ -106,6 +106,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}" f"{10 + len('bar')=}" f'{10 + len("bar")=}' +############################################################################## +# T-strings +############################################################################## + +# Escape `{` and `}` when merging a t-string with a string +"a {not_a_variable}" t"b {10}" "c" + +# Join, and break expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{ +expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +# Join, but don't break the expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +t"test{ +expression +}flat" t"can be { +joined +} together" + +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style +t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes + +# Different triple quoted strings +t"{'''test'''}" t'{"""other"""}' + +# Now with inner quotes +t"{'''test ' '''}" t'{"""other " """}' +t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}' +t"{b'''test ' '''}" t'{b"""other " """}' +t"{t'''test ' '''}" t'{t"""other " """}' + +# debug expressions containing quotes +t"{10 + len('bar')=}" t"{10 + len('bar')=}" +t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}" + +# We can't safely merge this pre Python 3.12 without altering the debug expression. +t"{10 + len('bar')=}" t'{10 + len("bar")=}' + + ############################################################################## # Don't join raw strings ############################################################################## @@ -116,6 +165,9 @@ R"a" "normal" f"test" fr"test" f"test" fR"test" +t"test" tr"test" +t"test" tR"test" + ############################################################################## # Don't join triple quoted strings @@ -125,9 +177,22 @@ f"test" fR"test" "single" f""""single""" +"single" t""""single""" + b"single" b"""triple""" +############################################################################## +# Don't join t-strings and f-strings +############################################################################## + +t"{interp}" f"{expr}" + +f"{expr}" t"{interp}" + +f"{expr}" "string" t"{interp}" + + ############################################################################## # Join strings in with statements ############################################################################## @@ -452,6 +517,50 @@ f"{10 + len('bar')=}no debug{10}{10 + len('bar')=}" f"{10 + len('bar')=}" f'{10 + len("bar")=}' +############################################################################## +# T-strings +############################################################################## + +# Escape `{` and `}` when merging a t-string with a string +t"a {{not_a_variable}}b {10}c" + +# Join, and break expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more" + +# Join, but don't break the expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more" + +t"test{expression}flatcan be {joined} together" + +aaaaaaaaaaa = ( + t"test{expression}flat" + t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + + +t"single quoted '{x}'double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t'single quote \' {x}double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}'double quote \" {x}\"" # More single quotes => use double quotes + +# Different triple quoted strings +t"{'''test'''}{'''other'''}" + +# Now with inner quotes +t"{'''test ' '''}{'''other " '''}" +t"{some_where_nested('''test ' ''')}{'''other " ''' + 'more'}" +t"{b'''test ' '''}{b'''other " '''}" +t"{t'''test ' '''}{t'''other " '''}" + +# debug expressions containing quotes +t"{10 + len('bar')=}{10 + len('bar')=}" +t"{10 + len('bar')=}no debug{10}{10 + len('bar')=}" + +# We can't safely merge this pre Python 3.12 without altering the debug expression. +t"{10 + len('bar')=}{10 + len("bar")=}" + + ############################################################################## # Don't join raw strings ############################################################################## @@ -462,6 +571,9 @@ R"a" "normal" f"test" rf"test" f"test" Rf"test" +t"test" rt"test" +t"test" Rt"test" + ############################################################################## # Don't join triple quoted strings @@ -471,9 +583,22 @@ f"test" Rf"test" "single" f""""single""" +"single" t""""single""" + b"single" b"""triple""" +############################################################################## +# Don't join t-strings and f-strings +############################################################################## + +t"{interp}" f"{expr}" + +f"{expr}" t"{interp}" + +f"{expr}" "string" t"{interp}" + + ############################################################################## # Join strings in with statements ############################################################################## @@ -780,7 +905,7 @@ f"aaaaaaaaaaaaaaaa \ ```diff --- Stable +++ Preview -@@ -242,9 +242,12 @@ +@@ -302,9 +302,12 @@ ############################################################################## # Use can_omit_optional_parentheses layout to avoid an instability where the formatter # picks the can_omit_optional_parentheses layout when the strings are joined. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap index 7133eb0b8c117..c5237dcb54a02 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap @@ -299,6 +299,155 @@ aaaaa[aaaaaaaaaaa] = ( ) +############################################################# +# T-Strings +############################################################# + +# Flatten and join the t-string +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline + +# Parenthesize the value and join it, inline the comment +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +# The target splits because of a magic trailing comma +# The string is joined and not parenthesized because it just fits into the line length (including comment). +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Same but starting with a joined string. They should both result in the same formatting. +[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + +# The target splits because of the magic trailing comma +# The string is **not** joined because it with the inlined comment exceeds the line length limit. +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# The target should be flat +# The string should be joined because it fits into the line length +a[ + aaaaaaa, + b +] = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccc" # comment +) + +# Same but starting with a joined string. They should both result in the same formatting. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccc" # comment + +# The target should be flat +# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Split an overlong target, but join the string if it fits +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment +) + +# Split both if necessary and keep multiline +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment +) + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions: +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + + # Trailing last-part comments a = ( @@ -380,7 +529,8 @@ self._attr_unique_id = ( return ( f"Exception in {call_back_name} when handling msg on " f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe] -)``` +) +``` ## Output ```python @@ -704,6 +854,172 @@ aaaaa[aaaaaaaaaaa] = ( ) +############################################################# +# T-Strings +############################################################# + +# Flatten and join the t-string +aaaaaaaaaaa = t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline + +# Parenthesize the value and join it, inline the comment +aaaaaaaaaaa = ( + t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline +) + +# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment +aaaaaaaaaaa = ( + t"test{expression}flat" + t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + + +# The target splits because of a magic trailing comma +# The string is joined and not parenthesized because it just fits into the line length (including comment). +a[ + aaaaaaa, + b, +] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Same but starting with a joined string. They should both result in the same formatting. +[ + aaaaaaa, + b, +] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + +# The target splits because of the magic trailing comma +# The string is **not** joined because it with the inlined comment exceeds the line length limit. +a[ + aaaaaaa, + b, +] = ( + t"ccccc{expression}cccccccccccccccccccc" + t"cccccccccccccccccccccccccccccccccccccccccc" +) # comment + + +# The target should be flat +# The string should be joined because it fits into the line length +a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment + +# Same but starting with a joined string. They should both result in the same formatting. +a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment + +# The target should be flat +# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit. +a[aaaaaaa, b] = ( + t"ccccc{expression}ccccccccccc" + "ccccccccccccccccccccccccccccccccccccccccccc" +) # comment + + +# Split an overlong target, but join the string if it fits +a[ + aaaaaaa, b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccc" # comment +) + +# Split both if necessary and keep multiline +a[ + aaaaaaa, b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{expression}cccccccccccccccccccccccccccccccc" + "ccccccccccccccccccccccccccccccc" +) # comment + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" +) # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" +) # comment + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] + }" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] + }" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions: +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + + # Trailing last-part comments a = ( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap new file mode 100644 index 0000000000000..3352f2e85f5b7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap @@ -0,0 +1,1545 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py +--- +## Input +```python +( + t'{one}' + t'{two}' +) + + +rt"Not-so-tricky \"quote" + +# Regression test for tstrings dropping comments +result_f = ( + 'Traceback (most recent call last):\n' + t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + # It also varies depending on the platform (stack size) + # Fortunately, we don't care about exactness here, so we use regex + r' \[Previous line repeated (\d+) more times\]' '\n' + 'RecursionError: maximum recursion depth exceeded\n' +) + + +# Regression for tstring dropping comments that were accidentally attached to +# an expression inside a formatted value +( + t'{1}' + # comment 1 + '' +) + +( + t'{1}' # comment 2 + t'{2}' +) + +( + t'{1}' + t'{2}' # comment 3 +) + +( + 1, ( # comment 4 + t'{2}' + ) +) + +( + ( + t'{1}' + # comment 5 + ), + 2 +) + +# https://github.com/astral-sh/ruff/issues/6841 +x = t'''a{""}b''' +y = t'''c{1}d"""e''' +z = t'''a{""}b''' t'''c{1}d"""e''' + +# T-String formatting test cases (Preview) + +# Simple expression with a mix of debug expression and comments. +x = t"{a}" +x = t"{ + a = }" +x = t"{ # comment 6 + a }" +x = t"{ # comment 7 + a = }" + +# Remove the parentheses as adding them doesn't make then fit within the line length limit. +# This is similar to how we format it before t-string formatting. +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc" +) +# Here, we would use the best fit layout to put the t-string indented on the next line +# similar to the next example. +aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +) + +# This should never add the optional parentheses because even after adding them, the +# t-string exceeds the line length limit. +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc" + +# Multiple larger expressions which exceeds the line length limit. Here, we need to decide +# whether to split at the first or second expression. This should work similarly to the +# assignment statement formatting where we split from right to left in preview mode. +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" + +# The above example won't split but when we start introducing line breaks: +x = t"aaaaaaaaaaaa { + bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { + ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd + } eeeeeeeeeeeeee" + +# But, in case comments are present, we would split at the expression containing the +# comments: +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10 + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { # comment 11 + ddddddddddddddd } eeeeeeeeeeeeee" + +# Here, the expression part itself starts with a curly brace so we need to add an extra +# space between the opening curly brace and the expression. +x = t"{ {'x': 1, 'y': 2} }" +# Although the extra space isn't required before the ending curly brace, we add it for +# consistency. +x = t"{ {'x': 1, 'y': 2}}" +x = t"{ {'x': 1, 'y': 2} = }" +x = t"{ # comment 12 + {'x': 1, 'y': 2} }" +x = t"{ # comment 13 + {'x': 1, 'y': 2} = }" +# But, if there's a format specifier or a conversion flag then we don't need to add +# any whitespace at the end +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb" +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" + +# But, in this case, we would split the expression itself because it exceeds the line +# length limit so we need not add the extra space. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} +}" +# And, split the expression itself because it exceeds the line length. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +############################################################################################# +# Quotes +############################################################################################# +t"foo 'bar' {x}" +t"foo \"bar\" {x}" +t'foo "bar" {x}' +t'foo \'bar\' {x}' +t"foo {"bar"}" + +t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes +t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes + +fr"single quotes ' {x}" # Keep double because `'` can't be escaped +fr'double quotes " {x}' # Keep single because `"` can't be escaped +fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes. + +# Here, the formatter will remove the escapes +t"foo {'\'bar\''}" +t"foo {'\"bar\"'}" + +# Quotes inside the expressions have no impact on the quote selection of the outer string. +# Required so that the following two examples result in the same formatting. +t'foo {10 + len("bar")}' +t"foo {10 + len('bar')}" + +# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression +t'foo {10 + len("bar")=}' +t'''foo {10 + len('''bar''')=}''' +t'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes + +# Triple-quoted strings +# It's ok to use the same quote char for the inner string if it's single-quoted. +t"""test {'inner'}""" +t"""test {"inner"}""" +# But if the inner string is also triple-quoted then we should preserve the existing quotes. +t"""test {'''inner'''}""" + +# It's not okay to change the quote style if the inner string is triple quoted and contains a quote. +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + +t"""test {t'inner {'''inner inner'''}'}""" +t"""test {t'''inner {"""inner inner"""}'''}""" + +# Magic trailing comma +# +# The expression formatting will result in breaking it across multiple lines with a +# trailing comma but as the expression isn't already broken, we will remove all the line +# breaks which results in the trailing comma being present. This test case makes sure +# that the trailing comma is removed as well. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# And, if the trailing comma is already present, we still need to remove it. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa" + +# Keep this Multiline by breaking it at the square brackets. +t"""aaaaaa {[ + xxxxxxxx, + yyyyyyyy, +]} ccc""" + +# Add the magic trailing comma because the elements don't fit within the line length limit +# when collapsed. +t"aaaaaa {[ + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + yyyyyyyyyyyy +]} ccccccc" + +# Remove the parentheses because they aren't required +xxxxxxxxxxxxxxx = ( + t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb { + xxxxxxxxxxx # comment 14 + + yyyyyyyyyy + } dddddddddd" +) + +# Comments + +# No comments should be dropped! +t"{ # comment 15 + # comment 16 + foo # comment 17 + # comment 18 +}" # comment 19 +# comment 20 + +# The specifier of a t-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. +t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable + :.3f} ddddddddddddddd eeeeeeee" + +# The same applies for triple quoted t-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable + :.3f} ddddddddddddddd eeeeeeee""" +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable + :.3f +} ddddddddddddddd eeeeeeee""" + + +# Throw in a random comment in it but surprise, this is not a comment but just a text +# which is part of the format specifier +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment +} cccccccccc""" +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment} cccccccccc""" + +# Conversion flags +# + +# Even in the case of debug expressions, we only need to preserve the whitespace within +# the expression part of the replacement field. +x = t"aaaaaaaaa { x = !r }" + +# Combine conversion flags with format specifiers +x = t"{x = !s + :>0}" + +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + +x = t""" +{ # comment 22 + x = :.0{y # comment 23 + }f}""" + +# Here, the debug expression is in a nested t-string so we should start preserving +# whitespaces from that point onwards. This means we should format the outer t-string. +x = t"""{"foo " + # comment 24 + t"{ x = + + }" # comment 25 + } + """ + +# Mix of various features. +t"""{ # comment 26 + foo # after foo + :>{ + x # after x + } + # comment 27 + # comment 28 +} woah {x}""" + +# Assignment statement + +# Even though this t-string has multiline expression, thus allowing us to break it at the +# curly braces, the t-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the t-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The t-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this t-string. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the t-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong +) + +# The following t-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = t"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = t"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the t-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + +# But, the following t-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" +) + +# This is an implicitly concatenated t-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two t-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Similar to the above example but this fits within the line length limit. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single t-string instead. + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# This is not a multiline t-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted t-string. +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment +) + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +) +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# t-strings in other positions + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if ( + t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if ( + t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): + pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert t"aaaaaaaaa{ + expression}bbbbbbbbbbbb", t"cccccccccc{ + expression}dddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{expression}dddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# t-strings as a single argument to a call expression to test whether it's huggable or not. +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call(t"{ + aaaaaa + + '''test + more''' +}") + +# Indentation + +# What should be the indentation? +# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590 +if indent0: + if indent1: + if indent2: + foo = t"""hello world +hello { + t"aaaaaaa { + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } bbbbbbbb" + + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } -------- +""" + + +# Implicit concatenated t-string containing quotes +_ = ( + 'This string should change its quotes to double quotes' + t'This string uses double quotes in an expression {"it's a quote"}' + t'This t-string does not use any quotes.' +) + +# Regression test for https://github.com/astral-sh/ruff/issues/14487 +t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc" + +# Regression test for https://github.com/astral-sh/ruff/issues/14778 +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" + +# Quotes reuse +t"{'a'}" + +# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes +t'foo {10 + len("bar")=}' +t'''foo {10 + len("""bar""")=}''' + +# 312+, it's okay to change the quotes here without creating an invalid t-string +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + + +# Regression tests for https://github.com/astral-sh/ruff/issues/13935 +t'{1: hy "user"}' +t'{1:hy "user"}' +t'{1: abcd "{1}" }' +t'{1: abcd "{'aa'}" }' +t'{1=: "abcd {'aa'}}' +t'{x:a{z:hy "user"}} \'\'\'' + +# Changing the outer quotes is fine because the format-spec is in a nested expression. +t'{t'{z=:hy "user"}'} \'\'\'' + + +# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim. +t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error +t'{1=: abcd \'\'}' # Changing the quotes here is fine because the inner quotes aren't the opposite quotes +t'{1=: abcd \"\"}' # Changing the quotes here is fine because the inner quotes are escaped +# Don't change the quotes in the following cases: +t'{x=:hy "user"} \'\'\'' +t'{x=:a{y:hy "user"}} \'\'\'' +t'{x=:a{y:{z:hy "user"}}} \'\'\'' +t'{x:a{y=:{z:hy "user"}}} \'\'\'' + +# This is fine because the debug expression and format spec are in a nested expression + +t"""{1=: "this" is fine}""" +t'''{1=: "this" is fine}''' # Change quotes to double quotes because they're preferred +t'{1=: {'ab"cd"'}}' # It's okay if the quotes are in an expression part. + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15459 +print(t"{ {1, 2, 3} - {2} }") +print(t"{ {1: 2}.keys() }") +print(t"{({1, 2, 3}) - ({2})}") +print(t"{1, 2, {3} }") +print(t"{(1, 2, {3})}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15535 +print(t"{ {}, }") # A single item tuple gets parenthesized +print(t"{ {}.values(), }") +print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized +print(t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized + {}, 1, +}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15536 +print(t"{ {}, 1, }") +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +``` + +```python +(t"{one}{two}") + + +rt"Not-so-tricky \"quote" + +# Regression test for tstrings dropping comments +result_f = ( + "Traceback (most recent call last):\n" + t' File "{__file__}", line {lineno_f + 5}, in _check_recursive_traceback_display\n' + " f()\n" + t' File "{__file__}", line {lineno_f + 1}, in f\n' + " f()\n" + t' File "{__file__}", line {lineno_f + 1}, in f\n' + " f()\n" + t' File "{__file__}", line {lineno_f + 1}, in f\n' + " f()\n" + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + # It also varies depending on the platform (stack size) + # Fortunately, we don't care about exactness here, so we use regex + r" \[Previous line repeated (\d+) more times\]" + "\n" + "RecursionError: maximum recursion depth exceeded\n" +) + + +# Regression for tstring dropping comments that were accidentally attached to +# an expression inside a formatted value +( + t"{1}" + # comment 1 + "" +) + +( + t"{1}" # comment 2 + t"{2}" +) + +( + t"{1}{2}" # comment 3 +) + +( + 1, + ( # comment 4 + t"{2}" + ), +) + +( + ( + t"{1}" + # comment 5 + ), + 2, +) + +# https://github.com/astral-sh/ruff/issues/6841 +x = t"""a{""}b""" +y = t'''c{1}d"""e''' +z = t"""a{""}b""" t'''c{1}d"""e''' + +# T-String formatting test cases (Preview) + +# Simple expression with a mix of debug expression and comments. +x = t"{a}" +x = t"{ + a = }" +x = t"{ # comment 6 + a +}" +x = t"{ # comment 7 + a = }" + +# Remove the parentheses as adding them doesn't make then fit within the line length limit. +# This is similar to how we format it before t-string formatting. +aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd} cccccccccc" +# Here, we would use the best fit layout to put the t-string indented on the next line +# similar to the next example. +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc} cccccccccc" +) +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc} cccccccccc" +) + +# This should never add the optional parentheses because even after adding them, the +# t-string exceeds the line length limit. +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'} ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' +} ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc" + +# Multiple larger expressions which exceeds the line length limit. Here, we need to decide +# whether to split at the first or second expression. This should work similarly to the +# assignment statement formatting where we split from right to left in preview mode. +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc {ddddddddddddddd} eeeeeeeeeeeeee" + +# The above example won't split but when we start introducing line breaks: +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" + +# But, in case comments are present, we would split at the expression containing the +# comments: +x = t"aaaaaaaaaaaa { + bbbbbbbbbbbbbb # comment 10 +} cccccccccccccccccccc {ddddddddddddddd} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { # comment 11 + ddddddddddddddd +} eeeeeeeeeeeeee" + +# Here, the expression part itself starts with a curly brace so we need to add an extra +# space between the opening curly brace and the expression. +x = t"{ {'x': 1, 'y': 2} }" +# Although the extra space isn't required before the ending curly brace, we add it for +# consistency. +x = t"{ {'x': 1, 'y': 2} }" +x = t"{ {'x': 1, 'y': 2} = }" +x = t"{ # comment 12 + {'x': 1, 'y': 2} +}" +x = t"{ # comment 13 + {'x': 1, 'y': 2} = }" +# But, if there's a format specifier or a conversion flag then we don't need to add +# any whitespace at the end +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb" +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" + +# But, in this case, we would split the expression itself because it exceeds the line +# length limit so we need not add the extra space. +xxxxxxx = ( + t"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} }" +) +# And, split the expression itself because it exceeds the line length. +xxxxxxx = t"{ + { + 'aaaaaaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'cccccccccccccccccccccccccc', + } +}" + +############################################################################################# +# Quotes +############################################################################################# +t"foo 'bar' {x}" +t'foo "bar" {x}' +t'foo "bar" {x}' +t"foo 'bar' {x}" +t"foo {'bar'}" + +t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t'single quote \' {x} double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes + +rf"single quotes ' {x}" # Keep double because `'` can't be escaped +rf'double quotes " {x}' # Keep single because `"` can't be escaped +rf"flip quotes {x}" # Use preferred quotes, because raw string contains now quotes. + +# Here, the formatter will remove the escapes +t"foo {"'bar'"}" +t"foo {'"bar"'}" + +# Quotes inside the expressions have no impact on the quote selection of the outer string. +# Required so that the following two examples result in the same formatting. +t"foo {10 + len('bar')}" +t"foo {10 + len('bar')}" + +# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression +t"foo {10 + len("bar")=}" +t"""foo {10 + len('''bar''')=}""" +t"""foo {10 + len('bar')=}""" # Fine to change the quotes because it uses triple quotes + +# Triple-quoted strings +# It's ok to use the same quote char for the inner string if it's single-quoted. +t"""test {"inner"}""" +t"""test {"inner"}""" +# But if the inner string is also triple-quoted then we should preserve the existing quotes. +t"""test {'''inner'''}""" + +# It's not okay to change the quote style if the inner string is triple quoted and contains a quote. +t"{'''other " '''}" +t"{'''other " ''' + 'more'}" +t"{b'''other " '''}" +t"{t'''other " '''}" + +t"""test {t"inner {'''inner inner'''}"}""" +t"""test {t'''inner {"""inner inner"""}'''}""" + +# Magic trailing comma +# +# The expression formatting will result in breaking it across multiple lines with a +# trailing comma but as the expression isn't already broken, we will remove all the line +# breaks which results in the trailing comma being present. This test case makes sure +# that the trailing comma is removed as well. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# And, if the trailing comma is already present, we still need to remove it. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# Keep this Multiline by breaking it at the square brackets. +t"""aaaaaa { + [ + xxxxxxxx, + yyyyyyyy, + ] +} ccc""" + +# Add the magic trailing comma because the elements don't fit within the line length limit +# when collapsed. +t"aaaaaa { + [ + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + yyyyyyyyyyyy, + ] +} ccccccc" + +# Remove the parentheses because they aren't required +xxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb { + xxxxxxxxxxx # comment 14 + + yyyyyyyyyy +} dddddddddd" + +# Comments + +# No comments should be dropped! +t"{ # comment 15 + # comment 16 + foo # comment 17 + # comment 18 +}" # comment 19 +# comment 20 + +# The specifier of a t-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single +# quoted f-string. +t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee" + +# The same applies for triple quoted t-strings, except that we need to preserve the newline before the closing `}`. +# or we risk altering the meaning of the f-string. +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee""" +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f +} ddddddddddddddd eeeeeeee""" + + +# Throw in a random comment in it but surprise, this is not a comment but just a text +# which is part of the format specifier +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment +} cccccccccc""" +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment} cccccccccc""" + +# Conversion flags +# + +# Even in the case of debug expressions, we only need to preserve the whitespace within +# the expression part of the replacement field. +x = t"aaaaaaaaa { x = !r}" + +# Combine conversion flags with format specifiers +x = t"{x = !s:>0}" + +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + +f"{ + 1 + # comment 21-3 + :}" + +f"{ + 1 # comment 21-4 + :} a" + +x = t""" +{ # comment 22 + x = :.0{y # comment 23 + }f}""" + +# Here, the debug expression is in a nested t-string so we should start preserving +# whitespaces from that point onwards. This means we should format the outer t-string. +x = t"""{ + "foo " # comment 24 + + t"{ x = + + }" # comment 25 +} + """ + +# Mix of various features. +t"""{ # comment 26 + foo # after foo + :>{ + x # after x + } + # comment 27 + # comment 28 +} woah {x}""" + +# Assignment statement + +# Even though this t-string has multiline expression, thus allowing us to break it at the +# curly braces, the t-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" +) + +# Same as above +xxxxxxx = ( + t"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} }" +) + +# Similar to the previous example, but the t-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = t"{ + { + 'aaaaaaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'cccccccccccccccccccccccccc', + } +}" + +# Same as above but with an inline comment. The t-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment +) + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this t-string. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the t-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# The following t-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = t"aaaa { + [ + 1, + 2, + ] +} bbbb" +# right-to-left +aaaa, bbbb = t"aaaa { + [ + 1, + 2, + ] +} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment +) + +# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the t-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) + +# But, the following t-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc +} ddddddddddddddddddd" +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbb + + cccccccccccccccccccccc + + dddddddddddddddddddddddddddd +} ddddddddddddddddddd" + +# This is an implicitly concatenated t-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two t-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = ( + t"test{expression}flat" + t"can be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + +# Similar to the above example but this fits within the line length limit. +a = t"test{expression}flatcan be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single t-string instead. + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] +}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] +}moreeeeeeeeeeeeeeeeeetest" # comment + +# Don't inline t-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +# This is not a multiline t-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted t-string. +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + b, + # comment + ] +}moee" # comment +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + b, + ] + }moee" + # comment +) + +# t-strings in other positions + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +if t"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccc{expression}dddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{ + expression +}dddddddddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", ( + t"cccccccccccccccc{expression}dddddddddddddddd" +) + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert ( + t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc { + expression +} dddddddddddddddddddddddddd" + +assert ( + t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), ( + t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" +) + +# t-strings as a single argument to a call expression to test whether it's huggable or not. +call(t"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}") + +call( + t"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}" +) + +call( + t"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }" +) + +call( + t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""" +) + +call( + t"""aaaaaaaaaaaaaaaa bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""" +) + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment +}""") + +call( + t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call( + t"{ + aaaaaa + + '''test + more''' + }" +) + +# Indentation + +# What should be the indentation? +# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590 +if indent0: + if indent1: + if indent2: + foo = t"""hello world +hello { + t"aaaaaaa { + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd', + ] + } bbbbbbbb" + + [ + "aaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbb", + "ccccccccccccccccccccc", + "ddddddddddddddddddddd", + ] + } -------- +""" + + +# Implicit concatenated t-string containing quotes +_ = ( + "This string should change its quotes to double quotes" + t"This string uses double quotes in an expression {"it's a quote"}" + t"This t-string does not use any quotes." +) + +# Regression test for https://github.com/astral-sh/ruff/issues/14487 +t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc" + +# Regression test for https://github.com/astral-sh/ruff/issues/14778 +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' if True else ''}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' if True else ''}" + +# Quotes reuse +t"{'a'}" + +# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes +t"foo {10 + len("bar")=}" +t"""foo {10 + len("""bar""")=}""" + +# 312+, it's okay to change the quotes here without creating an invalid t-string +t"{'''other " '''}" +t"{'''other " ''' + 'more'}" +t"{b'''other " '''}" +t"{t'''other " '''}" + + +# Regression tests for https://github.com/astral-sh/ruff/issues/13935 +t'{1: hy "user"}' +t'{1:hy "user"}' +t'{1: abcd "{1}" }' +t'{1: abcd "{"aa"}" }' +t'{1=: "abcd {'aa'}}' +t"{x:a{z:hy \"user\"}} '''" + +# Changing the outer quotes is fine because the format-spec is in a nested expression. +t"{t'{z=:hy "user"}'} '''" + + +# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim. +t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error +t"{1=: abcd \'\'}" # Changing the quotes here is fine because the inner quotes aren't the opposite quotes +t"{1=: abcd \"\"}" # Changing the quotes here is fine because the inner quotes are escaped +# Don't change the quotes in the following cases: +t'{x=:hy "user"} \'\'\'' +t'{x=:a{y:hy "user"}} \'\'\'' +t'{x=:a{y:{z:hy "user"}}} \'\'\'' +t'{x:a{y=:{z:hy "user"}}} \'\'\'' + +# This is fine because the debug expression and format spec are in a nested expression + +t"""{1=: "this" is fine}""" +t"""{1=: "this" is fine}""" # Change quotes to double quotes because they're preferred +t"{1=: {'ab"cd"'}}" # It's okay if the quotes are in an expression part. + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15459 +print(t"{ {1, 2, 3} - {2} }") +print(t"{ {1: 2}.keys() }") +print(t"{({1, 2, 3}) - ({2})}") +print(t"{1, 2, {3}}") +print(t"{(1, 2, {3})}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15535 +print(t"{({},)}") # A single item tuple gets parenthesized +print(t"{({}.values(),)}") +print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized +print( + t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized + ( + {}, + 1, + ) + }" +) + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15536 +print(t"{ {}, 1 }") +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap new file mode 100644 index 0000000000000..c24f246a5e687 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@f-string-carriage-return-newline.py.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/f-string-carriage-return-newline.py +--- +## Input +```python +# Regression test for https://github.com/astral-sh/ruff/issues/18667 +f"{ +1= +}" + +t"{ +1= +}" +``` + +## Output +```python +# Regression test for https://github.com/astral-sh/ruff/issues/18667 +f"{ +1= +}" + +t"{ +1= +}" +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap index 45fe2da38d5a6..533245670b8e3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@pattern__pattern_maybe_parenthesize.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern/pattern_maybe_parenthesize.py -snapshot_kind: text --- ## Input ```python @@ -295,7 +294,15 @@ match x: ]: pass - +match a, b: + case [], []: + ... + case [], _: + ... + case _, []: + ... + case _, _: + ... ``` @@ -592,4 +599,14 @@ match x: ccccccccccccccccccccccccccccccccc, ]: pass + +match a, b: + case [], []: + ... + case [], _: + ... + case _, []: + ... + case _, _: + ... ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@pattern_match_regression_brackets.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@pattern_match_regression_brackets.py.snap new file mode 100644 index 0000000000000..ac0c88e972b05 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@pattern_match_regression_brackets.py.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/pattern_match_regression_brackets.py +--- +## Input +```python +# Ruff in some cases added brackets around tuples in some cases; deviating from Black. +# Ensure we don't revert already-applied formats with the fix. +# See https://github.com/astral-sh/ruff/pull/18147 +match a, b: + case [[], []]: + ... + case [[], _]: + ... +``` + +## Output +```python +# Ruff in some cases added brackets around tuples in some cases; deviating from Black. +# Ensure we don't revert already-applied formats with the fix. +# See https://github.com/astral-sh/ruff/pull/18147 +match a, b: + case [[], []]: + ... + case [[], _]: + ... +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__decorated_class_after_function.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__decorated_class_after_function.pyi.snap new file mode 100644 index 0000000000000..a507735686246 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__decorated_class_after_function.pyi.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/decorated_class_after_function.pyi +--- +## Input +```python +# Issue #18865: Decorated classes below functions should be separated with blank lines +def hello(): ... +@lambda _, /: _ +class A: ... + +def world(): ... + +@final +class B: ... +``` + +## Output +```python +# Issue #18865: Decorated classes below functions should be separated with blank lines +def hello(): ... +@lambda _, /: _ +class A: ... + +def world(): ... +@final +class B: ... +``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -1,8 +1,10 @@ + # Issue #18865: Decorated classes below functions should be separated with blank lines + def hello(): ... ++ + @lambda _, /: _ + class A: ... + + def world(): ... ++ + @final + class B: ... +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__top_level.pyi.snap b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__top_level.pyi.snap index 563fe96da4e23..e3afefb66eb2c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@stub_files__top_level.pyi.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@stub_files__top_level.pyi.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/stub_files/top_level.pyi -snapshot_kind: text --- ## Input ```python @@ -43,3 +42,18 @@ class LockType3: ... @final class LockType4: ... ``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -4,6 +4,7 @@ + def count2(): ... + @final + def count3(): ... ++ + @final + class LockType1: ... + +``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@trailing_comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@trailing_comments.py.snap index d3d09022ea7d1..be5ad8e01f118 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@trailing_comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@trailing_comments.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py -snapshot_kind: text --- ## Input ```python @@ -9,6 +8,7 @@ snapshot_kind: text i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break +i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break @@ -44,6 +44,7 @@ i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break +i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break diff --git a/crates/ruff_python_index/src/indexer.rs b/crates/ruff_python_index/src/indexer.rs index bf1d53c64d9f3..82f913f484949 100644 --- a/crates/ruff_python_index/src/indexer.rs +++ b/crates/ruff_python_index/src/indexer.rs @@ -4,7 +4,7 @@ use ruff_python_ast::Stmt; use ruff_python_parser::{TokenKind, Tokens}; use ruff_python_trivia::{ - has_leading_content, has_trailing_content, is_python_whitespace, CommentRanges, + CommentRanges, has_leading_content, has_trailing_content, is_python_whitespace, }; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -53,7 +53,7 @@ impl Indexer { continuation_lines.push(line_start); // SAFETY: Safe because of the len assertion at the top of the function. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] { line_start = prev_end + TextSize::new((index + 1) as u32); } diff --git a/crates/ruff_python_literal/src/cformat.rs b/crates/ruff_python_literal/src/cformat.rs index cb724fa82c840..5427e7a854c57 100644 --- a/crates/ruff_python_literal/src/cformat.rs +++ b/crates/ruff_python_literal/src/cformat.rs @@ -241,7 +241,7 @@ where Ok((format_type, c)) } -#[allow(clippy::cast_possible_wrap)] +#[expect(clippy::cast_possible_wrap)] fn parse_quantity(iter: &mut ParseIter) -> Result, ParsingError> where T: Into + Copy, diff --git a/crates/ruff_python_literal/src/escape.rs b/crates/ruff_python_literal/src/escape.rs index 5a218dbcfd091..0d9d8577abc0b 100644 --- a/crates/ruff_python_literal/src/escape.rs +++ b/crates/ruff_python_literal/src/escape.rs @@ -1,6 +1,6 @@ use ruff_python_ast::{ - str::{Quote, TripleQuotes}, BytesLiteralFlags, StringFlags, StringLiteralFlags, + str::{Quote, TripleQuotes}, }; pub struct EscapeLayout { @@ -103,11 +103,7 @@ impl std::fmt::Display for StrRepr<'_, '_> { impl UnicodeEscape<'_> { const REPR_RESERVED_LEN: usize = 2; // for quotes - #[allow( - clippy::cast_possible_wrap, - clippy::cast_possible_truncation, - clippy::cast_sign_loss - )] + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn repr_layout(source: &str, preferred_quote: Quote) -> EscapeLayout { Self::output_layout_with_checker(source, preferred_quote, |a, b| { Some((a as isize).checked_add(b as isize)? as usize) @@ -265,11 +261,7 @@ impl<'a> AsciiEscape<'a> { } impl AsciiEscape<'_> { - #[allow( - clippy::cast_possible_wrap, - clippy::cast_possible_truncation, - clippy::cast_sign_loss - )] + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn repr_layout(source: &[u8], preferred_quote: Quote) -> EscapeLayout { Self::output_layout_with_checker(source, preferred_quote, 3, |a, b| { Some((a as isize).checked_add(b as isize)? as usize) diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index e6e96335bc9e6..ae45871866777 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -13,13 +13,14 @@ license = { workspace = true } [lib] [dependencies] -ruff_python_ast = { workspace = true } +ruff_python_ast = { workspace = true, features = ["get-size"] } ruff_python_trivia = { workspace = true } -ruff_text_size = { workspace = true } +ruff_text_size = { workspace = true, features = ["get-size"] } bitflags = { workspace = true } bstr = { workspace = true } compact_str = { workspace = true } +get-size2 = { workspace = true } memchr = { workspace = true } rustc-hash = { workspace = true } static_assertions = { workspace = true } diff --git a/crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py b/crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py index 45f01b2ea20d0..2252aae5db5ba 100644 --- a/crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py +++ b/crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py @@ -1,2 +1,3 @@ sum(x for x in range(10), 5) total(1, 2, x for x in range(5), 6) +sum(x for x in range(10),) diff --git a/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py b/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py index c8cf8e64ee749..0ffad964a2a30 100644 --- a/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py +++ b/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py @@ -1,5 +1,5 @@ -x = *a and b -x = *yield x -x = *yield from x -x = *lambda x: x +x = (*a and b,) +x = (42, *yield x) +x = (42, *yield from x) +x = (*lambda x: x,) x = x := 1 diff --git a/crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py b/crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py new file mode 100644 index 0000000000000..8b3169540758f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py @@ -0,0 +1,4 @@ +_ = *[42] +_ = *{42} +_ = *list() +_ = *(p + q) diff --git a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py deleted file mode 100644 index e1ca280b7692b..0000000000000 --- a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py +++ /dev/null @@ -1,12 +0,0 @@ -try: - pass -except x, y: - pass -except x, y as exc: - pass -try: - pass -except* x, y: - pass -except* x, y as eg: - pass diff --git a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py new file mode 100644 index 0000000000000..4e2122b1d5463 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py @@ -0,0 +1,8 @@ +try: + pass +except x, y as exc: + pass +try: + pass +except* x, y as eg: + pass diff --git a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py new file mode 100644 index 0000000000000..11da8cf313f01 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py @@ -0,0 +1,9 @@ +# parse_options: {"target-version": "3.13"} +try: + pass +except x, y: + pass +try: + pass +except* x, y: + pass diff --git a/crates/ruff_python_parser/resources/inline/err/f_string_conversion_follows_exclamation.py b/crates/ruff_python_parser/resources/inline/err/f_string_conversion_follows_exclamation.py new file mode 100644 index 0000000000000..17d523150fe58 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/f_string_conversion_follows_exclamation.py @@ -0,0 +1,3 @@ +f"{x! s}" +t"{x! s}" +f"{x! z}" diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py new file mode 100644 index 0000000000000..b7ed1ba1b0b9e --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py @@ -0,0 +1,2 @@ +nonlocal x +nonlocal x, y diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py index 07127b5f052d5..f6f2b5577aca3 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py @@ -1 +1,2 @@ -nonlocal +def _(): + nonlocal diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py index 303cb88b61d23..46854180075b6 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py @@ -1 +1,2 @@ -nonlocal x + 1 +def _(): + nonlocal x + 1 diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py index 24acf55245280..d0742c873124d 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py @@ -1,3 +1,4 @@ -nonlocal , -nonlocal x, -nonlocal x, y, +def _(): + nonlocal , + nonlocal x, + nonlocal x, y, diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py b/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py new file mode 100644 index 0000000000000..257a0e1209468 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"{}" +t"{ }" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py new file mode 100644 index 0000000000000..dcea20f590b2c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.14"} +t"{x!z}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py new file mode 100644 index 0000000000000..61fb5815ba89c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"{x!123}" +t"{x!'a'}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py new file mode 100644 index 0000000000000..77bf4eb55f460 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py @@ -0,0 +1,5 @@ +# parse_options: {"target-version": "3.14"} +# Starred expression inside t-string has a minimum precedence of bitwise or. +t"{*}" +t"{*x and y}" +t"{*yield x}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py b/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py new file mode 100644 index 0000000000000..0d9e70011cabd --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.14"} +t"{lambda x: x}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py new file mode 100644 index 0000000000000..b943b533e525f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.14"} +t"{" +t"{foo!r" +t"{foo=" +t"{" +t"""{""" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py new file mode 100644 index 0000000000000..cced3bb064bed --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"hello {x:" +t"hello {x:.3f" diff --git a/crates/ruff_python_parser/resources/inline/err/template_strings_py313.py b/crates/ruff_python_parser/resources/inline/err/template_strings_py313.py new file mode 100644 index 0000000000000..2c7fde825a9bd --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/template_strings_py313.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.13"} +t"{hey}" +t'{there}' +t"""what's +happening?""" +"implicitly"t"concatenated" diff --git a/crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py b/crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py index ecadabd4e33f1..a42c0867fb283 100644 --- a/crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py +++ b/crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py @@ -1 +1,3 @@ +zip((x for x in range(10)), (y for y in range(10))) sum(x for x in range(10)) +sum((x for x in range(10)),) diff --git a/crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py b/crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py new file mode 100644 index 0000000000000..2e8a1919b07b6 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py @@ -0,0 +1,4 @@ +_ = 4 +_ = [4] +_ = (*[1],) +_ = *[1], diff --git a/crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py b/crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py new file mode 100644 index 0000000000000..6d646a7de5397 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py @@ -0,0 +1,9 @@ +# parse_options: {"target-version": "3.14"} +try: + pass +except x, y: + pass +try: + pass +except* x, y: + pass diff --git a/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py b/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py new file mode 100644 index 0000000000000..8f51257d0e03b --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py @@ -0,0 +1,2 @@ +def _(): + nonlocal x diff --git a/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py b/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py index 7f652bb0a69f1..81caa434cb26f 100644 --- a/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py +++ b/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py @@ -1,2 +1,3 @@ -nonlocal x -nonlocal x, y, z +def _(): + nonlocal x + nonlocal x, y, z diff --git a/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py b/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py index e109ecaebc34f..c8c0425ce6f94 100644 --- a/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py +++ b/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py @@ -1,3 +1,2 @@ def foo(arg: int): ... def foo(arg: lambda x: x): ... -def foo(arg: (x := int)): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py b/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py new file mode 100644 index 0000000000000..18cfc9a08f339 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py @@ -0,0 +1,10 @@ +# parse_options: {"target-version": "3.14"} +t'Magic wand: { bag['wand'] }' # nested quotes +t"{'\n'.join(a)}" # escape sequence +t'''A complex trick: { + bag['bag'] # comment +}''' +t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting +t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes +t"test {a \ + } more" # line continuation diff --git a/crates/ruff_python_parser/resources/inline/ok/template_strings_py314.py b/crates/ruff_python_parser/resources/inline/ok/template_strings_py314.py new file mode 100644 index 0000000000000..541f49917addc --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/template_strings_py314.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.14"} +t"{hey}" +t'{there}' +t"""what's +happening?""" +"implicitly"t"concatenated" diff --git a/crates/ruff_python_parser/resources/invalid/expressions/parenthesized/tuple_starred_expr.py b/crates/ruff_python_parser/resources/invalid/expressions/parenthesized/tuple_starred_expr.py index 1a87159f0aaae..e3998d7ccec05 100644 --- a/crates/ruff_python_parser/resources/invalid/expressions/parenthesized/tuple_starred_expr.py +++ b/crates/ruff_python_parser/resources/invalid/expressions/parenthesized/tuple_starred_expr.py @@ -1,5 +1,5 @@ # For tuple expression, the minimum binding power of star expression is bitwise or. -# Test the first and any other element as the there are two separate calls. +# Test the first and any other element as there are two separate calls. (*x in y, z, *x in y) (*not x, z, *not x) diff --git a/crates/ruff_python_parser/resources/valid/expressions/t_string.py b/crates/ruff_python_parser/resources/valid/expressions/t_string.py new file mode 100644 index 0000000000000..0bb6278492490 --- /dev/null +++ b/crates/ruff_python_parser/resources/valid/expressions/t_string.py @@ -0,0 +1,74 @@ +# Empty t-strings +t"" +t"" +t'' +t"""""" +t'''''' + +t"{" t"}" +t"{foo!s}" +t"{3,}" +t"{3!=4:}" +t'{3:{"}"}>10}' +t'{3:{"{"}>10}' +t"{ foo = }" +t"{ foo = :.3f }" +t"{ foo = !s }" +t"{ 1, 2 = }" +t'{t"{3.1415=:.1f}":*^20}' + +{"foo " t"bar {x + y} " "baz": 10} +match foo: + case "one": + pass + case "implicitly " "concatenated": + pass + +t"\{foo}\{bar:\}" +t"\\{{foo\\}}" +t"""{ + foo:x + y + z +}""" +t"{ ( foo ) = }" + +t"normal {foo} {{another}} {bar} {{{three}}}" +t"normal {foo!a} {bar!s} {baz!r} {foobar}" +t"normal {x:y + 2}" +t"{x:{{1}.pop()}}" +t"{(lambda x:{x})}" +t"{x =}" +t"{ x = }" +t"{x=!a}" +t"{x:.3f!r =}" +t"{x = !r :.3f}" +t"{x:.3f=!r}" +"hello" t"{x}" +t"{x}" t"{y}" +t"{x}" "world" +t"Invalid args in command: {command, *args}" +"foo" t"{x}" "bar" +( + t"a" + t"b" + "c" + rt"d" + fr"e" +) + +# With unicode strings +u"foo" t"{bar}" "baz" " some" +"foo" t"{bar}" u"baz" " some" +"foo" t"{bar}" "baz" u" some" +u"foo" t"bar {baz} really" u"bar" "no" + + +# With f-strings +f"{this}" t"{that}" +t"{this}"f"{that}" +t"{this}" "that" f"{other}" +f"one {this} two" "that" t"three {other} four" + +# Nesting +t"{f"{t"{this}"}"}" diff --git a/crates/ruff_python_parser/resources/valid/statement/assignment.py b/crates/ruff_python_parser/resources/valid/statement/assignment.py index 992e5f8fbae2c..7e0b458335020 100644 --- a/crates/ruff_python_parser/resources/valid/statement/assignment.py +++ b/crates/ruff_python_parser/resources/valid/statement/assignment.py @@ -37,7 +37,7 @@ foo = 42 -[] = *data -() = *data +[] = (*data,) +() = (*data,) a, b = ab a = b = c \ No newline at end of file diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 3673ded43a3a9..2c2baa8dd7a86 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -3,11 +3,11 @@ use std::fmt::{self, Display}; use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextRange}; -use crate::TokenKind; +use crate::{TokenKind, string::InterpolatedStringKind}; /// Represents represent errors that occur during parsing and are /// returned by the `parse_*` functions. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] pub struct ParseError { pub error: ParseErrorType, pub location: TextRange, @@ -42,15 +42,21 @@ impl From for ParseError { } } +impl Ranged for ParseError { + fn range(&self) -> TextRange { + self.location + } +} + impl ParseError { pub fn error(self) -> ParseErrorType { self.error } } -/// Represents the different types of errors that can occur during parsing of an f-string. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FStringErrorType { +/// Represents the different types of errors that can occur during parsing of an f-string or t-string. +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +pub enum InterpolatedStringErrorType { /// Expected a right brace after an opened left brace. UnclosedLbrace, /// An invalid conversion flag was encountered. @@ -63,29 +69,39 @@ pub enum FStringErrorType { UnterminatedTripleQuotedString, /// A lambda expression without parentheses was encountered. LambdaWithoutParentheses, + /// Conversion flag does not immediately follow exclamation. + ConversionFlagNotImmediatelyAfterExclamation, + /// Newline inside of a format spec for a single quoted f- or t-string. + NewlineInFormatSpec, } -impl std::fmt::Display for FStringErrorType { +impl std::fmt::Display for InterpolatedStringErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - use FStringErrorType::{ - InvalidConversionFlag, LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace, - UnterminatedString, UnterminatedTripleQuotedString, - }; match self { - UnclosedLbrace => write!(f, "expecting '}}'"), - InvalidConversionFlag => write!(f, "invalid conversion character"), - SingleRbrace => write!(f, "single '}}' is not allowed"), - UnterminatedString => write!(f, "unterminated string"), - UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"), - LambdaWithoutParentheses => { + Self::UnclosedLbrace => write!(f, "expecting '}}'"), + Self::InvalidConversionFlag => write!(f, "invalid conversion character"), + Self::SingleRbrace => write!(f, "single '}}' is not allowed"), + Self::UnterminatedString => write!(f, "unterminated string"), + Self::UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"), + Self::LambdaWithoutParentheses => { write!(f, "lambda expressions are not allowed without parentheses") } + Self::ConversionFlagNotImmediatelyAfterExclamation => write!( + f, + "conversion type must come right after the exclamation mark" + ), + Self::NewlineInFormatSpec => { + write!( + f, + "newlines are not allowed in format specifiers when using single quotes" + ) + } } } } /// Represents the different types of errors that can occur during parsing. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] pub enum ParseErrorType { /// An unexpected error occurred. OtherError(String), @@ -126,8 +142,6 @@ pub enum ParseErrorType { /// A default value was found for a `*` or `**` parameter. VarParameterWithDefault, - /// A duplicate parameter was found in a function definition or lambda expression. - DuplicateParameter(String), /// A keyword argument was repeated. DuplicateKeywordArgumentError(String), @@ -179,12 +193,26 @@ pub enum ParseErrorType { /// An unexpected token was found at the end of an expression parsing UnexpectedExpressionToken, - /// An f-string error containing the [`FStringErrorType`]. - FStringError(FStringErrorType), + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), /// Parser encountered an error during lexing. Lexical(LexicalErrorType), } +impl ParseErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + impl std::error::Error for ParseErrorType {} impl std::fmt::Display for ParseErrorType { @@ -194,7 +222,7 @@ impl std::fmt::Display for ParseErrorType { ParseErrorType::ExpectedToken { found, expected } => { write!(f, "Expected {expected}, found {found}",) } - ParseErrorType::Lexical(ref lex_error) => write!(f, "{lex_error}"), + ParseErrorType::Lexical(lex_error) => write!(f, "{lex_error}"), ParseErrorType::SimpleStatementsOnSameLine => { f.write_str("Simple statements must be separated by newlines or semicolons") } @@ -285,18 +313,18 @@ impl std::fmt::Display for ParseErrorType { f.write_str("Invalid augmented assignment target") } ParseErrorType::InvalidDeleteTarget => f.write_str("Invalid delete target"), - ParseErrorType::DuplicateParameter(arg_name) => { - write!(f, "Duplicate parameter {arg_name:?}") - } ParseErrorType::DuplicateKeywordArgumentError(arg_name) => { write!(f, "Duplicate keyword argument {arg_name:?}") } ParseErrorType::UnexpectedIpythonEscapeCommand => { f.write_str("IPython escape commands are only allowed in `Mode::Ipython`") } - ParseErrorType::FStringError(ref fstring_error) => { + ParseErrorType::FStringError(fstring_error) => { write!(f, "f-string: {fstring_error}") } + ParseErrorType::TStringError(tstring_error) => { + write!(f, "t-string: {tstring_error}") + } ParseErrorType::UnexpectedExpressionToken => { write!(f, "Unexpected token at the end of an expression") } @@ -362,7 +390,7 @@ impl std::fmt::Display for LexicalError { } /// Represents the different types of errors that can occur during lexing. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub enum LexicalErrorType { // TODO: Can probably be removed, the places it is used seem to be able // to use the `UnicodeError` variant instead. @@ -380,8 +408,10 @@ pub enum LexicalErrorType { IndentationError, /// An unrecognized token was encountered. UnrecognizedToken { tok: char }, - /// An f-string error containing the [`FStringErrorType`]. - FStringError(FStringErrorType), + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), /// Invalid character encountered in a byte literal. InvalidByteLiteral, /// An unexpected character was encountered after a line continuation. @@ -394,33 +424,46 @@ pub enum LexicalErrorType { impl std::error::Error for LexicalErrorType {} +impl LexicalErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + impl std::fmt::Display for LexicalErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - LexicalErrorType::StringError => write!(f, "Got unexpected string"), - LexicalErrorType::FStringError(error) => write!(f, "f-string: {error}"), - LexicalErrorType::InvalidByteLiteral => { + Self::StringError => write!(f, "Got unexpected string"), + Self::FStringError(error) => write!(f, "f-string: {error}"), + Self::TStringError(error) => write!(f, "t-string: {error}"), + Self::InvalidByteLiteral => { write!(f, "bytes can only contain ASCII literal characters") } - LexicalErrorType::UnicodeError => write!(f, "Got unexpected unicode"), - LexicalErrorType::IndentationError => { + Self::UnicodeError => write!(f, "Got unexpected unicode"), + Self::IndentationError => { write!(f, "unindent does not match any outer indentation level") } - LexicalErrorType::UnrecognizedToken { tok } => { + Self::UnrecognizedToken { tok } => { write!(f, "Got unexpected token {tok}") } - LexicalErrorType::LineContinuationError => { + Self::LineContinuationError => { write!(f, "Expected a newline after line continuation character") } - LexicalErrorType::Eof => write!(f, "unexpected EOF while parsing"), - LexicalErrorType::OtherError(msg) => write!(f, "{msg}"), - LexicalErrorType::UnclosedStringError => { + Self::Eof => write!(f, "unexpected EOF while parsing"), + Self::OtherError(msg) => write!(f, "{msg}"), + Self::UnclosedStringError => { write!(f, "missing closing quote in string literal") } - LexicalErrorType::MissingUnicodeLbrace => { + Self::MissingUnicodeLbrace => { write!(f, "Missing `{{` in Unicode escape sequence") } - LexicalErrorType::MissingUnicodeRbrace => { + Self::MissingUnicodeRbrace => { write!(f, "Missing `}}` in Unicode escape sequence") } } @@ -431,7 +474,7 @@ impl std::fmt::Display for LexicalErrorType { /// /// An example of a version-related error is the use of a `match` statement before Python 3.10, when /// it was first introduced. See [`UnsupportedSyntaxErrorKind`] for other kinds of errors. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, get_size2::GetSize)] pub struct UnsupportedSyntaxError { pub kind: UnsupportedSyntaxErrorKind, pub range: TextRange, @@ -446,28 +489,28 @@ impl Ranged for UnsupportedSyntaxError { } /// The type of tuple unpacking for [`UnsupportedSyntaxErrorKind::StarTuple`]. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] pub enum StarTupleKind { Return, Yield, } /// The type of PEP 701 f-string error for [`UnsupportedSyntaxErrorKind::Pep701FString`]. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] pub enum FStringKind { Backslash, Comment, NestedQuote, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] pub enum UnparenthesizedNamedExprKind { SequenceIndex, SetLiteral, SetComprehension, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, get_size2::GetSize)] pub enum UnsupportedSyntaxErrorKind { Match, Walrus, @@ -818,6 +861,47 @@ pub enum UnsupportedSyntaxErrorKind { /// [PEG parser rewrite]: https://peps.python.org/pep-0617/ /// [Python 3.11 release]: https://docs.python.org/3/whatsnew/3.11.html#other-language-changes UnparenthesizedUnpackInFor, + /// Represents the use of multiple exception names in an except clause without an `as` binding, before Python 3.14. + /// + /// ## Examples + /// Before Python 3.14, catching multiple exceptions required + /// parentheses like so: + /// + /// ```python + /// try: + /// ... + /// except (ExceptionA, ExceptionB, ExceptionC): + /// ... + /// ``` + /// + /// Starting with Python 3.14, thanks to [PEP 758], it was permitted + /// to omit the parentheses: + /// + /// ```python + /// try: + /// ... + /// except ExceptionA, ExceptionB, ExceptionC: + /// ... + /// ``` + /// + /// However, parentheses are still required in the presence of an `as`: + /// + /// ```python + /// try: + /// ... + /// except (ExceptionA, ExceptionB, ExceptionC) as e: + /// ... + /// ``` + /// + /// + /// [PEP 758]: https://peps.python.org/pep-0758/ + UnparenthesizedExceptionTypes, + /// Represents the use of a template string (t-string) + /// literal prior to the implementation of [PEP 750] + /// in Python 3.14. + /// + /// [PEP 750]: https://peps.python.org/pep-0750/ + TemplateStrings, } impl Display for UnsupportedSyntaxError { @@ -834,7 +918,9 @@ impl Display for UnsupportedSyntaxError { ) => "Cannot use unparenthesized assignment expression as an element in a set literal", UnsupportedSyntaxErrorKind::UnparenthesizedNamedExpr( UnparenthesizedNamedExprKind::SetComprehension, - ) => "Cannot use unparenthesized assignment expression as an element in a set comprehension", + ) => { + "Cannot use unparenthesized assignment expression as an element in a set comprehension" + } UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => { "Cannot use parenthesized keyword argument name" } @@ -864,7 +950,7 @@ impl Display for UnsupportedSyntaxError { self.target_version, changed = self.kind.changed_version(), ), - } + }; } UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { "Cannot use positional-only parameter separator" @@ -893,6 +979,10 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { "Cannot use iterable unpacking in `for` statements" } + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + "Multiple exception types must be parenthesized" + } + UnsupportedSyntaxErrorKind::TemplateStrings => "Cannot use t-strings", }; write!( @@ -904,7 +994,7 @@ impl Display for UnsupportedSyntaxError { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum RelaxedDecoratorError { CallExpression, Other(&'static str), @@ -960,6 +1050,10 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { Change::Added(PythonVersion::PY39) } + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + Change::Added(PythonVersion::PY314) + } + UnsupportedSyntaxErrorKind::TemplateStrings => Change::Added(PythonVersion::PY314), } } diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index c3e52f844c82d..53cea048e753b 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -17,16 +17,18 @@ use ruff_python_ast::{Int, IpyEscapeKind, StringFlags}; use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{TextLen, TextRange, TextSize}; -use crate::error::{FStringErrorType, LexicalError, LexicalErrorType}; +use crate::Mode; +use crate::error::{InterpolatedStringErrorType, LexicalError, LexicalErrorType}; use crate::lexer::cursor::{Cursor, EOF_CHAR}; -use crate::lexer::fstring::{FStringContext, FStrings, FStringsCheckpoint}; use crate::lexer::indentation::{Indentation, Indentations, IndentationsCheckpoint}; +use crate::lexer::interpolated_string::{ + InterpolatedStringContext, InterpolatedStrings, InterpolatedStringsCheckpoint, +}; use crate::token::{TokenFlags, TokenKind, TokenValue}; -use crate::Mode; mod cursor; -mod fstring; mod indentation; +mod interpolated_string; const BOM: char = '\u{feff}'; @@ -65,8 +67,8 @@ pub struct Lexer<'src> { /// Lexer mode. mode: Mode, - /// F-string contexts. - fstrings: FStrings, + /// F-string and t-string contexts. + interpolated_strings: InterpolatedStrings, /// Errors encountered while lexing. errors: Vec, @@ -102,7 +104,7 @@ impl<'src> Lexer<'src> { indentations: Indentations::default(), pending_indentation: None, mode, - fstrings: FStrings::default(), + interpolated_strings: InterpolatedStrings::default(), errors: Vec::new(), }; @@ -162,11 +164,11 @@ impl<'src> Lexer<'src> { } fn lex_token(&mut self) -> TokenKind { - if let Some(fstring) = self.fstrings.current() { - if !fstring.is_in_expression(self.nesting) { - if let Some(token) = self.lex_fstring_middle_or_end() { - if matches!(token, TokenKind::FStringEnd) { - self.fstrings.pop(); + if let Some(interpolated_string) = self.interpolated_strings.current() { + if !interpolated_string.is_in_interpolation(self.nesting) { + if let Some(token) = self.lex_interpolated_string_middle_or_end() { + if token.is_interpolated_string_end() { + self.interpolated_strings.pop(); } return token; } @@ -281,7 +283,7 @@ impl<'src> Lexer<'src> { } fn handle_indentation(&mut self, indentation: Indentation) -> Option { - let token = match self.indentations.current().try_compare(indentation) { + match self.indentations.current().try_compare(indentation) { // Dedent Ok(Ordering::Greater) => { self.pending_indentation = Some(indentation); @@ -318,15 +320,11 @@ impl<'src> Lexer<'src> { self.indentations.indent(indentation); Some(TokenKind::Indent) } - Err(_) => { - return Some(self.push_error(LexicalError::new( - LexicalErrorType::IndentationError, - self.token_range(), - ))); - } - }; - - token + Err(_) => Some(self.push_error(LexicalError::new( + LexicalErrorType::IndentationError, + self.token_range(), + ))), + } } fn skip_whitespace(&mut self) -> Result<(), LexicalError> { @@ -510,23 +508,26 @@ impl<'src> Lexer<'src> { TokenKind::Lbrace } '}' => { - if let Some(fstring) = self.fstrings.current_mut() { - if fstring.nesting() == self.nesting { - return self.push_error(LexicalError::new( - LexicalErrorType::FStringError(FStringErrorType::SingleRbrace), - self.token_range(), - )); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + if interpolated_string.nesting() == self.nesting { + let error_type = LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::SingleRbrace, + interpolated_string.kind(), + ); + return self.push_error(LexicalError::new(error_type, self.token_range())); } - fstring.try_end_format_spec(self.nesting); + interpolated_string.try_end_format_spec(self.nesting); } self.nesting = self.nesting.saturating_sub(1); TokenKind::Rbrace } ':' => { if self - .fstrings + .interpolated_strings .current_mut() - .is_some_and(|fstring| fstring.try_start_format_spec(self.nesting)) + .is_some_and(|interpolated_string| { + interpolated_string.try_start_format_spec(self.nesting) + }) { TokenKind::Colon } else if self.cursor.eat_char('=') { @@ -577,11 +578,11 @@ impl<'src> Lexer<'src> { self.state = State::AfterNewline; TokenKind::Newline } else { - if let Some(fstring) = self.fstrings.current_mut() { - fstring.try_end_format_spec(self.nesting); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); } TokenKind::NonLogicalNewline - } + }; } '\r' => { self.cursor.eat_char('\n'); @@ -590,8 +591,8 @@ impl<'src> Lexer<'src> { self.state = State::AfterNewline; TokenKind::Newline } else { - if let Some(fstring) = self.fstrings.current_mut() { - fstring.try_end_format_spec(self.nesting); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); } TokenKind::NonLogicalNewline }; @@ -614,7 +615,7 @@ impl<'src> Lexer<'src> { /// Lex an identifier. Also used for keywords and string/bytes literals with a prefix. fn lex_identifier(&mut self, first: char) -> TokenKind { - // Detect potential string like rb'' b'' f'' u'' r'' + // Detect potential string like rb'' b'' f'' t'' u'' r'' let quote = match (first, self.cursor.first()) { (_, quote @ ('\'' | '"')) => self.try_single_char_prefix(first).then(|| { self.cursor.bump(); @@ -631,8 +632,10 @@ impl<'src> Lexer<'src> { }; if let Some(quote) = quote { - if self.current_flags.is_f_string() { - return self.lex_fstring_start(quote); + if self.current_flags.is_interpolated_string() { + if let Some(kind) = self.lex_interpolated_string_start(quote) { + return kind; + } } return self.lex_string(quote); @@ -715,6 +718,7 @@ impl<'src> Lexer<'src> { fn try_single_char_prefix(&mut self, first: char) -> bool { match first { 'f' | 'F' => self.current_flags |= TokenFlags::F_STRING, + 't' | 'T' => self.current_flags |= TokenFlags::T_STRING, 'u' | 'U' => self.current_flags |= TokenFlags::UNICODE_STRING, 'b' | 'B' => self.current_flags |= TokenFlags::BYTE_STRING, 'r' => self.current_flags |= TokenFlags::RAW_STRING_LOWERCASE, @@ -734,6 +738,12 @@ impl<'src> Lexer<'src> { ['R', 'f' | 'F'] | ['f' | 'F', 'R'] => { self.current_flags |= TokenFlags::F_STRING | TokenFlags::RAW_STRING_UPPERCASE; } + ['r', 't' | 'T'] | ['t' | 'T', 'r'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_LOWERCASE; + } + ['R', 't' | 'T'] | ['t' | 'T', 'R'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_UPPERCASE; + } ['r', 'b' | 'B'] | ['b' | 'B', 'r'] => { self.current_flags |= TokenFlags::BYTE_STRING | TokenFlags::RAW_STRING_LOWERCASE; } @@ -745,8 +755,8 @@ impl<'src> Lexer<'src> { true } - /// Lex a f-string start token. - fn lex_fstring_start(&mut self, quote: char) -> TokenKind { + /// Lex a f-string or t-string start token if positioned at the start of an f-string or t-string. + fn lex_interpolated_string_start(&mut self, quote: char) -> Option { #[cfg(debug_assertions)] debug_assert_eq!(self.cursor.previous(), quote); @@ -758,27 +768,31 @@ impl<'src> Lexer<'src> { self.current_flags |= TokenFlags::TRIPLE_QUOTED_STRING; } - self.fstrings - .push(FStringContext::new(self.current_flags, self.nesting)); + let ftcontext = InterpolatedStringContext::new(self.current_flags, self.nesting)?; + + let kind = ftcontext.kind(); + + self.interpolated_strings.push(ftcontext); - TokenKind::FStringStart + Some(kind.start_token()) } - /// Lex a f-string middle or end token. - fn lex_fstring_middle_or_end(&mut self) -> Option { + /// Lex an f-string or t-string middle or end token. + fn lex_interpolated_string_middle_or_end(&mut self) -> Option { // SAFETY: Safe because the function is only called when `self.fstrings` is not empty. - let fstring = self.fstrings.current().unwrap(); + let interpolated_string = self.interpolated_strings.current().unwrap(); + let string_kind = interpolated_string.kind(); // Check if we're at the end of the f-string. - if fstring.is_triple_quoted() { - let quote_char = fstring.quote_char(); + if interpolated_string.is_triple_quoted() { + let quote_char = interpolated_string.quote_char(); if self.cursor.eat_char3(quote_char, quote_char, quote_char) { - self.current_flags = fstring.flags(); - return Some(TokenKind::FStringEnd); + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); } - } else if self.cursor.eat_char(fstring.quote_char()) { - self.current_flags = fstring.flags(); - return Some(TokenKind::FStringEnd); + } else if self.cursor.eat_char(interpolated_string.quote_char()) { + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); } // We have to decode `{{` and `}}` into `{` and `}` respectively. As an @@ -790,7 +804,7 @@ impl<'src> Lexer<'src> { let mut last_offset = self.offset(); // This isn't going to change for the duration of the loop. - let in_format_spec = fstring.is_in_format_spec(self.nesting); + let in_format_spec = interpolated_string.is_in_format_spec(self.nesting); let mut in_named_unicode = false; @@ -800,28 +814,29 @@ impl<'src> Lexer<'src> { // in the source code and the one returned by `self.cursor.first()` when // we reach the end of the source code. EOF_CHAR if self.cursor.is_eof() => { - let error = if fstring.is_triple_quoted() { - FStringErrorType::UnterminatedTripleQuotedString + let error = if interpolated_string.is_triple_quoted() { + InterpolatedStringErrorType::UnterminatedTripleQuotedString } else { - FStringErrorType::UnterminatedString + InterpolatedStringErrorType::UnterminatedString }; - self.fstrings.pop(); + self.interpolated_strings.pop(); return Some(self.push_error(LexicalError::new( - LexicalErrorType::FStringError(error), + LexicalErrorType::from_interpolated_string_error(error, string_kind), self.token_range(), ))); } - '\n' | '\r' if !fstring.is_triple_quoted() => { - // If we encounter a newline while we're in a format spec, then - // we stop here and let the lexer emit the newline token. - // - // Relevant discussion: https://github.com/python/cpython/issues/110259 - if in_format_spec { - break; - } - self.fstrings.pop(); + '\n' | '\r' if !interpolated_string.is_triple_quoted() => { + // https://github.com/astral-sh/ruff/issues/18632 + self.interpolated_strings.pop(); + + let error_type = if in_format_spec { + InterpolatedStringErrorType::NewlineInFormatSpec + } else { + InterpolatedStringErrorType::UnterminatedString + }; + return Some(self.push_error(LexicalError::new( - LexicalErrorType::FStringError(FStringErrorType::UnterminatedString), + LexicalErrorType::from_interpolated_string_error(error_type, string_kind), self.token_range(), ))); } @@ -831,7 +846,7 @@ impl<'src> Lexer<'src> { // Don't consume `{` or `}` as we want them to be emitted as tokens. // They will be handled in the next iteration. continue; - } else if !fstring.is_raw_string() { + } else if !interpolated_string.is_raw_string() { if self.cursor.eat_char2('N', '{') { in_named_unicode = true; continue; @@ -844,8 +859,8 @@ impl<'src> Lexer<'src> { self.cursor.bump(); } } - quote @ ('\'' | '"') if quote == fstring.quote_char() => { - if let Some(triple_quotes) = fstring.triple_quotes() { + quote @ ('\'' | '"') if quote == interpolated_string.quote_char() => { + if let Some(triple_quotes) = interpolated_string.triple_quotes() { if self.cursor.rest().starts_with(triple_quotes) { break; } @@ -896,10 +911,10 @@ impl<'src> Lexer<'src> { normalized }; - self.current_value = TokenValue::FStringMiddle(value.into_boxed_str()); + self.current_value = TokenValue::InterpolatedStringMiddle(value.into_boxed_str()); - self.current_flags = fstring.flags(); - Some(TokenKind::FStringMiddle) + self.current_flags = interpolated_string.flags(); + Some(string_kind.middle_token()) } /// Lex a string literal. @@ -1156,7 +1171,7 @@ impl<'src> Lexer<'src> { return self.push_error(LexicalError::new( LexicalErrorType::OtherError(format!("{err:?}").into_boxed_str()), self.token_range(), - )) + )); } }; self.current_value = TokenValue::Int(value); @@ -1407,9 +1422,9 @@ impl<'src> Lexer<'src> { // i.e., it recovered from an unclosed parenthesis (`(`, `[`, or `{`). self.nesting -= 1; - // The lexer can't be moved back for a triple-quoted f-string because the newlines are - // part of the f-string itself, so there is no newline token to be emitted. - if self.current_flags.is_triple_quoted_fstring() { + // The lexer can't be moved back for a triple-quoted f/t-string because the newlines are + // part of the f/t-string itself, so there is no newline token to be emitted. + if self.current_flags.is_triple_quoted_interpolated_string() { return false; } @@ -1459,7 +1474,7 @@ impl<'src> Lexer<'src> { /// Retrieves the current offset of the cursor within the source code. // SAFETY: Lexer doesn't allow files larger than 4GB - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] #[inline] fn offset(&self) -> TextSize { TextSize::new(self.source.len() as u32) - self.cursor.text_len() @@ -1482,7 +1497,7 @@ impl<'src> Lexer<'src> { nesting: self.nesting, indentations_checkpoint: self.indentations.checkpoint(), pending_indentation: self.pending_indentation, - fstrings_checkpoint: self.fstrings.checkpoint(), + interpolated_strings_checkpoint: self.interpolated_strings.checkpoint(), errors_position: self.errors.len(), } } @@ -1499,7 +1514,7 @@ impl<'src> Lexer<'src> { nesting, indentations_checkpoint, pending_indentation, - fstrings_checkpoint, + interpolated_strings_checkpoint, errors_position, } = checkpoint; @@ -1516,7 +1531,8 @@ impl<'src> Lexer<'src> { self.nesting = nesting; self.indentations.rewind(indentations_checkpoint); self.pending_indentation = pending_indentation; - self.fstrings.rewind(fstrings_checkpoint); + self.interpolated_strings + .rewind(interpolated_strings_checkpoint); self.errors.truncate(errors_position); } @@ -1535,7 +1551,7 @@ pub(crate) struct LexerCheckpoint { nesting: u32, indentations_checkpoint: IndentationsCheckpoint, pending_indentation: Option, - fstrings_checkpoint: FStringsCheckpoint, + interpolated_strings_checkpoint: InterpolatedStringsCheckpoint, errors_position: usize, } @@ -1750,6 +1766,7 @@ mod tests { } } + #[track_caller] fn lex_valid(source: &str, mode: Mode, start_offset: TextSize) -> LexerOutput { let output = lex(source, mode, start_offset); @@ -1765,6 +1782,7 @@ mod tests { output } + #[track_caller] fn lex_invalid(source: &str, mode: Mode) -> LexerOutput { let output = lex(source, mode, TextSize::default()); @@ -1776,14 +1794,17 @@ mod tests { output } + #[track_caller] fn lex_source(source: &str) -> LexerOutput { lex_valid(source, Mode::Module, TextSize::default()) } + #[track_caller] fn lex_source_with_offset(source: &str, start_offset: TextSize) -> LexerOutput { lex_valid(source, Mode::Module, start_offset) } + #[track_caller] fn lex_jupyter_source(source: &str) -> LexerOutput { lex_valid(source, Mode::Ipython, TextSize::default()) } @@ -1956,8 +1977,7 @@ def f(arg=%timeit a = b): #[test] fn test_numbers() { - let source = - "0x2f 0o12 0b1101 0 123 123_45_67_890 0.2 1e+2 2.1e3 2j 2.2j 000 0x995DC9BBDF1939FA 0x995DC9BBDF1939FA995DC9BBDF1939FA"; + let source = "0x2f 0o12 0b1101 0 123 123_45_67_890 0.2 1e+2 2.1e3 2j 2.2j 000 0x995DC9BBDF1939FA 0x995DC9BBDF1939FA995DC9BBDF1939FA"; assert_snapshot!(lex_source(source)); } @@ -2377,6 +2397,13 @@ f'''__{ b c }__''' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_newline_format_spec() { + let source = r" f'__{ x:d }__' @@ -2385,7 +2412,7 @@ f'__{ b }__' "; - assert_snapshot!(lex_source(source)); + assert_snapshot!(lex_invalid(source, Mode::Module)); } #[test] @@ -2455,6 +2482,197 @@ f"{(lambda x:{x})}" assert_snapshot!(lex_source(source)); } + #[test] + fn test_empty_tstrings() { + let source = r#"t"" "" t"" t'' '' t"""""" t''''''"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_prefix() { + let source = r#"t"" t"" rt"" rt"" Rt"" Rt"" tr"" Tr"" tR"" TR"""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring() { + let source = r#"t"normal {foo} {{another}} {bar} {{{three}}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_parentheses() { + let source = r#"t"{}" t"{{}}" t" {}" t"{{{}}}" t"{{{{}}}}" t" {} {{}} {{{}}} {{{{}}}} ""#; + assert_snapshot!(lex_source(source)); + } + + fn tstring_single_quote_escape_eol(eol: &str) -> LexerOutput { + let source = format!(r"t'text \{eol} more text'"); + lex_source(&source) + } + + #[test] + fn test_tstring_single_quote_escape_unix_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(UNIX_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_mac_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(MAC_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_windows_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(WINDOWS_EOL)); + } + + #[test] + fn test_tstring_escape() { + let source = r#"t"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_braces() { + let source = r"t'\{foo}' t'\\{foo}' t'\{{foo}}' t'\\{{foo}}'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_raw() { + let source = r#"rt"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode() { + let source = r#"t"\N{BULLET} normal \Nope \N""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode_raw() { + let source = r#"rt"\N{BULLET} normal""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_named_expression() { + let source = r#"t"{x:=10} {(x:=10)} {x,{y:=10}} {[x:=10]}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_format_spec() { + let source = r#"t"{foo:} {x=!s:.3f} {x:.{y}f} {'':*^{1:{1}}} {x:{{1}.pop()}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_multiline_format_spec() { + // The last t-string is invalid syntactically but we should still lex it. + // Note that the `b` is a `Name` token and not a `TStringMiddle` token. + let source = r"t'''__{ + x:d +}__''' +t'''__{ + x:a + b + c +}__''' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_newline_format_spec() { + let source = r" +t'__{ + x:d +}__' +t'__{ + x:a + b +}__' +"; + assert_snapshot!(lex_invalid(source, Mode::Module)); + } + + #[test] + fn test_tstring_conversion() { + let source = r#"t"{x!s} {x=!r} {x:.3f!r} {{x!r}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_nested() { + let source = r#"t"foo {t"bar {x + t"{wow}"}"} baz" t'foo {t'bar'} some {t"another"}'"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_expression_multiline() { + let source = r#"t"first { + x + * + y +} second""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_multiline() { + let source = r#"t""" +hello + world +""" t''' + world +hello +''' t"some {t"""multiline +allowed {x}"""} string""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_comments() { + let source = r#"t""" +# not a comment { # comment { + x +} # not a comment +""""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_ipy_escape_command() { + let source = r#"t"foo {!pwd} bar""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_lambda_expression() { + let source = r#" +t"{lambda x:{x}}" +t"{(lambda x:{x})}" +"# + .trim(); + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_nul_char() { + let source = r"t'\0'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_nested_t_and_fstring() { + let source = r#"t"foo {f"bar {x + t"{wow}"}"} baz" f'foo {t'bar'!r} some {f"another"}'"#; + assert_snapshot!(lex_source(source)); + } + #[test] fn test_match_softkeyword_in_notebook() { let source = r"match foo: @@ -2463,7 +2681,7 @@ f"{(lambda x:{x})}" assert_snapshot!(lex_jupyter_source(source)); } - fn lex_fstring_error(source: &str) -> FStringErrorType { + fn lex_fstring_error(source: &str) -> InterpolatedStringErrorType { let output = lex(source, Mode::Module, TextSize::default()); match output .errors @@ -2479,7 +2697,9 @@ f"{(lambda x:{x})}" #[test] fn test_fstring_error() { - use FStringErrorType::{SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString}; + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; assert_eq!(lex_fstring_error("f'}'"), SingleRbrace); assert_eq!(lex_fstring_error("f'{{}'"), SingleRbrace); @@ -2504,4 +2724,48 @@ f"{(lambda x:{x})}" UnterminatedTripleQuotedString ); } + + fn lex_tstring_error(source: &str) -> InterpolatedStringErrorType { + let output = lex(source, Mode::Module, TextSize::default()); + match output + .errors + .into_iter() + .next() + .expect("lexer should give at least one error") + .into_error() + { + LexicalErrorType::TStringError(error) => error, + err => panic!("Expected TStringError: {err:?}"), + } + } + + #[test] + fn test_tstring_error() { + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; + + assert_eq!(lex_tstring_error("t'}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'foo}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\u007b}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{a:b}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{3:}}>10}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\{foo}\}'"), SingleRbrace); + + assert_eq!(lex_tstring_error(r#"t""#), UnterminatedString); + assert_eq!(lex_tstring_error(r"t'"), UnterminatedString); + + assert_eq!(lex_tstring_error(r#"t""""#), UnterminatedTripleQuotedString); + assert_eq!(lex_tstring_error(r"t'''"), UnterminatedTripleQuotedString); + assert_eq!( + lex_tstring_error(r#"t"""""#), + UnterminatedTripleQuotedString + ); + assert_eq!( + lex_tstring_error(r#"t""""""#), + UnterminatedTripleQuotedString + ); + } } diff --git a/crates/ruff_python_parser/src/lexer/cursor.rs b/crates/ruff_python_parser/src/lexer/cursor.rs index d1107f18ef2b1..413d648ca00b2 100644 --- a/crates/ruff_python_parser/src/lexer/cursor.rs +++ b/crates/ruff_python_parser/src/lexer/cursor.rs @@ -62,7 +62,7 @@ impl<'src> Cursor<'src> { /// /// Use [`Cursor::rest`] to get the remaining text. // SAFETY: The `source.text_len` call in `new` would panic if the string length is larger than a `u32`. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] pub(super) fn text_len(&self) -> TextSize { TextSize::new(self.chars.as_str().len() as u32) } diff --git a/crates/ruff_python_parser/src/lexer/fstring.rs b/crates/ruff_python_parser/src/lexer/fstring.rs deleted file mode 100644 index 7b702a77b7269..0000000000000 --- a/crates/ruff_python_parser/src/lexer/fstring.rs +++ /dev/null @@ -1,143 +0,0 @@ -use ruff_python_ast::StringFlags; - -use super::TokenFlags; - -/// The context representing the current f-string that the lexer is in. -#[derive(Clone, Debug)] -pub(crate) struct FStringContext { - flags: TokenFlags, - - /// The level of nesting for the lexer when it entered the current f-string. - /// The nesting level includes all kinds of parentheses i.e., round, square, - /// and curly. - nesting: u32, - - /// The current depth of format spec for the current f-string. This is because - /// there can be multiple format specs nested for the same f-string. - /// For example, `{a:{b:{c}}}` has 3 format specs. - format_spec_depth: u32, -} - -impl FStringContext { - pub(crate) const fn new(flags: TokenFlags, nesting: u32) -> Self { - assert!(flags.is_f_string()); - - Self { - flags, - nesting, - format_spec_depth: 0, - } - } - - pub(crate) const fn flags(&self) -> TokenFlags { - self.flags - } - - pub(crate) const fn nesting(&self) -> u32 { - self.nesting - } - - /// Returns the quote character for the current f-string. - pub(crate) fn quote_char(&self) -> char { - self.flags.quote_style().as_char() - } - - /// Returns the triple quotes for the current f-string if it is a triple-quoted - /// f-string, `None` otherwise. - pub(crate) fn triple_quotes(&self) -> Option<&'static str> { - if self.is_triple_quoted() { - Some(self.flags.quote_str()) - } else { - None - } - } - - /// Returns `true` if the current f-string is a raw f-string. - pub(crate) fn is_raw_string(&self) -> bool { - self.flags.is_raw_string() - } - - /// Returns `true` if the current f-string is a triple-quoted f-string. - pub(crate) fn is_triple_quoted(&self) -> bool { - self.flags.is_triple_quoted() - } - - /// Calculates the number of open parentheses for the current f-string - /// based on the current level of nesting for the lexer. - const fn open_parentheses_count(&self, current_nesting: u32) -> u32 { - current_nesting.saturating_sub(self.nesting) - } - - /// Returns `true` if the lexer is in a f-string expression i.e., between - /// two curly braces. - pub(crate) const fn is_in_expression(&self, current_nesting: u32) -> bool { - self.open_parentheses_count(current_nesting) > self.format_spec_depth - } - - /// Returns `true` if the lexer is in a f-string format spec i.e., after a colon. - pub(crate) const fn is_in_format_spec(&self, current_nesting: u32) -> bool { - self.format_spec_depth > 0 && !self.is_in_expression(current_nesting) - } - - /// Returns `true` if the context is in a valid position to start format spec - /// i.e., at the same level of nesting as the opening parentheses token. - /// Increments the format spec depth if it is. - /// - /// This assumes that the current character for the lexer is a colon (`:`). - pub(crate) fn try_start_format_spec(&mut self, current_nesting: u32) -> bool { - if self - .open_parentheses_count(current_nesting) - .saturating_sub(self.format_spec_depth) - == 1 - { - self.format_spec_depth += 1; - true - } else { - false - } - } - - /// Decrements the format spec depth if the current f-string is in a format - /// spec. - pub(crate) fn try_end_format_spec(&mut self, current_nesting: u32) { - if self.is_in_format_spec(current_nesting) { - self.format_spec_depth = self.format_spec_depth.saturating_sub(1); - } - } -} - -/// The f-strings stack is used to keep track of all the f-strings that the -/// lexer encounters. This is necessary because f-strings can be nested. -#[derive(Debug, Default)] -pub(crate) struct FStrings { - stack: Vec, -} - -impl FStrings { - pub(crate) fn push(&mut self, context: FStringContext) { - self.stack.push(context); - } - - pub(crate) fn pop(&mut self) -> Option { - self.stack.pop() - } - - pub(crate) fn current(&self) -> Option<&FStringContext> { - self.stack.last() - } - - pub(crate) fn current_mut(&mut self) -> Option<&mut FStringContext> { - self.stack.last_mut() - } - - pub(crate) fn checkpoint(&self) -> FStringsCheckpoint { - FStringsCheckpoint(self.stack.clone()) - } - - pub(crate) fn rewind(&mut self, checkpoint: FStringsCheckpoint) { - self.stack = checkpoint.0; - } -} - -#[derive(Debug, Clone)] -pub(crate) struct FStringsCheckpoint(Vec); diff --git a/crates/ruff_python_parser/src/lexer/interpolated_string.rs b/crates/ruff_python_parser/src/lexer/interpolated_string.rs new file mode 100644 index 0000000000000..826edfa79642e --- /dev/null +++ b/crates/ruff_python_parser/src/lexer/interpolated_string.rs @@ -0,0 +1,157 @@ +use ruff_python_ast::StringFlags; + +use crate::string::InterpolatedStringKind; + +use super::TokenFlags; + +/// The context representing the current f-string or t-string that the lexer is in. +#[derive(Clone, Debug)] +pub(crate) struct InterpolatedStringContext { + flags: TokenFlags, + + /// The level of nesting for the lexer when it entered the current f/t-string. + /// The nesting level includes all kinds of parentheses i.e., round, square, + /// and curly. + nesting: u32, + + /// The current depth of format spec for the current f/t-string. This is because + /// there can be multiple format specs nested for the same f-string. + /// For example, `{a:{b:{c}}}` has 3 format specs. + format_spec_depth: u32, +} + +impl InterpolatedStringContext { + pub(crate) const fn new(flags: TokenFlags, nesting: u32) -> Option { + if flags.is_interpolated_string() { + Some(Self { + flags, + nesting, + format_spec_depth: 0, + }) + } else { + None + } + } + + pub(crate) fn kind(&self) -> InterpolatedStringKind { + if self.flags.is_f_string() { + InterpolatedStringKind::FString + } else if self.flags.is_t_string() { + InterpolatedStringKind::TString + } else { + unreachable!("Can only be constructed when f-string or t-string flag is present") + } + } + + pub(crate) const fn flags(&self) -> TokenFlags { + self.flags + } + + pub(crate) const fn nesting(&self) -> u32 { + self.nesting + } + + /// Returns the quote character for the current f-string. + pub(crate) fn quote_char(&self) -> char { + self.flags.quote_style().as_char() + } + + /// Returns the triple quotes for the current f-string if it is a triple-quoted + /// f-string, `None` otherwise. + pub(crate) fn triple_quotes(&self) -> Option<&'static str> { + if self.is_triple_quoted() { + Some(self.flags.quote_str()) + } else { + None + } + } + + /// Returns `true` if the current f-string is a raw f-string. + pub(crate) fn is_raw_string(&self) -> bool { + self.flags.is_raw_string() + } + + /// Returns `true` if the current f-string is a triple-quoted f-string. + pub(crate) fn is_triple_quoted(&self) -> bool { + self.flags.is_triple_quoted() + } + + /// Calculates the number of open parentheses for the current f-string + /// based on the current level of nesting for the lexer. + const fn open_parentheses_count(&self, current_nesting: u32) -> u32 { + current_nesting.saturating_sub(self.nesting) + } + + /// Returns `true` if the lexer is in an f-string expression or t-string interpolation i.e., between + /// two curly braces. + pub(crate) const fn is_in_interpolation(&self, current_nesting: u32) -> bool { + self.open_parentheses_count(current_nesting) > self.format_spec_depth + } + + /// Returns `true` if the lexer is in a f-string format spec i.e., after a colon. + pub(crate) const fn is_in_format_spec(&self, current_nesting: u32) -> bool { + self.format_spec_depth > 0 && !self.is_in_interpolation(current_nesting) + } + + /// Returns `true` if the context is in a valid position to start format spec + /// i.e., at the same level of nesting as the opening parentheses token. + /// Increments the format spec depth if it is. + /// + /// This assumes that the current character for the lexer is a colon (`:`). + pub(crate) fn try_start_format_spec(&mut self, current_nesting: u32) -> bool { + if self + .open_parentheses_count(current_nesting) + .saturating_sub(self.format_spec_depth) + == 1 + { + self.format_spec_depth += 1; + true + } else { + false + } + } + + /// Decrements the format spec depth if the current f-string is in a format + /// spec. + pub(crate) fn try_end_format_spec(&mut self, current_nesting: u32) { + if self.is_in_format_spec(current_nesting) { + self.format_spec_depth = self.format_spec_depth.saturating_sub(1); + } + } +} + +/// The interpolated strings stack is used to keep track of all the f-strings and t-strings that the +/// lexer encounters. This is necessary because f-strings and t-strings can be nested. +#[derive(Debug, Default)] +pub(crate) struct InterpolatedStrings { + stack: Vec, +} + +impl InterpolatedStrings { + pub(crate) fn push(&mut self, context: InterpolatedStringContext) { + self.stack.push(context); + } + + pub(crate) fn pop(&mut self) -> Option { + self.stack.pop() + } + + pub(crate) fn current(&self) -> Option<&InterpolatedStringContext> { + self.stack.last() + } + + pub(crate) fn current_mut(&mut self) -> Option<&mut InterpolatedStringContext> { + self.stack.last_mut() + } + + pub(crate) fn checkpoint(&self) -> InterpolatedStringsCheckpoint { + InterpolatedStringsCheckpoint(self.stack.clone()) + } + + pub(crate) fn rewind(&mut self, checkpoint: InterpolatedStringsCheckpoint) { + self.stack = checkpoint.0; + } +} + +#[derive(Debug, Clone)] +pub(crate) struct InterpolatedStringsCheckpoint(Vec); diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index eae7ea75ab841..54061c58e8c7b 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -67,8 +67,8 @@ use std::iter::FusedIterator; use std::ops::Deref; pub use crate::error::{ - FStringErrorType, LexicalErrorType, ParseError, ParseErrorType, UnsupportedSyntaxError, - UnsupportedSyntaxErrorKind, + InterpolatedStringErrorType, LexicalErrorType, ParseError, ParseErrorType, + UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, }; pub use crate::parser::ParseOptions; pub use crate::token::{Token, TokenKind}; @@ -208,13 +208,14 @@ pub fn parse_parenthesized_expression_range( /// /// ``` /// use ruff_python_parser::parse_string_annotation; -/// use ruff_python_ast::{StringLiteral, StringLiteralFlags}; +/// use ruff_python_ast::{StringLiteral, StringLiteralFlags, AtomicNodeIndex}; /// use ruff_text_size::{TextRange, TextSize}; /// /// let string = StringLiteral { /// value: "'''\n int | str'''".to_string().into_boxed_str(), /// flags: StringLiteralFlags::empty(), /// range: TextRange::new(TextSize::new(0), TextSize::new(16)), +/// node_index: AtomicNodeIndex::dummy() /// }; /// let parsed = parse_string_annotation("'''\n int | str'''", &string); /// assert!(!parsed.is_ok()); @@ -303,7 +304,7 @@ pub fn parse_unchecked_source(source: &str, source_type: PySourceType) -> Parsed } /// Represents the parsed source code. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, get_size2::GetSize)] pub struct Parsed { syntax: T, tokens: Tokens, @@ -473,7 +474,7 @@ impl Parsed { } /// Tokens represents a vector of lexed [`Token`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct Tokens { raw: Vec, } @@ -637,6 +638,41 @@ impl Tokens { } } + /// Returns a slice of tokens before the given [`TextSize`] offset. + /// + /// If the given offset is between two tokens, the returned slice will end just before the + /// following token. In other words, if the offset is between the end of previous token and + /// start of next token, the returned slice will end just before the next token. + /// + /// # Panics + /// + /// If the given offset is inside a token range at any point + /// other than the start of the range. + pub fn before(&self, offset: TextSize) -> &[Token] { + match self.binary_search_by(|token| token.start().cmp(&offset)) { + Ok(idx) => &self[..idx], + Err(idx) => { + // We can't use `saturating_sub` here because a file could contain a BOM header, in + // which case the token starts at offset 3 for UTF-8 encoded file content. + if idx > 0 { + if let Some(prev) = self.get(idx - 1) { + // If it's equal to the end offset, then it's at a token boundary which is + // valid. If it's greater than the end offset, then it's in the gap between + // the tokens which is valid as well. + assert!( + offset >= prev.end(), + "Offset {:?} is inside a token range {:?}", + offset, + prev.range() + ); + } + } + + &self[..idx] + } + } + } + /// Returns a slice of tokens after the given [`TextSize`] offset. /// /// If the given offset is between two tokens, the returned slice will start from the following @@ -645,7 +681,8 @@ impl Tokens { /// /// # Panics /// - /// If the given offset is inside a token range. + /// If the given offset is inside a token range at any point + /// other than the start of the range. pub fn after(&self, offset: TextSize) -> &[Token] { match self.binary_search_by(|token| token.start().cmp(&offset)) { Ok(idx) => &self[idx..], @@ -947,6 +984,68 @@ mod tests { tokens.after(TextSize::new(5)); } + #[test] + fn tokens_before_offset_at_first_token_start() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(0)); + assert_eq!(before.len(), 0); + } + + #[test] + fn tokens_before_offset_after_first_token_gap() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(3)); + assert_eq!(before.len(), 1); + assert_eq!(before.last().unwrap().kind(), TokenKind::Def); + } + + #[test] + fn tokens_before_offset_at_second_token_start() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(4)); + assert_eq!(before.len(), 1); + assert_eq!(before.last().unwrap().kind(), TokenKind::Def); + } + + #[test] + fn tokens_before_offset_at_token_start() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(8)); + assert_eq!(before.len(), 3); + assert_eq!(before.last().unwrap().kind(), TokenKind::Lpar); + } + + #[test] + fn tokens_before_offset_at_token_end() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(11)); + assert_eq!(before.len(), 6); + assert_eq!(before.last().unwrap().kind(), TokenKind::Newline); + } + + #[test] + fn tokens_before_offset_between_tokens() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(13)); + assert_eq!(before.len(), 6); + assert_eq!(before.last().unwrap().kind(), TokenKind::Newline); + } + + #[test] + fn tokens_before_offset_at_last_token_end() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + let before = tokens.before(TextSize::new(33)); + assert_eq!(before.len(), 10); + assert_eq!(before.last().unwrap().kind(), TokenKind::Pass); + } + + #[test] + #[should_panic(expected = "Offset 5 is inside a token range 4..7")] + fn tokens_before_offset_inside_token() { + let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); + tokens.before(TextSize::new(5)); + } + #[test] fn tokens_in_range_at_token_offset() { let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter()); diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 3ccfb9935123a..e1f5f8c1245aa 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -6,22 +6,27 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_python_ast::name::Name; use ruff_python_ast::{ - self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements, - IpyEscapeKind, Number, Operator, OperatorPrecedence, StringFlags, UnaryOp, + self as ast, AnyStringFlags, AtomicNodeIndex, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, + FString, InterpolatedStringElement, InterpolatedStringElements, IpyEscapeKind, Number, + Operator, OperatorPrecedence, StringFlags, TString, UnaryOp, }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::error::{FStringKind, StarTupleKind, UnparenthesizedNamedExprKind}; use crate::parser::progress::ParserProgress; -use crate::parser::{helpers, FunctionKind, Parser}; -use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType}; +use crate::parser::{FunctionKind, Parser, helpers}; +use crate::string::{ + InterpolatedStringKind, StringType, parse_interpolated_string_literal_element, + parse_string_literal, +}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; use crate::{ - FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, + InterpolatedStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, + UnsupportedSyntaxErrorKind, }; -use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; +use super::{InterpolatedStringElementsKind, Parenthesized, RecoveryContextKind}; /// A token set consisting of a newline or end of file. const NEWLINE_EOF_SET: TokenSet = TokenSet::new([TokenKind::Newline, TokenKind::EndOfFile]); @@ -54,6 +59,7 @@ pub(super) const EXPR_SET: TokenSet = TokenSet::new([ TokenKind::Not, TokenKind::Yield, TokenKind::FStringStart, + TokenKind::TStringStart, TokenKind::IpyEscapeCommand, ]) .union(LITERAL_SET); @@ -299,6 +305,7 @@ impl<'src> Parser<'src> { op: bin_op, right: Box::new(right.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } }; @@ -466,6 +473,7 @@ impl<'src> Parser<'src> { range: identifier.range, id: identifier.id, ctx, + node_index: AtomicNodeIndex::dummy(), } } @@ -481,13 +489,21 @@ impl<'src> Parser<'src> { let TokenValue::Name(name) = self.bump_value(TokenKind::Name) else { unreachable!(); }; - return ast::Identifier { id: name, range }; + return ast::Identifier { + id: name, + range, + node_index: AtomicNodeIndex::dummy(), + }; } if self.current_token_kind().is_soft_keyword() { let id = Name::new(self.src_text(range)); self.bump_soft_keyword_as_name(); - return ast::Identifier { id, range }; + return ast::Identifier { + id, + range, + node_index: AtomicNodeIndex::dummy(), + }; } if self.current_token_kind().is_keyword() { @@ -502,7 +518,11 @@ impl<'src> Parser<'src> { let id = Name::new(self.src_text(range)); self.bump_any(); - ast::Identifier { id, range } + ast::Identifier { + id, + range, + node_index: AtomicNodeIndex::dummy(), + } } else { self.add_error( ParseErrorType::OtherError("Expected an identifier".into()), @@ -512,6 +532,7 @@ impl<'src> Parser<'src> { ast::Identifier { id: Name::empty(), range: self.missing_node_range(), + node_index: AtomicNodeIndex::dummy(), } } } @@ -531,6 +552,7 @@ impl<'src> Parser<'src> { Expr::NumberLiteral(ast::ExprNumberLiteral { value: Number::Float(value), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Complex => { @@ -540,6 +562,7 @@ impl<'src> Parser<'src> { Expr::NumberLiteral(ast::ExprNumberLiteral { value: Number::Complex { real, imag }, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Int => { @@ -549,6 +572,7 @@ impl<'src> Parser<'src> { Expr::NumberLiteral(ast::ExprNumberLiteral { value: Number::Int(value), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::True => { @@ -556,6 +580,7 @@ impl<'src> Parser<'src> { Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: true, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::False => { @@ -563,25 +588,30 @@ impl<'src> Parser<'src> { Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: false, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::None => { self.bump(TokenKind::None); Expr::NoneLiteral(ast::ExprNoneLiteral { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Ellipsis => { self.bump(TokenKind::Ellipsis); Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Name => Expr::Name(self.parse_name()), TokenKind::IpyEscapeCommand => { Expr::IpyEscapeCommand(self.parse_ipython_escape_command_expression()) } - TokenKind::String | TokenKind::FStringStart => self.parse_strings(), + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { + self.parse_strings() + } TokenKind::Lpar => { return self.parse_parenthesized_expression(); } @@ -600,6 +630,7 @@ impl<'src> Parser<'src> { range: self.missing_node_range(), id: Name::empty(), ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::dummy(), }) } } @@ -642,6 +673,7 @@ impl<'src> Parser<'src> { func: Box::new(func), arguments, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -661,127 +693,135 @@ impl<'src> Parser<'src> { let mut seen_keyword_argument = false; // foo = 1 let mut seen_keyword_unpacking = false; // **foo - self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| { - let argument_start = parser.node_start(); - if parser.eat(TokenKind::DoubleStar) { - let value = parser.parse_conditional_expression_or_higher(); - - keywords.push(ast::Keyword { - arg: None, - value: value.expr, - range: parser.node_range(argument_start), - }); - - seen_keyword_unpacking = true; - } else { - let start = parser.node_start(); - let mut parsed_expr = parser - .parse_named_expression_or_higher(ExpressionContext::starred_conditional()); - - match parser.current_token_kind() { - TokenKind::Async | TokenKind::For => { - if parsed_expr.is_unparenthesized_starred_expr() { - parser.add_error( - ParseErrorType::IterableUnpackingInComprehension, - &parsed_expr, - ); - } - - parsed_expr = Expr::Generator(parser.parse_generator_expression( - parsed_expr.expr, - start, - Parenthesized::No, - )) - .into(); - } - _ => { - if seen_keyword_unpacking && parsed_expr.is_unparenthesized_starred_expr() { - parser.add_error( - ParseErrorType::InvalidArgumentUnpackingOrder, - &parsed_expr, - ); - } - } - } - - let arg_range = parser.node_range(start); - if parser.eat(TokenKind::Equal) { - seen_keyword_argument = true; - let arg = if let ParsedExpr { - expr: Expr::Name(ident_expr), - is_parenthesized, - } = parsed_expr - { - // test_ok parenthesized_kwarg_py37 - // # parse_options: {"target-version": "3.7"} - // f((a)=1) - - // test_err parenthesized_kwarg_py38 - // # parse_options: {"target-version": "3.8"} - // f((a)=1) - // f((a) = 1) - // f( ( a ) = 1) - - if is_parenthesized { - parser.add_unsupported_syntax_error( - UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, - arg_range, - ); - } - - ast::Identifier { - id: ident_expr.id, - range: ident_expr.range, - } - } else { - // TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's - // not a name expression. We could add the expression into `args` but - // that means the error is a missing comma instead. - parser.add_error( - ParseErrorType::OtherError("Expected a parameter name".to_string()), - &parsed_expr, - ); - ast::Identifier { - id: Name::empty(), - range: parsed_expr.range(), - } - }; - + let has_trailing_comma = + self.parse_comma_separated_list(RecoveryContextKind::Arguments, |parser| { + let argument_start = parser.node_start(); + if parser.eat(TokenKind::DoubleStar) { let value = parser.parse_conditional_expression_or_higher(); keywords.push(ast::Keyword { - arg: Some(arg), + arg: None, value: value.expr, range: parser.node_range(argument_start), + node_index: AtomicNodeIndex::dummy(), }); + + seen_keyword_unpacking = true; } else { - if !parsed_expr.is_unparenthesized_starred_expr() { - if seen_keyword_unpacking { - parser.add_error( - ParseErrorType::PositionalAfterKeywordUnpacking, - &parsed_expr, - ); - } else if seen_keyword_argument { + let start = parser.node_start(); + let mut parsed_expr = parser + .parse_named_expression_or_higher(ExpressionContext::starred_conditional()); + + match parser.current_token_kind() { + TokenKind::Async | TokenKind::For => { + if parsed_expr.is_unparenthesized_starred_expr() { + parser.add_error( + ParseErrorType::IterableUnpackingInComprehension, + &parsed_expr, + ); + } + + parsed_expr = Expr::Generator(parser.parse_generator_expression( + parsed_expr.expr, + start, + Parenthesized::No, + )) + .into(); + } + _ => { + if seen_keyword_unpacking + && parsed_expr.is_unparenthesized_starred_expr() + { + parser.add_error( + ParseErrorType::InvalidArgumentUnpackingOrder, + &parsed_expr, + ); + } + } + } + + let arg_range = parser.node_range(start); + if parser.eat(TokenKind::Equal) { + seen_keyword_argument = true; + let arg = if let ParsedExpr { + expr: Expr::Name(ident_expr), + is_parenthesized, + } = parsed_expr + { + // test_ok parenthesized_kwarg_py37 + // # parse_options: {"target-version": "3.7"} + // f((a)=1) + + // test_err parenthesized_kwarg_py38 + // # parse_options: {"target-version": "3.8"} + // f((a)=1) + // f((a) = 1) + // f( ( a ) = 1) + + if is_parenthesized { + parser.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName, + arg_range, + ); + } + + ast::Identifier { + id: ident_expr.id, + range: ident_expr.range, + node_index: AtomicNodeIndex::dummy(), + } + } else { + // TODO(dhruvmanila): Parser shouldn't drop the `parsed_expr` if it's + // not a name expression. We could add the expression into `args` but + // that means the error is a missing comma instead. parser.add_error( - ParseErrorType::PositionalAfterKeywordArgument, + ParseErrorType::OtherError("Expected a parameter name".to_string()), &parsed_expr, ); + ast::Identifier { + id: Name::empty(), + range: parsed_expr.range(), + node_index: AtomicNodeIndex::dummy(), + } + }; + + let value = parser.parse_conditional_expression_or_higher(); + + keywords.push(ast::Keyword { + arg: Some(arg), + value: value.expr, + range: parser.node_range(argument_start), + node_index: AtomicNodeIndex::dummy(), + }); + } else { + if !parsed_expr.is_unparenthesized_starred_expr() { + if seen_keyword_unpacking { + parser.add_error( + ParseErrorType::PositionalAfterKeywordUnpacking, + &parsed_expr, + ); + } else if seen_keyword_argument { + parser.add_error( + ParseErrorType::PositionalAfterKeywordArgument, + &parsed_expr, + ); + } } + args.push(parsed_expr.expr); } - args.push(parsed_expr.expr); } - } - }); + }); self.expect(TokenKind::Rpar); let arguments = ast::Arguments { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), args: args.into_boxed_slice(), keywords: keywords.into_boxed_slice(), }; - self.validate_arguments(&arguments); + self.validate_arguments(&arguments, has_trailing_comma); arguments } @@ -818,9 +858,11 @@ impl<'src> Parser<'src> { range: slice_range, id: Name::empty(), ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::dummy(), })), ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }; } @@ -840,6 +882,7 @@ impl<'src> Parser<'src> { ctx: ExprContext::Load, range: self.node_range(slice_start), parenthesized: false, + node_index: AtomicNodeIndex::dummy(), }); } else if slice.is_starred_expr() { // If the only slice element is a starred expression, that is represented @@ -850,6 +893,7 @@ impl<'src> Parser<'src> { ctx: ExprContext::Load, range: self.node_range(slice_start), parenthesized: false, + node_index: AtomicNodeIndex::dummy(), }); } @@ -898,6 +942,7 @@ impl<'src> Parser<'src> { slice: Box::new(slice), ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -991,6 +1036,7 @@ impl<'src> Parser<'src> { Expr::Slice(ast::ExprSlice { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), lower, upper, step, @@ -1021,6 +1067,7 @@ impl<'src> Parser<'src> { op, operand: Box::new(operand.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1045,6 +1092,7 @@ impl<'src> Parser<'src> { attr, ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1088,6 +1136,7 @@ impl<'src> Parser<'src> { values, op, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1167,6 +1216,7 @@ impl<'src> Parser<'src> { ops: operators.into_boxed_slice(), comparators: comparators.into_boxed_slice(), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1174,12 +1224,15 @@ impl<'src> Parser<'src> { /// /// # Panics /// - /// If the parser isn't positioned at a `String` or `FStringStart` token. + /// If the parser isn't positioned at a `String`, `FStringStart`, or `TStringStart` token. /// /// See: (Search "strings:") pub(super) fn parse_strings(&mut self) -> Expr { - const STRING_START_SET: TokenSet = - TokenSet::new([TokenKind::String, TokenKind::FStringStart]); + const STRING_START_SET: TokenSet = TokenSet::new([ + TokenKind::String, + TokenKind::FStringStart, + TokenKind::TStringStart, + ]); let start = self.node_start(); let mut strings = vec![]; @@ -1191,8 +1244,36 @@ impl<'src> Parser<'src> { if self.at(TokenKind::String) { strings.push(self.parse_string_or_byte_literal()); - } else { - strings.push(StringType::FString(self.parse_fstring())); + } else if self.at(TokenKind::FStringStart) { + strings.push(StringType::FString( + self.parse_interpolated_string(InterpolatedStringKind::FString) + .into(), + )); + } else if self.at(TokenKind::TStringStart) { + // test_ok template_strings_py314 + // # parse_options: {"target-version": "3.14"} + // t"{hey}" + // t'{there}' + // t"""what's + // happening?""" + // "implicitly"t"concatenated" + + // test_err template_strings_py313 + // # parse_options: {"target-version": "3.13"} + // t"{hey}" + // t'{there}' + // t"""what's + // happening?""" + // "implicitly"t"concatenated" + let string_type = StringType::TString( + self.parse_interpolated_string(InterpolatedStringKind::TString) + .into(), + ); + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::TemplateStrings, + string_type.range(), + ); + strings.push(string_type); } } @@ -1207,14 +1288,22 @@ impl<'src> Parser<'src> { StringType::Str(string) => Expr::StringLiteral(ast::ExprStringLiteral { value: ast::StringLiteralValue::single(string), range, + node_index: AtomicNodeIndex::dummy(), }), StringType::Bytes(bytes) => Expr::BytesLiteral(ast::ExprBytesLiteral { value: ast::BytesLiteralValue::single(bytes), range, + node_index: AtomicNodeIndex::dummy(), }), StringType::FString(fstring) => Expr::FString(ast::ExprFString { value: ast::FStringValue::single(fstring), range, + node_index: AtomicNodeIndex::dummy(), + }), + StringType::TString(tstring) => Expr::TString(ast::ExprTString { + value: ast::TStringValue::single(tstring), + range, + node_index: AtomicNodeIndex::dummy(), }), }, _ => self.handle_implicitly_concatenated_strings(strings, range), @@ -1233,11 +1322,13 @@ impl<'src> Parser<'src> { ) -> Expr { assert!(strings.len() > 1); + let mut has_tstring = false; let mut has_fstring = false; let mut byte_literal_count = 0; for string in &strings { match string { StringType::FString(_) => has_fstring = true, + StringType::TString(_) => has_tstring = true, StringType::Bytes(_) => byte_literal_count += 1, StringType::Str(_) => {} } @@ -1266,7 +1357,7 @@ impl<'src> Parser<'src> { ); } // Only construct a byte expression if all the literals are bytes - // otherwise, we'll try either string or f-string. This is to retain + // otherwise, we'll try either string, t-string, or f-string. This is to retain // as much information as possible. Ordering::Equal => { let mut values = Vec::with_capacity(strings.len()); @@ -1279,6 +1370,7 @@ impl<'src> Parser<'src> { return Expr::from(ast::ExprBytesLiteral { value: ast::BytesLiteralValue::concatenated(values), range, + node_index: AtomicNodeIndex::dummy(), }); } Ordering::Greater => unreachable!(), @@ -1307,7 +1399,7 @@ impl<'src> Parser<'src> { // ) // 2 + 2 - if !has_fstring { + if !has_fstring && !has_tstring { let mut values = Vec::with_capacity(strings.len()); for string in strings { values.push(match string { @@ -1318,6 +1410,29 @@ impl<'src> Parser<'src> { return Expr::from(ast::ExprStringLiteral { value: ast::StringLiteralValue::concatenated(values), range, + node_index: AtomicNodeIndex::dummy(), + }); + } + + if has_tstring { + let mut parts = Vec::with_capacity(strings.len()); + for string in strings { + match string { + StringType::TString(tstring) => parts.push(ast::TStringPart::TString(tstring)), + StringType::FString(fstring) => { + parts.push(ruff_python_ast::TStringPart::FString(fstring)); + } + StringType::Str(string) => parts.push(ast::TStringPart::Literal(string)), + StringType::Bytes(bytes) => parts.push(ast::TStringPart::Literal( + ast::StringLiteral::invalid(bytes.range()), + )), + } + } + + return Expr::from(ast::ExprTString { + value: ast::TStringValue::concatenated(parts), + range, + node_index: AtomicNodeIndex::dummy(), }); } @@ -1325,6 +1440,9 @@ impl<'src> Parser<'src> { for string in strings { match string { StringType::FString(fstring) => parts.push(ast::FStringPart::FString(fstring)), + StringType::TString(_) => { + unreachable!("expected no tstring parts by this point") + } StringType::Str(string) => parts.push(ast::FStringPart::Literal(string)), StringType::Bytes(bytes) => parts.push(ast::FStringPart::Literal( ast::StringLiteral::invalid(bytes.range()), @@ -1335,6 +1453,7 @@ impl<'src> Parser<'src> { Expr::from(ast::ExprFString { value: ast::FStringValue::concatenated(parts), range, + node_index: AtomicNodeIndex::dummy(), }) } @@ -1370,6 +1489,7 @@ impl<'src> Parser<'src> { value: Box::new([]), range, flags: ast::BytesLiteralFlags::from(flags).with_invalid(), + node_index: AtomicNodeIndex::dummy(), }) } else { // test_err invalid_string_literal @@ -1379,30 +1499,39 @@ impl<'src> Parser<'src> { value: "".into(), range, flags: ast::StringLiteralFlags::from(flags).with_invalid(), + node_index: AtomicNodeIndex::dummy(), }) } } } } - /// Parses a f-string. + /// Parses an f/t-string. /// /// This does not handle implicitly concatenated strings. /// /// # Panics /// - /// If the parser isn't positioned at a `FStringStart` token. + /// If the parser isn't positioned at an `FStringStart` or + /// `TStringStart` token. /// - /// See: (Search "fstring:") + /// See: (Search "fstring:" or "tstring:") /// See: - fn parse_fstring(&mut self) -> ast::FString { + fn parse_interpolated_string( + &mut self, + kind: InterpolatedStringKind, + ) -> InterpolatedStringData { let start = self.node_start(); let flags = self.tokens.current_flags().as_any_string_flags(); - self.bump(TokenKind::FStringStart); - let elements = self.parse_fstring_elements(flags, FStringElementsKind::Regular); + self.bump(kind.start_token()); + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::Regular, + kind, + ); - self.expect(TokenKind::FStringEnd); + self.expect(kind.end_token()); // test_ok pep701_f_string_py312 // # parse_options: {"target-version": "3.12"} @@ -1416,6 +1545,18 @@ impl<'src> Parser<'src> { // f"test {a \ // } more" # line continuation + // test_ok pep750_t_string_py314 + // # parse_options: {"target-version": "3.14"} + // t'Magic wand: { bag['wand'] }' # nested quotes + // t"{'\n'.join(a)}" # escape sequence + // t'''A complex trick: { + // bag['bag'] # comment + // }''' + // t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting + // t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes + // t"test {a \ + // } more" # line continuation + // test_ok pep701_f_string_py311 // # parse_options: {"target-version": "3.11"} // f"outer {'# not a comment'}" @@ -1441,10 +1582,12 @@ impl<'src> Parser<'src> { let range = self.node_range(start); - if !self.options.target_version.supports_pep_701() { + if !self.options.target_version.supports_pep_701() + && matches!(kind, InterpolatedStringKind::FString) + { let quote_bytes = flags.quote_str().as_bytes(); let quote_len = flags.quote_len(); - for expr in elements.expressions() { + for expr in elements.interpolations() { for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes()) { let slash_position = TextSize::try_from(slash_position).unwrap(); @@ -1468,10 +1611,10 @@ impl<'src> Parser<'src> { self.check_fstring_comments(range); } - ast::FString { + InterpolatedStringData { elements, range, - flags: ast::FStringFlags::from(flags), + flags, } } @@ -1487,80 +1630,88 @@ impl<'src> Parser<'src> { })); } - /// Parses a list of f-string elements. + /// Parses a list of f/t-string elements. /// /// # Panics /// - /// If the parser isn't positioned at a `{` or `FStringMiddle` token. - fn parse_fstring_elements( + /// If the parser isn't positioned at a `{`, `FStringMiddle`, + /// or `TStringMiddle` token. + fn parse_interpolated_string_elements( &mut self, flags: ast::AnyStringFlags, - kind: FStringElementsKind, - ) -> FStringElements { + elements_kind: InterpolatedStringElementsKind, + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedStringElements { let mut elements = vec![]; + let middle_token_kind = string_kind.middle_token(); + + self.parse_list( + RecoveryContextKind::InterpolatedStringElements(elements_kind), + |parser| { + let element = match parser.current_token_kind() { + TokenKind::Lbrace => ast::InterpolatedStringElement::from( + parser.parse_interpolated_element(flags, string_kind), + ), + tok if tok == middle_token_kind => { + let range = parser.current_token_range(); + let TokenValue::InterpolatedStringMiddle(value) = + parser.bump_value(middle_token_kind) + else { + unreachable!() + }; + InterpolatedStringElement::Literal( + parse_interpolated_string_literal_element(value, flags, range) + .unwrap_or_else(|lex_error| { + // test_err invalid_fstring_literal_element + // f'hello \N{INVALID} world' + // f"""hello \N{INVALID} world""" + let location = lex_error.location(); + parser.add_error( + ParseErrorType::Lexical(lex_error.into_error()), + location, + ); + ast::InterpolatedStringLiteralElement { + value: "".into(), + range, + node_index: AtomicNodeIndex::dummy(), + } + }), + ) + } + // `Invalid` tokens are created when there's a lexical error, so + // we ignore it here to avoid creating unexpected token errors + TokenKind::Unknown => { + parser.bump_any(); + return; + } + tok => { + // This should never happen because the list parsing will only + // call this closure for the above token kinds which are the same + // as in the FIRST set. + unreachable!( + "{}: unexpected token `{tok:?}` at {:?}", + string_kind, + parser.current_token_range() + ); + } + }; + elements.push(element); + }, + ); - self.parse_list(RecoveryContextKind::FStringElements(kind), |parser| { - let element = match parser.current_token_kind() { - TokenKind::Lbrace => { - FStringElement::Expression(parser.parse_fstring_expression_element(flags)) - } - TokenKind::FStringMiddle => { - let range = parser.current_token_range(); - let TokenValue::FStringMiddle(value) = - parser.bump_value(TokenKind::FStringMiddle) - else { - unreachable!() - }; - FStringElement::Literal( - parse_fstring_literal_element(value, flags, range).unwrap_or_else( - |lex_error| { - // test_err invalid_fstring_literal_element - // f'hello \N{INVALID} world' - // f"""hello \N{INVALID} world""" - let location = lex_error.location(); - parser.add_error( - ParseErrorType::Lexical(lex_error.into_error()), - location, - ); - ast::FStringLiteralElement { - value: "".into(), - range, - } - }, - ), - ) - } - // `Invalid` tokens are created when there's a lexical error, so - // we ignore it here to avoid creating unexpected token errors - TokenKind::Unknown => { - parser.bump_any(); - return; - } - tok => { - // This should never happen because the list parsing will only - // call this closure for the above token kinds which are the same - // as in the FIRST set. - unreachable!( - "f-string: unexpected token `{tok:?}` at {:?}", - parser.current_token_range() - ); - } - }; - elements.push(element); - }); - - FStringElements::from(elements) + ast::InterpolatedStringElements::from(elements) } - /// Parses a f-string expression element. + /// Parses an f/t-string expression element. /// /// # Panics /// /// If the parser isn't positioned at a `{` token. - fn parse_fstring_expression_element( + fn parse_interpolated_element( &mut self, flags: ast::AnyStringFlags, - ) -> ast::FStringExpressionElement { + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedElement { let start = self.node_start(); self.bump(TokenKind::Lbrace); @@ -1568,11 +1719,23 @@ impl<'src> Parser<'src> { // f"{}" // f"{ }" + // test_err t_string_empty_expression + // # parse_options: {"target-version": "3.14"} + // t"{}" + // t"{ }" + // test_err f_string_invalid_starred_expr // # Starred expression inside f-string has a minimum precedence of bitwise or. // f"{*}" // f"{*x and y}" // f"{*yield x}" + + // test_err t_string_invalid_starred_expr + // # parse_options: {"target-version": "3.14"} + // # Starred expression inside t-string has a minimum precedence of bitwise or. + // t"{*}" + // t"{*x and y}" + // t"{*yield x}" let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); if !value.is_parenthesized && value.expr.is_lambda_expr() { @@ -1582,8 +1745,15 @@ impl<'src> Parser<'src> { // test_err f_string_lambda_without_parentheses // f"{lambda x: x}" + + // test_err t_string_lambda_without_parentheses + // # parse_options: {"target-version": "3.14"} + // t"{lambda x: x}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::LambdaWithoutParentheses), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::LambdaWithoutParentheses, + string_kind, + ), value.range(), ); } @@ -1601,6 +1771,19 @@ impl<'src> Parser<'src> { let conversion = if self.eat(TokenKind::Exclamation) { let conversion_flag_range = self.current_token_range(); if self.at(TokenKind::Name) { + // test_err f_string_conversion_follows_exclamation + // f"{x! s}" + // t"{x! s}" + // f"{x! z}" + if self.prev_token_end != conversion_flag_range.start() { + self.add_error( + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::ConversionFlagNotImmediatelyAfterExclamation, + string_kind, + ), + TextRange::new(self.prev_token_end, conversion_flag_range.start()), + ); + } let TokenValue::Name(name) = self.bump_value(TokenKind::Name) else { unreachable!(); }; @@ -1611,8 +1794,15 @@ impl<'src> Parser<'src> { _ => { // test_err f_string_invalid_conversion_flag_name_tok // f"{x!z}" + + // test_err t_string_invalid_conversion_flag_name_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!z}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), conversion_flag_range, ); ConversionFlag::None @@ -1622,8 +1812,16 @@ impl<'src> Parser<'src> { // test_err f_string_invalid_conversion_flag_other_tok // f"{x!123}" // f"{x!'a'}" + + // test_err t_string_invalid_conversion_flag_other_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!123}" + // t"{x!'a'}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), conversion_flag_range, ); // TODO(dhruvmanila): Avoid dropping this token @@ -1636,10 +1834,15 @@ impl<'src> Parser<'src> { let format_spec = if self.eat(TokenKind::Colon) { let spec_start = self.node_start(); - let elements = self.parse_fstring_elements(flags, FStringElementsKind::FormatSpec); - Some(Box::new(ast::FStringFormatSpec { + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::FormatSpec, + string_kind, + ); + Some(Box::new(ast::InterpolatedStringFormatSpec { range: self.node_range(spec_start), elements, + node_index: AtomicNodeIndex::dummy(), })) } else { None @@ -1658,23 +1861,40 @@ impl<'src> Parser<'src> { // f"{" // f"""{""" + // test_err t_string_unclosed_lbrace + // # parse_options: {"target-version": "3.14"} + // t"{" + // t"{foo!r" + // t"{foo=" + // t"{" + // t"""{""" + // The lexer does emit `FStringEnd` for the following test cases: // test_err f_string_unclosed_lbrace_in_format_spec // f"hello {x:" // f"hello {x:.3f" + + // test_err t_string_unclosed_lbrace_in_format_spec + // # parse_options: {"target-version": "3.14"} + // t"hello {x:" + // t"hello {x:.3f" self.add_error( - ParseErrorType::FStringError(FStringErrorType::UnclosedLbrace), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnclosedLbrace, + string_kind, + ), self.current_token_range(), ); } - ast::FStringExpressionElement { + ast::InterpolatedElement { expression: Box::new(value.expr), debug_text, conversion, format_spec, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1704,6 +1924,7 @@ impl<'src> Parser<'src> { elts: vec![], ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }); } @@ -1755,6 +1976,7 @@ impl<'src> Parser<'src> { return Expr::Dict(ast::ExprDict { items: vec![], range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }); } @@ -1865,6 +2087,7 @@ impl<'src> Parser<'src> { elts: vec![], ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), parenthesized: true, }) .into(); @@ -1953,6 +2176,7 @@ impl<'src> Parser<'src> { elts, ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), parenthesized: parenthesized.is_yes(), } } @@ -1981,6 +2205,7 @@ impl<'src> Parser<'src> { elts, ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2029,6 +2254,7 @@ impl<'src> Parser<'src> { ast::ExprSet { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), elts, } } @@ -2071,6 +2297,7 @@ impl<'src> Parser<'src> { ast::ExprDict { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), items, } } @@ -2138,6 +2365,7 @@ impl<'src> Parser<'src> { ast::Comprehension { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), target: target.expr, iter: iter.expr, ifs, @@ -2167,6 +2395,7 @@ impl<'src> Parser<'src> { elt: Box::new(element), generators, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), parenthesized: parenthesized.is_yes(), } } @@ -2187,6 +2416,7 @@ impl<'src> Parser<'src> { elt: Box::new(element), generators, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2208,6 +2438,7 @@ impl<'src> Parser<'src> { value: Box::new(value), generators, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2227,6 +2458,7 @@ impl<'src> Parser<'src> { elt: Box::new(element), generators, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2263,6 +2495,7 @@ impl<'src> Parser<'src> { value: Box::new(parsed_expr.expr), ctx: ExprContext::Load, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2285,6 +2518,7 @@ impl<'src> Parser<'src> { ast::ExprAwait { value: Box::new(parsed_expr.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2333,6 +2567,7 @@ impl<'src> Parser<'src> { Expr::Yield(ast::ExprYield { value, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } @@ -2372,6 +2607,7 @@ impl<'src> Parser<'src> { Expr::YieldFrom(ast::ExprYieldFrom { value: Box::new(expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } @@ -2412,6 +2648,7 @@ impl<'src> Parser<'src> { target: Box::new(target), value: Box::new(value.expr), range, + node_index: AtomicNodeIndex::dummy(), } } @@ -2459,6 +2696,7 @@ impl<'src> Parser<'src> { body: Box::new(body.expr), parameters, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2483,6 +2721,7 @@ impl<'src> Parser<'src> { test: Box::new(test.expr), orelse: Box::new(orelse.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2508,6 +2747,7 @@ impl<'src> Parser<'src> { let command = ast::ExprIpyEscapeCommand { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), kind, value, }; @@ -2521,9 +2761,9 @@ impl<'src> Parser<'src> { /// Performs the following validations on the function call arguments: /// 1. There aren't any duplicate keyword argument - /// 2. If there are more than one argument (positional or keyword), all generator expressions - /// present should be parenthesized. - fn validate_arguments(&mut self, arguments: &ast::Arguments) { + /// 2. If there are more than one argument (positional or keyword) or a single argument with a + /// trailing comma, all generator expressions present should be parenthesized. + fn validate_arguments(&mut self, arguments: &ast::Arguments, has_trailing_comma: bool) { let mut all_arg_names = FxHashSet::with_capacity_and_hasher(arguments.keywords.len(), FxBuildHasher); @@ -2541,7 +2781,7 @@ impl<'src> Parser<'src> { } } - if arguments.len() > 1 { + if has_trailing_comma || arguments.len() > 1 { for arg in &*arguments.args { if let Some(ast::ExprGenerator { range, @@ -2550,11 +2790,14 @@ impl<'src> Parser<'src> { }) = arg.as_generator_expr() { // test_ok args_unparenthesized_generator + // zip((x for x in range(10)), (y for y in range(10))) // sum(x for x in range(10)) + // sum((x for x in range(10)),) // test_err args_unparenthesized_generator // sum(x for x in range(10), 5) // total(1, 2, x for x in range(5), 6) + // sum(x for x in range(10),) self.add_error(ParseErrorType::UnparenthesizedGeneratorExpression, range); } } @@ -2749,3 +2992,32 @@ impl ExpressionContext { } } } + +#[derive(Debug)] +struct InterpolatedStringData { + elements: InterpolatedStringElements, + range: TextRange, + flags: AnyStringFlags, +} + +impl From for FString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + node_index: AtomicNodeIndex::dummy(), + } + } +} + +impl From for TString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + node_index: AtomicNodeIndex::dummy(), + } + } +} diff --git a/crates/ruff_python_parser/src/parser/helpers.rs b/crates/ruff_python_parser/src/parser/helpers.rs index c3e84cf69c96d..de89746333d4f 100644 --- a/crates/ruff_python_parser/src/parser/helpers.rs +++ b/crates/ruff_python_parser/src/parser/helpers.rs @@ -1,7 +1,7 @@ use ruff_python_ast::{self as ast, CmpOp, Expr, ExprContext, Number}; use ruff_text_size::{Ranged, TextRange}; -use crate::{error::RelaxedDecoratorError, TokenKind}; +use crate::{TokenKind, error::RelaxedDecoratorError}; /// Set the `ctx` for `Expr::Id`, `Expr::Attribute`, `Expr::Subscript`, `Expr::Starred`, /// `Expr::Tuple` and `Expr::List`. If `expr` is either `Expr::Tuple` or `Expr::List`, @@ -58,7 +58,7 @@ pub(super) fn detect_invalid_pre_py39_decorator_node( Expr::Name(_) => return None, Expr::Attribute(attribute) => { - return detect_invalid_pre_py39_decorator_node(&attribute.value) + return detect_invalid_pre_py39_decorator_node(&attribute.value); } Expr::Call(_) => return Some((RelaxedDecoratorError::CallExpression, expr.range())), @@ -94,6 +94,7 @@ pub(super) fn detect_invalid_pre_py39_decorator_node( Expr::YieldFrom(_) => "`yield from` expression", Expr::Compare(_) => "comparison expression", Expr::FString(_) => "f-string", + Expr::TString(_) => "t-string", Expr::Named(_) => "assignment expression", Expr::Subscript(_) => "subscript expression", Expr::IpyEscapeCommand(_) => "IPython escape command", diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index c602ba7c7547a..904e92df95b8c 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use bitflags::bitflags; -use ruff_python_ast::{Mod, ModExpression, ModModule}; +use ruff_python_ast::{AtomicNodeIndex, Mod, ModExpression, ModModule}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::error::UnsupportedSyntaxError; @@ -132,6 +132,7 @@ impl<'src> Parser<'src> { ModExpression { body: Box::new(parsed_expr.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -149,6 +150,7 @@ impl<'src> Parser<'src> { ModModule { body, range: TextRange::new(self.start_offset, self.current_token_range().end()), + node_index: AtomicNodeIndex::dummy(), } } @@ -539,17 +541,19 @@ impl<'src> Parser<'src> { } /// Parses a comma separated list of elements where each element is parsed - /// sing the given `parse_element` function. + /// using the given `parse_element` function. /// /// The difference between this function and `parse_comma_separated_list_into_vec` /// is that this function does not return the parsed elements. Instead, it is the /// caller's responsibility to handle the parsed elements. This is the reason /// that the `parse_element` parameter is bound to [`FnMut`] instead of [`Fn`]. + /// + /// Returns `true` if there is a trailing comma present. fn parse_comma_separated_list( &mut self, recovery_context_kind: RecoveryContextKind, mut parse_element: impl FnMut(&mut Parser<'src>), - ) { + ) -> bool { let mut progress = ParserProgress::default(); let saved_context = self.recovery_context; @@ -659,6 +663,8 @@ impl<'src> Parser<'src> { } self.recovery_context = saved_context; + + trailing_comma_range.is_some() } #[cold] @@ -794,7 +800,7 @@ impl WithItemKind { } #[derive(Debug, PartialEq, Copy, Clone)] -enum FStringElementsKind { +enum InterpolatedStringElementsKind { /// The regular f-string elements. /// /// For example, the `"hello "`, `x`, and `" world"` elements in: @@ -812,14 +818,16 @@ enum FStringElementsKind { FormatSpec, } -impl FStringElementsKind { - const fn list_terminator(self) -> TokenKind { +impl InterpolatedStringElementsKind { + const fn list_terminators(self) -> TokenSet { match self { - FStringElementsKind::Regular => TokenKind::FStringEnd, + InterpolatedStringElementsKind::Regular => { + TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd]) + } // test_ok fstring_format_spec_terminator // f"hello {x:} world" // f"hello {x:.3f} world" - FStringElementsKind::FormatSpec => TokenKind::Rbrace, + InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]), } } } @@ -927,9 +935,8 @@ enum RecoveryContextKind { /// When parsing a list of items in a `with` statement WithItems(WithItemKind), - /// When parsing a list of f-string elements which are either literal elements - /// or expressions. - FStringElements(FStringElementsKind), + /// When parsing a list of f-string or t-string elements which are either literal elements, expressions, or interpolations. + InterpolatedStringElements(InterpolatedStringElementsKind), } impl RecoveryContextKind { @@ -1113,8 +1120,8 @@ impl RecoveryContextKind { .at(TokenKind::Colon) .then_some(ListTerminatorKind::Regular), }, - RecoveryContextKind::FStringElements(kind) => { - if p.at(kind.list_terminator()) { + RecoveryContextKind::InterpolatedStringElements(kind) => { + if p.at_ts(kind.list_terminators()) { Some(ListTerminatorKind::Regular) } else { // test_err unterminated_fstring_newline_recovery @@ -1170,10 +1177,10 @@ impl RecoveryContextKind { ) || p.at_name_or_soft_keyword() } RecoveryContextKind::WithItems(_) => p.at_expr(), - RecoveryContextKind::FStringElements(_) => matches!( + RecoveryContextKind::InterpolatedStringElements(_) => matches!( p.current_token_kind(), // Literal element - TokenKind::FStringMiddle + TokenKind::FStringMiddle | TokenKind::TStringMiddle // Expression element | TokenKind::Lbrace ), @@ -1264,13 +1271,13 @@ impl RecoveryContextKind { "Expected an expression or the end of the with item list".to_string(), ), }, - RecoveryContextKind::FStringElements(kind) => match kind { - FStringElementsKind::Regular => ParseErrorType::OtherError( - "Expected an f-string element or the end of the f-string".to_string(), + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular => ParseErrorType::OtherError( + "Expected an f-string or t-string element or the end of the f-string or t-string".to_string(), + ), + InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError( + "Expected an f-string or t-string element or a '}'".to_string(), ), - FStringElementsKind::FormatSpec => { - ParseErrorType::OtherError("Expected an f-string element or a '}'".to_string()) - } }, } } @@ -1309,8 +1316,8 @@ bitflags! { const WITH_ITEMS_PARENTHESIZED = 1 << 25; const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26; const WITH_ITEMS_UNPARENTHESIZED = 1 << 28; - const F_STRING_ELEMENTS = 1 << 29; - const F_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; + const FT_STRING_ELEMENTS = 1 << 29; + const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; } } @@ -1363,10 +1370,10 @@ impl RecoveryContext { } WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED, }, - RecoveryContextKind::FStringElements(kind) => match kind { - FStringElementsKind::Regular => RecoveryContext::F_STRING_ELEMENTS, - FStringElementsKind::FormatSpec => { - RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS, + InterpolatedStringElementsKind::FormatSpec => { + RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC } }, } @@ -1435,11 +1442,13 @@ impl RecoveryContext { RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => { RecoveryContextKind::WithItems(WithItemKind::Unparenthesized) } - RecoveryContext::F_STRING_ELEMENTS => { - RecoveryContextKind::FStringElements(FStringElementsKind::Regular) - } - RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC => { - RecoveryContextKind::FStringElements(FStringElementsKind::FormatSpec) + RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::Regular, + ), + RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::FormatSpec, + ) } _ => return None, }) diff --git a/crates/ruff_python_parser/src/parser/pattern.rs b/crates/ruff_python_parser/src/parser/pattern.rs index 6d556795f4598..dd552410db6fa 100644 --- a/crates/ruff_python_parser/src/parser/pattern.rs +++ b/crates/ruff_python_parser/src/parser/pattern.rs @@ -1,12 +1,14 @@ use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, Expr, ExprContext, Number, Operator, Pattern, Singleton}; +use ruff_python_ast::{ + self as ast, AtomicNodeIndex, Expr, ExprContext, Number, Operator, Pattern, Singleton, +}; use ruff_text_size::{Ranged, TextSize}; +use crate::ParseErrorType; use crate::parser::progress::ParserProgress; -use crate::parser::{recovery, Parser, RecoveryContextKind, SequenceMatchPatternParentheses}; +use crate::parser::{Parser, RecoveryContextKind, SequenceMatchPatternParentheses, recovery}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; -use crate::ParseErrorType; use super::expression::ExpressionContext; @@ -110,6 +112,7 @@ impl Parser<'_> { lhs = Pattern::MatchOr(ast::PatternMatchOr { range: self.node_range(start), patterns, + node_index: AtomicNodeIndex::dummy(), }); } @@ -125,6 +128,7 @@ impl Parser<'_> { range: self.node_range(start), name: Some(ident), pattern: Some(Box::new(lhs)), + node_index: AtomicNodeIndex::dummy(), }); } @@ -200,18 +204,25 @@ impl Parser<'_> { } else { let key = match parser.parse_match_pattern_lhs(AllowStarPattern::No) { Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => *value, - Pattern::MatchSingleton(ast::PatternMatchSingleton { value, range }) => { - match value { - Singleton::None => Expr::NoneLiteral(ast::ExprNoneLiteral { range }), - Singleton::True => { - Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: true, range }) - } - Singleton::False => Expr::BooleanLiteral(ast::ExprBooleanLiteral { - value: false, - range, - }), + Pattern::MatchSingleton(ast::PatternMatchSingleton { + value, + range, + node_index, + }) => match value { + Singleton::None => { + Expr::NoneLiteral(ast::ExprNoneLiteral { range, node_index }) } - } + Singleton::True => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: true, + range, + node_index, + }), + Singleton::False => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: false, + range, + node_index, + }), + }, pattern => { parser.add_error( ParseErrorType::OtherError("Invalid mapping pattern key".to_string()), @@ -244,6 +255,7 @@ impl Parser<'_> { keys, patterns, rest, + node_index: AtomicNodeIndex::dummy(), } } @@ -267,6 +279,7 @@ impl Parser<'_> { } else { Some(ident) }, + node_index: AtomicNodeIndex::dummy(), } } @@ -306,6 +319,7 @@ impl Parser<'_> { return Pattern::MatchSequence(ast::PatternMatchSequence { patterns: vec![], range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }); } @@ -360,6 +374,7 @@ impl Parser<'_> { ast::PatternMatchSequence { range: self.node_range(start), patterns, + node_index: AtomicNodeIndex::dummy(), } } @@ -374,6 +389,7 @@ impl Parser<'_> { Pattern::MatchSingleton(ast::PatternMatchSingleton { value: Singleton::None, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::True => { @@ -381,6 +397,7 @@ impl Parser<'_> { Pattern::MatchSingleton(ast::PatternMatchSingleton { value: Singleton::True, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::False => { @@ -388,14 +405,16 @@ impl Parser<'_> { Pattern::MatchSingleton(ast::PatternMatchSingleton { value: Singleton::False, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } - TokenKind::String | TokenKind::FStringStart => { + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { let str = self.parse_strings(); Pattern::MatchValue(ast::PatternMatchValue { value: Box::new(str), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Complex => { @@ -408,8 +427,10 @@ impl Parser<'_> { value: Box::new(Expr::NumberLiteral(ast::ExprNumberLiteral { value: Number::Complex { real, imag }, range, + node_index: AtomicNodeIndex::dummy(), })), range, + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Int => { @@ -422,8 +443,10 @@ impl Parser<'_> { value: Box::new(Expr::NumberLiteral(ast::ExprNumberLiteral { value: Number::Int(value), range, + node_index: AtomicNodeIndex::dummy(), })), range, + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Float => { @@ -436,8 +459,10 @@ impl Parser<'_> { value: Box::new(Expr::NumberLiteral(ast::ExprNumberLiteral { value: Number::Float(value), range, + node_index: AtomicNodeIndex::dummy(), })), range, + node_index: AtomicNodeIndex::dummy(), }) } kind => { @@ -464,6 +489,7 @@ impl Parser<'_> { return Pattern::MatchValue(ast::PatternMatchValue { value: Box::new(Expr::UnaryOp(unary_expr)), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }); } } @@ -483,6 +509,7 @@ impl Parser<'_> { Pattern::MatchValue(ast::PatternMatchValue { value: Box::new(attribute), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } else { // test_ok match_as_pattern_soft_keyword @@ -503,6 +530,7 @@ impl Parser<'_> { range: ident.range, pattern: None, name: if &ident == "_" { None } else { Some(ident) }, + node_index: AtomicNodeIndex::dummy(), }) } } else { @@ -516,10 +544,12 @@ impl Parser<'_> { range: self.missing_node_range(), id: Name::empty(), ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::dummy(), }); Pattern::MatchValue(ast::PatternMatchValue { range: invalid_node.range(), value: Box::new(invalid_node), + node_index: AtomicNodeIndex::dummy(), }) } } @@ -575,8 +605,10 @@ impl Parser<'_> { op: operator, right: rhs_value, range, + node_index: AtomicNodeIndex::dummy(), })), range, + node_index: AtomicNodeIndex::dummy(), } } @@ -616,12 +648,14 @@ impl Parser<'_> { range: ident.range(), id: ident.id, ctx: ExprContext::Load, + node_index: AtomicNodeIndex::dummy(), })) } else { Box::new(Expr::Name(ast::ExprName { range: ident.range(), id: Name::empty(), ctx: ExprContext::Invalid, + node_index: AtomicNodeIndex::dummy(), })) } } @@ -673,6 +707,7 @@ impl Parser<'_> { ast::Identifier { id: Name::empty(), range: parser.missing_node_range(), + node_index: AtomicNodeIndex::dummy(), } }; @@ -682,6 +717,7 @@ impl Parser<'_> { attr: key, pattern: value_pattern, range: parser.node_range(pattern_start), + node_index: AtomicNodeIndex::dummy(), }); } else { has_seen_pattern = true; @@ -707,8 +743,10 @@ impl Parser<'_> { patterns, keywords, range: self.node_range(arguments_start), + node_index: AtomicNodeIndex::dummy(), }, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } } diff --git a/crates/ruff_python_parser/src/parser/recovery.rs b/crates/ruff_python_parser/src/parser/recovery.rs index 1dd4489a8c085..f66f7e6dd130d 100644 --- a/crates/ruff_python_parser/src/parser/recovery.rs +++ b/crates/ruff_python_parser/src/parser/recovery.rs @@ -27,27 +27,38 @@ use ruff_text_size::{Ranged, TextLen, TextRange}; /// without dropping one of them as there's no way to represent `x as y` as a valid expression. pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { match pattern { - Pattern::MatchSingleton(ast::PatternMatchSingleton { range, value }) => match value { - ast::Singleton::True => { - Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: true, range }) - } + Pattern::MatchSingleton(ast::PatternMatchSingleton { + range, + node_index, + value, + }) => match value { + ast::Singleton::True => Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: true, + range, + node_index, + }), ast::Singleton::False => Expr::BooleanLiteral(ast::ExprBooleanLiteral { value: false, range, + node_index, }), - ast::Singleton::None => Expr::NoneLiteral(ast::ExprNoneLiteral { range }), + ast::Singleton::None => Expr::NoneLiteral(ast::ExprNoneLiteral { range, node_index }), }, Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => *value, // We don't know which kind of sequence this is: `case [1, 2]:` or `case (1, 2):`. - Pattern::MatchSequence(ast::PatternMatchSequence { range, patterns }) => { - Expr::List(ast::ExprList { - elts: patterns.into_iter().map(pattern_to_expr).collect(), - ctx: ExprContext::Store, - range, - }) - } + Pattern::MatchSequence(ast::PatternMatchSequence { + range, + node_index, + patterns, + }) => Expr::List(ast::ExprList { + elts: patterns.into_iter().map(pattern_to_expr).collect(), + ctx: ExprContext::Store, + range, + node_index, + }), Pattern::MatchMapping(ast::PatternMatchMapping { range, + node_index, keys, patterns, rest, @@ -63,22 +74,30 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { if let Some(rest) = rest { let value = Expr::Name(ast::ExprName { range: rest.range, + node_index: node_index.clone(), id: rest.id, ctx: ExprContext::Store, }); items.push(ast::DictItem { key: None, value }); } - Expr::Dict(ast::ExprDict { range, items }) + Expr::Dict(ast::ExprDict { + range, + node_index, + items, + }) } Pattern::MatchClass(ast::PatternMatchClass { range, + node_index, cls, arguments, }) => Expr::Call(ast::ExprCall { range, + node_index: node_index.clone(), func: cls, arguments: ast::Arguments { range: arguments.range, + node_index: node_index.clone(), args: arguments .patterns .into_iter() @@ -89,18 +108,25 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { .into_iter() .map(|keyword_pattern| ast::Keyword { range: keyword_pattern.range, + node_index: node_index.clone(), arg: Some(keyword_pattern.attr), value: pattern_to_expr(keyword_pattern.pattern), }) .collect(), }, }), - Pattern::MatchStar(ast::PatternMatchStar { range, name }) => { + Pattern::MatchStar(ast::PatternMatchStar { + range, + node_index, + name, + }) => { if let Some(name) = name { Expr::Starred(ast::ExprStarred { range, + node_index: node_index.clone(), value: Box::new(Expr::Name(ast::ExprName { range: name.range, + node_index, id: name.id, ctx: ExprContext::Store, })), @@ -109,10 +135,12 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { } else { Expr::Starred(ast::ExprStarred { range, + node_index: node_index.clone(), value: Box::new(Expr::Name(ast::ExprName { range: TextRange::new(range.end() - "_".text_len(), range.end()), id: Name::new_static("_"), ctx: ExprContext::Store, + node_index, })), ctx: ExprContext::Store, }) @@ -120,32 +148,41 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { } Pattern::MatchAs(ast::PatternMatchAs { range, + node_index, pattern, name, }) => match (pattern, name) { (Some(_), Some(_)) => Expr::Name(ast::ExprName { range, + node_index, id: Name::empty(), ctx: ExprContext::Invalid, }), (Some(pattern), None) => pattern_to_expr(*pattern), (None, Some(name)) => Expr::Name(ast::ExprName { range: name.range, + node_index, id: name.id, ctx: ExprContext::Store, }), (None, None) => Expr::Name(ast::ExprName { range, + node_index, id: Name::new_static("_"), ctx: ExprContext::Store, }), }, - Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { + Pattern::MatchOr(ast::PatternMatchOr { + patterns, + node_index, + .. + }) => { let to_bin_expr = |left: Pattern, right: Pattern| ast::ExprBinOp { range: TextRange::new(left.start(), right.end()), left: Box::new(pattern_to_expr(left)), op: ast::Operator::BitOr, right: Box::new(pattern_to_expr(right)), + node_index: node_index.clone(), }; let mut iter = patterns.into_iter(); @@ -158,6 +195,7 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { left: Box::new(Expr::BinOp(expr_bin_op)), op: ast::Operator::BitOr, right: Box::new(pattern_to_expr(pattern)), + node_index: node_index.clone(), } })) } diff --git a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap index 29cb09b85afc3..ab002c583afa9 100644 --- a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap +++ b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__expr_mode_valid_syntax.snap @@ -1,10 +1,10 @@ --- source: crates/ruff_python_parser/src/parser/tests.rs expression: parsed.expr() -snapshot_kind: text --- Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("first"), ctx: Load, diff --git a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap index e5566ded21993..8d6fbc000ce9a 100644 --- a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap +++ b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__ipython_escape_commands.snap @@ -1,20 +1,23 @@ --- source: crates/ruff_python_parser/src/parser/tests.rs expression: parsed.syntax() -snapshot_kind: text --- Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..929, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..42, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 27..40, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("a"), ctx: Load, @@ -23,6 +26,7 @@ Module( op: Mod, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("b"), ctx: Load, @@ -34,6 +38,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 66..73, kind: Help2, value: "a.foo", @@ -41,6 +46,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 74..80, kind: Help, value: "a.foo", @@ -48,6 +54,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 81..88, kind: Help, value: "a.foo", @@ -55,6 +62,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 89..100, kind: Help2, value: "a.foo()", @@ -62,6 +70,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 115..128, kind: Magic, value: "timeit a = b", @@ -69,6 +78,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 129..147, kind: Magic, value: "timeit foo(b) % 3", @@ -76,6 +86,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 148..176, kind: Magic, value: "alias showPath pwd && ls -a", @@ -83,6 +94,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 177..205, kind: Magic, value: "timeit a = foo(b); b = 2", @@ -90,6 +102,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 206..226, kind: Magic, value: "matplotlib --inline", @@ -97,6 +110,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 227..253, kind: Magic, value: "matplotlib --inline", @@ -104,6 +118,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 277..309, kind: Shell, value: "pwd && ls -a | sed 's/^/\\ /'", @@ -111,6 +126,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 310..347, kind: Shell, value: "pwd && ls -a | sed 's/^/\\\\ /'", @@ -118,6 +134,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 348..393, kind: ShCap, value: "cd /Users/foo/Library/Application\\ Support/", @@ -125,16 +142,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 566..626, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 570..573, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 573..575, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -145,13 +167,16 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 581..626, value: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 598..620, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 598..599, id: Name("a"), ctx: Load, @@ -163,6 +188,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 619..620, id: Name("b"), ctx: Load, @@ -179,6 +205,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 656..664, kind: Paren, value: "foo 1 2", @@ -186,6 +213,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 665..673, kind: Quote2, value: "foo 1 2", @@ -193,6 +221,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 674..682, kind: Quote, value: "foo 1 2", @@ -200,10 +229,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 711..737, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 715..716, id: Name("a"), ctx: Store, @@ -211,9 +242,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 720..728, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 720..725, id: Name("range"), ctx: Load, @@ -221,9 +254,11 @@ Module( ), arguments: Arguments { range: 725..728, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 726..727, value: Int( 5, @@ -238,6 +273,7 @@ Module( body: [ IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 734..737, kind: Shell, value: "ls", @@ -249,10 +285,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 739..748, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 739..741, id: Name("p1"), ctx: Store, @@ -261,6 +299,7 @@ Module( ], value: IpyEscapeCommand( ExprIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 744..748, kind: Shell, value: "pwd", @@ -270,9 +309,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 749..763, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 749..751, id: Name("p2"), ctx: Store, @@ -280,6 +321,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 753..756, id: Name("str"), ctx: Load, @@ -288,6 +330,7 @@ Module( value: Some( IpyEscapeCommand( ExprIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 759..763, kind: Shell, value: "pwd", @@ -299,10 +342,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 764..784, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 764..767, id: Name("foo"), ctx: Store, @@ -311,6 +356,7 @@ Module( ], value: IpyEscapeCommand( ExprIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 770..784, kind: Magic, value: "foo bar", @@ -320,6 +366,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 786..791, kind: Magic, value: " foo", @@ -327,10 +374,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 792..813, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 792..795, id: Name("foo"), ctx: Store, @@ -339,6 +388,7 @@ Module( ], value: IpyEscapeCommand( ExprIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 798..813, kind: Magic, value: "foo # comment", @@ -348,6 +398,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 838..842, kind: Help, value: "foo", @@ -355,6 +406,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 843..852, kind: Help2, value: "foo.bar", @@ -362,6 +414,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 853..865, kind: Help, value: "foo.bar.baz", @@ -369,6 +422,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 866..874, kind: Help2, value: "foo[0]", @@ -376,6 +430,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 875..885, kind: Help, value: "foo[0][1]", @@ -383,6 +438,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 886..905, kind: Help2, value: "foo.bar[0].baz[1]", @@ -390,6 +446,7 @@ Module( ), IpyEscapeCommand( StmtIpyEscapeCommand { + node_index: AtomicNodeIndex(..), range: 906..929, kind: Help2, value: "foo.bar[0].baz[2].egg", diff --git a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap index fe8e440c17582..6ee807fc8c985 100644 --- a/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap +++ b/crates/ruff_python_parser/src/parser/snapshots/ruff_python_parser__parser__tests__unicode_aliases.snap @@ -1,15 +1,16 @@ --- source: crates/ruff_python_parser/src/parser/tests.rs expression: suite -snapshot_kind: text --- [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..37, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -18,11 +19,13 @@ snapshot_kind: text ], value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 4..37, value: StringLiteralValue { inner: Single( StringLiteral { range: 4..37, + node_index: AtomicNodeIndex(..), value: "\u{8}another cool trick", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 86d6fde4c1540..d738fc78d6592 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1,27 +1,25 @@ use compact_str::CompactString; use std::fmt::{Display, Write}; -use rustc_hash::{FxBuildHasher, FxHashSet}; - use ruff_python_ast::name::Name; use ruff_python_ast::{ - self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt, - WithItem, + self as ast, AtomicNodeIndex, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, + PythonVersion, Stmt, WithItem, }; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::error::StarTupleKind; -use crate::parser::expression::{ParsedExpr, EXPR_SET}; +use crate::parser::expression::{EXPR_SET, ParsedExpr}; use crate::parser::progress::ParserProgress; use crate::parser::{ - helpers, FunctionKind, Parser, RecoveryContext, RecoveryContextKind, WithItemKind, + FunctionKind, Parser, RecoveryContext, RecoveryContextKind, WithItemKind, helpers, }; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; use crate::{Mode, ParseErrorType, UnsupportedSyntaxErrorKind}; -use super::expression::ExpressionContext; use super::Parenthesized; +use super::expression::ExpressionContext; /// Tokens that represent compound statements. const COMPOUND_STMT_SET: TokenSet = TokenSet::new([ @@ -314,6 +312,7 @@ impl<'src> Parser<'src> { Stmt::Expr(ast::StmtExpr { range: self.node_range(start), value: Box::new(parsed_expr.expr), + node_index: AtomicNodeIndex::dummy(), }) } } @@ -369,6 +368,7 @@ impl<'src> Parser<'src> { ast::StmtDelete { targets, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -417,6 +417,7 @@ impl<'src> Parser<'src> { ast::StmtReturn { range: self.node_range(start), value, + node_index: AtomicNodeIndex::dummy(), } } @@ -522,6 +523,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), exc, cause, + node_index: AtomicNodeIndex::dummy(), } } @@ -562,6 +564,7 @@ impl<'src> Parser<'src> { ast::StmtImport { range: self.node_range(start), names, + node_index: AtomicNodeIndex::dummy(), } } @@ -673,6 +676,7 @@ impl<'src> Parser<'src> { names, level: leading_dots, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -689,9 +693,11 @@ impl<'src> Parser<'src> { name: ast::Identifier { id: Name::new_static("*"), range, + node_index: AtomicNodeIndex::dummy(), }, asname: None, range, + node_index: AtomicNodeIndex::dummy(), }; } @@ -724,6 +730,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), name, asname, + node_index: AtomicNodeIndex::dummy(), } } @@ -752,6 +759,7 @@ impl<'src> Parser<'src> { ast::Identifier { id: Name::from(dotted_name), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -767,6 +775,7 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Pass); ast::StmtPass { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -782,6 +791,7 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Continue); ast::StmtContinue { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -797,6 +807,7 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Break); ast::StmtBreak { range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -846,6 +857,7 @@ impl<'src> Parser<'src> { test: Box::new(test.expr), msg, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -884,6 +896,7 @@ impl<'src> Parser<'src> { ast::StmtGlobal { range: self.node_range(start), names, + node_index: AtomicNodeIndex::dummy(), } } @@ -899,12 +912,14 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Nonlocal); // test_err nonlocal_stmt_trailing_comma - // nonlocal , - // nonlocal x, - // nonlocal x, y, + // def _(): + // nonlocal , + // nonlocal x, + // nonlocal x, y, // test_err nonlocal_stmt_expression - // nonlocal x + 1 + // def _(): + // nonlocal x + 1 let names = self.parse_comma_separated_list_into_vec( RecoveryContextKind::Identifiers, Parser::parse_identifier, @@ -912,7 +927,8 @@ impl<'src> Parser<'src> { if names.is_empty() { // test_err nonlocal_stmt_empty - // nonlocal + // def _(): + // nonlocal self.add_error( ParseErrorType::EmptyNonlocalNames, self.current_token_range(), @@ -920,11 +936,13 @@ impl<'src> Parser<'src> { } // test_ok nonlocal_stmt - // nonlocal x - // nonlocal x, y, z + // def _(): + // nonlocal x + // nonlocal x, y, z ast::StmtNonlocal { range: self.node_range(start), names, + node_index: AtomicNodeIndex::dummy(), } } @@ -977,6 +995,7 @@ impl<'src> Parser<'src> { type_params: type_params.map(Box::new), value: Box::new(value.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -999,7 +1018,12 @@ impl<'src> Parser<'src> { self.add_error(ParseErrorType::UnexpectedIpythonEscapeCommand, range); } - ast::StmtIpyEscapeCommand { range, kind, value } + ast::StmtIpyEscapeCommand { + range, + kind, + value, + node_index: AtomicNodeIndex::dummy(), + } } /// Parses an IPython help end escape command at the statement level. @@ -1095,6 +1119,7 @@ impl<'src> Parser<'src> { value: value.into_boxed_str(), kind, range: self.node_range(parsed_expr.start()), + node_index: AtomicNodeIndex::dummy(), } } @@ -1125,10 +1150,10 @@ impl<'src> Parser<'src> { // a + b // test_err assign_stmt_invalid_value_expr - // x = *a and b - // x = *yield x - // x = *yield from x - // x = *lambda x: x + // x = (*a and b,) + // x = (42, *yield x) + // x = (42, *yield from x) + // x = (*lambda x: x,) // x = x := 1 let mut value = @@ -1162,6 +1187,7 @@ impl<'src> Parser<'src> { targets, value: Box::new(value.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1241,6 +1267,7 @@ impl<'src> Parser<'src> { value, simple, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1295,6 +1322,7 @@ impl<'src> Parser<'src> { op, value: Box::new(value.expr), range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1350,6 +1378,7 @@ impl<'src> Parser<'src> { body, elif_else_clauses, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1393,6 +1422,7 @@ impl<'src> Parser<'src> { test, body, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1542,6 +1572,7 @@ impl<'src> Parser<'src> { finalbody, is_star, range: self.node_range(try_start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1579,25 +1610,50 @@ impl<'src> Parser<'src> { .. }) ) { - // test_err except_stmt_unparenthesized_tuple - // try: - // pass - // except x, y: - // pass - // except x, y as exc: - // pass - // try: - // pass - // except* x, y: - // pass - // except* x, y as eg: - // pass - self.add_error( - ParseErrorType::OtherError( - "Multiple exception types must be parenthesized".to_string(), - ), - &parsed_expr, - ); + if self.at(TokenKind::As) { + // test_err except_stmt_unparenthesized_tuple_as + // try: + // pass + // except x, y as exc: + // pass + // try: + // pass + // except* x, y as eg: + // pass + self.add_error( + ParseErrorType::OtherError( + "Multiple exception types must be parenthesized when using `as`" + .to_string(), + ), + &parsed_expr, + ); + } else { + // test_err except_stmt_unparenthesized_tuple_no_as_py313 + // # parse_options: {"target-version": "3.13"} + // try: + // pass + // except x, y: + // pass + // try: + // pass + // except* x, y: + // pass + + // test_ok except_stmt_unparenthesized_tuple_no_as_py314 + // # parse_options: {"target-version": "3.14"} + // try: + // pass + // except x, y: + // pass + // try: + // pass + // except* x, y: + // pass + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes, + parsed_expr.range(), + ); + } } Some(Box::new(parsed_expr.expr)) } else { @@ -1666,6 +1722,7 @@ impl<'src> Parser<'src> { name, body: except_body, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }), block_kind, ) @@ -1777,6 +1834,7 @@ impl<'src> Parser<'src> { body, orelse, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1824,6 +1882,7 @@ impl<'src> Parser<'src> { body, orelse, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -1953,6 +2012,7 @@ impl<'src> Parser<'src> { is_async: false, returns, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2022,6 +2082,7 @@ impl<'src> Parser<'src> { type_params: type_params.map(Box::new), arguments, body, + node_index: AtomicNodeIndex::dummy(), } } @@ -2048,6 +2109,7 @@ impl<'src> Parser<'src> { body, is_async: false, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2316,6 +2378,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), context_expr: context_expr.expr, optional_vars, + node_index: AtomicNodeIndex::dummy(), }, } } @@ -2384,6 +2447,7 @@ impl<'src> Parser<'src> { subject: Box::new(subject), cases, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } TokenKind::Newline if matches!(self.peek2(), (TokenKind::Indent, TokenKind::Case)) => { @@ -2406,6 +2470,7 @@ impl<'src> Parser<'src> { subject: Box::new(subject), cases, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), }) } _ => { @@ -2453,6 +2518,7 @@ impl<'src> Parser<'src> { subject: Box::new(subject), cases, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2631,6 +2697,7 @@ impl<'src> Parser<'src> { guard, body, range: self.node_range(start), + node_index: AtomicNodeIndex::dummy(), } } @@ -2799,6 +2866,7 @@ impl<'src> Parser<'src> { decorators.push(ast::Decorator { expression: parsed_expr.expr, range: self.node_range(decorator_start), + node_index: AtomicNodeIndex::dummy(), }); // test_err decorator_missing_newline @@ -2985,7 +3053,6 @@ impl<'src> Parser<'src> { // test_ok param_with_annotation // def foo(arg: int): ... // def foo(arg: lambda x: x): ... - // def foo(arg: (x := int)): ... // test_err param_with_invalid_annotation // def foo(arg: *int): ... @@ -3013,6 +3080,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), name, annotation, + node_index: AtomicNodeIndex::dummy(), } } @@ -3062,6 +3130,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), parameter, default, + node_index: AtomicNodeIndex::dummy(), } } @@ -3339,10 +3408,6 @@ impl<'src> Parser<'src> { parameters.range = self.node_range(start); - // test_err params_duplicate_names - // def foo(a, a=10, *a, a, a: str, **a): ... - self.validate_parameters(¶meters); - parameters } @@ -3383,6 +3448,7 @@ impl<'src> Parser<'src> { ast::TypeParams { range: self.node_range(start), type_params, + node_index: AtomicNodeIndex::dummy(), } } @@ -3445,6 +3511,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), name, default, + node_index: AtomicNodeIndex::dummy(), }) // test_ok type_param_param_spec @@ -3484,6 +3551,7 @@ impl<'src> Parser<'src> { range: self.node_range(start), name, default, + node_index: AtomicNodeIndex::dummy(), }) // test_ok type_param_type_var // type X[T] = int @@ -3567,6 +3635,7 @@ impl<'src> Parser<'src> { name, bound, default, + node_index: AtomicNodeIndex::dummy(), }) } } @@ -3630,25 +3699,6 @@ impl<'src> Parser<'src> { } } - /// Validate that the given parameters doesn't have any duplicate names. - /// - /// Report errors for all the duplicate names found. - fn validate_parameters(&mut self, parameters: &ast::Parameters) { - let mut all_arg_names = - FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher); - - for parameter in parameters { - let range = parameter.name().range(); - let param_name = parameter.name().as_str(); - if !all_arg_names.insert(param_name) { - self.add_error( - ParseErrorType::DuplicateParameter(param_name.to_string()), - range, - ); - } - } - } - /// Classify the `match` soft keyword token. /// /// # Panics @@ -3699,6 +3749,7 @@ impl<'src> Parser<'src> { | TokenKind::Complex | TokenKind::String | TokenKind::FStringStart + | TokenKind::TStringStart | TokenKind::Lbrace | TokenKind::Tilde | TokenKind::Ellipsis diff --git a/crates/ruff_python_parser/src/parser/tests.rs b/crates/ruff_python_parser/src/parser/tests.rs index 645ec318d26c8..778637597cfe8 100644 --- a/crates/ruff_python_parser/src/parser/tests.rs +++ b/crates/ruff_python_parser/src/parser/tests.rs @@ -1,4 +1,4 @@ -use crate::{parse, parse_expression, parse_module, Mode, ParseOptions}; +use crate::{Mode, ParseOptions, parse, parse_expression, parse_module}; #[test] fn test_modes() { diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index e61c440fcf34d..0bade7b9a6ea7 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -6,14 +6,13 @@ use std::fmt::Display; use ruff_python_ast::{ - self as ast, - comparable::ComparableExpr, - visitor::{walk_expr, Visitor}, - Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, + self as ast, Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom, + comparable::ComparableExpr, + visitor::{Visitor, walk_expr}, }; use ruff_text_size::{Ranged, TextRange, TextSize}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxBuildHasher, FxHashSet}; #[derive(Debug, Default)] pub struct SemanticSyntaxChecker { @@ -32,6 +31,10 @@ pub struct SemanticSyntaxChecker { /// Python considers it a syntax error to import from `__future__` after any other /// non-`__future__`-importing statements. seen_futures_boundary: bool, + + /// The checker has traversed past the module docstring boundary (i.e. seen any statement in the + /// module). + seen_module_docstring_boundary: bool, } impl SemanticSyntaxChecker { @@ -70,14 +73,23 @@ impl SemanticSyntaxChecker { visitor.visit_pattern(&case.pattern); } } - Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. }) - | Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) + Stmt::FunctionDef(ast::StmtFunctionDef { + type_params, + parameters, + .. + }) => { + if let Some(type_params) = type_params { + Self::duplicate_type_parameter_name(type_params, ctx); + } + Self::duplicate_parameter_name(parameters, ctx); + } + Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) | Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { if let Some(type_params) = type_params { Self::duplicate_type_parameter_name(type_params, ctx); } } - Stmt::Assign(ast::StmtAssign { targets, .. }) => { + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() { // test_ok single_starred_assignment_target // (*a,) = (1,) @@ -92,8 +104,25 @@ impl SemanticSyntaxChecker { *range, ); } + + // test_ok assign_stmt_starred_expr_value + // _ = 4 + // _ = [4] + // _ = (*[1],) + // _ = *[1], + + // test_err assign_stmt_starred_expr_value + // _ = *[42] + // _ = *{42} + // _ = *list() + // _ = *(p + q) + Self::invalid_star_expression(value, ctx); } - Stmt::Return(ast::StmtReturn { value, range }) => { + Stmt::Return(ast::StmtReturn { + value, + range, + node_index: _, + }) => { if let Some(value) = value { // test_err single_star_return // def f(): return *x @@ -129,6 +158,22 @@ impl SemanticSyntaxChecker { AwaitOutsideAsyncFunctionKind::AsyncWith, ); } + Stmt::Nonlocal(ast::StmtNonlocal { range, .. }) => { + // test_ok nonlocal_declaration_at_module_level + // def _(): + // nonlocal x + + // test_err nonlocal_declaration_at_module_level + // nonlocal x + // nonlocal x, y + if ctx.in_module_scope() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel, + *range, + ); + } + } _ => {} } @@ -449,6 +494,32 @@ impl SemanticSyntaxChecker { } } + fn duplicate_parameter_name( + parameters: &ast::Parameters, + ctx: &Ctx, + ) { + if parameters.len() < 2 { + return; + } + + let mut all_arg_names = + FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher); + + for parameter in parameters { + let range = parameter.name().range(); + let param_name = parameter.name().as_str(); + if !all_arg_names.insert(param_name) { + // test_err params_duplicate_names + // def foo(a, a=10, *a, a, a: str, **a): ... + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateParameter(param_name.to_string()), + range, + ); + } + } + } + fn irrefutable_match_case(stmt: &ast::StmtMatch, ctx: &Ctx) { // test_ok irrefutable_case_pattern_at_end // match x: @@ -506,7 +577,7 @@ impl SemanticSyntaxChecker { // update internal state match stmt { Stmt::Expr(StmtExpr { value, .. }) - if !ctx.seen_docstring_boundary() && value.is_string_literal_expr() => {} + if !self.seen_module_docstring_boundary && value.is_string_literal_expr() => {} Stmt::ImportFrom(StmtImportFrom { module, .. }) => { // Allow __future__ imports until we see a non-__future__ import. if !matches!(module.as_deref(), Some("__future__")) { @@ -520,6 +591,8 @@ impl SemanticSyntaxChecker { self.seen_futures_boundary = true; } } + + self.seen_module_docstring_boundary = true; } /// Check `expr` for semantic syntax errors and update the checker's internal state. @@ -532,7 +605,7 @@ impl SemanticSyntaxChecker { elt, generators, .. }) => { Self::check_generator_expr(elt, generators, ctx); - Self::async_comprehension_outside_async_function(ctx, generators); + Self::async_comprehension_in_sync_comprehension(ctx, generators); for generator in generators.iter().filter(|g| g.is_async) { Self::await_outside_async_function( ctx, @@ -549,7 +622,7 @@ impl SemanticSyntaxChecker { }) => { Self::check_generator_expr(key, generators, ctx); Self::check_generator_expr(value, generators, ctx); - Self::async_comprehension_outside_async_function(ctx, generators); + Self::async_comprehension_in_sync_comprehension(ctx, generators); for generator in generators.iter().filter(|g| g.is_async) { Self::await_outside_async_function( ctx, @@ -569,6 +642,7 @@ impl SemanticSyntaxChecker { range, id, ctx: expr_ctx, + node_index: _, }) => { // test_err write_to_debug_expr // del __debug__ @@ -640,6 +714,12 @@ impl SemanticSyntaxChecker { Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await); } + Expr::Lambda(ast::ExprLambda { + parameters: Some(parameters), + .. + }) => { + Self::duplicate_parameter_name(parameters, ctx); + } _ => {} } } @@ -694,16 +774,21 @@ impl SemanticSyntaxChecker { // We are intentionally not inspecting the async status of the scope for now to mimic F704. // await-outside-async is PLE1142 instead, so we'll end up emitting both syntax errors for // cases that trigger F704 + + if ctx.in_function_scope() { + return; + } + if kind.is_await() { - if ctx.in_await_allowed_context() { - return; - } // `await` is allowed at the top level of a Jupyter notebook. // See: https://ipython.readthedocs.io/en/stable/interactive/autoawait.html. if ctx.in_module_scope() && ctx.in_notebook() { return; } - } else if ctx.in_function_scope() { + if ctx.in_await_allowed_context() { + return; + } + } else if ctx.in_yield_allowed_context() { return; } @@ -754,7 +839,7 @@ impl SemanticSyntaxChecker { } } - fn async_comprehension_outside_async_function( + fn async_comprehension_in_sync_comprehension( ctx: &Ctx, generators: &[ast::Comprehension], ) { @@ -766,10 +851,10 @@ impl SemanticSyntaxChecker { if ctx.in_notebook() && ctx.in_module_scope() { return; } - if ctx.in_async_context() && !ctx.in_sync_comprehension() { + if !ctx.in_sync_comprehension() { return; } - for generator in generators.iter().filter(|gen| gen.is_async) { + for generator in generators.iter().filter(|generator| generator.is_async) { // test_ok nested_async_comprehension_py311 // # parse_options: {"target-version": "3.11"} // async def f(): return [[x async for x in foo(n)] for n in range(3)] # list @@ -798,14 +883,14 @@ impl SemanticSyntaxChecker { // async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] Self::add_error( ctx, - SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version), + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version), generator.range, ); } } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct SemanticSyntaxError { pub kind: SemanticSyntaxErrorKind, pub range: TextRange, @@ -845,7 +930,10 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::WriteToDebug(kind) => match kind { WriteToDebugKind::Store => f.write_str("cannot assign to `__debug__`"), WriteToDebugKind::Delete(python_version) => { - write!(f, "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)") + write!( + f, + "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)" + ) } }, SemanticSyntaxErrorKind::InvalidExpression(kind, position) => { @@ -865,13 +953,13 @@ impl Display for SemanticSyntaxError { write!(f, "name `{name}` is used prior to global declaration") } SemanticSyntaxErrorKind::InvalidStarExpression => { - f.write_str("can't use starred expression here") + f.write_str("Starred expression cannot be used here") } - SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version) => { + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version) => { write!( f, - "cannot use an asynchronous comprehension outside of an asynchronous \ - function on Python {python_version} (syntax was added in 3.11)", + "cannot use an asynchronous comprehension inside of a synchronous comprehension \ + on Python {python_version} (syntax was added in 3.11)", ) } SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => { @@ -881,13 +969,25 @@ impl Display for SemanticSyntaxError { f.write_str("`return` statement outside of a function") } SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => { - write!(f, "`{kind}` outside of an asynchronous function") + write!(f, "{kind} outside of an asynchronous function") + } + SemanticSyntaxErrorKind::DuplicateParameter(name) => { + write!(f, r#"Duplicate parameter "{name}""#) + } + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { + write!(f, "nonlocal declaration not allowed at module level") } } } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +impl Ranged for SemanticSyntaxError { + fn range(&self) -> TextRange { + self.range + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum SemanticSyntaxErrorKind { /// Represents the use of a `__future__` import after the beginning of a file. /// @@ -1137,7 +1237,7 @@ pub enum SemanticSyntaxErrorKind { /// This was discussed in [BPO 33346] and fixed in Python 3.11. /// /// [BPO 33346]: https://github.com/python/cpython/issues/77527 - AsyncComprehensionOutsideAsyncFunction(PythonVersion), + AsyncComprehensionInSyncComprehension(PythonVersion), /// Represents the use of `yield`, `yield from`, or `await` outside of a function scope. /// @@ -1194,9 +1294,22 @@ pub enum SemanticSyntaxErrorKind { /// async with x: ... # error /// ``` AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind), + + /// Represents a duplicate parameter name in a function or lambda expression. + /// + /// ## Examples + /// + /// ```python + /// def f(x, x): ... + /// lambda x, x: ... + /// ``` + DuplicateParameter(String), + + /// Represents a nonlocal declaration at module level + NonlocalDeclarationAtModuleLevel, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum AwaitOutsideAsyncFunctionKind { Await, AsyncFor, @@ -1207,15 +1320,15 @@ pub enum AwaitOutsideAsyncFunctionKind { impl Display for AwaitOutsideAsyncFunctionKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { - AwaitOutsideAsyncFunctionKind::Await => "await", - AwaitOutsideAsyncFunctionKind::AsyncFor => "async for", - AwaitOutsideAsyncFunctionKind::AsyncWith => "async with", + AwaitOutsideAsyncFunctionKind::Await => "`await`", + AwaitOutsideAsyncFunctionKind::AsyncFor => "`async for`", + AwaitOutsideAsyncFunctionKind::AsyncWith => "`async with`", AwaitOutsideAsyncFunctionKind::AsyncComprehension => "asynchronous comprehension", }) } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum YieldOutsideFunctionKind { Yield, YieldFrom, @@ -1238,7 +1351,7 @@ impl Display for YieldOutsideFunctionKind { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum InvalidExpressionPosition { TypeVarBound, TypeVarDefault, @@ -1263,7 +1376,7 @@ impl Display for InvalidExpressionPosition { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum InvalidExpressionKind { Yield, NamedExpr, @@ -1280,7 +1393,7 @@ impl Display for InvalidExpressionKind { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum WriteToDebugKind { Store, Delete(PythonVersion), @@ -1584,9 +1697,6 @@ where /// x # here, classes break function scopes /// ``` pub trait SemanticSyntaxContext { - /// Returns `true` if a module's docstring boundary has been passed. - fn seen_docstring_boundary(&self) -> bool; - /// Returns `true` if `__future__`-style type annotations are enabled. fn future_annotations_or_stub(&self) -> bool; @@ -1625,6 +1735,35 @@ pub trait SemanticSyntaxContext { /// See the trait-level documentation for more details. fn in_await_allowed_context(&self) -> bool; + /// Returns `true` if the visitor is currently in a context where `yield` and `yield from` + /// expressions are allowed. + /// + /// Yield expressions are allowed only in: + /// 1. Function definitions + /// 2. Lambda expressions + /// + /// Unlike `await`, yield is not allowed in: + /// - Comprehensions (list, set, dict) + /// - Generator expressions + /// - Class definitions + /// + /// This method should traverse parent scopes to check if the closest relevant scope + /// is a function or lambda, and that no disallowed context (class, comprehension, generator) + /// intervenes. For example: + /// + /// ```python + /// def f(): + /// yield 1 # okay, in a function + /// lambda: (yield 1) # okay, in a lambda + /// + /// [(yield 1) for x in range(3)] # error, in a comprehension + /// ((yield 1) for x in range(3)) # error, in a generator expression + /// class C: + /// yield 1 # error, in a class within a function + /// ``` + /// + fn in_yield_allowed_context(&self) -> bool; + /// Returns `true` if the visitor is currently inside of a synchronous comprehension. /// /// This method is necessary because `in_async_context` only checks for the nearest, enclosing diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap new file mode 100644 index 0000000000000..d23fee4ae8716 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap @@ -0,0 +1,98 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + String( + "", + ), + 4..6, + TokenFlags( + DOUBLE_QUOTES, + ), + ), + ( + TStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 9..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 11..13, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 13..14, + TokenFlags( + T_STRING, + ), + ), + ( + String( + "", + ), + 15..17, + ), + ( + TStringStart, + 18..22, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 22..25, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 26..30, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 30..33, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 33..33, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap index 3a56937bcc65a..c515b59ec0f0d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "normal ", ), 2..9, @@ -37,7 +36,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {another} ", ), 14..27, @@ -60,7 +59,7 @@ snapshot_kind: text 31..32, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {", ), 32..35, @@ -83,7 +82,7 @@ snapshot_kind: text 41..42, ), ( - FStringMiddle( + InterpolatedStringMiddle( "}", ), 42..44, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap index dae04a5f0ca85..93e0b88bd9a3c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\n# not a comment ", ), 4..21, @@ -49,7 +48,7 @@ snapshot_kind: text 41..42, ), ( - FStringMiddle( + InterpolatedStringMiddle( " # not a comment\n", ), 42..59, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap index 80a868327786f..cff7b14e12fed 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -38,7 +37,7 @@ snapshot_kind: text 6..7, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 7..8, @@ -75,7 +74,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 14..15, @@ -98,7 +97,7 @@ snapshot_kind: text 17..18, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".3f!r", ), 18..23, @@ -111,7 +110,7 @@ snapshot_kind: text 23..24, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {x!r}", ), 24..32, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap index 7aae96b72fc6a..899139162ddb5 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 2..3, @@ -37,7 +36,7 @@ snapshot_kind: text 5..6, ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\"\\", ), 6..9, @@ -64,7 +63,7 @@ snapshot_kind: text 12..13, ), ( - FStringMiddle( + InterpolatedStringMiddle( " \\\"\\\"\\\n end", ), 13..24, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap index 3cfba863a21c0..a792cfee1117e 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 2..3, @@ -51,7 +50,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\\", ), 12..14, @@ -88,7 +87,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\{foo}", ), 23..31, @@ -111,7 +110,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\\{foo}", ), 35..44, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap index 0e14fbb35de4f..5fe4b168373dc 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 3..4, @@ -37,7 +36,7 @@ snapshot_kind: text 6..7, ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\"\\", ), 7..10, @@ -64,7 +63,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " \\\"\\\"\\\n end", ), 14..25, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap index c7fd18b79a8c3..5987a41f676e7 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "first ", ), 2..8, @@ -63,7 +62,7 @@ snapshot_kind: text 40..41, ), ( - FStringMiddle( + InterpolatedStringMiddle( " second", ), 41..48, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap index 95c43f76d1184..15a765a45ae2c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\nhello\n world\n", ), 4..21, @@ -37,7 +36,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\n world\nhello\n", ), 29..46, @@ -60,7 +59,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "some ", ), 52..57, @@ -80,7 +79,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "multiline\nallowed ", ), 62..80, @@ -114,7 +113,7 @@ snapshot_kind: text 86..87, ), ( - FStringMiddle( + InterpolatedStringMiddle( " string", ), 87..94, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap index 2ae41093603b1..5571d867a125e 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\N{BULLET} normal \\Nope \\N", ), 2..28, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap index b37611f0dab32..974d2cf9c355c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\N", ), 3..5, @@ -37,7 +36,7 @@ snapshot_kind: text 12..13, ), ( - FStringMiddle( + InterpolatedStringMiddle( " normal", ), 13..20, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap index de3e6d60f2777..8e1dc7e8d2f1d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 2..6, @@ -34,7 +33,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "bar ", ), 9..13, @@ -100,7 +99,7 @@ snapshot_kind: text 28..29, ), ( - FStringMiddle( + InterpolatedStringMiddle( " baz", ), 29..33, @@ -123,7 +122,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 37..41, @@ -143,7 +142,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "bar", ), 44..47, @@ -163,7 +162,7 @@ snapshot_kind: text 48..49, ), ( - FStringMiddle( + InterpolatedStringMiddle( " some ", ), 49..55, @@ -183,7 +182,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "another", ), 58..65, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap new file mode 100644 index 0000000000000..868a705a31b73 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap @@ -0,0 +1,168 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + ( + NonLogicalNewline, + 0..1, + ), + ( + FStringStart, + 1..3, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 3..5, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + NonLogicalNewline, + 6..7, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Colon, + 12..13, + ), + ( + Unknown, + 13..14, + ), + ( + NonLogicalNewline, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + Name( + Name("__"), + ), + 16..18, + ), + ( + Unknown, + 18..19, + ), + ( + Newline, + 19..20, + ), + ( + FStringStart, + 20..22, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 22..24, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("x"), + ), + 30..31, + ), + ( + Colon, + 31..32, + ), + ( + Unknown, + 32..33, + ), + ( + NonLogicalNewline, + 33..34, + ), + ( + Name( + Name("b"), + ), + 42..43, + ), + ( + NonLogicalNewline, + 43..44, + ), + ( + Rbrace, + 44..45, + ), + ( + Name( + Name("__"), + ), + 45..47, + ), + ( + Unknown, + 47..48, + ), + ( + Newline, + 48..49, + ), +] +``` +## Errors +``` +[ + LexicalError { + error: FStringError( + NewlineInFormatSpec, + ), + location: 13..14, + }, + LexicalError { + error: UnclosedStringError, + location: 18..19, + }, + LexicalError { + error: FStringError( + NewlineInFormatSpec, + ), + location: 32..33, + }, + LexicalError { + error: UnclosedStringError, + location: 47..48, + }, +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap index 287d62d08a42c..381aa8e626ba7 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -36,7 +35,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{}", ), 8..12, @@ -59,7 +58,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 16..17, @@ -90,7 +89,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{", ), 23..25, @@ -107,7 +106,7 @@ snapshot_kind: text 26..27, ), ( - FStringMiddle( + InterpolatedStringMiddle( "}", ), 27..29, @@ -130,7 +129,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{{}}", ), 33..41, @@ -153,7 +152,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 45..46, @@ -170,7 +169,7 @@ snapshot_kind: text 47..48, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {} {", ), 48..56, @@ -187,7 +186,7 @@ snapshot_kind: text 57..58, ), ( - FStringMiddle( + InterpolatedStringMiddle( "} {{}} ", ), 58..71, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap index 5476c1fa02ab0..53a3fe7908763 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(MAC_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\r more text", ), 2..19, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap index 19e0346f43483..d8e27f0661ab5 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(UNIX_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\n more text", ), 2..19, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap index c4f595a38927c..ba73b4a09d194 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(WINDOWS_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\r\n more text", ), 2..20, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap index 400f81636f8dd..26380715dc730 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -32,7 +31,7 @@ snapshot_kind: text 7..8, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 8..9, @@ -69,7 +68,7 @@ snapshot_kind: text 14..15, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".3f", ), 15..18, @@ -82,7 +81,7 @@ snapshot_kind: text 18..19, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 19..20, @@ -105,7 +104,7 @@ snapshot_kind: text 22..23, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".", ), 23..24, @@ -128,7 +127,7 @@ snapshot_kind: text 26..27, ), ( - FStringMiddle( + InterpolatedStringMiddle( "f", ), 27..28, @@ -141,7 +140,7 @@ snapshot_kind: text 28..29, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 29..30, @@ -164,7 +163,7 @@ snapshot_kind: text 33..34, ), ( - FStringMiddle( + InterpolatedStringMiddle( "*^", ), 34..36, @@ -209,7 +208,7 @@ snapshot_kind: text 43..44, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 44..45, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap index f48c742e6d26a..523b5d61333f8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 2..6, @@ -41,7 +40,7 @@ snapshot_kind: text 11..12, ), ( - FStringMiddle( + InterpolatedStringMiddle( " bar", ), 12..16, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap index 341455e1f2326..9a7be930a8b17 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 4..6, @@ -41,7 +40,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( "d\n", ), 14..16, @@ -54,7 +53,7 @@ snapshot_kind: text 16..17, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 17..19, @@ -81,7 +80,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 27..29, @@ -108,7 +107,7 @@ snapshot_kind: text 36..37, ), ( - FStringMiddle( + InterpolatedStringMiddle( "a\n b\n c\n", ), 37..61, @@ -121,7 +120,7 @@ snapshot_kind: text 61..62, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 62..64, @@ -140,157 +139,5 @@ snapshot_kind: text Newline, 67..68, ), - ( - FStringStart, - 68..70, - TokenFlags( - F_STRING, - ), - ), - ( - FStringMiddle( - "__", - ), - 70..72, - TokenFlags( - F_STRING, - ), - ), - ( - Lbrace, - 72..73, - ), - ( - NonLogicalNewline, - 73..74, - ), - ( - Name( - Name("x"), - ), - 78..79, - ), - ( - Colon, - 79..80, - ), - ( - FStringMiddle( - "d", - ), - 80..81, - TokenFlags( - F_STRING, - ), - ), - ( - NonLogicalNewline, - 81..82, - ), - ( - Rbrace, - 82..83, - ), - ( - FStringMiddle( - "__", - ), - 83..85, - TokenFlags( - F_STRING, - ), - ), - ( - FStringEnd, - 85..86, - TokenFlags( - F_STRING, - ), - ), - ( - Newline, - 86..87, - ), - ( - FStringStart, - 87..89, - TokenFlags( - F_STRING, - ), - ), - ( - FStringMiddle( - "__", - ), - 89..91, - TokenFlags( - F_STRING, - ), - ), - ( - Lbrace, - 91..92, - ), - ( - NonLogicalNewline, - 92..93, - ), - ( - Name( - Name("x"), - ), - 97..98, - ), - ( - Colon, - 98..99, - ), - ( - FStringMiddle( - "a", - ), - 99..100, - TokenFlags( - F_STRING, - ), - ), - ( - NonLogicalNewline, - 100..101, - ), - ( - Name( - Name("b"), - ), - 109..110, - ), - ( - NonLogicalNewline, - 110..111, - ), - ( - Rbrace, - 111..112, - ), - ( - FStringMiddle( - "__", - ), - 112..114, - TokenFlags( - F_STRING, - ), - ), - ( - FStringEnd, - 114..115, - TokenFlags( - F_STRING, - ), - ), - ( - Newline, - 115..116, - ), ] ``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap index 8f83f01d571ec..bf3571a289e51 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -28,7 +27,7 @@ snapshot_kind: text 4..5, ), ( - FStringMiddle( + InterpolatedStringMiddle( "=10", ), 5..8, @@ -41,7 +40,7 @@ snapshot_kind: text 8..9, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 9..10, @@ -82,7 +81,7 @@ snapshot_kind: text 18..19, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 19..20, @@ -133,7 +132,7 @@ snapshot_kind: text 30..31, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 31..32, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap index 73b431eccc1c5..377acaf33d86f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\0", ), 2..4, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap new file mode 100644 index 0000000000000..f2d59004e55af --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap @@ -0,0 +1,226 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + FStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar ", + ), + 9..13, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Lbrace, + 13..14, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + Plus, + 16..17, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("wow"), + ), + 21..24, + ), + ( + Rbrace, + 24..25, + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 26..27, + ), + ( + FStringEnd, + 27..28, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " baz", + ), + 29..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 33..34, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + FStringStart, + 35..37, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 37..41, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 41..42, + ), + ( + TStringStart, + 42..44, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar", + ), + 44..47, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 47..48, + TokenFlags( + T_STRING, + ), + ), + ( + Exclamation, + 48..49, + ), + ( + Name( + Name("r"), + ), + 49..50, + ), + ( + Rbrace, + 50..51, + ), + ( + InterpolatedStringMiddle( + " some ", + ), + 51..57, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 57..58, + ), + ( + FStringStart, + 58..60, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "another", + ), + 60..67, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + FStringEnd, + 67..68, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Rbrace, + 68..69, + ), + ( + FStringEnd, + 69..70, + TokenFlags( + F_STRING, + ), + ), + ( + Newline, + 70..70, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap new file mode 100644 index 0000000000000..dde7870ae82ec --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap @@ -0,0 +1,105 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "normal ", + ), + 2..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("foo"), + ), + 10..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " {another} ", + ), + 14..27, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 27..28, + ), + ( + Name( + Name("bar"), + ), + 28..31, + ), + ( + Rbrace, + 31..32, + ), + ( + InterpolatedStringMiddle( + " {", + ), + 32..35, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 35..36, + ), + ( + Name( + Name("three"), + ), + 36..41, + ), + ( + Rbrace, + 41..42, + ), + ( + InterpolatedStringMiddle( + "}", + ), + 42..44, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 44..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 45..45, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap new file mode 100644 index 0000000000000..5cbdf8897975b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\n# not a comment ", + ), + 4..21, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 21..22, + ), + ( + Comment, + 23..34, + ), + ( + NonLogicalNewline, + 34..35, + ), + ( + Name( + Name("x"), + ), + 39..40, + ), + ( + NonLogicalNewline, + 40..41, + ), + ( + Rbrace, + 41..42, + ), + ( + InterpolatedStringMiddle( + " # not a comment\n", + ), + 42..59, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 59..62, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 62..62, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap new file mode 100644 index 0000000000000..2e911b3250016 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("x"), + ), + 3..4, + ), + ( + Exclamation, + 4..5, + ), + ( + Name( + Name("s"), + ), + 5..6, + ), + ( + Rbrace, + 6..7, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 7..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 8..9, + ), + ( + Name( + Name("x"), + ), + 9..10, + ), + ( + Equal, + 10..11, + ), + ( + Exclamation, + 11..12, + ), + ( + Name( + Name("r"), + ), + 12..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 14..15, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 15..16, + ), + ( + Name( + Name("x"), + ), + 16..17, + ), + ( + Colon, + 17..18, + ), + ( + InterpolatedStringMiddle( + ".3f!r", + ), + 18..23, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 23..24, + ), + ( + InterpolatedStringMiddle( + " {x!r}", + ), + 24..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 32..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 33..33, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap new file mode 100644 index 0000000000000..a69d1b8d9786b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 3..4, + ), + ( + Name( + Name("x"), + ), + 4..5, + ), + ( + Colon, + 5..6, + ), + ( + InterpolatedStringMiddle( + "\\\"\\", + ), + 6..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Rbrace, + 11..12, + ), + ( + Rbrace, + 12..13, + ), + ( + InterpolatedStringMiddle( + " \\\"\\\"\\\n end", + ), + 13..24, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 24..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 25..25, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap new file mode 100644 index 0000000000000..2cf409eae54c6 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 2..3, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 3..4, + ), + ( + Name( + Name("foo"), + ), + 4..7, + ), + ( + Rbrace, + 7..8, + ), + ( + TStringEnd, + 8..9, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 10..12, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\\\", + ), + 12..14, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 14..15, + ), + ( + Name( + Name("foo"), + ), + 15..18, + ), + ( + Rbrace, + 18..19, + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 21..23, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\{foo}", + ), + 23..31, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 31..32, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 33..35, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\\\{foo}", + ), + 35..44, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 44..45, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 45..45, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap new file mode 100644 index 0000000000000..f7b2b27cb8bf8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 3..4, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 4..5, + ), + ( + Name( + Name("x"), + ), + 5..6, + ), + ( + Colon, + 6..7, + ), + ( + InterpolatedStringMiddle( + "\\\"\\", + ), + 7..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 10..11, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Rbrace, + 12..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " \\\"\\\"\\\n end", + ), + 14..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Newline, + 26..26, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap new file mode 100644 index 0000000000000..13894db564971 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "first ", + ), + 2..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 8..9, + ), + ( + NonLogicalNewline, + 9..10, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + NonLogicalNewline, + 15..16, + ), + ( + Star, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("y"), + ), + 38..39, + ), + ( + NonLogicalNewline, + 39..40, + ), + ( + Rbrace, + 40..41, + ), + ( + InterpolatedStringMiddle( + " second", + ), + 41..48, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 48..49, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 49..49, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap new file mode 100644 index 0000000000000..5f4f4496d1af9 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\nhello\n world\n", + ), + 4..21, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 21..24, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 25..29, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\n world\nhello\n", + ), + 29..46, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 46..49, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 50..52, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "some ", + ), + 52..57, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 57..58, + ), + ( + TStringStart, + 58..62, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "multiline\nallowed ", + ), + 62..80, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 80..81, + ), + ( + Name( + Name("x"), + ), + 81..82, + ), + ( + Rbrace, + 82..83, + ), + ( + TStringEnd, + 83..86, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 86..87, + ), + ( + InterpolatedStringMiddle( + " string", + ), + 87..94, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 94..95, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 95..95, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap new file mode 100644 index 0000000000000..f900cbf95b6db --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\N{BULLET} normal \\Nope \\N", + ), + 2..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 28..29, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 29..29, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap new file mode 100644 index 0000000000000..73b022ad3b082 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + InterpolatedStringMiddle( + "\\N", + ), + 3..5, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + Name( + Name("BULLET"), + ), + 6..12, + ), + ( + Rbrace, + 12..13, + ), + ( + InterpolatedStringMiddle( + " normal", + ), + 13..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 20..21, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Newline, + 21..21, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap new file mode 100644 index 0000000000000..f786acf7d87db --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + TStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar ", + ), + 9..13, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 13..14, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + Plus, + 16..17, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("wow"), + ), + 21..24, + ), + ( + Rbrace, + 24..25, + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 26..27, + ), + ( + TStringEnd, + 27..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " baz", + ), + 29..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 33..34, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 35..37, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 37..41, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 41..42, + ), + ( + TStringStart, + 42..44, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar", + ), + 44..47, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 47..48, + TokenFlags( + T_STRING, + ), + ), + ( + Rbrace, + 48..49, + ), + ( + InterpolatedStringMiddle( + " some ", + ), + 49..55, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 55..56, + ), + ( + TStringStart, + 56..58, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "another", + ), + 58..65, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 65..66, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 66..67, + ), + ( + TStringEnd, + 67..68, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 68..68, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap new file mode 100644 index 0000000000000..c4db37da4cb7d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap @@ -0,0 +1,168 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + ( + NonLogicalNewline, + 0..1, + ), + ( + TStringStart, + 1..3, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 3..5, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + NonLogicalNewline, + 6..7, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Colon, + 12..13, + ), + ( + Unknown, + 13..14, + ), + ( + NonLogicalNewline, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + Name( + Name("__"), + ), + 16..18, + ), + ( + Unknown, + 18..19, + ), + ( + Newline, + 19..20, + ), + ( + TStringStart, + 20..22, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 22..24, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("x"), + ), + 30..31, + ), + ( + Colon, + 31..32, + ), + ( + Unknown, + 32..33, + ), + ( + NonLogicalNewline, + 33..34, + ), + ( + Name( + Name("b"), + ), + 42..43, + ), + ( + NonLogicalNewline, + 43..44, + ), + ( + Rbrace, + 44..45, + ), + ( + Name( + Name("__"), + ), + 45..47, + ), + ( + Unknown, + 47..48, + ), + ( + Newline, + 48..49, + ), +] +``` +## Errors +``` +[ + LexicalError { + error: TStringError( + NewlineInFormatSpec, + ), + location: 13..14, + }, + LexicalError { + error: UnclosedStringError, + location: 18..19, + }, + LexicalError { + error: TStringError( + NewlineInFormatSpec, + ), + location: 32..33, + }, + LexicalError { + error: UnclosedStringError, + location: 47..48, + }, +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap new file mode 100644 index 0000000000000..fcccc68b40a04 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap @@ -0,0 +1,209 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Rbrace, + 3..4, + ), + ( + TStringEnd, + 4..5, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 6..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{}", + ), + 8..12, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 12..13, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 14..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + " ", + ), + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 17..18, + ), + ( + Rbrace, + 18..19, + ), + ( + TStringEnd, + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 21..23, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{", + ), + 23..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 25..26, + ), + ( + Rbrace, + 26..27, + ), + ( + InterpolatedStringMiddle( + "}", + ), + 27..29, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 29..30, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 31..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{{}}", + ), + 33..41, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 43..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + " ", + ), + 45..46, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 46..47, + ), + ( + Rbrace, + 47..48, + ), + ( + InterpolatedStringMiddle( + " {} {", + ), + 48..56, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 56..57, + ), + ( + Rbrace, + 57..58, + ), + ( + InterpolatedStringMiddle( + "} {{}} ", + ), + 58..71, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 71..72, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 72..72, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap new file mode 100644 index 0000000000000..8a285e58fb536 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap @@ -0,0 +1,153 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 4..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 6..7, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 8..11, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 11..12, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 13..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 18..21, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 21..22, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 23..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 26..27, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 28..31, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 31..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 33..36, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 36..37, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 38..41, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 43..46, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 46..47, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + Newline, + 47..47, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap new file mode 100644 index 0000000000000..515c486b9bd65 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(MAC_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\r more text", + ), + 2..19, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 20..20, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap new file mode 100644 index 0000000000000..eed02e6ab6cd9 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(UNIX_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\n more text", + ), + 2..19, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 20..20, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap new file mode 100644 index 0000000000000..424092796db07 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\r\n more text", + ), + 2..20, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 20..21, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 21..21, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap new file mode 100644 index 0000000000000..d8760b764d29d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap @@ -0,0 +1,289 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("foo"), + ), + 3..6, + ), + ( + Colon, + 6..7, + ), + ( + Rbrace, + 7..8, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 8..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Equal, + 11..12, + ), + ( + Exclamation, + 12..13, + ), + ( + Name( + Name("s"), + ), + 13..14, + ), + ( + Colon, + 14..15, + ), + ( + InterpolatedStringMiddle( + ".3f", + ), + 15..18, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 18..19, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("x"), + ), + 21..22, + ), + ( + Colon, + 22..23, + ), + ( + InterpolatedStringMiddle( + ".", + ), + 23..24, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + Name( + Name("y"), + ), + 25..26, + ), + ( + Rbrace, + 26..27, + ), + ( + InterpolatedStringMiddle( + "f", + ), + 27..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 29..30, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 30..31, + ), + ( + String( + "", + ), + 31..33, + ), + ( + Colon, + 33..34, + ), + ( + InterpolatedStringMiddle( + "*^", + ), + 34..36, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 36..37, + ), + ( + Int( + 1, + ), + 37..38, + ), + ( + Colon, + 38..39, + ), + ( + Lbrace, + 39..40, + ), + ( + Int( + 1, + ), + 40..41, + ), + ( + Rbrace, + 41..42, + ), + ( + Rbrace, + 42..43, + ), + ( + Rbrace, + 43..44, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 44..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 45..46, + ), + ( + Name( + Name("x"), + ), + 46..47, + ), + ( + Colon, + 47..48, + ), + ( + Lbrace, + 48..49, + ), + ( + Lbrace, + 49..50, + ), + ( + Int( + 1, + ), + 50..51, + ), + ( + Rbrace, + 51..52, + ), + ( + Dot, + 52..53, + ), + ( + Name( + Name("pop"), + ), + 53..56, + ), + ( + Lpar, + 56..57, + ), + ( + Rpar, + 57..58, + ), + ( + Rbrace, + 58..59, + ), + ( + Rbrace, + 59..60, + ), + ( + TStringEnd, + 60..61, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 61..61, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap new file mode 100644 index 0000000000000..739930ef4230b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + Exclamation, + 7..8, + ), + ( + Name( + Name("pwd"), + ), + 8..11, + ), + ( + Rbrace, + 11..12, + ), + ( + InterpolatedStringMiddle( + " bar", + ), + 12..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 17..17, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap new file mode 100644 index 0000000000000..679142f387a46 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap @@ -0,0 +1,125 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Lambda, + 3..9, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Colon, + 11..12, + ), + ( + Lbrace, + 12..13, + ), + ( + Name( + Name("x"), + ), + 13..14, + ), + ( + Rbrace, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 17..18, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Lpar, + 21..22, + ), + ( + Lambda, + 22..28, + ), + ( + Name( + Name("x"), + ), + 29..30, + ), + ( + Colon, + 30..31, + ), + ( + Lbrace, + 31..32, + ), + ( + Name( + Name("x"), + ), + 32..33, + ), + ( + Rbrace, + 33..34, + ), + ( + Rpar, + 34..35, + ), + ( + Rbrace, + 35..36, + ), + ( + TStringEnd, + 36..37, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 37..37, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap new file mode 100644 index 0000000000000..a1e48290d141e --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap @@ -0,0 +1,143 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 4..6, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + NonLogicalNewline, + 7..8, + ), + ( + Name( + Name("x"), + ), + 12..13, + ), + ( + Colon, + 13..14, + ), + ( + InterpolatedStringMiddle( + "d\n", + ), + 14..16, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 16..17, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 17..19, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 19..22, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 22..23, + ), + ( + TStringStart, + 23..27, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 27..29, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 29..30, + ), + ( + NonLogicalNewline, + 30..31, + ), + ( + Name( + Name("x"), + ), + 35..36, + ), + ( + Colon, + 36..37, + ), + ( + InterpolatedStringMiddle( + "a\n b\n c\n", + ), + 37..61, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 61..62, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 62..64, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 64..67, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 67..68, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap new file mode 100644 index 0000000000000..7ab9d20f3f8f8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap @@ -0,0 +1,187 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("x"), + ), + 3..4, + ), + ( + Colon, + 4..5, + ), + ( + InterpolatedStringMiddle( + "=10", + ), + 5..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 8..9, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 9..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 10..11, + ), + ( + Lpar, + 11..12, + ), + ( + Name( + Name("x"), + ), + 12..13, + ), + ( + ColonEqual, + 13..15, + ), + ( + Int( + 10, + ), + 15..17, + ), + ( + Rpar, + 17..18, + ), + ( + Rbrace, + 18..19, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("x"), + ), + 21..22, + ), + ( + Comma, + 22..23, + ), + ( + Lbrace, + 23..24, + ), + ( + Name( + Name("y"), + ), + 24..25, + ), + ( + ColonEqual, + 25..27, + ), + ( + Int( + 10, + ), + 27..29, + ), + ( + Rbrace, + 29..30, + ), + ( + Rbrace, + 30..31, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 31..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 32..33, + ), + ( + Lsqb, + 33..34, + ), + ( + Name( + Name("x"), + ), + 34..35, + ), + ( + ColonEqual, + 35..37, + ), + ( + Int( + 10, + ), + 37..39, + ), + ( + Rsqb, + 39..40, + ), + ( + Rbrace, + 40..41, + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 42..42, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap new file mode 100644 index 0000000000000..483ea5d6ec5eb --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\0", + ), + 2..4, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 4..5, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 5..5, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap index 84fbc3cae0ecd..04e5596e74227 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__backspace_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..15, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..15, + node_index: AtomicNodeIndex(..), value: "\u{8}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap index 4cfaed6c05d26..8f28967afe49c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__bell_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..9, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..9, + node_index: AtomicNodeIndex(..), value: "\u{7}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap index 9fce7955def23..258440c8d44d3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__carriage_return_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..21, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..21, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..21, + node_index: AtomicNodeIndex(..), value: "\r", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap index 9f6bf06e28248..447e6a7e416cd 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__character_tabulation_with_justification_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..45, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..45, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..45, + node_index: AtomicNodeIndex(..), value: "\u{89}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap index 9a8831f2dbaf5..ac810427d1e7c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__delete_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..12, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..12, + node_index: AtomicNodeIndex(..), value: "\u{7f}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap index 3fd84081eb8d9..ccac176830a4d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__dont_panic_on_8_in_octal_escape.snap @@ -1,15 +1,16 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..16, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("bold"), ctx: Store, @@ -18,11 +19,13 @@ snapshot_kind: text ], value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 7..16, value: StringLiteralValue { inner: Single( StringLiteral { range: 7..16, + node_index: AtomicNodeIndex(..), value: "\u{3}8[1m", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap index fd003b6a4af61..59f9b5213b1de 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__double_quoted_byte.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..738, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..738, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..738, + node_index: AtomicNodeIndex(..), value: [ 0, 1, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap index d924ebd21a712..4f706e1fde15d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..12, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..12, + node_index: AtomicNodeIndex(..), value: "\u{1b}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap index e830dd89ea42c..aad2af989480b 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_char_in_byte_literal.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..13, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..13, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..13, + node_index: AtomicNodeIndex(..), value: [ 111, 109, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap index cf13defcb6a26..cfc7e3ee09815 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__escape_octet.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..14, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..14, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..14, + node_index: AtomicNodeIndex(..), value: [ 35, 97, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap index bd6d96055975e..21c8747c01ec1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__form_feed_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..15, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..15, + node_index: AtomicNodeIndex(..), value: "\u{c}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap index 99985450fdc24..b418df379e042 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..22, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..22, value: FStringValue { inner: Single( FString( FString { range: 0..22, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..5, + node_index: AtomicNodeIndex(..), value: "aaa", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..10, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("bbb"), ctx: Load, @@ -38,16 +43,19 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 10..13, + node_index: AtomicNodeIndex(..), value: "ccc", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 13..18, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("ddd"), ctx: Load, @@ -59,8 +67,9 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 18..21, + node_index: AtomicNodeIndex(..), value: "eee", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap index 2721f57e773e9..8cb891a551b44 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..8, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..8, value: FStringValue { inner: Single( FString( FString { range: 0..8, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..4, + node_index: AtomicNodeIndex(..), value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 4..7, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap index 44b3a5dcad767..b1df67c78f8f5 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..8, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..8, value: FStringValue { inner: Single( FString( FString { range: 0..8, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..4, + node_index: AtomicNodeIndex(..), value: "\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 4..7, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap index 898ff2d34766c..c3fd51c096ba4 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..9, value: FStringValue { inner: Single( FString( FString { range: 0..9, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 3..5, + node_index: AtomicNodeIndex(..), value: "\\\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap index 1f21cd2286ed3..35e090916ce47 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..10, value: FStringValue { inner: Single( FString( FString { range: 0..10, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..7, id: Name("user"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap index 9a6ed3225841a..e05de72bc4655 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..38, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..38, value: FStringValue { inner: Single( FString( FString { range: 0..38, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..6, + node_index: AtomicNodeIndex(..), value: "mix ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 6..13, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..11, id: Name("user"), ctx: Load, @@ -43,16 +48,19 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 13..28, + node_index: AtomicNodeIndex(..), value: " with text and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 28..37, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..35, id: Name("second"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap index 8c786dbbd14ce..932339c3590d4 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..14, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..14, value: FStringValue { inner: Single( FString( FString { range: 0..14, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..13, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..7, id: Name("user"), ctx: Load, @@ -34,12 +38,14 @@ snapshot_kind: text ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 9..12, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 9..12, + node_index: AtomicNodeIndex(..), value: ">10", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap index eedba2c8bcc06..f651b99dff099 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..11, value: FStringValue { inner: Single( FString( FString { range: 0..11, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 4..5, + node_index: AtomicNodeIndex(..), value: "\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap index c65231cc33bdf..4d694d9dcaf9c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__hts_alias.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..9, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..9, + node_index: AtomicNodeIndex(..), value: "\u{88}", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap index ffd0191500176..52bb0728d36b3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..3, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..3, value: FStringValue { inner: Single( FString( FString { range: 0..3, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap new file mode 100644 index 0000000000000..3e32e6364a785 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap @@ -0,0 +1,34 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..3, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..3, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..3, + node_index: AtomicNodeIndex(..), + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap index 983da17da44fc..291eb7e8f4f5e 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..17, value: FStringValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text Literal( StringLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -27,10 +29,12 @@ snapshot_kind: text FString( FString { range: 9..17, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, + node_index: AtomicNodeIndex(..), value: "world", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap index 983da17da44fc..291eb7e8f4f5e 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..17, value: FStringValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text Literal( StringLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -27,10 +29,12 @@ snapshot_kind: text FString( FString { range: 9..17, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, + node_index: AtomicNodeIndex(..), value: "world", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap index 8479de9437c00..66e0a2cbd93cc 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..22, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..22, value: FStringValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text Literal( StringLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -27,23 +29,28 @@ snapshot_kind: text FString( FString { range: 9..22, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, + node_index: AtomicNodeIndex(..), value: "world", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 16..21, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, value: StringLiteralValue { inner: Single( StringLiteral { range: 17..20, + node_index: AtomicNodeIndex(..), value: "!", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap index 56a819bd717cb..d7830a63d0ecf 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..31, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..31, value: FStringValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text Literal( StringLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -27,23 +29,28 @@ snapshot_kind: text FString( FString { range: 9..22, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, + node_index: AtomicNodeIndex(..), value: "world", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 16..21, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, value: StringLiteralValue { inner: Single( StringLiteral { range: 17..20, + node_index: AtomicNodeIndex(..), value: "!", flags: StringLiteralFlags { quote_style: Double, @@ -71,6 +78,7 @@ snapshot_kind: text Literal( StringLiteral { range: 23..31, + node_index: AtomicNodeIndex(..), value: "again!", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap new file mode 100644 index 0000000000000..5e211ea8ca933 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap @@ -0,0 +1,64 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..18, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..18, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 0..9, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..8, + node_index: AtomicNodeIndex(..), + value: "Hello ", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap new file mode 100644 index 0000000000000..67c09862be6b1 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 0..9, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..8, + node_index: AtomicNodeIndex(..), + value: "Hello ", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + node_index: AtomicNodeIndex(..), + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap index 4ec8dc27becaf..d4d4e01a4afe3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..18, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..18, value: FStringValue { inner: Single( FString( FString { range: 0..18, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..5, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("a"), ctx: Load, @@ -31,11 +35,13 @@ snapshot_kind: text format_spec: None, }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..10, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("b"), ctx: Load, @@ -47,8 +53,9 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 10..17, + node_index: AtomicNodeIndex(..), value: "{foo}", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap index 58821196bd95e..93333df2aade4 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap @@ -1,29 +1,34 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..13, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..13, value: FStringValue { inner: Single( FString( FString { range: 0..13, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, + node_index: AtomicNodeIndex(..), expression: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 3..11, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3..5, value: Int( 42, @@ -36,6 +41,7 @@ snapshot_kind: text comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..11, value: Int( 42, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap index 9e5b4a2fc88f7..2e0f5c6474b43 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..16, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..16, value: FStringValue { inner: Single( FString( FString { range: 0..16, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..15, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("foo"), ctx: Load, @@ -29,14 +33,17 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..14, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..14, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 8..13, value: StringLiteralValue { inner: Concatenated( @@ -44,6 +51,7 @@ snapshot_kind: text strings: [ StringLiteral { range: 8..10, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -53,6 +61,7 @@ snapshot_kind: text }, StringLiteral { range: 11..13, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap index 28e6e4a2a1d65..479ea844b6436 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..15, value: FStringValue { inner: Single( FString( FString { range: 0..15, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..14, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("foo"), ctx: Load, @@ -29,14 +33,17 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..13, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..13, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..12, id: Name("spec"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap index 9dae288239476..c9ef305e708b5 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..13, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..13, value: FStringValue { inner: Single( FString( FString { range: 0..13, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("foo"), ctx: Load, @@ -29,19 +33,23 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..11, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..11, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 8..10, value: StringLiteralValue { inner: Single( StringLiteral { range: 8..10, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap index d5884e9d75424..42ef942c16787 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap @@ -1,29 +1,34 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..11, value: FStringValue { inner: Single( FString( FString { range: 0..11, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..10, + node_index: AtomicNodeIndex(..), expression: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 3..9, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3..4, value: Int( 1, @@ -36,6 +41,7 @@ snapshot_kind: text comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 2, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap index 738b731a411e9..3b428b414876a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..13, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..13, value: FStringValue { inner: Single( FString( FString { range: 0..13, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("foo"), ctx: Load, @@ -29,12 +33,14 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..11, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 7..11, + node_index: AtomicNodeIndex(..), value: "spec", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap index e1d2941dc5f13..764c3768a4012 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..10, value: FStringValue { inner: Single( FString( FString { range: 0..10, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap index e5857594c1b45..869d7ff2d3984 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..10, value: FStringValue { inner: Single( FString( FString { range: 0..10, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap index 3dca1cc84b74a..1989651e7ff01 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..10, value: FStringValue { inner: Single( FString( FString { range: 0..10, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, + node_index: AtomicNodeIndex(..), expression: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 3..8, value: None, }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap index 11f5a0bd1f674..c5e7745a97c27 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_concat.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..16, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..16, value: StringLiteralValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text strings: [ StringLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -25,6 +27,7 @@ snapshot_kind: text }, StringLiteral { range: 9..16, + node_index: AtomicNodeIndex(..), value: "world", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap index cdf62c658ad5b..295d3fd63490f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_string_triple_quotes_with_kind.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..20, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..20, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..20, + node_index: AtomicNodeIndex(..), value: "Hello, world!", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap new file mode 100644 index 0000000000000..e09433deb3d68 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..17, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..17, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: AtomicNodeIndex(..), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..17, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap new file mode 100644 index 0000000000000..e09433deb3d68 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..17, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..17, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: AtomicNodeIndex(..), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..17, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap new file mode 100644 index 0000000000000..3497f4897dde2 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: AtomicNodeIndex(..), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..22, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + node_index: AtomicNodeIndex(..), + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap new file mode 100644 index 0000000000000..9415927781806 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..31, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..31, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + node_index: AtomicNodeIndex(..), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..22, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + node_index: AtomicNodeIndex(..), + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 23..31, + node_index: AtomicNodeIndex(..), + value: "again!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap new file mode 100644 index 0000000000000..343661c2b0f6d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..18, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..18, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..18, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..5, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..4, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 7..8, + id: Name("b"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..17, + node_index: AtomicNodeIndex(..), + value: "{foo}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap new file mode 100644 index 0000000000000..676e05c163590 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap @@ -0,0 +1,73 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..13, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: AtomicNodeIndex(..), + expression: Compare( + ExprCompare { + node_index: AtomicNodeIndex(..), + range: 3..11, + left: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 3..5, + value: Int( + 42, + ), + }, + ), + ops: [ + Eq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 9..11, + value: Int( + 42, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap new file mode 100644 index 0000000000000..7154789c31cbe --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap @@ -0,0 +1,103 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..16, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..16, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..16, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..15, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..14, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..14, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 8..13, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 8..10, + node_index: AtomicNodeIndex(..), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + StringLiteral { + range: 11..13, + node_index: AtomicNodeIndex(..), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ], + value: "", + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap new file mode 100644 index 0000000000000..68bb64bd871ad --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..15, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..15, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..15, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..14, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..13, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..13, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 8..12, + id: Name("spec"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap new file mode 100644 index 0000000000000..e860374bd05fd --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..13, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 7..11, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 8..10, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 8..10, + node_index: AtomicNodeIndex(..), + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap new file mode 100644 index 0000000000000..4747eef4cd3cf --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap @@ -0,0 +1,73 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..11, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..10, + node_index: AtomicNodeIndex(..), + expression: Compare( + ExprCompare { + node_index: AtomicNodeIndex(..), + range: 3..9, + left: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 3..4, + value: Int( + 1, + ), + }, + ), + ops: [ + NotEq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 8..9, + value: Int( + 2, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap new file mode 100644 index 0000000000000..360789335eff9 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap @@ -0,0 +1,66 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..13, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 7..11, + node_index: AtomicNodeIndex(..), + value: "spec", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap new file mode 100644 index 0000000000000..cd25297e73a19 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap new file mode 100644 index 0000000000000..ab56895948994 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "= ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap new file mode 100644 index 0000000000000..bb265f1a94cc5 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: AtomicNodeIndex(..), + expression: Yield( + ExprYield { + node_index: AtomicNodeIndex(..), + range: 3..8, + value: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap index 2d864494e595c..0b9244c730362 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..18, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..18, value: FStringValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text Literal( StringLiteral { range: 0..9, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -27,10 +29,12 @@ snapshot_kind: text FString( FString { range: 10..18, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..17, + node_index: AtomicNodeIndex(..), value: "world", }, ), diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap index e3bafd8a1f461..76de944babaf5 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..22, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..22, value: FStringValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text Literal( StringLiteral { range: 0..9, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -27,10 +29,12 @@ snapshot_kind: text FString( FString { range: 10..18, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..17, + node_index: AtomicNodeIndex(..), value: "world", }, ), @@ -45,6 +49,7 @@ snapshot_kind: text Literal( StringLiteral { range: 19..22, + node_index: AtomicNodeIndex(..), value: "!", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap index ea637bffc3384..b6c48ef5d8ba4 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_1.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..17, value: StringLiteralValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text strings: [ StringLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -25,6 +27,7 @@ snapshot_kind: text }, StringLiteral { range: 9..17, + node_index: AtomicNodeIndex(..), value: "world", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap index 7748193e5befa..7ad459df3f5a8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_string_concat_2.snap @@ -1,14 +1,15 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..17, value: StringLiteralValue { inner: Concatenated( @@ -16,6 +17,7 @@ snapshot_kind: text strings: [ StringLiteral { range: 0..9, + node_index: AtomicNodeIndex(..), value: "Hello ", flags: StringLiteralFlags { quote_style: Single, @@ -25,6 +27,7 @@ snapshot_kind: text }, StringLiteral { range: 10..17, + node_index: AtomicNodeIndex(..), value: "world", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap new file mode 100644 index 0000000000000..0a72333dda37c --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap @@ -0,0 +1,56 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..18, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..18, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + node_index: AtomicNodeIndex(..), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap new file mode 100644 index 0000000000000..ac4d44cd3ff4e --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + node_index: AtomicNodeIndex(..), + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + node_index: AtomicNodeIndex(..), + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + node_index: AtomicNodeIndex(..), + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap index 68da7f5aaa72e..8c3cd6bc04657 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_1.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..8, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..8, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..8, + node_index: AtomicNodeIndex(..), value: [ 92, 120, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap index 6cf0a69a02074..f3a05cb29727f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_byte_literal_2.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..6, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..6, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..6, + node_index: AtomicNodeIndex(..), value: [ 92, 92, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap index ff531b735e42a..7156cd907306f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..7, value: FStringValue { inner: Single( FString( FString { range: 0..7, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 3..6, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap new file mode 100644 index 0000000000000..50449bdc14040 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap @@ -0,0 +1,54 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..7, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..7, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..7, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 3..6, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 4..5, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap index 921213a707749..b85749f0dc0f5 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__single_quoted_byte.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..738, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..738, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..738, + node_index: AtomicNodeIndex(..), value: [ 0, 1, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap index 67456edf4fa4a..432c43fcf4195 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_mac_eol.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..18, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..18, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..18, + node_index: AtomicNodeIndex(..), value: "text more text", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap index 67456edf4fa4a..432c43fcf4195 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_unix_eol.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..18, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..18, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..18, + node_index: AtomicNodeIndex(..), value: "text more text", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap index 46afa1625beb9..dfaefc86f8ac2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__string_parser_escaped_windows_eol.snap @@ -1,19 +1,21 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..19, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..19, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..19, + node_index: AtomicNodeIndex(..), value: "text more text", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap index 1d0ed68bc340b..a65a7d4b3945f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..11, value: FStringValue { inner: Single( FString( FString { range: 0..11, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap new file mode 100644 index 0000000000000..afbf6c6688efa --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap @@ -0,0 +1,54 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..11, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap new file mode 100644 index 0000000000000..8e099c0f668bc --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap @@ -0,0 +1,90 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..22, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..22, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..5, + node_index: AtomicNodeIndex(..), + value: "aaa", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 6..9, + id: Name("bbb"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..13, + node_index: AtomicNodeIndex(..), + value: "ccc", + }, + ), + Interpolation( + InterpolatedElement { + range: 13..18, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 14..17, + id: Name("ddd"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 18..21, + node_index: AtomicNodeIndex(..), + value: "eee", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap new file mode 100644 index 0000000000000..d65d8f0a38515 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..8, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..8, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..8, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + node_index: AtomicNodeIndex(..), + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap new file mode 100644 index 0000000000000..7b3fa584575c0 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..8, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..8, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..8, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + node_index: AtomicNodeIndex(..), + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap new file mode 100644 index 0000000000000..d285f2ff882b8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..9, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..9, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..9, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 3..5, + node_index: AtomicNodeIndex(..), + value: "\\\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap new file mode 100644 index 0000000000000..2540e8745ba15 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap new file mode 100644 index 0000000000000..2bb2303078ef4 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap @@ -0,0 +1,93 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..38, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..38, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..38, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..6, + node_index: AtomicNodeIndex(..), + value: "mix ", + }, + ), + Interpolation( + InterpolatedElement { + range: 6..13, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 7..11, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 13..28, + node_index: AtomicNodeIndex(..), + value: " with text and ", + }, + ), + Interpolation( + InterpolatedElement { + range: 28..37, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 29..35, + id: Name("second"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap new file mode 100644 index 0000000000000..43e424c4bc7e5 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..14, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..14, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..14, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..13, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 9..12, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 9..12, + node_index: AtomicNodeIndex(..), + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap new file mode 100644 index 0000000000000..188b88a0d3ef7 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..11, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 4..5, + node_index: AtomicNodeIndex(..), + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/string.rs b/crates/ruff_python_parser/src/string.rs index e2b08d1c078d5..8dd9190b90057 100644 --- a/crates/ruff_python_parser/src/string.rs +++ b/crates/ruff_python_parser/src/string.rs @@ -1,17 +1,22 @@ //! Parsing of string literals, bytes literals, and implicit string concatenation. use bstr::ByteSlice; +use std::fmt; -use ruff_python_ast::{self as ast, AnyStringFlags, Expr, StringFlags}; +use ruff_python_ast::{self as ast, AnyStringFlags, AtomicNodeIndex, Expr, StringFlags}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::error::{LexicalError, LexicalErrorType}; +use crate::{ + TokenKind, + error::{LexicalError, LexicalErrorType}, +}; #[derive(Debug)] pub(crate) enum StringType { Str(ast::StringLiteral), Bytes(ast::BytesLiteral), FString(ast::FString), + TString(ast::TString), } impl Ranged for StringType { @@ -20,6 +25,7 @@ impl Ranged for StringType { Self::Str(node) => node.range(), Self::Bytes(node) => node.range(), Self::FString(node) => node.range(), + Self::TString(node) => node.range(), } } } @@ -30,6 +36,48 @@ impl From for Expr { StringType::Str(node) => Expr::from(node), StringType::Bytes(node) => Expr::from(node), StringType::FString(node) => Expr::from(node), + StringType::TString(node) => Expr::from(node), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum InterpolatedStringKind { + FString, + TString, +} + +impl InterpolatedStringKind { + #[inline] + pub(crate) const fn start_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringStart, + InterpolatedStringKind::TString => TokenKind::TStringStart, + } + } + + #[inline] + pub(crate) const fn middle_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringMiddle, + InterpolatedStringKind::TString => TokenKind::TStringMiddle, + } + } + + #[inline] + pub(crate) const fn end_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringEnd, + InterpolatedStringKind::TString => TokenKind::TStringEnd, + } + } +} + +impl fmt::Display for InterpolatedStringKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterpolatedStringKind::FString => f.write_str("f-string"), + InterpolatedStringKind::TString => f.write_str("t-string"), } } } @@ -125,7 +173,7 @@ impl StringParser { return Err(LexicalError::new( LexicalErrorType::UnicodeError, TextRange::empty(self.position()), - )) + )); } } } @@ -231,12 +279,15 @@ impl StringParser { Ok(Some(EscapedChar::Literal(new_char))) } - fn parse_fstring_middle(mut self) -> Result { - // Fast-path: if the f-string doesn't contain any escape sequences, return the literal. + fn parse_interpolated_string_middle( + mut self, + ) -> Result { + // Fast-path: if the f-string or t-string doesn't contain any escape sequences, return the literal. let Some(mut index) = memchr::memchr3(b'{', b'}', b'\\', self.source.as_bytes()) else { - return Ok(ast::FStringLiteralElement { + return Ok(ast::InterpolatedStringLiteralElement { value: self.source, range: self.range, + node_index: AtomicNodeIndex::dummy(), }); }; @@ -249,7 +300,7 @@ impl StringParser { // Add the escaped character to the string. match &self.source.as_bytes()[self.cursor - 1] { - // If there are any curly braces inside a `FStringMiddle` token, + // If there are any curly braces inside a `F/TStringMiddle` token, // then they were escaped (i.e. `{{` or `}}`). This means that // we need increase the location by 2 instead of 1. b'{' => { @@ -260,7 +311,7 @@ impl StringParser { self.offset += TextSize::from(1); value.push('}'); } - // We can encounter a `\` as the last character in a `FStringMiddle` + // We can encounter a `\` as the last character in a `F/TStringMiddle` // token which is valid in this context. For example, // // ```python @@ -268,7 +319,7 @@ impl StringParser { // # ^ ^^ ^ // ``` // - // Here, the `FStringMiddle` token content will be "\" and " \" + // Here, the `F/TStringMiddle` token content will be "\" and " \" // which is invalid if we look at the content in isolation: // // ```python @@ -276,7 +327,7 @@ impl StringParser { // ``` // // However, the content is syntactically valid in the context of - // the f-string because it's a substring of the entire f-string. + // the f/t-string because it's a substring of the entire f/t-string. // This is still an invalid escape sequence, but we don't want to // raise a syntax error as is done by the CPython parser. It might // be supported in the future, refer to point 3: https://peps.python.org/pep-0701/#rejected-ideas @@ -311,9 +362,10 @@ impl StringParser { index = next_index; } - Ok(ast::FStringLiteralElement { + Ok(ast::InterpolatedStringLiteralElement { value: value.into_boxed_str(), range: self.range, + node_index: AtomicNodeIndex::dummy(), }) } @@ -335,6 +387,7 @@ impl StringParser { value: self.source.into_boxed_bytes(), range: self.range, flags: self.flags.into(), + node_index: AtomicNodeIndex::dummy(), })); } @@ -344,6 +397,7 @@ impl StringParser { value: self.source.into_boxed_bytes(), range: self.range, flags: self.flags.into(), + node_index: AtomicNodeIndex::dummy(), })); }; @@ -381,6 +435,7 @@ impl StringParser { value: value.into_boxed_slice(), range: self.range, flags: self.flags.into(), + node_index: AtomicNodeIndex::dummy(), })) } @@ -391,6 +446,7 @@ impl StringParser { value: self.source, range: self.range, flags: self.flags.into(), + node_index: AtomicNodeIndex::dummy(), })); } @@ -400,6 +456,7 @@ impl StringParser { value: self.source, range: self.range, flags: self.flags.into(), + node_index: AtomicNodeIndex::dummy(), })); }; @@ -437,6 +494,7 @@ impl StringParser { value: value.into_boxed_str(), range: self.range, flags: self.flags.into(), + node_index: AtomicNodeIndex::dummy(), })) } @@ -458,12 +516,12 @@ pub(crate) fn parse_string_literal( } // TODO(dhruvmanila): Move this to the new parser -pub(crate) fn parse_fstring_literal_element( +pub(crate) fn parse_interpolated_string_literal_element( source: Box, flags: AnyStringFlags, range: TextRange, -) -> Result { - StringParser::new(source, flags, range.start(), range).parse_fstring_middle() +) -> Result { + StringParser::new(source, flags, range.start(), range).parse_interpolated_string_middle() } #[cfg(test)] @@ -471,7 +529,7 @@ mod tests { use ruff_python_ast::Suite; use crate::error::LexicalErrorType; - use crate::{parse_module, FStringErrorType, ParseError, ParseErrorType, Parsed}; + use crate::{InterpolatedStringErrorType, ParseError, ParseErrorType, Parsed, parse_module}; const WINDOWS_EOL: &str = "\r\n"; const MAC_EOL: &str = "\r"; @@ -553,7 +611,7 @@ mod tests { insta::assert_debug_snapshot!(suite); } - fn parse_fstring_error(source: &str) -> FStringErrorType { + fn parse_fstring_error(source: &str) -> InterpolatedStringErrorType { parse_suite(source) .map_err(|e| match e.error { ParseErrorType::Lexical(LexicalErrorType::FStringError(e)) => e, @@ -565,7 +623,7 @@ mod tests { #[test] fn test_parse_invalid_fstring() { - use FStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; assert_eq!(parse_fstring_error(r#"f"{5!x}""#), InvalidConversionFlag); assert_eq!( @@ -616,6 +674,118 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_parse_tstring() { + let source = r#"t"{a}{ b }{{foo}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_spec() { + let source = r#"t"{foo:{spec}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_not_nested_spec() { + let source = r#"t"{foo:spec}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_empty_tstring() { + let source = r#"t"""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base() { + let source = r#"t"{user=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base_more() { + let source = r#"t"mix {user=} with text and {second=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_format() { + let source = r#"t"{user=:>10}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + fn parse_tstring_error(source: &str) -> InterpolatedStringErrorType { + parse_suite(source) + .map_err(|e| match e.error { + ParseErrorType::Lexical(LexicalErrorType::TStringError(e)) => e, + ParseErrorType::TStringError(e) => e, + e => unreachable!("Expected TStringError: {:?}", e), + }) + .expect_err("Expected error") + } + + #[test] + fn test_parse_invalid_tstring() { + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + + assert_eq!(parse_tstring_error(r#"t"{5!x}""#), InvalidConversionFlag); + assert_eq!( + parse_tstring_error("t'{lambda x:{x}}'"), + LambdaWithoutParentheses + ); + // NOTE: The parser produces the `LambdaWithoutParentheses` for this case, but + // since the parser only return the first error to maintain compatibility with + // the rest of the codebase, this test case fails. The `LambdaWithoutParentheses` + // error appears after the unexpected `tStringMiddle` token, which is between the + // `:` and the `{`. + // assert_eq!(parse_tstring_error("f'{lambda x: {x}}'"), LambdaWithoutParentheses); + assert!(parse_suite(r#"t"{class}""#).is_err()); + } + + #[test] + fn test_parse_tstring_not_equals() { + let source = r#"t"{1 != 2}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_equals() { + let source = r#"t"{42 == 42}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_prec_space() { + let source = r#"t"{x =}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_trailing_space() { + let source = r#"t"{x= }""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_yield_expr() { + let source = r#"t"{yield}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + #[test] fn test_parse_string_concat() { let source = "'Hello ' 'world'"; @@ -679,6 +849,62 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_parse_t_string_concat_1() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_2() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_3() { + let source = "'Hello ' t'world{\"!\"}'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_4() { + let source = "'Hello ' t'world{\"!\"}' 'again!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_1() { + let source = "u'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_2() { + let source = "u'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_1() { + let source = "f'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_2() { + let source = "f'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + #[test] fn test_parse_string_triple_quotes_with_kind() { let source = "u'''Hello, world!'''"; @@ -796,6 +1022,71 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_tstring_escaped_newline() { + let source = r#"t"\n{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_constant_range() { + let source = r#"t"aaa{bbb}ccc{ddd}eee""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_unescaped_newline() { + let source = r#"t""" +{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_escaped_character() { + let source = r#"t"\\{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_tstring() { + let source = r#"rt"{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_triple_quoted_raw_tstring() { + let source = r#"rt"""{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_line_continuation() { + let source = r#"rt"\ +{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_string_spec() { + let source = r#"t"{foo:{''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_concatenation_string_spec() { + let source = r#"t"{foo:{'' ''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + /// #[test] fn test_dont_panic_on_8_in_octal_escape() { diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 193aecac51266..240e015a3bcdf 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -12,12 +12,12 @@ use bitflags::bitflags; use ruff_python_ast::name::Name; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_ast::str_prefix::{ - AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, + AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix, }; use ruff_python_ast::{AnyStringFlags, BoolOp, Int, IpyEscapeKind, Operator, StringFlags, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, get_size2::GetSize)] pub struct Token { /// The kind of the token. kind: TokenKind, @@ -48,7 +48,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn is_triple_quoted_string(self) -> bool { self.unwrap_string_flags().is_triple_quoted() } @@ -57,7 +57,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn string_quote_style(self) -> Quote { self.unwrap_string_flags().quote_style() } @@ -66,7 +66,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn unwrap_string_flags(self) -> AnyStringFlags { self.string_flags() .unwrap_or_else(|| panic!("token to be a string")) @@ -81,7 +81,8 @@ impl Token { } } - /// Returns `true` if this is any kind of string token. + /// Returns `true` if this is any kind of string token - including + /// tokens in t-strings (which do not have type `str`). const fn is_any_string(self) -> bool { matches!( self.kind, @@ -89,6 +90,9 @@ impl Token { | TokenKind::FStringStart | TokenKind::FStringMiddle | TokenKind::FStringEnd + | TokenKind::TStringStart + | TokenKind::TStringMiddle + | TokenKind::TStringEnd ) } } @@ -120,7 +124,7 @@ impl fmt::Debug for Token { } /// A kind of a token. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord, get_size2::GetSize)] pub enum TokenKind { /// Token kind for a name, commonly known as an identifier. Name, @@ -140,6 +144,14 @@ pub enum TokenKind { FStringMiddle, /// Token kind for the end of an f-string. This includes the closing quote. FStringEnd, + /// Token kind for the start of a t-string. This includes the `t`/`T`/`tr` prefix + /// and the opening quote(s). + TStringStart, + /// Token kind that includes the portion of text inside the t-string that's not + /// part of the interpolation part and isn't an opening or closing brace. + TStringMiddle, + /// Token kind for the end of a t-string. This includes the closing quote. + TStringEnd, /// Token kind for a IPython escape command. IpyEscapeCommand, /// Token kind for a comment. These are filtered out of the token stream prior to parsing. @@ -462,6 +474,11 @@ impl TokenKind { matches!(self, TokenKind::Plus | TokenKind::Minus) } + #[inline] + pub const fn is_interpolated_string_end(self) -> bool { + matches!(self, TokenKind::FStringEnd | TokenKind::TStringEnd) + } + /// Returns the [`UnaryOp`] that corresponds to this token kind, if it is a unary arithmetic /// operator, otherwise return [None]. /// @@ -613,6 +630,9 @@ impl fmt::Display for TokenKind { TokenKind::FStringStart => "FStringStart", TokenKind::FStringMiddle => "FStringMiddle", TokenKind::FStringEnd => "FStringEnd", + TokenKind::TStringStart => "TStringStart", + TokenKind::TStringMiddle => "TStringMiddle", + TokenKind::TStringEnd => "TStringEnd", TokenKind::IpyEscapeCommand => "IPython escape command", TokenKind::Comment => "comment", TokenKind::Question => "'?'", @@ -722,16 +742,20 @@ bitflags! { const BYTE_STRING = 1 << 3; /// The token is an f-string i.e., prefixed with `f` or `F` const F_STRING = 1 << 4; + /// The token is a t-string i.e., prefixed with `t` or `T` + const T_STRING = 1 << 5; /// The token is a raw string and the prefix character is in lowercase. - const RAW_STRING_LOWERCASE = 1 << 5; + const RAW_STRING_LOWERCASE = 1 << 6; /// The token is a raw string and the prefix character is in uppercase. - const RAW_STRING_UPPERCASE = 1 << 6; + const RAW_STRING_UPPERCASE = 1 << 7; /// The token is a raw string i.e., prefixed with `r` or `R` const RAW_STRING = Self::RAW_STRING_LOWERCASE.bits() | Self::RAW_STRING_UPPERCASE.bits(); } } +impl get_size2::GetSize for TokenFlags {} + impl StringFlags for TokenFlags { fn quote_style(self) -> Quote { if self.intersects(TokenFlags::DOUBLE_QUOTES) { @@ -758,6 +782,14 @@ impl StringFlags for TokenFlags { } else { AnyStringPrefix::Format(FStringPrefix::Regular) } + } else if self.intersects(TokenFlags::T_STRING) { + if self.intersects(TokenFlags::RAW_STRING_LOWERCASE) { + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }) + } else if self.intersects(TokenFlags::RAW_STRING_UPPERCASE) { + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }) + } else { + AnyStringPrefix::Template(TStringPrefix::Regular) + } } else if self.intersects(TokenFlags::BYTE_STRING) { if self.intersects(TokenFlags::RAW_STRING_LOWERCASE) { AnyStringPrefix::Bytes(ByteStringPrefix::Raw { uppercase_r: false }) @@ -784,9 +816,19 @@ impl TokenFlags { self.intersects(TokenFlags::F_STRING) } - /// Returns `true` if the token is a triple-quoted f-string. - pub(crate) fn is_triple_quoted_fstring(self) -> bool { - self.contains(TokenFlags::F_STRING | TokenFlags::TRIPLE_QUOTED_STRING) + /// Returns `true` if the token is a t-string. + pub(crate) const fn is_t_string(self) -> bool { + self.intersects(TokenFlags::T_STRING) + } + + /// Returns `true` if the token is a t-string. + pub(crate) const fn is_interpolated_string(self) -> bool { + self.intersects(TokenFlags::T_STRING.union(TokenFlags::F_STRING)) + } + + /// Returns `true` if the token is a triple-quoted t-string. + pub(crate) fn is_triple_quoted_interpolated_string(self) -> bool { + self.intersects(TokenFlags::TRIPLE_QUOTED_STRING) && self.is_interpolated_string() } /// Returns `true` if the token is a raw string. @@ -819,7 +861,7 @@ pub(crate) enum TokenValue { String(Box), /// Token value that includes the portion of text inside the f-string that's not /// part of the expression part and isn't an opening or closing brace. - FStringMiddle(Box), + InterpolatedStringMiddle(Box), /// Token value for IPython escape commands. These are recognized by the lexer /// only when the mode is [`Mode::Ipython`]. IpyEscapeCommand { diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index 8b379af4c26f7..26088c7a1209d 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -1,9 +1,9 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; +use crate::Mode; use crate::error::LexicalError; use crate::lexer::{Lexer, LexerCheckpoint}; use crate::token::{Token, TokenFlags, TokenKind, TokenValue}; -use crate::Mode; /// Token source for the parser that skips over any trivia tokens. #[derive(Debug)] @@ -210,7 +210,7 @@ pub(crate) struct TokenSourceCheckpoint { /// of `contents`. /// /// See [#9546](https://github.com/astral-sh/ruff/pull/9546) for a more detailed explanation. -#[allow(dead_code)] +#[expect(dead_code)] fn allocate_tokens_vec(contents: &str) -> Vec { let lower_bound = contents.len().saturating_mul(15) / 100; Vec::with_capacity(lower_bound) diff --git a/crates/ruff_python_parser/src/typing.rs b/crates/ruff_python_parser/src/typing.rs index 613ac59695eb4..e0c690c3dd70d 100644 --- a/crates/ruff_python_parser/src/typing.rs +++ b/crates/ruff_python_parser/src/typing.rs @@ -4,7 +4,7 @@ use ruff_python_ast::relocate::relocate_expr; use ruff_python_ast::{Expr, ExprStringLiteral, ModExpression, StringLiteral}; use ruff_text_size::Ranged; -use crate::{parse_expression, parse_string_annotation, ParseError, Parsed}; +use crate::{ParseError, Parsed, parse_expression, parse_string_annotation}; type AnnotationParseResult = Result; diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index a7b0260010b99..7aa7a6f2e2fc6 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -5,13 +5,13 @@ use std::fs; use std::path::Path; use ruff_annotate_snippets::{Level, Renderer, Snippet}; -use ruff_python_ast::visitor::source_order::{walk_module, SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; use ruff_python_ast::{self as ast, AnyNodeRef, Mod, PythonVersion}; use ruff_python_parser::semantic_errors::{ SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, }; -use ruff_python_parser::{parse_unchecked, Mode, ParseErrorType, ParseOptions, Token}; +use ruff_python_parser::{Mode, ParseErrorType, ParseOptions, Token, parse_unchecked}; use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -40,7 +40,7 @@ fn inline_err() { fn test_valid_syntax(input_path: &Path) { let source = fs::read_to_string(input_path).expect("Expected test file to exist"); let options = extract_options(&source).unwrap_or_else(|| { - ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest()) + ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest_preview()) }); let parsed = parse_unchecked(&source, options.clone()); @@ -133,7 +133,7 @@ fn test_valid_syntax(input_path: &Path) { fn test_invalid_syntax(input_path: &Path) { let source = fs::read_to_string(input_path).expect("Expected test file to exist"); let options = extract_options(&source).unwrap_or_else(|| { - ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest()) + ParseOptions::from(Mode::Module).with_target_version(PythonVersion::PY314) }); let parsed = parse_unchecked(&source, options.clone()); @@ -274,7 +274,7 @@ fn extract_options(source: &str) -> Option { // Use it for quickly debugging a parser issue. #[test] #[ignore] -#[allow(clippy::print_stdout)] +#[expect(clippy::print_stdout)] fn parser_quick_test() { let source = "\ f'{' @@ -433,7 +433,9 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { ); if let Some(previous) = self.previous { - assert_ne!(previous.range().ordering(node.range()), Ordering::Greater, + assert_ne!( + previous.range().ordering(node.range()), + Ordering::Greater, "{path}: The ranges of the nodes are not strictly increasing when traversing the AST in pre-order.\nPrevious node: {previous:#?}\n\nCurrent node: {node:#?}\n\nRoot: {root:#?}", path = self.test_path.display(), root = self.parents.first() @@ -441,7 +443,8 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { } if let Some(parent) = self.parents.last() { - assert!(parent.range().contains_range(node.range()), + assert!( + parent.range().contains_range(node.range()), "{path}: The range of the parent node does not fully enclose the range of the child node.\nParent node: {parent:#?}\n\nChild node: {node:#?}\n\nRoot: {root:#?}", path = self.test_path.display(), root = self.parents.first() @@ -504,10 +507,6 @@ impl<'a> SemanticSyntaxCheckerVisitor<'a> { } impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { - fn seen_docstring_boundary(&self) -> bool { - false - } - fn future_annotations_or_stub(&self) -> bool { false } @@ -542,7 +541,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { } fn in_module_scope(&self) -> bool { - true + self.scopes.len() == 1 } fn in_function_scope(&self) -> bool { @@ -557,6 +556,10 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { true } + fn in_yield_allowed_context(&self) -> bool { + true + } + fn in_generator_scope(&self) -> bool { true } @@ -618,7 +621,7 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { self.visit_comprehension(comprehension); } self.scopes.push(Scope::Comprehension { - is_async: generators.iter().any(|gen| gen.is_async), + is_async: generators.iter().any(|generator| generator.is_async), }); self.visit_expr(elt); self.scopes.pop().unwrap(); @@ -633,7 +636,7 @@ impl Visitor<'_> for SemanticSyntaxCheckerVisitor<'_> { self.visit_comprehension(comprehension); } self.scopes.push(Scope::Comprehension { - is_async: generators.iter().any(|gen| gen.is_async), + is_async: generators.iter().any(|generator| generator.is_async), }); self.visit_expr(key); self.visit_expr(value); diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap index 76f99c4e88020..172e4daa89c02 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_annotation.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..63, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..11, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -22,9 +24,11 @@ Module( ), annotation: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3..7, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..7, id: Name("int"), ctx: Load, @@ -36,6 +40,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 1, @@ -48,9 +53,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 12..26, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..13, id: Name("x"), ctx: Store, @@ -58,10 +65,12 @@ Module( ), annotation: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 15..22, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("a"), ctx: Load, @@ -73,6 +82,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 1, @@ -85,9 +95,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 27..46, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Store, @@ -95,9 +107,11 @@ Module( ), annotation: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 30..42, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("b"), ctx: Load, @@ -108,6 +122,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 45..46, value: Int( 1, @@ -120,9 +135,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 47..51, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -130,6 +147,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 50..51, id: Name("y"), ctx: Load, @@ -141,10 +159,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 55..62, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..58, id: Name("int"), ctx: Store, @@ -153,6 +173,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 1, @@ -199,3 +220,23 @@ Module( 4 | x: y := int = 1 | ^^ Syntax Error: Expected a statement | + + +## Semantic Syntax Errors + + | +1 | x: *int = 1 +2 | x: yield a = 1 + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +3 | x: yield from b = 1 +4 | x: y := int = 1 + | + + + | +1 | x: *int = 1 +2 | x: yield a = 1 +3 | x: yield from b = 1 + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +4 | x: y := int = 1 + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap index 9bd4841679c93..4a80fdc2f52b3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..170, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..18, target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..5, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..5, + node_index: AtomicNodeIndex(..), value: "abc", flags: StringLiteralFlags { quote_style: Double, @@ -33,6 +36,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..10, id: Name("str"), ctx: Load, @@ -41,11 +45,13 @@ Module( value: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 13..18, value: StringLiteralValue { inner: Single( StringLiteral { range: 13..18, + node_index: AtomicNodeIndex(..), value: "def", flags: StringLiteralFlags { quote_style: Double, @@ -63,12 +69,15 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 19..37, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 19..25, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..23, id: Name("call"), ctx: Load, @@ -76,6 +85,7 @@ Module( ), arguments: Arguments { range: 23..25, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -83,6 +93,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..30, id: Name("str"), ctx: Load, @@ -91,11 +102,13 @@ Module( value: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 33..37, value: StringLiteralValue { inner: Single( StringLiteral { range: 33..37, + node_index: AtomicNodeIndex(..), value: "no", flags: StringLiteralFlags { quote_style: Double, @@ -113,12 +126,15 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 38..52, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 38..40, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("x"), ctx: Store, @@ -129,6 +145,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..45, id: Name("int"), ctx: Load, @@ -137,10 +154,12 @@ Module( value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 48..52, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 48..49, value: Int( 1, @@ -149,6 +168,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 2, @@ -166,13 +186,16 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 72..83, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 72..74, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("x"), ctx: Store, @@ -185,6 +208,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..79, id: Name("int"), ctx: Load, @@ -193,6 +217,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 1, @@ -205,13 +230,16 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 84..100, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 84..88, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..85, id: Name("x"), ctx: Store, @@ -219,6 +247,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..88, id: Name("y"), ctx: Store, @@ -231,6 +260,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..93, id: Name("int"), ctx: Load, @@ -239,10 +269,12 @@ Module( value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 96..100, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 96..97, value: Int( 1, @@ -251,6 +283,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 99..100, value: Int( 2, @@ -268,13 +301,16 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 101..119, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 101..107, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..103, id: Name("x"), ctx: Store, @@ -282,6 +318,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..106, id: Name("y"), ctx: Store, @@ -294,6 +331,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..112, id: Name("int"), ctx: Load, @@ -302,10 +340,12 @@ Module( value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 115..119, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 1, @@ -314,6 +354,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..119, value: Int( 2, @@ -331,13 +372,16 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 138..150, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 138..141, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("x"), ctx: Store, @@ -349,6 +393,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..146, id: Name("int"), ctx: Load, @@ -357,6 +402,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 149..150, value: Int( 1, @@ -369,13 +415,16 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 151..169, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 151..157, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 152..153, id: Name("x"), ctx: Store, @@ -383,6 +432,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 155..156, id: Name("y"), ctx: Store, @@ -394,6 +444,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 159..162, id: Name("int"), ctx: Load, @@ -402,10 +453,12 @@ Module( value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 165..169, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 165..166, value: Int( 1, @@ -414,6 +467,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 168..169, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap index f37b524ee3717..1d6945367089e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_value.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..65, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..17, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -22,6 +24,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("Any"), ctx: Load, @@ -30,14 +33,17 @@ Module( value: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 9..17, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 10..17, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("a"), ctx: Load, @@ -45,6 +51,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("b"), ctx: Load, @@ -62,9 +69,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 18..28, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("x"), ctx: Store, @@ -72,6 +81,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..24, id: Name("Any"), ctx: Load, @@ -80,6 +90,7 @@ Module( value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Load, @@ -91,9 +102,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..33, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 1, @@ -104,9 +117,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 34..64, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("x"), ctx: Store, @@ -114,6 +129,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..41, id: Name("list"), ctx: Load, @@ -122,10 +138,12 @@ Module( value: Some( List( ExprList { + node_index: AtomicNodeIndex(..), range: 44..64, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..46, id: Name("x"), ctx: Load, @@ -133,12 +151,15 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 48..54, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 49..54, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("a"), ctx: Load, @@ -147,6 +168,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("b"), ctx: Load, @@ -159,14 +181,17 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 56..63, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 57..63, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("a"), ctx: Load, @@ -174,6 +199,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap index dfd20d1d11e07..b1a670039436f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_missing_rhs.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..9, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..8, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -22,6 +24,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap index 304d630374a0a..957104e6d291b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_type_alias_annotation.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..37, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..7, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Store, @@ -22,6 +24,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..7, id: Name("type"), ctx: Load, @@ -33,10 +36,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 8..15, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("X"), ctx: Store, @@ -45,6 +50,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..15, id: Name("int"), ctx: Load, @@ -54,13 +60,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..28, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 16..28, parameters: None, body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..28, id: Name("type"), ctx: Load, @@ -72,10 +81,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 29..36, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("X"), ctx: Store, @@ -84,6 +95,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..36, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap index 47aa86d8a035d..745c8c8c402f8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/args_unparenthesized_generator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..65, + node_index: AtomicNodeIndex(..), + range: 0..92, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..28, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..28, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..3, id: Name("sum"), ctx: Load, @@ -25,12 +28,15 @@ Module( ), arguments: Arguments { range: 3..28, + node_index: AtomicNodeIndex(..), args: [ Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 4..24, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, @@ -39,8 +45,10 @@ Module( generators: [ Comprehension { range: 6..24, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Store, @@ -48,9 +56,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 15..24, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..20, id: Name("range"), ctx: Load, @@ -58,9 +68,11 @@ Module( ), arguments: Arguments { range: 20..24, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..23, value: Int( 10, @@ -81,6 +93,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 26..27, value: Int( 5, @@ -96,12 +109,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..64, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 29..64, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..34, id: Name("total"), ctx: Load, @@ -109,9 +125,11 @@ Module( ), arguments: Arguments { range: 34..64, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 35..36, value: Int( 1, @@ -120,6 +138,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 38..39, value: Int( 2, @@ -128,9 +147,11 @@ Module( ), Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 41..60, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Load, @@ -139,8 +160,10 @@ Module( generators: [ Comprehension { range: 43..60, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -148,9 +171,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 52..60, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..57, id: Name("range"), ctx: Load, @@ -158,9 +183,11 @@ Module( ), arguments: Arguments { range: 57..60, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 58..59, value: Int( 5, @@ -181,6 +208,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 62..63, value: Int( 6, @@ -194,6 +222,94 @@ Module( ), }, ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 65..91, + value: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 65..91, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 65..68, + id: Name("sum"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 68..91, + node_index: AtomicNodeIndex(..), + args: [ + Generator( + ExprGenerator { + node_index: AtomicNodeIndex(..), + range: 69..89, + elt: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 69..70, + id: Name("x"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 71..89, + node_index: AtomicNodeIndex(..), + target: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 75..76, + id: Name("x"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 80..89, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 80..85, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 85..89, + node_index: AtomicNodeIndex(..), + args: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 86..88, + value: Int( + 10, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + parenthesized: false, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ), ], }, ) @@ -204,6 +320,7 @@ Module( 1 | sum(x for x in range(10), 5) | ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here 2 | total(1, 2, x for x in range(5), 6) +3 | sum(x for x in range(10),) | @@ -211,4 +328,13 @@ Module( 1 | sum(x for x in range(10), 5) 2 | total(1, 2, x for x in range(5), 6) | ^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here +3 | sum(x for x in range(10),) + | + + + | +1 | sum(x for x in range(10), 5) +2 | total(1, 2, x for x in range(5), 6) +3 | sum(x for x in range(10),) + | ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap index 0de6bbd3f82f9..e4f9844f739ee 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assert_empty_msg.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 0..9, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap index 24dfb690d2c7d..8722a03c2e652 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assert_empty_test.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..7, body: [ Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 0..6, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..6, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap index 557cbe6bf3f37..bfef69718472f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assert_invalid_msg_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..83, body: [ Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 0..16, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 7..12, value: false, }, @@ -22,9 +24,11 @@ Module( msg: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 14..16, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("x"), ctx: Load, @@ -38,9 +42,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 17..30, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 24..29, value: false, }, @@ -50,9 +56,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 31..39, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("x"), ctx: Load, @@ -63,9 +71,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 40..61, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 47..52, value: false, }, @@ -73,10 +83,12 @@ Module( msg: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 54..61, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("x"), ctx: Load, @@ -90,9 +102,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 62..77, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 69..74, value: false, }, @@ -100,6 +114,7 @@ Module( msg: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..77, id: Name("x"), ctx: Load, @@ -110,9 +125,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..82, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap index afe4aca2ba14c..1f87e7e7b6857 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assert_invalid_test_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..55, body: [ Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 0..9, test: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 7..9, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("x"), ctx: Load, @@ -31,9 +34,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 10..23, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..23, id: Name("assert"), ctx: Load, @@ -44,9 +49,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..25, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Load, @@ -56,13 +63,16 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 26..40, test: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 33..40, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("x"), ctx: Load, @@ -76,9 +86,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 41..49, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..49, id: Name("x"), ctx: Load, @@ -89,9 +101,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..54, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 53..54, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap index 8f47b9c0422d7..e317e4b49c9e5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..58, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..5, targets: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 0..1, value: Int( 1, @@ -25,6 +27,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 1, @@ -35,10 +38,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 6..15, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Store, @@ -46,6 +51,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 1, @@ -55,6 +61,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 14..15, value: Int( 2, @@ -65,10 +72,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 16..33, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("x"), ctx: Store, @@ -76,6 +85,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 20..21, value: Int( 1, @@ -84,6 +94,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("y"), ctx: Store, @@ -91,6 +102,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..29, value: Int( 2, @@ -100,6 +112,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("z"), ctx: Load, @@ -109,19 +122,23 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 34..57, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 34..44, elts: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 35..38, value: StringLiteralValue { inner: Single( StringLiteral { range: 35..38, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -135,11 +152,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 40..43, value: StringLiteralValue { inner: Single( StringLiteral { range: 40..43, + node_index: AtomicNodeIndex(..), value: "b", flags: StringLiteralFlags { quote_style: Double, @@ -158,15 +177,18 @@ Module( ], value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 47..57, elts: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 48..51, value: StringLiteralValue { inner: Single( StringLiteral { range: 48..51, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -180,11 +202,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 53..56, value: StringLiteralValue { inner: Single( StringLiteral { range: 53..56, + node_index: AtomicNodeIndex(..), value: "b", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap index 306a96376995e..65ac6baf43b08 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap @@ -1,185 +1,272 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..72, + node_index: AtomicNodeIndex(..), + range: 0..90, body: [ Assign( StmtAssign { - range: 0..12, + node_index: AtomicNodeIndex(..), + range: 0..15, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 4..12, - value: BoolOp( - ExprBoolOp { - range: 5..12, - op: And, - values: [ - Name( - ExprName { - range: 5..6, - id: Name("a"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 11..12, - id: Name("b"), - ctx: Load, + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 4..15, + elts: [ + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 5..13, + value: BoolOp( + ExprBoolOp { + node_index: AtomicNodeIndex(..), + range: 6..13, + op: And, + values: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 6..7, + id: Name("a"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 12..13, + id: Name("b"), + ctx: Load, + }, + ), + ], }, ), - ], - }, - ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 13..25, + node_index: AtomicNodeIndex(..), + range: 16..34, targets: [ Name( ExprName { - range: 13..14, + node_index: AtomicNodeIndex(..), + range: 16..17, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 17..25, - value: Yield( - ExprYield { - range: 18..25, - value: Some( - Name( - ExprName { - range: 24..25, - id: Name("x"), - ctx: Load, + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 20..34, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 21..23, + value: Int( + 42, + ), + }, + ), + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 25..33, + value: Yield( + ExprYield { + node_index: AtomicNodeIndex(..), + range: 26..33, + value: Some( + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 32..33, + id: Name("x"), + ctx: Load, + }, + ), + ), }, ), - ), - }, - ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 26..43, + node_index: AtomicNodeIndex(..), + range: 35..58, targets: [ Name( ExprName { - range: 26..27, + node_index: AtomicNodeIndex(..), + range: 35..36, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 30..43, - value: YieldFrom( - ExprYieldFrom { - range: 31..43, - value: Name( - ExprName { - range: 42..43, - id: Name("x"), - ctx: Load, - }, - ), - }, - ), + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 39..58, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 40..42, + value: Int( + 42, + ), + }, + ), + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 44..57, + value: YieldFrom( + ExprYieldFrom { + node_index: AtomicNodeIndex(..), + range: 45..57, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 56..57, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 44..60, + node_index: AtomicNodeIndex(..), + range: 59..78, targets: [ Name( ExprName { - range: 44..45, + node_index: AtomicNodeIndex(..), + range: 59..60, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 48..60, - value: Lambda( - ExprLambda { - range: 49..60, - parameters: Some( - Parameters { - range: 56..57, - posonlyargs: [], - args: [ - ParameterWithDefault { - range: 56..57, - parameter: Parameter { - range: 56..57, - name: Identifier { - id: Name("x"), - range: 56..57, - }, - annotation: None, + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 63..78, + elts: [ + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 64..76, + value: Lambda( + ExprLambda { + node_index: AtomicNodeIndex(..), + range: 65..76, + parameters: Some( + Parameters { + range: 72..73, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 72..73, + node_index: AtomicNodeIndex(..), + parameter: Parameter { + range: 72..73, + node_index: AtomicNodeIndex(..), + name: Identifier { + id: Name("x"), + range: 72..73, + node_index: AtomicNodeIndex(..), + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, }, - default: None, - }, - ], - vararg: None, - kwonlyargs: [], - kwarg: None, - }, - ), - body: Name( - ExprName { - range: 59..60, - id: Name("x"), - ctx: Load, - }, - ), - }, - ), + ), + body: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 75..76, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 61..66, + node_index: AtomicNodeIndex(..), + range: 79..84, targets: [ Name( ExprName { - range: 61..62, + node_index: AtomicNodeIndex(..), + range: 79..80, id: Name("x"), ctx: Store, }, @@ -187,7 +274,8 @@ Module( ], value: Name( ExprName { - range: 65..66, + node_index: AtomicNodeIndex(..), + range: 83..84, id: Name("x"), ctx: Load, }, @@ -196,10 +284,12 @@ Module( ), Expr( StmtExpr { - range: 70..71, + node_index: AtomicNodeIndex(..), + range: 88..89, value: NumberLiteral( ExprNumberLiteral { - range: 70..71, + node_index: AtomicNodeIndex(..), + range: 88..89, value: Int( 1, ), @@ -214,44 +304,44 @@ Module( ## Errors | -1 | x = *a and b - | ^^^^^^^ Syntax Error: Boolean expression cannot be used here -2 | x = *yield x -3 | x = *yield from x +1 | x = (*a and b,) + | ^^^^^^^ Syntax Error: Boolean expression cannot be used here +2 | x = (42, *yield x) +3 | x = (42, *yield from x) | | -1 | x = *a and b -2 | x = *yield x - | ^^^^^^^ Syntax Error: Yield expression cannot be used here -3 | x = *yield from x -4 | x = *lambda x: x +1 | x = (*a and b,) +2 | x = (42, *yield x) + | ^^^^^^^ Syntax Error: Yield expression cannot be used here +3 | x = (42, *yield from x) +4 | x = (*lambda x: x,) | | -1 | x = *a and b -2 | x = *yield x -3 | x = *yield from x - | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here -4 | x = *lambda x: x +1 | x = (*a and b,) +2 | x = (42, *yield x) +3 | x = (42, *yield from x) + | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here +4 | x = (*lambda x: x,) 5 | x = x := 1 | | -2 | x = *yield x -3 | x = *yield from x -4 | x = *lambda x: x - | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here +2 | x = (42, *yield x) +3 | x = (42, *yield from x) +4 | x = (*lambda x: x,) + | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here 5 | x = x := 1 | | -3 | x = *yield from x -4 | x = *lambda x: x +3 | x = (42, *yield from x) +4 | x = (*lambda x: x,) 5 | x = x := 1 | ^^ Syntax Error: Expected a statement | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap index 0ed0320576257..0f58eff527dfa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_keyword_target.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_keyword_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..42, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..12, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Store, @@ -23,6 +25,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..8, id: Name("pass"), ctx: Store, @@ -31,6 +34,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("c"), ctx: Load, @@ -40,12 +44,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..18, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 13..18, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("a"), ctx: Load, @@ -54,6 +61,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("b"), ctx: Load, @@ -65,10 +73,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 19..35, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("a"), ctx: Store, @@ -76,6 +86,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("b"), ctx: Store, @@ -83,6 +94,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..31, id: Name("pass"), ctx: Store, @@ -91,6 +103,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("c"), ctx: Load, @@ -100,12 +113,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..41, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 36..41, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("a"), ctx: Load, @@ -114,6 +130,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_missing_rhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_missing_rhs.py.snap index 35e1921645262..6ef39d8045634 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_missing_rhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_missing_rhs.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_missing_rhs.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..38, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..3, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -24,6 +26,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -33,12 +36,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4..9, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4..9, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 1, @@ -48,6 +54,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 1, @@ -60,10 +67,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 10..17, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Store, @@ -71,6 +80,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("y"), ctx: Store, @@ -79,6 +89,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..17, id: Name(""), ctx: Invalid, @@ -88,12 +99,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..23, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 18..23, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 2, @@ -103,6 +117,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 22..23, value: Int( 2, @@ -115,10 +130,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 24..31, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Store, @@ -126,6 +143,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..27, id: Name(""), ctx: Store, @@ -134,6 +152,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 30..31, id: Name("y"), ctx: Load, @@ -143,12 +162,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..37, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 32..37, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 3, @@ -158,6 +180,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 36..37, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap new file mode 100644 index 0000000000000..5af1853e86661 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap @@ -0,0 +1,220 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..45, + body: [ + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 0..9, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 0..1, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 4..9, + value: List( + ExprList { + node_index: AtomicNodeIndex(..), + range: 5..9, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 6..8, + value: Int( + 42, + ), + }, + ), + ], + ctx: Load, + }, + ), + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 10..19, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 10..11, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 14..19, + value: Set( + ExprSet { + node_index: AtomicNodeIndex(..), + range: 15..19, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 16..18, + value: Int( + 42, + ), + }, + ), + ], + }, + ), + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 20..31, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 20..21, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 24..31, + value: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 25..31, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 25..29, + id: Name("list"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 29..31, + node_index: AtomicNodeIndex(..), + args: [], + keywords: [], + }, + }, + ), + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 32..44, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 32..33, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 36..44, + value: BinOp( + ExprBinOp { + node_index: AtomicNodeIndex(..), + range: 38..43, + left: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 38..39, + id: Name("p"), + ctx: Load, + }, + ), + op: Add, + right: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 42..43, + id: Name("q"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | _ = *[42] + | ^^^^^ Syntax Error: Starred expression cannot be used here +2 | _ = *{42} +3 | _ = *list() + | + + + | +1 | _ = *[42] +2 | _ = *{42} + | ^^^^^ Syntax Error: Starred expression cannot be used here +3 | _ = *list() +4 | _ = *(p + q) + | + + + | +1 | _ = *[42] +2 | _ = *{42} +3 | _ = *list() + | ^^^^^^^ Syntax Error: Starred expression cannot be used here +4 | _ = *(p + q) + | + + + | +2 | _ = *{42} +3 | _ = *list() +4 | _ = *(p + q) + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap index 678e667d880d7..7ebaaad4b78a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@async_unexpected_token.py.snap @@ -1,31 +1,35 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/async_unexpected_token.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..116, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 6..20, decorator_list: [], name: Identifier { id: Name("Foo"), range: 12..15, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, }, ), @@ -36,9 +40,11 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 27..42, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..37, id: Name("test"), ctx: Load, @@ -47,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 39..42, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 39..42, }, ), @@ -61,10 +69,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 49..54, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("x"), ctx: Store, @@ -73,6 +83,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 53..54, value: Int( 1, @@ -83,16 +94,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 61..81, is_async: true, decorator_list: [], name: Identifier { id: Name("foo"), range: 71..74, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 74..76, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -103,9 +119,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 78..81, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 78..81, }, ), @@ -116,9 +134,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 88..115, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 94..98, id: Name("test"), ctx: Load, @@ -127,9 +147,11 @@ Module( cases: [ MatchCase { range: 104..115, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 109..110, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -138,9 +160,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 112..115, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 112..115, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap index 17b749427a549..78da56cda60ce 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/aug_assign_stmt_invalid_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..59, body: [ AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 0..6, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 0..1, value: Int( 1, @@ -24,6 +26,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -34,14 +37,17 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 7..17, target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 7..10, value: StringLiteralValue { inner: Single( StringLiteral { range: 7..10, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -56,11 +62,13 @@ Module( op: Add, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 14..17, value: StringLiteralValue { inner: Single( StringLiteral { range: 14..17, + node_index: AtomicNodeIndex(..), value: "b", flags: StringLiteralFlags { quote_style: Double, @@ -76,12 +84,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 18..25, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 18..20, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("x"), ctx: Store, @@ -93,6 +104,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 24..25, value: Int( 1, @@ -103,14 +115,17 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 26..30, }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..35, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 34..35, value: Int( 1, @@ -121,9 +136,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 36..45, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("x"), ctx: Store, @@ -132,6 +149,7 @@ Module( op: Add, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..45, id: Name("pass"), ctx: Load, @@ -141,12 +159,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 46..58, target: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 47..52, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Load, @@ -155,6 +176,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("y"), ctx: Load, @@ -165,6 +187,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap index cd6e0380f9566..cfc583f1b86b6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/aug_assign_stmt_invalid_value.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..77, body: [ AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 0..13, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -23,14 +25,17 @@ Module( op: Add, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 5..13, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 6..13, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("a"), ctx: Load, @@ -38,6 +43,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..13, id: Name("b"), ctx: Load, @@ -53,9 +59,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 14..27, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("x"), ctx: Store, @@ -64,13 +72,16 @@ Module( op: Add, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 19..27, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 20..27, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("x"), ctx: Load, @@ -86,9 +97,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 28..46, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("x"), ctx: Store, @@ -97,12 +110,15 @@ Module( op: Add, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 33..46, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 34..46, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..46, id: Name("x"), ctx: Load, @@ -117,9 +133,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 47..64, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -128,22 +146,30 @@ Module( op: Add, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 52..64, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 53..64, parameters: Some( Parameters { range: 60..61, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 60..61, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 60..61, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 60..61, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -157,6 +183,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..64, id: Name("x"), ctx: Load, @@ -171,9 +198,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 65..71, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("x"), ctx: Store, @@ -182,6 +211,7 @@ Module( op: Add, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("y"), ctx: Load, @@ -191,9 +221,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 75..76, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_missing_rhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_missing_rhs.py.snap index d2ba825987081..50e27a1c5e9a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_missing_rhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_missing_rhs.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/aug_assign_stmt_missing_rhs.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..27, body: [ AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 0..4, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -23,6 +25,7 @@ Module( op: Add, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..4, id: Name(""), ctx: Invalid, @@ -32,12 +35,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..10, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -47,6 +53,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 1, @@ -59,9 +66,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 11..17, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("x"), ctx: Store, @@ -70,6 +79,7 @@ Module( op: Add, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("y"), ctx: Load, @@ -79,12 +89,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..26, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 21..26, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..22, value: Int( 2, @@ -94,6 +107,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap index 48c0bba70c1fe..0e0305e26b406 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/case_expect_indented_block.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..43, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..42, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -23,11 +25,14 @@ Module( cases: [ MatchCase { range: 19..26, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 24..25, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 24..25, value: Int( 1, @@ -41,11 +46,14 @@ Module( }, MatchCase { range: 31..42, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 36..37, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 36..37, value: Int( 2, @@ -58,9 +66,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 39..42, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 39..42, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap index 29be135fec38d..48a479382f0af 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap @@ -1,22 +1,24 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/class_def_empty_body.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..31, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..10, decorator_list: [], name: Identifier { id: Name("Foo"), range: 6..9, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, @@ -25,16 +27,19 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 11..23, decorator_list: [], name: Identifier { id: Name("Foo"), range: 17..20, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 20..22, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -44,10 +49,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 24..30, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Store, @@ -56,6 +63,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..30, value: Int( 42, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap index 05c4ed0abcc3b..6d68771f17f6f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap @@ -1,31 +1,35 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/class_def_missing_name.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..11, decorator_list: [], name: Identifier { id: Name(""), range: 5..5, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..11, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 8..11, }, ), @@ -36,16 +40,19 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 12..25, decorator_list: [], name: Identifier { id: Name(""), range: 17..17, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 18..20, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -53,9 +60,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..25, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 22..25, }, ), @@ -66,28 +75,34 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 26..52, decorator_list: [], name: Identifier { id: Name(""), range: 31..31, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 32..47, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 33..46, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("metaclass"), range: 33..42, + node_index: AtomicNodeIndex(..), }, ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..46, id: Name("ABC"), ctx: Load, @@ -100,9 +115,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap index bcbd8a0224d82..17f5d5b106140 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unclosed_type_param_list.py.snap @@ -1,33 +1,38 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/class_def_unclosed_type_param_list.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..41, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..33, decorator_list: [], name: Identifier { id: Name("Foo"), range: 6..9, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 9..17, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 10..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T1"), range: 10..12, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -36,9 +41,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 14..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T2"), range: 15..17, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -49,9 +56,11 @@ Module( arguments: Some( Arguments { range: 17..23, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("a"), ctx: Load, @@ -59,6 +68,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("b"), ctx: Load, @@ -71,6 +81,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 29..33, }, ), @@ -79,10 +90,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 34..40, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("x"), ctx: Store, @@ -91,6 +104,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 38..40, value: Int( 10, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap index 60c7fd49741e2..9d91dbf072625 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap @@ -7,34 +7,42 @@ input_file: crates/ruff_python_parser/resources/inline/err/class_type_params_py3 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..113, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 44..95, decorator_list: [], name: Identifier { id: Name("Foo"), range: 50..53, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 53..90, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 54..69, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("S"), range: 54..55, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 57..69, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..61, id: Name("str"), ctx: Load, @@ -42,6 +50,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..68, id: Name("bytes"), ctx: Load, @@ -59,13 +68,16 @@ Module( TypeVar( TypeParamTypeVar { range: 71..79, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 71..72, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..79, id: Name("float"), ctx: Load, @@ -78,9 +90,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 81..84, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 82..84, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -88,9 +102,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 86..89, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 88..89, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -102,9 +118,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..95, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 92..95, }, ), @@ -115,15 +133,18 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 96..112, decorator_list: [], name: Identifier { id: Name("Foo"), range: 102..105, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 105..107, + node_index: AtomicNodeIndex(..), type_params: [], }, ), @@ -131,9 +152,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..112, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 109..112, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap index 44bc4b2b510a6..52dcef4c1806c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/clause_expect_indented_block.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..171, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 53..61, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 56..60, value: true, }, @@ -25,14 +27,17 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 62..66, }, ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 162..170, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 165..169, value: true, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap index 6ed8d3277491b..0cec1c5726cf5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/clause_expect_single_statement.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..23, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..8, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -25,9 +27,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 9..22, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 12..16, value: true, }, @@ -35,6 +39,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 18..22, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap index fee330ecc28b2..749a5fa1ae447 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/comma_separated_missing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..15, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..14, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..14, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..14, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 12..13, value: Int( 1, @@ -38,9 +43,11 @@ Module( keywords: [ Keyword { range: 5..8, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap index 39ffd3982ff4c..3afc0336c3391 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/comma_separated_missing_comma_between_elements.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..92, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..91, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 83..91, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 84..85, value: Int( 0, @@ -27,6 +30,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 87..88, value: Int( 1, @@ -35,6 +39,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap index 799432dc3f042..0e1f29a3c074e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/comma_separated_missing_element_between_commas.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..12, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 0..11, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1..2, value: Int( 0, @@ -27,6 +30,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 1, @@ -35,6 +39,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap index 59fb82a1e5c4d..02c89d0035bd8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/comma_separated_missing_first_element.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..9, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..9, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap index 8713ae5f80d9e..636c0bdbac66d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/comprehension_missing_for_after_async.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..6, id: Name("async"), ctx: Load, @@ -24,12 +26,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..27, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 8..27, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("x"), ctx: Load, @@ -38,8 +43,10 @@ Module( generators: [ Comprehension { range: 11..26, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Store, @@ -47,6 +54,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..26, id: Name("iter"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap index 251a2c6c28119..91477355a39ee 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap @@ -7,24 +7,29 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_class.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..82, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..20, decorator_list: [], name: Identifier { id: Name("__debug__"), range: 6..15, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, }, ), @@ -35,22 +40,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 35..58, decorator_list: [], name: Identifier { id: Name("C"), range: 41..42, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 42..53, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 43..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 43..52, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -63,9 +73,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 55..58, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 55..58, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap index 517c64d7fb58b..f31edee08ac34 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_function ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..125, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..20, is_async: false, decorator_list: [], name: Identifier { id: Name("__debug__"), range: 4..13, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 13..15, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, }, ), @@ -44,23 +52,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 38..61, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 42..43, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 43..54, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 44..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 44..53, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -71,6 +84,9 @@ Module( ), parameters: Parameters { range: 54..56, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -81,9 +97,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), @@ -94,25 +112,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 85..106, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 89..90, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 90..101, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 91..100, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 91..100, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 91..100, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -127,9 +153,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 103..106, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 103..106, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap index 9cbb40853132a..380240dd334de 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_import.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..100, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..16, names: [ Alias { range: 7..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 7..16, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -26,18 +30,22 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 17..42, names: [ Alias { range: 24..42, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("debug"), range: 24..29, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("__debug__"), range: 33..42, + node_index: AtomicNodeIndex(..), }, ), }, @@ -46,19 +54,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 43..66, module: Some( Identifier { id: Name("x"), range: 48..49, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 57..66, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 57..66, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -68,24 +80,29 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 67..99, module: Some( Identifier { id: Name("x"), range: 72..73, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 81..99, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("debug"), range: 81..86, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("__debug__"), range: 90..99, + node_index: AtomicNodeIndex(..), }, ), }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap index 2b23fee0c414a..acb6dfcc11b03 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_match.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..33, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..32, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -22,14 +25,17 @@ Module( cases: [ MatchCase { range: 13..32, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 18..27, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("__debug__"), range: 18..27, + node_index: AtomicNodeIndex(..), }, ), }, @@ -38,9 +44,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap index d965f5306a693..d638e10acc3e0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_try.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..44, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..43, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..8, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 5..8, }, ), @@ -28,9 +32,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 9..43, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..25, id: Name("Exception"), ctx: Load, @@ -41,14 +47,17 @@ Module( Identifier { id: Name("__debug__"), range: 29..38, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..43, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 40..43, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap index 2817caa8c4c1e..06bce5209a0cb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_type_ali ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..95, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..26, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..14, id: Name("__debug__"), ctx: Store, @@ -22,9 +25,11 @@ Module( type_params: None, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 17..26, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..21, id: Name("list"), ctx: Load, @@ -32,6 +37,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..25, id: Name("int"), ctx: Load, @@ -44,9 +50,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 67..94, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..77, id: Name("Debug"), ctx: Store, @@ -55,13 +63,16 @@ Module( type_params: Some( TypeParams { range: 77..88, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 78..87, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 78..87, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -72,6 +83,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..94, id: Name("str"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap index 51a3a7cc6446e..3d0560bf59c36 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/inline/err/debug_shadow_with.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..39, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 0..38, is_async: false, items: [ WithItem { range: 5..33, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5..20, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..9, id: Name("open"), ctx: Load, @@ -28,14 +33,17 @@ Module( ), arguments: Arguments { range: 9..20, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 10..19, value: StringLiteralValue { inner: Single( StringLiteral { range: 10..19, + node_index: AtomicNodeIndex(..), value: "foo.txt", flags: StringLiteralFlags { quote_style: Double, @@ -55,6 +63,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..33, id: Name("__debug__"), ctx: Store, @@ -66,9 +75,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..38, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 35..38, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap index e86f1ea1f1d86..9b42c0f68442d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_await_expression_py38.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_await_expre ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..96, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..95, is_async: true, decorator_list: [], name: Identifier { id: Name("foo"), range: 55..58, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 58..60, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,16 +37,20 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 66..95, is_async: false, decorator_list: [ Decorator { range: 66..76, + node_index: AtomicNodeIndex(..), expression: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 67..76, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..76, id: Name("bar"), ctx: Load, @@ -53,10 +63,14 @@ Module( name: Identifier { id: Name("baz"), range: 85..88, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 88..90, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -67,9 +81,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..95, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 92..95, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap index e42d100e1f14f..0262ed63aae0f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_dict_literal_py38.py.snap @@ -7,23 +7,28 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_dict_litera ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..68, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..67, is_async: false, decorator_list: [ Decorator { range: 45..52, + node_index: AtomicNodeIndex(..), expression: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 46..52, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 47..48, value: Int( 3, @@ -33,6 +38,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 50..51, value: Int( 3, @@ -48,10 +54,14 @@ Module( name: Identifier { id: Name("bar"), range: 57..60, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 60..62, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -62,9 +72,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 64..67, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 64..67, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap index d530df39de0cf..5d24f31dd4e87 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_expression_py38.py.snap @@ -7,26 +7,33 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_expression_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..89, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..88, is_async: false, decorator_list: [ Decorator { range: 45..72, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..72, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..64, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 46..56, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..53, id: Name("buttons"), ctx: Load, @@ -34,6 +41,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 0, @@ -46,6 +54,7 @@ Module( attr: Identifier { id: Name("clicked"), range: 57..64, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -53,6 +62,7 @@ Module( attr: Identifier { id: Name("connect"), range: 65..72, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -62,10 +72,14 @@ Module( name: Identifier { id: Name("spam"), range: 77..81, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 81..83, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -76,9 +90,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..88, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 85..88, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap index 8f2af04e27591..666aa7e2baf00 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_float_literal_py38.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_float_liter ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..66, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..65, is_async: false, decorator_list: [ Decorator { range: 45..50, + node_index: AtomicNodeIndex(..), expression: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 46..50, value: Float( 3.14, @@ -29,10 +33,14 @@ Module( name: Identifier { id: Name("bar"), range: 55..58, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 58..60, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -43,9 +51,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 62..65, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 62..65, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_invalid_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_invalid_expression.py.snap index b89a8c6132395..a55f5868c2007 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_invalid_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_invalid_expression.py.snap @@ -1,27 +1,31 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/decorator_invalid_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..56, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..55, is_async: false, decorator_list: [ Decorator { range: 0..3, + node_index: AtomicNodeIndex(..), expression: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1..3, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("x"), ctx: Load, @@ -33,11 +37,14 @@ Module( }, Decorator { range: 4..9, + node_index: AtomicNodeIndex(..), expression: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, @@ -49,11 +56,14 @@ Module( }, Decorator { range: 10..17, + node_index: AtomicNodeIndex(..), expression: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..15, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("x"), ctx: Load, @@ -65,12 +75,15 @@ Module( }, Decorator { range: 18..26, + node_index: AtomicNodeIndex(..), expression: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 19..26, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("x"), ctx: Load, @@ -82,11 +95,14 @@ Module( }, Decorator { range: 27..40, + node_index: AtomicNodeIndex(..), expression: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 28..40, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("x"), ctx: Load, @@ -99,10 +115,14 @@ Module( name: Identifier { id: Name("foo"), range: 45..48, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 48..50, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -113,9 +133,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 52..55, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 52..55, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap index 38f833d0402da..61a1b2b2202fc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/decorator_missing_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..51, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 5..15, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5..10, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..8, id: Name("foo"), ctx: Load, @@ -25,6 +28,7 @@ Module( ), arguments: Arguments { range: 8..10, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -32,6 +36,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 12..15, }, ), @@ -41,13 +46,16 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 16..32, is_async: false, decorator_list: [ Decorator { range: 16..17, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..17, id: Name(""), ctx: Invalid, @@ -58,10 +66,14 @@ Module( name: Identifier { id: Name("foo"), range: 22..25, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 25..27, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -72,9 +84,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), @@ -85,16 +99,20 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 33..50, is_async: false, decorator_list: [ Decorator { range: 33..35, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 34..35, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..34, id: Name(""), ctx: Invalid, @@ -103,6 +121,7 @@ Module( op: MatMult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..35, id: Name(""), ctx: Invalid, @@ -115,10 +134,14 @@ Module( name: Identifier { id: Name("foo"), range: 40..43, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 43..45, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -129,9 +152,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 47..50, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 47..50, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap index 98cda543d8d29..88a78f186a2f9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/decorator_missing_newline.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..60, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..17, is_async: false, decorator_list: [ Decorator { range: 0..2, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -29,10 +32,14 @@ Module( name: Identifier { id: Name("foo"), range: 7..10, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 10..12, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -43,9 +50,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..17, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 14..17, }, ), @@ -56,13 +65,16 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 18..41, is_async: true, decorator_list: [ Decorator { range: 18..20, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("x"), ctx: Load, @@ -73,10 +85,14 @@ Module( name: Identifier { id: Name("foo"), range: 31..34, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 34..36, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -87,9 +103,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 38..41, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 38..41, }, ), @@ -100,12 +118,15 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 42..59, decorator_list: [ Decorator { range: 42..44, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("x"), ctx: Load, @@ -116,15 +137,18 @@ Module( name: Identifier { id: Name("Foo"), range: 51..54, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 56..59, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 56..59, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap index fbbef25495456..0cbedb39195d6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_named_expression_py37.py.snap @@ -7,23 +7,29 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_named_expre ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..85, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..84, is_async: false, decorator_list: [ Decorator { range: 45..69, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 46..69, func: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 47..63, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -31,19 +37,26 @@ Module( ), value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 52..63, parameters: Some( Parameters { range: 59..60, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 59..60, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 59..60, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 59..60, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -57,6 +70,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Load, @@ -68,9 +82,11 @@ Module( ), arguments: Arguments { range: 64..69, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..68, id: Name("foo"), ctx: Load, @@ -86,10 +102,14 @@ Module( name: Identifier { id: Name("bar"), range: 74..77, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 77..79, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -100,9 +120,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..84, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap index 2374561e1678d..4172d61656280 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_non_toplevel_call_expression_py38.py.snap @@ -7,26 +7,33 @@ input_file: crates/ruff_python_parser/resources/inline/err/decorator_non_topleve ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..73, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..72, is_async: false, decorator_list: [ Decorator { range: 45..57, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 46..57, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..55, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 46..51, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..49, id: Name("foo"), ctx: Load, @@ -34,6 +41,7 @@ Module( ), arguments: Arguments { range: 49..51, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -42,12 +50,14 @@ Module( attr: Identifier { id: Name("bar"), range: 52..55, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 55..57, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -58,10 +68,14 @@ Module( name: Identifier { id: Name("baz"), range: 62..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -72,9 +86,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 69..72, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 69..72, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap index 8e3126c2e33c5..d76a5c7bff80b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/decorator_unexpected_token.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..34, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 5..22, is_async: true, items: [ WithItem { range: 16..17, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("x"), ctx: Load, @@ -30,9 +33,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -43,10 +48,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 28..33, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("x"), ctx: Store, @@ -55,6 +62,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap index 6909999bfed48..40a5517f37278 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/del_debug_py39.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 43..56, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..56, id: Name("__debug__"), ctx: Del, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap index b17df25feb8d7..2e3f2be0afb4e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/del_incomplete_target ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 0..9, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Del, @@ -22,9 +25,11 @@ Module( ), Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 7..9, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("y"), ctx: Load, @@ -33,6 +38,7 @@ Module( attr: Identifier { id: Name(""), range: 9..9, + node_index: AtomicNodeIndex(..), }, ctx: Del, }, @@ -42,9 +48,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..11, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("z"), ctx: Load, @@ -54,10 +62,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 12..24, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("x"), ctx: Del, @@ -65,9 +75,11 @@ Module( ), Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 19..23, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("y"), ctx: Load, @@ -75,10 +87,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 22..23, lower: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("z"), ctx: Load, @@ -88,6 +102,7 @@ Module( upper: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..23, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap index da814920189fd..412384af7c374 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap @@ -1,17 +1,18 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/del_stmt_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..4, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 0..3, targets: [], }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap index 281c71c151fe7..3614aa08aa440 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/dotted_name_multiple_dots.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..25, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..11, names: [ Alias { range: 7..11, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a..b"), range: 7..11, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -27,13 +30,16 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 12..20, names: [ Alias { range: 19..20, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 19..20, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -42,9 +48,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..23, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, }, ), @@ -52,9 +60,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..24, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap index f4d1b42821b52..950b30f63de6e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/duplicate_match_class ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..231, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..230, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -22,11 +25,14 @@ Module( cases: [ MatchCase { range: 13..38, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 18..33, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..23, id: Name("Class"), ctx: Load, @@ -34,19 +40,24 @@ Module( ), arguments: PatternArguments { range: 23..33, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 24..27, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 24..25, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 26..27, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 26..27, value: Int( 1, @@ -58,15 +69,19 @@ Module( }, PatternKeyword { range: 29..32, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 29..30, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 31..32, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 31..32, value: Int( 2, @@ -84,9 +99,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..38, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 35..38, }, ), @@ -96,15 +113,19 @@ Module( }, MatchCase { range: 43..70, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 48..65, + node_index: AtomicNodeIndex(..), patterns: [ MatchClass( PatternMatchClass { range: 49..64, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..54, id: Name("Class"), ctx: Load, @@ -112,19 +133,24 @@ Module( ), arguments: PatternArguments { range: 54..64, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 55..58, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 55..56, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 57..58, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 1, @@ -136,15 +162,19 @@ Module( }, PatternKeyword { range: 60..63, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 60..61, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 62..63, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 62..63, value: Int( 2, @@ -165,9 +195,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..70, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 67..70, }, ), @@ -177,17 +209,21 @@ Module( }, MatchCase { range: 75..113, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 80..108, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, value: StringLiteralValue { inner: Single( StringLiteral { range: 81..84, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -201,11 +237,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 89..92, value: StringLiteralValue { inner: Single( StringLiteral { range: 89..92, + node_index: AtomicNodeIndex(..), value: "y", flags: StringLiteralFlags { quote_style: Double, @@ -222,11 +260,13 @@ Module( MatchAs( PatternMatchAs { range: 86..87, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 86..87, + node_index: AtomicNodeIndex(..), }, ), }, @@ -234,8 +274,10 @@ Module( MatchClass( PatternMatchClass { range: 94..107, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 94..97, id: Name("Foo"), ctx: Load, @@ -243,19 +285,24 @@ Module( ), arguments: PatternArguments { range: 97..107, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 98..101, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 98..99, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 100..101, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 100..101, value: Int( 1, @@ -267,15 +314,19 @@ Module( }, PatternKeyword { range: 103..106, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 103..104, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 105..106, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 105..106, value: Int( 2, @@ -297,9 +348,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 110..113, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 110..113, }, ), @@ -309,13 +362,16 @@ Module( }, MatchCase { range: 118..162, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 123..157, + node_index: AtomicNodeIndex(..), patterns: [ MatchMapping( PatternMatchMapping { range: 124..126, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -324,14 +380,17 @@ Module( MatchMapping( PatternMatchMapping { range: 128..156, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 129..132, value: StringLiteralValue { inner: Single( StringLiteral { range: 129..132, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -345,11 +404,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 137..140, value: StringLiteralValue { inner: Single( StringLiteral { range: 137..140, + node_index: AtomicNodeIndex(..), value: "y", flags: StringLiteralFlags { quote_style: Double, @@ -366,11 +427,13 @@ Module( MatchAs( PatternMatchAs { range: 134..135, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 134..135, + node_index: AtomicNodeIndex(..), }, ), }, @@ -378,8 +441,10 @@ Module( MatchClass( PatternMatchClass { range: 142..155, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..145, id: Name("Foo"), ctx: Load, @@ -387,19 +452,24 @@ Module( ), arguments: PatternArguments { range: 145..155, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 146..149, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 146..147, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 148..149, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 148..149, value: Int( 1, @@ -411,15 +481,19 @@ Module( }, PatternKeyword { range: 151..154, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 151..152, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 153..154, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 153..154, value: Int( 2, @@ -444,9 +518,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 159..162, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 159..162, }, ), @@ -456,11 +532,14 @@ Module( }, MatchCase { range: 167..230, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 172..225, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 172..177, id: Name("Class"), ctx: Load, @@ -468,19 +547,24 @@ Module( ), arguments: PatternArguments { range: 177..225, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 178..181, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 178..179, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 180..181, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 180..181, value: Int( 1, @@ -492,21 +576,26 @@ Module( }, PatternKeyword { range: 183..201, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("d"), range: 183..184, + node_index: AtomicNodeIndex(..), }, pattern: MatchMapping( PatternMatchMapping { range: 185..201, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 186..189, value: StringLiteralValue { inner: Single( StringLiteral { range: 186..189, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -520,11 +609,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 194..197, value: StringLiteralValue { inner: Single( StringLiteral { range: 194..197, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -541,8 +632,10 @@ Module( MatchValue( PatternMatchValue { range: 191..192, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 191..192, value: Int( 1, @@ -554,8 +647,10 @@ Module( MatchValue( PatternMatchValue { range: 199..200, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 199..200, value: Int( 2, @@ -571,15 +666,19 @@ Module( }, PatternKeyword { range: 203..224, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("other"), range: 203..208, + node_index: AtomicNodeIndex(..), }, pattern: MatchClass( PatternMatchClass { range: 209..224, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 209..214, id: Name("Class"), ctx: Load, @@ -587,19 +686,24 @@ Module( ), arguments: PatternArguments { range: 214..224, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 215..218, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 215..216, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 217..218, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 217..218, value: Int( 1, @@ -611,15 +715,19 @@ Module( }, PatternKeyword { range: 220..223, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 220..221, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 222..223, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 222..223, value: Int( 2, @@ -642,9 +750,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 227..230, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 227..230, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap index 4d4dbc2eca418..4cfbb7011e954 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_key.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/duplicate_match_key.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..533, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..532, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -22,17 +25,21 @@ Module( cases: [ MatchCase { range: 13..39, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 18..34, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, value: StringLiteralValue { inner: Single( StringLiteral { range: 19..22, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -46,11 +53,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 27..30, value: StringLiteralValue { inner: Single( StringLiteral { range: 27..30, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -67,8 +76,10 @@ Module( MatchValue( PatternMatchValue { range: 24..25, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 24..25, value: Int( 1, @@ -80,8 +91,10 @@ Module( MatchValue( PatternMatchValue { range: 32..33, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 2, @@ -98,9 +111,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..39, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 36..39, }, ), @@ -110,17 +125,21 @@ Module( }, MatchCase { range: 44..72, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 49..67, + node_index: AtomicNodeIndex(..), keys: [ BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 50..54, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 50..54, + node_index: AtomicNodeIndex(..), value: [ 120, ], @@ -136,11 +155,13 @@ Module( ), BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 59..63, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 59..63, + node_index: AtomicNodeIndex(..), value: [ 120, ], @@ -159,8 +180,10 @@ Module( MatchValue( PatternMatchValue { range: 56..57, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 56..57, value: Int( 1, @@ -172,8 +195,10 @@ Module( MatchValue( PatternMatchValue { range: 65..66, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 2, @@ -190,9 +215,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 69..72, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 69..72, }, ), @@ -202,12 +229,15 @@ Module( }, MatchCase { range: 77..99, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 82..94, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 0, @@ -216,6 +246,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 0, @@ -227,8 +258,10 @@ Module( MatchValue( PatternMatchValue { range: 86..87, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 86..87, value: Int( 1, @@ -240,8 +273,10 @@ Module( MatchValue( PatternMatchValue { range: 92..93, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 92..93, value: Int( 2, @@ -258,9 +293,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), @@ -270,12 +307,15 @@ Module( }, MatchCase { range: 104..130, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 109..125, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 110..113, value: Float( 1.0, @@ -284,6 +324,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..121, value: Float( 1.0, @@ -295,8 +336,10 @@ Module( MatchValue( PatternMatchValue { range: 115..116, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 1, @@ -308,8 +351,10 @@ Module( MatchValue( PatternMatchValue { range: 123..124, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 123..124, value: Int( 2, @@ -326,9 +371,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 127..130, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 127..130, }, ), @@ -338,15 +385,19 @@ Module( }, MatchCase { range: 135..171, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 140..166, + node_index: AtomicNodeIndex(..), keys: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 141..149, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 141..144, value: Float( 1.0, @@ -356,6 +407,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 147..149, value: Complex { real: 0.0, @@ -367,9 +419,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 154..162, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 154..157, value: Float( 1.0, @@ -379,6 +433,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..162, value: Complex { real: 0.0, @@ -393,8 +448,10 @@ Module( MatchValue( PatternMatchValue { range: 151..152, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 151..152, value: Int( 1, @@ -406,8 +463,10 @@ Module( MatchValue( PatternMatchValue { range: 164..165, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 164..165, value: Int( 2, @@ -424,9 +483,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 168..171, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 168..171, }, ), @@ -436,18 +497,22 @@ Module( }, MatchCase { range: 176..204, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 181..199, + node_index: AtomicNodeIndex(..), keys: [ BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 182..186, value: true, }, ), BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 191..195, value: true, }, @@ -457,8 +522,10 @@ Module( MatchValue( PatternMatchValue { range: 188..189, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 188..189, value: Int( 1, @@ -470,8 +537,10 @@ Module( MatchValue( PatternMatchValue { range: 197..198, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 197..198, value: Int( 2, @@ -488,9 +557,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 201..204, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 201..204, }, ), @@ -500,17 +571,21 @@ Module( }, MatchCase { range: 209..237, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 214..232, + node_index: AtomicNodeIndex(..), keys: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 215..219, }, ), NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 224..228, }, ), @@ -519,8 +594,10 @@ Module( MatchValue( PatternMatchValue { range: 221..222, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 221..222, value: Int( 1, @@ -532,8 +609,10 @@ Module( MatchValue( PatternMatchValue { range: 230..231, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 230..231, value: Int( 2, @@ -550,9 +629,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 234..237, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 234..237, }, ), @@ -562,17 +643,21 @@ Module( }, MatchCase { range: 242..319, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 247..314, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 253..277, value: StringLiteralValue { inner: Single( StringLiteral { range: 253..277, + node_index: AtomicNodeIndex(..), value: "x\n y\n z\n ", flags: StringLiteralFlags { quote_style: Double, @@ -586,11 +671,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 286..310, value: StringLiteralValue { inner: Single( StringLiteral { range: 286..310, + node_index: AtomicNodeIndex(..), value: "x\n y\n z\n ", flags: StringLiteralFlags { quote_style: Double, @@ -607,8 +694,10 @@ Module( MatchValue( PatternMatchValue { range: 279..280, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 279..280, value: Int( 1, @@ -620,8 +709,10 @@ Module( MatchValue( PatternMatchValue { range: 312..313, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 312..313, value: Int( 2, @@ -638,9 +729,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 316..319, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 316..319, }, ), @@ -650,17 +743,21 @@ Module( }, MatchCase { range: 324..358, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 329..353, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 330..333, value: StringLiteralValue { inner: Single( StringLiteral { range: 330..333, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -674,11 +771,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 338..341, value: StringLiteralValue { inner: Single( StringLiteral { range: 338..341, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -692,11 +791,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 346..349, value: StringLiteralValue { inner: Single( StringLiteral { range: 346..349, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -713,8 +814,10 @@ Module( MatchValue( PatternMatchValue { range: 335..336, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 335..336, value: Int( 1, @@ -726,8 +829,10 @@ Module( MatchValue( PatternMatchValue { range: 343..344, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 343..344, value: Int( 2, @@ -739,8 +844,10 @@ Module( MatchValue( PatternMatchValue { range: 351..352, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 351..352, value: Int( 3, @@ -757,9 +864,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 355..358, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 355..358, }, ), @@ -769,12 +878,15 @@ Module( }, MatchCase { range: 363..401, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 368..396, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 369..370, value: Int( 0, @@ -783,11 +895,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 375..378, value: StringLiteralValue { inner: Single( StringLiteral { range: 375..378, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -801,6 +915,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 383..384, value: Int( 0, @@ -809,11 +924,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 389..392, value: StringLiteralValue { inner: Single( StringLiteral { range: 389..392, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -830,8 +947,10 @@ Module( MatchValue( PatternMatchValue { range: 372..373, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 372..373, value: Int( 1, @@ -843,8 +962,10 @@ Module( MatchValue( PatternMatchValue { range: 380..381, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 380..381, value: Int( 1, @@ -856,8 +977,10 @@ Module( MatchValue( PatternMatchValue { range: 386..387, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 386..387, value: Int( 2, @@ -869,8 +992,10 @@ Module( MatchValue( PatternMatchValue { range: 394..395, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 394..395, value: Int( 2, @@ -887,9 +1012,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 398..401, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 398..401, }, ), @@ -899,21 +1026,26 @@ Module( }, MatchCase { range: 406..434, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 411..429, + node_index: AtomicNodeIndex(..), patterns: [ MatchMapping( PatternMatchMapping { range: 412..428, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 413..416, value: StringLiteralValue { inner: Single( StringLiteral { range: 413..416, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -927,11 +1059,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 421..424, value: StringLiteralValue { inner: Single( StringLiteral { range: 421..424, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -948,8 +1082,10 @@ Module( MatchValue( PatternMatchValue { range: 418..419, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 418..419, value: Int( 1, @@ -961,8 +1097,10 @@ Module( MatchValue( PatternMatchValue { range: 426..427, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 426..427, value: Int( 2, @@ -982,9 +1120,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 431..434, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 431..434, }, ), @@ -994,11 +1134,14 @@ Module( }, MatchCase { range: 439..477, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 444..472, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 444..447, id: Name("Foo"), ctx: Load, @@ -1006,19 +1149,24 @@ Module( ), arguments: PatternArguments { range: 447..472, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 448..451, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 448..449, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 450..451, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 450..451, value: Int( 1, @@ -1030,21 +1178,26 @@ Module( }, PatternKeyword { range: 453..471, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 453..454, + node_index: AtomicNodeIndex(..), }, pattern: MatchMapping( PatternMatchMapping { range: 455..471, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 456..459, value: StringLiteralValue { inner: Single( StringLiteral { range: 456..459, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -1058,11 +1211,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 464..467, value: StringLiteralValue { inner: Single( StringLiteral { range: 464..467, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -1079,8 +1234,10 @@ Module( MatchValue( PatternMatchValue { range: 461..462, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 461..462, value: Int( 1, @@ -1092,8 +1249,10 @@ Module( MatchValue( PatternMatchValue { range: 469..470, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 469..470, value: Int( 2, @@ -1115,9 +1274,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 474..477, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 474..477, }, ), @@ -1127,15 +1288,19 @@ Module( }, MatchCase { range: 482..532, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 487..527, + node_index: AtomicNodeIndex(..), patterns: [ MatchClass( PatternMatchClass { range: 488..496, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 488..491, id: Name("Foo"), ctx: Load, @@ -1143,19 +1308,24 @@ Module( ), arguments: PatternArguments { range: 491..496, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 492..495, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 492..493, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 494..495, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 494..495, value: Int( 1, @@ -1172,8 +1342,10 @@ Module( MatchClass( PatternMatchClass { range: 498..526, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 498..501, id: Name("Foo"), ctx: Load, @@ -1181,19 +1353,24 @@ Module( ), arguments: PatternArguments { range: 501..526, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 502..505, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 502..503, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 504..505, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 504..505, value: Int( 1, @@ -1205,21 +1382,26 @@ Module( }, PatternKeyword { range: 507..525, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 507..508, + node_index: AtomicNodeIndex(..), }, pattern: MatchMapping( PatternMatchMapping { range: 509..525, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 510..513, value: StringLiteralValue { inner: Single( StringLiteral { range: 510..513, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -1233,11 +1415,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 518..521, value: StringLiteralValue { inner: Single( StringLiteral { range: 518..521, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Double, @@ -1254,8 +1438,10 @@ Module( MatchValue( PatternMatchValue { range: 515..516, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 515..516, value: Int( 1, @@ -1267,8 +1453,10 @@ Module( MatchValue( PatternMatchValue { range: 523..524, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 523..524, value: Int( 2, @@ -1293,9 +1481,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 529..532, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 529..532, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap index 88b7f1ffb791a..55e020ad84b15 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/duplicate_type_parame ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..261, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..22, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..10, id: Name("Alias"), ctx: Store, @@ -22,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 10..16, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 11..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 11..12, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -37,9 +43,11 @@ Module( TypeVar( TypeParamTypeVar { range: 14..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 14..15, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -50,6 +58,7 @@ Module( ), value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -57,23 +66,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 23..45, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 27..28, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 28..34, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 29..30, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 29..30, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -82,9 +96,11 @@ Module( TypeVar( TypeParamTypeVar { range: 32..33, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 32..33, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -95,19 +111,26 @@ Module( ), parameters: Parameters { range: 34..40, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 35..39, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 35..39, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("t"), range: 35..36, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("T"), ctx: Load, @@ -126,9 +149,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 42..45, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 42..45, }, ), @@ -139,22 +164,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 46..64, decorator_list: [], name: Identifier { id: Name("C"), range: 52..53, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 53..59, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 54..55, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 54..55, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -163,9 +193,11 @@ Module( TypeVar( TypeParamTypeVar { range: 57..58, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 57..58, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -178,9 +210,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 61..64, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 61..64, }, ), @@ -191,9 +225,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 65..132, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..75, id: Name("Alias"), ctx: Store, @@ -202,13 +238,16 @@ Module( type_params: Some( TypeParams { range: 75..126, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 76..77, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 76..77, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -217,13 +256,16 @@ Module( TypeVar( TypeParamTypeVar { range: 79..85, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 79..80, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..85, id: Name("str"), ctx: Load, @@ -236,17 +278,21 @@ Module( TypeVar( TypeParamTypeVar { range: 87..102, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("V"), range: 87..88, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 90..102, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..94, id: Name("str"), ctx: Load, @@ -254,6 +300,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..101, id: Name("bytes"), ctx: Load, @@ -271,9 +318,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 104..107, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 105..107, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -281,9 +330,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 109..112, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 111..112, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -291,14 +342,17 @@ Module( TypeVar( TypeParamTypeVar { range: 114..125, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 114..115, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 118..125, id: Name("default"), ctx: Load, @@ -312,6 +366,7 @@ Module( ), value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 129..132, }, ), @@ -319,23 +374,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 133..154, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 137..138, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 138..147, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 139..140, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 139..140, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -344,9 +404,11 @@ Module( TypeVar( TypeParamTypeVar { range: 142..143, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 142..143, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -355,9 +417,11 @@ Module( TypeVar( TypeParamTypeVar { range: 145..146, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 145..146, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -368,6 +432,9 @@ Module( ), parameters: Parameters { range: 147..149, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -378,9 +445,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 151..154, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 151..154, }, ), @@ -391,23 +460,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 169..188, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 173..174, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 174..181, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 175..176, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 175..176, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -416,9 +490,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 178..180, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 179..180, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -428,6 +504,9 @@ Module( ), parameters: Parameters { range: 181..183, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -438,9 +517,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 185..188, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 185..188, }, ), @@ -451,23 +532,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 218..238, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 222..223, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 223..231, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 224..225, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 224..225, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -476,9 +562,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 227..230, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 229..230, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -488,6 +576,9 @@ Module( ), parameters: Parameters { range: 231..233, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -498,9 +589,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 235..238, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 235..238, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap index 070d99e74df7f..0530c751f8c98 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/except_star_py310.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..126, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 44..125, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), @@ -28,9 +32,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 53..76, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..71, id: Name("ValueError"), ctx: Load, @@ -41,9 +47,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 73..76, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 73..76, }, ), @@ -55,9 +63,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 77..98, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..93, id: Name("KeyError"), ctx: Load, @@ -68,9 +78,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 95..98, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 95..98, }, ), @@ -82,9 +94,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 99..125, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 115..120, id: Name("Error"), ctx: Load, @@ -95,9 +109,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..125, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 122..125, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_invalid_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_invalid_expression.py.snap index 3409ecda78583..fb1f2b6181124 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_invalid_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_invalid_expression.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_invalid_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..74, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..38, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -24,13 +26,16 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 14..38, + node_index: AtomicNodeIndex(..), type_: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 21..28, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Load, @@ -44,6 +49,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 34..38, }, ), @@ -58,10 +64,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 39..73, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 48..52, }, ), @@ -70,12 +78,15 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 53..73, + node_index: AtomicNodeIndex(..), type_: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 61..63, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Load, @@ -89,6 +100,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 69..73, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_as_name.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_as_name.py.snap index 309e29e699f14..b9b370d139a81 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_as_name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_as_name.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_missing_as_name.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..73, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..72, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -24,9 +26,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 14..43, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..30, id: Name("Exception"), ctx: Load, @@ -37,6 +41,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 39..43, }, ), @@ -46,9 +51,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 44..72, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..60, id: Name("Exception"), ctx: Load, @@ -59,6 +66,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 68..72, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception.py.snap index 16e649cc7d26c..fa14993f7942b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_missing_exception.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..166, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..37, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -24,16 +26,19 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 14..37, + node_index: AtomicNodeIndex(..), type_: None, name: Some( Identifier { id: Name("exc"), range: 24..27, + node_index: AtomicNodeIndex(..), }, ), body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 33..37, }, ), @@ -48,10 +53,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 92..165, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 101..105, }, ), @@ -60,11 +67,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 106..123, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 119..123, }, ), @@ -74,11 +83,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 124..140, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 136..140, }, ), @@ -88,16 +99,19 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 141..165, + node_index: AtomicNodeIndex(..), type_: None, name: Some( Identifier { id: Name("exc"), range: 152..155, + node_index: AtomicNodeIndex(..), }, ), body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 161..165, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception_and_as_name.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception_and_as_name.py.snap index ad463555ea040..fc9b78dde0110 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception_and_as_name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_missing_exception_and_as_name.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_missing_exception_and_as_name.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..34, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..33, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -24,11 +26,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 14..33, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 29..33, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap deleted file mode 100644 index 9a142e87afcb7..0000000000000 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap +++ /dev/null @@ -1,251 +0,0 @@ ---- -source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py -snapshot_kind: text ---- -## AST - -``` -Module( - ModModule { - range: 0..131, - body: [ - Try( - StmtTry { - range: 0..64, - body: [ - Pass( - StmtPass { - range: 9..13, - }, - ), - ], - handlers: [ - ExceptHandler( - ExceptHandlerExceptHandler { - range: 14..35, - type_: Some( - Tuple( - ExprTuple { - range: 21..25, - elts: [ - Name( - ExprName { - range: 21..22, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 24..25, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: None, - body: [ - Pass( - StmtPass { - range: 31..35, - }, - ), - ], - }, - ), - ExceptHandler( - ExceptHandlerExceptHandler { - range: 36..64, - type_: Some( - Tuple( - ExprTuple { - range: 43..47, - elts: [ - Name( - ExprName { - range: 43..44, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 46..47, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: Some( - Identifier { - id: Name("exc"), - range: 51..54, - }, - ), - body: [ - Pass( - StmtPass { - range: 60..64, - }, - ), - ], - }, - ), - ], - orelse: [], - finalbody: [], - is_star: false, - }, - ), - Try( - StmtTry { - range: 65..130, - body: [ - Pass( - StmtPass { - range: 74..78, - }, - ), - ], - handlers: [ - ExceptHandler( - ExceptHandlerExceptHandler { - range: 79..101, - type_: Some( - Tuple( - ExprTuple { - range: 87..91, - elts: [ - Name( - ExprName { - range: 87..88, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 90..91, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: None, - body: [ - Pass( - StmtPass { - range: 97..101, - }, - ), - ], - }, - ), - ExceptHandler( - ExceptHandlerExceptHandler { - range: 102..130, - type_: Some( - Tuple( - ExprTuple { - range: 110..114, - elts: [ - Name( - ExprName { - range: 110..111, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 113..114, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: Some( - Identifier { - id: Name("eg"), - range: 118..120, - }, - ), - body: [ - Pass( - StmtPass { - range: 126..130, - }, - ), - ], - }, - ), - ], - orelse: [], - finalbody: [], - is_star: true, - }, - ), - ], - }, -) -``` -## Errors - - | -1 | try: -2 | pass -3 | except x, y: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -4 | pass -5 | except x, y as exc: - | - - - | -3 | except x, y: -4 | pass -5 | except x, y as exc: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -6 | pass -7 | try: - | - - - | - 7 | try: - 8 | pass - 9 | except* x, y: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -10 | pass -11 | except* x, y as eg: - | - - - | - 9 | except* x, y: -10 | pass -11 | except* x, y as eg: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -12 | pass - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap new file mode 100644 index 0000000000000..ddc722c8922ea --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap @@ -0,0 +1,171 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..86, + body: [ + Try( + StmtTry { + node_index: AtomicNodeIndex(..), + range: 0..42, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 9..13, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 14..42, + node_index: AtomicNodeIndex(..), + type_: Some( + Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 21..25, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 21..22, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 24..25, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: Some( + Identifier { + id: Name("exc"), + range: 29..32, + node_index: AtomicNodeIndex(..), + }, + ), + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 38..42, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + node_index: AtomicNodeIndex(..), + range: 43..85, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 52..56, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 57..85, + node_index: AtomicNodeIndex(..), + type_: Some( + Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 65..69, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 68..69, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: Some( + Identifier { + id: Name("eg"), + range: 73..75, + node_index: AtomicNodeIndex(..), + }, + ), + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 81..85, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + ], + }, +) +``` +## Errors + + | +1 | try: +2 | pass +3 | except x, y as exc: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized when using `as` +4 | pass +5 | try: + | + + + | +5 | try: +6 | pass +7 | except* x, y as eg: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized when using `as` +8 | pass + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap new file mode 100644 index 0000000000000..848e2c129955f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap @@ -0,0 +1,159 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..117, + body: [ + Try( + StmtTry { + node_index: AtomicNodeIndex(..), + range: 44..79, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 53..57, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 58..79, + node_index: AtomicNodeIndex(..), + type_: Some( + Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 65..69, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 68..69, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 75..79, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + node_index: AtomicNodeIndex(..), + range: 80..116, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 89..93, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 94..116, + node_index: AtomicNodeIndex(..), + type_: Some( + Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 102..106, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 102..103, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 105..106, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 112..116, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +2 | try: +3 | pass +4 | except x, y: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized on Python 3.13 (syntax was added in Python 3.14) +5 | pass +6 | try: + | + + + | +6 | try: +7 | pass +8 | except* x, y: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized on Python 3.13 (syntax was added in Python 3.14) +9 | pass + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap index 4ef425440b982..0637b78f081a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/do ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..55, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..15, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,17 +28,21 @@ Module( ), arguments: Arguments { range: 4..15, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 5..14, + node_index: AtomicNodeIndex(..), arg: None, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 7..14, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("x"), ctx: Load, @@ -52,12 +60,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..27, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 16..27, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..20, id: Name("call"), ctx: Load, @@ -65,16 +76,20 @@ Module( ), arguments: Arguments { range: 20..27, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 21..26, + node_index: AtomicNodeIndex(..), arg: None, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 24..26, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("x"), ctx: Load, @@ -92,12 +107,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..38, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 28..38, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..32, id: Name("call"), ctx: Load, @@ -105,16 +123,20 @@ Module( ), arguments: Arguments { range: 32..38, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 33..37, + node_index: AtomicNodeIndex(..), arg: None, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 35..37, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("x"), ctx: Load, @@ -132,12 +154,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..54, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 40..54, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..44, id: Name("call"), ctx: Load, @@ -145,9 +170,11 @@ Module( ), arguments: Arguments { range: 44..54, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 1, @@ -158,9 +185,11 @@ Module( keywords: [ Keyword { range: 45..48, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap index d31cfdfc0b694..6e6e7699e23bc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/duplicate_keyword_arguments.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..28, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..28, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..3, id: Name("foo"), ctx: Load, @@ -25,18 +28,22 @@ Module( ), arguments: Arguments { range: 3..28, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 4..7, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 4..5, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 1, @@ -46,14 +53,17 @@ Module( }, Keyword { range: 9..12, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("b"), range: 9..10, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 11..12, value: Int( 2, @@ -63,14 +73,17 @@ Module( }, Keyword { range: 14..17, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("c"), range: 14..15, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..17, value: Int( 3, @@ -80,14 +93,17 @@ Module( }, Keyword { range: 19..22, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("b"), range: 19..20, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..22, value: Int( 4, @@ -97,14 +113,17 @@ Module( }, Keyword { range: 24..27, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 24..25, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 26..27, value: Int( 5, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap index 46cb028b2a619..e517103c145d9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/in ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..67, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..15, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,18 +28,22 @@ Module( ), arguments: Arguments { range: 4..15, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 5..14, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name(""), range: 5..10, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 1, @@ -51,12 +59,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..32, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 16..32, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..20, id: Name("call"), ctx: Load, @@ -64,18 +75,22 @@ Module( ), arguments: Arguments { range: 20..32, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 21..31, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name(""), range: 21..27, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 30..31, value: Int( 1, @@ -91,12 +106,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..47, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 34..47, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..38, id: Name("call"), ctx: Load, @@ -104,13 +122,16 @@ Module( ), arguments: Arguments { range: 38..47, + node_index: AtomicNodeIndex(..), args: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 39..46, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..46, id: Name("x"), ctx: Load, @@ -128,12 +149,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 48..66, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 48..66, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..52, id: Name("call"), ctx: Load, @@ -141,12 +165,15 @@ Module( ), arguments: Arguments { range: 52..66, + node_index: AtomicNodeIndex(..), args: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 53..65, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..65, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap index 1f990885a4eb7..99643af80e05f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/invalid_keyword_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..69, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..17, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,22 +28,27 @@ Module( ), arguments: Arguments { range: 4..17, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 5..16, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 9..16, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("y"), ctx: Load, @@ -58,12 +66,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..40, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 18..40, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..22, id: Name("call"), ctx: Load, @@ -71,21 +82,26 @@ Module( ), arguments: Arguments { range: 22..40, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 23..39, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 23..24, + node_index: AtomicNodeIndex(..), }, ), value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 27..39, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("y"), ctx: Load, @@ -102,12 +118,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 41..53, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 41..53, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..45, id: Name("call"), ctx: Load, @@ -115,21 +134,26 @@ Module( ), arguments: Arguments { range: 45..53, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 46..52, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 46..47, + node_index: AtomicNodeIndex(..), }, ), value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 50..52, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("y"), ctx: Load, @@ -147,12 +171,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..68, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 54..68, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..58, id: Name("call"), ctx: Load, @@ -160,21 +187,26 @@ Module( ), arguments: Arguments { range: 58..68, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 59..67, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 59..60, + node_index: AtomicNodeIndex(..), }, ), value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 64..66, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap index b4a999a44874e..2b60be9235fb7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/invalid_order.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..100, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..17, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..17, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("x"), ctx: Load, @@ -37,9 +42,11 @@ Module( keywords: [ Keyword { range: 5..13, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..13, id: Name("kwargs"), ctx: Load, @@ -54,12 +61,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..30, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 18..30, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..22, id: Name("call"), ctx: Load, @@ -67,9 +77,11 @@ Module( ), arguments: Arguments { range: 22..30, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("y"), ctx: Load, @@ -79,14 +91,17 @@ Module( keywords: [ Keyword { range: 23..26, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 23..24, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 1, @@ -102,12 +117,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..53, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 31..53, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..35, id: Name("call"), ctx: Load, @@ -115,9 +133,11 @@ Module( ), arguments: Arguments { range: 35..53, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("y"), ctx: Load, @@ -127,14 +147,17 @@ Module( keywords: [ Keyword { range: 36..39, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 36..37, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 38..39, value: Int( 1, @@ -144,9 +167,11 @@ Module( }, Keyword { range: 41..49, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..49, id: Name("kwargs"), ctx: Load, @@ -161,12 +186,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..75, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 54..75, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..58, id: Name("call"), ctx: Load, @@ -174,12 +202,15 @@ Module( ), arguments: Arguments { range: 58..75, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 69..74, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..74, id: Name("args"), ctx: Load, @@ -192,9 +223,11 @@ Module( keywords: [ Keyword { range: 59..67, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..67, id: Name("kwargs"), ctx: Load, @@ -209,12 +242,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 76..99, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 76..99, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..80, id: Name("call"), ctx: Load, @@ -222,12 +258,15 @@ Module( ), arguments: Arguments { range: 80..99, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 92..97, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..97, id: Name("args"), ctx: Load, @@ -240,9 +279,11 @@ Module( keywords: [ Keyword { range: 81..89, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..89, id: Name("kwargs"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap index 2fd99bd6cb3f0..bade10dafee52 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/missing_argument.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..10, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..10, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, @@ -35,6 +40,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap index 43abd82cae46c..a17128ef2c7f4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/missing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..9, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..9, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..9, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, @@ -35,6 +40,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_expression.py.snap index 1c86ec46e3621..1b515e8f95efa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_expression.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/mi ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..38, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..10, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..10, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 1, @@ -42,12 +48,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..21, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 11..21, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..15, id: Name("call"), ctx: Load, @@ -55,18 +64,22 @@ Module( ), arguments: Arguments { range: 15..21, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 16..19, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 16..17, + node_index: AtomicNodeIndex(..), }, ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..19, id: Name(""), ctx: Invalid, @@ -81,12 +94,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..32, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 22..32, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..26, id: Name("call"), ctx: Load, @@ -94,12 +110,15 @@ Module( ), arguments: Arguments { range: 26..32, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 27..28, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..28, id: Name(""), ctx: Invalid, @@ -110,6 +129,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 30..31, id: Name("y"), ctx: Load, @@ -124,9 +144,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..37, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..37, id: Name("foo"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap index 3c1e2f02bbb3c..d9a820dcd8fbd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/starred.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..64, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..28, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..28, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -25,15 +28,19 @@ Module( ), arguments: Arguments { range: 4..28, + node_index: AtomicNodeIndex(..), args: [ Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 5..27, elt: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 5..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..10, id: Name("data"), ctx: Load, @@ -45,8 +52,10 @@ Module( generators: [ Comprehension { range: 11..27, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..19, id: Name("data"), ctx: Store, @@ -54,6 +63,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..27, id: Name("iter"), ctx: Load, @@ -75,12 +85,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..43, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 29..43, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..33, id: Name("call"), ctx: Load, @@ -88,16 +101,20 @@ Module( ), arguments: Arguments { range: 33..43, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 34..42, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 35..42, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Load, @@ -118,12 +135,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..63, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 44..63, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..48, id: Name("call"), ctx: Load, @@ -131,15 +151,19 @@ Module( ), arguments: Arguments { range: 48..63, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 49..62, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 50..62, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..62, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap index 0ea8939500055..6eab95404964d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/un ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..26, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..5, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,6 +28,7 @@ Module( ), arguments: Arguments { range: 4..5, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -33,16 +38,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 7..26, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 11..14, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 14..16, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -53,6 +63,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 22..26, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap index af8a140c61834..d60f2bf8710a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/un ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..27, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..6, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..6, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..6, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, @@ -41,16 +47,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 8..27, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 12..15, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 15..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -61,6 +72,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 23..27, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap index d41c5727a7bab..ee3456db28520 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__unclosed_2.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/arguments/un ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..7, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..7, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, @@ -41,16 +47,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 9..28, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 13..16, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 16..18, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -61,6 +72,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 24..28, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap index 0ab3721b7a554..f2eec9b2cb912 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/attribute/invalid_member.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..16, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..1, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -24,9 +26,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1..3, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1..3, value: Float( 0.1, @@ -37,9 +41,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4..5, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, @@ -49,9 +55,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..7, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..7, value: Float( 0.1, @@ -62,9 +70,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..9, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..9, value: Float( 0.0, @@ -75,15 +85,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..15, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 10..15, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 10..12, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Load, @@ -92,12 +106,14 @@ Module( attr: Identifier { id: Name(""), range: 12..12, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 0, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap index a6c1f3f614e50..efa04a319e7bf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/attribute/multiple_dots.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..46, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 0..10, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 0..6, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("extra"), ctx: Load, @@ -29,6 +33,7 @@ Module( attr: Identifier { id: Name(""), range: 6..6, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -36,6 +41,7 @@ Module( attr: Identifier { id: Name("dot"), range: 7..10, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -44,9 +50,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..19, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..19, id: Name("multiple"), ctx: Load, @@ -56,18 +64,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..27, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 19..27, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), attr: Identifier { id: Name("dots"), range: 23..27, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -76,9 +88,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..36, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..36, id: Name("multiple"), ctx: Load, @@ -88,21 +102,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..45, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 36..45, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 36..40, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 36..39, }, ), attr: Identifier { id: Name(""), range: 40..40, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -110,6 +129,7 @@ Module( attr: Identifier { id: Name("dots"), range: 41..45, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap index 440c42423fefe..69af9f8041b7d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/attribute/no ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..141, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 87..93, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 87..93, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..92, id: Name("first"), ctx: Load, @@ -25,6 +29,7 @@ Module( attr: Identifier { id: Name(""), range: 93..93, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -33,9 +38,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..100, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 94..100, id: Name("second"), ctx: Load, @@ -45,12 +52,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 136..141, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 136..141, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..140, id: Name("last"), ctx: Load, @@ -59,6 +69,7 @@ Module( attr: Identifier { id: Name(""), range: 141..141, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_0.py.snap index 747e1c7a79539..f1e8020d1a8cb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/await/no_exp ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..73, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 61..66, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 61..66, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..66, id: Name(""), ctx: Invalid, @@ -28,12 +32,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..73, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 68..73, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -42,6 +49,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_1.py.snap index 7d67f0e7008b6..cf7d31d11b5e7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__no_expression_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/await/no_exp ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..85, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 59..64, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 59..64, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..64, id: Name(""), ctx: Invalid, @@ -28,16 +32,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 66..85, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 70..73, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 73..75, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -48,6 +57,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 81..85, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap index 87c433552a4a5..029e1723d9b72 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/await/recove ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..284, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 117..130, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 117..130, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 123..130, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("x"), ctx: Load, @@ -33,15 +38,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 154..162, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 154..162, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 160..162, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 161..162, id: Name("x"), ctx: Load, @@ -56,15 +65,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 163..173, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 163..173, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 170..172, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("x"), ctx: Load, @@ -79,16 +92,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 214..227, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 214..227, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 220..227, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 226..227, id: Name("x"), ctx: Load, @@ -103,25 +120,34 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 228..245, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 228..245, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 234..245, parameters: Some( Parameters { range: 241..242, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 241..242, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 241..242, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 241..242, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -135,6 +161,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 244..245, id: Name("x"), ctx: Load, @@ -148,16 +175,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 246..254, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 246..254, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 252..254, op: UAdd, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 253..254, id: Name("x"), ctx: Load, @@ -171,16 +202,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 255..263, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 255..263, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 261..263, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 262..263, id: Name("x"), ctx: Load, @@ -194,16 +229,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 264..272, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 264..272, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 270..272, op: Invert, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 271..272, id: Name("x"), ctx: Load, @@ -217,16 +256,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 273..284, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 273..284, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 279..284, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 283..284, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap index cb65ebee14364..fa628602c101c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/inval ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..15, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -25,19 +29,26 @@ Module( op: Add, right: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 4..15, parameters: Some( Parameters { range: 11..12, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 11..12, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 11..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 11..12, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -51,6 +62,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("y"), ctx: Load, @@ -64,12 +76,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..28, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 17..28, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Load, @@ -78,10 +93,12 @@ Module( op: Sub, right: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 21..28, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_lhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_lhs.py.snap index 7e9d24594d17e..b9bd494cd7f6b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_lhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_lhs.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/missi ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2..3, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("y"), ctx: Load, @@ -23,12 +26,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..10, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -38,6 +44,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_0.py.snap index 9f84e4249e785..ab67484feb3a6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/missi ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..3, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..3, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 0..1, value: Int( 0, @@ -26,6 +30,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -37,12 +42,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..10, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -52,6 +60,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_1.py.snap index 2951642fe3f0b..9357c59dc3aa4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__missing_rhs_1.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/missi ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..18, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..11, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..5, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 0..1, value: Int( 1, @@ -29,6 +34,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 2, @@ -40,9 +46,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 8..11, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 3, @@ -52,6 +60,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..11, id: Name(""), ctx: Invalid, @@ -65,12 +74,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..18, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 13..18, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 4, @@ -80,6 +92,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 17..18, value: Int( 5, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__multiple_ops.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__multiple_ops.py.snap index 2bcbe82a7e502..41adb471bfeb4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__multiple_ops.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__multiple_ops.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/multiple_ops.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..19, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..3, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..3, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -26,10 +29,12 @@ Module( op: Add, right: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 2..3, op: UAdd, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -43,12 +48,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4..9, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4..9, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 1, @@ -58,6 +66,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 2, @@ -70,12 +79,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..13, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 10..13, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Load, @@ -84,10 +96,12 @@ Module( op: Sub, right: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 12..13, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..13, id: Name(""), ctx: Invalid, @@ -101,12 +115,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..19, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 14..19, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 14..15, value: Int( 1, @@ -116,6 +133,7 @@ Module( op: Sub, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap index f38974105d4b6..475add5dc262c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/named_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..26, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..5, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -26,6 +29,7 @@ Module( op: Sub, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("y"), ctx: Load, @@ -37,13 +41,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..15, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 9..15, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 1, @@ -52,6 +59,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 2, @@ -67,12 +75,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..21, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 16..21, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("x"), ctx: Load, @@ -81,6 +92,7 @@ Module( op: Div, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("y"), ctx: Load, @@ -92,9 +104,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..26, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap index cc1cc268ad3dd..3fc18497c4ce5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/bin_op/starred_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..14, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..6, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 0..6, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -26,9 +29,11 @@ Module( op: Add, right: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 4..6, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("y"), ctx: Load, @@ -43,12 +48,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..14, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 7..14, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, @@ -57,9 +65,11 @@ Module( op: Pow, right: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 12..14, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap index 9b4a04616ca2a..a3515f3a7fa6d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap @@ -7,18 +7,22 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/bool_op/inva ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..31, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 0..17, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -26,19 +30,26 @@ Module( ), Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 6..17, parameters: Some( Parameters { range: 13..14, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 13..14, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 13..14, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 13..14, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -52,6 +63,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("y"), ctx: Load, @@ -66,14 +78,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..31, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 19..31, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("x"), ctx: Load, @@ -81,10 +96,12 @@ Module( ), Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 24..31, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 30..31, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap index 6818a4a9bf688..68ff71e827417 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/bool_op/missing_lhs.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..5, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4..5, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_rhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_rhs.py.snap index 34a82c7f26085..85ad8a317f91d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_rhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_rhs.py.snap @@ -7,18 +7,22 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/bool_op/miss ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..12, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 0..5, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -26,6 +30,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..5, id: Name(""), ctx: Invalid, @@ -38,12 +43,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..12, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 7..12, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 1, @@ -53,6 +61,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 11..12, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap index c7af1705281fa..e06f08171688b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/bool_op/named_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 0..7, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -27,6 +30,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("a"), ctx: Load, @@ -39,9 +43,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..12, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("b"), ctx: Load, @@ -51,14 +57,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..19, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 13..19, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("x"), ctx: Load, @@ -66,6 +75,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("a"), ctx: Load, @@ -78,9 +88,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..24, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap index 76db7d093a211..5369e4e69bd79 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/bool_op/starred_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..16, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..8, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 0..8, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -27,9 +30,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("y"), ctx: Load, @@ -45,14 +50,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..16, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 9..16, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("x"), ctx: Load, @@ -60,9 +68,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 14..16, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap index efd6a63b4df80..a64df1f7b321f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/inva ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..131, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..10, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -28,10 +32,12 @@ Module( comparators: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 5..10, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("y"), ctx: Load, @@ -46,10 +52,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 35..41, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("x"), ctx: Store, @@ -58,9 +66,11 @@ Module( ], value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 38..41, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..38, id: Name(""), ctx: Invalid, @@ -72,6 +82,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("y"), ctx: Load, @@ -84,9 +95,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 120..121, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("x"), ctx: Load, @@ -96,13 +109,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..128, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 122..128, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..128, id: Name("is"), ctx: Load, @@ -114,9 +130,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 129..130, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap index 7fac7231c751e..a118b221db5f8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/inva ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..34, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..20, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..20, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -28,19 +32,26 @@ Module( comparators: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 9..20, parameters: Some( Parameters { range: 16..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 16..17, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 16..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 16..17, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -54,6 +65,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("y"), ctx: Load, @@ -68,12 +80,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..34, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 22..34, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -85,10 +100,12 @@ Module( comparators: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 27..34, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..34, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_lhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_lhs.py.snap index 755eb042a5979..a38a989312cb1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_lhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_lhs.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/miss ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2..3, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("y"), ctx: Load, @@ -23,12 +26,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..10, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -38,6 +44,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_0.py.snap index 135f76de35938..b41171207cb07 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/miss ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..3, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..3, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -28,6 +32,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -40,12 +45,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..10, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -55,6 +63,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_1.py.snap index f61bcb3b89129..e58c0025af0d0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_1.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/miss ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..71, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 59..60, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("x"), ctx: Load, @@ -23,13 +26,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 61..64, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 61..64, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..64, id: Name(""), ctx: Invalid, @@ -41,12 +47,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..71, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 66..71, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 66..67, value: Int( 1, @@ -56,6 +65,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 70..71, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_2.py.snap index e530d30468f2e..f02148c958940 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__missing_rhs_2.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/miss ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..15, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..8, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..8, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -28,6 +32,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..8, id: Name(""), ctx: Invalid, @@ -40,12 +45,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..15, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 10..15, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 1, @@ -55,6 +63,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 14..15, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap index d3879e17a80c7..023e710c958e2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/multiple_equals.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..41, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 25..32, targets: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 25..29, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("x"), ctx: Load, @@ -30,6 +33,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..29, id: Name(""), ctx: Invalid, @@ -41,6 +45,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..32, id: Name("y"), ctx: Load, @@ -50,13 +55,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 33..40, targets: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 33..37, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..34, id: Name("x"), ctx: Load, @@ -68,6 +76,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..37, id: Name(""), ctx: Invalid, @@ -79,6 +88,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap index 27be524dd397c..fdeafe1b4d71f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/named_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..31, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..10, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -29,6 +32,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("y"), ctx: Load, @@ -41,13 +45,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..20, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 14..20, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 15..16, value: Int( 1, @@ -56,6 +63,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 2, @@ -71,12 +79,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..26, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 21..26, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("x"), ctx: Load, @@ -88,6 +99,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("y"), ctx: Load, @@ -100,9 +112,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..31, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 30..31, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap index 61e69b69f969f..90dd6b20562a6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/compare/star ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..39, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..7, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -28,9 +32,11 @@ Module( comparators: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 5..7, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("y"), ctx: Load, @@ -46,12 +52,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..19, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 8..19, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("x"), ctx: Load, @@ -63,9 +72,11 @@ Module( comparators: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 17..19, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("y"), ctx: Load, @@ -81,15 +92,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..27, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 21..27, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 22..27, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -101,6 +116,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("y"), ctx: Load, @@ -116,15 +132,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..39, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 28..39, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 29..39, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Load, @@ -136,6 +156,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap index 6d06ef0c123f9..7a000174912f1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/compreh ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..362, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..34, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 17..34, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("x"), ctx: Load, @@ -24,6 +28,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("y"), ctx: Load, @@ -32,8 +37,10 @@ Module( generators: [ Comprehension { range: 23..33, + node_index: AtomicNodeIndex(..), target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 27..28, value: Int( 1, @@ -42,6 +49,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("y"), ctx: Load, @@ -57,12 +65,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..54, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 35..54, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("x"), ctx: Load, @@ -70,6 +81,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("y"), ctx: Load, @@ -78,13 +90,16 @@ Module( generators: [ Comprehension { range: 41..53, + node_index: AtomicNodeIndex(..), target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 45..48, value: StringLiteralValue { inner: Single( StringLiteral { range: 45..48, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Single, @@ -98,6 +113,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..53, id: Name("y"), ctx: Load, @@ -113,12 +129,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 55..77, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 55..77, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Load, @@ -126,6 +145,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("y"), ctx: Load, @@ -134,11 +154,14 @@ Module( generators: [ Comprehension { range: 61..76, + node_index: AtomicNodeIndex(..), target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 65..71, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..69, id: Name("call"), ctx: Load, @@ -146,6 +169,7 @@ Module( ), arguments: Arguments { range: 69..71, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -153,6 +177,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("y"), ctx: Load, @@ -168,12 +193,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 78..100, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 78..100, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..80, id: Name("x"), ctx: Load, @@ -181,6 +209,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("y"), ctx: Load, @@ -189,12 +218,15 @@ Module( generators: [ Comprehension { range: 84..99, + node_index: AtomicNodeIndex(..), target: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 88..94, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("a"), ctx: Load, @@ -202,6 +234,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..93, id: Name("b"), ctx: Load, @@ -212,6 +245,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("y"), ctx: Load, @@ -227,12 +261,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 117..135, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 117..135, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 118..119, id: Name("x"), ctx: Load, @@ -240,6 +277,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..122, id: Name("y"), ctx: Load, @@ -248,8 +286,10 @@ Module( generators: [ Comprehension { range: 123..134, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("x"), ctx: Store, @@ -257,9 +297,11 @@ Module( ), iter: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 132..134, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..134, id: Name("y"), ctx: Load, @@ -278,12 +320,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 136..159, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 136..159, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("x"), ctx: Load, @@ -291,6 +336,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("y"), ctx: Load, @@ -299,8 +345,10 @@ Module( generators: [ Comprehension { range: 142..158, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("x"), ctx: Store, @@ -308,10 +356,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 151..158, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 157..158, id: Name("y"), ctx: Load, @@ -330,12 +380,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 160..188, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 160..188, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 161..162, id: Name("x"), ctx: Load, @@ -343,6 +396,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 164..165, id: Name("y"), ctx: Load, @@ -351,8 +405,10 @@ Module( generators: [ Comprehension { range: 166..187, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("x"), ctx: Store, @@ -360,9 +416,11 @@ Module( ), iter: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 175..187, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 186..187, id: Name("y"), ctx: Load, @@ -380,12 +438,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 189..216, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 189..216, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 190..191, id: Name("x"), ctx: Load, @@ -393,6 +454,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..194, id: Name("y"), ctx: Load, @@ -401,8 +463,10 @@ Module( generators: [ Comprehension { range: 195..215, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..200, id: Name("x"), ctx: Store, @@ -410,19 +474,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 204..215, parameters: Some( Parameters { range: 211..212, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 211..212, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 211..212, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 211..212, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -436,6 +507,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("y"), ctx: Load, @@ -453,12 +525,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 231..257, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 231..257, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 232..233, id: Name("x"), ctx: Load, @@ -466,6 +541,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 235..236, id: Name("y"), ctx: Load, @@ -474,8 +550,10 @@ Module( generators: [ Comprehension { range: 237..256, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..242, id: Name("x"), ctx: Store, @@ -483,6 +561,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 246..250, id: Name("data"), ctx: Load, @@ -491,9 +570,11 @@ Module( ifs: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 254..256, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 255..256, id: Name("y"), ctx: Load, @@ -512,12 +593,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 258..289, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 258..289, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..260, id: Name("x"), ctx: Load, @@ -525,6 +609,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 262..263, id: Name("y"), ctx: Load, @@ -533,8 +618,10 @@ Module( generators: [ Comprehension { range: 264..288, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 268..269, id: Name("x"), ctx: Store, @@ -542,6 +629,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 273..277, id: Name("data"), ctx: Load, @@ -550,10 +638,12 @@ Module( ifs: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 281..288, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 287..288, id: Name("y"), ctx: Load, @@ -572,12 +662,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 290..326, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 290..326, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 291..292, id: Name("x"), ctx: Load, @@ -585,6 +678,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 294..295, id: Name("y"), ctx: Load, @@ -593,8 +687,10 @@ Module( generators: [ Comprehension { range: 296..325, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 300..301, id: Name("x"), ctx: Store, @@ -602,6 +698,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 305..309, id: Name("data"), ctx: Load, @@ -610,9 +707,11 @@ Module( ifs: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 313..325, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 324..325, id: Name("y"), ctx: Load, @@ -630,12 +729,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 327..362, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 327..362, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 328..329, id: Name("x"), ctx: Load, @@ -643,6 +745,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 331..332, id: Name("y"), ctx: Load, @@ -651,8 +754,10 @@ Module( generators: [ Comprehension { range: 333..361, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 337..338, id: Name("x"), ctx: Store, @@ -660,6 +765,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 342..346, id: Name("data"), ctx: Load, @@ -668,19 +774,26 @@ Module( ifs: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 350..361, parameters: Some( Parameters { range: 357..358, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 357..358, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 357..358, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 357..358, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -694,6 +807,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 360..361, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap index 18f34772fa4e0..2375e262a6783 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/double_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..278, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 125..135, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 125..135, items: [ DictItem { key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("x"), ctx: Load, @@ -30,6 +34,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 133..134, value: Int( 1, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..134, id: Name(""), ctx: Invalid, @@ -52,15 +58,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 136..162, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 136..162, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("a"), ctx: Load, @@ -69,6 +78,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 140..141, value: Int( 1, @@ -80,15 +90,18 @@ Module( key: None, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 145..161, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 150..154, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..146, id: Name("x"), ctx: Load, @@ -96,6 +109,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..161, id: Name("y"), ctx: Load, @@ -111,28 +125,37 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 163..184, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 163..184, items: [ DictItem { key: None, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 166..177, parameters: Some( Parameters { range: 173..174, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 173..174, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 173..174, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 173..174, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -146,6 +169,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 176..177, id: Name("x"), ctx: Load, @@ -158,6 +182,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 179..180, id: Name("b"), ctx: Load, @@ -166,6 +191,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 182..183, value: Int( 2, @@ -180,15 +206,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 185..201, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 185..201, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 186..187, id: Name("a"), ctx: Load, @@ -197,6 +226,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 189..190, value: Int( 1, @@ -208,11 +238,13 @@ Module( key: None, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 194..200, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 194..195, id: Name("x"), ctx: Load, @@ -220,6 +252,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..200, id: Name("y"), ctx: Load, @@ -236,20 +269,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 202..219, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 202..219, items: [ DictItem { key: None, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 205..212, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 205..206, id: Name("x"), ctx: Load, @@ -257,6 +294,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 211..212, id: Name("y"), ctx: Load, @@ -270,6 +308,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("b"), ctx: Load, @@ -278,6 +317,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 217..218, value: Int( 2, @@ -292,15 +332,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 220..241, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 220..241, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 221..222, id: Name("a"), ctx: Load, @@ -309,6 +352,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 224..225, value: Int( 1, @@ -320,10 +364,12 @@ Module( key: None, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 229..234, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 233..234, id: Name("x"), ctx: Load, @@ -336,6 +382,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("b"), ctx: Load, @@ -344,6 +391,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 239..240, value: Int( 2, @@ -358,18 +406,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 242..252, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 242..252, items: [ DictItem { key: None, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 245..251, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("x"), ctx: Load, @@ -381,6 +433,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 250..251, id: Name("y"), ctx: Load, @@ -397,18 +450,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 253..267, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 253..267, items: [ DictItem { key: None, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 256..266, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 256..257, id: Name("x"), ctx: Load, @@ -420,6 +477,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 265..266, id: Name("y"), ctx: Load, @@ -436,18 +494,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 268..277, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 268..277, items: [ DictItem { key: None, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 271..276, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 271..272, id: Name("x"), ctx: Load, @@ -459,6 +521,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 275..276, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap index 2cdfca37149d4..22ca3a3d362a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star_comprehension.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/double_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..358, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..147, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 122..147, items: [ DictItem { key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 125..126, id: Name("x"), ctx: Load, @@ -30,6 +34,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("y"), ctx: Load, @@ -38,6 +43,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 130..133, id: Name("for"), ctx: Load, @@ -48,6 +54,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..135, id: Name("x"), ctx: Load, @@ -56,6 +63,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..135, id: Name(""), ctx: Invalid, @@ -66,9 +74,11 @@ Module( key: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 137..146, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("y"), ctx: Load, @@ -80,6 +90,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..146, id: Name("data"), ctx: Load, @@ -91,6 +102,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..146, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap index 45b456fe189ec..3b205a80f1463 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..24, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 0..24, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -28,6 +32,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..8, id: Name("def"), ctx: Load, @@ -38,9 +43,11 @@ Module( key: Some( Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 9..14, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..12, id: Name("foo"), ctx: Load, @@ -48,6 +55,7 @@ Module( ), arguments: Arguments { range: 12..14, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -56,6 +64,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..24, id: Name("pass"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap index 67b4fca986ec0..487654f51db0e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 0..10, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -28,9 +32,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -40,6 +46,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap index 99ceea07ed510..474f5b303833a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_2.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..27, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..6, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 0..6, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -28,6 +32,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 1, @@ -42,16 +47,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 8..27, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 12..15, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 15..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -62,6 +72,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 23..27, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap index b3123ce9b0317..a6bcfaa62163d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_0.py.snap @@ -7,22 +7,27 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/named_e ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..84, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 55..77, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 55..77, items: [ DictItem { key: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 56..62, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Store, @@ -30,6 +35,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 1, @@ -41,6 +47,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..65, id: Name("y"), ctx: Load, @@ -51,6 +58,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("z"), ctx: Load, @@ -59,6 +67,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..68, id: Name(""), ctx: Invalid, @@ -69,6 +78,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 72..73, value: Int( 2, @@ -78,6 +88,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("a"), ctx: Load, @@ -91,12 +102,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 79..84, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 79..84, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..80, id: Name("x"), ctx: Load, @@ -105,6 +119,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap index dd7855f8b4791..47c76d0afcc71 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__named_expression_1.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/named_e ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..86, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..79, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 57..79, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("x"), ctx: Load, @@ -28,6 +32,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..62, id: Name("y"), ctx: Load, @@ -38,6 +43,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 66..67, value: Int( 1, @@ -47,6 +53,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..67, id: Name(""), ctx: Invalid, @@ -57,6 +64,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..70, id: Name("z"), ctx: Load, @@ -65,6 +73,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("a"), ctx: Load, @@ -75,6 +84,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 77..78, value: Int( 2, @@ -84,6 +94,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..78, id: Name(""), ctx: Invalid, @@ -97,12 +108,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..86, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 81..86, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("x"), ctx: Load, @@ -111,6 +125,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..86, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap index 71d01983722cc..ecfb8d7b9a15d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/dict/recover ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..346, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 88..91, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 88..91, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..89, id: Name(""), ctx: Invalid, @@ -30,15 +34,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..105, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 93..105, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 94..95, value: Int( 1, @@ -48,6 +55,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 97..98, value: Int( 2, @@ -59,6 +67,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 100..101, value: Int( 3, @@ -68,6 +77,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 103..104, value: Int( 4, @@ -82,15 +92,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 107..115, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 107..115, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 108..109, value: Int( 1, @@ -100,6 +113,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 111..112, value: Int( 2, @@ -114,15 +128,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..144, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 133..144, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 134..135, value: Int( 1, @@ -132,6 +149,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 137..138, value: Int( 2, @@ -143,6 +161,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 139..140, value: Int( 3, @@ -152,6 +171,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 142..143, value: Int( 4, @@ -166,15 +186,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 157..162, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 157..162, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 158..159, value: Int( 1, @@ -184,6 +207,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..160, id: Name(""), ctx: Invalid, @@ -197,15 +221,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 201..205, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 201..205, items: [ DictItem { key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 204..204, id: Name(""), ctx: Invalid, @@ -219,15 +246,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 206..222, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 206..222, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 207..208, id: Name("x"), ctx: Load, @@ -236,6 +266,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 210..211, id: Name("y"), ctx: Load, @@ -246,6 +277,7 @@ Module( key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 215..215, id: Name(""), ctx: Invalid, @@ -256,6 +288,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 217..218, id: Name("a"), ctx: Load, @@ -264,6 +297,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 220..221, id: Name("b"), ctx: Load, @@ -277,18 +311,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 310..330, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 310..330, items: [ DictItem { key: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 311..313, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 312..313, id: Name("x"), ctx: Load, @@ -300,6 +338,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 315..316, id: Name("y"), ctx: Load, @@ -310,6 +349,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 318..319, id: Name("z"), ctx: Load, @@ -318,6 +358,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 321..322, id: Name("a"), ctx: Load, @@ -328,9 +369,11 @@ Module( key: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 324..326, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 325..326, id: Name("b"), ctx: Load, @@ -342,6 +385,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 328..329, id: Name("c"), ctx: Load, @@ -355,15 +399,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 331..345, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 331..345, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 332..333, id: Name("x"), ctx: Load, @@ -372,9 +419,11 @@ Module( ), value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 335..337, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 336..337, id: Name("y"), ctx: Load, @@ -388,6 +437,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 339..340, id: Name("z"), ctx: Load, @@ -396,9 +446,11 @@ Module( ), value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 342..344, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 343..344, id: Name("a"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap index 7a4f5b3f5d643..bf215d18b6919 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/emoji_identi ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..64, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..5, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Store, @@ -23,6 +26,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..5, id: Name(""), ctx: Invalid, @@ -32,10 +36,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 32..37, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("a"), ctx: Store, @@ -44,6 +50,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..37, id: Name(""), ctx: Invalid, @@ -53,13 +60,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 42..43, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 42..43, op: UAdd, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..43, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap index 8738dedd6d3e4..d4c139b3cc46e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap @@ -1,13 +1,13 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/emoji_statement.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..5, body: [], }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_0.py.snap index 380694740073e..b480e99948aa3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/if/missing_o ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..88, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..67, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 53..67, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..62, id: Name("expr"), ctx: Load, @@ -24,6 +28,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("x"), ctx: Load, @@ -31,6 +36,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..67, id: Name(""), ctx: Invalid, @@ -42,16 +48,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 69..88, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 73..76, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 76..78, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -62,6 +73,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 84..88, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_1.py.snap index 21c4bce330eef..beec6bcb14994 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_orelse_expr_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/if/missing_o ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..76, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 55..69, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 55..69, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..64, id: Name("expr"), ctx: Load, @@ -24,6 +28,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..56, id: Name("x"), ctx: Load, @@ -31,6 +36,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..69, id: Name(""), ctx: Invalid, @@ -42,12 +48,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..76, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 71..76, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 71..72, value: Int( 1, @@ -57,6 +66,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_0.py.snap index e621d76610c15..d3c32935800bd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/if/missing_t ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..76, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 51..55, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 51..55, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..55, id: Name(""), ctx: Invalid, @@ -24,6 +28,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("x"), ctx: Load, @@ -31,6 +36,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..55, id: Name(""), ctx: Invalid, @@ -42,16 +48,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 57..76, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 61..64, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 64..66, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -62,6 +73,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 72..76, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_1.py.snap index 1289e838434a5..39ddb5266add8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__missing_test_expr_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/if/missing_t ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..64, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..57, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 53..57, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..57, id: Name(""), ctx: Invalid, @@ -24,6 +28,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("x"), ctx: Load, @@ -31,6 +36,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..57, id: Name(""), ctx: Invalid, @@ -42,12 +48,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 59..64, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 59..64, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 59..60, value: Int( 1, @@ -57,6 +66,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 63..64, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap index dd35e9a27d710..e62779397c09c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/if/recover.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..215, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..43, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 26..43, test: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 31..36, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..36, id: Name("expr"), ctx: Load, @@ -30,6 +35,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("x"), ctx: Load, @@ -37,6 +43,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..43, id: Name("y"), ctx: Load, @@ -48,25 +55,34 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..67, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 44..67, test: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 49..60, parameters: Some( Parameters { range: 56..57, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 56..57, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 56..57, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 56..57, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -80,6 +96,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("x"), ctx: Load, @@ -89,6 +106,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Load, @@ -96,6 +114,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("y"), ctx: Load, @@ -107,16 +126,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..87, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 68..87, test: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 73..80, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..80, id: Name("x"), ctx: Load, @@ -127,6 +150,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -134,6 +158,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..87, id: Name("y"), ctx: Load, @@ -145,15 +170,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 88..112, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 88..112, test: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 93..105, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("x"), ctx: Load, @@ -163,6 +192,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..89, id: Name("x"), ctx: Load, @@ -170,6 +200,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..112, id: Name("y"), ctx: Load, @@ -181,12 +212,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 142..164, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 142..164, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 147..151, id: Name("expr"), ctx: Load, @@ -194,6 +228,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..143, id: Name("x"), ctx: Load, @@ -201,9 +236,11 @@ Module( ), orelse: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 157..164, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 158..164, id: Name("orelse"), ctx: Load, @@ -218,12 +255,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 165..187, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 165..187, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..174, id: Name("expr"), ctx: Load, @@ -231,6 +271,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..166, id: Name("x"), ctx: Load, @@ -238,10 +279,12 @@ Module( ), orelse: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 180..187, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 186..187, id: Name("y"), ctx: Load, @@ -256,12 +299,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 188..215, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 188..215, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..197, id: Name("expr"), ctx: Load, @@ -269,6 +315,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 188..189, id: Name("x"), ctx: Load, @@ -276,9 +323,11 @@ Module( ), orelse: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 203..215, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap index 3d9df97c4640e..ac95e6aff76d7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap @@ -1,33 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/lambda_default_parameters.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..20, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..20, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..20, parameters: Some( Parameters { range: 7..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 7..8, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 7..8, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -35,17 +43,21 @@ Module( }, ParameterWithDefault { range: 10..14, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 10..11, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 10..11, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 12..14, value: Int( 20, @@ -56,11 +68,14 @@ Module( }, ParameterWithDefault { range: 16..17, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 16..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 16..17, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -74,6 +89,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 19..20, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap index 04b08939f39db..30e3bf68b1f18 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap @@ -7,26 +7,35 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/lambda_dupli ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..91, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..14, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..14, parameters: Some( Parameters { range: 7..11, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 7..8, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 7..8, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -34,11 +43,14 @@ Module( }, ParameterWithDefault { range: 10..11, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 10..11, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 10..11, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -52,6 +64,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 1, @@ -64,22 +77,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..33, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 16..33, parameters: Some( Parameters { range: 23..30, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 23..24, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 23..24, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 23..24, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -90,11 +111,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 29..30, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 29..30, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 29..30, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -106,6 +130,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 1, @@ -118,22 +143,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..52, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 35..52, parameters: Some( Parameters { range: 42..49, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 42..43, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 42..43, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 42..43, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -141,17 +174,21 @@ Module( }, ParameterWithDefault { range: 45..49, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 45..46, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 45..46, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 47..49, value: Int( 20, @@ -168,6 +205,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, @@ -180,22 +218,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..69, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 54..69, parameters: Some( Parameters { range: 61..66, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 61..62, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 61..62, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 61..62, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -205,9 +251,11 @@ Module( vararg: Some( Parameter { range: 64..66, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 65..66, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -218,6 +266,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 68..69, value: Int( 1, @@ -230,22 +279,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..90, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 71..90, parameters: Some( Parameters { range: 78..87, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 78..79, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 78..79, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 78..79, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -257,9 +314,11 @@ Module( kwarg: Some( Parameter { range: 84..87, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 86..87, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -268,6 +327,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 1, @@ -284,6 +344,16 @@ Module( ``` ## Errors + | +7 | lambda a, *a: 1 +8 | +9 | lambda a, *, **a: 1 + | ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator + | + + +## Semantic Syntax Errors + | 1 | lambda a, a: 1 | ^ Syntax Error: Duplicate parameter "a" @@ -322,14 +392,6 @@ Module( | - | -7 | lambda a, *a: 1 -8 | -9 | lambda a, *, **a: 1 - | ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator - | - - | 7 | lambda a, *a: 1 8 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap index 124079789dd45..110db0cba9fa1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/compreh ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..376, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 33..48, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 33..48, elt: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 34..36, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("x"), ctx: Load, @@ -31,8 +36,10 @@ Module( generators: [ Comprehension { range: 37..47, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Store, @@ -40,6 +47,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("y"), ctx: Load, @@ -55,12 +63,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..81, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 67..81, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -69,8 +80,10 @@ Module( generators: [ Comprehension { range: 70..80, + node_index: AtomicNodeIndex(..), target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 74..75, value: Int( 1, @@ -79,6 +92,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..80, id: Name("y"), ctx: Load, @@ -94,12 +108,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..98, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 82..98, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("x"), ctx: Load, @@ -108,13 +125,16 @@ Module( generators: [ Comprehension { range: 85..97, + node_index: AtomicNodeIndex(..), target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 89..92, value: StringLiteralValue { inner: Single( StringLiteral { range: 89..92, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Single, @@ -128,6 +148,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("y"), ctx: Load, @@ -143,12 +164,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 99..118, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 99..118, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("x"), ctx: Load, @@ -157,11 +181,14 @@ Module( generators: [ Comprehension { range: 102..117, + node_index: AtomicNodeIndex(..), target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 106..112, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..110, id: Name("call"), ctx: Load, @@ -169,6 +196,7 @@ Module( ), arguments: Arguments { range: 110..112, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -176,6 +204,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..117, id: Name("y"), ctx: Load, @@ -191,12 +220,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..138, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 119..138, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("x"), ctx: Load, @@ -205,12 +237,15 @@ Module( generators: [ Comprehension { range: 122..137, + node_index: AtomicNodeIndex(..), target: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 126..132, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("a"), ctx: Load, @@ -218,6 +253,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 130..131, id: Name("b"), ctx: Load, @@ -228,6 +264,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("y"), ctx: Load, @@ -243,12 +280,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 155..170, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 155..170, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("x"), ctx: Load, @@ -257,8 +297,10 @@ Module( generators: [ Comprehension { range: 158..169, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 162..163, id: Name("x"), ctx: Store, @@ -266,9 +308,11 @@ Module( ), iter: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 167..169, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 168..169, id: Name("y"), ctx: Load, @@ -287,12 +331,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 171..191, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 171..191, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 172..173, id: Name("x"), ctx: Load, @@ -301,8 +348,10 @@ Module( generators: [ Comprehension { range: 174..190, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("x"), ctx: Store, @@ -310,10 +359,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 183..190, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("y"), ctx: Load, @@ -332,12 +383,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 192..217, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 192..217, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..194, id: Name("x"), ctx: Load, @@ -346,8 +400,10 @@ Module( generators: [ Comprehension { range: 195..216, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..200, id: Name("x"), ctx: Store, @@ -355,9 +411,11 @@ Module( ), iter: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 204..216, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 215..216, id: Name("y"), ctx: Load, @@ -375,12 +433,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 218..242, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 218..242, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 219..220, id: Name("x"), ctx: Load, @@ -389,8 +450,10 @@ Module( generators: [ Comprehension { range: 221..241, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 225..226, id: Name("x"), ctx: Store, @@ -398,19 +461,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 230..241, parameters: Some( Parameters { range: 237..238, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 237..238, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 237..238, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 237..238, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -424,6 +494,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 240..241, id: Name("y"), ctx: Load, @@ -441,12 +512,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 257..280, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 257..280, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 258..259, id: Name("x"), ctx: Load, @@ -455,8 +529,10 @@ Module( generators: [ Comprehension { range: 260..279, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 264..265, id: Name("x"), ctx: Store, @@ -464,6 +540,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 269..273, id: Name("data"), ctx: Load, @@ -472,9 +549,11 @@ Module( ifs: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 277..279, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 278..279, id: Name("y"), ctx: Load, @@ -493,12 +572,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 281..309, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 281..309, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 282..283, id: Name("x"), ctx: Load, @@ -507,8 +589,10 @@ Module( generators: [ Comprehension { range: 284..308, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 288..289, id: Name("x"), ctx: Store, @@ -516,6 +600,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 293..297, id: Name("data"), ctx: Load, @@ -524,10 +609,12 @@ Module( ifs: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 301..308, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 307..308, id: Name("y"), ctx: Load, @@ -546,12 +633,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 310..343, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 310..343, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 311..312, id: Name("x"), ctx: Load, @@ -560,8 +650,10 @@ Module( generators: [ Comprehension { range: 313..342, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 317..318, id: Name("x"), ctx: Store, @@ -569,6 +661,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 322..326, id: Name("data"), ctx: Load, @@ -577,9 +670,11 @@ Module( ifs: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 330..342, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 341..342, id: Name("y"), ctx: Load, @@ -597,12 +692,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 344..376, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 344..376, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 345..346, id: Name("x"), ctx: Load, @@ -611,8 +709,10 @@ Module( generators: [ Comprehension { range: 347..375, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 351..352, id: Name("x"), ctx: Store, @@ -620,6 +720,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 356..360, id: Name("data"), ctx: Load, @@ -628,19 +729,26 @@ Module( ifs: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 364..375, parameters: Some( Parameters { range: 371..372, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 371..372, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 371..372, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 371..372, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -654,6 +762,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 374..375, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap index efe0f9aaa32cb..04f72aae2310f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..43, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 42..43, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 42..43, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..43, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap index 20f04c756cc78..da496ea1cd9c4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..133, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 125..133, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 125..133, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 128..133, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("x"), ctx: Load, @@ -29,6 +34,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap index c253897cdee8f..c65c7f3f8f7f5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..141, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 131..141, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 131..141, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 132..133, value: Int( 1, @@ -26,9 +30,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 136..141, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("x"), ctx: Load, @@ -37,6 +43,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap index 9a64dd38fcd62..833765842b84f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_3.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/missing ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..140, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 114..119, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 114..119, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 1, @@ -26,6 +30,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..119, value: Int( 2, @@ -40,16 +45,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 121..140, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 125..128, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 128..130, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -60,6 +70,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 136..140, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap index a3979fcf8a6da..e3535344a6505 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/recover ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..208, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..85, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 82..85, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..83, id: Name(""), ctx: Invalid, @@ -31,13 +35,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 87..93, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 87..93, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 88..89, value: Int( 1, @@ -46,6 +53,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 91..92, value: Int( 2, @@ -60,13 +68,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 95..100, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 95..100, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 96..97, value: Int( 1, @@ -81,13 +92,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..123, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 118..123, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -96,6 +110,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 121..122, value: Int( 2, @@ -110,13 +125,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 156..162, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 156..162, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 157..158, value: Int( 1, @@ -125,6 +143,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..161, value: Int( 2, @@ -139,13 +158,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 185..194, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 185..194, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 186..187, value: Int( 1, @@ -154,9 +176,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 189..192, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("x"), ctx: Load, @@ -165,6 +189,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 192..192, id: Name(""), ctx: Invalid, @@ -180,13 +205,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 196..202, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 196..202, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 197..198, value: Int( 1, @@ -195,6 +223,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 200..201, value: Int( 2, @@ -209,16 +238,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 204..207, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 204..207, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 205..206, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 206..206, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap index 97dfb3722e30c..caa6a5a30d193 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/list/star_ex ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..200, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 84..93, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 84..93, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 86..88, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..88, id: Name("x"), ctx: Load, @@ -31,6 +36,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("y"), ctx: Load, @@ -44,19 +50,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..106, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 94..106, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 95..102, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 96..102, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("x"), ctx: Load, @@ -68,6 +79,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 101..102, id: Name("y"), ctx: Load, @@ -81,6 +93,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("z"), ctx: Load, @@ -94,20 +107,25 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 107..118, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 107..118, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 108..114, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 109..114, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("x"), ctx: Load, @@ -120,6 +138,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..117, id: Name("z"), ctx: Load, @@ -133,21 +152,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..132, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 119..132, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 120..128, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 121..128, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..122, id: Name("x"), ctx: Load, @@ -155,6 +179,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("y"), ctx: Load, @@ -168,6 +193,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 130..131, id: Name("z"), ctx: Load, @@ -181,21 +207,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..145, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 133..145, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 134..141, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 135..141, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..136, id: Name("x"), ctx: Load, @@ -203,6 +234,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("y"), ctx: Load, @@ -216,6 +248,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..144, id: Name("z"), ctx: Load, @@ -229,25 +262,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 146..168, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 146..168, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 147..164, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 148..164, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 153..157, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..149, id: Name("x"), ctx: Load, @@ -255,6 +294,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 163..164, id: Name("y"), ctx: Load, @@ -267,6 +307,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("z"), ctx: Load, @@ -280,29 +321,39 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 169..186, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 169..186, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 170..182, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 171..182, parameters: Some( Parameters { range: 178..179, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 178..179, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 178..179, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 178..179, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -316,6 +367,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 181..182, id: Name("x"), ctx: Load, @@ -328,6 +380,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..185, id: Name("z"), ctx: Load, @@ -341,19 +394,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 187..199, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 187..199, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 188..195, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 188..190, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("x"), ctx: Store, @@ -364,6 +422,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 194..195, value: Int( 2, @@ -374,6 +433,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 197..198, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap index c10ee4aac896e..e19733fe68916 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/named/invali ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..109, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..68, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 59..67, target: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 59..62, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("x"), ctx: Load, @@ -28,12 +33,14 @@ Module( attr: Identifier { id: Name("y"), range: 61..62, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 66..67, value: Int( 1, @@ -46,15 +53,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 69..80, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 70..79, target: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 70..74, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("x"), ctx: Load, @@ -62,6 +73,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("y"), ctx: Load, @@ -72,6 +84,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 1, @@ -84,15 +97,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..90, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 82..89, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 82..84, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("x"), ctx: Store, @@ -103,6 +120,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 88..89, value: Int( 1, @@ -115,16 +133,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 91..109, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 92..108, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 92..98, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("x"), ctx: Store, @@ -132,6 +154,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("y"), ctx: Store, @@ -143,10 +166,12 @@ Module( ), value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 102..108, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 103..104, value: Int( 1, @@ -155,6 +180,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 106..107, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap index 33371ac311523..8015e50f6a7c0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/named/missin ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..75, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..72, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..72, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap index 7fbfdf4dc9204..af8dfa521093d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/named/missin ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..33, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..33, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 29..33, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Store, @@ -24,6 +28,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..33, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap index 77c7726c6944e..aaa227cdfe103 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/named/missin ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..87, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 61..71, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 62..71, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Store, @@ -24,6 +28,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..71, id: Name("def"), ctx: Load, @@ -35,12 +40,15 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 72..87, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 72..77, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..75, id: Name("foo"), ctx: Load, @@ -48,6 +56,7 @@ Module( ), arguments: Arguments { range: 75..77, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -55,6 +64,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..87, id: Name("pass"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap index 1ec0d5d924e36..e7e25c0972136 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/named/missin ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..112, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 100..112, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 101..112, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 101..102, id: Name("x"), ctx: Store, @@ -24,9 +28,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 107..112, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 107..108, id: Name("x"), ctx: Load, @@ -35,6 +41,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..112, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_4.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_4.py.snap index 8f056e9ec54ba..dc699731cef50 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_4.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_4.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/named/missin ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..78, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 64..71, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 65..69, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("x"), ctx: Store, @@ -24,6 +28,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..69, id: Name(""), ctx: Invalid, @@ -35,12 +40,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 73..78, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 73..78, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("x"), ctx: Load, @@ -49,6 +57,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap index f547981164cd1..3ac0c1bdffc2c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..36, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 0..15, elt: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1..3, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("x"), ctx: Load, @@ -31,8 +36,10 @@ Module( generators: [ Comprehension { range: 4..14, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("x"), ctx: Store, @@ -40,6 +47,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("y"), ctx: Load, @@ -56,16 +64,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..24, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 16..24, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 17..23, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Store, @@ -73,6 +85,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 22..23, value: Int( 1, @@ -90,10 +103,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 25..35, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Store, @@ -101,6 +116,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap index 7b610ed0d11b4..1b1d87fe6d091 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..47, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 46..47, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..47, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap index 02ff2cf395a0f..5dee13a8a2d29 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..137, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 129..137, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 132..137, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("x"), ctx: Load, @@ -25,6 +29,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap index b793308c04bef..b5721df9676f0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..146, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 136..146, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 136..146, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 137..138, value: Int( 1, @@ -26,9 +30,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 141..146, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 141..142, id: Name("x"), ctx: Load, @@ -37,6 +43,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..146, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap index 9afe7ee6e72b1..bbf1fb9749f30 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_3.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..144, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..123, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 118..123, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -26,6 +30,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 2, @@ -41,16 +46,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 125..144, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 129..132, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 132..134, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -61,6 +71,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 140..144, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap index c61c600e25c38..9012f3e1bef88 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..125, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..70, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 67..69, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -29,9 +33,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..120, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..120, id: Name("x"), ctx: Load, @@ -41,9 +47,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 124..125, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 124..125, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap index e62134566929b..a44a6fc0663a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..267, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..86, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 83..86, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..84, id: Name(""), ctx: Invalid, @@ -32,13 +36,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 88..94, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 88..94, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 1, @@ -47,6 +54,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 92..93, value: Int( 2, @@ -62,13 +70,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..101, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 96..101, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 97..98, value: Int( 1, @@ -84,9 +95,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..121, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 120..121, value: Int( 1, @@ -97,9 +110,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..123, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 2, @@ -110,9 +125,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 157..162, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 158..159, value: Int( 1, @@ -121,6 +138,7 @@ Module( ), annotation: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 161..162, value: Int( 2, @@ -133,13 +151,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 186..195, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 186..195, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 187..188, value: Int( 1, @@ -148,9 +169,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 190..193, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 190..191, id: Name("x"), ctx: Load, @@ -159,6 +182,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..193, id: Name(""), ctx: Invalid, @@ -175,9 +199,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 197..199, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 198..199, value: Int( 1, @@ -188,9 +214,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 201..202, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 2, @@ -201,13 +229,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 255..267, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 255..267, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 255..256, id: Name("x"), ctx: Load, @@ -215,6 +246,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 258..259, id: Name("y"), ctx: Load, @@ -222,6 +254,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 263..264, value: Int( 2, @@ -230,6 +263,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..267, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap index 4b75ea06edf18..b566c6a594853 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap @@ -7,24 +7,30 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/parenthesize ``` Module( ModModule { - range: 0..536, + node_index: AtomicNodeIndex(..), + range: 0..532, body: [ Expr( StmtExpr { - range: 161..182, + node_index: AtomicNodeIndex(..), + range: 157..178, value: Tuple( ExprTuple { - range: 161..182, + node_index: AtomicNodeIndex(..), + range: 157..178, elts: [ Starred( ExprStarred { - range: 162..169, + node_index: AtomicNodeIndex(..), + range: 158..165, value: Compare( ExprCompare { - range: 163..169, + node_index: AtomicNodeIndex(..), + range: 159..165, left: Name( ExprName { - range: 163..164, + node_index: AtomicNodeIndex(..), + range: 159..160, id: Name("x"), ctx: Load, }, @@ -35,7 +41,8 @@ Module( comparators: [ Name( ExprName { - range: 168..169, + node_index: AtomicNodeIndex(..), + range: 164..165, id: Name("y"), ctx: Load, }, @@ -48,20 +55,24 @@ Module( ), Name( ExprName { - range: 171..172, + node_index: AtomicNodeIndex(..), + range: 167..168, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 174..181, + node_index: AtomicNodeIndex(..), + range: 170..177, value: Compare( ExprCompare { - range: 175..181, + node_index: AtomicNodeIndex(..), + range: 171..177, left: Name( ExprName { - range: 175..176, + node_index: AtomicNodeIndex(..), + range: 171..172, id: Name("x"), ctx: Load, }, @@ -72,7 +83,8 @@ Module( comparators: [ Name( ExprName { - range: 180..181, + node_index: AtomicNodeIndex(..), + range: 176..177, id: Name("y"), ctx: Load, }, @@ -92,21 +104,26 @@ Module( ), Expr( StmtExpr { - range: 183..202, + node_index: AtomicNodeIndex(..), + range: 179..198, value: Tuple( ExprTuple { - range: 183..202, + node_index: AtomicNodeIndex(..), + range: 179..198, elts: [ Starred( ExprStarred { - range: 184..190, + node_index: AtomicNodeIndex(..), + range: 180..186, value: UnaryOp( ExprUnaryOp { - range: 185..190, + node_index: AtomicNodeIndex(..), + range: 181..186, op: Not, operand: Name( ExprName { - range: 189..190, + node_index: AtomicNodeIndex(..), + range: 185..186, id: Name("x"), ctx: Load, }, @@ -118,21 +135,25 @@ Module( ), Name( ExprName { - range: 192..193, + node_index: AtomicNodeIndex(..), + range: 188..189, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 195..201, + node_index: AtomicNodeIndex(..), + range: 191..197, value: UnaryOp( ExprUnaryOp { - range: 196..201, + node_index: AtomicNodeIndex(..), + range: 192..197, op: Not, operand: Name( ExprName { - range: 200..201, + node_index: AtomicNodeIndex(..), + range: 196..197, id: Name("x"), ctx: Load, }, @@ -151,29 +172,35 @@ Module( ), Expr( StmtExpr { - range: 203..226, + node_index: AtomicNodeIndex(..), + range: 199..222, value: Tuple( ExprTuple { - range: 203..226, + node_index: AtomicNodeIndex(..), + range: 199..222, elts: [ Starred( ExprStarred { - range: 204..212, + node_index: AtomicNodeIndex(..), + range: 200..208, value: BoolOp( ExprBoolOp { - range: 205..212, + node_index: AtomicNodeIndex(..), + range: 201..208, op: And, values: [ Name( ExprName { - range: 205..206, + node_index: AtomicNodeIndex(..), + range: 201..202, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 211..212, + node_index: AtomicNodeIndex(..), + range: 207..208, id: Name("y"), ctx: Load, }, @@ -186,29 +213,34 @@ Module( ), Name( ExprName { - range: 214..215, + node_index: AtomicNodeIndex(..), + range: 210..211, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 217..225, + node_index: AtomicNodeIndex(..), + range: 213..221, value: BoolOp( ExprBoolOp { - range: 218..225, + node_index: AtomicNodeIndex(..), + range: 214..221, op: And, values: [ Name( ExprName { - range: 218..219, + node_index: AtomicNodeIndex(..), + range: 214..215, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 224..225, + node_index: AtomicNodeIndex(..), + range: 220..221, id: Name("y"), ctx: Load, }, @@ -228,29 +260,35 @@ Module( ), Expr( StmtExpr { - range: 227..248, + node_index: AtomicNodeIndex(..), + range: 223..244, value: Tuple( ExprTuple { - range: 227..248, + node_index: AtomicNodeIndex(..), + range: 223..244, elts: [ Starred( ExprStarred { - range: 228..235, + node_index: AtomicNodeIndex(..), + range: 224..231, value: BoolOp( ExprBoolOp { - range: 229..235, + node_index: AtomicNodeIndex(..), + range: 225..231, op: Or, values: [ Name( ExprName { - range: 229..230, + node_index: AtomicNodeIndex(..), + range: 225..226, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 234..235, + node_index: AtomicNodeIndex(..), + range: 230..231, id: Name("y"), ctx: Load, }, @@ -263,29 +301,34 @@ Module( ), Name( ExprName { - range: 237..238, + node_index: AtomicNodeIndex(..), + range: 233..234, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 240..247, + node_index: AtomicNodeIndex(..), + range: 236..243, value: BoolOp( ExprBoolOp { - range: 241..247, + node_index: AtomicNodeIndex(..), + range: 237..243, op: Or, values: [ Name( ExprName { - range: 241..242, + node_index: AtomicNodeIndex(..), + range: 237..238, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 246..247, + node_index: AtomicNodeIndex(..), + range: 242..243, id: Name("y"), ctx: Load, }, @@ -305,33 +348,40 @@ Module( ), Expr( StmtExpr { - range: 249..290, + node_index: AtomicNodeIndex(..), + range: 245..286, value: Tuple( ExprTuple { - range: 249..290, + node_index: AtomicNodeIndex(..), + range: 245..286, elts: [ Starred( ExprStarred { - range: 250..267, + node_index: AtomicNodeIndex(..), + range: 246..263, value: If( ExprIf { - range: 251..267, + node_index: AtomicNodeIndex(..), + range: 247..263, test: BooleanLiteral( ExprBooleanLiteral { - range: 256..260, + node_index: AtomicNodeIndex(..), + range: 252..256, value: true, }, ), body: Name( ExprName { - range: 251..252, + node_index: AtomicNodeIndex(..), + range: 247..248, id: Name("x"), ctx: Load, }, ), orelse: Name( ExprName { - range: 266..267, + node_index: AtomicNodeIndex(..), + range: 262..263, id: Name("y"), ctx: Load, }, @@ -343,33 +393,39 @@ Module( ), Name( ExprName { - range: 269..270, + node_index: AtomicNodeIndex(..), + range: 265..266, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 272..289, + node_index: AtomicNodeIndex(..), + range: 268..285, value: If( ExprIf { - range: 273..289, + node_index: AtomicNodeIndex(..), + range: 269..285, test: BooleanLiteral( ExprBooleanLiteral { - range: 278..282, + node_index: AtomicNodeIndex(..), + range: 274..278, value: true, }, ), body: Name( ExprName { - range: 273..274, + node_index: AtomicNodeIndex(..), + range: 269..270, id: Name("x"), ctx: Load, }, ), orelse: Name( ExprName { - range: 288..289, + node_index: AtomicNodeIndex(..), + range: 284..285, id: Name("y"), ctx: Load, }, @@ -388,29 +444,39 @@ Module( ), Expr( StmtExpr { - range: 291..322, + node_index: AtomicNodeIndex(..), + range: 287..318, value: Tuple( ExprTuple { - range: 291..322, + node_index: AtomicNodeIndex(..), + range: 287..318, elts: [ Starred( ExprStarred { - range: 292..304, + node_index: AtomicNodeIndex(..), + range: 288..300, value: Lambda( ExprLambda { - range: 293..304, + node_index: AtomicNodeIndex(..), + range: 289..300, parameters: Some( Parameters { - range: 300..301, + range: 296..297, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { - range: 300..301, + range: 296..297, + node_index: AtomicNodeIndex(..), parameter: Parameter { - range: 300..301, + range: 296..297, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), - range: 300..301, + range: 296..297, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -424,7 +490,8 @@ Module( ), body: Name( ExprName { - range: 303..304, + node_index: AtomicNodeIndex(..), + range: 299..300, id: Name("x"), ctx: Load, }, @@ -436,29 +503,38 @@ Module( ), Name( ExprName { - range: 306..307, + node_index: AtomicNodeIndex(..), + range: 302..303, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 309..321, + node_index: AtomicNodeIndex(..), + range: 305..317, value: Lambda( ExprLambda { - range: 310..321, + node_index: AtomicNodeIndex(..), + range: 306..317, parameters: Some( Parameters { - range: 317..318, + range: 313..314, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { - range: 317..318, + range: 313..314, + node_index: AtomicNodeIndex(..), parameter: Parameter { - range: 317..318, + range: 313..314, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), - range: 317..318, + range: 313..314, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -472,7 +548,8 @@ Module( ), body: Name( ExprName { - range: 320..321, + node_index: AtomicNodeIndex(..), + range: 316..317, id: Name("x"), ctx: Load, }, @@ -491,20 +568,25 @@ Module( ), Expr( StmtExpr { - range: 323..344, + node_index: AtomicNodeIndex(..), + range: 319..340, value: Tuple( ExprTuple { - range: 323..344, + node_index: AtomicNodeIndex(..), + range: 319..340, elts: [ Named( ExprNamed { - range: 324..331, + node_index: AtomicNodeIndex(..), + range: 320..327, target: Starred( ExprStarred { - range: 324..326, + node_index: AtomicNodeIndex(..), + range: 320..322, value: Name( ExprName { - range: 325..326, + node_index: AtomicNodeIndex(..), + range: 321..322, id: Name("x"), ctx: Store, }, @@ -514,7 +596,8 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { - range: 330..331, + node_index: AtomicNodeIndex(..), + range: 326..327, value: Int( 2, ), @@ -524,20 +607,24 @@ Module( ), Name( ExprName { - range: 333..334, + node_index: AtomicNodeIndex(..), + range: 329..330, id: Name("z"), ctx: Load, }, ), Named( ExprNamed { - range: 336..343, + node_index: AtomicNodeIndex(..), + range: 332..339, target: Starred( ExprStarred { - range: 336..338, + node_index: AtomicNodeIndex(..), + range: 332..334, value: Name( ExprName { - range: 337..338, + node_index: AtomicNodeIndex(..), + range: 333..334, id: Name("x"), ctx: Store, }, @@ -547,7 +634,8 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { - range: 342..343, + node_index: AtomicNodeIndex(..), + range: 338..339, value: Int( 2, ), @@ -564,20 +652,25 @@ Module( ), Expr( StmtExpr { - range: 367..386, + node_index: AtomicNodeIndex(..), + range: 363..382, value: Tuple( ExprTuple { - range: 367..386, + node_index: AtomicNodeIndex(..), + range: 363..382, elts: [ Starred( ExprStarred { - range: 367..374, + node_index: AtomicNodeIndex(..), + range: 363..370, value: Compare( ExprCompare { - range: 368..374, + node_index: AtomicNodeIndex(..), + range: 364..370, left: Name( ExprName { - range: 368..369, + node_index: AtomicNodeIndex(..), + range: 364..365, id: Name("x"), ctx: Load, }, @@ -588,7 +681,8 @@ Module( comparators: [ Name( ExprName { - range: 373..374, + node_index: AtomicNodeIndex(..), + range: 369..370, id: Name("y"), ctx: Load, }, @@ -601,20 +695,24 @@ Module( ), Name( ExprName { - range: 376..377, + node_index: AtomicNodeIndex(..), + range: 372..373, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 379..386, + node_index: AtomicNodeIndex(..), + range: 375..382, value: Compare( ExprCompare { - range: 380..386, + node_index: AtomicNodeIndex(..), + range: 376..382, left: Name( ExprName { - range: 380..381, + node_index: AtomicNodeIndex(..), + range: 376..377, id: Name("x"), ctx: Load, }, @@ -625,7 +723,8 @@ Module( comparators: [ Name( ExprName { - range: 385..386, + node_index: AtomicNodeIndex(..), + range: 381..382, id: Name("y"), ctx: Load, }, @@ -645,21 +744,26 @@ Module( ), Expr( StmtExpr { - range: 387..404, + node_index: AtomicNodeIndex(..), + range: 383..400, value: Tuple( ExprTuple { - range: 387..404, + node_index: AtomicNodeIndex(..), + range: 383..400, elts: [ Starred( ExprStarred { - range: 387..393, + node_index: AtomicNodeIndex(..), + range: 383..389, value: UnaryOp( ExprUnaryOp { - range: 388..393, + node_index: AtomicNodeIndex(..), + range: 384..389, op: Not, operand: Name( ExprName { - range: 392..393, + node_index: AtomicNodeIndex(..), + range: 388..389, id: Name("x"), ctx: Load, }, @@ -671,21 +775,25 @@ Module( ), Name( ExprName { - range: 395..396, + node_index: AtomicNodeIndex(..), + range: 391..392, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 398..404, + node_index: AtomicNodeIndex(..), + range: 394..400, value: UnaryOp( ExprUnaryOp { - range: 399..404, + node_index: AtomicNodeIndex(..), + range: 395..400, op: Not, operand: Name( ExprName { - range: 403..404, + node_index: AtomicNodeIndex(..), + range: 399..400, id: Name("x"), ctx: Load, }, @@ -704,29 +812,35 @@ Module( ), Expr( StmtExpr { - range: 405..426, + node_index: AtomicNodeIndex(..), + range: 401..422, value: Tuple( ExprTuple { - range: 405..426, + node_index: AtomicNodeIndex(..), + range: 401..422, elts: [ Starred( ExprStarred { - range: 405..413, + node_index: AtomicNodeIndex(..), + range: 401..409, value: BoolOp( ExprBoolOp { - range: 406..413, + node_index: AtomicNodeIndex(..), + range: 402..409, op: And, values: [ Name( ExprName { - range: 406..407, + node_index: AtomicNodeIndex(..), + range: 402..403, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 412..413, + node_index: AtomicNodeIndex(..), + range: 408..409, id: Name("y"), ctx: Load, }, @@ -739,29 +853,34 @@ Module( ), Name( ExprName { - range: 415..416, + node_index: AtomicNodeIndex(..), + range: 411..412, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 418..426, + node_index: AtomicNodeIndex(..), + range: 414..422, value: BoolOp( ExprBoolOp { - range: 419..426, + node_index: AtomicNodeIndex(..), + range: 415..422, op: And, values: [ Name( ExprName { - range: 419..420, + node_index: AtomicNodeIndex(..), + range: 415..416, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 425..426, + node_index: AtomicNodeIndex(..), + range: 421..422, id: Name("y"), ctx: Load, }, @@ -781,29 +900,35 @@ Module( ), Expr( StmtExpr { - range: 427..446, + node_index: AtomicNodeIndex(..), + range: 423..442, value: Tuple( ExprTuple { - range: 427..446, + node_index: AtomicNodeIndex(..), + range: 423..442, elts: [ Starred( ExprStarred { - range: 427..434, + node_index: AtomicNodeIndex(..), + range: 423..430, value: BoolOp( ExprBoolOp { - range: 428..434, + node_index: AtomicNodeIndex(..), + range: 424..430, op: Or, values: [ Name( ExprName { - range: 428..429, + node_index: AtomicNodeIndex(..), + range: 424..425, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 433..434, + node_index: AtomicNodeIndex(..), + range: 429..430, id: Name("y"), ctx: Load, }, @@ -816,29 +941,34 @@ Module( ), Name( ExprName { - range: 436..437, + node_index: AtomicNodeIndex(..), + range: 432..433, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 439..446, + node_index: AtomicNodeIndex(..), + range: 435..442, value: BoolOp( ExprBoolOp { - range: 440..446, + node_index: AtomicNodeIndex(..), + range: 436..442, op: Or, values: [ Name( ExprName { - range: 440..441, + node_index: AtomicNodeIndex(..), + range: 436..437, id: Name("x"), ctx: Load, }, ), Name( ExprName { - range: 445..446, + node_index: AtomicNodeIndex(..), + range: 441..442, id: Name("y"), ctx: Load, }, @@ -858,33 +988,40 @@ Module( ), Expr( StmtExpr { - range: 447..486, + node_index: AtomicNodeIndex(..), + range: 443..482, value: Tuple( ExprTuple { - range: 447..486, + node_index: AtomicNodeIndex(..), + range: 443..482, elts: [ Starred( ExprStarred { - range: 447..464, + node_index: AtomicNodeIndex(..), + range: 443..460, value: If( ExprIf { - range: 448..464, + node_index: AtomicNodeIndex(..), + range: 444..460, test: BooleanLiteral( ExprBooleanLiteral { - range: 453..457, + node_index: AtomicNodeIndex(..), + range: 449..453, value: true, }, ), body: Name( ExprName { - range: 448..449, + node_index: AtomicNodeIndex(..), + range: 444..445, id: Name("x"), ctx: Load, }, ), orelse: Name( ExprName { - range: 463..464, + node_index: AtomicNodeIndex(..), + range: 459..460, id: Name("y"), ctx: Load, }, @@ -896,33 +1033,39 @@ Module( ), Name( ExprName { - range: 466..467, + node_index: AtomicNodeIndex(..), + range: 462..463, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 469..486, + node_index: AtomicNodeIndex(..), + range: 465..482, value: If( ExprIf { - range: 470..486, + node_index: AtomicNodeIndex(..), + range: 466..482, test: BooleanLiteral( ExprBooleanLiteral { - range: 475..479, + node_index: AtomicNodeIndex(..), + range: 471..475, value: true, }, ), body: Name( ExprName { - range: 470..471, + node_index: AtomicNodeIndex(..), + range: 466..467, id: Name("x"), ctx: Load, }, ), orelse: Name( ExprName { - range: 485..486, + node_index: AtomicNodeIndex(..), + range: 481..482, id: Name("y"), ctx: Load, }, @@ -941,29 +1084,39 @@ Module( ), Expr( StmtExpr { - range: 487..516, + node_index: AtomicNodeIndex(..), + range: 483..512, value: Tuple( ExprTuple { - range: 487..516, + node_index: AtomicNodeIndex(..), + range: 483..512, elts: [ Starred( ExprStarred { - range: 487..499, + node_index: AtomicNodeIndex(..), + range: 483..495, value: Lambda( ExprLambda { - range: 488..499, + node_index: AtomicNodeIndex(..), + range: 484..495, parameters: Some( Parameters { - range: 495..496, + range: 491..492, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { - range: 495..496, + range: 491..492, + node_index: AtomicNodeIndex(..), parameter: Parameter { - range: 495..496, + range: 491..492, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), - range: 495..496, + range: 491..492, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -977,7 +1130,8 @@ Module( ), body: Name( ExprName { - range: 498..499, + node_index: AtomicNodeIndex(..), + range: 494..495, id: Name("x"), ctx: Load, }, @@ -989,29 +1143,38 @@ Module( ), Name( ExprName { - range: 501..502, + node_index: AtomicNodeIndex(..), + range: 497..498, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 504..516, + node_index: AtomicNodeIndex(..), + range: 500..512, value: Lambda( ExprLambda { - range: 505..516, + node_index: AtomicNodeIndex(..), + range: 501..512, parameters: Some( Parameters { - range: 512..513, + range: 508..509, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { - range: 512..513, + range: 508..509, + node_index: AtomicNodeIndex(..), parameter: Parameter { - range: 512..513, + range: 508..509, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), - range: 512..513, + range: 508..509, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1025,7 +1188,8 @@ Module( ), body: Name( ExprName { - range: 515..516, + node_index: AtomicNodeIndex(..), + range: 511..512, id: Name("x"), ctx: Load, }, @@ -1044,13 +1208,16 @@ Module( ), Expr( StmtExpr { - range: 517..519, + node_index: AtomicNodeIndex(..), + range: 513..515, value: Starred( ExprStarred { - range: 517..519, + node_index: AtomicNodeIndex(..), + range: 513..515, value: Name( ExprName { - range: 518..519, + node_index: AtomicNodeIndex(..), + range: 514..515, id: Name("x"), ctx: Load, }, @@ -1062,14 +1229,17 @@ Module( ), Expr( StmtExpr { - range: 523..536, + node_index: AtomicNodeIndex(..), + range: 519..532, value: Tuple( ExprTuple { - range: 523..536, + node_index: AtomicNodeIndex(..), + range: 519..532, elts: [ NumberLiteral( ExprNumberLiteral { - range: 523..524, + node_index: AtomicNodeIndex(..), + range: 519..520, value: Int( 2, ), @@ -1077,17 +1247,20 @@ Module( ), Name( ExprName { - range: 526..527, + node_index: AtomicNodeIndex(..), + range: 522..523, id: Name("z"), ctx: Load, }, ), Starred( ExprStarred { - range: 529..531, + node_index: AtomicNodeIndex(..), + range: 525..527, value: Name( ExprName { - range: 530..531, + node_index: AtomicNodeIndex(..), + range: 526..527, id: Name("x"), ctx: Load, }, @@ -1097,7 +1270,8 @@ Module( ), NumberLiteral( ExprNumberLiteral { - range: 535..536, + node_index: AtomicNodeIndex(..), + range: 531..532, value: Int( 2, ), @@ -1117,7 +1291,7 @@ Module( ## Errors | -2 | # Test the first and any other element as the there are two separate calls. +2 | # Test the first and any other element as there are two separate calls. 3 | 4 | (*x in y, z, *x in y) | ^^^^^^ Syntax Error: Comparison expression cannot be used here @@ -1127,7 +1301,7 @@ Module( | -2 | # Test the first and any other element as the there are two separate calls. +2 | # Test the first and any other element as there are two separate calls. 3 | 4 | (*x in y, z, *x in y) | ^^^^^^ Syntax Error: Comparison expression cannot be used here diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap index b692643db9843..abf42e4b03000 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/comprehe ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..377, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 33..48, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 33..48, elt: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 34..36, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("x"), ctx: Load, @@ -31,8 +36,10 @@ Module( generators: [ Comprehension { range: 37..47, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Store, @@ -40,6 +47,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("y"), ctx: Load, @@ -55,12 +63,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..81, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 67..81, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -69,8 +80,10 @@ Module( generators: [ Comprehension { range: 70..80, + node_index: AtomicNodeIndex(..), target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 74..75, value: Int( 1, @@ -79,6 +92,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..80, id: Name("y"), ctx: Load, @@ -94,12 +108,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..98, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 82..98, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("x"), ctx: Load, @@ -108,13 +125,16 @@ Module( generators: [ Comprehension { range: 85..97, + node_index: AtomicNodeIndex(..), target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 89..92, value: StringLiteralValue { inner: Single( StringLiteral { range: 89..92, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Single, @@ -128,6 +148,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("y"), ctx: Load, @@ -143,12 +164,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 99..118, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 99..118, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("x"), ctx: Load, @@ -157,11 +181,14 @@ Module( generators: [ Comprehension { range: 102..117, + node_index: AtomicNodeIndex(..), target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 106..112, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..110, id: Name("call"), ctx: Load, @@ -169,6 +196,7 @@ Module( ), arguments: Arguments { range: 110..112, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -176,6 +204,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..117, id: Name("y"), ctx: Load, @@ -191,12 +220,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..138, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 119..138, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("x"), ctx: Load, @@ -205,12 +237,15 @@ Module( generators: [ Comprehension { range: 122..137, + node_index: AtomicNodeIndex(..), target: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 126..132, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("a"), ctx: Load, @@ -218,6 +253,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 130..131, id: Name("b"), ctx: Load, @@ -228,6 +264,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("y"), ctx: Load, @@ -243,12 +280,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 155..170, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 155..170, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("x"), ctx: Load, @@ -257,8 +297,10 @@ Module( generators: [ Comprehension { range: 158..169, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 162..163, id: Name("x"), ctx: Store, @@ -266,9 +308,11 @@ Module( ), iter: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 167..169, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 168..169, id: Name("y"), ctx: Load, @@ -287,12 +331,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 171..191, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 171..191, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 172..173, id: Name("x"), ctx: Load, @@ -301,8 +348,10 @@ Module( generators: [ Comprehension { range: 174..190, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("x"), ctx: Store, @@ -310,10 +359,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 183..190, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("y"), ctx: Load, @@ -332,12 +383,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 192..217, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 192..217, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..194, id: Name("x"), ctx: Load, @@ -346,8 +400,10 @@ Module( generators: [ Comprehension { range: 195..216, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..200, id: Name("x"), ctx: Store, @@ -355,9 +411,11 @@ Module( ), iter: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 204..216, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 215..216, id: Name("y"), ctx: Load, @@ -375,12 +433,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 218..242, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 218..242, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 219..220, id: Name("x"), ctx: Load, @@ -389,8 +450,10 @@ Module( generators: [ Comprehension { range: 221..241, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 225..226, id: Name("x"), ctx: Store, @@ -398,19 +461,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 230..241, parameters: Some( Parameters { range: 237..238, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 237..238, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 237..238, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 237..238, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -424,6 +494,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 240..241, id: Name("y"), ctx: Load, @@ -441,12 +512,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 257..280, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 257..280, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 258..259, id: Name("x"), ctx: Load, @@ -455,8 +529,10 @@ Module( generators: [ Comprehension { range: 260..279, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 264..265, id: Name("x"), ctx: Store, @@ -464,6 +540,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 269..273, id: Name("data"), ctx: Load, @@ -472,9 +549,11 @@ Module( ifs: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 277..279, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 278..279, id: Name("y"), ctx: Load, @@ -493,12 +572,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 281..309, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 281..309, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 282..283, id: Name("x"), ctx: Load, @@ -507,8 +589,10 @@ Module( generators: [ Comprehension { range: 284..308, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 288..289, id: Name("x"), ctx: Store, @@ -516,6 +600,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 293..297, id: Name("data"), ctx: Load, @@ -524,10 +609,12 @@ Module( ifs: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 301..308, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 307..308, id: Name("y"), ctx: Load, @@ -546,12 +633,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 310..343, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 310..343, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 311..312, id: Name("x"), ctx: Load, @@ -560,8 +650,10 @@ Module( generators: [ Comprehension { range: 313..342, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 317..318, id: Name("x"), ctx: Store, @@ -569,6 +661,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 322..326, id: Name("data"), ctx: Load, @@ -577,9 +670,11 @@ Module( ifs: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 330..342, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 341..342, id: Name("y"), ctx: Load, @@ -597,12 +692,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 344..376, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 344..376, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 345..346, id: Name("x"), ctx: Load, @@ -611,8 +709,10 @@ Module( generators: [ Comprehension { range: 347..375, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 351..352, id: Name("x"), ctx: Store, @@ -620,6 +720,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 356..360, id: Name("data"), ctx: Load, @@ -628,19 +729,26 @@ Module( ifs: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 364..375, parameters: Some( Parameters { range: 371..372, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 371..372, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 371..372, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 371..372, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -654,6 +762,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 374..375, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap index 6b082389bac73..43438af0b7046 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/missing_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..47, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 46..47, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 46..47, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..47, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap index 3df9492f91909..b1a51ec0e1580 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/missing_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..136, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 128..136, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 128..136, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 131..136, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..132, id: Name("x"), ctx: Load, @@ -29,6 +34,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..136, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap index 101fbf0b1ab90..ee7e0553f36db 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/missing_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..144, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 134..144, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 134..144, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 135..136, value: Int( 1, @@ -26,9 +30,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 139..144, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("x"), ctx: Load, @@ -37,6 +43,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..144, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap index f0ac8ba0d955f..dd12017d4f61c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_3.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/missing_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..144, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..123, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 118..123, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -26,6 +30,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 2, @@ -39,16 +44,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 125..144, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 129..132, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 132..134, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -59,6 +69,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 140..144, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap index 6df00c1bc5d31..fd02629ffdc7f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/recover. ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..323, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 197..200, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 197..200, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 198..198, id: Name(""), ctx: Invalid, @@ -30,13 +34,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 202..208, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 202..208, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 203..204, value: Int( 1, @@ -45,6 +52,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 206..207, value: Int( 2, @@ -58,13 +66,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 210..215, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 210..215, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 211..212, value: Int( 1, @@ -78,13 +89,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 233..238, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 233..238, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 234..235, value: Int( 1, @@ -93,6 +107,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 236..237, value: Int( 2, @@ -106,15 +121,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 271..277, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 271..277, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 272..273, value: Int( 1, @@ -124,6 +142,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 275..276, value: Int( 2, @@ -138,13 +157,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 300..309, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 300..309, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 301..302, value: Int( 1, @@ -153,9 +175,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 304..307, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 304..305, id: Name("x"), ctx: Load, @@ -164,6 +188,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 307..307, id: Name(""), ctx: Invalid, @@ -178,13 +203,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 311..317, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 311..317, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 312..313, value: Int( 1, @@ -193,6 +221,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 315..316, value: Int( 2, @@ -206,16 +235,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 319..322, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 319..322, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 320..321, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 321..321, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap index 5fd3b81e29e4f..18fb551839be9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/set/star_exp ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..198, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..92, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 83..92, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 85..87, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..87, id: Name("x"), ctx: Load, @@ -31,6 +36,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("y"), ctx: Load, @@ -43,19 +49,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..105, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 93..105, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 94..101, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 95..101, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..96, id: Name("x"), ctx: Load, @@ -67,6 +78,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("y"), ctx: Load, @@ -80,6 +92,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 103..104, id: Name("z"), ctx: Load, @@ -92,20 +105,25 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 106..117, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 106..117, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 107..113, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 108..113, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 112..113, id: Name("x"), ctx: Load, @@ -118,6 +136,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 115..116, id: Name("z"), ctx: Load, @@ -130,21 +149,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..131, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 118..131, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 119..127, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 120..127, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("x"), ctx: Load, @@ -152,6 +176,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..127, id: Name("y"), ctx: Load, @@ -165,6 +190,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("z"), ctx: Load, @@ -177,21 +203,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 132..144, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 132..144, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 133..140, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 134..140, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..135, id: Name("x"), ctx: Load, @@ -199,6 +230,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("y"), ctx: Load, @@ -212,6 +244,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..143, id: Name("z"), ctx: Load, @@ -224,25 +257,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 145..167, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 145..167, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 146..163, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 147..163, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 152..156, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 147..148, id: Name("x"), ctx: Load, @@ -250,6 +289,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 162..163, id: Name("y"), ctx: Load, @@ -262,6 +302,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..166, id: Name("z"), ctx: Load, @@ -274,29 +315,39 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 168..185, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 168..185, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 169..181, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 170..181, parameters: Some( Parameters { range: 177..178, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 177..178, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 177..178, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 177..178, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -310,6 +361,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 180..181, id: Name("x"), ctx: Load, @@ -322,6 +374,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 183..184, id: Name("z"), ctx: Load, @@ -334,19 +387,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 186..198, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 186..198, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 187..194, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 187..189, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 188..189, id: Name("x"), ctx: Store, @@ -357,6 +415,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 193..194, value: Int( 2, @@ -367,6 +426,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 196..197, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap index 598a1ba08e83a..133fc59208017 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/subscript/in ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..133, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 0..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -24,13 +28,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 2..9, lower: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2..8, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("x"), ctx: Store, @@ -38,6 +45,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 1, @@ -58,12 +66,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 33..39, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 33..39, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..34, id: Name("x"), ctx: Load, @@ -71,13 +82,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 35..38, lower: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 35..37, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("x"), ctx: Load, @@ -98,12 +112,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..46, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 40..46, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("x"), ctx: Load, @@ -111,14 +128,17 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 42..45, lower: None, upper: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 43..45, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Load, @@ -138,12 +158,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 47..54, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 47..54, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Load, @@ -151,15 +174,18 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 49..53, lower: None, upper: None, step: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 51..53, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..53, id: Name("x"), ctx: Load, @@ -178,12 +204,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..73, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 70..73, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("x"), ctx: Load, @@ -191,6 +220,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name(""), ctx: Invalid, @@ -203,12 +233,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 123..133, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 123..133, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 123..124, id: Name("x"), ctx: Load, @@ -216,12 +249,15 @@ Module( ), slice: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 125..132, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 125..127, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..127, id: Name("x"), ctx: Store, @@ -232,6 +268,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 131..132, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap index 6491277ca7bcb..734e357a4ab3d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/subscript/un ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 0..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -24,14 +28,17 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 2..10, lower: None, upper: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, @@ -40,6 +47,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap index 10505cb6cb40c..0d95577060488 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/subscript/un ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..25, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 0..9, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Load, @@ -24,12 +28,14 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 2..9, lower: None, upper: None, step: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("def"), ctx: Load, @@ -45,12 +51,15 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 10..25, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 10..15, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..13, id: Name("foo"), ctx: Load, @@ -58,6 +67,7 @@ Module( ), arguments: Arguments { range: 13..15, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -65,6 +75,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..25, id: Name("pass"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap index 379c262e15bd5..f024ef0b37aa4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/unary.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 0..5, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, @@ -30,9 +33,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..10, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap index feeb3a56e814f..952700a605f50 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/expressions/unary/named_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..18, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..2, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 0..2, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -30,9 +33,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 6..7, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 1, @@ -43,13 +48,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..13, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 8..13, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..13, id: Name("x"), ctx: Load, @@ -61,9 +69,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..18, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 17..18, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_0.py.snap index 3ea59b57e8614..90fbf1bd9f05b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_0.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/unary/no_exp ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..3, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 0..3, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -29,12 +33,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..10, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Load, @@ -43,6 +50,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_1.py.snap index 62d13301c9fa8..2b9353fc3b7a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__no_expression_1.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/unary/no_exp ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..8, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..1, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 0..1, op: UAdd, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..1, id: Name(""), ctx: Invalid, @@ -29,12 +33,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3..8, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 3..8, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, @@ -43,6 +50,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap index 84e07f14e90b9..73536890eca5f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/yield/named_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..85, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 52..59, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 52..59, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("x"), ctx: Load, @@ -30,9 +34,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 63..64, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 63..64, value: Int( 1, @@ -43,17 +49,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..84, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 66..84, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 72..84, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 72..73, value: Int( 1, @@ -62,6 +72,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("x"), ctx: Load, @@ -69,6 +80,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 80..81, value: Int( 2, @@ -77,6 +89,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap index 2a976eb9a7eb1..6bd430e889b2c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/yield/star_e ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..67, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 37..47, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 37..47, value: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 44..46, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..46, id: Name("x"), ctx: Load, @@ -36,25 +41,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..66, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 49..66, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 55..66, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 55..63, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 56..63, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Load, @@ -62,6 +73,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("y"), ctx: Load, @@ -75,6 +87,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("z"), ctx: Load, @@ -118,7 +131,7 @@ Module( | 1 | # Cannot use starred expression here 2 | yield (*x) - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here 3 | 4 | yield *x and y, z | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap index 4adf70d811ce3..72433f9e7736f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/yield_from/s ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..100, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..83, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 70..83, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 81..83, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("x"), ctx: Load, @@ -34,19 +39,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 84..100, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 84..100, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 95..100, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 95..97, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("x"), ctx: Load, @@ -57,6 +67,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap index 40ffb45e66079..a14264ccfbb3b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/expressions/yield_from/u ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..192, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..47, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 35..47, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("x"), ctx: Load, @@ -28,9 +32,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 51..52, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, @@ -41,16 +47,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 89..104, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 89..104, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 100..104, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("x"), ctx: Load, @@ -58,6 +68,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 103..104, id: Name("y"), ctx: Load, @@ -74,16 +85,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 168..192, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 168..192, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 179..192, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 180..181, id: Name("x"), ctx: Load, @@ -91,14 +106,17 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 183..191, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 184..191, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..185, id: Name("x"), ctx: Load, @@ -106,6 +124,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 190..191, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap new file mode 100644 index 0000000000000..f3725bcbab3fb --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap @@ -0,0 +1,186 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/f_string_conversion_follows_exclamation.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..30, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 0..9, + value: FString( + ExprFString { + node_index: AtomicNodeIndex(..), + range: 0..9, + value: FStringValue { + inner: Single( + FString( + FString { + range: 0..9, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 2..8, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 10..19, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 10..19, + value: TStringValue { + inner: Single( + TString( + TString { + range: 10..19, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 12..18, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 13..14, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 20..29, + value: FString( + ExprFString { + node_index: AtomicNodeIndex(..), + range: 20..29, + value: FStringValue { + inner: Single( + FString( + FString { + range: 20..29, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 22..28, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 23..24, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | f"{x! s}" + | ^ Syntax Error: f-string: conversion type must come right after the exclamation mark +2 | t"{x! s}" +3 | f"{x! z}" + | + + + | +1 | f"{x! s}" +2 | t"{x! s}" + | ^ Syntax Error: t-string: conversion type must come right after the exclamation mark +3 | f"{x! z}" + | + + + | +1 | f"{x! s}" +2 | t"{x! s}" +3 | f"{x! z}" + | ^ Syntax Error: f-string: conversion type must come right after the exclamation mark + | + + + | +1 | f"{x! s}" +2 | t"{x! s}" +3 | f"{x! z}" + | ^ Syntax Error: f-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap index 76a48f2197271..42acb3033d9a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_empty_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..14, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..5, value: FStringValue { inner: Single( FString( FString { range: 0..5, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..4, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -53,21 +58,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 6..13, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 6..13, value: FStringValue { inner: Single( FString( FString { range: 6..13, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..12, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..9, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap index 4a635ad345086..0858ce958d9d1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_conversion_flag_name_tok.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..9, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..8, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..8, value: FStringValue { inner: Single( FString( FString { range: 0..8, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..7, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap index 2f36142537edc..165a659e99d4d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_conversion_flag_other_tok.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..22, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..10, value: FStringValue { inner: Single( FString( FString { range: 0..10, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, @@ -53,21 +58,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..21, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 11..21, value: FStringValue { inner: Single( FString( FString { range: 11..21, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 13..20, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap index e02a5c22b872e..acdb317e91fe5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap @@ -1,35 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_starred_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..112, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 77..83, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 77..83, value: FStringValue { inner: Single( FString( FString { range: 77..83, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 79..82, + node_index: AtomicNodeIndex(..), expression: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 80..81, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..81, id: Name(""), ctx: Invalid, @@ -59,29 +65,36 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 84..97, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 84..97, value: FStringValue { inner: Single( FString( FString { range: 84..97, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 86..96, + node_index: AtomicNodeIndex(..), expression: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 87..95, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 88..95, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..89, id: Name("x"), ctx: Load, @@ -89,6 +102,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 94..95, id: Name("y"), ctx: Load, @@ -121,28 +135,35 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 98..111, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 98..111, value: FStringValue { inner: Single( FString( FString { range: 98..111, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 100..110, + node_index: AtomicNodeIndex(..), expression: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 101..109, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 102..109, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap index 05f1e2e3ff779..6f98814ce9831 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap @@ -1,45 +1,56 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_lambda_without_parentheses.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..17, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..16, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..16, value: FStringValue { inner: Single( FString( FString { range: 0..16, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, + node_index: AtomicNodeIndex(..), expression: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 3..12, parameters: Some( Parameters { range: 10..11, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 10..11, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 10..11, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 10..11, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -53,6 +64,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..12, id: Name(""), ctx: Invalid, @@ -66,8 +78,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..14, + node_index: AtomicNodeIndex(..), value: " x", }, ), @@ -111,5 +124,5 @@ Module( | 1 | f"{lambda x: x}" - | ^ Syntax Error: Expected an f-string element or the end of the f-string + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap index 7714d9e16286f..62c1efddfb713 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap @@ -7,25 +7,31 @@ input_file: crates/ruff_python_parser/resources/inline/err/f_string_unclosed_lbr ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..38, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..4, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..4, value: FStringValue { inner: Single( FString( FString { range: 0..4, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..3, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..3, id: Name(""), ctx: Invalid, @@ -52,21 +58,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..14, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 5..14, value: FStringValue { inner: Single( FString( FString { range: 5..14, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..14, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..11, id: Name("foo"), ctx: Load, @@ -93,21 +104,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..23, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 15..23, value: FStringValue { inner: Single( FString( FString { range: 15..23, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 17..22, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..21, id: Name("foo"), ctx: Load, @@ -139,9 +155,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..37, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 24..37, value: FStringValue { inner: Concatenated( @@ -149,12 +167,15 @@ Module( FString( FString { range: 24..28, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 26..27, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..27, id: Name(""), ctx: Invalid, @@ -176,12 +197,15 @@ Module( FString( FString { range: 29..37, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 33..34, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..34, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap index c72b2029e9572..9d97896c805f1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap @@ -1,38 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_unclosed_lbrace_in_format_spec.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..29, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..12, value: FStringValue { inner: Single( FString( FString { range: 0..12, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..8, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..11, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("x"), ctx: Load, @@ -41,8 +47,9 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 11..11, + node_index: AtomicNodeIndex(..), elements: [], }, ), @@ -64,27 +71,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..28, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 13..28, value: FStringValue { inner: Single( FString( FString { range: 13..28, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 15..21, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 21..27, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -93,12 +106,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 24..27, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 24..27, + node_index: AtomicNodeIndex(..), value: ".3f", }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap index f4a68c5000ba0..cf2cb1c90be85 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/for_iter_unpack_py38. ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..106, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 43..63, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -22,13 +25,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 52..58, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 52..54, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("a"), ctx: Load, @@ -39,6 +45,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("b"), ctx: Load, @@ -52,9 +59,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 60..63, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 60..63, }, ), @@ -66,10 +75,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 64..84, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Store, @@ -77,10 +88,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 74..79, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("a"), ctx: Load, @@ -88,9 +101,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 77..79, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..79, id: Name("b"), ctx: Load, @@ -107,9 +122,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..84, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, }, ), @@ -121,10 +138,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 85..105, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("x"), ctx: Store, @@ -132,13 +151,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 94..100, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 94..96, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..96, id: Name("a"), ctx: Load, @@ -149,9 +171,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 98..100, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("b"), ctx: Load, @@ -168,9 +192,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 102..105, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 102..105, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap index cf0600a2cfbbe..2520538fcc2f7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_iter ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..71, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..22, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Store, @@ -22,14 +25,17 @@ Module( ), iter: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 9..17, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 10..17, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("a"), ctx: Load, @@ -37,6 +43,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("b"), ctx: Load, @@ -51,9 +58,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -65,10 +74,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 23..44, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Store, @@ -76,10 +87,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 32..39, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("a"), ctx: Load, @@ -91,9 +104,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 41..44, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 41..44, }, ), @@ -105,10 +120,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 45..60, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..55, id: Name("target"), ctx: Store, @@ -116,6 +133,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("x"), ctx: Load, @@ -127,9 +145,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 64..70, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 64..65, value: Int( 1, @@ -138,6 +158,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 67..70, }, ), @@ -187,7 +208,7 @@ Module( | 1 | for x in *a and b: ... - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here 2 | for x in yield a: ... 3 | for target in x := 1: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap index 547334181f0eb..491fd41a42e14 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_targ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..154, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..15, is_async: false, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..5, value: Int( 1, @@ -23,6 +26,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("x"), ctx: Load, @@ -31,9 +35,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..15, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 12..15, }, ), @@ -45,15 +51,18 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 16..33, is_async: false, target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, value: StringLiteralValue { inner: Single( StringLiteral { range: 20..23, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -67,6 +76,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Load, @@ -75,9 +85,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..33, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 30..33, }, ), @@ -89,18 +101,22 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 34..56, is_async: false, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 38..46, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 39..46, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("x"), ctx: Load, @@ -108,6 +124,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..46, id: Name("y"), ctx: Load, @@ -121,6 +138,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 50..51, id: Name("z"), ctx: Load, @@ -129,9 +147,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..56, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 53..56, }, ), @@ -143,16 +163,20 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 57..77, is_async: false, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 61..67, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 62..67, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Load, @@ -161,6 +185,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("y"), ctx: Load, @@ -173,6 +198,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..72, id: Name("z"), ctx: Load, @@ -181,9 +207,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 74..77, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 74..77, }, ), @@ -195,13 +223,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 78..99, is_async: false, target: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 82..89, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..89, id: Name("x"), ctx: Load, @@ -211,6 +242,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("z"), ctx: Load, @@ -219,9 +251,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), @@ -233,17 +267,21 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 100..121, is_async: false, target: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 104..116, value: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 110..116, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 110..111, id: Name("x"), ctx: Load, @@ -255,6 +293,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 115..116, id: Name("y"), ctx: Load, @@ -268,6 +307,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..116, id: Name(""), ctx: Invalid, @@ -276,9 +316,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..121, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 118..121, }, ), @@ -290,14 +332,17 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 122..153, is_async: false, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 126..143, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("x"), ctx: Store, @@ -305,6 +350,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 130..131, value: Int( 1, @@ -313,6 +359,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..134, id: Name("y"), ctx: Store, @@ -320,18 +367,22 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 136..142, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 137..142, elts: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 138..141, value: StringLiteralValue { inner: Single( StringLiteral { range: 138..141, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -356,6 +407,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 147..148, id: Name("z"), ctx: Load, @@ -364,9 +416,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 150..153, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 150..153, }, ), @@ -469,7 +523,7 @@ Module( 1 | for 1 in x: ... 2 | for "a" in x: ... 3 | for *x and y in z: ... - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here 4 | for *x | y in z: ... 5 | for await x in z: ... | @@ -479,7 +533,7 @@ Module( 2 | for "a" in x: ... 3 | for *x and y in z: ... 4 | for *x | y in z: ... - | ^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^ Syntax Error: Starred expression cannot be used here 5 | for await x in z: ... 6 | for yield x in y: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap index 0a6052ee9de52..cb545e9b38b35 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_target_binary_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..124, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..24, is_async: false, target: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 4..14, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, @@ -30,6 +33,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("y"), ctx: Load, @@ -40,6 +44,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("z"), ctx: Load, @@ -48,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..24, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 21..24, }, ), @@ -62,13 +69,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 25..45, is_async: false, target: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 29..35, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Load, @@ -80,6 +90,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("y"), ctx: Load, @@ -90,6 +101,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("z"), ctx: Load, @@ -98,9 +110,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 42..45, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 42..45, }, ), @@ -112,15 +126,18 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 46..66, is_async: false, target: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 50..56, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 50..51, id: Name("x"), ctx: Load, @@ -128,6 +145,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..56, id: Name("y"), ctx: Load, @@ -138,6 +156,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("z"), ctx: Load, @@ -146,9 +165,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 63..66, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 63..66, }, ), @@ -160,14 +181,17 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 67..83, is_async: false, target: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 71..73, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("x"), ctx: Store, @@ -177,6 +201,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("y"), ctx: Load, @@ -185,9 +210,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 80..83, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 80..83, }, ), @@ -199,14 +226,17 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 84..103, is_async: false, target: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 88..93, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..93, id: Name("x"), ctx: Store, @@ -216,6 +246,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("y"), ctx: Load, @@ -224,9 +255,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 100..103, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 100..103, }, ), @@ -238,13 +271,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 104..123, is_async: false, target: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 108..113, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("x"), ctx: Load, @@ -253,6 +289,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 112..113, id: Name("y"), ctx: Load, @@ -262,6 +299,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 117..118, id: Name("z"), ctx: Load, @@ -270,9 +308,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 120..123, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 120..123, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap index 6e47652cc1177..185ab4b7a7426 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_target_in_keyword.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..170, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 4..13, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("d"), ctx: Load, @@ -26,12 +29,15 @@ Module( ), arguments: Arguments { range: 5..13, + node_index: AtomicNodeIndex(..), args: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 6..12, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -43,6 +49,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("y"), ctx: Load, @@ -58,6 +65,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..23, id: Name("target"), ctx: Load, @@ -66,9 +74,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -80,16 +90,20 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 29..56, is_async: false, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 33..43, func: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 34..40, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("x"), ctx: Load, @@ -101,6 +115,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("y"), ctx: Load, @@ -111,6 +126,7 @@ Module( ), arguments: Arguments { range: 41..43, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -118,6 +134,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..51, id: Name("iter"), ctx: Load, @@ -126,9 +143,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..56, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 53..56, }, ), @@ -140,13 +159,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 57..82, is_async: false, target: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 62..68, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Load, @@ -158,6 +180,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("y"), ctx: Load, @@ -168,6 +191,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..77, id: Name("iter"), ctx: Load, @@ -176,9 +200,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 79..82, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 79..82, }, ), @@ -190,17 +216,21 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 83..111, is_async: false, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 87..98, elts: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 88..94, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..89, id: Name("x"), ctx: Load, @@ -212,6 +242,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("y"), ctx: Load, @@ -222,6 +253,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("z"), ctx: Store, @@ -234,6 +266,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..106, id: Name("iter"), ctx: Load, @@ -242,9 +275,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 108..111, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 108..111, }, ), @@ -256,17 +291,21 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 112..140, is_async: false, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 116..127, elts: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 117..123, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 117..118, id: Name("x"), ctx: Load, @@ -278,6 +317,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..123, id: Name("y"), ctx: Load, @@ -288,6 +328,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 125..126, id: Name("z"), ctx: Store, @@ -299,6 +340,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..135, id: Name("iter"), ctx: Load, @@ -307,9 +349,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 137..140, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 137..140, }, ), @@ -321,17 +365,21 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 141..169, is_async: false, target: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 145..156, elts: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 146..152, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("x"), ctx: Load, @@ -343,6 +391,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..152, id: Name("y"), ctx: Load, @@ -353,6 +402,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 154..155, id: Name("z"), ctx: Load, @@ -363,6 +413,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..164, id: Name("iter"), ctx: Load, @@ -371,9 +422,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 166..169, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 166..169, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap index 3390046c352a1..d16e1ec4bc4f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_missing_in_keyword.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..12, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("a"), ctx: Store, @@ -23,6 +25,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("b"), ctx: Load, @@ -31,9 +34,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..12, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 9..12, }, ), @@ -45,10 +50,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 13..23, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("a"), ctx: Store, @@ -56,6 +63,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..18, id: Name(""), ctx: Invalid, @@ -64,9 +72,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..23, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_iter.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_iter.py.snap index 240f900f06f95..d258342309078 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_iter.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_iter.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_missing_iter.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..20, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..19, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Store, @@ -23,6 +25,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..8, id: Name(""), ctx: Invalid, @@ -31,10 +34,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 14..19, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("a"), ctx: Store, @@ -43,6 +48,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap index 3c819fc4722e6..42ea10ed9f812 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_missing_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..14, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..13, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..6, id: Name("in"), ctx: Store, @@ -23,6 +25,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, @@ -31,9 +34,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..13, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 10..13, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap index 36a0203b9b6f3..2c98db482e7a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/from_import_dotted_names.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..67, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..16, module: Some( Identifier { id: Name("x"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 14..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 14..15, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -34,27 +38,33 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 17..34, module: Some( Identifier { id: Name("x"), range: 22..23, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 31..32, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 31..32, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 33..34, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 33..34, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -64,67 +74,83 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 35..66, module: Some( Identifier { id: Name("x"), range: 40..41, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 49..50, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 49..50, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 52..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 52..53, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 54..55, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 54..55, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 57..58, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 57..58, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 60..61, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 60..61, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 62..63, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 62..63, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 65..66, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("g"), range: 65..66, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap index fcc56371ed6d1..b84eb9a41acc9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap @@ -1,22 +1,24 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/from_import_empty_names.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..48, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..13, module: Some( Identifier { id: Name("x"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [], @@ -25,11 +27,13 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 14..30, module: Some( Identifier { id: Name("x"), range: 19..20, + node_index: AtomicNodeIndex(..), }, ), names: [], @@ -38,11 +42,13 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 31..47, module: Some( Identifier { id: Name("x"), range: 36..37, + node_index: AtomicNodeIndex(..), }, ), names: [], diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap index fcf455d6ba23d..9e3cbe4994892 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap @@ -1,17 +1,18 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/from_import_missing_module.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..19, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..4, module: None, names: [], @@ -20,14 +21,17 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 5..18, module: None, names: [ Alias { range: 17..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 17..18, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap index 582bd2f14ec70..761cbc2a7ec69 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_rpar.py.snap @@ -1,38 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/from_import_missing_rpar.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..19, module: Some( Identifier { id: Name("x"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 15..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 15..16, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 18..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 18..19, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -42,12 +48,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..25, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 20..25, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 20..21, value: Int( 1, @@ -57,6 +66,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 24..25, value: Int( 1, @@ -69,27 +79,33 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 26..46, module: Some( Identifier { id: Name("x"), range: 31..32, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 41..42, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 41..42, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 44..45, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 44..45, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -99,12 +115,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 47..52, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 47..52, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 47..48, value: Int( 2, @@ -114,6 +133,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap index 93824bef84498..0c0d1ff705ec8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap @@ -1,38 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/from_import_star_with_other_names.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..87, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..18, module: Some( Identifier { id: Name("x"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 14..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("*"), range: 14..15, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 17..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 17..18, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -42,35 +48,43 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 19..40, module: Some( Identifier { id: Name("x"), range: 24..25, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 33..34, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 33..34, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 36..37, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("*"), range: 36..37, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 39..40, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 39..40, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -80,32 +94,39 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 41..64, module: Some( Identifier { id: Name("x"), range: 46..47, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 55..56, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("*"), range: 55..56, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 58..64, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 58..59, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("b"), range: 63..64, + node_index: AtomicNodeIndex(..), }, ), }, @@ -115,35 +136,43 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 65..86, module: Some( Identifier { id: Name("x"), range: 70..71, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 79..80, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("*"), range: 79..80, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 82..83, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("*"), range: 82..83, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 85..86, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 85..86, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap index 6dbfd32f09872..4708a3d670bb1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/from_import_unparenthesized_trailing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..59, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..16, module: Some( Identifier { id: Name("a"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 14..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 14..15, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -34,24 +38,29 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 17..38, module: Some( Identifier { id: Name("a"), range: 22..23, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 31..37, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 31..32, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("c"), range: 36..37, + node_index: AtomicNodeIndex(..), }, ), }, @@ -61,27 +70,33 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 39..58, module: Some( Identifier { id: Name("a"), range: 44..45, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 53..54, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 53..54, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 56..57, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 56..57, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap index 1fa49e573cda7..2b793eb3a0723 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/function_def_empty_body.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..36, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..10, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..9, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -34,16 +39,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 11..28, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 15..18, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 18..20, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -53,6 +63,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..27, id: Name("int"), ctx: Load, @@ -64,10 +75,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 29..35, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Store, @@ -76,6 +89,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 33..35, value: Int( 42, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap index 0d1731ead85ae..630b9274b75a8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/function_def_invalid_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..74, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..22, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..9, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -30,9 +36,11 @@ Module( returns: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -45,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -58,16 +68,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 23..47, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 27..30, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 30..32, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -77,9 +92,11 @@ Module( returns: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 37..41, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..41, id: Name("int"), ctx: Load, @@ -92,9 +109,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..47, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 44..47, }, ), @@ -105,16 +124,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 48..73, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 52..55, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 55..57, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -124,10 +148,12 @@ Module( returns: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 61..68, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("x"), ctx: Load, @@ -140,9 +166,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..73, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 70..73, }, ), @@ -179,3 +207,13 @@ Module( 3 | def foo() -> yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here | + + +## Semantic Syntax Errors + + | +1 | def foo() -> *int: ... +2 | def foo() -> (*int): ... +3 | def foo() -> yield x: ... + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap index 12a4a4625a3ea..6b3b63253fdbd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/function_def_missing_identifier.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..31, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..11, is_async: false, decorator_list: [], name: Identifier { id: Name(""), range: 3..3, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 4..6, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -32,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..11, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 8..11, }, ), @@ -45,16 +52,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 12..30, is_async: false, decorator_list: [], name: Identifier { id: Name(""), range: 15..15, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 16..18, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -64,6 +76,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..25, id: Name("int"), ctx: Load, @@ -73,9 +86,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..30, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 27..30, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap index 3552186e7a219..e8b2fbeeac6bd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/function_def_missing_return_type.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..19, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..18, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..9, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -32,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..18, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 15..18, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap index bc3b151c5e388..f3b024f7e5ad8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap @@ -7,33 +7,43 @@ input_file: crates/ruff_python_parser/resources/inline/err/function_def_unclosed ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..74, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..18, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..18, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..14, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..14, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..14, id: Name("int"), ctx: Load, @@ -45,11 +55,14 @@ Module( }, ParameterWithDefault { range: 16..18, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 16..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 16..17, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -66,16 +79,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 19..43, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 23..26, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 26..28, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -86,10 +104,12 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 34..43, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 41..43, value: Int( 42, @@ -104,29 +124,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..74, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 48..51, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 51..74, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 52..58, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 52..58, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 52..53, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..58, id: Name("int"), ctx: Load, @@ -138,15 +167,19 @@ Module( }, ParameterWithDefault { range: 60..66, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 60..66, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 60..61, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..66, id: Name("str"), ctx: Load, @@ -158,17 +191,21 @@ Module( }, ParameterWithDefault { range: 67..73, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 67..68, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 67..68, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 71..73, value: Int( 10, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap index 8cc11d43fc00c..f8ef4f81691b6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_type_param_list.py.snap @@ -1,34 +1,39 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/function_def_unclosed_type_param_list.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..47, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..39, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 7..15, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 8..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T1"), range: 8..10, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -37,9 +42,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 12..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T2"), range: 13..15, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -49,15 +56,21 @@ Module( ), parameters: Parameters { range: 15..21, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 16..17, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 16..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 16..17, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -65,11 +78,14 @@ Module( }, ParameterWithDefault { range: 19..20, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 19..20, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 19..20, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -84,13 +100,16 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 27..39, value: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 34..39, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("a"), ctx: Load, @@ -99,6 +118,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("b"), ctx: Load, @@ -114,10 +134,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 40..46, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("x"), ctx: Store, @@ -126,6 +148,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..46, value: Int( 10, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap index ef7baf9f917c7..a2e19de94c776 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/function_def_unparenthesized_return_types.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..50, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..22, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..9, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,10 +36,12 @@ Module( returns: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 13..17, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..16, id: Name("int"), ctx: Load, @@ -49,9 +56,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -62,16 +71,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 23..49, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 27..30, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 30..32, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -81,10 +95,12 @@ Module( returns: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 36..44, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..39, id: Name("int"), ctx: Load, @@ -92,6 +108,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..44, id: Name("str"), ctx: Load, @@ -106,9 +123,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 46..49, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 46..49, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap index 80edd73d05ae2..18039d0ce0948 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap @@ -7,27 +7,33 @@ input_file: crates/ruff_python_parser/resources/inline/err/function_type_params_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..79, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..61, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 48..51, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 51..54, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 52..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 52..53, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -38,6 +44,9 @@ Module( ), parameters: Parameters { range: 54..56, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -48,9 +57,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), @@ -61,21 +72,27 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 62..78, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 66..69, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 69..71, + node_index: AtomicNodeIndex(..), type_params: [], }, ), parameters: Parameters { range: 71..73, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -86,9 +103,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 75..78, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 75..78, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap index 6a89da1d79623..6255c8fe595fc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap @@ -1,17 +1,18 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/global_stmt_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..7, body: [ Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 0..6, names: [], }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap index b2305e0df7414..d179c96eef2bb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap @@ -1,35 +1,40 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/global_stmt_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..13, body: [ Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 0..8, names: [ Identifier { id: Name("x"), range: 7..8, + node_index: AtomicNodeIndex(..), }, ], }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..12, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 9..12, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 11..12, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap index b6c872e3996ce..232acca04ddcc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap @@ -1,43 +1,49 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/global_stmt_trailing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..32, body: [ Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 0..8, names: [], }, ), Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 9..18, names: [ Identifier { id: Name("x"), range: 16..17, + node_index: AtomicNodeIndex(..), }, ], }, ), Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 19..31, names: [ Identifier { id: Name("x"), range: 26..27, + node_index: AtomicNodeIndex(..), }, Identifier { id: Name("y"), range: 29..30, + node_index: AtomicNodeIndex(..), }, ], }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap index d891db30aeb0e..888e539c24195 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_elif_missing_colon.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_elif_missing_colon.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..46, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..45, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, @@ -23,6 +25,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 10..14, }, ), @@ -30,9 +33,11 @@ Module( elif_else_clauses: [ ElifElseClause { range: 15..30, + node_index: AtomicNodeIndex(..), test: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("y"), ctx: Load, @@ -42,6 +47,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 26..30, }, ), @@ -49,10 +55,12 @@ Module( }, ElifElseClause { range: 31..45, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 41..45, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap index 9b30c83a9f2bf..0b8fecc04863b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_empty_body.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..15, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..8, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -25,12 +27,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..14, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 9..14, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 1, @@ -40,6 +45,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_elif_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_elif_test_expr.py.snap index dbc135744a766..ee46056de5836 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_elif_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_elif_test_expr.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_invalid_elif_test_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..56, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..55, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, @@ -23,6 +25,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 10..14, }, ), @@ -30,12 +33,15 @@ Module( elif_else_clauses: [ ElifElseClause { range: 15..32, + node_index: AtomicNodeIndex(..), test: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 20..22, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("x"), ctx: Load, @@ -48,6 +54,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 28..32, }, ), @@ -55,13 +62,16 @@ Module( }, ElifElseClause { range: 33..55, + node_index: AtomicNodeIndex(..), test: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 38..45, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Load, @@ -74,6 +84,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 51..55, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap index 558aa9bdbc2fa..0562b031f9beb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_invalid_test_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..48, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..10, test: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3..5, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, @@ -29,9 +32,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..10, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 7..10, }, ), @@ -43,13 +48,16 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 11..26, test: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 14..21, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("x"), ctx: Load, @@ -61,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..26, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 23..26, }, ), @@ -75,12 +85,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 27..47, test: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 30..42, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Load, @@ -91,9 +104,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..47, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 44..47, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap index b97e20e9bcfe9..091fe218f51a6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_colon.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_missing_colon.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..25, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..4, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("x"), ctx: Load, @@ -26,9 +28,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 5..18, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("x"), ctx: Load, @@ -37,6 +41,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 14..18, }, ), @@ -46,10 +51,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 19..24, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("a"), ctx: Store, @@ -58,6 +65,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 23..24, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap index bc2a35ae1d0a1..13cacd4b1073d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_missing_test.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..9, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..8, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..2, id: Name(""), ctx: Invalid, @@ -23,9 +25,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..8, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 5..8, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap index b8d421961c8f6..bef4a1b8523e6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/if_stmt_misspelled_el ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..47, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..17, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -21,6 +24,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 13..17, }, ), @@ -30,9 +34,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 18..22, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..21, id: Name("elf"), ctx: Store, @@ -40,6 +46,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..22, id: Name(""), ctx: Invalid, @@ -51,11 +58,13 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 27..31, }, ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 42..46, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap index 2ba54bef2f7d5..887ae67220567 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap @@ -7,18 +7,22 @@ input_file: crates/ruff_python_parser/resources/inline/err/implicitly_concatenat ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..47, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..7, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..7, + node_index: AtomicNodeIndex(..), value: "hello", flags: StringLiteralFlags { quote_style: Single, @@ -34,12 +38,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..20, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 15..20, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 15..16, value: Int( 1, @@ -49,6 +56,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 19..20, value: Int( 1, @@ -61,9 +69,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..40, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 21..40, value: FStringValue { inner: Concatenated( @@ -71,6 +81,7 @@ Module( Literal( StringLiteral { range: 21..28, + node_index: AtomicNodeIndex(..), value: "hello", flags: StringLiteralFlags { quote_style: Single, @@ -82,18 +93,22 @@ Module( FString( FString { range: 29..40, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..37, + node_index: AtomicNodeIndex(..), value: "world ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 37..40, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("x"), ctx: Load, @@ -121,12 +136,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 41..46, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 41..46, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 41..42, value: Int( 2, @@ -136,6 +154,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 45..46, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap index cdfccb44422a7..da094ee09c49b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/implicitly_concatenat ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..85, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..31, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 6..31, value: FStringValue { inner: Concatenated( @@ -21,6 +24,7 @@ Module( Literal( StringLiteral { range: 6..13, + node_index: AtomicNodeIndex(..), value: "hello", flags: StringLiteralFlags { quote_style: Single, @@ -32,18 +36,22 @@ Module( FString( FString { range: 18..31, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 20..26, + node_index: AtomicNodeIndex(..), value: "world ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 26..29, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Load, @@ -71,12 +79,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..37, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 32..37, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 1, @@ -86,6 +97,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 36..37, value: Int( 1, @@ -98,14 +110,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 38..51, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 44..51, value: StringLiteralValue { inner: Single( StringLiteral { range: 44..51, + node_index: AtomicNodeIndex(..), value: "first", flags: StringLiteralFlags { quote_style: Single, @@ -121,19 +136,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..76, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 68..76, value: FStringValue { inner: Single( FString( FString { range: 68..76, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 70..75, + node_index: AtomicNodeIndex(..), value: "third", }, ), @@ -153,12 +172,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 79..84, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 79..84, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 2, @@ -168,6 +190,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 2, @@ -198,7 +221,7 @@ Module( 2 | 'hello' 3 | f'world {x} 4 | ) - | ^ Syntax Error: Expected an f-string element or the end of the f-string + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string 5 | 1 + 1 6 | ( | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap index 230ad97b22181..23ff8ca3c45b8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/import_alias_missing_asname.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..12, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..11, names: [ Alias { range: 7..11, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 7..8, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap index ad8888e518ced..b198a714774b8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap @@ -1,17 +1,18 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..7, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..6, names: [], }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap index a9509f3d16f56..dedb2fd84b90d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap @@ -1,26 +1,29 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_parenthesized_names.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..25, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..6, names: [], }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("a"), ctx: Load, @@ -30,19 +33,23 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 11..17, names: [], }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..24, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 18..24, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("a"), ctx: Load, @@ -50,6 +57,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap index 70f4c3c14095b..81439cdee2229 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap @@ -1,29 +1,33 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_star_import.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..6, names: [], }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..8, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 7..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..8, id: Name(""), ctx: Invalid, @@ -36,13 +40,16 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 9..18, names: [ Alias { range: 16..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 16..17, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -51,16 +58,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..23, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 19..23, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 19..20, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..20, id: Name(""), ctx: Invalid, @@ -71,6 +82,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap index f5ab95692d9c1..d9ea1890a59a8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap @@ -1,38 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/import_stmt_trailing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..22, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..8, names: [], }, ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 9..21, names: [ Alias { range: 16..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 16..17, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 19..20, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 19..20, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap index a53ebf2fd2576..815e9022559ca 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap @@ -7,26 +7,32 @@ input_file: crates/ruff_python_parser/resources/inline/err/invalid_annotation_cl ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..247, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..26, decorator_list: [], name: Identifier { id: Name("F"), range: 6..7, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 7..10, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 8..9, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -38,12 +44,15 @@ Module( arguments: Some( Arguments { range: 10..21, + node_index: AtomicNodeIndex(..), args: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 11..20, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("y"), ctx: Store, @@ -51,6 +60,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..20, id: Name("list"), ctx: Load, @@ -65,9 +75,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..26, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 23..26, }, ), @@ -78,22 +90,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 27..53, decorator_list: [], name: Identifier { id: Name("I"), range: 33..34, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 34..37, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 35..36, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 35..36, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -105,13 +122,16 @@ Module( arguments: Some( Arguments { range: 37..48, + node_index: AtomicNodeIndex(..), args: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 39..46, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 45..46, value: Int( 1, @@ -128,9 +148,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 50..53, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 50..53, }, ), @@ -141,22 +163,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 54..85, decorator_list: [], name: Identifier { id: Name("J"), range: 60..61, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 61..64, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 62..63, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 62..63, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -168,12 +195,15 @@ Module( arguments: Some( Arguments { range: 64..80, + node_index: AtomicNodeIndex(..), args: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 66..78, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 77..78, value: Int( 1, @@ -189,9 +219,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..85, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 82..85, }, ), @@ -202,30 +234,37 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 86..112, decorator_list: [], name: Identifier { id: Name("K"), range: 92..93, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 93..107, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 94..106, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 94..95, + node_index: AtomicNodeIndex(..), }, bound: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 98..105, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..105, value: Int( 1, @@ -246,9 +285,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..112, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 109..112, }, ), @@ -259,29 +300,36 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 137..162, decorator_list: [], name: Identifier { id: Name("L"), range: 143..144, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 144..157, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 145..156, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 145..146, + node_index: AtomicNodeIndex(..), }, bound: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 149..155, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 149..150, id: Name("x"), ctx: Store, @@ -289,6 +337,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 154..155, value: Int( 1, @@ -308,9 +357,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 159..162, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 159..162, }, ), @@ -321,22 +372,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 193..219, decorator_list: [], name: Identifier { id: Name("M"), range: 199..200, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 200..203, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 201..202, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 201..202, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -348,12 +404,15 @@ Module( arguments: Some( Arguments { range: 203..214, + node_index: AtomicNodeIndex(..), args: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 205..212, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 211..212, value: Int( 1, @@ -369,9 +428,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 216..219, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 216..219, }, ), @@ -382,29 +443,36 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 220..246, decorator_list: [], name: Identifier { id: Name("N"), range: 226..227, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 227..241, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 228..240, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 228..229, + node_index: AtomicNodeIndex(..), }, bound: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 232..239, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 238..239, value: Int( 1, @@ -424,9 +492,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 243..246, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 243..246, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap index b21adb9b16ba2..2953a62e5533c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap @@ -7,27 +7,33 @@ input_file: crates/ruff_python_parser/resources/inline/err/invalid_annotation_fu ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..987, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, decorator_list: [], name: Identifier { id: Name("d"), range: 4..5, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 5..8, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 6..7, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 6..7, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -38,6 +44,9 @@ Module( ), parameters: Parameters { range: 8..10, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -47,9 +56,11 @@ Module( returns: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 15..22, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..22, value: Int( 1, @@ -62,9 +73,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -75,23 +88,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 29..58, is_async: false, decorator_list: [], name: Identifier { id: Name("e"), range: 33..34, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 34..37, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 35..36, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 35..36, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -102,22 +120,30 @@ Module( ), parameters: Parameters { range: 37..53, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 38..52, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 38..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 38..41, + node_index: AtomicNodeIndex(..), }, annotation: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 44..51, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 50..51, value: Int( 1, @@ -139,9 +165,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 55..58, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 55..58, }, ), @@ -152,23 +180,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 59..86, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 63..64, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 64..67, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 65..66, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 65..66, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -179,6 +212,9 @@ Module( ), parameters: Parameters { range: 67..69, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -188,9 +224,11 @@ Module( returns: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 74..80, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("y"), ctx: Store, @@ -198,6 +236,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 3, @@ -210,9 +249,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..86, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 83..86, }, ), @@ -223,23 +264,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 87..115, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 91..92, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 92..95, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 93..94, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 93..94, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -250,22 +296,30 @@ Module( ), parameters: Parameters { range: 95..110, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 96..109, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 96..109, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 96..99, + node_index: AtomicNodeIndex(..), }, annotation: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 102..108, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..103, id: Name("x"), ctx: Store, @@ -273,6 +327,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 107..108, value: Int( 1, @@ -294,9 +349,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 112..115, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 112..115, }, ), @@ -307,23 +364,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 116..143, is_async: false, decorator_list: [], name: Identifier { id: Name("h"), range: 120..121, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 121..124, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 122..123, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 122..123, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -334,23 +396,31 @@ Module( ), parameters: Parameters { range: 124..138, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 125..137, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 125..137, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 125..126, + node_index: AtomicNodeIndex(..), }, annotation: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 129..136, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 135..136, value: Int( 1, @@ -373,9 +443,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 140..143, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 140..143, }, ), @@ -386,23 +458,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 144..172, is_async: false, decorator_list: [], name: Identifier { id: Name("j"), range: 148..149, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 149..152, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 150..151, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 150..151, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -413,6 +490,9 @@ Module( ), parameters: Parameters { range: 152..154, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -422,10 +502,12 @@ Module( returns: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 159..166, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 165..166, value: Int( 1, @@ -439,9 +521,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 169..172, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 169..172, }, ), @@ -452,23 +536,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 173..205, is_async: false, decorator_list: [], name: Identifier { id: Name("l"), range: 177..178, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 178..181, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 179..180, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 179..180, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -479,22 +568,30 @@ Module( ), parameters: Parameters { range: 181..200, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 182..199, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 182..199, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 182..183, + node_index: AtomicNodeIndex(..), }, annotation: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 186..198, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 197..198, value: Int( 1, @@ -516,9 +613,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 202..205, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 202..205, }, ), @@ -529,23 +628,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 206..239, is_async: false, decorator_list: [], name: Identifier { id: Name("n"), range: 210..211, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 211..214, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 212..213, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 212..213, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -556,6 +660,9 @@ Module( ), parameters: Parameters { range: 214..216, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -565,9 +672,11 @@ Module( returns: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 221..233, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 232..233, value: Int( 1, @@ -580,9 +689,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 236..239, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 236..239, }, ), @@ -593,31 +704,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 240..266, is_async: false, decorator_list: [], name: Identifier { id: Name("p"), range: 244..245, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 245..259, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 246..258, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 246..247, + node_index: AtomicNodeIndex(..), }, bound: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 250..257, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 256..257, value: Int( 1, @@ -636,6 +754,9 @@ Module( ), parameters: Parameters { range: 259..261, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -646,9 +767,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 263..266, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 263..266, }, ), @@ -659,32 +782,39 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 297..324, is_async: false, decorator_list: [], name: Identifier { id: Name("q"), range: 301..302, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 302..317, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 303..316, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 303..304, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 308..315, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 314..315, value: Int( 1, @@ -702,6 +832,9 @@ Module( ), parameters: Parameters { range: 317..319, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -712,9 +845,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 321..324, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 321..324, }, ), @@ -725,31 +860,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 356..385, is_async: false, decorator_list: [], name: Identifier { id: Name("r"), range: 360..361, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 361..378, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 362..377, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 363..365, + node_index: AtomicNodeIndex(..), }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 369..376, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 375..376, value: Int( 1, @@ -767,6 +909,9 @@ Module( ), parameters: Parameters { range: 378..380, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -777,9 +922,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 382..385, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 382..385, }, ), @@ -790,31 +937,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 420..450, is_async: false, decorator_list: [], name: Identifier { id: Name("s"), range: 424..425, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 425..443, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 426..442, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 428..430, + node_index: AtomicNodeIndex(..), }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 434..441, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 440..441, value: Int( 1, @@ -832,6 +986,9 @@ Module( ), parameters: Parameters { range: 443..445, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -842,9 +999,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 447..450, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 447..450, }, ), @@ -855,30 +1014,37 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 481..506, is_async: false, decorator_list: [], name: Identifier { id: Name("t"), range: 485..486, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 486..499, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 487..498, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 487..488, + node_index: AtomicNodeIndex(..), }, bound: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 491..497, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 491..492, id: Name("x"), ctx: Store, @@ -886,6 +1052,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 496..497, value: Int( 1, @@ -903,6 +1070,9 @@ Module( ), parameters: Parameters { range: 499..501, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -913,9 +1083,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 503..506, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 503..506, }, ), @@ -926,31 +1098,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 543..569, is_async: false, decorator_list: [], name: Identifier { id: Name("u"), range: 547..548, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 548..562, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 549..561, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 549..550, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 554..560, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 554..555, id: Name("x"), ctx: Store, @@ -958,6 +1137,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 559..560, value: Int( 1, @@ -974,6 +1154,9 @@ Module( ), parameters: Parameters { range: 562..564, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -984,9 +1167,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 566..569, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 566..569, }, ), @@ -997,30 +1182,37 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 607..635, is_async: false, decorator_list: [], name: Identifier { id: Name("v"), range: 611..612, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 612..628, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 613..627, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 614..616, + node_index: AtomicNodeIndex(..), }, default: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 620..626, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 620..621, id: Name("x"), ctx: Store, @@ -1028,6 +1220,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 625..626, value: Int( 1, @@ -1044,6 +1237,9 @@ Module( ), parameters: Parameters { range: 628..630, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -1054,9 +1250,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 632..635, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 632..635, }, ), @@ -1067,30 +1265,37 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 676..705, is_async: false, decorator_list: [], name: Identifier { id: Name("w"), range: 680..681, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 681..698, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 682..697, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 684..686, + node_index: AtomicNodeIndex(..), }, default: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 690..696, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 690..691, id: Name("x"), ctx: Store, @@ -1098,6 +1303,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 695..696, value: Int( 1, @@ -1114,6 +1320,9 @@ Module( ), parameters: Parameters { range: 698..700, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -1124,9 +1333,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 702..705, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 702..705, }, ), @@ -1137,30 +1348,37 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 742..768, is_async: false, decorator_list: [], name: Identifier { id: Name("t"), range: 746..747, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 747..761, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 748..760, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 748..749, + node_index: AtomicNodeIndex(..), }, bound: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 752..759, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 758..759, value: Int( 1, @@ -1178,6 +1396,9 @@ Module( ), parameters: Parameters { range: 761..763, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -1188,9 +1409,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 765..768, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 765..768, }, ), @@ -1201,31 +1424,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 800..827, is_async: false, decorator_list: [], name: Identifier { id: Name("u"), range: 804..805, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 805..820, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 806..819, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 806..807, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 811..818, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 817..818, value: Int( 1, @@ -1242,6 +1472,9 @@ Module( ), parameters: Parameters { range: 820..822, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -1252,9 +1485,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 824..827, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 824..827, }, ), @@ -1265,30 +1500,37 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 860..889, is_async: false, decorator_list: [], name: Identifier { id: Name("v"), range: 864..865, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 865..882, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 866..881, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 867..869, + node_index: AtomicNodeIndex(..), }, default: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 873..880, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 879..880, value: Int( 1, @@ -1305,6 +1547,9 @@ Module( ), parameters: Parameters { range: 882..884, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -1315,9 +1560,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 886..889, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 886..889, }, ), @@ -1328,30 +1575,37 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 925..955, is_async: false, decorator_list: [], name: Identifier { id: Name("w"), range: 929..930, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 930..948, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 931..947, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 933..935, + node_index: AtomicNodeIndex(..), }, default: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 939..946, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 945..946, value: Int( 1, @@ -1368,6 +1622,9 @@ Module( ), parameters: Parameters { range: 948..950, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -1378,9 +1635,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 952..955, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 952..955, }, ), @@ -1397,7 +1656,7 @@ Module( | 1 | def d[T]() -> (await 1): ... - | ^^^^^^^ Syntax Error: await expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... | @@ -1406,7 +1665,7 @@ Module( | 1 | def d[T]() -> (await 1): ... 2 | def e[T](arg: (await 1)): ... - | ^^^^^^^ Syntax Error: await expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... | @@ -1416,7 +1675,7 @@ Module( 1 | def d[T]() -> (await 1): ... 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... - | ^^^^^^ Syntax Error: named expression cannot be used within a generic definition + | ^^^^^^ Syntax Error: named expression cannot be used within a type annotation 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... | @@ -1426,7 +1685,7 @@ Module( 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... - | ^^^^^^ Syntax Error: named expression cannot be used within a generic definition + | ^^^^^^ Syntax Error: named expression cannot be used within a type annotation 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... | @@ -1436,7 +1695,7 @@ Module( 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... - | ^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... | @@ -1446,7 +1705,7 @@ Module( 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... - | ^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 7 | def l[T](x: (yield from 1)): ... 8 | def n[T]() -> (yield from 1): ... | @@ -1456,7 +1715,7 @@ Module( 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... - | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 8 | def n[T]() -> (yield from 1): ... 9 | def p[T: (yield 1)](): ... # yield in TypeVar bound | @@ -1466,7 +1725,7 @@ Module( 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... 8 | def n[T]() -> (yield from 1): ... - | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 9 | def p[T: (yield 1)](): ... # yield in TypeVar bound 10 | def q[T = (yield 1)](): ... # yield in TypeVar default | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap index 49533fb33fce2..81e3240ff76ab 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/invalid_annotation_fu ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..316, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..68, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 48..49, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 49..51, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -30,9 +36,11 @@ Module( returns: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 56..62, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("y"), ctx: Store, @@ -40,6 +48,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 3, @@ -52,9 +61,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 65..68, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 65..68, }, ), @@ -65,32 +76,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 69..94, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 73..74, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 74..89, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 75..88, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 75..88, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 75..78, + node_index: AtomicNodeIndex(..), }, annotation: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 81..87, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("x"), ctx: Store, @@ -98,6 +119,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 86..87, value: Int( 1, @@ -119,9 +141,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 91..94, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 91..94, }, ), @@ -132,16 +156,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 95..235, is_async: false, decorator_list: [], name: Identifier { id: Name("outer"), range: 99..104, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 104..106, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -152,33 +181,43 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 112..136, is_async: false, decorator_list: [], name: Identifier { id: Name("i"), range: 116..117, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 117..131, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 118..130, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 118..130, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 118..119, + node_index: AtomicNodeIndex(..), }, annotation: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 122..129, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 128..129, value: Int( 1, @@ -201,9 +240,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..136, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 133..136, }, ), @@ -214,16 +255,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 141..166, is_async: false, decorator_list: [], name: Identifier { id: Name("k"), range: 145..146, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 146..148, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -233,10 +279,12 @@ Module( returns: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 153..160, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 159..160, value: Int( 1, @@ -250,9 +298,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 163..166, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 163..166, }, ), @@ -263,32 +313,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 171..200, is_async: false, decorator_list: [], name: Identifier { id: Name("m"), range: 175..176, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 176..195, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 177..194, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 177..194, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 177..178, + node_index: AtomicNodeIndex(..), }, annotation: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 181..193, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 192..193, value: Int( 1, @@ -310,9 +370,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 197..200, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 197..200, }, ), @@ -323,16 +385,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 205..235, is_async: false, decorator_list: [], name: Identifier { id: Name("o"), range: 209..210, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 210..212, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -342,9 +409,11 @@ Module( returns: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 217..229, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 228..229, value: Int( 1, @@ -357,9 +426,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 232..235, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 232..235, }, ), @@ -373,16 +444,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 236..315, is_async: true, decorator_list: [], name: Identifier { id: Name("outer"), range: 246..251, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 251..253, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -393,16 +469,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 259..284, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 263..264, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 264..266, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -412,9 +493,11 @@ Module( returns: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 271..278, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 277..278, value: Int( 1, @@ -427,9 +510,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 281..284, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 281..284, }, ), @@ -440,32 +525,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 289..315, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 293..294, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 294..310, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 295..309, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 295..309, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 295..298, + node_index: AtomicNodeIndex(..), }, annotation: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 301..308, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 307..308, value: Int( 1, @@ -487,9 +582,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 312..315, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 312..315, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap index 113edf8f44105..78ac67583b03b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/invalid_annotation_py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..144, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 44..55, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("a"), ctx: Store, @@ -21,9 +24,11 @@ Module( ), annotation: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 48..54, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..49, id: Name("x"), ctx: Store, @@ -31,6 +36,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 53..54, value: Int( 1, @@ -45,16 +51,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 56..107, is_async: false, decorator_list: [], name: Identifier { id: Name("outer"), range: 60..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -65,9 +76,11 @@ Module( body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 73..85, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("b"), ctx: Store, @@ -75,10 +88,12 @@ Module( ), annotation: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 77..84, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 1, @@ -94,9 +109,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 90..107, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("c"), ctx: Store, @@ -104,9 +121,11 @@ Module( ), annotation: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 94..106, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 105..106, value: Int( 1, @@ -124,16 +143,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 108..143, is_async: true, decorator_list: [], name: Identifier { id: Name("outer"), range: 118..123, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 123..125, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -144,9 +168,11 @@ Module( body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 131..143, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..132, id: Name("d"), ctx: Store, @@ -154,9 +180,11 @@ Module( ), annotation: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 135..142, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 141..142, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap index 8b4e9638b59c3..10e387a20bc1f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/invalid_annotation_ty ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..406, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..26, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -22,21 +25,26 @@ Module( type_params: Some( TypeParams { range: 6..20, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 7..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 7..8, + node_index: AtomicNodeIndex(..), }, bound: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 11..18, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 17..18, value: Int( 1, @@ -55,6 +63,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..26, id: Name("int"), ctx: Load, @@ -64,9 +73,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 48..75, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("X"), ctx: Store, @@ -75,22 +86,27 @@ Module( type_params: Some( TypeParams { range: 54..69, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 55..68, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 55..56, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 60..67, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 66..67, value: Int( 1, @@ -108,6 +124,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..75, id: Name("int"), ctx: Load, @@ -117,9 +134,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 98..127, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 103..104, id: Name("X"), ctx: Store, @@ -128,21 +147,26 @@ Module( type_params: Some( TypeParams { range: 104..121, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 105..120, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 106..108, + node_index: AtomicNodeIndex(..), }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 112..119, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..119, value: Int( 1, @@ -160,6 +184,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 124..127, id: Name("int"), ctx: Load, @@ -169,9 +194,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 153..183, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 158..159, id: Name("X"), ctx: Store, @@ -180,21 +207,26 @@ Module( type_params: Some( TypeParams { range: 159..177, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 160..176, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 162..164, + node_index: AtomicNodeIndex(..), }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 168..175, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 174..175, value: Int( 1, @@ -212,6 +244,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 180..183, id: Name("int"), ctx: Load, @@ -221,9 +254,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 205..223, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 210..211, id: Name("Y"), ctx: Store, @@ -232,10 +267,12 @@ Module( type_params: None, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 215..222, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 221..222, value: Int( 1, @@ -249,9 +286,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 254..271, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..260, id: Name("Y"), ctx: Store, @@ -260,9 +299,11 @@ Module( type_params: None, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 264..270, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 264..265, id: Name("x"), ctx: Store, @@ -270,6 +311,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 269..270, value: Int( 1, @@ -282,9 +324,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 308..334, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 313..314, id: Name("Y"), ctx: Store, @@ -293,20 +337,25 @@ Module( type_params: Some( TypeParams { range: 314..328, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 315..327, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 315..316, + node_index: AtomicNodeIndex(..), }, bound: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 319..326, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 325..326, value: Int( 1, @@ -324,6 +373,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 331..334, id: Name("int"), ctx: Load, @@ -333,9 +383,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 357..375, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 362..363, id: Name("Y"), ctx: Store, @@ -344,9 +396,11 @@ Module( type_params: None, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 367..374, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 373..374, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap index 0cb2a556d7b94..67b56f461720f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/invalid_byte_literal.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..44, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 0..12, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 0..12, + node_index: AtomicNodeIndex(..), value: [], flags: BytesLiteralFlags { quote_style: Single, @@ -35,14 +38,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..26, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 13..26, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 13..26, + node_index: AtomicNodeIndex(..), value: [], flags: BytesLiteralFlags { quote_style: Double, @@ -60,14 +66,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..43, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 27..43, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 27..43, + node_index: AtomicNodeIndex(..), value: [], flags: BytesLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap index c3731e868d969..444e3dd9792a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/invalid_del_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..75, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 0..9, targets: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4..9, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Load, @@ -27,6 +30,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 1, @@ -40,21 +44,25 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 10..22, targets: [ Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 14..22, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 15..18, value: StringLiteralValue { inner: Single( StringLiteral { range: 15..18, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Single, @@ -69,6 +77,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 20..21, value: Int( 1, @@ -84,19 +93,23 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 23..37, targets: [ Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 27..37, elts: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 28..31, value: StringLiteralValue { inner: Single( StringLiteral { range: 28..31, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Single, @@ -110,11 +123,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 33..36, value: StringLiteralValue { inner: Single( StringLiteral { range: 33..36, + node_index: AtomicNodeIndex(..), value: "y", flags: StringLiteralFlags { quote_style: Single, @@ -134,27 +149,32 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 38..74, targets: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 42..46, }, ), BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 48..52, value: true, }, ), BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 54..59, value: false, }, ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 1, @@ -163,6 +183,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 64..67, value: Float( 1.0, @@ -171,11 +192,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 69..74, value: StringLiteralValue { inner: Single( StringLiteral { range: 69..74, + node_index: AtomicNodeIndex(..), value: "abc", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap index 790a2e91ca9db..d401ee2d55219 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/invalid_fstring_literal_element.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..58, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..26, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..26, value: FStringValue { inner: Single( FString( FString { range: 0..26, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..25, + node_index: AtomicNodeIndex(..), value: "", }, ), @@ -44,19 +48,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..57, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 27..57, value: FStringValue { inner: Single( FString( FString { range: 27..57, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..54, + node_index: AtomicNodeIndex(..), value: "", }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap index f459755d73f0a..93f0879fae1ae 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/invalid_string_literal.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..56, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..25, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..25, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..25, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -35,14 +38,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..55, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 26..55, value: StringLiteralValue { inner: Single( StringLiteral { range: 26..55, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap index ca46e5e687880..3853b04afdb87 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@irrefutable_case_pattern.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/irrefutable_case_patt ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..317, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..61, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -22,14 +25,17 @@ Module( cases: [ MatchCase { range: 13..26, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 18..21, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("var"), range: 18..21, + node_index: AtomicNodeIndex(..), }, ), }, @@ -38,9 +44,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..26, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 23..26, }, ), @@ -50,11 +58,14 @@ Module( }, MatchCase { range: 50..61, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 55..56, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 55..56, value: Int( 2, @@ -67,9 +78,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), @@ -82,9 +95,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 62..102, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -93,9 +108,11 @@ Module( cases: [ MatchCase { range: 75..86, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 80..81, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -104,9 +121,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..86, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 83..86, }, ), @@ -116,11 +135,14 @@ Module( }, MatchCase { range: 91..102, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 96..97, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 96..97, value: Int( 2, @@ -133,9 +155,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 99..102, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 99..102, }, ), @@ -148,9 +172,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 125..222, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..132, id: Name("x"), ctx: Load, @@ -159,18 +185,22 @@ Module( cases: [ MatchCase { range: 138..160, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 143..155, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 143..147, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("var1"), range: 143..147, + node_index: AtomicNodeIndex(..), }, ), }, @@ -180,6 +210,7 @@ Module( Identifier { id: Name("var2"), range: 151..155, + node_index: AtomicNodeIndex(..), }, ), }, @@ -188,9 +219,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 157..160, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 157..160, }, ), @@ -200,11 +233,14 @@ Module( }, MatchCase { range: 211..222, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 216..217, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 216..217, value: Int( 2, @@ -217,9 +253,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 219..222, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 219..222, }, ), @@ -232,9 +270,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 223..316, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 229..230, id: Name("x"), ctx: Load, @@ -243,18 +283,23 @@ Module( cases: [ MatchCase { range: 236..264, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 241..259, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 241..253, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 241..253, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..245, id: Name("enum"), ctx: Load, @@ -263,6 +308,7 @@ Module( attr: Identifier { id: Name("variant"), range: 246..253, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -272,11 +318,13 @@ Module( MatchAs( PatternMatchAs { range: 256..259, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("var"), range: 256..259, + node_index: AtomicNodeIndex(..), }, ), }, @@ -288,9 +336,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 261..264, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 261..264, }, ), @@ -300,11 +350,14 @@ Module( }, MatchCase { range: 305..316, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 310..311, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 310..311, value: Int( 2, @@ -317,9 +370,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 313..316, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 313..316, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap index 0aa69e8be1803..305adc1eeaced 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/iter_unpack_return_py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..91, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 43..59, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..47, id: Name("rest"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..59, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 4, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 5, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 6, @@ -58,16 +65,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..90, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 64..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -78,14 +90,17 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 69..90, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 76..90, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 76..77, value: Int( 1, @@ -94,6 +109,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 2, @@ -102,6 +118,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 3, @@ -110,9 +127,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 85..90, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..90, id: Name("rest"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap index 44b0fe51e34c8..f3e6ac63aa8ad 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/iter_unpack_yield_py3 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..128, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 43..59, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..47, id: Name("rest"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..59, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 4, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 5, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 6, @@ -58,16 +65,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..89, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 64..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -78,17 +90,21 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 69..89, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 69..89, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 75..89, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 1, @@ -97,6 +113,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 2, @@ -105,6 +122,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 3, @@ -113,9 +131,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 84..89, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..89, id: Name("rest"), ctx: Load, @@ -139,16 +159,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 90..127, is_async: false, decorator_list: [], name: Identifier { id: Name("h"), range: 94..95, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 95..97, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -159,17 +184,21 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 99..127, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 99..127, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 105..127, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 105..106, value: Int( 1, @@ -178,14 +207,17 @@ Module( ), Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 109..123, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 115..123, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 2, @@ -194,9 +226,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 118..123, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..123, id: Name("rest"), ctx: Load, @@ -215,6 +249,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 126..127, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap index 98b0ef84a3e3d..920e8ab7d7d32 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap @@ -1,33 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/lambda_body_with_starred_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..62, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..12, parameters: Some( Parameters { range: 7..8, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 7..8, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 7..8, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -41,9 +49,11 @@ Module( ), body: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 10..12, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("y"), ctx: Load, @@ -58,26 +68,35 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..26, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 13..26, elts: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 13..25, parameters: Some( Parameters { range: 20..21, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 20..21, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 20..21, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 20..21, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -91,9 +110,11 @@ Module( ), body: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 23..25, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("y"), ctx: Load, @@ -113,26 +134,35 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..42, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 27..42, elts: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 27..39, parameters: Some( Parameters { range: 34..35, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 34..35, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 34..35, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 34..35, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -146,9 +176,11 @@ Module( ), body: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 37..39, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("y"), ctx: Load, @@ -161,6 +193,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("z"), ctx: Load, @@ -175,22 +208,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..61, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 43..61, parameters: Some( Parameters { range: 50..51, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 50..51, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 50..51, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 50..51, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -204,14 +245,17 @@ Module( ), body: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 53..61, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 54..61, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("y"), ctx: Load, @@ -219,6 +263,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap index fe4a4b01abd51..1b3cd9547258f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap @@ -1,33 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/lambda_body_with_yield_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..41, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..17, parameters: Some( Parameters { range: 7..8, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 7..8, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 7..8, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -41,10 +49,12 @@ Module( ), body: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 10..17, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("y"), ctx: Load, @@ -59,22 +69,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..40, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 18..40, parameters: Some( Parameters { range: 25..26, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 25..26, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 25..26, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 25..26, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -88,9 +106,11 @@ Module( ), body: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 28..40, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap index a951295d8a809..f4cf349ddd2b0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_before_py310.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/match_before_py310.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..79, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 45..78, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 2, @@ -23,11 +26,14 @@ Module( cases: [ MatchCase { range: 58..78, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 63..64, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 63..64, value: Int( 1, @@ -40,6 +46,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 74..78, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap index 81e020fe54a61..e4e0b0db63713 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_classify_as_keyword.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..33, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..32, subject: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 6..15, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..15, id: Name("foo"), ctx: Load, @@ -30,9 +33,11 @@ Module( cases: [ MatchCase { range: 21..32, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 26..27, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -41,9 +46,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap index 9681e681bba8b..5ce94bf138ca1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_classify_as_keyword_or_identifier.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_classify_as_keyword_or_identifier.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..39, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..38, subject: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..10, id: Name("foo"), ctx: Load, @@ -29,9 +32,11 @@ Module( cases: [ MatchCase { range: 27..38, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 32..33, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -40,9 +45,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..38, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 35..38, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap index 42e2347dd2fc3..6415f9724a551 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_expected_colon.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_expected_colon.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..29, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..28, subject: List( ExprList { + node_index: AtomicNodeIndex(..), range: 6..12, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 1, @@ -27,6 +30,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 2, @@ -40,9 +44,11 @@ Module( cases: [ MatchCase { range: 17..28, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 22..23, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -51,9 +57,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap index 5ba948d16b052..273c444e53840 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_expect_ind ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..23, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..22, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("foo"), ctx: Load, @@ -22,9 +25,11 @@ Module( cases: [ MatchCase { range: 11..22, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 16..17, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -33,9 +38,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap index 2844e904f7156..528b87d4e7afe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_expected_c ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..61, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..13, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -24,10 +27,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 13..18, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("x"), ctx: Store, @@ -36,6 +41,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 17..18, value: Int( 1, @@ -46,9 +52,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 19..32, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("x"), ctx: Load, @@ -59,9 +67,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 32..60, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("y"), ctx: Load, @@ -70,9 +80,11 @@ Module( cases: [ MatchCase { range: 49..60, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 54..55, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -81,9 +93,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..60, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 57..60, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap index 5028df2a0d252..0f5a1d34f4894 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_invalid_guard_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..100, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..30, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -23,14 +25,17 @@ Module( cases: [ MatchCase { range: 13..30, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 18..19, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 18..19, + node_index: AtomicNodeIndex(..), }, ), }, @@ -38,9 +43,11 @@ Module( guard: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 23..25, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("a"), ctx: Load, @@ -53,9 +60,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..30, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 27..30, }, ), @@ -68,9 +77,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 31..63, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("x"), ctx: Load, @@ -79,14 +90,17 @@ Module( cases: [ MatchCase { range: 44..63, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 49..50, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 49..50, + node_index: AtomicNodeIndex(..), }, ), }, @@ -94,9 +108,11 @@ Module( guard: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 55..57, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("a"), ctx: Load, @@ -109,9 +125,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 60..63, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 60..63, }, ), @@ -124,9 +142,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 64..99, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("x"), ctx: Load, @@ -135,14 +155,17 @@ Module( cases: [ MatchCase { range: 77..99, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 82..83, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 82..83, + node_index: AtomicNodeIndex(..), }, ), }, @@ -150,10 +173,12 @@ Module( guard: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 87..94, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("x"), ctx: Load, @@ -166,9 +191,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap index e7e2359f8363b..13285ab3c48f5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_subject_expr.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_invalid_subject_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..131, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..27, subject: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 7..9, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("x"), ctx: Load, @@ -29,9 +32,11 @@ Module( cases: [ MatchCase { range: 16..27, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 21..22, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -40,9 +45,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..27, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 24..27, }, ), @@ -55,21 +62,26 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 65..99, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 71..82, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 71..79, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 72..79, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("x"), ctx: Load, @@ -77,6 +89,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..79, id: Name("y"), ctx: Load, @@ -90,6 +103,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("z"), ctx: Load, @@ -103,9 +117,11 @@ Module( cases: [ MatchCase { range: 88..99, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 93..94, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -114,9 +130,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), @@ -129,13 +147,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 100..130, subject: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 106..113, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 112..113, id: Name("x"), ctx: Load, @@ -147,9 +168,11 @@ Module( cases: [ MatchCase { range: 119..130, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 124..125, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -158,9 +181,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 127..130, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 127..130, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap index 2dbcec05b1263..bda3c6e5c3692 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_missing_guard_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..27, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -23,14 +25,17 @@ Module( cases: [ MatchCase { range: 13..27, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 18..19, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 18..19, + node_index: AtomicNodeIndex(..), }, ), }, @@ -39,9 +44,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..27, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 24..27, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap index 81183473ca969..3b604f0505f19 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_missing_pattern.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..23, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -23,11 +25,14 @@ Module( cases: [ MatchCase { range: 13..23, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 17..17, + node_index: AtomicNodeIndex(..), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..17, id: Name(""), ctx: Invalid, @@ -39,9 +44,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..23, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap index 6a9ba216b3a0c..f22cb551fca4d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_no_newline ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..23, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..22, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("foo"), ctx: Load, @@ -22,9 +25,11 @@ Module( cases: [ MatchCase { range: 11..22, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 16..17, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -33,9 +38,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap index affa89318ac75..1bfa4ac064cd6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_single_starred_subject.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/match_stmt_single_starred_subject.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..27, subject: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..10, id: Name("foo"), ctx: Load, @@ -29,9 +32,11 @@ Module( cases: [ MatchCase { range: 16..27, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 21..22, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -40,9 +45,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..27, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 24..27, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap index 07d4518fcc44f..192d87e0b5485 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/mixed_bytes_and_non_bytes_literals.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..64, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..17, value: StringLiteralValue { inner: Concatenated( @@ -22,6 +24,7 @@ Module( strings: [ StringLiteral { range: 0..7, + node_index: AtomicNodeIndex(..), value: "first", flags: StringLiteralFlags { quote_style: Single, @@ -31,6 +34,7 @@ Module( }, StringLiteral { range: 8..17, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -49,9 +53,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..36, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 18..36, value: FStringValue { inner: Concatenated( @@ -59,10 +65,12 @@ Module( FString( FString { range: 18..26, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 20..25, + node_index: AtomicNodeIndex(..), value: "first", }, ), @@ -77,6 +85,7 @@ Module( Literal( StringLiteral { range: 27..36, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -94,9 +103,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 37..63, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 37..63, value: FStringValue { inner: Concatenated( @@ -104,6 +115,7 @@ Module( Literal( StringLiteral { range: 37..44, + node_index: AtomicNodeIndex(..), value: "first", flags: StringLiteralFlags { quote_style: Single, @@ -115,10 +127,12 @@ Module( FString( FString { range: 45..54, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 47..53, + node_index: AtomicNodeIndex(..), value: "second", }, ), @@ -133,6 +147,7 @@ Module( Literal( StringLiteral { range: 55..63, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap index c47d0febba18f..c3e7a8341c213 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/multiple_assignment_i ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..456, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..444, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 2, @@ -23,18 +26,22 @@ Module( cases: [ MatchCase { range: 13..32, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 18..27, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 19..20, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 19..20, + node_index: AtomicNodeIndex(..), }, ), }, @@ -42,11 +49,13 @@ Module( MatchAs( PatternMatchAs { range: 22..23, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 22..23, + node_index: AtomicNodeIndex(..), }, ), }, @@ -54,11 +63,13 @@ Module( MatchAs( PatternMatchAs { range: 25..26, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 25..26, + node_index: AtomicNodeIndex(..), }, ), }, @@ -70,9 +81,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), @@ -82,18 +95,22 @@ Module( }, MatchCase { range: 54..74, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 59..69, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 60..61, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 60..61, + node_index: AtomicNodeIndex(..), }, ), }, @@ -101,11 +118,13 @@ Module( MatchAs( PatternMatchAs { range: 63..64, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 63..64, + node_index: AtomicNodeIndex(..), }, ), }, @@ -113,10 +132,12 @@ Module( MatchStar( PatternMatchStar { range: 66..68, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("y"), range: 67..68, + node_index: AtomicNodeIndex(..), }, ), }, @@ -128,9 +149,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..74, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 71..74, }, ), @@ -140,18 +163,22 @@ Module( }, MatchCase { range: 96..115, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 101..110, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 102..103, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 102..103, + node_index: AtomicNodeIndex(..), }, ), }, @@ -159,11 +186,13 @@ Module( MatchAs( PatternMatchAs { range: 105..106, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 105..106, + node_index: AtomicNodeIndex(..), }, ), }, @@ -171,11 +200,13 @@ Module( MatchAs( PatternMatchAs { range: 108..109, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 108..109, + node_index: AtomicNodeIndex(..), }, ), }, @@ -187,9 +218,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 112..115, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 112..115, }, ), @@ -199,12 +232,15 @@ Module( }, MatchCase { range: 146..168, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 151..163, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 152..153, value: Int( 1, @@ -213,6 +249,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 158..159, value: Int( 2, @@ -224,11 +261,13 @@ Module( MatchAs( PatternMatchAs { range: 155..156, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 155..156, + node_index: AtomicNodeIndex(..), }, ), }, @@ -236,11 +275,13 @@ Module( MatchAs( PatternMatchAs { range: 161..162, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 161..162, + node_index: AtomicNodeIndex(..), }, ), }, @@ -253,9 +294,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 165..168, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 165..168, }, ), @@ -265,12 +308,15 @@ Module( }, MatchCase { range: 207..228, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 212..223, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..214, value: Int( 1, @@ -282,11 +328,13 @@ Module( MatchAs( PatternMatchAs { range: 216..217, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 216..217, + node_index: AtomicNodeIndex(..), }, ), }, @@ -296,6 +344,7 @@ Module( Identifier { id: Name("x"), range: 221..222, + node_index: AtomicNodeIndex(..), }, ), }, @@ -304,9 +353,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 225..228, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 225..228, }, ), @@ -316,11 +367,14 @@ Module( }, MatchCase { range: 269..290, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 274..285, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 274..279, id: Name("Class"), ctx: Load, @@ -328,15 +382,18 @@ Module( ), arguments: PatternArguments { range: 279..285, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 280..281, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 280..281, + node_index: AtomicNodeIndex(..), }, ), }, @@ -344,11 +401,13 @@ Module( MatchAs( PatternMatchAs { range: 283..284, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 283..284, + node_index: AtomicNodeIndex(..), }, ), }, @@ -362,9 +421,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 287..290, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 287..290, }, ), @@ -374,11 +435,14 @@ Module( }, MatchCase { range: 320..345, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 325..340, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 325..330, id: Name("Class"), ctx: Load, @@ -386,22 +450,27 @@ Module( ), arguments: PatternArguments { range: 330..340, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 331..334, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 331..332, + node_index: AtomicNodeIndex(..), }, pattern: MatchAs( PatternMatchAs { range: 333..334, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 333..334, + node_index: AtomicNodeIndex(..), }, ), }, @@ -409,18 +478,22 @@ Module( }, PatternKeyword { range: 336..339, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("z"), range: 336..337, + node_index: AtomicNodeIndex(..), }, pattern: MatchAs( PatternMatchAs { range: 338..339, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 338..339, + node_index: AtomicNodeIndex(..), }, ), }, @@ -434,9 +507,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 342..345, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 342..345, }, ), @@ -446,22 +521,27 @@ Module( }, MatchCase { range: 372..412, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 377..407, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 377..380, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 378..379, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 378..379, + node_index: AtomicNodeIndex(..), }, ), }, @@ -472,9 +552,11 @@ Module( MatchMapping( PatternMatchMapping { range: 383..389, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 384..385, value: Int( 1, @@ -486,11 +568,13 @@ Module( MatchAs( PatternMatchAs { range: 387..388, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 387..388, + node_index: AtomicNodeIndex(..), }, ), }, @@ -502,8 +586,10 @@ Module( MatchClass( PatternMatchClass { range: 392..407, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 392..397, id: Name("Class"), ctx: Load, @@ -511,22 +597,27 @@ Module( ), arguments: PatternArguments { range: 397..407, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 398..401, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 398..399, + node_index: AtomicNodeIndex(..), }, pattern: MatchAs( PatternMatchAs { range: 400..401, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 400..401, + node_index: AtomicNodeIndex(..), }, ), }, @@ -534,18 +625,22 @@ Module( }, PatternKeyword { range: 403..406, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("z"), range: 403..404, + node_index: AtomicNodeIndex(..), }, pattern: MatchAs( PatternMatchAs { range: 405..406, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 405..406, + node_index: AtomicNodeIndex(..), }, ), }, @@ -562,9 +657,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 409..412, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 409..412, }, ), @@ -574,18 +671,22 @@ Module( }, MatchCase { range: 428..444, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 433..439, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 433..434, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 433..434, + node_index: AtomicNodeIndex(..), }, ), }, @@ -595,6 +696,7 @@ Module( Identifier { id: Name("x"), range: 438..439, + node_index: AtomicNodeIndex(..), }, ), }, @@ -603,9 +705,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 441..444, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 441..444, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap index f67856c23fbd2..e7bbeb557a229 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/multiple_clauses_on_same_line.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..258, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..41, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -22,6 +24,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -29,9 +32,11 @@ Module( elif_else_clauses: [ ElifElseClause { range: 14..30, + node_index: AtomicNodeIndex(..), test: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 19..24, value: false, }, @@ -40,6 +45,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 26..30, }, ), @@ -47,10 +53,12 @@ Module( }, ElifElseClause { range: 31..41, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 37..41, }, ), @@ -61,9 +69,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 42..85, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 45..49, value: true, }, @@ -71,6 +81,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 51..55, }, ), @@ -78,9 +89,11 @@ Module( elif_else_clauses: [ ElifElseClause { range: 57..73, + node_index: AtomicNodeIndex(..), test: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 62..67, value: false, }, @@ -89,6 +102,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 69..73, }, ), @@ -96,10 +110,12 @@ Module( }, ElifElseClause { range: 75..85, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 81..85, }, ), @@ -110,10 +126,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 86..117, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("x"), ctx: Store, @@ -121,6 +139,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..99, id: Name("iter"), ctx: Load, @@ -129,6 +148,7 @@ Module( body: [ Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 101..106, }, ), @@ -136,6 +156,7 @@ Module( orelse: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 113..117, }, ), @@ -144,10 +165,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 118..150, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..123, id: Name("x"), ctx: Store, @@ -155,6 +178,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..131, id: Name("iter"), ctx: Load, @@ -163,6 +187,7 @@ Module( body: [ Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 133..138, }, ), @@ -170,6 +195,7 @@ Module( orelse: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 146..150, }, ), @@ -178,10 +204,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 151..202, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 156..160, }, ), @@ -190,9 +218,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 161..177, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 168..171, id: Name("exc"), ctx: Load, @@ -203,6 +233,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 173..177, }, ), @@ -213,6 +244,7 @@ Module( orelse: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 184..188, }, ), @@ -220,6 +252,7 @@ Module( finalbody: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 198..202, }, ), @@ -229,10 +262,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 203..257, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 208..212, }, ), @@ -241,9 +276,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 214..230, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 221..224, id: Name("exc"), ctx: Load, @@ -254,6 +291,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 226..230, }, ), @@ -264,6 +302,7 @@ Module( orelse: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 238..242, }, ), @@ -271,6 +310,7 @@ Module( finalbody: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 253..257, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap index 815b74f88cd78..0e70f2a74ac12 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/named_expr_slice.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..119, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 80..92, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 80..92, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..83, id: Name("lst"), ctx: Load, @@ -24,13 +28,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 84..91, lower: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 84..88, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..85, id: Name("x"), ctx: Store, @@ -38,6 +45,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 87..88, value: Int( 1, @@ -50,10 +58,12 @@ Module( upper: Some( UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 89..91, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 90..91, value: Int( 1, @@ -73,12 +83,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..100, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 93..100, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..96, id: Name("lst"), ctx: Load, @@ -86,10 +99,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 97..100, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 97..98, value: Int( 1, @@ -100,6 +115,7 @@ Module( upper: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("x"), ctx: Load, @@ -116,9 +132,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 102..103, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 102..103, value: Int( 1, @@ -129,12 +147,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 105..114, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 105..114, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..108, id: Name("lst"), ctx: Load, @@ -142,10 +163,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 109..114, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 109..110, value: Int( 1, @@ -156,6 +179,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 111..112, value: Int( 3, @@ -166,6 +190,7 @@ Module( step: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("x"), ctx: Load, @@ -181,9 +206,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 116..117, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 116..117, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap index d0166af2f4f0e..044df9bbb71cf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/named_expr_slice_pars ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..130, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 117..129, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 117..129, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 117..120, id: Name("lst"), ctx: Load, @@ -24,13 +28,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 121..128, lower: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 121..125, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..122, id: Name("x"), ctx: Store, @@ -38,6 +45,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 124..125, value: Int( 1, @@ -50,10 +58,12 @@ Module( upper: Some( UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 126..128, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 127..128, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap index 465ef924e78d6..9d6245421f2e9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/nested_async_comprehe ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..467, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..111, is_async: true, decorator_list: [], name: Identifier { id: Name("f"), range: 54..55, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 55..57, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,16 +37,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 59..111, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 66..111, elt: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 67..92, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -49,8 +59,10 @@ Module( generators: [ Comprehension { range: 70..91, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..81, id: Name("x"), ctx: Store, @@ -58,9 +70,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 85..91, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..88, id: Name("foo"), ctx: Load, @@ -68,9 +82,11 @@ Module( ), arguments: Arguments { range: 88..91, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("n"), ctx: Load, @@ -90,8 +106,10 @@ Module( generators: [ Comprehension { range: 93..110, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("n"), ctx: Store, @@ -99,9 +117,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 102..110, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..107, id: Name("range"), ctx: Load, @@ -109,9 +129,11 @@ Module( ), arguments: Arguments { range: 107..110, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 108..109, value: Int( 3, @@ -137,16 +159,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 122..192, is_async: true, decorator_list: [], name: Identifier { id: Name("g"), range: 132..133, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 133..135, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -157,16 +184,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 137..192, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 144..192, elt: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 145..173, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("x"), ctx: Load, @@ -174,6 +205,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 149..150, value: Int( 1, @@ -183,8 +215,10 @@ Module( generators: [ Comprehension { range: 151..172, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 161..162, id: Name("x"), ctx: Store, @@ -192,9 +226,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 166..172, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..169, id: Name("foo"), ctx: Load, @@ -202,9 +238,11 @@ Module( ), arguments: Arguments { range: 169..172, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("n"), ctx: Load, @@ -224,8 +262,10 @@ Module( generators: [ Comprehension { range: 174..191, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("n"), ctx: Store, @@ -233,9 +273,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 183..191, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 183..188, id: Name("range"), ctx: Load, @@ -243,9 +285,11 @@ Module( ), arguments: Arguments { range: 188..191, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 189..190, value: Int( 3, @@ -271,16 +315,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 200..267, is_async: true, decorator_list: [], name: Identifier { id: Name("h"), range: 210..211, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 211..213, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -291,16 +340,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 215..267, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 222..267, elt: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 223..248, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 224..225, id: Name("x"), ctx: Load, @@ -309,8 +362,10 @@ Module( generators: [ Comprehension { range: 226..247, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("x"), ctx: Store, @@ -318,9 +373,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 241..247, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..244, id: Name("foo"), ctx: Load, @@ -328,9 +385,11 @@ Module( ), arguments: Arguments { range: 244..247, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("n"), ctx: Load, @@ -350,8 +409,10 @@ Module( generators: [ Comprehension { range: 249..266, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 253..254, id: Name("n"), ctx: Store, @@ -359,9 +420,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 258..266, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 258..263, id: Name("range"), ctx: Load, @@ -369,9 +432,11 @@ Module( ), arguments: Arguments { range: 263..266, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 264..265, value: Int( 3, @@ -397,16 +462,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 277..371, is_async: true, decorator_list: [], name: Identifier { id: Name("i"), range: 287..288, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 288..290, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -417,20 +487,25 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 292..371, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 299..371, elt: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 300..352, elts: [ ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 301..328, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 302..303, id: Name("y"), ctx: Load, @@ -439,8 +514,10 @@ Module( generators: [ Comprehension { range: 304..327, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 314..315, id: Name("y"), ctx: Store, @@ -448,9 +525,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 319..327, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 319..324, id: Name("range"), ctx: Load, @@ -458,9 +537,11 @@ Module( ), arguments: Arguments { range: 324..327, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 325..326, value: Int( 1, @@ -480,9 +561,11 @@ Module( ), ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 330..351, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 331..332, id: Name("z"), ctx: Load, @@ -491,8 +574,10 @@ Module( generators: [ Comprehension { range: 333..350, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 337..338, id: Name("z"), ctx: Store, @@ -500,9 +585,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 342..350, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 342..347, id: Name("range"), ctx: Load, @@ -510,9 +597,11 @@ Module( ), arguments: Arguments { range: 347..350, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 348..349, value: Int( 2, @@ -538,8 +627,10 @@ Module( generators: [ Comprehension { range: 353..370, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 357..358, id: Name("x"), ctx: Store, @@ -547,9 +638,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 362..370, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 362..367, id: Name("range"), ctx: Load, @@ -557,9 +650,11 @@ Module( ), arguments: Arguments { range: 367..370, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 368..369, value: Int( 5, @@ -585,16 +680,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 372..466, is_async: true, decorator_list: [], name: Identifier { id: Name("j"), range: 382..383, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 383..385, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -605,20 +705,25 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 387..466, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 394..466, elt: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 395..447, elts: [ ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 396..417, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 397..398, id: Name("y"), ctx: Load, @@ -627,8 +732,10 @@ Module( generators: [ Comprehension { range: 399..416, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 403..404, id: Name("y"), ctx: Store, @@ -636,9 +743,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 408..416, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 408..413, id: Name("range"), ctx: Load, @@ -646,9 +755,11 @@ Module( ), arguments: Arguments { range: 413..416, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 414..415, value: Int( 1, @@ -668,9 +779,11 @@ Module( ), ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 419..446, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 420..421, id: Name("z"), ctx: Load, @@ -679,8 +792,10 @@ Module( generators: [ Comprehension { range: 422..445, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 432..433, id: Name("z"), ctx: Store, @@ -688,9 +803,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 437..445, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 437..442, id: Name("range"), ctx: Load, @@ -698,9 +815,11 @@ Module( ), arguments: Arguments { range: 442..445, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 443..444, value: Int( 2, @@ -726,8 +845,10 @@ Module( generators: [ Comprehension { range: 448..465, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 452..453, id: Name("x"), ctx: Store, @@ -735,9 +856,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 457..465, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 457..462, id: Name("range"), ctx: Load, @@ -745,9 +868,11 @@ Module( ), arguments: Arguments { range: 462..465, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 463..464, value: Int( 5, @@ -780,7 +905,7 @@ Module( | 1 | # parse_options: {"target-version": "3.10"} 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list - | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set | @@ -790,7 +915,7 @@ Module( 1 | # parse_options: {"target-version": "3.10"} 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict - | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] | @@ -800,7 +925,7 @@ Module( 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set - | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] | @@ -810,7 +935,7 @@ Module( 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] - | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] | @@ -819,5 +944,5 @@ Module( 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] - | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap index e6364e6c4c577..95c44e302d5e4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/node_range_with_gaps. ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..41, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..7, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..7, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -33,16 +39,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 18..32, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 22..25, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 25..27, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -53,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), @@ -66,16 +79,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 33..40, is_async: false, decorator_list: [], name: Identifier { id: Name("baz"), range: 37..40, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 40..40, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap new file mode 100644 index 0000000000000..d49417620bdb0 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..25, + body: [ + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 0..10, + names: [ + Identifier { + id: Name("x"), + range: 9..10, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 11..24, + names: [ + Identifier { + id: Name("x"), + range: 20..21, + node_index: AtomicNodeIndex(..), + }, + Identifier { + id: Name("y"), + range: 23..24, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | nonlocal x + | ^^^^^^^^^^ Syntax Error: nonlocal declaration not allowed at module level +2 | nonlocal x, y + | + + + | +1 | nonlocal x +2 | nonlocal x, y + | ^^^^^^^^^^^^^ Syntax Error: nonlocal declaration not allowed at module level + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap index 08015c08ab20e..dde2775dc31d2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap @@ -1,19 +1,48 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..9, + node_index: AtomicNodeIndex(..), + range: 0..22, body: [ - Nonlocal( - StmtNonlocal { - range: 0..8, - names: [], + FunctionDef( + StmtFunctionDef { + node_index: AtomicNodeIndex(..), + range: 0..21, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + node_index: AtomicNodeIndex(..), + }, + type_params: None, + parameters: Parameters { + range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 13..21, + names: [], + }, + ), + ], }, ), ], @@ -23,6 +52,7 @@ Module( ## Errors | -1 | nonlocal - | ^ Syntax Error: Nonlocal statement must have at least one name +1 | def _(): +2 | nonlocal + | ^ Syntax Error: Nonlocal statement must have at least one name | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap index 7903a99470370..739b49643a708 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap @@ -1,45 +1,78 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..15, + node_index: AtomicNodeIndex(..), + range: 0..28, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [ - Identifier { - id: Name("x"), - range: 9..10, - }, + FunctionDef( + StmtFunctionDef { + node_index: AtomicNodeIndex(..), + range: 0..27, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + node_index: AtomicNodeIndex(..), + }, + type_params: None, + parameters: Parameters { + range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 24..27, + value: UnaryOp( + ExprUnaryOp { + node_index: AtomicNodeIndex(..), + range: 24..27, + op: UAdd, + operand: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 26..27, + value: Int( + 1, + ), + }, + ), + }, + ), + }, + ), ], }, ), - Expr( - StmtExpr { - range: 11..14, - value: UnaryOp( - ExprUnaryOp { - range: 11..14, - op: UAdd, - operand: NumberLiteral( - ExprNumberLiteral { - range: 13..14, - value: Int( - 1, - ), - }, - ), - }, - ), - }, - ), ], }, ) @@ -47,6 +80,7 @@ Module( ## Errors | -1 | nonlocal x + 1 - | ^ Syntax Error: Simple statements must be separated by newlines or semicolons +1 | def _(): +2 | nonlocal x + 1 + | ^ Syntax Error: Simple statements must be separated by newlines or semicolons | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap index 4a901178b0a95..10cedaf7ba326 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap @@ -1,44 +1,78 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..38, + node_index: AtomicNodeIndex(..), + range: 0..59, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [], - }, - ), - Nonlocal( - StmtNonlocal { - range: 11..22, - names: [ - Identifier { - id: Name("x"), - range: 20..21, - }, - ], - }, - ), - Nonlocal( - StmtNonlocal { - range: 23..37, - names: [ - Identifier { - id: Name("x"), - range: 32..33, - }, - Identifier { - id: Name("y"), - range: 35..36, - }, + FunctionDef( + StmtFunctionDef { + node_index: AtomicNodeIndex(..), + range: 0..58, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + node_index: AtomicNodeIndex(..), + }, + type_params: None, + parameters: Parameters { + range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 13..23, + names: [], + }, + ), + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 28..39, + names: [ + Identifier { + id: Name("x"), + range: 37..38, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 44..58, + names: [ + Identifier { + id: Name("x"), + range: 53..54, + node_index: AtomicNodeIndex(..), + }, + Identifier { + id: Name("y"), + range: 56..57, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), ], }, ), @@ -49,32 +83,35 @@ Module( ## Errors | -1 | nonlocal , - | ^ Syntax Error: Expected an identifier -2 | nonlocal x, -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , + | ^ Syntax Error: Expected an identifier +3 | nonlocal x, +4 | nonlocal x, y, | | -1 | nonlocal , - | ^ Syntax Error: Nonlocal statement must have at least one name -2 | nonlocal x, -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , + | ^ Syntax Error: Nonlocal statement must have at least one name +3 | nonlocal x, +4 | nonlocal x, y, | | -1 | nonlocal , -2 | nonlocal x, - | ^ Syntax Error: Trailing comma not allowed -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , +3 | nonlocal x, + | ^ Syntax Error: Trailing comma not allowed +4 | nonlocal x, y, | | -1 | nonlocal , -2 | nonlocal x, -3 | nonlocal x, y, - | ^ Syntax Error: Trailing comma not allowed +2 | nonlocal , +3 | nonlocal x, +4 | nonlocal x, y, + | ^ Syntax Error: Trailing comma not allowed | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap index b0d2dec14add7..f23ebd2f93cc2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/param_missing_annotation.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..35, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..16, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..11, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..10, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -45,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..16, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 13..16, }, ), @@ -58,25 +68,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 17..34, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 21..24, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 24..29, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 25..27, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 25..27, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 25..26, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -91,9 +109,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..34, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 31..34, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap index f263e09822eff..ee03c9a0a6a56 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/param_missing_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..41, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..16, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..11, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..10, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -45,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..16, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 13..16, }, ), @@ -58,29 +68,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 17..40, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 21..24, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 24..35, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 25..33, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 25..31, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 25..26, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..31, id: Name("int"), ctx: Load, @@ -99,9 +118,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 37..40, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 37..40, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap index c63c0fdb25a9e..779f868a448ae 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap @@ -7,36 +7,47 @@ input_file: crates/ruff_python_parser/resources/inline/err/param_with_invalid_an ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..81, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..23, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..18, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..17, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 8..11, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -58,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..23, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, }, ), @@ -71,33 +84,43 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 24..52, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 28..31, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 31..47, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 32..46, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 32..46, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 32..35, + node_index: AtomicNodeIndex(..), }, annotation: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 37..46, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..46, id: Name("int"), ctx: Load, @@ -119,9 +142,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), @@ -132,29 +157,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 53..80, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 57..60, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 60..75, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 61..67, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 61..67, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 61..64, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("x"), ctx: Load, @@ -166,11 +200,14 @@ Module( }, ParameterWithDefault { range: 71..74, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 71..74, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("int"), range: 71..74, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -185,9 +222,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 77..80, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 77..80, }, ), @@ -224,3 +263,13 @@ Module( 3 | def foo(arg: x := int): ... | ^^ Syntax Error: Expected ',', found ':=' | + + +## Semantic Syntax Errors + + | +1 | def foo(arg: *int): ... +2 | def foo(arg: yield int): ... + | ^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +3 | def foo(arg: x := int): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap index 6bbc3b41f52fa..bec69b1894bed 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap @@ -1,45 +1,55 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/param_with_invalid_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..68, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..20, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..15, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..14, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 10..14, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..14, id: Name("int"), ctx: Load, @@ -59,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, }, ), @@ -72,34 +84,44 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 21..43, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 25..28, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 28..38, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 29..37, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 29..30, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 29..30, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 32..36, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..36, id: Name("int"), ctx: Load, @@ -119,9 +141,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..43, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 40..43, }, ), @@ -132,35 +156,45 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..67, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 48..51, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 51..62, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 52..61, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 52..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 52..53, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 54..61, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("y"), ctx: Load, @@ -180,9 +214,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 64..67, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 64..67, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap index f036adb4b7691..9360cfde290d9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap @@ -7,35 +7,45 @@ input_file: crates/ruff_python_parser/resources/inline/err/param_with_invalid_st ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..150, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..22, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 8..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 9..13, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 15..16, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..16, id: Name(""), ctx: Invalid, @@ -54,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -67,34 +79,44 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 23..57, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 27..30, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 30..52, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 31..51, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 32..36, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 39..50, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 40..50, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..45, id: Name("tuple"), ctx: Load, @@ -102,6 +124,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..49, id: Name("int"), ctx: Load, @@ -123,9 +146,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..57, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 54..57, }, ), @@ -136,36 +161,46 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 58..90, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 62..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..85, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 66..84, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 67..71, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 73..84, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 74..84, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..77, id: Name("int"), ctx: Load, @@ -173,6 +208,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..84, id: Name("str"), ctx: Load, @@ -194,9 +230,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 87..90, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 87..90, }, ), @@ -207,35 +245,45 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 91..120, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 95..98, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 98..115, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 99..114, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 100..104, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 106..114, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 107..114, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("x"), ctx: Load, @@ -257,9 +305,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 117..120, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 117..120, }, ), @@ -308,3 +358,14 @@ Module( | ^^^^^^^ Syntax Error: Yield expression cannot be used here 5 | # def foo(*args: **int): ... | + + +## Semantic Syntax Errors + + | +2 | def foo(*args: (*tuple[int])): ... +3 | def foo(*args: *int or str): ... +4 | def foo(*args: *yield x): ... + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +5 | # def foo(*args: **int): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap index ef87ca461ff9d..224824a9e7325 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap @@ -7,35 +7,45 @@ input_file: crates/ruff_python_parser/resources/inline/err/param_with_star_annot ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..69, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..68, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 48..51, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 51..63, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 52..62, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 53..57, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 59..62, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..62, id: Name("Ts"), ctx: Load, @@ -54,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 65..68, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 65..68, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap index f7bfc720440ac..9e0f94fdad06f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_duplicate_names.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..42, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..41, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..36, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -38,17 +46,21 @@ Module( }, ParameterWithDefault { range: 11..15, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 11..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 11..12, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..15, value: Int( 10, @@ -61,9 +73,11 @@ Module( vararg: Some( Parameter { range: 17..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 18..19, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -71,11 +85,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 21..22, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 21..22, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 21..22, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -83,15 +100,19 @@ Module( }, ParameterWithDefault { range: 24..30, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 24..30, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 24..25, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..30, id: Name("str"), ctx: Load, @@ -105,9 +126,11 @@ Module( kwarg: Some( Parameter { range: 32..35, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 34..35, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -117,9 +140,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 38..41, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 38..41, }, ), @@ -132,7 +157,7 @@ Module( }, ) ``` -## Errors +## Semantic Syntax Errors | 1 | def foo(a, a=10, *a, a, a: str, **a): ... diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap index 9a1f540021a7e..c2fdb7f4d5043 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_expected_after_star_separator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..98, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..15, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..10, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -32,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..15, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 12..15, }, ), @@ -45,16 +52,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 16..32, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 20..23, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 23..27, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -65,9 +77,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), @@ -78,25 +92,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 33..51, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 37..40, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 40..46, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 41..42, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 41..42, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 41..42, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -111,9 +133,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 48..51, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 48..51, }, ), @@ -124,25 +148,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 52..71, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 56..59, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 59..66, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 60..61, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 60..61, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 60..61, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -157,9 +189,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..71, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 68..71, }, ), @@ -170,16 +204,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 72..97, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 76..79, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 79..92, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -187,9 +226,11 @@ Module( kwarg: Some( Parameter { range: 83..91, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 85..91, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -199,9 +240,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..97, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 94..97, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap index 3decf036cf2ed..71846f8fc78c2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_kwarg_after_star_separator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..26, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..25, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..20, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -29,9 +34,11 @@ Module( kwarg: Some( Parameter { range: 11..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 13..19, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -41,9 +48,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..25, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 22..25, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap index 6fa8e4a41d19a..f26f826c50ebb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_multiple_kwargs.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..38, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..37, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..32, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -42,9 +50,11 @@ Module( kwarg: Some( Parameter { range: 22..31, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs2"), range: 24..31, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -54,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..37, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 34..37, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap index 5c081e0e49828..5bbcc79890345 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap @@ -1,35 +1,43 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_multiple_slash_separator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..24, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..19, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -39,11 +47,14 @@ Module( args: [ ParameterWithDefault { range: 17..18, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 17..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 17..18, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -58,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..24, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 21..24, }, ), @@ -71,24 +84,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 25..52, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 29..32, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 32..47, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 33..34, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 33..34, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 33..34, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -98,11 +119,14 @@ Module( args: [ ParameterWithDefault { range: 39..40, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 39..40, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 39..40, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -110,11 +134,14 @@ Module( }, ParameterWithDefault { range: 42..43, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 42..43, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 42..43, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -129,9 +156,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap index 68c9459f96df1..9e2ccfb2c9a61 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_multiple_star_separator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..24, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..19, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -41,11 +49,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 17..18, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 17..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 17..18, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -58,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..24, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 21..24, }, ), @@ -71,25 +84,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 25..52, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 29..32, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 32..47, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 33..34, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 33..34, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 33..34, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -100,11 +121,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 39..40, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 39..40, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 39..40, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -112,11 +136,14 @@ Module( }, ParameterWithDefault { range: 42..43, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 42..43, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 42..43, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -129,9 +156,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap index 4e350369bf888..a8a26d73bc2c0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_multiple_varargs.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..136, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..23, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -40,9 +48,11 @@ Module( vararg: Some( Parameter { range: 14..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 15..19, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -50,11 +60,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 21..22, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 21..22, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 21..22, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -67,9 +80,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -80,25 +95,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 63..97, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 67..70, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 70..92, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 71..72, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 71..72, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 71..72, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -108,9 +131,11 @@ Module( vararg: Some( Parameter { range: 74..80, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args1"), range: 75..80, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -118,11 +143,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 90..91, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 90..91, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 90..91, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -135,9 +163,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..97, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 94..97, }, ), @@ -148,25 +178,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 98..135, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 102..105, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 105..130, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 106..107, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 106..107, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 106..107, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -176,9 +214,11 @@ Module( vararg: Some( Parameter { range: 109..115, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args1"), range: 110..115, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -186,11 +226,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 117..118, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 117..118, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 117..118, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -198,11 +241,14 @@ Module( }, ParameterWithDefault { range: 120..121, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 120..121, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 120..121, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -215,9 +261,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 132..135, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 132..135, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap index 0a3233a0be0a7..be7f6b1b35253 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_no_arg_before_slash.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..35, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..15, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..10, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -32,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..15, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 12..15, }, ), @@ -45,25 +52,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 16..34, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 20..23, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 23..29, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 27..28, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 27..28, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 27..28, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -78,9 +93,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..34, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 31..34, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap index b8ce121d93bcc..4e01047ebb876 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap @@ -1,42 +1,51 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_non_default_after_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..30, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..29, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..24, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..12, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..12, value: Int( 10, @@ -47,11 +56,14 @@ Module( }, ParameterWithDefault { range: 14..15, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 14..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 14..15, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -59,15 +71,19 @@ Module( }, ParameterWithDefault { range: 17..23, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 17..23, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 17..18, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..23, id: Name("int"), ctx: Load, @@ -86,9 +102,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..29, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 26..29, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap index e1bb0c58b95bd..c2db1084e7780 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap @@ -1,35 +1,42 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_star_after_slash.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..105, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..19, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..14, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 8..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 9..10, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -41,9 +48,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..19, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 16..19, }, ), @@ -54,24 +63,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 20..48, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 24..27, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 27..43, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 28..29, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 28..29, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 28..29, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -82,9 +99,11 @@ Module( vararg: Some( Parameter { range: 31..36, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 32..36, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -92,11 +111,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 38..39, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 38..39, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 38..39, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -109,9 +131,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 45..48, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 45..48, }, ), @@ -122,24 +146,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 49..73, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 53..56, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 56..68, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 57..58, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 57..58, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 57..58, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -151,11 +183,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 66..67, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 66..67, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 66..67, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -168,9 +203,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..73, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 70..73, }, ), @@ -181,24 +218,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 74..104, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 78..81, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 81..99, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 82..83, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 82..83, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 82..83, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -210,11 +255,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 88..89, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 88..89, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 88..89, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -222,11 +270,14 @@ Module( }, ParameterWithDefault { range: 91..92, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 91..92, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 91..92, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -234,11 +285,14 @@ Module( }, ParameterWithDefault { range: 97..98, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 97..98, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 97..98, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -251,9 +305,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 101..104, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 101..104, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap index 0521d51896083..aa5149155a8f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_star_separator_after_star_param.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..61, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..23, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -40,9 +48,11 @@ Module( vararg: Some( Parameter { range: 11..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 12..16, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -50,11 +60,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 21..22, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 21..22, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 21..22, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -67,9 +80,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -80,25 +95,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 29..60, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 33..36, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 36..55, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 37..38, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 37..38, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 37..38, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -108,9 +131,11 @@ Module( vararg: Some( Parameter { range: 40..45, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 41..45, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -118,11 +143,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 47..48, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 47..48, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 47..48, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -130,11 +158,14 @@ Module( }, ParameterWithDefault { range: 50..51, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 50..51, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 50..51, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -147,9 +178,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..60, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 57..60, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap index 9bde14d96327e..f56f7d96abdc1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_var_keyword_with_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..43, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..36, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..20, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -42,9 +50,11 @@ Module( kwarg: Some( Parameter { range: 11..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 13..19, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -54,20 +64,24 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..36, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 20..36, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 21..24, value: StringLiteralValue { inner: Single( StringLiteral { range: 21..24, + node_index: AtomicNodeIndex(..), value: "b", flags: StringLiteralFlags { quote_style: Single, @@ -82,6 +96,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 26..27, value: Int( 1, @@ -93,11 +108,13 @@ Module( key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, value: StringLiteralValue { inner: Single( StringLiteral { range: 29..32, + node_index: AtomicNodeIndex(..), value: "c", flags: StringLiteralFlags { quote_style: Single, @@ -112,6 +129,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 34..35, value: Int( 2, @@ -129,9 +147,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 39..42, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 39..42, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap index cafb4857d2a91..5ca631e7d1f50 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap @@ -1,36 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_var_positional_with_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..30, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..23, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..9, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -40,9 +48,11 @@ Module( vararg: Some( Parameter { range: 11..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 12..16, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -54,13 +64,16 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..23, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 17..23, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 1, @@ -69,6 +82,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..22, value: Int( 2, @@ -87,9 +101,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..29, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 26..29, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap index 0cc6748cbb185..ca32f57b18313 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_context ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..126, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 43..73, is_async: false, items: [ WithItem { range: 49..57, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..52, id: Name("foo"), ctx: Load, @@ -26,6 +30,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Store, @@ -35,8 +40,10 @@ Module( }, WithItem { range: 59..67, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..62, id: Name("bar"), ctx: Load, @@ -45,6 +52,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("y"), ctx: Store, @@ -56,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..73, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 70..73, }, ), @@ -69,13 +79,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 74..99, is_async: false, items: [ WithItem { range: 80..83, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..83, id: Name("foo"), ctx: Load, @@ -85,8 +98,10 @@ Module( }, WithItem { range: 85..93, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..88, id: Name("bar"), ctx: Load, @@ -95,6 +110,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..93, id: Name("y"), ctx: Store, @@ -106,9 +122,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), @@ -119,13 +137,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 100..125, is_async: false, items: [ WithItem { range: 106..114, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..109, id: Name("foo"), ctx: Load, @@ -134,6 +155,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("x"), ctx: Store, @@ -143,8 +165,10 @@ Module( }, WithItem { range: 116..119, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..119, id: Name("bar"), ctx: Load, @@ -156,9 +180,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..125, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 122..125, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap index 4b55ae1a8a6ff..90fb2c97daf10 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_kwarg_p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..77, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..51, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 43..51, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("f"), ctx: Load, @@ -24,18 +28,22 @@ Module( ), arguments: Arguments { range: 44..51, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 45..50, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 46..47, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 49..50, value: Int( 1, @@ -51,12 +59,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 52..62, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 52..62, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..53, id: Name("f"), ctx: Load, @@ -64,18 +75,22 @@ Module( ), arguments: Arguments { range: 53..62, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 54..61, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 55..56, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 60..61, value: Int( 1, @@ -91,12 +106,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 63..76, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 63..76, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..64, id: Name("f"), ctx: Load, @@ -104,18 +122,22 @@ Module( ), arguments: Arguments { range: 64..76, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 66..75, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 68..69, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 74..75, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap index 5ac816ecc872b..78be042c66925 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap @@ -7,34 +7,42 @@ input_file: crates/ruff_python_parser/resources/inline/err/pep701_f_string_py311 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..549, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..74, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 44..74, value: FStringValue { inner: Single( FString( FString { range: 44..74, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..58, + node_index: AtomicNodeIndex(..), value: "Magic wand: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 58..73, + node_index: AtomicNodeIndex(..), expression: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 60..71, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..63, id: Name("bag"), ctx: Load, @@ -42,11 +50,13 @@ Module( ), slice: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 64..70, value: StringLiteralValue { inner: Single( StringLiteral { range: 64..70, + node_index: AtomicNodeIndex(..), value: "wand", flags: StringLiteralFlags { quote_style: Single, @@ -82,32 +92,40 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 95..112, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 95..112, value: FStringValue { inner: Single( FString( FString { range: 95..112, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 97..111, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 98..110, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 98..107, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 98..102, value: StringLiteralValue { inner: Single( StringLiteral { range: 98..102, + node_index: AtomicNodeIndex(..), value: "\n", flags: StringLiteralFlags { quote_style: Single, @@ -122,15 +140,18 @@ Module( attr: Identifier { id: Name("join"), range: 103..107, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 107..110, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("a"), ctx: Load, @@ -162,30 +183,37 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..220, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 148..220, value: FStringValue { inner: Single( FString( FString { range: 148..220, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 152..169, + node_index: AtomicNodeIndex(..), value: "A complex trick: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 169..217, + node_index: AtomicNodeIndex(..), expression: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 175..185, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 175..178, id: Name("bag"), ctx: Load, @@ -193,11 +221,13 @@ Module( ), slice: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 179..184, value: StringLiteralValue { inner: Single( StringLiteral { range: 179..184, + node_index: AtomicNodeIndex(..), value: "bag", flags: StringLiteralFlags { quote_style: Single, @@ -233,84 +263,105 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 221..254, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 221..254, value: FStringValue { inner: Single( FString( FString { range: 221..254, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 223..253, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 224..252, value: FStringValue { inner: Single( FString( FString { range: 224..252, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 226..251, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 227..250, value: FStringValue { inner: Single( FString( FString { range: 227..250, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 229..249, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 230..248, value: FStringValue { inner: Single( FString( FString { range: 230..248, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 232..247, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 233..246, value: FStringValue { inner: Single( FString( FString { range: 233..246, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 235..245, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 236..244, value: FStringValue { inner: Single( FString( FString { range: 236..244, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 238..243, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 239..242, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 239..240, value: Int( 1, @@ -320,6 +371,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 241..242, value: Int( 1, @@ -434,38 +486,47 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 276..310, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 276..310, value: FStringValue { inner: Single( FString( FString { range: 276..310, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 278..303, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 279..302, value: FStringValue { inner: Single( FString( FString { range: 279..302, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 283..293, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 284..292, value: StringLiteralValue { inner: Single( StringLiteral { range: 284..292, + node_index: AtomicNodeIndex(..), value: "nested", flags: StringLiteralFlags { quote_style: Double, @@ -483,8 +544,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 293..299, + node_index: AtomicNodeIndex(..), value: " inner", }, ), @@ -506,8 +568,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 303..309, + node_index: AtomicNodeIndex(..), value: " outer", }, ), @@ -527,27 +590,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 336..359, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 336..359, value: FStringValue { inner: Single( FString( FString { range: 336..359, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 338..343, + node_index: AtomicNodeIndex(..), value: "test ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 343..353, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 344..345, id: Name("a"), ctx: Load, @@ -559,8 +628,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 353..358, + node_index: AtomicNodeIndex(..), value: " more", }, ), @@ -580,33 +650,41 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 403..422, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 403..422, value: FStringValue { inner: Single( FString( FString { range: 403..422, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 407..419, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 408..418, value: FStringValue { inner: Single( FString( FString { range: 408..418, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 412..415, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 413..414, id: Name("x"), ctx: Load, @@ -650,32 +728,40 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 468..502, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 468..502, value: FStringValue { inner: Single( FString( FString { range: 468..502, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 470..501, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 471..500, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 471..480, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 471..475, value: StringLiteralValue { inner: Single( StringLiteral { range: 471..475, + node_index: AtomicNodeIndex(..), value: "\n", flags: StringLiteralFlags { quote_style: Single, @@ -690,24 +776,29 @@ Module( attr: Identifier { id: Name("join"), range: 476..480, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 480..500, + node_index: AtomicNodeIndex(..), args: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 481..499, elts: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 482..486, value: StringLiteralValue { inner: Single( StringLiteral { range: 482..486, + node_index: AtomicNodeIndex(..), value: "\t", flags: StringLiteralFlags { quote_style: Single, @@ -721,11 +812,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 488..492, value: StringLiteralValue { inner: Single( StringLiteral { range: 488..492, + node_index: AtomicNodeIndex(..), value: "\u{b}", flags: StringLiteralFlags { quote_style: Single, @@ -739,11 +832,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 494..498, value: StringLiteralValue { inner: Single( StringLiteral { range: 494..498, + node_index: AtomicNodeIndex(..), value: "\r", flags: StringLiteralFlags { quote_style: Single, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap index adb8b30bc14dd..6e1bdfaf6e122 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap @@ -7,28 +7,37 @@ input_file: crates/ruff_python_parser/resources/inline/err/pos_only_py37.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..136, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 43..61, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 47..50, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 50..56, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 51..52, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 51..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 51..52, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -44,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), @@ -57,24 +68,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 62..86, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 66..69, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 69..81, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 70..71, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 70..71, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 70..71, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -84,11 +103,14 @@ Module( args: [ ParameterWithDefault { range: 76..77, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 76..77, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 76..77, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -103,9 +125,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..86, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 83..86, }, ), @@ -116,24 +140,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 87..115, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 91..94, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 94..110, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 95..96, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 95..96, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 95..96, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -144,9 +176,11 @@ Module( vararg: Some( Parameter { range: 98..103, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 99..103, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -154,11 +188,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 108..109, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 108..109, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 108..109, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -171,9 +208,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 112..115, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 112..115, }, ), @@ -184,25 +223,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 116..135, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 120..123, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 123..130, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 124..125, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 124..125, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 124..125, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -217,9 +264,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 132..135, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 132..135, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap index bfd31fbf6dc4c..d6a06bfe2f1e5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap @@ -7,15 +7,18 @@ input_file: crates/ruff_python_parser/resources/inline/err/raise_stmt_from_witho ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..31, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 0..14, exc: None, cause: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..14, id: Name("exc"), ctx: Load, @@ -26,11 +29,13 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 15..30, exc: None, cause: Some( NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 26..30, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap index 5d6d3eef90d22..969eb0b526af8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/raise_stmt_invalid_cause.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 0..15, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -25,9 +27,11 @@ Module( cause: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..15, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("y"), ctx: Load, @@ -41,10 +45,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 16..36, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -54,10 +60,12 @@ Module( cause: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 29..36, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("y"), ctx: Load, @@ -71,10 +79,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 37..51, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("x"), ctx: Load, @@ -84,6 +94,7 @@ Module( cause: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 50..51, id: Name("y"), ctx: Load, @@ -94,9 +105,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 55..56, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 55..56, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap index 3bb6f03074036..fc1d86ea861a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/raise_stmt_invalid_exc.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..36, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 0..8, exc: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, @@ -33,14 +36,17 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 9..22, exc: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 15..22, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("x"), ctx: Load, @@ -55,10 +61,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 23..30, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Load, @@ -70,9 +78,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..35, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 34..35, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap index c000ffe470775..d25483b1bc1e2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/raise_stmt_unparenthesized_tuple_cause.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..34, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 0..15, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -25,10 +27,12 @@ Module( cause: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 13..15, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("y"), ctx: Load, @@ -44,10 +48,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 16..33, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -57,10 +63,12 @@ Module( cause: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 29..33, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("y"), ctx: Load, @@ -68,6 +76,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap index a5bd41c66eb8c..36f4db810c864 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/raise_stmt_unparenthesized_tuple_exc.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..38, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 0..8, exc: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 6..8, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -36,14 +39,17 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 9..19, exc: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 15..19, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("x"), ctx: Load, @@ -51,6 +57,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("y"), ctx: Load, @@ -67,14 +74,17 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 20..37, exc: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 26..30, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("x"), ctx: Load, @@ -82,6 +92,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("y"), ctx: Load, @@ -96,6 +107,7 @@ Module( cause: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap index 48dc7f7866b8d..aec52e3b459e7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lex_logical_token.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..979, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 48..59, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 51..59, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..55, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 55..59, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..59, id: Name("foo"), ctx: Load, @@ -43,16 +49,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..79, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 64..67, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 67..69, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -63,6 +74,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 75..79, }, ), @@ -71,12 +83,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 113..152, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 116..124, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..120, id: Name("call"), ctx: Load, @@ -84,9 +99,11 @@ Module( ), arguments: Arguments { range: 120..124, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..124, id: Name("foo"), ctx: Load, @@ -100,16 +117,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 129..152, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 133..136, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 136..138, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -120,6 +142,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 148..152, }, ), @@ -132,12 +155,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 228..269, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 231..239, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 231..235, id: Name("call"), ctx: Load, @@ -145,9 +171,11 @@ Module( ), arguments: Arguments { range: 235..239, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..239, id: Name("foo"), ctx: Load, @@ -161,16 +189,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 246..269, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 250..253, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 253..255, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -181,6 +214,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 265..269, }, ), @@ -193,12 +227,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 344..392, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 347..355, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 347..351, id: Name("call"), ctx: Load, @@ -206,9 +243,11 @@ Module( ), arguments: Arguments { range: 351..355, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 352..355, id: Name("foo"), ctx: Load, @@ -222,16 +261,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 369..392, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 373..376, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 376..378, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -242,6 +286,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 388..392, }, ), @@ -254,12 +299,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 453..499, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 456..472, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 456..460, id: Name("call"), ctx: Load, @@ -267,9 +315,11 @@ Module( ), arguments: Arguments { range: 460..472, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 461..464, id: Name("foo"), ctx: Load, @@ -277,10 +327,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 466..471, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 467..468, id: Name("a"), ctx: Load, @@ -288,6 +340,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 470..471, id: Name("b"), ctx: Load, @@ -305,16 +358,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 476..499, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 480..483, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 483..485, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -325,6 +383,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 495..499, }, ), @@ -337,12 +396,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 564..611, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 567..583, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 567..571, id: Name("call"), ctx: Load, @@ -350,9 +412,11 @@ Module( ), arguments: Arguments { range: 571..583, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 572..575, id: Name("foo"), ctx: Load, @@ -360,10 +424,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 577..582, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 578..579, id: Name("a"), ctx: Load, @@ -371,6 +437,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 581..582, id: Name("b"), ctx: Load, @@ -388,16 +455,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 588..611, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 592..595, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 595..597, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -408,6 +480,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 607..611, }, ), @@ -420,12 +493,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 772..824, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 775..796, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 775..779, id: Name("call"), ctx: Load, @@ -433,9 +509,11 @@ Module( ), arguments: Arguments { range: 779..796, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 780..783, id: Name("foo"), ctx: Load, @@ -443,10 +521,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 785..794, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 786..787, id: Name("a"), ctx: Load, @@ -454,6 +534,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 793..794, id: Name("b"), ctx: Load, @@ -471,16 +552,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 801..824, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 805..808, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 808..810, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -491,6 +577,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 820..824, }, ), @@ -503,12 +590,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 887..933, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 890..905, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 890..894, id: Name("call"), ctx: Load, @@ -516,27 +606,33 @@ Module( ), arguments: Arguments { range: 894..905, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 895..905, value: FStringValue { inner: Single( FString( FString { range: 895..905, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 897..903, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 903..905, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 904..905, id: Name("x"), ctx: Load, @@ -567,16 +663,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 910..933, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 914..917, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 917..919, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -587,6 +688,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 929..933, }, ), @@ -599,12 +701,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 936..956, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 939..956, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 939..943, id: Name("call"), ctx: Load, @@ -612,15 +717,18 @@ Module( ), arguments: Arguments { range: 943..956, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 944..951, value: FStringValue { inner: Single( FString( FString { range: 944..951, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Double, @@ -644,16 +752,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 956..979, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 960..963, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 963..965, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -664,6 +777,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 975..979, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap index e9879e5ba3305..065645a7ac3f2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..46, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..46, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3..19, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..7, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 7..19, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..11, id: Name("foo"), ctx: Load, @@ -34,10 +40,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 13..18, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("a"), ctx: Load, @@ -45,6 +53,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("b"), ctx: Load, @@ -62,16 +71,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 23..46, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 27..30, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 30..32, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -82,6 +96,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 42..46, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap index 31bb3a87f2044..37943705bb106 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_windows_eol.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lex_logical_token_win ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..50, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..48, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3..20, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..7, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 7..20, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..11, id: Name("foo"), ctx: Load, @@ -34,10 +40,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 13..18, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("a"), ctx: Load, @@ -45,6 +53,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("b"), ctx: Load, @@ -62,16 +71,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 24..48, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 28..31, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 31..33, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -82,6 +96,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 44..48, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index dbc237b1d3ac0..4cb5587c2a4d6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -7,36 +7,44 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lexing/fstring_format ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..298, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 162..192, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 162..192, value: FStringValue { inner: Single( FString( FString { range: 162..192, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 164..171, + node_index: AtomicNodeIndex(..), value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 171..191, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 172..180, value: StringLiteralValue { inner: Single( StringLiteral { range: 172..180, + node_index: AtomicNodeIndex(..), value: "string", flags: StringLiteralFlags { quote_style: Single, @@ -51,12 +59,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 181..191, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 181..191, + node_index: AtomicNodeIndex(..), value: " ", }, ), @@ -81,9 +91,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 192..198, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 192..198, id: Name("format"), ctx: Load, @@ -93,9 +105,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 199..203, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..203, id: Name("spec"), ctx: Load, @@ -105,32 +119,39 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 207..228, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 207..228, value: FStringValue { inner: Single( FString( FString { range: 207..228, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 209..216, + node_index: AtomicNodeIndex(..), value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 216..228, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 217..225, value: StringLiteralValue { inner: Single( StringLiteral { range: 217..225, + node_index: AtomicNodeIndex(..), value: "string", flags: StringLiteralFlags { quote_style: Single, @@ -145,16 +166,10 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 226..228, - elements: [ - Literal( - FStringLiteralElement { - range: 226..228, - value: "\\", - }, - ), - ], + node_index: AtomicNodeIndex(..), + elements: [], }, ), }, @@ -175,14 +190,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 237..250, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 237..250, value: StringLiteralValue { inner: Single( StringLiteral { range: 237..250, + node_index: AtomicNodeIndex(..), value: "format spec", flags: StringLiteralFlags { quote_style: Single, @@ -198,32 +216,39 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 253..285, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 253..285, value: FStringValue { inner: Single( FString( FString { range: 253..285, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 255..262, + node_index: AtomicNodeIndex(..), value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 262..284, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 263..271, value: StringLiteralValue { inner: Single( StringLiteral { range: 263..271, + node_index: AtomicNodeIndex(..), value: "string", flags: StringLiteralFlags { quote_style: Single, @@ -238,12 +263,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 272..284, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 272..284, + node_index: AtomicNodeIndex(..), value: "\\ ", }, ), @@ -268,9 +295,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 285..291, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 285..291, id: Name("format"), ctx: Load, @@ -280,9 +309,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 292..296, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 292..296, id: Name("spec"), ctx: Load, @@ -346,11 +377,22 @@ Module( 6 | 'format spec'} 7 | 8 | f'middle {'string':\\ - | ^ Syntax Error: f-string: unterminated string + | ^^ Syntax Error: f-string: newlines are not allowed in format specifiers when using single quotes 9 | 'format spec'} | + | + 6 | 'format spec'} + 7 | + 8 | f'middle {'string':\\ + | ^ Syntax Error: f-string: expecting '}' + 9 | 'format spec'} +10 | +11 | f'middle {'string':\\\ + | + + | 8 | f'middle {'string':\\ 9 | 'format spec'} diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap index abeaef0f131ae..ae9e10591970e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_1.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lexing/line_continuat ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..36, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..13, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..13, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..13, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("a"), ctx: Load, @@ -34,6 +40,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("b"), ctx: Load, @@ -48,16 +55,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 16..35, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 20..23, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 23..25, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -68,6 +80,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 31..35, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap index 935f436e821bd..97fbdf8b65be5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__line_continuation_windows_eol.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lexing/line_continuat ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..46, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..10, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("call"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), arguments: Arguments { range: 4..10, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("a"), ctx: Load, @@ -34,6 +40,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("b"), ctx: Load, @@ -48,16 +55,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 26..46, is_async: false, decorator_list: [], name: Identifier { id: Name("bar"), range: 30..33, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 33..35, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -68,6 +80,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 42..46, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap index 472a8579cc596..4aa6298cabfae 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap @@ -7,31 +7,38 @@ input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..198, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 166..178, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 166..178, value: FStringValue { inner: Single( FString( FString { range: 166..178, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 170..176, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 176..178, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 177..178, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap index ccf98eb430ac1..6a553051a5cb6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_fstring_2.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..183, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 167..183, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 167..183, value: FStringValue { inner: Single( FString( FString { range: 167..183, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 171..180, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 172..175, id: Name("foo"), ctx: Load, @@ -35,12 +40,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 176..180, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 176..180, + node_index: AtomicNodeIndex(..), value: ".3f\n", }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap index 4e4ba407eeaa2..2db1a776b19cc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_fstring_3.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..262, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 231..262, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 234..253, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 234..238, id: Name("call"), ctx: Load, @@ -25,21 +28,26 @@ Module( ), arguments: Arguments { range: 238..253, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 239..253, value: FStringValue { inner: Single( FString( FString { range: 239..253, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 243..250, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 244..245, id: Name("x"), ctx: Load, @@ -48,12 +56,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 246..250, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 246..250, + node_index: AtomicNodeIndex(..), value: ".3f\n", }, ), @@ -82,6 +92,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 258..262, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap index d4093c88394d3..e14ea1a654330 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/inline/err/rebound_comprehension ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..342, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..28, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 0..28, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2..8, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("a"), ctx: Store, @@ -27,6 +32,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 0, @@ -38,8 +44,10 @@ Module( generators: [ Comprehension { range: 10..27, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("a"), ctx: Store, @@ -47,9 +55,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 19..27, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..24, id: Name("range"), ctx: Load, @@ -57,9 +67,11 @@ Module( ), arguments: Arguments { range: 24..27, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 0, @@ -81,15 +93,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..57, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 29..57, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 31..37, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..32, id: Name("a"), ctx: Store, @@ -97,6 +113,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 36..37, value: Int( 0, @@ -108,8 +125,10 @@ Module( generators: [ Comprehension { range: 39..56, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("a"), ctx: Store, @@ -117,9 +136,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 48..56, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..53, id: Name("range"), ctx: Load, @@ -127,9 +148,11 @@ Module( ), arguments: Arguments { range: 53..56, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 0, @@ -151,15 +174,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..91, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 58..91, key: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 60..66, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("a"), ctx: Store, @@ -167,6 +194,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 0, @@ -177,6 +205,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..72, id: Name("val"), ctx: Load, @@ -185,8 +214,10 @@ Module( generators: [ Comprehension { range: 73..90, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("a"), ctx: Store, @@ -194,9 +225,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 82..90, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..87, id: Name("range"), ctx: Load, @@ -204,9 +237,11 @@ Module( ), arguments: Arguments { range: 87..90, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 88..89, value: Int( 0, @@ -228,12 +263,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..125, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 92..125, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..96, id: Name("key"), ctx: Load, @@ -241,9 +279,11 @@ Module( ), value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 99..105, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("a"), ctx: Store, @@ -251,6 +291,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..105, value: Int( 0, @@ -262,8 +303,10 @@ Module( generators: [ Comprehension { range: 107..124, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..112, id: Name("a"), ctx: Store, @@ -271,9 +314,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 116..124, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..121, id: Name("range"), ctx: Load, @@ -281,9 +326,11 @@ Module( ), arguments: Arguments { range: 121..124, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 0, @@ -305,15 +352,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 126..154, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 126..154, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 128..134, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("a"), ctx: Store, @@ -321,6 +372,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 133..134, value: Int( 0, @@ -332,8 +384,10 @@ Module( generators: [ Comprehension { range: 136..153, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("a"), ctx: Store, @@ -341,9 +395,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 145..153, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..150, id: Name("range"), ctx: Load, @@ -351,9 +407,11 @@ Module( ), arguments: Arguments { range: 150..153, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 151..152, value: Int( 0, @@ -376,19 +434,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 155..185, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 155..185, elt: List( ExprList { + node_index: AtomicNodeIndex(..), range: 156..166, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 158..164, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 158..159, id: Name("a"), ctx: Store, @@ -396,6 +459,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 163..164, value: Int( 0, @@ -411,8 +475,10 @@ Module( generators: [ Comprehension { range: 167..184, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("a"), ctx: Store, @@ -420,9 +486,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 176..184, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 176..181, id: Name("range"), ctx: Load, @@ -430,9 +498,11 @@ Module( ), arguments: Arguments { range: 181..184, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 182..183, value: Int( 0, @@ -454,15 +524,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 186..233, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 186..233, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 188..194, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 188..189, id: Name("a"), ctx: Store, @@ -470,6 +544,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 193..194, value: Int( 0, @@ -481,8 +556,10 @@ Module( generators: [ Comprehension { range: 196..214, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 200..201, id: Name("b"), ctx: Store, @@ -490,9 +567,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 205..214, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 205..210, id: Name("range"), ctx: Load, @@ -500,9 +579,11 @@ Module( ), arguments: Arguments { range: 211..214, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 212..213, value: Int( 0, @@ -519,8 +600,10 @@ Module( }, Comprehension { range: 215..232, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 219..220, id: Name("a"), ctx: Store, @@ -528,9 +611,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 224..232, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 224..229, id: Name("range"), ctx: Load, @@ -538,9 +623,11 @@ Module( ), arguments: Arguments { range: 229..232, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 230..231, value: Int( 0, @@ -562,15 +649,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 234..281, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 234..281, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 236..242, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("a"), ctx: Store, @@ -578,6 +669,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 241..242, value: Int( 0, @@ -589,8 +681,10 @@ Module( generators: [ Comprehension { range: 244..262, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 248..249, id: Name("a"), ctx: Store, @@ -598,9 +692,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 253..262, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 253..258, id: Name("range"), ctx: Load, @@ -608,9 +704,11 @@ Module( ), arguments: Arguments { range: 259..262, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 260..261, value: Int( 0, @@ -627,8 +725,10 @@ Module( }, Comprehension { range: 263..280, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 267..268, id: Name("b"), ctx: Store, @@ -636,9 +736,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 272..280, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 272..277, id: Name("range"), ctx: Load, @@ -646,9 +748,11 @@ Module( ), arguments: Arguments { range: 277..280, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 278..279, value: Int( 0, @@ -670,19 +774,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 282..341, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 282..341, elt: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 283..303, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 285..291, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 285..286, id: Name("a"), ctx: Store, @@ -690,6 +799,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 290..291, value: Int( 0, @@ -700,9 +810,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 295..301, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 295..296, id: Name("b"), ctx: Store, @@ -710,6 +822,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 300..301, value: Int( 1, @@ -726,8 +839,10 @@ Module( generators: [ Comprehension { range: 304..322, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 308..309, id: Name("a"), ctx: Store, @@ -735,9 +850,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 313..322, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 313..318, id: Name("range"), ctx: Load, @@ -745,9 +862,11 @@ Module( ), arguments: Arguments { range: 319..322, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 320..321, value: Int( 0, @@ -764,8 +883,10 @@ Module( }, Comprehension { range: 323..340, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..328, id: Name("b"), ctx: Store, @@ -773,9 +894,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 332..340, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 332..337, id: Name("range"), ctx: Load, @@ -783,9 +906,11 @@ Module( ), arguments: Arguments { range: 337..340, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 338..339, value: Int( 0, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap index 64c5b6ec7e637..b93edc683b61d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/return_stmt_invalid_e ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..74, body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 0..8, value: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 7..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..8, id: Name(""), ctx: Invalid, @@ -31,14 +35,17 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 9..23, value: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 16..23, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -52,13 +59,16 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 24..43, value: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 31..43, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..43, id: Name("x"), ctx: Load, @@ -71,10 +81,12 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 44..52, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("x"), ctx: Load, @@ -85,9 +97,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 56..57, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 56..57, value: Int( 1, @@ -98,18 +112,22 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 58..73, value: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 65..73, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 66..73, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("x"), ctx: Load, @@ -117,6 +135,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("y"), ctx: Load, @@ -186,7 +205,7 @@ Module( | 1 | return * - | ^ Syntax Error: can't use starred expression here + | ^ Syntax Error: Starred expression cannot be used here 2 | return yield x 3 | return yield from x | @@ -196,5 +215,5 @@ Module( 3 | return yield from x 4 | return x := 1 5 | return *x and y - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap index b1abaaa51346d..208ce1f1b0370 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/simple_and_compound_stmt_on_same_line.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..17, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..1, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Load, @@ -24,9 +26,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 3..16, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("b"), ctx: Load, @@ -35,14 +39,17 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..16, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap index 2973bbb41cc84..5c6e1ab082a41 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/simple_and_compound_stmt_on_same_line_in_block.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..59, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..13, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -22,6 +24,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -31,9 +34,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 14..28, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 17..22, value: false, }, @@ -41,6 +46,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 24..28, }, ), @@ -50,9 +56,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 29..42, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 32..36, value: true, }, @@ -60,6 +68,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 38..42, }, ), @@ -69,9 +78,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 44..58, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 47..52, value: false, }, @@ -79,6 +90,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 54..58, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap index 9bfaeb67a2f09..3e2eb8febcd1b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/simple_stmts_on_same_line.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..1, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Load, @@ -24,9 +26,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2..3, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("b"), ctx: Load, @@ -36,12 +40,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4..9, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4..9, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("a"), ctx: Load, @@ -50,6 +57,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("b"), ctx: Load, @@ -61,12 +69,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..15, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 10..15, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("c"), ctx: Load, @@ -75,6 +86,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("d"), ctx: Load, @@ -86,26 +98,31 @@ Module( ), Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 16..21, }, ), Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 23..31, }, ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 32..36, }, ), Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 38..46, }, ), Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 47..52, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap index 9f66a28749026..0e424616ad651 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/simple_stmts_on_same_line_in_block.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..46, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..45, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -22,26 +24,31 @@ Module( body: [ Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 9..14, }, ), Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 16..24, }, ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 25..29, }, ), Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 31..39, }, ), Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 40..45, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap index b7d6f53586740..75ce9e03204f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/single_star_for.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..35, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..16, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("_"), ctx: Store, @@ -22,9 +25,11 @@ Module( ), iter: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 9..11, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Load, @@ -36,9 +41,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..16, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 13..16, }, ), @@ -50,13 +57,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 17..34, is_async: false, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 21..23, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Store, @@ -67,6 +77,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..29, id: Name("xs"), ctx: Load, @@ -75,9 +86,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..34, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 31..34, }, ), @@ -95,7 +108,7 @@ Module( | 1 | for _ in *x: ... - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here 2 | for *x in xs: ... | @@ -103,5 +116,5 @@ Module( | 1 | for _ in *x: ... 2 | for *x in xs: ... - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap index 2754123634c6c..8c08d4f66c828 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/single_star_return.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..19, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..18, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 4..5, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,13 +37,16 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 9..18, value: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 16..18, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Load, @@ -60,5 +69,5 @@ Module( | 1 | def f(): return *x - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap index 6a5e4c0150587..273bb6dd53ccd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/err/single_star_yield.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..18, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..17, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 4..5, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,16 +37,20 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..17, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 9..17, value: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 15..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("x"), ctx: Load, @@ -65,5 +75,5 @@ Module( | 1 | def f(): yield *x - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap index 406d8532da4c7..fd275c3a2ed14 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/single_starred_assign ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..9, targets: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 0..2, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("a"), ctx: Store, @@ -29,10 +33,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5..9, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap index 0f06b4ecff29e..2f547ff272313 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/star_index_py310.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..293, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..55, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 44..55, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..47, id: Name("lst"), ctx: Load, @@ -24,13 +28,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 48..54, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 48..54, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..54, id: Name("index"), ctx: Load, @@ -51,22 +58,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 72..112, decorator_list: [], name: Identifier { id: Name("Array"), range: 78..83, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 83..107, + node_index: AtomicNodeIndex(..), args: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 84..106, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..91, id: Name("Generic"), ctx: Load, @@ -74,10 +86,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 92..105, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..97, id: Name("DType"), ctx: Load, @@ -85,9 +99,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 99..105, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..105, id: Name("Shape"), ctx: Load, @@ -111,9 +127,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..112, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 109..112, }, ), @@ -124,12 +142,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..161, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 148..161, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..151, id: Name("lst"), ctx: Load, @@ -137,10 +158,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 152..160, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 152..153, id: Name("a"), ctx: Load, @@ -148,9 +171,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 155..157, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("b"), ctx: Load, @@ -161,6 +186,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 159..160, id: Name("c"), ctx: Load, @@ -178,12 +204,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 185..198, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 185..198, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..188, id: Name("lst"), ctx: Load, @@ -191,10 +220,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 189..197, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("a"), ctx: Load, @@ -202,6 +233,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 192..193, id: Name("b"), ctx: Load, @@ -209,9 +241,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 195..197, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 196..197, id: Name("c"), ctx: Load, @@ -232,12 +266,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 222..233, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 222..233, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..225, id: Name("lst"), ctx: Load, @@ -245,13 +282,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 226..232, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 226..228, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 227..228, id: Name("a"), ctx: Load, @@ -262,9 +302,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 230..232, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 231..232, id: Name("b"), ctx: Load, @@ -285,12 +327,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 254..271, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 254..271, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 254..259, id: Name("array"), ctx: Load, @@ -298,14 +343,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 260..270, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 260..263, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 260..261, value: Int( 3, @@ -316,6 +364,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 262..263, value: Int( 5, @@ -328,9 +377,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 265..270, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..270, id: Name("idxs"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap index de033a8afe37b..4f9cc982f6ee7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/star_slices.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..19, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..18, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 0..18, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("array"), ctx: Load, @@ -24,13 +28,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 6..17, lower: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..12, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..12, id: Name("start"), ctx: Load, @@ -43,9 +50,11 @@ Module( upper: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("end"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap index 0c547eb7b4826..5853cdff0029f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap @@ -7,27 +7,33 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/function_type ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..988, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 796..824, is_async: false, decorator_list: [], name: Identifier { id: Name("keyword"), range: 800..807, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 807..817, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 808..809, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("A"), range: 808..809, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -36,9 +42,11 @@ Module( TypeVar( TypeParamTypeVar { range: 811..816, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("await"), range: 811..816, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -49,6 +57,9 @@ Module( ), parameters: Parameters { range: 817..819, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -59,9 +70,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 821..824, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 821..824, }, ), @@ -72,23 +85,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 826..862, is_async: false, decorator_list: [], name: Identifier { id: Name("not_a_type_param"), range: 830..846, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 846..855, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 847..848, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("A"), range: 847..848, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -97,9 +115,11 @@ Module( TypeVar( TypeParamTypeVar { range: 853..854, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("B"), range: 853..854, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -110,6 +130,9 @@ Module( ), parameters: Parameters { range: 855..857, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -120,9 +143,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 859..862, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 859..862, }, ), @@ -133,23 +158,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 864..896, is_async: false, decorator_list: [], name: Identifier { id: Name("multiple_commas"), range: 868..883, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 883..889, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 884..885, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("A"), range: 884..885, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -158,9 +188,11 @@ Module( TypeVar( TypeParamTypeVar { range: 887..888, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("B"), range: 887..888, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -171,6 +203,9 @@ Module( ), parameters: Parameters { range: 889..891, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -181,9 +216,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 893..896, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 893..896, }, ), @@ -194,23 +231,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 898..938, is_async: false, decorator_list: [], name: Identifier { id: Name("multiple_trailing_commas"), range: 902..926, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 926..931, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 927..928, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("A"), range: 927..928, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -221,6 +263,9 @@ Module( ), parameters: Parameters { range: 931..933, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -231,9 +276,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 935..938, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 935..938, }, ), @@ -244,23 +291,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 940..979, is_async: false, decorator_list: [], name: Identifier { id: Name("multiple_commas_and_recovery"), range: 944..972, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 972..976, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 973..974, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("A"), range: 973..974, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -271,6 +323,9 @@ Module( ), parameters: Parameters { range: 976..976, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -281,9 +336,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 976..979, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 976..979, value: Int( 100, @@ -297,9 +354,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 980..987, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 980..982, elts: [], ctx: Store, @@ -308,6 +367,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 984..987, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap index ea5649412f7d1..5ca66592b10f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/if_extra_clos ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..110, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 90..97, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 93..97, value: true, }, @@ -24,6 +27,7 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 105..109, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap index 76a3ff101f677..0a04441bbbf1b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/if_extra_inde ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..153, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 103..134, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 106..110, value: true, }, @@ -21,17 +24,21 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 116..120, }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 129..134, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 129..134, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("a"), ctx: Load, @@ -40,6 +47,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..134, id: Name("b"), ctx: Load, @@ -55,15 +63,18 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 140..144, }, ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 146..152, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("a"), ctx: Store, @@ -72,6 +83,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 150..152, value: Int( 10, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap index 153ed4c2512d1..26643d505d71a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/invalid_assig ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..788, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 201..206, targets: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 5, @@ -24,6 +27,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 205..206, value: Int( 3, @@ -34,9 +38,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 208..214, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 208..209, value: Int( 5, @@ -46,6 +52,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..214, value: Int( 3, @@ -56,9 +63,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 216..228, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 217..218, value: Int( 5, @@ -67,6 +76,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 221..224, id: Name("int"), ctx: Load, @@ -75,6 +85,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 227..228, value: Int( 3, @@ -87,15 +98,18 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 303..314, targets: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 303..309, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 303..304, id: Name("x"), ctx: Load, @@ -103,6 +117,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 308..309, id: Name("y"), ctx: Load, @@ -114,6 +129,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 312..314, value: Int( 42, @@ -124,13 +140,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 315..328, targets: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 316..322, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 316..317, id: Name("x"), ctx: Store, @@ -138,6 +157,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 321..322, value: Int( 5, @@ -149,6 +169,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 326..328, value: Int( 42, @@ -159,13 +180,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 329..339, targets: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 329..334, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 329..330, id: Name("x"), ctx: Load, @@ -174,6 +198,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 333..334, id: Name("y"), ctx: Load, @@ -184,6 +209,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 337..339, value: Int( 42, @@ -194,14 +220,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 340..347, targets: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 340..342, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 341..342, id: Name("x"), ctx: Store, @@ -212,6 +241,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 345..347, value: Int( 42, @@ -222,23 +252,31 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 348..366, targets: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 349..360, parameters: Some( Parameters { range: 356..357, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 356..357, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 356..357, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("_"), range: 356..357, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -252,6 +290,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 359..360, value: Int( 1, @@ -263,6 +302,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 364..366, value: Int( 42, @@ -273,13 +313,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 367..385, targets: [ If( ExprIf { + node_index: AtomicNodeIndex(..), range: 367..380, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 372..373, id: Name("b"), ctx: Load, @@ -287,6 +330,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 367..368, id: Name("a"), ctx: Load, @@ -294,6 +338,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 379..380, id: Name("c"), ctx: Load, @@ -304,6 +349,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 383..385, value: Int( 42, @@ -314,21 +360,25 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 386..399, targets: [ Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 386..394, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 387..390, value: StringLiteralValue { inner: Single( StringLiteral { range: 387..390, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -343,6 +393,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 392..393, value: Int( 5, @@ -356,6 +407,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 397..399, value: Int( 42, @@ -366,14 +418,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 400..408, targets: [ Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 400..403, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 401..402, id: Name("a"), ctx: Load, @@ -385,6 +440,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 406..408, value: Int( 42, @@ -395,13 +451,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 409..429, targets: [ ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 409..424, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 410..411, id: Name("x"), ctx: Load, @@ -410,8 +469,10 @@ Module( generators: [ Comprehension { range: 412..423, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 416..417, id: Name("x"), ctx: Store, @@ -419,6 +480,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 421..423, id: Name("xs"), ctx: Load, @@ -433,6 +495,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 427..429, value: Int( 42, @@ -443,13 +506,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 430..450, targets: [ SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 430..445, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 431..432, id: Name("x"), ctx: Load, @@ -458,8 +524,10 @@ Module( generators: [ Comprehension { range: 433..444, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 437..438, id: Name("x"), ctx: Store, @@ -467,6 +535,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 442..444, id: Name("xs"), ctx: Load, @@ -481,6 +550,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 448..450, value: Int( 42, @@ -491,13 +561,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 451..478, targets: [ DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 451..473, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 452..453, id: Name("x"), ctx: Load, @@ -505,9 +578,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 455..460, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 455..456, id: Name("x"), ctx: Load, @@ -516,6 +591,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 459..460, value: Int( 2, @@ -527,8 +603,10 @@ Module( generators: [ Comprehension { range: 461..472, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 465..466, id: Name("x"), ctx: Store, @@ -536,6 +614,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 470..472, id: Name("xs"), ctx: Load, @@ -550,6 +629,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 476..478, value: Int( 42, @@ -560,13 +640,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 479..499, targets: [ Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 479..494, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 480..481, id: Name("x"), ctx: Load, @@ -575,8 +658,10 @@ Module( generators: [ Comprehension { range: 482..493, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 486..487, id: Name("x"), ctx: Store, @@ -584,6 +669,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 491..493, id: Name("xs"), ctx: Load, @@ -599,6 +685,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 497..499, value: Int( 42, @@ -609,13 +696,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 500..512, targets: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 500..507, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 506..507, id: Name("x"), ctx: Load, @@ -626,6 +716,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 510..512, value: Int( 42, @@ -636,14 +727,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 513..527, targets: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 514..521, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 520..521, id: Name("x"), ctx: Load, @@ -655,6 +749,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 525..527, value: Int( 42, @@ -665,13 +760,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 528..548, targets: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 529..542, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 540..542, id: Name("xs"), ctx: Load, @@ -682,6 +780,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 546..548, value: Int( 42, @@ -692,13 +791,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 549..563, targets: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 549..558, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 549..550, id: Name("a"), ctx: Load, @@ -711,6 +813,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 553..554, id: Name("b"), ctx: Load, @@ -718,6 +821,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 557..558, id: Name("c"), ctx: Load, @@ -729,6 +833,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 561..563, value: Int( 42, @@ -739,13 +844,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 564..574, targets: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 564..569, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 564..567, id: Name("foo"), ctx: Load, @@ -753,6 +861,7 @@ Module( ), arguments: Arguments { range: 567..569, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -761,6 +870,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 572..574, value: Int( 42, @@ -771,22 +881,27 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 576..590, targets: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 576..585, value: FStringValue { inner: Single( FString( FString { range: 576..585, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 578..584, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 579..583, id: Name("quux"), ctx: Load, @@ -812,6 +927,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 588..590, value: Int( 42, @@ -822,22 +938,27 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 591..614, targets: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 591..609, value: FStringValue { inner: Single( FString( FString { range: 591..609, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 593..598, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 594..597, id: Name("foo"), ctx: Load, @@ -849,16 +970,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 598..603, + node_index: AtomicNodeIndex(..), value: " and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 603..608, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 604..607, id: Name("bar"), ctx: Load, @@ -884,6 +1008,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 612..614, value: Int( 42, @@ -894,15 +1019,18 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 616..626, targets: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 616..621, value: StringLiteralValue { inner: Single( StringLiteral { range: 616..621, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -917,6 +1045,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 624..626, value: Int( 42, @@ -927,15 +1056,18 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 627..638, targets: [ BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 627..633, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 627..633, + node_index: AtomicNodeIndex(..), value: [ 102, 111, @@ -954,6 +1086,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 636..638, value: Int( 42, @@ -964,10 +1097,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 639..647, targets: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 639..642, value: Int( 123, @@ -977,6 +1112,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 645..647, value: Int( 42, @@ -987,10 +1123,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 648..657, targets: [ BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 648..652, value: true, }, @@ -998,6 +1136,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 655..657, value: Int( 42, @@ -1008,16 +1147,19 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 658..667, targets: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 658..662, }, ), ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 665..667, value: Int( 42, @@ -1028,16 +1170,19 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 668..676, targets: [ EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 668..671, }, ), ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 674..676, value: Int( 42, @@ -1048,16 +1193,20 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 677..688, targets: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 677..683, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 678..683, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 678..681, id: Name("foo"), ctx: Load, @@ -1065,6 +1214,7 @@ Module( ), arguments: Arguments { range: 681..683, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1076,6 +1226,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 686..688, value: Int( 42, @@ -1086,14 +1237,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 689..717, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 689..702, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 690..691, id: Name("x"), ctx: Store, @@ -1101,9 +1255,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 693..698, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 693..696, id: Name("foo"), ctx: Load, @@ -1111,6 +1267,7 @@ Module( ), arguments: Arguments { range: 696..698, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1118,6 +1275,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 700..701, id: Name("y"), ctx: Store, @@ -1130,10 +1288,12 @@ Module( ], value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 705..717, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 706..708, value: Int( 42, @@ -1142,6 +1302,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 710..712, value: Int( 42, @@ -1150,6 +1311,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 714..716, value: Int( 42, @@ -1164,18 +1326,22 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 718..758, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 718..737, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 719..725, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 720..721, id: Name("a"), ctx: Store, @@ -1183,6 +1349,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 723..724, id: Name("b"), ctx: Store, @@ -1194,14 +1361,17 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 727..733, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 728..732, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 729..731, value: Int( 42, @@ -1218,6 +1388,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 735..736, id: Name("d"), ctx: Store, @@ -1230,14 +1401,17 @@ Module( ], value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 740..758, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 741..747, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 742..743, value: Int( 1, @@ -1246,6 +1420,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 745..746, value: Int( 2, @@ -1258,14 +1433,17 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 749..754, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 750..753, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 751..752, value: Int( 3, @@ -1282,6 +1460,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 756..757, value: Int( 4, @@ -1296,14 +1475,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 759..787, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 759..772, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 760..761, id: Name("x"), ctx: Store, @@ -1311,9 +1493,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 763..768, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 763..766, id: Name("foo"), ctx: Load, @@ -1321,6 +1505,7 @@ Module( ), arguments: Arguments { range: 766..768, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1328,6 +1513,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 770..771, id: Name("y"), ctx: Store, @@ -1341,10 +1527,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 775..787, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 776..778, value: Int( 42, @@ -1353,6 +1541,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 780..782, value: Int( 42, @@ -1361,6 +1550,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 784..786, value: Int( 42, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap index 4fa30fed7b61a..788a678251bd2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap @@ -7,18 +7,22 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/invalid_augme ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..611, body: [ AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 97..109, target: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 97..103, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("x"), ctx: Load, @@ -26,6 +30,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..103, id: Name("y"), ctx: Load, @@ -37,6 +42,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 107..109, value: Int( 42, @@ -47,12 +53,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 110..124, target: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 111..117, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..112, id: Name("x"), ctx: Store, @@ -60,6 +69,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 116..117, value: Int( 5, @@ -71,6 +81,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..124, value: Int( 42, @@ -81,12 +92,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 125..136, target: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 125..130, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 125..126, id: Name("x"), ctx: Load, @@ -95,6 +109,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("y"), ctx: Load, @@ -105,6 +120,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 134..136, value: Int( 42, @@ -115,13 +131,16 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 137..145, target: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 137..139, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 138..139, id: Name("x"), ctx: Store, @@ -132,6 +151,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 143..145, value: Int( 42, @@ -142,22 +162,30 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 146..165, target: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 147..158, parameters: Some( Parameters { range: 154..155, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 154..155, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 154..155, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("_"), range: 154..155, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -171,6 +199,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 157..158, value: Int( 1, @@ -182,6 +211,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 163..165, value: Int( 42, @@ -192,12 +222,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 166..185, target: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 166..179, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("b"), ctx: Load, @@ -205,6 +238,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("a"), ctx: Load, @@ -212,6 +246,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("c"), ctx: Load, @@ -222,6 +257,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 183..185, value: Int( 42, @@ -232,20 +268,24 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 186..200, target: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 186..194, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 187..190, value: StringLiteralValue { inner: Single( StringLiteral { range: 187..190, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -260,6 +300,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 192..193, value: Int( 5, @@ -273,6 +314,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 198..200, value: Int( 42, @@ -283,13 +325,16 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 201..210, target: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 201..204, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 202..203, id: Name("a"), ctx: Load, @@ -301,6 +346,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 208..210, value: Int( 42, @@ -311,12 +357,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 211..232, target: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 211..226, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 212..213, id: Name("x"), ctx: Load, @@ -325,8 +374,10 @@ Module( generators: [ Comprehension { range: 214..225, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 218..219, id: Name("x"), ctx: Store, @@ -334,6 +385,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 223..225, id: Name("xs"), ctx: Load, @@ -348,6 +400,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 230..232, value: Int( 42, @@ -358,12 +411,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 233..254, target: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 233..248, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 234..235, id: Name("x"), ctx: Load, @@ -372,8 +428,10 @@ Module( generators: [ Comprehension { range: 236..247, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 240..241, id: Name("x"), ctx: Store, @@ -381,6 +439,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..247, id: Name("xs"), ctx: Load, @@ -395,6 +454,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 252..254, value: Int( 42, @@ -405,12 +465,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 255..283, target: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 255..277, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 256..257, id: Name("x"), ctx: Load, @@ -418,9 +481,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 259..264, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..260, id: Name("x"), ctx: Load, @@ -429,6 +494,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 263..264, value: Int( 2, @@ -440,8 +506,10 @@ Module( generators: [ Comprehension { range: 265..276, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 269..270, id: Name("x"), ctx: Store, @@ -449,6 +517,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 274..276, id: Name("xs"), ctx: Load, @@ -463,6 +532,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 281..283, value: Int( 42, @@ -473,12 +543,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 284..305, target: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 284..299, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 285..286, id: Name("x"), ctx: Load, @@ -487,8 +560,10 @@ Module( generators: [ Comprehension { range: 287..298, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 291..292, id: Name("x"), ctx: Store, @@ -496,6 +571,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 296..298, id: Name("xs"), ctx: Load, @@ -511,6 +587,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 303..305, value: Int( 42, @@ -521,12 +598,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 306..319, target: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 306..313, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 312..313, id: Name("x"), ctx: Load, @@ -537,6 +617,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 317..319, value: Int( 42, @@ -547,13 +628,16 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 320..335, target: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 321..328, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..328, id: Name("x"), ctx: Load, @@ -565,6 +649,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 333..335, value: Int( 42, @@ -575,12 +660,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 336..357, target: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 337..350, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 348..350, id: Name("xs"), ctx: Load, @@ -591,6 +679,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 355..357, value: Int( 42, @@ -601,12 +690,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 358..373, target: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 358..367, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 358..359, id: Name("a"), ctx: Load, @@ -619,6 +711,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 362..363, id: Name("b"), ctx: Load, @@ -626,6 +719,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 366..367, id: Name("c"), ctx: Load, @@ -637,6 +731,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 371..373, value: Int( 42, @@ -647,12 +742,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 374..385, target: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 374..379, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 374..377, id: Name("foo"), ctx: Load, @@ -660,6 +758,7 @@ Module( ), arguments: Arguments { range: 377..379, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -668,6 +767,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 383..385, value: Int( 42, @@ -678,21 +778,26 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 387..402, target: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 387..396, value: FStringValue { inner: Single( FString( FString { range: 387..396, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 389..395, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 390..394, id: Name("quux"), ctx: Load, @@ -718,6 +823,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 400..402, value: Int( 42, @@ -728,21 +834,26 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 403..427, target: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 403..421, value: FStringValue { inner: Single( FString( FString { range: 403..421, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 405..410, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 406..409, id: Name("foo"), ctx: Load, @@ -754,16 +865,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 410..415, + node_index: AtomicNodeIndex(..), value: " and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 415..420, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 416..419, id: Name("bar"), ctx: Load, @@ -789,6 +903,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 425..427, value: Int( 42, @@ -799,14 +914,17 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 429..440, target: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 429..434, value: StringLiteralValue { inner: Single( StringLiteral { range: 429..434, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -821,6 +939,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 438..440, value: Int( 42, @@ -831,14 +950,17 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 441..453, target: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 441..447, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 441..447, + node_index: AtomicNodeIndex(..), value: [ 102, 111, @@ -857,6 +979,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 451..453, value: Int( 42, @@ -867,9 +990,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 454..463, target: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 454..457, value: Int( 123, @@ -879,6 +1004,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 461..463, value: Int( 42, @@ -889,9 +1015,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 464..474, target: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 464..468, value: true, }, @@ -899,6 +1027,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 472..474, value: Int( 42, @@ -909,15 +1038,18 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 475..485, target: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 475..479, }, ), op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 483..485, value: Int( 42, @@ -928,15 +1060,18 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 486..495, target: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 486..489, }, ), op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 493..495, value: Int( 42, @@ -947,15 +1082,19 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 496..508, target: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 496..502, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 497..502, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 497..500, id: Name("foo"), ctx: Load, @@ -963,6 +1102,7 @@ Module( ), arguments: Arguments { range: 500..502, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -974,6 +1114,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 506..508, value: Int( 42, @@ -984,13 +1125,16 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 509..538, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 509..522, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 510..511, id: Name("x"), ctx: Store, @@ -998,9 +1142,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 513..518, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 513..516, id: Name("foo"), ctx: Load, @@ -1008,6 +1154,7 @@ Module( ), arguments: Arguments { range: 516..518, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1015,6 +1162,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 520..521, id: Name("y"), ctx: Store, @@ -1027,10 +1175,12 @@ Module( op: Add, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 526..538, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 527..529, value: Int( 42, @@ -1039,6 +1189,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 531..533, value: Int( 42, @@ -1047,6 +1198,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 535..537, value: Int( 42, @@ -1061,17 +1213,21 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 539..580, target: List( ExprList { + node_index: AtomicNodeIndex(..), range: 539..558, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 540..546, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 541..542, id: Name("a"), ctx: Store, @@ -1079,6 +1235,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 544..545, id: Name("b"), ctx: Store, @@ -1090,14 +1247,17 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 548..554, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 549..553, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 550..552, value: Int( 42, @@ -1114,6 +1274,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 556..557, id: Name("d"), ctx: Store, @@ -1126,14 +1287,17 @@ Module( op: Add, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 562..580, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 563..569, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 564..565, value: Int( 1, @@ -1142,6 +1306,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 567..568, value: Int( 2, @@ -1154,14 +1319,17 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 571..576, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 572..575, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 573..574, value: Int( 3, @@ -1178,6 +1346,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 578..579, value: Int( 4, @@ -1192,13 +1361,16 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 581..610, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 581..594, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 582..583, id: Name("x"), ctx: Store, @@ -1206,9 +1378,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 585..590, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 585..588, id: Name("foo"), ctx: Load, @@ -1216,6 +1390,7 @@ Module( ), arguments: Arguments { range: 588..590, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1223,6 +1398,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 592..593, id: Name("y"), ctx: Store, @@ -1236,10 +1412,12 @@ Module( op: Add, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 598..610, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 599..601, value: Int( 42, @@ -1248,6 +1426,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 603..605, value: Int( 42, @@ -1256,6 +1435,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 607..609, value: Int( 42, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap index 45f7c9abf7617..58087f43bd7e8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_0.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/as_pattern_0.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..198, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..197, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -23,11 +25,14 @@ Module( cases: [ MatchCase { range: 127..197, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 132..146, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..139, id: Name(""), ctx: Invalid, @@ -35,15 +40,18 @@ Module( ), arguments: PatternArguments { range: 140..146, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 141..142, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 141..142, + node_index: AtomicNodeIndex(..), }, ), }, @@ -51,11 +59,13 @@ Module( MatchAs( PatternMatchAs { range: 144..145, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("b"), range: 144..145, + node_index: AtomicNodeIndex(..), }, ), }, @@ -69,6 +79,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 193..197, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap index a2e2631cab94f..c33ba5de9a3bd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_1.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/as_pattern_1.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..210, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..209, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -23,14 +25,18 @@ Module( cases: [ MatchCase { range: 140..209, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 145..158, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 145..158, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..152, id: Name(""), ctx: Invalid, @@ -39,6 +45,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 156..158, value: Complex { real: 0.0, @@ -54,6 +61,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 205..209, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap index 1607c5185fb0f..056268c64d25f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/match/as_patt ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..190, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..176, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -22,18 +25,22 @@ Module( cases: [ MatchCase { range: 159..176, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 164..170, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 164..165, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 164..165, + node_index: AtomicNodeIndex(..), }, ), }, @@ -43,6 +50,7 @@ Module( Identifier { id: Name("y"), range: 169..170, + node_index: AtomicNodeIndex(..), }, ), }, @@ -51,13 +59,16 @@ Module( body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 171..176, target: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 171..175, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 173..175, value: Complex { real: 0.0, @@ -69,6 +80,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 176..176, id: Name(""), ctx: Invalid, @@ -85,6 +97,7 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 185..189, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap index cf7490e5fd2fb..6ebf0be44b9fc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/match/as_patt ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..136, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..120, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -22,30 +25,37 @@ Module( cases: [ MatchCase { range: 103..120, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 108..117, + node_index: AtomicNodeIndex(..), cls: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 108..109, items: [], }, ), arguments: PatternArguments { range: 109..117, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 110..116, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 110..111, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 110..111, + node_index: AtomicNodeIndex(..), }, ), }, @@ -55,6 +65,7 @@ Module( Identifier { id: Name("y"), range: 115..116, + node_index: AtomicNodeIndex(..), }, ), }, @@ -68,9 +79,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..120, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -86,6 +99,7 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 131..135, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap index a05914e033363..6d705cce3a44d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_4.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/as_pattern_4.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..187, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..186, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -23,12 +25,15 @@ Module( cases: [ MatchCase { range: 156..186, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 161..172, + node_index: AtomicNodeIndex(..), keys: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 162..163, id: Name("x"), ctx: Store, @@ -36,6 +41,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 167..168, id: Name("y"), ctx: Store, @@ -46,11 +52,13 @@ Module( MatchAs( PatternMatchAs { range: 164..166, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("as"), range: 164..166, + node_index: AtomicNodeIndex(..), }, ), }, @@ -58,8 +66,10 @@ Module( MatchValue( PatternMatchValue { range: 170..171, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 170..171, value: Int( 1, @@ -76,6 +86,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 182..186, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap index 9bbae6b7802b3..1687f42248ed8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_class_pattern.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/invalid_class_pattern.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..383, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 44..285, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 50..57, id: Name("subject"), ctx: Load, @@ -23,11 +25,14 @@ Module( cases: [ MatchCase { range: 63..97, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 68..83, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..71, id: Name("Foo"), ctx: Load, @@ -35,19 +40,24 @@ Module( ), arguments: PatternArguments { range: 71..83, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 72..82, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name(""), range: 80..80, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 81..82, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 1, @@ -65,6 +75,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 93..97, }, ), @@ -72,11 +83,14 @@ Module( }, MatchCase { range: 102..135, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 107..121, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 107..110, id: Name("Foo"), ctx: Load, @@ -84,19 +98,24 @@ Module( ), arguments: PatternArguments { range: 110..121, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 111..120, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name(""), range: 118..118, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 119..120, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -114,6 +133,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 131..135, }, ), @@ -121,11 +141,14 @@ Module( }, MatchCase { range: 140..174, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 145..160, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..148, id: Name("Foo"), ctx: Load, @@ -133,19 +156,24 @@ Module( ), arguments: PatternArguments { range: 148..160, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 149..159, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name(""), range: 157..157, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 158..159, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 158..159, value: Int( 1, @@ -163,6 +191,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 170..174, }, ), @@ -170,11 +199,14 @@ Module( }, MatchCase { range: 179..217, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 184..203, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..187, id: Name("Foo"), ctx: Load, @@ -182,19 +214,24 @@ Module( ), arguments: PatternArguments { range: 187..203, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 188..202, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name(""), range: 200..200, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 201..202, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 1, @@ -212,6 +249,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 213..217, }, ), @@ -219,11 +257,14 @@ Module( }, MatchCase { range: 222..249, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 227..235, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 227..230, id: Name("Foo"), ctx: Load, @@ -231,19 +272,24 @@ Module( ), arguments: PatternArguments { range: 230..235, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 231..234, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name(""), range: 233..233, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 233..234, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 233..234, value: Int( 1, @@ -261,6 +307,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 245..249, }, ), @@ -268,11 +315,14 @@ Module( }, MatchCase { range: 254..285, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 259..271, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..262, id: Name("Foo"), ctx: Load, @@ -280,19 +330,24 @@ Module( ), arguments: PatternArguments { range: 262..271, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 263..270, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name(""), range: 269..269, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 269..270, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 269..270, value: Int( 1, @@ -310,6 +365,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 281..285, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap index 4ff98106389ff..abf680ec652dd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_lhs_or_rhs_pattern.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/invalid_lhs_or_rhs_pattern.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..695, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..332, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..25, id: Name("invalid_lhs_pattern"), ctx: Load, @@ -23,17 +25,22 @@ Module( cases: [ MatchCase { range: 31..60, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 36..46, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 36..46, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 36..41, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..39, id: Name("Foo"), ctx: Load, @@ -41,6 +48,7 @@ Module( ), arguments: Arguments { range: 39..41, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -49,6 +57,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..46, value: Complex { real: 0.0, @@ -64,6 +73,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 56..60, }, ), @@ -71,14 +81,18 @@ Module( }, MatchCase { range: 65..90, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 70..76, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 70..76, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("x"), ctx: Store, @@ -87,6 +101,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 74..76, value: Complex { real: 0.0, @@ -102,6 +117,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 86..90, }, ), @@ -109,14 +125,18 @@ Module( }, MatchCase { range: 95..120, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 100..106, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 100..106, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("_"), ctx: Store, @@ -125,6 +145,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..106, value: Complex { real: 0.0, @@ -140,6 +161,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 116..120, }, ), @@ -147,17 +169,22 @@ Module( }, MatchCase { range: 125..156, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 130..142, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 130..142, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 131..136, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 131..132, value: Int( 1, @@ -167,6 +194,7 @@ Module( op: BitOr, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 135..136, value: Int( 2, @@ -178,6 +206,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 140..142, value: Complex { real: 0.0, @@ -193,6 +222,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 152..156, }, ), @@ -200,18 +230,23 @@ Module( }, MatchCase { range: 161..191, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 166..177, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 166..177, left: List( ExprList { + node_index: AtomicNodeIndex(..), range: 166..172, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 167..168, value: Int( 1, @@ -220,6 +255,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 170..171, value: Int( 2, @@ -233,6 +269,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 175..177, value: Complex { real: 0.0, @@ -248,6 +285,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 187..191, }, ), @@ -255,20 +293,25 @@ Module( }, MatchCase { range: 196..229, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 201..215, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 201..215, left: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 201..210, items: [ DictItem { key: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 202..206, value: true, }, @@ -276,6 +319,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 208..209, value: Int( 1, @@ -289,6 +333,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..215, value: Complex { real: 0.0, @@ -304,6 +349,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 225..229, }, ), @@ -311,14 +357,18 @@ Module( }, MatchCase { range: 234..260, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 239..246, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 239..246, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 239..241, value: Complex { real: 0.0, @@ -329,6 +379,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 244..246, value: Complex { real: 0.0, @@ -344,6 +395,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 256..260, }, ), @@ -351,18 +403,23 @@ Module( }, MatchCase { range: 265..292, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 270..278, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 270..278, left: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 270..273, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 271..273, value: Complex { real: 0.0, @@ -375,6 +432,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 276..278, value: Complex { real: 0.0, @@ -390,6 +448,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 288..292, }, ), @@ -397,17 +456,22 @@ Module( }, MatchCase { range: 297..332, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 302..318, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 302..318, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 302..313, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 302..305, id: Name("Foo"), ctx: Load, @@ -415,9 +479,11 @@ Module( ), arguments: Arguments { range: 305..313, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 306..312, id: Name(""), ctx: Invalid, @@ -431,6 +497,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 316..318, value: Complex { real: 0.0, @@ -446,6 +513,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 328..332, }, ), @@ -456,9 +524,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 334..625, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 340..359, id: Name("invalid_rhs_pattern"), ctx: Load, @@ -467,14 +537,18 @@ Module( cases: [ MatchCase { range: 365..393, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 370..379, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 370..379, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 370..371, value: Int( 1, @@ -484,9 +558,11 @@ Module( op: Add, right: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 374..379, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 374..377, id: Name("Foo"), ctx: Load, @@ -494,6 +570,7 @@ Module( ), arguments: Arguments { range: 377..379, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -507,6 +584,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 389..393, }, ), @@ -514,14 +592,18 @@ Module( }, MatchCase { range: 398..422, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 403..408, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 403..408, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 403..404, value: Int( 2, @@ -531,6 +613,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 407..408, id: Name("x"), ctx: Store, @@ -544,6 +627,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 418..422, }, ), @@ -551,14 +635,18 @@ Module( }, MatchCase { range: 427..451, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 432..437, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 432..437, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 432..433, value: Int( 3, @@ -568,6 +656,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 436..437, id: Name("_"), ctx: Store, @@ -581,6 +670,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 447..451, }, ), @@ -588,14 +678,18 @@ Module( }, MatchCase { range: 456..486, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 461..472, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 461..472, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 461..462, value: Int( 4, @@ -605,9 +699,11 @@ Module( op: Add, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 466..471, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 466..467, value: Int( 1, @@ -617,6 +713,7 @@ Module( op: BitOr, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 470..471, value: Int( 2, @@ -633,6 +730,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 482..486, }, ), @@ -640,14 +738,18 @@ Module( }, MatchCase { range: 491..520, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 496..506, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 496..506, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 496..497, value: Int( 5, @@ -657,10 +759,12 @@ Module( op: Add, right: List( ExprList { + node_index: AtomicNodeIndex(..), range: 500..506, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 501..502, value: Int( 1, @@ -669,6 +773,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 504..505, value: Int( 2, @@ -687,6 +792,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 516..520, }, ), @@ -694,14 +800,18 @@ Module( }, MatchCase { range: 525..557, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 530..543, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 530..543, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 530..531, value: Int( 6, @@ -711,12 +821,14 @@ Module( op: Add, right: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 534..543, items: [ DictItem { key: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 535..539, value: true, }, @@ -724,6 +836,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 541..542, value: Int( 1, @@ -742,6 +855,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 553..557, }, ), @@ -749,14 +863,18 @@ Module( }, MatchCase { range: 562..586, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 567..572, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 567..572, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 567..568, value: Int( 1, @@ -766,6 +884,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 571..572, value: Int( 2, @@ -780,6 +899,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 582..586, }, ), @@ -787,14 +907,18 @@ Module( }, MatchCase { range: 591..625, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 596..611, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 596..611, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 596..597, value: Int( 1, @@ -804,9 +928,11 @@ Module( op: Add, right: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 600..611, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 600..603, id: Name("Foo"), ctx: Load, @@ -814,9 +940,11 @@ Module( ), arguments: Arguments { range: 603..611, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 604..610, id: Name(""), ctx: Invalid, @@ -835,6 +963,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 621..625, }, ), @@ -845,9 +974,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 627..694, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 633..656, id: Name("invalid_lhs_rhs_pattern"), ctx: Load, @@ -856,17 +987,22 @@ Module( cases: [ MatchCase { range: 662..694, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 667..680, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 667..680, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 667..672, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 667..670, id: Name("Foo"), ctx: Load, @@ -874,6 +1010,7 @@ Module( ), arguments: Arguments { range: 670..672, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -882,9 +1019,11 @@ Module( op: Add, right: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 675..680, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 675..678, id: Name("Bar"), ctx: Load, @@ -892,6 +1031,7 @@ Module( ), arguments: Arguments { range: 678..680, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -905,6 +1045,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 690..694, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap index dddbd65f4a589..69e3b740ee67d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/invalid_mapping_pattern.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..509, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 61..209, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..74, id: Name("subject"), ctx: Load, @@ -23,15 +25,19 @@ Module( cases: [ MatchCase { range: 80..105, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 85..91, + node_index: AtomicNodeIndex(..), keys: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 86..90, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..90, id: Name("key"), ctx: Store, @@ -45,8 +51,10 @@ Module( MatchValue( PatternMatchValue { range: 90..90, + node_index: AtomicNodeIndex(..), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..90, id: Name(""), ctx: Invalid, @@ -62,6 +70,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 101..105, }, ), @@ -69,15 +78,19 @@ Module( }, MatchCase { range: 110..138, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 115..124, + node_index: AtomicNodeIndex(..), keys: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 116..120, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 117..120, id: Name("key"), ctx: Store, @@ -91,8 +104,10 @@ Module( MatchValue( PatternMatchValue { range: 122..123, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 1, @@ -109,6 +124,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 134..138, }, ), @@ -116,15 +132,19 @@ Module( }, MatchCase { range: 143..170, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 148..156, + node_index: AtomicNodeIndex(..), keys: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 149..153, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..153, id: Name("key"), ctx: Store, @@ -138,8 +158,10 @@ Module( MatchValue( PatternMatchValue { range: 154..155, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 154..155, value: Int( 1, @@ -156,6 +178,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 166..170, }, ), @@ -163,15 +186,19 @@ Module( }, MatchCase { range: 175..209, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 180..195, + node_index: AtomicNodeIndex(..), keys: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 181..185, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 182..185, id: Name("key"), ctx: Store, @@ -182,6 +209,7 @@ Module( ), NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 187..191, }, ), @@ -190,8 +218,10 @@ Module( MatchValue( PatternMatchValue { range: 185..185, + node_index: AtomicNodeIndex(..), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..185, id: Name(""), ctx: Invalid, @@ -202,8 +232,10 @@ Module( MatchValue( PatternMatchValue { range: 193..194, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 193..194, value: Int( 1, @@ -220,6 +252,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 205..209, }, ), @@ -230,9 +263,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 305..462, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 311..318, id: Name("subject"), ctx: Load, @@ -241,12 +276,15 @@ Module( cases: [ MatchCase { range: 324..360, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 329..346, + node_index: AtomicNodeIndex(..), keys: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 338..342, }, ), @@ -255,8 +293,10 @@ Module( MatchValue( PatternMatchValue { range: 344..345, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 344..345, value: Int( 1, @@ -270,6 +310,7 @@ Module( Identifier { id: Name("rest"), range: 332..336, + node_index: AtomicNodeIndex(..), }, ), }, @@ -278,6 +319,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 356..360, }, ), @@ -285,12 +327,15 @@ Module( }, MatchCase { range: 365..411, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 370..397, + node_index: AtomicNodeIndex(..), keys: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 389..393, }, ), @@ -299,8 +344,10 @@ Module( MatchValue( PatternMatchValue { range: 395..396, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 395..396, value: Int( 1, @@ -314,6 +361,7 @@ Module( Identifier { id: Name("rest2"), range: 382..387, + node_index: AtomicNodeIndex(..), }, ), }, @@ -322,6 +370,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 407..411, }, ), @@ -329,12 +378,15 @@ Module( }, MatchCase { range: 416..462, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 421..448, + node_index: AtomicNodeIndex(..), keys: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 431..435, }, ), @@ -343,8 +395,10 @@ Module( MatchValue( PatternMatchValue { range: 437..438, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 437..438, value: Int( 1, @@ -358,6 +412,7 @@ Module( Identifier { id: Name("rest2"), range: 442..447, + node_index: AtomicNodeIndex(..), }, ), }, @@ -366,6 +421,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 458..462, }, ), @@ -376,9 +432,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 464..509, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 470..477, id: Name("subject"), ctx: Load, @@ -387,15 +445,19 @@ Module( cases: [ MatchCase { range: 483..509, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 488..504, + node_index: AtomicNodeIndex(..), keys: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 489..500, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 489..492, id: Name("Foo"), ctx: Load, @@ -403,9 +465,11 @@ Module( ), arguments: Arguments { range: 492..500, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 493..499, id: Name(""), ctx: Invalid, @@ -421,8 +485,10 @@ Module( MatchValue( PatternMatchValue { range: 502..503, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 502..503, value: Int( 1, @@ -439,9 +505,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 506..509, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 506..509, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap index fa73b40cf644b..ca41862659884 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__star_pattern_usage.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/star_pattern_usage.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..408, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 57..407, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..70, id: Name("subject"), ctx: Load, @@ -23,9 +25,11 @@ Module( cases: [ MatchCase { range: 76..97, + node_index: AtomicNodeIndex(..), pattern: MatchStar( PatternMatchStar { range: 81..83, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -33,6 +37,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 93..97, }, ), @@ -40,13 +45,16 @@ Module( }, MatchCase { range: 102..128, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 107..114, + node_index: AtomicNodeIndex(..), pattern: Some( MatchStar( PatternMatchStar { range: 107..109, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -55,6 +63,7 @@ Module( Identifier { id: Name("x"), range: 113..114, + node_index: AtomicNodeIndex(..), }, ), }, @@ -63,6 +72,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 124..128, }, ), @@ -70,13 +80,16 @@ Module( }, MatchCase { range: 133..156, + node_index: AtomicNodeIndex(..), pattern: MatchStar( PatternMatchStar { range: 138..142, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("foo"), range: 139..142, + node_index: AtomicNodeIndex(..), }, ), }, @@ -85,6 +98,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 152..156, }, ), @@ -92,17 +106,21 @@ Module( }, MatchCase { range: 161..188, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 166..174, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 166..170, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("foo"), range: 167..170, + node_index: AtomicNodeIndex(..), }, ), }, @@ -110,8 +128,10 @@ Module( MatchValue( PatternMatchValue { range: 173..174, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 173..174, value: Int( 1, @@ -127,6 +147,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 184..188, }, ), @@ -134,15 +155,19 @@ Module( }, MatchCase { range: 193..220, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 198..206, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 198..199, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 198..199, value: Int( 1, @@ -154,10 +179,12 @@ Module( MatchStar( PatternMatchStar { range: 202..206, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("foo"), range: 203..206, + node_index: AtomicNodeIndex(..), }, ), }, @@ -169,6 +196,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 216..220, }, ), @@ -176,11 +204,14 @@ Module( }, MatchCase { range: 225..251, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 230..237, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 230..233, id: Name("Foo"), ctx: Load, @@ -188,10 +219,12 @@ Module( ), arguments: PatternArguments { range: 233..237, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 234..236, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -204,6 +237,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 247..251, }, ), @@ -211,11 +245,14 @@ Module( }, MatchCase { range: 256..284, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 261..270, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 261..264, id: Name("Foo"), ctx: Load, @@ -223,17 +260,21 @@ Module( ), arguments: PatternArguments { range: 264..270, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 265..269, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 265..266, + node_index: AtomicNodeIndex(..), }, pattern: MatchStar( PatternMatchStar { range: 267..269, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -246,6 +287,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 280..284, }, ), @@ -253,15 +295,19 @@ Module( }, MatchCase { range: 289..312, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 294..298, + node_index: AtomicNodeIndex(..), keys: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 295..297, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 296..297, id: Name("_"), ctx: Store, @@ -275,8 +321,10 @@ Module( MatchValue( PatternMatchValue { range: 297..297, + node_index: AtomicNodeIndex(..), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 297..297, id: Name(""), ctx: Invalid, @@ -292,6 +340,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 308..312, }, ), @@ -299,15 +348,19 @@ Module( }, MatchCase { range: 317..343, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 322..329, + node_index: AtomicNodeIndex(..), keys: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 323..325, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 324..325, id: Name("_"), ctx: Store, @@ -321,8 +374,10 @@ Module( MatchValue( PatternMatchValue { range: 327..328, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 327..328, value: Int( 1, @@ -339,6 +394,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 339..343, }, ), @@ -346,12 +402,15 @@ Module( }, MatchCase { range: 348..377, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 353..363, + node_index: AtomicNodeIndex(..), keys: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 354..358, }, ), @@ -360,6 +419,7 @@ Module( MatchStar( PatternMatchStar { range: 360..362, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -371,6 +431,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 373..377, }, ), @@ -378,14 +439,18 @@ Module( }, MatchCase { range: 382..407, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 387..393, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 387..393, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 387..388, value: Int( 1, @@ -395,9 +460,11 @@ Module( op: Add, right: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 391..393, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 392..393, id: Name("_"), ctx: Store, @@ -414,6 +481,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 403..407, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap index f4dfce5b70480..b0a9f945ab898 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__unary_add_usage.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/statements/match/unary_add_usage.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..269, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 74..268, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..87, id: Name("subject"), ctx: Load, @@ -23,15 +25,19 @@ Module( cases: [ MatchCase { range: 93..114, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 98..100, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 98..100, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 99..100, value: Int( 1, @@ -46,6 +52,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 110..114, }, ), @@ -53,15 +60,19 @@ Module( }, MatchCase { range: 119..149, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 124..135, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 124..125, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 124..125, value: Int( 1, @@ -73,12 +84,15 @@ Module( MatchValue( PatternMatchValue { range: 128..130, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 128..130, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 129..130, value: Int( 2, @@ -92,12 +106,15 @@ Module( MatchValue( PatternMatchValue { range: 133..135, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 133..135, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 134..135, value: Int( 3, @@ -115,6 +132,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 145..149, }, ), @@ -122,15 +140,19 @@ Module( }, MatchCase { range: 154..184, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 159..170, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 160..161, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..161, value: Int( 1, @@ -142,12 +164,15 @@ Module( MatchValue( PatternMatchValue { range: 163..165, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 163..165, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 164..165, value: Int( 2, @@ -161,12 +186,15 @@ Module( MatchValue( PatternMatchValue { range: 167..169, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 167..169, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 168..169, value: Int( 3, @@ -184,6 +212,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 180..184, }, ), @@ -191,11 +220,14 @@ Module( }, MatchCase { range: 189..223, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 194..209, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 194..197, id: Name("Foo"), ctx: Load, @@ -203,23 +235,29 @@ Module( ), arguments: PatternArguments { range: 197..209, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 198..202, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 198..199, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 200..202, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 200..202, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 1, @@ -233,19 +271,24 @@ Module( }, PatternKeyword { range: 204..208, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 204..205, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 206..208, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 206..208, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 207..208, value: Int( 2, @@ -265,6 +308,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 219..223, }, ), @@ -272,18 +316,22 @@ Module( }, MatchCase { range: 228..268, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 233..254, + node_index: AtomicNodeIndex(..), keys: [ BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 234..238, value: true, }, ), BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 244..249, value: false, }, @@ -293,12 +341,15 @@ Module( MatchValue( PatternMatchValue { range: 240..242, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 240..242, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 241..242, value: Int( 1, @@ -312,12 +363,15 @@ Module( MatchValue( PatternMatchValue { range: 251..253, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 251..253, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 252..253, value: Int( 2, @@ -336,6 +390,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 264..268, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap index e05f49c03b445..3c07014614489 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap @@ -7,21 +7,26 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/with/ambiguou ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..950, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 163..188, is_async: false, items: [ WithItem { range: 168..182, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 168..182, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 169..174, id: Name("item1"), ctx: Load, @@ -29,6 +34,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 176..181, id: Name("item2"), ctx: Load, @@ -45,9 +51,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 185..188, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 185..188, }, ), @@ -58,17 +66,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 189..219, is_async: false, items: [ WithItem { range: 194..208, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 194..208, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 195..200, id: Name("item1"), ctx: Load, @@ -76,6 +88,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 202..207, id: Name("item2"), ctx: Load, @@ -90,8 +103,10 @@ Module( }, WithItem { range: 213..214, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 213..214, id: Name("f"), ctx: Load, @@ -103,9 +118,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 216..219, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 216..219, }, ), @@ -116,17 +133,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 220..252, is_async: false, items: [ WithItem { range: 225..239, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 225..239, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 226..231, id: Name("item1"), ctx: Load, @@ -134,6 +155,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 233..238, id: Name("item2"), ctx: Load, @@ -148,8 +170,10 @@ Module( }, WithItem { range: 241..246, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..246, id: Name("item3"), ctx: Load, @@ -161,9 +185,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 249..252, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 249..252, }, ), @@ -174,16 +200,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 253..270, is_async: false, items: [ WithItem { range: 258..265, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 259..264, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 260..264, id: Name("item"), ctx: Load, @@ -198,9 +228,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 267..270, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 267..270, }, ), @@ -211,16 +243,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 271..293, is_async: false, items: [ WithItem { range: 276..288, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 277..282, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 278..282, id: Name("item"), ctx: Load, @@ -232,6 +268,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 287..288, id: Name("f"), ctx: Store, @@ -243,9 +280,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 290..293, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 290..293, }, ), @@ -256,16 +295,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 294..321, is_async: false, items: [ WithItem { range: 300..315, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 300..310, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 300..304, id: Name("item"), ctx: Store, @@ -273,6 +316,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 308..310, value: Int( 10, @@ -284,6 +328,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 314..315, id: Name("f"), ctx: Store, @@ -295,9 +340,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 318..321, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 318..321, }, ), @@ -308,13 +355,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 322..357, is_async: false, items: [ WithItem { range: 328..333, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 328..333, id: Name("item1"), ctx: Load, @@ -324,11 +374,14 @@ Module( }, WithItem { range: 335..351, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 335..346, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 335..340, id: Name("item2"), ctx: Store, @@ -336,6 +389,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 344..346, value: Int( 10, @@ -347,6 +401,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 350..351, id: Name("f"), ctx: Store, @@ -358,9 +413,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 354..357, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 354..357, }, ), @@ -371,16 +428,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 358..396, is_async: false, items: [ WithItem { range: 363..384, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 363..384, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 364..365, id: Name("x"), ctx: Load, @@ -389,8 +450,10 @@ Module( generators: [ Comprehension { range: 366..384, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 370..371, id: Name("x"), ctx: Store, @@ -398,9 +461,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 375..384, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 375..380, id: Name("range"), ctx: Load, @@ -408,9 +473,11 @@ Module( ), arguments: Arguments { range: 380..384, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 381..383, value: Int( 10, @@ -433,8 +500,10 @@ Module( }, WithItem { range: 386..390, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 386..390, id: Name("item"), ctx: Load, @@ -446,9 +515,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 393..396, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 393..396, }, ), @@ -459,17 +530,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 397..410, is_async: false, items: [ WithItem { range: 402..410, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 402..410, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 403..407, id: Name("item"), ctx: Load, @@ -477,6 +552,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 409..410, id: Name("x"), ctx: Load, @@ -495,10 +571,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 411..429, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 415..416, id: Name("x"), ctx: Store, @@ -506,9 +584,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 420..429, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 420..425, id: Name("range"), ctx: Load, @@ -516,9 +596,11 @@ Module( ), arguments: Arguments { range: 425..429, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 426..428, value: Int( 10, @@ -536,9 +618,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 432..435, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 432..435, }, ), @@ -546,16 +630,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 496..515, is_async: false, items: [ WithItem { range: 502..509, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 503..508, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 504..508, id: Name("item"), ctx: Load, @@ -570,9 +658,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 512..515, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 512..515, }, ), @@ -583,19 +673,24 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 517..551, is_async: false, items: [ WithItem { range: 522..539, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 522..539, elt: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 523..525, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 524..525, id: Name("x"), ctx: Load, @@ -607,8 +702,10 @@ Module( generators: [ Comprehension { range: 526..539, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 530..531, id: Name("x"), ctx: Store, @@ -616,6 +713,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 535..539, id: Name("iter"), ctx: Load, @@ -632,8 +730,10 @@ Module( }, WithItem { range: 541..545, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 541..545, id: Name("item"), ctx: Load, @@ -645,9 +745,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 548..551, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 548..551, }, ), @@ -658,17 +760,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 552..567, is_async: false, items: [ WithItem { range: 557..567, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 557..567, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 558..563, id: Name("item1"), ctx: Load, @@ -676,9 +782,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 565..567, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 566..567, id: Name("x"), ctx: Load, @@ -700,10 +808,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 568..588, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 572..573, id: Name("x"), ctx: Store, @@ -711,10 +821,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 577..588, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 577..581, id: Name("iter"), ctx: Load, @@ -722,6 +834,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 583..588, id: Name("item2"), ctx: Load, @@ -738,9 +851,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 591..594, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 591..594, }, ), @@ -748,13 +863,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 595..617, is_async: false, items: [ WithItem { range: 601..607, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 601..602, id: Name("x"), ctx: Load, @@ -763,6 +881,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 606..607, id: Name("f"), ctx: Store, @@ -772,11 +891,14 @@ Module( }, WithItem { range: 609..611, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 609..611, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 610..611, id: Name("y"), ctx: Load, @@ -791,9 +913,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 614..617, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 614..617, }, ), @@ -804,16 +928,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 618..640, is_async: false, items: [ WithItem { range: 624..626, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 624..626, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 625..626, id: Name("x"), ctx: Load, @@ -826,8 +954,10 @@ Module( }, WithItem { range: 628..634, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 628..629, id: Name("y"), ctx: Load, @@ -836,6 +966,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 633..634, id: Name("f"), ctx: Store, @@ -847,9 +978,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 637..640, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 637..640, }, ), @@ -860,17 +993,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 641..663, is_async: false, items: [ WithItem { range: 646..658, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 646..658, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 647..648, id: Name("x"), ctx: Load, @@ -878,10 +1015,12 @@ Module( ), Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 650..657, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 656..657, id: Name("y"), ctx: Load, @@ -901,9 +1040,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 660..663, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 660..663, }, ), @@ -914,17 +1055,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 664..689, is_async: false, items: [ WithItem { range: 669..684, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 669..684, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 670..671, id: Name("x"), ctx: Load, @@ -932,14 +1077,17 @@ Module( ), Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 673..683, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 679..683, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 679..680, id: Name("y"), ctx: Load, @@ -947,6 +1095,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 682..683, id: Name("z"), ctx: Load, @@ -971,9 +1120,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 686..689, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 686..689, }, ), @@ -984,17 +1135,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 690..717, is_async: false, items: [ WithItem { range: 695..712, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 695..712, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 696..697, id: Name("x"), ctx: Load, @@ -1002,9 +1157,11 @@ Module( ), YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 699..711, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 710..711, id: Name("y"), ctx: Load, @@ -1023,9 +1180,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 714..717, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 714..717, }, ), @@ -1036,13 +1195,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 718..734, is_async: false, items: [ WithItem { range: 724..730, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 724..725, id: Name("x"), ctx: Load, @@ -1051,6 +1213,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 729..730, id: Name("f"), ctx: Store, @@ -1060,8 +1223,10 @@ Module( }, WithItem { range: 732..733, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 732..733, id: Name("y"), ctx: Load, @@ -1075,9 +1240,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 738..744, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 738..739, id: Name("f"), ctx: Store, @@ -1085,6 +1252,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 741..744, }, ), @@ -1094,16 +1262,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 745..777, is_async: false, items: [ WithItem { range: 750..771, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 750..766, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 751..752, id: Name("x"), ctx: Load, @@ -1112,8 +1284,10 @@ Module( generators: [ Comprehension { range: 753..766, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 757..758, id: Name("x"), ctx: Store, @@ -1121,6 +1295,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 762..766, id: Name("iter"), ctx: Load, @@ -1136,6 +1311,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 770..771, id: Name("y"), ctx: Store, @@ -1147,9 +1323,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 774..777, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 774..777, }, ), @@ -1160,13 +1338,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 837..854, is_async: false, items: [ WithItem { range: 843..853, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 844..848, id: Name("item"), ctx: Load, @@ -1175,6 +1356,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 852..853, id: Name("f"), ctx: Store, @@ -1188,9 +1370,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 857..860, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 857..860, }, ), @@ -1198,13 +1382,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 862..878, is_async: false, items: [ WithItem { range: 868..877, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 868..872, id: Name("item"), ctx: Load, @@ -1213,6 +1400,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 876..877, id: Name("f"), ctx: Store, @@ -1226,9 +1414,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 880..886, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 880..881, id: Name("x"), ctx: Store, @@ -1236,6 +1426,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 883..886, }, ), @@ -1245,13 +1436,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 887..904, is_async: false, items: [ WithItem { range: 893..903, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 893..897, id: Name("item"), ctx: Load, @@ -1260,6 +1454,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 901..903, id: Name("f1"), ctx: Store, @@ -1273,9 +1468,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 908..915, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 908..910, id: Name("f2"), ctx: Store, @@ -1283,6 +1480,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 912..915, }, ), @@ -1292,13 +1490,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 916..950, is_async: false, items: [ WithItem { range: 922..932, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 922..927, id: Name("item1"), ctx: Load, @@ -1307,6 +1508,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 931..932, id: Name("f"), ctx: Store, @@ -1316,11 +1518,14 @@ Module( }, WithItem { range: 934..944, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 934..944, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 934..939, id: Name("item2"), ctx: Store, @@ -1328,6 +1533,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 943..944, value: Int( 0, @@ -1342,9 +1548,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 947..950, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 947..950, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__empty_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__empty_with_items.py.snap index 9d3a53afb0bca..1798ad6c23613 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__empty_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__empty_with_items.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/with/empty_wi ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..105, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 88..98, is_async: false, items: [], body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 95..98, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 95..98, }, ), @@ -30,12 +34,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 100..105, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 100..105, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("x"), ctx: Load, @@ -44,6 +51,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap index e389ad74809d9..15bb2f058b3bc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/with/unclosed ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..14, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 0..14, is_async: false, items: [ WithItem { range: 5..6, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..6, id: Name(""), ctx: Invalid, @@ -29,12 +33,15 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..14, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 9..14, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("x"), ctx: Load, @@ -43,6 +50,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap index 057cb44426006..ce5678ded9b49 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/with/unclosed ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..6, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 0..6, is_async: false, items: [ WithItem { range: 5..6, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..6, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap index 7182e64ab16f0..c93979143a337 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/with/unparent ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..249, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 86..102, is_async: false, items: [ WithItem { range: 91..95, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..95, id: Name("item"), ctx: Load, @@ -29,6 +33,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 98..102, }, ), @@ -37,13 +42,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 103..124, is_async: false, items: [ WithItem { range: 108..117, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..112, id: Name("item"), ctx: Load, @@ -52,6 +60,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..117, id: Name("x"), ctx: Store, @@ -63,6 +72,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 120..124, }, ), @@ -71,16 +81,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 125..141, is_async: false, items: [ WithItem { range: 130..135, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 130..135, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..135, id: Name("item"), ctx: Load, @@ -95,6 +109,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 137..141, }, ), @@ -103,16 +118,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 142..163, is_async: false, items: [ WithItem { range: 147..157, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 147..152, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..152, id: Name("item"), ctx: Load, @@ -124,6 +143,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("x"), ctx: Store, @@ -135,6 +155,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 159..163, }, ), @@ -143,16 +164,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 164..193, is_async: false, items: [ WithItem { range: 169..175, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 169..175, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..175, id: Name("item1"), ctx: Load, @@ -165,8 +190,10 @@ Module( }, WithItem { range: 177..187, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 177..182, id: Name("item2"), ctx: Load, @@ -175,6 +202,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 186..187, id: Name("f"), ctx: Store, @@ -186,6 +214,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 189..193, }, ), @@ -194,13 +223,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 194..223, is_async: false, items: [ WithItem { range: 199..209, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..204, id: Name("item1"), ctx: Load, @@ -209,6 +241,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 208..209, id: Name("f"), ctx: Store, @@ -218,11 +251,14 @@ Module( }, WithItem { range: 211..217, + node_index: AtomicNodeIndex(..), context_expr: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 211..217, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 212..217, id: Name("item2"), ctx: Load, @@ -237,6 +273,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 219..223, }, ), @@ -245,13 +282,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 224..249, is_async: false, items: [ WithItem { range: 229..233, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 229..233, id: Name("item"), ctx: Load, @@ -261,8 +301,10 @@ Module( }, WithItem { range: 237..243, + node_index: AtomicNodeIndex(..), context_expr: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 237..238, value: Int( 0, @@ -272,6 +314,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 242..243, id: Name("f"), ctx: Store, @@ -283,6 +326,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 245..249, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap new file mode 100644 index 0000000000000..6b3811f0e18a9 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap @@ -0,0 +1,124 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..58, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..49, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..49, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..49, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..48, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 50..57, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 50..57, + value: TStringValue { + inner: Single( + TString( + TString { + range: 50..57, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 52..56, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 53..53, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{}" + | ^ Syntax Error: Expected an expression +3 | t"{ }" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{}" +3 | t"{ }" + | ^ Syntax Error: Expected an expression + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap new file mode 100644 index 0000000000000..ebf78178588aa --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap @@ -0,0 +1,69 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..53, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..52, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..52, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..52, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..51, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!z}" + | ^ Syntax Error: t-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap new file mode 100644 index 0000000000000..f4485237350b5 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap @@ -0,0 +1,124 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..66, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..54, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..54, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..54, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..53, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 55..65, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 55..65, + value: TStringValue { + inner: Single( + TString( + TString { + range: 55..65, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 57..64, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 58..59, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!123}" + | ^^^ Syntax Error: t-string: invalid conversion character +3 | t"{x!'a'}" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!123}" +3 | t"{x!'a'}" + | ^^^ Syntax Error: t-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap new file mode 100644 index 0000000000000..b2a595bf80eb7 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap @@ -0,0 +1,227 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..156, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 121..127, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 121..127, + value: TStringValue { + inner: Single( + TString( + TString { + range: 121..127, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 123..126, + node_index: AtomicNodeIndex(..), + expression: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 124..125, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 125..125, + id: Name(""), + ctx: Invalid, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 128..141, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 128..141, + value: TStringValue { + inner: Single( + TString( + TString { + range: 128..141, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 130..140, + node_index: AtomicNodeIndex(..), + expression: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 131..139, + value: BoolOp( + ExprBoolOp { + node_index: AtomicNodeIndex(..), + range: 132..139, + op: And, + values: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 132..133, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 138..139, + id: Name("y"), + ctx: Load, + }, + ), + ], + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 142..155, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 142..155, + value: TStringValue { + inner: Single( + TString( + TString { + range: 142..155, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 144..154, + node_index: AtomicNodeIndex(..), + expression: Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 145..153, + value: Yield( + ExprYield { + node_index: AtomicNodeIndex(..), + range: 146..153, + value: Some( + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 152..153, + id: Name("x"), + ctx: Load, + }, + ), + ), + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | # Starred expression inside t-string has a minimum precedence of bitwise or. +3 | t"{*}" + | ^ Syntax Error: Expected an expression +4 | t"{*x and y}" +5 | t"{*yield x}" + | + + + | +2 | # Starred expression inside t-string has a minimum precedence of bitwise or. +3 | t"{*}" +4 | t"{*x and y}" + | ^^^^^^^ Syntax Error: Boolean expression cannot be used here +5 | t"{*yield x}" + | + + + | +3 | t"{*}" +4 | t"{*x and y}" +5 | t"{*yield x}" + | ^^^^^^^ Syntax Error: Yield expression cannot be used here + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap new file mode 100644 index 0000000000000..121d002f28390 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..61, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..60, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..60, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..60, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..56, + node_index: AtomicNodeIndex(..), + expression: Lambda( + ExprLambda { + node_index: AtomicNodeIndex(..), + range: 47..56, + parameters: Some( + Parameters { + range: 54..55, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 54..55, + node_index: AtomicNodeIndex(..), + parameter: Parameter { + range: 54..55, + node_index: AtomicNodeIndex(..), + name: Identifier { + id: Name("x"), + range: 54..55, + node_index: AtomicNodeIndex(..), + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 56..56, + id: Name(""), + ctx: Invalid, + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 56..58, + node_index: AtomicNodeIndex(..), + value: " x", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^ Syntax Error: Expected an expression + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^^^^^^^^ Syntax Error: t-string: lambda expressions are not allowed without parentheses + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^ Syntax Error: t-string: expecting '}' + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap new file mode 100644 index 0000000000000..5c1f43a2b766a --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap @@ -0,0 +1,383 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..82, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..48, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..48, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..48, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..47, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 49..58, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 49..58, + value: TStringValue { + inner: Single( + TString( + TString { + range: 49..58, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 51..58, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 52..55, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 59..67, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 59..67, + value: TStringValue { + inner: Single( + TString( + TString { + range: 59..67, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 61..66, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 62..65, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 68..81, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 68..81, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 68..72, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 70..71, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 71..71, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 73..81, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 77..78, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 78..78, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: missing closing quote in string literal +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: t-string: unterminated string +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: t-string: unterminated string +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^^ Syntax Error: missing closing quote in string literal +4 | t"{foo=" +5 | t"{" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: t-string: unterminated string +4 | t"{foo=" +5 | t"{" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: t-string: unterminated string +4 | t"{foo=" +5 | t"{" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^^ Syntax Error: t-string: expecting '}' +5 | t"{" +6 | t"""{""" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: Expected TStringEnd, found Unknown +4 | t"{foo=" +5 | t"{" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: missing closing quote in string literal +5 | t"{" +6 | t"""{""" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: t-string: unterminated string +5 | t"{" +6 | t"""{""" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: t-string: unterminated string +5 | t"{" +6 | t"""{""" + | + + + | +3 | t"{foo!r" +4 | t"{foo=" +5 | t"{" + | ^ Syntax Error: missing closing quote in string literal +6 | t"""{""" + | + + + | +4 | t"{foo=" +5 | t"{" +6 | t"""{""" + | ^^^^ Syntax Error: Expected TStringEnd, found TStringStart + | + + + | +4 | t"{foo=" +5 | t"{" +6 | t"""{""" + | ^^^ Syntax Error: Expected an expression + | + + + | +5 | t"{" +6 | t"""{""" + | ^ Syntax Error: unexpected EOF while parsing + | + + + | +5 | t"{" +6 | t"""{""" + | ^ Syntax Error: t-string: unterminated string + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap new file mode 100644 index 0000000000000..117976d898dfc --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap @@ -0,0 +1,158 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..73, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..56, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..56, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..56, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 46..52, + node_index: AtomicNodeIndex(..), + value: "hello ", + }, + ), + Interpolation( + InterpolatedElement { + range: 52..55, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 53..54, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 55..55, + node_index: AtomicNodeIndex(..), + elements: [], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 57..72, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 57..72, + value: TStringValue { + inner: Single( + TString( + TString { + range: 57..72, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 59..65, + node_index: AtomicNodeIndex(..), + value: "hello ", + }, + ), + Interpolation( + InterpolatedElement { + range: 65..71, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 66..67, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 68..71, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 68..71, + node_index: AtomicNodeIndex(..), + value: ".3f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"hello {x:" + | ^ Syntax Error: t-string: expecting '}' +3 | t"hello {x:.3f" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"hello {x:" +3 | t"hello {x:.3f" + | ^ Syntax Error: t-string: expecting '}' + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap new file mode 100644 index 0000000000000..87586ee1bcd4a --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap @@ -0,0 +1,231 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/template_strings_py313.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..117, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..52, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..52, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..52, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..51, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 47..50, + id: Name("hey"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 53..63, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 53..63, + value: TStringValue { + inner: Single( + TString( + TString { + range: 53..63, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 55..62, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 56..61, + id: Name("there"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 64..88, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 64..88, + value: TStringValue { + inner: Single( + TString( + TString { + range: 64..88, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 68..85, + node_index: AtomicNodeIndex(..), + value: "what's\nhappening?", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 89..116, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 89..116, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 89..101, + node_index: AtomicNodeIndex(..), + value: "implicitly", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 101..116, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 103..115, + node_index: AtomicNodeIndex(..), + value: "concatenated", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +1 | # parse_options: {"target-version": "3.13"} +2 | t"{hey}" + | ^^^^^^^^ Syntax Error: Cannot use t-strings on Python 3.13 (syntax was added in Python 3.14) +3 | t'{there}' +4 | t"""what's + | + + + | +1 | # parse_options: {"target-version": "3.13"} +2 | t"{hey}" +3 | t'{there}' + | ^^^^^^^^^^ Syntax Error: Cannot use t-strings on Python 3.13 (syntax was added in Python 3.14) +4 | t"""what's +5 | happening?""" + | + + + | +2 | t"{hey}" +3 | t'{there}' +4 | / t"""what's +5 | | happening?""" + | |_____________^ Syntax Error: Cannot use t-strings on Python 3.13 (syntax was added in Python 3.14) +6 | "implicitly"t"concatenated" + | + + + | +4 | t"""what's +5 | happening?""" +6 | "implicitly"t"concatenated" + | ^^^^^^^^^^^^^^^ Syntax Error: Cannot use t-strings on Python 3.13 (syntax was added in Python 3.14) + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap index f3525218f438c..81e4bacde6996 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/try_stmt_invalid_orde ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..47, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..31, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -24,6 +27,7 @@ Module( finalbody: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 27..31, }, ), @@ -33,6 +37,7 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 42..46, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap index df9779babddc4..b86a2ad5bf5bf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/try_stmt_missing_exce ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..43, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..13, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -27,10 +30,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 14..42, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 23..27, }, ), @@ -39,6 +44,7 @@ Module( orelse: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 38..42, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap index 93dc9f223d15a..2bb1a1e8282ea 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/try_stmt_misspelled_e ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..165, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..13, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -27,9 +30,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 14..20, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..19, id: Name("exept"), ctx: Store, @@ -37,6 +42,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..20, id: Name(""), ctx: Invalid, @@ -48,20 +54,24 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 54..58, }, ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 72..76, }, ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 77..82, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("a"), ctx: Store, @@ -70,6 +80,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 1, @@ -80,10 +91,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 83..113, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 92..96, }, ), @@ -92,11 +105,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 97..113, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 109..113, }, ), @@ -111,9 +126,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 114..120, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..119, id: Name("exept"), ctx: Store, @@ -121,6 +138,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..120, id: Name(""), ctx: Invalid, @@ -132,15 +150,18 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 154..158, }, ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 159..164, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 159..160, id: Name("b"), ctx: Store, @@ -149,6 +170,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 163..164, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap index 0117d56216854..689c42622ecbe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/try_stmt_mixed_except ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..242, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..63, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -23,11 +26,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 14..30, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 26..30, }, ), @@ -37,9 +42,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 31..63, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..53, id: Name("ExceptionGroup"), ctx: Load, @@ -50,6 +57,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 59..63, }, ), @@ -64,10 +72,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 64..127, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 73..77, }, ), @@ -76,9 +86,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 78..110, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..100, id: Name("ExceptionGroup"), ctx: Load, @@ -89,6 +101,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 106..110, }, ), @@ -98,11 +111,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 111..127, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 123..127, }, ), @@ -117,10 +132,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 128..241, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 137..141, }, ), @@ -129,11 +146,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 142..158, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 154..158, }, ), @@ -143,11 +162,13 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 159..175, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 171..175, }, ), @@ -157,9 +178,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 176..208, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..198, id: Name("ExceptionGroup"), ctx: Load, @@ -170,6 +193,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 204..208, }, ), @@ -179,9 +203,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 209..241, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 217..231, id: Name("ExceptionGroup"), ctx: Load, @@ -192,6 +218,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 237..241, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap index 4cb4a0a1bc59d..b0c18e8a43e7d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/err/tuple_context_manager ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..327, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 216..236, is_async: false, items: [ WithItem { range: 222..225, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..225, id: Name("foo"), ctx: Load, @@ -27,8 +31,10 @@ Module( }, WithItem { range: 227..230, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 227..230, id: Name("bar"), ctx: Load, @@ -40,9 +46,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 233..236, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 233..236, }, ), @@ -53,16 +61,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 237..274, is_async: false, items: [ WithItem { range: 242..269, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 246..261, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 246..250, id: Name("open"), ctx: Load, @@ -70,14 +82,17 @@ Module( ), arguments: Arguments { range: 250..261, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 251..260, value: StringLiteralValue { inner: Single( StringLiteral { range: 251..260, + node_index: AtomicNodeIndex(..), value: "foo.txt", flags: StringLiteralFlags { quote_style: Single, @@ -97,6 +112,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..269, id: Name("foo"), ctx: Store, @@ -108,9 +124,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 271..274, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 271..274, }, ), @@ -121,13 +139,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 275..309, is_async: false, items: [ WithItem { range: 284..287, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 284..287, id: Name("foo"), ctx: Load, @@ -137,8 +158,10 @@ Module( }, WithItem { range: 291..294, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 291..294, id: Name("bar"), ctx: Load, @@ -148,8 +171,10 @@ Module( }, WithItem { range: 298..301, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 298..301, id: Name("baz"), ctx: Load, @@ -161,9 +186,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 306..309, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 306..309, }, ), @@ -174,13 +201,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 310..326, is_async: false, items: [ WithItem { range: 316..319, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 316..319, id: Name("foo"), ctx: Load, @@ -192,9 +222,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 323..326, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 323..326, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap index 8878c1663a8b9..96e430a647c3e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_alias_incomplete_stmt.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..21, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..4, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("type"), ctx: Load, @@ -24,9 +26,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..9, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..9, id: Name("type"), ctx: Load, @@ -36,9 +40,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..11, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Load, @@ -48,9 +54,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 12..20, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Store, @@ -59,6 +67,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..20, id: Name(""), ctx: Invalid, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap index 3b1d5c40abd1c..a7f11501f60d9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_alias_invalid_va ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..67, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..11, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("x"), ctx: Store, @@ -22,9 +25,11 @@ Module( type_params: None, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 9..11, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("y"), ctx: Load, @@ -37,9 +42,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 12..28, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Store, @@ -48,10 +55,12 @@ Module( type_params: None, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 21..28, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("y"), ctx: Load, @@ -64,9 +73,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 29..50, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("x"), ctx: Store, @@ -75,9 +86,11 @@ Module( type_params: None, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 38..50, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("y"), ctx: Load, @@ -89,9 +102,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 51..61, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Store, @@ -100,6 +115,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("x"), ctx: Load, @@ -109,9 +125,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 65..66, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap index 68d3ff052490d..c9de513d8a75e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_param_default_py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..149, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 44..65, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("X"), ctx: Store, @@ -22,18 +25,22 @@ Module( type_params: Some( TypeParams { range: 50..59, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 51..58, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 51..52, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..58, id: Name("int"), ctx: Load, @@ -47,6 +54,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..65, id: Name("int"), ctx: Load, @@ -56,28 +64,34 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 66..87, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 70..71, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 71..80, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 72..79, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 72..73, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..79, id: Name("int"), ctx: Load, @@ -91,6 +105,9 @@ Module( ), parameters: Parameters { range: 80..82, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -101,9 +118,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 84..87, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 84..87, }, ), @@ -114,27 +133,33 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 88..111, decorator_list: [], name: Identifier { id: Name("C"), range: 94..95, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 95..104, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 96..103, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 96..97, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..103, id: Name("int"), ctx: Load, @@ -149,6 +174,7 @@ Module( arguments: Some( Arguments { range: 104..106, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -156,9 +182,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 108..111, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 108..111, }, ), @@ -169,22 +197,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 112..148, decorator_list: [], name: Identifier { id: Name("D"), range: 118..119, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 119..141, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 120..121, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("S"), range: 120..121, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -193,14 +226,17 @@ Module( TypeVar( TypeParamTypeVar { range: 123..130, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 123..124, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..130, id: Name("int"), ctx: Load, @@ -212,14 +248,17 @@ Module( TypeVar( TypeParamTypeVar { range: 132..140, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 132..133, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..140, id: Name("uint"), ctx: Load, @@ -234,6 +273,7 @@ Module( arguments: Some( Arguments { range: 141..143, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -241,9 +281,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 145..148, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 145..148, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap index 6de003c4146fa..ce4ecda18c831 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_param_invalid_bo ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..103, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..21, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -22,20 +25,25 @@ Module( type_params: Some( TypeParams { range: 6..15, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 7..14, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 7..8, + node_index: AtomicNodeIndex(..), }, bound: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 10..14, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..14, id: Name("int"), ctx: Load, @@ -53,6 +61,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..21, id: Name("int"), ctx: Load, @@ -62,9 +71,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 22..46, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("X"), ctx: Store, @@ -73,21 +84,26 @@ Module( type_params: Some( TypeParams { range: 28..40, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 29..39, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 29..30, + node_index: AtomicNodeIndex(..), }, bound: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 32..39, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("x"), ctx: Load, @@ -105,6 +121,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..46, id: Name("int"), ctx: Load, @@ -114,9 +131,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 47..76, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..53, id: Name("X"), ctx: Store, @@ -125,20 +144,25 @@ Module( type_params: Some( TypeParams { range: 53..70, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 54..69, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 54..55, + node_index: AtomicNodeIndex(..), }, bound: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 57..69, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -155,6 +179,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..76, id: Name("int"), ctx: Load, @@ -164,9 +189,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 77..102, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("X"), ctx: Store, @@ -175,17 +202,21 @@ Module( type_params: Some( TypeParams { range: 83..96, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 84..88, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 84..85, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..88, id: Name("x"), ctx: Load, @@ -198,9 +229,11 @@ Module( TypeVar( TypeParamTypeVar { range: 92..95, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("int"), range: 92..95, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -211,6 +244,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..102, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap index 0e9672fd1c34c..84551e10b69c1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_param_missing_bound.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..41, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..17, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..11, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 7..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 7..8, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -40,6 +45,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -49,9 +55,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 18..40, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("X"), ctx: Store, @@ -60,13 +68,16 @@ Module( type_params: Some( TypeParams { range: 24..34, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 25..28, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T1"), range: 25..27, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -75,9 +86,11 @@ Module( TypeVar( TypeParamTypeVar { range: 31..33, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T2"), range: 31..33, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -88,6 +101,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..40, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap index 79c0ae76890c3..7b4e3d833c6a2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_param_param_spec_bound.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..23, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..10, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..10, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 7..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 9..10, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..10, id: Name(""), ctx: Invalid, @@ -48,9 +54,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..15, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..15, id: Name("int"), ctx: Load, @@ -60,9 +68,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..22, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap index 757545ca6efca..6cde8b6197543 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_param_param_spec ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..140, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..24, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -22,20 +25,25 @@ Module( type_params: Some( TypeParams { range: 6..18, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 7..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 9..10, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -52,6 +60,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..24, id: Name("int"), ctx: Load, @@ -61,9 +70,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 25..52, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 30..31, id: Name("X"), ctx: Store, @@ -72,21 +83,26 @@ Module( type_params: Some( TypeParams { range: 31..46, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 32..45, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 34..35, + node_index: AtomicNodeIndex(..), }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 38..45, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Load, @@ -103,6 +119,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..52, id: Name("int"), ctx: Load, @@ -112,9 +129,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 53..85, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("X"), ctx: Store, @@ -123,20 +142,25 @@ Module( type_params: Some( TypeParams { range: 59..79, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 60..78, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 62..63, + node_index: AtomicNodeIndex(..), }, default: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 66..78, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("x"), ctx: Load, @@ -152,6 +176,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..85, id: Name("int"), ctx: Load, @@ -161,9 +186,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 86..114, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("X"), ctx: Store, @@ -172,17 +199,21 @@ Module( type_params: Some( TypeParams { range: 92..108, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 93..100, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 95..96, + node_index: AtomicNodeIndex(..), }, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("x"), ctx: Load, @@ -194,9 +225,11 @@ Module( TypeVar( TypeParamTypeVar { range: 104..107, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("int"), range: 104..107, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -207,6 +240,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..114, id: Name("int"), ctx: Load, @@ -216,9 +250,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 115..139, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("X"), ctx: Store, @@ -227,20 +263,25 @@ Module( type_params: Some( TypeParams { range: 121..133, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 122..132, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 124..125, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 128..132, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..132, id: Name("int"), ctx: Load, @@ -257,6 +298,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..139, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap index 8ed2eb4c05040..ce7099a5e3aec 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_param_param_spec_missing_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..44, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..19, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..13, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 7..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 9..10, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..19, id: Name("int"), ctx: Load, @@ -48,9 +54,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 20..43, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("X"), ctx: Store, @@ -59,13 +67,16 @@ Module( type_params: Some( TypeParams { range: 26..37, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 27..32, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 29..30, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -73,9 +84,11 @@ Module( TypeVar( TypeParamTypeVar { range: 34..36, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T2"), range: 34..36, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -86,6 +99,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..43, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap index 9d93c799c78e4..17ce8c72fdb21 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_param_type_var_i ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..163, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..22, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -22,21 +25,26 @@ Module( type_params: Some( TypeParams { range: 6..16, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 7..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 7..8, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 11..15, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..15, id: Name("int"), ctx: Load, @@ -53,6 +61,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..22, id: Name("int"), ctx: Load, @@ -62,9 +71,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 23..48, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("X"), ctx: Store, @@ -73,22 +84,27 @@ Module( type_params: Some( TypeParams { range: 29..42, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 30..41, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 30..31, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 34..41, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("x"), ctx: Load, @@ -105,6 +121,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..48, id: Name("int"), ctx: Load, @@ -114,9 +131,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 49..76, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("X"), ctx: Store, @@ -125,22 +144,27 @@ Module( type_params: Some( TypeParams { range: 55..70, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 56..69, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 56..57, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 61..68, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("x"), ctx: Load, @@ -157,6 +181,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..76, id: Name("int"), ctx: Load, @@ -166,9 +191,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 77..107, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("X"), ctx: Store, @@ -177,21 +204,26 @@ Module( type_params: Some( TypeParams { range: 83..101, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 84..100, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 84..85, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 88..100, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("x"), ctx: Load, @@ -207,6 +239,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..107, id: Name("int"), ctx: Load, @@ -216,9 +249,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 108..134, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("X"), ctx: Store, @@ -227,18 +262,22 @@ Module( type_params: Some( TypeParams { range: 114..128, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 115..120, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 115..116, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..120, id: Name("x"), ctx: Load, @@ -250,9 +289,11 @@ Module( TypeVar( TypeParamTypeVar { range: 124..127, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("int"), range: 124..127, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -263,6 +304,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..134, id: Name("int"), ctx: Load, @@ -272,9 +314,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 135..162, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("X"), ctx: Store, @@ -283,17 +327,21 @@ Module( type_params: Some( TypeParams { range: 141..156, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 142..155, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 142..143, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..148, id: Name("int"), ctx: Load, @@ -303,9 +351,11 @@ Module( default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 151..155, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 152..155, id: Name("int"), ctx: Load, @@ -322,6 +372,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 159..162, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap index 1547b5dd007f3..5ad35632e6892 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_param_type_var_missing_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..64, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..17, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..11, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 7..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 7..8, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -40,6 +45,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -49,9 +55,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 18..40, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("X"), ctx: Store, @@ -60,17 +68,21 @@ Module( type_params: Some( TypeParams { range: 24..34, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 25..33, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 25..26, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..31, id: Name("int"), ctx: Load, @@ -85,6 +97,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..40, id: Name("int"), ctx: Load, @@ -94,9 +107,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 41..63, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("X"), ctx: Store, @@ -105,13 +120,16 @@ Module( type_params: Some( TypeParams { range: 47..57, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 48..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T1"), range: 48..50, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -120,9 +138,11 @@ Module( TypeVar( TypeParamTypeVar { range: 54..56, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T2"), range: 54..56, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -133,6 +153,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..63, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap index 3e6e16fa6f5de..3ffc891d81d14 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_param_type_var_tuple_bound.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..22, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..9, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..9, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 7..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 8..9, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..9, id: Name(""), ctx: Invalid, @@ -48,9 +54,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..14, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..14, id: Name("int"), ctx: Load, @@ -60,9 +68,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..21, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..21, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap index 61860a15efe40..c6ab71d84ff4a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_param_type_var_t ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..147, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..24, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -22,20 +25,25 @@ Module( type_params: Some( TypeParams { range: 6..18, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 7..17, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 8..10, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -52,6 +60,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..24, id: Name("int"), ctx: Load, @@ -61,9 +70,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 25..56, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 30..31, id: Name("X"), ctx: Store, @@ -72,25 +83,31 @@ Module( type_params: Some( TypeParams { range: 31..50, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 32..49, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 33..35, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 38..49, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 39..49, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..42, id: Name("int"), ctx: Load, @@ -98,6 +115,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..49, id: Name("str"), ctx: Load, @@ -117,6 +135,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..56, id: Name("int"), ctx: Load, @@ -126,9 +145,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 57..84, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("X"), ctx: Store, @@ -137,21 +158,26 @@ Module( type_params: Some( TypeParams { range: 63..78, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 64..77, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 65..67, + node_index: AtomicNodeIndex(..), }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 70..77, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..77, id: Name("x"), ctx: Load, @@ -168,6 +194,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..84, id: Name("int"), ctx: Load, @@ -177,9 +204,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 85..117, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("X"), ctx: Store, @@ -188,20 +217,25 @@ Module( type_params: Some( TypeParams { range: 91..111, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 92..110, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 93..95, + node_index: AtomicNodeIndex(..), }, default: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 98..110, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..110, id: Name("x"), ctx: Load, @@ -217,6 +251,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..117, id: Name("int"), ctx: Load, @@ -226,9 +261,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 118..146, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 123..124, id: Name("X"), ctx: Store, @@ -237,17 +274,21 @@ Module( type_params: Some( TypeParams { range: 124..140, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 125..132, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 126..128, + node_index: AtomicNodeIndex(..), }, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..132, id: Name("x"), ctx: Load, @@ -259,9 +300,11 @@ Module( TypeVar( TypeParamTypeVar { range: 136..139, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("int"), range: 136..139, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -272,6 +315,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..146, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap index 17303b668739a..b7e862a1056cc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_param_type_var_tuple_missing_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..44, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..19, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..13, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 7..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 8..10, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..19, id: Name("int"), ctx: Load, @@ -48,9 +54,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 20..43, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("X"), ctx: Store, @@ -59,13 +67,16 @@ Module( type_params: Some( TypeParams { range: 26..37, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 27..32, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 28..30, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -73,9 +84,11 @@ Module( TypeVar( TypeParamTypeVar { range: 34..36, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T2"), range: 34..36, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -86,6 +99,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..43, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap index fff0c28aa0033..fd3e6b168fe01 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap @@ -1,32 +1,38 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/type_params_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..52, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..21, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 7..9, + node_index: AtomicNodeIndex(..), type_params: [], }, ), parameters: Parameters { range: 9..11, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -37,6 +43,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 17..21, }, ), @@ -45,9 +52,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 22..51, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..36, id: Name("ListOrSet"), ctx: Store, @@ -56,14 +65,17 @@ Module( type_params: Some( TypeParams { range: 36..38, + node_index: AtomicNodeIndex(..), type_params: [], }, ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 41..51, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..45, id: Name("list"), ctx: Load, @@ -72,6 +84,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..51, id: Name("set"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap index dfb7a15121bce..df2f28b34dd9e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/err/type_stmt_py311.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 44..56, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("x"), ctx: Store, @@ -22,6 +25,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..56, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap index 55d81b949d7cd..cbeb13482ff1c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/unparenthesized_named ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..52, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 43..52, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..46, id: Name("lst"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), slice: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 47..51, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -34,6 +40,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 50..51, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap index a608f55121787..abbb6e06fa69e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/inline/err/unparenthesized_named ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..73, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..72, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 43..72, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 44..53, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..48, id: Name("last"), ctx: Store, @@ -27,6 +32,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..53, id: Name("x"), ctx: Load, @@ -37,8 +43,10 @@ Module( generators: [ Comprehension { range: 54..71, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("x"), ctx: Store, @@ -46,9 +54,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 63..71, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..68, id: Name("range"), ctx: Load, @@ -56,9 +66,11 @@ Module( ), arguments: Arguments { range: 68..71, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 69..70, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap index d03f0ebe48f06..16c0b89b5378c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/inline/err/unparenthesized_named ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..88, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..57, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 43..57, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 44..50, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Store, @@ -28,6 +33,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 49..50, value: Int( 1, @@ -38,6 +44,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 2, @@ -46,6 +53,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 55..56, value: Int( 3, @@ -59,13 +67,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..72, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 58..72, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 59..60, value: Int( 1, @@ -74,9 +85,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 62..68, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Store, @@ -84,6 +97,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 67..68, value: Int( 2, @@ -94,6 +108,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 70..71, value: Int( 3, @@ -107,13 +122,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 73..87, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 73..87, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 74..75, value: Int( 1, @@ -122,6 +140,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 77..78, value: Int( 2, @@ -130,9 +149,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 80..86, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..81, id: Name("x"), ctx: Store, @@ -140,6 +161,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 85..86, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap index c2c29483fd578..f02e2c45bd3fe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap @@ -7,19 +7,23 @@ input_file: crates/ruff_python_parser/resources/inline/err/unterminated_fstring_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..67, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..7, value: FStringValue { inner: Single( FString( FString { range: 0..7, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Double, @@ -36,12 +40,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..13, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 8..13, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 1, @@ -51,6 +58,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 12..13, value: Int( 1, @@ -63,27 +71,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..24, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 14..24, value: FStringValue { inner: Single( FString( FString { range: 14..24, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 16..22, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 22..24, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("x"), ctx: Load, @@ -110,12 +124,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..30, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 25..30, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 2, @@ -125,6 +142,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 29..30, value: Int( 2, @@ -137,27 +155,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..42, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 31..42, value: FStringValue { inner: Single( FString( FString { range: 31..42, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 33..39, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 39..42, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("x"), ctx: Load, @@ -166,8 +190,9 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 42..42, + node_index: AtomicNodeIndex(..), elements: [], }, ), @@ -189,12 +214,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..48, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 43..48, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 43..44, value: Int( 3, @@ -204,6 +232,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 47..48, value: Int( 3, @@ -216,27 +245,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..60, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 49..60, value: FStringValue { inner: Single( FString( FString { range: 49..60, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 51..57, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 57..60, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("x"), ctx: Load, @@ -263,12 +298,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 61..66, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 61..66, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 4, @@ -278,6 +316,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 4, @@ -345,7 +384,7 @@ Module( 3 | f"hello {x 4 | 2 + 2 5 | f"hello {x: - | ^ Syntax Error: f-string: unterminated string + | ^ Syntax Error: f-string: newlines are not allowed in format specifiers when using single quotes 6 | 3 + 3 7 | f"hello {x} | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap index 402445a7f6d10..9949fa6cbc5f1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/err/walrus_py37.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..54, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 45..53, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 46..52, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("x"), ctx: Store, @@ -24,6 +28,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap index 0e8d49ac7517b..4f87608d79b56 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/while_stmt_invalid_test_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..70, body: [ While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 0..13, test: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 6..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, @@ -29,9 +32,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..13, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 10..13, }, ), @@ -43,13 +48,16 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 14..32, test: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 20..27, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("x"), ctx: Load, @@ -61,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), @@ -75,9 +85,11 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 33..40, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("a"), ctx: Load, @@ -89,9 +101,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 42..48, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..43, id: Name("b"), ctx: Store, @@ -99,6 +113,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 45..48, }, ), @@ -108,12 +123,15 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 49..61, test: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 55..61, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..56, id: Name("a"), ctx: Store, @@ -121,6 +139,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 60..61, value: Int( 1, @@ -135,9 +154,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 63..69, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..64, id: Name("b"), ctx: Store, @@ -145,6 +166,7 @@ Module( ), annotation: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 66..69, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap index 39dfbec62b919..7006daf690aa9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_colon.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/while_stmt_missing_colon.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..40, body: [ While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 0..39, test: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 12..18, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..13, id: Name("a"), ctx: Load, @@ -29,6 +32,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..18, value: Int( 30, @@ -41,6 +45,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 35..39, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_test.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_test.py.snap index 59871538d36fc..e86db412de29b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_test.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_missing_test.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/while_stmt_missing_test.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..30, body: [ While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 0..11, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..5, id: Name(""), ctx: Invalid, @@ -23,9 +25,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..11, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 8..11, }, ), @@ -37,9 +41,11 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 12..29, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..17, id: Name(""), ctx: Invalid, @@ -48,10 +54,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 24..29, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("a"), ctx: Store, @@ -60,6 +68,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..29, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap index c73b00b2b3b0e..8a24d7009d379 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_colon.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/with_items_parenthesized_missing_colon.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 28..56, is_async: false, items: [ WithItem { range: 34..39, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..39, id: Name("item1"), ctx: Load, @@ -28,8 +31,10 @@ Module( }, WithItem { range: 41..46, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..46, id: Name("item2"), ctx: Load, @@ -41,6 +46,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 52..56, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap index 817874cdbed27..7a3cf04fb1747 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/with_items_parenthesized_missing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..160, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 0..23, is_async: false, items: [ WithItem { range: 6..11, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..11, id: Name("item1"), ctx: Load, @@ -28,8 +31,10 @@ Module( }, WithItem { range: 12..17, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..17, id: Name("item2"), ctx: Load, @@ -41,9 +46,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..23, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, }, ), @@ -54,13 +61,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 24..53, is_async: false, items: [ WithItem { range: 30..41, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 30..35, id: Name("item1"), ctx: Load, @@ -69,6 +79,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..41, id: Name("f1"), ctx: Store, @@ -78,8 +89,10 @@ Module( }, WithItem { range: 42..47, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..47, id: Name("item2"), ctx: Load, @@ -91,9 +104,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 50..53, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 50..53, }, ), @@ -104,13 +119,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 54..91, is_async: false, items: [ WithItem { range: 60..65, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..65, id: Name("item1"), ctx: Load, @@ -120,8 +138,10 @@ Module( }, WithItem { range: 67..72, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..72, id: Name("item2"), ctx: Load, @@ -131,8 +151,10 @@ Module( }, WithItem { range: 73..78, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..78, id: Name("item3"), ctx: Load, @@ -142,8 +164,10 @@ Module( }, WithItem { range: 80..85, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..85, id: Name("item4"), ctx: Load, @@ -155,9 +179,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 88..91, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 88..91, }, ), @@ -168,13 +194,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 92..135, is_async: false, items: [ WithItem { range: 98..103, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..103, id: Name("item1"), ctx: Load, @@ -184,8 +213,10 @@ Module( }, WithItem { range: 105..116, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..110, id: Name("item2"), ctx: Load, @@ -194,6 +225,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..116, id: Name("f1"), ctx: Store, @@ -203,8 +235,10 @@ Module( }, WithItem { range: 117..122, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 117..122, id: Name("item3"), ctx: Load, @@ -214,8 +248,10 @@ Module( }, WithItem { range: 124..129, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 124..129, id: Name("item4"), ctx: Load, @@ -227,9 +263,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 132..135, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 132..135, }, ), @@ -240,17 +278,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 136..159, is_async: false, items: [ WithItem { range: 141..154, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 141..154, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..147, id: Name("item1"), ctx: Load, @@ -258,6 +300,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 149..154, id: Name("item2"), ctx: Load, @@ -274,9 +317,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 156..159, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 156..159, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap index 617675786f5c5..cf762a1792c96 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/err/write_to_debug_expr.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..83, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 0..13, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..13, id: Name("__debug__"), ctx: Del, @@ -25,10 +28,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 14..36, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("x"), ctx: Del, @@ -36,6 +41,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("y"), ctx: Del, @@ -43,6 +49,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..33, id: Name("__debug__"), ctx: Del, @@ -50,6 +57,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("z"), ctx: Del, @@ -60,10 +68,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 37..50, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..46, id: Name("__debug__"), ctx: Store, @@ -72,6 +82,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 49..50, value: Int( 1, @@ -82,14 +93,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 51..82, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 51..69, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("x"), ctx: Store, @@ -97,6 +111,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("y"), ctx: Store, @@ -104,6 +119,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..66, id: Name("__debug__"), ctx: Store, @@ -111,6 +127,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("z"), ctx: Store, @@ -124,10 +141,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 72..82, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 72..73, value: Int( 1, @@ -136,6 +155,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 2, @@ -144,6 +164,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 3, @@ -152,6 +173,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 4, @@ -173,7 +195,7 @@ Module( | 1 | del __debug__ - | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.13 (syntax was removed in 3.9) + | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.14 (syntax was removed in 3.9) 2 | del x, y, __debug__, z 3 | __debug__ = 1 | @@ -182,7 +204,7 @@ Module( | 1 | del __debug__ 2 | del x, y, __debug__, z - | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.13 (syntax was removed in 3.9) + | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.14 (syntax was removed in 3.9) 3 | __debug__ = 1 4 | x, y, __debug__, z = 1, 2, 3, 4 | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@all_async_comprehension_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@all_async_comprehension_py310.py.snap index 85cb132698689..411a58148d8f5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@all_async_comprehension_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@all_async_comprehension_py310.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/all_async_comprehensio ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..126, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..125, is_async: true, decorator_list: [], name: Identifier { id: Name("test"), range: 54..58, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 58..60, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,16 +37,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 62..125, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 69..125, elt: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 70..100, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..72, id: Name("x"), ctx: Load, @@ -49,8 +59,10 @@ Module( generators: [ Comprehension { range: 73..99, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("x"), ctx: Store, @@ -58,9 +70,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 88..99, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..96, id: Name("elements"), ctx: Load, @@ -68,9 +82,11 @@ Module( ), arguments: Arguments { range: 96..99, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("n"), ctx: Load, @@ -90,8 +106,10 @@ Module( generators: [ Comprehension { range: 101..124, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..112, id: Name("n"), ctx: Store, @@ -99,9 +117,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 116..124, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..121, id: Name("range"), ctx: Load, @@ -109,9 +129,11 @@ Module( ), arguments: Arguments { range: 121..124, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_binary_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_binary_expr.py.snap index bfcd7c88d8e6f..40c7d68cfd2d9 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_binary_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_binary_expr.py.snap @@ -1,29 +1,33 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/ambiguous_lpar_with_items_binary_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..337, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 124..143, is_async: false, items: [ WithItem { range: 129..138, + node_index: AtomicNodeIndex(..), context_expr: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 129..138, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 130..131, id: Name("a"), ctx: Load, @@ -31,6 +35,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("b"), ctx: Load, @@ -45,9 +50,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 140..143, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 140..143, }, ), @@ -58,16 +65,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 144..166, is_async: false, items: [ WithItem { range: 149..161, + node_index: AtomicNodeIndex(..), context_expr: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 149..161, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("a"), ctx: Load, @@ -79,6 +90,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..161, id: Name("b"), ctx: Load, @@ -93,9 +105,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 163..166, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 163..166, }, ), @@ -106,18 +120,22 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 196..220, is_async: false, items: [ WithItem { range: 201..215, + node_index: AtomicNodeIndex(..), context_expr: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 201..215, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 202..203, id: Name("a"), ctx: Load, @@ -125,11 +143,13 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 208..215, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 208..209, id: Name("b"), ctx: Load, @@ -137,6 +157,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("c"), ctx: Load, @@ -154,9 +175,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 217..220, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 217..220, }, ), @@ -167,23 +190,28 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 221..245, is_async: false, items: [ WithItem { range: 226..240, + node_index: AtomicNodeIndex(..), context_expr: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 226..240, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 226..235, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 227..228, id: Name("a"), ctx: Load, @@ -191,6 +219,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 234..235, id: Name("b"), ctx: Load, @@ -201,6 +230,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 239..240, id: Name("c"), ctx: Load, @@ -215,9 +245,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 242..245, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 242..245, }, ), @@ -228,22 +260,28 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 246..272, is_async: false, items: [ WithItem { range: 251..267, + node_index: AtomicNodeIndex(..), context_expr: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 251..267, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 251..263, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 252..257, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 252..253, id: Name("a"), ctx: Load, @@ -252,6 +290,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 256..257, id: Name("b"), ctx: Load, @@ -262,6 +301,7 @@ Module( op: LShift, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 262..263, id: Name("c"), ctx: Load, @@ -272,6 +312,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..267, id: Name("d"), ctx: Load, @@ -285,9 +326,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 269..272, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 269..272, }, ), @@ -298,19 +341,24 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 312..336, is_async: false, items: [ WithItem { range: 317..331, + node_index: AtomicNodeIndex(..), context_expr: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 317..331, left: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 317..323, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 318..319, id: Name("a"), ctx: Load, @@ -318,6 +366,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 321..322, value: Int( 0, @@ -330,9 +379,11 @@ Module( op: Add, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 326..331, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 326..327, id: Name("b"), ctx: Load, @@ -341,6 +392,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 330..331, id: Name("c"), ctx: Load, @@ -356,9 +408,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 333..336, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 333..336, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_if_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_if_expr.py.snap index 22de70bfc86e4..00a32e1ab3bd5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_if_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@ambiguous_lpar_with_items_if_expr.py.snap @@ -1,33 +1,38 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/ambiguous_lpar_with_items_if_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..153, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, items: [ WithItem { range: 5..23, + node_index: AtomicNodeIndex(..), context_expr: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 5..23, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 12..16, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -35,6 +40,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("y"), ctx: Load, @@ -48,9 +54,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -61,25 +69,31 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 29..71, is_async: false, items: [ WithItem { range: 34..66, + node_index: AtomicNodeIndex(..), context_expr: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 34..66, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 55..59, value: true, }, ), body: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 34..51, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("x"), ctx: Load, @@ -88,8 +102,10 @@ Module( generators: [ Comprehension { range: 37..50, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Store, @@ -97,6 +113,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..50, id: Name("iter"), ctx: Load, @@ -111,6 +128,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("y"), ctx: Load, @@ -124,9 +142,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..71, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 68..71, }, ), @@ -137,25 +157,31 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 72..120, is_async: false, items: [ WithItem { range: 77..115, + node_index: AtomicNodeIndex(..), context_expr: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 77..115, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 104..108, value: true, }, ), body: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 77..100, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..79, id: Name("x"), ctx: Load, @@ -164,8 +190,10 @@ Module( generators: [ Comprehension { range: 80..99, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("x"), ctx: Store, @@ -173,6 +201,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..99, id: Name("iter"), ctx: Load, @@ -187,6 +216,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..115, id: Name("y"), ctx: Load, @@ -200,9 +230,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 117..120, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 117..120, }, ), @@ -213,25 +245,31 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 121..152, is_async: false, items: [ WithItem { range: 126..147, + node_index: AtomicNodeIndex(..), context_expr: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 126..147, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 136..140, value: true, }, ), body: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 126..132, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("x"), ctx: Load, @@ -239,6 +277,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 130..131, value: Int( 0, @@ -250,6 +289,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("y"), ctx: Load, @@ -263,9 +303,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 149..152, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 149..152, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@ann_assign_stmt_simple_target.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@ann_assign_stmt_simple_target.py.snap index 8dbc86f99307d..c0dc9bb14803d 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@ann_assign_stmt_simple_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@ann_assign_stmt_simple_target.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/ann_assign_stmt_simple_target.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..45, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..6, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Store, @@ -22,6 +24,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("int"), ctx: Load, @@ -33,9 +36,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 17..25, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("a"), ctx: Store, @@ -43,6 +48,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..25, id: Name("int"), ctx: Load, @@ -54,12 +60,15 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 26..34, target: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 26..29, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("a"), ctx: Load, @@ -68,12 +77,14 @@ Module( attr: Identifier { id: Name("b"), range: 28..29, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..34, id: Name("int"), ctx: Load, @@ -85,12 +96,15 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 35..44, target: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 35..39, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("a"), ctx: Load, @@ -98,6 +112,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 37..38, value: Int( 0, @@ -109,6 +124,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..44, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@args_unparenthesized_generator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@args_unparenthesized_generator.py.snap index 961e761f3007b..650a3b6e53aeb 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@args_unparenthesized_generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@args_unparenthesized_generator.py.snap @@ -1,67 +1,228 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/args_unparenthesized_generator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..26, + node_index: AtomicNodeIndex(..), + range: 0..107, body: [ Expr( StmtExpr { - range: 0..25, + node_index: AtomicNodeIndex(..), + range: 0..51, value: Call( ExprCall { - range: 0..25, + node_index: AtomicNodeIndex(..), + range: 0..51, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..3, + id: Name("zip"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 3..51, + node_index: AtomicNodeIndex(..), + args: [ + Generator( + ExprGenerator { + node_index: AtomicNodeIndex(..), + range: 4..26, + elt: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 7..25, + node_index: AtomicNodeIndex(..), + target: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 11..12, + id: Name("x"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 16..25, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 16..21, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 21..25, + node_index: AtomicNodeIndex(..), + args: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 22..24, + value: Int( + 10, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + parenthesized: true, + }, + ), + Generator( + ExprGenerator { + node_index: AtomicNodeIndex(..), + range: 28..50, + elt: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 29..30, + id: Name("y"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 31..49, + node_index: AtomicNodeIndex(..), + target: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 35..36, + id: Name("y"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 40..49, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 40..45, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 45..49, + node_index: AtomicNodeIndex(..), + args: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 46..48, + value: Int( + 10, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + parenthesized: true, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 52..77, + value: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 52..77, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 52..55, id: Name("sum"), ctx: Load, }, ), arguments: Arguments { - range: 3..25, + range: 55..77, + node_index: AtomicNodeIndex(..), args: [ Generator( ExprGenerator { - range: 4..24, + node_index: AtomicNodeIndex(..), + range: 56..76, elt: Name( ExprName { - range: 4..5, + node_index: AtomicNodeIndex(..), + range: 56..57, id: Name("x"), ctx: Load, }, ), generators: [ Comprehension { - range: 6..24, + range: 58..76, + node_index: AtomicNodeIndex(..), target: Name( ExprName { - range: 10..11, + node_index: AtomicNodeIndex(..), + range: 62..63, id: Name("x"), ctx: Store, }, ), iter: Call( ExprCall { - range: 15..24, + node_index: AtomicNodeIndex(..), + range: 67..76, func: Name( ExprName { - range: 15..20, + node_index: AtomicNodeIndex(..), + range: 67..72, id: Name("range"), ctx: Load, }, ), arguments: Arguments { - range: 20..24, + range: 72..76, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { - range: 21..23, + node_index: AtomicNodeIndex(..), + range: 73..75, value: Int( 10, ), @@ -86,6 +247,94 @@ Module( ), }, ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 78..106, + value: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 78..106, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 78..81, + id: Name("sum"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 81..106, + node_index: AtomicNodeIndex(..), + args: [ + Generator( + ExprGenerator { + node_index: AtomicNodeIndex(..), + range: 82..104, + elt: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 83..84, + id: Name("x"), + ctx: Load, + }, + ), + generators: [ + Comprehension { + range: 85..103, + node_index: AtomicNodeIndex(..), + target: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 89..90, + id: Name("x"), + ctx: Store, + }, + ), + iter: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 94..103, + func: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 94..99, + id: Name("range"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 99..103, + node_index: AtomicNodeIndex(..), + args: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 100..102, + value: Int( + 10, + ), + }, + ), + ], + keywords: [], + }, + }, + ), + ifs: [], + is_async: false, + }, + ], + parenthesized: true, + }, + ), + ], + keywords: [], + }, + }, + ), + }, + ), ], }, ) diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap new file mode 100644 index 0000000000000..a9a4f61078021 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap @@ -0,0 +1,177 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..36, + body: [ + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 0..5, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 0..1, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 4..5, + value: Int( + 4, + ), + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 6..13, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 6..7, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: List( + ExprList { + node_index: AtomicNodeIndex(..), + range: 10..13, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 11..12, + value: Int( + 4, + ), + }, + ), + ], + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 14..25, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 14..15, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 18..25, + elts: [ + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 19..23, + value: List( + ExprList { + node_index: AtomicNodeIndex(..), + range: 20..23, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 21..22, + value: Int( + 1, + ), + }, + ), + ], + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + node_index: AtomicNodeIndex(..), + range: 26..35, + targets: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 26..27, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 30..35, + elts: [ + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 30..34, + value: List( + ExprList { + node_index: AtomicNodeIndex(..), + range: 31..34, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 32..33, + value: Int( + 1, + ), + }, + ), + ], + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_targets_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_targets_terminator.py.snap index f188573e18146..52e3ad233850c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_targets_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_targets_terminator.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/assign_targets_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..39, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..13, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -23,6 +25,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("y"), ctx: Store, @@ -30,6 +33,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("z"), ctx: Store, @@ -38,6 +42,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 12..13, value: Int( 1, @@ -48,13 +53,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..19, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 15..19, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("a"), ctx: Load, @@ -62,6 +70,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("b"), ctx: Load, @@ -76,10 +85,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 20..33, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("x"), ctx: Store, @@ -87,6 +98,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("y"), ctx: Store, @@ -94,6 +106,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("z"), ctx: Store, @@ -102,6 +115,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 1, @@ -112,13 +126,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..38, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 34..38, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("a"), ctx: Load, @@ -126,6 +143,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_for_statement.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_for_statement.py.snap index dc8808567876c..f01730187c311 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_for_statement.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_for_statement.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/async_for_statement.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..30, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..29, is_async: true, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..16, id: Name("target"), ctx: Store, @@ -23,6 +25,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..24, id: Name("iter"), ctx: Load, @@ -31,9 +34,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..29, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 26..29, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_function_definition.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_function_definition.py.snap index 1ec8e02a7c166..52ec987c61341 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_function_definition.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_function_definition.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/async_function_definition.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..21, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..20, is_async: true, decorator_list: [], name: Identifier { id: Name("foo"), range: 10..13, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 13..15, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -32,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_with_statement.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_with_statement.py.snap index 2e7e68961ee63..be892875b2369 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_with_statement.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@async_with_statement.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/async_with_statement.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..21, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 0..20, is_async: true, items: [ WithItem { range: 11..15, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..15, id: Name("item"), ctx: Load, @@ -30,9 +33,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 17..20, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_def_arguments.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_def_arguments.py.snap index 37e765799fed3..d1f1b93250fe3 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_def_arguments.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_def_arguments.py.snap @@ -1,31 +1,35 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/class_def_arguments.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..32, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..14, decorator_list: [], name: Identifier { id: Name("Foo"), range: 6..9, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..14, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 11..14, }, ), @@ -36,16 +40,19 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 15..31, decorator_list: [], name: Identifier { id: Name("Foo"), range: 21..24, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 24..26, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -53,9 +60,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..31, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 28..31, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap index 99fc05f67b6a3..ad057f45f606e 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_keyword_in_case_pattern.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/class_keyword_in_case_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..34, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..33, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 2, @@ -23,11 +26,14 @@ Module( cases: [ MatchCase { range: 13..33, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 18..28, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..23, id: Name("Class"), ctx: Load, @@ -35,22 +41,27 @@ Module( ), arguments: PatternArguments { range: 23..28, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 24..27, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 24..25, + node_index: AtomicNodeIndex(..), }, pattern: MatchAs( PatternMatchAs { range: 26..27, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 26..27, + node_index: AtomicNodeIndex(..), }, ), }, @@ -64,9 +75,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..33, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 30..33, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap index 8ca2d420378d4..e55236a706576 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@class_type_params_py312.py.snap @@ -7,34 +7,42 @@ input_file: crates/ruff_python_parser/resources/inline/ok/class_type_params_py31 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..96, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 44..95, decorator_list: [], name: Identifier { id: Name("Foo"), range: 50..53, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 53..90, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 54..69, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("S"), range: 54..55, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 57..69, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..61, id: Name("str"), ctx: Load, @@ -42,6 +50,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..68, id: Name("bytes"), ctx: Load, @@ -59,13 +68,16 @@ Module( TypeVar( TypeParamTypeVar { range: 71..79, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 71..72, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..79, id: Name("float"), ctx: Load, @@ -78,9 +90,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 81..84, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 82..84, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -88,9 +102,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 86..89, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 88..89, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -102,9 +118,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..95, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 92..95, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@comma_separated_regular_list_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@comma_separated_regular_list_terminator.py.snap index 78f64ab6bdce9..9d9c72049a34c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@comma_separated_regular_list_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@comma_separated_regular_list_terminator.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/comma_separated_regular_list_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..181, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 141..144, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 141..144, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 142..143, value: Int( 0, @@ -33,13 +36,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 145..151, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 145..151, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 146..147, value: Int( 0, @@ -48,6 +54,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 149..150, value: Int( 1, @@ -62,13 +69,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 152..159, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 152..159, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 153..154, value: Int( 0, @@ -77,6 +87,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 156..157, value: Int( 1, @@ -91,13 +102,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 160..169, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 160..169, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 161..162, value: Int( 0, @@ -106,6 +120,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 164..165, value: Int( 1, @@ -114,6 +129,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 167..168, value: Int( 2, @@ -128,13 +144,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 170..180, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 170..180, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 171..172, value: Int( 0, @@ -143,6 +162,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 174..175, value: Int( 1, @@ -151,6 +171,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 177..178, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap index 6289f59327824..3a7f83d488b45 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@debug_rename_import.py.snap @@ -7,22 +7,27 @@ input_file: crates/ruff_python_parser/resources/inline/ok/debug_rename_import.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..86, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..25, names: [ Alias { range: 7..25, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 7..16, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("debug"), range: 20..25, + node_index: AtomicNodeIndex(..), }, ), }, @@ -31,19 +36,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 26..52, module: Some( Identifier { id: Name("__debug__"), range: 31..40, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 48..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Some"), range: 48..52, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -53,24 +62,29 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 53..85, module: Some( Identifier { id: Name("x"), range: 58..59, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 67..85, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("__debug__"), range: 67..76, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("debug"), range: 80..85, + node_index: AtomicNodeIndex(..), }, ), }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_async_function.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_async_function.py.snap index 53f7aff8e8507..14d34fa18b017 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_async_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_async_function.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/decorator_async_function.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..32, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..31, is_async: true, decorator_list: [ Decorator { range: 0..10, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..10, id: Name("decorator"), ctx: Load, @@ -29,10 +32,14 @@ Module( name: Identifier { id: Name("foo"), range: 21..24, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 24..26, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -43,9 +50,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..31, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 28..31, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap index 20b6b6d6dfb47..4ad9fb1dc8fac 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_await_expression_py39.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/decorator_await_expres ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..96, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..95, is_async: true, decorator_list: [], name: Identifier { id: Name("foo"), range: 55..58, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 58..60, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,16 +37,20 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 66..95, is_async: false, decorator_list: [ Decorator { range: 66..76, + node_index: AtomicNodeIndex(..), expression: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 67..76, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..76, id: Name("bar"), ctx: Load, @@ -53,10 +63,14 @@ Module( name: Identifier { id: Name("baz"), range: 85..88, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 88..90, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -67,9 +81,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..95, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 92..95, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap index c18f2aa994db4..25bdacc7477a8 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_dotted_ident_py38.py.snap @@ -7,23 +7,29 @@ input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_d ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..86, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..85, is_async: false, decorator_list: [ Decorator { range: 45..69, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..69, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..61, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..53, id: Name("buttons"), ctx: Load, @@ -32,6 +38,7 @@ Module( attr: Identifier { id: Name("clicked"), range: 54..61, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -39,6 +46,7 @@ Module( attr: Identifier { id: Name("connect"), range: 62..69, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -48,10 +56,14 @@ Module( name: Identifier { id: Name("spam"), range: 74..78, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 78..80, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -62,9 +74,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..85, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 82..85, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap index ff8fce644ff08..d3208630da234 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_eval_hack_py38.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_e ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..97, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..96, is_async: false, decorator_list: [ Decorator { range: 45..80, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 46..80, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..50, id: Name("eval"), ctx: Load, @@ -28,14 +33,17 @@ Module( ), arguments: Arguments { range: 50..80, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 51..79, value: StringLiteralValue { inner: Single( StringLiteral { range: 51..79, + node_index: AtomicNodeIndex(..), value: "buttons[0].clicked.connect", flags: StringLiteralFlags { quote_style: Double, @@ -57,10 +65,14 @@ Module( name: Identifier { id: Name("spam"), range: 85..89, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 89..91, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -71,9 +83,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..96, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 93..96, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap index cedb9372d8c7d..f83bba2ca6482 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_identity_hack_py38.py.snap @@ -7,29 +7,38 @@ input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_i ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..111, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..63, is_async: false, decorator_list: [], name: Identifier { id: Name("_"), range: 49..50, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 50..53, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 51..52, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 51..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 51..52, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -44,10 +53,12 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 55..63, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Load, @@ -61,16 +72,20 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 64..110, is_async: false, decorator_list: [ Decorator { range: 64..94, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 65..94, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("_"), ctx: Load, @@ -78,18 +93,23 @@ Module( ), arguments: Arguments { range: 66..94, + node_index: AtomicNodeIndex(..), args: [ Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 67..93, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 67..85, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 67..77, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..74, id: Name("buttons"), ctx: Load, @@ -97,6 +117,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 0, @@ -109,6 +130,7 @@ Module( attr: Identifier { id: Name("clicked"), range: 78..85, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -116,6 +138,7 @@ Module( attr: Identifier { id: Name("connect"), range: 86..93, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -130,10 +153,14 @@ Module( name: Identifier { id: Name("spam"), range: 99..103, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 103..105, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -144,9 +171,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 107..110, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 107..110, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap index 50c0502003ea5..500d2a4b2389b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@decorator_expression_py39.py.snap @@ -7,26 +7,33 @@ input_file: crates/ruff_python_parser/resources/inline/ok/decorator_expression_p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..129, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 45..88, is_async: false, decorator_list: [ Decorator { range: 45..72, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..72, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 46..64, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 46..56, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..53, id: Name("buttons"), ctx: Load, @@ -34,6 +41,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 0, @@ -46,6 +54,7 @@ Module( attr: Identifier { id: Name("clicked"), range: 57..64, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -53,6 +62,7 @@ Module( attr: Identifier { id: Name("connect"), range: 65..72, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -62,10 +72,14 @@ Module( name: Identifier { id: Name("spam"), range: 77..81, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 81..83, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -76,9 +90,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..88, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 85..88, }, ), @@ -89,19 +105,24 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 89..128, is_async: false, decorator_list: [ Decorator { range: 89..113, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 90..113, func: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 91..107, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("x"), ctx: Store, @@ -109,19 +130,26 @@ Module( ), value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 96..107, parameters: Some( Parameters { range: 103..104, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 103..104, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 103..104, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 103..104, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -135,6 +163,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..107, id: Name("x"), ctx: Load, @@ -146,9 +175,11 @@ Module( ), arguments: Arguments { range: 108..113, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..112, id: Name("foo"), ctx: Load, @@ -164,10 +195,14 @@ Module( name: Identifier { id: Name("bar"), range: 118..121, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 121..123, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -178,9 +213,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 125..128, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 125..128, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_debug_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_debug_py38.py.snap index 1ba0db0c2d533..ebaae1fd52466 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_debug_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_debug_py38.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/del_debug_py38.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 43..56, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..56, id: Name("__debug__"), ctx: Del, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_targets_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_targets_terminator.py.snap index 8c0830ab6e402..cd953e90416dd 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_targets_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@del_targets_terminator.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/del_targets_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..29, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 0..8, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("a"), ctx: Del, @@ -23,6 +25,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("b"), ctx: Del, @@ -33,13 +36,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..14, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 10..14, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("c"), ctx: Load, @@ -47,6 +53,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("d"), ctx: Load, @@ -61,10 +68,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 15..23, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("a"), ctx: Del, @@ -72,6 +81,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("b"), ctx: Del, @@ -82,13 +92,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..28, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 24..28, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("c"), ctx: Load, @@ -96,6 +109,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("d"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap index 2d63a9baf1433..9c3c3f576f89c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@dotted_name_normalized_spaces.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/dotted_name_normalized_spaces.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..32, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..12, names: [ Alias { range: 7..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a.b.c"), range: 7..12, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -27,13 +30,16 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 13..31, names: [ Alias { range: 20..31, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a.b.c"), range: 20..31, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap index 56b342c8b5eb2..277f35c1505e2 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@duplicate_match_key_attr.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/duplicate_match_key_at ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..40, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..39, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -22,15 +25,19 @@ Module( cases: [ MatchCase { range: 13..39, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 18..34, + node_index: AtomicNodeIndex(..), keys: [ Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 19..22, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("x"), ctx: Load, @@ -39,15 +46,18 @@ Module( attr: Identifier { id: Name("a"), range: 21..22, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 27..30, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("x"), ctx: Load, @@ -56,6 +66,7 @@ Module( attr: Identifier { id: Name("a"), range: 29..30, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -65,8 +76,10 @@ Module( MatchValue( PatternMatchValue { range: 24..25, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 24..25, value: Int( 1, @@ -78,8 +91,10 @@ Module( MatchValue( PatternMatchValue { range: 32..33, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 2, @@ -96,9 +111,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..39, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 36..39, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_star_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_star_py311.py.snap index f0a6018f84d55..c3efa0df92331 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_star_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_star_py311.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/ok/except_star_py311.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..77, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 44..76, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), @@ -28,9 +32,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 53..76, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..71, id: Name("ValueError"), ctx: Load, @@ -41,9 +47,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 73..76, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 73..76, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_as_name_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_as_name_soft_keyword.py.snap index c27aa0a2e9139..07ef1ddbd3c03 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_as_name_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_as_name_soft_keyword.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/except_stmt_as_name_soft_keyword.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..100, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..99, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5..8, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 5..8, }, ), @@ -29,9 +32,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 9..39, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..25, id: Name("Exception"), ctx: Load, @@ -42,14 +47,17 @@ Module( Identifier { id: Name("match"), range: 29..34, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..39, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 36..39, }, ), @@ -61,9 +69,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 40..69, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..56, id: Name("Exception"), ctx: Load, @@ -74,14 +84,17 @@ Module( Identifier { id: Name("case"), range: 60..64, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..69, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 66..69, }, ), @@ -93,9 +106,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 70..99, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..86, id: Name("Exception"), ctx: Load, @@ -106,14 +121,17 @@ Module( Identifier { id: Name("type"), range: 90..94, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap new file mode 100644 index 0000000000000..bf94e92b01cfd --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap @@ -0,0 +1,140 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..117, + body: [ + Try( + StmtTry { + node_index: AtomicNodeIndex(..), + range: 44..79, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 53..57, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 58..79, + node_index: AtomicNodeIndex(..), + type_: Some( + Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 65..69, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 68..69, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 75..79, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + node_index: AtomicNodeIndex(..), + range: 80..116, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 89..93, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 94..116, + node_index: AtomicNodeIndex(..), + type_: Some( + Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 102..106, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 102..103, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 105..106, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 112..116, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__arguments.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__arguments.py.snap index a7b3283c41e66..d5977dbfc4e14 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__arguments.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__arguments.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/valid/expressions/arguments.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..805, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 102..108, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 102..108, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..106, id: Name("call"), ctx: Load, @@ -24,6 +28,7 @@ Module( ), arguments: Arguments { range: 106..108, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -33,12 +38,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..119, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 109..119, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..113, id: Name("call"), ctx: Load, @@ -46,9 +54,11 @@ Module( ), arguments: Arguments { range: 113..119, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..115, id: Name("x"), ctx: Load, @@ -56,6 +66,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 117..118, id: Name("y"), ctx: Load, @@ -70,12 +81,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 120..131, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 120..131, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..124, id: Name("call"), ctx: Load, @@ -83,9 +97,11 @@ Module( ), arguments: Arguments { range: 124..131, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 125..126, id: Name("x"), ctx: Load, @@ -93,6 +109,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("y"), ctx: Load, @@ -107,12 +124,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 150..164, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 150..164, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..154, id: Name("call"), ctx: Load, @@ -120,18 +140,22 @@ Module( ), arguments: Arguments { range: 154..164, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 155..158, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 155..156, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 157..158, value: Int( 1, @@ -141,14 +165,17 @@ Module( }, Keyword { range: 160..163, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("y"), range: 160..161, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 162..163, value: Int( 2, @@ -164,12 +191,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 165..173, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 165..173, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..169, id: Name("call"), ctx: Load, @@ -177,12 +207,15 @@ Module( ), arguments: Arguments { range: 169..173, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 170..172, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("x"), ctx: Load, @@ -200,12 +233,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 174..183, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 174..183, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 174..178, id: Name("call"), ctx: Load, @@ -213,13 +249,16 @@ Module( ), arguments: Arguments { range: 178..183, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 179..182, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 181..182, id: Name("x"), ctx: Load, @@ -234,12 +273,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 193..205, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 193..205, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..197, id: Name("call"), ctx: Load, @@ -247,9 +289,11 @@ Module( ), arguments: Arguments { range: 197..205, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 198..199, id: Name("x"), ctx: Load, @@ -259,14 +303,17 @@ Module( keywords: [ Keyword { range: 201..204, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("y"), range: 201..202, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 203..204, value: Int( 1, @@ -282,12 +329,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 206..217, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 206..217, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 206..210, id: Name("call"), ctx: Load, @@ -295,9 +345,11 @@ Module( ), arguments: Arguments { range: 210..217, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 211..212, id: Name("x"), ctx: Load, @@ -305,9 +357,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 214..216, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 215..216, id: Name("y"), ctx: Load, @@ -325,12 +379,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 218..230, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 218..230, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 218..222, id: Name("call"), ctx: Load, @@ -338,9 +395,11 @@ Module( ), arguments: Arguments { range: 222..230, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 223..224, id: Name("x"), ctx: Load, @@ -350,9 +409,11 @@ Module( keywords: [ Keyword { range: 226..229, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 228..229, id: Name("y"), ctx: Load, @@ -367,12 +428,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 231..244, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 231..244, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 231..235, id: Name("call"), ctx: Load, @@ -380,12 +444,15 @@ Module( ), arguments: Arguments { range: 235..244, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 241..243, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 242..243, id: Name("y"), ctx: Load, @@ -398,14 +465,17 @@ Module( keywords: [ Keyword { range: 236..239, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 236..237, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 238..239, value: Int( 1, @@ -421,12 +491,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 245..259, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 245..259, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..249, id: Name("call"), ctx: Load, @@ -434,18 +507,22 @@ Module( ), arguments: Arguments { range: 249..259, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 250..253, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 250..251, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 252..253, value: Int( 1, @@ -455,9 +532,11 @@ Module( }, Keyword { range: 255..258, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 257..258, id: Name("y"), ctx: Load, @@ -472,12 +551,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 260..273, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 260..273, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 260..264, id: Name("call"), ctx: Load, @@ -485,12 +567,15 @@ Module( ), arguments: Arguments { range: 264..273, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 265..267, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..267, id: Name("x"), ctx: Load, @@ -503,9 +588,11 @@ Module( keywords: [ Keyword { range: 269..272, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 271..272, id: Name("y"), ctx: Load, @@ -520,12 +607,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 274..288, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 274..288, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 274..278, id: Name("call"), ctx: Load, @@ -533,12 +623,15 @@ Module( ), arguments: Arguments { range: 278..288, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 279..281, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 280..281, id: Name("x"), ctx: Load, @@ -549,6 +642,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 283..284, id: Name("y"), ctx: Load, @@ -556,6 +650,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 286..287, id: Name("z"), ctx: Load, @@ -570,12 +665,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 289..308, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 289..308, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 289..293, id: Name("call"), ctx: Load, @@ -583,13 +681,16 @@ Module( ), arguments: Arguments { range: 293..308, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 294..297, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 296..297, id: Name("x"), ctx: Load, @@ -598,14 +699,17 @@ Module( }, Keyword { range: 299..302, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("y"), range: 299..300, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 301..302, value: Int( 1, @@ -615,14 +719,17 @@ Module( }, Keyword { range: 304..307, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("z"), range: 304..305, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 306..307, value: Int( 2, @@ -638,12 +745,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 309..335, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 309..335, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 309..313, id: Name("call"), ctx: Load, @@ -651,12 +761,15 @@ Module( ), arguments: Arguments { range: 313..335, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 314..317, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 315..317, id: Name("x1"), ctx: Load, @@ -667,9 +780,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 319..322, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 320..322, id: Name("x2"), ctx: Load, @@ -682,9 +797,11 @@ Module( keywords: [ Keyword { range: 324..328, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 326..328, id: Name("y1"), ctx: Load, @@ -693,9 +810,11 @@ Module( }, Keyword { range: 330..334, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 332..334, id: Name("y2"), ctx: Load, @@ -710,12 +829,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 336..355, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 336..355, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 336..340, id: Name("call"), ctx: Load, @@ -723,18 +845,22 @@ Module( ), arguments: Arguments { range: 340..355, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 341..344, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 341..342, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 343..344, value: Int( 1, @@ -744,9 +870,11 @@ Module( }, Keyword { range: 346..349, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 348..349, id: Name("y"), ctx: Load, @@ -755,14 +883,17 @@ Module( }, Keyword { range: 351..354, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("z"), range: 351..352, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 353..354, value: Int( 1, @@ -778,12 +909,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 378..402, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 378..402, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 378..382, id: Name("call"), ctx: Load, @@ -791,27 +925,33 @@ Module( ), arguments: Arguments { range: 382..402, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 383..401, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 383..384, + node_index: AtomicNodeIndex(..), }, ), value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 385..401, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 390..394, value: true, }, ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 385..386, value: Int( 1, @@ -820,6 +960,7 @@ Module( ), orelse: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 400..401, value: Int( 2, @@ -837,12 +978,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 403..418, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 403..418, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 403..407, id: Name("call"), ctx: Load, @@ -850,21 +994,26 @@ Module( ), arguments: Arguments { range: 407..418, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 408..417, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 408..409, + node_index: AtomicNodeIndex(..), }, ), value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 410..417, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 416..417, id: Name("y"), ctx: Load, @@ -881,12 +1030,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 419..438, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 419..438, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 419..423, id: Name("call"), ctx: Load, @@ -894,31 +1046,41 @@ Module( ), arguments: Arguments { range: 423..438, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 424..437, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 424..425, + node_index: AtomicNodeIndex(..), }, ), value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 426..437, parameters: Some( Parameters { range: 433..434, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 433..434, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 433..434, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 433..434, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -932,6 +1094,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 436..437, id: Name("y"), ctx: Load, @@ -948,12 +1111,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 439..455, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 439..455, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 439..443, id: Name("call"), ctx: Load, @@ -961,21 +1127,26 @@ Module( ), arguments: Arguments { range: 443..455, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 444..454, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("x"), range: 444..445, + node_index: AtomicNodeIndex(..), }, ), value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 447..453, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 447..448, id: Name("y"), ctx: Store, @@ -983,6 +1154,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 452..453, value: Int( 1, @@ -1000,12 +1172,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 476..491, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 476..491, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 476..480, id: Name("call"), ctx: Load, @@ -1013,13 +1188,16 @@ Module( ), arguments: Arguments { range: 480..491, + node_index: AtomicNodeIndex(..), args: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 482..489, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 488..489, id: Name("x"), ctx: Load, @@ -1037,12 +1215,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 492..512, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 492..512, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 492..496, id: Name("call"), ctx: Load, @@ -1050,12 +1231,15 @@ Module( ), arguments: Arguments { range: 496..512, + node_index: AtomicNodeIndex(..), args: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 498..510, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 509..510, id: Name("x"), ctx: Load, @@ -1072,12 +1256,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 533..545, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 533..545, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 533..537, id: Name("call"), ctx: Load, @@ -1085,12 +1272,15 @@ Module( ), arguments: Arguments { range: 537..545, + node_index: AtomicNodeIndex(..), args: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 538..544, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 538..539, id: Name("x"), ctx: Store, @@ -1098,6 +1288,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 543..544, value: Int( 1, @@ -1115,12 +1306,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 546..572, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 546..572, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 546..550, id: Name("call"), ctx: Load, @@ -1128,15 +1322,19 @@ Module( ), arguments: Arguments { range: 550..572, + node_index: AtomicNodeIndex(..), args: [ Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 551..571, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 551..557, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 551..552, id: Name("x"), ctx: Store, @@ -1144,6 +1342,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 556..557, value: Int( 1, @@ -1155,8 +1354,10 @@ Module( generators: [ Comprehension { range: 558..571, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 562..563, id: Name("i"), ctx: Store, @@ -1164,6 +1365,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 567..571, id: Name("iter"), ctx: Load, @@ -1185,12 +1387,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 596..610, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 596..610, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 596..600, id: Name("call"), ctx: Load, @@ -1198,17 +1403,21 @@ Module( ), arguments: Arguments { range: 600..610, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 601..609, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 602..609, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 602..603, id: Name("x"), ctx: Load, @@ -1216,6 +1425,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 608..609, id: Name("y"), ctx: Load, @@ -1236,12 +1446,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 611..623, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 611..623, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 611..615, id: Name("call"), ctx: Load, @@ -1249,15 +1462,19 @@ Module( ), arguments: Arguments { range: 615..623, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 616..622, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 617..622, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 617..618, id: Name("x"), ctx: Load, @@ -1266,6 +1483,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 621..622, id: Name("y"), ctx: Load, @@ -1285,12 +1503,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 624..638, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 624..638, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 624..628, id: Name("call"), ctx: Load, @@ -1298,15 +1519,19 @@ Module( ), arguments: Arguments { range: 628..638, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 629..637, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 630..637, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 636..637, id: Name("x"), ctx: Load, @@ -1326,12 +1551,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 639..657, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 639..657, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 639..643, id: Name("call"), ctx: Load, @@ -1339,25 +1567,34 @@ Module( ), arguments: Arguments { range: 643..657, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 644..656, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 645..656, parameters: Some( Parameters { range: 652..653, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 652..653, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 652..653, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 652..653, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1371,6 +1608,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 655..656, id: Name("x"), ctx: Load, @@ -1390,12 +1628,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 658..681, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 658..681, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 658..662, id: Name("call"), ctx: Load, @@ -1403,21 +1644,26 @@ Module( ), arguments: Arguments { range: 662..681, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 663..680, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 664..680, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 669..673, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 664..665, id: Name("x"), ctx: Load, @@ -1425,6 +1671,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 679..680, id: Name("y"), ctx: Load, @@ -1444,12 +1691,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 700..709, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 700..709, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 700..704, id: Name("call"), ctx: Load, @@ -1457,13 +1707,16 @@ Module( ), arguments: Arguments { range: 704..709, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 705..708, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 707..708, id: Name("x"), ctx: Load, @@ -1478,12 +1731,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 710..725, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 710..725, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 710..714, id: Name("call"), ctx: Load, @@ -1491,18 +1747,22 @@ Module( ), arguments: Arguments { range: 714..725, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 715..724, + node_index: AtomicNodeIndex(..), arg: None, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 717..724, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 717..718, id: Name("x"), ctx: Load, @@ -1510,6 +1770,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 723..724, id: Name("y"), ctx: Load, @@ -1527,12 +1788,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 726..741, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 726..741, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 726..730, id: Name("call"), ctx: Load, @@ -1540,16 +1804,20 @@ Module( ), arguments: Arguments { range: 730..741, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 731..740, + node_index: AtomicNodeIndex(..), arg: None, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 733..740, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 739..740, id: Name("x"), ctx: Load, @@ -1566,12 +1834,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 742..766, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 742..766, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 742..746, id: Name("call"), ctx: Load, @@ -1579,22 +1850,27 @@ Module( ), arguments: Arguments { range: 746..766, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 747..765, + node_index: AtomicNodeIndex(..), arg: None, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 749..765, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 754..758, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 749..750, id: Name("x"), ctx: Load, @@ -1602,6 +1878,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 764..765, id: Name("y"), ctx: Load, @@ -1618,12 +1895,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 767..784, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 767..784, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 767..771, id: Name("call"), ctx: Load, @@ -1631,17 +1911,21 @@ Module( ), arguments: Arguments { range: 771..784, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 772..783, + node_index: AtomicNodeIndex(..), arg: None, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 775..782, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 781..782, id: Name("x"), ctx: Load, @@ -1659,12 +1943,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 785..804, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 785..804, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 785..789, id: Name("call"), ctx: Load, @@ -1672,26 +1959,35 @@ Module( ), arguments: Arguments { range: 789..804, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 790..803, + node_index: AtomicNodeIndex(..), arg: None, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 792..803, parameters: Some( Parameters { range: 799..800, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 799..800, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 799..800, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 799..800, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1705,6 +2001,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 802..803, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__attribute.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__attribute.py.snap index 2a37d0a8a9da0..e6b46d32b7b58 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__attribute.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__attribute.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/attribute.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..90, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 0..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("value"), ctx: Load, @@ -26,6 +29,7 @@ Module( attr: Identifier { id: Name("attr"), range: 6..10, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -34,15 +38,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..23, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 11..23, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 11..21, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..16, id: Name("value"), ctx: Load, @@ -51,12 +59,14 @@ Module( attr: Identifier { id: Name("attr"), range: 17..21, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 21..23, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -66,15 +76,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..36, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 24..36, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 24..31, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..29, id: Name("value"), ctx: Load, @@ -82,6 +96,7 @@ Module( ), arguments: Arguments { range: 29..31, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -90,6 +105,7 @@ Module( attr: Identifier { id: Name("attr"), range: 32..36, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -98,21 +114,27 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 37..55, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 37..55, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 37..51, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 37..49, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 37..44, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..42, id: Name("value"), ctx: Load, @@ -120,6 +142,7 @@ Module( ), arguments: Arguments { range: 42..44, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -128,12 +151,14 @@ Module( attr: Identifier { id: Name("attr"), range: 45..49, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 49..51, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -142,6 +167,7 @@ Module( attr: Identifier { id: Name("foo"), range: 52..55, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -150,15 +176,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 56..70, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 56..70, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 56..66, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..61, id: Name("value"), ctx: Load, @@ -167,6 +197,7 @@ Module( attr: Identifier { id: Name("attr"), range: 62..66, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -174,6 +205,7 @@ Module( attr: Identifier { id: Name("foo"), range: 67..70, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -182,18 +214,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..89, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 71..89, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 71..85, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 71..83, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..77, id: Name("value"), ctx: Load, @@ -202,12 +239,14 @@ Module( attr: Identifier { id: Name("attr"), range: 79..83, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 83..85, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -216,6 +255,7 @@ Module( attr: Identifier { id: Name("foo"), range: 86..89, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__await.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__await.py.snap index e5351ffe1e94e..d2791d35df9a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__await.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__await.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/await.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..211, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 0..7, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -29,15 +32,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..19, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 8..19, left: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 8..15, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("x"), ctx: Load, @@ -48,6 +55,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 1, @@ -60,17 +68,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..33, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 20..33, op: And, values: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 20..27, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("a"), ctx: Load, @@ -80,6 +92,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("b"), ctx: Load, @@ -92,15 +105,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..43, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 34..43, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 40..43, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("f"), ctx: Load, @@ -108,6 +125,7 @@ Module( ), arguments: Arguments { range: 41..43, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -119,16 +137,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..56, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 44..56, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 50..56, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, @@ -137,6 +159,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 2, @@ -153,16 +176,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..69, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 57..69, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 63..69, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 64..65, value: Int( 3, @@ -171,6 +198,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 67..68, value: Int( 4, @@ -186,18 +214,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..82, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 70..82, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 76..82, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("i"), ctx: Load, @@ -206,6 +238,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 80..81, value: Int( 5, @@ -222,16 +255,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..93, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 83..93, elts: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 83..90, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 7, @@ -242,6 +279,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 92..93, value: Int( 8, @@ -257,16 +295,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..107, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 94..107, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 100..107, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 101..102, value: Int( 9, @@ -275,6 +317,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..106, value: Int( 10, @@ -292,15 +335,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 108..120, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 108..120, left: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 108..115, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 114..115, value: Int( 1, @@ -315,6 +362,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -328,21 +376,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 121..146, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 121..146, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 132..136, value: true, }, ), body: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 121..128, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("x"), ctx: Load, @@ -352,6 +405,7 @@ Module( ), orelse: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 142..146, }, ), @@ -361,19 +415,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 147..158, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 147..158, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 153..158, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 154..156, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 155..156, id: Name("x"), ctx: Load, @@ -393,25 +452,34 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 159..178, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 159..178, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 166..177, parameters: Some( Parameters { range: 173..174, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 173..174, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 173..174, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 173..174, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -425,6 +493,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 176..177, id: Name("x"), ctx: Load, @@ -438,15 +507,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 179..192, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 179..192, left: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 179..186, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..186, id: Name("x"), ctx: Load, @@ -457,10 +530,12 @@ Module( op: Pow, right: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 190..192, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 191..192, id: Name("x"), ctx: Load, @@ -474,15 +549,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 193..211, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 193..211, left: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 193..200, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..200, id: Name("x"), ctx: Load, @@ -493,9 +572,11 @@ Module( op: Pow, right: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 204..211, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 210..211, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bin_op.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bin_op.py.snap index a6cc4528a1bfe..ac83b26a4d1a8 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bin_op.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bin_op.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/valid/expressions/bin_op.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..397, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..14, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 9..14, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 1, @@ -26,6 +30,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 2, @@ -38,12 +43,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..20, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 15..20, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 15..16, value: Int( 1, @@ -53,6 +61,7 @@ Module( op: Sub, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 19..20, value: Int( 2, @@ -65,12 +74,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..26, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 21..26, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..22, value: Int( 1, @@ -80,6 +92,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 2, @@ -92,12 +105,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..32, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 27..32, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 27..28, value: Int( 1, @@ -107,6 +123,7 @@ Module( op: Div, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 31..32, value: Int( 2, @@ -119,12 +136,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 33..39, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 33..39, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 33..34, value: Int( 1, @@ -134,6 +154,7 @@ Module( op: FloorDiv, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 38..39, value: Int( 2, @@ -146,12 +167,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..45, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 40..45, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 40..41, value: Int( 1, @@ -161,6 +185,7 @@ Module( op: Mod, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..45, value: Int( 2, @@ -173,12 +198,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 46..52, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 46..52, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 46..47, value: Int( 1, @@ -188,6 +216,7 @@ Module( op: Pow, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 2, @@ -200,12 +229,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..58, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 53..58, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 53..54, value: Int( 1, @@ -215,6 +247,7 @@ Module( op: BitOr, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 2, @@ -227,12 +260,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 59..64, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 59..64, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 59..60, value: Int( 1, @@ -242,6 +278,7 @@ Module( op: BitXor, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 63..64, value: Int( 2, @@ -254,12 +291,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 65..70, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 65..70, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 1, @@ -269,6 +309,7 @@ Module( op: BitAnd, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 69..70, value: Int( 2, @@ -281,12 +322,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..77, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 71..77, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 71..72, value: Int( 1, @@ -296,6 +340,7 @@ Module( op: RShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 76..77, value: Int( 2, @@ -308,12 +353,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 78..84, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 78..84, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 1, @@ -323,6 +371,7 @@ Module( op: LShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 2, @@ -335,12 +384,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..90, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 85..90, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 85..86, value: Int( 1, @@ -350,6 +402,7 @@ Module( op: MatMult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 2, @@ -362,18 +415,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 110..123, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 110..123, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 110..119, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 110..115, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 110..111, value: Int( 1, @@ -383,6 +441,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 114..115, value: Int( 2, @@ -394,6 +453,7 @@ Module( op: Sub, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..119, value: Int( 3, @@ -405,6 +465,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 4, @@ -417,24 +478,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 124..146, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 124..146, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 124..142, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 124..138, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 124..133, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 124..129, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 124..125, value: Int( 1, @@ -444,6 +512,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 128..129, value: Int( 2, @@ -455,6 +524,7 @@ Module( op: Div, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 132..133, value: Int( 3, @@ -466,6 +536,7 @@ Module( op: FloorDiv, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 137..138, value: Int( 4, @@ -477,6 +548,7 @@ Module( op: MatMult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 141..142, value: Int( 5, @@ -488,6 +560,7 @@ Module( op: Mod, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 145..146, value: Int( 6, @@ -500,21 +573,27 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 147..168, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 147..168, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 147..163, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 147..158, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 147..153, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 147..148, value: Int( 1, @@ -524,6 +603,7 @@ Module( op: LShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 152..153, value: Int( 2, @@ -535,6 +615,7 @@ Module( op: RShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 157..158, value: Int( 3, @@ -546,6 +627,7 @@ Module( op: RShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 162..163, value: Int( 4, @@ -557,6 +639,7 @@ Module( op: LShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 167..168, value: Int( 5, @@ -569,12 +652,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 193..202, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 193..202, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 193..194, value: Int( 1, @@ -584,9 +670,11 @@ Module( op: Add, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 197..202, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 197..198, value: Int( 2, @@ -596,6 +684,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 3, @@ -610,15 +699,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 203..212, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 203..212, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 203..208, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 203..204, value: Int( 1, @@ -628,6 +721,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 207..208, value: Int( 2, @@ -639,6 +733,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 211..212, value: Int( 3, @@ -651,24 +746,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 213..244, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 213..244, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 213..235, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 213..231, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 213..223, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 213..219, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..214, value: Int( 1, @@ -678,6 +780,7 @@ Module( op: Pow, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 218..219, value: Int( 2, @@ -689,6 +792,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 222..223, value: Int( 3, @@ -700,9 +804,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 226..231, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 226..227, value: Int( 4, @@ -712,6 +818,7 @@ Module( op: MatMult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 230..231, value: Int( 5, @@ -725,6 +832,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 234..235, value: Int( 6, @@ -736,9 +844,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 238..244, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 238..239, value: Int( 7, @@ -748,6 +858,7 @@ Module( op: FloorDiv, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 243..244, value: Int( 8, @@ -762,12 +873,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 270..306, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 270..306, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 270..271, value: Int( 1, @@ -777,12 +891,15 @@ Module( op: BitOr, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 274..306, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 274..279, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 274..275, value: Int( 2, @@ -792,6 +909,7 @@ Module( op: BitAnd, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 278..279, value: Int( 3, @@ -803,15 +921,19 @@ Module( op: BitXor, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 282..306, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 282..301, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 282..291, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 282..283, value: Int( 4, @@ -821,9 +943,11 @@ Module( op: Add, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 286..291, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 286..287, value: Int( 5, @@ -833,6 +957,7 @@ Module( op: MatMult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 290..291, value: Int( 6, @@ -846,9 +971,11 @@ Module( op: LShift, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 295..301, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 295..296, value: Int( 7, @@ -858,6 +985,7 @@ Module( op: FloorDiv, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 300..301, value: Int( 8, @@ -871,6 +999,7 @@ Module( op: RShift, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 305..306, value: Int( 9, @@ -887,15 +1016,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 324..339, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 324..339, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 324..335, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 324..325, value: Int( 1, @@ -905,9 +1038,11 @@ Module( op: Add, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 329..334, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 329..330, value: Int( 2, @@ -917,6 +1052,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 333..334, value: Int( 3, @@ -930,6 +1066,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 338..339, value: Int( 4, @@ -942,15 +1079,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 340..359, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 340..359, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 340..345, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 340..341, value: Int( 1, @@ -960,6 +1101,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 344..345, value: Int( 2, @@ -971,12 +1113,15 @@ Module( op: Add, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 349..358, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 349..354, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 349..350, value: Int( 3, @@ -986,6 +1131,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 353..354, value: Int( 4, @@ -997,6 +1143,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 357..358, value: Int( 5, @@ -1011,12 +1158,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 390..396, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 390..396, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 390..391, id: Name("x"), ctx: Load, @@ -1025,10 +1175,12 @@ Module( op: Add, right: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 393..396, op: UAdd, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 395..396, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bool_op.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bool_op.py.snap index 53f4b05b7a1cc..7805f521fb4f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bool_op.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__bool_op.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/bool_op.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..142, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..7, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 0..7, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Load, @@ -27,6 +30,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("b"), ctx: Load, @@ -39,14 +43,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 8..21, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 8..21, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("a"), ctx: Load, @@ -54,6 +61,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("b"), ctx: Load, @@ -61,6 +69,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("c"), ctx: Load, @@ -73,14 +82,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..28, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 22..28, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("a"), ctx: Load, @@ -88,6 +100,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..28, id: Name("b"), ctx: Load, @@ -100,14 +113,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..40, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 29..40, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("a"), ctx: Load, @@ -115,6 +131,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("b"), ctx: Load, @@ -122,6 +139,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("c"), ctx: Load, @@ -134,19 +152,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 41..53, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 41..53, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 41..48, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("a"), ctx: Load, @@ -154,6 +176,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("b"), ctx: Load, @@ -164,6 +187,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..53, id: Name("c"), ctx: Load, @@ -176,19 +200,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..88, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 54..88, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 54..67, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("a"), ctx: Load, @@ -196,6 +224,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("b"), ctx: Load, @@ -203,6 +232,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("c"), ctx: Load, @@ -213,6 +243,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..72, id: Name("d"), ctx: Load, @@ -220,11 +251,13 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 76..83, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..77, id: Name("e"), ctx: Load, @@ -232,6 +265,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("f"), ctx: Load, @@ -242,6 +276,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..88, id: Name("g"), ctx: Load, @@ -254,19 +289,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 89..105, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 89..105, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 89..100, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("a"), ctx: Load, @@ -274,10 +313,12 @@ Module( ), UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 95..100, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("b"), ctx: Load, @@ -290,6 +331,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("c"), ctx: Load, @@ -302,23 +344,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 106..124, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 106..124, value: Some( BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 112..124, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 112..119, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 112..113, id: Name("a"), ctx: Load, @@ -326,6 +373,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 118..119, id: Name("b"), ctx: Load, @@ -336,6 +384,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 123..124, id: Name("c"), ctx: Load, @@ -351,23 +400,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 125..141, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 125..141, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 125..136, op: And, values: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 125..130, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("a"), ctx: Load, @@ -377,6 +431,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..136, id: Name("b"), ctx: Load, @@ -387,6 +442,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("c"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__call.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__call.py.snap index a83f3774421b0..7a33b5db76879 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__call.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__call.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/call.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..349, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 114..120, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 114..120, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..118, id: Name("call"), ctx: Load, @@ -25,6 +28,7 @@ Module( ), arguments: Arguments { range: 118..120, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -34,15 +38,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 121..132, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 121..132, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 121..130, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..125, id: Name("attr"), ctx: Load, @@ -51,12 +59,14 @@ Module( attr: Identifier { id: Name("expr"), range: 126..130, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 130..132, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -66,15 +76,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..150, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 133..150, func: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 133..148, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..142, id: Name("subscript"), ctx: Load, @@ -82,10 +96,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 143..147, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 143..144, value: Int( 1, @@ -94,6 +110,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 146..147, value: Int( 2, @@ -110,6 +127,7 @@ Module( ), arguments: Arguments { range: 148..150, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -119,15 +137,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 151..162, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 151..162, func: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 151..160, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..156, id: Name("slice"), ctx: Load, @@ -135,11 +157,13 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 157..159, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 158..159, value: Int( 1, @@ -155,6 +179,7 @@ Module( ), arguments: Arguments { range: 160..162, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -164,16 +189,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 163..174, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 163..174, func: List( ExprList { + node_index: AtomicNodeIndex(..), range: 163..172, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 164..165, value: Int( 1, @@ -182,6 +211,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 167..168, value: Int( 2, @@ -190,6 +220,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 170..171, value: Int( 3, @@ -202,6 +233,7 @@ Module( ), arguments: Arguments { range: 172..174, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -211,16 +243,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 175..186, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 175..186, func: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 175..184, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 176..177, value: Int( 1, @@ -229,6 +265,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 179..180, value: Int( 2, @@ -237,6 +274,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 182..183, value: Int( 3, @@ -250,6 +288,7 @@ Module( ), arguments: Arguments { range: 184..186, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -259,15 +298,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 187..206, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 187..206, func: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 187..204, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 188..189, id: Name("x"), ctx: Load, @@ -276,8 +319,10 @@ Module( generators: [ Comprehension { range: 190..203, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 194..195, id: Name("x"), ctx: Store, @@ -285,6 +330,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..203, id: Name("iter"), ctx: Load, @@ -299,6 +345,7 @@ Module( ), arguments: Arguments { range: 204..206, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -308,16 +355,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 207..218, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 207..218, func: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 207..216, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 208..209, value: Int( 1, @@ -326,6 +377,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 211..212, value: Int( 2, @@ -334,6 +386,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 214..215, value: Int( 3, @@ -345,6 +398,7 @@ Module( ), arguments: Arguments { range: 216..218, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -354,18 +408,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 219..233, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 219..233, func: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 219..231, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 220..221, value: Int( 1, @@ -375,6 +433,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 223..224, value: Int( 2, @@ -386,6 +445,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 226..227, value: Int( 3, @@ -395,6 +455,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 229..230, value: Int( 4, @@ -407,6 +468,7 @@ Module( ), arguments: Arguments { range: 231..233, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -416,16 +478,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 234..245, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 234..245, func: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 235..242, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..242, id: Name("x"), ctx: Load, @@ -436,6 +502,7 @@ Module( ), arguments: Arguments { range: 243..245, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -445,18 +512,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 306..312, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 306..312, func: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 306..310, value: true, }, ), arguments: Arguments { range: 310..312, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -466,18 +537,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 313..320, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 313..320, func: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 313..318, value: false, }, ), arguments: Arguments { range: 318..320, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -487,17 +562,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 321..327, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 321..327, func: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 321..325, }, ), arguments: Arguments { range: 325..327, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -507,17 +586,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 328..338, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 328..338, func: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 328..336, value: StringLiteralValue { inner: Single( StringLiteral { range: 328..336, + node_index: AtomicNodeIndex(..), value: "string", flags: StringLiteralFlags { quote_style: Double, @@ -531,6 +614,7 @@ Module( ), arguments: Arguments { range: 336..338, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -540,12 +624,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 339..342, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 339..342, func: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 339..340, value: Int( 1, @@ -554,6 +641,7 @@ Module( ), arguments: Arguments { range: 340..342, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -563,12 +651,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 343..348, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 343..348, func: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 343..346, value: Float( 1.0, @@ -577,6 +668,7 @@ Module( ), arguments: Arguments { range: 346..348, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__compare.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__compare.py.snap index ec86643e38054..e2bd2b568cd30 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__compare.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__compare.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/compare.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..542, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..15, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 9..15, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("a"), ctx: Load, @@ -29,6 +32,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("b"), ctx: Load, @@ -41,12 +45,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..21, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 16..21, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("b"), ctx: Load, @@ -58,6 +65,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("a"), ctx: Load, @@ -70,12 +78,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..27, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 22..27, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("b"), ctx: Load, @@ -87,6 +98,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("a"), ctx: Load, @@ -99,12 +111,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..34, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 28..34, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("a"), ctx: Load, @@ -116,6 +131,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..34, id: Name("b"), ctx: Load, @@ -128,12 +144,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..41, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 35..41, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("a"), ctx: Load, @@ -145,6 +164,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("b"), ctx: Load, @@ -157,12 +177,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 42..48, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 42..48, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..43, id: Name("a"), ctx: Load, @@ -174,6 +197,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("b"), ctx: Load, @@ -186,12 +210,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..55, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 49..55, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("a"), ctx: Load, @@ -203,6 +230,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("c"), ctx: Load, @@ -215,12 +243,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 56..62, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 56..62, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("a"), ctx: Load, @@ -232,6 +263,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..62, id: Name("b"), ctx: Load, @@ -244,12 +276,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 63..73, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 63..73, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..64, id: Name("a"), ctx: Load, @@ -261,6 +296,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("c"), ctx: Load, @@ -273,12 +309,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 74..84, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 74..84, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("a"), ctx: Load, @@ -290,6 +329,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("b"), ctx: Load, @@ -302,12 +342,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 110..156, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 110..156, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 110..111, id: Name("a"), ctx: Load, @@ -323,6 +366,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..120, id: Name("b"), ctx: Load, @@ -330,6 +374,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("c"), ctx: Load, @@ -337,6 +382,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("d"), ctx: Load, @@ -344,6 +390,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("e"), ctx: Load, @@ -351,6 +398,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 155..156, id: Name("f"), ctx: Load, @@ -363,15 +411,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 177..203, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 177..203, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 177..182, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 177..178, id: Name("a"), ctx: Load, @@ -380,6 +432,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 181..182, id: Name("b"), ctx: Load, @@ -394,9 +447,11 @@ Module( comparators: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 185..190, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..186, id: Name("c"), ctx: Load, @@ -405,6 +460,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("d"), ctx: Load, @@ -414,9 +470,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 198..203, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 198..199, id: Name("e"), ctx: Load, @@ -425,6 +483,7 @@ Module( op: BitAnd, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 202..203, id: Name("f"), ctx: Load, @@ -439,16 +498,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 379..393, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 379..393, op: Not, operand: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 383..393, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 383..384, id: Name("x"), ctx: Load, @@ -460,6 +523,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 392..393, id: Name("y"), ctx: Load, @@ -474,14 +538,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 395..416, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 395..416, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 395..396, id: Name("x"), ctx: Load, @@ -489,14 +556,17 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 400..416, op: And, values: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 400..410, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 400..401, id: Name("y"), ctx: Load, @@ -508,6 +578,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 409..410, id: Name("z"), ctx: Load, @@ -518,6 +589,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 415..416, id: Name("a"), ctx: Load, @@ -533,12 +605,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 417..429, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 417..429, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 417..418, id: Name("x"), ctx: Load, @@ -550,9 +625,11 @@ Module( comparators: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 422..429, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 428..429, id: Name("y"), ctx: Load, @@ -567,12 +644,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 430..446, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 430..446, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 430..431, id: Name("x"), ctx: Load, @@ -584,9 +664,11 @@ Module( comparators: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 439..446, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 445..446, id: Name("y"), ctx: Load, @@ -601,12 +683,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 489..541, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 489..541, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 489..490, id: Name("a"), ctx: Load, @@ -626,6 +711,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 493..494, id: Name("b"), ctx: Load, @@ -633,6 +719,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 498..499, id: Name("c"), ctx: Load, @@ -640,6 +727,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 502..503, id: Name("d"), ctx: Load, @@ -647,6 +735,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 507..508, id: Name("e"), ctx: Load, @@ -654,6 +743,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 516..517, id: Name("f"), ctx: Load, @@ -661,6 +751,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 525..526, id: Name("g"), ctx: Load, @@ -668,6 +759,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 530..531, id: Name("h"), ctx: Load, @@ -675,6 +767,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 535..536, id: Name("i"), ctx: Load, @@ -682,6 +775,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 540..541, id: Name("j"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary.py.snap index 705faf5d2c801..d368b8654bceb 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/dictionary.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..622, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..11, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 9..11, items: [], }, @@ -23,15 +25,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..18, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 12..18, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 1, @@ -41,6 +46,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..17, value: Int( 2, @@ -55,15 +61,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..43, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 19..43, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 20..21, value: Int( 1, @@ -73,6 +82,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 23..24, value: Int( 2, @@ -84,6 +94,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("a"), ctx: Load, @@ -92,6 +103,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 29..30, value: Int( 1, @@ -103,6 +115,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("b"), ctx: Load, @@ -111,11 +124,13 @@ Module( ), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 35..42, value: StringLiteralValue { inner: Single( StringLiteral { range: 35..42, + node_index: AtomicNodeIndex(..), value: "hello", flags: StringLiteralFlags { quote_style: Single, @@ -135,9 +150,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..69, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 66..69, items: [], }, @@ -146,15 +163,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..100, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 70..100, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 76..77, value: Int( 1, @@ -164,6 +184,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 2, @@ -175,6 +196,7 @@ Module( key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 90..91, value: Int( 3, @@ -184,6 +206,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 97..98, value: Int( 4, @@ -198,21 +221,25 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 111..132, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 111..132, items: [ DictItem { key: Some( Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 112..118, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 113..114, value: Int( 1, @@ -222,6 +249,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 116..117, value: Int( 2, @@ -235,12 +263,14 @@ Module( ), value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 120..131, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 121..122, value: Int( 3, @@ -250,12 +280,14 @@ Module( ), value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 124..130, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 125..126, value: Int( 4, @@ -265,6 +297,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 128..129, value: Int( 5, @@ -287,28 +320,37 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 155..171, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 155..171, items: [ DictItem { key: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 156..167, parameters: Some( Parameters { range: 163..164, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 163..164, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 163..164, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 163..164, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -322,6 +364,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("x"), ctx: Load, @@ -332,6 +375,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 169..170, value: Int( 1, @@ -346,20 +390,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 172..202, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 172..202, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 173..176, value: StringLiteralValue { inner: Single( StringLiteral { range: 173..176, + node_index: AtomicNodeIndex(..), value: "A", flags: StringLiteralFlags { quote_style: Single, @@ -374,19 +422,26 @@ Module( ), value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 178..192, parameters: Some( Parameters { range: 185..186, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 185..186, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 185..186, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("p"), range: 185..186, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -400,6 +455,7 @@ Module( ), body: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 188..192, }, ), @@ -410,11 +466,13 @@ Module( key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 194..197, value: StringLiteralValue { inner: Single( StringLiteral { range: 194..197, + node_index: AtomicNodeIndex(..), value: "B", flags: StringLiteralFlags { quote_style: Single, @@ -429,6 +487,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 199..200, id: Name("C"), ctx: Load, @@ -442,18 +501,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 224..237, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 224..237, items: [ DictItem { key: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 226..232, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 226..227, id: Name("x"), ctx: Store, @@ -461,6 +524,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 231..232, value: Int( 1, @@ -472,6 +536,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 235..236, id: Name("y"), ctx: Load, @@ -485,18 +550,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 238..258, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 238..258, items: [ DictItem { key: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 240..246, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 240..241, id: Name("x"), ctx: Store, @@ -504,6 +573,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 245..246, value: Int( 1, @@ -515,9 +585,11 @@ Module( ), value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 250..256, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 250..251, id: Name("y"), ctx: Store, @@ -525,6 +597,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 255..256, value: Int( 2, @@ -541,15 +614,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 284..289, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 284..289, items: [ DictItem { key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 287..288, id: Name("d"), ctx: Load, @@ -563,15 +639,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 290..301, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 290..301, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 291..292, id: Name("a"), ctx: Load, @@ -580,6 +659,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 294..295, id: Name("b"), ctx: Load, @@ -590,6 +670,7 @@ Module( key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 299..300, id: Name("d"), ctx: Load, @@ -603,15 +684,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 302..312, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 302..312, items: [ DictItem { key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 305..306, id: Name("a"), ctx: Load, @@ -622,6 +706,7 @@ Module( key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 310..311, id: Name("b"), ctx: Load, @@ -635,20 +720,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 313..338, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 313..338, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 314..317, value: StringLiteralValue { inner: Single( StringLiteral { range: 314..317, + node_index: AtomicNodeIndex(..), value: "a", flags: StringLiteralFlags { quote_style: Double, @@ -663,11 +752,13 @@ Module( ), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 319..322, value: StringLiteralValue { inner: Single( StringLiteral { range: 319..322, + node_index: AtomicNodeIndex(..), value: "b", flags: StringLiteralFlags { quote_style: Double, @@ -684,6 +775,7 @@ Module( key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 326..327, id: Name("c"), ctx: Load, @@ -694,11 +786,13 @@ Module( key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 329..332, value: StringLiteralValue { inner: Single( StringLiteral { range: 329..332, + node_index: AtomicNodeIndex(..), value: "d", flags: StringLiteralFlags { quote_style: Double, @@ -713,11 +807,13 @@ Module( ), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 334..337, value: StringLiteralValue { inner: Single( StringLiteral { range: 334..337, + node_index: AtomicNodeIndex(..), value: "e", flags: StringLiteralFlags { quote_style: Double, @@ -737,15 +833,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 339..367, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 339..367, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 340..341, value: Int( 1, @@ -755,6 +854,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 343..344, value: Int( 2, @@ -766,17 +866,20 @@ Module( key: None, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 348..366, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 349..357, value: StringLiteralValue { inner: Single( StringLiteral { range: 349..357, + node_index: AtomicNodeIndex(..), value: "nested", flags: StringLiteralFlags { quote_style: Single, @@ -791,11 +894,13 @@ Module( ), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 359..365, value: StringLiteralValue { inner: Single( StringLiteral { range: 359..365, + node_index: AtomicNodeIndex(..), value: "dict", flags: StringLiteralFlags { quote_style: Single, @@ -819,18 +924,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 368..393, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 368..393, items: [ DictItem { key: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 369..374, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 369..370, id: Name("x"), ctx: Load, @@ -839,6 +948,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 373..374, value: Int( 1, @@ -850,9 +960,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 376..382, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 376..377, id: Name("y"), ctx: Load, @@ -861,6 +973,7 @@ Module( op: Pow, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 381..382, value: Int( 2, @@ -874,9 +987,11 @@ Module( key: None, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 386..392, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 386..390, id: Name("call"), ctx: Load, @@ -884,6 +999,7 @@ Module( ), arguments: Arguments { range: 390..392, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -897,19 +1013,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 460..471, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 460..471, items: [ DictItem { key: None, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 464..469, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 468..469, id: Name("x"), ctx: Load, @@ -925,15 +1045,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 494..515, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 494..515, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 495..496, value: Int( 1, @@ -943,15 +1066,18 @@ Module( ), value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 498..514, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 503..507, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 498..499, id: Name("x"), ctx: Load, @@ -959,6 +1085,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 513..514, id: Name("y"), ctx: Load, @@ -974,21 +1101,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 516..575, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 516..575, key: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 517..533, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 522..526, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 517..518, id: Name("x"), ctx: Load, @@ -996,6 +1128,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 532..533, id: Name("y"), ctx: Load, @@ -1005,6 +1138,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 535..536, id: Name("y"), ctx: Load, @@ -1013,8 +1147,10 @@ Module( generators: [ Comprehension { range: 537..555, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 541..542, id: Name("x"), ctx: Store, @@ -1022,9 +1158,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 546..555, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 546..551, id: Name("range"), ctx: Load, @@ -1032,9 +1170,11 @@ Module( ), arguments: Arguments { range: 551..555, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 552..554, value: Int( 10, @@ -1051,8 +1191,10 @@ Module( }, Comprehension { range: 556..574, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 560..561, id: Name("y"), ctx: Store, @@ -1060,9 +1202,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 565..574, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 565..570, id: Name("range"), ctx: Load, @@ -1070,9 +1214,11 @@ Module( ), arguments: Arguments { range: 570..574, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 571..573, value: Int( 10, @@ -1094,19 +1240,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 576..600, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 576..600, items: [ DictItem { key: Some( Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 577..583, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 578..579, value: Int( 1, @@ -1115,6 +1265,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 581..582, value: Int( 2, @@ -1127,6 +1278,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 585..586, value: Int( 3, @@ -1138,6 +1290,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 588..589, id: Name("x"), ctx: Load, @@ -1146,12 +1299,14 @@ Module( ), value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 591..598, items: [ DictItem { key: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 592..593, value: Int( 1, @@ -1161,6 +1316,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 595..596, value: Int( 2, @@ -1179,15 +1335,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 601..621, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 601..621, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 603..604, id: Name("x"), ctx: Load, @@ -1196,6 +1355,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 608..609, id: Name("y"), ctx: Load, @@ -1206,6 +1366,7 @@ Module( key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 613..614, id: Name("z"), ctx: Load, @@ -1214,6 +1375,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 618..619, id: Name("a"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary_comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary_comprehension.py.snap index ea7d3324bc499..d54aceae23f22 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary_comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__dictionary_comprehension.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/dictionary_comprehension.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..589, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..22, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 0..22, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("y"), ctx: Load, @@ -26,8 +29,10 @@ Module( generators: [ Comprehension { range: 3..21, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("y"), ctx: Store, @@ -35,10 +40,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 12..21, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 1, @@ -47,6 +54,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..17, value: Int( 2, @@ -55,6 +63,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 19..20, value: Int( 3, @@ -76,12 +85,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..42, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 23..42, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..26, id: Name("x1"), ctx: Load, @@ -89,6 +101,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..30, id: Name("x2"), ctx: Load, @@ -97,8 +110,10 @@ Module( generators: [ Comprehension { range: 31..41, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("y"), ctx: Store, @@ -106,6 +121,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("z"), ctx: Load, @@ -121,15 +137,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..73, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 43..73, key: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 44..49, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Load, @@ -138,6 +158,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 48..49, value: Int( 1, @@ -148,11 +169,13 @@ Module( ), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 51..54, value: StringLiteralValue { inner: Single( StringLiteral { range: 51..54, + node_index: AtomicNodeIndex(..), value: "x", flags: StringLiteralFlags { quote_style: Single, @@ -167,8 +190,10 @@ Module( generators: [ Comprehension { range: 55..72, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("i"), ctx: Store, @@ -176,9 +201,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 64..72, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..69, id: Name("range"), ctx: Load, @@ -186,9 +213,11 @@ Module( ), arguments: Arguments { range: 69..72, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 70..71, value: Int( 5, @@ -210,12 +239,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 74..122, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 74..122, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("b"), ctx: Load, @@ -223,9 +255,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 78..83, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..79, id: Name("c"), ctx: Load, @@ -234,6 +268,7 @@ Module( op: Mult, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 2, @@ -245,8 +280,10 @@ Module( generators: [ Comprehension { range: 84..121, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..89, id: Name("c"), ctx: Store, @@ -254,6 +291,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("d"), ctx: Load, @@ -262,9 +300,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 98..104, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("x"), ctx: Load, @@ -276,6 +316,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 103..104, id: Name("w"), ctx: Load, @@ -286,11 +327,13 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 108..116, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("y"), ctx: Load, @@ -298,6 +341,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..116, id: Name("yy"), ctx: Load, @@ -308,6 +352,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("z"), ctx: Load, @@ -323,12 +368,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 123..176, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 123..176, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 124..125, id: Name("a"), ctx: Load, @@ -336,9 +384,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 127..133, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("a"), ctx: Load, @@ -347,6 +397,7 @@ Module( op: Pow, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 132..133, value: Int( 2, @@ -358,8 +409,10 @@ Module( generators: [ Comprehension { range: 134..155, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 138..139, id: Name("b"), ctx: Store, @@ -367,6 +420,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..144, id: Name("c"), ctx: Load, @@ -375,11 +429,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 148..155, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..149, id: Name("d"), ctx: Load, @@ -387,6 +443,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 154..155, id: Name("e"), ctx: Load, @@ -400,8 +457,10 @@ Module( }, Comprehension { range: 156..175, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..161, id: Name("f"), ctx: Store, @@ -409,6 +468,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..166, id: Name("j"), ctx: Load, @@ -417,9 +477,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 170..175, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("k"), ctx: Load, @@ -431,6 +493,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 174..175, id: Name("h"), ctx: Load, @@ -449,12 +512,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 177..231, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 177..231, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("a"), ctx: Load, @@ -462,6 +528,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 181..182, id: Name("b"), ctx: Load, @@ -470,8 +537,10 @@ Module( generators: [ Comprehension { range: 183..204, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 187..188, id: Name("b"), ctx: Store, @@ -479,6 +548,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 192..193, id: Name("c"), ctx: Load, @@ -487,11 +557,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 197..204, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 197..198, id: Name("d"), ctx: Load, @@ -499,6 +571,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 203..204, id: Name("e"), ctx: Load, @@ -512,8 +585,10 @@ Module( }, Comprehension { range: 205..230, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 215..216, id: Name("f"), ctx: Store, @@ -521,6 +596,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 220..221, id: Name("j"), ctx: Load, @@ -529,9 +605,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 225..230, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 225..226, id: Name("k"), ctx: Load, @@ -543,6 +621,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 229..230, id: Name("h"), ctx: Load, @@ -561,12 +640,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 232..252, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 232..252, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 233..234, id: Name("a"), ctx: Load, @@ -574,6 +656,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("a"), ctx: Load, @@ -582,12 +665,15 @@ Module( generators: [ Comprehension { range: 238..251, + node_index: AtomicNodeIndex(..), target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 242..246, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 242..243, id: Name("b"), ctx: Store, @@ -595,6 +681,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("c"), ctx: Store, @@ -607,6 +694,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 250..251, id: Name("d"), ctx: Load, @@ -622,12 +710,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 391..416, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 391..416, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 392..393, id: Name("x"), ctx: Load, @@ -635,6 +726,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 395..396, id: Name("y"), ctx: Load, @@ -643,8 +735,10 @@ Module( generators: [ Comprehension { range: 397..415, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 401..402, id: Name("x"), ctx: Store, @@ -652,10 +746,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 407..414, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 413..414, id: Name("y"), ctx: Load, @@ -674,12 +770,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 417..447, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 417..447, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 418..419, id: Name("x"), ctx: Load, @@ -687,6 +786,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 421..422, id: Name("y"), ctx: Load, @@ -695,8 +795,10 @@ Module( generators: [ Comprehension { range: 423..446, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 427..428, id: Name("x"), ctx: Store, @@ -704,9 +806,11 @@ Module( ), iter: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 433..445, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 444..445, id: Name("y"), ctx: Load, @@ -724,12 +828,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 448..477, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 448..477, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 449..450, id: Name("x"), ctx: Load, @@ -737,6 +844,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 452..453, id: Name("y"), ctx: Load, @@ -745,8 +853,10 @@ Module( generators: [ Comprehension { range: 454..476, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 458..459, id: Name("x"), ctx: Store, @@ -754,19 +864,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 464..475, parameters: Some( Parameters { range: 471..472, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 471..472, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 471..472, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 471..472, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -780,6 +897,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 474..475, id: Name("y"), ctx: Load, @@ -797,12 +915,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 478..511, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 478..511, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 479..480, id: Name("x"), ctx: Load, @@ -810,6 +931,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 482..483, id: Name("y"), ctx: Load, @@ -818,8 +940,10 @@ Module( generators: [ Comprehension { range: 484..510, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 488..489, id: Name("x"), ctx: Store, @@ -827,6 +951,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 493..497, id: Name("data"), ctx: Load, @@ -835,10 +960,12 @@ Module( ifs: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 502..509, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 508..509, id: Name("y"), ctx: Load, @@ -857,12 +984,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 512..550, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 512..550, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 513..514, id: Name("x"), ctx: Load, @@ -870,6 +1000,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 516..517, id: Name("y"), ctx: Load, @@ -878,8 +1009,10 @@ Module( generators: [ Comprehension { range: 518..549, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 522..523, id: Name("x"), ctx: Store, @@ -887,6 +1020,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 527..531, id: Name("data"), ctx: Load, @@ -895,9 +1029,11 @@ Module( ifs: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 536..548, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 547..548, id: Name("y"), ctx: Load, @@ -915,12 +1051,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 551..588, value: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 551..588, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 552..553, id: Name("x"), ctx: Load, @@ -928,6 +1067,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 555..556, id: Name("y"), ctx: Load, @@ -936,8 +1076,10 @@ Module( generators: [ Comprehension { range: 557..587, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 561..562, id: Name("x"), ctx: Store, @@ -945,6 +1087,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 566..570, id: Name("data"), ctx: Load, @@ -953,19 +1096,26 @@ Module( ifs: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 575..586, parameters: Some( Parameters { range: 582..583, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 582..583, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 582..583, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 582..583, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -979,6 +1129,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 585..586, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap index 2fe8ea2855641..1fe9ef6fa7b96 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap @@ -1,26 +1,29 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/f_string.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..979, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..21, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 18..21, value: FStringValue { inner: Single( FString( FString { range: 18..21, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Double, @@ -37,15 +40,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..25, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 22..25, value: FStringValue { inner: Single( FString( FString { range: 22..25, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Double, @@ -62,15 +68,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..29, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 26..29, value: FStringValue { inner: Single( FString( FString { range: 26..29, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Single, @@ -87,15 +96,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..37, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 30..37, value: FStringValue { inner: Single( FString( FString { range: 30..37, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Double, @@ -112,15 +124,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 38..45, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 38..45, value: FStringValue { inner: Single( FString( FString { range: 38..45, + node_index: AtomicNodeIndex(..), elements: [], flags: FStringFlags { quote_style: Single, @@ -137,26 +152,32 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 47..56, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 47..56, value: FStringValue { inner: Single( FString( FString { range: 47..56, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 49..55, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 50..54, value: StringLiteralValue { inner: Single( StringLiteral { range: 50..54, + node_index: AtomicNodeIndex(..), value: " f", flags: StringLiteralFlags { quote_style: Double, @@ -189,21 +210,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..67, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 57..67, value: FStringValue { inner: Single( FString( FString { range: 57..67, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 59..66, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..63, id: Name("foo"), ctx: Load, @@ -230,25 +256,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..75, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 68..75, value: FStringValue { inner: Single( FString( FString { range: 68..75, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 70..74, + node_index: AtomicNodeIndex(..), expression: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 71..73, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 71..72, value: Int( 3, @@ -281,24 +313,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 76..86, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 76..86, value: FStringValue { inner: Single( FString( FString { range: 76..86, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 78..85, + node_index: AtomicNodeIndex(..), expression: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 79..83, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 3, @@ -311,6 +349,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 4, @@ -323,8 +362,9 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 84..84, + node_index: AtomicNodeIndex(..), elements: [], }, ), @@ -346,21 +386,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 87..102, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 87..102, value: FStringValue { inner: Single( FString( FString { range: 87..102, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 89..101, + node_index: AtomicNodeIndex(..), expression: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 90..91, value: Int( 3, @@ -370,19 +415,23 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 92..100, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 92..97, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 93..96, value: StringLiteralValue { inner: Single( StringLiteral { range: 93..96, + node_index: AtomicNodeIndex(..), value: "}", flags: StringLiteralFlags { quote_style: Double, @@ -400,8 +449,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 97..100, + node_index: AtomicNodeIndex(..), value: ">10", }, ), @@ -426,21 +476,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 103..118, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 103..118, value: FStringValue { inner: Single( FString( FString { range: 103..118, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 105..117, + node_index: AtomicNodeIndex(..), expression: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 106..107, value: Int( 3, @@ -450,19 +505,23 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 108..116, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 108..113, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 109..112, value: StringLiteralValue { inner: Single( StringLiteral { range: 109..112, + node_index: AtomicNodeIndex(..), value: "{", flags: StringLiteralFlags { quote_style: Double, @@ -480,8 +539,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 113..116, + node_index: AtomicNodeIndex(..), value: ">10", }, ), @@ -506,21 +566,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 119..133, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 119..133, value: FStringValue { inner: Single( FString( FString { range: 119..133, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 121..132, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 124..127, id: Name("foo"), ctx: Load, @@ -552,21 +617,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 134..154, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 134..154, value: FStringValue { inner: Single( FString( FString { range: 134..154, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 136..153, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..142, id: Name("foo"), ctx: Load, @@ -580,12 +650,14 @@ Module( ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 147..152, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 147..152, + node_index: AtomicNodeIndex(..), value: ".3f ", }, ), @@ -610,21 +682,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 155..173, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 155..173, value: FStringValue { inner: Single( FString( FString { range: 155..173, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 157..172, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..163, id: Name("foo"), ctx: Load, @@ -656,25 +733,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 174..190, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 174..190, value: FStringValue { inner: Single( FString( FString { range: 174..190, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 176..189, + node_index: AtomicNodeIndex(..), expression: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 179..183, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 179..180, value: Int( 1, @@ -683,6 +766,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 182..183, value: Int( 2, @@ -720,33 +804,41 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 191..217, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 191..217, value: FStringValue { inner: Single( FString( FString { range: 191..217, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 193..216, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 194..210, value: FStringValue { inner: Single( FString( FString { range: 194..210, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 196..209, + node_index: AtomicNodeIndex(..), expression: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 197..203, value: Float( 3.1415, @@ -761,12 +853,14 @@ Module( ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 205..208, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 205..208, + node_index: AtomicNodeIndex(..), value: ".1f", }, ), @@ -790,12 +884,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 211..215, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 211..215, + node_index: AtomicNodeIndex(..), value: "*^20", }, ), @@ -820,15 +916,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 219..253, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 219..253, items: [ DictItem { key: Some( FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 220..248, value: FStringValue { inner: Concatenated( @@ -836,6 +935,7 @@ Module( Literal( StringLiteral { range: 220..226, + node_index: AtomicNodeIndex(..), value: "foo ", flags: StringLiteralFlags { quote_style: Double, @@ -847,21 +947,26 @@ Module( FString( FString { range: 227..242, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 229..233, + node_index: AtomicNodeIndex(..), value: "bar ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 233..240, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 234..239, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 234..235, id: Name("x"), ctx: Load, @@ -870,6 +975,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 238..239, id: Name("y"), ctx: Load, @@ -883,8 +989,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 240..241, + node_index: AtomicNodeIndex(..), value: " ", }, ), @@ -899,6 +1006,7 @@ Module( Literal( StringLiteral { range: 243..248, + node_index: AtomicNodeIndex(..), value: "baz", flags: StringLiteralFlags { quote_style: Double, @@ -915,6 +1023,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 250..252, value: Int( 10, @@ -929,9 +1038,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 254..345, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 260..263, id: Name("foo"), ctx: Load, @@ -940,16 +1051,20 @@ Module( cases: [ MatchCase { range: 269..293, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 274..279, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 274..279, value: StringLiteralValue { inner: Single( StringLiteral { range: 274..279, + node_index: AtomicNodeIndex(..), value: "one", flags: StringLiteralFlags { quote_style: Double, @@ -967,6 +1082,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 289..293, }, ), @@ -974,11 +1090,14 @@ Module( }, MatchCase { range: 298..345, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 303..331, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 303..331, value: StringLiteralValue { inner: Concatenated( @@ -986,6 +1105,7 @@ Module( strings: [ StringLiteral { range: 303..316, + node_index: AtomicNodeIndex(..), value: "implicitly ", flags: StringLiteralFlags { quote_style: Double, @@ -995,6 +1115,7 @@ Module( }, StringLiteral { range: 317..331, + node_index: AtomicNodeIndex(..), value: "concatenated", flags: StringLiteralFlags { quote_style: Double, @@ -1015,6 +1136,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 341..345, }, ), @@ -1025,27 +1147,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 347..364, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 347..364, value: FStringValue { inner: Single( FString( FString { range: 347..364, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 349..350, + node_index: AtomicNodeIndex(..), value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 350..355, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 351..354, id: Name("foo"), ctx: Load, @@ -1057,16 +1185,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 355..356, + node_index: AtomicNodeIndex(..), value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 356..363, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 357..360, id: Name("bar"), ctx: Load, @@ -1075,12 +1206,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 361..362, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 361..362, + node_index: AtomicNodeIndex(..), value: "\\", }, ), @@ -1105,19 +1238,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 365..379, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 365..379, value: FStringValue { inner: Single( FString( FString { range: 365..379, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 367..378, + node_index: AtomicNodeIndex(..), value: "\\{foo\\}", }, ), @@ -1137,21 +1274,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 380..420, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 380..420, value: FStringValue { inner: Single( FString( FString { range: 380..420, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 384..417, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 390..393, id: Name("foo"), ctx: Load, @@ -1160,12 +1302,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 394..416, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 394..416, + node_index: AtomicNodeIndex(..), value: "x\n y\n z\n", }, ), @@ -1190,21 +1334,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 421..439, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 421..439, value: FStringValue { inner: Single( FString( FString { range: 421..439, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 423..438, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 428..431, id: Name("foo"), ctx: Load, @@ -1236,27 +1385,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 441..486, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 441..486, value: FStringValue { inner: Single( FString( FString { range: 441..486, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 443..450, + node_index: AtomicNodeIndex(..), value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 450..455, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 451..454, id: Name("foo"), ctx: Load, @@ -1268,16 +1423,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 455..468, + node_index: AtomicNodeIndex(..), value: " {another} ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 468..473, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 469..472, id: Name("bar"), ctx: Load, @@ -1289,16 +1447,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 473..476, + node_index: AtomicNodeIndex(..), value: " {", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 476..483, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 477..482, id: Name("three"), ctx: Load, @@ -1310,8 +1471,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 483..485, + node_index: AtomicNodeIndex(..), value: "}", }, ), @@ -1331,27 +1493,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 487..529, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 487..529, value: FStringValue { inner: Single( FString( FString { range: 487..529, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 489..496, + node_index: AtomicNodeIndex(..), value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 496..503, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 497..500, id: Name("foo"), ctx: Load, @@ -1363,16 +1531,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 503..504, + node_index: AtomicNodeIndex(..), value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 504..511, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 505..508, id: Name("bar"), ctx: Load, @@ -1384,16 +1555,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 511..512, + node_index: AtomicNodeIndex(..), value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 512..519, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 513..516, id: Name("baz"), ctx: Load, @@ -1405,16 +1579,19 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 519..520, + node_index: AtomicNodeIndex(..), value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 520..528, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 521..527, id: Name("foobar"), ctx: Load, @@ -1441,27 +1618,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 530..549, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 530..549, value: FStringValue { inner: Single( FString( FString { range: 530..549, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 532..539, + node_index: AtomicNodeIndex(..), value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 539..548, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 540..541, id: Name("x"), ctx: Load, @@ -1470,12 +1653,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 542..547, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 542..547, + node_index: AtomicNodeIndex(..), value: "y + 2", }, ), @@ -1500,21 +1685,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 550..568, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 550..568, value: FStringValue { inner: Single( FString( FString { range: 550..568, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 552..567, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 553..554, id: Name("x"), ctx: Load, @@ -1523,24 +1713,30 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 555..566, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 555..566, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 556..565, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 556..563, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 556..559, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 557..558, value: Int( 1, @@ -1553,12 +1749,14 @@ Module( attr: Identifier { id: Name("pop"), range: 560..563, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 563..565, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1590,34 +1788,45 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 569..588, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 569..588, value: FStringValue { inner: Single( FString( FString { range: 569..588, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 571..587, + node_index: AtomicNodeIndex(..), expression: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 573..585, parameters: Some( Parameters { range: 580..581, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 580..581, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 580..581, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 580..581, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1631,10 +1840,12 @@ Module( ), body: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 582..585, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 583..584, id: Name("x"), ctx: Load, @@ -1666,21 +1877,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 589..597, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 589..597, value: FStringValue { inner: Single( FString( FString { range: 589..597, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 591..596, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 592..593, id: Name("x"), ctx: Load, @@ -1712,21 +1928,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 598..611, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 598..611, value: FStringValue { inner: Single( FString( FString { range: 598..611, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 600..610, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 605..606, id: Name("x"), ctx: Load, @@ -1758,21 +1979,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 612..621, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 612..621, value: FStringValue { inner: Single( FString( FString { range: 612..621, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 614..620, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 615..616, id: Name("x"), ctx: Load, @@ -1804,21 +2030,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 622..636, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 622..636, value: FStringValue { inner: Single( FString( FString { range: 622..636, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 624..635, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 625..626, id: Name("x"), ctx: Load, @@ -1827,12 +2058,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 627..634, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 627..634, + node_index: AtomicNodeIndex(..), value: ".3f!r =", }, ), @@ -1857,21 +2090,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 637..653, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 637..653, value: FStringValue { inner: Single( FString( FString { range: 637..653, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 639..652, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 640..641, id: Name("x"), ctx: Load, @@ -1885,12 +2123,14 @@ Module( ), conversion: Repr, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 648..651, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 648..651, + node_index: AtomicNodeIndex(..), value: ".3f", }, ), @@ -1915,21 +2155,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 654..667, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 654..667, value: FStringValue { inner: Single( FString( FString { range: 654..667, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 656..666, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 657..658, id: Name("x"), ctx: Load, @@ -1938,12 +2183,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 659..665, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 659..665, + node_index: AtomicNodeIndex(..), value: ".3f=!r", }, ), @@ -1968,9 +2215,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 668..682, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 668..682, value: FStringValue { inner: Concatenated( @@ -1978,6 +2227,7 @@ Module( Literal( StringLiteral { range: 668..675, + node_index: AtomicNodeIndex(..), value: "hello", flags: StringLiteralFlags { quote_style: Double, @@ -1989,12 +2239,15 @@ Module( FString( FString { range: 676..682, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 678..681, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 679..680, id: Name("x"), ctx: Load, @@ -2022,9 +2275,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 683..696, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 683..696, value: FStringValue { inner: Concatenated( @@ -2032,12 +2287,15 @@ Module( FString( FString { range: 683..689, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 685..688, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 686..687, id: Name("x"), ctx: Load, @@ -2059,12 +2317,15 @@ Module( FString( FString { range: 690..696, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 692..695, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 693..694, id: Name("y"), ctx: Load, @@ -2092,9 +2353,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 697..711, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 697..711, value: FStringValue { inner: Concatenated( @@ -2102,12 +2365,15 @@ Module( FString( FString { range: 697..703, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 699..702, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 700..701, id: Name("x"), ctx: Load, @@ -2129,6 +2395,7 @@ Module( Literal( StringLiteral { range: 704..711, + node_index: AtomicNodeIndex(..), value: "world", flags: StringLiteralFlags { quote_style: Double, @@ -2146,31 +2413,38 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 712..756, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 712..756, value: FStringValue { inner: Single( FString( FString { range: 712..756, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 714..739, + node_index: AtomicNodeIndex(..), value: "Invalid args in command: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 739..755, + node_index: AtomicNodeIndex(..), expression: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 740..754, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 740..747, id: Name("command"), ctx: Load, @@ -2178,9 +2452,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 749..754, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 750..754, id: Name("args"), ctx: Load, @@ -2215,9 +2491,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 757..775, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 757..775, value: FStringValue { inner: Concatenated( @@ -2225,6 +2503,7 @@ Module( Literal( StringLiteral { range: 757..762, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -2236,12 +2515,15 @@ Module( FString( FString { range: 763..769, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 765..768, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 766..767, id: Name("x"), ctx: Load, @@ -2263,6 +2545,7 @@ Module( Literal( StringLiteral { range: 770..775, + node_index: AtomicNodeIndex(..), value: "bar", flags: StringLiteralFlags { quote_style: Double, @@ -2280,9 +2563,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 776..825, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 782..823, value: FStringValue { inner: Concatenated( @@ -2290,10 +2575,12 @@ Module( FString( FString { range: 782..786, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 784..785, + node_index: AtomicNodeIndex(..), value: "a", }, ), @@ -2308,10 +2595,12 @@ Module( FString( FString { range: 791..795, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 793..794, + node_index: AtomicNodeIndex(..), value: "b", }, ), @@ -2326,6 +2615,7 @@ Module( Literal( StringLiteral { range: 800..803, + node_index: AtomicNodeIndex(..), value: "c", flags: StringLiteralFlags { quote_style: Double, @@ -2337,10 +2627,12 @@ Module( FString( FString { range: 808..813, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 811..812, + node_index: AtomicNodeIndex(..), value: "d", }, ), @@ -2357,10 +2649,12 @@ Module( FString( FString { range: 818..823, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 821..822, + node_index: AtomicNodeIndex(..), value: "e", }, ), @@ -2383,9 +2677,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 850..879, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 850..879, value: FStringValue { inner: Concatenated( @@ -2393,6 +2689,7 @@ Module( Literal( StringLiteral { range: 850..856, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -2404,12 +2701,15 @@ Module( FString( FString { range: 857..865, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 859..864, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 860..863, id: Name("bar"), ctx: Load, @@ -2431,6 +2731,7 @@ Module( Literal( StringLiteral { range: 866..871, + node_index: AtomicNodeIndex(..), value: "baz", flags: StringLiteralFlags { quote_style: Double, @@ -2442,6 +2743,7 @@ Module( Literal( StringLiteral { range: 872..879, + node_index: AtomicNodeIndex(..), value: " some", flags: StringLiteralFlags { quote_style: Double, @@ -2459,9 +2761,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 880..909, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 880..909, value: FStringValue { inner: Concatenated( @@ -2469,6 +2773,7 @@ Module( Literal( StringLiteral { range: 880..885, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -2480,12 +2785,15 @@ Module( FString( FString { range: 886..894, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 888..893, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 889..892, id: Name("bar"), ctx: Load, @@ -2507,6 +2815,7 @@ Module( Literal( StringLiteral { range: 895..901, + node_index: AtomicNodeIndex(..), value: "baz", flags: StringLiteralFlags { quote_style: Double, @@ -2518,6 +2827,7 @@ Module( Literal( StringLiteral { range: 902..909, + node_index: AtomicNodeIndex(..), value: " some", flags: StringLiteralFlags { quote_style: Double, @@ -2535,9 +2845,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 910..939, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 910..939, value: FStringValue { inner: Concatenated( @@ -2545,6 +2857,7 @@ Module( Literal( StringLiteral { range: 910..915, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -2556,12 +2869,15 @@ Module( FString( FString { range: 916..924, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 918..923, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 919..922, id: Name("bar"), ctx: Load, @@ -2583,6 +2899,7 @@ Module( Literal( StringLiteral { range: 925..930, + node_index: AtomicNodeIndex(..), value: "baz", flags: StringLiteralFlags { quote_style: Double, @@ -2594,6 +2911,7 @@ Module( Literal( StringLiteral { range: 931..939, + node_index: AtomicNodeIndex(..), value: " some", flags: StringLiteralFlags { quote_style: Double, @@ -2611,9 +2929,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 940..978, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 940..978, value: FStringValue { inner: Concatenated( @@ -2621,6 +2941,7 @@ Module( Literal( StringLiteral { range: 940..946, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -2632,18 +2953,22 @@ Module( FString( FString { range: 947..966, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 949..953, + node_index: AtomicNodeIndex(..), value: "bar ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 953..958, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 954..957, id: Name("baz"), ctx: Load, @@ -2655,8 +2980,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 958..965, + node_index: AtomicNodeIndex(..), value: " really", }, ), @@ -2671,6 +2997,7 @@ Module( Literal( StringLiteral { range: 967..973, + node_index: AtomicNodeIndex(..), value: "bar", flags: StringLiteralFlags { quote_style: Double, @@ -2682,6 +3009,7 @@ Module( Literal( StringLiteral { range: 974..978, + node_index: AtomicNodeIndex(..), value: "no", flags: StringLiteralFlags { quote_style: Double, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__generator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__generator.py.snap index d93aef523add5..80e25b5146c21 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__generator.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/generator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..482, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..22, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 0..22, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -26,8 +29,10 @@ Module( generators: [ Comprehension { range: 3..21, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..13, id: Name("target"), ctx: Store, @@ -35,6 +40,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..21, id: Name("iter"), ctx: Load, @@ -51,12 +57,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..51, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 23..51, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Load, @@ -65,8 +74,10 @@ Module( generators: [ Comprehension { range: 26..50, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..42, id: Name("target"), ctx: Store, @@ -74,6 +85,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..50, id: Name("iter"), ctx: Load, @@ -90,12 +102,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 52..100, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 52..100, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("x"), ctx: Load, @@ -104,8 +119,10 @@ Module( generators: [ Comprehension { range: 55..99, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..65, id: Name("target"), ctx: Store, @@ -113,6 +130,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..73, id: Name("iter"), ctx: Load, @@ -121,9 +139,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 77..83, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("x"), ctx: Load, @@ -135,6 +155,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("y"), ctx: Load, @@ -145,11 +166,13 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 87..94, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..88, id: Name("a"), ctx: Load, @@ -157,6 +180,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("b"), ctx: Load, @@ -167,6 +191,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("c"), ctx: Load, @@ -183,12 +208,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 101..166, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 101..166, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..103, id: Name("x"), ctx: Load, @@ -197,8 +225,10 @@ Module( generators: [ Comprehension { range: 104..135, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..115, id: Name("target1"), ctx: Store, @@ -206,6 +236,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..124, id: Name("iter1"), ctx: Load, @@ -214,11 +245,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 128..135, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("x"), ctx: Load, @@ -226,6 +259,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..135, id: Name("y"), ctx: Load, @@ -239,8 +273,10 @@ Module( }, Comprehension { range: 136..165, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..147, id: Name("target2"), ctx: Store, @@ -248,6 +284,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..156, id: Name("iter2"), ctx: Load, @@ -256,9 +293,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 160..165, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..161, id: Name("a"), ctx: Load, @@ -270,6 +309,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 164..165, id: Name("b"), ctx: Load, @@ -289,12 +329,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 167..238, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 167..238, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 168..169, id: Name("x"), ctx: Load, @@ -303,8 +346,10 @@ Module( generators: [ Comprehension { range: 170..201, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 174..181, id: Name("target1"), ctx: Store, @@ -312,6 +357,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..190, id: Name("iter1"), ctx: Load, @@ -320,11 +366,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 194..201, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 194..195, id: Name("x"), ctx: Load, @@ -332,6 +380,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 200..201, id: Name("y"), ctx: Load, @@ -345,8 +394,10 @@ Module( }, Comprehension { range: 202..237, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 212..219, id: Name("target2"), ctx: Store, @@ -354,6 +405,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 223..228, id: Name("iter2"), ctx: Load, @@ -362,9 +414,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 232..237, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 232..233, id: Name("a"), ctx: Load, @@ -376,6 +430,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("b"), ctx: Load, @@ -395,15 +450,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 259..282, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 259..282, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 260..270, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 260..261, id: Name("x"), ctx: Store, @@ -411,9 +470,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 265..270, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 265..266, id: Name("y"), ctx: Load, @@ -422,6 +483,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 269..270, value: Int( 1, @@ -435,8 +497,10 @@ Module( generators: [ Comprehension { range: 271..281, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 275..276, id: Name("y"), ctx: Store, @@ -444,6 +508,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 280..281, id: Name("z"), ctx: Load, @@ -460,15 +525,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 300..326, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 300..326, elt: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 301..314, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 306..307, id: Name("y"), ctx: Load, @@ -476,6 +545,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 301..302, id: Name("x"), ctx: Load, @@ -483,6 +553,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 313..314, id: Name("y"), ctx: Load, @@ -493,8 +564,10 @@ Module( generators: [ Comprehension { range: 315..325, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 319..320, id: Name("y"), ctx: Store, @@ -502,6 +575,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 324..325, id: Name("z"), ctx: Load, @@ -518,20 +592,25 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 340..481, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 340..481, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 340..348, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 340..343, value: StringLiteralValue { inner: Single( StringLiteral { range: 340..343, + node_index: AtomicNodeIndex(..), value: " ", flags: StringLiteralFlags { quote_style: Double, @@ -546,18 +625,22 @@ Module( attr: Identifier { id: Name("join"), range: 344..348, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 348..481, + node_index: AtomicNodeIndex(..), args: [ Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 354..479, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 354..357, id: Name("sql"), ctx: Load, @@ -566,8 +649,10 @@ Module( generators: [ Comprehension { range: 362..479, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 366..369, id: Name("sql"), ctx: Store, @@ -575,13 +660,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 373..479, elts: [ If( ExprIf { + node_index: AtomicNodeIndex(..), range: 383..420, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 405..410, id: Name("limit"), ctx: Load, @@ -589,14 +677,17 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 383..401, left: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 383..393, value: StringLiteralValue { inner: Single( StringLiteral { range: 383..393, + node_index: AtomicNodeIndex(..), value: "LIMIT %d", flags: StringLiteralFlags { quote_style: Double, @@ -611,6 +702,7 @@ Module( op: Mod, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 396..401, id: Name("limit"), ctx: Load, @@ -620,6 +712,7 @@ Module( ), orelse: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 416..420, }, ), @@ -627,9 +720,11 @@ Module( ), If( ExprIf { + node_index: AtomicNodeIndex(..), range: 430..472, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 456..462, id: Name("offset"), ctx: Load, @@ -637,14 +732,17 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 431..451, left: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 431..442, value: StringLiteralValue { inner: Single( StringLiteral { range: 431..442, + node_index: AtomicNodeIndex(..), value: "OFFSET %d", flags: StringLiteralFlags { quote_style: Double, @@ -659,6 +757,7 @@ Module( op: Mod, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 445..451, id: Name("offset"), ctx: Load, @@ -668,6 +767,7 @@ Module( ), orelse: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 468..472, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__if.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__if.py.snap index 5cca4b3a1bac3..f21914594fbae 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__if.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__if.py.snap @@ -1,29 +1,33 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/if.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..423, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..16, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 0..16, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 5..9, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("a"), ctx: Load, @@ -31,6 +35,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..16, id: Name("b"), ctx: Load, @@ -42,12 +47,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..35, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 17..35, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Load, @@ -55,9 +63,11 @@ Module( ), body: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 17..20, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("f"), ctx: Load, @@ -65,6 +75,7 @@ Module( ), arguments: Arguments { range: 18..20, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -72,6 +83,7 @@ Module( ), orelse: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 31..35, }, ), @@ -81,12 +93,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..61, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 36..61, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("b"), ctx: Load, @@ -94,6 +109,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("a"), ctx: Load, @@ -101,9 +117,11 @@ Module( ), orelse: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 48..61, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("d"), ctx: Load, @@ -111,6 +129,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..49, id: Name("c"), ctx: Load, @@ -118,6 +137,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("e"), ctx: Load, @@ -131,15 +151,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 62..84, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 62..84, test: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 71..76, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 71..72, value: Int( 1, @@ -152,6 +176,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 0, @@ -163,9 +188,11 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 62..67, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 62..63, value: Int( 1, @@ -175,6 +202,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("x"), ctx: Load, @@ -184,10 +212,12 @@ Module( ), orelse: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 82..84, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 1, @@ -202,12 +232,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..108, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 85..108, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("x"), ctx: Load, @@ -215,11 +248,13 @@ Module( ), body: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 85..92, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..86, id: Name("a"), ctx: Load, @@ -227,6 +262,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("b"), ctx: Load, @@ -237,6 +273,7 @@ Module( ), orelse: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 103..108, value: false, }, @@ -247,12 +284,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..127, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 109..127, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..120, id: Name("y"), ctx: Load, @@ -260,9 +300,11 @@ Module( ), body: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 109..115, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..110, id: Name("x"), ctx: Load, @@ -274,6 +316,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..115, id: Name("y"), ctx: Load, @@ -284,6 +327,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..127, id: Name("x"), ctx: Load, @@ -295,17 +339,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 128..154, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 128..154, test: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 136..143, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("a"), ctx: Load, @@ -313,6 +361,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..143, id: Name("b"), ctx: Load, @@ -323,12 +372,14 @@ Module( ), body: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 128..132, value: true, }, ), orelse: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 149..154, value: false, }, @@ -339,13 +390,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 155..171, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 155..171, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 155..156, value: Int( 1, @@ -354,9 +408,11 @@ Module( ), If( ExprIf { + node_index: AtomicNodeIndex(..), range: 158..171, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 163..164, id: Name("a"), ctx: Load, @@ -364,6 +420,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 158..159, value: Int( 1, @@ -372,6 +429,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("c"), ctx: Load, @@ -388,18 +446,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 214..240, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 214..240, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 219..223, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("x"), ctx: Load, @@ -407,19 +469,26 @@ Module( ), orelse: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 229..240, parameters: Some( Parameters { range: 236..237, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 236..237, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 236..237, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 236..237, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -433,6 +502,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 239..240, id: Name("y"), ctx: Load, @@ -446,16 +516,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 302..323, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 302..323, test: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 308..315, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 314..315, id: Name("x"), ctx: Load, @@ -466,6 +540,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 302..303, id: Name("x"), ctx: Load, @@ -473,6 +548,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 322..323, id: Name("y"), ctx: Load, @@ -484,15 +560,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 324..350, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 324..350, test: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 330..342, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 341..342, id: Name("x"), ctx: Load, @@ -502,6 +582,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 324..325, id: Name("x"), ctx: Load, @@ -509,6 +590,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 349..350, id: Name("y"), ctx: Load, @@ -520,25 +602,34 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 351..376, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 351..376, test: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 357..368, parameters: Some( Parameters { range: 364..365, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 364..365, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 364..365, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 364..365, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -552,6 +643,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 367..368, id: Name("x"), ctx: Load, @@ -561,6 +653,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 351..352, id: Name("x"), ctx: Load, @@ -568,6 +661,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 375..376, id: Name("y"), ctx: Load, @@ -579,12 +673,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 408..423, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 409..422, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 414..415, id: Name("y"), ctx: Load, @@ -592,6 +689,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 409..410, id: Name("x"), ctx: Load, @@ -599,6 +697,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 421..422, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__lambda.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__lambda.py.snap index a21cc82b8a316..9e614f6c7e3a6 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__lambda.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__lambda.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/lambda.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..530, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..9, parameters: None, body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("a"), ctx: Load, @@ -30,13 +33,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 10..19, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 10..19, parameters: None, body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 1, @@ -49,22 +55,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..31, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 20..31, parameters: Some( Parameters { range: 27..28, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 27..28, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 27..28, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 27..28, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -78,6 +92,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 30..31, value: Int( 1, @@ -90,22 +105,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..48, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 32..48, parameters: Some( Parameters { range: 39..43, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 39..40, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 39..40, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 39..40, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -113,11 +136,14 @@ Module( }, ParameterWithDefault { range: 42..43, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 42..43, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 42..43, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -131,6 +157,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 45..48, }, ), @@ -140,22 +167,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..66, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 49..66, parameters: Some( Parameters { range: 56..63, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 56..57, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 56..57, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 56..57, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -163,11 +198,14 @@ Module( }, ParameterWithDefault { range: 59..60, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 59..60, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 59..60, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -175,11 +213,14 @@ Module( }, ParameterWithDefault { range: 62..63, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 62..63, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 62..63, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -193,6 +234,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 1, @@ -205,22 +247,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..90, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 67..90, parameters: Some( Parameters { range: 74..87, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 74..75, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 74..75, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 74..75, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -228,17 +278,21 @@ Module( }, ParameterWithDefault { range: 77..81, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 77..78, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 77..78, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..81, value: Int( 20, @@ -249,17 +303,21 @@ Module( }, ParameterWithDefault { range: 83..87, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 83..84, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 83..84, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 85..87, value: Int( 30, @@ -276,6 +334,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 1, @@ -288,22 +347,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 91..109, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 91..109, parameters: Some( Parameters { range: 98..102, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 98..99, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 98..99, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 98..99, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -311,11 +378,14 @@ Module( }, ParameterWithDefault { range: 101..102, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 101..102, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 101..102, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -329,9 +399,11 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 104..109, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("x"), ctx: Load, @@ -340,6 +412,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("y"), ctx: Load, @@ -353,22 +426,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 110..130, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 110..130, parameters: Some( Parameters { range: 117..123, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 117..118, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 117..118, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 117..118, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -376,17 +457,21 @@ Module( }, ParameterWithDefault { range: 120..123, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 120..121, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("z"), range: 120..121, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 122..123, value: Int( 1, @@ -403,9 +488,11 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 125..130, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 125..126, id: Name("z"), ctx: Load, @@ -414,6 +501,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("y"), ctx: Load, @@ -427,21 +515,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 131..143, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 131..143, parameters: Some( Parameters { range: 138..140, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 138..140, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 139..140, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -452,6 +547,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..143, id: Name("a"), ctx: Load, @@ -463,21 +559,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 144..166, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 144..166, parameters: Some( Parameters { range: 151..161, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 151..153, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 152..153, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -485,11 +588,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 155..156, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 155..156, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("z"), range: 155..156, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -497,17 +603,21 @@ Module( }, ParameterWithDefault { range: 158..161, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 158..159, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 158..159, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..161, value: Int( 0, @@ -522,6 +632,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 163..166, }, ), @@ -531,24 +642,32 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 167..187, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 167..187, parameters: Some( Parameters { range: 174..184, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 177..178, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 177..178, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 177..178, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -556,11 +675,14 @@ Module( }, ParameterWithDefault { range: 180..181, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 180..181, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 180..181, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -568,11 +690,14 @@ Module( }, ParameterWithDefault { range: 183..184, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 183..184, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 183..184, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -584,6 +709,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 186..187, value: Int( 1, @@ -596,24 +722,32 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 188..214, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 188..214, parameters: Some( Parameters { range: 195..211, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 198..199, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 198..199, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 198..199, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -621,17 +755,21 @@ Module( }, ParameterWithDefault { range: 201..205, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 201..202, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 201..202, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 203..205, value: Int( 20, @@ -642,17 +780,21 @@ Module( }, ParameterWithDefault { range: 207..211, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 207..208, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 207..208, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 209..211, value: Int( 30, @@ -667,6 +809,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..214, value: Int( 1, @@ -679,22 +822,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 215..241, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 215..241, parameters: Some( Parameters { range: 222..238, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 222..223, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 222..223, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 222..223, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -702,11 +853,14 @@ Module( }, ParameterWithDefault { range: 225..226, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 225..226, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 225..226, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -714,11 +868,14 @@ Module( }, ParameterWithDefault { range: 228..229, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 228..229, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 228..229, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -729,11 +886,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 234..235, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 234..235, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 234..235, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -741,11 +901,14 @@ Module( }, ParameterWithDefault { range: 237..238, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 237..238, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 237..238, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -757,6 +920,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 240..241, value: Int( 0, @@ -769,13 +933,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 242..262, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 242..262, parameters: Some( Parameters { range: 249..257, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -783,9 +952,11 @@ Module( kwarg: Some( Parameter { range: 249..257, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 251..257, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -794,9 +965,11 @@ Module( ), body: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 259..262, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..260, id: Name("f"), ctx: Load, @@ -804,6 +977,7 @@ Module( ), arguments: Arguments { range: 260..262, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -815,21 +989,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 263..294, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 263..294, parameters: Some( Parameters { range: 270..285, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 270..275, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 271..275, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -838,9 +1019,11 @@ Module( kwarg: Some( Parameter { range: 277..285, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 279..285, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -849,12 +1032,15 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 287..294, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 287..290, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 287..288, id: Name("f"), ctx: Load, @@ -862,6 +1048,7 @@ Module( ), arguments: Arguments { range: 288..290, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -870,6 +1057,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 293..294, value: Int( 1, @@ -884,21 +1072,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 295..334, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 295..334, parameters: Some( Parameters { range: 302..325, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 302..307, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 303..307, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -906,11 +1101,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 309..310, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 309..310, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 309..310, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -918,17 +1116,21 @@ Module( }, ParameterWithDefault { range: 312..315, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 312..313, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 312..313, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 314..315, value: Int( 1, @@ -941,9 +1143,11 @@ Module( kwarg: Some( Parameter { range: 317..325, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 319..325, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -952,12 +1156,15 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 327..334, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 327..330, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..328, id: Name("f"), ctx: Load, @@ -965,6 +1172,7 @@ Module( ), arguments: Arguments { range: 328..330, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -973,6 +1181,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 333..334, value: Int( 1, @@ -987,21 +1196,29 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 335..351, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 335..351, parameters: Some( Parameters { range: 342..346, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 342..343, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 342..343, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 342..343, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1016,6 +1233,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 348..351, }, ), @@ -1025,21 +1243,29 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 352..371, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 352..371, parameters: Some( Parameters { range: 359..366, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 359..360, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 359..360, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 359..360, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1049,11 +1275,14 @@ Module( args: [ ParameterWithDefault { range: 365..366, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 365..366, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 365..366, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1067,6 +1296,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 368..371, }, ), @@ -1076,27 +1306,36 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 372..391, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 372..391, parameters: Some( Parameters { range: 379..386, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 379..382, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 379..380, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 379..380, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 381..382, value: Int( 1, @@ -1114,6 +1353,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 388..391, }, ), @@ -1123,21 +1363,29 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 392..417, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 392..417, parameters: Some( Parameters { range: 399..412, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 399..400, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 399..400, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 399..400, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1145,11 +1393,14 @@ Module( }, ParameterWithDefault { range: 402..403, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 402..403, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 402..403, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1161,11 +1412,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 411..412, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 411..412, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 411..412, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1177,6 +1431,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 414..417, }, ), @@ -1186,28 +1441,37 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 418..440, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 418..440, parameters: Some( Parameters { range: 425..435, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 425..429, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 425..427, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kw"), range: 425..427, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 428..429, value: Int( 1, @@ -1221,11 +1485,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 434..435, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 434..435, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 434..435, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1237,6 +1504,7 @@ Module( ), body: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 437..440, }, ), @@ -1246,21 +1514,29 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 441..467, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 441..467, parameters: Some( Parameters { range: 448..464, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 448..449, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 448..449, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 448..449, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1268,17 +1544,21 @@ Module( }, ParameterWithDefault { range: 451..455, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 451..452, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 451..452, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 453..455, value: Int( 20, @@ -1291,17 +1571,21 @@ Module( args: [ ParameterWithDefault { range: 460..464, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 460..461, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 460..461, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 462..464, value: Int( 30, @@ -1318,6 +1602,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 466..467, value: Int( 1, @@ -1330,21 +1615,29 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 468..497, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 468..497, parameters: Some( Parameters { range: 475..494, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 475..476, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 475..476, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 475..476, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1352,11 +1645,14 @@ Module( }, ParameterWithDefault { range: 478..479, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 478..479, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 478..479, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1366,11 +1662,14 @@ Module( args: [ ParameterWithDefault { range: 484..485, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 484..485, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 484..485, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1381,11 +1680,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 490..491, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 490..491, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 490..491, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1393,11 +1695,14 @@ Module( }, ParameterWithDefault { range: 493..494, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 493..494, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 493..494, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1409,6 +1714,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 496..497, value: Int( 0, @@ -1421,21 +1727,29 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 498..530, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 498..530, parameters: Some( Parameters { range: 505..527, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 505..506, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 505..506, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 505..506, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1443,11 +1757,14 @@ Module( }, ParameterWithDefault { range: 508..509, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 508..509, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 508..509, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1457,11 +1774,14 @@ Module( args: [ ParameterWithDefault { range: 514..515, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 514..515, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 514..515, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1471,9 +1791,11 @@ Module( vararg: Some( Parameter { range: 517..519, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 518..519, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1481,11 +1803,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 521..522, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 521..522, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 521..522, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1495,9 +1820,11 @@ Module( kwarg: Some( Parameter { range: 524..527, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 526..527, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1506,6 +1833,7 @@ Module( ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 529..530, value: Int( 0, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list.py.snap index ae3fc6d22cbfd..dec8d0d279234 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/list.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..384, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..17, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 15..17, elts: [], ctx: Load, @@ -24,13 +26,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..21, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 18..21, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 19..20, value: Int( 1, @@ -45,13 +50,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..26, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 22..26, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 23..24, value: Int( 1, @@ -66,13 +74,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..36, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 27..36, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..29, value: Int( 1, @@ -81,6 +92,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 31..32, value: Int( 2, @@ -89,6 +101,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 34..35, value: Int( 3, @@ -103,13 +116,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 37..47, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 37..47, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 38..39, value: Int( 1, @@ -118,6 +134,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 41..42, value: Int( 2, @@ -126,6 +143,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..45, value: Int( 3, @@ -140,9 +158,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 75..78, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 75..78, elts: [], ctx: Load, @@ -152,13 +172,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 79..92, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 79..92, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 1, @@ -173,13 +196,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..114, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 93..114, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 99..100, value: Int( 1, @@ -188,6 +214,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 110..111, value: Int( 2, @@ -202,21 +229,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 125..132, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 125..132, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 126..131, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 127..130, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 128..129, value: Int( 1, @@ -239,17 +271,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..149, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 133..149, elts: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 134..140, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 135..136, value: Int( 1, @@ -258,6 +294,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 138..139, value: Int( 2, @@ -270,10 +307,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 142..148, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 143..144, value: Int( 3, @@ -282,6 +321,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 146..147, value: Int( 4, @@ -300,16 +340,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 170..178, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 170..178, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 171..177, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("x"), ctx: Store, @@ -317,6 +361,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 176..177, value: Int( 2, @@ -333,16 +378,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 179..188, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 179..188, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 180..186, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 180..181, id: Name("x"), ctx: Store, @@ -350,6 +399,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 185..186, value: Int( 2, @@ -366,13 +416,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 189..203, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 189..203, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 190..191, value: Int( 1, @@ -381,9 +434,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 193..199, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..194, id: Name("x"), ctx: Store, @@ -391,6 +446,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 198..199, value: Int( 2, @@ -401,6 +457,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 3, @@ -415,13 +472,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 223..233, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 223..233, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 224..225, value: Int( 1, @@ -430,9 +490,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 227..229, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 228..229, id: Name("x"), ctx: Load, @@ -443,6 +505,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 231..232, value: Int( 3, @@ -457,13 +520,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 234..248, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 234..248, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 235..236, value: Int( 1, @@ -472,12 +538,15 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 238..244, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 239..244, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 239..240, id: Name("x"), ctx: Load, @@ -486,6 +555,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 243..244, id: Name("y"), ctx: Load, @@ -498,6 +568,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 246..247, value: Int( 3, @@ -512,16 +583,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 271..334, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 271..334, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 272..277, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 272..273, value: Int( 1, @@ -531,6 +606,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 276..277, value: Int( 2, @@ -541,10 +617,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 279..291, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 280..281, value: Int( 1, @@ -553,6 +631,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 283..284, value: Int( 2, @@ -561,6 +640,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 286..287, value: Int( 3, @@ -569,6 +649,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 289..290, value: Int( 4, @@ -581,10 +662,12 @@ Module( ), Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 293..306, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 294..295, id: Name("a"), ctx: Load, @@ -592,9 +675,11 @@ Module( ), BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 297..302, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 297..298, id: Name("b"), ctx: Load, @@ -603,6 +688,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 301..302, id: Name("c"), ctx: Load, @@ -612,6 +698,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 304..305, id: Name("d"), ctx: Load, @@ -624,10 +711,12 @@ Module( ), Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 308..317, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 309..310, id: Name("a"), ctx: Load, @@ -635,6 +724,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 312..313, id: Name("b"), ctx: Load, @@ -642,6 +732,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 315..316, id: Name("c"), ctx: Load, @@ -652,12 +743,14 @@ Module( ), Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 319..325, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 320..321, id: Name("a"), ctx: Load, @@ -666,6 +759,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 323..324, value: Int( 1, @@ -678,9 +772,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 327..333, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..328, id: Name("x"), ctx: Store, @@ -688,6 +784,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 332..333, value: Int( 2, @@ -704,16 +801,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 335..383, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 335..383, elts: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 336..382, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 336..341, id: Name("call1"), ctx: Load, @@ -721,15 +822,19 @@ Module( ), arguments: Arguments { range: 341..382, + node_index: AtomicNodeIndex(..), args: [ Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 342..381, elt: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 342..361, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 342..347, id: Name("call2"), ctx: Load, @@ -737,15 +842,19 @@ Module( ), arguments: Arguments { range: 347..361, + node_index: AtomicNodeIndex(..), args: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 348..360, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 348..358, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 348..353, id: Name("value"), ctx: Load, @@ -754,12 +863,14 @@ Module( attr: Identifier { id: Name("attr"), range: 354..358, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 358..360, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -773,8 +884,10 @@ Module( generators: [ Comprehension { range: 362..381, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 366..373, id: Name("element"), ctx: Store, @@ -782,6 +895,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 377..381, id: Name("iter"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list_comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list_comprehension.py.snap index b81ae4169e2b1..de7a7eb783a04 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list_comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__list_comprehension.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/list_comprehension.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..776, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..26, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -24,9 +26,11 @@ Module( ], value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 4..26, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("y"), ctx: Load, @@ -35,8 +39,10 @@ Module( generators: [ Comprehension { range: 7..25, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("y"), ctx: Store, @@ -44,10 +50,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 16..25, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 17..18, value: Int( 1, @@ -56,6 +64,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 20..21, value: Int( 2, @@ -64,6 +73,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 23..24, value: Int( 3, @@ -85,12 +95,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..49, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 28..49, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Load, @@ -99,8 +112,10 @@ Module( generators: [ Comprehension { range: 31..48, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("i"), ctx: Store, @@ -108,9 +123,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 40..48, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..45, id: Name("range"), ctx: Load, @@ -118,9 +135,11 @@ Module( ), arguments: Arguments { range: 45..48, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 46..47, value: Int( 5, @@ -142,12 +161,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 50..91, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 50..91, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("b"), ctx: Load, @@ -156,8 +178,10 @@ Module( generators: [ Comprehension { range: 53..90, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("c"), ctx: Store, @@ -165,6 +189,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("d"), ctx: Load, @@ -173,9 +198,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 67..73, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("x"), ctx: Load, @@ -187,6 +214,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("w"), ctx: Load, @@ -197,11 +225,13 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 77..85, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("y"), ctx: Load, @@ -209,6 +239,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..85, id: Name("yy"), ctx: Load, @@ -219,6 +250,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("z"), ctx: Load, @@ -234,12 +266,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..137, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 92..137, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("a"), ctx: Load, @@ -248,8 +283,10 @@ Module( generators: [ Comprehension { range: 95..116, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("b"), ctx: Store, @@ -257,6 +294,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("c"), ctx: Load, @@ -265,11 +303,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 109..116, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..110, id: Name("d"), ctx: Load, @@ -277,6 +317,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 115..116, id: Name("e"), ctx: Load, @@ -290,8 +331,10 @@ Module( }, Comprehension { range: 117..136, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..122, id: Name("f"), ctx: Store, @@ -299,6 +342,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..127, id: Name("j"), ctx: Load, @@ -307,9 +351,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 131..136, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..132, id: Name("k"), ctx: Load, @@ -321,6 +367,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..136, id: Name("h"), ctx: Load, @@ -339,12 +386,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 138..189, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 138..189, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("a"), ctx: Load, @@ -353,8 +403,10 @@ Module( generators: [ Comprehension { range: 141..162, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..146, id: Name("b"), ctx: Store, @@ -362,6 +414,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("c"), ctx: Load, @@ -370,11 +423,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 155..162, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 155..156, id: Name("d"), ctx: Load, @@ -382,6 +437,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 161..162, id: Name("e"), ctx: Load, @@ -395,8 +451,10 @@ Module( }, Comprehension { range: 163..188, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 173..174, id: Name("f"), ctx: Store, @@ -404,6 +462,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("j"), ctx: Load, @@ -412,9 +471,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 183..188, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 183..184, id: Name("k"), ctx: Load, @@ -426,6 +487,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 187..188, id: Name("h"), ctx: Load, @@ -444,12 +506,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 190..209, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 190..209, elt: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 191..192, value: Int( 1, @@ -459,8 +524,10 @@ Module( generators: [ Comprehension { range: 193..208, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 197..198, id: Name("i"), ctx: Store, @@ -468,9 +535,11 @@ Module( ), iter: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 202..208, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 202..203, id: Name("x"), ctx: Load, @@ -482,6 +551,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 207..208, id: Name("a"), ctx: Load, @@ -500,12 +570,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 210..227, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 210..227, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 211..212, id: Name("a"), ctx: Load, @@ -514,12 +587,15 @@ Module( generators: [ Comprehension { range: 213..226, + node_index: AtomicNodeIndex(..), target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 217..221, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 217..218, id: Name("a"), ctx: Store, @@ -527,6 +603,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 220..221, id: Name("b"), ctx: Store, @@ -539,6 +616,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 225..226, id: Name("G"), ctx: Load, @@ -554,15 +632,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 228..257, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 228..257, elt: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 234..241, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 240..241, id: Name("x"), ctx: Load, @@ -573,12 +655,15 @@ Module( generators: [ Comprehension { range: 242..255, + node_index: AtomicNodeIndex(..), target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 246..250, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 246..247, id: Name("a"), ctx: Store, @@ -586,6 +671,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 249..250, id: Name("b"), ctx: Store, @@ -598,6 +684,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 254..255, id: Name("C"), ctx: Load, @@ -613,12 +700,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 258..300, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 258..300, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..260, id: Name("i"), ctx: Load, @@ -627,8 +717,10 @@ Module( generators: [ Comprehension { range: 261..299, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 265..266, id: Name("i"), ctx: Store, @@ -636,9 +728,11 @@ Module( ), iter: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 270..277, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 276..277, id: Name("x"), ctx: Load, @@ -649,9 +743,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 281..299, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 281..287, id: Name("entity"), ctx: Load, @@ -663,6 +759,7 @@ Module( comparators: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 295..299, }, ), @@ -679,12 +776,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 301..337, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 301..337, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 302..303, id: Name("x"), ctx: Load, @@ -693,8 +793,10 @@ Module( generators: [ Comprehension { range: 304..336, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 308..309, id: Name("x"), ctx: Store, @@ -702,15 +804,18 @@ Module( ), iter: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 314..330, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 319..323, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 314..315, id: Name("l"), ctx: Load, @@ -718,6 +823,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 329..330, id: Name("L"), ctx: Load, @@ -728,6 +834,7 @@ Module( ifs: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 335..336, id: Name("T"), ctx: Load, @@ -743,12 +850,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 338..380, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 338..380, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 339..340, id: Name("i"), ctx: Load, @@ -757,8 +867,10 @@ Module( generators: [ Comprehension { range: 341..379, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 345..346, id: Name("i"), ctx: Store, @@ -766,18 +878,22 @@ Module( ), iter: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 351..373, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 362..366, value: true, }, ), body: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 351..358, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 357..358, id: Name("x"), ctx: Load, @@ -787,6 +903,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 372..373, id: Name("X"), ctx: Load, @@ -797,6 +914,7 @@ Module( ifs: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 378..379, id: Name("F"), ctx: Load, @@ -812,12 +930,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 381..423, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 381..423, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 382..383, id: Name("i"), ctx: Load, @@ -826,8 +947,10 @@ Module( generators: [ Comprehension { range: 384..422, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 388..389, id: Name("i"), ctx: Store, @@ -835,18 +958,22 @@ Module( ), iter: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 393..417, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 400..416, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 405..409, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 400..401, id: Name("x"), ctx: Load, @@ -854,6 +981,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 415..416, id: Name("X"), ctx: Load, @@ -866,6 +994,7 @@ Module( ifs: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 421..422, id: Name("F"), ctx: Load, @@ -881,12 +1010,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 424..457, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 424..457, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 425..426, id: Name("f"), ctx: Load, @@ -895,8 +1027,10 @@ Module( generators: [ Comprehension { range: 427..456, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 431..432, id: Name("f"), ctx: Store, @@ -904,9 +1038,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 436..456, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 436..437, id: Name("c"), ctx: Load, @@ -914,18 +1050,22 @@ Module( ), arguments: Arguments { range: 437..456, + node_index: AtomicNodeIndex(..), args: [ If( ExprIf { + node_index: AtomicNodeIndex(..), range: 438..455, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 443..447, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 438..439, id: Name("x"), ctx: Load, @@ -933,6 +1073,7 @@ Module( ), orelse: List( ExprList { + node_index: AtomicNodeIndex(..), range: 453..455, elts: [], ctx: Load, @@ -955,12 +1096,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 596..618, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 596..618, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 597..598, id: Name("x"), ctx: Load, @@ -969,8 +1113,10 @@ Module( generators: [ Comprehension { range: 599..617, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 603..604, id: Name("x"), ctx: Store, @@ -978,10 +1124,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 609..616, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 615..616, id: Name("y"), ctx: Load, @@ -1000,12 +1148,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 619..646, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 619..646, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 620..621, id: Name("x"), ctx: Load, @@ -1014,8 +1165,10 @@ Module( generators: [ Comprehension { range: 622..645, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 626..627, id: Name("x"), ctx: Store, @@ -1023,9 +1176,11 @@ Module( ), iter: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 632..644, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 643..644, id: Name("y"), ctx: Load, @@ -1043,12 +1198,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 647..673, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 647..673, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 648..649, id: Name("x"), ctx: Load, @@ -1057,8 +1215,10 @@ Module( generators: [ Comprehension { range: 650..672, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 654..655, id: Name("x"), ctx: Store, @@ -1066,19 +1226,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 660..671, parameters: Some( Parameters { range: 667..668, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 667..668, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 667..668, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 667..668, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1092,6 +1259,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 670..671, id: Name("y"), ctx: Load, @@ -1109,12 +1277,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 674..704, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 674..704, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 675..676, id: Name("x"), ctx: Load, @@ -1123,8 +1294,10 @@ Module( generators: [ Comprehension { range: 677..703, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 681..682, id: Name("x"), ctx: Store, @@ -1132,6 +1305,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 686..690, id: Name("data"), ctx: Load, @@ -1140,10 +1314,12 @@ Module( ifs: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 695..702, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 701..702, id: Name("y"), ctx: Load, @@ -1162,12 +1338,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 705..740, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 705..740, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 706..707, id: Name("x"), ctx: Load, @@ -1176,8 +1355,10 @@ Module( generators: [ Comprehension { range: 708..739, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 712..713, id: Name("x"), ctx: Store, @@ -1185,6 +1366,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 717..721, id: Name("data"), ctx: Load, @@ -1193,9 +1375,11 @@ Module( ifs: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 726..738, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 737..738, id: Name("y"), ctx: Load, @@ -1213,12 +1397,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 741..775, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 741..775, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 742..743, id: Name("x"), ctx: Load, @@ -1227,8 +1414,10 @@ Module( generators: [ Comprehension { range: 744..774, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 748..749, id: Name("x"), ctx: Store, @@ -1236,6 +1425,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 753..757, id: Name("data"), ctx: Load, @@ -1244,19 +1434,26 @@ Module( ifs: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 762..773, parameters: Some( Parameters { range: 769..770, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 769..770, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 769..770, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 769..770, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1270,6 +1467,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 772..773, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__name.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__name.py.snap index dd9d18e848b94..506ee527223dc 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__name.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/name.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..76, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..1, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("_"), ctx: Load, @@ -24,9 +26,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2..5, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..4, id: Name("_"), ctx: Load, @@ -36,9 +40,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 6..8, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..8, id: Name("__"), ctx: Load, @@ -48,9 +54,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..17, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..17, id: Name("__init__"), ctx: Load, @@ -60,9 +68,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..22, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..22, id: Name("name"), ctx: Load, @@ -72,9 +82,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..29, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..28, id: Name("name"), ctx: Load, @@ -84,9 +96,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 60..65, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..65, id: Name("match"), ctx: Load, @@ -96,9 +110,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..70, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..70, id: Name("case"), ctx: Load, @@ -108,9 +124,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..75, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..75, id: Name("type"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__named.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__named.py.snap index 098a337277648..1ac703b53b2e6 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__named.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__named.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/named.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..157, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1..10, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..5, id: Name("name"), ctx: Store, @@ -25,6 +28,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 0, @@ -37,12 +41,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..29, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 13..28, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..17, id: Name("name"), ctx: Store, @@ -50,9 +57,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 22..27, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -61,6 +70,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("y"), ctx: Load, @@ -74,12 +84,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..45, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 31..44, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..35, id: Name("name"), ctx: Store, @@ -87,9 +100,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 39..44, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 39..40, value: Int( 1, @@ -99,6 +114,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 43..44, value: Int( 1, @@ -113,12 +129,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 46..63, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 47..62, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..51, id: Name("name"), ctx: Store, @@ -126,13 +145,16 @@ Module( ), value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 55..62, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 56..58, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("x"), ctx: Load, @@ -143,6 +165,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("y"), ctx: Load, @@ -159,12 +182,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 64..90, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 65..89, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..69, id: Name("name"), ctx: Store, @@ -172,15 +198,18 @@ Module( ), value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 73..89, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 78..82, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("x"), ctx: Load, @@ -188,6 +217,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..89, id: Name("y"), ctx: Load, @@ -201,12 +231,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 91..112, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 92..111, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..96, id: Name("name"), ctx: Store, @@ -214,19 +247,26 @@ Module( ), value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 100..111, parameters: Some( Parameters { range: 107..108, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 107..108, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 107..108, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 107..108, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -240,6 +280,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 110..111, id: Name("x"), ctx: Load, @@ -253,12 +294,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 113..132, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 114..131, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..118, id: Name("name"), ctx: Store, @@ -266,10 +310,12 @@ Module( ), value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 123..130, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("x"), ctx: Load, @@ -284,12 +330,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..157, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 134..156, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..138, id: Name("name"), ctx: Store, @@ -297,9 +346,11 @@ Module( ), value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 143..155, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 154..155, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__number_literal.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__number_literal.py.snap index 64d1e63f40b2e..46dac564f0a85 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__number_literal.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__number_literal.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/number_literal.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..700, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..13, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -24,6 +26,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4..13, value: Int( 123456789, @@ -34,10 +37,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 14..24, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("x"), ctx: Store, @@ -46,6 +51,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..24, value: Int( 123456, @@ -56,10 +62,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 25..31, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("x"), ctx: Store, @@ -68,6 +76,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 29..31, value: Float( 0.1, @@ -78,10 +87,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 32..38, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("x"), ctx: Store, @@ -90,6 +101,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 36..38, value: Float( 1.0, @@ -100,10 +112,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 39..47, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("x"), ctx: Store, @@ -112,6 +126,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 43..47, value: Float( 10.0, @@ -122,10 +137,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 48..56, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..49, id: Name("x"), ctx: Store, @@ -134,6 +151,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..56, value: Float( 0.1, @@ -144,10 +162,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 57..73, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("x"), ctx: Store, @@ -156,6 +176,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..73, value: Float( 1.00000001, @@ -166,10 +187,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 74..97, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("x"), ctx: Store, @@ -178,6 +201,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..97, value: Float( 123456789.12345679, @@ -188,10 +212,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 98..131, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("x"), ctx: Store, @@ -200,6 +226,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 102..131, value: Float( inf, @@ -210,10 +237,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 132..155, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("x"), ctx: Store, @@ -222,6 +251,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 136..155, value: Float( inf, @@ -232,10 +262,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 156..170, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("x"), ctx: Store, @@ -244,6 +276,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..170, value: Complex { real: 0.0, @@ -255,10 +288,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 171..195, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("x"), ctx: Store, @@ -267,6 +302,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 175..195, value: Complex { real: 0.0, @@ -278,10 +314,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 196..207, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 196..197, id: Name("x"), ctx: Store, @@ -290,6 +328,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 200..207, value: Int( 727756, @@ -300,10 +339,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 208..218, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 208..209, id: Name("x"), ctx: Store, @@ -312,6 +353,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 212..218, value: Int( 11, @@ -322,10 +364,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 219..228, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 219..220, id: Name("x"), ctx: Store, @@ -334,6 +378,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 223..228, value: Int( 511, @@ -344,10 +389,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 229..244, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 229..230, id: Name("x"), ctx: Store, @@ -356,6 +403,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 233..244, value: Float( 6e-9, @@ -366,10 +414,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 245..254, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("x"), ctx: Store, @@ -378,6 +428,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 249..254, value: Int( 10000, @@ -388,10 +439,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 255..265, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 255..256, id: Name("x"), ctx: Store, @@ -400,6 +453,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 259..265, value: Int( 133333, @@ -410,10 +464,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 286..298, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 286..287, id: Name("x"), ctx: Store, @@ -422,9 +478,11 @@ Module( ], value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 290..298, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 290..292, value: Float( 1.0, @@ -434,6 +492,7 @@ Module( attr: Identifier { id: Name("imag"), range: 294..298, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -442,10 +501,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 299..312, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 299..300, id: Name("x"), ctx: Store, @@ -454,9 +515,11 @@ Module( ], value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 303..312, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 303..307, value: Float( 10.0, @@ -466,6 +529,7 @@ Module( attr: Identifier { id: Name("imag"), range: 308..312, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -474,10 +538,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 313..326, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 313..314, id: Name("x"), ctx: Store, @@ -486,9 +552,11 @@ Module( ], value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 317..326, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 317..321, value: Float( 0.1, @@ -498,6 +566,7 @@ Module( attr: Identifier { id: Name("real"), range: 322..326, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -506,10 +575,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 327..356, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..328, id: Name("x"), ctx: Store, @@ -518,12 +589,15 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 331..356, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 331..354, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 331..350, value: Float( 123456789.12345679, @@ -533,12 +607,14 @@ Module( attr: Identifier { id: Name("hex"), range: 351..354, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 354..356, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -548,10 +624,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 357..396, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 357..358, id: Name("x"), ctx: Store, @@ -560,9 +638,11 @@ Module( ], value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 361..396, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 361..390, value: Float( inf, @@ -572,6 +652,7 @@ Module( attr: Identifier { id: Name("real"), range: 392..396, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -580,10 +661,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 397..433, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 397..398, id: Name("x"), ctx: Store, @@ -592,12 +675,15 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 401..433, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 401..431, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 401..420, value: Float( inf, @@ -607,12 +693,14 @@ Module( attr: Identifier { id: Name("conjugate"), range: 422..431, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 431..433, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -622,10 +710,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 434..453, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 434..435, id: Name("x"), ctx: Store, @@ -634,9 +724,11 @@ Module( ], value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 438..453, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 438..448, value: Complex { real: 0.0, @@ -647,6 +739,7 @@ Module( attr: Identifier { id: Name("real"), range: 449..453, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -655,10 +748,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 454..507, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 454..455, id: Name("x"), ctx: Store, @@ -667,12 +762,15 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 458..507, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 458..486, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 458..478, value: Complex { real: 0.0, @@ -683,21 +781,26 @@ Module( attr: Identifier { id: Name("__add__"), range: 479..486, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 486..507, + node_index: AtomicNodeIndex(..), args: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 487..506, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 487..504, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 487..493, value: Int( 11, @@ -707,12 +810,14 @@ Module( attr: Identifier { id: Name("bit_length"), range: 494..504, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 504..506, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -727,10 +832,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 508..531, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 508..509, id: Name("x"), ctx: Store, @@ -739,12 +846,15 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 512..531, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 512..529, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 512..519, value: Int( 727756, @@ -754,12 +864,14 @@ Module( attr: Identifier { id: Name("conjugate"), range: 520..529, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 529..531, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -769,10 +881,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 532..555, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 532..533, id: Name("x"), ctx: Store, @@ -781,12 +895,15 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 536..555, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 536..553, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 536..542, value: Int( 11, @@ -796,12 +913,14 @@ Module( attr: Identifier { id: Name("conjugate"), range: 544..553, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 553..555, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -811,10 +930,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 556..571, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 556..557, id: Name("x"), ctx: Store, @@ -823,9 +944,11 @@ Module( ], value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 560..571, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 560..565, value: Int( 511, @@ -835,6 +958,7 @@ Module( attr: Identifier { id: Name("real"), range: 567..571, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -843,10 +967,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 572..595, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 572..573, id: Name("x"), ctx: Store, @@ -855,12 +981,15 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 576..595, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 576..593, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 576..587, value: Float( 6e-9, @@ -870,12 +999,14 @@ Module( attr: Identifier { id: Name("hex"), range: 590..593, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 593..595, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -885,10 +1016,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 596..610, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 596..597, id: Name("x"), ctx: Store, @@ -897,10 +1030,12 @@ Module( ], value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 600..610, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 601..610, value: Complex { real: 0.0, @@ -914,12 +1049,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 612..632, test: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 615..623, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 615..617, value: Int( 10, @@ -929,6 +1067,7 @@ Module( attr: Identifier { id: Name("real"), range: 619..623, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -936,9 +1075,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 629..632, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 629..632, }, ), @@ -950,10 +1091,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 677..688, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 677..678, id: Name("y"), ctx: Store, @@ -962,9 +1105,11 @@ Module( ], value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 681..688, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 681..684, value: Int( 100, @@ -973,6 +1118,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 685..687, id: Name("no"), ctx: Load, @@ -985,10 +1131,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 689..700, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 689..690, id: Name("y"), ctx: Store, @@ -997,9 +1145,11 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 693..700, func: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 693..696, value: Int( 100, @@ -1008,9 +1158,11 @@ Module( ), arguments: Arguments { range: 696..700, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 697..699, id: Name("no"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__parenthesized.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__parenthesized.py.snap index f5a6858f1d4d7..747ed8ee3dece 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__parenthesized.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__parenthesized.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/parenthesized.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..92, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..6, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..5, id: Name("expr"), ctx: Load, @@ -24,12 +26,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 7..15, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 7..15, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..12, id: Name("expr"), ctx: Load, @@ -37,6 +42,7 @@ Module( ), arguments: Arguments { range: 13..15, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -46,18 +52,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..28, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 16..28, func: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 16..26, func: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 16..24, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..21, id: Name("expr"), ctx: Load, @@ -65,6 +76,7 @@ Module( ), arguments: Arguments { range: 22..24, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -72,6 +84,7 @@ Module( ), arguments: Arguments { range: 24..26, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -79,6 +92,7 @@ Module( ), arguments: Arguments { range: 26..28, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -88,19 +102,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..44, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 31..43, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 31..38, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..32, id: Name("a"), ctx: Load, @@ -108,6 +126,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("b"), ctx: Load, @@ -118,6 +137,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 42..43, id: Name("c"), ctx: Load, @@ -130,22 +150,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 45..58, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 46..57, parameters: Some( Parameters { range: 53..54, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 53..54, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 53..54, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 53..54, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -159,6 +187,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Load, @@ -170,12 +199,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 59..67, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 60..66, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("x"), ctx: Store, @@ -183,6 +215,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 2, @@ -195,13 +228,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..77, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 69..76, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("x"), ctx: Load, @@ -214,12 +250,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 78..92, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 79..91, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set.py.snap index f8ed36c227da7..eb99b5e4d078c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/set.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..313, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..16, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 14..16, items: [], }, @@ -23,13 +25,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..20, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 17..20, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 1, @@ -43,13 +48,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..25, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 21..25, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 22..23, value: Int( 1, @@ -63,13 +71,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..35, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 26..35, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 27..28, value: Int( 1, @@ -78,6 +89,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 30..31, value: Int( 2, @@ -86,6 +98,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 33..34, value: Int( 3, @@ -99,13 +112,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..46, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 36..46, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 37..38, value: Int( 1, @@ -114,6 +130,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 40..41, value: Int( 2, @@ -122,6 +139,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 43..44, value: Int( 3, @@ -135,9 +153,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 74..77, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 74..77, items: [], }, @@ -146,13 +166,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 78..91, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 78..91, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 88..89, value: Int( 1, @@ -166,13 +189,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..113, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 92..113, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 98..99, value: Int( 1, @@ -181,6 +207,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 109..110, value: Int( 2, @@ -194,17 +221,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 124..129, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 124..129, elts: [ Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 125..128, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 126..127, value: Int( 1, @@ -221,17 +252,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 130..146, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 130..146, elts: [ Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 131..137, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 132..133, value: Int( 1, @@ -240,6 +275,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 135..136, value: Int( 2, @@ -251,10 +287,12 @@ Module( ), Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 139..145, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 140..141, value: Int( 3, @@ -263,6 +301,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 143..144, value: Int( 4, @@ -279,16 +318,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 167..175, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 167..175, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 168..174, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 168..169, id: Name("x"), ctx: Store, @@ -296,6 +339,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 173..174, value: Int( 2, @@ -311,13 +355,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 176..190, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 176..190, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 177..178, value: Int( 1, @@ -326,9 +373,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 180..186, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 180..181, id: Name("x"), ctx: Store, @@ -336,6 +385,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 185..186, value: Int( 2, @@ -346,6 +396,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 188..189, value: Int( 3, @@ -359,13 +410,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 191..205, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 191..205, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 192..193, value: Int( 1, @@ -374,9 +428,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 196..202, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 196..197, id: Name("x"), ctx: Store, @@ -384,6 +440,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 201..202, value: Int( 2, @@ -399,13 +456,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 225..235, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 225..235, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 226..227, value: Int( 1, @@ -414,9 +474,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 229..231, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 230..231, id: Name("x"), ctx: Load, @@ -427,6 +489,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 233..234, value: Int( 3, @@ -440,13 +503,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 236..250, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 236..250, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 237..238, value: Int( 1, @@ -455,12 +521,15 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 240..246, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 241..246, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..242, id: Name("x"), ctx: Load, @@ -469,6 +538,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("y"), ctx: Load, @@ -481,6 +551,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 248..249, value: Int( 3, @@ -494,16 +565,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 273..312, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 273..312, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 274..279, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 274..275, value: Int( 1, @@ -513,6 +588,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 278..279, value: Int( 2, @@ -523,10 +599,12 @@ Module( ), Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 281..287, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 282..283, id: Name("a"), ctx: Load, @@ -534,6 +612,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 285..286, id: Name("b"), ctx: Load, @@ -546,10 +625,12 @@ Module( ), Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 289..298, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 290..291, value: Int( 1, @@ -558,6 +639,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 293..294, value: Int( 2, @@ -566,6 +648,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 296..297, value: Int( 3, @@ -577,12 +660,14 @@ Module( ), Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 300..311, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 301..302, id: Name("a"), ctx: Load, @@ -591,6 +676,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 304..305, id: Name("b"), ctx: Load, @@ -601,6 +687,7 @@ Module( key: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 309..310, id: Name("d"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set_comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set_comprehension.py.snap index 5365de4ac2dfe..8ead74d20d457 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set_comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__set_comprehension.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/set_comprehension.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..492, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..15, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 0..15, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("x"), ctx: Load, @@ -26,8 +29,10 @@ Module( generators: [ Comprehension { range: 3..14, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("i"), ctx: Store, @@ -35,6 +40,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..14, id: Name("ll"), ctx: Load, @@ -50,12 +56,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..57, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 16..57, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("b"), ctx: Load, @@ -64,8 +73,10 @@ Module( generators: [ Comprehension { range: 19..56, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("c"), ctx: Store, @@ -73,6 +84,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("d"), ctx: Load, @@ -81,9 +93,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 33..39, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..34, id: Name("x"), ctx: Load, @@ -95,6 +109,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("w"), ctx: Load, @@ -105,11 +120,13 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 43..51, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("y"), ctx: Load, @@ -117,6 +134,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..51, id: Name("yy"), ctx: Load, @@ -127,6 +145,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..56, id: Name("z"), ctx: Load, @@ -142,12 +161,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..103, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 58..103, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("a"), ctx: Load, @@ -156,8 +178,10 @@ Module( generators: [ Comprehension { range: 61..82, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("b"), ctx: Store, @@ -165,6 +189,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("c"), ctx: Load, @@ -173,11 +198,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 75..82, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("d"), ctx: Load, @@ -185,6 +212,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("e"), ctx: Load, @@ -198,8 +226,10 @@ Module( }, Comprehension { range: 83..102, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..88, id: Name("f"), ctx: Store, @@ -207,6 +237,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..93, id: Name("j"), ctx: Load, @@ -215,9 +246,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 97..102, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("k"), ctx: Load, @@ -229,6 +262,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 101..102, id: Name("h"), ctx: Load, @@ -247,12 +281,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 104..155, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 104..155, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..106, id: Name("a"), ctx: Load, @@ -261,8 +298,10 @@ Module( generators: [ Comprehension { range: 107..128, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..112, id: Name("b"), ctx: Store, @@ -270,6 +309,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..117, id: Name("c"), ctx: Load, @@ -278,11 +318,13 @@ Module( ifs: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 121..128, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..122, id: Name("d"), ctx: Load, @@ -290,6 +332,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("e"), ctx: Load, @@ -303,8 +346,10 @@ Module( }, Comprehension { range: 129..154, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("f"), ctx: Store, @@ -312,6 +357,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 144..145, id: Name("j"), ctx: Load, @@ -320,9 +366,11 @@ Module( ifs: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 149..154, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 149..150, id: Name("k"), ctx: Load, @@ -334,6 +382,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 153..154, id: Name("h"), ctx: Load, @@ -352,12 +401,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 156..173, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 156..173, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 157..158, id: Name("a"), ctx: Load, @@ -366,12 +418,15 @@ Module( generators: [ Comprehension { range: 159..172, + node_index: AtomicNodeIndex(..), target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 163..167, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 163..164, id: Name("a"), ctx: Store, @@ -379,6 +434,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("b"), ctx: Store, @@ -391,6 +447,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..172, id: Name("G"), ctx: Load, @@ -406,12 +463,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 312..334, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 312..334, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 313..314, id: Name("x"), ctx: Load, @@ -420,8 +480,10 @@ Module( generators: [ Comprehension { range: 315..333, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 319..320, id: Name("x"), ctx: Store, @@ -429,10 +491,12 @@ Module( ), iter: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 325..332, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 331..332, id: Name("y"), ctx: Load, @@ -451,12 +515,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 335..362, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 335..362, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 336..337, id: Name("x"), ctx: Load, @@ -465,8 +532,10 @@ Module( generators: [ Comprehension { range: 338..361, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 342..343, id: Name("x"), ctx: Store, @@ -474,9 +543,11 @@ Module( ), iter: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 348..360, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 359..360, id: Name("y"), ctx: Load, @@ -494,12 +565,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 363..389, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 363..389, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 364..365, id: Name("x"), ctx: Load, @@ -508,8 +582,10 @@ Module( generators: [ Comprehension { range: 366..388, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 370..371, id: Name("x"), ctx: Store, @@ -517,19 +593,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 376..387, parameters: Some( Parameters { range: 383..384, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 383..384, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 383..384, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 383..384, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -543,6 +626,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 386..387, id: Name("y"), ctx: Load, @@ -560,12 +644,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 390..420, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 390..420, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 391..392, id: Name("x"), ctx: Load, @@ -574,8 +661,10 @@ Module( generators: [ Comprehension { range: 393..419, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 397..398, id: Name("x"), ctx: Store, @@ -583,6 +672,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 402..406, id: Name("data"), ctx: Load, @@ -591,10 +681,12 @@ Module( ifs: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 411..418, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 417..418, id: Name("y"), ctx: Load, @@ -613,12 +705,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 421..456, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 421..456, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 422..423, id: Name("x"), ctx: Load, @@ -627,8 +722,10 @@ Module( generators: [ Comprehension { range: 424..455, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 428..429, id: Name("x"), ctx: Store, @@ -636,6 +733,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 433..437, id: Name("data"), ctx: Load, @@ -644,9 +742,11 @@ Module( ifs: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 442..454, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 453..454, id: Name("y"), ctx: Load, @@ -664,12 +764,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 457..491, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 457..491, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 458..459, id: Name("x"), ctx: Load, @@ -678,8 +781,10 @@ Module( generators: [ Comprehension { range: 460..490, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 464..465, id: Name("x"), ctx: Store, @@ -687,6 +792,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 469..473, id: Name("data"), ctx: Load, @@ -695,19 +801,26 @@ Module( ifs: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 478..489, parameters: Some( Parameters { range: 485..486, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 485..486, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 485..486, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 485..486, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -721,6 +834,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 488..489, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__slice.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__slice.py.snap index 2f33310702852..c04177fae75bd 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__slice.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__slice.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/slice.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..211, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..27, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 23..27, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("x"), ctx: Load, @@ -25,6 +28,7 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 25..26, lower: None, upper: None, @@ -38,12 +42,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..33, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 28..33, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("x"), ctx: Load, @@ -51,10 +58,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 30..32, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 30..31, value: Int( 1, @@ -73,12 +82,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 34..39, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 34..39, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("x"), ctx: Load, @@ -86,11 +98,13 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 36..38, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 37..38, value: Int( 2, @@ -108,12 +122,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..46, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 40..46, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("x"), ctx: Load, @@ -121,10 +138,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 42..45, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 42..43, value: Int( 1, @@ -135,6 +154,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..45, value: Int( 2, @@ -152,12 +172,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 47..52, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 47..52, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Load, @@ -165,6 +188,7 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 49..51, lower: None, upper: None, @@ -178,12 +202,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..59, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 53..59, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("x"), ctx: Load, @@ -191,10 +218,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 55..58, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 55..56, value: Int( 1, @@ -213,12 +242,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 60..66, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 60..66, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("x"), ctx: Load, @@ -226,11 +258,13 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 62..65, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 63..64, value: Int( 2, @@ -248,12 +282,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..74, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 67..74, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("x"), ctx: Load, @@ -261,10 +298,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 69..73, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 69..70, value: Int( 1, @@ -275,6 +314,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 71..72, value: Int( 2, @@ -292,12 +332,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 75..81, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 75..81, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("x"), ctx: Load, @@ -305,12 +348,14 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 77..80, lower: None, upper: None, step: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 3, @@ -327,12 +372,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..89, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 82..89, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("x"), ctx: Load, @@ -340,10 +388,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 84..88, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 84..85, value: Int( 1, @@ -355,6 +405,7 @@ Module( step: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 87..88, value: Int( 3, @@ -371,12 +422,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 90..97, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 90..97, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("x"), ctx: Load, @@ -384,11 +438,13 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 92..96, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 93..94, value: Int( 2, @@ -399,6 +455,7 @@ Module( step: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 95..96, value: Int( 3, @@ -415,12 +472,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 98..106, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 98..106, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("x"), ctx: Load, @@ -428,10 +488,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 100..105, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 100..101, value: Int( 1, @@ -442,6 +504,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 102..103, value: Int( 2, @@ -452,6 +515,7 @@ Module( step: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..105, value: Int( 3, @@ -468,12 +532,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 127..136, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 127..136, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("x"), ctx: Load, @@ -481,9 +548,11 @@ Module( ), slice: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 129..135, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("y"), ctx: Store, @@ -491,6 +560,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 134..135, value: Int( 2, @@ -506,12 +576,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 137..149, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 137..149, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("x"), ctx: Load, @@ -519,13 +592,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 139..148, lower: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 140..146, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 140..141, id: Name("y"), ctx: Store, @@ -533,6 +609,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 145..146, value: Int( 2, @@ -553,12 +630,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 150..160, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 150..160, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("x"), ctx: Load, @@ -566,13 +646,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 152..159, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 152..158, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 152..153, id: Name("y"), ctx: Store, @@ -580,6 +663,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 157..158, value: Int( 2, @@ -600,12 +684,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 202..210, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 202..210, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 202..203, id: Name("x"), ctx: Load, @@ -613,10 +700,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 204..209, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 204..205, value: Int( 1, @@ -625,11 +714,13 @@ Module( ), Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 206..208, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 207..208, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__starred.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__starred.py.snap index 3d1f786a8c23f..a4fe4f78b4714 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__starred.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__starred.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/starred.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..172, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..2, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 0..2, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("a"), ctx: Load, @@ -30,15 +33,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3..11, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3..11, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5..10, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("a"), ctx: Load, @@ -47,6 +54,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 1, @@ -62,15 +70,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..19, value: Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 12..19, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 13..19, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..14, id: Name("x"), ctx: Load, @@ -79,6 +91,7 @@ Module( attr: Identifier { id: Name("attr"), range: 15..19, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -90,10 +103,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 21..57, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..32, id: Name("array_slice"), ctx: Store, @@ -102,9 +117,11 @@ Module( ], value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 35..57, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..40, id: Name("array"), ctx: Load, @@ -112,10 +129,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 41..56, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 41..42, value: Int( 0, @@ -124,9 +143,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 44..52, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..52, id: Name("indexes"), ctx: Load, @@ -137,10 +158,12 @@ Module( ), UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 54..56, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 55..56, value: Int( 1, @@ -161,13 +184,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 58..94, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 58..80, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..63, id: Name("array"), ctx: Load, @@ -175,10 +201,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 64..79, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 64..65, value: Int( 0, @@ -187,9 +215,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 67..75, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..75, id: Name("indexes"), ctx: Load, @@ -200,10 +230,12 @@ Module( ), UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 77..79, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 1, @@ -223,6 +255,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..94, id: Name("array_slice"), ctx: Load, @@ -232,12 +265,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 95..140, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 95..140, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..100, id: Name("array"), ctx: Load, @@ -245,13 +281,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 101..139, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 101..119, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..119, id: Name("indexes_to_select"), ctx: Load, @@ -262,9 +301,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 121..139, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..139, id: Name("indexes_to_select"), ctx: Load, @@ -285,12 +326,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 141..171, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 141..171, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 141..146, id: Name("array"), ctx: Load, @@ -298,14 +342,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 147..170, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 147..150, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 147..148, value: Int( 3, @@ -316,6 +363,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 149..150, value: Int( 5, @@ -328,9 +376,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 152..170, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 153..170, id: Name("indexes_to_select"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__string.py.snap index 6cddb0139928a..f9480bed422c7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__string.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/string.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..163, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..13, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 0..13, value: StringLiteralValue { inner: Single( StringLiteral { range: 0..13, + node_index: AtomicNodeIndex(..), value: "Hello World", flags: StringLiteralFlags { quote_style: Single, @@ -35,14 +38,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..20, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 14..20, value: StringLiteralValue { inner: Single( StringLiteral { range: 14..20, + node_index: AtomicNodeIndex(..), value: "😎", flags: StringLiteralFlags { quote_style: Double, @@ -58,9 +64,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..32, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 21..32, value: StringLiteralValue { inner: Concatenated( @@ -68,6 +76,7 @@ Module( strings: [ StringLiteral { range: 21..26, + node_index: AtomicNodeIndex(..), value: "Foo", flags: StringLiteralFlags { quote_style: Single, @@ -77,6 +86,7 @@ Module( }, StringLiteral { range: 27..32, + node_index: AtomicNodeIndex(..), value: "Bar", flags: StringLiteralFlags { quote_style: Single, @@ -95,9 +105,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 33..60, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 39..58, value: StringLiteralValue { inner: Concatenated( @@ -105,6 +117,7 @@ Module( strings: [ StringLiteral { range: 39..42, + node_index: AtomicNodeIndex(..), value: "A", flags: StringLiteralFlags { quote_style: Single, @@ -114,6 +127,7 @@ Module( }, StringLiteral { range: 47..50, + node_index: AtomicNodeIndex(..), value: "B", flags: StringLiteralFlags { quote_style: Single, @@ -123,6 +137,7 @@ Module( }, StringLiteral { range: 55..58, + node_index: AtomicNodeIndex(..), value: "C", flags: StringLiteralFlags { quote_style: Single, @@ -141,14 +156,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 61..79, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 61..79, value: StringLiteralValue { inner: Single( StringLiteral { range: 61..79, + node_index: AtomicNodeIndex(..), value: "Olá, Mundo!", flags: StringLiteralFlags { quote_style: Single, @@ -164,14 +182,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 80..91, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 80..91, value: StringLiteralValue { inner: Single( StringLiteral { range: 80..91, + node_index: AtomicNodeIndex(..), value: "ABCDE", flags: StringLiteralFlags { quote_style: Double, @@ -187,9 +208,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..121, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 98..119, value: StringLiteralValue { inner: Concatenated( @@ -197,6 +220,7 @@ Module( strings: [ StringLiteral { range: 98..106, + node_index: AtomicNodeIndex(..), value: "aB", flags: StringLiteralFlags { quote_style: Single, @@ -206,6 +230,7 @@ Module( }, StringLiteral { range: 111..119, + node_index: AtomicNodeIndex(..), value: "cD", flags: StringLiteralFlags { quote_style: Single, @@ -224,14 +249,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..136, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 122..136, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 122..136, + node_index: AtomicNodeIndex(..), value: [ 104, 101, @@ -259,15 +287,18 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 137..161, value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 137..161, value: BytesLiteralValue { inner: Concatenated( [ BytesLiteral { range: 137..145, + node_index: AtomicNodeIndex(..), value: [ 98, 121, @@ -283,6 +314,7 @@ Module( }, BytesLiteral { range: 146..161, + node_index: AtomicNodeIndex(..), value: [ 99, 111, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__subscript.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__subscript.py.snap index 23d49419b20da..f90b4a3033bf7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__subscript.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__subscript.py.snap @@ -1,26 +1,30 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/subscript.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..266, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..10, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 0..10, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 0..7, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..4, id: Name("data"), ctx: Load, @@ -28,6 +32,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 0, @@ -39,6 +44,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 0, @@ -52,12 +58,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 11..21, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 11..21, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..15, id: Name("data"), ctx: Load, @@ -65,10 +74,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 16..20, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..17, value: Int( 0, @@ -77,6 +88,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 19..20, value: Int( 1, @@ -95,12 +107,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..31, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 22..31, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..26, id: Name("data"), ctx: Load, @@ -108,14 +123,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 27..30, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 27..29, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 27..28, value: Int( 0, @@ -139,12 +157,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..43, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 32..43, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..36, id: Name("data"), ctx: Load, @@ -152,14 +173,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 37..42, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 37..39, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 37..38, value: Int( 0, @@ -173,6 +197,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 41..42, value: Int( 1, @@ -191,12 +216,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..56, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 44..56, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..48, id: Name("data"), ctx: Load, @@ -204,14 +232,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 49..55, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 49..52, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 49..50, value: Int( 0, @@ -222,6 +253,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, @@ -234,6 +266,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 2, @@ -252,12 +285,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..80, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 57..80, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..61, id: Name("data"), ctx: Load, @@ -265,14 +301,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 62..79, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 62..67, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 62..63, value: Int( 0, @@ -283,6 +322,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 64..65, value: Int( 1, @@ -293,6 +333,7 @@ Module( step: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 66..67, value: Int( 2, @@ -304,6 +345,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 69..70, value: Int( 3, @@ -312,10 +354,12 @@ Module( ), Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 72..79, lower: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("a"), ctx: Load, @@ -325,9 +369,11 @@ Module( upper: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 74..79, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("b"), ctx: Load, @@ -336,6 +382,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 1, @@ -360,12 +407,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..93, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 81..93, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..85, id: Name("data"), ctx: Load, @@ -373,9 +423,11 @@ Module( ), slice: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 86..92, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..87, id: Name("a"), ctx: Store, @@ -383,6 +435,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("b"), ctx: Load, @@ -397,12 +450,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..106, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 94..106, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 94..98, id: Name("data"), ctx: Load, @@ -410,10 +466,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 99..105, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 99..100, lower: None, upper: None, @@ -422,11 +480,13 @@ Module( ), Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 102..105, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 103..105, value: Int( 11, @@ -449,12 +509,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 107..120, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 107..120, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 107..111, id: Name("data"), ctx: Load, @@ -462,10 +525,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 112..119, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 112..113, value: Int( 1, @@ -474,6 +539,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 2, @@ -482,6 +548,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..119, value: Int( 3, @@ -500,12 +567,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 121..132, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 121..132, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 121..125, id: Name("data"), ctx: Load, @@ -513,10 +583,12 @@ Module( ), slice: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 126..131, op: Invert, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..131, id: Name("flag"), ctx: Load, @@ -531,12 +603,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..148, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 133..148, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..137, id: Name("data"), ctx: Load, @@ -544,13 +619,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 138..147, lower: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 139..145, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("a"), ctx: Store, @@ -558,6 +636,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 144..145, value: Int( 0, @@ -578,12 +657,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 149..165, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 149..165, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 149..153, id: Name("data"), ctx: Load, @@ -591,13 +673,16 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 154..164, lower: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 155..161, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 155..156, id: Name("a"), ctx: Store, @@ -605,6 +690,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..161, value: Int( 0, @@ -617,6 +703,7 @@ Module( upper: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 163..164, id: Name("y"), ctx: Load, @@ -633,12 +720,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 226..234, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 226..234, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 226..230, id: Name("data"), ctx: Load, @@ -646,13 +736,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 231..233, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 231..233, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 232..233, id: Name("x"), ctx: Load, @@ -673,12 +766,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 235..249, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 235..249, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 235..239, id: Name("data"), ctx: Load, @@ -686,18 +782,22 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 240..248, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 240..248, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 241..248, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..242, id: Name("x"), ctx: Load, @@ -705,6 +805,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 247..248, id: Name("y"), ctx: Load, @@ -728,12 +829,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 250..265, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 250..265, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 250..254, id: Name("data"), ctx: Load, @@ -741,16 +845,20 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 255..264, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 255..264, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 257..263, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 257..258, id: Name("x"), ctx: Store, @@ -758,6 +866,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 262..263, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap new file mode 100644 index 0000000000000..b9d44898d11d6 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap @@ -0,0 +1,3505 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/valid/expressions/t_string.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..1143, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 18..21, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 18..21, + value: TStringValue { + inner: Single( + TString( + TString { + range: 18..21, + node_index: AtomicNodeIndex(..), + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 22..25, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 22..25, + value: TStringValue { + inner: Single( + TString( + TString { + range: 22..25, + node_index: AtomicNodeIndex(..), + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 26..29, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 26..29, + value: TStringValue { + inner: Single( + TString( + TString { + range: 26..29, + node_index: AtomicNodeIndex(..), + elements: [], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 30..37, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 30..37, + value: TStringValue { + inner: Single( + TString( + TString { + range: 30..37, + node_index: AtomicNodeIndex(..), + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 38..45, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 38..45, + value: TStringValue { + inner: Single( + TString( + TString { + range: 38..45, + node_index: AtomicNodeIndex(..), + elements: [], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 47..56, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 47..56, + value: TStringValue { + inner: Single( + TString( + TString { + range: 47..56, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 49..55, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 50..54, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 50..54, + node_index: AtomicNodeIndex(..), + value: " t", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 57..67, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 57..67, + value: TStringValue { + inner: Single( + TString( + TString { + range: 57..67, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 59..66, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 60..63, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 68..75, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 68..75, + value: TStringValue { + inner: Single( + TString( + TString { + range: 68..75, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 70..74, + node_index: AtomicNodeIndex(..), + expression: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 71..73, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 71..72, + value: Int( + 3, + ), + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 76..86, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 76..86, + value: TStringValue { + inner: Single( + TString( + TString { + range: 76..86, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 78..85, + node_index: AtomicNodeIndex(..), + expression: Compare( + ExprCompare { + node_index: AtomicNodeIndex(..), + range: 79..83, + left: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 79..80, + value: Int( + 3, + ), + }, + ), + ops: [ + NotEq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 82..83, + value: Int( + 4, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 84..84, + node_index: AtomicNodeIndex(..), + elements: [], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 87..102, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 87..102, + value: TStringValue { + inner: Single( + TString( + TString { + range: 87..102, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 89..101, + node_index: AtomicNodeIndex(..), + expression: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 90..91, + value: Int( + 3, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 92..100, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 92..97, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 93..96, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 93..96, + node_index: AtomicNodeIndex(..), + value: "}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 97..100, + node_index: AtomicNodeIndex(..), + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 103..118, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 103..118, + value: TStringValue { + inner: Single( + TString( + TString { + range: 103..118, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 105..117, + node_index: AtomicNodeIndex(..), + expression: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 106..107, + value: Int( + 3, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 108..116, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 108..113, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 109..112, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 109..112, + node_index: AtomicNodeIndex(..), + value: "{", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 113..116, + node_index: AtomicNodeIndex(..), + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 119..133, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 119..133, + value: TStringValue { + inner: Single( + TString( + TString { + range: 119..133, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 121..132, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 124..127, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 134..154, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 134..154, + value: TStringValue { + inner: Single( + TString( + TString { + range: 134..154, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 136..153, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 139..142, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 147..152, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 147..152, + node_index: AtomicNodeIndex(..), + value: ".3f ", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 155..173, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 155..173, + value: TStringValue { + inner: Single( + TString( + TString { + range: 155..173, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 157..172, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 160..163, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 174..190, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 174..190, + value: TStringValue { + inner: Single( + TString( + TString { + range: 174..190, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 176..189, + node_index: AtomicNodeIndex(..), + expression: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 179..183, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 179..180, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 182..183, + value: Int( + 2, + ), + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 191..217, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 191..217, + value: TStringValue { + inner: Single( + TString( + TString { + range: 191..217, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 193..216, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 194..210, + value: TStringValue { + inner: Single( + TString( + TString { + range: 194..210, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 196..209, + node_index: AtomicNodeIndex(..), + expression: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 197..203, + value: Float( + 3.1415, + ), + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 205..208, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 205..208, + node_index: AtomicNodeIndex(..), + value: ".1f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 211..215, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 211..215, + node_index: AtomicNodeIndex(..), + value: "*^20", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 219..253, + value: Dict( + ExprDict { + node_index: AtomicNodeIndex(..), + range: 219..253, + items: [ + DictItem { + key: Some( + TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 220..248, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 220..226, + node_index: AtomicNodeIndex(..), + value: "foo ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 227..242, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 229..233, + node_index: AtomicNodeIndex(..), + value: "bar ", + }, + ), + Interpolation( + InterpolatedElement { + range: 233..240, + node_index: AtomicNodeIndex(..), + expression: BinOp( + ExprBinOp { + node_index: AtomicNodeIndex(..), + range: 234..239, + left: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 234..235, + id: Name("x"), + ctx: Load, + }, + ), + op: Add, + right: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 238..239, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 240..241, + node_index: AtomicNodeIndex(..), + value: " ", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 243..248, + node_index: AtomicNodeIndex(..), + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + ), + value: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 250..252, + value: Int( + 10, + ), + }, + ), + }, + ], + }, + ), + }, + ), + Match( + StmtMatch { + node_index: AtomicNodeIndex(..), + range: 254..345, + subject: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 260..263, + id: Name("foo"), + ctx: Load, + }, + ), + cases: [ + MatchCase { + range: 269..293, + node_index: AtomicNodeIndex(..), + pattern: MatchValue( + PatternMatchValue { + range: 274..279, + node_index: AtomicNodeIndex(..), + value: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 274..279, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 274..279, + node_index: AtomicNodeIndex(..), + value: "one", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + }, + ), + guard: None, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 289..293, + }, + ), + ], + }, + MatchCase { + range: 298..345, + node_index: AtomicNodeIndex(..), + pattern: MatchValue( + PatternMatchValue { + range: 303..331, + node_index: AtomicNodeIndex(..), + value: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 303..331, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 303..316, + node_index: AtomicNodeIndex(..), + value: "implicitly ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + StringLiteral { + range: 317..331, + node_index: AtomicNodeIndex(..), + value: "concatenated", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ], + value: "implicitly concatenated", + }, + ), + }, + }, + ), + }, + ), + guard: None, + body: [ + Pass( + StmtPass { + node_index: AtomicNodeIndex(..), + range: 341..345, + }, + ), + ], + }, + ], + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 347..364, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 347..364, + value: TStringValue { + inner: Single( + TString( + TString { + range: 347..364, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 349..350, + node_index: AtomicNodeIndex(..), + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 350..355, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 351..354, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 355..356, + node_index: AtomicNodeIndex(..), + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 356..363, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 357..360, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 361..362, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 361..362, + node_index: AtomicNodeIndex(..), + value: "\\", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 365..379, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 365..379, + value: TStringValue { + inner: Single( + TString( + TString { + range: 365..379, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 367..378, + node_index: AtomicNodeIndex(..), + value: "\\{foo\\}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 380..420, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 380..420, + value: TStringValue { + inner: Single( + TString( + TString { + range: 380..420, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 384..417, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 390..393, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 394..416, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 394..416, + node_index: AtomicNodeIndex(..), + value: "x\n y\n z\n", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 421..439, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 421..439, + value: TStringValue { + inner: Single( + TString( + TString { + range: 421..439, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 423..438, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 428..431, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ( ", + trailing: " ) = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 441..486, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 441..486, + value: TStringValue { + inner: Single( + TString( + TString { + range: 441..486, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 443..450, + node_index: AtomicNodeIndex(..), + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 450..455, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 451..454, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 455..468, + node_index: AtomicNodeIndex(..), + value: " {another} ", + }, + ), + Interpolation( + InterpolatedElement { + range: 468..473, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 469..472, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 473..476, + node_index: AtomicNodeIndex(..), + value: " {", + }, + ), + Interpolation( + InterpolatedElement { + range: 476..483, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 477..482, + id: Name("three"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 483..485, + node_index: AtomicNodeIndex(..), + value: "}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 487..529, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 487..529, + value: TStringValue { + inner: Single( + TString( + TString { + range: 487..529, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 489..496, + node_index: AtomicNodeIndex(..), + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 496..503, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 497..500, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Ascii, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 503..504, + node_index: AtomicNodeIndex(..), + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 504..511, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 505..508, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 511..512, + node_index: AtomicNodeIndex(..), + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 512..519, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 513..516, + id: Name("baz"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Repr, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 519..520, + node_index: AtomicNodeIndex(..), + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 520..528, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 521..527, + id: Name("foobar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 530..549, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 530..549, + value: TStringValue { + inner: Single( + TString( + TString { + range: 530..549, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 532..539, + node_index: AtomicNodeIndex(..), + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 539..548, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 540..541, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 542..547, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 542..547, + node_index: AtomicNodeIndex(..), + value: "y + 2", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 550..568, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 550..568, + value: TStringValue { + inner: Single( + TString( + TString { + range: 550..568, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 552..567, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 553..554, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 555..566, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 555..566, + node_index: AtomicNodeIndex(..), + expression: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 556..565, + func: Attribute( + ExprAttribute { + node_index: AtomicNodeIndex(..), + range: 556..563, + value: Set( + ExprSet { + node_index: AtomicNodeIndex(..), + range: 556..559, + elts: [ + NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 557..558, + value: Int( + 1, + ), + }, + ), + ], + }, + ), + attr: Identifier { + id: Name("pop"), + range: 560..563, + node_index: AtomicNodeIndex(..), + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 563..565, + node_index: AtomicNodeIndex(..), + args: [], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 569..588, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 569..588, + value: TStringValue { + inner: Single( + TString( + TString { + range: 569..588, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 571..587, + node_index: AtomicNodeIndex(..), + expression: Lambda( + ExprLambda { + node_index: AtomicNodeIndex(..), + range: 573..585, + parameters: Some( + Parameters { + range: 580..581, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 580..581, + node_index: AtomicNodeIndex(..), + parameter: Parameter { + range: 580..581, + node_index: AtomicNodeIndex(..), + name: Identifier { + id: Name("x"), + range: 580..581, + node_index: AtomicNodeIndex(..), + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Set( + ExprSet { + node_index: AtomicNodeIndex(..), + range: 582..585, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 583..584, + id: Name("x"), + ctx: Load, + }, + ), + ], + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 589..597, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 589..597, + value: TStringValue { + inner: Single( + TString( + TString { + range: 589..597, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 591..596, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 592..593, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 598..611, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 598..611, + value: TStringValue { + inner: Single( + TString( + TString { + range: 598..611, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 600..610, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 605..606, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 612..621, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 612..621, + value: TStringValue { + inner: Single( + TString( + TString { + range: 612..621, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 614..620, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 615..616, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: Ascii, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 622..636, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 622..636, + value: TStringValue { + inner: Single( + TString( + TString { + range: 622..636, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 624..635, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 625..626, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 627..634, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 627..634, + node_index: AtomicNodeIndex(..), + value: ".3f!r =", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 637..653, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 637..653, + value: TStringValue { + inner: Single( + TString( + TString { + range: 637..653, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 639..652, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 640..641, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " = ", + }, + ), + conversion: Repr, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 648..651, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 648..651, + node_index: AtomicNodeIndex(..), + value: ".3f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 654..667, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 654..667, + value: TStringValue { + inner: Single( + TString( + TString { + range: 654..667, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 656..666, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 657..658, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 659..665, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 659..665, + node_index: AtomicNodeIndex(..), + value: ".3f=!r", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 668..682, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 668..682, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 668..675, + node_index: AtomicNodeIndex(..), + value: "hello", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 676..682, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 678..681, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 679..680, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 683..696, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 683..696, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 683..689, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 685..688, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 686..687, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 690..696, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 692..695, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 693..694, + id: Name("y"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 697..711, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 697..711, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 697..703, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 699..702, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 700..701, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 704..711, + node_index: AtomicNodeIndex(..), + value: "world", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 712..756, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 712..756, + value: TStringValue { + inner: Single( + TString( + TString { + range: 712..756, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 714..739, + node_index: AtomicNodeIndex(..), + value: "Invalid args in command: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 739..755, + node_index: AtomicNodeIndex(..), + expression: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 740..754, + elts: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 740..747, + id: Name("command"), + ctx: Load, + }, + ), + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 749..754, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 750..754, + id: Name("args"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 757..775, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 757..775, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 757..762, + node_index: AtomicNodeIndex(..), + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 763..769, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 765..768, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 766..767, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 770..775, + node_index: AtomicNodeIndex(..), + value: "bar", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 776..825, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 782..823, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 782..786, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 784..785, + node_index: AtomicNodeIndex(..), + value: "a", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 791..795, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 793..794, + node_index: AtomicNodeIndex(..), + value: "b", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 800..803, + node_index: AtomicNodeIndex(..), + value: "c", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 808..813, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 811..812, + node_index: AtomicNodeIndex(..), + value: "d", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 818..823, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 821..822, + node_index: AtomicNodeIndex(..), + value: "e", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 850..879, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 850..879, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 850..856, + node_index: AtomicNodeIndex(..), + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 857..865, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 859..864, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 860..863, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 866..871, + node_index: AtomicNodeIndex(..), + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 872..879, + node_index: AtomicNodeIndex(..), + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 880..909, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 880..909, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 880..885, + node_index: AtomicNodeIndex(..), + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 886..894, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 888..893, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 889..892, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 895..901, + node_index: AtomicNodeIndex(..), + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 902..909, + node_index: AtomicNodeIndex(..), + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 910..939, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 910..939, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 910..915, + node_index: AtomicNodeIndex(..), + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 916..924, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 918..923, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 919..922, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 925..930, + node_index: AtomicNodeIndex(..), + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 931..939, + node_index: AtomicNodeIndex(..), + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 940..978, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 940..978, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 940..946, + node_index: AtomicNodeIndex(..), + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 947..966, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 949..953, + node_index: AtomicNodeIndex(..), + value: "bar ", + }, + ), + Interpolation( + InterpolatedElement { + range: 953..958, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 954..957, + id: Name("baz"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 958..965, + node_index: AtomicNodeIndex(..), + value: " really", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 967..973, + node_index: AtomicNodeIndex(..), + value: "bar", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 974..978, + node_index: AtomicNodeIndex(..), + value: "no", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 998..1017, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 998..1017, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 998..1007, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1000..1006, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1001..1005, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 1008..1017, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1010..1016, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1011..1015, + id: Name("that"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 1018..1036, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 1018..1036, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 1018..1027, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1020..1026, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1021..1025, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 1027..1036, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1029..1035, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1030..1034, + id: Name("that"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 1037..1064, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 1037..1064, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 1037..1046, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1039..1045, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1040..1044, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 1047..1053, + node_index: AtomicNodeIndex(..), + value: "that", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 1054..1064, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1056..1063, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1057..1062, + id: Name("other"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 1065..1111, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 1065..1111, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 1065..1082, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 1067..1071, + node_index: AtomicNodeIndex(..), + value: "one ", + }, + ), + Interpolation( + InterpolatedElement { + range: 1071..1077, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1072..1076, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 1077..1081, + node_index: AtomicNodeIndex(..), + value: " two", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 1083..1089, + node_index: AtomicNodeIndex(..), + value: "that", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 1090..1111, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 1092..1098, + node_index: AtomicNodeIndex(..), + value: "three ", + }, + ), + Interpolation( + InterpolatedElement { + range: 1098..1105, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1099..1104, + id: Name("other"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 1105..1110, + node_index: AtomicNodeIndex(..), + value: " four", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 1123..1142, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 1123..1142, + value: TStringValue { + inner: Single( + TString( + TString { + range: 1123..1142, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1125..1141, + node_index: AtomicNodeIndex(..), + expression: FString( + ExprFString { + node_index: AtomicNodeIndex(..), + range: 1126..1140, + value: FStringValue { + inner: Single( + FString( + FString { + range: 1126..1140, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1128..1139, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 1129..1138, + value: TStringValue { + inner: Single( + TString( + TString { + range: 1129..1138, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 1131..1137, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 1132..1136, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__tuple.py.snap index e8a88a1b9da76..4b019c87550a9 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__tuple.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/tuple.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..276, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..21, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 19..21, elts: [], ctx: Load, @@ -25,9 +27,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 22..26, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 23..25, elts: [], ctx: Load, @@ -38,13 +42,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 27..37, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 27..37, elts: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 29..31, elts: [], ctx: Load, @@ -53,6 +60,7 @@ Module( ), Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 34..36, elts: [], ctx: Load, @@ -68,13 +76,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 38..42, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 38..42, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("a"), ctx: Load, @@ -89,13 +100,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..49, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 43..49, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("a"), ctx: Load, @@ -103,6 +117,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("b"), ctx: Load, @@ -117,13 +132,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 50..57, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..57, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("a"), ctx: Load, @@ -131,6 +149,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("b"), ctx: Load, @@ -145,13 +164,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..66, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 59..65, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..61, id: Name("a"), ctx: Load, @@ -159,6 +181,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..64, id: Name("b"), ctx: Load, @@ -173,13 +196,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 90..92, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 90..92, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("a"), ctx: Load, @@ -194,13 +220,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..97, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 93..97, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("a"), ctx: Load, @@ -208,6 +237,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 96..97, id: Name("b"), ctx: Load, @@ -222,13 +252,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 98..103, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 98..103, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("a"), ctx: Load, @@ -236,6 +269,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 101..102, id: Name("b"), ctx: Load, @@ -250,16 +284,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 126..129, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 126..129, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 126..128, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("a"), ctx: Load, @@ -277,13 +315,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 130..135, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 130..135, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 130..131, id: Name("a"), ctx: Load, @@ -291,9 +332,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 133..135, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..135, id: Name("b"), ctx: Load, @@ -311,19 +354,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 136..161, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 136..161, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 136..142, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 137..142, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("a"), ctx: Load, @@ -332,6 +380,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 141..142, id: Name("b"), ctx: Load, @@ -344,12 +393,15 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 144..152, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 145..152, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..152, id: Name("x"), ctx: Load, @@ -362,6 +414,7 @@ Module( ), Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 154..156, elts: [], ctx: Load, @@ -370,9 +423,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 158..161, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 159..161, elts: [], ctx: Load, @@ -391,16 +446,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 162..167, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 162..167, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 163..165, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 164..165, id: Name("a"), ctx: Load, @@ -418,13 +477,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 168..175, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 168..175, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 169..170, id: Name("a"), ctx: Load, @@ -432,9 +494,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 172..174, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 173..174, id: Name("b"), ctx: Load, @@ -452,19 +516,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 176..203, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 176..203, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 177..183, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 178..183, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("a"), ctx: Load, @@ -473,6 +542,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 182..183, id: Name("b"), ctx: Load, @@ -485,12 +555,15 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 185..193, value: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 186..193, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 192..193, id: Name("x"), ctx: Load, @@ -503,6 +576,7 @@ Module( ), Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 195..197, elts: [], ctx: Load, @@ -511,9 +585,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 199..202, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 200..202, elts: [], ctx: Load, @@ -532,16 +608,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 224..233, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 224..233, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 225..231, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 225..226, id: Name("x"), ctx: Store, @@ -549,6 +629,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 230..231, value: Int( 1, @@ -566,13 +647,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 234..245, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 234..245, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 235..236, id: Name("x"), ctx: Load, @@ -580,9 +664,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 238..244, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 238..239, id: Name("y"), ctx: Store, @@ -590,6 +676,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 243..244, value: Int( 2, @@ -607,13 +694,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 246..260, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 246..260, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 247..248, id: Name("x"), ctx: Load, @@ -621,9 +711,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 250..256, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 250..251, id: Name("y"), ctx: Store, @@ -631,6 +723,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 255..256, value: Int( 2, @@ -641,6 +734,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 258..259, id: Name("z"), ctx: Load, @@ -655,13 +749,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 261..275, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 261..275, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 261..262, id: Name("x"), ctx: Load, @@ -669,9 +766,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 265..271, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 265..266, id: Name("y"), ctx: Store, @@ -679,6 +778,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 270..271, value: Int( 2, @@ -689,6 +789,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 274..275, id: Name("z"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__unary_op.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__unary_op.py.snap index 6e164b817fcf3..cf09c3dcf5ee1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__unary_op.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__unary_op.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/unary_op.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..276, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..11, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 9..11, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 1, @@ -31,13 +34,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..14, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 12..14, op: UAdd, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 13..14, value: Int( 1, @@ -50,13 +56,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..17, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 15..17, op: Invert, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..17, value: Int( 1, @@ -69,13 +78,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 18..23, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 18..23, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -87,21 +99,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 36..40, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 36..40, op: USub, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 37..40, op: USub, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 38..40, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 39..40, value: Int( 1, @@ -118,21 +135,26 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 41..45, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 41..45, op: USub, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 42..45, op: UAdd, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 43..45, op: Invert, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..45, value: Int( 1, @@ -149,25 +171,31 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 46..53, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 46..53, op: Not, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 49..53, op: USub, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 50..53, op: UAdd, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 51..53, op: Invert, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 1, @@ -186,17 +214,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..63, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 54..63, op: Not, operand: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 58..63, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..63, id: Name("x"), ctx: Load, @@ -210,16 +242,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 84..93, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 84..93, op: USub, operand: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 86..93, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 92..93, value: Int( 1, @@ -234,19 +270,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 94..109, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 94..109, op: UAdd, operand: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 96..109, left: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 96..103, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 102..103, value: Int( 1, @@ -258,10 +299,12 @@ Module( op: Pow, right: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 107..109, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 108..109, value: Int( 2, @@ -278,17 +321,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 110..117, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 110..117, op: Invert, operand: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 111..117, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 112..113, value: Int( 1, @@ -297,6 +344,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 2, @@ -314,16 +362,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..124, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 118..124, left: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 118..120, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 119..120, value: Int( 1, @@ -335,6 +387,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 123..124, value: Int( 2, @@ -347,23 +400,28 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 212..246, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 212..246, op: Or, values: [ BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 212..223, op: And, values: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 212..217, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 216..217, id: Name("a"), ctx: Load, @@ -373,6 +431,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..223, id: Name("b"), ctx: Load, @@ -383,18 +442,22 @@ Module( ), BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 227..246, op: And, values: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 227..236, op: Not, operand: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 231..236, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 231..232, id: Name("c"), ctx: Load, @@ -403,6 +466,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 235..236, id: Name("d"), ctx: Load, @@ -414,10 +478,12 @@ Module( ), UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 241..246, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("e"), ctx: Load, @@ -435,16 +501,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 247..259, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 247..259, op: Not, operand: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 252..258, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 252..253, id: Name("x"), ctx: Store, @@ -452,6 +522,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 257..258, value: Int( 1, @@ -466,16 +537,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 260..275, value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 260..275, op: Not, operand: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 264..275, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 264..265, id: Name("a"), ctx: Load, @@ -484,10 +559,12 @@ Module( op: BitOr, right: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 269..274, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 273..274, id: Name("b"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield.py.snap index 063373e6a3da3..cafb60b46058d 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/valid/expressions/yield.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..166, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 0..5, value: None, }, @@ -22,13 +25,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 6..13, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 6..13, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..13, id: Name("x"), ctx: Load, @@ -41,16 +47,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..25, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 14..25, value: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 20..25, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("x"), ctx: Load, @@ -59,6 +69,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 24..25, value: Int( 1, @@ -74,18 +85,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..39, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 26..39, value: Some( BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 32..39, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("x"), ctx: Load, @@ -93,6 +108,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("y"), ctx: Load, @@ -108,16 +124,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..52, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 40..52, value: Some( Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 46..52, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..50, id: Name("call"), ctx: Load, @@ -125,6 +145,7 @@ Module( ), arguments: Arguments { range: 50..52, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -137,17 +158,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..65, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 53..65, value: Some( List( ExprList { + node_index: AtomicNodeIndex(..), range: 59..65, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 60..61, value: Int( 1, @@ -156,6 +181,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 63..64, value: Int( 2, @@ -173,17 +199,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 66..78, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 66..78, value: Some( Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 72..78, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 73..74, value: Int( 3, @@ -192,6 +222,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 76..77, value: Int( 4, @@ -208,19 +239,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 79..91, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 79..91, value: Some( Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 85..91, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..87, id: Name("x"), ctx: Load, @@ -229,6 +264,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 89..90, value: Int( 5, @@ -246,17 +282,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..102, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 92..102, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 98..102, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("x"), ctx: Load, @@ -264,6 +304,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 101..102, id: Name("y"), ctx: Load, @@ -281,17 +322,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 103..115, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 103..115, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 109..115, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 110..111, id: Name("x"), ctx: Load, @@ -299,6 +344,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("y"), ctx: Load, @@ -316,16 +362,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 116..128, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 116..128, value: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 122..128, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..123, id: Name("x"), ctx: Load, @@ -337,6 +387,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 127..128, id: Name("y"), ctx: Load, @@ -352,16 +403,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 129..143, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 129..143, value: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 136..142, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("x"), ctx: Store, @@ -369,6 +424,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 141..142, value: Int( 1, @@ -384,17 +440,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 144..155, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 144..155, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 150..155, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("x"), ctx: Load, @@ -402,9 +462,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 153..155, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 154..155, id: Name("y"), ctx: Load, @@ -425,20 +487,25 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 156..165, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 156..165, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 162..165, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 162..164, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 163..164, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield_from.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield_from.py.snap index d35161d5fce1b..0d7bfe36e3b23 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield_from.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__yield_from.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/yield_from.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..199, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 0..12, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("x"), ctx: Load, @@ -29,15 +32,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..29, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 13..29, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 24..29, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Load, @@ -46,6 +53,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..29, value: Int( 1, @@ -60,17 +68,21 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..48, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 30..48, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 41..48, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Load, @@ -78,6 +90,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("y"), ctx: Load, @@ -92,15 +105,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..66, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 49..66, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 60..66, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..64, id: Name("call"), ctx: Load, @@ -108,6 +125,7 @@ Module( ), arguments: Arguments { range: 64..66, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -119,16 +137,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..84, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 67..84, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 78..84, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 1, @@ -137,6 +159,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 2, @@ -153,16 +176,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..102, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 85..102, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 96..102, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 97..98, value: Int( 3, @@ -171,6 +198,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 100..101, value: Int( 4, @@ -186,18 +214,22 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 103..120, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 103..120, value: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 114..120, items: [ DictItem { key: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 115..116, id: Name("x"), ctx: Load, @@ -206,6 +238,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 118..119, value: Int( 5, @@ -222,16 +255,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 121..138, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 121..138, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 132..138, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 133..134, id: Name("x"), ctx: Load, @@ -239,6 +276,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..137, id: Name("y"), ctx: Load, @@ -255,15 +293,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 139..156, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 139..156, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 150..156, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("x"), ctx: Load, @@ -275,6 +317,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 155..156, id: Name("y"), ctx: Load, @@ -289,15 +332,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 157..176, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 157..176, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 169..175, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 169..170, id: Name("x"), ctx: Store, @@ -305,6 +352,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 174..175, value: Int( 1, @@ -319,16 +367,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 177..199, value: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 177..199, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 188..199, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("x"), ctx: Load, @@ -336,12 +388,15 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 192..198, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 193..198, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..194, id: Name("x"), ctx: Load, @@ -350,6 +405,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 197..198, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_valid_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_valid_expr.py.snap index 82261413967ea..8d3f2a874385a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_valid_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_in_target_valid_expr.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/for_in_target_valid_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..89, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, target: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 4..13, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("d"), ctx: Load, @@ -26,9 +29,11 @@ Module( ), slice: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 6..12, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -40,6 +45,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("y"), ctx: Load, @@ -53,6 +59,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..23, id: Name("target"), ctx: Load, @@ -61,9 +68,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -75,16 +84,20 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 29..57, is_async: false, target: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 33..44, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 34..40, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("x"), ctx: Load, @@ -96,6 +109,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("y"), ctx: Load, @@ -106,6 +120,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 42..43, value: Int( 0, @@ -117,6 +132,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..52, id: Name("iter"), ctx: Load, @@ -125,9 +141,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..57, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 54..57, }, ), @@ -139,16 +157,20 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 58..88, is_async: false, target: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 62..75, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 63..69, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..64, id: Name("x"), ctx: Load, @@ -160,6 +182,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("y"), ctx: Load, @@ -171,12 +194,14 @@ Module( attr: Identifier { id: Name("attr"), range: 71..75, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..83, id: Name("iter"), ctx: Load, @@ -185,9 +210,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..88, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 85..88, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py38.py.snap index 892b22f86a3a1..e981ecdd5d676 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py38.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/for_iter_unpack_py38.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..112, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 43..65, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -22,13 +25,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 52..60, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 53..55, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("a"), ctx: Load, @@ -39,6 +45,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("b"), ctx: Load, @@ -52,9 +59,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 62..65, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 62..65, }, ), @@ -66,10 +75,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 66..88, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("x"), ctx: Store, @@ -77,10 +88,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 75..83, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("a"), ctx: Load, @@ -88,9 +101,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 80..82, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("b"), ctx: Load, @@ -107,9 +122,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 85..88, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 85..88, }, ), @@ -121,10 +138,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 89..111, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("x"), ctx: Store, @@ -132,13 +151,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 98..106, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 99..101, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("a"), ctx: Load, @@ -149,9 +171,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 103..105, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("b"), ctx: Load, @@ -168,9 +192,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 108..111, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 108..111, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py39.py.snap index ac67500fd781a..2579c5e35f3a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@for_iter_unpack_py39.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/for_iter_unpack_py39.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..106, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 43..63, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -22,13 +25,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 52..58, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 52..54, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("a"), ctx: Load, @@ -39,6 +45,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("b"), ctx: Load, @@ -52,9 +59,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 60..63, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 60..63, }, ), @@ -66,10 +75,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 64..84, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Store, @@ -77,10 +88,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 74..79, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("a"), ctx: Load, @@ -88,9 +101,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 77..79, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..79, id: Name("b"), ctx: Load, @@ -107,9 +122,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..84, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, }, ), @@ -121,10 +138,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 85..105, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("x"), ctx: Store, @@ -132,13 +151,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 94..100, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 94..96, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..96, id: Name("a"), ctx: Load, @@ -149,9 +171,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 98..100, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("b"), ctx: Load, @@ -168,9 +192,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 102..105, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 102..105, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap index 52b37be22a3f1..4e27abaf5828b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_no_space.py.snap @@ -1,25 +1,28 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/from_import_no_space.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..30, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..13, module: None, names: [ Alias { range: 12..13, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 12..13, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -29,14 +32,17 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 14..29, module: None, names: [ Alias { range: 28..29, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 28..29, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap index 9b5dde8170a24..ffbf2fa38ddc2 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_soft_keyword_module_name.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/from_import_soft_keyword_module_name.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..104, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..25, module: Some( Identifier { id: Name("match"), range: 5..10, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 18..25, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("pattern"), range: 18..25, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -34,19 +38,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 26..46, module: Some( Identifier { id: Name("type"), range: 31..35, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 43..46, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("bar"), range: 43..46, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -56,19 +64,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 47..71, module: Some( Identifier { id: Name("case"), range: 52..56, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 64..71, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("pattern"), range: 64..71, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -78,19 +90,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 72..103, module: Some( Identifier { id: Name("match.type.case"), range: 77..92, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 100..103, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("foo"), range: 100..103, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap index ec6c1d92dc39a..afc649c201f3a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@from_import_stmt_terminator.py.snap @@ -1,38 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/from_import_stmt_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..97, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..20, module: Some( Identifier { id: Name("a"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 15..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 15..16, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 18..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 18..19, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -42,27 +48,33 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 21..41, module: Some( Identifier { id: Name("a"), range: 26..27, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 36..37, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 36..37, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 39..40, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 39..40, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -72,13 +84,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..47, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 43..47, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("x"), ctx: Load, @@ -86,6 +101,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("y"), ctx: Load, @@ -100,27 +116,33 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 48..66, module: Some( Identifier { id: Name("a"), range: 53..54, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 62..63, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 62..63, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 65..66, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 65..66, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -130,13 +152,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..72, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 68..72, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -144,6 +169,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..72, id: Name("y"), ctx: Load, @@ -158,27 +184,33 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 73..91, module: Some( Identifier { id: Name("a"), range: 78..79, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 87..88, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 87..88, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 90..91, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 90..91, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -188,13 +220,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 92..96, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 92..96, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..93, id: Name("x"), ctx: Load, @@ -202,6 +237,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..96, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap index 1e672a1ce1945..386e5a0f15d95 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap @@ -1,38 +1,44 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/fstring_format_spec_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..43, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..19, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 0..19, value: FStringValue { inner: Single( FString( FString { range: 0..19, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..8, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..12, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..10, id: Name("x"), ctx: Load, @@ -41,16 +47,18 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 11..11, + node_index: AtomicNodeIndex(..), elements: [], }, ), }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..18, + node_index: AtomicNodeIndex(..), value: " world", }, ), @@ -70,27 +78,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..42, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 20..42, value: FStringValue { inner: Single( FString( FString { range: 20..42, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 22..28, + node_index: AtomicNodeIndex(..), value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 28..35, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Load, @@ -99,12 +113,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 31..34, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..34, + node_index: AtomicNodeIndex(..), value: ".3f", }, ), @@ -114,8 +130,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 35..41, + node_index: AtomicNodeIndex(..), value: " world", }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parameter_range.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parameter_range.py.snap index 17d571227aed7..68a01e322fcbd 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parameter_range.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parameter_range.py.snap @@ -1,40 +1,49 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/function_def_parameter_range.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..56, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..55, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..43, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 13..23, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 13..23, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("first"), range: 13..18, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..23, id: Name("int"), ctx: Load, @@ -46,15 +55,19 @@ Module( }, ParameterWithDefault { range: 29..40, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 29..40, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("second"), range: 29..35, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..40, id: Name("int"), ctx: Load, @@ -72,6 +85,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..50, id: Name("int"), ctx: Load, @@ -81,9 +95,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 52..55, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 52..55, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parenthesized_return_types.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parenthesized_return_types.py.snap index 7076142333c7d..2008c48ad0c35 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parenthesized_return_types.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_parenthesized_return_types.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/function_def_parenthesized_return_types.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..54, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..24, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..9, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,10 +36,12 @@ Module( returns: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 13..19, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -49,9 +56,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..24, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 21..24, }, ), @@ -62,16 +71,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 25..53, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 29..32, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 32..34, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -81,10 +95,12 @@ Module( returns: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 38..48, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..42, id: Name("int"), ctx: Load, @@ -92,6 +108,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..47, id: Name("str"), ctx: Load, @@ -106,9 +123,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 50..53, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 50..53, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_valid_return_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_valid_return_expr.py.snap index f4bd699f74239..f3762e02fdce5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_valid_return_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_def_valid_return_expr.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/function_def_valid_ret ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..97, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..27, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..9, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -30,9 +36,11 @@ Module( returns: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 13..22, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..16, id: Name("int"), ctx: Load, @@ -41,6 +49,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..22, id: Name("str"), ctx: Load, @@ -52,9 +61,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..27, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 24..27, }, ), @@ -65,16 +76,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 28..57, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 32..35, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 35..37, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -84,19 +100,26 @@ Module( returns: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 41..52, parameters: Some( Parameters { range: 48..49, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 48..49, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 48..49, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 48..49, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -110,6 +133,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..52, id: Name("x"), ctx: Load, @@ -121,9 +145,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..57, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 54..57, }, ), @@ -134,16 +160,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 58..96, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 62..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -153,15 +184,18 @@ Module( returns: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 71..91, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 78..82, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..74, id: Name("int"), ctx: Load, @@ -169,6 +203,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..91, id: Name("str"), ctx: Load, @@ -180,9 +215,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..96, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 93..96, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap index ed6c0528a6835..c4bcad78137cc 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@function_type_params_py312.py.snap @@ -7,27 +7,33 @@ input_file: crates/ruff_python_parser/resources/inline/ok/function_type_params_p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..62, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..61, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 48..51, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 51..54, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 52..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 52..53, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -38,6 +44,9 @@ Module( ), parameters: Parameters { range: 54..56, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -48,9 +57,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@global_stmt.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@global_stmt.py.snap index 24c5d44be2c4b..e5a51faf375a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@global_stmt.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@global_stmt.py.snap @@ -1,41 +1,47 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/global_stmt.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..24, body: [ Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 0..8, names: [ Identifier { id: Name("x"), range: 7..8, + node_index: AtomicNodeIndex(..), }, ], }, ), Global( StmtGlobal { + node_index: AtomicNodeIndex(..), range: 9..23, names: [ Identifier { id: Name("x"), range: 16..17, + node_index: AtomicNodeIndex(..), }, Identifier { id: Name("y"), range: 19..20, + node_index: AtomicNodeIndex(..), }, Identifier { id: Name("z"), range: 22..23, + node_index: AtomicNodeIndex(..), }, ], }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap index f5e2f2da77c85..15b856b9bc98c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_as_name_soft_keyword.py.snap @@ -1,29 +1,33 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/import_as_name_soft_keyword.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..58, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..19, names: [ Alias { range: 7..19, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("foo"), range: 7..10, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("match"), range: 14..19, + node_index: AtomicNodeIndex(..), }, ), }, @@ -32,18 +36,22 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 20..38, names: [ Alias { range: 27..38, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("bar"), range: 27..30, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("case"), range: 34..38, + node_index: AtomicNodeIndex(..), }, ), }, @@ -52,18 +60,22 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 39..57, names: [ Alias { range: 46..57, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("baz"), range: 46..49, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("type"), range: 53..57, + node_index: AtomicNodeIndex(..), }, ), }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap index 0d1775d267d0d..507563d546b40 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@import_stmt_terminator.py.snap @@ -1,32 +1,37 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/import_stmt_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..42, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..11, names: [ Alias { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 7..8, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 10..11, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 10..11, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -35,21 +40,26 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 13..24, names: [ Alias { range: 20..21, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 20..21, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 23..24, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 23..24, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -58,21 +68,26 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 25..36, names: [ Alias { range: 32..33, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 32..33, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 35..36, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 35..36, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -81,13 +96,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 37..41, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 37..41, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("c"), ctx: Load, @@ -95,6 +113,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("d"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap index 0d818e98ea794..4dc3301709967 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@irrefutable_case_pattern_at_end.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/irrefutable_case_patte ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..176, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..42, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -22,11 +25,14 @@ Module( cases: [ MatchCase { range: 13..24, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 18..19, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 2, @@ -39,9 +45,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 21..24, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 21..24, }, ), @@ -51,14 +59,17 @@ Module( }, MatchCase { range: 29..42, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 34..37, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("var"), range: 34..37, + node_index: AtomicNodeIndex(..), }, ), }, @@ -67,9 +78,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 39..42, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 39..42, }, ), @@ -82,9 +95,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 43..83, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("x"), ctx: Load, @@ -93,11 +108,14 @@ Module( cases: [ MatchCase { range: 56..67, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 61..62, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 2, @@ -110,9 +128,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 64..67, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 64..67, }, ), @@ -122,9 +142,11 @@ Module( }, MatchCase { range: 72..83, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 77..78, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -133,9 +155,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 80..83, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 80..83, }, ), @@ -148,9 +172,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 84..175, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("x"), ctx: Load, @@ -159,14 +185,17 @@ Module( cases: [ MatchCase { range: 97..118, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 102..105, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("var"), range: 102..105, + node_index: AtomicNodeIndex(..), }, ), }, @@ -174,6 +203,7 @@ Module( guard: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 109..113, value: true, }, @@ -182,9 +212,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 115..118, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 115..118, }, ), @@ -194,11 +226,14 @@ Module( }, MatchCase { range: 164..175, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 169..170, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 169..170, value: Int( 2, @@ -211,9 +246,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 172..175, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 172..175, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py37.py.snap index a925e03d2ee82..470474aa7fb6f 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py37.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/iter_unpack_return_py3 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..93, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 43..59, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..47, id: Name("rest"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..59, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 4, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 5, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 6, @@ -58,16 +65,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..92, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 64..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -78,14 +90,17 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 69..92, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 76..92, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 77..78, value: Int( 1, @@ -94,6 +109,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 80..81, value: Int( 2, @@ -102,6 +118,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 3, @@ -110,9 +127,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 86..91, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..91, id: Name("rest"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py38.py.snap index d1fbfcc158f7b..e0f7c8ec2297b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_return_py38.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/iter_unpack_return_py3 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..91, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 43..59, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..47, id: Name("rest"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..59, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 4, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 5, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 6, @@ -58,16 +65,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..90, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 64..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -78,14 +90,17 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 69..90, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 76..90, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 76..77, value: Int( 1, @@ -94,6 +109,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 2, @@ -102,6 +118,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 3, @@ -110,9 +127,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 85..90, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..90, id: Name("rest"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py37.py.snap index e27bc0edc092b..092f50c312700 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py37.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/iter_unpack_yield_py37 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..92, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 43..59, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..47, id: Name("rest"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..59, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 4, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 5, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 6, @@ -58,16 +65,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..91, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 64..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -78,17 +90,21 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 69..91, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 69..91, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 75..91, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 76..77, value: Int( 1, @@ -97,6 +113,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 79..80, value: Int( 2, @@ -105,6 +122,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 3, @@ -113,9 +131,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 85..90, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..90, id: Name("rest"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py38.py.snap index 4aa3293a645db..78df6541052c5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@iter_unpack_yield_py38.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/inline/ok/iter_unpack_yield_py38 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..128, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 43..59, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..47, id: Name("rest"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 50..59, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 4, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 5, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 6, @@ -58,16 +65,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 60..89, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 64..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -78,17 +90,21 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 69..89, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 69..89, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 75..89, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 1, @@ -97,6 +113,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 2, @@ -105,6 +122,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 3, @@ -113,9 +131,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 84..89, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..89, id: Name("rest"), ctx: Load, @@ -139,16 +159,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 90..127, is_async: false, decorator_list: [], name: Identifier { id: Name("h"), range: 94..95, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 95..97, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -159,17 +184,21 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 99..127, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 99..127, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 105..127, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 105..106, value: Int( 1, @@ -178,14 +207,17 @@ Module( ), Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 109..123, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 115..123, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 115..116, value: Int( 2, @@ -194,9 +226,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 118..123, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..123, id: Name("rest"), ctx: Load, @@ -215,6 +249,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 126..127, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_no_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_no_parameters.py.snap index 6ecfea43d1d61..7a76f5f14cc87 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_no_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_no_parameters.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/lambda_with_no_parameters.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..10, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..9, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..9, parameters: None, body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_valid_body.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_valid_body.py.snap index 61366ad154bb1..c407279f9daea 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_valid_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@lambda_with_valid_body.py.snap @@ -1,33 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/lambda_with_valid_body.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..152, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..11, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 0..11, parameters: Some( Parameters { range: 7..8, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 7..8, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 7..8, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -41,6 +49,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..11, id: Name("x"), ctx: Load, @@ -52,22 +61,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 12..38, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 12..38, parameters: Some( Parameters { range: 19..20, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 19..20, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 19..20, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 19..20, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -81,15 +98,18 @@ Module( ), body: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 22..38, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 27..31, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..23, id: Name("x"), ctx: Load, @@ -97,6 +117,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("y"), ctx: Load, @@ -110,22 +131,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 39..56, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 39..56, parameters: Some( Parameters { range: 46..47, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 46..47, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 46..47, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 46..47, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -139,9 +168,11 @@ Module( ), body: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 49..56, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..56, id: Name("x"), ctx: Load, @@ -155,22 +186,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..82, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 57..82, parameters: Some( Parameters { range: 64..65, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 64..65, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 64..65, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 64..65, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -184,19 +223,26 @@ Module( ), body: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 67..82, parameters: Some( Parameters { range: 74..75, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 74..75, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 74..75, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 74..75, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -210,9 +256,11 @@ Module( ), body: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 77..82, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("x"), ctx: Load, @@ -221,6 +269,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("y"), ctx: Load, @@ -236,22 +285,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..102, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 83..102, parameters: Some( Parameters { range: 90..91, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 90..91, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 90..91, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 90..91, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -265,10 +322,12 @@ Module( ), body: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 94..101, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("x"), ctx: Load, @@ -283,26 +342,35 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 136..151, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 136..151, elts: [ Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 136..147, parameters: Some( Parameters { range: 143..144, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 143..144, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 143..144, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 143..144, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -316,6 +384,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("x"), ctx: Load, @@ -325,9 +394,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 149..151, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap index 7c12a5fce7488..a4b120445780c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_after_py310.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/match_after_py310.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..80, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 46..79, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 2, @@ -23,11 +26,14 @@ Module( cases: [ MatchCase { range: 59..79, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 64..65, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 64..65, value: Int( 1, @@ -40,6 +46,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 75..79, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap index 8f967d7704c24..c300ac48439f6 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/match_as_pattern.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..60, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..32, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("foo"), ctx: Load, @@ -22,14 +25,17 @@ Module( cases: [ MatchCase { range: 15..32, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 20..27, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("foo_bar"), range: 20..27, + node_index: AtomicNodeIndex(..), }, ), }, @@ -38,9 +44,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 29..32, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 29..32, }, ), @@ -53,9 +61,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 33..59, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..42, id: Name("foo"), ctx: Load, @@ -64,9 +74,11 @@ Module( cases: [ MatchCase { range: 48..59, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 53..54, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -75,9 +87,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 56..59, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 56..59, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap index fd3c2f1ed1a4e..6ca3b670574fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_as_pattern_soft_keyword.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/match_as_pattern_soft_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..91, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..29, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("foo"), ctx: Load, @@ -22,14 +25,17 @@ Module( cases: [ MatchCase { range: 15..29, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 20..24, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("case"), range: 20..24, + node_index: AtomicNodeIndex(..), }, ), }, @@ -38,9 +44,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..29, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 26..29, }, ), @@ -53,9 +61,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 30..60, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..39, id: Name("foo"), ctx: Load, @@ -64,14 +74,17 @@ Module( cases: [ MatchCase { range: 45..60, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 50..55, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("match"), range: 50..55, + node_index: AtomicNodeIndex(..), }, ), }, @@ -80,9 +93,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..60, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 57..60, }, ), @@ -95,9 +110,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 61..90, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..70, id: Name("foo"), ctx: Load, @@ -106,14 +123,17 @@ Module( cases: [ MatchCase { range: 76..90, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 81..85, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("type"), range: 81..85, + node_index: AtomicNodeIndex(..), }, ), }, @@ -122,9 +142,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 87..90, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 87..90, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap index 833fa6d042cd1..17a7f854b3a2f 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_attr_pattern_soft_keyword.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_attr_pattern_soft_keyword.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..131, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..130, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("foo"), ctx: Load, @@ -23,14 +25,18 @@ Module( cases: [ MatchCase { range: 15..34, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 20..29, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 20..29, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..25, id: Name("match"), ctx: Load, @@ -39,6 +45,7 @@ Module( attr: Identifier { id: Name("bar"), range: 26..29, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -49,9 +56,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..34, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 31..34, }, ), @@ -61,14 +70,18 @@ Module( }, MatchCase { range: 39..57, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 44..52, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 44..52, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..48, id: Name("case"), ctx: Load, @@ -77,6 +90,7 @@ Module( attr: Identifier { id: Name("bar"), range: 49..52, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -87,9 +101,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 54..57, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 54..57, }, ), @@ -99,14 +115,18 @@ Module( }, MatchCase { range: 62..80, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 67..75, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 67..75, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..71, id: Name("type"), ctx: Load, @@ -115,6 +135,7 @@ Module( attr: Identifier { id: Name("bar"), range: 72..75, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -125,9 +146,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 77..80, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 77..80, }, ), @@ -137,29 +160,38 @@ Module( }, MatchCase { range: 85..130, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 90..125, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 90..125, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 90..119, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 90..114, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 90..109, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 90..105, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 90..100, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..95, id: Name("match"), ctx: Load, @@ -168,6 +200,7 @@ Module( attr: Identifier { id: Name("case"), range: 96..100, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -175,6 +208,7 @@ Module( attr: Identifier { id: Name("type"), range: 101..105, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -182,6 +216,7 @@ Module( attr: Identifier { id: Name("bar"), range: 106..109, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -189,6 +224,7 @@ Module( attr: Identifier { id: Name("type"), range: 110..114, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -196,6 +232,7 @@ Module( attr: Identifier { id: Name("case"), range: 115..119, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -203,6 +240,7 @@ Module( attr: Identifier { id: Name("match"), range: 120..125, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -213,9 +251,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 127..130, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 127..130, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_1.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_1.py.snap index 5e454f2909e29..7c7131c6e54c2 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_1.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_identifier_1.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..18, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..17, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 0..17, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("match"), ctx: Load, @@ -29,6 +32,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..17, id: Name("case"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_2.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_2.py.snap index f80bb85b577df..e0c274a03d11e 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_identifier_2.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_identifier_2.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..149, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..5, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("match"), ctx: Load, @@ -24,12 +26,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 6..18, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 6..18, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..11, id: Name("match"), ctx: Load, @@ -41,6 +46,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 15..18, id: Name("foo"), ctx: Load, @@ -53,13 +59,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..31, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 19..31, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..23, id: Name("foo"), ctx: Load, @@ -67,6 +76,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..30, id: Name("match"), ctx: Load, @@ -81,13 +91,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..44, value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 32..44, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 33..36, id: Name("foo"), ctx: Load, @@ -95,6 +108,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..43, id: Name("match"), ctx: Load, @@ -108,13 +122,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 45..57, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 45..57, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..49, id: Name("foo"), ctx: Load, @@ -122,6 +139,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..56, id: Name("match"), ctx: Load, @@ -134,9 +152,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..63, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..63, id: Name("match"), ctx: Load, @@ -146,9 +166,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 65..75, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..70, id: Name("match"), ctx: Store, @@ -156,6 +178,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..75, id: Name("int"), ctx: Load, @@ -167,13 +190,16 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 76..82, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 76..82, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..81, id: Name("match"), ctx: Load, @@ -188,12 +214,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..92, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 83..92, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..88, id: Name("match"), ctx: Load, @@ -202,6 +231,7 @@ Module( attr: Identifier { id: Name("foo"), range: 89..92, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -210,12 +240,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 93..104, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 93..104, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..98, id: Name("match"), ctx: Load, @@ -224,6 +257,7 @@ Module( op: Div, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 101..104, id: Name("foo"), ctx: Load, @@ -235,12 +269,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 105..117, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 105..117, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..110, id: Name("match"), ctx: Load, @@ -249,6 +286,7 @@ Module( op: LShift, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..117, id: Name("foo"), ctx: Load, @@ -260,14 +298,17 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 118..131, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 118..131, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 118..123, id: Name("match"), ctx: Load, @@ -275,6 +316,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..131, id: Name("foo"), ctx: Load, @@ -287,12 +329,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 132..148, value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 132..148, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..137, id: Name("match"), ctx: Load, @@ -304,6 +349,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 145..148, id: Name("foo"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap index 044de1e15736e..2345dca24948a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_keyword_1.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..358, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..26, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..9, id: Name("foo"), ctx: Load, @@ -23,9 +25,11 @@ Module( cases: [ MatchCase { range: 15..26, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 20..21, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -34,9 +38,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 23..26, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 23..26, }, ), @@ -49,9 +55,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 27..51, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 33..34, value: Int( 1, @@ -61,9 +69,11 @@ Module( cases: [ MatchCase { range: 40..51, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 45..46, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -72,9 +82,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 48..51, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 48..51, }, ), @@ -87,9 +99,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 52..78, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, value: Float( 1.0, @@ -99,9 +113,11 @@ Module( cases: [ MatchCase { range: 67..78, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 72..73, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -110,9 +126,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 75..78, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 75..78, }, ), @@ -125,9 +143,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 79..104, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 85..87, value: Complex { real: 0.0, @@ -138,9 +158,11 @@ Module( cases: [ MatchCase { range: 93..104, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 98..99, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -149,9 +171,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 101..104, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 101..104, }, ), @@ -164,14 +188,17 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 105..133, subject: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 111..116, value: StringLiteralValue { inner: Single( StringLiteral { range: 111..116, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -186,9 +213,11 @@ Module( cases: [ MatchCase { range: 122..133, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 127..128, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -197,9 +226,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 130..133, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 130..133, }, ), @@ -212,27 +243,33 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 134..167, subject: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 140..150, value: FStringValue { inner: Single( FString( FString { range: 140..150, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 142..146, + node_index: AtomicNodeIndex(..), value: "foo ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 146..149, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 147..148, id: Name("x"), ctx: Load, @@ -258,9 +295,11 @@ Module( cases: [ MatchCase { range: 156..167, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 161..162, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -269,9 +308,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 164..167, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 164..167, }, ), @@ -284,13 +325,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 168..197, subject: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 174..180, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 175..176, value: Int( 1, @@ -299,6 +343,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 178..179, value: Int( 2, @@ -311,9 +356,11 @@ Module( cases: [ MatchCase { range: 186..197, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 191..192, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -322,9 +369,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 194..197, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 194..197, }, ), @@ -337,13 +386,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 198..225, subject: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 204..208, op: Invert, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 205..208, id: Name("foo"), ctx: Load, @@ -354,9 +406,11 @@ Module( cases: [ MatchCase { range: 214..225, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 219..220, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -365,9 +419,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 222..225, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 222..225, }, ), @@ -380,18 +436,22 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 226..252, subject: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 232..235, }, ), cases: [ MatchCase { range: 241..252, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 246..247, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -400,9 +460,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 249..252, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 249..252, }, ), @@ -415,13 +477,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 253..283, subject: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 259..266, op: Not, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 263..266, id: Name("foo"), ctx: Load, @@ -432,9 +497,11 @@ Module( cases: [ MatchCase { range: 272..283, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 277..278, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -443,9 +510,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 280..283, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 280..283, }, ), @@ -458,15 +527,19 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 284..318, subject: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 290..301, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 296..301, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 296..299, id: Name("foo"), ctx: Load, @@ -474,6 +547,7 @@ Module( ), arguments: Arguments { range: 299..301, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -484,9 +558,11 @@ Module( cases: [ MatchCase { range: 307..318, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 312..313, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -495,9 +571,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 315..318, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 315..318, }, ), @@ -510,22 +588,30 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 319..357, subject: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 325..340, parameters: Some( Parameters { range: 332..335, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 332..335, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 332..335, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("foo"), range: 332..335, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -539,6 +625,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 337..340, id: Name("foo"), ctx: Load, @@ -549,9 +636,11 @@ Module( cases: [ MatchCase { range: 346..357, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 351..352, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -560,9 +649,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 354..357, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 354..357, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap index d82c8dc0e1c85..d4d62a6c6f386 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_2.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_keyword_2.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..170, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..28, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..11, id: Name("match"), ctx: Load, @@ -23,9 +25,11 @@ Module( cases: [ MatchCase { range: 17..28, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 22..23, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -34,9 +38,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -49,9 +55,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 29..56, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..39, id: Name("case"), ctx: Load, @@ -60,9 +68,11 @@ Module( cases: [ MatchCase { range: 45..56, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 50..51, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -71,9 +81,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..56, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 53..56, }, ), @@ -86,9 +98,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 57..84, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..67, id: Name("type"), ctx: Load, @@ -97,9 +111,11 @@ Module( cases: [ MatchCase { range: 73..84, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 78..79, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -108,9 +124,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..84, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, }, ), @@ -123,18 +141,22 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 85..112, subject: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 91..95, }, ), cases: [ MatchCase { range: 101..112, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 106..107, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -143,9 +165,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..112, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 109..112, }, ), @@ -158,9 +182,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 113..140, subject: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 119..123, value: true, }, @@ -168,9 +194,11 @@ Module( cases: [ MatchCase { range: 129..140, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 134..135, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -179,9 +207,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 137..140, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 137..140, }, ), @@ -194,9 +224,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 141..169, subject: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 147..152, value: false, }, @@ -204,9 +236,11 @@ Module( cases: [ MatchCase { range: 158..169, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 163..164, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -215,9 +249,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 166..169, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 166..169, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap index 1c15bbba25b50..849251e03e811 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_or_identifier.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_keyword_or_identifier.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..225, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..12, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 0..12, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..5, id: Name("match"), ctx: Load, @@ -25,9 +28,11 @@ Module( ), arguments: Arguments { range: 6..12, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 1, @@ -36,6 +41,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..11, value: Int( 2, @@ -51,13 +57,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 27..67, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 33..39, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 34..35, value: Int( 1, @@ -66,6 +75,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 37..38, value: Int( 2, @@ -80,9 +90,11 @@ Module( cases: [ MatchCase { range: 56..67, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 61..62, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -91,9 +103,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 64..67, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 64..67, }, ), @@ -106,12 +120,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..78, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 68..78, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..73, id: Name("match"), ctx: Load, @@ -119,10 +136,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 75..77, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 75..76, value: Int( 1, @@ -141,13 +160,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 93..133, subject: List( ExprList { + node_index: AtomicNodeIndex(..), range: 99..105, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 100..101, value: Int( 1, @@ -156,6 +178,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 103..104, value: Int( 2, @@ -169,9 +192,11 @@ Module( cases: [ MatchCase { range: 122..133, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 127..128, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -180,9 +205,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 130..133, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 130..133, }, ), @@ -195,12 +222,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 134..145, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 134..145, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..139, id: Name("match"), ctx: Load, @@ -209,6 +239,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..145, id: Name("foo"), ctx: Load, @@ -220,12 +251,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 160..171, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 160..171, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..165, id: Name("match"), ctx: Load, @@ -234,6 +268,7 @@ Module( op: Sub, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 168..171, id: Name("foo"), ctx: Load, @@ -245,13 +280,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 186..224, subject: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 192..196, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 193..196, id: Name("foo"), ctx: Load, @@ -262,9 +300,11 @@ Module( cases: [ MatchCase { range: 213..224, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 218..219, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -273,9 +313,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 221..224, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 221..224, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap index 965e821ef377e..7416b29f05f23 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_parentheses_terminator.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_sequence_pattern_parentheses_terminator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..56, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -23,18 +25,22 @@ Module( cases: [ MatchCase { range: 19..35, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 24..30, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 25..26, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 25..26, + node_index: AtomicNodeIndex(..), }, ), }, @@ -42,11 +48,13 @@ Module( MatchAs( PatternMatchAs { range: 28..29, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("b"), range: 28..29, + node_index: AtomicNodeIndex(..), }, ), }, @@ -58,9 +66,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..35, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 32..35, }, ), @@ -70,18 +80,22 @@ Module( }, MatchCase { range: 40..56, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 45..51, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 46..47, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 46..47, + node_index: AtomicNodeIndex(..), }, ), }, @@ -89,11 +103,13 @@ Module( MatchAs( PatternMatchAs { range: 49..50, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("b"), range: 49..50, + node_index: AtomicNodeIndex(..), }, ), }, @@ -105,9 +121,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 53..56, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 53..56, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap index df1137b501fab..91f128eafcd65 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_sequence_pattern_terminator.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/match_sequence_pattern ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..95, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..94, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..13, id: Name("subject"), ctx: Load, @@ -22,14 +25,17 @@ Module( cases: [ MatchCase { range: 19..35, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 24..25, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 24..25, + node_index: AtomicNodeIndex(..), }, ), }, @@ -37,6 +43,7 @@ Module( guard: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Load, @@ -46,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 32..35, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 32..35, }, ), @@ -58,18 +67,22 @@ Module( }, MatchCase { range: 40..54, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 45..49, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 45..46, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 45..46, + node_index: AtomicNodeIndex(..), }, ), }, @@ -77,11 +90,13 @@ Module( MatchAs( PatternMatchAs { range: 48..49, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("b"), range: 48..49, + node_index: AtomicNodeIndex(..), }, ), }, @@ -93,9 +108,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 51..54, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 51..54, }, ), @@ -105,18 +122,22 @@ Module( }, MatchCase { range: 59..78, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 64..68, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 64..65, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 64..65, + node_index: AtomicNodeIndex(..), }, ), }, @@ -124,11 +145,13 @@ Module( MatchAs( PatternMatchAs { range: 67..68, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("b"), range: 67..68, + node_index: AtomicNodeIndex(..), }, ), }, @@ -139,6 +162,7 @@ Module( guard: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("x"), ctx: Load, @@ -148,9 +172,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 75..78, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 75..78, }, ), @@ -160,14 +186,17 @@ Module( }, MatchCase { range: 83..94, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 88..89, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 88..89, + node_index: AtomicNodeIndex(..), }, ), }, @@ -176,9 +205,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 91..94, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 91..94, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap index fd8bd905dc4ba..e0042a2cc5a75 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_subject_expr.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_stmt_subject_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..185, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..29, subject: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 6..12, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Store, @@ -25,6 +28,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 11..12, value: Int( 1, @@ -36,9 +40,11 @@ Module( cases: [ MatchCase { range: 18..29, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 23..24, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -47,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..29, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 26..29, }, ), @@ -62,12 +70,15 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 30..61, subject: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 37..43, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("x"), ctx: Store, @@ -75,6 +86,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 42..43, value: Int( 1, @@ -86,9 +98,11 @@ Module( cases: [ MatchCase { range: 50..61, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 55..56, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -97,9 +111,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), @@ -112,19 +128,24 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 121..153, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 127..136, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 127..133, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 128..133, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("x"), ctx: Load, @@ -133,6 +154,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("y"), ctx: Load, @@ -145,6 +167,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..136, id: Name("z"), ctx: Load, @@ -158,9 +181,11 @@ Module( cases: [ MatchCase { range: 142..153, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 147..148, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -169,9 +194,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 150..153, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 150..153, }, ), @@ -184,12 +211,15 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 154..184, subject: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 160..167, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("x"), ctx: Load, @@ -200,9 +230,11 @@ Module( cases: [ MatchCase { range: 173..184, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 178..179, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -211,9 +243,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 181..184, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 181..184, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap index 17ac4a2fa65ff..32f6791cd44fc 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_stmt_valid_guard_expr.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_stmt_valid_guard_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..158, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..34, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -23,14 +25,17 @@ Module( cases: [ MatchCase { range: 13..34, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 18..19, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 18..19, + node_index: AtomicNodeIndex(..), }, ), }, @@ -38,9 +43,11 @@ Module( guard: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 23..29, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("a"), ctx: Store, @@ -48,6 +55,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..29, value: Int( 1, @@ -60,9 +68,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 31..34, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 31..34, }, ), @@ -75,9 +85,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 35..79, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..42, id: Name("x"), ctx: Load, @@ -86,14 +98,17 @@ Module( cases: [ MatchCase { range: 48..79, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 53..54, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 53..54, + node_index: AtomicNodeIndex(..), }, ), }, @@ -101,15 +116,18 @@ Module( guard: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 58..74, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 63..67, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 58..59, id: Name("a"), ctx: Load, @@ -117,6 +135,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("b"), ctx: Load, @@ -128,9 +147,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 76..79, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 76..79, }, ), @@ -143,9 +164,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 80..119, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..87, id: Name("x"), ctx: Load, @@ -154,14 +177,17 @@ Module( cases: [ MatchCase { range: 93..119, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 98..99, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 98..99, + node_index: AtomicNodeIndex(..), }, ), }, @@ -169,19 +195,26 @@ Module( guard: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 103..114, parameters: Some( Parameters { range: 110..111, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 110..111, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 110..111, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 110..111, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -195,6 +228,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("b"), ctx: Load, @@ -206,9 +240,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 116..119, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 116..119, }, ), @@ -221,9 +257,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 120..157, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..127, id: Name("x"), ctx: Load, @@ -232,14 +270,17 @@ Module( cases: [ MatchCase { range: 133..157, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 138..139, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 138..139, + node_index: AtomicNodeIndex(..), }, ), }, @@ -247,10 +288,12 @@ Module( guard: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 144..151, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 150..151, id: Name("x"), ctx: Load, @@ -263,9 +306,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 154..157, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 154..157, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap index a12c28b913544..b61390df0d8bb 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/multiple_assignment_in_case_pattern.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..42, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 0..41, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 2, @@ -24,15 +26,19 @@ Module( cases: [ MatchCase { range: 13..41, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 18..36, + node_index: AtomicNodeIndex(..), patterns: [ MatchClass( PatternMatchClass { range: 18..26, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..23, id: Name("Class"), ctx: Load, @@ -40,15 +46,18 @@ Module( ), arguments: PatternArguments { range: 23..26, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 24..25, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 24..25, + node_index: AtomicNodeIndex(..), }, ), }, @@ -61,15 +70,18 @@ Module( MatchSequence( PatternMatchSequence { range: 29..32, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 30..31, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 30..31, + node_index: AtomicNodeIndex(..), }, ), }, @@ -80,11 +92,13 @@ Module( MatchAs( PatternMatchAs { range: 35..36, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 35..36, + node_index: AtomicNodeIndex(..), }, ), }, @@ -96,9 +110,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 38..41, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 38..41, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py310.py.snap index 2fb0f626fe0a0..b36ef0858ecad 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py310.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/nested_async_comprehen ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..181, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..116, is_async: true, decorator_list: [], name: Identifier { id: Name("f"), range: 54..55, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 55..57, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,12 +37,15 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 63..84, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 63..84, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..65, id: Name("_"), ctx: Load, @@ -45,8 +54,10 @@ Module( generators: [ Comprehension { range: 66..83, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("n"), ctx: Store, @@ -54,9 +65,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 75..83, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..80, id: Name("range"), ctx: Load, @@ -64,9 +77,11 @@ Module( ), arguments: Arguments { range: 80..83, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 81..82, value: Int( 3, @@ -88,12 +103,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 89..116, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 89..116, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("_"), ctx: Load, @@ -102,8 +120,10 @@ Module( generators: [ Comprehension { range: 92..115, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..103, id: Name("n"), ctx: Store, @@ -111,9 +131,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 107..115, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 107..112, id: Name("range"), ctx: Load, @@ -121,9 +143,11 @@ Module( ), arguments: Arguments { range: 112..115, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 113..114, value: Int( 3, @@ -148,16 +172,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 117..180, is_async: true, decorator_list: [], name: Identifier { id: Name("f"), range: 127..128, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 128..130, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -168,16 +197,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 136..148, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 140..141, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 141..143, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -188,9 +222,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 145..148, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 145..148, }, ), @@ -201,12 +237,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 153..180, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 153..180, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 154..155, id: Name("_"), ctx: Load, @@ -215,8 +254,10 @@ Module( generators: [ Comprehension { range: 156..179, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("n"), ctx: Store, @@ -224,9 +265,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 171..179, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 171..176, id: Name("range"), ctx: Load, @@ -234,9 +277,11 @@ Module( ), arguments: Arguments { range: 176..179, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 177..178, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py311.py.snap index b0fc9d849b97b..2a0a766dabd89 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nested_async_comprehension_py311.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/nested_async_comprehen ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..277, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..111, is_async: true, decorator_list: [], name: Identifier { id: Name("f"), range: 54..55, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 55..57, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,16 +37,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 59..111, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 66..111, elt: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 67..92, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("x"), ctx: Load, @@ -49,8 +59,10 @@ Module( generators: [ Comprehension { range: 70..91, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..81, id: Name("x"), ctx: Store, @@ -58,9 +70,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 85..91, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..88, id: Name("foo"), ctx: Load, @@ -68,9 +82,11 @@ Module( ), arguments: Arguments { range: 88..91, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 89..90, id: Name("n"), ctx: Load, @@ -90,8 +106,10 @@ Module( generators: [ Comprehension { range: 93..110, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("n"), ctx: Store, @@ -99,9 +117,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 102..110, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..107, id: Name("range"), ctx: Load, @@ -109,9 +129,11 @@ Module( ), arguments: Arguments { range: 107..110, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 108..109, value: Int( 3, @@ -137,16 +159,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 122..192, is_async: true, decorator_list: [], name: Identifier { id: Name("g"), range: 132..133, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 133..135, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -157,16 +184,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 137..192, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 144..192, elt: DictComp( ExprDictComp { + node_index: AtomicNodeIndex(..), range: 145..173, key: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("x"), ctx: Load, @@ -174,6 +205,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 149..150, value: Int( 1, @@ -183,8 +215,10 @@ Module( generators: [ Comprehension { range: 151..172, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 161..162, id: Name("x"), ctx: Store, @@ -192,9 +226,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 166..172, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..169, id: Name("foo"), ctx: Load, @@ -202,9 +238,11 @@ Module( ), arguments: Arguments { range: 169..172, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("n"), ctx: Load, @@ -224,8 +262,10 @@ Module( generators: [ Comprehension { range: 174..191, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 178..179, id: Name("n"), ctx: Store, @@ -233,9 +273,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 183..191, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 183..188, id: Name("range"), ctx: Load, @@ -243,9 +285,11 @@ Module( ), arguments: Arguments { range: 188..191, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 189..190, value: Int( 3, @@ -271,16 +315,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 200..267, is_async: true, decorator_list: [], name: Identifier { id: Name("h"), range: 210..211, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 211..213, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -291,16 +340,20 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 215..267, value: Some( ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 222..267, elt: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 223..248, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 224..225, id: Name("x"), ctx: Load, @@ -309,8 +362,10 @@ Module( generators: [ Comprehension { range: 226..247, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("x"), ctx: Store, @@ -318,9 +373,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 241..247, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 241..244, id: Name("foo"), ctx: Load, @@ -328,9 +385,11 @@ Module( ), arguments: Arguments { range: 244..247, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 245..246, id: Name("n"), ctx: Load, @@ -350,8 +409,10 @@ Module( generators: [ Comprehension { range: 249..266, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 253..254, id: Name("n"), ctx: Store, @@ -359,9 +420,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 258..266, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 258..263, id: Name("range"), ctx: Load, @@ -369,9 +432,11 @@ Module( ), arguments: Arguments { range: 263..266, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 264..265, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap index 766ff4bc236cd..76ed4f593790a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/non_duplicate_type_par ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..150, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..23, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..10, id: Name("Alias"), ctx: Store, @@ -22,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 10..13, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 11..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 11..12, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -39,9 +45,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 16..23, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..20, id: Name("list"), ctx: Load, @@ -49,6 +57,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("T"), ctx: Load, @@ -61,23 +70,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 24..43, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 28..29, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 29..32, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 30..31, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 30..31, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -88,19 +102,26 @@ Module( ), parameters: Parameters { range: 32..38, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 33..37, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 33..37, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("t"), range: 33..34, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("T"), ctx: Load, @@ -119,9 +140,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 40..43, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 40..43, }, ), @@ -132,22 +155,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 44..59, decorator_list: [], name: Identifier { id: Name("C"), range: 50..51, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 51..54, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 52..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 52..53, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -160,9 +188,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 56..59, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 56..59, }, ), @@ -173,22 +203,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 60..81, decorator_list: [], name: Identifier { id: Name("C"), range: 66..67, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 67..76, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 68..69, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 68..69, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -197,9 +232,11 @@ Module( TypeVar( TypeParamTypeVar { range: 71..72, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 71..72, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -208,9 +245,11 @@ Module( TypeVar( TypeParamTypeVar { range: 74..75, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("V"), range: 74..75, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -223,9 +262,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 78..81, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 78..81, }, ), @@ -236,9 +277,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 82..149, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 87..92, id: Name("Alias"), ctx: Store, @@ -247,13 +290,16 @@ Module( type_params: Some( TypeParams { range: 92..143, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 93..94, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 93..94, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -262,13 +308,16 @@ Module( TypeVar( TypeParamTypeVar { range: 96..102, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 96..97, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..102, id: Name("str"), ctx: Load, @@ -281,17 +330,21 @@ Module( TypeVar( TypeParamTypeVar { range: 104..119, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("V"), range: 104..105, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 107..119, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..111, id: Name("str"), ctx: Load, @@ -299,6 +352,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..118, id: Name("bytes"), ctx: Load, @@ -316,9 +370,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 121..124, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 122..124, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -326,9 +382,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 126..129, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 128..129, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -336,14 +394,17 @@ Module( TypeVar( TypeParamTypeVar { range: 131..142, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("D"), range: 131..132, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..142, id: Name("default"), ctx: Load, @@ -357,6 +418,7 @@ Module( ), value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 146..149, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_rebound_comprehension_variable.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_rebound_comprehension_variable.py.snap index 8c2d4b83431f4..3e80ba428d953 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_rebound_comprehension_variable.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_rebound_comprehension_variable.py.snap @@ -7,19 +7,24 @@ input_file: crates/ruff_python_parser/resources/inline/ok/non_rebound_comprehens ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..27, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..26, value: ListComp( ExprListComp { + node_index: AtomicNodeIndex(..), range: 0..26, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1..7, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..2, id: Name("a"), ctx: Store, @@ -27,6 +32,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..7, value: Int( 0, @@ -38,8 +44,10 @@ Module( generators: [ Comprehension { range: 8..25, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..13, id: Name("x"), ctx: Store, @@ -47,9 +55,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 17..25, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..22, id: Name("range"), ctx: Load, @@ -57,9 +67,11 @@ Module( ), arguments: Arguments { range: 22..25, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 23..24, value: Int( 0, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap new file mode 100644 index 0000000000000..c3a1f13b039e3 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..24, + body: [ + FunctionDef( + StmtFunctionDef { + node_index: AtomicNodeIndex(..), + range: 0..23, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + node_index: AtomicNodeIndex(..), + }, + type_params: None, + parameters: Parameters { + range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap index 33287486f8ae0..6ff8f3a6850ba 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap @@ -1,42 +1,76 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..28, + node_index: AtomicNodeIndex(..), + range: 0..45, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [ - Identifier { - id: Name("x"), - range: 9..10, - }, - ], - }, - ), - Nonlocal( - StmtNonlocal { - range: 11..27, - names: [ - Identifier { - id: Name("x"), - range: 20..21, - }, - Identifier { - id: Name("y"), - range: 23..24, - }, - Identifier { - id: Name("z"), - range: 26..27, - }, + FunctionDef( + StmtFunctionDef { + node_index: AtomicNodeIndex(..), + range: 0..44, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + node_index: AtomicNodeIndex(..), + }, + type_params: None, + parameters: Parameters { + range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + node_index: AtomicNodeIndex(..), + range: 28..44, + names: [ + Identifier { + id: Name("x"), + range: 37..38, + node_index: AtomicNodeIndex(..), + }, + Identifier { + id: Name("y"), + range: 40..41, + node_index: AtomicNodeIndex(..), + }, + Identifier { + id: Name("z"), + range: 43..44, + node_index: AtomicNodeIndex(..), + }, + ], + }, + ), ], }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__atom.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__atom.py.snap index 1fa2f56ed587a..c990da6f6ec14 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__atom.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__atom.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/other/atom.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..73, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 0..3, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 0..3, }, ), @@ -22,9 +24,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4..8, value: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 4..8, value: true, }, @@ -33,9 +37,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..14, value: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 9..14, value: false, }, @@ -44,9 +50,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 15..19, value: NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 15..19, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__decorator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__decorator.py.snap index fd32829f6c6af..4278651bd2531 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__decorator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@other__decorator.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/other/decorator.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..407, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..40, is_async: false, decorator_list: [ Decorator { range: 0..19, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1..19, id: Name("function_decorator"), ctx: Load, @@ -29,10 +32,14 @@ Module( name: Identifier { id: Name("test"), range: 24..28, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 28..30, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -43,6 +50,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 36..40, }, ), @@ -51,12 +59,15 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 43..80, decorator_list: [ Decorator { range: 43..59, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..59, id: Name("class_decorator"), ctx: Load, @@ -67,12 +78,14 @@ Module( name: Identifier { id: Name("Test"), range: 66..70, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 76..80, }, ), @@ -81,13 +94,16 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 83..106, is_async: false, decorator_list: [ Decorator { range: 83..93, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..93, id: Name("decorator"), ctx: Load, @@ -98,10 +114,14 @@ Module( name: Identifier { id: Name("f"), range: 98..99, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 99..101, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -112,9 +132,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 103..106, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 103..106, }, ), @@ -125,19 +147,24 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 109..128, is_async: false, decorator_list: [ Decorator { range: 109..115, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 110..115, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 110..113, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 110..111, id: Name("a"), ctx: Load, @@ -146,6 +173,7 @@ Module( attr: Identifier { id: Name("b"), range: 112..113, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -153,6 +181,7 @@ Module( attr: Identifier { id: Name("c"), range: 114..115, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -162,10 +191,14 @@ Module( name: Identifier { id: Name("f"), range: 120..121, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 121..123, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -176,9 +209,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 125..128, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 125..128, }, ), @@ -189,13 +224,16 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 131..153, is_async: false, decorator_list: [ Decorator { range: 131..133, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("a"), ctx: Load, @@ -204,14 +242,18 @@ Module( }, Decorator { range: 134..140, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 135..140, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 135..138, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 135..136, id: Name("a"), ctx: Load, @@ -220,6 +262,7 @@ Module( attr: Identifier { id: Name("b"), range: 137..138, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -227,6 +270,7 @@ Module( attr: Identifier { id: Name("c"), range: 139..140, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -236,10 +280,14 @@ Module( name: Identifier { id: Name("f"), range: 145..146, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 146..148, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -250,9 +298,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 150..153, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 150..153, }, ), @@ -263,12 +313,15 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 156..185, decorator_list: [ Decorator { range: 156..158, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 157..158, id: Name("a"), ctx: Load, @@ -277,11 +330,14 @@ Module( }, Decorator { range: 159..165, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 160..165, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 160..161, value: Int( 1, @@ -291,6 +347,7 @@ Module( op: BitOr, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 164..165, value: Int( 2, @@ -302,14 +359,18 @@ Module( }, Decorator { range: 166..172, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 167..172, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 167..170, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 167..168, id: Name("a"), ctx: Load, @@ -318,6 +379,7 @@ Module( attr: Identifier { id: Name("b"), range: 169..170, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -325,6 +387,7 @@ Module( attr: Identifier { id: Name("c"), range: 171..172, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -334,15 +397,18 @@ Module( name: Identifier { id: Name("T"), range: 179..180, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 182..185, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 182..185, }, ), @@ -353,16 +419,20 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 188..269, is_async: false, decorator_list: [ Decorator { range: 188..195, + node_index: AtomicNodeIndex(..), expression: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 189..195, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("x"), ctx: Store, @@ -370,6 +440,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 194..195, value: Int( 1, @@ -381,17 +452,21 @@ Module( }, Decorator { range: 196..213, + node_index: AtomicNodeIndex(..), expression: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 197..213, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 202..206, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 197..198, id: Name("x"), ctx: Load, @@ -399,6 +474,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 212..213, id: Name("y"), ctx: Load, @@ -409,21 +485,29 @@ Module( }, Decorator { range: 214..226, + node_index: AtomicNodeIndex(..), expression: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 215..226, parameters: Some( Parameters { range: 222..223, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 222..223, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 222..223, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 222..223, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -437,6 +521,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 225..226, id: Name("x"), ctx: Load, @@ -447,13 +532,16 @@ Module( }, Decorator { range: 227..235, + node_index: AtomicNodeIndex(..), expression: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 228..235, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 228..229, id: Name("x"), ctx: Load, @@ -461,6 +549,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 234..235, id: Name("y"), ctx: Load, @@ -472,12 +561,15 @@ Module( }, Decorator { range: 236..246, + node_index: AtomicNodeIndex(..), expression: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 238..245, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 244..245, id: Name("x"), ctx: Load, @@ -489,15 +581,19 @@ Module( }, Decorator { range: 247..256, + node_index: AtomicNodeIndex(..), expression: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 248..256, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 249..251, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 250..251, id: Name("x"), ctx: Load, @@ -508,9 +604,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 253..255, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 254..255, id: Name("y"), ctx: Load, @@ -529,10 +627,14 @@ Module( name: Identifier { id: Name("f"), range: 261..262, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 262..264, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -543,9 +645,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 266..269, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 266..269, }, ), @@ -556,16 +660,20 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 360..380, is_async: false, decorator_list: [ Decorator { range: 360..365, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 361..365, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 361..362, id: Name("x"), ctx: Load, @@ -574,6 +682,7 @@ Module( op: MatMult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 364..365, id: Name("y"), ctx: Load, @@ -586,10 +695,14 @@ Module( name: Identifier { id: Name("foo"), range: 370..373, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 373..375, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -600,9 +713,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 377..380, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 377..380, }, ), @@ -613,13 +728,16 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 383..407, is_async: false, decorator_list: [ Decorator { range: 383..385, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 384..385, id: Name("x"), ctx: Load, @@ -628,8 +746,10 @@ Module( }, Decorator { range: 388..390, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 389..390, id: Name("y"), ctx: Load, @@ -640,10 +760,14 @@ Module( name: Identifier { id: Name("foo"), range: 397..400, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 400..402, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -654,9 +778,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 404..407, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 404..407, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap index c3d622ebb0e10..af944be2fba0c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap @@ -7,33 +7,43 @@ input_file: crates/ruff_python_parser/resources/inline/ok/param_with_annotation. ``` Module( ModModule { - range: 0..84, + node_index: AtomicNodeIndex(..), + range: 0..54, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..22, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..17, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..16, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 8..11, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..16, id: Name("int"), ctx: Load, @@ -52,9 +62,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 19..22, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 19..22, }, ), @@ -65,42 +77,57 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 23..53, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 27..30, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 30..48, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 31..47, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 31..47, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 31..34, + node_index: AtomicNodeIndex(..), }, annotation: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 36..47, parameters: Some( Parameters { range: 43..44, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 43..44, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 43..44, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 43..44, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -114,6 +141,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("x"), ctx: Load, @@ -134,9 +162,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 50..53, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 50..53, }, ), @@ -145,72 +175,6 @@ Module( ], }, ), - FunctionDef( - StmtFunctionDef { - range: 54..83, - is_async: false, - decorator_list: [], - name: Identifier { - id: Name("foo"), - range: 58..61, - }, - type_params: None, - parameters: Parameters { - range: 61..78, - posonlyargs: [], - args: [ - ParameterWithDefault { - range: 62..77, - parameter: Parameter { - range: 62..77, - name: Identifier { - id: Name("arg"), - range: 62..65, - }, - annotation: Some( - Named( - ExprNamed { - range: 68..76, - target: Name( - ExprName { - range: 68..69, - id: Name("x"), - ctx: Store, - }, - ), - value: Name( - ExprName { - range: 73..76, - id: Name("int"), - ctx: Load, - }, - ), - }, - ), - ), - }, - default: None, - }, - ], - vararg: None, - kwonlyargs: [], - kwarg: None, - }, - returns: None, - body: [ - Expr( - StmtExpr { - range: 80..83, - value: EllipsisLiteral( - ExprEllipsisLiteral { - range: 80..83, - }, - ), - }, - ), - ], - }, - ), ], }, ) diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_default.py.snap index 62383e628332c..7951972b14ba8 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_default.py.snap @@ -1,55 +1,70 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/param_with_default.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..111, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..27, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..22, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..21, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 10..21, parameters: Some( Parameters { range: 17..18, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 17..18, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 17..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 17..18, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -63,6 +78,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("y"), ctx: Load, @@ -81,9 +97,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 24..27, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 24..27, }, ), @@ -94,40 +112,51 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 28..60, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 32..35, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 35..55, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 36..54, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 36..37, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 36..37, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 38..54, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 43..47, value: true, }, ), body: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 38..39, value: Int( 1, @@ -136,6 +165,7 @@ Module( ), orelse: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 53..54, value: Int( 2, @@ -155,9 +185,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..60, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 57..60, }, ), @@ -168,34 +200,44 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 61..84, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 65..68, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 68..79, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 69..78, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 69..70, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 69..70, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 71..78, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("y"), ctx: Load, @@ -214,9 +256,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..84, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, }, ), @@ -227,35 +271,45 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 85..110, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 89..92, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 92..105, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 93..104, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 93..94, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 93..94, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 96..103, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 102..103, id: Name("y"), ctx: Load, @@ -275,9 +329,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 107..110, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 107..110, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation.py.snap index bcf3af426dfa2..a9987458b010f 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation.py.snap @@ -1,45 +1,55 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/param_with_star_annotation.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..67, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..31, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..26, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 8..25, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 9..13, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 15..25, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 16..25, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..19, id: Name("int"), ctx: Load, @@ -48,6 +58,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..25, id: Name("str"), ctx: Load, @@ -68,9 +79,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 28..31, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 28..31, }, ), @@ -81,36 +94,46 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 32..66, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 36..39, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 39..61, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 40..60, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 41..45, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 47..60, value: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 49..59, op: Or, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..52, id: Name("int"), ctx: Load, @@ -118,6 +141,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..59, id: Name("str"), ctx: Load, @@ -139,9 +163,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 63..66, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 63..66, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap index ad415e1657979..8e1d5b24be1ba 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py310.py.snap @@ -7,31 +7,38 @@ input_file: crates/ruff_python_parser/resources/inline/ok/param_with_star_annota ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..432, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 169..206, module: Some( Identifier { id: Name("typing"), range: 174..180, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 188..197, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Annotated"), range: 188..197, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 199..206, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Literal"), range: 199..206, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -41,28 +48,36 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 207..230, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 211..214, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 214..225, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 215..224, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 216..220, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..224, id: Name("Ts"), ctx: Load, @@ -78,9 +93,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 227..230, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 227..230, }, ), @@ -91,31 +108,40 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 231..295, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 235..238, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 238..290, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 239..289, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 240..241, + node_index: AtomicNodeIndex(..), }, annotation: Some( Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 243..289, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 243..250, id: Name("Literal"), ctx: Load, @@ -123,11 +149,13 @@ Module( ), slice: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 251..288, value: StringLiteralValue { inner: Single( StringLiteral { range: 251..288, + node_index: AtomicNodeIndex(..), value: "this should allow arbitrary strings", flags: StringLiteralFlags { quote_style: Double, @@ -152,9 +180,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 292..295, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 292..295, }, ), @@ -165,31 +195,40 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 296..367, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 300..303, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 303..362, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 304..361, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 305..306, + node_index: AtomicNodeIndex(..), }, annotation: Some( Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 308..361, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 308..317, id: Name("Annotated"), ctx: Load, @@ -197,10 +236,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 318..360, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 318..321, id: Name("str"), ctx: Load, @@ -208,11 +249,13 @@ Module( ), StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 323..360, value: StringLiteralValue { inner: Single( StringLiteral { range: 323..360, + node_index: AtomicNodeIndex(..), value: "this should allow arbitrary strings", flags: StringLiteralFlags { quote_style: Double, @@ -242,9 +285,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 364..367, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 364..367, }, ), @@ -255,28 +300,36 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 368..405, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 372..375, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 375..400, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 376..386, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 377..381, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 383..386, id: Name("str"), ctx: Load, @@ -289,13 +342,16 @@ Module( kwarg: Some( Parameter { range: 388..399, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwds"), range: 390..394, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 396..399, id: Name("int"), ctx: Load, @@ -309,9 +365,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 402..405, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 402..405, }, ), @@ -322,31 +380,40 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 406..431, is_async: false, decorator_list: [], name: Identifier { id: Name("union"), range: 410..415, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 415..426, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 416..425, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 417..418, + node_index: AtomicNodeIndex(..), }, annotation: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 420..425, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 420..421, id: Name("A"), ctx: Load, @@ -355,6 +422,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 424..425, id: Name("B"), ctx: Load, @@ -372,9 +440,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 428..431, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 428..431, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap index 7156b308c55d2..0f1eed04f9f26 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_star_annotation_py311.py.snap @@ -7,35 +7,45 @@ input_file: crates/ruff_python_parser/resources/inline/ok/param_with_star_annota ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..69, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..68, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 48..51, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 51..63, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 52..62, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 53..57, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 59..62, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..62, id: Name("Ts"), ctx: Load, @@ -54,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 65..68, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 65..68, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_non_default_after_star.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_non_default_after_star.py.snap index 4c6a5faaeb102..2a4bdc6a75a1b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_non_default_after_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_non_default_after_star.py.snap @@ -1,42 +1,51 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/params_non_default_after_star.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..72, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..33, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..28, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 8..12, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 8..9, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 8..9, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 10..12, value: Int( 10, @@ -50,11 +59,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 17..18, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 17..18, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 17..18, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -62,17 +74,21 @@ Module( }, ParameterWithDefault { range: 20..24, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 20..21, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 20..21, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 22..24, value: Int( 11, @@ -83,11 +99,14 @@ Module( }, ParameterWithDefault { range: 26..27, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 26..27, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 26..27, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -100,9 +119,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 30..33, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 30..33, }, ), @@ -113,31 +134,40 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 34..71, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 38..41, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 41..66, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 42..46, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 42..43, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 42..43, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 44..46, value: Int( 10, @@ -150,9 +180,11 @@ Module( vararg: Some( Parameter { range: 48..53, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 49..53, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -160,11 +192,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 55..56, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 55..56, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 55..56, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -172,17 +207,21 @@ Module( }, ParameterWithDefault { range: 58..62, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 58..59, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 58..59, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 60..62, value: Int( 11, @@ -193,11 +232,14 @@ Module( }, ParameterWithDefault { range: 64..65, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 64..65, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 64..65, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -210,9 +252,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 68..71, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 68..71, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_seen_keyword_only_param_after_star.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_seen_keyword_only_param_after_star.py.snap index a01d1c06653ed..458f38d3292e0 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_seen_keyword_only_param_after_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@params_seen_keyword_only_param_after_star.py.snap @@ -1,38 +1,46 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/params_seen_keyword_only_param_after_star.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..61, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 4..7, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 7..23, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 11..12, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 11..12, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 11..12, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -42,9 +50,11 @@ Module( kwarg: Some( Parameter { range: 14..22, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 16..22, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -54,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -67,33 +79,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 29..60, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 33..36, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 36..55, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 40..44, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 40..41, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 40..41, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 42..44, value: Int( 10, @@ -106,9 +127,11 @@ Module( kwarg: Some( Parameter { range: 46..54, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 48..54, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -118,9 +141,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..60, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 57..60, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_context_manager_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_context_manager_py39.py.snap index fa05d59e24897..b8b9b08747d5b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_context_manager_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_context_manager_py39.py.snap @@ -7,17 +7,21 @@ input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_context_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..126, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 43..73, is_async: false, items: [ WithItem { range: 49..57, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..52, id: Name("foo"), ctx: Load, @@ -26,6 +30,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Store, @@ -35,8 +40,10 @@ Module( }, WithItem { range: 59..67, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..62, id: Name("bar"), ctx: Load, @@ -45,6 +52,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("y"), ctx: Store, @@ -56,9 +64,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 70..73, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 70..73, }, ), @@ -69,13 +79,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 74..99, is_async: false, items: [ WithItem { range: 80..83, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..83, id: Name("foo"), ctx: Load, @@ -85,8 +98,10 @@ Module( }, WithItem { range: 85..93, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..88, id: Name("bar"), ctx: Load, @@ -95,6 +110,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..93, id: Name("y"), ctx: Store, @@ -106,9 +122,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 96..99, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 96..99, }, ), @@ -119,13 +137,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 100..125, is_async: false, items: [ WithItem { range: 106..114, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..109, id: Name("foo"), ctx: Load, @@ -134,6 +155,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..114, id: Name("x"), ctx: Store, @@ -143,8 +165,10 @@ Module( }, WithItem { range: 116..119, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..119, id: Name("bar"), ctx: Load, @@ -156,9 +180,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 122..125, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 122..125, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap index bb42e78d8c5a9..15feb12d3839c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_kwarg_py37.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_kwarg_py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..52, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..51, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 43..51, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("f"), ctx: Load, @@ -24,18 +28,22 @@ Module( ), arguments: Arguments { range: 44..51, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 45..50, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 46..47, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 49..50, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap index c8e09fd54a995..b5b5b300e52a1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_index_py38.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_named_ex ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..55, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..54, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 43..54, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..46, id: Name("lst"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), slice: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 48..52, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..49, id: Name("x"), ctx: Store, @@ -34,6 +40,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap index 18a6f2406c045..c1f65ffc1dd2a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_named_expr_py38.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_named_ex ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..92, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..59, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 43..59, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 45..51, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..46, id: Name("x"), ctx: Store, @@ -28,6 +33,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 50..51, value: Int( 1, @@ -38,6 +44,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 54..55, value: Int( 2, @@ -46,6 +53,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 57..58, value: Int( 3, @@ -59,15 +67,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 60..91, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 60..91, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 62..71, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..66, id: Name("last"), ctx: Store, @@ -75,6 +87,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("x"), ctx: Load, @@ -85,8 +98,10 @@ Module( generators: [ Comprehension { range: 73..90, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("x"), ctx: Store, @@ -94,9 +109,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 82..90, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..87, id: Name("range"), ctx: Load, @@ -104,9 +121,11 @@ Module( ), arguments: Arguments { range: 87..90, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 88..89, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_star_index_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_star_index_py310.py.snap index 10df276256e0c..a1f2cd011db2c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_star_index_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@parenthesized_star_index_py310.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_star_index_py310.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..94, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 44..93, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 44..89, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..47, id: Name("out"), ctx: Load, @@ -26,19 +29,24 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 48..88, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 49..81, value: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 50..81, elt: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 51..62, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 51..56, id: Name("slice"), ctx: Load, @@ -46,9 +54,11 @@ Module( ), arguments: Arguments { range: 56..62, + node_index: AtomicNodeIndex(..), args: [ NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 57..61, }, ), @@ -60,8 +70,10 @@ Module( generators: [ Comprehension { range: 63..80, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("_"), ctx: Store, @@ -69,9 +81,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 72..80, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..77, id: Name("range"), ctx: Load, @@ -79,9 +93,11 @@ Module( ), arguments: Arguments { range: 77..80, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 78..79, value: Int( 2, @@ -105,9 +121,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 83..87, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..87, id: Name("ind"), ctx: Load, @@ -127,6 +145,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 92..93, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap index d4dcb42151531..6f728ba6daf91 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap @@ -7,36 +7,44 @@ input_file: crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311. ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..278, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..72, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 44..72, value: FStringValue { inner: Single( FString( FString { range: 44..72, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..52, + node_index: AtomicNodeIndex(..), value: "outer ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 52..71, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 53..70, value: StringLiteralValue { inner: Single( StringLiteral { range: 53..70, + node_index: AtomicNodeIndex(..), value: "# not a comment", flags: StringLiteralFlags { quote_style: Single, @@ -69,27 +77,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 73..106, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 73..106, value: FStringValue { inner: Single( FString( FString { range: 73..106, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 75..81, + node_index: AtomicNodeIndex(..), value: "outer ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 81..105, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("x"), ctx: Load, @@ -98,19 +112,23 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 84..104, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 84..103, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 85..102, value: StringLiteralValue { inner: Single( StringLiteral { range: 85..102, + node_index: AtomicNodeIndex(..), value: "# not a comment", flags: StringLiteralFlags { quote_style: Double, @@ -128,8 +146,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 103..104, + node_index: AtomicNodeIndex(..), value: " ", }, ), @@ -154,50 +173,62 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 107..147, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 107..147, value: FStringValue { inner: Single( FString( FString { range: 107..147, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 111..144, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 112..143, value: FStringValue { inner: Single( FString( FString { range: 112..143, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 116..140, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 117..139, value: FStringValue { inner: Single( FString( FString { range: 117..139, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 119..138, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 120..137, value: StringLiteralValue { inner: Single( StringLiteral { range: 120..137, + node_index: AtomicNodeIndex(..), value: "# not a comment", flags: StringLiteralFlags { quote_style: Double, @@ -264,78 +295,96 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..230, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 148..230, value: FStringValue { inner: Single( FString( FString { range: 148..230, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 152..208, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 153..207, value: FStringValue { inner: Single( FString( FString { range: 153..207, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 157..177, + node_index: AtomicNodeIndex(..), value: "# before expression ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 177..204, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 178..203, value: FStringValue { inner: Single( FString( FString { range: 178..203, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 180..185, + node_index: AtomicNodeIndex(..), value: "# aro", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 185..197, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 186..196, value: FStringValue { inner: Single( FString( FString { range: 186..196, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 188..189, + node_index: AtomicNodeIndex(..), value: "#", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 189..194, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 190..193, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 190..191, value: Int( 1, @@ -345,6 +394,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 192..193, value: Int( 1, @@ -359,8 +409,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 194..195, + node_index: AtomicNodeIndex(..), value: "#", }, ), @@ -382,8 +433,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 197..202, + node_index: AtomicNodeIndex(..), value: "und #", }, ), @@ -422,8 +474,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 208..227, + node_index: AtomicNodeIndex(..), value: " # after expression", }, ), @@ -443,27 +496,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 231..263, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 231..263, value: FStringValue { inner: Single( FString( FString { range: 231..263, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 233..254, + node_index: AtomicNodeIndex(..), value: "escape outside of \t ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 254..260, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 255..259, id: Name("expr"), ctx: Load, @@ -475,8 +534,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 260..262, + node_index: AtomicNodeIndex(..), value: "\n", }, ), @@ -496,19 +556,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 264..277, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 264..277, value: FStringValue { inner: Single( FString( FString { range: 264..277, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 266..276, + node_index: AtomicNodeIndex(..), value: "test\"abcd", }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap index c9eea808228f5..aad89368d1754 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap @@ -7,34 +7,42 @@ input_file: crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py312. ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..403, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..74, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 44..74, value: FStringValue { inner: Single( FString( FString { range: 44..74, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..58, + node_index: AtomicNodeIndex(..), value: "Magic wand: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 58..73, + node_index: AtomicNodeIndex(..), expression: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 60..71, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 60..63, id: Name("bag"), ctx: Load, @@ -42,11 +50,13 @@ Module( ), slice: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 64..70, value: StringLiteralValue { inner: Single( StringLiteral { range: 64..70, + node_index: AtomicNodeIndex(..), value: "wand", flags: StringLiteralFlags { quote_style: Single, @@ -82,32 +92,40 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 95..112, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 95..112, value: FStringValue { inner: Single( FString( FString { range: 95..112, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 97..111, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 98..110, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 98..107, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 98..102, value: StringLiteralValue { inner: Single( StringLiteral { range: 98..102, + node_index: AtomicNodeIndex(..), value: "\n", flags: StringLiteralFlags { quote_style: Single, @@ -122,15 +140,18 @@ Module( attr: Identifier { id: Name("join"), range: 103..107, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 107..110, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("a"), ctx: Load, @@ -162,30 +183,37 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..220, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 148..220, value: FStringValue { inner: Single( FString( FString { range: 148..220, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 152..169, + node_index: AtomicNodeIndex(..), value: "A complex trick: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 169..217, + node_index: AtomicNodeIndex(..), expression: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 175..185, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 175..178, id: Name("bag"), ctx: Load, @@ -193,11 +221,13 @@ Module( ), slice: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 179..184, value: StringLiteralValue { inner: Single( StringLiteral { range: 179..184, + node_index: AtomicNodeIndex(..), value: "bag", flags: StringLiteralFlags { quote_style: Single, @@ -233,84 +263,105 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 221..254, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 221..254, value: FStringValue { inner: Single( FString( FString { range: 221..254, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 223..253, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 224..252, value: FStringValue { inner: Single( FString( FString { range: 224..252, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 226..251, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 227..250, value: FStringValue { inner: Single( FString( FString { range: 227..250, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 229..249, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 230..248, value: FStringValue { inner: Single( FString( FString { range: 230..248, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 232..247, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 233..246, value: FStringValue { inner: Single( FString( FString { range: 233..246, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 235..245, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 236..244, value: FStringValue { inner: Single( FString( FString { range: 236..244, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 238..243, + node_index: AtomicNodeIndex(..), expression: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 239..242, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 239..240, value: Int( 1, @@ -320,6 +371,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 241..242, value: Int( 1, @@ -434,38 +486,47 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 276..310, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 276..310, value: FStringValue { inner: Single( FString( FString { range: 276..310, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 278..303, + node_index: AtomicNodeIndex(..), expression: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 279..302, value: FStringValue { inner: Single( FString( FString { range: 279..302, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 283..293, + node_index: AtomicNodeIndex(..), expression: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 284..292, value: StringLiteralValue { inner: Single( StringLiteral { range: 284..292, + node_index: AtomicNodeIndex(..), value: "nested", flags: StringLiteralFlags { quote_style: Double, @@ -483,8 +544,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 293..299, + node_index: AtomicNodeIndex(..), value: " inner", }, ), @@ -506,8 +568,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 303..309, + node_index: AtomicNodeIndex(..), value: " outer", }, ), @@ -527,27 +590,33 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 336..359, value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 336..359, value: FStringValue { inner: Single( FString( FString { range: 336..359, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 338..343, + node_index: AtomicNodeIndex(..), value: "test ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 343..353, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 344..345, id: Name("a"), ctx: Load, @@ -559,8 +628,9 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 353..358, + node_index: AtomicNodeIndex(..), value: " more", }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap new file mode 100644 index 0000000000000..6ad4b1584c3e7 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap @@ -0,0 +1,654 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..403, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..74, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..74, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..74, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 46..58, + node_index: AtomicNodeIndex(..), + value: "Magic wand: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 58..73, + node_index: AtomicNodeIndex(..), + expression: Subscript( + ExprSubscript { + node_index: AtomicNodeIndex(..), + range: 60..71, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 60..63, + id: Name("bag"), + ctx: Load, + }, + ), + slice: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 64..70, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 64..70, + node_index: AtomicNodeIndex(..), + value: "wand", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 95..112, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 95..112, + value: TStringValue { + inner: Single( + TString( + TString { + range: 95..112, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 97..111, + node_index: AtomicNodeIndex(..), + expression: Call( + ExprCall { + node_index: AtomicNodeIndex(..), + range: 98..110, + func: Attribute( + ExprAttribute { + node_index: AtomicNodeIndex(..), + range: 98..107, + value: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 98..102, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 98..102, + node_index: AtomicNodeIndex(..), + value: "\n", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + attr: Identifier { + id: Name("join"), + range: 103..107, + node_index: AtomicNodeIndex(..), + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 107..110, + node_index: AtomicNodeIndex(..), + args: [ + Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 108..109, + id: Name("a"), + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 148..220, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 148..220, + value: TStringValue { + inner: Single( + TString( + TString { + range: 148..220, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 152..169, + node_index: AtomicNodeIndex(..), + value: "A complex trick: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 169..217, + node_index: AtomicNodeIndex(..), + expression: Subscript( + ExprSubscript { + node_index: AtomicNodeIndex(..), + range: 175..185, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 175..178, + id: Name("bag"), + ctx: Load, + }, + ), + slice: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 179..184, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 179..184, + node_index: AtomicNodeIndex(..), + value: "bag", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 221..254, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 221..254, + value: TStringValue { + inner: Single( + TString( + TString { + range: 221..254, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 223..253, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 224..252, + value: TStringValue { + inner: Single( + TString( + TString { + range: 224..252, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 226..251, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 227..250, + value: TStringValue { + inner: Single( + TString( + TString { + range: 227..250, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 229..249, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 230..248, + value: TStringValue { + inner: Single( + TString( + TString { + range: 230..248, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 232..247, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 233..246, + value: TStringValue { + inner: Single( + TString( + TString { + range: 233..246, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 235..245, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 236..244, + value: TStringValue { + inner: Single( + TString( + TString { + range: 236..244, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 238..243, + node_index: AtomicNodeIndex(..), + expression: BinOp( + ExprBinOp { + node_index: AtomicNodeIndex(..), + range: 239..242, + left: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 239..240, + value: Int( + 1, + ), + }, + ), + op: Add, + right: NumberLiteral( + ExprNumberLiteral { + node_index: AtomicNodeIndex(..), + range: 241..242, + value: Int( + 1, + ), + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 276..310, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 276..310, + value: TStringValue { + inner: Single( + TString( + TString { + range: 276..310, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 278..303, + node_index: AtomicNodeIndex(..), + expression: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 279..302, + value: TStringValue { + inner: Single( + TString( + TString { + range: 279..302, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 283..293, + node_index: AtomicNodeIndex(..), + expression: StringLiteral( + ExprStringLiteral { + node_index: AtomicNodeIndex(..), + range: 284..292, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 284..292, + node_index: AtomicNodeIndex(..), + value: "nested", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 293..299, + node_index: AtomicNodeIndex(..), + value: " inner", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 303..309, + node_index: AtomicNodeIndex(..), + value: " outer", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 336..359, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 336..359, + value: TStringValue { + inner: Single( + TString( + TString { + range: 336..359, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 338..343, + node_index: AtomicNodeIndex(..), + value: "test ", + }, + ), + Interpolation( + InterpolatedElement { + range: 343..353, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 344..345, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 353..358, + node_index: AtomicNodeIndex(..), + value: " more", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap index 2e5f4b9e98a9a..69b218e41941d 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pos_only_py38.py.snap @@ -7,28 +7,37 @@ input_file: crates/ruff_python_parser/resources/inline/ok/pos_only_py38.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..62, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 43..61, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 47..50, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 50..56, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 51..52, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 51..52, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 51..52, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -44,9 +53,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@read_from_debug.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@read_from_debug.py.snap index 5bfd7c4128930..7d3b052d80b2c 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@read_from_debug.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@read_from_debug.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/read_from_debug.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..32, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..17, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..12, id: Name("__debug__"), ctx: Load, @@ -22,9 +25,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 14..17, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 14..17, }, ), @@ -36,10 +41,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 18..31, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("x"), ctx: Store, @@ -48,6 +55,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..31, id: Name("__debug__"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_in_block.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_in_block.py.snap index a819e56517279..46c8be7e44698 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_in_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_in_block.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/simple_stmts_in_block.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..84, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..13, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 3..7, value: true, }, @@ -22,6 +24,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 9..13, }, ), @@ -31,9 +34,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 14..27, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 17..21, value: true, }, @@ -41,6 +46,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 23..27, }, ), @@ -50,9 +56,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 29..52, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 32..36, value: true, }, @@ -60,11 +68,13 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 38..42, }, ), Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 44..52, }, ), @@ -74,9 +84,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 53..76, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 56..60, value: true, }, @@ -84,11 +96,13 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 62..66, }, ), Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 68..76, }, ), @@ -98,10 +112,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 78..83, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..79, id: Name("x"), ctx: Store, @@ -110,6 +126,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap index 2588cbfe7624a..41be29071fedc 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@simple_stmts_with_semicolons.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/simple_stmts_with_semicolons.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..51, body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 0..6, value: None, }, ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 8..16, names: [ Alias { range: 15..16, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 15..16, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -33,19 +37,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 18..33, module: Some( Identifier { id: Name("x"), range: 23..24, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 32..33, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 32..33, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -55,9 +63,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 35..36, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("z"), ctx: Load, @@ -67,9 +77,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 38..50, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("T"), ctx: Store, @@ -78,6 +90,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..50, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_star_in_tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_star_in_tuple.py.snap index ae3a5f8fc470b..4c44c9cbb38ef 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_star_in_tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_star_in_tuple.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/single_star_in_tuple.p ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..84, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..20, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 4..5, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 5..7, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -31,20 +37,25 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..20, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 9..20, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 15..20, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 16..18, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 17..18, id: Name("x"), ctx: Load, @@ -68,16 +79,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 21..42, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 25..26, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 26..28, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -88,17 +104,21 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 30..42, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 37..42, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 38..40, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("x"), ctx: Load, @@ -120,10 +140,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 43..62, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("_"), ctx: Store, @@ -131,13 +153,16 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 52..57, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 53..55, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..55, id: Name("x"), ctx: Load, @@ -154,9 +179,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 59..62, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 59..62, }, ), @@ -168,17 +195,21 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 63..83, is_async: false, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 67..72, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 68..70, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..70, id: Name("x"), ctx: Store, @@ -194,6 +225,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..78, id: Name("xs"), ctx: Load, @@ -202,9 +234,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 80..83, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 80..83, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_starred_assignment_target.py.snap index bd1289f2b39c0..b2022525334a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_starred_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@single_starred_assignment_target.py.snap @@ -7,21 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/single_starred_assignm ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..36, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..12, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 0..5, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1..3, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2..3, id: Name("a"), ctx: Store, @@ -38,10 +43,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 8..12, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 9..10, value: Int( 1, @@ -57,17 +64,21 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 13..23, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 13..16, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 13..15, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("a"), ctx: Store, @@ -84,10 +95,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 19..23, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 20..21, value: Int( 1, @@ -103,17 +116,21 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 24..35, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 24..28, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 25..27, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("a"), ctx: Store, @@ -129,10 +146,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 31..35, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 32..33, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@star_index_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@star_index_py311.py.snap index 241edaeadd8f8..daf54439c214a 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@star_index_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@star_index_py311.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/ok/star_index_py311.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..293, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 44..55, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 44..55, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..47, id: Name("lst"), ctx: Load, @@ -24,13 +28,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 48..54, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 48..54, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..54, id: Name("index"), ctx: Load, @@ -51,22 +58,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 72..112, decorator_list: [], name: Identifier { id: Name("Array"), range: 78..83, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 83..107, + node_index: AtomicNodeIndex(..), args: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 84..106, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..91, id: Name("Generic"), ctx: Load, @@ -74,10 +86,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 92..105, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 92..97, id: Name("DType"), ctx: Load, @@ -85,9 +99,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 99..105, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..105, id: Name("Shape"), ctx: Load, @@ -111,9 +127,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 109..112, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 109..112, }, ), @@ -124,12 +142,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..161, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 148..161, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..151, id: Name("lst"), ctx: Load, @@ -137,10 +158,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 152..160, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 152..153, id: Name("a"), ctx: Load, @@ -148,9 +171,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 155..157, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("b"), ctx: Load, @@ -161,6 +186,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 159..160, id: Name("c"), ctx: Load, @@ -178,12 +204,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 185..198, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 185..198, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..188, id: Name("lst"), ctx: Load, @@ -191,10 +220,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 189..197, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("a"), ctx: Load, @@ -202,6 +233,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 192..193, id: Name("b"), ctx: Load, @@ -209,9 +241,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 195..197, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 196..197, id: Name("c"), ctx: Load, @@ -232,12 +266,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 222..233, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 222..233, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..225, id: Name("lst"), ctx: Load, @@ -245,13 +282,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 226..232, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 226..228, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 227..228, id: Name("a"), ctx: Load, @@ -262,9 +302,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 230..232, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 231..232, id: Name("b"), ctx: Load, @@ -285,12 +327,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 254..271, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 254..271, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 254..259, id: Name("array"), ctx: Load, @@ -298,14 +343,17 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 260..270, elts: [ Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 260..263, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 260..261, value: Int( 3, @@ -316,6 +364,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 262..263, value: Int( 5, @@ -328,9 +377,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 265..270, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..270, id: Name("idxs"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap index 48d299be48416..df0e3f80ed7b3 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/ambiguous_lpar_with_items.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..3620, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 588..604, is_async: false, items: [ WithItem { range: 594..598, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 594..598, id: Name("item"), ctx: Load, @@ -30,9 +33,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 601..604, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 601..604, }, ), @@ -43,13 +48,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 605..622, is_async: false, items: [ WithItem { range: 611..615, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 611..615, id: Name("item"), ctx: Load, @@ -61,9 +69,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 619..622, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 619..622, }, ), @@ -74,13 +84,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 648..668, is_async: false, items: [ WithItem { range: 654..662, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 656..660, id: Name("item"), ctx: Load, @@ -92,9 +105,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 665..668, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 665..668, }, ), @@ -105,13 +120,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 669..693, is_async: false, items: [ WithItem { range: 675..680, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 675..680, id: Name("item1"), ctx: Load, @@ -121,8 +139,10 @@ Module( }, WithItem { range: 682..687, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 682..687, id: Name("item2"), ctx: Load, @@ -134,9 +154,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 690..693, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 690..693, }, ), @@ -147,13 +169,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 694..719, is_async: false, items: [ WithItem { range: 700..705, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 700..705, id: Name("item1"), ctx: Load, @@ -163,8 +188,10 @@ Module( }, WithItem { range: 707..712, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 707..712, id: Name("item2"), ctx: Load, @@ -176,9 +203,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 716..719, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 716..719, }, ), @@ -189,13 +218,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 745..794, is_async: false, items: [ WithItem { range: 751..758, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 752..757, id: Name("item1"), ctx: Load, @@ -205,8 +237,10 @@ Module( }, WithItem { range: 760..767, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 761..766, id: Name("item2"), ctx: Load, @@ -216,8 +250,10 @@ Module( }, WithItem { range: 769..779, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 769..774, id: Name("item3"), ctx: Load, @@ -226,6 +262,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 778..779, id: Name("f"), ctx: Store, @@ -235,8 +272,10 @@ Module( }, WithItem { range: 781..788, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 782..787, id: Name("item4"), ctx: Load, @@ -248,9 +287,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 791..794, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 791..794, }, ), @@ -261,17 +302,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 795..828, is_async: false, items: [ WithItem { range: 801..815, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 801..815, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 802..807, id: Name("item1"), ctx: Load, @@ -279,6 +324,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 809..814, id: Name("item2"), ctx: Load, @@ -293,8 +339,10 @@ Module( }, WithItem { range: 817..822, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 817..822, id: Name("item3"), ctx: Load, @@ -306,9 +354,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 825..828, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 825..828, }, ), @@ -319,17 +369,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 829..852, is_async: false, items: [ WithItem { range: 835..846, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 835..841, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 836..837, id: Name("x"), ctx: Load, @@ -337,6 +391,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 839..840, id: Name("y"), ctx: Load, @@ -350,6 +405,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 845..846, id: Name("f"), ctx: Store, @@ -361,9 +417,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 849..852, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 849..852, }, ), @@ -374,13 +432,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 853..889, is_async: false, items: [ WithItem { range: 859..870, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 859..864, id: Name("item1"), ctx: Load, @@ -389,6 +450,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 868..870, id: Name("f1"), ctx: Store, @@ -398,8 +460,10 @@ Module( }, WithItem { range: 872..883, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 872..877, id: Name("item2"), ctx: Load, @@ -408,6 +472,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 881..883, id: Name("f2"), ctx: Store, @@ -419,9 +484,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 886..889, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 886..889, }, ), @@ -432,13 +499,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 890..927, is_async: false, items: [ WithItem { range: 896..907, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 896..901, id: Name("item1"), ctx: Load, @@ -447,6 +517,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 905..907, id: Name("f1"), ctx: Store, @@ -456,8 +527,10 @@ Module( }, WithItem { range: 909..920, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 909..914, id: Name("item2"), ctx: Load, @@ -466,6 +539,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 918..920, id: Name("f2"), ctx: Store, @@ -477,9 +551,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 924..927, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 924..927, }, ), @@ -490,16 +566,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 953..976, is_async: false, items: [ WithItem { range: 959..969, + node_index: AtomicNodeIndex(..), context_expr: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 959..969, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 959..963, id: Name("item"), ctx: Load, @@ -511,6 +591,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 967..969, value: Int( 10, @@ -526,9 +607,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 973..976, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 973..976, }, ), @@ -539,16 +622,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 977..1001, is_async: false, items: [ WithItem { range: 983..995, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 984..994, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 984..988, id: Name("item"), ctx: Store, @@ -556,6 +643,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 992..994, value: Int( 10, @@ -570,9 +658,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 998..1001, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 998..1001, }, ), @@ -583,20 +673,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1002..1027, is_async: false, items: [ WithItem { range: 1008..1021, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1008..1021, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1009..1019, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1009..1013, id: Name("item"), ctx: Store, @@ -604,6 +699,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1017..1019, value: Int( 10, @@ -623,9 +719,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1024..1027, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1024..1027, }, ), @@ -636,20 +734,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1028..1048, is_async: false, items: [ WithItem { range: 1034..1042, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1034..1042, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1035..1040, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1036..1040, id: Name("item"), ctx: Load, @@ -669,9 +772,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1045..1048, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1045..1048, }, ), @@ -682,16 +787,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1049..1081, is_async: false, items: [ WithItem { range: 1055..1068, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1056..1067, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1056..1061, id: Name("item1"), ctx: Store, @@ -699,6 +808,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1065..1067, value: Int( 10, @@ -711,8 +821,10 @@ Module( }, WithItem { range: 1070..1075, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1070..1075, id: Name("item2"), ctx: Load, @@ -724,9 +836,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1078..1081, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1078..1081, }, ), @@ -737,13 +851,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1082..1119, is_async: false, items: [ WithItem { range: 1088..1098, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1088..1093, id: Name("item1"), ctx: Load, @@ -752,6 +869,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1097..1098, id: Name("f"), ctx: Store, @@ -761,11 +879,14 @@ Module( }, WithItem { range: 1100..1113, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1101..1112, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1101..1106, id: Name("item2"), ctx: Store, @@ -773,6 +894,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1110..1112, value: Int( 10, @@ -787,9 +909,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1116..1119, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1116..1119, }, ), @@ -800,16 +924,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1120..1137, is_async: false, items: [ WithItem { range: 1126..1131, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1126..1131, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1126..1129, id: Name("foo"), ctx: Load, @@ -817,6 +945,7 @@ Module( ), arguments: Arguments { range: 1129..1131, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -828,9 +957,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1134..1137, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1134..1137, }, ), @@ -841,16 +972,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1138..1156, is_async: false, items: [ WithItem { range: 1144..1149, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1144..1149, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1144..1147, id: Name("foo"), ctx: Load, @@ -858,6 +993,7 @@ Module( ), arguments: Arguments { range: 1147..1149, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -869,9 +1005,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1153..1156, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1153..1156, }, ), @@ -882,16 +1020,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1157..1179, is_async: false, items: [ WithItem { range: 1163..1173, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1163..1168, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1163..1166, id: Name("foo"), ctx: Load, @@ -899,6 +1041,7 @@ Module( ), arguments: Arguments { range: 1166..1168, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -907,6 +1050,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1172..1173, id: Name("f"), ctx: Store, @@ -918,9 +1062,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1176..1179, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1176..1179, }, ), @@ -931,25 +1077,31 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1180..1207, is_async: false, items: [ WithItem { range: 1186..1201, + node_index: AtomicNodeIndex(..), context_expr: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 1186..1201, value: FStringValue { inner: Single( FString( FString { range: 1186..1201, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 1188..1200, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1189..1193, id: Name("item"), ctx: Load, @@ -958,12 +1110,14 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 1195..1199, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 1195..1199, + node_index: AtomicNodeIndex(..), value: "= 42", }, ), @@ -990,9 +1144,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1204..1207, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1204..1207, }, ), @@ -1003,28 +1159,35 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1208..1237, is_async: false, items: [ WithItem { range: 1214..1231, + node_index: AtomicNodeIndex(..), context_expr: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 1214..1231, value: FStringValue { inner: Single( FString( FString { range: 1214..1231, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 1216..1230, + node_index: AtomicNodeIndex(..), expression: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1218..1228, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1218..1222, id: Name("item"), ctx: Store, @@ -1032,6 +1195,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1226..1228, value: Int( 42, @@ -1063,9 +1227,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1234..1237, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1234..1237, }, ), @@ -1076,16 +1242,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1238..1278, is_async: false, items: [ WithItem { range: 1244..1266, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 1244..1266, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1245..1246, id: Name("x"), ctx: Load, @@ -1094,8 +1264,10 @@ Module( generators: [ Comprehension { range: 1247..1265, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1251..1252, id: Name("x"), ctx: Store, @@ -1103,9 +1275,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1256..1265, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1256..1261, id: Name("range"), ctx: Load, @@ -1113,9 +1287,11 @@ Module( ), arguments: Arguments { range: 1261..1265, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1262..1264, value: Int( 10, @@ -1138,8 +1314,10 @@ Module( }, WithItem { range: 1268..1272, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1268..1272, id: Name("item"), ctx: Load, @@ -1151,9 +1329,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1275..1278, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1275..1278, }, ), @@ -1164,13 +1344,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1279..1319, is_async: false, items: [ WithItem { range: 1285..1289, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1285..1289, id: Name("item"), ctx: Load, @@ -1180,11 +1363,14 @@ Module( }, WithItem { range: 1291..1313, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 1291..1313, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1292..1293, id: Name("x"), ctx: Load, @@ -1193,8 +1379,10 @@ Module( generators: [ Comprehension { range: 1294..1312, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1298..1299, id: Name("x"), ctx: Store, @@ -1202,9 +1390,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1303..1312, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1303..1308, id: Name("range"), ctx: Load, @@ -1212,9 +1402,11 @@ Module( ), arguments: Arguments { range: 1308..1312, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1309..1311, value: Int( 10, @@ -1239,9 +1431,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1316..1319, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1316..1319, }, ), @@ -1252,13 +1446,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1320..1366, is_async: false, items: [ WithItem { range: 1326..1330, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1326..1330, id: Name("item"), ctx: Load, @@ -1268,11 +1465,14 @@ Module( }, WithItem { range: 1332..1354, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 1332..1354, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1333..1334, id: Name("x"), ctx: Load, @@ -1281,8 +1481,10 @@ Module( generators: [ Comprehension { range: 1335..1353, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1339..1340, id: Name("x"), ctx: Store, @@ -1290,9 +1492,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1344..1353, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1344..1349, id: Name("range"), ctx: Load, @@ -1300,9 +1504,11 @@ Module( ), arguments: Arguments { range: 1349..1353, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1350..1352, value: Int( 10, @@ -1325,8 +1531,10 @@ Module( }, WithItem { range: 1356..1360, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1356..1360, id: Name("item"), ctx: Load, @@ -1338,9 +1546,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1363..1366, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1363..1366, }, ), @@ -1351,16 +1561,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1367..1388, is_async: false, items: [ WithItem { range: 1373..1382, + node_index: AtomicNodeIndex(..), context_expr: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1373..1382, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1373..1377, id: Name("data"), ctx: Load, @@ -1368,10 +1582,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 1378..1381, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1378..1379, value: Int( 1, @@ -1382,6 +1598,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1380..1381, value: Int( 2, @@ -1401,9 +1618,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1385..1388, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1385..1388, }, ), @@ -1414,16 +1633,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1389..1415, is_async: false, items: [ WithItem { range: 1395..1409, + node_index: AtomicNodeIndex(..), context_expr: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1395..1404, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1395..1399, id: Name("data"), ctx: Load, @@ -1431,10 +1654,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 1400..1403, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1400..1401, value: Int( 1, @@ -1445,6 +1670,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1402..1403, value: Int( 2, @@ -1461,6 +1687,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1408..1409, id: Name("f"), ctx: Store, @@ -1472,9 +1699,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1412..1415, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1412..1415, }, ), @@ -1485,16 +1714,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1416..1450, is_async: false, items: [ WithItem { range: 1422..1444, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 1422..1439, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1423..1424, id: Name("x"), ctx: Load, @@ -1503,8 +1736,10 @@ Module( generators: [ Comprehension { range: 1425..1438, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1429..1430, id: Name("x"), ctx: Store, @@ -1512,6 +1747,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1434..1438, id: Name("iter"), ctx: Load, @@ -1527,6 +1763,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1443..1444, id: Name("y"), ctx: Store, @@ -1538,9 +1775,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1447..1450, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1447..1450, }, ), @@ -1551,13 +1790,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1663..1684, is_async: false, items: [ WithItem { range: 1668..1679, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1669..1673, id: Name("item"), ctx: Load, @@ -1566,6 +1808,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1678..1679, id: Name("f"), ctx: Store, @@ -1577,9 +1820,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1681..1684, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1681..1684, }, ), @@ -1590,16 +1835,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1685..1707, is_async: false, items: [ WithItem { range: 1690..1702, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1691..1701, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1691..1695, id: Name("item"), ctx: Store, @@ -1607,6 +1856,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1699..1701, value: Int( 10, @@ -1621,9 +1871,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1704..1707, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1704..1707, }, ), @@ -1634,16 +1886,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1708..1735, is_async: false, items: [ WithItem { range: 1713..1730, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1714..1724, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1714..1718, id: Name("item"), ctx: Store, @@ -1651,6 +1907,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1722..1724, value: Int( 10, @@ -1662,6 +1919,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1729..1730, id: Name("f"), ctx: Store, @@ -1673,9 +1931,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1732..1735, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1732..1735, }, ), @@ -1686,16 +1946,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1736..1762, is_async: false, items: [ WithItem { range: 1741..1757, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1744..1753, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1744..1748, id: Name("item"), ctx: Store, @@ -1703,6 +1967,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1752..1753, value: Int( 1, @@ -1717,9 +1982,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1759..1762, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1759..1762, }, ), @@ -1730,16 +1997,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1763..1793, is_async: false, items: [ WithItem { range: 1768..1781, + node_index: AtomicNodeIndex(..), context_expr: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1769..1780, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1769..1774, id: Name("item1"), ctx: Store, @@ -1747,6 +2018,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1778..1780, value: Int( 42, @@ -1759,8 +2031,10 @@ Module( }, WithItem { range: 1783..1788, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1783..1788, id: Name("item2"), ctx: Load, @@ -1772,9 +2046,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1790..1793, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1790..1793, }, ), @@ -1785,22 +2061,28 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1794..1828, is_async: false, items: [ WithItem { range: 1799..1823, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1799..1823, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1799..1821, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1800..1815, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1800..1804, id: Name("root"), ctx: Load, @@ -1809,6 +2091,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1807..1815, id: Name("filename"), ctx: Load, @@ -1819,12 +2102,14 @@ Module( attr: Identifier { id: Name("read"), range: 1817..1821, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 1821..1823, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1836,9 +2121,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1825..1828, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1825..1828, }, ), @@ -1849,22 +2136,28 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1851..1890, is_async: false, items: [ WithItem { range: 1856..1885, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1856..1880, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1856..1878, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1857..1872, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1857..1861, id: Name("root"), ctx: Load, @@ -1873,6 +2166,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1864..1872, id: Name("filename"), ctx: Load, @@ -1883,12 +2177,14 @@ Module( attr: Identifier { id: Name("read"), range: 1874..1878, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 1878..1880, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1897,6 +2193,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1884..1885, id: Name("f"), ctx: Store, @@ -1908,9 +2205,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1887..1890, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1887..1890, }, ), @@ -1921,16 +2220,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1913..1930, is_async: false, items: [ WithItem { range: 1918..1925, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1918..1925, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1919..1922, id: Name("foo"), ctx: Load, @@ -1938,6 +2241,7 @@ Module( ), arguments: Arguments { range: 1923..1925, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1949,9 +2253,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1927..1930, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1927..1930, }, ), @@ -1962,16 +2268,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1953..1975, is_async: false, items: [ WithItem { range: 1958..1970, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1958..1965, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1959..1962, id: Name("foo"), ctx: Load, @@ -1979,6 +2289,7 @@ Module( ), arguments: Arguments { range: 1963..1965, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1987,6 +2298,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1969..1970, id: Name("f"), ctx: Store, @@ -1998,9 +2310,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1972..1975, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1972..1975, }, ), @@ -2011,16 +2325,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 1998..2020, is_async: false, items: [ WithItem { range: 2003..2015, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 2004..2009, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2004..2007, id: Name("foo"), ctx: Load, @@ -2028,6 +2346,7 @@ Module( ), arguments: Arguments { range: 2007..2009, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -2036,6 +2355,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2014..2015, id: Name("f"), ctx: Store, @@ -2047,9 +2367,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2017..2020, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2017..2020, }, ), @@ -2060,16 +2382,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2021..2047, is_async: false, items: [ WithItem { range: 2026..2042, + node_index: AtomicNodeIndex(..), context_expr: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 2027..2036, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2027..2031, id: Name("data"), ctx: Load, @@ -2077,10 +2403,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 2032..2035, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2032..2033, value: Int( 1, @@ -2091,6 +2419,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2034..2035, value: Int( 2, @@ -2107,6 +2436,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2041..2042, id: Name("f"), ctx: Store, @@ -2118,9 +2448,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2044..2047, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2044..2047, }, ), @@ -2131,20 +2463,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2048..2070, is_async: false, items: [ WithItem { range: 2053..2065, + node_index: AtomicNodeIndex(..), context_expr: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 2053..2065, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2053..2062, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2054..2055, value: Int( 1, @@ -2153,6 +2490,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2057..2058, value: Int( 2, @@ -2161,6 +2499,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2060..2061, value: Int( 3, @@ -2174,6 +2513,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2063..2064, value: Int( 0, @@ -2189,9 +2529,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2067..2070, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2067..2070, }, ), @@ -2202,20 +2544,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2093..2120, is_async: false, items: [ WithItem { range: 2098..2115, + node_index: AtomicNodeIndex(..), context_expr: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 2098..2110, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2098..2107, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2099..2100, value: Int( 1, @@ -2224,6 +2571,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2102..2103, value: Int( 2, @@ -2232,6 +2580,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2105..2106, value: Int( 3, @@ -2245,6 +2594,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2108..2109, value: Int( 0, @@ -2257,6 +2607,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2114..2115, id: Name("f"), ctx: Store, @@ -2268,9 +2619,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2117..2120, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2117..2120, }, ), @@ -2281,13 +2634,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2143..2169, is_async: false, items: [ WithItem { range: 2148..2155, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2149..2154, id: Name("item1"), ctx: Load, @@ -2297,8 +2653,10 @@ Module( }, WithItem { range: 2157..2164, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2158..2163, id: Name("item2"), ctx: Load, @@ -2310,9 +2668,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2166..2169, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2166..2169, }, ), @@ -2323,16 +2683,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2170..2210, is_async: false, items: [ WithItem { range: 2175..2189, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 2176..2188, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2176..2180, id: Name("open"), ctx: Load, @@ -2340,14 +2704,17 @@ Module( ), arguments: Arguments { range: 2180..2188, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2181..2187, value: StringLiteralValue { inner: Single( StringLiteral { range: 2181..2187, + node_index: AtomicNodeIndex(..), value: "a.py", flags: StringLiteralFlags { quote_style: Single, @@ -2368,11 +2735,14 @@ Module( }, WithItem { range: 2191..2205, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 2192..2204, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2192..2196, id: Name("open"), ctx: Load, @@ -2380,14 +2750,17 @@ Module( ), arguments: Arguments { range: 2196..2204, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2197..2203, value: StringLiteralValue { inner: Single( StringLiteral { range: 2197..2203, + node_index: AtomicNodeIndex(..), value: "b.py", flags: StringLiteralFlags { quote_style: Single, @@ -2410,9 +2783,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2207..2210, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2207..2210, }, ), @@ -2423,17 +2798,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2211..2230, is_async: false, items: [ WithItem { range: 2216..2225, + node_index: AtomicNodeIndex(..), context_expr: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 2217..2224, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2223..2224, id: Name("x"), ctx: Load, @@ -2448,9 +2827,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2227..2230, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2227..2230, }, ), @@ -2461,17 +2842,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2231..2252, is_async: false, items: [ WithItem { range: 2237..2246, + node_index: AtomicNodeIndex(..), context_expr: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 2238..2245, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2244..2245, id: Name("x"), ctx: Load, @@ -2486,9 +2871,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2249..2252, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2249..2252, }, ), @@ -2499,16 +2886,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2253..2277, is_async: false, items: [ WithItem { range: 2258..2272, + node_index: AtomicNodeIndex(..), context_expr: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 2259..2271, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2270..2271, id: Name("x"), ctx: Load, @@ -2522,9 +2913,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2274..2277, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2274..2277, }, ), @@ -2535,16 +2928,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2278..2304, is_async: false, items: [ WithItem { range: 2284..2298, + node_index: AtomicNodeIndex(..), context_expr: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 2285..2297, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2296..2297, id: Name("x"), ctx: Load, @@ -2558,9 +2955,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2301..2304, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2301..2304, }, ), @@ -2571,17 +2970,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2305..2329, is_async: false, items: [ WithItem { range: 2310..2324, + node_index: AtomicNodeIndex(..), context_expr: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 2311..2318, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2317..2318, id: Name("x"), ctx: Load, @@ -2593,6 +2996,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2323..2324, id: Name("f"), ctx: Store, @@ -2604,9 +3008,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2326..2329, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2326..2329, }, ), @@ -2617,21 +3023,26 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2330..2355, is_async: false, items: [ WithItem { range: 2335..2350, + node_index: AtomicNodeIndex(..), context_expr: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 2336..2344, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2342..2344, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2342..2343, id: Name("x"), ctx: Load, @@ -2648,6 +3059,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2349..2350, id: Name("f"), ctx: Store, @@ -2659,9 +3071,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2352..2355, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2352..2355, }, ), @@ -2672,13 +3086,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2741..2753, is_async: false, items: [ WithItem { range: 2746..2748, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2746..2748, elts: [], ctx: Load, @@ -2691,9 +3108,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2750..2753, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2750..2753, }, ), @@ -2704,13 +3123,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2754..2771, is_async: false, items: [ WithItem { range: 2759..2766, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2759..2761, elts: [], ctx: Load, @@ -2720,6 +3142,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2765..2766, id: Name("f"), ctx: Store, @@ -2731,9 +3154,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2768..2771, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2768..2771, }, ), @@ -2744,20 +3169,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2772..2795, is_async: false, items: [ WithItem { range: 2777..2790, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2777..2790, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2778..2788, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2778..2782, id: Name("item"), ctx: Store, @@ -2765,6 +3195,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2786..2788, value: Int( 42, @@ -2784,9 +3215,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2792..2795, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2792..2795, }, ), @@ -2797,17 +3230,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2796..2820, is_async: false, items: [ WithItem { range: 2801..2815, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2801..2815, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2802..2803, value: Int( 1, @@ -2816,9 +3253,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2805..2814, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2805..2809, id: Name("item"), ctx: Store, @@ -2826,6 +3265,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2813..2814, value: Int( 2, @@ -2845,9 +3285,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2817..2820, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2817..2820, }, ), @@ -2858,20 +3300,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2821..2851, is_async: false, items: [ WithItem { range: 2826..2846, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2826..2846, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2827..2838, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2827..2832, id: Name("item1"), ctx: Store, @@ -2879,6 +3326,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2836..2838, value: Int( 10, @@ -2889,6 +3337,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2840..2845, id: Name("item2"), ctx: Load, @@ -2905,9 +3354,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2848..2851, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2848..2851, }, ), @@ -2918,17 +3369,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2852..2893, is_async: false, items: [ WithItem { range: 2857..2888, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2857..2883, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2858..2863, id: Name("item1"), ctx: Load, @@ -2936,9 +3391,11 @@ Module( ), Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2865..2875, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2865..2870, id: Name("item2"), ctx: Store, @@ -2946,6 +3403,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2874..2875, value: Int( 2, @@ -2956,6 +3414,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2877..2882, id: Name("item3"), ctx: Load, @@ -2969,6 +3428,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2887..2888, id: Name("f"), ctx: Store, @@ -2980,9 +3440,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2890..2893, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2890..2893, }, ), @@ -2993,17 +3455,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2894..2916, is_async: false, items: [ WithItem { range: 2899..2911, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2899..2906, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2900..2904, id: Name("item"), ctx: Load, @@ -3017,6 +3483,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2910..2911, id: Name("f"), ctx: Store, @@ -3028,9 +3495,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2913..2916, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2913..2916, }, ), @@ -3041,20 +3510,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2917..2935, is_async: false, items: [ WithItem { range: 2922..2930, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2922..2930, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 2923..2928, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2924..2928, id: Name("item"), ctx: Load, @@ -3074,9 +3548,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2932..2935, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2932..2935, }, ), @@ -3087,20 +3563,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2936..2959, is_async: false, items: [ WithItem { range: 2941..2954, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2941..2949, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 2942..2947, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2943..2947, id: Name("item"), ctx: Load, @@ -3117,6 +3598,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2953..2954, id: Name("f"), ctx: Store, @@ -3128,9 +3610,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2956..2959, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2956..2959, }, ), @@ -3141,17 +3625,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2960..2989, is_async: false, items: [ WithItem { range: 2965..2984, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2965..2979, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2966..2971, id: Name("item1"), ctx: Load, @@ -3159,6 +3647,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2973..2978, id: Name("item2"), ctx: Load, @@ -3172,6 +3661,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2983..2984, id: Name("f"), ctx: Store, @@ -3183,9 +3673,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2986..2989, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2986..2989, }, ), @@ -3196,17 +3688,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 2990..3020, is_async: false, items: [ WithItem { range: 2995..3015, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2995..3010, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2996..3001, id: Name("item1"), ctx: Load, @@ -3214,6 +3710,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3003..3008, id: Name("item2"), ctx: Load, @@ -3227,6 +3724,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3014..3015, id: Name("f"), ctx: Store, @@ -3238,9 +3736,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3017..3020, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3017..3020, }, ), @@ -3251,17 +3751,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3021..3052, is_async: false, items: [ WithItem { range: 3026..3040, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3026..3040, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3027..3032, id: Name("item1"), ctx: Load, @@ -3269,6 +3773,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3034..3039, id: Name("item2"), ctx: Load, @@ -3283,8 +3788,10 @@ Module( }, WithItem { range: 3042..3047, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3042..3047, id: Name("item3"), ctx: Load, @@ -3296,9 +3803,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3049..3052, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3049..3052, }, ), @@ -3309,21 +3818,26 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3053..3091, is_async: false, items: [ WithItem { range: 3058..3086, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3058..3081, elts: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3059..3073, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3060..3065, id: Name("item1"), ctx: Load, @@ -3331,6 +3845,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3067..3072, id: Name("item2"), ctx: Load, @@ -3343,6 +3858,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3075..3080, id: Name("item3"), ctx: Load, @@ -3356,6 +3872,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3085..3086, id: Name("f"), ctx: Store, @@ -3367,9 +3884,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3088..3091, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3088..3091, }, ), @@ -3380,17 +3899,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3092..3138, is_async: false, items: [ WithItem { range: 3097..3105, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3097..3105, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3098..3103, id: Name("item1"), ctx: Load, @@ -3405,8 +3928,10 @@ Module( }, WithItem { range: 3107..3112, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3107..3112, id: Name("item2"), ctx: Load, @@ -3416,12 +3941,15 @@ Module( }, WithItem { range: 3114..3133, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3114..3128, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3115..3120, id: Name("item3"), ctx: Load, @@ -3429,6 +3957,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3122..3127, id: Name("item4"), ctx: Load, @@ -3442,6 +3971,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3132..3133, id: Name("f"), ctx: Store, @@ -3453,9 +3983,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3135..3138, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3135..3138, }, ), @@ -3466,17 +3998,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3139..3182, is_async: false, items: [ WithItem { range: 3144..3164, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3144..3158, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3145..3150, id: Name("item1"), ctx: Load, @@ -3484,6 +4020,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3152..3157, id: Name("item2"), ctx: Load, @@ -3497,6 +4034,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3162..3164, id: Name("f1"), ctx: Store, @@ -3506,8 +4044,10 @@ Module( }, WithItem { range: 3166..3177, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3166..3171, id: Name("item3"), ctx: Load, @@ -3516,6 +4056,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3175..3177, id: Name("f2"), ctx: Store, @@ -3527,9 +4068,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3179..3182, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3179..3182, }, ), @@ -3540,17 +4083,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3183..3208, is_async: false, items: [ WithItem { range: 3188..3203, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3188..3203, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3189..3194, id: Name("item1"), ctx: Load, @@ -3558,9 +4105,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3196..3202, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3197..3202, id: Name("item2"), ctx: Load, @@ -3580,9 +4129,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3205..3208, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3205..3208, }, ), @@ -3593,17 +4144,21 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3209..3239, is_async: false, items: [ WithItem { range: 3214..3234, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3214..3229, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3215..3220, id: Name("item1"), ctx: Load, @@ -3611,9 +4166,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3222..3228, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3223..3228, id: Name("item2"), ctx: Load, @@ -3630,6 +4187,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3233..3234, id: Name("f"), ctx: Store, @@ -3641,9 +4199,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3236..3239, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3236..3239, }, ), @@ -3654,20 +4214,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3240..3271, is_async: false, items: [ WithItem { range: 3245..3266, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3245..3266, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 3246..3257, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3246..3251, id: Name("item1"), ctx: Store, @@ -3675,6 +4240,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3255..3257, value: Int( 10, @@ -3685,9 +4251,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3259..3265, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3260..3265, id: Name("item2"), ctx: Load, @@ -3707,9 +4275,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3268..3271, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3268..3271, }, ), @@ -3720,20 +4290,25 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3272..3305, is_async: false, items: [ WithItem { range: 3277..3300, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3277..3300, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 3279..3290, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3279..3284, id: Name("item1"), ctx: Store, @@ -3741,6 +4316,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3288..3290, value: Int( 10, @@ -3751,9 +4327,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 3293..3299, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3294..3299, id: Name("item2"), ctx: Load, @@ -3773,9 +4351,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3302..3305, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3302..3305, }, ), @@ -3786,16 +4366,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3510..3542, is_async: false, items: [ WithItem { range: 3515..3537, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 3515..3537, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3516..3517, id: Name("x"), ctx: Load, @@ -3804,8 +4388,10 @@ Module( generators: [ Comprehension { range: 3518..3536, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3522..3523, id: Name("x"), ctx: Store, @@ -3813,9 +4399,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3527..3536, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3527..3532, id: Name("range"), ctx: Load, @@ -3823,9 +4411,11 @@ Module( ), arguments: Arguments { range: 3532..3536, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3533..3535, value: Int( 10, @@ -3850,9 +4440,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3539..3542, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3539..3542, }, ), @@ -3863,16 +4455,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3543..3581, is_async: false, items: [ WithItem { range: 3548..3576, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 3548..3576, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3549..3550, id: Name("x"), ctx: Load, @@ -3881,8 +4477,10 @@ Module( generators: [ Comprehension { range: 3551..3575, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3561..3562, id: Name("x"), ctx: Store, @@ -3890,9 +4488,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3566..3575, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3566..3571, id: Name("range"), ctx: Load, @@ -3900,9 +4500,11 @@ Module( ), arguments: Arguments { range: 3571..3575, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3572..3574, value: Int( 10, @@ -3927,9 +4529,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3578..3581, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3578..3581, }, ), @@ -3940,16 +4544,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 3582..3620, is_async: false, items: [ WithItem { range: 3587..3609, + node_index: AtomicNodeIndex(..), context_expr: Generator( ExprGenerator { + node_index: AtomicNodeIndex(..), range: 3587..3609, elt: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3588..3589, id: Name("x"), ctx: Load, @@ -3958,8 +4566,10 @@ Module( generators: [ Comprehension { range: 3590..3608, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3594..3595, id: Name("x"), ctx: Store, @@ -3967,9 +4577,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3599..3608, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3599..3604, id: Name("range"), ctx: Load, @@ -3977,9 +4589,11 @@ Module( ), arguments: Arguments { range: 3604..3608, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3605..3607, value: Int( 10, @@ -4002,8 +4616,10 @@ Module( }, WithItem { range: 3611..3615, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3611..3615, id: Name("item"), ctx: Load, @@ -4015,9 +4631,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3617..3620, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3617..3620, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__annotated_assignment.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__annotated_assignment.py.snap index 493f3ba3d1437..4f2c579ca34c4 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__annotated_assignment.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__annotated_assignment.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/annotated_assignment.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..103, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 0..6, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -22,6 +24,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3..6, id: Name("int"), ctx: Load, @@ -33,9 +36,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 7..17, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Store, @@ -43,6 +48,7 @@ Module( ), annotation: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 10..13, id: Name("int"), ctx: Load, @@ -51,6 +57,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 16..17, value: Int( 1, @@ -63,9 +70,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 18..28, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("x"), ctx: Store, @@ -73,9 +82,11 @@ Module( ), annotation: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 23..28, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 23..24, value: Int( 1, @@ -85,6 +96,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 27..28, value: Int( 2, @@ -99,9 +111,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 29..55, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("x"), ctx: Store, @@ -109,12 +123,15 @@ Module( ), annotation: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 32..48, left: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 32..42, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..37, id: Name("tuple"), ctx: Load, @@ -122,6 +139,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..41, id: Name("int"), ctx: Load, @@ -133,6 +151,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 45..48, id: Name("int"), ctx: Load, @@ -143,10 +162,12 @@ Module( value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 51..55, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 1, @@ -164,9 +185,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 56..83, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Store, @@ -174,15 +197,18 @@ Module( ), annotation: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 59..79, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 66..70, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..62, id: Name("int"), ctx: Load, @@ -190,6 +216,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..79, id: Name("str"), ctx: Load, @@ -200,6 +227,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 82..83, value: Int( 1, @@ -212,9 +240,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 84..102, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..85, id: Name("x"), ctx: Store, @@ -222,19 +252,26 @@ Module( ), annotation: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 87..98, parameters: Some( Parameters { range: 94..95, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 94..95, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 94..95, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 94..95, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -248,6 +285,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 97..98, id: Name("y"), ctx: Load, @@ -258,6 +296,7 @@ Module( value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 101..102, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assert.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assert.py.snap index 1dce325d01c29..e73517455e5c7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assert.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assert.py.snap @@ -1,23 +1,26 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/assert.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..186, body: [ Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 0..12, test: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 7..12, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 7..8, value: Int( 1, @@ -30,6 +33,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 11..12, value: Int( 2, @@ -44,12 +48,15 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 13..26, test: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 20..26, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..24, id: Name("call"), ctx: Load, @@ -57,6 +64,7 @@ Module( ), arguments: Arguments { range: 24..26, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -67,14 +75,17 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 27..41, test: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 34..41, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..35, id: Name("a"), ctx: Load, @@ -82,6 +93,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 40..41, id: Name("b"), ctx: Load, @@ -95,22 +107,30 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 42..60, test: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 49..60, parameters: Some( Parameters { range: 56..57, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 56..57, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 56..57, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 56..57, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -124,6 +144,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("y"), ctx: Load, @@ -136,12 +157,15 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 61..75, test: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 68..75, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("x"), ctx: Load, @@ -154,18 +178,22 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 76..99, test: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 83..99, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 88..92, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("x"), ctx: Load, @@ -173,6 +201,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("y"), ctx: Load, @@ -185,9 +214,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 101..118, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 108..109, id: Name("x"), ctx: Load, @@ -196,11 +227,13 @@ Module( msg: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 111..118, value: StringLiteralValue { inner: Single( StringLiteral { range: 111..118, + node_index: AtomicNodeIndex(..), value: "error", flags: StringLiteralFlags { quote_style: Double, @@ -217,9 +250,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 119..140, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 126..127, id: Name("x"), ctx: Load, @@ -228,19 +263,26 @@ Module( msg: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 129..140, parameters: Some( Parameters { range: 136..137, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 136..137, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 136..137, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 136..137, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -254,6 +296,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..140, id: Name("y"), ctx: Load, @@ -266,9 +309,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 141..158, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..149, id: Name("x"), ctx: Load, @@ -277,9 +322,11 @@ Module( msg: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 151..158, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 157..158, id: Name("x"), ctx: Load, @@ -292,9 +339,11 @@ Module( ), Assert( StmtAssert { + node_index: AtomicNodeIndex(..), range: 159..185, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("x"), ctx: Load, @@ -303,15 +352,18 @@ Module( msg: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 169..185, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 174..178, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 169..170, id: Name("x"), ctx: Load, @@ -319,6 +371,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..185, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap index 8c67f41106456..d649315b7d8b5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/valid/statement/assignment.py ``` Module( ModModule { - range: 0..723, + node_index: AtomicNodeIndex(..), + range: 0..729, body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 0..13, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -23,10 +26,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 4..13, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -35,6 +40,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 8..9, value: Int( 2, @@ -43,6 +49,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 11..12, value: Int( 3, @@ -58,14 +65,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 15..33, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 15..21, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 16..17, id: Name("x"), ctx: Store, @@ -73,6 +83,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 19..20, id: Name("y"), ctx: Store, @@ -86,10 +97,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 24..33, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 25..26, value: Int( 1, @@ -98,6 +111,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 28..29, value: Int( 2, @@ -106,6 +120,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 31..32, value: Int( 3, @@ -121,14 +136,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 35..53, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 35..41, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("x"), ctx: Store, @@ -136,6 +154,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("y"), ctx: Store, @@ -148,10 +167,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 44..53, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 45..46, value: Int( 1, @@ -160,6 +181,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 48..49, value: Int( 2, @@ -168,6 +190,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 3, @@ -183,13 +206,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 55..70, targets: [ Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 55..58, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..56, id: Name("x"), ctx: Load, @@ -198,6 +224,7 @@ Module( attr: Identifier { id: Name("y"), range: 57..58, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, @@ -205,10 +232,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 61..70, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 62..63, value: Int( 1, @@ -217,6 +246,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 65..66, value: Int( 2, @@ -225,6 +255,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 68..69, value: Int( 3, @@ -240,13 +271,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 72..88, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 72..76, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("x"), ctx: Load, @@ -254,6 +288,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 74..75, id: Name("y"), ctx: Load, @@ -265,10 +300,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 79..88, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 80..81, value: Int( 1, @@ -277,6 +314,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 2, @@ -285,6 +323,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 86..87, value: Int( 3, @@ -300,14 +339,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 90..109, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 90..97, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("x"), ctx: Store, @@ -315,9 +357,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 94..96, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..96, id: Name("y"), ctx: Store, @@ -334,10 +378,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 100..109, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 101..102, value: Int( 1, @@ -346,6 +392,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..105, value: Int( 2, @@ -354,6 +401,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 107..108, value: Int( 3, @@ -369,14 +417,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 259..280, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 259..268, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 260..261, id: Name("x"), ctx: Store, @@ -384,6 +435,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 263..264, id: Name("y"), ctx: Store, @@ -391,6 +443,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 266..267, id: Name("z"), ctx: Store, @@ -403,10 +456,12 @@ Module( ], value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 271..280, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 272..273, value: Int( 1, @@ -415,6 +470,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 275..276, value: Int( 2, @@ -423,6 +479,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 278..279, value: Int( 3, @@ -437,14 +494,17 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 282..303, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 282..291, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 283..284, id: Name("x"), ctx: Store, @@ -452,6 +512,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 286..287, id: Name("y"), ctx: Store, @@ -459,6 +520,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 289..290, id: Name("z"), ctx: Store, @@ -472,10 +534,12 @@ Module( ], value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 294..303, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 295..296, value: Int( 1, @@ -484,6 +548,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 298..299, value: Int( 2, @@ -492,6 +557,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 301..302, value: Int( 3, @@ -507,13 +573,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 304..313, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 304..308, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 304..305, id: Name("x"), ctx: Load, @@ -521,6 +590,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 306..307, value: Int( 0, @@ -533,6 +603,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 311..313, value: Int( 42, @@ -543,13 +614,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 410..419, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 410..414, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 410..411, value: Int( 5, @@ -558,6 +632,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 412..413, value: Int( 0, @@ -570,6 +645,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 417..419, value: Int( 42, @@ -580,13 +656,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 420..433, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 420..426, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 420..421, id: Name("x"), ctx: Load, @@ -594,10 +673,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 422..425, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 422..423, value: Int( 1, @@ -608,6 +689,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 424..425, value: Int( 2, @@ -624,10 +706,12 @@ Module( ], value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 429..433, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 430..432, value: Int( 42, @@ -642,13 +726,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 529..542, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 529..535, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 529..530, value: Int( 5, @@ -657,10 +744,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 531..534, lower: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 531..532, value: Int( 1, @@ -671,6 +760,7 @@ Module( upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 533..534, value: Int( 2, @@ -687,10 +777,12 @@ Module( ], value: List( ExprList { + node_index: AtomicNodeIndex(..), range: 538..542, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 539..541, value: Int( 42, @@ -705,13 +797,16 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 544..556, targets: [ Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 544..551, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 544..547, id: Name("foo"), ctx: Load, @@ -720,6 +815,7 @@ Module( attr: Identifier { id: Name("bar"), range: 548..551, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, @@ -727,6 +823,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 554..556, value: Int( 42, @@ -737,18 +834,22 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 658..670, targets: [ Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 658..665, value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 658..663, value: StringLiteralValue { inner: Single( StringLiteral { range: 658..663, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -763,6 +864,7 @@ Module( attr: Identifier { id: Name("y"), range: 664..665, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, @@ -770,6 +872,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 668..670, value: Int( 42, @@ -780,10 +883,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 672..680, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 672..675, id: Name("foo"), ctx: Store, @@ -792,6 +897,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 678..680, value: Int( 42, @@ -802,77 +908,109 @@ Module( ), Assign( StmtAssign { - range: 682..692, + node_index: AtomicNodeIndex(..), + range: 682..695, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 682..684, elts: [], ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 687..692, - value: Name( - ExprName { - range: 688..692, - id: Name("data"), - ctx: Load, - }, - ), + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 687..695, + elts: [ + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 688..693, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 689..693, + id: Name("data"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 693..703, + node_index: AtomicNodeIndex(..), + range: 696..709, targets: [ Tuple( ExprTuple { - range: 693..695, + node_index: AtomicNodeIndex(..), + range: 696..698, elts: [], ctx: Store, parenthesized: true, }, ), ], - value: Starred( - ExprStarred { - range: 698..703, - value: Name( - ExprName { - range: 699..703, - id: Name("data"), - ctx: Load, - }, - ), + value: Tuple( + ExprTuple { + node_index: AtomicNodeIndex(..), + range: 701..709, + elts: [ + Starred( + ExprStarred { + node_index: AtomicNodeIndex(..), + range: 702..707, + value: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 703..707, + id: Name("data"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 704..713, + node_index: AtomicNodeIndex(..), + range: 710..719, targets: [ Tuple( ExprTuple { - range: 704..708, + node_index: AtomicNodeIndex(..), + range: 710..714, elts: [ Name( ExprName { - range: 704..705, + node_index: AtomicNodeIndex(..), + range: 710..711, id: Name("a"), ctx: Store, }, ), Name( ExprName { - range: 707..708, + node_index: AtomicNodeIndex(..), + range: 713..714, id: Name("b"), ctx: Store, }, @@ -885,7 +1023,8 @@ Module( ], value: Name( ExprName { - range: 711..713, + node_index: AtomicNodeIndex(..), + range: 717..719, id: Name("ab"), ctx: Load, }, @@ -894,18 +1033,21 @@ Module( ), Assign( StmtAssign { - range: 714..723, + node_index: AtomicNodeIndex(..), + range: 720..729, targets: [ Name( ExprName { - range: 714..715, + node_index: AtomicNodeIndex(..), + range: 720..721, id: Name("a"), ctx: Store, }, ), Name( ExprName { - range: 718..719, + node_index: AtomicNodeIndex(..), + range: 724..725, id: Name("b"), ctx: Store, }, @@ -913,7 +1055,8 @@ Module( ], value: Name( ExprName { - range: 722..723, + node_index: AtomicNodeIndex(..), + range: 728..729, id: Name("c"), ctx: Load, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__augmented_assignment.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__augmented_assignment.py.snap index da75ead75c817..aabb6b245bd57 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__augmented_assignment.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__augmented_assignment.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/augmented_assignment.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..212, body: [ AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 0..6, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 0..1, id: Name("x"), ctx: Store, @@ -23,6 +25,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5..6, value: Int( 1, @@ -33,12 +36,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 7..23, target: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 7..10, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 7..8, id: Name("x"), ctx: Load, @@ -47,6 +53,7 @@ Module( attr: Identifier { id: Name("y"), range: 9..10, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, @@ -54,10 +61,12 @@ Module( op: Add, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 14..23, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 15..16, value: Int( 1, @@ -66,6 +75,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 18..19, value: Int( 2, @@ -74,6 +84,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 21..22, value: Int( 3, @@ -89,12 +100,15 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 24..41, target: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 24..28, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Load, @@ -102,6 +116,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 26..27, id: Name("y"), ctx: Load, @@ -113,10 +128,12 @@ Module( op: Add, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 32..41, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 33..34, value: Int( 1, @@ -125,6 +142,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 36..37, value: Int( 2, @@ -133,6 +151,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 39..40, value: Int( 3, @@ -148,9 +167,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 86..92, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..87, id: Name("x"), ctx: Store, @@ -159,6 +180,7 @@ Module( op: Add, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 91..92, value: Int( 1, @@ -169,9 +191,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 93..99, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("x"), ctx: Store, @@ -180,6 +204,7 @@ Module( op: Sub, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 98..99, value: Int( 1, @@ -190,9 +215,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 100..106, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("x"), ctx: Store, @@ -201,6 +228,7 @@ Module( op: Mult, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 105..106, value: Int( 1, @@ -211,9 +239,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 107..113, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 107..108, id: Name("x"), ctx: Store, @@ -222,6 +252,7 @@ Module( op: Div, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 112..113, value: Int( 1, @@ -232,9 +263,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 114..121, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..115, id: Name("x"), ctx: Store, @@ -243,6 +276,7 @@ Module( op: FloorDiv, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 120..121, value: Int( 1, @@ -253,9 +287,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 122..128, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..123, id: Name("x"), ctx: Store, @@ -264,6 +300,7 @@ Module( op: Mod, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 127..128, value: Int( 1, @@ -274,9 +311,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 129..136, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..130, id: Name("x"), ctx: Store, @@ -285,6 +324,7 @@ Module( op: Pow, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 135..136, value: Int( 1, @@ -295,9 +335,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 137..143, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 137..138, id: Name("x"), ctx: Store, @@ -306,6 +348,7 @@ Module( op: BitAnd, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 142..143, value: Int( 1, @@ -316,9 +359,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 144..150, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 144..145, id: Name("x"), ctx: Store, @@ -327,6 +372,7 @@ Module( op: BitOr, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 149..150, value: Int( 1, @@ -337,9 +383,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 151..157, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..152, id: Name("x"), ctx: Store, @@ -348,6 +396,7 @@ Module( op: BitXor, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 156..157, value: Int( 1, @@ -358,9 +407,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 158..165, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 158..159, id: Name("x"), ctx: Store, @@ -369,6 +420,7 @@ Module( op: LShift, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 164..165, value: Int( 1, @@ -379,9 +431,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 166..173, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 166..167, id: Name("x"), ctx: Store, @@ -390,6 +444,7 @@ Module( op: RShift, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 172..173, value: Int( 1, @@ -400,9 +455,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 174..180, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 174..175, id: Name("x"), ctx: Store, @@ -411,6 +468,7 @@ Module( op: MatMult, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 179..180, value: Int( 1, @@ -421,9 +479,11 @@ Module( ), AugAssign( StmtAugAssign { + node_index: AtomicNodeIndex(..), range: 190..212, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 190..191, id: Name("a"), ctx: Store, @@ -432,12 +492,15 @@ Module( op: FloorDiv, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 196..212, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 197..202, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 197..198, id: Name("a"), ctx: Load, @@ -446,6 +509,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 201..202, id: Name("b"), ctx: Load, @@ -456,9 +520,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 206..212, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 206..207, id: Name("c"), ctx: Load, @@ -467,6 +533,7 @@ Module( op: Pow, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 211..212, value: Int( 2, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap index 8a6c4d8e8764b..9882e26371592 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__class.py.snap @@ -1,31 +1,35 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/class.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..1023, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..19, decorator_list: [], name: Identifier { id: Name("Test"), range: 6..10, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 16..19, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 16..19, }, ), @@ -36,16 +40,19 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 22..80, decorator_list: [], name: Identifier { id: Name("Test"), range: 28..32, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 32..34, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -53,25 +60,33 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..80, is_async: false, decorator_list: [], name: Identifier { id: Name("__init__"), range: 48..56, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 56..62, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 57..61, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 57..61, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("self"), range: 57..61, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -86,6 +101,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 76..80, }, ), @@ -97,22 +113,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 83..116, decorator_list: [], name: Identifier { id: Name("Test"), range: 89..93, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 93..107, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 99..101, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..101, id: Name("A"), ctx: Load, @@ -125,14 +146,17 @@ Module( keywords: [ Keyword { range: 94..97, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("a"), range: 94..95, + node_index: AtomicNodeIndex(..), }, ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 96..97, value: Int( 1, @@ -142,9 +166,11 @@ Module( }, Keyword { range: 103..106, + node_index: AtomicNodeIndex(..), arg: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..106, id: Name("k"), ctx: Load, @@ -157,9 +183,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 113..116, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 113..116, }, ), @@ -170,27 +198,34 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 119..168, decorator_list: [], name: Identifier { id: Name("Test"), range: 125..129, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 135..168, is_async: false, decorator_list: [], name: Identifier { id: Name("method"), range: 139..145, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 145..147, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -201,14 +236,17 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 157..168, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 157..161, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 157..158, id: Name("a"), ctx: Store, @@ -216,6 +254,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..161, id: Name("b"), ctx: Store, @@ -229,6 +268,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 164..168, id: Name("data"), ctx: Load, @@ -244,19 +284,23 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 171..289, decorator_list: [], name: Identifier { id: Name("Test"), range: 177..181, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 181..187, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 182..183, id: Name("A"), ctx: Load, @@ -264,6 +308,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 185..186, id: Name("B"), ctx: Load, @@ -276,25 +321,33 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 193..225, is_async: false, decorator_list: [], name: Identifier { id: Name("__init__"), range: 197..205, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 205..211, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 206..210, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 206..210, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("self"), range: 206..210, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -309,6 +362,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 221..225, }, ), @@ -317,25 +371,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 231..289, is_async: false, decorator_list: [], name: Identifier { id: Name("method_with_default"), range: 235..254, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 254..275, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 255..259, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 255..259, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("self"), range: 255..259, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -343,22 +405,27 @@ Module( }, ParameterWithDefault { range: 261..274, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 261..264, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 261..264, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 265..274, value: StringLiteralValue { inner: Single( StringLiteral { range: 265..274, + node_index: AtomicNodeIndex(..), value: "default", flags: StringLiteralFlags { quote_style: Single, @@ -381,6 +448,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 285..289, }, ), @@ -392,22 +460,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 331..351, decorator_list: [], name: Identifier { id: Name("Test"), range: 337..341, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 341..344, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 342..343, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 342..343, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -419,6 +492,7 @@ Module( arguments: Some( Arguments { range: 344..346, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -426,9 +500,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 348..351, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 348..351, }, ), @@ -439,27 +515,33 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 376..402, decorator_list: [], name: Identifier { id: Name("Test"), range: 382..386, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 386..395, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 387..394, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 387..388, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 391..394, id: Name("str"), ctx: Load, @@ -474,6 +556,7 @@ Module( arguments: Some( Arguments { range: 395..397, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -481,9 +564,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 399..402, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 399..402, }, ), @@ -494,26 +579,32 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 425..450, decorator_list: [], name: Identifier { id: Name("Test"), range: 431..435, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 435..443, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 436..442, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 436..437, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 439..442, id: Name("str"), ctx: Load, @@ -529,6 +620,7 @@ Module( arguments: Some( Arguments { range: 443..445, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -536,9 +628,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 447..450, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 447..450, }, ), @@ -549,29 +643,36 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 485..522, decorator_list: [], name: Identifier { id: Name("Test"), range: 491..495, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 495..515, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 496..514, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 496..497, + node_index: AtomicNodeIndex(..), }, bound: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 499..508, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 499..502, id: Name("int"), ctx: Load, @@ -580,6 +681,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 505..508, id: Name("str"), ctx: Load, @@ -591,6 +693,7 @@ Module( default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 511..514, id: Name("int"), ctx: Load, @@ -605,6 +708,7 @@ Module( arguments: Some( Arguments { range: 515..517, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -612,9 +716,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 519..522, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 519..522, }, ), @@ -625,30 +731,37 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 551..585, decorator_list: [], name: Identifier { id: Name("Test"), range: 557..561, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 561..578, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 562..577, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 562..563, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 565..577, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 566..569, id: Name("str"), ctx: Load, @@ -656,6 +769,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 571..576, id: Name("bytes"), ctx: Load, @@ -676,6 +790,7 @@ Module( arguments: Some( Arguments { range: 578..580, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -683,9 +798,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 582..585, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 582..585, }, ), @@ -696,22 +813,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 606..629, decorator_list: [], name: Identifier { id: Name("Test"), range: 612..616, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 616..622, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 617..618, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 617..618, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -720,9 +842,11 @@ Module( TypeVar( TypeParamTypeVar { range: 620..621, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 620..621, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -734,6 +858,7 @@ Module( arguments: Some( Arguments { range: 622..624, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -741,9 +866,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 626..629, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 626..629, }, ), @@ -754,22 +881,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 648..672, decorator_list: [], name: Identifier { id: Name("Test"), range: 654..658, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 658..665, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 659..660, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 659..660, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -778,9 +910,11 @@ Module( TypeVar( TypeParamTypeVar { range: 662..663, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 662..663, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -792,6 +926,7 @@ Module( arguments: Some( Arguments { range: 665..667, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -799,9 +934,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 669..672, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 669..672, }, ), @@ -812,22 +949,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 689..711, decorator_list: [], name: Identifier { id: Name("Test"), range: 695..699, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 699..704, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 700..703, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 701..703, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -838,6 +980,7 @@ Module( arguments: Some( Arguments { range: 704..706, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -845,9 +988,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 708..711, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 708..711, }, ), @@ -858,29 +1003,36 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 741..789, decorator_list: [], name: Identifier { id: Name("Test"), range: 747..751, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 751..782, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 752..781, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 753..755, + node_index: AtomicNodeIndex(..), }, default: Some( Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 758..781, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 758..764, id: Name("Unpack"), ctx: Load, @@ -888,9 +1040,11 @@ Module( ), slice: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 765..780, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 765..770, id: Name("tuple"), ctx: Load, @@ -898,10 +1052,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 771..779, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 771..774, id: Name("int"), ctx: Load, @@ -909,6 +1065,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 776..779, id: Name("str"), ctx: Load, @@ -934,6 +1091,7 @@ Module( arguments: Some( Arguments { range: 782..784, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -941,9 +1099,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 786..789, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 786..789, }, ), @@ -954,32 +1114,40 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 827..868, decorator_list: [], name: Identifier { id: Name("Test"), range: 833..837, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 837..861, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 838..860, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 839..841, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 844..860, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 845..860, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 845..850, id: Name("tuple"), ctx: Load, @@ -987,10 +1155,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 851..859, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 851..854, id: Name("int"), ctx: Load, @@ -998,6 +1168,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 856..859, id: Name("str"), ctx: Load, @@ -1023,6 +1194,7 @@ Module( arguments: Some( Arguments { range: 861..863, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1030,9 +1202,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 865..868, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 865..868, }, ), @@ -1043,22 +1217,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 882..904, decorator_list: [], name: Identifier { id: Name("Test"), range: 888..892, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 892..897, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 893..896, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 895..896, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -1069,6 +1248,7 @@ Module( arguments: Some( Arguments { range: 897..899, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1076,9 +1256,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 901..904, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 901..904, }, ), @@ -1089,30 +1271,37 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 931..966, decorator_list: [], name: Identifier { id: Name("Test"), range: 937..941, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 941..959, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 942..958, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 944..945, + node_index: AtomicNodeIndex(..), }, default: Some( List( ExprList { + node_index: AtomicNodeIndex(..), range: 948..958, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 949..952, id: Name("int"), ctx: Load, @@ -1120,6 +1309,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 954..957, id: Name("str"), ctx: Load, @@ -1138,6 +1328,7 @@ Module( arguments: Some( Arguments { range: 959..961, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1145,9 +1336,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 963..966, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 963..966, }, ), @@ -1158,22 +1351,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 982..1022, decorator_list: [], name: Identifier { id: Name("Test"), range: 988..992, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 992..1012, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 993..994, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("X"), range: 993..994, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -1182,13 +1380,16 @@ Module( TypeVar( TypeParamTypeVar { range: 996..1002, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Y"), range: 996..997, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 999..1002, id: Name("str"), ctx: Load, @@ -1201,9 +1402,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 1004..1006, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 1005..1006, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -1211,9 +1414,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 1008..1011, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 1010..1011, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -1224,6 +1429,7 @@ Module( arguments: Some( Arguments { range: 1012..1014, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1231,6 +1437,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1018..1022, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__delete.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__delete.py.snap index fbb6d09bf2b0f..bc20d70fae3c5 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__delete.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__delete.py.snap @@ -1,21 +1,23 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/delete.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..122, body: [ Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 0..5, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..5, id: Name("x"), ctx: Del, @@ -26,10 +28,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 6..13, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 11..12, id: Name("x"), ctx: Del, @@ -40,10 +44,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 14..23, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("a"), ctx: Del, @@ -51,6 +57,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("b"), ctx: Del, @@ -61,10 +68,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 24..40, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("a"), ctx: Del, @@ -72,10 +81,12 @@ Module( ), Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 31..37, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("b"), ctx: Del, @@ -83,6 +94,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 35..36, id: Name("c"), ctx: Del, @@ -95,6 +107,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 39..40, id: Name("d"), ctx: Del, @@ -105,14 +118,17 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 41..51, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 45..51, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("a"), ctx: Del, @@ -120,6 +136,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("b"), ctx: Del, @@ -134,14 +151,17 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 52..70, targets: [ List( ExprList { + node_index: AtomicNodeIndex(..), range: 56..70, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 57..58, id: Name("a"), ctx: Del, @@ -149,10 +169,12 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 60..66, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..62, id: Name("b"), ctx: Del, @@ -160,6 +182,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 64..65, id: Name("c"), ctx: Del, @@ -171,6 +194,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("d"), ctx: Del, @@ -185,13 +209,16 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 71..78, targets: [ Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 75..78, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("x"), ctx: Load, @@ -200,6 +227,7 @@ Module( attr: Identifier { id: Name("y"), range: 77..78, + node_index: AtomicNodeIndex(..), }, ctx: Del, }, @@ -209,13 +237,16 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 79..87, targets: [ Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 83..87, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 83..84, id: Name("x"), ctx: Load, @@ -223,6 +254,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 85..86, id: Name("y"), ctx: Load, @@ -236,14 +268,17 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 88..121, targets: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 92..121, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("x"), ctx: Del, @@ -251,9 +286,11 @@ Module( ), Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 105..108, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 105..106, id: Name("x"), ctx: Load, @@ -262,15 +299,18 @@ Module( attr: Identifier { id: Name("y"), range: 107..108, + node_index: AtomicNodeIndex(..), }, ctx: Del, }, ), Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 114..118, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..115, id: Name("x"), ctx: Load, @@ -278,6 +318,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 116..117, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__for.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__for.py.snap index fa69892a4a011..a003c4896d716 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__for.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__for.py.snap @@ -7,14 +7,17 @@ input_file: crates/ruff_python_parser/resources/valid/statement/for.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..523, body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 0..28, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4..10, id: Name("target"), ctx: Store, @@ -22,6 +25,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..18, id: Name("iter"), ctx: Load, @@ -30,6 +34,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 24..28, }, ), @@ -39,10 +44,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 30..63, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..40, id: Name("target"), ctx: Store, @@ -50,10 +57,12 @@ Module( ), iter: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 44..53, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 45..46, value: Int( 1, @@ -62,6 +71,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 48..49, value: Int( 2, @@ -70,6 +80,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 3, @@ -84,6 +95,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 59..63, }, ), @@ -93,13 +105,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 65..100, is_async: false, target: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 69..80, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..75, id: Name("target"), ctx: Load, @@ -108,15 +123,18 @@ Module( attr: Identifier { id: Name("attr"), range: 76..80, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 84..90, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..88, id: Name("call"), ctx: Load, @@ -124,6 +142,7 @@ Module( ), arguments: Arguments { range: 88..90, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -132,6 +151,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 96..100, }, ), @@ -141,13 +161,16 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 102..135, is_async: false, target: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 106..115, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..112, id: Name("target"), ctx: Load, @@ -155,6 +178,7 @@ Module( ), slice: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 113..114, value: Int( 0, @@ -166,9 +190,11 @@ Module( ), iter: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 119..125, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 119..120, id: Name("x"), ctx: Load, @@ -177,6 +203,7 @@ Module( attr: Identifier { id: Name("attr"), range: 121..125, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -184,6 +211,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 131..135, }, ), @@ -193,10 +221,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 137..167, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 141..147, id: Name("target"), ctx: Store, @@ -204,9 +234,11 @@ Module( ), iter: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 151..157, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..152, id: Name("x"), ctx: Load, @@ -218,6 +250,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("y"), ctx: Load, @@ -229,6 +262,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 163..167, }, ), @@ -238,10 +272,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 169..200, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 173..179, id: Name("target"), ctx: Store, @@ -249,11 +285,13 @@ Module( ), iter: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 183..190, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 183..184, id: Name("a"), ctx: Load, @@ -261,6 +299,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..190, id: Name("b"), ctx: Load, @@ -272,6 +311,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 196..200, }, ), @@ -281,14 +321,17 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 202..232, is_async: false, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 206..214, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 206..207, id: Name("a"), ctx: Store, @@ -296,6 +339,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 209..210, id: Name("b"), ctx: Store, @@ -303,6 +347,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 212..213, id: Name("c"), ctx: Store, @@ -315,6 +360,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 218..222, id: Name("iter"), ctx: Load, @@ -323,6 +369,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 228..232, }, ), @@ -332,14 +379,17 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 234..262, is_async: false, target: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 238..244, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 239..240, id: Name("a"), ctx: Store, @@ -347,6 +397,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 242..243, id: Name("b"), ctx: Store, @@ -359,6 +410,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 248..252, id: Name("iter"), ctx: Load, @@ -367,6 +419,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 258..262, }, ), @@ -376,10 +429,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 264..294, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 268..274, id: Name("target"), ctx: Store, @@ -387,10 +442,12 @@ Module( ), iter: List( ExprList { + node_index: AtomicNodeIndex(..), range: 278..284, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 279..280, value: Int( 1, @@ -399,6 +456,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 282..283, value: Int( 2, @@ -412,6 +470,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 290..294, }, ), @@ -421,10 +480,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 296..322, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 300..306, id: Name("target"), ctx: Store, @@ -432,9 +493,11 @@ Module( ), iter: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 310..317, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 316..317, id: Name("x"), ctx: Load, @@ -445,9 +508,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 319..322, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 319..322, }, ), @@ -459,10 +524,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 323..353, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..333, id: Name("target"), ctx: Store, @@ -470,19 +537,26 @@ Module( ), iter: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 337..348, parameters: Some( Parameters { range: 344..345, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 344..345, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 344..345, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 344..345, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -496,6 +570,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 347..348, id: Name("x"), ctx: Load, @@ -506,9 +581,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 350..353, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 350..353, }, ), @@ -520,10 +597,12 @@ Module( ), For( StmtFor { + node_index: AtomicNodeIndex(..), range: 354..389, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 358..364, id: Name("target"), ctx: Store, @@ -531,15 +610,18 @@ Module( ), iter: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 368..384, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 373..377, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 368..369, id: Name("x"), ctx: Load, @@ -547,6 +629,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 383..384, id: Name("y"), ctx: Load, @@ -557,9 +640,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 386..389, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 386..389, }, ), @@ -571,9 +656,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 391..522, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 394..395, id: Name("x"), ctx: Load, @@ -582,10 +669,12 @@ Module( body: [ For( StmtFor { + node_index: AtomicNodeIndex(..), range: 401..433, is_async: false, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 405..411, id: Name("target"), ctx: Store, @@ -593,6 +682,7 @@ Module( ), iter: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 415..419, id: Name("iter"), ctx: Load, @@ -601,6 +691,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 429..433, }, ), @@ -612,10 +703,12 @@ Module( elif_else_clauses: [ ElifElseClause { range: 508..522, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 518..522, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap index 3fa6228749039..d6f6e5015867e 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__from_import.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/from_import.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..259, body: [ ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 0..15, module: Some( Identifier { id: Name("a"), range: 5..6, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 14..15, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 14..15, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -34,14 +38,17 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 27..42, module: None, names: [ Alias { range: 41..42, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 41..42, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -51,37 +58,45 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 43..85, module: Some( Identifier { id: Name("foo.bar"), range: 48..55, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 63..71, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("baz"), range: 63..66, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("b"), range: 70..71, + node_index: AtomicNodeIndex(..), }, ), }, Alias { range: 73..85, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("FooBar"), range: 73..79, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("fb"), range: 83..85, + node_index: AtomicNodeIndex(..), }, ), }, @@ -91,19 +106,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 86..102, module: Some( Identifier { id: Name("a"), range: 92..93, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 101..102, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 101..102, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -113,14 +132,17 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 103..120, module: None, names: [ Alias { range: 119..120, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 119..120, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -130,14 +152,17 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 121..161, module: None, names: [ Alias { range: 160..161, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 160..161, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -147,19 +172,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 162..207, module: Some( Identifier { id: Name("a.b.c"), range: 193..198, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 206..207, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 206..207, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -169,40 +198,49 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 208..242, module: Some( Identifier { id: Name("module"), range: 213..219, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 228..229, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 228..229, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 231..237, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 231..232, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("B"), range: 236..237, + node_index: AtomicNodeIndex(..), }, ), }, Alias { range: 239..240, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 239..240, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -212,19 +250,23 @@ Module( ), ImportFrom( StmtImportFrom { + node_index: AtomicNodeIndex(..), range: 243..258, module: Some( Identifier { id: Name("a"), range: 248..249, + node_index: AtomicNodeIndex(..), }, ), names: [ Alias { range: 257..258, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("*"), range: 257..258, + node_index: AtomicNodeIndex(..), }, asname: None, }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap index 0dbedd8e92761..d2b7caef14121 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__function.py.snap @@ -1,27 +1,32 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/function.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..2399, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 0..29, is_async: false, decorator_list: [], name: Identifier { id: Name("no_parameters"), range: 4..17, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 17..19, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -32,6 +37,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 25..29, }, ), @@ -40,25 +46,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 32..76, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_parameters"), range: 36..57, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 57..66, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 58..59, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 58..59, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 58..59, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -66,11 +80,14 @@ Module( }, ParameterWithDefault { range: 61..62, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 61..62, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 61..62, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -78,11 +95,14 @@ Module( }, ParameterWithDefault { range: 64..65, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 64..65, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 64..65, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -97,6 +117,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 72..76, }, ), @@ -105,25 +126,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 79..149, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_parameters_with_default_values"), range: 83..124, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 124..139, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 125..126, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 125..126, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 125..126, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -131,17 +160,21 @@ Module( }, ParameterWithDefault { range: 128..132, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 128..129, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 128..129, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 130..132, value: Int( 20, @@ -152,17 +185,21 @@ Module( }, ParameterWithDefault { range: 134..138, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 134..135, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 134..135, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 136..138, value: Int( 30, @@ -180,6 +217,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 145..149, }, ), @@ -188,24 +226,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 152..226, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_parameters_with_default_values2"), range: 156..198, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 198..216, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 199..200, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 199..200, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 199..200, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -213,17 +259,21 @@ Module( }, ParameterWithDefault { range: 202..206, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 202..203, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 202..203, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 204..206, value: Int( 20, @@ -236,17 +286,21 @@ Module( args: [ ParameterWithDefault { range: 211..215, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 211..212, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 211..212, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..215, value: Int( 30, @@ -264,6 +318,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 222..226, }, ), @@ -272,24 +327,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 229..296, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_only_and_positional_parameters"), range: 233..274, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 274..286, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 275..276, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 275..276, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 275..276, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -299,11 +362,14 @@ Module( args: [ ParameterWithDefault { range: 281..282, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 281..282, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 281..282, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -311,11 +377,14 @@ Module( }, ParameterWithDefault { range: 284..285, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 284..285, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 284..285, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -330,6 +399,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 292..296, }, ), @@ -338,24 +408,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 299..393, is_async: false, decorator_list: [], name: Identifier { id: Name("pos_args_with_defaults_and_varargs_and_kwargs"), range: 303..348, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 348..383, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 349..350, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 349..350, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 349..350, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -363,17 +441,21 @@ Module( }, ParameterWithDefault { range: 352..356, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 352..353, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 352..353, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 354..356, value: Int( 20, @@ -386,17 +468,21 @@ Module( args: [ ParameterWithDefault { range: 361..365, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 361..362, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 361..362, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 363..365, value: Int( 30, @@ -409,9 +495,11 @@ Module( vararg: Some( Parameter { range: 367..372, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 368..372, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -420,9 +508,11 @@ Module( kwarg: Some( Parameter { range: 374..382, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 376..382, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -432,6 +522,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 389..393, }, ), @@ -440,27 +531,35 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 396..445, is_async: false, decorator_list: [], name: Identifier { id: Name("keyword_only_parameters"), range: 400..423, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 423..435, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 427..428, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 427..428, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 427..428, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -468,11 +567,14 @@ Module( }, ParameterWithDefault { range: 430..431, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 430..431, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 430..431, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -480,11 +582,14 @@ Module( }, ParameterWithDefault { range: 433..434, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 433..434, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 433..434, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -497,6 +602,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 441..445, }, ), @@ -505,27 +611,35 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 448..517, is_async: false, decorator_list: [], name: Identifier { id: Name("keyword_only_parameters_with_defaults"), range: 452..489, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 489..507, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 493..494, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 493..494, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 493..494, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -533,17 +647,21 @@ Module( }, ParameterWithDefault { range: 496..500, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 496..497, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 496..497, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 498..500, value: Int( 20, @@ -554,17 +672,21 @@ Module( }, ParameterWithDefault { range: 502..506, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 502..503, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 502..503, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 504..506, value: Int( 30, @@ -580,6 +702,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 513..517, }, ), @@ -588,24 +711,31 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 520..594, is_async: false, decorator_list: [], name: Identifier { id: Name("kw_only_args_with_defaults_and_varargs"), range: 524..562, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 562..584, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 563..568, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 564..568, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -613,11 +743,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 570..571, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 570..571, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 570..571, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -625,17 +758,21 @@ Module( }, ParameterWithDefault { range: 573..577, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 573..574, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 573..574, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 575..577, value: Int( 20, @@ -646,17 +783,21 @@ Module( }, ParameterWithDefault { range: 579..583, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 579..580, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 579..580, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 581..583, value: Int( 30, @@ -672,6 +813,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 590..594, }, ), @@ -680,27 +822,35 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 597..676, is_async: false, decorator_list: [], name: Identifier { id: Name("kw_only_args_with_defaults_and_kwargs"), range: 601..638, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 638..666, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, kwonlyargs: [ ParameterWithDefault { range: 642..643, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 642..643, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 642..643, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -708,17 +858,21 @@ Module( }, ParameterWithDefault { range: 645..649, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 645..646, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 645..646, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 647..649, value: Int( 20, @@ -729,17 +883,21 @@ Module( }, ParameterWithDefault { range: 651..655, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 651..652, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 651..652, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 653..655, value: Int( 30, @@ -752,9 +910,11 @@ Module( kwarg: Some( Parameter { range: 657..665, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 659..665, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -764,6 +924,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 672..676, }, ), @@ -772,24 +933,31 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 679..774, is_async: false, decorator_list: [], name: Identifier { id: Name("kw_only_args_with_defaults_and_varargs_and_kwargs"), range: 683..732, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 732..764, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 733..738, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 734..738, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -797,11 +965,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 740..741, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 740..741, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 740..741, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -809,17 +980,21 @@ Module( }, ParameterWithDefault { range: 743..747, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 743..744, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 743..744, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 745..747, value: Int( 20, @@ -830,17 +1005,21 @@ Module( }, ParameterWithDefault { range: 749..753, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 749..750, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 749..750, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 751..753, value: Int( 30, @@ -853,9 +1032,11 @@ Module( kwarg: Some( Parameter { range: 755..763, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 757..763, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -865,6 +1046,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 770..774, }, ), @@ -873,24 +1055,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 777..835, is_async: false, decorator_list: [], name: Identifier { id: Name("pos_and_kw_only_args"), range: 781..801, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 801..825, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 802..803, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 802..803, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 802..803, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -898,11 +1088,14 @@ Module( }, ParameterWithDefault { range: 805..806, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 805..806, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 805..806, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -912,11 +1105,14 @@ Module( args: [ ParameterWithDefault { range: 811..812, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 811..812, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 811..812, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -927,11 +1123,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 817..818, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 817..818, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 817..818, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -939,11 +1138,14 @@ Module( }, ParameterWithDefault { range: 820..821, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 820..821, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 820..821, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -951,11 +1153,14 @@ Module( }, ParameterWithDefault { range: 823..824, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 823..824, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 823..824, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -968,6 +1173,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 831..835, }, ), @@ -976,24 +1182,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 838..916, is_async: false, decorator_list: [], name: Identifier { id: Name("pos_and_kw_only_args_with_defaults"), range: 842..876, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 876..906, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 877..878, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 877..878, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 877..878, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1001,11 +1215,14 @@ Module( }, ParameterWithDefault { range: 880..881, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 880..881, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 880..881, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1015,11 +1232,14 @@ Module( args: [ ParameterWithDefault { range: 886..887, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 886..887, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 886..887, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1030,11 +1250,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 892..893, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 892..893, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 892..893, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1042,17 +1265,21 @@ Module( }, ParameterWithDefault { range: 895..899, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 895..896, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 895..896, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 897..899, value: Int( 20, @@ -1063,17 +1290,21 @@ Module( }, ParameterWithDefault { range: 901..905, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 901..902, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 901..902, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 903..905, value: Int( 30, @@ -1089,6 +1320,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 912..916, }, ), @@ -1097,24 +1329,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 919..1013, is_async: false, decorator_list: [], name: Identifier { id: Name("pos_and_kw_only_args_with_defaults_and_varargs"), range: 923..969, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 969..1003, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 970..971, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 970..971, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 970..971, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1122,11 +1362,14 @@ Module( }, ParameterWithDefault { range: 973..974, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 973..974, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 973..974, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1136,11 +1379,14 @@ Module( args: [ ParameterWithDefault { range: 979..980, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 979..980, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 979..980, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1150,9 +1396,11 @@ Module( vararg: Some( Parameter { range: 982..987, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 983..987, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1160,11 +1408,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 989..990, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 989..990, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 989..990, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1172,17 +1423,21 @@ Module( }, ParameterWithDefault { range: 992..996, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 992..993, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 992..993, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 994..996, value: Int( 20, @@ -1193,17 +1448,21 @@ Module( }, ParameterWithDefault { range: 998..1002, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 998..999, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 998..999, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1000..1002, value: Int( 30, @@ -1219,6 +1478,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1009..1013, }, ), @@ -1227,24 +1487,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1016..1121, is_async: false, decorator_list: [], name: Identifier { id: Name("pos_and_kw_only_args_with_defaults_and_kwargs"), range: 1020..1065, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1065..1111, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 1071..1072, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1071..1072, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1071..1072, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1252,11 +1520,14 @@ Module( }, ParameterWithDefault { range: 1074..1075, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1074..1075, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 1074..1075, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1266,11 +1537,14 @@ Module( args: [ ParameterWithDefault { range: 1080..1081, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1080..1081, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 1080..1081, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1281,11 +1555,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 1086..1087, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1086..1087, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 1086..1087, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1293,17 +1570,21 @@ Module( }, ParameterWithDefault { range: 1089..1093, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1089..1090, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 1089..1090, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1091..1093, value: Int( 20, @@ -1314,17 +1595,21 @@ Module( }, ParameterWithDefault { range: 1095..1099, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1095..1096, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 1095..1096, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1097..1099, value: Int( 30, @@ -1337,9 +1622,11 @@ Module( kwarg: Some( Parameter { range: 1101..1109, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 1103..1109, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1349,6 +1636,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1117..1121, }, ), @@ -1357,24 +1645,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1124..1245, is_async: false, decorator_list: [], name: Identifier { id: Name("pos_and_kw_only_args_with_defaults_and_varargs_and_kwargs"), range: 1128..1185, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1185..1235, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 1191..1192, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1191..1192, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1191..1192, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1382,11 +1678,14 @@ Module( }, ParameterWithDefault { range: 1194..1195, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1194..1195, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 1194..1195, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1396,11 +1695,14 @@ Module( args: [ ParameterWithDefault { range: 1200..1201, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1200..1201, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 1200..1201, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1410,9 +1712,11 @@ Module( vararg: Some( Parameter { range: 1203..1208, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 1204..1208, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1420,11 +1724,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 1210..1211, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1210..1211, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 1210..1211, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1432,17 +1739,21 @@ Module( }, ParameterWithDefault { range: 1213..1217, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1213..1214, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 1213..1214, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1215..1217, value: Int( 20, @@ -1453,17 +1764,21 @@ Module( }, ParameterWithDefault { range: 1219..1223, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1219..1220, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 1219..1220, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1221..1223, value: Int( 30, @@ -1476,9 +1791,11 @@ Module( kwarg: Some( Parameter { range: 1225..1233, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 1227..1233, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1488,6 +1805,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1241..1245, }, ), @@ -1496,25 +1814,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1248..1316, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_and_keyword_parameters"), range: 1252..1285, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1285..1306, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1286..1287, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1286..1287, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1286..1287, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1522,11 +1848,14 @@ Module( }, ParameterWithDefault { range: 1289..1290, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1289..1290, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 1289..1290, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1534,11 +1863,14 @@ Module( }, ParameterWithDefault { range: 1292..1293, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1292..1293, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 1292..1293, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1549,11 +1881,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 1298..1299, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1298..1299, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 1298..1299, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1561,11 +1896,14 @@ Module( }, ParameterWithDefault { range: 1301..1302, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1301..1302, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 1301..1302, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1573,11 +1911,14 @@ Module( }, ParameterWithDefault { range: 1304..1305, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1304..1305, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 1304..1305, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1590,6 +1931,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1312..1316, }, ), @@ -1598,25 +1940,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1319..1407, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_and_keyword_parameters_with_defaults"), range: 1323..1370, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1370..1397, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1371..1372, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1371..1372, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1371..1372, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1624,11 +1974,14 @@ Module( }, ParameterWithDefault { range: 1374..1375, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1374..1375, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 1374..1375, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1636,11 +1989,14 @@ Module( }, ParameterWithDefault { range: 1377..1378, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1377..1378, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 1377..1378, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1651,11 +2007,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 1383..1384, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1383..1384, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 1383..1384, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1663,17 +2022,21 @@ Module( }, ParameterWithDefault { range: 1386..1390, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1386..1387, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 1386..1387, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1388..1390, value: Int( 20, @@ -1684,17 +2047,21 @@ Module( }, ParameterWithDefault { range: 1392..1396, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1392..1393, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 1392..1393, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1394..1396, value: Int( 30, @@ -1710,6 +2077,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1403..1407, }, ), @@ -1718,25 +2086,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1410..1520, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_and_keyword_parameters_with_defaults_and_varargs"), range: 1414..1473, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1473..1510, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1479..1480, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1479..1480, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1479..1480, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1744,11 +2120,14 @@ Module( }, ParameterWithDefault { range: 1482..1483, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1482..1483, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 1482..1483, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1756,11 +2135,14 @@ Module( }, ParameterWithDefault { range: 1485..1486, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1485..1486, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 1485..1486, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1770,9 +2152,11 @@ Module( vararg: Some( Parameter { range: 1488..1493, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 1489..1493, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1780,11 +2164,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 1495..1496, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1495..1496, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 1495..1496, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1792,17 +2179,21 @@ Module( }, ParameterWithDefault { range: 1498..1502, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1498..1499, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 1498..1499, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1500..1502, value: Int( 20, @@ -1813,17 +2204,21 @@ Module( }, ParameterWithDefault { range: 1504..1508, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1504..1505, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 1504..1505, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1506..1508, value: Int( 30, @@ -1839,6 +2234,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1516..1520, }, ), @@ -1847,25 +2243,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1523..1654, is_async: false, decorator_list: [], name: Identifier { id: Name("positional_and_keyword_parameters_with_defaults_and_varargs_and_kwargs"), range: 1527..1597, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1597..1644, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1603..1604, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1603..1604, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1603..1604, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1873,11 +2277,14 @@ Module( }, ParameterWithDefault { range: 1606..1607, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1606..1607, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 1606..1607, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1885,11 +2292,14 @@ Module( }, ParameterWithDefault { range: 1609..1610, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1609..1610, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 1609..1610, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1899,9 +2309,11 @@ Module( vararg: Some( Parameter { range: 1612..1617, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 1613..1617, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1909,11 +2321,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 1619..1620, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1619..1620, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("d"), range: 1619..1620, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1921,17 +2336,21 @@ Module( }, ParameterWithDefault { range: 1622..1626, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1622..1623, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("e"), range: 1622..1623, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1624..1626, value: Int( 20, @@ -1942,17 +2361,21 @@ Module( }, ParameterWithDefault { range: 1628..1632, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1628..1629, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("f"), range: 1628..1629, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1630..1632, value: Int( 30, @@ -1965,9 +2388,11 @@ Module( kwarg: Some( Parameter { range: 1634..1642, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 1636..1642, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1977,6 +2402,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1650..1654, }, ), @@ -1985,23 +2411,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1703..1735, is_async: false, decorator_list: [], name: Identifier { id: Name("func"), range: 1707..1711, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 1711..1714, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 1712..1713, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 1712..1713, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -2012,19 +2443,26 @@ Module( ), parameters: Parameters { range: 1714..1720, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1715..1719, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1715..1719, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1715..1716, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1718..1719, id: Name("T"), ctx: Load, @@ -2042,6 +2480,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1724..1725, id: Name("T"), ctx: Load, @@ -2051,6 +2490,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1731..1735, }, ), @@ -2059,27 +2499,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1738..1775, is_async: false, decorator_list: [], name: Identifier { id: Name("func"), range: 1742..1746, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 1746..1754, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 1747..1753, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 1747..1748, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1750..1753, id: Name("str"), ctx: Load, @@ -2094,19 +2540,26 @@ Module( ), parameters: Parameters { range: 1754..1760, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1755..1759, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1755..1759, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1755..1756, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1758..1759, id: Name("T"), ctx: Load, @@ -2124,6 +2577,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1764..1765, id: Name("T"), ctx: Load, @@ -2133,6 +2587,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1771..1775, }, ), @@ -2141,31 +2596,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1778..1824, is_async: false, decorator_list: [], name: Identifier { id: Name("func"), range: 1782..1786, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 1786..1803, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 1787..1802, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 1787..1788, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1790..1802, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1791..1794, id: Name("str"), ctx: Load, @@ -2173,6 +2635,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1796..1801, id: Name("bytes"), ctx: Load, @@ -2192,19 +2655,26 @@ Module( ), parameters: Parameters { range: 1803..1809, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1804..1808, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1804..1808, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1804..1805, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1807..1808, id: Name("T"), ctx: Load, @@ -2222,6 +2692,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1813..1814, id: Name("T"), ctx: Load, @@ -2231,6 +2702,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1820..1824, }, ), @@ -2239,23 +2711,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1827..1873, is_async: false, decorator_list: [], name: Identifier { id: Name("func"), range: 1831..1835, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 1835..1840, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 1836..1839, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 1837..1839, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -2265,21 +2742,28 @@ Module( ), parameters: Parameters { range: 1840..1849, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 1841..1848, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 1842..1843, + node_index: AtomicNodeIndex(..), }, annotation: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1845..1848, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1846..1848, id: Name("Ts"), ctx: Load, @@ -2297,9 +2781,11 @@ Module( returns: Some( Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1853..1863, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1853..1858, id: Name("Tuple"), ctx: Load, @@ -2307,13 +2793,16 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1859..1862, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1859..1862, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1860..1862, id: Name("Ts"), ctx: Load, @@ -2334,6 +2823,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1869..1873, }, ), @@ -2342,23 +2832,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1876..1934, is_async: false, decorator_list: [], name: Identifier { id: Name("func"), range: 1880..1884, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 1884..1889, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 1885..1888, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 1887..1888, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -2368,21 +2863,28 @@ Module( ), parameters: Parameters { range: 1889..1924, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 1890..1903, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 1891..1895, + node_index: AtomicNodeIndex(..), }, annotation: Some( Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1897..1903, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1897..1898, id: Name("P"), ctx: Load, @@ -2391,6 +2893,7 @@ Module( attr: Identifier { id: Name("args"), range: 1899..1903, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2402,16 +2905,20 @@ Module( kwarg: Some( Parameter { range: 1905..1923, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 1907..1913, + node_index: AtomicNodeIndex(..), }, annotation: Some( Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1915..1923, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1915..1916, id: Name("P"), ctx: Load, @@ -2420,6 +2927,7 @@ Module( attr: Identifier { id: Name("kwargs"), range: 1917..1923, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2432,6 +2940,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1930..1934, }, ), @@ -2440,23 +2949,28 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1937..1978, is_async: false, decorator_list: [], name: Identifier { id: Name("func"), range: 1941..1945, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 1945..1966, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 1946..1947, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 1946..1947, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -2465,13 +2979,16 @@ Module( TypeVar( TypeParamTypeVar { range: 1949..1955, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 1949..1950, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1952..1955, id: Name("str"), ctx: Load, @@ -2484,9 +3001,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 1957..1960, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 1958..1960, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -2494,9 +3013,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 1962..1965, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 1964..1965, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -2506,6 +3027,9 @@ Module( ), parameters: Parameters { range: 1966..1968, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -2516,6 +3040,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1974..1978, }, ), @@ -2524,16 +3049,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 1981..2000, is_async: false, decorator_list: [], name: Identifier { id: Name("ellipsis"), range: 1985..1993, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 1993..1995, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -2544,9 +3074,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1997..2000, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 1997..2000, }, ), @@ -2557,16 +3089,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2003..2064, is_async: false, decorator_list: [], name: Identifier { id: Name("multiple_statements"), range: 2007..2026, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2026..2028, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -2576,6 +3113,7 @@ Module( returns: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2032..2035, id: Name("int"), ctx: Load, @@ -2585,12 +3123,15 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2041..2047, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 2041..2047, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2041..2045, id: Name("call"), ctx: Load, @@ -2598,6 +3139,7 @@ Module( ), arguments: Arguments { range: 2045..2047, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -2607,14 +3149,17 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2052..2056, }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 2061..2064, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 2061..2064, }, ), @@ -2625,24 +3170,31 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2067..2091, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2071..2074, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2074..2081, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 2075..2080, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 2076..2080, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2654,6 +3206,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2087..2091, }, ), @@ -2662,16 +3215,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2094..2121, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2098..2101, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2101..2111, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -2679,9 +3237,11 @@ Module( kwarg: Some( Parameter { range: 2102..2110, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 2104..2110, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2691,6 +3251,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2117..2121, }, ), @@ -2699,24 +3260,31 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2124..2158, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2128..2131, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2131..2148, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: Some( Parameter { range: 2132..2137, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("args"), range: 2133..2137, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2725,9 +3293,11 @@ Module( kwarg: Some( Parameter { range: 2139..2147, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kwargs"), range: 2141..2147, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2737,6 +3307,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2154..2158, }, ), @@ -2745,24 +3316,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2161..2184, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2165..2168, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2168..2174, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 2169..2170, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2169..2170, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 2169..2170, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2778,6 +3357,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2180..2184, }, ), @@ -2786,24 +3366,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2187..2213, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2191..2194, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2194..2203, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 2195..2196, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2195..2196, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 2195..2196, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2813,11 +3401,14 @@ Module( args: [ ParameterWithDefault { range: 2201..2202, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2201..2202, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 2201..2202, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2832,6 +3423,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2209..2213, }, ), @@ -2840,30 +3432,39 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2216..2242, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2220..2223, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2223..2232, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 2224..2227, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2224..2225, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 2224..2225, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2226..2227, value: Int( 1, @@ -2882,6 +3483,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2238..2242, }, ), @@ -2890,24 +3492,32 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2245..2277, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2249..2252, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2252..2267, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [ ParameterWithDefault { range: 2253..2254, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2253..2254, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 2253..2254, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2915,11 +3525,14 @@ Module( }, ParameterWithDefault { range: 2256..2257, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2256..2257, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 2256..2257, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2931,11 +3544,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 2265..2266, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2265..2266, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 2265..2266, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2948,6 +3564,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2273..2277, }, ), @@ -2956,31 +3573,40 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2280..2309, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2284..2287, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2287..2299, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 2288..2292, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2288..2290, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("kw"), range: 2288..2290, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2291..2292, value: Int( 1, @@ -2994,11 +3620,14 @@ Module( kwonlyargs: [ ParameterWithDefault { range: 2297..2298, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2297..2298, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 2297..2298, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -3011,6 +3640,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2305..2309, }, ), @@ -3019,29 +3649,38 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2312..2357, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2316..2319, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2319..2347, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 2320..2326, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2320..2326, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 2320..2321, + node_index: AtomicNodeIndex(..), }, annotation: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2323..2326, id: Name("int"), ctx: Load, @@ -3053,20 +3692,25 @@ Module( }, ParameterWithDefault { range: 2328..2336, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2328..2336, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("y"), range: 2328..2329, + node_index: AtomicNodeIndex(..), }, annotation: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2331..2336, value: StringLiteralValue { inner: Single( StringLiteral { range: 2331..2336, + node_index: AtomicNodeIndex(..), value: "str", flags: StringLiteralFlags { quote_style: Double, @@ -3084,18 +3728,23 @@ Module( }, ParameterWithDefault { range: 2338..2346, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2338..2346, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("z"), range: 2338..2339, + node_index: AtomicNodeIndex(..), }, annotation: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 2341..2346, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2341..2342, value: Int( 1, @@ -3105,6 +3754,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2345..2346, value: Int( 2, @@ -3126,6 +3776,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2353..2357, }, ), @@ -3134,25 +3785,33 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 2360..2398, is_async: false, decorator_list: [], name: Identifier { id: Name("foo"), range: 2364..2367, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 2367..2388, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 2368..2372, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2368..2372, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("self"), range: 2368..2372, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -3160,17 +3819,21 @@ Module( }, ParameterWithDefault { range: 2374..2377, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2374..2375, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 2374..2375, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2376..2377, value: Int( 1, @@ -3181,17 +3844,21 @@ Module( }, ParameterWithDefault { range: 2379..2382, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2379..2380, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 2379..2380, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2381..2382, value: Int( 2, @@ -3202,17 +3869,21 @@ Module( }, ParameterWithDefault { range: 2384..2387, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 2384..2385, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 2384..2385, + node_index: AtomicNodeIndex(..), }, annotation: None, }, default: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2386..2387, value: Int( 3, @@ -3230,6 +3901,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2394..2398, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__if.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__if.py.snap index 592cb88bad05e..51edcda2945ba 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__if.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__if.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/if.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..375, body: [ If( StmtIf { + node_index: AtomicNodeIndex(..), range: 0..28, test: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3..4, value: Int( 1, @@ -24,9 +26,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 6..8, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 6..8, value: Int( 10, @@ -39,9 +43,11 @@ Module( elif_else_clauses: [ ElifElseClause { range: 9..19, + node_index: AtomicNodeIndex(..), test: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 14..15, value: Int( 2, @@ -52,9 +58,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 17..19, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 17..19, value: Int( 20, @@ -67,13 +75,16 @@ Module( }, ElifElseClause { range: 20..28, + node_index: AtomicNodeIndex(..), test: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 26..28, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 26..28, value: Int( 30, @@ -89,9 +100,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 30..52, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 33..37, value: true, }, @@ -99,9 +112,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..44, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 43..44, value: Int( 1, @@ -112,9 +127,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 49..52, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 49..52, }, ), @@ -126,12 +143,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 53..85, test: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 56..61, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("x"), ctx: Load, @@ -143,6 +163,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 60..61, value: Int( 1, @@ -155,9 +176,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 67..70, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 67..70, }, ), @@ -167,10 +190,12 @@ Module( elif_else_clauses: [ ElifElseClause { range: 71..85, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 81..85, }, ), @@ -181,9 +206,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 87..117, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("a"), ctx: Load, @@ -192,6 +219,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 97..101, }, ), @@ -199,9 +227,11 @@ Module( elif_else_clauses: [ ElifElseClause { range: 102..117, + node_index: AtomicNodeIndex(..), test: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 107..108, id: Name("b"), ctx: Load, @@ -211,9 +241,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 114..117, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 114..117, }, ), @@ -226,14 +258,17 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 119..203, test: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 122..129, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..123, id: Name("a"), ctx: Load, @@ -241,6 +276,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..129, id: Name("b"), ctx: Load, @@ -252,9 +288,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 135..138, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 135..138, }, ), @@ -264,9 +302,11 @@ Module( elif_else_clauses: [ ElifElseClause { range: 139..157, + node_index: AtomicNodeIndex(..), test: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 144..148, value: true, }, @@ -275,9 +315,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 154..157, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 154..157, }, ), @@ -287,9 +329,11 @@ Module( }, ElifElseClause { range: 158..173, + node_index: AtomicNodeIndex(..), test: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 163..164, id: Name("c"), ctx: Load, @@ -299,9 +343,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 170..173, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 170..173, }, ), @@ -311,9 +357,11 @@ Module( }, ElifElseClause { range: 174..189, + node_index: AtomicNodeIndex(..), test: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 179..180, id: Name("d"), ctx: Load, @@ -323,9 +371,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 186..189, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 186..189, }, ), @@ -335,16 +385,20 @@ Module( }, ElifElseClause { range: 190..203, + node_index: AtomicNodeIndex(..), test: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 200..203, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 200..203, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 200..201, id: Name("f"), ctx: Load, @@ -352,6 +406,7 @@ Module( ), arguments: Arguments { range: 201..203, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -366,12 +421,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 229..260, test: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 232..238, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 232..233, id: Name("a"), ctx: Store, @@ -379,6 +437,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 237..238, id: Name("b"), ctx: Load, @@ -389,9 +448,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 240..243, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 240..243, }, ), @@ -401,12 +462,15 @@ Module( elif_else_clauses: [ ElifElseClause { range: 244..260, + node_index: AtomicNodeIndex(..), test: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 249..255, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 249..250, id: Name("a"), ctx: Store, @@ -414,6 +478,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 254..255, id: Name("b"), ctx: Load, @@ -425,9 +490,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 257..260, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 257..260, }, ), @@ -440,22 +507,30 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 261..302, test: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 264..275, parameters: Some( Parameters { range: 271..272, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 271..272, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 271..272, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 271..272, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -469,6 +544,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 274..275, id: Name("x"), ctx: Load, @@ -479,9 +555,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 277..280, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 277..280, }, ), @@ -491,22 +569,30 @@ Module( elif_else_clauses: [ ElifElseClause { range: 281..302, + node_index: AtomicNodeIndex(..), test: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 286..297, parameters: Some( Parameters { range: 293..294, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 293..294, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 293..294, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 293..294, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -520,6 +606,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 296..297, id: Name("x"), ctx: Load, @@ -531,9 +618,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 299..302, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 299..302, }, ), @@ -546,12 +635,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 303..336, test: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 306..313, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 312..313, id: Name("x"), ctx: Load, @@ -562,9 +654,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 315..318, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 315..318, }, ), @@ -574,12 +668,15 @@ Module( elif_else_clauses: [ ElifElseClause { range: 319..336, + node_index: AtomicNodeIndex(..), test: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 324..331, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 330..331, id: Name("x"), ctx: Load, @@ -591,9 +688,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 333..336, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 333..336, }, ), @@ -606,13 +705,16 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 337..374, test: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 341..348, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 347..348, id: Name("x"), ctx: Load, @@ -624,9 +726,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 351..354, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 351..354, }, ), @@ -636,13 +740,16 @@ Module( elif_else_clauses: [ ElifElseClause { range: 355..374, + node_index: AtomicNodeIndex(..), test: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 361..368, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 367..368, id: Name("x"), ctx: Load, @@ -655,9 +762,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 371..374, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 371..374, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap index 37d5ef833ac36..4d4d08b9635b8 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__import.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/import.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..92, body: [ Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 0..8, names: [ Alias { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 7..8, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -27,13 +30,16 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 9..21, names: [ Alias { range: 16..21, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a.b.c"), range: 16..21, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -42,18 +48,22 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 22..39, names: [ Alias { range: 29..39, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a.b.c"), range: 29..34, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("d"), range: 38..39, + node_index: AtomicNodeIndex(..), }, ), }, @@ -62,29 +72,36 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 40..54, names: [ Alias { range: 47..48, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a"), range: 47..48, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 50..51, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("b"), range: 50..51, + node_index: AtomicNodeIndex(..), }, asname: None, }, Alias { range: 53..54, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("c"), range: 53..54, + node_index: AtomicNodeIndex(..), }, asname: None, }, @@ -93,31 +110,38 @@ Module( ), Import( StmtImport { + node_index: AtomicNodeIndex(..), range: 55..91, names: [ Alias { range: 62..74, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("foo.bar"), range: 62..69, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("a"), range: 73..74, + node_index: AtomicNodeIndex(..), }, ), }, Alias { range: 76..91, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("a.b.c.d"), range: 76..83, + node_index: AtomicNodeIndex(..), }, asname: Some( Identifier { id: Name("abcd"), range: 87..91, + node_index: AtomicNodeIndex(..), }, ), }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap index ec88a56393c91..1a7192474dcd7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/valid/statement/match.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..5770, body: [ Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 67..103, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("x"), ctx: Load, @@ -22,15 +25,19 @@ Module( cases: [ MatchCase { range: 80..103, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 85..88, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 85..88, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 86..88, value: Complex { real: 0.0, @@ -46,10 +53,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 98..103, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 98..99, id: Name("y"), ctx: Store, @@ -58,6 +67,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 102..103, value: Int( 0, @@ -73,9 +83,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 126..167, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("x"), ctx: Load, @@ -84,11 +96,14 @@ Module( cases: [ MatchCase { range: 139..167, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 144..152, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 144..149, id: Name("bytes"), ctx: Load, @@ -96,15 +111,18 @@ Module( ), arguments: PatternArguments { range: 149..152, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 150..151, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 150..151, + node_index: AtomicNodeIndex(..), }, ), }, @@ -118,10 +136,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 162..167, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 162..163, id: Name("y"), ctx: Store, @@ -130,6 +150,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 166..167, value: Int( 0, @@ -145,9 +166,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 190..260, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 196..197, id: Name("x"), ctx: Load, @@ -156,11 +179,14 @@ Module( cases: [ MatchCase { range: 203..229, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 208..209, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 208..209, value: Int( 0, @@ -172,6 +198,7 @@ Module( guard: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 213..214, value: Int( 0, @@ -182,10 +209,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 224..229, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 224..225, id: Name("y"), ctx: Store, @@ -194,6 +223,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 228..229, value: Int( 0, @@ -206,11 +236,14 @@ Module( }, MatchCase { range: 234..260, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 239..240, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 239..240, value: Int( 0, @@ -222,6 +255,7 @@ Module( guard: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 244..245, value: Int( 1, @@ -232,10 +266,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 255..260, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 255..256, id: Name("y"), ctx: Store, @@ -244,6 +280,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 259..260, value: Int( 1, @@ -259,9 +296,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 283..332, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 289..290, value: Int( 3, @@ -271,15 +310,19 @@ Module( cases: [ MatchCase { range: 296..332, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 301..314, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 301..302, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 301..302, value: Int( 0, @@ -291,8 +334,10 @@ Module( MatchValue( PatternMatchValue { range: 305..306, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 305..306, value: Int( 1, @@ -304,8 +349,10 @@ Module( MatchValue( PatternMatchValue { range: 309..310, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 309..310, value: Int( 2, @@ -317,8 +364,10 @@ Module( MatchValue( PatternMatchValue { range: 313..314, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 313..314, value: Int( 3, @@ -334,10 +383,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 324..332, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 324..325, id: Name("x"), ctx: Store, @@ -346,6 +397,7 @@ Module( ], value: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 328..332, value: true, }, @@ -359,9 +411,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 355..403, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 361..362, id: Name("x"), ctx: Load, @@ -370,19 +424,24 @@ Module( cases: [ MatchCase { range: 368..403, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 373..388, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 373..379, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 374..375, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 374..375, value: Int( 0, @@ -394,8 +453,10 @@ Module( MatchValue( PatternMatchValue { range: 377..378, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 377..378, value: Int( 1, @@ -410,12 +471,15 @@ Module( MatchSequence( PatternMatchSequence { range: 382..388, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 383..384, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 383..384, value: Int( 1, @@ -427,8 +491,10 @@ Module( MatchValue( PatternMatchValue { range: 386..387, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 386..387, value: Int( 0, @@ -447,10 +513,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 398..403, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 398..399, id: Name("y"), ctx: Store, @@ -459,6 +527,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 402..403, value: Int( 0, @@ -474,9 +543,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 445..523, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 451..452, id: Name("x"), ctx: Load, @@ -485,13 +556,16 @@ Module( cases: [ MatchCase { range: 458..489, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 463..467, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 464..466, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -502,15 +576,18 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 477..489, value: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 484..489, value: StringLiteralValue { inner: Single( StringLiteral { range: 484..489, + node_index: AtomicNodeIndex(..), value: "seq", flags: StringLiteralFlags { quote_style: Double, @@ -529,9 +606,11 @@ Module( }, MatchCase { range: 494..523, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 499..501, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -541,15 +620,18 @@ Module( body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 511..523, value: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 518..523, value: StringLiteralValue { inner: Single( StringLiteral { range: 518..523, + node_index: AtomicNodeIndex(..), value: "map", flags: StringLiteralFlags { quote_style: Double, @@ -571,9 +653,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 546..714, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 552..553, id: Name("x"), ctx: Load, @@ -582,12 +666,15 @@ Module( cases: [ MatchCase { range: 559..594, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 564..579, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 565..566, value: Int( 0, @@ -599,12 +686,15 @@ Module( MatchSequence( PatternMatchSequence { range: 568..578, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 569..570, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 569..570, value: Int( 1, @@ -616,8 +706,10 @@ Module( MatchValue( PatternMatchValue { range: 572..573, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 572..573, value: Int( 2, @@ -629,6 +721,7 @@ Module( MatchMapping( PatternMatchMapping { range: 575..577, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -645,10 +738,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 589..594, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 589..590, id: Name("y"), ctx: Store, @@ -657,6 +752,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 593..594, value: Int( 0, @@ -669,16 +765,20 @@ Module( }, MatchCase { range: 599..687, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 604..672, + node_index: AtomicNodeIndex(..), patterns: [ MatchMapping( PatternMatchMapping { range: 604..626, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 605..606, value: Int( 0, @@ -690,16 +790,20 @@ Module( MatchOr( PatternMatchOr { range: 608..625, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 608..618, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 609..610, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 609..610, value: Int( 1, @@ -711,8 +815,10 @@ Module( MatchValue( PatternMatchValue { range: 612..613, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 612..613, value: Int( 2, @@ -724,6 +830,7 @@ Module( MatchMapping( PatternMatchMapping { range: 615..617, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -735,6 +842,7 @@ Module( MatchSingleton( PatternMatchSingleton { range: 621..625, + node_index: AtomicNodeIndex(..), value: True, }, ), @@ -748,9 +856,11 @@ Module( MatchMapping( PatternMatchMapping { range: 629..638, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 630..631, value: Int( 1, @@ -762,10 +872,12 @@ Module( MatchSequence( PatternMatchSequence { range: 633..637, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 634..636, + node_index: AtomicNodeIndex(..), patterns: [], }, ), @@ -779,9 +891,11 @@ Module( MatchMapping( PatternMatchMapping { range: 641..656, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 642..643, value: Int( 0, @@ -793,12 +907,15 @@ Module( MatchSequence( PatternMatchSequence { range: 645..655, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 646..647, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 646..647, value: Int( 1, @@ -810,8 +927,10 @@ Module( MatchValue( PatternMatchValue { range: 649..650, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 649..650, value: Int( 2, @@ -823,6 +942,7 @@ Module( MatchMapping( PatternMatchMapping { range: 652..654, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -838,19 +958,23 @@ Module( MatchSequence( PatternMatchSequence { range: 659..661, + node_index: AtomicNodeIndex(..), patterns: [], }, ), MatchValue( PatternMatchValue { range: 664..667, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 664..667, value: StringLiteralValue { inner: Single( StringLiteral { range: 664..667, + node_index: AtomicNodeIndex(..), value: "X", flags: StringLiteralFlags { quote_style: Double, @@ -867,6 +991,7 @@ Module( MatchMapping( PatternMatchMapping { range: 670..672, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -879,10 +1004,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 682..687, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 682..683, id: Name("y"), ctx: Store, @@ -891,6 +1018,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 686..687, value: Int( 1, @@ -903,9 +1031,11 @@ Module( }, MatchCase { range: 692..714, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 697..699, + node_index: AtomicNodeIndex(..), patterns: [], }, ), @@ -913,10 +1043,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 709..714, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 709..710, id: Name("y"), ctx: Store, @@ -925,6 +1057,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 713..714, value: Int( 2, @@ -940,9 +1073,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 737..782, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 743..744, id: Name("x"), ctx: Load, @@ -951,14 +1086,18 @@ Module( cases: [ MatchCase { range: 750..782, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 755..767, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 755..767, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 755..759, value: Float( 0.25, @@ -968,6 +1107,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 762..767, value: Complex { real: 0.0, @@ -983,10 +1123,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 777..782, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 777..778, id: Name("y"), ctx: Store, @@ -995,6 +1137,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 781..782, value: Int( 0, @@ -1010,9 +1153,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 805..841, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 811..812, id: Name("x"), ctx: Load, @@ -1021,15 +1166,19 @@ Module( cases: [ MatchCase { range: 818..841, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 823..826, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 823..826, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 824..826, value: Complex { real: 0.0, @@ -1045,10 +1194,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 836..841, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 836..837, id: Name("y"), ctx: Store, @@ -1057,6 +1208,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 840..841, value: Int( 0, @@ -1072,9 +1224,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 864..913, subject: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 870..871, value: Int( 4, @@ -1084,15 +1238,19 @@ Module( cases: [ MatchCase { range: 877..913, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 882..895, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 882..883, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 882..883, value: Int( 0, @@ -1104,8 +1262,10 @@ Module( MatchValue( PatternMatchValue { range: 886..887, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 886..887, value: Int( 1, @@ -1117,8 +1277,10 @@ Module( MatchValue( PatternMatchValue { range: 890..891, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 890..891, value: Int( 2, @@ -1130,8 +1292,10 @@ Module( MatchValue( PatternMatchValue { range: 894..895, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 894..895, value: Int( 3, @@ -1147,10 +1311,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 905..913, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 905..906, id: Name("x"), ctx: Store, @@ -1159,6 +1325,7 @@ Module( ], value: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 909..913, value: true, }, @@ -1172,9 +1339,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 936..975, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 942..943, id: Name("x"), ctx: Load, @@ -1183,11 +1352,14 @@ Module( cases: [ MatchCase { range: 949..975, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 954..955, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 954..955, value: Int( 0, @@ -1199,6 +1371,7 @@ Module( guard: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 959..960, id: Name("x"), ctx: Load, @@ -1208,10 +1381,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 970..975, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 970..971, id: Name("y"), ctx: Store, @@ -1220,6 +1395,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 974..975, value: Int( 0, @@ -1235,9 +1411,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 998..1098, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1004..1005, id: Name("x"), ctx: Load, @@ -1246,12 +1424,15 @@ Module( cases: [ MatchCase { range: 1011..1037, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 1016..1022, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1017..1018, value: Int( 1, @@ -1263,8 +1444,10 @@ Module( MatchValue( PatternMatchValue { range: 1020..1021, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1020..1021, value: Int( 0, @@ -1281,10 +1464,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1032..1037, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1032..1033, id: Name("y"), ctx: Store, @@ -1293,6 +1478,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1036..1037, value: Int( 0, @@ -1305,12 +1491,15 @@ Module( }, MatchCase { range: 1042..1068, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 1047..1053, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1048..1049, value: Int( 0, @@ -1322,8 +1511,10 @@ Module( MatchValue( PatternMatchValue { range: 1051..1052, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1051..1052, value: Int( 0, @@ -1340,10 +1531,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1063..1068, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1063..1064, id: Name("y"), ctx: Store, @@ -1352,6 +1545,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1067..1068, value: Int( 1, @@ -1364,15 +1558,18 @@ Module( }, MatchCase { range: 1073..1098, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 1078..1083, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: Some( Identifier { id: Name("z"), range: 1081..1082, + node_index: AtomicNodeIndex(..), }, ), }, @@ -1381,10 +1578,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1093..1098, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1093..1094, id: Name("y"), ctx: Store, @@ -1393,6 +1592,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1097..1098, value: Int( 2, @@ -1408,12 +1608,15 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1121..1162, subject: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1127..1132, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1127..1130, id: Name("Seq"), ctx: Load, @@ -1421,6 +1624,7 @@ Module( ), arguments: Arguments { range: 1130..1132, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -1429,13 +1633,16 @@ Module( cases: [ MatchCase { range: 1138..1162, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1143..1147, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 1144..1146, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -1446,10 +1653,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1157..1162, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1157..1158, id: Name("y"), ctx: Store, @@ -1458,6 +1667,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1161..1162, value: Int( 0, @@ -1473,9 +1683,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1185..1245, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1191..1192, id: Name("x"), ctx: Load, @@ -1484,11 +1696,14 @@ Module( cases: [ MatchCase { range: 1198..1219, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 1203..1204, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1203..1204, value: Int( 1, @@ -1501,10 +1716,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1214..1219, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1214..1215, id: Name("y"), ctx: Store, @@ -1513,6 +1730,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1218..1219, value: Int( 0, @@ -1525,11 +1743,14 @@ Module( }, MatchCase { range: 1224..1245, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 1229..1230, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1229..1230, value: Int( 1, @@ -1542,10 +1763,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1240..1245, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1240..1241, id: Name("y"), ctx: Store, @@ -1554,6 +1777,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1244..1245, value: Int( 1, @@ -1569,9 +1793,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1268..1315, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1274..1275, id: Name("x"), ctx: Load, @@ -1580,17 +1806,21 @@ Module( cases: [ MatchCase { range: 1281..1315, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 1286..1298, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 1287..1292, value: StringLiteralValue { inner: Single( StringLiteral { range: 1287..1292, + node_index: AtomicNodeIndex(..), value: "foo", flags: StringLiteralFlags { quote_style: Double, @@ -1607,11 +1837,13 @@ Module( MatchAs( PatternMatchAs { range: 1294..1297, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("bar"), range: 1294..1297, + node_index: AtomicNodeIndex(..), }, ), }, @@ -1624,10 +1856,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1308..1315, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1308..1309, id: Name("y"), ctx: Store, @@ -1636,6 +1870,7 @@ Module( ], value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1312..1315, id: Name("bar"), ctx: Load, @@ -1650,13 +1885,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1338..1392, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1344..1353, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1345..1346, value: Int( 0, @@ -1665,6 +1903,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1348..1349, value: Int( 1, @@ -1673,6 +1912,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1351..1352, value: Int( 2, @@ -1687,15 +1927,19 @@ Module( cases: [ MatchCase { range: 1359..1392, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1364..1377, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 1365..1366, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1365..1366, value: Int( 0, @@ -1707,8 +1951,10 @@ Module( MatchValue( PatternMatchValue { range: 1368..1369, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1368..1369, value: Int( 1, @@ -1720,10 +1966,12 @@ Module( MatchStar( PatternMatchStar { range: 1371..1373, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("x"), range: 1372..1373, + node_index: AtomicNodeIndex(..), }, ), }, @@ -1731,8 +1979,10 @@ Module( MatchValue( PatternMatchValue { range: 1375..1376, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1375..1376, value: Int( 2, @@ -1748,10 +1998,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1387..1392, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1387..1388, id: Name("y"), ctx: Store, @@ -1760,6 +2012,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1391..1392, value: Int( 0, @@ -1775,9 +2028,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1415..1529, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1421..1422, id: Name("x"), ctx: Load, @@ -1786,15 +2041,19 @@ Module( cases: [ MatchCase { range: 1428..1451, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1433..1436, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 1434..1435, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1434..1435, value: Int( 0, @@ -1810,10 +2069,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1446..1451, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1446..1447, id: Name("y"), ctx: Store, @@ -1822,6 +2083,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1450..1451, value: Int( 0, @@ -1834,15 +2096,19 @@ Module( }, MatchCase { range: 1456..1498, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1461..1467, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 1462..1463, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1462..1463, value: Int( 1, @@ -1854,8 +2120,10 @@ Module( MatchValue( PatternMatchValue { range: 1465..1466, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1465..1466, value: Int( 0, @@ -1870,9 +2138,11 @@ Module( guard: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1472..1482, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1472..1473, id: Name("x"), ctx: Store, @@ -1880,9 +2150,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1477..1482, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1477..1478, id: Name("x"), ctx: Load, @@ -1890,11 +2162,13 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 1479..1481, lower: None, upper: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1480..1481, value: Int( 0, @@ -1914,10 +2188,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1493..1498, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1493..1494, id: Name("y"), ctx: Store, @@ -1926,6 +2202,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1497..1498, value: Int( 1, @@ -1938,15 +2215,19 @@ Module( }, MatchCase { range: 1503..1529, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1508..1514, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 1509..1510, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1509..1510, value: Int( 1, @@ -1958,8 +2239,10 @@ Module( MatchValue( PatternMatchValue { range: 1512..1513, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1512..1513, value: Int( 0, @@ -1975,10 +2258,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1524..1529, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1524..1525, id: Name("y"), ctx: Store, @@ -1987,6 +2272,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1528..1529, value: Int( 2, @@ -2002,9 +2288,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1552..1595, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1558..1559, id: Name("w"), ctx: Load, @@ -2013,18 +2301,22 @@ Module( cases: [ MatchCase { range: 1565..1595, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1570..1580, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 1571..1572, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 1571..1572, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2032,11 +2324,13 @@ Module( MatchAs( PatternMatchAs { range: 1574..1575, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 1574..1575, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2044,6 +2338,7 @@ Module( MatchStar( PatternMatchStar { range: 1577..1579, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -2054,10 +2349,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1590..1595, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1590..1591, id: Name("z"), ctx: Store, @@ -2066,6 +2363,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1594..1595, value: Int( 0, @@ -2081,9 +2379,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1618..1664, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1624..1625, id: Name("x"), ctx: Load, @@ -2092,18 +2392,23 @@ Module( cases: [ MatchCase { range: 1631..1664, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 1636..1649, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1636..1649, left: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 1636..1641, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1637..1641, value: Float( 0.25, @@ -2115,6 +2420,7 @@ Module( op: Sub, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1644..1649, value: Complex { real: 0.0, @@ -2130,10 +2436,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1659..1664, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1659..1660, id: Name("y"), ctx: Store, @@ -2142,6 +2450,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1663..1664, value: Int( 0, @@ -2157,13 +2466,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1687..1726, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1693..1697, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1694..1695, id: Name("x"), ctx: Load, @@ -2177,18 +2489,22 @@ Module( cases: [ MatchCase { range: 1703..1726, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 1708..1711, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 1709..1710, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 1709..1710, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2200,10 +2516,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1721..1726, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1721..1722, id: Name("z"), ctx: Store, @@ -2212,6 +2530,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1725..1726, value: Int( 0, @@ -2227,9 +2546,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1749..1789, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1755..1756, id: Name("x"), ctx: Load, @@ -2238,20 +2559,26 @@ Module( cases: [ MatchCase { range: 1762..1789, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 1767..1774, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1767..1774, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1767..1772, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1767..1770, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1767..1768, id: Name("A"), ctx: Load, @@ -2260,6 +2587,7 @@ Module( attr: Identifier { id: Name("B"), range: 1769..1770, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2267,6 +2595,7 @@ Module( attr: Identifier { id: Name("C"), range: 1771..1772, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2274,6 +2603,7 @@ Module( attr: Identifier { id: Name("D"), range: 1773..1774, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2284,10 +2614,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1784..1789, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1784..1785, id: Name("y"), ctx: Store, @@ -2296,6 +2628,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1788..1789, value: Int( 0, @@ -2311,9 +2644,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1812..1849, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1818..1819, id: Name("x"), ctx: Load, @@ -2322,9 +2657,11 @@ Module( cases: [ MatchCase { range: 1825..1849, + node_index: AtomicNodeIndex(..), pattern: MatchSingleton( PatternMatchSingleton { range: 1830..1834, + node_index: AtomicNodeIndex(..), value: None, }, ), @@ -2332,10 +2669,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1844..1849, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1844..1845, id: Name("y"), ctx: Store, @@ -2344,6 +2683,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1848..1849, value: Int( 0, @@ -2359,9 +2699,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1872..1906, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1878..1879, id: Name("x"), ctx: Load, @@ -2370,11 +2712,14 @@ Module( cases: [ MatchCase { range: 1885..1906, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 1890..1891, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1890..1891, value: Int( 0, @@ -2387,10 +2732,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1901..1906, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1901..1902, id: Name("y"), ctx: Store, @@ -2399,6 +2746,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1905..1906, value: Int( 0, @@ -2414,9 +2762,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1929..1967, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1935..1936, id: Name("x"), ctx: Load, @@ -2425,9 +2775,11 @@ Module( cases: [ MatchCase { range: 1942..1967, + node_index: AtomicNodeIndex(..), pattern: MatchSingleton( PatternMatchSingleton { range: 1947..1952, + node_index: AtomicNodeIndex(..), value: False, }, ), @@ -2435,10 +2787,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1962..1967, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1962..1963, id: Name("y"), ctx: Store, @@ -2447,6 +2801,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1966..1967, value: Int( 0, @@ -2462,9 +2817,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 1990..2081, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1996..1997, id: Name("x"), ctx: Load, @@ -2473,9 +2830,11 @@ Module( cases: [ MatchCase { range: 2003..2025, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2008..2010, + node_index: AtomicNodeIndex(..), patterns: [], }, ), @@ -2483,10 +2842,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2020..2025, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2020..2021, id: Name("y"), ctx: Store, @@ -2495,6 +2856,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2024..2025, value: Int( 0, @@ -2507,20 +2869,25 @@ Module( }, MatchCase { range: 2030..2054, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2035..2039, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 2036..2038, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2036..2038, value: StringLiteralValue { inner: Single( StringLiteral { range: 2036..2038, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Double, @@ -2541,10 +2908,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2049..2054, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2049..2050, id: Name("y"), ctx: Store, @@ -2553,6 +2922,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2053..2054, value: Int( 1, @@ -2565,16 +2935,20 @@ Module( }, MatchCase { range: 2059..2081, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 2064..2066, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2064..2066, value: StringLiteralValue { inner: Single( StringLiteral { range: 2064..2066, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Double, @@ -2592,10 +2966,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2076..2081, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2076..2077, id: Name("y"), ctx: Store, @@ -2604,6 +2980,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2080..2081, value: Int( 2, @@ -2619,9 +2996,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2104..2138, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2110..2111, id: Name("x"), ctx: Load, @@ -2630,14 +3009,17 @@ Module( cases: [ MatchCase { range: 2117..2138, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 2122..2123, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 2122..2123, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2646,10 +3028,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2133..2138, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2133..2134, id: Name("y"), ctx: Store, @@ -2658,6 +3042,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2137..2138, value: Int( 0, @@ -2673,9 +3058,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2161..2207, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2167..2168, id: Name("w"), ctx: Load, @@ -2684,18 +3071,22 @@ Module( cases: [ MatchCase { range: 2174..2207, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2179..2192, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 2180..2181, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("x"), range: 2180..2181, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2703,11 +3094,13 @@ Module( MatchAs( PatternMatchAs { range: 2183..2184, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 2183..2184, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2715,10 +3108,12 @@ Module( MatchStar( PatternMatchStar { range: 2186..2191, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("rest"), range: 2187..2191, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2730,10 +3125,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2202..2207, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2202..2203, id: Name("z"), ctx: Store, @@ -2742,6 +3139,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2206..2207, value: Int( 0, @@ -2757,9 +3155,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2230..2307, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2236..2237, id: Name("x"), ctx: Load, @@ -2768,19 +3168,24 @@ Module( cases: [ MatchCase { range: 2243..2307, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 2248..2278, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 2249..2255, + node_index: AtomicNodeIndex(..), pattern: Some( MatchValue( PatternMatchValue { range: 2249..2250, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2249..2250, value: Int( 0, @@ -2794,6 +3199,7 @@ Module( Identifier { id: Name("z"), range: 2254..2255, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2801,12 +3207,15 @@ Module( MatchAs( PatternMatchAs { range: 2260..2266, + node_index: AtomicNodeIndex(..), pattern: Some( MatchValue( PatternMatchValue { range: 2260..2261, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2260..2261, value: Int( 1, @@ -2820,6 +3229,7 @@ Module( Identifier { id: Name("z"), range: 2265..2266, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2827,12 +3237,15 @@ Module( MatchAs( PatternMatchAs { range: 2271..2277, + node_index: AtomicNodeIndex(..), pattern: Some( MatchValue( PatternMatchValue { range: 2271..2272, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2271..2272, value: Int( 2, @@ -2846,6 +3259,7 @@ Module( Identifier { id: Name("z"), range: 2276..2277, + node_index: AtomicNodeIndex(..), }, ), }, @@ -2856,9 +3270,11 @@ Module( guard: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 2282..2292, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2282..2283, id: Name("z"), ctx: Load, @@ -2870,9 +3286,11 @@ Module( comparators: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 2287..2292, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2287..2288, id: Name("x"), ctx: Load, @@ -2881,6 +3299,7 @@ Module( op: Mod, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2291..2292, value: Int( 2, @@ -2896,10 +3315,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2302..2307, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2302..2303, id: Name("y"), ctx: Store, @@ -2908,6 +3329,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2306..2307, value: Int( 0, @@ -2923,9 +3345,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2330..2499, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2336..2337, id: Name("x"), ctx: Load, @@ -2934,12 +3358,15 @@ Module( cases: [ MatchCase { range: 2343..2378, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 2348..2363, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2349..2350, value: Int( 0, @@ -2951,12 +3378,15 @@ Module( MatchSequence( PatternMatchSequence { range: 2352..2362, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 2353..2354, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2353..2354, value: Int( 1, @@ -2968,8 +3398,10 @@ Module( MatchValue( PatternMatchValue { range: 2356..2357, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2356..2357, value: Int( 2, @@ -2981,6 +3413,7 @@ Module( MatchMapping( PatternMatchMapping { range: 2359..2361, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -2997,10 +3430,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2373..2378, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2373..2374, id: Name("y"), ctx: Store, @@ -3009,6 +3444,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2377..2378, value: Int( 0, @@ -3021,16 +3457,20 @@ Module( }, MatchCase { range: 2383..2472, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 2388..2457, + node_index: AtomicNodeIndex(..), patterns: [ MatchMapping( PatternMatchMapping { range: 2388..2411, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2389..2390, value: Int( 0, @@ -3042,16 +3482,20 @@ Module( MatchOr( PatternMatchOr { range: 2392..2410, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 2392..2402, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 2393..2394, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2393..2394, value: Int( 1, @@ -3063,8 +3507,10 @@ Module( MatchValue( PatternMatchValue { range: 2396..2397, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2396..2397, value: Int( 2, @@ -3076,6 +3522,7 @@ Module( MatchMapping( PatternMatchMapping { range: 2399..2401, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -3087,6 +3534,7 @@ Module( MatchSingleton( PatternMatchSingleton { range: 2405..2410, + node_index: AtomicNodeIndex(..), value: False, }, ), @@ -3100,9 +3548,11 @@ Module( MatchMapping( PatternMatchMapping { range: 2414..2423, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2415..2416, value: Int( 1, @@ -3114,10 +3564,12 @@ Module( MatchSequence( PatternMatchSequence { range: 2418..2422, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 2419..2421, + node_index: AtomicNodeIndex(..), patterns: [], }, ), @@ -3131,9 +3583,11 @@ Module( MatchMapping( PatternMatchMapping { range: 2426..2441, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2427..2428, value: Int( 0, @@ -3145,12 +3599,15 @@ Module( MatchSequence( PatternMatchSequence { range: 2430..2440, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 2431..2432, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2431..2432, value: Int( 1, @@ -3162,8 +3619,10 @@ Module( MatchValue( PatternMatchValue { range: 2434..2435, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2434..2435, value: Int( 2, @@ -3175,6 +3634,7 @@ Module( MatchMapping( PatternMatchMapping { range: 2437..2439, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -3190,19 +3650,23 @@ Module( MatchSequence( PatternMatchSequence { range: 2444..2446, + node_index: AtomicNodeIndex(..), patterns: [], }, ), MatchValue( PatternMatchValue { range: 2449..2452, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2449..2452, value: StringLiteralValue { inner: Single( StringLiteral { range: 2449..2452, + node_index: AtomicNodeIndex(..), value: "X", flags: StringLiteralFlags { quote_style: Double, @@ -3219,6 +3683,7 @@ Module( MatchMapping( PatternMatchMapping { range: 2455..2457, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: None, @@ -3231,10 +3696,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2467..2472, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2467..2468, id: Name("y"), ctx: Store, @@ -3243,6 +3710,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2471..2472, value: Int( 1, @@ -3255,9 +3723,11 @@ Module( }, MatchCase { range: 2477..2499, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2482..2484, + node_index: AtomicNodeIndex(..), patterns: [], }, ), @@ -3265,10 +3735,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2494..2499, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2494..2495, id: Name("y"), ctx: Store, @@ -3277,6 +3749,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2498..2499, value: Int( 2, @@ -3292,13 +3765,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2522..2568, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2528..2537, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2529..2530, value: Int( 0, @@ -3307,6 +3783,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2532..2533, value: Int( 1, @@ -3315,6 +3792,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2535..2536, value: Int( 2, @@ -3329,15 +3807,19 @@ Module( cases: [ MatchCase { range: 2543..2568, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2548..2553, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 2548..2549, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2548..2549, value: Int( 0, @@ -3349,10 +3831,12 @@ Module( MatchStar( PatternMatchStar { range: 2551..2553, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("x"), range: 2552..2553, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3364,10 +3848,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2563..2568, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2563..2564, id: Name("y"), ctx: Store, @@ -3376,6 +3862,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2567..2568, value: Int( 0, @@ -3391,13 +3878,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2591..2638, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2597..2606, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2598..2599, value: Int( 0, @@ -3406,6 +3896,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2601..2602, value: Int( 1, @@ -3414,6 +3905,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2604..2605, value: Int( 2, @@ -3428,17 +3920,21 @@ Module( cases: [ MatchCase { range: 2612..2638, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2617..2623, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 2617..2619, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("x"), range: 2618..2619, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3446,8 +3942,10 @@ Module( MatchValue( PatternMatchValue { range: 2621..2622, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2621..2622, value: Int( 2, @@ -3463,10 +3961,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2633..2638, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2633..2634, id: Name("y"), ctx: Store, @@ -3475,6 +3975,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2637..2638, value: Int( 0, @@ -3490,13 +3991,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2661..2697, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2667..2669, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2667..2668, id: Name("x"), ctx: Load, @@ -3510,18 +4014,22 @@ Module( cases: [ MatchCase { range: 2675..2697, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2680..2682, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 2680..2681, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 2680..2681, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3533,10 +4041,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2692..2697, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2692..2693, id: Name("z"), ctx: Store, @@ -3545,6 +4055,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2696..2697, value: Int( 0, @@ -3560,13 +4071,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2720..2760, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2726..2730, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2726..2727, id: Name("w"), ctx: Load, @@ -3574,6 +4088,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2729..2730, id: Name("x"), ctx: Load, @@ -3587,18 +4102,22 @@ Module( cases: [ MatchCase { range: 2736..2760, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2741..2745, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 2741..2742, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 2741..2742, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3606,11 +4125,13 @@ Module( MatchAs( PatternMatchAs { range: 2744..2745, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 2744..2745, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3622,10 +4143,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2755..2760, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2755..2756, id: Name("v"), ctx: Store, @@ -3634,6 +4157,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2759..2760, value: Int( 0, @@ -3649,16 +4173,20 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2783..2829, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 2789..2796, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 2789..2795, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2789..2790, id: Name("w"), ctx: Store, @@ -3666,6 +4194,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2794..2795, id: Name("x"), ctx: Load, @@ -3681,22 +4210,27 @@ Module( cases: [ MatchCase { range: 2802..2829, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 2807..2814, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 2807..2813, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 2807..2808, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("y"), range: 2807..2808, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3706,6 +4240,7 @@ Module( Identifier { id: Name("v"), range: 2812..2813, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3717,10 +4252,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 2824..2829, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2824..2825, id: Name("z"), ctx: Store, @@ -3729,6 +4266,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2828..2829, value: Int( 0, @@ -3744,9 +4282,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2831..2952, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2837..2838, id: Name("x"), ctx: Load, @@ -3755,23 +4295,29 @@ Module( cases: [ MatchCase { range: 2927..2952, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 2932..2938, + node_index: AtomicNodeIndex(..), value: FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 2932..2938, value: FStringValue { inner: Single( FString( FString { range: 2932..2938, + node_index: AtomicNodeIndex(..), elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2934..2937, + node_index: AtomicNodeIndex(..), expression: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 2935..2936, id: Name("y"), ctx: Load, @@ -3800,6 +4346,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 2948..2952, }, ), @@ -3810,20 +4357,24 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 2953..3025, subject: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 2959..2970, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 2960..2966, value: StringLiteralValue { inner: Single( StringLiteral { range: 2960..2966, + node_index: AtomicNodeIndex(..), value: "test", flags: StringLiteralFlags { quote_style: Double, @@ -3838,6 +4389,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 2968..2969, value: Int( 1, @@ -3851,15 +4403,18 @@ Module( cases: [ MatchCase { range: 2976..3025, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 2981..3004, + node_index: AtomicNodeIndex(..), keys: [], patterns: [], rest: Some( Identifier { id: Name("rest"), range: 2993..2997, + node_index: AtomicNodeIndex(..), }, ), }, @@ -3868,12 +4423,15 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3014..3025, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3014..3025, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3014..3019, id: Name("print"), ctx: Load, @@ -3881,9 +4439,11 @@ Module( ), arguments: Arguments { range: 3019..3025, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3020..3024, id: Name("rest"), ctx: Load, @@ -3903,20 +4463,24 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3026..3129, subject: Dict( ExprDict { + node_index: AtomicNodeIndex(..), range: 3032..3049, items: [ DictItem { key: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 3033..3040, value: StringLiteralValue { inner: Single( StringLiteral { range: 3033..3040, + node_index: AtomicNodeIndex(..), value: "label", flags: StringLiteralFlags { quote_style: Double, @@ -3931,11 +4495,13 @@ Module( ), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 3042..3048, value: StringLiteralValue { inner: Single( StringLiteral { range: 3042..3048, + node_index: AtomicNodeIndex(..), value: "test", flags: StringLiteralFlags { quote_style: Double, @@ -3954,17 +4520,21 @@ Module( cases: [ MatchCase { range: 3055..3129, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 3060..3107, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 3070..3077, value: StringLiteralValue { inner: Single( StringLiteral { range: 3070..3077, + node_index: AtomicNodeIndex(..), value: "label", flags: StringLiteralFlags { quote_style: Double, @@ -3981,16 +4551,20 @@ Module( MatchAs( PatternMatchAs { range: 3079..3100, + node_index: AtomicNodeIndex(..), pattern: Some( MatchOr( PatternMatchOr { range: 3079..3091, + node_index: AtomicNodeIndex(..), patterns: [ MatchClass( PatternMatchClass { range: 3079..3084, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3079..3082, id: Name("str"), ctx: Load, @@ -3998,6 +4572,7 @@ Module( ), arguments: PatternArguments { range: 3082..3084, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [], }, @@ -4006,6 +4581,7 @@ Module( MatchSingleton( PatternMatchSingleton { range: 3087..3091, + node_index: AtomicNodeIndex(..), value: None, }, ), @@ -4017,6 +4593,7 @@ Module( Identifier { id: Name("label"), range: 3095..3100, + node_index: AtomicNodeIndex(..), }, ), }, @@ -4029,12 +4606,15 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3117..3129, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 3117..3129, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3117..3122, id: Name("print"), ctx: Load, @@ -4042,9 +4622,11 @@ Module( ), arguments: Arguments { range: 3122..3129, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3123..3128, id: Name("label"), ctx: Load, @@ -4064,9 +4646,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3130..3170, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3136..3137, id: Name("x"), ctx: Load, @@ -4075,15 +4659,19 @@ Module( cases: [ MatchCase { range: 3143..3170, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 3148..3155, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 3149..3150, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3149..3150, value: Int( 0, @@ -4095,8 +4683,10 @@ Module( MatchValue( PatternMatchValue { range: 3152..3153, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3152..3153, value: Int( 1, @@ -4112,10 +4702,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 3165..3170, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3165..3166, id: Name("y"), ctx: Store, @@ -4124,6 +4716,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3169..3170, value: Int( 0, @@ -4139,9 +4732,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3171..3211, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3177..3178, id: Name("x"), ctx: Load, @@ -4150,15 +4745,19 @@ Module( cases: [ MatchCase { range: 3184..3211, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 3189..3196, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 3190..3191, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3190..3191, value: Int( 0, @@ -4170,8 +4769,10 @@ Module( MatchValue( PatternMatchValue { range: 3193..3194, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3193..3194, value: Int( 1, @@ -4187,10 +4788,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 3206..3211, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3206..3207, id: Name("y"), ctx: Store, @@ -4199,6 +4802,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3210..3211, value: Int( 0, @@ -4214,9 +4818,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3212..3249, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3218..3219, id: Name("x"), ctx: Load, @@ -4225,15 +4831,19 @@ Module( cases: [ MatchCase { range: 3225..3249, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 3230..3234, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 3231..3232, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3231..3232, value: Int( 0, @@ -4249,10 +4859,12 @@ Module( body: [ Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 3244..3249, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3244..3245, id: Name("y"), ctx: Store, @@ -4261,6 +4873,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3248..3249, value: Int( 0, @@ -4276,13 +4889,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3250..3284, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3256..3258, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3256..3257, id: Name("x"), ctx: Load, @@ -4296,14 +4912,17 @@ Module( cases: [ MatchCase { range: 3264..3284, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 3269..3270, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 3269..3270, + node_index: AtomicNodeIndex(..), }, ), }, @@ -4312,6 +4931,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 3280..3284, }, ), @@ -4322,13 +4942,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3285..3321, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3291..3295, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3291..3292, id: Name("x"), ctx: Load, @@ -4336,6 +4959,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3294..3295, id: Name("y"), ctx: Load, @@ -4349,14 +4973,17 @@ Module( cases: [ MatchCase { range: 3301..3321, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 3306..3307, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 3306..3307, + node_index: AtomicNodeIndex(..), }, ), }, @@ -4365,6 +4992,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 3317..3321, }, ), @@ -4375,13 +5003,16 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3322..3359, subject: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 3328..3333, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3328..3329, id: Name("x"), ctx: Load, @@ -4389,6 +5020,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3331..3332, id: Name("y"), ctx: Load, @@ -4402,14 +5034,17 @@ Module( cases: [ MatchCase { range: 3339..3359, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 3344..3345, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("z"), range: 3344..3345, + node_index: AtomicNodeIndex(..), }, ), }, @@ -4418,6 +5053,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 3355..3359, }, ), @@ -4428,9 +5064,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3385..3475, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3391..3392, id: Name("x"), ctx: Load, @@ -4439,9 +5077,11 @@ Module( cases: [ MatchCase { range: 3398..3420, + node_index: AtomicNodeIndex(..), pattern: MatchSingleton( PatternMatchSingleton { range: 3403..3407, + node_index: AtomicNodeIndex(..), value: None, }, ), @@ -4449,9 +5089,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3417..3420, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3417..3420, }, ), @@ -4461,9 +5103,11 @@ Module( }, MatchCase { range: 3425..3447, + node_index: AtomicNodeIndex(..), pattern: MatchSingleton( PatternMatchSingleton { range: 3430..3434, + node_index: AtomicNodeIndex(..), value: True, }, ), @@ -4471,9 +5115,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3444..3447, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3444..3447, }, ), @@ -4483,9 +5129,11 @@ Module( }, MatchCase { range: 3452..3475, + node_index: AtomicNodeIndex(..), pattern: MatchSingleton( PatternMatchSingleton { range: 3457..3462, + node_index: AtomicNodeIndex(..), value: False, }, ), @@ -4493,9 +5141,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3472..3475, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3472..3475, }, ), @@ -4508,9 +5158,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3497..3821, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3503..3504, id: Name("x"), ctx: Load, @@ -4519,14 +5171,18 @@ Module( cases: [ MatchCase { range: 3510..3531, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3515..3518, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 3515..3518, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3515..3516, id: Name("a"), ctx: Load, @@ -4535,6 +5191,7 @@ Module( attr: Identifier { id: Name("b"), range: 3517..3518, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -4545,9 +5202,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3528..3531, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3528..3531, }, ), @@ -4557,17 +5216,22 @@ Module( }, MatchCase { range: 3536..3559, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3541..3546, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 3541..3546, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 3541..3544, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3541..3542, id: Name("a"), ctx: Load, @@ -4576,6 +5240,7 @@ Module( attr: Identifier { id: Name("b"), range: 3543..3544, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -4583,6 +5248,7 @@ Module( attr: Identifier { id: Name("c"), range: 3545..3546, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -4593,9 +5259,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3556..3559, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3556..3559, }, ), @@ -4605,16 +5273,20 @@ Module( }, MatchCase { range: 3564..3584, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3569..3571, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 3569..3571, value: StringLiteralValue { inner: Single( StringLiteral { range: 3569..3571, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -4632,9 +5304,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3581..3584, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3581..3584, }, ), @@ -4644,16 +5318,20 @@ Module( }, MatchCase { range: 3589..3610, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3594..3597, + node_index: AtomicNodeIndex(..), value: BytesLiteral( ExprBytesLiteral { + node_index: AtomicNodeIndex(..), range: 3594..3597, value: BytesLiteralValue { inner: Single( BytesLiteral { range: 3594..3597, + node_index: AtomicNodeIndex(..), value: [], flags: BytesLiteralFlags { quote_style: Single, @@ -4671,9 +5349,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3607..3610, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3607..3610, }, ), @@ -4683,11 +5363,14 @@ Module( }, MatchCase { range: 3615..3634, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3620..3621, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3620..3621, value: Int( 1, @@ -4700,9 +5383,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3631..3634, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3631..3634, }, ), @@ -4712,11 +5397,14 @@ Module( }, MatchCase { range: 3639..3660, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3644..3647, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3644..3647, value: Float( 1.0, @@ -4729,9 +5417,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3657..3660, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3657..3660, }, ), @@ -4741,11 +5431,14 @@ Module( }, MatchCase { range: 3665..3687, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3670..3674, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3670..3674, value: Complex { real: 0.0, @@ -4759,9 +5452,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3684..3687, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3684..3687, }, ), @@ -4771,14 +5466,18 @@ Module( }, MatchCase { range: 3692..3716, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3697..3703, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 3697..3703, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3697..3698, value: Int( 1, @@ -4788,6 +5487,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3701..3703, value: Complex { real: 0.0, @@ -4803,9 +5503,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3713..3716, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3713..3716, }, ), @@ -4815,15 +5517,19 @@ Module( }, MatchCase { range: 3721..3741, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3726..3728, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 3726..3728, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3727..3728, value: Int( 1, @@ -4838,9 +5544,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3738..3741, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3738..3741, }, ), @@ -4850,15 +5558,19 @@ Module( }, MatchCase { range: 3746..3767, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3751..3754, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 3751..3754, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3752..3754, value: Float( 1.0, @@ -4873,9 +5585,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3764..3767, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3764..3767, }, ), @@ -4885,15 +5599,19 @@ Module( }, MatchCase { range: 3772..3795, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3777..3782, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 3777..3782, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3778..3782, value: Int( 1, @@ -4908,9 +5626,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3792..3795, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3792..3795, }, ), @@ -4920,11 +5640,14 @@ Module( }, MatchCase { range: 3800..3821, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 3806..3807, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3806..3807, value: Int( 1, @@ -4937,9 +5660,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3818..3821, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3818..3821, }, ), @@ -4952,9 +5677,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3840..3927, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3846..3847, id: Name("x"), ctx: Load, @@ -4963,15 +5690,19 @@ Module( cases: [ MatchCase { range: 3853..3876, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 3858..3863, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 3858..3859, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3858..3859, value: Int( 1, @@ -4983,8 +5714,10 @@ Module( MatchValue( PatternMatchValue { range: 3862..3863, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3862..3863, value: Int( 2, @@ -5000,9 +5733,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3873..3876, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3873..3876, }, ), @@ -5012,20 +5747,25 @@ Module( }, MatchCase { range: 3881..3927, + node_index: AtomicNodeIndex(..), pattern: MatchOr( PatternMatchOr { range: 3886..3914, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 3886..3888, + node_index: AtomicNodeIndex(..), value: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 3886..3888, value: StringLiteralValue { inner: Single( StringLiteral { range: 3886..3888, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -5042,8 +5782,10 @@ Module( MatchValue( PatternMatchValue { range: 3891..3894, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3891..3894, value: Float( 1.1, @@ -5055,12 +5797,15 @@ Module( MatchValue( PatternMatchValue { range: 3897..3899, + node_index: AtomicNodeIndex(..), value: UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 3897..3899, op: USub, operand: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3898..3899, value: Int( 1, @@ -5074,11 +5819,14 @@ Module( MatchValue( PatternMatchValue { range: 3902..3908, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 3902..3908, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3902..3903, value: Int( 1, @@ -5088,6 +5836,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 3906..3908, value: Complex { real: 0.0, @@ -5102,11 +5851,14 @@ Module( MatchValue( PatternMatchValue { range: 3911..3914, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 3911..3914, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3911..3912, id: Name("a"), ctx: Load, @@ -5115,6 +5867,7 @@ Module( attr: Identifier { id: Name("b"), range: 3913..3914, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -5128,9 +5881,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3924..3927, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3924..3927, }, ), @@ -5143,9 +5898,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3946..3978, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3952..3953, id: Name("x"), ctx: Load, @@ -5154,14 +5911,17 @@ Module( cases: [ MatchCase { range: 3959..3978, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 3964..3965, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 3964..3965, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5170,9 +5930,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 3975..3978, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 3975..3978, }, ), @@ -5185,9 +5947,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 3979..4016, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 3985..3986, id: Name("x"), ctx: Load, @@ -5196,18 +5960,22 @@ Module( cases: [ MatchCase { range: 3992..4016, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 3997..4003, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 3997..3998, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 3997..3998, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5217,6 +5985,7 @@ Module( Identifier { id: Name("b"), range: 4002..4003, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5225,9 +5994,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4013..4016, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4013..4016, }, ), @@ -5240,9 +6011,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 4017..4157, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4023..4024, id: Name("x"), ctx: Load, @@ -5251,19 +6024,24 @@ Module( cases: [ MatchCase { range: 4030..4060, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 4035..4047, + node_index: AtomicNodeIndex(..), pattern: Some( MatchOr( PatternMatchOr { range: 4035..4040, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4035..4036, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4035..4036, value: Int( 1, @@ -5275,8 +6053,10 @@ Module( MatchValue( PatternMatchValue { range: 4039..4040, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4039..4040, value: Int( 2, @@ -5293,6 +6073,7 @@ Module( Identifier { id: Name("two"), range: 4044..4047, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5301,9 +6082,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4057..4060, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4057..4060, }, ), @@ -5313,18 +6096,23 @@ Module( }, MatchCase { range: 4065..4096, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 4070..4083, + node_index: AtomicNodeIndex(..), pattern: Some( MatchValue( PatternMatchValue { range: 4070..4076, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4070..4076, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4070..4071, value: Int( 1, @@ -5334,6 +6122,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4074..4076, value: Complex { real: 0.0, @@ -5350,6 +6139,7 @@ Module( Identifier { id: Name("sum"), range: 4080..4083, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5358,9 +6148,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4093..4096, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4093..4096, }, ), @@ -5370,18 +6162,23 @@ Module( }, MatchCase { range: 4101..4128, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 4106..4115, + node_index: AtomicNodeIndex(..), pattern: Some( MatchValue( PatternMatchValue { range: 4106..4109, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 4106..4109, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4106..4107, id: Name("a"), ctx: Load, @@ -5390,6 +6187,7 @@ Module( attr: Identifier { id: Name("b"), range: 4108..4109, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -5401,6 +6199,7 @@ Module( Identifier { id: Name("ab"), range: 4113..4115, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5409,9 +6208,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4125..4128, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4125..4128, }, ), @@ -5421,13 +6222,16 @@ Module( }, MatchCase { range: 4133..4157, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 4138..4144, + node_index: AtomicNodeIndex(..), pattern: Some( MatchAs( PatternMatchAs { range: 4138..4139, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -5437,6 +6241,7 @@ Module( Identifier { id: Name("x"), range: 4143..4144, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5445,9 +6250,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4154..4157, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4154..4157, }, ), @@ -5460,9 +6267,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 4158..4190, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4164..4165, id: Name("x"), ctx: Load, @@ -5471,9 +6280,11 @@ Module( cases: [ MatchCase { range: 4171..4190, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 4176..4177, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -5482,9 +6293,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4187..4190, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4187..4190, }, ), @@ -5497,9 +6310,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 4215..4466, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4221..4222, id: Name("x"), ctx: Load, @@ -5508,15 +6323,19 @@ Module( cases: [ MatchCase { range: 4228..4253, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4233..4240, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4233..4234, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4233..4234, value: Int( 1, @@ -5528,8 +6347,10 @@ Module( MatchValue( PatternMatchValue { range: 4236..4237, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4236..4237, value: Int( 2, @@ -5541,8 +6362,10 @@ Module( MatchValue( PatternMatchValue { range: 4239..4240, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4239..4240, value: Int( 3, @@ -5558,9 +6381,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4250..4253, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4250..4253, }, ), @@ -5570,15 +6395,19 @@ Module( }, MatchCase { range: 4258..4286, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4263..4273, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4264..4265, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4264..4265, value: Int( 1, @@ -5590,8 +6419,10 @@ Module( MatchValue( PatternMatchValue { range: 4267..4268, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4267..4268, value: Int( 2, @@ -5603,8 +6434,10 @@ Module( MatchValue( PatternMatchValue { range: 4270..4271, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4270..4271, value: Int( 3, @@ -5620,9 +6453,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4283..4286, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4283..4286, }, ), @@ -5632,18 +6467,23 @@ Module( }, MatchCase { range: 4291..4331, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4296..4318, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4297..4303, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4297..4303, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4297..4298, value: Int( 1, @@ -5653,6 +6493,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4301..4303, value: Complex { real: 0.0, @@ -5667,11 +6508,13 @@ Module( MatchAs( PatternMatchAs { range: 4305..4306, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 4305..4306, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5679,17 +6522,21 @@ Module( MatchSingleton( PatternMatchSingleton { range: 4308..4312, + node_index: AtomicNodeIndex(..), value: None, }, ), MatchValue( PatternMatchValue { range: 4314..4317, + node_index: AtomicNodeIndex(..), value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 4314..4317, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4314..4315, id: Name("a"), ctx: Load, @@ -5698,6 +6545,7 @@ Module( attr: Identifier { id: Name("b"), range: 4316..4317, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -5711,9 +6559,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4328..4331, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4328..4331, }, ), @@ -5723,23 +6573,29 @@ Module( }, MatchCase { range: 4336..4370, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 4341..4357, + node_index: AtomicNodeIndex(..), pattern: Some( MatchSequence( PatternMatchSequence { range: 4341..4352, + node_index: AtomicNodeIndex(..), patterns: [ MatchAs( PatternMatchAs { range: 4342..4348, + node_index: AtomicNodeIndex(..), pattern: Some( MatchValue( PatternMatchValue { range: 4342..4343, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4342..4343, value: Int( 1, @@ -5753,6 +6609,7 @@ Module( Identifier { id: Name("X"), range: 4347..4348, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5760,11 +6617,13 @@ Module( MatchAs( PatternMatchAs { range: 4350..4351, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("b"), range: 4350..4351, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5777,6 +6636,7 @@ Module( Identifier { id: Name("S"), range: 4356..4357, + node_index: AtomicNodeIndex(..), }, ), }, @@ -5785,9 +6645,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4367..4370, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4367..4370, }, ), @@ -5797,15 +6659,19 @@ Module( }, MatchCase { range: 4375..4407, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4380..4394, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4381..4382, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4381..4382, value: Int( 1, @@ -5817,8 +6683,10 @@ Module( MatchValue( PatternMatchValue { range: 4384..4385, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4384..4385, value: Int( 2, @@ -5830,11 +6698,14 @@ Module( MatchValue( PatternMatchValue { range: 4387..4393, + node_index: AtomicNodeIndex(..), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 4387..4393, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4387..4388, value: Int( 3, @@ -5844,6 +6715,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4391..4393, value: Complex { real: 0.0, @@ -5862,9 +6734,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4404..4407, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4404..4407, }, ), @@ -5874,19 +6748,24 @@ Module( }, MatchCase { range: 4412..4440, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4417..4427, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 4418..4423, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4419..4420, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4419..4420, value: Int( 1, @@ -5898,8 +6777,10 @@ Module( MatchValue( PatternMatchValue { range: 4421..4422, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4421..4422, value: Int( 2, @@ -5914,8 +6795,10 @@ Module( MatchValue( PatternMatchValue { range: 4425..4426, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4425..4426, value: Int( 3, @@ -5931,9 +6814,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4437..4440, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4437..4440, }, ), @@ -5943,15 +6828,19 @@ Module( }, MatchCase { range: 4445..4466, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4450..4453, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4451..4452, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4451..4452, value: Int( 1, @@ -5967,9 +6856,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4463..4466, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4463..4466, }, ), @@ -5982,9 +6873,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 4487..4616, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4493..4494, id: Name("x"), ctx: Load, @@ -5993,17 +6886,21 @@ Module( cases: [ MatchCase { range: 4500..4521, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4505..4508, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 4505..4507, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("a"), range: 4506..4507, + node_index: AtomicNodeIndex(..), }, ), }, @@ -6015,9 +6912,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4518..4521, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4518..4521, }, ), @@ -6027,13 +6926,16 @@ Module( }, MatchCase { range: 4526..4547, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4531..4534, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 4531..4533, + node_index: AtomicNodeIndex(..), name: None, }, ), @@ -6044,9 +6946,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4544..4547, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4544..4547, }, ), @@ -6056,15 +6960,19 @@ Module( }, MatchCase { range: 4552..4583, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4557..4570, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4558..4559, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4558..4559, value: Int( 1, @@ -6076,8 +6984,10 @@ Module( MatchValue( PatternMatchValue { range: 4561..4562, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4561..4562, value: Int( 2, @@ -6089,10 +6999,12 @@ Module( MatchStar( PatternMatchStar { range: 4564..4569, + node_index: AtomicNodeIndex(..), name: Some( Identifier { id: Name("rest"), range: 4565..4569, + node_index: AtomicNodeIndex(..), }, ), }, @@ -6104,9 +7016,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4580..4583, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4580..4583, }, ), @@ -6116,21 +7030,26 @@ Module( }, MatchCase { range: 4588..4616, + node_index: AtomicNodeIndex(..), pattern: MatchSequence( PatternMatchSequence { range: 4593..4603, + node_index: AtomicNodeIndex(..), patterns: [ MatchStar( PatternMatchStar { range: 4594..4596, + node_index: AtomicNodeIndex(..), name: None, }, ), MatchValue( PatternMatchValue { range: 4598..4599, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4598..4599, value: Int( 1, @@ -6142,8 +7061,10 @@ Module( MatchValue( PatternMatchValue { range: 4601..4602, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4601..4602, value: Int( 2, @@ -6159,9 +7080,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4613..4616, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4613..4616, }, ), @@ -6174,9 +7097,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 4638..4910, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4644..4645, id: Name("x"), ctx: Load, @@ -6185,11 +7110,14 @@ Module( cases: [ MatchCase { range: 4651..4676, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4656..4663, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4656..4661, id: Name("Point"), ctx: Load, @@ -6197,6 +7125,7 @@ Module( ), arguments: PatternArguments { range: 4661..4663, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [], }, @@ -6206,9 +7135,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4673..4676, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4673..4676, }, ), @@ -6218,17 +7149,22 @@ Module( }, MatchCase { range: 4681..4710, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4686..4697, + node_index: AtomicNodeIndex(..), cls: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 4686..4695, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 4686..4689, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4686..4687, id: Name("a"), ctx: Load, @@ -6237,6 +7173,7 @@ Module( attr: Identifier { id: Name("b"), range: 4688..4689, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -6244,12 +7181,14 @@ Module( attr: Identifier { id: Name("Point"), range: 4690..4695, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: PatternArguments { range: 4695..4697, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [], }, @@ -6259,9 +7198,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4707..4710, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4707..4710, }, ), @@ -6271,11 +7212,14 @@ Module( }, MatchCase { range: 4715..4745, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4720..4732, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4720..4727, id: Name("Point2D"), ctx: Load, @@ -6283,19 +7227,24 @@ Module( ), arguments: PatternArguments { range: 4727..4732, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 4728..4731, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 4728..4729, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 4730..4731, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4730..4731, value: Int( 0, @@ -6313,9 +7262,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4742..4745, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4742..4745, }, ), @@ -6325,11 +7276,14 @@ Module( }, MatchCase { range: 4750..4786, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4755..4773, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4755..4762, id: Name("Point2D"), ctx: Load, @@ -6337,19 +7291,24 @@ Module( ), arguments: PatternArguments { range: 4762..4773, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 4763..4766, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 4763..4764, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 4765..4766, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4765..4766, value: Int( 0, @@ -6361,15 +7320,19 @@ Module( }, PatternKeyword { range: 4768..4771, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 4768..4769, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 4770..4771, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4770..4771, value: Int( 0, @@ -6387,9 +7350,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4783..4786, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4783..4786, }, ), @@ -6399,11 +7364,14 @@ Module( }, MatchCase { range: 4791..4822, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4796..4809, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4796..4803, id: Name("Point2D"), ctx: Load, @@ -6411,12 +7379,15 @@ Module( ), arguments: PatternArguments { range: 4803..4809, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4804..4805, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4804..4805, value: Int( 0, @@ -6428,8 +7399,10 @@ Module( MatchValue( PatternMatchValue { range: 4807..4808, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4807..4808, value: Int( 1, @@ -6447,9 +7420,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4819..4822, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4819..4822, }, ), @@ -6459,11 +7434,14 @@ Module( }, MatchCase { range: 4827..4865, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4832..4852, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4832..4839, id: Name("Point2D"), ctx: Load, @@ -6471,16 +7449,20 @@ Module( ), arguments: PatternArguments { range: 4839..4852, + node_index: AtomicNodeIndex(..), patterns: [ MatchSequence( PatternMatchSequence { range: 4840..4846, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4841..4842, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4841..4842, value: Int( 0, @@ -6492,8 +7474,10 @@ Module( MatchValue( PatternMatchValue { range: 4844..4845, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4844..4845, value: Int( 1, @@ -6509,15 +7493,19 @@ Module( keywords: [ PatternKeyword { range: 4848..4851, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 4848..4849, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 4850..4851, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4850..4851, value: Int( 1, @@ -6535,9 +7523,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4862..4865, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4862..4865, }, ), @@ -6547,11 +7537,14 @@ Module( }, MatchCase { range: 4870..4910, + node_index: AtomicNodeIndex(..), pattern: MatchClass( PatternMatchClass { range: 4875..4897, + node_index: AtomicNodeIndex(..), cls: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4875..4882, id: Name("Point2D"), ctx: Load, @@ -6559,23 +7552,29 @@ Module( ), arguments: PatternArguments { range: 4882..4897, + node_index: AtomicNodeIndex(..), patterns: [], keywords: [ PatternKeyword { range: 4883..4891, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("x"), range: 4883..4884, + node_index: AtomicNodeIndex(..), }, pattern: MatchSequence( PatternMatchSequence { range: 4885..4891, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 4886..4887, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4886..4887, value: Int( 0, @@ -6587,8 +7586,10 @@ Module( MatchValue( PatternMatchValue { range: 4889..4890, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4889..4890, value: Int( 1, @@ -6603,15 +7604,19 @@ Module( }, PatternKeyword { range: 4893..4896, + node_index: AtomicNodeIndex(..), attr: Identifier { id: Name("y"), range: 4893..4894, + node_index: AtomicNodeIndex(..), }, pattern: MatchValue( PatternMatchValue { range: 4895..4896, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4895..4896, value: Int( 1, @@ -6629,9 +7634,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4907..4910, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4907..4910, }, ), @@ -6644,12 +7651,15 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 4934..5028, subject: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 4940..4946, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4940..4941, id: Name("x"), ctx: Store, @@ -6657,6 +7667,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 4945..4946, id: Name("b"), ctx: Load, @@ -6667,12 +7678,15 @@ Module( cases: [ MatchCase { range: 4952..4976, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 4957..4963, + node_index: AtomicNodeIndex(..), keys: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 4958..4959, value: Int( 1, @@ -6684,6 +7698,7 @@ Module( MatchAs( PatternMatchAs { range: 4961..4962, + node_index: AtomicNodeIndex(..), pattern: None, name: None, }, @@ -6696,9 +7711,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 4973..4976, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 4973..4976, }, ), @@ -6708,17 +7725,21 @@ Module( }, MatchCase { range: 4981..5028, + node_index: AtomicNodeIndex(..), pattern: MatchMapping( PatternMatchMapping { range: 4986..5015, + node_index: AtomicNodeIndex(..), keys: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 4987..4989, value: StringLiteralValue { inner: Single( StringLiteral { range: 4987..4989, + node_index: AtomicNodeIndex(..), value: "", flags: StringLiteralFlags { quote_style: Single, @@ -6732,6 +7753,7 @@ Module( ), NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 4994..4998, }, ), @@ -6740,11 +7762,13 @@ Module( MatchAs( PatternMatchAs { range: 4991..4992, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 4991..4992, + node_index: AtomicNodeIndex(..), }, ), }, @@ -6752,12 +7776,15 @@ Module( MatchSequence( PatternMatchSequence { range: 5000..5006, + node_index: AtomicNodeIndex(..), patterns: [ MatchValue( PatternMatchValue { range: 5001..5002, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5001..5002, value: Int( 1, @@ -6769,8 +7796,10 @@ Module( MatchValue( PatternMatchValue { range: 5004..5005, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5004..5005, value: Int( 2, @@ -6787,6 +7816,7 @@ Module( Identifier { id: Name("rest"), range: 5010..5014, + node_index: AtomicNodeIndex(..), }, ), }, @@ -6795,9 +7825,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5025..5028, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 5025..5028, }, ), @@ -6810,9 +7842,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 5046..5106, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5052..5053, id: Name("y"), ctx: Load, @@ -6821,14 +7855,17 @@ Module( cases: [ MatchCase { range: 5059..5080, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 5064..5065, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("a"), range: 5064..5065, + node_index: AtomicNodeIndex(..), }, ), }, @@ -6836,9 +7873,11 @@ Module( guard: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 5069..5075, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5069..5070, id: Name("b"), ctx: Store, @@ -6846,6 +7885,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5074..5075, id: Name("c"), ctx: Load, @@ -6857,9 +7897,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5077..5080, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 5077..5080, }, ), @@ -6869,14 +7911,17 @@ Module( }, MatchCase { range: 5085..5106, + node_index: AtomicNodeIndex(..), pattern: MatchAs( PatternMatchAs { range: 5090..5091, + node_index: AtomicNodeIndex(..), pattern: None, name: Some( Identifier { id: Name("e"), range: 5090..5091, + node_index: AtomicNodeIndex(..), }, ), }, @@ -6884,9 +7929,11 @@ Module( guard: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 5096..5101, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5096..5097, value: Int( 1, @@ -6899,6 +7946,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5100..5101, value: Int( 2, @@ -6912,9 +7960,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5103..5106, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 5103..5106, }, ), @@ -6927,19 +7977,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5135..5150, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5135..5150, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5135..5147, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5135..5143, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5135..5140, id: Name("match"), ctx: Load, @@ -6948,6 +8003,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5142..5143, id: Name("a"), ctx: Load, @@ -6958,6 +8014,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5146..5147, id: Name("b"), ctx: Load, @@ -6967,6 +8024,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5149..5150, id: Name("c"), ctx: Load, @@ -6981,16 +8039,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5176..5193, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5176..5193, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5176..5190, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5176..5181, id: Name("match"), ctx: Load, @@ -6999,9 +8061,11 @@ Module( op: Mult, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5184..5189, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5184..5185, id: Name("a"), ctx: Load, @@ -7010,6 +8074,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5188..5189, id: Name("b"), ctx: Load, @@ -7021,6 +8086,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5192..5193, id: Name("c"), ctx: Load, @@ -7035,12 +8101,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5219..5236, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5219..5236, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5219..5224, id: Name("match"), ctx: Load, @@ -7048,15 +8117,19 @@ Module( ), arguments: Arguments { range: 5225..5236, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 5226..5232, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5227..5232, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5227..5228, id: Name("a"), ctx: Load, @@ -7065,6 +8138,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5231..5232, id: Name("b"), ctx: Load, @@ -7077,6 +8151,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5234..5235, id: Name("c"), ctx: Load, @@ -7091,15 +8166,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5263..5279, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5263..5279, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5263..5275, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5263..5268, id: Name("match"), ctx: Load, @@ -7108,9 +8187,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5270..5275, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5270..5271, id: Name("a"), ctx: Load, @@ -7119,6 +8200,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5274..5275, id: Name("b"), ctx: Load, @@ -7131,6 +8213,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5278..5279, id: Name("c"), ctx: Load, @@ -7142,15 +8225,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5306..5324, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5306..5324, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5306..5320, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5306..5311, id: Name("match"), ctx: Load, @@ -7159,9 +8246,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5314..5319, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5314..5315, id: Name("a"), ctx: Load, @@ -7170,6 +8259,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5318..5319, id: Name("b"), ctx: Load, @@ -7182,6 +8272,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5323..5324, id: Name("c"), ctx: Load, @@ -7193,18 +8284,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5351..5369, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5351..5369, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 5351..5365, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5351..5361, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5351..5356, id: Name("match"), ctx: Load, @@ -7212,13 +8308,16 @@ Module( ), arguments: Arguments { range: 5357..5361, + node_index: AtomicNodeIndex(..), args: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 5358..5360, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5359..5360, id: Name("a"), ctx: Load, @@ -7234,6 +8333,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5364..5365, id: Name("b"), ctx: Load, @@ -7244,6 +8344,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5368..5369, id: Name("c"), ctx: Load, @@ -7255,15 +8356,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5397..5407, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 5397..5407, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5397..5405, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5397..5402, id: Name("match"), ctx: Load, @@ -7271,6 +8376,7 @@ Module( ), arguments: Arguments { range: 5403..5405, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -7279,6 +8385,7 @@ Module( attr: Identifier { id: Name("a"), range: 5406..5407, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -7287,15 +8394,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5424..5436, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 5424..5436, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5424..5434, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5424..5429, id: Name("match"), ctx: Load, @@ -7303,9 +8414,11 @@ Module( ), arguments: Arguments { range: 5430..5434, + node_index: AtomicNodeIndex(..), args: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5431..5433, elts: [], ctx: Load, @@ -7320,6 +8433,7 @@ Module( attr: Identifier { id: Name("a"), range: 5435..5436, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -7328,15 +8442,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5455..5468, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 5455..5468, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5455..5466, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5455..5460, id: Name("match"), ctx: Load, @@ -7344,9 +8462,11 @@ Module( ), arguments: Arguments { range: 5461..5466, + node_index: AtomicNodeIndex(..), args: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5462..5464, elts: [], ctx: Load, @@ -7361,6 +8481,7 @@ Module( attr: Identifier { id: Name("a"), range: 5467..5468, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -7369,15 +8490,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5487..5498, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 5487..5498, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 5487..5496, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5487..5492, id: Name("match"), ctx: Load, @@ -7385,6 +8510,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5494..5495, id: Name("a"), ctx: Load, @@ -7396,6 +8522,7 @@ Module( attr: Identifier { id: Name("b"), range: 5497..5498, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -7404,15 +8531,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5516..5528, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 5516..5528, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 5516..5526, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5516..5521, id: Name("match"), ctx: Load, @@ -7420,10 +8551,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5523..5525, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5523..5524, id: Name("a"), ctx: Load, @@ -7440,6 +8573,7 @@ Module( attr: Identifier { id: Name("b"), range: 5527..5528, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -7448,15 +8582,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5569..5583, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 5569..5583, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 5569..5581, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5569..5574, id: Name("match"), ctx: Load, @@ -7464,10 +8602,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 5576..5580, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5577..5578, id: Name("a"), ctx: Load, @@ -7484,6 +8624,7 @@ Module( attr: Identifier { id: Name("b"), range: 5582..5583, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -7492,15 +8633,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5604..5621, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 5604..5621, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5604..5611, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5604..5609, id: Name("match"), ctx: Load, @@ -7508,6 +8653,7 @@ Module( ), arguments: Arguments { range: 5609..5611, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -7515,10 +8661,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 5612..5620, lower: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5612..5613, id: Name("a"), ctx: Load, @@ -7528,6 +8676,7 @@ Module( upper: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5619..5620, id: Name("b"), ctx: Load, @@ -7544,12 +8693,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 5641..5660, test: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 5644..5654, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5644..5649, id: Name("match"), ctx: Store, @@ -7557,6 +8709,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5653..5654, value: Int( 1, @@ -7568,6 +8721,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 5656..5660, }, ), @@ -7577,9 +8731,11 @@ Module( ), Match( StmtMatch { + node_index: AtomicNodeIndex(..), range: 5661..5715, subject: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5667..5672, id: Name("match"), ctx: Load, @@ -7588,11 +8744,14 @@ Module( cases: [ MatchCase { range: 5678..5690, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 5683..5684, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5683..5684, value: Int( 1, @@ -7605,6 +8764,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 5686..5690, }, ), @@ -7612,11 +8772,14 @@ Module( }, MatchCase { range: 5695..5715, + node_index: AtomicNodeIndex(..), pattern: MatchValue( PatternMatchValue { range: 5700..5701, + node_index: AtomicNodeIndex(..), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5700..5701, value: Int( 2, @@ -7629,6 +8792,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 5711..5715, }, ), @@ -7639,10 +8803,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 5716..5752, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5716..5721, id: Name("match"), ctx: Store, @@ -7651,19 +8817,26 @@ Module( ], value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 5724..5752, parameters: Some( Parameters { range: 5731..5736, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 5731..5736, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 5731..5736, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("query"), range: 5731..5736, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -7677,9 +8850,11 @@ Module( ), body: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 5738..5752, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5738..5743, id: Name("query"), ctx: Load, @@ -7691,6 +8866,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5747..5752, id: Name("event"), ctx: Load, @@ -7705,12 +8881,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 5753..5769, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5753..5769, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5753..5758, id: Name("print"), ctx: Load, @@ -7718,12 +8897,15 @@ Module( ), arguments: Arguments { range: 5758..5769, + node_index: AtomicNodeIndex(..), args: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 5759..5768, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5759..5764, id: Name("match"), ctx: Load, @@ -7731,9 +8913,11 @@ Module( ), arguments: Arguments { range: 5764..5768, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 5765..5767, value: Int( 12, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__raise.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__raise.py.snap index 1796953d2ae17..7329f1e3a9af8 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__raise.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__raise.py.snap @@ -1,17 +1,18 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/raise.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..289, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 8..13, exc: None, cause: None, @@ -19,10 +20,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 14..21, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 20..21, id: Name("a"), ctx: Load, @@ -34,14 +37,17 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 22..34, exc: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 28..34, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 29..30, id: Name("a"), ctx: Load, @@ -49,6 +55,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 32..33, id: Name("b"), ctx: Load, @@ -65,13 +72,16 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 35..46, exc: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 41..46, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 41..42, value: Int( 1, @@ -84,6 +94,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 45..46, value: Int( 2, @@ -99,15 +110,18 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 47..60, exc: Some( BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 53..60, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..54, id: Name("a"), ctx: Load, @@ -115,6 +129,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..60, id: Name("b"), ctx: Load, @@ -129,23 +144,31 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 61..78, exc: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 67..78, parameters: Some( Parameters { range: 74..75, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 74..75, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 74..75, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 74..75, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -159,6 +182,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..78, id: Name("y"), ctx: Load, @@ -172,13 +196,16 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 79..92, exc: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 85..92, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 91..92, id: Name("x"), ctx: Load, @@ -192,19 +219,23 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 93..115, exc: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 99..115, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 104..108, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 99..100, id: Name("x"), ctx: Load, @@ -212,6 +243,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 114..115, id: Name("y"), ctx: Load, @@ -225,10 +257,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 138..152, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 144..145, id: Name("x"), ctx: Load, @@ -238,6 +272,7 @@ Module( cause: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..152, id: Name("a"), ctx: Load, @@ -248,10 +283,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 153..172, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 159..160, id: Name("x"), ctx: Load, @@ -261,10 +298,12 @@ Module( cause: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 166..172, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 167..168, id: Name("a"), ctx: Load, @@ -272,6 +311,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("b"), ctx: Load, @@ -287,10 +327,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 173..191, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 179..180, id: Name("x"), ctx: Load, @@ -300,9 +342,11 @@ Module( cause: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 186..191, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 186..187, value: Int( 1, @@ -315,6 +359,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 190..191, value: Int( 2, @@ -329,10 +374,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 192..212, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 198..199, id: Name("x"), ctx: Load, @@ -342,11 +389,13 @@ Module( cause: Some( BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 205..212, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 205..206, id: Name("a"), ctx: Load, @@ -354,6 +403,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 211..212, id: Name("b"), ctx: Load, @@ -367,10 +417,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 213..237, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 219..220, id: Name("x"), ctx: Load, @@ -380,19 +432,26 @@ Module( cause: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 226..237, parameters: Some( Parameters { range: 233..234, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 233..234, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 233..234, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 233..234, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -406,6 +465,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("y"), ctx: Load, @@ -418,10 +478,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 238..258, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 244..245, id: Name("x"), ctx: Load, @@ -431,9 +493,11 @@ Module( cause: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 251..258, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 257..258, id: Name("x"), ctx: Load, @@ -446,10 +510,12 @@ Module( ), Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 259..288, exc: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 265..266, id: Name("x"), ctx: Load, @@ -459,15 +525,18 @@ Module( cause: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 272..288, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 277..281, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 272..273, id: Name("x"), ctx: Load, @@ -475,6 +544,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 287..288, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__return.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__return.py.snap index 162b1c9c347de..7cb3690eadb00 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__return.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__return.py.snap @@ -7,20 +7,24 @@ input_file: crates/ruff_python_parser/resources/valid/statement/return.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..167, body: [ Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 0..6, value: None, }, ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 7..15, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..15, id: Name("x"), ctx: Load, @@ -31,17 +35,21 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 16..29, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 23..29, elts: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 23..25, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 24..25, id: Name("x"), ctx: Load, @@ -52,9 +60,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 27..29, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..29, id: Name("y"), ctx: Load, @@ -73,13 +83,16 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 30..45, value: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 38..44, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..39, id: Name("x"), ctx: Store, @@ -87,6 +100,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 43..44, value: Int( 1, @@ -100,10 +114,12 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 46..57, value: Some( NoneLiteral( ExprNoneLiteral { + node_index: AtomicNodeIndex(..), range: 53..57, }, ), @@ -112,15 +128,18 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 58..72, value: Some( BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 65..72, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 65..66, id: Name("x"), ctx: Load, @@ -128,6 +147,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 71..72, id: Name("y"), ctx: Load, @@ -141,13 +161,16 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 73..85, value: Some( Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 80..85, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 80..81, value: Int( 1, @@ -160,6 +183,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 84..85, value: Int( 2, @@ -174,14 +198,17 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 86..98, value: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 93..98, elts: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 93..94, value: Int( 1, @@ -190,6 +217,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 96..97, value: Int( 2, @@ -206,13 +234,16 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 99..112, value: Some( Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 106..112, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..110, id: Name("call"), ctx: Load, @@ -220,6 +251,7 @@ Module( ), arguments: Arguments { range: 110..112, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -230,16 +262,20 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 113..132, value: Some( Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 120..132, func: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 120..130, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..124, id: Name("attr"), ctx: Load, @@ -248,12 +284,14 @@ Module( attr: Identifier { id: Name("value"), range: 125..130, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, ), arguments: Arguments { range: 130..132, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -264,13 +302,16 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 133..147, value: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 140..147, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 146..147, id: Name("x"), ctx: Load, @@ -283,23 +324,31 @@ Module( ), Return( StmtReturn { + node_index: AtomicNodeIndex(..), range: 148..166, value: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 155..166, parameters: Some( Parameters { range: 162..163, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 162..163, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 162..163, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 162..163, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -313,6 +362,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..166, id: Name("y"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__simple.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__simple.py.snap index 31c90f21190e9..3865245dce0f4 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__simple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__simple.py.snap @@ -1,30 +1,34 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/simple.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..172, body: [ Continue( StmtContinue { + node_index: AtomicNodeIndex(..), range: 61..69, }, ), Break( StmtBreak { + node_index: AtomicNodeIndex(..), range: 70..75, }, ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 77..86, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 80..81, id: Name("x"), ctx: Load, @@ -33,9 +37,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 83..86, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 83..86, }, ), @@ -47,9 +53,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 87..100, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 90..94, value: true, }, @@ -57,6 +65,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 96..100, }, ), @@ -66,9 +75,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 101..102, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 101..102, value: Int( 1, @@ -79,9 +90,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 104..105, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 104..105, value: Int( 2, @@ -92,14 +105,17 @@ Module( ), Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 107..111, }, ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 112..113, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 112..113, value: Int( 1, @@ -110,9 +126,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 115..118, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 115..118, }, ), @@ -120,12 +138,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 120..133, value: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 120..133, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 125..126, id: Name("b"), ctx: Load, @@ -133,6 +154,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 120..121, id: Name("a"), ctx: Load, @@ -140,6 +162,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 132..133, id: Name("c"), ctx: Load, @@ -151,9 +174,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 135..157, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 138..139, id: Name("c"), ctx: Load, @@ -162,9 +187,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 141..142, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 141..142, id: Name("B"), ctx: Load, @@ -174,10 +201,12 @@ Module( ), Delete( StmtDelete { + node_index: AtomicNodeIndex(..), range: 144..149, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 148..149, id: Name("A"), ctx: Del, @@ -190,13 +219,16 @@ Module( elif_else_clauses: [ ElifElseClause { range: 150..157, + node_index: AtomicNodeIndex(..), test: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 156..157, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 156..157, id: Name("C"), ctx: Load, @@ -211,9 +243,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 158..171, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 161..162, id: Name("x"), ctx: Load, @@ -222,13 +256,16 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 164..171, value: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 164..171, value: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 170..171, id: Name("x"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap index 3f799c4d6b0de..e6021299e9875 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/try.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..1223, body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 0..28, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 9..12, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 9..12, }, ), @@ -29,14 +32,17 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 13..28, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 25..28, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 25..28, }, ), @@ -53,13 +59,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 30..106, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 39..42, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 39..42, }, ), @@ -70,9 +79,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 43..74, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 50..60, id: Name("Exception1"), ctx: Load, @@ -83,14 +94,17 @@ Module( Identifier { id: Name("e"), range: 64..65, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 71..74, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 71..74, }, ), @@ -102,9 +116,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 75..106, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..92, id: Name("Exception2"), ctx: Load, @@ -115,14 +131,17 @@ Module( Identifier { id: Name("e"), range: 96..97, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 103..106, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 103..106, }, ), @@ -139,13 +158,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 108..184, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 117..120, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 117..120, }, ), @@ -156,9 +178,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 121..151, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 128..137, id: Name("Exception"), ctx: Load, @@ -169,14 +193,17 @@ Module( Identifier { id: Name("e"), range: 141..142, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..151, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 148..151, }, ), @@ -188,14 +215,17 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 152..167, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 164..167, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 164..167, }, ), @@ -209,9 +239,11 @@ Module( finalbody: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 181..184, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 181..184, }, ), @@ -223,13 +255,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 186..228, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 195..198, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 195..198, }, ), @@ -240,14 +275,17 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 199..214, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 211..214, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 211..214, }, ), @@ -260,9 +298,11 @@ Module( orelse: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 225..228, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 225..228, }, ), @@ -275,13 +315,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 230..289, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 239..242, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 239..242, }, ), @@ -292,14 +335,17 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 243..258, + node_index: AtomicNodeIndex(..), type_: None, name: None, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 255..258, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 255..258, }, ), @@ -312,9 +358,11 @@ Module( orelse: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 269..272, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 269..272, }, ), @@ -324,9 +372,11 @@ Module( finalbody: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 286..289, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 286..289, }, ), @@ -338,13 +388,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 291..320, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 300..303, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 300..303, }, ), @@ -356,9 +409,11 @@ Module( finalbody: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 317..320, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 317..320, }, ), @@ -370,13 +425,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 322..365, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 331..334, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 331..334, }, ), @@ -387,9 +445,11 @@ Module( orelse: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 345..348, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 345..348, }, ), @@ -399,9 +459,11 @@ Module( finalbody: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 362..365, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 362..365, }, ), @@ -413,13 +475,16 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 367..441, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 376..379, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 376..379, }, ), @@ -430,9 +495,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 380..409, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 388..394, id: Name("GroupA"), ctx: Load, @@ -443,14 +510,17 @@ Module( Identifier { id: Name("eg"), range: 398..400, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 406..409, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 406..409, }, ), @@ -462,9 +532,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 410..441, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 418..432, id: Name("ExceptionGroup"), ctx: Load, @@ -475,9 +547,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 438..441, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 438..441, }, ), @@ -494,17 +568,21 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 443..577, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 452..471, exc: Some( Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 458..471, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 458..468, id: Name("ValueError"), ctx: Load, @@ -512,9 +590,11 @@ Module( ), arguments: Arguments { range: 468..471, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 469..470, value: Int( 1, @@ -535,9 +615,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 472..525, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 479..488, id: Name("TypeError"), ctx: Load, @@ -548,17 +630,21 @@ Module( Identifier { id: Name("e"), range: 492..493, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 499..525, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 499..525, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 499..504, id: Name("print"), ctx: Load, @@ -566,30 +652,37 @@ Module( ), arguments: Arguments { range: 504..525, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 505..524, value: FStringValue { inner: Single( FString( FString { range: 505..524, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 507..514, + node_index: AtomicNodeIndex(..), value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 514..523, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 515..522, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 515..519, id: Name("type"), ctx: Load, @@ -597,9 +690,11 @@ Module( ), arguments: Arguments { range: 519..522, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 520..521, id: Name("e"), ctx: Load, @@ -640,9 +735,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 526..577, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 533..540, id: Name("OSError"), ctx: Load, @@ -653,17 +750,21 @@ Module( Identifier { id: Name("e"), range: 544..545, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 551..577, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 551..577, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 551..556, id: Name("print"), ctx: Load, @@ -671,30 +772,37 @@ Module( ), arguments: Arguments { range: 556..577, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 557..576, value: FStringValue { inner: Single( FString( FString { range: 557..576, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 559..566, + node_index: AtomicNodeIndex(..), value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 566..575, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 567..574, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 567..571, id: Name("type"), ctx: Load, @@ -702,9 +810,11 @@ Module( ), arguments: Arguments { range: 571..574, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 572..573, id: Name("e"), ctx: Load, @@ -750,17 +860,21 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 579..831, body: [ Raise( StmtRaise { + node_index: AtomicNodeIndex(..), range: 588..669, exc: Some( Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 594..669, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 594..608, id: Name("ExceptionGroup"), ctx: Load, @@ -768,14 +882,17 @@ Module( ), arguments: Arguments { range: 608..669, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 609..613, value: StringLiteralValue { inner: Single( StringLiteral { range: 609..613, + node_index: AtomicNodeIndex(..), value: "eg", flags: StringLiteralFlags { quote_style: Double, @@ -789,13 +906,16 @@ Module( ), List( ExprList { + node_index: AtomicNodeIndex(..), range: 615..668, elts: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 616..629, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 616..626, id: Name("ValueError"), ctx: Load, @@ -803,9 +923,11 @@ Module( ), arguments: Arguments { range: 626..629, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 627..628, value: Int( 1, @@ -819,9 +941,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 631..643, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 631..640, id: Name("TypeError"), ctx: Load, @@ -829,9 +953,11 @@ Module( ), arguments: Arguments { range: 640..643, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 641..642, value: Int( 2, @@ -845,9 +971,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 645..655, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 645..652, id: Name("OSError"), ctx: Load, @@ -855,9 +983,11 @@ Module( ), arguments: Arguments { range: 652..655, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 653..654, value: Int( 3, @@ -871,9 +1001,11 @@ Module( ), Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 657..667, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 657..664, id: Name("OSError"), ctx: Load, @@ -881,9 +1013,11 @@ Module( ), arguments: Arguments { range: 664..667, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 665..666, value: Int( 4, @@ -913,9 +1047,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 670..751, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 678..687, id: Name("TypeError"), ctx: Load, @@ -926,17 +1062,21 @@ Module( Identifier { id: Name("e"), range: 691..692, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 698..751, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 698..751, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 698..703, id: Name("print"), ctx: Load, @@ -944,30 +1084,37 @@ Module( ), arguments: Arguments { range: 703..751, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 704..750, value: FStringValue { inner: Single( FString( FString { range: 704..750, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 706..713, + node_index: AtomicNodeIndex(..), value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 713..722, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 714..721, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 714..718, id: Name("type"), ctx: Load, @@ -975,9 +1122,11 @@ Module( ), arguments: Arguments { range: 718..721, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 719..720, id: Name("e"), ctx: Load, @@ -994,19 +1143,23 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 722..735, + node_index: AtomicNodeIndex(..), value: " with nested ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 735..749, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 736..748, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 736..737, id: Name("e"), ctx: Load, @@ -1015,6 +1168,7 @@ Module( attr: Identifier { id: Name("exceptions"), range: 738..748, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -1049,9 +1203,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 752..831, + node_index: AtomicNodeIndex(..), type_: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 760..767, id: Name("OSError"), ctx: Load, @@ -1062,17 +1218,21 @@ Module( Identifier { id: Name("e"), range: 771..772, + node_index: AtomicNodeIndex(..), }, ), body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 778..831, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 778..831, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 778..783, id: Name("print"), ctx: Load, @@ -1080,30 +1240,37 @@ Module( ), arguments: Arguments { range: 783..831, + node_index: AtomicNodeIndex(..), args: [ FString( ExprFString { + node_index: AtomicNodeIndex(..), range: 784..830, value: FStringValue { inner: Single( FString( FString { range: 784..830, + node_index: AtomicNodeIndex(..), elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 786..793, + node_index: AtomicNodeIndex(..), value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 793..802, + node_index: AtomicNodeIndex(..), expression: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 794..801, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 794..798, id: Name("type"), ctx: Load, @@ -1111,9 +1278,11 @@ Module( ), arguments: Arguments { range: 798..801, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 799..800, id: Name("e"), ctx: Load, @@ -1130,19 +1299,23 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 802..815, + node_index: AtomicNodeIndex(..), value: " with nested ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 815..829, + node_index: AtomicNodeIndex(..), expression: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 816..828, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 816..817, id: Name("e"), ctx: Load, @@ -1151,6 +1324,7 @@ Module( attr: Identifier { id: Name("exceptions"), range: 818..828, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -1190,10 +1364,12 @@ Module( ), Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 833..1075, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 842..846, }, ), @@ -1202,14 +1378,17 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 847..875, + node_index: AtomicNodeIndex(..), type_: Some( StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 854..865, value: StringLiteralValue { inner: Single( StringLiteral { range: 854..865, + node_index: AtomicNodeIndex(..), value: "exception", flags: StringLiteralFlags { quote_style: Double, @@ -1226,6 +1405,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 871..875, }, ), @@ -1235,9 +1415,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 876..894, + node_index: AtomicNodeIndex(..), type_: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 883..884, value: Int( 1, @@ -1249,6 +1431,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 890..894, }, ), @@ -1258,9 +1441,11 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 895..916, + node_index: AtomicNodeIndex(..), type_: Some( BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 902..906, value: true, }, @@ -1270,6 +1455,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 912..916, }, ), @@ -1279,12 +1465,15 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 917..939, + node_index: AtomicNodeIndex(..), type_: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 924..929, left: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 924..925, value: Int( 1, @@ -1294,6 +1483,7 @@ Module( op: Add, right: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 928..929, value: Int( 1, @@ -1307,6 +1497,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 935..939, }, ), @@ -1316,12 +1507,15 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 940..962, + node_index: AtomicNodeIndex(..), type_: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 947..952, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 947..948, id: Name("a"), ctx: Load, @@ -1330,6 +1524,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 951..952, id: Name("b"), ctx: Load, @@ -1342,6 +1537,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 958..962, }, ), @@ -1351,14 +1547,17 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 963..987, + node_index: AtomicNodeIndex(..), type_: Some( BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 970..977, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 970..971, id: Name("x"), ctx: Load, @@ -1366,6 +1565,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 976..977, id: Name("y"), ctx: Load, @@ -1379,6 +1579,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 983..987, }, ), @@ -1388,12 +1589,15 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 988..1012, + node_index: AtomicNodeIndex(..), type_: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 995..1002, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1001..1002, id: Name("x"), ctx: Load, @@ -1406,6 +1610,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1008..1012, }, ), @@ -1415,22 +1620,30 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 1013..1041, + node_index: AtomicNodeIndex(..), type_: Some( Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 1020..1031, parameters: Some( Parameters { range: 1027..1028, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1027..1028, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1027..1028, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 1027..1028, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -1444,6 +1657,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1030..1031, id: Name("x"), ctx: Load, @@ -1456,6 +1670,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1037..1041, }, ), @@ -1465,18 +1680,22 @@ Module( ExceptHandler( ExceptHandlerExceptHandler { range: 1042..1075, + node_index: AtomicNodeIndex(..), type_: Some( If( ExprIf { + node_index: AtomicNodeIndex(..), range: 1049..1065, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 1054..1058, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1049..1050, id: Name("x"), ctx: Load, @@ -1484,6 +1703,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1064..1065, id: Name("y"), ctx: Load, @@ -1496,6 +1716,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1071..1075, }, ), @@ -1510,9 +1731,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 1077..1222, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 1080..1084, value: true, }, @@ -1520,10 +1743,12 @@ Module( body: [ Try( StmtTry { + node_index: AtomicNodeIndex(..), range: 1090..1133, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1103..1107, }, ), @@ -1533,6 +1758,7 @@ Module( finalbody: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1129..1133, }, ), @@ -1544,10 +1770,12 @@ Module( elif_else_clauses: [ ElifElseClause { range: 1208..1222, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1218..1222, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap index 817ab6399e0e5..ca837dcde3786 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__type.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/type.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..1828, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..12, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,6 +25,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 9..12, id: Name("int"), ctx: Load, @@ -32,9 +35,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 13..31, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 18..19, id: Name("X"), ctx: Store, @@ -43,9 +48,11 @@ Module( type_params: None, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 22..31, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 22..25, id: Name("int"), ctx: Load, @@ -54,6 +61,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 28..31, id: Name("str"), ctx: Load, @@ -65,9 +73,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 32..60, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 37..38, id: Name("X"), ctx: Store, @@ -76,9 +86,11 @@ Module( type_params: None, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 41..60, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 41..44, id: Name("int"), ctx: Load, @@ -87,11 +99,13 @@ Module( op: BitOr, right: StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 47..60, value: StringLiteralValue { inner: Single( StringLiteral { range: 47..60, + node_index: AtomicNodeIndex(..), value: "ForwardRefY", flags: StringLiteralFlags { quote_style: Double, @@ -109,9 +123,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 61..87, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..67, id: Name("X"), ctx: Store, @@ -120,13 +136,16 @@ Module( type_params: Some( TypeParams { range: 67..70, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 68..69, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 68..69, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -137,9 +156,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 73..87, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("T"), ctx: Load, @@ -148,9 +169,11 @@ Module( op: BitOr, right: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 77..87, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 77..81, id: Name("list"), ctx: Load, @@ -158,9 +181,11 @@ Module( ), slice: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 82..86, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 82..83, id: Name("X"), ctx: Load, @@ -168,6 +193,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..85, id: Name("T"), ctx: Load, @@ -185,9 +211,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 101..116, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 106..107, id: Name("X"), ctx: Store, @@ -196,13 +224,16 @@ Module( type_params: Some( TypeParams { range: 107..110, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 108..109, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 108..109, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -213,6 +244,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 113..116, id: Name("int"), ctx: Load, @@ -222,9 +254,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 117..145, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 122..123, id: Name("X"), ctx: Store, @@ -233,13 +267,16 @@ Module( type_params: Some( TypeParams { range: 123..126, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 124..125, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 124..125, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -250,12 +287,15 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 129..145, left: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 129..136, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..133, id: Name("list"), ctx: Load, @@ -263,6 +303,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 134..135, id: Name("T"), ctx: Load, @@ -274,9 +315,11 @@ Module( op: BitOr, right: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 139..145, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 139..142, id: Name("set"), ctx: Load, @@ -284,6 +327,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..144, id: Name("T"), ctx: Load, @@ -298,9 +342,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 146..178, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 151..152, id: Name("X"), ctx: Store, @@ -309,13 +355,16 @@ Module( type_params: Some( TypeParams { range: 152..165, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 153..154, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 153..154, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -324,9 +373,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 156..159, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 157..159, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -334,9 +385,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 161..164, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 163..164, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -346,10 +399,12 @@ Module( ), value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 168..178, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 169..170, id: Name("T"), ctx: Load, @@ -357,6 +412,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 172..174, id: Name("Ts"), ctx: Load, @@ -364,6 +420,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 176..177, id: Name("P"), ctx: Load, @@ -378,9 +435,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 179..216, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..185, id: Name("X"), ctx: Store, @@ -389,17 +448,21 @@ Module( type_params: Some( TypeParams { range: 185..203, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 186..192, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 186..187, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 189..192, id: Name("int"), ctx: Load, @@ -412,9 +475,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 194..197, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 195..197, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -422,9 +487,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 199..202, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 201..202, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -434,10 +501,12 @@ Module( ), value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 206..216, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 207..208, id: Name("T"), ctx: Load, @@ -445,6 +514,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 210..212, id: Name("Ts"), ctx: Load, @@ -452,6 +522,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("P"), ctx: Load, @@ -466,9 +537,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 217..261, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..223, id: Name("X"), ctx: Store, @@ -477,21 +550,26 @@ Module( type_params: Some( TypeParams { range: 223..248, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 224..237, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 224..225, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 227..237, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 228..231, id: Name("int"), ctx: Load, @@ -499,6 +577,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 233..236, id: Name("str"), ctx: Load, @@ -516,9 +595,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 239..242, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 240..242, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -526,9 +607,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 244..247, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 246..247, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -538,10 +621,12 @@ Module( ), value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 251..261, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 252..253, id: Name("T"), ctx: Load, @@ -549,6 +634,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 255..257, id: Name("Ts"), ctx: Load, @@ -556,6 +642,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 259..260, id: Name("P"), ctx: Load, @@ -570,9 +657,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 262..287, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 267..268, id: Name("X"), ctx: Store, @@ -581,18 +670,22 @@ Module( type_params: Some( TypeParams { range: 268..277, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 269..276, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 269..270, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 273..276, id: Name("int"), ctx: Load, @@ -606,9 +699,11 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 280..287, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 280..281, id: Name("T"), ctx: Load, @@ -617,6 +712,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 284..287, id: Name("str"), ctx: Load, @@ -628,9 +724,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 288..330, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 293..294, id: Name("X"), ctx: Store, @@ -639,20 +737,25 @@ Module( type_params: Some( TypeParams { range: 294..314, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 295..313, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 295..296, + node_index: AtomicNodeIndex(..), }, bound: Some( BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 298..307, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 298..301, id: Name("int"), ctx: Load, @@ -661,6 +764,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 304..307, id: Name("str"), ctx: Load, @@ -672,6 +776,7 @@ Module( default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 310..313, id: Name("int"), ctx: Load, @@ -685,12 +790,15 @@ Module( ), value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 317..330, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 317..324, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 317..318, id: Name("T"), ctx: Load, @@ -699,6 +807,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 321..324, id: Name("int"), ctx: Load, @@ -709,6 +818,7 @@ Module( op: BitOr, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 327..330, id: Name("str"), ctx: Load, @@ -720,9 +830,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 331..384, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 336..337, id: Name("X"), ctx: Store, @@ -731,23 +843,29 @@ Module( type_params: Some( TypeParams { range: 337..361, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 338..360, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 339..341, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 344..360, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 345..360, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 345..350, id: Name("tuple"), ctx: Load, @@ -755,10 +873,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 351..359, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 351..354, id: Name("int"), ctx: Load, @@ -766,6 +886,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 356..359, id: Name("str"), ctx: Load, @@ -790,9 +911,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 364..384, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 364..369, id: Name("tuple"), ctx: Load, @@ -800,10 +923,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 370..383, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 370..373, id: Name("int"), ctx: Load, @@ -811,9 +936,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 375..378, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 376..378, id: Name("Ts"), ctx: Load, @@ -824,6 +951,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 380..383, id: Name("str"), ctx: Load, @@ -841,9 +969,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 385..428, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 390..391, id: Name("X"), ctx: Store, @@ -852,21 +982,26 @@ Module( type_params: Some( TypeParams { range: 391..409, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 392..408, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 394..395, + node_index: AtomicNodeIndex(..), }, default: Some( List( ExprList { + node_index: AtomicNodeIndex(..), range: 398..408, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 399..402, id: Name("int"), ctx: Load, @@ -874,6 +1009,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 404..407, id: Name("str"), ctx: Load, @@ -891,9 +1027,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 412..428, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 412..420, id: Name("Callable"), ctx: Load, @@ -901,10 +1039,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 421..427, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 421..422, id: Name("P"), ctx: Load, @@ -912,6 +1052,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 424..427, id: Name("str"), ctx: Load, @@ -929,9 +1070,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 459..474, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 464..468, id: Name("type"), ctx: Store, @@ -940,6 +1083,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 471..474, id: Name("int"), ctx: Load, @@ -949,9 +1093,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 475..491, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 480..485, id: Name("match"), ctx: Store, @@ -960,6 +1106,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 488..491, id: Name("int"), ctx: Load, @@ -969,9 +1116,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 492..507, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 497..501, id: Name("case"), ctx: Store, @@ -980,6 +1129,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 504..507, id: Name("int"), ctx: Load, @@ -989,9 +1139,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 533..548, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 538..541, id: Name("foo"), ctx: Store, @@ -1000,6 +1152,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 544..548, id: Name("type"), ctx: Load, @@ -1009,9 +1162,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 549..565, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 554..557, id: Name("foo"), ctx: Store, @@ -1020,6 +1175,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 560..565, id: Name("match"), ctx: Load, @@ -1029,9 +1185,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 566..581, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 571..574, id: Name("foo"), ctx: Store, @@ -1040,6 +1198,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 577..581, id: Name("case"), ctx: Load, @@ -1049,9 +1208,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 605..620, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 613..614, id: Name("X"), ctx: Store, @@ -1060,6 +1221,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 617..620, id: Name("int"), ctx: Load, @@ -1069,9 +1231,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 621..636, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 626..627, id: Name("X"), ctx: Store, @@ -1080,6 +1244,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 633..636, id: Name("int"), ctx: Load, @@ -1089,9 +1254,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 637..652, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 642..643, id: Name("X"), ctx: Store, @@ -1100,6 +1267,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 649..652, id: Name("int"), ctx: Load, @@ -1109,9 +1277,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 653..673, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 658..659, id: Name("X"), ctx: Store, @@ -1120,6 +1290,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 668..671, id: Name("int"), ctx: Load, @@ -1129,9 +1300,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 674..693, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 685..686, id: Name("X"), ctx: Store, @@ -1140,13 +1313,16 @@ Module( type_params: Some( TypeParams { range: 686..689, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 687..688, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 687..688, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -1157,6 +1333,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 692..693, id: Name("T"), ctx: Load, @@ -1166,9 +1343,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 694..714, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 699..700, id: Name("X"), ctx: Store, @@ -1177,13 +1356,16 @@ Module( type_params: Some( TypeParams { range: 707..710, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 708..709, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 708..709, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -1194,6 +1376,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 713..714, id: Name("T"), ctx: Load, @@ -1203,9 +1386,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 715..734, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 720..721, id: Name("X"), ctx: Store, @@ -1214,13 +1399,16 @@ Module( type_params: Some( TypeParams { range: 721..724, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 722..723, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 722..723, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -1231,6 +1419,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 733..734, id: Name("T"), ctx: Load, @@ -1240,9 +1429,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 756..768, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 761..762, id: Name("X"), ctx: Store, @@ -1251,6 +1442,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 765..768, id: Name("int"), ctx: Load, @@ -1260,9 +1452,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 770..782, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 775..776, id: Name("X"), ctx: Store, @@ -1271,6 +1465,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 779..782, id: Name("str"), ctx: Load, @@ -1280,9 +1475,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 784..797, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 789..790, id: Name("X"), ctx: Store, @@ -1291,6 +1488,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 793..797, id: Name("type"), ctx: Load, @@ -1300,20 +1498,24 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 798..819, decorator_list: [], name: Identifier { id: Name("X"), range: 804..805, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: None, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 807..819, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 812..813, id: Name("X"), ctx: Store, @@ -1322,6 +1524,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 816..819, id: Name("int"), ctx: Load, @@ -1334,9 +1537,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 821..853, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 826..831, id: Name("Point"), ctx: Store, @@ -1345,9 +1550,11 @@ Module( type_params: None, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 834..853, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 834..839, id: Name("tuple"), ctx: Load, @@ -1355,10 +1562,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 840..852, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 840..845, id: Name("float"), ctx: Load, @@ -1366,6 +1575,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 847..852, id: Name("float"), ctx: Load, @@ -1383,9 +1593,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 854..881, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 859..864, id: Name("Point"), ctx: Store, @@ -1394,13 +1606,16 @@ Module( type_params: Some( TypeParams { range: 864..867, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 865..866, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 865..866, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -1411,9 +1626,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 870..881, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 870..875, id: Name("tuple"), ctx: Load, @@ -1421,10 +1638,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 876..880, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 876..877, id: Name("T"), ctx: Load, @@ -1432,6 +1651,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 879..880, id: Name("T"), ctx: Load, @@ -1449,9 +1669,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 882..918, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 887..894, id: Name("IntFunc"), ctx: Store, @@ -1460,13 +1682,16 @@ Module( type_params: Some( TypeParams { range: 894..899, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 895..898, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 897..898, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -1476,9 +1701,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 902..918, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 902..910, id: Name("Callable"), ctx: Load, @@ -1486,10 +1713,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 911..917, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 911..912, id: Name("P"), ctx: Load, @@ -1497,6 +1726,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 914..917, id: Name("int"), ctx: Load, @@ -1514,9 +1744,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 932..972, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 937..949, id: Name("LabeledTuple"), ctx: Store, @@ -1525,13 +1757,16 @@ Module( type_params: Some( TypeParams { range: 949..954, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 950..953, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 951..953, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -1541,9 +1776,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 957..972, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 957..962, id: Name("tuple"), ctx: Load, @@ -1551,10 +1788,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 963..971, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 963..966, id: Name("str"), ctx: Load, @@ -1562,9 +1801,11 @@ Module( ), Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 968..971, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 969..971, id: Name("Ts"), ctx: Load, @@ -1585,9 +1826,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 989..1037, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 994..1010, id: Name("HashableSequence"), ctx: Store, @@ -1596,17 +1839,21 @@ Module( type_params: Some( TypeParams { range: 1010..1023, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 1011..1022, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 1011..1012, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1014..1022, id: Name("Hashable"), ctx: Load, @@ -1621,9 +1868,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1026..1037, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1026..1034, id: Name("Sequence"), ctx: Load, @@ -1631,6 +1880,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1035..1036, id: Name("T"), ctx: Load, @@ -1643,9 +1893,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 1060..1110, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1065..1081, id: Name("IntOrStrSequence"), ctx: Store, @@ -1654,21 +1906,26 @@ Module( type_params: Some( TypeParams { range: 1081..1096, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 1082..1095, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 1082..1083, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1085..1095, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1086..1089, id: Name("int"), ctx: Load, @@ -1676,6 +1933,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1091..1094, id: Name("str"), ctx: Load, @@ -1695,9 +1953,11 @@ Module( ), value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1099..1110, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1099..1107, id: Name("Sequence"), ctx: Load, @@ -1705,6 +1965,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1108..1109, id: Name("T"), ctx: Load, @@ -1717,19 +1978,24 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1164..1178, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1164..1178, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1164..1175, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1164..1171, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1164..1168, id: Name("type"), ctx: Load, @@ -1738,6 +2004,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1170..1171, id: Name("a"), ctx: Load, @@ -1748,6 +2015,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1174..1175, id: Name("b"), ctx: Load, @@ -1757,6 +2025,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1177..1178, id: Name("c"), ctx: Load, @@ -1771,16 +2040,20 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1203..1219, value: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1203..1219, elts: [ BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1203..1216, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1203..1207, id: Name("type"), ctx: Load, @@ -1789,9 +2062,11 @@ Module( op: Mult, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1210..1215, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1210..1211, id: Name("a"), ctx: Load, @@ -1800,6 +2075,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1214..1215, id: Name("b"), ctx: Load, @@ -1811,6 +2087,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1218..1219, id: Name("c"), ctx: Load, @@ -1825,12 +2102,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1244..1260, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1244..1260, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1244..1248, id: Name("type"), ctx: Load, @@ -1838,15 +2118,19 @@ Module( ), arguments: Arguments { range: 1249..1260, + node_index: AtomicNodeIndex(..), args: [ Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 1250..1256, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1251..1256, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1251..1252, id: Name("a"), ctx: Load, @@ -1855,6 +2139,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1255..1256, id: Name("b"), ctx: Load, @@ -1867,6 +2152,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1258..1259, id: Name("c"), ctx: Load, @@ -1881,15 +2167,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1286..1301, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1286..1301, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1286..1297, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1286..1290, id: Name("type"), ctx: Load, @@ -1898,9 +2188,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1292..1297, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1292..1293, id: Name("a"), ctx: Load, @@ -1909,6 +2201,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1296..1297, id: Name("b"), ctx: Load, @@ -1921,6 +2214,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1300..1301, id: Name("c"), ctx: Load, @@ -1932,15 +2226,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1327..1344, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1327..1344, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1327..1340, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1327..1331, id: Name("type"), ctx: Load, @@ -1949,9 +2247,11 @@ Module( op: Sub, right: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1334..1339, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1334..1335, id: Name("a"), ctx: Load, @@ -1960,6 +2260,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1338..1339, id: Name("b"), ctx: Load, @@ -1972,6 +2273,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1343..1344, id: Name("c"), ctx: Load, @@ -1983,18 +2285,23 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1370..1387, value: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1370..1387, left: BinOp( ExprBinOp { + node_index: AtomicNodeIndex(..), range: 1370..1383, left: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1370..1379, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1370..1374, id: Name("type"), ctx: Load, @@ -2002,13 +2309,16 @@ Module( ), arguments: Arguments { range: 1375..1379, + node_index: AtomicNodeIndex(..), args: [ UnaryOp( ExprUnaryOp { + node_index: AtomicNodeIndex(..), range: 1376..1378, op: USub, operand: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1377..1378, id: Name("a"), ctx: Load, @@ -2024,6 +2334,7 @@ Module( op: Mult, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1382..1383, id: Name("b"), ctx: Load, @@ -2034,6 +2345,7 @@ Module( op: Add, right: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1386..1387, id: Name("c"), ctx: Load, @@ -2045,15 +2357,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1414..1423, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1414..1423, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1414..1421, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1414..1418, id: Name("type"), ctx: Load, @@ -2061,6 +2377,7 @@ Module( ), arguments: Arguments { range: 1419..1421, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -2069,6 +2386,7 @@ Module( attr: Identifier { id: Name("a"), range: 1422..1423, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2077,15 +2395,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1439..1450, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1439..1450, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1439..1448, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1439..1443, id: Name("type"), ctx: Load, @@ -2093,9 +2415,11 @@ Module( ), arguments: Arguments { range: 1444..1448, + node_index: AtomicNodeIndex(..), args: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1445..1447, elts: [], ctx: Load, @@ -2110,6 +2434,7 @@ Module( attr: Identifier { id: Name("a"), range: 1449..1450, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2118,15 +2443,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1468..1480, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1468..1480, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1468..1478, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1468..1472, id: Name("type"), ctx: Load, @@ -2134,9 +2463,11 @@ Module( ), arguments: Arguments { range: 1473..1478, + node_index: AtomicNodeIndex(..), args: [ Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1474..1476, elts: [], ctx: Load, @@ -2151,6 +2482,7 @@ Module( attr: Identifier { id: Name("a"), range: 1479..1480, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2159,15 +2491,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1498..1508, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1498..1508, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1498..1506, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1498..1502, id: Name("type"), ctx: Load, @@ -2175,6 +2511,7 @@ Module( ), slice: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1504..1505, id: Name("a"), ctx: Load, @@ -2186,6 +2523,7 @@ Module( attr: Identifier { id: Name("b"), range: 1507..1508, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2194,15 +2532,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1525..1536, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1525..1536, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1525..1534, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1525..1529, id: Name("type"), ctx: Load, @@ -2210,10 +2552,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1531..1533, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1531..1532, id: Name("a"), ctx: Load, @@ -2230,6 +2574,7 @@ Module( attr: Identifier { id: Name("b"), range: 1535..1536, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2238,15 +2583,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1575..1588, value: Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 1575..1588, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1575..1586, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1575..1579, id: Name("type"), ctx: Load, @@ -2254,10 +2603,12 @@ Module( ), slice: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 1581..1585, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1582..1583, id: Name("a"), ctx: Load, @@ -2274,6 +2625,7 @@ Module( attr: Identifier { id: Name("b"), range: 1587..1588, + node_index: AtomicNodeIndex(..), }, ctx: Load, }, @@ -2282,15 +2634,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1608..1624, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 1608..1624, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1608..1614, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1608..1612, id: Name("type"), ctx: Load, @@ -2298,6 +2654,7 @@ Module( ), arguments: Arguments { range: 1612..1614, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -2305,10 +2662,12 @@ Module( ), slice: Slice( ExprSlice { + node_index: AtomicNodeIndex(..), range: 1615..1623, lower: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1615..1616, id: Name("a"), ctx: Load, @@ -2318,6 +2677,7 @@ Module( upper: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1622..1623, id: Name("b"), ctx: Load, @@ -2334,12 +2694,15 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 1643..1661, test: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 1646..1655, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1646..1650, id: Name("type"), ctx: Store, @@ -2347,6 +2710,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1654..1655, value: Int( 1, @@ -2358,6 +2722,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 1657..1661, }, ), @@ -2367,10 +2732,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1662..1697, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1662..1666, id: Name("type"), ctx: Store, @@ -2379,19 +2746,26 @@ Module( ], value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 1669..1697, parameters: Some( Parameters { range: 1676..1681, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1676..1681, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1676..1681, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("query"), range: 1676..1681, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2405,9 +2779,11 @@ Module( ), body: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 1683..1697, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1683..1688, id: Name("query"), ctx: Load, @@ -2419,6 +2795,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1692..1697, id: Name("event"), ctx: Load, @@ -2433,12 +2810,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1698..1713, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1698..1713, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1698..1703, id: Name("print"), ctx: Load, @@ -2446,12 +2826,15 @@ Module( ), arguments: Arguments { range: 1703..1713, + node_index: AtomicNodeIndex(..), args: [ Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1704..1712, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1704..1708, id: Name("type"), ctx: Load, @@ -2459,9 +2842,11 @@ Module( ), arguments: Arguments { range: 1708..1712, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1709..1711, value: Int( 12, @@ -2482,12 +2867,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1714..1724, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1714..1724, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1714..1718, id: Name("type"), ctx: Load, @@ -2495,9 +2883,11 @@ Module( ), arguments: Arguments { range: 1718..1724, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1719..1723, id: Name("type"), ctx: Load, @@ -2512,10 +2902,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1725..1743, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1725..1726, id: Name("a"), ctx: Store, @@ -2524,9 +2916,11 @@ Module( ], value: Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 1732..1741, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1732..1736, id: Name("type"), ctx: Load, @@ -2538,6 +2932,7 @@ Module( comparators: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1740..1741, id: Name("C"), ctx: Load, @@ -2550,10 +2945,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1744..1760, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1744..1745, id: Name("a"), ctx: Store, @@ -2562,9 +2959,11 @@ Module( ], value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1751..1758, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1751..1755, id: Name("type"), ctx: Load, @@ -2572,9 +2971,11 @@ Module( ), arguments: Arguments { range: 1755..1758, + node_index: AtomicNodeIndex(..), args: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1756..1757, id: Name("b"), ctx: Load, @@ -2589,12 +2990,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1761..1778, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 1761..1778, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1761..1765, id: Name("type"), ctx: Load, @@ -2602,18 +3006,22 @@ Module( ), arguments: Arguments { range: 1766..1778, + node_index: AtomicNodeIndex(..), args: [], keywords: [ Keyword { range: 1769..1776, + node_index: AtomicNodeIndex(..), arg: Some( Identifier { id: Name("X"), range: 1769..1770, + node_index: AtomicNodeIndex(..), }, ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1773..1776, id: Name("int"), ctx: Load, @@ -2628,10 +3036,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1779..1787, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1779..1783, id: Name("type"), ctx: Store, @@ -2640,6 +3050,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1786..1787, value: Int( 1, @@ -2650,10 +3061,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1788..1800, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1788..1792, id: Name("type"), ctx: Store, @@ -2661,6 +3074,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1795..1796, id: Name("x"), ctx: Store, @@ -2669,6 +3083,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1799..1800, value: Int( 1, @@ -2679,10 +3094,12 @@ Module( ), Assign( StmtAssign { + node_index: AtomicNodeIndex(..), range: 1801..1813, targets: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1801..1802, id: Name("x"), ctx: Store, @@ -2690,6 +3107,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1805..1809, id: Name("type"), ctx: Store, @@ -2698,6 +3116,7 @@ Module( ], value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 1812..1813, value: Int( 1, @@ -2708,22 +3127,30 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 1814..1828, value: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 1814..1828, parameters: Some( Parameters { range: 1821..1822, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 1821..1822, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 1821..1822, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 1821..1822, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -2737,6 +3164,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 1824..1828, id: Name("type"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__while.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__while.py.snap index a70b73cc90e20..5c2e2603e1965 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__while.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__while.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/while.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..314, body: [ While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 0..16, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 6..7, id: Name("x"), ctx: Load, @@ -23,9 +25,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 13..16, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 13..16, }, ), @@ -37,17 +41,21 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 18..61, test: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 24..37, op: And, values: [ Compare( ExprCompare { + node_index: AtomicNodeIndex(..), range: 25..30, left: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 25..26, id: Name("x"), ctx: Load, @@ -59,6 +67,7 @@ Module( comparators: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 29..30, value: Int( 1, @@ -70,6 +79,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 36..37, id: Name("y"), ctx: Load, @@ -81,6 +91,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 43..47, }, ), @@ -88,9 +99,11 @@ Module( orelse: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..61, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 58..61, }, ), @@ -101,14 +114,17 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 63..152, test: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 69..76, op: And, values: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 69..70, id: Name("x"), ctx: Load, @@ -116,6 +132,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 75..76, id: Name("y"), ctx: Load, @@ -127,9 +144,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 82..85, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 82..85, }, ), @@ -137,12 +156,15 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 90..111, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 90..111, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..95, id: Name("print"), ctx: Load, @@ -150,14 +172,17 @@ Module( ), arguments: Arguments { range: 95..111, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 96..110, value: StringLiteralValue { inner: Single( StringLiteral { range: 96..110, + node_index: AtomicNodeIndex(..), value: "Hello World!", flags: StringLiteralFlags { quote_style: Single, @@ -180,12 +205,15 @@ Module( orelse: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 123..144, value: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 123..144, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 123..128, id: Name("print"), ctx: Load, @@ -193,14 +221,17 @@ Module( ), arguments: Arguments { range: 128..144, + node_index: AtomicNodeIndex(..), args: [ StringLiteral( ExprStringLiteral { + node_index: AtomicNodeIndex(..), range: 129..143, value: StringLiteralValue { inner: Single( StringLiteral { range: 129..143, + node_index: AtomicNodeIndex(..), value: "Olá, Mundo!", flags: StringLiteralFlags { quote_style: Single, @@ -221,9 +252,11 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 149..152, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 149..152, }, ), @@ -234,12 +267,15 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 154..171, test: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 160..166, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 160..161, id: Name("a"), ctx: Store, @@ -247,6 +283,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..166, id: Name("b"), ctx: Load, @@ -257,9 +294,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 168..171, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 168..171, }, ), @@ -271,17 +310,21 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 172..197, test: BoolOp( ExprBoolOp { + node_index: AtomicNodeIndex(..), range: 178..192, op: And, values: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 179..185, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 179..180, id: Name("a"), ctx: Store, @@ -289,6 +332,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..185, id: Name("b"), ctx: Load, @@ -298,6 +342,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 191..192, id: Name("c"), ctx: Load, @@ -309,9 +354,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 194..197, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 194..197, }, ), @@ -323,22 +370,30 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 198..220, test: Lambda( ExprLambda { + node_index: AtomicNodeIndex(..), range: 204..215, parameters: Some( Parameters { range: 211..212, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 211..212, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 211..212, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 211..212, + node_index: AtomicNodeIndex(..), }, annotation: None, }, @@ -352,6 +407,7 @@ Module( ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 214..215, id: Name("x"), ctx: Load, @@ -362,9 +418,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 217..220, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 217..220, }, ), @@ -376,12 +434,15 @@ Module( ), While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 221..239, test: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 227..234, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 233..234, id: Name("x"), ctx: Load, @@ -392,9 +453,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 236..239, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 236..239, }, ), @@ -406,9 +469,11 @@ Module( ), If( StmtIf { + node_index: AtomicNodeIndex(..), range: 241..313, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 244..248, value: true, }, @@ -416,9 +481,11 @@ Module( body: [ While( StmtWhile { + node_index: AtomicNodeIndex(..), range: 254..298, test: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 260..261, id: Name("x"), ctx: Load, @@ -427,6 +494,7 @@ Module( body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 271..275, }, ), @@ -434,6 +502,7 @@ Module( orelse: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 294..298, }, ), @@ -444,10 +513,12 @@ Module( elif_else_clauses: [ ElifElseClause { range: 299..313, + node_index: AtomicNodeIndex(..), test: None, body: [ Pass( StmtPass { + node_index: AtomicNodeIndex(..), range: 309..313, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__with.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__with.py.snap index 260f39d072d03..993b8cc48ad03 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__with.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__with.py.snap @@ -1,24 +1,27 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/with.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..361, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 137..151, is_async: false, items: [ WithItem { range: 142..146, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 142..146, id: Name("item"), ctx: Load, @@ -30,9 +33,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 148..151, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 148..151, }, ), @@ -43,13 +48,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 152..171, is_async: false, items: [ WithItem { range: 157..166, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 157..161, id: Name("item"), ctx: Load, @@ -58,6 +66,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 165..166, id: Name("f"), ctx: Store, @@ -69,9 +78,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 168..171, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 168..171, }, ), @@ -82,13 +93,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 172..194, is_async: false, items: [ WithItem { range: 177..182, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 177..182, id: Name("item1"), ctx: Load, @@ -98,8 +112,10 @@ Module( }, WithItem { range: 184..189, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 184..189, id: Name("item2"), ctx: Load, @@ -111,9 +127,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 191..194, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 191..194, }, ), @@ -124,13 +142,16 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 195..229, is_async: false, items: [ WithItem { range: 200..211, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 200..205, id: Name("item1"), ctx: Load, @@ -139,6 +160,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 209..211, id: Name("f1"), ctx: Store, @@ -148,8 +170,10 @@ Module( }, WithItem { range: 213..224, + node_index: AtomicNodeIndex(..), context_expr: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 213..218, id: Name("item2"), ctx: Load, @@ -158,6 +182,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 222..224, id: Name("f2"), ctx: Store, @@ -169,9 +194,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 226..229, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 226..229, }, ), @@ -182,22 +209,27 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 231..257, is_async: false, items: [ WithItem { range: 236..252, + node_index: AtomicNodeIndex(..), context_expr: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 236..252, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 241..245, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 236..237, id: Name("x"), ctx: Load, @@ -205,6 +237,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 251..252, id: Name("y"), ctx: Load, @@ -218,9 +251,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 254..257, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 254..257, }, ), @@ -231,22 +266,27 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 258..289, is_async: false, items: [ WithItem { range: 263..284, + node_index: AtomicNodeIndex(..), context_expr: If( ExprIf { + node_index: AtomicNodeIndex(..), range: 263..279, test: BooleanLiteral( ExprBooleanLiteral { + node_index: AtomicNodeIndex(..), range: 268..272, value: true, }, ), body: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 263..264, id: Name("x"), ctx: Load, @@ -254,6 +294,7 @@ Module( ), orelse: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 278..279, id: Name("y"), ctx: Load, @@ -264,6 +305,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 283..284, id: Name("f"), ctx: Store, @@ -275,9 +317,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 286..289, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 286..289, }, ), @@ -288,16 +332,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 313..334, is_async: false, items: [ WithItem { range: 318..329, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 318..324, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 318..322, id: Name("open"), ctx: Load, @@ -305,6 +353,7 @@ Module( ), arguments: Arguments { range: 322..324, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -313,6 +362,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 328..329, id: Name("f"), ctx: Store, @@ -324,9 +374,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 331..334, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 331..334, }, ), @@ -337,16 +389,20 @@ Module( ), With( StmtWith { + node_index: AtomicNodeIndex(..), range: 335..361, is_async: false, items: [ WithItem { range: 340..356, + node_index: AtomicNodeIndex(..), context_expr: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 340..346, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 340..344, id: Name("open"), ctx: Load, @@ -354,6 +410,7 @@ Module( ), arguments: Arguments { range: 344..346, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -362,9 +419,11 @@ Module( optional_vars: Some( Attribute( ExprAttribute { + node_index: AtomicNodeIndex(..), range: 350..356, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 350..351, id: Name("f"), ctx: Load, @@ -373,6 +432,7 @@ Module( attr: Identifier { id: Name("attr"), range: 352..356, + node_index: AtomicNodeIndex(..), }, ctx: Store, }, @@ -383,9 +443,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 358..361, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 358..361, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@template_strings_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@template_strings_py314.py.snap new file mode 100644 index 0000000000000..f314487ff6b82 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@template_strings_py314.py.snap @@ -0,0 +1,194 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/template_strings_py314.py +--- +## AST + +``` +Module( + ModModule { + node_index: AtomicNodeIndex(..), + range: 0..117, + body: [ + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 44..52, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 44..52, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..52, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 46..51, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 47..50, + id: Name("hey"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 53..63, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 53..63, + value: TStringValue { + inner: Single( + TString( + TString { + range: 53..63, + node_index: AtomicNodeIndex(..), + elements: [ + Interpolation( + InterpolatedElement { + range: 55..62, + node_index: AtomicNodeIndex(..), + expression: Name( + ExprName { + node_index: AtomicNodeIndex(..), + range: 56..61, + id: Name("there"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 64..88, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 64..88, + value: TStringValue { + inner: Single( + TString( + TString { + range: 64..88, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 68..85, + node_index: AtomicNodeIndex(..), + value: "what's\nhappening?", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: AtomicNodeIndex(..), + range: 89..116, + value: TString( + ExprTString { + node_index: AtomicNodeIndex(..), + range: 89..116, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 89..101, + node_index: AtomicNodeIndex(..), + value: "implicitly", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 101..116, + node_index: AtomicNodeIndex(..), + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 103..115, + node_index: AtomicNodeIndex(..), + value: "concatenated", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@tuple_context_manager_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@tuple_context_manager_py38.py.snap index 70180394159ad..46550ad2312b1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@tuple_context_manager_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@tuple_context_manager_py38.py.snap @@ -7,21 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/tuple_context_manager_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..85, body: [ With( StmtWith { + node_index: AtomicNodeIndex(..), range: 43..84, is_async: false, items: [ WithItem { range: 48..79, + node_index: AtomicNodeIndex(..), context_expr: Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 48..72, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 52..55, id: Name("foo"), ctx: Load, @@ -29,6 +34,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..62, id: Name("bar"), ctx: Load, @@ -36,6 +42,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 66..69, id: Name("baz"), ctx: Load, @@ -49,6 +56,7 @@ Module( optional_vars: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..79, id: Name("tup"), ctx: Store, @@ -60,9 +68,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 81..84, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 81..84, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap index 010da9ab55202..8387c4e874640 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_default_py313.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/type_param_default_py3 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..112, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 44..65, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("X"), ctx: Store, @@ -22,18 +25,22 @@ Module( type_params: Some( TypeParams { range: 50..59, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 51..58, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 51..52, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 55..58, id: Name("int"), ctx: Load, @@ -47,6 +54,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 62..65, id: Name("int"), ctx: Load, @@ -56,28 +64,34 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 66..87, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 70..71, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 71..80, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 72..79, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 72..73, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..79, id: Name("int"), ctx: Load, @@ -91,6 +105,9 @@ Module( ), parameters: Parameters { range: 80..82, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -101,9 +118,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 84..87, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 84..87, }, ), @@ -114,27 +133,33 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 88..111, decorator_list: [], name: Identifier { id: Name("C"), range: 94..95, + node_index: AtomicNodeIndex(..), }, type_params: Some( TypeParams { range: 95..104, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 96..103, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 96..97, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 100..103, id: Name("int"), ctx: Load, @@ -149,6 +174,7 @@ Module( arguments: Some( Arguments { range: 104..106, + node_index: AtomicNodeIndex(..), args: [], keywords: [], }, @@ -156,9 +182,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 108..111, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 108..111, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap index ad273b844ca88..18801458d7b05 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_param_spec.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/type_param_param_spec.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..90, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..17, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..11, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 7..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 9..10, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -48,9 +54,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 18..41, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("X"), ctx: Store, @@ -59,17 +67,21 @@ Module( type_params: Some( TypeParams { range: 24..35, + node_index: AtomicNodeIndex(..), type_params: [ ParamSpec( TypeParamParamSpec { range: 25..34, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 27..28, + node_index: AtomicNodeIndex(..), }, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..34, id: Name("int"), ctx: Load, @@ -83,6 +95,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..41, id: Name("int"), ctx: Load, @@ -92,9 +105,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 42..62, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("X"), ctx: Store, @@ -103,13 +118,16 @@ Module( type_params: Some( TypeParams { range: 48..56, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 49..50, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 49..50, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -118,9 +136,11 @@ Module( ParamSpec( TypeParamParamSpec { range: 52..55, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 54..55, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -130,6 +150,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..62, id: Name("int"), ctx: Load, @@ -139,9 +160,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 63..89, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 68..69, id: Name("X"), ctx: Store, @@ -150,13 +173,16 @@ Module( type_params: Some( TypeParams { range: 69..83, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 70..71, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 70..71, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -165,13 +191,16 @@ Module( ParamSpec( TypeParamParamSpec { range: 73..82, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("P"), range: 75..76, + node_index: AtomicNodeIndex(..), }, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 79..82, id: Name("int"), ctx: Load, @@ -185,6 +214,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 86..89, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap index 5f5cf396a8aae..1e340b41b1bb1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/type_param_type_var.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..147, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..15, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..9, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 7..8, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 7..8, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -40,6 +45,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 12..15, id: Name("int"), ctx: Load, @@ -49,9 +55,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 16..37, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 21..22, id: Name("X"), ctx: Store, @@ -60,18 +68,22 @@ Module( type_params: Some( TypeParams { range: 22..31, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 23..30, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 23..24, + node_index: AtomicNodeIndex(..), }, bound: None, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 27..30, id: Name("int"), ctx: Load, @@ -85,6 +97,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 34..37, id: Name("int"), ctx: Load, @@ -94,9 +107,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 38..64, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..44, id: Name("X"), ctx: Store, @@ -105,17 +120,21 @@ Module( type_params: Some( TypeParams { range: 44..58, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 45..57, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 45..46, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..51, id: Name("int"), ctx: Load, @@ -125,6 +144,7 @@ Module( default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 54..57, id: Name("int"), ctx: Load, @@ -138,6 +158,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 61..64, id: Name("int"), ctx: Load, @@ -147,9 +168,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 65..98, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 70..71, id: Name("X"), ctx: Store, @@ -158,21 +181,26 @@ Module( type_params: Some( TypeParams { range: 71..92, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 72..91, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 72..73, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 75..85, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 76..79, id: Name("int"), ctx: Load, @@ -180,6 +208,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..84, id: Name("int"), ctx: Load, @@ -194,6 +223,7 @@ Module( default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 88..91, id: Name("int"), ctx: Load, @@ -207,6 +237,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 95..98, id: Name("int"), ctx: Load, @@ -216,9 +247,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 99..146, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..105, id: Name("X"), ctx: Store, @@ -227,17 +260,21 @@ Module( type_params: Some( TypeParams { range: 105..140, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 106..118, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 106..107, + node_index: AtomicNodeIndex(..), }, bound: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 109..112, id: Name("int"), ctx: Load, @@ -247,6 +284,7 @@ Module( default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 115..118, id: Name("int"), ctx: Load, @@ -258,17 +296,21 @@ Module( TypeVar( TypeParamTypeVar { range: 120..139, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("U"), range: 120..121, + node_index: AtomicNodeIndex(..), }, bound: Some( Tuple( ExprTuple { + node_index: AtomicNodeIndex(..), range: 123..133, elts: [ Name( ExprName { + node_index: AtomicNodeIndex(..), range: 124..127, id: Name("int"), ctx: Load, @@ -276,6 +318,7 @@ Module( ), Name( ExprName { + node_index: AtomicNodeIndex(..), range: 129..132, id: Name("int"), ctx: Load, @@ -290,6 +333,7 @@ Module( default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 136..139, id: Name("int"), ctx: Load, @@ -303,6 +347,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 143..146, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap index 93e16848e431e..aa50926d78221 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_param_type_var_tuple.py.snap @@ -1,20 +1,22 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/type_param_type_var_tuple.py -snapshot_kind: text --- ## AST ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..115, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 0..17, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 5..6, id: Name("X"), ctx: Store, @@ -23,13 +25,16 @@ Module( type_params: Some( TypeParams { range: 6..11, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 7..10, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 8..10, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -39,6 +44,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 14..17, id: Name("int"), ctx: Load, @@ -48,9 +54,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 18..41, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 23..24, id: Name("X"), ctx: Store, @@ -59,17 +67,21 @@ Module( type_params: Some( TypeParams { range: 24..35, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 25..34, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 26..28, + node_index: AtomicNodeIndex(..), }, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 31..34, id: Name("int"), ctx: Load, @@ -83,6 +95,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 38..41, id: Name("int"), ctx: Load, @@ -92,9 +105,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 42..66, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("X"), ctx: Store, @@ -103,20 +118,25 @@ Module( type_params: Some( TypeParams { range: 48..60, + node_index: AtomicNodeIndex(..), type_params: [ TypeVarTuple( TypeParamTypeVarTuple { range: 49..59, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 50..52, + node_index: AtomicNodeIndex(..), }, default: Some( Starred( ExprStarred { + node_index: AtomicNodeIndex(..), range: 55..59, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..59, id: Name("int"), ctx: Load, @@ -133,6 +153,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 63..66, id: Name("int"), ctx: Load, @@ -142,9 +163,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 67..87, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 72..73, id: Name("X"), ctx: Store, @@ -153,13 +176,16 @@ Module( type_params: Some( TypeParams { range: 73..81, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 74..75, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 74..75, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -168,9 +194,11 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 77..80, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 78..80, + node_index: AtomicNodeIndex(..), }, default: None, }, @@ -180,6 +208,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 84..87, id: Name("int"), ctx: Load, @@ -189,9 +218,11 @@ Module( ), TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 88..114, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 93..94, id: Name("X"), ctx: Store, @@ -200,13 +231,16 @@ Module( type_params: Some( TypeParams { range: 94..108, + node_index: AtomicNodeIndex(..), type_params: [ TypeVar( TypeParamTypeVar { range: 95..96, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("T"), range: 95..96, + node_index: AtomicNodeIndex(..), }, bound: None, default: None, @@ -215,13 +249,16 @@ Module( TypeVarTuple( TypeParamTypeVarTuple { range: 98..107, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("Ts"), range: 99..101, + node_index: AtomicNodeIndex(..), }, default: Some( Name( ExprName { + node_index: AtomicNodeIndex(..), range: 104..107, id: Name("int"), ctx: Load, @@ -235,6 +272,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 111..114, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_stmt_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_stmt_py312.py.snap index 62eeead56c1fc..7e590b4515a15 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_stmt_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@type_stmt_py312.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/type_stmt_py312.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..57, body: [ TypeAlias( StmtTypeAlias { + node_index: AtomicNodeIndex(..), range: 44..56, name: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 49..50, id: Name("x"), ctx: Store, @@ -22,6 +25,7 @@ Module( type_params: None, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 53..56, id: Name("int"), ctx: Load, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap index e3329425de8f7..92c7cefb8639b 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_index_py39.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..53, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..52, value: Subscript( ExprSubscript { + node_index: AtomicNodeIndex(..), range: 43..52, value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 43..46, id: Name("lst"), ctx: Load, @@ -24,9 +28,11 @@ Module( ), slice: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 47..51, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 47..48, id: Name("x"), ctx: Store, @@ -34,6 +40,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 50..51, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap index 33d284c9b569a..0f8b9f47b721d 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@unparenthesized_named_expr_py39.py.snap @@ -7,20 +7,25 @@ input_file: crates/ruff_python_parser/resources/inline/ok/unparenthesized_named_ ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..88, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 43..57, value: Set( ExprSet { + node_index: AtomicNodeIndex(..), range: 43..57, elts: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 44..50, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("x"), ctx: Store, @@ -28,6 +33,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 49..50, value: Int( 1, @@ -38,6 +44,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 2, @@ -46,6 +53,7 @@ Module( ), NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 55..56, value: Int( 3, @@ -59,15 +67,19 @@ Module( ), Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 58..87, value: SetComp( ExprSetComp { + node_index: AtomicNodeIndex(..), range: 58..87, elt: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 59..68, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 59..63, id: Name("last"), ctx: Store, @@ -75,6 +87,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 67..68, id: Name("x"), ctx: Load, @@ -85,8 +98,10 @@ Module( generators: [ Comprehension { range: 69..86, + node_index: AtomicNodeIndex(..), target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("x"), ctx: Store, @@ -94,9 +109,11 @@ Module( ), iter: Call( ExprCall { + node_index: AtomicNodeIndex(..), range: 78..86, func: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 78..83, id: Name("range"), ctx: Load, @@ -104,9 +121,11 @@ Module( ), arguments: Arguments { range: 83..86, + node_index: AtomicNodeIndex(..), args: [ NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 84..85, value: Int( 3, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_class.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_class.py.snap index feeea973fbc0d..f4de814c8daad 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_class.py.snap @@ -7,26 +7,32 @@ input_file: crates/ruff_python_parser/resources/inline/ok/valid_annotation_class ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..137, body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 0..23, decorator_list: [], name: Identifier { id: Name("F"), range: 6..7, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 7..18, + node_index: AtomicNodeIndex(..), args: [ Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 8..17, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 8..9, id: Name("y"), ctx: Store, @@ -34,6 +40,7 @@ Module( ), value: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 13..17, id: Name("list"), ctx: Load, @@ -48,9 +55,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 20..23, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 20..23, }, ), @@ -61,16 +70,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 24..93, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 28..29, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 29..31, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -81,23 +95,28 @@ Module( body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 37..60, decorator_list: [], name: Identifier { id: Name("G"), range: 43..44, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 44..55, + node_index: AtomicNodeIndex(..), args: [ Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 46..53, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 52..53, value: Int( 1, @@ -114,9 +133,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 57..60, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 57..60, }, ), @@ -127,22 +148,27 @@ Module( ), ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 65..93, decorator_list: [], name: Identifier { id: Name("H"), range: 71..72, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 72..88, + node_index: AtomicNodeIndex(..), args: [ YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 74..86, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 85..86, value: Int( 1, @@ -158,9 +184,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 90..93, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 90..93, }, ), @@ -174,16 +202,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 94..136, is_async: true, decorator_list: [], name: Identifier { id: Name("f"), range: 104..105, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 105..107, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -194,22 +227,27 @@ Module( body: [ ClassDef( StmtClassDef { + node_index: AtomicNodeIndex(..), range: 113..136, decorator_list: [], name: Identifier { id: Name("G"), range: 119..120, + node_index: AtomicNodeIndex(..), }, type_params: None, arguments: Some( Arguments { range: 120..131, + node_index: AtomicNodeIndex(..), args: [ Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 122..129, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 128..129, value: Int( 1, @@ -225,9 +263,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..136, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 133..136, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_function_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_function_py313.py.snap index 1a58dffba2596..c1c32f560556f 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_function_py313.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_function_py313.py.snap @@ -7,20 +7,26 @@ input_file: crates/ruff_python_parser/resources/inline/ok/valid_annotation_funct ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..316, body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 44..68, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 48..49, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 49..51, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -30,9 +36,11 @@ Module( returns: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 56..62, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 56..57, id: Name("y"), ctx: Store, @@ -40,6 +48,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 61..62, value: Int( 3, @@ -52,9 +61,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 65..68, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 65..68, }, ), @@ -65,32 +76,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 69..94, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 73..74, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 74..89, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 75..88, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 75..88, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 75..78, + node_index: AtomicNodeIndex(..), }, annotation: Some( Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 81..87, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 81..82, id: Name("x"), ctx: Store, @@ -98,6 +119,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 86..87, value: Int( 1, @@ -119,9 +141,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 91..94, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 91..94, }, ), @@ -132,16 +156,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 95..235, is_async: false, decorator_list: [], name: Identifier { id: Name("outer"), range: 99..104, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 104..106, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -152,33 +181,43 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 112..136, is_async: false, decorator_list: [], name: Identifier { id: Name("i"), range: 116..117, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 117..131, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 118..130, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 118..130, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 118..119, + node_index: AtomicNodeIndex(..), }, annotation: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 122..129, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 128..129, value: Int( 1, @@ -201,9 +240,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 133..136, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 133..136, }, ), @@ -214,16 +255,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 141..166, is_async: false, decorator_list: [], name: Identifier { id: Name("k"), range: 145..146, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 146..148, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -233,10 +279,12 @@ Module( returns: Some( Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 153..160, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 159..160, value: Int( 1, @@ -250,9 +298,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 163..166, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 163..166, }, ), @@ -263,32 +313,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 171..200, is_async: false, decorator_list: [], name: Identifier { id: Name("m"), range: 175..176, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 176..195, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 177..194, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 177..194, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("x"), range: 177..178, + node_index: AtomicNodeIndex(..), }, annotation: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 181..193, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 192..193, value: Int( 1, @@ -310,9 +370,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 197..200, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 197..200, }, ), @@ -323,16 +385,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 205..235, is_async: false, decorator_list: [], name: Identifier { id: Name("o"), range: 209..210, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 210..212, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -342,9 +409,11 @@ Module( returns: Some( YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 217..229, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 228..229, value: Int( 1, @@ -357,9 +426,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 232..235, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 232..235, }, ), @@ -373,16 +444,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 236..315, is_async: true, decorator_list: [], name: Identifier { id: Name("outer"), range: 246..251, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 251..253, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -393,16 +469,21 @@ Module( body: [ FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 259..284, is_async: false, decorator_list: [], name: Identifier { id: Name("f"), range: 263..264, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 264..266, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -412,9 +493,11 @@ Module( returns: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 271..278, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 277..278, value: Int( 1, @@ -427,9 +510,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 281..284, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 281..284, }, ), @@ -440,32 +525,42 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 289..315, is_async: false, decorator_list: [], name: Identifier { id: Name("g"), range: 293..294, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 294..310, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [ ParameterWithDefault { range: 295..309, + node_index: AtomicNodeIndex(..), parameter: Parameter { range: 295..309, + node_index: AtomicNodeIndex(..), name: Identifier { id: Name("arg"), range: 295..298, + node_index: AtomicNodeIndex(..), }, annotation: Some( Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 301..308, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 307..308, value: Int( 1, @@ -487,9 +582,11 @@ Module( body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 312..315, value: EllipsisLiteral( ExprEllipsisLiteral { + node_index: AtomicNodeIndex(..), range: 312..315, }, ), diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_py313.py.snap index 9119467d8466e..be4b3961e8e84 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_py313.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@valid_annotation_py313.py.snap @@ -7,13 +7,16 @@ input_file: crates/ruff_python_parser/resources/inline/ok/valid_annotation_py313 ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..144, body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 44..55, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 44..45, id: Name("a"), ctx: Store, @@ -21,9 +24,11 @@ Module( ), annotation: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 48..54, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 48..49, id: Name("x"), ctx: Store, @@ -31,6 +36,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 53..54, value: Int( 1, @@ -45,16 +51,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 56..107, is_async: false, decorator_list: [], name: Identifier { id: Name("outer"), range: 60..65, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 65..67, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -65,9 +76,11 @@ Module( body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 73..85, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 73..74, id: Name("b"), ctx: Store, @@ -75,10 +88,12 @@ Module( ), annotation: Yield( ExprYield { + node_index: AtomicNodeIndex(..), range: 77..84, value: Some( NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 83..84, value: Int( 1, @@ -94,9 +109,11 @@ Module( ), AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 90..107, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 90..91, id: Name("c"), ctx: Store, @@ -104,9 +121,11 @@ Module( ), annotation: YieldFrom( ExprYieldFrom { + node_index: AtomicNodeIndex(..), range: 94..106, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 105..106, value: Int( 1, @@ -124,16 +143,21 @@ Module( ), FunctionDef( StmtFunctionDef { + node_index: AtomicNodeIndex(..), range: 108..143, is_async: true, decorator_list: [], name: Identifier { id: Name("outer"), range: 118..123, + node_index: AtomicNodeIndex(..), }, type_params: None, parameters: Parameters { range: 123..125, + node_index: AtomicNodeIndex( + 0, + ), posonlyargs: [], args: [], vararg: None, @@ -144,9 +168,11 @@ Module( body: [ AnnAssign( StmtAnnAssign { + node_index: AtomicNodeIndex(..), range: 131..143, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 131..132, id: Name("d"), ctx: Store, @@ -154,9 +180,11 @@ Module( ), annotation: Await( ExprAwait { + node_index: AtomicNodeIndex(..), range: 135..142, value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 141..142, value: Int( 1, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap index b0dfb2c4db18e..16008f3821501 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@walrus_py38.py.snap @@ -7,16 +7,20 @@ input_file: crates/ruff_python_parser/resources/inline/ok/walrus_py38.py ``` Module( ModModule { + node_index: AtomicNodeIndex(..), range: 0..54, body: [ Expr( StmtExpr { + node_index: AtomicNodeIndex(..), range: 45..53, value: Named( ExprNamed { + node_index: AtomicNodeIndex(..), range: 46..52, target: Name( ExprName { + node_index: AtomicNodeIndex(..), range: 46..47, id: Name("x"), ctx: Store, @@ -24,6 +28,7 @@ Module( ), value: NumberLiteral( ExprNumberLiteral { + node_index: AtomicNodeIndex(..), range: 51..52, value: Int( 1, diff --git a/crates/ruff_python_resolver/Cargo.toml b/crates/ruff_python_resolver/Cargo.toml deleted file mode 100644 index 65d9847c9d717..0000000000000 --- a/crates/ruff_python_resolver/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "ruff_python_resolver" -version = "0.0.0" -description = "A Python module resolver for Ruff" -publish = false -authors = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[lib] -doctest = false - -[dependencies] -log = { workspace = true } - -[dev-dependencies] -env_logger = { workspace = true } -tempfile = { workspace = true } -insta = { workspace = true } - -[lints] -workspace = true diff --git a/crates/ruff_python_resolver/resources/test/airflow/README.md b/crates/ruff_python_resolver/resources/test/airflow/README.md deleted file mode 100644 index 803b7c401b1d0..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# airflow - -This is a mock subset of the Airflow repository, used to test module resolution. diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py deleted file mode 100644 index 8b137891791fe..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py deleted file mode 100644 index 8b137891791fe..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py deleted file mode 100644 index 8b137891791fe..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py deleted file mode 100644 index 9b13b3153ea0a..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/api/common/mark_tasks.py +++ /dev/null @@ -1,14 +0,0 @@ -# Standard library. -import os - -# First-party. -from airflow.jobs.scheduler_job_runner import SchedulerJobRunner - -# Stub file. -from airflow.compat.functools import cached_property - -# Namespace package. -from airflow.providers.google.cloud.hooks.gcs import GCSHook - -# Third-party. -from sqlalchemy.orm import Query diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py deleted file mode 100644 index 13a83393a9124..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi b/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/compat/functools.pyi +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/scheduler_job_runner.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py b/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so deleted file mode 100644 index 0dff73800237f..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so +++ /dev/null @@ -1 +0,0 @@ -# Empty file included to support filesystem-based resolver tests. diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so deleted file mode 100644 index 0dff73800237f..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so +++ /dev/null @@ -1 +0,0 @@ -# Empty file included to support filesystem-based resolver tests. diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py deleted file mode 100644 index 8b137891791fe..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py b/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py deleted file mode 100644 index 75a6dead35cac..0000000000000 --- a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty file included to support filesystem-based resolver tests.""" diff --git a/crates/ruff_python_resolver/src/config.rs b/crates/ruff_python_resolver/src/config.rs deleted file mode 100644 index 072e44a993525..0000000000000 --- a/crates/ruff_python_resolver/src/config.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::path::PathBuf; - -pub(crate) struct Config { - /// Path to use for typeshed definitions. - pub(crate) typeshed_path: Option, - - /// Path to custom typings (stub) modules. - pub(crate) stub_path: Option, - - /// Path to a directory containing one or more virtual environment - /// directories. This is used in conjunction with the "venv" name in - /// the config file to identify the python environment used for resolving - /// third-party modules. - pub(crate) venv_path: Option, - - /// Default venv environment. - pub(crate) venv: Option, -} diff --git a/crates/ruff_python_resolver/src/execution_environment.rs b/crates/ruff_python_resolver/src/execution_environment.rs deleted file mode 100644 index b969ddc42b8eb..0000000000000 --- a/crates/ruff_python_resolver/src/execution_environment.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::path::PathBuf; - -use crate::python_platform::PythonPlatform; -use crate::python_version::PythonVersion; - -#[derive(Debug)] -pub(crate) struct ExecutionEnvironment { - /// The root directory of the execution environment. - pub(crate) root: PathBuf, - - /// The Python version of the execution environment. - pub(crate) python_version: PythonVersion, - - /// The Python platform of the execution environment. - pub(crate) python_platform: PythonPlatform, - - /// The extra search paths of the execution environment. - pub(crate) extra_paths: Vec, -} diff --git a/crates/ruff_python_resolver/src/host.rs b/crates/ruff_python_resolver/src/host.rs deleted file mode 100644 index be9b0a5e601d8..0000000000000 --- a/crates/ruff_python_resolver/src/host.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Expose the host environment to the resolver. - -use std::path::PathBuf; - -use crate::python_platform::PythonPlatform; -use crate::python_version::PythonVersion; - -/// A trait to expose the host environment to the resolver. -pub(crate) trait Host { - /// The search paths to use when resolving Python modules. - fn python_search_paths(&self) -> Vec; - - /// The Python version to use when resolving Python modules. - fn python_version(&self) -> PythonVersion; - - /// The OS platform to use when resolving Python modules. - fn python_platform(&self) -> PythonPlatform; -} - -/// A host that exposes a fixed set of search paths. -pub(crate) struct StaticHost { - search_paths: Vec, -} - -impl StaticHost { - pub(crate) fn new(search_paths: Vec) -> Self { - Self { search_paths } - } -} - -impl Host for StaticHost { - fn python_search_paths(&self) -> Vec { - self.search_paths.clone() - } - - fn python_version(&self) -> PythonVersion { - PythonVersion::Py312 - } - - fn python_platform(&self) -> PythonPlatform { - PythonPlatform::Darwin - } -} diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs deleted file mode 100644 index afa81d27ba8ef..0000000000000 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ /dev/null @@ -1,176 +0,0 @@ -use std::collections::BTreeMap; -use std::ffi::OsStr; -use std::io; -use std::path::{Path, PathBuf}; - -use crate::{native_module, py_typed}; - -/// A map of the submodules that are present in a namespace package. -/// -/// Namespace packages lack an `__init__.py` file. So when resolving symbols from a namespace -/// package, the symbols must be present as submodules. This map contains the submodules that are -/// present in the namespace package, keyed by their module name. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub(crate) struct ImplicitImports(BTreeMap); - -impl ImplicitImports { - /// Find the "implicit" imports within the namespace package at the given path. - pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> io::Result { - let mut submodules: BTreeMap = BTreeMap::new(); - - // Enumerate all files and directories in the path, expanding links. - for entry in dir_path.read_dir()?.flatten() { - let file_type = entry.file_type()?; - - let path = entry.path(); - if exclusions.contains(&path.as_path()) { - continue; - } - - // TODO(charlie): Support symlinks. - if file_type.is_file() { - // Add implicit file-based modules. - let Some(extension) = path.extension() else { - continue; - }; - - let (file_stem, is_native_lib) = if extension == "py" || extension == "pyi" { - // E.g., `foo.py` becomes `foo`. - let file_stem = path.file_stem().and_then(OsStr::to_str); - let is_native_lib = false; - (file_stem, is_native_lib) - } else if native_module::is_native_module_file_extension(extension) { - // E.g., `foo.abi3.so` becomes `foo`. - let file_stem = native_module::native_module_name(&path); - let is_native_lib = true; - (file_stem, is_native_lib) - } else { - continue; - }; - - let Some(name) = file_stem else { - continue; - }; - - // Always prefer stub files over non-stub files. - if submodules - .get(name) - .is_none_or(|implicit_import| !implicit_import.is_stub_file) - { - submodules.insert( - name.to_string(), - ImplicitImport { - is_stub_file: extension == "pyi", - is_native_lib, - path, - py_typed: None, - }, - ); - } - } else if file_type.is_dir() { - // Add implicit directory-based modules. - let py_file_path = path.join("__init__.py"); - let pyi_file_path = path.join("__init__.pyi"); - - let (path, is_stub_file) = if py_file_path.exists() { - (py_file_path, false) - } else if pyi_file_path.exists() { - (pyi_file_path, true) - } else { - continue; - }; - - let Some(name) = path.file_name().and_then(OsStr::to_str) else { - continue; - }; - submodules.insert( - name.to_string(), - ImplicitImport { - is_stub_file, - is_native_lib: false, - py_typed: py_typed::get_py_typed_info(&path), - path, - }, - ); - } - } - - Ok(Self(submodules)) - } - - /// Filter [`ImplicitImports`] to only those symbols that were imported. - pub(crate) fn filter(&self, imported_symbols: &[String]) -> Option { - if self.is_empty() || imported_symbols.is_empty() { - return None; - } - - let filtered: BTreeMap = self - .iter() - .filter(|(name, _)| imported_symbols.contains(name)) - .map(|(name, implicit_import)| (name.clone(), implicit_import.clone())) - .collect(); - - if filtered.len() == self.len() { - return None; - } - - Some(Self(filtered)) - } - - /// Returns `true` if the [`ImplicitImports`] resolves all the symbols requested by a - /// module descriptor. - pub(crate) fn resolves_namespace_package(&self, imported_symbols: &[String]) -> bool { - if !imported_symbols.is_empty() { - // TODO(charlie): Pyright uses: - // - // ```typescript - // !Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))` - // ``` - // - // However, that only checks if _any_ of the symbols are in the implicit imports. - for symbol in imported_symbols { - if !self.has(symbol) { - return false; - } - } - } else if self.is_empty() { - return false; - } - true - } - - /// Returns `true` if the module is present in the namespace package. - pub(crate) fn has(&self, name: &str) -> bool { - self.0.contains_key(name) - } - - /// Returns the number of implicit imports in the namespace package. - pub(crate) fn len(&self) -> usize { - self.0.len() - } - - /// Returns `true` if there are no implicit imports in the namespace package. - pub(crate) fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Returns an iterator over the implicit imports in the namespace package. - pub(crate) fn iter(&self) -> impl Iterator { - self.0.iter() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ImplicitImport { - /// Whether the implicit import is a stub file. - pub(crate) is_stub_file: bool, - - /// Whether the implicit import is a native module. - pub(crate) is_native_lib: bool, - - /// The path to the implicit import. - pub(crate) path: PathBuf, - - /// The `py.typed` information for the implicit import, if any. - pub(crate) py_typed: Option, -} diff --git a/crates/ruff_python_resolver/src/import_result.rs b/crates/ruff_python_resolver/src/import_result.rs deleted file mode 100644 index ddeab06c85aa5..0000000000000 --- a/crates/ruff_python_resolver/src/import_result.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Interface that describes the output of the import resolver. - -use std::path::PathBuf; - -use crate::implicit_imports::ImplicitImports; -use crate::py_typed::PyTypedInfo; - -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(clippy::struct_excessive_bools)] -pub(crate) struct ImportResult { - /// Whether the import name was relative (e.g., ".foo"). - pub(crate) is_relative: bool, - - /// Whether the import was resolved to a file or module. - pub(crate) is_import_found: bool, - - /// The path was partially resolved, but the specific submodule - /// defining the import was not found. For example, `foo.bar` was - /// not found, but `foo` was found. - pub(crate) is_partly_resolved: bool, - - /// The import refers to a namespace package (i.e., a folder without - /// an `__init__.py[i]` file at the final level of resolution). By - /// convention, we insert empty `PathBuf` segments into the resolved - /// paths vector to indicate intermediary namespace packages. - pub(crate) is_namespace_package: bool, - - /// The final resolved directory contains an `__init__.py[i]` file. - pub(crate) is_init_file_present: bool, - - /// The import resolved to a stub (`.pyi`) file within a stub package. - pub(crate) is_stub_package: bool, - - /// The import resolved to a built-in, local, or third-party module. - pub(crate) import_type: ImportType, - - /// A vector of resolved absolute paths for each file in the module - /// name. Typically includes a sequence of `__init__.py` files, followed - /// by the Python file defining the import itself, though the exact - /// structure can vary. For example, namespace packages will be represented - /// by empty `PathBuf` segments in the vector. - /// - /// For example, resolving `import foo.bar` might yield `./foo/__init__.py` and `./foo/bar.py`, - /// or `./foo/__init__.py` and `./foo/bar/__init__.py`. - pub(crate) resolved_paths: Vec, - - /// The search path used to resolve the module. - pub(crate) search_path: Option, - - /// The resolved file is a type hint (i.e., a `.pyi` file), rather - /// than a Python (`.py`) file. - pub(crate) is_stub_file: bool, - - /// The resolved file is a native library. - pub(crate) is_native_lib: bool, - - /// The resolved file is a hint hint (i.e., a `.pyi` file) from - /// `typeshed` in the standard library. - pub(crate) is_stdlib_typeshed_file: bool, - - /// The resolved file is a hint hint (i.e., a `.pyi` file) from - /// `typeshed` in third-party stubs. - pub(crate) is_third_party_typeshed_file: bool, - - /// The resolved file is a type hint (i.e., a `.pyi` file) from - /// the configured typing directory. - pub(crate) is_local_typings_file: bool, - - /// A map from file to resolved path, for all implicitly imported - /// modules that are part of a namespace package. - pub(crate) implicit_imports: ImplicitImports, - - /// Any implicit imports whose symbols were explicitly imported (i.e., via - /// a `from x import y` statement). - pub(crate) filtered_implicit_imports: ImplicitImports, - - /// If the import resolved to a type hint (i.e., a `.pyi` file), then - /// a non-type-hint resolution will be stored here. - #[allow(clippy::struct_field_names)] - pub(crate) non_stub_import_result: Option>, - - /// Information extracted from the `py.typed` in the package used to - /// resolve the import, if any. - pub(crate) py_typed_info: Option, - - /// The directory of the package, if any. - pub(crate) package_directory: Option, -} - -impl ImportResult { - /// An import result that indicates that the import was not found. - pub(crate) fn not_found() -> Self { - Self { - is_relative: false, - is_import_found: false, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: false, - is_stub_package: false, - import_type: ImportType::Local, - resolved_paths: vec![], - search_path: None, - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports::default(), - filtered_implicit_imports: ImplicitImports::default(), - non_stub_import_result: None, - py_typed_info: None, - package_directory: None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ImportType { - BuiltIn, - ThirdParty, - Local, -} diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs deleted file mode 100644 index 3cebaecdb78ba..0000000000000 --- a/crates/ruff_python_resolver/src/lib.rs +++ /dev/null @@ -1,948 +0,0 @@ -#![allow(dead_code)] - -mod config; -mod execution_environment; -mod host; -mod implicit_imports; -mod import_result; -mod module_descriptor; -mod native_module; -mod py_typed; -mod python_platform; -mod python_version; -mod resolver; -mod search; - -#[cfg(test)] -mod tests { - use std::fs::{create_dir_all, File}; - use std::io::{self, Write}; - use std::path::{Path, PathBuf}; - - use log::debug; - use tempfile::TempDir; - - use crate::config::Config; - use crate::execution_environment::ExecutionEnvironment; - use crate::host; - use crate::import_result::{ImportResult, ImportType}; - use crate::module_descriptor::ImportModuleDescriptor; - use crate::python_platform::PythonPlatform; - use crate::python_version::PythonVersion; - use crate::resolver::resolve_import; - - /// Create a file at the given path with the given content. - fn create(path: PathBuf, content: &str) -> io::Result { - if let Some(parent) = path.parent() { - create_dir_all(parent)?; - } - let mut f = File::create(&path)?; - f.write_all(content.as_bytes())?; - f.sync_all()?; - - Ok(path) - } - - /// Create an empty file at the given path. - fn empty(path: PathBuf) -> io::Result { - create(path, "") - } - - /// Create a partial `py.typed` file at the given path. - fn partial(path: PathBuf) -> io::Result { - create(path, "partial\n") - } - - /// Create a `py.typed` file at the given path. - fn typed(path: PathBuf) -> io::Result { - create(path, "# typed") - } - - #[derive(Debug, Default)] - struct ResolverOptions { - extra_paths: Vec, - library: Option, - stub_path: Option, - typeshed_path: Option, - venv_path: Option, - venv: Option, - } - - fn resolve_options( - source_file: impl AsRef, - name: &str, - root: impl Into, - options: ResolverOptions, - ) -> ImportResult { - let ResolverOptions { - extra_paths, - library, - stub_path, - typeshed_path, - venv_path, - venv, - } = options; - - let execution_environment = ExecutionEnvironment { - root: root.into(), - python_version: PythonVersion::Py37, - python_platform: PythonPlatform::Darwin, - extra_paths, - }; - - let module_descriptor = ImportModuleDescriptor { - leading_dots: name.chars().take_while(|c| *c == '.').count(), - name_parts: name - .chars() - .skip_while(|c| *c == '.') - .collect::() - .split('.') - .map(std::string::ToString::to_string) - .collect(), - imported_symbols: Vec::new(), - }; - - let config = Config { - typeshed_path, - stub_path, - venv_path, - venv, - }; - - let host = host::StaticHost::new(if let Some(library) = library { - vec![library] - } else { - Vec::new() - }); - - resolve_import( - source_file.as_ref(), - &execution_environment, - &module_descriptor, - &config, - &host, - ) - } - - fn setup() { - env_logger::builder().is_test(true).try_init().ok(); - } - - macro_rules! assert_debug_snapshot_normalize_paths { - ($value: ident) => {{ - // The debug representation for the backslash are two backslashes (escaping) - let $value = std::format!("{:#?}", $value).replace("\\\\", "/"); - insta::assert_snapshot!($value); - }}; - } - - #[test] - fn partial_stub_file_exists() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - let partial_stub_pyi = empty(library.join("myLib-stubs").join("partialStub.pyi"))?; - let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; - - let result = resolve_options( - partial_stub_py, - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.import_type, ImportType::ThirdParty); - assert_eq!( - result.resolved_paths, - // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', 'partialStub.pyi'` here. - // But that file doesn't exist. There's some kind of transform. - vec![PathBuf::new(), partial_stub_pyi] - ); - - Ok(()) - } - - #[test] - fn partial_stub_init_exists() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; - let partial_stub_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - partial_stub_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.import_type, ImportType::ThirdParty); - assert_eq!( - result.resolved_paths, - // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', '__init__.pyi'` here. - // But that file doesn't exist. There's some kind of transform. - vec![partial_stub_init_pyi] - ); - - Ok(()) - } - - #[test] - fn side_by_side_files() -> io::Result<()> { - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - empty(library.join("myLib/partialStub.pyi"))?; - empty(library.join("myLib/partialStub.py"))?; - empty(library.join("myLib/partialStub2.py"))?; - let my_file = empty(root.join("myFile.py"))?; - let side_by_side_stub_file = empty(library.join("myLib-stubs/partialStub.pyi"))?; - let partial_stub_file = empty(library.join("myLib-stubs/partialStub2.pyi"))?; - - // Stub package wins over original package (per PEP 561 rules). - let side_by_side_result = resolve_options( - &my_file, - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library.clone()), - ..Default::default() - }, - ); - assert!(side_by_side_result.is_import_found); - assert!(side_by_side_result.is_stub_file); - assert_eq!( - side_by_side_result.resolved_paths, - vec![PathBuf::new(), side_by_side_stub_file] - ); - - // Side by side stub doesn't completely disable partial stub. - let partial_stub_result = resolve_options( - &my_file, - "myLib.partialStub2", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - assert!(partial_stub_result.is_import_found); - assert!(partial_stub_result.is_stub_file); - assert_eq!( - partial_stub_result.resolved_paths, - vec![PathBuf::new(), partial_stub_file] - ); - - Ok(()) - } - - #[test] - fn stub_package() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib-stubs/stub.pyi"))?; - empty(library.join("myLib-stubs/__init__.pyi"))?; - let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; - - let result = resolve_options( - partial_stub_py, - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - // If fully typed stub package exists, that wins over the real package. - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn stub_namespace_package() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib-stubs/stub.pyi"))?; - let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; - - let result = resolve_options( - partial_stub_py.clone(), - "myLib.partialStub", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - // If fully typed stub package exists, that wins over the real package. - assert!(result.is_import_found); - assert!(!result.is_stub_file); - assert_eq!(result.resolved_paths, vec![PathBuf::new(), partial_stub_py]); - - Ok(()) - } - - #[test] - fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typing_folder = root.join("typing"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(library.join("myLib-stubs/py.typed"))?; - empty(library.join("myLib-stubs/__init__.pyi"))?; - let my_lib_pyi = empty(typing_folder.join("myLib.pyi"))?; - let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - my_lib_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - stub_path: Some(typing_folder), - ..Default::default() - }, - ); - - // If the package exists in typing folder, that gets picked up first (so we resolve to - // `myLib.pyi`). - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![my_lib_pyi]); - - Ok(()) - } - - #[test] - fn partial_stub_package_in_typing_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typing_folder = root.join("typing"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - partial(typing_folder.join("myLib-stubs/py.typed"))?; - let my_lib_stubs_init_pyi = empty(typing_folder.join("myLib-stubs/__init__.pyi"))?; - let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - my_lib_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - stub_path: Some(typing_folder), - ..Default::default() - }, - ); - - // If the package exists in typing folder, that gets picked up first (so we resolve to - // `myLib.pyi`). - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); - - Ok(()) - } - - #[test] - fn typeshed_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typeshed_folder = root.join("ts"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(typeshed_folder.join("stubs/myLibPackage/myLib.pyi"))?; - partial(library.join("myLib-stubs/py.typed"))?; - let my_lib_stubs_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; - let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; - - let result = resolve_options( - my_lib_init_py, - "myLib", - root, - ResolverOptions { - library: Some(library), - typeshed_path: Some(typeshed_folder), - ..Default::default() - }, - ); - - // Stub packages win over typeshed. - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); - - Ok(()) - } - - #[test] - fn py_typed_file() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib/__init__.py"))?; - partial(library.join("myLib-stubs/py.typed"))?; - let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; - let package_py_typed = typed(library.join("myLib/py.typed"))?; - - let result = resolve_options( - package_py_typed, - "myLib", - root, - ResolverOptions { - library: Some(library), - ..Default::default() - }, - ); - - // Partial stub package always overrides original package. - assert!(result.is_import_found); - assert!(result.is_stub_file); - assert_eq!(result.resolved_paths, vec![partial_stub_init_pyi]); - - Ok(()) - } - - #[test] - fn py_typed_library() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typeshed_folder = root.join("ts"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - typed(library.join("os/py.typed"))?; - let init_py = empty(library.join("os/__init__.py"))?; - let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; - - let result = resolve_options( - typeshed_init_pyi, - "os", - root, - ResolverOptions { - library: Some(library), - typeshed_path: Some(typeshed_folder), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.resolved_paths, vec![init_py]); - - Ok(()) - } - - #[test] - fn non_py_typed_library() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - let typeshed_folder = root.join("ts"); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("os/__init__.py"))?; - let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; - - let result = resolve_options( - typeshed_init_pyi.clone(), - "os", - root, - ResolverOptions { - library: Some(library), - typeshed_path: Some(typeshed_folder), - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::ThirdParty); - assert_eq!(result.resolved_paths, vec![typeshed_init_pyi]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_root() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file1 = empty(root.join("file1.py"))?; - let file2 = empty(root.join("file2.py"))?; - - let result = resolve_options(file2, "file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![file1]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_sub_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let test_init = empty(root.join("test/__init__.py"))?; - let test_file1 = empty(root.join("test/file1.py"))?; - let test_file2 = empty(root.join("test/file2.py"))?; - - let result = resolve_options(test_file2, "test.file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![test_init, test_file1]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let nested_init = empty(root.join("src/nested/__init__.py"))?; - let nested_file1 = empty(root.join("src/nested/file1.py"))?; - let nested_file2 = empty(root.join("src/nested/file2.py"))?; - - let result = resolve_options( - nested_file2, - "nested.file1", - root, - ResolverOptions::default(), - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![nested_init, nested_file1]); - - Ok(()) - } - - #[test] - fn import_file_sub_under_containing_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let nested_file1 = empty(root.join("src/nested/file1.py"))?; - let nested_file2 = empty(root.join("src/nested/nested2/file2.py"))?; - - let result = resolve_options(nested_file2, "file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![nested_file1]); - - Ok(()) - } - - #[test] - fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let temp_dir = TempDir::new()?; - let library = temp_dir.path().join("lib").join("site-packages"); - - empty(library.join("myLib/file1.py"))?; - let file2 = empty(library.join("myLib/file2.py"))?; - - let result = resolve_options(file2, "file1", root, ResolverOptions::default()); - - debug!("result: {result:?}"); - - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn nested_namespace_package_1() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file = empty(root.join("package1/a/b/c/d.py"))?; - let package1_init = empty(root.join("package1/a/__init__.py"))?; - let package2_init = empty(root.join("package2/a/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_init, - "a.b.c.d", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!( - result.resolved_paths, - vec![package1_init, PathBuf::new(), PathBuf::new(), file] - ); - - Ok(()) - } - - #[test] - fn nested_namespace_package_2() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file = empty(root.join("package1/a/b/c/d.py"))?; - let package1_init = empty(root.join("package1/a/b/c/__init__.py"))?; - let package2_init = empty(root.join("package2/a/b/c/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_init, - "a.b.c.d", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!( - result.resolved_paths, - vec![PathBuf::new(), PathBuf::new(), package1_init, file] - ); - - Ok(()) - } - - #[test] - fn nested_namespace_package_3() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - empty(root.join("package1/a/b/c/d.py"))?; - let package2_init = empty(root.join("package2/a/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_init, - "a.b.c.d", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn nested_namespace_package_4() -> io::Result<()> { - // See: https://github.com/microsoft/pyright/issues/5089. - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - empty(root.join("package1/a/b/__init__.py"))?; - empty(root.join("package1/a/b/c.py"))?; - empty(root.join("package2/a/__init__.py"))?; - let package2_a_b_init = empty(root.join("package2/a/b/__init__.py"))?; - - let package1 = root.join("package1"); - let package2 = root.join("package2"); - - let result = resolve_options( - package2_a_b_init, - "a.b.c", - root, - ResolverOptions { - extra_paths: vec![package1, package2], - ..Default::default() - }, - ); - - assert!(!result.is_import_found); - - Ok(()) - } - - // New tests, don't exist upstream. - #[test] - fn relative_import_side_by_side_file_root() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - let file1 = empty(root.join("file1.py"))?; - let file2 = empty(root.join("file2.py"))?; - - let result = resolve_options(file2, ".file1", root, ResolverOptions::default()); - - assert!(result.is_import_found); - assert_eq!(result.import_type, ImportType::Local); - assert_eq!(result.resolved_paths, vec![file1]); - - Ok(()) - } - - #[test] - fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> { - setup(); - - let temp_dir = TempDir::new()?; - let root = temp_dir.path(); - - empty(root.join("file1.py"))?; - let file2 = empty(root.join("file2.py"))?; - - let result = resolve_options(file2, "..file1", root, ResolverOptions::default()); - - assert!(!result.is_import_found); - - Ok(()) - } - - #[test] - fn airflow_standard_library() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "os", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } - - #[test] - fn airflow_first_party() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "airflow.jobs.scheduler_job_runner", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } - - #[test] - fn airflow_stub_file() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "airflow.compat.functools", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } - - #[test] - fn airflow_namespace_package() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "airflow.providers.google.cloud.hooks.gcs", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } - - #[test] - fn airflow_third_party() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "sqlalchemy.orm", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } - - #[test] - fn airflow_explicit_native_module() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "_watchdog_fsevents", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } - - #[test] - fn airflow_implicit_native_module() { - setup(); - - let root = PathBuf::from("./resources/test/airflow"); - let source_file = root.join("airflow/api/common/mark_tasks.py"); - - let result = resolve_options( - source_file, - "orjson", - root.clone(), - ResolverOptions { - venv_path: Some(root), - venv: Some(PathBuf::from("venv")), - ..Default::default() - }, - ); - - assert_debug_snapshot_normalize_paths!(result); - } -} diff --git a/crates/ruff_python_resolver/src/module_descriptor.rs b/crates/ruff_python_resolver/src/module_descriptor.rs deleted file mode 100644 index 7d71efafbc057..0000000000000 --- a/crates/ruff_python_resolver/src/module_descriptor.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ImportModuleDescriptor { - pub(crate) leading_dots: usize, - pub(crate) name_parts: Vec, - pub(crate) imported_symbols: Vec, -} - -impl ImportModuleDescriptor { - pub(crate) fn name(&self) -> String { - format!( - "{}{}", - ".".repeat(self.leading_dots), - &self.name_parts.join(".") - ) - } -} diff --git a/crates/ruff_python_resolver/src/native_module.rs b/crates/ruff_python_resolver/src/native_module.rs deleted file mode 100644 index 6cb0b97efce65..0000000000000 --- a/crates/ruff_python_resolver/src/native_module.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Support for native Python extension modules. - -use std::ffi::OsStr; -use std::io; -use std::path::{Path, PathBuf}; - -/// Returns `true` if the given file extension is that of a native module. -pub(crate) fn is_native_module_file_extension(file_extension: &OsStr) -> bool { - file_extension == "so" || file_extension == "pyd" || file_extension == "dylib" -} - -/// Given a file name, returns the name of the native module it represents. -/// -/// For example, given `foo.abi3.so`, return `foo`. -pub(crate) fn native_module_name(file_name: &Path) -> Option<&str> { - file_name - .file_stem() - .and_then(OsStr::to_str) - .map(|file_stem| { - file_stem - .split_once('.') - .map_or(file_stem, |(file_stem, _)| file_stem) - }) -} - -/// Returns `true` if the given file name is that of a native module with the given name. -pub(crate) fn is_native_module_file_name(module_name: &str, file_name: &Path) -> bool { - // The file name must be that of a native module. - if !file_name - .extension() - .is_some_and(is_native_module_file_extension) - { - return false; - } - - // The file must represent the module name. - native_module_name(file_name) == Some(module_name) -} - -/// Find the native module within the namespace package at the given path. -pub(crate) fn find_native_module( - module_name: &str, - dir_path: &Path, -) -> io::Result> { - Ok(dir_path - .read_dir()? - .flatten() - .filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file())) - .map(|entry| entry.path()) - .find(|path| is_native_module_file_name(module_name, path))) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - #[test] - fn module_name() { - assert_eq!( - super::native_module_name(&PathBuf::from("foo.so")), - Some("foo") - ); - - assert_eq!( - super::native_module_name(&PathBuf::from("foo.abi3.so")), - Some("foo") - ); - - assert_eq!( - super::native_module_name(&PathBuf::from("foo.cpython-38-x86_64-linux-gnu.so")), - Some("foo") - ); - - assert_eq!( - super::native_module_name(&PathBuf::from("foo.cp39-win_amd64.pyd")), - Some("foo") - ); - } - - #[test] - fn module_file_extension() { - assert!(super::is_native_module_file_extension("so".as_ref())); - assert!(super::is_native_module_file_extension("pyd".as_ref())); - assert!(super::is_native_module_file_extension("dylib".as_ref())); - assert!(!super::is_native_module_file_extension("py".as_ref())); - } -} diff --git a/crates/ruff_python_resolver/src/py_typed.rs b/crates/ruff_python_resolver/src/py_typed.rs deleted file mode 100644 index 258f801eed066..0000000000000 --- a/crates/ruff_python_resolver/src/py_typed.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Support for [PEP 561] (`py.typed` files). -//! -//! [PEP 561]: https://peps.python.org/pep-0561/ - -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct PyTypedInfo { - /// The path to the `py.typed` file. - py_typed_path: PathBuf, - - /// Whether the package is partially typed (as opposed to fully typed). - is_partially_typed: bool, -} - -/// Returns the `py.typed` information for the given directory, if any. -pub(crate) fn get_py_typed_info(dir_path: &Path) -> Option { - let py_typed_path = dir_path.join("py.typed"); - if py_typed_path.is_file() { - // Do a quick sanity check on the size before we attempt to read it. This - // file should always be really small - typically zero bytes in length. - let file_len = py_typed_path.metadata().ok()?.len(); - if file_len < 64 * 1024 { - // PEP 561 doesn't specify the format of "py.typed" in any detail other than - // to say that "If a stub package is partial it MUST include partial\n in a top - // level py.typed file." - let contents = std::fs::read_to_string(&py_typed_path).ok()?; - let is_partially_typed = - contents.contains("partial\n") || contents.contains("partial\r\n"); - Some(PyTypedInfo { - py_typed_path, - is_partially_typed, - }) - } else { - None - } - } else { - None - } -} diff --git a/crates/ruff_python_resolver/src/python_platform.rs b/crates/ruff_python_resolver/src/python_platform.rs deleted file mode 100644 index b82ebe256c1cc..0000000000000 --- a/crates/ruff_python_resolver/src/python_platform.rs +++ /dev/null @@ -1,20 +0,0 @@ -/// Enum to represent a Python platform. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) enum PythonPlatform { - Darwin, - Linux, - Windows, -} - -impl PythonPlatform { - /// Returns the platform-specific library names. These are the candidate names for the top-level - /// subdirectory within a virtual environment that contains the `site-packages` directory - /// (with a `pythonX.Y` directory in-between). - pub(crate) fn lib_names(&self) -> &[&'static str] { - match self { - PythonPlatform::Darwin => &["lib"], - PythonPlatform::Linux => &["lib", "lib64"], - PythonPlatform::Windows => &["Lib"], - } - } -} diff --git a/crates/ruff_python_resolver/src/python_version.rs b/crates/ruff_python_resolver/src/python_version.rs deleted file mode 100644 index aeb2a76b75cf2..0000000000000 --- a/crates/ruff_python_resolver/src/python_version.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Enum to represent a Python version. -#[derive(Debug, Copy, Clone)] -pub(crate) enum PythonVersion { - Py37, - Py38, - Py39, - Py310, - Py311, - Py312, -} - -impl PythonVersion { - /// The directory name (e.g., in a virtual environment) for this Python version. - pub(crate) fn dir(self) -> &'static str { - match self { - PythonVersion::Py37 => "python3.7", - PythonVersion::Py38 => "python3.8", - PythonVersion::Py39 => "python3.9", - PythonVersion::Py310 => "python3.10", - PythonVersion::Py311 => "python3.11", - PythonVersion::Py312 => "python3.12", - } - } -} diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs deleted file mode 100644 index 22228d349620e..0000000000000 --- a/crates/ruff_python_resolver/src/resolver.rs +++ /dev/null @@ -1,744 +0,0 @@ -//! Resolves Python imports to their corresponding files on disk. - -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; - -use log::debug; - -use crate::config::Config; -use crate::execution_environment::ExecutionEnvironment; -use crate::implicit_imports::ImplicitImports; -use crate::import_result::{ImportResult, ImportType}; -use crate::module_descriptor::ImportModuleDescriptor; -use crate::{host, native_module, py_typed, search}; - -#[allow(clippy::fn_params_excessive_bools)] -fn resolve_module_descriptor( - root: &Path, - module_descriptor: &ImportModuleDescriptor, - allow_partial: bool, - allow_native_lib: bool, - use_stub_package: bool, - allow_pyi: bool, - look_for_py_typed: bool, -) -> ImportResult { - if use_stub_package { - debug!("Attempting to resolve stub package using root path: {root:?}"); - } else { - debug!("Attempting to resolve using root path: {root:?}"); - } - - // Starting at the specified path, walk the file system to find the specified module. - let mut resolved_paths: Vec = Vec::new(); - let mut dir_path = root.to_path_buf(); - let mut is_namespace_package = false; - let mut is_init_file_present = false; - let mut is_stub_package = false; - let mut is_stub_file = false; - let mut is_native_lib = false; - let mut implicit_imports = None; - let mut package_directory = None; - let mut py_typed_info = None; - - // Ex) `from . import foo` - if module_descriptor.name_parts.is_empty() { - let py_file_path = dir_path.join("__init__.py"); - let pyi_file_path = dir_path.join("__init__.pyi"); - - if allow_pyi && pyi_file_path.is_file() { - debug!("Resolved import with file: {pyi_file_path:?}"); - resolved_paths.push(pyi_file_path.clone()); - } else if py_file_path.is_file() { - debug!("Resolved import with file: {py_file_path:?}"); - resolved_paths.push(py_file_path.clone()); - } else { - debug!("Partially resolved import with directory: {dir_path:?}"); - - // Add an empty path to indicate that the import is partially resolved. - resolved_paths.push(PathBuf::new()); - is_namespace_package = true; - } - - implicit_imports = ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok(); - } else { - for (i, part) in module_descriptor.name_parts.iter().enumerate() { - let is_first_part = i == 0; - let is_last_part = i == module_descriptor.name_parts.len() - 1; - - // Extend the directory path with the next segment. - let module_dir_path = if use_stub_package && is_first_part { - is_stub_package = true; - dir_path.join(format!("{part}-stubs")) - } else { - dir_path.join(part) - }; - - let found_directory = module_dir_path.is_dir(); - if found_directory { - if is_first_part { - package_directory = Some(module_dir_path.clone()); - } - - // Look for an `__init__.py[i]` in the directory. - let py_file_path = module_dir_path.join("__init__.py"); - let pyi_file_path = module_dir_path.join("__init__.pyi"); - is_init_file_present = false; - - if allow_pyi && pyi_file_path.is_file() { - debug!("Resolved import with file: {pyi_file_path:?}"); - resolved_paths.push(pyi_file_path.clone()); - if is_last_part { - is_stub_file = true; - } - is_init_file_present = true; - } else if py_file_path.is_file() { - debug!("Resolved import with file: {py_file_path:?}"); - resolved_paths.push(py_file_path.clone()); - is_init_file_present = true; - } - - if look_for_py_typed { - py_typed_info = - py_typed_info.or_else(|| py_typed::get_py_typed_info(&module_dir_path)); - } - - // We haven't reached the end of the import, and we found a matching directory. - // Proceed to the next segment. - if !is_last_part { - if !is_init_file_present { - resolved_paths.push(PathBuf::new()); - is_namespace_package = true; - py_typed_info = None; - } - - dir_path = module_dir_path; - continue; - } - - if is_init_file_present { - implicit_imports = - ImplicitImports::find(&module_dir_path, &[&py_file_path, &pyi_file_path]) - .ok(); - break; - } - } - - // We couldn't find a matching directory, or the directory didn't contain an - // `__init__.py[i]` file. Look for an `.py[i]` file with the same name as the - // segment, in lieu of a directory. - let py_file_path = module_dir_path.with_extension("py"); - let pyi_file_path = module_dir_path.with_extension("pyi"); - - if allow_pyi && pyi_file_path.is_file() { - debug!("Resolved import with file: {pyi_file_path:?}"); - resolved_paths.push(pyi_file_path); - if is_last_part { - is_stub_file = true; - } - } else if py_file_path.is_file() { - debug!("Resolved import with file: {py_file_path:?}"); - resolved_paths.push(py_file_path); - } else { - if allow_native_lib && dir_path.is_dir() { - // We couldn't find a `.py[i]` file; search for a native library. - if let Some(module_name) = module_dir_path.file_name().and_then(OsStr::to_str) { - if let Ok(Some(native_lib_path)) = - native_module::find_native_module(module_name, &dir_path) - { - debug!("Resolved import with file: {native_lib_path:?}"); - is_native_lib = true; - resolved_paths.push(native_lib_path); - } - } - } - - if !is_native_lib && found_directory { - debug!("Partially resolved import with directory: {dir_path:?}"); - resolved_paths.push(PathBuf::new()); - if is_last_part { - implicit_imports = - ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok(); - is_namespace_package = true; - } - } - } - - break; - } - } - - let import_found = if allow_partial { - !resolved_paths.is_empty() - } else { - resolved_paths.len() == module_descriptor.name_parts.len() - }; - - let is_partly_resolved = if resolved_paths.is_empty() { - false - } else { - resolved_paths.len() < module_descriptor.name_parts.len() - }; - - ImportResult { - is_relative: false, - is_import_found: import_found, - is_partly_resolved, - is_namespace_package, - is_init_file_present, - is_stub_package, - import_type: ImportType::Local, - resolved_paths, - search_path: Some(root.into()), - is_stub_file, - is_native_lib, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: implicit_imports.unwrap_or_default(), - filtered_implicit_imports: ImplicitImports::default(), - non_stub_import_result: None, - py_typed_info, - package_directory, - } -} - -/// Resolve an absolute module import based on the import resolution algorithm -/// defined in [PEP 420]. -/// -/// [PEP 420]: https://peps.python.org/pep-0420/ -#[allow(clippy::fn_params_excessive_bools)] -fn resolve_absolute_import( - root: &Path, - module_descriptor: &ImportModuleDescriptor, - allow_partial: bool, - allow_native_lib: bool, - use_stub_package: bool, - allow_pyi: bool, - look_for_py_typed: bool, -) -> ImportResult { - if allow_pyi && use_stub_package { - // Search for packaged stubs first. PEP 561 indicates that package authors can ship - // stubs separately from the package implementation by appending `-stubs` to its - // top-level directory name. - let import_result = resolve_module_descriptor( - root, - module_descriptor, - allow_partial, - false, - true, - true, - true, - ); - - if import_result.package_directory.is_some() { - // If this is a namespace package that wasn't resolved, assume that - // it's a partial stub package and continue looking for a real package. - if !import_result.is_namespace_package || import_result.is_import_found { - return import_result; - } - } - } - - // Search for a "real" package. - resolve_module_descriptor( - root, - module_descriptor, - allow_partial, - allow_native_lib, - false, - allow_pyi, - look_for_py_typed, - ) -} - -/// Resolve an absolute module import based on the import resolution algorithm, -/// taking into account the various competing files to which the import could -/// resolve. -/// -/// For example, prefers local imports over third-party imports, and stubs over -/// non-stubs. -fn resolve_best_absolute_import( - execution_environment: &ExecutionEnvironment, - module_descriptor: &ImportModuleDescriptor, - allow_pyi: bool, - config: &Config, - host: &Host, -) -> Option { - let import_name = module_descriptor.name(); - - // Search for local stub files (using `stub_path`). - if allow_pyi { - if let Some(stub_path) = config.stub_path.as_ref() { - debug!("Looking in stub path: {}", stub_path.display()); - - let mut typings_import = resolve_absolute_import( - stub_path, - module_descriptor, - false, - false, - true, - allow_pyi, - false, - ); - - if typings_import.is_import_found { - // Treat stub files as "local". - typings_import.import_type = ImportType::Local; - typings_import.is_local_typings_file = true; - - // If we resolved to a namespace package, ensure that all imported symbols are - // present in the namespace package's "implicit" imports. - if typings_import.is_namespace_package - && typings_import - .resolved_paths - .last() - .is_some_and(|path| path.as_os_str().is_empty()) - { - if typings_import - .implicit_imports - .resolves_namespace_package(&module_descriptor.imported_symbols) - { - return Some(typings_import); - } - } else { - return Some(typings_import); - } - } - - return None; - } - } - - // Look in the root directory of the execution environment. - debug!( - "Looking in root directory of execution environment: {}", - execution_environment.root.display() - ); - - let mut local_import = resolve_absolute_import( - &execution_environment.root, - module_descriptor, - false, - true, - true, - allow_pyi, - false, - ); - local_import.import_type = ImportType::Local; - - let mut best_result_so_far = Some(local_import); - - // Look in any extra paths. - for extra_path in &execution_environment.extra_paths { - debug!("Looking in extra path: {}", extra_path.display()); - - let mut local_import = resolve_absolute_import( - extra_path, - module_descriptor, - false, - true, - true, - allow_pyi, - false, - ); - local_import.import_type = ImportType::Local; - - best_result_so_far = Some(pick_best_import( - best_result_so_far, - local_import, - module_descriptor, - )); - } - - // Look for third-party imports in Python's `sys` path. - for search_path in search::python_search_paths(config, host) { - debug!("Looking in Python search path: {}", search_path.display()); - - let mut third_party_import = resolve_absolute_import( - &search_path, - module_descriptor, - false, - true, - true, - allow_pyi, - true, - ); - third_party_import.import_type = ImportType::ThirdParty; - - best_result_so_far = Some(pick_best_import( - best_result_so_far, - third_party_import, - module_descriptor, - )); - } - - // If a library is fully `py.typed`, prefer the current result. There's one exception: - // we're executing from `typeshed` itself. In that case, use the `typeshed` lookup below, - // rather than favoring `py.typed` libraries. - if let Some(typeshed_root) = search::typeshed_root(config, host) { - debug!( - "Looking in typeshed root directory: {}", - typeshed_root.display() - ); - if typeshed_root != execution_environment.root { - if best_result_so_far - .as_ref() - .is_some_and(|result| result.py_typed_info.is_some() && !result.is_partly_resolved) - { - return best_result_so_far; - } - } - } - - if allow_pyi && !module_descriptor.name_parts.is_empty() { - // Check for a stdlib typeshed file. - debug!("Looking for typeshed stdlib path: {import_name}"); - if let Some(mut typeshed_stdilib_import) = - find_typeshed_path(module_descriptor, true, config, host) - { - typeshed_stdilib_import.is_stdlib_typeshed_file = true; - return Some(typeshed_stdilib_import); - } - - // Check for a third-party typeshed file. - debug!("Looking for typeshed third-party path: {import_name}"); - if let Some(mut typeshed_third_party_import) = - find_typeshed_path(module_descriptor, false, config, host) - { - typeshed_third_party_import.is_third_party_typeshed_file = true; - - best_result_so_far = Some(pick_best_import( - best_result_so_far, - typeshed_third_party_import, - module_descriptor, - )); - } - } - - // We weren't able to find an exact match, so return the best - // partial match. - best_result_so_far -} - -/// Finds the `typeshed` path for the given module descriptor. -/// -/// Supports both standard library and third-party `typeshed` lookups. -fn find_typeshed_path( - module_descriptor: &ImportModuleDescriptor, - is_std_lib: bool, - config: &Config, - host: &Host, -) -> Option { - if is_std_lib { - debug!("Looking for typeshed `stdlib` path"); - } else { - debug!("Looking for typeshed `stubs` path"); - } - - let mut typeshed_paths = vec![]; - - if is_std_lib { - if let Some(path) = search::stdlib_typeshed_path(config, host) { - typeshed_paths.push(path); - } - } else { - if let Some(paths) = - search::third_party_typeshed_package_paths(module_descriptor, config, host) - { - typeshed_paths.extend(paths); - } - } - - for typeshed_path in typeshed_paths { - if typeshed_path.is_dir() { - let mut import_info = resolve_absolute_import( - &typeshed_path, - module_descriptor, - false, - false, - false, - true, - false, - ); - if import_info.is_import_found { - import_info.import_type = if is_std_lib { - ImportType::BuiltIn - } else { - ImportType::ThirdParty - }; - return Some(import_info); - } - } - } - - debug!("Typeshed path not found"); - None -} - -/// Given a current "best" import and a newly discovered result, returns the -/// preferred result. -fn pick_best_import( - best_import_so_far: Option, - new_import: ImportResult, - module_descriptor: &ImportModuleDescriptor, -) -> ImportResult { - let Some(best_import_so_far) = best_import_so_far else { - return new_import; - }; - - if new_import.is_import_found { - // Prefer traditional over namespace packages. - let so_far_index = best_import_so_far - .resolved_paths - .iter() - .position(|path| !path.as_os_str().is_empty()); - let new_index = new_import - .resolved_paths - .iter() - .position(|path| !path.as_os_str().is_empty()); - if so_far_index != new_index { - match (so_far_index, new_index) { - (None, Some(_)) => return new_import, - (Some(_), None) => return best_import_so_far, - (Some(so_far_index), Some(new_index)) => { - return if so_far_index < new_index { - best_import_so_far - } else { - new_import - } - } - _ => {} - } - } - - // Prefer "found" over "not found". - if !best_import_so_far.is_import_found { - return new_import; - } - - // If both results are namespace imports, prefer the result that resolves all - // imported symbols. - if best_import_so_far.is_namespace_package && new_import.is_namespace_package { - if !module_descriptor.imported_symbols.is_empty() { - if !best_import_so_far - .implicit_imports - .resolves_namespace_package(&module_descriptor.imported_symbols) - { - if new_import - .implicit_imports - .resolves_namespace_package(&module_descriptor.imported_symbols) - { - return new_import; - } - - // Prefer the namespace package that has an `__init__.py[i]` file present in the - // final directory over one that does not. - if best_import_so_far.is_init_file_present && !new_import.is_init_file_present { - return best_import_so_far; - } - if !best_import_so_far.is_init_file_present && new_import.is_init_file_present { - return new_import; - } - } - } - } - - // Prefer "py.typed" over "non-py.typed". - if best_import_so_far.py_typed_info.is_some() && new_import.py_typed_info.is_none() { - return best_import_so_far; - } - if best_import_so_far.py_typed_info.is_none() && best_import_so_far.py_typed_info.is_some() - { - return new_import; - } - - // Prefer stub files (`.pyi`) over non-stub files (`.py`). - if best_import_so_far.is_stub_file && !new_import.is_stub_file { - return best_import_so_far; - } - if !best_import_so_far.is_stub_file && new_import.is_stub_file { - return new_import; - } - - // If we're still tied, prefer a shorter resolution path. - if best_import_so_far.resolved_paths.len() > new_import.resolved_paths.len() { - return new_import; - } - } else if new_import.is_partly_resolved { - let so_far_index = best_import_so_far - .resolved_paths - .iter() - .position(|path| !path.as_os_str().is_empty()); - let new_index = new_import - .resolved_paths - .iter() - .position(|path| !path.as_os_str().is_empty()); - if so_far_index != new_index { - match (so_far_index, new_index) { - (None, Some(_)) => return new_import, - (Some(_), None) => return best_import_so_far, - (Some(so_far_index), Some(new_index)) => { - return if so_far_index < new_index { - best_import_so_far - } else { - new_import - } - } - _ => {} - } - } - } - - best_import_so_far -} - -/// Resolve a relative import. -fn resolve_relative_import( - source_file: &Path, - module_descriptor: &ImportModuleDescriptor, -) -> Option { - // Determine which search path this file is part of. - let mut directory = source_file; - for _ in 0..module_descriptor.leading_dots { - directory = directory.parent()?; - } - - // Now try to match the module parts from the current directory location. - let mut abs_import = resolve_absolute_import( - directory, - module_descriptor, - false, - true, - false, - true, - false, - ); - - if abs_import.is_stub_file { - // If we found a stub for a relative import, only search - // the same folder for the real module. Otherwise, it will - // error out on runtime. - abs_import.non_stub_import_result = Some(Box::new(resolve_absolute_import( - directory, - module_descriptor, - false, - true, - false, - false, - false, - ))); - } - - Some(abs_import) -} - -/// Resolve an absolute or relative import. -fn resolve_import_strict( - source_file: &Path, - execution_environment: &ExecutionEnvironment, - module_descriptor: &ImportModuleDescriptor, - config: &Config, - host: &Host, -) -> ImportResult { - let import_name = module_descriptor.name(); - - if module_descriptor.leading_dots > 0 { - debug!("Resolving relative import for: {import_name}"); - - let relative_import = resolve_relative_import(source_file, module_descriptor); - - if let Some(mut relative_import) = relative_import { - relative_import.is_relative = true; - return relative_import; - } - } else { - debug!("Resolving best absolute import for: {import_name}"); - - let best_import = resolve_best_absolute_import( - execution_environment, - module_descriptor, - true, - config, - host, - ); - - if let Some(mut best_import) = best_import { - if best_import.is_stub_file { - debug!("Resolving best non-stub absolute import for: {import_name}"); - - best_import.non_stub_import_result = Some(Box::new( - resolve_best_absolute_import( - execution_environment, - module_descriptor, - false, - config, - host, - ) - .unwrap_or_else(ImportResult::not_found), - )); - } - return best_import; - } - } - - ImportResult::not_found() -} - -/// Resolves an import, given the current file and the import descriptor. -/// -/// The algorithm is as follows: -/// -/// 1. If the import is relative, convert it to an absolute import. -/// 2. Find the "best" match for the import, allowing stub files. Search local imports, any -/// configured search paths, the Python path, the typeshed path, etc. -/// 3. If a stub file was found, find the "best" match for the import, disallowing stub files. -/// 4. If the import wasn't resolved, try to resolve it in the parent directory, then the parent's -/// parent, and so on, until the import root is reached. -pub(crate) fn resolve_import( - source_file: &Path, - execution_environment: &ExecutionEnvironment, - module_descriptor: &ImportModuleDescriptor, - config: &Config, - host: &Host, -) -> ImportResult { - let import_result = resolve_import_strict( - source_file, - execution_environment, - module_descriptor, - config, - host, - ); - if import_result.is_import_found || module_descriptor.leading_dots > 0 { - return import_result; - } - - // If we weren't able to resolve an absolute import, try resolving it in the - // importing file's directory, then the parent directory, and so on, until the - // import root is reached. - let root = execution_environment.root.as_path(); - let mut current = source_file; - while let Some(parent) = current.parent() { - if !parent.starts_with(root) { - break; - } - - debug!("Resolving absolute import in parent: {}", parent.display()); - - let mut result = - resolve_absolute_import(parent, module_descriptor, false, false, false, true, false); - - if result.is_import_found { - if let Some(implicit_imports) = result - .implicit_imports - .filter(&module_descriptor.imported_symbols) - { - result.implicit_imports = implicit_imports; - } - return result; - } - - current = parent; - } - - ImportResult::not_found() -} diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs deleted file mode 100644 index 5c35fa7b582f5..0000000000000 --- a/crates/ruff_python_resolver/src/search.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! Determine the appropriate search paths for the Python environment. - -use std::collections::HashMap; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::{fs, io}; - -use log::debug; - -use crate::config::Config; -use crate::host; -use crate::module_descriptor::ImportModuleDescriptor; -use crate::python_version::PythonVersion; - -const SITE_PACKAGES: &str = "site-packages"; - -/// Find the `site-packages` directory for the specified Python version. -fn find_site_packages_path( - lib_path: &Path, - python_version: Option, -) -> Option { - if lib_path.is_dir() { - debug!( - "Found path `{}`; looking for site-packages", - lib_path.display() - ); - } else { - debug!("Did not find `{}`", lib_path.display()); - } - - let site_packages_path = lib_path.join(SITE_PACKAGES); - if site_packages_path.is_dir() { - debug!("Found path `{}`", site_packages_path.display()); - return Some(site_packages_path); - } - - debug!( - "Did not find `{}`, so looking for Python subdirectory", - site_packages_path.display() - ); - - // There's no `site-packages` directory in the library directory; look for a `python3.X` - // directory instead. - let candidate_dirs: Vec = fs::read_dir(lib_path) - .ok()? - .filter_map(|entry| { - let entry = entry.ok()?; - let metadata = entry.metadata().ok()?; - - if metadata.file_type().is_dir() { - let dir_path = entry.path(); - if dir_path - .file_name() - .and_then(OsStr::to_str)? - .starts_with("python3.") - { - if dir_path.join(SITE_PACKAGES).is_dir() { - return Some(dir_path); - } - } - } else if metadata.file_type().is_symlink() { - let symlink_path = fs::read_link(entry.path()).ok()?; - if symlink_path - .file_name() - .and_then(OsStr::to_str)? - .starts_with("python3.") - { - if symlink_path.join(SITE_PACKAGES).is_dir() { - return Some(symlink_path); - } - } - } - - None - }) - .collect(); - - // If a `python3.X` directory does exist (and `3.X` matches the current Python version), - // prefer it over any other Python directories. - if let Some(python_version) = python_version { - if let Some(preferred_dir) = candidate_dirs.iter().find(|dir| { - dir.file_name() - .and_then(OsStr::to_str) - .is_some_and(|name| name == python_version.dir()) - }) { - debug!("Found path `{}`", preferred_dir.display()); - return Some(preferred_dir.join(SITE_PACKAGES)); - } - } - - // Fallback to the first `python3.X` directory that we found. - let default_dir = candidate_dirs.first()?; - debug!("Found path `{}`", default_dir.display()); - Some(default_dir.join(SITE_PACKAGES)) -} - -fn find_paths_from_pth_files(parent_dir: &Path) -> io::Result + '_> { - Ok(parent_dir - .read_dir()? - .flatten() - .filter(|entry| { - // Collect all *.pth files. - let Ok(file_type) = entry.file_type() else { - return false; - }; - file_type.is_file() || file_type.is_symlink() - }) - .map(|entry| entry.path()) - .filter(|path| path.extension() == Some(OsStr::new("pth"))) - .filter(|path| { - // Skip all files that are much larger than expected. - let Ok(metadata) = path.metadata() else { - return false; - }; - let file_len = metadata.len(); - file_len > 0 && file_len < 64 * 1024 - }) - .filter_map(|path| { - let data = fs::read_to_string(path).ok()?; - for line in data.lines() { - let trimmed_line = line.trim(); - if !trimmed_line.is_empty() - && !trimmed_line.starts_with('#') - && !trimmed_line.starts_with("import") - { - let pth_path = parent_dir.join(trimmed_line); - if pth_path.is_dir() { - return Some(pth_path); - } - } - } - None - })) -} - -/// Find the Python search paths for the given virtual environment. -fn find_python_search_paths(config: &Config, host: &Host) -> Vec { - if let Some(venv_path) = config.venv_path.as_ref() { - if let Some(venv) = config.venv.as_ref() { - let mut found_paths = vec![]; - - for lib_name in host.python_platform().lib_names() { - let lib_path = venv_path.join(venv).join(lib_name); - if let Some(site_packages_path) = find_site_packages_path(&lib_path, None) { - // Add paths from any `.pth` files in each of the `site-packages` directories. - if let Ok(pth_paths) = find_paths_from_pth_files(&site_packages_path) { - found_paths.extend(pth_paths); - } - - // Add the `site-packages` directory to the search path. - found_paths.push(site_packages_path); - } - } - - if !found_paths.is_empty() { - found_paths.sort(); - found_paths.dedup(); - - debug!("Found the following `site-packages` dirs"); - for path in &found_paths { - debug!(" {}", path.display()); - } - - return found_paths; - } - } - } - - // Fall back to the Python interpreter. - host.python_search_paths() -} - -/// Determine the relevant Python search paths. -pub(crate) fn python_search_paths(config: &Config, host: &Host) -> Vec { - // TODO(charlie): Cache search paths. - find_python_search_paths(config, host) -} - -/// Determine the root of the `typeshed` directory. -pub(crate) fn typeshed_root(config: &Config, host: &Host) -> Option { - if let Some(typeshed_path) = config.typeshed_path.as_ref() { - // Did the user specify a typeshed path? - if typeshed_path.is_dir() { - return Some(typeshed_path.clone()); - } - } else { - // If not, we'll look in the Python search paths. - for python_search_path in python_search_paths(config, host) { - let possible_typeshed_path = python_search_path.join("typeshed"); - if possible_typeshed_path.is_dir() { - return Some(possible_typeshed_path); - } - } - } - - None -} - -/// Determine the current `typeshed` subdirectory. -fn typeshed_subdirectory( - is_stdlib: bool, - config: &Config, - host: &Host, -) -> Option { - let typeshed_path = - typeshed_root(config, host)?.join(if is_stdlib { "stdlib" } else { "stubs" }); - if typeshed_path.is_dir() { - Some(typeshed_path) - } else { - None - } -} - -/// Generate a map from PyPI-registered package name to a list of paths -/// containing the package's stubs. -fn build_typeshed_third_party_package_map( - third_party_dir: &Path, -) -> io::Result>> { - let mut package_map = HashMap::new(); - - // Iterate over every directory. - for outer_entry in fs::read_dir(third_party_dir)? { - let outer_entry = outer_entry?; - if outer_entry.file_type()?.is_dir() { - // Iterate over any subdirectory children. - for inner_entry in fs::read_dir(outer_entry.path())? { - let inner_entry = inner_entry?; - - if inner_entry.file_type()?.is_dir() { - package_map - .entry(inner_entry.file_name().to_string_lossy().to_string()) - .or_insert_with(Vec::new) - .push(outer_entry.path()); - } else if inner_entry.file_type()?.is_file() { - if inner_entry - .path() - .extension() - .is_some_and(|extension| extension == "pyi") - { - if let Some(stripped_file_name) = inner_entry - .path() - .file_stem() - .and_then(std::ffi::OsStr::to_str) - .map(std::string::ToString::to_string) - { - package_map - .entry(stripped_file_name) - .or_insert_with(Vec::new) - .push(outer_entry.path()); - } - } - } - } - } - } - - Ok(package_map) -} - -/// Determine the current `typeshed` subdirectory for a third-party package. -pub(crate) fn third_party_typeshed_package_paths( - module_descriptor: &ImportModuleDescriptor, - config: &Config, - host: &Host, -) -> Option> { - let typeshed_path = typeshed_subdirectory(false, config, host)?; - let package_paths = build_typeshed_third_party_package_map(&typeshed_path).ok()?; - let first_name_part = module_descriptor.name_parts.first().map(String::as_str)?; - package_paths.get(first_name_part).cloned() -} - -/// Determine the current `typeshed` subdirectory for the standard library. -pub(crate) fn stdlib_typeshed_path( - config: &Config, - host: &Host, -) -> Option { - typeshed_subdirectory(true, config, host) -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap deleted file mode 100644 index 195c5aa002ef6..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: false, - is_stub_package: false, - import_type: ThirdParty, - resolved_paths: [ - "./resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so", - ], - search_path: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages", - ), - is_stub_file: false, - is_native_lib: true, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - {}, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: None, - package_directory: None, -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap deleted file mode 100644 index e171965662593..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap +++ /dev/null @@ -1,38 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: true, - is_stub_package: false, - import_type: Local, - resolved_paths: [ - "./resources/test/airflow/airflow/__init__.py", - "./resources/test/airflow/airflow/jobs/__init__.py", - "./resources/test/airflow/airflow/jobs/scheduler_job_runner.py", - ], - search_path: Some( - "./resources/test/airflow", - ), - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - {}, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: None, - package_directory: Some( - "./resources/test/airflow/airflow", - ), -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap deleted file mode 100644 index 97a2fb547be78..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap +++ /dev/null @@ -1,92 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: true, - is_stub_package: false, - import_type: ThirdParty, - resolved_paths: [ - "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi", - ], - search_path: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages", - ), - is_stub_file: true, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - { - "orjson": ImplicitImport { - is_stub_file: false, - is_native_lib: true, - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", - py_typed: None, - }, - }, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: Some( - ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: true, - is_stub_package: false, - import_type: ThirdParty, - resolved_paths: [ - "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py", - ], - search_path: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages", - ), - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - { - "orjson": ImplicitImport { - is_stub_file: false, - is_native_lib: true, - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", - py_typed: None, - }, - }, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: Some( - PyTypedInfo { - py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed", - is_partially_typed: false, - }, - ), - package_directory: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson", - ), - }, - ), - py_typed_info: Some( - PyTypedInfo { - py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed", - is_partially_typed: false, - }, - ), - package_directory: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson", - ), -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap deleted file mode 100644 index 5749d8157705d..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap +++ /dev/null @@ -1,41 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: true, - is_init_file_present: true, - is_stub_package: false, - import_type: Local, - resolved_paths: [ - "./resources/test/airflow/airflow/__init__.py", - "", - "./resources/test/airflow/airflow/providers/google/__init__.py", - "./resources/test/airflow/airflow/providers/google/cloud/__init__.py", - "./resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py", - "./resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py", - ], - search_path: Some( - "./resources/test/airflow", - ), - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - {}, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: None, - package_directory: Some( - "./resources/test/airflow/airflow", - ), -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap deleted file mode 100644 index 561a79df02227..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: false, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: false, - is_stub_package: false, - import_type: Local, - resolved_paths: [], - search_path: None, - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - {}, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: None, - package_directory: None, -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap deleted file mode 100644 index 8b5c084c238a4..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap +++ /dev/null @@ -1,72 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: true, - is_stub_package: false, - import_type: Local, - resolved_paths: [ - "./resources/test/airflow/airflow/__init__.py", - "./resources/test/airflow/airflow/compat/__init__.py", - "./resources/test/airflow/airflow/compat/functools.pyi", - ], - search_path: Some( - "./resources/test/airflow", - ), - is_stub_file: true, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - {}, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: Some( - ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: true, - is_stub_package: false, - import_type: Local, - resolved_paths: [ - "./resources/test/airflow/airflow/__init__.py", - "./resources/test/airflow/airflow/compat/__init__.py", - "./resources/test/airflow/airflow/compat/functools.py", - ], - search_path: Some( - "./resources/test/airflow", - ), - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - {}, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: None, - package_directory: Some( - "./resources/test/airflow/airflow", - ), - }, - ), - py_typed_info: None, - package_directory: Some( - "./resources/test/airflow/airflow", - ), -} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap deleted file mode 100644 index f625b0cce98dc..0000000000000 --- a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap +++ /dev/null @@ -1,56 +0,0 @@ ---- -source: crates/ruff_python_resolver/src/lib.rs -expression: result -snapshot_kind: text ---- -ImportResult { - is_relative: false, - is_import_found: true, - is_partly_resolved: false, - is_namespace_package: false, - is_init_file_present: true, - is_stub_package: false, - import_type: ThirdParty, - resolved_paths: [ - "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py", - "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py", - ], - search_path: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages", - ), - is_stub_file: false, - is_native_lib: false, - is_stdlib_typeshed_file: false, - is_third_party_typeshed_file: false, - is_local_typings_file: false, - implicit_imports: ImplicitImports( - { - "base": ImplicitImport { - is_stub_file: false, - is_native_lib: false, - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py", - py_typed: None, - }, - "dependency": ImplicitImport { - is_stub_file: false, - is_native_lib: false, - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", - py_typed: None, - }, - "query": ImplicitImport { - is_stub_file: false, - is_native_lib: false, - path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py", - py_typed: None, - }, - }, - ), - filtered_implicit_imports: ImplicitImports( - {}, - ), - non_stub_import_result: None, - py_typed_info: None, - package_directory: Some( - "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy", - ), -} diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index be9cac6c60c13..5949b71c88a93 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -136,7 +136,11 @@ fn is_class_method( pub fn is_stub(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool { function_def.body.iter().all(|stmt| match stmt { Stmt::Pass(_) => true, - Stmt::Expr(StmtExpr { value, range: _ }) => { + Stmt::Expr(StmtExpr { + value, + range: _, + node_index: _, + }) => { matches!( value.as_ref(), Expr::StringLiteral(_) | Expr::EllipsisLiteral(_) @@ -144,6 +148,7 @@ pub fn is_stub(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool } Stmt::Raise(StmtRaise { range: _, + node_index: _, exc: exception, cause: _, }) => exception.as_ref().is_some_and(|exc| { diff --git a/crates/ruff_python_semantic/src/analyze/imports.rs b/crates/ruff_python_semantic/src/analyze/imports.rs index 60313eba14e1f..e5f20f04d66f6 100644 --- a/crates/ruff_python_semantic/src/analyze/imports.rs +++ b/crates/ruff_python_semantic/src/analyze/imports.rs @@ -12,7 +12,11 @@ use crate::SemanticModel; /// ``` pub fn is_sys_path_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool { match stmt { - Stmt::Expr(ast::StmtExpr { value, range: _ }) => match value.as_ref() { + Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => match value.as_ref() { Expr::Call(ast::ExprCall { func, .. }) => semantic .resolve_qualified_name(func.as_ref()) .is_some_and(|qualified_name| { @@ -96,7 +100,12 @@ pub fn is_os_environ_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool /// matplotlib.use("Agg") /// ``` pub fn is_matplotlib_activation(stmt: &Stmt, semantic: &SemanticModel) -> bool { - let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else { + let Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = stmt + else { return false; }; let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs index 45540232b7118..c0f6f6d48220d 100644 --- a/crates/ruff_python_semantic/src/analyze/type_inference.rs +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -78,6 +78,7 @@ impl From<&Expr> for ResolvedPythonType { Expr::Tuple(_) => ResolvedPythonType::Atom(PythonType::Tuple), Expr::Generator(_) => ResolvedPythonType::Atom(PythonType::Generator), Expr::FString(_) => ResolvedPythonType::Atom(PythonType::String), + Expr::TString(_) => ResolvedPythonType::Unknown, Expr::StringLiteral(_) => ResolvedPythonType::Atom(PythonType::String), Expr::BytesLiteral(_) => ResolvedPythonType::Atom(PythonType::Bytes), Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { @@ -217,11 +218,11 @@ impl From<&Expr> for ResolvedPythonType { ) { // Ex) `"Hello" % "world"` (ResolvedPythonType::Atom(PythonType::String), _) => { - return ResolvedPythonType::Atom(PythonType::String) + return ResolvedPythonType::Atom(PythonType::String); } // Ex) `b"Hello" % b"world"` (ResolvedPythonType::Atom(PythonType::Bytes), _) => { - return ResolvedPythonType::Atom(PythonType::Bytes) + return ResolvedPythonType::Atom(PythonType::Bytes); } // Ex) `1 % 2` ( @@ -452,7 +453,7 @@ impl NumberLike { #[cfg(test)] mod tests { use ruff_python_ast::ModExpression; - use ruff_python_parser::{parse_expression, Parsed}; + use ruff_python_parser::{Parsed, parse_expression}; use crate::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 9b1f1c43f9a58..01220fea0e150 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -15,7 +15,7 @@ use ruff_python_stdlib::typing::{ is_typed_dict, is_typed_dict_member, }; use ruff_text_size::Ranged; -use smallvec::{smallvec, SmallVec}; +use smallvec::{SmallVec, smallvec}; use crate::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use crate::model::SemanticModel; @@ -252,9 +252,7 @@ pub fn is_immutable_annotation( .is_some_and(|qualified_name| { is_immutable_non_generic_type(qualified_name.segments()) || is_immutable_generic_type(qualified_name.segments()) - || extend_immutable_calls - .iter() - .any(|target| qualified_name == *target) + || extend_immutable_calls.contains(&qualified_name) }) } Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic @@ -289,6 +287,7 @@ pub fn is_immutable_annotation( op: Operator::BitOr, right, range: _, + node_index: _, }) => { is_immutable_annotation(left, semantic, extend_immutable_calls) && is_immutable_annotation(right, semantic, extend_immutable_calls) @@ -308,9 +307,7 @@ pub fn is_immutable_func( .resolve_qualified_name(map_subscript(func)) .is_some_and(|qualified_name| { is_immutable_return_type(qualified_name.segments()) - || extend_immutable_calls - .iter() - .any(|target| qualified_name == *target) + || extend_immutable_calls.contains(&qualified_name) }) } @@ -431,12 +428,52 @@ pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> boo pub fn traverse_union<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr) where F: FnMut(&'a Expr, &'a Expr), +{ + traverse_union_options(func, semantic, expr, UnionTraversalOptions::default()); +} + +/// Traverse a "union" type annotation, applying `func` to each union member. +/// +/// Supports traversal of `Union`, `|`, and `Optional` union expressions. +/// +/// The function is called with each expression in the union (excluding declarations of nested +/// unions) and the parent expression. +pub fn traverse_union_and_optional<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr) +where + F: FnMut(&'a Expr, &'a Expr), +{ + traverse_union_options( + func, + semantic, + expr, + UnionTraversalOptions { + traverse_optional: true, + }, + ); +} + +#[derive(Debug, Clone, Copy, Default)] +/// Options for traversing union types. +/// +/// See also [`traverse_union_options`]. +struct UnionTraversalOptions { + traverse_optional: bool, +} + +fn traverse_union_options<'a, F>( + func: &mut F, + semantic: &SemanticModel, + expr: &'a Expr, + options: UnionTraversalOptions, +) where + F: FnMut(&'a Expr, &'a Expr), { fn inner<'a, F>( func: &mut F, semantic: &SemanticModel, expr: &'a Expr, parent: Option<&'a Expr>, + options: UnionTraversalOptions, ) where F: FnMut(&'a Expr, &'a Expr), { @@ -446,6 +483,7 @@ where left, right, range: _, + node_index: _, }) = expr { // The union data structure usually looks like this: @@ -458,25 +496,31 @@ where // in the order they appear in the source code. // Traverse the left then right arms - inner(func, semantic, left, Some(expr)); - inner(func, semantic, right, Some(expr)); + inner(func, semantic, left, Some(expr), options); + inner(func, semantic, right, Some(expr), options); return; } - // Ex) `Union[x, y]` if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { + // Ex) `Union[x, y]` if semantic.match_typing_expr(value, "Union") { if let Expr::Tuple(tuple) = &**slice { // Traverse each element of the tuple within the union recursively to handle cases // such as `Union[..., Union[...]]` tuple .iter() - .for_each(|elem| inner(func, semantic, elem, Some(expr))); + .for_each(|elem| inner(func, semantic, elem, Some(expr), options)); return; } // Ex) `Union[Union[a, b]]` and `Union[a | b | c]` - inner(func, semantic, slice, Some(expr)); + inner(func, semantic, slice, Some(expr), options); + return; + } + // Ex) `Optional[x]` + if options.traverse_optional && semantic.match_typing_expr(value, "Optional") { + inner(func, semantic, value, Some(expr), options); + inner(func, semantic, slice, Some(expr), options); return; } } @@ -487,7 +531,7 @@ where } } - inner(func, semantic, expr, None); + inner(func, semantic, expr, None, options); } /// Traverse a "literal" type annotation, applying `func` to each literal member. @@ -639,6 +683,18 @@ pub fn check_type(binding: &Binding, semantic: &SemanticModel) - _ => false, }, + BindingKind::FunctionDefinition(_) => match binding.statement(semantic) { + // ```python + // def foo() -> int: + // ... + // ``` + Some(Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. })) => returns + .as_ref() + .is_some_and(|return_ann| T::match_annotation(return_ann, semantic)), + + _ => false, + }, + _ => false, } } @@ -1135,7 +1191,7 @@ pub fn find_assigned_value<'a>(symbol: &str, semantic: &'a SemanticModel<'a>) -> /// /// This function will return a `NumberLiteral` with value `Int(42)` when called with `foo` and a /// `StringLiteral` with value `"str"` when called with `bla`. -#[allow(clippy::single_match)] +#[expect(clippy::single_match)] pub fn find_binding_value<'a>(binding: &Binding, semantic: &'a SemanticModel) -> Option<&'a Expr> { match binding.kind { // Ex) `x := 1` @@ -1153,7 +1209,7 @@ pub fn find_binding_value<'a>(binding: &Binding, semantic: &'a SemanticModel) -> Some(Stmt::Assign(ast::StmtAssign { value, targets, .. })) => { return targets .iter() - .find_map(|target| match_value(binding, target, value)) + .find_map(|target| match_value(binding, target, value)); } Some(Stmt::AnnAssign(ast::StmtAnnAssign { value: Some(value), diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index a2f9c8ee34668..e762d5d95cd16 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -4,17 +4,17 @@ use std::ops::{Deref, DerefMut}; use bitflags::bitflags; use crate::all::DunderAllName; -use ruff_index::{newtype_index, IndexSlice, IndexVec}; +use ruff_index::{IndexSlice, IndexVec, newtype_index}; use ruff_python_ast::helpers::extract_handled_exceptions; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Stmt}; use ruff_text_size::{Ranged, TextRange}; +use crate::ScopeId; use crate::context::ExecutionContext; use crate::model::SemanticModel; use crate::nodes::NodeId; use crate::reference::ResolvedReferenceId; -use crate::ScopeId; #[derive(Debug, Clone)] pub struct Binding<'a> { @@ -714,6 +714,15 @@ pub trait Imported<'a> { /// Returns the member name of the imported symbol. For a straight import, this is equivalent /// to the qualified name; for a `from` import, this is the name of the imported symbol. fn member_name(&self) -> Cow<'a, str>; + + /// Returns the source module of the imported symbol. + /// + /// For example: + /// + /// - `import foo` returns `["foo"]` + /// - `import foo.bar` returns `["foo","bar"]` + /// - `from foo import bar` returns `["foo"]` + fn source_name(&self) -> &[&'a str]; } impl<'a> Imported<'a> for Import<'a> { @@ -731,6 +740,10 @@ impl<'a> Imported<'a> for Import<'a> { fn member_name(&self) -> Cow<'a, str> { Cow::Owned(self.qualified_name().to_string()) } + + fn source_name(&self) -> &[&'a str] { + self.qualified_name.segments() + } } impl<'a> Imported<'a> for SubmoduleImport<'a> { @@ -748,6 +761,10 @@ impl<'a> Imported<'a> for SubmoduleImport<'a> { fn member_name(&self) -> Cow<'a, str> { Cow::Owned(self.qualified_name().to_string()) } + + fn source_name(&self) -> &[&'a str] { + self.qualified_name.segments() + } } impl<'a> Imported<'a> for FromImport<'a> { @@ -765,6 +782,10 @@ impl<'a> Imported<'a> for FromImport<'a> { fn member_name(&self) -> Cow<'a, str> { Cow::Borrowed(self.qualified_name.segments()[self.qualified_name.segments().len() - 1]) } + + fn source_name(&self) -> &[&'a str] { + self.module_name() + } } /// A wrapper around an import [`BindingKind`] that can be any of the three types of imports. @@ -799,6 +820,14 @@ impl<'ast> Imported<'ast> for AnyImport<'_, 'ast> { Self::FromImport(import) => import.member_name(), } } + + fn source_name(&self) -> &[&'ast str] { + match self { + Self::Import(import) => import.source_name(), + Self::SubmoduleImport(import) => import.source_name(), + Self::FromImport(import) => import.source_name(), + } + } } #[cfg(test)] diff --git a/crates/ruff_python_semantic/src/branches.rs b/crates/ruff_python_semantic/src/branches.rs index 477f5522a65e7..d3fa0622f1a46 100644 --- a/crates/ruff_python_semantic/src/branches.rs +++ b/crates/ruff_python_semantic/src/branches.rs @@ -1,6 +1,6 @@ use std::ops::Index; -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{IndexVec, newtype_index}; /// ID uniquely identifying a branch in a program. /// diff --git a/crates/ruff_python_semantic/src/cfg/graph.rs b/crates/ruff_python_semantic/src/cfg/graph.rs index 5df49454526a5..59acb348ebabb 100644 --- a/crates/ruff_python_semantic/src/cfg/graph.rs +++ b/crates/ruff_python_semantic/src/cfg/graph.rs @@ -1,7 +1,7 @@ -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast::Stmt; use ruff_text_size::{Ranged, TextRange}; -use smallvec::{smallvec, SmallVec}; +use smallvec::{SmallVec, smallvec}; /// Returns the control flow graph associated to an array of statements pub fn build_cfg(stmts: &[Stmt]) -> ControlFlowGraph<'_> { diff --git a/crates/ruff_python_semantic/src/cfg/visualize.rs b/crates/ruff_python_semantic/src/cfg/visualize.rs index 4e0972914061a..64c9b18106300 100644 --- a/crates/ruff_python_semantic/src/cfg/visualize.rs +++ b/crates/ruff_python_semantic/src/cfg/visualize.rs @@ -208,7 +208,7 @@ impl<'stmt> MermaidGraph<'stmt> for CFGWithSource<'stmt> { return MermaidNode { content: "EXIT".to_string(), shape: MermaidNodeShape::DoubleCircle, - } + }; } }; diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index 3a14a0be4aaf2..245bd101793c6 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -5,17 +5,17 @@ use std::fmt::Debug; use std::ops::Deref; use std::path::Path; -use ruff_index::{newtype_index, IndexSlice, IndexVec}; +use ruff_index::{IndexSlice, IndexVec, newtype_index}; use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Stmt, StmtFunctionDef}; use ruff_text_size::{Ranged, TextRange}; +use crate::SemanticModel; use crate::analyze::visibility::{ - class_visibility, function_visibility, is_property, method_visibility, module_visibility, - Visibility, + Visibility, class_visibility, function_visibility, is_property, method_visibility, + module_visibility, }; use crate::model::all::DunderAllName; -use crate::SemanticModel; /// Id uniquely identifying a definition in a program. #[newtype_index] diff --git a/crates/ruff_python_semantic/src/globals.rs b/crates/ruff_python_semantic/src/globals.rs index a3c27db8a3e57..3a5e8307330a4 100644 --- a/crates/ruff_python_semantic/src/globals.rs +++ b/crates/ruff_python_semantic/src/globals.rs @@ -8,8 +8,8 @@ use ruff_python_ast::{self as ast, Stmt}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashMap; -use ruff_index::{newtype_index, IndexVec}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_index::{IndexVec, newtype_index}; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; /// Id uniquely identifying the set of global names for a given scope. #[newtype_index] @@ -74,7 +74,11 @@ impl<'a> GlobalsVisitor<'a> { impl<'a> StatementVisitor<'a> for GlobalsVisitor<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { match stmt { - Stmt::Global(ast::StmtGlobal { names, range: _ }) => { + Stmt::Global(ast::StmtGlobal { + names, + range: _, + node_index: _, + }) => { for name in names { self.0.insert(name.as_str(), name.range()); } diff --git a/crates/ruff_python_semantic/src/imports.rs b/crates/ruff_python_semantic/src/imports.rs index f340ec991915f..4cc55123cb30f 100644 --- a/crates/ruff_python_semantic/src/imports.rs +++ b/crates/ruff_python_semantic/src/imports.rs @@ -230,6 +230,7 @@ impl<'de> serde::de::Deserialize<'de> for NameImports { names, level, range: _, + node_index: _, }) => names .iter() .map(|name| { @@ -243,7 +244,11 @@ impl<'de> serde::de::Deserialize<'de> for NameImports { }) }) .collect(), - Stmt::Import(ast::StmtImport { names, range: _ }) => names + Stmt::Import(ast::StmtImport { + names, + range: _, + node_index: _, + }) => names .iter() .map(|name| { NameImport::Import(ModuleNameImport { @@ -273,7 +278,7 @@ impl schemars::JsonSchema for NameImports { "NameImports".to_string() } - fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { schemars::schema::SchemaObject { instance_type: Some(schemars::schema::InstanceType::String.into()), ..Default::default() diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index d24060b7c41ca..c6c1a65e6342d 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -8,6 +8,7 @@ use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; use ruff_python_ast::{self as ast, Expr, ExprContext, PySourceType, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use crate::Imported; use crate::binding::{ Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImport, Import, SubmoduleImport, @@ -22,7 +23,6 @@ use crate::reference::{ UnresolvedReferenceFlags, UnresolvedReferences, }; use crate::scope::{Scope, ScopeId, ScopeKind, Scopes}; -use crate::Imported; pub mod all; @@ -1503,24 +1503,48 @@ impl<'a> SemanticModel<'a> { /// Set the [`Globals`] for the current [`Scope`]. pub fn set_globals(&mut self, globals: Globals<'a>) { - // If any global bindings don't already exist in the global scope, add them. - for (name, range) in globals.iter() { - if self - .global_scope() - .get(name) - .is_none_or(|binding_id| self.bindings[binding_id].is_unbound()) - { - let id = self.bindings.push(Binding { - kind: BindingKind::Assignment, - range: *range, - references: Vec::new(), - scope: ScopeId::global(), - source: self.node_id, - context: self.execution_context(), - exceptions: self.exceptions(), - flags: BindingFlags::empty(), - }); - self.global_scope_mut().add(name, id); + // If any global bindings don't already exist in the global scope, add them, unless we are + // also in the global scope, where we don't want these to count as definitions for rules + // like `undefined-name` (F821). For example, adding bindings in the top-level scope causes + // a false negative in cases like this: + // + // ```python + // global x + // + // def f(): + // print(x) # F821 false negative + // ``` + // + // On the other hand, failing to add bindings in non-top-level scopes causes false + // positives: + // + // ```python + // def f(): + // global foo + // import foo + // + // def g(): + // foo.is_used() # F821 false positive + // ``` + if !self.at_top_level() { + for (name, range) in globals.iter() { + if self + .global_scope() + .get(name) + .is_none_or(|binding_id| self.bindings[binding_id].is_unbound()) + { + let id = self.bindings.push(Binding { + kind: BindingKind::Assignment, + range: *range, + references: Vec::new(), + scope: ScopeId::global(), + source: self.node_id, + context: self.execution_context(), + exceptions: self.exceptions(), + flags: BindingFlags::empty(), + }); + self.global_scope_mut().add(name, id); + } } } @@ -1580,7 +1604,7 @@ impl<'a> SemanticModel<'a> { let mut parent_expressions = self.current_expressions().skip(1); match parent_expressions.next() { - // The parent expression is of the inner union is a single `typing.Union`. + // The parent expression of the inner union is a single `typing.Union`. // Ex) `Union[Union[a, b]]` Some(Expr::Subscript(parent)) => self.match_typing_expr(&parent.value, "Union"), // The parent expression is of the inner union is a tuple with two or more @@ -1600,6 +1624,18 @@ impl<'a> SemanticModel<'a> { } } + /// Return `true` if the model is directly inside an Optional (e.g., the inner `Union` in + /// `Optional[Union[int, str]]`). + pub fn inside_optional(&self) -> bool { + let mut parent_expressions = self.current_expressions().skip(1); + matches!( + parent_expressions.next(), + // The parent expression is a single `typing.Optional`. + // Ex) `Optional[EXPR]` + Some(Expr::Subscript(parent)) if self.match_typing_expr(&parent.value, "Optional") + ) + } + /// Return `true` if the model is in a nested literal expression (e.g., the inner `Literal` in /// `Literal[Literal[int, str], float]`). pub fn in_nested_literal(&self) -> bool { @@ -1915,10 +1951,20 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::F_STRING) } + /// Return `true` if the model is in a t-string. + pub const fn in_t_string(&self) -> bool { + self.flags.intersects(SemanticModelFlags::T_STRING) + } + + /// Return `true` if the model is in an f-string or t-string. + pub const fn in_interpolated_string(&self) -> bool { + self.in_f_string() || self.in_t_string() + } + /// Return `true` if the model is in an f-string replacement field. - pub const fn in_f_string_replacement_field(&self) -> bool { + pub const fn in_interpolated_string_replacement_field(&self) -> bool { self.flags - .intersects(SemanticModelFlags::F_STRING_REPLACEMENT_FIELD) + .intersects(SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD) } /// Return `true` if the model is in boolean test. @@ -2048,6 +2094,20 @@ impl<'a> SemanticModel<'a> { None }) } + + /// Finds and returns the [`Scope`] corresponding to a given [`ast::StmtFunctionDef`]. + /// + /// This method searches all scopes created by a function definition, comparing the + /// [`TextRange`] of the provided `function_def` with the the range of the function + /// associated with the scope. + pub fn function_scope(&self, function_def: &ast::StmtFunctionDef) -> Option<&Scope> { + self.scopes.iter().find(|scope| { + let Some(function) = scope.kind.as_function() else { + return false; + }; + function.range() == function_def.range() + }) + } } pub struct ShadowedBinding { @@ -2437,7 +2497,7 @@ bitflags! { /// ```python /// f"first {x} second {y}" /// ``` - const F_STRING_REPLACEMENT_FIELD = 1 << 21; + const INTERPOLATED_STRING_REPLACEMENT_FIELD = 1 << 21; /// The model is visiting the bases tuple of a class. /// @@ -2525,6 +2585,15 @@ bitflags! { /// [#13824]: https://github.com/astral-sh/ruff/issues/13824 const NO_TYPE_CHECK = 1 << 28; + /// The model is in a t-string. + /// + /// For example, the model could be visiting `x` in: + /// ```python + /// t'{x}' + /// ``` + const T_STRING = 1 << 29; + + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits(); diff --git a/crates/ruff_python_semantic/src/model/all.rs b/crates/ruff_python_semantic/src/model/all.rs index e14f8e1e1c9cd..bc05d96efe415 100644 --- a/crates/ruff_python_semantic/src/model/all.rs +++ b/crates/ruff_python_semantic/src/model/all.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; -use ruff_python_ast::{self as ast, helpers::map_subscript, Expr, Stmt}; +use ruff_python_ast::{self as ast, Expr, Stmt, helpers::map_subscript}; use ruff_text_size::{Ranged, TextRange}; use crate::SemanticModel; @@ -82,7 +82,12 @@ impl SemanticModel<'_> { flags: &mut DunderAllFlags, ) { for elt in elts { - if let Expr::StringLiteral(ast::ExprStringLiteral { value, range }) = elt { + if let Expr::StringLiteral(ast::ExprStringLiteral { + value, + range, + node_index: _, + }) = elt + { names.push(DunderAllName { name: value.to_str(), range: *range, diff --git a/crates/ruff_python_semantic/src/nodes.rs b/crates/ruff_python_semantic/src/nodes.rs index 7464d569f0ea7..d1e6358ad9984 100644 --- a/crates/ruff_python_semantic/src/nodes.rs +++ b/crates/ruff_python_semantic/src/nodes.rs @@ -1,6 +1,6 @@ use std::ops::Index; -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast::{Expr, Stmt}; use ruff_text_size::{Ranged, TextRange}; diff --git a/crates/ruff_python_semantic/src/reference.rs b/crates/ruff_python_semantic/src/reference.rs index bee6413d4a9cd..0c5cfd69457b0 100644 --- a/crates/ruff_python_semantic/src/reference.rs +++ b/crates/ruff_python_semantic/src/reference.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use bitflags::bitflags; -use ruff_index::{newtype_index, IndexSlice, IndexVec}; +use ruff_index::{IndexSlice, IndexVec, newtype_index}; use ruff_python_ast::ExprContext; use ruff_text_size::{Ranged, TextRange}; diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 93cf49bba63ae..0ac31f26cc5e8 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -4,7 +4,7 @@ use bitflags::bitflags; use ruff_python_ast as ast; use rustc_hash::FxHashMap; -use ruff_index::{newtype_index, Idx, IndexSlice, IndexVec}; +use ruff_index::{Idx, IndexSlice, IndexVec, newtype_index}; use crate::binding::BindingId; use crate::globals::GlobalsId; diff --git a/crates/ruff_python_stdlib/src/str.rs b/crates/ruff_python_stdlib/src/str.rs index 048b2e59296cc..fe6936f41e820 100644 --- a/crates/ruff_python_stdlib/src/str.rs +++ b/crates/ruff_python_stdlib/src/str.rs @@ -28,7 +28,7 @@ pub fn is_lowercase(s: &str) -> bool { _ => { return s[i..] .chars() - .all(|c| c.is_lowercase() || !c.is_alphabetic()) + .all(|c| c.is_lowercase() || !c.is_alphabetic()); } } } @@ -65,7 +65,7 @@ pub fn is_uppercase(s: &str) -> bool { _ => { return s[i..] .chars() - .all(|c| c.is_uppercase() || !c.is_alphabetic()) + .all(|c| c.is_uppercase() || !c.is_alphabetic()); } } } diff --git a/crates/ruff_python_stdlib/src/sys/builtin_modules.rs b/crates/ruff_python_stdlib/src/sys/builtin_modules.rs index 6000857a2f821..9fce83d0ef8c8 100644 --- a/crates/ruff_python_stdlib/src/sys/builtin_modules.rs +++ b/crates/ruff_python_stdlib/src/sys/builtin_modules.rs @@ -9,7 +9,7 @@ /// modules. /// /// [builtin module]: https://docs.python.org/3/library/sys.html#sys.builtin_module_names -#[allow(clippy::unnested_or_patterns)] +#[expect(clippy::unnested_or_patterns)] pub fn is_builtin_module(minor_version: u8, module: &str) -> bool { matches!( (minor_version, module), diff --git a/crates/ruff_python_stdlib/src/sys/known_stdlib.rs b/crates/ruff_python_stdlib/src/sys/known_stdlib.rs index 7c08ebe800aac..4ab64366fc400 100644 --- a/crates/ruff_python_stdlib/src/sys/known_stdlib.rs +++ b/crates/ruff_python_stdlib/src/sys/known_stdlib.rs @@ -23,7 +23,6 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { | "_collections" | "_collections_abc" | "_compat_pickle" - | "_compression" | "_contextvars" | "_csv" | "_ctypes" @@ -285,6 +284,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { ) | ( 7, "_bootlocale" + | "_compression" | "_crypt" | "_dummy_thread" | "_msi" @@ -324,6 +324,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { ) | ( 8, "_bootlocale" + | "_compression" | "_crypt" | "_dummy_thread" | "_msi" @@ -368,6 +369,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { "_aix_support" | "_bootlocale" | "_bootsubprocess" + | "_compression" | "_crypt" | "_msi" | "_peg_parser" @@ -399,7 +401,6 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { | "nntplib" | "ossaudiodev" | "parser" - | "peg_parser" | "pipes" | "smtpd" | "sndhdr" @@ -414,6 +415,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { 10, "_aix_support" | "_bootsubprocess" + | "_compression" | "_crypt" | "_msi" | "_posixshmem" @@ -460,6 +462,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { | "__phello_alias__" | "_aix_support" | "_bootsubprocess" + | "_compression" | "_crypt" | "_msi" | "_posixshmem" @@ -507,6 +510,7 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { | "__hello_only__" | "__phello_alias__" | "_aix_support" + | "_compression" | "_crypt" | "_msi" | "_posixshmem" @@ -554,7 +558,9 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { | "__phello_alias__" | "_aix_support" | "_android_support" + | "_apple_support" | "_colorize" + | "_compression" | "_interpchannels" | "_interpqueues" | "_interpreters" @@ -583,6 +589,50 @@ pub fn is_known_standard_library(minor_version: u8, module: &str) -> bool { | "tomllib" | "xxlimited_35" | "zoneinfo" + ) | ( + 14, + "__hello_alias__" + | "__hello_only__" + | "__phello_alias__" + | "_aix_support" + | "_android_support" + | "_apple_support" + | "_ast_unparse" + | "_colorize" + | "_hmac" + | "_interpchannels" + | "_interpqueues" + | "_interpreters" + | "_ios_support" + | "_opcode_metadata" + | "_posixshmem" + | "_py_warnings" + | "_pydatetime" + | "_pylong" + | "_pyrepl" + | "_remote_debugging" + | "_sha2" + | "_statistics" + | "_suggestions" + | "_sysconfig" + | "_testcapi_datetime" + | "_testclinic" + | "_testclinic_limited" + | "_testinternalcapi" + | "_testlimitedcapi" + | "_testsinglephase" + | "_tokenize" + | "_types" + | "_typing" + | "_wmi" + | "_zoneinfo" + | "_zstd" + | "annotationlib" + | "compression" + | "graphlib" + | "tomllib" + | "xxlimited_35" + | "zoneinfo" ) ) } diff --git a/crates/ruff_python_trivia/src/comments.rs b/crates/ruff_python_trivia/src/comments.rs index 6eb42c26f3074..bd9d14b85fda6 100644 --- a/crates/ruff_python_trivia/src/comments.rs +++ b/crates/ruff_python_trivia/src/comments.rs @@ -1,6 +1,6 @@ use ruff_text_size::TextRange; -use crate::{is_python_whitespace, PythonWhitespace}; +use crate::{PythonWhitespace, is_python_whitespace}; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum SuppressionKind { diff --git a/crates/ruff_python_trivia/src/cursor.rs b/crates/ruff_python_trivia/src/cursor.rs index 8ab9c37d41f4b..a2c7e17f2b1a1 100644 --- a/crates/ruff_python_trivia/src/cursor.rs +++ b/crates/ruff_python_trivia/src/cursor.rs @@ -21,6 +21,11 @@ impl<'a> Cursor<'a> { } } + /// Retrieves the current offset of the cursor within the source code. + pub fn offset(&self) -> TextSize { + self.source_length - self.text_len() + } + /// Return the remaining input as a string slice. pub fn chars(&self) -> Chars<'a> { self.chars.clone() diff --git a/crates/ruff_python_trivia/src/pragmas.rs b/crates/ruff_python_trivia/src/pragmas.rs index 7fe50d71a0eb8..4d4cfde98abc7 100644 --- a/crates/ruff_python_trivia/src/pragmas.rs +++ b/crates/ruff_python_trivia/src/pragmas.rs @@ -26,5 +26,5 @@ pub fn is_pragma_comment(comment: &str) -> bool { // Case-sensitive match against a variety of pragmas that _do_ require a trailing colon. trimmed .split_once(':') - .is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff")) + .is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff" | "ty")) } diff --git a/crates/ruff_python_trivia/src/tokenizer.rs b/crates/ruff_python_trivia/src/tokenizer.rs index 4b0e1860f5d77..8b59197b77d69 100644 --- a/crates/ruff_python_trivia/src/tokenizer.rs +++ b/crates/ruff_python_trivia/src/tokenizer.rs @@ -2,7 +2,7 @@ use unicode_ident::{is_xid_continue, is_xid_start}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::{is_python_whitespace, Cursor}; +use crate::{Cursor, is_python_whitespace}; /// Searches for the first non-trivia character after `offset`. /// @@ -30,7 +30,7 @@ pub fn find_only_token_in_range( let token = tokens.next().expect("Expected a token"); debug_assert_eq!(token.kind(), token_kind); let mut tokens = tokens.skip_while(|token| token.kind == SimpleTokenKind::LParen); - #[allow(clippy::debug_assert_with_mut_call)] + #[expect(clippy::debug_assert_with_mut_call)] { debug_assert_eq!(tokens.next(), None); } @@ -114,7 +114,7 @@ pub fn lines_after_ignoring_trivia(offset: TextSize, code: &str) -> u32 { /// Counts the empty lines after `offset`, ignoring any trailing trivia on the same line as /// `offset`. -#[allow(clippy::cast_possible_truncation)] +#[expect(clippy::cast_possible_truncation)] pub fn lines_after_ignoring_end_of_line_trivia(offset: TextSize, code: &str) -> u32 { // SAFETY: We don't support files greater than 4GB, so casting to u32 is safe. SimpleTokenizer::starts_at(offset, code) diff --git a/crates/ruff_python_trivia_integration_tests/tests/block_comments.rs b/crates/ruff_python_trivia_integration_tests/tests/block_comments.rs index a39bae973f800..b522350c890d4 100644 --- a/crates/ruff_python_trivia_integration_tests/tests/block_comments.rs +++ b/crates/ruff_python_trivia_integration_tests/tests/block_comments.rs @@ -1,4 +1,4 @@ -use ruff_python_parser::{parse_unchecked, Mode, ParseOptions}; +use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ruff_python_trivia::CommentRanges; use ruff_text_size::TextSize; diff --git a/crates/ruff_python_trivia_integration_tests/tests/simple_tokenizer.rs b/crates/ruff_python_trivia_integration_tests/tests/simple_tokenizer.rs index 1041a9189cb4c..b26218732b47e 100644 --- a/crates/ruff_python_trivia_integration_tests/tests/simple_tokenizer.rs +++ b/crates/ruff_python_trivia_integration_tests/tests/simple_tokenizer.rs @@ -1,8 +1,8 @@ use insta::assert_debug_snapshot; -use ruff_python_parser::{parse_unchecked, Mode, ParseOptions}; -use ruff_python_trivia::{lines_after, lines_before, CommentRanges, SimpleToken, SimpleTokenizer}; +use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ruff_python_trivia::{BackwardsTokenizer, SimpleTokenKind}; +use ruff_python_trivia::{CommentRanges, SimpleToken, SimpleTokenizer, lines_after, lines_before}; use ruff_text_size::{TextLen, TextRange, TextSize}; struct TokenizationTestCase { diff --git a/crates/ruff_python_trivia_integration_tests/tests/whitespace.rs b/crates/ruff_python_trivia_integration_tests/tests/whitespace.rs index feb2268615f4b..630ee6da4d7e6 100644 --- a/crates/ruff_python_trivia_integration_tests/tests/whitespace.rs +++ b/crates/ruff_python_trivia_integration_tests/tests/whitespace.rs @@ -1,4 +1,4 @@ -use ruff_python_parser::{parse_module, ParseError}; +use ruff_python_parser::{ParseError, parse_module}; use ruff_python_trivia::has_trailing_content; use ruff_text_size::Ranged; diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 5b7e682d07d3e..4bfaf476638f0 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -13,6 +13,7 @@ license = { workspace = true } [lib] [dependencies] +ruff_db = { workspace = true } ruff_diagnostics = { workspace = true } ruff_formatter = { workspace = true } ruff_linter = { workspace = true } diff --git a/crates/ruff_server/src/edit.rs b/crates/ruff_server/src/edit.rs index 3a7ffb4e3eb73..d0dfb91ae3e25 100644 --- a/crates/ruff_server/src/edit.rs +++ b/crates/ruff_server/src/edit.rs @@ -31,6 +31,16 @@ pub enum PositionEncoding { UTF8, } +impl From for ruff_source_file::PositionEncoding { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::UTF8 => Self::Utf8, + PositionEncoding::UTF16 => Self::Utf16, + PositionEncoding::UTF32 => Self::Utf32, + } + } +} + /// A unique document ID, derived from a URL passed as part of an LSP request. /// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. #[derive(Clone, Debug)] diff --git a/crates/ruff_server/src/edit/notebook.rs b/crates/ruff_server/src/edit/notebook.rs index d71ada7808d45..154628d862639 100644 --- a/crates/ruff_server/src/edit/notebook.rs +++ b/crates/ruff_server/src/edit/notebook.rs @@ -247,7 +247,7 @@ mod tests { use super::NotebookDocument; enum TestCellContent { - #[allow(dead_code)] + #[expect(dead_code)] Markup(String), Code(String), } diff --git a/crates/ruff_server/src/edit/range.rs b/crates/ruff_server/src/edit/range.rs index 9ccef9e67de03..91b461751e44e 100644 --- a/crates/ruff_server/src/edit/range.rs +++ b/crates/ruff_server/src/edit/range.rs @@ -1,10 +1,10 @@ -use super::notebook; use super::PositionEncoding; +use super::notebook; use lsp_types as types; use ruff_notebook::NotebookIndex; -use ruff_source_file::OneIndexed; -use ruff_source_file::{LineIndex, SourceLocation}; -use ruff_text_size::{TextRange, TextSize}; +use ruff_source_file::LineIndex; +use ruff_source_file::{OneIndexed, SourceLocation}; +use ruff_text_size::TextRange; pub(crate) struct NotebookRange { pub(crate) cell: notebook::CellId, @@ -13,7 +13,7 @@ pub(crate) struct NotebookRange { pub(crate) trait RangeExt { fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) - -> TextRange; + -> TextRange; } pub(crate) trait ToRangeExt { @@ -38,76 +38,43 @@ impl RangeExt for lsp_types::Range { index: &LineIndex, encoding: PositionEncoding, ) -> TextRange { - let start_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + let start = index.offset( + SourceLocation { + line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize( + self.start.character, + )), + }, text, + encoding.into(), ); - let end_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + let end = index.offset( + SourceLocation { + line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize( + self.end.character, + )), + }, text, + encoding.into(), ); - let (start_column_offset, end_column_offset) = match encoding { - PositionEncoding::UTF8 => ( - TextSize::new(self.start.character), - TextSize::new(self.end.character), - ), - - PositionEncoding::UTF16 => { - // Fast path for ASCII only documents - if index.is_ascii() { - ( - TextSize::new(self.start.character), - TextSize::new(self.end.character), - ) - } else { - // UTF16 encodes characters either as one or two 16 bit words. - // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) - // UTF-16 with a text that may use variable-length characters. - ( - utf8_column_offset(self.start.character, &text[start_line]), - utf8_column_offset(self.end.character, &text[end_line]), - ) - } - } - PositionEncoding::UTF32 => { - // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. - return TextRange::new( - index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.character)), - text, - ), - index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.character)), - text, - ), - ); - } - }; - - TextRange::new( - start_line.start() + start_column_offset.clamp(TextSize::new(0), start_line.end()), - end_line.start() + end_column_offset.clamp(TextSize::new(0), end_line.end()), - ) + TextRange::new(start, end) } } impl ToRangeExt for TextRange { fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { types::Range { - start: source_location_to_position(&offset_to_source_location( + start: source_location_to_position(&index.source_location( self.start(), text, - index, - encoding, + encoding.into(), )), - end: source_location_to_position(&offset_to_source_location( + end: source_location_to_position(&index.source_location( self.end(), text, - index, - encoding, + encoding.into(), )), } } @@ -119,26 +86,26 @@ impl ToRangeExt for TextRange { notebook_index: &NotebookIndex, encoding: PositionEncoding, ) -> NotebookRange { - let start = offset_to_source_location(self.start(), text, source_index, encoding); - let mut end = offset_to_source_location(self.end(), text, source_index, encoding); - let starting_cell = notebook_index.cell(start.row); + let start = source_index.source_location(self.start(), text, encoding.into()); + let mut end = source_index.source_location(self.end(), text, encoding.into()); + let starting_cell = notebook_index.cell(start.line); // weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds') // we need to move it one character back (which should place it at the end of the last line). // we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset. - if notebook_index.cell(end.row) != starting_cell { - end.row = end.row.saturating_sub(1); - end.column = offset_to_source_location( - self.end().checked_sub(1.into()).unwrap_or_default(), - text, - source_index, - encoding, - ) - .column; + if notebook_index.cell(end.line) != starting_cell { + end.line = end.line.saturating_sub(1); + end.character_offset = source_index + .source_location( + self.end().checked_sub(1.into()).unwrap_or_default(), + text, + encoding.into(), + ) + .character_offset; } - let start = source_location_to_position(¬ebook_index.translate_location(&start)); - let end = source_location_to_position(¬ebook_index.translate_location(&end)); + let start = source_location_to_position(¬ebook_index.translate_source_location(&start)); + let end = source_location_to_position(¬ebook_index.translate_source_location(&end)); NotebookRange { cell: starting_cell @@ -149,67 +116,10 @@ impl ToRangeExt for TextRange { } } -/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. -fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::new(0); - - let mut i = 0u32; - - for c in line.chars() { - if i >= utf16_code_unit_offset { - break; - } - - // Count characters encoded as two 16 bit words as 2 characters. - { - utf8_code_unit_offset += - TextSize::new(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); - i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); - } - } - - utf8_code_unit_offset -} - -fn offset_to_source_location( - offset: TextSize, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, -) -> SourceLocation { - match encoding { - PositionEncoding::UTF8 => { - let row = index.line_index(offset); - let column = offset - index.line_start(row, text); - - SourceLocation { - column: OneIndexed::from_zero_indexed(column.to_usize()), - row, - } - } - PositionEncoding::UTF16 => { - let row = index.line_index(offset); - - let column = if index.is_ascii() { - (offset - index.line_start(row, text)).to_usize() - } else { - let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; - up_to_line.encode_utf16().count() - }; - - SourceLocation { - column: OneIndexed::from_zero_indexed(column), - row, - } - } - PositionEncoding::UTF32 => index.source_location(offset, text), - } -} - fn source_location_to_position(location: &SourceLocation) -> types::Position { types::Position { - line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), - character: u32::try_from(location.column.to_zero_indexed()) + line: u32::try_from(location.line.to_zero_indexed()).expect("row usize fits in u32"), + character: u32::try_from(location.character_offset.to_zero_indexed()) .expect("character usize fits in u32"), } } diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index 77377ca937984..e5d00ff0cf176 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -82,9 +82,11 @@ impl TextDocument { new_version: DocumentVersion, encoding: PositionEncoding, ) { - if let [lsp_types::TextDocumentContentChangeEvent { - range: None, text, .. - }] = changes.as_slice() + if let [ + lsp_types::TextDocumentContentChangeEvent { + range: None, text, .. + }, + ] = changes.as_slice() { tracing::debug!("Fast path - replacing entire document"); self.modify(|contents, version| { diff --git a/crates/ruff_server/src/fix.rs b/crates/ruff_server/src/fix.rs index 99477c6460751..0c910f67553d9 100644 --- a/crates/ruff_server/src/fix.rs +++ b/crates/ruff_server/src/fix.rs @@ -3,16 +3,16 @@ use std::borrow::Cow; use rustc_hash::FxHashMap; use crate::{ + PositionEncoding, edit::{Replacement, ToRangeExt}, resolve::is_document_excluded_for_linting, session::DocumentQuery, - PositionEncoding, }; use ruff_linter::package::PackageRoot; use ruff_linter::{ linter::FixerResult, packaging::detect_package_root, - settings::{flags, LinterSettings}, + settings::{LinterSettings, flags}, }; use ruff_notebook::SourceValue; use ruff_source_file::LineIndex; @@ -28,21 +28,22 @@ pub(crate) fn fix_all( ) -> crate::Result { let source_kind = query.make_source_kind(); let settings = query.settings(); - let document_path = query.file_path(); + let document_path = query.virtual_file_path(); // If the document is excluded, return an empty list of fixes. - let package = if let Some(document_path) = document_path.as_ref() { - if is_document_excluded_for_linting( - document_path, - &settings.file_resolver, - linter_settings, - query.text_document_language_id(), - ) { - return Ok(Fixes::default()); - } + if is_document_excluded_for_linting( + &document_path, + &settings.file_resolver, + linter_settings, + query.text_document_language_id(), + ) { + return Ok(Fixes::default()); + } + let file_path = query.file_path(); + let package = if let Some(file_path) = &file_path { detect_package_root( - document_path + file_path .parent() .expect("a path to a document should have a parent path"), &linter_settings.namespace_packages, @@ -65,7 +66,7 @@ pub(crate) fn fix_all( result, .. } = ruff_linter::linter::lint_fix( - &query.virtual_file_path(), + &document_path, package, flags::Noqa::Enabled, settings.unsafe_fixes, diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index 4a43de394b914..d63d94f5ec9ce 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -2,7 +2,7 @@ use std::path::Path; use ruff_formatter::PrintedRange; use ruff_python_ast::PySourceType; -use ruff_python_formatter::{format_module_source, FormatModuleError}; +use ruff_python_formatter::{FormatModuleError, format_module_source}; use ruff_text_size::TextRange; use ruff_workspace::FormatterSettings; @@ -12,10 +12,10 @@ pub(crate) fn format( document: &TextDocument, source_type: PySourceType, formatter_settings: &FormatterSettings, - path: Option<&Path>, + path: &Path, ) -> crate::Result> { let format_options = - formatter_settings.to_format_options(source_type, document.contents(), path); + formatter_settings.to_format_options(source_type, document.contents(), Some(path)); match format_module_source(document.contents(), format_options) { Ok(formatted) => { let formatted = formatted.into_code(); @@ -40,10 +40,10 @@ pub(crate) fn format_range( source_type: PySourceType, formatter_settings: &FormatterSettings, range: TextRange, - path: Option<&Path>, + path: &Path, ) -> crate::Result> { let format_options = - formatter_settings.to_format_options(source_type, document.contents(), path); + formatter_settings.to_format_options(source_type, document.contents(), Some(path)); match ruff_python_formatter::format_range(document.contents(), range, format_options) { Ok(formatted) => { @@ -73,8 +73,8 @@ mod tests { use ruff_text_size::{TextRange, TextSize}; use ruff_workspace::FormatterSettings; - use crate::format::{format, format_range}; use crate::TextDocument; + use crate::format::{format, format_range}; #[test] fn format_per_file_version() { @@ -97,7 +97,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a per_file_target_version, ..Default::default() }, - Some(Path::new("test.py")), + Path::new("test.py"), ) .expect("Expected no errors when formatting") .expect("Expected formatting changes"); @@ -119,7 +119,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a unresolved_target_version: PythonVersion::PY38, ..Default::default() }, - Some(Path::new("test.py")), + Path::new("test.py"), ) .expect("Expected no errors when formatting") .expect("Expected formatting changes"); @@ -167,7 +167,7 @@ sys.exit( ..Default::default() }, range, - Some(Path::new("test.py")), + Path::new("test.py"), ) .expect("Expected no errors when formatting") .expect("Expected formatting changes"); @@ -190,7 +190,7 @@ sys.exit( ..Default::default() }, range, - Some(Path::new("test.py")), + Path::new("test.py"), ) .expect("Expected no errors when formatting") .expect("Expected formatting changes"); diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 44d5e1bb52181..784538a23e8c0 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -1,13 +1,15 @@ //! ## The Ruff Language Server +use std::num::NonZeroUsize; + +use anyhow::Context as _; pub use edit::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument}; use lsp_types::CodeActionKind; -pub use server::Server; -pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session}; +pub use server::{ConnectionSender, MainLoopSender, Server}; +pub use session::{Client, ClientOptions, DocumentQuery, DocumentSnapshot, GlobalOptions, Session}; pub use workspace::{Workspace, Workspaces}; -#[macro_use] -mod message; +use crate::server::ConnectionInitializer; mod edit; mod fix; @@ -37,3 +39,35 @@ pub(crate) type Result = anyhow::Result; pub(crate) fn version() -> &'static str { ruff_linter::VERSION } + +pub fn run(preview: Option) -> Result<()> { + let four = NonZeroUsize::new(4).unwrap(); + + // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. + let worker_threads = std::thread::available_parallelism() + .unwrap_or(four) + .min(four); + + let (connection, io_threads) = ConnectionInitializer::stdio(); + + let server_result = Server::new(worker_threads, connection, preview) + .context("Failed to start server")? + .run(); + + let io_result = io_threads.join(); + + let result = match (server_result, io_result) { + (Ok(()), Ok(())) => Ok(()), + (Err(server), Err(io)) => Err(server).context(format!("IO thread error: {io}")), + (Err(server), _) => Err(server), + (_, Err(io)) => Err(io).context("IO thread error"), + }; + + if let Err(err) = result.as_ref() { + tracing::warn!("Server shut down with an error: {err}"); + } else { + tracing::info!("Server shut down"); + } + + result +} diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 10769f788f570..4283cde139f35 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -4,23 +4,22 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use crate::{ + DIAGNOSTIC_NAME, PositionEncoding, edit::{NotebookRange, ToRangeExt}, resolve::is_document_excluded_for_linting, session::DocumentQuery, - PositionEncoding, DIAGNOSTIC_NAME, }; -use ruff_diagnostics::{Applicability, DiagnosticKind, Edit, Fix}; +use ruff_db::diagnostic::Diagnostic; +use ruff_diagnostics::{Applicability, Edit, Fix}; use ruff_linter::{ - directives::{extract_directives, Flags}, + Locator, + directives::{Flags, extract_directives}, generate_noqa_edits, linter::check_path, - message::{DiagnosticMessage, Message, SyntaxErrorMessage}, package::PackageRoot, packaging::detect_package_root, - registry::AsRule, settings::flags, source_kind::SourceKind, - Locator, }; use ruff_notebook::Notebook; use ruff_python_codegen::Stylist; @@ -32,7 +31,8 @@ use ruff_text_size::{Ranged, TextRange}; /// This is serialized on the diagnostic `data` field. #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) struct AssociatedDiagnosticData { - pub(crate) kind: DiagnosticKind, + /// The message describing what the fix does, if it exists, or the diagnostic name otherwise. + pub(crate) title: String, /// Edits to fix the diagnostic. If this is empty, a fix /// does not exist. pub(crate) edits: Vec, @@ -69,21 +69,22 @@ pub(crate) fn check( ) -> DiagnosticsMap { let source_kind = query.make_source_kind(); let settings = query.settings(); - let document_path = query.file_path(); + let document_path = query.virtual_file_path(); // If the document is excluded, return an empty list of diagnostics. - let package = if let Some(document_path) = document_path.as_ref() { - if is_document_excluded_for_linting( - document_path, - &settings.file_resolver, - &settings.linter, - query.text_document_language_id(), - ) { - return DiagnosticsMap::default(); - } + if is_document_excluded_for_linting( + &document_path, + &settings.file_resolver, + &settings.linter, + query.text_document_language_id(), + ) { + return DiagnosticsMap::default(); + } + let file_path = query.file_path(); + let package = if let Some(file_path) = &file_path { detect_package_root( - document_path + file_path .parent() .expect("a path to a document should have a parent path"), &settings.linter.namespace_packages, @@ -95,13 +96,10 @@ pub(crate) fn check( let source_type = query.source_type(); - let target_version = if let Some(path) = &document_path { - settings.linter.resolve_target_version(path) - } else { - settings.linter.unresolved_target_version - }; + let target_version = settings.linter.resolve_target_version(&document_path); - let parse_options = ParseOptions::from(source_type).with_target_version(target_version); + let parse_options = + ParseOptions::from(source_type).with_target_version(target_version.parser_version()); // Parse once. let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), parse_options) @@ -121,8 +119,8 @@ pub(crate) fn check( let directives = extract_directives(parsed.tokens(), Flags::all(), &locator, &indexer); // Generate checks. - let messages = check_path( - &query.virtual_file_path(), + let diagnostics = check_path( + &document_path, package, &locator, &stylist, @@ -137,8 +135,8 @@ pub(crate) fn check( ); let noqa_edits = generate_noqa_edits( - &query.virtual_file_path(), - &messages, + &document_path, + &diagnostics, &locator, indexer.comment_ranges(), &settings.linter.external, @@ -161,28 +159,20 @@ pub(crate) fn check( } let lsp_diagnostics = - messages + diagnostics .into_iter() .zip(noqa_edits) - .filter_map(|(message, noqa_edit)| match message { - Message::Diagnostic(diagnostic_message) => Some(to_lsp_diagnostic( - diagnostic_message, - noqa_edit, - &source_kind, - locator.to_index(), - encoding, - )), - Message::SyntaxError(syntax_error_message) => { - if show_syntax_errors { - Some(syntax_error_to_lsp_diagnostic( - syntax_error_message, - &source_kind, - locator.to_index(), - encoding, - )) - } else { - None - } + .filter_map(|(message, noqa_edit)| { + if message.is_invalid_syntax() && !show_syntax_errors { + None + } else { + Some(to_lsp_diagnostic( + &message, + noqa_edit, + &source_kind, + locator.to_index(), + encoding, + )) } }); @@ -226,10 +216,7 @@ pub(crate) fn fixes_for_diagnostics( Ok(Some(DiagnosticFix { fixed_diagnostic, code: associated_data.code, - title: associated_data - .kind - .suggestion - .unwrap_or(associated_data.kind.name), + title: associated_data.title, noqa_edit: associated_data.noqa_edit, edits: associated_data.edits, })) @@ -241,27 +228,25 @@ pub(crate) fn fixes_for_diagnostics( /// Generates an LSP diagnostic with an associated cell index for the diagnostic to go in. /// If the source kind is a text document, the cell index will always be `0`. fn to_lsp_diagnostic( - diagnostic: DiagnosticMessage, + diagnostic: &Diagnostic, noqa_edit: Option, source_kind: &SourceKind, index: &LineIndex, encoding: PositionEncoding, ) -> (usize, lsp_types::Diagnostic) { - let DiagnosticMessage { - kind, - range: diagnostic_range, - fix, - .. - } = diagnostic; - - let rule = kind.rule(); + let diagnostic_range = diagnostic.expect_range(); + let name = diagnostic.name(); + let body = diagnostic.body().to_string(); + let fix = diagnostic.fix(); + let suggestion = diagnostic.suggestion(); + let code = diagnostic.secondary_code(); let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix)); let data = (fix.is_some() || noqa_edit.is_some()) .then(|| { + let code = code?.to_string(); let edits = fix - .as_ref() .into_iter() .flat_map(Fix::edits) .map(|edit| lsp_types::TextEdit { @@ -274,17 +259,15 @@ fn to_lsp_diagnostic( new_text: noqa_edit.into_content().unwrap_or_default().into_string(), }); serde_json::to_value(AssociatedDiagnosticData { - kind: kind.clone(), + title: suggestion.unwrap_or(name).to_string(), noqa_edit, edits, - code: rule.noqa_code().to_string(), + code, }) .ok() }) .flatten(); - let code = rule.noqa_code().to_string(); - let range: lsp_types::Range; let cell: usize; @@ -300,65 +283,37 @@ fn to_lsp_diagnostic( range = diagnostic_range.to_range(source_kind.source_code(), index, encoding); } + let (severity, tags, code) = if let Some(code) = code { + let code = code.to_string(); + ( + Some(severity(&code)), + tags(&code), + Some(lsp_types::NumberOrString::String(code)), + ) + } else { + (None, None, None) + }; + ( cell, lsp_types::Diagnostic { range, - severity: Some(severity(&code)), - tags: tags(&code), - code: Some(lsp_types::NumberOrString::String(code)), - code_description: rule.url().and_then(|url| { + severity, + tags, + code, + code_description: diagnostic.to_url().and_then(|url| { Some(lsp_types::CodeDescription { href: lsp_types::Url::parse(&url).ok()?, }) }), source: Some(DIAGNOSTIC_NAME.into()), - message: kind.body, + message: body, related_information: None, data, }, ) } -fn syntax_error_to_lsp_diagnostic( - syntax_error: SyntaxErrorMessage, - source_kind: &SourceKind, - index: &LineIndex, - encoding: PositionEncoding, -) -> (usize, lsp_types::Diagnostic) { - let range: lsp_types::Range; - let cell: usize; - - if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) { - NotebookRange { cell, range } = syntax_error.range.to_notebook_range( - source_kind.source_code(), - index, - notebook_index, - encoding, - ); - } else { - cell = usize::default(); - range = syntax_error - .range - .to_range(source_kind.source_code(), index, encoding); - } - - ( - cell, - lsp_types::Diagnostic { - range, - severity: Some(lsp_types::DiagnosticSeverity::ERROR), - tags: None, - code: None, - code_description: None, - source: Some(DIAGNOSTIC_NAME.into()), - message: syntax_error.message, - related_information: None, - data: None, - }, - ) -} - fn diagnostic_edit_range( range: TextRange, source_kind: &SourceKind, diff --git a/crates/ruff_server/src/logging.rs b/crates/ruff_server/src/logging.rs index c402c3a869fa6..6d3700eca2163 100644 --- a/crates/ruff_server/src/logging.rs +++ b/crates/ruff_server/src/logging.rs @@ -9,9 +9,9 @@ use serde::Deserialize; use std::{path::PathBuf, str::FromStr, sync::Arc}; use tracing::level_filters::LevelFilter; use tracing_subscriber::{ + Layer, fmt::{format::FmtSpan, time::ChronoLocal, writer::BoxMakeWriter}, layer::SubscriberExt, - Layer, }; pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Path>) { @@ -34,7 +34,7 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Pat .append(true) .open(&path) .map_err(|err| { - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!( "Failed to open file at {} for logging: {err}", diff --git a/crates/ruff_server/src/message.rs b/crates/ruff_server/src/message.rs deleted file mode 100644 index 1b007ea38c5c2..0000000000000 --- a/crates/ruff_server/src/message.rs +++ /dev/null @@ -1,54 +0,0 @@ -use anyhow::Context; -use lsp_types::notification::Notification; -use std::sync::OnceLock; - -use crate::server::ClientSender; - -static MESSENGER: OnceLock = OnceLock::new(); - -pub(crate) fn init_messenger(client_sender: ClientSender) { - MESSENGER - .set(client_sender) - .expect("messenger should only be initialized once"); -} - -pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) { - try_show_message(message, message_type).unwrap(); -} - -pub(super) fn try_show_message( - message: String, - message_type: lsp_types::MessageType, -) -> crate::Result<()> { - MESSENGER - .get() - .ok_or_else(|| anyhow::anyhow!("messenger not initialized"))? - .send(lsp_server::Message::Notification( - lsp_server::Notification { - method: lsp_types::notification::ShowMessage::METHOD.into(), - params: serde_json::to_value(lsp_types::ShowMessageParams { - typ: message_type, - message, - })?, - }, - )) - .context("Failed to send message")?; - - Ok(()) -} - -/// Sends a request to display an error to the client with a formatted message. The error is sent -/// in a `window/showMessage` notification. -macro_rules! show_err_msg { - ($msg:expr$(, $($arg:tt)*)?) => { - crate::message::show_message(::core::format_args!($msg$(, $($arg)*)?).to_string(), lsp_types::MessageType::ERROR) - }; -} - -/// Sends a request to display a warning to the client with a formatted message. The warning is -/// sent in a `window/showMessage` notification. -macro_rules! show_warn_msg { - ($msg:expr$(, $($arg:tt)*)?) => { - crate::message::show_message(::core::format_args!($msg$(, $($arg)*)?).to_string(), lsp_types::MessageType::WARNING) - }; -} diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 88c0273fb602f..19e0d75a233a3 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -1,19 +1,17 @@ //! Scheduling, I/O, and API endpoints. -use lsp_server as lsp; +use lsp_server::Connection; use lsp_types as types; use lsp_types::InitializeParams; +use lsp_types::MessageType; use std::num::NonZeroUsize; -// The new PanicInfoHook name requires MSRV >= 1.82 -#[allow(deprecated)] -use std::panic::PanicInfo; +use std::panic::PanicHookInfo; use std::str::FromStr; +use std::sync::Arc; use types::ClientCapabilities; use types::CodeActionKind; use types::CodeActionOptions; use types::DiagnosticOptions; -use types::DidChangeWatchedFilesRegistrationOptions; -use types::FileSystemWatcher; use types::NotebookCellSelector; use types::NotebookDocumentSyncOptions; use types::NotebookSelector; @@ -24,41 +22,43 @@ use types::TextDocumentSyncOptions; use types::WorkDoneProgressOptions; use types::WorkspaceFoldersServerCapabilities; -use self::connection::Connection; -use self::connection::ConnectionInitializer; -use self::schedule::event_loop_thread; -use self::schedule::Scheduler; -use self::schedule::Task; -use crate::session::AllSettings; -use crate::session::Session; -use crate::workspace::Workspaces; +pub(crate) use self::connection::ConnectionInitializer; +pub use self::connection::ConnectionSender; +use self::schedule::spawn_main_loop; use crate::PositionEncoding; +pub use crate::server::main_loop::MainLoopSender; +pub(crate) use crate::server::main_loop::{Event, MainLoopReceiver}; +use crate::session::{AllOptions, Client, Session}; +use crate::workspace::Workspaces; +pub(crate) use api::Error; mod api; -mod client; mod connection; +mod main_loop; mod schedule; -use crate::message::try_show_message; -pub(crate) use connection::ClientSender; - pub(crate) type Result = std::result::Result; pub struct Server { connection: Connection, client_capabilities: ClientCapabilities, worker_threads: NonZeroUsize, + main_loop_receiver: MainLoopReceiver, + main_loop_sender: MainLoopSender, session: Session, } impl Server { - pub fn new(worker_threads: NonZeroUsize, preview: Option) -> crate::Result { - let connection = ConnectionInitializer::stdio(); - + pub(crate) fn new( + worker_threads: NonZeroUsize, + connection: ConnectionInitializer, + preview: Option, + ) -> crate::Result { let (id, init_params) = connection.initialize_start()?; let client_capabilities = init_params.capabilities; let position_encoding = Self::find_best_position_encoding(&client_capabilities); + let server_capabilities = Self::server_capabilities(position_encoding); let connection = connection.initialize_finish( @@ -68,7 +68,7 @@ impl Server { crate::version(), )?; - crate::message::init_messenger(connection.make_sender()); + let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32); let InitializeParams { initialization_options, @@ -76,181 +76,61 @@ impl Server { .. } = init_params; - let mut all_settings = AllSettings::from_value( + let client = Client::new(main_loop_sender.clone(), connection.sender.clone()); + let mut all_options = AllOptions::from_value( initialization_options .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())), + &client, ); + if let Some(preview) = preview { - all_settings.set_preview(preview); + all_options.set_preview(preview); } - let AllSettings { - global_settings, - workspace_settings, - } = all_settings; + + let AllOptions { + global: global_options, + workspace: workspace_options, + } = all_options; crate::logging::init_logging( - global_settings.tracing.log_level.unwrap_or_default(), - global_settings.tracing.log_file.as_deref(), + global_options.tracing.log_level.unwrap_or_default(), + global_options.tracing.log_file.as_deref(), ); let workspaces = Workspaces::from_workspace_folders( workspace_folders, - workspace_settings.unwrap_or_default(), + workspace_options.unwrap_or_default(), )?; + tracing::debug!("Negotiated position encoding: {position_encoding:?}"); + + let global = global_options.into_settings(client.clone()); + Ok(Self { connection, worker_threads, + main_loop_sender, + main_loop_receiver, session: Session::new( &client_capabilities, position_encoding, - global_settings, + global, &workspaces, + &client, )?, client_capabilities, }) } - pub fn run(self) -> crate::Result<()> { - // The new PanicInfoHook name requires MSRV >= 1.82 - #[allow(deprecated)] - type PanicHook = Box) + 'static + Sync + Send>; - struct RestorePanicHook { - hook: Option, - } - - impl Drop for RestorePanicHook { - fn drop(&mut self) { - if let Some(hook) = self.hook.take() { - std::panic::set_hook(hook); - } - } - } - - // unregister any previously registered panic hook - // The hook will be restored when this function exits. - let _ = RestorePanicHook { - hook: Some(std::panic::take_hook()), - }; - - // When we panic, try to notify the client. - std::panic::set_hook(Box::new(move |panic_info| { - use std::io::Write; - - let backtrace = std::backtrace::Backtrace::force_capture(); - tracing::error!("{panic_info}\n{backtrace}"); - - // we also need to print to stderr directly for when using `$logTrace` because - // the message won't be sent to the client. - // But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken. - let mut stderr = std::io::stderr().lock(); - writeln!(stderr, "{panic_info}\n{backtrace}").ok(); + pub fn run(mut self) -> crate::Result<()> { + let client = Client::new( + self.main_loop_sender.clone(), + self.connection.sender.clone(), + ); - try_show_message( - "The Ruff language server exited with a panic. See the logs for more details." - .to_string(), - lsp_types::MessageType::ERROR, - ) - .ok(); - })); + let _panic_hook = ServerPanicHookHandler::new(client); - event_loop_thread(move || { - Self::event_loop( - &self.connection, - &self.client_capabilities, - self.session, - self.worker_threads, - )?; - self.connection.close()?; - Ok(()) - })? - .join() - } - - #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. - fn event_loop( - connection: &Connection, - client_capabilities: &ClientCapabilities, - mut session: Session, - worker_threads: NonZeroUsize, - ) -> crate::Result<()> { - let mut scheduler = - schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender()); - - Self::try_register_capabilities(client_capabilities, &mut scheduler); - for msg in connection.incoming() { - if connection.handle_shutdown(&msg)? { - break; - } - let task = match msg { - lsp::Message::Request(req) => api::request(req), - lsp::Message::Notification(notification) => api::notification(notification), - lsp::Message::Response(response) => scheduler.response(response), - }; - scheduler.dispatch(task); - } - - Ok(()) - } - - fn try_register_capabilities( - client_capabilities: &ClientCapabilities, - scheduler: &mut Scheduler, - ) { - let dynamic_registration = client_capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.did_change_watched_files) - .and_then(|watched_files| watched_files.dynamic_registration) - .unwrap_or_default(); - if dynamic_registration { - // Register all dynamic capabilities here - - // `workspace/didChangeWatchedFiles` - // (this registers the configuration file watcher) - let params = lsp_types::RegistrationParams { - registrations: vec![lsp_types::Registration { - id: "ruff-server-watch".into(), - method: "workspace/didChangeWatchedFiles".into(), - register_options: Some( - serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { - watchers: vec![ - FileSystemWatcher { - glob_pattern: types::GlobPattern::String( - "**/.ruff.toml".into(), - ), - kind: None, - }, - FileSystemWatcher { - glob_pattern: types::GlobPattern::String("**/ruff.toml".into()), - kind: None, - }, - FileSystemWatcher { - glob_pattern: types::GlobPattern::String( - "**/pyproject.toml".into(), - ), - kind: None, - }, - ], - }) - .unwrap(), - ), - }], - }; - - let response_handler = |()| { - tracing::info!("Configuration file watcher successfully registered"); - Task::nothing() - }; - - if let Err(err) = scheduler - .request::(params, response_handler) - { - tracing::error!("An error occurred when trying to register the configuration file watcher: {err}"); - } - } else { - tracing::warn!("LSP client does not support dynamic capability registration - automatic configuration reloading will not be available."); - } + spawn_main_loop(move || self.main_loop())?.join() } fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding { @@ -442,3 +322,63 @@ impl FromStr for SupportedCommand { }) } } + +type PanicHook = Box) + 'static + Sync + Send>; + +struct ServerPanicHookHandler { + hook: Option, + // Hold on to the strong reference for as long as the panic hook is set. + _client: Arc, +} + +impl ServerPanicHookHandler { + fn new(client: Client) -> Self { + let hook = std::panic::take_hook(); + let client = Arc::new(client); + + // Use a weak reference to the client because it must be dropped when exiting or the + // io-threads join hangs forever (because client has a reference to the connection sender). + let hook_client = Arc::downgrade(&client); + + // When we panic, try to notify the client. + std::panic::set_hook(Box::new(move |panic_info| { + use std::io::Write; + + let backtrace = std::backtrace::Backtrace::force_capture(); + tracing::error!("{panic_info}\n{backtrace}"); + + // we also need to print to stderr directly for when using `$logTrace` because + // the message won't be sent to the client. + // But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken. + let mut stderr = std::io::stderr().lock(); + writeln!(stderr, "{panic_info}\n{backtrace}").ok(); + + if let Some(client) = hook_client.upgrade() { + client + .show_message( + "The Ruff language server exited with a panic. See the logs for more details.", + MessageType::ERROR, + ) + .ok(); + } + })); + + Self { + hook: Some(hook), + _client: client, + } + } +} + +impl Drop for ServerPanicHookHandler { + fn drop(&mut self) { + if std::thread::panicking() { + // Calling `std::panic::set_hook` while panicking results in a panic. + return; + } + + if let Some(hook) = self.hook.take() { + std::panic::set_hook(hook); + } + } +} diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 7b95ea68e3eac..65967f04ece61 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -1,20 +1,36 @@ -use crate::{server::schedule::Task, session::Session}; -use lsp_server as server; +use std::panic::UnwindSafe; + +use anyhow::anyhow; +use lsp_server::{self as server, RequestId}; +use lsp_types::{notification::Notification, request::Request}; +use notifications as notification; +use requests as request; + +use crate::{ + server::{ + api::traits::{ + BackgroundDocumentNotificationHandler, BackgroundDocumentRequestHandler, + SyncNotificationHandler, + }, + schedule::Task, + }, + session::{Client, Session}, +}; mod diagnostics; mod notifications; mod requests; mod traits; -use notifications as notification; -use requests as request; - use self::traits::{NotificationHandler, RequestHandler}; -use super::{client::Responder, schedule::BackgroundSchedule, Result}; +use super::{Result, schedule::BackgroundSchedule}; -/// Defines the `document_url` method for implementers of [`traits::Notification`] and [`traits::Request`], -/// given the parameter type used by the implementer. +/// Defines the `document_url` method for implementers of [`Notification`] and [`Request`], given +/// the request or notification parameter type. +/// +/// This would only work if the parameter type has a `text_document` field with a `uri` field +/// that is of type [`lsp_types::Url`]. macro_rules! define_document_url { ($params:ident: &$p:ty) => { fn document_url($params: &$p) -> std::borrow::Cow { @@ -25,7 +41,13 @@ macro_rules! define_document_url { use define_document_url; -pub(super) fn request<'a>(req: server::Request) -> Task<'a> { +/// Processes a request from the client to the server. +/// +/// The LSP specification requires that each request has exactly one response. Therefore, +/// it's crucial that all paths in this method call [`Client::respond`] exactly once. +/// The only exception to this is requests that were cancelled by the client. In this case, +/// the response was already sent by the [`notification::CancelNotificationHandler`]. +pub(super) fn request(req: server::Request) -> Task { let id = req.id.clone(); match req.method.as_str() { @@ -38,7 +60,7 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { request::DocumentDiagnostic::METHOD => { background_request_task::(req, BackgroundSchedule::Worker) } - request::ExecuteCommand::METHOD => local_request_task::(req), + request::ExecuteCommand::METHOD => sync_request_task::(req), request::Format::METHOD => { background_request_task::(req, BackgroundSchedule::Fmt) } @@ -48,46 +70,67 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { request::Hover::METHOD => { background_request_task::(req, BackgroundSchedule::Worker) } + lsp_types::request::Shutdown::METHOD => sync_request_task::(req), method => { tracing::warn!("Received request {method} which does not have a handler"); - return Task::nothing(); + let result: Result<()> = Err(Error::new( + anyhow!("Unknown request: {method}"), + server::ErrorCode::MethodNotFound, + )); + return Task::immediate(id, result); } } .unwrap_or_else(|err| { tracing::error!("Encountered error when routing request with ID {id}: {err}"); - show_err_msg!( - "Ruff failed to handle a request from the editor. Check the logs for more details." - ); - let result: Result<()> = Err(err); - Task::immediate(id, result) + + Task::sync(move |_session, client| { + client.show_error_message( + "Ruff failed to handle a request from the editor. Check the logs for more details.", + ); + respond_silent_error( + id, + client, + lsp_server::ResponseError { + code: err.code as i32, + message: err.to_string(), + data: None, + }, + ); + }) }) } -pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { +pub(super) fn notification(notif: server::Notification) -> Task { match notif.method.as_str() { - notification::Cancel::METHOD => local_notification_task::(notif), notification::DidChange::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) } notification::DidChangeConfiguration::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) } notification::DidChangeWatchedFiles::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) } notification::DidChangeWorkspace::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) } - notification::DidClose::METHOD => local_notification_task::(notif), - notification::DidOpen::METHOD => local_notification_task::(notif), + notification::DidClose::METHOD => sync_notification_task::(notif), + notification::DidOpen::METHOD => sync_notification_task::(notif), notification::DidOpenNotebook::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) } notification::DidChangeNotebook::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) } notification::DidCloseNotebook::METHOD => { - local_notification_task::(notif) + sync_notification_task::(notif) + } + lsp_types::notification::Cancel::METHOD => { + sync_notification_task::(notif) + } + lsp_types::notification::SetTrace::METHOD => { + tracing::trace!("Ignoring `setTrace` notification"); + return Task::nothing(); } method => { tracing::warn!("Received notification {method} which does not have a handler."); @@ -96,69 +139,158 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { } .unwrap_or_else(|err| { tracing::error!("Encountered error when routing notification: {err}"); - show_err_msg!("Ruff failed to handle a notification from the editor. Check the logs for more details."); - Task::nothing() + Task::sync(|_session, client| { + client.show_error_message( + "Ruff failed to handle a notification from the editor. Check the logs for more details." + ); + }) }) } -fn local_request_task<'a, R: traits::SyncRequestHandler>( - req: server::Request, -) -> super::Result> { +fn sync_request_task(req: server::Request) -> Result +where + <::RequestType as Request>::Params: UnwindSafe, +{ let (id, params) = cast_request::(req)?; - Ok(Task::local(|session, notifier, requester, responder| { - let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered(); - let result = R::run(session, notifier, requester, params); - respond::(id, result, &responder); + Ok(Task::sync(move |session, client: &Client| { + let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); + let result = R::run(session, client, params); + respond::(&id, result, client); })) } -fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( +fn background_request_task( req: server::Request, schedule: BackgroundSchedule, -) -> super::Result> { +) -> Result +where + <::RequestType as Request>::Params: UnwindSafe, +{ let (id, params) = cast_request::(req)?; + Ok(Task::background(schedule, move |session: &Session| { - // TODO(jane): we should log an error if we can't take a snapshot. + let cancellation_token = session + .request_queue() + .incoming() + .cancellation_token(&id) + .expect("request should have been tested for cancellation before scheduling"); + + let url = R::document_url(¶ms).into_owned(); + let Some(snapshot) = session.take_snapshot(R::document_url(¶ms).into_owned()) else { - return Box::new(|_, _| {}); + tracing::warn!("Ignoring request because snapshot for path `{url:?}` doesn't exist."); + return Box::new(|_| {}); }; - Box::new(move |notifier, responder| { - let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered(); - let result = R::run_with_snapshot(snapshot, notifier, params); - respond::(id, result, &responder); + + Box::new(move |client| { + let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); + + // Test again if the request was cancelled since it was scheduled on the background task + // and, if so, return early + if cancellation_token.is_cancelled() { + tracing::trace!( + "Ignoring request id={id} method={} because it was cancelled", + R::METHOD + ); + + // We don't need to send a response here because the `cancel` notification + // handler already responded with a message. + return; + } + + let result = + std::panic::catch_unwind(|| R::run_with_snapshot(snapshot, client, params)); + + let response = request_result_to_response::(result); + respond::(&id, response, client); }) })) } -fn local_notification_task<'a, N: traits::SyncNotificationHandler>( - notif: server::Notification, -) -> super::Result> { +fn request_result_to_response( + result: std::result::Result< + Result<<::RequestType as Request>::Result>, + Box, + >, +) -> Result<<::RequestType as Request>::Result> +where + R: BackgroundDocumentRequestHandler, +{ + match result { + Ok(response) => response, + + Err(error) => { + let message = if let Some(panic_message) = panic_message(&error) { + format!("Request handler failed with: {panic_message}") + } else { + "Request handler failed".into() + }; + + Err(Error { + code: lsp_server::ErrorCode::InternalError, + error: anyhow!(message), + }) + } + } +} + +fn sync_notification_task(notif: server::Notification) -> Result { let (id, params) = cast_notification::(notif)?; - Ok(Task::local(move |session, notifier, requester, _| { - let _span = tracing::trace_span!("notification", method = N::METHOD).entered(); - if let Err(err) = N::run(session, notifier, requester, params) { + Ok(Task::sync(move |session, client| { + let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); + if let Err(err) = N::run(session, client, params) { tracing::error!("An error occurred while running {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + client + .show_error_message("Ruff encountered a problem. Check the logs for more details."); } })) } -#[allow(dead_code)] -fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>( +#[expect(dead_code)] +fn background_notification_thread( req: server::Notification, schedule: BackgroundSchedule, -) -> super::Result> { +) -> Result +where + N: BackgroundDocumentNotificationHandler, + <::NotificationType as Notification>::Params: UnwindSafe, +{ let (id, params) = cast_notification::(req)?; Ok(Task::background(schedule, move |session: &Session| { - // TODO(jane): we should log an error if we can't take a snapshot. - let Some(snapshot) = session.take_snapshot(N::document_url(¶ms).into_owned()) else { - return Box::new(|_, _| {}); + let url = N::document_url(¶ms); + + let Some(snapshot) = session.take_snapshot((*url).clone()) else { + tracing::debug!( + "Ignoring notification because snapshot for url `{url}` doesn't exist." + ); + return Box::new(|_| {}); }; - Box::new(move |notifier, _| { - let _span = tracing::trace_span!("notification", method = N::METHOD).entered(); - if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) { + Box::new(move |client| { + let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); + + let result = + match std::panic::catch_unwind(|| N::run_with_snapshot(snapshot, client, params)) { + Ok(result) => result, + Err(panic) => { + let message = if let Some(panic_message) = panic_message(&panic) { + format!("notification handler for {id} failed with: {panic_message}") + } else { + format!("notification handler for {id} failed") + }; + + tracing::error!(message); + client.show_error_message( + "Ruff encountered a panic. Check the logs for more details.", + ); + return; + } + }; + + if let Err(err) = result { tracing::error!("An error occurred while running {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + client.show_error_message( + "Ruff encountered a problem. Check the logs for more details.", + ); } }) })) @@ -170,12 +302,13 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH /// implementation. fn cast_request( request: server::Request, -) -> super::Result<( - server::RequestId, - <::RequestType as lsp_types::request::Request>::Params, +) -> Result<( + RequestId, + <::RequestType as Request>::Params, )> where - Req: traits::RequestHandler, + Req: RequestHandler, + <::RequestType as Request>::Params: UnwindSafe, { request .extract(Req::METHOD) @@ -191,21 +324,27 @@ where .with_failure_code(server::ErrorCode::InternalError) } -/// Sends back a response to the server using a [`Responder`]. +/// Sends back a response to the server, but only if the request wasn't cancelled. fn respond( - id: server::RequestId, - result: crate::server::Result< - <::RequestType as lsp_types::request::Request>::Result, - >, - responder: &Responder, + id: &RequestId, + result: Result<<::RequestType as Request>::Result>, + client: &Client, ) where - Req: traits::RequestHandler, + Req: RequestHandler, { if let Err(err) = &result { tracing::error!("An error occurred with request ID {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + client.show_error_message("Ruff encountered a problem. Check the logs for more details."); } - if let Err(err) = responder.respond(id, result) { + if let Err(err) = client.respond(id, result) { + tracing::error!("Failed to send response: {err}"); + } +} + +/// Sends back an error response to the server using a [`Client`] without showing a warning +/// to the user. +fn respond_silent_error(id: RequestId, client: &Client, error: lsp_server::ResponseError) { + if let Err(err) = client.respond_err(id, error) { tracing::error!("Failed to send response: {err}"); } } @@ -214,11 +353,13 @@ fn respond( /// a parameter type for a specific request handler. fn cast_notification( notification: server::Notification, -) -> super::Result< - ( - &'static str, - <::NotificationType as lsp_types::notification::Notification>::Params, -)> where N: traits::NotificationHandler{ +) -> Result<( + &'static str, + <::NotificationType as Notification>::Params, +)> +where + N: NotificationHandler, +{ Ok(( N::METHOD, notification @@ -271,3 +412,15 @@ impl std::fmt::Display for Error { self.error.fmt(f) } } + +fn panic_message<'a>( + err: &'a Box, +) -> Option> { + if let Some(s) = err.downcast_ref::() { + Some(s.into()) + } else if let Some(&s) = err.downcast_ref::<&str>() { + Some(s.into()) + } else { + None + } +} diff --git a/crates/ruff_server/src/server/api/diagnostics.rs b/crates/ruff_server/src/server/api/diagnostics.rs index 5f0b9f468d6e3..6f8efe47e83b4 100644 --- a/crates/ruff_server/src/server/api/diagnostics.rs +++ b/crates/ruff_server/src/server/api/diagnostics.rs @@ -1,7 +1,6 @@ use crate::{ lint::DiagnosticsMap, - server::client::Notifier, - session::{DocumentQuery, DocumentSnapshot}, + session::{Client, DocumentQuery, DocumentSnapshot}, }; use super::LSPResult; @@ -21,11 +20,11 @@ pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> DiagnosticsMa pub(super) fn publish_diagnostics_for_document( snapshot: &DocumentSnapshot, - notifier: &Notifier, + client: &Client, ) -> crate::server::Result<()> { for (uri, diagnostics) in generate_diagnostics(snapshot) { - notifier - .notify::( + client + .send_notification::( lsp_types::PublishDiagnosticsParams { uri, diagnostics, @@ -40,10 +39,10 @@ pub(super) fn publish_diagnostics_for_document( pub(super) fn clear_diagnostics_for_document( query: &DocumentQuery, - notifier: &Notifier, + client: &Client, ) -> crate::server::Result<()> { - notifier - .notify::( + client + .send_notification::( lsp_types::PublishDiagnosticsParams { uri: query.make_key().into_url(), diagnostics: vec![], diff --git a/crates/ruff_server/src/server/api/notifications.rs b/crates/ruff_server/src/server/api/notifications.rs index ade0c2fbd510f..d9a473d3fe993 100644 --- a/crates/ruff_server/src/server/api/notifications.rs +++ b/crates/ruff_server/src/server/api/notifications.rs @@ -10,7 +10,8 @@ mod did_open; mod did_open_notebook; use super::traits::{NotificationHandler, SyncNotificationHandler}; -pub(super) use cancel::Cancel; + +pub(super) use cancel::CancelNotificationHandler; pub(super) use did_change::DidChange; pub(super) use did_change_configuration::DidChangeConfiguration; pub(super) use did_change_notebook::DidChangeNotebook; diff --git a/crates/ruff_server/src/server/api/notifications/cancel.rs b/crates/ruff_server/src/server/api/notifications/cancel.rs index d9d011c3f7b32..85553866783de 100644 --- a/crates/ruff_server/src/server/api/notifications/cancel.rs +++ b/crates/ruff_server/src/server/api/notifications/cancel.rs @@ -1,23 +1,26 @@ -use crate::server::client::{Notifier, Requester}; +use lsp_server::RequestId; +use lsp_types::CancelParams; +use lsp_types::notification::Cancel; + use crate::server::Result; -use crate::session::Session; -use lsp_types as types; -use lsp_types::notification as notif; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::{Client, Session}; -pub(crate) struct Cancel; +pub(crate) struct CancelNotificationHandler; -impl super::NotificationHandler for Cancel { - type NotificationType = notif::Cancel; +impl NotificationHandler for CancelNotificationHandler { + type NotificationType = Cancel; } -impl super::SyncNotificationHandler for Cancel { - fn run( - _session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, - _params: types::CancelParams, - ) -> Result<()> { - // TODO(jane): Handle this once we have task cancellation in the scheduler. +impl SyncNotificationHandler for CancelNotificationHandler { + fn run(session: &mut Session, client: &Client, params: CancelParams) -> Result<()> { + let id: RequestId = match params.id { + lsp_types::NumberOrString::Number(id) => id.into(), + lsp_types::NumberOrString::String(id) => id.into(), + }; + + let _ = client.cancel(session, id); + Ok(()) } } diff --git a/crates/ruff_server/src/server/api/notifications/did_change.rs b/crates/ruff_server/src/server/api/notifications/did_change.rs index 6dd8253f941cc..8e77cb593fadf 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change.rs @@ -1,8 +1,7 @@ -use crate::server::api::diagnostics::publish_diagnostics_for_document; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::publish_diagnostics_for_document; +use crate::session::{Client, Session}; use lsp_server::ErrorCode; use lsp_types as types; use lsp_types::notification as notif; @@ -16,8 +15,7 @@ impl super::NotificationHandler for DidChange { impl super::SyncNotificationHandler for DidChange { fn run( session: &mut Session, - notifier: Notifier, - _requester: &mut Requester, + client: &Client, types::DidChangeTextDocumentParams { text_document: types::VersionedTextDocumentIdentifier { @@ -36,7 +34,7 @@ impl super::SyncNotificationHandler for DidChange { // Publish diagnostics if the client doesn't support pull diagnostics if !session.resolved_client_capabilities().pull_diagnostics { let snapshot = session.take_snapshot(key.into_url()).unwrap(); - publish_diagnostics_for_document(&snapshot, ¬ifier)?; + publish_diagnostics_for_document(&snapshot, client)?; } Ok(()) diff --git a/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs b/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs index 9a2e5af99c739..34982ec65659e 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs @@ -1,6 +1,5 @@ -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::session::{Client, Session}; use lsp_types as types; use lsp_types::notification as notif; @@ -13,8 +12,7 @@ impl super::NotificationHandler for DidChangeConfiguration { impl super::SyncNotificationHandler for DidChangeConfiguration { fn run( _session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, + _client: &Client, _params: types::DidChangeConfigurationParams, ) -> Result<()> { // TODO(jane): get this wired up after the pre-release diff --git a/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs index 97f12f4e1798b..d092ccacb8528 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_notebook.rs @@ -1,8 +1,7 @@ -use crate::server::api::diagnostics::publish_diagnostics_for_document; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::publish_diagnostics_for_document; +use crate::session::{Client, Session}; use lsp_server::ErrorCode; use lsp_types as types; use lsp_types::notification as notif; @@ -16,8 +15,7 @@ impl super::NotificationHandler for DidChangeNotebook { impl super::SyncNotificationHandler for DidChangeNotebook { fn run( session: &mut Session, - notifier: Notifier, - _requester: &mut Requester, + client: &Client, types::DidChangeNotebookDocumentParams { notebook_document: types::VersionedNotebookDocumentIdentifier { uri, version }, change: types::NotebookDocumentChangeEvent { cells, metadata }, @@ -32,7 +30,7 @@ impl super::SyncNotificationHandler for DidChangeNotebook { let snapshot = session .take_snapshot(key.into_url()) .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, ¬ifier)?; + publish_diagnostics_for_document(&snapshot, client)?; Ok(()) } diff --git a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs index b555917f141d5..bc97231411dd1 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs @@ -1,9 +1,7 @@ -use crate::server::api::diagnostics::publish_diagnostics_for_document; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; -use crate::server::schedule::Task; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::publish_diagnostics_for_document; +use crate::session::{Client, Session}; use lsp_types as types; use lsp_types::notification as notif; @@ -16,16 +14,19 @@ impl super::NotificationHandler for DidChangeWatchedFiles { impl super::SyncNotificationHandler for DidChangeWatchedFiles { fn run( session: &mut Session, - notifier: Notifier, - requester: &mut Requester, + client: &Client, params: types::DidChangeWatchedFilesParams, ) -> Result<()> { - session.reload_settings(¶ms.changes); + session.reload_settings(¶ms.changes, client); if !params.changes.is_empty() { if session.resolved_client_capabilities().workspace_refresh { - requester - .request::((), |()| Task::nothing()) + client + .send_request::( + session, + (), + |_, ()| (), + ) .with_failure_code(lsp_server::ErrorCode::InternalError)?; } else { // publish diagnostics for text documents @@ -33,7 +34,7 @@ impl super::SyncNotificationHandler for DidChangeWatchedFiles { let snapshot = session .take_snapshot(url.clone()) .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, ¬ifier)?; + publish_diagnostics_for_document(&snapshot, client)?; } } @@ -42,7 +43,7 @@ impl super::SyncNotificationHandler for DidChangeWatchedFiles { let snapshot = session .take_snapshot(url.clone()) .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, ¬ifier)?; + publish_diagnostics_for_document(&snapshot, client)?; } } diff --git a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs index a10b376a64de6..a121d1b2b4e96 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs @@ -1,7 +1,6 @@ -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::session::{Client, Session}; use lsp_types as types; use lsp_types::notification as notif; @@ -14,13 +13,12 @@ impl super::NotificationHandler for DidChangeWorkspace { impl super::SyncNotificationHandler for DidChangeWorkspace { fn run( session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, + client: &Client, params: types::DidChangeWorkspaceFoldersParams, ) -> Result<()> { for types::WorkspaceFolder { uri, .. } in params.event.added { session - .open_workspace_folder(uri) + .open_workspace_folder(uri, client) .with_failure_code(lsp_server::ErrorCode::InvalidParams)?; } for types::WorkspaceFolder { uri, .. } in params.event.removed { diff --git a/crates/ruff_server/src/server/api/notifications/did_close.rs b/crates/ruff_server/src/server/api/notifications/did_close.rs index 491fa06c3d0ae..a3075a4846b87 100644 --- a/crates/ruff_server/src/server/api/notifications/did_close.rs +++ b/crates/ruff_server/src/server/api/notifications/did_close.rs @@ -1,8 +1,7 @@ -use crate::server::api::diagnostics::clear_diagnostics_for_document; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::clear_diagnostics_for_document; +use crate::session::{Client, Session}; use lsp_types as types; use lsp_types::notification as notif; @@ -15,8 +14,7 @@ impl super::NotificationHandler for DidClose { impl super::SyncNotificationHandler for DidClose { fn run( session: &mut Session, - notifier: Notifier, - _requester: &mut Requester, + client: &Client, types::DidCloseTextDocumentParams { text_document: types::TextDocumentIdentifier { uri }, }: types::DidCloseTextDocumentParams, @@ -29,7 +27,7 @@ impl super::SyncNotificationHandler for DidClose { ); return Ok(()); }; - clear_diagnostics_for_document(snapshot.query(), ¬ifier)?; + clear_diagnostics_for_document(snapshot.query(), client)?; session .close_document(&key) diff --git a/crates/ruff_server/src/server/api/notifications/did_close_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_close_notebook.rs index 913ccf5d4a234..b70993b5e1225 100644 --- a/crates/ruff_server/src/server/api/notifications/did_close_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_close_notebook.rs @@ -1,7 +1,6 @@ -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::session::{Client, Session}; use lsp_types::notification as notif; use lsp_types::{self as types, NotebookDocumentIdentifier}; @@ -14,8 +13,7 @@ impl super::NotificationHandler for DidCloseNotebook { impl super::SyncNotificationHandler for DidCloseNotebook { fn run( session: &mut Session, - _notifier: Notifier, - _requester: &mut Requester, + _client: &Client, types::DidCloseNotebookDocumentParams { notebook_document: NotebookDocumentIdentifier { uri }, .. diff --git a/crates/ruff_server/src/server/api/notifications/did_open.rs b/crates/ruff_server/src/server/api/notifications/did_open.rs index 5e3e08ec988b0..41a6fb6cf8d13 100644 --- a/crates/ruff_server/src/server/api/notifications/did_open.rs +++ b/crates/ruff_server/src/server/api/notifications/did_open.rs @@ -1,9 +1,8 @@ -use crate::server::api::diagnostics::publish_diagnostics_for_document; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; -use crate::server::Result; -use crate::session::Session; use crate::TextDocument; +use crate::server::Result; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::publish_diagnostics_for_document; +use crate::session::{Client, Session}; use lsp_types as types; use lsp_types::notification as notif; @@ -16,8 +15,7 @@ impl super::NotificationHandler for DidOpen { impl super::SyncNotificationHandler for DidOpen { fn run( session: &mut Session, - notifier: Notifier, - _requester: &mut Requester, + client: &Client, types::DidOpenTextDocumentParams { text_document: types::TextDocumentItem { @@ -40,7 +38,7 @@ impl super::SyncNotificationHandler for DidOpen { anyhow::anyhow!("Unable to take snapshot for document with URL {uri}") }) .with_failure_code(lsp_server::ErrorCode::InternalError)?; - publish_diagnostics_for_document(&snapshot, ¬ifier)?; + publish_diagnostics_for_document(&snapshot, client)?; } Ok(()) diff --git a/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs b/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs index 4cd21c35fce2a..a75e88ecc5398 100644 --- a/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs +++ b/crates/ruff_server/src/server/api/notifications/did_open_notebook.rs @@ -1,9 +1,8 @@ use crate::edit::NotebookDocument; -use crate::server::api::diagnostics::publish_diagnostics_for_document; -use crate::server::api::LSPResult; -use crate::server::client::{Notifier, Requester}; use crate::server::Result; -use crate::session::Session; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::publish_diagnostics_for_document; +use crate::session::{Client, Session}; use lsp_server::ErrorCode; use lsp_types as types; use lsp_types::notification as notif; @@ -17,8 +16,7 @@ impl super::NotificationHandler for DidOpenNotebook { impl super::SyncNotificationHandler for DidOpenNotebook { fn run( session: &mut Session, - notifier: Notifier, - _requester: &mut Requester, + client: &Client, types::DidOpenNotebookDocumentParams { notebook_document: types::NotebookDocument { @@ -45,7 +43,7 @@ impl super::SyncNotificationHandler for DidOpenNotebook { let snapshot = session .take_snapshot(uri) .expect("snapshot should be available"); - publish_diagnostics_for_document(&snapshot, ¬ifier)?; + publish_diagnostics_for_document(&snapshot, client)?; Ok(()) } diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index 049f396f639f2..198ce4fe61e71 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -5,6 +5,7 @@ mod execute_command; mod format; mod format_range; mod hover; +mod shutdown; use super::{ define_document_url, @@ -17,5 +18,6 @@ pub(super) use execute_command::ExecuteCommand; pub(super) use format::Format; pub(super) use format_range::FormatRange; pub(super) use hover::Hover; +pub(super) use shutdown::ShutdownHandler; type FormatResponse = Option>; diff --git a/crates/ruff_server/src/server/api/requests/code_action.rs b/crates/ruff_server/src/server/api/requests/code_action.rs index ca43ed6cd52d7..b39543a773080 100644 --- a/crates/ruff_server/src/server/api/requests/code_action.rs +++ b/crates/ruff_server/src/server/api/requests/code_action.rs @@ -3,13 +3,13 @@ use lsp_types::{self as types, request as req}; use rustc_hash::FxHashSet; use types::{CodeActionKind, CodeActionOrCommand}; +use crate::DIAGNOSTIC_NAME; use crate::edit::WorkspaceEditTracker; -use crate::lint::{fixes_for_diagnostics, DiagnosticFix}; -use crate::server::api::LSPResult; +use crate::lint::{DiagnosticFix, fixes_for_diagnostics}; +use crate::server::Result; use crate::server::SupportedCodeAction; -use crate::server::{client::Notifier, Result}; -use crate::session::DocumentSnapshot; -use crate::DIAGNOSTIC_NAME; +use crate::server::api::LSPResult; +use crate::session::{Client, DocumentSnapshot}; use super::code_action_resolve::{resolve_edit_for_fix_all, resolve_edit_for_organize_imports}; @@ -23,7 +23,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { super::define_document_url!(params: &types::CodeActionParams); fn run_with_snapshot( snapshot: DocumentSnapshot, - _notifier: Notifier, + _client: &Client, params: types::CodeActionParams, ) -> Result> { let mut response: types::CodeActionResponse = types::CodeActionResponse::default(); diff --git a/crates/ruff_server/src/server/api/requests/code_action_resolve.rs b/crates/ruff_server/src/server/api/requests/code_action_resolve.rs index ed7e3c22b4da8..ea5691c9d0669 100644 --- a/crates/ruff_server/src/server/api/requests/code_action_resolve.rs +++ b/crates/ruff_server/src/server/api/requests/code_action_resolve.rs @@ -5,13 +5,14 @@ use lsp_types::{self as types, request as req}; use ruff_linter::codes::Rule; +use crate::PositionEncoding; use crate::edit::WorkspaceEditTracker; use crate::fix::Fixes; -use crate::server::api::LSPResult; +use crate::server::Result; use crate::server::SupportedCodeAction; -use crate::server::{client::Notifier, Result}; +use crate::server::api::LSPResult; +use crate::session::Client; use crate::session::{DocumentQuery, DocumentSnapshot, ResolvedClientCapabilities}; -use crate::PositionEncoding; pub(crate) struct CodeActionResolve; @@ -27,7 +28,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve { } fn run_with_snapshot( snapshot: DocumentSnapshot, - _notifier: Notifier, + _client: &Client, mut action: types::CodeAction, ) -> Result { let query = snapshot.query(); diff --git a/crates/ruff_server/src/server/api/requests/diagnostic.rs b/crates/ruff_server/src/server/api/requests/diagnostic.rs index e3a4fd0f4a78b..5315ea931bc05 100644 --- a/crates/ruff_server/src/server/api/requests/diagnostic.rs +++ b/crates/ruff_server/src/server/api/requests/diagnostic.rs @@ -1,6 +1,6 @@ use crate::server::api::diagnostics::generate_diagnostics; -use crate::server::{client::Notifier, Result}; use crate::session::DocumentSnapshot; +use crate::{server::Result, session::Client}; use lsp_types::{self as types, request as req}; use types::{ DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, @@ -17,7 +17,7 @@ impl super::BackgroundDocumentRequestHandler for DocumentDiagnostic { super::define_document_url!(params: &types::DocumentDiagnosticParams); fn run_with_snapshot( snapshot: DocumentSnapshot, - _notifier: Notifier, + _client: &Client, _params: types::DocumentDiagnosticParams, ) -> Result { Ok(DocumentDiagnosticReportResult::Report( diff --git a/crates/ruff_server/src/server/api/requests/execute_command.rs b/crates/ruff_server/src/server/api/requests/execute_command.rs index 2888e1c85e165..1303e0ee1451b 100644 --- a/crates/ruff_server/src/server/api/requests/execute_command.rs +++ b/crates/ruff_server/src/server/api/requests/execute_command.rs @@ -2,14 +2,13 @@ use std::fmt::Write; use std::str::FromStr; use crate::edit::WorkspaceEditTracker; +use crate::server::SupportedCommand; use crate::server::api::LSPResult; -use crate::server::schedule::Task; -use crate::server::{client, SupportedCommand}; -use crate::session::Session; +use crate::session::{Client, Session}; +use crate::{DIAGNOSTIC_NAME, DocumentKey}; use crate::{edit::DocumentVersion, server}; -use crate::{DocumentKey, DIAGNOSTIC_NAME}; use lsp_server::ErrorCode; -use lsp_types::{self as types, request as req, TextDocumentIdentifier}; +use lsp_types::{self as types, TextDocumentIdentifier, request as req}; use serde::Deserialize; pub(crate) struct ExecuteCommand; @@ -38,8 +37,7 @@ impl super::RequestHandler for ExecuteCommand { impl super::SyncRequestHandler for ExecuteCommand { fn run( session: &mut Session, - _notifier: client::Notifier, - requester: &mut client::Requester, + client: &Client, params: types::ExecuteCommandParams, ) -> server::Result> { let command = SupportedCommand::from_str(¶ms.command) @@ -76,7 +74,7 @@ impl super::SyncRequestHandler for ExecuteCommand { for Argument { uri, version } in arguments { let Some(snapshot) = session.take_snapshot(uri.clone()) else { tracing::error!("Document at {uri} could not be opened"); - show_err_msg!("Ruff does not recognize this file"); + client.show_error_message("Ruff does not recognize this file"); return Ok(None); }; match command { @@ -114,7 +112,8 @@ impl super::SyncRequestHandler for ExecuteCommand { if !edit_tracker.is_empty() { apply_edit( - requester, + session, + client, command.label(), edit_tracker.into_workspace_edit(), ) @@ -126,24 +125,25 @@ impl super::SyncRequestHandler for ExecuteCommand { } fn apply_edit( - requester: &mut client::Requester, + session: &mut Session, + client: &Client, label: &str, edit: types::WorkspaceEdit, ) -> crate::Result<()> { - requester.request::( + client.send_request::( + session, types::ApplyWorkspaceEditParams { label: Some(format!("{DIAGNOSTIC_NAME}: {label}")), edit, }, - |response| { + move |client, response| { if !response.applied { let reason = response .failure_reason .unwrap_or_else(|| String::from("unspecified reason")); tracing::error!("Failed to apply workspace edit: {reason}"); - show_err_msg!("Ruff was unable to apply edits: {reason}"); + client.show_error_message(format_args!("Ruff was unable to apply edits: {reason}")); } - Task::nothing() }, ) } diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index e54f02460181f..9066111217a5f 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -7,9 +7,9 @@ use ruff_source_file::LineIndex; use crate::edit::{Replacement, ToRangeExt}; use crate::fix::Fixes; use crate::resolve::is_document_excluded_for_formatting; +use crate::server::Result; use crate::server::api::LSPResult; -use crate::server::{client::Notifier, Result}; -use crate::session::{DocumentQuery, DocumentSnapshot}; +use crate::session::{Client, DocumentQuery, DocumentSnapshot}; use crate::{PositionEncoding, TextDocument}; pub(crate) struct Format; @@ -22,7 +22,7 @@ impl super::BackgroundDocumentRequestHandler for Format { super::define_document_url!(params: &types::DocumentFormattingParams); fn run_with_snapshot( snapshot: DocumentSnapshot, - _notifier: Notifier, + _client: &Client, _params: types::DocumentFormattingParams, ) -> Result { format_document(&snapshot) @@ -83,18 +83,16 @@ fn format_text_document( is_notebook: bool, ) -> Result { let settings = query.settings(); + let file_path = query.virtual_file_path(); // If the document is excluded, return early. - let file_path = query.file_path(); - if let Some(file_path) = &file_path { - if is_document_excluded_for_formatting( - file_path, - &settings.file_resolver, - &settings.formatter, - text_document.language_id(), - ) { - return Ok(None); - } + if is_document_excluded_for_formatting( + &file_path, + &settings.file_resolver, + &settings.formatter, + text_document.language_id(), + ) { + return Ok(None); } let source = text_document.contents(); @@ -102,7 +100,7 @@ fn format_text_document( text_document, query.source_type(), &settings.formatter, - file_path.as_deref(), + &file_path, ) .with_failure_code(lsp_server::ErrorCode::InternalError)?; let Some(mut formatted) = formatted else { diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index 72edf77e4d28d..4e4cddae1a9ce 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -1,11 +1,11 @@ use anyhow::Context; -use lsp_types::{self as types, request as req, Range}; +use lsp_types::{self as types, Range, request as req}; use crate::edit::{RangeExt, ToRangeExt}; use crate::resolve::is_document_excluded_for_formatting; +use crate::server::Result; use crate::server::api::LSPResult; -use crate::server::{client::Notifier, Result}; -use crate::session::{DocumentQuery, DocumentSnapshot}; +use crate::session::{Client, DocumentQuery, DocumentSnapshot}; use crate::{PositionEncoding, TextDocument}; pub(crate) struct FormatRange; @@ -18,7 +18,7 @@ impl super::BackgroundDocumentRequestHandler for FormatRange { super::define_document_url!(params: &types::DocumentRangeFormattingParams); fn run_with_snapshot( snapshot: DocumentSnapshot, - _notifier: Notifier, + _client: &Client, params: types::DocumentRangeFormattingParams, ) -> Result { format_document_range(&snapshot, params.range) @@ -47,18 +47,16 @@ fn format_text_document_range( encoding: PositionEncoding, ) -> Result { let settings = query.settings(); + let file_path = query.virtual_file_path(); // If the document is excluded, return early. - let file_path = query.file_path(); - if let Some(file_path) = &file_path { - if is_document_excluded_for_formatting( - file_path, - &settings.file_resolver, - &settings.formatter, - text_document.language_id(), - ) { - return Ok(None); - } + if is_document_excluded_for_formatting( + &file_path, + &settings.file_resolver, + &settings.formatter, + text_document.language_id(), + ) { + return Ok(None); } let text = text_document.contents(); @@ -69,7 +67,7 @@ fn format_text_document_range( query.source_type(), &settings.formatter, range, - file_path.as_deref(), + &file_path, ) .with_failure_code(lsp_server::ErrorCode::InternalError)?; diff --git a/crates/ruff_server/src/server/api/requests/hover.rs b/crates/ruff_server/src/server/api/requests/hover.rs index 02e698ccd4466..266234e868ed3 100644 --- a/crates/ruff_server/src/server/api/requests/hover.rs +++ b/crates/ruff_server/src/server/api/requests/hover.rs @@ -1,9 +1,9 @@ -use crate::server::{client::Notifier, Result}; -use crate::session::DocumentSnapshot; +use crate::server::Result; +use crate::session::{Client, DocumentSnapshot}; use anyhow::Context; use lsp_types::{self as types, request as req}; use regex::Regex; -use ruff_diagnostics::FixAvailability; +use ruff_linter::FixAvailability; use ruff_linter::registry::{Linter, Rule, RuleNamespace}; use ruff_source_file::OneIndexed; use std::fmt::Write; @@ -20,7 +20,7 @@ impl super::BackgroundDocumentRequestHandler for Hover { } fn run_with_snapshot( snapshot: DocumentSnapshot, - _notifier: Notifier, + _client: &Client, params: types::HoverParams, ) -> Result> { Ok(hover(&snapshot, ¶ms.text_document_position_params)) @@ -85,7 +85,7 @@ pub(crate) fn hover( fn format_rule_text(rule: Rule) -> String { let mut output = String::new(); - let _ = write!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code()); + let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code()); output.push('\n'); output.push('\n'); diff --git a/crates/ruff_server/src/server/api/requests/shutdown.rs b/crates/ruff_server/src/server/api/requests/shutdown.rs new file mode 100644 index 0000000000000..5ec89c79329cd --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/shutdown.rs @@ -0,0 +1,17 @@ +use crate::Session; +use crate::server::api::traits::{RequestHandler, SyncRequestHandler}; +use crate::session::Client; + +pub(crate) struct ShutdownHandler; + +impl RequestHandler for ShutdownHandler { + type RequestType = lsp_types::request::Shutdown; +} + +impl SyncRequestHandler for ShutdownHandler { + fn run(session: &mut Session, _client: &Client, _params: ()) -> crate::server::Result<()> { + tracing::debug!("Received shutdown request, waiting for shutdown notification"); + session.set_shutdown_requested(true); + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/traits.rs b/crates/ruff_server/src/server/api/traits.rs index 3c8e56b7a2b06..0003ea4f27692 100644 --- a/crates/ruff_server/src/server/api/traits.rs +++ b/crates/ruff_server/src/server/api/traits.rs @@ -1,7 +1,33 @@ -//! A stateful LSP implementation that calls into the Ruff API. +//! Traits for handling requests and notifications from the LSP client. +//! +//! This module defines the trait abstractions used by the language server to handle incoming +//! requests and notifications from clients. It provides a type-safe way to implement LSP handlers +//! with different execution models (synchronous or asynchronous) and automatic retry capabilities. +//! +//! All request and notification handlers must implement the base traits [`RequestHandler`] and +//! [`NotificationHandler`], respectively, which associate them with specific LSP request or +//! notification types. These base traits are then extended by more specific traits that define +//! the execution model of the handler. +//! +//! The [`SyncRequestHandler`] and [`SyncNotificationHandler`] traits are for handlers that +//! executes synchronously on the main loop, providing mutable access to the [`Session`] that +//! contains the current state of the server. This is useful for handlers that need to modify +//! the server state such as when the content of a file changes. +//! +//! The [`BackgroundDocumentRequestHandler`] and [`BackgroundDocumentNotificationHandler`] traits +//! are for handlers that operate on a single document and can be executed on a background thread. +//! These handlers will have access to a snapshot of the document at the time of the request or +//! notification, allowing them to perform operations without blocking the main loop. +//! +//! The [`SyncNotificationHandler`] is the most common trait that would be used because most +//! notifications are specific to a single document and require updating the server state. +//! Similarly, the [`BackgroundDocumentRequestHandler`] is the most common request handler that +//! would be used as most requests are document-specific and can be executed in the background. +//! +//! See the `./requests` and `./notifications` directories for concrete implementations of these +//! traits in action. -use crate::server::client::{Notifier, Requester}; -use crate::session::{DocumentSnapshot, Session}; +use crate::session::{Client, DocumentSnapshot, Session}; use lsp_types::notification::Notification as LSPNotification; use lsp_types::request::Request; @@ -13,30 +39,34 @@ pub(super) trait RequestHandler { } /// A request handler that needs mutable access to the session. -/// This will block the main message receiver loop, meaning that no -/// incoming requests or notifications will be handled while `run` is -/// executing. Try to avoid doing any I/O or long-running computations. +/// +/// This will block the main message receiver loop, meaning that no incoming requests or +/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or +/// long-running computations. pub(super) trait SyncRequestHandler: RequestHandler { fn run( session: &mut Session, - notifier: Notifier, - requester: &mut Requester, + client: &Client, params: <::RequestType as Request>::Params, ) -> super::Result<<::RequestType as Request>::Result>; } /// A request handler that can be run on a background thread. +/// +/// This handler is specific to requests that operate on a single document. pub(super) trait BackgroundDocumentRequestHandler: RequestHandler { - /// `document_url` can be implemented automatically with - /// `define_document_url!(params: &)` in the trait - /// implementation. + /// Returns the URL of the document that this request handler operates on. + /// + /// This method can be implemented automatically using the [`define_document_url`] macro. + /// + /// [`define_document_url`]: super::define_document_url fn document_url( params: &<::RequestType as Request>::Params, ) -> std::borrow::Cow; fn run_with_snapshot( snapshot: DocumentSnapshot, - notifier: Notifier, + client: &Client, params: <::RequestType as Request>::Params, ) -> super::Result<<::RequestType as Request>::Result>; } @@ -49,30 +79,32 @@ pub(super) trait NotificationHandler { } /// A notification handler that needs mutable access to the session. -/// This will block the main message receiver loop, meaning that no -/// incoming requests or notifications will be handled while `run` is -/// executing. Try to avoid doing any I/O or long-running computations. +/// +/// This will block the main message receiver loop, meaning that no incoming requests or +/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or +/// long-running computations. pub(super) trait SyncNotificationHandler: NotificationHandler { fn run( session: &mut Session, - notifier: Notifier, - requester: &mut Requester, + client: &Client, params: <::NotificationType as LSPNotification>::Params, ) -> super::Result<()>; } /// A notification handler that can be run on a background thread. pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler { - /// `document_url` can be implemented automatically with - /// `define_document_url!(params: &)` in the trait - /// implementation. + /// Returns the URL of the document that this notification handler operates on. + /// + /// This method can be implemented automatically using the [`define_document_url`] macro. + /// + /// [`define_document_url`]: super::define_document_url fn document_url( params: &<::NotificationType as LSPNotification>::Params, ) -> std::borrow::Cow; fn run_with_snapshot( snapshot: DocumentSnapshot, - notifier: Notifier, + client: &Client, params: <::NotificationType as LSPNotification>::Params, ) -> super::Result<()>; } diff --git a/crates/ruff_server/src/server/client.rs b/crates/ruff_server/src/server/client.rs deleted file mode 100644 index c5a502e213c5e..0000000000000 --- a/crates/ruff_server/src/server/client.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::any::TypeId; - -use lsp_server::{Notification, RequestId}; -use rustc_hash::FxHashMap; -use serde_json::Value; - -use super::{schedule::Task, ClientSender}; - -type ResponseBuilder<'s> = Box Task<'s>>; - -pub(crate) struct Client<'s> { - notifier: Notifier, - responder: Responder, - pub(super) requester: Requester<'s>, -} - -#[derive(Clone)] -pub(crate) struct Notifier(ClientSender); - -#[derive(Clone)] -pub(crate) struct Responder(ClientSender); - -pub(crate) struct Requester<'s> { - sender: ClientSender, - next_request_id: i32, - response_handlers: FxHashMap>, -} - -impl Client<'_> { - pub(super) fn new(sender: ClientSender) -> Self { - Self { - notifier: Notifier(sender.clone()), - responder: Responder(sender.clone()), - requester: Requester { - sender, - next_request_id: 1, - response_handlers: FxHashMap::default(), - }, - } - } - - pub(super) fn notifier(&self) -> Notifier { - self.notifier.clone() - } - - pub(super) fn responder(&self) -> Responder { - self.responder.clone() - } -} - -#[allow(dead_code)] // we'll need to use `Notifier` in the future -impl Notifier { - pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> - where - N: lsp_types::notification::Notification, - { - let method = N::METHOD.to_string(); - - let message = lsp_server::Message::Notification(Notification::new(method, params)); - - self.0.send(message) - } - - pub(crate) fn notify_method(&self, method: String) -> crate::Result<()> { - self.0 - .send(lsp_server::Message::Notification(Notification::new( - method, - Value::Null, - ))) - } -} - -impl Responder { - pub(crate) fn respond( - &self, - id: RequestId, - result: crate::server::Result, - ) -> crate::Result<()> - where - R: serde::Serialize, - { - self.0.send( - match result { - Ok(res) => lsp_server::Response::new_ok(id, res), - Err(crate::server::api::Error { code, error }) => { - lsp_server::Response::new_err(id, code as i32, format!("{error}")) - } - } - .into(), - ) - } -} - -impl<'s> Requester<'s> { - /// Sends a request of kind `R` to the client, with associated parameters. - /// The task provided by `response_handler` will be dispatched as soon as the response - /// comes back from the client. - pub(crate) fn request( - &mut self, - params: R::Params, - response_handler: impl Fn(R::Result) -> Task<'s> + 'static, - ) -> crate::Result<()> - where - R: lsp_types::request::Request, - { - let serialized_params = serde_json::to_value(params)?; - - self.response_handlers.insert( - self.next_request_id.into(), - Box::new(move |response: lsp_server::Response| { - match (response.error, response.result) { - (Some(err), _) => { - tracing::error!( - "Got an error from the client (code {}): {}", - err.code, - err.message - ); - Task::nothing() - } - (None, Some(response)) => match serde_json::from_value(response) { - Ok(response) => response_handler(response), - Err(error) => { - tracing::error!("Failed to deserialize response from server: {error}"); - Task::nothing() - } - }, - (None, None) => { - if TypeId::of::() == TypeId::of::<()>() { - // We can't call `response_handler(())` directly here, but - // since we _know_ the type expected is `()`, we can use - // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, - // so this branch works in the general case but we'll only - // hit it if the concrete type is `()`, so the `unwrap()` is safe here. - response_handler(serde_json::from_value(Value::Null).unwrap()); - } else { - tracing::error!( - "Server response was invalid: did not contain a result or error" - ); - } - Task::nothing() - } - } - }), - ); - - self.sender - .send(lsp_server::Message::Request(lsp_server::Request { - id: self.next_request_id.into(), - method: R::METHOD.into(), - params: serialized_params, - }))?; - - self.next_request_id += 1; - - Ok(()) - } - - pub(crate) fn pop_response_task(&mut self, response: lsp_server::Response) -> Task<'s> { - if let Some(handler) = self.response_handlers.remove(&response.id) { - handler(response) - } else { - tracing::error!( - "Received a response with ID {}, which was not expected", - response.id - ); - Task::nothing() - } - } -} diff --git a/crates/ruff_server/src/server/connection.rs b/crates/ruff_server/src/server/connection.rs index 5993023be8303..4993d2ba6cd6b 100644 --- a/crates/ruff_server/src/server/connection.rs +++ b/crates/ruff_server/src/server/connection.rs @@ -1,31 +1,17 @@ use lsp_server as lsp; -use lsp_types::{notification::Notification, request::Request}; -use std::sync::{Arc, Weak}; -type ConnectionSender = crossbeam::channel::Sender; -type ConnectionReceiver = crossbeam::channel::Receiver; +pub type ConnectionSender = crossbeam::channel::Sender; /// A builder for `Connection` that handles LSP initialization. pub(crate) struct ConnectionInitializer { connection: lsp::Connection, - threads: lsp::IoThreads, -} - -/// Handles inbound and outbound messages with the client. -pub(crate) struct Connection { - sender: Arc, - receiver: ConnectionReceiver, - threads: lsp::IoThreads, } impl ConnectionInitializer { /// Create a new LSP server connection over stdin/stdout. - pub(super) fn stdio() -> Self { + pub(crate) fn stdio() -> (Self, lsp::IoThreads) { let (connection, threads) = lsp::Connection::stdio(); - Self { - connection, - threads, - } + (Self { connection }, threads) } /// Starts the initialization process with the client by listening for an initialization request. @@ -46,7 +32,7 @@ impl ConnectionInitializer { server_capabilities: &lsp_types::ServerCapabilities, name: &str, version: &str, - ) -> crate::Result { + ) -> crate::Result { self.connection.initialize_finish( id, serde_json::json!({ @@ -57,109 +43,6 @@ impl ConnectionInitializer { } }), )?; - let Self { - connection: lsp::Connection { sender, receiver }, - threads, - } = self; - Ok(Connection { - sender: Arc::new(sender), - receiver, - threads, - }) - } -} - -impl Connection { - /// Make a new `ClientSender` for sending messages to the client. - pub(super) fn make_sender(&self) -> ClientSender { - ClientSender { - weak_sender: Arc::downgrade(&self.sender), - } - } - - /// An iterator over incoming messages from the client. - pub(super) fn incoming(&self) -> crossbeam::channel::Iter { - self.receiver.iter() - } - - /// Check and respond to any incoming shutdown requests; returns`true` if the server should be shutdown. - pub(super) fn handle_shutdown(&self, message: &lsp::Message) -> crate::Result { - match message { - lsp::Message::Request(lsp::Request { id, method, .. }) - if method == lsp_types::request::Shutdown::METHOD => - { - self.sender - .send(lsp::Response::new_ok(id.clone(), ()).into())?; - tracing::info!("Shutdown request received. Waiting for an exit notification..."); - - loop { - match &self - .receiver - .recv_timeout(std::time::Duration::from_secs(30))? - { - lsp::Message::Notification(lsp::Notification { method, .. }) - if method == lsp_types::notification::Exit::METHOD => - { - tracing::info!("Exit notification received. Server shutting down..."); - return Ok(true); - } - lsp::Message::Request(lsp::Request { id, method, .. }) => { - tracing::warn!( - "Server received unexpected request {method} ({id}) while waiting for exit notification", - ); - self.sender.send(lsp::Message::Response(lsp::Response::new_err( - id.clone(), - lsp::ErrorCode::InvalidRequest as i32, - "Server received unexpected request while waiting for exit notification".to_string(), - )))?; - } - message => { - tracing::warn!( - "Server received unexpected message while waiting for exit notification: {message:?}" - ); - } - } - } - } - lsp::Message::Notification(lsp::Notification { method, .. }) - if method == lsp_types::notification::Exit::METHOD => - { - anyhow::bail!("Server received an exit notification before a shutdown request was sent. Exiting..."); - } - _ => Ok(false), - } - } - - /// Join the I/O threads that underpin this connection. - /// This is guaranteed to be nearly immediate since - /// we close the only active channels to these threads prior - /// to joining them. - pub(super) fn close(self) -> crate::Result<()> { - std::mem::drop( - Arc::into_inner(self.sender) - .expect("the client sender shouldn't have more than one strong reference"), - ); - std::mem::drop(self.receiver); - self.threads.join()?; - Ok(()) - } -} - -/// A weak reference to an underlying sender channel, used for communication with the client. -/// If the `Connection` that created this `ClientSender` is dropped, any `send` calls will throw -/// an error. -#[derive(Clone, Debug)] -pub(crate) struct ClientSender { - weak_sender: Weak, -} - -// note: additional wrapper functions for senders may be implemented as needed. -impl ClientSender { - pub(crate) fn send(&self, msg: lsp::Message) -> crate::Result<()> { - let Some(sender) = self.weak_sender.upgrade() else { - anyhow::bail!("The connection with the client has been closed"); - }; - - Ok(sender.send(msg)?) + Ok(self.connection) } } diff --git a/crates/ruff_server/src/server/main_loop.rs b/crates/ruff_server/src/server/main_loop.rs new file mode 100644 index 0000000000000..b5943ad3dbd94 --- /dev/null +++ b/crates/ruff_server/src/server/main_loop.rs @@ -0,0 +1,209 @@ +use anyhow::anyhow; +use crossbeam::select; +use lsp_server::Message; +use lsp_types::{ + self as types, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, + notification::Notification as _, +}; + +use crate::{ + Server, + server::{api, schedule}, + session::Client, +}; + +pub type MainLoopSender = crossbeam::channel::Sender; +pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver; + +impl Server { + pub(super) fn main_loop(&mut self) -> crate::Result<()> { + self.initialize(&Client::new( + self.main_loop_sender.clone(), + self.connection.sender.clone(), + )); + + let mut scheduler = schedule::Scheduler::new(self.worker_threads); + + while let Ok(next_event) = self.next_event() { + let Some(next_event) = next_event else { + anyhow::bail!("client exited without proper shutdown sequence"); + }; + + match next_event { + Event::Message(msg) => { + let client = Client::new( + self.main_loop_sender.clone(), + self.connection.sender.clone(), + ); + + let task = match msg { + Message::Request(req) => { + self.session + .request_queue_mut() + .incoming_mut() + .register(req.id.clone(), req.method.clone()); + + if self.session.is_shutdown_requested() { + tracing::warn!( + "Received request after server shutdown was requested, discarding" + ); + client.respond_err( + req.id, + lsp_server::ResponseError { + code: lsp_server::ErrorCode::InvalidRequest as i32, + message: "Shutdown already requested".to_owned(), + data: None, + }, + )?; + continue; + } + + api::request(req) + } + Message::Notification(notification) => { + if notification.method == lsp_types::notification::Exit::METHOD { + if !self.session.is_shutdown_requested() { + return Err(anyhow!( + "Received exit notification before a shutdown request" + )); + } + + tracing::debug!("Received exit notification, exiting"); + return Ok(()); + } + + api::notification(notification) + } + + // Handle the response from the client to a server request + Message::Response(response) => { + if let Some(handler) = self + .session + .request_queue_mut() + .outgoing_mut() + .complete(&response.id) + { + handler(&client, response); + } else { + tracing::error!( + "Received a response with ID {}, which was not expected", + response.id + ); + } + + continue; + } + }; + + scheduler.dispatch(task, &mut self.session, client); + } + Event::SendResponse(response) => { + // Filter out responses for already canceled requests. + if let Some((start_time, method)) = self + .session + .request_queue_mut() + .incoming_mut() + .complete(&response.id) + { + let duration = start_time.elapsed(); + tracing::trace!(name: "message response", method, %response.id, duration = format_args!("{:0.2?}", duration)); + + self.connection.sender.send(Message::Response(response))?; + } else { + tracing::trace!( + "Ignoring response for canceled request id={}", + response.id + ); + } + } + } + } + + Ok(()) + } + + /// Waits for the next message from the client or action. + /// + /// Returns `Ok(None)` if the client connection is closed. + fn next_event(&self) -> Result, crossbeam::channel::RecvError> { + select!( + recv(self.connection.receiver) -> msg => { + // Ignore disconnect errors, they're handled by the main loop (it will exit). + Ok(msg.ok().map(Event::Message)) + }, + recv(self.main_loop_receiver) -> event => event.map(Some), + ) + } + + fn initialize(&mut self, client: &Client) { + let dynamic_registration = self + .client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_watched_files) + .and_then(|watched_files| watched_files.dynamic_registration) + .unwrap_or_default(); + if dynamic_registration { + // Register all dynamic capabilities here + + // `workspace/didChangeWatchedFiles` + // (this registers the configuration file watcher) + let params = lsp_types::RegistrationParams { + registrations: vec![lsp_types::Registration { + id: "ruff-server-watch".into(), + method: "workspace/didChangeWatchedFiles".into(), + register_options: Some( + serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { + watchers: vec![ + FileSystemWatcher { + glob_pattern: types::GlobPattern::String( + "**/.ruff.toml".into(), + ), + kind: None, + }, + FileSystemWatcher { + glob_pattern: types::GlobPattern::String("**/ruff.toml".into()), + kind: None, + }, + FileSystemWatcher { + glob_pattern: types::GlobPattern::String( + "**/pyproject.toml".into(), + ), + kind: None, + }, + ], + }) + .unwrap(), + ), + }], + }; + + let response_handler = |_: &Client, ()| { + tracing::info!("Configuration file watcher successfully registered"); + }; + + if let Err(err) = client.send_request::( + &self.session, + params, + response_handler, + ) { + tracing::error!( + "An error occurred when trying to register the configuration file watcher: {err}" + ); + } + } else { + tracing::warn!( + "LSP client does not support dynamic capability registration - automatic configuration reloading will not be available." + ); + } + } +} + +#[derive(Debug)] +pub enum Event { + /// An incoming message from the LSP client. + Message(lsp_server::Message), + + /// Send a response to the client + SendResponse(lsp_server::Response), +} diff --git a/crates/ruff_server/src/server/schedule.rs b/crates/ruff_server/src/server/schedule.rs index f03570686aa4a..e6ca9bd438628 100644 --- a/crates/ruff_server/src/server/schedule.rs +++ b/crates/ruff_server/src/server/schedule.rs @@ -1,6 +1,6 @@ use std::num::NonZeroUsize; -use crate::session::Session; +use crate::session::{Client, Session}; mod task; mod thread; @@ -12,13 +12,11 @@ use self::{ thread::ThreadPriority, }; -use super::{client::Client, ClientSender}; - /// The event loop thread is actually a secondary thread that we spawn from the /// _actual_ main thread. This secondary thread has a larger stack size /// than some OS defaults (Windows, for example) and is also designated as /// high-priority. -pub(crate) fn event_loop_thread( +pub(crate) fn spawn_main_loop( func: impl FnOnce() -> crate::Result<()> + Send + 'static, ) -> crate::Result>> { // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. @@ -32,69 +30,33 @@ pub(crate) fn event_loop_thread( ) } -pub(crate) struct Scheduler<'s> { - session: &'s mut Session, - client: Client<'s>, +pub(crate) struct Scheduler { fmt_pool: thread::Pool, background_pool: thread::Pool, } -impl<'s> Scheduler<'s> { - pub(super) fn new( - session: &'s mut Session, - worker_threads: NonZeroUsize, - sender: ClientSender, - ) -> Self { +impl Scheduler { + pub(super) fn new(worker_threads: NonZeroUsize) -> Self { const FMT_THREADS: usize = 1; Self { - session, fmt_pool: thread::Pool::new(NonZeroUsize::try_from(FMT_THREADS).unwrap()), background_pool: thread::Pool::new(worker_threads), - client: Client::new(sender), } } - /// Immediately sends a request of kind `R` to the client, with associated parameters. - /// The task provided by `response_handler` will be dispatched as soon as the response - /// comes back from the client. - pub(super) fn request( - &mut self, - params: R::Params, - response_handler: impl Fn(R::Result) -> Task<'s> + 'static, - ) -> crate::Result<()> - where - R: lsp_types::request::Request, - { - self.client.requester.request::(params, response_handler) - } - - /// Creates a task to handle a response from the client. - pub(super) fn response(&mut self, response: lsp_server::Response) -> Task<'s> { - self.client.requester.pop_response_task(response) - } - /// Dispatches a `task` by either running it as a blocking function or /// executing it on a background thread pool. - pub(super) fn dispatch(&mut self, task: task::Task<'s>) { + pub(super) fn dispatch(&mut self, task: Task, session: &mut Session, client: Client) { match task { Task::Sync(SyncTask { func }) => { - let notifier = self.client.notifier(); - let responder = self.client.responder(); - func( - self.session, - notifier, - &mut self.client.requester, - responder, - ); + func(session, &client); } Task::Background(BackgroundTaskBuilder { schedule, builder: func, }) => { - let static_func = func(self.session); - let notifier = self.client.notifier(); - let responder = self.client.responder(); - let task = move || static_func(notifier, responder); + let static_func = func(session); + let task = move || static_func(&client); match schedule { BackgroundSchedule::Worker => { self.background_pool.spawn(ThreadPriority::Worker, task); diff --git a/crates/ruff_server/src/server/schedule/task.rs b/crates/ruff_server/src/server/schedule/task.rs index e34b22ed6919f..2e768c2b18fb6 100644 --- a/crates/ruff_server/src/server/schedule/task.rs +++ b/crates/ruff_server/src/server/schedule/task.rs @@ -1,16 +1,13 @@ use lsp_server::RequestId; use serde::Serialize; -use crate::{ - server::client::{Notifier, Requester, Responder}, - session::Session, -}; +use crate::session::{Client, Session}; -type LocalFn<'s> = Box; +type LocalFn = Box; -type BackgroundFn = Box; +type BackgroundFn = Box; -type BackgroundFnBuilder<'s> = Box BackgroundFn + 's>; +type BackgroundFnBuilder = Box BackgroundFn>; /// Describes how the task should be run. #[derive(Clone, Copy, Debug, Default)] @@ -36,9 +33,9 @@ pub(in crate::server) enum BackgroundSchedule { /// while local tasks have exclusive access and can modify it as they please. Keep in mind that /// local tasks will **block** the main event loop, so only use local tasks if you **need** /// mutable state access or you need the absolute lowest latency possible. -pub(in crate::server) enum Task<'s> { - Background(BackgroundTaskBuilder<'s>), - Sync(SyncTask<'s>), +pub(in crate::server) enum Task { + Background(BackgroundTaskBuilder), + Sync(SyncTask), } // The reason why this isn't just a 'static background closure @@ -49,20 +46,20 @@ pub(in crate::server) enum Task<'s> { // that the inner closure can capture. This builder closure has a lifetime linked to the scheduler. // When the task is dispatched, the scheduler runs the synchronous builder, which takes the session // as a reference, to create the inner 'static closure. That closure is then moved to a background task pool. -pub(in crate::server) struct BackgroundTaskBuilder<'s> { +pub(in crate::server) struct BackgroundTaskBuilder { pub(super) schedule: BackgroundSchedule, - pub(super) builder: BackgroundFnBuilder<'s>, + pub(super) builder: BackgroundFnBuilder, } -pub(in crate::server) struct SyncTask<'s> { - pub(super) func: LocalFn<'s>, +pub(in crate::server) struct SyncTask { + pub(super) func: LocalFn, } -impl<'s> Task<'s> { +impl Task { /// Creates a new background task. pub(crate) fn background( schedule: BackgroundSchedule, - func: impl FnOnce(&Session) -> Box + 's, + func: impl FnOnce(&Session) -> Box + 'static, ) -> Self { Self::Background(BackgroundTaskBuilder { schedule, @@ -70,9 +67,7 @@ impl<'s> Task<'s> { }) } /// Creates a new local task. - pub(crate) fn local( - func: impl FnOnce(&mut Session, Notifier, &mut Requester, Responder) + 's, - ) -> Self { + pub(crate) fn sync(func: impl FnOnce(&mut Session, &Client) + 'static) -> Self { Self::Sync(SyncTask { func: Box::new(func), }) @@ -83,8 +78,8 @@ impl<'s> Task<'s> { where R: Serialize + Send + 'static, { - Self::local(move |_, _, _, responder| { - if let Err(err) = responder.respond(id, result) { + Self::sync(move |_, client| { + if let Err(err) = client.respond(&id, result) { tracing::error!("Unable to send immediate response: {err}"); } }) @@ -92,6 +87,6 @@ impl<'s> Task<'s> { /// Creates a local task that does nothing. pub(crate) fn nothing() -> Self { - Self::local(move |_, _, _, _| {}) + Self::sync(move |_, _| {}) } } diff --git a/crates/ruff_server/src/server/schedule/thread/pool.rs b/crates/ruff_server/src/server/schedule/thread/pool.rs index ea654a11d2af4..ac3e072ab879e 100644 --- a/crates/ruff_server/src/server/schedule/thread/pool.rs +++ b/crates/ruff_server/src/server/schedule/thread/pool.rs @@ -15,9 +15,10 @@ use std::{ num::NonZeroUsize, + panic::AssertUnwindSafe, sync::{ - atomic::{AtomicUsize, Ordering}, Arc, + atomic::{AtomicUsize, Ordering}, }, }; @@ -71,7 +72,26 @@ impl Pool { current_priority = job.requested_priority; } extant_tasks.fetch_add(1, Ordering::SeqCst); - (job.f)(); + + // SAFETY: it's safe to assume that `job.f` is unwind safe because we always + // abort the process if it panics. + // Panicking here ensures that we don't swallow errors and is the same as + // what rayon does. + // Any recovery should be implemented outside the thread pool (e.g. when + // dispatching requests/notifications etc). + if let Err(error) = std::panic::catch_unwind(AssertUnwindSafe(job.f)) { + if let Some(msg) = error.downcast_ref::() { + tracing::error!("Worker thread panicked with: {msg}; aborting"); + } else if let Some(msg) = error.downcast_ref::<&str>() { + tracing::error!("Worker thread panicked with: {msg}; aborting"); + } else { + tracing::error!( + "Worker thread panicked with: {error:?}; aborting" + ); + } + + std::process::abort(); + } extant_tasks.fetch_sub(1, Ordering::SeqCst); } } @@ -106,7 +126,7 @@ impl Pool { self.job_sender.send(job).unwrap(); } - #[allow(dead_code)] + #[expect(dead_code)] pub(super) fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } diff --git a/crates/ruff_server/src/server/schedule/thread/priority.rs b/crates/ruff_server/src/server/schedule/thread/priority.rs index e6a555242fcb7..d1e0fefdad7ef 100644 --- a/crates/ruff_server/src/server/schedule/thread/priority.rs +++ b/crates/ruff_server/src/server/schedule/thread/priority.rs @@ -183,14 +183,14 @@ mod imp { QoSClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND, }; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { libc::pthread_set_qos_class_self_np(c, 0) }; if code == 0 { return; } - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; match errno { @@ -223,12 +223,16 @@ mod imp { } pub(super) fn get_current_thread_qos_class() -> Option { - #[allow(unsafe_code)] + #[expect(unsafe_code)] let current_thread = unsafe { libc::pthread_self() }; let mut qos_class_raw = libc::qos_class_t::QOS_CLASS_UNSPECIFIED; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { - libc::pthread_get_qos_class_np(current_thread, &mut qos_class_raw, std::ptr::null_mut()) + libc::pthread_get_qos_class_np( + current_thread, + &raw mut qos_class_raw, + std::ptr::null_mut(), + ) }; if code != 0 { @@ -241,7 +245,7 @@ mod imp { // ones which we cannot handle anyway // // 0: https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/src/qos.c#L171-L177 - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; unreachable!("`pthread_get_qos_class_np` failed unexpectedly (os error {errno})"); } diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index 00fd9d6013c03..c87ba6d4eec62 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -4,19 +4,25 @@ use std::path::Path; use std::sync::Arc; use lsp_types::{ClientCapabilities, FileEvent, NotebookDocumentCellChange, Url}; -use settings::ResolvedClientSettings; +use settings::ClientSettings; use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument}; +use crate::session::request_queue::RequestQueue; +use crate::session::settings::GlobalClientSettings; use crate::workspace::Workspaces; use crate::{PositionEncoding, TextDocument}; pub(crate) use self::capabilities::ResolvedClientCapabilities; pub use self::index::DocumentQuery; -pub use self::settings::ClientSettings; -pub(crate) use self::settings::{AllSettings, WorkspaceSettingsMap}; +pub(crate) use self::options::{AllOptions, WorkspaceOptionsMap}; +pub use self::options::{ClientOptions, GlobalOptions}; +pub use client::Client; mod capabilities; +mod client; mod index; +mod options; +mod request_queue; mod settings; /// The global state for the LSP @@ -26,16 +32,23 @@ pub struct Session { /// The global position encoding, negotiated during LSP initialization. position_encoding: PositionEncoding, /// Global settings provided by the client. - global_settings: ClientSettings, + global_settings: GlobalClientSettings, + /// Tracks what LSP features the client supports and doesn't support. resolved_client_capabilities: Arc, + + /// Tracks the pending requests between client and server. + request_queue: RequestQueue, + + /// Has the client requested the server to shutdown. + shutdown_requested: bool, } /// An immutable snapshot of `Session` that references /// a specific document. pub struct DocumentSnapshot { resolved_client_capabilities: Arc, - client_settings: settings::ResolvedClientSettings, + client_settings: Arc, document_ref: index::DocumentQuery, position_encoding: PositionEncoding, } @@ -44,19 +57,38 @@ impl Session { pub fn new( client_capabilities: &ClientCapabilities, position_encoding: PositionEncoding, - global_settings: ClientSettings, + global: GlobalClientSettings, workspaces: &Workspaces, + client: &Client, ) -> crate::Result { Ok(Self { position_encoding, - index: index::Index::new(workspaces, &global_settings)?, - global_settings, + index: index::Index::new(workspaces, &global, client)?, + global_settings: global, resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( client_capabilities, )), + request_queue: RequestQueue::new(), + shutdown_requested: false, }) } + pub(crate) fn request_queue(&self) -> &RequestQueue { + &self.request_queue + } + + pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue { + &mut self.request_queue + } + + pub(crate) fn is_shutdown_requested(&self) -> bool { + self.shutdown_requested + } + + pub(crate) fn set_shutdown_requested(&mut self, requested: bool) { + self.shutdown_requested = requested; + } + pub fn key_from_url(&self, url: Url) -> DocumentKey { self.index.key_from_url(url) } @@ -66,7 +98,10 @@ impl Session { let key = self.key_from_url(url); Some(DocumentSnapshot { resolved_client_capabilities: self.resolved_client_capabilities.clone(), - client_settings: self.index.client_settings(&key, &self.global_settings), + client_settings: self + .index + .client_settings(&key) + .unwrap_or_else(|| self.global_settings.to_settings_arc()), document_ref: self.index.make_document_ref(key, &self.global_settings)?, position_encoding: self.position_encoding, }) @@ -134,13 +169,14 @@ impl Session { } /// Reloads the settings index based on the provided changes. - pub(crate) fn reload_settings(&mut self, changes: &[FileEvent]) { - self.index.reload_settings(changes); + pub(crate) fn reload_settings(&mut self, changes: &[FileEvent], client: &Client) { + self.index.reload_settings(changes, client); } /// Open a workspace folder at the given `url`. - pub(crate) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> { - self.index.open_workspace_folder(url, &self.global_settings) + pub(crate) fn open_workspace_folder(&mut self, url: Url, client: &Client) -> crate::Result<()> { + self.index + .open_workspace_folder(url, &self.global_settings, client) } /// Close a workspace folder at the given `url`. @@ -163,8 +199,8 @@ impl Session { } /// Returns the resolved global client settings. - pub(crate) fn global_client_settings(&self) -> ResolvedClientSettings { - ResolvedClientSettings::global(&self.global_settings) + pub(crate) fn global_client_settings(&self) -> &ClientSettings { + self.global_settings.to_settings() } /// Returns the number of open documents in the session. @@ -183,7 +219,7 @@ impl DocumentSnapshot { &self.resolved_client_capabilities } - pub(crate) fn client_settings(&self) -> &settings::ResolvedClientSettings { + pub(crate) fn client_settings(&self) -> &settings::ClientSettings { &self.client_settings } diff --git a/crates/ruff_server/src/session/capabilities.rs b/crates/ruff_server/src/session/capabilities.rs index 911457236d9de..83ab1dba63b65 100644 --- a/crates/ruff_server/src/session/capabilities.rs +++ b/crates/ruff_server/src/session/capabilities.rs @@ -2,7 +2,7 @@ use lsp_types::ClientCapabilities; use ruff_linter::display_settings; #[derive(Debug, Clone, PartialEq, Eq, Default)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub(crate) struct ResolvedClientCapabilities { pub(crate) code_action_deferred_edit_resolution: bool, pub(crate) apply_edit: bool, diff --git a/crates/ruff_server/src/session/client.rs b/crates/ruff_server/src/session/client.rs new file mode 100644 index 0000000000000..0cbcd449d5a22 --- /dev/null +++ b/crates/ruff_server/src/session/client.rs @@ -0,0 +1,248 @@ +use crate::Session; +use crate::server::{ConnectionSender, Event, MainLoopSender}; +use anyhow::{Context, anyhow}; +use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError}; +use serde_json::Value; +use std::any::TypeId; +use std::fmt::Display; + +pub(crate) type ClientResponseHandler = Box; + +#[derive(Clone, Debug)] +pub struct Client { + /// Channel to send messages back to the main loop. + main_loop_sender: MainLoopSender, + /// Channel to send messages directly to the LSP client without going through the main loop. + /// + /// This is generally preferred because it reduces pressure on the main loop but it may not always be + /// possible if access to data on [`Session`] is required, which background tasks don't have. + client_sender: ConnectionSender, +} + +impl Client { + pub fn new(main_loop_sender: MainLoopSender, client_sender: ConnectionSender) -> Self { + Self { + main_loop_sender, + client_sender, + } + } + + /// Sends a request of kind `R` to the client, with associated parameters. + /// + /// The request is sent immediately. + /// The `response_handler` will be dispatched as soon as the client response + /// is processed on the main-loop. The handler always runs on the main-loop thread. + /// + /// # Note + /// This method takes a `session` so that we can register the pending-request + /// and send the response directly to the client. If this ever becomes too limiting (because we + /// need to send a request from somewhere where we don't have access to session), consider introducing + /// a new `send_deferred_request` method that doesn't take a session and instead sends + /// an `Action` to the main loop to send the request (the main loop has always access to session). + pub(crate) fn send_request( + &self, + session: &Session, + params: R::Params, + response_handler: impl FnOnce(&Client, R::Result) + Send + 'static, + ) -> crate::Result<()> + where + R: lsp_types::request::Request, + { + let response_handler = Box::new(move |client: &Client, response: lsp_server::Response| { + let _span = + tracing::debug_span!("client_response", id=%response.id, method = R::METHOD) + .entered(); + + match (response.error, response.result) { + (Some(err), _) => { + tracing::error!( + "Got an error from the client (code {code}, method {method}): {message}", + code = err.code, + message = err.message, + method = R::METHOD + ); + } + (None, Some(response)) => match serde_json::from_value(response) { + Ok(response) => response_handler(client, response), + Err(error) => { + tracing::error!( + "Failed to deserialize client response (method={method}): {error}", + method = R::METHOD + ); + } + }, + (None, None) => { + if TypeId::of::() == TypeId::of::<()>() { + // We can't call `response_handler(())` directly here, but + // since we _know_ the type expected is `()`, we can use + // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, + // so this branch works in the general case but we'll only + // hit it if the concrete type is `()`, so the `unwrap()` is safe here. + response_handler(client, serde_json::from_value(Value::Null).unwrap()); + } else { + tracing::error!( + "Invalid client response: did not contain a result or error (method={method})", + method = R::METHOD + ); + } + } + } + }); + + let id = session + .request_queue() + .outgoing() + .register(response_handler); + + self.client_sender + .send(Message::Request(lsp_server::Request { + id, + method: R::METHOD.to_string(), + params: serde_json::to_value(params).context("Failed to serialize params")?, + })) + .with_context(|| { + format!("Failed to send request method={method}", method = R::METHOD) + })?; + + Ok(()) + } + + /// Sends a notification to the client. + pub(crate) fn send_notification(&self, params: N::Params) -> crate::Result<()> + where + N: lsp_types::notification::Notification, + { + let method = N::METHOD.to_string(); + + self.client_sender + .send(lsp_server::Message::Notification(Notification::new( + method, params, + ))) + .map_err(|error| { + anyhow!( + "Failed to send notification (method={method}): {error}", + method = N::METHOD + ) + }) + } + + /// Sends a notification without any parameters to the client. + /// + /// This is useful for notifications that don't require any data. + #[expect(dead_code)] + pub(crate) fn send_notification_no_params(&self, method: &str) -> crate::Result<()> { + self.client_sender + .send(lsp_server::Message::Notification(Notification::new( + method.to_string(), + Value::Null, + ))) + .map_err(|error| anyhow!("Failed to send notification (method={method}): {error}",)) + } + + /// Sends a response to the client for a given request ID. + /// + /// The response isn't sent immediately. Instead, it's queued up in the main loop + /// and checked for cancellation (each request must have exactly one response). + pub(crate) fn respond( + &self, + id: &RequestId, + result: crate::server::Result, + ) -> crate::Result<()> + where + R: serde::Serialize, + { + let response = match result { + Ok(res) => lsp_server::Response::new_ok(id.clone(), res), + Err(crate::server::Error { code, error }) => { + lsp_server::Response::new_err(id.clone(), code as i32, error.to_string()) + } + }; + + self.main_loop_sender + .send(Event::SendResponse(response)) + .map_err(|error| anyhow!("Failed to send response for request {id}: {error}")) + } + + /// Sends an error response to the client for a given request ID. + /// + /// The response isn't sent immediately. Instead, it's queued up in the main loop. + pub(crate) fn respond_err( + &self, + id: RequestId, + error: lsp_server::ResponseError, + ) -> crate::Result<()> { + let response = lsp_server::Response { + id, + result: None, + error: Some(error), + }; + + self.main_loop_sender + .send(Event::SendResponse(response)) + .map_err(|error| anyhow!("Failed to send response: {error}")) + } + + /// Shows a message to the user. + /// + /// This opens a pop up in VS Code showing `message`. + pub(crate) fn show_message( + &self, + message: impl Display, + message_type: lsp_types::MessageType, + ) -> crate::Result<()> { + self.send_notification::( + lsp_types::ShowMessageParams { + typ: message_type, + message: message.to_string(), + }, + ) + } + + /// Sends a request to display a warning to the client with a formatted message. The warning is + /// sent in a `window/showMessage` notification. + /// + /// Logs an error if the message could not be sent. + pub(crate) fn show_warning_message(&self, message: impl Display) { + let result = self.show_message(message, lsp_types::MessageType::WARNING); + + if let Err(err) = result { + tracing::error!("Failed to send warning message to the client: {err}"); + } + } + + /// Sends a request to display an error to the client with a formatted message. The error is + /// sent in a `window/showMessage` notification. + /// + /// Logs an error if the message could not be sent. + pub(crate) fn show_error_message(&self, message: impl Display) { + let result = self.show_message(message, lsp_types::MessageType::ERROR); + + if let Err(err) = result { + tracing::error!("Failed to send error message to the client: {err}"); + } + } + + pub(crate) fn cancel(&self, session: &mut Session, id: RequestId) -> crate::Result<()> { + let method_name = session.request_queue_mut().incoming_mut().cancel(&id); + + if let Some(method_name) = method_name { + tracing::debug!("Cancelled request id={id} method={method_name}"); + let error = ResponseError { + code: ErrorCode::RequestCanceled as i32, + message: "request was cancelled by client".to_owned(), + data: None, + }; + + // Use `client_sender` here instead of `respond_err` because + // `respond_err` filters out responses for canceled requests (which we just did!). + self.client_sender + .send(Message::Response(lsp_server::Response { + id, + result: None, + error: Some(error), + }))?; + } + + Ok(()) + } +} diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index 4f75a35fe6b0c..209b5121c8f28 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -11,13 +11,16 @@ use thiserror::Error; pub(crate) use ruff_settings::RuffSettings; use crate::edit::LanguageId; +use crate::session::Client; +use crate::session::options::Combine; +use crate::session::settings::GlobalClientSettings; use crate::workspace::{Workspace, Workspaces}; use crate::{ - edit::{DocumentKey, DocumentVersion, NotebookDocument}, PositionEncoding, TextDocument, + edit::{DocumentKey, DocumentVersion, NotebookDocument}, }; -use super::{settings::ResolvedClientSettings, ClientSettings}; +use super::settings::ClientSettings; mod ruff_settings; @@ -36,7 +39,7 @@ pub(crate) struct Index { /// Settings associated with a workspace. struct WorkspaceSettings { - client_settings: ResolvedClientSettings, + client_settings: Arc, ruff_settings: ruff_settings::RuffSettingsIndex, } @@ -70,11 +73,12 @@ pub enum DocumentQuery { impl Index { pub(super) fn new( workspaces: &Workspaces, - global_settings: &ClientSettings, + global: &GlobalClientSettings, + client: &Client, ) -> crate::Result { let mut settings = WorkspaceSettingsIndex::default(); for workspace in &**workspaces { - settings.register_workspace(workspace, global_settings)?; + settings.register_workspace(workspace, global, client)?; } Ok(Self { @@ -170,11 +174,12 @@ impl Index { pub(super) fn open_workspace_folder( &mut self, url: Url, - global_settings: &ClientSettings, + global: &GlobalClientSettings, + client: &Client, ) -> crate::Result<()> { // TODO(jane): Find a way for workspace client settings to be added or changed dynamically. self.settings - .register_workspace(&Workspace::new(url), global_settings) + .register_workspace(&Workspace::new(url), global, client) } pub(super) fn close_workspace_folder(&mut self, workspace_url: &Url) -> crate::Result<()> { @@ -201,7 +206,7 @@ impl Index { pub(super) fn make_document_ref( &self, key: DocumentKey, - global_settings: &ClientSettings, + global: &GlobalClientSettings, ) -> Option { let url = self.url_for_key(&key)?.clone(); @@ -230,13 +235,12 @@ impl Index { "No settings available for {} - falling back to default settings", url ); - let resolved_global = ResolvedClientSettings::global(global_settings); // The path here is only for completeness, it's okay to use a non-existing path // in case this is an unsaved (untitled) document. let path = Path::new(url.path()); let root = path.parent().unwrap_or(path); Arc::new(RuffSettings::fallback( - resolved_global.editor_settings(), + global.to_settings().editor_settings(), root, )) }); @@ -258,7 +262,7 @@ impl Index { /// registered in [`try_register_capabilities`] method. /// /// [`try_register_capabilities`]: crate::server::Server::try_register_capabilities - pub(super) fn reload_settings(&mut self, changes: &[FileEvent]) { + pub(super) fn reload_settings(&mut self, changes: &[FileEvent], client: &Client) { let mut indexed = FxHashSet::default(); for change in changes { @@ -286,6 +290,7 @@ impl Index { indexed.insert(root.clone()); settings.ruff_settings = ruff_settings::RuffSettingsIndex::new( + client, root, settings.client_settings.editor_settings(), false, @@ -330,21 +335,12 @@ impl Index { Ok(()) } - pub(super) fn client_settings( - &self, - key: &DocumentKey, - global_settings: &ClientSettings, - ) -> ResolvedClientSettings { - let Some(url) = self.url_for_key(key) else { - return ResolvedClientSettings::global(global_settings); - }; - let Some(WorkspaceSettings { + pub(super) fn client_settings(&self, key: &DocumentKey) -> Option> { + let url = self.url_for_key(key)?; + let WorkspaceSettings { client_settings, .. - }) = self.settings_for_url(url) - else { - return ResolvedClientSettings::global(global_settings); - }; - client_settings.clone() + } = self.settings_for_url(url)?; + Some(client_settings.clone()) } fn document_controller_for_key( @@ -422,25 +418,40 @@ impl WorkspaceSettingsIndex { fn register_workspace( &mut self, workspace: &Workspace, - global_settings: &ClientSettings, + global: &GlobalClientSettings, + client: &Client, ) -> crate::Result<()> { let workspace_url = workspace.url(); if workspace_url.scheme() != "file" { tracing::info!("Ignoring non-file workspace URL: {workspace_url}"); - show_warn_msg!("Ruff does not support non-file workspaces; Ignoring {workspace_url}"); + client.show_warning_message(format_args!( + "Ruff does not support non-file workspaces; Ignoring {workspace_url}" + )); return Ok(()); } let workspace_path = workspace_url.to_file_path().map_err(|()| { anyhow!("Failed to convert workspace URL to file path: {workspace_url}") })?; - let client_settings = if let Some(workspace_settings) = workspace.settings() { - ResolvedClientSettings::with_workspace(workspace_settings, global_settings) + let client_settings = if let Some(workspace_options) = workspace.options() { + let options = workspace_options.clone().combine(global.options().clone()); + let settings = match options.into_settings() { + Ok(settings) => settings, + Err(settings) => { + client.show_error_message(format_args!( + "The settings for the workspace {workspace_path} are invalid. Refer to the logs for more information.", + workspace_path = workspace_path.display() + )); + settings + } + }; + Arc::new(settings) } else { - ResolvedClientSettings::global(global_settings) + global.to_settings_arc() }; let workspace_settings_index = ruff_settings::RuffSettingsIndex::new( + client, &workspace_path, client_settings.editor_settings(), workspace.is_default(), @@ -517,7 +528,6 @@ impl DocumentController { } } - #[allow(dead_code)] pub(crate) fn as_text(&self) -> Option<&TextDocument> { match self { Self::Text(document) => Some(document), @@ -560,7 +570,7 @@ impl DocumentQuery { ruff_linter::source_kind::SourceKind::Python(document.contents().to_string()) } Self::Notebook { notebook, .. } => { - ruff_linter::source_kind::SourceKind::IpyNotebook(notebook.make_ruff_notebook()) + ruff_linter::source_kind::SourceKind::ipy_notebook(notebook.make_ruff_notebook()) } } } diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index c004f1d9f6a73..658a2a08cc611 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -1,26 +1,26 @@ use std::collections::BTreeMap; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::Context; use ignore::{WalkBuilder, WalkState}; use ruff_linter::settings::types::GlobPath; use ruff_linter::{settings::types::FilePattern, settings::types::PreviewMode}; +use ruff_workspace::Settings; use ruff_workspace::pyproject::find_fallback_target_version; use ruff_workspace::resolver::match_exclusion; -use ruff_workspace::Settings; use ruff_workspace::{ configuration::{Configuration, FormatConfiguration, LintConfiguration, RuleSelection}, pyproject::{find_user_settings_toml, settings_toml}, resolver::ConfigurationTransformer, }; -use crate::session::settings::{ - ConfigurationPreference, ResolvedConfiguration, ResolvedEditorSettings, -}; +use crate::session::Client; +use crate::session::options::ConfigurationPreference; +use crate::session::settings::{EditorSettings, ResolvedConfiguration}; #[derive(Debug)] pub struct RuffSettings { @@ -64,7 +64,7 @@ impl RuffSettings { /// /// In the absence of a valid configuration file, it gracefully falls back to /// editor-only settings. - pub(crate) fn fallback(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings { + pub(crate) fn fallback(editor_settings: &EditorSettings, root: &Path) -> RuffSettings { struct FallbackTransformer<'a> { inner: EditorConfigurationTransformer<'a>, } @@ -122,14 +122,14 @@ impl RuffSettings { /// Constructs [`RuffSettings`] by merging the editor-defined settings with the /// default configuration. - fn editor_only(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings { + fn editor_only(editor_settings: &EditorSettings, root: &Path) -> RuffSettings { Self::with_editor_settings(editor_settings, root, Configuration::default()) .expect("editor configuration should merge successfully with default configuration") } /// Merges the `configuration` with the editor defined settings. fn with_editor_settings( - editor_settings: &ResolvedEditorSettings, + editor_settings: &EditorSettings, root: &Path, configuration: Configuration, ) -> anyhow::Result { @@ -156,8 +156,9 @@ impl RuffSettingsIndex { /// server will be running in a single file mode, then only (1) and (2) will be resolved, /// skipping (3). pub(super) fn new( + client: &Client, root: &Path, - editor_settings: &ResolvedEditorSettings, + editor_settings: &EditorSettings, is_default_workspace: bool, ) -> Self { if editor_settings.configuration_preference == ConfigurationPreference::EditorOnly { @@ -243,10 +244,10 @@ impl RuffSettingsIndex { // means for different editors. if is_default_workspace { if has_error { - show_err_msg!( + client.show_error_message(format!( "Error while resolving settings from workspace {}. Please refer to the logs for more details.", root.display() - ); + )); } return RuffSettingsIndex { index, fallback }; @@ -359,10 +360,10 @@ impl RuffSettingsIndex { }); if has_error.load(Ordering::Relaxed) { - show_err_msg!( + client.show_error_message(format!( "Error while resolving settings from workspace {}. Please refer to the logs for more details.", root.display() - ); + )); } RuffSettingsIndex { @@ -392,11 +393,11 @@ impl RuffSettingsIndex { } } -struct EditorConfigurationTransformer<'a>(&'a ResolvedEditorSettings, &'a Path); +struct EditorConfigurationTransformer<'a>(&'a EditorSettings, &'a Path); impl ConfigurationTransformer for EditorConfigurationTransformer<'_> { fn transform(&self, filesystem_configuration: Configuration) -> Configuration { - let ResolvedEditorSettings { + let EditorSettings { configuration, format_preview, lint_preview, @@ -462,7 +463,7 @@ impl ConfigurationTransformer for EditorConfigurationTransformer<'_> { tracing::debug!( "Combining settings from editor-specified inline configuration" ); - match Configuration::from_options(options, None, project_root) { + match Configuration::from_options(*options, None, project_root) { Ok(configuration) => editor_configuration.combine(configuration), Err(err) => { tracing::error!( @@ -515,11 +516,11 @@ mod tests { /// This test ensures that the inline configuration is correctly applied to the configuration. #[test] fn inline_settings() { - let editor_settings = ResolvedEditorSettings { - configuration: Some(ResolvedConfiguration::Inline(Options { + let editor_settings = EditorSettings { + configuration: Some(ResolvedConfiguration::Inline(Box::new(Options { line_length: Some(LineLength::try_from(120).unwrap()), ..Default::default() - })), + }))), ..Default::default() }; @@ -533,11 +534,11 @@ mod tests { /// settings is prioritized. #[test] fn inline_and_specific_settings_resolution_order() { - let editor_settings = ResolvedEditorSettings { - configuration: Some(ResolvedConfiguration::Inline(Options { + let editor_settings = EditorSettings { + configuration: Some(ResolvedConfiguration::Inline(Box::new(Options { line_length: Some(LineLength::try_from(120).unwrap()), ..Default::default() - })), + }))), line_length: Some(LineLength::try_from(100).unwrap()), ..Default::default() }; diff --git a/crates/ruff_server/src/session/options.rs b/crates/ruff_server/src/session/options.rs new file mode 100644 index 0000000000000..0a23e712c70a5 --- /dev/null +++ b/crates/ruff_server/src/session/options.rs @@ -0,0 +1,1021 @@ +use std::{path::PathBuf, str::FromStr as _}; + +use lsp_types::Url; +use rustc_hash::FxHashMap; +use serde::Deserialize; +use serde_json::{Map, Value}; + +use ruff_linter::{RuleSelector, line_width::LineLength, rule_selector::ParseError}; + +use crate::session::{ + Client, + settings::{ClientSettings, EditorSettings, GlobalClientSettings, ResolvedConfiguration}, +}; + +pub(crate) type WorkspaceOptionsMap = FxHashMap; + +/// Determines how multiple conflicting configurations should be resolved - in this +/// case, the configuration from the client settings and configuration from local +/// `.toml` files (aka 'workspace' configuration). +#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) enum ConfigurationPreference { + /// Configuration set in the editor takes priority over configuration set in `.toml` files. + #[default] + EditorFirst, + /// Configuration set in `.toml` files takes priority over configuration set in the editor. + FilesystemFirst, + /// `.toml` files are ignored completely, and only the editor configuration is used. + EditorOnly, +} + +/// A direct representation of of `configuration` schema within the client settings. +#[derive(Clone, Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(untagged)] +pub(super) enum ClientConfiguration { + /// A path to a configuration file. + String(String), + /// An object containing the configuration options. + Object(Map), +} + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub struct GlobalOptions { + #[serde(flatten)] + client: ClientOptions, + + // These settings are only needed for tracing, and are only read from the global configuration. + // These will not be in the resolved settings. + #[serde(flatten)] + pub(crate) tracing: TracingOptions, +} + +impl GlobalOptions { + pub(crate) fn set_preview(&mut self, preview: bool) { + self.client.set_preview(preview); + } + + #[cfg(test)] + pub(crate) fn client(&self) -> &ClientOptions { + &self.client + } + + pub fn into_settings(self, client: Client) -> GlobalClientSettings { + GlobalClientSettings { + options: self.client, + settings: std::cell::OnceCell::default(), + client, + } + } +} + +/// This is a direct representation of the settings schema sent by the client. +#[derive(Clone, Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub struct ClientOptions { + configuration: Option, + fix_all: Option, + organize_imports: Option, + lint: Option, + format: Option, + code_action: Option, + exclude: Option>, + line_length: Option, + configuration_preference: Option, + + /// If `true` or [`None`], show syntax errors as diagnostics. + /// + /// This is useful when using Ruff with other language servers, allowing the user to refer + /// to syntax errors from only one source. + show_syntax_errors: Option, +} + +impl ClientOptions { + /// Resolves the options. + /// + /// Returns `Ok` if all options are valid. Otherwise, returns `Err` with the partially resolved settings + /// (ignoring any invalid settings). Error messages about the invalid settings are logged with tracing. + #[expect( + clippy::result_large_err, + reason = "The error is as large as the Ok variant" + )] + pub(crate) fn into_settings(self) -> Result { + let code_action = self.code_action.unwrap_or_default(); + let lint = self.lint.unwrap_or_default(); + let format = self.format.unwrap_or_default(); + let mut contains_invalid_settings = false; + + let configuration = self.configuration.and_then(|configuration| { + match ResolvedConfiguration::try_from(configuration) { + Ok(configuration) => Some(configuration), + Err(err) => { + tracing::error!("Failed to load settings from `configuration`: {err}"); + contains_invalid_settings = true; + None + } + } + }); + + let editor_settings = EditorSettings { + configuration, + lint_preview: lint.preview, + format_preview: format.preview, + select: lint.select.and_then(|select| { + Self::resolve_rules( + &select, + RuleSelectorKey::Select, + &mut contains_invalid_settings, + ) + }), + extend_select: lint.extend_select.and_then(|select| { + Self::resolve_rules( + &select, + RuleSelectorKey::ExtendSelect, + &mut contains_invalid_settings, + ) + }), + ignore: lint.ignore.and_then(|ignore| { + Self::resolve_rules( + &ignore, + RuleSelectorKey::Ignore, + &mut contains_invalid_settings, + ) + }), + exclude: self.exclude.clone(), + line_length: self.line_length, + configuration_preference: self.configuration_preference.unwrap_or_default(), + }; + + let resolved = ClientSettings { + editor_settings, + fix_all: self.fix_all.unwrap_or(true), + organize_imports: self.organize_imports.unwrap_or(true), + lint_enable: lint.enable.unwrap_or(true), + disable_rule_comment_enable: code_action + .disable_rule_comment + .and_then(|disable| disable.enable) + .unwrap_or(true), + fix_violation_enable: code_action + .fix_violation + .and_then(|fix| fix.enable) + .unwrap_or(true), + + show_syntax_errors: self.show_syntax_errors.unwrap_or(true), + }; + + if contains_invalid_settings { + Err(resolved) + } else { + Ok(resolved) + } + } + + fn resolve_rules( + rules: &[String], + key: RuleSelectorKey, + contains_invalid_settings: &mut bool, + ) -> Option> { + let (mut known, mut unknown) = (vec![], vec![]); + for rule in rules { + match RuleSelector::from_str(rule) { + Ok(selector) => known.push(selector), + Err(ParseError::Unknown(_)) => unknown.push(rule), + } + } + if !unknown.is_empty() { + *contains_invalid_settings = true; + tracing::error!("Unknown rule selectors found in `{key}`: {unknown:?}"); + } + if known.is_empty() { None } else { Some(known) } + } + + /// Update the preview flag for the linter and the formatter with the given value. + pub(crate) fn set_preview(&mut self, preview: bool) { + match self.lint.as_mut() { + None => self.lint = Some(LintOptions::default().with_preview(preview)), + Some(lint) => lint.set_preview(preview), + } + match self.format.as_mut() { + None => self.format = Some(FormatOptions::default().with_preview(preview)), + Some(format) => format.set_preview(preview), + } + } +} + +impl Combine for ClientOptions { + fn combine_with(&mut self, other: Self) { + self.configuration.combine_with(other.configuration); + self.fix_all.combine_with(other.fix_all); + self.organize_imports.combine_with(other.organize_imports); + self.lint.combine_with(other.lint); + self.format.combine_with(other.format); + self.code_action.combine_with(other.code_action); + self.exclude.combine_with(other.exclude); + self.line_length.combine_with(other.line_length); + self.configuration_preference + .combine_with(other.configuration_preference); + self.show_syntax_errors + .combine_with(other.show_syntax_errors); + } +} + +/// Settings needed to initialize tracing. These will only be +/// read from the global configuration. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct TracingOptions { + pub(crate) log_level: Option, + /// Path to the log file - tildes and environment variables are supported. + pub(crate) log_file: Option, +} + +/// This is a direct representation of the workspace settings schema, +/// which inherits the schema of [`ClientOptions`] and adds extra fields +/// to describe the workspace it applies to. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct WorkspaceOptions { + #[serde(flatten)] + options: ClientOptions, + workspace: Url, +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct LintOptions { + enable: Option, + preview: Option, + select: Option>, + extend_select: Option>, + ignore: Option>, +} + +impl LintOptions { + fn with_preview(mut self, preview: bool) -> LintOptions { + self.preview = Some(preview); + self + } + + fn set_preview(&mut self, preview: bool) { + self.preview = Some(preview); + } +} + +impl Combine for LintOptions { + fn combine_with(&mut self, other: Self) { + self.enable.combine_with(other.enable); + self.preview.combine_with(other.preview); + self.select.combine_with(other.select); + self.extend_select.combine_with(other.extend_select); + self.ignore.combine_with(other.ignore); + } +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct FormatOptions { + preview: Option, +} + +impl Combine for FormatOptions { + fn combine_with(&mut self, other: Self) { + self.preview.combine_with(other.preview); + } +} + +impl FormatOptions { + fn with_preview(mut self, preview: bool) -> FormatOptions { + self.preview = Some(preview); + self + } + + fn set_preview(&mut self, preview: bool) { + self.preview = Some(preview); + } +} + +#[derive(Clone, Debug, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct CodeActionOptions { + disable_rule_comment: Option, + fix_violation: Option, +} + +impl Combine for CodeActionOptions { + fn combine_with(&mut self, other: Self) { + self.disable_rule_comment + .combine_with(other.disable_rule_comment); + self.fix_violation.combine_with(other.fix_violation); + } +} + +#[derive(Clone, Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct CodeActionParameters { + enable: Option, +} + +impl Combine for CodeActionParameters { + fn combine_with(&mut self, other: Self) { + self.enable.combine_with(other.enable); + } +} + +/// This is the exact schema for initialization options sent in by the client +/// during initialization. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(untagged)] +enum InitializationOptions { + #[serde(rename_all = "camelCase")] + HasWorkspaces { + #[serde(rename = "globalSettings")] + global: GlobalOptions, + #[serde(rename = "settings")] + workspace: Vec, + }, + GlobalOnly { + #[serde(default)] + settings: GlobalOptions, + }, +} + +impl Default for InitializationOptions { + fn default() -> Self { + Self::GlobalOnly { + settings: GlobalOptions::default(), + } + } +} + +/// Built from the initialization options provided by the client. +#[derive(Debug)] +pub(crate) struct AllOptions { + pub(crate) global: GlobalOptions, + /// If this is `None`, the client only passed in global settings. + pub(crate) workspace: Option, +} + +impl AllOptions { + /// Initializes the controller from the serialized initialization options. + /// This fails if `options` are not valid initialization options. + pub(crate) fn from_value(options: serde_json::Value, client: &Client) -> Self { + Self::from_init_options( + serde_json::from_value(options) + .map_err(|err| { + tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); + client.show_error_message("Ruff received invalid client settings - falling back to default client settings."); + }) + .unwrap_or_default(), + ) + } + + /// Update the preview flag for both the global and all workspace settings. + pub(crate) fn set_preview(&mut self, preview: bool) { + self.global.set_preview(preview); + if let Some(workspace_options) = self.workspace.as_mut() { + for options in workspace_options.values_mut() { + options.set_preview(preview); + } + } + } + + fn from_init_options(options: InitializationOptions) -> Self { + let (global_options, workspace_options) = match options { + InitializationOptions::GlobalOnly { settings: options } => (options, None), + InitializationOptions::HasWorkspaces { + global: global_options, + workspace: workspace_options, + } => (global_options, Some(workspace_options)), + }; + + Self { + global: global_options, + workspace: workspace_options.map(|workspace_options| { + workspace_options + .into_iter() + .map(|workspace_options| { + (workspace_options.workspace, workspace_options.options) + }) + .collect() + }), + } + } +} + +#[derive(Copy, Clone)] +enum RuleSelectorKey { + Select, + ExtendSelect, + Ignore, +} + +impl std::fmt::Display for RuleSelectorKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RuleSelectorKey::Select => f.write_str("lint.select"), + RuleSelectorKey::ExtendSelect => f.write_str("lint.extendSelect"), + RuleSelectorKey::Ignore => f.write_str("lint.ignore"), + } + } +} + +pub(crate) trait Combine { + #[must_use] + fn combine(mut self, other: Self) -> Self + where + Self: Sized, + { + self.combine_with(other); + self + } + + fn combine_with(&mut self, other: Self); +} + +impl Combine for Option +where + T: Combine, +{ + fn combine(self, other: Self) -> Self + where + Self: Sized, + { + match (self, other) { + (Some(a), Some(b)) => Some(a.combine(b)), + (None, Some(b)) => Some(b), + (a, _) => a, + } + } + + fn combine_with(&mut self, other: Self) { + match (self, other) { + (Some(a), Some(b)) => { + a.combine_with(b); + } + (a @ None, Some(b)) => { + *a = Some(b); + } + _ => {} + } + } +} + +impl Combine for Vec { + fn combine_with(&mut self, _other: Self) { + // No-op, use own elements + } +} + +/// Implements [`Combine`] for a value that always returns `self` when combined with another value. +macro_rules! impl_noop_combine { + ($name:ident) => { + impl Combine for $name { + #[inline(always)] + fn combine_with(&mut self, _other: Self) {} + + #[inline(always)] + fn combine(self, _other: Self) -> Self { + self + } + } + }; +} + +// std types +impl_noop_combine!(bool); +impl_noop_combine!(usize); +impl_noop_combine!(u8); +impl_noop_combine!(u16); +impl_noop_combine!(u32); +impl_noop_combine!(u64); +impl_noop_combine!(u128); +impl_noop_combine!(isize); +impl_noop_combine!(i8); +impl_noop_combine!(i16); +impl_noop_combine!(i32); +impl_noop_combine!(i64); +impl_noop_combine!(i128); +impl_noop_combine!(String); + +// Custom types +impl_noop_combine!(ConfigurationPreference); +impl_noop_combine!(ClientConfiguration); +impl_noop_combine!(LineLength); + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + use ruff_python_formatter::QuoteStyle; + use ruff_workspace::options::{ + FormatOptions as RuffFormatOptions, LintCommonOptions, LintOptions, Options, + }; + use serde::de::DeserializeOwned; + + #[cfg(not(windows))] + use ruff_linter::registry::Linter; + + use super::*; + + #[cfg(not(windows))] + const VS_CODE_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/vs_code_initialization_options.json"); + const GLOBAL_ONLY_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/global_only.json"); + const EMPTY_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/empty.json"); + + // This fixture contains multiple workspaces with empty initialization options. It only sets + // the `cwd` and the `workspace` value. + const EMPTY_MULTIPLE_WORKSPACE_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/empty_multiple_workspace.json"); + + const INLINE_CONFIGURATION_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/inline_configuration.json"); + + fn deserialize_fixture(content: &str) -> T { + serde_json::from_str(content).expect("test fixture JSON should deserialize") + } + + #[cfg(not(windows))] + #[test] + fn test_vs_code_init_options_deserialize() { + let options: InitializationOptions = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE); + + assert_debug_snapshot!(options, @r#" + HasWorkspaces { + global: GlobalOptions { + client: ClientOptions { + configuration: None, + fix_all: Some( + false, + ), + organize_imports: Some( + true, + ), + lint: Some( + LintOptions { + enable: Some( + true, + ), + preview: Some( + true, + ), + select: Some( + [ + "F", + "I", + ], + ), + extend_select: None, + ignore: None, + }, + ), + format: Some( + FormatOptions { + preview: None, + }, + ), + code_action: Some( + CodeActionOptions { + disable_rule_comment: Some( + CodeActionParameters { + enable: Some( + false, + ), + }, + ), + fix_violation: Some( + CodeActionParameters { + enable: Some( + false, + ), + }, + ), + }, + ), + exclude: None, + line_length: None, + configuration_preference: None, + show_syntax_errors: Some( + true, + ), + }, + tracing: TracingOptions { + log_level: None, + log_file: None, + }, + }, + workspace: [ + WorkspaceOptions { + options: ClientOptions { + configuration: None, + fix_all: Some( + true, + ), + organize_imports: Some( + true, + ), + lint: Some( + LintOptions { + enable: Some( + true, + ), + preview: None, + select: None, + extend_select: None, + ignore: None, + }, + ), + format: Some( + FormatOptions { + preview: None, + }, + ), + code_action: Some( + CodeActionOptions { + disable_rule_comment: Some( + CodeActionParameters { + enable: Some( + false, + ), + }, + ), + fix_violation: Some( + CodeActionParameters { + enable: Some( + false, + ), + }, + ), + }, + ), + exclude: None, + line_length: None, + configuration_preference: None, + show_syntax_errors: Some( + true, + ), + }, + workspace: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/Users/test/projects/pandas", + query: None, + fragment: None, + }, + }, + WorkspaceOptions { + options: ClientOptions { + configuration: None, + fix_all: Some( + true, + ), + organize_imports: Some( + true, + ), + lint: Some( + LintOptions { + enable: Some( + true, + ), + preview: Some( + false, + ), + select: None, + extend_select: None, + ignore: None, + }, + ), + format: Some( + FormatOptions { + preview: None, + }, + ), + code_action: Some( + CodeActionOptions { + disable_rule_comment: Some( + CodeActionParameters { + enable: Some( + true, + ), + }, + ), + fix_violation: Some( + CodeActionParameters { + enable: Some( + false, + ), + }, + ), + }, + ), + exclude: None, + line_length: None, + configuration_preference: None, + show_syntax_errors: Some( + true, + ), + }, + workspace: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/Users/test/projects/scipy", + query: None, + fragment: None, + }, + }, + ], + } + "#); + } + + #[cfg(not(windows))] + #[test] + fn test_vs_code_workspace_settings_resolve() { + let options = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE); + let AllOptions { + global, + workspace: workspace_options, + } = AllOptions::from_init_options(options); + let path = + Url::from_str("file:///Users/test/projects/pandas").expect("path should be valid"); + let all_workspace_options = workspace_options.expect("workspace options should exist"); + + let workspace_options = all_workspace_options + .get(&path) + .expect("workspace options should exist") + .clone(); + let workspace_settings = workspace_options + .combine(global.client().clone()) + .into_settings() + .unwrap(); + + assert_eq!( + workspace_settings, + ClientSettings { + fix_all: true, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: false, + fix_violation_enable: false, + show_syntax_errors: true, + editor_settings: EditorSettings { + configuration: None, + lint_preview: Some(true), + format_preview: None, + select: Some(vec![ + RuleSelector::Linter(Linter::Pyflakes), + RuleSelector::Linter(Linter::Isort) + ]), + extend_select: None, + ignore: None, + exclude: None, + line_length: None, + configuration_preference: ConfigurationPreference::default(), + }, + } + ); + let path = + Url::from_str("file:///Users/test/projects/scipy").expect("path should be valid"); + let workspace_options = all_workspace_options + .get(&path) + .expect("workspace setting should exist") + .clone(); + + let workspace_settings = workspace_options + .combine(global.client().clone()) + .into_settings() + .unwrap(); + + assert_eq!( + workspace_settings, + ClientSettings { + fix_all: true, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: true, + fix_violation_enable: false, + show_syntax_errors: true, + editor_settings: EditorSettings { + configuration: None, + lint_preview: Some(false), + format_preview: None, + select: Some(vec![ + RuleSelector::Linter(Linter::Pyflakes), + RuleSelector::Linter(Linter::Isort) + ]), + extend_select: None, + ignore: None, + exclude: None, + line_length: None, + configuration_preference: ConfigurationPreference::EditorFirst, + }, + } + ); + } + + #[test] + fn test_global_only_init_options_deserialize() { + let options: InitializationOptions = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE); + + assert_debug_snapshot!(options, @r#" + GlobalOnly { + settings: GlobalOptions { + client: ClientOptions { + configuration: None, + fix_all: Some( + false, + ), + organize_imports: None, + lint: Some( + LintOptions { + enable: None, + preview: None, + select: None, + extend_select: None, + ignore: Some( + [ + "RUF001", + ], + ), + }, + ), + format: None, + code_action: Some( + CodeActionOptions { + disable_rule_comment: Some( + CodeActionParameters { + enable: Some( + false, + ), + }, + ), + fix_violation: None, + }, + ), + exclude: Some( + [ + "third_party", + ], + ), + line_length: Some( + LineLength( + 80, + ), + ), + configuration_preference: None, + show_syntax_errors: None, + }, + tracing: TracingOptions { + log_level: Some( + Warn, + ), + log_file: None, + }, + }, + } + "#); + } + + #[test] + fn test_global_only_resolves_correctly() { + let (main_loop_sender, main_loop_receiver) = crossbeam::channel::unbounded(); + let (client_sender, client_receiver) = crossbeam::channel::unbounded(); + + let options = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE); + + let AllOptions { global, .. } = AllOptions::from_init_options(options); + let client = Client::new(main_loop_sender, client_sender); + let global = global.into_settings(client); + assert_eq!( + global.to_settings(), + &ClientSettings { + fix_all: false, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: false, + fix_violation_enable: true, + show_syntax_errors: true, + editor_settings: EditorSettings { + configuration: None, + lint_preview: None, + format_preview: None, + select: None, + extend_select: None, + ignore: Some(vec![RuleSelector::from_str("RUF001").unwrap()]), + exclude: Some(vec!["third_party".into()]), + line_length: Some(LineLength::try_from(80).unwrap()), + configuration_preference: ConfigurationPreference::EditorFirst, + }, + } + ); + + assert!(main_loop_receiver.is_empty()); + assert!(client_receiver.is_empty()); + } + + #[test] + fn test_empty_init_options_deserialize() { + let options: InitializationOptions = deserialize_fixture(EMPTY_INIT_OPTIONS_FIXTURE); + + assert_eq!(options, InitializationOptions::default()); + } + + fn assert_preview_client_options(options: &ClientOptions, preview: bool) { + assert_eq!(options.lint.as_ref().unwrap().preview.unwrap(), preview); + assert_eq!(options.format.as_ref().unwrap().preview.unwrap(), preview); + } + + fn assert_preview_all_options(all_options: &AllOptions, preview: bool) { + assert_preview_client_options(all_options.global.client(), preview); + if let Some(workspace_options) = all_options.workspace.as_ref() { + for options in workspace_options.values() { + assert_preview_client_options(options, preview); + } + } + } + + #[test] + fn test_preview_flag() { + let options = deserialize_fixture(EMPTY_MULTIPLE_WORKSPACE_INIT_OPTIONS_FIXTURE); + let mut all_options = AllOptions::from_init_options(options); + + all_options.set_preview(false); + assert_preview_all_options(&all_options, false); + + all_options.set_preview(true); + assert_preview_all_options(&all_options, true); + } + + #[test] + fn inline_configuration() { + let (main_loop_sender, main_loop_receiver) = crossbeam::channel::unbounded(); + let (client_sender, client_receiver) = crossbeam::channel::unbounded(); + let client = Client::new(main_loop_sender, client_sender); + + let options: InitializationOptions = deserialize_fixture(INLINE_CONFIGURATION_FIXTURE); + + let AllOptions { + global, + workspace: None, + } = AllOptions::from_init_options(options) + else { + panic!("Expected global settings only"); + }; + + let global = global.into_settings(client); + + assert_eq!( + global.to_settings(), + &ClientSettings { + fix_all: true, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: true, + fix_violation_enable: true, + show_syntax_errors: true, + editor_settings: EditorSettings { + configuration: Some(ResolvedConfiguration::Inline(Box::new(Options { + line_length: Some(LineLength::try_from(100).unwrap()), + lint: Some(LintOptions { + common: LintCommonOptions { + extend_select: Some(vec![RuleSelector::from_str("I001").unwrap()]), + ..Default::default() + }, + ..Default::default() + }), + format: Some(RuffFormatOptions { + quote_style: Some(QuoteStyle::Single), + ..Default::default() + }), + ..Default::default() + }))), + extend_select: Some(vec![RuleSelector::from_str("RUF001").unwrap()]), + ..Default::default() + } + } + ); + + assert!(main_loop_receiver.is_empty()); + assert!(client_receiver.is_empty()); + } +} diff --git a/crates/ruff_server/src/session/request_queue.rs b/crates/ruff_server/src/session/request_queue.rs new file mode 100644 index 0000000000000..68696050bff87 --- /dev/null +++ b/crates/ruff_server/src/session/request_queue.rs @@ -0,0 +1,198 @@ +use crate::session::client::ClientResponseHandler; +use lsp_server::RequestId; +use rustc_hash::FxHashMap; +use std::cell::{Cell, OnceCell, RefCell}; +use std::fmt::Formatter; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::time::Instant; + +/// Tracks the pending requests between client and server. +pub(crate) struct RequestQueue { + incoming: Incoming, + outgoing: Outgoing, +} + +impl RequestQueue { + pub(super) fn new() -> Self { + Self { + incoming: Incoming::default(), + outgoing: Outgoing::default(), + } + } + + pub(crate) fn outgoing_mut(&mut self) -> &mut Outgoing { + &mut self.outgoing + } + + /// Returns the server to client request queue. + pub(crate) fn outgoing(&self) -> &Outgoing { + &self.outgoing + } + + /// Returns the client to server request queue. + pub(crate) fn incoming(&self) -> &Incoming { + &self.incoming + } + + pub(crate) fn incoming_mut(&mut self) -> &mut Incoming { + &mut self.incoming + } +} + +/// Requests from client -> server. +/// +/// Tracks which requests are pending. Requests that aren't registered are considered completed. +/// +/// A request is pending if: +/// +/// * it has been registered +/// * it hasn't been cancelled +/// * it hasn't been completed +/// +/// Tracking whether a request is pending is required to ensure that the server sends exactly +/// one response for every request as required by the LSP specification. +#[derive(Default, Debug)] +pub(crate) struct Incoming { + pending: FxHashMap, +} + +impl Incoming { + /// Registers a new pending request. + pub(crate) fn register(&mut self, request_id: RequestId, method: String) { + self.pending.insert(request_id, PendingRequest::new(method)); + } + + /// Cancels the pending request with the given id. + /// + /// Returns the method name if the request was still pending, `None` if it was already completed. + pub(super) fn cancel(&mut self, request_id: &RequestId) -> Option { + self.pending.remove(request_id).map(|mut pending| { + if let Some(cancellation_token) = pending.cancellation_token.take() { + cancellation_token.cancel(); + } + pending.method + }) + } + + /// Returns `true` if the request with the given id is still pending. + #[expect(dead_code)] + pub(crate) fn is_pending(&self, request_id: &RequestId) -> bool { + self.pending.contains_key(request_id) + } + + /// Returns the cancellation token for the given request id if the request is still pending. + pub(crate) fn cancellation_token( + &self, + request_id: &RequestId, + ) -> Option { + let pending = self.pending.get(request_id)?; + + Some(RequestCancellationToken::clone( + pending + .cancellation_token + .get_or_init(RequestCancellationToken::default), + )) + } + + /// Marks the request as completed. + /// + /// Returns the time when the request was registered and the request method name, or `None` if the request was not pending. + pub(crate) fn complete(&mut self, request_id: &RequestId) -> Option<(Instant, String)> { + self.pending + .remove(request_id) + .map(|pending| (pending.start_time, pending.method)) + } +} + +/// A request from the client to the server that hasn't been responded yet. +#[derive(Debug)] +struct PendingRequest { + /// The time when the request was registered. + /// + /// This does not include the time the request was queued in the main loop before it was registered. + start_time: Instant, + + /// The method name of the request. + method: String, + + /// A cancellation token to cancel this request. + /// + /// This is only initialized for background requests. Local tasks don't support cancellation (unless retried) + /// as they're processed immediately after receiving the request; Making it impossible for a + /// cancellation message to be processed before the task is completed. + cancellation_token: OnceCell, +} + +impl PendingRequest { + fn new(method: String) -> Self { + Self { + start_time: Instant::now(), + method, + cancellation_token: OnceCell::new(), + } + } +} + +/// Token to cancel a specific request. +/// +/// Can be shared between threads to check for cancellation *after* a request has been scheduled. +#[derive(Debug, Default)] +pub(crate) struct RequestCancellationToken(Arc); + +impl RequestCancellationToken { + /// Returns true if the request was cancelled. + pub(crate) fn is_cancelled(&self) -> bool { + self.0.load(std::sync::atomic::Ordering::Relaxed) + } + + /// Signals that the request should not be processed because it was cancelled. + fn cancel(&self) { + self.0.store(true, std::sync::atomic::Ordering::Relaxed); + } + + fn clone(this: &Self) -> Self { + RequestCancellationToken(this.0.clone()) + } +} + +/// Requests from server -> client. +#[derive(Default)] +pub(crate) struct Outgoing { + /// The id of the next request sent from the server to the client. + next_request_id: Cell, + + /// A map of request ids to the handlers that process the client-response. + response_handlers: RefCell>, +} + +impl Outgoing { + /// Registers a handler, returns the id for the request. + #[must_use] + pub(crate) fn register(&self, handler: ClientResponseHandler) -> RequestId { + let id = self.next_request_id.get(); + self.next_request_id.set(id + 1); + + self.response_handlers + .borrow_mut() + .insert(id.into(), handler); + id.into() + } + + /// Marks the request with the given id as complete and returns the handler to process the response. + /// + /// Returns `None` if the request was not found. + #[must_use] + pub(crate) fn complete(&mut self, request_id: &RequestId) -> Option { + self.response_handlers.get_mut().remove(request_id) + } +} + +impl std::fmt::Debug for Outgoing { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Outgoing") + .field("next_request_id", &self.next_request_id) + .field("response_handlers", &"") + .finish() + } +} diff --git a/crates/ruff_server/src/session/settings.rs b/crates/ruff_server/src/session/settings.rs index b7b24a2ffabd3..94cf41c827d19 100644 --- a/crates/ruff_server/src/session/settings.rs +++ b/crates/ruff_server/src/session/settings.rs @@ -1,33 +1,78 @@ -use std::{ops::Deref, path::PathBuf, str::FromStr}; +use std::{path::PathBuf, sync::Arc}; -use lsp_types::Url; -use rustc_hash::FxHashMap; -use serde::Deserialize; -use serde_json::{Map, Value}; use thiserror::Error; -use ruff_linter::line_width::LineLength; -use ruff_linter::rule_selector::ParseError; use ruff_linter::RuleSelector; +use ruff_linter::line_width::LineLength; use ruff_workspace::options::Options; -/// Maps a workspace URI to its associated client settings. Used during server initialization. -pub(crate) type WorkspaceSettingsMap = FxHashMap; +use crate::{ + ClientOptions, + session::{ + Client, + options::{ClientConfiguration, ConfigurationPreference}, + }, +}; + +pub struct GlobalClientSettings { + pub(super) options: ClientOptions, + + /// Lazily initialized client settings to avoid showing error warnings + /// when a field of the global settings has any errors but the field is overridden + /// in the workspace settings. This can avoid showing unnecessary errors + /// when the workspace settings e.g. select some rules that aren't available in a specific workspace + /// and said workspace overrides the selected rules. + pub(super) settings: std::cell::OnceCell>, + + pub(super) client: Client, +} + +impl GlobalClientSettings { + pub(super) fn options(&self) -> &ClientOptions { + &self.options + } + + fn settings_impl(&self) -> &Arc { + self.settings.get_or_init(|| { + let settings = self.options.clone().into_settings(); + let settings = match settings { + Ok(settings) => settings, + Err(settings) => { + self.client.show_error_message( + "Ruff received invalid settings from the editor. Refer to the logs for more information." + ); + settings + } + }; + Arc::new(settings) + }) + } + + /// Lazily resolves the client options to the settings. + pub(super) fn to_settings(&self) -> &ClientSettings { + self.settings_impl() + } + + /// Lazily resolves the client options to the settings. + pub(super) fn to_settings_arc(&self) -> Arc { + self.settings_impl().clone() + } +} /// Resolved client settings for a specific document. These settings are meant to be /// used directly by the server, and are *not* a 1:1 representation with how the client /// sends them. #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] -#[allow(clippy::struct_excessive_bools)] -pub(crate) struct ResolvedClientSettings { - fix_all: bool, - organize_imports: bool, - lint_enable: bool, - disable_rule_comment_enable: bool, - fix_violation_enable: bool, - show_syntax_errors: bool, - editor_settings: ResolvedEditorSettings, +#[expect(clippy::struct_excessive_bools)] +pub(crate) struct ClientSettings { + pub(super) fix_all: bool, + pub(super) organize_imports: bool, + pub(super) lint_enable: bool, + pub(super) disable_rule_comment_enable: bool, + pub(super) fix_violation_enable: bool, + pub(super) show_syntax_errors: bool, + pub(super) editor_settings: EditorSettings, } /// Contains the resolved values of 'editor settings' - Ruff configuration for the linter/formatter that was passed in via @@ -35,7 +80,7 @@ pub(crate) struct ResolvedClientSettings { /// if these were un-set. #[derive(Clone, Debug)] #[cfg_attr(test, derive(Default, PartialEq, Eq))] -pub(crate) struct ResolvedEditorSettings { +pub(crate) struct EditorSettings { pub(super) configuration: Option, pub(super) lint_preview: Option, pub(super) format_preview: Option, @@ -52,23 +97,23 @@ pub(crate) struct ResolvedEditorSettings { #[cfg_attr(test, derive(PartialEq, Eq))] pub(crate) enum ResolvedConfiguration { FilePath(PathBuf), - Inline(Options), + Inline(Box), } -impl TryFrom<&ClientConfiguration> for ResolvedConfiguration { +impl TryFrom for ResolvedConfiguration { type Error = ResolvedConfigurationError; - fn try_from(value: &ClientConfiguration) -> Result { + fn try_from(value: ClientConfiguration) -> Result { match value { ClientConfiguration::String(path) => Ok(ResolvedConfiguration::FilePath( - PathBuf::from(shellexpand::full(path)?.as_ref()), + PathBuf::from(shellexpand::full(&path)?.as_ref()), )), ClientConfiguration::Object(map) => { let options = toml::Table::try_from(map)?.try_into::()?; if options.extend.is_some() { Err(ResolvedConfigurationError::ExtendNotSupported) } else { - Ok(ResolvedConfiguration::Inline(options)) + Ok(ResolvedConfiguration::Inline(Box::new(options))) } } } @@ -89,408 +134,7 @@ pub(crate) enum ResolvedConfigurationError { ExtendNotSupported, } -/// Determines how multiple conflicting configurations should be resolved - in this -/// case, the configuration from the client settings and configuration from local -/// `.toml` files (aka 'workspace' configuration). -#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub(crate) enum ConfigurationPreference { - /// Configuration set in the editor takes priority over configuration set in `.toml` files. - #[default] - EditorFirst, - /// Configuration set in `.toml` files takes priority over configuration set in the editor. - FilesystemFirst, - /// `.toml` files are ignored completely, and only the editor configuration is used. - EditorOnly, -} - -/// A direct representation of of `configuration` schema within the client settings. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(untagged)] -enum ClientConfiguration { - /// A path to a configuration file. - String(String), - /// An object containing the configuration options. - Object(Map), -} - -/// This is a direct representation of the settings schema sent by the client. -#[derive(Debug, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub struct ClientSettings { - configuration: Option, - fix_all: Option, - organize_imports: Option, - lint: Option, - format: Option, - code_action: Option, - exclude: Option>, - line_length: Option, - configuration_preference: Option, - - /// If `true` or [`None`], show syntax errors as diagnostics. - /// - /// This is useful when using Ruff with other language servers, allowing the user to refer - /// to syntax errors from only one source. - show_syntax_errors: Option, - - // These settings are only needed for tracing, and are only read from the global configuration. - // These will not be in the resolved settings. - #[serde(flatten)] - pub(crate) tracing: TracingSettings, -} - impl ClientSettings { - /// Update the preview flag for the linter and the formatter with the given value. - pub(crate) fn set_preview(&mut self, preview: bool) { - match self.lint.as_mut() { - None => self.lint = Some(LintOptions::default().with_preview(preview)), - Some(lint) => lint.set_preview(preview), - } - match self.format.as_mut() { - None => self.format = Some(FormatOptions::default().with_preview(preview)), - Some(format) => format.set_preview(preview), - } - } -} - -/// Settings needed to initialize tracing. These will only be -/// read from the global configuration. -#[derive(Debug, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub(crate) struct TracingSettings { - pub(crate) log_level: Option, - /// Path to the log file - tildes and environment variables are supported. - pub(crate) log_file: Option, -} - -/// This is a direct representation of the workspace settings schema, -/// which inherits the schema of [`ClientSettings`] and adds extra fields -/// to describe the workspace it applies to. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct WorkspaceSettings { - #[serde(flatten)] - settings: ClientSettings, - workspace: Url, -} - -#[derive(Debug, Default, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct LintOptions { - enable: Option, - preview: Option, - select: Option>, - extend_select: Option>, - ignore: Option>, -} - -impl LintOptions { - fn with_preview(mut self, preview: bool) -> LintOptions { - self.preview = Some(preview); - self - } - - fn set_preview(&mut self, preview: bool) { - self.preview = Some(preview); - } -} - -#[derive(Debug, Default, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct FormatOptions { - preview: Option, -} - -impl FormatOptions { - fn with_preview(mut self, preview: bool) -> FormatOptions { - self.preview = Some(preview); - self - } - - fn set_preview(&mut self, preview: bool) { - self.preview = Some(preview); - } -} - -#[derive(Debug, Default, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct CodeActionOptions { - disable_rule_comment: Option, - fix_violation: Option, -} - -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct CodeActionParameters { - enable: Option, -} - -/// This is the exact schema for initialization options sent in by the client -/// during initialization. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(untagged)] -enum InitializationOptions { - #[serde(rename_all = "camelCase")] - HasWorkspaces { - global_settings: ClientSettings, - #[serde(rename = "settings")] - workspace_settings: Vec, - }, - GlobalOnly { - #[serde(default)] - settings: ClientSettings, - }, -} - -/// Built from the initialization options provided by the client. -#[derive(Debug)] -pub(crate) struct AllSettings { - pub(crate) global_settings: ClientSettings, - /// If this is `None`, the client only passed in global settings. - pub(crate) workspace_settings: Option, -} - -impl AllSettings { - /// Initializes the controller from the serialized initialization options. - /// This fails if `options` are not valid initialization options. - pub(crate) fn from_value(options: serde_json::Value) -> Self { - Self::from_init_options( - serde_json::from_value(options) - .map_err(|err| { - tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); - show_err_msg!("Ruff received invalid client settings - falling back to default client settings."); - }) - .unwrap_or_default(), - ) - } - - /// Update the preview flag for both the global and all workspace settings. - pub(crate) fn set_preview(&mut self, preview: bool) { - self.global_settings.set_preview(preview); - if let Some(workspace_settings) = self.workspace_settings.as_mut() { - for settings in workspace_settings.values_mut() { - settings.set_preview(preview); - } - } - } - - fn from_init_options(options: InitializationOptions) -> Self { - let (global_settings, workspace_settings) = match options { - InitializationOptions::GlobalOnly { settings } => (settings, None), - InitializationOptions::HasWorkspaces { - global_settings, - workspace_settings, - } => (global_settings, Some(workspace_settings)), - }; - - Self { - global_settings, - workspace_settings: workspace_settings.map(|workspace_settings| { - workspace_settings - .into_iter() - .map(|settings| (settings.workspace, settings.settings)) - .collect() - }), - } - } -} - -impl ResolvedClientSettings { - /// Resolves a series of client settings, prioritizing workspace settings over global settings. - /// Any fields not specified by either are set to their defaults. - pub(super) fn with_workspace( - workspace_settings: &ClientSettings, - global_settings: &ClientSettings, - ) -> Self { - Self::new_impl(&[workspace_settings, global_settings]) - } - - /// Resolves global settings only. - pub(super) fn global(global_settings: &ClientSettings) -> Self { - Self::new_impl(&[global_settings]) - } - - fn new_impl(all_settings: &[&ClientSettings]) -> Self { - let mut contains_invalid_settings = false; - - let settings = Self { - fix_all: Self::resolve_or(all_settings, |settings| settings.fix_all, true), - organize_imports: Self::resolve_or( - all_settings, - |settings| settings.organize_imports, - true, - ), - lint_enable: Self::resolve_or( - all_settings, - |settings| settings.lint.as_ref()?.enable, - true, - ), - disable_rule_comment_enable: Self::resolve_or( - all_settings, - |settings| { - settings - .code_action - .as_ref()? - .disable_rule_comment - .as_ref()? - .enable - }, - true, - ), - fix_violation_enable: Self::resolve_or( - all_settings, - |settings| { - settings - .code_action - .as_ref()? - .fix_violation - .as_ref()? - .enable - }, - true, - ), - show_syntax_errors: Self::resolve_or( - all_settings, - |settings| settings.show_syntax_errors, - true, - ), - editor_settings: ResolvedEditorSettings { - configuration: Self::resolve_optional(all_settings, |settings| { - settings.configuration.as_ref().and_then(|configuration| { - match ResolvedConfiguration::try_from(configuration) { - Ok(configuration) => Some(configuration), - Err(err) => { - tracing::error!( - "Failed to load settings from `configuration`: {err}" - ); - contains_invalid_settings = true; - None - } - } - }) - }), - lint_preview: Self::resolve_optional(all_settings, |settings| { - settings.lint.as_ref()?.preview - }), - format_preview: Self::resolve_optional(all_settings, |settings| { - settings.format.as_ref()?.preview - }), - select: Self::resolve_optional(all_settings, |settings| { - Self::resolve_rules( - settings.lint.as_ref()?.select.as_ref()?, - RuleSelectorKey::Select, - &mut contains_invalid_settings, - ) - }), - extend_select: Self::resolve_optional(all_settings, |settings| { - Self::resolve_rules( - settings.lint.as_ref()?.extend_select.as_ref()?, - RuleSelectorKey::ExtendSelect, - &mut contains_invalid_settings, - ) - }), - ignore: Self::resolve_optional(all_settings, |settings| { - Self::resolve_rules( - settings.lint.as_ref()?.ignore.as_ref()?, - RuleSelectorKey::Ignore, - &mut contains_invalid_settings, - ) - }), - exclude: Self::resolve_optional(all_settings, |settings| settings.exclude.clone()), - line_length: Self::resolve_optional(all_settings, |settings| settings.line_length), - configuration_preference: Self::resolve_or( - all_settings, - |settings| settings.configuration_preference, - ConfigurationPreference::EditorFirst, - ), - }, - }; - - if contains_invalid_settings { - show_err_msg!( - "Ruff received invalid settings from the editor. Refer to the logs for more information." - ); - } - - settings - } - - fn resolve_rules( - rules: &[String], - key: RuleSelectorKey, - contains_invalid_settings: &mut bool, - ) -> Option> { - let (mut known, mut unknown) = (vec![], vec![]); - for rule in rules { - match RuleSelector::from_str(rule) { - Ok(selector) => known.push(selector), - Err(ParseError::Unknown(_)) => unknown.push(rule), - } - } - if !unknown.is_empty() { - *contains_invalid_settings = true; - tracing::error!("Unknown rule selectors found in `{key}`: {unknown:?}"); - } - if known.is_empty() { - None - } else { - Some(known) - } - } - - /// Attempts to resolve a setting using a list of available client settings as sources. - /// Client settings that come earlier in the list take priority. This function is for fields - /// that do not have a default value and should be left unset. - /// Use [`ResolvedClientSettings::resolve_or`] for settings that should have default values. - fn resolve_optional( - all_settings: &[&ClientSettings], - get: impl FnMut(&ClientSettings) -> Option, - ) -> Option { - all_settings.iter().map(Deref::deref).find_map(get) - } - - /// Attempts to resolve a setting using a list of available client settings as sources. - /// Client settings that come earlier in the list take priority. `default` will be returned - /// if none of the settings specify the requested setting. - /// Use [`ResolvedClientSettings::resolve_optional`] if the setting should be optional instead - /// of having a default value. - fn resolve_or( - all_settings: &[&ClientSettings], - get: impl Fn(&ClientSettings) -> Option, - default: T, - ) -> T { - Self::resolve_optional(all_settings, get).unwrap_or(default) - } -} - -#[derive(Copy, Clone)] -enum RuleSelectorKey { - Select, - ExtendSelect, - Ignore, -} - -impl std::fmt::Display for RuleSelectorKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RuleSelectorKey::Select => f.write_str("lint.select"), - RuleSelectorKey::ExtendSelect => f.write_str("lint.extendSelect"), - RuleSelectorKey::Ignore => f.write_str("lint.ignore"), - } - } -} - -impl ResolvedClientSettings { pub(crate) fn fix_all(&self) -> bool { self.fix_all } @@ -515,501 +159,7 @@ impl ResolvedClientSettings { self.show_syntax_errors } - pub(crate) fn editor_settings(&self) -> &ResolvedEditorSettings { + pub(crate) fn editor_settings(&self) -> &EditorSettings { &self.editor_settings } } - -impl Default for InitializationOptions { - fn default() -> Self { - Self::GlobalOnly { - settings: ClientSettings::default(), - } - } -} - -#[cfg(test)] -mod tests { - use insta::assert_debug_snapshot; - use ruff_python_formatter::QuoteStyle; - use ruff_workspace::options::{ - FormatOptions as RuffFormatOptions, LintCommonOptions, LintOptions, - }; - use serde::de::DeserializeOwned; - - #[cfg(not(windows))] - use ruff_linter::registry::Linter; - - use super::*; - - #[cfg(not(windows))] - const VS_CODE_INIT_OPTIONS_FIXTURE: &str = - include_str!("../../resources/test/fixtures/settings/vs_code_initialization_options.json"); - const GLOBAL_ONLY_INIT_OPTIONS_FIXTURE: &str = - include_str!("../../resources/test/fixtures/settings/global_only.json"); - const EMPTY_INIT_OPTIONS_FIXTURE: &str = - include_str!("../../resources/test/fixtures/settings/empty.json"); - - // This fixture contains multiple workspaces with empty initialization options. It only sets - // the `cwd` and the `workspace` value. - const EMPTY_MULTIPLE_WORKSPACE_INIT_OPTIONS_FIXTURE: &str = - include_str!("../../resources/test/fixtures/settings/empty_multiple_workspace.json"); - - const INLINE_CONFIGURATION_FIXTURE: &str = - include_str!("../../resources/test/fixtures/settings/inline_configuration.json"); - - fn deserialize_fixture(content: &str) -> T { - serde_json::from_str(content).expect("test fixture JSON should deserialize") - } - - #[cfg(not(windows))] - #[test] - fn test_vs_code_init_options_deserialize() { - let options: InitializationOptions = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE); - - assert_debug_snapshot!(options, @r###" - HasWorkspaces { - global_settings: ClientSettings { - configuration: None, - fix_all: Some( - false, - ), - organize_imports: Some( - true, - ), - lint: Some( - LintOptions { - enable: Some( - true, - ), - preview: Some( - true, - ), - select: Some( - [ - "F", - "I", - ], - ), - extend_select: None, - ignore: None, - }, - ), - format: Some( - FormatOptions { - preview: None, - }, - ), - code_action: Some( - CodeActionOptions { - disable_rule_comment: Some( - CodeActionParameters { - enable: Some( - false, - ), - }, - ), - fix_violation: Some( - CodeActionParameters { - enable: Some( - false, - ), - }, - ), - }, - ), - exclude: None, - line_length: None, - configuration_preference: None, - show_syntax_errors: Some( - true, - ), - tracing: TracingSettings { - log_level: None, - log_file: None, - }, - }, - workspace_settings: [ - WorkspaceSettings { - settings: ClientSettings { - configuration: None, - fix_all: Some( - true, - ), - organize_imports: Some( - true, - ), - lint: Some( - LintOptions { - enable: Some( - true, - ), - preview: None, - select: None, - extend_select: None, - ignore: None, - }, - ), - format: Some( - FormatOptions { - preview: None, - }, - ), - code_action: Some( - CodeActionOptions { - disable_rule_comment: Some( - CodeActionParameters { - enable: Some( - false, - ), - }, - ), - fix_violation: Some( - CodeActionParameters { - enable: Some( - false, - ), - }, - ), - }, - ), - exclude: None, - line_length: None, - configuration_preference: None, - show_syntax_errors: Some( - true, - ), - tracing: TracingSettings { - log_level: None, - log_file: None, - }, - }, - workspace: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/Users/test/projects/pandas", - query: None, - fragment: None, - }, - }, - WorkspaceSettings { - settings: ClientSettings { - configuration: None, - fix_all: Some( - true, - ), - organize_imports: Some( - true, - ), - lint: Some( - LintOptions { - enable: Some( - true, - ), - preview: Some( - false, - ), - select: None, - extend_select: None, - ignore: None, - }, - ), - format: Some( - FormatOptions { - preview: None, - }, - ), - code_action: Some( - CodeActionOptions { - disable_rule_comment: Some( - CodeActionParameters { - enable: Some( - true, - ), - }, - ), - fix_violation: Some( - CodeActionParameters { - enable: Some( - false, - ), - }, - ), - }, - ), - exclude: None, - line_length: None, - configuration_preference: None, - show_syntax_errors: Some( - true, - ), - tracing: TracingSettings { - log_level: None, - log_file: None, - }, - }, - workspace: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/Users/test/projects/scipy", - query: None, - fragment: None, - }, - }, - ], - } - "###); - } - - #[cfg(not(windows))] - #[test] - fn test_vs_code_workspace_settings_resolve() { - let options = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE); - let AllSettings { - global_settings, - workspace_settings, - } = AllSettings::from_init_options(options); - let path = - Url::from_str("file:///Users/test/projects/pandas").expect("path should be valid"); - let workspace_settings = workspace_settings.expect("workspace settings should exist"); - assert_eq!( - ResolvedClientSettings::with_workspace( - workspace_settings - .get(&path) - .expect("workspace setting should exist"), - &global_settings - ), - ResolvedClientSettings { - fix_all: true, - organize_imports: true, - lint_enable: true, - disable_rule_comment_enable: false, - fix_violation_enable: false, - show_syntax_errors: true, - editor_settings: ResolvedEditorSettings { - configuration: None, - lint_preview: Some(true), - format_preview: None, - select: Some(vec![ - RuleSelector::Linter(Linter::Pyflakes), - RuleSelector::Linter(Linter::Isort) - ]), - extend_select: None, - ignore: None, - exclude: None, - line_length: None, - configuration_preference: ConfigurationPreference::default(), - }, - } - ); - let path = - Url::from_str("file:///Users/test/projects/scipy").expect("path should be valid"); - assert_eq!( - ResolvedClientSettings::with_workspace( - workspace_settings - .get(&path) - .expect("workspace setting should exist"), - &global_settings - ), - ResolvedClientSettings { - fix_all: true, - organize_imports: true, - lint_enable: true, - disable_rule_comment_enable: true, - fix_violation_enable: false, - show_syntax_errors: true, - editor_settings: ResolvedEditorSettings { - configuration: None, - lint_preview: Some(false), - format_preview: None, - select: Some(vec![ - RuleSelector::Linter(Linter::Pyflakes), - RuleSelector::Linter(Linter::Isort) - ]), - extend_select: None, - ignore: None, - exclude: None, - line_length: None, - configuration_preference: ConfigurationPreference::EditorFirst, - }, - } - ); - } - - #[test] - fn test_global_only_init_options_deserialize() { - let options: InitializationOptions = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE); - - assert_debug_snapshot!(options, @r#" - GlobalOnly { - settings: ClientSettings { - configuration: None, - fix_all: Some( - false, - ), - organize_imports: None, - lint: Some( - LintOptions { - enable: None, - preview: None, - select: None, - extend_select: None, - ignore: Some( - [ - "RUF001", - ], - ), - }, - ), - format: None, - code_action: Some( - CodeActionOptions { - disable_rule_comment: Some( - CodeActionParameters { - enable: Some( - false, - ), - }, - ), - fix_violation: None, - }, - ), - exclude: Some( - [ - "third_party", - ], - ), - line_length: Some( - LineLength( - 80, - ), - ), - configuration_preference: None, - show_syntax_errors: None, - tracing: TracingSettings { - log_level: Some( - Warn, - ), - log_file: None, - }, - }, - } - "#); - } - - #[test] - fn test_global_only_resolves_correctly() { - let options = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE); - - let AllSettings { - global_settings, .. - } = AllSettings::from_init_options(options); - assert_eq!( - ResolvedClientSettings::global(&global_settings), - ResolvedClientSettings { - fix_all: false, - organize_imports: true, - lint_enable: true, - disable_rule_comment_enable: false, - fix_violation_enable: true, - show_syntax_errors: true, - editor_settings: ResolvedEditorSettings { - configuration: None, - lint_preview: None, - format_preview: None, - select: None, - extend_select: None, - ignore: Some(vec![RuleSelector::from_str("RUF001").unwrap()]), - exclude: Some(vec!["third_party".into()]), - line_length: Some(LineLength::try_from(80).unwrap()), - configuration_preference: ConfigurationPreference::EditorFirst, - }, - } - ); - } - - #[test] - fn test_empty_init_options_deserialize() { - let options: InitializationOptions = deserialize_fixture(EMPTY_INIT_OPTIONS_FIXTURE); - - assert_eq!(options, InitializationOptions::default()); - } - - fn assert_preview_client_settings(settings: &ClientSettings, preview: bool) { - assert_eq!(settings.lint.as_ref().unwrap().preview.unwrap(), preview); - assert_eq!(settings.format.as_ref().unwrap().preview.unwrap(), preview); - } - - fn assert_preview_all_settings(all_settings: &AllSettings, preview: bool) { - assert_preview_client_settings(&all_settings.global_settings, preview); - if let Some(workspace_settings) = all_settings.workspace_settings.as_ref() { - for settings in workspace_settings.values() { - assert_preview_client_settings(settings, preview); - } - } - } - - #[test] - fn test_preview_flag() { - let options = deserialize_fixture(EMPTY_MULTIPLE_WORKSPACE_INIT_OPTIONS_FIXTURE); - let mut all_settings = AllSettings::from_init_options(options); - - all_settings.set_preview(false); - assert_preview_all_settings(&all_settings, false); - - all_settings.set_preview(true); - assert_preview_all_settings(&all_settings, true); - } - - #[test] - fn inline_configuration() { - let options: InitializationOptions = deserialize_fixture(INLINE_CONFIGURATION_FIXTURE); - - let AllSettings { - global_settings, - workspace_settings: None, - } = AllSettings::from_init_options(options) - else { - panic!("Expected global settings only"); - }; - - assert_eq!( - ResolvedClientSettings::global(&global_settings), - ResolvedClientSettings { - fix_all: true, - organize_imports: true, - lint_enable: true, - disable_rule_comment_enable: true, - fix_violation_enable: true, - show_syntax_errors: true, - editor_settings: ResolvedEditorSettings { - configuration: Some(ResolvedConfiguration::Inline(Options { - line_length: Some(LineLength::try_from(100).unwrap()), - lint: Some(LintOptions { - common: LintCommonOptions { - extend_select: Some(vec![RuleSelector::from_str("I001").unwrap()]), - ..Default::default() - }, - ..Default::default() - }), - format: Some(RuffFormatOptions { - quote_style: Some(QuoteStyle::Single), - ..Default::default() - }), - ..Default::default() - })), - extend_select: Some(vec![RuleSelector::from_str("RUF001").unwrap()]), - ..Default::default() - } - } - ); - } -} diff --git a/crates/ruff_server/src/workspace.rs b/crates/ruff_server/src/workspace.rs index b83a63b12f7f3..d8274e7669667 100644 --- a/crates/ruff_server/src/workspace.rs +++ b/crates/ruff_server/src/workspace.rs @@ -3,8 +3,7 @@ use std::ops::Deref; use lsp_types::{Url, WorkspaceFolder}; use thiserror::Error; -use crate::session::WorkspaceSettingsMap; -use crate::ClientSettings; +use crate::session::{ClientOptions, WorkspaceOptionsMap}; #[derive(Debug)] pub struct Workspaces(Vec); @@ -18,15 +17,15 @@ impl Workspaces { /// initialization. pub(crate) fn from_workspace_folders( workspace_folders: Option>, - mut workspace_settings: WorkspaceSettingsMap, + mut workspace_options: WorkspaceOptionsMap, ) -> std::result::Result { - let mut client_settings_for_url = |url: &Url| { - workspace_settings.remove(url).unwrap_or_else(|| { + let mut client_options_for_url = |url: &Url| { + workspace_options.remove(url).unwrap_or_else(|| { tracing::info!( - "No workspace settings found for {}, using default settings", + "No workspace options found for {}, using default options", url ); - ClientSettings::default() + ClientOptions::default() }) }; @@ -35,8 +34,8 @@ impl Workspaces { folders .into_iter() .map(|folder| { - let settings = client_settings_for_url(&folder.uri); - Workspace::new(folder.uri).with_settings(settings) + let options = client_options_for_url(&folder.uri); + Workspace::new(folder.uri).with_options(options) }) .collect() } else { @@ -48,8 +47,8 @@ impl Workspaces { ); let uri = Url::from_file_path(current_dir) .map_err(|()| WorkspacesError::InvalidCurrentDir)?; - let settings = client_settings_for_url(&uri); - vec![Workspace::default(uri).with_settings(settings)] + let options = client_options_for_url(&uri); + vec![Workspace::default(uri).with_options(options)] }; Ok(Workspaces(workspaces)) @@ -76,8 +75,8 @@ pub(crate) enum WorkspacesError { pub struct Workspace { /// The [`Url`] pointing to the root of the workspace. url: Url, - /// The client settings for this workspace. - settings: Option, + /// The client options for this workspace. + options: Option, /// Whether this is the default workspace as created by the server. This will be the case when /// no workspace folders were provided during initialization. is_default: bool, @@ -88,7 +87,7 @@ impl Workspace { pub fn new(url: Url) -> Self { Self { url, - settings: None, + options: None, is_default: false, } } @@ -97,15 +96,15 @@ impl Workspace { pub fn default(url: Url) -> Self { Self { url, - settings: None, + options: None, is_default: true, } } - /// Set the client settings for this workspace. + /// Set the client options for this workspace. #[must_use] - pub fn with_settings(mut self, settings: ClientSettings) -> Self { - self.settings = Some(settings); + pub fn with_options(mut self, options: ClientOptions) -> Self { + self.options = Some(options); self } @@ -114,9 +113,9 @@ impl Workspace { &self.url } - /// Returns the client settings for this workspace. - pub(crate) fn settings(&self) -> Option<&ClientSettings> { - self.settings.as_ref() + /// Returns the client options for this workspace. + pub(crate) fn options(&self) -> Option<&ClientOptions> { + self.options.as_ref() } /// Returns true if this is the default workspace. diff --git a/crates/ruff_server/tests/notebook.rs b/crates/ruff_server/tests/notebook.rs index d9e3f690b12f7..504d299048163 100644 --- a/crates/ruff_server/tests/notebook.rs +++ b/crates/ruff_server/tests/notebook.rs @@ -8,7 +8,7 @@ use lsp_types::{ Position, Range, TextDocumentContentChangeEvent, VersionedTextDocumentIdentifier, }; use ruff_notebook::SourceValue; -use ruff_server::{ClientSettings, Workspace, Workspaces}; +use ruff_server::{Client, ClientOptions, GlobalOptions, Workspace, Workspaces}; const SUPER_RESOLUTION_OVERVIEW_PATH: &str = "./resources/test/fixtures/tensorflow_test_notebook.ipynb"; @@ -28,14 +28,23 @@ fn super_resolution_overview() { insta::assert_snapshot!("initial_notebook", notebook_source(¬ebook)); + let (main_loop_sender, main_loop_receiver) = crossbeam::channel::unbounded(); + let (client_sender, client_receiver) = crossbeam::channel::unbounded(); + + let client = Client::new(main_loop_sender, client_sender); + + let options = GlobalOptions::default(); + let global = options.into_settings(client.clone()); + let mut session = ruff_server::Session::new( &ClientCapabilities::default(), ruff_server::PositionEncoding::UTF16, - ClientSettings::default(), - &Workspaces::new(vec![Workspace::new( - lsp_types::Url::from_file_path(file_path.parent().unwrap()).unwrap(), - ) - .with_settings(ClientSettings::default())]), + global, + &Workspaces::new(vec![ + Workspace::new(lsp_types::Url::from_file_path(file_path.parent().unwrap()).unwrap()) + .with_options(ClientOptions::default()), + ]), + &client, ) .unwrap(); @@ -304,6 +313,9 @@ fn super_resolution_overview() { "changed_notebook", notebook_source(snapshot.query().as_notebook().unwrap()) ); + + assert!(client_receiver.is_empty()); + assert!(main_loop_receiver.is_empty()); } fn notebook_source(notebook: &ruff_server::NotebookDocument) -> String { diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index 0f82f37169ee8..ffc41ca462b74 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -15,12 +15,14 @@ license = { workspace = true } [dependencies] ruff_text_size = { workspace = true } +get-size2 = { workspace = true, optional = true } memchr = { workspace = true } serde = { workspace = true, optional = true } [dev-dependencies] [features] +get-size = ["dep:get-size2"] serde = ["dep:serde", "ruff_text_size/serde"] [lints] diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs index 5bf43e3a1d493..c7c652bc64872 100644 --- a/crates/ruff_source_file/src/lib.rs +++ b/crates/ruff_source_file/src/lib.rs @@ -7,18 +7,18 @@ use serde::{Deserialize, Serialize}; use ruff_text_size::{Ranged, TextRange, TextSize}; -pub use crate::line_index::{LineIndex, OneIndexed}; +pub use crate::line_index::{LineIndex, OneIndexed, PositionEncoding}; pub use crate::line_ranges::LineRanges; pub use crate::newlines::{ - find_newline, Line, LineEnding, NewlineWithTrailingNewline, UniversalNewlineIterator, - UniversalNewlines, + Line, LineEnding, NewlineWithTrailingNewline, UniversalNewlineIterator, UniversalNewlines, + find_newline, }; mod line_index; mod line_ranges; mod newlines; -/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`SourceLocation`]. +/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`LineColumn`]. #[derive(Debug)] pub struct SourceCode<'src, 'index> { text: &'src str, @@ -33,10 +33,20 @@ impl<'src, 'index> SourceCode<'src, 'index> { } } - /// Computes the one indexed row and column numbers for `offset`. + /// Computes the one indexed line and column numbers for `offset`, skipping any potential BOM. #[inline] - pub fn source_location(&self, offset: TextSize) -> SourceLocation { - self.index.source_location(offset, self.text) + pub fn line_column(&self, offset: TextSize) -> LineColumn { + self.index.line_column(offset, self.text) + } + + #[inline] + pub fn source_location( + &self, + offset: TextSize, + position_encoding: PositionEncoding, + ) -> SourceLocation { + self.index + .source_location(offset, self.text, position_encoding) } #[inline] @@ -153,6 +163,7 @@ impl SourceFileBuilder { /// /// Cloning a [`SourceFile`] is cheap, because it only requires bumping a reference count. #[derive(Clone, Eq, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct SourceFile { inner: Arc, } @@ -185,7 +196,7 @@ impl SourceFile { } } - fn index(&self) -> &LineIndex { + pub fn index(&self) -> &LineIndex { self.inner .line_index .get_or_init(|| LineIndex::from_source_text(self.source_text())) @@ -215,6 +226,7 @@ impl Ord for SourceFile { } } +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] struct SourceFileInner { name: Box, code: Box, @@ -229,34 +241,62 @@ impl PartialEq for SourceFileInner { impl Eq for SourceFileInner {} -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +/// The line and column of an offset in a source file. +/// +/// See [`LineIndex::line_column`] for more information. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SourceLocation { - pub row: OneIndexed, +pub struct LineColumn { + /// The line in the source text. + pub line: OneIndexed, + /// The column (UTF scalar values) relative to the start of the line except any + /// potential BOM on the first line. pub column: OneIndexed, } -impl Default for SourceLocation { +impl Default for LineColumn { fn default() -> Self { Self { - row: OneIndexed::MIN, + line: OneIndexed::MIN, column: OneIndexed::MIN, } } } -impl Debug for SourceLocation { +impl Debug for LineColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SourceLocation") - .field("row", &self.row.get()) + f.debug_struct("LineColumn") + .field("line", &self.line.get()) .field("column", &self.column.get()) .finish() } } -impl std::fmt::Display for SourceLocation { +impl std::fmt::Display for LineColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{row}:{column}", row = self.row, column = self.column) + write!(f, "{line}:{column}", line = self.line, column = self.column) + } +} + +/// A position into a source file represented by the line number and the offset to that character relative to the start of that line. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SourceLocation { + /// The line in the source text. + pub line: OneIndexed, + /// The offset from the start of the line to the character. + /// + /// This can be a byte offset, the number of UTF16 code points, or the UTF8 code units, depending on the + /// [`PositionEncoding`] used. + pub character_offset: OneIndexed, +} + +impl Default for SourceLocation { + fn default() -> Self { + Self { + line: OneIndexed::MIN, + character_offset: OneIndexed::MIN, + } } } diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index e9d211ca5c1ff..8da47e0dba26f 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -5,21 +5,22 @@ use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; +use crate::{LineColumn, SourceLocation}; use ruff_text_size::{TextLen, TextRange, TextSize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::SourceLocation; - -/// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. +/// Index for fast [byte offset](TextSize) to [`LineColumn`] conversions. /// /// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count. #[derive(Clone, Eq, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct LineIndex { inner: Arc, } #[derive(Eq, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] struct LineIndexInner { line_starts: Vec, kind: IndexKind, @@ -44,7 +45,7 @@ impl LineIndex { b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue, b'\n' | b'\r' => { // SAFETY: Assertion above guarantees `i <= u32::MAX` - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] line_starts.push(TextSize::from(i as u32) + TextSize::from(1)); } _ => {} @@ -66,60 +67,146 @@ impl LineIndex { self.inner.kind } - /// Returns the row and column index for an offset. + /// Returns the line and column number for an UTF-8 byte offset. + /// + /// The `column` number is the nth-character of the line, except for the first line + /// where it doesn't include the UTF-8 BOM marker at the start of the file. + /// + /// ### BOM handling + /// + /// For files starting with a UTF-8 BOM marker, the byte offsets + /// in the range `0...3` are all mapped to line 0 and column 0. + /// Because of this, the conversion isn't losless. /// /// ## Examples /// /// ``` /// # use ruff_text_size::TextSize; - /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; - /// let source = "def a():\n pass"; - /// let index = LineIndex::from_source_text(source); + /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn}; + /// let source = format!("\u{FEFF}{}", "def a():\n pass"); + /// let index = LineIndex::from_source_text(&source); /// + /// // Before BOM, maps to after BOM /// assert_eq!( - /// index.source_location(TextSize::from(0), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// index.line_column(TextSize::from(0), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } /// ); /// + /// // After BOM, maps to after BOM /// assert_eq!( - /// index.source_location(TextSize::from(4), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } + /// index.line_column(TextSize::from(3), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// assert_eq!( + /// index.line_column(TextSize::from(7), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } /// ); /// assert_eq!( - /// index.source_location(TextSize::from(13), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } + /// index.line_column(TextSize::from(16), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } /// ); /// ``` /// /// ## Panics /// - /// If the offset is out of bounds. - pub fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { - match self.line_starts().binary_search(&offset) { - // Offset is at the start of a line - Ok(row) => SourceLocation { - row: OneIndexed::from_zero_indexed(row), - column: OneIndexed::from_zero_indexed(0), - }, - Err(next_row) => { - // SAFETY: Safe because the index always contains an entry for the offset 0 - let row = next_row - 1; - let mut line_start = self.line_starts()[row]; - - let column = if self.kind().is_ascii() { - usize::from(offset) - usize::from(line_start) - } else { - // Don't count the BOM character as a column. - if line_start == TextSize::from(0) && content.starts_with('\u{feff}') { - line_start = '\u{feff}'.text_len(); - } + /// If the byte offset isn't within the bounds of `content`. + pub fn line_column(&self, offset: TextSize, content: &str) -> LineColumn { + let location = self.source_location(offset, content, PositionEncoding::Utf32); + + // Don't count the BOM character as a column, but only on the first line. + let column = if location.line.to_zero_indexed() == 0 && content.starts_with('\u{feff}') { + location.character_offset.saturating_sub(1) + } else { + location.character_offset + }; - content[TextRange::new(line_start, offset)].chars().count() - }; + LineColumn { + line: location.line, + column, + } + } + + /// Given a UTF-8 byte offset, returns the line and character offset according to the given encoding. + /// + /// ### BOM handling + /// + /// Unlike [`Self::line_column`], this method does not skip the BOM character at the start of the file. + /// This allows for bidirectional mapping between [`SourceLocation`] and [`TextSize`] (see [`Self::offset`]). + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn, SourceLocation, PositionEncoding, Line}; + /// let source = format!("\u{FEFF}{}", "def a():\n pass"); + /// let index = LineIndex::from_source_text(&source); + /// + /// // Before BOM, maps to character 0 + /// assert_eq!( + /// index.source_location(TextSize::from(0), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// // After BOM, maps to after BOM + /// assert_eq!( + /// index.source_location(TextSize::from(3), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(1) } + /// ); + /// + /// assert_eq!( + /// index.source_location(TextSize::from(7), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(5) } + /// ); + /// assert_eq!( + /// index.source_location(TextSize::from(16), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(1), character_offset: OneIndexed::from_zero_indexed(4) } + /// ); + /// ``` + /// + /// ## Panics + /// + /// If the UTF-8 byte offset is out of bounds of `text`. + pub fn source_location( + &self, + offset: TextSize, + text: &str, + encoding: PositionEncoding, + ) -> SourceLocation { + let line = self.line_index(offset); + let line_start = self.line_start(line, text); + + if self.is_ascii() { + return SourceLocation { + line, + character_offset: OneIndexed::from_zero_indexed((offset - line_start).to_usize()), + }; + } + match encoding { + PositionEncoding::Utf8 => { + let character_offset = offset - line_start; SourceLocation { - row: OneIndexed::from_zero_indexed(row), - column: OneIndexed::from_zero_indexed(column), + line, + character_offset: OneIndexed::from_zero_indexed(character_offset.to_usize()), + } + } + PositionEncoding::Utf16 => { + let up_to_character = &text[TextRange::new(line_start, offset)]; + let character = up_to_character.encode_utf16().count(); + + SourceLocation { + line, + character_offset: OneIndexed::from_zero_indexed(character), + } + } + PositionEncoding::Utf32 => { + let up_to_character = &text[TextRange::new(line_start, offset)]; + let character = up_to_character.chars().count(); + + SourceLocation { + line, + character_offset: OneIndexed::from_zero_indexed(character), } } } @@ -141,7 +228,7 @@ impl LineIndex { /// /// ``` /// # use ruff_text_size::TextSize; - /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; + /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn}; /// let source = "def a():\n pass"; /// let index = LineIndex::from_source_text(source); /// @@ -221,83 +308,211 @@ impl LineIndex { } } - /// Returns the [byte offset](TextSize) at `line` and `column`. + /// Returns the [UTF-8 byte offset](TextSize) at `line` and `character` where character is counted using the given encoding. /// /// ## Examples /// - /// ### ASCII + /// ### ASCII only source text /// /// ``` - /// use ruff_source_file::{LineIndex, OneIndexed}; - /// use ruff_text_size::TextSize; + /// # use ruff_source_file::{SourceLocation, LineIndex, OneIndexed, PositionEncoding}; + /// # use ruff_text_size::TextSize; /// let source = r#"a = 4 /// c = "some string" /// x = b"#; /// /// let index = LineIndex::from_source_text(source); /// - /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// // First line, first character + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(0) + /// ); /// - /// // Second line, 4th column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4), source), TextSize::new(10)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(4) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(10) + /// ); /// /// // Offset past the end of the first line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10), source), TextSize::new(6)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(10) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(6) + /// ); /// /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(29)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(3), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(29) + /// ); /// ``` /// - /// ### UTF8 + /// ### Non-ASCII source text /// /// ``` - /// use ruff_source_file::{LineIndex, OneIndexed}; + /// use ruff_source_file::{LineIndex, OneIndexed, SourceLocation, PositionEncoding}; /// use ruff_text_size::TextSize; - /// let source = r#"a = 4 + /// let source = format!("\u{FEFF}{}", r#"a = 4 /// c = "❤️" - /// x = b"#; + /// x = b"#); /// - /// let index = LineIndex::from_source_text(source); + /// let index = LineIndex::from_source_text(&source); + /// + /// // First line, first character, points at the BOM + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(0) + /// ); + /// + /// // First line, after the BOM + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(1) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(3) + /// ); /// - /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// // second line, 7th character, after emoji, UTF32 + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(7) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(20) + /// ); + /// + /// // Second line, 7th character, after emoji, UTF 16 + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(7) + /// }, + /// &source, + /// PositionEncoding::Utf16, + /// ), + /// TextSize::new(20) + /// ); /// - /// // Third line, 2nd column, after emoji - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1), source), TextSize::new(20)); /// /// // Offset past the end of the second line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10), source), TextSize::new(19)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(10) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(22) + /// ); /// /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(24)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(3), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(27) + /// ); /// ``` - /// - pub fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { + pub fn offset( + &self, + position: SourceLocation, + text: &str, + position_encoding: PositionEncoding, + ) -> TextSize { // If start-of-line position after last line - if line.to_zero_indexed() > self.line_starts().len() { - return contents.text_len(); + if position.line.to_zero_indexed() > self.line_starts().len() { + return text.text_len(); } - let line_range = self.line_range(line, contents); + let line_range = self.line_range(position.line, text); - match self.kind() { - IndexKind::Ascii => { - line_range.start() - + TextSize::try_from(column.to_zero_indexed()) - .unwrap_or(line_range.len()) - .clamp(TextSize::new(0), line_range.len()) - } - IndexKind::Utf8 => { - let rest = &contents[line_range]; - let column_offset: TextSize = rest + let character_offset = position.character_offset.to_zero_indexed(); + let character_byte_offset = if self.is_ascii() { + TextSize::try_from(character_offset).unwrap() + } else { + let line = &text[line_range]; + + match position_encoding { + PositionEncoding::Utf8 => { + TextSize::try_from(position.character_offset.to_zero_indexed()).unwrap() + } + PositionEncoding::Utf16 => { + let mut byte_offset = TextSize::new(0); + let mut utf16_code_unit_offset = 0; + + for c in line.chars() { + if utf16_code_unit_offset >= character_offset { + break; + } + + // Count characters encoded as two 16 bit words as 2 characters. + byte_offset += c.text_len(); + utf16_code_unit_offset += c.len_utf16(); + } + + byte_offset + } + PositionEncoding::Utf32 => line .chars() - .take(column.to_zero_indexed()) + .take(position.character_offset.to_zero_indexed()) .map(ruff_text_size::TextLen::text_len) - .sum(); - line_range.start() + column_offset + .sum(), } - } + }; + + line_range.start() + character_byte_offset.clamp(TextSize::new(0), line_range.len()) } /// Returns the [byte offsets](TextSize) for every line @@ -321,6 +536,7 @@ impl Debug for LineIndex { } #[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] enum IndexKind { /// Optimized index for an ASCII only document Ascii, @@ -430,12 +646,25 @@ impl FromStr for OneIndexed { } } +#[derive(Copy, Clone, Debug)] +pub enum PositionEncoding { + /// Character offsets count the number of bytes from the start of the line. + Utf8, + + /// Character offsets count the number of UTF-16 code units from the start of the line. + Utf16, + + /// Character offsets count the number of UTF-32 code points units (the same as number of characters in Rust) + /// from the start of the line. + Utf32, +} + #[cfg(test)] mod tests { use ruff_text_size::TextSize; use crate::line_index::LineIndex; - use crate::{OneIndexed, SourceLocation}; + use crate::{LineColumn, OneIndexed}; #[test] fn ascii_index() { @@ -466,30 +695,30 @@ mod tests { let index = LineIndex::from_source_text(contents); // First row. - let loc = index.source_location(TextSize::from(2), contents); + let loc = index.line_column(TextSize::from(2), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(2) } ); // Second row. - let loc = index.source_location(TextSize::from(6), contents); + let loc = index.line_column(TextSize::from(6), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); - let loc = index.source_location(TextSize::from(11), contents); + let loc = index.line_column(TextSize::from(11), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(5) } ); @@ -502,23 +731,23 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); assert_eq!( - index.source_location(TextSize::from(4), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(4), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } ); assert_eq!( - index.source_location(TextSize::from(6), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(6), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(7), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(7), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -531,23 +760,23 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]); assert_eq!( - index.source_location(TextSize::from(4), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(4), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } ); assert_eq!( - index.source_location(TextSize::from(7), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(7), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(8), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(8), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -598,23 +827,23 @@ mod tests { // Second ' assert_eq!( - index.source_location(TextSize::from(9), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(9), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(6) } ); assert_eq!( - index.source_location(TextSize::from(11), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(11), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(12), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(12), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -632,23 +861,23 @@ mod tests { // Second ' assert_eq!( - index.source_location(TextSize::from(9), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(9), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(6) } ); assert_eq!( - index.source_location(TextSize::from(12), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(12), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(13), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(13), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -664,49 +893,49 @@ mod tests { ); // First row. - let loc = index.source_location(TextSize::from(0), contents); + let loc = index.line_column(TextSize::from(0), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } ); - let loc = index.source_location(TextSize::from(5), contents); + let loc = index.line_column(TextSize::from(5), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(5) } ); - let loc = index.source_location(TextSize::from(8), contents); + let loc = index.line_column(TextSize::from(8), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(6) } ); // Second row. - let loc = index.source_location(TextSize::from(10), contents); + let loc = index.line_column(TextSize::from(10), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); // One-past-the-end. - let loc = index.source_location(TextSize::from(15), contents); + let loc = index.line_column(TextSize::from(15), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(5) } ); diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index deb6d8469031a..1078750b35806 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -322,7 +322,7 @@ impl LineEnding { } } - #[allow(clippy::len_without_is_empty)] + #[expect(clippy::len_without_is_empty)] pub const fn len(&self) -> usize { match self { LineEnding::Lf | LineEnding::Cr => 1, diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index d51954503b68e..d658fe4db3c13 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -2,11 +2,17 @@ name = "ruff_text_size" version = "0.0.0" publish = false -edition = "2021" -rust-version = "1.67.1" +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] serde = { workspace = true, optional = true } +get-size2 = { workspace = true, optional = true } schemars = { workspace = true, optional = true } [dev-dependencies] @@ -15,6 +21,7 @@ static_assertions = { workspace = true } [features] serde = ["dep:serde"] +get-size = ["dep:get-size2"] [lints] workspace = true diff --git a/crates/ruff_text_size/src/range.rs b/crates/ruff_text_size/src/range.rs index f2767c3d75b8c..895cfea53074f 100644 --- a/crates/ruff_text_size/src/range.rs +++ b/crates/ruff_text_size/src/range.rs @@ -12,6 +12,7 @@ use { /// /// It is a logic error for `start` to be greater than `end`. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct TextRange { // Invariant: start <= end start: TextSize, diff --git a/crates/ruff_text_size/src/schemars_impls.rs b/crates/ruff_text_size/src/schemars_impls.rs index a1c7fa3618b98..4bc9ba2e01dc2 100644 --- a/crates/ruff_text_size/src/schemars_impls.rs +++ b/crates/ruff_text_size/src/schemars_impls.rs @@ -6,17 +6,17 @@ //! bindings to the Workspace API use crate::{TextRange, TextSize}; -use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::Schema}; impl JsonSchema for TextSize { fn schema_name() -> String { String::from("TextSize") } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { + fn json_schema(r#gen: &mut SchemaGenerator) -> Schema { // TextSize is represented as a raw u32, see serde_impls.rs for the // actual implementation - ::json_schema(gen) + ::json_schema(r#gen) } } @@ -25,9 +25,9 @@ impl JsonSchema for TextRange { String::from("TextRange") } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { + fn json_schema(r#gen: &mut SchemaGenerator) -> Schema { // TextSize is represented as (TextSize, TextSize), see serde_impls.rs // for the actual implementation - <(TextSize, TextSize)>::json_schema(gen) + <(TextSize, TextSize)>::json_schema(r#gen) } } diff --git a/crates/ruff_text_size/src/serde_impls.rs b/crates/ruff_text_size/src/serde_impls.rs index b6885d674ac2c..7558e9769e417 100644 --- a/crates/ruff_text_size/src/serde_impls.rs +++ b/crates/ruff_text_size/src/serde_impls.rs @@ -1,6 +1,6 @@ use { crate::{TextRange, TextSize}, - serde::{de, Deserialize, Deserializer, Serialize, Serializer}, + serde::{Deserialize, Deserializer, Serialize, Serializer, de}, }; impl Serialize for TextSize { @@ -31,7 +31,7 @@ impl Serialize for TextRange { } impl<'de> Deserialize<'de> for TextRange { - #[allow(clippy::nonminimal_bool)] + #[expect(clippy::nonminimal_bool)] fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/crates/ruff_text_size/src/size.rs b/crates/ruff_text_size/src/size.rs index 1b597698d3f26..dda5ded61db12 100644 --- a/crates/ruff_text_size/src/size.rs +++ b/crates/ruff_text_size/src/size.rs @@ -21,6 +21,7 @@ use { /// These escape hatches are primarily required for unit testing and when /// converting from UTF-8 size to another coordinate space, such as UTF-16. #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct TextSize { pub(crate) raw: u32, } diff --git a/crates/ruff_text_size/src/traits.rs b/crates/ruff_text_size/src/traits.rs index 0ea015135a3fe..69b0f647b5dde 100644 --- a/crates/ruff_text_size/src/traits.rs +++ b/crates/ruff_text_size/src/traits.rs @@ -31,7 +31,7 @@ impl TextLen for &'_ String { impl Sealed for char {} impl TextLen for char { #[inline] - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn text_len(self) -> TextSize { (self.len_utf8() as u32).into() } diff --git a/crates/ruff_text_size/tests/serde.rs b/crates/ruff_text_size/tests/serde.rs index 0d8f9d4a6af12..606b4168335e9 100644 --- a/crates/ruff_text_size/tests/serde.rs +++ b/crates/ruff_text_size/tests/serde.rs @@ -1,6 +1,6 @@ use { ruff_text_size::{TextRange, TextSize}, - serde_test::{assert_de_tokens_error, assert_tokens, Token}, + serde_test::{Token, assert_de_tokens_error, assert_tokens}, std::ops, }; diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 0bfc980a90300..e03e3b8d65a90 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.11.6" +version = "0.12.3" publish = false authors = { workspace = true } edition = { workspace = true } @@ -41,6 +41,8 @@ getrandom = { workspace = true, features = ["wasm_js"] } serde = { workspace = true } serde-wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true } +# Not a direct dependency but required to compile for Wasm. +uuid = { workspace = true, features = ["js"] } [dev-dependencies] wasm-bindgen-test = { workspace = true } diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 06b0b6973c54a..9066d17df88b8 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -1,31 +1,29 @@ use std::path::Path; use js_sys::Error; -use ruff_linter::message::{DiagnosticMessage, Message, SyntaxErrorMessage}; use ruff_linter::settings::types::PythonVersion; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use ruff_formatter::printer::SourceMapGeneration; use ruff_formatter::{FormatResult, Formatted, IndentStyle}; +use ruff_linter::Locator; use ruff_linter::directives; use ruff_linter::line_width::{IndentWidth, LineLength}; use ruff_linter::linter::check_path; -use ruff_linter::registry::AsRule; -use ruff_linter::settings::{flags, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX}; +use ruff_linter::settings::{DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, flags}; use ruff_linter::source_kind::SourceKind; -use ruff_linter::Locator; use ruff_python_ast::{Mod, PySourceType}; use ruff_python_codegen::Stylist; -use ruff_python_formatter::{format_module_ast, pretty_comments, PyFormatContext, QuoteStyle}; +use ruff_python_formatter::{PyFormatContext, QuoteStyle, format_module_ast, pretty_comments}; use ruff_python_index::Indexer; -use ruff_python_parser::{parse, parse_unchecked, Mode, ParseOptions, Parsed}; +use ruff_python_parser::{Mode, ParseOptions, Parsed, parse, parse_unchecked}; use ruff_python_trivia::CommentRanges; -use ruff_source_file::SourceLocation; +use ruff_source_file::{LineColumn, OneIndexed}; use ruff_text_size::Ranged; +use ruff_workspace::Settings; use ruff_workspace::configuration::Configuration; use ruff_workspace::options::{FormatOptions, LintCommonOptions, LintOptions, Options}; -use ruff_workspace::Settings; #[wasm_bindgen(typescript_custom_section)] const TYPES: &'static str = r#" @@ -61,8 +59,8 @@ export interface Diagnostic { pub struct ExpandedMessage { pub code: Option, pub message: String, - pub start_location: SourceLocation, - pub end_location: SourceLocation, + pub start_location: Location, + pub end_location: Location, pub fix: Option, } @@ -74,8 +72,8 @@ pub struct ExpandedFix { #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] struct ExpandedEdit { - location: SourceLocation, - end_location: SourceLocation, + location: Location, + end_location: Location, content: Option, } @@ -166,7 +164,8 @@ impl Workspace { let target_version = self.settings.linter.unresolved_target_version; // Parse once. - let options = ParseOptions::from(source_type).with_target_version(target_version); + let options = + ParseOptions::from(source_type).with_target_version(target_version.parser_version()); let parsed = parse_unchecked(source_kind.source_code(), options) .try_into_module() .expect("`PySourceType` always parses to a `ModModule`."); @@ -189,7 +188,7 @@ impl Workspace { ); // Generate checks. - let messages = check_path( + let diagnostics = check_path( Path::new(""), None, &locator, @@ -206,38 +205,25 @@ impl Workspace { let source_code = locator.to_source_code(); - let messages: Vec = messages + let messages: Vec = diagnostics .into_iter() - .map(|message| match message { - Message::Diagnostic(DiagnosticMessage { - kind, range, fix, .. - }) => ExpandedMessage { - code: Some(kind.rule().noqa_code().to_string()), - message: kind.body, - start_location: source_code.source_location(range.start()), - end_location: source_code.source_location(range.end()), - fix: fix.map(|fix| ExpandedFix { - message: kind.suggestion, - edits: fix - .edits() - .iter() - .map(|edit| ExpandedEdit { - location: source_code.source_location(edit.start()), - end_location: source_code.source_location(edit.end()), - content: edit.content().map(ToString::to_string), - }) - .collect(), - }), - }, - Message::SyntaxError(SyntaxErrorMessage { message, range, .. }) => { - ExpandedMessage { - code: None, - message, - start_location: source_code.source_location(range.start()), - end_location: source_code.source_location(range.end()), - fix: None, - } - } + .map(|msg| ExpandedMessage { + code: msg.secondary_code().map(ToString::to_string), + message: msg.body().to_string(), + start_location: source_code.line_column(msg.expect_range().start()).into(), + end_location: source_code.line_column(msg.expect_range().end()).into(), + fix: msg.fix().map(|fix| ExpandedFix { + message: msg.suggestion().map(ToString::to_string), + edits: fix + .edits() + .iter() + .map(|edit| ExpandedEdit { + location: source_code.line_column(edit.start()).into(), + end_location: source_code.line_column(edit.end()).into(), + content: edit.content().map(ToString::to_string), + }) + .collect(), + }), }) .collect(); @@ -316,3 +302,18 @@ impl<'a> ParsedModule<'a> { ) } } + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct Location { + pub row: OneIndexed, + pub column: OneIndexed, +} + +impl From for Location { + fn from(value: LineColumn) -> Self { + Self { + row: value.line, + column: value.column, + } + } +} diff --git a/crates/ruff_wasm/tests/api.rs b/crates/ruff_wasm/tests/api.rs index 49864125ff93a..036a435de0ec4 100644 --- a/crates/ruff_wasm/tests/api.rs +++ b/crates/ruff_wasm/tests/api.rs @@ -3,8 +3,8 @@ use wasm_bindgen_test::wasm_bindgen_test; use ruff_linter::registry::Rule; -use ruff_source_file::{OneIndexed, SourceLocation}; -use ruff_wasm::{ExpandedMessage, Workspace}; +use ruff_source_file::OneIndexed; +use ruff_wasm::{ExpandedMessage, Location, Workspace}; macro_rules! check { ($source:expr, $config:expr, $expected:expr) => {{ @@ -27,11 +27,11 @@ fn empty_config() { [ExpandedMessage { code: Some(Rule::IfTuple.noqa_code().to_string()), message: "If test is a tuple, which is always `True`".to_string(), - start_location: SourceLocation { + start_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(3) }, - end_location: SourceLocation { + end_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(9) }, @@ -48,11 +48,11 @@ fn syntax_error() { [ExpandedMessage { code: None, message: "SyntaxError: Expected an expression".to_string(), - start_location: SourceLocation { + start_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(3) }, - end_location: SourceLocation { + end_location: Location { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) }, @@ -65,15 +65,15 @@ fn syntax_error() { fn unsupported_syntax_error() { check!( "match 2:\n case 1: ...", - r#"{"preview": true}"#, + r#"{"target-version": "py39"}"#, [ExpandedMessage { code: None, message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(), - start_location: SourceLocation { + start_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) }, - end_location: SourceLocation { + end_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(5) }, diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 5a185dc755c84..f1c8f09b8393c 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -18,6 +18,7 @@ ruff_formatter = { workspace = true } ruff_graph = { workspace = true, features = ["serde", "schemars"] } ruff_linter = { workspace = true } ruff_macros = { workspace = true } +ruff_options_metadata = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_formatter = { workspace = true, features = ["serde"] } ruff_python_semantic = { workspace = true, features = ["serde"] } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 260f4edc68986..a844d6403a687 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -5,12 +5,12 @@ use std::borrow::Cow; use std::collections::BTreeMap; use std::env::VarError; -use std::num::{NonZeroU16, NonZeroU8}; +use std::num::{NonZeroU8, NonZeroU16}; use std::path::{Path, PathBuf}; use std::str::FromStr; -use anyhow::{anyhow, Context, Result}; -use glob::{glob, GlobError, Paths, PatternError}; +use anyhow::{Context, Result, anyhow}; +use glob::{GlobError, Paths, PatternError, glob}; use itertools::Itertools; use regex::Regex; use rustc_hash::{FxHashMap, FxHashSet}; @@ -22,8 +22,7 @@ use ruff_cache::cache_dir; use ruff_formatter::IndentStyle; use ruff_graph::{AnalyzeSettings, Direction}; use ruff_linter::line_width::{IndentWidth, LineLength}; -use ruff_linter::registry::RuleNamespace; -use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES}; +use ruff_linter::registry::{INCOMPATIBLE_CODES, Rule, RuleNamespace, RuleSet}; use ruff_linter::rule_selector::{PreviewOptions, Specificity}; use ruff_linter::rules::{flake8_import_conventions, isort, pycodestyle}; use ruff_linter::settings::fix_safety_table::FixSafetyTable; @@ -33,10 +32,12 @@ use ruff_linter::settings::types::{ FilePatternSet, GlobPath, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode, RequiredVersion, UnsafeFixes, }; -use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS}; +use ruff_linter::settings::{ + DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, TASK_TAGS, TargetVersion, +}; use ruff_linter::{ - fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message, RuleSelector, - RUFF_PKG_VERSION, + RUFF_PKG_VERSION, RuleSelector, fs, warn_user_once, warn_user_once_by_id, + warn_user_once_by_message, }; use ruff_python_ast as ast; use ruff_python_formatter::{ @@ -54,7 +55,7 @@ use crate::options::{ PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions, }; use crate::settings::{ - FileResolverSettings, FormatterSettings, LineEnding, Settings, EXCLUDE, INCLUDE, + EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, LineEnding, Settings, }; #[derive(Clone, Debug, Default)] @@ -164,6 +165,7 @@ impl Configuration { } } + let linter_target_version = TargetVersion(self.target_version); let target_version = self.target_version.unwrap_or_default(); let global_preview = self.preview.unwrap_or_default(); @@ -273,13 +275,12 @@ impl Configuration { project_root: project_root.to_path_buf(), }, - #[allow(deprecated)] linter: LinterSettings { rules, exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?, extension: self.extension.unwrap_or_default(), preview: lint_preview, - unresolved_target_version: target_version, + unresolved_target_version: linter_target_version, per_file_target_version, project_root: project_root.to_path_buf(), allowed_confusables: lint @@ -430,6 +431,7 @@ impl Configuration { .ruff .map(RuffOptions::into_settings) .unwrap_or_default(), + typing_extensions: lint.typing_extensions.unwrap_or(true), }, formatter, @@ -633,6 +635,7 @@ pub struct LintConfiguration { pub logger_objects: Option>, pub task_tags: Option>, pub typing_modules: Option>, + pub typing_extensions: Option, // Plugins pub flake8_annotations: Option, @@ -666,7 +669,7 @@ pub struct LintConfiguration { impl LintConfiguration { fn from_options(options: LintOptions, project_root: &Path) -> Result { - #[allow(deprecated)] + #[expect(deprecated)] let ignore = options .common .ignore @@ -674,7 +677,7 @@ impl LintConfiguration { .flatten() .chain(options.common.extend_ignore.into_iter().flatten()) .collect(); - #[allow(deprecated)] + #[expect(deprecated)] let unfixable = options .common .unfixable @@ -683,10 +686,12 @@ impl LintConfiguration { .chain(options.common.extend_unfixable.into_iter().flatten()) .collect(); - #[allow(deprecated)] + #[expect(deprecated)] let ignore_init_module_imports = { if options.common.ignore_init_module_imports.is_some() { - warn_user_once!("The `ignore-init-module-imports` option is deprecated and will be removed in a future release. Ruff's handling of imports in `__init__.py` files has been improved (in preview) and unused imports will always be flagged."); + warn_user_once!( + "The `ignore-init-module-imports` option is deprecated and will be removed in a future release. Ruff's handling of imports in `__init__.py` files has been improved (in preview) and unused imports will always be flagged." + ); } options.common.ignore_init_module_imports }; @@ -746,6 +751,7 @@ impl LintConfiguration { task_tags: options.common.task_tags, logger_objects: options.common.logger_objects, typing_modules: options.common.typing_modules, + typing_extensions: options.typing_extensions, // Plugins flake8_annotations: options.common.flake8_annotations, @@ -1041,7 +1047,9 @@ impl LintConfiguration { [] => (), [selection] => { let (prefix, code) = selection.prefix_and_code(); - return Err(anyhow!("Selection of deprecated rule `{prefix}{code}` is not allowed when preview is enabled.")); + return Err(anyhow!( + "Selection of deprecated rule `{prefix}{code}` is not allowed when preview is enabled." + )); } [..] => { let mut message = "Selection of deprecated rules is not allowed when preview is enabled. Remove selection of:".to_string(); @@ -1089,7 +1097,7 @@ impl LintConfiguration { // approach to give each pair it's own `warn_user_once`. for (preferred, expendable, message) in INCOMPATIBLE_CODES { if rules.enabled(*preferred) && rules.enabled(*expendable) { - warn_user_once_by_id!(expendable.as_ref(), "{}", message); + warn_user_once_by_id!(expendable.name().as_str(), "{}", message); rules.disable(*expendable); } } @@ -1170,6 +1178,7 @@ impl LintConfiguration { pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), ruff: self.ruff.combine(config.ruff), + typing_extensions: self.typing_extensions.or(config.typing_extensions), } } } @@ -1189,7 +1198,6 @@ pub struct FormatConfiguration { } impl FormatConfiguration { - #[allow(clippy::needless_pass_by_value)] pub fn from_options(options: FormatOptions, project_root: &Path) -> Result { Ok(Self { // `--extension` is a hidden command-line argument that isn't supported in configuration @@ -1227,7 +1235,6 @@ impl FormatConfiguration { } #[must_use] - #[allow(clippy::needless_pass_by_value)] pub fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), @@ -1256,7 +1263,6 @@ pub struct AnalyzeConfiguration { } impl AnalyzeConfiguration { - #[allow(clippy::needless_pass_by_value)] pub fn from_options(options: AnalyzeOptions, project_root: &Path) -> Result { Ok(Self { exclude: options.exclude.map(|paths| { @@ -1283,7 +1289,6 @@ impl AnalyzeConfiguration { } #[must_use] - #[allow(clippy::needless_pass_by_value)] pub fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), @@ -1335,7 +1340,7 @@ fn warn_about_deprecated_top_level_lint_options( top_level_options: &LintCommonOptions, path: Option<&Path>, ) { - #[allow(deprecated)] + #[expect(deprecated)] let LintCommonOptions { allowed_confusables, dummy_variable_rgx, @@ -1630,11 +1635,11 @@ mod tests { use anyhow::Result; + use ruff_linter::RuleSelector; use ruff_linter::codes::{Flake8Copyright, Pycodestyle, Refurb}; use ruff_linter::registry::{Linter, Rule, RuleSet}; use ruff_linter::rule_selector::PreviewOptions; use ruff_linter::settings::types::PreviewMode; - use ruff_linter::RuleSelector; use crate::configuration::{LintConfiguration, RuleSelection}; use crate::options::PydocstyleOptions; @@ -1655,7 +1660,6 @@ mod tests { Rule::BlankLinesBeforeNestedDefinition, ]; - #[allow(clippy::needless_pass_by_value)] fn resolve_rules( selections: impl IntoIterator, preview: Option, diff --git a/crates/ruff_workspace/src/lib.rs b/crates/ruff_workspace/src/lib.rs index 5307429613704..bc8505c34627f 100644 --- a/crates/ruff_workspace/src/lib.rs +++ b/crates/ruff_workspace/src/lib.rs @@ -3,7 +3,6 @@ pub mod options; pub mod pyproject; pub mod resolver; -pub mod options_base; mod settings; pub use settings::{FileResolverSettings, FormatterSettings, Settings}; diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index fa83cd684d9b3..f9dc3b5c9f531 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -6,7 +6,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; use strum::IntoEnumIterator; -use crate::options_base::{OptionsMetadata, Visit}; use crate::settings::LineEnding; use ruff_formatter::IndentStyle; use ruff_graph::Direction; @@ -30,8 +29,9 @@ use ruff_linter::rules::{ use ruff_linter::settings::types::{ IdentifierPattern, OutputFormat, PythonVersion, RequiredVersion, }; -use ruff_linter::{warn_user_once, RuleSelector}; +use ruff_linter::{RuleSelector, warn_user_once}; use ruff_macros::{CombineOptions, OptionsMetadata}; +use ruff_options_metadata::{OptionsMetadata, Visit}; use ruff_python_ast::name::Name; use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle}; use ruff_python_semantic::NameImports; @@ -353,7 +353,7 @@ pub struct Options { scope = "per-file-target-version", example = r#" # Override the project-wide Python version for a developer scripts directory: - "scripts/**.py" = "py312" + "scripts/*.py" = "py312" "# )] pub per_file_target_version: Option>, @@ -513,6 +513,22 @@ pub struct LintOptions { "# )] pub preview: Option, + + /// Whether to allow imports from the third-party `typing_extensions` module for Python versions + /// before a symbol was added to the first-party `typing` module. + /// + /// Many rules try to import symbols from the `typing` module but fall back to + /// `typing_extensions` for earlier versions of Python. This option can be used to disable this + /// fallback behavior in cases where `typing_extensions` is not installed. + #[option( + default = "true", + value_type = "bool", + example = r#" + # Disable `typing_extensions` imports + typing-extensions = false + "# + )] + pub typing_extensions: Option, } /// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`]. @@ -540,10 +556,10 @@ impl schemars::JsonSchema for DeprecatedTopLevelLintOptions { "DeprecatedTopLevelLintOptions" )) } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { use schemars::schema::Schema; - let common_schema = LintCommonOptions::json_schema(gen); + let common_schema = LintCommonOptions::json_schema(generator); let mut schema_obj = common_schema.into_object(); if let Some(object) = schema_obj.object.as_mut() { @@ -1134,14 +1150,14 @@ impl Flake8BanditOptions { extend_markup_names: self .extend_markup_names .or_else(|| { - #[allow(deprecated)] + #[expect(deprecated)] ruff_options.and_then(|options| options.extend_markup_names.clone()) }) .unwrap_or_default(), allowed_markup_calls: self .allowed_markup_calls .or_else(|| { - #[allow(deprecated)] + #[expect(deprecated)] ruff_options.and_then(|options| options.allowed_markup_calls.clone()) }) .unwrap_or_default(), @@ -1292,7 +1308,7 @@ pub struct Flake8BuiltinsOptions { impl Flake8BuiltinsOptions { pub fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings { - #[allow(deprecated)] + #[expect(deprecated)] ruff_linter::rules::flake8_builtins::settings::Settings { ignorelist: self .ignorelist @@ -1980,7 +1996,8 @@ pub struct Flake8TidyImportsOptions { /// List of specific modules that may not be imported at module level, and should instead be /// imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` - /// block, or some other nested context). + /// block, or some other nested context). This also affects the rule `import-outside-top-level` + /// if `banned-module-level-imports` is enabled. #[option( default = r#"[]"#, value_type = r#"list[str]"#, @@ -2654,7 +2671,9 @@ impl IsortOptions { let force_sort_within_sections = self.force_sort_within_sections.unwrap_or_default(); let lines_between_types = self.lines_between_types.unwrap_or_default(); if force_sort_within_sections && lines_between_types != 0 { - warn_user_once!("`lines-between-types` is ignored when `force-sort-within-sections` is set to `true`"); + warn_user_once!( + "`lines-between-types` is ignored when `force-sort-within-sections` is set to `true`" + ); } // Extract any configuration options that deal with user-defined sections. @@ -3102,7 +3121,7 @@ pub struct PydocstyleOptions { default = r#"false"#, value_type = "bool", example = r#" - ignore_var_parameters = true + ignore-var-parameters = true "# )] pub ignore_var_parameters: Option, @@ -3568,7 +3587,7 @@ pub struct FormatOptions { /// Setting `skip-magic-trailing-comma = true` changes the formatting to: /// /// ```python - /// # The arguments remain on separate lines because of the trailing comma after `b` + /// # The arguments are collapsed to a single line because the trailing comma is ignored /// def test(a, b): /// pass /// ``` @@ -3876,6 +3895,7 @@ pub struct LintOptionsWire { pydoclint: Option, ruff: Option, preview: Option, + typing_extensions: Option, } impl From for LintOptions { @@ -3930,10 +3950,11 @@ impl From for LintOptions { pydoclint, ruff, preview, + typing_extensions, } = value; LintOptions { - #[allow(deprecated)] + #[expect(deprecated)] common: LintCommonOptions { allowed_confusables, dummy_variable_rgx, @@ -3985,6 +4006,7 @@ impl From for LintOptions { pydoclint, ruff, preview, + typing_extensions, } } } diff --git a/crates/ruff_workspace/src/options_base.rs b/crates/ruff_workspace/src/options_base.rs deleted file mode 100644 index e3e4c650a27d7..0000000000000 --- a/crates/ruff_workspace/src/options_base.rs +++ /dev/null @@ -1,434 +0,0 @@ -use serde::{Serialize, Serializer}; -use std::collections::BTreeMap; - -use std::fmt::{Debug, Display, Formatter}; - -/// Visits [`OptionsMetadata`]. -/// -/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata. -pub trait Visit { - /// Visits an [`OptionField`] value named `name`. - fn record_field(&mut self, name: &str, field: OptionField); - - /// Visits an [`OptionSet`] value named `name`. - fn record_set(&mut self, name: &str, group: OptionSet); -} - -/// Returns metadata for its options. -pub trait OptionsMetadata { - /// Visits the options metadata of this object by calling `visit` for each option. - fn record(visit: &mut dyn Visit); - - fn documentation() -> Option<&'static str> { - None - } - - /// Returns the extracted metadata. - fn metadata() -> OptionSet - where - Self: Sized + 'static, - { - OptionSet::of::() - } -} - -impl OptionsMetadata for Option -where - T: OptionsMetadata, -{ - fn record(visit: &mut dyn Visit) { - T::record(visit); - } -} - -/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`]. -#[derive(Clone, PartialEq, Eq, Debug, Serialize)] -#[serde(untagged)] -pub enum OptionEntry { - /// A single option. - Field(OptionField), - - /// A set of options. - Set(OptionSet), -} - -impl Display for OptionEntry { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - OptionEntry::Set(set) => std::fmt::Display::fmt(set, f), - OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f), - } - } -} - -/// A set of options. -/// -/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing -/// [`OptionsMetadata`]. -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct OptionSet { - record: fn(&mut dyn Visit), - doc: fn() -> Option<&'static str>, -} - -impl OptionSet { - pub fn of() -> Self - where - T: OptionsMetadata + 'static, - { - Self { - record: T::record, - doc: T::documentation, - } - } - - /// Visits the options in this set by calling `visit` for each option. - pub fn record(&self, visit: &mut dyn Visit) { - let record = self.record; - record(visit); - } - - pub fn documentation(&self) -> Option<&'static str> { - let documentation = self.doc; - documentation() - } - - /// Returns `true` if this set has an option that resolves to `name`. - /// - /// The name can be separated by `.` to find a nested option. - /// - /// ## Examples - /// - /// ### Test for the existence of a child option - /// - /// ```rust - /// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit}; - /// - /// struct WithOptions; - /// - /// impl OptionsMetadata for WithOptions { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// }); - /// } - /// } - /// - /// assert!(WithOptions::metadata().has("ignore-git-ignore")); - /// assert!(!WithOptions::metadata().has("does-not-exist")); - /// ``` - /// ### Test for the existence of a nested option - /// - /// ```rust - /// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit}; - /// - /// struct Root; - /// - /// impl OptionsMetadata for Root { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None - /// }); - /// - /// visit.record_set("format", Nested::metadata()); - /// } - /// } - /// - /// struct Nested; - /// - /// impl OptionsMetadata for Nested { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("hard-tabs", OptionField { - /// doc: "Use hard tabs for indentation and spaces for alignment.", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None - /// }); - /// } - /// } - /// - /// assert!(Root::metadata().has("format.hard-tabs")); - /// assert!(!Root::metadata().has("format.spaces")); - /// assert!(!Root::metadata().has("lint.hard-tabs")); - /// ``` - pub fn has(&self, name: &str) -> bool { - self.find(name).is_some() - } - - /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise. - /// - /// The name can be separated by `.` to find a nested option. - /// - /// ## Examples - /// - /// ### Find a child option - /// - /// ```rust - /// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; - /// - /// struct WithOptions; - /// - /// static IGNORE_GIT_IGNORE: OptionField = OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None - /// }; - /// - /// impl OptionsMetadata for WithOptions { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone()); - /// } - /// } - /// - /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore"), Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))); - /// assert_eq!(WithOptions::metadata().find("does-not-exist"), None); - /// ``` - /// ### Find a nested option - /// - /// ```rust - /// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; - /// - /// static HARD_TABS: OptionField = OptionField { - /// doc: "Use hard tabs for indentation and spaces for alignment.", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None - /// }; - /// - /// struct Root; - /// - /// impl OptionsMetadata for Root { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None - /// }); - /// - /// visit.record_set("format", Nested::metadata()); - /// } - /// } - /// - /// struct Nested; - /// - /// impl OptionsMetadata for Nested { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("hard-tabs", HARD_TABS.clone()); - /// } - /// } - /// - /// assert_eq!(Root::metadata().find("format.hard-tabs"), Some(OptionEntry::Field(HARD_TABS.clone()))); - /// assert_eq!(Root::metadata().find("format"), Some(OptionEntry::Set(Nested::metadata()))); - /// assert_eq!(Root::metadata().find("format.spaces"), None); - /// assert_eq!(Root::metadata().find("lint.hard-tabs"), None); - /// ``` - pub fn find(&self, name: &str) -> Option { - struct FindOptionVisitor<'a> { - option: Option, - parts: std::str::Split<'a, char>, - needle: &'a str, - } - - impl Visit for FindOptionVisitor<'_> { - fn record_set(&mut self, name: &str, set: OptionSet) { - if self.option.is_none() && name == self.needle { - if let Some(next) = self.parts.next() { - self.needle = next; - set.record(self); - } else { - self.option = Some(OptionEntry::Set(set)); - } - } - } - - fn record_field(&mut self, name: &str, field: OptionField) { - if self.option.is_none() && name == self.needle { - if self.parts.next().is_none() { - self.option = Some(OptionEntry::Field(field)); - } - } - } - } - - let mut parts = name.split('.'); - - if let Some(first) = parts.next() { - let mut visitor = FindOptionVisitor { - parts, - needle: first, - option: None, - }; - - self.record(&mut visitor); - visitor.option - } else { - None - } - } - - pub fn collect_fields(&self) -> Vec<(String, OptionField)> { - struct FieldsCollector(Vec<(String, OptionField)>); - - impl Visit for FieldsCollector { - fn record_field(&mut self, name: &str, field: OptionField) { - self.0.push((name.to_string(), field)); - } - - fn record_set(&mut self, _name: &str, _group: OptionSet) {} - } - - let mut visitor = FieldsCollector(vec![]); - self.record(&mut visitor); - visitor.0 - } -} - -/// Visitor that writes out the names of all fields and sets. -struct DisplayVisitor<'fmt, 'buf> { - f: &'fmt mut Formatter<'buf>, - result: std::fmt::Result, -} - -impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> { - fn new(f: &'fmt mut Formatter<'buf>) -> Self { - Self { f, result: Ok(()) } - } - - fn finish(self) -> std::fmt::Result { - self.result - } -} - -impl Visit for DisplayVisitor<'_, '_> { - fn record_set(&mut self, name: &str, _: OptionSet) { - self.result = self.result.and_then(|()| writeln!(self.f, "{name}")); - } - - fn record_field(&mut self, name: &str, field: OptionField) { - self.result = self.result.and_then(|()| { - write!(self.f, "{name}")?; - - if field.deprecated.is_some() { - write!(self.f, " (deprecated)")?; - } - - writeln!(self.f) - }); - } -} - -impl Display for OptionSet { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut visitor = DisplayVisitor::new(f); - self.record(&mut visitor); - visitor.finish() - } -} - -struct SerializeVisitor<'a> { - entries: &'a mut BTreeMap, -} - -impl Visit for SerializeVisitor<'_> { - fn record_set(&mut self, name: &str, set: OptionSet) { - // Collect the entries of the set. - let mut entries = BTreeMap::new(); - let mut visitor = SerializeVisitor { - entries: &mut entries, - }; - set.record(&mut visitor); - - // Insert the set into the entries. - for (key, value) in entries { - self.entries.insert(format!("{name}.{key}"), value); - } - } - - fn record_field(&mut self, name: &str, field: OptionField) { - self.entries.insert(name.to_string(), field); - } -} - -impl Serialize for OptionSet { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut entries = BTreeMap::new(); - let mut visitor = SerializeVisitor { - entries: &mut entries, - }; - self.record(&mut visitor); - entries.serialize(serializer) - } -} - -impl Debug for OptionSet { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(self, f) - } -} - -#[derive(Debug, Eq, PartialEq, Clone, Serialize)] -pub struct OptionField { - pub doc: &'static str, - /// Ex) `"false"` - pub default: &'static str, - /// Ex) `"bool"` - pub value_type: &'static str, - /// Ex) `"per-file-ignores"` - pub scope: Option<&'static str>, - pub example: &'static str, - pub deprecated: Option, -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct Deprecated { - pub since: Option<&'static str>, - pub message: Option<&'static str>, -} - -impl Display for OptionField { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{}", self.doc)?; - writeln!(f)?; - writeln!(f, "Default value: {}", self.default)?; - writeln!(f, "Type: {}", self.value_type)?; - - if let Some(deprecated) = &self.deprecated { - write!(f, "Deprecated")?; - - if let Some(since) = deprecated.since { - write!(f, " (since {since})")?; - } - - if let Some(message) = deprecated.message { - write!(f, ": {message}")?; - } - - writeln!(f)?; - } - - writeln!(f, "Example usage:\n```toml\n{}\n```", self.example) - } -} diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index cbee2032f9fcb..f849487aa55ca 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -183,9 +183,13 @@ pub(super) fn load_options>( if let Some(dir) = path.parent() { let fallback = get_fallback_target_version(dir); if let Some(version) = fallback { - debug!("Derived `target-version` from `requires-python` in `pyproject.toml`: {version:?}"); + debug!( + "Derived `target-version` from `requires-python` in `pyproject.toml`: {version:?}" + ); } else { - debug!("No `pyproject.toml` with `requires-python` in same directory; `target-version` unspecified"); + debug!( + "No `pyproject.toml` with `requires-python` in same directory; `target-version` unspecified" + ); } ruff.target_version = fallback; } @@ -277,7 +281,7 @@ mod tests { use ruff_linter::settings::types::PatternPrefixPair; use crate::options::{Flake8BuiltinsOptions, LintCommonOptions, LintOptions, Options}; - use crate::pyproject::{find_settings_toml, parse_pyproject_toml, Pyproject, Tools}; + use crate::pyproject::{Pyproject, Tools, find_settings_toml, parse_pyproject_toml}; #[test] @@ -399,7 +403,7 @@ strict-checking = false "#, )?; - #[allow(deprecated)] + #[expect(deprecated)] let expected = Flake8BuiltinsOptions { builtins_allowed_modules: Some(vec!["asyncio".to_string()]), allowed_modules: Some(vec!["sys".to_string()]), @@ -436,33 +440,39 @@ strict-checking = false ); assert!(!settings.strict_checking); - assert!(toml::from_str::( - r" + assert!( + toml::from_str::( + r" [tool.black] [tool.ruff] line_length = 79 ", - ) - .is_err()); + ) + .is_err() + ); - assert!(toml::from_str::( - r#" + assert!( + toml::from_str::( + r#" [tool.black] [tool.ruff.lint] select = ["E123"] "#, - ) - .is_err()); + ) + .is_err() + ); - assert!(toml::from_str::( - r" + assert!( + toml::from_str::( + r" [tool.black] [tool.ruff] line-length = 79 other-attribute = 1 ", - ) - .is_err()); + ) + .is_err() + ); Ok(()) } diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 918bb8125058b..84a31ebbeea4f 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -7,8 +7,8 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::RwLock; -use anyhow::{anyhow, bail}; use anyhow::{Context, Result}; +use anyhow::{anyhow, bail}; use globset::{Candidate, GlobSet}; use ignore::{DirEntry, Error, ParallelVisitor, WalkBuilder, WalkState}; use itertools::Itertools; @@ -23,9 +23,9 @@ use ruff_linter::package::PackageRoot; use ruff_linter::packaging::is_package; use crate::configuration::Configuration; -use crate::pyproject::{settings_toml, TargetVersionStrategy}; +use crate::pyproject::{TargetVersionStrategy, settings_toml}; use crate::settings::Settings; -use crate::{pyproject, FileResolverSettings}; +use crate::{FileResolverSettings, pyproject}; /// The configuration information from a `pyproject.toml` file. #[derive(Debug)] @@ -667,7 +667,7 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> { impl Drop for PythonFilesVisitor<'_, '_> { fn drop(&mut self) { let mut merged = self.global.merged.lock().unwrap(); - let (ref mut files, ref mut error) = &mut *merged; + let (files, error) = &mut *merged; if files.is_empty() { *files = std::mem::take(&mut self.local_files); @@ -910,7 +910,7 @@ pub fn match_any_inclusion( #[cfg(test)] mod tests { - use std::fs::{create_dir, File}; + use std::fs::{File, create_dir}; use std::path::Path; use anyhow::Result; @@ -924,9 +924,9 @@ mod tests { use crate::configuration::Configuration; use crate::pyproject::find_settings_toml; use crate::resolver::{ - is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings, ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, - ResolvedFile, Resolver, + ResolvedFile, Resolver, is_file_excluded, match_exclusion, python_files_in_path, + resolve_root_settings, }; use crate::settings::Settings; use crate::tests::test_resource_path; diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 9ca5ef9168205..a20bee0c9d486 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -3,11 +3,11 @@ use ruff_cache::cache_dir; use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth}; use ruff_graph::AnalyzeSettings; use ruff_linter::display_settings; +use ruff_linter::settings::LinterSettings; use ruff_linter::settings::types::{ CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes, }; -use ruff_linter::settings::LinterSettings; use ruff_macros::CacheKey; use ruff_python_ast::{PySourceType, PythonVersion}; use ruff_python_formatter::{ @@ -19,7 +19,6 @@ use std::fmt; use std::path::{Path, PathBuf}; #[derive(Debug, CacheKey)] -#[allow(clippy::struct_excessive_bools)] pub struct Settings { #[cache_key(ignore)] pub cache_dir: PathBuf, diff --git a/crates/ty/CONTRIBUTING.md b/crates/ty/CONTRIBUTING.md new file mode 100644 index 0000000000000..388892cf62290 --- /dev/null +++ b/crates/ty/CONTRIBUTING.md @@ -0,0 +1,141 @@ +# Contributing to ty + +Welcome! We're happy to have you here. Thank you in advance for your contribution to ty. + +> [!NOTE] +> +> This guide is for ty. If you're looking to contribute to Ruff, please see +> [the Ruff contributing guide](../../CONTRIBUTING.md). + +## The Basics + +We welcome contributions in the form of pull requests. + +For small changes (e.g., bug fixes), feel free to submit a PR. + +For larger changes (e.g. new diagnostics, new functionality, new configuration options), consider +creating an [**issue**](https://github.com/astral-sh/ty/issues) outlining your proposed change. +You can also join us on [Discord](https://discord.com/invite/astral-sh) to discuss your idea with the +community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ty/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +in the issue tracker, along with [bugs](https://github.com/astral-sh/ty/issues?q=is%3Aissue+is%3Aopen+label%3Abug) +that are ready for contributions. + +### Prerequisites + +ty is written in Rust. You'll need to install the +[Rust toolchain](https://www.rust-lang.org/tools/install) for development. + +You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to +run Python utility commands. + +You can optionally install pre-commit hooks to automatically run the validation checks +when making a commit: + +```shell +uv tool install pre-commit +pre-commit install +``` + +We recommend [nextest](https://nexte.st/) to run ty's test suite (via `cargo nextest run`), +though it's not strictly necessary: + +```shell +cargo install cargo-nextest --locked +``` + +Throughout this guide, any usages of `cargo test` can be replaced with `cargo nextest run`, +if you choose to install `nextest`. + +### Development + +After cloning the repository, run ty locally from the repository root with: + +```shell +cargo run --bin ty -- check --project /path/to/project/ +``` + +Prior to opening a pull request, ensure that your code has been auto-formatted, +and that it passes both the lint and test validation checks: + +```shell +cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting +cargo test # Rust testing +uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc. +``` + +These checks will run on GitHub Actions when you open your pull request, but running them locally +will save you time and expedite the merge process. + +If you're using VS Code, you can also install the recommended [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension to get these checks while editing. + +Include the text `[ty]` at the beginning of your pull request title, to distinguish ty pull requests +from Ruff ones. + +Your pull request will be reviewed by a maintainer, which may involve a few rounds of iteration +prior to merging. + +### Debugging ty + +ty can optionally emit extensive tracing output, which can be very useful in understanding its +operation and debugging issues; see [`crates/ty/docs/tracing.md`](./docs/tracing.md) for details. + +### Project Structure + +The codebase is structured as a monorepo with a [flat crate structure](https://matklad.github.io/2021/08/22/large-rust-workspaces.html), +such that all crates are contained in a flat `crates` directory. + +The vast majority of ty's code lives in the `ty_python_semantic` crate (located at +`crates/ty_python_semantic`). As a contributor, that's the crate that'll probably be most relevant +to you. + +At the time of writing, the repository includes the following ty-specific crates (in addition to +crates shared with Ruff, such as `ruff_db`, `ruff_python_ast`, and `ruff_python_parser`): + +- `ty_python_semantic`: The core type checker, which includes the type inference engine and + semantic analysis. +- `ty_test`: The Markdown-based test framework for ty, "mdtest". +- `ty`: The command-line interface. +- `ty_ide`: IDE features (hover, go-to-definition, autocomplete) for the language server. +- `ty_project`: Discovery and representation of a Python project to be checked by ty. +- `ty_server`: The ty language server. +- `ty_vendored`: A vendored copy of [typeshed](https://github.com/python/typeshed), which holds type + annotations for the Python standard library. +- `ty_wasm`: library crate for exposing ty as a WebAssembly module. Powers the + [ty Playground](https://play.ty.dev/). + +## Writing tests + +Core type checking tests are written as Markdown code blocks. +They can be found in [`crates/ty_python_semantic/resources/mdtest`][resources-mdtest]. +See [`crates/ty_test/README.md`][mdtest-readme] for more information +on the test framework itself. + +Any ty pull request to improve ty's type inference or type checking logic should include mdtests +demonstrating the effect of the change. + +We write mdtests in a "literate" style, with prose explaining the motivation of each test, and any +context necessary to understand the feature being demonstrated. + +### Property tests + +ty uses property-based testing to test the core type relations. These tests are located in +[`crates/ty_python_semantic/src/types/property_tests.rs`](../ty_python_semantic/src/types/property_tests.rs). + +The property tests do not run in CI on every PR, just once daily. It is advisable to run them +locally after modifying core type relation methods (`is_subtype_of`, `is_equivalent_to`, etc.) to +ensure that the changes do not break any of the properties. + +## Ecosystem CI (mypy-primer) + +GitHub Actions will run your changes against a number of real-world projects from GitHub and +report on any linter or formatter differences. See [`crates/ty/docs/mypy_primer.md`](./docs/mypy_primer.md) +for instructions on running these checks locally. + +## Coding guidelines + +We use the [Salsa](https://github.com/salsa-rs/salsa) library for incremental computation. Many +methods take a Salsa database (usually `db: &'db dyn Db`) as an argument. This should always be the +first argument (or second after `self`). + +[mdtest-readme]: ../ty_test/README.md +[resources-mdtest]: ../ty_python_semantic/resources/mdtest diff --git a/crates/ty/Cargo.toml b/crates/ty/Cargo.toml new file mode 100644 index 0000000000000..14538eea2fdf7 --- /dev/null +++ b/crates/ty/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "ty" +version = "0.0.0" +# required for correct pypi metadata +homepage = "https://github.com/astral-sh/ty/" +documentation = "https://docs.astral.sh/ty/" +# Releases occur in this other repository! +repository = "https://github.com/astral-sh/ty/" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ruff_db = { workspace = true, features = ["os", "cache"] } +ruff_python_ast = { workspace = true } +ty_python_semantic = { workspace = true } +ty_project = { workspace = true, features = ["zstd"] } +ty_server = { workspace = true } +ty_static = { workspace = true } + +anyhow = { workspace = true } +argfile = { workspace = true } +clap = { workspace = true, features = ["wrap_help", "string", "env"] } +clap_complete_command = { workspace = true } +colored = { workspace = true } +crossbeam = { workspace = true } +ctrlc = { version = "3.4.4" } +indicatif = { workspace = true } +jiff = { workspace = true } +rayon = { workspace = true } +salsa = { workspace = true } +tracing = { workspace = true, features = ["release_max_level_debug"] } +tracing-subscriber = { workspace = true } +tracing-flame = { workspace = true } +wild = { workspace = true } + +[dev-dependencies] +ruff_db = { workspace = true, features = ["testing"] } +ruff_python_trivia = { workspace = true } + +dunce = { workspace = true } +insta = { workspace = true, features = ["filters"] } +insta-cmd = { workspace = true } +filetime = { workspace = true } +regex = { workspace = true } +tempfile = { workspace = true } +toml = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ty/README.md b/crates/ty/README.md new file mode 100644 index 0000000000000..fb98292a0f8c1 --- /dev/null +++ b/crates/ty/README.md @@ -0,0 +1,9 @@ +# ty + +ty is an extremely fast type checker. +Currently, it is a work-in-progress and not ready for production use. + +The Rust code for ty lives in this repository; see [CONTRIBUTING.md](CONTRIBUTING.md) for more +information on contributing to ty. + +See [the ty repo](https://github.com/astral-sh/ty) for ty documentation and releases. diff --git a/crates/ty/build.rs b/crates/ty/build.rs new file mode 100644 index 0000000000000..98df7eae015c5 --- /dev/null +++ b/crates/ty/build.rs @@ -0,0 +1,134 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; + +fn main() { + // The workspace root directory is not available without walking up the tree + // https://github.com/rust-lang/cargo/issues/3946 + let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("..") + .join(".."); + + version_info(&workspace_root); + commit_info(&workspace_root); + + let target = std::env::var("TARGET").unwrap(); + println!("cargo::rustc-env=RUST_HOST_TARGET={target}"); +} + +/// Retrieve the version from the `dist-workspace.toml` file and set `TY_VERSION`. +fn version_info(workspace_root: &Path) { + let dist_file = workspace_root.join("dist-workspace.toml"); + if !dist_file.exists() { + return; + } + + println!("cargo:rerun-if-changed={}", dist_file.display()); + + let dist_file = fs::read_to_string(dist_file); + if let Ok(dist_file) = dist_file { + let lines = dist_file.lines(); + for line in lines { + if line.starts_with("version =") { + let (_key, version) = line.split_once('=').unwrap(); + println!( + "cargo::rustc-env=TY_VERSION={}", + version.trim().trim_matches('"') + ); + break; + } + } + } +} + +/// Retrieve commit information from the Git repository. +fn commit_info(workspace_root: &Path) { + // If not in a git repository, do not attempt to retrieve commit information + let git_dir = workspace_root.join(".git"); + if !git_dir.exists() { + return; + } + + if let Some(git_head_path) = git_head(&git_dir) { + println!("cargo:rerun-if-changed={}", git_head_path.display()); + + let git_head_contents = fs::read_to_string(git_head_path); + if let Ok(git_head_contents) = git_head_contents { + // The contents are either a commit or a reference in the following formats + // - "" when the head is detached + // - "ref " when working on a branch + // If a commit, checking if the HEAD file has changed is sufficient + // If a ref, we need to add the head file for that ref to rebuild on commit + let mut git_ref_parts = git_head_contents.split_whitespace(); + git_ref_parts.next(); + if let Some(git_ref) = git_ref_parts.next() { + let git_ref_path = git_dir.join(git_ref); + println!("cargo:rerun-if-changed={}", git_ref_path.display()); + } + } + } + + let output = match Command::new("git") + .arg("log") + .arg("-1") + .arg("--date=short") + .arg("--abbrev=9") + .arg("--format=%H %h %cd %(describe:tags)") + .current_dir(workspace_root) + .output() + { + Ok(output) if output.status.success() => output, + _ => return, + }; + let stdout = String::from_utf8(output.stdout).unwrap(); + let mut parts = stdout.split_whitespace(); + let mut next = || parts.next().unwrap(); + let _commit_hash = next(); + println!("cargo::rustc-env=TY_COMMIT_SHORT_HASH={}", next()); + println!("cargo::rustc-env=TY_COMMIT_DATE={}", next()); + + // Describe can fail for some commits + // https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem + if let Some(describe) = parts.next() { + let mut describe_parts = describe.split('-'); + let last_tag = describe_parts.next().unwrap(); + + println!("cargo::rustc-env=TY_LAST_TAG={last_tag}"); + + // If this is the tagged commit, this component will be missing + println!( + "cargo::rustc-env=TY_LAST_TAG_DISTANCE={}", + describe_parts.next().unwrap_or("0") + ); + } +} + +fn git_head(git_dir: &Path) -> Option { + // The typical case is a standard git repository. + let git_head_path = git_dir.join("HEAD"); + if git_head_path.exists() { + return Some(git_head_path); + } + if !git_dir.is_file() { + return None; + } + // If `.git/HEAD` doesn't exist and `.git` is actually a file, + // then let's try to attempt to read it as a worktree. If it's + // a worktree, then its contents will look like this, e.g.: + // + // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 + // + // And the HEAD file we want to watch will be at: + // + // /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD + let contents = fs::read_to_string(git_dir).ok()?; + let (label, worktree_path) = contents.split_once(':')?; + if label != "gitdir" { + return None; + } + let worktree_path = worktree_path.trim(); + Some(PathBuf::from(worktree_path)) +} diff --git a/crates/ty/docs/.gitattributes b/crates/ty/docs/.gitattributes new file mode 100644 index 0000000000000..5d0bdac7feb36 --- /dev/null +++ b/crates/ty/docs/.gitattributes @@ -0,0 +1,3 @@ +rules.md -diff linguist-generated=true text=auto eol=lf +cli.md -diff linguist-generated=true text=auto eol=lf +configuration.md -diff linguist-generated=true text=auto eol=lf diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md new file mode 100644 index 0000000000000..efe141813ed31 --- /dev/null +++ b/crates/ty/docs/cli.md @@ -0,0 +1,153 @@ + + +# CLI Reference + +## ty + +An extremely fast Python type checker. + +

Usage

+ +``` +ty +``` + +

Commands

+ +
ty check

Check a project for type errors

+
ty server

Start the language server

+
ty version

Display ty's version

+
ty help

Print this message or the help of the given subcommand(s)

+
+ +## ty check + +Check a project for type errors + +

Usage

+ +``` +ty check [OPTIONS] [PATH]... +``` + +

Arguments

+ +
PATHS

List of files or directories to check [default: the project root]

+
+ +

Options

+ +
--color when

Control when colored output is used

+

Possible values:

+
    +
  • auto: Display colors if the output goes to an interactive terminal
  • +
  • always: Always display colors
  • +
  • never: Never display colors
  • +
--config, -c config-option

A TOML <KEY> = <VALUE> pair (such as you might find in a ty.toml configuration file) +overriding a specific configuration option.

+

Overrides of individual settings using this option always take precedence +over all configuration files.

+
--config-file path

The path to a ty.toml file to use for configuration.

+

While ty configuration can be included in a pyproject.toml file, it is not allowed in this context.

+

May also be set with the TY_CONFIG_FILE environment variable.

--error rule

Treat the given rule as having severity 'error'. Can be specified multiple times.

+
--error-on-warning

Use exit code 1 if there are any warning-level diagnostics

+
--exclude exclude

Glob patterns for files to exclude from type checking.

+

Uses gitignore-style syntax to exclude files and directories from type checking. Supports patterns like tests/, *.tmp, **/__pycache__/**.

+
--exit-zero

Always use exit code 0, even when there are error-level diagnostics

+
--extra-search-path path

Additional path to use as a module-resolution source (can be passed multiple times)

+
--help, -h

Print help (see a summary with '-h')

+
--ignore rule

Disables the rule. Can be specified multiple times.

+
--output-format output-format

The format to use for printing diagnostic messages

+

Possible values:

+
    +
  • full: Print diagnostics verbosely, with context and helpful hints [default]
  • +
  • concise: Print diagnostics concisely, one per line
  • +
--project project

Run the command within the given project directory.

+

All pyproject.toml files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (.venv) unless the venv-path option is set.

+

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+
--python path

Path to the Python environment.

+

ty uses the Python environment to resolve type information and third-party dependencies.

+

If not specified, ty will attempt to infer it from the VIRTUAL_ENV or CONDA_PREFIX environment variables, or discover a .venv directory in the project root or working directory.

+

If a path to a Python interpreter is provided, e.g., .venv/bin/python3, ty will attempt to find an environment two directories up from the interpreter's path, e.g., .venv. At this time, ty does not invoke the interpreter to determine the location of the environment. This means that ty will not resolve dynamic executables such as a shim.

+

ty will search in the resolved environment's site-packages directories for type information and third-party imports.

+
--python-platform, --platform platform

Target platform to assume when resolving types.

+

This is used to specialize the type of sys.platform and will affect the visibility of platform-specific functions and attributes. If the value is set to all, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.

+
--python-version, --target-version version

Python version to assume when resolving types.

+

The Python version affects allowed syntax, type definitions of the standard library, and type definitions of first- and third-party modules that are conditional on the Python version.

+

If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the project.requires-python setting in a pyproject.toml file and use the minimum version from the specified range 2. Check for an activated or configured Python environment and attempt to infer the Python version of that environment 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13)

+

Possible values:

+
    +
  • 3.7
  • +
  • 3.8
  • +
  • 3.9
  • +
  • 3.10
  • +
  • 3.11
  • +
  • 3.12
  • +
  • 3.13
  • +
--quiet

Use quiet output

+
--respect-ignore-files

Respect file exclusions via .gitignore and other standard ignore files. Use --no-respect-gitignore to disable

+
--typeshed, --custom-typeshed-dir path

Custom directory to use for stdlib typeshed stubs

+
--verbose, -v

Use verbose output (or -vv and -vvv for more verbose output)

+
--warn rule

Treat the given rule as having severity 'warn'. Can be specified multiple times.

+
--watch, -W

Watch files for changes and recheck files related to the changed files

+
+ +## ty server + +Start the language server + +

Usage

+ +``` +ty server +``` + +

Options

+ +
--help, -h

Print help

+
+ +## ty version + +Display ty's version + +

Usage

+ +``` +ty version +``` + +

Options

+ +
--help, -h

Print help

+
+ +## ty generate-shell-completion + +Generate shell completion + +

Usage

+ +``` +ty generate-shell-completion +``` + +

Arguments

+ +
SHELL
+ +

Options

+ +
--help, -h

Print help

+
+ +## ty help + +Print this message or the help of the given subcommand(s) + +

Usage

+ +``` +ty help [COMMAND] +``` + diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md new file mode 100644 index 0000000000000..f2a39837fa717 --- /dev/null +++ b/crates/ty/docs/configuration.md @@ -0,0 +1,488 @@ + + +# Configuration +## `rules` + +Configures the enabled rules and their severity. + +See [the rules documentation](https://ty.dev/rules) for a list of all available rules. + +Valid severities are: + +* `ignore`: Disable the rule. +* `warn`: Enable the rule and create a warning diagnostic. +* `error`: Enable the rule and create an error diagnostic. + ty will exit with a non-zero code if any error diagnostics are emitted. + +**Default value**: `{...}` + +**Type**: `dict[RuleName, "ignore" | "warn" | "error"]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.rules] +possibly-unresolved-reference = "warn" +division-by-zero = "ignore" +``` + +--- + +## `environment` + +### `extra-paths` + +List of user-provided paths that should take first priority in the module resolution. +Examples in other type checkers are mypy's `MYPYPATH` environment variable, +or pyright's `stubPath` configuration setting. + +**Default value**: `[]` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +extra-paths = ["~/shared/my-search-path"] +``` + +--- + +### `python` + +Path to the Python installation from which ty resolves type information and third-party dependencies. + +ty will search in the path's `site-packages` directories for type information and +third-party imports. + +This option is commonly used to specify the path to a virtual environment. + +**Default value**: `null` + +**Type**: `str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +python = "./.venv" +``` + +--- + +### `python-platform` + +Specifies the target platform that will be used to analyze the source code. +If specified, ty will understand conditions based on comparisons with `sys.platform`, such +as are commonly found in typeshed to reflect the differing contents of the standard library across platforms. +If `all` is specified, ty will assume that the source code can run on any platform. + +If no platform is specified, ty will use the current platform: +- `win32` for Windows +- `darwin` for macOS +- `android` for Android +- `ios` for iOS +- `linux` for everything else + +**Default value**: `` + +**Type**: `"win32" | "darwin" | "android" | "ios" | "linux" | "all" | str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +# Tailor type stubs and conditionalized type definitions to windows. +python-platform = "win32" +``` + +--- + +### `python-version` + +Specifies the version of Python that will be used to analyze the source code. +The version should be specified as a string in the format `M.m` where `M` is the major version +and `m` is the minor (e.g. `"3.0"` or `"3.6"`). +If a version is provided, ty will generate errors if the source code makes use of language features +that are not supported in that version. + +If a version is not specified, ty will try the following techniques in order of preference +to determine a value: +1. Check for the `project.requires-python` setting in a `pyproject.toml` file + and use the minimum version from the specified range +2. Check for an activated or configured Python environment + and attempt to infer the Python version of that environment +3. Fall back to the default value (see below) + +For some language features, ty can also understand conditionals based on comparisons +with `sys.version_info`. These are commonly found in typeshed, for example, +to reflect the differing contents of the standard library across Python versions. + +**Default value**: `"3.13"` + +**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | .` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +python-version = "3.12" +``` + +--- + +### `root` + +The root paths of the project, used for finding first-party modules. + +Accepts a list of directory paths searched in priority order (first has highest priority). + +If left unspecified, ty will try to detect common project layouts and initialize `root` accordingly: + +* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) +* if a `.//` directory exists, include `.` and `./` in the first party search path +* otherwise, default to `.` (flat layout) + +Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), +it will also be included in the first party search path. + +**Default value**: `null` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +# Multiple directories (priority order) +root = ["./src", "./lib", "./vendor"] +``` + +--- + +### `typeshed` + +Optional path to a "typeshed" directory on disk for us to use for standard-library types. +If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, +bundled as a zip file in the binary + +**Default value**: `null` + +**Type**: `str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.environment] +typeshed = "/path/to/custom/typeshed" +``` + +--- + +## `overrides` + +Configuration override that applies to specific files based on glob patterns. + +An override allows you to apply different rule configurations to specific +files or directories. Multiple overrides can match the same file, with +later overrides take precedence. + +### Precedence + +- Later overrides in the array take precedence over earlier ones +- Override rules take precedence over global rules for matching files + +### Examples + +```toml +# Relax rules for test files +[[tool.ty.overrides]] +include = ["tests/**", "**/test_*.py"] + +[tool.ty.overrides.rules] +possibly-unresolved-reference = "warn" + +# Ignore generated files but still check important ones +[[tool.ty.overrides]] +include = ["generated/**"] +exclude = ["generated/important.py"] + +[tool.ty.overrides.rules] +possibly-unresolved-reference = "ignore" +``` + + +### `exclude` + +A list of file and directory patterns to exclude from this override. + +Patterns follow a syntax similar to `.gitignore`. +Exclude patterns take precedence over include patterns within the same override. + +If not specified, defaults to `[]` (excludes no files). + +**Default value**: `null` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[[tool.ty.overrides]] +exclude = [ + "generated", + "*.proto", + "tests/fixtures/**", + "!tests/fixtures/important.py" # Include this one file +] +``` + +--- + +### `include` + +A list of file and directory patterns to include for this override. + +The `include` option follows a similar syntax to `.gitignore` but reversed: +Including a file or directory will make it so that it (and its contents) +are affected by this override. + +If not specified, defaults to `["**"]` (matches all files). + +**Default value**: `null` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[[tool.ty.overrides]] +include = [ + "src", + "tests", +] +``` + +--- + +### `rules` + +Rule overrides for files matching the include/exclude patterns. + +These rules will be merged with the global rules, with override rules +taking precedence for matching files. You can set rules to different +severity levels or disable them entirely. + +**Default value**: `{...}` + +**Type**: `dict[RuleName, "ignore" | "warn" | "error"]` + +**Example usage** (`pyproject.toml`): + +```toml +[[tool.ty.overrides]] +include = ["src"] + +[tool.ty.overrides.rules] +possibly-unresolved-reference = "ignore" +``` + +--- + +## `src` + +### `exclude` + +A list of file and directory patterns to exclude from type checking. + +Patterns follow a syntax similar to `.gitignore`: + +- `./src/` matches only a directory +- `./src` matches both files and directories +- `src` matches files or directories named `src` +- `*` matches any (possibly empty) sequence of characters (except `/`). +- `**` matches zero or more path components. + This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. + A sequence of more than two consecutive `*` characters is also invalid. +- `?` matches any single character except `/` +- `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, + so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. +- `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded) + +All paths are anchored relative to the project root (`src` only +matches `/src` and not `/test/src`). +To exclude any directory or file named `src`, use `**/src` instead. + +By default, ty excludes commonly ignored directories: + +- `**/.bzr/` +- `**/.direnv/` +- `**/.eggs/` +- `**/.git/` +- `**/.git-rewrite/` +- `**/.hg/` +- `**/.mypy_cache/` +- `**/.nox/` +- `**/.pants.d/` +- `**/.pytype/` +- `**/.ruff_cache/` +- `**/.svn/` +- `**/.tox/` +- `**/.venv/` +- `**/__pypackages__/` +- `**/_build/` +- `**/buck-out/` +- `**/dist/` +- `**/node_modules/` +- `**/venv/` + +You can override any default exclude by using a negated pattern. For example, +to re-include `dist` use `exclude = ["!dist"]` + +**Default value**: `null` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.src] +exclude = [ + "generated", + "*.proto", + "tests/fixtures/**", + "!tests/fixtures/important.py" # Include this one file +] +``` + +--- + +### `include` + +A list of files and directories to check. The `include` option +follows a similar syntax to `.gitignore` but reversed: +Including a file or directory will make it so that it (and its contents) +are type checked. + +- `./src/` matches only a directory +- `./src` matches both files and directories +- `src` matches a file or directory named `src` +- `*` matches any (possibly empty) sequence of characters (except `/`). +- `**` matches zero or more path components. + This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. + A sequence of more than two consecutive `*` characters is also invalid. +- `?` matches any single character except `/` +- `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, + so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. + +All paths are anchored relative to the project root (`src` only +matches `/src` and not `/test/src`). + +`exclude` takes precedence over `include`. + +**Default value**: `null` + +**Type**: `list[str]` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.src] +include = [ + "src", + "tests", +] +``` + +--- + +### `respect-ignore-files` + +Whether to automatically exclude files that are ignored by `.ignore`, +`.gitignore`, `.git/info/exclude`, and global `gitignore` files. +Enabled by default. + +**Default value**: `true` + +**Type**: `bool` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.src] +respect-ignore-files = false +``` + +--- + +### `root` + +> [!WARN] "Deprecated" +> This option has been deprecated. Use `environment.root` instead. + +The root of the project, used for finding first-party modules. + +If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly: + +* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) +* if a `.//` directory exists, include `.` and `./` in the first party search path +* otherwise, default to `.` (flat layout) + +Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), +it will also be included in the first party search path. + +**Default value**: `null` + +**Type**: `str` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.src] +root = "./app" +``` + +--- + +## `terminal` + +### `error-on-warning` + +Use exit code 1 if there are any warning-level diagnostics. + +Defaults to `false`. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.terminal] +# Error if ty emits any warning-level diagnostics. +error-on-warning = true +``` + +--- + +### `output-format` + +The format to use for printing diagnostic messages. + +Defaults to `full`. + +**Default value**: `full` + +**Type**: `full | concise` + +**Example usage** (`pyproject.toml`): + +```toml +[tool.ty.terminal] +output-format = "concise" +``` + +--- + diff --git a/crates/ty/docs/environment.md b/crates/ty/docs/environment.md new file mode 100644 index 0000000000000..30935a8a094ff --- /dev/null +++ b/crates/ty/docs/environment.md @@ -0,0 +1,55 @@ +# Environment variables + +ty defines and respects the following environment variables: + +### `TY_LOG` + +If set, ty will use this value as the log level for its `--verbose` output. +Accepts any filter compatible with the `tracing_subscriber` crate. + +For example: + +- `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line +- `TY_LOG=trace` will enable all trace-level logging. + +See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax) +for more. + +### `TY_LOG_PROFILE` + +If set to `"1"` or `"true"`, ty will enable flamegraph profiling. +This creates a `tracing.folded` file that can be used to generate flame graphs +for performance analysis. + +### `TY_MAX_PARALLELISM` + +Specifies an upper limit for the number of tasks ty is allowed to run in parallel. + +For example, how many files should be checked in parallel. +This isn't the same as a thread limit. ty may spawn additional threads +when necessary, e.g. to watch for file system changes or a dedicated UI thread. + +## Externally-defined variables + +ty also reads the following externally defined environment variables: + +### `CONDA_PREFIX` + +Used to detect an activated Conda environment location. +If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred. + +### `RAYON_NUM_THREADS` + +Specifies an upper limit for the number of threads ty uses when performing work in parallel. +Equivalent to `TY_MAX_PARALLELISM`. + +This is a standard Rayon environment variable. + +### `VIRTUAL_ENV` + +Used to detect an activated virtual environment. + +### `XDG_CONFIG_HOME` + +Path to user-level configuration directory on Unix systems. + diff --git a/crates/ty/docs/mypy_primer.md b/crates/ty/docs/mypy_primer.md new file mode 100644 index 0000000000000..26cc17c23874c --- /dev/null +++ b/crates/ty/docs/mypy_primer.md @@ -0,0 +1,69 @@ +# Running `mypy_primer` + +## Basics + +`mypy_primer` can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run: + +```sh +uvx --from "git+https://github.com/hauntsaninja/mypy_primer" mypy_primer -h +``` + +Alternatively, you can install the forked version of `mypy_primer` using: + +```sh +uv tool install "git+https://github.com/hauntsaninja/mypy_primer" +``` + +and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]). + +## Showing the diagnostics diff between two Git revisions + +To show the diagnostics diff between two Git revisions (e.g. your feature branch and `main`), run: + +```sh +mypy_primer \ + --type-checker ty \ + --old origin/main \ + --new my/feature \ + --debug \ + --output concise \ + --project-selector '/black$' +``` + +This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the +diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt))\$"`. + +You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `ty_paths` configuration +option to work correctly. + +## Avoiding recompilation + +If you want to run `mypy_primer` repeatedly, e.g. for different projects, but for the same combination of `--old` and `--new`, you +can use set the `MYPY_PRIMER_NO_REBUILD` environment variable to avoid recompilation of ty: + +```sh +MYPY_PRIMER_NO_REBUILD=1 mypy_primer … +``` + +## Running from a local copy of the repository + +If you are working on a local branch, you can use `mypy_primer`'s `--repo` option to specify the path to your local copy of the `ruff` repository. +This allows `mypy_primer` to check out local branches: + +```sh +mypy_primer --type-checker ty \ + --repo ./ruff \ + --old main \ + --new my/local-branch \ + --project-selector '/beartype$' \ + --debug \ + --output concise +``` + +Notes: + +- You might need to clean up `/tmp/mypy_primer` in order for this to work correctly. +- This must be run from _outside_ the `ruff` repo. + +[full list of ecosystem projects]: https://github.com/hauntsaninja/mypy_primer/blob/master/mypy_primer/projects.py +[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md new file mode 100644 index 0000000000000..39e60651a976b --- /dev/null +++ b/crates/ty/docs/rules.md @@ -0,0 +1,1928 @@ + + +# Rules + +## `byte-string-type-annotation` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20byte-string-type-annotation) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L36) + + +**What it does** + +Checks for byte-strings in type annotation positions. + +**Why is this bad?** + +Static analysis tools like ty can't analyse type annotations that use byte-string notation. + +**Examples** + +```python +def test(): -> b"int": + ... +``` + +Use instead: +```python +def test(): -> "int": + ... +``` + +## `call-non-callable` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L98) + + +**What it does** + +Checks for calls to non-callable objects. + +**Why is this bad?** + +Calling a non-callable object will raise a `TypeError` at runtime. + +**Examples** + +```python +4() # TypeError: 'int' object is not callable +``` + +## `conflicting-argument-forms` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L142) + + +**What it does** + +Checks whether an argument is used as both a value and a type form in a call. + +**Why is this bad?** + +Such calls have confusing semantics and often indicate a logic error. + +**Examples** + +```python +from typing import reveal_type +from ty_extensions import is_singleton + +if flag: + f = repr # Expects a value +else: + f = is_singleton # Expects a type form + +f(int) # error +``` + +## `conflicting-declarations` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L168) + + +**What it does** + +Checks whether a variable has been declared as two conflicting types. + +**Why is this bad** + +A variable with two conflicting declarations likely indicates a mistake. +Moreover, it could lead to incorrect or ill-defined type inference for +other code that relies on these variables. + +**Examples** + +```python +if b: + a: int +else: + a: str + +a = 1 +``` + +## `conflicting-metaclass` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L193) + + +**What it does** + +Checks for class definitions where the metaclass of the class +being created would not be a subclass of the metaclasses of +all the class's bases. + +**Why is it bad?** + +Such a class definition raises a `TypeError` at runtime. + +**Examples** + +```python +class M1(type): ... +class M2(type): ... +class A(metaclass=M1): ... +class B(metaclass=M2): ... + +# TypeError: metaclass conflict +class C(A, B): ... +``` + +## `cyclic-class-definition` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L219) + + +**What it does** + +Checks for class definitions in stub files that inherit +(directly or indirectly) from themselves. + +**Why is it bad?** + +Although forward references are natively supported in stub files, +inheritance cycles are still disallowed, as it is impossible to +resolve a consistent [method resolution order] for a class that +inherits from itself. + +**Examples** + +```python +# foo.pyi +class A(B): ... +class B(A): ... +``` + +[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + +## `duplicate-base` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L263) + + +**What it does** + +Checks for class definitions with duplicate bases. + +**Why is this bad?** + +Class definitions with duplicate bases raise `TypeError` at runtime. + +**Examples** + +```python +class A: ... + +# TypeError: duplicate base class +class B(A, A): ... +``` + +## `duplicate-kw-only` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L284) + + +**What it does** + +Checks for dataclass definitions with more than one field +annotated with `KW_ONLY`. + +**Why is this bad?** + +`dataclasses.KW_ONLY` is a special marker used to +emulate the `*` syntax in normal signatures. +It can only be used once per dataclass. + +Attempting to annotate two different fields with +it will lead to a runtime error. + +**Examples** + +```python +from dataclasses import dataclass, KW_ONLY + +@dataclass +class A: # Crash at runtime + b: int + _1: KW_ONLY + c: str + _2: KW_ONLY + d: bytes +``` + +## `escape-character-in-forward-annotation` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20escape-character-in-forward-annotation) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L120) + + +TODO #14889 + +## `fstring-type-annotation` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20fstring-type-annotation) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L11) + + +**What it does** + +Checks for f-strings in type annotation positions. + +**Why is this bad?** + +Static analysis tools like ty can't analyse type annotations that use f-string notation. + +**Examples** + +```python +def test(): -> f"int": + ... +``` + +Use instead: +```python +def test(): -> "int": + ... +``` + +## `implicit-concatenated-string-type-annotation` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20implicit-concatenated-string-type-annotation) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L86) + + +**What it does** + +Checks for implicit concatenated strings in type annotation positions. + +**Why is this bad?** + +Static analysis tools like ty can't analyse type annotations that use implicit concatenated strings. + +**Examples** + +```python +def test(): -> "Literal[" "5" "]": + ... +``` + +Use instead: +```python +def test(): -> "Literal[5]": + ... +``` + +## `inconsistent-mro` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L426) + + +**What it does** + +Checks for classes with an inconsistent [method resolution order] (MRO). + +**Why is this bad?** + +Classes with an inconsistent MRO will raise a `TypeError` at runtime. + +**Examples** + +```python +class A: ... +class B(A): ... + +# TypeError: Cannot create a consistent method resolution order +class C(A, B): ... +``` + +[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + +## `index-out-of-bounds` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450) + + +**What it does** + +Checks for attempts to use an out of bounds index to get an item from +a container. + +**Why is this bad?** + +Using an out of bounds index will raise an `IndexError` at runtime. + +**Examples** + +```python +t = (0, 1, 2) +t[3] # IndexError: tuple index out of range +``` + +## `instance-layout-conflict` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316) + + +**What it does** + +Checks for classes definitions which will fail at runtime due to +"instance memory layout conflicts". + +This error is usually caused by attempting to combine multiple classes +that define non-empty `__slots__` in a class's [Method Resolution Order] +(MRO), or by attempting to combine multiple builtin classes in a class's +MRO. + +**Why is this bad?** + +Inheriting from bases with conflicting instance memory layouts +will lead to a `TypeError` at runtime. + +An instance memory layout conflict occurs when CPython cannot determine +the memory layout instances of a class should have, because the instance +memory layout of one of its bases conflicts with the instance memory layout +of one or more of its other bases. + +For example, if a Python class defines non-empty `__slots__`, this will +impact the memory layout of instances of that class. Multiple inheritance +from more than one different class defining non-empty `__slots__` is not +allowed: + +```python +class A: + __slots__ = ("a", "b") + +class B: + __slots__ = ("a", "b") # Even if the values are the same + +# TypeError: multiple bases have instance lay-out conflict +class C(A, B): ... +``` + +An instance layout conflict can also be caused by attempting to use +multiple inheritance with two builtin classes, due to the way that these +classes are implemented in a CPython C extension: + +```python +class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict +``` + +Note that pure-Python classes with no `__slots__`, or pure-Python classes +with empty `__slots__`, are always compatible: + +```python +class A: ... +class B: + __slots__ = () +class C: + __slots__ = ("a", "b") + +# fine +class D(A, B, C): ... +``` + +**Known problems** + +Classes that have "dynamic" definitions of `__slots__` (definitions do not consist +of string literals, or tuples of string literals) are not currently considered solid +bases by ty. + +Additionally, this check is not exhaustive: many C extensions (including several in +the standard library) define classes that use extended memory layouts and thus cannot +coexist in a single MRO. Since it is currently not possible to represent this fact in +stub files, having a full knowledge of these classes is also impossible. When it comes +to classes that do not define `__slots__` at the Python level, therefore, ty, currently +only hard-codes a number of cases where it knows that a class will produce instances with +an atypical memory layout. + +**Further reading** + +- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) +- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order) + +[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + +## `invalid-argument-type` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L470) + + +**What it does** + +Detects call arguments whose type is not assignable to the corresponding typed parameter. + +**Why is this bad?** + +Passing an argument of a type the function (or callable object) does not accept violates +the expectations of the function author and may cause unexpected runtime errors within the +body of the function. + +**Examples** + +```python +def func(x: int): ... +func("foo") # error: [invalid-argument-type] +``` + +## `invalid-assignment` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L510) + + +**What it does** + +Checks for assignments where the type of the value +is not [assignable to] the type of the assignee. + +**Why is this bad?** + +Such assignments break the rules of the type system and +weaken a type checker's ability to accurately reason about your code. + +**Examples** + +```python +a: int = '' +``` + +[assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable + +## `invalid-attribute-access` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1514) + + +**What it does** + +Checks for assignments to class variables from instances +and assignments to instance variables from its class. + +**Why is this bad?** + +Incorrect assignments break the rules of the type system and +weaken a type checker's ability to accurately reason about your code. + +**Examples** + +```python +class C: + class_var: ClassVar[int] = 1 + instance_var: int + +C.class_var = 3 # okay +C().class_var = 3 # error: Cannot assign to class variable + +C().instance_var = 3 # okay +C.instance_var = 3 # error: Cannot assign to instance variable +``` + +## `invalid-base` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532) + + +**What it does** + +Checks for class definitions that have bases which are not instances of `type`. + +**Why is this bad?** + +Class definitions with bases like this will lead to `TypeError` being raised at runtime. + +**Examples** + +```python +class A(42): ... # error: [invalid-base] +``` + +## `invalid-context-manager` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L583) + + +**What it does** + +Checks for expressions used in `with` statements +that do not implement the context manager protocol. + +**Why is this bad?** + +Such a statement will raise `TypeError` at runtime. + +**Examples** + +```python +# TypeError: 'int' object does not support the context manager protocol +with 1: + print(2) +``` + +## `invalid-declaration` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L604) + + +**What it does** + +Checks for declarations where the inferred type of an existing symbol +is not [assignable to] its post-hoc declared type. + +**Why is this bad?** + +Such declarations break the rules of the type system and +weaken a type checker's ability to accurately reason about your code. + +**Examples** + +```python +a = 1 +a: str +``` + +[assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable + +## `invalid-exception-caught` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627) + + +**What it does** + +Checks for exception handlers that catch non-exception classes. + +**Why is this bad?** + +Catching classes that do not inherit from `BaseException` will raise a TypeError at runtime. + +**Example** + +```python +try: + 1 / 0 +except 1: + ... +``` + +Use instead: +```python +try: + 1 / 0 +except ZeroDivisionError: + ... +``` + +**References** + +- [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) + +**Ruff rule** + + This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes) + +## `invalid-generic-class` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L663) + + +**What it does** + +Checks for the creation of invalid generic classes + +**Why is this bad?** + +There are several requirements that you must follow when defining a generic class. + +**Examples** + +```python +from typing import Generic, TypeVar + +T = TypeVar("T") # okay + +# error: class uses both PEP-695 syntax and legacy syntax +class C[U](Generic[T]): ... +``` + +**References** + +- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + +## `invalid-legacy-type-variable` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L689) + + +**What it does** + +Checks for the creation of invalid legacy `TypeVar`s + +**Why is this bad?** + +There are several requirements that you must follow when creating a legacy `TypeVar`. + +**Examples** + +```python +from typing import TypeVar + +T = TypeVar("T") # okay +Q = TypeVar("S") # error: TypeVar name must match the variable it's assigned to +T = TypeVar("T") # error: TypeVars should not be redefined + +# error: TypeVar must be immediately assigned to a variable +def f(t: TypeVar("U")): ... +``` + +**References** + +- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + +## `invalid-metaclass` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L738) + + +**What it does** + +Checks for arguments to `metaclass=` that are invalid. + +**Why is this bad?** + +Python allows arbitrary expressions to be used as the argument to `metaclass=`. +These expressions, however, need to be callable and accept the same arguments +as `type.__new__`. + +**Example** + + +```python +def f(): ... + +# TypeError: f() takes 0 positional arguments but 3 were given +class B(metaclass=f): ... +``` + +**References** + +- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses) + +## `invalid-overload` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765) + + +**What it does** + +Checks for various invalid `@overload` usages. + +**Why is this bad?** + +The `@overload` decorator is used to define functions and methods that accepts different +combinations of arguments and return different types based on the arguments passed. This is +mainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type +checker may not be able to provide correct type information. + +**Example** + + +Defining only one overload: + +```py +from typing import overload + +@overload +def foo(x: int) -> int: ... +def foo(x: int | None) -> int | None: + return x +``` + +Or, not providing an implementation for the overloaded definition: + +```py +from typing import overload + +@overload +def foo() -> None: ... +@overload +def foo(x: int) -> int: ... +``` + +**References** + +- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload) + +## `invalid-parameter-default` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L808) + + +**What it does** + +Checks for default values that can't be +assigned to the parameter's annotated type. + +**Why is this bad?** + +This breaks the rules of the type system and +weakens a type checker's ability to accurately reason about your code. + +**Examples** + +```python +def f(a: int = ''): ... +``` + +## `invalid-protocol` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L398) + + +**What it does** + +Checks for invalidly defined protocol classes. + +**Why is this bad?** + +An invalidly defined protocol class may lead to the type checker inferring +unexpected things. It may also lead to `TypeError`s at runtime. + +**Examples** + +A `Protocol` class cannot inherit from a non-`Protocol` class; +this raises a `TypeError` at runtime: + +```pycon +>>> from typing import Protocol +>>> class Foo(int, Protocol): ... +... +Traceback (most recent call last): + File "", line 1, in + class Foo(int, Protocol): ... +TypeError: Protocols can only inherit from other protocols, got +``` + +## `invalid-raise` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L828) + + +Checks for `raise` statements that raise non-exceptions or use invalid +causes for their raised exceptions. + +**Why is this bad?** + +Only subclasses or instances of `BaseException` can be raised. +For an exception's cause, the same rules apply, except that `None` is also +permitted. Violating these rules results in a `TypeError` at runtime. + +**Examples** + +```python +def f(): + try: + something() + except NameError: + raise "oops!" from f + +def g(): + raise NotImplemented from 42 +``` + +Use instead: +```python +def f(): + try: + something() + except NameError as e: + raise RuntimeError("oops!") from e + +def g(): + raise NotImplementedError from None +``` + +**References** + +- [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise) +- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) + +## `invalid-return-type` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L491) + + +**What it does** + +Detects returned values that can't be assigned to the function's annotated return type. + +**Why is this bad?** + +Returning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function. + +**Examples** + +```python +def func() -> int: + return "a" # error: [invalid-return-type] +``` + +## `invalid-super-argument` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871) + + +**What it does** + +Detects `super()` calls where: +- the first argument is not a valid class literal, or +- the second argument is not an instance or subclass of the first argument. + +**Why is this bad?** + +`super(type, obj)` expects: +- the first argument to be a class, +- and the second argument to satisfy one of the following: + - `isinstance(obj, type)` is `True` + - `issubclass(obj, type)` is `True` + +Violating this relationship will raise a `TypeError` at runtime. + +**Examples** + +```python +class A: + ... +class B(A): + ... + +super(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)` + +super(A(), B()) # error: `A()` is not a class + +super(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)` +super(B, A) # error: `A` does not satisfy `issubclass(A, B)` +``` + +**References** + +- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) + +## `invalid-syntax-in-forward-annotation` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-syntax-in-forward-annotation) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L111) + + +TODO #14889 + +## `invalid-type-alias-type` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L717) + + +**What it does** + +Checks for the creation of invalid `TypeAliasType`s + +**Why is this bad?** + +There are several requirements that you must follow when creating a `TypeAliasType`. + +**Examples** + +```python +from typing import TypeAliasType + +IntOrStr = TypeAliasType("IntOrStr", int | str) # okay +NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal +``` + +## `invalid-type-checking-constant` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L910) + + +**What it does** + +Checks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an +annotation not assignable from `bool`. + +**Why is this bad?** + +The name `TYPE_CHECKING` is reserved for a flag that can be used to provide conditional +code seen only by the type checker, and not at runtime. Normally this flag is imported from +`typing` or `typing_extensions`, but it can also be defined locally. If defined locally, it +must be assigned the value `False` at runtime; the type checker will consider its value to +be `True`. If annotated, it must be annotated as a type that can accept `bool` values. + +**Examples** + +```python +TYPE_CHECKING: str +TYPE_CHECKING = '' +``` + +## `invalid-type-form` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L934) + + +**What it does** + +Checks for expressions that are used as [type expressions] +but cannot validly be interpreted as such. + +**Why is this bad?** + +Such expressions cannot be understood by ty. +In some cases, they might raise errors at runtime. + +**Examples** + +```python +from typing import Annotated + +a: type[1] # `1` is not a type +b: Annotated[int] # `Annotated` expects at least two arguments +``` +[type expressions]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions + +## `invalid-type-guard-call` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L986) + + +**What it does** + +Checks for type guard function calls without a valid target. + +**Why is this bad?** + +The first non-keyword non-variadic argument to a type guard function +is its target and must map to a symbol. + +Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like +expressions are invalid as narrowing targets. + +**Examples** + +```python +from typing import TypeIs + +def f(v: object) -> TypeIs[int]: ... + +f() # Error +f(*a) # Error +f(10) # Error +``` + +## `invalid-type-guard-definition` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L958) + + +**What it does** + +Checks for type guard functions without +a first non-self-like non-keyword-only non-variadic parameter. + +**Why is this bad?** + +Type narrowing functions must accept at least one positional argument +(non-static methods must accept another in addition to `self`/`cls`). + +Extra parameters/arguments are allowed but do not affect narrowing. + +**Examples** + +```python +from typing import TypeIs + +def f() -> TypeIs[int]: ... # Error, no parameter +def f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed +def f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments +class C: + def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` +``` + +## `invalid-type-variable-constraints` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014) + + +**What it does** + +Checks for constrained [type variables] with only one constraint. + +**Why is this bad?** + +A constrained type variable must have at least two constraints. + +**Examples** + +```python +from typing import TypeVar + +T = TypeVar('T', str) # invalid constrained TypeVar +``` + +Use instead: +```python +T = TypeVar('T', str, int) # valid constrained TypeVar +# or +T = TypeVar('T', bound=str) # valid bound TypeVar +``` + +[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar + +## `missing-argument` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1043) + + +**What it does** + +Checks for missing required arguments in a call. + +**Why is this bad?** + +Failing to provide a required argument will raise a `TypeError` at runtime. + +**Examples** + +```python +def func(x: int): ... +func() # TypeError: func() missing 1 required positional argument: 'x' +``` + +## `no-matching-overload` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062) + + +**What it does** + +Checks for calls to an overloaded function that do not match any of the overloads. + +**Why is this bad?** + +Failing to provide the correct arguments to one of the overloads will raise a `TypeError` +at runtime. + +**Examples** + +```python +@overload +def func(x: int): ... +@overload +def func(x: bool): ... +func("string") # error: [no-matching-overload] +``` + +## `non-subscriptable` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085) + + +**What it does** + +Checks for subscripting objects that do not support subscripting. + +**Why is this bad?** + +Subscripting an object that does not support it will raise a `TypeError` at runtime. + +**Examples** + +```python +4[1] # TypeError: 'int' object is not subscriptable +``` + +## `not-iterable` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1103) + + +**What it does** + +Checks for objects that are not iterable but are used in a context that requires them to be. + +**Why is this bad?** + +Iterating over an object that is not iterable will raise a `TypeError` at runtime. + +**Examples** + + +```python +for i in 34: # TypeError: 'int' object is not iterable + pass +``` + +## `parameter-already-assigned` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1154) + + +**What it does** + +Checks for calls which provide more than one argument for a single parameter. + +**Why is this bad?** + +Providing multiple values for a single parameter will raise a `TypeError` at runtime. + +**Examples** + + +```python +def f(x: int) -> int: ... + +f(1, x=2) # Error raised here +``` + +## `raw-string-type-annotation` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20raw-string-type-annotation) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L61) + + +**What it does** + +Checks for raw-strings in type annotation positions. + +**Why is this bad?** + +Static analysis tools like ty can't analyse type annotations that use raw-string notation. + +**Examples** + +```python +def test(): -> r"int": + ... +``` + +Use instead: +```python +def test(): -> "int": + ... +``` + +## `static-assert-error` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1490) + + +**What it does** + +Makes sure that the argument of `static_assert` is statically known to be true. + +**Why is this bad?** + +A `static_assert` call represents an explicit request from the user +for the type checker to emit an error if the argument cannot be verified +to evaluate to `True` in a boolean context. + +**Examples** + +```python +from ty_extensions import static_assert + +static_assert(1 + 1 == 3) # error: evaluates to `False` + +static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness +``` + +## `subclass-of-final-class` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245) + + +**What it does** + +Checks for classes that subclass final classes. + +**Why is this bad?** + +Decorating a class with `@final` declares to the type checker that it should not be subclassed. + +**Example** + + +```python +from typing import final + +@final +class A: ... +class B(A): ... # Error raised here +``` + +## `too-many-positional-arguments` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290) + + +**What it does** + +Checks for calls that pass more positional arguments than the callable can accept. + +**Why is this bad?** + +Passing too many positional arguments will raise `TypeError` at runtime. + +**Example** + + +```python +def f(): ... + +f("foo") # Error raised here +``` + +## `type-assertion-failure` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268) + + +**What it does** + +Checks for `assert_type()` and `assert_never()` calls where the actual type +is not the same as the asserted type. + +**Why is this bad?** + +`assert_type()` allows confirming the inferred type of a certain value. + +**Example** + + +```python +def _(x: int): + assert_type(x, int) # fine + assert_type(x, str) # error: Actual type does not match asserted type +``` + +## `unavailable-implicit-super-arguments` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1311) + + +**What it does** + +Detects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable. + +**Why is this bad?** + +When `super()` is used without arguments, Python tries to find two things: +the nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls). +If either of these is missing, the call will fail at runtime with a `RuntimeError`. + +**Examples** + +```python +super() # error: no enclosing class or function found + +def func(): + super() # error: no enclosing class or first argument exists + +class A: + f = super() # error: no enclosing function to provide the first argument + + def method(self): + def nested(): + super() # error: first argument does not exist in this nested function + + lambda: super() # error: first argument does not exist in this lambda + + (super() for _ in range(10)) # error: argument is not available in generator expression + + super() # okay! both enclosing class and first argument are available +``` + +**References** + +- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) + +## `unknown-argument` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1368) + + +**What it does** + +Checks for keyword arguments in calls that don't match any parameter of the callable. + +**Why is this bad?** + +Providing an unknown keyword argument will raise `TypeError` at runtime. + +**Example** + + +```python +def f(x: int) -> int: ... + +f(x=1, y=2) # Error raised here +``` + +## `unresolved-attribute` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1389) + + +**What it does** + +Checks for unresolved attributes. + +**Why is this bad?** + +Accessing an unbound attribute will raise an `AttributeError` at runtime. +An unresolved attribute is not guaranteed to exist from the type alone, +so this could also indicate that the object is not of the type that the user expects. + +**Examples** + +```python +class A: ... + +A().foo # AttributeError: 'A' object has no attribute 'foo' +``` + +## `unresolved-import` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411) + + +**What it does** + +Checks for import statements for which the module cannot be resolved. + +**Why is this bad?** + +Importing a module that cannot be resolved will raise a `ModuleNotFoundError` +at runtime. + +**Examples** + +```python +import foo # ModuleNotFoundError: No module named 'foo' +``` + +## `unresolved-reference` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430) + + +**What it does** + +Checks for references to names that are not defined. + +**Why is this bad?** + +Using an undefined variable will raise a `NameError` at runtime. + +**Example** + + +```python +print(x) # NameError: name 'x' is not defined +``` + +## `unsupported-bool-conversion` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1123) + + +**What it does** + +Checks for bool conversions where the object doesn't correctly implement `__bool__`. + +**Why is this bad?** + +If an exception is raised when you attempt to evaluate the truthiness of an object, +using the object in a boolean context will fail at runtime. + +**Examples** + + +```python +class NotBoolable: + __bool__ = None + +b1 = NotBoolable() +b2 = NotBoolable() + +if b1: # exception raised here + pass + +b1 and b2 # exception raised here +not b1 # exception raised here +b1 < b2 < b1 # exception raised here +``` + +## `unsupported-operator` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1449) + + +**What it does** + +Checks for binary expressions, comparisons, and unary expressions where +the operands don't support the operator. + +**Why is this bad?** + +Attempting to use an unsupported operator will raise a `TypeError` at +runtime. + +**Examples** + +```python +class A: ... + +A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' +``` + +## `zero-stepsize-in-slice` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471) + + +**What it does** + +Checks for step size 0 in slices. + +**Why is this bad?** + +A slice with a step size of zero will raise a `ValueError` at runtime. + +**Examples** + +```python +l = list(range(10)) +l[1:10:0] # ValueError: slice step cannot be zero +``` + +## `invalid-ignore-comment` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L65) + + +**What it does** + +Checks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect. + +**Why is this bad?** + +A syntactically incorrect ignore comment is probably a mistake and is useless. + +**Examples** + +```py +a = 20 / 0 # type: ignoree +``` + +Use instead: + +```py +a = 20 / 0 # type: ignore +``` + +## `possibly-unbound-attribute` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1175) + + +**What it does** + +Checks for possibly unbound attributes. + +**Why is this bad?** + +Attempting to access an unbound attribute will raise an `AttributeError` at runtime. + +**Examples** + +```python +class A: + if b: + c = 0 + +A.c # AttributeError: type object 'A' has no attribute 'c' +``` + +## `possibly-unbound-implicit-call` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L116) + + +**What it does** + +Checks for implicit calls to possibly unbound methods. + +**Why is this bad?** + +Expressions such as `x[y]` and `x * y` call methods +under the hood (`__getitem__` and `__mul__` respectively). +Calling an unbound method will raise an `AttributeError` at runtime. + +**Examples** + +```python +import datetime + +class A: + if datetime.date.today().weekday() != 6: + def __getitem__(self, v): ... + +A()[0] # TypeError: 'A' object is not subscriptable +``` + +## `possibly-unbound-import` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197) + + +**What it does** + +Checks for imports of symbols that may be unbound. + +**Why is this bad?** + +Importing an unbound module or name will raise a `ModuleNotFoundError` +or `ImportError` at runtime. + +**Examples** + +```python +# module.py +import datetime + +if datetime.date.today().weekday() != 6: + a = 1 + +# main.py +from module import a # ImportError: cannot import name 'a' from 'module' +``` + +## `redundant-cast` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542) + + +**What it does** + +Detects redundant `cast` calls where the value already has the target type. + +**Why is this bad?** + +These casts have no effect and can be removed. + +**Example** + +```python +def f() -> int: + return 10 + +cast(int, f()) # Redundant +``` + +## `undefined-reveal` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350) + + +**What it does** + +Checks for calls to `reveal_type` without importing it. + +**Why is this bad?** + +Using `reveal_type` without importing it will raise a `NameError` at runtime. + +**Examples** + +```python +reveal_type(1) # NameError: name 'reveal_type' is not defined +``` + +## `unknown-rule` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-rule) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L40) + + +**What it does** + +Checks for `ty: ignore[code]` where `code` isn't a known lint rule. + +**Why is this bad?** + +A `ty: ignore[code]` directive with a `code` that doesn't match +any known rule will not suppress any type errors, and is probably a mistake. + +**Examples** + +```py +a = 20 / 0 # ty: ignore[division-by-zer] +``` + +Use instead: + +```py +a = 20 / 0 # ty: ignore[division-by-zero] +``` + +## `unsupported-base` + + +Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L550) + + +**What it does** + +Checks for class definitions that have bases which are unsupported by ty. + +**Why is this bad?** + +If a class has a base that is an instance of a complex type such as a union type, +ty will not be able to resolve the [method resolution order] (MRO) for the class. +This will lead to an inferior understanding of your codebase and unpredictable +type-checking behavior. + +**Examples** + +```python +import datetime + +class A: ... +class B: ... + +if datetime.date.today().weekday() != 6: + C = A +else: + C = B + +class D(C): ... # error: [unsupported-base] +``` + +[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + +## `division-by-zero` + + +Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L245) + + +**What it does** + +It detects division by zero. + +**Why is this bad?** + +Dividing by zero raises a `ZeroDivisionError` at runtime. + +**Examples** + +```python +5 / 0 +``` + +## `possibly-unresolved-reference` + + +Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1223) + + +**What it does** + +Checks for references to names that are possibly not defined. + +**Why is this bad?** + +Using an undefined variable will raise a `NameError` at runtime. + +**Example** + + +```python +for i in range(0): + x = i + +print(x) # NameError: name 'x' is not defined +``` + +## `unused-ignore-comment` + + +Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L15) + + +**What it does** + +Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable. + +**Why is this bad?** + +A `type: ignore` directive that no longer matches any diagnostic violations is likely +included by mistake, and should be removed to avoid confusion. + +**Examples** + +```py +a = 20 / 2 # ty: ignore[division-by-zero] +``` + +Use instead: + +```py +a = 20 / 2 +``` + diff --git a/crates/red_knot/docs/tracing-flamegraph.png b/crates/ty/docs/tracing-flamegraph.png similarity index 100% rename from crates/red_knot/docs/tracing-flamegraph.png rename to crates/ty/docs/tracing-flamegraph.png diff --git a/crates/ty/docs/tracing.md b/crates/ty/docs/tracing.md new file mode 100644 index 0000000000000..898b8271d1f23 --- /dev/null +++ b/crates/ty/docs/tracing.md @@ -0,0 +1,140 @@ +# Tracing + +Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a +particular thing. +Note, tracing messages with severity `debug` or greater are user-facing. They should be phrased accordingly. +Tracing spans are only shown when using `-vvv`. + +## Verbosity levels + +The CLI supports different verbosity levels. + +- default: Only show errors and warnings. +- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, + etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you + which packages are fresh. +- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When + you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific + action or state change. +- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. + Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you + almost certainly want to use `TY_LOG` to filter it down to the area your investigating. + +## Better logging with `TY_LOG` and `TY_MAX_PARALLELISM` + +By default, the CLI shows messages from the `ruff` and `ty` crates. Tracing messages from other crates are not shown. +The `TY_LOG` environment variable allows you to customize which messages are shown by specifying one +or +more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). + +The `TY_MAX_PARALLELISM` environment variable, meanwhile, can be used to control the level of parallelism ty uses. +By default, ty will attempt to parallelize its work so that multiple files are checked simultaneously, +but this can result in a confused logging output where messages from different threads are intertwined and non +determinism. +To switch off parallelism entirely and have more readable logs, use `TY_MAX_PARALLELISM=1` (or `RAYON_NUM_THREADS=1`). + +### Examples + +#### Show all debug messages + +Shows debug messages from all crates. + +```bash +TY_LOG=debug +``` + +#### Show salsa query execution messages + +Show the salsa `execute: my_query` messages in addition to all ty messages. + +```bash +TY_LOG=ruff=trace,ty=trace,salsa=info +``` + +#### Show typing traces + +Only show traces for the `ty_python_semantic::types` module. + +```bash +TY_LOG="ty_python_semantic::types" +``` + +Note: Ensure that you use `-vvv` to see tracing spans. + +#### Show messages for a single file + +Shows all messages that are inside of a span for a specific file. + +```bash +TY_LOG=ty[{file=/home/micha/astral/test/x.py}]=trace +``` + +**Note**: Tracing still shows all spans because tracing can't know at the time of entering the span +whether one if its children has the file `x.py`. + +**Note**: Salsa currently logs the entire memoized values. In our case, the source text and parsed AST. +This very quickly leads to extremely long outputs. + +## Tracing and Salsa + +Be mindful about using `tracing` in Salsa queries, especially when using `warn` or `error` because it isn't guaranteed +that the query will execute after restoring from a persistent cache. In which case the user won't see the message. + +For example, don't use `tracing` to show the user a message when generating a lint violation failed +because the message would only be shown when linting the file the first time, but not on subsequent analysis +runs or when restoring from a persistent cache. This can be confusing for users because they +don't understand why a specific lint violation isn't raised. Instead, change your +query to return the failure as part of the query's result or use a Salsa accumulator. + +## Tracing in tests + +You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in +tests. + +```rust +use ruff_db::testing::setup_logging; + +#[test] +fn test() { + let _logging = setup_logging(); + + tracing::info!("This message will be printed to stderr"); +} +``` + +Note: Most test runners capture stderr and only show its output when a test fails. + +Note also that `setup_logging` only sets up logging for the current thread because +[`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be +called **once**. + +## Release builds + +`trace!` events are removed in release builds. + +## Profiling + +ty generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable +`TY_LOG_PROFILE` to `1` or `true`. + +```bash +TY_LOG_PROFILE=1 ty -- --current-directory=../test -vvv +``` + +You can convert the textual representation into a visual one using `inferno`. + +```shell +cargo install inferno +``` + +```shell +# flamegraph +cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg + +# flamechart +cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg +``` + +![Example flamegraph](./tracing-flamegraph.png) + +See [`tracing-flame`](https://crates.io/crates/tracing-flame) for more details. diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs new file mode 100644 index 0000000000000..c177c9d64bce7 --- /dev/null +++ b/crates/ty/src/args.rs @@ -0,0 +1,409 @@ +use crate::logging::Verbosity; +use crate::python_version::PythonVersion; +use clap::error::ErrorKind; +use clap::{ArgAction, ArgMatches, Error, Parser}; +use ruff_db::system::SystemPathBuf; +use ty_project::combine::Combine; +use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions}; +use ty_project::metadata::value::{RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource}; +use ty_python_semantic::lint; + +#[derive(Debug, Parser)] +#[command(author, name = "ty", about = "An extremely fast Python type checker.")] +#[command(long_version = crate::version::version())] +pub struct Cli { + #[command(subcommand)] + pub(crate) command: Command, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, clap::Subcommand)] +pub(crate) enum Command { + /// Check a project for type errors. + Check(CheckCommand), + + /// Start the language server + Server, + + /// Display ty's version + Version, + + /// Generate shell completion + #[clap(hide = true)] + GenerateShellCompletion { shell: clap_complete_command::Shell }, +} + +#[derive(Debug, Parser)] +pub(crate) struct CheckCommand { + /// List of files or directories to check. + #[clap( + help = "List of files or directories to check [default: the project root]", + value_name = "PATH" + )] + pub paths: Vec, + + /// Run the command within the given project directory. + /// + /// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory, + /// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set. + /// + /// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory. + #[arg(long, value_name = "PROJECT")] + pub(crate) project: Option, + + /// Path to the Python environment. + /// + /// ty uses the Python environment to resolve type information and third-party dependencies. + /// + /// If not specified, ty will attempt to infer it from the `VIRTUAL_ENV` or `CONDA_PREFIX` + /// environment variables, or discover a `.venv` directory in the project root or working + /// directory. + /// + /// If a path to a Python interpreter is provided, e.g., `.venv/bin/python3`, ty will attempt to + /// find an environment two directories up from the interpreter's path, e.g., `.venv`. At this + /// time, ty does not invoke the interpreter to determine the location of the environment. This + /// means that ty will not resolve dynamic executables such as a shim. + /// + /// ty will search in the resolved environment's `site-packages` directories for type + /// information and third-party imports. + #[arg(long, value_name = "PATH")] + pub(crate) python: Option, + + /// Custom directory to use for stdlib typeshed stubs. + #[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")] + pub(crate) typeshed: Option, + + /// Additional path to use as a module-resolution source (can be passed multiple times). + #[arg(long, value_name = "PATH")] + pub(crate) extra_search_path: Option>, + + /// Python version to assume when resolving types. + /// + /// The Python version affects allowed syntax, type definitions of the standard library, and + /// type definitions of first- and third-party modules that are conditional on the Python version. + /// + /// If a version is not specified on the command line or in a configuration file, + /// ty will try the following techniques in order of preference to determine a value: + /// 1. Check for the `project.requires-python` setting in a `pyproject.toml` file + /// and use the minimum version from the specified range + /// 2. Check for an activated or configured Python environment + /// and attempt to infer the Python version of that environment + /// 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13) + #[arg(long, value_name = "VERSION", alias = "target-version")] + pub(crate) python_version: Option, + + /// Target platform to assume when resolving types. + /// + /// This is used to specialize the type of `sys.platform` and will affect the visibility + /// of platform-specific functions and attributes. If the value is set to `all`, no + /// assumptions are made about the target platform. If unspecified, the current system's + /// platform will be used. + #[arg(long, value_name = "PLATFORM", alias = "platform")] + pub(crate) python_platform: Option, + + #[clap(flatten)] + pub(crate) verbosity: Verbosity, + + #[clap(flatten)] + pub(crate) rules: RulesArg, + + #[clap(flatten)] + pub(crate) config: ConfigsArg, + + /// The path to a `ty.toml` file to use for configuration. + /// + /// While ty configuration can be included in a `pyproject.toml` file, it is not allowed in this context. + #[arg(long, env = "TY_CONFIG_FILE", value_name = "PATH")] + pub(crate) config_file: Option, + + /// The format to use for printing diagnostic messages. + #[arg(long)] + pub(crate) output_format: Option, + + /// Control when colored output is used. + #[arg(long, value_name = "WHEN")] + pub(crate) color: Option, + + /// Use exit code 1 if there are any warning-level diagnostics. + #[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)] + pub(crate) error_on_warning: Option, + + /// Always use exit code 0, even when there are error-level diagnostics. + #[arg(long)] + pub(crate) exit_zero: bool, + + /// Watch files for changes and recheck files related to the changed files. + #[arg(long, short = 'W')] + pub(crate) watch: bool, + + /// Respect file exclusions via `.gitignore` and other standard ignore files. + /// Use `--no-respect-gitignore` to disable. + #[arg( + long, + overrides_with("no_respect_ignore_files"), + help_heading = "File selection", + default_missing_value = "true", + num_args = 0..1 + )] + respect_ignore_files: Option, + #[clap(long, overrides_with("respect_ignore_files"), hide = true)] + no_respect_ignore_files: bool, + + /// Glob patterns for files to exclude from type checking. + /// + /// Uses gitignore-style syntax to exclude files and directories from type checking. + /// Supports patterns like `tests/`, `*.tmp`, `**/__pycache__/**`. + #[arg(long, help_heading = "File selection")] + exclude: Option>, +} + +impl CheckCommand { + pub(crate) fn into_options(self) -> Options { + let rules = if self.rules.is_empty() { + None + } else { + Some( + self.rules + .into_iter() + .map(|(rule, level)| (RangedValue::cli(rule), RangedValue::cli(level))) + .collect(), + ) + }; + + // --no-respect-gitignore defaults to false and is set true by CLI flag. If passed, override config file + // Otherwise, only pass this through if explicitly set (don't default to anything here to + // make sure that doesn't take precedence over an explicitly-set config file value) + let respect_ignore_files = self + .no_respect_ignore_files + .then_some(false) + .or(self.respect_ignore_files); + let options = Options { + environment: Some(EnvironmentOptions { + python_version: self + .python_version + .map(|version| RangedValue::cli(version.into())), + python_platform: self + .python_platform + .map(|platform| RangedValue::cli(platform.into())), + python: self.python.map(RelativePathBuf::cli), + typeshed: self.typeshed.map(RelativePathBuf::cli), + extra_paths: self.extra_search_path.map(|extra_search_paths| { + extra_search_paths + .into_iter() + .map(RelativePathBuf::cli) + .collect() + }), + ..EnvironmentOptions::default() + }), + terminal: Some(TerminalOptions { + output_format: self + .output_format + .map(|output_format| RangedValue::cli(output_format.into())), + error_on_warning: self.error_on_warning, + }), + src: Some(SrcOptions { + respect_ignore_files, + exclude: self.exclude.map(|excludes| { + RangedValue::cli(excludes.iter().map(RelativeGlobPattern::cli).collect()) + }), + ..SrcOptions::default() + }), + rules, + ..Options::default() + }; + // Merge with options passed in via --config + options.combine(self.config.into_options().unwrap_or_default()) + } +} + +/// A list of rules to enable or disable with a given severity. +/// +/// This type is used to parse the `--error`, `--warn`, and `--ignore` arguments +/// while preserving the order in which they were specified (arguments last override previous severities). +#[derive(Debug)] +pub(crate) struct RulesArg(Vec<(String, lint::Level)>); + +impl RulesArg { + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn into_iter(self) -> impl Iterator { + self.0.into_iter() + } +} + +impl clap::FromArgMatches for RulesArg { + fn from_arg_matches(matches: &ArgMatches) -> Result { + let mut rules = Vec::new(); + + for (level, arg_id) in [ + (lint::Level::Ignore, "ignore"), + (lint::Level::Warn, "warn"), + (lint::Level::Error, "error"), + ] { + let indices = matches.indices_of(arg_id).into_iter().flatten(); + let levels = matches.get_many::(arg_id).into_iter().flatten(); + rules.extend( + indices + .zip(levels) + .map(|(index, rule)| (index, rule, level)), + ); + } + + // Sort by their index so that values specified later override earlier ones. + rules.sort_by_key(|(index, _, _)| *index); + + Ok(Self( + rules + .into_iter() + .map(|(_, rule, level)| (rule.to_owned(), level)) + .collect(), + )) + } + + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> { + self.0 = Self::from_arg_matches(matches)?.0; + Ok(()) + } +} + +impl clap::Args for RulesArg { + fn augment_args(cmd: clap::Command) -> clap::Command { + const HELP_HEADING: &str = "Enabling / disabling rules"; + + cmd.arg( + clap::Arg::new("error") + .long("error") + .action(ArgAction::Append) + .help("Treat the given rule as having severity 'error'. Can be specified multiple times.") + .value_name("RULE") + .help_heading(HELP_HEADING), + ) + .arg( + clap::Arg::new("warn") + .long("warn") + .action(ArgAction::Append) + .help("Treat the given rule as having severity 'warn'. Can be specified multiple times.") + .value_name("RULE") + .help_heading(HELP_HEADING), + ) + .arg( + clap::Arg::new("ignore") + .long("ignore") + .action(ArgAction::Append) + .help("Disables the rule. Can be specified multiple times.") + .value_name("RULE") + .help_heading(HELP_HEADING), + ) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + Self::augment_args(cmd) + } +} + +/// The diagnostic output format. +#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)] +pub enum OutputFormat { + /// Print diagnostics verbosely, with context and helpful hints \[default\]. + /// + /// Diagnostic messages may include additional context and + /// annotations on the input to help understand the message. + #[default] + #[value(name = "full")] + Full, + /// Print diagnostics concisely, one per line. + /// + /// This will guarantee that each diagnostic is printed on + /// a single line. Only the most important or primary aspects + /// of the diagnostic are included. Contextual information is + /// dropped. + #[value(name = "concise")] + Concise, +} + +impl From for ruff_db::diagnostic::DiagnosticFormat { + fn from(format: OutputFormat) -> ruff_db::diagnostic::DiagnosticFormat { + match format { + OutputFormat::Full => Self::Full, + OutputFormat::Concise => Self::Concise, + } + } +} + +/// Control when colored output is used. +#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)] +pub(crate) enum TerminalColor { + /// Display colors if the output goes to an interactive terminal. + #[default] + Auto, + + /// Always display colors. + Always, + + /// Never display colors. + Never, +} + +/// A TOML ` = ` pair +/// (such as you might find in a `ty.toml` configuration file) +/// overriding a specific configuration option. +/// +/// Overrides of individual settings using this option always take precedence +/// over all configuration files. +#[derive(Debug, Clone)] +pub(crate) struct ConfigsArg(Option); + +impl clap::FromArgMatches for ConfigsArg { + fn from_arg_matches(matches: &ArgMatches) -> Result { + let combined = matches + .get_many::("config") + .into_iter() + .flatten() + .map(|s| { + Options::from_toml_str(s, ValueSource::Cli) + .map_err(|err| Error::raw(ErrorKind::InvalidValue, err.to_string())) + }) + .collect::, _>>()? + .into_iter() + .reduce(|acc, item| item.combine(acc)); + Ok(Self(combined)) + } + + fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> { + self.0 = Self::from_arg_matches(matches)?.0; + Ok(()) + } +} + +impl clap::Args for ConfigsArg { + fn augment_args(cmd: clap::Command) -> clap::Command { + cmd.arg( + clap::Arg::new("config") + .short('c') + .long("config") + .value_name("CONFIG_OPTION") + .help("A TOML ` = ` pair overriding a specific configuration option.") + .long_help( + " +A TOML ` = ` pair (such as you might find in a `ty.toml` configuration file) +overriding a specific configuration option. + +Overrides of individual settings using this option always take precedence +over all configuration files.", + ) + .action(ArgAction::Append), + ) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + Self::augment_args(cmd) + } +} + +impl ConfigsArg { + pub(crate) fn into_options(self) -> Option { + self.0 + } +} diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs new file mode 100644 index 0000000000000..1286bae8c8f65 --- /dev/null +++ b/crates/ty/src/lib.rs @@ -0,0 +1,497 @@ +mod args; +mod logging; +mod printer; +mod python_version; +mod version; + +pub use args::Cli; +use ty_static::EnvVars; + +use std::fmt::Write; +use std::process::{ExitCode, Termination}; + +use anyhow::Result; +use std::sync::Mutex; + +use crate::args::{CheckCommand, Command, TerminalColor}; +use crate::logging::setup_tracing; +use crate::printer::Printer; +use anyhow::{Context, anyhow}; +use clap::{CommandFactory, Parser}; +use colored::Colorize; +use crossbeam::channel as crossbeam_channel; +use rayon::ThreadPoolBuilder; +use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity}; +use ruff_db::max_parallelism; +use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; +use salsa::plumbing::ZalsaDatabase; +use ty_project::metadata::options::ProjectOptionsOverrides; +use ty_project::watch::ProjectWatcher; +use ty_project::{Db, watch}; +use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_server::run_server; + +pub fn run() -> anyhow::Result { + setup_rayon(); + + let args = wild::args_os(); + let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX) + .context("Failed to read CLI arguments from file")?; + let args = Cli::parse_from(args); + + match args.command { + Command::Server => run_server().map(|()| ExitStatus::Success), + Command::Check(check_args) => run_check(check_args), + Command::Version => version().map(|()| ExitStatus::Success), + Command::GenerateShellCompletion { shell } => { + use std::io::stdout; + + shell.generate(&mut Cli::command(), &mut stdout()); + Ok(ExitStatus::Success) + } + } +} + +pub(crate) fn version() -> Result<()> { + let mut stdout = Printer::default().stream_for_requested_summary().lock(); + let version_info = crate::version::version(); + writeln!(stdout, "ty {}", &version_info)?; + Ok(()) +} + +fn run_check(args: CheckCommand) -> anyhow::Result { + set_colored_override(args.color); + + let verbosity = args.verbosity.level(); + let _guard = setup_tracing(verbosity, args.color.unwrap_or_default())?; + + let printer = Printer::default().with_verbosity(verbosity); + + tracing::warn!( + "ty is pre-release software and not ready for production use. \ + Expect to encounter bugs, missing features, and fatal errors.", + ); + + tracing::debug!("Version: {}", version::version()); + + // The base path to which all CLI arguments are relative to. + let cwd = { + let cwd = std::env::current_dir().context("Failed to get the current working directory")?; + SystemPathBuf::from_path_buf(cwd) + .map_err(|path| { + anyhow!( + "The current working directory `{}` contains non-Unicode characters. ty only supports Unicode paths.", + path.display() + ) + })? + }; + + let project_path = args + .project + .as_ref() + .map(|project| { + if project.as_std_path().is_dir() { + Ok(SystemPath::absolute(project, &cwd)) + } else { + Err(anyhow!( + "Provided project path `{project}` is not a directory" + )) + } + }) + .transpose()? + .unwrap_or_else(|| cwd.clone()); + + let check_paths: Vec<_> = args + .paths + .iter() + .map(|path| SystemPath::absolute(path, &cwd)) + .collect(); + + let system = OsSystem::new(&cwd); + let watch = args.watch; + let exit_zero = args.exit_zero; + let config_file = args + .config_file + .as_ref() + .map(|path| SystemPath::absolute(path, &cwd)); + + let mut project_metadata = match &config_file { + Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?, + None => ProjectMetadata::discover(&project_path, &system)?, + }; + + let options = args.into_options(); + project_metadata.apply_options(options.clone()); + project_metadata.apply_configuration_files(&system)?; + + let mut db = ProjectDatabase::new(project_metadata, system)?; + + if !check_paths.is_empty() { + db.project().set_included_paths(&mut db, check_paths); + } + + let project_options_overrides = ProjectOptionsOverrides::new(config_file, options); + let (main_loop, main_loop_cancellation_token) = + MainLoop::new(project_options_overrides, printer); + + // Listen to Ctrl+C and abort the watch mode. + let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token)); + ctrlc::set_handler(move || { + let mut lock = main_loop_cancellation_token.lock().unwrap(); + + if let Some(token) = lock.take() { + token.stop(); + } + })?; + + let exit_status = if watch { + main_loop.watch(&mut db)? + } else { + main_loop.run(&mut db)? + }; + + let mut stdout = printer.stream_for_requested_summary().lock(); + match std::env::var(EnvVars::TY_MEMORY_REPORT).as_deref() { + Ok("short") => write!(stdout, "{}", db.salsa_memory_dump().display_short())?, + Ok("mypy_primer") => write!(stdout, "{}", db.salsa_memory_dump().display_mypy_primer())?, + Ok("full") => write!(stdout, "{}", db.salsa_memory_dump().display_full())?, + _ => {} + } + + std::mem::forget(db); + + if exit_zero { + Ok(ExitStatus::Success) + } else { + Ok(exit_status) + } +} + +#[derive(Copy, Clone)] +pub enum ExitStatus { + /// Checking was successful and there were no errors. + Success = 0, + + /// Checking was successful but there were errors. + Failure = 1, + + /// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...) + Error = 2, + + /// Internal ty error (panic, or any other error that isn't due to the user using the + /// program incorrectly or transient environment errors). + InternalError = 101, +} + +impl Termination for ExitStatus { + fn report(self) -> ExitCode { + ExitCode::from(self as u8) + } +} + +struct MainLoop { + /// Sender that can be used to send messages to the main loop. + sender: crossbeam_channel::Sender, + + /// Receiver for the messages sent **to** the main loop. + receiver: crossbeam_channel::Receiver, + + /// The file system watcher, if running in watch mode. + watcher: Option, + + /// Interface for displaying information to the user. + printer: Printer, + + project_options_overrides: ProjectOptionsOverrides, +} + +impl MainLoop { + fn new( + project_options_overrides: ProjectOptionsOverrides, + printer: Printer, + ) -> (Self, MainLoopCancellationToken) { + let (sender, receiver) = crossbeam_channel::bounded(10); + + ( + Self { + sender: sender.clone(), + receiver, + watcher: None, + project_options_overrides, + printer, + }, + MainLoopCancellationToken { sender }, + ) + } + + fn watch(mut self, db: &mut ProjectDatabase) -> Result { + tracing::debug!("Starting watch mode"); + let sender = self.sender.clone(); + let watcher = watch::directory_watcher(move |event| { + sender.send(MainLoopMessage::ApplyChanges(event)).unwrap(); + })?; + + self.watcher = Some(ProjectWatcher::new(watcher, db)); + + // Do not show progress bars with `--watch`, indicatif does not seem to + // handle cancelling independent progress bars very well. + // TODO(zanieb): We can probably use `MultiProgress` to handle this case in the future. + self.printer = self.printer.with_no_progress(); + self.run(db)?; + + Ok(ExitStatus::Success) + } + + fn run(self, db: &mut ProjectDatabase) -> Result { + self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + + let result = self.main_loop(db); + + tracing::debug!("Exiting main loop"); + + result + } + + fn main_loop(mut self, db: &mut ProjectDatabase) -> Result { + // Schedule the first check. + tracing::debug!("Starting main loop"); + + let mut revision = 0u64; + + while let Ok(message) = self.receiver.recv() { + match message { + MainLoopMessage::CheckWorkspace => { + let db = db.clone(); + let sender = self.sender.clone(); + + // Spawn a new task that checks the project. This needs to be done in a separate thread + // to prevent blocking the main loop here. + rayon::spawn(move || { + match salsa::Cancelled::catch(|| { + let mut reporter = IndicatifReporter::from(self.printer); + db.check_with_reporter(&mut reporter) + }) { + Ok(result) => { + // Send the result back to the main loop for printing. + sender + .send(MainLoopMessage::CheckCompleted { result, revision }) + .unwrap(); + } + Err(cancelled) => { + tracing::debug!("Check has been cancelled: {cancelled:?}"); + } + } + }); + } + + MainLoopMessage::CheckCompleted { + result, + revision: check_revision, + } => { + let terminal_settings = db.project().settings(db).terminal(); + let display_config = DisplayDiagnosticConfig::default() + .format(terminal_settings.output_format) + .color(colored::control::SHOULD_COLORIZE.should_colorize()); + + if check_revision == revision { + if db.project().files(db).is_empty() { + tracing::warn!("No python files found under the given path(s)"); + } + + // TODO: We should have an official flag to silence workspace diagnostics. + if std::env::var("TY_MEMORY_REPORT").as_deref() == Ok("mypy_primer") { + return Ok(ExitStatus::Success); + } + + if result.is_empty() { + writeln!( + self.printer.stream_for_success_summary(), + "{}", + "All checks passed!".green().bold() + )?; + + if self.watcher.is_none() { + return Ok(ExitStatus::Success); + } + } else { + let mut max_severity = Severity::Info; + let diagnostics_count = result.len(); + + let mut stdout = self.printer.stream_for_details().lock(); + for diagnostic in result { + // Only render diagnostics if they're going to be displayed, since doing + // so is expensive. + if stdout.is_enabled() { + write!(stdout, "{}", diagnostic.display(db, &display_config))?; + } + + max_severity = max_severity.max(diagnostic.severity()); + } + + writeln!( + self.printer.stream_for_failure_summary(), + "Found {} diagnostic{}", + diagnostics_count, + if diagnostics_count > 1 { "s" } else { "" } + )?; + + if max_severity.is_fatal() { + tracing::warn!( + "A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details." + ); + } + + if self.watcher.is_none() { + return Ok(match max_severity { + Severity::Info => ExitStatus::Success, + Severity::Warning => { + if terminal_settings.error_on_warning { + ExitStatus::Failure + } else { + ExitStatus::Success + } + } + Severity::Error => ExitStatus::Failure, + Severity::Fatal => ExitStatus::InternalError, + }); + } + } + } else { + tracing::debug!( + "Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}" + ); + } + } + + MainLoopMessage::ApplyChanges(changes) => { + revision += 1; + // Automatically cancels any pending queries and waits for them to complete. + db.apply_changes(changes, Some(&self.project_options_overrides)); + if let Some(watcher) = self.watcher.as_mut() { + watcher.update(db); + } + self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + } + MainLoopMessage::Exit => { + // Cancel any pending queries and wait for them to complete. + // TODO: Don't use Salsa internal APIs + // [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries) + let _ = db.zalsa_mut(); + return Ok(ExitStatus::Success); + } + } + + tracing::debug!("Waiting for next main loop message."); + } + + Ok(ExitStatus::Success) + } +} + +/// A progress reporter for `ty check`. +enum IndicatifReporter { + /// A constructed reporter that is not yet ready, contains the target for the progress bar. + Pending(indicatif::ProgressDrawTarget), + /// A reporter that is ready, containing a progress bar to report to. + /// + /// Initialization of the bar is deferred to [`ty_project::ProgressReporter::set_files`] so we + /// do not initialize the bar too early as it may take a while to collect the number of files to + /// process and we don't want to display an empty "0/0" bar. + Initialized(indicatif::ProgressBar), +} + +impl From for IndicatifReporter { + fn from(printer: Printer) -> Self { + Self::Pending(printer.progress_target()) + } +} + +impl ty_project::ProgressReporter for IndicatifReporter { + fn set_files(&mut self, files: usize) { + let target = match std::mem::replace( + self, + IndicatifReporter::Pending(indicatif::ProgressDrawTarget::hidden()), + ) { + Self::Pending(target) => target, + Self::Initialized(_) => panic!("The progress reporter should only be initialized once"), + }; + + let bar = indicatif::ProgressBar::with_draw_target(Some(files as u64), target); + bar.set_style( + indicatif::ProgressStyle::with_template( + "{msg:8.dim} {bar:60.green/dim} {pos}/{len} files", + ) + .unwrap() + .progress_chars("--"), + ); + bar.set_message("Checking"); + *self = Self::Initialized(bar); + } + + fn report_file(&self, _file: &ruff_db::files::File) { + match self { + IndicatifReporter::Initialized(progress_bar) => { + progress_bar.inc(1); + } + IndicatifReporter::Pending(_) => { + panic!("`report_file` called before `set_files`") + } + } + } +} + +#[derive(Debug)] +struct MainLoopCancellationToken { + sender: crossbeam_channel::Sender, +} + +impl MainLoopCancellationToken { + fn stop(self) { + self.sender.send(MainLoopMessage::Exit).unwrap(); + } +} + +/// Message sent from the orchestrator to the main loop. +#[derive(Debug)] +enum MainLoopMessage { + CheckWorkspace, + CheckCompleted { + /// The diagnostics that were found during the check. + result: Vec, + revision: u64, + }, + ApplyChanges(Vec), + Exit, +} + +fn set_colored_override(color: Option) { + let Some(color) = color else { + return; + }; + + match color { + TerminalColor::Auto => { + colored::control::unset_override(); + } + TerminalColor::Always => { + colored::control::set_override(true); + } + TerminalColor::Never => { + colored::control::set_override(false); + } + } +} + +/// Initializes the global rayon thread pool to never use more than `TY_MAX_PARALLELISM` threads. +fn setup_rayon() { + ThreadPoolBuilder::default() + .num_threads(max_parallelism().get()) + // Use a reasonably large stack size to avoid running into stack overflows too easily. The + // size was chosen in such a way as to still be able to handle large expressions involving + // binary operators (x + x + … + x) both during the AST walk in semantic index building as + // well as during type checking. Using this stack size, we can handle handle expressions + // that are several times larger than the corresponding limits in existing type checkers. + .stack_size(16 * 1024 * 1024) + .build_global() + .unwrap(); +} diff --git a/crates/ty/src/logging.rs b/crates/ty/src/logging.rs new file mode 100644 index 0000000000000..a85d527a477f8 --- /dev/null +++ b/crates/ty/src/logging.rs @@ -0,0 +1,288 @@ +//! Sets up logging for ty + +use crate::args::TerminalColor; +use anyhow::Context; +use colored::Colorize; +use std::fmt; +use std::fs::File; +use std::io::{BufWriter, IsTerminal}; +use tracing::{Event, Subscriber}; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; +use tracing_subscriber::registry::LookupSpan; +use ty_static::EnvVars; + +/// Logging flags to `#[command(flatten)]` into your CLI +#[derive(clap::Args, Debug, Clone, Default)] +#[command(about = None, long_about = None)] +pub(crate) struct Verbosity { + #[arg( + long, + short = 'v', + help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)", + action = clap::ArgAction::Count, + global = true, + overrides_with = "quiet", + )] + verbose: u8, + + #[arg( + long, + help = "Use quiet output", + action = clap::ArgAction::Count, + global = true, + overrides_with = "verbose", + )] + quiet: u8, +} + +impl Verbosity { + /// Returns the verbosity level based on the number of `-v` and `-q` flags. + /// + /// Returns `None` if the user did not specify any verbosity flags. + pub(crate) fn level(&self) -> VerbosityLevel { + // `--quiet` and `--verbose` are mutually exclusive in Clap, so we can just check one first. + match self.quiet { + 0 => {} + _ => return VerbosityLevel::Quiet, + // TODO(zanieb): Add support for `-qq` with a "silent" mode + } + + match self.verbose { + 0 => VerbosityLevel::Default, + 1 => VerbosityLevel::Verbose, + 2 => VerbosityLevel::ExtraVerbose, + _ => VerbosityLevel::Trace, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)] +pub(crate) enum VerbosityLevel { + /// Quiet output. Only shows Ruff and ty events up to the [`ERROR`](tracing::Level::ERROR). + /// Silences output except for summary information. + Quiet, + + /// Default output level. Only shows Ruff and ty events up to the [`WARN`](tracing::Level::WARN). + #[default] + Default, + + /// Enables verbose output. Emits Ruff and ty events up to the [`INFO`](tracing::Level::INFO). + /// Corresponds to `-v`. + Verbose, + + /// Enables a more verbose tracing format and emits Ruff and ty events up to [`DEBUG`](tracing::Level::DEBUG). + /// Corresponds to `-vv` + ExtraVerbose, + + /// Enables all tracing events and uses a tree-like output format. Corresponds to `-vvv`. + Trace, +} + +impl VerbosityLevel { + const fn level_filter(self) -> LevelFilter { + match self { + VerbosityLevel::Quiet => LevelFilter::ERROR, + VerbosityLevel::Default => LevelFilter::WARN, + VerbosityLevel::Verbose => LevelFilter::INFO, + VerbosityLevel::ExtraVerbose => LevelFilter::DEBUG, + VerbosityLevel::Trace => LevelFilter::TRACE, + } + } + + pub(crate) const fn is_trace(self) -> bool { + matches!(self, VerbosityLevel::Trace) + } + + pub(crate) const fn is_extra_verbose(self) -> bool { + matches!(self, VerbosityLevel::ExtraVerbose) + } +} + +pub(crate) fn setup_tracing( + level: VerbosityLevel, + color: TerminalColor, +) -> anyhow::Result { + use tracing_subscriber::prelude::*; + + // The `TY_LOG` environment variable overrides the default log level. + let filter = if let Ok(log_env_variable) = std::env::var(EnvVars::TY_LOG) { + EnvFilter::builder() + .parse(log_env_variable) + .context("Failed to parse directives specified in TY_LOG environment variable.")? + } else { + match level { + VerbosityLevel::Default => { + // Show warning traces + EnvFilter::default().add_directive(LevelFilter::WARN.into()) + } + level => { + let level_filter = level.level_filter(); + + // Show info|debug|trace events, but allow `TY_LOG` to override + let filter = EnvFilter::default().add_directive( + format!("ty={level_filter}") + .parse() + .expect("Hardcoded directive to be valid"), + ); + + filter.add_directive( + format!("ruff={level_filter}") + .parse() + .expect("Hardcoded directive to be valid"), + ) + } + } + }; + + let (profiling_layer, guard) = setup_profile(); + + let registry = tracing_subscriber::registry() + .with(filter) + .with(profiling_layer); + + let ansi = match color { + TerminalColor::Auto => { + colored::control::SHOULD_COLORIZE.should_colorize() && std::io::stderr().is_terminal() + } + TerminalColor::Always => true, + TerminalColor::Never => false, + }; + + if level.is_trace() { + let subscriber = registry.with( + tracing_subscriber::fmt::layer() + .event_format(tracing_subscriber::fmt::format().pretty()) + .with_thread_ids(true) + .with_ansi(ansi) + .with_writer(std::io::stderr), + ); + + subscriber.init(); + } else { + let subscriber = registry.with( + tracing_subscriber::fmt::layer() + .event_format(TyFormat { + display_level: true, + display_timestamp: level.is_extra_verbose(), + show_spans: false, + }) + .with_ansi(ansi) + .with_writer(std::io::stderr), + ); + + subscriber.init(); + } + + Ok(TracingGuard { + _flame_guard: guard, + }) +} + +#[expect(clippy::type_complexity)] +fn setup_profile() -> ( + Option>>, + Option>>, +) +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + if let Ok("1" | "true") = std::env::var(EnvVars::TY_LOG_PROFILE).as_deref() { + let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded") + .expect("Flame layer to be created"); + (Some(layer), Some(guard)) + } else { + (None, None) + } +} + +pub(crate) struct TracingGuard { + _flame_guard: Option>>, +} + +struct TyFormat { + display_timestamp: bool, + display_level: bool, + show_spans: bool, +} + +/// See +impl FormatEvent for TyFormat +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let meta = event.metadata(); + let ansi = writer.has_ansi_escapes(); + + if self.display_timestamp { + let timestamp = jiff::Zoned::now() + .strftime("%Y-%m-%d %H:%M:%S.%f") + .to_string(); + if ansi { + write!(writer, "{} ", timestamp.dimmed())?; + } else { + write!( + writer, + "{} ", + jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S.%f") + )?; + } + } + + if self.display_level { + let level = meta.level(); + // Same colors as tracing + if ansi { + let formatted_level = level.to_string(); + match *level { + tracing::Level::TRACE => { + write!(writer, "{} ", formatted_level.purple().bold())?; + } + tracing::Level::DEBUG => write!(writer, "{} ", formatted_level.blue().bold())?, + tracing::Level::INFO => write!(writer, "{} ", formatted_level.green().bold())?, + tracing::Level::WARN => write!(writer, "{} ", formatted_level.yellow().bold())?, + tracing::Level::ERROR => write!(writer, "{} ", level.to_string().red().bold())?, + } + } else { + write!(writer, "{level} ")?; + } + } + + if self.show_spans { + let span = event.parent(); + let mut seen = false; + + let span = span + .and_then(|id| ctx.span(id)) + .or_else(|| ctx.lookup_current()); + + let scope = span.into_iter().flat_map(|span| span.scope().from_root()); + + for span in scope { + seen = true; + if ansi { + write!(writer, "{}:", span.metadata().name().bold())?; + } else { + write!(writer, "{}:", span.metadata().name())?; + } + } + + if seen { + writer.write_char(' ')?; + } + } + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } +} diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs new file mode 100644 index 0000000000000..6dbae583feb56 --- /dev/null +++ b/crates/ty/src/main.rs @@ -0,0 +1,33 @@ +use colored::Colorize; +use std::io; +use ty::{ExitStatus, run}; + +pub fn main() -> ExitStatus { + run().unwrap_or_else(|error| { + use io::Write; + + // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. + let mut stderr = io::stderr().lock(); + + // This communicates that this isn't a linter error but ty itself hard-errored for + // some reason (e.g. failed to resolve the configuration) + writeln!(stderr, "{}", "ty failed".red().bold()).ok(); + // Currently we generally only see one error, but e.g. with io errors when resolving + // the configuration it is help to chain errors ("resolving configuration failed" -> + // "failed to read file: subdir/pyproject.toml") + for cause in error.chain() { + // Exit "gracefully" on broken pipe errors. + // + // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 + if let Some(ioerr) = cause.downcast_ref::() { + if ioerr.kind() == io::ErrorKind::BrokenPipe { + return ExitStatus::Success; + } + } + + writeln!(stderr, " {} {cause}", "Cause:".bold()).ok(); + } + + ExitStatus::Error + }) +} diff --git a/crates/ty/src/printer.rs b/crates/ty/src/printer.rs new file mode 100644 index 0000000000000..669786a90f1e0 --- /dev/null +++ b/crates/ty/src/printer.rs @@ -0,0 +1,172 @@ +use std::io::StdoutLock; + +use indicatif::ProgressDrawTarget; + +use crate::logging::VerbosityLevel; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) struct Printer { + verbosity: VerbosityLevel, + no_progress: bool, +} + +impl Printer { + #[must_use] + pub(crate) fn with_no_progress(self) -> Self { + Self { + verbosity: self.verbosity, + no_progress: true, + } + } + + #[must_use] + pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self { + Self { + verbosity, + no_progress: self.no_progress, + } + } + + /// Return the [`ProgressDrawTarget`] for this printer. + pub(crate) fn progress_target(self) -> ProgressDrawTarget { + if self.no_progress { + return ProgressDrawTarget::hidden(); + } + + match self.verbosity { + VerbosityLevel::Quiet => ProgressDrawTarget::hidden(), + VerbosityLevel::Default => ProgressDrawTarget::stderr(), + // Hide the progress bar when in verbose mode. + // Otherwise, it gets interleaved with log messages. + VerbosityLevel::Verbose => ProgressDrawTarget::hidden(), + VerbosityLevel::ExtraVerbose => ProgressDrawTarget::hidden(), + VerbosityLevel::Trace => ProgressDrawTarget::hidden(), + } + } + + /// Return the [`Stdout`] stream for important messages. + /// + /// Unlike [`Self::stdout_general`], the returned stream will be enabled when + /// [`VerbosityLevel::Quiet`] is used. + fn stdout_important(self) -> Stdout { + match self.verbosity { + VerbosityLevel::Quiet => Stdout::enabled(), + VerbosityLevel::Default => Stdout::enabled(), + VerbosityLevel::Verbose => Stdout::enabled(), + VerbosityLevel::ExtraVerbose => Stdout::enabled(), + VerbosityLevel::Trace => Stdout::enabled(), + } + } + + /// Return the [`Stdout`] stream for general messages. + /// + /// The returned stream will be disabled when [`VerbosityLevel::Quiet`] is used. + fn stdout_general(self) -> Stdout { + match self.verbosity { + VerbosityLevel::Quiet => Stdout::disabled(), + VerbosityLevel::Default => Stdout::enabled(), + VerbosityLevel::Verbose => Stdout::enabled(), + VerbosityLevel::ExtraVerbose => Stdout::enabled(), + VerbosityLevel::Trace => Stdout::enabled(), + } + } + + /// Return the [`Stdout`] stream for a summary message that was explicitly requested by the + /// user. + /// + /// For example, in `ty version` the user has requested the version information and we should + /// display it even if [`VerbosityLevel::Quiet`] is used. Or, in `ty check`, if the + /// `TY_MEMORY_REPORT` variable has been set, we should display the memory report because the + /// user has opted-in to display. + pub(crate) fn stream_for_requested_summary(self) -> Stdout { + self.stdout_important() + } + + /// Return the [`Stdout`] stream for a summary message on failure. + /// + /// For example, in `ty check`, this would be used for the message indicating the number of + /// diagnostics found. The failure summary should capture information that is not reflected in + /// the exit code. + pub(crate) fn stream_for_failure_summary(self) -> Stdout { + self.stdout_important() + } + + /// Return the [`Stdout`] stream for a summary message on success. + /// + /// For example, in `ty check`, this would be used for the message indicating that no diagnostic + /// were found. The success summary does not capture important information for users that have + /// opted-in to [`VerbosityLevel::Quiet`]. + pub(crate) fn stream_for_success_summary(self) -> Stdout { + self.stdout_general() + } + + /// Return the [`Stdout`] stream for detailed messages. + /// + /// For example, in `ty check`, this would be used for the diagnostic output. + pub(crate) fn stream_for_details(self) -> Stdout { + self.stdout_general() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum StreamStatus { + Enabled, + Disabled, +} + +#[derive(Debug)] +pub(crate) struct Stdout { + status: StreamStatus, + lock: Option>, +} + +impl Stdout { + fn enabled() -> Self { + Self { + status: StreamStatus::Enabled, + lock: None, + } + } + + fn disabled() -> Self { + Self { + status: StreamStatus::Disabled, + lock: None, + } + } + + pub(crate) fn lock(mut self) -> Self { + match self.status { + StreamStatus::Enabled => { + // Drop the previous lock first, to avoid deadlocking + self.lock.take(); + self.lock = Some(std::io::stdout().lock()); + } + StreamStatus::Disabled => self.lock = None, + } + self + } + + fn handle(&mut self) -> Box { + match self.lock.as_mut() { + Some(lock) => Box::new(lock), + None => Box::new(std::io::stdout()), + } + } + + pub(crate) fn is_enabled(&self) -> bool { + matches!(self.status, StreamStatus::Enabled) + } +} + +impl std::fmt::Write for Stdout { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + match self.status { + StreamStatus::Enabled => { + let _ = write!(self.handle(), "{s}"); + Ok(()) + } + StreamStatus::Disabled => Ok(()), + } + } +} diff --git a/crates/red_knot/src/python_version.rs b/crates/ty/src/python_version.rs similarity index 100% rename from crates/red_knot/src/python_version.rs rename to crates/ty/src/python_version.rs diff --git a/crates/ty/src/version.rs b/crates/ty/src/version.rs new file mode 100644 index 0000000000000..d063de5277c97 --- /dev/null +++ b/crates/ty/src/version.rs @@ -0,0 +1,126 @@ +//! Code for representing ty's release version number. +use std::fmt; + +/// Information about the git repository where ty was built from. +pub(crate) struct CommitInfo { + short_commit_hash: String, + commit_date: String, + commits_since_last_tag: u32, + last_tag: Option, +} + +/// ty's version. +pub(crate) struct VersionInfo { + /// ty's version, such as "0.5.1" + version: String, + /// Information about the git commit we may have been built from. + /// + /// `None` if not built from a git repo or if retrieval failed. + commit_info: Option, +} + +impl fmt::Display for VersionInfo { + /// Formatted version information: `[+] ( )` + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.version)?; + + if let Some(ref ci) = self.commit_info { + if ci.commits_since_last_tag > 0 { + write!(f, "+{}", ci.commits_since_last_tag)?; + } + write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?; + } + + Ok(()) + } +} + +impl From for clap::builder::Str { + fn from(val: VersionInfo) -> Self { + val.to_string().into() + } +} + +/// Returns information about ty's version. +pub(crate) fn version() -> VersionInfo { + // Environment variables are only read at compile-time + macro_rules! option_env_str { + ($name:expr) => { + option_env!($name).map(|s| s.to_string()) + }; + } + + // Commit info is pulled from git and set by `build.rs` + let commit_info = option_env_str!("TY_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo { + short_commit_hash, + commit_date: option_env_str!("TY_COMMIT_DATE").unwrap(), + commits_since_last_tag: option_env_str!("TY_LAST_TAG_DISTANCE") + .as_deref() + .map_or(0, |value| value.parse::().unwrap_or(0)), + last_tag: option_env_str!("TY_LAST_TAG"), + }); + + // The version is pulled from `dist-workspace.toml` and set by `build.rs` + let version = option_env_str!("TY_VERSION").unwrap_or_else(|| { + // If missing, using the last tag + commit_info + .as_ref() + .and_then(|info| { + info.last_tag.as_ref().map(|tag| { + tag.strip_prefix("v") + .map(std::string::ToString::to_string) + .unwrap_or(tag.clone()) + }) + }) + .unwrap_or("unknown".to_string()) + }); + + VersionInfo { + version, + commit_info, + } +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + + use super::{CommitInfo, VersionInfo}; + + #[test] + fn version_formatting() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: None, + }; + assert_snapshot!(version, @"0.0.0"); + } + + #[test] + fn version_formatting_with_commit_info() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 0, + last_tag: None, + }), + }; + assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); + } + + #[test] + fn version_formatting_with_commits_since_last_tag() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 24, + last_tag: None, + }), + }; + assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); + } +} diff --git a/crates/ty/tests/cli/config_option.rs b/crates/ty/tests/cli/config_option.rs new file mode 100644 index 0000000000000..4732b2823659a --- /dev/null +++ b/crates/ty/tests/cli/config_option.rs @@ -0,0 +1,206 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +#[test] +fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + // Long flag + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Short flag + assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { + let case = CliTest::with_files(vec![ + ( + "ty.toml", + r#" + [terminal] + error-on-warning = true + "#, + ), + ("test.py", r"print(x) # [unresolved-reference]"), + ])?; + + // Exit code of 1 due to the setting in `ty.toml` + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Exit code of 0 because the `ty.toml` setting is overwritten by `--config` + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn cli_config_args_invalid_option() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(1)")?; + assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: TOML parse error at line 1, column 1 + | + 1 | bad-option=true + | ^^^^^^^^^^ + unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `overrides` + + + Usage: ty + + For more information, try '--help'. + "); + + Ok(()) +} + +#[test] +fn config_file_override() -> anyhow::Result<()> { + // Set `error-on-warning` to true in the configuration file + // Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors + let case = CliTest::with_files(vec![ + ("test.py", r"print(x) # [unresolved-reference]"), + ( + "ty-override.toml", + r#" + [terminal] + error-on-warning = true + "#, + ), + ])?; + + // Ensure flag works via CLI arg + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Ensure the flag works via an environment variable + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} diff --git a/crates/ty/tests/cli/exit_code.rs b/crates/ty/tests/cli/exit_code.rs new file mode 100644 index 0000000000000..7c7a93e488561 --- /dev/null +++ b/crates/ty/tests/cli/exit_code.rs @@ -0,0 +1,272 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +#[test] +fn only_warnings() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn only_info() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + reveal_type(1) + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:3:13 + | + 2 | from typing_extensions import reveal_type + 3 | reveal_type(1) + | ^ `Literal[1]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + reveal_type(1) + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:3:13 + | + 2 | from typing_extensions import reveal_type + 3 | reveal_type(1) + | ^ `Literal[1]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("test.py", r"print(x) # [unresolved-reference]"), + ( + "ty.toml", + r#" + [terminal] + error-on-warning = true + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn both_warnings_and_errors() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:2:7 + | + 2 | print(x) # [unresolved-reference] + | ^ + 3 | print(4[1]) # [non-subscriptable] + | + info: rule `unresolved-reference` was selected on the command line + + error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + --> test.py:3:7 + | + 2 | print(x) # [unresolved-reference] + 3 | print(4[1]) # [non-subscriptable] + | ^ + | + info: rule `non-subscriptable` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r###" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "###, + )?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:2:7 + | + 2 | print(x) # [unresolved-reference] + | ^ + 3 | print(4[1]) # [non-subscriptable] + | + info: rule `unresolved-reference` was selected on the command line + + error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + --> test.py:3:7 + | + 2 | print(x) # [unresolved-reference] + 3 | print(4[1]) # [non-subscriptable] + | ^ + | + info: rule `non-subscriptable` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn exit_zero_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:2:7 + | + 2 | print(x) # [unresolved-reference] + | ^ + 3 | print(4[1]) # [non-subscriptable] + | + info: rule `unresolved-reference` was selected on the command line + + error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + --> test.py:3:7 + | + 2 | print(x) # [unresolved-reference] + 3 | print(4[1]) # [non-subscriptable] + | ^ + | + info: rule `non-subscriptable` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} diff --git a/crates/ty/tests/cli/file_selection.rs b/crates/ty/tests/cli/file_selection.rs new file mode 100644 index 0000000000000..4f84e2e7d52ec --- /dev/null +++ b/crates/ty/tests/cli/file_selection.rs @@ -0,0 +1,726 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +/// Test exclude CLI argument functionality +#[test] +fn exclude_argument() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "tests/test_main.py", + r#" + print(another_undefined_var) # error: unresolved-reference + "#, + ), + ( + "temp_file.py", + r#" + print(temp_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // Test that exclude argument is recognized and works + assert_cmd_snapshot!(case.command().arg("--exclude").arg("tests/"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + error[unresolved-reference]: Name `temp_undefined_var` used when not defined + --> temp_file.py:2:7 + | + 2 | print(temp_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Test multiple exclude patterns + assert_cmd_snapshot!(case.command().arg("--exclude").arg("tests/").arg("--exclude").arg("temp_*.py"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Test configuration file include functionality +#[test] +fn configuration_include() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "tests/test_main.py", + r#" + print(another_undefined_var) # error: unresolved-reference + "#, + ), + ( + "other.py", + r#" + print(other_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // Test include via configuration - should only check included files + case.write_file( + "ty.toml", + r#" + [src] + include = ["src"] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Test multiple include patterns via configuration + case.write_file( + "ty.toml", + r#" + [src] + include = ["src", "other.py"] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `other_undefined_var` used when not defined + --> other.py:2:7 + | + 2 | print(other_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Test configuration file exclude functionality +#[test] +fn configuration_exclude() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "tests/test_main.py", + r#" + print(another_undefined_var) # error: unresolved-reference + "#, + ), + ( + "temp_file.py", + r#" + print(temp_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // Test exclude via configuration + case.write_file( + "ty.toml", + r#" + [src] + exclude = ["tests/"] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + error[unresolved-reference]: Name `temp_undefined_var` used when not defined + --> temp_file.py:2:7 + | + 2 | print(temp_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Test multiple exclude patterns via configuration + case.write_file( + "ty.toml", + r#" + [src] + exclude = ["tests/", "temp_*.py"] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Test that exclude takes precedence over include in configuration +#[test] +fn exclude_precedence_over_include() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "src/test_helper.py", + r#" + print(helper_undefined_var) # error: unresolved-reference + "#, + ), + ( + "other.py", + r#" + print(other_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // Include all src files but exclude test files - exclude should win + case.write_file( + "ty.toml", + r#" + [src] + include = ["src"] + exclude = ["**/test_*.py"] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Test that CLI exclude overrides configuration include +#[test] +fn exclude_argument_precedence_include_argument() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "tests/test_main.py", + r#" + print(another_undefined_var) # error: unresolved-reference + "#, + ), + ( + "other.py", + r#" + print(other_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // Configuration includes all files, but CLI excludes tests + case.write_file( + "ty.toml", + r#" + [src] + include = ["src/", "tests/"] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--exclude").arg("tests/"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + Ok(()) +} + +/// Test that default excludes can be removed using negated patterns +#[test] +fn remove_default_exclude() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "dist/generated.py", + r#" + print(another_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // By default, 'dist' directory should be excluded (see default excludes) + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Now override the default exclude by using a negated pattern to re-include 'dist' + case.write_file( + "ty.toml", + r#" + [src] + exclude = ["!**/dist/"] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `another_undefined_var` used when not defined + --> dist/generated.py:2:7 + | + 2 | print(another_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Test that configuration excludes can be removed via CLI negation +#[test] +fn cli_removes_config_exclude() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "build/output.py", + r#" + print(build_undefined_var) # error: unresolved-reference + "#, + ), + ])?; + + // Configuration excludes the build directory + case.write_file( + "ty.toml", + r#" + [src] + exclude = ["build/"] + "#, + )?; + + // Verify that build/ is excluded by configuration + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Now remove the configuration exclude via CLI negation + assert_cmd_snapshot!(case.command().arg("--exclude").arg("!build/"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `build_undefined_var` used when not defined + --> build/output.py:2:7 + | + 2 | print(build_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Test behavior when explicitly checking a path that matches an exclude pattern +#[test] +fn explicit_path_overrides_exclude() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "tests/generated.py", + r#" + print(dist_undefined_var) # error: unresolved-reference + "#, + ), + ( + "dist/other.py", + r#" + print(other_undefined_var) # error: unresolved-reference + "#, + ), + ( + "ty.toml", + r#" + [src] + exclude = ["tests/generated.py"] + "#, + ), + ])?; + + // dist is excluded by default and `tests/generated` is excluded in the project, so only src/main.py should be checked + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `undefined_var` used when not defined + --> src/main.py:2:7 + | + 2 | print(undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Explicitly checking a file in an excluded directory should still check that file + assert_cmd_snapshot!(case.command().arg("tests/generated.py"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `dist_undefined_var` used when not defined + --> tests/generated.py:2:7 + | + 2 | print(dist_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Explicitly checking the entire excluded directory should check all files in it + assert_cmd_snapshot!(case.command().arg("dist/"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `other_undefined_var` used when not defined + --> dist/other.py:2:7 + | + 2 | print(other_undefined_var) # error: unresolved-reference + | ^^^^^^^^^^^^^^^^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn invalid_include_pattern() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "ty.toml", + r#" + [src] + include = [ + "src/**test/" + ] + "#, + ), + ])?; + + // By default, dist/ is excluded, so only src/main.py should be checked + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: error[invalid-glob]: Invalid include pattern + --> ty.toml:4:5 + | + 2 | [src] + 3 | include = [ + 4 | "src/**test/" + | ^^^^^^^^^^^^^ Too many stars at position 5 + 5 | ] + | + "#); + + Ok(()) +} + +#[test] +fn invalid_include_pattern_concise_output() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "ty.toml", + r#" + [src] + include = [ + "src/**test/" + ] + "#, + ), + ])?; + + // By default, dist/ is excluded, so only src/main.py should be checked + assert_cmd_snapshot!(case.command().arg("--output-format").arg("concise"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: error[invalid-glob] ty.toml:4:5: Invalid include pattern: Too many stars at position 5 + "); + + Ok(()) +} + +#[test] +fn invalid_exclude_pattern() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "src/main.py", + r#" + print(undefined_var) # error: unresolved-reference + "#, + ), + ( + "ty.toml", + r#" + [src] + exclude = [ + "../src" + ] + "#, + ), + ])?; + + // By default, dist/ is excluded, so only src/main.py should be checked + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: error[invalid-glob]: Invalid exclude pattern + --> ty.toml:4:5 + | + 2 | [src] + 3 | exclude = [ + 4 | "../src" + | ^^^^^^^^ The parent directory operator (`..`) at position 1 is not allowed + 5 | ] + | + "#); + + Ok(()) +} diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs new file mode 100644 index 0000000000000..14c237a729a41 --- /dev/null +++ b/crates/ty/tests/cli/main.rs @@ -0,0 +1,778 @@ +mod config_option; +mod exit_code; +mod file_selection; +mod python_environment; +mod rule_selection; + +use anyhow::Context as _; +use insta::internals::SettingsBindDropGuard; +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; +use std::{ + fmt::Write, + path::{Path, PathBuf}, + process::Command, +}; +use tempfile::TempDir; + +#[test] +fn test_quiet_output() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", "x: int = 1")?; + + // By default, we emit an "all checks passed" message + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // With `quiet`, the message is not displayed + assert_cmd_snapshot!(case.command().arg("--quiet"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + let case = CliTest::with_file("test.py", "x: int = 'foo'")?; + + // By default, we emit a diagnostic + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int` + --> test.py:1:1 + | + 1 | x: int = 'foo' + | ^ + | + info: rule `invalid-assignment` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + // With `quiet`, the diagnostic is not displayed, just the summary message + assert_cmd_snapshot!(case.command().arg("--quiet"), @r" + success: false + exit_code: 1 + ----- stdout ----- + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn test_run_in_sub_directory() -> anyhow::Result<()> { + let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?; + assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + Ok(()) +} + +#[test] +fn test_include_hidden_files_by_default() -> anyhow::Result<()> { + let case = CliTest::with_files([(".test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> .test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + Ok(()) +} + +#[test] +fn test_respect_ignore_files() -> anyhow::Result<()> { + // First test that the default option works correctly (the file is skipped) + let case = CliTest::with_files([(".ignore", "test.py"), ("test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + WARN No python files found under the given path(s) + "); + + // Test that we can set to false via CLI + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Test that we can set to false via config file + case.write_file("ty.toml", "src.respect-ignore-files = false")?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Ensure CLI takes precedence + case.write_file("ty.toml", "src.respect-ignore-files = true")?; + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + Ok(()) +} + +/// Paths specified on the CLI are relative to the current working directory and not the project root. +/// +/// We test this by adding an extra search path from the CLI to the libs directory when +/// running the CLI from the child directory (using relative paths). +/// +/// Project layout: +/// ``` +/// - libs +/// |- utils.py +/// - child +/// | - test.py +/// - pyproject.toml +/// ``` +/// +/// And the command is run in the `child` directory. +#[test] +fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.11" + "#, + ), + ( + "libs/utils.py", + r#" + def add(a: int, b: int) -> int: + return a + b + "#, + ), + ( + "child/test.py", + r#" + from utils import add + + stat = add(10, 15) + "#, + ), + ])?; + + // Make sure that the CLI fails when the `libs` directory is not in the search path. + assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `utils` + --> test.py:2:6 + | + 2 | from utils import add + | ^^^^^ + 3 | + 4 | stat = add(10, 15) + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Paths specified in a configuration file are relative to the project root. +/// +/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run +/// the CLI from a subdirectory. +/// +/// Project layout: +/// ``` +/// - libs +/// |- utils.py +/// - child +/// | - test.py +/// - pyproject.toml +/// ``` +#[test] +fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.11" + extra-paths = ["libs"] + "#, + ), + ( + "libs/utils.py", + r#" + def add(a: int, b: int) -> int: + return a + b + "#, + ), + ( + "child/test.py", + r#" + from utils import add + + stat = add(10, 15) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn user_configuration() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "project/ty.toml", + r#" + [rules] + division-by-zero = "warn" + "#, + ), + ( + "project/main.py", + r#" + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) + "#, + ), + ])?; + + let config_directory = case.root().join("home/.config"); + let config_env_var = if cfg!(windows) { + "APPDATA" + } else { + "XDG_CONFIG_HOME" + }; + + assert_cmd_snapshot!( + case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), + @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected in the configuration file + + error[unresolved-reference]: Name `prin` used when not defined + --> main.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + // The user-level configuration sets the severity for `unresolved-reference` to warn. + // Changing the level for `division-by-zero` has no effect, because the project-level configuration + // has higher precedence. + case.write_file( + config_directory.join("ty/ty.toml"), + r#" + [rules] + division-by-zero = "error" + unresolved-reference = "warn" + "#, + )?; + + assert_cmd_snapshot!( + case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), + @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected in the configuration file + + warning[unresolved-reference]: Name `prin` used when not defined + --> main.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +#[test] +fn check_specific_paths() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "project/main.py", + r#" + y = 4 / 0 # error: division-by-zero + "#, + ), + ( + "project/tests/test_main.py", + r#" + import does_not_exist # error: unresolved-import + "#, + ), + ( + "project/other.py", + r#" + from main2 import z # error: unresolved-import + + print(z) + "#, + ), + ])?; + + assert_cmd_snapshot!( + case.command(), + @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `main2` + --> project/other.py:2:6 + | + 2 | from main2 import z # error: unresolved-import + | ^^^^^ + 3 | + 4 | print(z) + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> project/tests/test_main.py:2:8 + | + 2 | import does_not_exist # error: unresolved-import + | ^^^^^^^^^^^^^^ + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "### + ); + + // Now check only the `tests` and `other.py` files. + // We should no longer see any diagnostics related to `main.py`. + assert_cmd_snapshot!( + case.command().arg("project/tests").arg("project/other.py"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `main2` + --> project/other.py:2:6 + | + 2 | from main2 import z # error: unresolved-import + | ^^^^^ + 3 | + 4 | print(z) + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> project/tests/test_main.py:2:8 + | + 2 | import does_not_exist # error: unresolved-import + | ^^^^^^^^^^^^^^ + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +#[test] +fn check_non_existing_path() -> anyhow::Result<()> { + let case = CliTest::with_files([])?; + + let mut settings = insta::Settings::clone_current(); + settings.add_filter( + ®ex::escape("The system cannot find the path specified. (os error 3)"), + "No such file or directory (os error 2)", + ); + let _s = settings.bind_to_scope(); + + assert_cmd_snapshot!( + case.command().arg("project/main.py").arg("project/tests"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + error[io]: `/project/main.py`: No such file or directory (os error 2) + + error[io]: `/project/tests`: No such file or directory (os error 2) + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + WARN No python files found under the given path(s) + " + ); + + Ok(()) +} + +#[test] +fn concise_diagnostics() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference] test.py:2:7: Name `x` used when not defined + error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// This tests the diagnostic format for revealed type. +/// +/// This test was introduced because changes were made to +/// how the revealed type diagnostic was constructed and +/// formatted in "verbose" mode. But it required extra +/// logic to ensure the concise version didn't regress on +/// information content. So this test was introduced to +/// capture that. +#[test] +fn concise_revealed_type() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + + x = "hello" + reveal_type(x) + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]` + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +#[test] +fn can_handle_large_binop_expressions() -> anyhow::Result<()> { + let mut content = String::new(); + writeln!( + &mut content, + " + from typing_extensions import reveal_type + total = 1{plus_one_repeated} + reveal_type(total) + ", + plus_one_repeated = " + 1".repeat(2000 - 1) + )?; + + let case = CliTest::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:4:13 + | + 2 | from typing_extensions import reveal_type + 3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1... + 4 | reveal_type(total) + | ^^^^^ `Literal[2000]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +pub(crate) struct CliTest { + _temp_dir: TempDir, + _settings_scope: SettingsBindDropGuard, + project_dir: PathBuf, +} + +impl CliTest { + pub(crate) fn new() -> anyhow::Result { + let temp_dir = TempDir::new()?; + + // Canonicalize the tempdir path because macos uses symlinks for tempdirs + // and that doesn't play well with our snapshot filtering. + // Simplify with dunce because otherwise we get UNC paths on Windows. + let project_dir = dunce::simplified( + &temp_dir + .path() + .canonicalize() + .context("Failed to canonicalize project path")?, + ) + .to_path_buf(); + + let mut settings = insta::Settings::clone_current(); + settings.add_filter(&tempdir_filter(&project_dir), "/"); + settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); + settings.add_filter( + r#"The system cannot find the file specified."#, + "No such file or directory", + ); + + let settings_scope = settings.bind_to_scope(); + + Ok(Self { + project_dir, + _temp_dir: temp_dir, + _settings_scope: settings_scope, + }) + } + + pub(crate) fn with_files<'a>( + files: impl IntoIterator, + ) -> anyhow::Result { + let case = Self::new()?; + case.write_files(files)?; + Ok(case) + } + + pub(crate) fn with_file(path: impl AsRef, content: &str) -> anyhow::Result { + let case = Self::new()?; + case.write_file(path, content)?; + Ok(case) + } + + pub(crate) fn write_files<'a>( + &self, + files: impl IntoIterator, + ) -> anyhow::Result<()> { + for (path, content) in files { + self.write_file(path, content)?; + } + + Ok(()) + } + + fn ensure_parent_directory(path: &Path) -> anyhow::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory `{}`", parent.display()))?; + } + Ok(()) + } + + pub(crate) fn write_file(&self, path: impl AsRef, content: &str) -> anyhow::Result<()> { + let path = path.as_ref(); + let path = self.project_dir.join(path); + + Self::ensure_parent_directory(&path)?; + + std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content)) + .with_context(|| format!("Failed to write file `{path}`", path = path.display()))?; + + Ok(()) + } + + #[cfg(unix)] + pub(crate) fn write_symlink( + &self, + original: impl AsRef, + link: impl AsRef, + ) -> anyhow::Result<()> { + let link = link.as_ref(); + let link = self.project_dir.join(link); + + let original = original.as_ref(); + let original = self.project_dir.join(original); + + Self::ensure_parent_directory(&link)?; + + std::os::unix::fs::symlink(original, &link) + .with_context(|| format!("Failed to write symlink `{link}`", link = link.display()))?; + + Ok(()) + } + + pub(crate) fn root(&self) -> &Path { + &self.project_dir + } + + pub(crate) fn command(&self) -> Command { + let mut command = Command::new(get_cargo_bin("ty")); + command.current_dir(&self.project_dir).arg("check"); + + // Unset all environment variables because they can affect test behavior. + command.env_clear(); + + command + } +} + +fn tempdir_filter(path: &Path) -> String { + format!(r"{}\\?/?", regex::escape(path.to_str().unwrap())) +} diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs new file mode 100644 index 0000000000000..a84269d666cb6 --- /dev/null +++ b/crates/ty/tests/cli/python_environment.rs @@ -0,0 +1,1219 @@ +use insta_cmd::assert_cmd_snapshot; +use ruff_python_ast::PythonVersion; + +use crate::CliTest; + +/// Specifying an option on the CLI should take precedence over the same setting in the +/// project's configuration. Here, this is tested for the Python version. +#[test] +fn config_override_python_version() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.11" + "#, + ), + ( + "test.py", + r#" + import sys + + # Access `sys.last_exc` that was only added in Python 3.12 + print(sys.last_exc) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-attribute]: Type `` has no attribute `last_exc` + --> test.py:5:7 + | + 4 | # Access `sys.last_exc` that was only added in Python 3.12 + 5 | print(sys.last_exc) + | ^^^^^^^^^^^^ + | + info: rule `unresolved-attribute` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Same as above, but for the Python platform. +#[test] +fn config_override_python_platform() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-platform = "linux" + "#, + ), + ( + "test.py", + r#" + import sys + from typing_extensions import reveal_type + + reveal_type(sys.platform) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:5:13 + | + 3 | from typing_extensions import reveal_type + 4 | + 5 | reveal_type(sys.platform) + | ^^^^^^^^^^^^ `Literal["linux"]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:5:13 + | + 3 | from typing_extensions import reveal_type + 4 | + 5 | reveal_type(sys.platform) + | ^^^^^^^^^^^^ `LiteralString` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.8" + "#, + ), + ( + "test.py", + r#" + aiter + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:2:1 + | + 2 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types + --> pyproject.toml:3:18 + | + 2 | [tool.ty.environment] + 3 | python-version = "3.8" + | ^^^^^ Python 3.8 assumed due to this configuration setting + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:2:1 + | + 2 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.9 was assumed when resolving types because it was specified on the command line + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// This tests that, even if no Python *version* has been specified on the CLI or in a config file, +/// ty is still able to infer the Python version from a `--python` argument on the CLI, +/// *even if* the `--python` argument points to a system installation. +/// +/// We currently cannot infer the Python version from a system installation on Windows: +/// on Windows, we can only infer the Python version from a virtual environment. +/// This is because we use the layout of the Python installation to infer the Python version: +/// on Unix, the `site-packages` directory of an installation will be located at +/// `/lib/pythonX.Y/site-packages`. On Windows, however, the `site-packages` +/// directory will be located at `/Lib/site-packages`, which doesn't give us the +/// same information. +#[cfg(not(windows))] +#[test] +fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { + let cpython_case = CliTest::with_files([ + ("pythons/Python3.8/bin/python", ""), + ("pythons/Python3.8/lib/python3.8/site-packages/foo.py", ""), + ("test.py", "aiter"), + ])?; + + assert_cmd_snapshot!(cpython_case.command().arg("--python").arg("pythons/Python3.8/bin/python"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:1:1 + | + 1 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types because of the layout of your Python installation + info: The primary `site-packages` directory of your installation was found at `lib/python3.8/site-packages/` + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + let pypy_case = CliTest::with_files([ + ("pythons/pypy3.8/bin/python", ""), + ("pythons/pypy3.8/lib/pypy3.8/site-packages/foo.py", ""), + ("test.py", "aiter"), + ])?; + + assert_cmd_snapshot!(pypy_case.command().arg("--python").arg("pythons/pypy3.8/bin/python"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:1:1 + | + 1 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types because of the layout of your Python installation + info: The primary `site-packages` directory of your installation was found at `lib/pypy3.8/site-packages/` + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + let free_threaded_case = CliTest::with_files([ + ("pythons/Python3.13t/bin/python", ""), + ( + "pythons/Python3.13t/lib/python3.13t/site-packages/foo.py", + "", + ), + ("test.py", "import string.templatelib"), + ])?; + + assert_cmd_snapshot!(free_threaded_case.command().arg("--python").arg("pythons/Python3.13t/bin/python"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `string.templatelib` + --> test.py:1:8 + | + 1 | import string.templatelib + | ^^^^^^^^^^^^^^^^^^ + | + info: The stdlib module `string.templatelib` is only available on Python 3.14+ + info: Python 3.13 was assumed when resolving modules because of the layout of your Python installation + info: The primary `site-packages` directory of your installation was found at `lib/python3.13t/site-packages/` + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// On Unix systems, it's common for a Python installation at `.venv/bin/python` to only be a symlink +/// to a system Python installation. We must be careful not to resolve the symlink too soon! +/// If we do, we will incorrectly add the system installation's `site-packages` as a search path, +/// when we should be adding the virtual environment's `site-packages` directory as a search path instead. +#[cfg(unix)] +#[test] +fn python_argument_points_to_symlinked_executable() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "system-installation/lib/python3.13/site-packages/foo.py", + "", + ), + ("system-installation/bin/python", ""), + ( + "strange-venv-location/lib/python3.13/site-packages/bar.py", + "", + ), + ( + "test.py", + "\ +import foo +import bar", + ), + ])?; + + case.write_symlink( + "system-installation/bin/python", + "strange-venv-location/bin/python", + )?; + + assert_cmd_snapshot!(case.command().arg("--python").arg("strange-venv-location/bin/python"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `foo` + --> test.py:1:8 + | + 1 | import foo + | ^^^ + 2 | import bar + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "venv" + "#, + ), + ( + "venv/pyvenv.cfg", + r#" + version = 3.8 + home = foo/bar/bin + "#, + ), + if cfg!(target_os = "windows") { + ("foo/bar/bin/python.exe", "") + } else { + ("foo/bar/bin/python", "") + }, + if cfg!(target_os = "windows") { + ("venv/Lib/site-packages/foo.py", "") + } else { + ("venv/lib/python3.8/site-packages/foo.py", "") + }, + ("test.py", "aiter"), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:1:1 + | + 1 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types because of your virtual environment + --> venv/pyvenv.cfg:2:11 + | + 2 | version = 3.8 + | ^^^ Python version inferred from virtual environment metadata file + 3 | home = foo/bar/bin + | + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "venv" + "#, + ), + ( + "venv/pyvenv.cfg", + r#"home = foo/bar/bin + + + version = 3.8"#, + ), + if cfg!(target_os = "windows") { + ("foo/bar/bin/python.exe", "") + } else { + ("foo/bar/bin/python", "") + }, + if cfg!(target_os = "windows") { + ("venv/Lib/site-packages/foo.py", "") + } else { + ("venv/lib/python3.8/site-packages/foo.py", "") + }, + ("test.py", "aiter"), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:1:1 + | + 1 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types because of your virtual environment + --> venv/pyvenv.cfg:4:23 + | + 4 | version = 3.8 + | ^^^ Python version inferred from virtual environment metadata file + | + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [project] + requires-python = ">=3.8" + "#, + ), + ( + "test.py", + r#" + match object(): + case int(): + pass + case _: + pass + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:2:1 + | + 2 | match object(): + | ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10) + 3 | case int(): + 4 | pass + | + info: Python 3.8 was assumed when parsing syntax + --> pyproject.toml:3:19 + | + 2 | [project] + 3 | requires-python = ">=3.8" + | ^^^^^^^ Python 3.8 assumed due to this configuration setting + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:2:1 + | + 2 | match object(): + | ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + 3 | case int(): + 4 | pass + | + info: Python 3.9 was assumed when parsing syntax because it was specified on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn python_cli_argument_virtual_environment() -> anyhow::Result<()> { + let path_to_executable = if cfg!(windows) { + "my-venv/Scripts/python.exe" + } else { + "my-venv/bin/python" + }; + + let other_venv_path = "my-venv/foo/some_other_file.txt"; + + let case = CliTest::with_files([ + ("test.py", ""), + ( + if cfg!(windows) { + "my-venv/Lib/site-packages/foo.py" + } else { + "my-venv/lib/python3.13/site-packages/foo.py" + }, + "", + ), + (path_to_executable, ""), + (other_venv_path, ""), + ])?; + + // Passing a path to the installation works + assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // And so does passing a path to the executable inside the installation + assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // But random other paths inside the installation are rejected + assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid `--python` argument `/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk + "); + + // And so are paths that do not exist on disk + assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid `--python` argument `/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk + Cause: No such file or directory (os error 2) + "); + + Ok(()) +} + +#[test] +fn python_cli_argument_system_installation() -> anyhow::Result<()> { + let path_to_executable = if cfg!(windows) { + "Python3.11/python.exe" + } else { + "Python3.11/bin/python" + }; + + let case = CliTest::with_files([ + ("test.py", ""), + ( + if cfg!(windows) { + "Python3.11/Lib/site-packages/foo.py" + } else { + "Python3.11/lib/python3.11/site-packages/foo.py" + }, + "", + ), + (path_to_executable, ""), + ])?; + + // Passing a path to the installation works + assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // And so does passing a path to the executable inside the installation + assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn config_file_broken_python_setting() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [project] + name = "test" + version = "0.1.0" + description = "Some description" + readme = "README.md" + requires-python = ">=3.13" + dependencies = [] + + [tool.ty.environment] + python = "not-a-directory-or-executable" + "#, + ), + ("test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid `environment.python` setting + + --> Invalid setting in configuration file `/pyproject.toml` + | + 9 | + 10 | [tool.ty.environment] + 11 | python = "not-a-directory-or-executable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk + | + + Cause: No such file or directory (os error 2) + "#); + + Ok(()) +} + +#[test] +fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "directory-but-no-site-packages" + "#, + ), + ("directory-but-no-site-packages/lib/foo.py", ""), + ("test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Failed to discover the site-packages directory + Cause: Invalid `environment.python` setting + + --> Invalid setting in configuration file `/pyproject.toml` + | + 1 | + 2 | [tool.ty.environment] + 3 | python = "directory-but-no-site-packages" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable + | + "#); + + Ok(()) +} + +// This error message is never emitted on Windows, because Windows installations have simpler layouts +#[cfg(not(windows))] +#[test] +fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "directory-but-no-site-packages" + "#, + ), + ("directory-but-no-site-packages/foo.py", ""), + ("test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Failed to discover the site-packages directory + Cause: Failed to iterate over the contents of the `lib` directory of the Python installation + + --> Invalid setting in configuration file `/pyproject.toml` + | + 1 | + 2 | [tool.ty.environment] + 3 | python = "directory-but-no-site-packages" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + + Cause: No such file or directory (os error 2) + "#); + + Ok(()) +} + +#[test] +fn defaults_to_a_new_python_version() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "ty.toml", + &*format!( + r#" + [environment] + python-version = "{}" + python-platform = "linux" + "#, + PythonVersion::default() + ), + ), + ( + "main.py", + r#" + import os + + os.grantpt(1) # only available on unix, Python 3.13 or newer + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-attribute]: Type `` has no attribute `grantpt` + --> main.py:4:1 + | + 2 | import os + 3 | + 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer + | ^^^^^^^^^^ + | + info: rule `unresolved-attribute` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Use default (which should be latest supported) + let case = CliTest::with_files([ + ( + "ty.toml", + r#" + [environment] + python-platform = "linux" + "#, + ), + ( + "main.py", + r#" + import os + + os.grantpt(1) # only available on unix, Python 3.13 or newer + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// The `site-packages` directory is used by ty for external import. +/// Ty does the following checks to discover the `site-packages` directory in the order: +/// 1) If `VIRTUAL_ENV` environment variable is set +/// 2) If `CONDA_PREFIX` environment variable is set +/// 3) If a `.venv` directory exists at the project root +/// +/// This test is aiming at validating the logic around `CONDA_PREFIX`. +/// +/// A conda-like environment file structure is used +/// We test by first not setting the `CONDA_PREFIX` and expect a fail. +/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass. +/// +/// ├── project +/// │ └── test.py +/// └── conda-env +/// └── lib +/// └── python3.13 +/// └── site-packages +/// └── package1 +/// └── __init__.py +/// +/// test.py imports package1 +/// And the command is run in the `child` directory. +#[test] +fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> { + let conda_package1_path = if cfg!(windows) { + "conda-env/Lib/site-packages/package1/__init__.py" + } else { + "conda-env/lib/python3.13/site-packages/package1/__init__.py" + }; + + let case = CliTest::with_files([ + ( + "project/test.py", + r#" + import package1 + "#, + ), + ( + conda_package1_path, + r#" + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `package1` + --> test.py:2:8 + | + 2 | import package1 + | ^^^^^^^^ + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // do command : CONDA_PREFIX=/conda_env + assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn src_root_deprecation_warning() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.src] + root = "./src" + "#, + ), + ("src/test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. + --> pyproject.toml:3:8 + | + 2 | [tool.ty.src] + 3 | root = "./src" + | ^^^^^^^ + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +#[test] +fn src_root_deprecation_warning_with_environment_root() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.src] + root = "./src" + + [tool.ty.environment] + root = ["./app"] + "#, + ), + ("app/test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. + --> pyproject.toml:3:8 + | + 2 | [tool.ty.src] + 3 | root = "./src" + | ^^^^^^^ + 4 | + 5 | [tool.ty.environment] + | + info: The `src.root` setting was ignored in favor of the `environment.root` setting + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +#[test] +fn environment_root_takes_precedence_over_src_root() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.src] + root = "./src" + + [tool.ty.environment] + root = ["./app"] + "#, + ), + ("src/test.py", "import my_module"), + ( + "app/my_module.py", + "# This module exists in app/ but not src/", + ), + ])?; + + // The test should pass because environment.root points to ./app where my_module.py exists + // If src.root took precedence, it would fail because my_module.py doesn't exist in ./src + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. + --> pyproject.toml:3:8 + | + 2 | [tool.ty.src] + 3 | root = "./src" + | ^^^^^^^ + 4 | + 5 | [tool.ty.environment] + | + info: The `src.root` setting was ignored in favor of the `environment.root` setting + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +#[test] +fn default_root_src_layout() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("src/foo.py", "foo = 10"), + ("bar.py", "bar = 20"), + ( + "src/main.py", + r#" + from foo import foo + from bar import bar + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn default_root_project_name_folder() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [project] + name = "psycopg" + "#, + ), + ("psycopg/psycopg/foo.py", "foo = 10"), + ("bar.py", "bar = 20"), + ( + "psycopg/psycopg/main.py", + r#" + from psycopg.foo import foo + from bar import bar + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn default_root_flat_layout() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("app/foo.py", "foo = 10"), + ("bar.py", "bar = 20"), + ( + "app/main.py", + r#" + from app.foo import foo + from bar import bar + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn default_root_tests_folder() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("src/foo.py", "foo = 10"), + ("tests/bar.py", "bar = 20"), + ( + "tests/test_bar.py", + r#" + from foo import foo + from bar import bar + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// If `tests/__init__.py` is present, it is considered a package and `tests` is not added to `sys.path`. +#[test] +fn default_root_tests_package() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("src/foo.py", "foo = 10"), + ("tests/__init__.py", ""), + ("tests/bar.py", "bar = 20"), + ( + "tests/test_bar.py", + r#" + from foo import foo + from bar import bar # expected unresolved import + + print(f"{foo} {bar}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `bar` + --> tests/test_bar.py:3:6 + | + 2 | from foo import foo + 3 | from bar import bar # expected unresolved import + | ^^^ + 4 | + 5 | print(f"{foo} {bar}") + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs new file mode 100644 index 0000000000000..7600a0d698231 --- /dev/null +++ b/crates/ty/tests/cli/rule_selection.rs @@ -0,0 +1,902 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +/// The rule severity can be changed in the configuration file +#[test] +fn configuration_rule_severity() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) # unresolved-reference + "#, + )?; + + // Assert that there's an `unresolved-reference` diagnostic (error). + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `prin` used when not defined + --> test.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) # unresolved-reference + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + case.write_file( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "warn" # promote to warn + unresolved-reference = "ignore" + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected in the configuration file + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` +#[test] +fn cli_rule_severity() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + import does_not_exit + + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) # unresolved-reference + "#, + )?; + + // Assert that there's an `unresolved-reference` diagnostic (error) + // and an unresolved-import (error) diagnostic by default. + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `does_not_exit` + --> test.py:2:8 + | + 2 | import does_not_exit + | ^^^^^^^^^^^^^ + 3 | + 4 | y = 4 / 0 + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-reference]: Name `prin` used when not defined + --> test.py:9:1 + | + 7 | x = a + 8 | + 9 | prin(x) # unresolved-reference + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + assert_cmd_snapshot!( + case + .command() + .arg("--ignore") + .arg("unresolved-reference") + .arg("--warn") + .arg("division-by-zero") + .arg("--warn") + .arg("unresolved-import"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-import]: Cannot resolve imported module `does_not_exit` + --> test.py:2:8 + | + 2 | import does_not_exit + | ^^^^^^^^^^^^^ + 3 | + 4 | y = 4 / 0 + | + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` was selected on the command line + + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:4:5 + | + 2 | import does_not_exit + 3 | + 4 | y = 4 / 0 + | ^^^^^ + 5 | + 6 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected on the command line + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and +/// values specified last override previous severities. +#[test] +fn cli_rule_severity_precedence() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) # unresolved-reference + "#, + )?; + + // Assert that there's a `unresolved-reference` diagnostic (error) by default. + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `prin` used when not defined + --> test.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) # unresolved-reference + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + assert_cmd_snapshot!( + case + .command() + .arg("--warn") + .arg("unresolved-reference") + .arg("--warn") + .arg("division-by-zero") + .arg("--ignore") + .arg("unresolved-reference"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +/// ty warns about unknown rules specified in a configuration file +#[test] +fn configuration_unknown_rules() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zer = "warn" # incorrect rule name + "#, + ), + ("test.py", "print(10)"), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + warning[unknown-rule]: Unknown lint rule `division-by-zer` + --> pyproject.toml:3:1 + | + 2 | [tool.ty.rules] + 3 | division-by-zer = "warn" # incorrect rule name + | ^^^^^^^^^^^^^^^ + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// ty warns about unknown rules specified in a CLI argument +#[test] +fn cli_unknown_rules() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", "print(10)")?; + + assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unknown-rule]: Unknown lint rule `division-by-zer` + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Basic override functionality: override rules for specific files +#[test] +fn overrides_basic() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + unresolved-reference = "error" + + [[tool.ty.overrides]] + include = ["tests/**"] + + [tool.ty.overrides.rules] + division-by-zero = "warn" + unresolved-reference = "ignore" + "#, + ), + ( + "main.py", + r#" + y = 4 / 0 # division-by-zero: error (global) + x = 1 + prin(x) # unresolved-reference: error (global) + "#, + ), + ( + "tests/test_main.py", + r#" + y = 4 / 0 # division-by-zero: warn (override) + x = 1 + prin(x) # unresolved-reference: ignore (override) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: error (global) + | ^^^^^ + 3 | x = 1 + 4 | prin(x) # unresolved-reference: error (global) + | + info: rule `division-by-zero` was selected in the configuration file + + error[unresolved-reference]: Name `prin` used when not defined + --> main.py:4:1 + | + 2 | y = 4 / 0 # division-by-zero: error (global) + 3 | x = 1 + 4 | prin(x) # unresolved-reference: error (global) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> tests/test_main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: warn (override) + | ^^^^^ + 3 | x = 1 + 4 | prin(x) # unresolved-reference: ignore (override) + | + info: rule `division-by-zero` was selected in the configuration file + + Found 3 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + Ok(()) +} + +/// Multiple overrides: later overrides take precedence +#[test] +fn overrides_precedence() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + # First override: all test files + [[tool.ty.overrides]] + include = ["tests/**"] + [tool.ty.overrides.rules] + division-by-zero = "warn" + + # Second override: specific test file (takes precedence) + [[tool.ty.overrides]] + include = ["tests/important.py"] + [tool.ty.overrides.rules] + division-by-zero = "ignore" + "#, + ), + ( + "tests/test_main.py", + r#" + y = 4 / 0 # division-by-zero: warn (first override) + "#, + ), + ( + "tests/important.py", + r#" + y = 4 / 0 # division-by-zero: ignore (second override) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> tests/test_main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: warn (first override) + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Override with exclude patterns +#[test] +fn overrides_exclude() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + include = ["tests/**"] + exclude = ["tests/important.py"] + [tool.ty.overrides.rules] + division-by-zero = "warn" + "#, + ), + ( + "tests/test_main.py", + r#" + y = 4 / 0 # division-by-zero: warn (override applies) + "#, + ), + ( + "tests/important.py", + r#" + y = 4 / 0 # division-by-zero: error (override excluded) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> tests/important.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: error (override excluded) + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> tests/test_main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: warn (override applies) + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Override without rules inherits global rules +#[test] +fn overrides_inherit_global() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "warn" + unresolved-reference = "error" + + [[tool.ty.overrides]] + include = ["tests/**"] + + [tool.ty.overrides.rules] + # Override only division-by-zero, unresolved-reference should inherit from global + division-by-zero = "ignore" + "#, + ), + ( + "main.py", + r#" + y = 4 / 0 # division-by-zero: warn (global) + prin(y) # unresolved-reference: error (global) + "#, + ), + ( + "tests/test_main.py", + r#" + y = 4 / 0 # division-by-zero: ignore (overridden) + prin(y) # unresolved-reference: error (inherited from global) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 # division-by-zero: warn (global) + | ^^^^^ + 3 | prin(y) # unresolved-reference: error (global) + | + info: rule `division-by-zero` was selected in the configuration file + + error[unresolved-reference]: Name `prin` used when not defined + --> main.py:3:1 + | + 2 | y = 4 / 0 # division-by-zero: warn (global) + 3 | prin(y) # unresolved-reference: error (global) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + error[unresolved-reference]: Name `prin` used when not defined + --> tests/test_main.py:3:1 + | + 2 | y = 4 / 0 # division-by-zero: ignore (overridden) + 3 | prin(y) # unresolved-reference: error (inherited from global) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + Found 3 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// ty warns about invalid glob patterns in override include patterns +#[test] +fn overrides_invalid_include_glob() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + include = ["tests/[invalid"] # Invalid glob: unclosed bracket + [tool.ty.overrides.rules] + division-by-zero = "warn" + "#, + ), + ( + "test.py", + r#" + y = 4 / 0 + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: error[invalid-glob]: Invalid include pattern + --> pyproject.toml:6:12 + | + 5 | [[tool.ty.overrides]] + 6 | include = ["tests/[invalid"] # Invalid glob: unclosed bracket + | ^^^^^^^^^^^^^^^^ unclosed character class; missing ']' + 7 | [tool.ty.overrides.rules] + 8 | division-by-zero = "warn" + | + "#); + + Ok(()) +} + +/// ty warns about invalid glob patterns in override exclude patterns +#[test] +fn overrides_invalid_exclude_glob() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + include = ["tests/**"] + exclude = ["***/invalid"] # Invalid glob: triple asterisk + [tool.ty.overrides.rules] + division-by-zero = "warn" + "#, + ), + ( + "test.py", + r#" + y = 4 / 0 + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: error[invalid-glob]: Invalid exclude pattern + --> pyproject.toml:7:12 + | + 5 | [[tool.ty.overrides]] + 6 | include = ["tests/**"] + 7 | exclude = ["***/invalid"] # Invalid glob: triple asterisk + | ^^^^^^^^^^^^^ Too many stars at position 1 + 8 | [tool.ty.overrides.rules] + 9 | division-by-zero = "warn" + | + "#); + + Ok(()) +} + +/// ty warns when an overrides section has neither include nor exclude +#[test] +fn overrides_missing_include_exclude() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + # Missing both include and exclude - should warn + [tool.ty.overrides.rules] + division-by-zero = "warn" + "#, + ), + ( + "test.py", + r#" + y = 4 / 0 + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + warning[unnecessary-overrides-section]: Unnecessary `overrides` section + --> pyproject.toml:5:1 + | + 3 | division-by-zero = "error" + 4 | + 5 | [[tool.ty.overrides]] + | ^^^^^^^^^^^^^^^^^^^^^ This overrides section applies to all files + 6 | # Missing both include and exclude - should warn + 7 | [tool.ty.overrides.rules] + | + info: It has no `include` or `exclude` option restricting the files + info: Restrict the files by adding a pattern to `include` or `exclude`... + info: or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files + + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// ty warns when an overrides section has an empty include array +#[test] +fn overrides_empty_include() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + include = [] # Empty include - won't match any files + [tool.ty.overrides.rules] + division-by-zero = "warn" + "#, + ), + ( + "test.py", + r#" + y = 4 / 0 + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + warning[empty-include]: Empty include matches no files + --> pyproject.toml:6:11 + | + 5 | [[tool.ty.overrides]] + 6 | include = [] # Empty include - won't match any files + | ^^ This `include` list is empty + 7 | [tool.ty.overrides.rules] + 8 | division-by-zero = "warn" + | + info: Remove the `include` option to match all files or add a pattern to match specific files + + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// ty warns when an overrides section has no actual overrides +#[test] +fn overrides_no_actual_overrides() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + include = ["*.py"] # Has patterns but no rule overrides + # Missing [tool.ty.overrides.rules] section entirely + "#, + ), + ( + "test.py", + r#" + y = 4 / 0 + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + warning[useless-overrides-section]: Useless `overrides` section + --> pyproject.toml:5:1 + | + 3 | division-by-zero = "error" + 4 | + 5 | / [[tool.ty.overrides]] + 6 | | include = ["*.py"] # Has patterns but no rule overrides + | |__________________^ This overrides section configures no rules + 7 | # Missing [tool.ty.overrides.rules] section entirely + | + info: It has no `rules` table + info: Add a `[overrides.rules]` table... + info: or remove the `[[overrides]]` section if there's nothing to override + + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// ty warns about unknown rules specified in an overrides section +#[test] +fn overrides_unknown_rules() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "error" + + [[tool.ty.overrides]] + include = ["tests/**"] + + [tool.ty.overrides.rules] + division-by-zero = "warn" + division-by-zer = "error" # incorrect rule name + "#, + ), + ( + "main.py", + r#" + y = 4 / 0 + "#, + ), + ( + "tests/test_main.py", + r#" + y = 4 / 0 + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + warning[unknown-rule]: Unknown lint rule `division-by-zer` + --> pyproject.toml:10:1 + | + 8 | [tool.ty.overrides.rules] + 9 | division-by-zero = "warn" + 10 | division-by-zer = "error" # incorrect rule name + | ^^^^^^^^^^^^^^^ + | + + error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> tests/test_main.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + | + info: rule `division-by-zero` was selected in the configuration file + + Found 3 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} diff --git a/crates/red_knot/tests/file_watching.rs b/crates/ty/tests/file_watching.rs similarity index 88% rename from crates/red_knot/tests/file_watching.rs rename to crates/ty/tests/file_watching.rs index acdb7071c7fde..545c824b1a910 100644 --- a/crates/red_knot/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -2,20 +2,20 @@ use std::collections::HashSet; use std::io::Write; use std::time::{Duration, Instant}; -use anyhow::{anyhow, Context}; -use red_knot_project::metadata::options::{EnvironmentOptions, Options}; -use red_knot_project::metadata::pyproject::{PyProject, Tool}; -use red_knot_project::metadata::value::{RangedValue, RelativePathBuf}; -use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher}; -use red_knot_project::{Db, ProjectDatabase, ProjectMetadata}; -use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform}; -use ruff_db::files::{system_path_to_file, File, FileError}; +use anyhow::{Context, anyhow}; +use ruff_db::Db as _; +use ruff_db::files::{File, FileError, system_path_to_file}; use ruff_db::source::source_text; use ruff_db::system::{ - file_time_now, OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard, + OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard, file_time_now, }; -use ruff_db::{Db as _, Upcast}; use ruff_python_ast::PythonVersion; +use ty_project::metadata::options::{EnvironmentOptions, Options, ProjectOptionsOverrides}; +use ty_project::metadata::pyproject::{PyProject, Tool}; +use ty_project::metadata::value::{RangedValue, RelativePathBuf}; +use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher}; +use ty_project::{Db, ProjectDatabase, ProjectMetadata}; +use ty_python_semantic::{ModuleName, PythonPlatform, resolve_module}; struct TestCase { db: ProjectDatabase, @@ -164,8 +164,12 @@ impl TestCase { Ok(all_events) } - fn apply_changes(&mut self, changes: Vec) { - self.db.apply_changes(changes, None); + fn apply_changes( + &mut self, + changes: Vec, + project_options_overrides: Option<&ProjectOptionsOverrides>, + ) { + self.db.apply_changes(changes, project_options_overrides); } fn update_options(&mut self, options: Options) -> anyhow::Result<()> { @@ -173,16 +177,14 @@ impl TestCase { self.project_path("pyproject.toml").as_std_path(), toml::to_string(&PyProject { project: None, - tool: Some(Tool { - knot: Some(options), - }), + tool: Some(Tool { ty: Some(options) }), }) .context("Failed to serialize options")?, ) .context("Failed to write configuration")?; let changes = self.take_watch_changes(event_for_file("pyproject.toml")); - self.apply_changes(changes); + self.apply_changes(changes, None); if let Some(watcher) = &mut self.watcher { watcher.update(&self.db); @@ -382,9 +384,7 @@ where project_path.join("pyproject.toml").as_std_path(), toml::to_string(&PyProject { project: None, - tool: Some(Tool { - knot: Some(options), - }), + tool: Some(Tool { ty: Some(options) }), }) .context("Failed to serialize options")?, ) @@ -396,16 +396,18 @@ where let mut project = ProjectMetadata::discover(&project_path, &system)?; project.apply_configuration_files(&system)?; - let program_settings = project.to_program_settings(&system); - - for path in program_settings - .search_paths - .extra_paths - .iter() - .chain(program_settings.search_paths.custom_typeshed.as_ref()) - { - std::fs::create_dir_all(path.as_std_path()) - .with_context(|| format!("Failed to create search path `{path}`"))?; + // We need a chance to create the directories here. + if let Some(environment) = project.options().environment.as_ref() { + for path in environment + .extra_paths + .as_deref() + .unwrap_or_default() + .iter() + .chain(environment.typeshed.as_ref()) + { + std::fs::create_dir_all(path.absolute(&project_path, &system).as_std_path()) + .with_context(|| format!("Failed to create search path `{path}`"))?; + } } let mut db = ProjectDatabase::new(project, system)?; @@ -480,7 +482,7 @@ fn new_file() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); let foo = case.system_file(&foo_path).expect("foo.py to exist."); @@ -503,7 +505,7 @@ fn new_ignored_file() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert!(case.system_file(&foo_path).is_ok()); case.assert_indexed_project_files([bar_file]); @@ -539,7 +541,7 @@ fn new_non_project_file() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("black.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert!(case.system_file(&black_path).is_ok()); @@ -580,7 +582,7 @@ fn new_files_with_explicit_included_paths() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("test2.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); let sub_a_file = case.system_file(&sub_a_path).expect("sub/a.py to exist"); @@ -625,7 +627,7 @@ fn new_file_in_included_out_of_project_directory() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("script2.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); let src_a_file = case.system_file(&src_a).unwrap(); let outside_b_file = case.system_file(&outside_b_path).unwrap(); @@ -652,7 +654,7 @@ fn changed_file() -> anyhow::Result<()> { assert!(!changes.is_empty()); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')"); case.assert_indexed_project_files([foo]); @@ -675,7 +677,7 @@ fn deleted_file() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert!(!foo.exists(case.db())); case.assert_indexed_project_files([]); @@ -707,7 +709,7 @@ fn move_file_to_trash() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert!(!foo.exists(case.db())); case.assert_indexed_project_files([]); @@ -734,7 +736,7 @@ fn move_file_to_project() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); let foo_in_project = case.system_file(&foo_in_project)?; @@ -759,7 +761,7 @@ fn rename_file() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("bar.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert!(!foo.exists(case.db())); @@ -786,10 +788,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> { .with_context(|| "Failed to create __init__.py")?; std::fs::write(a_original_path.as_std_path(), "").with_context(|| "Failed to create a.py")?; - let sub_a_module = resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap(), - ); + let sub_a_module = resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()); assert_eq!(sub_a_module, None); case.assert_indexed_project_files([bar]); @@ -800,7 +799,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("sub")); - case.apply_changes(changes); + case.apply_changes(changes, None); let init_file = case .system_file(sub_new_path.join("__init__.py")) @@ -810,11 +809,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> { .expect("a.py to exist"); // `import sub.a` should now resolve - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()); case.assert_indexed_project_files([bar, init_file, a_file]); @@ -830,11 +825,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> { ])?; let bar = case.system_file(case.project_path("bar.py")).unwrap(); - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()); let sub_path = case.project_path("sub"); let init_file = case @@ -853,14 +844,10 @@ fn directory_moved_to_trash() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("sub")); - case.apply_changes(changes); + case.apply_changes(changes, None); // `import sub.a` should no longer resolve - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_none()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()); assert!(!init_file.exists(case.db())); assert!(!a_file.exists(case.db())); @@ -880,16 +867,8 @@ fn directory_renamed() -> anyhow::Result<()> { let bar = case.system_file(case.project_path("bar.py")).unwrap(); - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_some()); - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("foo.baz").unwrap() - ) - .is_none()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none()); let sub_path = case.project_path("sub"); let sub_init = case @@ -910,20 +889,12 @@ fn directory_renamed() -> anyhow::Result<()> { // Linux and windows only emit an event for the newly created root directory, but not for every new component. let changes = case.stop_watch(event_for_file("sub")); - case.apply_changes(changes); + case.apply_changes(changes, None); // `import sub.a` should no longer resolve - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_none()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()); // `import foo.baz` should now resolve - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("foo.baz").unwrap() - ) - .is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some()); // The old paths are no longer tracked assert!(!sub_init.exists(case.db())); @@ -956,11 +927,7 @@ fn directory_deleted() -> anyhow::Result<()> { let bar = case.system_file(case.project_path("bar.py")).unwrap(); - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some()); let sub_path = case.project_path("sub"); @@ -977,14 +944,10 @@ fn directory_deleted() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("sub")); - case.apply_changes(changes); + case.apply_changes(changes, None); // `import sub.a` should no longer resolve - assert!(resolve_module( - case.db().upcast(), - &ModuleName::new_static("sub.a").unwrap() - ) - .is_none()); + assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none()); assert!(!init_file.exists(case.db())); assert!(!a_file.exists(case.db())); @@ -1021,9 +984,9 @@ fn search_path() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("a.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); - assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some()); case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]); Ok(()) @@ -1036,7 +999,7 @@ fn add_search_path() -> anyhow::Result<()> { let site_packages = case.project_path("site_packages"); std::fs::create_dir_all(site_packages.as_std_path())?; - assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_none()); + assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_none()); // Register site-packages as a search path. case.update_options(Options { @@ -1052,9 +1015,9 @@ fn add_search_path() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("a.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); - assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some()); + assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some()); Ok(()) } @@ -1121,7 +1084,7 @@ print(sys.last_exc, os.getegid()) Ok(()) })?; - let diagnostics = case.db.check().context("Failed to check project.")?; + let diagnostics = case.db.check(); assert_eq!(diagnostics.len(), 2); assert_eq!( @@ -1146,7 +1109,7 @@ print(sys.last_exc, os.getegid()) }) .expect("Search path settings to be valid"); - let diagnostics = case.db.check().context("Failed to check project.")?; + let diagnostics = case.db.check(); assert!(diagnostics.is_empty()); Ok(()) @@ -1199,7 +1162,7 @@ fn changed_versions_file() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("VERSIONS")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some()); @@ -1253,7 +1216,7 @@ fn hard_links_in_project() -> anyhow::Result<()> { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')"); @@ -1324,7 +1287,7 @@ fn hard_links_to_target_outside_project() -> anyhow::Result<()> { let changes = case.stop_watch(ChangeEvent::is_changed); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')"); @@ -1363,7 +1326,7 @@ mod unix { let changes = case.stop_watch(event_for_file("foo.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!( foo.permissions(case.db()), @@ -1424,19 +1387,14 @@ mod unix { Ok(()) })?; - let baz = resolve_module( - case.db().upcast(), - &ModuleName::new_static("bar.baz").unwrap(), - ) - .expect("Expected bar.baz to exist in site-packages."); + let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap()) + .expect("Expected bar.baz to exist in site-packages."); let baz_project = case.project_path("bar/baz.py"); + let baz_file = baz.file().unwrap(); + assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ..."); assert_eq!( - source_text(case.db(), baz.file()).as_str(), - "def baz(): ..." - ); - assert_eq!( - baz.file().path(case.db()).as_system_path(), + baz_file.path(case.db()).as_system_path(), Some(&*baz_project) ); @@ -1448,10 +1406,10 @@ mod unix { let changes = case.take_watch_changes(event_for_file("baz.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!( - source_text(case.db(), baz.file()).as_str(), + source_text(case.db(), baz_file).as_str(), "def baz(): print('Version 2')" ); @@ -1461,10 +1419,10 @@ mod unix { let changes = case.stop_watch(event_for_file("baz.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!( - source_text(case.db(), baz.file()).as_str(), + source_text(case.db(), baz_file).as_str(), "def baz(): print('Version 3')" ); @@ -1505,11 +1463,9 @@ mod unix { Ok(()) })?; - let baz = resolve_module( - case.db().upcast(), - &ModuleName::new_static("bar.baz").unwrap(), - ) - .expect("Expected bar.baz to exist in site-packages."); + let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap()) + .expect("Expected bar.baz to exist in site-packages."); + let baz_file = baz.file().unwrap(); let bar_baz = case.project_path("bar/baz.py"); let patched_bar_baz = case.project_path("patched/bar/baz.py"); @@ -1520,11 +1476,8 @@ mod unix { "def baz(): ..." ); - assert_eq!( - source_text(case.db(), baz.file()).as_str(), - "def baz(): ..." - ); - assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz)); + assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ..."); + assert_eq!(baz_file.path(case.db()).as_system_path(), Some(&*bar_baz)); case.assert_indexed_project_files([patched_bar_baz_file]); @@ -1534,7 +1487,7 @@ mod unix { let changes = case.stop_watch(event_for_file("baz.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); // The file watcher is guaranteed to emit one event for the changed file, but it isn't specified // if the event is emitted for the "original" or linked path because both paths are watched. @@ -1546,14 +1499,14 @@ mod unix { // // I further tested how good editor support is for symlinked files and it is not good ;) // * VS Code doesn't update the file content if a file gets changed through a symlink - // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot). + // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as ty). // // That's why I think it's fine to not support this case for now. let patched_baz_text = source_text(case.db(), patched_bar_baz_file); let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')"; - let bar_baz_text = source_text(case.db(), baz.file()); + let bar_baz_text = source_text(case.db(), baz_file); let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')"; assert!( @@ -1615,11 +1568,8 @@ mod unix { Ok(()) })?; - let baz = resolve_module( - case.db().upcast(), - &ModuleName::new_static("bar.baz").unwrap(), - ) - .expect("Expected bar.baz to exist in site-packages."); + let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap()) + .expect("Expected bar.baz to exist in site-packages."); let baz_site_packages_path = case.project_path(".venv/lib/python3.12/site-packages/bar/baz.py"); let baz_site_packages = case.system_file(&baz_site_packages_path).unwrap(); @@ -1636,7 +1586,7 @@ mod unix { "def baz(): ..." ); assert_eq!( - baz.file().path(case.db()).as_system_path(), + baz.file().unwrap().path(case.db()).as_system_path(), Some(&*baz_original) ); @@ -1648,7 +1598,7 @@ mod unix { let changes = case.stop_watch(event_for_file("baz.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); assert_eq!( source_text(case.db(), baz_original_file).as_str(), @@ -1658,7 +1608,7 @@ mod unix { // It would be nice if this is supported but the underlying file system watchers // only emit a single event. For reference // * VS Code doesn't update the file content if a file gets changed through a symlink - // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot). + // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as ty). // We could add support for it by keeping a reverse map from `real_path` to symlinked path but // it doesn't seem worth doing considering that as prominent tools like PyCharm don't support it. // Pyright does support it, thanks to chokidar. @@ -1682,7 +1632,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> { [project] name = "inner" - [tool.knot] + [tool.ty] "#, )?; @@ -1692,7 +1642,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> { [project] name = "outer" - [tool.knot] + [tool.ty] "#, )?; @@ -1705,7 +1655,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> { let changes = case.stop_watch(ChangeEvent::is_deleted); - case.apply_changes(changes); + case.apply_changes(changes, None); // It should now pick up the outer project. assert_eq!(case.db().project().root(case.db()), case.root_path()); @@ -1732,9 +1682,9 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { )?; let config_directory = context.join_root_path("home/.config"); - std::fs::create_dir_all(config_directory.join("knot").as_std_path())?; + std::fs::create_dir_all(config_directory.join("ty").as_std_path())?; std::fs::write( - config_directory.join("knot/knot.toml").as_std_path(), + config_directory.join("ty/ty.toml").as_std_path(), r#" [rules] division-by-zero = "ignore" @@ -1753,10 +1703,7 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { let foo = case .system_file(case.project_path("foo.py")) .expect("foo.py to exist"); - let diagnostics = case - .db() - .check_file(foo) - .context("Failed to check project.")?; + let diagnostics = case.db().check_file(foo); assert!( diagnostics.is_empty(), @@ -1765,21 +1712,84 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { // Enable division-by-zero in the user configuration with warning severity update_file( - case.root_path().join("home/.config/knot/knot.toml"), + case.root_path().join("home/.config/ty/ty.toml"), r#" [rules] division-by-zero = "warn" "#, )?; - let changes = case.stop_watch(event_for_file("knot.toml")); + let changes = case.stop_watch(event_for_file("ty.toml")); + + case.apply_changes(changes, None); + + let diagnostics = case.db().check_file(foo); + + assert!( + diagnostics.len() == 1, + "Expected exactly one diagnostic but got: {diagnostics:#?}" + ); + + Ok(()) +} - case.apply_changes(changes); +#[test] +fn changes_to_config_file_override() -> anyhow::Result<()> { + let mut case = setup(|context: &mut SetupContext| { + std::fs::write( + context.join_project_path("pyproject.toml").as_std_path(), + r#" + [project] + name = "test" + "#, + )?; + + std::fs::write( + context.join_project_path("foo.py").as_std_path(), + "a = 10 / 0", + )?; + + std::fs::write( + context.join_project_path("ty-override.toml").as_std_path(), + r#" + [rules] + division-by-zero = "ignore" + "#, + )?; + + Ok(()) + })?; + + let foo = case + .system_file(case.project_path("foo.py")) + .expect("foo.py to exist"); + let diagnostics = case.db().check_file(foo); + + assert!( + diagnostics.is_empty(), + "Expected no diagnostics but got: {diagnostics:#?}" + ); + + // Enable division-by-zero in the explicitly specified configuration with warning severity + update_file( + case.project_path("ty-override.toml"), + r#" + [rules] + division-by-zero = "warn" + "#, + )?; + + let changes = case.stop_watch(event_for_file("ty-override.toml")); + + case.apply_changes( + changes, + Some(&ProjectOptionsOverrides::new( + Some(case.project_path("ty-override.toml")), + Options::default(), + )), + ); - let diagnostics = case - .db() - .check_file(foo) - .context("Failed to check project.")?; + let diagnostics = case.db().check_file(foo); assert!( diagnostics.len() == 1, @@ -1793,7 +1803,7 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { /// /// This test currently fails on case-insensitive systems because `Files` is case-sensitive /// but the `System::metadata` call isn't. This means that -/// Red Knot considers both `Lib.py` and `lib.py` to exist when only `lib.py` does +/// ty considers both `Lib.py` and `lib.py` to exist when only `lib.py` does /// /// The incoming change events then are no-ops because they don't change either file's /// status nor does it update their last modified time (renaming a file doesn't bump it's @@ -1805,7 +1815,7 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { /// `System` calls should be case sensitive. This would be the most consistent /// but might be hard to pull off. /// -/// What the right solution is also depends on if Red Knot itself should be case +/// What the right solution is also depends on if ty itself should be case /// sensitive or not. E.g. should `include="src"` be case sensitive on all systems /// or only on case-sensitive systems? /// @@ -1851,7 +1861,7 @@ fn rename_files_casing_only() -> anyhow::Result<()> { } let changes = case.stop_watch(event_for_file("Lib.py")); - case.apply_changes(changes); + case.apply_changes(changes, None); // Resolving `lib` should now fail but `Lib` should now succeed assert_eq!( diff --git a/crates/ty_ide/Cargo.toml b/crates/ty_ide/Cargo.toml new file mode 100644 index 0000000000000..f54aa96ae728c --- /dev/null +++ b/crates/ty_ide/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "ty_ide" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +bitflags = { workspace = true } +ruff_db = { workspace = true } +ruff_python_ast = { workspace = true } +ruff_python_parser = { workspace = true } +ruff_python_trivia = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } +ty_python_semantic = { workspace = true } + +regex = { workspace = true } +rustc-hash = { workspace = true } +salsa = { workspace = true } +smallvec = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +ty_vendored = { workspace = true } + +insta = { workspace = true, features = ["filters"] } + +[lints] +workspace = true diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs new file mode 100644 index 0000000000000..f689a04533ea0 --- /dev/null +++ b/crates/ty_ide/src/completion.rs @@ -0,0 +1,2519 @@ +use std::cmp::Ordering; + +use ruff_db::files::File; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_python_ast as ast; +use ruff_python_parser::{Token, TokenAt, TokenKind}; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::{Completion, NameKind, SemanticModel}; + +use crate::Db; +use crate::find_node::covering_node; + +pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec> { + let parsed = parsed_module(db, file).load(db); + + let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else { + return vec![]; + }; + let Some(target) = target_token.ast(&parsed, offset) else { + return vec![]; + }; + + let model = SemanticModel::new(db, file); + let mut completions = match target { + CompletionTargetAst::ObjectDot { expr } => model.attribute_completions(expr), + CompletionTargetAst::ImportFrom { import, name } => model.import_completions(import, name), + CompletionTargetAst::Scoped { node } => model.scoped_completions(node), + }; + completions.sort_by(compare_suggestions); + completions.dedup_by(|c1, c2| c1.name == c2.name); + completions +} + +/// The kind of tokens identified under the cursor. +#[derive(Debug)] +enum CompletionTargetTokens<'t> { + /// A `object.attribute` token form was found, where + /// `attribute` may be empty. + /// + /// This requires a name token followed by a dot token. + /// + /// This is "possibly" an `object.attribute` because + /// the object token may not correspond to an object + /// or it may correspond to *part* of an object. + /// This is resolved when we try to find an overlapping + /// AST `ExprAttribute` node later. If we couldn't, then + /// this is probably not an `object.attribute`. + PossibleObjectDot { + /// The token preceding the dot. + object: &'t Token, + /// The token, if non-empty, following the dot. + /// + /// This is currently unused, but we should use this + /// eventually to remove completions that aren't a + /// prefix of what has already been typed. (We are + /// currently relying on the LSP client to do this.) + #[expect(dead_code)] + attribute: Option<&'t Token>, + }, + /// A `from module import attribute` token form was found, where + /// `attribute` may be empty. + ImportFrom { + /// The module being imported from. + module: &'t Token, + }, + /// A token was found under the cursor, but it didn't + /// match any of our anticipated token patterns. + Generic { token: &'t Token }, + /// No token was found, but we have the offset of the + /// cursor. + Unknown { offset: TextSize }, +} + +impl<'t> CompletionTargetTokens<'t> { + /// Look for the best matching token pattern at the given offset. + fn find(parsed: &ParsedModuleRef, offset: TextSize) -> Option> { + static OBJECT_DOT_EMPTY: [TokenKind; 1] = [TokenKind::Dot]; + static OBJECT_DOT_NON_EMPTY: [TokenKind; 2] = [TokenKind::Dot, TokenKind::Name]; + + let offset = match parsed.tokens().at_offset(offset) { + TokenAt::None => return Some(CompletionTargetTokens::Unknown { offset }), + TokenAt::Single(tok) => tok.end(), + TokenAt::Between(_, tok) => tok.start(), + }; + let before = parsed.tokens().before(offset); + Some( + // Our strategy when it comes to `object.attribute` here is + // to look for the `.` and then take the token immediately + // preceding it. Later, we look for an `ExprAttribute` AST + // node that overlaps (even partially) with this token. And + // that's the object we try to complete attributes for. + if let Some([_dot]) = token_suffix_by_kinds(before, OBJECT_DOT_EMPTY) { + let object = before[..before.len() - 1].last()?; + CompletionTargetTokens::PossibleObjectDot { + object, + attribute: None, + } + } else if let Some([_dot, attribute]) = + token_suffix_by_kinds(before, OBJECT_DOT_NON_EMPTY) + { + let object = before[..before.len() - 2].last()?; + CompletionTargetTokens::PossibleObjectDot { + object, + attribute: Some(attribute), + } + } else if let Some(module) = import_from_tokens(before) { + CompletionTargetTokens::ImportFrom { module } + } else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Float]) { + // If we're writing a `float`, then we should + // specifically not offer completions. This wouldn't + // normally be an issue, but if completions are + // automatically triggered by a `.` (which is what we + // request as an LSP server), then we can get here + // in the course of just writing a decimal number. + return None; + } else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Ellipsis]) { + // Similarly as above. If we've just typed an ellipsis, + // then we shouldn't show completions. Note that + // this doesn't prevent `....` from showing + // completions (which would be the attributes available + // on an `ellipsis` object). + return None; + } else { + let Some(last) = before.last() else { + return Some(CompletionTargetTokens::Unknown { offset }); + }; + CompletionTargetTokens::Generic { token: last } + }, + ) + } + + /// Returns a corresponding AST node for these tokens. + /// + /// `offset` should be the offset of the cursor. + /// + /// If no plausible AST node could be found, then `None` is returned. + fn ast( + &self, + parsed: &'t ParsedModuleRef, + offset: TextSize, + ) -> Option> { + match *self { + CompletionTargetTokens::PossibleObjectDot { object, .. } => { + let covering_node = covering_node(parsed.syntax().into(), object.range()) + // We require that the end of the node range not + // exceed the cursor offset. This avoids selecting + // a node "too high" in the AST in cases where + // completions are requested in the middle of an + // expression. e.g., `foo..bar`. + .find_last(|node| node.is_expr_attribute() && node.range().end() <= offset) + .ok()?; + match covering_node.node() { + ast::AnyNodeRef::ExprAttribute(expr) => { + Some(CompletionTargetAst::ObjectDot { expr }) + } + _ => None, + } + } + CompletionTargetTokens::ImportFrom { module, .. } => { + let covering_node = covering_node(parsed.syntax().into(), module.range()) + .find_first(|node| node.is_stmt_import_from()) + .ok()?; + let ast::AnyNodeRef::StmtImportFrom(import) = covering_node.node() else { + return None; + }; + Some(CompletionTargetAst::ImportFrom { import, name: None }) + } + CompletionTargetTokens::Generic { token } => { + let covering_node = covering_node(parsed.syntax().into(), token.range()); + Some(CompletionTargetAst::Scoped { + node: covering_node.node(), + }) + } + CompletionTargetTokens::Unknown { offset } => { + let range = TextRange::empty(offset); + let covering_node = covering_node(parsed.syntax().into(), range); + Some(CompletionTargetAst::Scoped { + node: covering_node.node(), + }) + } + } + } +} + +/// The AST node patterns that we support identifying under the cursor. +#[derive(Debug)] +enum CompletionTargetAst<'t> { + /// A `object.attribute` scenario, where we want to + /// list attributes on `object` for completions. + ObjectDot { expr: &'t ast::ExprAttribute }, + /// A `from module import attribute` scenario, where we want to + /// list attributes on `module` for completions. + ImportFrom { + /// The import statement. + import: &'t ast::StmtImportFrom, + /// An index into `import.names` if relevant. When this is + /// set, the index is guaranteed to be valid. + name: Option, + }, + /// A scoped scenario, where we want to list all items available in + /// the most narrow scope containing the giving AST node. + Scoped { node: ast::AnyNodeRef<'t> }, +} + +/// Returns a suffix of `tokens` corresponding to the `kinds` given. +/// +/// If a suffix of `tokens` with the given `kinds` could not be found, +/// then `None` is returned. +/// +/// This is useful for matching specific patterns of token sequences +/// in order to identify what kind of completions we should offer. +fn token_suffix_by_kinds( + tokens: &[Token], + kinds: [TokenKind; N], +) -> Option<[&Token; N]> { + if kinds.len() > tokens.len() { + return None; + } + for (token, expected_kind) in tokens.iter().rev().zip(kinds.iter().rev()) { + if &token.kind() != expected_kind { + return None; + } + } + Some(std::array::from_fn(|i| { + &tokens[tokens.len() - (kinds.len() - i)] + })) +} + +/// Looks for the start of a `from module import ` statement. +/// +/// If found, one arbitrary token forming `module` is returned. +fn import_from_tokens(tokens: &[Token]) -> Option<&Token> { + use TokenKind as TK; + + /// The number of tokens we're willing to consume backwards from + /// the cursor's position until we give up looking for a `from + /// module import ` pattern. The state machine below has + /// lots of opportunities to bail way earlier than this, but if + /// there's, e.g., a long list of name tokens for something that + /// isn't an import, then we could end up doing a lot of wasted + /// work here. Probably humans aren't often working with single + /// import statements over 1,000 tokens long. + /// + /// The other thing to consider here is that, by the time we get to + /// this point, ty has already done some work proportional to the + /// length of `tokens` anyway. The unit of work we do below is very + /// small. + const LIMIT: usize = 1_000; + + /// A state used to "parse" the tokens preceding the user's cursor, + /// in reverse, to detect a "from import" statement. + enum S { + Start, + Names, + Module, + } + + let mut state = S::Start; + let mut module_token: Option<&Token> = None; + // Move backward through the tokens until we get to + // the `from` token. + for token in tokens.iter().rev().take(LIMIT) { + state = match (state, token.kind()) { + // It's okay to pop off a newline token here initially, + // since it may occur when the name being imported is + // empty. + (S::Start, TK::Newline) => S::Names, + // Munch through tokens that can make up an alias. + // N.B. We could also consider taking any token here + // *except* some limited set of tokens (like `Newline`). + // That might work well if it turns out that listing + // all possible allowable tokens is too brittle. + ( + S::Start | S::Names, + TK::Name + | TK::Comma + | TK::As + | TK::Case + | TK::Match + | TK::Type + | TK::Star + | TK::Lpar + | TK::Rpar + | TK::NonLogicalNewline + // It's not totally clear the conditions under + // which this occurs (I haven't read our tokenizer), + // but it appears in code like this, where this is + // the entire file contents: + // + // from sys import ( + // abiflags, + // + // + // It seems harmless to just allow this "unknown" + // token here to make the above work. + | TK::Unknown, + ) => S::Names, + (S::Start | S::Names, TK::Import) => S::Module, + // Munch through tokens that can make up a module. + ( + S::Module, + TK::Name | TK::Dot | TK::Ellipsis | TK::Case | TK::Match | TK::Type | TK::Unknown, + ) => { + // It's okay if there are multiple module + // tokens here. Just taking the last one + // (which is the one appearing first in + // the source code) is fine. We only need + // this to find the corresponding AST node, + // so any of the tokens should work fine. + module_token = Some(token); + S::Module + } + (S::Module, TK::From) => return module_token, + _ => return None, + }; + } + None +} + +/// Order completions lexicographically, with these exceptions: +/// +/// 1) A `_[^_]` prefix sorts last and +/// 2) A `__` prefix sorts last except before (1) +/// +/// This has the effect of putting all dunder attributes after "normal" +/// attributes, and all single-underscore attributes after dunder attributes. +fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering { + let (kind1, kind2) = (NameKind::classify(&c1.name), NameKind::classify(&c2.name)); + kind1.cmp(&kind2).then_with(|| c1.name.cmp(&c2.name)) +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + use ruff_python_parser::{Mode, ParseOptions, TokenKind, Tokens}; + use ty_python_semantic::Completion; + + use crate::completion; + use crate::tests::{CursorTest, cursor_test}; + + use super::token_suffix_by_kinds; + + #[test] + fn token_suffixes_match() { + insta::assert_debug_snapshot!( + token_suffix_by_kinds(&tokenize("foo.x"), [TokenKind::Newline]), + @r" + Some( + [ + Newline 5..5, + ], + ) + ", + ); + + insta::assert_debug_snapshot!( + token_suffix_by_kinds(&tokenize("foo.x"), [TokenKind::Name, TokenKind::Newline]), + @r" + Some( + [ + Name 4..5, + Newline 5..5, + ], + ) + ", + ); + + let all = [ + TokenKind::Name, + TokenKind::Dot, + TokenKind::Name, + TokenKind::Newline, + ]; + insta::assert_debug_snapshot!( + token_suffix_by_kinds(&tokenize("foo.x"), all), + @r" + Some( + [ + Name 0..3, + Dot 3..4, + Name 4..5, + Newline 5..5, + ], + ) + ", + ); + } + + #[test] + fn token_suffixes_nomatch() { + insta::assert_debug_snapshot!( + token_suffix_by_kinds(&tokenize("foo.x"), [TokenKind::Name]), + @"None", + ); + + let too_many = [ + TokenKind::Dot, + TokenKind::Name, + TokenKind::Dot, + TokenKind::Name, + TokenKind::Newline, + ]; + insta::assert_debug_snapshot!( + token_suffix_by_kinds(&tokenize("foo.x"), too_many), + @"None", + ); + } + + // At time of writing (2025-05-22), the tests below show some of the + // naivete of our completions. That is, we don't even take what has been + // typed into account. We just kind return all possible completions + // regardless of what has been typed and rely on the client to do filtering + // based on prefixes and what not. + // + // In the future, we might consider using "text edits,"[1] which will let + // us have more control over which completions are shown to the end user. + // But that will require us to at least do some kind of filtering based on + // what has been typed. + // + // [1]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion + + #[test] + fn empty() { + let test = cursor_test( + "\ + +", + ); + + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + #[test] + fn builtins() { + let test = cursor_test( + "\ + +", + ); + test.assert_completions_include("filter"); + // Sunder items should be filtered out + test.assert_completions_do_not_include("_T"); + // Dunder attributes should not be stripped + test.assert_completions_include("__annotations__"); + // See `private_symbols_in_stub` for more comprehensive testing private of symbol filtering. + } + + #[test] + fn builtins_not_included_object_attr() { + let test = cursor_test( + "\ +import re + +re. +", + ); + test.assert_completions_do_not_include("filter"); + } + + #[test] + fn builtins_not_included_import() { + let test = cursor_test( + "\ +from re import +", + ); + test.assert_completions_do_not_include("filter"); + } + + #[test] + fn imports1() { + let test = cursor_test( + "\ +import re + + +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"re"); + } + + #[test] + fn imports2() { + let test = cursor_test( + "\ +from os import path + + +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"path"); + } + + // N.B. We don't currently explore module APIs. This + // is still just emitting symbols from the detected scope. + #[test] + fn module_api() { + let test = cursor_test( + "\ +import re + +re. +", + ); + test.assert_completions_include("findall"); + } + + #[test] + fn private_symbols_in_stub() { + let test = CursorTest::builder() + .source( + "package/__init__.pyi", + r#"\ +from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol + +public_name = 1 +_private_name = 1 +__mangled_name = 1 +__dunder_name__ = 1 + +public_type_var = TypeVar("public_type_var") +_private_type_var = TypeVar("_private_type_var") +__mangled_type_var = TypeVar("__mangled_type_var") + +public_param_spec = ParamSpec("public_param_spec") +_private_param_spec = ParamSpec("_private_param_spec") + +public_type_var_tuple = TypeVarTuple("public_type_var_tuple") +_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple") + +public_explicit_type_alias: TypeAlias = Literal[1] +_private_explicit_type_alias: TypeAlias = Literal[1] + +class PublicProtocol(Protocol): + def method(self) -> None: ... + +class _PrivateProtocol(Protocol): + def method(self) -> None: ... +"#, + ) + .source("main.py", "import package; package.") + .build(); + test.assert_completions_include("public_name"); + test.assert_completions_include("_private_name"); + test.assert_completions_include("__mangled_name"); + test.assert_completions_include("__dunder_name__"); + test.assert_completions_include("public_type_var"); + test.assert_completions_do_not_include("_private_type_var"); + test.assert_completions_do_not_include("__mangled_type_var"); + test.assert_completions_include("public_param_spec"); + test.assert_completions_do_not_include("_private_param_spec"); + test.assert_completions_include("public_type_var_tuple"); + test.assert_completions_do_not_include("_private_type_var_tuple"); + test.assert_completions_include("public_explicit_type_alias"); + test.assert_completions_include("_private_explicit_type_alias"); + test.assert_completions_include("PublicProtocol"); + test.assert_completions_do_not_include("_PrivateProtocol"); + } + + /// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names + /// are visible. + #[test] + fn private_symbols_in_module() { + let test = CursorTest::builder() + .source( + "package/__init__.py", + r#"\ +from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol + +public_name = 1 +_private_name = 1 +__mangled_name = 1 +__dunder_name__ = 1 + +public_type_var = TypeVar("public_type_var") +_private_type_var = TypeVar("_private_type_var") +__mangled_type_var = TypeVar("__mangled_type_var") + +public_param_spec = ParamSpec("public_param_spec") +_private_param_spec = ParamSpec("_private_param_spec") + +public_type_var_tuple = TypeVarTuple("public_type_var_tuple") +_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple") + +public_explicit_type_alias: TypeAlias = Literal[1] +_private_explicit_type_alias: TypeAlias = Literal[1] + +class PublicProtocol(Protocol): + def method(self) -> None: ... + +class _PrivateProtocol(Protocol): + def method(self) -> None: ... +"#, + ) + .source("main.py", "import package; package.") + .build(); + test.assert_completions_include("public_name"); + test.assert_completions_include("_private_name"); + test.assert_completions_include("__mangled_name"); + test.assert_completions_include("__dunder_name__"); + test.assert_completions_include("public_type_var"); + test.assert_completions_include("_private_type_var"); + test.assert_completions_include("__mangled_type_var"); + test.assert_completions_include("public_param_spec"); + test.assert_completions_include("_private_param_spec"); + test.assert_completions_include("public_type_var_tuple"); + test.assert_completions_include("_private_type_var_tuple"); + test.assert_completions_include("public_explicit_type_alias"); + test.assert_completions_include("_private_explicit_type_alias"); + test.assert_completions_include("PublicProtocol"); + test.assert_completions_include("_PrivateProtocol"); + } + + #[test] + fn one_function_prefix() { + let test = cursor_test( + "\ +def foo(): ... + +f +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn one_function_not_prefix() { + let test = cursor_test( + "\ +def foo(): ... + +g +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn one_function_blank() { + let test = cursor_test( + "\ +def foo(): ... + + +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + "); + } + + #[test] + fn nested_function_prefix() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): ... + +f +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn nested_function_blank() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): ... + + +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + "); + } + + #[test] + fn nested_function_not_in_global_scope_prefix() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): ... + f +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + foofoo + "); + } + + #[test] + fn nested_function_not_in_global_scope_blank() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): ... + +", + ); + + // FIXME: Should include `foofoo`. + // + // `foofoo` isn't included at present (2025-05-22). The problem + // here is that the AST for `def foo():` doesn't encompass the + // trailing indentation. So when the cursor position is in that + // trailing indentation, we can't (easily) get a handle to the + // right scope. And even if we could, the AST expressions for + // `def foo():` and `def foofoo(): ...` end at precisely the + // same point. So there is no AST we can hold after the end of + // `foofoo` but before the end of `foo`. So at the moment, it's + // not totally clear how to get the right scope. + // + // If we didn't want to change the ranges on the AST nodes, + // another approach here would be to get the inner most scope, + // and explore its ancestors until we get to a level that + // matches the current cursor's indentation. This seems fraught + // however. It's not clear to me that we can always assume a + // correspondence between scopes and indentation level. + assert_snapshot!(test.completions_without_builtins(), @r" + foo + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_prefix1() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + f +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + foofoo + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_prefix2() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + f", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + foofoo + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_prefix3() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + f +def frob(): ... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + foofoo + frob + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_prefix4() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... +f +def frob(): ... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + frob + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_prefix5() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + f +def frob(): ... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + foo + foofoo + foofoofoo + frob + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_blank1() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + +", + ); + + // FIXME: Should include `foofoo` (but not `foofoofoo`). + // + // The tests below fail for the same reason that + // `nested_function_not_in_global_scope_blank` fails: there is no + // space in the AST ranges after the end of `foofoofoo` but before + // the end of `foofoo`. So either the AST needs to be tweaked to + // account for the indented whitespace, or some other technique + // needs to be used to get the scope containing `foofoo` but not + // `foofoofoo`. + assert_snapshot!(test.completions_without_builtins(), @r" + foo + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_blank2() { + let test = cursor_test( + " \ +def foo(): + def foofoo(): + def foofoofoo(): ... + ", + ); + + // FIXME: Should include `foofoo` (but not `foofoofoo`). + assert_snapshot!(test.completions_without_builtins(), @r" + foo + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_blank3() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + +def frob(): ... + ", + ); + + // FIXME: Should include `foofoo` (but not `foofoofoo`). + assert_snapshot!(test.completions_without_builtins(), @r" + foo + frob + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_blank4() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + + +def frob(): ... +", + ); + + // FIXME: Should include `foofoo` (but not `foofoofoo`). + assert_snapshot!(test.completions_without_builtins(), @r" + foo + frob + "); + } + + #[test] + fn double_nested_function_not_in_global_scope_blank5() { + let test = cursor_test( + "\ +def foo(): + def foofoo(): + def foofoofoo(): ... + + + +def frob(): ... +", + ); + + // FIXME: Should include `foofoo` (but not `foofoofoo`). + assert_snapshot!(test.completions_without_builtins(), @r" + foo + frob + "); + } + + #[test] + fn list_comprehension1() { + let test = cursor_test( + "\ +[ for bar in [1, 2, 3]] +", + ); + + // TODO: it would be good if `bar` was included here, but + // the list comprehension is not yet valid and so we do not + // detect this as a definition of `bar`. + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + #[test] + fn list_comprehension2() { + let test = cursor_test( + "\ +[f for foo in [1, 2, 3]] +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn lambda_prefix1() { + let test = cursor_test( + "\ +(lambda foo: (1 + f + 2))(2) +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn lambda_prefix2() { + let test = cursor_test( + "\ +(lambda foo: f + 1)(2) +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn lambda_prefix3() { + let test = cursor_test( + "\ +(lambda foo: (f + 1))(2) +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn lambda_prefix4() { + let test = cursor_test( + "\ +(lambda foo: 1 + f)(2) +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn lambda_blank1() { + let test = cursor_test( + "\ +(lambda foo: 1 + + 2)(2) +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"foo"); + } + + #[test] + fn lambda_blank2() { + let test = cursor_test( + "\ +(lambda foo: + 1)(2) +", + ); + + // FIXME: Should include `foo`. + // + // These fails for similar reasons as above: the body of the + // lambda doesn't include the position of because + // is inside leading or trailing whitespace. (Even + // when enclosed in parentheses. Specifically, parentheses + // aren't part of the node's range unless it's relevant e.g., + // tuples.) + // + // The `lambda_blank1` test works because there are expressions + // on either side of . + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + #[test] + fn lambda_blank3() { + let test = cursor_test( + "\ +(lambda foo: ( + 1))(2) +", + ); + + // FIXME: Should include `foo`. + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + #[test] + fn lambda_blank4() { + let test = cursor_test( + "\ +(lambda foo: 1 + )(2) +", + ); + + // FIXME: Should include `foo`. + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + #[test] + fn class_prefix1() { + let test = cursor_test( + "\ +class Foo: + bar = 1 + quux = b + frob = 3 +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + Foo + bar + frob + quux + "); + } + + #[test] + fn class_prefix2() { + let test = cursor_test( + "\ +class Foo: + bar = 1 + quux = b +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + Foo + bar + quux + "); + } + + #[test] + fn class_blank1() { + let test = cursor_test( + "\ +class Foo: + bar = 1 + quux = + frob = 3 +", + ); + + // FIXME: Should include `bar`, `quux` and `frob`. + // (Unclear if `Foo` should be included, but a false + // positive isn't the end of the world.) + // + // These don't work for similar reasons as other + // tests above with the inside of whitespace. + assert_snapshot!(test.completions_without_builtins(), @r" + Foo + "); + } + + #[test] + fn class_blank2() { + let test = cursor_test( + "\ +class Foo: + bar = 1 + quux = + frob = 3 +", + ); + + // FIXME: Should include `bar`, `quux` and `frob`. + // (Unclear if `Foo` should be included, but a false + // positive isn't the end of the world.) + assert_snapshot!(test.completions_without_builtins(), @r" + Foo + "); + } + + #[test] + fn class_super1() { + let test = cursor_test( + "\ +class Bar: ... + +class Foo(): + bar = 1 +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + Bar + Foo + "); + } + + #[test] + fn class_super2() { + let test = cursor_test( + "\ +class Foo(): + bar = 1 + +class Bar: ... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + Bar + Foo + "); + } + + #[test] + fn class_super3() { + let test = cursor_test( + "\ +class Foo( + bar = 1 + +class Bar: ... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + Bar + Foo + "); + } + + #[test] + fn class_super4() { + let test = cursor_test( + "\ +class Bar: ... + +class Foo(", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + Bar + Foo + "); + } + + #[test] + fn class_init1() { + let test = cursor_test( + "\ +class Quux: + def __init__(self): + self.foo = 1 + self.bar = 2 + self.baz = 3 + +quux = Quux() +quux. +", + ); + + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + bar :: Unknown | Literal[2] + baz :: Unknown | Literal[3] + foo :: Unknown | Literal[1] + __annotations__ :: dict[str, Any] + __class__ :: type + __delattr__ :: bound method object.__delattr__(name: str, /) -> None + __dict__ :: dict[str, Any] + __dir__ :: bound method object.__dir__() -> Iterable[str] + __doc__ :: str | None + __eq__ :: bound method object.__eq__(value: object, /) -> bool + __format__ :: bound method object.__format__(format_spec: str, /) -> str + __getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any + __getstate__ :: bound method object.__getstate__() -> object + __hash__ :: bound method object.__hash__() -> int + __init__ :: bound method Quux.__init__() -> Unknown + __init_subclass__ :: bound method object.__init_subclass__() -> None + __module__ :: str + __ne__ :: bound method object.__ne__(value: object, /) -> bool + __new__ :: bound method object.__new__() -> Self + __reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...] + __reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: bound method object.__repr__() -> str + __setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None + __sizeof__ :: bound method object.__sizeof__() -> int + __str__ :: bound method object.__str__() -> str + __subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool + "); + } + + #[test] + fn class_init2() { + let test = cursor_test( + "\ +class Quux: + def __init__(self): + self.foo = 1 + self.bar = 2 + self.baz = 3 + +quux = Quux() +quux.b +", + ); + + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + bar :: Unknown | Literal[2] + baz :: Unknown | Literal[3] + foo :: Unknown | Literal[1] + __annotations__ :: dict[str, Any] + __class__ :: type + __delattr__ :: bound method object.__delattr__(name: str, /) -> None + __dict__ :: dict[str, Any] + __dir__ :: bound method object.__dir__() -> Iterable[str] + __doc__ :: str | None + __eq__ :: bound method object.__eq__(value: object, /) -> bool + __format__ :: bound method object.__format__(format_spec: str, /) -> str + __getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any + __getstate__ :: bound method object.__getstate__() -> object + __hash__ :: bound method object.__hash__() -> int + __init__ :: bound method Quux.__init__() -> Unknown + __init_subclass__ :: bound method object.__init_subclass__() -> None + __module__ :: str + __ne__ :: bound method object.__ne__(value: object, /) -> bool + __new__ :: bound method object.__new__() -> Self + __reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...] + __reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: bound method object.__repr__() -> str + __setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None + __sizeof__ :: bound method object.__sizeof__() -> int + __str__ :: bound method object.__str__() -> str + __subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool + "); + } + + #[test] + fn class_init3() { + let test = cursor_test( + "\ +class Quux: + def __init__(self): + self.foo = 1 + self.bar = 2 + self. + self.baz = 3 +", + ); + + // FIXME: This should list completions on `self`, which should + // include, at least, `foo` and `bar`. At time of writing + // (2025-06-04), the type of `self` is inferred as `Unknown` in + // this context. This in turn prevents us from getting a list + // of available attributes. + // + // See: https://github.com/astral-sh/ty/issues/159 + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn class_attributes1() { + let test = cursor_test( + "\ +class Quux: + some_attribute: int = 1 + + def __init__(self): + self.foo = 1 + self.bar = 2 + self.baz = 3 + + def some_method(self) -> int: + return 1 + + @property + def some_property(self) -> int: + return 1 + + @classmethod + def some_class_method(self) -> int: + return 1 + + @staticmethod + def some_static_method(self) -> int: + return 1 + +Quux. +", + ); + + assert_snapshot!(test.completions_without_builtins_with_types(), @r" + mro :: def mro(self) -> list[type] + some_attribute :: int + some_class_method :: bound method .some_class_method() -> int + some_method :: def some_method(self) -> int + some_property :: property + some_static_method :: def some_static_method(self) -> int + __annotations__ :: dict[str, Any] + __base__ :: type | None + __bases__ :: tuple[type, ...] + __basicsize__ :: int + __call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any + __class__ :: + __delattr__ :: def __delattr__(self, name: str, /) -> None + __dict__ :: MappingProxyType[str, Any] + __dictoffset__ :: int + __dir__ :: def __dir__(self) -> Iterable[str] + __doc__ :: str | None + __eq__ :: def __eq__(self, value: object, /) -> bool + __flags__ :: int + __format__ :: def __format__(self, format_spec: str, /) -> str + __getattribute__ :: def __getattribute__(self, name: str, /) -> Any + __getstate__ :: def __getstate__(self) -> object + __hash__ :: def __hash__(self) -> int + __init__ :: def __init__(self) -> Unknown + __init_subclass__ :: def __init_subclass__(cls) -> None + __instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool + __itemsize__ :: int + __module__ :: str + __mro__ :: tuple[, ] + __name__ :: str + __ne__ :: def __ne__(self, value: object, /) -> bool + __new__ :: def __new__(cls) -> Self + __or__ :: def __or__(self, value: Any, /) -> UnionType + __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] + __qualname__ :: str + __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] + __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] + __repr__ :: def __repr__(self) -> str + __ror__ :: def __ror__(self, value: Any, /) -> UnionType + __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None + __sizeof__ :: def __sizeof__(self) -> int + __str__ :: def __str__(self) -> str + __subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool + __subclasses__ :: def __subclasses__(self: Self) -> list[Self] + __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool + __text_signature__ :: str | None + __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] + __weakrefoffset__ :: int + "); + } + + // We don't yet take function parameters into account. + #[test] + fn call_prefix1() { + let test = cursor_test( + "\ +def bar(okay=None): ... + +foo = 1 + +bar(o +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + bar + foo + "); + } + + #[test] + fn call_blank1() { + let test = cursor_test( + "\ +def bar(okay=None): ... + +foo = 1 + +bar( +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + bar + foo + "); + } + + #[test] + fn duplicate1() { + let test = cursor_test( + "\ +def foo(): ... + +class C: + def foo(self): ... + def bar(self): + f +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + C + bar + foo + self + "); + } + + #[test] + fn instance_methods_are_not_regular_functions1() { + let test = cursor_test( + "\ +class C: + def foo(self): ... + + +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"C"); + } + + #[test] + fn instance_methods_are_not_regular_functions2() { + let test = cursor_test( + "\ +class C: + def foo(self): ... + def bar(self): + f +", + ); + + // FIXME: Should NOT include `foo` here, since + // that is only a method that can be called on + // `self`. + assert_snapshot!(test.completions_without_builtins(), @r" + C + bar + foo + self + "); + } + + #[test] + fn identifier_keyword_clash1() { + let test = cursor_test( + "\ +classy_variable_name = 1 + +class +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"classy_variable_name"); + } + + #[test] + fn identifier_keyword_clash2() { + let test = cursor_test( + "\ +some_symbol = 1 + +print(f\"{some +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"some_symbol"); + } + + #[test] + fn statically_unreachable_symbols() { + let test = cursor_test( + "\ +if 1 + 2 != 3: + hidden_symbol = 1 + +hidden_ +", + ); + + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + #[test] + fn completions_inside_unreachable_sections() { + let test = cursor_test( + "\ +import sys + +if sys.platform == \"not-my-current-platform\": + only_available_in_this_branch = 1 + + on +", + ); + + // TODO: ideally, `only_available_in_this_branch` should be available here, but we + // currently make no effort to provide a good IDE experience within sections that + // are unreachable + assert_snapshot!(test.completions_without_builtins(), @"sys"); + } + + #[test] + fn star_import() { + let test = cursor_test( + "\ +from typing import * + +Re +", + ); + + test.assert_completions_include("Reversible"); + // `ReadableBuffer` is a symbol in `typing`, but it is not re-exported + test.assert_completions_do_not_include("ReadableBuffer"); + } + + #[test] + fn attribute_access_empty_list() { + let test = cursor_test( + "\ +[]. +", + ); + + test.assert_completions_include("append"); + } + + #[test] + fn attribute_access_empty_dict() { + let test = cursor_test( + "\ +{}. +", + ); + + test.assert_completions_include("values"); + test.assert_completions_do_not_include("add"); + } + + #[test] + fn attribute_access_set() { + let test = cursor_test( + "\ +{1}. +", + ); + + test.assert_completions_include("add"); + test.assert_completions_do_not_include("values"); + } + + #[test] + fn attribute_parens() { + let test = cursor_test( + "\ +class A: + x: str + +a = A() +(a). +", + ); + + test.assert_completions_include("x"); + } + + #[test] + fn attribute_double_parens() { + let test = cursor_test( + "\ +class A: + x: str + +a = A() +((a)). +", + ); + + test.assert_completions_include("x"); + } + + #[test] + fn attribute_on_constructor_directly() { + let test = cursor_test( + "\ +class A: + x: str + +A(). +", + ); + + test.assert_completions_include("x"); + } + + #[test] + fn attribute_not_on_integer() { + let test = cursor_test( + "\ +3. +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn attribute_on_integer() { + let test = cursor_test( + "\ +(3). +", + ); + + test.assert_completions_include("bit_length"); + } + + #[test] + fn attribute_on_float() { + let test = cursor_test( + "\ +3.14. +", + ); + + test.assert_completions_include("conjugate"); + } + + #[test] + fn nested_attribute_access1() { + let test = cursor_test( + "\ +class A: + x: str + +class B: + a: A + +b = B() +b.a. +", + ); + + test.assert_completions_do_not_include("a"); + test.assert_completions_include("x"); + } + + #[test] + fn nested_attribute_access2() { + let test = cursor_test( + "\ +class B: + c: int + +class A: + b: B + +a = A() +([1] + [a.b.] + [3]).pop() +", + ); + + test.assert_completions_include("c"); + test.assert_completions_do_not_include("b"); + test.assert_completions_do_not_include("pop"); + } + + #[test] + fn nested_attribute_access3() { + let test = cursor_test( + "\ +a = A() +([1] + [\"abc\".] + [3]).pop() +", + ); + + test.assert_completions_include("capitalize"); + test.assert_completions_do_not_include("append"); + test.assert_completions_do_not_include("pop"); + } + + #[test] + fn nested_attribute_access4() { + let test = cursor_test( + "\ +class B: + c: int + +class A: + b: B + +def foo() -> A: + return A() + +foo(). +", + ); + + test.assert_completions_include("b"); + test.assert_completions_do_not_include("c"); + } + + #[test] + fn nested_attribute_access5() { + let test = cursor_test( + "\ +class B: + c: int + +class A: + b: B + +def foo() -> A: + return A() + +foo().b. +", + ); + + test.assert_completions_include("c"); + test.assert_completions_do_not_include("b"); + } + + #[test] + fn betwixt_attribute_access1() { + let test = cursor_test( + "\ +class Foo: + xyz: str + +class Bar: + foo: Foo + +class Quux: + bar: Bar + +quux = Quux() +quux..foo.xyz +", + ); + + test.assert_completions_include("bar"); + test.assert_completions_do_not_include("xyz"); + test.assert_completions_do_not_include("foo"); + } + + #[test] + fn betwixt_attribute_access2() { + let test = cursor_test( + "\ +class Foo: + xyz: str + +class Bar: + foo: Foo + +class Quux: + bar: Bar + +quux = Quux() +quux.b.foo.xyz +", + ); + + test.assert_completions_include("bar"); + test.assert_completions_do_not_include("xyz"); + test.assert_completions_do_not_include("foo"); + } + + #[test] + fn betwixt_attribute_access3() { + let test = cursor_test( + "\ +class Foo: + xyz: str + +class Bar: + foo: Foo + +class Quux: + bar: Bar + +quux = Quux() +.foo.xyz +", + ); + + test.assert_completions_include("quux"); + } + + #[test] + fn betwixt_attribute_access4() { + let test = cursor_test( + "\ +class Foo: + xyz: str + +class Bar: + foo: Foo + +class Quux: + bar: Bar + +quux = Quux() +q.foo.xyz +", + ); + + test.assert_completions_include("quux"); + } + + #[test] + fn ellipsis1() { + let test = cursor_test( + "\ +... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn ellipsis2() { + let test = cursor_test( + "\ +.... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + __annotations__ + __class__ + __delattr__ + __dict__ + __dir__ + __doc__ + __eq__ + __format__ + __getattribute__ + __getstate__ + __hash__ + __init__ + __init_subclass__ + __module__ + __ne__ + __new__ + __reduce__ + __reduce_ex__ + __repr__ + __setattr__ + __sizeof__ + __str__ + __subclasshook__ + "); + } + + #[test] + fn ellipsis3() { + let test = cursor_test( + "\ +class Foo: ... +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn ordering() { + let test = cursor_test( + "\ +class A: + foo: str + _foo: str + __foo__: str + __foo: str + FOO: str + _FOO: str + __FOO__: str + __FOO: str + +A. +", + ); + + assert_snapshot!( + test.completions_if(|c| c.name.contains("FOO") || c.name.contains("foo")), + @r" + FOO + foo + __FOO__ + __foo__ + _FOO + __FOO + __foo + _foo + ", + ); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_function_identifier1() { + let test = cursor_test( + "\ +def m +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_function_identifier2() { + let test = cursor_test( + "\ +def m(): pass +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn fscope_id_missing_function_identifier3() { + let test = cursor_test( + "\ +def m(): pass + +", + ); + + assert_snapshot!(test.completions_without_builtins(), @r" + m + "); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_class_identifier1() { + let test = cursor_test( + "\ +class M +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_type_alias1() { + let test = cursor_test( + "\ +Fo = float +", + ); + + assert_snapshot!(test.completions_without_builtins(), @"Fo"); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_import1() { + let test = cursor_test( + "\ +import fo +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_import2() { + let test = cursor_test( + "\ +import foo as ba +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_from_import1() { + let test = cursor_test( + "\ +from fo import wat +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_from_import2() { + let test = cursor_test( + "\ +from foo import wa +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_from_import3() { + let test = cursor_test( + "\ +from foo import wat as ba +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_try_except1() { + let test = cursor_test( + "\ +try: + pass +except Type: + pass +", + ); + + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_global1() { + let test = cursor_test( + "\ +def _(): + global fo +", + ); + + assert_snapshot!(test.completions_without_builtins(), @""); + } + + #[test] + fn string_dot_attr1() { + let test = cursor_test( + r#" +foo = 1 +bar = 2 + +class Foo: + def method(self): ... + +f = Foo() + +# String, this is not an attribute access +"f. +"#, + ); + + // TODO: This should not have any completions suggested for it. + // We do correctly avoid giving `object.attr` completions here, + // but we instead fall back to scope based completions. Since + // we're inside a string, we should avoid giving completions at + // all. + assert_snapshot!(test.completions_without_builtins(), @r" + Foo + bar + f + foo + "); + } + + #[test] + fn string_dot_attr2() { + let test = cursor_test( + r#" +foo = 1 +bar = 2 + +class Foo: + def method(self): ... + +f = Foo() + +# F-string, this is an attribute access +f"{f. +"#, + ); + + test.assert_completions_include("method"); + } + + #[test] + fn no_panic_for_attribute_table_that_contains_subscript() { + let test = cursor_test( + r#" +class Point: + def orthogonal_direction(self): + self[0].is_zero + +def test_point(p2: Point): + p2. +"#, + ); + test.assert_completions_include("orthogonal_direction"); + } + + #[test] + fn from_import1() { + let test = cursor_test( + "\ +from sys import +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import2() { + let test = cursor_test( + "\ +from sys import abiflags, +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import3() { + let test = cursor_test( + "\ +from sys import , abiflags +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import4() { + let test = cursor_test( + "\ +from sys import abiflags, \ + +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import5() { + let test = cursor_test( + "\ +from sys import abiflags as foo, +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import6() { + let test = cursor_test( + "\ +from sys import abiflags as foo, g +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import7() { + let test = cursor_test( + "\ +from sys import abiflags as foo, \ + +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import8() { + let test = cursor_test( + "\ +from sys import abiflags as foo, \ + g +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import9() { + let test = cursor_test( + "\ +from sys import ( + abiflags, + +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import10() { + let test = cursor_test( + "\ +from sys import ( + abiflags, + +) +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import11() { + let test = cursor_test( + "\ +from sys import ( + +) +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import_unknown_in_module() { + let test = cursor_test( + "\ +foo = 1 +from ? import +", + ); + assert_snapshot!(test.completions_without_builtins(), @r""); + } + + #[test] + fn from_import_unknown_in_import_names1() { + let test = cursor_test( + "\ +from sys import ?, +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import_unknown_in_import_names2() { + let test = cursor_test( + "\ +from sys import ??, +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn from_import_unknown_in_import_names3() { + let test = cursor_test( + "\ +from sys import ??, , ?? +", + ); + test.assert_completions_include("getsizeof"); + } + + #[test] + fn relative_from_import1() { + let test = CursorTest::builder() + .source("package/__init__.py", "") + .source( + "package/foo.py", + "\ +Cheetah = 1 +Lion = 2 +Cougar = 3 +", + ) + .source("package/sub1/sub2/bar.py", "from ...foo import ") + .build(); + test.assert_completions_include("Cheetah"); + } + + #[test] + fn relative_from_import2() { + let test = CursorTest::builder() + .source("package/__init__.py", "") + .source( + "package/sub1/foo.py", + "\ +Cheetah = 1 +Lion = 2 +Cougar = 3 +", + ) + .source("package/sub1/sub2/bar.py", "from ..foo import ") + .build(); + test.assert_completions_include("Cheetah"); + } + + #[test] + fn relative_from_import3() { + let test = CursorTest::builder() + .source("package/__init__.py", "") + .source( + "package/sub1/sub2/foo.py", + "\ +Cheetah = 1 +Lion = 2 +Cougar = 3 +", + ) + .source("package/sub1/sub2/bar.py", "from .foo import ") + .build(); + test.assert_completions_include("Cheetah"); + } + + #[test] + fn import_submodule_not_attribute1() { + let test = cursor_test( + "\ +import importlib +importlib. +", + ); + test.assert_completions_do_not_include("resources"); + } + + #[test] + fn import_submodule_not_attribute2() { + let test = cursor_test( + "\ +import importlib.resources +importlib. +", + ); + test.assert_completions_include("resources"); + } + + #[test] + fn import_submodule_not_attribute3() { + let test = cursor_test( + "\ +import importlib +import importlib.resources +importlib. +", + ); + test.assert_completions_include("resources"); + } + + #[test] + fn regression_test_issue_642() { + // Regression test for https://github.com/astral-sh/ty/issues/642 + + let test = cursor_test( + r#" + match 0: + case 1 i: + pass + "#, + ); + + assert_snapshot!( + test.completions_without_builtins(), + @"", + ); + } + + impl CursorTest { + /// Returns all completions except for builtins. + fn completions_without_builtins(&self) -> String { + self.completions_if(|c| !c.builtin) + } + + fn completions_without_builtins_with_types(&self) -> String { + self.completions_if_snapshot( + |c| !c.builtin, + |c| format!("{} :: {}", c.name, c.ty.display(&self.db)), + ) + } + + fn completions_if(&self, predicate: impl Fn(&Completion) -> bool) -> String { + self.completions_if_snapshot(predicate, |c| c.name.as_str().to_string()) + } + + fn completions_if_snapshot( + &self, + predicate: impl Fn(&Completion) -> bool, + snapshot: impl Fn(&Completion) -> String, + ) -> String { + let completions = completion(&self.db, self.cursor.file, self.cursor.offset); + if completions.is_empty() { + return "".to_string(); + } + let included = completions + .iter() + .filter(|label| predicate(label)) + .map(snapshot) + .collect::>(); + if included.is_empty() { + // It'd be nice to include the actual number of + // completions filtered out, but in practice, the + // number is environment dependent. For example, on + // Windows, there are 231 builtins, but on Unix, there + // are 230. So we just leave out the number I guess. + // ---AG + return "".to_string(); + } + included.join("\n") + } + + #[track_caller] + fn assert_completions_include(&self, expected: &str) { + let completions = completion(&self.db, self.cursor.file, self.cursor.offset); + + assert!( + completions + .iter() + .any(|completion| completion.name == expected), + "Expected completions to include `{expected}`" + ); + } + + #[track_caller] + fn assert_completions_do_not_include(&self, unexpected: &str) { + let completions = completion(&self.db, self.cursor.file, self.cursor.offset); + + assert!( + completions + .iter() + .all(|completion| completion.name != unexpected), + "Expected completions to not include `{unexpected}`", + ); + } + } + + fn tokenize(src: &str) -> Tokens { + let parsed = ruff_python_parser::parse(src, ParseOptions::from(Mode::Module)) + .expect("valid Python source for token stream"); + parsed.tokens().clone() + } +} diff --git a/crates/ty_ide/src/db.rs b/crates/ty_ide/src/db.rs new file mode 100644 index 0000000000000..3d9a9c564eb66 --- /dev/null +++ b/crates/ty_ide/src/db.rs @@ -0,0 +1,117 @@ +use ty_python_semantic::Db as SemanticDb; + +#[salsa::db] +pub trait Db: SemanticDb {} + +#[cfg(test)] +pub(crate) mod tests { + use std::sync::{Arc, Mutex}; + + use super::Db; + use ruff_db::Db as SourceDb; + use ruff_db::files::{File, Files}; + use ruff_db::system::{DbWithTestSystem, System, TestSystem}; + use ruff_db::vendored::VendoredFileSystem; + use ty_python_semantic::lint::{LintRegistry, RuleSelection}; + use ty_python_semantic::{Db as SemanticDb, Program, default_lint_registry}; + + type Events = Arc>>; + + #[salsa::db] + #[derive(Clone)] + pub(crate) struct TestDb { + storage: salsa::Storage, + files: Files, + system: TestSystem, + vendored: VendoredFileSystem, + events: Events, + rule_selection: Arc, + } + + #[expect(dead_code)] + impl TestDb { + pub(crate) fn new() -> Self { + let events = Events::default(); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let events = events.clone(); + move |event| { + tracing::trace!("event: {event:?}"); + let mut events = events.lock().unwrap(); + events.push(event); + } + }))), + system: TestSystem::default(), + vendored: ty_vendored::file_system().clone(), + events, + files: Files::default(), + rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), + } + } + + /// Takes the salsa events. + pub(crate) fn take_salsa_events(&mut self) -> Vec { + let mut events = self.events.lock().unwrap(); + + std::mem::take(&mut *events) + } + + /// Clears the salsa events. + /// + /// ## Panics + /// If there are any pending salsa snapshots. + pub(crate) fn clear_salsa_events(&mut self) { + self.take_salsa_events(); + } + } + + impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } + } + + #[salsa::db] + impl SourceDb for TestDb { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> ruff_python_ast::PythonVersion { + Program::get(self).python_version(self) + } + } + + #[salsa::db] + impl SemanticDb for TestDb { + fn is_file_open(&self, file: File) -> bool { + !file.path(self).is_vendored_path() + } + + fn rule_selection(&self, _file: File) -> &RuleSelection { + &self.rule_selection + } + + fn lint_registry(&self) -> &LintRegistry { + default_lint_registry() + } + } + + #[salsa::db] + impl Db for TestDb {} + + #[salsa::db] + impl salsa::Database for TestDb {} +} diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs new file mode 100644 index 0000000000000..c592ed84aade8 --- /dev/null +++ b/crates/ty_ide/src/docstring.rs @@ -0,0 +1,664 @@ +//! Docstring parsing utilities for language server features. +//! +//! This module provides functionality for extracting structured information from +//! Python docstrings, including parameter documentation for signature help. +//! Supports Google-style, NumPy-style, and reST/Sphinx-style docstrings. +//! There are no formal specifications for any of these formats, so the parsing +//! logic needs to be tolerant of variations. + +use regex::Regex; +use ruff_python_trivia::leading_indentation; +use ruff_source_file::UniversalNewlines; +use std::collections::HashMap; +use std::sync::LazyLock; + +// Static regex instances to avoid recompilation +static GOOGLE_SECTION_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)^\s*(Args|Arguments|Parameters)\s*:\s*$") + .expect("Google section regex should be valid") +}); + +static GOOGLE_PARAM_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:\s*(.+)") + .expect("Google parameter regex should be valid") +}); + +static NUMPY_SECTION_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)^\s*Parameters\s*$").expect("NumPy section regex should be valid") +}); + +static NUMPY_UNDERLINE_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*-+\s*$").expect("NumPy underline regex should be valid")); + +static REST_PARAM_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^\s*:param\s+(?:(\w+)\s+)?(\w+)\s*:\s*(.+)") + .expect("reST parameter regex should be valid") +}); + +/// Extract parameter documentation from popular docstring formats. +/// Returns a map of parameter names to their documentation. +pub fn get_parameter_documentation(docstring: &str) -> HashMap { + let mut param_docs = HashMap::new(); + + // Google-style docstrings + param_docs.extend(extract_google_style_params(docstring)); + + // NumPy-style docstrings + param_docs.extend(extract_numpy_style_params(docstring)); + + // reST/Sphinx-style docstrings + param_docs.extend(extract_rest_style_params(docstring)); + + param_docs +} + +/// Extract parameter documentation from Google-style docstrings. +fn extract_google_style_params(docstring: &str) -> HashMap { + let mut param_docs = HashMap::new(); + + let mut in_args_section = false; + let mut current_param: Option = None; + let mut current_doc = String::new(); + + for line_obj in docstring.universal_newlines() { + let line = line_obj.as_str(); + if GOOGLE_SECTION_REGEX.is_match(line) { + in_args_section = true; + continue; + } + + if in_args_section { + // Check if we hit another section (starts with a word followed by colon at line start) + if !line.starts_with(' ') && !line.starts_with('\t') && line.contains(':') { + if let Some(colon_pos) = line.find(':') { + let section_name = line[..colon_pos].trim(); + // If this looks like another section, stop processing args + if !section_name.is_empty() + && section_name + .chars() + .all(|c| c.is_alphabetic() || c.is_whitespace()) + { + // Check if this is a known section name + let known_sections = [ + "Returns", "Return", "Raises", "Yields", "Yield", "Examples", + "Example", "Note", "Notes", "Warning", "Warnings", + ]; + if known_sections.contains(§ion_name) { + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + in_args_section = false; + continue; + } + } + } + } + + if let Some(captures) = GOOGLE_PARAM_REGEX.captures(line) { + // Save previous parameter if exists + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + + // Start new parameter + if let (Some(param), Some(desc)) = (captures.get(1), captures.get(3)) { + current_param = Some(param.as_str().to_string()); + current_doc = desc.as_str().to_string(); + } + } else if line.starts_with(' ') || line.starts_with('\t') { + // This is a continuation of the current parameter documentation + if current_param.is_some() { + if !current_doc.is_empty() { + current_doc.push('\n'); + } + current_doc.push_str(line.trim()); + } + } else { + // This is a line that doesn't start with whitespace and isn't a parameter + // It might be a section or other content, so stop processing args + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + in_args_section = false; + } + } + } + + // Don't forget the last parameter + if let Some(param_name) = current_param { + param_docs.insert(param_name, current_doc.trim().to_string()); + } + + param_docs +} + +/// Calculate the indentation level of a line (number of leading whitespace characters) +fn get_indentation_level(line: &str) -> usize { + leading_indentation(line).len() +} + +/// Extract parameter documentation from NumPy-style docstrings. +fn extract_numpy_style_params(docstring: &str) -> HashMap { + let mut param_docs = HashMap::new(); + + let mut lines = docstring + .universal_newlines() + .map(|line| line.as_str()) + .peekable(); + let mut in_params_section = false; + let mut found_underline = false; + let mut current_param: Option = None; + let mut current_doc = String::new(); + let mut base_param_indent: Option = None; + let mut base_content_indent: Option = None; + + while let Some(line) = lines.next() { + if NUMPY_SECTION_REGEX.is_match(line) { + // Check if the next line is an underline + if let Some(next_line) = lines.peek() { + if NUMPY_UNDERLINE_REGEX.is_match(next_line) { + in_params_section = true; + found_underline = false; + base_param_indent = None; + base_content_indent = None; + continue; + } + } + } + + if in_params_section && !found_underline { + if NUMPY_UNDERLINE_REGEX.is_match(line) { + found_underline = true; + continue; + } + } + + if in_params_section && found_underline { + let current_indent = get_indentation_level(line); + let trimmed = line.trim(); + + // Skip empty lines + if trimmed.is_empty() { + continue; + } + + // Check if we hit another section + if current_indent == 0 { + if let Some(next_line) = lines.peek() { + if NUMPY_UNDERLINE_REGEX.is_match(next_line) { + // This is another section + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + in_params_section = false; + continue; + } + } + } + + // Determine if this could be a parameter line + let could_be_param = if let Some(base_indent) = base_param_indent { + // We've seen parameters before - check if this matches the expected parameter indentation + current_indent == base_indent + } else { + // First potential parameter - check if it has reasonable indentation and content + current_indent > 0 + && (trimmed.contains(':') + || trimmed.chars().all(|c| c.is_alphanumeric() || c == '_')) + }; + + if could_be_param { + // Check if this could be a section header by looking at the next line + if let Some(next_line) = lines.peek() { + if NUMPY_UNDERLINE_REGEX.is_match(next_line) { + // This is a section header, not a parameter + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + in_params_section = false; + continue; + } + } + + // Set base indentation levels on first parameter + if base_param_indent.is_none() { + base_param_indent = Some(current_indent); + } + + // Handle parameter with type annotation (param : type) + if trimmed.contains(':') { + // Save previous parameter if exists + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + + // Extract parameter name and description + let parts: Vec<&str> = trimmed.splitn(2, ':').collect(); + if parts.len() == 2 { + let param_name = parts[0].trim(); + + // Extract just the parameter name (before any type info) + let param_name = param_name.split_whitespace().next().unwrap_or(param_name); + current_param = Some(param_name.to_string()); + current_doc.clear(); // Description comes on following lines, not on this line + } + } else { + // Handle parameter without type annotation + // Save previous parameter if exists + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + + // This line is the parameter name + current_param = Some(trimmed.to_string()); + current_doc.clear(); + } + } else if current_param.is_some() { + // Determine if this is content for the current parameter + let is_content = if let Some(base_content) = base_content_indent { + // We've seen content before - check if this matches expected content indentation + current_indent >= base_content + } else { + // First potential content line - should be more indented than parameter + if let Some(base_param) = base_param_indent { + current_indent > base_param + } else { + // Fallback: any indented content + current_indent > 0 + } + }; + + if is_content { + // Set base content indentation on first content line + if base_content_indent.is_none() { + base_content_indent = Some(current_indent); + } + + // This is a continuation of the current parameter documentation + if !current_doc.is_empty() { + current_doc.push('\n'); + } + current_doc.push_str(trimmed); + } else { + // This line doesn't match our expected indentation patterns + // Save current parameter and stop processing + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + in_params_section = false; + } + } + } + } + + // Don't forget the last parameter + if let Some(param_name) = current_param { + param_docs.insert(param_name, current_doc.trim().to_string()); + } + + param_docs +} + +/// Extract parameter documentation from reST/Sphinx-style docstrings. +fn extract_rest_style_params(docstring: &str) -> HashMap { + let mut param_docs = HashMap::new(); + + let mut current_param: Option = None; + let mut current_doc = String::new(); + + for line_obj in docstring.universal_newlines() { + let line = line_obj.as_str(); + if let Some(captures) = REST_PARAM_REGEX.captures(line) { + // Save previous parameter if exists + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + + // Extract parameter name and description + if let (Some(param_match), Some(desc_match)) = (captures.get(2), captures.get(3)) { + current_param = Some(param_match.as_str().to_string()); + current_doc = desc_match.as_str().to_string(); + } + } else if current_param.is_some() { + let trimmed = line.trim(); + + // Check if this is a new section - stop processing if we hit section headers + if trimmed == "Parameters" || trimmed == "Args" || trimmed == "Arguments" { + // Save current param and stop processing + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + break; + } + + // Check if this is another directive line starting with ':' + if trimmed.starts_with(':') { + // This is a new directive, save current param + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + // Let the next iteration handle this directive + continue; + } + + // Check if this is a continuation line (indented) + if line.starts_with(" ") && !trimmed.is_empty() { + // This is a continuation line + if !current_doc.is_empty() { + current_doc.push('\n'); + } + current_doc.push_str(trimmed); + } else if !trimmed.is_empty() && !line.starts_with(' ') && !line.starts_with('\t') { + // This is a non-indented line - likely end of the current parameter + if let Some(param_name) = current_param.take() { + param_docs.insert(param_name, current_doc.trim().to_string()); + current_doc.clear(); + } + break; + } + } + } + + // Don't forget the last parameter + if let Some(param_name) = current_param { + param_docs.insert(param_name, current_doc.trim().to_string()); + } + + param_docs +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_google_style_parameter_documentation() { + let docstring = r#" + This is a function description. + + Args: + param1 (str): The first parameter description + param2 (int): The second parameter description + This is a continuation of param2 description. + param3: A parameter without type annotation + + Returns: + str: The return value description + "#; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 3); + assert_eq!(¶m_docs["param1"], "The first parameter description"); + assert_eq!( + ¶m_docs["param2"], + "The second parameter description\nThis is a continuation of param2 description." + ); + assert_eq!(¶m_docs["param3"], "A parameter without type annotation"); + } + + #[test] + fn test_numpy_style_parameter_documentation() { + let docstring = r#" + This is a function description. + + Parameters + ---------- + param1 : str + The first parameter description + param2 : int + The second parameter description + This is a continuation of param2 description. + param3 + A parameter without type annotation + + Returns + ------- + str + The return value description + "#; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 3); + assert_eq!( + param_docs.get("param1").expect("param1 should exist"), + "The first parameter description" + ); + assert_eq!( + param_docs.get("param2").expect("param2 should exist"), + "The second parameter description\nThis is a continuation of param2 description." + ); + assert_eq!( + param_docs.get("param3").expect("param3 should exist"), + "A parameter without type annotation" + ); + } + + #[test] + fn test_no_parameter_documentation() { + let docstring = r#" + This is a simple function description without parameter documentation. + "#; + + let param_docs = get_parameter_documentation(docstring); + assert!(param_docs.is_empty()); + } + + #[test] + fn test_mixed_style_parameter_documentation() { + let docstring = r#" + This is a function description. + + Args: + param1 (str): Google-style parameter + param2 (int): Another Google-style parameter + + Parameters + ---------- + param3 : bool + NumPy-style parameter + "#; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 3); + assert_eq!( + param_docs.get("param1").expect("param1 should exist"), + "Google-style parameter" + ); + assert_eq!( + param_docs.get("param2").expect("param2 should exist"), + "Another Google-style parameter" + ); + assert_eq!( + param_docs.get("param3").expect("param3 should exist"), + "NumPy-style parameter" + ); + } + + #[test] + fn test_rest_style_parameter_documentation() { + let docstring = r#" + This is a function description. + + :param str param1: The first parameter description + :param int param2: The second parameter description + This is a continuation of param2 description. + :param param3: A parameter without type annotation + :returns: The return value description + :rtype: str + "#; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 3); + assert_eq!( + param_docs.get("param1").expect("param1 should exist"), + "The first parameter description" + ); + assert_eq!( + param_docs.get("param2").expect("param2 should exist"), + "The second parameter description\nThis is a continuation of param2 description." + ); + assert_eq!( + param_docs.get("param3").expect("param3 should exist"), + "A parameter without type annotation" + ); + } + + #[test] + fn test_mixed_style_with_rest_parameter_documentation() { + let docstring = r#" + This is a function description. + + Args: + param1 (str): Google-style parameter + + :param int param2: reST-style parameter + :param param3: Another reST-style parameter + + Parameters + ---------- + param4 : bool + NumPy-style parameter + "#; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 4); + assert_eq!( + param_docs.get("param1").expect("param1 should exist"), + "Google-style parameter" + ); + assert_eq!( + param_docs.get("param2").expect("param2 should exist"), + "reST-style parameter" + ); + assert_eq!( + param_docs.get("param3").expect("param3 should exist"), + "Another reST-style parameter" + ); + assert_eq!( + param_docs.get("param4").expect("param4 should exist"), + "NumPy-style parameter" + ); + } + + #[test] + fn test_numpy_style_with_different_indentation() { + let docstring = r#" + This is a function description. + + Parameters + ---------- + param1 : str + The first parameter description + param2 : int + The second parameter description + This is a continuation of param2 description. + param3 + A parameter without type annotation + + Returns + ------- + str + The return value description + "#; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 3); + assert_eq!( + param_docs.get("param1").expect("param1 should exist"), + "The first parameter description" + ); + assert_eq!( + param_docs.get("param2").expect("param2 should exist"), + "The second parameter description\nThis is a continuation of param2 description." + ); + assert_eq!( + param_docs.get("param3").expect("param3 should exist"), + "A parameter without type annotation" + ); + } + + #[test] + fn test_numpy_style_with_tabs_and_mixed_indentation() { + // Using raw strings to avoid tab/space conversion issues in the test + let docstring = " + This is a function description. + + Parameters + ---------- +\tparam1 : str +\t\tThe first parameter description +\tparam2 : int +\t\tThe second parameter description +\t\tThis is a continuation of param2 description. +\tparam3 +\t\tA parameter without type annotation + "; + + let param_docs = get_parameter_documentation(docstring); + + assert_eq!(param_docs.len(), 3); + assert_eq!( + param_docs.get("param1").expect("param1 should exist"), + "The first parameter description" + ); + assert_eq!( + param_docs.get("param2").expect("param2 should exist"), + "The second parameter description\nThis is a continuation of param2 description." + ); + assert_eq!( + param_docs.get("param3").expect("param3 should exist"), + "A parameter without type annotation" + ); + } + + #[test] + fn test_universal_newlines() { + // Test with Windows-style line endings (\r\n) + let docstring_windows = "This is a function description.\r\n\r\nArgs:\r\n param1 (str): The first parameter\r\n param2 (int): The second parameter\r\n"; + + // Test with old Mac-style line endings (\r) + let docstring_mac = "This is a function description.\r\rArgs:\r param1 (str): The first parameter\r param2 (int): The second parameter\r"; + + // Test with Unix-style line endings (\n) - should work the same + let docstring_unix = "This is a function description.\n\nArgs:\n param1 (str): The first parameter\n param2 (int): The second parameter\n"; + + let param_docs_windows = get_parameter_documentation(docstring_windows); + let param_docs_mac = get_parameter_documentation(docstring_mac); + let param_docs_unix = get_parameter_documentation(docstring_unix); + + // All should produce the same results + assert_eq!(param_docs_windows.len(), 2); + assert_eq!(param_docs_mac.len(), 2); + assert_eq!(param_docs_unix.len(), 2); + + assert_eq!( + param_docs_windows.get("param1"), + Some(&"The first parameter".to_string()) + ); + assert_eq!( + param_docs_mac.get("param1"), + Some(&"The first parameter".to_string()) + ); + assert_eq!( + param_docs_unix.get("param1"), + Some(&"The first parameter".to_string()) + ); + } +} diff --git a/crates/ty_ide/src/find_node.rs b/crates/ty_ide/src/find_node.rs new file mode 100644 index 0000000000000..7e8dec4b1b575 --- /dev/null +++ b/crates/ty_ide/src/find_node.rs @@ -0,0 +1,129 @@ +use ruff_python_ast::AnyNodeRef; +use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal}; +use ruff_text_size::{Ranged, TextRange}; +use std::fmt; +use std::fmt::Formatter; + +/// Returns the node with a minimal range that fully contains `range`. +/// +/// If `range` is empty and falls within a parser *synthesized* node generated during error recovery, +/// then the first node with the given range is returned. +/// +/// ## Panics +/// Panics if `range` is not contained within `root`. +pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode { + struct Visitor<'a> { + range: TextRange, + found: bool, + ancestors: Vec>, + } + + impl<'a> SourceOrderVisitor<'a> for Visitor<'a> { + fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal { + // If the node fully contains the range, than it is a possible match but traverse into its children + // to see if there's a node with a narrower range. + if !self.found && node.range().contains_range(self.range) { + self.ancestors.push(node); + TraversalSignal::Traverse + } else { + TraversalSignal::Skip + } + } + + fn leave_node(&mut self, node: AnyNodeRef<'a>) { + if !self.found && self.ancestors.last() == Some(&node) { + self.found = true; + } + } + } + + assert!( + root.range().contains_range(range), + "Range is not contained within root" + ); + + let mut visitor = Visitor { + range, + found: false, + ancestors: Vec::new(), + }; + + root.visit_source_order(&mut visitor); + if visitor.ancestors.is_empty() { + visitor.ancestors.push(root); + } + CoveringNode { + nodes: visitor.ancestors, + } +} + +/// The node with a minimal range that fully contains the search range. +pub(crate) struct CoveringNode<'a> { + /// The covering node, along with all of its ancestors up to the + /// root. The root is always the first element and the covering + /// node found is always the last node. This sequence is guaranteed + /// to be non-empty. + nodes: Vec>, +} + +impl<'a> CoveringNode<'a> { + /// Returns the covering node found. + pub(crate) fn node(&self) -> AnyNodeRef<'a> { + *self + .nodes + .last() + .expect("`CoveringNode::nodes` should always be non-empty") + } + + /// Returns the node's parent. + pub(crate) fn parent(&self) -> Option> { + let penultimate = self.nodes.len().checked_sub(2)?; + self.nodes.get(penultimate).copied() + } + + /// Finds the first node that fully covers the range and fulfills + /// the given predicate. + /// + /// The "first" here means that the node closest to a leaf is + /// returned. + pub(crate) fn find_first(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result { + let Some(index) = self.find_first_index(f) else { + return Err(self); + }; + self.nodes.truncate(index + 1); + Ok(self) + } + + /// Finds the last node that fully covers the range and fulfills + /// the given predicate. + /// + /// The "last" here means that after finding the "first" such node, + /// the highest ancestor found satisfying the given predicate is + /// returned. Note that this is *not* the same as finding the node + /// closest to the root that satisfies the given predictate. + pub(crate) fn find_last(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result { + let Some(mut index) = self.find_first_index(&f) else { + return Err(self); + }; + while index > 0 && f(self.nodes[index - 1]) { + index -= 1; + } + self.nodes.truncate(index + 1); + Ok(self) + } + + /// Finds the index of the node that fully covers the range and + /// fulfills the given predicate. + /// + /// If there are no nodes matching the given predictate, then + /// `None` is returned. + fn find_first_index(&self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Option { + self.nodes.iter().rposition(|node| f(*node)) + } +} + +impl fmt::Debug for CoveringNode<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("CoveringNode").field(&self.node()).finish() + } +} diff --git a/crates/red_knot_ide/src/goto.rs b/crates/ty_ide/src/goto.rs similarity index 77% rename from crates/red_knot_ide/src/goto.rs rename to crates/ty_ide/src/goto.rs index ef8030141e9dc..e75a43d37f723 100644 --- a/crates/red_knot_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -1,28 +1,25 @@ use crate::find_node::covering_node; use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::{HasType, SemanticModel}; use ruff_db::files::{File, FileRange}; -use ruff_db::parsed::{parsed_module, ParsedModule}; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::types::Type; +use ty_python_semantic::{HasType, SemanticModel}; pub fn goto_type_definition( db: &dyn Db, file: File, offset: TextSize, ) -> Option> { - let parsed = parsed_module(db.upcast(), file); - let goto_target = find_goto_target(parsed, offset)?; + let module = parsed_module(db, file).load(db); + let goto_target = find_goto_target(&module, offset)?; - let model = SemanticModel::new(db.upcast(), file); + let model = SemanticModel::new(db, file); let ty = goto_target.inferred_type(&model)?; - tracing::debug!( - "Inferred type of covering node is {}", - ty.display(db.upcast()) - ); + tracing::debug!("Inferred type of covering node is {}", ty.display(db)); let navigation_targets = ty.navigation_targets(db); @@ -128,8 +125,8 @@ pub(crate) enum GotoTarget<'a> { }, } -impl<'db> GotoTarget<'db> { - pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option> { +impl GotoTarget<'_> { + pub(crate) fn inferred_type<'db>(self, model: &SemanticModel<'db>) -> Option> { let ty = match self { GotoTarget::Expression(expression) => expression.inferred_type(model), GotoTarget::FunctionDef(function) => function.inferred_type(model), @@ -183,7 +180,10 @@ impl Ranged for GotoTarget<'_> { } } -pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option { +pub(crate) fn find_goto_target( + parsed: &ParsedModuleRef, + offset: TextSize, +) -> Option> { let token = parsed .tokens() .at_offset(offset) @@ -197,7 +197,7 @@ pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Optio })?; let covering_node = covering_node(parsed.syntax().into(), token.range()) - .find(|node| node.is_identifier() || node.is_expression()) + .find_first(|node| node.is_identifier() || node.is_expression()) .ok()?; tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind()); @@ -253,8 +253,8 @@ pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Optio #[cfg(test)] mod tests { - use crate::tests::{cursor_test, CursorTest, IntoDiagnostic}; - use crate::{goto_type_definition, NavigationTarget}; + use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; + use crate::{NavigationTarget, goto_type_definition}; use insta::assert_snapshot; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, @@ -272,9 +272,9 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" - info: lint:goto-type-definition: Type definition - --> /main.py:2:19 + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:19 | 2 | class Test: ... | ^^^^ @@ -282,14 +282,14 @@ mod tests { 4 | ab = Test() | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | class Test: ... 3 | 4 | ab = Test() | ^^ | - "###); + "); } #[test] @@ -304,9 +304,9 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" - info: lint:goto-type-definition: Type definition - --> /main.py:2:17 + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:17 | 2 | def foo(a, b): ... | ^^^ @@ -314,14 +314,14 @@ mod tests { 4 | ab = foo | info: Source - --> /main.py:6:13 + --> main.py:6:13 | 4 | ab = foo 5 | 6 | ab | ^^ | - "###); + "); } #[test] @@ -343,8 +343,8 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r" - info: lint:goto-type-definition: Type definition - --> /main.py:3:17 + info[goto-type-definition]: Type definition + --> main.py:3:17 | 3 | def foo(a, b): ... | ^^^ @@ -352,7 +352,7 @@ mod tests { 5 | def bar(a, b): ... | info: Source - --> /main.py:12:13 + --> main.py:12:13 | 10 | a = bar 11 | @@ -360,8 +360,8 @@ mod tests { | ^ | - info: lint:goto-type-definition: Type definition - --> /main.py:5:17 + info[goto-type-definition]: Type definition + --> main.py:5:17 | 3 | def foo(a, b): ... 4 | @@ -371,7 +371,7 @@ mod tests { 7 | if random.choice(): | info: Source - --> /main.py:12:13 + --> main.py:12:13 | 10 | a = bar 11 | @@ -394,14 +394,14 @@ mod tests { test.write_file("lib.py", "a = 10").unwrap(); assert_snapshot!(test.goto_type_definition(), @r" - info: lint:goto-type-definition: Type definition - --> /lib.py:1:1 + info[goto-type-definition]: Type definition + --> lib.py:1:1 | 1 | a = 10 | ^^^^^^ | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | import lib 3 | @@ -422,18 +422,18 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:438:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:461:7 | - 436 | def __getitem__(self, key: int, /) -> str | int | None: ... - 437 | - 438 | class str(Sequence[str]): + 459 | def __getitem__(self, key: int, /) -> str | int | None: ... + 460 | + 461 | class str(Sequence[str]): | ^^^ - 439 | @overload - 440 | def __new__(cls, object: object = ...) -> Self: ... + 462 | @overload + 463 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | a: str = "test" 3 | @@ -451,18 +451,18 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:438:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:461:7 | - 436 | def __getitem__(self, key: int, /) -> str | int | None: ... - 437 | - 438 | class str(Sequence[str]): + 459 | def __getitem__(self, key: int, /) -> str | int | None: ... + 460 | + 461 | class str(Sequence[str]): | ^^^ - 439 | @overload - 440 | def __new__(cls, object: object = ...) -> Self: ... + 462 | @overload + 463 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:2:22 + --> main.py:2:22 | 2 | a: str = "test" | ^^^^^^ @@ -478,20 +478,20 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" - info: lint:goto-type-definition: Type definition - --> /main.py:2:24 + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:24 | 2 | type Alias[T: int = bool] = list[T] | ^ | info: Source - --> /main.py:2:46 + --> main.py:2:46 | 2 | type Alias[T: int = bool] = list[T] | ^ | - "###); + "); } #[test] @@ -522,6 +522,40 @@ mod tests { assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); } + #[test] + fn goto_type_of_bare_type_alias_type() { + let test = cursor_test( + r#" + from typing_extensions import TypeAliasType + + Alias = TypeAliasType("Alias", tuple[int, int]) + + Alias + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> main.py:4:13 + | + 2 | from typing_extensions import TypeAliasType + 3 | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + | ^^^^^ + 5 | + 6 | Alias + | + info: Source + --> main.py:6:13 + | + 4 | Alias = TypeAliasType("Alias", tuple[int, int]) + 5 | + 6 | Alias + | ^^^^^ + | + "#); + } + #[test] fn goto_type_on_keyword_argument() { let test = cursor_test( @@ -533,18 +567,18 @@ mod tests { ); assert_snapshot!(test.goto_type_definition(), @r#" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:438:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:461:7 | - 436 | def __getitem__(self, key: int, /) -> str | int | None: ... - 437 | - 438 | class str(Sequence[str]): + 459 | def __getitem__(self, key: int, /) -> str | int | None: ... + 460 | + 461 | class str(Sequence[str]): | ^^^ - 439 | @overload - 440 | def __new__(cls, object: object = ...) -> Self: ... + 462 | @overload + 463 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:4:18 + --> main.py:4:18 | 2 | def test(a: str): ... 3 | @@ -568,18 +602,18 @@ mod tests { // the keyword is typed as a string. It's only the passed argument that // is an int. Navigating to `str` would match pyright's behavior. assert_snapshot!(test.goto_type_definition(), @r" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:231:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:244:7 | - 229 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed - 230 | - 231 | class int: + 242 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed + 243 | + 244 | class int: | ^^^ - 232 | @overload - 233 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ... + 245 | @overload + 246 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ... | info: Source - --> /main.py:4:18 + --> main.py:4:18 | 2 | def test(a: str): ... 3 | @@ -602,18 +636,18 @@ f(**kwargs) ); assert_snapshot!(test.goto_type_definition(), @r#" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:1086:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:1136:7 | - 1084 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - 1085 | - 1086 | class dict(MutableMapping[_KT, _VT]): + 1134 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + 1135 | + 1136 | class dict(MutableMapping[_KT, _VT]): | ^^^^ - 1087 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics - 1088 | # Also multiprocessing.managers.SyncManager.dict() + 1137 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics + 1138 | # Also multiprocessing.managers.SyncManager.dict() | info: Source - --> /main.py:6:5 + --> main.py:6:5 | 4 | kwargs = { "name": "test"} 5 | @@ -633,18 +667,18 @@ f(**kwargs) ); assert_snapshot!(test.goto_type_definition(), @r" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:438:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:461:7 | - 436 | def __getitem__(self, key: int, /) -> str | int | None: ... - 437 | - 438 | class str(Sequence[str]): + 459 | def __getitem__(self, key: int, /) -> str | int | None: ... + 460 | + 461 | class str(Sequence[str]): | ^^^ - 439 | @overload - 440 | def __new__(cls, object: object = ...) -> Self: ... + 462 | @overload + 463 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: str): 3 | a @@ -666,23 +700,23 @@ f(**kwargs) "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" - info: lint:goto-type-definition: Type definition - --> /main.py:2:19 + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:19 | 2 | class X: | ^ 3 | def foo(a, b): ... | info: Source - --> /main.py:7:13 + --> main.py:7:13 | 5 | x = X() 6 | 7 | x.foo() | ^ | - "###); + "); } #[test] @@ -695,9 +729,9 @@ f(**kwargs) "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" - info: lint:goto-type-definition: Type definition - --> /main.py:2:17 + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:17 | 2 | def foo(a, b): ... | ^^^ @@ -705,14 +739,14 @@ f(**kwargs) 4 | foo() | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | def foo(a, b): ... 3 | 4 | foo() | ^^^ | - "###); + "); } #[test] @@ -726,18 +760,18 @@ f(**kwargs) ); assert_snapshot!(test.goto_type_definition(), @r" - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:438:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:461:7 | - 436 | def __getitem__(self, key: int, /) -> str | int | None: ... - 437 | - 438 | class str(Sequence[str]): + 459 | def __getitem__(self, key: int, /) -> str | int | None: ... + 460 | + 461 | class str(Sequence[str]): | ^^^ - 439 | @overload - 440 | def __new__(cls, object: object = ...) -> Self: ... + 462 | @overload + 463 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:4:27 + --> main.py:4:27 | 2 | def foo(a: str | None, b): 3 | if a is not None: @@ -757,35 +791,35 @@ f(**kwargs) ); assert_snapshot!(test.goto_type_definition(), @r" - info: lint:goto-type-definition: Type definition - --> stdlib/types.pyi:671:11 + info[goto-type-definition]: Type definition + --> stdlib/types.pyi:691:11 | - 669 | if sys.version_info >= (3, 10): - 670 | @final - 671 | class NoneType: + 689 | if sys.version_info >= (3, 10): + 690 | @final + 691 | class NoneType: | ^^^^^^^^ - 672 | def __bool__(self) -> Literal[False]: ... + 692 | def __bool__(self) -> Literal[False]: ... | info: Source - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: str | None, b): 3 | a | ^ | - info: lint:goto-type-definition: Type definition - --> stdlib/builtins.pyi:438:7 + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:461:7 | - 436 | def __getitem__(self, key: int, /) -> str | int | None: ... - 437 | - 438 | class str(Sequence[str]): + 459 | def __getitem__(self, key: int, /) -> str | int | None: ... + 460 | + 461 | class str(Sequence[str]): | ^^^ - 439 | @overload - 440 | def __new__(cls, object: object = ...) -> Self: ... + 462 | @overload + 463 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: str | None, b): 3 | a @@ -796,7 +830,8 @@ f(**kwargs) impl CursorTest { fn goto_type_definition(&self) -> String { - let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset) + let Some(targets) = + goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs new file mode 100644 index 0000000000000..024422af3bf85 --- /dev/null +++ b/crates/ty_ide/src/hover.rs @@ -0,0 +1,778 @@ +use crate::goto::{GotoTarget, find_goto_target}; +use crate::{Db, MarkupKind, RangedValue}; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::parsed_module; +use ruff_text_size::{Ranged, TextSize}; +use std::fmt; +use std::fmt::Formatter; +use ty_python_semantic::SemanticModel; +use ty_python_semantic::types::Type; + +pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option>> { + let parsed = parsed_module(db, file).load(db); + let goto_target = find_goto_target(&parsed, offset)?; + + if let GotoTarget::Expression(expr) = goto_target { + if expr.is_literal_expr() { + return None; + } + } + + let model = SemanticModel::new(db, file); + let ty = goto_target.inferred_type(&model)?; + + tracing::debug!("Inferred type of covering node is {}", ty.display(db)); + + // TODO: Add documentation of the symbol (not the type's definition). + // TODO: Render the symbol's signature instead of just its type. + let contents = vec![HoverContent::Type(ty)]; + + Some(RangedValue { + range: FileRange::new(file, goto_target.range()), + value: Hover { contents }, + }) +} + +pub struct Hover<'db> { + contents: Vec>, +} + +impl<'db> Hover<'db> { + /// Renders the hover to a string using the specified markup kind. + pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> { + DisplayHover { + db, + hover: self, + kind, + } + } + + fn iter(&self) -> std::slice::Iter<'_, HoverContent<'db>> { + self.contents.iter() + } +} + +impl<'db> IntoIterator for Hover<'db> { + type Item = HoverContent<'db>; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.contents.into_iter() + } +} + +impl<'a, 'db> IntoIterator for &'a Hover<'db> { + type Item = &'a HoverContent<'db>; + type IntoIter = std::slice::Iter<'a, HoverContent<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct DisplayHover<'a> { + db: &'a dyn Db, + hover: &'a Hover<'a>, + kind: MarkupKind, +} + +impl fmt::Display for DisplayHover<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut first = true; + for content in &self.hover.contents { + if !first { + self.kind.horizontal_line().fmt(f)?; + } + + content.display(self.db, self.kind).fmt(f)?; + first = false; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum HoverContent<'db> { + Type(Type<'db>), +} + +impl<'db> HoverContent<'db> { + fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> { + DisplayHoverContent { + db, + content: self, + kind, + } + } +} + +pub(crate) struct DisplayHoverContent<'a, 'db> { + db: &'db dyn Db, + content: &'a HoverContent<'db>, + kind: MarkupKind, +} + +impl fmt::Display for DisplayHoverContent<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.content { + HoverContent::Type(ty) => self + .kind + .fenced_code_block(ty.display(self.db), "python") + .fmt(f), + } + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{CursorTest, cursor_test}; + use crate::{MarkupKind, hover}; + use insta::assert_snapshot; + use ruff_db::diagnostic::{ + Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName, + Severity, Span, + }; + use ruff_text_size::{Ranged, TextRange}; + + #[test] + fn hover_basic() { + let test = cursor_test( + r#" + a = 10 + + a + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[10] + --------------------------------------------- + ```python + Literal[10] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:9 + | + 2 | a = 10 + 3 | + 4 | a + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_member() { + let test = cursor_test( + r#" + class Foo: + a: int = 10 + + def __init__(a: int, b: str): + self.a = a + self.b: str = b + + foo = Foo() + foo.a + "#, + ); + + assert_snapshot!(test.hover(), @r" + int + --------------------------------------------- + ```python + int + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:10:9 + | + 9 | foo = Foo() + 10 | foo.a + | ^^^^- + | | | + | | Cursor offset + | source + | + "); + } + + #[test] + fn hover_function_typed_variable() { + let test = cursor_test( + r#" + def foo(a, b): ... + + foo + "#, + ); + + assert_snapshot!(test.hover(), @r" + def foo(a, b) -> Unknown + --------------------------------------------- + ```python + def foo(a, b) -> Unknown + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:13 + | + 2 | def foo(a, b): ... + 3 | + 4 | foo + | ^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_binary_expression() { + let test = cursor_test( + r#" + def foo(a: int, b: int, c: int): + a + b == c + "#, + ); + + assert_snapshot!(test.hover(), @r" + bool + --------------------------------------------- + ```python + bool + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:17 + | + 2 | def foo(a: int, b: int, c: int): + 3 | a + b == c + | ^^^^^^^^-^ + | | | + | | Cursor offset + | source + | + "); + } + + #[test] + fn hover_keyword_parameter() { + let test = cursor_test( + r#" + def test(a: int): ... + + test(a= 123) + "#, + ); + + // TODO: This should reveal `int` because the user hovers over the parameter and not the value. + assert_snapshot!(test.hover(), @r" + Literal[123] + --------------------------------------------- + ```python + Literal[123] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:18 + | + 2 | def test(a: int): ... + 3 | + 4 | test(a= 123) + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_union() { + let test = cursor_test( + r#" + + def foo(a, b): ... + + def bar(a, b): ... + + if random.choice([True, False]): + a = foo + else: + a = bar + + a + "#, + ); + + assert_snapshot!(test.hover(), @r" + (def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown) + --------------------------------------------- + ```python + (def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:12:13 + | + 10 | a = bar + 11 | + 12 | a + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_module() { + let mut test = cursor_test( + r#" + import lib + + lib + "#, + ); + + test.write_file("lib.py", "a = 10").unwrap(); + + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + ```python + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:13 + | + 2 | import lib + 3 | + 4 | lib + | ^^- + | | | + | | Cursor offset + | source + | + "); + } + + #[test] + fn hover_type_of_expression_with_type_var_type() { + let test = cursor_test( + r#" + type Alias[T: int = bool] = list[T] + "#, + ); + + assert_snapshot!(test.hover(), @r" + T + --------------------------------------------- + ```python + T + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:46 + | + 2 | type Alias[T: int = bool] = list[T] + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_type_of_expression_with_type_param_spec() { + let test = cursor_test( + r#" + type Alias[**P = [int, str]] = Callable[P, int] + "#, + ); + + assert_snapshot!(test.hover(), @r" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:53 + | + 2 | type Alias[**P = [int, str]] = Callable[P, int] + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_type_of_expression_with_type_var_tuple() { + let test = cursor_test( + r#" + type Alias[*Ts = ()] = tuple[*Ts] + "#, + ); + + assert_snapshot!(test.hover(), @r" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:43 + | + 2 | type Alias[*Ts = ()] = tuple[*Ts] + | ^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_variable_assignment() { + let test = cursor_test( + r#" + value = 1 + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[1] + --------------------------------------------- + ```python + Literal[1] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:13 + | + 2 | value = 1 + | ^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_augmented_assignment() { + let test = cursor_test( + r#" + value = 1 + value += 2 + "#, + ); + + // We currently show the *previous* value of the variable (1), not the new one (3). + // Showing the new value might be more intuitive for some users, but the actual 'use' + // of the `value` symbol here in read-context is `1`. This comment mainly exists to + // signal that it might be okay to revisit this in the future and reveal 3 instead. + assert_snapshot!(test.hover(), @r" + Literal[1] + --------------------------------------------- + ```python + Literal[1] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:13 + | + 2 | value = 1 + 3 | value += 2 + | ^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_attribute_assignment() { + let test = cursor_test( + r#" + class C: + attr: int = 1 + + C.attr = 2 + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[2] + --------------------------------------------- + ```python + Literal[2] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:5:13 + | + 3 | attr: int = 1 + 4 | + 5 | C.attr = 2 + | ^^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_augmented_attribute_assignment() { + let test = cursor_test( + r#" + class C: + attr = 1 + + C.attr += 2 + "#, + ); + + // See the comment in the `hover_augmented_assignment` test above. The same + // reasoning applies here. + assert_snapshot!(test.hover(), @r" + Unknown | Literal[1] + --------------------------------------------- + ```python + Unknown | Literal[1] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:5:13 + | + 3 | attr = 1 + 4 | + 5 | C.attr += 2 + | ^^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_annotated_assignment() { + let test = cursor_test( + r#" + class Foo: + a: int + "#, + ); + + assert_snapshot!(test.hover(), @r" + int + --------------------------------------------- + ```python + int + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:13 + | + 2 | class Foo: + 3 | a: int + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_annotated_assignment_with_rhs() { + let test = cursor_test( + r#" + class Foo: + a: int = 1 + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[1] + --------------------------------------------- + ```python + Literal[1] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:13 + | + 2 | class Foo: + 3 | a: int = 1 + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_annotated_attribute_assignment() { + let test = cursor_test( + r#" + class Foo: + def __init__(self, a: int): + self.a: int = a + "#, + ); + + assert_snapshot!(test.hover(), @r" + int + --------------------------------------------- + ```python + int + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:17 + | + 2 | class Foo: + 3 | def __init__(self, a: int): + 4 | self.a: int = a + | ^^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_type_narrowing() { + let test = cursor_test( + r#" + def foo(a: str | None, b): + if a is not None: + print(a) + "#, + ); + + assert_snapshot!(test.hover(), @r" + str + --------------------------------------------- + ```python + str + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:4:27 + | + 2 | def foo(a: str | None, b): + 3 | if a is not None: + 4 | print(a) + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_whitespace() { + let test = cursor_test( + r#" + class C: + + foo: str = 'bar' + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_literal_int() { + let test = cursor_test( + r#" + print( + 0 + 1 + ) + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_literal_ellipsis() { + let test = cursor_test( + r#" + print( + ... + ) + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_docstring() { + let test = cursor_test( + r#" + def f(): + """Lorem ipsum dolor sit amet.""" + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + impl CursorTest { + fn hover(&self) -> String { + use std::fmt::Write; + + let Some(hover) = hover(&self.db, self.cursor.file, self.cursor.offset) else { + return "Hover provided no content".to_string(); + }; + + let source = hover.range; + + let mut buf = String::new(); + + write!( + &mut buf, + "{plaintext}{line}{markdown}{line}", + plaintext = hover.display(&self.db, MarkupKind::PlainText), + line = MarkupKind::PlainText.horizontal_line(), + markdown = hover.display(&self.db, MarkupKind::Markdown), + ) + .unwrap(); + + let config = DisplayDiagnosticConfig::default() + .color(false) + .format(DiagnosticFormat::Full); + + let mut diagnostic = Diagnostic::new( + DiagnosticId::Lint(LintName::of("hover")), + Severity::Info, + "Hovered content is", + ); + diagnostic.annotate( + Annotation::primary(Span::from(source.file()).with_range(source.range())) + .message("source"), + ); + diagnostic.annotate( + Annotation::secondary( + Span::from(source.file()).with_range(TextRange::empty(self.cursor.offset)), + ) + .message("Cursor offset"), + ); + + write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap(); + + buf + } + } +} diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs new file mode 100644 index 0000000000000..670e83267f8bf --- /dev/null +++ b/crates/ty_ide/src/inlay_hints.rs @@ -0,0 +1,277 @@ +use crate::Db; +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; +use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use std::fmt; +use std::fmt::Formatter; +use ty_python_semantic::types::Type; +use ty_python_semantic::{HasType, SemanticModel}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct InlayHint<'db> { + pub position: TextSize, + pub content: InlayHintContent<'db>, +} + +impl<'db> InlayHint<'db> { + pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> { + self.content.display(db) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum InlayHintContent<'db> { + Type(Type<'db>), + ReturnType(Type<'db>), +} + +impl<'db> InlayHintContent<'db> { + pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> { + DisplayInlayHint { db, hint: self } + } +} + +pub struct DisplayInlayHint<'a, 'db> { + db: &'db dyn Db, + hint: &'a InlayHintContent<'db>, +} + +impl fmt::Display for DisplayInlayHint<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.hint { + InlayHintContent::Type(ty) => { + write!(f, ": {}", ty.display(self.db)) + } + InlayHintContent::ReturnType(ty) => { + write!(f, " -> {}", ty.display(self.db)) + } + } + } +} + +pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec> { + let mut visitor = InlayHintVisitor::new(db, file, range); + + let ast = parsed_module(db, file).load(db); + + visitor.visit_body(ast.suite()); + + visitor.hints +} + +struct InlayHintVisitor<'db> { + model: SemanticModel<'db>, + hints: Vec>, + in_assignment: bool, + range: TextRange, +} + +impl<'db> InlayHintVisitor<'db> { + fn new(db: &'db dyn Db, file: File, range: TextRange) -> Self { + Self { + model: SemanticModel::new(db, file), + hints: Vec::new(), + in_assignment: false, + range, + } + } + + fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) { + self.hints.push(InlayHint { + position, + content: InlayHintContent::Type(ty), + }); + } +} + +impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> { + fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal { + if self.range.intersect(node.range()).is_some() { + TraversalSignal::Traverse + } else { + TraversalSignal::Skip + } + } + + fn visit_stmt(&mut self, stmt: &Stmt) { + let node = AnyNodeRef::from(stmt); + + if !self.enter_node(node).is_traverse() { + return; + } + + match stmt { + Stmt::Assign(assign) => { + self.in_assignment = true; + for target in &assign.targets { + self.visit_expr(target); + } + self.in_assignment = false; + + return; + } + // TODO + Stmt::FunctionDef(_) => {} + Stmt::For(_) => {} + Stmt::Expr(_) => { + // Don't traverse into expression statements because we don't show any hints. + return; + } + _ => {} + } + + source_order::walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, expr: &'_ Expr) { + if !self.in_assignment { + return; + } + + match expr { + Expr::Name(name) => { + if name.ctx.is_store() { + let ty = expr.inferred_type(&self.model); + self.add_type_hint(expr.range().end(), ty); + } + } + _ => { + source_order::walk_expr(self, expr); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use insta::assert_snapshot; + use ruff_db::{ + Db as _, + files::{File, system_path_to_file}, + source::source_text, + }; + use ruff_text_size::TextSize; + + use crate::db::tests::TestDb; + + use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings, + }; + + pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest { + const START: &str = ""; + const END: &str = ""; + + let mut db = TestDb::new(); + + let start = source.find(START); + let end = source + .find(END) + .map(|x| if start.is_some() { x - START.len() } else { x }) + .unwrap_or(source.len()); + + let range = TextRange::new( + TextSize::try_from(start.unwrap_or_default()).unwrap(), + TextSize::try_from(end).unwrap(), + ); + + let source = source.replace(START, ""); + let source = source.replace(END, ""); + + db.write_file("main.py", source) + .expect("write to memory file system to be successful"); + + let file = system_path_to_file(&db, "main.py").expect("newly written file to existing"); + + let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")]) + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search path settings"); + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths, + }, + ); + + InlayHintTest { db, file, range } + } + + pub(super) struct InlayHintTest { + pub(super) db: TestDb, + pub(super) file: File, + pub(super) range: TextRange, + } + + impl InlayHintTest { + fn inlay_hints(&self) -> String { + let hints = inlay_hints(&self.db, self.file, self.range); + + let mut buf = source_text(&self.db, self.file).as_str().to_string(); + + let mut offset = 0; + + for hint in hints { + let end_position = (hint.position.to_u32() as usize) + offset; + let hint_str = format!("[{}]", hint.display(&self.db)); + buf.insert_str(end_position, &hint_str); + offset += hint_str.len(); + } + + buf + } + } + + #[test] + fn test_assign_statement() { + let test = inlay_hint_test("x = 1"); + + assert_snapshot!(test.inlay_hints(), @r" + x[: Literal[1]] = 1 + "); + } + + #[test] + fn test_tuple_assignment() { + let test = inlay_hint_test("x, y = (1, 'abc')"); + + assert_snapshot!(test.inlay_hints(), @r#" + x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc') + "#); + } + + #[test] + fn test_nested_tuple_assignment() { + let test = inlay_hint_test("x, (y, z) = (1, ('abc', 2))"); + + assert_snapshot!(test.inlay_hints(), @r#" + x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2)) + "#); + } + + #[test] + fn test_assign_statement_with_type_annotation() { + let test = inlay_hint_test("x: int = 1"); + + assert_snapshot!(test.inlay_hints(), @r" + x: int = 1 + "); + } + + #[test] + fn test_assign_statement_out_of_range() { + let test = inlay_hint_test("x = 1\ny = 2"); + + assert_snapshot!(test.inlay_hints(), @r" + x[: Literal[1]] = 1 + y = 2 + "); + } +} diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs new file mode 100644 index 0000000000000..b3706539cb092 --- /dev/null +++ b/crates/ty_ide/src/lib.rs @@ -0,0 +1,387 @@ +mod completion; +mod db; +mod docstring; +mod find_node; +mod goto; +mod hover; +mod inlay_hints; +mod markup; +mod semantic_tokens; +mod signature_help; + +pub use completion::completion; +pub use db::Db; +pub use docstring::get_parameter_documentation; +pub use goto::goto_type_definition; +pub use hover::hover; +pub use inlay_hints::inlay_hints; +pub use markup::MarkupKind; +pub use semantic_tokens::{ + SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens, +}; +pub use signature_help::{ParameterDetails, SignatureDetails, SignatureHelpInfo, signature_help}; + +use ruff_db::files::{File, FileRange}; +use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::FxHashSet; +use std::ops::{Deref, DerefMut}; +use ty_python_semantic::types::{Type, TypeDefinition}; + +/// Information associated with a text range. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct RangedValue { + pub range: FileRange, + pub value: T, +} + +impl RangedValue { + pub fn file_range(&self) -> FileRange { + self.range + } +} + +impl Deref for RangedValue { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl DerefMut for RangedValue { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +impl IntoIterator for RangedValue +where + T: IntoIterator, +{ + type Item = T::Item; + type IntoIter = T::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.value.into_iter() + } +} + +/// Target to which the editor can navigate to. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct NavigationTarget { + file: File, + + /// The range that should be focused when navigating to the target. + /// + /// This is typically not the full range of the node. For example, it's the range of the class's name in a class definition. + /// + /// The `focus_range` must be fully covered by `full_range`. + focus_range: TextRange, + + /// The range covering the entire target. + full_range: TextRange, +} + +impl NavigationTarget { + pub fn file(&self) -> File { + self.file + } + + pub fn focus_range(&self) -> TextRange { + self.focus_range + } + + pub fn full_range(&self) -> TextRange { + self.full_range + } +} + +#[derive(Debug, Clone)] +pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>); + +impl NavigationTargets { + fn single(target: NavigationTarget) -> Self { + Self(smallvec::smallvec![target]) + } + + fn empty() -> Self { + Self(smallvec::SmallVec::new()) + } + + fn unique(targets: impl IntoIterator) -> Self { + let unique: FxHashSet<_> = targets.into_iter().collect(); + if unique.is_empty() { + Self::empty() + } else { + let mut targets = unique.into_iter().collect::>(); + targets.sort_by_key(|target| (target.file, target.focus_range.start())); + Self(targets.into()) + } + } + + fn iter(&self) -> std::slice::Iter<'_, NavigationTarget> { + self.0.iter() + } + + #[cfg(test)] + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl IntoIterator for NavigationTargets { + type Item = NavigationTarget; + type IntoIter = smallvec::IntoIter<[NavigationTarget; 1]>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a NavigationTargets { + type Item = &'a NavigationTarget; + type IntoIter = std::slice::Iter<'a, NavigationTarget>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl FromIterator for NavigationTargets { + fn from_iter>(iter: T) -> Self { + Self::unique(iter) + } +} + +pub trait HasNavigationTargets { + fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets; +} + +impl HasNavigationTargets for Type<'_> { + fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + match self { + Type::Union(union) => union + .iter(db) + .flat_map(|target| target.navigation_targets(db)) + .collect(), + + Type::Intersection(intersection) => { + // Only consider the positive elements because the negative elements are mainly from narrowing constraints. + let mut targets = intersection.iter_positive(db).filter(|ty| !ty.is_unknown()); + + let Some(first) = targets.next() else { + return NavigationTargets::empty(); + }; + + match targets.next() { + Some(_) => { + // If there are multiple types in the intersection, we can't navigate to a single one + // because the type is the intersection of all those types. + NavigationTargets::empty() + } + None => first.navigation_targets(db), + } + } + + ty => ty + .definition(db) + .map(|definition| definition.navigation_targets(db)) + .unwrap_or_else(NavigationTargets::empty), + } + } +} + +impl HasNavigationTargets for TypeDefinition<'_> { + fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + let Some(full_range) = self.full_range(db) else { + return NavigationTargets::empty(); + }; + + NavigationTargets::single(NavigationTarget { + file: full_range.file(), + focus_range: self.focus_range(db).unwrap_or(full_range).range(), + full_range: full_range.range(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::db::tests::TestDb; + use insta::internals::SettingsBindDropGuard; + use ruff_db::Db; + use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig}; + use ruff_db::files::{File, system_path_to_file}; + use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; + use ruff_text_size::TextSize; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings, + }; + + /// A way to create a simple single-file (named `main.py`) cursor test. + /// + /// Use cases that require multiple files with a `` marker + /// in a file other than `main.py` can use `CursorTest::builder()`. + pub(super) fn cursor_test(source: &str) -> CursorTest { + CursorTest::builder().source("main.py", source).build() + } + + pub(super) struct CursorTest { + pub(super) db: TestDb, + pub(super) cursor: Cursor, + _insta_settings_guard: SettingsBindDropGuard, + } + + impl CursorTest { + pub(super) fn builder() -> CursorTestBuilder { + CursorTestBuilder::default() + } + + pub(super) fn write_file( + &mut self, + path: impl AsRef, + content: &str, + ) -> std::io::Result<()> { + self.db.write_file(path, content) + } + + pub(super) fn render_diagnostics(&self, diagnostics: I) -> String + where + I: IntoIterator, + D: IntoDiagnostic, + { + use std::fmt::Write; + + let mut buf = String::new(); + + let config = DisplayDiagnosticConfig::default() + .color(false) + .format(DiagnosticFormat::Full); + for diagnostic in diagnostics { + let diag = diagnostic.into_diagnostic(); + write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); + } + + buf + } + } + + /// The file and offset into that file containing + /// a `` marker. + pub(super) struct Cursor { + pub(super) file: File, + pub(super) offset: TextSize, + } + + #[derive(Default)] + pub(super) struct CursorTestBuilder { + /// A list of source files, corresponding to the + /// file's path and its contents. + sources: Vec, + } + + impl CursorTestBuilder { + pub(super) fn build(&self) -> CursorTest { + let mut db = TestDb::new(); + let mut cursor: Option = None; + for &Source { + ref path, + ref contents, + cursor_offset, + } in &self.sources + { + db.write_file(path, contents) + .expect("write to memory file system to be successful"); + let Some(offset) = cursor_offset else { + continue; + }; + + let file = system_path_to_file(&db, path).expect("newly written file to existing"); + // This assert should generally never trip, since + // we have an assert on `CursorTestBuilder::source` + // to ensure we never have more than one marker. + assert!( + cursor.is_none(), + "found more than one source that contains ``" + ); + cursor = Some(Cursor { file, offset }); + } + + let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")]) + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search path settings"); + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths, + }, + ); + + let mut insta_settings = insta::Settings::clone_current(); + insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); + // Filter out TODO types because they are different between debug and release builds. + insta_settings.add_filter(r"@Todo\(.+\)", "@Todo"); + + let insta_settings_guard = insta_settings.bind_to_scope(); + + CursorTest { + db, + cursor: cursor.expect("at least one source to contain ``"), + _insta_settings_guard: insta_settings_guard, + } + } + + pub(super) fn source( + &mut self, + path: impl Into, + contents: impl Into, + ) -> &mut CursorTestBuilder { + const MARKER: &str = ""; + + let path = path.into(); + let contents = contents.into(); + let Some(cursor_offset) = contents.find(MARKER) else { + self.sources.push(Source { + path, + contents, + cursor_offset: None, + }); + return self; + }; + + if let Some(source) = self.sources.iter().find(|src| src.cursor_offset.is_some()) { + panic!( + "cursor tests must contain exactly one file \ + with a `` marker, but found a marker \ + in both `{path1}` and `{path2}`", + path1 = source.path, + path2 = path, + ); + } + + let mut without_cursor_marker = contents[..cursor_offset].to_string(); + without_cursor_marker.push_str(&contents[cursor_offset + MARKER.len()..]); + let cursor_offset = + TextSize::try_from(cursor_offset).expect("source to be smaller than 4GB"); + self.sources.push(Source { + path, + contents: without_cursor_marker, + cursor_offset: Some(cursor_offset), + }); + self + } + } + + struct Source { + path: SystemPathBuf, + contents: String, + cursor_offset: Option, + } + + pub(super) trait IntoDiagnostic { + fn into_diagnostic(self) -> Diagnostic; + } +} diff --git a/crates/red_knot_ide/src/markup.rs b/crates/ty_ide/src/markup.rs similarity index 100% rename from crates/red_knot_ide/src/markup.rs rename to crates/ty_ide/src/markup.rs diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs new file mode 100644 index 0000000000000..b9969d305cf09 --- /dev/null +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -0,0 +1,1916 @@ +use crate::Db; +use bitflags::bitflags; +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast as ast; +use ruff_python_ast::visitor::source_order::{ + SourceOrderVisitor, TraversalSignal, walk_expr, walk_stmt, +}; +use ruff_python_ast::{ + AnyNodeRef, BytesLiteral, Expr, FString, InterpolatedStringElement, Stmt, StringLiteral, + TypeParam, +}; +use ruff_text_size::{Ranged, TextLen, TextRange}; +use std::ops::Deref; +use ty_python_semantic::{ + HasType, SemanticModel, + semantic_index::definition::DefinitionKind, + types::{Type, definition_kind_for_name}, +}; + +// This module walks the AST and collects a set of "semantic tokens" for a file +// or a range within a file. Each semantic token provides a "token type" and zero +// or more "modifiers". This information can be used by an editor to provide +// color coding based on semantic meaning. + +// Current limitations and areas for future improvement: + +// TODO: Need to provide better classification for name tokens that are imported +// from other modules. Currently, these are classified based on their types, +// which often means they're classified as variables when they should be classes +// in many cases. + +// TODO: Need to handle semantic tokens within quoted annotations. + +// TODO: Need to properly handle Annotated expressions. All type arguments other +// than the first should be treated as value expressions, not as type expressions. + +// TODO: An identifier that resolves to a parameter when used within a function +// should be classified as a parameter, selfParameter, or clsParameter token. + +// TODO: Properties (or perhaps more generally, descriptor objects?) should be +// classified as property tokens rather than just variables. + +// TODO: Special forms like Protocol and TypedDict should probably be classified +// as class tokens, but they are currently classified as variables. + +// TODO: Type aliases (including those defined with the Python 3.12 "type" statement) +// do not currently have a dedicated semantic token type, but they maybe should. + +// TODO: Additional token modifiers might be added (e.g. for static methods, +// abstract methods and classes). + +/// Semantic token types supported by the language server. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SemanticTokenType { + // This enum must be kept in sync with the SemanticTokenType below. + Namespace, + Class, + Parameter, + SelfParameter, + ClsParameter, + Variable, + Property, + Function, + Method, + Keyword, + String, + Number, + Decorator, + BuiltinConstant, + TypeParameter, +} + +impl SemanticTokenType { + /// Returns all supported semantic token types as enum variants. + pub const fn all() -> [SemanticTokenType; 15] { + [ + SemanticTokenType::Namespace, + SemanticTokenType::Class, + SemanticTokenType::Parameter, + SemanticTokenType::SelfParameter, + SemanticTokenType::ClsParameter, + SemanticTokenType::Variable, + SemanticTokenType::Property, + SemanticTokenType::Function, + SemanticTokenType::Method, + SemanticTokenType::Keyword, + SemanticTokenType::String, + SemanticTokenType::Number, + SemanticTokenType::Decorator, + SemanticTokenType::BuiltinConstant, + SemanticTokenType::TypeParameter, + ] + } + + /// Converts this semantic token type to its LSP string representation. + /// Some of these are standardized terms in the LSP specification, + /// while others are specific to the ty language server. It's important + /// to use the standardized ones where possible because clients can + /// use these for standardized color coding and syntax highlighting. + /// For details, refer to this LSP specification: + /// + pub const fn as_lsp_concept(&self) -> &'static str { + match self { + SemanticTokenType::Namespace => "namespace", + SemanticTokenType::Class => "class", + SemanticTokenType::Parameter => "parameter", + SemanticTokenType::SelfParameter => "selfParameter", + SemanticTokenType::ClsParameter => "clsParameter", + SemanticTokenType::Variable => "variable", + SemanticTokenType::Property => "property", + SemanticTokenType::Function => "function", + SemanticTokenType::Method => "method", + SemanticTokenType::Keyword => "keyword", + SemanticTokenType::String => "string", + SemanticTokenType::Number => "number", + SemanticTokenType::Decorator => "decorator", + SemanticTokenType::BuiltinConstant => "builtinConstant", + SemanticTokenType::TypeParameter => "typeParameter", + } + } +} + +bitflags! { + /// Semantic token modifiers using bit flags. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct SemanticTokenModifier: u32 { + const DEFINITION = 1 << 0; + const READONLY = 1 << 1; + const ASYNC = 1 << 2; + } +} + +impl SemanticTokenModifier { + /// Returns all supported token modifiers for LSP capabilities. + /// Some of these are standardized terms in the LSP specification, + /// while others may be specific to the ty language server. It's + /// important to use the standardized ones where possible because + /// clients can use these for standardized color coding and syntax + /// highlighting. For details, refer to this LSP specification: + /// + pub fn all_names() -> Vec<&'static str> { + vec!["definition", "readonly", "async"] + } +} + +/// A semantic token with its position and classification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticToken { + pub range: TextRange, + pub token_type: SemanticTokenType, + pub modifiers: SemanticTokenModifier, +} + +impl Ranged for SemanticToken { + fn range(&self) -> TextRange { + self.range + } +} + +/// The result of semantic tokenization. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticTokens { + tokens: Vec, +} + +impl SemanticTokens { + /// Create a new `SemanticTokens` instance. + pub fn new(tokens: Vec) -> Self { + Self { tokens } + } +} + +impl Deref for SemanticTokens { + type Target = [SemanticToken]; + + fn deref(&self) -> &Self::Target { + &self.tokens + } +} + +/// Generates semantic tokens for a Python file within the specified range. +/// Pass None to get tokens for the entire file. +pub fn semantic_tokens(db: &dyn Db, file: File, range: Option) -> SemanticTokens { + let parsed = parsed_module(db, file).load(db); + let semantic_model = SemanticModel::new(db, file); + + let mut visitor = SemanticTokenVisitor::new(&semantic_model, file, range); + visitor.visit_body(parsed.suite()); + + SemanticTokens::new(visitor.tokens) +} + +/// AST visitor that collects semantic tokens. +struct SemanticTokenVisitor<'db> { + semantic_model: &'db SemanticModel<'db>, + file: File, + tokens: Vec, + in_class_scope: bool, + in_type_annotation: bool, + range_filter: Option, +} + +impl<'db> SemanticTokenVisitor<'db> { + fn new( + semantic_model: &'db SemanticModel<'db>, + file: File, + range_filter: Option, + ) -> Self { + Self { + semantic_model, + file, + tokens: Vec::new(), + in_class_scope: false, + in_type_annotation: false, + range_filter, + } + } + + fn add_token( + &mut self, + ranged: impl Ranged, + token_type: SemanticTokenType, + modifiers: SemanticTokenModifier, + ) { + let range = ranged.range(); + // Only emit tokens that intersect with the range filter, if one is specified + if let Some(range_filter) = self.range_filter { + if range.intersect(range_filter).is_none() { + return; + } + } + + // Debug assertion to ensure tokens are added in file order + debug_assert!( + self.tokens + .last() + .is_none_or(|last| last.start() <= range.start()), + "Tokens must be added in file order: previous token ends at {:?}, new token starts at {:?}", + self.tokens.last().map(SemanticToken::start), + range.start() + ); + + self.tokens.push(SemanticToken { + range, + token_type, + modifiers, + }); + } + + fn is_constant_name(name: &str) -> bool { + name.chars().all(|c| c.is_uppercase() || c == '_') && name.len() > 1 + } + + fn classify_name(&self, name: &ast::ExprName) -> (SemanticTokenType, SemanticTokenModifier) { + // First try to classify the token based on its definition kind. + let definition_kind = definition_kind_for_name(self.semantic_model.db(), self.file, name); + + if let Some(definition_kind) = definition_kind { + let name_str = name.id.as_str(); + if let Some(classification) = + self.classify_from_definition_kind(&definition_kind, name_str) + { + return classification; + } + } + + // Fall back to type-based classification. + let ty = name.inferred_type(self.semantic_model); + let name_str = name.id.as_str(); + self.classify_from_type_and_name_str(ty, name_str) + } + + fn classify_from_definition_kind( + &self, + definition_kind: &DefinitionKind<'_>, + name_str: &str, + ) -> Option<(SemanticTokenType, SemanticTokenModifier)> { + let mut modifiers = SemanticTokenModifier::empty(); + + match definition_kind { + DefinitionKind::Function(_) => { + // Check if this is a method based on current scope + if self.in_class_scope { + Some((SemanticTokenType::Method, modifiers)) + } else { + Some((SemanticTokenType::Function, modifiers)) + } + } + DefinitionKind::Class(_) => Some((SemanticTokenType::Class, modifiers)), + DefinitionKind::TypeVar(_) => Some((SemanticTokenType::TypeParameter, modifiers)), + DefinitionKind::Parameter(_) => Some((SemanticTokenType::Parameter, modifiers)), + DefinitionKind::VariadicPositionalParameter(_) => { + Some((SemanticTokenType::Parameter, modifiers)) + } + DefinitionKind::VariadicKeywordParameter(_) => { + Some((SemanticTokenType::Parameter, modifiers)) + } + DefinitionKind::TypeAlias(_) => Some((SemanticTokenType::TypeParameter, modifiers)), + DefinitionKind::Import(_) + | DefinitionKind::ImportFrom(_) + | DefinitionKind::StarImport(_) => { + // For imports, return None to fall back to type-based classification + // This allows imported names to be classified based on what they actually are + // (e.g., imported classes as Class, imported functions as Function, etc.) + None + } + _ => { + // For other definition kinds (assignments, etc.), apply constant naming convention + if Self::is_constant_name(name_str) { + modifiers |= SemanticTokenModifier::READONLY; + } + Some((SemanticTokenType::Variable, modifiers)) + } + } + } + + fn classify_from_type_and_name_str( + &self, + ty: Type, + name_str: &str, + ) -> (SemanticTokenType, SemanticTokenModifier) { + let mut modifiers = SemanticTokenModifier::empty(); + + // In type annotation contexts, names that refer to nominal instances or protocol instances + // should be classified as Class tokens (e.g., "int" in "x: int" should be a Class token) + if self.in_type_annotation { + match ty { + Type::NominalInstance(_) | Type::ProtocolInstance(_) => { + return (SemanticTokenType::Class, modifiers); + } + _ => { + // Continue with normal classification for other types in annotations + } + } + } + + match ty { + Type::ClassLiteral(_) => (SemanticTokenType::Class, modifiers), + Type::TypeVar(_) => (SemanticTokenType::TypeParameter, modifiers), + Type::FunctionLiteral(_) => { + // Check if this is a method based on current scope + if self.in_class_scope { + (SemanticTokenType::Method, modifiers) + } else { + (SemanticTokenType::Function, modifiers) + } + } + Type::BoundMethod(_) => (SemanticTokenType::Method, modifiers), + Type::ModuleLiteral(_) => (SemanticTokenType::Namespace, modifiers), + _ => { + // Check for constant naming convention + if Self::is_constant_name(name_str) { + modifiers |= SemanticTokenModifier::READONLY; + } + // For other types (variables, modules, etc.), assume variable + (SemanticTokenType::Variable, modifiers) + } + } + } + + fn classify_from_type_for_attribute( + ty: Type, + attr_name: &ast::Identifier, + ) -> (SemanticTokenType, SemanticTokenModifier) { + let attr_name_str = attr_name.id.as_str(); + let mut modifiers = SemanticTokenModifier::empty(); + + // Classify based on the inferred type of the attribute + match ty { + Type::ClassLiteral(_) => (SemanticTokenType::Class, modifiers), + Type::FunctionLiteral(_) => { + // This is a function accessed as an attribute, likely a method + (SemanticTokenType::Method, modifiers) + } + Type::BoundMethod(_) => { + // Method bound to an instance + (SemanticTokenType::Method, modifiers) + } + Type::ModuleLiteral(_) => { + // Module accessed as an attribute (e.g., from os import path) + (SemanticTokenType::Namespace, modifiers) + } + _ if ty.is_property_instance() => { + // Actual Python property + (SemanticTokenType::Property, modifiers) + } + _ => { + // Check for constant naming convention + if Self::is_constant_name(attr_name_str) { + modifiers |= SemanticTokenModifier::READONLY; + } + + // For other types (variables, constants, etc.), classify as variable + (SemanticTokenType::Variable, modifiers) + } + } + } + + fn classify_parameter( + &self, + _param: &ast::Parameter, + is_first: bool, + func: &ast::StmtFunctionDef, + ) -> SemanticTokenType { + if is_first && self.in_class_scope { + // Check if this is a classmethod (has @classmethod decorator) + // TODO - replace with a more robust way to check whether this is a classmethod + let is_classmethod = + func.decorator_list + .iter() + .any(|decorator| match &decorator.expression { + ast::Expr::Name(name) => name.id.as_str() == "classmethod", + ast::Expr::Attribute(attr) => attr.attr.id.as_str() == "classmethod", + _ => false, + }); + + // Check if this is a staticmethod (has @staticmethod decorator) + // TODO - replace with a more robust way to check whether this is a staticmethod + let is_staticmethod = + func.decorator_list + .iter() + .any(|decorator| match &decorator.expression { + ast::Expr::Name(name) => name.id.as_str() == "staticmethod", + ast::Expr::Attribute(attr) => attr.attr.id.as_str() == "staticmethod", + _ => false, + }); + + if is_staticmethod { + // Static methods don't have self/cls parameters + SemanticTokenType::Parameter + } else if is_classmethod { + // First parameter of a classmethod is cls parameter + SemanticTokenType::ClsParameter + } else { + // First parameter of an instance method is self parameter + SemanticTokenType::SelfParameter + } + } else { + SemanticTokenType::Parameter + } + } + + fn add_dotted_name_tokens(&mut self, name: &ast::Identifier, token_type: SemanticTokenType) { + let name_str = name.id.as_str(); + let name_start = name.start(); + + // Split the dotted name and calculate positions for each part + let mut current_offset = ruff_text_size::TextSize::default(); + for part in name_str.split('.') { + if !part.is_empty() { + self.add_token( + ruff_text_size::TextRange::at(name_start + current_offset, part.text_len()), + token_type, + SemanticTokenModifier::empty(), + ); + } + // Move past this part and the dot + current_offset += part.text_len() + '.'.text_len(); + } + } + + fn classify_from_alias_type( + &self, + ty: Type, + local_name: &ast::Identifier, + ) -> (SemanticTokenType, SemanticTokenModifier) { + self.classify_from_type_and_name_str(ty, local_name.id.as_str()) + } + + fn visit_type_annotation(&mut self, annotation: &ast::Expr) { + let prev_in_type_annotation = self.in_type_annotation; + self.in_type_annotation = true; + self.visit_expr(annotation); + self.in_type_annotation = prev_in_type_annotation; + } + + // Visit parameters for a function or lambda expression and classify + // them as parameters, selfParameter, or clsParameter as appropriate. + fn visit_parameters( + &mut self, + parameters: &ast::Parameters, + func: Option<&ast::StmtFunctionDef>, + ) { + // Parameters + for (i, param) in parameters.args.iter().enumerate() { + let token_type = if let Some(func) = func { + // For function definitions, use the classification logic to determine + // whether this is a self/cls parameter or just a regular parameter + self.classify_parameter(¶m.parameter, i == 0, func) + } else { + // For lambdas, all parameters are just parameters (no self/cls) + SemanticTokenType::Parameter + }; + + self.add_token( + param.parameter.name.range(), + token_type, + SemanticTokenModifier::empty(), + ); + + // Handle parameter type annotations + if let Some(annotation) = ¶m.parameter.annotation { + self.visit_type_annotation(annotation); + } + } + } +} + +impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { + fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal { + // If we have a range filter and this node doesn't intersect, skip it + // and all its children as an optimization + if let Some(range_filter) = self.range_filter { + if node.range().intersect(range_filter).is_none() { + return TraversalSignal::Skip; + } + } + TraversalSignal::Traverse + } + + fn visit_stmt(&mut self, stmt: &Stmt) { + match stmt { + ast::Stmt::FunctionDef(func) => { + // Visit decorator expressions + for decorator in &func.decorator_list { + self.visit_decorator(decorator); + } + + // Function name + self.add_token( + func.name.range(), + if self.in_class_scope { + SemanticTokenType::Method + } else { + SemanticTokenType::Function + }, + if func.is_async { + SemanticTokenModifier::DEFINITION | SemanticTokenModifier::ASYNC + } else { + SemanticTokenModifier::DEFINITION + }, + ); + + // Type parameters (Python 3.12+ syntax) + if let Some(type_params) = &func.type_params { + for type_param in &type_params.type_params { + self.visit_type_param(type_param); + } + } + + self.visit_parameters(&func.parameters, Some(func)); + + // Handle return type annotation + if let Some(returns) = &func.returns { + self.visit_type_annotation(returns); + } + + // Clear the in_class_scope flag so inner functions + // are not treated as methods + let prev_in_class = self.in_class_scope; + self.in_class_scope = false; + self.visit_body(&func.body); + self.in_class_scope = prev_in_class; + } + ast::Stmt::ClassDef(class) => { + // Visit decorator expressions + for decorator in &class.decorator_list { + self.visit_decorator(decorator); + } + + // Class name + self.add_token( + class.name.range(), + SemanticTokenType::Class, + SemanticTokenModifier::DEFINITION, + ); + + // Type parameters (Python 3.12+ syntax) + if let Some(type_params) = &class.type_params { + for type_param in &type_params.type_params { + self.visit_type_param(type_param); + } + } + + // Handle base classes and type annotations in inheritance + if let Some(arguments) = &class.arguments { + // Visit base class arguments + for arg in &arguments.args { + self.visit_expr(arg); + } + // Visit keyword arguments (for metaclass, etc.) + for keyword in &arguments.keywords { + self.visit_expr(&keyword.value); + } + } + + let prev_in_class = self.in_class_scope; + self.in_class_scope = true; + self.visit_body(&class.body); + self.in_class_scope = prev_in_class; + } + ast::Stmt::AnnAssign(assign) => { + // Handle annotated assignments (e.g., x: int = 5) + if let ast::Expr::Name(name) = assign.target.as_ref() { + let (token_type, modifiers) = self.classify_name(name); + self.add_token(name, token_type, modifiers); + } + + // Handle the type annotation + self.visit_type_annotation(&assign.annotation); + + // Handle the value if present + if let Some(value) = &assign.value { + self.visit_expr(value); + } + } + ast::Stmt::Import(import) => { + for alias in &import.names { + if let Some(asname) = &alias.asname { + self.add_token( + asname.range(), + SemanticTokenType::Namespace, + SemanticTokenModifier::empty(), + ); + } else { + // Create separate tokens for each part of a dotted module name + self.add_dotted_name_tokens(&alias.name, SemanticTokenType::Namespace); + } + } + } + ast::Stmt::ImportFrom(import) => { + if let Some(module) = &import.module { + // Create separate tokens for each part of a dotted module name + self.add_dotted_name_tokens(module, SemanticTokenType::Namespace); + } + for alias in &import.names { + if let Some(asname) = &alias.asname { + // For aliased imports (from X import Y as Z), classify Z based on what Y is + let ty = alias.inferred_type(self.semantic_model); + let (token_type, modifiers) = self.classify_from_alias_type(ty, asname); + self.add_token(asname, token_type, modifiers); + } else { + // For direct imports (from X import Y), use semantic classification + let ty = alias.inferred_type(self.semantic_model); + let (token_type, modifiers) = + self.classify_from_alias_type(ty, &alias.name); + self.add_token(&alias.name, token_type, modifiers); + } + } + } + _ => { + // For all other statement types, let the default visitor handle them + walk_stmt(self, stmt); + } + } + } + + fn visit_expr(&mut self, expr: &Expr) { + match expr { + ast::Expr::Name(name) => { + let (token_type, modifiers) = self.classify_name(name); + self.add_token(name, token_type, modifiers); + walk_expr(self, expr); + } + ast::Expr::Attribute(attr) => { + // Visit the base expression first (e.g., 'os' in 'os.path') + self.visit_expr(&attr.value); + + // Then add token for the attribute name (e.g., 'path' in 'os.path') + let ty = expr.inferred_type(self.semantic_model); + let (token_type, modifiers) = + Self::classify_from_type_for_attribute(ty, &attr.attr); + self.add_token(&attr.attr, token_type, modifiers); + } + ast::Expr::NumberLiteral(_) => { + self.add_token( + expr.range(), + SemanticTokenType::Number, + SemanticTokenModifier::empty(), + ); + } + ast::Expr::BooleanLiteral(_) => { + self.add_token( + expr.range(), + SemanticTokenType::BuiltinConstant, + SemanticTokenModifier::empty(), + ); + } + ast::Expr::NoneLiteral(_) => { + self.add_token( + expr.range(), + SemanticTokenType::BuiltinConstant, + SemanticTokenModifier::empty(), + ); + } + ast::Expr::Lambda(lambda) => { + // Handle lambda parameters + if let Some(parameters) = &lambda.parameters { + self.visit_parameters(parameters, None); + } + + // Visit the lambda body + self.visit_expr(&lambda.body); + } + _ => { + // For all other expression types, let the default visitor handle them + walk_expr(self, expr); + } + } + } + + fn visit_string_literal(&mut self, string_literal: &StringLiteral) { + // Emit a semantic token for this string literal part + self.add_token( + string_literal.range(), + SemanticTokenType::String, + SemanticTokenModifier::empty(), + ); + } + + fn visit_bytes_literal(&mut self, bytes_literal: &BytesLiteral) { + // Emit a semantic token for this bytes literal part + self.add_token( + bytes_literal.range(), + SemanticTokenType::String, + SemanticTokenModifier::empty(), + ); + } + + fn visit_f_string(&mut self, f_string: &FString) { + // F-strings contain elements that can be literal strings or expressions + for element in &f_string.elements { + match element { + InterpolatedStringElement::Literal(literal_element) => { + // This is a literal string part within the f-string + self.add_token( + literal_element.range(), + SemanticTokenType::String, + SemanticTokenModifier::empty(), + ); + } + InterpolatedStringElement::Interpolation(expr_element) => { + // This is an expression within the f-string - visit it normally + self.visit_expr(&expr_element.expression); + + // Handle format spec if present + if let Some(format_spec) = &expr_element.format_spec { + // Format specs can contain their own interpolated elements + for spec_element in &format_spec.elements { + match spec_element { + InterpolatedStringElement::Literal(literal) => { + self.add_token( + literal.range(), + SemanticTokenType::String, + SemanticTokenModifier::empty(), + ); + } + InterpolatedStringElement::Interpolation(nested_expr) => { + self.visit_expr(&nested_expr.expression); + } + } + } + } + } + } + } + } + + /// Visit decorators, handling simple name decorators vs complex expressions + fn visit_decorator(&mut self, decorator: &ast::Decorator) { + match &decorator.expression { + ast::Expr::Name(name) => { + // Simple decorator like @staticmethod - use Decorator token type + self.add_token( + name.range(), + SemanticTokenType::Decorator, + SemanticTokenModifier::empty(), + ); + } + _ => { + // Complex decorator like @app.route("/path") - use normal expression rules + self.visit_expr(&decorator.expression); + } + } + } + + fn visit_type_param(&mut self, type_param: &TypeParam) { + // Emit token for the type parameter name + let name_range = type_param.name().range(); + self.add_token( + name_range, + SemanticTokenType::TypeParameter, + SemanticTokenModifier::DEFINITION, + ); + + // Visit bound expression (for TypeVar) + match type_param { + TypeParam::TypeVar(type_var) => { + if let Some(bound) = &type_var.bound { + self.visit_type_annotation(bound); + } + if let Some(default) = &type_var.default { + self.visit_type_annotation(default); + } + } + TypeParam::ParamSpec(param_spec) => { + if let Some(default) = ¶m_spec.default { + self.visit_type_annotation(default); + } + } + TypeParam::TypeVarTuple(type_var_tuple) => { + if let Some(default) = &type_var_tuple.default { + self.visit_type_annotation(default); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::cursor_test; + use insta::assert_snapshot; + + /// Helper function to get semantic tokens for full file (for testing) + fn semantic_tokens_full_file(db: &dyn Db, file: File) -> SemanticTokens { + semantic_tokens(db, file, None) + } + + /// Helper function to convert semantic tokens to a snapshot-friendly text format + fn semantic_tokens_to_snapshot(db: &dyn Db, file: File, tokens: &SemanticTokens) -> String { + use std::fmt::Write; + let source = ruff_db::source::source_text(db, file); + let mut result = String::new(); + + for token in tokens.iter() { + let token_text = &source[token.range()]; + let modifiers_text = if token.modifiers.is_empty() { + String::new() + } else { + let mut mods = Vec::new(); + if token.modifiers.contains(SemanticTokenModifier::DEFINITION) { + mods.push("definition"); + } + if token.modifiers.contains(SemanticTokenModifier::READONLY) { + mods.push("readonly"); + } + if token.modifiers.contains(SemanticTokenModifier::ASYNC) { + mods.push("async"); + } + format!(" [{}]", mods.join(", ")) + }; + + writeln!( + result, + "{:?} @ {}..{}: {:?}{}", + token_text, + u32::from(token.range().start()), + u32::from(token.range().end()), + token.token_type, + modifiers_text + ) + .unwrap(); + } + + result + } + + #[test] + fn test_semantic_tokens_basic() { + let test = cursor_test("def foo(): pass"); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "foo" @ 4..7: Function [definition] + "###); + } + + #[test] + fn test_semantic_tokens_class() { + let test = cursor_test("class MyClass: pass"); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "MyClass" @ 6..13: Class [definition] + "###); + } + + #[test] + fn test_semantic_tokens_variables() { + let test = cursor_test( + " +x = 42 +y = 'hello' +", + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "x" @ 1..2: Variable + "42" @ 5..7: Number + "y" @ 8..9: Variable + "'hello'" @ 12..19: String + "###); + } + + #[test] + fn test_semantic_tokens_self_parameter() { + let test = cursor_test( + " +class MyClass: + def method(self, x): pass +", + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "MyClass" @ 7..14: Class [definition] + "method" @ 24..30: Method [definition] + "self" @ 31..35: SelfParameter + "x" @ 37..38: Parameter + "###); + } + + #[test] + fn test_semantic_tokens_cls_parameter() { + let test = cursor_test( + " +class MyClass: + @classmethod + def method(cls, x): pass +", + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "MyClass" @ 7..14: Class [definition] + "classmethod" @ 21..32: Decorator + "method" @ 41..47: Method [definition] + "cls" @ 48..51: ClsParameter + "x" @ 53..54: Parameter + "#); + } + + #[test] + fn test_semantic_tokens_staticmethod_parameter() { + let test = cursor_test( + " +class MyClass: + @staticmethod + def method(x, y): pass +", + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "MyClass" @ 7..14: Class [definition] + "staticmethod" @ 21..33: Decorator + "method" @ 42..48: Method [definition] + "x" @ 49..50: Parameter + "y" @ 52..53: Parameter + "#); + } + + #[test] + fn test_semantic_tokens_custom_self_cls_names() { + let test = cursor_test( + " +class MyClass: + def method(instance, x): pass + @classmethod + def other(klass, y): pass +", + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "MyClass" @ 7..14: Class [definition] + "method" @ 24..30: Method [definition] + "instance" @ 31..39: SelfParameter + "x" @ 41..42: Parameter + "classmethod" @ 55..66: Decorator + "other" @ 75..80: Method [definition] + "klass" @ 81..86: ClsParameter + "y" @ 88..89: Parameter + "#); + } + + #[test] + fn test_semantic_tokens_modifiers() { + let test = cursor_test( + " +class MyClass: + CONSTANT = 42 + async def method(self): pass +", + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "MyClass" @ 7..14: Class [definition] + "CONSTANT" @ 20..28: Variable [readonly] + "42" @ 31..33: Number + "method" @ 48..54: Method [definition, async] + "self" @ 55..59: SelfParameter + "###); + } + + #[test] + fn test_semantic_classification_vs_heuristic() { + let test = cursor_test( + " +import sys +class MyClass: + pass + +def my_function(): + return 42 + +x = MyClass() +y = my_function() +z = sys.version +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "sys" @ 8..11: Namespace + "MyClass" @ 18..25: Class [definition] + "my_function" @ 41..52: Function [definition] + "42" @ 67..69: Number + "x" @ 71..72: Variable + "MyClass" @ 75..82: Class + "y" @ 85..86: Variable + "my_function" @ 89..100: Function + "z" @ 103..104: Variable + "sys" @ 107..110: Namespace + "version" @ 111..118: Variable + "#); + } + + #[test] + fn test_builtin_constants() { + let test = cursor_test( + " +x = True +y = False +z = None +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "x" @ 1..2: Variable + "True" @ 5..9: BuiltinConstant + "y" @ 10..11: Variable + "False" @ 14..19: BuiltinConstant + "z" @ 20..21: Variable + "None" @ 24..28: BuiltinConstant + "###); + } + + #[test] + fn test_builtin_constants_in_expressions() { + let test = cursor_test( + " +def check(value): + if value is None: + return False + return True + +result = check(None) +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "check" @ 5..10: Function [definition] + "value" @ 11..16: Parameter + "value" @ 26..31: Variable + "None" @ 35..39: BuiltinConstant + "False" @ 56..61: BuiltinConstant + "True" @ 73..77: BuiltinConstant + "result" @ 79..85: Variable + "check" @ 88..93: Function + "None" @ 94..98: BuiltinConstant + "#); + } + + #[test] + fn test_semantic_tokens_range() { + let test = cursor_test( + " +def function1(): + x = 42 + return x + +def function2(): + y = \"hello\" + z = True + return y + z +", + ); + + let full_tokens = semantic_tokens(&test.db, test.cursor.file, None); + + // Get the range that covers only the second function + // Hardcoded offsets: function2 starts at position 42, source ends at position 108 + let range = ruff_text_size::TextRange::new( + ruff_text_size::TextSize::from(42u32), + ruff_text_size::TextSize::from(108u32), + ); + + let range_tokens = semantic_tokens(&test.db, test.cursor.file, Some(range)); + + // Range-based tokens should have fewer tokens than full scan + // (should exclude tokens from function1) + assert!(range_tokens.len() < full_tokens.len()); + + // Test both full tokens and range tokens with snapshots + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &full_tokens), @r#" + "function1" @ 5..14: Function [definition] + "x" @ 22..23: Variable + "42" @ 26..28: Number + "x" @ 40..41: Variable + "function2" @ 47..56: Function [definition] + "y" @ 64..65: Variable + "/"hello/"" @ 68..75: String + "z" @ 80..81: Variable + "True" @ 84..88: BuiltinConstant + "y" @ 100..101: Variable + "z" @ 104..105: Variable + "#); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &range_tokens), @r#" + "function2" @ 47..56: Function [definition] + "y" @ 64..65: Variable + "/"hello/"" @ 68..75: String + "z" @ 80..81: Variable + "True" @ 84..88: BuiltinConstant + "y" @ 100..101: Variable + "z" @ 104..105: Variable + "#); + + // Verify that no tokens from range_tokens have ranges outside the requested range + for token in range_tokens.iter() { + assert!( + range.contains_range(token.range()), + "Token at {:?} is outside requested range {:?}", + token.range(), + range + ); + } + } + + #[test] + fn test_dotted_module_names() { + let test = cursor_test( + " +import os.path +import sys.version_info +from urllib.parse import urlparse +from collections.abc import Mapping +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "os" @ 8..10: Namespace + "path" @ 11..15: Namespace + "sys" @ 23..26: Namespace + "version_info" @ 27..39: Namespace + "urllib" @ 45..51: Namespace + "parse" @ 52..57: Namespace + "urlparse" @ 65..73: Function + "collections" @ 79..90: Namespace + "abc" @ 91..94: Namespace + "Mapping" @ 102..109: Class + "#); + } + + #[test] + fn test_module_type_classification() { + let test = cursor_test( + " +import os +import sys +from collections import defaultdict + +# os and sys should be classified as namespace/module types +x = os +y = sys +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "os" @ 8..10: Namespace + "sys" @ 18..21: Namespace + "collections" @ 27..38: Namespace + "defaultdict" @ 46..57: Class + "x" @ 119..120: Namespace + "os" @ 123..125: Namespace + "y" @ 126..127: Namespace + "sys" @ 130..133: Namespace + "#); + } + + #[test] + fn test_import_classification() { + let test = cursor_test( + " +from os import path +from collections import defaultdict, OrderedDict, Counter +from typing import List, Dict, Optional +from mymodule import CONSTANT, my_function, MyClass +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "os" @ 6..8: Namespace + "path" @ 16..20: Namespace + "collections" @ 26..37: Namespace + "defaultdict" @ 45..56: Class + "OrderedDict" @ 58..69: Class + "Counter" @ 71..78: Class + "typing" @ 84..90: Namespace + "List" @ 98..102: Variable + "Dict" @ 104..108: Variable + "Optional" @ 110..118: Variable + "mymodule" @ 124..132: Namespace + "CONSTANT" @ 140..148: Variable [readonly] + "my_function" @ 150..161: Variable + "MyClass" @ 163..170: Variable + "#); + } + + #[test] + fn test_attribute_classification() { + let test = cursor_test( + " +import os +import sys +from collections import defaultdict +from typing import List + +class MyClass: + CONSTANT = 42 + + def method(self): + return \"hello\" + + @property + def prop(self): + return self.CONSTANT + +obj = MyClass() + +# Test various attribute accesses +x = os.path # path should be namespace (module) +y = obj.method # method should be method (bound method) +z = obj.CONSTANT # CONSTANT should be variable with readonly modifier +w = obj.prop # prop should be property +v = MyClass.method # method should be method (function) +u = List.__name__ # __name__ should be variable +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "os" @ 8..10: Namespace + "sys" @ 18..21: Namespace + "collections" @ 27..38: Namespace + "defaultdict" @ 46..57: Class + "typing" @ 63..69: Namespace + "List" @ 77..81: Variable + "MyClass" @ 89..96: Class [definition] + "CONSTANT" @ 102..110: Variable [readonly] + "42" @ 113..115: Number + "method" @ 129..135: Method [definition] + "self" @ 136..140: SelfParameter + "/"hello/"" @ 158..165: String + "property" @ 176..184: Decorator + "prop" @ 193..197: Method [definition] + "self" @ 198..202: SelfParameter + "self" @ 220..224: Variable + "CONSTANT" @ 225..233: Variable [readonly] + "obj" @ 235..238: Variable + "MyClass" @ 241..248: Class + "x" @ 286..287: Namespace + "os" @ 290..292: Namespace + "path" @ 293..297: Namespace + "y" @ 347..348: Method + "obj" @ 351..354: Variable + "method" @ 355..361: Method + "z" @ 413..414: Variable + "obj" @ 417..420: Variable + "CONSTANT" @ 421..429: Variable [readonly] + "w" @ 491..492: Variable + "obj" @ 495..498: Variable + "prop" @ 499..503: Variable + "v" @ 542..543: Function + "MyClass" @ 546..553: Class + "method" @ 554..560: Method + "u" @ 604..605: Variable + "List" @ 608..612: Variable + "__name__" @ 613..621: Variable + "#); + } + + #[test] + fn test_attribute_fallback_classification() { + let test = cursor_test( + " +class MyClass: + some_attr = \"value\" + +obj = MyClass() +# Test attribute that might not have detailed semantic info +x = obj.some_attr # Should fall back to variable, not property +y = obj.unknown_attr # Should fall back to variable +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "MyClass" @ 7..14: Class [definition] + "some_attr" @ 20..29: Variable + "/"value/"" @ 32..39: String + "obj" @ 45..48: Variable + "MyClass" @ 51..58: Class + "x" @ 121..122: Variable + "obj" @ 125..128: Variable + "some_attr" @ 129..138: Variable + "y" @ 191..192: Variable + "obj" @ 195..198: Variable + "unknown_attr" @ 199..211: Variable + "#); + } + + #[test] + fn test_constant_name_detection() { + let test = cursor_test( + " +class MyClass: + UPPER_CASE = 42 + lower_case = 24 + MixedCase = 12 + A = 1 + +obj = MyClass() +x = obj.UPPER_CASE # Should have readonly modifier +y = obj.lower_case # Should not have readonly modifier +z = obj.MixedCase # Should not have readonly modifier +w = obj.A # Should not have readonly modifier (length == 1) +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "MyClass" @ 7..14: Class [definition] + "UPPER_CASE" @ 20..30: Variable [readonly] + "42" @ 33..35: Number + "lower_case" @ 40..50: Variable + "24" @ 53..55: Number + "MixedCase" @ 60..69: Variable + "12" @ 72..74: Number + "A" @ 79..80: Variable + "1" @ 83..84: Number + "obj" @ 90..93: Variable + "MyClass" @ 96..103: Class + "x" @ 106..107: Variable + "obj" @ 110..113: Variable + "UPPER_CASE" @ 114..124: Variable [readonly] + "y" @ 160..161: Variable + "obj" @ 164..167: Variable + "lower_case" @ 168..178: Variable + "z" @ 220..221: Variable + "obj" @ 224..227: Variable + "MixedCase" @ 228..237: Variable + "w" @ 278..279: Variable + "obj" @ 282..285: Variable + "A" @ 286..287: Variable + "#); + } + + #[test] + fn test_type_annotations() { + let test = cursor_test( + r#" +from typing import List, Optional + +def function_with_annotations(param1: int, param2: str) -> Optional[List[str]]: + pass + +x: int = 42 +y: Optional[str] = None +"#, + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "typing" @ 6..12: Namespace + "List" @ 20..24: Variable + "Optional" @ 26..34: Variable + "function_with_annotations" @ 40..65: Function [definition] + "param1" @ 66..72: Parameter + "int" @ 74..77: Class + "param2" @ 79..85: Parameter + "str" @ 87..90: Class + "Optional" @ 95..103: Variable + "List" @ 104..108: Variable + "str" @ 109..112: Class + "x" @ 126..127: Variable + "int" @ 129..132: Class + "42" @ 135..137: Number + "y" @ 138..139: Variable + "Optional" @ 141..149: Variable + "str" @ 150..153: Class + "None" @ 157..161: BuiltinConstant + "#); + } + + #[test] + fn test_debug_int_classification() { + let test = cursor_test( + " +x: int = 42 +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r###" + "x" @ 1..2: Variable + "int" @ 4..7: Class + "42" @ 10..12: Number + "###); + } + + #[test] + fn test_debug_user_defined_type_classification() { + let test = cursor_test( + " +class MyClass: + pass + +x: MyClass = MyClass() +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "MyClass" @ 7..14: Class [definition] + "x" @ 26..27: Variable + "MyClass" @ 29..36: Class + "MyClass" @ 39..46: Class + "#); + } + + #[test] + fn test_type_annotation_vs_variable_classification() { + let test = cursor_test( + " +from typing import List, Optional + +class MyClass: + pass + +def test_function(param: int, other: MyClass) -> Optional[List[str]]: + # Variable assignments - should be Variable tokens + x: int = 42 + y: MyClass = MyClass() + z: List[str] = [\"hello\"] + + # Type annotations should be Class tokens: + # int, MyClass, Optional, List, str + return None +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "typing" @ 6..12: Namespace + "List" @ 20..24: Variable + "Optional" @ 26..34: Variable + "MyClass" @ 42..49: Class [definition] + "test_function" @ 65..78: Function [definition] + "param" @ 79..84: Parameter + "int" @ 86..89: Class + "other" @ 91..96: Parameter + "MyClass" @ 98..105: Class + "Optional" @ 110..118: Variable + "List" @ 119..123: Variable + "str" @ 124..127: Class + "x" @ 190..191: Variable + "int" @ 193..196: Class + "42" @ 199..201: Number + "y" @ 206..207: Variable + "MyClass" @ 209..216: Class + "MyClass" @ 219..226: Class + "z" @ 233..234: Variable + "List" @ 236..240: Variable + "str" @ 241..244: Class + "/"hello/"" @ 249..256: String + "None" @ 361..365: BuiltinConstant + "#); + } + + #[test] + fn test_protocol_types_in_annotations() { + let test = cursor_test( + " +from typing import Protocol + +class MyProtocol(Protocol): + def method(self) -> int: ... + +def test_function(param: MyProtocol) -> None: + pass +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "typing" @ 6..12: Namespace + "Protocol" @ 20..28: Variable + "MyProtocol" @ 36..46: Class [definition] + "Protocol" @ 47..55: Variable + "method" @ 66..72: Method [definition] + "self" @ 73..77: SelfParameter + "int" @ 82..85: Class + "test_function" @ 96..109: Function [definition] + "param" @ 110..115: Parameter + "MyProtocol" @ 117..127: Class + "None" @ 132..136: BuiltinConstant + "#); + } + + #[test] + fn test_protocol_type_annotation_vs_value_context() { + let test = cursor_test( + " +from typing import Protocol + +class MyProtocol(Protocol): + def method(self) -> int: ... + +# Value context - MyProtocol is still a class literal, so should be Class +my_protocol_var = MyProtocol + +# Type annotation context - should be Class +def test_function(param: MyProtocol) -> MyProtocol: + return param +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "typing" @ 6..12: Namespace + "Protocol" @ 20..28: Variable + "MyProtocol" @ 36..46: Class [definition] + "Protocol" @ 47..55: Variable + "method" @ 66..72: Method [definition] + "self" @ 73..77: SelfParameter + "int" @ 82..85: Class + "my_protocol_var" @ 166..181: Class + "MyProtocol" @ 184..194: Class + "test_function" @ 246..259: Function [definition] + "param" @ 260..265: Parameter + "MyProtocol" @ 267..277: Class + "MyProtocol" @ 282..292: Class + "param" @ 305..310: Parameter + "#); + } + + #[test] + fn test_type_parameters_pep695() { + let test = cursor_test( + " +# Test Python 3.12 PEP 695 type parameter syntax + +# Generic function with TypeVar +def func[T](x: T) -> T: + return x + +# Generic function with TypeVarTuple +def func_tuple[*Ts](args: tuple[*Ts]) -> tuple[*Ts]: + return args + +# Generic function with ParamSpec +def func_paramspec[**P](func: Callable[P, int]) -> Callable[P, str]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> str: + return str(func(*args, **kwargs)) + return wrapper + +# Generic class with multiple type parameters +class Container[T, U]: + def __init__(self, value1: T, value2: U): + self.value1: T = value1 + self.value2: U = value2 + + def get_first(self) -> T: + return self.value1 + + def get_second(self) -> U: + return self.value2 + +# Generic class with bounds and defaults +class BoundedContainer[T: int, U = str]: + def process(self, x: T, y: U) -> tuple[T, U]: + return (x, y) +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "func" @ 87..91: Function [definition] + "T" @ 92..93: TypeParameter [definition] + "x" @ 95..96: Parameter + "T" @ 98..99: TypeParameter + "T" @ 104..105: TypeParameter + "x" @ 118..119: Parameter + "func_tuple" @ 164..174: Function [definition] + "Ts" @ 176..178: TypeParameter [definition] + "args" @ 180..184: Parameter + "tuple" @ 186..191: Class + "Ts" @ 193..195: Variable + "tuple" @ 201..206: Class + "Ts" @ 208..210: Variable + "args" @ 224..228: Parameter + "func_paramspec" @ 268..282: Function [definition] + "P" @ 285..286: TypeParameter [definition] + "func" @ 288..292: Parameter + "Callable" @ 294..302: Variable + "P" @ 303..304: Variable + "int" @ 306..309: Class + "Callable" @ 315..323: Variable + "P" @ 324..325: Variable + "str" @ 327..330: Class + "wrapper" @ 341..348: Function [definition] + "str" @ 387..390: Class + "str" @ 407..410: Class + "func" @ 411..415: Variable + "args" @ 417..421: Parameter + "kwargs" @ 425..431: Parameter + "wrapper" @ 445..452: Function + "Container" @ 506..515: Class [definition] + "T" @ 516..517: TypeParameter [definition] + "U" @ 519..520: TypeParameter [definition] + "__init__" @ 531..539: Method [definition] + "self" @ 540..544: SelfParameter + "value1" @ 546..552: Parameter + "T" @ 554..555: TypeParameter + "value2" @ 557..563: Parameter + "U" @ 565..566: TypeParameter + "T" @ 590..591: TypeParameter + "value1" @ 594..600: Parameter + "U" @ 622..623: TypeParameter + "value2" @ 626..632: Parameter + "get_first" @ 646..655: Method [definition] + "self" @ 656..660: SelfParameter + "T" @ 665..666: TypeParameter + "self" @ 683..687: Variable + "value1" @ 688..694: Variable + "get_second" @ 708..718: Method [definition] + "self" @ 719..723: SelfParameter + "U" @ 728..729: TypeParameter + "self" @ 746..750: Variable + "value2" @ 751..757: Variable + "BoundedContainer" @ 806..822: Class [definition] + "T" @ 823..824: TypeParameter [definition] + "int" @ 826..829: Class + "U" @ 831..832: TypeParameter [definition] + "str" @ 835..838: Class + "process" @ 849..856: Method [definition] + "self" @ 857..861: SelfParameter + "x" @ 863..864: Parameter + "T" @ 866..867: TypeParameter + "y" @ 869..870: Parameter + "U" @ 872..873: TypeParameter + "tuple" @ 878..883: Class + "T" @ 884..885: TypeParameter + "U" @ 887..888: TypeParameter + "x" @ 907..908: Parameter + "y" @ 910..911: Parameter + "#); + } + + #[test] + fn test_type_parameters_usage_in_function_body() { + let test = cursor_test( + " +def generic_function[T](value: T) -> T: + # Type parameter T should be recognized here too + result: T = value + temp = result # This could potentially be T as well + return result +", + ); + + let tokens = semantic_tokens(&test.db, test.cursor.file, None); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "generic_function" @ 5..21: Function [definition] + "T" @ 22..23: TypeParameter [definition] + "value" @ 25..30: Parameter + "T" @ 32..33: TypeParameter + "T" @ 38..39: TypeParameter + "result" @ 98..104: Variable + "T" @ 106..107: TypeParameter + "value" @ 110..115: Parameter + "temp" @ 120..124: TypeParameter + "result" @ 127..133: Variable + "result" @ 184..190: Variable + "#); + } + + #[test] + fn test_decorator_classification() { + let test = cursor_test( + r#" +@staticmethod +@property +@app.route("/path") +def my_function(): + pass + +@dataclass +class MyClass: + pass +"#, + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "staticmethod" @ 2..14: Decorator + "property" @ 16..24: Decorator + "app" @ 26..29: Variable + "route" @ 30..35: Variable + "/"/path/"" @ 36..43: String + "my_function" @ 49..60: Function [definition] + "dataclass" @ 75..84: Decorator + "MyClass" @ 91..98: Class [definition] + "#); + } + + #[test] + fn test_implicitly_concatenated_strings() { + let test = cursor_test( + r#"x = "hello" "world" +y = ("multi" + "line" + "string") +z = 'single' "mixed" 'quotes'"#, + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "x" @ 0..1: Variable + "/"hello/"" @ 4..11: String + "/"world/"" @ 12..19: String + "y" @ 20..21: Variable + "/"multi/"" @ 25..32: String + "/"line/"" @ 39..45: String + "/"string/"" @ 52..60: String + "z" @ 62..63: Variable + "'single'" @ 66..74: String + "/"mixed/"" @ 75..82: String + "'quotes'" @ 83..91: String + "#); + } + + #[test] + fn test_bytes_literals() { + let test = cursor_test( + r#"x = b"hello" b"world" +y = (b"multi" + b"line" + b"bytes") +z = b'single' b"mixed" b'quotes'"#, + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "x" @ 0..1: Variable + "b/"hello/"" @ 4..12: String + "b/"world/"" @ 13..21: String + "y" @ 22..23: Variable + "b/"multi/"" @ 27..35: String + "b/"line/"" @ 42..49: String + "b/"bytes/"" @ 56..64: String + "z" @ 66..67: Variable + "b'single'" @ 70..79: String + "b/"mixed/"" @ 80..88: String + "b'quotes'" @ 89..98: String + "#); + } + + #[test] + fn test_mixed_string_and_bytes_literals() { + let test = cursor_test( + r#"# Test mixed string and bytes literals +string_concat = "hello" "world" +bytes_concat = b"hello" b"world" +mixed_quotes_str = 'single' "double" 'single' +mixed_quotes_bytes = b'single' b"double" b'single' +regular_string = "just a string" +regular_bytes = b"just bytes""#, + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "string_concat" @ 39..52: Variable + "/"hello/"" @ 55..62: String + "/"world/"" @ 63..70: String + "bytes_concat" @ 71..83: Variable + "b/"hello/"" @ 86..94: String + "b/"world/"" @ 95..103: String + "mixed_quotes_str" @ 104..120: Variable + "'single'" @ 123..131: String + "/"double/"" @ 132..140: String + "'single'" @ 141..149: String + "mixed_quotes_bytes" @ 150..168: Variable + "b'single'" @ 171..180: String + "b/"double/"" @ 181..190: String + "b'single'" @ 191..200: String + "regular_string" @ 201..215: Variable + "/"just a string/"" @ 218..233: String + "regular_bytes" @ 234..247: Variable + "b/"just bytes/"" @ 250..263: String + "#); + } + + #[test] + fn test_fstring_with_mixed_literals() { + let test = cursor_test( + r#" +# Test f-strings with various literal types +name = "Alice" +data = b"hello" +value = 42 + +# F-string with string literals, expressions, and other literals +result = f"Hello {name}! Value: {value}, Data: {data!r}" + +# F-string with concatenated string and bytes literals +mixed = f"prefix" + b"suffix" + +# Complex f-string with nested expressions +complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}" +"#, + ); + + let tokens = semantic_tokens_full_file(&test.db, test.cursor.file); + + assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#" + "name" @ 45..49: Variable + "/"Alice/"" @ 52..59: String + "data" @ 60..64: Variable + "b/"hello/"" @ 67..75: String + "value" @ 76..81: Variable + "42" @ 84..86: Number + "result" @ 153..159: Variable + "Hello " @ 164..170: String + "name" @ 171..175: Variable + "! Value: " @ 176..185: String + "value" @ 186..191: Variable + ", Data: " @ 192..200: String + "data" @ 201..205: Variable + "mixed" @ 266..271: Variable + "prefix" @ 276..282: String + "b/"suffix/"" @ 286..295: String + "complex_fstring" @ 340..355: Variable + "User: " @ 360..366: String + "name" @ 367..371: Variable + "upper" @ 372..377: Method + ", Count: " @ 380..389: String + "len" @ 390..393: Function + "data" @ 394..398: Variable + ", Hex: " @ 400..407: String + "value" @ 408..413: Variable + "x" @ 414..415: String + "#); + } +} diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs new file mode 100644 index 0000000000000..d3038e3fb18ee --- /dev/null +++ b/crates/ty_ide/src/signature_help.rs @@ -0,0 +1,687 @@ +//! This module handles the "signature help" request in the language server +//! protocol. This request is typically issued by a client when the user types +//! an open parenthesis and starts to enter arguments for a function call. +//! The signature help provides information that the editor displays to the +//! user about the target function signature including parameter names, +//! types, and documentation. It supports multiple signatures for union types +//! and overloads. + +use crate::{Db, docstring::get_parameter_documentation, find_node::covering_node}; +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::{self as ast, AnyNodeRef}; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::semantic_index::definition::Definition; +use ty_python_semantic::types::{CallSignatureDetails, call_signature_details}; + +// Limitations of the current implementation: + +// TODO - If the target function is declared in a stub file but defined (implemented) +// in a source file, the documentation will not reflect the a docstring that appears +// only in the implementation. To do this, we'll need to map the function or +// method in the stub to the implementation and extract the docstring from there. + +/// Information about a function parameter +#[derive(Debug, Clone)] +pub struct ParameterDetails { + /// The parameter name (e.g., "param1") + pub name: String, + /// The parameter label in the signature (e.g., "param1: str") + pub label: String, + /// Documentation specific to the parameter, typically extracted from the + /// function's docstring + pub documentation: Option, +} + +/// Information about a function signature +#[derive(Debug, Clone)] +pub struct SignatureDetails { + /// Text representation of the full signature (including input parameters and return type). + pub label: String, + /// Documentation for the signature, typically from the function's docstring. + pub documentation: Option, + /// Information about each of the parameters in left-to-right order. + pub parameters: Vec, + /// Index of the parameter that corresponds to the argument where the + /// user's cursor is currently positioned. + pub active_parameter: Option, +} + +/// Signature help information for function calls +#[derive(Debug, Clone)] +pub struct SignatureHelpInfo { + /// Information about each of the signatures for the function call. We + /// need to handle multiple because of unions, overloads, and composite + /// calls like constructors (which invoke both __new__ and __init__). + pub signatures: Vec, + /// Index of the "active signature" which is the first signature where + /// all arguments that are currently present in the code map to parameters. + pub active_signature: Option, +} + +/// Signature help information for function calls at the given position +pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option { + let parsed = parsed_module(db, file).load(db); + + // Get the call expression at the given position. + let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?; + + // Get signature details from the semantic analyzer. + let signature_details: Vec> = + call_signature_details(db, file, call_expr); + + if signature_details.is_empty() { + return None; + } + + // Find the active signature - the first signature where all arguments map to parameters. + let active_signature_index = find_active_signature_from_details(&signature_details); + + // Convert to SignatureDetails objects. + let signatures: Vec = signature_details + .into_iter() + .map(|details| { + create_signature_details_from_call_signature_details(db, &details, current_arg_index) + }) + .collect(); + + Some(SignatureHelpInfo { + signatures, + active_signature: active_signature_index, + }) +} + +/// Returns the innermost call expression that contains the specified offset +/// and the index of the argument that the offset maps to. +fn get_call_expr( + parsed: &ruff_db::parsed::ParsedModuleRef, + offset: TextSize, +) -> Option<(&ast::ExprCall, usize)> { + // Create a range from the offset for the covering_node function. + let range = TextRange::new(offset, offset); + + // Find the covering node at the given position that is a function call. + let covering_node = covering_node(parsed.syntax().into(), range) + .find_first(|node| matches!(node, AnyNodeRef::ExprCall(_))) + .ok()?; + + // Get the function call expression. + let AnyNodeRef::ExprCall(call_expr) = covering_node.node() else { + return None; + }; + + // Determine which argument corresponding to the current cursor location. + let current_arg_index = get_argument_index(call_expr, offset); + + Some((call_expr, current_arg_index)) +} + +/// Determine which argument is associated with the specified offset. +/// Returns zero if not within any argument. +fn get_argument_index(call_expr: &ast::ExprCall, offset: TextSize) -> usize { + let mut current_arg = 0; + + for (i, arg) in call_expr.arguments.arguments_source_order().enumerate() { + if offset <= arg.end() { + return i; + } + current_arg = i + 1; + } + + current_arg +} + +/// Create signature details from `CallSignatureDetails`. +fn create_signature_details_from_call_signature_details( + db: &dyn crate::Db, + details: &CallSignatureDetails, + current_arg_index: usize, +) -> SignatureDetails { + let signature_label = details.label.clone(); + + let documentation = get_callable_documentation(db, details.definition); + + // Translate the argument index to parameter index using the mapping. + let active_parameter = + if details.argument_to_parameter_mapping.is_empty() && current_arg_index == 0 { + Some(0) + } else { + details + .argument_to_parameter_mapping + .get(current_arg_index) + .and_then(|¶m_index| param_index) + .or({ + // If we can't find a mapping for this argument, but we have a current + // argument index, use that as the active parameter if it's within bounds. + if current_arg_index < details.parameter_label_offsets.len() { + Some(current_arg_index) + } else { + None + } + }) + }; + + SignatureDetails { + label: signature_label.clone(), + documentation: Some(documentation), + parameters: create_parameters_from_offsets( + &details.parameter_label_offsets, + &signature_label, + db, + details.definition, + &details.parameter_names, + ), + active_parameter, + } +} + +/// Determine appropriate documentation for a callable type based on its original type. +fn get_callable_documentation(db: &dyn crate::Db, definition: Option) -> String { + // TODO: If the definition is located within a stub file and no docstring + // is present, try to map the symbol to an implementation file and extract + // the docstring from that location. + if let Some(definition) = definition { + definition.docstring(db).unwrap_or_default() + } else { + String::new() + } +} + +/// Create `ParameterDetails` objects from parameter label offsets. +fn create_parameters_from_offsets( + parameter_offsets: &[TextRange], + signature_label: &str, + db: &dyn crate::Db, + definition: Option, + parameter_names: &[String], +) -> Vec { + // Extract parameter documentation from the function's docstring if available. + let param_docs = if let Some(definition) = definition { + let docstring = definition.docstring(db); + docstring + .map(|doc| get_parameter_documentation(&doc)) + .unwrap_or_default() + } else { + std::collections::HashMap::new() + }; + + parameter_offsets + .iter() + .enumerate() + .map(|(i, offset)| { + // Extract the parameter label from the signature string. + let start = usize::from(offset.start()); + let end = usize::from(offset.end()); + let label = signature_label + .get(start..end) + .unwrap_or("unknown") + .to_string(); + + // Get the parameter name for documentation lookup. + let param_name = parameter_names.get(i).map(String::as_str).unwrap_or(""); + + ParameterDetails { + name: param_name.to_string(), + label, + documentation: param_docs.get(param_name).cloned(), + } + }) + .collect() +} + +/// Find the active signature index from `CallSignatureDetails`. +/// The active signature is the first signature where all arguments present in the call +/// have valid mappings to parameters (i.e., none of the mappings are None). +fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]) -> Option { + let first = signature_details.first()?; + + // If there are no arguments in the mapping, just return the first signature. + if first.argument_to_parameter_mapping.is_empty() { + return Some(0); + } + + // First, try to find a signature where all arguments have valid parameter mappings. + let perfect_match = signature_details.iter().position(|details| { + // Check if all arguments have valid parameter mappings (i.e., are not None). + details + .argument_to_parameter_mapping + .iter() + .all(Option::is_some) + }); + + if let Some(index) = perfect_match { + return Some(index); + } + + // If no perfect match, find the signature with the most valid argument mappings. + let (best_index, _) = signature_details + .iter() + .enumerate() + .max_by_key(|(_, details)| { + details + .argument_to_parameter_mapping + .iter() + .filter(|mapping| mapping.is_some()) + .count() + })?; + + Some(best_index) +} + +#[cfg(test)] +mod tests { + use crate::signature_help::SignatureHelpInfo; + use crate::tests::{CursorTest, cursor_test}; + + #[test] + fn signature_help_basic_function_call() { + let test = cursor_test( + r#" + def example_function(param1: str, param2: int) -> str: + """This is a docstring for the example function. + + Args: + param1: The first parameter as a string + param2: The second parameter as an integer + + Returns: + A formatted string combining both parameters + """ + return f"{param1}: {param2}" + + result = example_function( + "#, + ); + + // Test that signature help is provided + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures.len(), 1); + + let signature = &result.signatures[0]; + assert!(signature.label.contains("param1") && signature.label.contains("param2")); + + // Verify that the docstring is extracted and included in the documentation + let expected_docstring = concat!( + "This is a docstring for the example function.\n", + " \n", + " Args:\n", + " param1: The first parameter as a string\n", + " param2: The second parameter as an integer\n", + " \n", + " Returns:\n", + " A formatted string combining both parameters\n", + " " + ); + assert_eq!( + signature.documentation, + Some(expected_docstring.to_string()) + ); + + assert_eq!(result.active_signature, Some(0)); + assert_eq!(signature.active_parameter, Some(0)); + } + + #[test] + fn signature_help_method_call() { + let test = cursor_test( + r#" + class MyClass: + def my_method(self, arg1: str, arg2: bool) -> None: + pass + + obj = MyClass() + obj.my_method(arg2=True, arg1= + "#, + ); + + // Test that signature help is provided for method calls + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures.len(), 1); + + let signature = &result.signatures[0]; + assert!(signature.label.contains("arg1") && signature.label.contains("arg2")); + assert_eq!(result.active_signature, Some(0)); + + // Check the active parameter from the active signature + if let Some(active_sig_index) = result.active_signature { + let active_signature = &result.signatures[active_sig_index]; + assert_eq!(active_signature.active_parameter, Some(0)); + } + } + + #[test] + fn signature_help_nested_function_calls() { + let test = cursor_test( + r#" + def outer(a: int) -> int: + return a * 2 + + def inner(b: str) -> str: + return b.upper() + + result = outer(inner( + "#, + ); + + // Test that signature help focuses on the innermost function call + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures.len(), 1); + + let signature = &result.signatures[0]; + assert!(signature.label.contains("str") || signature.label.contains("->")); + assert_eq!(result.active_signature, Some(0)); + assert_eq!(signature.active_parameter, Some(0)); + } + + #[test] + fn signature_help_union_callable() { + let test = cursor_test( + r#" + import random + def func_a(x: int) -> int: + return x + + def func_b(y: str) -> str: + return y + + if random.random() > 0.5: + f = func_a + else: + f = func_b + + f( + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + + assert_eq!(result.signatures.len(), 2); + + let signature = &result.signatures[0]; + assert_eq!(signature.label, "(x: int) -> int"); + assert_eq!(signature.parameters.len(), 1); + + // Check parameter information + let param = &signature.parameters[0]; + assert_eq!(param.label, "x: int"); + assert_eq!(param.name, "x"); + + // Validate the second signature (from func_b) + let signature_b = &result.signatures[1]; + assert_eq!(signature_b.label, "(y: str) -> str"); + assert_eq!(signature_b.parameters.len(), 1); + + // Check parameter information for the second signature + let param_b = &signature_b.parameters[0]; + assert_eq!(param_b.label, "y: str"); + assert_eq!(param_b.name, "y"); + + assert_eq!(result.active_signature, Some(0)); + + // Check the active parameter from the active signature + if let Some(active_sig_index) = result.active_signature { + let active_signature = &result.signatures[active_sig_index]; + assert_eq!(active_signature.active_parameter, Some(0)); + } + } + + #[test] + fn signature_help_overloaded_function() { + let test = cursor_test( + r#" + from typing import overload + + @overload + def process(value: int) -> str: ... + + @overload + def process(value: str) -> int: ... + + def process(value): + if isinstance(value, int): + return str(value) + else: + return len(value) + + result = process( + "#, + ); + + // Test that signature help is provided for overloaded functions + let result = test.signature_help().expect("Should have signature help"); + + // We should have signatures for the overloads + assert_eq!(result.signatures.len(), 2); + assert_eq!(result.active_signature, Some(0)); + + // Check the active parameter from the active signature + if let Some(active_sig_index) = result.active_signature { + let active_signature = &result.signatures[active_sig_index]; + assert_eq!(active_signature.active_parameter, Some(0)); + } + + // Validate the first overload: process(value: int) -> str + let signature1 = &result.signatures[0]; + assert_eq!(signature1.label, "(value: int) -> str"); + assert_eq!(signature1.parameters.len(), 1); + + let param1 = &signature1.parameters[0]; + assert_eq!(param1.label, "value: int"); + assert_eq!(param1.name, "value"); + + // Validate the second overload: process(value: str) -> int + let signature2 = &result.signatures[1]; + assert_eq!(signature2.label, "(value: str) -> int"); + assert_eq!(signature2.parameters.len(), 1); + + let param2 = &signature2.parameters[0]; + assert_eq!(param2.label, "value: str"); + assert_eq!(param2.name, "value"); + } + + #[test] + fn signature_help_class_constructor() { + let test = cursor_test( + r#" + class Point: + """A simple point class representing a 2D coordinate.""" + + def __init__(self, x: int, y: int): + """Initialize a point with x and y coordinates. + + Args: + x: The x-coordinate + y: The y-coordinate + """ + self.x = x + self.y = y + + point = Point( + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + + // Should have exactly one signature for the constructor + assert_eq!(result.signatures.len(), 1); + let signature = &result.signatures[0]; + + // Validate the constructor signature + assert_eq!(signature.label, "(x: int, y: int) -> Point"); + assert_eq!(signature.parameters.len(), 2); + + // Validate the first parameter (x: int) + let param_x = &signature.parameters[0]; + assert_eq!(param_x.label, "x: int"); + assert_eq!(param_x.name, "x"); + assert_eq!(param_x.documentation, Some("The x-coordinate".to_string())); + + // Validate the second parameter (y: int) + let param_y = &signature.parameters[1]; + assert_eq!(param_y.label, "y: int"); + assert_eq!(param_y.name, "y"); + assert_eq!(param_y.documentation, Some("The y-coordinate".to_string())); + + // Should have the __init__ method docstring as documentation (not the class docstring) + let expected_docstring = "Initialize a point with x and y coordinates.\n \n Args:\n x: The x-coordinate\n y: The y-coordinate\n "; + assert_eq!( + signature.documentation, + Some(expected_docstring.to_string()) + ); + } + + #[test] + fn signature_help_callable_object() { + let test = cursor_test( + r#" + class Multiplier: + def __call__(self, x: int) -> int: + return x * 2 + + multiplier = Multiplier() + result = multiplier( + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + + // Should have a signature for the callable object + assert!(!result.signatures.is_empty()); + let signature = &result.signatures[0]; + + // Should provide signature help for the callable + assert!(signature.label.contains("int") || signature.label.contains("->")); + } + + #[test] + fn signature_help_subclass_of_constructor() { + let test = cursor_test( + r#" + from typing import Type + + def create_instance(cls: Type[list]) -> list: + return cls( + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + + // Should have a signature + assert!(!result.signatures.is_empty()); + let signature = &result.signatures[0]; + + // Should have empty documentation for now + assert_eq!(signature.documentation, Some(String::new())); + } + + #[test] + fn signature_help_parameter_label_offsets() { + let test = cursor_test( + r#" + def test_function(param1: str, param2: int, param3: bool) -> str: + return f"{param1}: {param2}, {param3}" + + result = test_function( + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures.len(), 1); + + let signature = &result.signatures[0]; + assert_eq!(signature.parameters.len(), 3); + + // Check that we have parameter labels + for (i, param) in signature.parameters.iter().enumerate() { + let expected_param_spec = match i { + 0 => "param1: str", + 1 => "param2: int", + 2 => "param3: bool", + _ => panic!("Unexpected parameter index"), + }; + assert_eq!(param.label, expected_param_spec); + } + } + + #[test] + fn signature_help_active_signature_selection() { + // This test verifies that the algorithm correctly selects the first signature + // where all arguments present in the call have valid parameter mappings. + let test = cursor_test( + r#" + from typing import overload + + @overload + def process(value: int) -> str: ... + + @overload + def process(value: str, flag: bool) -> int: ... + + def process(value, flag=None): + if isinstance(value, int): + return str(value) + elif flag is not None: + return len(value) if flag else 0 + else: + return len(value) + + # Call with two arguments - should select the second overload + result = process("hello", True) + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + + // Should have signatures for the overloads. + assert!(!result.signatures.is_empty()); + + // Check that we have an active signature and parameter + if let Some(active_sig_index) = result.active_signature { + let active_signature = &result.signatures[active_sig_index]; + assert_eq!(active_signature.active_parameter, Some(1)); + } + } + + #[test] + fn signature_help_parameter_documentation() { + let test = cursor_test( + r#" + def documented_function(param1: str, param2: int) -> str: + """This is a function with parameter documentation. + + Args: + param1: The first parameter description + param2: The second parameter description + """ + return f"{param1}: {param2}" + + result = documented_function( + "#, + ); + + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures.len(), 1); + + let signature = &result.signatures[0]; + assert_eq!(signature.parameters.len(), 2); + + // Check that parameter documentation is extracted + let param1 = &signature.parameters[0]; + assert_eq!( + param1.documentation, + Some("The first parameter description".to_string()) + ); + + let param2 = &signature.parameters[1]; + assert_eq!( + param2.documentation, + Some("The second parameter description".to_string()) + ); + } + + impl CursorTest { + fn signature_help(&self) -> Option { + crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset) + } + } +} diff --git a/crates/ty_project/Cargo.toml b/crates/ty_project/Cargo.toml new file mode 100644 index 0000000000000..dcef49bd4a3a9 --- /dev/null +++ b/crates/ty_project/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "ty_project" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +authors.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ruff_cache = { workspace = true } +ruff_db = { workspace = true, features = ["cache", "serde"] } +ruff_macros = { workspace = true } +ruff_options_metadata = { workspace = true } +ruff_python_ast = { workspace = true, features = ["serde"] } +ruff_python_formatter = { workspace = true, optional = true } +ruff_text_size = { workspace = true } +ty_ide = { workspace = true } +ty_python_semantic = { workspace = true, features = ["serde"] } +ty_vendored = { workspace = true } + +anyhow = { workspace = true } +camino = { workspace = true } +colored = { workspace = true } +crossbeam = { workspace = true } +get-size2 = { workspace = true } +globset = { workspace = true } +notify = { workspace = true } +pep440_rs = { workspace = true, features = ["version-ranges"] } +ordermap = { workspace = true, features = ["serde"] } +rayon = { workspace = true } +regex = { workspace = true } +regex-automata = { workspace = true } +rustc-hash = { workspace = true } +salsa = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true } +thiserror = { workspace = true } +toml = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +ruff_db = { workspace = true, features = ["testing"] } +insta = { workspace = true, features = ["redactions", "ron"] } + +[features] +default = ["zstd"] +deflate = ["ty_vendored/deflate"] +schemars = ["dep:schemars", "ruff_db/schemars", "ty_python_semantic/schemars"] +zstd = ["ty_vendored/zstd"] +format = ["ruff_python_formatter"] + +[lints] +workspace = true diff --git a/crates/red_knot_project/src/combine.rs b/crates/ty_project/src/combine.rs similarity index 84% rename from crates/red_knot_project/src/combine.rs rename to crates/ty_project/src/combine.rs index 659c23df5c26e..98222d4ee8460 100644 --- a/crates/red_knot_project/src/combine.rs +++ b/crates/ty_project/src/combine.rs @@ -1,8 +1,9 @@ use std::{collections::HashMap, hash::BuildHasher}; -use red_knot_python_semantic::{PythonPath, PythonPlatform}; +use ordermap::OrderMap; use ruff_db::system::SystemPathBuf; use ruff_python_ast::PythonVersion; +use ty_python_semantic::PythonPlatform; /// Combine two values, preferring the values in `self`. /// @@ -22,7 +23,7 @@ use ruff_python_ast::PythonVersion; /// For example: patterns coming last in file inclusion and exclusion patterns /// allow overriding earlier patterns, matching the `gitignore` behavior. /// Generally speaking, it feels more intuitive if later values override earlier values -/// than the other way around: `knot --exclude png --exclude "!important.png"`. +/// than the other way around: `ty --exclude png --exclude "!important.png"`. /// /// The main downside of this approach is that the ordering can be surprising in cases /// where the option has a "first match" semantic and not a "last match" wins. @@ -35,7 +36,7 @@ use ruff_python_ast::PythonVersion; /// ``` /// /// ```bash -/// knot --extra-paths a +/// ty --extra-paths a /// ``` /// /// That's why a user might expect that this configuration results in `["a", "b", "c"]`, @@ -111,6 +112,18 @@ where } } +impl Combine for OrderMap +where + K: Eq + std::hash::Hash, + S: BuildHasher, +{ + fn combine_with(&mut self, other: Self) { + for (k, v) in other { + self.entry(k).or_insert(v); + } + } +} + /// Implements [`Combine`] for a value that always returns `self` when combined with another value. macro_rules! impl_noop_combine { ($name:ident) => { @@ -128,7 +141,6 @@ macro_rules! impl_noop_combine { impl_noop_combine!(SystemPathBuf); impl_noop_combine!(PythonPlatform); -impl_noop_combine!(PythonPath); impl_noop_combine!(PythonVersion); // std types @@ -150,6 +162,7 @@ impl_noop_combine!(String); #[cfg(test)] mod tests { use crate::combine::Combine; + use ordermap::OrderMap; use std::collections::HashMap; #[test] @@ -188,4 +201,24 @@ mod tests { ])) ); } + + #[test] + fn combine_order_map() { + let a: OrderMap = OrderMap::from_iter([(1, "a"), (2, "a"), (3, "a")]); + let b: OrderMap = OrderMap::from_iter([(0, "b"), (2, "b"), (5, "b")]); + + assert_eq!(None.combine(Some(b.clone())), Some(b.clone())); + assert_eq!(Some(a.clone()).combine(None), Some(a.clone())); + assert_eq!( + Some(a).combine(Some(b)), + // The value from `a` takes precedence + Some(OrderMap::from_iter([ + (1, "a"), + (2, "a"), + (3, "a"), + (0, "b"), + (5, "b") + ])) + ); + } } diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs new file mode 100644 index 0000000000000..76f001b3b8adb --- /dev/null +++ b/crates/ty_project/src/db.rs @@ -0,0 +1,567 @@ +use std::fmt::Formatter; +use std::panic::{AssertUnwindSafe, RefUnwindSafe}; +use std::sync::Arc; +use std::{cmp, fmt}; + +use crate::metadata::settings::file_settings; +use crate::{DEFAULT_LINT_REGISTRY, DummyReporter}; +use crate::{ProgressReporter, Project, ProjectMetadata}; +use ruff_db::Db as SourceDb; +use ruff_db::diagnostic::Diagnostic; +use ruff_db::files::{File, Files}; +use ruff_db::system::System; +use ruff_db::vendored::VendoredFileSystem; +use salsa::Event; +use salsa::plumbing::ZalsaDatabase; +use ty_ide::Db as IdeDb; +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::{Db as SemanticDb, Program}; + +mod changes; + +#[salsa::db] +pub trait Db: SemanticDb { + fn project(&self) -> Project; +} + +#[salsa::db] +#[derive(Clone)] +pub struct ProjectDatabase { + project: Option, + files: Files, + + // IMPORTANT: Never return clones of `system` outside `ProjectDatabase` (only return references) + // or the "trick" to get a mutable `Arc` in `Self::system_mut` is no longer guaranteed to work. + system: Arc, + + // IMPORTANT: This field must be the last because we use `zalsa_mut` (drops all other storage references) + // to drop all other references to the database, which gives us exclusive access to other `Arc`s stored on this db. + // However, for this to work it's important that the `storage` is dropped AFTER any `Arc` that + // we try to mutably borrow using `Arc::get_mut` (like `system`). + storage: salsa::Storage, +} + +impl ProjectDatabase { + pub fn new(project_metadata: ProjectMetadata, system: S) -> anyhow::Result + where + S: System + 'static + Send + Sync + RefUnwindSafe, + { + let mut db = Self { + project: None, + storage: salsa::Storage::new(if tracing::enabled!(tracing::Level::TRACE) { + Some(Box::new({ + move |event: Event| { + if matches!(event.kind, salsa::EventKind::WillCheckCancellation) { + return; + } + + tracing::trace!("Salsa event: {event:?}"); + } + })) + } else { + None + }), + files: Files::default(), + system: Arc::new(system), + }; + + // TODO: Use the `program_settings` to compute the key for the database's persistent + // cache and load the cache if it exists. + // we may want to have a dedicated method for this? + + // Initialize the `Program` singleton + let program_settings = project_metadata.to_program_settings(db.system(), db.vendored())?; + Program::from_settings(&db, program_settings); + + db.project = Some( + Project::from_metadata(&db, project_metadata) + .map_err(|error| anyhow::anyhow!("{}", error.pretty(&db)))?, + ); + + Ok(db) + } + + /// Checks all open files in the project and its dependencies. + pub fn check(&self) -> Vec { + self.check_with_mode(CheckMode::OpenFiles) + } + + /// Checks all open files in the project and its dependencies, using the given reporter. + pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec { + let reporter = AssertUnwindSafe(reporter); + self.project().check(self, CheckMode::OpenFiles, reporter) + } + + /// Check the project with the given mode. + pub fn check_with_mode(&self, mode: CheckMode) -> Vec { + let mut reporter = DummyReporter; + let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter); + self.project().check(self, mode, reporter) + } + + #[tracing::instrument(level = "debug", skip(self))] + pub fn check_file(&self, file: File) -> Vec { + self.project().check_file(self, file) + } + + /// Returns a mutable reference to the system. + /// + /// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock. + pub fn system_mut(&mut self) -> &mut dyn System { + // TODO: Use a more official method to cancel other queries. + // https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries + let _ = self.zalsa_mut(); + + Arc::get_mut(&mut self.system) + .expect("ref count should be 1 because `zalsa_mut` drops all other DB references.") + } + + /// Returns a [`SalsaMemoryDump`] that can be use to dump Salsa memory usage information + /// to the CLI after a typechecker run. + pub fn salsa_memory_dump(&self) -> SalsaMemoryDump { + let salsa_db = self as &dyn salsa::Database; + + let mut ingredients = salsa_db.structs_info(); + let mut memos = salsa_db.queries_info().into_iter().collect::>(); + + ingredients.sort_by_key(|ingredient| cmp::Reverse(ingredient.size_of_fields())); + memos.sort_by_key(|(_, memo)| cmp::Reverse(memo.size_of_fields())); + + let mut total_fields = 0; + let mut total_metadata = 0; + for ingredient in &ingredients { + total_metadata += ingredient.size_of_metadata(); + total_fields += ingredient.size_of_fields(); + } + + let mut total_memo_fields = 0; + let mut total_memo_metadata = 0; + for (_, memo) in &memos { + total_memo_fields += memo.size_of_fields(); + total_memo_metadata += memo.size_of_metadata(); + } + + SalsaMemoryDump { + total_fields, + total_metadata, + total_memo_fields, + total_memo_metadata, + ingredients, + memos, + } + } +} + +impl std::fmt::Debug for ProjectDatabase { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("ProjectDatabase") + .field("project", &self.project) + .field("files", &self.files) + .field("system", &self.system) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CheckMode { + /// Checks only the open files in the project. + OpenFiles, + + /// Checks all files in the project, ignoring the open file set. + /// + /// This includes virtual files, such as those created by the language server. + AllFiles, +} + +/// Stores memory usage information. +pub struct SalsaMemoryDump { + total_fields: usize, + total_metadata: usize, + total_memo_fields: usize, + total_memo_metadata: usize, + ingredients: Vec, + memos: Vec<(&'static str, salsa::IngredientInfo)>, +} + +#[allow(clippy::cast_precision_loss)] +fn bytes_to_mb(total: usize) -> f64 { + total as f64 / 1_000_000. +} + +#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +impl SalsaMemoryDump { + /// Returns a short report that provides total memory usage information. + pub fn display_short(&self) -> impl fmt::Display + '_ { + struct DisplayShort<'a>(&'a SalsaMemoryDump); + + impl fmt::Display for DisplayShort<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let SalsaMemoryDump { + total_fields, + total_metadata, + total_memo_fields, + total_memo_metadata, + ref ingredients, + ref memos, + } = *self.0; + + writeln!(f, "=======SALSA SUMMARY=======")?; + + writeln!( + f, + "TOTAL MEMORY USAGE: {:.2}MB", + bytes_to_mb( + total_metadata + total_fields + total_memo_fields + total_memo_metadata + ) + )?; + + writeln!( + f, + " struct metadata = {:.2}MB", + bytes_to_mb(total_metadata), + )?; + writeln!(f, " struct fields = {:.2}MB", bytes_to_mb(total_fields))?; + writeln!( + f, + " memo metadata = {:.2}MB", + bytes_to_mb(total_memo_metadata), + )?; + writeln!( + f, + " memo fields = {:.2}MB", + bytes_to_mb(total_memo_fields), + )?; + + writeln!(f, "QUERY COUNT: {}", memos.len())?; + writeln!(f, "STRUCT COUNT: {}", ingredients.len())?; + + Ok(()) + } + } + + DisplayShort(self) + } + + /// Returns a short report that provides fine-grained memory usage information per + /// Salsa ingredient. + pub fn display_full(&self) -> impl fmt::Display + '_ { + struct DisplayFull<'a>(&'a SalsaMemoryDump); + + impl fmt::Display for DisplayFull<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let SalsaMemoryDump { + total_fields, + total_metadata, + total_memo_fields, + total_memo_metadata, + ref ingredients, + ref memos, + } = *self.0; + + writeln!(f, "=======SALSA STRUCTS=======")?; + + for ingredient in ingredients { + writeln!( + f, + "{:<50} metadata={:<8} fields={:<8} count={}", + format!("`{}`", ingredient.debug_name()), + format!("{:.2}MB", bytes_to_mb(ingredient.size_of_metadata())), + format!("{:.2}MB", bytes_to_mb(ingredient.size_of_fields())), + ingredient.count() + )?; + } + + writeln!(f, "=======SALSA QUERIES=======")?; + + for (query_fn, memo) in memos { + writeln!(f, "`{query_fn} -> {}`", memo.debug_name())?; + + writeln!( + f, + " metadata={:<8} fields={:<8} count={}", + format!("{:.2}MB", bytes_to_mb(memo.size_of_metadata())), + format!("{:.2}MB", bytes_to_mb(memo.size_of_fields())), + memo.count() + )?; + } + + writeln!(f, "=======SALSA SUMMARY=======")?; + writeln!( + f, + "TOTAL MEMORY USAGE: {:.2}MB", + bytes_to_mb( + total_metadata + total_fields + total_memo_fields + total_memo_metadata + ) + )?; + + writeln!( + f, + " struct metadata = {:.2}MB", + bytes_to_mb(total_metadata), + )?; + writeln!(f, " struct fields = {:.2}MB", bytes_to_mb(total_fields))?; + writeln!( + f, + " memo metadata = {:.2}MB", + bytes_to_mb(total_memo_metadata), + )?; + writeln!( + f, + " memo fields = {:.2}MB", + bytes_to_mb(total_memo_fields), + )?; + + Ok(()) + } + } + + DisplayFull(self) + } + + /// Returns a redacted report that provides rounded totals of memory usage, to avoid + /// overly sensitive diffs in `mypy-primer` runs. + pub fn display_mypy_primer(&self) -> impl fmt::Display + '_ { + struct DisplayShort<'a>(&'a SalsaMemoryDump); + + fn round_memory(total: usize) -> usize { + // Round the number to the nearest power of 1.05. This gives us a + // 2.5% threshold before the memory usage number is considered to have + // changed. + // + // TODO: Small changes in memory usage may cause the number to be rounded + // into the next power if it happened to already be close to the threshold. + // This also means that differences may surface as a result of small changes + // over time that are unrelated to the current change. Ideally we could compare + // the exact numbers across runs and compute the difference, but we don't have + // the infrastructure for that currently. + const BASE: f64 = 1.05; + BASE.powf(bytes_to_mb(total).log(BASE).round()) as usize + } + + impl fmt::Display for DisplayShort<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let SalsaMemoryDump { + total_fields, + total_metadata, + total_memo_fields, + total_memo_metadata, + .. + } = *self.0; + + writeln!(f, "=======SALSA SUMMARY=======")?; + + writeln!( + f, + "TOTAL MEMORY USAGE: ~{}MB", + round_memory( + total_metadata + total_fields + total_memo_fields + total_memo_metadata + ) + )?; + + writeln!( + f, + " struct metadata = ~{}MB", + round_memory(total_metadata) + )?; + writeln!(f, " struct fields = ~{}MB", round_memory(total_fields))?; + writeln!( + f, + " memo metadata = ~{}MB", + round_memory(total_memo_metadata) + )?; + writeln!( + f, + " memo fields = ~{}MB", + round_memory(total_memo_fields) + )?; + + Ok(()) + } + } + + DisplayShort(self) + } +} + +#[salsa::db] +impl IdeDb for ProjectDatabase {} + +#[salsa::db] +impl SemanticDb for ProjectDatabase { + fn is_file_open(&self, file: File) -> bool { + let Some(project) = &self.project else { + return false; + }; + + project.is_file_open(self, file) + } + + fn rule_selection(&self, file: File) -> &RuleSelection { + let settings = file_settings(self, file); + settings.rules(self) + } + + fn lint_registry(&self) -> &LintRegistry { + &DEFAULT_LINT_REGISTRY + } +} + +#[salsa::db] +impl SourceDb for ProjectDatabase { + fn vendored(&self) -> &VendoredFileSystem { + ty_vendored::file_system() + } + + fn system(&self) -> &dyn System { + &*self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> ruff_python_ast::PythonVersion { + Program::get(self).python_version(self) + } +} + +#[salsa::db] +impl salsa::Database for ProjectDatabase {} + +#[salsa::db] +impl Db for ProjectDatabase { + fn project(&self) -> Project { + self.project.unwrap() + } +} + +#[cfg(feature = "format")] +mod format { + use crate::ProjectDatabase; + use ruff_db::files::File; + use ruff_python_formatter::{Db as FormatDb, PyFormatOptions}; + + #[salsa::db] + impl FormatDb for ProjectDatabase { + fn format_options(&self, file: File) -> PyFormatOptions { + let source_ty = file.source_type(self); + PyFormatOptions::from_source_type(source_ty) + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use std::sync::{Arc, Mutex}; + + use ruff_db::Db as SourceDb; + use ruff_db::files::Files; + use ruff_db::system::{DbWithTestSystem, System, TestSystem}; + use ruff_db::vendored::VendoredFileSystem; + use ty_python_semantic::Program; + use ty_python_semantic::lint::{LintRegistry, RuleSelection}; + + use crate::DEFAULT_LINT_REGISTRY; + use crate::db::Db; + use crate::{Project, ProjectMetadata}; + + type Events = Arc>>; + + #[salsa::db] + #[derive(Clone)] + pub(crate) struct TestDb { + storage: salsa::Storage, + events: Events, + files: Files, + system: TestSystem, + vendored: VendoredFileSystem, + project: Option, + } + + impl TestDb { + pub(crate) fn new(project: ProjectMetadata) -> Self { + let events = Events::default(); + let mut db = Self { + storage: salsa::Storage::new(Some(Box::new({ + let events = events.clone(); + move |event| { + let mut events = events.lock().unwrap(); + events.push(event); + } + }))), + system: TestSystem::default(), + vendored: ty_vendored::file_system().clone(), + files: Files::default(), + events, + project: None, + }; + + let project = Project::from_metadata(&db, project).unwrap(); + db.project = Some(project); + db + } + } + + impl TestDb { + /// Takes the salsa events. + pub(crate) fn take_salsa_events(&mut self) -> Vec { + let mut events = self.events.lock().unwrap(); + + std::mem::take(&mut *events) + } + } + + impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } + } + + #[salsa::db] + impl SourceDb for TestDb { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> ruff_python_ast::PythonVersion { + Program::get(self).python_version(self) + } + } + + #[salsa::db] + impl ty_python_semantic::Db for TestDb { + fn is_file_open(&self, file: ruff_db::files::File) -> bool { + !file.path(self).is_vendored_path() + } + + fn rule_selection(&self, _file: ruff_db::files::File) -> &RuleSelection { + self.project().rules(self) + } + + fn lint_registry(&self) -> &LintRegistry { + &DEFAULT_LINT_REGISTRY + } + } + + #[salsa::db] + impl Db for TestDb { + fn project(&self) -> Project { + self.project.unwrap() + } + } + + #[salsa::db] + impl salsa::Database for TestDb {} +} diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs new file mode 100644 index 0000000000000..f1b203c29936e --- /dev/null +++ b/crates/ty_project/src/db/changes.rs @@ -0,0 +1,310 @@ +use crate::db::{Db, ProjectDatabase}; +use crate::metadata::options::ProjectOptionsOverrides; +use crate::watch::{ChangeEvent, CreatedKind, DeletedKind}; +use crate::{Project, ProjectMetadata}; +use std::collections::BTreeSet; + +use crate::walk::ProjectFilesWalker; +use ruff_db::Db as _; +use ruff_db::files::{File, Files}; +use ruff_db::system::SystemPath; +use rustc_hash::FxHashSet; +use salsa::Setter; +use ty_python_semantic::Program; + +/// Represents the result of applying changes to the project database. +pub struct ChangeResult { + project_changed: bool, + custom_stdlib_changed: bool, +} + +impl ChangeResult { + /// Returns `true` if the project structure has changed. + pub fn project_changed(&self) -> bool { + self.project_changed + } + + /// Returns `true` if the custom stdlib's VERSIONS file has changed. + pub fn custom_stdlib_changed(&self) -> bool { + self.custom_stdlib_changed + } +} + +impl ProjectDatabase { + #[tracing::instrument(level = "debug", skip(self, changes, project_options_overrides))] + pub fn apply_changes( + &mut self, + changes: Vec, + project_options_overrides: Option<&ProjectOptionsOverrides>, + ) -> ChangeResult { + let mut project = self.project(); + let project_root = project.root(self).to_path_buf(); + let config_file_override = + project_options_overrides.and_then(|options| options.config_file_override.clone()); + let options = + project_options_overrides.map(|project_options| project_options.options.clone()); + let program = Program::get(self); + let custom_stdlib_versions_path = program + .custom_stdlib_search_path(self) + .map(|path| path.join("VERSIONS")); + + let mut result = ChangeResult { + project_changed: false, + custom_stdlib_changed: false, + }; + // Paths that were added + let mut added_paths = FxHashSet::default(); + + // Deduplicate the `sync` calls. Many file watchers emit multiple events for the same path. + let mut synced_files = FxHashSet::default(); + let mut sync_recursively = BTreeSet::default(); + + let mut sync_path = |db: &mut ProjectDatabase, path: &SystemPath| { + if synced_files.insert(path.to_path_buf()) { + File::sync_path(db, path); + } + }; + + for change in changes { + tracing::trace!("Handle change: {:?}", change); + + if let Some(path) = change.system_path() { + if let Some(config_file) = &config_file_override { + if config_file.as_path() == path { + result.project_changed = true; + + continue; + } + } + + if matches!( + path.file_name(), + Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml") + ) { + // Changes to ignore files or settings can change the project structure or add/remove files. + result.project_changed = true; + + continue; + } + + if Some(path) == custom_stdlib_versions_path.as_deref() { + result.custom_stdlib_changed = true; + } + } + + match change { + ChangeEvent::Changed { path, kind: _ } | ChangeEvent::Opened(path) => { + sync_path(self, &path); + } + + ChangeEvent::Created { kind, path } => { + match kind { + CreatedKind::File => sync_path(self, &path), + CreatedKind::Directory | CreatedKind::Any => { + sync_recursively.insert(path.clone()); + } + } + + // Unlike other files, it's not only important to update the status of existing + // and known `File`s (`sync_recursively`), it's also important to discover new files + // that were added in the project's root (or any of the paths included for checking). + // + // This is important because `Project::check` iterates over all included files. + // The code below walks the `added_paths` and adds all files that + // should be included in the project. We can skip this check for + // paths that aren't part of the project or shouldn't be included + // when checking the project. + + if self.system().is_file(&path) { + if project.is_file_included(self, &path) { + // Add the parent directory because `walkdir` always visits explicitly passed files + // even if they match an exclude filter. + added_paths.insert(path.parent().unwrap().to_path_buf()); + } + } else if project.is_directory_included(self, &path) { + added_paths.insert(path); + } + } + + ChangeEvent::Deleted { kind, path } => { + let is_file = match kind { + DeletedKind::File => true, + DeletedKind::Directory => { + // file watchers emit an event for every deleted file. No need to scan the entire dir. + continue; + } + DeletedKind::Any => self + .files + .try_system(self, &path) + .is_some_and(|file| file.exists(self)), + }; + + if is_file { + sync_path(self, &path); + + if let Some(file) = self.files().try_system(self, &path) { + project.remove_file(self, file); + } + } else { + sync_recursively.insert(path.clone()); + + if custom_stdlib_versions_path + .as_ref() + .is_some_and(|versions_path| versions_path.starts_with(&path)) + { + result.custom_stdlib_changed = true; + } + + let directory_included = project.is_directory_included(self, &path); + + if directory_included || path == project_root { + // TODO: Shouldn't it be enough to simply traverse the project files and remove all + // that start with the given path? + tracing::debug!( + "Reload project because of a path that could have been a directory." + ); + + // Perform a full-reload in case the deleted directory contained the pyproject.toml. + // We may want to make this more clever in the future, to e.g. iterate over the + // indexed files and remove the once that start with the same path, unless + // the deleted path is the project configuration. + result.project_changed = true; + } else if !directory_included { + tracing::debug!( + "Skipping reload because directory '{path}' isn't included in the project" + ); + } + } + } + + ChangeEvent::CreatedVirtual(path) | ChangeEvent::ChangedVirtual(path) => { + File::sync_virtual_path(self, &path); + } + + ChangeEvent::DeletedVirtual(path) => { + if let Some(virtual_file) = self.files().try_virtual_file(&path) { + virtual_file.close(self); + } + } + + ChangeEvent::Rescan => { + result.project_changed = true; + Files::sync_all(self); + sync_recursively.clear(); + break; + } + } + } + + let sync_recursively = sync_recursively.into_iter(); + let mut last = None; + + for path in sync_recursively { + // Avoid re-syncing paths that are sub-paths of each other. + if let Some(last) = &last { + if path.starts_with(last) { + continue; + } + } + + Files::sync_recursively(self, &path); + last = Some(path); + } + + if result.project_changed { + let new_project_metadata = match config_file_override { + Some(config_file) => ProjectMetadata::from_config_file(config_file, self.system()), + None => ProjectMetadata::discover(&project_root, self.system()), + }; + match new_project_metadata { + Ok(mut metadata) => { + if let Some(cli_options) = options { + metadata.apply_options(cli_options); + } + + if let Err(error) = metadata.apply_configuration_files(self.system()) { + tracing::error!( + "Failed to apply configuration files, continuing without applying them: {error}" + ); + } + + match metadata.to_program_settings(self.system(), self.vendored()) { + Ok(program_settings) => { + let program = Program::get(self); + program.update_from_settings(self, program_settings); + } + Err(error) => { + tracing::error!( + "Failed to convert metadata to program settings, continuing without applying them: {error}" + ); + } + } + + if metadata.root() == project.root(self) { + tracing::debug!("Reloading project after structural change"); + project.reload(self, metadata); + } else { + match Project::from_metadata(self, metadata) { + Ok(new_project) => { + tracing::debug!("Replace project after structural change"); + project = new_project; + } + Err(error) => { + tracing::error!( + "Keeping old project configuration because loading the new settings failed with: {error}" + ); + + project + .set_settings_diagnostics(self) + .to(vec![error.into_diagnostic()]); + } + } + + self.project = Some(project); + } + } + Err(error) => { + tracing::error!( + "Failed to load project, keeping old project configuration: {error}" + ); + } + } + + return result; + } else if result.custom_stdlib_changed { + match project + .metadata(self) + .to_program_settings(self.system(), self.vendored()) + { + Ok(program_settings) => { + program.update_from_settings(self, program_settings); + } + Err(error) => { + tracing::error!("Failed to resolve program settings: {error}"); + } + } + } + + let diagnostics = if let Some(walker) = ProjectFilesWalker::incremental(self, added_paths) { + // Use directory walking to discover newly added files. + let (files, diagnostics) = walker.collect_vec(self); + + for file in files { + project.add_file(self, file); + } + + diagnostics + } else { + Vec::new() + }; + + // Note: We simply replace all IO related diagnostics here. This isn't ideal, because + // it removes IO errors that may still be relevant. However, tracking IO errors correctly + // across revisions doesn't feel essential, considering that they're rare. However, we could + // implement a `BTreeMap` or similar and only prune the diagnostics from paths that we've + // re-scanned (or that were removed etc). + project.replace_index_diagnostics(self, diagnostics); + + result + } +} diff --git a/crates/red_knot_project/src/files.rs b/crates/ty_project/src/files.rs similarity index 99% rename from crates/red_knot_project/src/files.rs rename to crates/ty_project/src/files.rs index 182e20c03a18e..3021aa6143805 100644 --- a/crates/red_knot_project/src/files.rs +++ b/crates/ty_project/src/files.rs @@ -161,6 +161,10 @@ impl Indexed<'_> { pub(super) fn diagnostics(&self) -> &[IOErrorDiagnostic] { &self.inner.diagnostics } + + pub(super) fn len(&self) -> usize { + self.inner.files.len() + } } impl Deref for Indexed<'_> { @@ -250,10 +254,10 @@ impl Drop for IndexedMut<'_> { mod tests { use rustc_hash::FxHashSet; - use crate::db::tests::TestDb; + use crate::ProjectMetadata; use crate::db::Db; + use crate::db::tests::TestDb; use crate::files::Index; - use crate::ProjectMetadata; use ruff_db::files::system_path_to_file; use ruff_db::system::{DbWithWritableSystem as _, SystemPathBuf}; use ruff_python_ast::name::Name; diff --git a/crates/ty_project/src/glob.rs b/crates/ty_project/src/glob.rs new file mode 100644 index 0000000000000..e98a1e881cee6 --- /dev/null +++ b/crates/ty_project/src/glob.rs @@ -0,0 +1,97 @@ +use ruff_db::system::SystemPath; + +pub(crate) use exclude::{ExcludeFilter, ExcludeFilterBuilder}; +pub(crate) use include::{IncludeFilter, IncludeFilterBuilder}; +pub(crate) use portable::{ + AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern, +}; + +mod exclude; +mod include; +mod portable; + +/// Path filtering based on an an exclude and include glob pattern set. +/// +/// Exclude patterns take precedence over includes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IncludeExcludeFilter { + include: IncludeFilter, + exclude: ExcludeFilter, +} + +impl IncludeExcludeFilter { + pub(crate) fn new(include: IncludeFilter, exclude: ExcludeFilter) -> Self { + Self { include, exclude } + } + + /// Returns whether this directory is included in this filter. + /// + /// Note, this function never returns [`IncludeResult::Included`] for a path that is not included or excluded. + /// However, it may return [`IncludeResult::Included`] for directories that are not excluded, but where + /// it requires traversal to decide if any of its subdirectories or files are included. This, for example, + /// is the case when using wildcard include-patterns like `**/test`. Prefix wildcards require to traverse `src` + /// because it can't be known ahead of time whether it contains a `test` directory or file. + pub(crate) fn is_directory_maybe_included( + &self, + path: &SystemPath, + mode: GlobFilterCheckMode, + ) -> IncludeResult { + if self.exclude.match_directory(path, mode) { + IncludeResult::Excluded + } else if self.include.match_directory(path) { + IncludeResult::Included + } else { + IncludeResult::NotIncluded + } + } + + pub(crate) fn is_file_included( + &self, + path: &SystemPath, + mode: GlobFilterCheckMode, + ) -> IncludeResult { + if self.exclude.match_file(path, mode) { + IncludeResult::Excluded + } else if self.include.match_file(path) { + IncludeResult::Included + } else { + IncludeResult::NotIncluded + } + } +} + +impl std::fmt::Display for IncludeExcludeFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "include={}, exclude={}", &self.include, &self.exclude) + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) enum GlobFilterCheckMode { + /// The paths are checked top-to-bottom and inclusion is determined + /// for each path during the traversal. + TopDown, + + /// An adhoc test if a single file or directory is included. + /// + /// This is more expensive than a [`Self::TopDown`] check + /// because it may require testing every ancestor path in addition to the + /// path itself to ensure no ancestor path matches an exclude rule. + Adhoc, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(crate) enum IncludeResult { + /// The path matches or at least is a prefix of an include pattern. + /// + /// For directories: This isn't a guarantee that any file in this directory gets included + /// but we need to traverse it to make this decision. + Included, + + /// The path matches an exclude pattern. + Excluded, + + /// The path matches neither an include nor an exclude pattern and, therefore, + /// isn't included. + NotIncluded, +} diff --git a/crates/ty_project/src/glob/exclude.rs b/crates/ty_project/src/glob/exclude.rs new file mode 100644 index 0000000000000..4f236db1964d5 --- /dev/null +++ b/crates/ty_project/src/glob/exclude.rs @@ -0,0 +1,288 @@ +//! Exclude filter supporting gitignore-like globs. +//! +//! * `src` excludes a file or directory named `src` anywhere in the path. +//! * `/src/` excludes a directory named `src` at the root of the path. +//! * `/src` excludes a directory or file named `src` at the root of the path. +//! * `/src/**` excludes all files and directories inside a directory named `src` but not `src` itself. +//! * `!src` allows a file or directory named `src` anywhere in the path + +use std::fmt::Formatter; +use std::sync::Arc; + +use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder}; +use regex_automata::util::pool::Pool; +use ruff_db::system::SystemPath; + +use crate::GlobFilterCheckMode; +use crate::glob::portable::AbsolutePortableGlobPattern; + +/// A filter for gitignore-like globs that excludes files and directories. +/// +/// # Equality +/// +/// Two filters are equal if they're constructed from the same patterns (including order). +/// Two filters that exclude the exact same files but were constructed from different patterns aren't considered +/// equal. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ExcludeFilter { + ignore: Gitignore, +} + +impl ExcludeFilter { + /// Returns `true` if the path to a directory is definitely excluded and `false` otherwise. + pub(crate) fn match_directory(&self, path: &SystemPath, mode: GlobFilterCheckMode) -> bool { + self.matches(path, mode, true) + } + + /// Returns `true` if the path to a file is definitely excluded and `false` otherwise. + pub(crate) fn match_file(&self, path: &SystemPath, mode: GlobFilterCheckMode) -> bool { + self.matches(path, mode, false) + } + + fn matches(&self, path: &SystemPath, mode: GlobFilterCheckMode, directory: bool) -> bool { + match mode { + GlobFilterCheckMode::TopDown => { + match self.ignore.matched(path, directory) { + // No hit or an allow hit means the file or directory is not excluded. + Match::None | Match::Allow => false, + Match::Ignore => true, + } + } + GlobFilterCheckMode::Adhoc => { + for ancestor in path.ancestors() { + match self.ignore.matched(ancestor, directory) { + // If the path is allowlisted or there's no hit, try the parent to ensure we don't return false + // for a folder where there's an exclude for a parent. + Match::None | Match::Allow => {} + Match::Ignore => return true, + } + } + + false + } + } + } +} + +impl std::fmt::Display for ExcludeFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(&self.ignore.globs).finish() + } +} + +pub(crate) struct ExcludeFilterBuilder { + ignore: GitignoreBuilder, +} + +impl ExcludeFilterBuilder { + pub(crate) fn new() -> Self { + Self { + ignore: GitignoreBuilder::new(), + } + } + + pub(crate) fn add( + &mut self, + pattern: &AbsolutePortableGlobPattern, + ) -> Result<&mut Self, globset::Error> { + self.ignore.add(pattern)?; + + Ok(self) + } + + pub(crate) fn build(self) -> Result { + Ok(ExcludeFilter { + ignore: self.ignore.build()?, + }) + } +} + +/// Matcher for gitignore like globs. +/// +/// This code is our own vendored copy of the ignore's crate `Gitignore` type. +/// +/// The differences with the ignore's crate version are: +/// +/// * All globs are anchored. `src` matches `./src` only and not `**/src` to be consistent with `include`. +/// * It makes use of the fact that all our globs are absolute. This simplifies the implementation a fair bit. +/// Making globs absolute is also motivated by the fact that the globs can come from both the CLI and configuration files, +/// where the paths are anchored relative to the current working directory or the project root respectively. +/// * It uses [`globset::Error`] over the ignore's crate `Error` type. +/// * Removes supported for commented lines, because the patterns aren't read +/// from a `.gitignore` file. This removes the need to escape `#` for file names starting with `#`, +/// +/// You can find the original source on [GitHub](https://github.com/BurntSushi/ripgrep/blob/cbc598f245f3c157a872b69102653e2e349b6d92/crates/ignore/src/gitignore.rs#L81). +/// +/// # Equality +/// +/// Two ignore matches are only equal if they're constructed from the same patterns (including order). +/// Two matchers that were constructed from different patterns but result in +/// including the same files don't compare equal. +#[derive(Clone)] +struct Gitignore { + set: GlobSet, + globs: Vec, + matches: Option>>>, +} + +impl Gitignore { + /// Returns whether the given path (file or directory) matched a pattern in + /// this gitignore matcher. + /// + /// `is_dir` should be true if the path refers to a directory and false + /// otherwise. + /// + /// The path must be absolute or it will only match prefix-wildcard patterns. + fn matched(&self, path: &SystemPath, is_dir: bool) -> Match { + if self.globs.is_empty() { + return Match::None; + } + + let mut matches = self.matches.as_ref().unwrap().get(); + let candidate = Candidate::new(path); + self.set.matches_candidate_into(&candidate, &mut matches); + for &i in matches.iter().rev() { + let glob = &self.globs[i]; + if !glob.is_only_dir || is_dir { + return if glob.is_ignore() { + Match::Ignore + } else { + Match::Allow + }; + } + } + Match::None + } +} + +impl std::fmt::Debug for Gitignore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Gitignore") + .field(&self.globs) + .finish_non_exhaustive() + } +} + +impl PartialEq for Gitignore { + fn eq(&self, other: &Self) -> bool { + self.globs == other.globs + } +} + +impl Eq for Gitignore {} + +#[derive(Copy, Clone, Debug)] +enum Match { + /// The path matches no pattern. + None, + + /// The path matches an ignore pattern (a positive pattern) + /// It should be ignored. + Ignore, + + /// The path matches an allow pattern (a negative pattern). + /// It should not be ignored. + Allow, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct IgnoreGlob { + /// The pattern that was originally parsed. + original: String, + + /// This is a pattern allowing a path (it starts with a `!`, possibly undoing a previous ignore) + is_allow: bool, + + /// Whether this pattern only matches directories. + is_only_dir: bool, +} + +impl IgnoreGlob { + const fn is_ignore(&self) -> bool { + !self.is_allow + } +} + +/// Builds a matcher for git-ignore like globs. +/// +/// All globs need to use absolute paths, unless they're unanchored (contain no `/`). +#[derive(Clone, Debug)] +struct GitignoreBuilder { + builder: GlobSetBuilder, + globs: Vec, +} + +impl GitignoreBuilder { + /// Create a new builder for a gitignore file. + fn new() -> GitignoreBuilder { + GitignoreBuilder { + builder: GlobSetBuilder::new(), + globs: vec![], + } + } + + /// Builds a new matcher from the globs added so far. + /// + /// Once a matcher is built, no new globs can be added to it. + fn build(&self) -> Result { + let set = self.builder.build()?; + + Ok(Gitignore { + set, + globs: self.globs.clone(), + matches: Some(Arc::new(Pool::new(Vec::new))), + }) + } + + /// Adds a gitignore like glob pattern to this builder. + /// + /// If the pattern could not be parsed as a glob, then an error is returned. + fn add( + &mut self, + pattern: &AbsolutePortableGlobPattern, + ) -> Result<&mut GitignoreBuilder, globset::Error> { + let mut glob = IgnoreGlob { + original: pattern.relative().to_string(), + is_allow: false, + is_only_dir: false, + }; + + let mut pattern = pattern.absolute(); + + // File names starting with `!` are escaped with a backslash. Strip the backslash. + // This is not a negated pattern! + if pattern.starts_with("\\!") { + pattern = &pattern[1..]; + } else if let Some(after) = pattern.strip_prefix("!") { + glob.is_allow = true; + pattern = after; + } + + // If it ends with a slash, then this should only match directories, + // but the slash should otherwise not be used while globbing. + if let Some(before) = pattern.strip_suffix('/') { + glob.is_only_dir = true; + pattern = before; + } + + let mut actual = pattern.to_string(); + + // If the glob ends with `/**`, then we should only match everything + // inside a directory, but not the directory itself. Standard globs + // will match the directory. So we add `/*` to force the issue. + if actual.ends_with("/**") { + actual = format!("{actual}/*"); + } + + let parsed = GlobBuilder::new(&actual) + .literal_separator(true) + // No need to support Windows-style paths, so the backslash can be used an escape. + .backslash_escape(true) + .build()?; + + self.builder.add(parsed); + self.globs.push(glob); + + Ok(self) + } +} diff --git a/crates/ty_project/src/glob/include.rs b/crates/ty_project/src/glob/include.rs new file mode 100644 index 0000000000000..c3c61f26f2991 --- /dev/null +++ b/crates/ty_project/src/glob/include.rs @@ -0,0 +1,406 @@ +use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder}; +use regex_automata::dfa; +use regex_automata::dfa::Automaton; +use ruff_db::system::SystemPath; +use std::fmt::Formatter; +use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}; +use tracing::warn; + +use crate::glob::portable::AbsolutePortableGlobPattern; + +/// Chosen at a whim -Konsti +const DFA_SIZE_LIMIT: usize = 1_000_000; + +/// Path filter based on a set of include globs. +/// +/// The patterns are similar to gitignore, but reversed: +/// +/// * `/src`: matches a file or directory with its content named `src` +/// * `/src/`: matches a directory with its content named `src` +/// * `/src/**` or `/src/*`: matches the content of `src`, but not a file named `src` +/// +/// Negated patterns are not supported. +/// +/// Internally, the globs are converted to a regex and then to a DFA, which unlike the globs and the +/// regex allows to check for prefix matches. +/// +/// ## Equality +/// Equality is based on the patterns from which a filter was constructed. +/// +/// Because of that, two filters that include the exact same files but were +/// constructed from different patterns (or even just order) compare unequal. +#[derive(Clone)] +pub(crate) struct IncludeFilter { + glob_set: GlobSet, + original_patterns: Box<[String]>, + dfa: Option>>, +} + +impl IncludeFilter { + /// Whether the file matches any of the globs. + pub(crate) fn match_file(&self, path: impl AsRef) -> bool { + let path = path.as_ref(); + + self.glob_set.is_match(path) + } + + /// Check whether a directory or any of its children can be matched by any of the globs. + /// + /// This never returns `false` if any child matches, but it may return `true` even if we + /// don't end up including any child. + pub(crate) fn match_directory(&self, path: impl AsRef) -> bool { + self.match_directory_impl(path.as_ref()) + } + + fn match_directory_impl(&self, path: &SystemPath) -> bool { + let Some(dfa) = &self.dfa else { + return true; + }; + + // Allow the root path + if path == SystemPath::new("") { + return true; + } + + let config_anchored = + regex_automata::util::start::Config::new().anchored(regex_automata::Anchored::Yes); + let mut state = dfa.start_state(&config_anchored).unwrap(); + + let byte_path = path + .as_str() + .strip_suffix('/') + .unwrap_or(path.as_str()) + .as_bytes(); + for b in byte_path { + state = dfa.next_state(state, *b); + } + // Say we're looking at a directory `foo/bar`. We want to continue if either `foo/bar` is + // a match, e.g., from `foo/*`, or a path below it can match, e.g., from `foo/bar/*`. + let eoi_state = dfa.next_eoi_state(state); + // We must not call `next_eoi_state` on the slash state, we want to only check if more + // characters (path components) are allowed, not if we're matching the `$` anchor at the + // end. + let slash_state = dfa.next_state(state, u8::try_from(MAIN_SEPARATOR).unwrap()); + + debug_assert!( + !dfa.is_quit_state(eoi_state) && !dfa.is_quit_state(slash_state), + "matcher is in quit state" + ); + + dfa.is_match_state(eoi_state) || !dfa.is_dead_state(slash_state) + } +} + +impl std::fmt::Debug for IncludeFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("IncludeFilter") + .field(&self.original_patterns) + .finish_non_exhaustive() + } +} + +impl std::fmt::Display for IncludeFilter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(&self.original_patterns).finish() + } +} + +impl PartialEq for IncludeFilter { + fn eq(&self, other: &Self) -> bool { + self.original_patterns == other.original_patterns + } +} + +impl Eq for IncludeFilter {} + +#[derive(Debug)] +pub(crate) struct IncludeFilterBuilder { + set: GlobSetBuilder, + original_pattern: Vec, + regexes: Vec, +} + +impl IncludeFilterBuilder { + pub(crate) fn new() -> Self { + Self { + set: GlobSetBuilder::new(), + original_pattern: Vec::new(), + regexes: Vec::new(), + } + } + + /// Adds an include pattern to the filter. + pub(crate) fn add( + &mut self, + input: &AbsolutePortableGlobPattern, + ) -> Result<&mut Self, globset::Error> { + let mut glob_pattern = input.absolute(); + + let mut only_directory = false; + + // A pattern ending with a `/` should only match directories. E.g. `src/` only matches directories + // whereas `src` matches both files and directories. + // We need to remove the `/` to ensure that a path missing the trailing `/` matches. + if let Some(after) = glob_pattern.strip_suffix('/') { + // Escaped `/` or `\` aren't allowed. `portable_glob::parse` will error + only_directory = true; + glob_pattern = after; + } + + // If regex ends with `/**`, only push that one glob and regex + // Otherwise, push two regex, one for `/**` and one for without + let glob = GlobBuilder::new(glob_pattern) + .literal_separator(true) + // No need to support Windows-style paths, so the backslash can be used a escape. + .backslash_escape(true) + .build()?; + self.original_pattern.push(input.relative().to_string()); + + // `lib` is the same as `lib/**` + // Add a glob that matches `lib` exactly, change the glob to `lib/**`. + if glob_pattern.ends_with("**") { + self.push_prefix_regex(&glob); + self.set.add(glob); + } else { + let prefix_glob = GlobBuilder::new(&format!("{glob_pattern}/**")) + .literal_separator(true) + // No need to support Windows-style paths, so the backslash can be used a escape. + .backslash_escape(true) + .build()?; + + self.push_prefix_regex(&prefix_glob); + self.set.add(prefix_glob); + + // The reason we add the exact glob, e.g. `src` when the original pattern was `src/` is + // so that `match_file` returns true when matching against a file. However, we don't + // need to do this if this is a pattern that should only match a directory (specifically, its contents). + if !only_directory { + self.set.add(glob); + } + } + + Ok(self) + } + + fn push_prefix_regex(&mut self, glob: &Glob) { + let main_separator = regex::escape(MAIN_SEPARATOR_STR); + + let regex = glob + .regex() + // We are using a custom DFA builder + .strip_prefix("(?-u)") + .expect("a glob is a non-unicode byte regex") + // Match windows paths if applicable + .replace('/', &main_separator); + + self.regexes.push(regex); + } + + /// The filter matches if any of the globs matches. + /// + /// See for the error returned. + pub(crate) fn build(self) -> Result { + let glob_set = self.set.build()?; + + let dfa_builder = dfa::dense::Builder::new() + .syntax( + // The glob regex is a byte matcher + regex_automata::util::syntax::Config::new() + .unicode(false) + .utf8(false), + ) + .configure( + dfa::dense::Config::new() + .start_kind(dfa::StartKind::Anchored) + // DFA can grow exponentially, in which case we bail out + .dfa_size_limit(Some(DFA_SIZE_LIMIT)) + .determinize_size_limit(Some(DFA_SIZE_LIMIT)), + ) + .build_many(&self.regexes); + let dfa = if let Ok(dfa) = dfa_builder { + Some(dfa) + } else { + // TODO(konsti): `regex_automata::dfa::dense::BuildError` should allow asking whether + // is a size error + warn!( + "Glob expressions regex is larger than {DFA_SIZE_LIMIT} bytes, \ + falling back to full directory traversal!" + ); + None + }; + + Ok(IncludeFilter { + glob_set, + dfa, + original_patterns: self.original_pattern.into(), + }) + } +} + +#[cfg(test)] +mod tests { + use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR}; + + use crate::glob::include::{IncludeFilter, IncludeFilterBuilder}; + use crate::glob::{PortableGlobKind, PortableGlobPattern}; + use ruff_db::system::{MemoryFileSystem, walk_directory::WalkState}; + + fn create_filter(patterns: impl IntoIterator) -> IncludeFilter { + let mut builder = IncludeFilterBuilder::new(); + for pattern in patterns { + builder + .add( + &PortableGlobPattern::parse(pattern, PortableGlobKind::Include) + .unwrap() + .into_absolute(""), + ) + .unwrap(); + } + + builder.build().unwrap() + } + + fn setup_files(files: impl IntoIterator) -> MemoryFileSystem { + let fs = MemoryFileSystem::new(); + + fs.write_files_all(files.into_iter().map(|name| (name, ""))) + .unwrap(); + fs + } + + #[track_caller] + fn assert_match_directory(filter: &IncludeFilter, path: &str) { + assert!(filter.match_directory(path.replace('/', MAIN_SEPARATOR_STR))); + } + + #[track_caller] + fn assert_not_match_directory(filter: &IncludeFilter, path: &str) { + assert!(!filter.match_directory(path.replace('/', MAIN_SEPARATOR_STR))); + } + + #[test] + fn match_directory() { + // `lib` is the same as `src/**`. It includes a file or directory (including its contents) + // `src/*`: The same as `src/**` + let filter = create_filter(["lib", "src/*", "tests/**", "a/test-*/b", "files/*.py"]); + + assert_match_directory(&filter, "lib"); + assert_match_directory(&filter, "lib/more/test"); + + assert_match_directory(&filter, "src"); + assert_match_directory(&filter, "src/more/test"); + + assert_match_directory(&filter, "tests"); + assert_match_directory(&filter, "tests/more/test"); + + assert_match_directory(&filter, "a"); + assert_match_directory(&filter, "a/test-b"); + + assert_not_match_directory(&filter, "a/test-b/x"); + assert_not_match_directory(&filter, "a/test"); + + assert_match_directory(&filter, "files/a.py"); + assert_match_directory(&filter, "files/a.py/bcd"); + + assert_not_match_directory(&filter, "not_included"); + assert_not_match_directory(&filter, "files/a.pi"); + } + + #[test] + fn match_file() { + // `lib` is the same as `src/**`. It includes a file or directory (including its contents) + // `src/*`: The same as `src/**` + let filter = create_filter([ + "lib", + "src/*", + "directory/", + "tests/**", + "a/test-*/b", + "files/*.py", + ]); + + assert!(filter.match_file("lib")); + assert!(filter.match_file("lib/more/test")); + + // Unlike `directory`, `directory/` only includes a directory with the given name and its contents + assert!(!filter.match_file("directory")); + assert!(filter.match_file("directory/more/test")); + + // Unlike `src`, `src/*` only includes a directory with the given name. + assert!(!filter.match_file("src")); + assert!(filter.match_file("src/more/test")); + + // Unlike `tests`, `tests/**` only includes files under `tests`, but not a file named tests + assert!(!filter.match_file("tests")); + assert!(filter.match_file("tests/more/test")); + + // Unlike `match_directory`, prefixes should not be included. + assert!(!filter.match_file("a")); + assert!(!filter.match_file("a/test-b")); + + assert!(!filter.match_file("a/test-b/x")); + assert!(!filter.match_file("a/test")); + + assert!(filter.match_file("files/a.py")); + assert!(filter.match_file("files/a.py/bcd")); + + assert!(!filter.match_file("not_included")); + assert!(!filter.match_file("files/a.pi")); + } + + /// Check that we skip directories that can never match. + #[test] + fn prefilter() { + let filter = create_filter(["/a/b/test-*/d", "/a/b/c/e", "/b/c"]); + let fs = setup_files([ + // Should visit + "/a/b/test-a/d", + "/a/b/c/e", + "/b/c", + // Can skip + "/d/e", + "/a/b/x/f", + ]); + + let visited = std::sync::Mutex::new(Vec::new()); + + // Test the prefix filtering + fs.walk_directory("/").run(|| { + Box::new(|entry| { + let entry = entry.unwrap(); + + if entry.file_type().is_directory() { + if !filter.match_directory(entry.path()) { + return WalkState::Skip; + } + } + + visited + .lock() + .unwrap() + .push(entry.path().as_str().replace(MAIN_SEPARATOR, "/")); + + WalkState::Continue + }) + }); + + let mut visited = visited.into_inner().unwrap(); + visited.sort(); + + // Assert that it didn't traverse into `/d` or `/a/b/x` + assert_eq!( + visited, + [ + "/", + "/a", + "/a/b", + "/a/b/c", + "/a/b/c/e", + "/a/b/test-a", + "/a/b/test-a/d", + "/b", + "/b/c" + ] + ); + } +} diff --git a/crates/ty_project/src/glob/portable.rs b/crates/ty_project/src/glob/portable.rs new file mode 100644 index 0000000000000..dd21666cc4e46 --- /dev/null +++ b/crates/ty_project/src/glob/portable.rs @@ -0,0 +1,402 @@ +//! Cross-language glob syntax from +//! [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/). +//! +//! The glob syntax matches the `uv` variant of uv's `uv-globfilter` crate. +//! We intentionally use the same syntax to give users a consistent experience +//! across our tools. +//! +//! [Source](https://github.com/astral-sh/uv/blob/main/crates/uv-globfilter/src/portable_glob.rs) + +use ruff_db::system::SystemPath; +use std::error::Error as _; +use std::ops::Deref; +use std::{fmt::Write, path::MAIN_SEPARATOR}; +use thiserror::Error; + +/// Pattern that only uses cross-language glob syntax based on [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/): +/// +/// - Alphanumeric characters, underscores (`_`), hyphens (`-`) and dots (`.`) are matched verbatim. +/// - The special glob characters are: +/// - `*`: Matches any number of characters except path separators +/// - `?`: Matches a single character except the path separator +/// - `**`: Matches any number of characters including path separators +/// - `[]`, containing only the verbatim matched characters: Matches a single of the characters contained. Within +/// `[...]`, the hyphen indicates a locale-agnostic range (e.g. `a-z`, order based on Unicode code points). Hyphens at +/// the start or end are matched literally. +/// - `\`: It escapes the following character to be matched verbatim (extension to PEP 639). +/// - The path separator is the forward slash character (`/`). Patterns are relative to the given directory, a leading slash +/// character for absolute paths is not supported. +/// - Parent directory indicators (`..`) are not allowed. +/// +/// These rules mean that matching the backslash (`\`) is forbidden, which avoid collisions with the windows path separator. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub(crate) struct PortableGlobPattern<'a> { + pattern: &'a str, + kind: PortableGlobKind, +} + +impl<'a> PortableGlobPattern<'a> { + /// Parses a portable glob pattern. Returns an error if the pattern isn't valid. + pub(crate) fn parse(glob: &'a str, kind: PortableGlobKind) -> Result { + let mut chars = glob.chars().enumerate().peekable(); + + if matches!(kind, PortableGlobKind::Exclude) { + chars.next_if(|(_, c)| *c == '!'); + } + + // A `..` is on a parent directory indicator at the start of the string or after a directory + // separator. + let mut start_or_slash = true; + // The number of consecutive stars before the current character. + while let Some((offset, c)) = chars.next() { + let pos = offset + 1; + + // `***` or `**literals` can be correctly represented with less stars. They are banned by + // `glob`, they are allowed by `globset` and PEP 639 is ambiguous, so we're filtering them + // out. + if c == '*' { + let mut star_run = 1; + while let Some((_, c)) = chars.peek() { + if *c == '*' { + star_run += 1; + chars.next(); + } else { + break; + } + } + if star_run >= 3 { + return Err(PortableGlobError::TooManyStars { + // We don't update pos for the stars. + pos, + }); + } else if star_run == 2 { + if chars.peek().is_some_and(|(_, c)| *c != '/') { + return Err(PortableGlobError::TooManyStars { + // We don't update pos for the stars. + pos, + }); + } + } + start_or_slash = false; + } else if c.is_alphanumeric() || matches!(c, '_' | '-' | '?') { + start_or_slash = false; + } else if c == '.' { + if start_or_slash && matches!(chars.peek(), Some((_, '.'))) { + return Err(PortableGlobError::ParentDirectory { pos }); + } + start_or_slash = false; + } else if c == '/' { + start_or_slash = true; + } else if c == '[' { + for (pos, c) in chars.by_ref() { + if c.is_alphanumeric() || matches!(c, '_' | '-' | '.') { + // Allowed. + } else if c == ']' { + break; + } else { + return Err(PortableGlobError::InvalidCharacterRange { + pos, + invalid: InvalidChar(c), + }); + } + } + start_or_slash = false; + } else if c == '\\' { + match chars.next() { + Some((pos, '/' | '\\')) => { + // For cross-platform compatibility, we don't allow forward slashes or + // backslashes to be escaped. + return Err(PortableGlobError::InvalidEscapee { pos }); + } + Some(_) => { + // Escaped character + } + None => { + return Err(PortableGlobError::TrailingEscape { pos }); + } + } + } else { + return Err(PortableGlobError::InvalidCharacter { + pos, + invalid: InvalidChar(c), + }); + } + } + Ok(PortableGlobPattern { + pattern: glob, + kind, + }) + } + + /// Anchors pattern at `cwd`. + /// + /// `is_exclude` indicates whether this is a pattern in an exclude filter. + /// + /// This method similar to [`SystemPath::absolute`] but for a glob pattern. + /// The main difference is that this method always uses `/` as path separator. + pub(crate) fn into_absolute(self, cwd: impl AsRef) -> AbsolutePortableGlobPattern { + let mut pattern = self.pattern; + let mut negated = false; + + if matches!(self.kind, PortableGlobKind::Exclude) { + // If the pattern starts with `!`, we need to remove it and then anchor the rest. + if let Some(after) = self.pattern.strip_prefix('!') { + pattern = after; + negated = true; + } + } + + if pattern.starts_with('/') { + return AbsolutePortableGlobPattern { + absolute: pattern.to_string(), + relative: self.pattern.to_string(), + }; + } + + let mut rest = pattern; + let mut prefix = cwd.as_ref().to_path_buf().into_utf8_path_buf(); + + loop { + if let Some(after) = rest.strip_prefix("./") { + rest = after; + } else if let Some(after) = rest.strip_prefix("../") { + prefix.pop(); + rest = after; + } else { + break; + } + } + + let mut output = String::with_capacity(prefix.as_str().len() + rest.len()); + + for component in prefix.components() { + match component { + camino::Utf8Component::Prefix(utf8_prefix_component) => { + output.push_str(&utf8_prefix_component.as_str().replace(MAIN_SEPARATOR, "/")); + } + + camino::Utf8Component::RootDir => { + output.push('/'); + continue; + } + camino::Utf8Component::CurDir => {} + camino::Utf8Component::ParentDir => output.push_str("../"), + camino::Utf8Component::Normal(component) => { + output.push_str(component); + output.push('/'); + } + } + } + + output.push_str(rest); + if negated { + // If the pattern is negated, we need to keep the leading `!`. + AbsolutePortableGlobPattern { + absolute: format!("!{output}"), + relative: self.pattern.to_string(), + } + } else { + AbsolutePortableGlobPattern { + absolute: output, + relative: self.pattern.to_string(), + } + } + } +} + +impl Deref for PortableGlobPattern<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.pattern + } +} + +/// A portable glob pattern that uses absolute paths. +/// +/// E.g., `./src/**` becomes `/root/src/**` when anchored to `/root`. +#[derive(Debug, Eq, PartialEq, Hash)] +pub(crate) struct AbsolutePortableGlobPattern { + absolute: String, + relative: String, +} + +impl AbsolutePortableGlobPattern { + /// Returns the absolute path of this glob pattern. + pub(crate) fn absolute(&self) -> &str { + &self.absolute + } + + /// Returns the relative path of this glob pattern. + pub(crate) fn relative(&self) -> &str { + &self.relative + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub(crate) enum PortableGlobKind { + /// An include pattern. Doesn't allow negated patterns. + Include, + + /// An exclude pattern. Allows for negated patterns. + Exclude, +} + +#[derive(Debug, Error)] +pub(crate) enum PortableGlobError { + /// Shows the failing glob in the error message. + #[error("{desc}", desc=.0.description())] + GlobError(#[from] globset::Error), + + #[error("The parent directory operator (`..`) at position {pos} is not allowed")] + ParentDirectory { pos: usize }, + + #[error( + "Invalid character `{invalid}` at position {pos}. hint: Characters can be escaped with a backslash" + )] + InvalidCharacter { pos: usize, invalid: InvalidChar }, + + #[error("Path separators can't be escaped, invalid character at position {pos}")] + InvalidEscapee { pos: usize }, + + #[error("Invalid character `{invalid}` in range at position {pos}")] + InvalidCharacterRange { pos: usize, invalid: InvalidChar }, + + #[error("Too many stars at position {pos}")] + TooManyStars { pos: usize }, + + #[error("Trailing backslash at position {pos}")] + TrailingEscape { pos: usize }, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct InvalidChar(pub char); + +impl std::fmt::Display for InvalidChar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + '\'' => f.write_char('\''), + c => c.escape_debug().fmt(f), + } + } +} + +#[cfg(test)] +mod tests { + + use crate::glob::{PortableGlobKind, PortableGlobPattern}; + use insta::assert_snapshot; + use ruff_db::system::SystemPath; + + #[test] + fn test_error() { + #[track_caller] + fn parse_err(glob: &str) -> String { + let error = PortableGlobPattern::parse(glob, PortableGlobKind::Exclude).unwrap_err(); + error.to_string() + } + + assert_snapshot!( + parse_err(".."), + @"The parent directory operator (`..`) at position 1 is not allowed" + ); + assert_snapshot!( + parse_err("licenses/.."), + @"The parent directory operator (`..`) at position 10 is not allowed" + ); + assert_snapshot!( + parse_err("licenses/LICEN!E.txt"), + @"Invalid character `!` at position 15. hint: Characters can be escaped with a backslash" + ); + assert_snapshot!( + parse_err("licenses/LICEN[!C]E.txt"), + @"Invalid character `!` in range at position 15" + ); + assert_snapshot!( + parse_err("licenses/LICEN[C?]E.txt"), + @"Invalid character `?` in range at position 16" + ); + assert_snapshot!( + parse_err("******"), + @"Too many stars at position 1" + ); + assert_snapshot!( + parse_err("licenses/**license"), + @"Too many stars at position 10" + ); + assert_snapshot!( + parse_err("licenses/***/licenses.csv"), + @"Too many stars at position 10" + ); + assert_snapshot!( + parse_err(r"**/@test"), + @"Invalid character `@` at position 4. hint: Characters can be escaped with a backslash" + ); + // Escapes are not allowed in strict PEP 639 mode + assert_snapshot!( + parse_err(r"public domain/Gulliver\\’s Travels.txt"), + @r"Invalid character ` ` at position 7. hint: Characters can be escaped with a backslash" + ); + assert_snapshot!( + parse_err(r"**/@test"), + @"Invalid character `@` at position 4. hint: Characters can be escaped with a backslash" + ); + // Escaping slashes is not allowed. + assert_snapshot!( + parse_err(r"licenses\\MIT.txt"), + @r"Path separators can't be escaped, invalid character at position 9" + ); + assert_snapshot!( + parse_err(r"licenses\/MIT.txt"), + @r"Path separators can't be escaped, invalid character at position 9" + ); + } + + #[test] + fn test_valid() { + let cases = [ + r"licenses/*.txt", + r"licenses/**/*.txt", + r"LICEN[CS]E.txt", + r"LICEN?E.txt", + r"[a-z].txt", + r"[a-z._-].txt", + r"*/**", + r"LICENSE..txt", + r"LICENSE_file-1.txt", + // (google translate) + r"licenses/라이센스*.txt", + r"licenses/ライセンス*.txt", + r"licenses/执照*.txt", + r"src/**", + ]; + let cases_uv = [ + r"public-domain/Gulliver\’s\ Travels.txt", + // https://github.com/astral-sh/uv/issues/13280 + r"**/\@test", + ]; + for case in cases.iter().chain(cases_uv.iter()) { + PortableGlobPattern::parse(case, PortableGlobKind::Exclude).unwrap(); + } + } + + #[track_caller] + fn assert_absolute_path(pattern: &str, relative_to: impl AsRef, expected: &str) { + let pattern = PortableGlobPattern::parse(pattern, PortableGlobKind::Exclude).unwrap(); + let pattern = pattern.into_absolute(relative_to); + assert_eq!(pattern.absolute(), expected); + } + + #[test] + fn absolute_pattern() { + assert_absolute_path("/src", "/root", "/src"); + assert_absolute_path("./src", "/root", "/root/src"); + } + + #[test] + #[cfg(windows)] + fn absolute_pattern_windows() { + assert_absolute_path("./src", r"C:\root", "C:/root/src"); + assert_absolute_path("./src", r"\\server\test", "//server/test/src"); + } +} diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs new file mode 100644 index 0000000000000..79778d93fa2c1 --- /dev/null +++ b/crates/ty_project/src/lib.rs @@ -0,0 +1,774 @@ +use crate::glob::{GlobFilterCheckMode, IncludeResult}; +use crate::metadata::options::{OptionDiagnostic, ToSettingsError}; +use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; +pub use db::{CheckMode, Db, ProjectDatabase, SalsaMemoryDump}; +use files::{Index, Indexed, IndexedFiles}; +use metadata::settings::Settings; +pub use metadata::{ProjectMetadata, ProjectMetadataError}; +use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic}; +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_db::source::{SourceTextError, source_text}; +use ruff_db::system::{SystemPath, SystemPathBuf}; +use rustc_hash::FxHashSet; +use salsa::Durability; +use salsa::Setter; +use std::backtrace::BacktraceStatus; +use std::panic::{AssertUnwindSafe, UnwindSafe}; +use std::sync::Arc; +use thiserror::Error; +use tracing::error; +use ty_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection}; +use ty_python_semantic::types::check_types; +use ty_python_semantic::{add_inferred_python_version_hint_to_diagnostic, register_lints}; + +pub mod combine; + +mod db; +mod files; +mod glob; +pub mod metadata; +mod walk; +pub mod watch; + +pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock = + std::sync::LazyLock::new(default_lints_registry); + +pub fn default_lints_registry() -> LintRegistry { + let mut builder = LintRegistryBuilder::default(); + register_lints(&mut builder); + builder.build() +} + +/// The project as a Salsa ingredient. +/// +/// ## How is a project different from a program? +/// There are two (related) motivations: +/// +/// 1. Program is defined in `ruff_db` and it can't reference the settings types for the linter and formatter +/// without introducing a cyclic dependency. The project is defined in a higher level crate +/// where it can reference these setting types. +/// 2. Running `ruff check` with different target versions results in different programs (settings) but +/// it remains the same project. That's why program is a narrowed view of the project only +/// holding on to the most fundamental settings required for checking. +#[salsa::input] +#[derive(Debug)] +pub struct Project { + /// The files that are open in the project. + /// + /// Setting the open files to a non-`None` value changes `check` to only check the + /// open files rather than all files in the project. + #[returns(as_deref)] + #[default] + open_fileset: Option>>, + + /// The first-party files of this project. + #[default] + #[returns(ref)] + file_set: IndexedFiles, + + /// The metadata describing the project, including the unresolved options. + /// + /// We box the metadata here because it's a fairly large type and + /// reducing the size of `Project` helps reduce the size of the + /// salsa allocated table for `Project`. + #[returns(deref)] + pub metadata: Box, + + /// The resolved project settings. + /// + /// We box the metadata here because it's a fairly large type and + /// reducing the size of `Project` helps reduce the size of the + /// salsa allocated table for `Project`. + #[returns(deref)] + pub settings: Box, + + /// The paths that should be included when checking this project. + /// + /// The default (when this list is empty) is to include all files in the project root + /// (that satisfy the configured include and exclude patterns). + /// However, it's sometimes desired to only check a subset of the project, e.g. to see + /// the diagnostics for a single file or a folder. + /// + /// This list gets initialized by the paths passed to `ty check ` + /// + /// ## How is this different from `open_files`? + /// + /// The `included_paths` is closely related to `open_files`. The only difference is that + /// `open_files` is already a resolved set of files whereas `included_paths` is only a list of paths + /// that are resolved to files by indexing them. The other difference is that + /// new files added to any directory in `included_paths` will be indexed and added to the project + /// whereas `open_files` needs to be updated manually (e.g. by the IDE). + /// + /// In short, `open_files` is cheaper in contexts where the set of files is known, like + /// in an IDE when the user only wants to check the open tabs. This could be modeled + /// with `included_paths` too but it would require an explicit walk dir step that's simply unnecessary. + #[default] + #[returns(deref)] + included_paths_list: Vec, + + /// Diagnostics that were generated when resolving the project settings. + #[returns(deref)] + settings_diagnostics: Vec, +} + +/// A progress reporter. +pub trait ProgressReporter: Send + Sync { + /// Initialize the reporter with the number of files. + fn set_files(&mut self, files: usize); + + /// Report the completion of a given file. + fn report_file(&self, file: &File); +} + +/// A no-op implementation of [`ProgressReporter`]. +#[derive(Default)] +pub struct DummyReporter; + +impl ProgressReporter for DummyReporter { + fn set_files(&mut self, _files: usize) {} + fn report_file(&self, _file: &File) {} +} + +#[salsa::tracked] +impl Project { + pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Result { + let (settings, diagnostics) = metadata.options().to_settings(db, metadata.root())?; + + let project = Project::builder(Box::new(metadata), Box::new(settings), diagnostics) + .durability(Durability::MEDIUM) + .open_fileset_durability(Durability::LOW) + .file_set_durability(Durability::LOW) + .new(db); + + Ok(project) + } + + pub fn root(self, db: &dyn Db) -> &SystemPath { + self.metadata(db).root() + } + + pub fn name(self, db: &dyn Db) -> &str { + self.metadata(db).name() + } + + /// Returns the resolved linter rules for the project. + /// + /// This is a salsa query to prevent re-computing queries if other, unrelated + /// settings change. For example, we don't want that changing the terminal settings + /// invalidates any type checking queries. + #[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] + pub fn rules(self, db: &dyn Db) -> Arc { + self.settings(db).to_rules() + } + + /// Returns `true` if `path` is both part of the project and included (see `included_paths_list`). + /// + /// Unlike [Self::files], this method does not respect `.gitignore` files. It only checks + /// the project's include and exclude settings as well as the paths that were passed to `ty check `. + /// This means, that this method is an over-approximation of `Self::files` and may return `true` for paths + /// that won't be included when checking the project because they're ignored in a `.gitignore` file. + pub fn is_file_included(self, db: &dyn Db, path: &SystemPath) -> bool { + ProjectFilesFilter::from_project(db, self) + .is_file_included(path, GlobFilterCheckMode::Adhoc) + == IncludeResult::Included + } + + pub fn is_directory_included(self, db: &dyn Db, path: &SystemPath) -> bool { + ProjectFilesFilter::from_project(db, self) + .is_directory_included(path, GlobFilterCheckMode::Adhoc) + == IncludeResult::Included + } + + pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) { + tracing::debug!("Reloading project"); + assert_eq!(self.root(db), metadata.root()); + + if &metadata != self.metadata(db) { + match metadata.options().to_settings(db, metadata.root()) { + Ok((settings, settings_diagnostics)) => { + if self.settings(db) != &settings { + self.set_settings(db).to(Box::new(settings)); + } + + if self.settings_diagnostics(db) != settings_diagnostics { + self.set_settings_diagnostics(db).to(settings_diagnostics); + } + } + Err(error) => { + self.set_settings_diagnostics(db) + .to(vec![error.into_diagnostic()]); + } + } + + self.set_metadata(db).to(Box::new(metadata)); + } + + self.reload_files(db); + } + + /// Checks all open files in the project and its dependencies. + pub(crate) fn check( + self, + db: &ProjectDatabase, + mode: CheckMode, + mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>, + ) -> Vec { + let project_span = tracing::debug_span!("Project::check"); + let _span = project_span.enter(); + + tracing::debug!("Checking project '{name}'", name = self.name(db)); + + let mut diagnostics: Vec = Vec::new(); + diagnostics.extend( + self.settings_diagnostics(db) + .iter() + .map(OptionDiagnostic::to_diagnostic), + ); + + let files = match mode { + CheckMode::OpenFiles => ProjectFiles::new(db, self), + // TODO: Consider open virtual files as well + CheckMode::AllFiles => ProjectFiles::Indexed(self.files(db)), + }; + reporter.set_files(files.len()); + + diagnostics.extend( + files + .diagnostics() + .iter() + .map(IOErrorDiagnostic::to_diagnostic), + ); + + let check_start = ruff_db::Instant::now(); + let file_diagnostics = std::sync::Mutex::new(vec![]); + + { + let db = db.clone(); + let file_diagnostics = &file_diagnostics; + let project_span = &project_span; + let reporter = &reporter; + + rayon::scope(move |scope| { + for file in &files { + let db = db.clone(); + scope.spawn(move |_| { + let check_file_span = + tracing::debug_span!(parent: project_span, "check_file", ?file); + let _entered = check_file_span.entered(); + + let result = check_file_impl(&db, file); + file_diagnostics + .lock() + .unwrap() + .extend(result.iter().map(Clone::clone)); + + reporter.report_file(&file); + }); + } + }); + } + + tracing::debug!( + "Checking all files took {:.3}s", + check_start.elapsed().as_secs_f64(), + ); + + let mut file_diagnostics = file_diagnostics.into_inner().unwrap(); + file_diagnostics.sort_by(|left, right| { + left.rendering_sort_key(db) + .cmp(&right.rendering_sort_key(db)) + }); + diagnostics.extend(file_diagnostics); + diagnostics + } + + pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec { + if !self.is_file_open(db, file) { + return Vec::new(); + } + + check_file_impl(db, file).iter().map(Clone::clone).collect() + } + + /// Opens a file in the project. + /// + /// This changes the behavior of `check` to only check the open files rather than all files in the project. + pub fn open_file(self, db: &mut dyn Db, file: File) { + tracing::debug!("Opening file `{}`", file.path(db)); + + let mut open_files = self.take_open_files(db); + open_files.insert(file); + self.set_open_files(db, open_files); + } + + /// Closes a file in the project. + pub fn close_file(self, db: &mut dyn Db, file: File) -> bool { + tracing::debug!("Closing file `{}`", file.path(db)); + + let mut open_files = self.take_open_files(db); + let removed = open_files.remove(&file); + + if removed { + self.set_open_files(db, open_files); + } + + removed + } + + pub fn set_included_paths(self, db: &mut dyn Db, paths: Vec) { + tracing::debug!("Setting included paths: {paths}", paths = paths.len()); + + self.set_included_paths_list(db).to(paths); + self.reload_files(db); + } + + /// Returns the paths that should be checked. + /// + /// The default is to check the entire project in which case this method returns + /// the project root. However, users can specify to only check specific sub-folders or + /// even files of a project by using `ty check `. In that case, this method + /// returns the provided absolute paths. + /// + /// Note: The CLI doesn't prohibit users from specifying paths outside the project root. + /// This can be useful to check arbitrary files, but it isn't something we recommend. + /// We should try to support this use case but it's okay if there are some limitations around it. + fn included_paths_or_root(self, db: &dyn Db) -> &[SystemPathBuf] { + match self.included_paths_list(db) { + [] => std::slice::from_ref(&self.metadata(db).root), + paths => paths, + } + } + + /// Returns the open files in the project or `None` if the entire project should be checked. + pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet> { + self.open_fileset(db) + } + + /// Sets the open files in the project. + /// + /// This changes the behavior of `check` to only check the open files rather than all files in the project. + #[tracing::instrument(level = "debug", skip(self, db))] + pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet) { + tracing::debug!("Set open project files (count: {})", open_files.len()); + + self.set_open_fileset(db).to(Some(Arc::new(open_files))); + } + + /// This takes the open files from the project and returns them. + /// + /// This changes the behavior of `check` to check all files in the project instead of just the open files. + fn take_open_files(self, db: &mut dyn Db) -> FxHashSet { + tracing::debug!("Take open project files"); + + // Salsa will cancel any pending queries and remove its own reference to `open_files` + // so that the reference counter to `open_files` now drops to 1. + let open_files = self.set_open_fileset(db).to(None); + + if let Some(open_files) = open_files { + Arc::try_unwrap(open_files).unwrap() + } else { + FxHashSet::default() + } + } + + /// Returns `true` if the file is open in the project. + /// + /// A file is considered open when: + /// * explicitly set as an open file using [`open_file`](Self::open_file) + /// * It has a [`SystemPath`] and belongs to a package's `src` files + /// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath) + pub fn is_file_open(self, db: &dyn Db, file: File) -> bool { + let path = file.path(db); + + // Try to return early to avoid adding a dependency on `open_files` or `file_set` which + // both have a durability of `LOW`. + if path.is_vendored_path() { + return false; + } + + if let Some(open_files) = self.open_files(db) { + open_files.contains(&file) + } else if file.path(db).is_system_path() { + self.files(db).contains(&file) + } else { + file.path(db).is_system_virtual_path() + } + } + + #[tracing::instrument(level = "debug", skip(self, db))] + pub fn remove_file(self, db: &mut dyn Db, file: File) { + tracing::debug!( + "Removing file `{}` from project `{}`", + file.path(db), + self.name(db) + ); + + let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { + return; + }; + + index.remove(file); + } + + pub fn add_file(self, db: &mut dyn Db, file: File) { + tracing::debug!( + "Adding file `{}` to project `{}`", + file.path(db), + self.name(db) + ); + + let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { + return; + }; + + index.insert(file); + } + + /// Replaces the diagnostics from indexing the project files with `diagnostics`. + /// + /// This is a no-op if the project files haven't been indexed yet. + pub fn replace_index_diagnostics(self, db: &mut dyn Db, diagnostics: Vec) { + let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { + return; + }; + + index.set_diagnostics(diagnostics); + } + + /// Returns the files belonging to this project. + pub fn files(self, db: &dyn Db) -> Indexed<'_> { + let files = self.file_set(db); + + match files.get() { + Index::Lazy(vacant) => { + let _entered = + tracing::debug_span!("Project::index_files", project = %self.name(db)) + .entered(); + let start = ruff_db::Instant::now(); + + let walker = ProjectFilesWalker::new(db); + let (files, diagnostics) = walker.collect_set(db); + + tracing::info!( + "Indexed {} file(s) in {:.3}s", + files.len(), + start.elapsed().as_secs_f64() + ); + vacant.set(files, diagnostics) + } + Index::Indexed(indexed) => indexed, + } + } + + pub fn reload_files(self, db: &mut dyn Db) { + tracing::debug!("Reloading files for project `{}`", self.name(db)); + + if !self.file_set(db).is_lazy() { + // Force a re-index of the files in the next revision. + self.set_file_set(db).to(IndexedFiles::lazy()); + } + } +} + +#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> { + let mut diagnostics: Vec = Vec::new(); + + // Abort checking if there are IO errors. + let source = source_text(db, file); + + if let Some(read_error) = source.read_error() { + diagnostics.push( + IOErrorDiagnostic { + file: Some(file), + error: read_error.clone().into(), + } + .to_diagnostic(), + ); + return diagnostics.into_boxed_slice(); + } + + let parsed = parsed_module(db, file); + + let parsed_ref = parsed.load(db); + diagnostics.extend( + parsed_ref + .errors() + .iter() + .map(|error| Diagnostic::invalid_syntax(file, &error.error, error)), + ); + + diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| { + let mut error = Diagnostic::invalid_syntax(file, error, error); + add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax"); + error + })); + + { + let db = AssertUnwindSafe(db); + match catch(&**db, file, || check_types(*db, file)) { + Ok(Some(type_check_diagnostics)) => { + diagnostics.extend(type_check_diagnostics); + } + Ok(None) => {} + Err(diagnostic) => diagnostics.push(diagnostic), + } + } + + if db + .project() + .open_fileset(db) + .is_none_or(|files| !files.contains(&file)) + { + // Drop the AST now that we are done checking this file. It is not currently open, + // so it is unlikely to be accessed again soon. If any queries need to access the AST + // from across files, it will be re-parsed. + parsed.clear(); + } + + diagnostics.sort_unstable_by_key(|diagnostic| { + diagnostic + .primary_span() + .and_then(|span| span.range()) + .unwrap_or_default() + .start() + }); + + diagnostics.into_boxed_slice() +} + +#[derive(Debug)] +enum ProjectFiles<'a> { + OpenFiles(&'a FxHashSet), + Indexed(files::Indexed<'a>), +} + +impl<'a> ProjectFiles<'a> { + fn new(db: &'a dyn Db, project: Project) -> Self { + if let Some(open_files) = project.open_files(db) { + ProjectFiles::OpenFiles(open_files) + } else { + ProjectFiles::Indexed(project.files(db)) + } + } + + fn diagnostics(&self) -> &[IOErrorDiagnostic] { + match self { + ProjectFiles::OpenFiles(_) => &[], + ProjectFiles::Indexed(indexed) => indexed.diagnostics(), + } + } + + fn len(&self) -> usize { + match self { + ProjectFiles::OpenFiles(open_files) => open_files.len(), + ProjectFiles::Indexed(indexed) => indexed.len(), + } + } +} + +impl<'a> IntoIterator for &'a ProjectFiles<'a> { + type Item = File; + type IntoIter = ProjectFilesIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()), + ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed { + files: indexed.into_iter(), + }, + } + } +} + +enum ProjectFilesIter<'db> { + OpenFiles(std::collections::hash_set::Iter<'db, File>), + Indexed { files: files::IndexedIter<'db> }, +} + +impl Iterator for ProjectFilesIter<'_> { + type Item = File; + + fn next(&mut self) -> Option { + match self { + ProjectFilesIter::OpenFiles(files) => files.next().copied(), + ProjectFilesIter::Indexed { files } => files.next(), + } + } +} + +#[derive(Debug, Clone)] +pub struct IOErrorDiagnostic { + file: Option, + error: IOErrorKind, +} + +impl IOErrorDiagnostic { + fn to_diagnostic(&self) -> Diagnostic { + let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error); + if let Some(file) = self.file { + diag.annotate(Annotation::primary(Span::from(file))); + } + diag + } +} + +#[derive(Error, Debug, Clone)] +enum IOErrorKind { + #[error(transparent)] + Walk(#[from] walk::WalkError), + + #[error(transparent)] + SourceText(#[from] SourceTextError), +} + +fn catch(db: &dyn Db, file: File, f: F) -> Result, Diagnostic> +where + F: FnOnce() -> R + UnwindSafe, +{ + match ruff_db::panic::catch_unwind(|| { + // Ignore salsa errors + salsa::Cancelled::catch(f).ok() + }) { + Ok(result) => Ok(result), + Err(error) => { + use std::fmt::Write; + let mut message = String::new(); + message.push_str("Panicked"); + + if let Some(location) = error.location { + let _ = write!(&mut message, " at {location}"); + } + + let _ = write!( + &mut message, + " when checking `{file}`", + file = file.path(db) + ); + + if let Some(payload) = error.payload.as_str() { + let _ = write!(&mut message, ": `{payload}`"); + } + + let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "This indicates a bug in ty.", + )); + + let report_message = "If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!"; + diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message)); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!( + "Platform: {os} {arch}", + os = std::env::consts::OS, + arch = std::env::consts::ARCH + ), + )); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!( + "Args: {args:?}", + args = std::env::args().collect::>() + ), + )); + + if let Some(backtrace) = error.backtrace { + match backtrace.status() { + BacktraceStatus::Disabled => { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information", + )); + } + BacktraceStatus::Captured => { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!("Backtrace:\n{backtrace}"), + )); + } + _ => {} + } + } + + if let Some(backtrace) = error.salsa_backtrace { + salsa::attach(db, || { + diagnostic.sub(SubDiagnostic::new(Severity::Info, backtrace.to_string())); + }); + } + + Err(diagnostic) + } + } +} + +#[cfg(test)] +mod tests { + use crate::ProjectMetadata; + use crate::check_file_impl; + use crate::db::tests::TestDb; + use ruff_db::Db as _; + use ruff_db::files::system_path_to_file; + use ruff_db::source::source_text; + use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf}; + use ruff_db::testing::assert_function_query_was_not_run; + use ruff_python_ast::name::Name; + use ty_python_semantic::types::check_types; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings, + }; + + #[test] + fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { + let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/")); + let mut db = TestDb::new(project); + let path = SystemPath::new("test.py"); + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]) + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search path settings"), + }, + ); + + db.write_file(path, "x = 10")?; + let file = system_path_to_file(&db, path).unwrap(); + + // Now the file gets deleted before we had a chance to read its source text. + db.memory_file_system().remove_file(path)?; + file.sync(&mut db); + + assert_eq!(source_text(&db, file).as_str(), ""); + assert_eq!( + check_file_impl(&db, file) + .iter() + .map(|diagnostic| diagnostic.primary_message().to_string()) + .collect::>(), + vec!["Failed to read file: No such file or directory".to_string()] + ); + + let events = db.take_salsa_events(); + assert_function_query_was_not_run(&db, check_types, file, &events); + + // The user now creates a new file with an empty text. The source text + // content returned by `source_text` remains unchanged, but the diagnostics should get updated. + db.write_file(path, "").unwrap(); + + assert_eq!(source_text(&db, file).as_str(), ""); + assert_eq!( + check_file_impl(&db, file) + .iter() + .map(|diagnostic| diagnostic.primary_message().to_string()) + .collect::>(), + vec![] as Vec + ); + + Ok(()) + } +} diff --git a/crates/red_knot_project/src/metadata.rs b/crates/ty_project/src/metadata.rs similarity index 82% rename from crates/red_knot_project/src/metadata.rs rename to crates/ty_project/src/metadata.rs index dc80da724e5fe..fe99c6df008eb 100644 --- a/crates/red_knot_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -1,15 +1,16 @@ use configuration_file::{ConfigurationFile, ConfigurationFileError}; -use red_knot_python_semantic::ProgramSettings; use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::name::Name; use std::sync::Arc; use thiserror::Error; +use ty_python_semantic::ProgramSettings; use crate::combine::Combine; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; use crate::metadata::value::ValueSource; -use options::KnotTomlError; -use options::Options; +pub use options::Options; +use options::TyTomlError; mod configuration_file; pub mod options; @@ -48,16 +49,36 @@ impl ProjectMetadata { } } + pub fn from_config_file( + path: SystemPathBuf, + system: &dyn System, + ) -> Result { + tracing::debug!("Using overridden configuration file at '{path}'"); + + let config_file = ConfigurationFile::from_path(path.clone(), system).map_err(|error| { + ProjectMetadataError::ConfigurationFileError { + source: Box::new(error), + path: path.clone(), + } + })?; + + let options = config_file.into_options(); + + Ok(Self { + name: Name::new(system.current_directory().file_name().unwrap_or("root")), + root: system.current_directory().to_path_buf(), + options, + extra_configuration_paths: vec![path], + }) + } + /// Loads a project from a `pyproject.toml` file. pub(crate) fn from_pyproject( pyproject: PyProject, root: SystemPathBuf, ) -> Result { Self::from_options( - pyproject - .tool - .and_then(|tool| tool.knot) - .unwrap_or_default(), + pyproject.tool.and_then(|tool| tool.ty).unwrap_or_default(), root, pyproject.project.as_ref(), ) @@ -103,17 +124,17 @@ impl ProjectMetadata { /// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence /// the resolve the project's root. /// - /// 1. The closest `pyproject.toml` with a `tool.knot` section or `knot.toml`. + /// 1. The closest `pyproject.toml` with a `tool.ty` section or `ty.toml`. /// 1. The closest `pyproject.toml`. /// 1. Fallback to use `path` as the root and use the default settings. pub fn discover( path: &SystemPath, system: &dyn System, - ) -> Result { + ) -> Result { tracing::debug!("Searching for a project in '{path}'"); if !system.is_directory(path) { - return Err(ProjectDiscoveryError::NotADirectory(path.to_path_buf())); + return Err(ProjectMetadataError::NotADirectory(path.to_path_buf())); } let mut closest_project: Option = None; @@ -128,38 +149,40 @@ impl ProjectMetadata { ) { Ok(pyproject) => Some(pyproject), Err(error) => { - return Err(ProjectDiscoveryError::InvalidPyProject { + return Err(ProjectMetadataError::InvalidPyProject { path: pyproject_path, source: Box::new(error), - }) + }); } } } else { None }; - // A `knot.toml` takes precedence over a `pyproject.toml`. - let knot_toml_path = project_root.join("knot.toml"); - if let Ok(knot_str) = system.read_to_string(&knot_toml_path) { + // A `ty.toml` takes precedence over a `pyproject.toml`. + let ty_toml_path = project_root.join("ty.toml"); + if let Ok(ty_str) = system.read_to_string(&ty_toml_path) { let options = match Options::from_toml_str( - &knot_str, - ValueSource::File(Arc::new(knot_toml_path.clone())), + &ty_str, + ValueSource::File(Arc::new(ty_toml_path.clone())), ) { Ok(options) => options, Err(error) => { - return Err(ProjectDiscoveryError::InvalidKnotToml { - path: knot_toml_path, + return Err(ProjectMetadataError::InvalidTyToml { + path: ty_toml_path, source: Box::new(error), - }) + }); } }; if pyproject .as_ref() - .is_some_and(|project| project.knot().is_some()) + .is_some_and(|project| project.ty().is_some()) { // TODO: Consider using a diagnostic here - tracing::warn!("Ignoring the `tool.knot` section in `{pyproject_path}` because `{knot_toml_path}` takes precedence."); + tracing::warn!( + "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." + ); } tracing::debug!("Found project at '{}'", project_root); @@ -172,7 +195,7 @@ impl ProjectMetadata { .and_then(|pyproject| pyproject.project.as_ref()), ) .map_err(|err| { - ProjectDiscoveryError::InvalidRequiresPythonConstraint { + ProjectMetadataError::InvalidRequiresPythonConstraint { source: err, path: pyproject_path, } @@ -182,17 +205,17 @@ impl ProjectMetadata { } if let Some(pyproject) = pyproject { - let has_knot_section = pyproject.knot().is_some(); + let has_ty_section = pyproject.ty().is_some(); let metadata = ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) .map_err( - |err| ProjectDiscoveryError::InvalidRequiresPythonConstraint { + |err| ProjectMetadataError::InvalidRequiresPythonConstraint { source: err, path: pyproject_path, }, )?; - if has_knot_section { + if has_ty_section { tracing::debug!("Found project at '{}'", project_root); return Ok(metadata); @@ -208,13 +231,15 @@ impl ProjectMetadata { // No project found, but maybe a pyproject.toml was found. let metadata = if let Some(closest_project) = closest_project { tracing::debug!( - "Project without `tool.knot` section: '{}'", + "Project without `tool.ty` section: '{}'", closest_project.root() ); closest_project } else { - tracing::debug!("The ancestor directories contain no `pyproject.toml`. Falling back to a virtual project."); + tracing::debug!( + "The ancestor directories contain no `pyproject.toml`. Falling back to a virtual project." + ); // Create a project with a default configuration Self::new( @@ -242,12 +267,17 @@ impl ProjectMetadata { &self.extra_configuration_paths } - pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings { - self.options.to_program_settings(self.root(), system) + pub fn to_program_settings( + &self, + system: &dyn System, + vendored: &VendoredFileSystem, + ) -> anyhow::Result { + self.options + .to_program_settings(self.root(), self.name(), system, vendored) } /// Combine the project options with the CLI options where the CLI options take precedence. - pub fn apply_cli_options(&mut self, options: Options) { + pub fn apply_options(&mut self, options: Options) { self.options = options.combine(std::mem::take(&mut self.options)); } @@ -280,7 +310,7 @@ impl ProjectMetadata { } #[derive(Debug, Error)] -pub enum ProjectDiscoveryError { +pub enum ProjectMetadataError { #[error("project path '{0}' is not a directory")] NotADirectory(SystemPathBuf), @@ -290,9 +320,9 @@ pub enum ProjectDiscoveryError { path: SystemPathBuf, }, - #[error("{path} is not a valid `knot.toml`: {source}")] - InvalidKnotToml { - source: Box, + #[error("{path} is not a valid `ty.toml`: {source}")] + InvalidTyToml { + source: Box, path: SystemPathBuf, }, @@ -301,18 +331,24 @@ pub enum ProjectDiscoveryError { source: ResolveRequiresPythonError, path: SystemPathBuf, }, + + #[error("Error loading configuration file at {path}: {source}")] + ConfigurationFileError { + source: Box, + path: SystemPathBuf, + }, } #[cfg(test)] mod tests { //! Integration tests for project discovery - use anyhow::{anyhow, Context}; + use anyhow::{Context, anyhow}; use insta::assert_ron_snapshot; use ruff_db::system::{SystemPathBuf, TestSystem}; use ruff_python_ast::PythonVersion; - use crate::{ProjectDiscoveryError, ProjectMetadata}; + use crate::{ProjectMetadata, ProjectMetadataError}; #[test] fn project_without_pyproject() -> anyhow::Result<()> { @@ -400,7 +436,7 @@ mod tests { [project] name = "backend" - [tool.knot + [tool.ty "#, ), (root.join("db/__init__.py"), ""), @@ -408,15 +444,17 @@ mod tests { .context("Failed to write files")?; let Err(error) = ProjectMetadata::discover(&root, &system) else { - return Err(anyhow!("Expected project discovery to fail because of invalid syntax in the pyproject.toml")); + return Err(anyhow!( + "Expected project discovery to fail because of invalid syntax in the pyproject.toml" + )); }; assert_error_eq( &error, - r#"/app/pyproject.toml is not a valid `pyproject.toml`: TOML parse error at line 5, column 31 + r#"/app/pyproject.toml is not a valid `pyproject.toml`: TOML parse error at line 5, column 29 | -5 | [tool.knot - | ^ +5 | [tool.ty + | ^ invalid table header expected `.`, `]` "#, @@ -439,7 +477,7 @@ expected `.`, `]` [project] name = "project-root" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -449,7 +487,7 @@ expected `.`, `]` [project] name = "nested-project" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -489,7 +527,7 @@ expected `.`, `]` [project] name = "project-root" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -499,7 +537,7 @@ expected `.`, `]` [project] name = "nested-project" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -526,7 +564,7 @@ expected `.`, `]` } #[test] - fn nested_projects_without_knot_sections() -> anyhow::Result<()> { + fn nested_projects_without_ty_sections() -> anyhow::Result<()> { let system = TestSystem::default(); let root = SystemPathBuf::from("/app"); @@ -566,7 +604,7 @@ expected `.`, `]` } #[test] - fn nested_projects_with_outer_knot_section() -> anyhow::Result<()> { + fn nested_projects_with_outer_ty_section() -> anyhow::Result<()> { let system = TestSystem::default(); let root = SystemPathBuf::from("/app"); @@ -579,7 +617,7 @@ expected `.`, `]` [project] name = "project-root" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.10" "#, ), @@ -612,12 +650,12 @@ expected `.`, `]` Ok(()) } - /// A `knot.toml` takes precedence over any `pyproject.toml`. + /// A `ty.toml` takes precedence over any `pyproject.toml`. /// /// However, the `pyproject.toml` is still loaded to get the project name and, in the future, /// the requires-python constraint. #[test] - fn project_with_knot_and_pyproject_toml() -> anyhow::Result<()> { + fn project_with_ty_and_pyproject_toml() -> anyhow::Result<()> { let system = TestSystem::default(); let root = SystemPathBuf::from("/app"); @@ -631,12 +669,12 @@ expected `.`, `]` name = "super-app" requires-python = ">=3.12" - [tool.knot.src] + [tool.ty.src] root = "this_option_is_ignored" "#, ), ( - root.join("knot.toml"), + root.join("ty.toml"), r#" [src] root = "src" @@ -834,7 +872,7 @@ expected `.`, `]` [project] requires-python = ">=3.12" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.10" "#, ) @@ -871,10 +909,15 @@ expected `.`, `]` .context("Failed to write file")?; let Err(error) = ProjectMetadata::discover(&root, &system) else { - return Err(anyhow!("Expected project discovery to fail because the `requires-python` doesn't specify a lower bound (it only specifies an upper bound).")); + return Err(anyhow!( + "Expected project discovery to fail because the `requires-python` doesn't specify a lower bound (it only specifies an upper bound)." + )); }; - assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `<3.12` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`."); + assert_error_eq( + &error, + "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `<3.12` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.", + ); Ok(()) } @@ -896,10 +939,15 @@ expected `.`, `]` .context("Failed to write file")?; let Err(error) = ProjectMetadata::discover(&root, &system) else { - return Err(anyhow!("Expected project discovery to fail because the `requires-python` specifiers are empty and don't define a lower bound.")); + return Err(anyhow!( + "Expected project discovery to fail because the `requires-python` specifiers are empty and don't define a lower bound." + )); }; - assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`."); + assert_error_eq( + &error, + "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.", + ); Ok(()) } @@ -921,16 +969,21 @@ expected `.`, `]` .context("Failed to write file")?; let Err(error) = ProjectMetadata::discover(&root, &system) else { - return Err(anyhow!("Expected project discovery to fail because of the requires-python major version that is larger than 255.")); + return Err(anyhow!( + "Expected project discovery to fail because of the requires-python major version that is larger than 255." + )); }; - assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): The major version `999` is larger than the maximum supported value 255"); + assert_error_eq( + &error, + "Invalid `requires-python` version specifier (`/app/pyproject.toml`): The major version `999` is larger than the maximum supported value 255", + ); Ok(()) } #[track_caller] - fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) { + fn assert_error_eq(error: &ProjectMetadataError, message: &str) { assert_eq!(error.to_string().replace('\\', "/"), message); } diff --git a/crates/ty_project/src/metadata/configuration_file.rs b/crates/ty_project/src/metadata/configuration_file.rs new file mode 100644 index 0000000000000..ad985cdff5c70 --- /dev/null +++ b/crates/ty_project/src/metadata/configuration_file.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; + +use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use thiserror::Error; + +use crate::metadata::value::ValueSource; + +use super::options::{Options, TyTomlError}; + +/// A `ty.toml` configuration file with the options it contains. +pub(crate) struct ConfigurationFile { + path: SystemPathBuf, + options: Options, +} + +impl ConfigurationFile { + pub(crate) fn from_path( + path: SystemPathBuf, + system: &dyn System, + ) -> Result { + let ty_toml_str = system.read_to_string(&path).map_err(|source| { + ConfigurationFileError::FileReadError { + source, + path: path.clone(), + } + })?; + + match Options::from_toml_str(&ty_toml_str, ValueSource::File(Arc::new(path.clone()))) { + Ok(options) => Ok(Self { path, options }), + Err(error) => Err(ConfigurationFileError::InvalidTyToml { + source: Box::new(error), + path, + }), + } + } + /// Loads the user-level configuration file if it exists. + /// + /// Returns `None` if the file does not exist or if the concept of user-level configurations + /// doesn't exist on `system`. + pub(crate) fn user(system: &dyn System) -> Result, ConfigurationFileError> { + let Some(configuration_directory) = system.user_config_directory() else { + return Ok(None); + }; + + let ty_toml_path = configuration_directory.join("ty").join("ty.toml"); + + tracing::debug!( + "Searching for a user-level configuration at `{path}`", + path = &ty_toml_path + ); + + let Ok(ty_toml_str) = system.read_to_string(&ty_toml_path) else { + return Ok(None); + }; + + match Options::from_toml_str( + &ty_toml_str, + ValueSource::File(Arc::new(ty_toml_path.clone())), + ) { + Ok(options) => Ok(Some(Self { + path: ty_toml_path, + options, + })), + Err(error) => Err(ConfigurationFileError::InvalidTyToml { + source: Box::new(error), + path: ty_toml_path, + }), + } + } + + /// Returns the path to the configuration file. + pub(crate) fn path(&self) -> &SystemPath { + &self.path + } + + pub(crate) fn into_options(self) -> Options { + self.options + } +} + +#[derive(Debug, Error)] +pub enum ConfigurationFileError { + #[error("{path} is not a valid `ty.toml`: {source}")] + InvalidTyToml { + source: Box, + path: SystemPathBuf, + }, + #[error("Failed to read `{path}`: {source}")] + FileReadError { + #[source] + source: std::io::Error, + path: SystemPathBuf, + }, +} diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs new file mode 100644 index 0000000000000..acb89e8cc1ca8 --- /dev/null +++ b/crates/ty_project/src/metadata/options.rs @@ -0,0 +1,1499 @@ +use crate::Db; +use crate::combine::Combine; +use crate::glob::{ExcludeFilter, IncludeExcludeFilter, IncludeFilter, PortableGlobKind}; +use crate::metadata::settings::{OverrideSettings, SrcSettings}; + +use super::settings::{Override, Settings, TerminalSettings}; +use crate::metadata::value::{ + RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource, ValueSourceGuard, +}; +use anyhow::Context; +use ordermap::OrderMap; +use ruff_db::RustDoc; +use ruff_db::diagnostic::{ + Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity, + Span, SubDiagnostic, +}; +use ruff_db::files::system_path_to_file; +use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_db::vendored::VendoredFileSystem; +use ruff_macros::{Combine, OptionsMetadata, RustDoc}; +use ruff_options_metadata::{OptionSet, OptionsMetadata, Visit}; +use ruff_python_ast::PythonVersion; +use rustc_hash::FxHasher; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::fmt::{self, Debug, Display}; +use std::hash::BuildHasherDefault; +use std::ops::Deref; +use std::sync::Arc; +use thiserror::Error; +use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; +use ty_python_semantic::{ + ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionFileSource, + PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError, + SearchPaths, SitePackagesPaths, SysPrefixPathOrigin, +}; + +#[derive( + Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, OptionsMetadata, +)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + /// Configures the type checking environment. + #[option_group] + #[serde(skip_serializing_if = "Option::is_none")] + pub environment: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[option_group] + pub src: Option, + + /// Configures the enabled rules and their severity. + /// + /// See [the rules documentation](https://ty.dev/rules) for a list of all available rules. + /// + /// Valid severities are: + /// + /// * `ignore`: Disable the rule. + /// * `warn`: Enable the rule and create a warning diagnostic. + /// * `error`: Enable the rule and create an error diagnostic. + /// ty will exit with a non-zero code if any error diagnostics are emitted. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"{...}"#, + value_type = r#"dict[RuleName, "ignore" | "warn" | "error"]"#, + example = r#" + [tool.ty.rules] + possibly-unresolved-reference = "warn" + division-by-zero = "ignore" + "# + )] + pub rules: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[option_group] + pub terminal: Option, + + /// Override configurations for specific file patterns. + /// + /// Each override specifies include/exclude patterns and rule configurations + /// that apply to matching files. Multiple overrides can match the same file, + /// with later overrides taking precedence. + #[serde(skip_serializing_if = "Option::is_none")] + #[option_group] + pub overrides: Option, +} + +impl Options { + pub fn from_toml_str(content: &str, source: ValueSource) -> Result { + 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 + where + D: serde::Deserializer<'de>, + { + let _guard = ValueSourceGuard::new(source, false); + Self::deserialize(deserializer) + } + + pub(crate) fn to_program_settings( + &self, + project_root: &SystemPath, + project_name: &str, + system: &dyn System, + vendored: &VendoredFileSystem, + ) -> anyhow::Result { + let environment = self.environment.or_default(); + + let options_python_version = + environment + .python_version + .as_ref() + .map(|ranged_version| PythonVersionWithSource { + version: **ranged_version, + source: match ranged_version.source() { + ValueSource::Cli => PythonVersionSource::Cli, + ValueSource::File(path) => PythonVersionSource::ConfigFile( + PythonVersionFileSource::new(path.clone(), ranged_version.range()), + ), + }, + }); + + let python_platform = environment + .python_platform + .as_deref() + .cloned() + .unwrap_or_else(|| { + let default = PythonPlatform::default(); + tracing::info!("Defaulting to python-platform `{default}`"); + default + }); + + let python_environment = if let Some(python_path) = environment.python.as_ref() { + let origin = match python_path.source() { + ValueSource::Cli => SysPrefixPathOrigin::PythonCliFlag, + ValueSource::File(path) => { + SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range()) + } + }; + + Some(PythonEnvironment::new( + python_path.absolute(project_root, system), + origin, + system, + )?) + } else { + PythonEnvironment::discover(project_root, system) + .context("Failed to discover local Python environment")? + }; + + let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() { + python_environment + .site_packages_paths(system) + .context("Failed to discover the site-packages directory")? + } else { + tracing::debug!("No virtual environment found"); + + SitePackagesPaths::default() + }; + + let python_version = options_python_version + .or_else(|| { + python_environment + .as_ref()? + .python_version_from_metadata() + .cloned() + }) + .or_else(|| site_packages_paths.python_version_from_layout()) + .unwrap_or_default(); + + let search_paths = self.to_search_paths( + project_root, + project_name, + site_packages_paths, + system, + vendored, + )?; + + tracing::info!( + "Python version: Python {python_version}, platform: {python_platform}", + python_version = python_version.version + ); + + Ok(ProgramSettings { + python_version, + python_platform, + search_paths, + }) + } + + fn to_search_paths( + &self, + project_root: &SystemPath, + project_name: &str, + site_packages_paths: SitePackagesPaths, + system: &dyn System, + vendored: &VendoredFileSystem, + ) -> Result { + let environment = self.environment.or_default(); + let src = self.src.or_default(); + + #[allow(deprecated)] + let src_roots = if let Some(roots) = environment + .root + .as_deref() + .or_else(|| Some(std::slice::from_ref(src.root.as_ref()?))) + { + roots + .iter() + .map(|root| root.absolute(project_root, system)) + .collect() + } else { + let src = project_root.join("src"); + + let mut roots = if system.is_directory(&src) { + // Default to `src` and the project root if `src` exists and the root hasn't been specified. + // This corresponds to the `src-layout` + tracing::debug!( + "Including `.` and `./src` in `environment.root` because a `./src` directory exists" + ); + vec![project_root.to_path_buf(), src] + } else if system.is_directory(&project_root.join(project_name).join(project_name)) { + // `src-layout` but when the folder isn't called `src` but has the same name as the project. + // For example, the "src" folder for `psycopg` is called `psycopg` and the python files are in `psycopg/psycopg/_adapters_map.py` + tracing::debug!( + "Including `.` and `/{project_name}` in `environment.root` because a `./{project_name}/{project_name}` directory exists" + ); + + vec![project_root.to_path_buf(), project_root.join(project_name)] + } else { + // Default to a [flat project structure](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). + tracing::debug!("Including `.` in `environment.root`"); + vec![project_root.to_path_buf()] + }; + + // Considering pytest test discovery conventions, + // we also include the `tests` directory if it exists and is not a package. + let tests_dir = project_root.join("tests"); + if system.is_directory(&tests_dir) + && !system.is_file(&tests_dir.join("__init__.py")) + && !roots.contains(&tests_dir) + { + // If the `tests` directory exists and is not a package, include it as a source root. + tracing::debug!( + "Including `./tests` in `environment.root` because a `./tests` directory exists" + ); + + roots.push(tests_dir); + } + + roots + }; + + let settings = SearchPathSettings { + extra_paths: environment + .extra_paths + .as_deref() + .unwrap_or_default() + .iter() + .map(|path| path.absolute(project_root, system)) + .collect(), + src_roots, + custom_typeshed: environment + .typeshed + .as_ref() + .map(|path| path.absolute(project_root, system)), + site_packages_paths: site_packages_paths.into_vec(), + }; + + settings.to_search_paths(system, vendored) + } + + pub(crate) fn to_settings( + &self, + db: &dyn Db, + project_root: &SystemPath, + ) -> Result<(Settings, Vec), ToSettingsError> { + let mut diagnostics = Vec::new(); + let rules = self.to_rule_selection(db, &mut diagnostics); + + let terminal_options = self.terminal.or_default(); + let terminal = TerminalSettings { + output_format: terminal_options + .output_format + .as_deref() + .copied() + .unwrap_or_default(), + error_on_warning: terminal_options.error_on_warning.unwrap_or_default(), + }; + + let src_options = self.src.or_default(); + + #[allow(deprecated)] + if let Some(src_root) = src_options.root.as_ref() { + let mut diagnostic = OptionDiagnostic::new( + DiagnosticId::DeprecatedSetting, + "The `src.root` setting is deprecated. Use `environment.root` instead.".to_string(), + Severity::Warning, + ); + + if let Some(file) = src_root + .source() + .file() + .and_then(|path| system_path_to_file(db, path).ok()) + { + diagnostic = diagnostic.with_annotation(Some(Annotation::primary( + Span::from(file).with_optional_range(src_root.range()), + ))); + } + + if self.environment.or_default().root.is_some() { + diagnostic = diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "The `src.root` setting was ignored in favor of the `environment.root` setting", + )); + } + + diagnostics.push(diagnostic); + } + + let src = src_options + .to_settings(db, project_root, &mut diagnostics) + .map_err(|err| ToSettingsError { + diagnostic: err, + output_format: terminal.output_format, + color: colored::control::SHOULD_COLORIZE.should_colorize(), + })?; + + let overrides = self + .to_overrides_settings(db, project_root, &mut diagnostics) + .map_err(|err| ToSettingsError { + diagnostic: err, + output_format: terminal.output_format, + color: colored::control::SHOULD_COLORIZE.should_colorize(), + })?; + + let settings = Settings { + rules: Arc::new(rules), + terminal, + src, + overrides, + }; + + Ok((settings, diagnostics)) + } + + #[must_use] + fn to_rule_selection( + &self, + db: &dyn Db, + diagnostics: &mut Vec, + ) -> RuleSelection { + self.rules.or_default().to_rule_selection(db, diagnostics) + } + + fn to_overrides_settings( + &self, + db: &dyn Db, + project_root: &SystemPath, + diagnostics: &mut Vec, + ) -> Result, Box> { + let override_options = &**self.overrides.or_default(); + + let mut overrides = Vec::with_capacity(override_options.len()); + + for override_option in override_options { + let override_instance = + override_option.to_override(db, project_root, self.rules.as_ref(), diagnostics)?; + + if let Some(value) = override_instance { + overrides.push(value); + } + } + + Ok(overrides) + } +} + +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct EnvironmentOptions { + /// The root paths of the project, used for finding first-party modules. + /// + /// Accepts a list of directory paths searched in priority order (first has highest priority). + /// + /// If left unspecified, ty will try to detect common project layouts and initialize `root` accordingly: + /// + /// * if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) + /// * if a `.//` directory exists, include `.` and `./` in the first party search path + /// * otherwise, default to `.` (flat layout) + /// + /// Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), + /// it will also be included in the first party search path. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = "list[str]", + example = r#" + # Multiple directories (priority order) + root = ["./src", "./lib", "./vendor"] + "# + )] + pub root: Option>, + + /// Specifies the version of Python that will be used to analyze the source code. + /// The version should be specified as a string in the format `M.m` where `M` is the major version + /// and `m` is the minor (e.g. `"3.0"` or `"3.6"`). + /// If a version is provided, ty will generate errors if the source code makes use of language features + /// that are not supported in that version. + /// + /// If a version is not specified, ty will try the following techniques in order of preference + /// to determine a value: + /// 1. Check for the `project.requires-python` setting in a `pyproject.toml` file + /// and use the minimum version from the specified range + /// 2. Check for an activated or configured Python environment + /// and attempt to infer the Python version of that environment + /// 3. Fall back to the default value (see below) + /// + /// For some language features, ty can also understand conditionals based on comparisons + /// with `sys.version_info`. These are commonly found in typeshed, for example, + /// to reflect the differing contents of the standard library across Python versions. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#""3.13""#, + value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | ."#, + example = r#" + python-version = "3.12" + "# + )] + pub python_version: Option>, + + /// Specifies the target platform that will be used to analyze the source code. + /// If specified, ty will understand conditions based on comparisons with `sys.platform`, such + /// as are commonly found in typeshed to reflect the differing contents of the standard library across platforms. + /// If `all` is specified, ty will assume that the source code can run on any platform. + /// + /// If no platform is specified, ty will use the current platform: + /// - `win32` for Windows + /// - `darwin` for macOS + /// - `android` for Android + /// - `ios` for iOS + /// - `linux` for everything else + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#""#, + value_type = r#""win32" | "darwin" | "android" | "ios" | "linux" | "all" | str"#, + example = r#" + # Tailor type stubs and conditionalized type definitions to windows. + python-platform = "win32" + "# + )] + pub python_platform: Option>, + + /// List of user-provided paths that should take first priority in the module resolution. + /// Examples in other type checkers are mypy's `MYPYPATH` environment variable, + /// or pyright's `stubPath` configuration setting. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#" + extra-paths = ["~/shared/my-search-path"] + "# + )] + pub extra_paths: Option>, + + /// Optional path to a "typeshed" directory on disk for us to use for standard-library types. + /// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, + /// bundled as a zip file in the binary + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = "str", + example = r#" + typeshed = "/path/to/custom/typeshed" + "# + )] + pub typeshed: Option, + + /// Path to the Python installation from which ty resolves type information and third-party dependencies. + /// + /// ty will search in the path's `site-packages` directories for type information and + /// third-party imports. + /// + /// This option is commonly used to specify the path to a virtual environment. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = "str", + example = r#" + python = "./.venv" + "# + )] + pub python: Option, +} + +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct SrcOptions { + /// The root of the project, used for finding first-party modules. + /// + /// If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly: + /// + /// * if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) + /// * if a `.//` directory exists, include `.` and `./` in the first party search path + /// * otherwise, default to `.` (flat layout) + /// + /// Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), + /// it will also be included in the first party search path. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = "str", + example = r#" + root = "./app" + "# + )] + #[deprecated(note = "Use `environment.root` instead.")] + pub root: Option, + + /// Whether to automatically exclude files that are ignored by `.ignore`, + /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files. + /// Enabled by default. + #[option( + default = r#"true"#, + value_type = r#"bool"#, + example = r#" + respect-ignore-files = false + "# + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub respect_ignore_files: Option, + + /// A list of files and directories to check. The `include` option + /// follows a similar syntax to `.gitignore` but reversed: + /// Including a file or directory will make it so that it (and its contents) + /// are type checked. + /// + /// - `./src/` matches only a directory + /// - `./src` matches both files and directories + /// - `src` matches a file or directory named `src` + /// - `*` matches any (possibly empty) sequence of characters (except `/`). + /// - `**` matches zero or more path components. + /// This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. + /// A sequence of more than two consecutive `*` characters is also invalid. + /// - `?` matches any single character except `/` + /// - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, + /// so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. + /// + /// All paths are anchored relative to the project root (`src` only + /// matches `/src` and not `/test/src`). + /// + /// `exclude` takes precedence over `include`. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = r#"list[str]"#, + example = r#" + include = [ + "src", + "tests", + ] + "# + )] + pub include: Option>>, + + /// A list of file and directory patterns to exclude from type checking. + /// + /// Patterns follow a syntax similar to `.gitignore`: + /// + /// - `./src/` matches only a directory + /// - `./src` matches both files and directories + /// - `src` matches files or directories named `src` + /// - `*` matches any (possibly empty) sequence of characters (except `/`). + /// - `**` matches zero or more path components. + /// This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. + /// A sequence of more than two consecutive `*` characters is also invalid. + /// - `?` matches any single character except `/` + /// - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, + /// so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. + /// - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded) + /// + /// All paths are anchored relative to the project root (`src` only + /// matches `/src` and not `/test/src`). + /// To exclude any directory or file named `src`, use `**/src` instead. + /// + /// By default, ty excludes commonly ignored directories: + /// + /// - `**/.bzr/` + /// - `**/.direnv/` + /// - `**/.eggs/` + /// - `**/.git/` + /// - `**/.git-rewrite/` + /// - `**/.hg/` + /// - `**/.mypy_cache/` + /// - `**/.nox/` + /// - `**/.pants.d/` + /// - `**/.pytype/` + /// - `**/.ruff_cache/` + /// - `**/.svn/` + /// - `**/.tox/` + /// - `**/.venv/` + /// - `**/__pypackages__/` + /// - `**/_build/` + /// - `**/buck-out/` + /// - `**/dist/` + /// - `**/node_modules/` + /// - `**/venv/` + /// + /// You can override any default exclude by using a negated pattern. For example, + /// to re-include `dist` use `exclude = ["!dist"]` + #[option( + default = r#"null"#, + value_type = r#"list[str]"#, + example = r#" + exclude = [ + "generated", + "*.proto", + "tests/fixtures/**", + "!tests/fixtures/important.py" # Include this one file + ] + "# + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude: Option>>, +} + +impl SrcOptions { + fn to_settings( + &self, + db: &dyn Db, + project_root: &SystemPath, + diagnostics: &mut Vec, + ) -> Result> { + let include = build_include_filter( + db, + project_root, + self.include.as_ref(), + GlobFilterContext::SrcRoot, + diagnostics, + )?; + let exclude = build_exclude_filter( + db, + project_root, + self.exclude.as_ref(), + DEFAULT_SRC_EXCLUDES, + GlobFilterContext::SrcRoot, + )?; + let files = IncludeExcludeFilter::new(include, exclude); + + Ok(SrcSettings { + respect_ignore_files: self.respect_ignore_files.unwrap_or(true), + files, + }) + } +} + +#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, Hash)] +#[serde(rename_all = "kebab-case", transparent)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Rules { + #[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))] + inner: OrderMap, RangedValue, BuildHasherDefault>, +} + +impl FromIterator<(RangedValue, RangedValue)> for Rules { + fn from_iter, RangedValue)>>( + iter: T, + ) -> Self { + Self { + inner: iter.into_iter().collect(), + } + } +} + +impl Rules { + /// Convert the rules to a `RuleSelection` with diagnostics. + pub fn to_rule_selection( + &self, + db: &dyn Db, + diagnostics: &mut Vec, + ) -> RuleSelection { + let registry = db.lint_registry(); + + // Initialize the selection with the defaults + let mut selection = RuleSelection::from_registry(registry); + + for (rule_name, level) in &self.inner { + let source = rule_name.source(); + match registry.get(rule_name) { + Ok(lint) => { + let lint_source = match source { + ValueSource::File(_) => LintSource::File, + ValueSource::Cli => LintSource::Cli, + }; + if let Ok(severity) = Severity::try_from(**level) { + selection.enable(lint, severity, lint_source); + } else { + selection.disable(lint); + } + } + Err(error) => { + // `system_path_to_file` can return `Err` if the file was deleted since the configuration + // was read. This should be rare and it should be okay to default to not showing a configuration + // file in that case. + let file = source + .file() + .and_then(|path| system_path_to_file(db, path).ok()); + + // TODO: Add a note if the value was configured on the CLI + let diagnostic = match error { + GetLintError::Unknown(_) => OptionDiagnostic::new( + DiagnosticId::UnknownRule, + format!("Unknown lint rule `{rule_name}`"), + Severity::Warning, + ), + GetLintError::PrefixedWithCategory { suggestion, .. } => { + OptionDiagnostic::new( + DiagnosticId::UnknownRule, + format!( + "Unknown lint rule `{rule_name}`. Did you mean `{suggestion}`?" + ), + Severity::Warning, + ) + } + + GetLintError::Removed(_) => OptionDiagnostic::new( + DiagnosticId::UnknownRule, + format!("Unknown lint rule `{rule_name}`"), + Severity::Warning, + ), + }; + + let annotation = file.map(Span::from).map(|span| { + Annotation::primary(span.with_optional_range(rule_name.range())) + }); + diagnostics.push(diagnostic.with_annotation(annotation)); + } + } + } + + selection + } + + pub(super) fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + +/// Default exclude patterns for src options. +const DEFAULT_SRC_EXCLUDES: &[&str] = &[ + "**/.bzr/", + "**/.direnv/", + "**/.eggs/", + "**/.git/", + "**/.git-rewrite/", + "**/.hg/", + "**/.mypy_cache/", + "**/.nox/", + "**/.pants.d/", + "**/.pytype/", + "**/.ruff_cache/", + "**/.svn/", + "**/.tox/", + "**/.venv/", + "**/__pypackages__/", + "**/_build/", + "**/buck-out/", + "**/dist/", + "**/node_modules/", + "**/venv/", +]; + +/// Helper function to build an include filter from patterns with proper error handling. +fn build_include_filter( + db: &dyn Db, + project_root: &SystemPath, + include_patterns: Option<&RangedValue>>, + context: GlobFilterContext, + diagnostics: &mut Vec, +) -> Result> { + use crate::glob::{IncludeFilterBuilder, PortableGlobPattern}; + + let system = db.system(); + let mut includes = IncludeFilterBuilder::new(); + + if let Some(include_patterns) = include_patterns { + if include_patterns.is_empty() { + // An override with an empty include `[]` won't match any files. + let mut diagnostic = OptionDiagnostic::new( + DiagnosticId::EmptyInclude, + "Empty include matches no files".to_string(), + Severity::Warning, + ) + .sub(SubDiagnostic::new( + Severity::Info, + "Remove the `include` option to match all files or add a pattern to match specific files", + )); + + // Add source annotation if we have source information + if let Some(source_file) = include_patterns.source().file() { + if let Ok(file) = system_path_to_file(db, source_file) { + let annotation = Annotation::primary( + Span::from(file).with_optional_range(include_patterns.range()), + ) + .message("This `include` list is empty"); + diagnostic = diagnostic.with_annotation(Some(annotation)); + } + } + + diagnostics.push(diagnostic); + } + + for pattern in include_patterns { + pattern.absolute(project_root, system, PortableGlobKind::Include) + .and_then(|include| Ok(includes.add(&include)?)) + .map_err(|err| { + let diagnostic = OptionDiagnostic::new( + DiagnosticId::InvalidGlob, + format!("Invalid include pattern `{pattern}`: {err}"), + Severity::Error, + ); + + match pattern.source() { + ValueSource::File(file_path) => { + if let Ok(file) = system_path_to_file(db, &**file_path) { + diagnostic + .with_message("Invalid include pattern") + .with_annotation(Some( + Annotation::primary( + Span::from(file) + .with_optional_range(pattern.range()), + ) + .message(err.to_string()), + )) + } else { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!("The pattern is defined in the `{}` option in your configuration file", context.include_name()), + )) + } + } + ValueSource::Cli => diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "The pattern was specified on the CLI", + )), + } + })?; + } + } else { + includes + .add( + &PortableGlobPattern::parse("**", PortableGlobKind::Include) + .unwrap() + .into_absolute(""), + ) + .unwrap(); + } + + includes.build().map_err(|_| { + let diagnostic = OptionDiagnostic::new( + DiagnosticId::InvalidGlob, + format!("The `{}` patterns resulted in a regex that is too large", context.include_name()), + Severity::Error, + ); + Box::new(diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "Please open an issue on the ty repository and share the patterns that caused the error.", + ))) + }) +} + +/// Helper function to build an exclude filter from patterns with proper error handling. +fn build_exclude_filter( + db: &dyn Db, + project_root: &SystemPath, + exclude_patterns: Option<&RangedValue>>, + default_patterns: &[&str], + context: GlobFilterContext, +) -> Result> { + use crate::glob::{ExcludeFilterBuilder, PortableGlobPattern}; + + let system = db.system(); + let mut excludes = ExcludeFilterBuilder::new(); + + for pattern in default_patterns { + PortableGlobPattern::parse(pattern, PortableGlobKind::Exclude) + .and_then(|exclude| Ok(excludes.add(&exclude.into_absolute(""))?)) + .unwrap_or_else(|err| { + panic!("Expected default exclude to be valid glob but adding it failed with: {err}") + }); + } + + // Add user-specified excludes + if let Some(exclude_patterns) = exclude_patterns { + for exclude in exclude_patterns { + exclude.absolute(project_root, system, PortableGlobKind::Exclude) + .and_then(|pattern| Ok(excludes.add(&pattern)?)) + .map_err(|err| { + let diagnostic = OptionDiagnostic::new( + DiagnosticId::InvalidGlob, + format!("Invalid exclude pattern `{exclude}`: {err}"), + Severity::Error, + ); + + match exclude.source() { + ValueSource::File(file_path) => { + if let Ok(file) = system_path_to_file(db, &**file_path) { + diagnostic + .with_message("Invalid exclude pattern") + .with_annotation(Some( + Annotation::primary( + Span::from(file) + .with_optional_range(exclude.range()), + ) + .message(err.to_string()), + )) + } else { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!("The pattern is defined in the `{}` option in your configuration file", context.exclude_name()), + )) + } + } + ValueSource::Cli => diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "The pattern was specified on the CLI", + )), + } + })?; + } + } + + excludes.build().map_err(|_| { + let diagnostic = OptionDiagnostic::new( + DiagnosticId::InvalidGlob, + format!("The `{}` patterns resulted in a regex that is too large", context.exclude_name()), + Severity::Error, + ); + Box::new(diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "Please open an issue on the ty repository and share the patterns that caused the error.", + ))) + }) +} + +/// Context for filter operations, used in error messages +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GlobFilterContext { + /// Source root configuration context + SrcRoot, + /// Override configuration context + Overrides, +} + +impl GlobFilterContext { + fn include_name(self) -> &'static str { + match self { + Self::SrcRoot => "src.include", + Self::Overrides => "overrides.include", + } + } + + fn exclude_name(self) -> &'static str { + match self { + Self::SrcRoot => "src.exclude", + Self::Overrides => "overrides.exclude", + } + } +} + +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct TerminalOptions { + /// The format to use for printing diagnostic messages. + /// + /// Defaults to `full`. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"full"#, + value_type = "full | concise", + example = r#" + output-format = "concise" + "# + )] + pub output_format: Option>, + /// Use exit code 1 if there are any warning-level diagnostics. + /// + /// Defaults to `false`. + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Error if ty emits any warning-level diagnostics. + error-on-warning = true + "# + )] + pub error_on_warning: Option, +} + +/// Configuration override that applies to specific files based on glob patterns. +/// +/// An override allows you to apply different rule configurations to specific +/// files or directories. Multiple overrides can match the same file, with +/// later overrides take precedence. +/// +/// ### Precedence +/// +/// - Later overrides in the array take precedence over earlier ones +/// - Override rules take precedence over global rules for matching files +/// +/// ### Examples +/// +/// ```toml +/// # Relax rules for test files +/// [[tool.ty.overrides]] +/// include = ["tests/**", "**/test_*.py"] +/// +/// [tool.ty.overrides.rules] +/// possibly-unresolved-reference = "warn" +/// +/// # Ignore generated files but still check important ones +/// [[tool.ty.overrides]] +/// include = ["generated/**"] +/// exclude = ["generated/important.py"] +/// +/// [tool.ty.overrides.rules] +/// possibly-unresolved-reference = "ignore" +/// ``` +#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize, RustDoc)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(transparent)] +pub struct OverridesOptions(Vec>); + +impl OptionsMetadata for OverridesOptions { + fn documentation() -> Option<&'static str> { + Some(::rust_doc()) + } + + fn record(visit: &mut dyn Visit) { + OptionSet::of::().record(visit); + } +} + +impl Deref for OverridesOptions { + type Target = [RangedValue]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive( + Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize, OptionsMetadata, +)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct OverrideOptions { + /// A list of file and directory patterns to include for this override. + /// + /// The `include` option follows a similar syntax to `.gitignore` but reversed: + /// Including a file or directory will make it so that it (and its contents) + /// are affected by this override. + /// + /// If not specified, defaults to `["**"]` (matches all files). + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = r#"list[str]"#, + example = r#" + [[tool.ty.overrides]] + include = [ + "src", + "tests", + ] + "# + )] + pub include: Option>>, + + /// A list of file and directory patterns to exclude from this override. + /// + /// Patterns follow a syntax similar to `.gitignore`. + /// Exclude patterns take precedence over include patterns within the same override. + /// + /// If not specified, defaults to `[]` (excludes no files). + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"null"#, + value_type = r#"list[str]"#, + example = r#" + [[tool.ty.overrides]] + exclude = [ + "generated", + "*.proto", + "tests/fixtures/**", + "!tests/fixtures/important.py" # Include this one file + ] + "# + )] + pub exclude: Option>>, + + /// Rule overrides for files matching the include/exclude patterns. + /// + /// These rules will be merged with the global rules, with override rules + /// taking precedence for matching files. You can set rules to different + /// severity levels or disable them entirely. + #[serde(skip_serializing_if = "Option::is_none")] + #[option( + default = r#"{...}"#, + value_type = r#"dict[RuleName, "ignore" | "warn" | "error"]"#, + example = r#" + [[tool.ty.overrides]] + include = ["src"] + + [tool.ty.overrides.rules] + possibly-unresolved-reference = "ignore" + "# + )] + pub rules: Option, +} + +impl RangedValue { + fn to_override( + &self, + db: &dyn Db, + project_root: &SystemPath, + global_rules: Option<&Rules>, + diagnostics: &mut Vec, + ) -> Result, Box> { + let rules = self.rules.or_default(); + + // First, warn about incorrect or useless overrides. + if rules.is_empty() { + let mut diagnostic = OptionDiagnostic::new( + DiagnosticId::UselessOverridesSection, + "Useless `overrides` section".to_string(), + Severity::Warning, + ); + + diagnostic = if self.rules.is_none() { + diagnostic = diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "It has no `rules` table", + )); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "Add a `[overrides.rules]` table...", + )) + } else { + diagnostic = diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "The rules table is empty", + )); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "Add a rule to `[overrides.rules]` to override specific rules...", + )) + }; + + diagnostic = diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "or remove the `[[overrides]]` section if there's nothing to override", + )); + + // Add source annotation if we have source information + if let Some(source_file) = self.source().file() { + if let Ok(file) = system_path_to_file(db, source_file) { + let annotation = + Annotation::primary(Span::from(file).with_optional_range(self.range())) + .message("This overrides section configures no rules"); + diagnostic = diagnostic.with_annotation(Some(annotation)); + } + } + + diagnostics.push(diagnostic); + // Return `None`, because this override doesn't override anything + return Ok(None); + } + + let include_missing = self.include.is_none(); + let exclude_empty = self + .exclude + .as_ref() + .is_none_or(|exclude| exclude.is_empty()); + + if include_missing && exclude_empty { + // Neither include nor exclude specified - applies to all files + let mut diagnostic = OptionDiagnostic::new( + DiagnosticId::UnnecessaryOverridesSection, + "Unnecessary `overrides` section".to_string(), + Severity::Warning, + ); + + diagnostic = if self.exclude.is_none() { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "It has no `include` or `exclude` option restricting the files", + )) + } else { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "It has no `include` option and `exclude` is empty", + )) + }; + + diagnostic = diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "Restrict the files by adding a pattern to `include` or `exclude`...", + )); + + diagnostic = diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files", + )); + + // Add source annotation if we have source information + if let Some(source_file) = self.source().file() { + if let Ok(file) = system_path_to_file(db, source_file) { + let annotation = + Annotation::primary(Span::from(file).with_optional_range(self.range())) + .message("This overrides section applies to all files"); + diagnostic = diagnostic.with_annotation(Some(annotation)); + } + } + + diagnostics.push(diagnostic); + } + + // The override is at least (partially) valid. + // Construct the matcher and resolve the settings. + let include = build_include_filter( + db, + project_root, + self.include.as_ref(), + GlobFilterContext::Overrides, + diagnostics, + )?; + + let exclude = build_exclude_filter( + db, + project_root, + self.exclude.as_ref(), + &[], + GlobFilterContext::Overrides, + )?; + + let files = IncludeExcludeFilter::new(include, exclude); + + // Merge global rules with override rules, with override rules taking precedence + let mut merged_rules = rules.into_owned(); + + if let Some(global_rules) = global_rules { + merged_rules = merged_rules.combine(global_rules.clone()); + } + + // Convert merged rules to rule selection + let rule_selection = merged_rules.to_rule_selection(db, diagnostics); + + let override_instance = Override { + files, + options: Arc::new(InnerOverrideOptions { + rules: self.rules.clone(), + }), + settings: Arc::new(OverrideSettings { + rules: rule_selection, + }), + }; + + Ok(Some(override_instance)) + } +} + +/// The options for an override but without the include/exclude patterns. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Combine)] +pub(super) struct InnerOverrideOptions { + /// Raw rule options as specified in the configuration. + /// Used when multiple overrides match a file and need to be merged. + pub(super) rules: Option, +} + +/// Error returned when the settings can't be resolved because of a hard error. +#[derive(Debug)] +pub struct ToSettingsError { + diagnostic: Box, + output_format: DiagnosticFormat, + color: bool, +} + +impl ToSettingsError { + pub fn pretty<'a>(&'a self, db: &'a dyn Db) -> impl fmt::Display + use<'a> { + struct DisplayPretty<'a> { + db: &'a dyn ruff_db::Db, + error: &'a ToSettingsError, + } + + impl fmt::Display for DisplayPretty<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display_config = DisplayDiagnosticConfig::default() + .format(self.error.output_format) + .color(self.error.color); + + write!( + f, + "{}", + self.error + .diagnostic + .to_diagnostic() + .display(&self.db, &display_config) + ) + } + } + + DisplayPretty { db, error: self } + } + + pub fn into_diagnostic(self) -> OptionDiagnostic { + *self.diagnostic + } +} + +impl Display for ToSettingsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.diagnostic.message) + } +} + +impl std::error::Error for ToSettingsError {} + +#[cfg(feature = "schemars")] +mod schema { + use crate::DEFAULT_LINT_REGISTRY; + use schemars::JsonSchema; + use schemars::r#gen::SchemaGenerator; + use schemars::schema::{ + InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation, + }; + use ty_python_semantic::lint::Level; + + pub(super) struct Rules; + + impl JsonSchema for Rules { + fn schema_name() -> String { + "Rules".to_string() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let registry = &*DEFAULT_LINT_REGISTRY; + + let level_schema = generator.subschema_for::(); + + let properties: schemars::Map = registry + .lints() + .iter() + .map(|lint| { + ( + lint.name().to_string(), + Schema::Object(SchemaObject { + metadata: Some(Box::new(Metadata { + title: Some(lint.summary().to_string()), + description: Some(lint.documentation()), + deprecated: lint.status.is_deprecated(), + default: Some(lint.default_level.to_string().into()), + ..Metadata::default() + })), + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![level_schema.clone()]), + ..Default::default() + })), + ..Default::default() + }), + ) + }) + .collect(); + + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties, + // Allow unknown rules: ty will warn about them. + // It gives a better experience when using an older ty version because + // the schema will not deny rules that have been removed in newer versions. + additional_properties: Some(Box::new(level_schema)), + ..ObjectValidation::default() + })), + + ..Default::default() + }) + } + } +} + +#[derive(Error, Debug)] +pub enum TyTomlError { + #[error(transparent)] + TomlSyntax(#[from] toml::de::Error), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct OptionDiagnostic { + id: DiagnosticId, + message: String, + severity: Severity, + annotation: Option, + sub: Vec, +} + +impl OptionDiagnostic { + pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self { + Self { + id, + message, + severity, + annotation: None, + sub: Vec::new(), + } + } + + #[must_use] + fn with_message(self, message: impl Display) -> Self { + OptionDiagnostic { + message: message.to_string(), + ..self + } + } + + #[must_use] + fn with_annotation(self, annotation: Option) -> Self { + OptionDiagnostic { annotation, ..self } + } + + #[must_use] + fn sub(mut self, sub: SubDiagnostic) -> Self { + self.sub.push(sub); + self + } + + pub(crate) fn to_diagnostic(&self) -> Diagnostic { + let mut diag = Diagnostic::new(self.id, self.severity, &self.message); + if let Some(annotation) = self.annotation.clone() { + diag.annotate(annotation); + } + + for sub in &self.sub { + diag.sub(sub.clone()); + } + + diag + } +} + +/// This is a wrapper for options that actually get loaded from configuration files +/// and the CLI, which also includes a `config_file_override` option that overrides +/// default configuration discovery with an explicitly-provided path to a configuration file +pub struct ProjectOptionsOverrides { + pub config_file_override: Option, + pub options: Options, +} + +impl ProjectOptionsOverrides { + pub fn new(config_file_override: Option, options: Options) -> Self { + Self { + config_file_override, + options, + } + } +} + +trait OrDefault { + type Target: ToOwned; + + fn or_default(&self) -> Cow<'_, Self::Target>; +} + +impl OrDefault for Option +where + T: Default + ToOwned, +{ + type Target = T; + + fn or_default(&self) -> Cow<'_, Self::Target> { + match self { + Some(value) => Cow::Borrowed(value), + None => Cow::Owned(T::default()), + } + } +} diff --git a/crates/red_knot_project/src/metadata/pyproject.rs b/crates/ty_project/src/metadata/pyproject.rs similarity index 92% rename from crates/red_knot_project/src/metadata/pyproject.rs rename to crates/ty_project/src/metadata/pyproject.rs index 2be857f50e419..21dd5f02380d4 100644 --- a/crates/red_knot_project/src/metadata/pyproject.rs +++ b/crates/ty_project/src/metadata/pyproject.rs @@ -1,6 +1,6 @@ use crate::metadata::options::Options; use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard}; -use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers}; +use pep440_rs::{Version, VersionSpecifiers, release_specifiers_to_ranges}; use ruff_python_ast::PythonVersion; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::Bound; @@ -18,8 +18,8 @@ pub struct PyProject { } impl PyProject { - pub(crate) fn knot(&self) -> Option<&Options> { - self.tool.as_ref().and_then(|tool| tool.knot.as_ref()) + pub(crate) fn ty(&self) -> Option<&Options> { + self.tool.as_ref().and_then(|tool| tool.ty.as_ref()) } } @@ -85,7 +85,7 @@ impl Project { Bound::Unbounded => { return Err(ResolveRequiresPythonError::NoLowerBound( requires_python.to_string(), - )) + )); } }; @@ -103,7 +103,7 @@ impl Project { let major = u8::try_from(major).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(major))?; let minor = - u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(minor))?; + u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMinor(minor))?; Ok(Some( requires_python @@ -119,14 +119,16 @@ pub enum ResolveRequiresPythonError { TooLargeMajor(u64), #[error("The minor version `{0}` is larger than the maximum supported value 255")] TooLargeMinor(u64), - #[error("value `{0}` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.")] + #[error( + "value `{0}` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`." + )] NoLowerBound(String), } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Tool { - pub knot: Option, + pub ty: Option, } /// The normalized name of a package. @@ -233,7 +235,8 @@ pub(crate) enum InvalidPackageNameError { NonAlphanumericStart(char), #[error("name must end with letter or number but it ends with '{0}'")] NonAlphanumericEnd(char), - #[error("valid name consists only of ASCII letters and numbers, period, underscore and hyphen but name contains '{0}'" + #[error( + "valid name consists only of ASCII letters and numbers, period, underscore and hyphen but name contains '{0}'" )] InvalidCharacter(char), #[error("name must not be empty")] diff --git a/crates/ty_project/src/metadata/settings.rs b/crates/ty_project/src/metadata/settings.rs new file mode 100644 index 0000000000000..98cef0f74c551 --- /dev/null +++ b/crates/ty_project/src/metadata/settings.rs @@ -0,0 +1,203 @@ +use std::sync::Arc; + +use ruff_db::{diagnostic::DiagnosticFormat, files::File}; +use ty_python_semantic::lint::RuleSelection; + +use crate::metadata::options::InnerOverrideOptions; +use crate::{Db, combine::Combine, glob::IncludeExcludeFilter}; + +/// The resolved [`super::Options`] for the project. +/// +/// Unlike [`super::Options`], the struct has default values filled in and +/// uses representations that are optimized for reads (instead of preserving the source representation). +/// It's also not required that this structure precisely resembles the TOML schema, although +/// it's encouraged to use a similar structure. +/// +/// It's worth considering to adding a salsa query for specific settings to +/// limit the blast radius when only some settings change. For example, +/// changing the terminal settings shouldn't invalidate any core type-checking queries. +/// This can be achieved by adding a salsa query for the type checking specific settings. +/// +/// Settings that are part of [`ty_python_semantic::ProgramSettings`] are not included here. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Settings { + pub(super) rules: Arc, + pub(super) terminal: TerminalSettings, + pub(super) src: SrcSettings, + + /// Settings for configuration overrides that apply to specific file patterns. + /// + /// Each override can specify include/exclude patterns and rule configurations + /// that apply to matching files. Multiple overrides can match the same file, + /// with later overrides taking precedence. + pub(super) overrides: Vec, +} + +impl Settings { + pub fn rules(&self) -> &RuleSelection { + &self.rules + } + + pub fn src(&self) -> &SrcSettings { + &self.src + } + + pub fn to_rules(&self) -> Arc { + self.rules.clone() + } + + pub fn terminal(&self) -> &TerminalSettings { + &self.terminal + } + + pub fn overrides(&self) -> &[Override] { + &self.overrides + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TerminalSettings { + pub output_format: DiagnosticFormat, + pub error_on_warning: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SrcSettings { + pub respect_ignore_files: bool, + pub files: IncludeExcludeFilter, +} + +/// A single configuration override that applies to files matching specific patterns. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Override { + /// File pattern filter to determine which files this override applies to. + pub(super) files: IncludeExcludeFilter, + + /// The raw options as specified in the configuration (minus `include` and `exclude`. + /// Necessary to merge multiple overrides if necessary. + pub(super) options: Arc, + + /// Pre-resolved rule selection for this override alone. + /// Used for efficient lookup when only this override matches a file. + pub(super) settings: Arc, +} + +impl Override { + /// Returns whether this override applies to the given file path. + pub fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool { + use crate::glob::{GlobFilterCheckMode, IncludeResult}; + + matches!( + self.files + .is_file_included(path, GlobFilterCheckMode::Adhoc), + IncludeResult::Included + ) + } +} + +/// Resolves the settings for a given file. +#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn file_settings(db: &dyn Db, file: File) -> FileSettings { + let settings = db.project().settings(db); + + let path = match file.path(db) { + ruff_db::files::FilePath::System(path) => path, + ruff_db::files::FilePath::SystemVirtual(_) | ruff_db::files::FilePath::Vendored(_) => { + return FileSettings::Global; + } + }; + + let mut matching_overrides = settings + .overrides() + .iter() + .filter(|over| over.matches_file(path)); + + let Some(first) = matching_overrides.next() else { + // If the file matches no override, it uses the global settings. + return FileSettings::Global; + }; + + let Some(second) = matching_overrides.next() else { + tracing::debug!("Applying override for file `{path}`: {}", first.files); + // If the file matches only one override, return that override's settings. + return FileSettings::File(Arc::clone(&first.settings)); + }; + + let mut filters = tracing::enabled!(tracing::Level::DEBUG) + .then(|| format!("({}), ({})", first.files, second.files)); + + let mut overrides = vec![Arc::clone(&first.options), Arc::clone(&second.options)]; + + for over in matching_overrides { + use std::fmt::Write; + + if let Some(filters) = &mut filters { + let _ = write!(filters, ", ({})", over.files); + } + + overrides.push(Arc::clone(&over.options)); + } + + if let Some(filters) = &filters { + tracing::debug!("Applying multiple overrides for file `{path}`: {filters}"); + } + + merge_overrides(db, overrides, ()) +} + +/// Merges multiple override options, caching the result. +/// +/// Overrides often apply to multiple files. This query ensures that we avoid +/// resolving the same override combinations multiple times. +/// +/// ## What's up with the `()` argument? +/// +/// This is to make Salsa happy because it requires that queries with only a single argument +/// take a salsa-struct as argument, which isn't the case here. The `()` enables salsa's +/// automatic interning for the arguments. +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] +fn merge_overrides(db: &dyn Db, overrides: Vec>, _: ()) -> FileSettings { + let mut overrides = overrides.into_iter().rev(); + let mut merged = (*overrides.next().unwrap()).clone(); + + for option in overrides { + merged.combine_with((*option).clone()); + } + + merged + .rules + .combine_with(db.project().metadata(db).options().rules.clone()); + + let Some(rules) = merged.rules else { + return FileSettings::Global; + }; + + // It's okay to ignore the errors here because the rules are eagerly validated + // during `overrides.to_settings()`. + let rules = rules.to_rule_selection(db, &mut Vec::new()); + FileSettings::File(Arc::new(OverrideSettings { rules })) +} + +/// The resolved settings for a file. +#[derive(Debug, Eq, PartialEq, Clone, get_size2::GetSize)] +pub enum FileSettings { + /// The file uses the global settings. + Global, + + /// The file has specific override settings. + File(Arc), +} + +impl FileSettings { + pub fn rules<'a>(&'a self, db: &'a dyn Db) -> &'a RuleSelection { + match self { + FileSettings::Global => db.project().settings(db).rules(), + FileSettings::File(override_settings) => &override_settings.rules, + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, get_size2::GetSize)] +pub struct OverrideSettings { + pub(super) rules: RuleSelection, +} diff --git a/crates/red_knot_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs similarity index 82% rename from crates/red_knot_project/src/metadata/value.rs rename to crates/ty_project/src/metadata/value.rs index e4defe98ed23e..09d457d875e49 100644 --- a/crates/red_knot_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -1,5 +1,8 @@ -use crate::combine::Combine; use crate::Db; +use crate::combine::Combine; +use crate::glob::{ + AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern, +}; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_macros::Combine; use ruff_text_size::{TextRange, TextSize}; @@ -7,6 +10,7 @@ use serde::{Deserialize, Deserializer}; use std::cell::RefCell; use std::cmp::Ordering; use std::fmt; +use std::fmt::Formatter; use std::hash::{Hash, Hasher}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; @@ -32,6 +36,10 @@ impl ValueSource { ValueSource::Cli => None, } } + + pub const fn is_cli(&self) -> bool { + matches!(self, ValueSource::Cli) + } } thread_local! { @@ -155,7 +163,7 @@ where } // The type already has an `iter` method thanks to `Deref`. -#[allow(clippy::into_iter_without_iter)] +#[expect(clippy::into_iter_without_iter)] impl<'a, T> IntoIterator for &'a RangedValue where &'a T: IntoIterator, @@ -168,7 +176,7 @@ where } // The type already has a `into_iter_mut` method thanks to `DerefMut`. -#[allow(clippy::into_iter_without_iter)] +#[expect(clippy::into_iter_without_iter)] impl<'a, T> IntoIterator for &'a mut RangedValue where &'a mut T: IntoIterator, @@ -324,6 +332,14 @@ impl RelativePathBuf { &self.0 } + pub fn source(&self) -> &ValueSource { + self.0.source() + } + + pub fn range(&self) -> Option { + self.0.range() + } + /// Returns the owned relative path. pub fn into_path_buf(self) -> SystemPathBuf { self.0.into_inner() @@ -344,3 +360,65 @@ impl RelativePathBuf { SystemPath::absolute(&self.0, relative_to) } } + +impl fmt::Display for RelativePathBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[derive( + Debug, + Clone, + serde::Serialize, + serde::Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Combine, +)] +#[serde(transparent)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct RelativeGlobPattern(RangedValue); + +impl RelativeGlobPattern { + pub fn new(pattern: impl AsRef, source: ValueSource) -> Self { + Self(RangedValue::new(pattern.as_ref().to_string(), source)) + } + + pub fn cli(pattern: impl AsRef) -> Self { + Self::new(pattern, ValueSource::Cli) + } + + pub(crate) fn source(&self) -> &ValueSource { + self.0.source() + } + + pub(crate) fn range(&self) -> Option { + self.0.range() + } + + /// Resolves the absolute pattern for `self` based on its origin. + pub(crate) fn absolute( + &self, + project_root: &SystemPath, + system: &dyn System, + kind: PortableGlobKind, + ) -> Result { + let relative_to = match &self.0.source { + ValueSource::File(_) => project_root, + ValueSource::Cli => system.current_directory(), + }; + + let pattern = PortableGlobPattern::parse(&self.0, kind)?; + Ok(pattern.into_absolute(relative_to)) + } +} + +impl std::fmt::Display for RelativeGlobPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/crates/ty_project/src/walk.rs b/crates/ty_project/src/walk.rs new file mode 100644 index 0000000000000..ea576503df40e --- /dev/null +++ b/crates/ty_project/src/walk.rs @@ -0,0 +1,305 @@ +use crate::glob::IncludeExcludeFilter; +use crate::{Db, GlobFilterCheckMode, IOErrorDiagnostic, IOErrorKind, IncludeResult, Project}; +use ruff_db::files::{File, system_path_to_file}; +use ruff_db::system::walk_directory::{ErrorKind, WalkDirectoryBuilder, WalkState}; +use ruff_db::system::{SystemPath, SystemPathBuf}; +use ruff_python_ast::PySourceType; +use rustc_hash::{FxBuildHasher, FxHashSet}; +use std::path::PathBuf; +use thiserror::Error; + +/// Filter that decides which files are included in the project. +/// +/// In the future, this will hold a reference to the `include` and `exclude` pattern. +/// +/// This struct mainly exists because `dyn Db` isn't `Send` or `Sync`, making it impossible +/// to access fields from within the walker. +#[derive(Debug)] +pub(crate) struct ProjectFilesFilter<'a> { + /// The same as [`Project::included_paths_or_root`]. + included_paths: &'a [SystemPathBuf], + + /// The resolved `src.include` and `src.exclude` filter. + src_filter: &'a IncludeExcludeFilter, +} + +impl<'a> ProjectFilesFilter<'a> { + pub(crate) fn from_project(db: &'a dyn Db, project: Project) -> Self { + Self { + included_paths: project.included_paths_or_root(db), + src_filter: &project.settings(db).src().files, + } + } + + fn match_included_paths( + &self, + path: &SystemPath, + mode: GlobFilterCheckMode, + ) -> Option { + match mode { + GlobFilterCheckMode::TopDown => Some(CheckPathMatch::Partial), + GlobFilterCheckMode::Adhoc => { + self.included_paths + .iter() + .filter_map(|included_path| { + if let Ok(relative_path) = path.strip_prefix(included_path) { + // Exact matches are always included + if relative_path.as_str().is_empty() { + Some(CheckPathMatch::Full) + } else { + Some(CheckPathMatch::Partial) + } + } else { + None + } + }) + .max() + } + } + } + + /// Returns `true` if a file is part of the project and included in the paths to check. + /// + /// A file is included in the checked files if it is a sub path of the project's root + /// (when no CLI path arguments are specified) or if it is a sub path of any path provided on the CLI (`ty check `) AND: + /// + /// * It matches a positive `include` pattern and isn't excluded by a later negative `include` pattern. + /// * It doesn't match a positive `exclude` pattern or is re-included by a later negative `exclude` pattern. + /// + /// ## Note + /// + /// This method may return `true` for files that don't end up being included when walking the + /// project tree because it doesn't consider `.gitignore` and other ignore files when deciding + /// if a file's included. + pub(crate) fn is_file_included( + &self, + path: &SystemPath, + mode: GlobFilterCheckMode, + ) -> IncludeResult { + match self.match_included_paths(path, mode) { + None => IncludeResult::NotIncluded, + Some(CheckPathMatch::Partial) => self.src_filter.is_file_included(path, mode), + Some(CheckPathMatch::Full) => IncludeResult::Included, + } + } + + pub(crate) fn is_directory_included( + &self, + path: &SystemPath, + mode: GlobFilterCheckMode, + ) -> IncludeResult { + match self.match_included_paths(path, mode) { + None => IncludeResult::NotIncluded, + Some(CheckPathMatch::Partial) => { + self.src_filter.is_directory_maybe_included(path, mode) + } + Some(CheckPathMatch::Full) => IncludeResult::Included, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum CheckPathMatch { + /// The path is a partial match of the checked path (it's a sub path) + Partial, + + /// The path matches a check path exactly. + Full, +} + +pub(crate) struct ProjectFilesWalker<'a> { + walker: WalkDirectoryBuilder, + + filter: ProjectFilesFilter<'a>, +} + +impl<'a> ProjectFilesWalker<'a> { + pub(crate) fn new(db: &'a dyn Db) -> Self { + let project = db.project(); + + let filter = ProjectFilesFilter::from_project(db, project); + + Self::from_paths(db, project.included_paths_or_root(db), filter) + .expect("included_paths_or_root to never return an empty iterator") + } + + /// Creates a walker for indexing the project files incrementally. + /// + /// The main difference to a full project walk is that `paths` may contain paths + /// that aren't part of the included files. + pub(crate) fn incremental

(db: &'a dyn Db, paths: impl IntoIterator) -> Option + where + P: AsRef, + { + let project = db.project(); + + let filter = ProjectFilesFilter::from_project(db, project); + + Self::from_paths(db, paths, filter) + } + + fn from_paths

( + db: &'a dyn Db, + paths: impl IntoIterator, + filter: ProjectFilesFilter<'a>, + ) -> Option + where + P: AsRef, + { + let mut paths = paths.into_iter(); + + let mut walker = db + .system() + .walk_directory(paths.next()?.as_ref()) + .standard_filters(db.project().settings(db).src().respect_ignore_files) + .ignore_hidden(false); + + for path in paths { + walker = walker.add(path); + } + + Some(Self { walker, filter }) + } + + /// Walks the project paths and collects the paths of all files that + /// are included in the project. + pub(crate) fn walk_paths(self) -> (Vec, Vec) { + let paths = std::sync::Mutex::new(Vec::new()); + let diagnostics = std::sync::Mutex::new(Vec::new()); + + self.walker.run(|| { + Box::new(|entry| { + match entry { + Ok(entry) => { + // Skip excluded directories unless they were explicitly passed to the walker + // (which is the case passed to `ty check `). + if entry.file_type().is_directory() && entry.depth() > 0 { + return match self.filter.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown) { + IncludeResult::Included => WalkState::Continue, + IncludeResult::Excluded => { + tracing::debug!("Skipping directory '{path}' because it is excluded by a default or `src.exclude` pattern", path=entry.path()); + WalkState::Skip + }, + IncludeResult::NotIncluded => { + tracing::debug!("Skipping directory `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI", path=entry.path()); + WalkState::Skip + }, + }; + } + + if entry.file_type().is_file() { + // Ignore any non python files to avoid creating too many entries in `Files`. + if entry + .path() + .extension() + .and_then(PySourceType::try_from_extension) + .is_none() + { + return WalkState::Continue; + } + + // For all files, except the ones that were explicitly passed to the walker (CLI), + // check if they're included in the project. + if entry.depth() > 0 { + match self.filter.is_file_included(entry.path(), GlobFilterCheckMode::TopDown) { + IncludeResult::Included => {}, + IncludeResult::Excluded => { + tracing::debug!("Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.", path=entry.path()); + return WalkState::Continue; + }, + IncludeResult::NotIncluded => { + tracing::debug!("Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.", path=entry.path()); + return WalkState::Continue; + }, + } + } + + let mut paths = paths.lock().unwrap(); + paths.push(entry.into_path()); + } + } + Err(error) => match error.kind() { + ErrorKind::Loop { .. } => { + unreachable!("Loops shouldn't be possible without following symlinks.") + } + ErrorKind::Io { path, err } => { + let mut diagnostics = diagnostics.lock().unwrap(); + let error = if let Some(path) = path { + WalkError::IOPathError { + path: path.clone(), + error: err.to_string(), + } + } else { + WalkError::IOError { + error: err.to_string(), + } + }; + + diagnostics.push(IOErrorDiagnostic { + file: None, + error: IOErrorKind::Walk(error), + }); + } + ErrorKind::NonUtf8Path { path } => { + diagnostics.lock().unwrap().push(IOErrorDiagnostic { + file: None, + error: IOErrorKind::Walk(WalkError::NonUtf8Path { + path: path.clone(), + }), + }); + } + }, + } + + WalkState::Continue + }) + }); + + ( + paths.into_inner().unwrap(), + diagnostics.into_inner().unwrap(), + ) + } + + pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec, Vec) { + let (paths, diagnostics) = self.walk_paths(); + + ( + paths + .into_iter() + .filter_map(move |path| { + // If this returns `None`, then the file was deleted between the `walk_directory` call and now. + // We can ignore this. + system_path_to_file(db, &path).ok() + }) + .collect(), + diagnostics, + ) + } + + pub(crate) fn collect_set(self, db: &dyn Db) -> (FxHashSet, Vec) { + let (paths, diagnostics) = self.walk_paths(); + + let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher); + + for path in paths { + if let Ok(file) = system_path_to_file(db, &path) { + files.insert(file); + } + } + + (files, diagnostics) + } +} + +#[derive(Error, Debug, Clone)] +pub(crate) enum WalkError { + #[error("`{path}`: {error}")] + IOPathError { path: SystemPathBuf, error: String }, + + #[error("Failed to walk project directory: {error}")] + IOError { error: String }, + + #[error("`{path}` is not a valid UTF-8 path")] + NonUtf8Path { path: PathBuf }, +} diff --git a/crates/red_knot_project/src/watch.rs b/crates/ty_project/src/watch.rs similarity index 98% rename from crates/red_knot_project/src/watch.rs rename to crates/ty_project/src/watch.rs index fe5db5a1ed423..9a7c62837c5c0 100644 --- a/crates/red_knot_project/src/watch.rs +++ b/crates/ty_project/src/watch.rs @@ -1,6 +1,6 @@ pub use project_watcher::ProjectWatcher; use ruff_db::system::{SystemPath, SystemPathBuf, SystemVirtualPathBuf}; -pub use watcher::{directory_watcher, EventHandler, Watcher}; +pub use watcher::{EventHandler, Watcher, directory_watcher}; mod project_watcher; mod watcher; diff --git a/crates/red_knot_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs similarity index 94% rename from crates/red_knot_project/src/watch/project_watcher.rs rename to crates/ty_project/src/watch/project_watcher.rs index 817957591950d..2a6a167312dab 100644 --- a/crates/red_knot_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -3,10 +3,9 @@ use std::hash::Hasher; use tracing::info; -use red_knot_python_semantic::system_module_search_paths; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_db::system::{SystemPath, SystemPathBuf}; -use ruff_db::Upcast; +use ty_python_semantic::system_module_search_paths; use crate::db::{Db, ProjectDatabase}; use crate::watch::Watcher; @@ -41,7 +40,7 @@ impl ProjectWatcher { } pub fn update(&mut self, db: &ProjectDatabase) { - let search_paths: Vec<_> = system_module_search_paths(db.upcast()).collect(); + let search_paths: Vec<_> = system_module_search_paths(db).collect(); let project_path = db.project().root(db); let new_cache_key = Self::compute_cache_key(project_path, &search_paths); @@ -105,7 +104,9 @@ impl ProjectWatcher { // Ruff otherwise stills works as expected. if let Err(error) = self.watcher.watch(path) { // TODO: Log a user-facing warning. - tracing::warn!("Failed to setup watcher for path `{path}`: {error}. You have to restart Ruff after making changes to files under this path or you might see stale results."); + tracing::warn!( + "Failed to setup watcher for path `{path}`: {error}. You have to restart Ruff after making changes to files under this path or you might see stale results." + ); self.has_errored_paths = true; } else { self.watched_paths.push(path.to_path_buf()); diff --git a/crates/red_knot_project/src/watch/watcher.rs b/crates/ty_project/src/watch/watcher.rs similarity index 98% rename from crates/red_knot_project/src/watch/watcher.rs rename to crates/ty_project/src/watch/watcher.rs index 8cee2dd7b5578..f3789a526b9e9 100644 --- a/crates/red_knot_project/src/watch/watcher.rs +++ b/crates/ty_project/src/watch/watcher.rs @@ -1,5 +1,5 @@ use notify::event::{CreateKind, MetadataKind, ModifyKind, RemoveKind, RenameMode}; -use notify::{recommended_watcher, EventKind, RecommendedWatcher, RecursiveMode, Watcher as _}; +use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher as _, recommended_watcher}; use ruff_db::system::{SystemPath, SystemPathBuf}; @@ -186,7 +186,7 @@ impl Debouncer { } } - #[allow(clippy::unused_self, clippy::needless_pass_by_value)] + #[expect(clippy::unused_self, clippy::needless_pass_by_value)] fn add_error(&mut self, error: notify::Error) { // Micha: I skimmed through some of notify's source code and it seems the most common errors // are IO errors. All other errors should really only happen when adding or removing a watched folders. diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml new file mode 100644 index 0000000000000..3550ce0e9b001 --- /dev/null +++ b/crates/ty_python_semantic/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "ty_python_semantic" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +ruff_db = { workspace = true } +ruff_annotate_snippets = { workspace = true } +ruff_index = { workspace = true, features = ["salsa"] } +ruff_macros = { workspace = true } +ruff_python_ast = { workspace = true, features = ["salsa"] } +ruff_python_parser = { workspace = true } +ruff_python_stdlib = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } +ruff_python_literal = { workspace = true } +ruff_python_trivia = { workspace = true } +ty_static = { workspace = true } + +anyhow = { workspace = true } +bitflags = { workspace = true } +camino = { workspace = true } +colored = { workspace = true } +compact_str = { workspace = true } +drop_bomb = { workspace = true } +get-size2 = { workspace = true } +indexmap = { workspace = true } +itertools = { workspace = true } +ordermap = { workspace = true } +salsa = { workspace = true, features = ["compact_str"] } +thiserror = { workspace = true } +tracing = { workspace = true } +rustc-hash = { workspace = true } +hashbrown = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +smallvec = { workspace = true } +static_assertions = { workspace = true } +test-case = { workspace = true } +memchr = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } + +[dev-dependencies] +ruff_db = { workspace = true, features = ["testing", "os"] } +ruff_python_parser = { workspace = true } +ty_python_semantic = { workspace = true, features = ["testing"] } +ty_static = { workspace = true } +ty_test = { workspace = true } +ty_vendored = { workspace = true } + +anyhow = { workspace = true } +dir-test = { workspace = true } +glob = { workspace = true } +insta = { workspace = true } +tempfile = { workspace = true } +quickcheck = { version = "1.0.3", default-features = false } +quickcheck_macros = { version = "1.0.0" } + +[features] +serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"] +testing = [] + +[lints] +workspace = true diff --git a/crates/red_knot_python_semantic/build.rs b/crates/ty_python_semantic/build.rs similarity index 100% rename from crates/red_knot_python_semantic/build.rs rename to crates/ty_python_semantic/build.rs diff --git a/crates/red_knot_python_semantic/mdtest.py b/crates/ty_python_semantic/mdtest.py similarity index 88% rename from crates/red_knot_python_semantic/mdtest.py rename to crates/ty_python_semantic/mdtest.py index a82edee78263b..e238c9ce89f65 100644 --- a/crates/red_knot_python_semantic/mdtest.py +++ b/crates/ty_python_semantic/mdtest.py @@ -1,4 +1,4 @@ -"""A runner for Markdown-based tests for Red Knot""" +"""A runner for Markdown-based tests for ty""" # /// script # requires-python = ">=3.11" # dependencies = [ @@ -18,8 +18,14 @@ from rich.console import Console from watchfiles import Change, watch -CRATE_NAME: Final = "red_knot_python_semantic" +CRATE_NAME: Final = "ty_python_semantic" CRATE_ROOT: Final = Path(__file__).resolve().parent +TY_VENDORED: Final = CRATE_ROOT.parent / "ty_vendored" +DIRS_TO_WATCH: Final = ( + CRATE_ROOT, + TY_VENDORED, + CRATE_ROOT.parent / "ty_test/src", +) MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest" @@ -158,20 +164,24 @@ def watch(self) -> Never: self._run_mdtest() self.console.print("[dim]Ready to watch for changes...[/dim]") - for changes in watch(CRATE_ROOT): + for changes in watch(*DIRS_TO_WATCH): new_md_files = set() changed_md_files = set() rust_code_has_changed = False + vendored_typeshed_has_changed = False for change, path_str in changes: path = Path(path_str) - if path.suffix == ".rs": - rust_code_has_changed = True - continue - - if path.suffix != ".md": - continue + match path.suffix: + case ".rs": + rust_code_has_changed = True + case ".pyi" if path.is_relative_to(TY_VENDORED): + vendored_typeshed_has_changed = True + case ".md": + pass + case _: + continue try: relative_path = Path(path).relative_to(MDTEST_DIR) @@ -199,6 +209,11 @@ def watch(self) -> Never: if rust_code_has_changed: if self._recompile_tests("Rust code has changed, recompiling tests..."): self._run_mdtest() + elif vendored_typeshed_has_changed: + if self._recompile_tests( + "Vendored typeshed has changed, recompiling tests..." + ): + self._run_mdtest() elif new_md_files: files = " ".join(file.as_posix() for file in new_md_files) self._recompile_tests( diff --git a/crates/red_knot_python_semantic/mdtest.py.lock b/crates/ty_python_semantic/mdtest.py.lock similarity index 100% rename from crates/red_knot_python_semantic/mdtest.py.lock rename to crates/ty_python_semantic/mdtest.py.lock diff --git a/crates/ty_python_semantic/resources/README.md b/crates/ty_python_semantic/resources/README.md new file mode 100644 index 0000000000000..ea0a69f18cd1d --- /dev/null +++ b/crates/ty_python_semantic/resources/README.md @@ -0,0 +1,4 @@ +Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking; +executed by the `tests/mdtest.rs` integration test. + +See `crates/ty_test/README.md` for documentation of this test format. diff --git a/crates/red_knot_project/resources/test/corpus/00_const.py b/crates/ty_python_semantic/resources/corpus/00_const.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_const.py rename to crates/ty_python_semantic/resources/corpus/00_const.py diff --git a/crates/red_knot_project/resources/test/corpus/00_empty.py b/crates/ty_python_semantic/resources/corpus/00_empty.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_empty.py rename to crates/ty_python_semantic/resources/corpus/00_empty.py diff --git a/crates/red_knot_project/resources/test/corpus/00_expr_discard.py b/crates/ty_python_semantic/resources/corpus/00_expr_discard.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_expr_discard.py rename to crates/ty_python_semantic/resources/corpus/00_expr_discard.py diff --git a/crates/red_knot_project/resources/test/corpus/00_expr_var1.py b/crates/ty_python_semantic/resources/corpus/00_expr_var1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_expr_var1.py rename to crates/ty_python_semantic/resources/corpus/00_expr_var1.py diff --git a/crates/red_knot_project/resources/test/corpus/01_expr_unary.py b/crates/ty_python_semantic/resources/corpus/01_expr_unary.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/01_expr_unary.py rename to crates/ty_python_semantic/resources/corpus/01_expr_unary.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_attr.py b/crates/ty_python_semantic/resources/corpus/02_expr_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_attr.py rename to crates/ty_python_semantic/resources/corpus/02_expr_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline.py b/crates/ty_python_semantic/resources/corpus/02_expr_attr_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline.py rename to crates/ty_python_semantic/resources/corpus/02_expr_attr_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline_assign.py b/crates/ty_python_semantic/resources/corpus/02_expr_attr_multiline_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline_assign.py rename to crates/ty_python_semantic/resources/corpus/02_expr_attr_multiline_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_bin_bool.py b/crates/ty_python_semantic/resources/corpus/02_expr_bin_bool.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_bin_bool.py rename to crates/ty_python_semantic/resources/corpus/02_expr_bin_bool.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_binary.py b/crates/ty_python_semantic/resources/corpus/02_expr_binary.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_binary.py rename to crates/ty_python_semantic/resources/corpus/02_expr_binary.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline.py b/crates/ty_python_semantic/resources/corpus/02_expr_bool_op_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline.py rename to crates/ty_python_semantic/resources/corpus/02_expr_bool_op_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline2.py b/crates/ty_python_semantic/resources/corpus/02_expr_bool_op_multiline2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline2.py rename to crates/ty_python_semantic/resources/corpus/02_expr_bool_op_multiline2.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_rel.py b/crates/ty_python_semantic/resources/corpus/02_expr_rel.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_rel.py rename to crates/ty_python_semantic/resources/corpus/02_expr_rel.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_rel_multiple.py b/crates/ty_python_semantic/resources/corpus/02_expr_rel_multiple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_rel_multiple.py rename to crates/ty_python_semantic/resources/corpus/02_expr_rel_multiple.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_subscr.py b/crates/ty_python_semantic/resources/corpus/02_expr_subscr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_subscr.py rename to crates/ty_python_semantic/resources/corpus/02_expr_subscr.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict.py b/crates/ty_python_semantic/resources/corpus/03_dict.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict.py rename to crates/ty_python_semantic/resources/corpus/03_dict.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict_ex.py b/crates/ty_python_semantic/resources/corpus/03_dict_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict_ex.py rename to crates/ty_python_semantic/resources/corpus/03_dict_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict_literal_large.py b/crates/ty_python_semantic/resources/corpus/03_dict_literal_large.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict_literal_large.py rename to crates/ty_python_semantic/resources/corpus/03_dict_literal_large.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict_unpack_huge.py b/crates/ty_python_semantic/resources/corpus/03_dict_unpack_huge.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict_unpack_huge.py rename to crates/ty_python_semantic/resources/corpus/03_dict_unpack_huge.py diff --git a/crates/red_knot_project/resources/test/corpus/03_list.py b/crates/ty_python_semantic/resources/corpus/03_list.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_list.py rename to crates/ty_python_semantic/resources/corpus/03_list.py diff --git a/crates/red_knot_project/resources/test/corpus/03_list_ex.py b/crates/ty_python_semantic/resources/corpus/03_list_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_list_ex.py rename to crates/ty_python_semantic/resources/corpus/03_list_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/03_list_large.py b/crates/ty_python_semantic/resources/corpus/03_list_large.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_list_large.py rename to crates/ty_python_semantic/resources/corpus/03_list_large.py diff --git a/crates/red_knot_project/resources/test/corpus/03_set.py b/crates/ty_python_semantic/resources/corpus/03_set.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_set.py rename to crates/ty_python_semantic/resources/corpus/03_set.py diff --git a/crates/red_knot_project/resources/test/corpus/03_set_multi.py b/crates/ty_python_semantic/resources/corpus/03_set_multi.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_set_multi.py rename to crates/ty_python_semantic/resources/corpus/03_set_multi.py diff --git a/crates/red_knot_project/resources/test/corpus/03_slice.py b/crates/ty_python_semantic/resources/corpus/03_slice.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_slice.py rename to crates/ty_python_semantic/resources/corpus/03_slice.py diff --git a/crates/red_knot_project/resources/test/corpus/03_slice_ext.py b/crates/ty_python_semantic/resources/corpus/03_slice_ext.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_slice_ext.py rename to crates/ty_python_semantic/resources/corpus/03_slice_ext.py diff --git a/crates/red_knot_project/resources/test/corpus/03_tuple.py b/crates/ty_python_semantic/resources/corpus/03_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_tuple.py rename to crates/ty_python_semantic/resources/corpus/03_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/03_tuple_ex.py b/crates/ty_python_semantic/resources/corpus/03_tuple_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_tuple_ex.py rename to crates/ty_python_semantic/resources/corpus/03_tuple_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign.py b/crates/ty_python_semantic/resources/corpus/04_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign.py rename to crates/ty_python_semantic/resources/corpus/04_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_attr.py b/crates/ty_python_semantic/resources/corpus/04_assign_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_attr.py rename to crates/ty_python_semantic/resources/corpus/04_assign_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_attr_func.py b/crates/ty_python_semantic/resources/corpus/04_assign_attr_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_attr_func.py rename to crates/ty_python_semantic/resources/corpus/04_assign_attr_func.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_named_expr.py b/crates/ty_python_semantic/resources/corpus/04_assign_named_expr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_named_expr.py rename to crates/ty_python_semantic/resources/corpus/04_assign_named_expr.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_subscr.py b/crates/ty_python_semantic/resources/corpus/04_assign_subscr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_subscr.py rename to crates/ty_python_semantic/resources/corpus/04_assign_subscr.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_unpack.py b/crates/ty_python_semantic/resources/corpus/04_assign_unpack.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_unpack.py rename to crates/ty_python_semantic/resources/corpus/04_assign_unpack.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_unpack_ex.py b/crates/ty_python_semantic/resources/corpus/04_assign_unpack_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_unpack_ex.py rename to crates/ty_python_semantic/resources/corpus/04_assign_unpack_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_unpack_tuple.py b/crates/ty_python_semantic/resources/corpus/04_assign_unpack_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_unpack_tuple.py rename to crates/ty_python_semantic/resources/corpus/04_assign_unpack_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign.py b/crates/ty_python_semantic/resources/corpus/04_aug_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign.py rename to crates/ty_python_semantic/resources/corpus/04_aug_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_multiline.py b/crates/ty_python_semantic/resources/corpus/04_aug_assign_attr_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_multiline.py rename to crates/ty_python_semantic/resources/corpus/04_aug_assign_attr_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_sub.py b/crates/ty_python_semantic/resources/corpus/04_aug_assign_attr_sub.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_sub.py rename to crates/ty_python_semantic/resources/corpus/04_aug_assign_attr_sub.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall.py b/crates/ty_python_semantic/resources/corpus/05_funcall.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall.py rename to crates/ty_python_semantic/resources/corpus/05_funcall.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_1.py b/crates/ty_python_semantic/resources/corpus/05_funcall_1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_1.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_1.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_2.py b/crates/ty_python_semantic/resources/corpus/05_funcall_2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_2.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_2.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_in_multiline_tuple.py b/crates/ty_python_semantic/resources/corpus/05_funcall_in_multiline_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_in_multiline_tuple.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_in_multiline_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_kw.py b/crates/ty_python_semantic/resources/corpus/05_funcall_kw.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_kw.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_kw.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_kw_many.py b/crates/ty_python_semantic/resources/corpus/05_funcall_kw_many.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_kw_many.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_kw_many.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_kw_pos.py b/crates/ty_python_semantic/resources/corpus/05_funcall_kw_pos.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_kw_pos.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_kw_pos.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_method_multiline.py b/crates/ty_python_semantic/resources/corpus/05_funcall_method_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_method_multiline.py rename to crates/ty_python_semantic/resources/corpus/05_funcall_method_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_kwargs.py b/crates/ty_python_semantic/resources/corpus/06_funcall_kwargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_kwargs.py rename to crates/ty_python_semantic/resources/corpus/06_funcall_kwargs.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_many_args.py b/crates/ty_python_semantic/resources/corpus/06_funcall_many_args.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_many_args.py rename to crates/ty_python_semantic/resources/corpus/06_funcall_many_args.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_starargs_ex.py b/crates/ty_python_semantic/resources/corpus/06_funcall_starargs_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_starargs_ex.py rename to crates/ty_python_semantic/resources/corpus/06_funcall_starargs_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_varargs.py b/crates/ty_python_semantic/resources/corpus/06_funcall_varargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_varargs.py rename to crates/ty_python_semantic/resources/corpus/06_funcall_varargs.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs.py b/crates/ty_python_semantic/resources/corpus/06_funcall_varargs_kwargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs.py rename to crates/ty_python_semantic/resources/corpus/06_funcall_varargs_kwargs.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py b/crates/ty_python_semantic/resources/corpus/06_funcall_varargs_kwargs_mixed.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py rename to crates/ty_python_semantic/resources/corpus/06_funcall_varargs_kwargs_mixed.py diff --git a/crates/red_knot_project/resources/test/corpus/07_ifexpr.py b/crates/ty_python_semantic/resources/corpus/07_ifexpr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/07_ifexpr.py rename to crates/ty_python_semantic/resources/corpus/07_ifexpr.py diff --git a/crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline.py b/crates/ty_python_semantic/resources/corpus/07_ifexpr_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline.py rename to crates/ty_python_semantic/resources/corpus/07_ifexpr_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline2.py b/crates/ty_python_semantic/resources/corpus/07_ifexpr_multiline2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline2.py rename to crates/ty_python_semantic/resources/corpus/07_ifexpr_multiline2.py diff --git a/crates/red_knot_project/resources/test/corpus/08_del.py b/crates/ty_python_semantic/resources/corpus/08_del.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/08_del.py rename to crates/ty_python_semantic/resources/corpus/08_del.py diff --git a/crates/red_knot_project/resources/test/corpus/08_del_multi.py b/crates/ty_python_semantic/resources/corpus/08_del_multi.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/08_del_multi.py rename to crates/ty_python_semantic/resources/corpus/08_del_multi.py diff --git a/crates/red_knot_project/resources/test/corpus/09_pass.py b/crates/ty_python_semantic/resources/corpus/09_pass.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/09_pass.py rename to crates/ty_python_semantic/resources/corpus/09_pass.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if.py b/crates/ty_python_semantic/resources/corpus/10_if.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if.py rename to crates/ty_python_semantic/resources/corpus/10_if.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_chained_compare.py b/crates/ty_python_semantic/resources/corpus/10_if_chained_compare.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_chained_compare.py rename to crates/ty_python_semantic/resources/corpus/10_if_chained_compare.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_false.py b/crates/ty_python_semantic/resources/corpus/10_if_false.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_false.py rename to crates/ty_python_semantic/resources/corpus/10_if_false.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_invalid.py b/crates/ty_python_semantic/resources/corpus/10_if_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_invalid.py rename to crates/ty_python_semantic/resources/corpus/10_if_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_true.py b/crates/ty_python_semantic/resources/corpus/10_if_true.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_true.py rename to crates/ty_python_semantic/resources/corpus/10_if_true.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_with_named_expr.py b/crates/ty_python_semantic/resources/corpus/10_if_with_named_expr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_with_named_expr.py rename to crates/ty_python_semantic/resources/corpus/10_if_with_named_expr.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else.py b/crates/ty_python_semantic/resources/corpus/11_if_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else.py rename to crates/ty_python_semantic/resources/corpus/11_if_else.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else_deeply_nested_for.py b/crates/ty_python_semantic/resources/corpus/11_if_else_deeply_nested_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else_deeply_nested_for.py rename to crates/ty_python_semantic/resources/corpus/11_if_else_deeply_nested_for.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else_false.py b/crates/ty_python_semantic/resources/corpus/11_if_else_false.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else_false.py rename to crates/ty_python_semantic/resources/corpus/11_if_else_false.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else_true.py b/crates/ty_python_semantic/resources/corpus/11_if_else_true.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else_true.py rename to crates/ty_python_semantic/resources/corpus/11_if_else_true.py diff --git a/crates/red_knot_project/resources/test/corpus/12_if_elif.py b/crates/ty_python_semantic/resources/corpus/12_if_elif.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/12_if_elif.py rename to crates/ty_python_semantic/resources/corpus/12_if_elif.py diff --git a/crates/red_knot_project/resources/test/corpus/12_if_elif_else.py b/crates/ty_python_semantic/resources/corpus/12_if_elif_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/12_if_elif_else.py rename to crates/ty_python_semantic/resources/corpus/12_if_elif_else.py diff --git a/crates/red_knot_project/resources/test/corpus/13_ifelse_complex1.py b/crates/ty_python_semantic/resources/corpus/13_ifelse_complex1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/13_ifelse_complex1.py rename to crates/ty_python_semantic/resources/corpus/13_ifelse_complex1.py diff --git a/crates/red_knot_project/resources/test/corpus/13_ifelse_many.py b/crates/ty_python_semantic/resources/corpus/13_ifelse_many.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/13_ifelse_many.py rename to crates/ty_python_semantic/resources/corpus/13_ifelse_many.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while.py b/crates/ty_python_semantic/resources/corpus/15_while.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while.py rename to crates/ty_python_semantic/resources/corpus/15_while.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break.py b/crates/ty_python_semantic/resources/corpus/15_while_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break.py rename to crates/ty_python_semantic/resources/corpus/15_while_break.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_in_finally.py b/crates/ty_python_semantic/resources/corpus/15_while_break_in_finally.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_in_finally.py rename to crates/ty_python_semantic/resources/corpus/15_while_break_in_finally.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_class.py b/crates/ty_python_semantic/resources/corpus/15_while_break_invalid_in_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_class.py rename to crates/ty_python_semantic/resources/corpus/15_while_break_invalid_in_class.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_func.py b/crates/ty_python_semantic/resources/corpus/15_while_break_invalid_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_func.py rename to crates/ty_python_semantic/resources/corpus/15_while_break_invalid_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_non_empty.py b/crates/ty_python_semantic/resources/corpus/15_while_break_non_empty.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_non_empty.py rename to crates/ty_python_semantic/resources/corpus/15_while_break_non_empty.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_non_exit.py b/crates/ty_python_semantic/resources/corpus/15_while_break_non_exit.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_non_exit.py rename to crates/ty_python_semantic/resources/corpus/15_while_break_non_exit.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_continue.py b/crates/ty_python_semantic/resources/corpus/15_while_continue.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_continue.py rename to crates/ty_python_semantic/resources/corpus/15_while_continue.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_false.py b/crates/ty_python_semantic/resources/corpus/15_while_false.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_false.py rename to crates/ty_python_semantic/resources/corpus/15_while_false.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_infinite.py b/crates/ty_python_semantic/resources/corpus/15_while_infinite.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_infinite.py rename to crates/ty_python_semantic/resources/corpus/15_while_infinite.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_true.py b/crates/ty_python_semantic/resources/corpus/15_while_true.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_true.py rename to crates/ty_python_semantic/resources/corpus/15_while_true.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for.py b/crates/ty_python_semantic/resources/corpus/16_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for.py rename to crates/ty_python_semantic/resources/corpus/16_for.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_break.py b/crates/ty_python_semantic/resources/corpus/16_for_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_break.py rename to crates/ty_python_semantic/resources/corpus/16_for_break.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_class.py b/crates/ty_python_semantic/resources/corpus/16_for_break_invalid_in_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_class.py rename to crates/ty_python_semantic/resources/corpus/16_for_break_invalid_in_class.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_func.py b/crates/ty_python_semantic/resources/corpus/16_for_break_invalid_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_func.py rename to crates/ty_python_semantic/resources/corpus/16_for_break_invalid_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_continue.py b/crates/ty_python_semantic/resources/corpus/16_for_continue.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_continue.py rename to crates/ty_python_semantic/resources/corpus/16_for_continue.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_else.py b/crates/ty_python_semantic/resources/corpus/16_for_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_else.py rename to crates/ty_python_semantic/resources/corpus/16_for_else.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_invalid.py b/crates/ty_python_semantic/resources/corpus/16_for_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_invalid.py rename to crates/ty_python_semantic/resources/corpus/16_for_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_list_literal.py b/crates/ty_python_semantic/resources/corpus/16_for_list_literal.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_list_literal.py rename to crates/ty_python_semantic/resources/corpus/16_for_list_literal.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_nested_ifs.py b/crates/ty_python_semantic/resources/corpus/16_for_nested_ifs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_nested_ifs.py rename to crates/ty_python_semantic/resources/corpus/16_for_nested_ifs.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda.py b/crates/ty_python_semantic/resources/corpus/20_lambda.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda.py rename to crates/ty_python_semantic/resources/corpus/20_lambda.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda_const.py b/crates/ty_python_semantic/resources/corpus/20_lambda_const.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda_const.py rename to crates/ty_python_semantic/resources/corpus/20_lambda_const.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda_default_arg.py b/crates/ty_python_semantic/resources/corpus/20_lambda_default_arg.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda_default_arg.py rename to crates/ty_python_semantic/resources/corpus/20_lambda_default_arg.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda_ifelse.py b/crates/ty_python_semantic/resources/corpus/20_lambda_ifelse.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda_ifelse.py rename to crates/ty_python_semantic/resources/corpus/20_lambda_ifelse.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func1.py b/crates/ty_python_semantic/resources/corpus/21_func1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func1.py rename to crates/ty_python_semantic/resources/corpus/21_func1.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func1_ret.py b/crates/ty_python_semantic/resources/corpus/21_func1_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func1_ret.py rename to crates/ty_python_semantic/resources/corpus/21_func1_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func_assign.py b/crates/ty_python_semantic/resources/corpus/21_func_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func_assign.py rename to crates/ty_python_semantic/resources/corpus/21_func_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func_assign2.py b/crates/ty_python_semantic/resources/corpus/21_func_assign2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func_assign2.py rename to crates/ty_python_semantic/resources/corpus/21_func_assign2.py diff --git a/crates/red_knot_project/resources/test/corpus/22_func_arg.py b/crates/ty_python_semantic/resources/corpus/22_func_arg.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/22_func_arg.py rename to crates/ty_python_semantic/resources/corpus/22_func_arg.py diff --git a/crates/red_knot_project/resources/test/corpus/22_func_vararg.py b/crates/ty_python_semantic/resources/corpus/22_func_vararg.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/22_func_vararg.py rename to crates/ty_python_semantic/resources/corpus/22_func_vararg.py diff --git a/crates/red_knot_project/resources/test/corpus/23_func_ret.py b/crates/ty_python_semantic/resources/corpus/23_func_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/23_func_ret.py rename to crates/ty_python_semantic/resources/corpus/23_func_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/23_func_ret_val.py b/crates/ty_python_semantic/resources/corpus/23_func_ret_val.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/23_func_ret_val.py rename to crates/ty_python_semantic/resources/corpus/23_func_ret_val.py diff --git a/crates/red_knot_project/resources/test/corpus/24_func_if_ret.py b/crates/ty_python_semantic/resources/corpus/24_func_if_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/24_func_if_ret.py rename to crates/ty_python_semantic/resources/corpus/24_func_if_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/24_func_ifelse_ret.py b/crates/ty_python_semantic/resources/corpus/24_func_ifelse_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/24_func_ifelse_ret.py rename to crates/ty_python_semantic/resources/corpus/24_func_ifelse_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/24_func_ifnot_ret.py b/crates/ty_python_semantic/resources/corpus/24_func_ifnot_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/24_func_ifnot_ret.py rename to crates/ty_python_semantic/resources/corpus/24_func_ifnot_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations.py b/crates/ty_python_semantic/resources/corpus/25_func_annotations.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations.py rename to crates/ty_python_semantic/resources/corpus/25_func_annotations.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_nested.py b/crates/ty_python_semantic/resources/corpus/25_func_annotations_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_nested.py rename to crates/ty_python_semantic/resources/corpus/25_func_annotations_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_same_name.py b/crates/ty_python_semantic/resources/corpus/25_func_annotations_same_name.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_same_name.py rename to crates/ty_python_semantic/resources/corpus/25_func_annotations_same_name.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_scope.py b/crates/ty_python_semantic/resources/corpus/25_func_annotations_scope.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_scope.py rename to crates/ty_python_semantic/resources/corpus/25_func_annotations_scope.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_starred.py b/crates/ty_python_semantic/resources/corpus/25_func_annotations_starred.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_starred.py rename to crates/ty_python_semantic/resources/corpus/25_func_annotations_starred.py diff --git a/crates/red_knot_project/resources/test/corpus/26_func_const_defaults.py b/crates/ty_python_semantic/resources/corpus/26_func_const_defaults.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/26_func_const_defaults.py rename to crates/ty_python_semantic/resources/corpus/26_func_const_defaults.py diff --git a/crates/red_knot_project/resources/test/corpus/26_func_defaults_same_name.py b/crates/ty_python_semantic/resources/corpus/26_func_defaults_same_name.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/26_func_defaults_same_name.py rename to crates/ty_python_semantic/resources/corpus/26_func_defaults_same_name.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic.py b/crates/ty_python_semantic/resources/corpus/27_func_generic.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_bound.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_bound.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_bound.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_bound.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_constraint.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_constraint.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_constraint.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_constraint.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_default.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_default.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_default.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_paramspec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_paramspec.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec_default.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_paramspec_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec_default.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_paramspec_default.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_tuple.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_tuple.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_tuple_default.py b/crates/ty_python_semantic/resources/corpus/27_func_generic_tuple_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_tuple_default.py rename to crates/ty_python_semantic/resources/corpus/27_func_generic_tuple_default.py diff --git a/crates/red_knot_project/resources/test/corpus/30_func_enclosed.py b/crates/ty_python_semantic/resources/corpus/30_func_enclosed.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/30_func_enclosed.py rename to crates/ty_python_semantic/resources/corpus/30_func_enclosed.py diff --git a/crates/red_knot_project/resources/test/corpus/30_func_enclosed_many.py b/crates/ty_python_semantic/resources/corpus/30_func_enclosed_many.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/30_func_enclosed_many.py rename to crates/ty_python_semantic/resources/corpus/30_func_enclosed_many.py diff --git a/crates/red_knot_project/resources/test/corpus/31_func_global.py b/crates/ty_python_semantic/resources/corpus/31_func_global.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/31_func_global.py rename to crates/ty_python_semantic/resources/corpus/31_func_global.py diff --git a/crates/red_knot_project/resources/test/corpus/31_func_global_annotated_later.py b/crates/ty_python_semantic/resources/corpus/31_func_global_annotated_later.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/31_func_global_annotated_later.py rename to crates/ty_python_semantic/resources/corpus/31_func_global_annotated_later.py diff --git a/crates/red_knot_project/resources/test/corpus/31_func_nonlocal.py b/crates/ty_python_semantic/resources/corpus/31_func_nonlocal.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/31_func_nonlocal.py rename to crates/ty_python_semantic/resources/corpus/31_func_nonlocal.py diff --git a/crates/red_knot_project/resources/test/corpus/32_func_global_nested.py b/crates/ty_python_semantic/resources/corpus/32_func_global_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/32_func_global_nested.py rename to crates/ty_python_semantic/resources/corpus/32_func_global_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py b/crates/ty_python_semantic/resources/corpus/33_func_with_docstring_optimizable_tuple_and_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py rename to crates/ty_python_semantic/resources/corpus/33_func_with_docstring_optimizable_tuple_and_return.py diff --git a/crates/red_knot_project/resources/test/corpus/40_import.py b/crates/ty_python_semantic/resources/corpus/40_import.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/40_import.py rename to crates/ty_python_semantic/resources/corpus/40_import.py diff --git a/crates/red_knot_project/resources/test/corpus/41_from_import.py b/crates/ty_python_semantic/resources/corpus/41_from_import.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/41_from_import.py rename to crates/ty_python_semantic/resources/corpus/41_from_import.py diff --git a/crates/red_knot_project/resources/test/corpus/42_import_from_dot.py b/crates/ty_python_semantic/resources/corpus/42_import_from_dot.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/42_import_from_dot.py rename to crates/ty_python_semantic/resources/corpus/42_import_from_dot.py diff --git a/crates/red_knot_project/resources/test/corpus/50_yield.py b/crates/ty_python_semantic/resources/corpus/50_yield.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/50_yield.py rename to crates/ty_python_semantic/resources/corpus/50_yield.py diff --git a/crates/red_knot_project/resources/test/corpus/51_gen_comp.py b/crates/ty_python_semantic/resources/corpus/51_gen_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/51_gen_comp.py rename to crates/ty_python_semantic/resources/corpus/51_gen_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/51_gen_comp2.py b/crates/ty_python_semantic/resources/corpus/51_gen_comp2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/51_gen_comp2.py rename to crates/ty_python_semantic/resources/corpus/51_gen_comp2.py diff --git a/crates/red_knot_project/resources/test/corpus/52_gen_comp_if.py b/crates/ty_python_semantic/resources/corpus/52_gen_comp_if.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/52_gen_comp_if.py rename to crates/ty_python_semantic/resources/corpus/52_gen_comp_if.py diff --git a/crates/red_knot_project/resources/test/corpus/53_dict_comp.py b/crates/ty_python_semantic/resources/corpus/53_dict_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_dict_comp.py rename to crates/ty_python_semantic/resources/corpus/53_dict_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/53_list_comp.py b/crates/ty_python_semantic/resources/corpus/53_list_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_list_comp.py rename to crates/ty_python_semantic/resources/corpus/53_list_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/53_list_comp_method.py b/crates/ty_python_semantic/resources/corpus/53_list_comp_method.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_list_comp_method.py rename to crates/ty_python_semantic/resources/corpus/53_list_comp_method.py diff --git a/crates/red_knot_project/resources/test/corpus/53_set_comp.py b/crates/ty_python_semantic/resources/corpus/53_set_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_set_comp.py rename to crates/ty_python_semantic/resources/corpus/53_set_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_func.py b/crates/ty_python_semantic/resources/corpus/54_list_comp_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_func.py rename to crates/ty_python_semantic/resources/corpus/54_list_comp_func.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_lambda.py b/crates/ty_python_semantic/resources/corpus/54_list_comp_lambda.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_lambda.py rename to crates/ty_python_semantic/resources/corpus/54_list_comp_lambda.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_lambda_listcomp.py b/crates/ty_python_semantic/resources/corpus/54_list_comp_lambda_listcomp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_lambda_listcomp.py rename to crates/ty_python_semantic/resources/corpus/54_list_comp_lambda_listcomp.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_recur_func.py b/crates/ty_python_semantic/resources/corpus/54_list_comp_recur_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_recur_func.py rename to crates/ty_python_semantic/resources/corpus/54_list_comp_recur_func.py diff --git a/crates/red_knot_project/resources/test/corpus/55_list_comp_nested.py b/crates/ty_python_semantic/resources/corpus/55_list_comp_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/55_list_comp_nested.py rename to crates/ty_python_semantic/resources/corpus/55_list_comp_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/56_yield_from.py b/crates/ty_python_semantic/resources/corpus/56_yield_from.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/56_yield_from.py rename to crates/ty_python_semantic/resources/corpus/56_yield_from.py diff --git a/crates/red_knot_project/resources/test/corpus/57_await.py b/crates/ty_python_semantic/resources/corpus/57_await.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/57_await.py rename to crates/ty_python_semantic/resources/corpus/57_await.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for.py b/crates/ty_python_semantic/resources/corpus/58_async_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for.py rename to crates/ty_python_semantic/resources/corpus/58_async_for.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_break.py b/crates/ty_python_semantic/resources/corpus/58_async_for_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_break.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_break.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_continue.py b/crates/ty_python_semantic/resources/corpus/58_async_for_continue.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_continue.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_continue.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_dict_comp.py b/crates/ty_python_semantic/resources/corpus/58_async_for_dict_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_dict_comp.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_dict_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_else.py b/crates/ty_python_semantic/resources/corpus/58_async_for_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_else.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_else.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_gen_comp.py b/crates/ty_python_semantic/resources/corpus/58_async_for_gen_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_gen_comp.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_gen_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_list_comp.py b/crates/ty_python_semantic/resources/corpus/58_async_for_list_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_list_comp.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_list_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_set_comp.py b/crates/ty_python_semantic/resources/corpus/58_async_for_set_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_set_comp.py rename to crates/ty_python_semantic/resources/corpus/58_async_for_set_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/59_async_with.py b/crates/ty_python_semantic/resources/corpus/59_async_with.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/59_async_with.py rename to crates/ty_python_semantic/resources/corpus/59_async_with.py diff --git a/crates/red_knot_project/resources/test/corpus/59_async_with_nested_with.py b/crates/ty_python_semantic/resources/corpus/59_async_with_nested_with.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/59_async_with_nested_with.py rename to crates/ty_python_semantic/resources/corpus/59_async_with_nested_with.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_except.py b/crates/ty_python_semantic/resources/corpus/60_try_except.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_except.py rename to crates/ty_python_semantic/resources/corpus/60_try_except.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_except2.py b/crates/ty_python_semantic/resources/corpus/60_try_except2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_except2.py rename to crates/ty_python_semantic/resources/corpus/60_try_except2.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_except_bare.py b/crates/ty_python_semantic/resources/corpus/60_try_except_bare.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_except_bare.py rename to crates/ty_python_semantic/resources/corpus/60_try_except_bare.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally.py b/crates/ty_python_semantic/resources/corpus/60_try_finally.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally.py rename to crates/ty_python_semantic/resources/corpus/60_try_finally.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_codeobj.py b/crates/ty_python_semantic/resources/corpus/60_try_finally_codeobj.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_codeobj.py rename to crates/ty_python_semantic/resources/corpus/60_try_finally_codeobj.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_cond.py b/crates/ty_python_semantic/resources/corpus/60_try_finally_cond.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_cond.py rename to crates/ty_python_semantic/resources/corpus/60_try_finally_cond.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_for.py b/crates/ty_python_semantic/resources/corpus/60_try_finally_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_for.py rename to crates/ty_python_semantic/resources/corpus/60_try_finally_for.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_ret.py b/crates/ty_python_semantic/resources/corpus/60_try_finally_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_ret.py rename to crates/ty_python_semantic/resources/corpus/60_try_finally_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/61_try_except_finally.py b/crates/ty_python_semantic/resources/corpus/61_try_except_finally.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/61_try_except_finally.py rename to crates/ty_python_semantic/resources/corpus/61_try_except_finally.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_as.py b/crates/ty_python_semantic/resources/corpus/62_try_except_as.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_as.py rename to crates/ty_python_semantic/resources/corpus/62_try_except_as.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_break.py b/crates/ty_python_semantic/resources/corpus/62_try_except_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_break.py rename to crates/ty_python_semantic/resources/corpus/62_try_except_break.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_cond.py b/crates/ty_python_semantic/resources/corpus/62_try_except_cond.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_cond.py rename to crates/ty_python_semantic/resources/corpus/62_try_except_cond.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_double_nested_inside_if_else.py b/crates/ty_python_semantic/resources/corpus/62_try_except_double_nested_inside_if_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_double_nested_inside_if_else.py rename to crates/ty_python_semantic/resources/corpus/62_try_except_double_nested_inside_if_else.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_return.py b/crates/ty_python_semantic/resources/corpus/62_try_except_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_return.py rename to crates/ty_python_semantic/resources/corpus/62_try_except_return.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise.py b/crates/ty_python_semantic/resources/corpus/63_raise.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise.py rename to crates/ty_python_semantic/resources/corpus/63_raise.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise_func.py b/crates/ty_python_semantic/resources/corpus/63_raise_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise_func.py rename to crates/ty_python_semantic/resources/corpus/63_raise_func.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise_x.py b/crates/ty_python_semantic/resources/corpus/63_raise_x.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise_x.py rename to crates/ty_python_semantic/resources/corpus/63_raise_x.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise_x_from_y.py b/crates/ty_python_semantic/resources/corpus/63_raise_x_from_y.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise_x_from_y.py rename to crates/ty_python_semantic/resources/corpus/63_raise_x_from_y.py diff --git a/crates/red_knot_project/resources/test/corpus/64_assert.py b/crates/ty_python_semantic/resources/corpus/64_assert.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/64_assert.py rename to crates/ty_python_semantic/resources/corpus/64_assert.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with.py b/crates/ty_python_semantic/resources/corpus/67_with.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with.py rename to crates/ty_python_semantic/resources/corpus/67_with.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_as.py b/crates/ty_python_semantic/resources/corpus/67_with_as.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_as.py rename to crates/ty_python_semantic/resources/corpus/67_with_as.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_as_func.py b/crates/ty_python_semantic/resources/corpus/67_with_as_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_as_func.py rename to crates/ty_python_semantic/resources/corpus/67_with_as_func.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_cond_return.py b/crates/ty_python_semantic/resources/corpus/67_with_cond_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_cond_return.py rename to crates/ty_python_semantic/resources/corpus/67_with_cond_return.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py b/crates/ty_python_semantic/resources/corpus/67_with_inside_try_finally_multiple_terminal_elif.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py rename to crates/ty_python_semantic/resources/corpus/67_with_inside_try_finally_multiple_terminal_elif.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py b/crates/ty_python_semantic/resources/corpus/67_with_inside_try_finally_preceding_terminal_except.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py rename to crates/ty_python_semantic/resources/corpus/67_with_inside_try_finally_preceding_terminal_except.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_multi_exit.py b/crates/ty_python_semantic/resources/corpus/67_with_multi_exit.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_multi_exit.py rename to crates/ty_python_semantic/resources/corpus/67_with_multi_exit.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_non_name_target.py b/crates/ty_python_semantic/resources/corpus/67_with_non_name_target.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_non_name_target.py rename to crates/ty_python_semantic/resources/corpus/67_with_non_name_target.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_return.py b/crates/ty_python_semantic/resources/corpus/67_with_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_return.py rename to crates/ty_python_semantic/resources/corpus/67_with_return.py diff --git a/crates/red_knot_project/resources/test/corpus/68_with2.py b/crates/ty_python_semantic/resources/corpus/68_with2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/68_with2.py rename to crates/ty_python_semantic/resources/corpus/68_with2.py diff --git a/crates/red_knot_project/resources/test/corpus/69_for_try_except_continue1.py b/crates/ty_python_semantic/resources/corpus/69_for_try_except_continue1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/69_for_try_except_continue1.py rename to crates/ty_python_semantic/resources/corpus/69_for_try_except_continue1.py diff --git a/crates/red_knot_project/resources/test/corpus/69_for_try_except_continue2.py b/crates/ty_python_semantic/resources/corpus/69_for_try_except_continue2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/69_for_try_except_continue2.py rename to crates/ty_python_semantic/resources/corpus/69_for_try_except_continue2.py diff --git a/crates/red_knot_project/resources/test/corpus/69_for_try_except_continue3.py b/crates/ty_python_semantic/resources/corpus/69_for_try_except_continue3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/69_for_try_except_continue3.py rename to crates/ty_python_semantic/resources/corpus/69_for_try_except_continue3.py diff --git a/crates/red_knot_project/resources/test/corpus/70_class.py b/crates/ty_python_semantic/resources/corpus/70_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/70_class.py rename to crates/ty_python_semantic/resources/corpus/70_class.py diff --git a/crates/red_knot_project/resources/test/corpus/70_class_base.py b/crates/ty_python_semantic/resources/corpus/70_class_base.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/70_class_base.py rename to crates/ty_python_semantic/resources/corpus/70_class_base.py diff --git a/crates/red_knot_project/resources/test/corpus/70_class_doc_str.py b/crates/ty_python_semantic/resources/corpus/70_class_doc_str.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/70_class_doc_str.py rename to crates/ty_python_semantic/resources/corpus/70_class_doc_str.py diff --git a/crates/red_knot_project/resources/test/corpus/71_class_meth.py b/crates/ty_python_semantic/resources/corpus/71_class_meth.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/71_class_meth.py rename to crates/ty_python_semantic/resources/corpus/71_class_meth.py diff --git a/crates/red_knot_project/resources/test/corpus/71_class_var.py b/crates/ty_python_semantic/resources/corpus/71_class_var.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/71_class_var.py rename to crates/ty_python_semantic/resources/corpus/71_class_var.py diff --git a/crates/red_knot_project/resources/test/corpus/72_class_mix.py b/crates/ty_python_semantic/resources/corpus/72_class_mix.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/72_class_mix.py rename to crates/ty_python_semantic/resources/corpus/72_class_mix.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic.py b/crates/ty_python_semantic/resources/corpus/73_class_generic.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_bounds.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_bounds.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_bounds.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_bounds.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_constraints.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_constraints.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_constraints.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_constraints.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_defaults.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_defaults.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_defaults.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_defaults.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_paramspec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_paramspec.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec_default.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_paramspec_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec_default.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_paramspec_default.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_tuple.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_tuple.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_tuple_default.py b/crates/ty_python_semantic/resources/corpus/73_class_generic_tuple_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_tuple_default.py rename to crates/ty_python_semantic/resources/corpus/73_class_generic_tuple_default.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_kwargs.py b/crates/ty_python_semantic/resources/corpus/74_class_kwargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_kwargs.py rename to crates/ty_python_semantic/resources/corpus/74_class_kwargs.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_kwargs_2.py b/crates/ty_python_semantic/resources/corpus/74_class_kwargs_2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_kwargs_2.py rename to crates/ty_python_semantic/resources/corpus/74_class_kwargs_2.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_super.py b/crates/ty_python_semantic/resources/corpus/74_class_super.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_super.py rename to crates/ty_python_semantic/resources/corpus/74_class_super.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_super_nested.py b/crates/ty_python_semantic/resources/corpus/74_class_super_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_super_nested.py rename to crates/ty_python_semantic/resources/corpus/74_class_super_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/74_just_super.py b/crates/ty_python_semantic/resources/corpus/74_just_super.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_just_super.py rename to crates/ty_python_semantic/resources/corpus/74_just_super.py diff --git a/crates/red_knot_project/resources/test/corpus/75_classderef.py b/crates/ty_python_semantic/resources/corpus/75_classderef.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/75_classderef.py rename to crates/ty_python_semantic/resources/corpus/75_classderef.py diff --git a/crates/red_knot_project/resources/test/corpus/75_classderef_no.py b/crates/ty_python_semantic/resources/corpus/75_classderef_no.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/75_classderef_no.py rename to crates/ty_python_semantic/resources/corpus/75_classderef_no.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal1.py b/crates/ty_python_semantic/resources/corpus/76_class_nonlocal1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal1.py rename to crates/ty_python_semantic/resources/corpus/76_class_nonlocal1.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal2.py b/crates/ty_python_semantic/resources/corpus/76_class_nonlocal2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal2.py rename to crates/ty_python_semantic/resources/corpus/76_class_nonlocal2.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal3.py b/crates/ty_python_semantic/resources/corpus/76_class_nonlocal3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal3.py rename to crates/ty_python_semantic/resources/corpus/76_class_nonlocal3.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal4.py b/crates/ty_python_semantic/resources/corpus/76_class_nonlocal4.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal4.py rename to crates/ty_python_semantic/resources/corpus/76_class_nonlocal4.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal5.py b/crates/ty_python_semantic/resources/corpus/76_class_nonlocal5.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal5.py rename to crates/ty_python_semantic/resources/corpus/76_class_nonlocal5.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__.py b/crates/ty_python_semantic/resources/corpus/77_class__class__.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__nested.py b/crates/ty_python_semantic/resources/corpus/77_class__class__nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__nested.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__nested.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__no_class.py b/crates/ty_python_semantic/resources/corpus/77_class__class__no_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__no_class.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__no_class.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals.py b/crates/ty_python_semantic/resources/corpus/77_class__class__nonlocals.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__nonlocals.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals_2.py b/crates/ty_python_semantic/resources/corpus/77_class__class__nonlocals_2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals_2.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__nonlocals_2.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__param.py b/crates/ty_python_semantic/resources/corpus/77_class__class__param.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__param.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__param.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__param_lambda.py b/crates/ty_python_semantic/resources/corpus/77_class__class__param_lambda.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__param_lambda.py rename to crates/ty_python_semantic/resources/corpus/77_class__class__param_lambda.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_body_cond.py b/crates/ty_python_semantic/resources/corpus/78_class_body_cond.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_body_cond.py rename to crates/ty_python_semantic/resources/corpus/78_class_body_cond.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_dec.py b/crates/ty_python_semantic/resources/corpus/78_class_dec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_dec.py rename to crates/ty_python_semantic/resources/corpus/78_class_dec.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_dec_member.py b/crates/ty_python_semantic/resources/corpus/78_class_dec_member.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_dec_member.py rename to crates/ty_python_semantic/resources/corpus/78_class_dec_member.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_dec_member_func.py b/crates/ty_python_semantic/resources/corpus/78_class_dec_member_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_dec_member_func.py rename to crates/ty_python_semantic/resources/corpus/78_class_dec_member_func.py diff --git a/crates/red_knot_project/resources/test/corpus/79_metaclass.py b/crates/ty_python_semantic/resources/corpus/79_metaclass.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/79_metaclass.py rename to crates/ty_python_semantic/resources/corpus/79_metaclass.py diff --git a/crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs1.py b/crates/ty_python_semantic/resources/corpus/80_func_kwonlyargs1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs1.py rename to crates/ty_python_semantic/resources/corpus/80_func_kwonlyargs1.py diff --git a/crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs2.py b/crates/ty_python_semantic/resources/corpus/80_func_kwonlyargs2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs2.py rename to crates/ty_python_semantic/resources/corpus/80_func_kwonlyargs2.py diff --git a/crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs3.py b/crates/ty_python_semantic/resources/corpus/80_func_kwonlyargs3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs3.py rename to crates/ty_python_semantic/resources/corpus/80_func_kwonlyargs3.py diff --git a/crates/red_knot_project/resources/test/corpus/81_func_kwonlyargs_defaults.py b/crates/ty_python_semantic/resources/corpus/81_func_kwonlyargs_defaults.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/81_func_kwonlyargs_defaults.py rename to crates/ty_python_semantic/resources/corpus/81_func_kwonlyargs_defaults.py diff --git a/crates/ty_python_semantic/resources/corpus/83_jupyter_notebook_ipython_magic.ipynb b/crates/ty_python_semantic/resources/corpus/83_jupyter_notebook_ipython_magic.ipynb new file mode 120000 index 0000000000000..a323f576562dd --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/83_jupyter_notebook_ipython_magic.ipynb @@ -0,0 +1 @@ +../../../ruff_notebook/resources/test/fixtures/jupyter/unused_variable.ipynb \ No newline at end of file diff --git a/crates/red_knot_project/resources/test/corpus/85_match.py b/crates/ty_python_semantic/resources/corpus/85_match.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match.py rename to crates/ty_python_semantic/resources/corpus/85_match.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_as.py b/crates/ty_python_semantic/resources/corpus/85_match_as.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_as.py rename to crates/ty_python_semantic/resources/corpus/85_match_as.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_attr.py b/crates/ty_python_semantic/resources/corpus/85_match_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_attr.py rename to crates/ty_python_semantic/resources/corpus/85_match_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_class.py b/crates/ty_python_semantic/resources/corpus/85_match_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_class.py rename to crates/ty_python_semantic/resources/corpus/85_match_class.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_default.py b/crates/ty_python_semantic/resources/corpus/85_match_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_default.py rename to crates/ty_python_semantic/resources/corpus/85_match_default.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_guard.py b/crates/ty_python_semantic/resources/corpus/85_match_guard.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_guard.py rename to crates/ty_python_semantic/resources/corpus/85_match_guard.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_guard_with_named_expr.py b/crates/ty_python_semantic/resources/corpus/85_match_guard_with_named_expr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_guard_with_named_expr.py rename to crates/ty_python_semantic/resources/corpus/85_match_guard_with_named_expr.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_in_func.py b/crates/ty_python_semantic/resources/corpus/85_match_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_in_func.py rename to crates/ty_python_semantic/resources/corpus/85_match_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_in_func_with_rest.py b/crates/ty_python_semantic/resources/corpus/85_match_in_func_with_rest.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_in_func_with_rest.py rename to crates/ty_python_semantic/resources/corpus/85_match_in_func_with_rest.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_in_func_with_star.py b/crates/ty_python_semantic/resources/corpus/85_match_in_func_with_star.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_in_func_with_star.py rename to crates/ty_python_semantic/resources/corpus/85_match_in_func_with_star.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_invalid.py b/crates/ty_python_semantic/resources/corpus/85_match_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_invalid.py rename to crates/ty_python_semantic/resources/corpus/85_match_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_mapping.py b/crates/ty_python_semantic/resources/corpus/85_match_mapping.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_mapping.py rename to crates/ty_python_semantic/resources/corpus/85_match_mapping.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_mapping_subpattern.py b/crates/ty_python_semantic/resources/corpus/85_match_mapping_subpattern.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_mapping_subpattern.py rename to crates/ty_python_semantic/resources/corpus/85_match_mapping_subpattern.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_or.py b/crates/ty_python_semantic/resources/corpus/85_match_or.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_or.py rename to crates/ty_python_semantic/resources/corpus/85_match_or.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_sequence.py b/crates/ty_python_semantic/resources/corpus/85_match_sequence.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_sequence.py rename to crates/ty_python_semantic/resources/corpus/85_match_sequence.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_sequence_wildcard.py b/crates/ty_python_semantic/resources/corpus/85_match_sequence_wildcard.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_sequence_wildcard.py rename to crates/ty_python_semantic/resources/corpus/85_match_sequence_wildcard.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_singleton.py b/crates/ty_python_semantic/resources/corpus/85_match_singleton.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_singleton.py rename to crates/ty_python_semantic/resources/corpus/85_match_singleton.py diff --git a/crates/ty_python_semantic/resources/corpus/88_regression_generic_method_with_nested_function.py b/crates/ty_python_semantic/resources/corpus/88_regression_generic_method_with_nested_function.py new file mode 100644 index 0000000000000..055f2c1721f49 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/88_regression_generic_method_with_nested_function.py @@ -0,0 +1,9 @@ +# Regression test for an issue that came up while working +# on https://github.com/astral-sh/ruff/pull/17769 + +class C: + def method[T](self, x: T) -> T: + def inner(): + self.attr = 1 + +C().attr diff --git a/crates/ty_python_semantic/resources/corpus/88_regression_issue_17792.py b/crates/ty_python_semantic/resources/corpus/88_regression_issue_17792.py new file mode 100644 index 0000000000000..c9977a8397bb2 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/88_regression_issue_17792.py @@ -0,0 +1,15 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/17792 + +from __future__ import annotations + + +class C: ... + + +def f(arg: C): + pass + + +x, _ = f(1) + +assert x diff --git a/crates/ty_python_semantic/resources/corpus/88_regression_issue_738.py b/crates/ty_python_semantic/resources/corpus/88_regression_issue_738.py new file mode 100644 index 0000000000000..0e13bbf1720ed --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/88_regression_issue_738.py @@ -0,0 +1 @@ +[. diff --git a/crates/red_knot_project/resources/test/corpus/88_regression_tuple_type_short_circuit.py b/crates/ty_python_semantic/resources/corpus/88_regression_tuple_type_short_circuit.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/88_regression_tuple_type_short_circuit.py rename to crates/ty_python_semantic/resources/corpus/88_regression_tuple_type_short_circuit.py diff --git a/crates/red_knot_project/resources/test/corpus/89_type_alias.py b/crates/ty_python_semantic/resources/corpus/89_type_alias.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/89_type_alias.py rename to crates/ty_python_semantic/resources/corpus/89_type_alias.py diff --git a/crates/red_knot_project/resources/test/corpus/90_docstring_class.py b/crates/ty_python_semantic/resources/corpus/90_docstring_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/90_docstring_class.py rename to crates/ty_python_semantic/resources/corpus/90_docstring_class.py diff --git a/crates/red_knot_project/resources/test/corpus/90_docstring_func.py b/crates/ty_python_semantic/resources/corpus/90_docstring_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/90_docstring_func.py rename to crates/ty_python_semantic/resources/corpus/90_docstring_func.py diff --git a/crates/red_knot_project/resources/test/corpus/90_docstring_mod.py b/crates/ty_python_semantic/resources/corpus/90_docstring_mod.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/90_docstring_mod.py rename to crates/ty_python_semantic/resources/corpus/90_docstring_mod.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers1.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers1.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers1.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers2.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers2.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers2.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers2_comp.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers2_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers2_comp.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers2_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers3.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers3.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers3.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers4.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers4.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers4.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers4.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers_dict.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers_dict.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers_dict.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers_dict.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers_dict_comp.py b/crates/ty_python_semantic/resources/corpus/91_line_numbers_dict_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers_dict_comp.py rename to crates/ty_python_semantic/resources/corpus/91_line_numbers_dict_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/92_qual_class_in_class.py b/crates/ty_python_semantic/resources/corpus/92_qual_class_in_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/92_qual_class_in_class.py rename to crates/ty_python_semantic/resources/corpus/92_qual_class_in_class.py diff --git a/crates/red_knot_project/resources/test/corpus/92_qual_class_in_func.py b/crates/ty_python_semantic/resources/corpus/92_qual_class_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/92_qual_class_in_func.py rename to crates/ty_python_semantic/resources/corpus/92_qual_class_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/93_deadcode.py b/crates/ty_python_semantic/resources/corpus/93_deadcode.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/93_deadcode.py rename to crates/ty_python_semantic/resources/corpus/93_deadcode.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat.py b/crates/ty_python_semantic/resources/corpus/94_strformat.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat.py rename to crates/ty_python_semantic/resources/corpus/94_strformat.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_complex.py b/crates/ty_python_semantic/resources/corpus/94_strformat_complex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_complex.py rename to crates/ty_python_semantic/resources/corpus/94_strformat_complex.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_conv.py b/crates/ty_python_semantic/resources/corpus/94_strformat_conv.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_conv.py rename to crates/ty_python_semantic/resources/corpus/94_strformat_conv.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_conversion.py b/crates/ty_python_semantic/resources/corpus/94_strformat_conversion.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_conversion.py rename to crates/ty_python_semantic/resources/corpus/94_strformat_conversion.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_spec.py b/crates/ty_python_semantic/resources/corpus/94_strformat_spec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_spec.py rename to crates/ty_python_semantic/resources/corpus/94_strformat_spec.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py b/crates/ty_python_semantic/resources/corpus/95_annotation_assign_subscript_no_rhs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_assign_subscript_no_rhs.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_assign_tuple.py b/crates/ty_python_semantic/resources/corpus/95_annotation_assign_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_assign_tuple.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_assign_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_class.py b/crates/ty_python_semantic/resources/corpus/95_annotation_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_class.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_class.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_class_multiline.py b/crates/ty_python_semantic/resources/corpus/95_annotation_class_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_class_multiline.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_class_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_class_no_value.py b/crates/ty_python_semantic/resources/corpus/95_annotation_class_no_value.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_class_no_value.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_class_no_value.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_fstring_invalid.py b/crates/ty_python_semantic/resources/corpus/95_annotation_fstring_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_fstring_invalid.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_fstring_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_func.py b/crates/ty_python_semantic/resources/corpus/95_annotation_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_func.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_func.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_func_future.py b/crates/ty_python_semantic/resources/corpus/95_annotation_func_future.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_func_future.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_func_future.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_global.py b/crates/ty_python_semantic/resources/corpus/95_annotation_global.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_global.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_global.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_global_simple.py b/crates/ty_python_semantic/resources/corpus/95_annotation_global_simple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_global_simple.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_global_simple.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_local_attr.py b/crates/ty_python_semantic/resources/corpus/95_annotation_local_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_local_attr.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_local_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_module.py b/crates/ty_python_semantic/resources/corpus/95_annotation_module.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_module.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_module.py diff --git a/crates/ty_python_semantic/resources/corpus/95_annotation_string_tuple.py b/crates/ty_python_semantic/resources/corpus/95_annotation_string_tuple.py new file mode 100644 index 0000000000000..84681b729e3b1 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/95_annotation_string_tuple.py @@ -0,0 +1 @@ +t: "tuple[list[int]]" diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_union.py b/crates/ty_python_semantic/resources/corpus/95_annotation_union.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_union.py rename to crates/ty_python_semantic/resources/corpus/95_annotation_union.py diff --git a/crates/red_knot_project/resources/test/corpus/96_debug.py b/crates/ty_python_semantic/resources/corpus/96_debug.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/96_debug.py rename to crates/ty_python_semantic/resources/corpus/96_debug.py diff --git a/crates/red_knot_project/resources/test/corpus/97_global_nonlocal_store.py b/crates/ty_python_semantic/resources/corpus/97_global_nonlocal_store.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/97_global_nonlocal_store.py rename to crates/ty_python_semantic/resources/corpus/97_global_nonlocal_store.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_future_annotations.py b/crates/ty_python_semantic/resources/corpus/98_ann_assign_annotation_future_annotations.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_future_annotations.py rename to crates/ty_python_semantic/resources/corpus/98_ann_assign_annotation_future_annotations.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_wrong_future.py b/crates/ty_python_semantic/resources/corpus/98_ann_assign_annotation_wrong_future.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_wrong_future.py rename to crates/ty_python_semantic/resources/corpus/98_ann_assign_annotation_wrong_future.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_simple_annotation.py b/crates/ty_python_semantic/resources/corpus/98_ann_assign_simple_annotation.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_simple_annotation.py rename to crates/ty_python_semantic/resources/corpus/98_ann_assign_simple_annotation.py diff --git a/crates/red_knot_project/resources/test/corpus/99_empty_jump_target_insts.py b/crates/ty_python_semantic/resources/corpus/99_empty_jump_target_insts.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/99_empty_jump_target_insts.py rename to crates/ty_python_semantic/resources/corpus/99_empty_jump_target_insts.py diff --git a/crates/ty_python_semantic/resources/corpus/callable_with_concatenate.py b/crates/ty_python_semantic/resources/corpus/callable_with_concatenate.py new file mode 100644 index 0000000000000..19e984edbbe39 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/callable_with_concatenate.py @@ -0,0 +1,6 @@ +from typing_extensions import TypeVar, Callable, Concatenate, ParamSpec + +_T = TypeVar("_T") +_P = ParamSpec("_P") + +def f(self, callable: Callable[Concatenate[_T, _P], _T]) -> Callable[_P, _T]: ... diff --git a/crates/red_knot_project/resources/test/corpus/cycle_narrowing_constraints.py b/crates/ty_python_semantic/resources/corpus/cycle_narrowing_constraints.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/cycle_narrowing_constraints.py rename to crates/ty_python_semantic/resources/corpus/cycle_narrowing_constraints.py diff --git a/crates/red_knot_project/resources/test/corpus/cycle_negative_narrowing_constraints.py b/crates/ty_python_semantic/resources/corpus/cycle_negative_narrowing_constraints.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/cycle_negative_narrowing_constraints.py rename to crates/ty_python_semantic/resources/corpus/cycle_negative_narrowing_constraints.py diff --git a/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py b/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py new file mode 100644 index 0000000000000..a951d5034fc31 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/except_handler_with_Any_bound_typevar.py @@ -0,0 +1,14 @@ +def name_1[name_0: name_0](name_2: name_0): + try: + pass + except name_2: + pass + +from typing import Any + +def name_2[T: Any](x: T): + try: + pass + except x: + pass + diff --git a/crates/ty_python_semantic/resources/corpus/literal_slices.py b/crates/ty_python_semantic/resources/corpus/literal_slices.py new file mode 100644 index 0000000000000..b1b8ae6fe5bd6 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/literal_slices.py @@ -0,0 +1,6 @@ +from typing import Literal + +class Format: + STRING = "string" + +def evaluate(a: Literal[Format.STRING], b: Literal[-1]) -> str: ... diff --git a/crates/ty_python_semantic/resources/corpus/self_referential_function_annotation.py b/crates/ty_python_semantic/resources/corpus/self_referential_function_annotation.py new file mode 100644 index 0000000000000..5bbb1beb11557 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/self_referential_function_annotation.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +def foo(a: foo()): + pass diff --git a/crates/ty_python_semantic/resources/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py b/crates/ty_python_semantic/resources/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py new file mode 100644 index 0000000000000..7dc644bec95f2 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/sub_exprs_not_found_in_evaluate_expr_compare.py @@ -0,0 +1,11 @@ +# This is a regression test for `infer_expression_types`. +# ref: https://github.com/astral-sh/ruff/pull/18041#discussion_r2094573989 + +class C: + def f(self, other: "C"): + if self.a > other.b or self.b: + return False + if self: + return True + +C().a diff --git a/crates/ty_python_semantic/resources/corpus/ty_extensions.py b/crates/ty_python_semantic/resources/corpus/ty_extensions.py new file mode 100644 index 0000000000000..64810a630764f --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/ty_extensions.py @@ -0,0 +1,30 @@ +""" +Make sure that types are inferred for all subexpressions of the following +annotations involving ty_extension `_SpecialForm`s. + +This is a regression test for https://github.com/astral-sh/ty/issues/366 +""" + +from ty_extensions import CallableTypeOf, Intersection, Not, TypeOf + + +class A: ... + + +class B: ... + + +def _(x: Not[A]): + pass + + +def _(x: Intersection[A], y: Intersection[A, B]): + pass + + +def _(x: TypeOf[1j]): + pass + + +def _(x: CallableTypeOf[str]): + pass diff --git a/crates/red_knot_python_semantic/resources/mdtest/.mdformat.toml b/crates/ty_python_semantic/resources/mdtest/.mdformat.toml similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/.mdformat.toml rename to crates/ty_python_semantic/resources/mdtest/.mdformat.toml diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md rename to crates/ty_python_semantic/resources/mdtest/annotations/annotated.md index 3fb2cc3138264..893fb3c6372dd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md @@ -47,7 +47,7 @@ def _(flag: bool): def _(x: Annotated | bool): reveal_type(x) # revealed: Unknown | bool -# error: [invalid-type-form] +# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)" def _(x: Annotated[()]): reveal_type(x) # revealed: Unknown @@ -76,7 +76,7 @@ from typing_extensions import Annotated class C(Annotated[int, "foo"]): ... # TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]` -reveal_type(C.__mro__) # revealed: tuple[Literal[C], @Todo(Inference of subscript on special form), Literal[object]] +reveal_type(C.__mro__) # revealed: tuple[, @Todo(Inference of subscript on special form), ] ``` ### Not parameterized @@ -88,5 +88,5 @@ from typing_extensions import Annotated # error: [invalid-base] class C(Annotated): ... -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]] +reveal_type(C.__mro__) # revealed: tuple[, Unknown, ] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md new file mode 100644 index 0000000000000..d4b1e6f502224 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md @@ -0,0 +1,150 @@ +# Any + +## Annotation + +`typing.Any` is a way to name the Any type. + +```py +from typing import Any + +x: Any = 1 +x = "foo" + +def f(): + reveal_type(x) # revealed: Any +``` + +## Aliased to a different name + +If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type. + +```py +from typing import Any as RenamedAny + +x: RenamedAny = 1 +x = "foo" + +def f(): + reveal_type(x) # revealed: Any +``` + +## Shadowed class + +If you define your own class named `Any`, using that in a type expression refers to your class, and +isn't a spelling of the Any type. + +```py +class Any: ... + +x: Any + +def f(): + reveal_type(x) # revealed: Any + +# This verifies that we're not accidentally seeing typing.Any, since str is assignable +# to that but not to our locally defined class. +y: Any = "not an Any" # error: [invalid-assignment] +``` + +## Subclasses of `Any` + +The spec allows you to define subclasses of `Any`. + +`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be +allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since +`Subclass` might have `int` as a superclass, and is therefore assignable to `int`. + +```py +from typing import Any + +class SubclassOfAny(Any): ... + +reveal_type(SubclassOfAny.__mro__) # revealed: tuple[, Any, ] + +x: SubclassOfAny = 1 # error: [invalid-assignment] +y: int = SubclassOfAny() +``` + +`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not +possibly be a subclass of `FinalClass`: + +```py +from typing import final + +@final +class FinalClass: ... + +f: FinalClass = SubclassOfAny() # error: [invalid-assignment] + +@final +class OtherFinalClass: ... + +f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment] +``` + +A subclass of `Any` can also be assigned to arbitrary `Callable` and `Protocol` types: + +```py +from typing import Callable, Any, Protocol + +def takes_callable1(f: Callable): + f() + +takes_callable1(SubclassOfAny()) + +def takes_callable2(f: Callable[[int], None]): + f(1) + +takes_callable2(SubclassOfAny()) + +class CallbackProtocol(Protocol): + def __call__(self, x: int, /) -> None: ... + +def takes_callback_proto(f: CallbackProtocol): + f(1) + +takes_callback_proto(SubclassOfAny()) + +class OtherProtocol(Protocol): + x: int + @property + def foo(self) -> bytes: ... + @foo.setter + def foo(self, x: str) -> None: ... + +def takes_other_protocol(f: OtherProtocol): ... + +takes_other_protocol(SubclassOfAny()) +``` + +A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed: + +```py +from typing import Any, Literal + +class MockAny(Any): + pass + +x: Literal[1] = MockAny() # error: [invalid-assignment] +``` + +A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should +be assignable to (almost) any type: + +```py +from unittest.mock import MagicMock + +x: int = MagicMock() +``` + +## Invalid + +`Any` cannot be parameterized: + +```py +from typing import Any + +# error: [invalid-type-form] "Type `typing.Any` expected no type parameter" +def f(x: Any[int]): + reveal_type(x) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md new file mode 100644 index 0000000000000..688dcb321ad15 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -0,0 +1,393 @@ +# Callable + +References: + +- + +Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see: +). 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 + +The `Callable` special form requires _exactly_ two arguments where the first argument is either a +parameter type list, parameter specification, `typing.Concatenate`, or `...` and the second argument +is the return type. Here, we explore various invalid forms. + +### Empty + +A bare `Callable` without any type arguments: + +```py +from typing import Callable + +def _(c: Callable): + reveal_type(c) # revealed: (...) -> Unknown +``` + +### Invalid parameter type argument + +When it's not a list: + +```py +from typing import Callable + +# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +def _(c: Callable[int, str]): + reveal_type(c) # revealed: (...) -> Unknown +``` + +Or, when it's a literal type: + +```py +# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" +def _(c: Callable[42, str]): + reveal_type(c) # revealed: (...) -> Unknown +``` + +Or, when one of the parameter type is invalid in the list: + +```py +# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" +# error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" +def _(c: Callable[[int, 42, str, False], None]): + # revealed: (int, Unknown, str, Unknown, /) -> None + reveal_type(c) +``` + +### Missing return type + + + +Using a parameter list: + +```py +from typing import Callable + +# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" +def _(c: Callable[[int, str]]): + reveal_type(c) # revealed: (...) -> Unknown +``` + +Or, an ellipsis: + +```py +# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" +def _(c: Callable[...]): + reveal_type(c) # revealed: (...) -> Unknown +``` + +Or something else that's invalid in a type expression generally: + +```py +# fmt: off + +def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" + {1, 2} # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" + ] + ): + reveal_type(c) # revealed: (...) -> Unknown +``` + +### More than two arguments + +We can't reliably infer the callable type if there are more then 2 arguments because we don't know +which argument corresponds to either the parameters or the return type. + +```py +from typing import Callable + +# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" +def _(c: Callable[[int], str, str]): + reveal_type(c) # revealed: (...) -> Unknown +``` + +### List as the second argument + +```py +from typing import Callable + +# fmt: off + +def _(c: Callable[ + int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" + [str] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" + ] + ): + reveal_type(c) # revealed: (...) -> Unknown +``` + +### Tuple as the second argument + +```py +from typing import Callable + +# fmt: off + +def _(c: Callable[ + int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`" + (str, ) # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression" + ] + ): + reveal_type(c) # revealed: (...) -> Unknown +``` + +### List as both arguments + +```py +from typing import Callable + +# error: [invalid-type-form] "List literals are not allowed in this context in a type expression" +def _(c: Callable[[int], [str]]): + reveal_type(c) # revealed: (int, /) -> Unknown +``` + +### Three list arguments + +```py +from typing import Callable + +# fmt: off + + +def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" + [int], + [str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" + [bytes] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression" + ] + ): + reveal_type(c) # revealed: (...) -> Unknown +``` + +## Simple + +A simple `Callable` with multiple parameters and a return type: + +```py +from typing import Callable + +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 ty_extensions import Intersection, Not + +class Foo: ... + +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], Foo], + f: Intersection[Not[Callable[[int, str], Intersection[int, Foo]]]], +): + reveal_type(c) # revealed: ((int | str, /) -> int) & int + reveal_type(d) # revealed: int & ((int | str, /) -> int) + reveal_type(e) # revealed: int & ((int | str, /) -> int) & Foo + reveal_type(f) # revealed: ~((int, str, /) -> int & Foo) +``` + +## Nested + +A nested `Callable` as one of the parameter types: + +```py +from typing import Callable + +def _(c: Callable[[Callable[[int], str]], int]): + reveal_type(c) # revealed: ((int, /) -> str, /) -> int +``` + +And, as the return type: + +```py +def _(c: Callable[[int, str], Callable[[int], int]]): + reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int +``` + +## Gradual form + +The `Callable` special form supports the use of `...` in place of the list of parameter types. This +is a [gradual form] indicating that the type is consistent with any input signature: + +```py +from typing import Callable + +def gradual_form(c: Callable[..., str]): + reveal_type(c) # revealed: (...) -> str +``` + +## Using `typing.Concatenate` + +Using `Concatenate` as the first argument to `Callable`: + +```py +from typing_extensions import Callable, Concatenate + +def _(c: Callable[Concatenate[int, str, ...], int]): + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int +``` + +And, as one of the parameter types: + +```py +def _(c: Callable[[Concatenate[int, str, ...], int], int]): + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int +``` + +Other type expressions can be nested inside `Concatenate`: + +```py +def _(c: Callable[[Concatenate[int | str, type[str], ...], int], int]): + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int +``` + +But providing fewer than 2 arguments to `Concatenate` is an error: + +```py +# fmt: off + +def _( + c: Callable[Concatenate[int], int], # error: [invalid-type-form] "Special form `typing.Concatenate` expected at least 2 parameters but got 1" + d: Callable[Concatenate[(int,)], int], # error: [invalid-type-form] "Special form `typing.Concatenate` expected at least 2 parameters but got 1" + e: Callable[Concatenate[()], int] # error: [invalid-type-form] "Special form `typing.Concatenate` expected at least 2 parameters but got 0" +): + reveal_type(c) # revealed: (...) -> int + reveal_type(d) # revealed: (...) -> int + reveal_type(e) # revealed: (...) -> int + +# fmt: on +``` + +## Using `typing.ParamSpec` + +```toml +[environment] +python-version = "3.12" +``` + +Using a `ParamSpec` in a `Callable` annotation: + +```py +from typing_extensions import Callable + +def _[**P1](c: Callable[P1, int]): + reveal_type(P1.args) # revealed: @Todo(ParamSpec) + reveal_type(P1.kwargs) # revealed: @Todo(ParamSpec) + + # TODO: Signature should be (**P1) -> int + reveal_type(c) # revealed: (...) -> int +``` + +And, using the legacy syntax: + +```py +from typing_extensions import ParamSpec + +P2 = ParamSpec("P2") + +# TODO: argument list should not be `...` (requires `ParamSpec` support) +def _(c: Callable[P2, int]): + reveal_type(c) # revealed: (...) -> int +``` + +## Using `typing.Unpack` + +Using the unpack operator (`*`): + +```py +from typing_extensions import Callable, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +def _(c: Callable[[int, *Ts], int]): + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int +``` + +And, using the legacy syntax using `Unpack`: + +```py +from typing_extensions import Unpack + +def _(c: Callable[[int, Unpack[Ts]], int]): + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int +``` + +## Member lookup + +```py +from typing import Callable + +def _(c: Callable[[int], int]): + reveal_type(c.__init__) # revealed: bound method object.__init__() -> None + reveal_type(c.__class__) # revealed: type + reveal_type(c.__call__) # revealed: (int, /) -> int +``` + +Unlike other type checkers, we do _not_ allow attributes to be accessed that would only be available +on function-like callables: + +```py +def f_wrong(c: Callable[[], None]): + # error: [unresolved-attribute] "Type `() -> None` has no attribute `__qualname__`" + c.__qualname__ + + # error: [unresolved-attribute] "Unresolved attribute `__qualname__` on type `() -> None`." + c.__qualname__ = "my_callable" +``` + +We do this, because at runtime, calls to `f_wrong` with a non-function callable would raise an +`AttributeError`: + +```py +class MyCallable: + def __call__(self) -> None: + pass + +f_wrong(MyCallable()) # raises `AttributeError` at runtime +``` + +If users want to read/write to attributes such as `__qualname__`, they need to check the existence +of the attribute first: + +```py +from inspect import getattr_static + +def f_okay(c: Callable[[], None]): + if hasattr(c, "__qualname__"): + c.__qualname__ # okay + # `hasattr` only guarantees that an attribute is readable. + # error: [invalid-assignment] "Object of type `Literal["my_callable"]` is not assignable to attribute `__qualname__` on type `(() -> None) & `" + c.__qualname__ = "my_callable" + + result = getattr_static(c, "__qualname__") + reveal_type(result) # revealed: Never + if isinstance(result, property) and result.fset: + c.__qualname__ = "my_callable" # okay +``` + +[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md rename to crates/ty_python_semantic/resources/mdtest/annotations/deferred.md index 7f44fd7f76c5f..8db8d9040920a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md @@ -48,6 +48,11 @@ reveal_type(get_foo()) # revealed: Foo ## Deferred self-reference annotations in a class definition +```toml +[environment] +python-version = "3.12" +``` + ```py from __future__ import annotations @@ -94,6 +99,11 @@ class Foo: ## Non-deferred self-reference annotations in a class definition +```toml +[environment] +python-version = "3.12" +``` + ```py class Foo: # error: [unresolved-reference] @@ -146,3 +156,24 @@ def _(): def f(self) -> C: return self ``` + +## Base class references + +### Not deferred by __future__.annotations + +```py +from __future__ import annotations + +class A(B): # error: [unresolved-reference] + pass + +class B: + pass +``` + +### Deferred in stub files + +```pyi +class A(B): ... +class B: ... +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md similarity index 77% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md rename to crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md index e32bb5d108d21..fada88bd9b4e3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md @@ -88,3 +88,26 @@ def assigns_complex(x: complex): def f(x: complex): reveal_type(x) # revealed: int | float | complex ``` + +## Narrowing + +`int`, `float` and `complex` are all disjoint, which means that the union `int | float` can easily +be narrowed to `int` or `float`: + +```py +from typing_extensions import assert_type +from ty_extensions import JustFloat + +def f(x: complex): + reveal_type(x) # revealed: int | float | complex + + if isinstance(x, int): + reveal_type(x) # revealed: int + elif isinstance(x, float): + reveal_type(x) # revealed: float + else: + reveal_type(x) # revealed: complex + + assert isinstance(x, float) + assert_type(x, JustFloat) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md new file mode 100644 index 0000000000000..9dca3893b7bf8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -0,0 +1,240 @@ +# Tests for invalid types in type expressions + +## Invalid types are rejected + +Many types are illegal in the context of a type expression: + +```py +import typing +from ty_extensions import AlwaysTruthy, AlwaysFalsy +from typing_extensions import Literal, Never + +class A: ... + +def _( + a: type[int], + b: AlwaysTruthy, + c: AlwaysFalsy, + d: Literal[True], + e: Literal["bar"], + f: Literal[b"foo"], + g: tuple[int, str], + h: Never, + i: int, + j: A, +): + def foo(): ... + def invalid( + a_: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression" + b_: b, # error: [invalid-type-form] + c_: c, # error: [invalid-type-form] + d_: d, # error: [invalid-type-form] + e_: e, # error: [invalid-type-form] + f_: f, # error: [invalid-type-form] + g_: g, # error: [invalid-type-form] + h_: h, # error: [invalid-type-form] + i_: typing, # error: [invalid-type-form] + j_: foo, # error: [invalid-type-form] + k_: i, # error: [invalid-type-form] "Variable of type `int` is not allowed in a type expression" + l_: j, # error: [invalid-type-form] "Variable of type `A` is not allowed in a type expression" + ): + reveal_type(a_) # revealed: Unknown + reveal_type(b_) # revealed: Unknown + reveal_type(c_) # revealed: Unknown + reveal_type(d_) # revealed: Unknown + reveal_type(e_) # revealed: Unknown + reveal_type(f_) # revealed: Unknown + reveal_type(g_) # revealed: Unknown + reveal_type(h_) # revealed: Unknown + reveal_type(i_) # revealed: Unknown + reveal_type(j_) # revealed: Unknown +``` + +## Invalid AST nodes + +```py +def bar() -> None: + return None + +async def outer(): # avoid unrelated syntax errors on yield, yield from, and await + def _( + a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" + b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions" + c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions" + d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" + e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" + g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" + h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions" + i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions" + j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions" + k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions" + l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions" + m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" + n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions" + o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions" + p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions" + q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions" + r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions" + ): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: Unknown + reveal_type(e) # revealed: int | Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: Unknown + reveal_type(i) # revealed: Unknown + reveal_type(j) # revealed: Unknown + reveal_type(k) # revealed: Unknown + reveal_type(p) # revealed: Unknown + reveal_type(q) # revealed: int | Unknown + reveal_type(r) # revealed: @Todo(unknown type subscript) + +class Mat: + def __init__(self, value: int): + self.value = value + + def __matmul__(self, other) -> int: + return 42 + +def invalid_binary_operators( + a: "1" + "2", # error: [invalid-type-form] "Invalid binary operator `+` in type annotation" + b: 3 - 5.0, # error: [invalid-type-form] "Invalid binary operator `-` in type annotation" + c: 4 * -2, # error: [invalid-type-form] "Invalid binary operator `*` in type annotation" + d: Mat(4) @ Mat(2), # error: [invalid-type-form] "Invalid binary operator `@` in type annotation" + e: 10 / 2, # error: [invalid-type-form] "Invalid binary operator `/` in type annotation" + f: 10 % 3, # error: [invalid-type-form] "Invalid binary operator `%` in type annotation" + g: 2**-0.5, # error: [invalid-type-form] "Invalid binary operator `**` in type annotation" + h: 10 // 3, # error: [invalid-type-form] "Invalid binary operator `//` in type annotation" + i: 1 << 2, # error: [invalid-type-form] "Invalid binary operator `<<` in type annotation" + j: 4 >> 42, # error: [invalid-type-form] "Invalid binary operator `>>` in type annotation" + k: 5 ^ 3, # error: [invalid-type-form] "Invalid binary operator `^` in type annotation" + l: 5 & 3, # error: [invalid-type-form] "Invalid binary operator `&` in type annotation" +): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: Unknown + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: Unknown + reveal_type(i) # revealed: Unknown + reveal_type(j) # revealed: Unknown + reveal_type(k) # revealed: Unknown + reveal_type(l) # revealed: Unknown +``` + +## Invalid Collection based AST nodes + +```toml +[environment] +python-version = "3.12" +``` + +```py +def _( + a: {1: 2}, # error: [invalid-type-form] "Dict literals are not allowed in type expressions" + b: {1, 2}, # error: [invalid-type-form] "Set literals are not allowed in type expressions" + c: {k: v for k, v in [(1, 2)]}, # error: [invalid-type-form] "Dict comprehensions are not allowed in type expressions" + d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions" + e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions" + f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions" + # error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?" + g: [int, str], + # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?" + h: (int, str), + i: (), # error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?" +): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: Unknown + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: Unknown + reveal_type(i) # revealed: Unknown + +# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?" +class name_0[name_2: [int]]: + pass + +# error: [invalid-type-form] "List literals are not allowed in this context in a type expression" +# error: [invalid-type-form] "Dict literals are not allowed in type expressions" +class name_4[name_1: [{}]]: + pass +``` + +## Diagnostics for common errors + + + +### Module-literal used when you meant to use a class from that module + +It's pretty common in Python to accidentally use a module-literal type in a type expression when you +*meant* to use a class by the same name that comes from that module. We emit a nice subdiagnostic +for this case: + +`foo.py`: + +```py +import datetime + +def f(x: datetime): ... # error: [invalid-type-form] +``` + +`PIL/Image.py`: + +```py +class Image: ... +``` + +`bar.py`: + +```py +from PIL import Image + +def g(x: Image): ... # error: [invalid-type-form] +``` + +### List-literal used when you meant to use a list or tuple + +```py +def _( + x: [int], # error: [invalid-type-form] +) -> [int]: # error: [invalid-type-form] + return x +``` + +```py +def _( + x: [int, str], # error: [invalid-type-form] +) -> [int, str]: # error: [invalid-type-form] + return x +``` + +### Tuple-literal used when you meant to use a tuple + +```py +def _( + x: (), # error: [invalid-type-form] +) -> (): # error: [invalid-type-form] + return x +``` + +```py +def _( + x: (int,), # error: [invalid-type-form] +) -> (int,): # error: [invalid-type-form] + return x +``` + +```py +def _( + x: (int, str), # error: [invalid-type-form] +) -> (int, str): # error: [invalid-type-form] + return x +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md rename to crates/ty_python_semantic/resources/mdtest/annotations/literal.md index 138478dbdf677..865bf073f1fdd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md @@ -137,7 +137,7 @@ from other import Literal a1: Literal[26] def f(): - reveal_type(a1) # revealed: @Todo(generics) + reveal_type(a1) # revealed: @Todo(unknown type subscript) ``` ## Detecting typing_extensions.Literal diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md rename to crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md index 45dff62b80510..0496dbb4ce402 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md @@ -38,8 +38,12 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form] ```py from typing_extensions import LiteralString -a: LiteralString[str] # error: [invalid-type-form] -b: LiteralString["foo"] # error: [invalid-type-form] +# error: [invalid-type-form] +a: LiteralString[str] + +# error: [invalid-type-form] +# error: [unresolved-reference] "Name `foo` used when not defined" +b: LiteralString["foo"] ``` ### As a base class @@ -72,13 +76,11 @@ reveal_type(baz) # revealed: Literal["bazfoo"] qux = (foo, bar) reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]] -# TODO: Infer "LiteralString" -reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function) +reveal_type(foo.join(qux)) # revealed: LiteralString template: LiteralString = "{}, {}" reveal_type(template) # revealed: Literal["{}, {}"] -# TODO: Infer `LiteralString` -reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function) +reveal_type(template.format(foo, bar)) # revealed: LiteralString ``` ### Assignability diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/never.md b/crates/ty_python_semantic/resources/mdtest/annotations/never.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/never.md rename to crates/ty_python_semantic/resources/mdtest/annotations/never.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md similarity index 75% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md rename to crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 0c5142ef70d8b..5dc14964ccb73 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -1,6 +1,6 @@ # NewType -Currently, red-knot doesn't support `typing.NewType` in type annotations. +Currently, ty doesn't support `typing.NewType` in type annotations. ## Valid forms @@ -12,7 +12,7 @@ X = GenericAlias(type, ()) A = NewType("A", int) # TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased # to be compatible with `type` -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `type`, found `NewType`" +# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `NewType`" B = GenericAlias(A, ()) def _( diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/optional.md b/crates/ty_python_semantic/resources/mdtest/annotations/optional.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/optional.md rename to crates/ty_python_semantic/resources/mdtest/annotations/optional.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md new file mode 100644 index 0000000000000..8a99fac64c860 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -0,0 +1,207 @@ +# Self + +`Self` is treated as if it were a `TypeVar` bound to the class it's being used on. + +`typing.Self` is only available in Python 3.11 and later. + +## Methods + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Self + +class Shape: + def set_scale(self: Self, scale: float) -> Self: + reveal_type(self) # revealed: Self + return self + + def nested_type(self: Self) -> list[Self]: + return [self] + + def nested_func(self: Self) -> Self: + def inner() -> Self: + reveal_type(self) # revealed: Self + return self + return inner() + + def implicit_self(self) -> Self: + # TODO: first argument in a method should be considered as "typing.Self" + reveal_type(self) # revealed: Unknown + return self + +reveal_type(Shape().nested_type()) # revealed: list[Shape] +reveal_type(Shape().nested_func()) # revealed: Shape + +class Circle(Shape): + def set_scale(self: Self, scale: float) -> Self: + reveal_type(self) # revealed: Self + return self + +class Outer: + class Inner: + def foo(self: Self) -> Self: + reveal_type(self) # revealed: Self + return self +``` + +## typing_extensions + +```toml +[environment] +python-version = "3.10" +``` + +```py +from typing_extensions import Self + +class C: + def method(self: Self) -> Self: + return self + +reveal_type(C().method()) # revealed: C +``` + +## Class Methods + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Self, TypeVar + +class Shape: + def foo(self: Self) -> Self: + return self + + @classmethod + def bar(cls: type[Self]) -> Self: + # TODO: type[Shape] + reveal_type(cls) # revealed: @Todo(unsupported type[X] special form) + return cls() + +class Circle(Shape): ... + +reveal_type(Shape().foo()) # revealed: Shape +# TODO: Shape +reveal_type(Shape.bar()) # revealed: Unknown +``` + +## Attributes + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Self + +class LinkedList: + value: int + next_node: Self + + def next(self: Self) -> Self: + reveal_type(self.value) # revealed: int + return self.next_node + +reveal_type(LinkedList().next()) # revealed: LinkedList +``` + +## Generic Classes + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Self, Generic, TypeVar + +T = TypeVar("T") + +class Container(Generic[T]): + value: T + def set_value(self: Self, value: T) -> Self: + return self + +int_container: Container[int] = Container[int]() +reveal_type(int_container) # revealed: Container[int] +reveal_type(int_container.set_value(1)) # revealed: Container[int] +``` + +## Protocols + +TODO: + +## Annotations + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Self + +class Shape: + def union(self: Self, other: Self | None): + reveal_type(other) # revealed: Self | None + return self +``` + +## Invalid Usage + +`Self` cannot be used in the signature of a function or variable. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Self, Generic, TypeVar + +T = TypeVar("T") + +# error: [invalid-type-form] +def x(s: Self): ... + +# error: [invalid-type-form] +b: Self + +# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self" +class Foo: + # TODO: rejected Self because self has a different type + def has_existing_self_annotation(self: T) -> Self: + return self # error: [invalid-return-type] + + def return_concrete_type(self) -> Self: + # TODO: tell user to use "Foo" instead of "Self" + # error: [invalid-return-type] + return Foo() + + @staticmethod + # TODO: reject because of staticmethod + def make() -> Self: + # error: [invalid-return-type] + return Foo() + +class Bar(Generic[T]): + foo: T + def bar(self) -> T: + return self.foo + +# error: [invalid-type-form] +class Baz(Bar[Self]): ... + +class MyMetaclass(type): + # TODO: rejected + def __new__(cls) -> Self: + return super().__new__(cls) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md new file mode 100644 index 0000000000000..3bb3de68bf7db --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -0,0 +1,22 @@ +# Starred expression annotations + +```toml +[environment] +python-version = "3.11" +``` + +Type annotations for `*args` can be starred expressions themselves: + +```py +from typing_extensions import TypeVarTuple + +Ts = TypeVarTuple("Ts") + +def append_int(*args: *Ts) -> tuple[*Ts, int]: + reveal_type(args) # revealed: @Todo(PEP 646) + + return (*args, 1) + +# TODO should be tuple[Literal[True], Literal["a"], int] +reveal_type(append_int(True, "a")) # revealed: @Todo(PEP 646) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md new file mode 100644 index 0000000000000..990fbe33fdbfd --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -0,0 +1,170 @@ +# Typing-module aliases to other stdlib classes + +The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but +still need to be supported by a type checker. + +## Correspondence + +All of the following symbols can be mapped one-to-one with the actual type: + +```py +import typing + +def f( + list_bare: typing.List, + list_parametrized: typing.List[int], + dict_bare: typing.Dict, + dict_parametrized: typing.Dict[int, str], + set_bare: typing.Set, + set_parametrized: typing.Set[int], + frozen_set_bare: typing.FrozenSet, + frozen_set_parametrized: typing.FrozenSet[str], + chain_map_bare: typing.ChainMap, + chain_map_parametrized: typing.ChainMap[str, int], + counter_bare: typing.Counter, + counter_parametrized: typing.Counter[int], + default_dict_bare: typing.DefaultDict, + default_dict_parametrized: typing.DefaultDict[str, int], + deque_bare: typing.Deque, + deque_parametrized: typing.Deque[str], + ordered_dict_bare: typing.OrderedDict, + ordered_dict_parametrized: typing.OrderedDict[int, str], +): + reveal_type(list_bare) # revealed: list[Unknown] + reveal_type(list_parametrized) # revealed: list[int] + + reveal_type(dict_bare) # revealed: dict[Unknown, Unknown] + reveal_type(dict_parametrized) # revealed: dict[int, str] + + reveal_type(set_bare) # revealed: set[Unknown] + reveal_type(set_parametrized) # revealed: set[int] + + reveal_type(frozen_set_bare) # revealed: frozenset[Unknown] + reveal_type(frozen_set_parametrized) # revealed: frozenset[str] + + reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown] + reveal_type(chain_map_parametrized) # revealed: ChainMap[str, int] + + reveal_type(counter_bare) # revealed: Counter[Unknown] + reveal_type(counter_parametrized) # revealed: Counter[int] + + reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown] + reveal_type(default_dict_parametrized) # revealed: defaultdict[str, int] + + reveal_type(deque_bare) # revealed: deque[Unknown] + reveal_type(deque_parametrized) # revealed: deque[str] + + reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown] + reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[int, str] +``` + +## Incorrect number of type arguments + +In case the incorrect number of type arguments is passed, a diagnostic is given. + +```py +import typing + +def f( + # error: [invalid-type-form] "Legacy alias `typing.List` expected exactly 1 argument, got 2" + incorrect_list: typing.List[int, int], + # error: [invalid-type-form] "Legacy alias `typing.Dict` expected exactly 2 arguments, got 3" + incorrect_dict: typing.Dict[int, int, int], + # error: [invalid-type-form] "Legacy alias `typing.Dict` expected exactly 2 arguments, got 1" + incorrect_dict2: typing.Dict[int], # type argument is not a tuple here + # error: [invalid-type-form] + incorrect_set: typing.Set[int, int], + # error: [invalid-type-form] + incorrect_frozen_set: typing.FrozenSet[int, int], + # error: [invalid-type-form] + incorrect_chain_map: typing.ChainMap[int, int, int], + # error: [invalid-type-form] + incorrect_chain_map2: typing.ChainMap[int], + # error: [invalid-type-form] + incorrect_counter: typing.Counter[int, int], + # error: [invalid-type-form] + incorrect_default_dict: typing.DefaultDict[int, int, int], + # error: [invalid-type-form] + incorrect_default_dict2: typing.DefaultDict[int], + # error: [invalid-type-form] + incorrect_deque: typing.Deque[int, int], + # error: [invalid-type-form] + incorrect_ordered_dict: typing.OrderedDict[int, int, int], + # error: [invalid-type-form] + incorrect_ordered_dict2: typing.OrderedDict[int], +): + reveal_type(incorrect_list) # revealed: list[Unknown] + reveal_type(incorrect_dict) # revealed: dict[Unknown, Unknown] + reveal_type(incorrect_dict2) # revealed: dict[Unknown, Unknown] + reveal_type(incorrect_set) # revealed: set[Unknown] + reveal_type(incorrect_frozen_set) # revealed: frozenset[Unknown] + reveal_type(incorrect_chain_map) # revealed: ChainMap[Unknown, Unknown] + reveal_type(incorrect_chain_map2) # revealed: ChainMap[Unknown, Unknown] + reveal_type(incorrect_counter) # revealed: Counter[Unknown] + reveal_type(incorrect_default_dict) # revealed: defaultdict[Unknown, Unknown] + reveal_type(incorrect_default_dict2) # revealed: defaultdict[Unknown, Unknown] + reveal_type(incorrect_deque) # revealed: deque[Unknown] + reveal_type(incorrect_ordered_dict) # revealed: OrderedDict[Unknown, Unknown] + reveal_type(incorrect_ordered_dict2) # revealed: OrderedDict[Unknown, Unknown] +``` + +## Inheritance + +The aliases can be inherited from. Some of these are still partially or wholly TODOs. + +```py +import typing + +#################### +### Built-ins +#################### + +class ListSubclass(typing.List): ... + +# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(ListSubclass.__mro__) + +class DictSubclass(typing.Dict): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(DictSubclass.__mro__) + +class SetSubclass(typing.Set): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(SetSubclass.__mro__) + +class FrozenSetSubclass(typing.FrozenSet): ... + +# revealed: tuple[, , , , , , typing.Protocol, typing.Generic, ] +reveal_type(FrozenSetSubclass.__mro__) + +#################### +### `collections` +#################### + +class ChainMapSubclass(typing.ChainMap): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(ChainMapSubclass.__mro__) + +class CounterSubclass(typing.Counter): ... + +# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(CounterSubclass.__mro__) + +class DefaultDictSubclass(typing.DefaultDict): ... + +# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(DefaultDictSubclass.__mro__) + +class DequeSubclass(typing.Deque): ... + +# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(DequeSubclass.__mro__) + +class OrderedDictSubclass(typing.OrderedDict): ... + +# revealed: tuple[, , , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(OrderedDictSubclass.__mro__) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md new file mode 100644 index 0000000000000..5777070441e37 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -0,0 +1,213 @@ +# String annotations + +## Simple + +```py +def f(v: "int"): + reveal_type(v) # revealed: int +``` + +## Nested + +```py +def f(v: "'int'"): + reveal_type(v) # revealed: int +``` + +## Type expression + +```py +def f1(v: "int | str", w: "tuple[int, str]"): + reveal_type(v) # revealed: int | str + reveal_type(w) # revealed: tuple[int, str] +``` + +## Partial + +```py +def f(v: tuple[int, "str"]): + reveal_type(v) # revealed: tuple[int, str] +``` + +## Deferred + +```py +def f(v: "Foo"): + reveal_type(v) # revealed: Foo + +class Foo: ... +``` + +## Deferred (undefined) + +```py +# error: [unresolved-reference] +def f(v: "Foo"): + reveal_type(v) # revealed: Unknown +``` + +## Partial deferred + +```py +def f(v: int | "Foo"): + reveal_type(v) # revealed: int | Foo + +class Foo: ... +``` + +## `typing.Literal` + +```py +from typing import Literal + +def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'): + reveal_type(v) # revealed: Literal["Foo", "Bar"] + reveal_type(w) # revealed: Literal["Foo", "Bar"] + +class Foo: ... +``` + +## Various string kinds + +```py +def f1( + # error: [raw-string-type-annotation] "Type expressions cannot use raw string literal" + a: r"int", + # error: [fstring-type-annotation] "Type expressions cannot use f-strings" + b: f"int", + # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal" + c: b"int", + d: "int", + # error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals" + e: "in" "t", + # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters" + f: "\N{LATIN SMALL LETTER I}nt", + # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters" + g: "\x69nt", + h: """int""", + # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal" + i: "b'int'", +): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: int + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: int + reveal_type(i) # revealed: Unknown +``` + +## Various string kinds in `typing.Literal` + +```py +from typing import Literal + +def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]): + reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"] +``` + +## Class variables + +```py +MyType = int + +class Aliases: + MyType = str + + forward: "MyType" = "value" + not_forward: MyType = "value" + +reveal_type(Aliases.forward) # revealed: str +reveal_type(Aliases.not_forward) # revealed: str +``` + +## Annotated assignment + +```py +a: "int" = 1 +b: "'int'" = 1 +c: "Foo" +# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`" +d: "Foo" = 1 + +class Foo: ... + +c = Foo() + +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[1] +reveal_type(c) # revealed: Foo +reveal_type(d) # revealed: Foo +``` + +## Parameter + +TODO: Add tests once parameter inference is supported + +## Invalid expressions + +The expressions in these string annotations aren't valid expressions in this context but we +shouldn't panic. + +```py +a: "1 or 2" +b: "(x := 1)" +# error: [invalid-type-form] +c: "1 + 2" +d: "lambda x: x" +e: "x if True else y" +f: "{'a': 1, 'b': 2}" +g: "{1, 2}" +h: "[i for i in range(5)]" +i: "{i for i in range(5)}" +j: "{i: i for i in range(5)}" +k: "(i for i in range(5))" +l: "await 1" +# error: [invalid-syntax-in-forward-annotation] +m: "yield 1" +# error: [invalid-syntax-in-forward-annotation] +n: "yield from 1" +o: "1 < 2" +p: "call()" +r: "[1, 2]" +s: "(1, 2)" +``` + +## Multi line annotation + +Quoted type annotations should be parsed as if surrounded by parentheses. + +```py +def valid( + a1: """( + int | + str + ) + """, + a2: """ + int | + str + """, +): + reveal_type(a1) # revealed: int | str + reveal_type(a2) # revealed: int | str + +def invalid( + # error: [invalid-syntax-in-forward-annotation] + a1: """ + int | +str) +""", + # error: [invalid-syntax-in-forward-annotation] + a2: """ + int) | +str +""", + # error: [invalid-syntax-in-forward-annotation] + a3: """ + (int)) """, +): + pass +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/union.md b/crates/ty_python_semantic/resources/mdtest/annotations/union.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/union.md rename to crates/ty_python_semantic/resources/mdtest/annotations/union.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md new file mode 100644 index 0000000000000..254ed90eea873 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -0,0 +1,97 @@ +# Unsupported special forms + +## Not yet supported + +Several special forms are unsupported by ty currently. However, we also don't emit false-positive +errors if you use one in an annotation: + +```py +from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar + +P = ParamSpec("P") +Ts = TypeVarTuple("Ts") +R_co = TypeVar("R_co", covariant=True) + +Alias: TypeAlias = int + +def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: + reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...] + reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`) + +def g() -> TypeGuard[int]: ... +def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: + reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...] + reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)] + return callback(42, *args, **kwargs) + +class Foo: + def method(self, x: Self): + reveal_type(x) # revealed: Self +``` + +## Type expressions + +One thing that is supported is error messages for using special forms in type expressions. + +```py +from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic + +def _( + a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression" + b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression" + c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression" + d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression" + e: ParamSpec, + f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions" +) -> None: + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: Unknown + + def foo(a_: e) -> None: + reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`) +``` + +## Inheritance + +You can't inherit from most of these. `typing.Callable` is an exception. + +```py +from typing import Callable +from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic + +class A(Self): ... # error: [invalid-base] +class B(Unpack): ... # error: [invalid-base] +class C(TypeGuard): ... # error: [invalid-base] +class D(TypeIs): ... # error: [invalid-base] +class E(Concatenate): ... # error: [invalid-base] +class F(Callable): ... +class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`" + +reveal_type(F.__mro__) # revealed: tuple[, @Todo(Support for Callable as a base class), ] +``` + +## Subscriptability + +```toml +[environment] +python-version = "3.12" +``` + +Some of these are not subscriptable: + +```py +from typing_extensions import Self, TypeAlias, TypeVar + +T = TypeVar("T") + +# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter" +X: TypeAlias[T] = int + +class Foo[T]: + # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" + # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" + def method(self: Self[int]) -> Self[int]: + reveal_type(self) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md new file mode 100644 index 0000000000000..dc22ac2539cb7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md @@ -0,0 +1,18 @@ +# Unsupported special types + +We do not understand the functional syntax for creating `NamedTuple`s, `TypedDict`s or `Enum`s yet. +But we also do not emit false positives when these are used in type expressions. + +```py +import collections +import enum +import typing + +MyEnum = enum.Enum("MyEnum", ["foo", "bar", "baz"]) +MyIntEnum = enum.IntEnum("MyIntEnum", ["foo", "bar", "baz"]) +MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int}) +MyNamedTuple1 = typing.NamedTuple("MyNamedTuple1", [("foo", int)]) +MyNamedTuple2 = collections.namedtuple("MyNamedTuple2", ["foo"]) + +def f(a: MyEnum, b: MyTypedDict, c: MyNamedTuple1, d: MyNamedTuple2): ... +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md rename to crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md index 7826fbf92d1bd..271b6fd014c3c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md @@ -2,8 +2,8 @@ ## Not yet fully supported -Several type qualifiers are unsupported by red-knot currently. However, we also don't emit -false-positive errors if you use one in an annotation: +Several type qualifiers are unsupported by ty currently. However, we also don't emit false-positive +errors if you use one in an annotation: ```py from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict @@ -11,8 +11,6 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict X: Final = 42 Y: Final[int] = 42 -# TODO: `TypedDict` is actually valid as a base -# error: [invalid-base] class Bar(TypedDict): x: Required[int] y: NotRequired[str] diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md similarity index 80% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md rename to crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 42a70bf04d309..12183c5b2854b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not ## Tuple annotations are understood +```toml +[environment] +python-version = "3.12" +``` + `module.py`: ```py @@ -51,13 +56,12 @@ reveal_type(a) # revealed: tuple[()] reveal_type(b) # revealed: tuple[int] reveal_type(c) # revealed: tuple[str, int] reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] +reveal_type(e) # revealed: tuple[str, ...] -# TODO: homogeneous tuples, PEP-646 tuples, generics -reveal_type(e) # revealed: @Todo(full tuple[...] support) -reveal_type(f) # revealed: @Todo(full tuple[...] support) -reveal_type(g) # revealed: @Todo(full tuple[...] support) -reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)] +reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes] +reveal_type(g) # revealed: @Todo(PEP 646) +reveal_type(h) # revealed: tuple[list[int], list[int]] reveal_type(i) # revealed: tuple[str | int, str | int] reveal_type(j) # revealed: tuple[str | int] ``` @@ -71,7 +75,7 @@ a: tuple[()] = (1, 2) # error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`" b: tuple[int] = ("foo",) -# error: [invalid-assignment] "Object of type `tuple[list, Literal["foo"]]` is not assignable to `tuple[str | int, str]`" +# error: [invalid-assignment] "Object of type `tuple[list[Unknown], Literal["foo"]]` is not assignable to `tuple[str | int, str]`" c: tuple[str | int, str] = ([], "foo") ``` @@ -84,6 +88,33 @@ def foo(v: str | int | None, w: str | str | None, x: str | str): reveal_type(x) # revealed: str ``` +## PEP-604 in non-type-expression context + +### In Python 3.10 and later + +```toml +[environment] +python-version = "3.10" +``` + +```py +IntOrStr = int | str +``` + +### Earlier versions + + + +```toml +[environment] +python-version = "3.9" +``` + +```py +# error: [unsupported-operator] +IntOrStr = int | str +``` + ## Attribute expressions in type annotations are understood ```py diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md rename to crates/ty_python_semantic/resources/mdtest/assignment/augmented.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md b/crates/ty_python_semantic/resources/mdtest/assignment/multi_target.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md rename to crates/ty_python_semantic/resources/mdtest/assignment/multi_target.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md b/crates/ty_python_semantic/resources/mdtest/assignment/unbound.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md rename to crates/ty_python_semantic/resources/mdtest/assignment/unbound.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md b/crates/ty_python_semantic/resources/mdtest/assignment/walrus.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md rename to crates/ty_python_semantic/resources/mdtest/assignment/walrus.md diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md new file mode 100644 index 0000000000000..bba2b4688dab2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -0,0 +1,2376 @@ +# Attributes + +Tests for attribute access on various kinds of types. + +## Class and instance variables + +### Pure instance variables + +#### Variable only declared/bound in `__init__` + +Variables only declared and/or bound in `__init__` are pure instance variables. They cannot be +accessed on the class itself. + +```py +class C: + def __init__(self, param: int | None, flag: bool = False) -> None: + value = 1 if flag else "a" + self.inferred_from_value = value + self.inferred_from_other_attribute = self.inferred_from_value + self.inferred_from_param = param + self.declared_only: bytes + self.declared_and_bound: bool = True + if flag: + self.possibly_undeclared_unbound: str = "possibly set in __init__" + +c_instance = C(1) + +reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] + +# TODO: Same here. This should be `Unknown | Literal[1, "a"]` +reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown + +# There is no special handling of attributes that are (directly) assigned to a declared parameter, +# which means we union with `Unknown` here, since the attribute itself is not declared. This is +# something that we might want to change in the future. +# +# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion. +reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None + +reveal_type(c_instance.declared_only) # revealed: bytes + +reveal_type(c_instance.declared_and_bound) # revealed: bool + +reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str + +# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`. +c_instance.inferred_from_value = "value set on instance" + +# This assignment is also fine: +c_instance.declared_and_bound = False + +# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `declared_and_bound` of type `bool`" +c_instance.declared_and_bound = "incompatible" + +# mypy shows no error here, but pyright raises "reportAttributeAccessIssue" +# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `` itself." +reveal_type(C.inferred_from_value) # revealed: Unknown + +# error: [unresolved-attribute] +reveal_type(C.declared_and_bound) # revealed: Unknown + +# mypy shows no error here, but pyright raises "reportAttributeAccessIssue" +# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object ``" +C.inferred_from_value = "overwritten on class" + +# This assignment is fine: +c_instance.declared_and_bound = False + +# Strictly speaking, inferring this as `Literal[False]` rather than `bool` is unsound in general +# (we don't know what else happened to `c_instance` between the assignment and the use here), +# but mypy and pyright support this. +reveal_type(c_instance.declared_and_bound) # revealed: Literal[False] +``` + +#### Variable declared in class body and possibly bound in `__init__` + +The same rule applies even if the variable is *declared* (not bound!) in the class body: it is still +a pure instance variable. + +```py +class C: + declared_and_bound: str | None + + def __init__(self) -> None: + self.declared_and_bound = "value set in __init__" + +c_instance = C() + +reveal_type(c_instance.declared_and_bound) # revealed: str | None + +reveal_type(C.declared_and_bound) # revealed: str | None + +C.declared_and_bound = "overwritten on class" + +# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`" +c_instance.declared_and_bound = 1 +``` + +#### Variable declared in class body and not bound anywhere + +If a variable is declared in the class body but not bound anywhere, we consider it to be accessible +on instances and the class itself. It would be more consistent to treat this as a pure instance +variable (and require the attribute to be annotated with `ClassVar` if it should be accessible on +the class as well), but other type checkers allow this as well. This is also heavily relied on in +the Python ecosystem: + +```py +class C: + only_declared: str + +c_instance = C() + +reveal_type(c_instance.only_declared) # revealed: str + +reveal_type(C.only_declared) # revealed: str + +C.only_declared = "overwritten on class" +``` + +#### Mixed declarations/bindings in class body and `__init__` + +```py +class C: + only_declared_in_body: str | None + declared_in_body_and_init: str | None + + declared_in_body_defined_in_init: str | None + + bound_in_body_declared_in_init = "a" + + bound_in_body_and_init = None + + def __init__(self, flag) -> None: + self.only_declared_in_init: str | None + self.declared_in_body_and_init: str | None = None + + self.declared_in_body_defined_in_init = "a" + + self.bound_in_body_declared_in_init: str | None + + if flag: + self.bound_in_body_and_init = "a" + +c_instance = C(True) + +reveal_type(c_instance.only_declared_in_body) # revealed: str | None +reveal_type(c_instance.only_declared_in_init) # revealed: str | None +reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None + +reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None + +# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API, +# which is planned in https://github.com/astral-sh/ruff/issues/14297 +reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None + +reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"] +``` + +#### Variable defined in non-`__init__` method + +We also recognize pure instance variables if they are defined in a method that is not `__init__`. + +```py +class C: + def __init__(self, param: int | None, flag: bool = False) -> None: + self.initialize(param, flag) + + def initialize(self, param: int | None, flag: bool) -> None: + value = 1 if flag else "a" + self.inferred_from_value = value + self.inferred_from_other_attribute = self.inferred_from_value + self.inferred_from_param = param + self.declared_only: bytes + self.declared_and_bound: bool = True + +c_instance = C(1) + +reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"] + +# TODO: Should be `Unknown | Literal[1, "a"]` +reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown + +reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None + +reveal_type(c_instance.declared_only) # revealed: bytes + +reveal_type(c_instance.declared_and_bound) # revealed: bool + +# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `` itself." +reveal_type(C.inferred_from_value) # revealed: Unknown + +# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object ``" +C.inferred_from_value = "overwritten on class" +``` + +#### Variable defined in multiple methods + +If we see multiple un-annotated assignments to a single attribute (`self.x` below), we build the +union of all inferred types (and `Unknown`). If we see multiple conflicting declarations of the same +attribute, that should be an error. + +```py +def get_int() -> int: + return 0 + +def get_str() -> str: + return "a" + +class C: + z: int + + def __init__(self) -> None: + self.x = get_int() + self.y: int = 1 + + def other_method(self): + self.x = get_str() + + # TODO: this redeclaration should be an error + self.y: str = "a" + + # TODO: this redeclaration should be an error + self.z: str = "a" + +c_instance = C() + +reveal_type(c_instance.x) # revealed: Unknown | int | str +reveal_type(c_instance.y) # revealed: int +reveal_type(c_instance.z) # revealed: int +``` + +#### Attributes defined in multi-target assignments + +```py +class C: + def __init__(self) -> None: + self.a = self.b = 1 + +c_instance = C() + +reveal_type(c_instance.a) # revealed: Unknown | Literal[1] +reveal_type(c_instance.b) # revealed: Unknown | Literal[1] +``` + +#### Augmented assignments + +```py +class Weird: + def __iadd__(self, other: None) -> str: + return "a" + +class C: + def __init__(self) -> None: + self.w = Weird() + self.w += None + +# TODO: Mypy and pyright do not support this, but it would be great if we could +# infer `Unknown | str` here (`Weird` is not a possible type for the `w` attribute). +reveal_type(C().w) # revealed: Unknown | Weird +``` + +#### Attributes defined in tuple unpackings + +```py +def returns_tuple() -> tuple[int, str]: + return (1, "a") + +class C: + a1, b1 = (1, "a") + c1, d1 = returns_tuple() + + def __init__(self) -> None: + self.a2, self.b2 = (1, "a") + self.c2, self.d2 = returns_tuple() + +c_instance = C() + +reveal_type(c_instance.a1) # revealed: Unknown | Literal[1] +reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"] +reveal_type(c_instance.c1) # revealed: Unknown | int +reveal_type(c_instance.d1) # revealed: Unknown | str + +reveal_type(c_instance.a2) # revealed: Unknown | Literal[1] + +reveal_type(c_instance.b2) # revealed: Unknown | Literal["a"] + +reveal_type(c_instance.c2) # revealed: Unknown | int +reveal_type(c_instance.d2) # revealed: Unknown | str +``` + +#### Starred assignments + +```py +class C: + def __init__(self) -> None: + self.a, *self.b = (1, 2, 3) + +c_instance = C() +reveal_type(c_instance.a) # revealed: Unknown | Literal[1] +reveal_type(c_instance.b) # revealed: Unknown | list[Literal[2, 3]] +``` + +#### Attributes defined in for-loop (unpacking) + +```py +class IntIterator: + def __next__(self) -> int: + return 1 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +class TupleIterator: + def __next__(self) -> tuple[int, str]: + return (1, "a") + +class TupleIterable: + def __iter__(self) -> TupleIterator: + return TupleIterator() + +class NonIterable: ... + +class C: + def __init__(self): + for self.x in IntIterable(): + pass + + for _, self.y in TupleIterable(): + pass + + # TODO: We should emit a diagnostic here + for self.z in NonIterable(): + pass + +reveal_type(C().x) # revealed: Unknown | int +reveal_type(C().y) # revealed: Unknown | str +``` + +#### Attributes defined in `with` statements + +```py +class ContextManager: + def __enter__(self) -> int | None: + return 1 + + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + +class C: + def __init__(self) -> None: + with ContextManager() as self.x: + pass + +c_instance = C() + +reveal_type(c_instance.x) # revealed: Unknown | int | None +``` + +#### Attributes defined in `with` statements, but with unpacking + +```py +class ContextManager: + def __enter__(self) -> tuple[int | None, int]: + return 1, 2 + + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + +class C: + def __init__(self) -> None: + with ContextManager() as (self.x, self.y): + pass + +c_instance = C() + +reveal_type(c_instance.x) # revealed: Unknown | int | None +reveal_type(c_instance.y) # revealed: Unknown | int +``` + +#### Attributes defined in comprehensions + +```py +class IntIterator: + def __next__(self) -> int: + return 1 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +class TupleIterator: + def __next__(self) -> tuple[int, str]: + return (1, "a") + +class TupleIterable: + def __iter__(self) -> TupleIterator: + return TupleIterator() + +class C: + def __init__(self) -> None: + [... for self.a in IntIterable()] + [... for (self.b, self.c) in TupleIterable()] + [... for self.d in IntIterable() for self.e in IntIterable()] + [[... for self.f in IntIterable()] for _ in IntIterable()] + [[... for self.g in IntIterable()] for self in [D()]] + +class D: + g: int + +c_instance = C() + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.a) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.b) # revealed: Unknown + +# TODO: no error, reveal Unknown | str +# error: [unresolved-attribute] +reveal_type(c_instance.c) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.d) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.e) # revealed: Unknown + +# TODO: no error, reveal Unknown | int +# error: [unresolved-attribute] +reveal_type(c_instance.f) # revealed: Unknown + +# This one is correctly not resolved as an attribute: +# error: [unresolved-attribute] +reveal_type(c_instance.g) # revealed: Unknown +``` + +#### Conditionally declared / bound attributes + +We currently treat implicit instance attributes to be bound, even if they are only conditionally +defined: + +```py +def flag() -> bool: + return True + +class C: + def f(self) -> None: + if flag(): + self.a1: str | None = "a" + self.b1 = 1 + if flag(): + def f(self) -> None: + self.a2: str | None = "a" + self.b2 = 1 + +c_instance = C() + +reveal_type(c_instance.a1) # revealed: str | None +reveal_type(c_instance.a2) # revealed: str | None +reveal_type(c_instance.b1) # revealed: Unknown | Literal[1] +reveal_type(c_instance.b2) # revealed: Unknown | Literal[1] +``` + +#### Methods that does not use `self` as a first parameter + +```py +class C: + # This might trigger a stylistic lint like `invalid-first-argument-name-for-method`, but + # it should be supported in general: + def __init__(this) -> None: + this.declared_and_bound: str | None = "a" + +reveal_type(C().declared_and_bound) # revealed: str | None +``` + +#### Aliased `self` parameter + +```py +class C: + def __init__(self) -> None: + this = self + this.declared_and_bound: str | None = "a" + +# This would ideally be `str | None`, but mypy/pyright don't support this either, +# so `Unknown` + a diagnostic is also fine. +# error: [unresolved-attribute] +reveal_type(C().declared_and_bound) # revealed: Unknown +``` + +#### Static methods do not influence implicitly defined attributes + +```py +class Other: + x: int + +class C: + @staticmethod + def f(other: Other) -> None: + other.x = 1 + +# error: [unresolved-attribute] +reveal_type(C.x) # revealed: Unknown + +# error: [unresolved-attribute] +reveal_type(C().x) # revealed: Unknown + +# This also works if `staticmethod` is aliased: + +my_staticmethod = staticmethod + +class D: + @my_staticmethod + def f(other: Other) -> None: + other.x = 1 + +# error: [unresolved-attribute] +reveal_type(D.x) # revealed: Unknown + +# error: [unresolved-attribute] +reveal_type(D().x) # revealed: Unknown +``` + +If `staticmethod` is something else, that should not influence the behavior: + +```py +def staticmethod(f): + return f + +class C: + @staticmethod + def f(self) -> None: + self.x = 1 + +reveal_type(C().x) # revealed: Unknown | Literal[1] +``` + +And if `staticmethod` is fully qualified, that should also be recognized: + +```py +import builtins + +class Other: + x: int + +class C: + @builtins.staticmethod + def f(other: Other) -> None: + other.x = 1 + +# error: [unresolved-attribute] +reveal_type(C.x) # revealed: Unknown + +# error: [unresolved-attribute] +reveal_type(C().x) # revealed: Unknown +``` + +#### Attributes defined in statically-known-to-be-false branches + +```py +class C: + def __init__(self) -> None: + # We use a "significantly complex" condition here (instead of just `False`) + # for a proper comparison with mypy and pyright, which distinguish between + # conditions that can be resolved from a simple pattern matching and those + # that need proper type inference. + if (2 + 3) < 4: + self.x: str = "a" + +# error: [unresolved-attribute] +reveal_type(C().x) # revealed: Unknown +``` + +```py +class C: + def __init__(self, cond: bool) -> None: + if True: + self.a = 1 + else: + self.a = "a" + + if False: + self.b = 2 + + if cond: + return + + self.c = 3 + + self.d = 4 + self.d = 5 + + def set_c(self, c: str) -> None: + self.c = c + if False: + def set_e(self, e: str) -> None: + self.e = e + +reveal_type(C(True).a) # revealed: Unknown | Literal[1] +# error: [unresolved-attribute] +reveal_type(C(True).b) # revealed: Unknown +reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str +# Ideally, this would just be `Unknown | Literal[5]`, but we currently do not +# attempt to analyze control flow within methods more closely. All reachable +# attribute assignments are considered, so `self.x = 4` is also included: +reveal_type(C(True).d) # revealed: Unknown | Literal[4, 5] +# error: [unresolved-attribute] +reveal_type(C(True).e) # revealed: Unknown +``` + +#### Attributes considered always bound + +```py +class C: + def __init__(self, cond: bool): + self.x = 1 + if cond: + raise ValueError("Something went wrong") + + # We consider this attribute is always bound. + # This is because, it is not possible to access a partially-initialized object by normal means. + self.y = 2 + +reveal_type(C(False).x) # revealed: Unknown | Literal[1] +reveal_type(C(False).y) # revealed: Unknown | Literal[2] + +class C: + def __init__(self, b: bytes) -> None: + self.b = b + + try: + s = b.decode() + except UnicodeDecodeError: + raise ValueError("Invalid UTF-8 sequence") + + self.s = s + +reveal_type(C(b"abc").b) # revealed: Unknown | bytes +reveal_type(C(b"abc").s) # revealed: Unknown | str + +class C: + def __init__(self, iter) -> None: + self.x = 1 + + for _ in iter: + pass + + # The for-loop may not stop, + # but we consider the subsequent attributes to be definitely-bound. + self.y = 2 + +reveal_type(C([]).x) # revealed: Unknown | Literal[1] +reveal_type(C([]).y) # revealed: Unknown | Literal[2] +``` + +#### Diagnostics are reported for the right-hand side of attribute assignments + +```py +class C: + def __init__(self) -> None: + # error: [too-many-positional-arguments] + # error: [invalid-argument-type] + self.x: int = len(1, 2, 3) +``` + +### Pure class variables (`ClassVar`) + +#### Annotated with `ClassVar` type qualifier + +Class variables annotated with the [`typing.ClassVar`] type qualifier are pure class variables. They +cannot be overwritten on instances, but they can be accessed on instances. + +For more details, see the [typing spec on `ClassVar`]. + +```py +from typing import ClassVar + +class C: + pure_class_variable1: ClassVar[str] = "value in class body" + pure_class_variable2: ClassVar = 1 + + def method(self): + # TODO: this should be an error + self.pure_class_variable1 = "value set through instance" + +reveal_type(C.pure_class_variable1) # revealed: str + +reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1] + +c_instance = C() + +# It is okay to access a pure class variable on an instance. +reveal_type(c_instance.pure_class_variable1) # revealed: str + +reveal_type(c_instance.pure_class_variable2) # revealed: Unknown | Literal[1] + +# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`" +c_instance.pure_class_variable1 = "value set on instance" + +C.pure_class_variable1 = "overwritten on class" + +# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_class_variable1` of type `str`" +C.pure_class_variable1 = 1 + +class Subclass(C): + pure_class_variable1: ClassVar[str] = "overwritten on subclass" + +reveal_type(Subclass.pure_class_variable1) # revealed: str +``` + +If a class variable is additionally qualified as `Final`, we do not union with `Unknown` for bare +`ClassVar`s: + +```py +from typing import Final + +class D: + final1: Final[ClassVar] = 1 + final2: ClassVar[Final] = 1 + final3: ClassVar[Final[int]] = 1 + final4: Final[ClassVar[int]] = 1 + +reveal_type(D.final1) # revealed: Literal[1] +reveal_type(D.final2) # revealed: Literal[1] +reveal_type(D.final3) # revealed: int +reveal_type(D.final4) # revealed: int +``` + +#### Variable only mentioned in a class method + +We also consider a class variable to be a pure class variable if it is only mentioned in a class +method. + +```py +class C: + @classmethod + def class_method(cls): + cls.pure_class_variable = "value set in class method" + +# for a more realistic example, let's actually call the method +C.class_method() + +reveal_type(C.pure_class_variable) # revealed: Unknown | Literal["value set in class method"] + +C.pure_class_variable = "overwritten on class" +reveal_type(C.pure_class_variable) # revealed: Literal["overwritten on class"] + +c_instance = C() +reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"] + +# TODO: should raise an error. +c_instance.pure_class_variable = "value set on instance" +``` + +### Instance variables with class-level default values + +These are instance attributes, but the fact that we can see that they have a binding (not a +declaration) in the class body means that reading the value from the class directly is also +permitted. This is the only difference for these attributes as opposed to "pure" instance +attributes. + +#### Basic + +```py +class C: + variable_with_class_default1: str = "value in class body" + variable_with_class_default2 = 1 + + def instance_method(self): + self.variable_with_class_default1 = "value set in instance method" + +reveal_type(C.variable_with_class_default1) # revealed: str + +reveal_type(C.variable_with_class_default2) # revealed: Unknown | Literal[1] + +c_instance = C() + +reveal_type(c_instance.variable_with_class_default1) # revealed: str +reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Literal[1] + +c_instance.variable_with_class_default1 = "value set on instance" + +reveal_type(C.variable_with_class_default1) # revealed: str +reveal_type(c_instance.variable_with_class_default1) # revealed: Literal["value set on instance"] + +C.variable_with_class_default1 = "overwritten on class" + +reveal_type(C.variable_with_class_default1) # revealed: Literal["overwritten on class"] +reveal_type(c_instance.variable_with_class_default1) # revealed: Literal["value set on instance"] +``` + +#### Descriptor attributes as class variables + +Whether they are explicitly qualified as `ClassVar`, or just have a class level default, we treat +descriptor attributes as class variables. This test mainly makes sure that we do *not* treat them as +instance variables. This would lead to a different outcome, since the `__get__` method would not be +called (the descriptor protocol is not invoked for instance variables). + +```py +from typing import ClassVar + +class Descriptor: + def __get__(self, instance, owner) -> int: + return 42 + +class C: + a: ClassVar[Descriptor] + b: Descriptor = Descriptor() + c: ClassVar[Descriptor] = Descriptor() + +reveal_type(C().a) # revealed: int +reveal_type(C().b) # revealed: int +reveal_type(C().c) # revealed: int +``` + +### Inheritance of class/instance attributes + +#### Instance variable defined in a base class + +```py +class Base: + declared_in_body: int | None = 1 + + base_class_attribute_1: str | None + base_class_attribute_2: str | None + base_class_attribute_3: str | None + + def __init__(self) -> None: + self.defined_in_init: str | None = "value in base" + +class Intermediate(Base): + # Redeclaring base class attributes with the *same *type is fine: + base_class_attribute_1: str | None = None + + # 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 + # of same name in class "Base". Variable is mutable so its type is invariant" + # + # We should introduce a diagnostic for this. Whether or not that should be + # enabled by default can still be discussed. + # + # TODO: This should be an error + base_class_attribute_2: str + + # Redeclaring attributes with a *wider type* directly violates LSP. + # + # In this case, both mypy and pyright report an error. + # + # TODO: This should be an error + base_class_attribute_3: str | int | None + +class Derived(Intermediate): ... + +reveal_type(Derived.declared_in_body) # revealed: int | None + +reveal_type(Derived().declared_in_body) # revealed: int | None + +reveal_type(Derived().defined_in_init) # revealed: str | None +``` + +## Accessing attributes on class objects + +When accessing attributes on class objects, they are always looked up on the type of the class +object first, i.e. on the metaclass: + +```py +from typing import Literal + +class Meta1: + attr: Literal["metaclass value"] = "metaclass value" + +class C1(metaclass=Meta1): ... + +reveal_type(C1.attr) # revealed: Literal["metaclass value"] +``` + +However, the metaclass attribute only takes precedence over a class-level attribute if it is a data +descriptor. If it is a non-data descriptor or a normal attribute, the class-level attribute is used +instead (see the [descriptor protocol tests] for data/non-data descriptor attributes): + +```py +class Meta2: + attr: str = "metaclass value" + +class C2(metaclass=Meta2): + attr: Literal["class value"] = "class value" + +reveal_type(C2.attr) # revealed: Literal["class value"] +``` + +If the class-level attribute is only partially defined, we union the metaclass attribute with the +class-level attribute: + +```py +def _(flag: bool): + class Meta3: + attr1 = "metaclass value" + attr2: Literal["metaclass value"] = "metaclass value" + + class C3(metaclass=Meta3): + if flag: + attr1 = "class value" + # TODO: Neither mypy nor pyright show an error here, but we could consider emitting a conflicting-declaration diagnostic here. + attr2: Literal["class value"] = "class value" + + reveal_type(C3.attr1) # revealed: Unknown | Literal["metaclass value", "class value"] + reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"] +``` + +If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute` +diagnostic: + +```py +def _(flag: bool): + class Meta4: + if flag: + attr1: str = "metaclass value" + + class C4(metaclass=Meta4): ... + # error: [possibly-unbound-attribute] + reveal_type(C4.attr1) # revealed: str +``` + +Finally, if both the metaclass attribute and the class-level attribute are only partially defined, +we union them and emit a `possibly-unbound-attribute` diagnostic: + +```py +def _(flag1: bool, flag2: bool): + class Meta5: + if flag1: + attr1 = "metaclass value" + + class C5(metaclass=Meta5): + if flag2: + attr1 = "class value" + + # error: [possibly-unbound-attribute] + reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"] +``` + +## Invalid access to attribute + + + +If an undefined variable is used in a method, and an attribute with the same name is defined and +accessible, then we emit a subdiagnostic suggesting the use of `self.`. (These don't appear inline +here; see the diagnostic snapshots.) + +```py +class Foo: + x: int + + def method(self): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +```py +class Foo: + x: int = 1 + + def method(self): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +```py +class Foo: + def __init__(self): + self.x = 1 + + def method(self): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +In a staticmethod, we don't suggest that it might be an attribute. + +```py +class Foo: + def __init__(self): + self.x = 42 + + @staticmethod + def static_method(): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +In a classmethod, if the name matches a class attribute, we suggest `cls.`. + +```py +from typing import ClassVar + +class Foo: + x: ClassVar[int] = 42 + + @classmethod + def class_method(cls): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +In a classmethod, if the name matches an instance-only attribute, we don't suggest anything. + +```py +class Foo: + def __init__(self): + self.x = 42 + + @classmethod + def class_method(cls): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +We also don't suggest anything if the method is (invalidly) decorated with both `@classmethod` and +`@staticmethod`: + +```py +class Foo: + x: ClassVar[int] + + @classmethod + @staticmethod + def class_method(cls): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +In an instance method that uses some other parameter name in place of `self`, we use that parameter +name in the sub-diagnostic. + +```py +class Foo: + def __init__(self): + self.x = 42 + + def method(other): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +In a classmethod that uses some other parameter name in place of `cls`, we use that parameter name +in the sub-diagnostic. + +```py +from typing import ClassVar + +class Foo: + x: ClassVar[int] = 42 + + @classmethod + def class_method(c_other): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +We don't suggest anything if an instance method or a classmethod only has variadic arguments, or if +the first parameter is keyword-only: + +```py +from typing import ClassVar + +class Foo: + x: ClassVar[int] = 42 + + def instance_method(*args, **kwargs): + # error: [unresolved-reference] "Name `x` used when not defined" + print(x) + + @classmethod + def class_method(*, cls): + # error: [unresolved-reference] "Name `x` used when not defined" + y = x +``` + +## Unions of attributes + +If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we +infer those union types accordingly: + +```py +def _(flag: bool): + if flag: + class C1: + x = 1 + y: int = 1 + + else: + class C1: + x = 2 + y: int | str = "b" + + reveal_type(C1.x) # revealed: Unknown | Literal[1, 2] + reveal_type(C1.y) # revealed: int | str + + C1.y = 100 + # error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type ` | `" + C1.y = "problematic" + + class C2: + if flag: + x = 3 + y: int = 3 + else: + x = 4 + y: int | str = "d" + + reveal_type(C2.x) # revealed: Unknown | Literal[3, 4] + reveal_type(C2.y) # revealed: int | str + + C2.y = 100 + # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`" + C2.y = None + # TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment` + C2.y = "problematic" + + if flag: + class Meta3(type): + x = 5 + y: int = 5 + + else: + class Meta3(type): + x = 6 + y: int | str = "f" + + class C3(metaclass=Meta3): ... + reveal_type(C3.x) # revealed: Unknown | Literal[5, 6] + reveal_type(C3.y) # revealed: int | str + + C3.y = 100 + # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`" + C3.y = None + # TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment` + C3.y = "problematic" + + class Meta4(type): + if flag: + x = 7 + y: int = 7 + else: + x = 8 + y: int | str = "h" + + class C4(metaclass=Meta4): ... + reveal_type(C4.x) # revealed: Unknown | Literal[7, 8] + reveal_type(C4.y) # revealed: int | str + + C4.y = 100 + # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`" + C4.y = None + # TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment` + C4.y = "problematic" +``` + +## Unions with possibly unbound paths + +### Definite boundness within a class + +In this example, the `x` attribute is not defined in the `C2` element of the union: + +```py +def _(flag1: bool, flag2: bool): + class C1: + x = 1 + + class C2: ... + + class C3: + x = 3 + + C = C1 if flag1 else C2 if flag2 else C3 + + # error: [possibly-unbound-attribute] "Attribute `x` on type ` | | ` is possibly unbound" + reveal_type(C.x) # revealed: Unknown | Literal[1, 3] + + # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type ` | | `" + C.x = 100 + + # error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound" + reveal_type(C().x) # revealed: Unknown | Literal[1, 3] + + # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`" + C().x = 100 +``` + +### Possibly-unbound within a class + +We raise the same diagnostic if the attribute is possibly-unbound in at least one element of the +union: + +```py +def _(flag: bool, flag1: bool, flag2: bool): + class C1: + x = 1 + + class C2: + if flag: + x = 2 + + class C3: + x = 3 + + C = C1 if flag1 else C2 if flag2 else C3 + + # error: [possibly-unbound-attribute] "Attribute `x` on type ` | | ` is possibly unbound" + reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3] + + # error: [possibly-unbound-attribute] + C.x = 100 + + # Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually, + # see the "Possibly unbound/undeclared instance attribute" section below. + # error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound" + reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3] + + # error: [possibly-unbound-attribute] + C().x = 100 +``` + +### Possibly-unbound within gradual types + +```py +from typing import Any + +def _(flag: bool): + class Base: + x: Any + + class Derived(Base): + if flag: + # Redeclaring `x` with a more static type is okay in terms of LSP. + x: int + + reveal_type(Derived().x) # revealed: int | Any + + Derived().x = 1 + + # TODO + # The following assignment currently fails, because we first check if "a" is assignable to the + # attribute on the meta-type of `Derived`, i.e. ``. When accessing the class + # member `x` on `Derived`, we only see the `x: int` declaration and do not union it with the + # type of the base class attribute `x: Any`. This could potentially be improved. Note that we + # see a type of `int | Any` above because we have the full union handling of possibly-unbound + # *instance* attributes. + + # error: [invalid-assignment] "Object of type `Literal["a"]` is not assignable to attribute `x` of type `int`" + Derived().x = "a" +``` + +### Attribute possibly unbound on a subclass but not on a superclass + +```py +def _(flag: bool): + class Foo: + x = 1 + + class Bar(Foo): + if flag: + x = 2 + + reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1] + Bar.x = 3 + + reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1] + Bar().x = 3 +``` + +### Attribute possibly unbound on a subclass and on a superclass + +```py +def _(flag: bool): + class Foo: + if flag: + x = 1 + + class Bar(Foo): + if flag: + x = 2 + + # error: [possibly-unbound-attribute] + reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1] + + # error: [possibly-unbound-attribute] + Bar.x = 3 + + # error: [possibly-unbound-attribute] + reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1] + + # error: [possibly-unbound-attribute] + Bar().x = 3 +``` + +### Possibly unbound/undeclared instance attribute + +We currently treat implicit instance attributes to be bound, even if they are only conditionally +defined within a method. If the class-level definition or the whole method is only conditionally +available, we emit a `possibly-unbound-attribute` diagnostic. + +#### Possibly unbound and undeclared + +```py +def _(flag: bool): + class Foo: + if flag: + x: int + + def __init(self): + if flag: + self.x = 1 + + reveal_type(Foo().x) # revealed: int | Unknown + + Foo().x = 1 +``` + +#### Possibly unbound + +```py +def _(flag: bool): + class Foo: + def __init(self): + if flag: + self.x = 1 + self.y = "a" + else: + self.y = "b" + + reveal_type(Foo().x) # revealed: Unknown | Literal[1] + + Foo().x = 2 + + reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"] + Foo().y = "c" +``` + +### Unions with all paths unbound + +If the symbol is unbound in all elements of the union, we detect that: + +```py +def _(flag: bool): + class C1: ... + class C2: ... + C = C1 if flag else C2 + + # error: [unresolved-attribute] "Type ` | ` has no attribute `x`" + reveal_type(C.x) # revealed: Unknown + + # TODO: This should ideally be a `unresolved-attribute` error. We need better union + # handling in `validate_attribute_assignment` for this. + # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type ` | `" + C.x = 1 +``` + +## Inherited class attributes + +### Basic + +```py +class A: + X = "foo" + +class B(A): ... +class C(B): ... + +reveal_type(C.X) # revealed: Unknown | Literal["foo"] + +C.X = "bar" +``` + +### Multiple inheritance + +```py +class O: ... + +class F(O): + X = 56 + +class E(O): + X = 42 + +class D(O): ... +class C(D, F): ... +class B(E, D): ... +class A(B, C): ... + +# revealed: tuple[, , , , , , , ] +reveal_type(A.__mro__) + +# `E` is earlier in the MRO than `F`, so we should use the type of `E.X` +reveal_type(A.X) # revealed: Unknown | Literal[42] + +A.X = 100 +``` + +## Intersections of attributes + +### Attribute only available on one element + +```py +from ty_extensions import Intersection + +class A: + x: int = 1 + +class B: ... + +def _(a_and_b: Intersection[A, B]): + reveal_type(a_and_b.x) # revealed: int + + a_and_b.x = 2 + +# Same for class objects +def _(a_and_b: Intersection[type[A], type[B]]): + reveal_type(a_and_b.x) # revealed: int + + a_and_b.x = 2 +``` + +### Attribute available on both elements + +```py +from ty_extensions import Intersection + +class P: ... +class Q: ... +class R(P, Q): ... + +class A: + x: P = P() + +class B: + x: Q = Q() + +def _(a_and_b: Intersection[A, B]): + reveal_type(a_and_b.x) # revealed: P & Q + a_and_b.x = R() + +# Same for class objects +def _(a_and_b: Intersection[type[A], type[B]]): + reveal_type(a_and_b.x) # revealed: P & Q + a_and_b.x = R() +``` + +### Possible unboundness + +```py +from ty_extensions import Intersection + +class P: ... +class Q: ... +class R(P, Q): ... + +def _(flag: bool): + class A1: + if flag: + x: P = P() + + class B1: ... + + def inner1(a_and_b: Intersection[A1, B1]): + # error: [possibly-unbound-attribute] + reveal_type(a_and_b.x) # revealed: P + + # error: [possibly-unbound-attribute] + a_and_b.x = R() + # Same for class objects + def inner1_class(a_and_b: Intersection[type[A1], type[B1]]): + # error: [possibly-unbound-attribute] + reveal_type(a_and_b.x) # revealed: P + + # error: [possibly-unbound-attribute] + a_and_b.x = R() + + class A2: + if flag: + x: P = P() + + class B1: + x: Q = Q() + + def inner2(a_and_b: Intersection[A2, B1]): + reveal_type(a_and_b.x) # revealed: P & Q + + # TODO: this should not be an error, we need better intersection + # handling in `validate_attribute_assignment` for this + # error: [possibly-unbound-attribute] + a_and_b.x = R() + # Same for class objects + def inner2_class(a_and_b: Intersection[type[A2], type[B1]]): + reveal_type(a_and_b.x) # revealed: P & Q + + class A3: + if flag: + x: P = P() + + class B3: + if flag: + x: Q = Q() + + def inner3(a_and_b: Intersection[A3, B3]): + # error: [possibly-unbound-attribute] + reveal_type(a_and_b.x) # revealed: P & Q + + # error: [possibly-unbound-attribute] + a_and_b.x = R() + # Same for class objects + def inner3_class(a_and_b: Intersection[type[A3], type[B3]]): + # error: [possibly-unbound-attribute] + reveal_type(a_and_b.x) # revealed: P & Q + + # error: [possibly-unbound-attribute] + a_and_b.x = R() + + class A4: ... + class B4: ... + + def inner4(a_and_b: Intersection[A4, B4]): + # error: [unresolved-attribute] + reveal_type(a_and_b.x) # revealed: Unknown + + # error: [invalid-assignment] + a_and_b.x = R() + # Same for class objects + def inner4_class(a_and_b: Intersection[type[A4], type[B4]]): + # error: [unresolved-attribute] + reveal_type(a_and_b.x) # revealed: Unknown + + # error: [invalid-assignment] + a_and_b.x = R() +``` + +### Intersection of implicit instance attributes + +```py +from ty_extensions import Intersection + +class P: ... +class Q: ... + +class A: + def __init__(self): + self.x: P = P() + +class B: + def __init__(self): + self.x: Q = Q() + +def _(a_and_b: Intersection[A, B]): + reveal_type(a_and_b.x) # revealed: P & Q +``` + +## Attribute access on `Any` + +The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows +from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on +`object` -- but if the attribute *does* exist on `object`, the type of the attribute is +` & Any`. + +```py +from typing import Any + +class Foo(Any): ... + +reveal_type(Foo.bar) # revealed: Any +reveal_type(Foo.__repr__) # revealed: (def __repr__(self) -> str) & Any +``` + +Similar principles apply if `Any` appears in the middle of an inheritance hierarchy: + +```py +from typing import ClassVar, Literal + +class A: + x: ClassVar[Literal[1]] = 1 + +class B(Any): ... +class C(B, A): ... + +reveal_type(C.__mro__) # revealed: tuple[, , Any, , ] +reveal_type(C.x) # revealed: Literal[1] & Any +``` + +## Classes with custom `__getattr__` methods + +### Basic + +If a type provides a custom `__getattr__` method, we use the return type of that method as the type +for unknown attributes. Consider the following `CustomGetAttr` class: + +```py +from typing import Literal + +def flag() -> bool: + return True + +class GetAttrReturnType: ... + +class CustomGetAttr: + class_attr: int = 1 + + if flag(): + possibly_unbound: bytes = b"a" + + def __init__(self) -> None: + self.instance_attr: str = "a" + + def __getattr__(self, name: str) -> GetAttrReturnType: + return GetAttrReturnType() +``` + +We can access arbitrary attributes on instances of this class, and the type of the attribute will be +`GetAttrReturnType`: + +```py +c = CustomGetAttr() + +reveal_type(c.whatever) # revealed: GetAttrReturnType +``` + +If an attribute is defined on the class, it takes precedence over the `__getattr__` method: + +```py +reveal_type(c.class_attr) # revealed: int +``` + +If the class attribute is possibly unbound, we union the type of the attribute with the fallback +type of the `__getattr__` method: + +```py +reveal_type(c.possibly_unbound) # revealed: bytes | GetAttrReturnType +``` + +Instance attributes also take precedence over the `__getattr__` method: + +```py +# Note: we could attempt to union with the fallback type of `__getattr__` here, as we currently do not +# attempt to determine if instance attributes are always bound or not. Neither mypy nor pyright do this, +# so it's not a priority. +reveal_type(c.instance_attr) # revealed: str +``` + +Importantly, `__getattr__` is only called if attributes are accessed on instances, not if they are +accessed on the class itself: + +```py +# error: [unresolved-attribute] +CustomGetAttr.whatever +``` + +### Type of the `name` parameter + +If the `name` parameter of the `__getattr__` method is annotated with a (union of) literal type(s), +we only consider the attribute access to be valid if the accessed attribute is one of them: + +```py +from typing import Literal + +class Date: + def __getattr__(self, name: Literal["day", "month", "year"]) -> int: + return 0 + +date = Date() + +reveal_type(date.day) # revealed: int +reveal_type(date.month) # revealed: int +reveal_type(date.year) # revealed: int + +# error: [unresolved-attribute] "Type `Date` has no attribute `century`" +reveal_type(date.century) # revealed: Unknown +``` + +### `argparse.Namespace` + +A standard library example of a class with a custom `__getattr__` method is `argparse.Namespace`: + +```py +import argparse + +def _(ns: argparse.Namespace): + reveal_type(ns.whatever) # revealed: Any +``` + +## Classes with custom `__getattribute__` methods + +If a type provides a custom `__getattribute__`, we use its return type as the type for unknown +attributes. Note that this behavior differs from runtime, where `__getattribute__` is called +unconditionally, even for known attributes. The rationale for doing this is that it allows users to +specify more precise types for specific attributes, such as `x: str` in the example below. This +behavior matches other type checkers such as mypy and pyright. + +```py +from typing import Any + +class Foo: + x: str + def __getattribute__(self, attr: str) -> Any: + return 42 + +reveal_type(Foo().x) # revealed: str +reveal_type(Foo().y) # revealed: Any +``` + +A standard library example for a class with a custom `__getattribute__` method is `SimpleNamespace`: + +```py +from types import SimpleNamespace + +sn = SimpleNamespace(a="a") + +reveal_type(sn.a) # revealed: Any +``` + +`__getattribute__` takes precedence over `__getattr__`: + +```py +class C: + def __getattribute__(self, name: str) -> int: + return 1 + + def __getattr__(self, name: str) -> str: + return "a" + +c = C() + +reveal_type(c.x) # revealed: int +``` + +Like all dunder methods, `__getattribute__` is not looked up on instances: + +```py +def external_getattribute(name) -> int: + return 1 + +class ThisFails: + def __init__(self): + self.__getattribute__ = external_getattribute + +# error: [unresolved-attribute] +ThisFails().x +``` + +## Classes with custom `__setattr__` methods + +### Basic + +If a type provides a custom `__setattr__` method, we use the parameter type of that method as the +type to validate attribute assignments. Consider the following `CustomSetAttr` class: + +```py +class CustomSetAttr: + def __setattr__(self, name: str, value: int) -> None: + pass +``` + +We can set arbitrary attributes on instances of this class: + +```py +c = CustomSetAttr() + +c.whatever = 42 +``` + +### Type of the `name` parameter + +If the `name` parameter of the `__setattr__` method is annotated with a (union of) literal type(s), +we only consider the attribute assignment to be valid if the assigned attribute is one of them: + +```py +from typing import Literal + +class Date: + def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + pass + +date = Date() +date.day = 8 +date.month = 4 +date.year = 2025 + +# error: [unresolved-attribute] "Can not assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method." +date.tz = "UTC" +``` + +### Return type of `__setattr__` + +If the return type of the `__setattr__` method is `Never`, we do not allow any attribute assignments +on instances of that class: + +```py +from typing_extensions import Never + +class Frozen: + existing: int = 1 + + def __setattr__(self, name, value) -> Never: + raise AttributeError("Attributes can not be modified") + +instance = Frozen() +instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `non_existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`" +instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`" +``` + +### `__setattr__` on `object` + +`object` has a custom `__setattr__` implementation, but we still emit an error if a non-existing +attribute is assigned on an `object` instance. + +```py +obj = object() +obj.non_existing = 1 # error: [unresolved-attribute] +``` + +### Setting attributes on `Never` / `Any` + +Setting attributes on `Never` itself should be allowed (even though it has a `__setattr__` attribute +of type `Never`): + +```py +from typing_extensions import Never, Any + +def _(n: Never): + reveal_type(n.__setattr__) # revealed: Never + + # No error: + n.non_existing = 1 +``` + +And similarly for `Any`: + +```py +def _(a: Any): + reveal_type(a.__setattr__) # revealed: Any + + # No error: + a.non_existing = 1 +``` + +### Possibly unbound `__setattr__` method + +If a `__setattr__` method is only partially bound, the behavior is still the same: + +```py +from typing_extensions import Never + +def flag() -> bool: + return True + +class Frozen: + if flag(): + def __setattr__(self, name, value) -> Never: + raise AttributeError("Attributes can not be modified") + +instance = Frozen() +instance.non_existing = 2 # error: [invalid-assignment] +instance.existing = 2 # error: [invalid-assignment] +``` + +### `argparse.Namespace` + +A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`: + +```py +import argparse + +def _(ns: argparse.Namespace): + ns.whatever = 42 +``` + +## Objects of all types have a `__class__` method + +The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as +`type(x)`. + +```py +import typing_extensions + +reveal_type(typing_extensions.__class__) # revealed: +reveal_type(type(typing_extensions)) # revealed: + +a = 42 +reveal_type(a.__class__) # revealed: +reveal_type(type(a)) # revealed: + +b = "42" +reveal_type(b.__class__) # revealed: + +c = b"42" +reveal_type(c.__class__) # revealed: + +d = True +reveal_type(d.__class__) # revealed: + +e = (42, 42) +reveal_type(e.__class__) # revealed: + +def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): + reveal_type(a.__class__) # revealed: type[int] + reveal_type(type(a)) # revealed: type[int] + + reveal_type(b.__class__) # revealed: + reveal_type(type(b)) # revealed: + + reveal_type(c.__class__) # revealed: type[int] | type[str] + reveal_type(type(c)) # revealed: type[int] | type[str] + + # `type[type]`, a.k.a., either the class `type` or some subclass of `type`. + # It would be incorrect to infer `Literal[type]` here, + # as `c` could be some subclass of `str` with a custom metaclass. + # All we know is that the metaclass must be a (non-strict) subclass of `type`. + reveal_type(d.__class__) # revealed: type[type] + +reveal_type(f.__class__) # revealed: + +class Foo: ... + +reveal_type(Foo.__class__) # revealed: +``` + +## Module attributes + +### Basic + +`mod.py`: + +```py +global_symbol: str = "a" +``` + +```py +import mod + +reveal_type(mod.global_symbol) # revealed: str +mod.global_symbol = "b" + +# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`" +mod.global_symbol = 1 + +# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`" +(_, mod.global_symbol) = (..., 1) + +# TODO: this should be an error, but we do not understand list unpackings yet. +[_, mod.global_symbol] = [1, 2] + +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +# error: [invalid-assignment] "Object of type `int` is not assignable to attribute `global_symbol` of type `str`" +for mod.global_symbol in IntIterable(): + pass +``` + +### Nested module attributes + +`outer/__init__.py`: + +```py +``` + +`outer/nested/__init__.py`: + +```py +``` + +`outer/nested/inner.py`: + +```py +class Outer: + class Nested: + class Inner: + attr: int = 1 +``` + +```py +import outer.nested.inner + +reveal_type(outer.nested.inner.Outer.Nested.Inner.attr) # revealed: int + +# error: [invalid-assignment] +outer.nested.inner.Outer.Nested.Inner.attr = "a" +``` + +### Unions of module attributes + +`mod1.py`: + +```py +global_symbol: str = "a" +``` + +`mod2.py`: + +```py +global_symbol: str = "a" +``` + +```py +import mod1 +import mod2 + +def _(flag: bool): + if flag: + mod = mod1 + else: + mod = mod2 + + mod.global_symbol = "b" + + # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` on type ` | `" + mod.global_symbol = 1 +``` + +## Literal types + +### Function-literal attributes + +Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all +functions are instances of that class: + +```py +def f(): ... + +reveal_type(f.__defaults__) # revealed: tuple[Any, ...] | None +reveal_type(f.__kwdefaults__) # revealed: dict[str, Any] | None +``` + +Some attributes are special-cased, however: + +```py +reveal_type(f.__get__) # revealed: +reveal_type(f.__call__) # revealed: +``` + +### Int-literal attributes + +Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal +integers are instances of that class: + +```py +reveal_type((2).bit_length) # revealed: bound method Literal[2].bit_length() -> int +reveal_type((2).denominator) # revealed: Literal[1] +``` + +Some attributes are special-cased, however: + +```py +reveal_type((2).numerator) # revealed: Literal[2] +reveal_type((2).real) # revealed: Literal[2] +``` + +### Bool-literal attributes + +Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal +bools are instances of that class: + +```py +# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int] +reveal_type(True.__and__) +# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int] +reveal_type(False.__or__) +``` + +Some attributes are special-cased, however: + +```py +reveal_type(True.numerator) # revealed: Literal[1] +reveal_type(False.real) # revealed: Literal[0] +``` + +### Bytes-literal attributes + +All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: + +```py +# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes +reveal_type(b"foo".join) +# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool +reveal_type(b"foo".endswith) +``` + +## Instance attribute edge cases + +### Assignment to attribute that does not correspond to the instance + +```py +class Other: + x: int = 1 + +class C: + def __init__(self, other: Other) -> None: + other.x = 1 + +def f(c: C): + # error: [unresolved-attribute] + reveal_type(c.x) # revealed: Unknown +``` + +### Nested classes + +```py +class Outer: + def __init__(self): + self.x: int = 1 + + class Middle: + # has no 'x' attribute + + class Inner: + def __init__(self): + self.x: str = "a" + +reveal_type(Outer().x) # revealed: int + +# error: [unresolved-attribute] +Outer.Middle().x + +reveal_type(Outer.Middle.Inner().x) # revealed: str +``` + +### Shadowing of `self` + +```py +class Other: + x: int = 1 + +class C: + def __init__(self) -> None: + # Redeclaration of self. `self` does not refer to the instance anymore. + self: Other = Other() + self.x: int = 1 + +# TODO: this should be an error +C().x +``` + +### Assignment to `self` after nested function + +```py +class Other: + x: str = "a" + +class C: + def __init__(self) -> None: + def nested_function(self: Other): + self.x = "b" + self.x: int = 1 + +reveal_type(C().x) # revealed: int +``` + +### Assignment to `self` from nested function + +```py +class C: + def __init__(self) -> None: + def set_attribute(value: str): + self.x: str = value + set_attribute("a") + +# TODO: ideally, this would be `str`. Mypy supports this, pyright does not. +# error: [unresolved-attribute] +reveal_type(C().x) # revealed: Unknown +``` + +### Accessing attributes on `Never` + +Arbitrary attributes can be accessed on `Never` without emitting any errors: + +```py +from typing_extensions import Never + +def f(never: Never): + reveal_type(never.arbitrary_attribute) # revealed: Never + + # Assigning `Never` to an attribute on `Never` is also allowed: + never.another_attribute = never +``` + +### Cyclic implicit attributes + +Inferring types for undeclared implicit attributes can be cyclic: + +```py +class C: + def __init__(self): + self.x = 1 + + def copy(self, other: "C"): + self.x = other.x + +reveal_type(C().x) # revealed: Unknown | Literal[1] +``` + +If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute: + +```py +class D: + def copy(self, other: "D"): + self.x = other.x + +reveal_type(D().x) # revealed: Unknown +``` + +If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to +that name, so these cases don't trigger any cycle: + +```py +class E: + def __init__(self): + self.x: int = 1 + + def copy(self, other: "E"): + self.x = other.x + +reveal_type(E().x) # revealed: int + +class F: + def __init__(self): + self.x = 1 + + def copy(self, other: "F"): + self.x: int = other.x + +reveal_type(F().x) # revealed: int + +class G: + def copy(self, other: "G"): + self.x: int = other.x + +reveal_type(G().x) # revealed: int +``` + +We can even handle cycles involving multiple classes: + +```py +class A: + def __init__(self): + self.x = 1 + + def copy(self, other: "B"): + self.x = other.x + +class B: + def copy(self, other: "A"): + self.x = other.x + +reveal_type(B().x) # revealed: Unknown | Literal[1] +reveal_type(A().x) # revealed: Unknown | Literal[1] +``` + +This case additionally tests our union/intersection simplification logic: + +```py +class H: + def __init__(self): + self.x = 1 + + def copy(self, other: "H"): + self.x = other.x or self.x +``` + +### Builtin types attributes + +This test can probably be removed eventually, but we currently include it because we do not yet +understand generic bases and protocols, and we want to make sure that we can still use builtin types +in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more +information. + +```py +class C: + a_int: int = 1 + a_str: str = "a" + a_bytes: bytes = b"a" + a_bool: bool = True + a_float: float = 1.0 + a_complex: complex = 1 + 1j + a_tuple: tuple[int] = (1,) + a_range: range = range(1) + a_slice: slice = slice(1) + a_type: type = int + a_none: None = None + +reveal_type(C.a_int) # revealed: int +reveal_type(C.a_str) # revealed: str +reveal_type(C.a_bytes) # revealed: bytes +reveal_type(C.a_bool) # revealed: bool +reveal_type(C.a_float) # revealed: int | float +reveal_type(C.a_complex) # revealed: int | float | complex +reveal_type(C.a_tuple) # revealed: tuple[int] +reveal_type(C.a_range) # revealed: range +# TODO: revealed: slice[Any, Literal[1], Any] +reveal_type(C.a_slice) # revealed: slice[Any, Any, Any] +reveal_type(C.a_type) # revealed: type +reveal_type(C.a_none) # revealed: None +``` + +### Generic methods + +We also detect implicit instance attributes on methods that are themselves generic. We have an extra +test for this because generic functions have an extra type-params scope in between the function body +scope and the outer scope, so we need to make sure that our implementation can still recognize `f` +as a method of `C` here: + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C: + def f[T](self, t: T) -> T: + self.x: int = 1 + return t + +reveal_type(C().x) # revealed: int +``` + +### Attributes defined in methods with unknown decorators + +When an attribute is defined in a method that is decorated with an unknown decorator, we consider it +to be accessible on both the class itself and instances of that class. This is consistent with the +gradual guarantee, because the unknown decorator *could* be an alias for `builtins.classmethod`. + +```py +# error: [unresolved-import] +from unknown_library import unknown_decorator + +class C: + @unknown_decorator + def f(self): + self.x: int = 1 + +reveal_type(C.x) # revealed: int +reveal_type(C().x) # revealed: int +``` + +## Enum classes + +Enums are not supported yet; attribute access on an enum class is inferred as `Todo`. + +```py +import enum + +reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes) + +class Foo(enum.Enum): + BAR = 1 + +reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes) +reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes) +reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes) +``` + +## References + +Some of the tests in the *Class and instance variables* section draw inspiration from +[pyright's documentation] on this topic. + +[descriptor protocol tests]: descriptor_protocol.md +[pyright's documentation]: https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables +[typing spec on `classvar`]: https://typing.python.org/en/latest/spec/class-compat.html#classvar +[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar diff --git a/crates/ty_python_semantic/resources/mdtest/binary/booleans.md b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md new file mode 100644 index 0000000000000..e0bcca56e950f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md @@ -0,0 +1,167 @@ +## Binary operations on booleans + +## Basic Arithmetic + +We try to be precise and all operations except for division will result in Literal type. + +```py +a = True +b = False + +reveal_type(a + a) # revealed: Literal[2] +reveal_type(a + b) # revealed: Literal[1] +reveal_type(b + a) # revealed: Literal[1] +reveal_type(b + b) # revealed: Literal[0] + +reveal_type(a - a) # revealed: Literal[0] +reveal_type(a - b) # revealed: Literal[1] +reveal_type(b - a) # revealed: Literal[-1] +reveal_type(b - b) # revealed: Literal[0] + +reveal_type(a * a) # revealed: Literal[1] +reveal_type(a * b) # revealed: Literal[0] +reveal_type(b * a) # revealed: Literal[0] +reveal_type(b * b) # revealed: Literal[0] + +reveal_type(a % a) # revealed: Literal[0] +reveal_type(b % a) # revealed: Literal[0] + +reveal_type(a // a) # revealed: Literal[1] +reveal_type(b // a) # revealed: Literal[0] + +reveal_type(a**a) # revealed: Literal[1] +reveal_type(a**b) # revealed: Literal[1] +reveal_type(b**a) # revealed: Literal[0] +reveal_type(b**b) # revealed: Literal[1] + +# Division +reveal_type(a / a) # revealed: float +reveal_type(b / a) # revealed: float +b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero" +a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" + +# bitwise OR +reveal_type(a | a) # revealed: Literal[True] +reveal_type(a | b) # revealed: Literal[True] +reveal_type(b | a) # revealed: Literal[True] +reveal_type(b | b) # revealed: Literal[False] + +# bitwise AND +reveal_type(a & a) # revealed: Literal[True] +reveal_type(a & b) # revealed: Literal[False] +reveal_type(b & a) # revealed: Literal[False] +reveal_type(b & b) # revealed: Literal[False] + +# bitwise XOR +reveal_type(a ^ a) # revealed: Literal[False] +reveal_type(a ^ b) # revealed: Literal[True] +reveal_type(b ^ a) # revealed: Literal[True] +reveal_type(b ^ b) # revealed: Literal[False] +``` + +## Arithmetic with a variable + +```py +def _(a: bool): + def lhs_is_int(x: int): + reveal_type(x + a) # revealed: int + reveal_type(x - a) # revealed: int + reveal_type(x * a) # revealed: int + reveal_type(x // a) # revealed: int + reveal_type(x / a) # revealed: int | float + reveal_type(x % a) # revealed: int + + def rhs_is_int(x: int): + reveal_type(a + x) # revealed: int + reveal_type(a - x) # revealed: int + reveal_type(a * x) # revealed: int + reveal_type(a // x) # revealed: int + reveal_type(a / x) # revealed: int | float + reveal_type(a % x) # revealed: int + + def lhs_is_bool(x: bool): + reveal_type(x + a) # revealed: int + reveal_type(x - a) # revealed: int + reveal_type(x * a) # revealed: int + reveal_type(x // a) # revealed: int + reveal_type(x / a) # revealed: int | float + reveal_type(x % a) # revealed: int + + def rhs_is_bool(x: bool): + reveal_type(a + x) # revealed: int + reveal_type(a - x) # revealed: int + reveal_type(a * x) # revealed: int + reveal_type(a // x) # revealed: int + reveal_type(a / x) # revealed: int | float + reveal_type(a % x) # revealed: int + + def both_are_bool(x: bool, y: bool): + reveal_type(x + y) # revealed: int + reveal_type(x - y) # revealed: int + reveal_type(x * y) # revealed: int + reveal_type(x // y) # revealed: int + reveal_type(x / y) # revealed: int | float + reveal_type(x % y) # revealed: int +``` + +## Bitwise operations with a variable + +```py +import random + +def _(a: bool): + def lhs_is_int(x: int): + reveal_type(x | a) # revealed: int + reveal_type(x & a) # revealed: int + reveal_type(x ^ a) # revealed: int + + def rhs_is_int(x: int): + reveal_type(a | x) # revealed: int + reveal_type(a & x) # revealed: int + reveal_type(a ^ x) # revealed: int + + def lhs_is_int_literal(): + reveal_type(0 | a) # revealed: int + reveal_type(0 & a) # revealed: int + reveal_type(0 ^ a) # revealed: int + + reveal_type(1 | a) # revealed: int + reveal_type(1 & a) # revealed: int + reveal_type(1 ^ a) # revealed: int + + def lhs_is_true(): + reveal_type(True | a) # revealed: bool + reveal_type(True & a) # revealed: bool + reveal_type(True ^ a) # revealed: bool + + def rhs_is_true(): + reveal_type(a | True) # revealed: bool + reveal_type(a & True) # revealed: bool + reveal_type(a ^ True) # revealed: bool + + def lhs_is_false(): + reveal_type(False | a) # revealed: bool + reveal_type(False & a) # revealed: bool + reveal_type(False ^ a) # revealed: bool + + def rhs_is_false(): + reveal_type(a | False) # revealed: bool + reveal_type(a & False) # revealed: bool + reveal_type(a ^ False) # revealed: bool + + def both_are_bool(x: bool, y: bool): + reveal_type(x | y) # revealed: bool + reveal_type(x & y) # revealed: bool + reveal_type(x ^ y) # revealed: bool + + def lhs_is_int_literal_rhs_is_bool_literal(): + reveal_type(0 & True) # revealed: Literal[0] + reveal_type(0 | True) # revealed: Literal[1] + reveal_type(3 & True) # revealed: Literal[1] + reveal_type(3 | True) # revealed: Literal[3] + + reveal_type(0 & False) # revealed: Literal[0] + reveal_type(0 | False) # revealed: Literal[0] + reveal_type(3 & False) # revealed: Literal[0] + reveal_type(3 | False) # revealed: Literal[3] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/classes.md b/crates/ty_python_semantic/resources/mdtest/binary/classes.md new file mode 100644 index 0000000000000..89dceef0e28b1 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/binary/classes.md @@ -0,0 +1,27 @@ +# Binary operations on classes + +## Union of two classes + +Unioning two classes via the `|` operator is only available in Python 3.10 and later. + +```toml +[environment] +python-version = "3.10" +``` + +```py +class A: ... +class B: ... + +reveal_type(A | B) # revealed: UnionType +``` + +## Union of two classes (prior to 3.10) + +```py +class A: ... +class B: ... + +# error: "Operator `|` is unsupported between objects of type `` and ``" +reveal_type(A | B) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md new file mode 100644 index 0000000000000..d2587b7a75807 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -0,0 +1,379 @@ +# Custom binary operations + +## Class instances + +```py +from typing import Literal + +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + + def __sub__(self, other) -> Literal["-"]: + return "-" + + def __mul__(self, other) -> Literal["*"]: + return "*" + + def __matmul__(self, other) -> Literal["@"]: + return "@" + + def __truediv__(self, other) -> Literal["/"]: + return "/" + + def __mod__(self, other) -> Literal["%"]: + return "%" + + def __pow__(self, other) -> Literal["**"]: + return "**" + + def __lshift__(self, other) -> Literal["<<"]: + return "<<" + + def __rshift__(self, other) -> Literal[">>"]: + return ">>" + + def __or__(self, other) -> Literal["|"]: + return "|" + + def __xor__(self, other) -> Literal["^"]: + return "^" + + def __and__(self, other) -> Literal["&"]: + return "&" + + def __floordiv__(self, other) -> Literal["//"]: + return "//" + +class Sub(Yes): ... +class No: ... + +# Yes implements all of the dunder methods. +reveal_type(Yes() + Yes()) # revealed: Literal["+"] +reveal_type(Yes() - Yes()) # revealed: Literal["-"] +reveal_type(Yes() * Yes()) # revealed: Literal["*"] +reveal_type(Yes() @ Yes()) # revealed: Literal["@"] +reveal_type(Yes() / Yes()) # revealed: Literal["/"] +reveal_type(Yes() % Yes()) # revealed: Literal["%"] +reveal_type(Yes() ** Yes()) # revealed: Literal["**"] +reveal_type(Yes() << Yes()) # revealed: Literal["<<"] +reveal_type(Yes() >> Yes()) # revealed: Literal[">>"] +reveal_type(Yes() | Yes()) # revealed: Literal["|"] +reveal_type(Yes() ^ Yes()) # revealed: Literal["^"] +reveal_type(Yes() & Yes()) # revealed: Literal["&"] +reveal_type(Yes() // Yes()) # revealed: Literal["//"] + +# Sub inherits Yes's implementation of the dunder methods. +reveal_type(Sub() + Sub()) # revealed: Literal["+"] +reveal_type(Sub() - Sub()) # revealed: Literal["-"] +reveal_type(Sub() * Sub()) # revealed: Literal["*"] +reveal_type(Sub() @ Sub()) # revealed: Literal["@"] +reveal_type(Sub() / Sub()) # revealed: Literal["/"] +reveal_type(Sub() % Sub()) # revealed: Literal["%"] +reveal_type(Sub() ** Sub()) # revealed: Literal["**"] +reveal_type(Sub() << Sub()) # revealed: Literal["<<"] +reveal_type(Sub() >> Sub()) # revealed: Literal[">>"] +reveal_type(Sub() | Sub()) # revealed: Literal["|"] +reveal_type(Sub() ^ Sub()) # revealed: Literal["^"] +reveal_type(Sub() & Sub()) # revealed: Literal["&"] +reveal_type(Sub() // Sub()) # revealed: Literal["//"] + +# No does not implement any of the dunder methods. +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`" +reveal_type(No() + No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`" +reveal_type(No() - No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`" +reveal_type(No() * No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`" +reveal_type(No() @ No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`" +reveal_type(No() / No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`" +reveal_type(No() % No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`" +reveal_type(No() ** No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`" +reveal_type(No() << No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`" +reveal_type(No() >> No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`" +reveal_type(No() | No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`" +reveal_type(No() ^ No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`" +reveal_type(No() & No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`" +reveal_type(No() // No()) # revealed: Unknown + +# Yes does not implement any of the reflected dunder methods. +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() + Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() - Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() * Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() @ Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() / Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() % Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() ** Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() << Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() >> Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() | Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() ^ Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() & Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() // Yes()) # revealed: Unknown +``` + +## Subclass reflections override superclass dunders + +```py +from typing import Literal + +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + + def __sub__(self, other) -> Literal["-"]: + return "-" + + def __mul__(self, other) -> Literal["*"]: + return "*" + + def __matmul__(self, other) -> Literal["@"]: + return "@" + + def __truediv__(self, other) -> Literal["/"]: + return "/" + + def __mod__(self, other) -> Literal["%"]: + return "%" + + def __pow__(self, other) -> Literal["**"]: + return "**" + + def __lshift__(self, other) -> Literal["<<"]: + return "<<" + + def __rshift__(self, other) -> Literal[">>"]: + return ">>" + + def __or__(self, other) -> Literal["|"]: + return "|" + + def __xor__(self, other) -> Literal["^"]: + return "^" + + def __and__(self, other) -> Literal["&"]: + return "&" + + def __floordiv__(self, other) -> Literal["//"]: + return "//" + +class Sub(Yes): + def __radd__(self, other) -> Literal["r+"]: + return "r+" + + def __rsub__(self, other) -> Literal["r-"]: + return "r-" + + def __rmul__(self, other) -> Literal["r*"]: + return "r*" + + def __rmatmul__(self, other) -> Literal["r@"]: + return "r@" + + def __rtruediv__(self, other) -> Literal["r/"]: + return "r/" + + def __rmod__(self, other) -> Literal["r%"]: + return "r%" + + def __rpow__(self, other) -> Literal["r**"]: + return "r**" + + def __rlshift__(self, other) -> Literal["r<<"]: + return "r<<" + + def __rrshift__(self, other) -> Literal["r>>"]: + return "r>>" + + def __ror__(self, other) -> Literal["r|"]: + return "r|" + + def __rxor__(self, other) -> Literal["r^"]: + return "r^" + + def __rand__(self, other) -> Literal["r&"]: + return "r&" + + def __rfloordiv__(self, other) -> Literal["r//"]: + return "r//" + +class No: + def __radd__(self, other) -> Literal["r+"]: + return "r+" + + def __rsub__(self, other) -> Literal["r-"]: + return "r-" + + def __rmul__(self, other) -> Literal["r*"]: + return "r*" + + def __rmatmul__(self, other) -> Literal["r@"]: + return "r@" + + def __rtruediv__(self, other) -> Literal["r/"]: + return "r/" + + def __rmod__(self, other) -> Literal["r%"]: + return "r%" + + def __rpow__(self, other) -> Literal["r**"]: + return "r**" + + def __rlshift__(self, other) -> Literal["r<<"]: + return "r<<" + + def __rrshift__(self, other) -> Literal["r>>"]: + return "r>>" + + def __ror__(self, other) -> Literal["r|"]: + return "r|" + + def __rxor__(self, other) -> Literal["r^"]: + return "r^" + + def __rand__(self, other) -> Literal["r&"]: + return "r&" + + def __rfloordiv__(self, other) -> Literal["r//"]: + return "r//" + +# Subclass reflected dunder methods take precedence over the superclass's regular dunders. +reveal_type(Yes() + Sub()) # revealed: Literal["r+"] +reveal_type(Yes() - Sub()) # revealed: Literal["r-"] +reveal_type(Yes() * Sub()) # revealed: Literal["r*"] +reveal_type(Yes() @ Sub()) # revealed: Literal["r@"] +reveal_type(Yes() / Sub()) # revealed: Literal["r/"] +reveal_type(Yes() % Sub()) # revealed: Literal["r%"] +reveal_type(Yes() ** Sub()) # revealed: Literal["r**"] +reveal_type(Yes() << Sub()) # revealed: Literal["r<<"] +reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"] +reveal_type(Yes() | Sub()) # revealed: Literal["r|"] +reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"] +reveal_type(Yes() & Sub()) # revealed: Literal["r&"] +reveal_type(Yes() // Sub()) # revealed: Literal["r//"] + +# But for an unrelated class, the superclass regular dunders are used. +reveal_type(Yes() + No()) # revealed: Literal["+"] +reveal_type(Yes() - No()) # revealed: Literal["-"] +reveal_type(Yes() * No()) # revealed: Literal["*"] +reveal_type(Yes() @ No()) # revealed: Literal["@"] +reveal_type(Yes() / No()) # revealed: Literal["/"] +reveal_type(Yes() % No()) # revealed: Literal["%"] +reveal_type(Yes() ** No()) # revealed: Literal["**"] +reveal_type(Yes() << No()) # revealed: Literal["<<"] +reveal_type(Yes() >> No()) # revealed: Literal[">>"] +reveal_type(Yes() | No()) # revealed: Literal["|"] +reveal_type(Yes() ^ No()) # revealed: Literal["^"] +reveal_type(Yes() & No()) # revealed: Literal["&"] +reveal_type(Yes() // No()) # revealed: Literal["//"] +``` + +## Classes + +Dunder methods defined in a class are available to instances of that class, but not to the class +itself. (For these operators to work on the class itself, they would have to be defined on the +class's type, i.e. `type`.) + +```py +from typing import Literal + +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + +class Sub(Yes): ... +class No: ... + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +reveal_type(Yes + Yes) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +reveal_type(Sub + Sub) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `` and ``" +reveal_type(No + No) # revealed: Unknown +``` + +## Subclass + +```py +from typing import Literal + +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + +class Sub(Yes): ... +class No: ... + +def yes() -> type[Yes]: + return Yes + +def sub() -> type[Sub]: + return Sub + +def no() -> type[No]: + return No + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`" +reveal_type(yes() + yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`" +reveal_type(sub() + sub()) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`" +reveal_type(no() + no()) # revealed: Unknown +``` + +## Function literals + +```py +def f(): + pass + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f + f) # revealed: Unknown +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f - f) # revealed: Unknown +# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f * f) # revealed: Unknown +# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f @ f) # revealed: Unknown +# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f / f) # revealed: Unknown +# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f % f) # revealed: Unknown +# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f**f) # revealed: Unknown +# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f << f) # revealed: Unknown +# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f >> f) # revealed: Unknown +# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f | f) # revealed: Unknown +# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f ^ f) # revealed: Unknown +# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f & f) # revealed: Unknown +# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +reveal_type(f // f) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/in.md b/crates/ty_python_semantic/resources/mdtest/binary/in.md new file mode 100644 index 0000000000000..b85fef5f1d827 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/binary/in.md @@ -0,0 +1,49 @@ +# Static binary operations using `in` + +## Basic functionality + +This demonstrates type inference support for ` in `: + +```py +from ty_extensions import static_assert + +static_assert("foo" in ("quux", "foo", "baz")) +static_assert("foo" not in ("quux", "bar", "baz")) +``` + +## With variables + +```py +from ty_extensions import static_assert + +x = ("quux", "foo", "baz") +static_assert("foo" in x) + +x = ("quux", "bar", "baz") +static_assert("foo" not in x) +``` + +## Statically unknown results in a `bool` + +```py +def _(a: str, b: str): + reveal_type("foo" in (a, b)) # revealed: bool +``` + +## Values being unknown doesn't mean the result is unknown + +For example, when the types are completely disjoint: + +```py +from ty_extensions import static_assert + +def _(a: int, b: int): + static_assert("foo" not in (a, b)) +``` + +## Failure cases + +```py +# We don't support byte strings. +reveal_type(b"foo" not in (b"quux", b"foo", b"baz")) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/binary/instances.md rename to crates/ty_python_semantic/resources/mdtest/binary/instances.md index b435a34d76940..c27e70ed76e0f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -310,26 +310,21 @@ reveal_type(A() + 1) # revealed: A reveal_type(1 + A()) # revealed: A reveal_type(A() + "foo") # revealed: A -# TODO should be `A` since `str.__add__` doesn't support `A` instances -# TODO overloads -reveal_type("foo" + A()) # revealed: @Todo(return type of overloaded function) +reveal_type("foo" + A()) # revealed: A reveal_type(A() + b"foo") # revealed: A # TODO should be `A` since `bytes.__add__` doesn't support `A` instances reveal_type(b"foo" + A()) # revealed: bytes reveal_type(A() + ()) # revealed: A -# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances -reveal_type(() + A()) # revealed: @Todo(return type of overloaded function) +reveal_type(() + A()) # revealed: A literal_string_instance = "foo" * 1_000_000_000 # the test is not testing what it's meant to be testing if this isn't a `LiteralString`: reveal_type(literal_string_instance) # revealed: LiteralString reveal_type(A() + literal_string_instance) # revealed: A -# TODO should be `A` since `str.__add__` doesn't support `A` instances -# TODO overloads -reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of overloaded function) +reveal_type(literal_string_instance + A()) # revealed: A ``` ## Operations involving instances of classes inheriting from `Any` @@ -392,13 +387,13 @@ class A(metaclass=Meta): ... class B(metaclass=Meta): ... reveal_type(A + B) # revealed: int -# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[A]` and `Literal[B]`" +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `` and ``" reveal_type(A - B) # revealed: Unknown reveal_type(A < B) # revealed: bool reveal_type(A > B) # revealed: bool -# error: [unsupported-operator] "Operator `<=` is not supported for types `Literal[A]` and `Literal[B]`" +# error: [unsupported-operator] "Operator `<=` is not supported for types `` and ``" reveal_type(A <= B) # revealed: Unknown reveal_type(A[0]) # revealed: str diff --git a/crates/ty_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md new file mode 100644 index 0000000000000..95561a295ee50 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/binary/integers.md @@ -0,0 +1,143 @@ +# Binary operations on integers + +## Basic Arithmetic + +```py +reveal_type(2 + 1) # revealed: Literal[3] +reveal_type(3 - 4) # revealed: Literal[-1] +reveal_type(3 * -1) # revealed: Literal[-3] +reveal_type(-3 // 3) # revealed: Literal[-1] +reveal_type(-3 / 3) # revealed: float +reveal_type(5 % 3) # revealed: Literal[2] +reveal_type(3 | 4) # revealed: Literal[7] +reveal_type(5 & 6) # revealed: Literal[4] +reveal_type(7 ^ 2) # revealed: Literal[5] + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`" +reveal_type(2 + "f") # revealed: Unknown + +def lhs(x: int): + reveal_type(x + 1) # revealed: int + reveal_type(x - 4) # revealed: int + reveal_type(x * -1) # revealed: int + reveal_type(x // 3) # revealed: int + reveal_type(x / 3) # revealed: int | float + reveal_type(x % 3) # revealed: int + +def rhs(x: int): + reveal_type(2 + x) # revealed: int + reveal_type(3 - x) # revealed: int + reveal_type(3 * x) # revealed: int + reveal_type(-3 // x) # revealed: int + reveal_type(-3 / x) # revealed: int | float + reveal_type(5 % x) # revealed: int + +def both(x: int): + reveal_type(x + x) # revealed: int + reveal_type(x - x) # revealed: int + reveal_type(x * x) # revealed: int + reveal_type(x // x) # revealed: int + reveal_type(x / x) # revealed: int | float + reveal_type(x % x) # revealed: int +``` + +## Power + +For power if the result fits in the int literal type it will be a Literal type. Otherwise the +outcome is int. + +```py +largest_u32 = 4_294_967_295 +reveal_type(2**2) # revealed: Literal[4] +reveal_type(1 ** (largest_u32 + 1)) # revealed: int +reveal_type(2**largest_u32) # revealed: int + +def variable(x: int): + reveal_type(x**2) # revealed: int + # TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching + reveal_type(2**x) # revealed: int + # TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching + reveal_type(x**x) # revealed: int +``` + +If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but +the second argument is >=0, an `int` is still returned: + +```py +reveal_type(1**0) # revealed: Literal[1] +reveal_type(0**1) # revealed: Literal[0] +reveal_type(0**0) # revealed: Literal[1] +reveal_type((-1) ** 2) # revealed: Literal[1] +reveal_type(2 ** (-1)) # revealed: float +reveal_type((-1) ** (-1)) # revealed: float +``` + +## Division and Modulus + +Division works differently in Python than in Rust. If the result is negative and there is a +remainder, the division rounds down (instead of towards zero). The remainder needs to be adjusted to +compensate so that `(lhs // rhs) * rhs + (lhs % rhs) == lhs`: + +```py +reveal_type(256 % 129) # revealed: Literal[127] +reveal_type(-256 % 129) # revealed: Literal[2] +reveal_type(256 % -129) # revealed: Literal[-2] +reveal_type(-256 % -129) # revealed: Literal[-127] + +reveal_type(129 % 16) # revealed: Literal[1] +reveal_type(-129 % 16) # revealed: Literal[15] +reveal_type(129 % -16) # revealed: Literal[-15] +reveal_type(-129 % -16) # revealed: Literal[-1] + +reveal_type(10 // 8) # revealed: Literal[1] +reveal_type(-10 // 8) # revealed: Literal[-2] +reveal_type(10 // -8) # revealed: Literal[-2] +reveal_type(-10 // -8) # revealed: Literal[1] + +reveal_type(10 // 6) # revealed: Literal[1] +reveal_type(-10 // 6) # revealed: Literal[-2] +reveal_type(10 // -6) # revealed: Literal[-2] +reveal_type(-10 // -6) # revealed: Literal[1] +``` + +## Division by Zero + +This error is really outside the current Python type system, because e.g. `int.__truediv__` and +friends are not annotated to indicate that it's an error, and we don't even have a facility to +permit such an annotation. So arguably divide-by-zero should be a lint error rather than a type +checker error. But we choose to go ahead and error in the cases that are very likely to be an error: +dividing something typed as `int` or `float` by something known to be `Literal[0]`. + +This isn't _definitely_ an error, because the object typed as `int` or `float` could be an instance +of a custom subclass which overrides division behavior to handle zero without error. But if this +unusual case occurs, the error can be avoided by explicitly typing the dividend as that safe custom +subclass; we only emit the error if the LHS type is exactly `int` or `float`, not if its a subclass. + +```py +a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero" +reveal_type(a) # revealed: float + +b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero" +reveal_type(b) # revealed: int + +c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero" +reveal_type(c) # revealed: int + +# error: "Cannot divide object of type `int` by zero" +reveal_type(int() / 0) # revealed: int | float + +# error: "Cannot divide object of type `Literal[1]` by zero" +reveal_type(1 / False) # revealed: float +# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" +True / False +# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" +bool(1) / False + +# error: "Cannot divide object of type `float` by zero" +reveal_type(1.0 / 0) # revealed: int | float + +class MyInt(int): ... + +# No error for a subclass of int +reveal_type(MyInt(3) / 0) # revealed: int | float +``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/tuples.md b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md new file mode 100644 index 0000000000000..947262fc691ab --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md @@ -0,0 +1,48 @@ +# Binary operations on tuples + +## Concatenation for heterogeneous tuples + +```py +reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]] +reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]] +reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]] +reveal_type(() + ()) # revealed: tuple[()] + +def _(x: tuple[int, str], y: tuple[None, tuple[int]]): + reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]] + reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str] +``` + +## Concatenation for homogeneous tuples + +```py +def _(x: tuple[int, ...], y: tuple[str, ...]): + reveal_type(x + x) # revealed: tuple[int, ...] + reveal_type(x + y) # revealed: tuple[int | str, ...] + reveal_type((1, 2) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]] + reveal_type(x + (3, 4)) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]] + reveal_type((1, 2) + x + (3, 4)) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]] + reveal_type((1, 2) + y + (3, 4) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]] +``` + +We get the same results even when we use a legacy type alias, even though this involves first +inferring the `tuple[...]` expression as a value form. (Doing so gives a generic alias of the +`tuple` type, but as a special case, we include the full detailed tuple element specification in +specializations of `tuple`.) + +```py +from typing import Literal + +OneTwo = tuple[Literal[1], Literal[2]] +ThreeFour = tuple[Literal[3], Literal[4]] +IntTuple = tuple[int, ...] +StrTuple = tuple[str, ...] + +def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour): + reveal_type(x + x) # revealed: tuple[int, ...] + reveal_type(x + y) # revealed: tuple[int | str, ...] + reveal_type(one_two + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]] + reveal_type(x + three_four) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]] + reveal_type(one_two + x + three_four) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]] + reveal_type(one_two + y + three_four + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/unions.md rename to crates/ty_python_semantic/resources/mdtest/binary/unions.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md rename to crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md index 6ad75f185bb35..f77eea2d3140f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md +++ b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md @@ -43,7 +43,7 @@ if True and (x := 1): ```py def _(flag: bool): - flag or (x := 1) or reveal_type(x) # revealed: Literal[1] + flag or (x := 1) or reveal_type(x) # revealed: Never # error: [unresolved-reference] flag or reveal_type(y) or (y := 1) # revealed: Unknown diff --git a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md rename to crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md index 3c41c72e440da..ab4f3ff0de064 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md +++ b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md @@ -292,3 +292,66 @@ reveal_type(a) # revealed: Unknown # Modifications allowed in this case: a = None ``` + +## In stub files + +In stub files, we have a minor modification to the rules above: we do not union with `Unknown` for +undeclared symbols. + +### Undeclared and bound + +`mod.pyi`: + +```pyi +MyInt = int + +class C: + MyStr = str +``` + +```py +from mod import MyInt, C + +reveal_type(MyInt) # revealed: +reveal_type(C.MyStr) # revealed: +``` + +### Undeclared and possibly unbound + +`mod.pyi`: + +```pyi +def flag() -> bool: + return True + +if flag(): + MyInt = int + + class C: + MyStr = str +``` + +```py +# error: [possibly-unbound-import] +# error: [possibly-unbound-import] +from mod import MyInt, C + +reveal_type(MyInt) # revealed: +reveal_type(C.MyStr) # revealed: +``` + +### Undeclared and unbound + +`mod.pyi`: + +```pyi +if False: + MyInt = int +``` + +```py +# error: [unresolved-import] +from mod import MyInt + +reveal_type(MyInt) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/annotation.md b/crates/ty_python_semantic/resources/mdtest/call/annotation.md similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/call/annotation.md rename to crates/ty_python_semantic/resources/mdtest/call/annotation.md index 709ad8e1a6d7a..937d4226fc4a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/annotation.md +++ b/crates/ty_python_semantic/resources/mdtest/call/annotation.md @@ -9,8 +9,8 @@ def _(c: Callable[[], int]): def _(c: Callable[[int, str], int]): reveal_type(c(1, "a")) # revealed: int - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`" - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`" + # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["a"]`" + # error: [invalid-argument-type] "Argument is incorrect: Expected `str`, found `Literal[1]`" reveal_type(c("a", 1)) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md new file mode 100644 index 0000000000000..c9bb6621777fa --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -0,0 +1,107 @@ +# Calling builtins + +## `bool` with incorrect arguments + +```py +class NotBool: + __bool__ = None + +# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2" +bool(1, 2) + +# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly. +bool(NotBool()) +``` + +## Calls to `type()` + +A single-argument call to `type()` returns an object that has the argument's meta-type. (This is +tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the +tests for the `__class__` attribute.) + +```py +reveal_type(type(1)) # revealed: +``` + +But a three-argument call to type creates a dynamic instance of the `type` class: + +```py +class Base: ... + +reveal_type(type("Foo", (), {})) # revealed: type + +reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type +``` + +Other numbers of arguments are invalid + +```py +# error: [no-matching-overload] "No overload of class `type` matches arguments" +type("Foo", ()) + +# error: [no-matching-overload] "No overload of class `type` matches arguments" +type("Foo", (), {}, weird_other_arg=42) +``` + +The following calls are also invalid, due to incorrect argument types: + +```py +class Base: ... + +# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`" +type(b"Foo", (), {}) + +# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found ``" +type("Foo", Base, {}) + +# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`" +type("Foo", (1, 2), {}) + +# TODO: this should be an error +type("Foo", (Base,), {b"attr": 1}) +``` + +## Calls to `str()` + +### Valid calls + +```py +str() +str("") +str(b"") +str(1) +str(object=1) + +str(b"M\xc3\xbcsli", "utf-8") +str(b"M\xc3\xbcsli", "utf-8", "replace") + +str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16") +str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore") + +str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8") +str(bytearray(), "utf-8") + +str(encoding="utf-8", object=b"M\xc3\xbcsli") +str(b"", errors="replace") +str(encoding="utf-8") +str(errors="replace") +``` + +### Invalid calls + +```py +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal[1]`" +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[2]`" +str(1, 2) + +# error: [no-matching-overload] +str(o=1) + +# First argument is not a bytes-like object: +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal["Müsli"]`" +str("Müsli", "utf-8") + +# Second argument is not a valid encoding: +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`" +str(b"M\xc3\xbcsli", b"utf-8") +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md rename to crates/ty_python_semantic/resources/mdtest/call/callable_instance.md index 9f43e7b621983..d6e36e061f908 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md @@ -85,7 +85,7 @@ class C: c = C() -# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`" +# error: 15 [invalid-argument-type] "Argument to bound method `__call__` is incorrect: Expected `int`, found `Literal["foo"]`" reveal_type(c("foo")) # revealed: int ``` @@ -99,7 +99,7 @@ class C: c = C() -# error: 13 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `C`" +# error: 13 [invalid-argument-type] "Argument to bound method `__call__` is incorrect: Expected `int`, found `C`" reveal_type(c()) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md new file mode 100644 index 0000000000000..c8bdd664cda20 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -0,0 +1,432 @@ +# Constructor + +When classes are instantiated, Python calls the metaclass's `__call__` method. The metaclass of most +Python classes is the class `builtins.type`. + +`type.__call__` calls the `__new__` method of the class, which is responsible for creating the +instance. `__init__` is then called on the constructed instance with the same arguments that were +passed to `__new__`. + +Both `__new__` and `__init__` are looked up using the descriptor protocol, i.e., `__get__` is called +if these attributes are descriptors. `__new__` is always treated as a static method, i.e., `cls` is +passed as the first argument. `__init__` has no special handling; it is fetched as a bound method +and called just like any other dunder method. + +`type.__call__` does other things too, but this is not yet handled by us. + +Since every class has `object` in it's MRO, the default implementations are `object.__new__` and +`object.__init__`. They have some special behavior, namely: + +- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for + `object`), no arguments are accepted and `TypeError` is raised if any are passed. +- If `__new__` is defined but `__init__` is not, `object.__init__` will allow arbitrary arguments! + +As of today there are a number of behaviors that we do not support: + +- `__new__` is assumed to return an instance of the class on which it is called +- User defined `__call__` on metaclass is ignored + +## Creating an instance of the `object` class itself + +Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods +as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves +differently depending on whether `__new__` is defined or not), we have to test the behavior of +`object` itself. + +```py +reveal_type(object()) # revealed: object + +# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1" +reveal_type(object(1)) # revealed: object +``` + +## No init or new + +```py +class Foo: ... + +reveal_type(Foo()) # revealed: Foo + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" +reveal_type(Foo(1)) # revealed: Foo +``` + +## `__new__` present on the class itself + +```py +class Foo: + def __new__(cls, x: int) -> "Foo": + return object.__new__(cls) + +reveal_type(Foo(1)) # revealed: Foo + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +reveal_type(Foo()) # revealed: Foo +# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 2, got 3" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## `__new__` present on a superclass + +If the `__new__` method is defined on a superclass, we can still infer the signature of the +constructor from it. + +```py +from typing_extensions import Self + +class Base: + def __new__(cls, x: int) -> Self: + return cls() + +class Foo(Base): ... + +reveal_type(Foo(1)) # revealed: Foo + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +reveal_type(Foo()) # revealed: Foo +# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 2, got 3" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## Conditional `__new__` + +```py +def _(flag: bool) -> None: + class Foo: + if flag: + def __new__(cls, x: int): ... + else: + def __new__(cls, x: int, y: int = 1): ... + + reveal_type(Foo(1)) # revealed: Foo + # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `int`, found `Literal["1"]`" + # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `int`, found `Literal["1"]`" + reveal_type(Foo("1")) # revealed: Foo + # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" + # error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" + reveal_type(Foo()) # revealed: Foo + # error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 2, got 3" + reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## A descriptor in place of `__new__` + +```py +class SomeCallable: + def __call__(self, cls, x: int) -> "Foo": + obj = object.__new__(cls) + obj.x = x + return obj + +class Descriptor: + def __get__(self, instance, owner) -> SomeCallable: + return SomeCallable() + +class Foo: + __new__: Descriptor = Descriptor() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +## A callable instance in place of `__new__` + +### Bound + +```py +class Callable: + def __call__(self, cls, x: int) -> "Foo": + return object.__new__(cls) + +class Foo: + __new__ = Callable() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +### Possibly Unbound + +#### Possibly unbound `__new__` method + +```py +def _(flag: bool) -> None: + class Foo: + if flag: + def __new__(cls): + return object.__new__(cls) + + # error: [possibly-unbound-implicit-call] + reveal_type(Foo()) # revealed: Foo + + # error: [possibly-unbound-implicit-call] + # error: [too-many-positional-arguments] + reveal_type(Foo(1)) # revealed: Foo +``` + +#### Possibly unbound `__call__` on `__new__` callable + +```py +def _(flag: bool) -> None: + class Callable: + if flag: + def __call__(self, cls, x: int) -> "Foo": + return object.__new__(cls) + + class Foo: + __new__ = Callable() + + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo(1)) # revealed: Foo + # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" + # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo()) # revealed: Foo +``` + +## `__init__` present on the class itself + +If the class has an `__init__` method, we can infer the signature of the constructor from it. + +```py +class Foo: + def __init__(self, x: int): ... + +reveal_type(Foo(1)) # revealed: Foo + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 2, got 3" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## `__init__` present on a superclass + +If the `__init__` method is defined on a superclass, we can still infer the signature of the +constructor from it. + +```py +class Base: + def __init__(self, x: int): ... + +class Foo(Base): ... + +reveal_type(Foo(1)) # revealed: Foo + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 2, got 3" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## Conditional `__init__` + +```py +def _(flag: bool) -> None: + class Foo: + if flag: + def __init__(self, x: int): ... + else: + def __init__(self, x: int, y: int = 1): ... + + reveal_type(Foo(1)) # revealed: Foo + # error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `int`, found `Literal["1"]`" + # error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `int`, found `Literal["1"]`" + reveal_type(Foo("1")) # revealed: Foo + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" + reveal_type(Foo()) # revealed: Foo + # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 2, got 3" + reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## A descriptor in place of `__init__` + +```py +class SomeCallable: + # TODO: at runtime `__init__` is checked to return `None` and + # a `TypeError` is raised if it doesn't. However, apparently + # this is not true when the descriptor is used as `__init__`. + # However, we may still want to check this. + def __call__(self, x: int) -> str: + return "a" + +class Descriptor: + def __get__(self, instance, owner) -> SomeCallable: + return SomeCallable() + +class Foo: + __init__: Descriptor = Descriptor() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +## A callable instance in place of `__init__` + +### Bound + +```py +class Callable: + def __call__(self, x: int) -> None: + pass + +class Foo: + __init__ = Callable() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +### Possibly Unbound + +```py +def _(flag: bool) -> None: + class Callable: + if flag: + def __call__(self, x: int) -> None: + pass + + class Foo: + __init__ = Callable() + + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo(1)) # revealed: Foo + # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" + # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo()) # revealed: Foo +``` + +## `__new__` and `__init__` both present + +### Identical signatures + +A common case is to have `__new__` and `__init__` with identical signatures (except for the first +argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect. + +At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are +incorrect. However, we decided that it is better to report errors for both methods, since after +fixing the `__new__` method, the user may forget to fix the `__init__` method. + +```py +class Foo: + def __new__(cls, x: int) -> "Foo": + return object.__new__(cls) + + def __init__(self, x: int): ... + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo + +reveal_type(Foo(1)) # revealed: Foo +``` + +### Compatible signatures + +But they can also be compatible, but not identical. We should correctly report errors only for the +mthod that would fail. + +```py +class Foo: + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def __init__(self, x: int) -> None: + self.x = x + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo +reveal_type(Foo(1)) # revealed: Foo + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 2, got 3" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +### Incompatible signatures + +```py +import abc + +class Foo: + def __new__(cls) -> "Foo": + return object.__new__(cls) + + def __init__(self, x): + self.x = 42 + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo + +# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" +reveal_type(Foo(42)) # revealed: Foo + +class Foo2: + def __new__(cls, x) -> "Foo2": + return object.__new__(cls) + + def __init__(self): + pass + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +reveal_type(Foo2()) # revealed: Foo2 + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" +reveal_type(Foo2(42)) # revealed: Foo2 + +class Foo3(metaclass=abc.ABCMeta): + def __new__(cls) -> "Foo3": + return object.__new__(cls) + + def __init__(self, x): + self.x = 42 + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo3()) # revealed: Foo3 + +# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2" +reveal_type(Foo3(42)) # revealed: Foo3 + +class Foo4(metaclass=abc.ABCMeta): + def __new__(cls, x) -> "Foo4": + return object.__new__(cls) + + def __init__(self): + pass + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +reveal_type(Foo4()) # revealed: Foo4 + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" +reveal_type(Foo4(42)) # revealed: Foo4 +``` + +### Lookup of `__new__` + +The `__new__` method is always invoked on the class itself, never on the metaclass. This is +different from how other dunder methods like `__lt__` are implicitly called (always on the +meta-type, never on the type itself). + +```py +from typing_extensions import Literal + +class Meta(type): + def __new__(mcls, name, bases, namespace, /, **kwargs): + return super().__new__(mcls, name, bases, namespace) + + def __lt__(cls, other) -> Literal[True]: + return True + +class C(metaclass=Meta): ... + +# No error is raised here, since we don't implicitly call `Meta.__new__` +reveal_type(C()) # revealed: C + +# Meta.__lt__ is implicitly called here: +reveal_type(C < C) # revealed: Literal[True] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/call/dunder.md rename to crates/ty_python_semantic/resources/mdtest/call/dunder.md index 1f31f8bbdda64..7eadb96dae9ea 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md @@ -59,6 +59,8 @@ ClassWithNormalDunder[0] ## Operating on instances +### Attaching dunder methods to instances in methods + When invoking a dunder method on an instance of a class, it is looked up on the class: ```py @@ -112,10 +114,41 @@ def _(flag: bool): this_fails = ThisFails() - # error: [call-possibly-unbound-method] + # error: [possibly-unbound-implicit-call] reveal_type(this_fails[0]) # revealed: Unknown | str ``` +### Dunder methods as class-level annotations with no value + +Class-level annotations with no value assigned are considered to be accessible on the class: + +```py +from typing import Callable + +class C: + __call__: Callable[..., None] + +C()() + +_: Callable[..., None] = C() +``` + +And of course the same is true if we have only an implicit assignment inside a method: + +```py +from typing import Callable + +class C: + def __init__(self): + self.__call__ = lambda *a, **kw: None + +# error: [call-non-callable] +C()() + +# error: [invalid-assignment] +_: Callable[..., None] = C() +``` + ## When the dunder is not a method A dunder can also be a non-method callable: @@ -222,7 +255,8 @@ class NotSubscriptable2: self.__getitem__ = external_getitem def _(union: NotSubscriptable1 | NotSubscriptable2): - # error: [non-subscriptable] + # error: [non-subscriptable] "Cannot subscript object of type `NotSubscriptable2` with no `__getitem__` method" + # error: [non-subscriptable] "Cannot subscript object of type `NotSubscriptable1` with no `__getitem__` method" union[0] ``` @@ -236,6 +270,6 @@ def _(flag: bool): return str(key) c = C() - # error: [call-possibly-unbound-method] + # error: [possibly-unbound-implicit-call] reveal_type(c[0]) # revealed: str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder_import.md b/crates/ty_python_semantic/resources/mdtest/call/dunder_import.md new file mode 100644 index 0000000000000..24e7db6449ae5 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder_import.md @@ -0,0 +1,91 @@ +# `__import__` + +The global function `__import__()` allows for dynamic imports. + +A few of its call patterns are recognized and resolved to literal module types instead of the +general `ModuleType`, which is used as the fallback for unrecognized call patterns and unresolvable +names. + +## Basic + +```py +reveal_type(__import__("sys")) # revealed: +reveal_type(__import__(name="shutil")) # revealed: + +reveal_type(__import__("nonexistent")) # revealed: ModuleType +reveal_type(__import__("collections.abc")) # revealed: ModuleType +reveal_type(__import__("fnmatch", globals())) # revealed: ModuleType +reveal_type(__import__("shelve", fromlist=[""])) # revealed: ModuleType +``` + +## Unions + +The specified name must be a string literal. Different modules must be imported explicitly. + +```py +def _(flag: bool): + if flag: + name = "sys" + else: + name = "os" + + reveal_type(name) # revealed: Literal["sys", "os"] + reveal_type(__import__(name)) # revealed: ModuleType + + if flag: + module = __import__("heapq") + else: + module = __import__("curses") + + reveal_type(module) # revealed: | +``` + +## Nested modules + +`main.py`: + +```py +# TODO: Should be `` +a = reveal_type(__import__("a.b.c")) # revealed: ModuleType + +# TODO: Should be `int`, `str`, `bytes` +# error: [unresolved-attribute] +reveal_type(a.a) # revealed: Unknown +# error: [unresolved-attribute] +reveal_type(a.b.b) # revealed: Unknown +# error: [unresolved-attribute] +reveal_type(a.b.c.c) # revealed: Unknown +``` + +`a/__init__.py`: + +```py +a: int = 1 +``` + +`a/b/__init__.py`: + +```py +b: str = "" +``` + +`a/b/c.py`: + +```py +c: bytes = b"" +``` + +## `importlib.import_module()` + +`importlib.import_module()` has similar semantics, but returns the submodule. + +```py +import importlib + +reveal_type(importlib.import_module("bisect")) # revealed: +reveal_type(importlib.import_module("os.path")) # revealed: +reveal_type(importlib.import_module(name="tempfile")) # revealed: + +reveal_type(importlib.import_module("nonexistent")) # revealed: ModuleType +reveal_type(importlib.import_module("config", "logging")) # revealed: ModuleType +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md new file mode 100644 index 0000000000000..847377ae00529 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -0,0 +1,329 @@ +# Call expression + +## Simple + +```py +def get_int() -> int: + return 42 + +reveal_type(get_int()) # revealed: int +``` + +## Async + +```py +async def get_int_async() -> int: + return 42 + +# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]` +reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType) +``` + +## Generic + +```toml +[environment] +python-version = "3.12" +``` + +```py +def get_int[T]() -> int: + return 42 + +reveal_type(get_int()) # revealed: int +``` + +## Decorated + +```py +from typing import Callable + +def foo() -> int: + return 42 + +def decorator(func) -> Callable[[], int]: + return foo + +@decorator +def bar() -> str: + return "bar" + +reveal_type(bar()) # revealed: int +``` + +## Invalid callable + +```py +nonsense = 123 +x = nonsense() # error: "Object of type `Literal[123]` is not callable" +``` + +## Potentially unbound function + +```py +def _(flag: bool): + if flag: + def foo() -> int: + return 42 + # error: [possibly-unresolved-reference] + reveal_type(foo()) # revealed: int +``` + +## Wrong argument type + +### Positional argument, positional-or-keyword parameter + +```py +def f(x: int) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +reveal_type(f("foo")) # revealed: int +``` + +### Positional argument, positional-only parameter + +```py +def f(x: int, /) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +reveal_type(f("foo")) # revealed: int +``` + +### Positional argument, variadic parameter + +```py +def f(*args: int) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +reveal_type(f("foo")) # revealed: int +``` + +### Keyword argument, positional-or-keyword parameter + +```py +def f(x: int) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +reveal_type(f(x="foo")) # revealed: int +``` + +### Keyword argument, keyword-only parameter + +```py +def f(*, x: int) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +reveal_type(f(x="foo")) # revealed: int +``` + +### Keyword argument, keywords parameter + +```py +def f(**kwargs: int) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +reveal_type(f(x="foo")) # revealed: int +``` + +### Correctly match keyword out-of-order + +```py +def f(x: int = 1, y: str = "foo") -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `Literal[2]`" +# error: 20 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["bar"]`" +reveal_type(f(y=2, x="bar")) # revealed: int +``` + +## Too many positional arguments + +### One too many + +```py +def f() -> int: + return 1 + +# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1" +reveal_type(f("foo")) # revealed: int +``` + +### Two too many + +```py +def f() -> int: + return 1 + +# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2" +reveal_type(f("foo", "bar")) # revealed: int +``` + +### No too-many-positional if variadic is taken + +```py +def f(*args: int) -> int: + return 1 + +reveal_type(f(1, 2, 3)) # revealed: int +``` + +### Multiple keyword arguments map to keyword variadic parameter + +```py +def f(**kwargs: int) -> int: + return 1 + +reveal_type(f(foo=1, bar=2)) # revealed: int +``` + +## Missing arguments + +### No defaults or variadic + +```py +def f(x: int) -> int: + return 1 + +# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" +reveal_type(f()) # revealed: int +``` + +### With default + +```py +def f(x: int, y: str = "foo") -> int: + return 1 + +# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" +reveal_type(f()) # revealed: int +``` + +### Defaulted argument is not required + +```py +def f(x: int = 1) -> int: + return 1 + +reveal_type(f()) # revealed: int +``` + +### With variadic + +```py +def f(x: int, *y: str) -> int: + return 1 + +# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" +reveal_type(f()) # revealed: int +``` + +### Variadic argument is not required + +```py +def f(*args: int) -> int: + return 1 + +reveal_type(f()) # revealed: int +``` + +### Keywords argument is not required + +```py +def f(**kwargs: int) -> int: + return 1 + +reveal_type(f()) # revealed: int +``` + +### Multiple + +```py +def f(x: int, y: int) -> int: + return 1 + +# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`" +reveal_type(f()) # revealed: int +``` + +## Unknown argument + +```py +def f(x: int) -> int: + return 1 + +# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`" +reveal_type(f(x=1, y=2)) # revealed: int +``` + +## Parameter already assigned + +```py +def f(x: int) -> int: + return 1 + +# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" +reveal_type(f(1, x=2)) # revealed: int +``` + +## Special functions + +Some functions require special handling in type inference. Here, we make sure that we still emit +proper diagnostics in case of missing or superfluous arguments. + +### `reveal_type` + +```py +from typing_extensions import reveal_type + +# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`" +reveal_type() + +# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2" +reveal_type(1, 2) +``` + +### `static_assert` + +```py +from ty_extensions import static_assert + +# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`" +static_assert() + +# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3" +static_assert(True, 2, 3) +``` + +### `len` + +```py +# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`" +len() + +# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2" +len([], 1) +``` + +### Type API predicates + +```py +from ty_extensions import is_subtype_of + +# error: [missing-argument] +is_subtype_of() + +# error: [missing-argument] +is_subtype_of(int) + +# error: [too-many-positional-arguments] +is_subtype_of(int, int, int) + +# error: [too-many-positional-arguments] +is_subtype_of(int, int, int, int) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md rename to crates/ty_python_semantic/resources/mdtest/call/getattr_static.md index 79b0c821d6403..a68bae12d2616 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md +++ b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md @@ -115,7 +115,7 @@ inspect.getattr_static() # error: [missing-argument] "No argument provided for required parameter `attr`" inspect.getattr_static(C()) -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`" +# error: [invalid-argument-type] "Argument to function `getattr_static` is incorrect: Expected `str`, found `Literal[1]`" inspect.getattr_static(C(), 1) # error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4" @@ -144,8 +144,7 @@ from typing import Any def _(a: Any, tuple_of_any: tuple[Any]): reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"] - # TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int` - # revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"] + # revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/call/invalid_syntax.md new file mode 100644 index 0000000000000..3d88ff2ebf1a1 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/invalid_syntax.md @@ -0,0 +1,44 @@ +# Invalid signatures + +## Multiple arguments with the same name + +We always map a keyword argument to the first parameter of that name. + +```py +# error: [invalid-syntax] "Duplicate parameter "x"" +def f(x: int, x: str) -> int: + return 1 + +# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`" +# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" +reveal_type(f(1, x=2)) # revealed: int +``` + +## Positional after non-positional + +When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them +to the valid order: + +```py +# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter" +def f(**kw: int, x: str) -> int: + return 1 + +# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `Literal[1]`" +reveal_type(f(1)) # revealed: int +``` + +## Non-defaulted after defaulted + +We emit a syntax diagnostic for this, but it doesn't cause any problems for binding. + +```py +# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default" +def f(x: int = 1, y: str) -> int: + return 1 + +reveal_type(f(y="foo")) # revealed: int +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`" +# error: [missing-argument] "No argument provided for required parameter `y` of function `f`" +reveal_type(f("foo")) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md new file mode 100644 index 0000000000000..3c5ebbeb4de8d --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -0,0 +1,573 @@ +# Methods + +## Background: Functions as descriptors + +> Note: See also this related section in the descriptor guide: [Functions and methods]. + +Say we have a simple class `C` with a function definition `f` inside its body: + +```py +class C: + def f(self, x: int) -> str: + return "a" +``` + +Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance +(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors +because they implement a `__get__` method. This is crucial in making sure that method calls work as +expected. In general, the signature of the `__get__` method in the descriptor protocol is +`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The +passed value for the `instance` argument depends on whether the attribute is accessed from the class +object (in which case it is `None`), or from an instance (in which case it is the instance of type +`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize: + +- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)` +- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)` + +Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the +function attribute. The way the special `__get__` method *on functions* works is as follows. In the +former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In +the latter case, it returns a *bound method* object: + +```py +from inspect import getattr_static + +reveal_type(getattr_static(C, "f")) # revealed: def f(self, x: int) -> str + +reveal_type(getattr_static(C, "f").__get__) # revealed: + +reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f(self, x: int) -> str +reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method C.f(x: int) -> str +``` + +In conclusion, this is why we see the following two types when accessing the `f` attribute on the +class object `C` and on an instance `C()`: + +```py +reveal_type(C.f) # revealed: def f(self, x: int) -> str +reveal_type(C().f) # revealed: bound method C.f(x: int) -> str +``` + +A bound method is a callable object that contains a reference to the `instance` that it was called +on (can be inspected via `__self__`), and the function object that it refers to (can be inspected +via `__func__`): + +```py +bound_method = C().f + +reveal_type(bound_method.__self__) # revealed: C +reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str +``` + +When we call the bound method, the `instance` is implicitly passed as the first argument (`self`): + +```py +reveal_type(C().f(1)) # revealed: str +reveal_type(bound_method(1)) # revealed: str +``` + +When we call the function object itself, we need to pass the `instance` explicitly: + +```py +C.f(1) # error: [missing-argument] + +reveal_type(C.f(C(), 1)) # revealed: str +``` + +When we access methods from derived classes, they will be bound to instances of the derived class: + +```py +class D(C): + pass + +reveal_type(D().f) # revealed: bound method D.f(x: int) -> str +``` + +If we access an attribute on a bound method object itself, it will defer to `types.MethodType`: + +```py +reveal_type(bound_method.__hash__) # revealed: bound method MethodType.__hash__() -> int +``` + +If an attribute is not available on the bound method object, it will be looked up on the underlying +function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound +methods, even though it is not available on `types.MethodType`: + +```py +reveal_type(bound_method.__kwdefaults__) # revealed: dict[str, Any] | None +``` + +## Basic method calls on class objects and instances + +```py +class Base: + def method_on_base(self, x: int | None) -> str: + return "a" + +class Derived(Base): + def method_on_derived(self, x: bytes) -> tuple[int, str]: + return (1, "a") + +reveal_type(Base().method_on_base(1)) # revealed: str +reveal_type(Base.method_on_base(Base(), 1)) # revealed: str + +Base().method_on_base("incorrect") # error: [invalid-argument-type] +Base().method_on_base() # error: [missing-argument] +Base().method_on_base(1, 2) # error: [too-many-positional-arguments] + +reveal_type(Derived().method_on_base(1)) # revealed: str +reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str] +reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str +reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str] +``` + +## Method calls on literals + +### Boolean literals + +```py +reveal_type(True.bit_length()) # revealed: int +reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]] +``` + +### Integer literals + +```py +reveal_type((42).bit_length()) # revealed: int +``` + +### String literals + +```py +reveal_type("abcde".find("abc")) # revealed: int +reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes + +"abcde".find(123) # error: [invalid-argument-type] +``` + +### Bytes literals + +```py +reveal_type(b"abcde".startswith(b"abc")) # revealed: bool +``` + +## Method calls on `LiteralString` + +```py +from typing_extensions import LiteralString + +def f(s: LiteralString) -> None: + reveal_type(s.find("a")) # revealed: int +``` + +## Method calls on `tuple` + +```py +def f(t: tuple[int, str]) -> None: + reveal_type(t.index("a")) # revealed: int +``` + +## Method calls on unions + +```py +from typing import Any + +class A: + def f(self) -> int: + return 1 + +class B: + def f(self) -> str: + return "a" + +def f(a_or_b: A | B, any_or_a: Any | A): + reveal_type(a_or_b.f) # revealed: (bound method A.f() -> int) | (bound method B.f() -> str) + reveal_type(a_or_b.f()) # revealed: int | str + + reveal_type(any_or_a.f) # revealed: Any | (bound method A.f() -> int) + reveal_type(any_or_a.f()) # revealed: Any | int +``` + +## Method calls on `KnownInstance` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +type IntOrStr = int | str + +reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm +``` + +## Method calls on types not disjoint from `None` + +Very few methods are defined on `object`, `None`, and other types not disjoint from `None`. However, +descriptor-binding behaviour works on these types in exactly the same way as descriptor binding on +other types. This is despite the fact that `None` is used as a sentinel internally by the descriptor +protocol to indicate that a method was accessed on the class itself rather than an instance of the +class: + +```py +from typing import Protocol, Literal +from ty_extensions import AlwaysFalsy + +class Foo: ... + +class SupportsStr(Protocol): + def __str__(self) -> str: ... + +class Falsy(Protocol): + def __bool__(self) -> Literal[False]: ... + +def _(a: object, b: SupportsStr, c: Falsy, d: AlwaysFalsy, e: None, f: Foo | None): + a.__str__() + b.__str__() + c.__str__() + d.__str__() + # TODO: these should not error + e.__str__() # error: [missing-argument] + f.__str__() # error: [missing-argument] +``` + +## Error cases: Calling `__get__` for methods + +The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed: + +```pyi +from types import FunctionType, MethodType +from typing import overload + +@overload +def __get__(self, instance: None, owner: type, /) -> FunctionType: ... +@overload +def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... +``` + +Here, we test that this signature is enforced correctly: + +```py +from inspect import getattr_static + +class C: + def f(self, x: int) -> str: + return "a" + +method_wrapper = getattr_static(C, "f").__get__ + +reveal_type(method_wrapper) # revealed: + +# All of these are fine: +method_wrapper(C(), C) +method_wrapper(C()) +method_wrapper(C(), None) +method_wrapper(None, C) + +reveal_type(object.__str__.__get__(object(), None)()) # revealed: str + +# TODO: passing `None` without an `owner` argument fails at runtime. +# Ideally we would emit a diagnostic here: +method_wrapper(None) + +# Passing something that is not assignable to `type` as the `owner` argument is an +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" +method_wrapper(None, 1) + +# TODO: passing `None` as the `owner` argument when `instance` is `None` fails at runtime. +# Ideally we would emit a diagnostic here. +method_wrapper(None, None) + +# Calling `__get__` without any arguments is an +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" +method_wrapper() + +# Calling `__get__` with too many positional arguments is an +# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" +method_wrapper(C(), C, "one too many") +``` + +## Fallback to metaclass + +When a method is accessed on a class object, it is looked up on the metaclass if it is not found on +the class itself. This also creates a bound method that is bound to the class object itself: + +```py +from __future__ import annotations + +class Meta(type): + def f(cls, arg: int) -> str: + return "a" + +class C(metaclass=Meta): + pass + +reveal_type(C.f) # revealed: bound method .f(arg: int) -> str +reveal_type(C.f(1)) # revealed: str +``` + +The method `f` can not be accessed from an instance of the class: + +```py +# error: [unresolved-attribute] "Type `C` has no attribute `f`" +C().f +``` + +A metaclass function can be shadowed by a method on the class: + +```py +from typing import Any, Literal + +class D(metaclass=Meta): + def f(arg: int) -> Literal["a"]: + return "a" + +reveal_type(D.f(1)) # revealed: Literal["a"] +``` + +If the class method is possibly unbound, we union the return types: + +```py +def flag() -> bool: + return True + +class E(metaclass=Meta): + if flag(): + def f(arg: int) -> Any: + return "a" + +reveal_type(E.f(1)) # revealed: str | Any +``` + +## `@classmethod` + +### Basic + +When a `@classmethod` attribute is accessed, it returns a bound method object, even when accessed on +the class object itself: + +```py +from __future__ import annotations + +class C: + @classmethod + def f(cls: type[C], x: int) -> str: + return "a" + +reveal_type(C.f) # revealed: bound method .f(x: int) -> str +reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str +``` + +The `cls` method argument is then implicitly passed as the first argument when calling the method: + +```py +reveal_type(C.f(1)) # revealed: str +reveal_type(C().f(1)) # revealed: str +``` + +When the class method is called incorrectly, we detect it: + +```py +C.f("incorrect") # error: [invalid-argument-type] +C.f() # error: [missing-argument] +C.f(1, 2) # error: [too-many-positional-arguments] +``` + +If the `cls` parameter is wrongly annotated, we emit an error at the call site: + +```py +class D: + @classmethod + def f(cls: D): + # This function is wrongly annotated, it should be `type[D]` instead of `D` + pass + +# error: [invalid-argument-type] "Argument to bound method `f` is incorrect: Expected `D`, found ``" +D.f() +``` + +When a class method is accessed on a derived class, it is bound to that derived class: + +```py +class Derived(C): + pass + +reveal_type(Derived.f) # revealed: bound method .f(x: int) -> str +reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str + +reveal_type(Derived.f(1)) # revealed: str +reveal_type(Derived().f(1)) # revealed: str +``` + +### Accessing the classmethod as a static member + +Accessing a `@classmethod`-decorated function at runtime returns a `classmethod` object. We +currently don't model this explicitly: + +```py +from inspect import getattr_static + +class C: + @classmethod + def f(cls): ... + +reveal_type(getattr_static(C, "f")) # revealed: def f(cls) -> Unknown +reveal_type(getattr_static(C, "f").__get__) # revealed: +``` + +But we correctly model how the `classmethod` descriptor works: + +```py +reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method .f() -> Unknown +reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method .f() -> Unknown +reveal_type(getattr_static(C, "f").__get__(C())) # revealed: bound method type[C].f() -> Unknown +``` + +The `owner` argument takes precedence over the `instance` argument: + +```py +reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method .f() -> Unknown +``` + +### Classmethods mixed with other decorators + +```toml +[environment] +python-version = "3.12" +``` + +When a `@classmethod` is additionally decorated with another decorator, it is still treated as a +class method: + +```py +from __future__ import annotations + +def does_nothing[T](f: T) -> T: + return f + +class C: + @classmethod + @does_nothing + def f1(cls: type[C], x: int) -> str: + return "a" + + @does_nothing + @classmethod + def f2(cls: type[C], x: int) -> str: + return "a" + +reveal_type(C.f1(1)) # revealed: str +reveal_type(C().f1(1)) # revealed: str +reveal_type(C.f2(1)) # revealed: str +reveal_type(C().f2(1)) # revealed: str +``` + +## `@staticmethod` + +### Basic + +When a `@staticmethod` attribute is accessed, it returns the underlying function object. This is +true whether it's accessed on the class or on an instance of the class. + +```py +from __future__ import annotations + +class C: + @staticmethod + def f(x: int) -> str: + return "a" + +reveal_type(C.f) # revealed: def f(x: int) -> str +reveal_type(C().f) # revealed: def f(x: int) -> str +``` + +The method can then be called like a regular function from either the class or an instance, with no +implicit first argument passed. + +```py +reveal_type(C.f(1)) # revealed: str +reveal_type(C().f(1)) # revealed: str +``` + +When the static method is called incorrectly, we detect it: + +```py +C.f("incorrect") # error: [invalid-argument-type] +C.f() # error: [missing-argument] +C.f(1, 2) # error: [too-many-positional-arguments] +``` + +When a static method is accessed on a derived class, it behaves identically: + +```py +class Derived(C): + pass + +reveal_type(Derived.f) # revealed: def f(x: int) -> str +reveal_type(Derived().f) # revealed: def f(x: int) -> str + +reveal_type(Derived.f(1)) # revealed: str +reveal_type(Derived().f(1)) # revealed: str +``` + +### Accessing the staticmethod as a static member + +```py +from inspect import getattr_static + +class C: + @staticmethod + def f(): ... +``` + +Accessing the staticmethod as a static member. This will reveal the raw function, as `staticmethod` +is transparent when accessed via `getattr_static`. + +```py +reveal_type(getattr_static(C, "f")) # revealed: def f() -> Unknown +``` + +The `__get__` of a `staticmethod` object simply returns the underlying function. It ignores both the +instance and owner arguments. + +```py +reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f() -> Unknown +reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: def f() -> Unknown +reveal_type(getattr_static(C, "f").__get__(C())) # revealed: def f() -> Unknown +reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: def f() -> Unknown +``` + +### Staticmethods mixed with other decorators + +```toml +[environment] +python-version = "3.12" +``` + +When a `@staticmethod` is additionally decorated with another decorator, it is still treated as a +static method: + +```py +from __future__ import annotations + +def does_nothing[T](f: T) -> T: + return f + +class C: + @staticmethod + @does_nothing + def f1(x: int) -> str: + return "a" + + @does_nothing + @staticmethod + def f2(x: int) -> str: + return "a" + +reveal_type(C.f1(1)) # revealed: str +reveal_type(C().f1(1)) # revealed: str +reveal_type(C.f2(1)) # revealed: str +reveal_type(C().f2(1)) # revealed: str +``` + +[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/never.md b/crates/ty_python_semantic/resources/mdtest/call/never.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/never.md rename to crates/ty_python_semantic/resources/mdtest/call/never.md diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md new file mode 100644 index 0000000000000..a66e28ad7e366 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -0,0 +1,916 @@ +# Overloads + +When ty evaluates the call of an overloaded function, it attempts to "match" the supplied arguments +with one or more overloads. This document describes the algorithm that it uses for overload +matching, which is the same as the one mentioned in the +[spec](https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation). + +## Arity check + +The first step is to perform arity check. The non-overloaded cases are described in the +[function](./function.md) document. + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def f() -> None: ... +@overload +def f(x: int) -> int: ... +``` + +```py +from overloaded import f + +# These match a single overload +reveal_type(f()) # revealed: None +reveal_type(f(1)) # revealed: int + +# error: [no-matching-overload] "No overload of function `f` matches arguments" +reveal_type(f("a", "b")) # revealed: Unknown +``` + +## Type checking + +The second step is to perform type checking. This is done for all the overloads that passed the +arity check. + +### Single match + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +@overload +def f(x: bytes) -> bytes: ... +``` + +Here, all of the calls below pass the arity check for all overloads, so we proceed to type checking +which filters out all but the matching overload: + +```py +from overloaded import f + +reveal_type(f(1)) # revealed: int +reveal_type(f("a")) # revealed: str +reveal_type(f(b"b")) # revealed: bytes +``` + +### Single match error + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def f() -> None: ... +@overload +def f(x: int) -> int: ... +@overload +def f(x: int, y: int) -> int: ... +``` + +If the arity check only matches a single overload, it should be evaluated as a regular +(non-overloaded) function call. This means that any diagnostics resulted during type checking that +call should be reported directly and not as a `no-matching-overload` error. + +```py +from typing_extensions import reveal_type + +from overloaded import f + +reveal_type(f()) # revealed: None + +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`" +reveal_type(f("a")) # revealed: Unknown +``` + +More examples of this diagnostic can be found in the +[single_matching_overload.md](../diagnostics/single_matching_overload.md) document. + +### Multiple matches + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B(A): ... + +@overload +def f(x: A) -> A: ... +@overload +def f(x: B, y: int = 0) -> B: ... +``` + +```py +from overloaded import A, B, f + +# These calls pass the arity check, and type checking matches both overloads: +reveal_type(f(A())) # revealed: A +reveal_type(f(B())) # revealed: A + +# But, in this case, the arity check filters out the first overload, so we only have one match: +reveal_type(f(B(), 1)) # revealed: B +``` + +## Argument type expansion + +This step is performed only if the previous steps resulted in **no matches**. + +In this case, the algorithm would perform +[argument type expansion](https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion) +and loops over from the type checking step, evaluating the argument lists. + +### Expanding the only argument + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... +class C: ... + +@overload +def f(x: A) -> A: ... +@overload +def f(x: B) -> B: ... +@overload +def f(x: C) -> C: ... +``` + +```py +from overloaded import A, B, C, f + +def _(ab: A | B, ac: A | C, bc: B | C): + reveal_type(f(ab)) # revealed: A | B + reveal_type(f(bc)) # revealed: B | C + reveal_type(f(ac)) # revealed: A | C +``` + +### Expanding first argument + +If the set of argument lists created by expanding the first argument evaluates successfully, the +algorithm shouldn't expand the second argument. + +`overloaded.pyi`: + +```pyi +from typing import Literal, overload + +class A: ... +class B: ... +class C: ... +class D: ... + +@overload +def f(x: A, y: C) -> A: ... +@overload +def f(x: A, y: D) -> B: ... +@overload +def f(x: B, y: C) -> C: ... +@overload +def f(x: B, y: D) -> D: ... +``` + +```py +from overloaded import A, B, C, D, f + +def _(a_b: A | B): + reveal_type(f(a_b, C())) # revealed: A | C + reveal_type(f(a_b, D())) # revealed: B | D + +# But, if it doesn't, it should expand the second argument and try again: +def _(a_b: A | B, c_d: C | D): + reveal_type(f(a_b, c_d)) # revealed: A | B | C | D +``` + +### Expanding second argument + +If the first argument cannot be expanded, the algorithm should move on to the second argument, +keeping the first argument as is. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... +class C: ... +class D: ... + +@overload +def f(x: A, y: B) -> B: ... +@overload +def f(x: A, y: C) -> C: ... +@overload +def f(x: B, y: D) -> D: ... +``` + +```py +from overloaded import A, B, C, D, f + +def _(a: A, bc: B | C, cd: C | D): + # This also tests that partial matching works correctly as the argument type expansion results + # in matching the first and second overloads, but not the third one. + reveal_type(f(a, bc)) # revealed: B | C + + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(a, cd)) # revealed: Unknown +``` + +### Generics (legacy) + +`overloaded.pyi`: + +```pyi +from typing import TypeVar, overload + +_T = TypeVar("_T") + +class A: ... +class B: ... + +@overload +def f(x: A) -> A: ... +@overload +def f(x: _T) -> _T: ... +``` + +```py +from overloaded import A, f + +def _(x: int, y: A | int): + reveal_type(f(x)) # revealed: int + reveal_type(f(y)) # revealed: A | int +``` + +### Generics (PEP 695) + +```toml +[environment] +python-version = "3.12" +``` + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... + +@overload +def f(x: B) -> B: ... +@overload +def f[T](x: T) -> T: ... +``` + +```py +from overloaded import B, f + +def _(x: int, y: B | int): + reveal_type(f(x)) # revealed: int + reveal_type(f(y)) # revealed: B | int +``` + +### Expanding `bool` + +`overloaded.pyi`: + +```pyi +from typing import Literal, overload + +class T: ... +class F: ... + +@overload +def f(x: Literal[True]) -> T: ... +@overload +def f(x: Literal[False]) -> F: ... +``` + +```py +from overloaded import f + +def _(flag: bool): + reveal_type(f(True)) # revealed: T + reveal_type(f(False)) # revealed: F + reveal_type(f(flag)) # revealed: T | F +``` + +### Expanding `tuple` + +`overloaded.pyi`: + +```pyi +from typing import Literal, overload + +class A: ... +class B: ... +class C: ... +class D: ... + +@overload +def f(x: tuple[A, int], y: tuple[int, Literal[True]]) -> A: ... +@overload +def f(x: tuple[A, int], y: tuple[int, Literal[False]]) -> B: ... +@overload +def f(x: tuple[B, int], y: tuple[int, Literal[True]]) -> C: ... +@overload +def f(x: tuple[B, int], y: tuple[int, Literal[False]]) -> D: ... +``` + +```py +from overloaded import A, B, f + +def _(x: tuple[A | B, int], y: tuple[int, bool]): + reveal_type(f(x, y)) # revealed: A | B | C | D +``` + +### Expanding `type` + +There's no special handling for expanding `type[A | B]` type because ty stores this type in it's +distributed form, which is `type[A] | type[B]`. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... + +@overload +def f(x: type[A]) -> A: ... +@overload +def f(x: type[B]) -> B: ... +``` + +```py +from overloaded import A, B, f + +def _(x: type[A | B]): + reveal_type(x) # revealed: type[A] | type[B] + reveal_type(f(x)) # revealed: A | B +``` + +### Expanding enums + +`overloaded.pyi`: + +```pyi +from enum import Enum +from typing import Literal, overload + +class SomeEnum(Enum): + A = 1 + B = 2 + C = 3 + + +class A: ... +class B: ... +class C: ... + +@overload +def f(x: Literal[SomeEnum.A]) -> A: ... +@overload +def f(x: Literal[SomeEnum.B]) -> B: ... +@overload +def f(x: Literal[SomeEnum.C]) -> C: ... +``` + +```py +from overloaded import SomeEnum, A, B, C, f + +def _(x: SomeEnum): + reveal_type(f(SomeEnum.A)) # revealed: A + # TODO: This should be `B` once enums are supported and are expanded + reveal_type(f(SomeEnum.B)) # revealed: A + # TODO: This should be `C` once enums are supported and are expanded + reveal_type(f(SomeEnum.C)) # revealed: A + # TODO: This should be `A | B | C` once enums are supported and are expanded + reveal_type(f(x)) # revealed: A +``` + +### No matching overloads + +> If argument expansion has been applied to all arguments and one or more of the expanded argument +> lists cannot be evaluated successfully, generate an error and stop. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... +class C: ... +class D: ... + +@overload +def f(x: A) -> A: ... +@overload +def f(x: B) -> B: ... +``` + +```py +from overloaded import A, B, C, D, f + +def _(ab: A | B, ac: A | C, cd: C | D): + reveal_type(f(ab)) # revealed: A | B + + # The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first + # overload while the second list doesn't match any of the overloads, so we generate an + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(ac)) # revealed: Unknown + + # None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(cd)) # revealed: Unknown +``` + +## Filtering overloads with variadic arguments and parameters + +TODO + +## Filtering based on `Any` / `Unknown` + +This is the step 5 of the overload call evaluation algorithm which specifies that: + +> For all arguments, determine whether all possible materializations of the argument’s type are +> assignable to the corresponding parameter type for each of the remaining overloads. If so, +> eliminate all of the subsequent remaining overloads. + +This is only performed if the previous step resulted in more than one matching overload. + +### Single list argument + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +@overload +def f(x: list[int]) -> int: ... +@overload +def f(x: list[Any]) -> int: ... +@overload +def f(x: Any) -> str: ... +``` + +For the above definition, anything other than `list` should match the last overload: + +```py +from typing import Any + +from overloaded import f + +# Anything other than `list` should match the last overload +reveal_type(f(1)) # revealed: str + +def _(list_int: list[int], list_any: list[Any]): + reveal_type(f(list_int)) # revealed: int + reveal_type(f(list_any)) # revealed: int +``` + +### Single list argument (ambiguous) + +The overload definition is the same as above, but the return type of the second overload is changed +to `str` to make the overload matching ambiguous if the argument is a `list[Any]`. + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +@overload +def f(x: list[int]) -> int: ... +@overload +def f(x: list[Any]) -> str: ... +@overload +def f(x: Any) -> str: ... +``` + +```py +from typing import Any + +from overloaded import f + +# Anything other than `list` should match the last overload +reveal_type(f(1)) # revealed: str + +def _(list_int: list[int], list_any: list[Any]): + # All materializations of `list[int]` are assignable to `list[int]`, so it matches the first + # overload. + reveal_type(f(list_int)) # revealed: int + + # All materializations of `list[Any]` are assignable to `list[int]` and `list[Any]`, but the + # return type of first and second overloads are not equivalent, so the overload matching + # is ambiguous. + reveal_type(f(list_any)) # revealed: Unknown +``` + +### Single tuple argument + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +@overload +def f(x: tuple[int, str]) -> int: ... +@overload +def f(x: tuple[int, Any]) -> int: ... +@overload +def f(x: Any) -> str: ... +``` + +```py +from typing import Any + +from overloaded import f + +reveal_type(f("a")) # revealed: str +reveal_type(f((1, "b"))) # revealed: int +reveal_type(f((1, 2))) # revealed: int + +def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]): + # All materializations are assignable to first overload, so second and third overloads are + # eliminated + reveal_type(f(int_str)) # revealed: int + + # All materializations are assignable to second overload, so the third overload is eliminated; + # the return type of first and second overload is equivalent + reveal_type(f(int_any)) # revealed: int + + # All materializations of `tuple[Any, Any]` are assignable to the parameters of all the + # overloads, but the return types aren't equivalent, so the overload matching is ambiguous + reveal_type(f(any_any)) # revealed: Unknown +``` + +### Multiple arguments + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +class A: ... +class B: ... + +@overload +def f(x: list[int], y: tuple[int, str]) -> A: ... +@overload +def f(x: list[Any], y: tuple[int, Any]) -> A: ... +@overload +def f(x: list[Any], y: tuple[Any, Any]) -> B: ... +``` + +```py +from typing import Any + +from overloaded import A, f + +def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]): + # All materializations of both argument types are assignable to the first overload, so the + # second and third overloads are filtered out + reveal_type(f(list_int, int_str)) # revealed: A + + # All materialization of first argument is assignable to first overload and for the second + # argument, they're assignable to the second overload, so the third overload is filtered out + reveal_type(f(list_int, int_any)) # revealed: A + + # All materialization of first argument is assignable to second overload and for the second + # argument, they're assignable to the first overload, so the third overload is filtered out + reveal_type(f(list_any, int_str)) # revealed: A + + # All materializations of both arguments are assignable to the second overload, so the third + # overload is filtered out + reveal_type(f(list_any, int_any)) # revealed: A + + # All materializations of first argument is assignable to the second overload and for the second + # argument, they're assignable to the third overload, so no overloads are filtered out; the + # return types of the remaining overloads are not equivalent, so overload matching is ambiguous + reveal_type(f(list_int, any_any)) # revealed: Unknown +``` + +### `LiteralString` and `str` + +`overloaded.pyi`: + +```pyi +from typing import overload +from typing_extensions import LiteralString + +@overload +def f(x: LiteralString) -> LiteralString: ... +@overload +def f(x: str) -> str: ... +``` + +```py +from typing import Any +from typing_extensions import LiteralString + +from overloaded import f + +def _(literal: LiteralString, string: str, any: Any): + reveal_type(f(literal)) # revealed: LiteralString + reveal_type(f(string)) # revealed: str + + # `Any` matches both overloads, but the return types are not equivalent. + # Pyright and mypy both reveal `str` here, contrary to the spec. + reveal_type(f(any)) # revealed: Unknown +``` + +### Generics + +`overloaded.pyi`: + +```pyi +from typing import Any, TypeVar, overload + +_T = TypeVar("_T") + +class A: ... +class B: ... + +@overload +def f(x: list[int]) -> A: ... +@overload +def f(x: list[_T]) -> _T: ... +@overload +def f(x: Any) -> B: ... +``` + +```py +from typing import Any + +from overloaded import f + +def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any): + reveal_type(f(list_int)) # revealed: A + # TODO: Should be `str` + reveal_type(f(list_str)) # revealed: Unknown + reveal_type(f(list_any)) # revealed: Unknown + reveal_type(f(any)) # revealed: Unknown +``` + +### Generics (multiple arguments) + +`overloaded.pyi`: + +```pyi +from typing import Any, TypeVar, overload + +_T = TypeVar("_T") + +@overload +def f(x: int, y: Any) -> int: ... +@overload +def f(x: str, y: _T) -> _T: ... +``` + +```py +from typing import Any + +from overloaded import f + +def _(integer: int, string: str, any: Any, list_any: list[Any]): + reveal_type(f(integer, string)) # revealed: int + reveal_type(f(string, integer)) # revealed: int + + # This matches the second overload and is _not_ the case of ambiguous overload matching. + reveal_type(f(string, any)) # revealed: Any + + reveal_type(f(string, list_any)) # revealed: list[Any] +``` + +### Generic `self` + +`overloaded.pyi`: + +```pyi +from typing import Any, overload, TypeVar, Generic + +_T = TypeVar("_T") + +class A(Generic[_T]): + @overload + def method(self: "A[int]") -> int: ... + @overload + def method(self: "A[Any]") -> int: ... + +class B(Generic[_T]): + @overload + def method(self: "B[int]") -> int: ... + @overload + def method(self: "B[Any]") -> str: ... +``` + +```py +from typing import Any + +from overloaded import A, B + +def _(a_int: A[int], a_str: A[str], a_any: A[Any]): + reveal_type(a_int.method()) # revealed: int + reveal_type(a_str.method()) # revealed: int + reveal_type(a_any.method()) # revealed: int + +def _(b_int: B[int], b_str: B[str], b_any: B[Any]): + reveal_type(b_int.method()) # revealed: int + reveal_type(b_str.method()) # revealed: str + reveal_type(b_any.method()) # revealed: Unknown +``` + +### Variadic argument + +TODO: A variadic parameter is being assigned to a number of parameters of the same type + +### Non-participating fully-static parameter + +Ref: + +A non-participating parameter would be the one where the set of materializations of the argument +type, that are assignable to the parameter type at the same index, is same for the overloads for +which step 5 needs to be performed. + +`overloaded.pyi`: + +```pyi +from typing import Literal, overload + +@overload +def f(x: str, *, flag: Literal[True]) -> int: ... +@overload +def f(x: str, *, flag: Literal[False] = ...) -> str: ... +@overload +def f(x: str, *, flag: bool = ...) -> int | str: ... +``` + +In the following example, for the `f(any, flag=True)` call, the materializations of first argument +type `Any` that are assignable to `str` is same for overloads 1 and 3 (at the time of step 5), so +for the purposes of overload matching that parameter can be ignored. If `Any` materializes to +anything that's not assignable to `str`, all of the overloads would already be filtered out which +will raise a `no-matching-overload` error. + +```py +from typing import Any + +from overloaded import f + +def _(any: Any): + reveal_type(f(any, flag=True)) # revealed: int + reveal_type(f(any, flag=False)) # revealed: str +``` + +### Non-participating gradual parameter + +`overloaded.pyi`: + +```pyi +from typing import Any, Literal, overload + +@overload +def f(x: tuple[str, Any], *, flag: Literal[True]) -> int: ... +@overload +def f(x: tuple[str, Any], *, flag: Literal[False] = ...) -> str: ... +@overload +def f(x: tuple[str, Any], *, flag: bool = ...) -> int | str: ... +``` + +```py +from typing import Any + +from overloaded import f + +def _(any: Any): + reveal_type(f(any, flag=True)) # revealed: int + reveal_type(f(any, flag=False)) # revealed: str +``` + +### Argument type expansion + +This filtering can also happen for each of the expanded argument lists. + +#### No ambiguity + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +class A: ... +class B: ... + +@overload +def f(x: tuple[A, B]) -> A: ... +@overload +def f(x: tuple[B, A]) -> B: ... +@overload +def f(x: tuple[A, Any]) -> A: ... +@overload +def f(x: tuple[B, Any]) -> B: ... +``` + +Here, the argument `tuple[A | B, Any]` doesn't match any of the overloads, so we perform argument +type expansion which results in two argument lists: + +1. `tuple[A, Any]` +1. `tuple[B, Any]` + +The first argument list matches overload 1 and 3 via `Any` materialization for which the return +types are equivalent (`A`). Similarly, the second argument list matches overload 2 and 4 via `Any` +materialization for which the return types are equivalent (`B`). The final return type for the call +will be the union of the return types. + +```py +from typing import Any + +from overloaded import A, B, f + +def _(arg: tuple[A | B, Any]): + reveal_type(f(arg)) # revealed: A | B +``` + +#### One argument list ambiguous + +The example used here is same as the previous one, but the return type of the last overload is +changed so that it's not equivalent to the return type of the second overload, creating an ambiguous +matching for the second argument list. + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +class A: ... +class B: ... +class C: ... + +@overload +def f(x: tuple[A, B]) -> A: ... +@overload +def f(x: tuple[B, A]) -> B: ... +@overload +def f(x: tuple[A, Any]) -> A: ... +@overload +def f(x: tuple[B, Any]) -> C: ... +``` + +```py +from typing import Any + +from overloaded import A, B, C, f + +def _(arg: tuple[A | B, Any]): + reveal_type(f(arg)) # revealed: A | Unknown +``` + +#### Both argument lists ambiguous + +Here, both argument lists created by expanding the argument type are ambiguous, so the final return +type is `Any`. + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +class A: ... +class B: ... +class C: ... + +@overload +def f(x: tuple[A, B]) -> A: ... +@overload +def f(x: tuple[B, A]) -> B: ... +@overload +def f(x: tuple[A, Any]) -> C: ... +@overload +def f(x: tuple[B, Any]) -> C: ... +``` + +```py +from typing import Any + +from overloaded import A, B, C, f + +def _(arg: tuple[A | B, Any]): + reveal_type(f(arg)) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/str_startswith.md b/crates/ty_python_semantic/resources/mdtest/call/str_startswith.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/str_startswith.md rename to crates/ty_python_semantic/resources/mdtest/call/str_startswith.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md similarity index 84% rename from crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md rename to crates/ty_python_semantic/resources/mdtest/call/subclass_of.md index 9fe47b9b6c9e3..544c4c7c90bb0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md +++ b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md @@ -20,11 +20,11 @@ class C: def _(subclass_of_c: type[C]): reveal_type(subclass_of_c(1)) # revealed: C - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`" + # error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `int`, found `Literal["a"]`" reveal_type(subclass_of_c("a")) # revealed: C # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" reveal_type(subclass_of_c()) # revealed: C - # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" + # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 2, got 3" reveal_type(subclass_of_c(1, 2)) # revealed: C ``` @@ -32,7 +32,7 @@ def _(subclass_of_c: type[C]): ```py from typing import Any -from knot_extensions import Unknown +from ty_extensions import Unknown def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]): reveal_type(subclass_of_any()) # revealed: Any diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md new file mode 100644 index 0000000000000..b7df4043834f4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -0,0 +1,253 @@ +# Unions in calls + +## Union of return types + +```py +def _(flag: bool): + if flag: + def f() -> int: + return 1 + else: + def f() -> str: + return "foo" + reveal_type(f()) # revealed: int | str +``` + +## Calling with an unknown union + +```py +from nonexistent import f # error: [unresolved-import] "Cannot resolve imported module `nonexistent`" + +def coinflip() -> bool: + return True + +if coinflip(): + def f() -> int: + return 1 + +reveal_type(f()) # revealed: Unknown | int +``` + +## Non-callable elements in a union + +Calling a union with a non-callable element should emit a diagnostic. + +```py +def _(flag: bool): + if flag: + f = 1 + else: + def f() -> int: + return 1 + x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable" + reveal_type(x) # revealed: Unknown | int +``` + +## Multiple non-callable elements in a union + +Calling a union with multiple non-callable elements should mention all of them in the diagnostic. + +```py +def _(flag: bool, flag2: bool): + if flag: + f = 1 + elif flag2: + f = "foo" + else: + def f() -> int: + return 1 + # error: [call-non-callable] "Object of type `Literal[1]` is not callable" + # error: [call-non-callable] "Object of type `Literal["foo"]` is not callable" + # revealed: Unknown | int + reveal_type(f()) +``` + +## All non-callable union elements + +Calling a union with no callable elements can emit a simpler diagnostic. + +```py +def _(flag: bool): + if flag: + f = 1 + else: + f = "foo" + + x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable" + reveal_type(x) # revealed: Unknown +``` + +## Mismatching signatures + +Calling a union where the arguments don't match the signature of all variants. + +```py +def f1(a: int) -> int: + return a + +def f2(a: str) -> str: + return a + +def _(flag: bool): + if flag: + f = f1 + else: + f = f2 + + # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`" + x = f(3) + reveal_type(x) # revealed: int | str +``` + +## Any non-callable variant + +```py +def f1(a: int): ... +def _(flag: bool): + if flag: + f = f1 + else: + f = "This is a string literal" + + # error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable" + x = f(3) + reveal_type(x) # revealed: Unknown +``` + +## Union of binding errors + +```py +def f1(): ... +def f2(): ... +def _(flag: bool): + if flag: + f = f1 + else: + f = f2 + + # error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1" + # error: [too-many-positional-arguments] "Too many positional arguments to function `f2`: expected 0, got 1" + x = f(3) + reveal_type(x) # revealed: Unknown +``` + +## One not-callable, one wrong argument + +```py +class C: ... + +def f1(): ... +def _(flag: bool): + if flag: + f = f1 + else: + f = C() + + # error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1" + # error: [call-non-callable] "Object of type `C` is not callable" + x = f(3) + reveal_type(x) # revealed: Unknown +``` + +## Union including a special-cased function + +```py +def _(flag: bool): + if flag: + f = str + else: + f = repr + reveal_type(str("string")) # revealed: Literal["string"] + reveal_type(repr("string")) # revealed: Literal["'string'"] + reveal_type(f("string")) # revealed: Literal["string", "'string'"] +``` + +## Unions with literals and negations + +```py +from typing import Literal +from ty_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to + +static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy])) +static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy])) +static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""])) +static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""])) + +static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]])) +static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]])) +static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""])) +static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""])) + +def _( + a: Literal["a", ""] | Not[AlwaysFalsy], + b: Literal["a", ""] | Not[Literal[""]], + c: Literal[""] | Not[Literal[""]], + d: Not[Literal[""]] | Literal[""], + e: Literal["a"] | Not[Literal["a"]], + f: Literal[b"b"] | Not[Literal[b"b"]], + g: Not[Literal[b"b"]] | Literal[b"b"], + h: Literal[42] | Not[Literal[42]], + i: Not[Literal[42]] | Literal[42], +): + reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy + reveal_type(b) # revealed: object + reveal_type(c) # revealed: object + reveal_type(d) # revealed: object + reveal_type(e) # revealed: object + reveal_type(f) # revealed: object + reveal_type(g) # revealed: object + reveal_type(h) # revealed: object + reveal_type(i) # revealed: object +``` + +## Cannot use an argument as both a value and a type form + +```py +from ty_extensions import is_singleton + +def _(flag: bool): + if flag: + f = repr + else: + f = is_singleton + # error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call" + reveal_type(f(int)) # revealed: str | Literal[False] +``` + +## Size limit on unions of literals + +Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`, +`bytes`, `str`). + +```py +from typing import Literal + +def _(literals_2: Literal[0, 1], b: bool, flag: bool): + literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3] + literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15] + literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63] + literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127] + + # Going beyond the MAX_UNION_LITERALS limit (currently 200): + literals_256 = 16 * literals_16 + literals_16 + reveal_type(literals_256) # revealed: int + + # Going beyond the limit when another type is already part of the union + bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127] + literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255] + + # Now union the two: + reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int +``` + +## Simplifying gradually-equivalent types + +If two types are gradually equivalent, we can keep just one of them in a union: + +```py +from typing import Any, Union +from ty_extensions import Intersection, Not + +def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]): + reveal_type(x) # revealed: Any & ~int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md new file mode 100644 index 0000000000000..ecffca0fb65d1 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -0,0 +1,410 @@ +# Super + +Python defines the terms _bound super object_ and _unbound super object_. + +An **unbound super object** is created when `super` is called with only one argument. (e.g. +`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is +rarely used in practice. + +A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the +implicit form `super()`, where both the pivot class and the owner are inferred. This is the most +common usage. + +## Basic Usage + +### Explicit Super Object + +`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the +specified pivot class. + +```py +class A: + def a(self): ... + aa: int = 1 + +class B(A): + def b(self): ... + bb: int = 2 + +class C(B): + def c(self): ... + cc: int = 3 + +reveal_type(C.__mro__) # revealed: tuple[, , , ] + +super(C, C()).a +super(C, C()).b +# error: [unresolved-attribute] "Type `, C>` has no attribute `c`" +super(C, C()).c + +super(B, C()).a +# error: [unresolved-attribute] "Type `, C>` has no attribute `b`" +super(B, C()).b +# error: [unresolved-attribute] "Type `, C>` has no attribute `c`" +super(B, C()).c + +# error: [unresolved-attribute] "Type `, C>` has no attribute `a`" +super(A, C()).a +# error: [unresolved-attribute] "Type `, C>` has no attribute `b`" +super(A, C()).b +# error: [unresolved-attribute] "Type `, C>` has no attribute `c`" +super(A, C()).c + +reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown +reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown +reveal_type(super(C, C()).aa) # revealed: int +reveal_type(super(C, C()).bb) # revealed: int +``` + +### Implicit Super Object + +The implicit form `super()` is same as `super(__class__, )`. The `__class__` refers +to the class that contains the function where `super()` is used. The first argument refers to the +current method’s first parameter (typically `self` or `cls`). + +```py +from __future__ import annotations + +class A: + def __init__(self, a: int): ... + @classmethod + def f(cls): ... + +class B(A): + def __init__(self, a: int): + # TODO: Once `Self` is supported, this should be `, B>` + reveal_type(super()) # revealed: , Unknown> + super().__init__(a) + + @classmethod + def f(cls): + # TODO: Once `Self` is supported, this should be `, >` + reveal_type(super()) # revealed: , Unknown> + super().f() + +super(B, B(42)).__init__(42) +super(B, B).f() +``` + +### Unbound Super Object + +Calling `super(cls)` without a second argument returns an _unbound super object_. This is treated as +a plain `super` instance and does not support name lookup via the MRO. + +```py +class A: + a: int = 42 + +class B(A): ... + +reveal_type(super(B)) # revealed: super + +# error: [unresolved-attribute] "Type `super` has no attribute `a`" +super(B).a +``` + +## Attribute Assignment + +`super()` objects do not allow attribute assignment — even if the attribute is resolved +successfully. + +```py +class A: + a: int = 3 + +class B(A): ... + +reveal_type(super(B, B()).a) # revealed: int +# error: [invalid-assignment] "Cannot assign to attribute `a` on type `, B>`" +super(B, B()).a = 3 +# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`" +super(B).a = 5 +``` + +## Dynamic Types + +If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a +member, it should effectively behave like a dynamic type. + +```py +class A: + a: int = 1 + +def f(x): + reveal_type(x) # revealed: Unknown + + reveal_type(super(x, x)) # revealed: + reveal_type(super(A, x)) # revealed: , Unknown> + reveal_type(super(x, A())) # revealed: + + reveal_type(super(x, x).a) # revealed: Unknown + reveal_type(super(A, x).a) # revealed: Unknown + reveal_type(super(x, A()).a) # revealed: Unknown +``` + +## Implicit `super()` in Complex Structure + +```py +from __future__ import annotations + +class A: + def test(self): + reveal_type(super()) # revealed: , Unknown> + + class B: + def test(self): + reveal_type(super()) # revealed: , Unknown> + + class C(A.B): + def test(self): + reveal_type(super()) # revealed: , Unknown> + + def inner(t: C): + reveal_type(super()) # revealed: , C> + lambda x: reveal_type(super()) # revealed: , Unknown> +``` + +## Built-ins and Literals + +```py +reveal_type(super(bool, True)) # revealed: , bool> +reveal_type(super(bool, bool())) # revealed: , bool> +reveal_type(super(int, bool())) # revealed: , bool> +reveal_type(super(int, 3)) # revealed: , int> +reveal_type(super(str, "")) # revealed: , str> +``` + +## Descriptor Behavior with Super + +Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can +differ depending on whether the second argument to `super` is a class or an instance. + +```py +class A: + def a1(self): ... + @classmethod + def a2(cls): ... + +class B(A): ... + +# A.__dict__["a1"].__get__(B(), B) +reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown +# A.__dict__["a2"].__get__(B(), B) +reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown + +# A.__dict__["a1"].__get__(None, B) +reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown +# A.__dict__["a2"].__get__(None, B) +reveal_type(super(B, B).a2) # revealed: bound method .a2() -> Unknown +``` + +## Union of Supers + +When the owner is a union type, `super()` is built separately for each branch, and the resulting +super objects are combined into a union. + +```py +class A: ... + +class B: + b: int = 42 + +class C(A, B): ... +class D(B, A): ... + +def f(x: C | D): + reveal_type(C.__mro__) # revealed: tuple[, , , ] + reveal_type(D.__mro__) # revealed: tuple[, , , ] + + s = super(A, x) + reveal_type(s) # revealed: , C> | , D> + + # error: [possibly-unbound-attribute] "Attribute `b` on type `, C> | , D>` is possibly unbound" + s.b + +def f(flag: bool): + x = str() if flag else str("hello") + reveal_type(x) # revealed: Literal["", "hello"] + reveal_type(super(str, x)) # revealed: , str> + +def f(x: int | str): + # error: [invalid-super-argument] "`str` is not an instance or subclass of `` in `super(, str)` call" + super(int, x) +``` + +Even when `super()` is constructed separately for each branch of a union, it should behave correctly +in all cases. + +```py +def f(flag: bool): + if flag: + class A: + x = 1 + y: int = 1 + + a: str = "hello" + + class B(A): ... + s = super(B, B()) + else: + class C: + x = 2 + y: int | str = "test" + + class D(C): ... + s = super(D, D()) + + reveal_type(s) # revealed: , B> | , D> + + reveal_type(s.x) # revealed: Unknown | Literal[1, 2] + reveal_type(s.y) # revealed: int | str + + # error: [possibly-unbound-attribute] "Attribute `a` on type `, B> | , D>` is possibly unbound" + reveal_type(s.a) # revealed: str +``` + +## Supers with Generic Classes + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A[T]: + def f(self, a: T) -> T: + return a + +class B[T](A[T]): + def f(self, b: T) -> T: + return super().f(b) +``` + +## Invalid Usages + +### Unresolvable `super()` Calls + +If an appropriate class and argument cannot be found, a runtime error will occur. + +```py +from __future__ import annotations + +# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" +reveal_type(super()) # revealed: Unknown + +def f(): + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" + super() + +# No first argument in its scope +class A: + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" + s = super() + + def f(self): + def g(): + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" + super() + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" + lambda: super() + + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" + (super() for _ in range(10)) + + @staticmethod + def h(): + # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context" + super() +``` + +### Failing Condition Checks + +```toml +[environment] +python-version = "3.12" +``` + +`super()` requires its first argument to be a valid class, and its second argument to be either an +instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at +runtime. + +```py +def f(x: int): + # error: [invalid-super-argument] "`int` is not a valid class" + super(x, x) + + type IntAlias = int + # error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class" + super(IntAlias, 0) + +# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `` in `super(, Literal[""])` call" +# revealed: Unknown +reveal_type(super(int, str())) + +# error: [invalid-super-argument] "`` is not an instance or subclass of `` in `super(, )` call" +# revealed: Unknown +reveal_type(super(int, str)) + +class A: ... +class B(A): ... + +# error: [invalid-super-argument] "`A` is not an instance or subclass of `` in `super(, A)` call" +# revealed: Unknown +reveal_type(super(B, A())) + +# error: [invalid-super-argument] "`object` is not an instance or subclass of `` in `super(, object)` call" +# revealed: Unknown +reveal_type(super(B, object())) + +# error: [invalid-super-argument] "`` is not an instance or subclass of `` in `super(, )` call" +# revealed: Unknown +reveal_type(super(B, A)) + +# error: [invalid-super-argument] "`` is not an instance or subclass of `` in `super(, )` call" +# revealed: Unknown +reveal_type(super(B, object)) + +super(object, object()).__class__ +``` + +### Instance Member Access via `super` + +Accessing instance members through `super()` is not allowed. + +```py +from __future__ import annotations + +class A: + def __init__(self, a: int): + self.a = a + +class B(A): + def __init__(self, a: int): + super().__init__(a) + # TODO: Once `Self` is supported, this should raise `unresolved-attribute` error + super().a + +# error: [unresolved-attribute] "Type `, B>` has no attribute `a`" +super(B, B(42)).a +``` + +### Dunder Method Resolution + +Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super +object itself. In other words, `super` should not be treated as if it inherits attributes of the +`owner`. + +```py +class A: + def __getitem__(self, key: int) -> int: + return 42 + +class B(A): ... + +reveal_type(A()[0]) # revealed: int +reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int +# error: [non-subscriptable] "Cannot subscript object of type `, B>` with no `__getitem__` method" +super(B, B())[0] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/byte_literals.md b/crates/ty_python_semantic/resources/mdtest/comparison/byte_literals.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/byte_literals.md rename to crates/ty_python_semantic/resources/mdtest/comparison/byte_literals.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md rename to crates/ty_python_semantic/resources/mdtest/comparison/identity.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md rename to crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md rename to crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/integers.md b/crates/ty_python_semantic/resources/mdtest/comparison/integers.md new file mode 100644 index 0000000000000..eeccd6a60a461 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/comparison/integers.md @@ -0,0 +1,27 @@ +# Comparison: Integers + +## Integer literals + +```py +reveal_type(1 == 1 == True) # revealed: Literal[True] +reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False] +reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True] +reveal_type(1 < 1) # revealed: Literal[False] +reveal_type(1 > 1) # revealed: Literal[False] +reveal_type(1 is 1) # revealed: bool +reveal_type(1 is not 1) # revealed: bool +reveal_type(1 is 2) # revealed: Literal[False] +reveal_type(1 is not 7) # revealed: Literal[True] +# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`" +reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True] +``` + +## Integer instance + +```py +# TODO: implement lookup of `__eq__` on typeshed `int` stub. +def _(a: int, b: int): + reveal_type(1 == a) # revealed: bool + reveal_type(9 < a) # revealed: bool + reveal_type(a < b) # revealed: bool +``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md new file mode 100644 index 0000000000000..35d7203c989d1 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -0,0 +1,177 @@ +# Comparison: Intersections + +## Positive contributions + +If we have an intersection type `A & B` and we get a definitive true/false answer for one of the +types, we can infer that the result for the intersection type is also true/false: + +```py +from typing import Literal + +class Base: + def __gt__(self, other) -> bool: + return False + +class Child1(Base): + def __eq__(self, other) -> Literal[True]: + return True + +class Child2(Base): ... + +def _(x: Base): + c1 = Child1() + + # Create an intersection type through narrowing: + if isinstance(x, Child1): + if isinstance(x, Child2): + reveal_type(x) # revealed: Child1 & Child2 + + reveal_type(x == 1) # revealed: Literal[True] + + # Other comparison operators fall back to the base type: + reveal_type(x > 1) # revealed: bool + reveal_type(x is c1) # revealed: bool +``` + +## Negative contributions + +Negative contributions to the intersection type only allow simplifications in a few special cases +(equality and identity comparisons). + +### Equality comparisons + +#### Literal strings + +```py +x = "x" * 1_000_000_000 +y = "y" * 1_000_000_000 +reveal_type(x) # revealed: LiteralString + +if x != "abc": + reveal_type(x) # revealed: LiteralString & ~Literal["abc"] + + # TODO: This should be `Literal[False]` + reveal_type(x == "abc") # revealed: bool + # TODO: This should be `Literal[False]` + reveal_type("abc" == x) # revealed: bool + reveal_type(x == "something else") # revealed: bool + reveal_type("something else" == x) # revealed: bool + + # TODO: This should be `Literal[True]` + reveal_type(x != "abc") # revealed: bool + # TODO: This should be `Literal[True]` + reveal_type("abc" != x) # revealed: bool + reveal_type(x != "something else") # revealed: bool + reveal_type("something else" != x) # revealed: bool + + reveal_type(x == y) # revealed: bool + reveal_type(y == x) # revealed: bool + reveal_type(x != y) # revealed: bool + reveal_type(y != x) # revealed: bool + + reveal_type(x >= "abc") # revealed: bool + reveal_type("abc" >= x) # revealed: bool + + reveal_type(x in "abc") # revealed: bool + reveal_type("abc" in x) # revealed: bool +``` + +#### Integers + +```py +def _(x: int): + if x != 1: + reveal_type(x) # revealed: int & ~Literal[1] + + reveal_type(x != 1) # revealed: bool + reveal_type(x != 2) # revealed: bool + + reveal_type(x == 1) # revealed: bool + reveal_type(x == 2) # revealed: bool +``` + +### Identity comparisons + +```py +class A: ... + +def _(o: object): + a = A() + n = None + + if o is not None: + reveal_type(o) # revealed: ~None + reveal_type(o is n) # revealed: Literal[False] + reveal_type(o is not n) # revealed: Literal[True] +``` + +## Diagnostics + +### Unsupported operators for positive contributions + +Raise an error if the given operator is unsupported for all positive contributions to the +intersection type: + +```py +class NonContainer1: ... +class NonContainer2: ... + +def _(x: object): + if isinstance(x, NonContainer1): + if isinstance(x, NonContainer2): + reveal_type(x) # revealed: NonContainer1 & NonContainer2 + + # error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer1`" + reveal_type(2 in x) # revealed: bool +``` + +Do not raise an error if at least one of the positive contributions to the intersection type support +the operator: + +```py +class Container: + def __contains__(self, x) -> bool: + return False + +def _(x: object): + if isinstance(x, NonContainer1): + if isinstance(x, Container): + if isinstance(x, NonContainer2): + reveal_type(x) # revealed: NonContainer1 & Container & NonContainer2 + reveal_type(2 in x) # revealed: bool +``` + +Do also raise an error if the intersection has no positive contributions at all, unless the operator +is supported on `object`: + +```py +def _(x: object): + if not isinstance(x, NonContainer1): + reveal_type(x) # revealed: ~NonContainer1 + + # error: [unsupported-operator] "Operator `in` is not supported for types `int` and `object`, in comparing `Literal[2]` with `~NonContainer1`" + reveal_type(2 in x) # revealed: bool + + reveal_type(2 is x) # revealed: bool +``` + +### Unsupported operators for negative contributions + +Do *not* raise an error if any of the negative contributions to the intersection type are +unsupported for the given operator: + +```py +class Container: + def __contains__(self, x) -> bool: + return False + +class NonContainer: ... + +def _(x: object): + if isinstance(x, Container): + if not isinstance(x, NonContainer): + reveal_type(x) # revealed: Container & ~NonContainer + + # No error here! + reveal_type(2 in x) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md rename to crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md index 6cf77e5f1e0af..2da190aa5d6d5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md @@ -37,7 +37,7 @@ class C: return self x = A() < B() < C() -reveal_type(x) # revealed: A & ~AlwaysTruthy | B +reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B y = 0 < 1 < A() < 3 reveal_type(y) # revealed: Literal[False] | A diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md b/crates/ty_python_semantic/resources/mdtest/comparison/strings.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md rename to crates/ty_python_semantic/resources/mdtest/comparison/strings.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md rename to crates/ty_python_semantic/resources/mdtest/comparison/tuples.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md rename to crates/ty_python_semantic/resources/mdtest/comparison/unions.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md rename to crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md new file mode 100644 index 0000000000000..a32244c09abb4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -0,0 +1,152 @@ +# Comprehensions + +## Basic comprehensions + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +# revealed: int +[reveal_type(x) for x in IntIterable()] + +class IteratorOfIterables: + def __next__(self) -> IntIterable: + return IntIterable() + +class IterableOfIterables: + def __iter__(self) -> IteratorOfIterables: + return IteratorOfIterables() + +# revealed: tuple[int, IntIterable] +[reveal_type((x, y)) for y in IterableOfIterables() for x in y] + +# revealed: int +{reveal_type(x): 0 for x in IntIterable()} + +# revealed: int +{0: reveal_type(x) for x in IntIterable()} +``` + +## Nested comprehension + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +# revealed: tuple[int, int] +[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()] +``` + +## Comprehension referencing outer comprehension + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +class IteratorOfIterables: + def __next__(self) -> IntIterable: + return IntIterable() + +class IterableOfIterables: + def __iter__(self) -> IteratorOfIterables: + return IteratorOfIterables() + +# revealed: tuple[int, IntIterable] +[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()] +``` + +## Comprehension with unbound iterable + +Iterating over an unbound iterable yields `Unknown`: + +```py +# error: [unresolved-reference] "Name `x` used when not defined" +# revealed: Unknown +[reveal_type(z) for z in x] + +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +# error: [not-iterable] "Object of type `int` is not iterable" +# revealed: tuple[int, Unknown] +[reveal_type((x, z)) for x in IntIterable() for z in x] +``` + +## Starred expressions + +Starred expressions must be iterable + +```py +class NotIterable: ... + +class Iterator: + def __next__(self) -> int: + return 42 + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +# This is fine: +x = [*Iterable()] + +# error: [not-iterable] "Object of type `NotIterable` is not iterable" +y = [*NotIterable()] +``` + +## Async comprehensions + +### Basic + +```py +class AsyncIterator: + async def __anext__(self) -> int: + return 42 + +class AsyncIterable: + def __aiter__(self) -> AsyncIterator: + return AsyncIterator() + +async def _(): + # revealed: @Todo(async iterables/iterators) + [reveal_type(x) async for x in AsyncIterable()] +``` + +### Invalid async comprehension + +This tests that we understand that `async` comprehensions do *not* work according to the synchronous +iteration protocol + +```py +class Iterator: + def __next__(self) -> int: + return 42 + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +async def _(): + # revealed: @Todo(async iterables/iterators) + [reveal_type(x) async for x in Iterable()] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md rename to crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md index b14d358ea04bd..48b912cce13f9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md @@ -42,6 +42,6 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" 3 if NotBoolable() else 4 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md rename to crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md index 9a3fc4f8f4c89..c7a8c7732b4dd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md @@ -154,10 +154,10 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" if NotBoolable(): ... -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" elif NotBoolable(): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/conditional/match.md b/crates/ty_python_semantic/resources/mdtest/conditional/match.md new file mode 100644 index 0000000000000..532c8c46cf978 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/conditional/match.md @@ -0,0 +1,304 @@ +# Pattern matching + +```toml +[environment] +python-version = "3.10" +``` + +## With wildcard + +```py +def _(target: int): + match target: + case 1: + y = 2 + case _: + y = 3 + + reveal_type(y) # revealed: Literal[2, 3] +``` + +## Without wildcard + +```py +def _(target: int): + match target: + case 1: + y = 2 + case 2: + y = 3 + + # revealed: Literal[2, 3] + # error: [possibly-unresolved-reference] + reveal_type(y) +``` + +## Basic match + +```py +def _(target: int): + y = 1 + y = 2 + + match target: + case 1: + y = 3 + case 2: + y = 4 + + reveal_type(y) # revealed: Literal[2, 3, 4] +``` + +## Value match + +A value pattern matches based on equality: the first `case` branch here will be taken if `subject` +is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a +custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider +the possibility that the `case` branch might be taken even though the type `C` is disjoint from the +type `Literal[2]`. + +This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than +`Literal[1]`: + +```py +from typing import final + +@final +class C: + pass + +def _(subject: C): + y = 1 + match subject: + case 2: + y = 3 + reveal_type(y) # revealed: Literal[1, 3] +``` + +## Class match + +A `case` branch with a class pattern is taken if the subject is an instance of the given class, and +all subpatterns in the class pattern match. + +```py +from typing import final + +class Foo: + pass + +class FooSub(Foo): + pass + +class Bar: + pass + +@final +class Baz: + pass + +def _(target: FooSub): + y = 1 + + match target: + case Baz(): + y = 2 + case Foo(): + y = 3 + case Bar(): + y = 4 + + reveal_type(y) # revealed: Literal[3] + +def _(target: FooSub): + y = 1 + + match target: + case Baz(): + y = 2 + case Bar(): + y = 3 + case Foo(): + y = 4 + + reveal_type(y) # revealed: Literal[3, 4] + +def _(target: FooSub | str): + y = 1 + + match target: + case Baz(): + y = 2 + case Foo(): + y = 3 + case Bar(): + y = 4 + + reveal_type(y) # revealed: Literal[1, 3, 4] +``` + +## Singleton match + +Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks. + +```py +from typing import Literal + +def _(target: Literal[True, False]): + y = 1 + + match target: + case True: + y = 2 + case False: + y = 3 + case None: + y = 4 + + # TODO: with exhaustiveness checking, this should be Literal[2, 3] + reveal_type(y) # revealed: Literal[1, 2, 3] + +def _(target: bool): + y = 1 + + match target: + case True: + y = 2 + case False: + y = 3 + case None: + y = 4 + + # TODO: with exhaustiveness checking, this should be Literal[2, 3] + reveal_type(y) # revealed: Literal[1, 2, 3] + +def _(target: None): + y = 1 + + match target: + case True: + y = 2 + case False: + y = 3 + case None: + y = 4 + + reveal_type(y) # revealed: Literal[4] + +def _(target: None | Literal[True]): + y = 1 + + match target: + case True: + y = 2 + case False: + y = 3 + case None: + y = 4 + + # TODO: with exhaustiveness checking, this should be Literal[2, 4] + reveal_type(y) # revealed: Literal[1, 2, 4] + +# bool is an int subclass +def _(target: int): + y = 1 + + match target: + case True: + y = 2 + case False: + y = 3 + case None: + y = 4 + + reveal_type(y) # revealed: Literal[1, 2, 3] + +def _(target: str): + y = 1 + + match target: + case True: + y = 2 + case False: + y = 3 + case None: + y = 4 + + reveal_type(y) # revealed: Literal[1] +``` + +## Or match + +A `|` pattern matches if any of the subpatterns match. + +```py +from typing import Literal, final + +def _(target: Literal["foo", "baz"]): + y = 1 + + match target: + case "foo" | "bar": + y = 2 + case "baz": + y = 3 + + # TODO: with exhaustiveness, this should be Literal[2, 3] + reveal_type(y) # revealed: Literal[1, 2, 3] + +def _(target: None): + y = 1 + + match target: + case None | 3: + y = 2 + case "foo" | 4 | True: + y = 3 + + reveal_type(y) # revealed: Literal[2] + +@final +class Baz: + pass + +def _(target: int | None | float): + y = 1 + + match target: + case None | 3: + y = 2 + case Baz(): + y = 3 + + reveal_type(y) # revealed: Literal[1, 2] + +class Foo: ... + +def _(target: None | Foo): + y = 1 + + match target: + case Baz() | True | False: + y = 2 + case int(): + y = 3 + + reveal_type(y) # revealed: Literal[1, 3] +``` + +## Guard with object that implements `__bool__` incorrectly + +```py +class NotBoolable: + __bool__: int = 3 + +def _(target: int, flag: NotBoolable): + y = 1 + match target: + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" + case 1 if flag: + y = 2 + case 2: + y = 3 + + reveal_type(y) # revealed: Literal[1, 2, 3] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md new file mode 100644 index 0000000000000..0bd3b5b2a6df4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -0,0 +1,33 @@ +# Cycles + +## Function signature + +Deferred annotations can result in cycles in resolving a function signature: + +```py +from __future__ import annotations + +# error: [invalid-type-form] +def f(x: f): + pass + +reveal_type(f) # revealed: def f(x: Unknown) -> Unknown +``` + +## Unpacking + +See: + +```py +class Point: + def __init__(self, x: int = 0, y: int = 0) -> None: + self.x = x + self.y = y + + def replace_with(self, other: "Point") -> None: + self.x, self.y = other.x, other.y + +p = Point() +reveal_type(p.x) # revealed: Unknown | int +reveal_type(p.y) # revealed: Unknown | int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md new file mode 100644 index 0000000000000..879557ad3c6dc --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -0,0 +1,291 @@ +# `typing.dataclass_transform` + +```toml +[environment] +python-version = "3.12" +``` + +`dataclass_transform` is a decorator that can be used to let type checkers know that a function, +class, or metaclass is a `dataclass`-like construct. + +## Basic example + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +def my_dataclass[T](cls: type[T]) -> type[T]: + # modify cls + return cls + +@my_dataclass +class Person: + name: str + age: int | None = None + +Person("Alice", 20) +Person("Bob", None) +Person("Bob") + +# error: [missing-argument] +Person() +``` + +## Decorating decorators that take parameters themselves + +If we want our `dataclass`-like decorator to also take parameters, that is also possible: + +```py +from typing_extensions import dataclass_transform, Callable + +@dataclass_transform() +def versioned_class[T](*, version: int = 1): + def decorator(cls): + # modify cls + return cls + return decorator + +@versioned_class(version=2) +class Person: + name: str + age: int | None = None + +Person("Alice", 20) + +# error: [missing-argument] +Person() +``` + +We properly type-check the arguments to the decorator: + +```py +from typing_extensions import dataclass_transform, Callable + +# error: [invalid-argument-type] +@versioned_class(version="a string") +class C: + name: str +``` + +## Types of decorators + +The examples from this section are straight from the Python documentation on +[`typing.dataclass_transform`]. + +### Decorating a decorator function + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +def create_model[T](cls: type[T]) -> type[T]: + ... + return cls + +@create_model +class CustomerModel: + id: int + name: str + +CustomerModel(id=1, name="Test") +``` + +### Decorating a metaclass + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +class ModelMeta(type): ... + +class ModelBase(metaclass=ModelMeta): ... + +class CustomerModel(ModelBase): + id: int + name: str + +CustomerModel(id=1, name="Test") + +# error: [missing-argument] +CustomerModel() +``` + +### Decorating a base class + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +class ModelBase: ... + +class CustomerModel(ModelBase): + id: int + name: str + +# TODO: this is not supported yet +# error: [unknown-argument] +# error: [unknown-argument] +CustomerModel(id=1, name="Test") +``` + +## Arguments to `dataclass_transform` + +### `eq_default` + +`eq=True/False` does not have a observable effect (apart from a minor change regarding whether +`other` is positional-only or not, which is not modelled at the moment). + +### `order_default` + +The `order_default` argument controls whether methods such as `__lt__` are generated by default. +This can be overwritten using the `order` argument to the custom decorator: + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +def normal(*, order: bool = False): + raise NotImplementedError + +@dataclass_transform(order_default=False) +def order_default_false(*, order: bool = False): + raise NotImplementedError + +@dataclass_transform(order_default=True) +def order_default_true(*, order: bool = True): + raise NotImplementedError + +@normal +class Normal: + inner: int + +Normal(1) < Normal(2) # error: [unsupported-operator] + +@normal(order=True) +class NormalOverwritten: + inner: int + +NormalOverwritten(1) < NormalOverwritten(2) + +@order_default_false +class OrderFalse: + inner: int + +OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator] + +@order_default_false(order=True) +class OrderFalseOverwritten: + inner: int + +OrderFalseOverwritten(1) < OrderFalseOverwritten(2) + +@order_default_true +class OrderTrue: + inner: int + +OrderTrue(1) < OrderTrue(2) + +@order_default_true(order=False) +class OrderTrueOverwritten: + inner: int + +# error: [unsupported-operator] +OrderTrueOverwritten(1) < OrderTrueOverwritten(2) +``` + +### `kw_only_default` + +To do + +### `field_specifiers` + +To do + +## Overloaded dataclass-like decorators + +In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the +implementation, or to *one* of the overloads. + +### Applying `dataclass_transform` to the implementation + +```py +from typing_extensions import dataclass_transform, TypeVar, Callable, overload + +T = TypeVar("T", bound=type) + +@overload +def versioned_class( + cls: T, + *, + version: int = 1, +) -> T: ... +@overload +def versioned_class( + *, + version: int = 1, +) -> Callable[[T], T]: ... +@dataclass_transform() +def versioned_class( + cls: T | None = None, + *, + version: int = 1, +) -> T | Callable[[T], T]: + raise NotImplementedError + +@versioned_class +class D1: + x: str + +@versioned_class(version=2) +class D2: + x: str + +D1("a") +D2("a") + +D1(1.2) # error: [invalid-argument-type] +D2(1.2) # error: [invalid-argument-type] +``` + +### Applying `dataclass_transform` to an overload + +```py +from typing_extensions import dataclass_transform, TypeVar, Callable, overload + +T = TypeVar("T", bound=type) + +@overload +@dataclass_transform() +def versioned_class( + cls: T, + *, + version: int = 1, +) -> T: ... +@overload +def versioned_class( + *, + version: int = 1, +) -> Callable[[T], T]: ... +def versioned_class( + cls: T | None = None, + *, + version: int = 1, +) -> T | Callable[[T], T]: + raise NotImplementedError + +@versioned_class +class D1: + x: str + +@versioned_class(version=2) +class D2: + x: str + +D1("a") +D2("a") + +D1(1.2) # error: [invalid-argument-type] +D2(1.2) # error: [invalid-argument-type] +``` + +[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md new file mode 100644 index 0000000000000..b685b37fe07c7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -0,0 +1,1010 @@ +# Dataclasses + +## Basic + +Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`, +`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass` +decorator. By default, only the three mentioned methods are generated. + +```py +from dataclasses import dataclass + +@dataclass +class Person: + name: str + age: int | None = None + +alice1 = Person("Alice", 30) +alice2 = Person(name="Alice", age=30) +alice3 = Person(age=30, name="Alice") +alice4 = Person("Alice", age=30) + +reveal_type(alice1) # revealed: Person +reveal_type(type(alice1)) # revealed: type[Person] + +reveal_type(alice1.name) # revealed: str +reveal_type(alice1.age) # revealed: int | None + +reveal_type(repr(alice1)) # revealed: str + +reveal_type(alice1 == alice2) # revealed: bool +reveal_type(alice1 == "Alice") # revealed: bool + +bob = Person("Bob") +bob2 = Person("Bob", None) +bob3 = Person(name="Bob") +bob4 = Person(name="Bob", age=None) +``` + +The signature of the `__init__` method is generated based on the classes attributes. The following +calls are not valid: + +```py +# error: [missing-argument] +Person() + +# error: [too-many-positional-arguments] +Person("Eve", 20, "too many arguments") + +# error: [invalid-argument-type] +Person("Eve", "string instead of int") + +# error: [invalid-argument-type] +# error: [invalid-argument-type] +Person(20, "Eve") +``` + +## Signature of `__init__` + +Declarations in the class body are used to generate the signature of the `__init__` method. If the +attributes are not just declarations, but also bindings, the type inferred from bindings is used as +the default value. + +```py +from dataclasses import dataclass + +@dataclass +class D: + x: int + y: str = "default" + z: int | None = 1 + 2 + +reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None +``` + +This also works if the declaration and binding are split: + +```py +@dataclass +class D: + x: int | None + x = None + +reveal_type(D.__init__) # revealed: (self: D, x: int | None = None) -> None +``` + +Non-fully static types are handled correctly: + +```py +from typing import Any + +@dataclass +class C: + w: type[Any] + x: Any + y: int | Any + z: tuple[int, Any] + +reveal_type(C.__init__) # revealed: (self: C, w: type[Any], x: Any, y: int | Any, z: tuple[int, Any]) -> None +``` + +Variables without annotations are ignored: + +```py +@dataclass +class D: + x: int + y = 1 + +reveal_type(D.__init__) # revealed: (self: D, x: int) -> None +``` + +If attributes without default values are declared after attributes with default values, a +`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case: + +```py +@dataclass +class D: + x: int = 1 + # TODO: this should be an error: field without default defined after field with default + y: str +``` + +Pure class attributes (`ClassVar`) are not included in the signature of `__init__`: + +```py +from typing import ClassVar + +@dataclass +class D: + x: int + y: ClassVar[str] = "default" + z: bool + +reveal_type(D.__init__) # revealed: (self: D, x: int, z: bool) -> None + +d = D(1, True) +reveal_type(d.x) # revealed: int +reveal_type(d.y) # revealed: str +reveal_type(d.z) # revealed: bool +``` + +Function declarations do not affect the signature of `__init__`: + +```py +@dataclass +class D: + x: int + + def y(self) -> str: + return "" + +reveal_type(D.__init__) # revealed: (self: D, x: int) -> None +``` + +And neither do nested class declarations: + +```py +@dataclass +class D: + x: int + + class Nested: + y: str + +reveal_type(D.__init__) # revealed: (self: D, x: int) -> None +``` + +But if there is a variable annotation with a function or class literal type, the signature of +`__init__` will include this field: + +```py +from ty_extensions import TypeOf + +class SomeClass: ... + +def some_function() -> None: ... +@dataclass +class D: + function_literal: TypeOf[some_function] + class_literal: TypeOf[SomeClass] + class_subtype_of: type[SomeClass] + +# revealed: (self: D, function_literal: def some_function() -> None, class_literal: , class_subtype_of: type[SomeClass]) -> None +reveal_type(D.__init__) +``` + +More realistically, dataclasses can have `Callable` attributes: + +```py +from typing import Callable + +@dataclass +class D: + c: Callable[[int], str] + +reveal_type(D.__init__) # revealed: (self: D, c: (int, /) -> str) -> None +``` + +Implicit instance attributes do not affect the signature of `__init__`: + +```py +@dataclass +class D: + x: int + + def f(self, y: str) -> None: + self.y: str = y + +reveal_type(D(1).y) # revealed: str + +reveal_type(D.__init__) # revealed: (self: D, x: int) -> None +``` + +Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't +be included in the signature of `__init__`. This is a case that we currently don't detect: + +```py +@dataclass +class D: + # (x) is an expression, not a "simple name" + (x): int = 1 + +# TODO: should ideally not include a `x` parameter +reveal_type(D.__init__) # revealed: (self: D, x: int = Literal[1]) -> None +``` + +## `@dataclass` calls with arguments + +The `@dataclass` decorator can take several arguments to customize the existence of the generated +methods. The following test makes sure that we still treat the class as a dataclass if (the default) +arguments are passed in: + +```py +from dataclasses import dataclass + +@dataclass(init=True, repr=True, eq=True) +class Person: + name: str + age: int | None = None + +alice = Person("Alice", 30) +reveal_type(repr(alice)) # revealed: str +reveal_type(alice == alice) # revealed: bool +``` + +If `init` is set to `False`, no `__init__` method is generated: + +```py +from dataclasses import dataclass + +@dataclass(init=False) +class C: + x: int + +C() # Okay + +# error: [too-many-positional-arguments] +C(1) + +repr(C()) + +C() == C() +``` + +## Other dataclass parameters + +### `repr` + +A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but +in that case `__repr__` is still available via `object.__repr__`: + +```py +from dataclasses import dataclass + +@dataclass(repr=False) +class WithoutRepr: + x: int + +reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str +``` + +### `eq` + +The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but +`__eq__` is still available via `object.__eq__`: + +```py +from dataclasses import dataclass + +@dataclass(eq=False) +class WithoutEq: + x: int + +reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool +``` + +### `order` + +```toml +[environment] +python-version = "3.12" +``` + +`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__` +methods will be generated: + +```py +from dataclasses import dataclass + +@dataclass +class WithoutOrder: + x: int + +WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator] +WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator] +WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator] +WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator] + +@dataclass(order=True) +class WithOrder: + x: int + +WithOrder(1) < WithOrder(2) +WithOrder(1) <= WithOrder(2) +WithOrder(1) > WithOrder(2) +WithOrder(1) >= WithOrder(2) +``` + +Comparisons are only allowed for `WithOrder` instances: + +```py +WithOrder(1) < 2 # error: [unsupported-operator] +WithOrder(1) <= 2 # error: [unsupported-operator] +WithOrder(1) > 2 # error: [unsupported-operator] +WithOrder(1) >= 2 # error: [unsupported-operator] +``` + +This also works for generic dataclasses: + +```py +from dataclasses import dataclass + +@dataclass(order=True) +class GenericWithOrder[T]: + x: T + +GenericWithOrder[int](1) < GenericWithOrder[int](1) + +GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator] +``` + +If a class already defines one of the comparison methods, a `TypeError` is raised at runtime. +Ideally, we would emit a diagnostic in that case: + +```py +@dataclass(order=True) +class AlreadyHasCustomDunderLt: + x: int + + # TODO: Ideally, we would emit a diagnostic here + def __lt__(self, other: object) -> bool: + return False +``` + +### `unsafe_hash` + +To do + +### `frozen` + +If true (the default is False), assigning to fields will generate a diagnostic. + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class MyFrozenClass: + x: int + +frozen_instance = MyFrozenClass(1) +frozen_instance.x = 2 # error: [invalid-assignment] +``` + +If `__setattr__()` or `__delattr__()` is defined in the class, we should emit a diagnostic. + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class MyFrozenClass: + x: int + + # TODO: Emit a diagnostic here + def __setattr__(self, name: str, value: object) -> None: ... + + # TODO: Emit a diagnostic here + def __delattr__(self, name: str) -> None: ... +``` + +This also works for generic dataclasses: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class MyFrozenGeneric[T]: + x: T + +frozen_instance = MyFrozenGeneric[int](1) +frozen_instance.x = 2 # error: [invalid-assignment] +``` + +When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute` +is emitted: + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class MyFrozenClass: ... + +frozen = MyFrozenClass() +frozen.x = 2 # error: [unresolved-attribute] +``` + +### `match_args` + +To do + +### `kw_only` + +To do + +### `slots` + +To do + +### `weakref_slot` + +To do + +## `Final` fields + +Dataclass fields can be annotated with `Final`, which means that the field cannot be reassigned +after the instance is created. Fields that are additionally annotated with `ClassVar` are not part +of the `__init__` signature. + +```py +from dataclasses import dataclass +from typing import Final, ClassVar + +@dataclass +class C: + # a `Final` annotation without a right-hand side is not allowed in normal classes, + # but valid for dataclasses. The field will be initialized in the synthesized + # `__init__` method + instance_variable_no_default: Final[int] + instance_variable: Final[int] = 1 + class_variable1: ClassVar[Final[int]] = 1 + class_variable2: ClassVar[Final[int]] = 1 + +reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None + +c = C(1) +# TODO: this should be an error +c.instance_variable = 2 +``` + +## Inheritance + +### Normal class inheriting from a dataclass + +```py +from dataclasses import dataclass + +@dataclass +class Base: + x: int + +class Derived(Base): ... + +d = Derived(1) # OK +reveal_type(d.x) # revealed: int +``` + +### Dataclass inheriting from normal class + +```py +from dataclasses import dataclass + +class Base: + x: int = 1 + +@dataclass +class Derived(Base): + y: str + +d = Derived("a") + +# error: [too-many-positional-arguments] +# error: [invalid-argument-type] +Derived(1, "a") +``` + +### Dataclass inheriting from another dataclass + +```py +from dataclasses import dataclass + +@dataclass +class Base: + x: int + y: str + +@dataclass +class Derived(Base): + z: bool + +d = Derived(1, "a", True) # OK + +reveal_type(d.x) # revealed: int +reveal_type(d.y) # revealed: str +reveal_type(d.z) # revealed: bool + +# error: [missing-argument] +Derived(1, "a") + +# error: [missing-argument] +Derived(True) +``` + +### Overwriting attributes from base class + +The following example comes from the +[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x` +attribute appears just once in the `__init__` signature, and the default value is taken from the +derived class + +```py +from dataclasses import dataclass +from typing import Any + +@dataclass +class Base: + x: Any = 15.0 + y: int = 0 + +@dataclass +class C(Base): + z: int = 10 + x: int = 15 + +reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None +``` + +## Conditionally defined fields + +### Statically known conditions + +Fields that are defined in always-reachable branches are always present in the synthesized +`__init__` method. Fields that are defined in never-reachable branches are not present: + +```py +from dataclasses import dataclass + +@dataclass +class C: + normal: int + + if 1 + 2 == 3: + always_present: str + + if 1 + 2 == 4: + never_present: bool + +reveal_type(C.__init__) # revealed: (self: C, normal: int, always_present: str) -> None +``` + +### Dynamic conditions + +If a field is conditionally defined, we currently assume that it is always present. A more complex +alternative here would be to synthesized a union of all possible `__init__` signatures: + +```py +from dataclasses import dataclass + +def flag() -> bool: + return True + +@dataclass +class C: + normal: int + + if flag(): + conditionally_present: str + +reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_present: str) -> None +``` + +## Generic dataclasses + +```toml +[environment] +python-version = "3.12" +``` + +```py +from dataclasses import dataclass + +@dataclass +class DataWithDescription[T]: + data: T + description: str + +reveal_type(DataWithDescription[int]) # revealed: + +d_int = DataWithDescription[int](1, "description") # OK +reveal_type(d_int.data) # revealed: int +reveal_type(d_int.description) # revealed: str + +# error: [invalid-argument-type] +DataWithDescription[int](None, "description") +``` + +## Descriptor-typed fields + +### Same type in `__get__` and `__set__` + +For the following descriptor, the return type of `__get__` and the type of the `value` parameter in +`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of +the type of the descriptor), and the default value is also of this type: + +```py +from typing import overload +from dataclasses import dataclass + +class UppercaseString: + _value: str = "" + + def __get__(self, instance: object, owner: None | type) -> str: + return self._value + + def __set__(self, instance: object, value: str) -> None: + self._value = value.upper() + +@dataclass +class C: + upper: UppercaseString = UppercaseString() + +reveal_type(C.__init__) # revealed: (self: C, upper: str = str) -> None + +c = C("abc") +reveal_type(c.upper) # revealed: str + +# This is also okay: +C() + +# error: [invalid-argument-type] +C(1) + +# error: [too-many-positional-arguments] +C("a", "b") +``` + +### Different types in `__get__` and `__set__` + +In general, the type of the `__init__` parameter is determined by the `value` parameter type of the +`__set__` method (`str` in the example below). However, the default value is generated by calling +the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None` +for the `instance` argument. + +```py +from typing import Literal, overload +from dataclasses import dataclass + +class ConvertToLength: + _len: int = 0 + + @overload + def __get__(self, instance: None, owner: type) -> Literal[""]: ... + @overload + def __get__(self, instance: object, owner: type | None) -> int: ... + def __get__(self, instance: object | None, owner: type | None) -> str | int: + if instance is None: + return "" + + return self._len + + def __set__(self, instance, value: str) -> None: + self._len = len(value) + +@dataclass +class C: + converter: ConvertToLength = ConvertToLength() + +reveal_type(C.__init__) # revealed: (self: C, converter: str = Literal[""]) -> None + +c = C("abc") +reveal_type(c.converter) # revealed: int + +# This is also okay: +C() + +# error: [invalid-argument-type] +C(1) + +# error: [too-many-positional-arguments] +C("a", "b") +``` + +### With overloaded `__set__` method + +If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the +union of all possible `value` parameter types: + +```py +from typing import overload +from dataclasses import dataclass + +class AcceptsStrAndInt: + def __get__(self, instance, owner) -> int: + return 0 + + @overload + def __set__(self, instance: object, value: str) -> None: ... + @overload + def __set__(self, instance: object, value: int) -> None: ... + def __set__(self, instance: object, value) -> None: + pass + +@dataclass +class C: + field: AcceptsStrAndInt = AcceptsStrAndInt() + +reveal_type(C.__init__) # revealed: (self: C, field: str | int = int) -> None +``` + +## `dataclasses.field` + +To do + +## `dataclass.fields` + +Dataclasses have a special `__dataclass_fields__` class variable member. The `DataclassInstance` +protocol checks for the presence of this attribute. It is used in the `dataclasses.fields` and +`dataclasses.asdict` functions, for example: + +```py +from dataclasses import dataclass, fields, asdict + +@dataclass +class Foo: + x: int + +foo = Foo(1) + +reveal_type(foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] +reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...] +reveal_type(asdict(foo)) # revealed: dict[str, Any] +``` + +The class objects themselves also have a `__dataclass_fields__` attribute: + +```py +reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] +``` + +They can be passed into `fields` as well, because it also accepts `type[DataclassInstance]` +arguments: + +```py +reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...] +``` + +But calling `asdict` on the class object is not allowed: + +```py +# TODO: this should be a invalid-argument-type error, but we don't properly check the +# types (and more importantly, the `ClassVar` type qualifier) of protocol members yet. +asdict(Foo) +``` + +## `dataclasses.KW_ONLY` + + + +If an attribute is annotated with `dataclasses.KW_ONLY`, it is not added to the synthesized +`__init__` of the class. Instead, this special marker annotation causes Python at runtime to ensure +that all annotations following it have keyword-only parameters generated for them in the class's +synthesized `__init__` method. + +```toml +[environment] +python-version = "3.10" +``` + +```py +from dataclasses import dataclass, field, KW_ONLY +from typing_extensions import reveal_type + +@dataclass +class C: + x: int + _: KW_ONLY + y: str + +reveal_type(C.__init__) # revealed: (self: C, x: int, *, y: str) -> None + +# error: [missing-argument] +# error: [too-many-positional-arguments] +C(3, "") + +C(3, y="") +``` + +Using `KW_ONLY` to annotate more than one field in a dataclass causes a `TypeError` to be raised at +runtime: + +```py +@dataclass +class Fails: # error: [duplicate-kw-only] + a: int + b: KW_ONLY + c: str + d: KW_ONLY + e: bytes + +reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None +``` + +This also works if `KW_ONLY` is used in a conditional branch: + +```py +def flag() -> bool: + return True + +@dataclass +class D: # error: [duplicate-kw-only] + x: int + _1: KW_ONLY + + if flag(): + y: str + _2: KW_ONLY + z: float +``` + +## Other special cases + +### `dataclasses.dataclass` + +We also understand dataclasses if they are decorated with the fully qualified name: + +```py +import dataclasses + +@dataclasses.dataclass +class C: + x: str + +reveal_type(C.__init__) # revealed: (self: C, x: str) -> None +``` + +### Dataclass with custom `__init__` method + +If a class already defines `__init__`, it is not replaced by the `dataclass` decorator. + +```py +from dataclasses import dataclass + +@dataclass(init=True) +class C: + x: str + + def __init__(self, x: int) -> None: + self.x = str(x) + +C(1) # OK + +# error: [invalid-argument-type] +C("a") +``` + +Similarly, if we set `init=False`, we still recognize the custom `__init__` method: + +```py +@dataclass(init=False) +class D: + def __init__(self, x: int) -> None: + self.x = str(x) + +D(1) # OK +D() # error: [missing-argument] +``` + +### Return type of `dataclass(...)` + +A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator. +We can store the callable in a variable and later use it as a decorator: + +```py +from dataclasses import dataclass + +dataclass_with_order = dataclass(order=True) + +reveal_type(dataclass_with_order) # revealed: + +@dataclass_with_order +class C: + x: int + +C(1) < C(2) # ok +``` + +### Using `dataclass` as a function + +```py +from dataclasses import dataclass + +class B: + x: int + +# error: [missing-argument] +dataclass(B)() + +# error: [invalid-argument-type] +dataclass(B)("a") + +reveal_type(dataclass(B)(3).x) # revealed: int +``` + +## Internals + +The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`, +and attributes like the MRO are unchanged: + +```py +from dataclasses import dataclass + +@dataclass +class Person: + name: str + age: int | None = None + +reveal_type(type(Person)) # revealed: +reveal_type(Person.__mro__) # revealed: tuple[, ] +``` + +The generated methods have the following signatures: + +```py +reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int | None = None) -> None + +reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str + +reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool +``` + +## Function-like behavior of synthesized methods + +Here, we make sure that the synthesized methods of dataclasses behave like proper functions. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from dataclasses import dataclass +from typing import Callable +from types import FunctionType +from ty_extensions import CallableTypeOf, TypeOf, static_assert, is_subtype_of, is_assignable_to + +@dataclass +class C: + x: int + +reveal_type(C.__init__) # revealed: (self: C, x: int) -> None +reveal_type(type(C.__init__)) # revealed: + +# We can access attributes that are defined on functions: +reveal_type(type(C.__init__).__code__) # revealed: CodeType +reveal_type(C.__init__.__code__) # revealed: CodeType + +def equivalent_signature(self: C, x: int) -> None: + pass + +type DunderInitType = TypeOf[C.__init__] +type EquivalentPureCallableType = Callable[[C, int], None] +type EquivalentFunctionLikeCallableType = CallableTypeOf[equivalent_signature] + +static_assert(is_subtype_of(DunderInitType, EquivalentPureCallableType)) +static_assert(is_assignable_to(DunderInitType, EquivalentPureCallableType)) + +static_assert(not is_subtype_of(EquivalentPureCallableType, DunderInitType)) +static_assert(not is_assignable_to(EquivalentPureCallableType, DunderInitType)) + +static_assert(is_subtype_of(DunderInitType, EquivalentFunctionLikeCallableType)) +static_assert(is_assignable_to(DunderInitType, EquivalentFunctionLikeCallableType)) + +static_assert(not is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType)) +static_assert(not is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType)) + +static_assert(is_subtype_of(DunderInitType, FunctionType)) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md new file mode 100644 index 0000000000000..cafed930ec92b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/fields.md @@ -0,0 +1,35 @@ +# Dataclass fields + +## Basic + +```py +from dataclasses import dataclass, field + +@dataclass +class Member: + name: str + role: str = field(default="user") + tag: str | None = field(default=None, init=False) + +# TODO: this should not include the `tag` parameter, since it has `init=False` set +# revealed: (self: Member, name: str, role: str = Unknown, tag: str | None = Unknown) -> None +reveal_type(Member.__init__) + +alice = Member(name="Alice", role="admin") +reveal_type(alice.role) # revealed: str +alice.role = "moderator" + +# TODO: this should be an error, `tag` has `init=False` +bob = Member(name="Bob", tag="VIP") +``` + +## The `field` function + +```py +from dataclasses import field + +# TODO: this should be `Literal[1]`. This is currently blocked on enum support, because +# the `dataclasses.field` overloads make use of a `_MISSING_TYPE` enum, for which we +# infer a @Todo type, and therefore pick the wrong overload. +reveal_type(field(default=1)) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/declaration/error.md b/crates/ty_python_semantic/resources/mdtest/declaration/error.md new file mode 100644 index 0000000000000..6633ba562d5d7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/declaration/error.md @@ -0,0 +1,93 @@ +# Errors while declaring + +## Violates previous assignment + +```py +x = 1 +x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`" +``` + +## Incompatible declarations + +```py +def _(flag: bool): + if flag: + x: str + else: + x: int + + x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: `str` and `int`" +``` + +## Incompatible declarations for 2 (out of 3) types + +```py +def _(flag1: bool, flag2: bool): + if flag1: + x: str + elif flag2: + x: int + + # Here, the declared type for `x` is `int | str | Unknown`. + x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: `str` and `int`" +``` + +## Incompatible declarations with repeated types + +```py +def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): + if flag1: + x: str + elif flag2: + x: int + elif flag3: + x: int + elif flag4: + x: str + else: + x: bytes + + x = "a" # error: [conflicting-declarations] "Conflicting declared types for `x`: `str`, `int` and `bytes`" +``` + +## Incompatible declarations with bad assignment + +```py +def _(flag: bool): + if flag: + x: str + else: + x: int + + # error: [conflicting-declarations] + # error: [invalid-assignment] + x = b"foo" +``` + +## No errors + +Currently, we avoid raising the conflicting-declarations for the following cases: + +### Partial declarations + +```py +def _(flag: bool): + if flag: + x: int + + x = 1 +``` + +### Partial declarations in try-except + +Refer to + +```py +def _(): + try: + x: int = 1 + except: + x = 2 + + x = 3 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/decorators.md rename to crates/ty_python_semantic/resources/mdtest/decorators.md index 3ba75ec10b0d0..f92eca800360e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -145,10 +145,10 @@ def f(x: int) -> int: return x**2 # TODO: Should be `_lru_cache_wrapper[int]` -reveal_type(f) # revealed: @Todo(generics) +reveal_type(f) # revealed: _lru_cache_wrapper[Unknown] # TODO: Should be `int` -reveal_type(f(1)) # revealed: @Todo(generics) +reveal_type(f(1)) # revealed: Unknown ``` ## Lambdas as decorators @@ -207,7 +207,7 @@ first argument: def wrong_signature(f: int) -> str: return "a" -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `def f(x) -> Unknown`" +# error: [invalid-argument-type] "Argument to function `wrong_signature` is incorrect: Expected `int`, found `def f(x) -> Unknown`" @wrong_signature def f(x): ... diff --git a/crates/ty_python_semantic/resources/mdtest/del.md b/crates/ty_python_semantic/resources/mdtest/del.md new file mode 100644 index 0000000000000..2007bca7382a3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -0,0 +1,121 @@ +# `del` statement + +## Basic + +```py +a = 1 +del a +# error: [unresolved-reference] +reveal_type(a) # revealed: Unknown + +# error: [invalid-syntax] "Invalid delete target" +del 1 + +# error: [unresolved-reference] +del a + +x, y = 1, 2 +del x, y +# error: [unresolved-reference] +reveal_type(x) # revealed: Unknown +# error: [unresolved-reference] +reveal_type(y) # revealed: Unknown + +def cond() -> bool: + return True + +b = 1 +if cond(): + del b + +# error: [possibly-unresolved-reference] +reveal_type(b) # revealed: Literal[1] + +c = 1 +if cond(): + c = 2 +else: + del c + +# error: [possibly-unresolved-reference] +reveal_type(c) # revealed: Literal[2] + +d = 1 + +def delete(): + # TODO: this results in `UnboundLocalError`; we should emit `unresolved-reference` + del d + +delete() +reveal_type(d) # revealed: Literal[1] + +def delete_global(): + global d + del d + +delete_global() +# The variable should have been removed, but we won't check it for now. +reveal_type(d) # revealed: Literal[1] +``` + +## Delete attributes + +If an attribute is referenced after being deleted, it will be an error at runtime. But we don't +treat this as an error (because there may have been a redefinition by a method between the `del` +statement and the reference). However, deleting an attribute invalidates type narrowing by +assignment, and the attribute type will be the originally declared type. + +### Invalidate narrowing + +```py +class C: + x: int = 1 + +c = C() +del c.x +reveal_type(c.x) # revealed: int + +# error: [unresolved-attribute] +del c.non_existent + +c.x = 1 +reveal_type(c.x) # revealed: Literal[1] +del c.x +reveal_type(c.x) # revealed: int +``` + +### Delete an instance attribute definition + +```py +class C: + x: int = 1 + +c = C() +reveal_type(c.x) # revealed: int + +del C.x +c = C() +# This attribute is unresolved, but we won't check it for now. +reveal_type(c.x) # revealed: int +``` + +## Delete items + +Deleting an item also invalidates the narrowing by the assignment, but accessing the item itself is +still valid. + +```py +def f(l: list[int]): + del l[0] + # If the length of `l` was 1, this will be a runtime error, + # but if it was greater than that, it will not be an error. + reveal_type(l[0]) # revealed: int + + # error: [call-non-callable] + del l["string"] + + l[0] = 1 + reveal_type(l[0]) # revealed: Literal[1] + del l[0] + reveal_type(l[0]) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md rename to crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index aeabb336ecbac..6d413f710065e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -269,7 +269,7 @@ on the metaclass: ```py C1.meta_data_descriptor = 1 -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `Literal[C1]` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `` with custom `__set__` method" C1.meta_data_descriptor = "invalid" ``` @@ -371,7 +371,7 @@ def _(flag: bool): # TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not # wrong, but they could be subsumed under a higher-level diagnostic. - # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `Literal[C5]` with custom `__set__` method" + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `` with custom `__set__` method" # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`" C5.meta_data_descriptor1 = None @@ -459,11 +459,9 @@ class Descriptor: class C: d: Descriptor = Descriptor() -# TODO: should be `Literal["called on class object"] -reveal_type(C.d) # revealed: LiteralString +reveal_type(C.d) # revealed: Literal["called on class object"] -# TODO: should be `Literal["called on instance"] -reveal_type(C().d) # revealed: LiteralString +reveal_type(C().d) # revealed: Literal["called on instance"] ``` ## Descriptor protocol for dunder methods @@ -551,6 +549,19 @@ reveal_type(C.get_name()) # revealed: str reveal_type(C("42").get_name()) # revealed: str ``` +### Built-in `staticmethod` descriptor + +```py +class C: + @staticmethod + def helper(value: str) -> str: + return value + +reveal_type(C.helper("42")) # revealed: str +c = C() +reveal_type(c.helper("string")) # revealed: str +``` + ### Functions as descriptors Functions are descriptors because they implement a `__get__` method. This is crucial in making sure @@ -608,8 +619,9 @@ wrapper_descriptor() # error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(f) -# Calling it without the `owner` argument if `instance` is not `None` is an -# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" +# TODO: Calling it without the `owner` argument if `instance` is not `None` fails at runtime. +# Ideally we would emit a diagnostic here, +# but this is hard to model without introducing false positives elsewhere wrapper_descriptor(f, None) # But calling it with an instance is fine (in this case, the `owner` argument is optional): @@ -701,9 +713,7 @@ class C: descriptor = Descriptor() C.descriptor = "something else" - -# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments -reveal_type(C.descriptor) # revealed: Unknown | int +reveal_type(C.descriptor) # revealed: Literal["something else"] ``` ### Possibly unbound descriptor attributes @@ -726,13 +736,13 @@ def _(flag: bool): non_data: NonDataDescriptor = NonDataDescriptor() data: DataDescriptor = DataDescriptor() - # error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound" + # error: [possibly-unbound-attribute] "Attribute `non_data` on type `` is possibly unbound" reveal_type(PossiblyUnbound.non_data) # revealed: int # error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound" reveal_type(PossiblyUnbound().non_data) # revealed: int - # error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound" + # error: [possibly-unbound-attribute] "Attribute `data` on type `` is possibly unbound" reveal_type(PossiblyUnbound.data) # revealed: int # error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound" diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md new file mode 100644 index 0000000000000..19627f8351043 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md @@ -0,0 +1,306 @@ +# No matching overload diagnostics + + + +## Calls to overloaded functions + +```py +from typing import overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +def f(x: int | str) -> int | str: + return x + +f(b"foo") # error: [no-matching-overload] +``` + +## Call to function with many unmatched overloads + +Note that it would be fine to use `pow` here as an example of a routine with many overloads, but at +time of writing (2025-05-14), ty doesn't support some of the type signatures of those overloads. +Which in turn makes snapshotting a bit annoying, since the output can depend on how ty is compiled +(because of how `Todo` types are dealt with when `debug_assertions` is enabled versus disabled). + +```py +from typing import overload + +class Foo: ... + +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: str, b: int, c: int): ... +@overload +def foo(a: int, b: str, c: int): ... +@overload +def foo(a: int, b: int, c: str): ... +@overload +def foo(a: str, b: str, c: int): ... +@overload +def foo(a: int, b: str, c: str): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: float, b: int, c: int): ... +@overload +def foo(a: int, b: float, c: int): ... +@overload +def foo(a: int, b: int, c: float): ... +@overload +def foo(a: float, b: float, c: int): ... +@overload +def foo(a: int, b: float, c: float): ... +@overload +def foo(a: float, b: float, c: float): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: float, b: str, c: str): ... +@overload +def foo(a: str, b: float, c: str): ... +@overload +def foo(a: str, b: str, c: float): ... +@overload +def foo(a: float, b: float, c: str): ... +@overload +def foo(a: str, b: float, c: float): ... +@overload +def foo(a: float, b: float, c: float): ... +def foo(a, b, c): ... + +foo(Foo(), Foo()) # error: [no-matching-overload] +``` + +## Call to function with too many unmatched overloads + +This is like the above example, but has an excessive number of overloads to the point that ty will +cut off the list in the diagnostic and emit a message stating the number of omitted overloads. + +```py +from typing import overload + +class Foo: ... + +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: str, b: int, c: int): ... +@overload +def foo(a: int, b: str, c: int): ... +@overload +def foo(a: int, b: int, c: str): ... +@overload +def foo(a: str, b: str, c: int): ... +@overload +def foo(a: int, b: str, c: str): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: float, b: int, c: int): ... +@overload +def foo(a: int, b: float, c: int): ... +@overload +def foo(a: int, b: int, c: float): ... +@overload +def foo(a: float, b: float, c: int): ... +@overload +def foo(a: int, b: float, c: float): ... +@overload +def foo(a: float, b: float, c: float): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: float, b: str, c: str): ... +@overload +def foo(a: str, b: float, c: str): ... +@overload +def foo(a: str, b: str, c: float): ... +@overload +def foo(a: float, b: float, c: str): ... +@overload +def foo(a: str, b: float, c: float): ... +@overload +def foo(a: float, b: float, c: float): ... +@overload +def foo(a: list[int], b: list[int], c: list[int]): ... +@overload +def foo(a: list[str], b: list[int], c: list[int]): ... +@overload +def foo(a: list[int], b: list[str], c: list[int]): ... +@overload +def foo(a: list[int], b: list[int], c: list[str]): ... +@overload +def foo(a: list[str], b: list[str], c: list[int]): ... +@overload +def foo(a: list[int], b: list[str], c: list[str]): ... +@overload +def foo(a: list[str], b: list[str], c: list[str]): ... +@overload +def foo(a: list[int], b: list[int], c: list[int]): ... +@overload +def foo(a: list[float], b: list[int], c: list[int]): ... +@overload +def foo(a: list[int], b: list[float], c: list[int]): ... +@overload +def foo(a: list[int], b: list[int], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[int]): ... +@overload +def foo(a: list[int], b: list[float], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[float]): ... +@overload +def foo(a: list[str], b: list[str], c: list[str]): ... +@overload +def foo(a: list[float], b: list[str], c: list[str]): ... +@overload +def foo(a: list[str], b: list[float], c: list[str]): ... +@overload +def foo(a: list[str], b: list[str], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[str]): ... +@overload +def foo(a: list[str], b: list[float], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[float]): ... +@overload +def foo(a: bool, b: bool, c: bool): ... +@overload +def foo(a: str, b: bool, c: bool): ... +@overload +def foo(a: bool, b: str, c: bool): ... +@overload +def foo(a: bool, b: bool, c: str): ... +@overload +def foo(a: str, b: str, c: bool): ... +@overload +def foo(a: bool, b: str, c: str): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: bool, b: int, c: int): ... +@overload +def foo(a: int, b: bool, c: int): ... +@overload +def foo(a: int, b: int, c: bool): ... +@overload +def foo(a: bool, b: bool, c: int): ... +@overload +def foo(a: int, b: bool, c: bool): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: float, b: bool, c: bool): ... +@overload +def foo(a: bool, b: float, c: bool): ... +@overload +def foo(a: bool, b: bool, c: float): ... +@overload +def foo(a: float, b: float, c: bool): ... +@overload +def foo(a: bool, b: float, c: float): ... +def foo(a, b, c): ... + +foo(Foo(), Foo()) # error: [no-matching-overload] +``` + +## Calls to overloaded functions with lots of parameters + +```py +from typing import overload + +@overload +def f( + lion: int, + turtle: int, + tortoise: int, + goat: int, + capybara: int, + chicken: int, + ostrich: int, + gorilla: int, + giraffe: int, + condor: int, + kangaroo: int, + anaconda: int, + tarantula: int, + millipede: int, + leopard: int, + hyena: int, +) -> int: ... +@overload +def f( + lion: str, + turtle: str, + tortoise: str, + goat: str, + capybara: str, + chicken: str, + ostrich: str, + gorilla: str, + giraffe: str, + condor: str, + kangaroo: str, + anaconda: str, + tarantula: str, + millipede: str, + leopard: str, + hyena: str, +) -> str: ... +def f( + lion: int | str, + turtle: int | str, + tortoise: int | str, + goat: int | str, + capybara: int | str, + chicken: int | str, + ostrict: int | str, + gorilla: int | str, + giraffe: int | str, + condor: int | str, + kangaroo: int | str, + anaconda: int | str, + tarantula: int | str, + millipede: int | str, + leopard: int | str, + hyena: int | str, +) -> int | str: + return 0 + +f(b"foo") # error: [no-matching-overload] +``` + +## A method call with unmatched overloads + +```py +from typing import overload + +class Foo: + @overload + def bar(self, x: int) -> int: ... + @overload + def bar(self, x: str) -> str: ... + def bar(self, x: int | str) -> int | str: + return x + +foo = Foo() +foo.bar(b"wat") # error: [no-matching-overload] +``` + +## A class constructor with unmatched overloads + +TODO: At time of writing (2025-05-15), this has non-ideal diagnostics that doesn't show the +unmatched overloads. + +```py +type() # error: [no-matching-overload] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md new file mode 100644 index 0000000000000..0edba939b5f87 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -0,0 +1,349 @@ +# Semantic syntax error diagnostics + +## `async` comprehensions in synchronous comprehensions + +### Python 3.10 + + + +Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even +within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)): + +```toml +[environment] +python-version = "3.10" +``` + +```py +async def elements(n): + yield n + +async def f(): + # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)" + return {n: [x async for x in elements(n)] for n in range(3)} +``` + +If all of the comprehensions are `async`, on the other hand, the code was still valid: + +```py +async def test(): + return [[x async for x in elements(n)] async for n in range(3)] +``` + +These are a couple of tricky but valid cases to check that nested scope handling is wired up +correctly in the `SemanticSyntaxContext` trait: + +```py +async def f(): + [x for x in [1]] and [x async for x in elements(1)] + +async def f(): + def g(): + pass + [x async for x in elements(1)] +``` + +### Python 3.11 + +All of these same examples are valid after Python 3.11: + +```toml +[environment] +python-version = "3.11" +``` + +```py +async def elements(n): + yield n + +async def f(): + return {n: [x async for x in elements(n)] for n in range(3)} +``` + +## Late `__future__` import + +```py +from collections import namedtuple + +# error: [invalid-syntax] "__future__ imports must be at the top of the file" +from __future__ import print_function +``` + +## Invalid annotation + +This one might be a bit redundant with the `invalid-type-form` error. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +# error: [invalid-type-form] "Named expressions are not allowed in type expressions" +# error: [invalid-syntax] "named expression cannot be used within a type annotation" +def f() -> (y := 3): ... +``` + +## Duplicate `match` key + +```toml +[environment] +python-version = "3.10" +``` + +```py +match 2: + # error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`" + case {"x": 1, "x": 2}: + ... +``` + +## Duplicate `match` class attribute + +Attribute names in class patterns must be unique: + +```toml +[environment] +python-version = "3.10" +``` + +```py +class Point: + pass + +obj = Point() +match obj: + # error: [invalid-syntax] "attribute name `x` repeated in class pattern" + case Point(x=1, x=2): + pass +``` + +## `return`, `yield`, `yield from`, and `await` outside function + +```py +# error: [invalid-syntax] "`return` statement outside of a function" +return + +# error: [invalid-syntax] "`yield` statement outside of a function" +yield + +# error: [invalid-syntax] "`yield from` statement outside of a function" +yield from [] + +# error: [invalid-syntax] "`await` statement outside of a function" +# error: [invalid-syntax] "`await` outside of an asynchronous function" +await 1 + +def f(): + # error: [invalid-syntax] "`await` outside of an asynchronous function" + await 1 +``` + +Generators are evaluated lazily, so `await` is allowed, even outside of a function. + +```py +async def g(): + yield 1 + +(x async for x in g()) +``` + +## Rebound comprehension variable + +Walrus operators cannot rebind variables already in use as iterators: + +```py +# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" +[x := 2 for x in range(10)] + +# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" +{y := 5 for y in range(10)} +``` + +## Multiple case assignments + +Variable names in pattern matching must be unique within a single pattern: + +```toml +[environment] +python-version = "3.10" +``` + +```py +x = [1, 2] +match x: + # error: [invalid-syntax] "multiple assignments to name `a` in pattern" + case [a, a]: + pass + case _: + pass + +d = {"key": "value"} +match d: + # error: [invalid-syntax] "multiple assignments to name `b` in pattern" + case {"key": b, "other": b}: + pass +``` + +## Duplicate type parameter + +Type parameter names must be unique in a generic class or function definition: + +```toml +[environment] +python-version = "3.12" +``` + +```py +# error: [invalid-syntax] "duplicate type parameter" +class C[T, T]: + pass + +# error: [invalid-syntax] "duplicate type parameter" +def f[X, Y, X](): + pass +``` + +## Invalid star expression + +Star expressions can't be used in certain contexts: + +```py +def func(): + # error: [invalid-syntax] "Starred expression cannot be used here" + return *[1, 2, 3] + +def gen(): + # error: [invalid-syntax] "Starred expression cannot be used here" + yield * [1, 2, 3] + +# error: [invalid-syntax] "Starred expression cannot be used here" +for *x in range(10): + pass + +# error: [invalid-syntax] "Starred expression cannot be used here" +for x in *range(10): + pass +``` + +## Irrefutable case pattern + +Irrefutable patterns, i.e. wildcard or capture patterns, must be the last case in a match statement. +Following case statements are unreachable. + +```toml +[environment] +python-version = "3.12" +``` + +```py +value = 5 + +match value: + # error: [invalid-syntax] "wildcard makes remaining patterns unreachable" + case _: # Irrefutable wildcard pattern + pass + case 5: + pass + +match value: + # error: [invalid-syntax] "name capture `variable` makes remaining patterns unreachable" + case variable: # Irrefutable capture pattern + pass + case 10: + pass +``` + +## Single starred assignment + +Starred assignment targets cannot appear by themselves. They must be in the context of a list or +tuple. + +```py +# error: [invalid-syntax] "starred assignment target must be in a list or tuple" +*a = [1, 2, 3, 4] +``` + +## Write to debug + +The special Python builtin `__debug__` should not be modified. + +```toml +[environment] +python-version = "3.12" +``` + +```py +# error: [invalid-syntax] "cannot assign to `__debug__`" +__debug__ = False + +# error: [invalid-syntax] "cannot assign to `__debug__`" +def process(__debug__): + pass + +# error: [invalid-syntax] "cannot assign to `__debug__`" +class Generic[__debug__]: + pass +``` + +## Invalid expression + +Certain expressions like `yield` or inlined walrus assignments are not valid in specific contexts. + +```toml +[environment] +python-version = "3.12" +``` + +```py +def _(): + # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" + # error: [invalid-syntax] "yield expression cannot be used within a TypeVar bound" + type X[T: (yield 1)] = int + +def _(): + # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" + # error: [invalid-syntax] "yield expression cannot be used within a type alias" + type Y = (yield 1) + +# error: [invalid-type-form] "Named expressions are not allowed in type expressions" +# error: [invalid-syntax] "named expression cannot be used within a generic definition" +def f[T](x: int) -> (y := 3): + return x + +def _(): + # error: [invalid-syntax] "yield expression cannot be used within a generic definition" + class C[T]((yield from [object])): + pass +``` + +## `await` outside async function + +This error includes `await`, `async for`, `async with`, and `async` comprehensions. + +```py +async def elements(n): + yield n + +def _(): + # error: [invalid-syntax] "`await` outside of an asynchronous function" + await 1 + # error: [invalid-syntax] "`async for` outside of an asynchronous function" + async for _ in elements(1): + ... + # error: [invalid-syntax] "`async with` outside of an asynchronous function" + async with elements(1) as x: + ... + # error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function" + [x async for x in elements(1)] +``` + +## Load before `global` declaration + +```py +def f(): + x = 1 + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md new file mode 100644 index 0000000000000..c63631c1e43a7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md @@ -0,0 +1,19 @@ +# Shadowing + + + +## Implicit class shadowing + +```py +class C: ... + +C = 1 # error: [invalid-assignment] +``` + +## Implicit function shadowing + +```py +def f(): ... + +f = 1 # error: [invalid-assignment] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md new file mode 100644 index 0000000000000..63b762b320d7d --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md @@ -0,0 +1,168 @@ +# Single matching overload + + + +## Limited number of overloads + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def f() -> None: ... +@overload +def f(x: int) -> int: ... +@overload +def f(x: int, y: int) -> int: ... +``` + +```py +from overloaded import f + +f("a") # error: [invalid-argument-type] +``` + +## Call to function with too many unmatched overloads + +This has an excessive number of overloads to the point that ty will cut off the list in the +diagnostic and emit a message stating the number of omitted overloads. + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def foo(a: int): ... +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: str, b: int, c: int): ... +@overload +def foo(a: int, b: str, c: int): ... +@overload +def foo(a: int, b: int, c: str): ... +@overload +def foo(a: str, b: str, c: int): ... +@overload +def foo(a: int, b: str, c: str): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: float, b: int, c: int): ... +@overload +def foo(a: int, b: float, c: int): ... +@overload +def foo(a: int, b: int, c: float): ... +@overload +def foo(a: float, b: float, c: int): ... +@overload +def foo(a: int, b: float, c: float): ... +@overload +def foo(a: float, b: float, c: float): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: float, b: str, c: str): ... +@overload +def foo(a: str, b: float, c: str): ... +@overload +def foo(a: str, b: str, c: float): ... +@overload +def foo(a: float, b: float, c: str): ... +@overload +def foo(a: str, b: float, c: float): ... +@overload +def foo(a: float, b: float, c: float): ... +@overload +def foo(a: list[int], b: list[int], c: list[int]): ... +@overload +def foo(a: list[str], b: list[int], c: list[int]): ... +@overload +def foo(a: list[int], b: list[str], c: list[int]): ... +@overload +def foo(a: list[int], b: list[int], c: list[str]): ... +@overload +def foo(a: list[str], b: list[str], c: list[int]): ... +@overload +def foo(a: list[int], b: list[str], c: list[str]): ... +@overload +def foo(a: list[str], b: list[str], c: list[str]): ... +@overload +def foo(a: list[int], b: list[int], c: list[int]): ... +@overload +def foo(a: list[float], b: list[int], c: list[int]): ... +@overload +def foo(a: list[int], b: list[float], c: list[int]): ... +@overload +def foo(a: list[int], b: list[int], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[int]): ... +@overload +def foo(a: list[int], b: list[float], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[float]): ... +@overload +def foo(a: list[str], b: list[str], c: list[str]): ... +@overload +def foo(a: list[float], b: list[str], c: list[str]): ... +@overload +def foo(a: list[str], b: list[float], c: list[str]): ... +@overload +def foo(a: list[str], b: list[str], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[str]): ... +@overload +def foo(a: list[str], b: list[float], c: list[float]): ... +@overload +def foo(a: list[float], b: list[float], c: list[float]): ... +@overload +def foo(a: bool, b: bool, c: bool): ... +@overload +def foo(a: str, b: bool, c: bool): ... +@overload +def foo(a: bool, b: str, c: bool): ... +@overload +def foo(a: bool, b: bool, c: str): ... +@overload +def foo(a: str, b: str, c: bool): ... +@overload +def foo(a: bool, b: str, c: str): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: int, b: int, c: int): ... +@overload +def foo(a: bool, b: int, c: int): ... +@overload +def foo(a: int, b: bool, c: int): ... +@overload +def foo(a: int, b: int, c: bool): ... +@overload +def foo(a: bool, b: bool, c: int): ... +@overload +def foo(a: int, b: bool, c: bool): ... +@overload +def foo(a: str, b: str, c: str): ... +@overload +def foo(a: float, b: bool, c: bool): ... +@overload +def foo(a: bool, b: float, c: bool): ... +@overload +def foo(a: bool, b: bool, c: float): ... +@overload +def foo(a: float, b: float, c: bool): ... +@overload +def foo(a: bool, b: float, c: float): ... +``` + +```py +from typing import overload + +from overloaded import foo + +foo("foo") # error: [invalid-argument-type] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md new file mode 100644 index 0000000000000..4f406c165f754 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md @@ -0,0 +1,140 @@ +# Calling a union of function types + + + +```toml +[environment] +python-version = "3.12" +``` + +## A smaller scale example + +```py +def f1() -> int: + return 0 + +def f2(name: str) -> int: + return 0 + +def _(flag: bool): + if flag: + f = f1 + else: + f = f2 + # error: [too-many-positional-arguments] + # error: [invalid-argument-type] + x = f(3) +``` + +## Multiple variants but only one is invalid + +This test in particular demonstrates some of the smarts of this diagnostic. Namely, since only one +variant is invalid, additional context specific to that variant is added to the diagnostic output. +(If more than one variant is invalid, then this additional context is elided to avoid overwhelming +the end user.) + +```py +def f1(a: int) -> int: + return 0 + +def f2(name: str) -> int: + return 0 + +def _(flag: bool): + if flag: + f = f1 + else: + f = f2 + # error: [invalid-argument-type] + x = f(3) +``` + +## Try to cover all possible reasons + +These tests is likely to become stale over time, but this was added when the union-specific +diagnostic was initially created. In each test, we try to cover as much as we can. This is mostly +just ensuring that we get test coverage for each of the possible diagnostic messages. + +### Cover non-keyword related reasons + +```py +from inspect import getattr_static +from typing import overload + +def f1() -> int: + return 0 + +def f2(name: str) -> int: + return 0 + +def f3(a: int, b: int) -> int: + return 0 + +def f4[T: str](x: T) -> int: + return 0 + +@overload +def f5() -> None: ... +@overload +def f5(x: str) -> str: ... +def f5(x: str | None = None) -> str | None: + return x + +@overload +def f6() -> None: ... +@overload +def f6(x: str, y: str) -> str: ... +def f6(x: str | None = None, y: str | None = None) -> str | None: + return x + y if x and y else None + +def _(n: int): + class PossiblyNotCallable: + if n == 0: + def __call__(self) -> int: + return 0 + + if n == 0: + f = f1 + elif n == 1: + f = f2 + elif n == 2: + f = f3 + elif n == 3: + f = f4 + elif n == 4: + f = 5 + elif n == 5: + f = f5 + elif n == 6: + f = f6 + else: + f = PossiblyNotCallable() + # error: [too-many-positional-arguments] + # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`" + # error: [missing-argument] + # error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`" + # error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`" + # error: [no-matching-overload] "No overload of function `f6` matches arguments" + # error: [call-non-callable] "Object of type `Literal[5]` is not callable" + # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" + x = f(3) +``` + +### Cover keyword argument related reasons + +```py +def any(*args, **kwargs) -> int: + return 0 + +def f1(name: str) -> int: + return 0 + +def _(n: int): + if n == 0: + f = f1 + else: + f = any + # error: [parameter-already-assigned] + # error: [unknown-argument] + y = f("foo", name="bar", unknown="quux") +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md new file mode 100644 index 0000000000000..3c731106f332c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md @@ -0,0 +1,27 @@ +# Unpacking + + + +## Right hand side not iterable + +```py +a, b = 1 # error: [not-iterable] +``` + +## Exactly too many values to unpack + +```py +a, b = (1, 2, 3) # error: [invalid-assignment] +``` + +## Exactly too few values to unpack + +```py +a, b = (1,) # error: [invalid-assignment] +``` + +## Too few values to unpack + +```py +[a, *b, c, d] = (1, 2) # error: [invalid-assignment] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md index 059cb4e5e9a8d..c07a3a2067141 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md @@ -40,8 +40,8 @@ stat = add(10, 15) ## Using `from` with an unknown current module -This is another case handled separately in Red Knot, where a `.` provokes relative module name -resolution, but where the module name is not resolvable. +This is another case handled separately in ty, where a `.` provokes relative module name resolution, +but where the module name is not resolvable. ```py from .does_not_exist import add # error: [unresolved-import] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md new file mode 100644 index 0000000000000..6be987a4e2eb4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md @@ -0,0 +1,14 @@ +# Diagnostics for unresolved references + +## New builtin used on old Python version + + + +```toml +[environment] +python-version = "3.9" +``` + +```py +aiter # error: [unresolved-reference] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md new file mode 100644 index 0000000000000..9cb91b89fdaff --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md @@ -0,0 +1,61 @@ + + +# Different ways that `unsupported-bool-conversion` can occur + +## Has a `__bool__` method, but has incorrect parameters + +```py +class NotBoolable: + def __bool__(self, foo): + return False + +a = NotBoolable() + +# error: [unsupported-bool-conversion] +10 and a and True +``` + +## Has a `__bool__` method, but has an incorrect return type + +```py +class NotBoolable: + def __bool__(self) -> str: + return "wat" + +a = NotBoolable() + +# error: [unsupported-bool-conversion] +10 and a and True +``` + +## Has a `__bool__` attribute, but it's not callable + +```py +class NotBoolable: + __bool__: int = 3 + +a = NotBoolable() + +# error: [unsupported-bool-conversion] +10 and a and True +``` + +## Part of a union where at least one member has incorrect `__bool__` method + +```py +class NotBoolable1: + def __bool__(self) -> str: + return "wat" + +class NotBoolable2: + pass + +class NotBoolable3: + __bool__: int = 3 + +def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3: + return NotBoolable2() + +# error: [unsupported-bool-conversion] +10 and get() and True +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md new file mode 100644 index 0000000000000..0a35fe4153c00 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md @@ -0,0 +1,37 @@ +# Version-related syntax error diagnostics + +## `match` statement + +The `match` statement was introduced in Python 3.10. + +### Before 3.10 + + + +We should emit a syntax error before 3.10. + +```toml +[environment] +python-version = "3.9" +``` + +```py +match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" + case 1: + print("it's one") +``` + +### After 3.10 + +On or after 3.10, no error should be reported. + +```toml +[environment] +python-version = "3.10" +``` + +```py +match 2: + case 1: + print("it's one") +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md similarity index 75% rename from crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md rename to crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index b4d51b3927841..abb5117564c9f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -2,30 +2,63 @@ ## Basic functionality -`assert_never` makes sure that the type of the argument is `Never`. If it is not, a -`type-assertion-failure` diagnostic is emitted. +`assert_never` makes sure that the type of the argument is `Never`. + +### Correct usage ```py from typing_extensions import assert_never, Never, Any -from knot_extensions import Unknown +from ty_extensions import Unknown -def _(never: Never, any_: Any, unknown: Unknown, flag: bool): +def _(never: Never): assert_never(never) # fine +``` + +### Diagnostics + + + +If it is not, a `type-assertion-failure` diagnostic is emitted. +```py +from typing_extensions import assert_never, Never, Any +from ty_extensions import Unknown + +def _(): assert_never(0) # error: [type-assertion-failure] + +def _(): assert_never("") # error: [type-assertion-failure] + +def _(): assert_never(None) # error: [type-assertion-failure] + +def _(): assert_never([]) # error: [type-assertion-failure] + +def _(): assert_never({}) # error: [type-assertion-failure] + +def _(): assert_never(()) # error: [type-assertion-failure] + +def _(flag: bool, never: Never): assert_never(1 if flag else never) # error: [type-assertion-failure] +def _(any_: Any): assert_never(any_) # error: [type-assertion-failure] + +def _(unknown: Unknown): assert_never(unknown) # error: [type-assertion-failure] ``` ## Use case: Type narrowing and exhaustiveness checking +```toml +[environment] +python-version = "3.10" +``` + `assert_never` can be used in combination with type narrowing as a way to make sure that all cases are handled in a series of `isinstance` checks or other narrowing patterns that are supported. @@ -53,7 +86,7 @@ def if_else_isinstance_error(obj: A | B): elif isinstance(obj, C): pass else: - # error: [type-assertion-failure] "Expected type `Never`, got `B & ~A & ~C` instead" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) def if_else_singletons_success(obj: Literal[1, "a"] | None): @@ -74,7 +107,7 @@ def if_else_singletons_error(obj: Literal[1, "a"] | None): elif obj is None: pass else: - # error: [type-assertion-failure] "Expected type `Never`, got `Literal["a"]` instead" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) def match_singletons_success(obj: Literal[1, "a"] | None): @@ -87,7 +120,7 @@ def match_singletons_success(obj: Literal[1, "a"] | None): pass case _ as obj: # TODO: Ideally, we would not emit an error here - # error: [type-assertion-failure] "Expected type `Never`, got `@Todo" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) def match_singletons_error(obj: Literal[1, "a"] | None): @@ -101,6 +134,6 @@ def match_singletons_error(obj: Literal[1, "a"] | None): case _ as obj: # TODO: We should emit an error here, but the message should # show the type `Literal["a"]` instead of `@Todo(…)`. - # error: [type-assertion-failure] "Expected type `Never`, got `@Todo" + # error: [type-assertion-failure] "Argument does not have asserted type `Never`" assert_never(obj) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/directives/assert_type.md rename to crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index af0291493d63d..07ad5d555b07e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -2,6 +2,8 @@ ## Basic + + ```py from typing_extensions import assert_type @@ -55,7 +57,7 @@ def _(a: type[int]): from typing import Any from typing_extensions import Literal, assert_type -from knot_extensions import Unknown +from ty_extensions import Unknown # Any and Unknown are considered equivalent def _(a: Unknown, b: Any): @@ -80,7 +82,7 @@ Tuple types with the same elements are the same. ```py from typing_extensions import Any, assert_type -from knot_extensions import Unknown +from ty_extensions import Unknown def _(a: tuple[int, str, bytes]): assert_type(a, tuple[int, str, bytes]) # fine @@ -122,7 +124,7 @@ regardless of order. ```py from typing_extensions import assert_type -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class A: ... class B: ... diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md new file mode 100644 index 0000000000000..6943eceee6c9b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -0,0 +1,80 @@ +# `cast` + +`cast()` takes two arguments, one type and one value, and returns a value of the given type. + +The (inferred) type of the value and the given type do not need to have any correlation. + +```py +from typing import Literal, cast, Any + +reveal_type(True) # revealed: Literal[True] +reveal_type(cast(str, True)) # revealed: str +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 + +# 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") + +def function_returning_int() -> int: + return 10 + +# error: [redundant-cast] "Value is already of type `int`" +cast(int, function_returning_int()) + +def function_returning_any() -> Any: + return "blah" + +# error: [redundant-cast] "Value is already of type `Any`" +cast(Any, function_returning_any()) +``` + +Complex type expressions (which may be unsupported) do not lead to spurious `[redundant-cast]` +diagnostics. + +```py +from typing import Callable + +def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]): + a = cast(Callable[[list[bytes]], None], x) + b = cast(tuple[list[bytes]], y) +``` + +A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are +understood as gradually equivalent types by ty, they are understood as different types by human +readers of ty's output. For `Unknown` in particular, we may consider it differently in the context +of some opt-in diagnostics, as it indicates that the gradual type has come about due to an invalid +annotation, missing annotation or missing type argument somewhere. + +A cast from `Unknown` to `Todo` or `Any` is also not considered a "redundant cast", as this breaks +the gradual guarantee and leads to cascading errors when an object is inferred as having type +`Unknown` due to a missing import or similar. + +```py +from ty_extensions import Unknown + +def f(x: Any, y: Unknown, z: Any | str | int): + a = cast(dict[str, Any], x) + reveal_type(a) # revealed: dict[str, Any] + + b = cast(Any, y) + reveal_type(b) # revealed: Any + + c = cast(Unknown, y) + reveal_type(c) # revealed: Unknown + + d = cast(Unknown, x) + reveal_type(d) # revealed: Unknown + + e = cast(str | int | Any, z) # error: [redundant-cast] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/doc/README.md b/crates/ty_python_semantic/resources/mdtest/doc/README.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/doc/README.md rename to crates/ty_python_semantic/resources/mdtest/doc/README.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md b/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md rename to crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md index 72676dfbebf63..50e73bef0f928 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md +++ b/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md @@ -42,7 +42,7 @@ def f(w: Wrapper) -> None: v: int | None = w.value # This function call is incorrect, because `w.value` could be `None`. We therefore emit the following - # error: "Argument to this function is incorrect: Expected `int`, found `Unknown | None`" + # error: "Argument to function `accepts_int` is incorrect: Expected `int`, found `Unknown | None`" c = accepts_int(w.value) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/exception/basic.md b/crates/ty_python_semantic/resources/mdtest/exception/basic.md new file mode 100644 index 0000000000000..87f05e55fcda3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/exception/basic.md @@ -0,0 +1,280 @@ +# Exception Handling + +## Single Exception + +```py +import re + +try: + help() +except NameError as e: + reveal_type(e) # revealed: NameError +except re.error as f: + reveal_type(f) # revealed: error +``` + +## Unknown type in except handler does not cause spurious diagnostic + +```py +from nonexistent_module import foo # error: [unresolved-import] + +try: + help() +except foo as e: + reveal_type(foo) # revealed: Unknown + reveal_type(e) # revealed: Unknown +``` + +## Multiple Exceptions in a Tuple + +```py +EXCEPTIONS = (AttributeError, TypeError) + +try: + help() +except (RuntimeError, OSError) as e: + reveal_type(e) # revealed: RuntimeError | OSError +except EXCEPTIONS as f: + reveal_type(f) # revealed: AttributeError | TypeError +``` + +## Dynamic exception types + +```py +def foo( + x: type[AttributeError], + y: tuple[type[OSError], type[RuntimeError]], + z: tuple[type[BaseException], ...], + zz: tuple[type[TypeError | RuntimeError], ...], + zzz: type[BaseException] | tuple[type[BaseException], ...], +): + try: + help() + except x as e: + reveal_type(e) # revealed: AttributeError + except y as f: + reveal_type(f) # revealed: OSError | RuntimeError + except z as g: + reveal_type(g) # revealed: BaseException + except zz as h: + reveal_type(h) # revealed: TypeError | RuntimeError + except zzz as i: + reveal_type(i) # revealed: BaseException +``` + +We do not emit an `invalid-exception-caught` if a class is caught that has `Any` or `Unknown` in its +MRO, as the dynamic element in the MRO could materialize to some subclass of `BaseException`: + +```py +from compat import BASE_EXCEPTION_CLASS # error: [unresolved-import] "Cannot resolve imported module `compat`" + +class Error(BASE_EXCEPTION_CLASS): ... + +try: + ... +except Error as err: + ... +``` + +## Exception with no captured type + +```py +try: + {}.get("foo") +except TypeError: + pass +``` + +## Exception which catches typevar + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable + +def silence[T: type[BaseException]]( + func: Callable[[], None], + exception_type: T, +): + try: + func() + except exception_type as e: + reveal_type(e) # revealed: T'instance + +def silence2[T: ( + type[ValueError], + type[TypeError], +)](func: Callable[[], None], exception_type: T,): + try: + func() + except exception_type as e: + reveal_type(e) # revealed: T'instance +``` + +## Invalid exception handlers + +```py +try: + pass +# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +except 3 as e: + reveal_type(e) # revealed: Unknown + +try: + pass +# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +except (ValueError, OSError, "foo", b"bar") as e: + reveal_type(e) # revealed: ValueError | OSError | Unknown + +def foo( + x: type[str], + y: tuple[type[OSError], type[RuntimeError], int], + z: tuple[type[str], ...], +): + try: + help() + # error: [invalid-exception-caught] + except x as e: + reveal_type(e) # revealed: Unknown + # error: [invalid-exception-caught] + except y as f: + reveal_type(f) # revealed: OSError | RuntimeError | Unknown + # error: [invalid-exception-caught] + except z as g: + reveal_type(g) # revealed: Unknown + +try: + {}.get("foo") +# error: [invalid-exception-caught] +except int: + pass +``` + +## Object raised is not an exception + +```py +try: + raise AttributeError() # fine +except: + ... + +try: + raise FloatingPointError # fine +except: + ... + +try: + raise 1 # error: [invalid-raise] +except: + ... + +try: + raise int # error: [invalid-raise] +except: + ... + +def _(e: Exception | type[Exception]): + raise e # fine + +def _(e: Exception | type[Exception] | None): + raise e # error: [invalid-raise] +``` + +## Exception cause is not an exception + +```py +def _(): + try: + raise EOFError() from GeneratorExit # fine + except: + ... + +def _(): + try: + raise StopIteration from MemoryError() # fine + except: + ... + +def _(): + try: + raise BufferError() from None # fine + except: + ... + +def _(): + try: + raise ZeroDivisionError from False # error: [invalid-raise] + except: + ... + +def _(): + try: + raise SystemExit from bool() # error: [invalid-raise] + except: + ... + +def _(): + try: + raise + except KeyboardInterrupt as e: # fine + reveal_type(e) # revealed: KeyboardInterrupt + raise LookupError from e # fine + +def _(): + try: + raise + except int as e: # error: [invalid-exception-caught] + reveal_type(e) # revealed: Unknown + raise KeyError from e + +def _(e: Exception | type[Exception]): + raise ModuleNotFoundError from e # fine + +def _(e: Exception | type[Exception] | None): + raise IndexError from e # fine + +def _(e: int | None): + raise IndexError from e # error: [invalid-raise] +``` + +## The caught exception is cleared at the end of the except clause + +```py +e = None +reveal_type(e) # revealed: None + +try: + raise ValueError() +except ValueError as e: + reveal_type(e) # revealed: ValueError +# error: [unresolved-reference] +reveal_type(e) # revealed: Unknown + +e = None + +def cond() -> bool: + return True + +try: + if cond(): + raise ValueError() +except ValueError as e: + reveal_type(e) # revealed: ValueError +# error: [possibly-unresolved-reference] +reveal_type(e) # revealed: None + +def f(x: type[Exception]): + e = None + try: + raise x + except ValueError as e: + pass + except: + pass + # error: [possibly-unresolved-reference] + reveal_type(e) # revealed: None +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md b/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md rename to crates/ty_python_semantic/resources/mdtest/exception/control_flow.md index 2db3cf362d479..fe1e253c31757 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md +++ b/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md @@ -600,12 +600,12 @@ except: reveal_type(x) # revealed: E x = Bar - reveal_type(x) # revealed: Literal[Bar] + reveal_type(x) # revealed: finally: - # TODO: should be `Literal[1] | Literal[foo] | Literal[Bar]` - reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar] + # TODO: should be `Literal[1] | | ` + reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | -reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar] +reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | ``` [1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d diff --git a/crates/ty_python_semantic/resources/mdtest/exception/except_star.md b/crates/ty_python_semantic/resources/mdtest/exception/except_star.md new file mode 100644 index 0000000000000..36ccd7d82a3e9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/exception/except_star.md @@ -0,0 +1,72 @@ +# `except*` + +`except*` is only available in Python 3.11 and later: + +```toml +[environment] +python-version = "3.11" +``` + +## `except*` with `BaseException` + +```py +try: + help() +except* BaseException as e: + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] +``` + +## `except*` with specific exception + +```py +try: + help() +except* OSError as e: + reveal_type(e) # revealed: ExceptionGroup[OSError] +``` + +## `except*` with multiple exceptions + +```py +try: + help() +except* (TypeError, AttributeError) as e: + reveal_type(e) # revealed: ExceptionGroup[TypeError | AttributeError] +``` + +## `except*` with mix of `Exception`s and `BaseException`s + +```py +try: + help() +except* (KeyboardInterrupt, AttributeError) as e: + reveal_type(e) # revealed: BaseExceptionGroup[KeyboardInterrupt | AttributeError] +``` + +## `except*` with no captured exception type + +```py +try: + help() +except* TypeError: + pass +``` + +## Invalid `except*` handlers with or without a captured exception type + +```py +try: + help() +except* int: # error: [invalid-exception-caught] + pass + +try: + help() +except* 3 as e: # error: [invalid-exception-caught] + reveal_type(e) # revealed: BaseExceptionGroup[Unknown] + +try: + help() +except* (AttributeError, 42) as e: # error: [invalid-exception-caught] + reveal_type(e) # revealed: BaseExceptionGroup[AttributeError | Unknown] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/exception/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/exception/invalid_syntax.md diff --git a/crates/ty_python_semantic/resources/mdtest/expression/assert.md b/crates/ty_python_semantic/resources/mdtest/expression/assert.md new file mode 100644 index 0000000000000..ddb429a576e09 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/expression/assert.md @@ -0,0 +1,9 @@ +## Condition with object that implements `__bool__` incorrectly + +```py +class NotBoolable: + __bool__: int = 3 + +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +assert NotBoolable() +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md rename to crates/ty_python_semantic/resources/mdtest/expression/attribute.md index 1ccf74edd4efe..43df56264cb5c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md @@ -26,9 +26,9 @@ def _(flag: bool): reveal_type(A.union_declared) # revealed: int | str - # error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound" + # error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `` is possibly unbound" reveal_type(A.possibly_unbound) # revealed: str - # error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`" + # error: [unresolved-attribute] "Type `` has no attribute `non_existent`" reveal_type(A.non_existent) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md new file mode 100644 index 0000000000000..7a9f25f637403 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -0,0 +1,155 @@ +# Expressions + +## OR + +```py +def _(foo: str): + reveal_type(True or False) # revealed: Literal[True] + reveal_type("x" or "y" or "z") # revealed: Literal["x"] + reveal_type("" or "y" or "z") # revealed: Literal["y"] + reveal_type(False or "z") # revealed: Literal["z"] + reveal_type(False or True) # revealed: Literal[True] + reveal_type(False or False) # revealed: Literal[False] + reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False] + reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True] +``` + +## AND + +```py +def _(foo: str): + reveal_type(True and False) # revealed: Literal[False] + reveal_type(False and True) # revealed: Literal[False] + reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False] + reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True] + reveal_type("x" and "y" and "z") # revealed: Literal["z"] + reveal_type("x" and "y" and "") # revealed: Literal[""] + reveal_type("" and "y") # revealed: Literal[""] +``` + +## Simple function calls to bool + +```py +def _(flag: bool): + if flag: + x = True + else: + x = False + + reveal_type(x) # revealed: bool +``` + +## Complex + +```py +reveal_type("x" and "y" or "z") # revealed: Literal["y"] +reveal_type("x" or "y" and "z") # revealed: Literal["x"] +reveal_type("" and "y" or "z") # revealed: Literal["z"] +reveal_type("" or "y" and "z") # revealed: Literal["z"] +reveal_type("x" and "y" or "") # revealed: Literal["y"] +reveal_type("x" or "y" and "") # revealed: Literal["x"] +``` + +## `bool()` function + +## Evaluates to builtin + +`a.py`: + +```py +redefined_builtin_bool: type[bool] = bool + +def my_bool(x) -> bool: + return True +``` + +```py +from a import redefined_builtin_bool, my_bool + +reveal_type(redefined_builtin_bool(0)) # revealed: Literal[False] +reveal_type(my_bool(0)) # revealed: bool +``` + +## Truthy values + +```py +reveal_type(bool(1)) # revealed: Literal[True] +reveal_type(bool((0,))) # revealed: Literal[True] +reveal_type(bool("NON EMPTY")) # revealed: Literal[True] +reveal_type(bool(True)) # revealed: Literal[True] + +def foo(): ... + +reveal_type(bool(foo)) # revealed: Literal[True] +``` + +## Falsy values + +```py +reveal_type(bool(0)) # revealed: Literal[False] +reveal_type(bool(())) # revealed: Literal[False] +reveal_type(bool(None)) # revealed: Literal[False] +reveal_type(bool("")) # revealed: Literal[False] +reveal_type(bool(False)) # revealed: Literal[False] +reveal_type(bool()) # revealed: Literal[False] +``` + +## Ambiguous values + +```py +reveal_type(bool([])) # revealed: bool +reveal_type(bool({})) # revealed: bool +reveal_type(bool(set())) # revealed: bool +``` + +## `__bool__` returning `NoReturn` + +```py +from typing import NoReturn + +class NotBoolable: + def __bool__(self) -> NoReturn: + raise NotImplementedError("This object can't be converted to a boolean") + +# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't +# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix. +if NotBoolable(): + ... +``` + +## Not callable `__bool__` + +```py +class NotBoolable: + __bool__: None = None + +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" +if NotBoolable(): + ... +``` + +## Not-boolable union + +```py +def test(cond: bool): + class NotBoolable: + __bool__: int | None = None if cond else 3 + + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" + if NotBoolable(): + ... +``` + +## Union with some variants implementing `__bool__` incorrectly + +```py +def test(cond: bool): + class NotBoolable: + __bool__: None = None + + a = 10 if cond else NotBoolable() + + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`" + if a: + ... +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/if.md b/crates/ty_python_semantic/resources/mdtest/expression/if.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/if.md rename to crates/ty_python_semantic/resources/mdtest/expression/if.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md similarity index 75% rename from crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md rename to crates/ty_python_semantic/resources/mdtest/expression/lambda.md index 9db77532cb8a5..b48efaad70bf4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md @@ -79,15 +79,13 @@ lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1] Using a variadic parameter: ```py -# TODO: should be `tuple[Unknown, ...]` (needs generics) -lambda *args: reveal_type(args) # revealed: tuple +lambda *args: reveal_type(args) # revealed: tuple[Unknown, ...] ``` -Using a keyword-varidic parameter: +Using a keyword-variadic parameter: ```py -# TODO: should be `dict[str, Unknown]` (needs generics) -lambda **kwargs: reveal_type(kwargs) # revealed: dict +lambda **kwargs: reveal_type(kwargs) # revealed: dict[str, Unknown] ``` ## Nested `lambda` expressions @@ -117,3 +115,22 @@ a5: Callable[[], None] = lambda x: None # error: [invalid-assignment] a6: Callable[[int], None] = lambda: None ``` + +## Function-like behavior of lambdas + +All `lambda` functions are instances of `types.FunctionType` and should have access to the same set +of attributes. + +```py +x = lambda y: y + +reveal_type(x.__code__) # revealed: CodeType +reveal_type(x.__name__) # revealed: str +reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None +reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)] +reveal_type(x.__dict__) # revealed: dict[str, Any] +reveal_type(x.__doc__) # revealed: str | None +reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None +reveal_type(x.__module__) # revealed: str +reveal_type(x.__qualname__) # revealed: str +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md b/crates/ty_python_semantic/resources/mdtest/expression/len.md similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/expression/len.md rename to crates/ty_python_semantic/resources/mdtest/expression/len.md index 209272b0130f1..6dd80b10db7e5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/len.md @@ -11,30 +11,28 @@ reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14] reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11] reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7] -reveal_type( # revealed: Literal[7] - len( +# fmt: off + +reveal_type(len( # revealed: Literal[7] """foo bar""" - ) -) -reveal_type( # revealed: Literal[9] - len( +)) + +reveal_type(len( # revealed: Literal[9] r"""foo\r bar""" - ) -) -reveal_type( # revealed: Literal[7] - len( +)) + +reveal_type(len( # revealed: Literal[7] b"""foo bar""" - ) -) -reveal_type( # revealed: Literal[9] - len( +)) +reveal_type(len( # revealed: Literal[9] rb"""foo\r bar""" - ) -) +)) + +# fmt: on ``` ### Tuples @@ -43,22 +41,22 @@ bar""" reveal_type(len(())) # revealed: Literal[0] reveal_type(len((1,))) # revealed: Literal[1] reveal_type(len((1, 2))) # revealed: Literal[2] - -# TODO: Handle constructor calls -reveal_type(len(tuple())) # revealed: int +reveal_type(len(tuple())) # revealed: Literal[0] # TODO: Handle star unpacks; Should be: Literal[0] reveal_type(len((*[],))) # revealed: Literal[1] +# fmt: off + # TODO: Handle star unpacks; Should be: Literal[1] -reveal_type( # revealed: Literal[2] - len( - ( - *[], - 1, - ) +reveal_type(len( # revealed: Literal[2] + ( + *[], + 1, ) -) +)) + +# fmt: on # TODO: Handle star unpacks; Should be: Literal[2] reveal_type(len((*[], 1, 2))) # revealed: Literal[3] diff --git a/crates/red_knot_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/final.md rename to crates/ty_python_semantic/resources/mdtest/final.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/function/parameters.md rename to crates/ty_python_semantic/resources/mdtest/function/parameters.md index 8c7cdfd42ca5b..eb7316fe91a2d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/ty_python_semantic/resources/mdtest/function/parameters.md @@ -25,12 +25,8 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, reveal_type(f) # revealed: Literal[4] reveal_type(g) # revealed: Unknown | Literal[5] reveal_type(h) # revealed: Literal[6] - - # TODO: should be `tuple[object, ...]` (needs generics) - reveal_type(args) # revealed: tuple - - # TODO: should be `dict[str, str]` (needs generics) - reveal_type(kwargs) # revealed: dict + reveal_type(args) # revealed: tuple[object, ...] + reveal_type(kwargs) # revealed: dict[str, str] ``` ## Unannotated variadic parameters @@ -39,11 +35,8 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, ```py def g(*args, **kwargs): - # TODO: should be `tuple[Unknown, ...]` (needs generics) - reveal_type(args) # revealed: tuple - - # TODO: should be `dict[str, Unknown]` (needs generics) - reveal_type(kwargs) # revealed: dict + reveal_type(args) # revealed: tuple[Unknown, ...] + reveal_type(kwargs) # revealed: dict[str, Unknown] ``` ## Annotation is present but not a fully static type @@ -76,6 +69,11 @@ def g(x: Any = "foo"): ## Stub functions +```toml +[environment] +python-version = "3.12" +``` + ### In Protocol ```py diff --git a/crates/ty_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md new file mode 100644 index 0000000000000..1ca2b10dca9b4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -0,0 +1,429 @@ +# Function return type + +When a function's return type is annotated, all return statements are checked to ensure that the +type of the returned value is assignable to the annotated return type. + +## Basic examples + +A return value assignable to the annotated return type is valid. + +```py +def f() -> int: + return 1 +``` + +The type of the value obtained by calling a function is the annotated return type, not the inferred +return type. + +```py +reveal_type(f()) # revealed: int +``` + +A `raise` is equivalent to a return of `Never`, which is assignable to any annotated return type. + +```py +def f() -> str: + raise ValueError() + +reveal_type(f()) # revealed: str +``` + +## Stub functions + +"Stub" function definitions (that is, function definitions with an empty body) are permissible in +stub files, or in a few other locations: Protocol method definitions, abstract methods, and +overloads. In this case the function body is considered to be omitted (thus no return type checking +is performed on it), not assumed to implicitly return `None`. + +A stub function's "empty" body may contain only an optional docstring, followed (optionally) by an +ellipsis (`...`) or `pass`. + +### In stub file + +```pyi +def f() -> int: ... + +def f() -> int: + pass + +def f() -> int: + """Some docstring""" + +def f() -> int: + """Some docstring""" + ... +``` + +### In Protocol + +```toml +[environment] +python-version = "3.12" +``` + +```py +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]): + 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 + +```toml +[environment] +python-version = "3.12" +``` + +```py +from abc import ABC, abstractmethod + +class Foo(ABC): + @abstractmethod + def f(self) -> int: ... + @abstractmethod + 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 + +```py +from typing import overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +def f(x: int | str): + return x +``` + +## Conditional return type + +```py +def f(cond: bool) -> int: + if cond: + return 1 + else: + return 2 + +def f(cond: bool) -> int | None: + if cond: + return 1 + else: + return + +def f(cond: bool) -> int: + if cond: + return 1 + else: + raise ValueError() + +def f(cond: bool) -> str | int: + if cond: + return "a" + else: + return 1 +``` + +## Implicit return type + +```py +def f(cond: bool) -> int | None: + if cond: + return 1 + +# no implicit return +def f() -> int: + if True: + return 1 + +# no implicit return +def f(cond: bool) -> int: + cond = True + if cond: + return 1 + +def f(cond: bool) -> int: + if cond: + cond = True + else: + return 1 + if cond: + return 2 +``` + +## Invalid return type + + + +```py +# error: [invalid-return-type] +def f() -> int: + 1 + +def f() -> str: + # error: [invalid-return-type] + return 1 + +def f() -> int: + # error: [invalid-return-type] + return + +from typing import TypeVar + +T = TypeVar("T") + +# error: [invalid-return-type] +def m(x: T) -> T: ... +``` + +## Invalid return type in stub file + + + +```pyi +def f() -> int: + # error: [invalid-return-type] + return ... + +# error: [invalid-return-type] +def foo() -> int: + print("...") + ... + +# error: [invalid-return-type] +def foo() -> int: + f"""{foo} is a function that ...""" + ... +``` + +## Invalid conditional return type + + + +```py +def f(cond: bool) -> str: + if cond: + return "a" + else: + # error: [invalid-return-type] + return 1 + +def f(cond: bool) -> str: + if cond: + # error: [invalid-return-type] + return 1 + else: + # error: [invalid-return-type] + return 2 +``` + +## Invalid implicit return type + + + +```py +def f() -> None: + if False: + # error: [invalid-return-type] + return 1 + +# error: [invalid-return-type] +def f(cond: bool) -> int: + if cond: + return 1 + +# error: [invalid-return-type] +def f(cond: bool) -> int: + if cond: + raise ValueError() + +# error: [invalid-return-type] +def f(cond: bool) -> int: + if cond: + cond = False + else: + return 1 + if cond: + return 2 +``` + +## Invalid implicit return type always None + + + +If the function has no `return` statement or if it has only bare `return` statement (no variable in +the return statement), then we show a diagnostic hint that the return annotation should be `-> None` +or a `return` statement should be added. + +```py +# error: [invalid-return-type] +def f() -> int: + print("hello") +``` + +## NotImplemented + +### Default Python version + +`NotImplemented` is a special symbol in Python. It is commonly used to control the fallback behavior +of special dunder methods. You can find more details in the +[documentation](https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations). + +```py +from __future__ import annotations + +class A: + def __add__(self, o: A) -> A: + return NotImplemented +``` + +However, as shown below, `NotImplemented` should not cause issues with the declared return type. + +```py +def f() -> int: + return NotImplemented + +def f(cond: bool) -> int: + if cond: + return 1 + else: + return NotImplemented + +def f(x: int) -> int | str: + if x < 0: + return -1 + elif x == 0: + return NotImplemented + else: + return "test" + +def f(cond: bool) -> str: + return "hello" if cond else NotImplemented + +def f(cond: bool) -> int: + # error: [invalid-return-type] "Return type does not match returned value: expected `int`, found `Literal["hello"]`" + return "hello" if cond else NotImplemented +``` + +### Python 3.10+ + +Unlike Ellipsis, `_NotImplementedType` remains in `builtins.pyi` regardless of the Python version. +Even if `builtins._NotImplementedType` is fully replaced by `types.NotImplementedType` in the +future, it should still work as expected. + +```toml +[environment] +python-version = "3.10" +``` + +```py +def f() -> int: + return NotImplemented + +def f(cond: bool) -> str: + return "hello" if cond else NotImplemented +``` + +## Generator functions + + + +A function with a `yield` or `yield from` expression anywhere in its body is a +[generator function](https://docs.python.org/3/glossary.html#term-generator). A generator function +implicitly returns an instance of `types.GeneratorType` even if it does not contain any `return` +statements. + +```py +import types +import typing + +def f() -> types.GeneratorType: + yield 42 + +def g() -> typing.Generator: + yield 42 + +def h() -> typing.Iterator: + yield 42 + +def i() -> typing.Iterable: + yield 42 + +def i2() -> typing.Generator: + yield from i() + +def j() -> str: # error: [invalid-return-type] + yield 42 +``` + +If it is an `async` function with a `yield` statement in its body, it is an +[asynchronous generator function](https://docs.python.org/3/glossary.html#term-asynchronous-generator). +An asynchronous generator function implicitly returns an instance of `types.AsyncGeneratorType` even +if it does not contain any `return` statements. + +```py +import types +import typing + +async def f() -> types.AsyncGeneratorType: + yield 42 + +async def g() -> typing.AsyncGenerator: + yield 42 + +async def h() -> typing.AsyncIterator: + yield 42 + +async def i() -> typing.AsyncIterable: + yield 42 + +async def j() -> str: # error: [invalid-return-type] + yield 42 +``` + +## Diagnostics for `invalid-return-type` on non-protocol subclasses of protocol classes + + + +We emit a nice subdiagnostic in this situation explaining the probable error here: + +```py +from typing_extensions import Protocol + +class Abstract(Protocol): + def method(self) -> str: ... + +class Concrete(Abstract): + def method(self) -> str: ... # error: [invalid-return-type] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/builtins.md b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md new file mode 100644 index 0000000000000..e98de384ab480 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md @@ -0,0 +1,36 @@ +# Generic builtins + +## Variadic keyword arguments with a custom `dict` + +When we define `dict` in a custom typeshed, we must take care to define it as a generic class in the +same way as in the real typeshed. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class object: ... +class int: ... +class dict[K, V, Extra]: ... +``` + +`/typeshed/stdlib/typing_extensions.pyi`: + +```pyi +def reveal_type(obj, /): ... +``` + +If we don't, then we may get "surprising" results when inferring the types of variadic keyword +arguments. + +```py +def f(**kwargs): + reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] + +def g(**kwargs: int): + reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md new file mode 100644 index 0000000000000..a15bc32da7f63 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -0,0 +1,611 @@ +# Generic classes: Legacy syntax + +## Defining a generic class + +At its simplest, to define a generic class using the legacy syntax, you inherit from the +`typing.Generic` special form, which is "specialized" with the generic class's type variables. + +```py +from ty_extensions import generic_context +from typing import Generic, TypeVar + +T = TypeVar("T") +S = TypeVar("S") + +class SingleTypevar(Generic[T]): ... +class MultipleTypevars(Generic[T, S]): ... + +reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T] +reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S] +``` + +Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other +class: + +```py +class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base] +class AlsoBad(Generic[T], Generic[S]): ... # error: [duplicate-base] +``` + +You cannot use the same typevar more than once. + +```py +# TODO: error +class RepeatedTypevar(Generic[T, T]): ... +``` + +You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples). + +```py +# error: [invalid-argument-type] "`` is not a valid argument to `Generic`" +class GenericOfType(Generic[int]): ... +``` + +You can also define a generic class by inheriting from some _other_ generic class, and specializing +it with typevars. + +```py +class InheritedGeneric(MultipleTypevars[T, S]): ... +class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ... +class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ... + +reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[T, S] +reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[T] +reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None +``` + +If you don't specialize a generic base class, we use the default specialization, which maps each +typevar to its default value or `Any`. Since that base class is fully specialized, it does not make +the inheriting class generic. + +```py +class InheritedGenericDefaultSpecialization(MultipleTypevars): ... + +reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None +``` + +When inheriting from a generic class, you can optionally inherit from `typing.Generic` as well. But +if you do, you have to mention all of the typevars that you use in your other base classes. + +```py +class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ... + +# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes" +class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ... +class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ... +class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ... + +# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes" +class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ... + +reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: tuple[T, S] +reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) # revealed: tuple[T] +reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) # revealed: tuple[T, S] +``` + +## Specializing generic classes explicitly + +The type parameter can be specified explicitly: + +```py +from typing import Generic, Literal, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + x: T + +reveal_type(C[int]()) # revealed: C[int] +reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]] +``` + +The specialization must match the generic types: + +```py +# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2" +reveal_type(C[int, int]()) # revealed: Unknown +``` + +If the type variable has an upper bound, the specialized type must satisfy that bound: + +```py +from typing import Union + +BoundedT = TypeVar("BoundedT", bound=int) +BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str]) + +class Bounded(Generic[BoundedT]): ... +class BoundedByUnion(Generic[BoundedByUnionT]): ... +class IntSubclass(int): ... + +reveal_type(Bounded[int]()) # revealed: Bounded[int] +reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`" +reveal_type(Bounded[str]()) # revealed: Unknown + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`" +reveal_type(Bounded[int | str]()) # revealed: Unknown + +reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] +reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass] +reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] +reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] +``` + +If the type variable is constrained, the specialized type must satisfy those constraints: + +```py +ConstrainedT = TypeVar("ConstrainedT", int, str) + +class Constrained(Generic[ConstrainedT]): ... + +reveal_type(Constrained[int]()) # revealed: Constrained[int] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Constrained[Unknown] +reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] + +reveal_type(Constrained[str]()) # revealed: Constrained[str] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Unknown +reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`" +reveal_type(Constrained[object]()) # revealed: Unknown +``` + +If the type variable has a default, it can be omitted: + +```py +WithDefaultU = TypeVar("WithDefaultU", default=int) + +class WithDefault(Generic[T, WithDefaultU]): ... + +reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] +reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] +``` + +## Inferring generic class parameters + +We can infer the type parameter from a type context: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + x: T + +c: C[int] = C() +# TODO: revealed: C[int] +reveal_type(c) # revealed: C[Unknown] +``` + +The typevars of a fully specialized generic class should no longer be visible: + +```py +# TODO: revealed: int +reveal_type(c.x) # revealed: Unknown +``` + +If the type parameter is not specified explicitly, and there are no constraints that let us infer a +specific type, we infer the typevar's default type: + +```py +DefaultT = TypeVar("DefaultT", default=int) + +class D(Generic[DefaultT]): ... + +reveal_type(D()) # revealed: D[int] +``` + +If a typevar does not provide a default, we use `Unknown`: + +```py +reveal_type(C()) # revealed: C[Unknown] +``` + +## Inferring generic class parameters from constructors + +If the type of a constructor parameter is a class typevar, we can use that to infer the type +parameter. The types inferred from a type context and from a constructor parameter must be +consistent with each other. + +### `__new__` only + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __new__(cls, x: T) -> "C[T]": + return object.__new__(cls) + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### `__init__` only + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### Identical `__new__` and `__init__` signatures + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __new__(cls, x: T) -> "C[T]": + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### Compatible `__new__` and `__init__` signatures + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __new__(cls, *args, **kwargs) -> "C[T]": + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") + +class D(Generic[T]): + def __new__(cls, x: T) -> "D[T]": + return object.__new__(cls) + + def __init__(self, *args, **kwargs) -> None: ... + +reveal_type(D(1)) # revealed: D[int] + +# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`" +wrong_innards: D[int] = D("five") +``` + +### Both present, `__new__` inherited from a generic base class + +If either method comes from a generic base class, we don't currently use its inferred specialization +to specialize the class. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") + +class C(Generic[T, U]): + def __new__(cls, *args, **kwargs) -> "C[T, U]": + return object.__new__(cls) + +class D(C[V, int]): + def __init__(self, x: V) -> None: ... + +reveal_type(D(1)) # revealed: D[int] +``` + +### `__init__` is itself generic + +```py +from typing import Generic, TypeVar + +S = TypeVar("S") +T = TypeVar("T") + +class C(Generic[T]): + def __init__(self, x: T, y: S) -> None: ... + +reveal_type(C(1, 1)) # revealed: C[int] +reveal_type(C(1, "string")) # revealed: C[int] +reveal_type(C(1, True)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five", 1) +``` + +### Some `__init__` overloads only apply to certain specializations + +```py +from typing import overload, Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + @overload + def __init__(self: "C[str]", x: str) -> None: ... + @overload + def __init__(self: "C[bytes]", x: bytes) -> None: ... + @overload + def __init__(self: "C[int]", x: bytes) -> None: ... + @overload + def __init__(self, x: int) -> None: ... + def __init__(self, x: str | bytes | int) -> None: ... + +reveal_type(C("string")) # revealed: C[str] +reveal_type(C(b"bytes")) # revealed: C[bytes] +reveal_type(C(12)) # revealed: C[Unknown] + +C[str]("string") +C[str](b"bytes") # error: [no-matching-overload] +C[str](12) + +C[bytes]("string") # error: [no-matching-overload] +C[bytes](b"bytes") +C[bytes](12) + +C[int]("string") # error: [no-matching-overload] +C[int](b"bytes") +C[int](12) + +C[None]("string") # error: [no-matching-overload] +C[None](b"bytes") # error: [no-matching-overload] +C[None](12) +``` + +### Synthesized methods with dataclasses + +```py +from dataclasses import dataclass +from typing import Generic, TypeVar + +T = TypeVar("T") + +@dataclass +class A(Generic[T]): + x: T + +reveal_type(A(x=1)) # revealed: A[int] +``` + +## Generic subclass + +When a generic subclass fills its superclass's type parameter with one of its own, the actual types +propagate through: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") +W = TypeVar("W") + +class Parent(Generic[T]): + x: T + +class ExplicitlyGenericChild(Parent[U], Generic[U]): ... +class ExplicitlyGenericGrandchild(ExplicitlyGenericChild[V], Generic[V]): ... +class ExplicitlyGenericGreatgrandchild(ExplicitlyGenericGrandchild[W], Generic[W]): ... +class ImplicitlyGenericChild(Parent[U]): ... +class ImplicitlyGenericGrandchild(ImplicitlyGenericChild[V]): ... +class ImplicitlyGenericGreatgrandchild(ImplicitlyGenericGrandchild[W]): ... + +reveal_type(Parent[int]().x) # revealed: int +reveal_type(ExplicitlyGenericChild[int]().x) # revealed: int +reveal_type(ImplicitlyGenericChild[int]().x) # revealed: int +reveal_type(ExplicitlyGenericGrandchild[int]().x) # revealed: int +reveal_type(ImplicitlyGenericGrandchild[int]().x) # revealed: int +reveal_type(ExplicitlyGenericGreatgrandchild[int]().x) # revealed: int +reveal_type(ImplicitlyGenericGreatgrandchild[int]().x) # revealed: int +``` + +## Generic methods + +Generic classes can contain methods that are themselves generic. The generic methods can refer to +the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in +scope for the method. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class C(Generic[T]): + def method(self, u: U) -> U: + return u + +c: C[int] = C[int]() +reveal_type(c.method("string")) # revealed: Literal["string"] +``` + +## Specializations propagate + +In a specialized generic alias, the specialization is applied to the attributes and methods of the +class. + +```py +from typing import Generic, TypeVar, Protocol + +T = TypeVar("T") +U = TypeVar("U") + +class LinkedList(Generic[T]): ... + +class C(Generic[T, U]): + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> LinkedList[T]: + return LinkedList[T]() + +c = C[int, str]() +reveal_type(c.x) # revealed: int +reveal_type(c.y) # revealed: str +reveal_type(c.method1()) # revealed: int +reveal_type(c.method2()) # revealed: str +reveal_type(c.method3()) # revealed: LinkedList[int] + +class SomeProtocol(Protocol[T]): + x: T + +class Foo(Generic[T]): + x: T + +class D(Generic[T, U]): + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> SomeProtocol[T]: + return Foo() + +d = D[int, str]() +reveal_type(d.x) # revealed: int +reveal_type(d.y) # revealed: str +reveal_type(d.method1()) # revealed: int +reveal_type(d.method2()) # revealed: str +reveal_type(d.method3()) # revealed: SomeProtocol[int] +reveal_type(d.method3().x) # revealed: int +``` + +When a method is overloaded, the specialization is applied to all overloads. + +```py +from typing import overload, Generic, TypeVar + +S = TypeVar("S") + +class WithOverloadedMethod(Generic[T]): + @overload + def method(self, x: T) -> T: + return x + + @overload + def method(self, x: S) -> S | T: + return x + + def method(self, x: S | T) -> S | T: + return x + +reveal_type(WithOverloadedMethod[int].method) # revealed: Overload[(self, x: int) -> int, (self, x: S) -> S | int] +``` + +## Cyclic class definitions + +### F-bounded quantification + +A class can use itself as the type parameter of one of its superclasses. (This is also known as the +[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) + +#### In a stub file + +Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). + +```pyi +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +class Sub(Base[Sub]): ... + +reveal_type(Sub) # revealed: +``` + +#### With string forward references + +A similar case can work in a non-stub file, if forward references are stringified: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +class Sub(Base["Sub"]): ... + +reveal_type(Sub) # revealed: +``` + +#### Without string forward references + +In a non-stub file, without stringified forward references, this raises a `NameError`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... + +# error: [unresolved-reference] +class Sub(Base[Sub]): ... +``` + +### Cyclic inheritance as a generic parameter + +```pyi +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Derived(list[Derived[T]], Generic[T]): ... +``` + +### Direct cyclic inheritance + +Inheritance that would result in a cyclic MRO is detected as an error. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: [unresolved-reference] +class C(C, Generic[T]): ... + +# error: [unresolved-reference] +class D(D[int], Generic[T]): ... +``` + +[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern +[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md new file mode 100644 index 0000000000000..da947217f8a31 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -0,0 +1,360 @@ +# Generic functions: Legacy syntax + +## Typevar must be used at least twice + +If you're only using a typevar for a single parameter, you don't need the typevar — just use +`object` (or the typevar's upper bound): + +```py +from typing import TypeVar + +T = TypeVar("T") + +# TODO: error, should be (x: object) +def typevar_not_needed(x: T) -> None: + pass + +BoundedT = TypeVar("BoundedT", bound=int) + +# TODO: error, should be (x: int) +def bounded_typevar_not_needed(x: BoundedT) -> None: + pass +``` + +Typevars are only needed if you use them more than once. For instance, to specify that two +parameters must both have the same type: + +```py +def two_params(x: T, y: T) -> T: + return x +``` + +or to specify that a return value is the same as a parameter: + +```py +def return_value(x: T) -> T: + return x +``` + +Each typevar must also appear _somewhere_ in the parameter list: + +```py +def absurd() -> T: + # There's no way to construct a T! + raise ValueError("absurd") +``` + +## Inferring generic function parameter types + +If the type of a generic function parameter is a typevar, then we can infer what type that typevar +is bound to at each call site. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def f(x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(1.0)) # revealed: float +reveal_type(f(True)) # revealed: Literal[True] +reveal_type(f("string")) # revealed: Literal["string"] +``` + +## Inferring “deep” generic parameter types + +The matching up of call arguments and discovery of constraints on typevars can be a recursive +process for arbitrarily-nested generic classes and protocols in parameters. + +TODO: Note that we can currently only infer a specialization for a generic protocol when the +argument _explicitly_ implements the protocol by listing it as a base class. + +```py +from typing import Protocol, TypeVar + +T = TypeVar("T") + +class CanIndex(Protocol[T]): + def __getitem__(self, index: int) -> T: ... + +class ExplicitlyImplements(CanIndex[T]): ... + +def takes_in_list(x: list[T]) -> list[T]: + return x + +def takes_in_protocol(x: CanIndex[T]) -> T: + return x[0] + +def deep_list(x: list[str]) -> None: + reveal_type(takes_in_list(x)) # revealed: list[str] + # TODO: revealed: str + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deeper_list(x: list[set[str]]) -> None: + reveal_type(takes_in_list(x)) # revealed: list[set[str]] + # TODO: revealed: set[str] + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deep_explicit(x: ExplicitlyImplements[str]) -> None: + reveal_type(takes_in_protocol(x)) # revealed: str + +def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: + reveal_type(takes_in_protocol(x)) # revealed: set[str] + +def takes_in_type(x: type[T]) -> type[T]: + return x + +reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form) +``` + +This also works when passing in arguments that are subclasses of the parameter type. + +```py +class Sub(list[int]): ... +class GenericSub(list[T]): ... + +reveal_type(takes_in_list(Sub())) # revealed: list[int] +# TODO: revealed: int +reveal_type(takes_in_protocol(Sub())) # revealed: Unknown + +reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str] +# TODO: revealed: str +reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown + +class ExplicitSub(ExplicitlyImplements[int]): ... +class ExplicitGenericSub(ExplicitlyImplements[T]): ... + +reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int +reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str +``` + +## Inferring tuple parameter types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypeVar + +T = TypeVar("T") + +def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T: + return x[-2] + +# TODO: revealed: Literal[True] +reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T: + return x[1] + +# TODO: revealed: Literal[b"foo"] +reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_fixed_tuple(x: tuple[T, int]) -> T: + return x[0] + +reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True] + +def takes_homogeneous_tuple(x: tuple[T, ...]) -> T: + return x[0] + +# TODO: revealed: Literal[42] +reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown +# TODO: revealed: Literal[42, 43] +reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown +``` + +## Inferring a bound typevar + + + +```py +from typing import TypeVar +from typing_extensions import reveal_type + +T = TypeVar("T", bound=int) + +def f(x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(True)) # revealed: Literal[True] +# error: [invalid-argument-type] +reveal_type(f("string")) # revealed: Unknown +``` + +## Inferring a constrained typevar + + + +```py +from typing import TypeVar +from typing_extensions import reveal_type + +T = TypeVar("T", int, None) + +def f(x: T) -> T: + return x + +reveal_type(f(1)) # revealed: int +reveal_type(f(True)) # revealed: int +reveal_type(f(None)) # revealed: None +# error: [invalid-argument-type] +reveal_type(f("string")) # revealed: Unknown +``` + +## Typevar constraints + +If a type parameter has an upper bound, that upper bound constrains which types can be used for that +typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar +in the function. + +```py +from typing import TypeVar + +T = TypeVar("T", bound=int) + +def good_param(x: T) -> None: + reveal_type(x) # revealed: T +``` + +If the function is annotated as returning the typevar, this means that the upper bound is _not_ +assignable to that typevar, since return types are contravariant. In `bad`, we can infer that +`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the +return value is not guaranteed to be compatible for all `T: int`. + +```py +def good_return(x: T) -> T: + return x + +def bad_return(x: T) -> T: + # error: [invalid-return-type] "Return type does not match returned value: expected `T`, found `int`" + return x + 1 +``` + +## All occurrences of the same typevar have the same type + +If a typevar appears multiple times in a function signature, all occurrences have the same type. + +```py +from typing import TypeVar + +T = TypeVar("T") +S = TypeVar("S") + +def different_types(cond: bool, t: T, s: S) -> T: + if cond: + return t + else: + # error: [invalid-return-type] "Return type does not match returned value: expected `T`, found `S`" + return s + +def same_types(cond: bool, t1: T, t2: T) -> T: + if cond: + return t1 + else: + return t2 +``` + +## All occurrences of the same constrained typevar have the same type + +The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__` +methods that are compatible with the return type, so the `return` expression is always well-typed: + +```py +from typing import TypeVar + +T = TypeVar("T", int, str) + +def same_constrained_types(t1: T, t2: T) -> T: + # TODO: no error + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`" + return t1 + t2 +``` + +This is _not_ the same as a union type, because of this additional constraint that the two +occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types, +and an `int` and a `str` cannot be added together: + +```py +def unions_are_different(t1: int | str, t2: int | str) -> int | str: + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + return t1 + t2 +``` + +## Typevar inference is a unification problem + +When inferring typevar assignments in a generic function call, we cannot simply solve constraints +eagerly for each parameter in turn. We must solve a unification problem involving all of the +parameters simultaneously. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def two_params(x: T, y: T) -> T: + return x + +reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] +reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] +``` + +When one of the parameters is a union, we attempt to find the smallest specialization that satisfies +all of the constraints. + +```py +def union_param(x: T | None) -> T: + if x is None: + raise ValueError + return x + +reveal_type(union_param("a")) # revealed: Literal["a"] +reveal_type(union_param(1)) # revealed: Literal[1] +reveal_type(union_param(None)) # revealed: Unknown +``` + +```py +def union_and_nonunion_params(x: T | int, y: T) -> T: + return y + +reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] +``` + +```py +S = TypeVar("S") + +def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]: + return y + +reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] +reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] +``` + +## Inferring nested generic function calls + +We can infer type assignments in nested calls to multiple generic functions. If they use the same +type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def f(x: T) -> tuple[T, int]: + return (x, 1) + +def g(x: T) -> T | None: + return x + +reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] +reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md new file mode 100644 index 0000000000000..cf3cff8177bcf --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -0,0 +1,228 @@ +# Legacy type variables + +The tests in this file focus on how type variables are defined using the legacy notation. Most +_uses_ of type variables are tested in other files in this directory; we do not duplicate every test +for both type variable syntaxes. + +Unless otherwise specified, all quotations come from the [Generics] section of the typing spec. + +## Type variables + +### Defining legacy type variables + +> Generics can be parameterized by using a factory available in `typing` called `TypeVar`. + +This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available +in newer Python releases. + +```py +from typing import TypeVar + +T = TypeVar("T") +reveal_type(type(T)) # revealed: +reveal_type(T) # revealed: typing.TypeVar +reveal_type(T.__name__) # revealed: Literal["T"] +``` + +### Directly assigned to a variable + +> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as +> part of a larger expression). + +```py +from typing import TypeVar + +T = TypeVar("T") +# TODO: no error +# error: [invalid-legacy-type-variable] +U: TypeVar = TypeVar("U") + +# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable" +# error: [invalid-type-form] "Function calls are not allowed in type expressions" +TestList = list[TypeVar("W")] +``` + +### `TypeVar` parameter must match variable name + +> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned. + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)" +T = TypeVar("Q") +``` + +### No redefinition + +> Type variables must not be redefined. + +```py +from typing import TypeVar + +T = TypeVar("T") + +# TODO: error +T = TypeVar("T") +``` + +### Type variables with a default + +Note that the `__default__` property is only available in Python ≥3.13. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +reveal_type(T.__default__) # revealed: int +reveal_type(T.__bound__) # revealed: None +reveal_type(T.__constraints__) # revealed: tuple[()] + +S = TypeVar("S") +reveal_type(S.__default__) # revealed: NoDefault +``` + +### Using other typevars as a default + +```py +from typing import Generic, TypeVar, Union + +T = TypeVar("T") +U = TypeVar("U", default=T) +V = TypeVar("V", default=Union[T, U]) + +class Valid(Generic[T, U, V]): ... + +reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown] +reveal_type(Valid[int]()) # revealed: Valid[int, int, int] +reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str] +reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None] + +# TODO: error, default value for U isn't available in the generic context +class Invalid(Generic[U]): ... +``` + +### Type variables with an upper bound + +```py +from typing import TypeVar + +T = TypeVar("T", bound=int) +reveal_type(T.__bound__) # revealed: int +reveal_type(T.__constraints__) # revealed: tuple[()] + +S = TypeVar("S") +reveal_type(S.__bound__) # revealed: None +``` + +### Type variables with constraints + +```py +from typing import TypeVar + +T = TypeVar("T", int, str) +reveal_type(T.__constraints__) # revealed: tuple[int, str] + +S = TypeVar("S") +reveal_type(S.__constraints__) # revealed: tuple[()] +``` + +### Cannot have only one constraint + +> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should +> be at least two constraints, if any; specifying a single constraint is disallowed. + +```py +from typing import TypeVar + +# TODO: error: [invalid-type-variable-constraints] +T = TypeVar("T", int) +``` + +### Cannot be both covariant and contravariant + +> To facilitate the declaration of container types where covariant or contravariant type checking is +> acceptable, type variables accept keyword arguments `covariant=True` or `contravariant=True`. At +> most one of these may be passed. + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", covariant=True, contravariant=True) +``` + +### Variance parameters must be unambiguous + +```py +from typing import TypeVar + +def cond() -> bool: + return True + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", covariant=cond()) + +# error: [invalid-legacy-type-variable] +U = TypeVar("U", contravariant=cond()) +``` + +## Callability + +A typevar bound to a Callable type is callable: + +```py +from typing import Callable, TypeVar + +T = TypeVar("T", bound=Callable[[], int]) + +def bound(f: T): + reveal_type(f) # revealed: T + reveal_type(f()) # revealed: int +``` + +Same with a constrained typevar, as long as all constraints are callable: + +```py +T = TypeVar("T", Callable[[], int], Callable[[], str]) + +def constrained(f: T): + reveal_type(f) # revealed: T + reveal_type(f()) # revealed: int | str +``` + +## Meta-type + +The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the +meta-types of the constraints: + +```py +from typing import TypeVar + +T_normal = TypeVar("T_normal") + +def normal(x: T_normal): + reveal_type(type(x)) # revealed: type + +T_bound_object = TypeVar("T_bound_object", bound=object) + +def bound_object(x: T_bound_object): + reveal_type(type(x)) # revealed: type + +T_bound_int = TypeVar("T_bound_int", bound=int) + +def bound_int(x: T_bound_int): + reveal_type(type(x)) # revealed: type[int] + +T_constrained = TypeVar("T_constrained", int, str) + +def constrained(x: T_constrained): + reveal_type(type(x)) # revealed: type[int] | type[str] +``` + +[generics]: https://typing.python.org/en/latest/spec/generics.html diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md new file mode 100644 index 0000000000000..4c2a7032ab04b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -0,0 +1,275 @@ +# Variance: Legacy syntax + +Type variables have a property called _variance_ that affects the subtyping and assignability +relations. Much more detail can be found in the [spec]. To summarize, each typevar is either +**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not +currently mentioned in the typing spec, but is a fourth case that we must consider.) + +For all of the examples below, we will consider typevars `T` and `U`, two generic classes using +those typevars `C[T]` and `D[U]`, and two types `A` and `B`. + +(Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype +nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however, +assignable to any specialization of `C`, regardless of variance, via materialization.) + +## Covariance + +With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B` and `C <: D`, +then `C[A] <: C[B]` and `C[A] <: D[B]`. + +Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of +`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would +get from the sequence is a valid `int`. + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any, Generic, TypeVar + +class A: ... +class B(A): ... + +T = TypeVar("T", covariant=True) +U = TypeVar("U", covariant=True) + +class C(Generic[T]): + def receive(self) -> T: + raise ValueError + +class D(C[U]): + pass + +static_assert(is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(is_assignable_to(D[B], C[A])) +static_assert(not is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +static_assert(is_subtype_of(C[B], C[A])) +static_assert(is_subtype_of(C[A], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(not is_subtype_of(C[Any], C[Any])) + +static_assert(is_subtype_of(D[B], C[A])) +static_assert(not is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +## Contravariance + +With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B` and +`C <: D`, then `C[B] <: C[A]` and `D[B] <: C[A]`. + +Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives +`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool` +that you pass into the consumer is a valid `int`. + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any, Generic, TypeVar + +class A: ... +class B(A): ... + +T = TypeVar("T", contravariant=True) +U = TypeVar("U", contravariant=True) + +class C(Generic[T]): + def send(self, value: T): ... + +class D(C[U]): + pass + +static_assert(not is_assignable_to(C[B], C[A])) +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_assignable_to(D[B], C[A])) +static_assert(is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(not is_subtype_of(D[B], C[A])) +static_assert(is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +## Invariance + +With an invariant typevar, only equivalent specializations of the generic class are subtypes of or +assignable to each other. + +This often occurs for types that are both producers _and_ consumers, like a mutable `list`. +Iterating over the elements in a list would work with a covariant typevar, just like with the +"producer" type above. Appending elements to a list would work with a contravariant typevar, just +like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant +at the same time! + +If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list +of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list +would no longer only contain elements that are subtypes of `bool`. + +Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a +mutable list of `int`s, since you might try to extract elements from the list: you expect every +element that you extract to be a subtype of `bool`, but the list can contain any `int`. + +In the end, if you expect a mutable list, you must always be given a list of exactly that type, +since we can't know in advance which of the allowed methods you'll want to use. + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any, Generic, TypeVar + +class A: ... +class B(A): ... + +T = TypeVar("T") +U = TypeVar("U") + +class C(Generic[T]): + def send(self, value: T): ... + def receive(self) -> T: + raise ValueError + +class D(C[U]): + pass + +static_assert(not is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_assignable_to(D[B], C[A])) +static_assert(not is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(not is_subtype_of(D[B], C[A])) +static_assert(not is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +## Bivariance + +With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact, +gradually equivalent to) each other, and all fully static specializations are subtypes of (and +equivalent to) each other. + +It is not possible to construct a legacy typevar that is explicitly bivariant. + +[spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md new file mode 100644 index 0000000000000..9726dbae54225 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -0,0 +1,516 @@ +# Generic classes: PEP 695 syntax + +```toml +[environment] +python-version = "3.13" +``` + +## Defining a generic class + +At its simplest, to define a generic class using PEP 695 syntax, you add a list of typevars after +the class name. + +```py +from ty_extensions import generic_context + +class SingleTypevar[T]: ... +class MultipleTypevars[T, S]: ... + +reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T] +reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S] +``` + +You cannot use the same typevar more than once. + +```py +# error: [invalid-syntax] "duplicate type parameter" +class RepeatedTypevar[T, T]: ... +``` + +You can only use typevars (TODO: or param specs or typevar tuples) in the class's generic context. + +```py +# TODO: error +class GenericOfType[int]: ... +``` + +You can also define a generic class by inheriting from some _other_ generic class, and specializing +it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in +your base classes. + +```py +class InheritedGeneric[U, V](MultipleTypevars[U, V]): ... +class InheritedGenericPartiallySpecialized[U](MultipleTypevars[U, int]): ... +class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ... + +reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[U, V] +reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[U] +reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None +``` + +If you don't specialize a generic base class, we use the default specialization, which maps each +typevar to its default value or `Any`. Since that base class is fully specialized, it does not make +the inheriting class generic. + +```py +class InheritedGenericDefaultSpecialization(MultipleTypevars): ... + +reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None +``` + +You cannot use PEP-695 syntax and the legacy syntax in the same class definition. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables" +class BothGenericSyntaxes[U](Generic[T]): ... + +reveal_type(BothGenericSyntaxes.__mro__) # revealed: tuple[, Unknown, ] + +# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables" +# error: [invalid-base] "Cannot inherit from plain `Generic`" +class DoublyInvalid[T](Generic): ... + +reveal_type(DoublyInvalid.__mro__) # revealed: tuple[, Unknown, ] +``` + +Generic classes implicitly inherit from `Generic`: + +```py +class Foo[T]: ... + +# revealed: tuple[, typing.Generic, ] +reveal_type(Foo.__mro__) +# revealed: tuple[, typing.Generic, ] +reveal_type(Foo[int].__mro__) + +class A: ... +class Bar[T](A): ... + +# revealed: tuple[, , typing.Generic, ] +reveal_type(Bar.__mro__) +# revealed: tuple[, , typing.Generic, ] +reveal_type(Bar[int].__mro__) + +class B: ... +class Baz[T](A, B): ... + +# revealed: tuple[, , , typing.Generic, ] +reveal_type(Baz.__mro__) +# revealed: tuple[, , , typing.Generic, ] +reveal_type(Baz[int].__mro__) +``` + +## Specializing generic classes explicitly + +The type parameter can be specified explicitly: + +```py +from typing import Literal + +class C[T]: + x: T + +reveal_type(C[int]()) # revealed: C[int] +reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]] +``` + +The specialization must match the generic types: + +```py +# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2" +reveal_type(C[int, int]()) # revealed: Unknown +``` + +If the type variable has an upper bound, the specialized type must satisfy that bound: + +```py +class Bounded[T: int]: ... +class BoundedByUnion[T: int | str]: ... +class IntSubclass(int): ... + +reveal_type(Bounded[int]()) # revealed: Bounded[int] +reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `str`" +reveal_type(Bounded[str]()) # revealed: Unknown + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to class `Bounded` is incorrect: Expected `int`, found `int | str`" +reveal_type(Bounded[int | str]()) # revealed: Unknown + +reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] +reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass] +reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] +reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] +``` + +If the type variable is constrained, the specialized type must satisfy those constraints: + +```py +class Constrained[T: (int, str)]: ... + +reveal_type(Constrained[int]()) # revealed: Constrained[int] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Constrained[Unknown] +reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] + +reveal_type(Constrained[str]()) # revealed: Constrained[str] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Unknown +reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to class `Constrained` is incorrect: Expected `int | str`, found `object`" +reveal_type(Constrained[object]()) # revealed: Unknown +``` + +If the type variable has a default, it can be omitted: + +```py +class WithDefault[T, U = int]: ... + +reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] +reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] +``` + +## Inferring generic class parameters + +We can infer the type parameter from a type context: + +```py +class C[T]: + x: T + +c: C[int] = C() +# TODO: revealed: C[int] +reveal_type(c) # revealed: C[Unknown] +``` + +The typevars of a fully specialized generic class should no longer be visible: + +```py +# TODO: revealed: int +reveal_type(c.x) # revealed: Unknown +``` + +If the type parameter is not specified explicitly, and there are no constraints that let us infer a +specific type, we infer the typevar's default type: + +```py +class D[T = int]: ... + +reveal_type(D()) # revealed: D[int] +``` + +If a typevar does not provide a default, we use `Unknown`: + +```py +reveal_type(C()) # revealed: C[Unknown] +``` + +## Inferring generic class parameters from constructors + +If the type of a constructor parameter is a class typevar, we can use that to infer the type +parameter. The types inferred from a type context and from a constructor parameter must be +consistent with each other. + +### `__new__` only + +```py +class C[T]: + def __new__(cls, x: T) -> "C[T]": + return object.__new__(cls) + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### `__init__` only + +```py +class C[T]: + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### Identical `__new__` and `__init__` signatures + +```py +class C[T]: + def __new__(cls, x: T) -> "C[T]": + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### Compatible `__new__` and `__init__` signatures + +```py +class C[T]: + def __new__(cls, *args, **kwargs) -> "C[T]": + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") + +class D[T]: + def __new__(cls, x: T) -> "D[T]": + return object.__new__(cls) + + def __init__(self, *args, **kwargs) -> None: ... + +reveal_type(D(1)) # revealed: D[int] + +# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`" +wrong_innards: D[int] = D("five") +``` + +### Both present, `__new__` inherited from a generic base class + +If either method comes from a generic base class, we don't currently use its inferred specialization +to specialize the class. + +```py +class C[T, U]: + def __new__(cls, *args, **kwargs) -> "C[T, U]": + return object.__new__(cls) + +class D[V](C[V, int]): + def __init__(self, x: V) -> None: ... + +reveal_type(D(1)) # revealed: D[int] +``` + +### `__init__` is itself generic + +```py +class C[T]: + def __init__[S](self, x: T, y: S) -> None: ... + +reveal_type(C(1, 1)) # revealed: C[int] +reveal_type(C(1, "string")) # revealed: C[int] +reveal_type(C(1, True)) # revealed: C[int] + +# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five", 1) +``` + +### Some `__init__` overloads only apply to certain specializations + +```py +from typing import overload + +class C[T]: + @overload + def __init__(self: C[str], x: str) -> None: ... + @overload + def __init__(self: C[bytes], x: bytes) -> None: ... + @overload + def __init__(self: C[int], x: bytes) -> None: ... + @overload + def __init__(self, x: int) -> None: ... + def __init__(self, x: str | bytes | int) -> None: ... + +reveal_type(C("string")) # revealed: C[str] +reveal_type(C(b"bytes")) # revealed: C[bytes] +reveal_type(C(12)) # revealed: C[Unknown] + +C[str]("string") +C[str](b"bytes") # error: [no-matching-overload] +C[str](12) + +C[bytes]("string") # error: [no-matching-overload] +C[bytes](b"bytes") +C[bytes](12) + +C[int]("string") # error: [no-matching-overload] +C[int](b"bytes") +C[int](12) + +C[None]("string") # error: [no-matching-overload] +C[None](b"bytes") # error: [no-matching-overload] +C[None](12) +``` + +### Synthesized methods with dataclasses + +```py +from dataclasses import dataclass + +@dataclass +class A[T]: + x: T + +reveal_type(A(x=1)) # revealed: A[int] +``` + +## Generic subclass + +When a generic subclass fills its superclass's type parameter with one of its own, the actual types +propagate through: + +```py +class Parent[T]: + x: T + +class Child[U](Parent[U]): ... +class Grandchild[V](Child[V]): ... +class Greatgrandchild[W](Child[W]): ... + +reveal_type(Parent[int]().x) # revealed: int +reveal_type(Child[int]().x) # revealed: int +reveal_type(Grandchild[int]().x) # revealed: int +reveal_type(Greatgrandchild[int]().x) # revealed: int +``` + +## Generic methods + +Generic classes can contain methods that are themselves generic. The generic methods can refer to +the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in +scope for the method. + +```py +class C[T]: + def method[U](self, u: U) -> U: + return u + # error: [unresolved-reference] + def cannot_use_outside_of_method(self, u: U): ... + + # TODO: error + def cannot_shadow_class_typevar[T](self, t: T): ... + +c: C[int] = C[int]() +reveal_type(c.method("string")) # revealed: Literal["string"] +``` + +## Specializations propagate + +In a specialized generic alias, the specialization is applied to the attributes and methods of the +class. + +```py +class LinkedList[T]: ... + +class C[T, U]: + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> LinkedList[T]: + return LinkedList[T]() + +c = C[int, str]() +reveal_type(c.x) # revealed: int +reveal_type(c.y) # revealed: str +reveal_type(c.method1()) # revealed: int +reveal_type(c.method2()) # revealed: str +reveal_type(c.method3()) # revealed: LinkedList[int] +``` + +When a method is overloaded, the specialization is applied to all overloads. + +```py +from typing import overload + +class WithOverloadedMethod[T]: + @overload + def method(self, x: T) -> T: + return x + + @overload + def method[S](self, x: S) -> S | T: + return x + + def method[S](self, x: S | T) -> S | T: + return x + +reveal_type(WithOverloadedMethod[int].method) # revealed: Overload[(self, x: int) -> int, (self, x: S) -> S | int] +``` + +## Cyclic class definitions + +### F-bounded quantification + +A class can use itself as the type parameter of one of its superclasses. (This is also known as the +[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) + +#### In a stub file + +Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). + +```pyi +class Base[T]: ... +class Sub(Base[Sub]): ... + +reveal_type(Sub) # revealed: +``` + +#### With string forward references + +A similar case can work in a non-stub file, if forward references are stringified: + +```py +class Base[T]: ... +class Sub(Base["Sub"]): ... + +reveal_type(Sub) # revealed: +``` + +#### Without string forward references + +In a non-stub file, without stringified forward references, this raises a `NameError`: + +```py +class Base[T]: ... + +# error: [unresolved-reference] +class Sub(Base[Sub]): ... +``` + +### Cyclic inheritance as a generic parameter + +```pyi +class Derived[T](list[Derived[T]]): ... +``` + +### Direct cyclic inheritance + +Inheritance that would result in a cyclic MRO is detected as an error. + +```py +# error: [cyclic-class-definition] +class C[T](C): ... + +# error: [cyclic-class-definition] +class D[T](D[int]): ... +``` + +[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern +[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md new file mode 100644 index 0000000000000..bc7f96c0e4653 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -0,0 +1,372 @@ +# Generic functions: PEP 695 syntax + +```toml +[environment] +python-version = "3.12" +``` + +## Typevar must be used at least twice + +If you're only using a typevar for a single parameter, you don't need the typevar — just use +`object` (or the typevar's upper bound): + +```py +# TODO: error, should be (x: object) +def typevar_not_needed[T](x: T) -> None: + pass + +# TODO: error, should be (x: int) +def bounded_typevar_not_needed[T: int](x: T) -> None: + pass +``` + +Typevars are only needed if you use them more than once. For instance, to specify that two +parameters must both have the same type: + +```py +def two_params[T](x: T, y: T) -> T: + return x +``` + +or to specify that a return value is the same as a parameter: + +```py +def return_value[T](x: T) -> T: + return x +``` + +Each typevar must also appear _somewhere_ in the parameter list: + +```py +def absurd[T]() -> T: + # There's no way to construct a T! + raise ValueError("absurd") +``` + +## Inferring generic function parameter types + +If the type of a generic function parameter is a typevar, then we can infer what type that typevar +is bound to at each call site. + +```py +def f[T](x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(1.0)) # revealed: float +reveal_type(f(True)) # revealed: Literal[True] +reveal_type(f("string")) # revealed: Literal["string"] +``` + +## Inferring “deep” generic parameter types + +The matching up of call arguments and discovery of constraints on typevars can be a recursive +process for arbitrarily-nested generic classes and protocols in parameters. + +TODO: Note that we can currently only infer a specialization for a generic protocol when the +argument _explicitly_ implements the protocol by listing it as a base class. + +```py +from typing import Protocol, TypeVar + +S = TypeVar("S") + +class CanIndex(Protocol[S]): + def __getitem__(self, index: int) -> S: ... + +class ExplicitlyImplements[T](CanIndex[T]): ... + +def takes_in_list[T](x: list[T]) -> list[T]: + return x + +def takes_in_protocol[T](x: CanIndex[T]) -> T: + return x[0] + +def deep_list(x: list[str]) -> None: + reveal_type(takes_in_list(x)) # revealed: list[str] + # TODO: revealed: str + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deeper_list(x: list[set[str]]) -> None: + reveal_type(takes_in_list(x)) # revealed: list[set[str]] + # TODO: revealed: set[str] + reveal_type(takes_in_protocol(x)) # revealed: Unknown + +def deep_explicit(x: ExplicitlyImplements[str]) -> None: + reveal_type(takes_in_protocol(x)) # revealed: str + +def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None: + reveal_type(takes_in_protocol(x)) # revealed: set[str] + +def takes_in_type[T](x: type[T]) -> type[T]: + return x + +reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form) +``` + +This also works when passing in arguments that are subclasses of the parameter type. + +```py +class Sub(list[int]): ... +class GenericSub[T](list[T]): ... + +reveal_type(takes_in_list(Sub())) # revealed: list[int] +# TODO: revealed: int +reveal_type(takes_in_protocol(Sub())) # revealed: Unknown + +reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str] +# TODO: revealed: str +reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown + +class ExplicitSub(ExplicitlyImplements[int]): ... +class ExplicitGenericSub[T](ExplicitlyImplements[T]): ... + +reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int +reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str +``` + +## Inferring tuple parameter types + +```py +def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T: + return x[-2] + +# TODO: revealed: Literal[True] +reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T: + return x[1] + +# TODO: revealed: Literal[b"foo"] +reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown + +def takes_fixed_tuple[T](x: tuple[T, int]) -> T: + return x[0] + +reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True] + +def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T: + return x[0] + +# TODO: revealed: Literal[42] +reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown +# TODO: revealed: Literal[42, 43] +reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown +``` + +## Inferring a bound typevar + + + +```py +from typing_extensions import reveal_type + +def f[T: int](x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(True)) # revealed: Literal[True] +# error: [invalid-argument-type] +reveal_type(f("string")) # revealed: Unknown +``` + +## Inferring a constrained typevar + + + +```py +from typing_extensions import reveal_type + +def f[T: (int, None)](x: T) -> T: + return x + +reveal_type(f(1)) # revealed: int +reveal_type(f(True)) # revealed: int +reveal_type(f(None)) # revealed: None +# error: [invalid-argument-type] +reveal_type(f("string")) # revealed: Unknown +``` + +## Typevar constraints + +If a type parameter has an upper bound, that upper bound constrains which types can be used for that +typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar +in the function. + +```py +def good_param[T: int](x: T) -> None: + reveal_type(x) # revealed: T +``` + +If the function is annotated as returning the typevar, this means that the upper bound is _not_ +assignable to that typevar, since return types are contravariant. In `bad`, we can infer that +`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the +return value is not guaranteed to be compatible for all `T: int`. + +```py +def good_return[T: int](x: T) -> T: + return x + +def bad_return[T: int](x: T) -> T: + # error: [invalid-return-type] "Return type does not match returned value: expected `T`, found `int`" + return x + 1 +``` + +## All occurrences of the same typevar have the same type + +If a typevar appears multiple times in a function signature, all occurrences have the same type. + +```py +def different_types[T, S](cond: bool, t: T, s: S) -> T: + if cond: + return t + else: + # error: [invalid-return-type] "Return type does not match returned value: expected `T`, found `S`" + return s + +def same_types[T](cond: bool, t1: T, t2: T) -> T: + if cond: + return t1 + else: + return t2 +``` + +## All occurrences of the same constrained typevar have the same type + +The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__` +methods that are compatible with the return type, so the `return` expression is always well-typed: + +```py +def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: + # TODO: no error + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`" + return t1 + t2 +``` + +This is _not_ the same as a union type, because of this additional constraint that the two +occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types, +and an `int` and a `str` cannot be added together: + +```py +def unions_are_different(t1: int | str, t2: int | str) -> int | str: + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + return t1 + t2 +``` + +## Typevar inference is a unification problem + +When inferring typevar assignments in a generic function call, we cannot simply solve constraints +eagerly for each parameter in turn. We must solve a unification problem involving all of the +parameters simultaneously. + +```py +def two_params[T](x: T, y: T) -> T: + return x + +reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] +reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] +``` + +When one of the parameters is a union, we attempt to find the smallest specialization that satisfies +all of the constraints. + +```py +def union_param[T](x: T | None) -> T: + if x is None: + raise ValueError + return x + +reveal_type(union_param("a")) # revealed: Literal["a"] +reveal_type(union_param(1)) # revealed: Literal[1] +reveal_type(union_param(None)) # revealed: Unknown +``` + +```py +def union_and_nonunion_params[T](x: T | int, y: T) -> T: + return y + +reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] +``` + +```py +def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]: + return y + +reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] +reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] +``` + +## Inferring nested generic function calls + +We can infer type assignments in nested calls to multiple generic functions. If they use the same +type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below. + +```py +def f[T](x: T) -> tuple[T, int]: + return (x, 1) + +def g[T](x: T) -> T | None: + return x + +reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] +reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None +``` + +## Protocols as TypeVar bounds + +Protocol types can be used as TypeVar bounds, just like nominal types. + +```py +from typing import Any, Protocol +from ty_extensions import static_assert, is_assignable_to + +class SupportsClose(Protocol): + def close(self) -> None: ... + +class ClosableFullyStaticProtocol(Protocol): + x: int + def close(self) -> None: ... + +class ClosableNonFullyStaticProtocol(Protocol): + x: Any + def close(self) -> None: ... + +class ClosableFullyStaticNominal: + x: int + def close(self) -> None: ... + +class ClosableNonFullyStaticNominal: + x: int + def close(self) -> None: ... + +class NotClosableProtocol(Protocol): ... +class NotClosableNominal: ... + +def close_and_return[T: SupportsClose](x: T) -> T: + x.close() + return x + +def f( + a: SupportsClose, + b: ClosableFullyStaticProtocol, + c: ClosableNonFullyStaticProtocol, + d: ClosableFullyStaticNominal, + e: ClosableNonFullyStaticNominal, + f: NotClosableProtocol, + g: NotClosableNominal, +): + reveal_type(close_and_return(a)) # revealed: SupportsClose + reveal_type(close_and_return(b)) # revealed: ClosableFullyStaticProtocol + reveal_type(close_and_return(c)) # revealed: ClosableNonFullyStaticProtocol + reveal_type(close_and_return(d)) # revealed: ClosableFullyStaticNominal + reveal_type(close_and_return(e)) # revealed: ClosableNonFullyStaticNominal + + # error: [invalid-argument-type] "does not satisfy upper bound" + reveal_type(close_and_return(f)) # revealed: Unknown + # error: [invalid-argument-type] "does not satisfy upper bound" + reveal_type(close_and_return(g)) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md new file mode 100644 index 0000000000000..1ea17def0a735 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -0,0 +1,744 @@ +# PEP 695 Generics + +```toml +[environment] +python-version = "3.12" +``` + +[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables. + +## Type variables + +### Defining PEP 695 type variables + +PEP 695 introduces a new syntax for defining type variables. The resulting type variables are +instances of `typing.TypeVar`, just like legacy type variables. + +```py +def f[T](): + reveal_type(type(T)) # revealed: + reveal_type(T) # revealed: typing.TypeVar + reveal_type(T.__name__) # revealed: Literal["T"] +``` + +### Type variables with a default + +Note that the `__default__` property is only available in Python ≥3.13. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def f[T = int](): + reveal_type(T.__default__) # revealed: int + reveal_type(T.__bound__) # revealed: None + reveal_type(T.__constraints__) # revealed: tuple[()] + +def g[S](): + reveal_type(S.__default__) # revealed: NoDefault +``` + +### Using other typevars as a default + +```toml +[environment] +python-version = "3.13" +``` + +```py +class Valid[T, U = T, V = T | U]: ... + +reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown] +reveal_type(Valid[int]()) # revealed: Valid[int, int, int] +reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str] +reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None] + +# error: [unresolved-reference] +class Invalid[S = T]: ... +``` + +### Type variables with an upper bound + +```py +def f[T: int](): + reveal_type(T.__bound__) # revealed: int + reveal_type(T.__constraints__) # revealed: tuple[()] + +def g[S](): + reveal_type(S.__bound__) # revealed: None +``` + +### Type variables with constraints + +```py +def f[T: (int, str)](): + reveal_type(T.__constraints__) # revealed: tuple[int, str] + reveal_type(T.__bound__) # revealed: None + +def g[S](): + reveal_type(S.__constraints__) # revealed: tuple[()] +``` + +### Cannot have only one constraint + +> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should +> be at least two constraints, if any; specifying a single constraint is disallowed. + +```py +# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types" +def f[T: (int,)](): + pass +``` + +## Invalid uses + +Note that many of the invalid uses of legacy typevars do not apply to PEP 695 typevars, since the +PEP 695 syntax is only allowed places where typevars are allowed. + +## Displaying typevars + +We use a suffix when displaying the typevars of a generic function or class. This helps distinguish +different uses of the same typevar. + +```py +def f[T](x: T, y: T) -> None: + # TODO: revealed: T@f + reveal_type(x) # revealed: T + +class C[T]: + def m(self, x: T) -> None: + # TODO: revealed: T@c + reveal_type(x) # revealed: T +``` + +## Subtyping and assignability + +(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static +typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual +typevars.) + +We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be +specialized to. Properties are true of the typevar only if they are true for every valid +specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type +(including other typevars). + +```py +from ty_extensions import is_assignable_to, is_subtype_of, static_assert + +class Super: ... +class Base(Super): ... +class Sub(Base): ... +class Unrelated: ... + +def unbounded_unconstrained[T, U](t: T, u: U) -> None: + static_assert(is_assignable_to(T, T)) + static_assert(is_assignable_to(T, object)) + static_assert(not is_assignable_to(T, Super)) + static_assert(is_assignable_to(U, U)) + static_assert(is_assignable_to(U, object)) + static_assert(not is_assignable_to(U, Super)) + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(is_subtype_of(T, T)) + static_assert(is_subtype_of(T, object)) + static_assert(not is_subtype_of(T, Super)) + static_assert(is_subtype_of(U, U)) + static_assert(is_subtype_of(U, object)) + static_assert(not is_subtype_of(U, Super)) + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) +``` + +A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of +its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does +not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the +typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound +is a final class, since the typevar can still be specialized to `Never`.) + +```py +from typing import Any +from typing_extensions import final + +def bounded[T: Super](t: T) -> None: + static_assert(is_assignable_to(T, Super)) + static_assert(not is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Sub, T)) + + static_assert(is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Sub, T)) + +def bounded_by_gradual[T: Any](t: T) -> None: + static_assert(is_assignable_to(T, Any)) + static_assert(is_assignable_to(Any, T)) + static_assert(is_assignable_to(T, Super)) + static_assert(not is_assignable_to(Super, T)) + static_assert(is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(Sub, T)) + + static_assert(not is_subtype_of(T, Any)) + static_assert(not is_subtype_of(Any, T)) + static_assert(not is_subtype_of(T, Super)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(Sub, T)) + +@final +class FinalClass: ... + +def bounded_final[T: FinalClass](t: T) -> None: + static_assert(is_assignable_to(T, FinalClass)) + static_assert(not is_assignable_to(FinalClass, T)) + + static_assert(is_subtype_of(T, FinalClass)) + static_assert(not is_subtype_of(FinalClass, T)) +``` + +Two distinct fully static typevars are not subtypes of each other, even if they have the same +bounds, since there is (still) no guarantee that they will be specialized to the same type. This is +true even if both typevars are bounded by the same final class, since you can specialize the +typevars to `Never` in addition to that final class. + +```py +def two_bounded[T: Super, U: Super](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) + +def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) +``` + +A constrained fully static typevar is assignable to the union of its constraints, but not to any of +the constraints individually. None of the constraints are subtypes of the typevar, though the +intersection of all of its constraints is a subtype of the typevar. + +```py +from ty_extensions import Intersection + +def constrained[T: (Base, Unrelated)](t: T) -> None: + static_assert(not is_assignable_to(T, Super)) + static_assert(not is_assignable_to(T, Base)) + static_assert(not is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Unrelated)) + static_assert(is_assignable_to(T, Super | Unrelated)) + static_assert(is_assignable_to(T, Base | Unrelated)) + static_assert(not is_assignable_to(T, Sub | Unrelated)) + static_assert(not is_assignable_to(Super, T)) + static_assert(not is_assignable_to(Unrelated, T)) + static_assert(not is_assignable_to(Super | Unrelated, T)) + static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + + static_assert(not is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Base)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Unrelated)) + static_assert(is_subtype_of(T, Super | Unrelated)) + static_assert(is_subtype_of(T, Base | Unrelated)) + static_assert(not is_subtype_of(T, Sub | Unrelated)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Unrelated, T)) + static_assert(not is_subtype_of(Super | Unrelated, T)) + static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) + +def constrained_by_gradual[T: (Base, Any)](t: T) -> None: + static_assert(is_assignable_to(T, Super)) + static_assert(is_assignable_to(T, Base)) + static_assert(not is_assignable_to(T, Sub)) + static_assert(not is_assignable_to(T, Unrelated)) + static_assert(is_assignable_to(T, Any)) + static_assert(is_assignable_to(T, Super | Any)) + static_assert(is_assignable_to(T, Super | Unrelated)) + static_assert(not is_assignable_to(Super, T)) + static_assert(is_assignable_to(Base, T)) + static_assert(not is_assignable_to(Unrelated, T)) + static_assert(is_assignable_to(Any, T)) + static_assert(not is_assignable_to(Super | Any, T)) + static_assert(is_assignable_to(Base | Any, T)) + static_assert(not is_assignable_to(Super | Unrelated, T)) + static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + static_assert(is_assignable_to(Intersection[Base, Any], T)) + + static_assert(not is_subtype_of(T, Super)) + static_assert(not is_subtype_of(T, Base)) + static_assert(not is_subtype_of(T, Sub)) + static_assert(not is_subtype_of(T, Unrelated)) + static_assert(not is_subtype_of(T, Any)) + static_assert(not is_subtype_of(T, Super | Any)) + static_assert(not is_subtype_of(T, Super | Unrelated)) + static_assert(not is_subtype_of(Super, T)) + static_assert(not is_subtype_of(Base, T)) + static_assert(not is_subtype_of(Unrelated, T)) + static_assert(not is_subtype_of(Any, T)) + static_assert(not is_subtype_of(Super | Any, T)) + static_assert(not is_subtype_of(Base | Any, T)) + static_assert(not is_subtype_of(Super | Unrelated, T)) + static_assert(not is_subtype_of(Intersection[Base, Unrelated], T)) + static_assert(not is_subtype_of(Intersection[Base, Any], T)) +``` + +Two distinct fully static typevars are not subtypes of each other, even if they have the same +constraints, and even if any of the constraints are final. There must always be at least two +distinct constraints, meaning that there is (still) no guarantee that they will be specialized to +the same type. + +```py +def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) + +@final +class AnotherFinalClass: ... + +def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: + static_assert(not is_assignable_to(T, U)) + static_assert(not is_assignable_to(U, T)) + + static_assert(not is_subtype_of(T, U)) + static_assert(not is_subtype_of(U, T)) +``` + +A bound or constrained typevar is a subtype of itself in a union: + +```py +def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: + static_assert(is_assignable_to(T, T | None)) + static_assert(is_assignable_to(U, U | None)) + + static_assert(is_subtype_of(T, T | None)) + static_assert(is_subtype_of(U, U | None)) +``` + +And an intersection of a typevar with another type is always a subtype of the TypeVar: + +```py +from ty_extensions import Intersection, Not, is_disjoint_from + +class A: ... + +def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: + static_assert(is_assignable_to(Intersection[T, Unrelated], T)) + static_assert(is_subtype_of(Intersection[T, Unrelated], T)) + + static_assert(is_assignable_to(Intersection[U, A], U)) + static_assert(is_subtype_of(Intersection[U, A], U)) + + static_assert(is_disjoint_from(Not[T], T)) + static_assert(is_disjoint_from(T, Not[T])) + static_assert(is_disjoint_from(Not[U], U)) + static_assert(is_disjoint_from(U, Not[U])) +``` + +## Equivalence + +A `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there is no +guarantee that they will be specialized to the same type. (This is true even if both typevars are +bounded by the same final class, since you can specialize the typevars to `Never` in addition to +that final class.) + +```py +from typing import final +from ty_extensions import is_equivalent_to, static_assert + +@final +class FinalClass: ... + +@final +class SecondFinalClass: ... + +def f[A, B, C: FinalClass, D: FinalClass, E: (FinalClass, SecondFinalClass), F: (FinalClass, SecondFinalClass)](): + static_assert(is_equivalent_to(A, A)) + static_assert(is_equivalent_to(B, B)) + static_assert(is_equivalent_to(C, C)) + static_assert(is_equivalent_to(D, D)) + static_assert(is_equivalent_to(E, E)) + static_assert(is_equivalent_to(F, F)) + + static_assert(not is_equivalent_to(A, B)) + static_assert(not is_equivalent_to(C, D)) + static_assert(not is_equivalent_to(E, F)) +``` + +TypeVars which have non-fully-static bounds or constraints are also self-equivalent. + +```py +from typing import final, Any +from ty_extensions import is_equivalent_to, static_assert + +# fmt: off + +def f[ + A: tuple[Any], + B: tuple[Any], + C: (tuple[Any], tuple[Any, Any]), + D: (tuple[Any], tuple[Any, Any]) +](): + static_assert(is_equivalent_to(A, A)) + static_assert(is_equivalent_to(B, B)) + static_assert(is_equivalent_to(C, C)) + static_assert(is_equivalent_to(D, D)) + +# fmt: on +``` + +## Singletons and single-valued types + +(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the +claims also apply to _single-valued_ types.) + +An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a +non-singleton type. + +```py +from ty_extensions import is_singleton, is_single_valued, static_assert + +def unbounded_unconstrained[T](t: T) -> None: + static_assert(not is_singleton(T)) + static_assert(not is_single_valued(T)) +``` + +A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be +specialized to `Never`. + +```py +def bounded[T: None](t: T) -> None: + static_assert(not is_singleton(T)) + static_assert(not is_single_valued(T)) +``` + +A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot +specialize a constrained typevar to a subtype of a constraint.) + +```py +from typing_extensions import Literal + +def constrained_non_singletons[T: (int, str)](t: T) -> None: + static_assert(not is_singleton(T)) + static_assert(not is_single_valued(T)) + +def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None: + static_assert(is_singleton(T)) + +def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: + static_assert(is_single_valued(T)) +``` + +## Unions involving typevars + +The union of an unbounded unconstrained typevar with any other type cannot be simplified, since +there is no guarantee what type the typevar will be specialized to. + +```py +from typing import Any + +class Super: ... +class Base(Super): ... +class Sub(Base): ... +class Unrelated: ... + +def unbounded_unconstrained[T](t: T) -> None: + def _(x: T | Super) -> None: + reveal_type(x) # revealed: T | Super + + def _(x: T | Base) -> None: + reveal_type(x) # revealed: T | Base + + def _(x: T | Sub) -> None: + reveal_type(x) # revealed: T | Sub + + def _(x: T | Unrelated) -> None: + reveal_type(x) # revealed: T | Unrelated + + def _(x: T | Any) -> None: + reveal_type(x) # revealed: T | Any +``` + +The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be +specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound +cannot be simplified. (The typevar might be specialized to a different subtype of the bound.) + +```py +def bounded[T: Base](t: T) -> None: + def _(x: T | Super) -> None: + reveal_type(x) # revealed: Super + + def _(x: T | Base) -> None: + reveal_type(x) # revealed: Base + + def _(x: T | Sub) -> None: + reveal_type(x) # revealed: T | Sub + + def _(x: T | Unrelated) -> None: + reveal_type(x) # revealed: T | Unrelated + + def _(x: T | Any) -> None: + reveal_type(x) # revealed: T | Any +``` + +The union of a constrained typevar with a type depends on how that type relates to the constraints. +If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely, +if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the +union cannot be simplified. + +```py +def constrained[T: (Base, Sub)](t: T) -> None: + def _(x: T | Super) -> None: + reveal_type(x) # revealed: Super + + def _(x: T | Base) -> None: + reveal_type(x) # revealed: Base + + def _(x: T | Sub) -> None: + reveal_type(x) # revealed: T + + def _(x: T | Unrelated) -> None: + reveal_type(x) # revealed: T | Unrelated + + def _(x: T | Any) -> None: + reveal_type(x) # revealed: T | Any +``` + +## Intersections involving typevars + +The intersection of an unbounded unconstrained typevar with any other type cannot be simplified, +since there is no guarantee what type the typevar will be specialized to. + +```py +from ty_extensions import Intersection +from typing import Any + +class Super: ... +class Base(Super): ... +class Sub(Base): ... +class Unrelated: ... + +def unbounded_unconstrained[T](t: T) -> None: + def _(x: Intersection[T, Super]) -> None: + reveal_type(x) # revealed: T & Super + + def _(x: Intersection[T, Base]) -> None: + reveal_type(x) # revealed: T & Base + + def _(x: Intersection[T, Sub]) -> None: + reveal_type(x) # revealed: T & Sub + + def _(x: Intersection[T, Unrelated]) -> None: + reveal_type(x) # revealed: T & Unrelated + + def _(x: Intersection[T, Any]) -> None: + reveal_type(x) # revealed: T & Any +``` + +The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar +itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded +typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a +different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint +from its bound is `Never`. + +```py +def bounded[T: Base](t: T) -> None: + def _(x: Intersection[T, Super]) -> None: + reveal_type(x) # revealed: T + + def _(x: Intersection[T, Base]) -> None: + reveal_type(x) # revealed: T + + def _(x: Intersection[T, Sub]) -> None: + reveal_type(x) # revealed: T & Sub + + def _(x: Intersection[T, None]) -> None: + reveal_type(x) # revealed: Never + + def _(x: Intersection[T, Any]) -> None: + reveal_type(x) # revealed: T & Any +``` + +Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must +be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints, +since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The +`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it +would not represent a single set of runtime objects; it would instead represent a _set of_ sets of +runtime objects. This is one reason we have not actually added this connector to our data model yet. +Nevertheless, describing constrained typevars this way helps explain how we simplify intersections +involving them. + +This means that when intersecting a constrained typevar with a type `T`, constraints that are +supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover, +constraints that are disjoint from `T` are no longer valid specializations of the typevar, since +`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we +can simplify the intersection as a whole to that constraint. + +```py +def constrained[T: (Base, Sub, Unrelated)](t: T) -> None: + def _(x: Intersection[T, Base]) -> None: + # With OneOf this would be OneOf[Base, Sub] + reveal_type(x) # revealed: T & Base + + def _(x: Intersection[T, Unrelated]) -> None: + reveal_type(x) # revealed: Unrelated + + def _(x: Intersection[T, Sub]) -> None: + reveal_type(x) # revealed: Sub + + def _(x: Intersection[T, None]) -> None: + reveal_type(x) # revealed: Never + + def _(x: Intersection[T, Any]) -> None: + reveal_type(x) # revealed: T & Any +``` + +We can simplify the intersection similarly when removing a type from a constrained typevar, since +this is modeled internally as an intersection with a negation. + +```py +from ty_extensions import Not + +def remove_constraint[T: (int, str, bool)](t: T) -> None: + def _(x: Intersection[T, Not[int]]) -> None: + reveal_type(x) # revealed: str + + def _(x: Intersection[T, Not[str]]) -> None: + # With OneOf this would be OneOf[int, bool] + reveal_type(x) # revealed: T & ~str + + def _(x: Intersection[T, Not[bool]]) -> None: + reveal_type(x) # revealed: T & ~bool + + def _(x: Intersection[T, Not[int], Not[str]]) -> None: + reveal_type(x) # revealed: Never + + def _(x: Intersection[T, Not[None]]) -> None: + reveal_type(x) # revealed: T + + def _(x: Intersection[T, Not[Any]]) -> None: + reveal_type(x) # revealed: T & Any +``` + +The intersection of a typevar with any other type is assignable to (and if fully static, a subtype +of) itself. + +```py +from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Not + +def intersection_is_assignable[T](t: T) -> None: + static_assert(is_assignable_to(Intersection[T, None], T)) + static_assert(is_assignable_to(Intersection[T, Not[None]], T)) + + static_assert(is_subtype_of(Intersection[T, None], T)) + static_assert(is_subtype_of(Intersection[T, Not[None]], T)) +``` + +## Narrowing + +We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar: + +```py +class P: ... +class Q: ... +class R: ... + +def f[T: (P, Q)](t: T) -> None: + if isinstance(t, P): + reveal_type(t) # revealed: P + p: P = t + else: + reveal_type(t) # revealed: Q & ~P + q: Q = t + + if isinstance(t, Q): + reveal_type(t) # revealed: Q + q: Q = t + else: + reveal_type(t) # revealed: P & ~Q + p: P = t + +def g[T: (P, Q, R)](t: T) -> None: + if isinstance(t, P): + reveal_type(t) # revealed: P + p: P = t + elif isinstance(t, Q): + reveal_type(t) # revealed: Q & ~P + q: Q = t + else: + reveal_type(t) # revealed: R & ~P & ~Q + r: R = t + + if isinstance(t, P): + reveal_type(t) # revealed: P + p: P = t + elif isinstance(t, Q): + reveal_type(t) # revealed: Q & ~P + q: Q = t + elif isinstance(t, R): + reveal_type(t) # revealed: R & ~P & ~Q + r: R = t + else: + reveal_type(t) # revealed: Never +``` + +If the constraints are disjoint, simplification does eliminate the redundant negative: + +```py +def h[T: (P, None)](t: T) -> None: + if t is None: + reveal_type(t) # revealed: None + p: None = t + else: + reveal_type(t) # revealed: P + p: P = t +``` + +## Callability + +A typevar bound to a Callable type is callable: + +```py +from typing import Callable + +def bound[T: Callable[[], int]](f: T): + reveal_type(f) # revealed: T + reveal_type(f()) # revealed: int +``` + +Same with a constrained typevar, as long as all constraints are callable: + +```py +def constrained[T: (Callable[[], int], Callable[[], str])](f: T): + reveal_type(f) # revealed: T + reveal_type(f()) # revealed: int | str +``` + +## Meta-type + +The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the +meta-types of the constraints: + +```py +def normal[T](x: T): + reveal_type(type(x)) # revealed: type + +def bound_object[T: object](x: T): + reveal_type(type(x)) # revealed: type + +def bound_int[T: int](x: T): + reveal_type(type(x)) # revealed: type[int] + +def constrained[T: (int, str)](x: T): + reveal_type(type(x)) # revealed: type[int] | type[str] +``` + +[pep 695]: https://peps.python.org/pep-0695/ diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md new file mode 100644 index 0000000000000..e98d01a35fd02 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -0,0 +1,383 @@ +# Variance: PEP 695 syntax + +```toml +[environment] +python-version = "3.12" +``` + +Type variables have a property called _variance_ that affects the subtyping and assignability +relations. Much more detail can be found in the [spec]. To summarize, each typevar is either +**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not +currently mentioned in the typing spec, but is a fourth case that we must consider.) + +For all of the examples below, we will consider typevars `T` and `U`, two generic classes using +those typevars `C[T]` and `D[U]`, and two types `A` and `B`. + +(Note that dynamic types like `Any` never participate in subtyping, so `C[Any]` is neither a subtype +nor supertype of any other specialization of `C`, regardless of `T`'s variance. It is, however, +assignable to any specialization of `C`, regardless of variance, via materialization.) + +## Covariance + +With a covariant typevar, subtyping and assignability are in "alignment": if `A <: B` and `C <: D`, +then `C[A] <: C[B]` and `C[A] <: D[B]`. + +Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of +`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would +get from the sequence is a valid `int`. + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def receive(self) -> T: + raise ValueError + +class D[U](C[U]): + pass + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(D[B], C[A])) +static_assert(not is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(D[B], C[A])) +static_assert(not is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +## Contravariance + +With a contravariant typevar, subtyping and assignability are in "opposition": if `A <: B` and +`C <: D`, then `C[B] <: C[A]` and `D[B] <: C[A]`. + +Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives +`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool` +that you pass into the consumer is a valid `int`. + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def send(self, value: T): ... + +class D[U](C[U]): + pass + +static_assert(not is_assignable_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_assignable_to(D[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(not is_subtype_of(D[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +## Invariance + +With an invariant typevar, only equivalent specializations of the generic class are subtypes of or +assignable to each other. + +This often occurs for types that are both producers _and_ consumers, like a mutable `list`. +Iterating over the elements in a list would work with a covariant typevar, just like with the +"producer" type above. Appending elements to a list would work with a contravariant typevar, just +like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant +at the same time! + +If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list +of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list +would no longer only contain elements that are subtypes of `bool`. + +Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a +mutable list of `int`s, since you might try to extract elements from the list: you expect every +element that you extract to be a subtype of `bool`, but the list can contain any `int`. + +In the end, if you expect a mutable list, you must always be given a list of exactly that type, +since we can't know in advance which of the allowed methods you'll want to use. + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def send(self, value: T): ... + def receive(self) -> T: + raise ValueError + +class D[U](C[U]): + pass + +static_assert(not is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_assignable_to(D[B], C[A])) +static_assert(not is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(not is_subtype_of(D[B], C[A])) +static_assert(not is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +## Bivariance + +With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact, +gradually equivalent to) each other, and all fully static specializations are subtypes of (and +equivalent to) each other. + +This is a bit of pathological case, which really only happens when the class doesn't use the typevar +at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_ +the typevar was used.) + +```py +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + pass + +class D[U](C[U]): + pass + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(D[B], C[A])) +static_assert(is_subtype_of(C[A], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(D[A], C[B])) +static_assert(is_assignable_to(D[A], C[Any])) +static_assert(is_assignable_to(D[B], C[Any])) +static_assert(is_assignable_to(D[Any], C[A])) +static_assert(is_assignable_to(D[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(not is_subtype_of(C[Any], C[Any])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(D[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[A], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[A], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[B], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[Any], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[Any], C[B])) + +static_assert(not is_equivalent_to(D[A], C[A])) +static_assert(not is_equivalent_to(D[B], C[B])) +static_assert(not is_equivalent_to(D[B], C[A])) +static_assert(not is_equivalent_to(D[A], C[B])) +static_assert(not is_equivalent_to(D[A], C[Any])) +static_assert(not is_equivalent_to(D[B], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[A])) +static_assert(not is_equivalent_to(D[Any], C[B])) + +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) +``` + +[spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md rename to crates/ty_python_semantic/resources/mdtest/generics/scoping.md index b4f9992a9a482..90426ff6b1e16 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -1,5 +1,10 @@ # Scoping rules for type variables +```toml +[environment] +python-version = "3.12" +``` + Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing spec. @@ -79,7 +84,7 @@ class C[T]: c: C[int] = C[int]() c.m1(1) c.m2(1) -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["string"]`" +# error: [invalid-argument-type] "Argument to bound method `m2` is incorrect: Expected `int`, found `Literal["string"]`" c.m2("string") ``` @@ -97,7 +102,7 @@ class C[T]: return "a" reveal_type(getattr_static(C[int], "f")) # revealed: def f(self, x: int) -> str -reveal_type(getattr_static(C[int], "f").__get__) # revealed: +reveal_type(getattr_static(C[int], "f").__get__) # revealed: reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: def f(self, x: int) -> str # revealed: bound method C[int].f(x: int) -> str reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int])) @@ -132,15 +137,12 @@ from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") -# TODO: no error -# error: [invalid-base] class Legacy(Generic[T]): def m(self, x: T, y: S) -> S: return y legacy: Legacy[int] = Legacy() -# TODO: revealed: str -reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions) +reveal_type(legacy.m(1, "string")) # revealed: Literal["string"] ``` With PEP 695 syntax, it is clearer that the method uses a separate typevar: @@ -169,13 +171,11 @@ S = TypeVar("S") def f(x: T) -> None: x: list[T] = [] - # TODO: error + # TODO: invalid-assignment error y: list[S] = [] -# TODO: no error -# error: [invalid-base] class C(Generic[T]): - # TODO: error + # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] # This is not an error, as shown in the previous test @@ -195,11 +195,11 @@ S = TypeVar("S") def f[T](x: T) -> None: x: list[T] = [] - # TODO: error + # TODO: invalid assignment error y: list[S] = [] class C[T]: - # TODO: error + # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] def m1(self, x: S) -> S: @@ -254,8 +254,7 @@ def f[T](x: T, y: T) -> None: class Ok[S]: ... # TODO: error for reuse of typevar class Bad1[T]: ... - # TODO: no non-subscriptable error, error for reuse of typevar - # error: [non-subscriptable] + # TODO: error for reuse of typevar class Bad2(Iterable[T]): ... ``` @@ -268,8 +267,7 @@ class C[T]: class Ok1[S]: ... # TODO: error for reuse of typevar class Bad1[T]: ... - # TODO: no non-subscriptable error, error for reuse of typevar - # error: [non-subscriptable] + # TODO: error for reuse of typevar class Bad2(Iterable[T]): ... ``` @@ -283,7 +281,7 @@ class C[T]: ok1: list[T] = [] class Bad: - # TODO: error + # TODO: error: cannot refer to T in nested scope bad: list[T] = [] class Inner[S]: ... diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md new file mode 100644 index 0000000000000..05b4ceb75a785 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -0,0 +1,488 @@ +# List all members + +## Basic functionality + + + +The `ty_extensions.all_members` function allows access to a tuple of accessible members/attributes +on a given object. For example, all member functions of `str` are available on `"a"`: + +```py +from ty_extensions import all_members, static_assert + +members_of_str = all_members("a") + +static_assert("replace" in members_of_str) +static_assert("startswith" in members_of_str) +static_assert("isupper" in members_of_str) +``` + +Similarly, special members such as `__add__` are also available: + +```py +static_assert("__add__" in members_of_str) +static_assert("__gt__" in members_of_str) +``` + +Members of base classes are also included (these dunder methods are defined on `object`): + +```py +static_assert("__doc__" in members_of_str) +static_assert("__repr__" in members_of_str) +``` + +Non-existent members are not included: + +```py +static_assert("non_existent" not in members_of_str) +``` + +Note: The full list of all members is relatively long, but `reveal_type` can theoretically be used +to see them all: + +```py +from typing_extensions import reveal_type + +reveal_type(members_of_str) # error: [revealed-type] +``` + +## Kinds of types + +### Class instances + +For instances of classes, `all_members` returns class members and implicit instance members of all +classes in the MRO: + +```py +from ty_extensions import all_members, static_assert + +class Base: + base_class_attr: int = 1 + + def f_base(self): + self.base_instance_attr: str = "Base" + +class Intermediate(Base): + intermediate_attr: int = 2 + + def f_intermediate(self): + self.intermediate_instance_attr: str = "Intermediate" + +class C(Intermediate): + class_attr: int = 3 + + def f_c(self): + self.instance_attr = "C" + + @property + def property_attr(self) -> int: + return 1 + + @classmethod + def class_method(cls) -> int: + return 1 + + @staticmethod + def static_method() -> int: + return 1 + +members_of_instance = all_members(C()) + +static_assert("base_class_attr" in members_of_instance) +static_assert("intermediate_attr" in members_of_instance) +static_assert("class_attr" in members_of_instance) + +static_assert("base_instance_attr" in members_of_instance) +static_assert("intermediate_instance_attr" in members_of_instance) +static_assert("instance_attr" in members_of_instance) + +static_assert("f_base" in members_of_instance) +static_assert("f_intermediate" in members_of_instance) +static_assert("f_c" in members_of_instance) + +static_assert("property_attr" in members_of_instance) +static_assert("class_method" in members_of_instance) +static_assert("static_method" in members_of_instance) + +static_assert("non_existent" not in members_of_instance) +``` + +### Class objects + +Class-level attributes can also be accessed through the class itself: + +```py +from ty_extensions import all_members, static_assert + +class Base: + base_attr: int = 1 + +class C(Base): + class_attr: str = "c" + + def f(self): + self.instance_attr = True + +members_of_class = all_members(C) + +static_assert("class_attr" in members_of_class) +static_assert("base_attr" in members_of_class) + +static_assert("non_existent" not in members_of_class) +``` + +But instance attributes can not be accessed this way: + +```py +static_assert("instance_attr" not in members_of_class) +``` + +When a class has a metaclass, members of that metaclass (and bases of that metaclass) are also +accessible: + +```py +class MetaBase(type): + meta_base_attr = 1 + +class Meta(MetaBase): + meta_attr = 2 + +class D(Base, metaclass=Meta): + class_attr = 3 + +static_assert("meta_base_attr" in all_members(D)) +static_assert("meta_attr" in all_members(D)) +static_assert("base_attr" in all_members(D)) +static_assert("class_attr" in all_members(D)) +``` + +### Generic classes + +```py +from ty_extensions import all_members, static_assert +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + base_attr: T + +static_assert("base_attr" in all_members(C[int])) +static_assert("base_attr" in all_members(C[int]())) +``` + +### Other instance-like types + +```py +from ty_extensions import all_members, static_assert +from typing_extensions import LiteralString + +static_assert("__xor__" in all_members(True)) +static_assert("bit_length" in all_members(1)) +static_assert("startswith" in all_members("a")) +static_assert("__buffer__" in all_members(b"a")) +static_assert("is_integer" in all_members(3.14)) + +def _(literal_string: LiteralString): + static_assert("startswith" in all_members(literal_string)) + +static_assert("count" in all_members(("some", "tuple", 1, 2))) + +static_assert("__doc__" in all_members(len)) +static_assert("__doc__" in all_members("a".startswith)) +``` + +### Unions + +For unions, `all_members` will only return members that are available on all elements of the union. + +```py +from ty_extensions import all_members, static_assert + +class A: + on_both: int = 1 + only_on_a: str = "a" + +class B: + on_both: int = 2 + only_on_b: str = "b" + +def f(union: A | B): + static_assert("on_both" in all_members(union)) + static_assert("only_on_a" not in all_members(union)) + static_assert("only_on_b" not in all_members(union)) +``` + +### Intersections + +#### Only positive types + +Conversely, for intersections, `all_members` will list members that are available on any of the +elements: + +```py +from ty_extensions import all_members, static_assert + +class A: + on_both: int = 1 + only_on_a: str = "a" + +class B: + on_both: int = 2 + only_on_b: str = "b" + +def f(intersection: object): + if isinstance(intersection, A): + if isinstance(intersection, B): + static_assert("on_both" in all_members(intersection)) + static_assert("only_on_a" in all_members(intersection)) + static_assert("only_on_b" in all_members(intersection)) +``` + +#### With negative types + +It also works when negative types are introduced: + +```py +from ty_extensions import all_members, static_assert + +class A: + on_all: int = 1 + only_on_a: str = "a" + only_on_ab: str = "a" + only_on_ac: str = "a" + +class B: + on_all: int = 2 + only_on_b: str = "b" + only_on_ab: str = "b" + only_on_bc: str = "b" + +class C: + on_all: int = 3 + only_on_c: str = "c" + only_on_ac: str = "c" + only_on_bc: str = "c" + +def f(intersection: object): + if isinstance(intersection, A): + if isinstance(intersection, B): + if not isinstance(intersection, C): + reveal_type(intersection) # revealed: A & B & ~C + static_assert("on_all" in all_members(intersection)) + static_assert("only_on_a" in all_members(intersection)) + static_assert("only_on_b" in all_members(intersection)) + static_assert("only_on_c" not in all_members(intersection)) + static_assert("only_on_ab" in all_members(intersection)) + static_assert("only_on_ac" in all_members(intersection)) + static_assert("only_on_bc" in all_members(intersection)) +``` + +## Modules + +### Basic support with sub-modules + +`all_members` can also list attributes on modules: + +```py +from ty_extensions import all_members, static_assert +import math + +static_assert("pi" in all_members(math)) +static_assert("cos" in all_members(math)) +``` + +This also works for submodules: + +```py +import os + +static_assert("path" in all_members(os)) + +import os.path + +static_assert("join" in all_members(os.path)) +``` + +Special members available on all modules are also included: + +```py +static_assert("__name__" in all_members(math)) +static_assert("__doc__" in all_members(math)) +``` + +### `__all__` is not respected for direct module access + +`foo.py`: + +```py +from ty_extensions import all_members, static_assert + +import bar + +static_assert("lion" in all_members(bar)) +static_assert("tiger" in all_members(bar)) +``` + +`bar.py`: + +```py +__all__ = ["lion"] + +lion = 1 +tiger = 1 +``` + +### `__all__` is respected for glob imports + +`foo.py`: + +```py +from ty_extensions import all_members, static_assert + +import bar + +static_assert("lion" in all_members(bar)) +static_assert("tiger" not in all_members(bar)) +``` + +`bar.py`: + +```py +from quux import * +``` + +`quux.py`: + +```py +__all__ = ["lion"] + +lion = 1 +tiger = 1 +``` + +### `__all__` is respected for stub files + +`module.py`: + +```py +def evaluate(x=None): + if x is None: + return 0 + return x +``` + +`module.pyi`: + +```pyi +from typing import Optional + +__all__ = ["evaluate"] + +def evaluate(x: Optional[int] = None) -> int: ... +``` + +`play.py`: + +```py +from ty_extensions import all_members, static_assert + +import module + +static_assert("evaluate" in all_members(module)) +static_assert("Optional" not in all_members(module)) +``` + +## Conditionally available members + +Some members are only conditionally available. For example, `int.bit_count` was only introduced in +Python 3.10: + +### 3.9 + +```toml +[environment] +python-version = "3.9" +``` + +```py +from ty_extensions import all_members, static_assert + +static_assert("bit_count" not in all_members(42)) +``` + +### 3.10 + +```toml +[environment] +python-version = "3.10" +``` + +```py +from ty_extensions import all_members, static_assert + +static_assert("bit_count" in all_members(42)) +``` + +## Failures cases + +### Dynamically added members + +Dynamically added members can not be accessed: + +```py +from ty_extensions import all_members, static_assert + +class C: + static_attr = 1 + + def __setattr__(self, name: str, value: str) -> None: + pass + + def __getattr__(self, name: str) -> str: + return "a" + +c = C() +c.dynamic_attr = "a" + +static_assert("static_attr" in all_members(c)) +static_assert("dynamic_attr" not in all_members(c)) +``` + +### Dataclasses + +So far, we do not include synthetic members of dataclasses. + +```py +from ty_extensions import all_members, static_assert +from dataclasses import dataclass + +@dataclass(order=True) +class Person: + name: str + age: int + +static_assert("name" in all_members(Person)) +static_assert("age" in all_members(Person)) + +# These are always available, since they are also defined on `object`: +static_assert("__init__" in all_members(Person)) +static_assert("__repr__" in all_members(Person)) +static_assert("__eq__" in all_members(Person)) + +# TODO: this should ideally be available: +static_assert("__lt__" in all_members(Person)) # error: [static-assert-error] +``` + +### Attributes not available at runtime + +Typeshed includes some attributes in `object` that are not available for some (builtin) types. For +example, `__annotations__` does not exist on `int` at runtime, but it is available as an attribute +on `object` in typeshed: + +```py +from ty_extensions import all_members, static_assert + +# TODO: this should ideally not be available: +static_assert("__annotations__" not in all_members(3)) # error: [static-assert-error] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/basic.md b/crates/ty_python_semantic/resources/mdtest/import/basic.md new file mode 100644 index 0000000000000..45abcf5018533 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/basic.md @@ -0,0 +1,222 @@ +# Structures + +## Class import following + +```py +from b import C as D + +E = D +reveal_type(E) # revealed: +``` + +`b.py`: + +```py +class C: ... +``` + +## Module member resolution + +```py +import b + +D = b.C +reveal_type(D) # revealed: +``` + +`b.py`: + +```py +class C: ... +``` + +## Nested + +```py +import a.b + +reveal_type(a.b.C) # revealed: +``` + +`a/__init__.py`: + +```py +``` + +`a/b.py`: + +```py +class C: ... +``` + +## Deeply nested + +```py +import a.b.c + +reveal_type(a.b.c.C) # revealed: +``` + +`a/__init__.py`: + +```py +``` + +`a/b/__init__.py`: + +```py +``` + +`a/b/c.py`: + +```py +class C: ... +``` + +## Nested with rename + +```py +import a.b as b + +reveal_type(b.C) # revealed: +``` + +`a/__init__.py`: + +```py +``` + +`a/b.py`: + +```py +class C: ... +``` + +## Deeply nested with rename + +```py +import a.b.c as c + +reveal_type(c.C) # revealed: +``` + +`a/__init__.py`: + +```py +``` + +`a/b/__init__.py`: + +```py +``` + +`a/b/c.py`: + +```py +class C: ... +``` + +## Unresolvable module import + + + +```py +import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve imported module `zqzqzqzqzqzqzq`" +``` + +## Unresolvable submodule imports + + + +```py +# Topmost component resolvable, submodule not resolvable: +import a.foo # error: [unresolved-import] "Cannot resolve imported module `a.foo`" + +# Topmost component unresolvable: +import b.foo # error: [unresolved-import] "Cannot resolve imported module `b.foo`" +``` + +`a/__init__.py`: + +```py +``` + +## Long paths + +It's unlikely that a single module component is as long as in this example, but Windows treats paths +that are longer than 200 and something specially. This test ensures that ty can handle those paths +gracefully. + +```toml +system = "os" +``` + +`AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath/__init__.py`: + +```py +class Foo: ... +``` + +```py +from AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath import ( + Foo, +) + +reveal_type(Foo()) # revealed: Foo +``` + +## Multiple objects imported from an unresolved module + + + +If multiple members are imported from a module that cannot be resolved, only a single diagnostic is +emitted for the `import from` statement: + +```py +# error: [unresolved-import] +from does_not_exist import foo, bar, baz +``` + +## Attempting to import a stdlib module that's not yet been added + + + +```toml +[environment] +python-version = "3.10" +``` + +```py +import tomllib # error: [unresolved-import] +from string.templatelib import Template # error: [unresolved-import] +from importlib.resources import abc # error: [unresolved-import] +``` + +## Attempting to import a stdlib module that was previously removed + + + +```toml +[environment] +python-version = "3.13" +``` + +```py +import aifc # error: [unresolved-import] +from distutils import sysconfig # error: [unresolved-import] +``` + +## Cannot shadow core standard library modules + +`types.py`: + +```py +x: int +``` + +```py +# error: [unresolved-import] +from types import x + +from types import FunctionType +``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/builtins.md b/crates/ty_python_semantic/resources/mdtest/import/builtins.md new file mode 100644 index 0000000000000..f8ef2a2f9a8f7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/builtins.md @@ -0,0 +1,78 @@ +# Builtins + +## Importing builtin module + +Builtin symbols can be explicitly imported: + +```py +import builtins + +reveal_type(builtins.chr) # revealed: def chr(i: SupportsIndex, /) -> str +``` + +## Implicit use of builtin + +Or used implicitly: + +```py +reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str +reveal_type(str) # revealed: +``` + +## Builtin symbol from custom typeshed + +If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the +builtins from the "actual" vendored typeshed: + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class Custom: ... + +custom_builtin: Custom +``` + +`/typeshed/stdlib/typing_extensions.pyi`: + +```pyi +def reveal_type(obj, /): ... +``` + +```py +reveal_type(custom_builtin) # revealed: Custom + +# error: [unresolved-reference] +reveal_type(str) # revealed: Unknown +``` + +## Unknown builtin (later defined) + +`foo` has a type of `Unknown` in this example, as it relies on `bar` which has not been defined at +that point: + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +foo = bar +bar = 1 +``` + +`/typeshed/stdlib/typing_extensions.pyi`: + +```pyi +def reveal_type(obj, /): ... +``` + +```py +reveal_type(foo) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/case_sensitive.md b/crates/ty_python_semantic/resources/mdtest/import/case_sensitive.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/case_sensitive.md rename to crates/ty_python_semantic/resources/mdtest/import/case_sensitive.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md b/crates/ty_python_semantic/resources/mdtest/import/conditional.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/conditional.md rename to crates/ty_python_semantic/resources/mdtest/import/conditional.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conflicts.md b/crates/ty_python_semantic/resources/mdtest/import/conflicts.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/conflicts.md rename to crates/ty_python_semantic/resources/mdtest/import/conflicts.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md b/crates/ty_python_semantic/resources/mdtest/import/conventions.md similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/import/conventions.md rename to crates/ty_python_semantic/resources/mdtest/import/conventions.md index bb7cf890b0a6c..c4950b5d1cc61 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md +++ b/crates/ty_python_semantic/resources/mdtest/import/conventions.md @@ -8,10 +8,9 @@ Reference: ## Builtins scope -When looking up for a name, red knot will fallback to using the builtins scope if the name is not -found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the -builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not -re-exported. +When looking up for a name, ty will fallback to using the builtins scope if the name is not found in +the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the builtins +scope, contains multiple symbols from other modules (e.g., `typing`) but those are not re-exported. ```py # These symbols are being imported in `builtins.pyi` but shouldn't be considered as being @@ -96,6 +95,7 @@ from typing import Any, Literal `foo.pyi`: ```pyi + ``` ## Nested non-exports @@ -188,7 +188,7 @@ reveal_type(Foo) # revealed: Unknown ```pyi from b import AnyFoo as Foo -reveal_type(Foo) # revealed: Literal[AnyFoo] +reveal_type(Foo) # revealed: ``` `b.pyi`: @@ -202,9 +202,9 @@ class AnyFoo: ... Here, the symbol is re-exported using the `__all__` variable. ```py -# TODO: This should *not* be an error but we don't understand `__all__` yet. -# error: "Module `a` has no member `Foo`" from a import Foo + +reveal_type(Foo) # revealed: ``` `a.pyi`: @@ -221,6 +221,44 @@ __all__ = ['Foo'] class Foo: ... ``` +## Re-exports with `__all__` + +If a symbol is re-exported via redundant alias but is not included in `__all__`, it shouldn't raise +an error when using named import. + +`named_import.py`: + +```py +from a import Foo + +reveal_type(Foo) # revealed: +``` + +`a.pyi`: + +```pyi +from b import Foo as Foo + +__all__ = [] +``` + +`b.pyi`: + +```pyi +class Foo: ... +``` + +However, a star import _would_ raise an error. + +`star_import.py`: + +```py +from a import * + +# error: [unresolved-reference] "Name `Foo` used when not defined" +reveal_type(Foo) # revealed: Unknown +``` + ## Re-exports in `__init__.pyi` Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error @@ -252,11 +290,13 @@ class Foo: ... `a/b/__init__.pyi`: ```pyi + ``` `a/b/c.pyi`: ```pyi + ``` ## Conditional re-export in stub file @@ -282,7 +322,7 @@ def coinflip() -> bool: ... if coinflip(): Foo: str = ... -reveal_type(Foo) # revealed: Literal[Foo] | str +reveal_type(Foo) # revealed: | str ``` `b.pyi`: @@ -300,7 +340,7 @@ the other does not. # error: "Member `Foo` of module `a` is possibly unbound" from a import Foo -reveal_type(Foo) # revealed: Literal[Foo] +reveal_type(Foo) # revealed: ``` `a.pyi`: @@ -313,7 +353,7 @@ if coinflip(): else: from b import Foo as Foo -reveal_type(Foo) # revealed: Literal[Foo] +reveal_type(Foo) # revealed: ``` `b.pyi`: @@ -328,7 +368,7 @@ class Foo: ... # error: "Member `Foo` of module `a` is possibly unbound" from a import Foo -reveal_type(Foo) # revealed: Literal[Foo] +reveal_type(Foo) # revealed: ``` `a.pyi`: diff --git a/crates/ty_python_semantic/resources/mdtest/import/cyclic.md b/crates/ty_python_semantic/resources/mdtest/import/cyclic.md new file mode 100644 index 0000000000000..4af714c7dea14 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/cyclic.md @@ -0,0 +1,108 @@ +## Cyclic imports + +### Regression tests + +#### Issue 261 + +See: + +`main.py`: + +```py +from foo import bar + +reveal_type(bar) # revealed: +``` + +`foo/__init__.py`: + +```py +from foo import bar + +__all__ = ["bar"] +``` + +`foo/bar/__init__.py`: + +```py +# empty +``` + +#### Issue 113 + +See: + +`main.py`: + +```py +from pkg.sub import A + +# TODO: This should be `` +reveal_type(A) # revealed: Never +``` + +`pkg/outer.py`: + +```py +class A: ... +``` + +`pkg/sub/__init__.py`: + +```py +from ..outer import * +from .inner import * +``` + +`pkg/sub/inner.py`: + +```py +from pkg.sub import A +``` + +### Actual cycle + +The following example fails at runtime. Ideally, we would emit a diagnostic here. For now, we only +make sure that this does not lead to a module resolution cycle. + +`main.py`: + +```py +from module import x + +reveal_type(x) # revealed: Unknown +``` + +`module.py`: + +```py +# error: [unresolved-import] +from module import x +``` + +### Normal self-referential import + +Some modules like `sys` in typeshed import themselves. Here, we make sure that this does not lead to +cycles or unresolved imports. + +`module/__init__.py`: + +```py +import module # self-referential import + +from module.sub import x +``` + +`module/sub.py`: + +```py +x: int = 1 +``` + +`main.py`: + +```py +from module import x + +reveal_type(x) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md new file mode 100644 index 0000000000000..d77973265b0ac --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md @@ -0,0 +1,845 @@ +# `__all__` + +Reference: + + +NOTE: This file only includes the usage of `__all__` for named-imports i.e., +`from module import symbol`. For the usage of `__all__` in wildcard imports, refer to +[star.md](star.md). + +## Undefined + +`exporter.py`: + +```py +class A: ... +class B: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: None +reveal_type(dunder_all_names(exporter)) +``` + +## Global scope + +The `__all__` variable is only recognized from the global scope of the module. It is not recognized +from the local scope of a function or class. + +`exporter.py`: + +```py +__all__ = ["A"] + +def foo(): + __all__.append("B") + +class Foo: + __all__ += ["C"] + +class A: ... +class B: ... +class C: ... + +foo() +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"]] +reveal_type(dunder_all_names(exporter)) +``` + +## Supported idioms + +According to the [specification], the following idioms are supported: + +### List assignment + +`exporter.py`: + +```py +__all__ = ["A", "B"] + +class A: ... +class B: ... +``` + +`exporter_annotated.py`: + +```py +__all__: list[str] = ["C", "D"] + +class C: ... +class D: ... +``` + +`importer.py`: + +```py +import exporter +import exporter_annotated +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["B"]] +reveal_type(dunder_all_names(exporter)) + +# revealed: tuple[Literal["C"], Literal["D"]] +reveal_type(dunder_all_names(exporter_annotated)) +``` + +### List assignment (shadowed) + +`exporter.py`: + +```py +__all__ = ["A", "B"] + +class A: ... +class B: ... + +__all__ = ["C", "D"] + +class C: ... +class D: ... +``` + +`exporter_annotated.py`: + +```py +__all__ = ["X"] + +class X: ... + +__all__: list[str] = ["Y", "Z"] + +class Y: ... +class Z: ... +``` + +`importer.py`: + +```py +import exporter +import exporter_annotated +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["C"], Literal["D"]] +reveal_type(dunder_all_names(exporter)) + +# revealed: tuple[Literal["Y"], Literal["Z"]] +reveal_type(dunder_all_names(exporter_annotated)) +``` + +### Tuple assignment + +`exporter.py`: + +```py +__all__ = ("A", "B") + +class A: ... +class B: ... +``` + +`exporter_annotated.py`: + +```py +__all__: tuple[str, ...] = ("C", "D") + +class C: ... +class D: ... +``` + +`importer.py`: + +```py +import exporter +import exporter_annotated +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["B"]] +reveal_type(dunder_all_names(exporter)) + +# revealed: tuple[Literal["C"], Literal["D"]] +reveal_type(dunder_all_names(exporter_annotated)) +``` + +### Tuple assignment (shadowed) + +`exporter.py`: + +```py +__all__ = ("A", "B") + +class A: ... +class B: ... + +__all__ = ("C", "D") + +class C: ... +class D: ... +``` + +`exporter_annotated.py`: + +```py +__all__ = ("X",) + +class X: ... + +__all__: tuple[str, ...] = ("Y", "Z") + +class Y: ... +class Z: ... +``` + +`importer.py`: + +```py +import exporter +import exporter_annotated +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["C"], Literal["D"]] +reveal_type(dunder_all_names(exporter)) + +# revealed: tuple[Literal["Y"], Literal["Z"]] +reveal_type(dunder_all_names(exporter_annotated)) +``` + +### Augmenting list with a list or submodule `__all__` + +`subexporter.py`: + +```py +__all__ = ["A", "B"] + +class A: ... +class B: ... +``` + +`exporter.py`: + +```py +import subexporter + +__all__ = [] +__all__ += ["C", "D"] +__all__ += subexporter.__all__ + +class C: ... +class D: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Augmenting list with a list or submodule `__all__` (2) + +The same again, but the submodule is an attribute expression rather than a name expression: + +`exporter/__init__.py`: + +```py +``` + +`exporter/sub.py`: + +```py +__all__ = ["foo"] + +foo = 42 +``` + +`exporter/sub2.py`: + +```py +__all__ = ["bar"] + +bar = 56 +``` + +`module.py`: + +```py +import exporter.sub +import exporter.sub2 + +__all__ = [] + +if True: + __all__.extend(exporter.sub.__all__) + __all__ += exporter.sub2.__all__ +``` + +`main.py`: + +```py +import module +from ty_extensions import dunder_all_names + +reveal_type(dunder_all_names(module)) # revealed: tuple[Literal["bar"], Literal["foo"]] +``` + +### Extending with a list or submodule `__all__` + +`subexporter.py`: + +```py +__all__ = ["A", "B"] + +class A: ... +class B: ... +``` + +`exporter.py`: + +```py +import subexporter + +__all__ = [] +__all__.extend(["C", "D"]) +__all__.extend(("E", "F")) +__all__.extend({"G", "H"}) +__all__.extend(subexporter.__all__) + +class C: ... +class D: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["B"], Literal["C"], Literal["D"], Literal["E"], Literal["F"], Literal["G"], Literal["H"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Appending a single symbol + +`exporter.py`: + +```py +__all__ = ["A"] +__all__.append("B") + +class A: ... +class B: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["B"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Removing a single symbol + +`exporter.py`: + +```py +__all__ = ["A", "B"] +__all__.remove("A") + +# Non-existant symbol in `__all__` at this point +# TODO: This raises `ValueError` at runtime, maybe we should raise a diagnostic as well? +__all__.remove("C") + +class A: ... +class B: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["B"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Mixed + +`subexporter.py`: + +```py +__all__ = [] + +__all__ = ["A"] +__all__.append("B") +__all__.extend(["C"]) +__all__.remove("B") + +class A: ... +class B: ... +class C: ... +``` + +`exporter.py`: + +```py +import subexporter + +__all__ = [] +__all__ += ["D"] +__all__ += subexporter.__all__ + +class D: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["C"], Literal["D"]] +reveal_type(dunder_all_names(exporter)) +``` + +## Invalid + +### Unsupported idioms + +Idioms that are not mentioned in the [specification] are not recognized by `ty` and if they're used, +`__all__` is considered to be undefined for that module. This is to avoid false positives. + +`bar.py`: + +```py +__all__ = ["A", "B"] + +class A: ... +class B: ... +``` + +`foo.py`: + +```py +import bar as bar +``` + +`exporter.py`: + +```py +import foo +from ty_extensions import dunder_all_names + +__all__ = [] + +# revealed: tuple[Literal["A"], Literal["B"]] +reveal_type(dunder_all_names(foo.bar)) + +# Only direct attribute access of modules are recognized +# TODO: warning diagnostic +__all__.extend(foo.bar.__all__) +# TODO: warning diagnostic +__all__ += foo.bar.__all__ + +# Augmented assignment is only allowed when the value is a list expression +# TODO: warning diagnostic +__all__ += ("C",) + +# Other methods on `list` are not recognized +# TODO: warning diagnostic +__all__.insert(0, "C") +# TODO: warning diagnostic +__all__.clear() + +__all__.append("C") +# `pop` is not valid; use `remove` instead +# TODO: warning diagnostic +__all__.pop() + +# Sets are not recognized +# TODO: warning diagnostic +__all__ = {"C", "D"} + +class C: ... +class D: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: None +reveal_type(dunder_all_names(exporter)) +``` + +### Non-string elements + +Similarly, if `__all__` contains any non-string elements, we will consider `__all__` to not be +defined for that module. This is also to avoid false positives. + +`subexporter.py`: + +```py +__all__ = ("A", "B") + +class A: ... +class B: ... +``` + +`exporter1.py`: + +```py +import subexporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["B"]] +reveal_type(dunder_all_names(subexporter)) + +# TODO: warning diagnostic +__all__ = ("C", *subexporter.__all__) + +class C: ... +``` + +`importer.py`: + +```py +import exporter1 +from ty_extensions import dunder_all_names + +# revealed: None +reveal_type(dunder_all_names(exporter1)) +``` + +## Statically known branches + +### Python 3.10 + +```toml +[environment] +python-version = "3.10" +``` + +`exporter.py`: + +```py +import sys + +__all__ = ["AllVersion"] + +if sys.version_info >= (3, 12): + __all__ += ["Python312"] +elif sys.version_info >= (3, 11): + __all__ += ["Python311"] +else: + __all__ += ["Python310"] + +class AllVersion: ... + +if sys.version_info >= (3, 12): + class Python312: ... + +elif sys.version_info >= (3, 11): + class Python311: ... + +else: + class Python310: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["AllVersion"], Literal["Python310"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Python 3.11 + +```toml +[environment] +python-version = "3.11" +``` + +`exporter.py`: + +```py +import sys + +__all__ = ["AllVersion"] + +if sys.version_info >= (3, 12): + __all__ += ["Python312"] +elif sys.version_info >= (3, 11): + __all__ += ["Python311"] +else: + __all__ += ["Python310"] + +class AllVersion: ... + +if sys.version_info >= (3, 12): + class Python312: ... + +elif sys.version_info >= (3, 11): + class Python311: ... + +else: + class Python310: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["AllVersion"], Literal["Python311"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Python 3.12 + +```toml +[environment] +python-version = "3.12" +``` + +`exporter.py`: + +```py +import sys + +__all__ = ["AllVersion"] + +if sys.version_info >= (3, 12): + __all__ += ["Python312"] +elif sys.version_info >= (3, 11): + __all__ += ["Python311"] +else: + __all__ += ["Python310"] + +class AllVersion: ... + +if sys.version_info >= (3, 12): + class Python312: ... + +elif sys.version_info >= (3, 11): + class Python311: ... + +else: + class Python310: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["AllVersion"], Literal["Python312"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Multiple `if` statements + +```toml +[environment] +python-version = "3.11" +``` + +`exporter.py`: + +```py +import sys + +__all__ = ["AllVersion"] + +if sys.version_info >= (3, 12): + __all__ += ["Python312"] + +if sys.version_info >= (3, 11): + __all__ += ["Python311"] + +if sys.version_info >= (3, 10): + __all__ += ["Python310"] + +class AllVersion: ... + +if sys.version_info >= (3, 12): + class Python312: ... + +if sys.version_info >= (3, 11): + class Python311: ... + +if sys.version_info >= (3, 10): + class Python310: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["AllVersion"], Literal["Python310"], Literal["Python311"]] +reveal_type(dunder_all_names(exporter)) +``` + +## Origin + +`__all__` can be defined in a module mainly in the following three ways: + +### Directly in the module + +`exporter.py`: + +```py +__all__ = ["A"] + +class A: ... +``` + +`importer.py`: + +```py +import exporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Using named import + +`subexporter.py`: + +```py +__all__ = ["A"] + +class A: ... +``` + +`exporter.py`: + +```py +from subexporter import __all__ + +__all__.append("B") + +class B: ... +``` + +`importer.py`: + +```py +import exporter +import subexporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"]] +reveal_type(dunder_all_names(subexporter)) +# revealed: tuple[Literal["A"], Literal["B"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Using wildcard import (1) + +Wildcard import doesn't export `__all__` unless it is explicitly included in the `__all__` of the +module. + +`subexporter.py`: + +```py +__all__ = ["A", "__all__"] + +class A: ... +``` + +`exporter.py`: + +```py +from subexporter import * + +# TODO: Should be `list[str]` +# TODO: Should we avoid including `Unknown` for this case? +reveal_type(__all__) # revealed: Unknown | list[Unknown] + +__all__.append("B") + +class B: ... +``` + +`importer.py`: + +```py +import exporter +import subexporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"], Literal["__all__"]] +reveal_type(dunder_all_names(subexporter)) +# revealed: tuple[Literal["A"], Literal["B"], Literal["__all__"]] +reveal_type(dunder_all_names(exporter)) +``` + +### Using wildcard import (2) + +`subexporter.py`: + +```py +__all__ = ["A"] + +class A: ... +``` + +`exporter.py`: + +```py +from subexporter import * + +# error: [unresolved-reference] +reveal_type(__all__) # revealed: Unknown + +# error: [unresolved-reference] +__all__.append("B") + +class B: ... +``` + +`importer.py`: + +```py +import exporter +import subexporter +from ty_extensions import dunder_all_names + +# revealed: tuple[Literal["A"]] +reveal_type(dunder_all_names(subexporter)) +# revealed: None +reveal_type(dunder_all_names(exporter)) +``` + +[specification]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols diff --git a/crates/ty_python_semantic/resources/mdtest/import/errors.md b/crates/ty_python_semantic/resources/mdtest/import/errors.md new file mode 100644 index 0000000000000..14e785613ba35 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/errors.md @@ -0,0 +1,88 @@ +# Unresolved Imports + +## Unresolved import statement + +```py +import bar # error: "Cannot resolve imported module `bar`" + +reveal_type(bar) # revealed: Unknown +``` + +## Unresolved import from statement + +```py +from bar import baz # error: "Cannot resolve imported module `bar`" + +reveal_type(baz) # revealed: Unknown +``` + +## Unresolved import from resolved module + +`a.py`: + +```py +``` + +```py +from a import thing # error: "Module `a` has no member `thing`" + +reveal_type(thing) # revealed: Unknown +``` + +## Resolved import of symbol from unresolved import + +`a.py`: + +```py +import foo as foo # error: "Cannot resolve imported module `foo`" + +reveal_type(foo) # revealed: Unknown +``` + +Importing the unresolved import into a second file should not trigger an additional "unresolved +import" violation: + +```py +from a import foo + +reveal_type(foo) # revealed: Unknown +``` + +## No implicit shadowing + +`b.py`: + +```py +x: int +``` + +```py +from b import x + +x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]" +``` + +## Import cycle + +`a.py`: + +```py +class A: ... + +reveal_type(A.__mro__) # revealed: tuple[, ] +import b + +class C(b.B): ... + +reveal_type(C.__mro__) # revealed: tuple[, , , ] +``` + +`b.py`: + +```py +from a import A + +class B(A): ... + +reveal_type(B.__mro__) # revealed: tuple[, , ] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md diff --git a/crates/ty_python_semantic/resources/mdtest/import/namespace.md b/crates/ty_python_semantic/resources/mdtest/import/namespace.md new file mode 100644 index 0000000000000..3b5f63e9818b3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/namespace.md @@ -0,0 +1,148 @@ +# Namespace package + +## Basic namespace package + +```toml +[environment] +python = "/.venv" +``` + +`parent/child/one.py`: + +```py +one = 1 +``` + +`/.venv//parent/child/two.py`: + +```py +two = 2 +``` + +`main.py`: + +```py +import parent.child.one +import parent.child.two +``` + +`from.py` + +```py +from parent.child import one, two + +reveal_type(one) # revealed: +reveal_type(two) # revealed: +``` + +## Regular package in namespace package + +```toml +[environment] +python = "/.venv" +``` + +An adapted test case from the +[PEP420 examples](https://peps.python.org/pep-0420/#nested-namespace-packages). The +`src/parent/child` package is a regular package. Therefore, `site_packages/parent/child/two.py` +should not be resolved. + +```ignore +src + parent + child + __init__.py + one.py +.venv/site-packages + parent + child + two.py +``` + +`parent/child/__init__.py`: + +```py +``` + +`parent/child/one.py`: + +```py +one = 1 +``` + +`/.venv//parent/child/two.py`: + +```py +two = 2 +``` + +`main.py`: + +```py +import parent.child.one + +import parent.child.two # error: [unresolved-import] +``` + +## Priority between file and identically named namespace package + +If there's a namespace package with the same name as a module, the module takes precedence. + +`foo.py`: + +```py +x = "module" +``` + +`foo/bar.py`: + +```py +x = "namespace" +``` + +```py +from foo import x + +reveal_type(x) # revealed: Unknown | Literal["module"] + +import foo.bar # error: [unresolved-import] +``` + +## `from` import with namespace package + +Regression test for + +`google/cloud/pubsub_v1/__init__.py`: + +```py +class PublisherClient: ... +``` + +```py +from google.cloud import pubsub_v1 + +reveal_type(pubsub_v1.PublisherClient) # revealed: +``` + +## `from` root importing sub-packages + +Regresssion test for + +`opentelemetry/trace/__init__.py`: + +```py +class Trace: ... +``` + +`opentelemetry/metrics/__init__.py`: + +```py +class Metric: ... +``` + +```py +from opentelemetry import trace, metrics + +reveal_type(trace) # revealed: +reveal_type(metrics) # revealed: +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md b/crates/ty_python_semantic/resources/mdtest/import/relative.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/import/relative.md rename to crates/ty_python_semantic/resources/mdtest/import/relative.md index a07ac61b9bf13..9c3e303a9d73c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md +++ b/crates/ty_python_semantic/resources/mdtest/import/relative.md @@ -222,8 +222,8 @@ reveal_type(package.foo.X) # revealed: Unknown ## Relative imports at the top of a search path Relative imports at the top of a search path result in a runtime error: -`ImportError: attempted relative import with no known parent package`. That's why Red Knot should -disallow them. +`ImportError: attempted relative import with no known parent package`. That's why ty should disallow +them. `parser.py`: @@ -255,7 +255,7 @@ python-version = "3.13" ```py from foo import A -reveal_type(A) # revealed: Literal[A] +reveal_type(A) # revealed: ``` `/src/.venv//foo/__init__.py`: diff --git a/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md b/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md new file mode 100644 index 0000000000000..a1c256c375533 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md @@ -0,0 +1,207 @@ +# Tests for `site-packages` discovery + +## Malformed or absent `version` fields + +The `version`/`version_info` key in a `pyvenv.cfg` file is provided by most virtual-environment +creation tools to indicate the Python version the virtual environment is for. They key is useful for +our purposes, so we try to parse it when possible. However, the key is not read by the CPython +standard library, and is provided under different keys depending on which virtual-environment +creation tool created the `pyvenv.cfg` file (the stdlib `venv` module calls the key `version`, +whereas uv and virtualenv both call it `version_info`). We therefore do not return an error when +discovering a virtual environment's `site-packages` directory if the virtula environment contains a +`pyvenv.cfg` file which doesn't have this key, or if the associated value of the key doesn't parse +according to our expectations. The file isn't really *invalid* in this situation. + +### No `version` field + +```toml +[environment] +python = "/.venv" +``` + +`/.venv/pyvenv.cfg`: + +```cfg +home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin +``` + +`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`: + +```text +``` + +`/.venv//foo.py`: + +```py +X: int = 42 +``` + +`/src/main.py`: + +```py +from foo import X + +reveal_type(X) # revealed: int +``` + +### Malformed stdlib-style version field + +```toml +[environment] +python = "/.venv" +``` + +`/.venv/pyvenv.cfg`: + +```cfg +home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin +version = wut +``` + +`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`: + +```text +``` + +`/.venv//foo.py`: + +```py +X: int = 42 +``` + +`/src/main.py`: + +```py +from foo import X + +reveal_type(X) # revealed: int +``` + +### Malformed uv-style version field + +```toml +[environment] +python = "/.venv" +``` + +`/.venv/pyvenv.cfg`: + +```cfg +home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin +version_info = no-really-wut +``` + +`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`: + +```text +``` + +`/.venv//foo.py`: + +```py +X: int = 42 +``` + +`/src/main.py`: + +```py +from foo import X + +reveal_type(X) # revealed: int +``` + +## Ephemeral uv environments + +If you use the `--with` flag when invoking `uv run`, uv will create an "ephemeral" virtual +environment that is layered on top of the pre-existing environment. `site-packages` directories from +the pre-existing environment will be added as an import search path at runtime as well as the +`site-packages` directory from the ephemeral environment. The `VIRTUAL_ENV` environment variable +will only point to the ephemeral virtual environment, but, following uv commit +`7bba3d00d4ad1fb3daba86b98eb25d8d9e9836ae`, uv writes the `sys.prefix` path of the parent +environment to an `extends-environment` key in the ephemeral environment's `pyvenv.cfg` file. + +This test ensures that we are able to resolve imports that point to packages in either +`site-packages` directory (the one of the ephemeral environment or the one of the parent +environment) if we detect that an ephemeral uv environment has been activated. + +```toml +[environment] +python = "/.venv" +``` + +`/.venv/pyvenv.cfg`: + +```cfg +home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin +implementation = CPython +uv = 0.7.6 +version_info = 3.13.2 +include-system-site-packages = false +prompt = ruff +extends-environment = /.other-environment +``` + +`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`: + +```text +``` + +`/.venv//foo.py`: + +```py +X: int = 42 +``` + +`/.other-environment//bar.py`: + +```py +Y: "str" = "Y" +``` + +`/src/main.py`: + +```py +from foo import X +from bar import Y + +reveal_type(X) # revealed: int +reveal_type(Y) # revealed: str +``` + +## `pyvenv.cfg` files with unusual values + +`pyvenv.cfg` files can have unusual values in them, which can contain arbitrary characters. This +includes `=` characters. The following is a regression test for +. + +```toml +[environment] +python = "/.venv" +``` + +`/.venv/pyvenv.cfg`: + +```cfg +home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin +version_info = 3.13 +command = /.pyenv/versions/3.13.3/bin/python3.13 -m venv --without-pip --prompt="python-default/3.13.3" /somewhere-else/python/virtualenvs/python-default/3.13.3 +``` + +`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`: + +```text +``` + +`/.venv//foo.py`: + +```py +X: int = 42 +``` + +`/src/main.py`: + +```py +from foo import X + +reveal_type(X) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/import/star.md rename to crates/ty_python_semantic/resources/mdtest/import/star.md index 023d46ab100ba..8ac7ea4e0260d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -122,6 +122,11 @@ from c import Y # error: [unresolved-import] ## Esoteric definitions and redefinintions +```toml +[environment] +python-version = "3.12" +``` + We understand all public symbols defined in an external module as being imported by a `*` import, not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section provides tests for definitions, and redefinitions, that use more esoteric AST nodes. @@ -184,7 +189,7 @@ match 42: ... case [O]: ... - case P | Q: + case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable" ... case object(foo=R): ... @@ -284,7 +289,7 @@ match 42: ... case [D]: ... - case E | F: + case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable" ... case object(foo=G): ... @@ -352,7 +357,7 @@ match 42: ... case [D]: ... - case E | F: + case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable" ... case object(foo=G): ... @@ -480,7 +485,7 @@ reveal_type(s) # revealed: Unknown reveal_type(t) # revealed: Unknown # TODO: these should all reveal `Unknown | int` and should not emit errors. -# (We don't generally model elsewhere in red-knot that bindings from walruses +# (We don't generally model elsewhere in ty that bindings from walruses # "leak" from comprehension scopes into outer scopes, but we should.) # See https://github.com/astral-sh/ruff/issues/16954 # error: [unresolved-reference] @@ -650,7 +655,7 @@ from b import * reveal_type(X) # revealed: bool ``` -## Visibility constraints +## Reachability constraints If an `importer` module contains a `from exporter import *` statement in its global namespace, the statement will *not* necessarily import *all* symbols that have definitions in `exporter.py`'s @@ -659,13 +664,13 @@ imported by the `*` import if at least one definition for that symbol is visible `exporter.py`'s global scope. For example, say that `exporter.py` contains a symbol `X` in its global scope, and the definition -for `X` in `exporter.py` has visibility constraints vis1. The +for `X` in `exporter.py` has reachability constraints c1. The `from exporter import *` statement in `importer.py` creates a definition for `X` in `importer`, and -there are visibility constraints vis2 on the import statement in -`importer.py`. This means that the overall visibility constraints on the `X` definnition created by -the import statement in `importer.py` will be vis1 AND vis2. +there are reachability constraints c2 on the import statement in +`importer.py`. This means that the overall reachability constraints on the `X` definition created by +the import statement in `importer.py` will be c1 AND c2. -A visibility constraint in the external module must be understood and evaluated whether or not its +A reachability constraint in the external module must be understood and evaluated whether or not its truthiness can be statically determined. ### Statically known branches in the external module @@ -707,11 +712,19 @@ reveal_type(Y) # revealed: Unknown # to be dead code given the `python-version` configuration. # Thus this still reveals `Literal[True]`. reveal_type(Z) # revealed: Literal[True] + +# Make sure that reachability constraints are also correctly applied +# for nonlocal lookups: +def _(): + reveal_type(X) # revealed: bool + # error: [unresolved-reference] + reveal_type(Y) # revealed: Unknown + reveal_type(Z) # revealed: bool ``` -### Multiple `*` imports with always-false visibility constraints +### Multiple `*` imports with always-false reachability constraints -Our understanding of visibility constraints in an external module remains accurate, even if there +Our understanding of reachability constraints in an external module remains accurate, even if there are multiple `*` imports from that module. ```toml @@ -740,7 +753,7 @@ from exporter import * reveal_type(Z) # revealed: Literal[True] ``` -### Ambiguous visibility constraints +### Ambiguous reachability constraints Some constraints in the external module may resolve to an "ambiguous truthiness". For these, we should emit `possibly-unresolved-reference` diagnostics when they are used in the module in which @@ -770,7 +783,7 @@ reveal_type(A) # revealed: Unknown | Literal[1] reveal_type(B) # revealed: Unknown | Literal[2, 3] ``` -### Visibility constraints in the importing module +### Reachability constraints in the importing module `exporter.py`: @@ -791,7 +804,7 @@ if coinflip(): reveal_type(A) # revealed: Unknown | Literal[1] ``` -### Visibility constraints in the exporting module *and* the importing module +### Reachability constraints in the exporting module *and* the importing module ```toml [environment] @@ -822,8 +835,8 @@ if sys.version_info >= (3, 12): from exporter import * # it's correct to have no diagnostics here as this branch is unreachable - reveal_type(A) # revealed: Unknown - reveal_type(B) # revealed: bool + reveal_type(A) # revealed: Never + reveal_type(B) # revealed: Never else: from exporter import * @@ -894,8 +907,8 @@ reveal_type(__protected) # revealed: bool reveal_type(__dunder__) # revealed: bool reveal_type(___thunder___) # revealed: bool -# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown` -reveal_type(Y) # revealed: bool +# error: [unresolved-reference] +reveal_type(Y) # revealed: Unknown ``` ### Simple list `__all__` @@ -916,8 +929,8 @@ from exporter import * reveal_type(X) # revealed: bool -# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown` -reveal_type(Y) # revealed: bool +# error: [unresolved-reference] +reveal_type(Y) # revealed: Unknown ``` ### `__all__` with additions later on in the global scope @@ -944,15 +957,13 @@ __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 +E: bool = False ``` `c.py`: @@ -964,11 +975,10 @@ 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 +# error: [unresolved-reference] +reveal_type(E) # revealed: Unknown ``` ### `__all__` with subtractions later on in the global scope @@ -980,7 +990,7 @@ one way of subtracting from `__all__` that type checkers are required to support ```py __all__ = ["A", "B"] -__all__.remove("A") +__all__.remove("B") A: bool = True B: bool = True @@ -993,8 +1003,8 @@ from exporter import * reveal_type(A) # revealed: bool -# TODO should emit an [unresolved-reference] diagnostic & reveal `Unknown` -reveal_type(B) # revealed: bool +# error: [unresolved-reference] +reveal_type(B) # revealed: Unknown ``` ### Invalid `__all__` @@ -1120,8 +1130,8 @@ else: ```py from exporter import * -# TODO: should reveal `Unknown` and emit `[unresolved-reference]` -reveal_type(X) # revealed: bool +# error: [unresolved-reference] +reveal_type(X) # revealed: Unknown # error: [unresolved-reference] reveal_type(Y) # revealed: Unknown @@ -1194,8 +1204,8 @@ else: ```py from exporter import * -# TODO: should reveal `Unknown` & emit `[unresolved-reference] -reveal_type(X) # revealed: bool +# error: [unresolved-reference] +reveal_type(X) # revealed: Unknown # error: [unresolved-reference] reveal_type(Y) # revealed: Unknown @@ -1230,9 +1240,11 @@ __all__ = [] 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 +# error: [unresolved-reference] +reveal_type(X) # revealed: Unknown + +# error: [unresolved-reference] +reveal_type(Y) # revealed: Unknown ``` ### `__all__` in a stub file @@ -1252,7 +1264,11 @@ Y: bool = True ```pyi from a import X, Y -__all__ = ["X"] +__all__ = ["X", "Z"] + +Z: bool = True + +Nope: bool = True ``` `c.py`: @@ -1260,18 +1276,21 @@ __all__ = ["X"] ```py from b import * -# TODO: should not error, should reveal `bool` -# (`X` is re-exported from `b.pyi` due to presence in `__all__`) -# See https://github.com/astral-sh/ruff/issues/16159 -# -# error: [unresolved-reference] -reveal_type(X) # revealed: Unknown +# `X` is re-exported from `b.pyi` due to presence in `__all__` +reveal_type(X) # revealed: bool # This diagnostic is accurate: `Y` does not use the "redundant alias" convention in `b.pyi`, -# nor is it included in `b.__all__`, so it is not exported from `b.pyi` +# nor is it included in `b.__all__`, so it is not exported from `b.pyi`. It would still be +# an error if it used the "redundant alias" convention as `__all__` would take precedence. # # error: [unresolved-reference] reveal_type(Y) # revealed: Unknown + +# `Z` is defined in `b.pyi` and included in `__all__` +reveal_type(Z) # revealed: bool + +# error: [unresolved-reference] +reveal_type(Nope) # revealed: Unknown ``` ## `global` statements in non-global scopes @@ -1348,12 +1367,9 @@ are present due to `*` imports. ```py import collections.abc -reveal_type(collections.abc.Sequence) # revealed: Literal[Sequence] +reveal_type(collections.abc.Sequence) # revealed: reveal_type(collections.abc.Callable) # revealed: typing.Callable - -# TODO: false positive as it's only re-exported from `_collections.abc` due to presence in `__all__` -# error: [unresolved-attribute] -reveal_type(collections.abc.Set) # revealed: Unknown +reveal_type(collections.abc.Set) # revealed: ``` ## Invalid `*` imports @@ -1364,13 +1380,12 @@ If the module is unresolved, we emit a diagnostic just like for any other unreso ```py # TODO: not a great error message -from foo import * # error: [unresolved-import] "Cannot resolve import `foo`" +from foo import * # error: [unresolved-import] "Cannot resolve imported module `foo`" ``` ### Nested scope -A `*` import in a nested scope are always a syntax error. Red-knot does not infer any bindings from -them: +A `*` import in a nested scope are always a syntax error. Ty does not infer any bindings from them: `exporter.py`: @@ -1382,7 +1397,7 @@ X: bool = True ```py def f(): - # TODO: we should emit a syntax errror here (tracked by https://github.com/astral-sh/ruff/issues/11934) + # TODO: we should emit a syntax error here (tracked by https://github.com/astral-sh/ruff/issues/17412) from exporter import * # error: [unresolved-reference] diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/stub_packages.md b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/import/stub_packages.md rename to crates/ty_python_semantic/resources/mdtest/import/stub_packages.md index fef47bf47deb8..3f99e5d92b54a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/stub_packages.md +++ b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md @@ -284,3 +284,32 @@ from shapes import Hexagon, Pentagon reveal_type(Pentagon().sides) # revealed: int reveal_type(Hexagon().area) # revealed: int | float ``` + +## Relative import in stub package + +Regression test for + +```toml +[environment] +extra-paths = ["/packages"] +``` + +`/packages/yaml-stubs/__init__.pyi`: + +```pyi +from .loader import * +``` + +`/packages/yaml-stubs/loader.pyi`: + +```pyi +class YamlLoader: ... +``` + +`main.py`: + +```py +import yaml + +reveal_type(yaml.YamlLoader) # revealed: +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/stubs.md b/crates/ty_python_semantic/resources/mdtest/import/stubs.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/stubs.md rename to crates/ty_python_semantic/resources/mdtest/import/stubs.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/tracking.md b/crates/ty_python_semantic/resources/mdtest/import/tracking.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/import/tracking.md rename to crates/ty_python_semantic/resources/mdtest/import/tracking.md index effbcd4a9849f..f31193b7628e3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/tracking.md +++ b/crates/ty_python_semantic/resources/mdtest/import/tracking.md @@ -27,7 +27,7 @@ has been imported. import a # Would be an error with flow-sensitive tracking -reveal_type(a.b.C) # revealed: Literal[C] +reveal_type(a.b.C) # revealed: import a.b ``` @@ -53,10 +53,10 @@ submodule `b`, even though `a.b` is never imported in the main module. from q import a, b reveal_type(b) # revealed: -reveal_type(b.C) # revealed: Literal[C] +reveal_type(b.C) # revealed: reveal_type(a.b) # revealed: -reveal_type(a.b.C) # revealed: Literal[C] +reveal_type(a.b.C) # revealed: ``` `a/__init__.py`: diff --git a/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md new file mode 100644 index 0000000000000..4f5bc1dd2b533 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md @@ -0,0 +1,302 @@ +# Tests for ty's `instance-layout-conflict` error code + +## `__slots__`: not specified or empty + +```py +class A: ... + +class B: + __slots__ = () + +class C: + __slots__ = ("lorem", "ipsum") + +class AB(A, B): ... # fine +class AC(A, C): ... # fine +class BC(B, C): ... # fine +class ABC(A, B, C): ... # fine +``` + +## `__slots__`: incompatible tuples + + + +```py +class A: + __slots__ = ("a", "b") + +class B: + __slots__ = ("c", "d") + +class C( # error: [instance-layout-conflict] + A, + B, +): ... +``` + +## `__slots__` are the same value + +```py +class A: + __slots__ = ("a", "b") + +class B: + __slots__ = ("a", "b") + +class C( # error: [instance-layout-conflict] + A, + B, +): ... +``` + +## `__slots__` is a string + +```py +class A: + __slots__ = "abc" + +class B: + __slots__ = ("abc",) + +class AB( # error: [instance-layout-conflict] + A, + B, +): ... +``` + +## Invalid `__slots__` definitions + +TODO: Emit diagnostics + +```py +class NonString1: + __slots__ = 42 + +class NonString2: + __slots__ = b"ar" + +class NonIdentifier1: + __slots__ = "42" + +class NonIdentifier2: + __slots__ = ("lorem", "42") + +class NonIdentifier3: + __slots__ = (e for e in ("lorem", "42")) +``` + +## Inherited `__slots__` + +```py +class A: + __slots__ = ("a", "b") + +class B(A): ... + +class C: + __slots__ = ("c", "d") + +class D(C): ... +class E( # error: [instance-layout-conflict] + B, + D, +): ... +``` + +## A single "solid base" + +```py +class A: + __slots__ = ("a", "b") + +class B(A): ... +class C(A): ... +class D(B, A): ... # fine +class E(B, C, A): ... # fine +``` + +## Post-hoc modifications to `__slots__` + +```py +class A: + __slots__ = () + __slots__ += ("a", "b") + +reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]] + +class B: + __slots__ = ("c", "d") + +class C( # error: [instance-layout-conflict] + A, + B, +): ... +``` + +## Explicitly annotated `__slots__` + +We do not emit false positives on classes with empty `__slots__` definitions, even if the +`__slots__` definitions are annotated with variadic tuples: + +```py +class Foo: + __slots__: tuple[str, ...] = () + +class Bar: + __slots__: tuple[str, ...] = () + +class Baz(Foo, Bar): ... # fine +``` + +## Built-ins with implicit layouts + + + +Certain classes implemented in C extensions also have an extended instance memory layout, in the +same way as classes that define non-empty `__slots__`. (CPython internally calls all such classes +with a unique instance memory layout "solid bases", and we also borrow this term.) There is +currently no generalized way for ty to detect such a C-extension class, as there is currently no way +of expressing the fact that a class is a solid base in a stub file. However, ty special-cases +certain builtin classes in order to detect that attempting to combine them in a single MRO would +fail: + +```py +# fmt: off + +class A( # error: [instance-layout-conflict] + int, + str +): ... + +class B: + __slots__ = ("b",) + +class C( # error: [instance-layout-conflict] + int, + B, +): ... +class D(int): ... + +class E( # error: [instance-layout-conflict] + D, + str +): ... + +class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] + +# fmt: on +``` + +We avoid emitting an `instance-layout-conflict` diagnostic for this class definition, because +`range` is `@final`, so we'll complain about the `class` statement anyway: + +```py +class Foo(range, str): ... # error: [subclass-of-final-class] +``` + +## Multiple "solid bases" where one is a subclass of the other + +A class is permitted to multiple-inherit from multiple solid bases if one is a subclass of the +other: + +```py +class A: + __slots__ = ("a",) + +class B(A): + __slots__ = ("b",) + +class C(B, A): ... # fine +``` + +The same principle, but a more complex example: + +```py +class AA: + __slots__ = ("a",) + +class BB(AA): + __slots__ = ("b",) + +class CC(BB): ... +class DD(AA): ... +class FF(CC, DD): ... # fine +``` + +## False negatives + +### Possibly unbound `__slots__` + +```py +def _(flag: bool): + class A: + if flag: + __slots__ = ("a", "b") + + class B: + __slots__ = ("c", "d") + + # Might or might not be fine at runtime + class C(A, B): ... +``` + +### Bound `__slots__` but with different types + +```py +def _(flag: bool): + class A: + if flag: + __slots__ = ("a", "b") + else: + __slots__ = () + + class B: + __slots__ = ("c", "d") + + # Might or might not be fine at runtime + class C(A, B): ... +``` + +### Non-tuple `__slots__` definitions + +```py +class A: + __slots__ = ["a", "b"] # This is treated as "dynamic" + +class B: + __slots__ = ("c", "d") + +# False negative: [incompatible-slots] +class C(A, B): ... +``` + +### Diagnostic if `__slots__` is externally modified + +We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol +is not declared — a case in which we union with `Unknown` for other public symbols. The reason for +this is that `__slots__` has a special handling in the runtime. Modifying it externally is actually +allowed, but those changes do not take effect. If you have a class `C` with `__slots__ = ("foo",)` +and externally set `C.__slots__ = ("bar",)`, you still can't access `C.bar`. And you can still +access `C.foo`. We therefore issue a diagnostic for such assignments: + +```py +class A: + __slots__ = ("a",) + + # Modifying `__slots__` from within the class body is fine: + __slots__ = ("a", "b") + +# No `Unknown` here: +reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]] + +# But modifying it externally is not: + +# error: [invalid-assignment] +A.__slots__ = ("a",) + +# error: [invalid-assignment] +A.__slots__ = ("a", "b_new") + +# error: [invalid-assignment] +A.__slots__ = ("a", "b", "c") +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/intersection_types.md rename to crates/ty_python_semantic/resources/mdtest/intersection_types.md index 6aed38694a92f..66f350bfe936d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -8,7 +8,7 @@ intersection types (note that we display negative contributions at the end; the matter): ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -35,7 +35,7 @@ classes. We use `P`, `Q`, `R`, … to denote types that are non-disjoint: ```py -from knot_extensions import static_assert, is_disjoint_from +from ty_extensions import static_assert, is_disjoint_from class P: ... class Q: ... @@ -56,7 +56,7 @@ We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, a supertype of these: ```py -from knot_extensions import static_assert, is_disjoint_from, is_subtype_of +from ty_extensions import static_assert, is_disjoint_from, is_subtype_of from typing import Literal static_assert(is_disjoint_from(Literal[1], Literal[2])) @@ -73,7 +73,7 @@ static_assert(is_subtype_of(Literal[3], int)) Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes: ```py -from knot_extensions import static_assert, is_subtype_of, is_disjoint_from +from ty_extensions import static_assert, is_subtype_of, is_disjoint_from class A: ... class B(A): ... @@ -111,7 +111,7 @@ If we have an intersection with a single element, we can simplify to that elemen show an intersection with a single negative contribution as just the negation of that element. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... @@ -128,7 +128,7 @@ def _( We eagerly flatten nested intersections types. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -179,7 +179,7 @@ We always normalize our representation to a _union of intersections_, so when we intersection_, we distribute the union over the respective elements: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -191,9 +191,9 @@ def _( i2: Intersection[P | Q | R, S], i3: Intersection[P | Q, R | S], ) -> None: - reveal_type(i1) # revealed: P & Q | P & R | P & S - reveal_type(i2) # revealed: P & S | Q & S | R & S - reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S + reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S) + reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S) + reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S) def simplifications_for_same_elements( i1: Intersection[P, Q | P], @@ -216,7 +216,7 @@ def simplifications_for_same_elements( # = P & Q | P & R | Q | Q & R # = Q | P & R # (again, because Q is a supertype of P & Q and of Q & R) - reveal_type(i3) # revealed: Q | P & R + reveal_type(i3) # revealed: Q | (P & R) # (P | Q) & (P | Q) # = P & P | P & Q | Q & P | Q & Q @@ -231,7 +231,7 @@ Distribution also applies to a negation operation. This is a manifestation of on [De Morgan's laws], namely `~(P | Q) = ~P & ~Q`: ```py -from knot_extensions import Not +from ty_extensions import Not from typing import Literal class P: ... @@ -251,7 +251,7 @@ def example_literals(i: Not[Literal[1, 2]]) -> None: The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -272,7 +272,7 @@ def _( of the [complement laws] of set theory. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing_extensions import Never def _( @@ -290,7 +290,7 @@ in intersections, and can be eagerly simplified out. `object & P` is equivalent `object & ~P` is equivalent to `~P` for any type `P`. ```py -from knot_extensions import Intersection, Not, is_equivalent_to, static_assert +from ty_extensions import Intersection, Not, is_equivalent_to, static_assert class P: ... @@ -304,11 +304,14 @@ Continuing with more [complement laws], if we see both `P` and `~P` in an inters simplify to `Never`, even in the presence of other types: ```py -from knot_extensions import Intersection, Not -from typing import Any +from ty_extensions import Intersection, Not +from typing import Any, Generic, TypeVar + +T_co = TypeVar("T_co", covariant=True) class P: ... class Q: ... +class R(Generic[T_co]): ... def _( i1: Intersection[P, Not[P]], @@ -317,6 +320,8 @@ def _( i4: Intersection[Not[P], Q, P], i5: Intersection[P, Any, Not[P]], i6: Intersection[Not[P], Any, P], + i7: Intersection[R[P], Not[R[P]]], + i8: Intersection[R[P], Not[R[Q]]], ) -> None: reveal_type(i1) # revealed: Never reveal_type(i2) # revealed: Never @@ -324,6 +329,8 @@ def _( reveal_type(i4) # revealed: Never reveal_type(i5) # revealed: Never reveal_type(i6) # revealed: Never + reveal_type(i7) # revealed: Never + reveal_type(i8) # revealed: R[P] & ~R[Q] ``` ### Union of a type and its negation @@ -331,21 +338,29 @@ def _( Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not +from typing import Generic, TypeVar + +T_co = TypeVar("T_co", covariant=True) class P: ... class Q: ... +class R(Generic[T_co]): ... def _( i1: P | Not[P], i2: Not[P] | P, i3: P | Q | Not[P], i4: Not[P] | Q | P, + i5: R[P] | Not[R[P]], + i6: R[P] | Not[R[Q]], ) -> None: reveal_type(i1) # revealed: object reveal_type(i2) # revealed: object reveal_type(i3) # revealed: object reveal_type(i4) # revealed: object + reveal_type(i5) # revealed: object + reveal_type(i6) # revealed: R[P] | ~R[Q] ``` ### Negation is an involution @@ -353,7 +368,7 @@ def _( The final of the [complement laws] states that negating twice is equivalent to not negating at all: ```py -from knot_extensions import Not +from ty_extensions import Not class P: ... @@ -380,7 +395,7 @@ If we intersect with `Never`, we can simplify the whole intersection to `Never`, dynamic types involved: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing_extensions import Never, Any class P: ... @@ -405,7 +420,7 @@ def _( If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Literal, Any class P: ... @@ -443,7 +458,7 @@ If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a subtype: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Literal def _( @@ -476,7 +491,7 @@ Subtypes are contained within their supertypes, so we can simplify intersections superfluous supertypes: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class A: ... @@ -535,7 +550,7 @@ def _( For negative contributions, this property is reversed. Here we can remove superfluous _subtypes_: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class A: ... @@ -594,7 +609,7 @@ def _( If there are multiple negative subtypes, all of them can be removed: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class A: ... class B1(A): ... @@ -622,7 +637,7 @@ When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we ca intersection to `Never`: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class A: ... @@ -662,7 +677,7 @@ Nonetheless, intersections of `AlwaysFalsy` or `AlwaysTruthy` with `bool` _can_ to the fact that `bool` is a `@final` class at runtime that cannot be subclassed. ```py -from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy +from ty_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy from typing_extensions import Literal class P: ... @@ -702,6 +717,18 @@ def never( reveal_type(d) # revealed: Never ``` +Regression tests for complex nested simplifications: + +```py +from typing_extensions import Any, assert_type + +def _(x: Intersection[bool, Not[Intersection[Any, Not[AlwaysTruthy], Not[AlwaysFalsy]]]]): + assert_type(x, bool) + +def _(x: Intersection[bool, Any] | Literal[True] | Literal[False]): + assert_type(x, bool) +``` + ## Simplification of `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` Similarly, intersections between `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` can be @@ -709,7 +736,7 @@ simplified, due to the fact that a `LiteralString` inhabitant is known to have ` exactly `str` (and not a subclass of `str`): ```py -from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy, Unknown +from ty_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy, Unknown from typing_extensions import LiteralString def f( @@ -742,7 +769,7 @@ This slightly strange-looking test is a regression test for a mistake that was n . ```py -from knot_extensions import AlwaysFalsy, Intersection, Unknown +from ty_extensions import AlwaysFalsy, Intersection, Unknown from typing_extensions import Literal def _(x: Intersection[str, Unknown, AlwaysFalsy, Literal[""]]): @@ -758,7 +785,7 @@ is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We simplify `~Any` to `Any` in intersections. The same applies to `Unknown`. ```py -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown from typing_extensions import Any, Never class P: ... @@ -788,7 +815,7 @@ The intersection of an unknown set of runtime values with (another) unknown set still an unknown set of runtime values: ```py -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown from typing_extensions import Any class P: ... @@ -823,7 +850,7 @@ of another unknown set of values is not necessarily empty, so we keep the positi ```py from typing import Any -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown def any( i1: Intersection[Any, Not[Any]], @@ -842,11 +869,11 @@ def unknown( ### Mixed dynamic types -We currently do not simplify mixed dynamic types, but might consider doing so in the future: +Gradually-equivalent types can be simplified out of intersections: ```py from typing import Any -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown def mixed( i1: Intersection[Any, Unknown], @@ -854,22 +881,22 @@ def mixed( i3: Intersection[Not[Any], Unknown], i4: Intersection[Not[Any], Not[Unknown]], ) -> None: - reveal_type(i1) # revealed: Any & Unknown - reveal_type(i2) # revealed: Any & Unknown - reveal_type(i3) # revealed: Any & Unknown - reveal_type(i4) # revealed: Any & Unknown + reveal_type(i1) # revealed: Any + reveal_type(i2) # revealed: Any + reveal_type(i3) # revealed: Any + reveal_type(i4) # revealed: Any ``` ## Invalid ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not -# error: [invalid-type-form] "`knot_extensions.Intersection` requires at least one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.Intersection` requires at least one argument when used in a type expression" def f(x: Intersection) -> None: reveal_type(x) # revealed: Unknown -# error: [invalid-type-form] "`knot_extensions.Not` requires exactly one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.Not` requires exactly one argument when used in a type expression" def f(x: Not) -> None: reveal_type(x) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md new file mode 100644 index 0000000000000..cc90879401628 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md @@ -0,0 +1,106 @@ +# Syntax errors + +Test cases to ensure that ty does not panic if there are syntax errors in the source code. + +The parser cannot recover from certain syntax errors completely which is why the number of syntax +errors could be more than expected in the following examples. For instance, if there's a keyword +(like `for`) in the middle of another statement (like function definition), then it's more likely +that the rest of the tokens are going to be part of the `for` statement and not the function +definition. But, it's not necessary that the remaining tokens are valid in the context of a `for` +statement. + +## Keyword as identifiers + +When keywords are used as identifiers, the parser recovers from this syntax error by emitting an +error and including the text value of the keyword to create the `Identifier` node. + +### Name expression + +#### Assignment + +```py +# error: [invalid-syntax] +pass = 1 +``` + +#### Type alias + +```py +# error: [invalid-syntax] +# error: [invalid-syntax] +type pass = 1 +``` + +#### Function definition + +```py +# error: [invalid-syntax] +# error: [invalid-syntax] +# error: [invalid-syntax] +# error: [invalid-syntax] +# error: [invalid-syntax] +def True(for): + # error: [invalid-syntax] + # error: [invalid-syntax] + pass +``` + +#### For + +```py +# error: [invalid-syntax] +# error: [invalid-syntax] +# error: [unresolved-reference] "Name `pass` used when not defined" +for while in pass: + pass +``` + +#### While + +```py +# error: [invalid-syntax] +# error: [unresolved-reference] "Name `in` used when not defined" +while in: + pass +``` + +#### Match + +```py +# error: [invalid-syntax] +# error: [invalid-syntax] +# error: [unresolved-reference] "Name `match` used when not defined" +match while: + # error: [invalid-syntax] + # error: [invalid-syntax] + # error: [invalid-syntax] + # error: [unresolved-reference] "Name `case` used when not defined" + case in: + # error: [invalid-syntax] + # error: [invalid-syntax] + pass +``` + +### Attribute expression + +```py +# TODO: Check when support for attribute expressions is added + +# error: [invalid-syntax] +# error: [unresolved-reference] "Name `foo` used when not defined" +for x in foo.pass: + pass +``` + +## Invalid annotation + +### `typing.Callable` + +```py +from typing import Callable + +# error: [invalid-syntax] "Expected index or slice expression" +# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)" +def _(c: Callable[]): + reveal_type(c) # revealed: (...) -> Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/known_constants.md b/crates/ty_python_semantic/resources/mdtest/known_constants.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/known_constants.md rename to crates/ty_python_semantic/resources/mdtest/known_constants.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md b/crates/ty_python_semantic/resources/mdtest/literal/boolean.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md rename to crates/ty_python_semantic/resources/mdtest/literal/boolean.md diff --git a/crates/ty_python_semantic/resources/mdtest/literal/bytes.md b/crates/ty_python_semantic/resources/mdtest/literal/bytes.md new file mode 100644 index 0000000000000..391eff8ad5546 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/literal/bytes.md @@ -0,0 +1,10 @@ +# Bytes literals + +## Simple + +```py +reveal_type(b"t" b"y") # revealed: Literal[b"ty"] +reveal_type(b"hello") # revealed: Literal[b"hello"] +reveal_type(b"world" + b"!") # revealed: Literal[b"world!"] +reveal_type(b"\xff\x00") # revealed: Literal[b"\xff\x00"] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md new file mode 100644 index 0000000000000..37abd2b98bb20 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -0,0 +1,7 @@ +# Dictionaries + +## Empty dictionary + +```py +reveal_type({}) # revealed: dict[Unknown, Unknown] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md new file mode 100644 index 0000000000000..53915f27c2961 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md @@ -0,0 +1,7 @@ +# Lists + +## Empty list + +```py +reveal_type([]) # revealed: list[Unknown] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md new file mode 100644 index 0000000000000..85acd78e3e1d2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md @@ -0,0 +1,7 @@ +# Sets + +## Basic set + +```py +reveal_type({1, 2}) # revealed: set[Unknown] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/tuple.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md rename to crates/ty_python_semantic/resources/mdtest/literal/collections/tuple.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/complex.md b/crates/ty_python_semantic/resources/mdtest/literal/complex.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/complex.md rename to crates/ty_python_semantic/resources/mdtest/literal/complex.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md b/crates/ty_python_semantic/resources/mdtest/literal/ellipsis.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md rename to crates/ty_python_semantic/resources/mdtest/literal/ellipsis.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md b/crates/ty_python_semantic/resources/mdtest/literal/f_string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md rename to crates/ty_python_semantic/resources/mdtest/literal/f_string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/float.md b/crates/ty_python_semantic/resources/mdtest/literal/float.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/float.md rename to crates/ty_python_semantic/resources/mdtest/literal/float.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/integer.md b/crates/ty_python_semantic/resources/mdtest/literal/integer.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/integer.md rename to crates/ty_python_semantic/resources/mdtest/literal/integer.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/string.md b/crates/ty_python_semantic/resources/mdtest/literal/string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/string.md rename to crates/ty_python_semantic/resources/mdtest/literal/string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md rename to crates/ty_python_semantic/resources/mdtest/loops/async_for.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/loops/for.md rename to crates/ty_python_semantic/resources/mdtest/loops/for.md index 8fec8a52eed2a..85f2390aa8919 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -286,7 +286,7 @@ class Test: def __iter__(self) -> TestIter | int: return TestIter() -# error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method" +# error: [not-iterable] "Object of type `Test` may not be iterable" for x in Test(): reveal_type(x) # revealed: int ``` @@ -316,12 +316,12 @@ def _(flag: bool): else: __iter__: None = None - # error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable" + # error: [not-iterable] "Object of type `Iterable1` may not be iterable" for x in Iterable1(): # TODO... `int` might be ideal here? reveal_type(x) # revealed: int | Unknown - # error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable" + # error: [not-iterable] "Object of type `Iterable2` may not be iterable" for y in Iterable2(): # TODO... `int` might be ideal here? reveal_type(y) # revealed: int | Unknown @@ -376,7 +376,7 @@ def _(flag: bool): def __iter__(self) -> Iterator: return Iterator() - # error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method" + # error: [not-iterable] "Object of type `Iterable` may not be iterable" for x in Iterable(): reveal_type(x) # revealed: int ``` @@ -461,7 +461,7 @@ def _(flag: bool): return Iterator() __getitem__: None = None - # error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable" + # error: [not-iterable] "Object of type `Iterable` may not be iterable" for x in Iterable(): reveal_type(x) # revealed: int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md b/crates/ty_python_semantic/resources/mdtest/loops/iterators.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md rename to crates/ty_python_semantic/resources/mdtest/loops/iterators.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md rename to crates/ty_python_semantic/resources/mdtest/loops/while_loop.md index 397a06b742dac..5a5784b85daf1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md @@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" while NotBoolable(): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/mdtest_config.md b/crates/ty_python_semantic/resources/mdtest/mdtest_config.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/mdtest_config.md rename to crates/ty_python_semantic/resources/mdtest/mdtest_config.md index d028c7a84a7a7..6db92ff3210ef 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/mdtest_config.md +++ b/crates/ty_python_semantic/resources/mdtest/mdtest_config.md @@ -1,5 +1,5 @@ -This test makes sure that `red_knot_test` correctly parses the TOML configuration blocks and applies -the correct settings hierarchically. +This test makes sure that `ty_test` correctly parses the TOML configuration blocks and applies the +correct settings hierarchically. The following configuration will be attached to the *root* section (without any heading): diff --git a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md rename to crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md index e4255708fe373..e436f401fbc2b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +++ b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md @@ -22,6 +22,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example: `/typeshed/stdlib/builtins.pyi`: ```pyi +class object: ... class BuiltinClass: ... builtin_symbol: BuiltinClass @@ -80,7 +81,7 @@ new_module: 3.11- ```py from old_module import OldClass -# error: [unresolved-import] "Cannot resolve import `new_module`" +# error: [unresolved-import] "Cannot resolve imported module `new_module`" from new_module import NewClass ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md similarity index 78% rename from crates/red_knot_python_semantic/resources/mdtest/metaclass.md rename to crates/ty_python_semantic/resources/mdtest/metaclass.md index 33cd463848442..3c4762fe8fefe 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -3,19 +3,19 @@ ```py class M(type): ... -reveal_type(M.__class__) # revealed: Literal[type] +reveal_type(M.__class__) # revealed: ``` ## `object` ```py -reveal_type(object.__class__) # revealed: Literal[type] +reveal_type(object.__class__) # revealed: ``` ## `type` ```py -reveal_type(type.__class__) # revealed: Literal[type] +reveal_type(type.__class__) # revealed: ``` ## Basic @@ -24,7 +24,7 @@ reveal_type(type.__class__) # revealed: Literal[type] class M(type): ... class B(metaclass=M): ... -reveal_type(B.__class__) # revealed: Literal[M] +reveal_type(B.__class__) # revealed: ``` ## Invalid metaclass @@ -37,7 +37,7 @@ class M: ... class A(metaclass=M): ... # TODO: emit a diagnostic for the invalid metaclass -reveal_type(A.__class__) # revealed: Literal[M] +reveal_type(A.__class__) # revealed: ``` ## Linear inheritance @@ -50,7 +50,26 @@ class M(type): ... class A(metaclass=M): ... class B(A): ... -reveal_type(B.__class__) # revealed: Literal[M] +reveal_type(B.__class__) # revealed: +``` + +## Linear inheritance with PEP 695 generic class + +The same is true if the base with the metaclass is a generic class. + +```toml +[environment] +python-version = "3.13" +``` + +```py +class M(type): ... +class A[T](metaclass=M): ... +class B(A): ... +class C(A[int]): ... + +reveal_type(B.__class__) # revealed: +reveal_type(C.__class__) # revealed: ``` ## Conflict (1) @@ -98,7 +117,7 @@ class A(metaclass=M): ... class B(metaclass=M): ... class C(A, B): ... -reveal_type(C.__class__) # revealed: Literal[M] +reveal_type(C.__class__) # revealed: ``` ## Metaclass metaclass @@ -112,7 +131,7 @@ class M3(M2): ... class A(metaclass=M3): ... class B(A): ... -reveal_type(A.__class__) # revealed: Literal[M3] +reveal_type(A.__class__) # revealed: ``` ## Diamond inheritance @@ -140,14 +159,14 @@ from nonexistent_module import UnknownClass # error: [unresolved-import] class C(UnknownClass): ... # TODO: should be `type[type] & Unknown` -reveal_type(C.__class__) # revealed: Literal[type] +reveal_type(C.__class__) # revealed: class M(type): ... class A(metaclass=M): ... class B(A, UnknownClass): ... # TODO: should be `type[M] & Unknown` -reveal_type(B.__class__) # revealed: Literal[M] +reveal_type(B.__class__) # revealed: ``` ## Duplicate @@ -157,7 +176,7 @@ class M(type): ... class A(metaclass=M): ... class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`" -reveal_type(B.__class__) # revealed: Literal[M] +reveal_type(B.__class__) # revealed: ``` ## Non-class @@ -172,14 +191,14 @@ def f(*args, **kwargs) -> int: class A(metaclass=f): ... # TODO: Should be `int` -reveal_type(A) # revealed: Literal[A] +reveal_type(A) # revealed: reveal_type(A.__class__) # revealed: type[int] def _(n: int): # error: [invalid-metaclass] class B(metaclass=n): ... # TODO: Should be `Unknown` - reveal_type(B) # revealed: Literal[B] + reveal_type(B) # revealed: reveal_type(B.__class__) # revealed: type[Unknown] def _(flag: bool): @@ -188,7 +207,7 @@ def _(flag: bool): # error: [invalid-metaclass] class C(metaclass=m): ... # TODO: Should be `int | Unknown` - reveal_type(C) # revealed: Literal[C] + reveal_type(C) # revealed: reveal_type(C.__class__) # revealed: type[Unknown] class SignatureMismatch: ... @@ -197,9 +216,9 @@ class SignatureMismatch: ... class D(metaclass=SignatureMismatch): ... # TODO: Should be `Unknown` -reveal_type(D) # revealed: Literal[D] +reveal_type(D) # revealed: # TODO: Should be `type[Unknown]` -reveal_type(D.__class__) # revealed: Literal[SignatureMismatch] +reveal_type(D.__class__) # revealed: ``` ## Cyclic @@ -216,11 +235,16 @@ reveal_type(A.__class__) # revealed: type[Unknown] ## PEP 695 generic +```toml +[environment] +python-version = "3.12" +``` + ```py class M(type): ... class A[T: str](metaclass=M): ... -reveal_type(A.__class__) # revealed: Literal[M] +reveal_type(A.__class__) # revealed: ``` ## Metaclasses of metaclasses @@ -231,9 +255,9 @@ class Bar(type, metaclass=Foo): ... class Baz(type, metaclass=Bar): ... class Spam(metaclass=Baz): ... -reveal_type(Spam.__class__) # revealed: Literal[Baz] -reveal_type(Spam.__class__.__class__) # revealed: Literal[Bar] -reveal_type(Spam.__class__.__class__.__class__) # revealed: Literal[Foo] +reveal_type(Spam.__class__) # revealed: +reveal_type(Spam.__class__.__class__) # revealed: +reveal_type(Spam.__class__.__class__.__class__) # revealed: def test(x: Spam): reveal_type(x.__class__) # revealed: type[Spam] diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md new file mode 100644 index 0000000000000..1a73deb594637 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -0,0 +1,657 @@ +# Method Resolution Order tests + +Tests that assert that we can infer the correct type for a class's `__mro__` attribute. + +This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to +know the precise possible values of a class's Method Resolution Order, or we won't be able to infer +the correct type of attributes accessed from instances. + +For documentation on method resolution orders, see: + +- +- + +## No bases + +```py +class C: ... + +reveal_type(C.__mro__) # revealed: tuple[, ] +``` + +## The special case: `object` itself + +```py +reveal_type(object.__mro__) # revealed: tuple[] +``` + +## Explicit inheritance from `object` + +```py +class C(object): ... + +reveal_type(C.__mro__) # revealed: tuple[, ] +``` + +## Explicit inheritance from non-`object` single base + +```py +class A: ... +class B(A): ... + +reveal_type(B.__mro__) # revealed: tuple[, , ] +``` + +## Linearization of multiple bases + +```py +class A: ... +class B: ... +class C(A, B): ... + +reveal_type(C.__mro__) # revealed: tuple[, , , ] +``` + +## Complex diamond inheritance (1) + +This is "ex_2" from + +```py +class O: ... +class X(O): ... +class Y(O): ... +class A(X, Y): ... +class B(Y, X): ... + +reveal_type(A.__mro__) # revealed: tuple[, , , , ] +reveal_type(B.__mro__) # revealed: tuple[, , , , ] +``` + +## Complex diamond inheritance (2) + +This is "ex_5" from + +```py +class O: ... +class F(O): ... +class E(O): ... +class D(O): ... +class C(D, F): ... +class B(D, E): ... +class A(B, C): ... + +# revealed: tuple[, , , , ] +reveal_type(C.__mro__) +# revealed: tuple[, , , , ] +reveal_type(B.__mro__) +# revealed: tuple[, , , , , , , ] +reveal_type(A.__mro__) +``` + +## Complex diamond inheritance (3) + +This is "ex_6" from + +```py +class O: ... +class F(O): ... +class E(O): ... +class D(O): ... +class C(D, F): ... +class B(E, D): ... +class A(B, C): ... + +# revealed: tuple[, , , , ] +reveal_type(C.__mro__) +# revealed: tuple[, , , , ] +reveal_type(B.__mro__) +# revealed: tuple[, , , , , , , ] +reveal_type(A.__mro__) +``` + +## Complex diamond inheritance (4) + +This is "ex_9" from + +```py +class O: ... +class A(O): ... +class B(O): ... +class C(O): ... +class D(O): ... +class E(O): ... +class K1(A, B, C): ... +class K2(D, B, E): ... +class K3(D, A): ... +class Z(K1, K2, K3): ... + +# revealed: tuple[, , , , , ] +reveal_type(K1.__mro__) +# revealed: tuple[, , , , , ] +reveal_type(K2.__mro__) +# revealed: tuple[, , , , ] +reveal_type(K3.__mro__) +# revealed: tuple[, , , , , , , , , , ] +reveal_type(Z.__mro__) +``` + +## Inheritance from `Unknown` + +```py +from does_not_exist import DoesNotExist # error: [unresolved-import] + +class A(DoesNotExist): ... +class B: ... +class C: ... +class D(A, B, C): ... +class E(B, C): ... +class F(E, A): ... + +reveal_type(A.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(D.__mro__) # revealed: tuple[, , Unknown, , , ] +reveal_type(E.__mro__) # revealed: tuple[, , , ] +# revealed: tuple[, , , , , Unknown, ] +reveal_type(F.__mro__) +``` + +## Inheritance with intersections that include `Unknown` + +An intersection that includes `Unknown` or `Any` is permitted as long as the intersection is not +disjoint from `type`. + +```py +from does_not_exist import DoesNotExist # error: [unresolved-import] + +reveal_type(DoesNotExist) # revealed: Unknown + +if hasattr(DoesNotExist, "__mro__"): + reveal_type(DoesNotExist) # revealed: Unknown & + + class Foo(DoesNotExist): ... # no error! + reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + +if not isinstance(DoesNotExist, type): + reveal_type(DoesNotExist) # revealed: Unknown & ~type + + class Foo(DoesNotExist): ... # error: [unsupported-base] + reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +## Inheritance from `type[Any]` and `type[Unknown]` + +Inheritance from `type[Any]` and `type[Unknown]` is also permitted, in keeping with the gradual +guarantee: + +```py +from typing import Any +from ty_extensions import Unknown, Intersection + +def f(x: type[Any], y: Intersection[Unknown, type[Any]]): + class Foo(x): ... + reveal_type(Foo.__mro__) # revealed: tuple[, Any, ] + + class Bar(y): ... + reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] +``` + +## `__bases__` lists that cause errors at runtime + +If the class's `__bases__` cause an exception to be raised at runtime and therefore the class +creation to fail, we infer the class's `__mro__` as being `[, Unknown, object]`: + +```py +# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[, ]`" +class Foo(object, int): ... + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + +class Bar(Foo): ... + +reveal_type(Bar.__mro__) # revealed: tuple[, , Unknown, ] + +# This is the `TypeError` at the bottom of "ex_2" +# in the examples at +class O: ... +class X(O): ... +class Y(O): ... +class A(X, Y): ... +class B(Y, X): ... + +reveal_type(A.__mro__) # revealed: tuple[, , , , ] +reveal_type(B.__mro__) # revealed: tuple[, , , , ] + +# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[, ]`" +class Z(A, B): ... + +reveal_type(Z.__mro__) # revealed: tuple[, Unknown, ] + +class AA(Z): ... + +reveal_type(AA.__mro__) # revealed: tuple[, , Unknown, ] +``` + +## `__bases__` includes a `Union` + + + +We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we +find a union type in a class's bases, we infer the class's `__mro__` as being +`[, Unknown, object]`, the same as for MROs that cause errors at runtime. + +```py +from typing_extensions import reveal_type + +def returns_bool() -> bool: + return True + +class A: ... +class B: ... + +if returns_bool(): + x = A +else: + x = B + +reveal_type(x) # revealed: | + +# error: 11 [unsupported-base] "Unsupported class base with type ` | `" +class Foo(x): ... + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +## `__bases__` is a union of a dynamic type and valid bases + +If a dynamic type such as `Any` or `Unknown` is one of the elements in the union, and all other +types *would be* valid class bases, we do not emit an `invalid-base` or `unsupported-base` +diagnostic, and we use the dynamic type as a base to prevent further downstream errors. + +```py +from typing import Any + +def _(flag: bool, any: Any): + if flag: + Base = any + else: + class Base: ... + + class Foo(Base): ... + reveal_type(Foo.__mro__) # revealed: tuple[, Any, ] +``` + +## `__bases__` includes multiple `Union`s + +```py +def returns_bool() -> bool: + return True + +class A: ... +class B: ... +class C: ... +class D: ... + +if returns_bool(): + x = A +else: + x = B + +if returns_bool(): + y = C +else: + y = D + +reveal_type(x) # revealed: | +reveal_type(y) # revealed: | + +# error: 11 [unsupported-base] "Unsupported class base with type ` | `" +# error: 14 [unsupported-base] "Unsupported class base with type ` | `" +class Foo(x, y): ... + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +## `__bases__` lists that cause errors... now with `Union`s + +```py +def returns_bool() -> bool: + return True + +class O: ... +class X(O): ... +class Y(O): ... + +if returns_bool(): + foo = Y +else: + foo = object + +# error: 21 [unsupported-base] "Unsupported class base with type ` | `" +class PossibleError(foo, X): ... + +reveal_type(PossibleError.__mro__) # revealed: tuple[, Unknown, ] + +class A(X, Y): ... + +reveal_type(A.__mro__) # revealed: tuple[, , , , ] + +if returns_bool(): + class B(X, Y): ... + +else: + class B(Y, X): ... + +# revealed: tuple[, , , , ] | tuple[, , , , ] +reveal_type(B.__mro__) + +# error: 12 [unsupported-base] "Unsupported class base with type ` | `" +class Z(A, B): ... + +reveal_type(Z.__mro__) # revealed: tuple[, Unknown, ] +``` + +## `__bases__` lists that include objects that are not instances of `type` + + + +```py +class Foo(2): ... # error: [invalid-base] +``` + +A base that is not an instance of `type` but does have an `__mro_entries__` method will not raise an +exception at runtime, so we issue `unsupported-base` rather than `invalid-base`: + +```py +class Foo: + def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: + return () + +class Bar(Foo()): ... # error: [unsupported-base] +``` + +But for objects that have badly defined `__mro_entries__`, `invalid-base` is emitted rather than +`unsupported-base`: + +```py +class Bad1: + def __mro_entries__(self, bases, extra_arg): + return () + +class Bad2: + def __mro_entries__(self, bases) -> int: + return 42 + +class BadSub1(Bad1()): ... # error: [invalid-base] +class BadSub2(Bad2()): ... # error: [invalid-base] +``` + +## `__bases__` lists with duplicate bases + + + +```py +from typing_extensions import reveal_type + +class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + +class Spam: ... +class Eggs: ... +class Bar: ... +class Baz: ... + +# fmt: off + +# error: [duplicate-base] "Duplicate base class `Spam`" +# error: [duplicate-base] "Duplicate base class `Eggs`" +class Ham( + Spam, + Eggs, + Bar, + Baz, + Spam, + Eggs, +): ... + +# fmt: on + +reveal_type(Ham.__mro__) # revealed: tuple[, Unknown, ] + +class Mushrooms: ... +class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] + +reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] + +# fmt: off + +# error: [duplicate-base] "Duplicate base class `Eggs`" +class VeryEggyOmelette( + Eggs, + Ham, + Spam, + Eggs, + Mushrooms, + Bar, + Eggs, + Baz, + Eggs, +): ... + +# fmt: off +``` + +A `type: ignore` comment can suppress `duplicate-bases` errors if it is on the first or last line of +the class "header": + +```py +# fmt: off + +class A: ... + +class B( # type: ignore[duplicate-base] + A, + A, +): ... + +class C( + A, + A +): # type: ignore[duplicate-base] + x: int + +# fmt: on +``` + +But it will not suppress the error if it occurs in the class body, or on the duplicate base itself. +The justification for this is that it is the class definition as a whole that will raise an +exception at runtime, not a sub-expression in the class's bases list. + +```py +# fmt: off + +# error: [duplicate-base] +class D( + A, + # error: [unused-ignore-comment] + A, # type: ignore[duplicate-base] +): ... + +# error: [duplicate-base] +class E( + A, + A +): + # error: [unused-ignore-comment] + x: int # type: ignore[duplicate-base] + +# fmt: on +``` + +## `__bases__` lists with duplicate `Unknown` bases + +We do not emit errors on classes where multiple bases are inferred as `Unknown`, `Todo` or `Any`. +Usually having duplicate bases in a bases list like this would cause us to emit a diagnostic; +however, for gradual types this would break the +[gradual guarantee](https://typing.python.org/en/latest/spec/concepts.html#the-gradual-guarantee): +the dynamic base can usually be materialised to a type that would lead to a resolvable MRO. + +```py +from unresolvable_module import UnknownBase1, UnknownBase2 # error: [unresolved-import] + +reveal_type(UnknownBase1) # revealed: Unknown +reveal_type(UnknownBase2) # revealed: Unknown + +# no error here -- we respect the gradual guarantee: +class Foo(UnknownBase1, UnknownBase2): ... + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +However, if there are duplicate class elements, we do emit an error, even if there are also multiple +dynamic members. The following class definition will definitely fail, no matter what the dynamic +bases materialize to: + +```py +# error: [duplicate-base] "Duplicate base class `Foo`" +class Bar(UnknownBase1, Foo, UnknownBase2, Foo): ... + +reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] +``` + +## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes + +```py +from does_not_exist import unknown_object # error: [unresolved-import] + +reveal_type(unknown_object) # revealed: Unknown +reveal_type(unknown_object.__mro__) # revealed: Unknown +``` + +## MROs of classes that use multiple inheritance with generic aliases and subscripted `Generic` + +```py +from typing import Generic, TypeVar, Iterator + +T = TypeVar("T") + +class peekable(Generic[T], Iterator[T]): ... + +# revealed: tuple[, , , typing.Protocol, typing.Generic, ] +reveal_type(peekable.__mro__) + +class peekable2(Iterator[T], Generic[T]): ... + +# revealed: tuple[, , , typing.Protocol, typing.Generic, ] +reveal_type(peekable2.__mro__) + +class Base: ... +class Intermediate(Base, Generic[T]): ... +class Sub(Intermediate[T], Base): ... + +# revealed: tuple[, , , typing.Generic, ] +reveal_type(Sub.__mro__) +``` + +## Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases + + + +```py +from typing_extensions import Protocol, TypeVar, Generic + +T = TypeVar("T") + +class Foo(Protocol): ... +class Bar(Protocol[T]): ... +class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] +``` + +## Classes that inherit from themselves + +These are invalid, but we need to be able to handle them gracefully without panicking. + +```pyi +class Foo(Foo): ... # error: [cyclic-class-definition] + +reveal_type(Foo) # revealed: +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + +class Bar: ... +class Baz: ... +class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition] + +reveal_type(Boz) # revealed: +reveal_type(Boz.__mro__) # revealed: tuple[, Unknown, ] +``` + +## Classes with indirect cycles in their MROs + +These are similarly unlikely, but we still shouldn't crash: + +```pyi +class Foo(Bar): ... # error: [cyclic-class-definition] +class Bar(Baz): ... # error: [cyclic-class-definition] +class Baz(Foo): ... # error: [cyclic-class-definition] + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(Baz.__mro__) # revealed: tuple[, Unknown, ] +``` + +## Classes with cycles in their MROs, and multiple inheritance + +```pyi +class Spam: ... +class Foo(Bar): ... # error: [cyclic-class-definition] +class Bar(Baz): ... # error: [cyclic-class-definition] +class Baz(Foo, Spam): ... # error: [cyclic-class-definition] + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(Bar.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(Baz.__mro__) # revealed: tuple[, Unknown, ] +``` + +## Classes with cycles in their MRO, and a sub-graph + +```pyi +class FooCycle(BarCycle): ... # error: [cyclic-class-definition] +class Foo: ... +class BarCycle(FooCycle): ... # error: [cyclic-class-definition] +class Bar(Foo): ... + +# Avoid emitting the errors for these. The classes have cyclic superclasses, +# but are not themselves cyclic... +class Baz(Bar, BarCycle): ... +class Spam(Baz): ... + +reveal_type(FooCycle.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(BarCycle.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(Baz.__mro__) # revealed: tuple[, Unknown, ] +reveal_type(Spam.__mro__) # revealed: tuple[, Unknown, ] +``` + +## Other classes with possible cycles + +```toml +[environment] +python-version = "3.13" +``` + +```pyi +class C(C.a): ... +reveal_type(C.__class__) # revealed: +reveal_type(C.__mro__) # revealed: tuple[, Unknown, ] + +class D(D.a): + a: D +reveal_type(D.__class__) # revealed: +reveal_type(D.__mro__) # revealed: tuple[, Unknown, ] + +class E[T](E.a): ... +reveal_type(E.__class__) # revealed: +reveal_type(E.__mro__) # revealed: tuple[, Unknown, typing.Generic, ] + +class F[T](F(), F): ... # error: [cyclic-class-definition] +reveal_type(F.__class__) # revealed: type[Unknown] +reveal_type(F.__mro__) # revealed: tuple[, Unknown, ] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md new file mode 100644 index 0000000000000..dfc81a73f0e77 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -0,0 +1,193 @@ +# `NamedTuple` + +`NamedTuple` is a type-safe way to define named tuples — a tuple where each field can be accessed by +name, and not just by its numeric position within the tuple: + +## `typing.NamedTuple` + +### Basics + +```py +from typing import NamedTuple + +class Person(NamedTuple): + id: int + name: str + age: int | None = None + +alice = Person(1, "Alice", 42) +alice = Person(id=1, name="Alice", age=42) +bob = Person(2, "Bob") +bob = Person(id=2, name="Bob") + +reveal_type(alice.id) # revealed: int +reveal_type(alice.name) # revealed: str +reveal_type(alice.age) # revealed: int | None + +# TODO: These should reveal the types of the fields +reveal_type(alice[0]) # revealed: Unknown +reveal_type(alice[1]) # revealed: Unknown +reveal_type(alice[2]) # revealed: Unknown + +# error: [missing-argument] +Person(3) + +# error: [too-many-positional-arguments] +Person(3, "Eve", 99, "extra") + +# error: [invalid-argument-type] +Person(id="3", name="Eve") + +# TODO: over-writing NamedTuple fields should be an error +alice.id = 42 +bob.age = None +``` + +Alternative functional syntax: + +```py +Person2 = NamedTuple("Person", [("id", int), ("name", str)]) +alice2 = Person2(1, "Alice") + +# TODO: should be an error +Person2(1) + +reveal_type(alice2.id) # revealed: @Todo(functional `NamedTuple` syntax) +reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax) +``` + +### Definition + +TODO: Fields without default values should come before fields with. + +```py +from typing import NamedTuple + +class Location(NamedTuple): + altitude: float = 0.0 + latitude: float # this should be an error + longitude: float +``` + +### Multiple Inheritance + +Multiple inheritance is not supported for `NamedTuple` classes: + +```py +from typing import NamedTuple + +# This should ideally emit a diagnostic +class C(NamedTuple, object): + id: int + name: str +``` + +### Inheriting from a `NamedTuple` + +Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the +synthesized `__new__` signature: + +```py +from typing import NamedTuple + +class User(NamedTuple): + id: int + name: str + +class SuperUser(User): + level: int + +# This is fine: +alice = SuperUser(1, "Alice") +reveal_type(alice.level) # revealed: int + +# This is an error because `level` is not part of the signature: +# error: [too-many-positional-arguments] +alice = SuperUser(1, "Alice", 3) +``` + +TODO: If any fields added by the subclass conflict with those in the base class, that should be +flagged. + +```py +from typing import NamedTuple + +class User(NamedTuple): + id: int + name: str + +class SuperUser(User): + id: int # this should be an error +``` + +### Generic named tuples + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NamedTuple + +class Property[T](NamedTuple): + name: str + value: T + +reveal_type(Property("height", 3.4)) # revealed: Property[float] +``` + +## Attributes on `NamedTuple` + +The following attributes are available on `NamedTuple` classes / instances: + +```py +from typing import NamedTuple + +class Person(NamedTuple): + name: str + age: int | None = None + +reveal_type(Person._field_defaults) # revealed: dict[str, Any] +reveal_type(Person._fields) # revealed: tuple[str, ...] +reveal_type(Person._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Self +reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any] +reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self + +# TODO: should be `Person` once we support `Self` +reveal_type(Person._make(("Alice", 42))) # revealed: Unknown + +person = Person("Alice", 42) + +reveal_type(person._asdict()) # revealed: dict[str, Any] +# TODO: should be `Person` once we support `Self` +reveal_type(person._replace(name="Bob")) # revealed: Unknown +``` + +## `collections.namedtuple` + +```py +from collections import namedtuple + +Person = namedtuple("Person", ["id", "name", "age"], defaults=[None]) + +alice = Person(1, "Alice", 42) +bob = Person(2, "Bob") +``` + +## NamedTuple with custom `__getattr__` + +This is a regression test for . Make sure that the +`__getattr__` method does not interfere with the `NamedTuple` behavior. + +```py +from typing import NamedTuple + +class Vec2(NamedTuple): + x: float = 0.0 + y: float = 0.0 + + def __getattr__(self, attrs: str): ... + +Vec2(0.0, 0.0) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assert.md b/crates/ty_python_semantic/resources/mdtest/narrow/assert.md new file mode 100644 index 0000000000000..0fab83880ee9f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assert.md @@ -0,0 +1,114 @@ +# Narrowing with assert statements + +## `assert` a value `is None` or `is not None` + +```py +def _(x: str | None, y: str | None): + assert x is not None + reveal_type(x) # revealed: str + assert y is None + reveal_type(y) # revealed: None +``` + +## `assert` a value is truthy or falsy + +```py +def _(x: bool, y: bool): + assert x + reveal_type(x) # revealed: Literal[True] + assert not y + reveal_type(y) # revealed: Literal[False] +``` + +## `assert` with `is` and `==` for literals + +```py +from typing import Literal + +def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): + assert x is 2 + reveal_type(x) # revealed: Literal[2] + assert y == 2 + reveal_type(y) # revealed: Literal[2] +``` + +## `assert` with `isinstance` + +```py +def _(x: int | str): + assert isinstance(x, int) + reveal_type(x) # revealed: int +``` + +## `assert` a value `in` a tuple + +```py +from typing import Literal + +def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): + assert x in (1, 2) + reveal_type(x) # revealed: Literal[1, 2] + assert y not in (1, 2) + reveal_type(y) # revealed: Literal[3] +``` + +## Assertions with messages + +```py +def _(x: int | None, y: int | None): + reveal_type(x) # revealed: int | None + assert x is None, reveal_type(x) # revealed: int + reveal_type(x) # revealed: None + + reveal_type(y) # revealed: int | None + assert isinstance(y, int), reveal_type(y) # revealed: None + reveal_type(y) # revealed: int +``` + +## Assertions with definitions inside the message + +```py +def one(x: int | None): + assert x is None, (y := x * 42) * reveal_type(y) # revealed: int + + # error: [unresolved-reference] + reveal_type(y) # revealed: Unknown + +def two(x: int | None, y: int | None): + assert x is None, (y := 42) * reveal_type(y) # revealed: Literal[42] + reveal_type(y) # revealed: int | None +``` + +## Assertions with `test` predicates that are statically known to always be `True` + +```py +assert True, (x := 1) + +# error: [unresolved-reference] +reveal_type(x) # revealed: Unknown + +assert False, (y := 1) + +# The `assert` statement is terminal if `test` resolves to `False`, +# so even though we know the `msg` branch will have been taken here +# (we know what the truthiness of `False is!), we also know that the +# `y` definition is not visible from this point in control flow +# (because this point in control flow is unreachable). +# We make sure that this does not emit an `[unresolved-reference]` +# diagnostic by adding a reachability constraint, +# but the inferred type is `Unknown`. +# +reveal_type(y) # revealed: Unknown +``` + +## Assertions with messages that reference definitions from the `test` + +```py +def one(x: int | None): + assert (y := x), reveal_type(y) # revealed: (int & ~AlwaysTruthy) | None + reveal_type(y) # revealed: int & ~AlwaysFalsy + +def two(x: int | None): + assert isinstance((y := x), int), reveal_type(y) # revealed: None + reveal_type(y) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md new file mode 100644 index 0000000000000..d5a59ad275cef --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md @@ -0,0 +1,324 @@ +# Narrowing by assignment + +## Attribute + +### Basic + +```py +class A: + x: int | None = None + y = None + + def __init__(self): + self.z = None + +a = A() +a.x = 0 +a.y = 0 +a.z = 0 + +reveal_type(a.x) # revealed: Literal[0] +reveal_type(a.y) # revealed: Literal[0] +reveal_type(a.z) # revealed: Literal[0] + +# Make sure that we infer the narrowed type for eager +# scopes (class, comprehension) and the non-narrowed +# public type for lazy scopes (function) +class _: + reveal_type(a.x) # revealed: Literal[0] + reveal_type(a.y) # revealed: Literal[0] + reveal_type(a.z) # revealed: Literal[0] + +[reveal_type(a.x) for _ in range(1)] # revealed: Literal[0] +[reveal_type(a.y) for _ in range(1)] # revealed: Literal[0] +[reveal_type(a.z) for _ in range(1)] # revealed: Literal[0] + +def _(): + reveal_type(a.x) # revealed: Unknown | int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +if False: + a = A() +reveal_type(a.x) # revealed: Literal[0] +reveal_type(a.y) # revealed: Literal[0] +reveal_type(a.z) # revealed: Literal[0] + +if True: + a = A() +reveal_type(a.x) # revealed: int | None +reveal_type(a.y) # revealed: Unknown | None +reveal_type(a.z) # revealed: Unknown | None + +a.x = 0 +a.y = 0 +a.z = 0 +reveal_type(a.x) # revealed: Literal[0] +reveal_type(a.y) # revealed: Literal[0] +reveal_type(a.z) # revealed: Literal[0] + +class _: + a = A() + reveal_type(a.x) # revealed: int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +def cond() -> bool: + return True + +class _: + if False: + a = A() + reveal_type(a.x) # revealed: Literal[0] + reveal_type(a.y) # revealed: Literal[0] + reveal_type(a.z) # revealed: Literal[0] + + if cond(): + a = A() + reveal_type(a.x) # revealed: int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +class _: + a = A() + + class Inner: + reveal_type(a.x) # revealed: int | None + reveal_type(a.y) # revealed: Unknown | None + reveal_type(a.z) # revealed: Unknown | None + +a = A() +# error: [unresolved-attribute] +a.dynamically_added = 0 +# error: [unresolved-attribute] +reveal_type(a.dynamically_added) # revealed: Literal[0] + +# error: [unresolved-reference] +does.nt.exist = 0 +# error: [unresolved-reference] +reveal_type(does.nt.exist) # revealed: Unknown +``` + +### Narrowing chain + +```py +class D: ... + +class C: + d: D | None = None + +class B: + c1: C | None = None + c2: C | None = None + +class A: + b: B | None = None + +a = A() +a.b = B() +a.b.c1 = C() +a.b.c2 = C() +a.b.c1.d = D() +a.b.c2.d = D() +reveal_type(a.b) # revealed: B +reveal_type(a.b.c1) # revealed: C +reveal_type(a.b.c1.d) # revealed: D + +a.b.c1 = C() +reveal_type(a.b) # revealed: B +reveal_type(a.b.c1) # revealed: C +reveal_type(a.b.c1.d) # revealed: D | None +reveal_type(a.b.c2.d) # revealed: D + +a.b.c1.d = D() +a.b = B() +reveal_type(a.b) # revealed: B +reveal_type(a.b.c1) # revealed: C | None +reveal_type(a.b.c2) # revealed: C | None +# error: [possibly-unbound-attribute] +reveal_type(a.b.c1.d) # revealed: D | None +# error: [possibly-unbound-attribute] +reveal_type(a.b.c2.d) # revealed: D | None +``` + +### Do not narrow the type of a `property` by assignment + +```py +class C: + def __init__(self): + self._x: int = 0 + + @property + def x(self) -> int: + return self._x + + @x.setter + def x(self, value: int) -> None: + self._x = abs(value) + +c = C() +c.x = -1 +# Don't infer `c.x` to be `Literal[-1]` +reveal_type(c.x) # revealed: int +``` + +### Do not narrow the type of a descriptor by assignment + +```py +class Descriptor: + def __get__(self, instance: object, owner: type) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + desc: Descriptor = Descriptor() + +c = C() +c.desc = -1 +# Don't infer `c.desc` to be `Literal[-1]` +reveal_type(c.desc) # revealed: int +``` + +## Subscript + +### Specialization for builtin types + +Type narrowing based on assignment to a subscript expression is generally unsound, because arbitrary +`__getitem__`/`__setitem__` methods on a class do not necessarily guarantee that the passed-in value +for `__setitem__` is stored and can be retrieved unmodified via `__getitem__`. Therefore, we +currently only perform assignment-based narrowing on a few built-in classes (`list`, `dict`, +`bytesarray`, `TypedDict` and `collections` types) where we are confident that this kind of +narrowing can be performed soundly. This is the same approach as pyright. + +```py +from typing import TypedDict +from collections import ChainMap, defaultdict + +l: list[int | None] = [None] +l[0] = 0 +d: dict[int, int] = {1: 1} +d[0] = 0 +b: bytearray = bytearray(b"abc") +b[0] = 0 +dd: defaultdict[int, int] = defaultdict(int) +dd[0] = 0 +cm: ChainMap[int, int] = ChainMap({1: 1}, {0: 0}) +cm[0] = 0 +# TODO: should be ChainMap[int, int] +reveal_type(cm) # revealed: ChainMap[Unknown, Unknown] + +reveal_type(l[0]) # revealed: Literal[0] +reveal_type(d[0]) # revealed: Literal[0] +reveal_type(b[0]) # revealed: Literal[0] +reveal_type(dd[0]) # revealed: Literal[0] +# TODO: should be Literal[0] +reveal_type(cm[0]) # revealed: Unknown + +class C: + reveal_type(l[0]) # revealed: Literal[0] + reveal_type(d[0]) # revealed: Literal[0] + reveal_type(b[0]) # revealed: Literal[0] + reveal_type(dd[0]) # revealed: Literal[0] + # TODO: should be Literal[0] + reveal_type(cm[0]) # revealed: Unknown + +[reveal_type(l[0]) for _ in range(1)] # revealed: Literal[0] +[reveal_type(d[0]) for _ in range(1)] # revealed: Literal[0] +[reveal_type(b[0]) for _ in range(1)] # revealed: Literal[0] +[reveal_type(dd[0]) for _ in range(1)] # revealed: Literal[0] +# TODO: should be Literal[0] +[reveal_type(cm[0]) for _ in range(1)] # revealed: Unknown + +def _(): + reveal_type(l[0]) # revealed: int | None + reveal_type(d[0]) # revealed: int + reveal_type(b[0]) # revealed: int + reveal_type(dd[0]) # revealed: int + reveal_type(cm[0]) # revealed: int + +class D(TypedDict): + x: int + label: str + +td = D(x=1, label="a") +td["x"] = 0 +# TODO: should be Literal[0] +reveal_type(td["x"]) # revealed: @Todo(TypedDict) + +# error: [unresolved-reference] +does["not"]["exist"] = 0 +# error: [unresolved-reference] +reveal_type(does["not"]["exist"]) # revealed: Unknown + +non_subscriptable = 1 +# error: [non-subscriptable] +non_subscriptable[0] = 0 +# error: [non-subscriptable] +reveal_type(non_subscriptable[0]) # revealed: Unknown +``` + +### No narrowing for custom classes with arbitrary `__getitem__` / `__setitem__` + +```py +class C: + def __init__(self): + self.l: list[str] = [] + + def __getitem__(self, index: int) -> str: + return self.l[index] + + def __setitem__(self, index: int, value: str | int) -> None: + if len(self.l) == index: + self.l.append(str(value)) + else: + self.l[index] = str(value) + +c = C() +c[0] = 0 +reveal_type(c[0]) # revealed: str +``` + +## Complex target + +```py +class A: + x: list[int | None] = [] + +class B: + a: A | None = None + +b = B() +b.a = A() +b.a.x[0] = 0 + +reveal_type(b.a.x[0]) # revealed: Literal[0] + +class C: + reveal_type(b.a.x[0]) # revealed: Literal[0] + +def _(): + # error: [possibly-unbound-attribute] + reveal_type(b.a.x[0]) # revealed: Unknown | int | None + # error: [possibly-unbound-attribute] + reveal_type(b.a.x) # revealed: Unknown | list[int | None] + reveal_type(b.a) # revealed: Unknown | A | None +``` + +## Invalid assignments are not used for narrowing + +```py +class C: + x: int | None + l: list[int] + +def f(c: C, s: str): + c.x = s # error: [invalid-assignment] + reveal_type(c.x) # revealed: int | None + s = c.x # error: [invalid-assignment] + + # TODO: This assignment is invalid and should result in an error. + c.l[0] = s + reveal_type(c.l[0]) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md b/crates/ty_python_semantic/resources/mdtest/narrow/bool-call.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md rename to crates/ty_python_semantic/resources/mdtest/narrow/bool-call.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md rename to crates/ty_python_semantic/resources/mdtest/narrow/boolean.md diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md new file mode 100644 index 0000000000000..934fd3f605bbf --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md @@ -0,0 +1,223 @@ +# Narrowing for complex targets (attribute expressions, subscripts) + +We support type narrowing for attributes and subscripts. + +## Attribute narrowing + +### Basic + +```py +from ty_extensions import Unknown + +class C: + x: int | None = None + +c = C() + +reveal_type(c.x) # revealed: int | None + +if c.x is not None: + reveal_type(c.x) # revealed: int +else: + reveal_type(c.x) # revealed: None + +if c.x is not None: + c.x = None + +reveal_type(c.x) # revealed: None + +c = C() + +if c.x is None: + c.x = 1 + +reveal_type(c.x) # revealed: int + +class _: + reveal_type(c.x) # revealed: int + +c = C() + +class _: + if c.x is None: + c.x = 1 + reveal_type(c.x) # revealed: int + +# TODO: should be `int` +reveal_type(c.x) # revealed: int | None + +class D: + x = None + +def unknown() -> Unknown: + return 1 + +d = D() +reveal_type(d.x) # revealed: Unknown | None +d.x = 1 +reveal_type(d.x) # revealed: Literal[1] +d.x = unknown() +reveal_type(d.x) # revealed: Unknown +``` + +Narrowing can be "reset" by assigning to the attribute: + +```py +c = C() + +if c.x is None: + reveal_type(c.x) # revealed: None + c.x = 1 + reveal_type(c.x) # revealed: Literal[1] + c.x = None + reveal_type(c.x) # revealed: None + +reveal_type(c.x) # revealed: int | None +``` + +Narrowing can also be "reset" by assigning to the object: + +```py +c = C() + +if c.x is None: + reveal_type(c.x) # revealed: None + c = C() + reveal_type(c.x) # revealed: int | None + +reveal_type(c.x) # revealed: int | None +``` + +### Multiple predicates + +```py +class C: + value: str | None + +def foo(c: C): + if c.value and len(c.value): + reveal_type(c.value) # revealed: str & ~AlwaysFalsy + + # error: [invalid-argument-type] "Argument to function `len` is incorrect: Expected `Sized`, found `str | None`" + if len(c.value) and c.value: + reveal_type(c.value) # revealed: str & ~AlwaysFalsy + + if c.value is None or not len(c.value): + reveal_type(c.value) # revealed: str | None + else: # c.value is not None and len(c.value) + # TODO: should be # `str & ~AlwaysFalsy` + reveal_type(c.value) # revealed: str +``` + +### Generic class + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C[T]: + x: T + y: T + + def __init__(self, x: T): + self.x = x + self.y = x + +def f(a: int | None): + c = C(a) + reveal_type(c.x) # revealed: int | None + reveal_type(c.y) # revealed: int | None + if c.x is not None: + reveal_type(c.x) # revealed: int + # In this case, it may seem like we can narrow it down to `int`, + # but different values ​​may be reassigned to `x` and `y` in another place. + reveal_type(c.y) # revealed: int | None + +def g[T](c: C[T]): + reveal_type(c.x) # revealed: T + reveal_type(c.y) # revealed: T + reveal_type(c) # revealed: C[T] + + if isinstance(c.x, int): + reveal_type(c.x) # revealed: T & int + reveal_type(c.y) # revealed: T + reveal_type(c) # revealed: C[T] + if isinstance(c.x, int) and isinstance(c.y, int): + reveal_type(c.x) # revealed: T & int + reveal_type(c.y) # revealed: T & int + # TODO: Probably better if inferred as `C[T & int]` (mypy and pyright don't support this) + reveal_type(c) # revealed: C[T] +``` + +### With intermediate scopes + +```py +class C: + def __init__(self): + self.x: int | None = None + self.y: int | None = None + +c = C() +reveal_type(c.x) # revealed: int | None +if c.x is not None: + reveal_type(c.x) # revealed: int + reveal_type(c.y) # revealed: int | None + +if c.x is not None: + def _(): + reveal_type(c.x) # revealed: Unknown | int | None + +def _(): + if c.x is not None: + reveal_type(c.x) # revealed: (Unknown & ~None) | int +``` + +## Subscript narrowing + +### Number subscript + +```py +def _(t1: tuple[int | None, int | None], t2: tuple[int, int] | tuple[None, None]): + if t1[0] is not None: + reveal_type(t1[0]) # revealed: int + reveal_type(t1[1]) # revealed: int | None + + n = 0 + if t1[n] is not None: + # Non-literal subscript narrowing are currently not supported, as well as mypy, pyright + reveal_type(t1[0]) # revealed: int | None + reveal_type(t1[n]) # revealed: int | None + reveal_type(t1[1]) # revealed: int | None + + if t2[0] is not None: + reveal_type(t2[0]) # revealed: int + # TODO: should be int + reveal_type(t2[1]) # revealed: int | None +``` + +### String subscript + +```py +def _(d: dict[str, str | None]): + if d["a"] is not None: + reveal_type(d["a"]) # revealed: str + reveal_type(d["b"]) # revealed: str | None +``` + +## Combined attribute and subscript narrowing + +```py +class C: + def __init__(self): + self.x: tuple[int | None, int | None] = (None, None) + +class D: + def __init__(self): + self.c: tuple[C] | None = None + +d = D() +if d.c is not None and d.c[0].x[0] is not None: + reveal_type(d.c[0].x[0]) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md new file mode 100644 index 0000000000000..666c5e6b68281 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md @@ -0,0 +1,237 @@ +# Narrowing for conditionals with boolean expressions + +## Narrowing in `and` conditional + +```py +class A: ... +class B: ... + +def _(x: A | B): + if isinstance(x, A) and isinstance(x, B): + reveal_type(x) # revealed: A & B + else: + reveal_type(x) # revealed: (B & ~A) | (A & ~B) +``` + +## Arms might not add narrowing constraints + +```py +class A: ... +class B: ... + +def _(flag: bool, x: A | B): + if isinstance(x, A) and flag: + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: A | B + + if flag and isinstance(x, A): + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: A | B + + reveal_type(x) # revealed: A | B +``` + +## Statically known arms + +```py +class A: ... +class B: ... + +def _(x: A | B): + if isinstance(x, A) and True: + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: B & ~A + + if True and isinstance(x, A): + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: B & ~A + + if False and isinstance(x, A): + # TODO: should emit an `unreachable code` diagnostic + reveal_type(x) # revealed: Never + else: + reveal_type(x) # revealed: A | B + + if False or isinstance(x, A): + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: B & ~A + + if True or isinstance(x, A): + reveal_type(x) # revealed: A | B + else: + # TODO: should emit an `unreachable code` diagnostic + reveal_type(x) # revealed: Never + + reveal_type(x) # revealed: A | B +``` + +## The type of multiple symbols can be narrowed down + +```py +class A: ... +class B: ... + +def _(x: A | B, y: A | B): + if isinstance(x, A) and isinstance(y, B): + reveal_type(x) # revealed: A + reveal_type(y) # revealed: B + else: + # No narrowing: Only-one or both checks might have failed + reveal_type(x) # revealed: A | B + reveal_type(y) # revealed: A | B + + reveal_type(x) # revealed: A | B + reveal_type(y) # revealed: A | B +``` + +## Narrowing in `or` conditional + +```py +class A: ... +class B: ... +class C: ... + +def _(x: A | B | C): + if isinstance(x, A) or isinstance(x, B): + reveal_type(x) # revealed: A | B + else: + reveal_type(x) # revealed: C & ~A & ~B +``` + +## In `or`, all arms should add constraint in order to narrow + +```py +class A: ... +class B: ... +class C: ... + +def _(flag: bool, x: A | B | C): + if isinstance(x, A) or isinstance(x, B) or flag: + reveal_type(x) # revealed: A | B | C + else: + reveal_type(x) # revealed: C & ~A & ~B +``` + +## in `or`, all arms should narrow the same set of symbols + +```py +class A: ... +class B: ... +class C: ... + +def _(x: A | B | C, y: A | B | C): + if isinstance(x, A) or isinstance(y, A): + # The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here. + reveal_type(x) # revealed: A | B | C + # The same for `y` + reveal_type(y) # revealed: A | B | C + else: + reveal_type(x) # revealed: (B & ~A) | (C & ~A) + reveal_type(y) # revealed: (B & ~A) | (C & ~A) + + if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)): + # Here, types of `x` and `y` can be narrowd since all `or` arms constraint them. + reveal_type(x) # revealed: A | B + reveal_type(y) # revealed: A | B + else: + reveal_type(x) # revealed: A | B | C + reveal_type(y) # revealed: A | B | C +``` + +## mixing `and` and `not` + +```py +class A: ... +class B: ... +class C: ... + +def _(x: A | B | C): + if isinstance(x, B) and not isinstance(x, C): + reveal_type(x) # revealed: B & ~C + else: + # ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C + reveal_type(x) # revealed: (A & ~B) | C +``` + +## mixing `or` and `not` + +```py +class A: ... +class B: ... +class C: ... + +def _(x: A | B | C): + if isinstance(x, B) or not isinstance(x, C): + reveal_type(x) # revealed: B | (A & ~C) + else: + reveal_type(x) # revealed: C & ~B +``` + +## `or` with nested `and` + +```py +class A: ... +class B: ... +class C: ... + +def _(x: A | B | C): + if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)): + reveal_type(x) # revealed: A | (B & ~C) + else: + # ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B) + reveal_type(x) # revealed: C & ~A +``` + +## `and` with nested `or` + +```py +class A: ... +class B: ... +class C: ... + +def _(x: A | B | C): + if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)): + # A & (B | ~C) -> (A & B) | (A & ~C) + reveal_type(x) # revealed: (A & B) | (A & ~C) + else: + # ~((A & B) | (A & ~C)) -> + # ~(A & B) & ~(A & ~C) -> + # (~A | ~B) & (~A | C) -> + # [(~A | ~B) & ~A] | [(~A | ~B) & C] -> + # ~A | (~A & C) | (~B & C) -> + # ~A | (C & ~B) -> + # ~A | (C & ~B) The positive side of ~A is A | B | C -> + reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B) +``` + +## Boolean expression internal narrowing + +```py +def _(x: str | None, y: str | None): + if x is None and y is not x: + reveal_type(y) # revealed: str + + # Neither of the conditions alone is sufficient for narrowing y's type: + if x is None: + reveal_type(y) # revealed: str | None + + if y is not x: + reveal_type(y) # revealed: str | None +``` + +## Assignment expressions + +```py +def f() -> bool: + return True + +if x := f(): + reveal_type(x) # revealed: Literal[True] +else: + reveal_type(x) # revealed: Literal[False] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md new file mode 100644 index 0000000000000..96b2153b90f08 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md @@ -0,0 +1,60 @@ +# Narrowing for conditionals with elif and else + +## Positive contributions become negative in elif-else blocks + +```py +def _(x: int): + if x == 1: + # cannot narrow; could be a subclass of `int` + reveal_type(x) # revealed: int + elif x == 2: + reveal_type(x) # revealed: int & ~Literal[1] + elif x != 3: + reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] +``` + +## Positive contributions become negative in elif-else blocks, with simplification + +```py +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 + + if x == 1: + reveal_type(x) # revealed: Literal[1] + elif x == 2: + reveal_type(x) # revealed: Literal[2] + else: + reveal_type(x) # revealed: Literal[3] +``` + +## Multiple negative contributions using elif, with simplification + +```py +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 + + if x != 1: + reveal_type(x) # revealed: Literal[2, 3] + elif x != 2: + reveal_type(x) # revealed: Literal[1] + elif x == 3: + reveal_type(x) # revealed: Never + else: + reveal_type(x) # revealed: Never +``` + +## Assignment expressions + +```py +class Foo: ... +class Bar: ... + +def f() -> Foo | Bar | None: ... + +if isinstance(x := f(), Foo): + reveal_type(x) # revealed: Foo +elif isinstance(x, Bar): + reveal_type(x) # revealed: Bar & ~Foo +else: + reveal_type(x) # revealed: None +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md new file mode 100644 index 0000000000000..4ba04803c6347 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -0,0 +1,157 @@ +# Narrowing for `!=` conditionals + +## `x != None` + +```py +def _(flag: bool): + x = None if flag else 1 + + if x != None: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: None +``` + +## `!=` for other singleton types + +```py +def _(flag: bool): + x = True if flag else False + + if x != False: + reveal_type(x) # revealed: Literal[True] + else: + reveal_type(x) # revealed: Literal[False] +``` + +## `x != y` where `y` is of literal type + +```py +def _(flag: bool): + x = 1 if flag else 2 + + if x != 1: + reveal_type(x) # revealed: Literal[2] +``` + +## `x != y` where `y` is a single-valued type + +```py +def _(flag: bool): + class A: ... + class B: ... + C = A if flag else B + + if C != A: + reveal_type(C) # revealed: + else: + reveal_type(C) # revealed: +``` + +## `x != y` where `y` has multiple single-valued options + +```py +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 + y = 2 if flag2 else 3 + + if x != y: + reveal_type(x) # revealed: Literal[1, 2] + else: + reveal_type(x) # revealed: Literal[2] +``` + +## `!=` for non-single-valued types + +Only single-valued types should narrow the type: + +```py +def _(flag: bool, a: int, y: int): + x = a if flag else None + + if x != y: + reveal_type(x) # revealed: int | None +``` + +## Mix of single-valued and non-single-valued types + +```py +def _(flag1: bool, flag2: bool, a: int): + x = 1 if flag1 else 2 + y = 2 if flag2 else a + + if x != y: + reveal_type(x) # revealed: Literal[1, 2] + else: + reveal_type(x) # revealed: Literal[1, 2] +``` + +## Assignment expressions + +```py +from typing import Literal + +def f() -> Literal[1, 2, 3]: + return 1 + +if (x := f()) != 1: + reveal_type(x) # revealed: Literal[2, 3] +else: + reveal_type(x) # revealed: Literal[1] +``` + +## Union with `Any` + +```py +from typing import Any + +def _(x: Any | None, y: Any | None): + if x != 1: + reveal_type(x) # revealed: (Any & ~Literal[1]) | None + if y == 1: + reveal_type(y) # revealed: Any & ~None +``` + +## Booleans and integers + +```py +from typing import Literal + +def _(b: bool, i: Literal[1, 2]): + if b == 1: + reveal_type(b) # revealed: Literal[True] + else: + reveal_type(b) # revealed: Literal[False] + + if b == 6: + reveal_type(b) # revealed: Never + else: + reveal_type(b) # revealed: bool + + if b == 0: + reveal_type(b) # revealed: Literal[False] + else: + reveal_type(b) # revealed: Literal[True] + + if i == True: + reveal_type(i) # revealed: Literal[1] + else: + reveal_type(i) # revealed: Literal[2] +``` + +## Narrowing `LiteralString` in union + +```py +from typing_extensions import Literal, LiteralString, Any + +def _(s: LiteralString | None, t: LiteralString | Any): + if s == "foo": + reveal_type(s) # revealed: Literal["foo"] + + if s == 1: + reveal_type(s) # revealed: Never + + if t == "foo": + # TODO could be `Literal["foo"] | Any` + reveal_type(t) # revealed: LiteralString | Any +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md new file mode 100644 index 0000000000000..865eb48788c01 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -0,0 +1,94 @@ +# Narrowing for `in` conditionals + +## `in` for tuples + +```py +def _(x: int): + if x in (1, 2, 3): + reveal_type(x) # revealed: int + else: + reveal_type(x) # revealed: int +``` + +```py +def _(x: str): + if x in ("a", "b", "c"): + reveal_type(x) # revealed: str + else: + reveal_type(x) # revealed: str +``` + +```py +from typing import Literal + +def _(x: Literal[1, 2, "a", "b", False, b"abc"]): + if x in (1,): + reveal_type(x) # revealed: Literal[1] + elif x in (2, "a"): + reveal_type(x) # revealed: Literal[2, "a"] + elif x in (b"abc",): + reveal_type(x) # revealed: Literal[b"abc"] + elif x not in (3,): + reveal_type(x) # revealed: Literal["b", False] + else: + reveal_type(x) # revealed: Never +``` + +```py +def _(x: Literal["a", "b", "c", 1]): + if x in ("a", "b", "c", 2): + reveal_type(x) # revealed: Literal["a", "b", "c"] + else: + reveal_type(x) # revealed: Literal[1] +``` + +## `in` for `str` and literal strings + +```py +def _(x: str): + if x in "abc": + reveal_type(x) # revealed: str + else: + reveal_type(x) # revealed: str +``` + +```py +from typing import Literal + +def _(x: Literal["a", "b", "c", "d"]): + if x in "abc": + reveal_type(x) # revealed: Literal["a", "b", "c"] + else: + reveal_type(x) # revealed: Literal["d"] +``` + +```py +def _(x: Literal["a", "b", "c", "e"]): + if x in "abcd": + reveal_type(x) # revealed: Literal["a", "b", "c"] + else: + reveal_type(x) # revealed: Literal["e"] +``` + +```py +def _(x: Literal[1, "a", "b", "c", "d"]): + # error: [unsupported-operator] + if x in "abc": + reveal_type(x) # revealed: Literal["a", "b", "c"] + else: + reveal_type(x) # revealed: Literal[1, "d"] +``` + +## Assignment expressions + +```py +from typing import Literal + +def f() -> Literal[1, 2, 3]: + return 1 + +if (x := f()) in (1,): + reveal_type(x) # revealed: Literal[1] +else: + reveal_type(x) # revealed: Literal[2, 3] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index 8a95bfc278f81..c7d99c48b2e4f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -100,3 +100,16 @@ def _(flag: bool): else: reveal_type(x) # revealed: Literal[42] ``` + +## Assignment expressions + +```py +from typing import Literal + +def f() -> Literal[1, 2] | None: ... + +if (x := f()) is None: + reveal_type(x) # revealed: None +else: + reveal_type(x) # revealed: Literal[1, 2] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md index 980a66a68d2ee..fba62e921323b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md @@ -82,3 +82,14 @@ def _(x_flag: bool, y_flag: bool): reveal_type(x) # revealed: bool reveal_type(y) # revealed: bool ``` + +## Assignment expressions + +```py +def f() -> int | str | None: ... + +if (x := f()) is not None: + reveal_type(x) # revealed: int | str +else: + reveal_type(x) # revealed: None +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md new file mode 100644 index 0000000000000..654e158d81327 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -0,0 +1,355 @@ +# Narrowing for nested conditionals + +## Multiple negative contributions + +```py +def _(x: int): + if x != 1: + if x != 2: + if x != 3: + reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] +``` + +## Multiple negative contributions with simplification + +```py +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 + + if x != 1: + reveal_type(x) # revealed: Literal[2, 3] + if x != 2: + reveal_type(x) # revealed: Literal[3] +``` + +## elif-else blocks + +```py +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 + + if x != 1: + reveal_type(x) # revealed: Literal[2, 3] + if x == 2: + reveal_type(x) # revealed: Literal[2] + elif x == 3: + reveal_type(x) # revealed: Literal[3] + else: + reveal_type(x) # revealed: Never + + elif x != 2: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: Never +``` + +## Comprehensions + +```py +def _(xs: list[int | None], ys: list[str | bytes], list_of_optional_lists: list[list[int | None] | None]): + [reveal_type(x) for x in xs if x is not None] # revealed: int + [reveal_type(y) for y in ys if isinstance(y, str)] # revealed: str + + [_ for x in xs if x is not None if reveal_type(x) // 3 != 0] # revealed: int + + [reveal_type(x) for x in xs if x is not None if x != 0 if x != 1] # revealed: int & ~Literal[0] & ~Literal[1] + + [reveal_type((x, y)) for x in xs if x is not None for y in ys if isinstance(y, str)] # revealed: tuple[int, str] + [reveal_type((x, y)) for y in ys if isinstance(y, str) for x in xs if x is not None] # revealed: tuple[int, str] + + [reveal_type(i) for inner in list_of_optional_lists if inner is not None for i in inner if i is not None] # revealed: int +``` + +## Cross-scope narrowing + +Narrowing constraints are also valid in eager nested scopes (however, because class variables are +not visible from nested scopes, constraints on those variables are invalid). + +Currently they are assumed to be invalid in lazy nested scopes since there is a possibility that the +constraints may no longer be valid due to a "time lag". However, it may be possible to determine +that some of them are valid by performing a more detailed analysis (e.g. checking that the narrowing +target has not changed in all places where the function is called). + +### Narrowing by attribute/subscript assignments + +```py +class A: + x: str | None = None + + def update_x(self, value: str | None): + self.x = value + +a = A() +a.x = "a" + +class B: + reveal_type(a.x) # revealed: Literal["a"] + +def f(): + reveal_type(a.x) # revealed: Unknown | str | None + +[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"] + +a = A() + +class C: + reveal_type(a.x) # revealed: str | None + +def g(): + reveal_type(a.x) # revealed: Unknown | str | None + +[reveal_type(a.x) for _ in range(1)] # revealed: str | None + +a = A() +a.x = "a" +a.update_x("b") + +class D: + # TODO: should be `str | None` + reveal_type(a.x) # revealed: Literal["a"] + +def h(): + reveal_type(a.x) # revealed: Unknown | str | None + +# TODO: should be `str | None` +[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"] +``` + +### Narrowing by attribute/subscript assignments in nested scopes + +```py +class D: ... + +class C: + d: D | None = None + +class B: + c1: C | None = None + c2: C | None = None + +class A: + b: B | None = None + +a = A() +a.b = B() + +class _: + a.b.c1 = C() + + class _: + a.b.c1.d = D() + a = 1 + + class _3: + reveal_type(a) # revealed: A + reveal_type(a.b.c1.d) # revealed: D + + class _: + a = 1 + # error: [unresolved-attribute] + a.b.c1.d = D() + + class _3: + reveal_type(a) # revealed: A + # TODO: should be `D | None` + reveal_type(a.b.c1.d) # revealed: Unknown + +a.b.c1 = C() +a.b.c1.d = D() + +class _: + a.b = B() + + class _: + # error: [possibly-unbound-attribute] + reveal_type(a.b.c1.d) # revealed: D | None + reveal_type(a.b.c1) # revealed: C | None +``` + +### Narrowing constraints introduced in eager nested scopes + +```py +g: str | None = "a" + +class A: + x: str | None = None + +a = A() + +l: list[str | None] = [None] + +def f(x: str | None): + def _(): + if x is not None: + reveal_type(x) # revealed: str + + if not isinstance(x, str): + reveal_type(x) # revealed: None + + if g is not None: + reveal_type(g) # revealed: str + + if a.x is not None: + reveal_type(a.x) # revealed: (Unknown & ~None) | str + + if l[0] is not None: + reveal_type(l[0]) # revealed: str + + class C: + if x is not None: + reveal_type(x) # revealed: str + + if not isinstance(x, str): + reveal_type(x) # revealed: None + + if g is not None: + reveal_type(g) # revealed: str + + if a.x is not None: + reveal_type(a.x) # revealed: (Unknown & ~None) | str + + if l[0] is not None: + reveal_type(l[0]) # revealed: str + + [reveal_type(x) for _ in range(1) if x is not None] # revealed: str +``` + +### Narrowing constraints introduced in the outer scope + +```py +g: str | None = "a" + +class A: + x: str | None = None + +a = A() + +l: list[str | None] = [None] + +def f(x: str | None): + if x is not None: + def _(): + # If there is a possibility that `x` may be rewritten after this function definition, + # the constraint `x is not None` outside the function is no longer be applicable for narrowing. + reveal_type(x) # revealed: str | None + + class C: + reveal_type(x) # revealed: str + + [reveal_type(x) for _ in range(1)] # revealed: str + + if g is not None: + def _(): + reveal_type(g) # revealed: str | None + + class D: + reveal_type(g) # revealed: str + + [reveal_type(g) for _ in range(1)] # revealed: str + + if a.x is not None: + def _(): + reveal_type(a.x) # revealed: Unknown | str | None + + class D: + reveal_type(a.x) # revealed: (Unknown & ~None) | str + + [reveal_type(a.x) for _ in range(1)] # revealed: (Unknown & ~None) | str + + if l[0] is not None: + def _(): + reveal_type(l[0]) # revealed: str | None + + class D: + reveal_type(l[0]) # revealed: str + + [reveal_type(l[0]) for _ in range(1)] # revealed: str +``` + +### Narrowing constraints introduced in multiple scopes + +```py +from typing import Literal + +g: str | Literal[1] | None = "a" + +class A: + x: str | Literal[1] | None = None + +a = A() + +l: list[str | Literal[1] | None] = [None] + +def f(x: str | Literal[1] | None): + class C: + if x is not None: + def _(): + if x != 1: + reveal_type(x) # revealed: str | None + + class D: + if x != 1: + reveal_type(x) # revealed: str + + [reveal_type(x) for _ in range(1) if x != 1] # revealed: str + + if g is not None: + def _(): + if g != 1: + reveal_type(g) # revealed: str | None + + class D: + if g != 1: + reveal_type(g) # revealed: str + + if a.x is not None: + def _(): + if a.x != 1: + reveal_type(a.x) # revealed: (Unknown & ~Literal[1]) | str | None + + class D: + if a.x != 1: + reveal_type(a.x) # revealed: (Unknown & ~Literal[1] & ~None) | str + + if l[0] is not None: + def _(): + if l[0] != 1: + reveal_type(l[0]) # revealed: str | None + + class D: + if l[0] != 1: + reveal_type(l[0]) # revealed: str +``` + +### Narrowing constraints with bindings in class scope, and nested scopes + +```py +from typing import Literal + +g: str | Literal[1] | None = "a" + +def f(flag: bool): + class C: + (g := None) if flag else (g := None) + # `g` is always bound here, so narrowing checks don't apply to nested scopes + if g is not None: + class F: + reveal_type(g) # revealed: str | Literal[1] | None + + class C: + # this conditional binding leaves "unbound" visible, so following narrowing checks apply + None if flag else (g := None) + + if g is not None: + class F: + reveal_type(g) # revealed: str | Literal[1] + + # This class variable is not visible from the nested class scope. + g = None + + # This additional constraint is not relevant to nested scopes, since it only applies to + # a binding of `g` that they cannot see: + if g is None: + class E: + reveal_type(g) # revealed: str | Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/not.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/not.md diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md new file mode 100644 index 0000000000000..c8148f54f6787 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md @@ -0,0 +1,66 @@ +# Narrowing using `hasattr()` + +The builtin function `hasattr()` can be used to narrow nominal and structural types. This is +accomplished using an intersection with a synthesized protocol: + +```py +from typing import final +from typing_extensions import LiteralString + +class Foo: ... + +@final +class Bar: ... + +def f(x: Foo): + if hasattr(x, "spam"): + reveal_type(x) # revealed: Foo & + reveal_type(x.spam) # revealed: object + else: + reveal_type(x) # revealed: Foo & ~ + + # TODO: should error and reveal `Unknown` + reveal_type(x.spam) # revealed: @Todo(map_with_boundness: intersections with negative contributions) + + if hasattr(x, "not-an-identifier"): + reveal_type(x) # revealed: Foo + else: + reveal_type(x) # revealed: Foo + +def g(x: Bar): + if hasattr(x, "spam"): + reveal_type(x) # revealed: Never + reveal_type(x.spam) # revealed: Never + else: + reveal_type(x) # revealed: Bar + + # error: [unresolved-attribute] + reveal_type(x.spam) # revealed: Unknown + +def returns_bool() -> bool: + return False + +class Baz: + if returns_bool(): + x: int = 42 + +def h(obj: Baz): + reveal_type(obj) # revealed: Baz + # error: [possibly-unbound-attribute] + reveal_type(obj.x) # revealed: int + + if hasattr(obj, "x"): + reveal_type(obj) # revealed: Baz & + reveal_type(obj.x) # revealed: int + else: + reveal_type(obj) # revealed: Baz & ~ + + # TODO: should emit `[unresolved-attribute]` and reveal `Unknown` + reveal_type(obj.x) # revealed: @Todo(map_with_boundness: intersections with negative contributions) + +def i(x: int | LiteralString): + if hasattr(x, "capitalize"): + reveal_type(x) # revealed: (int & ) | LiteralString + else: + reveal_type(x) # revealed: int & ~ +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md new file mode 100644 index 0000000000000..17fde60063e88 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -0,0 +1,310 @@ +# Narrowing for `isinstance` checks + +Narrowing for `isinstance(object, classinfo)` expressions. + +## `classinfo` is a single type + +```py +def _(flag: bool): + x = 1 if flag else "a" + + if isinstance(x, int): + reveal_type(x) # revealed: Literal[1] + + if isinstance(x, str): + reveal_type(x) # revealed: Literal["a"] + if isinstance(x, int): + reveal_type(x) # revealed: Never + + if isinstance(x, (int, object)): + reveal_type(x) # revealed: Literal[1, "a"] +``` + +## `classinfo` is a tuple of types + +Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tuple[(int, str)])`. +The former is equivalent to `isinstance(x, int | str)`: + +```py +def _(flag: bool, flag1: bool, flag2: bool): + x = 1 if flag else "a" + + if isinstance(x, (int, str)): + reveal_type(x) # revealed: Literal[1, "a"] + else: + reveal_type(x) # revealed: Never + + if isinstance(x, (int, bytes)): + reveal_type(x) # revealed: Literal[1] + + if isinstance(x, (bytes, str)): + reveal_type(x) # revealed: Literal["a"] + + # No narrowing should occur if a larger type is also + # one of the possibilities: + if isinstance(x, (int, object)): + reveal_type(x) # revealed: Literal[1, "a"] + else: + reveal_type(x) # revealed: Never + + y = 1 if flag1 else "a" if flag2 else b"b" + if isinstance(y, (int, str)): + reveal_type(y) # revealed: Literal[1, "a"] + + if isinstance(y, (int, bytes)): + reveal_type(y) # revealed: Literal[1, b"b"] + + if isinstance(y, (str, bytes)): + reveal_type(y) # revealed: Literal["a", b"b"] +``` + +## `classinfo` is a nested tuple of types + +```py +def _(flag: bool): + x = 1 if flag else "a" + + if isinstance(x, (bool, (bytes, int))): + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: Literal["a"] +``` + +## Class types + +```py +class A: ... +class B: ... +class C: ... + +x = object() + +if isinstance(x, A): + reveal_type(x) # revealed: A + if isinstance(x, B): + reveal_type(x) # revealed: A & B + else: + reveal_type(x) # revealed: A & ~B + +if isinstance(x, (A, B)): + reveal_type(x) # revealed: A | B +elif isinstance(x, (A, C)): + reveal_type(x) # revealed: C & ~A & ~B +else: + reveal_type(x) # revealed: ~A & ~B & ~C +``` + +## No narrowing for instances of `builtins.type` + +```py +def _(flag: bool, t: type): + x = 1 if flag else "foo" + + if isinstance(x, t): + reveal_type(x) # revealed: Literal[1, "foo"] +``` + +## Do not use custom `isinstance` for narrowing + +```py +def _(flag: bool): + def isinstance(x, t): + return True + x = 1 if flag else "a" + + if isinstance(x, int): + reveal_type(x) # revealed: Literal[1, "a"] +``` + +## Do support narrowing if `isinstance` is aliased + +```py +def _(flag: bool): + isinstance_alias = isinstance + + x = 1 if flag else "a" + + if isinstance_alias(x, int): + reveal_type(x) # revealed: Literal[1] +``` + +## Do support narrowing if `isinstance` is imported + +```py +from builtins import isinstance as imported_isinstance + +def _(flag: bool): + x = 1 if flag else "a" + + if imported_isinstance(x, int): + reveal_type(x) # revealed: Literal[1] +``` + +## Do not narrow if second argument is not a type + +```py +def _(flag: bool): + x = 1 if flag else "a" + + # TODO: this should cause us to emit a diagnostic during + # type checking + if isinstance(x, "a"): + reveal_type(x) # revealed: Literal[1, "a"] + + # TODO: this should cause us to emit a diagnostic during + # type checking + if isinstance(x, "int"): + reveal_type(x) # revealed: Literal[1, "a"] +``` + +## Do not narrow if there are keyword arguments + +```py +def _(flag: bool): + x = 1 if flag else "a" + + # error: [unknown-argument] + if isinstance(x, int, foo="bar"): + reveal_type(x) # revealed: Literal[1, "a"] +``` + +## `type[]` types are narrowed as well as class-literal types + +```py +def _(x: object, y: type[int]): + if isinstance(x, y): + reveal_type(x) # revealed: int +``` + +## Adding a disjoint element to an existing intersection + +We used to incorrectly infer `Literal` booleans for some of these. + +```py +from ty_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy + +class P: ... + +def f( + a: Intersection[P, AlwaysTruthy], + b: Intersection[P, AlwaysFalsy], + c: Intersection[P, Not[AlwaysTruthy]], + d: Intersection[P, Not[AlwaysFalsy]], +): + if isinstance(a, bool): + reveal_type(a) # revealed: Never + else: + reveal_type(a) # revealed: P & AlwaysTruthy + + if isinstance(b, bool): + reveal_type(b) # revealed: Never + else: + reveal_type(b) # revealed: P & AlwaysFalsy + + if isinstance(c, bool): + reveal_type(c) # revealed: Never + else: + reveal_type(c) # revealed: P & ~AlwaysTruthy + + if isinstance(d, bool): + reveal_type(d) # revealed: Never + else: + reveal_type(d) # revealed: P & ~AlwaysFalsy +``` + +## Narrowing if an object of type `Any` or `Unknown` is used as the second argument + +In order to preserve the gradual guarantee, we intersect with the type of the second argument if the +type of the second argument is a dynamic type: + +```py +from typing import Any +from something_unresolvable import SomethingUnknown # error: [unresolved-import] + +class Foo: ... + +def f(a: Foo, b: Any): + if isinstance(a, SomethingUnknown): + reveal_type(a) # revealed: Foo & Unknown + + if isinstance(a, b): + reveal_type(a) # revealed: Foo & Any +``` + +## Narrowing if an object with an intersection/union/TypeVar type is used as the second argument + +If an intersection with only positive members is used as the second argument, and all positive +members of the intersection are valid arguments for the second argument to `isinstance()`, we +intersect with each positive member of the intersection: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Intersection + +class Foo: ... + +class Bar: + attribute: int + +class Baz: + attribute: str + +def f(x: Foo, y: Intersection[type[Bar], type[Baz]], z: type[Any]): + if isinstance(x, y): + reveal_type(x) # revealed: Foo & Bar & Baz + + if isinstance(x, z): + reveal_type(x) # revealed: Foo & Any +``` + +The same if a union type is used: + +```py +def g(x: Foo, y: type[Bar | Baz]): + if isinstance(x, y): + reveal_type(x) # revealed: (Foo & Bar) | (Foo & Baz) +``` + +And even if a `TypeVar` is used, providing it has valid upper bounds/constraints: + +```py +from typing import TypeVar + +T = TypeVar("T", bound=type[Bar]) + +def h_old_syntax(x: Foo, y: T) -> T: + if isinstance(x, y): + reveal_type(x) # revealed: Foo & Bar + reveal_type(x.attribute) # revealed: int + + return y + +def h[U: type[Bar | Baz]](x: Foo, y: U) -> U: + if isinstance(x, y): + reveal_type(x) # revealed: (Foo & Bar) | (Foo & Baz) + reveal_type(x.attribute) # revealed: int | str + + return y +``` + +Or even a tuple of tuple of typevars that have intersection bounds... + +```py +from ty_extensions import Intersection + +class Spam: ... +class Eggs: ... +class Ham: ... +class Mushrooms: ... + +def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](x: Foo, y: T, z: U) -> tuple[T, U]: + if isinstance(x, (y, (z, Mushrooms))): + reveal_type(x) # revealed: (Foo & Bar & Baz) | (Foo & Bar & Spam) | (Foo & Eggs) | (Foo & Ham) | (Foo & Mushrooms) + + return (y, z) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md new file mode 100644 index 0000000000000..ce77126d32156 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -0,0 +1,359 @@ +# Narrowing for `issubclass` checks + +Narrowing for `issubclass(class, classinfo)` expressions. + +## `classinfo` is a single type + +### Basic example + +```py +def _(flag: bool): + t = int if flag else str + + if issubclass(t, bytes): + reveal_type(t) # revealed: Never + + if issubclass(t, object): + reveal_type(t) # revealed: | + + if issubclass(t, int): + reveal_type(t) # revealed: + else: + reveal_type(t) # revealed: + + if issubclass(t, str): + reveal_type(t) # revealed: + if issubclass(t, int): + reveal_type(t) # revealed: Never +``` + +### Proper narrowing in `elif` and `else` branches + +```py +def _(flag1: bool, flag2: bool): + t = int if flag1 else str if flag2 else bytes + + if issubclass(t, int): + reveal_type(t) # revealed: + else: + reveal_type(t) # revealed: | + + if issubclass(t, int): + reveal_type(t) # revealed: + elif issubclass(t, str): + reveal_type(t) # revealed: + else: + reveal_type(t) # revealed: +``` + +### Multiple derived classes + +```py +class Base: ... +class Derived1(Base): ... +class Derived2(Base): ... +class Unrelated: ... + +def _(flag1: bool, flag2: bool, flag3: bool): + t1 = Derived1 if flag1 else Derived2 + + if issubclass(t1, Base): + reveal_type(t1) # revealed: | + + if issubclass(t1, Derived1): + reveal_type(t1) # revealed: + else: + reveal_type(t1) # revealed: + + t2 = Derived1 if flag2 else Base + + if issubclass(t2, Base): + reveal_type(t2) # revealed: | + + t3 = Derived1 if flag3 else Unrelated + + if issubclass(t3, Base): + reveal_type(t3) # revealed: + else: + reveal_type(t3) # revealed: +``` + +### Narrowing for non-literals + +```py +class A: ... +class B: ... + +def _(t: type[object]): + if issubclass(t, A): + reveal_type(t) # revealed: type[A] + if issubclass(t, B): + reveal_type(t) # revealed: type[A] & type[B] + else: + reveal_type(t) # revealed: type & ~type[A] +``` + +### Handling of `None` + +`types.NoneType` is only available in Python 3.10 and later: + +```toml +[environment] +python-version = "3.10" +``` + +```py +from types import NoneType + +def _(flag: bool): + t = int if flag else NoneType + + if issubclass(t, NoneType): + reveal_type(t) # revealed: + + if issubclass(t, type(None)): + reveal_type(t) # revealed: +``` + +## `classinfo` contains multiple types + +### (Nested) tuples of types + +```py +class Unrelated: ... + +def _(flag1: bool, flag2: bool): + t = int if flag1 else str if flag2 else bytes + + if issubclass(t, (int, (Unrelated, (bytes,)))): + reveal_type(t) # revealed: | + else: + reveal_type(t) # revealed: +``` + +## Special cases + +### Emit a diagnostic if the first argument is of wrong type + +#### Too wide + +`type[object]` is a subtype of `object`, but not every `object` can be passed as the first argument +to `issubclass`: + +```py +class A: ... + +t = object() + +# error: [invalid-argument-type] +if issubclass(t, A): + reveal_type(t) # revealed: type[A] +``` + +#### Wrong + +`Literal[1]` and `type` are entirely disjoint, so the inferred type of `Literal[1] & type[int]` is +eagerly simplified to `Never` as a result of the type narrowing in the `if issubclass(t, int)` +branch: + +```py +t = 1 + +# error: [invalid-argument-type] +if issubclass(t, int): + reveal_type(t) # revealed: Never +``` + +### Do not use custom `issubclass` for narrowing + +```py +def issubclass(c, ci): + return True + +def flag() -> bool: + return True + +t = int if flag() else str +if issubclass(t, int): + reveal_type(t) # revealed: | +``` + +### Do support narrowing if `issubclass` is aliased + +```py +issubclass_alias = issubclass + +def flag() -> bool: + return True + +t = int if flag() else str +if issubclass_alias(t, int): + reveal_type(t) # revealed: +``` + +### Do support narrowing if `issubclass` is imported + +```py +from builtins import issubclass as imported_issubclass + +def flag() -> bool: + return True + +t = int if flag() else str +if imported_issubclass(t, int): + reveal_type(t) # revealed: +``` + +### Do not narrow if second argument is not a proper `classinfo` argument + +```py +from typing import Any + +def flag() -> bool: + return True + +t = int if flag() else str + +# TODO: this should cause us to emit a diagnostic during +# type checking +if issubclass(t, "str"): + reveal_type(t) # revealed: | + +# TODO: this should cause us to emit a diagnostic during +# type checking +if issubclass(t, (bytes, "str")): + reveal_type(t) # revealed: | + +# TODO: this should cause us to emit a diagnostic during +# type checking +if issubclass(t, Any): + reveal_type(t) # revealed: | +``` + +### Do not narrow if there are keyword arguments + +```py +def flag() -> bool: + return True + +t = int if flag() else str + +# error: [unknown-argument] +if issubclass(t, int, foo="bar"): + reveal_type(t) # revealed: | +``` + +### `type[]` types are narrowed as well as class-literal types + +```py +def _(x: type, y: type[int]): + if issubclass(x, y): + reveal_type(x) # revealed: type[int] +``` + +### Disjoint `type[]` types are narrowed to `Never` + +Here, `type[UsesMeta1]` and `type[UsesMeta2]` are disjoint because a common subclass of `UsesMeta1` +and `UsesMeta2` could only exist if a common subclass of their metaclasses could exist. This is +known to be impossible due to the fact that `Meta1` is marked as `@final`. + +```py +from typing import final + +@final +class Meta1(type): ... + +class Meta2(type): ... +class UsesMeta1(metaclass=Meta1): ... +class UsesMeta2(metaclass=Meta2): ... + +def _(x: type[UsesMeta1], y: type[UsesMeta2]): + if issubclass(x, y): + reveal_type(x) # revealed: Never + else: + reveal_type(x) # revealed: type[UsesMeta1] + + if issubclass(y, x): + reveal_type(y) # revealed: Never + else: + reveal_type(y) # revealed: type[UsesMeta2] +``` + +## Narrowing if an object with an intersection/union/TypeVar type is used as the second argument + +If an intersection with only positive members is used as the second argument, and all positive +members of the intersection are valid arguments for the second argument to `isinstance()`, we +intersect with each positive member of the intersection: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, ClassVar +from ty_extensions import Intersection + +class Foo: ... + +class Bar: + attribute: ClassVar[int] + +class Baz: + attribute: ClassVar[str] + +def f(x: type[Foo], y: Intersection[type[Bar], type[Baz]], z: type[Any]): + if issubclass(x, y): + reveal_type(x) # revealed: type[Foo] & type[Bar] & type[Baz] + + if issubclass(x, z): + reveal_type(x) # revealed: type[Foo] & Any +``` + +The same if a union type is used: + +```py +def g(x: type[Foo], y: type[Bar | Baz]): + if issubclass(x, y): + reveal_type(x) # revealed: (type[Foo] & type[Bar]) | (type[Foo] & type[Baz]) +``` + +And even if a `TypeVar` is used, providing it has valid upper bounds/constraints: + +```py +from typing import TypeVar + +T = TypeVar("T", bound=type[Bar]) + +def h_old_syntax(x: type[Foo], y: T) -> T: + if issubclass(x, y): + reveal_type(x) # revealed: type[Foo] & type[Bar] + reveal_type(x.attribute) # revealed: int + + return y + +def h[U: type[Bar | Baz]](x: type[Foo], y: U) -> U: + if issubclass(x, y): + reveal_type(x) # revealed: (type[Foo] & type[Bar]) | (type[Foo] & type[Baz]) + reveal_type(x.attribute) # revealed: int | str + + return y +``` + +Or even a tuple of tuple of typevars that have intersection bounds... + +```py +from ty_extensions import Intersection + +class Spam: ... +class Eggs: ... +class Ham: ... +class Mushrooms: ... + +def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](x: type[Foo], y: T, z: U) -> tuple[T, U]: + if issubclass(x, (y, (z, Mushrooms))): + # revealed: (type[Foo] & type[Bar] & type[Baz]) | (type[Foo] & type[Bar] & type[Spam]) | (type[Foo] & type[Eggs]) | (type[Foo] & type[Ham]) | (type[Foo] & type[Mushrooms]) + reveal_type(x) + + return (y, z) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md new file mode 100644 index 0000000000000..b6b0ec90e8228 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -0,0 +1,212 @@ +# Narrowing for `match` statements + +```toml +[environment] +python-version = "3.10" +``` + +## Single `match` pattern + +```py +def _(flag: bool): + x = None if flag else 1 + + reveal_type(x) # revealed: None | Literal[1] + + y = 0 + + match x: + case None: + y = x + + reveal_type(y) # revealed: Literal[0] | None +``` + +## Class patterns + +```py +def get_object() -> object: + return object() + +class A: ... +class B: ... + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case A(): + reveal_type(x) # revealed: A + case B(): + reveal_type(x) # revealed: B & ~A + +reveal_type(x) # revealed: object +``` + +## Class pattern with guard + +```py +def get_object() -> object: + return object() + +class A: + def y() -> int: + return 1 + +class B: ... + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case A() if reveal_type(x): # revealed: A + pass + case B() if reveal_type(x): # revealed: B + pass + +reveal_type(x) # revealed: object +``` + +## Value patterns + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case "foo": + reveal_type(x) # revealed: Literal["foo"] + case 42: + reveal_type(x) # revealed: Literal[42] + case 6.0: + reveal_type(x) # revealed: float + case 1j: + reveal_type(x) # revealed: complex + case b"foo": + reveal_type(x) # revealed: Literal[b"foo"] + +reveal_type(x) # revealed: object +``` + +## Value patterns with guard + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case "foo" if reveal_type(x): # revealed: Literal["foo"] + pass + case 42 if reveal_type(x): # revealed: Literal[42] + pass + case 6.0 if reveal_type(x): # revealed: float + pass + case 1j if reveal_type(x): # revealed: complex + pass + case b"foo" if reveal_type(x): # revealed: Literal[b"foo"] + pass + +reveal_type(x) # revealed: object +``` + +## Or patterns + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case "foo" | 42 | None: + reveal_type(x) # revealed: Literal["foo", 42] | None + case "foo" | tuple(): + reveal_type(x) # revealed: tuple[Unknown, ...] + case True | False: + reveal_type(x) # revealed: bool + case 3.14 | 2.718 | 1.414: + reveal_type(x) # revealed: float + +reveal_type(x) # revealed: object +``` + +## Or patterns with guard + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case "foo" | 42 | None if reveal_type(x): # revealed: Literal["foo", 42] | None + pass + case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple[Unknown, ...] + pass + case True | False if reveal_type(x): # revealed: bool + pass + case 3.14 | 2.718 | 1.414 if reveal_type(x): # revealed: float + pass + +reveal_type(x) # revealed: object +``` + +## Narrowing due to guard + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case str() | float() if type(x) is str: + reveal_type(x) # revealed: str + case "foo" | 42 | None if isinstance(x, int): + reveal_type(x) # revealed: Literal[42] + case False if x: + reveal_type(x) # revealed: Never + case "foo" if x := "bar": + reveal_type(x) # revealed: Literal["bar"] + +reveal_type(x) # revealed: object +``` + +## Guard and reveal_type in guard + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case str() | float() if type(x) is str and reveal_type(x): # revealed: str + pass + case "foo" | 42 | None if isinstance(x, int) and reveal_type(x): # revealed: Literal[42] + pass + case False if x and reveal_type(x): # revealed: Never + pass + case "foo" if (x := "bar") and reveal_type(x): # revealed: Literal["bar"] + pass + +reveal_type(x) # revealed: object +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md b/crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md rename to crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md new file mode 100644 index 0000000000000..d6df7c3276e1b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -0,0 +1,349 @@ +# Narrowing For Truthiness Checks (`if x` or `if not x`) + +## Value Literals + +```py +from typing import Literal + +def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]: + return 0 + +x = foo() + +if x: + reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"] +else: + reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()] + +if not x: + reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()] +else: + reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"] + +if x and not x: + reveal_type(x) # revealed: Never +else: + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] + +if not (x and not x): + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] +else: + reveal_type(x) # revealed: Never + +if x or not x: + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] +else: + reveal_type(x) # revealed: Never + +if not (x or not x): + reveal_type(x) # revealed: Never +else: + reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()] + +if (isinstance(x, int) or isinstance(x, str)) and x: + reveal_type(x) # revealed: Literal[-1, True, "foo"] +else: + reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()] +``` + +## Function Literals + +Basically functions are always truthy. + +```py +def flag() -> bool: + return True + +def foo(hello: int) -> bytes: + return b"" + +def bar(world: str, *args, **kwargs) -> float: + return 0.0 + +x = foo if flag() else bar + +if x: + reveal_type(x) # revealed: (def foo(hello: int) -> bytes) | (def bar(world: str, *args, **kwargs) -> int | float) +else: + reveal_type(x) # revealed: Never +``` + +## Mutable Truthiness + +### Truthiness of Instances + +The boolean value of an instance is not always consistent. For example, `__bool__` can be customized +to return random values, or in the case of a `list()`, the result depends on the number of elements +in the list. Therefore, these types should not be narrowed by `if x` or `if not x`. + +```py +class A: ... +class B: ... + +def f(x: A | B): + if x: + reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy) + else: + reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy) + + if x and not x: + reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy) + else: + reveal_type(x) # revealed: A | B + + if x or not x: + reveal_type(x) # revealed: A | B + else: + reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy) +``` + +### Truthiness of Types + +Also, types may not be Truthy. This is because `__bool__` can be customized via a metaclass. +Although this is a very rare case, we may consider metaclass checks in the future to handle this +more accurately. + +```py +def flag() -> bool: + return True + +x = int if flag() else str +reveal_type(x) # revealed: | + +if x: + reveal_type(x) # revealed: ( & ~AlwaysFalsy) | ( & ~AlwaysFalsy) +else: + reveal_type(x) # revealed: ( & ~AlwaysTruthy) | ( & ~AlwaysTruthy) +``` + +## Determined Truthiness + +Some custom classes can have a boolean value that is consistently determined as either `True` or +`False`, regardless of the instance's state. This is achieved by defining a `__bool__` method that +always returns a fixed value. + +These types can always be fully narrowed in boolean contexts, as shown below: + +```py +from typing import Literal + +class T: + def __bool__(self) -> Literal[True]: + return True + +class F: + def __bool__(self) -> Literal[False]: + return False + +t = T() + +if t: + reveal_type(t) # revealed: T +else: + reveal_type(t) # revealed: Never + +f = F() + +if f: + reveal_type(f) # revealed: Never +else: + reveal_type(f) # revealed: F +``` + +## Narrowing Complex Intersection and Union + +```py +from typing import Literal + +class A: ... +class B: ... + +def flag() -> bool: + return True + +def instance() -> A | B: + return A() + +def literals() -> Literal[0, 42, "", "hello"]: + return 42 + +x = instance() +y = literals() + +if isinstance(x, str) and not isinstance(x, B): + reveal_type(x) # revealed: A & str & ~B + reveal_type(y) # revealed: Literal[0, 42, "", "hello"] + + z = x if flag() else y + + reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"] + + if z: + reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"] + else: + reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""] +``` + +## Narrowing Multiple Variables + +```py +from typing import Literal + +def f(x: Literal[0, 1], y: Literal["", "hello"]): + if x and y and not x and not y: + reveal_type(x) # revealed: Never + reveal_type(y) # revealed: Never + else: + # ~(x or not x) and ~(y or not y) + reveal_type(x) # revealed: Literal[0, 1] + reveal_type(y) # revealed: Literal["", "hello"] + + if (x or not x) and (y and not y): + reveal_type(x) # revealed: Never + reveal_type(y) # revealed: Never + else: + # ~(x or not x) or ~(y and not y) + reveal_type(x) # revealed: Literal[0, 1] + reveal_type(y) # revealed: Literal["", "hello"] +``` + +## Control Flow Merging + +After merging control flows, when we take the union of all constraints applied in each branch, we +should return to the original state. + +```py +class A: ... + +x = A() + +if x and not x: + y = x + reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy +else: + y = x + reveal_type(y) # revealed: A + +reveal_type(y) # revealed: A +``` + +## Truthiness of classes + +```py +from typing import Literal + +class MetaAmbiguous(type): + def __bool__(self) -> bool: + return True + +class MetaFalsy(type): + def __bool__(self) -> Literal[False]: + return False + +class MetaTruthy(type): + def __bool__(self) -> Literal[True]: + return True + +class MetaDeferred(type): + def __bool__(self) -> MetaAmbiguous: + raise NotImplementedError + +class AmbiguousClass(metaclass=MetaAmbiguous): ... +class FalsyClass(metaclass=MetaFalsy): ... +class TruthyClass(metaclass=MetaTruthy): ... +class DeferredClass(metaclass=MetaDeferred): ... + +def _( + a: type[AmbiguousClass], + t: type[TruthyClass], + f: type[FalsyClass], + d: type[DeferredClass], + ta: type[TruthyClass | AmbiguousClass], + af: type[AmbiguousClass] | type[FalsyClass], + flag: bool, +): + reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] + if ta: + reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy) + + reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass] + if af: + reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy + + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`" + if d: + # TODO: Should be `Unknown` + reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy + + tf = TruthyClass if flag else FalsyClass + reveal_type(tf) # revealed: | + + if tf: + reveal_type(tf) # revealed: + else: + reveal_type(tf) # revealed: +``` + +## Narrowing in chained boolean expressions + +```py +from typing import Literal + +class A: ... + +def _(x: Literal[0, 1]): + reveal_type(x or A()) # revealed: Literal[1] | A + reveal_type(x and A()) # revealed: Literal[0] | A + +def _(x: str): + reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A + reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A + +def _(x: bool | str): + reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A + reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A + +class Falsy: + def __bool__(self) -> Literal[False]: + return False + +class Truthy: + def __bool__(self) -> Literal[True]: + return True + +def _(x: Falsy | Truthy): + reveal_type(x or A()) # revealed: Truthy | A + reveal_type(x and A()) # revealed: Falsy | A + +class MetaFalsy(type): + def __bool__(self) -> Literal[False]: + return False + +class MetaTruthy(type): + def __bool__(self) -> Literal[True]: + return True + +class FalsyClass(metaclass=MetaFalsy): ... +class TruthyClass(metaclass=MetaTruthy): ... + +def _(x: type[FalsyClass] | type[TruthyClass]): + reveal_type(x or A()) # revealed: type[TruthyClass] | A + reveal_type(x and A()) # revealed: type[FalsyClass] | A +``` + +## Truthiness narrowing for `LiteralString` + +```py +from typing_extensions import LiteralString + +def _(x: LiteralString): + if x: + reveal_type(x) # revealed: LiteralString & ~Literal[""] + else: + reveal_type(x) # revealed: Literal[""] + + if not x: + reveal_type(x) # revealed: Literal[""] + else: + reveal_type(x) # revealed: LiteralString & ~Literal[""] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/type.md rename to crates/ty_python_semantic/resources/mdtest/narrow/type.md index e77a6152e7717..004d53be3fa97 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -78,7 +78,7 @@ No narrowing should occur if `type` is used to dynamically create a class: def _(x: str | int): # The following diagnostic is valid, since the three-argument form of `type` # can only be called with `str` as the first argument. - # error: [no-matching-overload] "No overload of class `type` matches arguments" + # error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`" if type(x, (), {}) is str: reveal_type(x) # revealed: str | int else: @@ -111,6 +111,11 @@ def _(x: A | B): ## Narrowing for generic classes +```toml +[environment] +python-version = "3.13" +``` + Note that `type` returns the runtime class of an object, which does _not_ include specializations in the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the specialization that we compare with; we must narrow to an unknown specialization of the generic @@ -122,7 +127,8 @@ class B: ... def _[T](x: A | B): if type(x) is A[str]: - reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown] + # `type()` never returns a generic alias, so `type(x)` cannot be `A[str]` + reveal_type(x) # revealed: Never else: reveal_type(x) # revealed: A[int] | B ``` @@ -139,3 +145,13 @@ def _(x: Base): # express a constraint like `Base & ~ProperSubtypeOf[Base]`. reveal_type(x) # revealed: Base ``` + +## Assignment expressions + +```py +def _(x: object): + if (y := type(x)) is bool: + reveal_type(y) # revealed: + if (type(y := x)) is bool: + reveal_type(y) # revealed: bool +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md new file mode 100644 index 0000000000000..69586641c99d0 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -0,0 +1,333 @@ +# User-defined type guards + +User-defined type guards are functions of which the return type is either `TypeGuard[...]` or +`TypeIs[...]`. + +## Display + +```py +from ty_extensions import Intersection, Not, TypeOf +from typing_extensions import TypeGuard, TypeIs + +def _( + a: TypeGuard[str], + b: TypeIs[str | int], + c: TypeGuard[Intersection[complex, Not[int], Not[float]]], + d: TypeIs[tuple[TypeOf[bytes]]], + e: TypeGuard, # error: [invalid-type-form] + f: TypeIs, # error: [invalid-type-form] +): + # TODO: Should be `TypeGuard[str]` + reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(b) # revealed: TypeIs[str | int] + # TODO: Should be `TypeGuard[complex & ~int & ~float]` + reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(d) # revealed: TypeIs[tuple[]] + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown + +# TODO: error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`" +def _(a) -> TypeGuard[str]: ... + +# error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeIs[str]`" +def _(a) -> TypeIs[str]: ... +def f(a) -> TypeGuard[str]: + return True + +def g(a) -> TypeIs[str]: + return True + +def _(a: object): + # TODO: Should be `TypeGuard[str @ a]` + reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(g(a)) # revealed: TypeIs[str @ a] +``` + +## Parameters + +A user-defined type guard must accept at least one positional argument (in addition to `self`/`cls` +for non-static methods). + +```pyi +from typing_extensions import TypeGuard, TypeIs + +# TODO: error: [invalid-type-guard-definition] +def _() -> TypeGuard[str]: ... + +# TODO: error: [invalid-type-guard-definition] +def _(**kwargs) -> TypeIs[str]: ... + +class _: + # fine + def _(self, /, a) -> TypeGuard[str]: ... + @classmethod + def _(cls, a) -> TypeGuard[str]: ... + @staticmethod + def _(a) -> TypeIs[str]: ... + + # errors + def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition] + def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition] + @classmethod + def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition] + @classmethod + def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition] + @staticmethod + def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition] +``` + +For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter, +if any. + +```pyi +from typing import Any +from typing_extensions import TypeIs + +def _(a: object) -> TypeIs[str]: ... +def _(a: Any) -> TypeIs[str]: ... +def _(a: tuple[object]) -> TypeIs[tuple[str]]: ... +def _(a: str | Any) -> TypeIs[str]: ... +def _(a) -> TypeIs[str]: ... + +# TODO: error: [invalid-type-guard-definition] +def _(a: int) -> TypeIs[str]: ... + +# TODO: error: [invalid-type-guard-definition] +def _(a: bool | str) -> TypeIs[int]: ... +``` + +## Arguments to special forms + +`TypeGuard` and `TypeIs` accept exactly one type argument. + +```py +from typing_extensions import TypeGuard, TypeIs + +a = 123 + +# TODO: error: [invalid-type-form] +def f(_) -> TypeGuard[int, str]: ... + +# error: [invalid-type-form] "Special form `typing.TypeIs` expected exactly one type parameter" +# error: [invalid-type-form] "Variable of type `Literal[123]` is not allowed in a type expression" +def g(_) -> TypeIs[a, str]: ... + +# TODO: Should be `Unknown` +reveal_type(f(0)) # revealed: @Todo(`TypeGuard[]` special form) +reveal_type(g(0)) # revealed: Unknown +``` + +## Return types + +All code paths in a type guard function must return booleans. + +```py +from typing_extensions import Literal, TypeGuard, TypeIs, assert_never + +def _(a: object, flag: bool) -> TypeGuard[str]: + if flag: + return 0 + + # TODO: error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `Literal["foo"]`" + return "foo" + +# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" +def f(a: object, flag: bool) -> TypeIs[str]: + if flag: + # error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`" + return 1.2 + +def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: + if a == "foo": + # Logically wrong, but allowed regardless + return False + + return False +``` + +## Invalid calls + +```py +from typing import Any +from typing_extensions import TypeGuard, TypeIs + +def f(a: object) -> TypeGuard[str]: + return True + +def g(a: object) -> TypeIs[int]: + return True + +def _(d: Any): + if f(): # error: [missing-argument] + ... + + # TODO: no error, once we support splatted call args + if g(*d): # error: [missing-argument] + ... + + if f("foo"): # TODO: error: [invalid-type-guard-call] + ... + + if g(a=d): # error: [invalid-type-guard-call] + ... +``` + +## Narrowing + +```py +from typing import Any +from typing_extensions import TypeGuard, TypeIs + +class Foo: ... +class Bar: ... + +def guard_foo(a: object) -> TypeGuard[Foo]: + return True + +def is_bar(a: object) -> TypeIs[Bar]: + return True + +def _(a: Foo | Bar): + if guard_foo(a): + # TODO: Should be `Foo` + reveal_type(a) # revealed: Foo | Bar + else: + reveal_type(a) # revealed: Foo | Bar + + if is_bar(a): + reveal_type(a) # revealed: Bar + else: + reveal_type(a) # revealed: Foo & ~Bar +``` + +Attribute and subscript narrowing is supported: + +```py +from typing_extensions import Any, Generic, Protocol, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + v: T + +def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]): + # TODO: Should be `TypeGuard[Foo @ a[1]]` + if reveal_type(guard_foo(a[1])): # revealed: @Todo(`TypeGuard[]` special form) + # TODO: Should be `tuple[Bar, Foo]` + reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo] + # TODO: Should be `Foo` + reveal_type(a[1]) # revealed: Bar | Foo + + if reveal_type(is_bar(a[0])): # revealed: TypeIs[Bar @ a[0]] + # TODO: Should be `tuple[Bar, Bar & Foo]` + reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo] + reveal_type(a[0]) # revealed: Bar + + # TODO: Should be `TypeGuard[Foo @ c.v]` + if reveal_type(guard_foo(c.v)): # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(c) # revealed: C[Any] + # TODO: Should be `Foo` + reveal_type(c.v) # revealed: Any + + if reveal_type(is_bar(c.v)): # revealed: TypeIs[Bar @ c.v] + reveal_type(c) # revealed: C[Any] + reveal_type(c.v) # revealed: Any & Bar +``` + +Indirect usage is supported within the same scope: + +```py +def _(a: Foo | Bar): + b = guard_foo(a) + c = is_bar(a) + + reveal_type(a) # revealed: Foo | Bar + # TODO: Should be `TypeGuard[Foo @ a]` + reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form) + reveal_type(c) # revealed: TypeIs[Bar @ a] + + if b: + # TODO should be `Foo` + reveal_type(a) # revealed: Foo | Bar + else: + reveal_type(a) # revealed: Foo | Bar + + if c: + # TODO should be `Bar` + reveal_type(a) # revealed: Foo | Bar + else: + # TODO should be `Foo & ~Bar` + reveal_type(a) # revealed: Foo | Bar +``` + +Further writes to the narrowed place invalidate the narrowing: + +```py +def _(x: Foo | Bar, flag: bool) -> None: + b = is_bar(x) + reveal_type(b) # revealed: TypeIs[Bar @ x] + + if flag: + x = Foo() + + if b: + reveal_type(x) # revealed: Foo | Bar +``` + +The `TypeIs` type remains effective across generic boundaries: + +```py +from typing_extensions import TypeVar, reveal_type + +T = TypeVar("T") + +def f(v: object) -> TypeIs[Bar]: + return True + +def g(v: T) -> T: + return v + +def _(a: Foo): + # `reveal_type()` has the type `[T]() -> T` + if reveal_type(f(a)): # revealed: TypeIs[Bar @ a] + reveal_type(a) # revealed: Foo & Bar + + if g(f(a)): + reveal_type(a) # revealed: Foo & Bar +``` + +## `TypeGuard` special cases + +```py +from typing import Any +from typing_extensions import TypeGuard, TypeIs + +class Foo: ... +class Bar: ... +class Baz(Bar): ... + +def guard_foo(a: object) -> TypeGuard[Foo]: + return True + +def is_bar(a: object) -> TypeIs[Bar]: + return True + +def does_not_narrow_in_negative_case(a: Foo | Bar): + if not guard_foo(a): + # TODO: Should be `Bar` + reveal_type(a) # revealed: Foo | Bar + else: + reveal_type(a) # revealed: Foo | Bar + +def narrowed_type_must_be_exact(a: object, b: Baz): + if guard_foo(b): + # TODO: Should be `Foo` + reveal_type(b) # revealed: Baz + + if isinstance(a, Baz) and is_bar(a): + reveal_type(a) # revealed: Baz + + if isinstance(a, Bar) and guard_foo(a): + # TODO: Should be `Foo` + reveal_type(a) # revealed: Bar +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md b/crates/ty_python_semantic/resources/mdtest/narrow/while.md similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/while.md rename to crates/ty_python_semantic/resources/mdtest/narrow/while.md index af8141b6469b9..deae318666bff 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/while.md @@ -59,3 +59,17 @@ while x != 1: x = next_item() ``` + +## With `break` statements + +```py +def next_item() -> int | None: + return 1 + +while True: + x = next_item() + if x is not None: + break + +reveal_type(x) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md new file mode 100644 index 0000000000000..650dd9da44845 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -0,0 +1,687 @@ +# Overloads + +Reference: + +## `typing.overload` + +The definition of `typing.overload` in typeshed is an identity function. + +```py +from typing import overload + +def foo(x: int) -> int: + return x + +reveal_type(foo) # revealed: def foo(x: int) -> int +bar = overload(foo) +reveal_type(bar) # revealed: def foo(x: int) -> int +``` + +## Functions + +```py +from typing import overload + +@overload +def add() -> None: ... +@overload +def add(x: int) -> int: ... +@overload +def add(x: int, y: int) -> int: ... +def add(x: int | None = None, y: int | None = None) -> int | None: + return (x or 0) + (y or 0) + +reveal_type(add) # revealed: Overload[() -> None, (x: int) -> int, (x: int, y: int) -> int] +reveal_type(add()) # revealed: None +reveal_type(add(1)) # revealed: int +reveal_type(add(1, 2)) # revealed: int +``` + +## Overriding + +These scenarios are to verify that the overloaded and non-overloaded definitions are correctly +overridden by each other. + +An overloaded function is overriding another overloaded function: + +```py +from typing import overload + +@overload +def foo() -> None: ... +@overload +def foo(x: int) -> int: ... +def foo(x: int | None = None) -> int | None: + return x + +reveal_type(foo) # revealed: Overload[() -> None, (x: int) -> int] +reveal_type(foo()) # revealed: None +reveal_type(foo(1)) # revealed: int + +@overload +def foo() -> None: ... +@overload +def foo(x: str) -> str: ... +def foo(x: str | None = None) -> str | None: + return x + +reveal_type(foo) # revealed: Overload[() -> None, (x: str) -> str] +reveal_type(foo()) # revealed: None +reveal_type(foo("")) # revealed: str +``` + +A non-overloaded function is overriding an overloaded function: + +```py +def foo(x: int) -> int: + return x + +reveal_type(foo) # revealed: def foo(x: int) -> int +``` + +An overloaded function is overriding a non-overloaded function: + +```py +reveal_type(foo) # revealed: def foo(x: int) -> int + +@overload +def foo() -> None: ... +@overload +def foo(x: bytes) -> bytes: ... +def foo(x: bytes | None = None) -> bytes | None: + return x + +reveal_type(foo) # revealed: Overload[() -> None, (x: bytes) -> bytes] +reveal_type(foo()) # revealed: None +reveal_type(foo(b"")) # revealed: bytes +``` + +## Methods + +```py +from typing import overload + +class Foo1: + @overload + def method(self) -> None: ... + @overload + def method(self, x: int) -> int: ... + def method(self, x: int | None = None) -> int | None: + return x + +foo1 = Foo1() +reveal_type(foo1.method) # revealed: Overload[() -> None, (x: int) -> int] +reveal_type(foo1.method()) # revealed: None +reveal_type(foo1.method(1)) # revealed: int + +class Foo2: + @overload + def method(self) -> None: ... + @overload + def method(self, x: str) -> str: ... + def method(self, x: str | None = None) -> str | None: + return x + +foo2 = Foo2() +reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str] +reveal_type(foo2.method()) # revealed: None +reveal_type(foo2.method("")) # revealed: str +``` + +## Constructor + +```py +from typing import overload + +class Foo: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, x: int) -> None: ... + def __init__(self, x: int | None = None) -> None: + self.x = x + +foo = Foo() +reveal_type(foo) # revealed: Foo +reveal_type(foo.x) # revealed: Unknown | int | None + +foo1 = Foo(1) +reveal_type(foo1) # revealed: Foo +reveal_type(foo1.x) # revealed: Unknown | int | None +``` + +## Version specific + +Function definitions can vary between multiple Python versions. + +### Overload and non-overload (3.9) + +Here, the same function is overloaded in one version and not in another. + +```toml +[environment] +python-version = "3.9" +``` + +```py +import sys +from typing import overload + +if sys.version_info < (3, 10): + def func(x: int) -> int: + return x + +elif sys.version_info <= (3, 12): + @overload + def func() -> None: ... + @overload + def func(x: int) -> int: ... + def func(x: int | None = None) -> int | None: + return x + +reveal_type(func) # revealed: def func(x: int) -> int +func() # error: [missing-argument] +``` + +### Overload and non-overload (3.10) + +```toml +[environment] +python-version = "3.10" +``` + +```py +import sys +from typing import overload + +if sys.version_info < (3, 10): + def func(x: int) -> int: + return x + +elif sys.version_info <= (3, 12): + @overload + def func() -> None: ... + @overload + def func(x: int) -> int: ... + def func(x: int | None = None) -> int | None: + return x + +reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int] +reveal_type(func()) # revealed: None +reveal_type(func(1)) # revealed: int +``` + +### Some overloads are version specific (3.9) + +```toml +[environment] +python-version = "3.9" +``` + +`overloaded.pyi`: + +```pyi +import sys +from typing import overload + +if sys.version_info >= (3, 10): + @overload + def func() -> None: ... + +@overload +def func(x: int) -> int: ... +@overload +def func(x: str) -> str: ... +``` + +`main.py`: + +```py +from overloaded import func + +reveal_type(func) # revealed: Overload[(x: int) -> int, (x: str) -> str] +func() # error: [no-matching-overload] +reveal_type(func(1)) # revealed: int +reveal_type(func("")) # revealed: str +``` + +### Some overloads are version specific (3.10) + +```toml +[environment] +python-version = "3.10" +``` + +`overloaded.pyi`: + +```pyi +import sys +from typing import overload + +@overload +def func() -> None: ... + +if sys.version_info >= (3, 10): + @overload + def func(x: int) -> int: ... + +@overload +def func(x: str) -> str: ... +``` + +`main.py`: + +```py +from overloaded import func + +reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int, (x: str) -> str] +reveal_type(func()) # revealed: None +reveal_type(func(1)) # revealed: int +reveal_type(func("")) # revealed: str +``` + +## Generic + +```toml +[environment] +python-version = "3.12" +``` + +For an overloaded generic function, it's not necessary for all overloads to be generic. + +```py +from typing import overload + +@overload +def func() -> None: ... +@overload +def func[T](x: T) -> T: ... +def func[T](x: T | None = None) -> T | None: + return x + +reveal_type(func) # revealed: Overload[() -> None, (x: T) -> T] +reveal_type(func()) # revealed: None +reveal_type(func(1)) # revealed: Literal[1] +reveal_type(func("")) # revealed: Literal[""] +``` + +## Invalid + +### At least two overloads + + + +At least two `@overload`-decorated definitions must be present. + +```py +from typing import overload + +@overload +def func(x: int) -> int: ... + +# error: [invalid-overload] +def func(x: int | str) -> int | str: + return x +``` + +```pyi +from typing import overload + +@overload +# error: [invalid-overload] +def func(x: int) -> int: ... +``` + +### Overload without an implementation + +#### Regular modules + + + +In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one +non-`@overload`-decorated definition (for the same function/method). + +```py +from typing import overload + +@overload +def func(x: int) -> int: ... +@overload +# error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" +def func(x: str) -> str: ... + +class Foo: + @overload + def method(self, x: int) -> int: ... + @overload + # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" + def method(self, x: str) -> str: ... +``` + +#### Stub files + +Overload definitions within stub files are exempt from this check. + +```pyi +from typing import overload + +@overload +def func(x: int) -> int: ... +@overload +def func(x: str) -> str: ... +``` + +#### Protocols + +Overload definitions within protocols are exempt from this check. + +```py +from typing import Protocol, overload + +class Foo(Protocol): + @overload + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... +``` + +#### Abstract methods + +Overload definitions within abstract base classes are exempt from this check. + +```py +from abc import ABC, abstractmethod +from typing import overload + +class AbstractFoo(ABC): + @overload + @abstractmethod + def f(self, x: int) -> int: ... + @overload + @abstractmethod + def f(self, x: str) -> str: ... +``` + +Using the `@abstractmethod` decorator requires that the class's metaclass is `ABCMeta` or is derived +from it. + +```py +class Foo: + @overload + @abstractmethod + def f(self, x: int) -> int: ... + @overload + @abstractmethod + # error: [invalid-overload] + def f(self, x: str) -> str: ... +``` + +And, the `@abstractmethod` decorator must be present on all the `@overload`-ed methods. + +```py +class PartialFoo1(ABC): + @overload + @abstractmethod + def f(self, x: int) -> int: ... + @overload + # error: [invalid-overload] + def f(self, x: str) -> str: ... + +class PartialFoo(ABC): + @overload + def f(self, x: int) -> int: ... + @overload + @abstractmethod + # error: [invalid-overload] + def f(self, x: str) -> str: ... +``` + +### Inconsistent decorators + +#### `@staticmethod` + +If one overload signature is decorated with `@staticmethod`, all overload signatures must be +similarly decorated. The implementation, if present, must also have a consistent decorator. + +```py +from __future__ import annotations + +from typing import overload + +class CheckStaticMethod: + @overload + def method1(x: int) -> int: ... + @overload + def method1(x: str) -> str: ... + @staticmethod + # error: [invalid-overload] "Overloaded function `method1` does not use the `@staticmethod` decorator consistently" + def method1(x: int | str) -> int | str: + return x + + @overload + def method2(x: int) -> int: ... + @overload + @staticmethod + def method2(x: str) -> str: ... + @staticmethod + # error: [invalid-overload] + def method2(x: int | str) -> int | str: + return x + + @overload + @staticmethod + def method3(x: int) -> int: ... + @overload + @staticmethod + def method3(x: str) -> str: ... + # error: [invalid-overload] + def method3(x: int | str) -> int | str: + return x + + @overload + @staticmethod + def method4(x: int) -> int: ... + @overload + @staticmethod + def method4(x: str) -> str: ... + @staticmethod + def method4(x: int | str) -> int | str: + return x +``` + +#### `@classmethod` + + + +The same rules apply for `@classmethod` as for [`@staticmethod`](#staticmethod). + +```py +from __future__ import annotations + +from typing import overload + +class CheckClassMethod: + def __init__(self, x: int) -> None: + self.x = x + + @overload + @classmethod + def try_from1(cls, x: int) -> CheckClassMethod: ... + @overload + def try_from1(cls, x: str) -> None: ... + @classmethod + # error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently" + def try_from1(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None + + @overload + def try_from2(cls, x: int) -> CheckClassMethod: ... + @overload + @classmethod + def try_from2(cls, x: str) -> None: ... + @classmethod + # error: [invalid-overload] + def try_from2(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None + + @overload + @classmethod + def try_from3(cls, x: int) -> CheckClassMethod: ... + @overload + @classmethod + def try_from3(cls, x: str) -> None: ... + # error: [invalid-overload] + def try_from3(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None + + @overload + @classmethod + def try_from4(cls, x: int) -> CheckClassMethod: ... + @overload + @classmethod + def try_from4(cls, x: str) -> None: ... + @classmethod + def try_from4(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None +``` + +#### `@final` + + + +If a `@final` decorator is supplied for a function with overloads, the decorator should be applied +only to the overload implementation if it is present. + +```py +from typing_extensions import final, overload + +class Foo: + @overload + def method1(self, x: int) -> int: ... + @overload + def method1(self, x: str) -> str: ... + @final + def method1(self, x: int | str) -> int | str: + return x + + @overload + @final + def method2(self, x: int) -> int: ... + @overload + def method2(self, x: str) -> str: ... + # error: [invalid-overload] + def method2(self, x: int | str) -> int | str: + return x + + @overload + def method3(self, x: int) -> int: ... + @overload + @final + def method3(self, x: str) -> str: ... + # error: [invalid-overload] + def method3(self, x: int | str) -> int | str: + return x +``` + +If an overload implementation isn't present (for example, in a stub file), the `@final` decorator +should be applied only to the first overload. + +```pyi +from typing_extensions import final, overload + +class Foo: + @overload + @final + def method1(self, x: int) -> int: ... + @overload + def method1(self, x: str) -> str: ... + + @overload + def method2(self, x: int) -> int: ... + @final + @overload + # error: [invalid-overload] + def method2(self, x: str) -> str: ... +``` + +#### `@override` + + + +The same rules apply for `@override` as for [`@final`](#final). + +```py +from typing_extensions import overload, override + +class Base: + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + def method(self, x: int | str) -> int | str: + return x + +class Sub1(Base): + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + @override + def method(self, x: int | str) -> int | str: + return x + +class Sub2(Base): + @overload + def method(self, x: int) -> int: ... + @overload + @override + def method(self, x: str) -> str: ... + # error: [invalid-overload] + def method(self, x: int | str) -> int | str: + return x + +class Sub3(Base): + @overload + @override + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + # error: [invalid-overload] + def method(self, x: int | str) -> int | str: + return x +``` + +And, similarly, in stub files: + +```pyi +from typing_extensions import overload, override + +class Base: + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + +class Sub1(Base): + @overload + @override + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + +class Sub2(Base): + @overload + def method(self, x: int) -> int: ... + @overload + @override + # error: [invalid-overload] + def method(self, x: str) -> str: ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md new file mode 100644 index 0000000000000..65c4f988659b1 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -0,0 +1,140 @@ +# PEP 695 type aliases + +PEP 695 type aliases are only available in Python 3.12 and later: + +```toml +[environment] +python-version = "3.12" +``` + +## Basic + +```py +type IntOrStr = int | str + +reveal_type(IntOrStr) # revealed: typing.TypeAliasType +reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"] + +x: IntOrStr = 1 + +reveal_type(x) # revealed: Literal[1] + +def f() -> None: + reveal_type(x) # revealed: int | str +``` + +## `__value__` attribute + +```py +type IntOrStr = int | str + +reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`) +``` + +## Invalid assignment + +```py +type OptionalInt = int | None + +# error: [invalid-assignment] +x: OptionalInt = "1" +``` + +## Type aliases in type aliases + +```py +type IntOrStr = int | str +type IntOrStrOrBytes = IntOrStr | bytes + +x: IntOrStrOrBytes = 1 + +def f() -> None: + reveal_type(x) # revealed: int | str | bytes +``` + +## Aliased type aliases + +```py +type IntOrStr = int | str +MyIntOrStr = IntOrStr + +x: MyIntOrStr = 1 + +# error: [invalid-assignment] +y: MyIntOrStr = None +``` + +## Generic type aliases + +```py +type ListOrSet[T] = list[T] | set[T] +reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] +``` + +## `TypeAliasType` properties + +Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type + +```py +from ty_extensions import static_assert, is_equivalent_to, is_disjoint_from, TypeOf + +type Alias1 = int +type Alias2 = int + +type TypeAliasType1 = TypeOf[Alias1] +type TypeAliasType2 = TypeOf[Alias2] + +static_assert(not is_equivalent_to(TypeAliasType1, TypeAliasType2)) +static_assert(is_disjoint_from(TypeAliasType1, TypeAliasType2)) +``` + +## Direct use of `TypeAliasType` + +`TypeAliasType` can also be used directly. This is useful for versions of Python prior to 3.12. + +```toml +[environment] +python-version = "3.9" +``` + +### Basic example + +```py +from typing_extensions import TypeAliasType, Union + +IntOrStr = TypeAliasType("IntOrStr", Union[int, str]) + +reveal_type(IntOrStr) # revealed: typing.TypeAliasType + +reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"] + +def f(x: IntOrStr) -> None: + reveal_type(x) # revealed: int | str +``` + +### Generic example + +```py +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T") + +IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) + +def f(x: IntAnd[str]) -> None: + reveal_type(x) # revealed: @Todo(Generic PEP-695 type alias) +``` + +### Error cases + +#### Name is not a string literal + +```py +from typing_extensions import TypeAliasType + +def get_name() -> str: + return "IntOrStr" + +# error: [invalid-type-alias-type] "The name of a `typing.TypeAlias` must be a string literal" +IntOrStr = TypeAliasType(get_name(), int | str) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/properties.md rename to crates/ty_python_semantic/resources/mdtest/properties.md index 2a24f2cd5f0f6..aff91ead1bc5a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -146,7 +146,7 @@ class C: @property def attr(self) -> int: return 1 - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `(Any, Any, /) -> None`, found `def attr(self) -> None`" + # error: [invalid-argument-type] "Argument to bound method `setter` is incorrect: Expected `(Any, Any, /) -> None`, found `def attr(self) -> None`" @attr.setter def attr(self) -> None: pass @@ -156,7 +156,7 @@ class C: ```py class C: - # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `((Any, /) -> Any) | None`, found `def attr(self, x: int) -> int`" + # error: [invalid-argument-type] "Argument to class `property` is incorrect: Expected `((Any, /) -> Any) | None`, found `def attr(self, x: int) -> int`" @property def attr(self, x: int) -> int: return 1 diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md new file mode 100644 index 0000000000000..1d951ad3ed52e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -0,0 +1,2007 @@ +# Protocols + +> [!NOTE] +> +> See also: +> +> - The [typing specification section on protocols][typing_spec_protocols] +> - The many [protocol conformance tests] provided by the Typing Council for type checkers +> - Mypy's [documentation][mypy_protocol_docs] and [tests][mypy_protocol_tests] for protocols + +Most types in Python are *nominal* types: a fully static nominal type `X` is only a subtype of +another fully static nominal type `Y` if the class `X` is a subclass of the class `Y`. +`typing.Protocol` (or its backport, `typing_extensions.Protocol`) can be used to define *structural* +types, on the other hand: a type which is defined by its properties and behaviour. + +## Defining a protocol + +```toml +[environment] +python-version = "3.12" +``` + +A protocol is defined by inheriting from the `Protocol` class, which is annotated as an instance of +`_SpecialForm` in typeshed's stubs. + +```py +from typing import Protocol + +class MyProtocol(Protocol): ... + +reveal_type(MyProtocol.__mro__) # revealed: tuple[, typing.Protocol, typing.Generic, ] +``` + +Just like for any other class base, it is an error for `Protocol` to appear multiple times in a +class's bases: + +```py +class Foo(Protocol, Protocol): ... # error: [duplicate-base] + +reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +Protocols can also be generic, either by including `Generic[]` in the bases list, subscripting +`Protocol` directly in the bases list, using PEP-695 type parameters, or some combination of the +above: + +```py +from typing import TypeVar, Generic + +T = TypeVar("T") + +class Bar0(Protocol[T]): + x: T + +class Bar1(Protocol[T], Generic[T]): + x: T + +class Bar2[T](Protocol): + x: T + +# error: [invalid-generic-class] "Cannot both inherit from subscripted `Protocol` and use PEP 695 type variables" +class Bar3[T](Protocol[T]): + x: T + +# Note that this class definition *will* actually succeed at runtime, +# unlike classes that combine PEP-695 type parameters with inheritance from `Generic[]` +reveal_type(Bar3.__mro__) # revealed: tuple[, typing.Protocol, typing.Generic, ] +``` + +It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list +simultaneously: + +```py +class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base] + x: T + +# revealed: tuple[, Unknown, ] +reveal_type(DuplicateBases.__mro__) +``` + +The introspection helper `typing(_extensions).is_protocol` can be used to verify whether a class is +a protocol class or not: + +```py +from typing_extensions import is_protocol + +reveal_type(is_protocol(MyProtocol)) # revealed: Literal[True] +reveal_type(is_protocol(Bar0)) # revealed: Literal[True] +reveal_type(is_protocol(Bar1)) # revealed: Literal[True] +reveal_type(is_protocol(Bar2)) # revealed: Literal[True] +reveal_type(is_protocol(Bar3)) # revealed: Literal[True] + +class NotAProtocol: ... + +reveal_type(is_protocol(NotAProtocol)) # revealed: Literal[False] +``` + +A type checker should follow the typeshed stubs if a non-class is passed in, and typeshed's stubs +indicate that the argument passed in must be an instance of `type`. + +```py +# We could also reasonably infer `Literal[False]` here, but it probably doesn't matter that much: +# error: [invalid-argument-type] +reveal_type(is_protocol("not a class")) # revealed: bool +``` + +For a class to be considered a protocol class, it must have `Protocol` directly in its bases tuple: +it is not sufficient for it to have `Protocol` in its MRO. + +```py +class SubclassOfMyProtocol(MyProtocol): ... + +# revealed: tuple[, , typing.Protocol, typing.Generic, ] +reveal_type(SubclassOfMyProtocol.__mro__) + +reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: Literal[False] +``` + +A protocol class may inherit from other protocols, however, as long as it re-inherits from +`Protocol`: + +```py +class SubProtocol(MyProtocol, Protocol): ... + +reveal_type(is_protocol(SubProtocol)) # revealed: Literal[True] + +class OtherProtocol(Protocol): + some_attribute: str + +class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... + +# revealed: tuple[, , , , typing.Protocol, typing.Generic, ] +reveal_type(ComplexInheritance.__mro__) + +reveal_type(is_protocol(ComplexInheritance)) # revealed: Literal[True] +``` + +If `Protocol` is present in the bases tuple, all other bases in the tuple must be protocol classes, +or `TypeError` is raised at runtime when the class is created. + +```py +# error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`" +class Invalid(NotAProtocol, Protocol): ... + +# revealed: tuple[, , typing.Protocol, typing.Generic, ] +reveal_type(Invalid.__mro__) + +# error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`" +class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... + +# revealed: tuple[, , , , typing.Protocol, typing.Generic, ] +reveal_type(AlsoInvalid.__mro__) +``` + +But two exceptions to this rule are `object` and `Generic`: + +```py +from typing import TypeVar, Generic + +T = TypeVar("T") + +# Note: pyright and pyrefly do not consider this to be a valid `Protocol` class, +# but mypy does (and has an explicit test for this behaviour). Mypy was the +# reference implementation for PEP-544, and its behaviour also matches the CPython +# runtime, so we choose to follow its behaviour here rather than that of the other +# type checkers. +class Fine(Protocol, object): ... + +reveal_type(Fine.__mro__) # revealed: tuple[, typing.Protocol, typing.Generic, ] + +class StillFine(Protocol, Generic[T], object): ... +class EvenThis[T](Protocol, object): ... +class OrThis(Protocol[T], Generic[T]): ... +class AndThis(Protocol[T], Generic[T], object): ... +``` + +And multiple inheritance from a mix of protocol and non-protocol classes is fine as long as +`Protocol` itself is not in the bases list: + +```py +class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ... + +# revealed: tuple[, , , typing.Protocol, typing.Generic, , ] +reveal_type(FineAndDandy.__mro__) +``` + +But if `Protocol` is not present in the bases list, the resulting class doesn't count as a protocol +class anymore: + +```py +reveal_type(is_protocol(FineAndDandy)) # revealed: Literal[False] +``` + +A class does not *have* to inherit from a protocol class in order for it to be considered a subtype +of that protocol (more on that below). However, classes that explicitly inherit from a protocol +class are understood as subtypes of that protocol, the same as with nominal types: + +```py +from ty_extensions import static_assert, is_subtype_of, is_assignable_to + +static_assert(is_subtype_of(SubclassOfMyProtocol, MyProtocol)) +static_assert(is_assignable_to(SubclassOfMyProtocol, MyProtocol)) + +static_assert(is_subtype_of(SubProtocol, MyProtocol)) +static_assert(is_assignable_to(SubProtocol, MyProtocol)) + +static_assert(is_subtype_of(ComplexInheritance, SubProtocol)) +static_assert(is_assignable_to(ComplexInheritance, SubProtocol)) + +static_assert(is_subtype_of(ComplexInheritance, OtherProtocol)) +static_assert(is_assignable_to(ComplexInheritance, SubProtocol)) + +static_assert(is_subtype_of(FineAndDandy, MyProtocol)) +static_assert(is_assignable_to(FineAndDandy, MyProtocol)) + +static_assert(is_subtype_of(FineAndDandy, OtherProtocol)) +static_assert(is_assignable_to(FineAndDandy, OtherProtocol)) +``` + +Note, however, that `Protocol` itself is not a type, so it is an error to pass it to `is_subtype_of` +or `is_assignable_to`: + +```py +is_subtype_of(MyProtocol, Protocol) # error: [invalid-type-form] +is_assignable_to(MyProtocol, Protocol) # error: [invalid-type-form] +``` + +And it is also an error to use `Protocol` in type expressions: + +```py +# fmt: off + +def f( + x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions" + y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too +): + reveal_type(x) # revealed: Unknown + + # TODO: should be `type[Unknown]` + reveal_type(y) # revealed: @Todo(unsupported type[X] special form) + +# fmt: on +``` + +Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime: + +```py +# Could also be `Literal[True]`, but `bool` is fine: +reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool +``` + +## `typing.Protocol` versus `typing_extensions.Protocol` + +`typing.Protocol` and its backport in `typing_extensions` should be treated as exactly equivalent. + +```py +import typing +import typing_extensions +from ty_extensions import static_assert, is_equivalent_to, TypeOf + +static_assert(is_equivalent_to(TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol])) +static_assert(is_equivalent_to(int | str | TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol] | str | int)) + +class Foo(typing.Protocol): + x: int + +class Bar(typing_extensions.Protocol): + x: int + +static_assert(typing_extensions.is_protocol(Foo)) +static_assert(typing_extensions.is_protocol(Bar)) +static_assert(is_equivalent_to(Foo, Bar)) +``` + +The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`: + +```py +@typing_extensions.runtime_checkable +class RuntimeCheckableFoo(typing.Protocol): + x: int + +@typing.runtime_checkable +class RuntimeCheckableBar(typing_extensions.Protocol): + x: int + +static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) +static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) +static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) + +# These should not error because the protocols are decorated with `@runtime_checkable` +isinstance(object(), RuntimeCheckableFoo) +isinstance(object(), RuntimeCheckableBar) +``` + +However, we understand that they are not necessarily the same symbol at the same memory address at +runtime -- these reveal `bool` rather than `Literal[True]` or `Literal[False]`, which would be +incorrect: + +```py +reveal_type(typing.Protocol is typing_extensions.Protocol) # revealed: bool +reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool +``` + +## Calls to protocol classes + + + +Neither `Protocol`, nor any protocol class, can be directly instantiated: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import Protocol, reveal_type + +# error: [call-non-callable] +reveal_type(Protocol()) # revealed: Unknown + +class MyProtocol(Protocol): + x: int + +# error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +reveal_type(MyProtocol()) # revealed: MyProtocol + +class GenericProtocol[T](Protocol): + x: T + +# error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] +``` + +But a non-protocol class can be instantiated, even if it has `Protocol` in its MRO: + +```py +class SubclassOfMyProtocol(MyProtocol): ... + +reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol + +class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... + +reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] +``` + +And as a corollary, `type[MyProtocol]` can also be called: + +```py +def f(x: type[MyProtocol]): + reveal_type(x()) # revealed: MyProtocol +``` + +## Members of a protocol + +A protocol defines an interface through its *members*: if a protocol `Foo` has members `X` and `Y`, +a type `Bar` can only be a subtype of `Foo` if inhabitants of `Bar` also have attributes `X` and +`Y`. + +A protocol class defines its members through declarations in the class body. The members of a +protocol can be introspected using the function `typing.get_protocol_members`, which is backported +via `typing_extensions`. + +```py +from typing_extensions import Protocol, get_protocol_members + +class Foo(Protocol): + x: int + + @property + def y(self) -> str: + return "y" + + @property + def z(self) -> int: + return 42 + + @z.setter + def z(self, z: int) -> None: ... + def method_member(self) -> bytes: + return b"foo" + +reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["method_member", "x", "y", "z"]] +``` + +Certain special attributes and methods are not considered protocol members at runtime, and should +not be considered protocol members by type checkers either: + +```py +class Lumberjack(Protocol): + __slots__ = () + __match_args__ = () + _abc_foo: str # any attribute starting with `_abc_` is excluded as a protocol attribute + x: int + + def __new__(cls, x: int) -> "Lumberjack": + return object.__new__(cls) + + def __init__(self, x: int) -> None: + self.x = x + +reveal_type(get_protocol_members(Lumberjack)) # revealed: frozenset[Literal["x"]] +``` + +A sub-protocol inherits and extends the members of its superclass protocol(s): + +```py +class Bar(Protocol): + spam: str + +class Baz(Bar, Protocol): + ham: memoryview + +reveal_type(get_protocol_members(Baz)) # revealed: frozenset[Literal["ham", "spam"]] + +class Baz2(Bar, Foo, Protocol): ... + +# revealed: frozenset[Literal["method_member", "spam", "x", "y", "z"]] +reveal_type(get_protocol_members(Baz2)) +``` + +## Protocol members in statically known branches + +The list of protocol members does not include any members declared in branches that are statically +known to be unreachable: + +```toml +[environment] +python-version = "3.9" +``` + +```py +import sys +from typing_extensions import Protocol, get_protocol_members + +class Foo(Protocol): + if sys.version_info >= (3, 10): + a: int + b = 42 + def c(self) -> None: ... + else: + d: int + e = 56 + def f(self) -> None: ... + +reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]] +``` + +## Invalid calls to `get_protocol_members()` + + + +Calling `get_protocol_members` on a non-protocol class raises an error at runtime: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import Protocol, get_protocol_members + +class NotAProtocol: ... + +get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + +class AlsoNotAProtocol(NotAProtocol, object): ... + +get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] +``` + +The original class object must be passed to the function; a specialised version of a generic version +does not suffice: + +```py +class GenericProtocol[T](Protocol): ... + +get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549) +``` + +## Subtyping of protocols with attribute members + +In the following example, the protocol class `HasX` defines an interface such that any other fully +static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a +mutable `x` attribute of type `int`: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Protocol, Any, ClassVar +from collections.abc import Sequence +from ty_extensions import static_assert, is_assignable_to, is_subtype_of + +class HasX(Protocol): + x: int + +class HasXY(Protocol): + x: int + y: int + +class Foo: + x: int + +static_assert(is_subtype_of(Foo, HasX)) +static_assert(is_assignable_to(Foo, HasX)) +static_assert(not is_subtype_of(Foo, HasXY)) +static_assert(not is_assignable_to(Foo, HasXY)) + +class FooSub(Foo): ... + +static_assert(is_subtype_of(FooSub, HasX)) +static_assert(is_assignable_to(FooSub, HasX)) +static_assert(not is_subtype_of(FooSub, HasXY)) +static_assert(not is_assignable_to(FooSub, HasXY)) + +class FooBool(Foo): + x: bool + +static_assert(not is_subtype_of(FooBool, HasX)) +static_assert(not is_assignable_to(FooBool, HasX)) + +class FooAny: + x: Any + +static_assert(not is_subtype_of(FooAny, HasX)) +static_assert(is_assignable_to(FooAny, HasX)) + +class SubclassOfAny(Any): ... + +class FooSubclassOfAny: + x: SubclassOfAny + +static_assert(not is_subtype_of(FooSubclassOfAny, HasX)) + +# `FooSubclassOfAny` is assignable to `HasX` for the following reason. The `x` attribute on `FooSubclassOfAny` +# is accessible on the class itself. When accessing `x` on an instance, the descriptor protocol is invoked, and +# `__get__` is looked up on `SubclassOfAny`. Every member access on `SubclassOfAny` yields `Any`, so `__get__` is +# also available, and calling `Any` also yields `Any`. Thus, accessing `x` on an instance of `FooSubclassOfAny` +# yields `Any`, which is assignable to `int` and vice versa. +static_assert(is_assignable_to(FooSubclassOfAny, HasX)) + +class FooWithY(Foo): + y: int + +assert is_subtype_of(FooWithY, HasXY) +static_assert(is_assignable_to(FooWithY, HasXY)) + +class Bar: + x: str + +static_assert(not is_subtype_of(Bar, HasX)) +static_assert(not is_assignable_to(Bar, HasX)) + +class Baz: + y: int + +static_assert(not is_subtype_of(Baz, HasX)) +static_assert(not is_assignable_to(Baz, HasX)) + +class Qux: + def __init__(self, x: int) -> None: + self.x: int = x + +static_assert(is_subtype_of(Qux, HasX)) +static_assert(is_assignable_to(Qux, HasX)) + +class HalfUnknownQux: + def __init__(self, x: int) -> None: + self.x = x + +reveal_type(HalfUnknownQux(1).x) # revealed: Unknown | int + +static_assert(not is_subtype_of(HalfUnknownQux, HasX)) +static_assert(is_assignable_to(HalfUnknownQux, HasX)) + +class FullyUnknownQux: + def __init__(self, x) -> None: + self.x = x + +static_assert(not is_subtype_of(FullyUnknownQux, HasX)) +static_assert(is_assignable_to(FullyUnknownQux, HasX)) + +class HasXWithDefault(Protocol): + x: int = 0 + +class FooWithZero: + x: int = 0 + +# TODO: these should pass +static_assert(is_subtype_of(FooWithZero, HasXWithDefault)) # error: [static-assert-error] +static_assert(is_assignable_to(FooWithZero, HasXWithDefault)) # error: [static-assert-error] +static_assert(not is_subtype_of(Foo, HasXWithDefault)) +static_assert(not is_assignable_to(Foo, HasXWithDefault)) +static_assert(not is_subtype_of(Qux, HasXWithDefault)) +static_assert(not is_assignable_to(Qux, HasXWithDefault)) + +class HasClassVarX(Protocol): + x: ClassVar[int] + +static_assert(is_subtype_of(FooWithZero, HasClassVarX)) +static_assert(is_assignable_to(FooWithZero, HasClassVarX)) +# TODO: these should pass +static_assert(not is_subtype_of(Foo, HasClassVarX)) # error: [static-assert-error] +static_assert(not is_assignable_to(Foo, HasClassVarX)) # error: [static-assert-error] +static_assert(not is_subtype_of(Qux, HasClassVarX)) # error: [static-assert-error] +static_assert(not is_assignable_to(Qux, HasClassVarX)) # error: [static-assert-error] + +static_assert(is_subtype_of(Sequence[Foo], Sequence[HasX])) +static_assert(is_assignable_to(Sequence[Foo], Sequence[HasX])) +static_assert(not is_subtype_of(list[Foo], list[HasX])) +static_assert(not is_assignable_to(list[Foo], list[HasX])) +``` + +Note that declaring an attribute member on a protocol mandates that the attribute must be mutable. A +type with a read-only `x` property does not satisfy the `HasX` interface; nor does a type with a +`Final` `x` attribute. The type of the attribute must also be treated as invariant due to the +attribute's mutability: + +```py +from typing import Final + +class A: + @property + def x(self) -> int: + return 42 + +# TODO: these should pass +static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error] + +class B: + x: Final = 42 + +# TODO: these should pass +static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error] + +class IntSub(int): ... + +class C: + x: IntSub + +# due to invariance, a type is only a subtype of `HasX` +# if its `x` attribute is of type *exactly* `int`: +# a subclass of `int` does not satisfy the interface +static_assert(not is_subtype_of(C, HasX)) +static_assert(not is_assignable_to(C, HasX)) +``` + +All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes +can never be considered to inhabit a protocol that declares a mutable-attribute member: + +```py +from dataclasses import dataclass +from typing import NamedTuple + +@dataclass +class MutableDataclass: + x: int + +static_assert(is_subtype_of(MutableDataclass, HasX)) +static_assert(is_assignable_to(MutableDataclass, HasX)) + +@dataclass(frozen=True) +class ImmutableDataclass: + x: int + +# TODO: these should pass +static_assert(not is_subtype_of(ImmutableDataclass, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(ImmutableDataclass, HasX)) # error: [static-assert-error] + +class NamedTupleWithX(NamedTuple): + x: int + +# TODO: these should pass +static_assert(not is_subtype_of(NamedTupleWithX, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(NamedTupleWithX, HasX)) # error: [static-assert-error] +``` + +However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX` +protocol only specifies what the type of `x` should be when accessed from instances; instances of +`XProperty` in the below example have a mutable attribute `x` of type `int`: + +```py +class XProperty: + _x: int + + @property + def x(self) -> int: + return self._x + + @x.setter + def x(self, x: int) -> None: + self._x = x**2 + +static_assert(is_subtype_of(XProperty, HasX)) +static_assert(is_assignable_to(XProperty, HasX)) +``` + +Attribute members on protocol classes are allowed to have default values, just like instance +attributes on other classes. Similar to nominal classes, attributes with defaults can be accessed on +the class object itself and any explicit subclasses of the protocol class. It cannot be assumed to +exist on the meta-type of any arbitrary inhabitant of the protocol type, however; an implicit +subtype of the protocol will not necessarily have a default value for the instance attribute +provided in its class body: + +```py +class HasXWithDefault(Protocol): + x: int = 42 + +reveal_type(HasXWithDefault.x) # revealed: int + +class ExplicitSubclass(HasXWithDefault): ... + +reveal_type(ExplicitSubclass.x) # revealed: int + +def f(arg: HasXWithDefault): + # TODO: should emit `[unresolved-reference]` and reveal `Unknown` + reveal_type(type(arg).x) # revealed: int +``` + +Assignments in a class body of a protocol -- of any kind -- are not permitted by ty unless the +symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is +stricter validation of protocol members than many other type checkers currently apply (as of +2025/04/21). + +The reason for this strict validation is that undeclared variables in the class body would lead to +an ambiguous interface being declared by the protocol. + +```py +from typing_extensions import TypeAlias, get_protocol_members + +class MyContext: + def __enter__(self) -> int: + return 42 + + def __exit__(self, *args) -> None: ... + +class LotsOfBindings(Protocol): + a: int + a = 42 # this is fine, since `a` is declared in the class body + b: int = 56 # this is also fine, by the same principle + + type c = str # this is very strange but I can't see a good reason to disallow it + d: TypeAlias = bytes # same here + + class Nested: ... # also weird, but we should also probably allow it + class NestedProtocol(Protocol): ... # same here... + e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared) + + f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared) + + h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared) + + for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared) + pass + + with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared) + pass + + match object(): + case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared) + ... + +# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]] +reveal_type(get_protocol_members(LotsOfBindings)) +``` + +Attribute members are allowed to have assignments in methods on the protocol class, just like +non-protocol classes. Unlike other classes, however, instance attributes that are not declared in +the class body are disallowed. This is mandated by [the spec][spec_protocol_members]: + +> Additional attributes *only* defined in the body of a method by assignment via `self` are not +> allowed. The rationale for this is that the protocol class implementation is often not shared by +> subtypes, so the interface should not depend on the default implementation. + +```py +class Foo(Protocol): + x: int + y: str + + def __init__(self) -> None: + self.x = 42 # fine + self.a = 56 # TODO: should emit diagnostic + self.b: int = 128 # TODO: should emit diagnostic + + def non_init_method(self) -> None: + self.y = 64 # fine + self.c = 72 # TODO: should emit diagnostic + +# Note: the list of members does not include `a`, `b` or `c`, +# as none of these attributes is declared in the class body. +reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["non_init_method", "x", "y"]] +``` + +If a member is declared in a superclass of a protocol class, it is fine for it to be assigned to in +the sub-protocol class without a redeclaration: + +```py +class Super(Protocol): + x: int + +class Sub(Super, Protocol): + x = 42 # no error here, since it's declared in the superclass + +reveal_type(get_protocol_members(Super)) # revealed: frozenset[Literal["x"]] +reveal_type(get_protocol_members(Sub)) # revealed: frozenset[Literal["x"]] +``` + +If a protocol has 0 members, then all other types are assignable to it, and all fully static types +are subtypes of it: + +```py +from typing import Protocol + +class UniversalSet(Protocol): ... + +static_assert(is_assignable_to(object, UniversalSet)) +static_assert(is_subtype_of(object, UniversalSet)) +``` + +Which means that `UniversalSet` here is in fact an equivalent type to `object`: + +```py +from ty_extensions import is_equivalent_to + +static_assert(is_equivalent_to(UniversalSet, object)) +``` + +`object` is a subtype of certain other protocols too. Since all fully static types (whether nominal +or structural) are subtypes of `object`, these protocols are also subtypes of `object`; and this +means that these protocols are also equivalent to `UniversalSet` and `object`: + +```py +class SupportsStr(Protocol): + def __str__(self) -> str: ... + +static_assert(is_equivalent_to(SupportsStr, UniversalSet)) +static_assert(is_equivalent_to(SupportsStr, object)) + +class SupportsClass(Protocol): + @property + def __class__(self) -> type: ... + +static_assert(is_equivalent_to(SupportsClass, UniversalSet)) +static_assert(is_equivalent_to(SupportsClass, SupportsStr)) +static_assert(is_equivalent_to(SupportsClass, object)) +``` + +If a protocol contains members that are not defined on `object`, then that protocol will (like all +types in Python) still be assignable to `object`, but `object` will not be assignable to that +protocol: + +```py +static_assert(is_assignable_to(HasX, object)) +static_assert(is_subtype_of(HasX, object)) +static_assert(not is_assignable_to(object, HasX)) +static_assert(not is_subtype_of(object, HasX)) +``` + +But `object` is the *only* fully static nominal type that a protocol type can ever be assignable to +or a subtype of: + +```py +static_assert(not is_assignable_to(HasX, Foo)) +static_assert(not is_subtype_of(HasX, Foo)) +``` + +## Equivalence of protocols + +Two protocols are considered equivalent types if they specify the same interface, even if they have +different names: + +```py +from typing import Protocol +from ty_extensions import is_equivalent_to, static_assert + +class HasX(Protocol): + x: int + +class AlsoHasX(Protocol): + x: int + +static_assert(is_equivalent_to(HasX, AlsoHasX)) +``` + +And unions containing equivalent protocols are recognised as equivalent, even when the order is not +identical: + +```py +class HasY(Protocol): + y: str + +class AlsoHasY(Protocol): + y: str + +class A: ... +class B: ... + +static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) +``` + +Protocols are considered equivalent if their members are equivalent, even if those members are +differently ordered unions: + +```py +class C: ... + +class UnionProto1(Protocol): + x: A | B | C + +class UnionProto2(Protocol): + x: C | A | B + +static_assert(is_equivalent_to(UnionProto1, UnionProto2)) +static_assert(is_equivalent_to(UnionProto1 | A | B, B | UnionProto2 | A)) +``` + +## Intersections of protocols + +An intersection of two protocol types `X` and `Y` is equivalent to a protocol type `Z` that inherits +from both `X` and `Y`: + +```py +from typing import Protocol +from ty_extensions import Intersection, static_assert, is_equivalent_to + +class HasX(Protocol): + x: int + +class HasY(Protocol): + y: str + +class HasXAndYProto(HasX, HasY, Protocol): ... + +# TODO: this should pass +static_assert(is_equivalent_to(HasXAndYProto, Intersection[HasX, HasY])) # error: [static-assert-error] +``` + +But this is only true if the subclass has `Protocol` in its explicit bases (otherwise, it is a +nominal type rather than a structural type): + +```py +class HasXAndYNominal(HasX, HasY): ... + +static_assert(not is_equivalent_to(HasXAndYNominal, Intersection[HasX, HasY])) +``` + +A protocol type `X` and a nominal type `Y` can be inferred as disjoint types if `Y` is a `@final` +type and `Y` does not satisfy the interface declared by `X`. But if `Y` is not `@final`, then this +does not hold true, since a subclass of `Y` could always provide additional methods or attributes +that would lead to it satisfying `X`'s interface: + +```py +from typing import final +from ty_extensions import is_disjoint_from + +class NotFinalNominal: ... + +@final +class FinalNominal: ... + +static_assert(not is_disjoint_from(NotFinalNominal, HasX)) +static_assert(is_disjoint_from(FinalNominal, HasX)) + +def _(arg1: Intersection[HasX, NotFinalNominal], arg2: Intersection[HasX, FinalNominal]): + reveal_type(arg1) # revealed: HasX & NotFinalNominal + reveal_type(arg2) # revealed: Never +``` + +The disjointness of a single protocol member with the type of an attribute on another type is enough +to make the whole protocol disjoint from the other type, even if all other members on the protocol +are satisfied by the other type. This applies to both `@final` types and non-final types: + +```py +class Proto(Protocol): + x: int + y: str + z: bytes + +class Foo: + x: int + y: str + z: None + +static_assert(is_disjoint_from(Proto, Foo)) + +@final +class FinalFoo: + x: int + y: str + z: None + +static_assert(is_disjoint_from(Proto, FinalFoo)) +``` + +## Intersections of protocols with types that have possibly unbound attributes + +Note that if a `@final` class has a possibly unbound attribute corresponding to the protocol member, +instance types and class-literal types referring to that class cannot be a subtype of the protocol +but will also not be disjoint from the protocol: + +`a.py`: + +```py +from typing import final, ClassVar, Protocol +from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to + +def who_knows() -> bool: + return False + +@final +class Foo: + if who_knows(): + x: ClassVar[int] = 42 + +class HasReadOnlyX(Protocol): + @property + def x(self) -> int: ... + +static_assert(not is_subtype_of(Foo, HasReadOnlyX)) +static_assert(not is_assignable_to(Foo, HasReadOnlyX)) +static_assert(not is_disjoint_from(Foo, HasReadOnlyX)) + +static_assert(not is_subtype_of(type[Foo], HasReadOnlyX)) +static_assert(not is_assignable_to(type[Foo], HasReadOnlyX)) +static_assert(not is_disjoint_from(type[Foo], HasReadOnlyX)) + +static_assert(not is_subtype_of(TypeOf[Foo], HasReadOnlyX)) +static_assert(not is_assignable_to(TypeOf[Foo], HasReadOnlyX)) +static_assert(not is_disjoint_from(TypeOf[Foo], HasReadOnlyX)) +``` + +A similar principle applies to module-literal types that have possibly unbound attributes: + +`b.py`: + +```py +def who_knows() -> bool: + return False + +if who_knows(): + x: int = 42 +``` + +`c.py`: + +```py +import b +from a import HasReadOnlyX +from ty_extensions import TypeOf, static_assert, is_subtype_of, is_disjoint_from, is_assignable_to + +static_assert(not is_subtype_of(TypeOf[b], HasReadOnlyX)) +static_assert(not is_assignable_to(TypeOf[b], HasReadOnlyX)) +static_assert(not is_disjoint_from(TypeOf[b], HasReadOnlyX)) +``` + +If the possibly unbound attribute's type is disjoint from the type of the protocol member, though, +it is still disjoint from the protocol. This applies to both `@final` types and non-final types: + +`d.py`: + +```py +from a import HasReadOnlyX, who_knows +from typing import final, ClassVar, Protocol +from ty_extensions import static_assert, is_disjoint_from, TypeOf + +class Proto(Protocol): + x: int + +class Foo: + def __init__(self): + if who_knows(): + self.x: None = None + +@final +class FinalFoo: + def __init__(self): + if who_knows(): + self.x: None = None + +static_assert(is_disjoint_from(Foo, Proto)) +static_assert(is_disjoint_from(FinalFoo, Proto)) +``` + +## Satisfying a protocol's interface + +A type does not have to be an `Instance` type in order to be a subtype of a protocol. Other +protocols can be a subtype of a protocol, as can `ModuleLiteral` types, `ClassLiteral` types, and +others. Another protocol can be a subtype of `HasX` either through "explicit" (nominal) inheritance +from `HasX`, or by specifying a superset of `HasX`'s interface: + +`module.py`: + +```py +x: int = 42 +``` + +`main.py`: + +```py +import module +from typing import Protocol +from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf + +class HasX(Protocol): + x: int + +static_assert(is_subtype_of(TypeOf[module], HasX)) +static_assert(is_assignable_to(TypeOf[module], HasX)) + +class ExplicitProtocolSubtype(HasX, Protocol): + y: int + +static_assert(is_subtype_of(ExplicitProtocolSubtype, HasX)) +static_assert(is_assignable_to(ExplicitProtocolSubtype, HasX)) + +class ImplicitProtocolSubtype(Protocol): + x: int + y: str + +static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) +static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) + +class Meta(type): + x: int + +class UsesMeta(metaclass=Meta): ... + +# TODO: these should pass +static_assert(is_subtype_of(UsesMeta, HasX)) # error: [static-assert-error] +static_assert(is_assignable_to(UsesMeta, HasX)) # error: [static-assert-error] +``` + +## `ClassVar` attribute members + +If a protocol `ClassVarX` has a `ClassVar` attribute member `x` with type `int`, this indicates that +a readable `x` attribute must be accessible on any inhabitant of `ClassVarX`, and that a readable +`x` attribute must *also* be accessible on the *type* of that inhabitant: + +`classvars.py`: + +```py +from typing import ClassVar, Protocol +from ty_extensions import is_subtype_of, is_assignable_to, static_assert + +class ClassVarXProto(Protocol): + x: ClassVar[int] + +def f(obj: ClassVarXProto): + reveal_type(obj.x) # revealed: int + reveal_type(type(obj).x) # revealed: int + obj.x = 42 # error: [invalid-attribute-access] "Cannot assign to ClassVar `x` from an instance of type `ClassVarXProto`" + +class InstanceAttrX: + x: int + +# TODO: these should pass +static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error] +static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error] + +class PropertyX: + @property + def x(self) -> int: + return 42 + +# TODO: these should pass +static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) # error: [static-assert-error] +static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) # error: [static-assert-error] + +class ClassVarX: + x: ClassVar[int] = 42 + +static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) +static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) +``` + +This is mentioned by the +[spec](https://typing.python.org/en/latest/spec/protocol.html#protocol-members) and tested in the +[conformance suite](https://github.com/python/typing/blob/main/conformance/tests/protocols_definition.py) +as something that must be supported by type checkers: + +> To distinguish between protocol class variables and protocol instance variables, the special +> `ClassVar` annotation should be used. + +## Subtyping of protocols with property members + +A read-only property on a protocol can be satisfied by a mutable attribute, a read-only property, a +read/write property, a `Final` attribute, or a `ClassVar` attribute: + +```py +from typing import ClassVar, Final, Protocol +from ty_extensions import is_subtype_of, is_assignable_to, static_assert + +class HasXProperty(Protocol): + @property + def x(self) -> int: ... + +class XAttr: + x: int + +static_assert(is_subtype_of(XAttr, HasXProperty)) +static_assert(is_assignable_to(XAttr, HasXProperty)) + +class XReadProperty: + @property + def x(self) -> int: + return 42 + +static_assert(is_subtype_of(XReadProperty, HasXProperty)) +static_assert(is_assignable_to(XReadProperty, HasXProperty)) + +class XReadWriteProperty: + @property + def x(self) -> int: + return 42 + + @x.setter + def x(self, val: int) -> None: ... + +static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) +static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) + +class XClassVar: + x: ClassVar[int] = 42 + +static_assert(is_subtype_of(XClassVar, HasXProperty)) +static_assert(is_assignable_to(XClassVar, HasXProperty)) + +class XFinal: + x: Final = 42 + +static_assert(is_subtype_of(XFinal, HasXProperty)) +static_assert(is_assignable_to(XFinal, HasXProperty)) +``` + +A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below +example satisfies the `HasXProperty` interface even though the type of the `x` attribute on `XSub` +is a subtype of `int` rather than being exactly `int`. + +```py +class MyInt(int): ... + +class XSub: + x: MyInt + +static_assert(is_subtype_of(XSub, HasXProperty)) +static_assert(is_assignable_to(XSub, HasXProperty)) +``` + +A read/write property on a protocol, where the getter returns the same type that the setter takes, +is equivalent to a normal mutable attribute on a protocol. + +```py +class HasMutableXProperty(Protocol): + @property + def x(self) -> int: ... + @x.setter + def x(self, val: int) -> None: ... + +class XAttr: + x: int + +static_assert(is_subtype_of(XAttr, HasXProperty)) +static_assert(is_assignable_to(XAttr, HasXProperty)) + +class XReadProperty: + @property + def x(self) -> int: + return 42 + +# TODO: these should pass +static_assert(not is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error] + +class XReadWriteProperty: + @property + def x(self) -> int: + return 42 + + @x.setter + def x(self, val: int) -> None: ... + +static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) +static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) + +class XSub: + x: MyInt + +# TODO: should pass +static_assert(not is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error] +``` + +A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable +attribute `x`. Both are subtypes of a protocol with a read-only prooperty `x`: + +```py +from ty_extensions import is_equivalent_to + +class HasMutableXAttr(Protocol): + x: int + +# TODO: should pass +static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error] + +static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) +static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) + +static_assert(is_subtype_of(HasMutableXProperty, HasXProperty)) +static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) +``` + +A read/write property on a protocol, where the setter accepts a subtype of the type returned by the +getter, can be satisfied by a mutable attribute of any type bounded by the upper bound of the +getter-returned type and the lower bound of the setter-accepted type. + +This follows from the principle that a type `X` can only be a subtype of a given protocol if the +`X`'s behaviour is a superset of the behaviour specified by the interface declared by the protocol. +In the below example, the behaviour of an instance of `XAttr` is a superset of the behaviour +specified by the protocol `HasAsymmetricXProperty`. The protocol specifies that reading an `x` +attribute on the instance must resolve to an instance of `int` or a subclass thereof, and `XAttr` +satisfies this requirement. The protocol also specifies that you must be able to assign instances of +`MyInt` to the `x` attribute, and again this is satisfied by `XAttr`: on instances of `XAttr`, you +can assign *any* instance of `int` to the `x` attribute, and thus by extension you can assign any +instance of `IntSub` to the `x` attribute, since any instance of `IntSub` is an instance of `int`: + +```py +class HasAsymmetricXProperty(Protocol): + @property + def x(self) -> int: ... + @x.setter + def x(self, val: MyInt) -> None: ... + +class XAttr: + x: int + +static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) +``` + +The end conclusion of this is that the getter-returned type of a property is always covariant and +the setter-accepted type is always contravariant. The combination of these leads to invariance for a +regular mutable attribute, where the implied getter-returned and setter-accepted types are the same. + +```py +class XAttrSub: + x: MyInt + +static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) + +class MyIntSub(MyInt): + pass + +class XAttrSubSub: + x: MyIntSub + +# TODO: should pass +static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error] +``` + +An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal +class whose getter and setter types satisfy the covariant and contravariant requirements, +respectively. + +```py +class XAsymmetricProperty: + @property + def x(self) -> MyInt: + return MyInt(0) + + @x.setter + def x(self, x: int) -> None: ... + +static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) +``` + +A custom descriptor attribute on the nominal class will also suffice: + +```py +class Descriptor: + def __get__(self, instance, owner) -> MyInt: + return MyInt(0) + + def __set__(self, value: int) -> None: ... + +class XCustomDescriptor: + x: Descriptor = Descriptor() + +static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) +``` + +Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a +`__getattr__` method returning a suitable type. A read/write property can be satisfied by a nominal +class that defines a `__getattr__` method returning a suitable type *and* a `__setattr__` method +accepting a suitable type: + +```py +class HasGetAttr: + def __getattr__(self, attr: str) -> int: + return 42 + +static_assert(is_subtype_of(HasGetAttr, HasXProperty)) +static_assert(is_assignable_to(HasGetAttr, HasXProperty)) + +# TODO: these should pass +static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error] +static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error] + +class HasGetAttrWithUnsuitableReturn: + def __getattr__(self, attr: str) -> tuple[int, int]: + return (1, 2) + +# TODO: these should pass +static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error] + +class HasGetAttrAndSetAttr: + def __getattr__(self, attr: str) -> MyInt: + return MyInt(0) + + def __setattr__(self, attr: str, value: int) -> None: ... + +static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) +static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) + +# TODO: these should pass +static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] +static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] +``` + +## Subtyping of protocols with method members + +A protocol can have method members. `T` is assignable to `P` in the following example because the +class `T` has a method `m` which is assignable to the `Callable` supertype of the method `P.m`: + +```py +from typing import Protocol +from ty_extensions import is_subtype_of, static_assert + +class P(Protocol): + def m(self, x: int, /) -> None: ... + +class NominalSubtype: + def m(self, y: int) -> None: ... + +class NotSubtype: + def m(self, x: int) -> int: + return 42 + +static_assert(is_subtype_of(NominalSubtype, P)) + +# TODO: should pass +static_assert(not is_subtype_of(NotSubtype, P)) # error: [static-assert-error] +``` + +## Equivalence of protocols with method members + +Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the +signature of `P1.x` is equivalent to the signature of `P2.x`, even though ty would normally model +any two function definitions as inhabiting distinct function-literal types. + +```py +from typing import Protocol +from ty_extensions import is_equivalent_to, static_assert + +class P1(Protocol): + def x(self, y: int) -> None: ... + +class P2(Protocol): + def x(self, y: int) -> None: ... + +static_assert(is_equivalent_to(P1, P2)) +``` + +As with protocols that only have non-method members, this also holds true when they appear in +differently ordered unions: + +```py +class A: ... +class B: ... + +static_assert(is_equivalent_to(A | B | P1, P2 | B | A)) +``` + +## Narrowing of protocols + + + +By default, a protocol class cannot be used as the second argument to `isinstance()` or +`issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the +type inside these branches (this matches the behaviour of other type checkers): + +```py +from typing_extensions import Protocol, reveal_type + +class HasX(Protocol): + x: int + +def f(arg: object, arg2: type): + if isinstance(arg, HasX): # error: [invalid-argument-type] + reveal_type(arg) # revealed: HasX + else: + reveal_type(arg) # revealed: ~HasX + + if issubclass(arg2, HasX): # error: [invalid-argument-type] + reveal_type(arg2) # revealed: type[HasX] + else: + reveal_type(arg2) # revealed: type & ~type[HasX] +``` + +A protocol class decorated with `@typing(_extensions).runtime_checkable` *can* be used as the second +argument to `isisinstance()` at runtime: + +```py +from typing import runtime_checkable + +@runtime_checkable +class RuntimeCheckableHasX(Protocol): + x: int + +def f(arg: object): + if isinstance(arg, RuntimeCheckableHasX): # no error! + reveal_type(arg) # revealed: RuntimeCheckableHasX + else: + reveal_type(arg) # revealed: ~RuntimeCheckableHasX +``` + +but in order for a protocol class to be used as the second argument to `issubclass()`, it must +satisfy two conditions: + +1. It must be decorated with `@runtime_checkable` +1. It must *only* have method members (protocols with attribute members are not permitted) + +```py +@runtime_checkable +class OnlyMethodMembers(Protocol): + def method(self) -> None: ... + +def f(arg1: type, arg2: type): + if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) + reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] + else: + reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] + + if issubclass(arg2, OnlyMethodMembers): # no error! + reveal_type(arg2) # revealed: type[OnlyMethodMembers] + else: + reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] +``` + +## Truthiness of protocol instance + +An instance of a protocol type generally has ambiguous truthiness: + +```py +from typing import Protocol + +class Foo(Protocol): + x: int + +def f(foo: Foo): + reveal_type(bool(foo)) # revealed: bool +``` + +But this is not the case if the protocol has a `__bool__` method member that returns `Literal[True]` +or `Literal[False]`: + +```py +from typing import Literal + +class Truthy(Protocol): + def __bool__(self) -> Literal[True]: ... + +class FalsyFoo(Foo, Protocol): + def __bool__(self) -> Literal[False]: ... + +class FalsyFooSubclass(FalsyFoo, Protocol): + y: str + +def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass): + reveal_type(bool(a)) # revealed: Literal[True] + reveal_type(bool(b)) # revealed: Literal[False] + reveal_type(bool(c)) # revealed: Literal[False] +``` + +The same works with a class-level declaration of `__bool__`: + +```py +from typing import Callable + +class InstanceAttrBool(Protocol): + __bool__: Callable[[], Literal[True]] + +def h(obj: InstanceAttrBool): + reveal_type(bool(obj)) # revealed: Literal[True] +``` + +## Callable protocols + +An instance of a protocol type is callable if the protocol defines a `__call__` method: + +```py +from typing import Protocol + +class CallMeMaybe(Protocol): + def __call__(self, x: int) -> str: ... + +def f(obj: CallMeMaybe): + reveal_type(obj(42)) # revealed: str + obj("bar") # error: [invalid-argument-type] +``` + +An instance of a protocol like this can be assignable to a `Callable` type, but only if it has the +right signature: + +```py +from typing import Callable +from ty_extensions import is_subtype_of, is_assignable_to, static_assert + +static_assert(is_subtype_of(CallMeMaybe, Callable[[int], str])) +static_assert(is_assignable_to(CallMeMaybe, Callable[[int], str])) +static_assert(not is_subtype_of(CallMeMaybe, Callable[[str], str])) +static_assert(not is_assignable_to(CallMeMaybe, Callable[[str], str])) +static_assert(not is_subtype_of(CallMeMaybe, Callable[[CallMeMaybe, int], str])) +static_assert(not is_assignable_to(CallMeMaybe, Callable[[CallMeMaybe, int], str])) + +def g(obj: Callable[[int], str], obj2: CallMeMaybe, obj3: Callable[[str], str]): + obj = obj2 + obj3 = obj2 # error: [invalid-assignment] +``` + +By the same token, a `Callable` type can also be assignable to a protocol-instance type if the +signature implied by the `Callable` type is assignable to the signature of the `__call__` method +specified by the protocol: + +```py +from ty_extensions import TypeOf + +class Foo(Protocol): + def __call__(self, x: int, /) -> str: ... + +static_assert(is_subtype_of(Callable[[int], str], Foo)) +static_assert(is_assignable_to(Callable[[int], str], Foo)) + +# TODO: these should pass +static_assert(not is_subtype_of(Callable[[str], str], Foo)) # error: [static-assert-error] +static_assert(not is_assignable_to(Callable[[str], str], Foo)) # error: [static-assert-error] +static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error] +static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error] + +def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]): + obj2 = obj + + # TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable + # to the declared type of `obj2` + obj2 = obj3 + +def satisfies_foo(x: int) -> str: + return "foo" + +static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) +static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo)) +``` + +## Protocols are never singleton types, and are never single-valued types + +It *might* be possible to have a singleton protocol-instance type...? + +For example, `WeirdAndWacky` in the following snippet only has a single possible inhabitant: `None`! +It is thus a singleton type. However, going out of our way to recognise it as such is probably not +worth it. Such cases should anyway be exceedingly rare and/or contrived. + +```py +from typing import Protocol, Callable +from ty_extensions import is_singleton, is_single_valued + +class WeirdAndWacky(Protocol): + @property + def __class__(self) -> Callable[[], None]: ... + +reveal_type(is_singleton(WeirdAndWacky)) # revealed: Literal[False] +reveal_type(is_single_valued(WeirdAndWacky)) # revealed: Literal[False] +``` + +## Integration test: `typing.SupportsIndex` and `typing.Sized` + +`typing.SupportsIndex` and `typing.Sized` are two protocols that are very commonly used in the wild. + +```py +from typing import SupportsIndex, Sized, Literal + +def one(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex): + a: SupportsIndex = some_int + b: SupportsIndex = some_literal_int + c: SupportsIndex = some_indexable + +def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): + a: Sized = some_list + b: Sized = some_tuple + c: Sized = some_sized +``` + +## Recursive protocols + +### Properties + +```py +from __future__ import annotations + +from typing import Protocol, Any, TypeVar +from ty_extensions import static_assert, is_assignable_to, is_subtype_of, is_equivalent_to + +class RecursiveFullyStatic(Protocol): + parent: RecursiveFullyStatic + x: int + +class RecursiveNonFullyStatic(Protocol): + parent: RecursiveNonFullyStatic + x: Any + +# TODO: these should pass, once we take into account types of members +static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic)) # error: [static-assert-error] +static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic)) # error: [static-assert-error] + +static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic)) +static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic)) +static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveFullyStatic)) + +class AlsoRecursiveFullyStatic(Protocol): + parent: AlsoRecursiveFullyStatic + x: int + +static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic)) + +class RecursiveOptionalParent(Protocol): + parent: RecursiveOptionalParent | None + +static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent)) + +static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent)) +static_assert(not is_assignable_to(RecursiveOptionalParent, RecursiveNonFullyStatic)) + +class Other(Protocol): + z: str + +def _(rec: RecursiveFullyStatic, other: Other): + reveal_type(rec.parent.parent.parent) # revealed: RecursiveFullyStatic + + rec.parent.parent.parent = rec + rec = rec.parent.parent.parent + + rec.parent.parent.parent = other # error: [invalid-assignment] + other = rec.parent.parent.parent # error: [invalid-assignment] + +class Foo(Protocol): + @property + def x(self) -> "Foo": ... + +class Bar(Protocol): + @property + def x(self) -> "Bar": ... + +# TODO: this should pass +# error: [static-assert-error] +static_assert(is_equivalent_to(Foo, Bar)) + +T = TypeVar("T", bound="TypeVarRecursive") + +class TypeVarRecursive(Protocol): + # TODO: commenting this out will cause a stack overflow. + # x: T + y: "TypeVarRecursive" + +def _(t: TypeVarRecursive): + # reveal_type(t.x) # revealed: T + reveal_type(t.y) # revealed: TypeVarRecursive +``` + +### Nested occurrences of self-reference + +Make sure that we handle self-reference correctly, even if the self-reference appears deeply nested +within the type of a protocol member: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +from typing import Protocol, Callable +from ty_extensions import Intersection, Not, is_assignable_to, is_equivalent_to, static_assert + +class C: ... + +class GenericC[T](Protocol): + pass + +class Recursive(Protocol): + direct: Recursive + + union: None | Recursive + + intersection1: Intersection[C, Recursive] + intersection2: Intersection[C, Not[Recursive]] + + t: tuple[int, tuple[str, Recursive]] + + callable1: Callable[[int], Recursive] + callable2: Callable[[Recursive], int] + + subtype_of: type[Recursive] + + generic: GenericC[Recursive] + + def method(self, x: Recursive) -> Recursive: ... + + nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive] + +static_assert(is_equivalent_to(Recursive, Recursive)) +static_assert(is_assignable_to(Recursive, Recursive)) + +def _(r: Recursive): + reveal_type(r.direct) # revealed: Recursive + reveal_type(r.union) # revealed: None | Recursive + reveal_type(r.intersection1) # revealed: C & Recursive + # revealed: @Todo(map_with_boundness: intersections with negative contributions) | (C & ~Recursive) + reveal_type(r.intersection2) + reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]] + reveal_type(r.callable1) # revealed: (int, /) -> Recursive + reveal_type(r.callable2) # revealed: (Recursive, /) -> int + reveal_type(r.subtype_of) # revealed: type[Recursive] + reveal_type(r.generic) # revealed: GenericC[Recursive] + reveal_type(r.method(r)) # revealed: Recursive + reveal_type(r.nested) # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> Recursive) + + reveal_type(r.method(r).callable1(1).direct.t[1][1]) # revealed: Recursive +``` + +### Mutually-recursive protocols + +```py +from typing import Protocol +from ty_extensions import is_equivalent_to, static_assert + +class Foo(Protocol): + x: "Bar" + +class Bar(Protocol): + x: Foo + +static_assert(is_equivalent_to(Foo, Bar)) +``` + +### Disjointness of recursive protocol and recursive final type + +```py +from typing import Protocol +from ty_extensions import is_disjoint_from, static_assert + +class Proto(Protocol): + x: "Proto" + +class Nominal: + x: "Nominal" + +static_assert(not is_disjoint_from(Proto, Nominal)) +``` + +### Regression test: narrowing with self-referential protocols + +This snippet caused us to panic on an early version of the implementation for protocols. + +```py +from typing import Protocol + +class A(Protocol): + def x(self) -> "B | A": ... + +class B(Protocol): + def y(self): ... + +obj = something_unresolvable # error: [unresolved-reference] +reveal_type(obj) # revealed: Unknown +if isinstance(obj, (B, A)): + reveal_type(obj) # revealed: (Unknown & B) | (Unknown & A) +``` + +### Protocols that use `Self` + +`Self` is a `TypeVar` with an upper bound of the class in which it is defined. This means that +`Self` annotations in protocols can also be tricky to handle without infinite recursion and stack +overflows. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import Protocol, Self +from ty_extensions import static_assert + +class _HashObject(Protocol): + def copy(self) -> Self: ... + +class Foo: ... + +# Attempting to build this union caused us to overflow on an early version of +# +x: Foo | _HashObject +``` + +Some other similar cases that caused issues in our early `Protocol` implementation: + +`a.py`: + +```py +from typing_extensions import Protocol, Self + +class PGconn(Protocol): + def connect(self) -> Self: ... + +class Connection: + pgconn: PGconn + +def is_crdb(conn: PGconn) -> bool: + return isinstance(conn, Connection) +``` + +and: + +`b.py`: + +```py +from typing_extensions import Protocol + +class PGconn(Protocol): + def connect[T: PGconn](self: T) -> T: ... + +class Connection: + pgconn: PGconn + +def f(x: PGconn): + isinstance(x, Connection) +``` + +### Recursive protocols used as the first argument to `cast()` + +These caused issues in an early version of our `Protocol` implementation due to the fact that we use +a recursive function in our `cast()` implementation to check whether a type contains `Unknown` or +`Todo`. Recklessly recursing into a type causes stack overflows if the type is recursive: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import cast, Protocol + +class Iterator[T](Protocol): + def __iter__(self) -> Iterator[T]: ... + +def f(value: Iterator): + cast(Iterator, value) # error: [redundant-cast] +``` + +## TODO + +Add tests for: + +- More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec]. +- Protocols with instance-method members, including: + - Protocols with methods that have parameters or the return type unannotated + - Protocols with methods that have parameters or the return type annotated with `Any` +- Protocols with `@classmethod` and `@staticmethod` +- Assignability of non-instance types to protocols with instance-method members (e.g. a + class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method) +- Protocols with methods that have annotated `self` parameters. + [Spec reference][self_types_protocols_spec]. +- Protocols with overloaded method members +- `super()` on nominal subtypes (explicit and implicit) of protocol classes +- [Recursive protocols][recursive_protocols_spec] +- Generic protocols +- Non-generic protocols with function-scoped generic methods +- Protocols with instance attributes annotated with `Callable` (can a nominal type with a method + satisfy that protocol, and if so in what cases?) +- Protocols decorated with `@final` +- Equivalence and subtyping between `Callable` types and protocols that define `__call__` + +[mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping +[mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test +[protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests +[protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols +[recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols +[self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols +[spec_protocol_members]: https://typing.python.org/en/latest/spec/protocol.html#protocol-members +[typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html diff --git a/crates/ty_python_semantic/resources/mdtest/public_types.md b/crates/ty_python_semantic/resources/mdtest/public_types.md new file mode 100644 index 0000000000000..15536f1d04298 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/public_types.md @@ -0,0 +1,423 @@ +# Public types + +## Basic + +The "public type" of a symbol refers to the type that is inferred in a nested scope for a symbol +defined in an outer enclosing scope. Since it is not generally possible to analyze the full control +flow of a program, we currently make the simplifying assumption that an inner scope (such as the +`inner` function below) could be executed at any position in the enclosing scope. The public type +should therefore be the union of all possible types that the symbol could have. + +In the following example, depending on when `inner()` is called, the type of `x` could either be `A` +or `B`: + +```py +class A: ... +class B: ... +class C: ... + +def outer() -> None: + x = A() + + def inner() -> None: + # TODO: We might ideally be able to eliminate `Unknown` from the union here since `x` resolves to an + # outer scope that is a function scope (as opposed to module global scope), and `x` is never declared + # nonlocal in a nested scope that also assigns to it. + reveal_type(x) # revealed: Unknown | A | B + # This call would observe `x` as `A`. + inner() + + x = B() + + # This call would observe `x` as `B`. + inner() +``` + +Similarly, if control flow in the outer scope can split, the public type of `x` should reflect that: + +```py +def outer(flag: bool) -> None: + x = A() + + def inner() -> None: + reveal_type(x) # revealed: Unknown | A | B | C + inner() + + if flag: + x = B() + + inner() + else: + x = C() + + inner() + + inner() +``` + +If a binding is not reachable, it is not considered in the public type: + +```py +def outer() -> None: + x = A() + + def inner() -> None: + reveal_type(x) # revealed: Unknown | A | C + inner() + + if False: + x = B() # this binding of `x` is unreachable + inner() + + x = C() + inner() + +def outer(flag: bool) -> None: + x = A() + + def inner() -> None: + reveal_type(x) # revealed: Unknown | A | C + inner() + + if flag: + return + + x = B() # this binding of `x` is unreachable + + x = C() + inner() +``` + +If a symbol is only conditionally bound, we do not raise any errors: + +```py +def outer(flag: bool) -> None: + if flag: + x = A() + + def inner() -> None: + reveal_type(x) # revealed: Unknown | A + inner() +``` + +In the future, we may try to be smarter about which bindings must or must not be a visible to a +given nested scope, depending where it is defined. In the above case, this shouldn't change the +behavior -- `x` is defined before `inner` in the same branch, so should be considered +definitely-bound for `inner`. But in other cases we may want to emit `possibly-unresolved-reference` +in future: + +```py +def outer(flag: bool) -> None: + if flag: + x = A() + + def inner() -> None: + # TODO: Ideally, we would emit a possibly-unresolved-reference error here. + reveal_type(x) # revealed: Unknown | A + inner() +``` + +The public type is available, even if the end of the outer scope is unreachable. This is a +regression test. A previous version of ty used the end-of-scope position to determine the public +type, which would have resulted in incorrect type inference here: + +```py +def outer() -> None: + x = A() + + def inner() -> None: + reveal_type(x) # revealed: Unknown | A + inner() + + return + # unreachable + +def outer(flag: bool) -> None: + x = A() + + def inner() -> None: + reveal_type(x) # revealed: Unknown | A | B + if flag: + x = B() + inner() + return + # unreachable + + inner() + +def outer(x: A) -> None: + def inner() -> None: + reveal_type(x) # revealed: A + raise +``` + +An arbitrary level of nesting is supported: + +```py +def f0() -> None: + x = A() + + def f1() -> None: + def f2() -> None: + def f3() -> None: + def f4() -> None: + reveal_type(x) # revealed: Unknown | A | B + f4() + f3() + f2() + f1() + + x = B() + + f1() +``` + +## At module level + +The behavior is the same if the outer scope is the global scope of a module: + +```py +def flag() -> bool: + return True + +if flag(): + x = 1 + + def f() -> None: + reveal_type(x) # revealed: Unknown | Literal[1, 2] + # Function only used inside this branch + f() + + x = 2 + + # Function only used inside this branch + f() +``` + +## Mixed declarations and bindings + +When a declaration only appears in one branch, we also consider the types of the symbol's bindings +in other branches: + +```py +def flag() -> bool: + return True + +if flag(): + A: str = "" +else: + A = None + +reveal_type(A) # revealed: Literal[""] | None + +def _(): + reveal_type(A) # revealed: str | None +``` + +This pattern appears frequently with conditional imports. The `import` statement is both a +declaration and a binding, but we still add `None` to the public type union in a situation like +this: + +```py +try: + import optional_dependency # ty: ignore +except ImportError: + optional_dependency = None + +reveal_type(optional_dependency) # revealed: Unknown | None + +def _(): + reveal_type(optional_dependency) # revealed: Unknown | None +``` + +## Limitations + +### Type narrowing + +We currently do not further analyze control flow, so we do not support cases where the inner scope +is only executed in a branch where the type of `x` is narrowed: + +```py +class A: ... + +def outer(x: A | None): + if x is not None: + def inner() -> None: + # TODO: should ideally be `A` + reveal_type(x) # revealed: A | None + inner() +``` + +### Shadowing + +Similarly, since we do not analyze control flow in the outer scope here, we assume that `inner()` +could be called between the two assignments to `x`: + +```py +def outer() -> None: + def inner() -> None: + # TODO: this should ideally be `Unknown | Literal[1]`, but no other type checker supports this either + reveal_type(x) # revealed: Unknown | None | Literal[1] + x = None + + # [additional code here] + + x = 1 + + inner() +``` + +This is currently even true if the `inner` function is only defined after the second assignment to +`x`: + +```py +def outer() -> None: + x = None + + # [additional code here] + + x = 1 + + def inner() -> None: + # TODO: this should be `Unknown | Literal[1]`. Mypy and pyright support this. + reveal_type(x) # revealed: Unknown | None | Literal[1] + inner() +``` + +A similar case derived from an ecosystem example, involving declared types: + +```py +class C: ... + +def outer(x: C | None): + x = x or C() + + reveal_type(x) # revealed: C + + def inner() -> None: + # TODO: this should ideally be `C` + reveal_type(x) # revealed: C | None + inner() +``` + +### Assignments to nonlocal variables + +Writes to the outer-scope variable are currently not detected: + +```py +def outer() -> None: + x = None + + def set_x() -> None: + nonlocal x + x = 1 + set_x() + + def inner() -> None: + # TODO: this should ideally be `Unknown | None | Literal[1]`. Mypy and pyright support this. + reveal_type(x) # revealed: Unknown | None + inner() +``` + +## Handling of overloads + +### With implementation + +Overloads need special treatment, because here, we do not want to consider *all* possible +definitions of `f`. This would otherwise result in a union of all three definitions of `f`: + +```py +from typing import overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... +def f(x: int | str) -> int | str: + raise NotImplementedError + +reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] + +def _(): + reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] +``` + +This also works if there are conflicting declarations: + +```py +def flag() -> bool: + return True + +if flag(): + @overload + def g(x: int) -> int: ... + @overload + def g(x: str) -> str: ... + def g(x: int | str) -> int | str: + return x + +else: + g: str = "" + +def _(): + reveal_type(g) # revealed: (Overload[(x: int) -> int, (x: str) -> str]) | str + +# error: [conflicting-declarations] +g = "test" +``` + +### Without an implementation + +Similarly, if there is no implementation, we only consider the last overload definition. + +```pyi +from typing import overload + +@overload +def f(x: int) -> int: ... +@overload +def f(x: str) -> str: ... + +reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] + +def _(): + reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] +``` + +This also works if there are conflicting declarations: + +```pyi +def flag() -> bool: + return True + +if flag(): + @overload + def g(x: int) -> int: ... + @overload + def g(x: str) -> str: ... +else: + g: str + +def _(): + reveal_type(g) # revealed: (Overload[(x: int) -> int, (x: str) -> str]) | str +``` + +### Overload only defined in one branch + +```py +from typing import overload + +def flag() -> bool: + return True + +if flag(): + @overload + def f(x: int) -> int: ... + @overload + def f(x: str) -> str: ... + def f(x: int | str) -> int | str: + raise NotImplementedError + + def _(): + reveal_type(f) # revealed: Overload[(x: int) -> int, (x: str) -> str] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md b/crates/ty_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md rename to crates/ty_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md new file mode 100644 index 0000000000000..0df01f788364a --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md @@ -0,0 +1,36 @@ +# Builtin scope + +## Conditional local override of builtin + +If a builtin name is conditionally shadowed by a local variable, a name lookup should union the +builtin type with the conditionally-defined type: + +```py +def _(flag: bool) -> None: + if flag: + abs = 1 + chr: int = 1 + + reveal_type(abs) # revealed: Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T) + reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str) +``` + +## Conditionally global override of builtin + +If a builtin name is conditionally shadowed by a global variable, a name lookup should union the +builtin type with the conditionally-defined type: + +```py +def flag() -> bool: + return True + +if flag(): + abs = 1 + chr: int = 1 + +def _(): + # TODO: Should ideally be `Unknown | Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)` + reveal_type(abs) # revealed: Unknown | Literal[1] + # TODO: Should ideally be `int | (def chr(i: SupportsIndex, /) -> str)` + reveal_type(chr) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md rename to crates/ty_python_semantic/resources/mdtest/scopes/eager.md index 533330ddb72de..9677c8f70d57d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md @@ -12,7 +12,7 @@ Function definitions are evaluated lazily. x = 1 def f(): - reveal_type(x) # revealed: Unknown | Literal[2] + reveal_type(x) # revealed: Unknown | Literal[1, 2] x = 2 ``` @@ -299,7 +299,7 @@ def _(): x = 1 def f(): - # revealed: Unknown | Literal[2] + # revealed: Unknown | Literal[1, 2] [reveal_type(x) for a in range(1)] x = 2 ``` @@ -316,7 +316,7 @@ def _(): class A: def f(): - # revealed: Unknown | Literal[2] + # revealed: Unknown | Literal[1, 2] reveal_type(x) x = 2 @@ -333,7 +333,7 @@ def _(): def f(): def g(): - # revealed: Unknown | Literal[2] + # revealed: Unknown | Literal[1, 2] reveal_type(x) x = 2 ``` @@ -351,7 +351,7 @@ def _(): class A: def f(): - # revealed: Unknown | Literal[2] + # revealed: Unknown | Literal[1, 2] [reveal_type(x) for a in range(1)] x = 2 @@ -389,7 +389,7 @@ x = int class C: var: ClassVar[x] -reveal_type(C.var) # revealed: Unknown | str +reveal_type(C.var) # revealed: Unknown | int | str x = str ``` @@ -404,7 +404,8 @@ x = int class C: var: ClassVar[x] -reveal_type(C.var) # revealed: Unknown | str +# TODO: should ideally be `str`, but we currently consider all reachable bindings +reveal_type(C.var) # revealed: int | str x = str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md b/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md new file mode 100644 index 0000000000000..7163aa69808c3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global-constants.md @@ -0,0 +1,14 @@ +# Global Constants + +## `__debug__` constant + +The [`__debug__` constant] should be globally available: + +```py +reveal_type(__debug__) # revealed: bool + +def foo(): + reveal_type(__debug__) # revealed: bool +``` + +[`__debug__` constant]: https://docs.python.org/3/library/constants.html#debug__ diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md new file mode 100644 index 0000000000000..3e4a884c6d302 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global.md @@ -0,0 +1,213 @@ +# `global` references + +## Implicit global in function + +A name reference to a never-defined symbol in a function is implicitly a global lookup. + +```py +x = 1 + +def f(): + reveal_type(x) # revealed: Unknown | Literal[1] +``` + +## Explicit global in function + +```py +x = 1 + +def f(): + global x + reveal_type(x) # revealed: Unknown | Literal[1] +``` + +## Unassignable type in function + +```py +x: int = 1 + +def f(): + y: int = 1 + # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" + y = "" + + global x + # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" + x = "" + + global z + # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" + z = "" + +z: int +``` + +## Nested intervening scope + +A `global` statement causes lookup to skip any bindings in intervening scopes: + +```py +x: int = 1 + +def outer(): + x: str = "" + + def inner(): + global x + reveal_type(x) # revealed: int +``` + +## Narrowing + +An assignment following a `global` statement should narrow the type in the local scope after the +assignment. + +```py +x: int | None + +def f(): + global x + x = 1 + reveal_type(x) # revealed: Literal[1] +``` + +## `nonlocal` and `global` + +A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython +marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line. + +```py +x = 1 + +def f(): + x = 1 + def g() -> None: + nonlocal x + global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global" + x = None +``` + +## Global declaration after `global` statement + +```py +def f(): + global x + y = x + x = 1 # No error. + +x = 2 +``` + +## Semantic syntax errors + +Using a name prior to its `global` declaration in the same scope is a syntax error. + +```py +def f(): + print(x) + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + print(x) + +def f(): + global x + print(x) + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + print(x) + +def f(): + print(x) + global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" + print(x) + +def f(): + global x, y + print(x) + global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" + print(x) + +def f(): + x = 1 + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + x = 1 + +def f(): + global x + x = 1 + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + x = 1 + +def f(): + del x + global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" + del x + +def f(): + global x, y + del x + global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" + del x + +def f(): + del x + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + del x + +def f(): + global x + del x + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + del x + +def f(): + del x + global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" + del x + +def f(): + global x, y + del x + global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration" + del x + +def f(): + print(f"{x=}") + global x # error: [invalid-syntax] "name `x` is used prior to global declaration" + +# still an error in module scope +x = None +global x # error: [invalid-syntax] "name `x` is used prior to global declaration" +``` + +## Local bindings override preceding `global` bindings + +```py +x = 42 + +def f(): + global x + reveal_type(x) # revealed: Unknown | Literal[42] + x = "56" + reveal_type(x) # revealed: Literal["56"] +``` + +## Local assignment prevents falling back to the outer scope + +```py +x = 42 + +def f(): + # error: [unresolved-reference] "Name `x` used when not defined" + reveal_type(x) # revealed: Unknown + x = "56" + reveal_type(x) # revealed: Literal["56"] +``` + +## Annotating a `global` binding is a syntax error + +```py +x: int = 1 + +def f(): + global x + x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global" +``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md new file mode 100644 index 0000000000000..140a4ac72bacb --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -0,0 +1,219 @@ +# Implicit globals from `types.ModuleType` + +## Implicit `ModuleType` globals + +All modules are instances of `types.ModuleType`. If a name can't be found in any local or global +scope, we look it up as an attribute on `types.ModuleType` in typeshed before deciding that the name +is unbound. + +```py +reveal_type(__name__) # revealed: str +# Typeshed says this is str | None, but for a pure-Python on-disk module its always str +reveal_type(__file__) # revealed: str +reveal_type(__loader__) # revealed: LoaderProtocol | None +reveal_type(__package__) # revealed: str | None +reveal_type(__doc__) # revealed: str | None +reveal_type(__spec__) # revealed: ModuleSpec | None +reveal_type(__path__) # revealed: MutableSequence[str] +reveal_type(__builtins__) # revealed: Any + +import sys + +reveal_type(sys.__builtins__) # revealed: Any + +from builtins import __builtins__ as __bi__ + +reveal_type(__bi__) # revealed: Any + +class X: + reveal_type(__name__) # revealed: str + +def foo(): + reveal_type(__name__) # revealed: str +``` + +However, three attributes on `types.ModuleType` are not present as implicit module globals; these +are excluded: + +```py +# error: [unresolved-reference] +# revealed: Unknown +reveal_type(__getattr__) + +# error: [unresolved-reference] +# revealed: Unknown +reveal_type(__dict__) + +# error: [unresolved-reference] +# revealed: Unknown +reveal_type(__init__) +``` + +## `ModuleType` globals combined with explicit assignments and declarations + +A `ModuleType` attribute can be overridden in the global scope with a different type, but it must be +a type assignable to the declaration on `ModuleType` unless it is accompanied by an explicit +redeclaration: + +`module.py`: + +```py +__file__ = None +__path__: list[str] = [] +__doc__: int # error: [invalid-declaration] "Cannot declare type `int` for inferred type `str | None`" +# error: [invalid-declaration] "Cannot shadow implicit global attribute `__package__` with declaration of type `int`" +__package__: int = 42 +__spec__ = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to `ModuleSpec | None`" +``` + +`main.py`: + +```py +import module + +reveal_type(module.__file__) # revealed: Unknown | None +reveal_type(module.__path__) # revealed: list[str] +reveal_type(module.__doc__) # revealed: Unknown +reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None + +def nested_scope(): + global __loader__ + reveal_type(__loader__) # revealed: LoaderProtocol | None + __loader__ = 56 # error: [invalid-assignment] "Object of type `Literal[56]` is not assignable to `LoaderProtocol | None`" +``` + +## Accessed as attributes + +`ModuleType` attributes can also be accessed as attributes on module-literal types. The special +attributes `__dict__` and `__init__`, and all attributes on `builtins.object`, can also be accessed +as attributes on module-literal types, despite the fact that these are inaccessible as globals from +inside the module: + +```py +import typing + +reveal_type(typing.__name__) # revealed: str +reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ellipsis) -> None + +# For a stub module, we don't know that `__file__` is a string (at runtime it may be entirely +# unset, but we follow typeshed here): +reveal_type(typing.__file__) # revealed: str | None + +# These come from `builtins.object`, not `types.ModuleType`: +reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool + +reveal_type(typing.__class__) # revealed: + +reveal_type(typing.__dict__) # revealed: dict[str, Any] +``` + +Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with +dynamic imports; but we ignore that for module-literal types where we know exactly which module +we're dealing with: + +```py +# error: [unresolved-attribute] +reveal_type(typing.__getattr__) # revealed: Unknown +``` + +## `types.ModuleType.__dict__` takes precedence over global variable `__dict__` + +It's impossible to override the `__dict__` attribute of `types.ModuleType` instances from inside the +module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named +`__dict__` in the module's global namespace: + +`foo.py`: + +```py +__dict__ = "foo" + +reveal_type(__dict__) # revealed: Literal["foo"] +``` + +`bar.py`: + +```py +import foo +from foo import __dict__ as foo_dict + +reveal_type(foo.__dict__) # revealed: dict[str, Any] +reveal_type(foo_dict) # revealed: dict[str, Any] +``` + +## Conditionally global or `ModuleType` attribute + +Attributes overridden in the module namespace take priority. If a builtin name is conditionally +defined as a global, however, a name lookup should union the `ModuleType` type with the +conditionally defined type: + +```py +__file__ = "foo" + +def returns_bool() -> bool: + return True + +if returns_bool(): + __name__ = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `str`" + +reveal_type(__file__) # revealed: Literal["foo"] +reveal_type(__name__) # revealed: str +``` + +## Conditionally global or `ModuleType` attribute, with annotation + +The same is true if the name is annotated: + +```py +# error: [invalid-declaration] "Cannot shadow implicit global attribute `__file__` with declaration of type `int`" +__file__: int = 42 + +def returns_bool() -> bool: + return True + +if returns_bool(): + # error: [invalid-declaration] "Cannot shadow implicit global attribute `__name__` with declaration of type `int`" + __name__: int = 1 + +reveal_type(__file__) # revealed: Literal[42] +reveal_type(__name__) # revealed: Literal[1] | str +``` + +## Implicit global attributes in the current module override implicit globals from builtins + +Here, we take the type of the implicit global symbol `__name__` from the `types.ModuleType` stub +(which in this custom typeshed specifies the type as `bytes`). This is because the `main` module has +an implicit `__name__` global that shadows the builtin `__name__` symbol. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class object: ... +class int: ... +class bytes: ... + +__name__: int = 42 +``` + +`/typeshed/stdlib/types.pyi`: + +```pyi +class ModuleType: + __name__: bytes +``` + +`/typeshed/stdlib/typing_extensions.pyi`: + +```pyi +def reveal_type(obj, /): ... +``` + +`main.py`: + +```py +reveal_type(__name__) # revealed: bytes +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md similarity index 79% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md rename to crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md index 7aa16794c3553..862757ce95e0e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -43,14 +43,3 @@ def f(): def h(): reveal_type(x) # revealed: Unknown | Literal[1] ``` - -## Implicit global in function - -A name reference to a never-defined symbol in a function is implicitly a global lookup. - -```py -x = 1 - -def f(): - reveal_type(x) # revealed: Unknown | Literal[1] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md rename to crates/ty_python_semantic/resources/mdtest/scopes/unbound.md index 6e4d57195adf1..4a15c071e8b07 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md @@ -16,7 +16,7 @@ class C: if flag: x = 2 -# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound" +# error: [possibly-unbound-attribute] "Attribute `x` on type `` is possibly unbound" reveal_type(C.x) # revealed: Unknown | Literal[2] reveal_type(C.y) # revealed: Unknown | Literal[1] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/shadowing/class.md b/crates/ty_python_semantic/resources/mdtest/shadowing/class.md new file mode 100644 index 0000000000000..97550ec57ca84 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/shadowing/class.md @@ -0,0 +1,19 @@ +# Classes shadowing + +## Implicit error + +```py +class C: ... + +C = 1 # error: "Implicit shadowing of class `C`" +``` + +## Explicit + +No diagnostic is raised in the case of explicit shadowing: + +```py +class C: ... + +C: int = 1 +``` diff --git a/crates/ty_python_semantic/resources/mdtest/shadowing/function.md b/crates/ty_python_semantic/resources/mdtest/shadowing/function.md new file mode 100644 index 0000000000000..dda365e13a644 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/shadowing/function.md @@ -0,0 +1,53 @@ +# Function shadowing + +## Parameter + +Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function. +No diagnostics should be generated. + +```py +def f(x: str): + x: int = int(x) +``` + +## Implicit error + +```py +def f(): ... + +f = 1 # error: "Implicit shadowing of function `f`" +``` + +## Explicit shadowing + +```py +def f(): ... + +f: int = 1 +``` + +## Explicit shadowing involving `def` statements + +Since a `def` statement is a declaration, one `def` can shadow another `def`, or shadow a previous +non-`def` declaration, without error. + +```py +f = 1 +reveal_type(f) # revealed: Literal[1] + +def f(): ... + +reveal_type(f) # revealed: def f() -> Unknown + +def f(x: int) -> int: + raise NotImplementedError + +reveal_type(f) # revealed: def f(x: int) -> int + +f: int = 1 +reveal_type(f) # revealed: Literal[1] + +def f(): ... + +reveal_type(f) # revealed: def f() -> Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md b/crates/ty_python_semantic/resources/mdtest/shadowing/variable_declaration.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md rename to crates/ty_python_semantic/resources/mdtest/shadowing/variable_declaration.md diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/all_members.md_-_List_all_members_-_Basic_functionality_(6b9531a70334bfad).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/all_members.md_-_List_all_members_-_Basic_functionality_(6b9531a70334bfad).snap new file mode 100644 index 0000000000000..55a377e0c33cb --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/all_members.md_-_List_all_members_-_Basic_functionality_(6b9531a70334bfad).snap @@ -0,0 +1,44 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: all_members.md - List all members - Basic functionality +mdtest path: crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from ty_extensions import all_members, static_assert + 2 | + 3 | members_of_str = all_members("a") + 4 | + 5 | static_assert("replace" in members_of_str) + 6 | static_assert("startswith" in members_of_str) + 7 | static_assert("isupper" in members_of_str) + 8 | static_assert("__add__" in members_of_str) + 9 | static_assert("__gt__" in members_of_str) +10 | static_assert("__doc__" in members_of_str) +11 | static_assert("__repr__" in members_of_str) +12 | static_assert("non_existent" not in members_of_str) +13 | from typing_extensions import reveal_type +14 | +15 | reveal_type(members_of_str) # error: [revealed-type] +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:15:13 + | +13 | from typing_extensions import reveal_type +14 | +15 | reveal_type(members_of_str) # error: [revealed-type] + | ^^^^^^^^^^^^^^ `tuple[Literal["__add__"], Literal["__annotations__"], Literal["__class__"], Literal["__contains__"], Literal["__delattr__"], Literal["__dict__"], Literal["__dir__"], Literal["__doc__"], Literal["__eq__"], Literal["__format__"], Literal["__ge__"], Literal["__getattribute__"], Literal["__getitem__"], Literal["__getnewargs__"], Literal["__gt__"], Literal["__hash__"], Literal["__init__"], Literal["__init_subclass__"], Literal["__iter__"], Literal["__le__"], Literal["__len__"], Literal["__lt__"], Literal["__mod__"], Literal["__module__"], Literal["__mul__"], Literal["__ne__"], Literal["__new__"], Literal["__reduce__"], Literal["__reduce_ex__"], Literal["__repr__"], Literal["__reversed__"], Literal["__rmul__"], Literal["__setattr__"], Literal["__sizeof__"], Literal["__str__"], Literal["__subclasshook__"], Literal["capitalize"], Literal["casefold"], Literal["center"], Literal["count"], Literal["encode"], Literal["endswith"], Literal["expandtabs"], Literal["find"], Literal["format"], Literal["format_map"], Literal["index"], Literal["isalnum"], Literal["isalpha"], Literal["isascii"], Literal["isdecimal"], Literal["isdigit"], Literal["isidentifier"], Literal["islower"], Literal["isnumeric"], Literal["isprintable"], Literal["isspace"], Literal["istitle"], Literal["isupper"], Literal["join"], Literal["ljust"], Literal["lower"], Literal["lstrip"], Literal["maketrans"], Literal["partition"], Literal["removeprefix"], Literal["removesuffix"], Literal["replace"], Literal["rfind"], Literal["rindex"], Literal["rjust"], Literal["rpartition"], Literal["rsplit"], Literal["rstrip"], Literal["split"], Literal["splitlines"], Literal["startswith"], Literal["strip"], Literal["swapcase"], Literal["title"], Literal["translate"], Literal["upper"], Literal["zfill"]]` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno\342\200\246_-_PEP-604_in_non-type-\342\200\246_-_Earlier_versions_(f2859c9800f37c7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno\342\200\246_-_PEP-604_in_non-type-\342\200\246_-_Earlier_versions_(f2859c9800f37c7).snap" new file mode 100644 index 0000000000000..3cfe98c8f76e9 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno\342\200\246_-_PEP-604_in_non-type-\342\200\246_-_Earlier_versions_(f2859c9800f37c7).snap" @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: annotations.md - Assignment with annotations - PEP-604 in non-type-expression context - Earlier versions +mdtest path: crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | # error: [unsupported-operator] +2 | IntOrStr = int | str +``` + +# Diagnostics + +``` +error[unsupported-operator]: Operator `|` is unsupported between objects of type `` and `` + --> src/mdtest_snippet.py:2:12 + | +1 | # error: [unsupported-operator] +2 | IntOrStr = int | str + | ^^^^^^^^^ + | +info: Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later +info: Python 3.9 was assumed when resolving types because it was specified on the command line +info: rule `unsupported-operator` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap new file mode 100644 index 0000000000000..8c2ae5522a0ec --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_never.md_-_`assert_never`_-_Basic_functionality_-_Diagnostics_(be8f5d8b0718ee54).snap @@ -0,0 +1,197 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: assert_never.md - `assert_never` - Basic functionality - Diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import assert_never, Never, Any + 2 | from ty_extensions import Unknown + 3 | + 4 | def _(): + 5 | assert_never(0) # error: [type-assertion-failure] + 6 | + 7 | def _(): + 8 | assert_never("") # error: [type-assertion-failure] + 9 | +10 | def _(): +11 | assert_never(None) # error: [type-assertion-failure] +12 | +13 | def _(): +14 | assert_never([]) # error: [type-assertion-failure] +15 | +16 | def _(): +17 | assert_never({}) # error: [type-assertion-failure] +18 | +19 | def _(): +20 | assert_never(()) # error: [type-assertion-failure] +21 | +22 | def _(flag: bool, never: Never): +23 | assert_never(1 if flag else never) # error: [type-assertion-failure] +24 | +25 | def _(any_: Any): +26 | assert_never(any_) # error: [type-assertion-failure] +27 | +28 | def _(unknown: Unknown): +29 | assert_never(unknown) # error: [type-assertion-failure] +``` + +# Diagnostics + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:5:5 + | +4 | def _(): +5 | assert_never(0) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^-^ + | | + | Inferred type of argument is `Literal[0]` +6 | +7 | def _(): + | +info: `Never` and `Literal[0]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:8:5 + | + 7 | def _(): + 8 | assert_never("") # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `Literal[""]` + 9 | +10 | def _(): + | +info: `Never` and `Literal[""]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:11:5 + | +10 | def _(): +11 | assert_never(None) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^----^ + | | + | Inferred type of argument is `None` +12 | +13 | def _(): + | +info: `Never` and `None` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:14:5 + | +13 | def _(): +14 | assert_never([]) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `list[Unknown]` +15 | +16 | def _(): + | +info: `Never` and `list[Unknown]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:17:5 + | +16 | def _(): +17 | assert_never({}) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `dict[Unknown, Unknown]` +18 | +19 | def _(): + | +info: `Never` and `dict[Unknown, Unknown]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:20:5 + | +19 | def _(): +20 | assert_never(()) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--^ + | | + | Inferred type of argument is `tuple[()]` +21 | +22 | def _(flag: bool, never: Never): + | +info: `Never` and `tuple[()]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:23:5 + | +22 | def _(flag: bool, never: Never): +23 | assert_never(1 if flag else never) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^--------------------^ + | | + | Inferred type of argument is `Literal[1]` +24 | +25 | def _(any_: Any): + | +info: `Never` and `Literal[1]` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:26:5 + | +25 | def _(any_: Any): +26 | assert_never(any_) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^----^ + | | + | Inferred type of argument is `Any` +27 | +28 | def _(unknown: Unknown): + | +info: `Never` and `Any` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` + +``` +error[type-assertion-failure]: Argument does not have asserted type `Never` + --> src/mdtest_snippet.py:29:5 + | +28 | def _(unknown: Unknown): +29 | assert_never(unknown) # error: [type-assertion-failure] + | ^^^^^^^^^^^^^-------^ + | | + | Inferred type of argument is `Unknown` + | +info: `Never` and `Unknown` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic_(c507788da2659ec9).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic_(c507788da2659ec9).snap new file mode 100644 index 0000000000000..4b9da52c7b267 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/assert_type.md_-_`assert_type`_-_Basic_(c507788da2659ec9).snap @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: assert_type.md - `assert_type` - Basic +mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import assert_type +2 | +3 | def _(x: int): +4 | assert_type(x, int) # fine +5 | assert_type(x, str) # error: [type-assertion-failure] +``` + +# Diagnostics + +``` +error[type-assertion-failure]: Argument does not have asserted type `str` + --> src/mdtest_snippet.py:5:5 + | +3 | def _(x: int): +4 | assert_type(x, int) # fine +5 | assert_type(x, str) # error: [type-assertion-failure] + | ^^^^^^^^^^^^-^^^^^^ + | | + | Inferred type of argument is `int` + | +info: `str` and `int` are not equivalent types +info: rule `type-assertion-failure` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_me\342\200\246_(116c27bd98838df7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_me\342\200\246_(116c27bd98838df7).snap" new file mode 100644 index 0000000000000..3cdd4bf7acc64 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_me\342\200\246_(116c27bd98838df7).snap" @@ -0,0 +1,40 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class WrongDescriptor: + 2 | def __set__(self, instance: object, value: int, extra: int) -> None: + 3 | pass + 4 | + 5 | class C: + 6 | attr: WrongDescriptor = WrongDescriptor() + 7 | + 8 | instance = C() + 9 | +10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) +11 | instance.attr = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method + --> src/mdtest_snippet.py:11:1 + | +10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) +11 | instance.attr = 1 # error: [invalid-assignment] + | ^^^^^^^^^^^^^ + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_typ\342\200\246_(a903c11fedbc5020).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_typ\342\200\246_(a903c11fedbc5020).snap" new file mode 100644 index 0000000000000..850a5b3ceaf53 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_typ\342\200\246_(a903c11fedbc5020).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Descriptor: + 2 | def __set__(self, instance: object, value: int) -> None: + 3 | pass + 4 | + 5 | class C: + 6 | attr: Descriptor = Descriptor() + 7 | + 8 | instance = C() + 9 | instance.attr = 1 # fine +10 | +11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) +12 | instance.attr = "wrong" # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method + --> src/mdtest_snippet.py:12:1 + | +11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) +12 | instance.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^^^^^^^^ + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Instance_attributes_\342\200\246_(ebfb3de6d1b96b23).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Instance_attributes_\342\200\246_(ebfb3de6d1b96b23).snap" new file mode 100644 index 0000000000000..9b62b2d7ffde5 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Instance_attributes_\342\200\246_(ebfb3de6d1b96b23).snap" @@ -0,0 +1,53 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | attr: int = 0 +3 | +4 | instance = C() +5 | instance.attr = 1 # fine +6 | instance.attr = "wrong" # error: [invalid-assignment] +7 | +8 | C.attr = 1 # fine +9 | C.attr = "wrong" # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + --> src/mdtest_snippet.py:6:1 + | +4 | instance = C() +5 | instance.attr = 1 # fine +6 | instance.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^^^^^^^^ +7 | +8 | C.attr = 1 # fine + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + --> src/mdtest_snippet.py:9:1 + | +8 | C.attr = 1 # fine +9 | C.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^ + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" new file mode 100644 index 0000000000000..369a67a25667b --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" @@ -0,0 +1,53 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _(flag: bool) -> None: +2 | class C: +3 | if flag: +4 | attr: int = 0 +5 | +6 | C.attr = 1 # error: [possibly-unbound-attribute] +7 | +8 | instance = C() +9 | instance.attr = 1 # error: [possibly-unbound-attribute] +``` + +# Diagnostics + +``` +warning[possibly-unbound-attribute]: Attribute `attr` on type `` is possibly unbound + --> src/mdtest_snippet.py:6:5 + | +4 | attr: int = 0 +5 | +6 | C.attr = 1 # error: [possibly-unbound-attribute] + | ^^^^^^ +7 | +8 | instance = C() + | +info: rule `possibly-unbound-attribute` is enabled by default + +``` + +``` +warning[possibly-unbound-attribute]: Attribute `attr` on type `C` is possibly unbound + --> src/mdtest_snippet.py:9:5 + | +8 | instance = C() +9 | instance.attr = 1 # error: [possibly-unbound-attribute] + | ^^^^^^^^^^^^^ + | +info: rule `possibly-unbound-attribute` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Pure_instance_attrib\342\200\246_(d13d57d3cc36face).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Pure_instance_attrib\342\200\246_(d13d57d3cc36face).snap" new file mode 100644 index 0000000000000..27df8aba9674e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Pure_instance_attrib\342\200\246_(d13d57d3cc36face).snap" @@ -0,0 +1,54 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | def __init__(self): +3 | self.attr: int = 0 +4 | +5 | instance = C() +6 | instance.attr = 1 # fine +7 | instance.attr = "wrong" # error: [invalid-assignment] +8 | +9 | C.attr = 1 # error: [invalid-attribute-access] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + --> src/mdtest_snippet.py:7:1 + | +5 | instance = C() +6 | instance.attr = 1 # fine +7 | instance.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^^^^^^^^ +8 | +9 | C.attr = 1 # error: [invalid-attribute-access] + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-attribute-access]: Cannot assign to instance attribute `attr` from the class object `` + --> src/mdtest_snippet.py:9:1 + | +7 | instance.attr = "wrong" # error: [invalid-assignment] +8 | +9 | C.attr = 1 # error: [invalid-attribute-access] + | ^^^^^^ + | +info: rule `invalid-attribute-access` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Setting_attributes_o\342\200\246_(467e26496f4c0c13).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Setting_attributes_o\342\200\246_(467e26496f4c0c13).snap" new file mode 100644 index 0000000000000..7491ce72aecb3 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Setting_attributes_o\342\200\246_(467e26496f4c0c13).snap" @@ -0,0 +1,51 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Setting attributes on union types +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def _(flag: bool) -> None: + 2 | if flag: + 3 | class C1: + 4 | attr: int = 0 + 5 | + 6 | else: + 7 | class C1: + 8 | attr: str = "" + 9 | +10 | # TODO: The error message here could be improved to explain why the assignment fails. +11 | C1.attr = 1 # error: [invalid-assignment] +12 | +13 | class C2: +14 | if flag: +15 | attr: int = 0 +16 | else: +17 | attr: str = "" +18 | +19 | # TODO: This should be an error +20 | C2.attr = 1 +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type ` | ` + --> src/mdtest_snippet.py:11:5 + | +10 | # TODO: The error message here could be improved to explain why the assignment fails. +11 | C1.attr = 1 # error: [invalid-assignment] + | ^^^^^^^ +12 | +13 | class C2: + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Unknown_attributes_(368ba83a71ef2120).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Unknown_attributes_(368ba83a71ef2120).snap" new file mode 100644 index 0000000000000..c7beb2d87fba4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Unknown_attributes_(368ba83a71ef2120).snap" @@ -0,0 +1,50 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - Unknown attributes +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: ... +2 | +3 | C.non_existent = 1 # error: [unresolved-attribute] +4 | +5 | instance = C() +6 | instance.non_existent = 1 # error: [unresolved-attribute] +``` + +# Diagnostics + +``` +error[unresolved-attribute]: Unresolved attribute `non_existent` on type ``. + --> src/mdtest_snippet.py:3:1 + | +1 | class C: ... +2 | +3 | C.non_existent = 1 # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^ +4 | +5 | instance = C() + | +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Unresolved attribute `non_existent` on type `C`. + --> src/mdtest_snippet.py:6:1 + | +5 | instance = C() +6 | instance.non_existent = 1 # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^^^^^^^^ + | +info: rule `unresolved-attribute` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_`ClassVar`s_(8d7cca27987b099d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_`ClassVar`s_(8d7cca27987b099d).snap" new file mode 100644 index 0000000000000..f623fe78e79da --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_`ClassVar`s_(8d7cca27987b099d).snap" @@ -0,0 +1,53 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attribute_assignment.md - Attribute assignment - `ClassVar`s +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import ClassVar + 2 | + 3 | class C: + 4 | attr: ClassVar[int] = 0 + 5 | + 6 | C.attr = 1 # fine + 7 | C.attr = "wrong" # error: [invalid-assignment] + 8 | + 9 | instance = C() +10 | instance.attr = 1 # error: [invalid-attribute-access] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + --> src/mdtest_snippet.py:7:1 + | +6 | C.attr = 1 # fine +7 | C.attr = "wrong" # error: [invalid-assignment] + | ^^^^^^ +8 | +9 | instance = C() + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-attribute-access]: Cannot assign to ClassVar `attr` from an instance of type `C` + --> src/mdtest_snippet.py:10:1 + | + 9 | instance = C() +10 | instance.attr = 1 # error: [invalid-attribute-access] + | ^^^^^^^^^^^^^ + | +info: rule `invalid-attribute-access` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at\342\200\246_(5457445ffed43a87).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at\342\200\246_(5457445ffed43a87).snap" new file mode 100644 index 0000000000000..fc16938a28395 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Invalid_access_to_at\342\200\246_(5457445ffed43a87).snap" @@ -0,0 +1,264 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: attributes.md - Attributes - Invalid access to attribute +mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Foo: + 2 | x: int + 3 | + 4 | def method(self): + 5 | # error: [unresolved-reference] "Name `x` used when not defined" + 6 | y = x + 7 | class Foo: + 8 | x: int = 1 + 9 | +10 | def method(self): +11 | # error: [unresolved-reference] "Name `x` used when not defined" +12 | y = x +13 | class Foo: +14 | def __init__(self): +15 | self.x = 1 +16 | +17 | def method(self): +18 | # error: [unresolved-reference] "Name `x` used when not defined" +19 | y = x +20 | class Foo: +21 | def __init__(self): +22 | self.x = 42 +23 | +24 | @staticmethod +25 | def static_method(): +26 | # error: [unresolved-reference] "Name `x` used when not defined" +27 | y = x +28 | from typing import ClassVar +29 | +30 | class Foo: +31 | x: ClassVar[int] = 42 +32 | +33 | @classmethod +34 | def class_method(cls): +35 | # error: [unresolved-reference] "Name `x` used when not defined" +36 | y = x +37 | class Foo: +38 | def __init__(self): +39 | self.x = 42 +40 | +41 | @classmethod +42 | def class_method(cls): +43 | # error: [unresolved-reference] "Name `x` used when not defined" +44 | y = x +45 | class Foo: +46 | x: ClassVar[int] +47 | +48 | @classmethod +49 | @staticmethod +50 | def class_method(cls): +51 | # error: [unresolved-reference] "Name `x` used when not defined" +52 | y = x +53 | class Foo: +54 | def __init__(self): +55 | self.x = 42 +56 | +57 | def method(other): +58 | # error: [unresolved-reference] "Name `x` used when not defined" +59 | y = x +60 | from typing import ClassVar +61 | +62 | class Foo: +63 | x: ClassVar[int] = 42 +64 | +65 | @classmethod +66 | def class_method(c_other): +67 | # error: [unresolved-reference] "Name `x` used when not defined" +68 | y = x +69 | from typing import ClassVar +70 | +71 | class Foo: +72 | x: ClassVar[int] = 42 +73 | +74 | def instance_method(*args, **kwargs): +75 | # error: [unresolved-reference] "Name `x` used when not defined" +76 | print(x) +77 | +78 | @classmethod +79 | def class_method(*, cls): +80 | # error: [unresolved-reference] "Name `x` used when not defined" +81 | y = x +``` + +# Diagnostics + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:6:13 + | +4 | def method(self): +5 | # error: [unresolved-reference] "Name `x` used when not defined" +6 | y = x + | ^ +7 | class Foo: +8 | x: int = 1 + | +info: An attribute `x` is available: consider using `self.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:12:13 + | +10 | def method(self): +11 | # error: [unresolved-reference] "Name `x` used when not defined" +12 | y = x + | ^ +13 | class Foo: +14 | def __init__(self): + | +info: An attribute `x` is available: consider using `self.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:19:13 + | +17 | def method(self): +18 | # error: [unresolved-reference] "Name `x` used when not defined" +19 | y = x + | ^ +20 | class Foo: +21 | def __init__(self): + | +info: An attribute `x` is available: consider using `self.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:27:13 + | +25 | def static_method(): +26 | # error: [unresolved-reference] "Name `x` used when not defined" +27 | y = x + | ^ +28 | from typing import ClassVar + | +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:36:13 + | +34 | def class_method(cls): +35 | # error: [unresolved-reference] "Name `x` used when not defined" +36 | y = x + | ^ +37 | class Foo: +38 | def __init__(self): + | +info: An attribute `x` is available: consider using `cls.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:44:13 + | +42 | def class_method(cls): +43 | # error: [unresolved-reference] "Name `x` used when not defined" +44 | y = x + | ^ +45 | class Foo: +46 | x: ClassVar[int] + | +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:52:13 + | +50 | def class_method(cls): +51 | # error: [unresolved-reference] "Name `x` used when not defined" +52 | y = x + | ^ +53 | class Foo: +54 | def __init__(self): + | +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:59:13 + | +57 | def method(other): +58 | # error: [unresolved-reference] "Name `x` used when not defined" +59 | y = x + | ^ +60 | from typing import ClassVar + | +info: An attribute `x` is available: consider using `other.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:68:13 + | +66 | def class_method(c_other): +67 | # error: [unresolved-reference] "Name `x` used when not defined" +68 | y = x + | ^ +69 | from typing import ClassVar + | +info: An attribute `x` is available: consider using `c_other.x` +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:76:15 + | +74 | def instance_method(*args, **kwargs): +75 | # error: [unresolved-reference] "Name `x` used when not defined" +76 | print(x) + | ^ +77 | +78 | @classmethod + | +info: rule `unresolved-reference` is enabled by default + +``` + +``` +error[unresolved-reference]: Name `x` used when not defined + --> src/mdtest_snippet.py:81:13 + | +79 | def class_method(*, cls): +80 | # error: [unresolved-reference] "Name `x` used when not defined" +81 | y = x + | ^ + | +info: rule `unresolved-reference` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" new file mode 100644 index 0000000000000..dd2756ab92065 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" @@ -0,0 +1,65 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Structures - Attempting to import a stdlib module that's not yet been added +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | import tomllib # error: [unresolved-import] +2 | from string.templatelib import Template # error: [unresolved-import] +3 | from importlib.resources import abc # error: [unresolved-import] +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `tomllib` + --> src/mdtest_snippet.py:1:8 + | +1 | import tomllib # error: [unresolved-import] + | ^^^^^^^ +2 | from string.templatelib import Template # error: [unresolved-import] +3 | from importlib.resources import abc # error: [unresolved-import] + | +info: The stdlib module `tomllib` is only available on Python 3.11+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Cannot resolve imported module `string.templatelib` + --> src/mdtest_snippet.py:2:6 + | +1 | import tomllib # error: [unresolved-import] +2 | from string.templatelib import Template # error: [unresolved-import] + | ^^^^^^^^^^^^^^^^^^ +3 | from importlib.resources import abc # error: [unresolved-import] + | +info: The stdlib module `string.templatelib` is only available on Python 3.14+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Module `importlib.resources` has no member `abc` + --> src/mdtest_snippet.py:3:33 + | +1 | import tomllib # error: [unresolved-import] +2 | from string.templatelib import Template # error: [unresolved-import] +3 | from importlib.resources import abc # error: [unresolved-import] + | ^^^ + | +info: The stdlib module `importlib.resources` only has a `abc` submodule on Python 3.11+ +info: Python 3.10 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" new file mode 100644 index 0000000000000..964ecc9e64abd --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" @@ -0,0 +1,47 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Structures - Attempting to import a stdlib module that was previously removed +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | import aifc # error: [unresolved-import] +2 | from distutils import sysconfig # error: [unresolved-import] +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `aifc` + --> src/mdtest_snippet.py:1:8 + | +1 | import aifc # error: [unresolved-import] + | ^^^^ +2 | from distutils import sysconfig # error: [unresolved-import] + | +info: The stdlib module `aifc` is only available on Python <=3.12 +info: Python 3.13 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Cannot resolve imported module `distutils` + --> src/mdtest_snippet.py:2:6 + | +1 | import aifc # error: [unresolved-import] +2 | from distutils import sysconfig # error: [unresolved-import] + | ^^^^^^^^^ + | +info: The stdlib module `distutils` is only available on Python <=3.11 +info: Python 3.13 was assumed when resolving modules because it was specified on the command line +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" new file mode 100644 index 0000000000000..13b63a659f01c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" @@ -0,0 +1,32 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Structures - Multiple objects imported from an unresolved module +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | # error: [unresolved-import] +2 | from does_not_exist import foo, bar, baz +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> src/mdtest_snippet.py:2:6 + | +1 | # error: [unresolved-import] +2 | from does_not_exist import foo, bar, baz + | ^^^^^^^^^^^^^^ + | +info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" new file mode 100644 index 0000000000000..faa9a6155f984 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" @@ -0,0 +1,30 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Structures - Unresolvable module import +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve imported module `zqzqzqzqzqzqzq`" +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `zqzqzqzqzqzqzq` + --> src/mdtest_snippet.py:1:8 + | +1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve imported module `zqzqzqzqzqzqzq`" + | ^^^^^^^^^^^^^^ + | +info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" new file mode 100644 index 0000000000000..1bbe221bb7bc2 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" @@ -0,0 +1,55 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: basic.md - Structures - Unresolvable submodule imports +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | # Topmost component resolvable, submodule not resolvable: +2 | import a.foo # error: [unresolved-import] "Cannot resolve imported module `a.foo`" +3 | +4 | # Topmost component unresolvable: +5 | import b.foo # error: [unresolved-import] "Cannot resolve imported module `b.foo`" +``` + +## a/__init__.py + +``` +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `a.foo` + --> src/mdtest_snippet.py:2:8 + | +1 | # Topmost component resolvable, submodule not resolvable: +2 | import a.foo # error: [unresolved-import] "Cannot resolve imported module `a.foo`" + | ^^^^^ +3 | +4 | # Topmost component unresolvable: + | +info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment +info: rule `unresolved-import` is enabled by default + +``` + +``` +error[unresolved-import]: Cannot resolve imported module `b.foo` + --> src/mdtest_snippet.py:5:8 + | +4 | # Topmost component unresolvable: +5 | import b.foo # error: [unresolved-import] "Cannot resolve imported module `b.foo`" + | ^^^^^ + | +info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY\342\200\246_(dd1b8f2f71487f16).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY\342\200\246_(dd1b8f2f71487f16).snap" new file mode 100644 index 0000000000000..00b925ed42552 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/dataclasses.md_-_Dataclasses_-_`dataclasses.KW_ONLY\342\200\246_(dd1b8f2f71487f16).snap" @@ -0,0 +1,143 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: dataclasses.md - Dataclasses - `dataclasses.KW_ONLY` +mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from dataclasses import dataclass, field, KW_ONLY + 2 | from typing_extensions import reveal_type + 3 | + 4 | @dataclass + 5 | class C: + 6 | x: int + 7 | _: KW_ONLY + 8 | y: str + 9 | +10 | reveal_type(C.__init__) # revealed: (self: C, x: int, *, y: str) -> None +11 | +12 | # error: [missing-argument] +13 | # error: [too-many-positional-arguments] +14 | C(3, "") +15 | +16 | C(3, y="") +17 | @dataclass +18 | class Fails: # error: [duplicate-kw-only] +19 | a: int +20 | b: KW_ONLY +21 | c: str +22 | d: KW_ONLY +23 | e: bytes +24 | +25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None +26 | def flag() -> bool: +27 | return True +28 | +29 | @dataclass +30 | class D: # error: [duplicate-kw-only] +31 | x: int +32 | _1: KW_ONLY +33 | +34 | if flag(): +35 | y: str +36 | _2: KW_ONLY +37 | z: float +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:10:13 + | + 8 | y: str + 9 | +10 | reveal_type(C.__init__) # revealed: (self: C, x: int, *, y: str) -> None + | ^^^^^^^^^^ `(self: C, x: int, *, y: str) -> None` +11 | +12 | # error: [missing-argument] + | + +``` + +``` +error[missing-argument]: No argument provided for required parameter `y` + --> src/mdtest_snippet.py:14:1 + | +12 | # error: [missing-argument] +13 | # error: [too-many-positional-arguments] +14 | C(3, "") + | ^^^^^^^^ +15 | +16 | C(3, y="") + | +info: rule `missing-argument` is enabled by default + +``` + +``` +error[too-many-positional-arguments]: Too many positional arguments: expected 1, got 2 + --> src/mdtest_snippet.py:14:6 + | +12 | # error: [missing-argument] +13 | # error: [too-many-positional-arguments] +14 | C(3, "") + | ^^ +15 | +16 | C(3, y="") + | +info: rule `too-many-positional-arguments` is enabled by default + +``` + +``` +error[duplicate-kw-only]: Dataclass has more than one field annotated with `KW_ONLY` + --> src/mdtest_snippet.py:18:7 + | +16 | C(3, y="") +17 | @dataclass +18 | class Fails: # error: [duplicate-kw-only] + | ^^^^^ +19 | a: int +20 | b: KW_ONLY + | +info: `KW_ONLY` fields: `b`, `d` +info: rule `duplicate-kw-only` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:25:13 + | +23 | e: bytes +24 | +25 | reveal_type(Fails.__init__) # revealed: (self: Fails, a: int, *, c: str, e: bytes) -> None + | ^^^^^^^^^^^^^^ `(self: Fails, a: int, *, c: str, e: bytes) -> None` +26 | def flag() -> bool: +27 | return True + | + +``` + +``` +error[duplicate-kw-only]: Dataclass has more than one field annotated with `KW_ONLY` + --> src/mdtest_snippet.py:30:7 + | +29 | @dataclass +30 | class D: # error: [duplicate-kw-only] + | ^ +31 | x: int +32 | _1: KW_ONLY + | +info: `KW_ONLY` fields: `_1`, `_2` +info: rule `duplicate-kw-only` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_`typing.Final`_-_Full_diagnostics_(174fdd8134fb325b).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_`typing.Final`_-_Full_diagnostics_(174fdd8134fb325b).snap new file mode 100644 index 0000000000000..77b57442e8d3e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_`typing.Final`_-_Full_diagnostics_(174fdd8134fb325b).snap @@ -0,0 +1,59 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: final.md - `typing.Final` - Full diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Final + 2 | + 3 | MY_CONSTANT: Final[int] = 1 + 4 | + 5 | # more code + 6 | + 7 | MY_CONSTANT = 2 # error: [invalid-assignment] + 8 | from _stat import ST_INO + 9 | +10 | ST_INO = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not allowed + --> src/mdtest_snippet.py:3:14 + | +1 | from typing import Final +2 | +3 | MY_CONSTANT: Final[int] = 1 + | ---------- Symbol declared as `Final` here +4 | +5 | # more code +6 | +7 | MY_CONSTANT = 2 # error: [invalid-assignment] + | ^^^^^^^^^^^^^^^ Symbol later reassigned here +8 | from _stat import ST_INO + | +info: rule `invalid-assignment` is enabled by default + +``` + +``` +error[invalid-assignment]: Reassignment of `Final` symbol `ST_INO` is not allowed + --> src/mdtest_snippet.py:10:1 + | + 8 | from _stat import ST_INO + 9 | +10 | ST_INO = 1 # error: [invalid-assignment] + | ^^^^^^^^^^ Reassignment of `Final` symbol + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_me\342\200\246_(3ffe352bb3a76715).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_me\342\200\246_(3ffe352bb3a76715).snap" new file mode 100644 index 0000000000000..a9104c65260e0 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_me\342\200\246_(3ffe352bb3a76715).snap" @@ -0,0 +1,55 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Bad `__getitem__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Iterable: + 4 | # invalid because it will implicitly be passed an `int` + 5 | # by the interpreter + 6 | def __getitem__(self, key: str) -> int: + 7 | return 42 + 8 | + 9 | # error: [not-iterable] +10 | for x in Iterable(): +11 | reveal_type(x) # revealed: int +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable` is not iterable + --> src/mdtest_snippet.py:10:10 + | + 9 | # error: [not-iterable] +10 | for x in Iterable(): + | ^^^^^^^^^^ +11 | reveal_type(x) # revealed: int + | +info: It has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:11:17 + | + 9 | # error: [not-iterable] +10 | for x in Iterable(): +11 | reveal_type(x) # revealed: int + | ^ `int` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable_(3153247bb9a9b72a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable_(3153247bb9a9b72a).snap new file mode 100644 index 0000000000000..5ccdf53de306d --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable_(3153247bb9a9b72a).snap @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Invalid iterable +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | nonsense = 123 +2 | for x in nonsense: # error: [not-iterable] +3 | pass +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Literal[123]` is not iterable + --> src/mdtest_snippet.py:2:10 + | +1 | nonsense = 123 +2 | for x in nonsense: # error: [not-iterable] + | ^^^^^^^^ +3 | pass + | +info: It doesn't have an `__iter__` method or a `__getitem__` method +info: rule `not-iterable` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_i\342\200\246_(a90ba167a7c191eb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_i\342\200\246_(a90ba167a7c191eb).snap" new file mode 100644 index 0000000000000..9a1a1fd62f2ea --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_i\342\200\246_(a90ba167a7c191eb).snap" @@ -0,0 +1,39 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - New over old style iteration protocol +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotIterable: +2 | def __getitem__(self, key: int) -> int: +3 | return 42 +4 | __iter__: None = None +5 | +6 | for x in NotIterable(): # error: [not-iterable] +7 | pass +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `NotIterable` is not iterable + --> src/mdtest_snippet.py:6:10 + | +4 | __iter__: None = None +5 | +6 | for x in NotIterable(): # error: [not-iterable] + | ^^^^^^^^^^^^^ +7 | pass + | +info: Its `__iter__` attribute has type `None`, which is not callable +info: rule `not-iterable` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method\342\200\246_(36425dbcbd793d2b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method\342\200\246_(36425dbcbd793d2b).snap" new file mode 100644 index 0000000000000..ad4ffa4b00b39 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method\342\200\246_(36425dbcbd793d2b).snap" @@ -0,0 +1,51 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - No `__iter__` method and `__getitem__` is not callable +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import reveal_type +2 | +3 | class Bad: +4 | __getitem__: None = None +5 | +6 | # error: [not-iterable] +7 | for x in Bad(): +8 | reveal_type(x) # revealed: Unknown +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Bad` is not iterable + --> src/mdtest_snippet.py:7:10 + | +6 | # error: [not-iterable] +7 | for x in Bad(): + | ^^^^^ +8 | reveal_type(x) # revealed: Unknown + | +info: It has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:8:17 + | +6 | # error: [not-iterable] +7 | for x in Bad(): +8 | reveal_type(x) # revealed: Unknown + | ^ `Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callabl\342\200\246_(49a21e4b7fe6e97b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callabl\342\200\246_(49a21e4b7fe6e97b).snap" new file mode 100644 index 0000000000000..1a20384b0dd61 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callabl\342\200\246_(49a21e4b7fe6e97b).snap" @@ -0,0 +1,104 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly-not-callable `__getitem__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def _(flag: bool): + 4 | class CustomCallable: + 5 | if flag: + 6 | def __call__(self, *args, **kwargs) -> int: + 7 | return 42 + 8 | else: + 9 | __call__: None = None +10 | +11 | class Iterable1: +12 | __getitem__: CustomCallable = CustomCallable() +13 | +14 | class Iterable2: +15 | if flag: +16 | def __getitem__(self, key: int) -> int: +17 | return 42 +18 | else: +19 | __getitem__: None = None +20 | +21 | # error: [not-iterable] +22 | for x in Iterable1(): +23 | # TODO... `int` might be ideal here? +24 | reveal_type(x) # revealed: int | Unknown +25 | +26 | # error: [not-iterable] +27 | for y in Iterable2(): +28 | # TODO... `int` might be ideal here? +29 | reveal_type(y) # revealed: int | Unknown +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable1` may not be iterable + --> src/mdtest_snippet.py:22:14 + | +21 | # error: [not-iterable] +22 | for x in Iterable1(): + | ^^^^^^^^^^^ +23 | # TODO... `int` might be ideal here? +24 | reveal_type(x) # revealed: int | Unknown + | +info: It has no `__iter__` method and its `__getitem__` attribute is invalid +info: `__getitem__` has type `CustomCallable`, which is not callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:24:21 + | +22 | for x in Iterable1(): +23 | # TODO... `int` might be ideal here? +24 | reveal_type(x) # revealed: int | Unknown + | ^ `int | Unknown` +25 | +26 | # error: [not-iterable] + | + +``` + +``` +error[not-iterable]: Object of type `Iterable2` may not be iterable + --> src/mdtest_snippet.py:27:14 + | +26 | # error: [not-iterable] +27 | for y in Iterable2(): + | ^^^^^^^^^^^ +28 | # TODO... `int` might be ideal here? +29 | reveal_type(y) # revealed: int | Unknown + | +info: It has no `__iter__` method and its `__getitem__` attribute is invalid +info: `__getitem__` has type `(bound method Iterable2.__getitem__(key: int) -> int) | None`, which is not callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:29:21 + | +27 | for y in Iterable2(): +28 | # TODO... `int` might be ideal here? +29 | reveal_type(y) # revealed: int | Unknown + | ^ `int | Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(6388761c90a0555c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(6388761c90a0555c).snap" new file mode 100644 index 0000000000000..ca18f1184a476 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(6388761c90a0555c).snap" @@ -0,0 +1,104 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly invalid `__iter__` methods +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Iterator: + 4 | def __next__(self) -> int: + 5 | return 42 + 6 | + 7 | def _(flag: bool): + 8 | class Iterable1: + 9 | if flag: +10 | def __iter__(self) -> Iterator: +11 | return Iterator() +12 | else: +13 | def __iter__(self, invalid_extra_arg) -> Iterator: +14 | return Iterator() +15 | +16 | # error: [not-iterable] +17 | for x in Iterable1(): +18 | reveal_type(x) # revealed: int +19 | +20 | class Iterable2: +21 | if flag: +22 | def __iter__(self) -> Iterator: +23 | return Iterator() +24 | else: +25 | __iter__: None = None +26 | +27 | # error: [not-iterable] +28 | for x in Iterable2(): +29 | # TODO: `int` would probably be better here: +30 | reveal_type(x) # revealed: int | Unknown +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable1` may not be iterable + --> src/mdtest_snippet.py:17:14 + | +16 | # error: [not-iterable] +17 | for x in Iterable1(): + | ^^^^^^^^^^^ +18 | reveal_type(x) # revealed: int + | +info: Its `__iter__` method may have an invalid signature +info: Type of `__iter__` is `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)` +info: Expected signature for `__iter__` is `def __iter__(self): ...` +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:18:21 + | +16 | # error: [not-iterable] +17 | for x in Iterable1(): +18 | reveal_type(x) # revealed: int + | ^ `int` +19 | +20 | class Iterable2: + | + +``` + +``` +error[not-iterable]: Object of type `Iterable2` may not be iterable + --> src/mdtest_snippet.py:28:14 + | +27 | # error: [not-iterable] +28 | for x in Iterable2(): + | ^^^^^^^^^^^ +29 | # TODO: `int` would probably be better here: +30 | reveal_type(x) # revealed: int | Unknown + | +info: Its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:30:21 + | +28 | for x in Iterable2(): +29 | # TODO: `int` would probably be better here: +30 | reveal_type(x) # revealed: int | Unknown + | ^ `int | Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(6805a6032e504b63).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(6805a6032e504b63).snap" new file mode 100644 index 0000000000000..ba0f58ca09c5a --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(6805a6032e504b63).snap" @@ -0,0 +1,100 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly invalid `__getitem__` methods +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def _(flag: bool): + 4 | class Iterable1: + 5 | if flag: + 6 | def __getitem__(self, item: int) -> str: + 7 | return "foo" + 8 | else: + 9 | __getitem__: None = None +10 | +11 | class Iterable2: +12 | if flag: +13 | def __getitem__(self, item: int) -> str: +14 | return "foo" +15 | else: +16 | def __getitem__(self, item: str) -> int: +17 | return 42 +18 | +19 | # error: [not-iterable] +20 | for x in Iterable1(): +21 | # TODO: `str` might be better +22 | reveal_type(x) # revealed: str | Unknown +23 | +24 | # error: [not-iterable] +25 | for y in Iterable2(): +26 | reveal_type(y) # revealed: str | int +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable1` may not be iterable + --> src/mdtest_snippet.py:20:14 + | +19 | # error: [not-iterable] +20 | for x in Iterable1(): + | ^^^^^^^^^^^ +21 | # TODO: `str` might be better +22 | reveal_type(x) # revealed: str | Unknown + | +info: It has no `__iter__` method and its `__getitem__` attribute is invalid +info: `__getitem__` has type `(bound method Iterable1.__getitem__(item: int) -> str) | None`, which is not callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:22:21 + | +20 | for x in Iterable1(): +21 | # TODO: `str` might be better +22 | reveal_type(x) # revealed: str | Unknown + | ^ `str | Unknown` +23 | +24 | # error: [not-iterable] + | + +``` + +``` +error[not-iterable]: Object of type `Iterable2` may not be iterable + --> src/mdtest_snippet.py:25:14 + | +24 | # error: [not-iterable] +25 | for y in Iterable2(): + | ^^^^^^^^^^^ +26 | reveal_type(y) # revealed: str | int + | +info: It has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:26:21 + | +24 | # error: [not-iterable] +25 | for y in Iterable2(): +26 | reveal_type(y) # revealed: str | int + | ^ `str | int` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(c626bde8651b643a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(c626bde8651b643a).snap" new file mode 100644 index 0000000000000..91c7d40dbd8c3 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__\342\200\246_(c626bde8651b643a).snap" @@ -0,0 +1,107 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly invalid `__next__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def _(flag: bool): + 4 | class Iterator1: + 5 | if flag: + 6 | def __next__(self) -> int: + 7 | return 42 + 8 | else: + 9 | def __next__(self, invalid_extra_arg) -> str: +10 | return "foo" +11 | +12 | class Iterator2: +13 | if flag: +14 | def __next__(self) -> int: +15 | return 42 +16 | else: +17 | __next__: None = None +18 | +19 | class Iterable1: +20 | def __iter__(self) -> Iterator1: +21 | return Iterator1() +22 | +23 | class Iterable2: +24 | def __iter__(self) -> Iterator2: +25 | return Iterator2() +26 | +27 | # error: [not-iterable] +28 | for x in Iterable1(): +29 | reveal_type(x) # revealed: int | str +30 | +31 | # error: [not-iterable] +32 | for y in Iterable2(): +33 | # TODO: `int` would probably be better here: +34 | reveal_type(y) # revealed: int | Unknown +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable1` may not be iterable + --> src/mdtest_snippet.py:28:14 + | +27 | # error: [not-iterable] +28 | for x in Iterable1(): + | ^^^^^^^^^^^ +29 | reveal_type(x) # revealed: int | str + | +info: Its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method +info: Expected signature for `__next__` is `def __next__(self): ...`) +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:29:21 + | +27 | # error: [not-iterable] +28 | for x in Iterable1(): +29 | reveal_type(x) # revealed: int | str + | ^ `int | str` +30 | +31 | # error: [not-iterable] + | + +``` + +``` +error[not-iterable]: Object of type `Iterable2` may not be iterable + --> src/mdtest_snippet.py:32:14 + | +31 | # error: [not-iterable] +32 | for y in Iterable2(): + | ^^^^^^^^^^^ +33 | # TODO: `int` would probably be better here: +34 | reveal_type(y) # revealed: int | Unknown + | +info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:34:21 + | +32 | for y in Iterable2(): +33 | # TODO: `int` would probably be better here: +34 | reveal_type(y) # revealed: int | Unknown + | ^ `int | Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" new file mode 100644 index 0000000000000..c7ec49090296d --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" @@ -0,0 +1,63 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def _(flag: bool): + 4 | class Iterator: + 5 | def __next__(self) -> int: + 6 | return 42 + 7 | + 8 | class Iterable: + 9 | if flag: +10 | def __iter__(self) -> Iterator: +11 | return Iterator() +12 | # invalid signature because it only accepts a `str`, +13 | # but the old-style iteration protocol will pass it an `int` +14 | def __getitem__(self, key: str) -> bytes: +15 | return bytes() +16 | +17 | # error: [not-iterable] +18 | for x in Iterable(): +19 | reveal_type(x) # revealed: int | bytes +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable` may not be iterable + --> src/mdtest_snippet.py:18:14 + | +17 | # error: [not-iterable] +18 | for x in Iterable(): + | ^^^^^^^^^^ +19 | reveal_type(x) # revealed: int | bytes + | +info: It may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:19:21 + | +17 | # error: [not-iterable] +18 | for x in Iterable(): +19 | reveal_type(x) # revealed: int | bytes + | ^ `int | bytes` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" new file mode 100644 index 0000000000000..48013f59483b0 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" @@ -0,0 +1,61 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__` +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Iterator: + 4 | def __next__(self) -> int: + 5 | return 42 + 6 | + 7 | def _(flag1: bool, flag2: bool): + 8 | class Iterable: + 9 | if flag1: +10 | def __iter__(self) -> Iterator: +11 | return Iterator() +12 | if flag2: +13 | def __getitem__(self, key: int) -> bytes: +14 | return bytes() +15 | +16 | # error: [not-iterable] +17 | for x in Iterable(): +18 | reveal_type(x) # revealed: int | bytes +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable` may not be iterable + --> src/mdtest_snippet.py:17:14 + | +16 | # error: [not-iterable] +17 | for x in Iterable(): + | ^^^^^^^^^^ +18 | reveal_type(x) # revealed: int | bytes + | +info: It may not have an `__iter__` method or a `__getitem__` method +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:18:21 + | +16 | # error: [not-iterable] +17 | for x in Iterable(): +18 | reveal_type(x) # revealed: int | bytes + | ^ `int | bytes` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" new file mode 100644 index 0000000000000..9d09acef590b3 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" @@ -0,0 +1,110 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__` +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Iterator: + 4 | def __next__(self) -> bytes: + 5 | return b"foo" + 6 | + 7 | def _(flag: bool, flag2: bool): + 8 | class Iterable1: + 9 | if flag: +10 | def __getitem__(self, item: int) -> str: +11 | return "foo" +12 | else: +13 | __getitem__: None = None +14 | +15 | if flag2: +16 | def __iter__(self) -> Iterator: +17 | return Iterator() +18 | +19 | class Iterable2: +20 | if flag: +21 | def __getitem__(self, item: int) -> str: +22 | return "foo" +23 | else: +24 | def __getitem__(self, item: str) -> int: +25 | return 42 +26 | if flag2: +27 | def __iter__(self) -> Iterator: +28 | return Iterator() +29 | +30 | # error: [not-iterable] +31 | for x in Iterable1(): +32 | # TODO: `bytes | str` might be better +33 | reveal_type(x) # revealed: bytes | str | Unknown +34 | +35 | # error: [not-iterable] +36 | for y in Iterable2(): +37 | reveal_type(y) # revealed: bytes | str | int +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable1` may not be iterable + --> src/mdtest_snippet.py:31:14 + | +30 | # error: [not-iterable] +31 | for x in Iterable1(): + | ^^^^^^^^^^^ +32 | # TODO: `bytes | str` might be better +33 | reveal_type(x) # revealed: bytes | str | Unknown + | +info: It may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:33:21 + | +31 | for x in Iterable1(): +32 | # TODO: `bytes | str` might be better +33 | reveal_type(x) # revealed: bytes | str | Unknown + | ^ `bytes | str | Unknown` +34 | +35 | # error: [not-iterable] + | + +``` + +``` +error[not-iterable]: Object of type `Iterable2` may not be iterable + --> src/mdtest_snippet.py:36:14 + | +35 | # error: [not-iterable] +36 | for y in Iterable2(): + | ^^^^^^^^^^^ +37 | reveal_type(y) # revealed: bytes | str | int + | +info: It may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:37:21 + | +35 | # error: [not-iterable] +36 | for y in Iterable2(): +37 | reveal_type(y) # revealed: bytes | str | int + | ^ `bytes | str | int` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterab\342\200\246_(6177bb6d13a22241).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterab\342\200\246_(6177bb6d13a22241).snap" new file mode 100644 index 0000000000000..e7ab06ed09bcc --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterab\342\200\246_(6177bb6d13a22241).snap" @@ -0,0 +1,63 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Union type as iterable where one union element has invalid `__iter__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class TestIter: + 4 | def __next__(self) -> int: + 5 | return 42 + 6 | + 7 | class Test: + 8 | def __iter__(self) -> TestIter: + 9 | return TestIter() +10 | +11 | class Test2: +12 | def __iter__(self) -> int: +13 | return 42 +14 | +15 | def _(flag: bool): +16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) +17 | # error: [not-iterable] +18 | for x in Test() if flag else Test2(): +19 | reveal_type(x) # revealed: int +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Test | Test2` may not be iterable + --> src/mdtest_snippet.py:18:14 + | +16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) +17 | # error: [not-iterable] +18 | for x in Test() if flag else Test2(): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | reveal_type(x) # revealed: int + | +info: Its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:19:21 + | +17 | # error: [not-iterable] +18 | for x in Test() if flag else Test2(): +19 | reveal_type(x) # revealed: int + | ^ `int` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterab\342\200\246_(ba36fbef63a14969).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterab\342\200\246_(ba36fbef63a14969).snap" new file mode 100644 index 0000000000000..8eb966da9ddb4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterab\342\200\246_(ba36fbef63a14969).snap" @@ -0,0 +1,58 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - Union type as iterable where one union element has no `__iter__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class TestIter: + 4 | def __next__(self) -> int: + 5 | return 42 + 6 | + 7 | class Test: + 8 | def __iter__(self) -> TestIter: + 9 | return TestIter() +10 | +11 | def _(flag: bool): +12 | # error: [not-iterable] +13 | for x in Test() if flag else 42: +14 | reveal_type(x) # revealed: int +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Test | Literal[42]` may not be iterable + --> src/mdtest_snippet.py:13:14 + | +11 | def _(flag: bool): +12 | # error: [not-iterable] +13 | for x in Test() if flag else 42: + | ^^^^^^^^^^^^^^^^^^^^^^ +14 | reveal_type(x) # revealed: int + | +info: It may not have an `__iter__` method and it doesn't have a `__getitem__` method +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:14:21 + | +12 | # error: [not-iterable] +13 | for x in Test() if flag else 42: +14 | reveal_type(x) # revealed: int + | ^ `int` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_it\342\200\246_(a1cdf01ad69ac37c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_it\342\200\246_(a1cdf01ad69ac37c).snap" new file mode 100644 index 0000000000000..3e5d3bf4ebf20 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_it\342\200\246_(a1cdf01ad69ac37c).snap" @@ -0,0 +1,72 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - With non-callable iterator +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def _(flag: bool): + 4 | class NotIterable: + 5 | if flag: + 6 | __iter__: int = 1 + 7 | else: + 8 | __iter__: None = None + 9 | +10 | # error: [not-iterable] +11 | for x in NotIterable(): +12 | pass +13 | +14 | # revealed: Unknown +15 | # error: [possibly-unresolved-reference] +16 | reveal_type(x) +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `NotIterable` is not iterable + --> src/mdtest_snippet.py:11:14 + | +10 | # error: [not-iterable] +11 | for x in NotIterable(): + | ^^^^^^^^^^^^^ +12 | pass + | +info: Its `__iter__` attribute has type `int | None`, which is not callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[possibly-unresolved-reference]: Name `x` used when possibly not defined + --> src/mdtest_snippet.py:16:17 + | +14 | # revealed: Unknown +15 | # error: [possibly-unresolved-reference] +16 | reveal_type(x) + | ^ + | +info: rule `possibly-unresolved-reference` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:16:17 + | +14 | # revealed: Unknown +15 | # error: [possibly-unresolved-reference] +16 | reveal_type(x) + | ^ `Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_\342\200\246_(92e3fdd69edad63d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_\342\200\246_(92e3fdd69edad63d).snap" new file mode 100644 index 0000000000000..25942db89e132 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_\342\200\246_(92e3fdd69edad63d).snap" @@ -0,0 +1,52 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - `__iter__` does not return an iterator +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import reveal_type +2 | +3 | class Bad: +4 | def __iter__(self) -> int: +5 | return 42 +6 | +7 | # error: [not-iterable] +8 | for x in Bad(): +9 | reveal_type(x) # revealed: Unknown +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Bad` is not iterable + --> src/mdtest_snippet.py:8:10 + | +7 | # error: [not-iterable] +8 | for x in Bad(): + | ^^^^^ +9 | reveal_type(x) # revealed: Unknown + | +info: Its `__iter__` method returns an object of type `int`, which has no `__next__` method +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:9:17 + | +7 | # error: [not-iterable] +8 | for x in Bad(): +9 | reveal_type(x) # revealed: Unknown + | ^ `Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_wi\342\200\246_(1136c0e783d61ba4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_wi\342\200\246_(1136c0e783d61ba4).snap" new file mode 100644 index 0000000000000..e37675bbc2806 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_wi\342\200\246_(1136c0e783d61ba4).snap" @@ -0,0 +1,57 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - `__iter__` method with a bad signature +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Iterator: + 4 | def __next__(self) -> int: + 5 | return 42 + 6 | + 7 | class Iterable: + 8 | def __iter__(self, extra_arg) -> Iterator: + 9 | return Iterator() +10 | +11 | # error: [not-iterable] +12 | for x in Iterable(): +13 | reveal_type(x) # revealed: int +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable` is not iterable + --> src/mdtest_snippet.py:12:10 + | +11 | # error: [not-iterable] +12 | for x in Iterable(): + | ^^^^^^^^^^ +13 | reveal_type(x) # revealed: int + | +info: Its `__iter__` method has an invalid signature +info: Expected signature `def __iter__(self): ...` +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:13:17 + | +11 | # error: [not-iterable] +12 | for x in Iterable(): +13 | reveal_type(x) # revealed: int + | ^ `int` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_a\342\200\246_(707bd02a22c4acc8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_a\342\200\246_(707bd02a22c4acc8).snap" new file mode 100644 index 0000000000000..b5c24e5283a44 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_a\342\200\246_(707bd02a22c4acc8).snap" @@ -0,0 +1,96 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: for.md - For loops - `__iter__` returns an iterator with an invalid `__next__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Iterator1: + 4 | def __next__(self, extra_arg) -> int: + 5 | return 42 + 6 | + 7 | class Iterator2: + 8 | __next__: None = None + 9 | +10 | class Iterable1: +11 | def __iter__(self) -> Iterator1: +12 | return Iterator1() +13 | +14 | class Iterable2: +15 | def __iter__(self) -> Iterator2: +16 | return Iterator2() +17 | +18 | # error: [not-iterable] +19 | for x in Iterable1(): +20 | reveal_type(x) # revealed: int +21 | +22 | # error: [not-iterable] +23 | for y in Iterable2(): +24 | reveal_type(y) # revealed: Unknown +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Iterable1` is not iterable + --> src/mdtest_snippet.py:19:10 + | +18 | # error: [not-iterable] +19 | for x in Iterable1(): + | ^^^^^^^^^^^ +20 | reveal_type(x) # revealed: int + | +info: Its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method +info: Expected signature for `__next__` is `def __next__(self): ...` +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:20:17 + | +18 | # error: [not-iterable] +19 | for x in Iterable1(): +20 | reveal_type(x) # revealed: int + | ^ `int` +21 | +22 | # error: [not-iterable] + | + +``` + +``` +error[not-iterable]: Object of type `Iterable2` is not iterable + --> src/mdtest_snippet.py:23:10 + | +22 | # error: [not-iterable] +23 | for y in Iterable2(): + | ^^^^^^^^^^^ +24 | reveal_type(y) # revealed: Unknown + | +info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable +info: rule `not-iterable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:24:17 + | +22 | # error: [not-iterable] +23 | for y in Iterable2(): +24 | reveal_type(y) # revealed: Unknown + | ^ `Unknown` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___L\342\200\246_-_Inferring_a_bound_ty\342\200\246_(d50204b9d91b7bd1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___L\342\200\246_-_Inferring_a_bound_ty\342\200\246_(d50204b9d91b7bd1).snap" new file mode 100644 index 0000000000000..3831ad8e67aa6 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___L\342\200\246_-_Inferring_a_bound_ty\342\200\246_(d50204b9d91b7bd1).snap" @@ -0,0 +1,91 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a bound typevar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar + 2 | from typing_extensions import reveal_type + 3 | + 4 | T = TypeVar("T", bound=int) + 5 | + 6 | def f(x: T) -> T: + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: Literal[1] +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:9:13 + | + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: Literal[1] + | ^^^^ `Literal[1]` +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:10:13 + | + 9 | reveal_type(f(1)) # revealed: Literal[1] +10 | reveal_type(f(True)) # revealed: Literal[True] + | ^^^^^^^ `Literal[True]` +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:12:13 + | +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^ `Unknown` + | + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:12:15 + | +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:4:1 + | +2 | from typing_extensions import reveal_type +3 | +4 | T = TypeVar("T", bound=int) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +5 | +6 | def f(x: T) -> T: + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___L\342\200\246_-_Inferring_a_constrai\342\200\246_(48ab83f977c109b4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___L\342\200\246_-_Inferring_a_constrai\342\200\246_(48ab83f977c109b4).snap" new file mode 100644 index 0000000000000..f0223b5e64d2c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___L\342\200\246_-_Inferring_a_constrai\342\200\246_(48ab83f977c109b4).snap" @@ -0,0 +1,106 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a constrained typevar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar + 2 | from typing_extensions import reveal_type + 3 | + 4 | T = TypeVar("T", int, None) + 5 | + 6 | def f(x: T) -> T: + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: int +10 | reveal_type(f(True)) # revealed: int +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:9:13 + | + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: int + | ^^^^ `int` +10 | reveal_type(f(True)) # revealed: int +11 | reveal_type(f(None)) # revealed: None + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:10:13 + | + 9 | reveal_type(f(1)) # revealed: int +10 | reveal_type(f(True)) # revealed: int + | ^^^^^^^ `int` +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:11:13 + | + 9 | reveal_type(f(1)) # revealed: int +10 | reveal_type(f(True)) # revealed: int +11 | reveal_type(f(None)) # revealed: None + | ^^^^^^^ `None` +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:13:13 + | +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^ `Unknown` + | + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:13:15 + | +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:4:1 + | +2 | from typing_extensions import reveal_type +3 | +4 | T = TypeVar("T", int, None) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +5 | +6 | def f(x: T) -> T: + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___P\342\200\246_-_Inferring_a_bound_ty\342\200\246_(5935d14c26afe407).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___P\342\200\246_-_Inferring_a_bound_ty\342\200\246_(5935d14c26afe407).snap" new file mode 100644 index 0000000000000..fc5bb331f1b0e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___P\342\200\246_-_Inferring_a_bound_ty\342\200\246_(5935d14c26afe407).snap" @@ -0,0 +1,87 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a bound typevar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import reveal_type +2 | +3 | def f[T: int](x: T) -> T: +4 | return x +5 | +6 | reveal_type(f(1)) # revealed: Literal[1] +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] +9 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:6:13 + | +4 | return x +5 | +6 | reveal_type(f(1)) # revealed: Literal[1] + | ^^^^ `Literal[1]` +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:7:13 + | +6 | reveal_type(f(1)) # revealed: Literal[1] +7 | reveal_type(f(True)) # revealed: Literal[True] + | ^^^^^^^ `Literal[True]` +8 | # error: [invalid-argument-type] +9 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:9:13 + | +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] +9 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^ `Unknown` + | + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:9:15 + | +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] +9 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import reveal_type +2 | +3 | def f[T: int](x: T) -> T: + | ^^^^^^ +4 | return x + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___P\342\200\246_-_Inferring_a_constrai\342\200\246_(d2c475fccc70a8e2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___P\342\200\246_-_Inferring_a_constrai\342\200\246_(d2c475fccc70a8e2).snap" new file mode 100644 index 0000000000000..8a961a23fcf5e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___P\342\200\246_-_Inferring_a_constrai\342\200\246_(d2c475fccc70a8e2).snap" @@ -0,0 +1,102 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a constrained typevar +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def f[T: (int, None)](x: T) -> T: + 4 | return x + 5 | + 6 | reveal_type(f(1)) # revealed: int + 7 | reveal_type(f(True)) # revealed: int + 8 | reveal_type(f(None)) # revealed: None + 9 | # error: [invalid-argument-type] +10 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:6:13 + | +4 | return x +5 | +6 | reveal_type(f(1)) # revealed: int + | ^^^^ `int` +7 | reveal_type(f(True)) # revealed: int +8 | reveal_type(f(None)) # revealed: None + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:7:13 + | +6 | reveal_type(f(1)) # revealed: int +7 | reveal_type(f(True)) # revealed: int + | ^^^^^^^ `int` +8 | reveal_type(f(None)) # revealed: None +9 | # error: [invalid-argument-type] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:8:13 + | + 6 | reveal_type(f(1)) # revealed: int + 7 | reveal_type(f(True)) # revealed: int + 8 | reveal_type(f(None)) # revealed: None + | ^^^^^^^ `None` + 9 | # error: [invalid-argument-type] +10 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:10:13 + | + 8 | reveal_type(f(None)) # revealed: None + 9 | # error: [invalid-argument-type] +10 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^ `Unknown` + | + +``` + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:10:15 + | + 8 | reveal_type(f(None)) # revealed: None + 9 | # error: [invalid-argument-type] +10 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import reveal_type +2 | +3 | def f[T: (int, None)](x: T) -> T: + | ^^^^^^^^^^^^^^ +4 | return x + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" new file mode 100644 index 0000000000000..8c9a693f17e22 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Built-ins_with_impli\342\200\246_(f5857d64ce69ca1d).snap" @@ -0,0 +1,173 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - Built-ins with implicit layouts +mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | # fmt: off + 2 | + 3 | class A( # error: [instance-layout-conflict] + 4 | int, + 5 | str + 6 | ): ... + 7 | + 8 | class B: + 9 | __slots__ = ("b",) +10 | +11 | class C( # error: [instance-layout-conflict] +12 | int, +13 | B, +14 | ): ... +15 | class D(int): ... +16 | +17 | class E( # error: [instance-layout-conflict] +18 | D, +19 | str +20 | ): ... +21 | +22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] +23 | +24 | # fmt: on +25 | class Foo(range, str): ... # error: [subclass-of-final-class] +``` + +# Diagnostics + +``` +error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases + --> src/mdtest_snippet.py:3:7 + | +1 | # fmt: off +2 | +3 | class A( # error: [instance-layout-conflict] + | _______^ +4 | | int, +5 | | str +6 | | ): ... + | |_^ Bases `int` and `str` cannot be combined in multiple inheritance +7 | +8 | class B: + | +info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts + --> src/mdtest_snippet.py:4:5 + | +3 | class A( # error: [instance-layout-conflict] +4 | int, + | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension +5 | str + | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension +6 | ): ... + | +info: rule `instance-layout-conflict` is enabled by default + +``` + +``` +error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases + --> src/mdtest_snippet.py:11:7 + | + 9 | __slots__ = ("b",) +10 | +11 | class C( # error: [instance-layout-conflict] + | _______^ +12 | | int, +13 | | B, +14 | | ): ... + | |_^ Bases `int` and `B` cannot be combined in multiple inheritance +15 | class D(int): ... + | +info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts + --> src/mdtest_snippet.py:12:5 + | +11 | class C( # error: [instance-layout-conflict] +12 | int, + | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension +13 | B, + | - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` +14 | ): ... +15 | class D(int): ... + | +info: rule `instance-layout-conflict` is enabled by default + +``` + +``` +error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases + --> src/mdtest_snippet.py:17:7 + | +15 | class D(int): ... +16 | +17 | class E( # error: [instance-layout-conflict] + | _______^ +18 | | D, +19 | | str +20 | | ): ... + | |_^ Bases `D` and `str` cannot be combined in multiple inheritance +21 | +22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] + | +info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts + --> src/mdtest_snippet.py:18:5 + | +17 | class E( # error: [instance-layout-conflict] +18 | D, + | - + | | + | `D` instances have a distinct memory layout because `D` inherits from `int` + | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension +19 | str + | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension +20 | ): ... + | +info: rule `instance-layout-conflict` is enabled by default + +``` + +``` +error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases + --> src/mdtest_snippet.py:22:7 + | +20 | ): ... +21 | +22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `str`, `bytes` and `bytearray` cannot be combined in multiple inheritance +23 | +24 | # fmt: on + | +info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts + --> src/mdtest_snippet.py:22:9 + | +20 | ): ... +21 | +22 | class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict] + | --- --- ----- --------- `bytearray` instances have a distinct memory layout because of the way `bytearray` is implemented in a C extension + | | | | + | | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension + | | `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension + | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension +23 | +24 | # fmt: on + | +info: rule `instance-layout-conflict` is enabled by default + +``` + +``` +error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `range` + --> src/mdtest_snippet.py:25:11 + | +24 | # fmt: on +25 | class Foo(range, str): ... # error: [subclass-of-final-class] + | ^^^^^ + | +info: rule `subclass-of-final-class` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" new file mode 100644 index 0000000000000..be6151d4fb9b2 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" @@ -0,0 +1,54 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: instance_layout_conflict.md - Tests for ty's `instance-layout-conflict` error code - `__slots__`: incompatible tuples +mdtest path: crates/ty_python_semantic/resources/mdtest/instance_layout_conflict.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class A: + 2 | __slots__ = ("a", "b") + 3 | + 4 | class B: + 5 | __slots__ = ("c", "d") + 6 | + 7 | class C( # error: [instance-layout-conflict] + 8 | A, + 9 | B, +10 | ): ... +``` + +# Diagnostics + +``` +error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases + --> src/mdtest_snippet.py:7:7 + | + 5 | __slots__ = ("c", "d") + 6 | + 7 | class C( # error: [instance-layout-conflict] + | _______^ + 8 | | A, + 9 | | B, +10 | | ): ... + | |_^ Bases `A` and `B` cannot be combined in multiple inheritance + | +info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts + --> src/mdtest_snippet.py:8:5 + | + 7 | class C( # error: [instance-layout-conflict] + 8 | A, + | - `A` instances have a distinct memory layout because `A` defines non-empty `__slots__` + 9 | B, + | - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` +10 | ): ... + | +info: rule `instance-layout-conflict` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on\342\200\246_-_Operations_involving\342\200\246_(492b1163b8163c05).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on\342\200\246_-_Operations_involving\342\200\246_(492b1163b8163c05).snap" new file mode 100644 index 0000000000000..ac85e00fa018c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on\342\200\246_-_Operations_involving\342\200\246_(492b1163b8163c05).snap" @@ -0,0 +1,37 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods +mdtest path: crates/ty_python_semantic/resources/mdtest/binary/instances.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | __bool__: int = 3 +3 | +4 | a = NotBoolable() +5 | +6 | # error: [unsupported-bool-conversion] +7 | 10 and a and True +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:7:8 + | +6 | # error: [unsupported-bool-conversion] +7 | 10 and a and True + | ^ + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(f80dbf5dd571c940).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(f80dbf5dd571c940).snap" new file mode 100644 index 0000000000000..6e2b255903654 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(f80dbf5dd571c940).snap" @@ -0,0 +1,91 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - List-literal used when you meant to use a list or tuple +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def _( +2 | x: [int], # error: [invalid-type-form] +3 | ) -> [int]: # error: [invalid-type-form] +4 | return x +5 | def _( +6 | x: [int, str], # error: [invalid-type-form] +7 | ) -> [int, str]: # error: [invalid-type-form] +8 | return x +``` + +# Diagnostics + +``` +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:2:8 + | +1 | def _( +2 | x: [int], # error: [invalid-type-form] + | ^^^^^ Did you mean `list[int]`? +3 | ) -> [int]: # error: [invalid-type-form] +4 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:3:6 + | +1 | def _( +2 | x: [int], # error: [invalid-type-form] +3 | ) -> [int]: # error: [invalid-type-form] + | ^^^^^ Did you mean `list[int]`? +4 | return x +5 | def _( + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:6:8 + | +4 | return x +5 | def _( +6 | x: [int, str], # error: [invalid-type-form] + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +7 | ) -> [int, str]: # error: [invalid-type-form] +8 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:7:6 + | +5 | def _( +6 | x: [int, str], # error: [invalid-type-form] +7 | ) -> [int, str]: # error: [invalid-type-form] + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +8 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" new file mode 100644 index 0000000000000..abe619e045871 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" @@ -0,0 +1,62 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Module-literal used when you meant to use a class from that module +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +--- + +# Python source files + +## foo.py + +``` +1 | import datetime +2 | +3 | def f(x: datetime): ... # error: [invalid-type-form] +``` + +## PIL/Image.py + +``` +1 | class Image: ... +``` + +## bar.py + +``` +1 | from PIL import Image +2 | +3 | def g(x: Image): ... # error: [invalid-type-form] +``` + +# Diagnostics + +``` +error[invalid-type-form]: Variable of type `` is not allowed in a type expression + --> src/foo.py:3:10 + | +1 | import datetime +2 | +3 | def f(x: datetime): ... # error: [invalid-type-form] + | ^^^^^^^^ + | +info: Did you mean to use the module's member `datetime.datetime` instead? +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Variable of type `` is not allowed in a type expression + --> src/bar.py:3:10 + | +1 | from PIL import Image +2 | +3 | def g(x: Image): ... # error: [invalid-type-form] + | ^^^^^ + | +info: Did you mean to use the module's member `Image.Image` instead? +info: rule `invalid-type-form` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" new file mode 100644 index 0000000000000..5e268cd80ce59 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" @@ -0,0 +1,129 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Tuple-literal used when you meant to use a tuple +mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def _( + 2 | x: (), # error: [invalid-type-form] + 3 | ) -> (): # error: [invalid-type-form] + 4 | return x + 5 | def _( + 6 | x: (int,), # error: [invalid-type-form] + 7 | ) -> (int,): # error: [invalid-type-form] + 8 | return x + 9 | def _( +10 | x: (int, str), # error: [invalid-type-form] +11 | ) -> (int, str): # error: [invalid-type-form] +12 | return x +``` + +# Diagnostics + +``` +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:2:8 + | +1 | def _( +2 | x: (), # error: [invalid-type-form] + | ^^ Did you mean `tuple[()]`? +3 | ) -> (): # error: [invalid-type-form] +4 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:3:6 + | +1 | def _( +2 | x: (), # error: [invalid-type-form] +3 | ) -> (): # error: [invalid-type-form] + | ^^ Did you mean `tuple[()]`? +4 | return x +5 | def _( + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:6:8 + | +4 | return x +5 | def _( +6 | x: (int,), # error: [invalid-type-form] + | ^^^^^^ Did you mean `tuple[int]`? +7 | ) -> (int,): # error: [invalid-type-form] +8 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:7:6 + | +5 | def _( +6 | x: (int,), # error: [invalid-type-form] +7 | ) -> (int,): # error: [invalid-type-form] + | ^^^^^^ Did you mean `tuple[int]`? +8 | return x +9 | def _( + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:10:8 + | + 8 | return x + 9 | def _( +10 | x: (int, str), # error: [invalid-type-form] + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +11 | ) -> (int, str): # error: [invalid-type-form] +12 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` + +``` +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:11:6 + | + 9 | def _( +10 | x: (int, str), # error: [invalid-type-form] +11 | ) -> (int, str): # error: [invalid-type-form] + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +12 | return x + | +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +info: rule `invalid-type-form` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Basic_(16be9d90a741761).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Basic_(16be9d90a741761).snap" new file mode 100644 index 0000000000000..2dbdf7c5250e4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Basic_(16be9d90a741761).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int) -> int: +2 | return x * x +3 | +4 | foo("hello") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:5 + | +2 | return x * x +3 | +4 | foo("hello") # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int) -> int: + | ^^^ ------ Parameter declared here +2 | return x * x + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Calls_to_methods_(4b3b8695d519a02).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Calls_to_methods_(4b3b8695d519a02).snap" new file mode 100644 index 0000000000000..e0483e581db39 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Calls_to_methods_(4b3b8695d519a02).snap" @@ -0,0 +1,43 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to methods +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | def square(self, x: int) -> int: +3 | return x * x +4 | +5 | c = C() +6 | c.square("hello") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to bound method `square` is incorrect + --> src/mdtest_snippet.py:6:10 + | +5 | c = C() +6 | c.square("hello") # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:2:9 + | +1 | class C: +2 | def square(self, x: int) -> int: + | ^^^^^^ ------ Parameter declared here +3 | return x * x + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Different_files_(d02c38e2dd054b4c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Different_files_(d02c38e2dd054b4c).snap" new file mode 100644 index 0000000000000..f192c9723a836 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Different_files_(d02c38e2dd054b4c).snap" @@ -0,0 +1,47 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## package.py + +``` +1 | def foo(x: int) -> int: +2 | return x * x +``` + +## mdtest_snippet.py + +``` +1 | import package +2 | +3 | package.foo("hello") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:3:13 + | +1 | import package +2 | +3 | package.foo("hello") # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/package.py:1:5 + | +1 | def foo(x: int) -> int: + | ^^^ ------ Parameter declared here +2 | return x * x + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Different_source_ord\342\200\246_(9b0bf549733d3f0a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Different_source_ord\342\200\246_(9b0bf549733d3f0a).snap" new file mode 100644 index 0000000000000..672717075387d --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Different_source_ord\342\200\246_(9b0bf549733d3f0a).snap" @@ -0,0 +1,45 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def bar(): +2 | foo("hello") # error: [invalid-argument-type] +3 | +4 | def foo(x: int) -> int: +5 | return x * x +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:2:9 + | +1 | def bar(): +2 | foo("hello") # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` +3 | +4 | def foo(x: int) -> int: + | +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +2 | foo("hello") # error: [invalid-argument-type] +3 | +4 | def foo(x: int) -> int: + | ^^^ ------ Parameter declared here +5 | return x * x + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_(ee38fd34ceba3293).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_(ee38fd34ceba3293).snap" new file mode 100644 index 0000000000000..d26e1fe2f9c0a --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_(ee38fd34ceba3293).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int, y: int, z: int) -> int: +2 | return x * y * z +3 | +4 | foo(1, "hello", 3) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:8 + | +2 | return x * y * z +3 | +4 | foo(1, "hello", 3) # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, z: int) -> int: + | ^^^ ------ Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_acro\342\200\246_(1d5d112808c49e9d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_acro\342\200\246_(1d5d112808c49e9d).snap" new file mode 100644 index 0000000000000..e9e1198983d2e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_acro\342\200\246_(1d5d112808c49e9d).snap" @@ -0,0 +1,49 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo( +2 | x: int, +3 | y: int, +4 | z: int, +5 | ) -> int: +6 | return x * y * z +7 | +8 | foo(1, "hello", 3) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:8:8 + | +6 | return x * y * z +7 | +8 | foo(1, "hello", 3) # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo( + | ^^^ +2 | x: int, +3 | y: int, + | ------ Parameter declared here +4 | z: int, +5 | ) -> int: + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_with\342\200\246_(4bc5c16cd568b8ec).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_with\342\200\246_(4bc5c16cd568b8ec).snap" new file mode 100644 index 0000000000000..6aa878c8df62e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Many_parameters_with\342\200\246_(4bc5c16cd568b8ec).snap" @@ -0,0 +1,84 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int, y: int, z: int) -> int: +2 | return x * y * z +3 | +4 | # error: [invalid-argument-type] +5 | # error: [invalid-argument-type] +6 | # error: [invalid-argument-type] +7 | foo("a", "b", "c") +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:7:5 + | +5 | # error: [invalid-argument-type] +6 | # error: [invalid-argument-type] +7 | foo("a", "b", "c") + | ^^^ Expected `int`, found `Literal["a"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, z: int) -> int: + | ^^^ ------ Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:7:10 + | +5 | # error: [invalid-argument-type] +6 | # error: [invalid-argument-type] +7 | foo("a", "b", "c") + | ^^^ Expected `int`, found `Literal["b"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, z: int) -> int: + | ^^^ ------ Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:7:15 + | +5 | # error: [invalid-argument-type] +6 | # error: [invalid-argument-type] +7 | foo("a", "b", "c") + | ^^^ Expected `int`, found `Literal["c"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, z: int) -> int: + | ^^^ ------ Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Test_calling_a_funct\342\200\246_(3b18271a821a59b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Test_calling_a_funct\342\200\246_(3b18271a821a59b).snap" new file mode 100644 index 0000000000000..a608b1f7de9cb --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Test_calling_a_funct\342\200\246_(3b18271a821a59b).snap" @@ -0,0 +1,45 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed` +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | import json +2 | +3 | json.loads(5) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `loads` is incorrect + --> src/mdtest_snippet.py:3:12 + | +1 | import json +2 | +3 | json.loads(5) # error: [invalid-argument-type] + | ^ Expected `str | bytes | bytearray`, found `Literal[5]` + | +info: Function defined here + --> stdlib/json/__init__.pyi:39:5 + | +37 | **kwds: Any, +38 | ) -> None: ... +39 | def loads( + | ^^^^^ +40 | s: str | bytes | bytearray, + | -------------------------- Parameter declared here +41 | *, +42 | cls: type[JSONDecoder] | None = None, + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Keyword_only_argumen\342\200\246_(8d9f18c78137411).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Keyword_only_argumen\342\200\246_(8d9f18c78137411).snap" new file mode 100644 index 0000000000000..640a3f0d5c84a --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Keyword_only_argumen\342\200\246_(8d9f18c78137411).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int, y: int, *, z: int = 0) -> int: +2 | return x * y * z +3 | +4 | foo(1, 2, z="hello") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:11 + | +2 | return x * y * z +3 | +4 | foo(1, 2, z="hello") # error: [invalid-argument-type] + | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, *, z: int = 0) -> int: + | ^^^ ---------- Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Mix_of_arguments_(cfc64b1136058112).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Mix_of_arguments_(cfc64b1136058112).snap" new file mode 100644 index 0000000000000..b2b1b00b55a80 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Mix_of_arguments_(cfc64b1136058112).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int, /, y: int, *, z: int = 0) -> int: +2 | return x * y * z +3 | +4 | foo(1, 2, z="hello") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:11 + | +2 | return x * y * z +3 | +4 | foo(1, 2, z="hello") # error: [invalid-argument-type] + | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, /, y: int, *, z: int = 0) -> int: + | ^^^ ---------- Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_One_keyword_argument_(cc34b2f7d19d427e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_One_keyword_argument_(cc34b2f7d19d427e).snap" new file mode 100644 index 0000000000000..a43e5fa7f870d --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_One_keyword_argument_(cc34b2f7d19d427e).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int, y: int, z: int = 0) -> int: +2 | return x * y * z +3 | +4 | foo(1, 2, "hello") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:11 + | +2 | return x * y * z +3 | +4 | foo(1, 2, "hello") # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, z: int = 0) -> int: + | ^^^ ---------- Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Only_positional_(3dc93b1709eb3be9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Only_positional_(3dc93b1709eb3be9).snap" new file mode 100644 index 0000000000000..c2647c7550542 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Only_positional_(3dc93b1709eb3be9).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(x: int, y: int, z: int, /) -> int: +2 | return x * y * z +3 | +4 | foo(1, "hello", 3) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:8 + | +2 | return x * y * z +3 | +4 | foo(1, "hello", 3) # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(x: int, y: int, z: int, /) -> int: + | ^^^ ------ Parameter declared here +2 | return x * y * z + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Synthetic_arguments_(4c09844bbbf47741).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Synthetic_arguments_(4c09844bbbf47741).snap" new file mode 100644 index 0000000000000..a21d9085a59bb --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Synthetic_arguments_(4c09844bbbf47741).snap" @@ -0,0 +1,43 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | def __call__(self, x: int) -> int: +3 | return 1 +4 | +5 | c = C() +6 | c("wrong") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to bound method `__call__` is incorrect + --> src/mdtest_snippet.py:6:3 + | +5 | c = C() +6 | c("wrong") # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["wrong"]` + | +info: Function defined here + --> src/mdtest_snippet.py:2:9 + | +1 | class C: +2 | def __call__(self, x: int) -> int: + | ^^^^^^^^ ------ Parameter declared here +3 | return 1 + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Variadic_arguments_(e26a3e7b2773a63b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Variadic_arguments_(e26a3e7b2773a63b).snap" new file mode 100644 index 0000000000000..bd74bf3b8db79 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Variadic_arguments_(e26a3e7b2773a63b).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(*numbers: int) -> int: +2 | return len(numbers) +3 | +4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:14 + | +2 | return len(numbers) +3 | +4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type] + | ^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(*numbers: int) -> int: + | ^^^ ------------- Parameter declared here +2 | return len(numbers) + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Variadic_keyword_arg\342\200\246_(4c855e39ea6baeaf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Variadic_keyword_arg\342\200\246_(4c855e39ea6baeaf).snap" new file mode 100644 index 0000000000000..fe922058743a8 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ\342\200\246_-_Invalid_argument_typ\342\200\246_-_Tests_for_a_variety_\342\200\246_-_Variadic_keyword_arg\342\200\246_(4c855e39ea6baeaf).snap" @@ -0,0 +1,41 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def foo(**numbers: int) -> int: +2 | return len(numbers) +3 | +4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:4:20 + | +2 | return len(numbers) +3 | +4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type] + | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` + | +info: Function defined here + --> src/mdtest_snippet.py:1:5 + | +1 | def foo(**numbers: int) -> int: + | ^^^ -------------- Parameter declared here +2 | return len(numbers) + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh\342\200\246_-_Return_type_that_doe\342\200\246_(feccf6b9da1e7cd3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh\342\200\246_-_Return_type_that_doe\342\200\246_(feccf6b9da1e7cd3).snap" new file mode 100644 index 0000000000000..d6698b03f4b34 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membersh\342\200\246_-_Return_type_that_doe\342\200\246_(feccf6b9da1e7cd3).snap" @@ -0,0 +1,57 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class NotBoolable: + 2 | __bool__: int = 3 + 3 | + 4 | class WithContains: + 5 | def __contains__(self, item) -> NotBoolable: + 6 | return NotBoolable() + 7 | + 8 | # error: [unsupported-bool-conversion] + 9 | 10 in WithContains() +10 | # error: [unsupported-bool-conversion] +11 | 10 not in WithContains() +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:9:1 + | + 8 | # error: [unsupported-bool-conversion] + 9 | 10 in WithContains() + | ^^^^^^^^^^^^^^^^^^^^ +10 | # error: [unsupported-bool-conversion] +11 | 10 not in WithContains() + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:11:1 + | + 9 | 10 in WithContains() +10 | # error: [unsupported-bool-conversion] +11 | 10 not in WithContains() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" new file mode 100644 index 0000000000000..81c10d0361136 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" @@ -0,0 +1,37 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: mro.md - Method Resolution Order tests - Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases +mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import Protocol, TypeVar, Generic +2 | +3 | T = TypeVar("T") +4 | +5 | class Foo(Protocol): ... +6 | class Bar(Protocol[T]): ... +7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] +``` + +# Diagnostics + +``` +error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO) for class `Baz` with bases list `[typing.Protocol[T], , ]` + --> src/mdtest_snippet.py:7:1 + | +5 | class Foo(Protocol): ... +6 | class Bar(Protocol[T]): ... +7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +info: rule `inconsistent-mro` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" new file mode 100644 index 0000000000000..1b60443d4da5c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" @@ -0,0 +1,78 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: mro.md - Method Resolution Order tests - `__bases__` includes a `Union` +mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def returns_bool() -> bool: + 4 | return True + 5 | + 6 | class A: ... + 7 | class B: ... + 8 | + 9 | if returns_bool(): +10 | x = A +11 | else: +12 | x = B +13 | +14 | reveal_type(x) # revealed: | +15 | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" +17 | class Foo(x): ... +18 | +19 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] +``` + +# Diagnostics + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:14:13 + | +12 | x = B +13 | +14 | reveal_type(x) # revealed: | + | ^ ` | ` +15 | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" + | + +``` + +``` +warning[unsupported-base]: Unsupported class base with type ` | ` + --> src/mdtest_snippet.py:17:11 + | +16 | # error: 11 [unsupported-base] "Unsupported class base with type ` | `" +17 | class Foo(x): ... + | ^ +18 | +19 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | +info: ty cannot resolve a consistent MRO for class `Foo` due to this base +info: Only class objects or `Any` are supported as class bases +info: rule `unsupported-base` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:19:13 + | +17 | class Foo(x): ... +18 | +19 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | ^^^^^^^^^^^ `tuple[, Unknown, ]` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" new file mode 100644 index 0000000000000..3a7a401bb766e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" @@ -0,0 +1,97 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists that include objects that are not instances of `type` +mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Foo(2): ... # error: [invalid-base] + 2 | class Foo: + 3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: + 4 | return () + 5 | + 6 | class Bar(Foo()): ... # error: [unsupported-base] + 7 | class Bad1: + 8 | def __mro_entries__(self, bases, extra_arg): + 9 | return () +10 | +11 | class Bad2: +12 | def __mro_entries__(self, bases) -> int: +13 | return 42 +14 | +15 | class BadSub1(Bad1()): ... # error: [invalid-base] +16 | class BadSub2(Bad2()): ... # error: [invalid-base] +``` + +# Diagnostics + +``` +error[invalid-base]: Invalid class base with type `Literal[2]` + --> src/mdtest_snippet.py:1:11 + | +1 | class Foo(2): ... # error: [invalid-base] + | ^ +2 | class Foo: +3 | def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]: + | +info: Definition of class `Foo` will raise `TypeError` at runtime +info: rule `invalid-base` is enabled by default + +``` + +``` +warning[unsupported-base]: Unsupported class base with type `Foo` + --> src/mdtest_snippet.py:6:11 + | +4 | return () +5 | +6 | class Bar(Foo()): ... # error: [unsupported-base] + | ^^^^^ +7 | class Bad1: +8 | def __mro_entries__(self, bases, extra_arg): + | +info: ty cannot resolve a consistent MRO for class `Bar` due to this base +info: Only class objects or `Any` are supported as class bases +info: rule `unsupported-base` is enabled by default + +``` + +``` +error[invalid-base]: Invalid class base with type `Bad1` + --> src/mdtest_snippet.py:15:15 + | +13 | return 42 +14 | +15 | class BadSub1(Bad1()): ... # error: [invalid-base] + | ^^^^^^ +16 | class BadSub2(Bad2()): ... # error: [invalid-base] + | +info: Definition of class `BadSub1` will raise `TypeError` at runtime +info: An instance type is only a valid class base if it has a valid `__mro_entries__` method +info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments +info: Expected a signature at least as permissive as `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]` +info: rule `invalid-base` is enabled by default + +``` + +``` +error[invalid-base]: Invalid class base with type `Bad2` + --> src/mdtest_snippet.py:16:15 + | +15 | class BadSub1(Bad1()): ... # error: [invalid-base] +16 | class BadSub2(Bad2()): ... # error: [invalid-base] + | ^^^^^^ + | +info: Definition of class `BadSub2` will raise `TypeError` at runtime +info: An instance type is only a valid class base if it has a valid `__mro_entries__` method +info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types +info: rule `invalid-base` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" new file mode 100644 index 0000000000000..fceb6462c89ec --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" @@ -0,0 +1,402 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: mro.md - Method Resolution Order tests - `__bases__` lists with duplicate bases +mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + 4 | + 5 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + 6 | + 7 | class Spam: ... + 8 | class Eggs: ... + 9 | class Bar: ... +10 | class Baz: ... +11 | +12 | # fmt: off +13 | +14 | # error: [duplicate-base] "Duplicate base class `Spam`" +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( +17 | Spam, +18 | Eggs, +19 | Bar, +20 | Baz, +21 | Spam, +22 | Eggs, +23 | ): ... +24 | +25 | # fmt: on +26 | +27 | reveal_type(Ham.__mro__) # revealed: tuple[, Unknown, ] +28 | +29 | class Mushrooms: ... +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] +31 | +32 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] +33 | +34 | # fmt: off +35 | +36 | # error: [duplicate-base] "Duplicate base class `Eggs`" +37 | class VeryEggyOmelette( +38 | Eggs, +39 | Ham, +40 | Spam, +41 | Eggs, +42 | Mushrooms, +43 | Bar, +44 | Eggs, +45 | Baz, +46 | Eggs, +47 | ): ... +48 | +49 | # fmt: off +50 | # fmt: off +51 | +52 | class A: ... +53 | +54 | class B( # type: ignore[duplicate-base] +55 | A, +56 | A, +57 | ): ... +58 | +59 | class C( +60 | A, +61 | A +62 | ): # type: ignore[duplicate-base] +63 | x: int +64 | +65 | # fmt: on +66 | # fmt: off +67 | +68 | # error: [duplicate-base] +69 | class D( +70 | A, +71 | # error: [unused-ignore-comment] +72 | A, # type: ignore[duplicate-base] +73 | ): ... +74 | +75 | # error: [duplicate-base] +76 | class E( +77 | A, +78 | A +79 | ): +80 | # error: [unused-ignore-comment] +81 | x: int # type: ignore[duplicate-base] +82 | +83 | # fmt: on +``` + +# Diagnostics + +``` +error[duplicate-base]: Duplicate base class `str` + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import reveal_type +2 | +3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + | ^^^^^^^^^^^^^ +4 | +5 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | +info: The definition of class `Foo` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:3:11 + | +1 | from typing_extensions import reveal_type +2 | +3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" + | --- ^^^ Class `str` later repeated here + | | + | Class `str` first included in bases list here +4 | +5 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:5:13 + | +3 | class Foo(str, str): ... # error: [duplicate-base] "Duplicate base class `str`" +4 | +5 | reveal_type(Foo.__mro__) # revealed: tuple[, Unknown, ] + | ^^^^^^^^^^^ `tuple[, Unknown, ]` +6 | +7 | class Spam: ... + | + +``` + +``` +error[duplicate-base]: Duplicate base class `Spam` + --> src/mdtest_snippet.py:16:7 + | +14 | # error: [duplicate-base] "Duplicate base class `Spam`" +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( + | _______^ +17 | | Spam, +18 | | Eggs, +19 | | Bar, +20 | | Baz, +21 | | Spam, +22 | | Eggs, +23 | | ): ... + | |_^ +24 | +25 | # fmt: on + | +info: The definition of class `Ham` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:17:5 + | +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( +17 | Spam, + | ---- Class `Spam` first included in bases list here +18 | Eggs, +19 | Bar, +20 | Baz, +21 | Spam, + | ^^^^ Class `Spam` later repeated here +22 | Eggs, +23 | ): ... + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +error[duplicate-base]: Duplicate base class `Eggs` + --> src/mdtest_snippet.py:16:7 + | +14 | # error: [duplicate-base] "Duplicate base class `Spam`" +15 | # error: [duplicate-base] "Duplicate base class `Eggs`" +16 | class Ham( + | _______^ +17 | | Spam, +18 | | Eggs, +19 | | Bar, +20 | | Baz, +21 | | Spam, +22 | | Eggs, +23 | | ): ... + | |_^ +24 | +25 | # fmt: on + | +info: The definition of class `Ham` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:18:5 + | +16 | class Ham( +17 | Spam, +18 | Eggs, + | ---- Class `Eggs` first included in bases list here +19 | Bar, +20 | Baz, +21 | Spam, +22 | Eggs, + | ^^^^ Class `Eggs` later repeated here +23 | ): ... + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:27:13 + | +25 | # fmt: on +26 | +27 | reveal_type(Ham.__mro__) # revealed: tuple[, Unknown, ] + | ^^^^^^^^^^^ `tuple[, Unknown, ]` +28 | +29 | class Mushrooms: ... + | + +``` + +``` +error[duplicate-base]: Duplicate base class `Mushrooms` + --> src/mdtest_snippet.py:30:7 + | +29 | class Mushrooms: ... +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +31 | +32 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] + | +info: The definition of class `Omelette` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:30:28 + | +29 | class Mushrooms: ... +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] + | --------- ^^^^^^^^^ Class `Mushrooms` later repeated here + | | + | Class `Mushrooms` first included in bases list here +31 | +32 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:32:13 + | +30 | class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base] +31 | +32 | reveal_type(Omelette.__mro__) # revealed: tuple[, Unknown, ] + | ^^^^^^^^^^^^^^^^ `tuple[, Unknown, ]` +33 | +34 | # fmt: off + | + +``` + +``` +error[duplicate-base]: Duplicate base class `Eggs` + --> src/mdtest_snippet.py:37:7 + | +36 | # error: [duplicate-base] "Duplicate base class `Eggs`" +37 | class VeryEggyOmelette( + | _______^ +38 | | Eggs, +39 | | Ham, +40 | | Spam, +41 | | Eggs, +42 | | Mushrooms, +43 | | Bar, +44 | | Eggs, +45 | | Baz, +46 | | Eggs, +47 | | ): ... + | |_^ +48 | +49 | # fmt: off + | +info: The definition of class `VeryEggyOmelette` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:38:5 + | +36 | # error: [duplicate-base] "Duplicate base class `Eggs`" +37 | class VeryEggyOmelette( +38 | Eggs, + | ---- Class `Eggs` first included in bases list here +39 | Ham, +40 | Spam, +41 | Eggs, + | ^^^^ Class `Eggs` later repeated here +42 | Mushrooms, +43 | Bar, +44 | Eggs, + | ^^^^ Class `Eggs` later repeated here +45 | Baz, +46 | Eggs, + | ^^^^ Class `Eggs` later repeated here +47 | ): ... + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +error[duplicate-base]: Duplicate base class `A` + --> src/mdtest_snippet.py:69:7 + | +68 | # error: [duplicate-base] +69 | class D( + | _______^ +70 | | A, +71 | | # error: [unused-ignore-comment] +72 | | A, # type: ignore[duplicate-base] +73 | | ): ... + | |_^ +74 | +75 | # error: [duplicate-base] + | +info: The definition of class `D` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:70:5 + | +68 | # error: [duplicate-base] +69 | class D( +70 | A, + | - Class `A` first included in bases list here +71 | # error: [unused-ignore-comment] +72 | A, # type: ignore[duplicate-base] + | ^ Class `A` later repeated here +73 | ): ... + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +info[unused-ignore-comment] + --> src/mdtest_snippet.py:72:9 + | +70 | A, +71 | # error: [unused-ignore-comment] +72 | A, # type: ignore[duplicate-base] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive +73 | ): ... + | + +``` + +``` +error[duplicate-base]: Duplicate base class `A` + --> src/mdtest_snippet.py:76:7 + | +75 | # error: [duplicate-base] +76 | class E( + | _______^ +77 | | A, +78 | | A +79 | | ): + | |_^ +80 | # error: [unused-ignore-comment] +81 | x: int # type: ignore[duplicate-base] + | +info: The definition of class `E` will raise `TypeError` at runtime + --> src/mdtest_snippet.py:77:5 + | +75 | # error: [duplicate-base] +76 | class E( +77 | A, + | - Class `A` first included in bases list here +78 | A + | ^ Class `A` later repeated here +79 | ): +80 | # error: [unused-ignore-comment] + | +info: rule `duplicate-base` is enabled by default + +``` + +``` +info[unused-ignore-comment] + --> src/mdtest_snippet.py:81:13 + | +79 | ): +80 | # error: [unused-ignore-comment] +81 | x: int # type: ignore[duplicate-base] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive +82 | +83 | # fmt: on + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" new file mode 100644 index 0000000000000..9a446afe71840 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" @@ -0,0 +1,29 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - A class constructor with unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | type() # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of class `type` matches arguments + --> src/mdtest_snippet.py:1:1 + | +1 | type() # error: [no-matching-overload] + | ^^^^^^ + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" new file mode 100644 index 0000000000000..66eccf602ade6 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" @@ -0,0 +1,63 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - A method call with unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | class Foo: + 4 | @overload + 5 | def bar(self, x: int) -> int: ... + 6 | @overload + 7 | def bar(self, x: str) -> str: ... + 8 | def bar(self, x: int | str) -> int | str: + 9 | return x +10 | +11 | foo = Foo() +12 | foo.bar(b"wat") # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of bound method `bar` matches arguments + --> src/mdtest_snippet.py:12:1 + | +11 | foo = Foo() +12 | foo.bar(b"wat") # error: [no-matching-overload] + | ^^^^^^^^^^^^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:5:9 + | +3 | class Foo: +4 | @overload +5 | def bar(self, x: int) -> int: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^ +6 | @overload +7 | def bar(self, x: str) -> str: ... + | +info: Possible overloads for bound method `bar`: +info: (self, x: int) -> int +info: (self, x: str) -> str +info: Overload implementation defined here + --> src/mdtest_snippet.py:8:9 + | +6 | @overload +7 | def bar(self, x: str) -> str: ... +8 | def bar(self, x: int | str) -> int | str: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +9 | return x + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" new file mode 100644 index 0000000000000..68563b01be43a --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" @@ -0,0 +1,120 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - Call to function with many unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | class Foo: ... + 4 | + 5 | @overload + 6 | def foo(a: int, b: int, c: int): ... + 7 | @overload + 8 | def foo(a: str, b: int, c: int): ... + 9 | @overload +10 | def foo(a: int, b: str, c: int): ... +11 | @overload +12 | def foo(a: int, b: int, c: str): ... +13 | @overload +14 | def foo(a: str, b: str, c: int): ... +15 | @overload +16 | def foo(a: int, b: str, c: str): ... +17 | @overload +18 | def foo(a: str, b: str, c: str): ... +19 | @overload +20 | def foo(a: int, b: int, c: int): ... +21 | @overload +22 | def foo(a: float, b: int, c: int): ... +23 | @overload +24 | def foo(a: int, b: float, c: int): ... +25 | @overload +26 | def foo(a: int, b: int, c: float): ... +27 | @overload +28 | def foo(a: float, b: float, c: int): ... +29 | @overload +30 | def foo(a: int, b: float, c: float): ... +31 | @overload +32 | def foo(a: float, b: float, c: float): ... +33 | @overload +34 | def foo(a: str, b: str, c: str): ... +35 | @overload +36 | def foo(a: float, b: str, c: str): ... +37 | @overload +38 | def foo(a: str, b: float, c: str): ... +39 | @overload +40 | def foo(a: str, b: str, c: float): ... +41 | @overload +42 | def foo(a: float, b: float, c: str): ... +43 | @overload +44 | def foo(a: str, b: float, c: float): ... +45 | @overload +46 | def foo(a: float, b: float, c: float): ... +47 | def foo(a, b, c): ... +48 | +49 | foo(Foo(), Foo()) # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of function `foo` matches arguments + --> src/mdtest_snippet.py:49:1 + | +47 | def foo(a, b, c): ... +48 | +49 | foo(Foo(), Foo()) # error: [no-matching-overload] + | ^^^^^^^^^^^^^^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:6:5 + | +5 | @overload +6 | def foo(a: int, b: int, c: int): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +7 | @overload +8 | def foo(a: str, b: int, c: int): ... + | +info: Possible overloads for function `foo`: +info: (a: int, b: int, c: int) -> Unknown +info: (a: str, b: int, c: int) -> Unknown +info: (a: int, b: str, c: int) -> Unknown +info: (a: int, b: int, c: str) -> Unknown +info: (a: str, b: str, c: int) -> Unknown +info: (a: int, b: str, c: str) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int, b: int, c: int) -> Unknown +info: (a: int | float, b: int, c: int) -> Unknown +info: (a: int, b: int | float, c: int) -> Unknown +info: (a: int, b: int, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int) -> Unknown +info: (a: int, b: int | float, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int | float, b: str, c: str) -> Unknown +info: (a: str, b: int | float, c: str) -> Unknown +info: (a: str, b: str, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: str) -> Unknown +info: (a: str, b: int | float, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: Overload implementation defined here + --> src/mdtest_snippet.py:47:5 + | +45 | @overload +46 | def foo(a: float, b: float, c: float): ... +47 | def foo(a, b, c): ... + | ^^^^^^^^^^^^ +48 | +49 | foo(Foo(), Foo()) # error: [no-matching-overload] + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" new file mode 100644 index 0000000000000..b14961eedf366 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" @@ -0,0 +1,230 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - Call to function with too many unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | class Foo: ... + 4 | + 5 | @overload + 6 | def foo(a: int, b: int, c: int): ... + 7 | @overload + 8 | def foo(a: str, b: int, c: int): ... + 9 | @overload + 10 | def foo(a: int, b: str, c: int): ... + 11 | @overload + 12 | def foo(a: int, b: int, c: str): ... + 13 | @overload + 14 | def foo(a: str, b: str, c: int): ... + 15 | @overload + 16 | def foo(a: int, b: str, c: str): ... + 17 | @overload + 18 | def foo(a: str, b: str, c: str): ... + 19 | @overload + 20 | def foo(a: int, b: int, c: int): ... + 21 | @overload + 22 | def foo(a: float, b: int, c: int): ... + 23 | @overload + 24 | def foo(a: int, b: float, c: int): ... + 25 | @overload + 26 | def foo(a: int, b: int, c: float): ... + 27 | @overload + 28 | def foo(a: float, b: float, c: int): ... + 29 | @overload + 30 | def foo(a: int, b: float, c: float): ... + 31 | @overload + 32 | def foo(a: float, b: float, c: float): ... + 33 | @overload + 34 | def foo(a: str, b: str, c: str): ... + 35 | @overload + 36 | def foo(a: float, b: str, c: str): ... + 37 | @overload + 38 | def foo(a: str, b: float, c: str): ... + 39 | @overload + 40 | def foo(a: str, b: str, c: float): ... + 41 | @overload + 42 | def foo(a: float, b: float, c: str): ... + 43 | @overload + 44 | def foo(a: str, b: float, c: float): ... + 45 | @overload + 46 | def foo(a: float, b: float, c: float): ... + 47 | @overload + 48 | def foo(a: list[int], b: list[int], c: list[int]): ... + 49 | @overload + 50 | def foo(a: list[str], b: list[int], c: list[int]): ... + 51 | @overload + 52 | def foo(a: list[int], b: list[str], c: list[int]): ... + 53 | @overload + 54 | def foo(a: list[int], b: list[int], c: list[str]): ... + 55 | @overload + 56 | def foo(a: list[str], b: list[str], c: list[int]): ... + 57 | @overload + 58 | def foo(a: list[int], b: list[str], c: list[str]): ... + 59 | @overload + 60 | def foo(a: list[str], b: list[str], c: list[str]): ... + 61 | @overload + 62 | def foo(a: list[int], b: list[int], c: list[int]): ... + 63 | @overload + 64 | def foo(a: list[float], b: list[int], c: list[int]): ... + 65 | @overload + 66 | def foo(a: list[int], b: list[float], c: list[int]): ... + 67 | @overload + 68 | def foo(a: list[int], b: list[int], c: list[float]): ... + 69 | @overload + 70 | def foo(a: list[float], b: list[float], c: list[int]): ... + 71 | @overload + 72 | def foo(a: list[int], b: list[float], c: list[float]): ... + 73 | @overload + 74 | def foo(a: list[float], b: list[float], c: list[float]): ... + 75 | @overload + 76 | def foo(a: list[str], b: list[str], c: list[str]): ... + 77 | @overload + 78 | def foo(a: list[float], b: list[str], c: list[str]): ... + 79 | @overload + 80 | def foo(a: list[str], b: list[float], c: list[str]): ... + 81 | @overload + 82 | def foo(a: list[str], b: list[str], c: list[float]): ... + 83 | @overload + 84 | def foo(a: list[float], b: list[float], c: list[str]): ... + 85 | @overload + 86 | def foo(a: list[str], b: list[float], c: list[float]): ... + 87 | @overload + 88 | def foo(a: list[float], b: list[float], c: list[float]): ... + 89 | @overload + 90 | def foo(a: bool, b: bool, c: bool): ... + 91 | @overload + 92 | def foo(a: str, b: bool, c: bool): ... + 93 | @overload + 94 | def foo(a: bool, b: str, c: bool): ... + 95 | @overload + 96 | def foo(a: bool, b: bool, c: str): ... + 97 | @overload + 98 | def foo(a: str, b: str, c: bool): ... + 99 | @overload +100 | def foo(a: bool, b: str, c: str): ... +101 | @overload +102 | def foo(a: str, b: str, c: str): ... +103 | @overload +104 | def foo(a: int, b: int, c: int): ... +105 | @overload +106 | def foo(a: bool, b: int, c: int): ... +107 | @overload +108 | def foo(a: int, b: bool, c: int): ... +109 | @overload +110 | def foo(a: int, b: int, c: bool): ... +111 | @overload +112 | def foo(a: bool, b: bool, c: int): ... +113 | @overload +114 | def foo(a: int, b: bool, c: bool): ... +115 | @overload +116 | def foo(a: str, b: str, c: str): ... +117 | @overload +118 | def foo(a: float, b: bool, c: bool): ... +119 | @overload +120 | def foo(a: bool, b: float, c: bool): ... +121 | @overload +122 | def foo(a: bool, b: bool, c: float): ... +123 | @overload +124 | def foo(a: float, b: float, c: bool): ... +125 | @overload +126 | def foo(a: bool, b: float, c: float): ... +127 | def foo(a, b, c): ... +128 | +129 | foo(Foo(), Foo()) # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of function `foo` matches arguments + --> src/mdtest_snippet.py:129:1 + | +127 | def foo(a, b, c): ... +128 | +129 | foo(Foo(), Foo()) # error: [no-matching-overload] + | ^^^^^^^^^^^^^^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:6:5 + | +5 | @overload +6 | def foo(a: int, b: int, c: int): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +7 | @overload +8 | def foo(a: str, b: int, c: int): ... + | +info: Possible overloads for function `foo`: +info: (a: int, b: int, c: int) -> Unknown +info: (a: str, b: int, c: int) -> Unknown +info: (a: int, b: str, c: int) -> Unknown +info: (a: int, b: int, c: str) -> Unknown +info: (a: str, b: str, c: int) -> Unknown +info: (a: int, b: str, c: str) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int, b: int, c: int) -> Unknown +info: (a: int | float, b: int, c: int) -> Unknown +info: (a: int, b: int | float, c: int) -> Unknown +info: (a: int, b: int, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int) -> Unknown +info: (a: int, b: int | float, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int | float, b: str, c: str) -> Unknown +info: (a: str, b: int | float, c: str) -> Unknown +info: (a: str, b: str, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: str) -> Unknown +info: (a: str, b: int | float, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: list[int], b: list[int], c: list[int]) -> Unknown +info: (a: list[str], b: list[int], c: list[int]) -> Unknown +info: (a: list[int], b: list[str], c: list[int]) -> Unknown +info: (a: list[int], b: list[int], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[int]) -> Unknown +info: (a: list[int], b: list[str], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[str]) -> Unknown +info: (a: list[int], b: list[int], c: list[int]) -> Unknown +info: (a: list[int | float], b: list[int], c: list[int]) -> Unknown +info: (a: list[int], b: list[int | float], c: list[int]) -> Unknown +info: (a: list[int], b: list[int], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[int]) -> Unknown +info: (a: list[int], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[str], b: list[str], c: list[str]) -> Unknown +info: (a: list[int | float], b: list[str], c: list[str]) -> Unknown +info: (a: list[str], b: list[int | float], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[str]) -> Unknown +info: (a: list[str], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: bool, b: bool, c: bool) -> Unknown +info: (a: str, b: bool, c: bool) -> Unknown +info: (a: bool, b: str, c: bool) -> Unknown +info: (a: bool, b: bool, c: str) -> Unknown +info: (a: str, b: str, c: bool) -> Unknown +info: (a: bool, b: str, c: str) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int, b: int, c: int) -> Unknown +info: ... omitted 11 overloads +info: Overload implementation defined here + --> src/mdtest_snippet.py:127:5 + | +125 | @overload +126 | def foo(a: bool, b: float, c: float): ... +127 | def foo(a, b, c): ... + | ^^^^^^^^^^^^ +128 | +129 | foo(Foo(), Foo()) # error: [no-matching-overload] + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" new file mode 100644 index 0000000000000..367bfd4dd8d03 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" @@ -0,0 +1,61 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | @overload + 4 | def f(x: int) -> int: ... + 5 | @overload + 6 | def f(x: str) -> str: ... + 7 | def f(x: int | str) -> int | str: + 8 | return x + 9 | +10 | f(b"foo") # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of function `f` matches arguments + --> src/mdtest_snippet.py:10:1 + | + 8 | return x + 9 | +10 | f(b"foo") # error: [no-matching-overload] + | ^^^^^^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:4:5 + | +3 | @overload +4 | def f(x: int) -> int: ... + | ^^^^^^^^^^^^^^^^ +5 | @overload +6 | def f(x: str) -> str: ... + | +info: Possible overloads for function `f`: +info: (x: int) -> int +info: (x: str) -> str +info: Overload implementation defined here + --> src/mdtest_snippet.py:7:5 + | +5 | @overload +6 | def f(x: str) -> str: ... +7 | def f(x: int | str) -> int | str: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +8 | return x + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" new file mode 100644 index 0000000000000..83837288e207c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" @@ -0,0 +1,148 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions with lots of parameters +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | @overload + 4 | def f( + 5 | lion: int, + 6 | turtle: int, + 7 | tortoise: int, + 8 | goat: int, + 9 | capybara: int, +10 | chicken: int, +11 | ostrich: int, +12 | gorilla: int, +13 | giraffe: int, +14 | condor: int, +15 | kangaroo: int, +16 | anaconda: int, +17 | tarantula: int, +18 | millipede: int, +19 | leopard: int, +20 | hyena: int, +21 | ) -> int: ... +22 | @overload +23 | def f( +24 | lion: str, +25 | turtle: str, +26 | tortoise: str, +27 | goat: str, +28 | capybara: str, +29 | chicken: str, +30 | ostrich: str, +31 | gorilla: str, +32 | giraffe: str, +33 | condor: str, +34 | kangaroo: str, +35 | anaconda: str, +36 | tarantula: str, +37 | millipede: str, +38 | leopard: str, +39 | hyena: str, +40 | ) -> str: ... +41 | def f( +42 | lion: int | str, +43 | turtle: int | str, +44 | tortoise: int | str, +45 | goat: int | str, +46 | capybara: int | str, +47 | chicken: int | str, +48 | ostrict: int | str, +49 | gorilla: int | str, +50 | giraffe: int | str, +51 | condor: int | str, +52 | kangaroo: int | str, +53 | anaconda: int | str, +54 | tarantula: int | str, +55 | millipede: int | str, +56 | leopard: int | str, +57 | hyena: int | str, +58 | ) -> int | str: +59 | return 0 +60 | +61 | f(b"foo") # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of function `f` matches arguments + --> src/mdtest_snippet.py:61:1 + | +59 | return 0 +60 | +61 | f(b"foo") # error: [no-matching-overload] + | ^^^^^^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:4:5 + | + 3 | @overload + 4 | def f( + | _____^ + 5 | | lion: int, + 6 | | turtle: int, + 7 | | tortoise: int, + 8 | | goat: int, + 9 | | capybara: int, +10 | | chicken: int, +11 | | ostrich: int, +12 | | gorilla: int, +13 | | giraffe: int, +14 | | condor: int, +15 | | kangaroo: int, +16 | | anaconda: int, +17 | | tarantula: int, +18 | | millipede: int, +19 | | leopard: int, +20 | | hyena: int, +21 | | ) -> int: ... + | |________^ +22 | @overload +23 | def f( + | +info: Possible overloads for function `f`: +info: (lion: int, turtle: int, tortoise: int, goat: int, capybara: int, chicken: int, ostrich: int, gorilla: int, giraffe: int, condor: int, kangaroo: int, anaconda: int, tarantula: int, millipede: int, leopard: int, hyena: int) -> int +info: (lion: str, turtle: str, tortoise: str, goat: str, capybara: str, chicken: str, ostrich: str, gorilla: str, giraffe: str, condor: str, kangaroo: str, anaconda: str, tarantula: str, millipede: str, leopard: str, hyena: str) -> str +info: Overload implementation defined here + --> src/mdtest_snippet.py:41:5 + | +39 | hyena: str, +40 | ) -> str: ... +41 | def f( + | _____^ +42 | | lion: int | str, +43 | | turtle: int | str, +44 | | tortoise: int | str, +45 | | goat: int | str, +46 | | capybara: int | str, +47 | | chicken: int | str, +48 | | ostrict: int | str, +49 | | gorilla: int | str, +50 | | giraffe: int | str, +51 | | condor: int | str, +52 | | kangaroo: int | str, +53 | | anaconda: int | str, +54 | | tarantula: int | str, +55 | | millipede: int | str, +56 | | leopard: int | str, +57 | | hyena: int | str, +58 | | ) -> int | str: + | |______________^ +59 | return 0 + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen\342\200\246_(ab3f546bf004e24d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen\342\200\246_(ab3f546bf004e24d).snap" new file mode 100644 index 0000000000000..defb8528ec745 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implemen\342\200\246_(ab3f546bf004e24d).snap" @@ -0,0 +1,35 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly +mdtest path: crates/ty_python_semantic/resources/mdtest/unary/not.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | __bool__: int = 3 +3 | +4 | # error: [unsupported-bool-conversion] +5 | not NotBoolable() +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:5:1 + | +4 | # error: [unsupported-bool-conversion] +5 | not NotBoolable() + | ^^^^^^^^^^^^^^^^^ + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" new file mode 100644 index 0000000000000..e8d42bc20c3b8 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" @@ -0,0 +1,67 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - At least two overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import overload +2 | +3 | @overload +4 | def func(x: int) -> int: ... +5 | +6 | # error: [invalid-overload] +7 | def func(x: int | str) -> int | str: +8 | return x +``` + +## mdtest_snippet.pyi + +``` +1 | from typing import overload +2 | +3 | @overload +4 | # error: [invalid-overload] +5 | def func(x: int) -> int: ... +``` + +# Diagnostics + +``` +error[invalid-overload]: Overloaded function `func` requires at least two overloads + --> src/mdtest_snippet.py:4:5 + | +3 | @overload +4 | def func(x: int) -> int: ... + | ---- Only one overload defined here +5 | +6 | # error: [invalid-overload] +7 | def func(x: int | str) -> int | str: + | ^^^^ +8 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: Overloaded function `func` requires at least two overloads + --> src/mdtest_snippet.pyi:5:5 + | +3 | @overload +4 | # error: [invalid-overload] +5 | def func(x: int) -> int: ... + | ---- + | | + | Only one overload defined here + | +info: rule `invalid-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" new file mode 100644 index 0000000000000..a2e027157665f --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" @@ -0,0 +1,131 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@classmethod` +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from __future__ import annotations + 2 | + 3 | from typing import overload + 4 | + 5 | class CheckClassMethod: + 6 | def __init__(self, x: int) -> None: + 7 | self.x = x + 8 | + 9 | @overload +10 | @classmethod +11 | def try_from1(cls, x: int) -> CheckClassMethod: ... +12 | @overload +13 | def try_from1(cls, x: str) -> None: ... +14 | @classmethod +15 | # error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently" +16 | def try_from1(cls, x: int | str) -> CheckClassMethod | None: +17 | if isinstance(x, int): +18 | return cls(x) +19 | return None +20 | +21 | @overload +22 | def try_from2(cls, x: int) -> CheckClassMethod: ... +23 | @overload +24 | @classmethod +25 | def try_from2(cls, x: str) -> None: ... +26 | @classmethod +27 | # error: [invalid-overload] +28 | def try_from2(cls, x: int | str) -> CheckClassMethod | None: +29 | if isinstance(x, int): +30 | return cls(x) +31 | return None +32 | +33 | @overload +34 | @classmethod +35 | def try_from3(cls, x: int) -> CheckClassMethod: ... +36 | @overload +37 | @classmethod +38 | def try_from3(cls, x: str) -> None: ... +39 | # error: [invalid-overload] +40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: +41 | if isinstance(x, int): +42 | return cls(x) +43 | return None +44 | +45 | @overload +46 | @classmethod +47 | def try_from4(cls, x: int) -> CheckClassMethod: ... +48 | @overload +49 | @classmethod +50 | def try_from4(cls, x: str) -> None: ... +51 | @classmethod +52 | def try_from4(cls, x: int | str) -> CheckClassMethod | None: +53 | if isinstance(x, int): +54 | return cls(x) +55 | return None +``` + +# Diagnostics + +``` +error[invalid-overload]: Overloaded function `try_from1` does not use the `@classmethod` decorator consistently + --> src/mdtest_snippet.py:13:9 + | +11 | def try_from1(cls, x: int) -> CheckClassMethod: ... +12 | @overload +13 | def try_from1(cls, x: str) -> None: ... + | --------- Missing here +14 | @classmethod +15 | # error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently" +16 | def try_from1(cls, x: int | str) -> CheckClassMethod | None: + | ^^^^^^^^^ +17 | if isinstance(x, int): +18 | return cls(x) + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: Overloaded function `try_from2` does not use the `@classmethod` decorator consistently + --> src/mdtest_snippet.py:28:9 + | +26 | @classmethod +27 | # error: [invalid-overload] +28 | def try_from2(cls, x: int | str) -> CheckClassMethod | None: + | ^^^^^^^^^ +29 | if isinstance(x, int): +30 | return cls(x) + | + ::: src/mdtest_snippet.py:22:9 + | +21 | @overload +22 | def try_from2(cls, x: int) -> CheckClassMethod: ... + | --------- Missing here +23 | @overload +24 | @classmethod + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: Overloaded function `try_from3` does not use the `@classmethod` decorator consistently + --> src/mdtest_snippet.py:40:9 + | +38 | def try_from3(cls, x: str) -> None: ... +39 | # error: [invalid-overload] +40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: + | --------- + | | + | Missing here +41 | if isinstance(x, int): +42 | return cls(x) + | +info: rule `invalid-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" new file mode 100644 index 0000000000000..6799a92a57f38 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" @@ -0,0 +1,114 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@final` +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import final, overload + 2 | + 3 | class Foo: + 4 | @overload + 5 | def method1(self, x: int) -> int: ... + 6 | @overload + 7 | def method1(self, x: str) -> str: ... + 8 | @final + 9 | def method1(self, x: int | str) -> int | str: +10 | return x +11 | +12 | @overload +13 | @final +14 | def method2(self, x: int) -> int: ... +15 | @overload +16 | def method2(self, x: str) -> str: ... +17 | # error: [invalid-overload] +18 | def method2(self, x: int | str) -> int | str: +19 | return x +20 | +21 | @overload +22 | def method3(self, x: int) -> int: ... +23 | @overload +24 | @final +25 | def method3(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method3(self, x: int | str) -> int | str: +28 | return x +``` + +## mdtest_snippet.pyi + +``` + 1 | from typing_extensions import final, overload + 2 | + 3 | class Foo: + 4 | @overload + 5 | @final + 6 | def method1(self, x: int) -> int: ... + 7 | @overload + 8 | def method1(self, x: str) -> str: ... + 9 | +10 | @overload +11 | def method2(self, x: int) -> int: ... +12 | @final +13 | @overload +14 | # error: [invalid-overload] +15 | def method2(self, x: str) -> str: ... +``` + +# Diagnostics + +``` +error[invalid-overload]: `@final` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:18:9 + | +16 | def method2(self, x: str) -> str: ... +17 | # error: [invalid-overload] +18 | def method2(self, x: int | str) -> int | str: + | ------- + | | + | Implementation defined here +19 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:27:9 + | +25 | def method3(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method3(self, x: int | str) -> int | str: + | ------- + | | + | Implementation defined here +28 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@final` decorator should be applied only to the first overload + --> src/mdtest_snippet.pyi:11:9 + | +10 | @overload +11 | def method2(self, x: int) -> int: ... + | ------- First overload defined here +12 | @final +13 | @overload +14 | # error: [invalid-overload] +15 | def method2(self, x: str) -> str: ... + | ^^^^^^^ + | +info: rule `invalid-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" new file mode 100644 index 0000000000000..80ff3214b4b02 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" @@ -0,0 +1,132 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@override` +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import overload, override + 2 | + 3 | class Base: + 4 | @overload + 5 | def method(self, x: int) -> int: ... + 6 | @overload + 7 | def method(self, x: str) -> str: ... + 8 | def method(self, x: int | str) -> int | str: + 9 | return x +10 | +11 | class Sub1(Base): +12 | @overload +13 | def method(self, x: int) -> int: ... +14 | @overload +15 | def method(self, x: str) -> str: ... +16 | @override +17 | def method(self, x: int | str) -> int | str: +18 | return x +19 | +20 | class Sub2(Base): +21 | @overload +22 | def method(self, x: int) -> int: ... +23 | @overload +24 | @override +25 | def method(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method(self, x: int | str) -> int | str: +28 | return x +29 | +30 | class Sub3(Base): +31 | @overload +32 | @override +33 | def method(self, x: int) -> int: ... +34 | @overload +35 | def method(self, x: str) -> str: ... +36 | # error: [invalid-overload] +37 | def method(self, x: int | str) -> int | str: +38 | return x +``` + +## mdtest_snippet.pyi + +``` + 1 | from typing_extensions import overload, override + 2 | + 3 | class Base: + 4 | @overload + 5 | def method(self, x: int) -> int: ... + 6 | @overload + 7 | def method(self, x: str) -> str: ... + 8 | + 9 | class Sub1(Base): +10 | @overload +11 | @override +12 | def method(self, x: int) -> int: ... +13 | @overload +14 | def method(self, x: str) -> str: ... +15 | +16 | class Sub2(Base): +17 | @overload +18 | def method(self, x: int) -> int: ... +19 | @overload +20 | @override +21 | # error: [invalid-overload] +22 | def method(self, x: str) -> str: ... +``` + +# Diagnostics + +``` +error[invalid-overload]: `@override` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:27:9 + | +25 | def method(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method(self, x: int | str) -> int | str: + | ------ + | | + | Implementation defined here +28 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@override` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:37:9 + | +35 | def method(self, x: str) -> str: ... +36 | # error: [invalid-overload] +37 | def method(self, x: int | str) -> int | str: + | ------ + | | + | Implementation defined here +38 | return x + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: `@override` decorator should be applied only to the first overload + --> src/mdtest_snippet.pyi:18:9 + | +16 | class Sub2(Base): +17 | @overload +18 | def method(self, x: int) -> int: ... + | ------ First overload defined here +19 | @overload +20 | @override +21 | # error: [invalid-overload] +22 | def method(self, x: str) -> str: ... + | ^^^^^^ + | +info: rule `invalid-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" new file mode 100644 index 0000000000000..21e0603051ed7 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" @@ -0,0 +1,59 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Overload without an implementation - Regular modules +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | @overload + 4 | def func(x: int) -> int: ... + 5 | @overload + 6 | # error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" + 7 | def func(x: str) -> str: ... + 8 | + 9 | class Foo: +10 | @overload +11 | def method(self, x: int) -> int: ... +12 | @overload +13 | # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" +14 | def method(self, x: str) -> str: ... +``` + +# Diagnostics + +``` +error[invalid-overload]: Overloaded non-stub function `func` must have an implementation + --> src/mdtest_snippet.py:7:5 + | +5 | @overload +6 | # error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" +7 | def func(x: str) -> str: ... + | ^^^^ +8 | +9 | class Foo: + | +info: rule `invalid-overload` is enabled by default + +``` + +``` +error[invalid-overload]: Overloaded non-stub function `method` must have an implementation + --> src/mdtest_snippet.py:14:9 + | +12 | @overload +13 | # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" +14 | def method(self, x: str) -> str: ... + | ^^^^^^ + | +info: rule `invalid-overload` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" new file mode 100644 index 0000000000000..3553f119c2242 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" @@ -0,0 +1,179 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Calls to protocol classes +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Protocol, reveal_type + 2 | + 3 | # error: [call-non-callable] + 4 | reveal_type(Protocol()) # revealed: Unknown + 5 | + 6 | class MyProtocol(Protocol): + 7 | x: int + 8 | + 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +10 | reveal_type(MyProtocol()) # revealed: MyProtocol +11 | +12 | class GenericProtocol[T](Protocol): +13 | x: T +14 | +15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] +17 | class SubclassOfMyProtocol(MyProtocol): ... +18 | +19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol +20 | +21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... +22 | +23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] +24 | def f(x: type[MyProtocol]): +25 | reveal_type(x()) # revealed: MyProtocol +``` + +# Diagnostics + +``` +error[call-non-callable]: Object of type `typing.Protocol` is not callable + --> src/mdtest_snippet.py:4:13 + | +3 | # error: [call-non-callable] +4 | reveal_type(Protocol()) # revealed: Unknown + | ^^^^^^^^^^ +5 | +6 | class MyProtocol(Protocol): + | +info: rule `call-non-callable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:4:13 + | +3 | # error: [call-non-callable] +4 | reveal_type(Protocol()) # revealed: Unknown + | ^^^^^^^^^^ `Unknown` +5 | +6 | class MyProtocol(Protocol): + | + +``` + +``` +error[call-non-callable]: Cannot instantiate class `MyProtocol` + --> src/mdtest_snippet.py:10:13 + | + 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +10 | reveal_type(MyProtocol()) # revealed: MyProtocol + | ^^^^^^^^^^^^ This call will raise `TypeError` at runtime +11 | +12 | class GenericProtocol[T](Protocol): + | +info: Protocol classes cannot be instantiated + --> src/mdtest_snippet.py:6:7 + | +4 | reveal_type(Protocol()) # revealed: Unknown +5 | +6 | class MyProtocol(Protocol): + | ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here +7 | x: int + | +info: rule `call-non-callable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:10:13 + | + 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +10 | reveal_type(MyProtocol()) # revealed: MyProtocol + | ^^^^^^^^^^^^ `MyProtocol` +11 | +12 | class GenericProtocol[T](Protocol): + | + +``` + +``` +error[call-non-callable]: Cannot instantiate class `GenericProtocol` + --> src/mdtest_snippet.py:16:13 + | +15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] + | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +17 | class SubclassOfMyProtocol(MyProtocol): ... + | +info: Protocol classes cannot be instantiated + --> src/mdtest_snippet.py:12:7 + | +10 | reveal_type(MyProtocol()) # revealed: MyProtocol +11 | +12 | class GenericProtocol[T](Protocol): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol` declared as a protocol here +13 | x: T + | +info: rule `call-non-callable` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:16:13 + | +15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] + | ^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]` +17 | class SubclassOfMyProtocol(MyProtocol): ... + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:19:13 + | +17 | class SubclassOfMyProtocol(MyProtocol): ... +18 | +19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol + | ^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol` +20 | +21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:23:13 + | +21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... +22 | +23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]` +24 | def f(x: type[MyProtocol]): +25 | reveal_type(x()) # revealed: MyProtocol + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:25:17 + | +23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] +24 | def f(x: type[MyProtocol]): +25 | reveal_type(x()) # revealed: MyProtocol + | ^^^ `MyProtocol` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" new file mode 100644 index 0000000000000..8d4d21634abc4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" @@ -0,0 +1,84 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Invalid calls to `get_protocol_members()` +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Protocol, get_protocol_members + 2 | + 3 | class NotAProtocol: ... + 4 | + 5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + 6 | + 7 | class AlsoNotAProtocol(NotAProtocol, object): ... + 8 | + 9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] +10 | class GenericProtocol[T](Protocol): ... +11 | +12 | get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549) +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Invalid argument to `get_protocol_members` + --> src/mdtest_snippet.py:5:1 + | +3 | class NotAProtocol: ... +4 | +5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +6 | +7 | class AlsoNotAProtocol(NotAProtocol, object): ... + | +info: Only protocol classes can be passed to `get_protocol_members` +info: `NotAProtocol` is declared here, but it is not a protocol class: + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol, get_protocol_members +2 | +3 | class NotAProtocol: ... + | ^^^^^^^^^^^^ +4 | +5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + | +info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol` +info: See https://typing.python.org/en/latest/spec/protocol.html# +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Invalid argument to `get_protocol_members` + --> src/mdtest_snippet.py:9:1 + | + 7 | class AlsoNotAProtocol(NotAProtocol, object): ... + 8 | + 9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +10 | class GenericProtocol[T](Protocol): ... + | +info: Only protocol classes can be passed to `get_protocol_members` +info: `AlsoNotAProtocol` is declared here, but it is not a protocol class: + --> src/mdtest_snippet.py:7:7 + | +5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] +6 | +7 | class AlsoNotAProtocol(NotAProtocol, object): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +8 | +9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] + | +info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol` +info: See https://typing.python.org/en/latest/spec/protocol.html# +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" new file mode 100644 index 0000000000000..65263835c3846 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" @@ -0,0 +1,243 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Narrowing of protocols +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Protocol, reveal_type + 2 | + 3 | class HasX(Protocol): + 4 | x: int + 5 | + 6 | def f(arg: object, arg2: type): + 7 | if isinstance(arg, HasX): # error: [invalid-argument-type] + 8 | reveal_type(arg) # revealed: HasX + 9 | else: +10 | reveal_type(arg) # revealed: ~HasX +11 | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] +13 | reveal_type(arg2) # revealed: type[HasX] +14 | else: +15 | reveal_type(arg2) # revealed: type & ~type[HasX] +16 | from typing import runtime_checkable +17 | +18 | @runtime_checkable +19 | class RuntimeCheckableHasX(Protocol): +20 | x: int +21 | +22 | def f(arg: object): +23 | if isinstance(arg, RuntimeCheckableHasX): # no error! +24 | reveal_type(arg) # revealed: RuntimeCheckableHasX +25 | else: +26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX +27 | @runtime_checkable +28 | class OnlyMethodMembers(Protocol): +29 | def method(self) -> None: ... +30 | +31 | def f(arg1: type, arg2: type): +32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) +33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] +34 | else: +35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] +36 | +37 | if issubclass(arg2, OnlyMethodMembers): # no error! +38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] +39 | else: +40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Class `HasX` cannot be used as the second argument to `isinstance` + --> src/mdtest_snippet.py:7:8 + | +6 | def f(arg: object, arg2: type): +7 | if isinstance(arg, HasX): # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +8 | reveal_type(arg) # revealed: HasX +9 | else: + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol, reveal_type +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:8:21 + | + 6 | def f(arg: object, arg2: type): + 7 | if isinstance(arg, HasX): # error: [invalid-argument-type] + 8 | reveal_type(arg) # revealed: HasX + | ^^^ `HasX` + 9 | else: +10 | reveal_type(arg) # revealed: ~HasX + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:10:21 + | + 8 | reveal_type(arg) # revealed: HasX + 9 | else: +10 | reveal_type(arg) # revealed: ~HasX + | ^^^ `~HasX` +11 | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] + | + +``` + +``` +error[invalid-argument-type]: Class `HasX` cannot be used as the second argument to `issubclass` + --> src/mdtest_snippet.py:12:8 + | +10 | reveal_type(arg) # revealed: ~HasX +11 | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +13 | reveal_type(arg2) # revealed: type[HasX] +14 | else: + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol, reveal_type +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `issubclass` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:13:21 + | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] +13 | reveal_type(arg2) # revealed: type[HasX] + | ^^^^ `type[HasX]` +14 | else: +15 | reveal_type(arg2) # revealed: type & ~type[HasX] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:15:21 + | +13 | reveal_type(arg2) # revealed: type[HasX] +14 | else: +15 | reveal_type(arg2) # revealed: type & ~type[HasX] + | ^^^^ `type & ~type[HasX]` +16 | from typing import runtime_checkable + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:24:21 + | +22 | def f(arg: object): +23 | if isinstance(arg, RuntimeCheckableHasX): # no error! +24 | reveal_type(arg) # revealed: RuntimeCheckableHasX + | ^^^ `RuntimeCheckableHasX` +25 | else: +26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:26:21 + | +24 | reveal_type(arg) # revealed: RuntimeCheckableHasX +25 | else: +26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX + | ^^^ `~RuntimeCheckableHasX` +27 | @runtime_checkable +28 | class OnlyMethodMembers(Protocol): + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:33:21 + | +31 | def f(arg1: type, arg2: type): +32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) +33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] + | ^^^^ `type[RuntimeCheckableHasX]` +34 | else: +35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:35:21 + | +33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] +34 | else: +35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] + | ^^^^ `type & ~type[RuntimeCheckableHasX]` +36 | +37 | if issubclass(arg2, OnlyMethodMembers): # no error! + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:38:21 + | +37 | if issubclass(arg2, OnlyMethodMembers): # no error! +38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] + | ^^^^ `type[OnlyMethodMembers]` +39 | else: +40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] + | + +``` + +``` +info[revealed-type]: Revealed type + --> src/mdtest_snippet.py:40:21 + | +38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] +39 | else: +40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] + | ^^^^ `type & ~type[OnlyMethodMembers]` + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`inv\342\200\246_(35563a74094b14d5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`inv\342\200\246_(35563a74094b14d5).snap" new file mode 100644 index 0000000000000..d9a0e67f6d459 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`inv\342\200\246_(35563a74094b14d5).snap" @@ -0,0 +1,49 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Diagnostics for `invalid-return-type` on non-protocol subclasses of protocol classes +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import Protocol +2 | +3 | class Abstract(Protocol): +4 | def method(self) -> str: ... +5 | +6 | class Concrete(Abstract): +7 | def method(self) -> str: ... # error: [invalid-return-type] +``` + +# Diagnostics + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `str` + --> src/mdtest_snippet.py:7:25 + | +6 | class Concrete(Abstract): +7 | def method(self) -> str: ... # error: [invalid-return-type] + | ^^^ + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: Only functions in stub files, methods on protocol classes, or methods with `@abstractmethod` are permitted to have empty bodies +info: Class `Concrete` has `typing.Protocol` in its MRO, but it is not a protocol class +info: Only classes that directly inherit from `typing.Protocol` or `typing_extensions.Protocol` are considered protocol classes + --> src/mdtest_snippet.py:6:7 + | +4 | def method(self) -> str: ... +5 | +6 | class Concrete(Abstract): + | ^^^^^^^^^^^^^^^^^^ `Protocol` not present in `Concrete`'s immediate bases +7 | def method(self) -> str: ... # error: [invalid-return-type] + | +info: See https://typing.python.org/en/latest/spec/protocol.html# +info: rule `invalid-return-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_(d9ed06b61b14fd4c).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_(d9ed06b61b14fd4c).snap new file mode 100644 index 0000000000000..fe49d4d2be885 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_(d9ed06b61b14fd4c).snap @@ -0,0 +1,87 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Generator functions +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | import types + 2 | import typing + 3 | + 4 | def f() -> types.GeneratorType: + 5 | yield 42 + 6 | + 7 | def g() -> typing.Generator: + 8 | yield 42 + 9 | +10 | def h() -> typing.Iterator: +11 | yield 42 +12 | +13 | def i() -> typing.Iterable: +14 | yield 42 +15 | +16 | def i2() -> typing.Generator: +17 | yield from i() +18 | +19 | def j() -> str: # error: [invalid-return-type] +20 | yield 42 +21 | import types +22 | import typing +23 | +24 | async def f() -> types.AsyncGeneratorType: +25 | yield 42 +26 | +27 | async def g() -> typing.AsyncGenerator: +28 | yield 42 +29 | +30 | async def h() -> typing.AsyncIterator: +31 | yield 42 +32 | +33 | async def i() -> typing.AsyncIterable: +34 | yield 42 +35 | +36 | async def j() -> str: # error: [invalid-return-type] +37 | yield 42 +``` + +# Diagnostics + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:19:12 + | +17 | yield from i() +18 | +19 | def j() -> str: # error: [invalid-return-type] + | ^^^ expected `str`, found `types.GeneratorType` +20 | yield 42 +21 | import types + | +info: Function is inferred as returning `types.GeneratorType` because it is a generator function +info: See https://docs.python.org/3/glossary.html#term-generator for more details +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:36:18 + | +34 | yield 42 +35 | +36 | async def j() -> str: # error: [invalid-return-type] + | ^^^ expected `str`, found `types.AsyncGeneratorType` +37 | yield 42 + | +info: Function is inferred as returning `types.AsyncGeneratorType` because it is an async generator function +info: See https://docs.python.org/3/glossary.html#term-asynchronous-generator for more details +info: rule `invalid-return-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" new file mode 100644 index 0000000000000..516ed04ec67bf --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" @@ -0,0 +1,91 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Invalid conditional return type +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def f(cond: bool) -> str: + 2 | if cond: + 3 | return "a" + 4 | else: + 5 | # error: [invalid-return-type] + 6 | return 1 + 7 | + 8 | def f(cond: bool) -> str: + 9 | if cond: +10 | # error: [invalid-return-type] +11 | return 1 +12 | else: +13 | # error: [invalid-return-type] +14 | return 2 +``` + +# Diagnostics + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:1:22 + | +1 | def f(cond: bool) -> str: + | --- Expected `str` because of return type +2 | if cond: +3 | return "a" +4 | else: +5 | # error: [invalid-return-type] +6 | return 1 + | ^ expected `str`, found `Literal[1]` +7 | +8 | def f(cond: bool) -> str: + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:8:22 + | + 6 | return 1 + 7 | + 8 | def f(cond: bool) -> str: + | --- Expected `str` because of return type + 9 | if cond: +10 | # error: [invalid-return-type] +11 | return 1 + | ^ expected `str`, found `Literal[1]` +12 | else: +13 | # error: [invalid-return-type] + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:14:16 + | +12 | else: +13 | # error: [invalid-return-type] +14 | return 2 + | ^ expected `str`, found `Literal[2]` + | + ::: src/mdtest_snippet.py:8:22 + | + 6 | return 1 + 7 | + 8 | def f(cond: bool) -> str: + | --- Expected `str` because of return type + 9 | if cond: +10 | # error: [invalid-return-type] + | +info: rule `invalid-return-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" new file mode 100644 index 0000000000000..889888f0a30ca --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" @@ -0,0 +1,100 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Invalid implicit return type +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def f() -> None: + 2 | if False: + 3 | # error: [invalid-return-type] + 4 | return 1 + 5 | + 6 | # error: [invalid-return-type] + 7 | def f(cond: bool) -> int: + 8 | if cond: + 9 | return 1 +10 | +11 | # error: [invalid-return-type] +12 | def f(cond: bool) -> int: +13 | if cond: +14 | raise ValueError() +15 | +16 | # error: [invalid-return-type] +17 | def f(cond: bool) -> int: +18 | if cond: +19 | cond = False +20 | else: +21 | return 1 +22 | if cond: +23 | return 2 +``` + +# Diagnostics + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:1:12 + | +1 | def f() -> None: + | ---- Expected `None` because of return type +2 | if False: +3 | # error: [invalid-return-type] +4 | return 1 + | ^ expected `None`, found `Literal[1]` +5 | +6 | # error: [invalid-return-type] + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Function can implicitly return `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.py:7:22 + | +6 | # error: [invalid-return-type] +7 | def f(cond: bool) -> int: + | ^^^ +8 | if cond: +9 | return 1 + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.py:12:22 + | +11 | # error: [invalid-return-type] +12 | def f(cond: bool) -> int: + | ^^^ +13 | if cond: +14 | raise ValueError() + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Function can implicitly return `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.py:17:22 + | +16 | # error: [invalid-return-type] +17 | def f(cond: bool) -> int: + | ^^^ +18 | if cond: +19 | cond = False + | +info: rule `invalid-return-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" new file mode 100644 index 0000000000000..a4fd88d692c93 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Invalid implicit return type always None +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | # error: [invalid-return-type] +2 | def f() -> int: +3 | print("hello") +``` + +# Diagnostics + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.py:2:12 + | +1 | # error: [invalid-return-type] +2 | def f() -> int: + | ^^^ +3 | print("hello") + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: rule `invalid-return-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap new file mode 100644 index 0000000000000..a5913a7d5f4d3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap @@ -0,0 +1,99 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Invalid return type +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | # error: [invalid-return-type] + 2 | def f() -> int: + 3 | 1 + 4 | + 5 | def f() -> str: + 6 | # error: [invalid-return-type] + 7 | return 1 + 8 | + 9 | def f() -> int: +10 | # error: [invalid-return-type] +11 | return +12 | +13 | from typing import TypeVar +14 | +15 | T = TypeVar("T") +16 | +17 | # error: [invalid-return-type] +18 | def m(x: T) -> T: ... +``` + +# Diagnostics + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.py:2:12 + | +1 | # error: [invalid-return-type] +2 | def f() -> int: + | ^^^ +3 | 1 + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:5:12 + | +3 | 1 +4 | +5 | def f() -> str: + | --- Expected `str` because of return type +6 | # error: [invalid-return-type] +7 | return 1 + | ^ expected `str`, found `Literal[1]` +8 | +9 | def f() -> int: + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:9:12 + | + 7 | return 1 + 8 | + 9 | def f() -> int: + | --- Expected `int` because of return type +10 | # error: [invalid-return-type] +11 | return + | ^^^^^^ expected `int`, found `None` +12 | +13 | from typing import TypeVar + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `T` + --> src/mdtest_snippet.py:18:16 + | +17 | # error: [invalid-return-type] +18 | def m(x: T) -> T: ... + | ^ + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: Only functions in stub files, methods on protocol classes, or methods with `@abstractmethod` are permitted to have empty bodies +info: rule `invalid-return-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" new file mode 100644 index 0000000000000..6f834b61e3637 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" @@ -0,0 +1,76 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Invalid return type in stub file +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.pyi + +``` + 1 | def f() -> int: + 2 | # error: [invalid-return-type] + 3 | return ... + 4 | + 5 | # error: [invalid-return-type] + 6 | def foo() -> int: + 7 | print("...") + 8 | ... + 9 | +10 | # error: [invalid-return-type] +11 | def foo() -> int: +12 | f"""{foo} is a function that ...""" +13 | ... +``` + +# Diagnostics + +``` +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.pyi:1:12 + | +1 | def f() -> int: + | --- Expected `int` because of return type +2 | # error: [invalid-return-type] +3 | return ... + | ^^^ expected `int`, found `ellipsis` +4 | +5 | # error: [invalid-return-type] + | +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.pyi:6:14 + | +5 | # error: [invalid-return-type] +6 | def foo() -> int: + | ^^^ +7 | print("...") +8 | ... + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: rule `invalid-return-type` is enabled by default + +``` + +``` +error[invalid-return-type]: Function always implicitly returns `None`, which is not assignable to return type `int` + --> src/mdtest_snippet.pyi:11:14 + | +10 | # error: [invalid-return-type] +11 | def foo() -> int: + | ^^^ +12 | f"""{foo} is a function that ...""" +13 | ... + | +info: Consider changing the return annotation to `-> None` or adding a `return` statement +info: rule `invalid-return-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com\342\200\246_-_Chained_comparisons_\342\200\246_(c391c13e2abc18a0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com\342\200\246_-_Chained_comparisons_\342\200\246_(c391c13e2abc18a0).snap" new file mode 100644 index 0000000000000..e40ecc83614de --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Com\342\200\246_-_Chained_comparisons_\342\200\246_(c391c13e2abc18a0).snap" @@ -0,0 +1,64 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class NotBoolable: + 2 | __bool__: int = 3 + 3 | + 4 | class Comparable: + 5 | def __lt__(self, item) -> NotBoolable: + 6 | return NotBoolable() + 7 | + 8 | def __gt__(self, item) -> NotBoolable: + 9 | return NotBoolable() +10 | +11 | # error: [unsupported-bool-conversion] +12 | 10 < Comparable() < 20 +13 | # error: [unsupported-bool-conversion] +14 | 10 < Comparable() < Comparable() +15 | +16 | Comparable() < Comparable() # fine +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:12:1 + | +11 | # error: [unsupported-bool-conversion] +12 | 10 < Comparable() < 20 + | ^^^^^^^^^^^^^^^^^ +13 | # error: [unsupported-bool-conversion] +14 | 10 < Comparable() < Comparable() + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:14:1 + | +12 | 10 < Comparable() < 20 +13 | # error: [unsupported-bool-conversion] +14 | 10 < Comparable() < Comparable() + | ^^^^^^^^^^^^^^^^^ +15 | +16 | Comparable() < Comparable() # fine + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro\342\200\246_-_Semantic_syntax_erro\342\200\246_-_`async`_comprehensio\342\200\246_-_Python_3.10_(96aa8ec77d46553d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro\342\200\246_-_Semantic_syntax_erro\342\200\246_-_`async`_comprehensio\342\200\246_-_Python_3.10_(96aa8ec77d46553d).snap" new file mode 100644 index 0000000000000..e1e2deae539ef --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro\342\200\246_-_Semantic_syntax_erro\342\200\246_-_`async`_comprehensio\342\200\246_-_Python_3.10_(96aa8ec77d46553d).snap" @@ -0,0 +1,46 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: semantic_syntax_errors.md - Semantic syntax error diagnostics - `async` comprehensions in synchronous comprehensions - Python 3.10 +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | async def elements(n): + 2 | yield n + 3 | + 4 | async def f(): + 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)" + 6 | return {n: [x async for x in elements(n)] for n in range(3)} + 7 | async def test(): + 8 | return [[x async for x in elements(n)] async for n in range(3)] + 9 | async def f(): +10 | [x for x in [1]] and [x async for x in elements(1)] +11 | +12 | async def f(): +13 | def g(): +14 | pass +15 | [x async for x in elements(1)] +``` + +# Diagnostics + +``` +error[invalid-syntax] + --> src/mdtest_snippet.py:6:19 + | +4 | async def f(): +5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (synt... +6 | return {n: [x async for x in elements(n)] for n in range(3)} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) +7 | async def test(): +8 | return [[x async for x in elements(n)] async for n in range(3)] + | + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado\342\200\246_(c8ff9e3a079e8bd5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado\342\200\246_(c8ff9e3a079e8bd5).snap" new file mode 100644 index 0000000000000..5109c05d9a773 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado\342\200\246_(c8ff9e3a079e8bd5).snap" @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: shadowing.md - Shadowing - Implicit class shadowing +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: ... +2 | +3 | C = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Implicit shadowing of class `C` + --> src/mdtest_snippet.py:3:1 + | +1 | class C: ... +2 | +3 | C = 1 # error: [invalid-assignment] + | ^ + | +info: Annotate to make it explicit if this is intentional +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh\342\200\246_(a1515328b775ebc1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh\342\200\246_(a1515328b775ebc1).snap" new file mode 100644 index 0000000000000..c9e3b2b326195 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh\342\200\246_(a1515328b775ebc1).snap" @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: shadowing.md - Shadowing - Implicit function shadowing +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def f(): ... +2 | +3 | f = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Implicit shadowing of function `f` + --> src/mdtest_snippet.py:3:1 + | +1 | def f(): ... +2 | +3 | f = 1 # error: [invalid-assignment] + | ^ + | +info: Annotate to make it explicit if this is intentional +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" new file mode 100644 index 0000000000000..3761148d09bb3 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" @@ -0,0 +1,226 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: single_matching_overload.md - Single matching overload - Call to function with too many unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md +--- + +# Python source files + +## overloaded.pyi + +``` + 1 | from typing import overload + 2 | + 3 | @overload + 4 | def foo(a: int): ... + 5 | @overload + 6 | def foo(a: int, b: int, c: int): ... + 7 | @overload + 8 | def foo(a: str, b: int, c: int): ... + 9 | @overload + 10 | def foo(a: int, b: str, c: int): ... + 11 | @overload + 12 | def foo(a: int, b: int, c: str): ... + 13 | @overload + 14 | def foo(a: str, b: str, c: int): ... + 15 | @overload + 16 | def foo(a: int, b: str, c: str): ... + 17 | @overload + 18 | def foo(a: str, b: str, c: str): ... + 19 | @overload + 20 | def foo(a: int, b: int, c: int): ... + 21 | @overload + 22 | def foo(a: float, b: int, c: int): ... + 23 | @overload + 24 | def foo(a: int, b: float, c: int): ... + 25 | @overload + 26 | def foo(a: int, b: int, c: float): ... + 27 | @overload + 28 | def foo(a: float, b: float, c: int): ... + 29 | @overload + 30 | def foo(a: int, b: float, c: float): ... + 31 | @overload + 32 | def foo(a: float, b: float, c: float): ... + 33 | @overload + 34 | def foo(a: str, b: str, c: str): ... + 35 | @overload + 36 | def foo(a: float, b: str, c: str): ... + 37 | @overload + 38 | def foo(a: str, b: float, c: str): ... + 39 | @overload + 40 | def foo(a: str, b: str, c: float): ... + 41 | @overload + 42 | def foo(a: float, b: float, c: str): ... + 43 | @overload + 44 | def foo(a: str, b: float, c: float): ... + 45 | @overload + 46 | def foo(a: float, b: float, c: float): ... + 47 | @overload + 48 | def foo(a: list[int], b: list[int], c: list[int]): ... + 49 | @overload + 50 | def foo(a: list[str], b: list[int], c: list[int]): ... + 51 | @overload + 52 | def foo(a: list[int], b: list[str], c: list[int]): ... + 53 | @overload + 54 | def foo(a: list[int], b: list[int], c: list[str]): ... + 55 | @overload + 56 | def foo(a: list[str], b: list[str], c: list[int]): ... + 57 | @overload + 58 | def foo(a: list[int], b: list[str], c: list[str]): ... + 59 | @overload + 60 | def foo(a: list[str], b: list[str], c: list[str]): ... + 61 | @overload + 62 | def foo(a: list[int], b: list[int], c: list[int]): ... + 63 | @overload + 64 | def foo(a: list[float], b: list[int], c: list[int]): ... + 65 | @overload + 66 | def foo(a: list[int], b: list[float], c: list[int]): ... + 67 | @overload + 68 | def foo(a: list[int], b: list[int], c: list[float]): ... + 69 | @overload + 70 | def foo(a: list[float], b: list[float], c: list[int]): ... + 71 | @overload + 72 | def foo(a: list[int], b: list[float], c: list[float]): ... + 73 | @overload + 74 | def foo(a: list[float], b: list[float], c: list[float]): ... + 75 | @overload + 76 | def foo(a: list[str], b: list[str], c: list[str]): ... + 77 | @overload + 78 | def foo(a: list[float], b: list[str], c: list[str]): ... + 79 | @overload + 80 | def foo(a: list[str], b: list[float], c: list[str]): ... + 81 | @overload + 82 | def foo(a: list[str], b: list[str], c: list[float]): ... + 83 | @overload + 84 | def foo(a: list[float], b: list[float], c: list[str]): ... + 85 | @overload + 86 | def foo(a: list[str], b: list[float], c: list[float]): ... + 87 | @overload + 88 | def foo(a: list[float], b: list[float], c: list[float]): ... + 89 | @overload + 90 | def foo(a: bool, b: bool, c: bool): ... + 91 | @overload + 92 | def foo(a: str, b: bool, c: bool): ... + 93 | @overload + 94 | def foo(a: bool, b: str, c: bool): ... + 95 | @overload + 96 | def foo(a: bool, b: bool, c: str): ... + 97 | @overload + 98 | def foo(a: str, b: str, c: bool): ... + 99 | @overload +100 | def foo(a: bool, b: str, c: str): ... +101 | @overload +102 | def foo(a: str, b: str, c: str): ... +103 | @overload +104 | def foo(a: int, b: int, c: int): ... +105 | @overload +106 | def foo(a: bool, b: int, c: int): ... +107 | @overload +108 | def foo(a: int, b: bool, c: int): ... +109 | @overload +110 | def foo(a: int, b: int, c: bool): ... +111 | @overload +112 | def foo(a: bool, b: bool, c: int): ... +113 | @overload +114 | def foo(a: int, b: bool, c: bool): ... +115 | @overload +116 | def foo(a: str, b: str, c: str): ... +117 | @overload +118 | def foo(a: float, b: bool, c: bool): ... +119 | @overload +120 | def foo(a: bool, b: float, c: bool): ... +121 | @overload +122 | def foo(a: bool, b: bool, c: float): ... +123 | @overload +124 | def foo(a: float, b: float, c: bool): ... +125 | @overload +126 | def foo(a: bool, b: float, c: float): ... +``` + +## mdtest_snippet.py + +``` +1 | from typing import overload +2 | +3 | from overloaded import foo +4 | +5 | foo("foo") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:5:5 + | +3 | from overloaded import foo +4 | +5 | foo("foo") # error: [invalid-argument-type] + | ^^^^^ Expected `int`, found `Literal["foo"]` + | +info: Matching overload defined here + --> src/overloaded.pyi:4:5 + | +3 | @overload +4 | def foo(a: int): ... + | ^^^ ------ Parameter declared here +5 | @overload +6 | def foo(a: int, b: int, c: int): ... + | +info: Non-matching overloads for function `foo`: +info: (a: int, b: int, c: int) -> Unknown +info: (a: str, b: int, c: int) -> Unknown +info: (a: int, b: str, c: int) -> Unknown +info: (a: int, b: int, c: str) -> Unknown +info: (a: str, b: str, c: int) -> Unknown +info: (a: int, b: str, c: str) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int, b: int, c: int) -> Unknown +info: (a: int | float, b: int, c: int) -> Unknown +info: (a: int, b: int | float, c: int) -> Unknown +info: (a: int, b: int, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int) -> Unknown +info: (a: int, b: int | float, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: (a: int | float, b: str, c: str) -> Unknown +info: (a: str, b: int | float, c: str) -> Unknown +info: (a: str, b: str, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: str) -> Unknown +info: (a: str, b: int | float, c: int | float) -> Unknown +info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: list[int], b: list[int], c: list[int]) -> Unknown +info: (a: list[str], b: list[int], c: list[int]) -> Unknown +info: (a: list[int], b: list[str], c: list[int]) -> Unknown +info: (a: list[int], b: list[int], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[int]) -> Unknown +info: (a: list[int], b: list[str], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[str]) -> Unknown +info: (a: list[int], b: list[int], c: list[int]) -> Unknown +info: (a: list[int | float], b: list[int], c: list[int]) -> Unknown +info: (a: list[int], b: list[int | float], c: list[int]) -> Unknown +info: (a: list[int], b: list[int], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[int]) -> Unknown +info: (a: list[int], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[str], b: list[str], c: list[str]) -> Unknown +info: (a: list[int | float], b: list[str], c: list[str]) -> Unknown +info: (a: list[str], b: list[int | float], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[str]) -> Unknown +info: (a: list[str], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: bool, b: bool, c: bool) -> Unknown +info: (a: str, b: bool, c: bool) -> Unknown +info: (a: bool, b: str, c: bool) -> Unknown +info: (a: bool, b: bool, c: str) -> Unknown +info: (a: str, b: str, c: bool) -> Unknown +info: (a: bool, b: str, c: str) -> Unknown +info: (a: str, b: str, c: str) -> Unknown +info: ... omitted 12 overloads +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" new file mode 100644 index 0000000000000..198de896b9a39 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" @@ -0,0 +1,59 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: single_matching_overload.md - Single matching overload - Limited number of overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md +--- + +# Python source files + +## overloaded.pyi + +``` +1 | from typing import overload +2 | +3 | @overload +4 | def f() -> None: ... +5 | @overload +6 | def f(x: int) -> int: ... +7 | @overload +8 | def f(x: int, y: int) -> int: ... +``` + +## mdtest_snippet.py + +``` +1 | from overloaded import f +2 | +3 | f("a") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:3:3 + | +1 | from overloaded import f +2 | +3 | f("a") # error: [invalid-argument-type] + | ^^^ Expected `int`, found `Literal["a"]` + | +info: Matching overload defined here + --> src/overloaded.pyi:6:5 + | +4 | def f() -> None: ... +5 | @overload +6 | def f(x: int) -> int: ... + | ^ ------ Parameter declared here +7 | @overload +8 | def f(x: int, y: int) -> int: ... + | +info: Non-matching overloads for function `f`: +info: () -> None +info: (x: int, y: int) -> int +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Accidental_use_of_no\342\200\246_(b07503f9b773ea61).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Accidental_use_of_no\342\200\246_(b07503f9b773ea61).snap" new file mode 100644 index 0000000000000..b8243d669fa7e --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Accidental_use_of_no\342\200\246_(b07503f9b773ea61).snap" @@ -0,0 +1,85 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: sync.md - With statements - Accidental use of non-async `with` +mdtest path: crates/ty_python_semantic/resources/mdtest/with/sync.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class Manager: + 2 | async def __aenter__(self): ... + 3 | async def __aexit__(self, *args): ... + 4 | + 5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" + 6 | with Manager(): + 7 | ... + 8 | class Manager: + 9 | async def __aenter__(self): ... +10 | async def __aexit__(self, typ: str, exc, traceback): ... +11 | +12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +13 | with Manager(): +14 | ... +15 | class Manager: +16 | async def __aenter__(self, wrong_extra_arg): ... +17 | async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ... +18 | +19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +20 | with Manager(): +21 | ... +``` + +# Diagnostics + +``` +error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__` + --> src/mdtest_snippet.py:6:6 + | +5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and... +6 | with Manager(): + | ^^^^^^^^^ +7 | ... +8 | class Manager: + | +info: Objects of type `Manager` can be used as async context managers +info: Consider using `async with` here +info: rule `invalid-context-manager` is enabled by default + +``` + +``` +error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__` + --> src/mdtest_snippet.py:13:6 + | +12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an... +13 | with Manager(): + | ^^^^^^^^^ +14 | ... +15 | class Manager: + | +info: Objects of type `Manager` can be used as async context managers +info: Consider using `async with` here +info: rule `invalid-context-manager` is enabled by default + +``` + +``` +error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__` + --> src/mdtest_snippet.py:20:6 + | +19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an... +20 | with Manager(): + | ^^^^^^^^^ +21 | ... + | +info: Objects of type `Manager` can be used as async context managers +info: Consider using `async with` here +info: rule `invalid-context-manager` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" new file mode 100644 index 0000000000000..e8fe6a5285d02 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" @@ -0,0 +1,49 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__` +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class NotBoolable: + 2 | __bool__: int = 5 + 3 | + 4 | class Comparable: + 5 | def __lt__(self, other) -> NotBoolable: + 6 | return NotBoolable() + 7 | + 8 | def __gt__(self, other) -> NotBoolable: + 9 | return NotBoolable() +10 | +11 | a = (1, Comparable()) +12 | b = (1, Comparable()) +13 | +14 | # error: [unsupported-bool-conversion] +15 | a < b < b +16 | +17 | a < b # fine +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable | Literal[False]` + --> src/mdtest_snippet.py:15:1 + | +14 | # error: [unsupported-bool-conversion] +15 | a < b < b + | ^^^^^ +16 | +17 | a < b # fine + | +info: `__bool__` on `NotBoolable | Literal[False]` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" new file mode 100644 index 0000000000000..10cc6b703f850 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" @@ -0,0 +1,39 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__` +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | __bool__: None = None +3 | +4 | class A: +5 | def __eq__(self, other) -> NotBoolable: +6 | return NotBoolable() +7 | +8 | # error: [unsupported-bool-conversion] +9 | (A(),) == (A(),) +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:9:1 + | +8 | # error: [unsupported-bool-conversion] +9 | (A(),) == (A(),) + | ^^^^^^^^^^^^^^^^ + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" new file mode 100644 index 0000000000000..f332f243c3926 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" @@ -0,0 +1,70 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: union_call.md - Calling a union of function types - A smaller scale example +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def f1() -> int: + 2 | return 0 + 3 | + 4 | def f2(name: str) -> int: + 5 | return 0 + 6 | + 7 | def _(flag: bool): + 8 | if flag: + 9 | f = f1 +10 | else: +11 | f = f2 +12 | # error: [too-many-positional-arguments] +13 | # error: [invalid-argument-type] +14 | x = f(3) +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f2` is incorrect + --> src/mdtest_snippet.py:14:11 + | +12 | # error: [too-many-positional-arguments] +13 | # error: [invalid-argument-type] +14 | x = f(3) + | ^ Expected `str`, found `Literal[3]` + | +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +2 | return 0 +3 | +4 | def f2(name: str) -> int: + | ^^ --------- Parameter declared here +5 | return 0 + | +info: Union variant `def f2(name: str) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int)` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[too-many-positional-arguments]: Too many positional arguments to function `f1`: expected 0, got 1 + --> src/mdtest_snippet.py:14:11 + | +12 | # error: [too-many-positional-arguments] +13 | # error: [invalid-argument-type] +14 | x = f(3) + | ^ + | +info: Union variant `def f1() -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int)` +info: rule `too-many-positional-arguments` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" new file mode 100644 index 0000000000000..4c278cbf2d12b --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" @@ -0,0 +1,54 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: union_call.md - Calling a union of function types - Multiple variants but only one is invalid +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def f1(a: int) -> int: + 2 | return 0 + 3 | + 4 | def f2(name: str) -> int: + 5 | return 0 + 6 | + 7 | def _(flag: bool): + 8 | if flag: + 9 | f = f1 +10 | else: +11 | f = f2 +12 | # error: [invalid-argument-type] +13 | x = f(3) +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f2` is incorrect + --> src/mdtest_snippet.py:13:11 + | +11 | f = f2 +12 | # error: [invalid-argument-type] +13 | x = f(3) + | ^ Expected `str`, found `Literal[3]` + | +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +2 | return 0 +3 | +4 | def f2(name: str) -> int: + | ^^ --------- Parameter declared here +5 | return 0 + | +info: Union variant `def f2(name: str) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1(a: int) -> int) | (def f2(name: str) -> int)` +info: rule `invalid-argument-type` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" new file mode 100644 index 0000000000000..0403b492357ad --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" @@ -0,0 +1,61 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: union_call.md - Calling a union of function types - Try to cover all possible reasons - Cover keyword argument related reasons +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | def any(*args, **kwargs) -> int: + 2 | return 0 + 3 | + 4 | def f1(name: str) -> int: + 5 | return 0 + 6 | + 7 | def _(n: int): + 8 | if n == 0: + 9 | f = f1 +10 | else: +11 | f = any +12 | # error: [parameter-already-assigned] +13 | # error: [unknown-argument] +14 | y = f("foo", name="bar", unknown="quux") +``` + +# Diagnostics + +``` +error[parameter-already-assigned]: Multiple values provided for parameter `name` of function `f1` + --> src/mdtest_snippet.py:14:18 + | +12 | # error: [parameter-already-assigned] +13 | # error: [unknown-argument] +14 | y = f("foo", name="bar", unknown="quux") + | ^^^^^^^^^^ + | +info: Union variant `def f1(name: str) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)` +info: rule `parameter-already-assigned` is enabled by default + +``` + +``` +error[unknown-argument]: Argument `unknown` does not match any known parameter of function `f1` + --> src/mdtest_snippet.py:14:30 + | +12 | # error: [parameter-already-assigned] +13 | # error: [unknown-argument] +14 | y = f("foo", name="bar", unknown="quux") + | ^^^^^^^^^^^^^^ + | +info: Union variant `def f1(name: str) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)` +info: rule `unknown-argument` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" new file mode 100644 index 0000000000000..b3244bc8cb9e6 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" @@ -0,0 +1,248 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: union_call.md - Calling a union of function types - Try to cover all possible reasons - Cover non-keyword related reasons +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from inspect import getattr_static + 2 | from typing import overload + 3 | + 4 | def f1() -> int: + 5 | return 0 + 6 | + 7 | def f2(name: str) -> int: + 8 | return 0 + 9 | +10 | def f3(a: int, b: int) -> int: +11 | return 0 +12 | +13 | def f4[T: str](x: T) -> int: +14 | return 0 +15 | +16 | @overload +17 | def f5() -> None: ... +18 | @overload +19 | def f5(x: str) -> str: ... +20 | def f5(x: str | None = None) -> str | None: +21 | return x +22 | +23 | @overload +24 | def f6() -> None: ... +25 | @overload +26 | def f6(x: str, y: str) -> str: ... +27 | def f6(x: str | None = None, y: str | None = None) -> str | None: +28 | return x + y if x and y else None +29 | +30 | def _(n: int): +31 | class PossiblyNotCallable: +32 | if n == 0: +33 | def __call__(self) -> int: +34 | return 0 +35 | +36 | if n == 0: +37 | f = f1 +38 | elif n == 1: +39 | f = f2 +40 | elif n == 2: +41 | f = f3 +42 | elif n == 3: +43 | f = f4 +44 | elif n == 4: +45 | f = 5 +46 | elif n == 5: +47 | f = f5 +48 | elif n == 6: +49 | f = f6 +50 | else: +51 | f = PossiblyNotCallable() +52 | # error: [too-many-positional-arguments] +53 | # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`" +54 | # error: [missing-argument] +55 | # error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`" +56 | # error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`" +57 | # error: [no-matching-overload] "No overload of function `f6` matches arguments" +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) +``` + +# Diagnostics + +``` +error[call-non-callable]: Object of type `Literal[5]` is not callable + --> src/mdtest_snippet.py:60:9 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^^^^ + | +info: Union variant `Literal[5]` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `call-non-callable` is enabled by default + +``` + +``` +error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method) + --> src/mdtest_snippet.py:60:9 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^^^^ + | +info: Union variant `PossiblyNotCallable` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `call-non-callable` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `b` of function `f3` + --> src/mdtest_snippet.py:60:9 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^^^^ + | +info: Union variant `def f3(a: int, b: int) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `missing-argument` is enabled by default + +``` + +``` +error[no-matching-overload]: No overload of function `f6` matches arguments + --> src/mdtest_snippet.py:60:9 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:24:5 + | +23 | @overload +24 | def f6() -> None: ... + | ^^^^^^^^^^^^ +25 | @overload +26 | def f6(x: str, y: str) -> str: ... + | +info: Possible overloads for function `f6`: +info: () -> None +info: (x: str, y: str) -> str +info: Overload implementation defined here + --> src/mdtest_snippet.py:27:5 + | +25 | @overload +26 | def f6(x: str, y: str) -> str: ... +27 | def f6(x: str | None = None, y: str | None = None) -> str | None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +28 | return x + y if x and y else None + | +info: Union variant `Overload[() -> None, (x: str, y: str) -> str]` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `no-matching-overload` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f2` is incorrect + --> src/mdtest_snippet.py:60:11 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^ Expected `str`, found `Literal[3]` + | +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +5 | return 0 +6 | +7 | def f2(name: str) -> int: + | ^^ --------- Parameter declared here +8 | return 0 + | +info: Union variant `def f2(name: str) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f4` is incorrect + --> src/mdtest_snippet.py:60:11 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^ Argument type `Literal[3]` does not satisfy upper bound of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:13:8 + | +11 | return 0 +12 | +13 | def f4[T: str](x: T) -> int: + | ^^^^^^ +14 | return 0 + | +info: Union variant `def f4(x: T) -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f5` is incorrect + --> src/mdtest_snippet.py:60:11 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^ Expected `str`, found `Literal[3]` + | +info: Matching overload defined here + --> src/mdtest_snippet.py:19:5 + | +17 | def f5() -> None: ... +18 | @overload +19 | def f5(x: str) -> str: ... + | ^^ ------ Parameter declared here +20 | def f5(x: str | None = None) -> str | None: +21 | return x + | +info: Non-matching overloads for function `f5`: +info: () -> None +info: Union variant `Overload[() -> None, (x: str) -> str]` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[too-many-positional-arguments]: Too many positional arguments to function `f1`: expected 0, got 1 + --> src/mdtest_snippet.py:60:11 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^ + | +info: Union variant `def f1() -> int` is incompatible with this call site +info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `too-many-positional-arguments` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_valu\342\200\246_(f920ea85eefe9cfe).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_valu\342\200\246_(f920ea85eefe9cfe).snap" new file mode 100644 index 0000000000000..55d6a1a7ea7e5 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_valu\342\200\246_(f920ea85eefe9cfe).snap" @@ -0,0 +1,31 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unpacking.md - Unpacking - Exactly too few values to unpack +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | a, b = (1,) # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Not enough values to unpack + --> src/mdtest_snippet.py:1:1 + | +1 | a, b = (1,) # error: [invalid-assignment] + | ^^^^ ---- Got 1 + | | + | Expected 2 + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_val\342\200\246_(a53a2aec02bc999).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_val\342\200\246_(a53a2aec02bc999).snap" new file mode 100644 index 0000000000000..8aad685ea46f9 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_val\342\200\246_(a53a2aec02bc999).snap" @@ -0,0 +1,31 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unpacking.md - Unpacking - Exactly too many values to unpack +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | a, b = (1, 2, 3) # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Too many values to unpack + --> src/mdtest_snippet.py:1:1 + | +1 | a, b = (1, 2, 3) # error: [invalid-assignment] + | ^^^^ --------- Got 3 + | | + | Expected 2 + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_\342\200\246_(fae6e2d526396252).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_\342\200\246_(fae6e2d526396252).snap" new file mode 100644 index 0000000000000..e26eb8fae48bc --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_\342\200\246_(fae6e2d526396252).snap" @@ -0,0 +1,30 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unpacking.md - Unpacking - Right hand side not iterable +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | a, b = 1 # error: [not-iterable] +``` + +# Diagnostics + +``` +error[not-iterable]: Object of type `Literal[1]` is not iterable + --> src/mdtest_snippet.py:1:8 + | +1 | a, b = 1 # error: [not-iterable] + | ^ + | +info: It doesn't have an `__iter__` method or a `__getitem__` method +info: rule `not-iterable` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_un\342\200\246_(cef19e6b2b58e6a3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_un\342\200\246_(cef19e6b2b58e6a3).snap" new file mode 100644 index 0000000000000..c255da479bee2 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_un\342\200\246_(cef19e6b2b58e6a3).snap" @@ -0,0 +1,31 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unpacking.md - Unpacking - Too few values to unpack +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error[invalid-assignment]: Not enough values to unpack + --> src/mdtest_snippet.py:1:1 + | +1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment] + | ^^^^^^^^^^^^^ ------ Got 2 + | | + | Expected at least 3 + | +info: rule `invalid-assignment` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" new file mode 100644 index 0000000000000..633cb50b16dd7 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_import.md - Unresolved import diagnostics - An unresolvable import that does not use `from` +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | import does_not_exist # error: [unresolved-import] +2 | +3 | x = does_not_exist.foo +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> src/mdtest_snippet.py:1:8 + | +1 | import does_not_exist # error: [unresolved-import] + | ^^^^^^^^^^^^^^ +2 | +3 | x = does_not_exist.foo + | +info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" new file mode 100644 index 0000000000000..2972b4a66cb71 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" @@ -0,0 +1,36 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with a resolvable module but unresolvable item +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +--- + +# Python source files + +## a.py + +``` +1 | does_exist1 = 1 +2 | does_exist2 = 2 +``` + +## mdtest_snippet.py + +``` +1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] +``` + +# Diagnostics + +``` +error[unresolved-import]: Module `a` has no member `does_not_exist` + --> src/mdtest_snippet.py:1:28 + | +1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] + | ^^^^^^^^^^^^^^ + | +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" new file mode 100644 index 0000000000000..5288e0941c30f --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown nested module +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] +2 | +3 | stat = add(10, 15) +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `.does_not_exist.foo.bar` + --> src/mdtest_snippet.py:1:7 + | +1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] + | ^^^^^^^^^^^^^^^^^^^^^^ +2 | +3 | stat = add(10, 15) + | +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" new file mode 100644 index 0000000000000..7fd8b0c264552 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown current module +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from .does_not_exist import add # error: [unresolved-import] +2 | +3 | stat = add(10, 15) +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `.does_not_exist` + --> src/mdtest_snippet.py:1:7 + | +1 | from .does_not_exist import add # error: [unresolved-import] + | ^^^^^^^^^^^^^^ +2 | +3 | stat = add(10, 15) + | +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" new file mode 100644 index 0000000000000..0d687bf11c773 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" @@ -0,0 +1,34 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unresolvable module +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from does_not_exist import add # error: [unresolved-import] +2 | +3 | stat = add(10, 15) +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> src/mdtest_snippet.py:1:6 + | +1 | from does_not_exist import add # error: [unresolved-import] + | ^^^^^^^^^^^^^^ +2 | +3 | stat = add(10, 15) + | +info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" new file mode 100644 index 0000000000000..ea6e6e88a1ec2 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" @@ -0,0 +1,45 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with too many leading dots +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +--- + +# Python source files + +## package/__init__.py + +``` +``` + +## package/foo.py + +``` +1 | def add(x, y): +2 | return x + y +``` + +## package/subpackage/subsubpackage/__init__.py + +``` +1 | from ....foo import add # error: [unresolved-import] +2 | +3 | stat = add(10, 15) +``` + +# Diagnostics + +``` +error[unresolved-import]: Cannot resolve imported module `....foo` + --> src/package/subpackage/subsubpackage/__init__.py:1:10 + | +1 | from ....foo import add # error: [unresolved-import] + | ^^^ +2 | +3 | stat = add(10, 15) + | +info: rule `unresolved-import` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" new file mode 100644 index 0000000000000..fd14c51a15ea4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" @@ -0,0 +1,31 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unresolved_reference.md - Diagnostics for unresolved references - New builtin used on old Python version +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | aiter # error: [unresolved-reference] +``` + +# Diagnostics + +``` +error[unresolved-reference]: Name `aiter` used when not defined + --> src/mdtest_snippet.py:1:1 + | +1 | aiter # error: [unresolved-reference] + | ^^^^^ + | +info: `aiter` was added as a builtin in Python 3.10 +info: Python 3.9 was assumed when resolving types because it was specified on the command line +info: rule `unresolved-reference` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" new file mode 100644 index 0000000000000..343aaccc77d28 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" @@ -0,0 +1,37 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` attribute, but it's not callable +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | __bool__: int = 3 +3 | +4 | a = NotBoolable() +5 | +6 | # error: [unsupported-bool-conversion] +7 | 10 and a and True +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:7:8 + | +6 | # error: [unsupported-bool-conversion] +7 | 10 and a and True + | ^ + | +info: `__bool__` on `NotBoolable` must be callable +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" new file mode 100644 index 0000000000000..09343ef5d62a2 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" @@ -0,0 +1,47 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` method, but has an incorrect return type +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | def __bool__(self) -> str: +3 | return "wat" +4 | +5 | a = NotBoolable() +6 | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:8:8 + | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True + | ^ + | +info: `str` is not assignable to `bool` + --> src/mdtest_snippet.py:2:9 + | +1 | class NotBoolable: +2 | def __bool__(self) -> str: + | -------- ^^^ Incorrect return type + | | + | Method defined here +3 | return "wat" + | +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" new file mode 100644 index 0000000000000..9957e4c64f38c --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" @@ -0,0 +1,47 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` method, but has incorrect parameters +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | def __bool__(self, foo): +3 | return False +4 | +5 | a = NotBoolable() +6 | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable` + --> src/mdtest_snippet.py:8:8 + | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True + | ^ + | +info: `__bool__` methods must only have a `self` parameter + --> src/mdtest_snippet.py:2:9 + | +1 | class NotBoolable: +2 | def __bool__(self, foo): + | --------^^^^^^^^^^^ Incorrect parameters + | | + | Method defined here +3 | return False + | +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" new file mode 100644 index 0000000000000..22d0cc6adade4 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" @@ -0,0 +1,44 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Part of a union where at least one member has incorrect `__bool__` method +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class NotBoolable1: + 2 | def __bool__(self) -> str: + 3 | return "wat" + 4 | + 5 | class NotBoolable2: + 6 | pass + 7 | + 8 | class NotBoolable3: + 9 | __bool__: int = 3 +10 | +11 | def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3: +12 | return NotBoolable2() +13 | +14 | # error: [unsupported-bool-conversion] +15 | 10 and get() and True +``` + +# Diagnostics + +``` +error[unsupported-bool-conversion]: Boolean conversion is unsupported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly + --> src/mdtest_snippet.py:15:8 + | +14 | # error: [unsupported-bool-conversion] +15 | 10 and get() and True + | ^^^^^ + | +info: rule `unsupported-bool-conversion` is enabled by default + +``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" new file mode 100644 index 0000000000000..2500cc4544dc3 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" @@ -0,0 +1,32 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: version_related_syntax_errors.md - Version-related syntax error diagnostics - `match` statement - Before 3.10 +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" +2 | case 1: +3 | print("it's one") +``` + +# Diagnostics + +``` +error[invalid-syntax] + --> src/mdtest_snippet.py:1:1 + | +1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" + | ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) +2 | case 1: +3 | print("it's one") + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md rename to crates/ty_python_semantic/resources/mdtest/statically_known_branches.md index 20f8efe7bbca2..f6d65447cf1d3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md +++ b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md @@ -163,7 +163,7 @@ other ## Based on type inference -For the the rest of this test suite, we will mostly use `True` and `False` literals to indicate +For the rest of this test suite, we will mostly use `True` and `False` literals to indicate statically known conditions, but here, we show that the results are truly based on type inference, not some special handling of specific conditions in semantic index building. We use two modules to demonstrate this, since semantic index building is inherently single-module: @@ -994,8 +994,46 @@ else: reveal_type(x) # revealed: Literal[1] ``` +#### `if` nested inside `while True` + +These are regression test for . First, make sure that we +do not panic in the original scenario: + +```py +def flag() -> bool: + return True + +while True: + if flag(): + break + else: + c = 1 + break + +c # error: [possibly-unresolved-reference] +``` + +And also check that we understand control flow correctly: + +```py +c = 1 + +while True: + if False: + c = 2 + break + break + +reveal_type(c) # revealed: Literal[1] +``` + ## `match` statements +```toml +[environment] +python-version = "3.10" +``` + ### Single-valued types, always true ```py @@ -1118,6 +1156,7 @@ def _(s: str): ```toml [environment] python-platform = "darwin" +python-version = "3.10" ``` ```py @@ -1203,18 +1242,27 @@ def f() -> None: #### `if True` +`mod.py`: + ```py x: str if True: x: int +``` -def f() -> None: - reveal_type(x) # revealed: int +`main.py`: + +```py +from mod import x + +reveal_type(x) # revealed: int ``` #### `if False … else` +`mod.py`: + ```py x: str @@ -1222,13 +1270,20 @@ if False: pass else: x: int +``` -def f() -> None: - reveal_type(x) # revealed: int +`main.py`: + +```py +from mod import x + +reveal_type(x) # revealed: int ``` ### Ambiguous +`mod.py`: + ```py def flag() -> bool: return True @@ -1237,9 +1292,14 @@ x: str if flag(): x: int +``` -def f() -> None: - reveal_type(x) # revealed: str | int +`main.py`: + +```py +from mod import x + +reveal_type(x) # revealed: str | int ``` ## Conditional function definitions @@ -1439,6 +1499,8 @@ if False: ```py # error: [unresolved-import] from module import symbol + +reveal_type(symbol) # revealed: Unknown ``` #### Always true, bound diff --git a/crates/ty_python_semantic/resources/mdtest/stubs/class.md b/crates/ty_python_semantic/resources/mdtest/stubs/class.md new file mode 100644 index 0000000000000..58355ec62b143 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/stubs/class.md @@ -0,0 +1,47 @@ +# Class definitions in stubs + +## Cyclical class definition + +```toml +[environment] +python-version = "3.12" +``` + +In type stubs, classes can reference themselves in their base class definitions. For example, in +`typeshed`, we have `class str(Sequence[str]): ...`. + +```pyi +class Foo[T]: ... + +class Bar(Foo[Bar]): ... + +reveal_type(Bar) # revealed: +reveal_type(Bar.__mro__) # revealed: tuple[, , typing.Generic, ] +``` + +## 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 +too. That is, `symbol: type` is the same as `symbol: type = ...`. + +One implication of this is that we'll always treat symbols in class scope as safe to be accessed +from the class object itself. We'll never infer a "pure instance attribute" from a stub. + +`b.pyi`: + +```pyi +from typing import ClassVar + +class C: + class_or_instance_var: int +``` + +```py +from typing import ClassVar, Literal + +from b import C + +# No error here, since we treat `class_or_instance_var` as bound on the class. +reveal_type(C.class_or_instance_var) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md b/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md rename to crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md b/crates/ty_python_semantic/resources/mdtest/stubs/locals.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md rename to crates/ty_python_semantic/resources/mdtest/stubs/locals.md diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md b/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md new file mode 100644 index 0000000000000..1939318c20632 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md @@ -0,0 +1,50 @@ +# Bytes subscripts + +## Indexing + +```py +b = b"\x00abc\xff" + +reveal_type(b[0]) # revealed: Literal[0] +reveal_type(b[1]) # revealed: Literal[97] +reveal_type(b[4]) # revealed: Literal[255] + +reveal_type(b[-1]) # revealed: Literal[255] +reveal_type(b[-2]) # revealed: Literal[99] +reveal_type(b[-5]) # revealed: Literal[0] + +reveal_type(b[False]) # revealed: Literal[0] +reveal_type(b[True]) # revealed: Literal[97] + +x = b[5] # error: [index-out-of-bounds] "Index 5 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" +reveal_type(x) # revealed: Unknown + +y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" +reveal_type(y) # revealed: Unknown + +def _(n: int): + a = b"abcde"[n] + reveal_type(a) # revealed: int +``` + +## Slices + +```py +b: bytes = b"\x00abc\xff" + +reveal_type(b[0:2]) # revealed: Literal[b"\x00a"] +reveal_type(b[-3:]) # revealed: Literal[b"bc\xff"] + +b[0:4:0] # error: [zero-stepsize-in-slice] +b[:4:0] # error: [zero-stepsize-in-slice] +b[0::0] # error: [zero-stepsize-in-slice] +b[::0] # error: [zero-stepsize-in-slice] + +def _(m: int, n: int): + byte_slice1 = b[m:n] + reveal_type(byte_slice1) # revealed: bytes + +def _(s: bytes) -> bytes: + byte_slice2 = s[0:5] + return reveal_type(byte_slice2) # revealed: bytes +``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/class.md b/crates/ty_python_semantic/resources/mdtest/subscript/class.md new file mode 100644 index 0000000000000..86de205086e24 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/subscript/class.md @@ -0,0 +1,104 @@ +# Class subscript + +## Class getitem unbound + +```py +class NotSubscriptable: ... + +# error: "Cannot subscript object of type `` with no `__class_getitem__` method" +a = NotSubscriptable[0] +``` + +## Class getitem + +```py +class Identity: + def __class_getitem__(cls, item: int) -> str: + return str(item) + +reveal_type(Identity[0]) # revealed: str +``` + +## Class getitem union + +```py +def _(flag: bool): + class UnionClassGetItem: + if flag: + def __class_getitem__(cls, item: int) -> str: + return str(item) + else: + def __class_getitem__(cls, item: int) -> int: + return item + + reveal_type(UnionClassGetItem[0]) # revealed: str | int +``` + +## Class getitem with class union + +```py +def _(flag: bool): + class A: + def __class_getitem__(cls, item: int) -> str: + return str(item) + + class B: + def __class_getitem__(cls, item: int) -> int: + return item + + x = A if flag else B + + reveal_type(x) # revealed: | + reveal_type(x[0]) # revealed: str | int +``` + +## Class getitem with unbound method union + +```py +def _(flag: bool): + if flag: + class Spam: + def __class_getitem__(self, x: int) -> str: + return "foo" + + else: + class Spam: ... + # error: [non-subscriptable] "Cannot subscript object of type `` with no `__class_getitem__` method" + # revealed: str | Unknown + reveal_type(Spam[42]) +``` + +## Class getitem non-class union + +```py +def _(flag: bool): + if flag: + class Eggs: + def __class_getitem__(self, x: int) -> str: + return "foo" + + else: + Eggs = 1 + + a = Eggs[42] # error: "Cannot subscript object of type `Literal[1]` with no `__getitem__` method" + + reveal_type(a) # revealed: str | Unknown +``` + +## Intersection of nominal-instance types + +If a subscript operation could succeed for *any* positive element of an intersection, no diagnostic +should be reported even if it would not succeed for some other element of the intersection. + +```py +class Foo: ... + +class Bar: + def __getitem__(self, key: str) -> int: + return 42 + +def f(x: Foo): + if isinstance(x, Bar): + # TODO: should be `int` + reveal_type(x["whatever"]) # revealed: @Todo(Subscript expressions on intersections) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md rename to crates/ty_python_semantic/resources/mdtest/subscript/instance.md diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/lists.md b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md new file mode 100644 index 0000000000000..dff1905d0d9de --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md @@ -0,0 +1,39 @@ +# List subscripts + +## Indexing into lists + +A list can be indexed into with: + +- numbers +- slices + +```py +x = [1, 2, 3] +reveal_type(x) # revealed: list[Unknown] + +# TODO reveal int +reveal_type(x[0]) # revealed: Unknown + +# TODO reveal list[int] +reveal_type(x[0:1]) # revealed: list[Unknown] + +# error: [call-non-callable] +reveal_type(x["a"]) # revealed: Unknown +``` + +## Assignments within list assignment + +In assignment, we might also have a named assignment. This should also get type checked. + +```py +x = [1, 2, 3] +x[0 if (y := 2) else 1] = 5 + +# TODO: better error than "method `__getitem__` not callable on type `list`" +# error: [call-non-callable] +x["a" if (y := 2) else 1] = 6 + +# TODO: better error than "method `__getitem__` not callable on type `list`" +# error: [call-non-callable] +x["a" if (y := 2) else "b"] = 6 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md b/crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md rename to crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/string.md b/crates/ty_python_semantic/resources/mdtest/subscript/string.md new file mode 100644 index 0000000000000..469300c0b4eda --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/subscript/string.md @@ -0,0 +1,92 @@ +# String subscripts + +## Indexing + +```py +s = "abcde" + +reveal_type(s[0]) # revealed: Literal["a"] +reveal_type(s[1]) # revealed: Literal["b"] +reveal_type(s[-1]) # revealed: Literal["e"] +reveal_type(s[-2]) # revealed: Literal["d"] + +reveal_type(s[False]) # revealed: Literal["a"] +reveal_type(s[True]) # revealed: Literal["b"] + +a = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5" +reveal_type(a) # revealed: Unknown + +b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5" +reveal_type(b) # revealed: Unknown + +def _(n: int): + a = "abcde"[n] + reveal_type(a) # revealed: LiteralString +``` + +## Slices + +```py +def _(m: int, n: int, s2: str): + s = "abcde" + + reveal_type(s[0:0]) # revealed: Literal[""] + reveal_type(s[0:1]) # revealed: Literal["a"] + reveal_type(s[0:2]) # revealed: Literal["ab"] + reveal_type(s[0:5]) # revealed: Literal["abcde"] + reveal_type(s[0:6]) # revealed: Literal["abcde"] + reveal_type(s[1:3]) # revealed: Literal["bc"] + + reveal_type(s[-3:5]) # revealed: Literal["cde"] + reveal_type(s[-4:-2]) # revealed: Literal["bc"] + reveal_type(s[-10:10]) # revealed: Literal["abcde"] + + reveal_type(s[0:]) # revealed: Literal["abcde"] + reveal_type(s[2:]) # revealed: Literal["cde"] + reveal_type(s[5:]) # revealed: Literal[""] + reveal_type(s[:2]) # revealed: Literal["ab"] + reveal_type(s[:0]) # revealed: Literal[""] + reveal_type(s[:2]) # revealed: Literal["ab"] + reveal_type(s[:10]) # revealed: Literal["abcde"] + reveal_type(s[:]) # revealed: Literal["abcde"] + + reveal_type(s[::-1]) # revealed: Literal["edcba"] + reveal_type(s[::2]) # revealed: Literal["ace"] + reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"] + reveal_type(s[::-2]) # revealed: Literal["eca"] + reveal_type(s[-1::-3]) # revealed: Literal["eb"] + + reveal_type(s[None:2:None]) # revealed: Literal["ab"] + reveal_type(s[1:None:1]) # revealed: Literal["bcde"] + reveal_type(s[None:None:None]) # revealed: Literal["abcde"] + + start = 1 + stop = None + step = 2 + reveal_type(s[start:stop:step]) # revealed: Literal["bd"] + + reveal_type(s[False:True]) # revealed: Literal["a"] + reveal_type(s[True:3]) # revealed: Literal["bc"] + + s[0:4:0] # error: [zero-stepsize-in-slice] + s[:4:0] # error: [zero-stepsize-in-slice] + s[0::0] # error: [zero-stepsize-in-slice] + s[::0] # error: [zero-stepsize-in-slice] + + substring1 = s[m:n] + reveal_type(substring1) # revealed: LiteralString + + substring2 = s2[0:5] + reveal_type(substring2) # revealed: str +``` + +## Unsupported slice types + +```py +# TODO: It would be great if we raised an error here. This can be done once +# we have support for overloads and generics, and once typeshed has a more +# precise annotation for `str.__getitem__`, that makes use of the generic +# `slice[..]` type. We could then infer `slice[str, str]` here and see that +# it doesn't match the signature of `str.__getitem__`. +"foo"["bar":"baz"] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md new file mode 100644 index 0000000000000..17886ffaef737 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -0,0 +1,221 @@ +# Tuple subscripts + +## Indexing + +```py +t = (1, "a", "b") + +reveal_type(t[0]) # revealed: Literal[1] +reveal_type(t[1]) # revealed: Literal["a"] +reveal_type(t[-1]) # revealed: Literal["b"] +reveal_type(t[-2]) # revealed: Literal["a"] + +reveal_type(t[False]) # revealed: Literal[1] +reveal_type(t[True]) # revealed: Literal["a"] + +a = t[4] # error: [index-out-of-bounds] +reveal_type(a) # revealed: Unknown + +b = t[-4] # error: [index-out-of-bounds] +reveal_type(b) # revealed: Unknown +``` + +## Slices + +```py +def _(m: int, n: int): + t = (1, "a", None, b"b") + + reveal_type(t[0:0]) # revealed: tuple[()] + reveal_type(t[0:1]) # revealed: tuple[Literal[1]] + reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]] + reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None] + + reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]] + reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None] + reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + + reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]] + reveal_type(t[4:]) # revealed: tuple[()] + reveal_type(t[:0]) # revealed: tuple[()] + reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]] + reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + + reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]] + reveal_type(t[::2]) # revealed: tuple[Literal[1], None] + reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]] + reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]] + reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]] + + reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]] + reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]] + reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + + start = 1 + stop = None + step = 2 + reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]] + + reveal_type(t[False:True]) # revealed: tuple[Literal[1]] + reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None] + + t[0:4:0] # error: [zero-stepsize-in-slice] + t[:4:0] # error: [zero-stepsize-in-slice] + t[0::0] # error: [zero-stepsize-in-slice] + t[::0] # error: [zero-stepsize-in-slice] + + tuple_slice = t[m:n] + reveal_type(tuple_slice) # revealed: tuple[Literal[1, "a", b"b"] | None, ...] +``` + +## Slices of homogeneous and mixed tuples + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Literal + +def homogeneous(t: tuple[str, ...]) -> None: + reveal_type(t[0]) # revealed: str + reveal_type(t[1]) # revealed: str + reveal_type(t[2]) # revealed: str + reveal_type(t[3]) # revealed: str + + reveal_type(t[-1]) # revealed: str + reveal_type(t[-2]) # revealed: str + reveal_type(t[-3]) # revealed: str + reveal_type(t[-4]) # revealed: str + +def mixed(s: tuple[str, ...]) -> None: + t = (1, 2, 3) + s + (8, 9, 10) + + reveal_type(t[0]) # revealed: Literal[1] + reveal_type(t[1]) # revealed: Literal[2] + reveal_type(t[2]) # revealed: Literal[3] + reveal_type(t[3]) # revealed: str | Literal[8] + reveal_type(t[4]) # revealed: str | Literal[8, 9] + reveal_type(t[5]) # revealed: str | Literal[8, 9, 10] + + reveal_type(t[-1]) # revealed: Literal[10] + reveal_type(t[-2]) # revealed: Literal[9] + reveal_type(t[-3]) # revealed: Literal[8] + reveal_type(t[-4]) # revealed: Literal[3] | str + reveal_type(t[-5]) # revealed: Literal[2, 3] | str + reveal_type(t[-6]) # revealed: Literal[1, 2, 3] | str +``` + +## `tuple` as generic alias + +For tuple instances, we can track more detailed information about the length and element types of +the tuple. This information carries over to the generic alias that the tuple is an instance of. + +```py +def _(a: tuple, b: tuple[int], c: tuple[int, str], d: tuple[int, ...]) -> None: + reveal_type(a) # revealed: tuple[Unknown, ...] + reveal_type(b) # revealed: tuple[int] + reveal_type(c) # revealed: tuple[int, str] + reveal_type(d) # revealed: tuple[int, ...] + +reveal_type(tuple) # revealed: +reveal_type(tuple[int]) # revealed: +reveal_type(tuple[int, str]) # revealed: +reveal_type(tuple[int, ...]) # revealed: +``` + +## Inheritance + +```toml +[environment] +python-version = "3.9" +``` + +```py +class A(tuple[int, str]): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(A.__mro__) + +class C(tuple): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(C.__mro__) +``` + +## `typing.Tuple` + +### Correspondence with `tuple` + +`typing.Tuple` can be used interchangeably with `tuple`: + +```py +from typing import Any, Tuple + +class A: ... + +def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]): + reveal_type(c) # revealed: tuple[Unknown, ...] + reveal_type(d) # revealed: tuple[int, A] + reveal_type(e) # revealed: tuple[Any, ...] +``` + +### Inheritance + +Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself +is not a class. + +```toml +[environment] +python-version = "3.9" +``` + +```py +from typing import Tuple + +class A(Tuple[int, str]): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(A.__mro__) + +class C(Tuple): ... + +# revealed: tuple[, , , , , , , typing.Protocol, typing.Generic, ] +reveal_type(C.__mro__) +``` + +### Union subscript access + +```py +def test(val: tuple[str] | tuple[int]): + reveal_type(val[0]) # revealed: str | int + +def test2(val: tuple[str, None] | list[int | float]): + reveal_type(val[0]) # revealed: str | int | float +``` + +### Union subscript access with non-indexable type + +```py +def test3(val: tuple[str] | tuple[int] | int): + # error: [non-subscriptable] "Cannot subscript object of type `int` with no `__getitem__` method" + reveal_type(val[0]) # revealed: str | int | Unknown +``` + +### Intersection subscript access + +```py +from ty_extensions import Intersection + +class Foo: ... +class Bar: ... + +def test4(val: Intersection[tuple[Foo], tuple[Bar]]): + # TODO: should be `Foo & Bar` + reveal_type(val[0]) # revealed: @Todo(Subscript expressions on intersections) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md b/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md rename to crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md index 21f2cb9dc4b71..37e32020613a7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md @@ -93,8 +93,8 @@ def test() -> Undefined: ## `no_type_check` on classes isn't supported -Red Knot does not support decorating classes with `no_type_check`. The behaviour of `no_type_check` -when applied to classes is +ty does not support decorating classes with `no_type_check`. The behaviour of `no_type_check` when +applied to classes is [not specified currently](https://typing.python.org/en/latest/spec/directives.html#no-type-check), and is not supported by Pyright or mypy. @@ -117,6 +117,6 @@ from typing import no_type_check @no_type_check def test(): - # error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" - return x + 5 # knot: ignore[unresolved-reference] + # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'" + return x + 5 # ty: ignore[unresolved-reference] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md new file mode 100644 index 0000000000000..9a1930f015684 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -0,0 +1,191 @@ +# Suppressing errors with `ty: ignore` + +Type check errors can be suppressed by a `ty: ignore` comment on the same line as the violation. + +## Simple `ty: ignore` + +```py +a = 4 + test # ty: ignore +``` + +## Suppressing a specific code + +```py +a = 4 + test # ty: ignore[unresolved-reference] +``` + +## Unused suppression + +```py +test = 10 +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'possibly-unresolved-reference'" +a = test + 3 # ty: ignore[possibly-unresolved-reference] +``` + +## Unused suppression if the error codes don't match + +```py +# error: [unresolved-reference] +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'possibly-unresolved-reference'" +a = test + 3 # ty: ignore[possibly-unresolved-reference] +``` + +## Suppressed unused comment + +```py +# error: [unused-ignore-comment] +a = 10 / 2 # ty: ignore[division-by-zero] +a = 10 / 2 # ty: ignore[division-by-zero, unused-ignore-comment] +a = 10 / 2 # ty: ignore[unused-ignore-comment, division-by-zero] +a = 10 / 2 # ty: ignore[unused-ignore-comment] # type: ignore +a = 10 / 2 # type: ignore # ty: ignore[unused-ignore-comment] +``` + +## Unused ignore comment + +```py +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unused-ignore-comment'" +a = 10 / 0 # ty: ignore[division-by-zero, unused-ignore-comment] +``` + +## Multiple unused comments + +Today, ty emits a diagnostic for every unused code. We might want to group the codes by comment at +some point in the future. + +```py +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'division-by-zero'" +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'" +a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference] + +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'" +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'" +a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference] +``` + +## Multiple suppressions + +```py +# fmt: off +def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # ty: ignore[fstring-type-annotation, byte-string-type-annotation] +``` + +## Can't suppress syntax errors + + + +```py +# error: [invalid-syntax] +# error: [unused-ignore-comment] +def test($): # ty: ignore + pass +``` + + + +## Can't suppress `revealed-type` diagnostics + +```py +a = 10 +# revealed: Literal[10] +# error: [unknown-rule] "Unknown rule `revealed-type`" +reveal_type(a) # ty: ignore[revealed-type] +``` + +## Extra whitespace in type ignore comments is allowed + +```py +a = 10 / 0 # ty : ignore +a = 10 / 0 # ty: ignore [ division-by-zero ] +``` + +## Whitespace is optional + +```py +# fmt: off +a = 10 / 0 #ty:ignore[division-by-zero] +``` + +## Trailing codes comma + +Trailing commas in the codes section are allowed: + +```py +a = 10 / 0 # ty: ignore[division-by-zero,] +``` + +## Invalid characters in codes + +```py +# error: [division-by-zero] +# error: [invalid-ignore-comment] "Invalid `ty: ignore` comment: expected a alphanumeric character or `-` or `_` as code" +a = 10 / 0 # ty: ignore[*-*] +``` + +## Trailing whitespace + + + +```py +a = 10 / 0 # ty: ignore[division-by-zero] + # ^^^^^^ trailing whitespace +``` + + + +## Missing comma + +A missing comma results in an invalid suppression comment. We may want to recover from this in the +future. + +```py +# error: [unresolved-reference] +# error: [invalid-ignore-comment] "Invalid `ty: ignore` comment: expected a comma separating the rule codes" +a = x / 0 # ty: ignore[division-by-zero unresolved-reference] +``` + +## Missing closing bracket + +```py +# error: [unresolved-reference] "Name `x` used when not defined" +# error: [invalid-ignore-comment] "Invalid `ty: ignore` comment: expected a comma separating the rule codes" +a = x / 2 # ty: ignore[unresolved-reference +``` + +## Empty codes + +An empty codes array suppresses no-diagnostics and is always useless + +```py +# error: [division-by-zero] +# error: [unused-ignore-comment] "Unused `ty: ignore` without a code" +a = 4 / 0 # ty: ignore[] +``` + +## File-level suppression comments + +File level suppression comments are currently intentionally unsupported because we've yet to decide +if they should use a different syntax that also supports enabling rules or changing the rule's +severity: `ty: possibly-undefined-reference=error` + +```py +# error: [unused-ignore-comment] +# ty: ignore[division-by-zero] + +a = 4 / 0 # error: [division-by-zero] +``` + +## Unknown rule + +```py +# error: [unknown-rule] "Unknown rule `is-equal-14`" +a = 10 + 4 # ty: ignore[is-equal-14] +``` + +## Code with `lint:` prefix + +```py +# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?" +# error: [division-by-zero] +a = 10 / 0 # ty: ignore[lint:division-by-zero] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/type_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/suppressions/type_ignore.md rename to crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md index 87cf6ee69e23b..43e9eedcc22fd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/type_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md @@ -87,8 +87,8 @@ a = test \ ## Codes -Mypy supports `type: ignore[code]`. Red Knot doesn't understand mypy's rule names. Therefore, ignore -the codes and suppress all errors. +Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the +codes and suppress all errors. ```py a = test # type: ignore[name-defined] @@ -116,7 +116,7 @@ a = test + 2 # type: ignoree ## Invalid - ignore on opening parentheses `type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses -in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot. +in Pyright. Neither Ruff, nor mypy support this and neither does ty. ```py # fmt: off diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md b/crates/ty_python_semantic/resources/mdtest/sys_platform.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/sys_platform.md rename to crates/ty_python_semantic/resources/mdtest/sys_platform.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md b/crates/ty_python_semantic/resources/mdtest/sys_version_info.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md rename to crates/ty_python_semantic/resources/mdtest/sys_version_info.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md similarity index 78% rename from crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md rename to crates/ty_python_semantic/resources/mdtest/terminal_statements.md index 016ab3d6558af..02b97f2a1d4f8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md @@ -570,25 +570,246 @@ def f(): reveal_type(x) # revealed: Literal[1] ``` +## Calls to functions returning `Never` / `NoReturn` + +These calls should be treated as terminal statements. + +### No implicit return + +If we see a call to a function returning `Never`, we should be able to understand that the function +cannot implicitly return `None`. In the below examples, verify that there are no errors emitted for +invalid return type. + +```py +from typing import NoReturn +import sys + +def f() -> NoReturn: + sys.exit(1) +``` + +Let's try cases where the function annotated with `NoReturn` is some sub-expression. + +```py +from typing import NoReturn +import sys + +# TODO: this is currently not yet supported +# error: [invalid-return-type] +def _() -> NoReturn: + 3 + sys.exit(1) + +# TODO: this is currently not yet supported +# error: [invalid-return-type] +def _() -> NoReturn: + 3 if sys.exit(1) else 4 +``` + +### Type narrowing + +If a variable's type is a union, and some types in the union result in a function marked with +`NoReturn` being called, then we should correctly narrow the variable's type. + +```py +from typing import NoReturn +import sys + +def g(x: int | None): + if x is None: + sys.exit(1) + + # TODO: should be just `int`, not `int | None` + # See https://github.com/astral-sh/ty/issues/685 + reveal_type(x) # revealed: int | None +``` + +### Possibly unresolved diagnostics + +If the codepath on which a variable is not defined eventually returns `Never`, use of the variable +should not give any diagnostics. + +```py +import sys + +def _(flag: bool): + if flag: + x = 3 + else: + sys.exit() + + x # No possibly-unresolved-references diagnostic here. +``` + +Similarly, there shouldn't be any diagnostics if the `except` block of a `try/except` construct has +a call with `NoReturn`. + +```py +import sys + +def _(): + try: + x = 3 + except: + sys.exit() + + x # No possibly-unresolved-references diagnostic here. +``` + +### Bindings in branches + +In case of a `NoReturn` call being present in conditionals, the revealed type of the end of the +branch should reflect the path which did not hit any of the `NoReturn` calls. These tests are +similar to the ones for `return` above. + +```py +import sys + +def call_in_then_branch(cond: bool): + if cond: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + else: + x = "test" + reveal_type(x) # revealed: Literal["test"] + reveal_type(x) # revealed: Literal["test"] + +def call_in_else_branch(cond: bool): + if cond: + x = "test" + reveal_type(x) # revealed: Literal["test"] + else: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + reveal_type(x) # revealed: Literal["test"] + +def call_in_both_branches(cond: bool): + if cond: + x = "terminal1" + reveal_type(x) # revealed: Literal["terminal1"] + sys.exit() + else: + x = "terminal2" + reveal_type(x) # revealed: Literal["terminal2"] + sys.exit() + + reveal_type(x) # revealed: Never + +def call_in_nested_then_branch(cond1: bool, cond2: bool): + if cond1: + x = "test1" + reveal_type(x) # revealed: Literal["test1"] + else: + if cond2: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + else: + x = "test2" + reveal_type(x) # revealed: Literal["test2"] + reveal_type(x) # revealed: Literal["test2"] + reveal_type(x) # revealed: Literal["test1", "test2"] + +def call_in_nested_else_branch(cond1: bool, cond2: bool): + if cond1: + x = "test1" + reveal_type(x) # revealed: Literal["test1"] + else: + if cond2: + x = "test2" + reveal_type(x) # revealed: Literal["test2"] + else: + x = "terminal" + reveal_type(x) # revealed: Literal["terminal"] + sys.exit() + reveal_type(x) # revealed: Literal["test2"] + reveal_type(x) # revealed: Literal["test1", "test2"] + +def call_in_both_nested_branches(cond1: bool, cond2: bool): + if cond1: + x = "test" + reveal_type(x) # revealed: Literal["test"] + else: + x = "terminal0" + if cond2: + x = "terminal1" + reveal_type(x) # revealed: Literal["terminal1"] + sys.exit() + else: + x = "terminal2" + reveal_type(x) # revealed: Literal["terminal2"] + sys.exit() + reveal_type(x) # revealed: Literal["test"] +``` + +### Overloads + +If only some overloads of a function are marked with `NoReturn`, we should run the overload +evaluation algorithm when evaluating the constraints. + +```py +from typing import NoReturn, overload + +@overload +def f(x: int) -> NoReturn: ... +@overload +def f(x: str) -> int: ... +def f(x): ... + +# No errors +def _() -> NoReturn: + f(3) + +# This should be an error because of implicitly returning `None` +# error: [invalid-return-type] +def _() -> NoReturn: + f("") +``` + +### Other callables + +If other types of callables are annotated with `NoReturn`, we should still be ablt to infer correct +reachability. + +```py +import sys + +from typing import NoReturn + +class C: + def __call__(self) -> NoReturn: + sys.exit() + + def die(self) -> NoReturn: + sys.exit() + +# No "implicitly returns `None`" diagnostic +def _() -> NoReturn: + C()() + +# No "implicitly returns `None`" diagnostic +def _() -> NoReturn: + C().die() +``` + ## Nested functions Free references inside of a function body refer to variables defined in the containing scope. Function bodies are _lazy scopes_: at runtime, these references are not resolved immediately at the point of the function definition. Instead, they are resolved _at the time of the call_, which means -that their values (and types) can be different for different invocations. For simplicity, we instead -resolve free references _at the end of the containing scope_. That means that in the examples below, -all of the `x` bindings should be visible to the `reveal_type`, regardless of where we place the -`return` statements. - -TODO: These currently produce the wrong results, but not because of our terminal statement support. -See [ruff#15777](https://github.com/astral-sh/ruff/issues/15777) for more details. +that their values (and types) can be different for different invocations. For simplicity, we +currently consider _all reachable bindings_ in the containing scope: ```py def top_level_return(cond1: bool, cond2: bool): x = 1 def g(): - # TODO eliminate Unknown + # TODO We could potentially eliminate `Unknown` from the union here, + # because `x` resolves to an enclosing function-like scope and there + # are no nested `nonlocal` declarations of that symbol that might + # modify it. reveal_type(x) # revealed: Unknown | Literal[1, 2, 3] if cond1: if cond2: @@ -601,8 +822,7 @@ def return_from_if(cond1: bool, cond2: bool): x = 1 def g(): - # TODO: Literal[1, 2, 3] - reveal_type(x) # revealed: Unknown | Literal[1] + reveal_type(x) # revealed: Unknown | Literal[1, 2, 3] if cond1: if cond2: x = 2 @@ -614,8 +834,7 @@ def return_from_nested_if(cond1: bool, cond2: bool): x = 1 def g(): - # TODO: Literal[1, 2, 3] - reveal_type(x) # revealed: Unknown | Literal[1, 3] + reveal_type(x) # revealed: Unknown | Literal[1, 2, 3] if cond1: if cond2: x = 2 @@ -626,9 +845,9 @@ def return_from_nested_if(cond1: bool, cond2: bool): ## Statically known terminal statements -We model reachability using the same visibility constraints that we use to model statically known -bounds. In this example, we see that the `return` statement is always executed, and therefore that -the `"b"` assignment is not visible to the `reveal_type`. +We model reachability using the same constraints that we use to model statically known bounds. In +this example, we see that the `return` statement is always executed, and therefore that the `"b"` +assignment is not visible to the `reveal_type`. ```py def _(cond: bool): diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_api.md b/crates/ty_python_semantic/resources/mdtest/type_api.md similarity index 79% rename from crates/red_knot_python_semantic/resources/mdtest/type_api.md rename to crates/ty_python_semantic/resources/mdtest/type_api.md index d68656d7252a9..6f7c2c8180272 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_api.md +++ b/crates/ty_python_semantic/resources/mdtest/type_api.md @@ -1,14 +1,14 @@ -# Type API (`knot_extensions`) +# Type API (`ty_extensions`) -This document describes the internal `knot_extensions` API for creating and manipulating types as -well as testing various type system properties. +This document describes the internal `ty_extensions` API for creating and manipulating types as well +as testing various type system properties. ## Type extensions The Python language itself allows us to perform a variety of operations on types. For example, we can build a union of types like `int | None`, or we can use type constructors such as `list[int]` -and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot, -like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the +and `type[int]` to create new types. But some type-level operations that we rely on in ty, like +intersections, cannot yet be expressed in Python. The `ty_extensions` module provides the `Intersection` and `Not` type constructors (special forms) which allow us to construct these types directly. @@ -16,15 +16,19 @@ directly. ```py from typing import Literal -from knot_extensions import Not, static_assert +from ty_extensions import Not, static_assert def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None: reveal_type(n1) # revealed: ~int reveal_type(n2) # revealed: int reveal_type(n3) # revealed: ~int -# error: "Special form `knot_extensions.Not` expected exactly one type parameter" +# error: "Special form `ty_extensions.Not` expected exactly 1 type argument, got 2" n: Not[int, str] +# error: [invalid-type-form] "Special form `ty_extensions.Not` expected exactly 1 type argument, got 0" +o: Not[()] + +p: Not[(int,)] def static_truthiness(not_one: Not[Literal[1]]) -> None: # TODO: `bool` is not incorrect, but these would ideally be `Literal[True]` and `Literal[False]` @@ -42,8 +46,13 @@ def static_truthiness(not_one: Not[Literal[1]]) -> None: ### Intersection +```toml +[environment] +python-version = "3.12" +``` + ```py -from knot_extensions import Intersection, Not, is_subtype_of, static_assert +from ty_extensions import Intersection, Not, is_subtype_of, static_assert from typing_extensions import Literal, Never class S: ... @@ -82,13 +91,11 @@ The `Unknown` type is a special type that we use to represent actually unknown t annotation), as opposed to `Any` which represents an explicitly unknown type. ```py -from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static +from ty_extensions import Unknown, static_assert, is_assignable_to static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) -static_assert(not is_fully_static(Unknown)) - def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None: reveal_type(x) # revealed: Unknown reveal_type(y) # revealed: tuple[str, Unknown] @@ -100,10 +107,10 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None ```py class C(Unknown): ... -# revealed: tuple[Literal[C], Unknown, Literal[object]] +# revealed: tuple[, Unknown, ] reveal_type(C.__mro__) -# error: "Special form `knot_extensions.Unknown` expected no type parameter" +# error: "Special form `ty_extensions.Unknown` expected no type parameter" u: Unknown[str] ``` @@ -117,7 +124,7 @@ They do not accept any type arguments. ```py from typing_extensions import Literal -from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_subtype_of, static_assert +from ty_extensions import AlwaysFalsy, AlwaysTruthy, is_subtype_of, static_assert static_assert(is_subtype_of(Literal[True], AlwaysTruthy)) static_assert(is_subtype_of(Literal[False], AlwaysFalsy)) @@ -141,12 +148,12 @@ def f( ### Basics -The `knot_extensions` module provides a `static_assert` function that can be used to enforce +The `ty_extensions` module provides a `static_assert` function that can be used to enforce properties at type-check time. The function takes an arbitrary expression and raises a type error if the expression is not of statically known truthiness. ```py -from knot_extensions import static_assert +from ty_extensions import static_assert from typing import TYPE_CHECKING import sys @@ -177,7 +184,7 @@ static_assert(sys.version_info >= (3, 6)) Static assertions can be used to enforce narrowing constraints: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert def f(x: int | None) -> None: if x is not None: @@ -191,7 +198,7 @@ def f(x: int | None) -> None: See also: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert static_assert(True) static_assert(False) # error: "Static assertion error: argument evaluates to `False`" @@ -216,7 +223,7 @@ static_assert(b"") # error: "Static assertion error: argument of type `Literal[ We provide various tailored error messages for wrong argument types to `static_assert`: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert static_assert(2 * 3 == 6) @@ -230,7 +237,7 @@ class InvalidBoolDunder: def __bool__(self) -> int: return 1 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`; the return type of its bool method (`int`) isn't assignable to `bool" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`" static_assert(InvalidBoolDunder()) ``` @@ -239,7 +246,7 @@ static_assert(InvalidBoolDunder()) Alternatively, users can provide custom error messages: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert # error: "Static assertion error: I really want this to be true" static_assert(1 + 1 == 3, "I really want this to be true") @@ -261,14 +268,14 @@ static_assert(False, shouted_message) ## Type predicates -The `knot_extensions` module also provides predicates to test various properties of types. These are +The `ty_extensions` module also provides predicates to test various properties of types. These are implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of the test. ### Equivalence ```py -from knot_extensions import is_equivalent_to, static_assert +from ty_extensions import is_equivalent_to, static_assert from typing_extensions import Never, Union static_assert(is_equivalent_to(type, type[object])) @@ -282,7 +289,7 @@ static_assert(not is_equivalent_to(int | str, int | str | bytes)) ### Subtyping ```py -from knot_extensions import is_subtype_of, static_assert +from ty_extensions import is_subtype_of, static_assert static_assert(is_subtype_of(bool, int)) static_assert(not is_subtype_of(str, int)) @@ -306,7 +313,7 @@ static_assert(not is_subtype_of(Base, Unrelated)) ### Assignability ```py -from knot_extensions import is_assignable_to, static_assert +from ty_extensions import is_assignable_to, static_assert from typing import Any static_assert(is_assignable_to(int, Any)) @@ -317,30 +324,17 @@ static_assert(not is_assignable_to(int, str)) ### Disjointness ```py -from knot_extensions import is_disjoint_from, static_assert +from ty_extensions import is_disjoint_from, static_assert from typing import Literal static_assert(is_disjoint_from(None, int)) static_assert(not is_disjoint_from(Literal[2] | str, int)) ``` -### Fully static types - -```py -from knot_extensions import is_fully_static, static_assert -from typing import Any - -static_assert(is_fully_static(int | str)) -static_assert(is_fully_static(type[int])) - -static_assert(not is_fully_static(int | Any)) -static_assert(not is_fully_static(type[Any])) -``` - ### Singleton types ```py -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert from typing import Literal static_assert(is_singleton(None)) @@ -353,7 +347,7 @@ static_assert(not is_singleton(Literal["a"])) ### Single-valued types ```py -from knot_extensions import is_single_valued, static_assert +from ty_extensions import is_single_valued, static_assert from typing import Literal static_assert(is_single_valued(None)) @@ -373,7 +367,7 @@ type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` the expression `str`: ```py -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert # This is incorrect and therefore fails with ... # error: "Static assertion error: argument evaluates to `False`" @@ -391,16 +385,16 @@ class Derived(Base): ... ```py def type_of_annotation() -> None: t1: TypeOf[Base] = Base - t2: TypeOf[Base] = Derived # error: [invalid-assignment] + t2: TypeOf[(Base,)] = Derived # error: [invalid-assignment] # Note how this is different from `type[…]` which includes subclasses: s1: type[Base] = Base s2: type[Base] = Derived # no error here -# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter" +# error: "Special form `ty_extensions.TypeOf` expected exactly 1 type argument, got 3" t: TypeOf[int, str, bytes] -# error: [invalid-type-form] "`knot_extensions.TypeOf` requires exactly one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.TypeOf` requires exactly one argument when used in a type expression" def f(x: TypeOf) -> None: reveal_type(x) # revealed: Unknown ``` @@ -414,7 +408,7 @@ which can then be used to test various type properties. It accepts a single type parameter which is expected to be a callable object. ```py -from knot_extensions import CallableTypeOf +from ty_extensions import CallableTypeOf def f1(): return @@ -425,15 +419,23 @@ def f2() -> int: def f3(x: int, y: str) -> None: return -# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeOf` expected exactly one type parameter" +# error: [invalid-type-form] "Special form `ty_extensions.CallableTypeOf` expected exactly 1 type argument, got 2" c1: CallableTypeOf[f1, f2] -# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`" +# error: [invalid-type-form] "Expected the first argument to `ty_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`" c2: CallableTypeOf["foo"] -# error: [invalid-type-form] "`knot_extensions.CallableTypeOf` requires exactly one argument when used in a type expression" +# error: [invalid-type-form] "Expected the first argument to `ty_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`" +c20: CallableTypeOf[("foo",)] + +# error: [invalid-type-form] "`ty_extensions.CallableTypeOf` requires exactly one argument when used in a type expression" def f(x: CallableTypeOf) -> None: reveal_type(x) # revealed: Unknown + +c3: CallableTypeOf[(f3,)] + +# error: [invalid-type-form] "Special form `ty_extensions.CallableTypeOf` expected exactly 1 type argument, got 0" +c4: CallableTypeOf[()] ``` Using it in annotation to reveal the signature of the callable object: diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/README.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/README.md new file mode 100644 index 0000000000000..99fa88d552b0e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/README.md @@ -0,0 +1,20 @@ +# Type compendium + +The type compendium contains "fact sheets" about important, interesting, and peculiar types in (ty's +interpretation of) Python's type system. It is meant to be an educational reference for developers +and users of ty. It is also a living document that ensures that our implementation of these types +and their properties is consistent with the specification. + +## Table of contents + +- [`Never`](never.md) +- [`Object`](object.md) +- [`None`](none.md) +- [Integer `Literal`s](integer_literals.md) +- String `Literal`s, `LiteralString` +- [`tuple` types](tuple.md) +- Class instance types +- [`Any`](any.md) +- Class literal types, `type[C]`, `type[object]`, `type[Any]` +- [`AlwaysTruthy`, `AlwaysFalsy`](always_truthy_falsy.md) +- [`Not[T]`](not_t.md) diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md new file mode 100644 index 0000000000000..ede8b40a30e5e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/always_truthy_falsy.md @@ -0,0 +1,175 @@ +# `AlwaysTruthy` and `AlwaysFalsy` + +```toml +[environment] +python-version = "3.12" +``` + +The types `AlwaysTruthy` and `AlwaysFalsy` describe the set of values that are always truthy or +always falsy, respectively. More concretely, a value `at` is of type `AlwaysTruthy` if we can +statically infer that `bool(at)` is always `True`, i.e. that the expression `bool(at)` has type +`Literal[True]`. Conversely, a value `af` is of type `AlwaysFalsy` if we can statically infer that +`bool(af)` is always `False`, i.e. that `bool(af)` has type `Literal[False]`. + +## Examples + +Here, we give a few examples of values that belong to these types: + +```py +from ty_extensions import AlwaysTruthy, AlwaysFalsy +from typing_extensions import Literal + +class CustomAlwaysTruthyType: + def __bool__(self) -> Literal[True]: + return True + +class CustomAlwaysFalsyType: + def __bool__(self) -> Literal[False]: + return False + +at: AlwaysTruthy +at = True +at = 1 +at = 123 +at = -1 +at = "non empty" +at = b"non empty" +at = CustomAlwaysTruthyType() + +af: AlwaysFalsy +af = False +af = None +af = 0 +af = "" +af = b"" +af = CustomAlwaysFalsyType() +``` + +## `AlwaysTruthy` and `AlwaysFalsy` are disjoint + +It follows directly from the definition that `AlwaysTruthy` and `AlwaysFalsy` are disjoint types: + +```py +from ty_extensions import static_assert, is_disjoint_from, AlwaysTruthy, AlwaysFalsy + +static_assert(is_disjoint_from(AlwaysTruthy, AlwaysFalsy)) +``` + +## `Truthy` and `Falsy` + +It is useful to also define the types `Truthy = ~AlwaysFalsy` and `Falsy = ~AlwaysTruthy`. These +types describe the set of values that *can* be truthy (`bool(t)` can return `True`) or falsy +(`bool(f)` can return `False`), respectively. + +Finally, we can also define the type `AmbiguousTruthiness = Truthy & Falsy`, which describes the set +of values that can be truthy *and* falsy. This intersection is not empty. In the following, we give +examples for values that belong to these three types: + +```py +from ty_extensions import static_assert, is_equivalent_to, is_disjoint_from, Not, Intersection, AlwaysTruthy, AlwaysFalsy +from typing_extensions import Never +from random import choice + +type Truthy = Not[AlwaysFalsy] +type Falsy = Not[AlwaysTruthy] + +type AmbiguousTruthiness = Intersection[Truthy, Falsy] + +static_assert(is_disjoint_from(AlwaysTruthy, AmbiguousTruthiness)) +static_assert(is_disjoint_from(AlwaysFalsy, AmbiguousTruthiness)) +static_assert(not is_disjoint_from(Truthy, Falsy)) + +class CustomAmbiguousTruthinessType: + def __bool__(self) -> bool: + return choice((True, False)) + +def maybe_empty_list() -> list[int]: + return choice(([], [1, 2, 3])) + +reveal_type(bool(maybe_empty_list())) # revealed: bool +reveal_type(bool(CustomAmbiguousTruthinessType())) # revealed: bool + +t: Truthy +t = True +t = 1 +# TODO: This assignment should be okay +t = maybe_empty_list() # error: [invalid-assignment] +# TODO: This assignment should be okay +t = CustomAmbiguousTruthinessType() # error: [invalid-assignment] + +a: AmbiguousTruthiness +# TODO: This assignment should be okay +a = maybe_empty_list() # error: [invalid-assignment] +# TODO: This assignment should be okay +a = CustomAmbiguousTruthinessType() # error: [invalid-assignment] + +f: Falsy +f = False +f = None +# TODO: This assignment should be okay +f = maybe_empty_list() # error: [invalid-assignment] +# TODO: This assignment should be okay +f = CustomAmbiguousTruthinessType() # error: [invalid-assignment] +``` + +## Subtypes of `AlwaysTruthy`, `AlwaysFalsy` + +```py +from ty_extensions import static_assert, is_subtype_of, is_disjoint_from, AlwaysTruthy, AlwaysFalsy +from typing_extensions import Literal +``` + +These two types are disjoint, so types (that are not equivalent to Never) can only be a subtype of +either one of them. + +```py +static_assert(is_disjoint_from(AlwaysTruthy, AlwaysFalsy)) +``` + +Types that only contain always-truthy values + +```py +static_assert(is_subtype_of(Literal[True], AlwaysTruthy)) +static_assert(is_subtype_of(Literal[1], AlwaysTruthy)) +static_assert(is_subtype_of(Literal[-1], AlwaysTruthy)) +static_assert(is_subtype_of(Literal["non empty"], AlwaysTruthy)) +static_assert(is_subtype_of(Literal[b"non empty"], AlwaysTruthy)) +``` + +Types that only contain always-falsy values + +```py +static_assert(is_subtype_of(None, AlwaysFalsy)) +static_assert(is_subtype_of(Literal[False], AlwaysFalsy)) +static_assert(is_subtype_of(Literal[0], AlwaysFalsy)) +static_assert(is_subtype_of(Literal[""], AlwaysFalsy)) +static_assert(is_subtype_of(Literal[b""], AlwaysFalsy)) +static_assert(is_subtype_of(Literal[False] | Literal[0], AlwaysFalsy)) +``` + +Ambiguous truthiness types + +```py +static_assert(not is_subtype_of(bool, AlwaysTruthy)) +static_assert(not is_subtype_of(bool, AlwaysFalsy)) + +static_assert(not is_subtype_of(list[int], AlwaysTruthy)) +static_assert(not is_subtype_of(list[int], AlwaysFalsy)) +``` + +## Open questions + +Is `tuple[()]` always falsy? We currently model it this way, but this is +[under discussion](https://github.com/astral-sh/ruff/issues/15528). + +```py +from ty_extensions import static_assert, is_subtype_of, AlwaysFalsy + +static_assert(is_subtype_of(tuple[()], AlwaysFalsy)) +``` + +## References + +See also: + +- Our test suite on [narrowing for `if x` and `if not x`](../narrow/truthiness.md). diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md new file mode 100644 index 0000000000000..a0de2576b98b2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md @@ -0,0 +1,130 @@ +# `Any` + +## Introduction + +The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown static +type, which means that it represents an *unknown* set of runtime values. + +## Every type is assignable to `Any`, and `Any` is assignable to every type + +```py +from ty_extensions import static_assert, is_assignable_to +from typing_extensions import Never, Any + +class C: ... + +static_assert(is_assignable_to(C, Any)) +static_assert(is_assignable_to(Any, C)) + +static_assert(is_assignable_to(object, Any)) +static_assert(is_assignable_to(Any, object)) + +static_assert(is_assignable_to(Never, Any)) +static_assert(is_assignable_to(Any, Never)) + +static_assert(is_assignable_to(type, Any)) +static_assert(is_assignable_to(Any, type)) + +static_assert(is_assignable_to(type[Any], Any)) +static_assert(is_assignable_to(Any, type[Any])) +``` + +`Any` is also assignable to itself (like every type): + +```py +static_assert(is_assignable_to(Any, Any)) +``` + +## Unions with `Any`: `Any | T` + +The union `Any | T` of `Any` with a fully static type `T` describes an unknown set of values that is +*at least as large* as the set of values described by `T`. It represents an unknown fully-static +type with *lower bound* `T`. Again, this can be demonstrated using the assignable-to relation: + +```py +from ty_extensions import static_assert, is_assignable_to, is_equivalent_to +from typing_extensions import Any + +# A class hierarchy Small <: Medium <: Big + +class Big: ... +class Medium(Big): ... +class Small(Medium): ... + +static_assert(is_assignable_to(Any | Medium, Big)) +static_assert(is_assignable_to(Any | Medium, Medium)) + +# `Any | Medium` is at least as large as `Medium`, so we can not assign it to `Small`: +static_assert(not is_assignable_to(Any | Medium, Small)) +``` + +The union `Any | object` is equivalent to `object`. This is true for every union with `object`, but +it is worth demonstrating: + +```py +static_assert(is_equivalent_to(Any | object, object)) +static_assert(is_equivalent_to(object | Any, object)) +``` + +## Intersections with `Any`: `Any & T` + +The intersection `Any & T` of `Any` with a fully static type `T` describes an unknown set of values +that is *no larger than* the set of values described by `T`. It represents an unknown fully-static +type with *upper bound* `T`: + +```py +from ty_extensions import static_assert, is_assignable_to, Intersection, is_equivalent_to +from typing import Any + +class Big: ... +class Medium(Big): ... +class Small(Medium): ... + +static_assert(is_assignable_to(Small, Intersection[Any, Medium])) +static_assert(is_assignable_to(Medium, Intersection[Any, Medium])) +``` + +`Any & Medium` is no larger than `Medium`, so we can not assign `Big` to it. There is no possible +materialization of `Any & Medium` that would make it as big as `Big`: + +```py +static_assert(not is_assignable_to(Big, Intersection[Any, Medium])) +``` + +`Any & Never` represents an "unknown" fully-static type which is no larger than `Never`. There is no +such fully-static type, except for `Never` itself. So `Any & Never` is equivalent to `Never`: + +```py +from typing_extensions import Never + +static_assert(is_equivalent_to(Intersection[Any, Never], Never)) +static_assert(is_equivalent_to(Intersection[Never, Any], Never)) +``` + +## Tuples with `Any` + +This section demonstrates the following passage from the [type system concepts] documentation on +gradual types: + +> A type such as `tuple[int, Any]` […] does not represent a single set of Python objects; rather, it +> represents a (bounded) range of possible sets of values. […] In the same way that `Any` does not +> represent "the set of all Python objects" but rather "an unknown set of objects", +> `tuple[int, Any]` does not represent "the set of all length-two tuples whose first element is an +> integer". That is a fully static type, spelled `tuple[int, object]`. By contrast, +> `tuple[int, Any]` represents some unknown set of tuple values; it might be the set of all tuples +> of two integers, or the set of all tuples of an integer and a string, or some other set of tuple +> values. +> +> In practice, this difference is seen (for example) in the fact that we can assign an expression of +> type `tuple[int, Any]` to a target typed as `tuple[int, int]`, whereas assigning +> `tuple[int, object]` to `tuple[int, int]` is a static type error. + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Any + +static_assert(is_assignable_to(tuple[int, Any], tuple[int, int])) +static_assert(not is_assignable_to(tuple[int, object], tuple[int, int])) +``` + +[type system concepts]: https://typing.readthedocs.io/en/latest/spec/concepts.html#gradual-types diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md new file mode 100644 index 0000000000000..d8d42ae7ad2d0 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md @@ -0,0 +1,234 @@ +# Integer `Literal`s + +An integer literal type represents the set of all integer objects with one specific value. For +example, the type `Literal[54165]` represents the set of all integer objects with the value `54165`. + +## Integer `Literal`s are not singleton types + +This does not necessarily mean that the type is a singleton type, i.e., a type with only one +inhabitant. The reason for this is that there might be multiple Python runtime objects (at different +memory locations) that all represent the same integer value. For example, the following code snippet +may print `False`. + +```py +x = 54165 +y = 54165 + +print(x is y) +``` + +In practice, on CPython 3.13.0, this program prints `True` when executed as a script, but `False` +when executed in the REPL. + +Since this is an implementation detail of the Python runtime, we model all integer literals as +non-singleton types: + +```py +from ty_extensions import static_assert, is_singleton +from typing import Literal + +static_assert(not is_singleton(Literal[0])) +static_assert(not is_singleton(Literal[1])) +static_assert(not is_singleton(Literal[54165])) +``` + +This has implications for type-narrowing. For example, you can not use the `is not` operator to +check whether a variable has a specific integer literal type, but this is not a recommended practice +anyway. + +```py +def f(x: int): + if x is 54165: + # This works, because if `x` is the same object as that left-hand-side literal, then it + # must have the same value. + reveal_type(x) # revealed: Literal[54165] + + if x is not 54165: + # But here, we can not narrow the type (to `int & ~Literal[54165]`), because `x` might also + # have the value `54165`, but a different object identity. + reveal_type(x) # revealed: int +``` + +## Integer `Literal`s are single-valued types + +There is a slightly weaker property that integer literals have. They are single-valued types, which +means that all objects of the type have the same value, i.e. they compare equal to each other: + +```py +from ty_extensions import static_assert, is_single_valued +from typing import Literal + +static_assert(is_single_valued(Literal[0])) +static_assert(is_single_valued(Literal[1])) +static_assert(is_single_valued(Literal[54165])) +``` + +And this can be used for type-narrowing using not-equal comparisons: + +```py +def f(x: int): + if x == 54165: + # The reason that no narrowing occurs here is that there might be subclasses of `int` + # that override `__eq__`. This is not specific to integer literals though, and generally + # applies to `==` comparisons. + reveal_type(x) # revealed: int + + if x != 54165: + reveal_type(x) # revealed: int & ~Literal[54165] +``` + +## Subtyping relationships + +### Subtypes of `int` + +All integer literals are subtypes of `int`: + +```py +from ty_extensions import static_assert, is_subtype_of +from typing import Literal + +static_assert(is_subtype_of(Literal[0], int)) +static_assert(is_subtype_of(Literal[1], int)) +static_assert(is_subtype_of(Literal[54165], int)) +``` + +It is tempting to think that `int` is equivalent to the union of all integer literals, +`… | Literal[-1] | Literal[0] | Literal[1] | …`, but this is not the case. `True` and `False` are +also inhabitants of the `int` type, but they are not inhabitants of any integer literal type: + +```py +static_assert(is_subtype_of(Literal[True], int)) +static_assert(is_subtype_of(Literal[False], int)) + +static_assert(not is_subtype_of(Literal[True], Literal[1])) +static_assert(not is_subtype_of(Literal[False], Literal[0])) +``` + +Also, `int` can be subclassed, and instances of that subclass are also subtypes of `int`: + +```py +class CustomInt(int): + pass + +static_assert(is_subtype_of(CustomInt, int)) +``` + +### No subtypes of `float` and `complex` + +```toml +[environment] +python-version = "3.12" +``` + +Integer literals are _not_ subtypes of `float`, but the typing spec describes a special case for +[`float` and `complex`] which accepts integers (and therefore also integer literals) in places where +a `float` or `complex` is expected. We use the types `JustFloat` and `JustComplex` below, because ty +recognizes an annotation of `float` as `int | float` to support that typing system special case. + +```py +from ty_extensions import static_assert, is_subtype_of, JustFloat, JustComplex +from typing import Literal + +# Not subtypes of `float` and `complex` +static_assert(not is_subtype_of(Literal[0], JustFloat) and not is_subtype_of(Literal[0], JustComplex)) +static_assert(not is_subtype_of(Literal[1], JustFloat) and not is_subtype_of(Literal[1], JustComplex)) +static_assert(not is_subtype_of(Literal[54165], JustFloat) and not is_subtype_of(Literal[54165], JustComplex)) +``` + +The typing system special case can be seen in the following example: + +```py +a: JustFloat = 1 # error: [invalid-assignment] +b: JustComplex = 1 # error: [invalid-assignment] + +x: float = 1 +y: complex = 1 +``` + +### Subtypes of integer `Literal`s? + +The only subtypes of an integer literal type _that can be named_ are the type itself and `Never`: + +```py +from ty_extensions import static_assert, is_subtype_of +from typing_extensions import Never, Literal + +static_assert(is_subtype_of(Literal[54165], Literal[54165])) +static_assert(is_subtype_of(Never, Literal[54165])) +``` + +## Disjointness of integer `Literal`s + +Two integer literal types `Literal[a]` and `Literal[b]` are disjoint if `a != b`: + +```py +from ty_extensions import static_assert, is_disjoint_from +from typing import Literal + +static_assert(is_disjoint_from(Literal[0], Literal[1])) +static_assert(is_disjoint_from(Literal[0], Literal[54165])) + +static_assert(not is_disjoint_from(Literal[0], Literal[0])) +static_assert(not is_disjoint_from(Literal[54165], Literal[54165])) +``` + +## Integer literal math + +```toml +[environment] +python-version = "3.12" +``` + +We support a whole range of arithmetic operations on integer literal types. For example, we can +statically verify that (3, 4, 5) is a Pythagorean triple: + +```py +from ty_extensions import static_assert + +static_assert(3**2 + 4**2 == 5**2) +``` + +Using unions of integer literals, we can even use this to solve equations over a finite domain +(determine whether there is a solution or not): + +```py +from typing import Literal, assert_type + +type Nat = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +def pythagorean_triples(a: Nat, b: Nat, c: Nat): + # Answer is `bool`, because solutions do exist (3² + 4² = 5²) + assert_type(a**2 + b**2 == c**2, bool) + +def fermats_last_theorem(a: Nat, b: Nat, c: Nat): + # Answer is `Literal[False]`, because no solutions exist + assert_type(a**3 + b**3 == c**3, Literal[False]) +``` + +## Truthiness + +Integer literals are always-truthy, except for `0`, which is always-falsy: + +```py +from ty_extensions import static_assert + +static_assert(-54165) +static_assert(-1) +static_assert(not 0) +static_assert(1) +static_assert(54165) +``` + +This can be used for type-narrowing: + +```py +from typing_extensions import Literal, assert_type + +def f(x: Literal[0, 1, 54365]): + if x: + assert_type(x, Literal[1, 54365]) + else: + assert_type(x, Literal[0]) +``` + +[`float` and `complex`]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md new file mode 100644 index 0000000000000..fbef3e571b55d --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/never.md @@ -0,0 +1,188 @@ +# `Never` + +`Never` represents the empty set of values. + +## `Never` is a subtype of every type + +The `Never` type is the bottom type of Python's type system. It is a subtype of every type, but no +type is a subtype of `Never`, except for `Never` itself or type variables with upper bound `Never`. + +```py +from ty_extensions import static_assert, is_subtype_of +from typing_extensions import Never, TypeVar + +class C: ... + +static_assert(is_subtype_of(Never, int)) +static_assert(is_subtype_of(Never, object)) +static_assert(is_subtype_of(Never, C)) +static_assert(is_subtype_of(Never, Never)) + +static_assert(not is_subtype_of(int, Never)) + +T = TypeVar("T", bound=Never) +static_assert(is_subtype_of(T, Never)) +``` + +## `Never` is assignable to every type + +`Never` is assignable to every type. This fact is useful when calling error-handling functions in a +context that requires a value of a specific type. For example, changing the `Never` return type to +`None` below would cause a type error: + +```py +from ty_extensions import static_assert, is_assignable_to +from typing_extensions import Never, Any + +static_assert(is_assignable_to(Never, int)) +static_assert(is_assignable_to(Never, object)) +static_assert(is_assignable_to(Never, Any)) +static_assert(is_assignable_to(Never, Never)) + +def raise_error() -> Never: + raise Exception("...") + +def f(divisor: int) -> None: + x: float = (1 / divisor) if divisor != 0 else raise_error() +``` + +## `Never` in annotations + +`Never` can be used in functions to indicate that the function never returns. For example, if a +function always raises an exception, if it calls `sys.exit()`, if it enters an infinite loop, or if +it calls itself recursively. All of these functions "Never" return control back to the caller: + +```py +from typing_extensions import Never + +def raises_unconditionally() -> Never: + raise Exception("This function always raises an exception") + +def exits_unconditionally() -> Never: + import sys + + return sys.exit(1) + +def loops_forever() -> Never: + while True: + pass + +def recursive_never() -> Never: + return recursive_never() +``` + +Similarly, if `Never` is used in parameter positions, it indicates that the function can "Never" be +called, because it can never be passed a value of type `Never` (there are none): + +```py +def can_not_be_called(n: Never) -> int: + return 0 +``` + +## `Never` is disjoint from every other type + +Two types `A` and `B` are disjoint if their intersection is empty. Since `Never` has no inhabitants, +it is disjoint from every other type: + +```py +from ty_extensions import static_assert, is_disjoint_from +from typing_extensions import Never + +class C: ... + +static_assert(is_disjoint_from(Never, int)) +static_assert(is_disjoint_from(Never, object)) +static_assert(is_disjoint_from(Never, C)) +static_assert(is_disjoint_from(Never, Never)) +``` + +## Unions with `Never` + +`Never` can always be removed from unions: + +```py +from ty_extensions import static_assert, is_equivalent_to +from typing_extensions import Never + +class P: ... +class Q: ... + +static_assert(is_equivalent_to(P | Never | Q | None, P | Q | None)) +``` + +## Intersections with `Never` + +Intersecting with `Never` results in `Never`: + +```py +from ty_extensions import static_assert, is_equivalent_to, Intersection +from typing_extensions import Never + +class P: ... +class Q: ... + +static_assert(is_equivalent_to(Intersection[P, Never, Q], Never)) +``` + +## `Never` is the complement of `object` + +`object` describes the set of all possible values, while `Never` describes the empty set. The two +types are complements of each other: + +```py +from ty_extensions import static_assert, is_equivalent_to, Not +from typing_extensions import Never + +static_assert(is_equivalent_to(Not[object], Never)) +static_assert(is_equivalent_to(Not[Never], object)) +``` + +This duality is also reflected in other facts: + +- `Never` is a subtype of every type, while `object` is a supertype of every type. +- `Never` is assignable to every type, while `object` is assignable from every type. +- `Never` is disjoint from every type, while `object` overlaps with every type. +- Building a union with `Never` is a no-op, intersecting with `object` is a no-op. +- Interecting with `Never` results in `Never`, building a union with `object` results in `object`. + +## Lists of `Never` + +`list[Never]` is a reasonable type that is *not* equivalent to `Never`. The empty list inhabits this +type: + +```py +from typing_extensions import Never + +x: list[Never] = [] +``` + +## Tuples involving `Never` + +A type like `tuple[int, Never]` has no inhabitants, and so it is equivalent to `Never`: + +```py +from ty_extensions import static_assert, is_equivalent_to +from typing_extensions import Never + +static_assert(is_equivalent_to(tuple[int, Never], Never)) +``` + +Note that this is not the case for the homogenous tuple type `tuple[Never, ...]` though, because +that type is inhabited by the empty tuple: + +```py +static_assert(not is_equivalent_to(tuple[Never, ...], Never)) + +t: tuple[Never, ...] = () +``` + +## `NoReturn` is the same as `Never` + +The `NoReturn` type is a different name for `Never`: + +```py +from ty_extensions import static_assert, is_equivalent_to +from typing_extensions import NoReturn, Never + +static_assert(is_equivalent_to(NoReturn, Never)) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md new file mode 100644 index 0000000000000..08fcd7905b117 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/none.md @@ -0,0 +1,80 @@ +# `None` + +## `None` as a singleton type + +The type `None` (or `NoneType`, see below) is a singleton type that has only one inhabitant: the +object `None`. + +```py +from ty_extensions import static_assert, is_singleton, is_equivalent_to + +n: None = None + +static_assert(is_singleton(None)) +``` + +Just like for other singleton types, the only subtypes of `None` are `None` itself and `Never`: + +```py +from ty_extensions import static_assert, is_subtype_of +from typing_extensions import Never + +static_assert(is_subtype_of(None, None)) +static_assert(is_subtype_of(Never, None)) +``` + +## Relationship to `Optional[T]` + +The type `Optional[T]` is an alias for `T | None` (or `Union[T, None]`): + +```py +from ty_extensions import static_assert, is_equivalent_to +from typing import Optional, Union + +class T: ... + +static_assert(is_equivalent_to(Optional[T], T | None)) +static_assert(is_equivalent_to(Optional[T], Union[T, None])) +``` + +## Type narrowing using `is` + +Just like for other singleton types, we support type narrowing using `is` or `is not` checks: + +```py +from typing_extensions import assert_type + +class T: ... + +def f(x: T | None): + if x is None: + assert_type(x, None) + else: + assert_type(x, T) + + assert_type(x, T | None) + + if x is not None: + assert_type(x, T) + else: + assert_type(x, None) +``` + +## `NoneType` + +`None` is special in that the name of the instance at runtime can be used as a type as well: The +object `None` is an instance of type `None`. When a distinction between the two is needed, the +spelling `NoneType` can be used, which is available since Python 3.10. `NoneType` is equivalent to +`None`: + +```toml +[environment] +python-version = "3.10" +``` + +```py +from ty_extensions import static_assert, is_equivalent_to +from types import NoneType + +static_assert(is_equivalent_to(NoneType, None)) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md new file mode 100644 index 0000000000000..261452e000209 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md @@ -0,0 +1,120 @@ +# `Not[T]` + +The type `Not[T]` is the complement of the type `T`. It describes the set of all values that are +*not* in `T`. + +## `Not[T]` is disjoint from `T` + +`Not[T]` is disjoint from `T`: + +```py +from ty_extensions import Not, static_assert, is_disjoint_from + +class T: ... +class S(T): ... + +static_assert(is_disjoint_from(Not[T], T)) +static_assert(is_disjoint_from(Not[T], S)) +``` + +## The union of `T` and `Not[T]` is equivalent to `object` + +Together, `T` and `Not[T]` describe the set of all values. So the union of both types is equivalent +to `object`: + +```py +from ty_extensions import Not, static_assert, is_equivalent_to + +class T: ... + +static_assert(is_equivalent_to(T | Not[T], object)) +``` + +## `Not[T]` reverses subtyping relationships + +If `S <: T`, then `Not[T] <: Not[S]`:, similar to how negation in logic reverses the order of `<=`: + +```py +from ty_extensions import Not, static_assert, is_subtype_of + +class T: ... +class S(T): ... + +static_assert(is_subtype_of(S, T)) +static_assert(is_subtype_of(Not[T], Not[S])) +``` + +## `Not[T]` reverses assignability relationships + +Assignability relationships are similarly reversed: + +```py +from ty_extensions import Not, Intersection, static_assert, is_assignable_to +from typing import Any + +class T: ... +class S(T): ... + +static_assert(is_assignable_to(S, T)) +static_assert(is_assignable_to(Not[T], Not[S])) + +static_assert(is_assignable_to(Intersection[Any, S], Intersection[Any, T])) + +static_assert(is_assignable_to(Not[Intersection[Any, S]], Not[Intersection[Any, T]])) +``` + +## Subtyping and disjointness + +If two types `P` and `Q` are disjoint, then `P` must be a subtype of `Not[Q]`, and vice versa: + +```py +from ty_extensions import Not, static_assert, is_subtype_of, is_disjoint_from +from typing import final + +@final +class P: ... + +@final +class Q: ... + +static_assert(is_disjoint_from(P, Q)) + +static_assert(is_subtype_of(P, Not[Q])) +static_assert(is_subtype_of(Q, Not[P])) +``` + +## De-Morgan's laws + +Given two unrelated types `P` and `Q`, we can demonstrate De-Morgan's laws in the context of +set-theoretic types: + +```py +from ty_extensions import Not, static_assert, is_equivalent_to, Intersection + +class P: ... +class Q: ... +``` + +The negation of a union is the intersection of the negations: + +```py +static_assert(is_equivalent_to(Not[P | Q], Intersection[Not[P], Not[Q]])) +``` + +Conversely, the negation of an intersection is the union of the negations: + +```py +static_assert(is_equivalent_to(Not[Intersection[P, Q]], Not[P] | Not[Q])) +``` + +## Negation of gradual types + +`Any` represents an unknown set of values. So `Not[Any]` also represents an unknown set of values. +The two gradual types are equivalent: + +```py +from ty_extensions import static_assert, is_equivalent_to, Not +from typing import Any + +static_assert(is_equivalent_to(Not[Any], Any)) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md new file mode 100644 index 0000000000000..f8c9e4fec2b78 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/object.md @@ -0,0 +1,78 @@ +# `object` + +The `object` type represents the set of all Python objects. + +## `object` is a supertype of all types + +It is the top type in Python's type system, i.e., it is a supertype of all other types: + +```py +from ty_extensions import static_assert, is_subtype_of + +static_assert(is_subtype_of(int, object)) +static_assert(is_subtype_of(str, object)) +static_assert(is_subtype_of(type, object)) +static_assert(is_subtype_of(object, object)) +``` + +## Every type is assignable to `object` + +Everything can be assigned to the type `object`. This fact can be used to create heterogeneous +collections of objects (but also erases more specific type information): + +```py +from ty_extensions import static_assert, is_assignable_to +from typing_extensions import Any, Never + +static_assert(is_assignable_to(int, object)) +static_assert(is_assignable_to(str | bytes, object)) +static_assert(is_assignable_to(type, object)) +static_assert(is_assignable_to(object, object)) +static_assert(is_assignable_to(Never, object)) +static_assert(is_assignable_to(Any, object)) + +x: list[object] = [1, "a", ()] +``` + +## `object` overlaps with all types + +There is no type that is disjoint from `object` except for `Never`: + +```py +from ty_extensions import static_assert, is_disjoint_from +from typing_extensions import Any, Never + +static_assert(not is_disjoint_from(int, object)) +static_assert(not is_disjoint_from(str, object)) +static_assert(not is_disjoint_from(type, object)) +static_assert(not is_disjoint_from(object, object)) +static_assert(not is_disjoint_from(Any, object)) +static_assert(is_disjoint_from(Never, object)) +``` + +## Unions with `object` + +Unions with `object` are equivalent to `object`: + +```py +from ty_extensions import static_assert, is_equivalent_to + +static_assert(is_equivalent_to(int | object | None, object)) +``` + +## Intersections with `object` + +Intersecting with `object` is equivalent to the original type: + +```py +from ty_extensions import static_assert, is_equivalent_to, Intersection + +class P: ... +class Q: ... + +static_assert(is_equivalent_to(Intersection[P, object, Q], Intersection[P, Q])) +``` + +## `object` is the complement of `Never` + +See corresponding section in the fact sheet for [`Never`](never.md). diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md new file mode 100644 index 0000000000000..bd2babc0bf4b4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -0,0 +1,395 @@ +# Tuples + +## Tuples as product types + +Tuples can be used to construct product types. Inhabitants of the type `tuple[P, Q]` are ordered +pairs `(p, q)` where `p` is an inhabitant of `P` and `q` is an inhabitant of `Q`, analogous to the +Cartesian product of sets. + +```py +from typing_extensions import assert_type + +class P: ... +class Q: ... + +def _(p: P, q: Q): + assert_type((p, q), tuple[P, Q]) +``` + +## Instantiating tuples + +Like all classes, tuples can be instantiated by invoking the `tuple` class. When instantiating a +specialization of `tuple` we (TODO: should) check that the values passed in match the element types +defined in the specialization. + +```py +from typing_extensions import Iterable, Never + +reveal_type(tuple()) # revealed: tuple[()] +reveal_type(tuple[int]((1,))) # revealed: tuple[int] +reveal_type(().__class__()) # revealed: tuple[()] +reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]] + +def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]): + reveal_type(tuple(x)) # revealed: tuple[int, ...] + reveal_type(tuple(y)) # revealed: tuple[str, ...] + reveal_type(tuple(z)) # revealed: tuple[Unknown, ...] + + # This is correct as the only inhabitants of `list[Never]` can be empty lists + reveal_type(tuple(aa)) # revealed: tuple[()] + +reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]] + +# TODO: should be `tuple[Literal[1], ...]` +reveal_type(tuple([1])) # revealed: tuple[Unknown, ...] + +# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `list[Unknown]`" +reveal_type(tuple[int]([1])) # revealed: tuple[int] + +# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, str]`, found `tuple[Literal[1]]`" +reveal_type(tuple[int, str]((1,))) # revealed: tuple[int, str] + +# error: [missing-argument] "No argument provided for required parameter `iterable`" +reveal_type((1,).__class__()) # revealed: tuple[Literal[1]] + +# error: [missing-argument] "No argument provided for required parameter `iterable`" +reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]] +``` + +## Subtyping relationships + +The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1` +and `S2` is a subtype of `T2`, and similar for other lengths of tuples: + +```py +from ty_extensions import static_assert, is_subtype_of + +class T1: ... +class S1(T1): ... +class T2: ... +class S2(T2): ... + +static_assert(is_subtype_of(tuple[S1], tuple[T1])) +static_assert(not is_subtype_of(tuple[T1], tuple[S1])) + +static_assert(is_subtype_of(tuple[S1, S2], tuple[T1, T2])) +static_assert(not is_subtype_of(tuple[T1, S2], tuple[S1, T2])) +static_assert(not is_subtype_of(tuple[S1, T2], tuple[T1, S2])) +``` + +Different-length tuples are not related via subtyping: + +```py +static_assert(not is_subtype_of(tuple[S1], tuple[T1, T2])) +``` + +## The empty tuple + +The type of the empty tuple `()` is spelled `tuple[()]`. It is [not a singleton type], because +different instances of `()` are not guaranteed to be the same object (even if this is the case in +CPython at the time of writing). + +The empty tuple can also be subclassed (further clarifying that it is not a singleton type): + +```py +from ty_extensions import static_assert, is_singleton, is_subtype_of, is_equivalent_to, is_assignable_to + +static_assert(not is_singleton(tuple[()])) + +class AnotherEmptyTuple(tuple[()]): ... + +static_assert(not is_equivalent_to(AnotherEmptyTuple, tuple[()])) + +static_assert(is_subtype_of(AnotherEmptyTuple, tuple[()])) +static_assert(is_assignable_to(AnotherEmptyTuple, tuple[()])) +``` + +## Non-empty tuples + +For the same reason as above (two instances of a tuple with the same elements might not be the same +object), non-empty tuples are also not singleton types — even if all their elements are singletons: + +```py +from ty_extensions import static_assert, is_singleton + +static_assert(is_singleton(None)) + +static_assert(not is_singleton(tuple[None])) +``` + +## Tuples containing `Never` + +```toml +[environment] +python-version = "3.11" +``` + +The `Never` type contains no inhabitants, so a tuple type that contains `Never` as a mandatory +element also contains no inhabitants. + +```py +from typing import Never +from ty_extensions import static_assert, is_equivalent_to + +static_assert(is_equivalent_to(tuple[Never], Never)) +static_assert(is_equivalent_to(tuple[int, Never], Never)) +static_assert(is_equivalent_to(tuple[Never, *tuple[int, ...]], Never)) +``` + +If the variable-length portion of a tuple is `Never`, then that portion of the tuple must always be +empty. This means that the tuple is not actually variable-length! + +```py +from typing import Never +from ty_extensions import static_assert, is_equivalent_to + +static_assert(is_equivalent_to(tuple[Never, ...], tuple[()])) +static_assert(is_equivalent_to(tuple[int, *tuple[Never, ...]], tuple[int])) +static_assert(is_equivalent_to(tuple[int, *tuple[Never, ...], int], tuple[int, int])) +static_assert(is_equivalent_to(tuple[*tuple[Never, ...], int], tuple[int])) +``` + +## Homogeneous non-empty tuples + +```toml +[environment] +python-version = "3.11" +``` + +A homogeneous tuple can contain zero or more elements of a particular type. You can represent a +tuple that can contain _one_ or more elements of that type (or any other number of minimum elements) +using a mixed tuple. + +```py +def takes_zero_or_more(t: tuple[int, ...]) -> None: ... +def takes_one_or_more(t: tuple[int, *tuple[int, ...]]) -> None: ... +def takes_two_or_more(t: tuple[int, int, *tuple[int, ...]]) -> None: ... + +takes_zero_or_more(()) +takes_zero_or_more((1,)) +takes_zero_or_more((1, 2)) + +takes_one_or_more(()) # error: [invalid-argument-type] +takes_one_or_more((1,)) +takes_one_or_more((1, 2)) + +takes_two_or_more(()) # error: [invalid-argument-type] +takes_two_or_more((1,)) # error: [invalid-argument-type] +takes_two_or_more((1, 2)) +``` + +The required elements can also appear in the suffix of the mixed tuple type. + +```py +def takes_one_or_more_suffix(t: tuple[*tuple[int, ...], int]) -> None: ... +def takes_two_or_more_suffix(t: tuple[*tuple[int, ...], int, int]) -> None: ... +def takes_two_or_more_mixed(t: tuple[int, *tuple[int, ...], int]) -> None: ... + +takes_one_or_more_suffix(()) # error: [invalid-argument-type] +takes_one_or_more_suffix((1,)) +takes_one_or_more_suffix((1, 2)) + +takes_two_or_more_suffix(()) # error: [invalid-argument-type] +takes_two_or_more_suffix((1,)) # error: [invalid-argument-type] +takes_two_or_more_suffix((1, 2)) + +takes_two_or_more_mixed(()) # error: [invalid-argument-type] +takes_two_or_more_mixed((1,)) # error: [invalid-argument-type] +takes_two_or_more_mixed((1, 2)) +``` + +The tuple types are equivalent regardless of whether the required elements appear in the prefix or +suffix. + +```py +from ty_extensions import static_assert, is_subtype_of, is_equivalent_to + +static_assert(is_equivalent_to(tuple[int, *tuple[int, ...]], tuple[*tuple[int, ...], int])) + +static_assert(is_equivalent_to(tuple[int, int, *tuple[int, ...]], tuple[*tuple[int, ...], int, int])) +static_assert(is_equivalent_to(tuple[int, int, *tuple[int, ...]], tuple[int, *tuple[int, ...], int])) +``` + +This is true when the prefix/suffix and variable-length types are equivalent, not just identical. + +```py +from ty_extensions import static_assert, is_subtype_of, is_equivalent_to + +static_assert(is_equivalent_to(tuple[int | str, *tuple[str | int, ...]], tuple[*tuple[str | int, ...], int | str])) + +static_assert( + is_equivalent_to(tuple[int | str, str | int, *tuple[str | int, ...]], tuple[*tuple[int | str, ...], str | int, int | str]) +) +static_assert( + is_equivalent_to(tuple[int | str, str | int, *tuple[str | int, ...]], tuple[str | int, *tuple[int | str, ...], int | str]) +) +``` + +## Disjointness + +```toml +[environment] +python-version = "3.11" +``` + +Two tuples with incompatible minimum lengths are always disjoint, regardless of their element types. +(The lengths are incompatible if the minimum length of one tuple is larger than the maximum length +of the other.) + +```py +from ty_extensions import static_assert, is_disjoint_from + +static_assert(is_disjoint_from(tuple[()], tuple[int])) +static_assert(not is_disjoint_from(tuple[()], tuple[int, ...])) +static_assert(not is_disjoint_from(tuple[int], tuple[int, ...])) +static_assert(not is_disjoint_from(tuple[str, ...], tuple[int, ...])) +``` + +A tuple that is required to contain elements `P1, P2` is disjoint from a tuple that is required to +contain elements `Q1, Q2` if either `P1` is disjoint from `Q1` or if `P2` is disjoint from `Q2`. + +```py +from typing import final + +@final +class F1: ... + +@final +class F2: ... + +class N1: ... +class N2: ... + +static_assert(is_disjoint_from(F1, F2)) +static_assert(not is_disjoint_from(N1, N2)) + +static_assert(is_disjoint_from(tuple[F1, F2], tuple[F2, F1])) +static_assert(is_disjoint_from(tuple[F1, N1], tuple[F2, N2])) +static_assert(is_disjoint_from(tuple[N1, F1], tuple[N2, F2])) +static_assert(not is_disjoint_from(tuple[N1, N2], tuple[N2, N1])) + +static_assert(is_disjoint_from(tuple[F1, *tuple[int, ...], F2], tuple[F2, *tuple[int, ...], F1])) +static_assert(is_disjoint_from(tuple[F1, *tuple[int, ...], N1], tuple[F2, *tuple[int, ...], N2])) +static_assert(is_disjoint_from(tuple[N1, *tuple[int, ...], F1], tuple[N2, *tuple[int, ...], F2])) +static_assert(not is_disjoint_from(tuple[N1, *tuple[int, ...], N2], tuple[N2, *tuple[int, ...], N1])) + +static_assert(not is_disjoint_from(tuple[F1, F2, *tuple[object, ...]], tuple[*tuple[object, ...], F2, F1])) +static_assert(not is_disjoint_from(tuple[F1, N1, *tuple[object, ...]], tuple[*tuple[object, ...], F2, N2])) +static_assert(not is_disjoint_from(tuple[N1, F1, *tuple[object, ...]], tuple[*tuple[object, ...], N2, F2])) +static_assert(not is_disjoint_from(tuple[N1, N2, *tuple[object, ...]], tuple[*tuple[object, ...], N2, N1])) +``` + +The variable-length portion of a tuple can never cause the tuples to be disjoint, since all +variable-length tuple types contain the empty tuple. (Note that per above, the variable-length +portion of a tuple cannot be `Never`; internally we simplify this to a fixed-length tuple.) + +```py +static_assert(not is_disjoint_from(tuple[F1, ...], tuple[F2, ...])) +static_assert(not is_disjoint_from(tuple[N1, ...], tuple[N2, ...])) +``` + +We currently model tuple types to _not_ be disjoint from arbitrary instance types, because we allow +for the possibility of `tuple` to be subclassed + +```py +class C: ... + +static_assert(not is_disjoint_from(tuple[int, str], C)) + +class CommonSubtype(tuple[int, str], C): ... +``` + +Note: This is inconsistent with the fact that we model heterogeneous tuples to be disjoint from +other heterogeneous tuples above: + +```py +class I1(tuple[F1, F2]): ... +class I2(tuple[F2, F1]): ... + +# TODO +# This is a subtype of both `tuple[F1, F2]` and `tuple[F2, F1]`, so those two heterogeneous tuples +# should not be disjoint from each other (see conflicting test above). +class CommonSubtypeOfTuples(I1, I2): ... +``` + +## Truthiness + +```toml +[environment] +python-version = "3.11" +``` + +The truthiness of the empty tuple is `False`. + +```py +from typing_extensions import assert_type, Literal +from ty_extensions import static_assert, is_assignable_to, AlwaysFalsy + +assert_type(bool(()), Literal[False]) + +static_assert(is_assignable_to(tuple[()], AlwaysFalsy)) +``` + +The truthiness of non-empty tuples is always `True`. This is true even if all elements are falsy, +and even if any element is gradual, since the truthiness of a tuple depends only on its length, not +its content. + +```py +from typing_extensions import assert_type, Any, Literal +from ty_extensions import static_assert, is_assignable_to, AlwaysTruthy + +assert_type(bool((False,)), Literal[True]) +assert_type(bool((False, False)), Literal[True]) + +static_assert(is_assignable_to(tuple[Any], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[Any, Any], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[bool], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[bool, bool], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[Literal[False]], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[Literal[False], Literal[False]], AlwaysTruthy)) +``` + +The truthiness of variable-length tuples is ambiguous, since that type contains both empty and +non-empty tuples. + +```py +from typing_extensions import Any, Literal +from ty_extensions import static_assert, is_assignable_to, AlwaysFalsy, AlwaysTruthy + +static_assert(not is_assignable_to(tuple[Any, ...], AlwaysFalsy)) +static_assert(not is_assignable_to(tuple[Any, ...], AlwaysTruthy)) +static_assert(not is_assignable_to(tuple[bool, ...], AlwaysFalsy)) +static_assert(not is_assignable_to(tuple[bool, ...], AlwaysTruthy)) +static_assert(not is_assignable_to(tuple[Literal[False], ...], AlwaysFalsy)) +static_assert(not is_assignable_to(tuple[Literal[False], ...], AlwaysTruthy)) +static_assert(not is_assignable_to(tuple[Literal[True], ...], AlwaysFalsy)) +static_assert(not is_assignable_to(tuple[Literal[True], ...], AlwaysTruthy)) + +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[int, *tuple[bool, ...]], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[int, *tuple[Literal[False], ...]], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[int, *tuple[Literal[True], ...]], AlwaysTruthy)) + +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[*tuple[bool, ...], int], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[*tuple[Literal[False], ...], int], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[*tuple[Literal[True], ...], int], AlwaysTruthy)) + +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[int, *tuple[bool, ...], int], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[int, *tuple[Literal[False], ...], int], AlwaysTruthy)) +static_assert(is_assignable_to(tuple[int, *tuple[Literal[True], ...], int], AlwaysTruthy)) +``` + +Both of these results are conflicting with the fact that tuples can be subclassed, and that we +currently allow subclasses of `tuple` to overwrite `__bool__` (or `__len__`): + +```py +class NotAlwaysTruthyTuple(tuple[int]): + def __bool__(self) -> bool: + return False + +t: tuple[int] = NotAlwaysTruthyTuple((1,)) +``` + +[not a singleton type]: https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957 diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md new file mode 100644 index 0000000000000..e733df9b94ec9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -0,0 +1,174 @@ +# type special form + +## Class literal + +```py +class A: ... + +def _(c: type[A]): + reveal_type(c) # revealed: type[A] +``` + +## Nested class literal + +```py +class A: + class B: ... + +def f(c: type[A.B]): + reveal_type(c) # revealed: type[B] +``` + +## Deeply nested class literal + +```py +class A: + class B: + class C: ... + +def f(c: type[A.B.C]): + reveal_type(c) # revealed: type[C] +``` + +## Class literal from another module + +```py +from a import A + +def f(c: type[A]): + reveal_type(c) # revealed: type[A] +``` + +`a.py`: + +```py +class A: ... +``` + +## Qualified class literal from another module + +```py +import a + +def f(c: type[a.B]): + reveal_type(c) # revealed: type[B] +``` + +`a.py`: + +```py +class B: ... +``` + +## Deeply qualified class literal from another module + +`a/test.py`: + +```py +import a.b + +def f(c: type[a.b.C]): + reveal_type(c) # revealed: type[C] +``` + +`a/__init__.py`: + +```py +``` + +`a/b.py`: + +```py +class C: ... +``` + +## New-style union of classes + +```py +class BasicUser: ... +class ProUser: ... + +class A: + class B: + class C: ... + +def _(u: type[BasicUser | ProUser | A.B.C]): + # revealed: type[BasicUser] | type[ProUser] | type[C] + reveal_type(u) +``` + +## Old-style union of classes + +```py +from typing import Union + +class BasicUser: ... +class ProUser: ... + +class A: + class B: + class C: ... + +def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]): + reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] + reveal_type(b) # revealed: type[str] + reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C] +``` + +## New-style and old-style unions in combination + +```py +from typing import Union + +class BasicUser: ... +class ProUser: ... + +class A: + class B: + class C: ... + +def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]): + reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] + reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str] +``` + +## Illegal parameters + +```py +class A: ... +class B: ... + +# error: [invalid-type-form] +_: type[A, B] +``` + +## As a base class + +```py +class Foo(type[int]): ... + +# TODO: should be `tuple[, , ] +reveal_type(Foo.__mro__) # revealed: tuple[, @Todo(GenericAlias instance), ] +``` + +## `@final` classes + +`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is +used as the type argument. This applies to standard-library classes and user-defined classes: + +```toml +[environment] +python-version = "3.10" +``` + +```py +from types import EllipsisType +from typing import final + +@final +class Foo: ... + +def _(x: type[Foo], y: type[EllipsisType]): + reveal_type(x) # revealed: + reveal_type(y) # revealed: +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md b/crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md rename to crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/typing_dot_Type.md b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/type_of/typing_dot_Type.md rename to crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md index ff31bb1516676..bda56c9384185 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/typing_dot_Type.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md @@ -28,5 +28,5 @@ class C(Type): ... # Runtime value: `(C, type, typing.Generic, object)` # TODO: Add `Generic` to the MRO -reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[type], Literal[object]] +reveal_type(C.__mro__) # revealed: tuple[, , ] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md new file mode 100644 index 0000000000000..ab730adb68e19 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -0,0 +1,1172 @@ +# Assignable-to relation + +The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target). +This allows us to check if a type `S` can be used in a context where a type `T` is expected +(function arguments, variable assignments). See the [typing documentation] for a precise definition +of this concept. + +## Basic types + +### Fully static + +Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be +assignable to `T`. Two equivalent types are subtypes of each other: + +```py +from ty_extensions import static_assert, is_assignable_to + +class Parent: ... +class Child1(Parent): ... +class Child2(Parent): ... +class Grandchild(Child1, Child2): ... +class Unrelated: ... + +static_assert(is_assignable_to(int, int)) +static_assert(is_assignable_to(Parent, Parent)) +static_assert(is_assignable_to(Child1, Parent)) +static_assert(is_assignable_to(Grandchild, Parent)) +static_assert(is_assignable_to(Unrelated, Unrelated)) + +static_assert(not is_assignable_to(str, int)) +static_assert(not is_assignable_to(object, int)) +static_assert(not is_assignable_to(Parent, Child1)) +static_assert(not is_assignable_to(Unrelated, Parent)) +static_assert(not is_assignable_to(Child1, Child2)) +``` + +### Gradual types + +The dynamic type is assignable to or from any type. + +```py +from ty_extensions import static_assert, is_assignable_to, Unknown +from typing import Any, Literal + +static_assert(is_assignable_to(Unknown, Literal[1])) +static_assert(is_assignable_to(Any, Literal[1])) +static_assert(is_assignable_to(Literal[1], Unknown)) +static_assert(is_assignable_to(Literal[1], Any)) +``` + +## Literal types + +### Boolean literals + +`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`, +which is in turn a subtype of `int`: + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Literal + +static_assert(is_assignable_to(Literal[True], Literal[True])) +static_assert(is_assignable_to(Literal[True], bool)) +static_assert(is_assignable_to(Literal[True], int)) + +static_assert(not is_assignable_to(Literal[True], Literal[False])) +static_assert(not is_assignable_to(bool, Literal[True])) +``` + +### Integer literals + +```py +from ty_extensions import static_assert, is_assignable_to +from typing import Literal + +static_assert(is_assignable_to(Literal[1], Literal[1])) +static_assert(is_assignable_to(Literal[1], int)) + +static_assert(not is_assignable_to(Literal[1], Literal[2])) +static_assert(not is_assignable_to(int, Literal[1])) +static_assert(not is_assignable_to(Literal[1], str)) +``` + +### String literals and `LiteralString` + +All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in +turn a subtype of `str`: + +```py +from ty_extensions import static_assert, is_assignable_to +from typing_extensions import Literal, LiteralString +from typing import Sequence, Any + +static_assert(is_assignable_to(Literal["foo"], Literal["foo"])) +static_assert(is_assignable_to(Literal["foo"], LiteralString)) +static_assert(is_assignable_to(Literal["foo"], str)) +static_assert(is_assignable_to(Literal["foo"], Sequence)) +static_assert(is_assignable_to(Literal["foo"], Sequence[str])) +static_assert(is_assignable_to(Literal["foo"], Sequence[Any])) + +static_assert(is_assignable_to(LiteralString, str)) +static_assert(is_assignable_to(LiteralString, Sequence)) +static_assert(is_assignable_to(LiteralString, Sequence[str])) +static_assert(is_assignable_to(LiteralString, Sequence[Any])) + +static_assert(not is_assignable_to(Literal["foo"], Literal["bar"])) +static_assert(not is_assignable_to(str, Literal["foo"])) +static_assert(not is_assignable_to(str, LiteralString)) +``` + +### Byte literals + +```py +from ty_extensions import static_assert, is_assignable_to +from typing_extensions import Literal, LiteralString + +static_assert(is_assignable_to(Literal[b"foo"], bytes)) +static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"])) + +static_assert(not is_assignable_to(Literal[b"foo"], str)) +static_assert(not is_assignable_to(Literal[b"foo"], LiteralString)) +static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"])) +static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"])) +static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"])) +``` + +### Slice literals + +The type of a slice literal is currently inferred as a specialization of `slice`. + +```py +from ty_extensions import TypeOf, is_assignable_to, static_assert + +static_assert(is_assignable_to(TypeOf[1:2:3], slice)) +static_assert(is_assignable_to(TypeOf[1:2:3], slice[int])) +``` + +## `type[…]` and class literals + +In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`. +This contrasts with `type[str]`, which represents "all possible subclasses of `str`". + +Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent +"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to +`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which +is known to be no larger than the set of possible objects represented by `type`. + +```py +from ty_extensions import static_assert, is_assignable_to, Unknown, TypeOf +from typing import Any + +static_assert(is_assignable_to(type, type)) +static_assert(is_assignable_to(type[object], type[object])) + +static_assert(is_assignable_to(type, type[object])) +static_assert(is_assignable_to(type[object], type)) + +static_assert(is_assignable_to(type[str], type[object])) +static_assert(is_assignable_to(TypeOf[str], type[object])) +static_assert(is_assignable_to(type[str], type)) +static_assert(is_assignable_to(TypeOf[str], type)) + +static_assert(is_assignable_to(type[str], type[str])) +static_assert(is_assignable_to(TypeOf[str], type[str])) + +static_assert(not is_assignable_to(TypeOf[int], type[str])) +static_assert(not is_assignable_to(type, type[str])) +static_assert(not is_assignable_to(type[object], type[str])) + +static_assert(is_assignable_to(type[Any], type[Any])) +static_assert(is_assignable_to(type[Any], type[object])) +static_assert(is_assignable_to(type[object], type[Any])) +static_assert(is_assignable_to(type, type[Any])) +static_assert(is_assignable_to(type[Any], type[str])) +static_assert(is_assignable_to(type[str], type[Any])) +static_assert(is_assignable_to(TypeOf[str], type[Any])) + +static_assert(is_assignable_to(type[Unknown], type[Unknown])) +static_assert(is_assignable_to(type[Unknown], type[object])) +static_assert(is_assignable_to(type[object], type[Unknown])) +static_assert(is_assignable_to(type, type[Unknown])) +static_assert(is_assignable_to(type[Unknown], type[str])) +static_assert(is_assignable_to(type[str], type[Unknown])) +static_assert(is_assignable_to(TypeOf[str], type[Unknown])) + +static_assert(is_assignable_to(type[Unknown], type[Any])) +static_assert(is_assignable_to(type[Any], type[Unknown])) + +static_assert(not is_assignable_to(object, type[Any])) +static_assert(not is_assignable_to(str, type[Any])) + +class Meta(type): ... + +static_assert(is_assignable_to(type[Any], Meta)) +static_assert(is_assignable_to(type[Unknown], Meta)) +static_assert(is_assignable_to(Meta, type[Any])) +static_assert(is_assignable_to(Meta, type[Unknown])) + +class AnyMeta(metaclass=Any): ... + +static_assert(is_assignable_to(type[AnyMeta], type)) +static_assert(is_assignable_to(type[AnyMeta], type[object])) +static_assert(is_assignable_to(type[AnyMeta], type[Any])) + +from typing import TypeVar, Generic, Any + +T_co = TypeVar("T_co", covariant=True) + +class Foo(Generic[T_co]): ... +class Bar(Foo[T_co], Generic[T_co]): ... + +static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar], type[Foo])) +static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]])) +static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) + +# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types) +static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error] +static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error] +``` + +## `type[]` is not assignable to types disjoint from `builtins.type` + +```py +from typing import Any +from ty_extensions import is_assignable_to, static_assert + +static_assert(not is_assignable_to(type[Any], None)) +``` + +## Inheriting `Any` + +### Class-literal types + +Class-literal types that inherit from `Any` are assignable to any type `T` where `T` is assignable +to `type`: + +```py +from typing import Any +from ty_extensions import is_assignable_to, static_assert, TypeOf + +def test(x: Any): + class Foo(x): ... + class Bar(Any): ... + static_assert(is_assignable_to(TypeOf[Foo], Any)) + static_assert(is_assignable_to(TypeOf[Foo], type)) + static_assert(is_assignable_to(TypeOf[Foo], type[int])) + static_assert(is_assignable_to(TypeOf[Foo], type[Any])) + + static_assert(is_assignable_to(TypeOf[Bar], Any)) + static_assert(is_assignable_to(TypeOf[Bar], type)) + static_assert(is_assignable_to(TypeOf[Bar], type[int])) + static_assert(is_assignable_to(TypeOf[Bar], type[Any])) + + static_assert(not is_assignable_to(TypeOf[Foo], int)) + static_assert(not is_assignable_to(TypeOf[Bar], int)) +``` + +This is because the `Any` element in the MRO could materialize to any subtype of `type`. + +### Nominal instance and subclass-of types + +Instances of classes that inherit `Any` are assignable to any non-final type. + +```py +from ty_extensions import is_assignable_to, static_assert +from typing_extensions import Any, final + +class InheritsAny(Any): + pass + +class Arbitrary: + pass + +@final +class FinalClass: + pass + +static_assert(is_assignable_to(InheritsAny, Arbitrary)) +static_assert(is_assignable_to(InheritsAny, Any)) +static_assert(is_assignable_to(InheritsAny, object)) +static_assert(not is_assignable_to(InheritsAny, FinalClass)) +``` + +Similar for subclass-of types: + +```py +static_assert(is_assignable_to(type[Any], type[Any])) +static_assert(is_assignable_to(type[object], type[Any])) +static_assert(is_assignable_to(type[Any], type[Arbitrary])) +static_assert(is_assignable_to(type[Any], type[object])) +``` + +## Heterogeneous tuple types + +```py +from ty_extensions import static_assert, is_assignable_to, AlwaysTruthy, AlwaysFalsy +from typing import Literal, Any + +static_assert(is_assignable_to(tuple[()], tuple[()])) +static_assert(is_assignable_to(tuple[int], tuple[int])) +static_assert(is_assignable_to(tuple[int], tuple[Any])) +static_assert(is_assignable_to(tuple[Any], tuple[int])) +static_assert(is_assignable_to(tuple[int, str], tuple[int, str])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int])) +static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int])) +static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int])) +static_assert(is_assignable_to(tuple[()], tuple)) +static_assert(is_assignable_to(tuple[int, str], tuple)) +static_assert(is_assignable_to(tuple[Any], tuple)) + +# TODO: It is not yet clear if we want the following two assertions to hold. +# See https://github.com/astral-sh/ruff/issues/15528 for more details. The +# short version is: We either need to special-case enforcement of the Liskov +# substitution principle on `__bool__` and `__len__` for tuple subclasses, +# or we need to negate these assertions. +static_assert(is_assignable_to(tuple[()], AlwaysFalsy)) +static_assert(is_assignable_to(tuple[int], AlwaysTruthy)) + +static_assert(not is_assignable_to(tuple[()], tuple[int])) +static_assert(not is_assignable_to(tuple[int], tuple[str])) +static_assert(not is_assignable_to(tuple[int], tuple[int, str])) +static_assert(not is_assignable_to(tuple[int, str], tuple[int])) +static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int])) +static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str])) +``` + +## Assignability of heterogeneous tuple types to homogeneous tuple types + +```toml +[environment] +python-version = "3.12" +``` + +While a homogeneous tuple type is not assignable to any heterogeneous tuple types, a heterogeneous +tuple type can be assignable to a homogeneous tuple type, and homogeneous tuple types can be +assignable to `Sequence`: + +```py +from typing import Literal, Any, Sequence +from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy + +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[Literal[2], ...]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[*tuple[Literal[1], ...], Literal[2]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[str, ...], Literal[2]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[2], *tuple[str, ...]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[*tuple[str, ...], Literal[1], Literal[2]])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, ...])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int | str, ...])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[Not[AlwaysFalsy], ...])) +static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], Sequence[int])) +static_assert(is_assignable_to(tuple[int, ...], Sequence[int])) +static_assert(is_assignable_to(tuple[int, ...], Sequence[Any])) +static_assert(is_assignable_to(tuple[Any, ...], Sequence[int])) + +static_assert(is_assignable_to(tuple[()], tuple[Literal[1, 2], ...])) +static_assert(is_assignable_to(tuple[()], tuple[int, ...])) +static_assert(is_assignable_to(tuple[()], tuple[int | str, ...])) +static_assert(is_assignable_to(tuple[()], tuple[Not[AlwaysFalsy], ...])) +static_assert(is_assignable_to(tuple[()], Sequence[int])) + +static_assert(not is_assignable_to(tuple[int, int], tuple[str, ...])) +``` + +## Assignability of two mixed tuple types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, Any, Sequence +from ty_extensions import static_assert, is_assignable_to, Not, AlwaysFalsy + +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...]], + ) +) + +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...]], + ) +) + +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...]], + ) +) + +static_assert( + not is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[Literal[1], Literal[2], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_assignable_to( + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[Literal[1], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[Literal[1], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_assignable_to( + tuple[*tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[*tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_assignable_to( + tuple[*tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +``` + +## Assignability of the gradual tuple + +```toml +[environment] +python-version = "3.12" +``` + +As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, which +is assignable to every tuple of any length. + +```py +from typing import Any +from ty_extensions import static_assert, is_assignable_to + +static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[Any])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[int, ...])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[int])) +static_assert(is_assignable_to(tuple[Any, ...], tuple[int, int])) +``` + +This also applies when `tuple[Any, ...]` is unpacked into a mixed tuple. + +```py +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[int, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, ...])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...]], tuple[int, int])) + +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any, ...])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[*tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, ...])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int])) +static_assert(is_assignable_to(tuple[*tuple[Any, ...], int], tuple[int, int])) + +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int])) +static_assert(is_assignable_to(tuple[int, *tuple[Any, ...], int], tuple[int, int])) +``` + +The same is not true of fully static tuple types, since an unbounded homogeneous tuple is defined to +be the _union_ of all tuple lengths, not the _gradual choice_ of them. + +```py +static_assert(is_assignable_to(tuple[int, ...], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[Any])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, ...], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[int])) +static_assert(not is_assignable_to(tuple[int, ...], tuple[int, int])) + +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, *tuple[int, ...]])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...]], tuple[int, int])) + +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[*tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[*tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[int])) +static_assert(not is_assignable_to(tuple[*tuple[int, ...], int], tuple[int, int])) + +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int])) +static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, int])) +``` + +## Union types + +```py +from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert, is_assignable_to, Unknown +from typing_extensions import Literal, Any, LiteralString + +static_assert(is_assignable_to(int, int | str)) +static_assert(is_assignable_to(str, int | str)) +static_assert(is_assignable_to(int | str, int | str)) +static_assert(is_assignable_to(str | int, int | str)) +static_assert(is_assignable_to(Literal[1], int | str)) +static_assert(is_assignable_to(Literal[1], Unknown | str)) +static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2])) +static_assert(is_assignable_to(Literal[1] | Literal[2], int)) +static_assert(is_assignable_to(Literal[1] | None, int | None)) +static_assert(is_assignable_to(Any, int | str)) +static_assert(is_assignable_to(Any | int, int)) +static_assert(is_assignable_to(str, int | Any)) + +static_assert(not is_assignable_to(int | None, int)) +static_assert(not is_assignable_to(int | None, str | None)) +static_assert(not is_assignable_to(Literal[1] | None, int)) +static_assert(not is_assignable_to(Literal[1] | None, str | None)) +static_assert(not is_assignable_to(Any | int | str, int)) + +# TODO: No errors +# error: [static-assert-error] +static_assert(is_assignable_to(bool, Literal[False] | AlwaysTruthy)) +# error: [static-assert-error] +static_assert(is_assignable_to(bool, Literal[True] | AlwaysFalsy)) +# error: [static-assert-error] +static_assert(is_assignable_to(LiteralString, Literal[""] | AlwaysTruthy)) +static_assert(not is_assignable_to(Literal[True] | AlwaysFalsy, Literal[False] | AlwaysTruthy)) +``` + +## Intersection types + +```py +from ty_extensions import static_assert, is_assignable_to, Intersection, Not, AlwaysTruthy, AlwaysFalsy +from typing_extensions import Any, Literal, final, LiteralString + +class Parent: ... +class Child1(Parent): ... +class Child2(Parent): ... +class Grandchild(Child1, Child2): ... +class Unrelated: ... + +static_assert(is_assignable_to(Intersection[Child1, Child2], Child1)) +static_assert(is_assignable_to(Intersection[Child1, Child2], Child2)) +static_assert(is_assignable_to(Intersection[Child1, Child2], Parent)) +static_assert(is_assignable_to(Intersection[Child1, Parent], Parent)) + +static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent)) +static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1)) +static_assert(is_assignable_to(Intersection[Child1, Unrelated, Child2], Intersection[Child1, Unrelated])) + +static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1)) +static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent)) +static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent)) + +static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2])) +static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1])) +static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2])) +static_assert(not is_assignable_to(Intersection[Child1, Child2], Intersection[Parent, Unrelated])) + +static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated])) +static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]])) +# The literal `1` is not assignable to `Parent`, so the intersection of int and Parent is definitely an int that is not `1` +static_assert(is_assignable_to(Intersection[int, Parent], Intersection[int, Not[Literal[1]]])) +static_assert(not is_assignable_to(int, Not[int])) +static_assert(not is_assignable_to(int, Not[Literal[1]])) + +static_assert(is_assignable_to(Not[Parent], Not[Child1])) +static_assert(not is_assignable_to(Not[Parent], Parent)) +static_assert(not is_assignable_to(Intersection[Unrelated, Not[Parent]], Parent)) + +# Intersection with `Any` dominates the left hand side of intersections +static_assert(is_assignable_to(Intersection[Any, Parent], Parent)) +static_assert(is_assignable_to(Intersection[Any, Child1], Parent)) +static_assert(is_assignable_to(Intersection[Any, Child2, Not[Child1]], Parent)) +static_assert(is_assignable_to(Intersection[Any, Parent], Unrelated)) +static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Parent, Unrelated])) +static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Parent)) +static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Parent, Unrelated])) + +# Even Any & Not[Parent] is assignable to Parent, since it could be Never +static_assert(is_assignable_to(Intersection[Any, Not[Parent]], Parent)) +static_assert(is_assignable_to(Intersection[Any, Not[Parent]], Not[Parent])) + +# Intersection with `Any` is effectively ignored on the right hand side for the sake of assignment +static_assert(is_assignable_to(Parent, Intersection[Any, Parent])) +static_assert(is_assignable_to(Parent, Parent | Intersection[Any, Unrelated])) +static_assert(is_assignable_to(Child1, Intersection[Any, Parent])) +static_assert(not is_assignable_to(Literal[1], Intersection[Any, Parent])) +static_assert(not is_assignable_to(Unrelated, Intersection[Any, Parent])) + +# Intersections with Any on both sides combine the above logic - the LHS dominates and Any is ignored on the right hand side +static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Any, Parent])) +static_assert(is_assignable_to(Intersection[Any, Unrelated], Intersection[Any, Parent])) +static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Any, Parent, Unrelated])) +static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]])) +static_assert(is_assignable_to(Intersection[Literal[1], Any], Intersection[Unrelated, Not[Any]])) + +# TODO: No errors +# The condition `is_assignable_to(T & U, U)` should still be satisfied after the following transformations: +# `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` +# error: [static-assert-error] +static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal[""]]], AlwaysTruthy)) +# error: [static-assert-error] +static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal["", "a"]]], AlwaysTruthy)) +# `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` +# error: [static-assert-error] +static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy])) +# error: [static-assert-error] +static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy])) +``` + +## General properties + +See also: our property tests in `property_tests.rs`. + +### Everything is assignable to `object` + +`object` is Python's top type; the set of all possible objects at runtime: + +```py +from ty_extensions import static_assert, is_assignable_to, Unknown +from typing import Literal, Any + +static_assert(is_assignable_to(str, object)) +static_assert(is_assignable_to(Literal[1], object)) +static_assert(is_assignable_to(object, object)) +static_assert(is_assignable_to(type, object)) +static_assert(is_assignable_to(Any, object)) +static_assert(is_assignable_to(Unknown, object)) +static_assert(is_assignable_to(type[object], object)) +static_assert(is_assignable_to(type[str], object)) +static_assert(is_assignable_to(type[Any], object)) +``` + +### Every type is assignable to `Any` / `Unknown` + +`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so +any type is assignable to them: + +```py +from ty_extensions import static_assert, is_assignable_to, Unknown +from typing import Literal, Any + +static_assert(is_assignable_to(str, Any)) +static_assert(is_assignable_to(Literal[1], Any)) +static_assert(is_assignable_to(object, Any)) +static_assert(is_assignable_to(type, Any)) +static_assert(is_assignable_to(Any, Any)) +static_assert(is_assignable_to(Unknown, Any)) +static_assert(is_assignable_to(type[object], Any)) +static_assert(is_assignable_to(type[str], Any)) +static_assert(is_assignable_to(type[Any], Any)) + +static_assert(is_assignable_to(str, Unknown)) +static_assert(is_assignable_to(Literal[1], Unknown)) +static_assert(is_assignable_to(object, Unknown)) +static_assert(is_assignable_to(type, Unknown)) +static_assert(is_assignable_to(Any, Unknown)) +static_assert(is_assignable_to(Unknown, Unknown)) +static_assert(is_assignable_to(type[object], Unknown)) +static_assert(is_assignable_to(type[str], Unknown)) +static_assert(is_assignable_to(type[Any], Unknown)) +``` + +### `Never` is assignable to every type + +`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore +assignable to any arbitrary type. + +```py +from ty_extensions import static_assert, is_assignable_to, Unknown +from typing_extensions import Never, Any, Literal + +static_assert(is_assignable_to(Never, str)) +static_assert(is_assignable_to(Never, Literal[1])) +static_assert(is_assignable_to(Never, object)) +static_assert(is_assignable_to(Never, type)) +static_assert(is_assignable_to(Never, Any)) +static_assert(is_assignable_to(Never, Unknown)) +static_assert(is_assignable_to(Never, type[object])) +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 ty_extensions import CallableTypeOf, 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 ty_extensions import CallableTypeOf, 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(CallableTypeOf[positional_only], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[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(CallableTypeOf[positional_only], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None])) +static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None])) +``` + +### Function types + +```py +from typing import Any, Callable + +def f(x: Any) -> str: + return "" + +def g(x: Any) -> int: + return 1 + +c: Callable[[Any], str] = f + +# error: [invalid-assignment] "Object of type `def g(x: Any) -> int` is not assignable to `(Any, /) -> str`" +c: Callable[[Any], str] = g +``` + +### Method types + +```py +from typing import Any, Callable + +class A: + def f(self, x: Any) -> str: + return "" + + def g(self, x: Any) -> int: + return 1 + +c: Callable[[Any], str] = A().f + +# error: [invalid-assignment] "Object of type `bound method A.g(x: Any) -> int` is not assignable to `(Any, /) -> str`" +c: Callable[[Any], str] = A().g +``` + +### Class literal types + +```py +from typing import Any, Callable + +c: Callable[[object], type] = type +c: Callable[[str], Any] = str +c: Callable[[str], Any] = int + +# error: [invalid-assignment] +c: Callable[[str], Any] = object + +class A: + def __init__(self, x: int) -> None: ... + +a: Callable[[int], A] = A + +class C: + def __new__(cls, *args, **kwargs) -> "C": + return super().__new__(cls) + + def __init__(self, x: int) -> None: ... + +c: Callable[[int], C] = C +``` + +### Generic class literal types + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable + +class B[T]: + def __init__(self, x: T) -> None: ... + +b: Callable[[int], B[int]] = B[int] + +class C[T]: + def __new__(cls, *args, **kwargs) -> "C[T]": + return super().__new__(cls) + + def __init__(self, x: T) -> None: ... + +c: Callable[[int], C[int]] = C[int] +``` + +### Overloads + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +@overload +def overloaded() -> None: ... +@overload +def overloaded(a: str) -> str: ... +@overload +def overloaded(a: str, b: Any) -> str: ... +``` + +```py +from overloaded import overloaded +from typing import Any, Callable + +c: Callable[[], None] = overloaded +c: Callable[[str], str] = overloaded +c: Callable[[str, Any], Any] = overloaded +c: Callable[..., str] = overloaded + +# error: [invalid-assignment] +c: Callable[..., int] = overloaded + +# error: [invalid-assignment] +c: Callable[[int], str] = overloaded +``` + +### Classes with `__call__` + +```py +from typing import Callable, Any +from ty_extensions import static_assert, is_assignable_to + +class TakesAny: + def __call__(self, a: Any) -> str: + return "" + +class ReturnsAny: + def __call__(self, a: str) -> Any: ... + +static_assert(is_assignable_to(TakesAny, Callable[[int], str])) +static_assert(not is_assignable_to(TakesAny, Callable[[int], int])) + +static_assert(is_assignable_to(ReturnsAny, Callable[[str], int])) +static_assert(not is_assignable_to(ReturnsAny, Callable[[int], int])) + +from functools import partial + +def f(x: int, y: str) -> None: ... + +c1: Callable[[int], None] = partial(f, y="a") +``` + +### Generic classes with `__call__` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import Callable, Any, Generic, TypeVar, ParamSpec +from ty_extensions import static_assert, is_assignable_to + +T = TypeVar("T") +P = ParamSpec("P") + +class Foo[T]: + def __call__(self): ... + +class FooLegacy(Generic[T]): + def __call__(self): ... + +class Bar[T, **P]: + def __call__(self): ... + +# TODO: should not error +class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`" + def __call__(self): ... + +static_assert(is_assignable_to(Foo, Callable[..., Any])) +static_assert(is_assignable_to(FooLegacy, Callable[..., Any])) +static_assert(is_assignable_to(Bar, Callable[..., Any])) +static_assert(is_assignable_to(BarLegacy, Callable[..., Any])) + +class Spam[T]: ... +class SpamLegacy(Generic[T]): ... +class Eggs[T, **P]: ... + +# TODO: should not error +class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`" + +static_assert(not is_assignable_to(Spam, Callable[..., Any])) +static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any])) +static_assert(not is_assignable_to(Eggs, Callable[..., Any])) + +# TODO: should pass +static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error] +``` + +### Classes with `__call__` as attribute + +An instance type is assignable to a compatible callable type if the instance type's class has a +callable `__call__` attribute. + +TODO: for the moment, we don't consider the callable type as a bound-method descriptor, but this may +change for better compatibility with mypy/pyright. + +```py +from typing import Callable +from ty_extensions import static_assert, is_assignable_to + +def call_impl(a: int) -> str: + return "" + +class A: + __call__: Callable[[int], str] = call_impl + +static_assert(is_assignable_to(A, Callable[[int], str])) +static_assert(not is_assignable_to(A, Callable[[int], int])) +reveal_type(A()(1)) # revealed: str +``` + +### Subclass of + +#### Type of a class with constructor methods + +```py +from typing import Callable +from ty_extensions import static_assert, is_assignable_to + +class A: + def __init__(self, x: int) -> None: ... + +class B: + def __new__(cls, x: str) -> "B": + return super().__new__(cls) + +static_assert(is_assignable_to(type[A], Callable[[int], A])) +static_assert(not is_assignable_to(type[A], Callable[[str], A])) + +static_assert(is_assignable_to(type[B], Callable[[str], B])) +static_assert(not is_assignable_to(type[B], Callable[[int], B])) +``` + +#### Type with no generic parameters + +```py +from typing import Callable, Any +from ty_extensions import static_assert, is_assignable_to + +static_assert(is_assignable_to(type, Callable[..., Any])) +``` + +## Generics + +### Assignability of generic types parameterized by gradual types + +If `Foo` is a class that is generic over a single type variable `T`, `Foo[X]` will be assignable to +`Foo[Y]` iff `X` is assignable to `Y` AND `Y` is assignable to `X`. + +This might appear to be the same principle as the "gradual equivalence" relation, but it is subtly +different. Two gradual types can be said to be "gradually equivalent" iff they have exactly the same +sets of possible materializations -- if they represent the same sets of possible types (the same +sets of sets of possible runtime objects). By this principle `int | Any` is gradually equivalent to +`Unknown | int`, since they have exactly the same sets of posisble materializations. But +`bool | Any` is not equivalent to `int`, since there are many possible materializations of +`bool | Any` that are not assignable to `int`. It is therefore _not_ necessary for `X` to be +gradually equivalent to `Y` in order for `Foo[X]` to be assignable to `Foo[Y]`; it is _only_ +necessary for `X` and `Y` to be mutually assignable. + +```py +from typing import Any, TypeVar, Generic +from ty_extensions import static_assert, is_assignable_to + +InvariantTypeVar = TypeVar("InvariantTypeVar") + +class Foo(Generic[InvariantTypeVar]): + x: InvariantTypeVar + +class A: ... +class B(A): ... +class C: ... + +static_assert(is_assignable_to(Foo[A], Foo[B | Any])) +static_assert(is_assignable_to(Foo[B | Any], Foo[A])) +static_assert(is_assignable_to(Foo[Foo[Any]], Foo[Foo[A | C]])) +static_assert(is_assignable_to(Foo[Foo[A | C]], Foo[Foo[Any]])) +static_assert(is_assignable_to(Foo[tuple[A]], Foo[tuple[Any] | tuple[B]])) +static_assert(is_assignable_to(Foo[tuple[Any] | tuple[B]], Foo[tuple[A]])) + +def f(obj: Foo[A]): + g(obj) + +def g(obj: Foo[B | Any]): + f(obj) + +def f2(obj: Foo[Foo[Any]]): + g2(obj) + +def g2(obj: Foo[Foo[A | C]]): + f2(obj) + +def f3(obj: Foo[tuple[Any] | tuple[B]]): + g3(obj) + +def g3(obj: Foo[tuple[A]]): + f3(obj) +``` + +## `TypeGuard` and `TypeIs` + +`TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`. + +```py +from ty_extensions import Unknown, is_assignable_to, static_assert +from typing_extensions import Any, TypeGuard, TypeIs + +static_assert(is_assignable_to(TypeGuard[Unknown], bool)) +static_assert(is_assignable_to(TypeIs[Any], bool)) + +# TODO no error +static_assert(not is_assignable_to(TypeGuard[Unknown], str)) # error: [static-assert-error] +static_assert(not is_assignable_to(TypeIs[Any], str)) +``` + +[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form +[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form +[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md new file mode 100644 index 0000000000000..81231b4ad5cb9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -0,0 +1,707 @@ +# Disjointness relation + +Two types `S` and `T` are disjoint if their intersection `S & T` is empty (equivalent to `Never`). +This means that it is known that no possible runtime object inhabits both types simultaneously. + +## Basic builtin types + +```py +from typing_extensions import Literal, LiteralString, Any +from ty_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert + +static_assert(is_disjoint_from(bool, str)) +static_assert(not is_disjoint_from(bool, bool)) +static_assert(not is_disjoint_from(bool, int)) +static_assert(not is_disjoint_from(bool, object)) + +static_assert(not is_disjoint_from(Any, bool)) +static_assert(not is_disjoint_from(Any, Any)) +static_assert(not is_disjoint_from(Any, Not[Any])) + +static_assert(not is_disjoint_from(LiteralString, LiteralString)) +static_assert(not is_disjoint_from(str, LiteralString)) +``` + +## Class hierarchies + +```py +from ty_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of +from typing import final + +class A: ... +class B1(A): ... +class B2(A): ... + +# B1 and B2 are subclasses of A, so they are not disjoint from A: +static_assert(not is_disjoint_from(A, B1)) +static_assert(not is_disjoint_from(A, B2)) + +# The two subclasses B1 and B2 are also not disjoint ... +static_assert(not is_disjoint_from(B1, B2)) + +# ... because they could share a common subclass ... +class C(B1, B2): ... + +# ... which lies in their intersection: +static_assert(is_subtype_of(C, Intersection[B1, B2])) + +# However, if a class is marked final, it can not be subclassed ... +@final +class FinalSubclass(A): ... + +static_assert(not is_disjoint_from(FinalSubclass, A)) + +# ... which makes it disjoint from B1, B2: +static_assert(is_disjoint_from(B1, FinalSubclass)) +static_assert(is_disjoint_from(B2, FinalSubclass)) + +# Instance types can also be disjoint if they have disjoint metaclasses. +# No possible subclass of `Meta1` and `Meta2` could exist, therefore +# no possible subclass of `UsesMeta1` and `UsesMeta2` can exist: +class Meta1(type): ... +class UsesMeta1(metaclass=Meta1): ... + +@final +class Meta2(type): ... + +class UsesMeta2(metaclass=Meta2): ... + +static_assert(is_disjoint_from(UsesMeta1, UsesMeta2)) +``` + +## `@final` builtin types + +Some builtins types are declared as `@final`: + +```py +from ty_extensions import static_assert, is_disjoint_from + +class Foo: ... + +# `range`, `slice` and `memoryview` are all declared as `@final`: +static_assert(is_disjoint_from(range, Foo)) +static_assert(is_disjoint_from(type[range], type[Foo])) +static_assert(is_disjoint_from(slice, Foo)) +static_assert(is_disjoint_from(type[slice], type[Foo])) +static_assert(is_disjoint_from(memoryview, Foo)) +static_assert(is_disjoint_from(type[memoryview], type[Foo])) +``` + +## "Solid base" builtin types + +Most other builtins can be subclassed and can even be used in multiple inheritance. However, builtin +classes *cannot* generally be used in multiple inheritance with other builtin types. This is because +the CPython interpreter considers these classes "solid bases": due to the way they are implemented +in C, they have atypical instance memory layouts. No class can ever have more than one "solid base" +in its MRO. + +It's not currently possible for ty to detect in a generalized way whether a class is a "solid base" +or not, but we special-case some commonly used builtin types: + +```py +from typing import Any +from ty_extensions import static_assert, is_disjoint_from + +class Foo: ... + +static_assert(is_disjoint_from(list, dict)) +static_assert(is_disjoint_from(list[Foo], dict)) +static_assert(is_disjoint_from(list[Any], dict)) +static_assert(is_disjoint_from(list, dict[Foo, Foo])) +static_assert(is_disjoint_from(list[Foo], dict[Foo, Foo])) +static_assert(is_disjoint_from(list[Any], dict[Foo, Foo])) +static_assert(is_disjoint_from(list, dict[Any, Any])) +static_assert(is_disjoint_from(list[Foo], dict[Any, Any])) +static_assert(is_disjoint_from(list[Any], dict[Any, Any])) +static_assert(is_disjoint_from(type[list], type[dict])) +``` + +## Other solid bases + +As well as certain classes that are implemented in C extensions, any class that declares non-empty +`__slots__` is also considered a "solid base"; these types are also considered to be disjoint by ty: + +```py +from ty_extensions import static_assert, is_disjoint_from + +class A: + __slots__ = ("a",) + +class B: + __slots__ = ("a",) + +class C: + __slots__ = () + +static_assert(is_disjoint_from(A, B)) +static_assert(is_disjoint_from(type[A], type[B])) +static_assert(not is_disjoint_from(A, C)) +static_assert(not is_disjoint_from(type[A], type[C])) +static_assert(not is_disjoint_from(B, C)) +static_assert(not is_disjoint_from(type[B], type[C])) +``` + +Two solid bases are not disjoint if one inherits from the other, however: + +```py +class D(A): + __slots__ = ("d",) + +static_assert(is_disjoint_from(D, B)) +static_assert(not is_disjoint_from(D, A)) +``` + +## Tuple types + +```py +from typing_extensions import Literal, Never +from ty_extensions import TypeOf, is_disjoint_from, static_assert + +static_assert(is_disjoint_from(tuple[()], TypeOf[object])) +static_assert(is_disjoint_from(tuple[()], TypeOf[Literal])) + +static_assert(is_disjoint_from(tuple[None], None)) +static_assert(is_disjoint_from(tuple[None], Literal[b"a"])) +static_assert(is_disjoint_from(tuple[None], Literal["a"])) +static_assert(is_disjoint_from(tuple[None], Literal[1])) +static_assert(is_disjoint_from(tuple[None], Literal[True])) + +static_assert(is_disjoint_from(tuple[Literal[1]], tuple[Literal[2]])) +static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1]])) +static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[3]])) + +static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], int])) +static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[int, ...])) + +# TODO: should pass +static_assert(is_disjoint_from(tuple[int, int], tuple[None, ...])) # error: [static-assert-error] +``` + +## Unions + +```py +from typing_extensions import Literal +from ty_extensions import Intersection, is_disjoint_from, static_assert + +static_assert(is_disjoint_from(Literal[1, 2], Literal[3])) +static_assert(is_disjoint_from(Literal[1, 2], Literal[3, 4])) + +static_assert(not is_disjoint_from(Literal[1, 2], Literal[2])) +static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) +``` + +## Intersections + +```py +from typing_extensions import Literal, final, Any, LiteralString +from ty_extensions import Intersection, is_disjoint_from, static_assert, Not, AlwaysFalsy + +@final +class P: ... + +@final +class Q: ... + +@final +class R: ... + +# For three pairwise disjoint classes ... +static_assert(is_disjoint_from(P, Q)) +static_assert(is_disjoint_from(P, R)) +static_assert(is_disjoint_from(Q, R)) + +# ... their intersections are also disjoint: +static_assert(is_disjoint_from(Intersection[P, Q], R)) +static_assert(is_disjoint_from(Intersection[P, R], Q)) +static_assert(is_disjoint_from(Intersection[Q, R], P)) + +# On the other hand, for non-disjoint classes ... +class X: ... +class Y: ... +class Z: ... + +static_assert(not is_disjoint_from(X, Y)) +static_assert(not is_disjoint_from(X, Z)) +static_assert(not is_disjoint_from(Y, Z)) + +# ... their intersections are also not disjoint: +static_assert(not is_disjoint_from(Intersection[X, Y], Z)) +static_assert(not is_disjoint_from(Intersection[X, Z], Y)) +static_assert(not is_disjoint_from(Intersection[Y, Z], X)) + +# If one side has a positive fully-static element and the other side has a negative of that element, they are disjoint +static_assert(is_disjoint_from(int, Not[int])) +static_assert(is_disjoint_from(Intersection[X, Y, Not[Z]], Intersection[X, Z])) +static_assert(is_disjoint_from(Intersection[X, Not[Literal[1]]], Literal[1])) + +class Parent: ... +class Child(Parent): ... + +static_assert(not is_disjoint_from(Parent, Child)) +static_assert(not is_disjoint_from(Parent, Not[Child])) +static_assert(not is_disjoint_from(Not[Parent], Not[Child])) +static_assert(is_disjoint_from(Not[Parent], Child)) +static_assert(is_disjoint_from(Intersection[X, Not[Parent]], Child)) +static_assert(is_disjoint_from(Intersection[X, Not[Parent]], Intersection[X, Child])) + +static_assert(not is_disjoint_from(Intersection[Any, X], Intersection[Any, Not[Y]])) +static_assert(not is_disjoint_from(Intersection[Any, Not[Y]], Intersection[Any, X])) + +static_assert(is_disjoint_from(Intersection[int, Any], Not[int])) +static_assert(is_disjoint_from(Not[int], Intersection[int, Any])) + +# TODO https://github.com/astral-sh/ty/issues/216 +static_assert(is_disjoint_from(AlwaysFalsy, Intersection[LiteralString, Not[Literal[""]]])) # error: [static-assert-error] +``` + +## Special types + +### `Never` + +`Never` is disjoint from every type, including itself. + +```py +from typing_extensions import Never +from ty_extensions import is_disjoint_from, static_assert + +static_assert(is_disjoint_from(Never, Never)) +static_assert(is_disjoint_from(Never, None)) +static_assert(is_disjoint_from(Never, int)) +static_assert(is_disjoint_from(Never, object)) +``` + +### `None` + +```py +from typing_extensions import Literal, LiteralString +from ty_extensions import is_disjoint_from, static_assert, Intersection, Not + +static_assert(is_disjoint_from(None, Literal[True])) +static_assert(is_disjoint_from(None, Literal[1])) +static_assert(is_disjoint_from(None, Literal["test"])) +static_assert(is_disjoint_from(None, Literal[b"test"])) +static_assert(is_disjoint_from(None, LiteralString)) +static_assert(is_disjoint_from(None, int)) +static_assert(is_disjoint_from(None, type[object])) + +static_assert(not is_disjoint_from(None, None)) +static_assert(not is_disjoint_from(None, int | None)) +static_assert(not is_disjoint_from(None, object)) + +static_assert(is_disjoint_from(Intersection[int, Not[str]], None)) +static_assert(is_disjoint_from(None, Intersection[int, Not[str]])) +``` + +### Literals + +```py +from typing_extensions import Literal, LiteralString +from ty_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert, AlwaysFalsy, AlwaysTruthy + +static_assert(is_disjoint_from(Literal[True], Literal[False])) +static_assert(is_disjoint_from(Literal[True], Literal[1])) +static_assert(is_disjoint_from(Literal[False], Literal[0])) + +static_assert(is_disjoint_from(Literal[1], Literal[2])) + +static_assert(is_disjoint_from(Literal["a"], Literal["b"])) + +static_assert(is_disjoint_from(Literal[b"a"], LiteralString)) +static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"])) +static_assert(is_disjoint_from(Literal[b"a"], Literal["a"])) + +static_assert(is_disjoint_from(type[object], TypeOf[Literal])) +static_assert(is_disjoint_from(type[str], LiteralString)) + +static_assert(not is_disjoint_from(Literal[True], Literal[True])) +static_assert(not is_disjoint_from(Literal[False], Literal[False])) +static_assert(not is_disjoint_from(Literal[True], bool)) +static_assert(not is_disjoint_from(Literal[True], int)) + +static_assert(not is_disjoint_from(Literal[1], Literal[1])) + +static_assert(not is_disjoint_from(Literal["a"], Literal["a"])) +static_assert(not is_disjoint_from(Literal["a"], LiteralString)) +static_assert(not is_disjoint_from(Literal["a"], str)) + +# TODO: No errors +# error: [static-assert-error] +static_assert(is_disjoint_from(AlwaysFalsy, Intersection[LiteralString, Not[Literal[""]]])) +# error: [static-assert-error] +static_assert(is_disjoint_from(Intersection[Not[Literal[True]], Not[Literal[False]]], bool)) +# error: [static-assert-error] +static_assert(is_disjoint_from(Intersection[AlwaysFalsy, Not[Literal[False]]], bool)) +# error: [static-assert-error] +static_assert(is_disjoint_from(Intersection[AlwaysTruthy, Not[Literal[True]]], bool)) + +# TODO: No errors +# The condition `is_disjoint(T, Not[T])` must still be satisfied after the following transformations: +# `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` +# error: [static-assert-error] +static_assert(is_disjoint_from(Intersection[LiteralString, AlwaysTruthy], Not[LiteralString] | AlwaysFalsy)) +# `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` +# error: [static-assert-error] +static_assert(is_disjoint_from(Intersection[LiteralString, Not[AlwaysFalsy]], Not[LiteralString] | AlwaysFalsy)) +``` + +### Class, module and function literals + +```toml +[environment] +python-version = "3.12" +``` + +```py +from types import ModuleType, FunctionType +from ty_extensions import TypeOf, is_disjoint_from, static_assert + +class A: ... +class B: ... + +type LiteralA = TypeOf[A] +type LiteralB = TypeOf[B] + +# Class literals for different classes are always disjoint. +# They are singleton types that only contain the class object itself. +static_assert(is_disjoint_from(LiteralA, LiteralB)) + +# The class A is a subclass of A, so A is not disjoint from type[A]: +static_assert(not is_disjoint_from(LiteralA, type[A])) + +# The class A is disjoint from type[B] because it's not a subclass of B: +static_assert(is_disjoint_from(LiteralA, type[B])) + +# However, type[A] is not disjoint from type[B], as there could be +# classes that inherit from both A and B: +static_assert(not is_disjoint_from(type[A], type[B])) + +import random +import math + +static_assert(is_disjoint_from(TypeOf[random], TypeOf[math])) +static_assert(not is_disjoint_from(TypeOf[random], ModuleType)) +static_assert(not is_disjoint_from(TypeOf[random], object)) + +def f(): ... +def g(): ... + +static_assert(is_disjoint_from(TypeOf[f], TypeOf[g])) +static_assert(not is_disjoint_from(TypeOf[f], FunctionType)) +static_assert(not is_disjoint_from(TypeOf[f], object)) +``` + +### `AlwaysTruthy` and `AlwaysFalsy` + +```py +from ty_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert +from typing import Literal + +static_assert(is_disjoint_from(None, AlwaysTruthy)) +static_assert(not is_disjoint_from(None, AlwaysFalsy)) + +static_assert(is_disjoint_from(AlwaysFalsy, AlwaysTruthy)) +static_assert(not is_disjoint_from(str, AlwaysFalsy)) +static_assert(not is_disjoint_from(str, AlwaysTruthy)) + +static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy)) +static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy)) +``` + +### Instance types versus `type[T]` types + +An instance type is disjoint from a `type[T]` type if the instance type is `@final` and the class of +the instance type is not a subclass of `T`'s metaclass. + +```py +from typing import final +from ty_extensions import is_disjoint_from, static_assert + +@final +class Foo: ... + +static_assert(is_disjoint_from(Foo, type[int])) +static_assert(is_disjoint_from(type[object], Foo)) +static_assert(is_disjoint_from(type[dict], Foo)) + +# Instance types can be disjoint from `type[]` types +# even if the instance type is a subtype of `type` + +@final +class Meta1(type): ... + +class UsesMeta1(metaclass=Meta1): ... + +static_assert(not is_disjoint_from(Meta1, type[UsesMeta1])) + +class Meta2(type): ... +class UsesMeta2(metaclass=Meta2): ... + +static_assert(not is_disjoint_from(Meta2, type[UsesMeta2])) +static_assert(is_disjoint_from(Meta1, type[UsesMeta2])) +``` + +### `type[T]` versus `type[S]` + +By the same token, `type[T]` is disjoint from `type[S]` if `T` is `@final`, `S` is `@final`, or the +metaclass of `T` is disjoint from the metaclass of `S`. + +```py +from typing import final +from ty_extensions import static_assert, is_disjoint_from + +@final +class Meta1(type): ... + +class Meta2(type): ... + +static_assert(is_disjoint_from(type[Meta1], type[Meta2])) + +class UsesMeta1(metaclass=Meta1): ... +class UsesMeta2(metaclass=Meta2): ... + +static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2])) +``` + +### `property` + +```py +from ty_extensions import is_disjoint_from, static_assert, TypeOf +from typing import final + +class C: + @property + def prop(self) -> int: + return 1 + +reveal_type(C.prop) # revealed: property + +@final +class D: + pass + +class Whatever: ... + +static_assert(not is_disjoint_from(Whatever, TypeOf[C.prop])) +static_assert(not is_disjoint_from(TypeOf[C.prop], Whatever)) +static_assert(is_disjoint_from(TypeOf[C.prop], D)) +static_assert(is_disjoint_from(D, TypeOf[C.prop])) +``` + +### `TypeGuard` and `TypeIs` + +```py +from ty_extensions import static_assert, is_disjoint_from +from typing_extensions import TypeGuard, TypeIs + +static_assert(not is_disjoint_from(bool, TypeGuard[str])) +static_assert(not is_disjoint_from(bool, TypeIs[str])) + +# TODO no error +static_assert(is_disjoint_from(str, TypeGuard[str])) # error: [static-assert-error] +static_assert(is_disjoint_from(str, TypeIs[str])) +``` + +### `Protocol` + +A protocol is disjoint from another type if any of the protocol's members are available as an +attribute on the other type *but* the type of the attribute on the other type is disjoint from the +type of the protocol's member. + +```py +from typing_extensions import Protocol, Literal, final, ClassVar +from ty_extensions import is_disjoint_from, static_assert + +class HasAttrA(Protocol): + attr: Literal["a"] + +class SupportsInt(Protocol): + def __int__(self) -> int: ... + +class A: + attr: Literal["a"] + +class B: + attr: Literal["b"] + +class C: + foo: int + +class D: + attr: int + +@final +class E: + pass + +@final +class F: + def __int__(self) -> int: + return 1 + +static_assert(not is_disjoint_from(HasAttrA, A)) +static_assert(is_disjoint_from(HasAttrA, B)) +# A subclass of E may satisfy HasAttrA +static_assert(not is_disjoint_from(HasAttrA, C)) +static_assert(is_disjoint_from(HasAttrA, D)) +static_assert(is_disjoint_from(HasAttrA, E)) + +static_assert(is_disjoint_from(SupportsInt, E)) +static_assert(not is_disjoint_from(SupportsInt, F)) + +class NotIterable(Protocol): + __iter__: ClassVar[None] + +static_assert(is_disjoint_from(tuple[int, int], NotIterable)) + +class Foo: + BAR: ClassVar[int] + +class BarNone(Protocol): + BAR: None + +static_assert(is_disjoint_from(type[Foo], BarNone)) +``` + +## Callables + +No two callable types are disjoint because there exists a non-empty callable type +`(*args: object, **kwargs: object) -> Never` that is a subtype of all fully static callable types. +As such, for any two callable types, it is possible to conceive of a runtime callable object that +would inhabit both types simultaneously. + +```py +from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert +from typing_extensions import Callable, Literal, Never + +def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ... + +static_assert(not is_disjoint_from(Callable[[], Never], CallableTypeOf[mixed])) +static_assert(not is_disjoint_from(Callable[[int, str], float], CallableTypeOf[mixed])) + +# Using gradual form +static_assert(not is_disjoint_from(Callable[..., None], Callable[[], None])) +static_assert(not is_disjoint_from(Callable[..., None], Callable[..., None])) +static_assert(not is_disjoint_from(Callable[..., None], Callable[[Literal[1]], None])) + +# Using `Never` +static_assert(not is_disjoint_from(Callable[[], Never], Callable[[], Never])) +static_assert(not is_disjoint_from(Callable[[Never], str], Callable[[Never], int])) +``` + +A callable type is disjoint from all literal types. + +```py +from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert +from typing_extensions import Callable, Literal, Never + +static_assert(is_disjoint_from(Callable[[], None], Literal[""])) +static_assert(is_disjoint_from(Callable[[], None], Literal[b""])) +static_assert(is_disjoint_from(Callable[[], None], Literal[1])) +static_assert(is_disjoint_from(Callable[[], None], Literal[True])) +``` + +A callable type is disjoint from nominal instance types where the classes are final and whose +`__call__` is not callable. + +```py +from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert +from typing_extensions import Any, Callable, final + +@final +class C: ... + +static_assert(is_disjoint_from(bool, Callable[..., Any])) +static_assert(is_disjoint_from(C, Callable[..., Any])) +static_assert(is_disjoint_from(bool | C, Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[..., Any], bool)) +static_assert(is_disjoint_from(Callable[..., Any], C)) +static_assert(is_disjoint_from(Callable[..., Any], bool | C)) + +static_assert(not is_disjoint_from(str, Callable[..., Any])) +static_assert(not is_disjoint_from(bool | str, Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], str)) +static_assert(not is_disjoint_from(Callable[..., Any], bool | str)) + +def bound_with_valid_type(): + @final + class D: + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + + static_assert(not is_disjoint_from(D, Callable[..., Any])) + static_assert(not is_disjoint_from(Callable[..., Any], D)) + +def possibly_unbound_with_valid_type(flag: bool): + @final + class E: + if flag: + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + + static_assert(not is_disjoint_from(E, Callable[..., Any])) + static_assert(not is_disjoint_from(Callable[..., Any], E)) + +def bound_with_invalid_type(): + @final + class F: + __call__: int = 1 + + static_assert(is_disjoint_from(F, Callable[..., Any])) + static_assert(is_disjoint_from(Callable[..., Any], F)) + +def possibly_unbound_with_invalid_type(flag: bool): + @final + class G: + if flag: + __call__: int = 1 + + static_assert(is_disjoint_from(G, Callable[..., Any])) + static_assert(is_disjoint_from(Callable[..., Any], G)) +``` + +A callable type is disjoint from special form types, except for callable special forms. + +```py +from ty_extensions import is_disjoint_from, static_assert, TypeOf +from typing_extensions import Any, Callable, TypedDict +from typing import Literal, Union, Optional, Final, Type, ChainMap, Counter, OrderedDict, DefaultDict, Deque + +# Most special forms are disjoint from callable types because they are +# type constructors/annotations that are subscripted, not called. +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Literal])) +static_assert(is_disjoint_from(TypeOf[Literal], Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[[], None], TypeOf[Union])) +static_assert(is_disjoint_from(TypeOf[Union], Callable[[], None])) + +static_assert(is_disjoint_from(Callable[[int], str], TypeOf[Optional])) +static_assert(is_disjoint_from(TypeOf[Optional], Callable[[int], str])) + +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Type])) +static_assert(is_disjoint_from(TypeOf[Type], Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Final])) +static_assert(is_disjoint_from(TypeOf[Final], Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Callable])) +static_assert(is_disjoint_from(TypeOf[Callable], Callable[..., Any])) + +# However, some special forms are callable (TypedDict and collection constructors) +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[TypedDict])) +static_assert(not is_disjoint_from(TypeOf[TypedDict], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[ChainMap])) +static_assert(not is_disjoint_from(TypeOf[ChainMap], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[Counter])) +static_assert(not is_disjoint_from(TypeOf[Counter], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[DefaultDict])) +static_assert(not is_disjoint_from(TypeOf[DefaultDict], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[Deque])) +static_assert(not is_disjoint_from(TypeOf[Deque], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[OrderedDict])) +static_assert(not is_disjoint_from(TypeOf[OrderedDict], Callable[..., Any])) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md new file mode 100644 index 0000000000000..b309c266be3e0 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -0,0 +1,556 @@ +# Equivalence relation + +`is_equivalent_to` implements [the equivalence relation] on types. + +For fully static types, two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is +a subtype of `A` (that is, the two types represent the same set of values). + +Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also +materializations of `B`, and all materializations of `B` are also materializations of `A`. + +## Basic + +### Fully static + +```py +from typing_extensions import Literal, LiteralString, Never +from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy + +static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2])) +static_assert(is_equivalent_to(type[object], type)) +static_assert(is_equivalent_to(type, type[object])) + +static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0])) +static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2])) +static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3])) +static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2])) + +static_assert(is_equivalent_to(Never, Never)) +static_assert(is_equivalent_to(AlwaysTruthy, AlwaysTruthy)) +static_assert(is_equivalent_to(AlwaysFalsy, AlwaysFalsy)) +static_assert(is_equivalent_to(LiteralString, LiteralString)) + +static_assert(is_equivalent_to(Literal[True], Literal[True])) +static_assert(is_equivalent_to(Literal[False], Literal[False])) +static_assert(is_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2])) + +static_assert(is_equivalent_to(TypeOf[str], TypeOf[str])) +static_assert(is_equivalent_to(type, type[object])) +``` + +### Gradual + +```py +from typing import Any +from typing_extensions import Literal, LiteralString, Never +from ty_extensions import Unknown, is_equivalent_to, static_assert + +static_assert(is_equivalent_to(Any, Any)) +static_assert(is_equivalent_to(Unknown, Unknown)) +static_assert(is_equivalent_to(Any, Unknown)) +static_assert(not is_equivalent_to(Any, None)) + +static_assert(not is_equivalent_to(type, type[Any])) +static_assert(not is_equivalent_to(type[object], type[Any])) +``` + +## Unions and intersections + +```py +from typing import Any +from ty_extensions import Intersection, Not, Unknown, is_equivalent_to, static_assert + +static_assert(is_equivalent_to(str | int, str | int)) +static_assert(is_equivalent_to(str | int | Any, str | int | Unknown)) +static_assert(is_equivalent_to(str | int, int | str)) +static_assert(is_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]])) +static_assert(is_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]])) + +static_assert(not is_equivalent_to(str | int, int | str | bytes)) +static_assert(not is_equivalent_to(str | int | bytes, int | str | dict)) + +static_assert(is_equivalent_to(Unknown, Unknown | Any)) +static_assert(is_equivalent_to(Unknown, Intersection[Unknown, Any])) + +class P: ... +class Q: ... +class R: ... +class S: ... + +static_assert(is_equivalent_to(P | Q | R, P | R | Q)) # 1 +static_assert(is_equivalent_to(P | Q | R, Q | P | R)) # 2 +static_assert(is_equivalent_to(P | Q | R, Q | R | P)) # 3 +static_assert(is_equivalent_to(P | Q | R, R | P | Q)) # 4 +static_assert(is_equivalent_to(P | Q | R, R | Q | P)) # 5 +static_assert(is_equivalent_to(P | R | Q, Q | P | R)) # 6 +static_assert(is_equivalent_to(P | R | Q, Q | R | P)) # 7 +static_assert(is_equivalent_to(P | R | Q, R | P | Q)) # 8 +static_assert(is_equivalent_to(P | R | Q, R | Q | P)) # 9 +static_assert(is_equivalent_to(Q | P | R, Q | R | P)) # 10 +static_assert(is_equivalent_to(Q | P | R, R | P | Q)) # 11 +static_assert(is_equivalent_to(Q | P | R, R | Q | P)) # 12 +static_assert(is_equivalent_to(Q | R | P, R | P | Q)) # 13 +static_assert(is_equivalent_to(Q | R | P, R | Q | P)) # 14 +static_assert(is_equivalent_to(R | P | Q, R | Q | P)) # 15 + +static_assert(is_equivalent_to(str | None, None | str)) + +static_assert(is_equivalent_to(Intersection[P, Q], Intersection[Q, P])) +static_assert(is_equivalent_to(Intersection[Q, Not[P]], Intersection[Not[P], Q])) +static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P], R, Q])) +static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q])) +``` + +## Tuples + +```py +from ty_extensions import Unknown, is_equivalent_to, static_assert +from typing import Any + +static_assert(is_equivalent_to(tuple[str, Any], tuple[str, Unknown])) + +static_assert(not is_equivalent_to(tuple[str, int], tuple[str, int, bytes])) +static_assert(not is_equivalent_to(tuple[str, int], tuple[int, str])) +``` + +## Tuples containing equivalent but differently ordered unions/intersections are equivalent + +```py +from ty_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not +from typing import Literal + +class P: ... +class Q: ... +class R: ... +class S: ... + +static_assert(is_equivalent_to(tuple[P | Q], tuple[Q | P])) +static_assert(is_equivalent_to(tuple[P | None], tuple[None | P])) +static_assert( + is_equivalent_to(tuple[Intersection[P, Q] | Intersection[R, Not[S]]], tuple[Intersection[Not[S], R] | Intersection[Q, P]]) +) +``` + +## Unions containing tuples containing tuples containing unions (etc.) + +```py +from ty_extensions import is_equivalent_to, static_assert, Intersection + +class P: ... +class Q: ... + +static_assert( + is_equivalent_to( + tuple[tuple[tuple[P | Q]]] | P, + tuple[tuple[tuple[Q | P]]] | P, + ) +) +static_assert( + is_equivalent_to( + tuple[tuple[tuple[tuple[tuple[Intersection[P, Q]]]]]], + tuple[tuple[tuple[tuple[tuple[Intersection[Q, P]]]]]], + ) +) +``` + +## Intersections containing tuples containing unions + +```py +from ty_extensions import is_equivalent_to, static_assert, Intersection + +class P: ... +class Q: ... +class R: ... + +static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R])) +``` + +## Unions containing generic instances parameterized by unions + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import is_equivalent_to, static_assert + +class A: ... +class B: ... +class Foo[T]: ... + +static_assert(is_equivalent_to(A | Foo[A | B], Foo[B | A] | A)) +``` + +## Callable + +### Equivalent + +For an equivalence relationship, the default value does not necessarily need to be the same but if +the parameter in one of the callable has a default value then the corresponding parameter in the +other callable should also have a default value. + +```py +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert +from typing import Callable + +def f1(a: int = 1) -> None: ... +def f2(a: int = 2) -> None: ... + +static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) +static_assert(is_equivalent_to(CallableTypeOf[f1] | bool | CallableTypeOf[f2], CallableTypeOf[f2] | bool | CallableTypeOf[f1])) +``` + +The names of the positional-only, variadic and keyword-variadic parameters does not need to be the +same. + +```py +def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ... +def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ... + +static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) +static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])) +``` + +Putting it all together, the following two callables are equivalent: + +```py +def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ... +def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ... + +static_assert(is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6])) +static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], CallableTypeOf[f6] | bool | CallableTypeOf[f5])) +``` + +### Not equivalent + +There are multiple cases when two callable types are not equivalent which are enumerated below. + +```py +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert +from typing import Callable +``` + +When the number of parameters is different: + +```py +def f1(a: int) -> None: ... +def f2(a: int, b: int) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) +``` + +When the return types are not equivalent in one or both of the callable types: + +```py +def f3(): ... +def f4() -> None: ... + +static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None])) +static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3])) +static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) +static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3])) +``` + +When the parameter names are different: + +```py +def f5(a: int) -> None: ... +def f6(b: int) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6])) +``` + +When only one of the callable types has parameter names: + +```py +static_assert(not is_equivalent_to(CallableTypeOf[f5], Callable[[int], None])) +``` + +When the parameter kinds are different: + +```py +def f7(a: int, /) -> None: ... +def f8(a: int) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeOf[f7], CallableTypeOf[f8])) +``` + +When the annotated types of the parameters are not equivalent or absent in one or both of the +callable types: + +```py +def f9(a: int) -> None: ... +def f10(a: str) -> None: ... +def f11(a) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10])) +static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11])) +static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10])) +static_assert(is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11])) +``` + +When the default value for a parameter is present only in one of the callable type: + +```py +def f12(a: int) -> None: ... +def f13(a: int = 2) -> None: ... + +static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13])) +static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12])) +``` + +### Unions containing `Callable`s + +Two unions containing different `Callable` types are equivalent even if the unions are differently +ordered: + +```py +from ty_extensions import CallableTypeOf, Unknown, is_equivalent_to, static_assert + +def f(x): ... +def g(x: Unknown): ... + +static_assert(is_equivalent_to(CallableTypeOf[f] | int | str, str | int | CallableTypeOf[g])) +``` + +### Unions containing `Callable`s containing unions + +Differently ordered unions inside `Callable`s inside unions can still be equivalent: + +```py +from typing import Callable +from ty_extensions import is_equivalent_to, static_assert + +static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int)) +``` + +### Overloads + +#### One overload + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... + +@overload +def overloaded(a: Child) -> None: ... +@overload +def overloaded(a: Parent) -> None: ... +@overload +def overloaded(a: Grandparent) -> None: ... +``` + +```py +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert +from overloaded import Grandparent, Parent, Child, overloaded + +def grandparent(a: Grandparent) -> None: ... + +static_assert(is_equivalent_to(CallableTypeOf[grandparent], CallableTypeOf[overloaded])) +static_assert(is_equivalent_to(CallableTypeOf[overloaded], CallableTypeOf[grandparent])) +``` + +#### Both overloads + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... + +@overload +def pg(a: Parent) -> None: ... +@overload +def pg(a: Grandparent) -> None: ... + +@overload +def cpg(a: Child) -> None: ... +@overload +def cpg(a: Parent) -> None: ... +@overload +def cpg(a: Grandparent) -> None: ... +``` + +```py +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert +from overloaded import pg, cpg + +static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg])) +static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg])) +``` + +### Function-literal types and bound-method types + +Function-literal types and bound-method types are always considered self-equivalent. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import is_equivalent_to, TypeOf, static_assert + +def f(): ... + +static_assert(is_equivalent_to(TypeOf[f], TypeOf[f])) + +class A: + def method(self) -> int: + return 42 + +static_assert(is_equivalent_to(TypeOf[A.method], TypeOf[A.method])) +type X = TypeOf[A.method] +static_assert(is_equivalent_to(X, X)) +``` + +### Non-fully-static callable types + +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 above. + +```py +from ty_extensions import Unknown, CallableTypeOf, is_equivalent_to, static_assert +from typing import Any, Callable + +static_assert(is_equivalent_to(Callable[..., int], Callable[..., int])) +static_assert(is_equivalent_to(Callable[..., Any], Callable[..., Unknown])) +static_assert(is_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None])) + +static_assert(not is_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None])) +static_assert(not is_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None])) +static_assert(not is_equivalent_to(Callable[..., None], Callable[[], None])) +``` + +A function with no explicit return type should be gradual equivalent to a callable with a return +type of `Any`. + +```py +def f1(): + return + +static_assert(is_equivalent_to(CallableTypeOf[f1], Callable[[], Any])) +``` + +And, similarly for parameters with no annotations. + +```py +def f2(a, b, /) -> None: + return + +static_assert(is_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None])) +``` + +Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs` +parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable +with `...` as the parameter type. + +```py +def variadic_without_annotation(*args, **kwargs): + return + +def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any: + return + +static_assert(is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any])) +static_assert(is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any])) +``` + +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): + return + +def variadic_kwargs(**kwargs): + return + +static_assert(not is_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any])) +static_assert(not is_equivalent_to(CallableTypeOf[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_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) + +def f3(a=1): ... +def f4(a=2): ... +def f5(a): ... + +static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) +static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])) +static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5])) + +def f6(a, /): ... + +static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6])) +``` + +## Module-literal types + +Two "copies" of a single-file module are considered equivalent types, even if the different copies +were originally imported in different first-party modules: + +`module.py`: + +```py +import typing +``` + +`main.py`: + +```py +import typing +from module import typing as other_typing +from ty_extensions import TypeOf, static_assert, is_equivalent_to + +static_assert(is_equivalent_to(TypeOf[typing], TypeOf[other_typing])) +static_assert(is_equivalent_to(TypeOf[typing] | int | str, str | int | TypeOf[other_typing])) +``` + +We currently do not consider module-literal types to be equivalent if the underlying module is a +package and the different "copies" of the module were originally imported in different modules. This +is because we might consider submodules to be available as attributes on one copy but not on the +other, depending on whether those submodules were explicitly imported in the original importing +module: + +`module2.py`: + +```py +import importlib +import importlib.abc +``` + +`main2.py`: + +```py +import importlib +from module2 import importlib as other_importlib +from ty_extensions import TypeOf, static_assert, is_equivalent_to + +# error: [unresolved-attribute] "Type `` has no attribute `abc`" +reveal_type(importlib.abc) # revealed: Unknown + +reveal_type(other_importlib.abc) # revealed: + +static_assert(not is_equivalent_to(TypeOf[importlib], TypeOf[other_importlib])) +``` + +[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize +[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_single_valued.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md index b6339cc6b0d7d..e947c248831f7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md @@ -5,7 +5,7 @@ A type is single-valued iff it is not empty and all inhabitants of it compare eq ```py import types from typing_extensions import Any, Literal, LiteralString, Never, Callable -from knot_extensions import is_single_valued, static_assert, TypeOf +from ty_extensions import is_single_valued, static_assert, TypeOf static_assert(is_single_valued(None)) static_assert(is_single_valued(Literal[True])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md similarity index 81% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md index a60efdfd9e8f7..a3c41449bc76c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md @@ -6,7 +6,7 @@ A type is a singleton type iff it has exactly one inhabitant. ```py from typing_extensions import Literal, Never, Callable -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(None)) static_assert(is_singleton(Literal[True])) @@ -39,7 +39,7 @@ python-version = "3.12" ```py from typing_extensions import _NoDefaultType -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(_NoDefaultType)) ``` @@ -53,7 +53,7 @@ python-version = "3.13" ```py from typing import _NoDefaultType -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(_NoDefaultType)) ``` @@ -72,7 +72,7 @@ python-version = "3.9" ``` ```py -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(Ellipsis.__class__)) static_assert(is_singleton((...).__class__)) @@ -90,7 +90,7 @@ python-version = "3.10" ```py import types -from knot_extensions import static_assert, is_singleton +from ty_extensions import static_assert, is_singleton static_assert(is_singleton(types.EllipsisType)) ``` @@ -108,7 +108,7 @@ python-version = "3.9" ``` ```py -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(NotImplemented.__class__)) ``` @@ -126,12 +126,9 @@ python-version = "3.10" ```py import types -from knot_extensions import static_assert, is_singleton +from ty_extensions import static_assert, is_singleton -# TODO: types.NotImplementedType is a TypeAlias of builtins._NotImplementedType -# Once TypeAlias support is added, it should satisfy `is_singleton` -reveal_type(types.NotImplementedType) # revealed: Unknown | Literal[_NotImplementedType] -static_assert(not is_singleton(types.NotImplementedType)) +static_assert(is_singleton(types.NotImplementedType)) ``` ### Callables @@ -148,7 +145,7 @@ have to hold true; it's more of a unit test for our current implementation. ```py import types from typing import Callable -from knot_extensions import static_assert, is_singleton, TypeOf +from ty_extensions import static_assert, is_singleton, TypeOf class A: def method(self): ... diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md new file mode 100644 index 0000000000000..5726ce83f79ff --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -0,0 +1,2009 @@ +# Subtype relation + +```toml +[environment] +python-version = "3.12" +``` + +The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`. + +A fully static type `S` is a subtype of another fully static type `T` iff the set of values +represented by `S` is a subset of the set of values represented by `T`. + +A non fully static type `S` can also be safely considered a subtype of a non fully static type `T`, +if all possible materializations of `S` represent sets of values that are a subset of every possible +set of values represented by a materialization of `T`. + +See the [typing documentation] for more information. + +## Basic builtin types + +- `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a + supertype of `bool` (present in `bool`s bases and MRO). +- `int` is not a subtype of `float`/`complex`, although this is muddied by the + [special case for float and complex] where annotations of `float` and `complex` are interpreted + as `int | float` and `int | float | complex`, respectively. + +```py +from ty_extensions import is_subtype_of, static_assert, JustFloat, JustComplex + +static_assert(is_subtype_of(bool, bool)) +static_assert(is_subtype_of(bool, int)) +static_assert(is_subtype_of(bool, object)) + +static_assert(is_subtype_of(int, int)) +static_assert(is_subtype_of(int, object)) + +static_assert(is_subtype_of(object, object)) + +static_assert(not is_subtype_of(int, bool)) +static_assert(not is_subtype_of(int, str)) +static_assert(not is_subtype_of(object, int)) + +static_assert(not is_subtype_of(int, JustFloat)) +static_assert(not is_subtype_of(int, JustComplex)) + +static_assert(is_subtype_of(TypeError, Exception)) +static_assert(is_subtype_of(FloatingPointError, Exception)) +``` + +## Class hierarchies + +```py +from ty_extensions import is_subtype_of, static_assert +from typing_extensions import Never + +class A: ... +class B1(A): ... +class B2(A): ... +class C(B1, B2): ... + +static_assert(is_subtype_of(B1, A)) +static_assert(not is_subtype_of(A, B1)) + +static_assert(is_subtype_of(B2, A)) +static_assert(not is_subtype_of(A, B2)) + +static_assert(not is_subtype_of(B1, B2)) +static_assert(not is_subtype_of(B2, B1)) + +static_assert(is_subtype_of(C, B1)) +static_assert(is_subtype_of(C, B2)) +static_assert(not is_subtype_of(B1, C)) +static_assert(not is_subtype_of(B2, C)) +static_assert(is_subtype_of(C, A)) +static_assert(not is_subtype_of(A, C)) + +static_assert(is_subtype_of(Never, A)) +static_assert(is_subtype_of(Never, B1)) +static_assert(is_subtype_of(Never, B2)) +static_assert(is_subtype_of(Never, C)) + +static_assert(is_subtype_of(A, object)) +static_assert(is_subtype_of(B1, object)) +static_assert(is_subtype_of(B2, object)) +static_assert(is_subtype_of(C, object)) +``` + +## Literal types + +```py +from typing_extensions import Literal, LiteralString +from ty_extensions import is_subtype_of, static_assert, TypeOf, JustFloat + +# Boolean literals +static_assert(is_subtype_of(Literal[True], bool)) +static_assert(is_subtype_of(Literal[True], int)) +static_assert(is_subtype_of(Literal[True], object)) + +# Integer literals +static_assert(is_subtype_of(Literal[1], int)) +static_assert(is_subtype_of(Literal[1], object)) + +static_assert(not is_subtype_of(Literal[1], bool)) + +static_assert(not is_subtype_of(Literal[1], JustFloat)) + +# String literals +static_assert(is_subtype_of(Literal["foo"], LiteralString)) +static_assert(is_subtype_of(Literal["foo"], str)) +static_assert(is_subtype_of(Literal["foo"], object)) + +static_assert(is_subtype_of(LiteralString, str)) +static_assert(is_subtype_of(LiteralString, object)) + +# Bytes literals +static_assert(is_subtype_of(Literal[b"foo"], bytes)) +static_assert(is_subtype_of(Literal[b"foo"], object)) +``` + +## Heterogeneous tuple types + +```py +from ty_extensions import is_subtype_of, static_assert + +class A1: ... +class B1(A1): ... +class A2: ... +class B2(A2): ... +class Unrelated: ... + +static_assert(is_subtype_of(B1, A1)) +static_assert(is_subtype_of(B2, A2)) + +# Zero-element tuples +static_assert(is_subtype_of(tuple[()], tuple[()])) +static_assert(not is_subtype_of(tuple[()], tuple[Unrelated])) + +# One-element tuples +static_assert(is_subtype_of(tuple[B1], tuple[A1])) +static_assert(not is_subtype_of(tuple[B1], tuple[Unrelated])) +static_assert(not is_subtype_of(tuple[B1], tuple[()])) +static_assert(not is_subtype_of(tuple[B1], tuple[A1, Unrelated])) + +# Two-element tuples +static_assert(is_subtype_of(tuple[B1, B2], tuple[A1, A2])) +static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, A2])) +static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, Unrelated])) +static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, Unrelated])) +static_assert(not is_subtype_of(tuple[B1, B2], tuple[()])) +static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1])) +static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, A2, Unrelated])) +static_assert(is_subtype_of(tuple[int], tuple[object, ...])) +``` + +## Subtyping of heterogeneous tuple types and homogeneous tuple types + +While a homogeneous tuple type is not a subtype of any heterogeneous tuple types, a heterogeneous +tuple type can be a subtype of a homogeneous tuple type, and homogeneous tuple types can be subtypes +of `Sequence`: + +```py +from typing import Literal, Any, Sequence +from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy + +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1, 2], ...])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[Literal[2], ...]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[*tuple[Literal[1], ...], Literal[2]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], *tuple[str, ...], Literal[2]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[2], *tuple[str, ...]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[*tuple[str, ...], Literal[1], Literal[2]])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int, ...])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[int | str, ...])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Not[AlwaysFalsy], ...])) +static_assert(is_subtype_of(tuple[Literal[1], Literal[2]], Sequence[int])) +static_assert(is_subtype_of(tuple[int, ...], Sequence[int])) + +static_assert(is_subtype_of(tuple[()], tuple[Literal[1, 2], ...])) +static_assert(is_subtype_of(tuple[()], tuple[int, ...])) +static_assert(is_subtype_of(tuple[()], tuple[int | str, ...])) +static_assert(is_subtype_of(tuple[()], tuple[Not[AlwaysFalsy], ...])) +static_assert(is_subtype_of(tuple[()], Sequence[int])) + +static_assert(not is_subtype_of(tuple[Literal[1], Literal[2]], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, int], tuple[str, ...])) +static_assert(not is_subtype_of(tuple[int, ...], Sequence[Any])) +static_assert(not is_subtype_of(tuple[Any, ...], Sequence[int])) +``` + +## Subtyping of two mixed tuple types + +```py +from typing import Literal, Any, Sequence +from ty_extensions import static_assert, is_subtype_of, Not, AlwaysFalsy + +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...]], + ) +) + +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], *tuple[int, ...]], + ) +) + +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...], Literal[10]], + ) +) +static_assert( + is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + tuple[*tuple[int, ...]], + ) +) + +static_assert( + not is_subtype_of( + tuple[Literal["foo"], *tuple[int, ...]], + tuple[int, ...], + ) +) +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...], Literal["foo"]], + tuple[int, ...], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], Literal[2], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_subtype_of( + tuple[Literal[1], *tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], *tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[Literal[1], *tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) + +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...], Literal[9], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...], Literal[10]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +static_assert( + not is_subtype_of( + tuple[*tuple[int, ...]], + tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[9], Literal[10]], + ) +) +``` + +## Subtyping of the gradual tuple + +```toml +[environment] +python-version = "3.12" +``` + +As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, not +only in the type of its elements, but also in its length. + +Its subtyping follows the general rule for subtyping of gradual types. + +```py +from typing import Any, Never +from ty_extensions import static_assert, is_subtype_of + +static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[int])) +static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, int])) +static_assert(is_subtype_of(tuple[Any, ...], tuple[object, ...])) +static_assert(is_subtype_of(tuple[Never, ...], tuple[Any, ...])) +``` + +Same applies when `tuple[Any, ...]` is unpacked into a mixed tuple. + +```py +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[int, ...]])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[*tuple[int, ...], int])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[*tuple[Any, ...], int], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[Any, Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, int])) +``` + +Unbounded homogeneous tuples of a non-Any type are defined to be the _union_ of all tuple lengths, +not the _gradual choice_ of them, so no variable-length tuples are a subtype of _any_ fixed-length +tuple. + +```py +static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[int, ...], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[int])) +static_assert(not is_subtype_of(tuple[int, ...], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, *tuple[Any, ...]])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, *tuple[int, ...]])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...]], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[*tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[*tuple[int, ...], int], tuple[*tuple[int, ...], int])) +static_assert(is_subtype_of(tuple[*tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[*tuple[int, ...], int], tuple[int, int])) + +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[Any, ...], int])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[Any, Any])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, *tuple[int, ...], int])) +static_assert(is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, ...])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int])) +static_assert(not is_subtype_of(tuple[int, *tuple[int, ...], int], tuple[int, int])) +``` + +## Union types + +```py +from ty_extensions import is_subtype_of, static_assert +from typing import Literal + +class A: ... +class B1(A): ... +class B2(A): ... +class Unrelated1: ... +class Unrelated2: ... + +static_assert(is_subtype_of(B1, A)) +static_assert(is_subtype_of(B2, A)) + +# Union on the right hand side +static_assert(is_subtype_of(B1, A | Unrelated1)) +static_assert(is_subtype_of(B1, Unrelated1 | A)) + +static_assert(not is_subtype_of(B1, Unrelated1 | Unrelated2)) + +# Union on the left hand side +static_assert(is_subtype_of(B1 | B2, A)) +static_assert(is_subtype_of(B1 | B2 | A, object)) + +static_assert(not is_subtype_of(B1 | Unrelated1, A)) +static_assert(not is_subtype_of(Unrelated1 | B1, A)) + +# Union on both sides +static_assert(is_subtype_of(B1 | bool, A | int)) +static_assert(is_subtype_of(B1 | bool, int | A)) + +static_assert(not is_subtype_of(B1 | bool, Unrelated1 | int)) +static_assert(not is_subtype_of(B1 | bool, int | Unrelated1)) + +# Example: Unions of literals +static_assert(is_subtype_of(Literal[1, 2, 3], int)) +static_assert(not is_subtype_of(Literal[1, "two", 3], int)) +``` + +## Intersection types + +```py +from typing_extensions import Literal, LiteralString +from ty_extensions import Intersection, Not, is_subtype_of, static_assert + +class A: ... +class B1(A): ... +class B2(A): ... +class C(B1, B2): ... +class Unrelated: ... + +static_assert(is_subtype_of(B1, A)) +static_assert(is_subtype_of(B2, A)) +static_assert(is_subtype_of(C, A)) +static_assert(is_subtype_of(C, B1)) +static_assert(is_subtype_of(C, B2)) + +# For complements, the subtyping relation is reversed: +static_assert(is_subtype_of(Not[A], Not[B1])) +static_assert(is_subtype_of(Not[A], Not[B2])) +static_assert(is_subtype_of(Not[A], Not[C])) +static_assert(is_subtype_of(Not[B1], Not[C])) +static_assert(is_subtype_of(Not[B2], Not[C])) + +# The intersection of two types is a subtype of both: +static_assert(is_subtype_of(Intersection[B1, B2], B1)) +static_assert(is_subtype_of(Intersection[B1, B2], B2)) +# … and of their common supertype: +static_assert(is_subtype_of(Intersection[B1, B2], A)) + +# A common subtype of two types is a subtype of their intersection: +static_assert(is_subtype_of(C, Intersection[B1, B2])) +# … but not the other way around: +static_assert(not is_subtype_of(Intersection[B1, B2], C)) + +# "Removing" B1 from A leaves a subtype of A. +static_assert(is_subtype_of(Intersection[A, Not[B1]], A)) +static_assert(is_subtype_of(Intersection[A, Not[B1]], Not[B1])) + +# B1 and B2 are not disjoint, so this is not true: +static_assert(not is_subtype_of(B2, Intersection[A, Not[B1]])) +# … but for two disjoint subtypes, it is: +static_assert(is_subtype_of(Literal[2], Intersection[int, Not[Literal[1]]])) + +# A and Unrelated are not related, so this is not true: +static_assert(not is_subtype_of(Intersection[A, Not[B1]], Not[Unrelated])) +# … but for a disjoint type like `None`, it is: +static_assert(is_subtype_of(Intersection[A, Not[B1]], Not[None])) + +# Complements of types are still subtypes of `object`: +static_assert(is_subtype_of(Not[A], object)) + +# More examples: +static_assert(is_subtype_of(type[str], Not[None])) +static_assert(is_subtype_of(Not[LiteralString], object)) + +static_assert(not is_subtype_of(Intersection[int, Not[Literal[2]]], Intersection[int, Not[Literal[3]]])) +static_assert(not is_subtype_of(Not[Literal[2]], Not[Literal[3]])) +static_assert(not is_subtype_of(Not[Literal[2]], Not[int])) +static_assert(not is_subtype_of(int, Not[Literal[3]])) +static_assert(not is_subtype_of(Literal[1], Intersection[int, Not[Literal[1]]])) +``` + +## Special types + +### `Never` + +`Never` is a subtype of all types. + +```py +from typing_extensions import Literal, Never +from ty_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert + +static_assert(is_subtype_of(Never, Never)) +static_assert(is_subtype_of(Never, Literal[True])) +static_assert(is_subtype_of(Never, bool)) +static_assert(is_subtype_of(Never, int)) +static_assert(is_subtype_of(Never, object)) + +static_assert(is_subtype_of(Never, AlwaysTruthy)) +static_assert(is_subtype_of(Never, AlwaysFalsy)) +``` + +### `AlwaysTruthy` and `AlwaysFalsy` + +```py +from ty_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert +from typing_extensions import Literal, LiteralString + +static_assert(is_subtype_of(Literal[1], AlwaysTruthy)) +static_assert(is_subtype_of(Literal[0], AlwaysFalsy)) + +static_assert(is_subtype_of(AlwaysTruthy, object)) +static_assert(is_subtype_of(AlwaysFalsy, object)) + +static_assert(not is_subtype_of(Literal[1], AlwaysFalsy)) +static_assert(not is_subtype_of(Literal[0], AlwaysTruthy)) + +static_assert(not is_subtype_of(str, AlwaysTruthy)) +static_assert(not is_subtype_of(str, AlwaysFalsy)) + +# TODO: No errors +# error: [static-assert-error] +static_assert(is_subtype_of(bool, Literal[False] | AlwaysTruthy)) +# error: [static-assert-error] +static_assert(is_subtype_of(bool, Literal[True] | AlwaysFalsy)) +# error: [static-assert-error] +static_assert(is_subtype_of(LiteralString, Literal[""] | AlwaysTruthy)) +static_assert(not is_subtype_of(Literal[True] | AlwaysFalsy, Literal[False] | AlwaysTruthy)) + +# TODO: No errors +# The condition `is_subtype_of(T & U, U)` must still be satisfied after the following transformations: +# `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` +# error: [static-assert-error] +static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], AlwaysTruthy)) +# error: [static-assert-error] +static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], AlwaysTruthy)) +# `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` +# error: [static-assert-error] +static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy])) +# error: [static-assert-error] +static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy])) +``` + +### `TypeGuard` and `TypeIs` + +Fully-static `TypeGuard[...]` and `TypeIs[...]` are subtypes of `bool`. + +```py +from ty_extensions import is_subtype_of, static_assert +from typing_extensions import TypeGuard, TypeIs + +# TODO: TypeGuard +# static_assert(is_subtype_of(TypeGuard[int], bool)) +# static_assert(is_subtype_of(TypeGuard[int], int)) +static_assert(is_subtype_of(TypeIs[str], bool)) +static_assert(is_subtype_of(TypeIs[str], int)) +``` + +`TypeIs` is invariant. `TypeGuard` is covariant. + +```py +from ty_extensions import is_equivalent_to, is_subtype_of, static_assert +from typing_extensions import TypeGuard, TypeIs + +# TODO: TypeGuard +# static_assert(is_subtype_of(TypeGuard[int], TypeGuard[int])) +# static_assert(is_subtype_of(TypeGuard[bool], TypeGuard[int])) +static_assert(is_subtype_of(TypeIs[int], TypeIs[int])) +static_assert(is_subtype_of(TypeIs[int], TypeIs[int])) + +static_assert(not is_subtype_of(TypeGuard[int], TypeGuard[bool])) +static_assert(not is_subtype_of(TypeIs[bool], TypeIs[int])) +static_assert(not is_subtype_of(TypeIs[int], TypeIs[bool])) +``` + +### Module literals + +```py +from types import ModuleType +from ty_extensions import TypeOf, is_subtype_of, static_assert +from typing_extensions import assert_type +import typing + +assert_type(typing, TypeOf[typing]) + +static_assert(is_subtype_of(TypeOf[typing], ModuleType)) +``` + +### Slice literals + +The type of a slice literal is currently inferred as a specialization of `slice`. + +```py +from ty_extensions import TypeOf, is_subtype_of, static_assert + +# slice's default specialization is slice[Any, Any, Any], which does not participate in subtyping. +static_assert(not is_subtype_of(TypeOf[1:2:3], slice)) +static_assert(is_subtype_of(TypeOf[1:2:3], slice[int])) +``` + +### Special forms + +```py +from typing import _SpecialForm, Literal +from ty_extensions import TypeOf, is_subtype_of, static_assert + +static_assert(is_subtype_of(TypeOf[Literal], _SpecialForm)) +static_assert(is_subtype_of(TypeOf[Literal], object)) + +static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal])) +``` + +## Class literal types and `type[…]` + +### Basic + +```py +from typing import _SpecialForm, Any +from typing_extensions import Literal, assert_type +from ty_extensions import TypeOf, is_subtype_of, static_assert + +class Meta(type): ... +class HasCustomMetaclass(metaclass=Meta): ... + +type LiteralBool = TypeOf[bool] +type LiteralInt = TypeOf[int] +type LiteralStr = TypeOf[str] +type LiteralObject = TypeOf[object] + +assert_type(bool, LiteralBool) +assert_type(int, LiteralInt) +assert_type(str, LiteralStr) +assert_type(object, LiteralObject) + +# bool + +static_assert(is_subtype_of(LiteralBool, LiteralBool)) +static_assert(is_subtype_of(LiteralBool, type[bool])) +static_assert(is_subtype_of(LiteralBool, type[int])) +static_assert(is_subtype_of(LiteralBool, type[object])) +static_assert(is_subtype_of(LiteralBool, type)) +static_assert(is_subtype_of(LiteralBool, object)) + +static_assert(not is_subtype_of(LiteralBool, LiteralInt)) +static_assert(not is_subtype_of(LiteralBool, LiteralObject)) +static_assert(not is_subtype_of(LiteralBool, bool)) + +static_assert(not is_subtype_of(type, type[bool])) + +static_assert(not is_subtype_of(LiteralBool, type[Any])) + +# int + +static_assert(is_subtype_of(LiteralInt, LiteralInt)) +static_assert(is_subtype_of(LiteralInt, type[int])) +static_assert(is_subtype_of(LiteralInt, type[object])) +static_assert(is_subtype_of(LiteralInt, type)) +static_assert(is_subtype_of(LiteralInt, object)) + +static_assert(not is_subtype_of(LiteralInt, LiteralObject)) +static_assert(not is_subtype_of(LiteralInt, int)) + +static_assert(not is_subtype_of(type, type[int])) + +static_assert(not is_subtype_of(LiteralInt, type[Any])) + +# str + +static_assert(is_subtype_of(LiteralStr, type[str])) +static_assert(is_subtype_of(LiteralStr, type)) +static_assert(is_subtype_of(LiteralStr, type[object])) + +static_assert(not is_subtype_of(type[str], LiteralStr)) + +static_assert(not is_subtype_of(LiteralStr, type[Any])) + +# custom metaclasses + +type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass] + +static_assert(is_subtype_of(LiteralHasCustomMetaclass, Meta)) +static_assert(is_subtype_of(Meta, type[object])) +static_assert(is_subtype_of(Meta, type)) + +static_assert(not is_subtype_of(Meta, type[type])) + +static_assert(not is_subtype_of(Meta, type[Any])) + +# generics + +type LiteralListOfInt = TypeOf[list[int]] + +assert_type(list[int], LiteralListOfInt) + +static_assert(is_subtype_of(LiteralListOfInt, type)) + +static_assert(not is_subtype_of(LiteralListOfInt, type[Any])) +``` + +### Unions of class literals + +```py +from typing_extensions import assert_type +from ty_extensions import TypeOf, is_subtype_of, static_assert + +class Base: ... +class Derived(Base): ... +class Unrelated: ... + +type LiteralBase = TypeOf[Base] +type LiteralDerived = TypeOf[Derived] +type LiteralUnrelated = TypeOf[Unrelated] + +assert_type(Base, LiteralBase) +assert_type(Derived, LiteralDerived) +assert_type(Unrelated, LiteralUnrelated) + +static_assert(is_subtype_of(LiteralBase, type)) +static_assert(is_subtype_of(LiteralBase, object)) + +static_assert(is_subtype_of(LiteralBase, type[Base])) +static_assert(is_subtype_of(LiteralDerived, type[Base])) +static_assert(is_subtype_of(LiteralDerived, type[Derived])) + +static_assert(not is_subtype_of(LiteralBase, type[Derived])) +static_assert(is_subtype_of(type[Derived], type[Base])) + +static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, type)) +static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object)) +``` + +## Non-fully-static types + +A non-fully-static type can be considered a subtype of another type if all possible materializations +of the first type represent sets of values that are a subset of every possible set of values +represented by a materialization of the second type. + +```py +from ty_extensions import Unknown, is_subtype_of, static_assert, Intersection +from typing_extensions import Any + +static_assert(not is_subtype_of(Any, Any)) +static_assert(not is_subtype_of(Any, int)) +static_assert(not is_subtype_of(int, Any)) +static_assert(is_subtype_of(Any, object)) +static_assert(not is_subtype_of(object, Any)) + +static_assert(is_subtype_of(int, Any | int)) +static_assert(is_subtype_of(Intersection[Any, int], int)) +static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any])) +``` + +The same for `Unknown`: + +```py +static_assert(not is_subtype_of(Unknown, Unknown)) +static_assert(not is_subtype_of(Unknown, int)) +static_assert(not is_subtype_of(int, Unknown)) +static_assert(is_subtype_of(Unknown, object)) +static_assert(not is_subtype_of(object, Unknown)) + +static_assert(is_subtype_of(int, Unknown | int)) +static_assert(is_subtype_of(Intersection[Unknown, int], int)) +static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown])) +``` + +Instances of classes that inherit `Any` are not subtypes of some other `Arbitrary` class, because +the `Any` they inherit from could materialize to something (e.g. `object`) that is not a subclass of +that class. + +Similarly, they are not subtypes of `Any`, because there are possible materializations of `Any` that +would not satisfy the subtype relation. + +They are subtypes of `object`. + +```py +class InheritsAny(Any): + pass + +class Arbitrary: + pass + +static_assert(not is_subtype_of(InheritsAny, Arbitrary)) +static_assert(not is_subtype_of(InheritsAny, Any)) +static_assert(is_subtype_of(InheritsAny, object)) +``` + +Similar for subclass-of types: + +```py +static_assert(not is_subtype_of(type[Any], type[Any])) +static_assert(not is_subtype_of(type[object], type[Any])) +static_assert(not is_subtype_of(type[Any], type[Arbitrary])) +static_assert(is_subtype_of(type[Any], type[object])) +``` + +## Callable + +The general principle is that a callable type is a subtype of another if it's more flexible in what +it accepts and more specific in what it returns. + +References: + +- +- + +### Return type + +Return types are covariant. + +```py +from typing import Callable +from ty_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 ty_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. + +#### Positional-only + +```py +from typing import Callable +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf + +def float_param(a: float, /) -> None: ... +def int_param(a: int, /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param])) +static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[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: + +```py +def int_param_different_name(b: int, /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_param_different_name])) +static_assert(is_subtype_of(CallableTypeOf[int_param_different_name], CallableTypeOf[int_param])) +``` + +Multiple positional-only parameters are checked in order: + +```py +def multi_param1(a: float, b: int, c: str, /) -> None: ... +def multi_param2(b: int, c: bool, a: str, /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2])) +static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[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 + +If the parameter has a default value, it's treated as optional. This means that the parameter at the +corresponding position in the supertype does not need to have a default value. + +```py +from typing import Callable +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf + +def float_with_default(a: float = 1, /) -> None: ... +def int_with_default(a: int = 1, /) -> None: ... +def int_without_default(a: int, /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) +static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) + +static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_without_default])) +static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[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: + +```py +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) +``` + +The subtype can include any number of positional-only parameters as long as they have the default +value: + +```py +def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[multi_param], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param])) +``` + +#### Positional-only with other kinds + +If a parameter is declared as positional-only, then the corresponding parameter in the supertype +cannot be any other parameter kind. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def positional_only(a: int, /) -> None: ... +def standard(a: int) -> None: ... +def keyword_only(*, a: int) -> None: ... +def variadic(*a: int) -> None: ... +def keyword_variadic(**a: int) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[standard])) +static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_only])) +static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[variadic])) +static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_variadic])) +``` + +#### Standard + +A standard parameter is either a positional or a keyword parameter. + +Unlike positional-only parameters, standard parameters should have the same name in the subtype. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def int_param_a(a: int) -> None: ... +def int_param_b(b: int) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[int_param_a], CallableTypeOf[int_param_b])) +static_assert(not is_subtype_of(CallableTypeOf[int_param_b], CallableTypeOf[int_param_a])) +``` + +Apart from the name, it behaves the same as positional-only parameters. + +```py +def float_param(a: float) -> None: ... +def int_param(a: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param])) +static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param])) +``` + +With the same rules for default values as well. + +```py +def float_with_default(a: float = 1) -> None: ... +def int_with_default(a: int = 1) -> None: ... +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) +static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) + +static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_param])) +static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_with_default])) + +static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) +``` + +Multiple standard parameters are checked in order along with their names: + +```py +def multi_param1(a: float, b: int, c: str) -> None: ... +def multi_param2(a: int, b: bool, c: str) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2])) +static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1])) +``` + +The subtype can include as many standard parameters as long as they have the default value: + +```py +def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[multi_param_default], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param_default])) +``` + +#### Standard with keyword-only + +A keyword-only parameter in the supertype can be substituted with the corresponding standard +parameter in the subtype with the same name. This is because a standard parameter is more flexible +than a keyword-only parameter. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def standard_a(a: int) -> None: ... +def keyword_b(*, b: int) -> None: ... + +# The name of the parameters are different +static_assert(not is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[keyword_b])) + +def standard_float(a: float) -> None: ... +def keyword_int(*, a: int) -> None: ... + +# Here, the name of the parameters are the same +static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[keyword_int])) + +def standard_with_default(a: int = 1) -> None: ... +def keyword_with_default(*, a: int = 1) -> None: ... +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keyword_with_default])) +static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty])) +``` + +The position of the keyword-only parameters does not matter: + +```py +def multi_standard(a: float, b: int, c: str) -> None: ... +def multi_keyword(*, b: bool, c: str, a: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_keyword])) +``` + +#### Standard with positional-only + +A positional-only parameter in the supertype can be substituted with the corresponding standard +parameter in the subtype at the same position. This is because a standard parameter is more flexible +than a positional-only parameter. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def standard_a(a: int) -> None: ... +def positional_b(b: int, /) -> None: ... + +# The names are not important in this context +static_assert(is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[positional_b])) + +def standard_float(a: float) -> None: ... +def positional_int(a: int, /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[positional_int])) + +def standard_with_default(a: int = 1) -> None: ... +def positional_with_default(a: int = 1, /) -> None: ... +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[positional_with_default])) +static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty])) +``` + +The position of the positional-only parameters matter: + +```py +def multi_standard(a: float, b: int, c: str) -> None: ... +def multi_positional1(b: int, c: bool, a: str, /) -> None: ... + +# Here, the type of the parameter `a` makes the subtype relation invalid +def multi_positional2(b: int, a: float, c: str, /) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional1])) +static_assert(not is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional2])) +``` + +#### Standard with variadic + +A variadic or keyword-variadic parameter in the supertype cannot be substituted with a standard +parameter in the subtype. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def standard(a: int) -> None: ... +def variadic(*a: int) -> None: ... +def keyword_variadic(**a: int) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[variadic])) +static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword_variadic])) +``` + +#### Variadic + +The name of the variadic parameter does not need to be the same in the subtype. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def variadic_float(*args2: float) -> None: ... +def variadic_int(*args1: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[variadic_float], CallableTypeOf[variadic_int])) +static_assert(not is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[variadic_float])) +``` + +The variadic parameter does not need to be present in the supertype: + +```py +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[variadic_int])) +``` + +#### Variadic with positional-only + +If the subtype has a variadic parameter then any unmatched positional-only parameter from the +supertype should be checked against the variadic parameter. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def variadic(a: int, /, *args: float) -> None: ... + +# Here, the parameter `b` and `c` are unmatched +def positional_only(a: int, b: float, c: int, /) -> None: ... + +# Here, the parameter `b` is unmatched and there's also a variadic parameter +def positional_variadic(a: int, b: float, /, *args: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_only])) +static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_variadic])) +``` + +#### Variadic with other kinds + +Variadic parameter in a subtype can only be used to match against an unmatched positional-only +parameters from the supertype, not any other parameter kind. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def variadic(*args: int) -> None: ... + +# Both positional-only parameters are unmatched so uses the variadic parameter but the other +# parameter `c` remains and cannot be matched. +def standard(a: int, b: float, /, c: int) -> None: ... + +# Similarly, for other kinds +def keyword_only(a: int, /, *, b: int) -> None: ... +def keyword_variadic(a: int, /, **kwargs: int) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[standard])) +static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_only])) +static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic])) +``` + +But, there are special cases when matching against standard parameters. This is due to the fact that +a standard parameter can be passed as a positional or keyword parameter. This means that the +subtyping relation needs to consider both cases. + +```py +def variadic_keyword(*args: int, **kwargs: int) -> None: ... +def standard_int(a: int) -> None: ... +def standard_float(a: float) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int])) +static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float])) +``` + +If the type of either the variadic or keyword-variadic parameter is not a supertype of the standard +parameter, then the subtyping relation is invalid. + +```py +def variadic_bool(*args: bool, **kwargs: int) -> None: ... +def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int])) +static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int])) +``` + +The standard parameter can follow a variadic parameter in the subtype. + +```py +def standard_variadic_int(a: int, *args: int) -> None: ... +def standard_variadic_float(a: int, *args: float) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int])) +static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float])) +``` + +The keyword part of the standard parameter can be matched against keyword-only parameter with the +same name if the keyword-variadic parameter is absent. + +```py +def variadic_a(*args: int, a: int) -> None: ... +def variadic_b(*args: int, b: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int])) +# The parameter name is different +static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int])) +``` + +#### Keyword-only + +For keyword-only parameters, the name should be the same: + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def keyword_int(*, a: int) -> None: ... +def keyword_float(*, a: float) -> None: ... +def keyword_b(*, b: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[keyword_float], CallableTypeOf[keyword_int])) +static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_float])) +static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_b])) +``` + +But, the order of the keyword-only parameters is not required to be the same: + +```py +def keyword_ab(*, a: float, b: float) -> None: ... +def keyword_ba(*, b: int, a: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[keyword_ab], CallableTypeOf[keyword_ba])) +static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keyword_ab])) +``` + +#### Keyword-only with default + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def float_with_default(*, a: float = 1) -> None: ... +def int_with_default(*, a: int = 1) -> None: ... +def int_keyword(*, a: int) -> None: ... +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default])) +static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default])) + +static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_keyword])) +static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[int_with_default])) + +static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default])) +``` + +Keyword-only parameters with default values can be mixed with the ones without default values in any +order: + +```py +# A keyword-only parameter with a default value follows the one without a default value (it's valid) +def mixed(*, b: int = 1, a: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[int_keyword])) +static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixed])) +``` + +#### Keyword-only with standard + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def keywords1(*, a: int, b: int) -> None: ... +def standard(b: float, a: float) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[keywords1], CallableTypeOf[standard])) +static_assert(is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keywords1])) +``` + +The subtype can include additional standard parameters as long as it has the default value: + +```py +def standard_with_default(b: float, a: float, c: float = 1) -> None: ... +def standard_without_default(b: float, a: float, c: float) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[standard_without_default], CallableTypeOf[keywords1])) +static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keywords1])) +``` + +Here, we mix keyword-only parameters with standard parameters: + +```py +def keywords2(*, a: int, c: int, b: int) -> None: ... +def mixed(b: float, a: float, *, c: float) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[keywords2], CallableTypeOf[mixed])) +static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[keywords2])) +``` + +But, we shouldn't consider any unmatched positional-only parameters: + +```py +def mixed_positional(b: float, /, a: float, *, c: float) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[mixed_positional], CallableTypeOf[keywords2])) +``` + +But, an unmatched variadic parameter is still valid: + +```py +def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywords2])) +``` + +#### Keyword-variadic + +The name of the keyword-variadic parameter does not need to be the same in the subtype. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def kwargs_float(**kwargs2: float) -> None: ... +def kwargs_int(**kwargs1: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[kwargs_float], CallableTypeOf[kwargs_int])) +static_assert(not is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[kwargs_float])) +``` + +A variadic parameter can be omitted in the subtype: + +```py +def empty() -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[kwargs_int])) +``` + +#### Keyword-variadic with keyword-only + +If the subtype has a keyword-variadic parameter then any unmatched keyword-only parameter from the +supertype should be checked against the keyword-variadic parameter. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def kwargs(**kwargs: float) -> None: ... +def keyword_only(*, a: int, b: float, c: bool) -> None: ... +def keyword_variadic(*, a: int, **kwargs: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_only])) +static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_variadic])) +``` + +This is valid only for keyword-only parameters, not any other parameter kind: + +```py +def mixed1(a: int, *, b: int) -> None: ... + +# Same as above but with the default value +def mixed2(a: int = 1, *, b: int) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed1])) +static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed2])) +``` + +#### Empty + +When the supertype has an empty list of parameters, then the subtype can have any kind of parameters +as long as they contain the default values for non-variadic parameters. + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def empty() -> None: ... +def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[empty])) +static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed])) +``` + +#### Object + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf +from typing import Callable + +def f1(a: int, b: str, /, *c: float, d: int = 1, **e: float) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[f1], object)) +static_assert(not is_subtype_of(object, CallableTypeOf[f1])) + +def _( + f3: Callable[[int, str], None], +) -> None: + static_assert(is_subtype_of(TypeOf[f3], object)) + static_assert(not is_subtype_of(object, TypeOf[f3])) + +class C: + def foo(self) -> None: ... + +static_assert(is_subtype_of(TypeOf[C.foo], object)) +static_assert(not is_subtype_of(object, TypeOf[C.foo])) +``` + +#### Gradual form + +A callable type with `...` parameters can be considered a supertype of a callable type that accepts +any arguments of any type, but otherwise is not a subtype or supertype of any callable type. + +```py +from typing import Callable, Never +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def bottom(*args: object, **kwargs: object) -> Never: + raise Exception() + +type BottomCallable = CallableTypeOf[bottom] + +static_assert(is_subtype_of(BottomCallable, Callable[..., Never])) +static_assert(is_subtype_of(BottomCallable, Callable[..., int])) + +static_assert(not is_subtype_of(Callable[[], object], Callable[..., object])) +static_assert(not is_subtype_of(Callable[..., object], Callable[[], object])) +``` + +According to the spec, `*args: Any, **kwargs: Any` is equivalent to `...`. This is a subtle but +important distinction. No materialization of the former signature (if taken literally) can have any +required arguments, but `...` can materialize to a signature with required arguments. The below test +would not pass if we didn't handle this special case. + +```py +from typing import Callable, Any +from ty_extensions import is_subtype_of, static_assert, CallableTypeOf + +def f(*args: Any, **kwargs: Any) -> Any: ... + +static_assert(not is_subtype_of(CallableTypeOf[f], Callable[[], object])) +``` + +### Classes with `__call__` + +```py +from typing import Callable, Any +from ty_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to + +class A: + def __call__(self, a: int) -> int: + return a + +a = A() + +static_assert(is_subtype_of(A, Callable[[int], int])) +static_assert(not is_subtype_of(A, Callable[[], int])) +static_assert(not is_subtype_of(Callable[[int], int], A)) +static_assert(not is_subtype_of(A, Callable[[Any], int])) +static_assert(not is_subtype_of(A, Callable[[int], Any])) + +def f(fn: Callable[[int], int]) -> None: ... + +f(a) +``` + +### Classes with `__call__` as attribute + +An instance type can be a subtype of a compatible callable type if the instance type's class has a +callable `__call__` attribute. + +TODO: for the moment, we don't consider the callable type as a bound-method descriptor, but this may +change for better compatibility with mypy/pyright. + +```py +from typing import Callable +from ty_extensions import static_assert, is_subtype_of + +def call_impl(a: int) -> str: + return "" + +class A: + __call__: Callable[[int], str] = call_impl + +static_assert(is_subtype_of(A, Callable[[int], str])) +static_assert(not is_subtype_of(A, Callable[[int], int])) +reveal_type(A()(1)) # revealed: str +``` + +### Class literals + +#### Classes with metaclasses + +```py +from typing import Callable, overload +from typing_extensions import Self +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class MetaWithReturn(type): + def __call__(cls) -> "A": + return super().__call__() + +class A(metaclass=MetaWithReturn): ... + +static_assert(is_subtype_of(TypeOf[A], Callable[[], A])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[object], A])) + +class MetaWithDifferentReturn(type): + def __call__(cls) -> int: + return super().__call__() + +class B(metaclass=MetaWithDifferentReturn): ... + +static_assert(is_subtype_of(TypeOf[B], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[B], Callable[[], B])) + +class MetaWithOverloadReturn(type): + @overload + def __call__(cls, x: int) -> int: ... + @overload + def __call__(cls) -> str: ... + def __call__(cls, x: int | None = None) -> str | int: + return super().__call__() + +class C(metaclass=MetaWithOverloadReturn): ... + +static_assert(is_subtype_of(TypeOf[C], Callable[[int], int])) +static_assert(is_subtype_of(TypeOf[C], Callable[[], str])) +``` + +#### Classes with `__new__` + +```py +from typing import Callable, overload +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def __new__(cls, a: int) -> int: + return a + +static_assert(is_subtype_of(TypeOf[A], Callable[[int], int])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[], int])) + +class B: ... +class C(B): ... + +class D: + def __new__(cls) -> B: + return B() + +class E(D): + def __new__(cls) -> C: + return C() + +static_assert(is_subtype_of(TypeOf[E], Callable[[], C])) +static_assert(is_subtype_of(TypeOf[E], Callable[[], B])) +static_assert(not is_subtype_of(TypeOf[D], Callable[[], C])) +static_assert(is_subtype_of(TypeOf[D], Callable[[], B])) + +class F: + @overload + def __new__(cls) -> int: ... + @overload + def __new__(cls, x: int) -> "F": ... + def __new__(cls, x: int | None = None) -> "int | F": + return 1 if x is None else object.__new__(cls) + + def __init__(self, y: str) -> None: ... + +static_assert(is_subtype_of(TypeOf[F], Callable[[int], F])) +static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[F], Callable[[str], F])) +``` + +#### Classes with `__call__` and `__new__` + +If `__call__` and `__new__` are both present, `__call__` takes precedence. + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class MetaWithIntReturn(type): + def __call__(cls) -> int: + return super().__call__() + +class F(metaclass=MetaWithIntReturn): + def __new__(cls) -> str: + return super().__new__(cls) + +static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) +``` + +#### Classes with `__init__` + +```py +from typing import Callable, overload +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def __init__(self, a: int) -> None: ... + +static_assert(is_subtype_of(TypeOf[A], Callable[[int], A])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[], A])) + +class B: + @overload + def __init__(self, a: int) -> None: ... + @overload + def __init__(self) -> None: ... + def __init__(self, a: int | None = None) -> None: ... + +static_assert(is_subtype_of(TypeOf[B], Callable[[int], B])) +static_assert(is_subtype_of(TypeOf[B], Callable[[], B])) + +class C: ... + +# TODO: This assertion should be true once we understand `Self` +# error: [static-assert-error] "Static assertion error: argument evaluates to `False`" +static_assert(is_subtype_of(TypeOf[C], Callable[[], C])) + +class D[T]: + def __init__(self, x: T) -> None: ... + +static_assert(is_subtype_of(TypeOf[D[int]], Callable[[int], D[int]])) +static_assert(not is_subtype_of(TypeOf[D[int]], Callable[[str], D[int]])) +``` + +#### Classes with `__init__` and `__new__` + +```py +from typing import Callable, overload, Self +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def __new__(cls, a: int) -> Self: + return super().__new__(cls) + + def __init__(self, a: int) -> None: ... + +static_assert(is_subtype_of(TypeOf[A], Callable[[int], A])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[], A])) + +class B: + def __new__(cls, a: int) -> int: + return super().__new__(cls) + + def __init__(self, a: str) -> None: ... + +static_assert(is_subtype_of(TypeOf[B], Callable[[int], int])) +static_assert(not is_subtype_of(TypeOf[B], Callable[[str], B])) + +class C: + def __new__(cls, *args, **kwargs) -> "C": + return super().__new__(cls) + + def __init__(self, x: int) -> None: ... + +# Not subtype because __new__ signature is not fully static +static_assert(not is_subtype_of(TypeOf[C], Callable[[int], C])) +static_assert(not is_subtype_of(TypeOf[C], Callable[[], C])) + +class D: ... + +class E: + @overload + def __new__(cls) -> int: ... + @overload + def __new__(cls, x: int) -> D: ... + def __new__(cls, x: int | None = None) -> int | D: + return D() + + def __init__(self, y: str) -> None: ... + +static_assert(is_subtype_of(TypeOf[E], Callable[[int], D])) +static_assert(is_subtype_of(TypeOf[E], Callable[[], int])) + +class F[T]: + def __new__(cls, x: T) -> "F[T]": + return super().__new__(cls) + + def __init__(self, x: T) -> None: ... + +static_assert(is_subtype_of(TypeOf[F[int]], Callable[[int], F[int]])) +static_assert(not is_subtype_of(TypeOf[F[int]], Callable[[str], F[int]])) +``` + +#### Classes with `__call__`, `__new__` and `__init__` + +If `__call__`, `__new__` and `__init__` are all present, `__call__` takes precedence. + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class MetaWithIntReturn(type): + def __call__(cls) -> int: + return super().__call__() + +class F(metaclass=MetaWithIntReturn): + def __new__(cls) -> str: + return super().__new__(cls) + + def __init__(self, x: int) -> None: ... + +static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) +static_assert(not is_subtype_of(TypeOf[F], Callable[[int], F])) +``` + +### Subclass of + +#### Type of a class with constructor methods + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def __init__(self, x: int) -> None: ... + +class B: + def __new__(cls, x: str) -> "B": + return super().__new__(cls) + +static_assert(is_subtype_of(type[A], Callable[[int], A])) +static_assert(not is_subtype_of(type[A], Callable[[str], A])) + +static_assert(is_subtype_of(type[B], Callable[[str], B])) +static_assert(not is_subtype_of(type[B], Callable[[int], B])) +``` + +### Dataclasses + +Dataclasses synthesize a `__init__` method. + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of +from dataclasses import dataclass + +@dataclass +class A: + x: "A" | None + +static_assert(is_subtype_of(type[A], Callable[[A], A])) +static_assert(is_subtype_of(type[A], Callable[[None], A])) +static_assert(is_subtype_of(type[A], Callable[[A | None], A])) +static_assert(not is_subtype_of(type[A], Callable[[int], A])) +``` + +### Bound methods + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def f(self, a: int) -> int: + return a + + @classmethod + def g(cls, a: int) -> int: + return a + +a = A() + +static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], int])) +static_assert(is_subtype_of(TypeOf[a.g], Callable[[int], int])) +static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], int])) + +static_assert(not is_subtype_of(TypeOf[a.f], Callable[[float], int])) +static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int])) + +# TODO: This assertion should be true +# error: [static-assert-error] "Static assertion error: argument evaluates to `False`" +static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int])) +``` + +### Overloads + +#### Subtype overloaded + +For `B <: A`, if a callable `B` is overloaded with two or more signatures, it is a subtype of +callable `A` if _at least one_ of the overloaded signatures in `B` is a subtype of `A`. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... +class C: ... + +@overload +def overloaded(x: A) -> None: ... +@overload +def overloaded(x: B) -> None: ... +``` + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert +from overloaded import A, B, C, overloaded + +def accepts_a(x: A) -> None: ... +def accepts_b(x: B) -> None: ... +def accepts_c(x: C) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_a])) +static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_b])) +static_assert(not is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_c])) +``` + +#### Supertype overloaded + +For `B <: A`, if a callable `A` is overloaded with two or more signatures, callable `B` is a subtype +of `A` if `B` is a subtype of _all_ of the signatures in `A`. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... + +@overload +def overloaded(a: Child) -> None: ... +@overload +def overloaded(a: Parent) -> None: ... +@overload +def overloaded(a: Grandparent) -> None: ... +``` + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert +from overloaded import Grandparent, Parent, Child, overloaded + +# This is a subtype of only the first overload +def child(a: Child) -> None: ... + +# This is a subtype of the first and second overload +def parent(a: Parent) -> None: ... + +# This is the only function that's a subtype of all overloads +def grandparent(a: Grandparent) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[child], CallableTypeOf[overloaded])) +static_assert(not is_subtype_of(CallableTypeOf[parent], CallableTypeOf[overloaded])) +static_assert(is_subtype_of(CallableTypeOf[grandparent], CallableTypeOf[overloaded])) +``` + +#### Both overloads + +For `B <: A`, if both `A` and `B` is a callable that's overloaded with two or more signatures, then +`B` is a subtype of `A` if for _every_ signature in `A`, there is _at least one_ signature in `B` +that is a subtype of it. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... +class Other: ... + +@overload +def pg(a: Parent) -> None: ... +@overload +def pg(a: Grandparent) -> None: ... + +@overload +def po(a: Parent) -> None: ... +@overload +def po(a: Other) -> None: ... + +@overload +def go(a: Grandparent) -> None: ... +@overload +def go(a: Other) -> None: ... + +@overload +def cpg(a: Child) -> None: ... +@overload +def cpg(a: Parent) -> None: ... +@overload +def cpg(a: Grandparent) -> None: ... + +@overload +def empty_go() -> Child: ... +@overload +def empty_go(a: Grandparent) -> None: ... +@overload +def empty_go(a: Other) -> Other: ... + +@overload +def empty_cp() -> Parent: ... +@overload +def empty_cp(a: Child) -> None: ... +@overload +def empty_cp(a: Parent) -> None: ... +``` + +```py +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert +from overloaded import pg, po, go, cpg, empty_go, empty_cp + +static_assert(is_subtype_of(CallableTypeOf[pg], CallableTypeOf[cpg])) +static_assert(is_subtype_of(CallableTypeOf[cpg], CallableTypeOf[pg])) + +static_assert(not is_subtype_of(CallableTypeOf[po], CallableTypeOf[pg])) +static_assert(not is_subtype_of(CallableTypeOf[pg], CallableTypeOf[po])) + +static_assert(is_subtype_of(CallableTypeOf[go], CallableTypeOf[pg])) +static_assert(not is_subtype_of(CallableTypeOf[pg], CallableTypeOf[go])) + +# Overload 1 in `empty_go` is a subtype of overload 1 in `empty_cp` +# Overload 2 in `empty_go` is a subtype of overload 2 in `empty_cp` +# Overload 2 in `empty_go` is a subtype of overload 3 in `empty_cp` +# +# All overloads in `empty_cp` has a subtype in `empty_go` +static_assert(is_subtype_of(CallableTypeOf[empty_go], CallableTypeOf[empty_cp])) + +static_assert(not is_subtype_of(CallableTypeOf[empty_cp], CallableTypeOf[empty_go])) +``` + +#### Order of overloads + +Order of overloads is irrelevant for subtyping. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... + +@overload +def overload_ab(x: A) -> None: ... +@overload +def overload_ab(x: B) -> None: ... + +@overload +def overload_ba(x: B) -> None: ... +@overload +def overload_ba(x: A) -> None: ... +``` + +```py +from overloaded import overload_ab, overload_ba +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +static_assert(is_subtype_of(CallableTypeOf[overload_ab], CallableTypeOf[overload_ba])) +static_assert(is_subtype_of(CallableTypeOf[overload_ba], CallableTypeOf[overload_ab])) +``` + +[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form +[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form +[special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex +[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md new file mode 100644 index 0000000000000..ebfb45801c571 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -0,0 +1,409 @@ +# Materialization + +There are two materializations of a type: + +- The top materialization (or upper bound materialization) of a type, which is the most general form + of that type that is fully static +- The bottom materialization (or lower bound materialization) of a type, which is the most specific + form of that type that is fully static + +More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of `Any` and +`Unknown` replaced as follows: + +- In covariant position, it's replaced with `object` +- In contravariant position, it's replaced with `Never` +- In invariant position, it's replaced with an unresolved type variable + +The top materialization starts from the covariant position while the bottom materialization starts +from the contravariant position. + +TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an existential type +representing "all lists, containing any type". We currently represent this by replacing `Any` in +invariant position with an unresolved type variable. + +## Replacement rules + +### Top materialization + +The dynamic type at the top-level is replaced with `object`. + +```py +from typing import Any, Callable +from ty_extensions import Unknown, top_materialization + +reveal_type(top_materialization(Any)) # revealed: object +reveal_type(top_materialization(Unknown)) # revealed: object +``` + +The contravariant position is replaced with `Never`. + +```py +reveal_type(top_materialization(Callable[[Any], None])) # revealed: (Never, /) -> None +``` + +The invariant position is replaced with an unresolved type variable. + +```py +reveal_type(top_materialization(list[Any])) # revealed: list[T_all] +``` + +### Bottom materialization + +The dynamic type at the top-level is replaced with `Never`. + +```py +from typing import Any, Callable +from ty_extensions import Unknown, bottom_materialization + +reveal_type(bottom_materialization(Any)) # revealed: Never +reveal_type(bottom_materialization(Unknown)) # revealed: Never +``` + +The contravariant position is replaced with `object`. + +```py +# revealed: (object, object, /) -> None +reveal_type(bottom_materialization(Callable[[Any, Unknown], None])) +``` + +The invariant position is replaced in the same way as the top materialization, with an unresolved +type variable. + +```py +reveal_type(bottom_materialization(list[Any])) # revealed: list[T_all] +``` + +## Fully static types + +The top / bottom (and only) materialization of any fully static type is just itself. + +```py +from typing import Any, Literal +from ty_extensions import TypeOf, bottom_materialization, top_materialization + +reveal_type(top_materialization(int)) # revealed: int +reveal_type(bottom_materialization(int)) # revealed: int + +reveal_type(top_materialization(Literal[1])) # revealed: Literal[1] +reveal_type(bottom_materialization(Literal[1])) # revealed: Literal[1] + +reveal_type(top_materialization(Literal[True])) # revealed: Literal[True] +reveal_type(bottom_materialization(Literal[True])) # revealed: Literal[True] + +reveal_type(top_materialization(Literal["abc"])) # revealed: Literal["abc"] +reveal_type(bottom_materialization(Literal["abc"])) # revealed: Literal["abc"] + +reveal_type(top_materialization(int | str)) # revealed: int | str +reveal_type(bottom_materialization(int | str)) # revealed: int | str +``` + +We currently treat function literals as fully static types, so they remain unchanged even though the +signature might have `Any` in it. (TODO: this is probably not right.) + +```py +def function(x: Any) -> None: ... + +class A: + def method(self, x: Any) -> None: ... + +reveal_type(top_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None +reveal_type(bottom_materialization(TypeOf[function])) # revealed: def function(x: Any) -> None + +reveal_type(top_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None +reveal_type(bottom_materialization(TypeOf[A().method])) # revealed: bound method A.method(x: Any) -> None +``` + +## Callable + +For a callable, the parameter types are in a contravariant position, and the return type is in a +covariant position. + +```py +from typing import Any, Callable +from ty_extensions import TypeOf, Unknown, bottom_materialization, top_materialization + +def _(callable: Callable[[Any, Unknown], Any]) -> None: + # revealed: (Never, Never, /) -> object + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (object, object, /) -> Never + reveal_type(bottom_materialization(TypeOf[callable])) +``` + +The parameter types in a callable inherits the contravariant position. + +```py +def _(callable: Callable[[int, tuple[int | Any]], tuple[Any]]) -> None: + # revealed: (int, tuple[int], /) -> tuple[object] + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (int, tuple[object], /) -> Never + reveal_type(bottom_materialization(TypeOf[callable])) +``` + +But, if the callable itself is in a contravariant position, then the variance is flipped i.e., if +the outer variance is covariant, it's flipped to contravariant, and if it's contravariant, it's +flipped to covariant, invariant remains invariant. + +```py +def _(callable: Callable[[Any, Callable[[Unknown], Any]], Callable[[Any, int], Any]]) -> None: + # revealed: (Never, (object, /) -> Never, /) -> (Never, int, /) -> object + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (object, (Never, /) -> object, /) -> (object, int, /) -> Never + reveal_type(bottom_materialization(TypeOf[callable])) +``` + +## Tuple + +All positions in a tuple are covariant. + +```py +from typing import Any +from ty_extensions import Unknown, bottom_materialization, top_materialization + +reveal_type(top_materialization(tuple[Any, int])) # revealed: tuple[object, int] +reveal_type(bottom_materialization(tuple[Any, int])) # revealed: Never + +reveal_type(top_materialization(tuple[Unknown, int])) # revealed: tuple[object, int] +reveal_type(bottom_materialization(tuple[Unknown, int])) # revealed: Never + +reveal_type(top_materialization(tuple[Any, int, Unknown])) # revealed: tuple[object, int, object] +reveal_type(bottom_materialization(tuple[Any, int, Unknown])) # revealed: Never +``` + +Except for when the tuple itself is in a contravariant position, then all positions in the tuple +inherit the contravariant position. + +```py +from typing import Callable +from ty_extensions import TypeOf + +def _(callable: Callable[[tuple[Any, int], tuple[str, Unknown]], None]) -> None: + # revealed: (Never, Never, /) -> None + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (tuple[object, int], tuple[str, object], /) -> None + reveal_type(bottom_materialization(TypeOf[callable])) +``` + +And, similarly for an invariant position. + +```py +reveal_type(top_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]] +reveal_type(bottom_materialization(list[tuple[Any, int]])) # revealed: list[tuple[T_all, int]] + +reveal_type(top_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]] +reveal_type(bottom_materialization(list[tuple[str, Unknown]])) # revealed: list[tuple[str, T_all]] + +reveal_type(top_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]] +reveal_type(bottom_materialization(list[tuple[Any, int, Unknown]])) # revealed: list[tuple[T_all, int, T_all]] +``` + +## Union + +All positions in a union are covariant. + +```py +from typing import Any +from ty_extensions import Unknown, bottom_materialization, top_materialization + +reveal_type(top_materialization(Any | int)) # revealed: object +reveal_type(bottom_materialization(Any | int)) # revealed: int + +reveal_type(top_materialization(Unknown | int)) # revealed: object +reveal_type(bottom_materialization(Unknown | int)) # revealed: int + +reveal_type(top_materialization(int | str | Any)) # revealed: object +reveal_type(bottom_materialization(int | str | Any)) # revealed: int | str +``` + +Except for when the union itself is in a contravariant position, then all positions in the union +inherit the contravariant position. + +```py +from typing import Callable +from ty_extensions import TypeOf + +def _(callable: Callable[[Any | int, str | Unknown], None]) -> None: + # revealed: (int, str, /) -> None + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (object, object, /) -> None + reveal_type(bottom_materialization(TypeOf[callable])) +``` + +And, similarly for an invariant position. + +```py +reveal_type(top_materialization(list[Any | int])) # revealed: list[T_all | int] +reveal_type(bottom_materialization(list[Any | int])) # revealed: list[T_all | int] + +reveal_type(top_materialization(list[str | Unknown])) # revealed: list[str | T_all] +reveal_type(bottom_materialization(list[str | Unknown])) # revealed: list[str | T_all] + +reveal_type(top_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int] +reveal_type(bottom_materialization(list[Any | int | Unknown])) # revealed: list[T_all | int] +``` + +## Intersection + +All positions in an intersection are covariant. + +```py +from typing import Any +from ty_extensions import Intersection, Unknown, bottom_materialization, top_materialization + +reveal_type(top_materialization(Intersection[Any, int])) # revealed: int +reveal_type(bottom_materialization(Intersection[Any, int])) # revealed: Never + +# Here, the top materialization of `Any | int` is `object` and the intersection of it with tuple +# revealed: tuple[str, object] +reveal_type(top_materialization(Intersection[Any | int, tuple[str, Unknown]])) +# revealed: Never +reveal_type(bottom_materialization(Intersection[Any | int, tuple[str, Unknown]])) + +class Foo: ... + +# revealed: Foo & tuple[str] +reveal_type(bottom_materialization(Intersection[Any | Foo, tuple[str]])) + +reveal_type(top_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int] +reveal_type(bottom_materialization(Intersection[list[Any], list[int]])) # revealed: list[T_all] & list[int] +``` + +## Negation (via `Not`) + +All positions in a negation are contravariant. + +```py +from typing import Any +from ty_extensions import Not, Unknown, bottom_materialization, top_materialization + +# ~Any is still Any, so the top materialization is object +reveal_type(top_materialization(Not[Any])) # revealed: object +reveal_type(bottom_materialization(Not[Any])) # revealed: Never + +# tuple[Any, int] is in a contravariant position, so the +# top materialization is Never and the negation of it +# revealed: object +reveal_type(top_materialization(Not[tuple[Any, int]])) +# revealed: ~tuple[object, int] +reveal_type(bottom_materialization(Not[tuple[Any, int]])) +``` + +## `type` + +```py +from typing import Any +from ty_extensions import Unknown, bottom_materialization, top_materialization + +reveal_type(top_materialization(type[Any])) # revealed: type +reveal_type(bottom_materialization(type[Any])) # revealed: Never + +reveal_type(top_materialization(type[Unknown])) # revealed: type +reveal_type(bottom_materialization(type[Unknown])) # revealed: Never + +reveal_type(top_materialization(type[int | Any])) # revealed: type +reveal_type(bottom_materialization(type[int | Any])) # revealed: type[int] + +# Here, `T` has an upper bound of `type` +reveal_type(top_materialization(list[type[Any]])) # revealed: list[T_all] +reveal_type(bottom_materialization(list[type[Any]])) # revealed: list[T_all] +``` + +## Type variables + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Never, TypeVar +from ty_extensions import ( + TypeOf, + Unknown, + bottom_materialization, + top_materialization, + static_assert, + is_subtype_of, +) + +def bounded_by_gradual[T: Any](t: T) -> None: + # Top materialization of `T: Any` is `T: object` + + # Bottom materialization of `T: Any` is `T: Never` + static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never)) + +def constrained_by_gradual[T: (int, Any)](t: T) -> None: + # Top materialization of `T: (int, Any)` is `T: (int, object)` + + # Bottom materialization of `T: (int, Any)` is `T: (int, Never)` + static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int)) +``` + +## Generics + +For generics, the materialization depends on the surrounding variance and the variance of the type +variable itself. + +- If the type variable is invariant, the materialization happens in an invariant position +- If the type variable is covariant, the materialization happens as per the surrounding variance +- If the type variable is contravariant, the materialization happens as per the surrounding + variance, but the variance is flipped + +```py +from typing import Any, Generic, TypeVar +from ty_extensions import bottom_materialization, top_materialization + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + +class GenericInvariant(Generic[T]): + pass + +class GenericCovariant(Generic[T_co]): + pass + +class GenericContravariant(Generic[T_contra]): + pass + +reveal_type(top_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all] +reveal_type(bottom_materialization(GenericInvariant[Any])) # revealed: GenericInvariant[T_all] + +reveal_type(top_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[object] +reveal_type(bottom_materialization(GenericCovariant[Any])) # revealed: GenericCovariant[Never] + +reveal_type(top_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[Never] +reveal_type(bottom_materialization(GenericContravariant[Any])) # revealed: GenericContravariant[object] +``` + +Parameters in callable are contravariant, so the variance should be flipped: + +```py +from typing import Callable +from ty_extensions import TypeOf + +def invariant(callable: Callable[[GenericInvariant[Any]], None]) -> None: + # revealed: (GenericInvariant[T_all], /) -> None + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (GenericInvariant[T_all], /) -> None + reveal_type(bottom_materialization(TypeOf[callable])) + +def covariant(callable: Callable[[GenericCovariant[Any]], None]) -> None: + # revealed: (GenericCovariant[Never], /) -> None + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (GenericCovariant[object], /) -> None + reveal_type(bottom_materialization(TypeOf[callable])) + +def contravariant(callable: Callable[[GenericContravariant[Any]], None]) -> None: + # revealed: (GenericContravariant[object], /) -> None + reveal_type(top_materialization(TypeOf[callable])) + + # revealed: (GenericContravariant[Never], /) -> None + reveal_type(bottom_materialization(TypeOf[callable])) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/str_repr.md b/crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/str_repr.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md new file mode 100644 index 0000000000000..4cb8bdbc458e6 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -0,0 +1,145 @@ +# Truthiness + +## Literals + +```py +from typing_extensions import Literal, LiteralString +from ty_extensions import AlwaysFalsy, AlwaysTruthy + +def _( + a: Literal[1], + b: Literal[-1], + c: Literal["foo"], + d: tuple[Literal[0]], + e: Literal[1, 2], + f: AlwaysTruthy, +): + reveal_type(bool(a)) # revealed: Literal[True] + reveal_type(bool(b)) # revealed: Literal[True] + reveal_type(bool(c)) # revealed: Literal[True] + reveal_type(bool(d)) # revealed: Literal[True] + reveal_type(bool(e)) # revealed: Literal[True] + reveal_type(bool(f)) # revealed: Literal[True] + +def _( + a: tuple[()], + b: Literal[0], + c: Literal[""], + d: Literal[b""], + e: Literal[0, 0], + f: AlwaysFalsy, +): + reveal_type(bool(a)) # revealed: Literal[False] + reveal_type(bool(b)) # revealed: Literal[False] + reveal_type(bool(c)) # revealed: Literal[False] + reveal_type(bool(d)) # revealed: Literal[False] + reveal_type(bool(e)) # revealed: Literal[False] + reveal_type(bool(f)) # revealed: Literal[False] + +def _( + a: str, + b: Literal[1, 0], + c: str | Literal[0], + d: str | Literal[1], +): + reveal_type(bool(a)) # revealed: bool + reveal_type(bool(b)) # revealed: bool + reveal_type(bool(c)) # revealed: bool + reveal_type(bool(d)) # revealed: bool +``` + +## Instances + +Checks that we don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin: + +### __bool__ is bool + +```py +class BoolIsBool: + __bool__ = bool + +reveal_type(bool(BoolIsBool())) # revealed: bool +``` + +### Conditional __bool__ method + +```py +def flag() -> bool: + return True + +class Boom: + if flag(): + __bool__ = bool + else: + __bool__ = int + +reveal_type(bool(Boom())) # revealed: bool +``` + +### Possibly unbound __bool__ method + +```py +from typing import Literal + +def flag() -> bool: + return True + +class PossiblyUnboundTrue: + if flag(): + def __bool__(self) -> Literal[True]: + return True + +reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool +``` + +### Special-cased classes + +Some special-cased `@final` classes are known by ty to have instances that are either always truthy +or always falsy. + +```toml +[environment] +python-version = "3.12" +``` + +```py +import types +import typing +import sys +from ty_extensions import AlwaysTruthy, static_assert, is_subtype_of +from typing_extensions import _NoDefaultType + +static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy)) +static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy)) +static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy)) +static_assert(is_subtype_of(slice, AlwaysTruthy)) +static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy)) +static_assert(is_subtype_of(types.MethodType, AlwaysTruthy)) +static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy)) +static_assert(is_subtype_of(typing.TypeAliasType, AlwaysTruthy)) +static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy)) +static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy)) +``` + +### `Callable` types always have ambiguous truthiness + +```py +from typing import Callable + +def f(x: Callable, y: Callable[[int], str]): + reveal_type(bool(x)) # revealed: bool + reveal_type(bool(y)) # revealed: bool +``` + +But certain callable single-valued types are known to be always truthy: + +```py +from types import FunctionType + +class A: + def method(self): ... + +reveal_type(bool(A().method)) # revealed: Literal[True] +reveal_type(bool(f.__get__)) # revealed: Literal[True] +reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md b/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md index b0a1ad3077167..5536b84c98d10 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md @@ -10,7 +10,7 @@ zero element in multiplication, similar to how a Cartesian product with the empt set. ```py -from knot_extensions import static_assert, is_equivalent_to +from ty_extensions import static_assert, is_equivalent_to from typing_extensions import Never, NoReturn static_assert(is_equivalent_to(Never, tuple[Never])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md rename to crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index fdf443db9cc04..4e2135bd3c468 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -21,8 +21,7 @@ class C: reveal_type(C.a) # revealed: int reveal_type(C.b) # revealed: int reveal_type(C.c) # revealed: int -# TODO: should be Unknown | Literal[1] -reveal_type(C.d) # revealed: Unknown +reveal_type(C.d) # revealed: Unknown | Literal[1] reveal_type(C.e) # revealed: int c = C() @@ -64,31 +63,13 @@ c = C() c.a = 2 ``` -and similarly here: - -```py -class Base: - a: ClassVar[int] = 1 - -class Derived(Base): - if flag(): - a: int - -reveal_type(Derived.a) # revealed: int - -d = Derived() - -# error: [invalid-attribute-access] -d.a = 2 -``` - ## Too many arguments ```py from typing import ClassVar class C: - # error: [invalid-type-form] "Type qualifier `typing.ClassVar` expects exactly one type parameter" + # error: [invalid-type-form] "Type qualifier `typing.ClassVar` expected exactly 1 argument, got 2" x: ClassVar[int, str] = 1 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md new file mode 100644 index 0000000000000..57f64cd875d02 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -0,0 +1,283 @@ +# `typing.Final` + +[`typing.Final`] is a type qualifier that is used to indicate that a symbol may not be reassigned in +any scope. Final names declared in class scopes cannot be overridden in subclasses. + +## Basic type inference + +### `Final` with type + +Declared symbols that are additionally qualified with `Final` use the declared type when accessed +from another scope. Local uses of the symbol will use the inferred type, which may be more specific: + +`mod.py`: + +```py +from typing import Final, Annotated + +FINAL_A: Final[int] = 1 +FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1 +FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1 +FINAL_D: "Final[int]" = 1 +FINAL_F: Final[int] +FINAL_F = 1 + +reveal_type(FINAL_A) # revealed: Literal[1] +reveal_type(FINAL_B) # revealed: Literal[1] +reveal_type(FINAL_C) # revealed: Literal[1] +reveal_type(FINAL_D) # revealed: Literal[1] +reveal_type(FINAL_D) # revealed: Literal[1] + +def nonlocal_uses(): + reveal_type(FINAL_A) # revealed: int + reveal_type(FINAL_B) # revealed: int + reveal_type(FINAL_C) # revealed: int + reveal_type(FINAL_D) # revealed: int + reveal_type(FINAL_F) # revealed: int +``` + +Imported types: + +```py +from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_F + +reveal_type(FINAL_A) # revealed: int +reveal_type(FINAL_B) # revealed: int +reveal_type(FINAL_C) # revealed: int +reveal_type(FINAL_D) # revealed: int +reveal_type(FINAL_F) # revealed: int +``` + +### `Final` without a type + +When a symbol is qualified with `Final` but no type is specified, the type is inferred from the +right-hand side of the assignment. We do not union the inferred type with `Unknown`, because the +symbol cannot be modified: + +`mod.py`: + +```py +from typing import Final + +FINAL_A: Final = 1 + +reveal_type(FINAL_A) # revealed: Literal[1] + +def nonlocal_uses(): + reveal_type(FINAL_A) # revealed: Literal[1] +``` + +`main.py`: + +```py +from mod import FINAL_A + +reveal_type(FINAL_A) # revealed: Literal[1] +``` + +### In class definitions + +```py +from typing import Final + +class C: + FINAL_A: Final[int] = 1 + FINAL_B: Final = 1 + + def __init__(self): + self.FINAL_C: Final[int] = 1 + self.FINAL_D: Final = 1 + +reveal_type(C.FINAL_A) # revealed: int +reveal_type(C.FINAL_B) # revealed: Literal[1] + +reveal_type(C().FINAL_A) # revealed: int +reveal_type(C().FINAL_B) # revealed: Literal[1] +reveal_type(C().FINAL_C) # revealed: int +# TODO: this should be `Literal[1]` +reveal_type(C().FINAL_D) # revealed: Unknown +``` + +## Not modifiable + +### Names + +Symbols qualified with `Final` cannot be reassigned, and attempting to do so will result in an +error: + +`mod.py`: + +```py +from typing import Final, Annotated + +FINAL_A: Final[int] = 1 +FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1 +FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1 +FINAL_D: "Final[int]" = 1 +FINAL_E: Final[int] +FINAL_E = 1 +FINAL_F: Final = 1 + +FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed" +FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed" +FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed" +FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed" +FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed" +FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed" + +def global_use(): + global FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F + FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed" + FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed" + FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed" + FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed" + FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed" + FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed" + +def local_use(): + # These are not errors, because they refer to local variables + FINAL_A = 2 + FINAL_B = 2 + FINAL_C = 2 + FINAL_D = 2 + FINAL_E = 2 + FINAL_F = 2 + +def nonlocal_use(): + X: Final[int] = 1 + def inner(): + nonlocal X + # TODO: this should be an error + X = 2 +``` + +`main.py`: + +```py +from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F + +FINAL_A = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_A` is not allowed" +FINAL_B = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_B` is not allowed" +FINAL_C = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_C` is not allowed" +FINAL_D = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_D` is not allowed" +FINAL_E = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_E` is not allowed" +FINAL_F = 2 # error: [invalid-assignment] "Reassignment of `Final` symbol `FINAL_F` is not allowed" +``` + +### Attributes + +Assignments to attributes qualified with `Final` are also not allowed: + +```py +from typing import Final + +class C: + FINAL_A: Final[int] = 1 + FINAL_B: Final = 1 + + def __init__(self): + self.FINAL_C: Final[int] = 1 + self.FINAL_D: Final = 1 + +# TODO: these should be errors (that mention `Final`) +C.FINAL_A = 2 +# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`" +C.FINAL_B = 2 + +# TODO: these should be errors (that mention `Final`) +c = C() +c.FINAL_A = 2 +# error: [invalid-assignment] "Object of type `Literal[2]` is not assignable to attribute `FINAL_B` of type `Literal[1]`" +c.FINAL_B = 2 +c.FINAL_C = 2 +c.FINAL_D = 2 +``` + +## Mutability + +Objects qualified with `Final` *can be modified*. `Final` represents a constant reference to an +object, but that object itself may still be mutable: + +```py +from typing import Final + +class C: + x: int = 1 + +FINAL_C_INSTANCE: Final[C] = C() +FINAL_C_INSTANCE.x = 2 + +FINAL_LIST: Final[list[int]] = [1, 2, 3] +FINAL_LIST[0] = 4 +``` + +## Too many arguments + +```py +from typing import Final + +class C: + # error: [invalid-type-form] "Type qualifier `typing.Final` expected exactly 1 argument, got 2" + x: Final[int, str] = 1 +``` + +## Illegal `Final` in type expression + +```py +from typing import Final + +class C: + # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" + x: Final | int + + # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" + y: int | Final[str] +``` + +## No assignment + +```py +from typing import Final + +DECLARED_THEN_BOUND: Final[int] +DECLARED_THEN_BOUND = 1 +``` + +## No assignment for bare `Final` + +```py +from typing import Final + +# TODO: This should be an error +NO_RHS: Final + +class C: + # TODO: This should be an error + NO_RHS: Final +``` + +## Full diagnostics + + + +Annotated assignment: + +```py +from typing import Final + +MY_CONSTANT: Final[int] = 1 + +# more code + +MY_CONSTANT = 2 # error: [invalid-assignment] +``` + +Imported `Final` symbol: + +```py +from _stat import ST_INO + +ST_INO = 1 # error: [invalid-assignment] +``` + +[`typing.final`]: https://docs.python.org/3/library/typing.html#typing.Final diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md new file mode 100644 index 0000000000000..217bfc9e4d5b3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -0,0 +1,27 @@ +# `TypedDict` + +We do not support `TypedDict`s yet. This test mainly exists to make sure that we do not emit any +errors for the definition of a `TypedDict`. + +```py +from typing_extensions import TypedDict, Required + +class Person(TypedDict): + name: str + age: int | None + +# TODO: This should not be an error: +# error: [invalid-assignment] +alice: Person = {"name": "Alice", "age": 30} + +# Alternative syntax +Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False) + +msg = Message(id=1, content="Hello") + +# No errors for yet-unsupported features (`closed`): +OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) + +reveal_type(Person.__required_keys__) # revealed: @Todo(TypedDict) +reveal_type(Message.__required_keys__) # revealed: @Todo(TypedDict) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md new file mode 100644 index 0000000000000..1544e42890805 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -0,0 +1,169 @@ +# Custom unary operations + +## Class instances + +```py +class Yes: + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Sub(Yes): ... +class No: ... + +reveal_type(+Yes()) # revealed: bool +reveal_type(-Yes()) # revealed: str +reveal_type(~Yes()) # revealed: int + +reveal_type(+Sub()) # revealed: bool +reveal_type(-Sub()) # revealed: str +reveal_type(~Sub()) # revealed: int + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`" +reveal_type(+No()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`" +reveal_type(-No()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`" +reveal_type(~No()) # revealed: Unknown +``` + +## Classes + +Dunder methods defined in a class are available to instances of that class, but not to the class +itself. (For these operators to work on the class itself, they would have to be defined on the +class's type, i.e. `type`.) + +```py +class Yes: + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Sub(Yes): ... +class No: ... + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +reveal_type(+Yes) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +reveal_type(-Yes) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +reveal_type(~Yes) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +reveal_type(+Sub) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +reveal_type(-Sub) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +reveal_type(~Sub) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +reveal_type(+No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +reveal_type(-No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +reveal_type(~No) # revealed: Unknown +``` + +## Function literals + +```py +def f(): + pass + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `def f() -> Unknown`" +reveal_type(+f) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `def f() -> Unknown`" +reveal_type(-f) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `def f() -> Unknown`" +reveal_type(~f) # revealed: Unknown +``` + +## Subclass + +```py +class Yes: + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Sub(Yes): ... +class No: ... + +def yes() -> type[Yes]: + return Yes + +def sub() -> type[Sub]: + return Sub + +def no() -> type[No]: + return No + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`" +reveal_type(+yes()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`" +reveal_type(-yes()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`" +reveal_type(~yes()) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`" +reveal_type(+sub()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`" +reveal_type(-sub()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`" +reveal_type(~sub()) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`" +reveal_type(+no()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`" +reveal_type(-no()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`" +reveal_type(~no()) # revealed: Unknown +``` + +## Metaclass + +```py +class Meta(type): + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Yes(metaclass=Meta): ... +class Sub(Yes): ... +class No: ... + +reveal_type(+Yes) # revealed: bool +reveal_type(-Yes) # revealed: str +reveal_type(~Yes) # revealed: int + +reveal_type(+Sub) # revealed: bool +reveal_type(-Sub) # revealed: str +reveal_type(~Sub) # revealed: int + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type ``" +reveal_type(+No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type ``" +reveal_type(-No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type ``" +reveal_type(~No) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/integers.md b/crates/ty_python_semantic/resources/mdtest/unary/integers.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unary/integers.md rename to crates/ty_python_semantic/resources/mdtest/unary/integers.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md rename to crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/unary/not.md rename to crates/ty_python_semantic/resources/mdtest/unary/not.md index 82f589517af48..e01796a9f7506 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/not.md @@ -187,7 +187,7 @@ class MethodBoolInvalid: def __bool__(self) -> int: return 0 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`; the return type of its bool method (`int`) isn't assignable to `bool" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`" # revealed: bool reveal_type(not MethodBoolInvalid()) diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md new file mode 100644 index 0000000000000..919fd4921b29e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -0,0 +1,273 @@ +# Union types + +This test suite covers certain basic properties and simplification strategies for union types. + +## Basic unions + +```py +from typing import Literal + +def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None: + reveal_type(u1) # revealed: int | str + reveal_type(u2) # revealed: Literal[0, 1] +``` + +## Duplicate elements are collapsed + +```py +def _(u1: int | int | str, u2: int | str | int) -> None: + reveal_type(u1) # revealed: int | str + reveal_type(u2) # revealed: int | str +``` + +## `Never` is removed + +`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant, +and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`. + +```py +from typing_extensions import Never, NoReturn + +def never(u1: int | Never, u2: int | Never | str) -> None: + reveal_type(u1) # revealed: int + reveal_type(u2) # revealed: int | str + +def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None: + reveal_type(u1) # revealed: int + reveal_type(u2) # revealed: int | str +``` + +## `object` subsumes everything + +Unions with `object` can be simplified to `object`: + +```py +from typing_extensions import Never, Any + +def _( + u1: int | object, + u2: object | int, + u3: Any | object, + u4: object | Any, + u5: object | Never, + u6: Never | object, + u7: int | str | object | bytes | Any, +) -> None: + reveal_type(u1) # revealed: object + reveal_type(u2) # revealed: object + reveal_type(u3) # revealed: object + reveal_type(u4) # revealed: object + reveal_type(u5) # revealed: object + reveal_type(u6) # revealed: object + reveal_type(u7) # revealed: object +``` + +## Flattening of nested unions + +```py +from typing import Literal + +def _( + u1: (int | str) | bytes, + u2: int | (str | bytes), + u3: int | (str | (bytes | bytearray)), +) -> None: + reveal_type(u1) # revealed: int | str | bytes + reveal_type(u2) # revealed: int | str | bytes + reveal_type(u3) # revealed: int | str | bytes | bytearray +``` + +## Simplification using subtyping + +The type `S | T` can be simplified to `T` if `S` is a subtype of `T`: + +```py +from typing_extensions import Literal, LiteralString + +def _( + u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString +) -> None: + reveal_type(u1) # revealed: str + reveal_type(u2) # revealed: str + reveal_type(u3) # revealed: str + reveal_type(u4) # revealed: str | bytes +``` + +## Boolean literals + +The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`: + +```py +from typing import Literal + +def _( + u1: Literal[True, False], + u2: bool | Literal[True], + u3: Literal[True] | bool, + u4: Literal[True] | Literal[True, 17], + u5: Literal[True, False, True, 17], +) -> None: + reveal_type(u1) # revealed: bool + reveal_type(u2) # revealed: bool + reveal_type(u3) # revealed: bool + reveal_type(u4) # revealed: Literal[True, 17] + reveal_type(u5) # revealed: bool | Literal[17] +``` + +## Do not erase `Unknown` + +```py +from ty_extensions import Unknown + +def _(u1: Unknown | str, u2: str | Unknown) -> None: + reveal_type(u1) # revealed: Unknown | str + reveal_type(u2) # revealed: str | Unknown +``` + +## Collapse multiple `Unknown`s + +Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a +union are still redundant: + +```py +from ty_extensions import Unknown + +def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: + reveal_type(u1) # revealed: Unknown | str + reveal_type(u2) # revealed: Unknown | str + reveal_type(u3) # revealed: str | Unknown +``` + +## Subsume multiple elements + +Simplifications still apply when `Unknown` is present. + +```py +from ty_extensions import Unknown + +def _(u1: int | Unknown | bool) -> None: + reveal_type(u1) # revealed: int | Unknown +``` + +## Union of intersections + +We can simplify unions of intersections: + +```py +from ty_extensions import Intersection, Not + +class P: ... +class Q: ... + +def _( + i1: Intersection[P, Q] | Intersection[P, Q], + i2: Intersection[P, Q] | Intersection[Q, P], +) -> None: + reveal_type(i1) # revealed: P & Q + reveal_type(i2) # revealed: P & Q +``` + +## Unions of literals with `AlwaysTruthy` and `AlwaysFalsy` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, Union +from ty_extensions import AlwaysTruthy, AlwaysFalsy, is_equivalent_to, static_assert + +type strings = Literal["foo", ""] +type ints = Literal[0, 1] +type bytes = Literal[b"foo", b""] + +def _( + strings_or_truthy: strings | AlwaysTruthy, + truthy_or_strings: AlwaysTruthy | strings, + strings_or_falsy: strings | AlwaysFalsy, + falsy_or_strings: AlwaysFalsy | strings, + ints_or_truthy: ints | AlwaysTruthy, + truthy_or_ints: AlwaysTruthy | ints, + ints_or_falsy: ints | AlwaysFalsy, + falsy_or_ints: AlwaysFalsy | ints, + bytes_or_truthy: bytes | AlwaysTruthy, + truthy_or_bytes: AlwaysTruthy | bytes, + bytes_or_falsy: bytes | AlwaysFalsy, + falsy_or_bytes: AlwaysFalsy | bytes, +): + reveal_type(strings_or_truthy) # revealed: Literal[""] | AlwaysTruthy + reveal_type(truthy_or_strings) # revealed: AlwaysTruthy | Literal[""] + + reveal_type(strings_or_falsy) # revealed: Literal["foo"] | AlwaysFalsy + reveal_type(falsy_or_strings) # revealed: AlwaysFalsy | Literal["foo"] + + reveal_type(ints_or_truthy) # revealed: Literal[0] | AlwaysTruthy + reveal_type(truthy_or_ints) # revealed: AlwaysTruthy | Literal[0] + + reveal_type(ints_or_falsy) # revealed: Literal[1] | AlwaysFalsy + reveal_type(falsy_or_ints) # revealed: AlwaysFalsy | Literal[1] + + reveal_type(bytes_or_truthy) # revealed: Literal[b""] | AlwaysTruthy + reveal_type(truthy_or_bytes) # revealed: AlwaysTruthy | Literal[b""] + + reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy + reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"] + +type SA = Union[Literal[""], AlwaysTruthy, Literal["foo"]] +static_assert(is_equivalent_to(SA, Literal[""] | AlwaysTruthy)) + +type SD = Union[Literal[""], AlwaysTruthy, Literal["foo"], AlwaysFalsy, AlwaysTruthy, int] +static_assert(is_equivalent_to(SD, AlwaysTruthy | AlwaysFalsy | int)) + +type BA = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"]] +static_assert(is_equivalent_to(BA, Literal[b""] | AlwaysTruthy)) + +type BD = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"], AlwaysFalsy, AlwaysTruthy, int] +static_assert(is_equivalent_to(BD, AlwaysTruthy | AlwaysFalsy | int)) + +type IA = Union[Literal[0], AlwaysTruthy, Literal[1]] +static_assert(is_equivalent_to(IA, Literal[0] | AlwaysTruthy)) + +type ID = Union[Literal[0], AlwaysTruthy, Literal[1], AlwaysFalsy, AlwaysTruthy, str] +static_assert(is_equivalent_to(ID, AlwaysTruthy | AlwaysFalsy | str)) +``` + +## Unions with intersections of literals and Any + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Literal +from ty_extensions import Intersection + +type SA = Literal[""] +type SB = Intersection[Literal[""], Any] +type SC = SA | SB +type SD = SB | SA + +def _(c: SC, d: SD): + reveal_type(c) # revealed: Literal[""] + reveal_type(d) # revealed: Literal[""] + +type IA = Literal[0] +type IB = Intersection[Literal[0], Any] +type IC = IA | IB +type ID = IB | IA + +def _(c: IC, d: ID): + reveal_type(c) # revealed: Literal[0] + reveal_type(d) # revealed: Literal[0] + +type BA = Literal[b""] +type BB = Intersection[Literal[b""], Any] +type BC = BA | BB +type BD = BB | BA + +def _(c: BC, d: BD): + reveal_type(c) # revealed: Literal[b""] + reveal_type(d) # revealed: Literal[b""] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md new file mode 100644 index 0000000000000..02d4952780841 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/unpacking.md @@ -0,0 +1,1010 @@ +# Unpacking + +If there are not enough or too many values when unpacking, an error will occur and the types of all +variables (if nested tuple unpacking fails, only the variables within the failed tuples) is inferred +to be `Unknown`. + +## Tuple + +### Simple tuple + +```py +(a, b, c) = (1, 2, 3) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[2] +reveal_type(c) # revealed: Literal[3] +``` + +### Simple list + +```py +[a, b, c] = (1, 2, 3) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[2] +reveal_type(c) # revealed: Literal[3] +``` + +### Simple mixed + +```py +[a, (b, c), d] = (1, (2, 3), 4) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[2] +reveal_type(c) # revealed: Literal[3] +reveal_type(d) # revealed: Literal[4] +``` + +### Multiple assignment + +```py +a, b = c = 1, 2 +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[2] +reveal_type(c) # revealed: tuple[Literal[1], Literal[2]] +``` + +### Nested tuple with unpacking + +```py +(a, (b, c), d) = (1, (2, 3), 4) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[2] +reveal_type(c) # revealed: Literal[3] +reveal_type(d) # revealed: Literal[4] +``` + +### Nested tuple without unpacking + +```py +(a, b, c) = (1, (2, 3), 4) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: tuple[Literal[2], Literal[3]] +reveal_type(c) # revealed: Literal[4] +``` + +### Uneven unpacking (1) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected 3" +(a, b, c) = (1, 2) +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +reveal_type(c) # revealed: Unknown +``` + +### Uneven unpacking (2) + +```py +# error: [invalid-assignment] "Too many values to unpack: Expected 2" +(a, b) = (1, 2, 3) +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Nested uneven unpacking (1) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" +(a, (b, c), d) = (1, (2,), 3) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Unknown +reveal_type(c) # revealed: Unknown +reveal_type(d) # revealed: Literal[3] +``` + +### Nested uneven unpacking (2) + +```py +# error: [invalid-assignment] "Too many values to unpack: Expected 2" +(a, (b, c), d) = (1, (2, 3, 4), 5) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Unknown +reveal_type(c) # revealed: Unknown +reveal_type(d) # revealed: Literal[5] +``` + +### Starred expression (1) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3" +[a, *b, c, d] = (1, 2) +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: list[Unknown] +reveal_type(c) # revealed: Unknown +reveal_type(d) # revealed: Unknown +``` + +### Starred expression (2) + +```py +[a, *b, c] = (1, 2) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: list[Never] +reveal_type(c) # revealed: Literal[2] +``` + +### Starred expression (3) + +```py +[a, *b, c] = (1, 2, 3) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: list[Literal[2]] +reveal_type(c) # revealed: Literal[3] +``` + +### Starred expression (4) + +```py +[a, *b, c, d] = (1, 2, 3, 4, 5, 6) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: list[Literal[2, 3, 4]] +reveal_type(c) # revealed: Literal[5] +reveal_type(d) # revealed: Literal[6] +``` + +### Starred expression (5) + +```py +[a, b, *c] = (1, 2, 3, 4) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal[2] +reveal_type(c) # revealed: list[Literal[3, 4]] +``` + +### Starred expression (6) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected at least 5" +(a, b, c, *d, e, f) = (1,) +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +reveal_type(c) # revealed: Unknown +reveal_type(d) # revealed: list[Unknown] +reveal_type(e) # revealed: Unknown +reveal_type(f) # revealed: Unknown +``` + +### Non-iterable unpacking + +```py +# error: "Object of type `Literal[1]` is not iterable" +a, b = 1 +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Custom iterator unpacking + +```py +class Iterator: + def __next__(self) -> int: + return 42 + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +(a, b) = Iterable() +reveal_type(a) # revealed: int +reveal_type(b) # revealed: int +``` + +### Custom iterator unpacking nested + +```py +class Iterator: + def __next__(self) -> int: + return 42 + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +(a, (b, c), d) = (1, Iterable(), 2) +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: int +reveal_type(c) # revealed: int +reveal_type(d) # revealed: Literal[2] +``` + +## List + +### Literal unpacking + +```py +a, b = [1, 2] +# TODO: should be `int` for both `a` and `b` +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Simple unpacking + +```py +def _(value: list[int]): + a, b = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Nested unpacking + +```py +def _(value: list[list[int]]): + a, (b, c) = value + reveal_type(a) # revealed: list[int] + reveal_type(b) # revealed: int + reveal_type(c) # revealed: int +``` + +### Invalid nested unpacking + +```py +def _(value: list[int]): + # error: [not-iterable] "Object of type `int` is not iterable" + a, (b, c) = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown +``` + +### Starred expression + +```py +def _(value: list[int]): + a, *b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[int] + reveal_type(c) # revealed: int +``` + +## Homogeneous tuples + +### Simple unpacking + +```py +def _(value: tuple[int, ...]): + a, b = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Nested unpacking + +```py +def _(value: tuple[tuple[int, ...], ...]): + a, (b, c) = value + reveal_type(a) # revealed: tuple[int, ...] + reveal_type(b) # revealed: int + reveal_type(c) # revealed: int +``` + +### Invalid nested unpacking + +```py +def _(value: tuple[int, ...]): + # error: [not-iterable] "Object of type `int` is not iterable" + a, (b, c) = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown +``` + +### Starred expression + +```py +def _(value: tuple[int, ...]): + a, *b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[int] + reveal_type(c) # revealed: int +``` + +## Mixed tuples + +```toml +[environment] +python-version = "3.11" +``` + +### Simple unpacking (1) + +```py +def _(value: tuple[int, *tuple[str, ...]]): + a, b = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: str +``` + +### Simple unpacking (2) + +```py +def _(value: tuple[int, int, *tuple[str, ...]]): + a, b = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Simple unpacking (3) + +```py +def _(value: tuple[int, *tuple[str, ...], int]): + a, b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: str + reveal_type(c) # revealed: int +``` + +### Invalid unpacked + +```py +def _(value: tuple[int, int, int, *tuple[str, ...]]): + # error: [invalid-assignment] "Too many values to unpack: Expected 2" + a, b = value + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown +``` + +### Nested unpacking + +```py +def _(value: tuple[str, *tuple[tuple[int, ...], ...]]): + a, (b, c) = value + reveal_type(a) # revealed: str + reveal_type(b) # revealed: int + reveal_type(c) # revealed: int +``` + +### Invalid nested unpacking + +```py +def _(value: tuple[str, *tuple[int, ...]]): + # error: [not-iterable] "Object of type `int` is not iterable" + a, (b, c) = value + reveal_type(a) # revealed: str + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown +``` + +### Starred expression (1) + +```py +def _(value: tuple[int, *tuple[str, ...]]): + a, *b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[str] + reveal_type(c) # revealed: str +``` + +### Starred expression (2) + +```py +def _(value: tuple[int, *tuple[str, ...], int]): + a, *b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[str] + reveal_type(c) # revealed: int +``` + +### Starred expression (3) + +```py +def _(value: tuple[int, *tuple[str, ...], int]): + a, *b, c, d = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[str] + reveal_type(c) # revealed: str + reveal_type(d) # revealed: int +``` + +### Starred expression (4) + +```py +def _(value: tuple[int, int, *tuple[str, ...], int]): + a, *b, c = value + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[int | str] + reveal_type(c) # revealed: int +``` + +## String + +### Simple unpacking + +```py +a, b = "ab" +reveal_type(a) # revealed: LiteralString +reveal_type(b) # revealed: LiteralString +``` + +### Uneven unpacking (1) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected 3" +a, b, c = "ab" +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +reveal_type(c) # revealed: Unknown +``` + +### Uneven unpacking (2) + +```py +# error: [invalid-assignment] "Too many values to unpack: Expected 2" +a, b = "abc" +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Starred expression (1) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3" +(a, *b, c, d) = "ab" +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: list[Unknown] +reveal_type(c) # revealed: Unknown +reveal_type(d) # revealed: Unknown +``` + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected at least 3" +(a, b, *c, d) = "a" +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +reveal_type(c) # revealed: list[Unknown] +reveal_type(d) # revealed: Unknown +``` + +### Starred expression (2) + +```py +(a, *b, c) = "ab" +reveal_type(a) # revealed: LiteralString +reveal_type(b) # revealed: list[Never] +reveal_type(c) # revealed: LiteralString +``` + +### Starred expression (3) + +```py +(a, *b, c) = "abc" +reveal_type(a) # revealed: LiteralString +reveal_type(b) # revealed: list[LiteralString] +reveal_type(c) # revealed: LiteralString +``` + +### Starred expression (4) + +```py +(a, *b, c, d) = "abcdef" +reveal_type(a) # revealed: LiteralString +reveal_type(b) # revealed: list[LiteralString] +reveal_type(c) # revealed: LiteralString +reveal_type(d) # revealed: LiteralString +``` + +### Starred expression (5) + +```py +(a, b, *c) = "abcd" +reveal_type(a) # revealed: LiteralString +reveal_type(b) # revealed: LiteralString +reveal_type(c) # revealed: list[LiteralString] +``` + +### Starred expression (6) + +```py +from typing_extensions import LiteralString + +def _(s: LiteralString): + a, b, *c = s + reveal_type(a) # revealed: LiteralString + reveal_type(b) # revealed: LiteralString + reveal_type(c) # revealed: list[LiteralString] +``` + +### Unicode + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" +(a, b) = "é" + +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Unicode escape (1) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" +(a, b) = "\u9e6c" + +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Unicode escape (2) + +```py +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" +(a, b) = "\U0010ffff" + +reveal_type(a) # revealed: Unknown +reveal_type(b) # revealed: Unknown +``` + +### Surrogates + +```py +(a, b) = "\ud800\udfff" + +reveal_type(a) # revealed: LiteralString +reveal_type(b) # revealed: LiteralString +``` + +## Union + +### Same types + +Union of two tuples of equal length and each element is of the same type. + +```py +def _(arg: tuple[int, int] | tuple[int, int]): + (a, b) = arg + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Mixed types (1) + +Union of two tuples of equal length and one element differs in its type. + +```py +def _(arg: tuple[int, int] | tuple[int, str]): + a, b = arg + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int | str +``` + +### Mixed types (2) + +Union of two tuples of equal length and both the element types are different. + +```py +def _(arg: tuple[int, str] | tuple[str, int]): + a, b = arg + reveal_type(a) # revealed: int | str + reveal_type(b) # revealed: str | int +``` + +### Mixed types (3) + +Union of three tuples of equal length and various combination of element types: + +1. All same types +1. One different type +1. All different types + +```py +def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str]): + a, b, c = arg + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int | str + reveal_type(c) # revealed: int | bytes | str +``` + +### Nested + +```py +from typing import Literal + +def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]): + a, (b, c) = arg + reveal_type(a) # revealed: int | tuple[int, bytes] + reveal_type(b) # revealed: str + reveal_type(c) # revealed: bytes | LiteralString +``` + +### Starred expression + +```py +def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): + a, *b, c = arg + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[bytes] | list[int | str] + reveal_type(c) # revealed: int | bytes +``` + +### Size mismatch (1) + +```py +def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): + # error: [invalid-assignment] "Too many values to unpack: Expected 2" + # error: [invalid-assignment] "Too many values to unpack: Expected 2" + a, b = arg + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown +``` + +### Size mismatch (2) + +```py +def _(arg: tuple[int, bytes] | tuple[int, str]): + # error: [invalid-assignment] "Not enough values to unpack: Expected 3" + # error: [invalid-assignment] "Not enough values to unpack: Expected 3" + a, b, c = arg + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown +``` + +### Same literal types + +```py +def _(flag: bool): + if flag: + value = (1, 2) + else: + value = (3, 4) + + a, b = value + reveal_type(a) # revealed: Literal[1, 3] + reveal_type(b) # revealed: Literal[2, 4] +``` + +### Mixed literal types + +```py +def _(flag: bool): + if flag: + value = (1, 2) + else: + value = ("a", "b") + + a, b = value + reveal_type(a) # revealed: Literal[1, "a"] + reveal_type(b) # revealed: Literal[2, "b"] +``` + +### Typing literal + +```py +from typing import Literal + +def _(arg: tuple[int, int] | Literal["ab"]): + a, b = arg + reveal_type(a) # revealed: int | LiteralString + reveal_type(b) # revealed: int | LiteralString +``` + +### Custom iterator (1) + +```py +class Iterator: + def __next__(self) -> tuple[int, int] | tuple[int, str]: + return (1, 2) + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +((a, b), c) = Iterable() +reveal_type(a) # revealed: int +reveal_type(b) # revealed: int | str +reveal_type(c) # revealed: tuple[int, int] | tuple[int, str] +``` + +### Custom iterator (2) + +```py +class Iterator: + def __next__(self) -> bytes: + return b"" + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +def _(arg: tuple[int, str] | Iterable): + a, b = arg + reveal_type(a) # revealed: int | bytes + reveal_type(b) # revealed: str | bytes +``` + +## For statement + +Unpacking in a `for` statement. + +### Same types + +```py +def _(arg: tuple[tuple[int, int], tuple[int, int]]): + for a, b in arg: + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Mixed types (1) + +```py +def _(arg: tuple[tuple[int, int], tuple[int, str]]): + for a, b in arg: + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int | str +``` + +### Mixed types (2) + +```py +def _(arg: tuple[tuple[int, str], tuple[str, int]]): + for a, b in arg: + reveal_type(a) # revealed: int | str + reveal_type(b) # revealed: str | int +``` + +### Mixed types (3) + +```py +def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]): + for a, b, c in arg: + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int | str + reveal_type(c) # revealed: int | bytes | str +``` + +### Same literal values + +```py +for a, b in ((1, 2), (3, 4)): + reveal_type(a) # revealed: Literal[1, 3] + reveal_type(b) # revealed: Literal[2, 4] +``` + +### Mixed literal values (1) + +```py +for a, b in ((1, 2), ("a", "b")): + reveal_type(a) # revealed: Literal[1, "a"] + reveal_type(b) # revealed: Literal[2, "b"] +``` + +### Mixed literals values (2) + +```py +# error: "Object of type `Literal[1]` is not iterable" +# error: "Object of type `Literal[2]` is not iterable" +# error: "Object of type `Literal[4]` is not iterable" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" +for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"): + reveal_type(a) # revealed: Unknown | Literal[3, 5] + reveal_type(b) # revealed: Unknown | Literal["a", "b"] +``` + +### Custom iterator (1) + +```py +class Iterator: + def __next__(self) -> tuple[int, int]: + return (1, 2) + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +for a, b in Iterable(): + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Custom iterator (2) + +```py +class Iterator: + def __next__(self) -> bytes: + return b"" + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +def _(arg: tuple[tuple[int, str], Iterable]): + for a, b in arg: + reveal_type(a) # revealed: int | bytes + reveal_type(b) # revealed: str | bytes +``` + +## With statement + +Unpacking in a `with` statement. + +### Same types + +```py +class ContextManager: + def __enter__(self) -> tuple[int, int]: + return (1, 2) + + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + +with ContextManager() as (a, b): + reveal_type(a) # revealed: int + reveal_type(b) # revealed: int +``` + +### Mixed types + +```py +class ContextManager: + def __enter__(self) -> tuple[int, str]: + return (1, "a") + + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + +with ContextManager() as (a, b): + reveal_type(a) # revealed: int + reveal_type(b) # revealed: str +``` + +### Nested + +```py +class ContextManager: + def __enter__(self) -> tuple[int, tuple[str, bytes]]: + return (1, ("a", b"bytes")) + + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + +with ContextManager() as (a, (b, c)): + reveal_type(a) # revealed: int + reveal_type(b) # revealed: str + reveal_type(c) # revealed: bytes +``` + +### Starred expression + +```py +class ContextManager: + def __enter__(self) -> tuple[int, int, int]: + return (1, 2, 3) + + def __exit__(self, exc_type, exc_value, traceback) -> None: + pass + +with ContextManager() as (a, *b): + reveal_type(a) # revealed: int + reveal_type(b) # revealed: list[int] +``` + +### Unbound context manager expression + +```py +# error: [unresolved-reference] "Name `nonexistant` used when not defined" +with nonexistant as (x, y): + reveal_type(x) # revealed: Unknown + reveal_type(y) # revealed: Unknown +``` + +### Invalid unpacking + +```py +class ContextManager: + def __enter__(self) -> tuple[int, str]: + return (1, "a") + + def __exit__(self, *args) -> None: + pass + +# error: [invalid-assignment] "Not enough values to unpack: Expected 3" +with ContextManager() as (a, b, c): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown +``` + +## Comprehension + +Unpacking in a comprehension. + +### Same types + +```py +def _(arg: tuple[tuple[int, int], tuple[int, int]]): + # revealed: tuple[int, int] + [reveal_type((a, b)) for a, b in arg] +``` + +### Mixed types (1) + +```py +def _(arg: tuple[tuple[int, int], tuple[int, str]]): + # revealed: tuple[int, int | str] + [reveal_type((a, b)) for a, b in arg] +``` + +### Mixed types (2) + +```py +def _(arg: tuple[tuple[int, str], tuple[str, int]]): + # revealed: tuple[int | str, str | int] + [reveal_type((a, b)) for a, b in arg] +``` + +### Mixed types (3) + +```py +def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]): + # revealed: tuple[int, int | str, int | bytes | str] + [reveal_type((a, b, c)) for a, b, c in arg] +``` + +### Same literal values + +```py +# revealed: tuple[Literal[1, 3], Literal[2, 4]] +[reveal_type((a, b)) for a, b in ((1, 2), (3, 4))] +``` + +### Mixed literal values (1) + +```py +# revealed: tuple[Literal[1, "a"], Literal[2, "b"]] +[reveal_type((a, b)) for a, b in ((1, 2), ("a", "b"))] +``` + +### Mixed literals values (2) + +```py +# error: "Object of type `Literal[1]` is not iterable" +# error: "Object of type `Literal[2]` is not iterable" +# error: "Object of type `Literal[4]` is not iterable" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" +# revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]] +[reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")] +``` + +### Custom iterator (1) + +```py +class Iterator: + def __next__(self) -> tuple[int, int]: + return (1, 2) + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +# revealed: tuple[int, int] +[reveal_type((a, b)) for a, b in Iterable()] +``` + +### Custom iterator (2) + +```py +class Iterator: + def __next__(self) -> bytes: + return b"" + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +def _(arg: tuple[tuple[int, str], Iterable]): + # revealed: tuple[int | bytes, str | bytes] + [reveal_type((a, b)) for a, b in arg] +``` + +## Empty + +Unpacking an empty tuple or list shouldn't raise any diagnostics. + +```py +[] = [] +() = () +[] = () +() = [] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/unreachable.md rename to crates/ty_python_semantic/resources/mdtest/unreachable.md index f6b70c724ec20..2d01b4dbbfdbc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md +++ b/crates/ty_python_semantic/resources/mdtest/unreachable.md @@ -72,6 +72,17 @@ def f2(): # TODO: we should mark this as unreachable print("unreachable") + +def f3(): + if False: + return + elif True: + return + else: + pass + + # TODO: we should mark this as unreachable + print("unreachable") ``` ### `Never` / `NoReturn` @@ -211,9 +222,8 @@ reachable or not. Some developers like to use things like early `return` stateme and for this use case, it is helpful to still see some diagnostics in unreachable sections. We currently follow the second approach, but we do not attempt to provide the full set of -diagnostics in unreachable sections. In fact, we silence a certain category of diagnostics -(`unresolved-reference`, `unresolved-attribute`, …), in order to avoid *incorrect* diagnostics. In -the future, we may revisit this decision. +diagnostics in unreachable sections. In fact, a large number of diagnostics are suppressed in +unreachable code, simply due to the fact that we infer `Never` for most of the symbols. ### Use of variables in unreachable code @@ -231,16 +241,16 @@ def f(): ### Use of variable in nested function -In the example below, since we use `x` in the `inner` function, we use the "public" type of `x`, -which currently refers to the end-of-scope type of `x`. Since the end of the `outer` scope is -unreachable, we need to make sure that we do not emit an `unresolved-reference` diagnostic: +This is a regression test for a behavior that previously caused problems when the public type still +referred to the end-of-scope, which would result in an unresolved-reference error here since the end +of the scope is unreachable. ```py def outer(): x = 1 def inner(): - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: Unknown | Literal[1] while True: pass ``` @@ -301,7 +311,24 @@ elif sys.version_info >= (3, 11): elif sys.version_info >= (3, 10): pass else: - pass + # This branch is also unreachable, because the previous `elif` branch is always true + ExceptionGroup # no error here +``` + +And for nested `if` statements: + +```py +def _(flag: bool): + if flag: + if sys.version_info >= (3, 11): + ExceptionGroup # no error here + else: + pass + + if sys.version_info < (3, 11): + pass + else: + ExceptionGroup # no error here ``` The same works for ternary expressions: @@ -346,6 +373,15 @@ def f(): ExceptionGroup ``` +Similarly, assertions with statically-known falsy conditions can lead to unreachable code: + +```py +def f(): + assert sys.version_info > (3, 11) + + ExceptionGroup +``` + Finally, not that anyone would ever use it, but it also works for `while` loops: ```py @@ -353,6 +389,18 @@ while sys.version_info >= (3, 11): ExceptionGroup ``` +### Infinite loops + +We also do not emit diagnostics in unreachable code after an infinite loop: + +```py +def f(): + while True: + pass + + ExceptionGroup # no error here +``` + ### Silencing errors for actually unknown symbols We currently also silence diagnostics for symbols that are not actually defined anywhere. It is @@ -475,33 +523,26 @@ def f(): 1 / 0 # error: [division-by-zero] ``` -## Limitations of the current approach +### Conflicting type information -The current approach of silencing only a subset of diagnostics in unreachable code leads to some -problems, and we may want to re-evaluate this decision in the future. To illustrate, consider the -following example: +We also support cases where type information for symbols conflicts between mutually exclusive +branches: ```py -if False: +import sys + +if sys.version_info >= (3, 11): x: int = 1 else: x: str = "a" -if False: - # TODO We currently emit a false positive here: - # error: [invalid-assignment] "Object of type `Literal["a"]` is not assignable to `int`" +if sys.version_info >= (3, 11): other: int = x else: other: str = x ``` -The problem here originates from the fact that the type of `x` in the `False` branch conflicts with -the visible type of `x` in the `True` branch. When we type-check the lower `False` branch, we only -see the visible definition of `x`, which has a type of `str`. - -In principle, this means that all diagnostics that depend on type information from "outside" the -unreachable section should be silenced. Similar problems to the one above can occur for other rule -types as well: +This is also supported for function calls, attribute accesses, etc.: ```py from typing import Literal @@ -529,24 +570,14 @@ else: number: Literal[0] = 0 if False: - # TODO - # error: [invalid-argument-type] f(2) - # TODO - # error: [unknown-argument] g(a=2, b=3) - # TODO - # error: [invalid-assignment] C.x = 2 d: D = D() - # TODO - # error: [call-non-callable] d() - # TODO - # error: [division-by-zero] 1 / number ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/with/async.md rename to crates/ty_python_semantic/resources/mdtest/with/async.md diff --git a/crates/ty_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md new file mode 100644 index 0000000000000..89d0b281da36e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/with/sync.md @@ -0,0 +1,193 @@ +# With statements + +## Basic `with` statement + +The type of the target variable in a `with` statement is the return type from the context manager's +`__enter__` method. + +```py +class Target: ... + +class Manager: + def __enter__(self) -> Target: + return Target() + + def __exit__(self, exc_type, exc_value, traceback): ... + +with Manager() as f: + reveal_type(f) # revealed: Target +``` + +## Union context manager + +```py +def _(flag: bool): + class Manager1: + def __enter__(self) -> str: + return "foo" + + def __exit__(self, exc_type, exc_value, traceback): ... + + class Manager2: + def __enter__(self) -> int: + return 42 + + def __exit__(self, exc_type, exc_value, traceback): ... + + context_expr = Manager1() if flag else Manager2() + + with context_expr as f: + reveal_type(f) # revealed: str | int +``` + +## Context manager without an `__enter__` or `__exit__` method + +```py +class Manager: ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` + +## Context manager without an `__enter__` method + +```py +class Manager: + def __exit__(self, exc_tpe, exc_value, traceback): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__`" +with Manager(): + ... +``` + +## Context manager without an `__exit__` method + +```py +class Manager: + def __enter__(self): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__exit__`" +with Manager(): + ... +``` + +## Context manager with non-callable `__enter__` attribute + +```py +class Manager: + __enter__: int = 42 + + def __exit__(self, exc_tpe, exc_value, traceback): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" +with Manager(): + ... +``` + +## Context manager with non-callable `__exit__` attribute + +```py +from typing_extensions import Self + +class Manager: + def __enter__(self) -> Self: + return self + __exit__: int = 32 + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`" +with Manager(): + ... +``` + +## Context expression with possibly-unbound union variants + +```py +def _(flag: bool): + class Manager1: + def __enter__(self) -> str: + return "foo" + + def __exit__(self, exc_type, exc_value, traceback): ... + + class NotAContextManager: ... + context_expr = Manager1() if flag else NotAContextManager() + + # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound" + with context_expr as f: + reveal_type(f) # revealed: str +``` + +## Context expression with "sometimes" callable `__enter__` method + +```py +def _(flag: bool): + class Manager: + if flag: + def __enter__(self) -> str: + return "abcd" + + def __exit__(self, *args): ... + + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound" + with Manager() as f: + reveal_type(f) # revealed: str +``` + +## Invalid `__enter__` signature + +```py +class Manager: + def __enter__() -> str: + return "foo" + + def __exit__(self, exc_type, exc_value, traceback): ... + +context_expr = Manager() + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" +with context_expr as f: + reveal_type(f) # revealed: str +``` + +## Accidental use of non-async `with` + + + +If a synchronous `with` statement is used on a type with `__aenter__` and `__aexit__`, we show a +diagnostic hint that the user might have intended to use `asnyc with` instead. + +```py +class Manager: + async def __aenter__(self): ... + async def __aexit__(self, *args): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` + +The sub-diagnostic is also provided if the signatures of `__aenter__` and `__aexit__` do not match +the expected signatures for a context manager: + +```py +class Manager: + async def __aenter__(self): ... + async def __aexit__(self, typ: str, exc, traceback): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` + +Similarly, we also show the hint if the functions have the wrong number of arguments: + +```py +class Manager: + async def __aenter__(self, wrong_extra_arg): ... + async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ... + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" +with Manager(): + ... +``` diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt new file mode 100644 index 0000000000000..8a016e761dc69 --- /dev/null +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -0,0 +1,23 @@ +Tanjun # too many iterations +antidote # hangs / slow (single threaded) +artigraph # cycle panics (value_type_) +arviz # too many iterations on versions of arviz newer than https://github.com/arviz-devs/arviz/commit/3205b82bb4d6097c31f7334d7ac51a6de37002d0 +core # cycle panics (value_type_) +cpython # too many cycle iterations +hydpy # too many iterations +ibis # too many iterations +jax # too many iterations +mypy # too many iterations (self-recursive type alias) +packaging # too many iterations +pandas # slow (9s) +pandera # too many iterations +pip # vendors packaging, see above +pylint # cycle panics (self-recursive type alias) +pyodide # too many cycle iterations +scikit-build-core # too many cycle iterations +setuptools # vendors packaging, see above +spack # slow, success, but mypy-primer hangs processing the output +spark # too many iterations +steam.py # hangs (single threaded) +xarray # too many iterations +zope.interface # https://github.com/astral-sh/ty/issues/764 diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt new file mode 100644 index 0000000000000..f9c0dbd6b54da --- /dev/null +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -0,0 +1,125 @@ +AutoSplit +DateType +Expression +PyGithub +PyWinCtl +SinbadCogs +aiohttp +aiohttp-devtools +aioredis +aiortc +alectryon +alerta +altair +anyio +apprise +async-utils +asynq +attrs +bandersnatch +beartype +bidict +black +bokeh +boostedblob +check-jsonschema +cki-lib +cloud-init +colour +com2ann +comtypes +cwltool +dacite +dd-trace-py +dedupe +discord.py +django-stubs +downforeveryone +dragonchain +dulwich +flake8 +flake8-pyi +freqtrade +git-revise +graphql-core +httpx-caching +hydra-zen +ignite +imagehash +isort +itsdangerous +janus +jinja +koda-validate +kopf +kornia +manticore +materialize +meson +mitmproxy +mkdocs +mkosi +mongo-python-driver +more-itertools +mypy-protobuf +mypy_primer +nionutils +nox +openlibrary +operator +optuna +paasta +pandas-stubs +paroxython +parso +pegen +poetry +porcupine +ppb-vector +prefect +psycopg +pwndbg +pybind11 +pycryptodome +pydantic +pyinstrument +pyjwt +pylox +pyp +pyppeteer +pytest +pytest-robotframework +python-chess +python-htmlgen +python-sop +pywin32 +rclip +rich +rotki +schema_salad +schemathesis +scikit-learn +scipy +scrapy +sockeye +speedrun.com_global_scoreboard_webapp +sphinx +starlette +static-frame +stone +strawberry +streamlit +svcs +sympy +tornado +trio +twine +typeshed-stats +urllib3 +vision +websockets +werkzeug +xarray-dataclasses +yarl +zipp +zulip diff --git a/crates/ty_python_semantic/resources/primer/memory.txt b/crates/ty_python_semantic/resources/primer/memory.txt new file mode 100644 index 0000000000000..ed55f3ea57524 --- /dev/null +++ b/crates/ty_python_semantic/resources/primer/memory.txt @@ -0,0 +1,4 @@ +flake8 +sphinx +prefect +trio diff --git a/crates/ty_python_semantic/src/ast_node_ref.rs b/crates/ty_python_semantic/src/ast_node_ref.rs new file mode 100644 index 0000000000000..c5f7f115cc80c --- /dev/null +++ b/crates/ty_python_semantic/src/ast_node_ref.rs @@ -0,0 +1,138 @@ +use std::fmt::Debug; +use std::marker::PhantomData; + +use ruff_db::parsed::ParsedModuleRef; +use ruff_python_ast::{AnyNodeRef, NodeIndex}; +use ruff_python_ast::{AnyRootNodeRef, HasNodeIndex}; +use ruff_text_size::Ranged; + +/// Reference to an AST node. +/// +/// This type acts as a reference to an AST node within a given module that remains +/// stable regardless of whether the AST is garbage collected. As such, accessing a +/// node through the [`AstNodeRef`] requires a reference to the current [`ParsedModuleRef`] +/// for the module containing the node. +/// +/// ## 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 +/// 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` +/// has the effect that salsa will consider the expression as "unchanged" for as long as it: +/// +/// * belongs to the same file +/// * belongs to the same scope +/// * has the same kind +/// * was created in the same order +/// +/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving +/// us some form of scope-stable identity for expressions. Only queries accessing the node field +/// run on every AST change. All other queries only run when the expression's identity changes. +#[derive(Clone)] +pub struct AstNodeRef { + /// A pointer to the [`ruff_db::parsed::ParsedModule`] that this node was created from. + module_ptr: *const (), + + /// Debug information. + #[cfg(debug_assertions)] + kind: ruff_python_ast::NodeKind, + #[cfg(debug_assertions)] + range: ruff_text_size::TextRange, + + /// The index of the node in the AST. + index: NodeIndex, + + _node: PhantomData, +} + +impl AstNodeRef +where + T: HasNodeIndex + Ranged + PartialEq + Debug, + for<'ast> AnyNodeRef<'ast>: From<&'ast T>, + for<'ast> &'ast T: TryFrom>, +{ + /// Creates a new `AstNodeRef` that references `node`. + /// + /// This method may panic or produce unspecified results if the provided module is from a + /// different file or Salsa revision than the module to which the node belongs. + pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self { + let index = node.node_index().load(); + debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node)); + + Self { + index, + module_ptr: module_ref.module().as_ptr(), + #[cfg(debug_assertions)] + kind: AnyNodeRef::from(node).kind(), + #[cfg(debug_assertions)] + range: node.range(), + _node: PhantomData, + } + } + + /// Returns a reference to the wrapped node. + /// + /// This method may panic or produce unspecified results if the provided module is from a + /// different file or Salsa revision than the module to which the node belongs. + pub fn node<'ast>(&self, module_ref: &'ast ParsedModuleRef) -> &'ast T { + debug_assert_eq!(module_ref.module().as_ptr(), self.module_ptr); + + // Note that the module pointer is guaranteed to be stable within the Salsa + // revision, so the file contents cannot have changed by the above assertion. + module_ref + .get_by_index(self.index) + .try_into() + .ok() + .expect("AST indices should never change within the same revision") + } +} + +impl get_size2::GetSize for AstNodeRef {} + +#[allow(clippy::missing_fields_in_debug)] +impl Debug for AstNodeRef +where + T: Debug, + for<'ast> &'ast T: TryFrom>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(debug_assertions)] + { + f.debug_struct("AstNodeRef") + .field("kind", &self.kind) + .field("range", &self.range) + .finish() + } + + #[cfg(not(debug_assertions))] + { + // Unfortunately we have no access to the AST here. + f.debug_tuple("AstNodeRef").finish_non_exhaustive() + } + } +} + +#[expect(unsafe_code)] +unsafe impl salsa::Update for AstNodeRef { + unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { + let old_ref = unsafe { &mut (*old_pointer) }; + + // Two nodes are guaranteed to be equal as long as they refer to the same node index + // within the same module. Note that the module pointer is guaranteed to be stable + // within the Salsa revision, so the file contents cannot have changed. + if old_ref.module_ptr == new_value.module_ptr && old_ref.index == new_value.index { + false + } else { + *old_ref = new_value; + true + } + } +} + +// SAFETY: The `module_ptr` is only used for pointer equality and never accessed directly. +#[expect(unsafe_code)] +unsafe impl Send for AstNodeRef where T: Send {} +#[expect(unsafe_code)] +unsafe impl Sync for AstNodeRef where T: Sync {} diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs new file mode 100644 index 0000000000000..815c37653cbcf --- /dev/null +++ b/crates/ty_python_semantic/src/db.rs @@ -0,0 +1,195 @@ +use crate::lint::{LintRegistry, RuleSelection}; +use ruff_db::Db as SourceDb; +use ruff_db::files::File; + +/// Database giving access to semantic information about a Python program. +#[salsa::db] +pub trait Db: SourceDb { + fn is_file_open(&self, file: File) -> bool; + + /// Resolves the rule selection for a given file. + fn rule_selection(&self, file: File) -> &RuleSelection; + + fn lint_registry(&self) -> &LintRegistry; +} + +#[cfg(test)] +pub(crate) mod tests { + use std::sync::{Arc, Mutex}; + + use crate::program::{Program, SearchPathSettings}; + use crate::{ + ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource, + default_lint_registry, + }; + + use super::Db; + use crate::lint::{LintRegistry, RuleSelection}; + use anyhow::Context; + use ruff_db::Db as SourceDb; + use ruff_db::files::{File, Files}; + use ruff_db::system::{ + DbWithTestSystem, DbWithWritableSystem as _, System, SystemPath, SystemPathBuf, TestSystem, + }; + use ruff_db::vendored::VendoredFileSystem; + use ruff_python_ast::PythonVersion; + + type Events = Arc>>; + + #[salsa::db] + #[derive(Clone)] + pub(crate) struct TestDb { + storage: salsa::Storage, + files: Files, + system: TestSystem, + vendored: VendoredFileSystem, + events: Events, + rule_selection: Arc, + } + + impl TestDb { + pub(crate) fn new() -> Self { + let events = Events::default(); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let events = events.clone(); + move |event| { + tracing::trace!("event: {event:?}"); + let mut events = events.lock().unwrap(); + events.push(event); + } + }))), + system: TestSystem::default(), + vendored: ty_vendored::file_system().clone(), + events, + files: Files::default(), + rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), + } + } + + /// Takes the salsa events. + pub(crate) fn take_salsa_events(&mut self) -> Vec { + let mut events = self.events.lock().unwrap(); + + std::mem::take(&mut *events) + } + + /// Clears the salsa events. + /// + /// ## Panics + /// If there are any pending salsa snapshots. + pub(crate) fn clear_salsa_events(&mut self) { + self.take_salsa_events(); + } + } + + impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } + } + + #[salsa::db] + impl SourceDb for TestDb { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } + } + + #[salsa::db] + impl Db for TestDb { + fn is_file_open(&self, file: File) -> bool { + !file.path(self).is_vendored_path() + } + + fn rule_selection(&self, _file: File) -> &RuleSelection { + &self.rule_selection + } + + fn lint_registry(&self) -> &LintRegistry { + default_lint_registry() + } + } + + #[salsa::db] + impl salsa::Database for TestDb {} + + pub(crate) struct TestDbBuilder<'a> { + /// Target Python version + python_version: PythonVersion, + /// Target Python platform + python_platform: PythonPlatform, + /// Path and content pairs for files that should be present + files: Vec<(&'a str, &'a str)>, + } + + impl<'a> TestDbBuilder<'a> { + pub(crate) fn new() -> Self { + Self { + python_version: PythonVersion::default(), + python_platform: PythonPlatform::default(), + files: vec![], + } + } + + pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self { + self.python_version = version; + self + } + + pub(crate) fn with_file( + mut self, + path: &'a (impl AsRef + ?Sized), + content: &'a str, + ) -> Self { + self.files.push((path.as_ref().as_str(), content)); + self + } + + pub(crate) fn build(self) -> anyhow::Result { + let mut db = TestDb::new(); + + let src_root = SystemPathBuf::from("/src"); + db.memory_file_system().create_directory_all(&src_root)?; + + db.write_files(self.files) + .context("Failed to write test files")?; + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: self.python_version, + source: PythonVersionSource::default(), + }, + python_platform: self.python_platform, + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored()) + .context("Invalid search path settings")?, + }, + ); + + Ok(db) + } + } + + pub(crate) fn setup_db() -> TestDb { + TestDbBuilder::new().build().expect("valid TestDb setup") + } +} diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs new file mode 100644 index 0000000000000..caf71b0a4dc6c --- /dev/null +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -0,0 +1,456 @@ +use rustc_hash::FxHashSet; + +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::name::Name; +use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; +use ruff_python_ast::{self as ast}; + +use crate::semantic_index::{SemanticIndex, semantic_index}; +use crate::types::{Truthiness, Type, infer_expression_types}; +use crate::{Db, ModuleName, resolve_module}; + +#[allow(clippy::ref_option)] +fn dunder_all_names_cycle_recover( + _db: &dyn Db, + _value: &Option>, + _count: u32, + _file: File, +) -> salsa::CycleRecoveryAction>> { + salsa::CycleRecoveryAction::Iterate +} + +fn dunder_all_names_cycle_initial(_db: &dyn Db, _file: File) -> Option> { + None +} + +/// Returns a set of names in the `__all__` variable for `file`, [`None`] if it is not defined or +/// if it contains invalid elements. +#[salsa::tracked(returns(as_ref), cycle_fn=dunder_all_names_cycle_recover, cycle_initial=dunder_all_names_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option> { + let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered(); + + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let mut collector = DunderAllNamesCollector::new(db, file, index); + collector.visit_body(module.suite()); + collector.into_names() +} + +/// A visitor that collects the names in the `__all__` variable of a module. +struct DunderAllNamesCollector<'db> { + db: &'db dyn Db, + file: File, + + /// The semantic index for the module. + index: &'db SemanticIndex<'db>, + + /// The origin of the `__all__` variable in the current module, [`None`] if it is not defined. + origin: Option, + + /// A flag indicating whether the module uses unrecognized `__all__` idioms or there are any + /// invalid elements in `__all__`. + invalid: bool, + + /// A set of names found in `__all__` for the current module. + names: FxHashSet, +} + +impl<'db> DunderAllNamesCollector<'db> { + fn new(db: &'db dyn Db, file: File, index: &'db SemanticIndex<'db>) -> Self { + Self { + db, + file, + index, + origin: None, + invalid: false, + names: FxHashSet::default(), + } + } + + /// Updates the origin of `__all__` in the current module. + /// + /// This will clear existing names if the origin is changed to mimic the behavior of overriding + /// `__all__` in the current module. + fn update_origin(&mut self, origin: DunderAllOrigin) { + if self.origin.is_some() { + self.names.clear(); + } + self.origin = Some(origin); + } + + /// Extends the current set of names with the names from the given expression which can be + /// either a list/tuple/set of string-literal names or a module's `__all__` variable. + /// + /// Returns `true` if the expression is a valid list/tuple/set or module `__all__`, `false` otherwise. + fn extend(&mut self, expr: &ast::Expr) -> bool { + match expr { + // `__all__ += [...]` + // `__all__.extend([...])` + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) + | ast::Expr::Set(ast::ExprSet { elts, .. }) => self.add_names(elts), + + // `__all__ += module.__all__` + // `__all__.extend(module.__all__)` + ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { + if attr != "__all__" { + return false; + } + let Type::ModuleLiteral(module_literal) = self.standalone_expression_type(value) + else { + return false; + }; + let Some(module_dunder_all_names) = module_literal + .module(self.db) + .file() + .and_then(|file| dunder_all_names(self.db, file)) + else { + // The module either does not have a `__all__` variable or it is invalid. + return false; + }; + self.names.extend(module_dunder_all_names.iter().cloned()); + true + } + + _ => false, + } + } + + /// Processes a call idiom for `__all__` and updates the set of names accordingly. + /// + /// Returns `true` if the call idiom is recognized and valid, `false` otherwise. + fn process_call_idiom( + &mut self, + function_name: &ast::Identifier, + arguments: &ast::Arguments, + ) -> bool { + if arguments.len() != 1 { + return false; + } + let Some(argument) = arguments.find_positional(0) else { + return false; + }; + match function_name.as_str() { + // `__all__.extend([...])` + // `__all__.extend(module.__all__)` + "extend" => { + if !self.extend(argument) { + return false; + } + } + + // `__all__.append(...)` + "append" => { + let Some(name) = create_name(argument) else { + return false; + }; + self.names.insert(name); + } + + // `__all__.remove(...)` + "remove" => { + let Some(name) = create_name(argument) else { + return false; + }; + self.names.remove(&name); + } + + _ => return false, + } + true + } + + /// Returns the names in `__all__` from the module imported from the given `import_from` + /// statement. + /// + /// Returns [`None`] if module resolution fails, invalid syntax, or if the module does not have + /// a `__all__` variable. + fn dunder_all_names_for_import_from( + &self, + import_from: &ast::StmtImportFrom, + ) -> Option<&'db FxHashSet> { + let module_name = + ModuleName::from_import_statement(self.db, self.file, import_from).ok()?; + let module = resolve_module(self.db, &module_name)?; + dunder_all_names(self.db, module.file()?) + } + + /// Infer the type of a standalone expression. + /// + /// # Panics + /// + /// This function panics if `expr` was not marked as a standalone expression during semantic indexing. + fn standalone_expression_type(&self, expr: &ast::Expr) -> Type<'db> { + infer_expression_types(self.db, self.index.expression(expr)).expression_type(expr) + } + + /// Evaluate the given expression and return its truthiness. + /// + /// Returns [`None`] if the expression type doesn't implement `__bool__` correctly. + fn evaluate_test_expr(&self, expr: &ast::Expr) -> Option { + self.standalone_expression_type(expr).try_bool(self.db).ok() + } + + /// Add valid names to the set. + /// + /// Returns `false` if any of the names are invalid. + fn add_names(&mut self, exprs: &[ast::Expr]) -> bool { + for expr in exprs { + let Some(name) = create_name(expr) else { + return false; + }; + self.names.insert(name); + } + true + } + + /// Consumes `self` and returns the collected set of names. + /// + /// Returns [`None`] if `__all__` is not defined in the current module or if it contains + /// invalid elements. + fn into_names(self) -> Option> { + if self.origin.is_none() { + None + } else if self.invalid { + tracing::debug!("Invalid `__all__` in `{}`", self.file.path(self.db)); + None + } else { + Some(self.names) + } + } +} + +impl<'db> StatementVisitor<'db> for DunderAllNamesCollector<'db> { + fn visit_stmt(&mut self, stmt: &'db ast::Stmt) { + if self.invalid { + return; + } + + match stmt { + ast::Stmt::ImportFrom(import_from @ ast::StmtImportFrom { names, .. }) => { + for ast::Alias { name, asname, .. } in names { + // `from module import *` where `module` is a module with a top-level `__all__` + // variable that contains the "__all__" element. + if name == "*" { + // Here, we need to use the `dunder_all_names` query instead of the + // `exported_names` query because a `*`-import does not import the + // `__all__` attribute unless it is explicitly included in the `__all__` of + // the module. + let Some(all_names) = self.dunder_all_names_for_import_from(import_from) + else { + self.invalid = true; + continue; + }; + + if all_names.contains(&Name::new_static("__all__")) { + self.update_origin(DunderAllOrigin::StarImport); + self.names.extend(all_names.iter().cloned()); + } + } else { + // `from module import __all__` + // `from module import __all__ as __all__` + if name != "__all__" + || asname.as_ref().is_some_and(|asname| asname != "__all__") + { + continue; + } + + // We could do the `__all__` lookup lazily in case it's not needed. This would + // happen if a `__all__` is imported from another module but then the module + // redefines it. For example: + // + // ```python + // from module import __all__ as __all__ + // + // __all__ = ["a", "b"] + // ``` + // + // I'm avoiding this for now because it doesn't seem likely to happen in + // practice. + let Some(all_names) = self.dunder_all_names_for_import_from(import_from) + else { + self.invalid = true; + continue; + }; + + self.update_origin(DunderAllOrigin::ExternalModule); + self.names.extend(all_names.iter().cloned()); + } + } + } + + ast::Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { + let [target] = targets.as_slice() else { + return; + }; + if !is_dunder_all(target) { + return; + } + match &**value { + // `__all__ = [...]` + // `__all__ = (...)` + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + self.update_origin(DunderAllOrigin::CurrentModule); + if !self.add_names(elts) { + self.invalid = true; + } + } + _ => { + self.invalid = true; + } + } + } + + ast::Stmt::AugAssign(ast::StmtAugAssign { + target, + op: ast::Operator::Add, + value, + .. + }) => { + if self.origin.is_none() { + // We can't update `__all__` if it doesn't already exist. + return; + } + if !is_dunder_all(target) { + return; + } + if !self.extend(value) { + self.invalid = true; + } + } + + ast::Stmt::AnnAssign(ast::StmtAnnAssign { + target, + value: Some(value), + .. + }) => { + if !is_dunder_all(target) { + return; + } + match &**value { + // `__all__: list[str] = [...]` + // `__all__: tuple[str, ...] = (...)` + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + self.update_origin(DunderAllOrigin::CurrentModule); + if !self.add_names(elts) { + self.invalid = true; + } + } + _ => { + self.invalid = true; + } + } + } + + ast::Stmt::Expr(ast::StmtExpr { value: expr, .. }) => { + if self.origin.is_none() { + // We can't update `__all__` if it doesn't already exist. + return; + } + let Some(ast::ExprCall { + func, arguments, .. + }) = expr.as_call_expr() + else { + return; + }; + let Some(ast::ExprAttribute { + value, + attr, + ctx: ast::ExprContext::Load, + .. + }) = func.as_attribute_expr() + else { + return; + }; + if !is_dunder_all(value) { + return; + } + if !self.process_call_idiom(attr, arguments) { + self.invalid = true; + } + } + + ast::Stmt::If(ast::StmtIf { + test, + body, + elif_else_clauses, + .. + }) => match self.evaluate_test_expr(test) { + Some(Truthiness::AlwaysTrue) => self.visit_body(body), + Some(Truthiness::AlwaysFalse) => { + for ast::ElifElseClause { test, body, .. } in elif_else_clauses { + if let Some(test) = test { + match self.evaluate_test_expr(test) { + Some(Truthiness::AlwaysTrue) => { + self.visit_body(body); + break; + } + Some(Truthiness::AlwaysFalse) => {} + Some(Truthiness::Ambiguous) | None => { + break; + } + } + } else { + self.visit_body(body); + } + } + } + Some(Truthiness::Ambiguous) | None => {} + }, + + ast::Stmt::For(..) + | ast::Stmt::While(..) + | ast::Stmt::With(..) + | ast::Stmt::Match(..) + | ast::Stmt::Try(..) => { + walk_stmt(self, stmt); + } + + ast::Stmt::FunctionDef(..) | ast::Stmt::ClassDef(..) => { + // Avoid recursing into any nested scopes as `__all__` is only valid at the module + // level. + } + + ast::Stmt::AugAssign(..) + | ast::Stmt::AnnAssign(..) + | ast::Stmt::Delete(..) + | ast::Stmt::Return(..) + | ast::Stmt::Raise(..) + | ast::Stmt::Assert(..) + | ast::Stmt::Import(..) + | ast::Stmt::Global(..) + | ast::Stmt::Nonlocal(..) + | ast::Stmt::TypeAlias(..) + | ast::Stmt::Pass(..) + | ast::Stmt::Break(..) + | ast::Stmt::Continue(..) + | ast::Stmt::IpyEscapeCommand(..) => {} + } + } +} + +#[derive(Debug, Clone)] +enum DunderAllOrigin { + /// The `__all__` variable is defined in the current module. + CurrentModule, + + /// The `__all__` variable is imported from another module. + ExternalModule, + + /// The `__all__` variable is imported from a module via a `*`-import. + StarImport, +} + +/// Checks if the given expression is a name expression for `__all__`. +fn is_dunder_all(expr: &ast::Expr) -> bool { + matches!(expr, ast::Expr::Name(ast::ExprName { id, .. }) if id == "__all__") +} + +/// Create and return a [`Name`] from the given expression, [`None`] if it is an invalid expression +/// for a `__all__` element. +fn create_name(expr: &ast::Expr) -> Option { + Some(Name::new(expr.as_string_literal_expr()?.value.to_str())) +} diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs new file mode 100644 index 0000000000000..007abd253d7dc --- /dev/null +++ b/crates/ty_python_semantic/src/lib.rs @@ -0,0 +1,65 @@ +use std::hash::BuildHasherDefault; + +use rustc_hash::FxHasher; + +use crate::lint::{LintRegistry, LintRegistryBuilder}; +use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COMMENT}; +pub use db::Db; +pub use module_name::ModuleName; +pub use module_resolver::{ + KnownModule, Module, SearchPathValidationError, SearchPaths, resolve_module, + system_module_search_paths, +}; +pub use program::{ + Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource, + PythonVersionWithSource, SearchPathSettings, +}; +pub use python_platform::PythonPlatform; +pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel}; +pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; +pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic; + +pub mod ast_node_ref; +mod db; +mod dunder_all; +pub mod lint; +pub(crate) mod list; +mod module_name; +mod module_resolver; +mod node_key; +pub(crate) mod place; +mod program; +mod python_platform; +pub mod semantic_index; +mod semantic_model; +pub(crate) mod site_packages; +mod suppression; +pub mod types; +mod unpack; +mod util; + +#[cfg(feature = "testing")] +pub mod pull_types; + +type FxOrderSet = ordermap::set::OrderSet>; +type FxIndexMap = indexmap::IndexMap>; +type FxIndexSet = indexmap::IndexSet>; + +/// Returns the default registry with all known semantic lints. +pub fn default_lint_registry() -> &'static LintRegistry { + static REGISTRY: std::sync::LazyLock = std::sync::LazyLock::new(|| { + let mut registry = LintRegistryBuilder::default(); + register_lints(&mut registry); + registry.build() + }); + + ®ISTRY +} + +/// Register all known semantic lints. +pub fn register_lints(registry: &mut LintRegistryBuilder) { + types::register_lints(registry); + registry.register_lint(&UNUSED_IGNORE_COMMENT); + registry.register_lint(&UNKNOWN_RULE); + registry.register_lint(&INVALID_IGNORE_COMMENT); +} diff --git a/crates/red_knot_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs similarity index 92% rename from crates/red_knot_python_semantic/src/lint.rs rename to crates/ty_python_semantic/src/lint.rs index 0e229536664a4..0800ee59833c2 100644 --- a/crates/red_knot_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -32,7 +32,7 @@ pub struct LintMetadata { pub line: u32, } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -210,8 +210,8 @@ impl LintStatus { /// Declares a lint rule with the given metadata. /// /// ```rust -/// use red_knot_python_semantic::declare_lint; -/// use red_knot_python_semantic::lint::{LintStatus, Level}; +/// use ty_python_semantic::declare_lint; +/// use ty_python_semantic::lint::{LintStatus, Level}; /// /// declare_lint! { /// /// ## What it does @@ -244,7 +244,7 @@ macro_rules! declare_lint { } ) => { $( #[doc = $doc] )+ - #[allow(clippy::needless_update)] + #[expect(clippy::needless_update)] $vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata { name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)), summary: $summary, @@ -262,7 +262,7 @@ macro_rules! declare_lint { /// /// Implements `PartialEq`, `Eq`, and `Hash` based on the `LintMetadata` pointer /// for fast comparison and lookup. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, get_size2::GetSize)] pub struct LintId { definition: &'static LintMetadata, } @@ -415,7 +415,7 @@ impl LintRegistry { } } -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub enum GetLintError { /// The name maps to this removed lint. #[error("lint `{0}` has been removed")] @@ -463,7 +463,7 @@ impl From<&'static LintMetadata> for LintEntry { } } -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, get_size2::GetSize)] pub struct RuleSelection { /// Map with the severity for each enabled lint rule. /// @@ -475,12 +475,26 @@ impl RuleSelection { /// Creates a new rule selection from all known lints in the registry that are enabled /// according to their default severity. pub fn from_registry(registry: &LintRegistry) -> Self { + Self::from_registry_with_default(registry, None) + } + + /// Creates a new rule selection from all known lints in the registry, including lints that are default by default. + /// Lints that are disabled by default use the `default_severity`. + pub fn all(registry: &LintRegistry, default_severity: Severity) -> Self { + Self::from_registry_with_default(registry, Some(default_severity)) + } + + fn from_registry_with_default( + registry: &LintRegistry, + default_severity: Option, + ) -> Self { let lints = registry .lints() .iter() .filter_map(|lint| { Severity::try_from(lint.default_level()) .ok() + .or(default_severity) .map(|severity| (*lint, (severity, LintSource::Default))) }) .collect(); @@ -505,6 +519,10 @@ impl RuleSelection { self.lints.get(&lint).map(|(severity, _)| *severity) } + pub fn get(&self, lint: LintId) -> Option<(Severity, LintSource)> { + self.lints.get(&lint).copied() + } + /// Returns `true` if the `lint` is enabled. pub fn is_enabled(&self, lint: LintId) -> bool { self.severity(lint).is_some() @@ -523,7 +541,7 @@ impl RuleSelection { } } -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, get_size2::GetSize)] pub enum LintSource { /// The user didn't enable the rule explicitly, instead it's enabled by default. #[default] diff --git a/crates/red_knot_python_semantic/src/list.rs b/crates/ty_python_semantic/src/list.rs similarity index 97% rename from crates/red_knot_python_semantic/src/list.rs rename to crates/ty_python_semantic/src/list.rs index fe6e55b2f0c26..093d6e6376f7c 100644 --- a/crates/red_knot_python_semantic/src/list.rs +++ b/crates/ty_python_semantic/src/list.rs @@ -65,11 +65,11 @@ use std::cmp::Ordering; use std::marker::PhantomData; use std::ops::Deref; -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{IndexVec, newtype_index}; /// A handle to an association list. Use [`ListStorage`] to access its elements, and /// [`ListBuilder`] to construct other lists based on this one. -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, get_size2::GetSize)] pub(crate) struct List { last: Option, _phantom: PhantomData<(K, V)>, @@ -95,12 +95,12 @@ impl Default for List { } #[newtype_index] -#[derive(PartialOrd, Ord)] +#[derive(PartialOrd, Ord, get_size2::GetSize)] struct ListCellId; /// Stores one or more association lists. This type provides read-only access to the lists. Use a /// [`ListBuilder`] to create lists. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, get_size2::GetSize)] pub(crate) struct ListStorage { cells: IndexVec>, } @@ -111,7 +111,7 @@ pub(crate) struct ListStorage { /// **Terminology**: The elements of a cons cell are usually called `head` and `tail` (assuming /// you're not in Lisp-land, where they're called `car` and `cdr`). The elements of a snoc cell /// are usually called `rest` and `last`. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, get_size2::GetSize)] struct ListCell { rest: Option, key: K, @@ -180,7 +180,7 @@ impl ListBuilder { /// as our return type, since we never return `None`. However, for consistency with our other /// methods, we always use `Option` as the return type for any method that can return a /// list. - #[allow(clippy::unnecessary_wraps)] + #[expect(clippy::unnecessary_wraps)] fn add_cell(&mut self, rest: Option, key: K, value: V) -> Option { Some(self.storage.cells.push(ListCell { rest, key, value })) } @@ -319,7 +319,7 @@ impl ListBuilder { /// Returns the intersection of two lists. The result will contain an entry for any key that /// appears in both lists. The corresponding values will be combined using the `combine` /// function that you provide. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub(crate) fn intersect_with( &mut self, a: List, @@ -372,7 +372,7 @@ impl ListBuilder { impl ListStorage { /// Iterates through the elements in a set _in reverse order_. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub(crate) fn iter_set_reverse(&self, set: List) -> ListSetReverseIterator { ListSetReverseIterator { storage: self, @@ -513,7 +513,7 @@ mod tests { impl ListStorage { /// Iterates through the entries in a list _in reverse order by key_. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub(crate) fn iter_reverse(&self, list: List) -> ListReverseIterator<'_, K, V> { ListReverseIterator { storage: self, @@ -649,7 +649,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_set_from_vec(elements: Vec) -> bool { let mut builder = ListBuilder::default(); let set = builder.set_from_elements(&elements); @@ -660,7 +660,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_set_intersection(a_elements: Vec, b_elements: Vec) -> bool { let mut builder = ListBuilder::default(); let a = builder.set_from_elements(&a_elements); @@ -712,7 +712,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_list_from_vec(pairs: Vec<(u16, u16)>) -> bool { let mut builder = ListBuilder::default(); let list = builder.set_from_pairs(&pairs); @@ -723,7 +723,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_list_intersection( a_elements: Vec<(u16, u16)>, b_elements: Vec<(u16, u16)>, diff --git a/crates/red_knot_python_semantic/src/module_name.rs b/crates/ty_python_semantic/src/module_name.rs similarity index 76% rename from crates/red_knot_python_semantic/src/module_name.rs rename to crates/ty_python_semantic/src/module_name.rs index 9d08cec93ef9b..eb6ec828de2b4 100644 --- a/crates/red_knot_python_semantic/src/module_name.rs +++ b/crates/ty_python_semantic/src/module_name.rs @@ -13,7 +13,7 @@ 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`). -#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize)] pub struct ModuleName(compact_str::CompactString); impl ModuleName { @@ -47,7 +47,7 @@ impl ModuleName { /// ## Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(ModuleName::new_static("foo.bar").as_deref(), Some("foo.bar")); /// assert_eq!(ModuleName::new_static(""), None); @@ -73,7 +73,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().components().collect::>(), vec!["foo", "bar", "baz"]); /// ``` @@ -87,7 +87,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(ModuleName::new_static("foo.bar").unwrap().parent(), Some(ModuleName::new_static("foo").unwrap())); /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().parent(), Some(ModuleName::new_static("foo.bar").unwrap())); @@ -106,7 +106,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert!(ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap())); /// @@ -127,6 +127,67 @@ impl ModuleName { true } + /// Given a parent module name of this module name, return the relative + /// portion of this module name. + /// + /// For example, a parent module name of `importlib` with this module name + /// as `importlib.resources`, this returns `resources`. + /// + /// If `parent` isn't a parent name of this module name, then this returns + /// `None`. + /// + /// # Examples + /// + /// This example shows some cases where `parent` is an actual parent of the + /// module name: + /// + /// ``` + /// use ty_python_semantic::ModuleName; + /// + /// let this = ModuleName::new_static("importlib.resources").unwrap(); + /// let parent = ModuleName::new_static("importlib").unwrap(); + /// assert_eq!(this.relative_to(&parent), ModuleName::new_static("resources")); + /// + /// let this = ModuleName::new_static("foo.bar.baz.quux").unwrap(); + /// let parent = ModuleName::new_static("foo.bar").unwrap(); + /// assert_eq!(this.relative_to(&parent), ModuleName::new_static("baz.quux")); + /// ``` + /// + /// This shows some cases where it isn't a parent: + /// + /// ``` + /// use ty_python_semantic::ModuleName; + /// + /// let this = ModuleName::new_static("importliblib.resources").unwrap(); + /// let parent = ModuleName::new_static("importlib").unwrap(); + /// assert_eq!(this.relative_to(&parent), None); + /// + /// let this = ModuleName::new_static("foo.bar.baz.quux").unwrap(); + /// let parent = ModuleName::new_static("foo.barbaz").unwrap(); + /// assert_eq!(this.relative_to(&parent), None); + /// + /// let this = ModuleName::new_static("importlibbbbb.resources").unwrap(); + /// let parent = ModuleName::new_static("importlib").unwrap(); + /// assert_eq!(this.relative_to(&parent), None); + /// ``` + #[must_use] + pub fn relative_to(&self, parent: &ModuleName) -> Option { + let relative_name = self.0.strip_prefix(&*parent.0)?.strip_prefix('.')?; + // At this point, `relative_name` *has* to be a + // proper suffix of `self`. Otherwise, one of the two + // `strip_prefix` calls above would return `None`. + // (Notably, a valid `ModuleName` cannot end with a `.`.) + assert!(!relative_name.is_empty()); + // This must also be true for this implementation to be + // correct. That is, the parent must be a prefix of this + // module name according to the rules of how module name + // components are split up. This could technically trip if + // the implementation of `starts_with` diverges from the + // implementation in this routine. But that seems unlikely. + debug_assert!(self.starts_with(parent)); + Some(ModuleName(CompactString::from(relative_name))) + } + #[must_use] #[inline] pub fn as_str(&self) -> &str { @@ -138,7 +199,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(&*ModuleName::from_components(["a"]).unwrap(), "a"); /// assert_eq!(&*ModuleName::from_components(["a", "b"]).unwrap(), "a.b"); @@ -179,7 +240,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// let mut module_name = ModuleName::new_static("foo").unwrap(); /// module_name.extend(&ModuleName::new_static("bar").unwrap()); @@ -197,7 +258,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!( /// ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::>(), @@ -222,6 +283,7 @@ impl ModuleName { level, names: _, range: _, + node_index: _, } = node; let module = module.as_deref(); diff --git a/crates/ty_python_semantic/src/module_resolver/mod.rs b/crates/ty_python_semantic/src/module_resolver/mod.rs new file mode 100644 index 0000000000000..b041ea14abc54 --- /dev/null +++ b/crates/ty_python_semantic/src/module_resolver/mod.rs @@ -0,0 +1,47 @@ +use std::iter::FusedIterator; + +pub use module::{KnownModule, Module}; +pub use path::SearchPathValidationError; +pub use resolver::SearchPaths; +pub(crate) use resolver::file_to_module; +pub use resolver::resolve_module; +use ruff_db::system::SystemPath; + +use crate::Db; +use crate::module_resolver::resolver::search_paths; +use resolver::SearchPathIterator; + +mod module; +mod path; +mod resolver; +mod typeshed; + +#[cfg(test)] +mod testing; + +/// Returns an iterator over all search paths pointing to a system path +pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter { + SystemModuleSearchPathsIter { + inner: search_paths(db), + } +} + +pub struct SystemModuleSearchPathsIter<'db> { + inner: SearchPathIterator<'db>, +} + +impl<'db> Iterator for SystemModuleSearchPathsIter<'db> { + type Item = &'db SystemPath; + + fn next(&mut self) -> Option { + loop { + let next = self.inner.next()?; + + if let Some(system_path) = next.as_system_path() { + return Some(system_path); + } + } + } +} + +impl FusedIterator for SystemModuleSearchPathsIter<'_> {} diff --git a/crates/ty_python_semantic/src/module_resolver/module.rs b/crates/ty_python_semantic/src/module_resolver/module.rs new file mode 100644 index 0000000000000..9241095703750 --- /dev/null +++ b/crates/ty_python_semantic/src/module_resolver/module.rs @@ -0,0 +1,252 @@ +use std::fmt::Formatter; +use std::str::FromStr; +use std::sync::Arc; + +use ruff_db::files::File; + +use super::path::SearchPath; +use crate::module_name::ModuleName; + +/// Representation of a Python module. +#[derive(Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub struct Module { + inner: Arc, +} + +impl Module { + pub(crate) fn file_module( + name: ModuleName, + kind: ModuleKind, + search_path: SearchPath, + file: File, + ) -> Self { + let known = KnownModule::try_from_search_path_and_name(&search_path, &name); + + Self { + inner: Arc::new(ModuleInner::FileModule { + name, + kind, + search_path, + file, + known, + }), + } + } + + pub(crate) fn namespace_package(name: ModuleName) -> Self { + Self { + inner: Arc::new(ModuleInner::NamespacePackage { name }), + } + } + + /// The absolute name of the module (e.g. `foo.bar`) + pub fn name(&self) -> &ModuleName { + match &*self.inner { + ModuleInner::FileModule { name, .. } => name, + ModuleInner::NamespacePackage { name, .. } => name, + } + } + + /// The file to the source code that defines this module + /// + /// This is `None` for namespace packages. + pub fn file(&self) -> Option { + match &*self.inner { + ModuleInner::FileModule { file, .. } => Some(*file), + ModuleInner::NamespacePackage { .. } => None, + } + } + + /// Is this a module that we special-case somehow? If so, which one? + pub fn known(&self) -> Option { + match &*self.inner { + ModuleInner::FileModule { known, .. } => *known, + ModuleInner::NamespacePackage { .. } => None, + } + } + + /// Does this module represent the given known module? + pub fn is_known(&self, known_module: KnownModule) -> bool { + self.known() == Some(known_module) + } + + /// The search path from which the module was resolved. + pub(crate) fn search_path(&self) -> Option<&SearchPath> { + match &*self.inner { + ModuleInner::FileModule { search_path, .. } => Some(search_path), + ModuleInner::NamespacePackage { .. } => None, + } + } + + /// Determine whether this module is a single-file module or a package + pub fn kind(&self) -> ModuleKind { + match &*self.inner { + ModuleInner::FileModule { kind, .. } => *kind, + ModuleInner::NamespacePackage { .. } => ModuleKind::Package, + } + } +} + +impl std::fmt::Debug for Module { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Module") + .field("name", &self.name()) + .field("kind", &self.kind()) + .field("file", &self.file()) + .field("search_path", &self.search_path()) + .field("known", &self.known()) + .finish() + } +} + +#[derive(PartialEq, Eq, Hash, get_size2::GetSize)] +enum ModuleInner { + /// A module that resolves to a file (`lib.py` or `package/__init__.py`) + FileModule { + name: ModuleName, + kind: ModuleKind, + search_path: SearchPath, + file: File, + known: Option, + }, + + /// A namespace package. Namespace packages are special because + /// there are multiple possible paths and they have no corresponding + /// code file. + NamespacePackage { name: ModuleName }, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] +pub enum ModuleKind { + /// A single-file module (e.g. `foo.py` or `foo.pyi`) + Module, + + /// A python package (`foo/__init__.py` or `foo/__init__.pyi`) + Package, +} + +impl ModuleKind { + pub const fn is_package(self) -> bool { + matches!(self, ModuleKind::Package) + } + pub const fn is_module(self) -> bool { + matches!(self, ModuleKind::Module) + } +} + +/// Enumeration of various core stdlib modules in which important types are located +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum_macros::EnumString, get_size2::GetSize)] +#[cfg_attr(test, derive(strum_macros::EnumIter))] +#[strum(serialize_all = "snake_case")] +pub enum KnownModule { + Builtins, + Enum, + Types, + #[strum(serialize = "_typeshed")] + Typeshed, + TypingExtensions, + Typing, + Sys, + Abc, + Dataclasses, + Collections, + Inspect, + #[strum(serialize = "_typeshed._type_checker_internals")] + TypeCheckerInternals, + TyExtensions, + #[strum(serialize = "importlib")] + ImportLib, + #[cfg(test)] + #[strum(serialize = "unittest.mock")] + UnittestMock, +} + +impl KnownModule { + pub const fn as_str(self) -> &'static str { + match self { + Self::Builtins => "builtins", + Self::Enum => "enum", + Self::Types => "types", + Self::Typing => "typing", + Self::Typeshed => "_typeshed", + Self::TypingExtensions => "typing_extensions", + Self::Sys => "sys", + Self::Abc => "abc", + Self::Dataclasses => "dataclasses", + Self::Collections => "collections", + Self::Inspect => "inspect", + Self::TypeCheckerInternals => "_typeshed._type_checker_internals", + Self::TyExtensions => "ty_extensions", + Self::ImportLib => "importlib", + #[cfg(test)] + Self::UnittestMock => "unittest.mock", + } + } + + pub fn name(self) -> ModuleName { + ModuleName::new_static(self.as_str()) + .unwrap_or_else(|| panic!("{self} should be a valid module name!")) + } + + pub(crate) fn try_from_search_path_and_name( + search_path: &SearchPath, + name: &ModuleName, + ) -> Option { + if search_path.is_standard_library() { + Self::from_str(name.as_str()).ok() + } else { + None + } + } + + pub const fn is_builtins(self) -> bool { + matches!(self, Self::Builtins) + } + + pub const fn is_typing(self) -> bool { + matches!(self, Self::Typing) + } + + pub const fn is_ty_extensions(self) -> bool { + matches!(self, Self::TyExtensions) + } + + pub const fn is_inspect(self) -> bool { + matches!(self, Self::Inspect) + } + + pub const fn is_enum(self) -> bool { + matches!(self, Self::Enum) + } + + pub const fn is_importlib(self) -> bool { + matches!(self, Self::ImportLib) + } +} + +impl std::fmt::Display for KnownModule { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use strum::IntoEnumIterator; + + #[test] + fn known_module_roundtrip_from_str() { + let stdlib_search_path = SearchPath::vendored_stdlib(); + + for module in KnownModule::iter() { + let module_name = module.name(); + + assert_eq!( + KnownModule::try_from_search_path_and_name(&stdlib_search_path, &module_name), + Some(module), + "The strum `EnumString` implementation appears to be incorrect for `{module_name}`" + ); + } + } +} diff --git a/crates/red_knot_python_semantic/src/module_resolver/path.rs b/crates/ty_python_semantic/src/module_resolver/path.rs similarity index 91% rename from crates/red_knot_python_semantic/src/module_resolver/path.rs rename to crates/ty_python_semantic/src/module_resolver/path.rs index 6fa67f1a1825d..bb451b36301ff 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/path.rs +++ b/crates/ty_python_semantic/src/module_resolver/path.rs @@ -4,13 +4,11 @@ use std::fmt; use std::sync::Arc; use camino::{Utf8Path, Utf8PathBuf}; - -use ruff_db::files::{system_path_to_file, vendored_path_to_file, File, FileError}; +use ruff_db::files::{File, FileError, system_path_to_file, vendored_path_to_file}; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::vendored::{VendoredPath, VendoredPathBuf}; -use super::typeshed::{typeshed_versions, TypeshedVersionsParseError, TypeshedVersionsQueryResult}; -use crate::db::Db; +use super::typeshed::{TypeshedVersionsParseError, TypeshedVersionsQueryResult, typeshed_versions}; use crate::module_name::ModuleName; use crate::module_resolver::resolver::ResolverContext; use crate::site_packages::SitePackagesDiscoveryError; @@ -78,7 +76,7 @@ impl ModulePath { | SearchPathInner::FirstParty(search_path) | SearchPathInner::SitePackages(search_path) | SearchPathInner::Editable(search_path) => { - system_path_to_file(resolver.db.upcast(), search_path.join(relative_path)) + system_path_to_file(resolver.db, search_path.join(relative_path)) == Err(FileError::IsADirectory) } SearchPathInner::StandardLibraryCustom(stdlib_root) => { @@ -86,7 +84,7 @@ impl ModulePath { TypeshedVersionsQueryResult::DoesNotExist => false, TypeshedVersionsQueryResult::Exists | TypeshedVersionsQueryResult::MaybeExists => { - system_path_to_file(resolver.db.upcast(), stdlib_root.join(relative_path)) + system_path_to_file(resolver.db, stdlib_root.join(relative_path)) == Err(FileError::IsADirectory) } } @@ -117,16 +115,15 @@ impl ModulePath { | SearchPathInner::Editable(search_path) => { let absolute_path = search_path.join(relative_path); - system_path_to_file(resolver.db.upcast(), absolute_path.join("__init__.py")).is_ok() - || system_path_to_file(resolver.db.upcast(), absolute_path.join("__init__.pyi")) - .is_ok() + system_path_to_file(resolver.db, absolute_path.join("__init__.py")).is_ok() + || system_path_to_file(resolver.db, absolute_path.join("__init__.pyi")).is_ok() } SearchPathInner::StandardLibraryCustom(search_path) => { match query_stdlib_version(relative_path, resolver) { TypeshedVersionsQueryResult::DoesNotExist => false, TypeshedVersionsQueryResult::Exists | TypeshedVersionsQueryResult::MaybeExists => system_path_to_file( - resolver.db.upcast(), + resolver.db, search_path.join(relative_path).join("__init__.pyi"), ) .is_ok(), @@ -144,9 +141,26 @@ impl ModulePath { } } + pub(super) fn to_system_path(&self) -> Option { + let ModulePath { + search_path, + relative_path, + } = self; + match &*search_path.0 { + SearchPathInner::Extra(search_path) + | SearchPathInner::FirstParty(search_path) + | SearchPathInner::SitePackages(search_path) + | SearchPathInner::Editable(search_path) => Some(search_path.join(relative_path)), + SearchPathInner::StandardLibraryCustom(stdlib_root) => { + Some(stdlib_root.join(relative_path)) + } + SearchPathInner::StandardLibraryVendored(_) => None, + } + } + #[must_use] pub(super) fn to_file(&self, resolver: &ResolverContext) -> Option { - let db = resolver.db.upcast(); + let db = resolver.db; let ModulePath { search_path, relative_path, @@ -189,7 +203,18 @@ impl ModulePath { stdlib_path_to_module_name(relative_path) } else { let parent = relative_path.parent()?; - let parent_components = parent.components().map(|component| component.as_str()); + let parent_components = parent.components().enumerate().map(|(index, component)| { + let component = component.as_str(); + + // For stub packages, strip the `-stubs` suffix from the first component + // because it isn't a valid module name part AND the module name is the name without the `-stubs`. + if index == 0 { + component.strip_suffix("-stubs").unwrap_or(component) + } else { + component + } + }); + let skip_final_part = relative_path.ends_with("__init__.py") || relative_path.ends_with("__init__.pyi"); if skip_final_part { @@ -298,62 +323,37 @@ fn query_stdlib_version( /// If validation fails for a search path derived from the user settings, /// a message must be displayed to the user, /// as type checking cannot be done reliably in these circumstances. -#[derive(Debug)] -pub(crate) enum SearchPathValidationError { +#[derive(Debug, thiserror::Error)] +pub enum SearchPathValidationError { /// The path provided by the user was not a directory + #[error("{0} does not point to a directory")] NotADirectory(SystemPathBuf), /// The path provided by the user is a directory, /// but no `stdlib/` subdirectory exists. /// (This is only relevant for stdlib search paths.) + #[error("The directory at {0} has no `stdlib/` subdirectory")] NoStdlibSubdirectory(SystemPathBuf), /// The typeshed path provided by the user is a directory, /// but `stdlib/VERSIONS` could not be read. /// (This is only relevant for stdlib search paths.) + #[error("Failed to read the custom typeshed versions file '{path}'")] FailedToReadVersionsFile { path: SystemPathBuf, + #[source] error: std::io::Error, }, /// The path provided by the user is a directory, /// and a `stdlib/VERSIONS` file exists, but it fails to parse. /// (This is only relevant for stdlib search paths.) + #[error(transparent)] VersionsParseError(TypeshedVersionsParseError), /// Failed to discover the site-packages for the configured virtual environment. - SitePackagesDiscovery(SitePackagesDiscoveryError), -} - -impl fmt::Display for SearchPathValidationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotADirectory(path) => write!(f, "{path} does not point to a directory"), - Self::NoStdlibSubdirectory(path) => { - write!(f, "The directory at {path} has no `stdlib/` subdirectory") - } - Self::FailedToReadVersionsFile { path, error } => { - write!( - f, - "Failed to read the custom typeshed versions file '{path}': {error}" - ) - } - Self::VersionsParseError(underlying_error) => underlying_error.fmt(f), - SearchPathValidationError::SitePackagesDiscovery(error) => { - write!(f, "Failed to discover the site-packages directory: {error}") - } - } - } -} - -impl std::error::Error for SearchPathValidationError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - if let Self::VersionsParseError(underlying_error) = self { - Some(underlying_error) - } else { - None - } - } + #[error("Failed to discover the site-packages directory")] + SitePackagesDiscovery(#[source] SitePackagesDiscoveryError), } impl From for SearchPathValidationError { @@ -370,7 +370,7 @@ impl From for SearchPathValidationError { type SearchPathResult = Result; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] enum SearchPathInner { Extra(SystemPathBuf), FirstParty(SystemPathBuf), @@ -405,7 +405,7 @@ enum SearchPathInner { /// or the "Editable" category. For the "First-party", "Site-packages" /// and "Standard-library" categories, however, there will always be exactly /// one search path from that category in any given list of search paths. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub(crate) struct SearchPath(Arc); impl SearchPath { @@ -432,8 +432,10 @@ impl SearchPath { } /// Create a new standard-library search path pointing to a custom directory on disk - pub(crate) fn custom_stdlib(db: &dyn Db, typeshed: &SystemPath) -> SearchPathResult { - let system = db.system(); + pub(crate) fn custom_stdlib( + system: &dyn System, + typeshed: &SystemPath, + ) -> SearchPathResult { if !system.is_directory(typeshed) { return Err(SearchPathValidationError::NotADirectory( typeshed.to_path_buf(), @@ -501,6 +503,10 @@ impl SearchPath { ) } + pub(crate) fn is_first_party(&self) -> bool { + matches!(&*self.0, SearchPathInner::FirstParty(_)) + } + fn is_valid_extension(&self, extension: &str) -> bool { if self.is_standard_library() { extension == "pyi" @@ -680,7 +686,7 @@ mod tests { .build(); assert_eq!( - SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()) + SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()) .unwrap() .to_module_path() .with_py_extension(), @@ -688,7 +694,7 @@ mod tests { ); assert_eq!( - &SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()) + &SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()) .unwrap() .join("foo") .with_pyi_extension(), @@ -799,7 +805,7 @@ mod tests { let TestCase { db, stdlib, .. } = TestCaseBuilder::new() .with_mocked_typeshed(MockedTypeshed::default()) .build(); - SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()) + SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()) .unwrap() .to_module_path() .push("bar.py"); @@ -811,7 +817,7 @@ mod tests { let TestCase { db, stdlib, .. } = TestCaseBuilder::new() .with_mocked_typeshed(MockedTypeshed::default()) .build(); - SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()) + SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()) .unwrap() .to_module_path() .push("bar.rs"); @@ -843,7 +849,7 @@ mod tests { .with_mocked_typeshed(MockedTypeshed::default()) .build(); - let root = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap(); + let root = SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()).unwrap(); // Must have a `.pyi` extension or no extension: let bad_absolute_path = SystemPath::new("foo/stdlib/x.py"); @@ -891,7 +897,7 @@ mod tests { .with_mocked_typeshed(typeshed) .with_python_version(python_version) .build(); - let stdlib = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap(); + let stdlib = SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()).unwrap(); (db, stdlib) } @@ -923,10 +929,12 @@ mod tests { assert!(asyncio_regular_package.is_regular_package(&resolver)); // Paths to directories don't resolve to VfsFiles assert_eq!(asyncio_regular_package.to_file(&resolver), None); - assert!(asyncio_regular_package - .join("__init__.pyi") - .to_file(&resolver) - .is_some()); + assert!( + asyncio_regular_package + .join("__init__.pyi") + .to_file(&resolver) + .is_some() + ); // The `asyncio` package exists on Python 3.8, but the `asyncio.tasks` submodule does not, // according to the `VERSIONS` file in our typeshed mock: @@ -1056,10 +1064,12 @@ mod tests { assert!(collections_regular_package.is_regular_package(&resolver)); // (This is still `None`, as directories don't resolve to `Vfs` files) assert_eq!(collections_regular_package.to_file(&resolver), None); - assert!(collections_regular_package - .join("__init__.pyi") - .to_file(&resolver) - .is_some()); + assert!( + collections_regular_package + .join("__init__.pyi") + .to_file(&resolver) + .is_some() + ); // ...and so should the `asyncio.tasks` submodule (though it's still not a directory): let asyncio_tasks_module = stdlib_path.join("asyncio/tasks.pyi"); diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs new file mode 100644 index 0000000000000..be6f40c2465f2 --- /dev/null +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -0,0 +1,2049 @@ +use std::borrow::Cow; +use std::fmt; +use std::iter::FusedIterator; +use std::str::Split; + +use compact_str::format_compact; +use rustc_hash::{FxBuildHasher, FxHashSet}; + +use ruff_db::files::{File, FilePath, FileRootKind}; +use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf}; +use ruff_db::vendored::{VendoredFileSystem, VendoredPath}; +use ruff_python_ast::PythonVersion; + +use crate::db::Db; +use crate::module_name::ModuleName; +use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions}; +use crate::{Program, SearchPathSettings}; + +use super::module::{Module, ModuleKind}; +use super::path::{ModulePath, SearchPath, SearchPathValidationError}; + +/// Resolves a module name to a module. +pub fn resolve_module(db: &dyn Db, module_name: &ModuleName) -> Option { + let interned_name = ModuleNameIngredient::new(db, module_name); + + resolve_module_query(db, interned_name) +} + +/// Salsa query that resolves an interned [`ModuleNameIngredient`] to a module. +/// +/// This query should not be called directly. Instead, use [`resolve_module`]. It only exists +/// because Salsa requires the module name to be an ingredient. +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn resolve_module_query<'db>( + db: &'db dyn Db, + module_name: ModuleNameIngredient<'db>, +) -> Option { + let name = module_name.name(db); + let _span = tracing::trace_span!("resolve_module", %name).entered(); + + let Some(resolved) = resolve_name(db, name) else { + tracing::debug!("Module `{name}` not found in search paths"); + return None; + }; + + let module = match resolved { + ResolvedName::FileModule(module) => { + tracing::trace!( + "Resolved module `{name}` to `{path}`", + path = module.file.path(db) + ); + Module::file_module(name.clone(), module.kind, module.search_path, module.file) + } + ResolvedName::NamespacePackage => { + tracing::trace!("Module `{name}` is a namespace package"); + Module::namespace_package(name.clone()) + } + }; + + Some(module) +} + +/// Resolves the module for the given path. +/// +/// Returns `None` if the path is not a module locatable via any of the known search paths. +#[allow(unused)] +pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option { + // It's not entirely clear on first sight why this method calls `file_to_module` instead of + // it being the other way round, considering that the first thing that `file_to_module` does + // is to retrieve the file's path. + // + // The reason is that `file_to_module` is a tracked Salsa query and salsa queries require that + // all arguments are Salsa ingredients (something stored in Salsa). `Path`s aren't salsa ingredients but + // `VfsFile` is. So what we do here is to retrieve the `path`'s `VfsFile` so that we can make + // use of Salsa's caching and invalidation. + let file = path.to_file(db)?; + file_to_module(db, file) +} + +#[derive(Debug, Clone, Copy)] +enum SystemOrVendoredPathRef<'a> { + System(&'a SystemPath), + Vendored(&'a VendoredPath), +} + +impl std::fmt::Display for SystemOrVendoredPathRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SystemOrVendoredPathRef::System(system) => system.fmt(f), + SystemOrVendoredPathRef::Vendored(vendored) => vendored.fmt(f), + } + } +} + +/// Resolves the module for the file with the given id. +/// +/// Returns `None` if the file is not a module locatable via any of the known search paths. +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option { + let _span = tracing::trace_span!("file_to_module", ?file).entered(); + + let path = match file.path(db) { + FilePath::System(system) => SystemOrVendoredPathRef::System(system), + FilePath::Vendored(vendored) => SystemOrVendoredPathRef::Vendored(vendored), + FilePath::SystemVirtual(_) => return None, + }; + + let module_name = search_paths(db).find_map(|candidate| { + let relative_path = match path { + SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path), + SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path), + }?; + relative_path.to_module_name() + })?; + + // Resolve the module name to see if Python would resolve the name to the same path. + // If it doesn't, then that means that multiple modules have the same name in different + // root paths, but that the module corresponding to `path` is in a lower priority search path, + // in which case we ignore it. + let module = resolve_module(db, &module_name)?; + let module_file = module.file()?; + + if file.path(db) == module_file.path(db) { + Some(module) + } else { + // This path is for a module with the same name but with a different precedence. For example: + // ``` + // src/foo.py + // src/foo/__init__.py + // ``` + // The module name of `src/foo.py` is `foo`, but the module loaded by Python is `src/foo/__init__.py`. + // That means we need to ignore `src/foo.py` even though it resolves to the same module name. + None + } +} + +pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator { + Program::get(db).search_paths(db).iter(db) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SearchPaths { + /// Search paths that have been statically determined purely from reading ty's configuration settings. + /// These shouldn't ever change unless the config settings themselves change. + static_paths: Vec, + + /// site-packages paths are not included in the above field: + /// if there are multiple site-packages paths, editable installations can appear + /// *between* the site-packages paths on `sys.path` at runtime. + /// That means we can't know where a second or third `site-packages` path should sit + /// in terms of module-resolution priority until we've discovered the editable installs + /// for the first `site-packages` path + site_packages: Vec, + + typeshed_versions: TypeshedVersions, +} + +impl SearchPaths { + /// Validate and normalize the raw settings given by the user + /// into settings we can use for module resolution + /// + /// This method also implements the typing spec's [module resolution order]. + /// + /// [module resolution order]: https://typing.python.org/en/latest/spec/distributing.html#import-resolution-ordering + pub(crate) fn from_settings( + settings: &SearchPathSettings, + system: &dyn System, + vendored: &VendoredFileSystem, + ) -> Result { + fn canonicalize(path: &SystemPath, system: &dyn System) -> SystemPathBuf { + system + .canonicalize_path(path) + .unwrap_or_else(|_| path.to_path_buf()) + } + + let SearchPathSettings { + extra_paths, + src_roots, + custom_typeshed: typeshed, + site_packages_paths, + } = settings; + + let mut static_paths = vec![]; + + for path in extra_paths { + let path = canonicalize(path, system); + tracing::debug!("Adding extra search-path '{path}'"); + + static_paths.push(SearchPath::extra(system, path)?); + } + + for src_root in src_roots { + tracing::debug!("Adding first-party search path '{src_root}'"); + static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?); + } + + let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed { + let typeshed = canonicalize(typeshed, system); + tracing::debug!("Adding custom-stdlib search path '{typeshed}'"); + + let versions_path = typeshed.join("stdlib/VERSIONS"); + + let versions_content = system.read_to_string(&versions_path).map_err(|error| { + SearchPathValidationError::FailedToReadVersionsFile { + path: versions_path, + error, + } + })?; + + let parsed: TypeshedVersions = versions_content.parse()?; + + let search_path = SearchPath::custom_stdlib(system, &typeshed)?; + + (parsed, search_path) + } else { + tracing::debug!("Using vendored stdlib"); + ( + vendored_typeshed_versions(vendored), + SearchPath::vendored_stdlib(), + ) + }; + + static_paths.push(stdlib_path); + + let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len()); + + for path in site_packages_paths { + tracing::debug!("Adding site-packages search path '{path}'"); + site_packages.push(SearchPath::site_packages(system, path.clone())?); + } + + // TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step + + // Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]). + // (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo` + // as module resolution paths simultaneously.) + // + // This code doesn't use an `IndexSet` because the key is the system path and not the search root. + // + // [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site + let mut seen_paths = FxHashSet::with_capacity_and_hasher(static_paths.len(), FxBuildHasher); + + static_paths.retain(|path| { + if let Some(path) = path.as_system_path() { + seen_paths.insert(path.to_path_buf()) + } else { + true + } + }); + + Ok(SearchPaths { + static_paths, + site_packages, + typeshed_versions, + }) + } + + pub(crate) fn try_register_static_roots(&self, db: &dyn Db) { + let files = db.files(); + for path in self.static_paths.iter().chain(self.site_packages.iter()) { + if let Some(system_path) = path.as_system_path() { + if !path.is_first_party() { + files.try_add_root(db, system_path, FileRootKind::LibrarySearchPath); + } + } + } + } + + pub(super) fn iter<'a>(&'a self, db: &'a dyn Db) -> SearchPathIterator<'a> { + SearchPathIterator { + db, + static_paths: self.static_paths.iter(), + dynamic_paths: None, + } + } + + pub(crate) fn custom_stdlib(&self) -> Option<&SystemPath> { + self.static_paths.iter().find_map(|search_path| { + if search_path.is_standard_library() { + search_path.as_system_path() + } else { + None + } + }) + } + + pub(crate) fn typeshed_versions(&self) -> &TypeshedVersions { + &self.typeshed_versions + } +} + +/// Collect all dynamic search paths. For each `site-packages` path: +/// - Collect that `site-packages` path +/// - Collect any search paths listed in `.pth` files in that `site-packages` directory +/// due to editable installations of third-party packages. +/// +/// The editable-install search paths for the first `site-packages` directory +/// should come between the two `site-packages` directories when it comes to +/// module-resolution priority. +#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec { + tracing::debug!("Resolving dynamic module resolution paths"); + + let SearchPaths { + static_paths, + site_packages, + typeshed_versions: _, + } = Program::get(db).search_paths(db); + + let mut dynamic_paths = Vec::new(); + + if site_packages.is_empty() { + return dynamic_paths; + } + + let mut existing_paths: FxHashSet<_> = static_paths + .iter() + .filter_map(|path| path.as_system_path()) + .map(Cow::Borrowed) + .collect(); + + let files = db.files(); + let system = db.system(); + + for site_packages_search_path in site_packages { + let site_packages_dir = site_packages_search_path + .as_system_path() + .expect("Expected site package path to be a system path"); + + if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) { + continue; + } + + let site_packages_root = files + .root(db, site_packages_dir) + .expect("Site-package root to have been created"); + + // This query needs to be re-executed each time a `.pth` file + // is added, modified or removed from the `site-packages` directory. + // However, we don't use Salsa queries to read the source text of `.pth` files; + // we use the APIs on the `System` trait directly. As such, add a dependency on the + // site-package directory's revision. + site_packages_root.revision(db); + + dynamic_paths.push(site_packages_search_path.clone()); + + // As well as modules installed directly into `site-packages`, + // the directory may also contain `.pth` files. + // Each `.pth` file in `site-packages` may contain one or more lines + // containing a (relative or absolute) path. + // Each of these paths may point to an editable install of a package, + // so should be considered an additional search path. + let pth_file_iterator = match PthFileIterator::new(db, site_packages_dir) { + Ok(iterator) => iterator, + Err(error) => { + tracing::warn!( + "Failed to search for editable installation in {site_packages_dir}: {error}" + ); + continue; + } + }; + + // The Python documentation specifies that `.pth` files in `site-packages` + // are processed in alphabetical order, so collecting and then sorting is necessary. + // https://docs.python.org/3/library/site.html#module-site + let mut all_pth_files: Vec = pth_file_iterator.collect(); + all_pth_files.sort_unstable_by(|a, b| a.path.cmp(&b.path)); + + let installations = all_pth_files.iter().flat_map(PthFile::items); + + for installation in installations { + let installation = system + .canonicalize_path(&installation) + .unwrap_or(installation); + + if existing_paths.insert(Cow::Owned(installation.clone())) { + match SearchPath::editable(system, installation.clone()) { + Ok(search_path) => { + tracing::debug!( + "Adding editable installation to module resolution path {path}", + path = installation + ); + dynamic_paths.push(search_path); + } + + Err(error) => { + tracing::debug!("Skipping editable installation: {error}"); + } + } + } + } + } + + dynamic_paths +} + +/// Iterate over the available module-resolution search paths, +/// following the invariants maintained by [`sys.path` at runtime]: +/// "No item is added to `sys.path` more than once." +/// Dynamic search paths (required for editable installs into `site-packages`) +/// are only calculated lazily. +/// +/// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site +pub(crate) struct SearchPathIterator<'db> { + db: &'db dyn Db, + static_paths: std::slice::Iter<'db, SearchPath>, + dynamic_paths: Option>, +} + +impl<'db> Iterator for SearchPathIterator<'db> { + type Item = &'db SearchPath; + + fn next(&mut self) -> Option { + let SearchPathIterator { + db, + static_paths, + dynamic_paths, + } = self; + + static_paths.next().or_else(|| { + dynamic_paths + .get_or_insert_with(|| dynamic_resolution_paths(*db).iter()) + .next() + }) + } +} + +impl FusedIterator for SearchPathIterator<'_> {} + +/// Represents a single `.pth` file in a `site-packages` directory. +/// One or more lines in a `.pth` file may be a (relative or absolute) +/// path that represents an editable installation of a package. +struct PthFile<'db> { + path: SystemPathBuf, + contents: String, + site_packages: &'db SystemPath, +} + +impl<'db> PthFile<'db> { + /// Yield paths in this `.pth` file that appear to represent editable installations, + /// and should therefore be added as module-resolution search paths. + fn items(&'db self) -> impl Iterator + 'db { + let PthFile { + path: _, + contents, + site_packages, + } = self; + + // Empty lines or lines starting with '#' are ignored by the Python interpreter. + // Lines that start with "import " or "import\t" do not represent editable installs at all; + // instead, these are lines that are executed by Python at startup. + // https://docs.python.org/3/library/site.html#module-site + contents.lines().filter_map(move |line| { + let line = line.trim_end(); + if line.is_empty() + || line.starts_with('#') + || line.starts_with("import ") + || line.starts_with("import\t") + { + return None; + } + + Some(SystemPath::absolute(line, site_packages)) + }) + } +} + +/// Iterator that yields a [`PthFile`] instance for every `.pth` file +/// found in a given `site-packages` directory. +struct PthFileIterator<'db> { + db: &'db dyn Db, + directory_iterator: Box> + 'db>, + site_packages: &'db SystemPath, +} + +impl<'db> PthFileIterator<'db> { + fn new(db: &'db dyn Db, site_packages: &'db SystemPath) -> std::io::Result { + Ok(Self { + db, + directory_iterator: db.system().read_directory(site_packages)?, + site_packages, + }) + } +} + +impl<'db> Iterator for PthFileIterator<'db> { + type Item = PthFile<'db>; + + fn next(&mut self) -> Option { + let PthFileIterator { + db, + directory_iterator, + site_packages, + } = self; + + let system = db.system(); + + loop { + let entry_result = directory_iterator.next()?; + let Ok(entry) = entry_result else { + continue; + }; + let file_type = entry.file_type(); + if file_type.is_directory() { + continue; + } + let path = entry.into_path(); + if path.extension() != Some("pth") { + continue; + } + + let contents = match system.read_to_string(&path) { + Ok(contents) => contents, + Err(error) => { + tracing::warn!("Failed to read .pth file '{path}': {error}"); + continue; + } + }; + + return Some(PthFile { + path, + contents, + site_packages, + }); + } + } +} + +/// A thin wrapper around `ModuleName` to make it a Salsa ingredient. +/// +/// This is needed because Salsa requires that all query arguments are salsa ingredients. +#[salsa::interned(debug)] +struct ModuleNameIngredient<'db> { + #[returns(ref)] + pub(super) name: ModuleName, +} + +/// Returns `true` if the module name refers to a standard library module which can't be shadowed +/// by a first-party module. +/// +/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as the +/// `types` module, which tends to be imported early in Python startup, so can't be consistently +/// shadowed, and is important to type checking. +fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool { + module_name == "types" || ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name) +} + +/// Given a module name and a list of search paths in which to lookup modules, +/// attempt to resolve the module name +fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option { + let program = Program::get(db); + let python_version = program.python_version(db); + let resolver_state = ResolverContext::new(db, python_version); + let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str()); + + let name = RelaxedModuleName::new(name); + let stub_name = name.to_stub_package(); + let mut is_namespace_package = false; + + for search_path in search_paths(db) { + // When a builtin module is imported, standard module resolution is bypassed: + // the module name always resolves to the stdlib module, + // even if there's a module of the same name in the first-party root + // (which would normally result in the stdlib module being overridden). + // TODO: offer a diagnostic if there is a first-party module of the same name + if is_non_shadowable && !search_path.is_standard_library() { + continue; + } + + if !search_path.is_standard_library() { + match resolve_name_in_search_path(&resolver_state, &stub_name, search_path) { + Ok((package_kind, ResolvedName::FileModule(module))) => { + if package_kind.is_root() && module.kind.is_module() { + tracing::trace!( + "Search path '{search_path} contains a module named `{stub_name}` but a standalone module isn't a valid stub." + ); + } else { + return Some(ResolvedName::FileModule(module)); + } + } + Ok((_, ResolvedName::NamespacePackage)) => { + is_namespace_package = true; + } + Err(PackageKind::Root) => { + tracing::trace!( + "Search path '{search_path}' contains no stub package named `{stub_name}`." + ); + } + Err(PackageKind::Regular) => { + tracing::trace!( + "Stub-package in `{search_path} doesn't contain module: `{name}`" + ); + // stub exists, but the module doesn't. + // TODO: Support partial packages. + return None; + } + Err(PackageKind::Namespace) => { + tracing::trace!( + "Stub-package in `{search_path} doesn't contain module: `{name}` but it is a namespace package, keep going." + ); + // stub exists, but the module doesn't. But this is a namespace package, + // keep searching the next search path for a stub package with the same name. + continue; + } + } + } + + match resolve_name_in_search_path(&resolver_state, &name, search_path) { + Ok((_, ResolvedName::FileModule(module))) => { + return Some(ResolvedName::FileModule(module)); + } + Ok((_, ResolvedName::NamespacePackage)) => { + is_namespace_package = true; + } + Err(kind) => match kind { + PackageKind::Root => { + tracing::trace!( + "Search path '{search_path}' contains no package named `{name}`." + ); + } + PackageKind::Regular => { + // For regular packages, don't search the next search path. All files of that + // package must be in the same location + tracing::trace!("Package in `{search_path} doesn't contain module: `{name}`"); + return None; + } + PackageKind::Namespace => { + tracing::trace!( + "Package in `{search_path} doesn't contain module: `{name}` but it is a namespace package, keep going." + ); + } + }, + } + } + + if is_namespace_package { + return Some(ResolvedName::NamespacePackage); + } + + None +} + +#[derive(Debug)] +enum ResolvedName { + /// A module that resolves to a file. + FileModule(ResolvedFileModule), + + /// The module name resolved to a namespace package. + /// + /// For example, `from opentelemetry import trace, metrics` where `opentelemetry` is a namespace package (and `trace` and `metrics` are sub packages). + NamespacePackage, +} + +#[derive(Debug)] +struct ResolvedFileModule { + kind: ModuleKind, + search_path: SearchPath, + file: File, +} + +fn resolve_name_in_search_path( + context: &ResolverContext, + name: &RelaxedModuleName, + search_path: &SearchPath, +) -> Result<(PackageKind, ResolvedName), PackageKind> { + let mut components = name.components(); + let module_name = components.next_back().unwrap(); + + let resolved_package = resolve_package(search_path, components, context)?; + + let mut package_path = resolved_package.path; + + package_path.push(module_name); + + // Check for a regular package first (highest priority) + package_path.push("__init__"); + if let Some(regular_package) = resolve_file_module(&package_path, context) { + return Ok(( + resolved_package.kind, + ResolvedName::FileModule(ResolvedFileModule { + search_path: search_path.clone(), + kind: ModuleKind::Package, + file: regular_package, + }), + )); + } + + // Check for a file module next + package_path.pop(); + + if let Some(file_module) = resolve_file_module(&package_path, context) { + return Ok(( + resolved_package.kind, + ResolvedName::FileModule(ResolvedFileModule { + file: file_module, + kind: ModuleKind::Module, + search_path: search_path.clone(), + }), + )); + } + + // Last resort, check if a folder with the given name exists. + // If so, then this is a namespace package. + // We need to skip this check for typeshed because the `resolve_file_module` can also return `None` + // if the `__init__.py` exists but isn't available for the current Python version. + // Let's assume that the `xml` module is only available on Python 3.11+ and we're resolving for Python 3.10: + // * `resolve_file_module("xml/__init__.pyi")` returns `None` even though the file exists but the + // module isn't available for the current Python version. + // * The check here would now return `true` because the `xml` directory exists, resulting + // in a false positive for a namespace package. + // + // Since typeshed doesn't use any namespace packages today (May 2025), simply skip this + // check which also helps performance. If typeshed ever uses namespace packages, ensure that + // this check also takes the `VERSIONS` file into consideration. + if !search_path.is_standard_library() && package_path.is_directory(context) { + if let Some(path) = package_path.to_system_path() { + let system = context.db.system(); + if system.case_sensitivity().is_case_sensitive() + || system.path_exists_case_sensitive( + &path, + package_path.search_path().as_system_path().unwrap(), + ) + { + return Ok((resolved_package.kind, ResolvedName::NamespacePackage)); + } + } + } + + Err(resolved_package.kind) +} + +/// If `module` exists on disk with either a `.pyi` or `.py` extension, +/// return the [`File`] corresponding to that path. +/// +/// `.pyi` files take priority, as they always have priority when +/// resolving modules. +fn resolve_file_module(module: &ModulePath, resolver_state: &ResolverContext) -> Option { + // Stubs have precedence over source files + let file = module + .with_pyi_extension() + .to_file(resolver_state) + .or_else(|| { + module + .with_py_extension() + .and_then(|path| path.to_file(resolver_state)) + })?; + + // For system files, test if the path has the correct casing. + // We can skip this step for vendored files or virtual files because + // those file systems are case sensitive (we wouldn't get to this point). + if let Some(path) = file.path(resolver_state.db).as_system_path() { + let system = resolver_state.db.system(); + if !system.case_sensitivity().is_case_sensitive() + && !system + .path_exists_case_sensitive(path, module.search_path().as_system_path().unwrap()) + { + return None; + } + } + + Some(file) +} + +fn resolve_package<'a, 'db, I>( + module_search_path: &SearchPath, + components: I, + resolver_state: &ResolverContext<'db>, +) -> Result +where + I: Iterator, +{ + let mut package_path = module_search_path.to_module_path(); + + // `true` if inside a folder that is a namespace package (has no `__init__.py`). + // Namespace packages are special because they can be spread across multiple search paths. + // https://peps.python.org/pep-0420/ + let mut in_namespace_package = false; + + // `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`. + let mut in_sub_package = false; + + // For `foo.bar.baz`, test that `foo` and `baz` both contain a `__init__.py`. + for folder in components { + package_path.push(folder); + + let is_regular_package = package_path.is_regular_package(resolver_state); + + if is_regular_package { + in_namespace_package = false; + } else if package_path.is_directory(resolver_state) + // Pure modules hide namespace packages with the same name + && resolve_file_module(&package_path, resolver_state).is_none() + { + // A directory without an `__init__.py(i)` is a namespace package, continue with the next folder. + in_namespace_package = true; + } else if in_namespace_package { + // Package not found but it is part of a namespace package. + return Err(PackageKind::Namespace); + } else if in_sub_package { + // A regular sub package wasn't found. + return Err(PackageKind::Regular); + } else { + // We couldn't find `foo` for `foo.bar.baz`, search the next search path. + return Err(PackageKind::Root); + } + + in_sub_package = true; + } + + let kind = if in_namespace_package { + PackageKind::Namespace + } else if in_sub_package { + PackageKind::Regular + } else { + PackageKind::Root + }; + + Ok(ResolvedPackage { + kind, + path: package_path, + }) +} + +#[derive(Debug)] +struct ResolvedPackage { + path: ModulePath, + kind: PackageKind, +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +enum PackageKind { + /// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`. + Root, + + /// A regular sub-package where the parent contains an `__init__.py`. + /// + /// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`. + Regular, + + /// A sub-package in a namespace package. A namespace package is a package without an `__init__.py`. + /// + /// For example, `bar` in `foo.bar` if the `foo` directory contains no `__init__.py`. + Namespace, +} + +impl PackageKind { + pub(crate) const fn is_root(self) -> bool { + matches!(self, PackageKind::Root) + } +} + +pub(super) struct ResolverContext<'db> { + pub(super) db: &'db dyn Db, + pub(super) python_version: PythonVersion, +} + +impl<'db> ResolverContext<'db> { + pub(super) fn new(db: &'db dyn Db, python_version: PythonVersion) -> Self { + Self { db, python_version } + } + + pub(super) fn vendored(&self) -> &VendoredFileSystem { + self.db.vendored() + } +} + +/// A [`ModuleName`] but with relaxed semantics to allow `-stubs.path` +#[derive(Debug)] +struct RelaxedModuleName(compact_str::CompactString); + +impl RelaxedModuleName { + fn new(name: &ModuleName) -> Self { + Self(name.as_str().into()) + } + + fn components(&self) -> Split<'_, char> { + self.0.split('.') + } + + fn to_stub_package(&self) -> Self { + if let Some((package, rest)) = self.0.split_once('.') { + Self(format_compact!("{package}-stubs.{rest}")) + } else { + Self(format_compact!("{package}-stubs", package = self.0)) + } + } +} + +impl fmt::Display for RelaxedModuleName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use ruff_db::Db; + use ruff_db::files::{File, FilePath, system_path_to_file}; + use ruff_db::system::{DbWithTestSystem as _, DbWithWritableSystem as _}; + use ruff_db::testing::{ + assert_const_function_query_was_not_run, assert_function_query_was_not_run, + }; + use ruff_python_ast::PythonVersion; + + use crate::db::tests::TestDb; + use crate::module_name::ModuleName; + use crate::module_resolver::module::ModuleKind; + use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; + use crate::{ProgramSettings, PythonPlatform, PythonVersionWithSource}; + + use super::*; + + #[test] + fn first_party_module() { + let TestCase { db, src, .. } = TestCaseBuilder::new() + .with_src_files(&[("foo.py", "print('Hello, world!')")]) + .build(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + + assert_eq!( + Some(&foo_module), + resolve_module(&db, &foo_module_name).as_ref() + ); + + assert_eq!("foo", foo_module.name()); + assert_eq!(&src, foo_module.search_path().unwrap()); + assert_eq!(ModuleKind::Module, foo_module.kind()); + + let expected_foo_path = src.join("foo.py"); + assert_eq!(&expected_foo_path, foo_module.file().unwrap().path(&db)); + assert_eq!( + Some(foo_module), + path_to_module(&db, &FilePath::System(expected_foo_path)) + ); + } + + #[test] + fn builtins_vendored() { + let TestCase { db, stdlib, .. } = TestCaseBuilder::new() + .with_vendored_typeshed() + .with_src_files(&[("builtins.py", "FOOOO = 42")]) + .build(); + + let builtins_module_name = ModuleName::new_static("builtins").unwrap(); + let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve"); + + assert_eq!( + builtins.file().unwrap().path(&db), + &stdlib.join("builtins.pyi") + ); + } + + #[test] + fn builtins_custom() { + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: &[("builtins.pyi", "def min(a, b): ...")], + versions: "builtins: 3.8-", + }; + + const SRC: &[FileSpec] = &[("builtins.py", "FOOOO = 42")]; + + let TestCase { db, stdlib, .. } = TestCaseBuilder::new() + .with_src_files(SRC) + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let builtins_module_name = ModuleName::new_static("builtins").unwrap(); + let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve"); + + assert_eq!( + builtins.file().unwrap().path(&db), + &stdlib.join("builtins.pyi") + ); + } + + #[test] + fn stdlib() { + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], + versions: "functools: 3.8-", + }; + + let TestCase { db, stdlib, .. } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let functools_module_name = ModuleName::new_static("functools").unwrap(); + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + + assert_eq!( + Some(&functools_module), + resolve_module(&db, &functools_module_name).as_ref() + ); + + assert_eq!(&stdlib, functools_module.search_path().unwrap()); + assert_eq!(ModuleKind::Module, functools_module.kind()); + + let expected_functools_path = stdlib.join("functools.pyi"); + assert_eq!( + &expected_functools_path, + functools_module.file().unwrap().path(&db) + ); + + assert_eq!( + Some(functools_module), + path_to_module(&db, &FilePath::System(expected_functools_path)) + ); + } + + fn create_module_names(raw_names: &[&str]) -> Vec { + raw_names + .iter() + .map(|raw| ModuleName::new(raw).unwrap()) + .collect() + } + + #[test] + fn stdlib_resolution_respects_versions_file_py38_existing_modules() { + const VERSIONS: &str = "\ + asyncio: 3.8- # 'Regular' package on py38+ + asyncio.tasks: 3.9-3.11 # Submodule on py39+ only + functools: 3.8- # Top-level single-file module + xml: 3.8-3.8 # Namespace package on py38 only + "; + + const STDLIB: &[FileSpec] = &[ + ("asyncio/__init__.pyi", ""), + ("asyncio/tasks.pyi", ""), + ("functools.pyi", ""), + ("xml/etree.pyi", ""), + ]; + + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: STDLIB, + versions: VERSIONS, + }; + + let TestCase { db, stdlib, .. } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]); + for module_name in existing_modules { + let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| { + panic!("Expected module {module_name} to exist in the mock stdlib") + }); + let search_path = resolved_module.search_path().unwrap(); + assert_eq!( + &stdlib, search_path, + "Search path for {module_name} was unexpectedly {search_path:?}" + ); + assert!( + search_path.is_standard_library(), + "Expected a stdlib search path, but got {search_path:?}" + ); + } + } + + #[test] + fn stdlib_resolution_respects_versions_file_py38_nonexisting_modules() { + const VERSIONS: &str = "\ + asyncio: 3.8- # 'Regular' package on py38+ + asyncio.tasks: 3.9-3.11 # Submodule on py39+ only + collections: 3.9- # 'Regular' package on py39+ + importlib: 3.9- # Namespace package on py39+ + xml: 3.8-3.8 # Namespace package on 3.8 only + "; + + const STDLIB: &[FileSpec] = &[ + ("collections/__init__.pyi", ""), + ("asyncio/__init__.pyi", ""), + ("asyncio/tasks.pyi", ""), + ("importlib/abc.pyi", ""), + ("xml/etree.pyi", ""), + ]; + + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: STDLIB, + versions: VERSIONS, + }; + + let TestCase { db, .. } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let nonexisting_modules = create_module_names(&[ + "collections", + "importlib", + "importlib.abc", + "xml", + "asyncio.tasks", + ]); + + for module_name in nonexisting_modules { + assert!( + resolve_module(&db, &module_name).is_none(), + "Unexpectedly resolved a module for {module_name}" + ); + } + } + + #[test] + fn stdlib_resolution_respects_versions_file_py39_existing_modules() { + const VERSIONS: &str = "\ + asyncio: 3.8- # 'Regular' package on py38+ + asyncio.tasks: 3.9-3.11 # Submodule on py39+ only + collections: 3.9- # 'Regular' package on py39+ + functools: 3.8- # Top-level single-file module + importlib: 3.9- # Namespace package on py39+ + "; + + const STDLIB: &[FileSpec] = &[ + ("asyncio/__init__.pyi", ""), + ("asyncio/tasks.pyi", ""), + ("collections/__init__.pyi", ""), + ("functools.pyi", ""), + ("importlib/abc.pyi", ""), + ]; + + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: STDLIB, + versions: VERSIONS, + }; + + let TestCase { db, stdlib, .. } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY39) + .build(); + + let existing_modules = create_module_names(&[ + "asyncio", + "functools", + "importlib.abc", + "collections", + "asyncio.tasks", + ]); + + for module_name in existing_modules { + let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| { + panic!("Expected module {module_name} to exist in the mock stdlib") + }); + let search_path = resolved_module.search_path().unwrap(); + assert_eq!( + &stdlib, search_path, + "Search path for {module_name} was unexpectedly {search_path:?}" + ); + assert!( + search_path.is_standard_library(), + "Expected a stdlib search path, but got {search_path:?}" + ); + } + } + #[test] + fn stdlib_resolution_respects_versions_file_py39_nonexisting_modules() { + const VERSIONS: &str = "\ + importlib: 3.9- # Namespace package on py39+ + xml: 3.8-3.8 # Namespace package on 3.8 only + "; + + const STDLIB: &[FileSpec] = &[("importlib/abc.pyi", ""), ("xml/etree.pyi", "")]; + + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: STDLIB, + versions: VERSIONS, + }; + + let TestCase { db, .. } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY39) + .build(); + + let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]); + for module_name in nonexisting_modules { + assert!( + resolve_module(&db, &module_name).is_none(), + "Unexpectedly resolved a module for {module_name}" + ); + } + } + + #[test] + fn first_party_precedence_over_stdlib() { + const SRC: &[FileSpec] = &[("functools.py", "def update_wrapper(): ...")]; + + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], + versions: "functools: 3.8-", + }; + + let TestCase { db, src, .. } = TestCaseBuilder::new() + .with_src_files(SRC) + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let functools_module_name = ModuleName::new_static("functools").unwrap(); + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + + assert_eq!( + Some(&functools_module), + resolve_module(&db, &functools_module_name).as_ref() + ); + assert_eq!(&src, functools_module.search_path().unwrap()); + assert_eq!(ModuleKind::Module, functools_module.kind()); + assert_eq!( + &src.join("functools.py"), + functools_module.file().unwrap().path(&db) + ); + + assert_eq!( + Some(functools_module), + path_to_module(&db, &FilePath::System(src.join("functools.py"))) + ); + } + + #[test] + fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() { + let TestCase { db, stdlib, .. } = TestCaseBuilder::new() + .with_vendored_typeshed() + .with_python_version(PythonVersion::default()) + .build(); + + let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap(); + let pydoc_data_topics = resolve_module(&db, &pydoc_data_topics_name).unwrap(); + + assert_eq!("pydoc_data.topics", pydoc_data_topics.name()); + assert_eq!(pydoc_data_topics.search_path().unwrap(), &stdlib); + assert_eq!( + pydoc_data_topics.file().unwrap().path(&db), + &stdlib.join("pydoc_data/topics.pyi") + ); + } + + #[test] + fn resolve_package() { + let TestCase { src, db, .. } = TestCaseBuilder::new() + .with_src_files(&[("foo/__init__.py", "print('Hello, world!'")]) + .build(); + + let foo_path = src.join("foo/__init__.py"); + let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); + + assert_eq!("foo", foo_module.name()); + assert_eq!(&src, foo_module.search_path().unwrap()); + assert_eq!(&foo_path, foo_module.file().unwrap().path(&db)); + + assert_eq!( + Some(&foo_module), + path_to_module(&db, &FilePath::System(foo_path)).as_ref() + ); + + // Resolving by directory doesn't resolve to the init file. + assert_eq!( + None, + path_to_module(&db, &FilePath::System(src.join("foo"))) + ); + } + + #[test] + fn package_priority_over_module() { + const SRC: &[FileSpec] = &[ + ("foo/__init__.py", "print('Hello, world!')"), + ("foo.py", "print('Hello, world!')"), + ]; + + let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); + + let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); + let foo_init_path = src.join("foo/__init__.py"); + + assert_eq!(&src, foo_module.search_path().unwrap()); + assert_eq!(&foo_init_path, foo_module.file().unwrap().path(&db)); + assert_eq!(ModuleKind::Package, foo_module.kind()); + + assert_eq!( + Some(foo_module), + path_to_module(&db, &FilePath::System(foo_init_path)) + ); + assert_eq!( + None, + path_to_module(&db, &FilePath::System(src.join("foo.py"))) + ); + } + + #[test] + fn typing_stub_over_module() { + const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")]; + + let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); + + let foo = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); + let foo_stub = src.join("foo.pyi"); + + assert_eq!(&src, foo.search_path().unwrap()); + assert_eq!(&foo_stub, foo.file().unwrap().path(&db)); + + assert_eq!(Some(foo), path_to_module(&db, &FilePath::System(foo_stub))); + assert_eq!( + None, + path_to_module(&db, &FilePath::System(src.join("foo.py"))) + ); + } + + #[test] + fn sub_packages() { + const SRC: &[FileSpec] = &[ + ("foo/__init__.py", ""), + ("foo/bar/__init__.py", ""), + ("foo/bar/baz.py", "print('Hello, world!)'"), + ]; + + let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); + + let baz_module = + resolve_module(&db, &ModuleName::new_static("foo.bar.baz").unwrap()).unwrap(); + let baz_path = src.join("foo/bar/baz.py"); + + assert_eq!(&src, baz_module.search_path().unwrap()); + assert_eq!(&baz_path, baz_module.file().unwrap().path(&db)); + + assert_eq!( + Some(baz_module), + path_to_module(&db, &FilePath::System(baz_path)) + ); + } + + #[test] + fn module_search_path_priority() { + let TestCase { + db, + src, + site_packages, + .. + } = TestCaseBuilder::new() + .with_src_files(&[("foo.py", "")]) + .with_site_packages_files(&[("foo.py", "")]) + .build(); + + let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); + let foo_src_path = src.join("foo.py"); + + assert_eq!(&src, foo_module.search_path().unwrap()); + assert_eq!(&foo_src_path, foo_module.file().unwrap().path(&db)); + assert_eq!( + Some(foo_module), + path_to_module(&db, &FilePath::System(foo_src_path)) + ); + + assert_eq!( + None, + path_to_module(&db, &FilePath::System(site_packages.join("foo.py"))) + ); + } + + #[test] + #[cfg(target_family = "unix")] + fn symlink() -> anyhow::Result<()> { + use anyhow::Context; + + use crate::{ + PythonPlatform, PythonVersionSource, PythonVersionWithSource, program::Program, + }; + use ruff_db::system::{OsSystem, SystemPath}; + + use crate::db::tests::TestDb; + + let mut db = TestDb::new(); + + let temp_dir = tempfile::tempdir()?; + let root = temp_dir + .path() + .canonicalize() + .context("Failed to canonicalize temp dir")?; + let root = SystemPath::from_std_path(&root).unwrap(); + db.use_system(OsSystem::new(root)); + + let src = root.join("src"); + let site_packages = root.join("site-packages"); + let custom_typeshed = root.join("typeshed"); + + let foo = src.join("foo.py"); + let bar = src.join("bar.py"); + + std::fs::create_dir_all(src.as_std_path())?; + std::fs::create_dir_all(site_packages.as_std_path())?; + std::fs::create_dir_all(custom_typeshed.join("stdlib").as_std_path())?; + std::fs::File::create(custom_typeshed.join("stdlib/VERSIONS").as_std_path())?; + + std::fs::write(foo.as_std_path(), "")?; + std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?; + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY38, + source: PythonVersionSource::default(), + }, + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings { + custom_typeshed: Some(custom_typeshed), + site_packages_paths: vec![site_packages], + ..SearchPathSettings::new(vec![src.clone()]) + } + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search path settings"), + }, + ); + + let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap(); + let bar_module = resolve_module(&db, &ModuleName::new_static("bar").unwrap()).unwrap(); + + assert_ne!(foo_module, bar_module); + + assert_eq!(&src, foo_module.search_path().unwrap()); + assert_eq!(&foo, foo_module.file().unwrap().path(&db)); + + // `foo` and `bar` shouldn't resolve to the same file + + assert_eq!(&src, bar_module.search_path().unwrap()); + assert_eq!(&bar, bar_module.file().unwrap().path(&db)); + assert_eq!(&foo, foo_module.file().unwrap().path(&db)); + + assert_ne!(&foo_module, &bar_module); + + assert_eq!( + Some(foo_module), + path_to_module(&db, &FilePath::System(foo)) + ); + assert_eq!( + Some(bar_module), + path_to_module(&db, &FilePath::System(bar)) + ); + + Ok(()) + } + + #[test] + fn deleting_an_unrelated_file_doesnt_change_module_resolution() { + let TestCase { mut db, src, .. } = TestCaseBuilder::new() + .with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")]) + .with_python_version(PythonVersion::PY38) + .build(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + + let bar_path = src.join("bar.py"); + let bar = system_path_to_file(&db, &bar_path).expect("bar.py to exist"); + + db.clear_salsa_events(); + + // Delete `bar.py` + db.memory_file_system().remove_file(&bar_path).unwrap(); + bar.sync(&mut db); + + // Re-query the foo module. The foo module should still be cached because `bar.py` isn't relevant + // for resolving `foo`. + + let foo_module2 = resolve_module(&db, &foo_module_name); + + assert!( + !db.take_salsa_events() + .iter() + .any(|event| { matches!(event.kind, salsa::EventKind::WillExecute { .. }) }) + ); + + assert_eq!(Some(foo_module), foo_module2); + } + + #[test] + fn adding_file_on_which_module_resolution_depends_invalidates_previously_failing_query_that_now_succeeds() + -> anyhow::Result<()> { + let TestCase { mut db, src, .. } = TestCaseBuilder::new().build(); + let foo_path = src.join("foo.py"); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + assert_eq!(resolve_module(&db, &foo_module_name), None); + + // Now write the foo file + db.write_file(&foo_path, "x = 1")?; + + let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist"); + + let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve"); + assert_eq!(foo_file, foo_module.file().unwrap()); + + Ok(()) + } + + #[test] + fn removing_file_on_which_module_resolution_depends_invalidates_previously_successful_query_that_now_fails() + -> anyhow::Result<()> { + const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/__init__.py", "x = 2")]; + + let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_module = resolve_module(&db, &foo_module_name).expect("foo module to exist"); + let foo_init_path = src.join("foo/__init__.py"); + + assert_eq!(&foo_init_path, foo_module.file().unwrap().path(&db)); + + // Delete `foo/__init__.py` and the `foo` folder. `foo` should now resolve to `foo.py` + db.memory_file_system().remove_file(&foo_init_path)?; + db.memory_file_system() + .remove_directory(foo_init_path.parent().unwrap())?; + File::sync_path(&mut db, &foo_init_path); + File::sync_path(&mut db, foo_init_path.parent().unwrap()); + + let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve"); + assert_eq!(&src.join("foo.py"), foo_module.file().unwrap().path(&db)); + + Ok(()) + } + + #[test] + fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() { + const TYPESHED: MockedTypeshed = MockedTypeshed { + versions: "functools: 3.8-", + stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], + }; + + let TestCase { + mut db, + stdlib, + site_packages, + .. + } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let functools_module_name = ModuleName::new_static("functools").unwrap(); + let stdlib_functools_path = stdlib.join("functools.pyi"); + + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + assert_eq!(functools_module.search_path().unwrap(), &stdlib); + assert_eq!( + Ok(functools_module.file().unwrap()), + system_path_to_file(&db, &stdlib_functools_path) + ); + + // Adding a file to site-packages does not invalidate the query, + // since site-packages takes lower priority in the module resolution + db.clear_salsa_events(); + let site_packages_functools_path = site_packages.join("functools.py"); + db.write_file(&site_packages_functools_path, "f: int") + .unwrap(); + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + let events = db.take_salsa_events(); + assert_function_query_was_not_run( + &db, + resolve_module_query, + ModuleNameIngredient::new(&db, functools_module_name), + &events, + ); + assert_eq!(functools_module.search_path().unwrap(), &stdlib); + assert_eq!( + Ok(functools_module.file().unwrap()), + system_path_to_file(&db, &stdlib_functools_path) + ); + } + + #[test] + fn adding_file_to_search_path_with_higher_priority_invalidates_the_query() { + const TYPESHED: MockedTypeshed = MockedTypeshed { + versions: "functools: 3.8-", + stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], + }; + + let TestCase { + mut db, + stdlib, + src, + .. + } = TestCaseBuilder::new() + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let functools_module_name = ModuleName::new_static("functools").unwrap(); + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + assert_eq!(functools_module.search_path().unwrap(), &stdlib); + assert_eq!( + Ok(functools_module.file().unwrap()), + system_path_to_file(&db, stdlib.join("functools.pyi")) + ); + + // Adding a first-party file invalidates the query, + // since first-party files take higher priority in module resolution: + let src_functools_path = src.join("functools.py"); + db.write_file(&src_functools_path, "FOO: int").unwrap(); + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + assert_eq!(functools_module.search_path().unwrap(), &src); + assert_eq!( + Ok(functools_module.file().unwrap()), + system_path_to_file(&db, &src_functools_path) + ); + } + + #[test] + fn deleting_file_from_higher_priority_search_path_invalidates_the_query() { + const SRC: &[FileSpec] = &[("functools.py", "FOO: int")]; + + const TYPESHED: MockedTypeshed = MockedTypeshed { + versions: "functools: 3.8-", + stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")], + }; + + let TestCase { + mut db, + stdlib, + src, + .. + } = TestCaseBuilder::new() + .with_src_files(SRC) + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY38) + .build(); + + let functools_module_name = ModuleName::new_static("functools").unwrap(); + let src_functools_path = src.join("functools.py"); + + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + assert_eq!(functools_module.search_path().unwrap(), &src); + assert_eq!( + Ok(functools_module.file().unwrap()), + system_path_to_file(&db, &src_functools_path) + ); + + // If we now delete the first-party file, + // it should resolve to the stdlib: + db.memory_file_system() + .remove_file(&src_functools_path) + .unwrap(); + File::sync_path(&mut db, &src_functools_path); + let functools_module = resolve_module(&db, &functools_module_name).unwrap(); + assert_eq!(functools_module.search_path().unwrap(), &stdlib); + assert_eq!( + Ok(functools_module.file().unwrap()), + system_path_to_file(&db, stdlib.join("functools.pyi")) + ); + } + + #[test] + fn editable_install_absolute_path() { + const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")]; + let x_directory = [("/x/src/foo/__init__.py", ""), ("/x/src/foo/bar.py", "")]; + + let TestCase { mut db, .. } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + db.write_files(x_directory).unwrap(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap(); + + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + let foo_bar_module = resolve_module(&db, &foo_bar_module_name).unwrap(); + + assert_eq!( + foo_module.file().unwrap().path(&db), + &FilePath::system("/x/src/foo/__init__.py") + ); + assert_eq!( + foo_bar_module.file().unwrap().path(&db), + &FilePath::system("/x/src/foo/bar.py") + ); + } + + #[test] + fn editable_install_pth_file_with_whitespace() { + const SITE_PACKAGES: &[FileSpec] = &[ + ("_foo.pth", " /x/src"), + ("_bar.pth", "/y/src "), + ]; + let external_files = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")]; + + let TestCase { mut db, .. } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + db.write_files(external_files).unwrap(); + + // Lines with leading whitespace in `.pth` files do not parse: + let foo_module_name = ModuleName::new_static("foo").unwrap(); + assert_eq!(resolve_module(&db, &foo_module_name), None); + + // Lines with trailing whitespace in `.pth` files do: + let bar_module_name = ModuleName::new_static("bar").unwrap(); + let bar_module = resolve_module(&db, &bar_module_name).unwrap(); + assert_eq!( + bar_module.file().unwrap().path(&db), + &FilePath::system("/y/src/bar.py") + ); + } + + #[test] + fn editable_install_relative_path() { + const SITE_PACKAGES: &[FileSpec] = &[ + ("_foo.pth", "../../x/../x/y/src"), + ("../x/y/src/foo.pyi", ""), + ]; + + let TestCase { db, .. } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + + assert_eq!( + foo_module.file().unwrap().path(&db), + &FilePath::system("/x/y/src/foo.pyi") + ); + } + + #[test] + fn editable_install_multiple_pth_files_with_multiple_paths() { + const COMPLEX_PTH_FILE: &str = "\ +/ + +# a comment +/baz + +import not_an_editable_install; do_something_else_crazy_dynamic() + +# another comment +spam + +not_a_directory +"; + + const SITE_PACKAGES: &[FileSpec] = &[ + ("_foo.pth", "../../x/../x/y/src"), + ("_lots_of_others.pth", COMPLEX_PTH_FILE), + ("../x/y/src/foo.pyi", ""), + ("spam/spam.py", ""), + ]; + + let root_files = [("/a.py", ""), ("/baz/b.py", "")]; + + let TestCase { + mut db, + site_packages, + .. + } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + db.write_files(root_files).unwrap(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let a_module_name = ModuleName::new_static("a").unwrap(); + let b_module_name = ModuleName::new_static("b").unwrap(); + let spam_module_name = ModuleName::new_static("spam").unwrap(); + + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + let a_module = resolve_module(&db, &a_module_name).unwrap(); + let b_module = resolve_module(&db, &b_module_name).unwrap(); + let spam_module = resolve_module(&db, &spam_module_name).unwrap(); + + assert_eq!( + foo_module.file().unwrap().path(&db), + &FilePath::system("/x/y/src/foo.pyi") + ); + assert_eq!( + a_module.file().unwrap().path(&db), + &FilePath::system("/a.py") + ); + assert_eq!( + b_module.file().unwrap().path(&db), + &FilePath::system("/baz/b.py") + ); + assert_eq!( + spam_module.file().unwrap().path(&db), + &FilePath::System(site_packages.join("spam/spam.py")) + ); + } + + #[test] + fn module_resolution_paths_cached_between_different_module_resolutions() { + const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src"), ("_bar.pth", "/y/src")]; + let external_directories = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")]; + + let TestCase { mut db, .. } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + db.write_files(external_directories).unwrap(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let bar_module_name = ModuleName::new_static("bar").unwrap(); + + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + assert_eq!( + foo_module.file().unwrap().path(&db), + &FilePath::system("/x/src/foo.py") + ); + + db.clear_salsa_events(); + let bar_module = resolve_module(&db, &bar_module_name).unwrap(); + assert_eq!( + bar_module.file().unwrap().path(&db), + &FilePath::system("/y/src/bar.py") + ); + let events = db.take_salsa_events(); + assert_const_function_query_was_not_run(&db, dynamic_resolution_paths, &events); + } + + #[test] + fn deleting_pth_file_on_which_module_resolution_depends_invalidates_cache() { + const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")]; + let x_directory = [("/x/src/foo.py", "")]; + + let TestCase { + mut db, + site_packages, + .. + } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + db.write_files(x_directory).unwrap(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + assert_eq!( + foo_module.file().unwrap().path(&db), + &FilePath::system("/x/src/foo.py") + ); + + db.memory_file_system() + .remove_file(site_packages.join("_foo.pth")) + .unwrap(); + + File::sync_path(&mut db, &site_packages.join("_foo.pth")); + + assert_eq!(resolve_module(&db, &foo_module_name), None); + } + + #[test] + fn deleting_editable_install_on_which_module_resolution_depends_invalidates_cache() { + const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")]; + let x_directory = [("/x/src/foo.py", "")]; + + let TestCase { mut db, .. } = TestCaseBuilder::new() + .with_site_packages_files(SITE_PACKAGES) + .build(); + + db.write_files(x_directory).unwrap(); + + let foo_module_name = ModuleName::new_static("foo").unwrap(); + let foo_module = resolve_module(&db, &foo_module_name).unwrap(); + let src_path = SystemPathBuf::from("/x/src"); + assert_eq!( + foo_module.file().unwrap().path(&db), + &FilePath::System(src_path.join("foo.py")) + ); + + db.memory_file_system() + .remove_file(src_path.join("foo.py")) + .unwrap(); + db.memory_file_system().remove_directory(&src_path).unwrap(); + File::sync_path(&mut db, &src_path.join("foo.py")); + File::sync_path(&mut db, &src_path); + assert_eq!(resolve_module(&db, &foo_module_name), None); + } + + #[test] + fn no_duplicate_search_paths_added() { + let TestCase { db, .. } = TestCaseBuilder::new() + .with_src_files(&[("foo.py", "")]) + .with_site_packages_files(&[("_foo.pth", "/src")]) + .build(); + + let search_paths: Vec<&SearchPath> = search_paths(&db).collect(); + + assert!(search_paths.contains( + &&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap() + )); + assert!( + !search_paths.contains( + &&SearchPath::editable(db.system(), SystemPathBuf::from("/src")).unwrap() + ) + ); + } + + #[test] + fn multiple_site_packages_with_editables() { + let mut db = TestDb::new(); + + let venv_site_packages = SystemPathBuf::from("/venv-site-packages"); + let site_packages_pth = venv_site_packages.join("foo.pth"); + let system_site_packages = SystemPathBuf::from("/system-site-packages"); + let editable_install_location = SystemPathBuf::from("/x/y/a.py"); + let system_site_packages_location = system_site_packages.join("a.py"); + + db.memory_file_system() + .create_directory_all("/src") + .unwrap(); + db.write_files([ + (&site_packages_pth, "/x/y"), + (&editable_install_location, ""), + (&system_site_packages_location, ""), + ]) + .unwrap(); + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings { + site_packages_paths: vec![venv_site_packages, system_site_packages], + ..SearchPathSettings::new(vec![SystemPathBuf::from("/src")]) + } + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search path settings"), + }, + ); + + // The editable installs discovered from the `.pth` file in the first `site-packages` directory + // take precedence over the second `site-packages` directory... + let a_module_name = ModuleName::new_static("a").unwrap(); + let a_module = resolve_module(&db, &a_module_name).unwrap(); + assert_eq!( + a_module.file().unwrap().path(&db), + &editable_install_location + ); + + db.memory_file_system() + .remove_file(&site_packages_pth) + .unwrap(); + File::sync_path(&mut db, &site_packages_pth); + + // ...But now that the `.pth` file in the first `site-packages` directory has been deleted, + // the editable install no longer exists, so the module now resolves to the file in the + // second `site-packages` directory + let a_module = resolve_module(&db, &a_module_name).unwrap(); + assert_eq!( + a_module.file().unwrap().path(&db), + &system_site_packages_location + ); + } + + #[test] + #[cfg(unix)] + fn case_sensitive_resolution_with_symlinked_directory() -> anyhow::Result<()> { + use anyhow::Context; + use ruff_db::system::OsSystem; + + let temp_dir = tempfile::TempDir::new()?; + let root = SystemPathBuf::from_path_buf( + temp_dir + .path() + .canonicalize() + .context("Failed to canonicalized path")?, + ) + .expect("UTF8 path for temp dir"); + + let mut db = TestDb::new(); + + let src = root.join("src"); + let a_package_target = root.join("a-package"); + let a_src = src.join("a"); + + db.use_system(OsSystem::new(&root)); + + db.write_file( + a_package_target.join("__init__.py"), + "class Foo: x: int = 4", + ) + .context("Failed to write `a-package/__init__.py`")?; + + db.write_file(src.join("main.py"), "print('Hy')") + .context("Failed to write `main.py`")?; + + // The symlink triggers the slow-path in the `OsSystem`'s `exists_path_case_sensitive` + // code because canonicalizing the path for `a/__init__.py` results in `a-package/__init__.py` + std::os::unix::fs::symlink(a_package_target.as_std_path(), a_src.as_std_path()) + .context("Failed to symlink `src/a` to `a-package`")?; + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![src]) + .to_search_paths(db.system(), db.vendored()) + .expect("valid search path settings"), + }, + ); + + // Now try to resolve the module `A` (note the capital `A` instead of `a`). + let a_module_name = ModuleName::new_static("A").unwrap(); + assert_eq!(resolve_module(&db, &a_module_name), None); + + // Now lookup the same module using the lowercase `a` and it should resolve to the file in the system site-packages + let a_module_name = ModuleName::new_static("a").unwrap(); + let a_module = resolve_module(&db, &a_module_name).expect("a.py to resolve"); + assert!( + a_module + .file() + .unwrap() + .path(&db) + .as_str() + .ends_with("src/a/__init__.py"), + ); + + Ok(()) + } + + #[test] + fn file_to_module_where_one_search_path_is_subdirectory_of_other() { + let project_directory = SystemPathBuf::from("/project"); + let site_packages = project_directory.join(".venv/lib/python3.13/site-packages"); + let installed_foo_module = site_packages.join("foo/__init__.py"); + + let mut db = TestDb::new(); + db.write_file(&installed_foo_module, "").unwrap(); + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings { + site_packages_paths: vec![site_packages.clone()], + ..SearchPathSettings::new(vec![project_directory]) + } + .to_search_paths(db.system(), db.vendored()) + .unwrap(), + }, + ); + + let foo_module_file = File::new(&db, FilePath::System(installed_foo_module)); + let module = file_to_module(&db, foo_module_file).unwrap(); + assert_eq!(module.search_path().unwrap(), &site_packages); + } +} diff --git a/crates/red_knot_python_semantic/src/module_resolver/testing.rs b/crates/ty_python_semantic/src/module_resolver/testing.rs similarity index 90% rename from crates/red_knot_python_semantic/src/module_resolver/testing.rs rename to crates/ty_python_semantic/src/module_resolver/testing.rs index 75e0dfdc233b5..9b06764e163dd 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/testing.rs +++ b/crates/ty_python_semantic/src/module_resolver/testing.rs @@ -1,3 +1,4 @@ +use ruff_db::Db; use ruff_db::system::{ DbWithTestSystem as _, DbWithWritableSystem as _, SystemPath, SystemPathBuf, }; @@ -6,7 +7,7 @@ use ruff_python_ast::PythonVersion; use crate::db::tests::TestDb; use crate::program::{Program, SearchPathSettings}; -use crate::{ProgramSettings, PythonPath, PythonPlatform}; +use crate::{ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource}; /// A test case for the module resolver. /// @@ -235,17 +236,20 @@ impl TestCaseBuilder { Program::from_settings( &db, ProgramSettings { - python_version, + python_version: PythonVersionWithSource { + version: python_version, + source: PythonVersionSource::default(), + }, python_platform, search_paths: SearchPathSettings { - extra_paths: vec![], - src_roots: vec![src.clone()], custom_typeshed: Some(typeshed.clone()), - python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]), - }, + site_packages_paths: vec![site_packages.clone()], + ..SearchPathSettings::new(vec![src.clone()]) + } + .to_search_paths(db.system(), db.vendored()) + .expect("valid search path settings"), }, - ) - .expect("Valid program settings"); + ); TestCase { db, @@ -293,15 +297,19 @@ impl TestCaseBuilder { Program::from_settings( &db, ProgramSettings { - python_version, + python_version: PythonVersionWithSource { + version: python_version, + source: PythonVersionSource::default(), + }, python_platform, search_paths: SearchPathSettings { - python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]), + site_packages_paths: vec![site_packages.clone()], ..SearchPathSettings::new(vec![src.clone()]) - }, + } + .to_search_paths(db.system(), db.vendored()) + .expect("valid search path settings"), }, - ) - .expect("Valid search path settings"); + ); TestCase { db, diff --git a/crates/red_knot_python_semantic/src/module_resolver/typeshed.rs b/crates/ty_python_semantic/src/module_resolver/typeshed.rs similarity index 83% rename from crates/red_knot_python_semantic/src/module_resolver/typeshed.rs rename to crates/ty_python_semantic/src/module_resolver/typeshed.rs index 8a3fe2e645690..432c9bd4fa677 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/typeshed.rs +++ b/crates/ty_python_semantic/src/module_resolver/typeshed.rs @@ -4,16 +4,19 @@ use std::num::{NonZeroU16, NonZeroUsize}; use std::ops::{RangeFrom, RangeInclusive}; use std::str::FromStr; -use ruff_python_ast::PythonVersion; +use ruff_db::vendored::VendoredFileSystem; +use ruff_python_ast::{PythonVersion, PythonVersionDeserializationError}; use rustc_hash::FxHashMap; +use crate::Program; use crate::db::Db; use crate::module_name::ModuleName; -use crate::Program; -pub(in crate::module_resolver) fn vendored_typeshed_versions(db: &dyn Db) -> TypeshedVersions { +pub(in crate::module_resolver) fn vendored_typeshed_versions( + vendored: &VendoredFileSystem, +) -> TypeshedVersions { TypeshedVersions::from_str( - &db.vendored() + &vendored .read_to_string("stdlib/VERSIONS") .expect("The vendored typeshed stubs should contain a VERSIONS file"), ) @@ -25,7 +28,7 @@ pub(crate) fn typeshed_versions(db: &dyn Db) -> &TypeshedVersions { } #[derive(Debug, PartialEq, Eq, Clone)] -pub(crate) struct TypeshedVersionsParseError { +pub struct TypeshedVersionsParseError { line_number: Option, reason: TypeshedVersionsParseErrorKind, } @@ -49,63 +52,34 @@ impl fmt::Display for TypeshedVersionsParseError { impl std::error::Error for TypeshedVersionsParseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - if let TypeshedVersionsParseErrorKind::IntegerParsingFailure { err, .. } = &self.reason { - Some(err) + if let TypeshedVersionsParseErrorKind::VersionParseError(err) = &self.reason { + err.source() } else { None } } } -#[derive(Debug, PartialEq, Eq, Clone)] -pub(super) enum TypeshedVersionsParseErrorKind { +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] +pub(crate) enum TypeshedVersionsParseErrorKind { + #[error("File has too many lines ({0}); maximum allowed is {max_allowed}", max_allowed = NonZeroU16::MAX)] TooManyLines(NonZeroUsize), + #[error("Expected every non-comment line to have exactly one colon")] UnexpectedNumberOfColons, + #[error("Expected all components of '{0}' to be valid Python identifiers")] InvalidModuleName(String), + #[error("Expected every non-comment line to have exactly one '-' character")] UnexpectedNumberOfHyphens, - UnexpectedNumberOfPeriods(String), - IntegerParsingFailure { - version: String, - err: std::num::ParseIntError, - }, + #[error("{0}")] + VersionParseError(#[from] PythonVersionDeserializationError), } -impl fmt::Display for TypeshedVersionsParseErrorKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TooManyLines(num_lines) => write!( - f, - "File has too many lines ({num_lines}); maximum allowed is {}", - NonZeroU16::MAX - ), - Self::UnexpectedNumberOfColons => { - f.write_str("Expected every non-comment line to have exactly one colon") - } - Self::InvalidModuleName(name) => write!( - f, - "Expected all components of '{name}' to be valid Python identifiers" - ), - Self::UnexpectedNumberOfHyphens => { - f.write_str("Expected every non-comment line to have exactly one '-' character") - } - Self::UnexpectedNumberOfPeriods(format) => write!( - f, - "Expected all versions to be in the form {{MAJOR}}.{{MINOR}}; got '{format}'" - ), - Self::IntegerParsingFailure { version, err } => write!( - f, - "Failed to convert '{version}' to a pair of integers due to {err}", - ), - } - } -} - -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct TypeshedVersions(FxHashMap); impl TypeshedVersions { #[must_use] - fn exact(&self, module_name: &ModuleName) -> Option<&PyVersionRange> { + pub(crate) fn exact(&self, module_name: &ModuleName) -> Option<&PyVersionRange> { self.0.get(module_name) } @@ -237,7 +211,7 @@ impl FromStr for TypeshedVersions { return Err(TypeshedVersionsParseError { line_number: Some(line_number), reason, - }) + }); } }; } @@ -257,19 +231,44 @@ impl fmt::Display for TypeshedVersions { } #[derive(Debug, Clone, Eq, PartialEq, Hash)] -enum PyVersionRange { +pub(crate) enum PyVersionRange { AvailableFrom(RangeFrom), AvailableWithin(RangeInclusive), } impl PyVersionRange { #[must_use] - fn contains(&self, version: PythonVersion) -> bool { + pub(crate) fn contains(&self, version: PythonVersion) -> bool { match self { Self::AvailableFrom(inner) => inner.contains(&version), Self::AvailableWithin(inner) => inner.contains(&version), } } + + /// Display the version range in a way that is suitable for rendering in user-facing diagnostics. + pub(crate) fn diagnostic_display(&self) -> impl std::fmt::Display { + struct DiagnosticDisplay<'a>(&'a PyVersionRange); + + impl fmt::Display for DiagnosticDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + PyVersionRange::AvailableFrom(range_from) => write!(f, "{}+", range_from.start), + PyVersionRange::AvailableWithin(range_inclusive) => { + // Don't trust the start Python version if it's 3.0 or lower. + // Typeshed doesn't attempt to give accurate start versions if a module was added + // in the Python 2 era. + if range_inclusive.start() <= &(PythonVersion { major: 3, minor: 0 }) { + write!(f, "<={}", range_inclusive.end()) + } else { + write!(f, "{}-{}", range_inclusive.start(), range_inclusive.end()) + } + } + } + } + } + + DiagnosticDisplay(self) + } } impl FromStr for PyVersionRange { @@ -279,12 +278,12 @@ impl FromStr for PyVersionRange { let mut parts = s.split('-').map(str::trim); match (parts.next(), parts.next(), parts.next()) { (Some(lower), Some(""), None) => { - let lower = python_version_from_versions_file_string(lower)?; + let lower = PythonVersion::from_str(lower)?; Ok(Self::AvailableFrom(lower..)) } (Some(lower), Some(upper), None) => { - let lower = python_version_from_versions_file_string(lower)?; - let upper = python_version_from_versions_file_string(upper)?; + let lower = PythonVersion::from_str(lower)?; + let upper = PythonVersion::from_str(upper)?; Ok(Self::AvailableWithin(lower..=upper)) } _ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens), @@ -303,34 +302,14 @@ impl fmt::Display for PyVersionRange { } } -fn python_version_from_versions_file_string( - s: &str, -) -> Result { - let mut parts = s.split('.').map(str::trim); - let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else { - return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods( - s.to_string(), - )); - }; - PythonVersion::try_from((major, minor)).map_err(|int_parse_error| { - TypeshedVersionsParseErrorKind::IntegerParsingFailure { - version: s.to_string(), - err: int_parse_error, - } - }) -} - #[cfg(test)] mod tests { use std::fmt::Write as _; use std::num::{IntErrorKind, NonZeroU16}; use std::path::Path; - use insta::assert_snapshot; - - use crate::db::tests::TestDb; - use super::*; + use insta::assert_snapshot; const TYPESHED_STDLIB_DIR: &str = "stdlib"; @@ -350,9 +329,7 @@ mod tests { #[test] fn can_parse_vendored_versions_file() { - let db = TestDb::new(); - - let versions = vendored_typeshed_versions(&db); + let versions = vendored_typeshed_versions(ty_vendored::file_system()); assert!(versions.len() > 100); assert!(versions.len() < 1000); @@ -389,10 +366,9 @@ mod tests { #[test] fn typeshed_versions_consistent_with_vendored_stubs() { - let db = TestDb::new(); - let vendored_typeshed_versions = vendored_typeshed_versions(&db); + let vendored_typeshed_versions = vendored_typeshed_versions(ty_vendored::file_system()); let vendored_typeshed_dir = - Path::new(env!("CARGO_MANIFEST_DIR")).join("../red_knot_vendored/vendor/typeshed"); + Path::new(env!("CARGO_MANIFEST_DIR")).join("../ty_vendored/vendor/typeshed"); let mut empty_iterator = true; @@ -656,15 +632,17 @@ foo: 3.8- # trailing comment TypeshedVersions::from_str("foo: 38-"), Err(TypeshedVersionsParseError { line_number: ONE, - reason: TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods("38".to_string()) + reason: TypeshedVersionsParseErrorKind::VersionParseError( + PythonVersionDeserializationError::WrongPeriodNumber(Box::from("38")) + ) }) ); assert_eq!( TypeshedVersions::from_str("foo: 3..8-"), Err(TypeshedVersionsParseError { line_number: ONE, - reason: TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods( - "3..8".to_string() + reason: TypeshedVersionsParseErrorKind::VersionParseError( + PythonVersionDeserializationError::WrongPeriodNumber(Box::from("3..8")) ) }) ); @@ -672,8 +650,8 @@ foo: 3.8- # trailing comment TypeshedVersions::from_str("foo: 3.8-3..11"), Err(TypeshedVersionsParseError { line_number: ONE, - reason: TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods( - "3..11".to_string() + reason: TypeshedVersionsParseErrorKind::VersionParseError( + PythonVersionDeserializationError::WrongPeriodNumber(Box::from("3..11")) ) }) ); @@ -683,20 +661,30 @@ foo: 3.8- # trailing comment fn invalid_typeshed_versions_non_digits() { let err = TypeshedVersions::from_str("foo: 1.two-").unwrap_err(); assert_eq!(err.line_number, ONE); - let TypeshedVersionsParseErrorKind::IntegerParsingFailure { version, err } = err.reason + let TypeshedVersionsParseErrorKind::VersionParseError( + PythonVersionDeserializationError::InvalidMinorVersion(invalid_minor, parse_error), + ) = err.reason else { - panic!() + panic!( + "Expected an invalid-minor-version parse error, got `{}`", + err.reason + ) }; - assert_eq!(version, "1.two".to_string()); - assert_eq!(*err.kind(), IntErrorKind::InvalidDigit); + assert_eq!(&*invalid_minor, "two"); + assert_eq!(*parse_error.kind(), IntErrorKind::InvalidDigit); let err = TypeshedVersions::from_str("foo: 3.8-four.9").unwrap_err(); assert_eq!(err.line_number, ONE); - let TypeshedVersionsParseErrorKind::IntegerParsingFailure { version, err } = err.reason + let TypeshedVersionsParseErrorKind::VersionParseError( + PythonVersionDeserializationError::InvalidMajorVersion(invalid_major, parse_error), + ) = err.reason else { - panic!() + panic!( + "Expected an invalid-major-version parse error, got `{}`", + err.reason + ) }; - assert_eq!(version, "four.9".to_string()); - assert_eq!(*err.kind(), IntErrorKind::InvalidDigit); + assert_eq!(&*invalid_major, "four"); + assert_eq!(*parse_error.kind(), IntErrorKind::InvalidDigit); } } diff --git a/crates/ty_python_semantic/src/node_key.rs b/crates/ty_python_semantic/src/node_key.rs new file mode 100644 index 0000000000000..18edfe1a043e7 --- /dev/null +++ b/crates/ty_python_semantic/src/node_key.rs @@ -0,0 +1,14 @@ +use ruff_python_ast::{HasNodeIndex, NodeIndex}; + +/// Compact key for a node for use in a hash map. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] +pub(super) struct NodeKey(NodeIndex); + +impl NodeKey { + pub(super) fn from_node(node: N) -> Self + where + N: HasNodeIndex, + { + NodeKey(node.node_index().load()) + } +} diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs new file mode 100644 index 0000000000000..0c1901b5911d9 --- /dev/null +++ b/crates/ty_python_semantic/src/place.rs @@ -0,0 +1,1571 @@ +use ruff_db::files::File; + +use crate::dunder_all::dunder_all_names; +use crate::module_resolver::file_to_module; +use crate::semantic_index::definition::{Definition, DefinitionState}; +use crate::semantic_index::place::{PlaceExpr, ScopeId, ScopedPlaceId}; +use crate::semantic_index::{ + BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, place_table, +}; +use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map}; +use crate::types::{ + DynamicType, KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, + UnionType, binding_type, declaration_type, todo_type, +}; +use crate::{Db, FxOrderSet, KnownModule, Program, resolve_module}; + +pub(crate) use implicit_globals::{ + module_type_implicit_global_declaration, module_type_implicit_global_symbol, +}; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)] +pub(crate) enum Boundness { + Bound, + PossiblyUnbound, +} + +impl Boundness { + pub(crate) const fn max(self, other: Self) -> Self { + match (self, other) { + (Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound, + (Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound, + } + } +} + +/// The result of a place lookup, which can either be a (possibly unbound) type +/// or a completely unbound place. +/// +/// Consider this example: +/// ```py +/// bound = 1 +/// +/// if flag: +/// possibly_unbound = 2 +/// ``` +/// +/// If we look up places in this scope, we would get the following results: +/// ```rs +/// bound: Place::Type(Type::IntLiteral(1), Boundness::Bound), +/// possibly_unbound: Place::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound), +/// non_existent: Place::Unbound, +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) enum Place<'db> { + Type(Type<'db>, Boundness), + Unbound, +} + +impl<'db> Place<'db> { + /// Constructor that creates a `Place` with boundness [`Boundness::Bound`]. + pub(crate) fn bound(ty: impl Into>) -> Self { + Place::Type(ty.into(), Boundness::Bound) + } + + /// Constructor that creates a [`Place`] with a [`crate::types::TodoType`] type + /// and boundness [`Boundness::Bound`]. + #[allow(unused_variables)] // Only unused in release builds + pub(crate) fn todo(message: &'static str) -> Self { + Place::Type(todo_type!(message), Boundness::Bound) + } + + pub(crate) fn is_unbound(&self) -> bool { + matches!(self, Place::Unbound) + } + + /// Returns the type of the place, ignoring possible unboundness. + /// + /// If the place is *definitely* unbound, this function will return `None`. Otherwise, + /// if there is at least one control-flow path where the place is bound, return the type. + pub(crate) fn ignore_possibly_unbound(&self) -> Option> { + match self { + Place::Type(ty, _) => Some(*ty), + Place::Unbound => None, + } + } + + #[cfg(test)] + #[track_caller] + pub(crate) fn expect_type(self) -> Type<'db> { + self.ignore_possibly_unbound() + .expect("Expected a (possibly unbound) type, not an unbound place") + } + + #[must_use] + pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Place<'db> { + match self { + Place::Type(ty, boundness) => Place::Type(f(ty), boundness), + Place::Unbound => Place::Unbound, + } + } + + #[must_use] + pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> PlaceAndQualifiers<'db> { + PlaceAndQualifiers { + place: self, + qualifiers, + } + } + + /// Try to call `__get__(None, owner)` on the type of this place (not on the meta type). + /// If it succeeds, return the `__get__` return type. Otherwise, returns the original place. + /// This is used to resolve (potential) descriptor attributes. + pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Place<'db> { + match self { + Place::Type(Type::Union(union), boundness) => union.map_with_boundness(db, |elem| { + Place::Type(*elem, boundness).try_call_dunder_get(db, owner) + }), + + Place::Type(Type::Intersection(intersection), boundness) => intersection + .map_with_boundness(db, |elem| { + Place::Type(*elem, boundness).try_call_dunder_get(db, owner) + }), + + Place::Type(self_ty, boundness) => { + if let Some((dunder_get_return_ty, _)) = + self_ty.try_call_dunder_get(db, Type::none(db), owner) + { + Place::Type(dunder_get_return_ty, boundness) + } else { + self + } + } + + Place::Unbound => Place::Unbound, + } + } +} + +impl<'db> From> for PlaceAndQualifiers<'db> { + fn from(value: LookupResult<'db>) -> Self { + match value { + Ok(type_and_qualifiers) => { + Place::Type(type_and_qualifiers.inner_type(), Boundness::Bound) + .with_qualifiers(type_and_qualifiers.qualifiers()) + } + Err(LookupError::Unbound(qualifiers)) => Place::Unbound.with_qualifiers(qualifiers), + Err(LookupError::PossiblyUnbound(type_and_qualifiers)) => { + Place::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound) + .with_qualifiers(type_and_qualifiers.qualifiers()) + } + } + } +} + +/// Possible ways in which a place lookup can (possibly or definitely) fail. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum LookupError<'db> { + Unbound(TypeQualifiers), + PossiblyUnbound(TypeAndQualifiers<'db>), +} + +impl<'db> LookupError<'db> { + /// Fallback (wholly or partially) to `fallback` to create a new [`LookupResult`]. + pub(crate) fn or_fall_back_to( + self, + db: &'db dyn Db, + fallback: PlaceAndQualifiers<'db>, + ) -> LookupResult<'db> { + let fallback = fallback.into_lookup_result(); + match (&self, &fallback) { + (LookupError::Unbound(_), _) => fallback, + (LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound(_))) => Err(self), + (LookupError::PossiblyUnbound(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new( + UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), + ty.qualifiers().union(ty2.qualifiers()), + )), + (LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => { + Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( + UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]), + ty.qualifiers().union(ty2.qualifiers()), + ))) + } + } + } +} + +/// A [`Result`] type in which the `Ok` variant represents a definitely bound place +/// and the `Err` variant represents a place that is either definitely or possibly unbound. +/// +/// Note that this type is exactly isomorphic to [`Place`]. +/// In the future, we could possibly consider removing `Place` and using this type everywhere instead. +pub(crate) type LookupResult<'db> = Result, LookupError<'db>>; + +/// Infer the public type of a symbol (its type as seen from outside its scope) in the given +/// `scope`. +#[allow(unused)] +pub(crate) fn symbol<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + name: &str, + considered_definitions: ConsideredDefinitions, +) -> PlaceAndQualifiers<'db> { + symbol_impl( + db, + scope, + name, + RequiresExplicitReExport::No, + considered_definitions, + ) +} + +/// Infer the public type of a place (its type as seen from outside its scope) in the given +/// `scope`. +pub(crate) fn place<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + expr: &PlaceExpr, + considered_definitions: ConsideredDefinitions, +) -> PlaceAndQualifiers<'db> { + place_impl( + db, + scope, + expr, + RequiresExplicitReExport::No, + considered_definitions, + ) +} + +/// Infer the public type of a class symbol (its type as seen from outside its scope) in the given +/// `scope`. +pub(crate) fn class_symbol<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + name: &str, +) -> PlaceAndQualifiers<'db> { + place_table(db, scope) + .place_id_by_name(name) + .map(|place| { + let place_and_quals = place_by_id( + db, + scope, + place, + RequiresExplicitReExport::No, + ConsideredDefinitions::EndOfScope, + ); + + if !place_and_quals.place.is_unbound() { + // Trust the declared type if we see a class-level declaration + return place_and_quals; + } + + if let PlaceAndQualifiers { + place: Place::Type(ty, _), + qualifiers, + } = place_and_quals + { + // Otherwise, we need to check if the symbol has bindings + let use_def = use_def_map(db, scope); + let bindings = use_def.end_of_scope_bindings(place); + let inferred = place_from_bindings_impl(db, bindings, RequiresExplicitReExport::No); + + // TODO: we should not need to calculate inferred type second time. This is a temporary + // solution until the notion of Boundness and Declaredness is split. See #16036, #16264 + match inferred { + Place::Unbound => Place::Unbound.with_qualifiers(qualifiers), + Place::Type(_, boundness) => { + Place::Type(ty, boundness).with_qualifiers(qualifiers) + } + } + } else { + Place::Unbound.into() + } + }) + .unwrap_or_default() +} + +/// Infers the public type of an explicit module-global symbol as seen from within the same file. +/// +/// Note that all global scopes also include various "implicit globals" such as `__name__`, +/// `__doc__` and `__file__`. This function **does not** consider those symbols; it will return +/// `Place::Unbound` for them. Use the (currently test-only) `global_symbol` query to also include +/// those additional symbols. +/// +/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). +pub(crate) fn explicit_global_symbol<'db>( + db: &'db dyn Db, + file: File, + name: &str, +) -> PlaceAndQualifiers<'db> { + symbol_impl( + db, + global_scope(db, file), + name, + RequiresExplicitReExport::No, + ConsideredDefinitions::AllReachable, + ) +} + +/// Infers the public type of an explicit module-global symbol as seen from within the same file. +/// +/// Unlike [`explicit_global_symbol`], this function also considers various "implicit globals" +/// such as `__name__`, `__doc__` and `__file__`. These are looked up as attributes on `types.ModuleType` +/// rather than being looked up as symbols explicitly defined/declared in the global scope. +/// +/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). +#[allow(unused)] +pub(crate) fn global_symbol<'db>( + db: &'db dyn Db, + file: File, + name: &str, +) -> PlaceAndQualifiers<'db> { + explicit_global_symbol(db, file, name) + .or_fall_back_to(db, || module_type_implicit_global_symbol(db, name)) +} + +/// Infers the public type of an imported symbol. +/// +/// If `requires_explicit_reexport` is [`None`], it will be inferred from the file's source type. +/// For stub files, explicit re-export will be required, while for non-stub files, it will not. +pub(crate) fn imported_symbol<'db>( + db: &'db dyn Db, + file: File, + name: &str, + requires_explicit_reexport: Option, +) -> PlaceAndQualifiers<'db> { + let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| { + if file.is_stub(db) { + RequiresExplicitReExport::Yes + } else { + RequiresExplicitReExport::No + } + }); + + // If it's not found in the global scope, check if it's present as an instance on + // `types.ModuleType` or `builtins.object`. + // + // We do a more limited version of this in `module_type_implicit_global_symbol`, + // but there are two crucial differences here: + // - If a member is looked up as an attribute, `__init__` is also available on the module, but + // it isn't available as a global from inside the module + // - If a member is looked up as an attribute, members on `builtins.object` are also available + // (because `types.ModuleType` inherits from `object`); these attributes are also not + // available as globals from inside the module. + // + // The same way as in `module_type_implicit_global_symbol`, however, we need to be careful to + // ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with + // dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which + // module we're dealing with. + symbol_impl( + db, + global_scope(db, file), + name, + requires_explicit_reexport, + ConsideredDefinitions::EndOfScope, + ) + .or_fall_back_to(db, || { + if name == "__getattr__" { + Place::Unbound.into() + } else if name == "__builtins__" { + Place::bound(Type::any()).into() + } else { + KnownClass::ModuleType.to_instance(db).member(db, name) + } + }) +} + +/// Lookup the type of `symbol` in the builtins namespace. +/// +/// Returns `Place::Unbound` if the `builtins` module isn't available for some reason. +/// +/// Note that this function is only intended for use in the context of the builtins *namespace* +/// and should not be used when a symbol is being explicitly imported from the `builtins` module +/// (e.g. `from builtins import int`). +pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { + resolve_module(db, &KnownModule::Builtins.name()) + .and_then(|module| { + let file = module.file()?; + Some( + symbol_impl( + db, + global_scope(db, file), + symbol, + RequiresExplicitReExport::Yes, + ConsideredDefinitions::EndOfScope, + ) + .or_fall_back_to(db, || { + // We're looking up in the builtins namespace and not the module, so we should + // do the normal lookup in `types.ModuleType` and not the special one as in + // `imported_symbol`. + module_type_implicit_global_symbol(db, symbol) + }), + ) + }) + .unwrap_or_default() +} + +/// Lookup the type of `symbol` in a given known module. +/// +/// Returns `Place::Unbound` if the given known module cannot be resolved for some reason. +pub(crate) fn known_module_symbol<'db>( + db: &'db dyn Db, + known_module: KnownModule, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + resolve_module(db, &known_module.name()) + .and_then(|module| { + let file = module.file()?; + Some(imported_symbol(db, file, symbol, None)) + }) + .unwrap_or_default() +} + +/// Lookup the type of `symbol` in the `typing` module namespace. +/// +/// Returns `Place::Unbound` if the `typing` module isn't available for some reason. +#[inline] +#[cfg(test)] +pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { + known_module_symbol(db, KnownModule::Typing, symbol) +} + +/// Lookup the type of `symbol` in the `typing_extensions` module namespace. +/// +/// Returns `Place::Unbound` if the `typing_extensions` module isn't available for some reason. +#[inline] +pub(crate) fn typing_extensions_symbol<'db>( + db: &'db dyn Db, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + known_module_symbol(db, KnownModule::TypingExtensions, symbol) +} + +/// Get the `builtins` module scope. +/// +/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`. +pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option> { + core_module_scope(db, KnownModule::Builtins) +} + +/// Get the scope of a core stdlib module. +/// +/// Can return `None` if a custom typeshed is used that is missing the core module in question. +fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option> { + let module = resolve_module(db, &core_module.name())?; + Some(global_scope(db, module.file()?)) +} + +/// Infer the combined type from an iterator of bindings, and return it +/// together with boundness information in a [`Place`]. +/// +/// The type will be a union if there are multiple bindings with different types. +pub(super) fn place_from_bindings<'db>( + db: &'db dyn Db, + bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, +) -> Place<'db> { + place_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No) +} + +/// Build a declared type from a [`DeclarationsIterator`]. +/// +/// If there is only one declaration, or all declarations declare the same type, returns +/// `Ok(..)`. If there are conflicting declarations, returns an `Err(..)` variant with +/// a union of the declared types as well as a list of all conflicting types. +/// +/// This function also returns declaredness information (see [`Place`]) and a set of +/// [`TypeQualifiers`] that have been specified on the declaration(s). +pub(crate) fn place_from_declarations<'db>( + db: &'db dyn Db, + declarations: DeclarationsIterator<'_, 'db>, +) -> PlaceFromDeclarationsResult<'db> { + place_from_declarations_impl(db, declarations, RequiresExplicitReExport::No) +} + +pub(crate) type DeclaredTypeAndConflictingTypes<'db> = + (TypeAndQualifiers<'db>, Box>>); + +/// The result of looking up a declared type from declarations; see [`place_from_declarations`]. +pub(crate) type PlaceFromDeclarationsResult<'db> = + Result, DeclaredTypeAndConflictingTypes<'db>>; + +/// A type with declaredness information, and a set of type qualifiers. +/// +/// This is used to represent the result of looking up the declared type. Consider this +/// example: +/// ```py +/// class C: +/// if flag: +/// variable: ClassVar[int] +/// ``` +/// If we look up the declared type of `variable` in the scope of class `C`, we will get +/// the type `int`, a "declaredness" of [`Boundness::PossiblyUnbound`], and the information +/// that this comes with a [`CLASS_VAR`] type qualifier. +/// +/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct PlaceAndQualifiers<'db> { + pub(crate) place: Place<'db>, + pub(crate) qualifiers: TypeQualifiers, +} + +impl Default for PlaceAndQualifiers<'_> { + fn default() -> Self { + PlaceAndQualifiers { + place: Place::Unbound, + qualifiers: TypeQualifiers::empty(), + } + } +} + +impl<'db> PlaceAndQualifiers<'db> { + /// Constructor that creates a [`PlaceAndQualifiers`] instance with a [`TodoType`] type + /// and no qualifiers. + /// + /// [`TodoType`]: crate::types::TodoType + pub(crate) fn todo(message: &'static str) -> Self { + Self { + place: Place::todo(message), + qualifiers: TypeQualifiers::empty(), + } + } + + /// Returns `true` if the place has a `ClassVar` type qualifier. + pub(crate) fn is_class_var(&self) -> bool { + self.qualifiers.contains(TypeQualifiers::CLASS_VAR) + } + + /// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type. + pub(crate) fn is_bare_final(&self) -> Option { + match self { + PlaceAndQualifiers { place, qualifiers } + if (qualifiers.contains(TypeQualifiers::FINAL) + && place + .ignore_possibly_unbound() + .is_some_and(|ty| ty.is_unknown())) => + { + Some(*qualifiers) + } + _ => None, + } + } + + #[must_use] + pub(crate) fn map_type( + self, + f: impl FnOnce(Type<'db>) -> Type<'db>, + ) -> PlaceAndQualifiers<'db> { + PlaceAndQualifiers { + place: self.place.map_type(f), + qualifiers: self.qualifiers, + } + } + + /// Transform place and qualifiers into a [`LookupResult`], + /// a [`Result`] type in which the `Ok` variant represents a definitely bound place + /// and the `Err` variant represents a place that is either definitely or possibly unbound. + pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { + match self { + PlaceAndQualifiers { + place: Place::Type(ty, Boundness::Bound), + qualifiers, + } => Ok(TypeAndQualifiers::new(ty, qualifiers)), + PlaceAndQualifiers { + place: Place::Type(ty, Boundness::PossiblyUnbound), + qualifiers, + } => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new( + ty, qualifiers, + ))), + PlaceAndQualifiers { + place: Place::Unbound, + qualifiers, + } => Err(LookupError::Unbound(qualifiers)), + } + } + + /// Safely unwrap the place and the qualifiers into a [`TypeQualifiers`]. + /// + /// If the place is definitely unbound or possibly unbound, it will be transformed into a + /// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning + /// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller + /// to ensure that a diagnostic is emitted if the place is possibly or definitely unbound. + pub(crate) fn unwrap_with_diagnostic( + self, + diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, + ) -> TypeAndQualifiers<'db> { + self.into_lookup_result().unwrap_or_else(diagnostic_fn) + } + + /// Fallback (partially or fully) to another place if `self` is partially or fully unbound. + /// + /// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`. + /// 2. Else, evaluate `fallback_fn()`: + /// 1. If `self` is definitely unbound, return the result of `fallback_fn()`. + /// 2. Else, if `fallback` is definitely unbound, return `self`. + /// 3. Else, if `self` is possibly unbound and `fallback` is definitely bound, + /// return `Place(, Boundness::Bound)` + /// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound, + /// return `Place(, Boundness::PossiblyUnbound)` + #[must_use] + pub(crate) fn or_fall_back_to( + self, + db: &'db dyn Db, + fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, + ) -> Self { + self.into_lookup_result() + .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) + .into() + } +} + +impl<'db> From> for PlaceAndQualifiers<'db> { + fn from(place: Place<'db>) -> Self { + place.with_qualifiers(TypeQualifiers::empty()) + } +} + +fn place_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &PlaceAndQualifiers<'db>, + _count: u32, + _scope: ScopeId<'db>, + _place_id: ScopedPlaceId, + _requires_explicit_reexport: RequiresExplicitReExport, + _considered_definitions: ConsideredDefinitions, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn place_cycle_initial<'db>( + _db: &'db dyn Db, + _scope: ScopeId<'db>, + _place_id: ScopedPlaceId, + _requires_explicit_reexport: RequiresExplicitReExport, + _considered_definitions: ConsideredDefinitions, +) -> PlaceAndQualifiers<'db> { + Place::bound(Type::Never).into() +} + +#[salsa::tracked(cycle_fn=place_cycle_recover, cycle_initial=place_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +fn place_by_id<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + place_id: ScopedPlaceId, + requires_explicit_reexport: RequiresExplicitReExport, + considered_definitions: ConsideredDefinitions, +) -> PlaceAndQualifiers<'db> { + let use_def = use_def_map(db, scope); + + // If the place is declared, the public type is based on declarations; otherwise, it's based + // on inference from bindings. + + let declarations = match considered_definitions { + ConsideredDefinitions::EndOfScope => use_def.end_of_scope_declarations(place_id), + ConsideredDefinitions::AllReachable => use_def.all_reachable_declarations(place_id), + }; + + let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport); + + let all_considered_bindings = || match considered_definitions { + ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id), + ConsideredDefinitions::AllReachable => use_def.all_reachable_bindings(place_id), + }; + + // If a symbol is undeclared, but qualified with `typing.Final`, we use the right-hand side + // inferred type, without unioning with `Unknown`, because it can not be modified. + if let Some(qualifiers) = declared + .as_ref() + .ok() + .and_then(PlaceAndQualifiers::is_bare_final) + { + let bindings = all_considered_bindings(); + return place_from_bindings_impl(db, bindings, requires_explicit_reexport) + .with_qualifiers(qualifiers); + } + + // Handle bare `ClassVar` annotations by falling back to the union of `Unknown` and the + // inferred type. + match declared { + Ok(PlaceAndQualifiers { + place: Place::Type(Type::Dynamic(DynamicType::Unknown), declaredness), + qualifiers, + }) if qualifiers.contains(TypeQualifiers::CLASS_VAR) => { + let bindings = all_considered_bindings(); + match place_from_bindings_impl(db, bindings, requires_explicit_reexport) { + Place::Type(inferred, boundness) => { + return Place::Type( + UnionType::from_elements(db, [Type::unknown(), inferred]), + boundness, + ) + .with_qualifiers(qualifiers); + } + Place::Unbound => { + return Place::Type(Type::unknown(), declaredness).with_qualifiers(qualifiers); + } + } + } + _ => {} + } + + match declared { + // Place is declared, trust the declared type + Ok( + place_and_quals @ PlaceAndQualifiers { + place: Place::Type(_, Boundness::Bound), + qualifiers: _, + }, + ) => place_and_quals, + // Place is possibly declared + Ok(PlaceAndQualifiers { + place: Place::Type(declared_ty, Boundness::PossiblyUnbound), + qualifiers, + }) => { + let bindings = all_considered_bindings(); + let boundness_analysis = bindings.boundness_analysis; + let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport); + + let place = match inferred { + // Place is possibly undeclared and definitely unbound + Place::Unbound => { + // TODO: We probably don't want to report `Bound` here. This requires a bit of + // design work though as we might want a different behavior for stubs and for + // normal modules. + Place::Type(declared_ty, Boundness::Bound) + } + // Place is possibly undeclared and (possibly) bound + Place::Type(inferred_ty, boundness) => Place::Type( + UnionType::from_elements(db, [inferred_ty, declared_ty]), + if boundness_analysis == BoundnessAnalysis::AssumeBound { + Boundness::Bound + } else { + boundness + }, + ), + }; + + PlaceAndQualifiers { place, qualifiers } + } + // Place is undeclared, return the union of `Unknown` with the inferred type + Ok(PlaceAndQualifiers { + place: Place::Unbound, + qualifiers: _, + }) => { + let bindings = all_considered_bindings(); + let boundness_analysis = bindings.boundness_analysis; + let mut inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport); + + if boundness_analysis == BoundnessAnalysis::AssumeBound { + if let Place::Type(ty, Boundness::PossiblyUnbound) = inferred { + inferred = Place::Type(ty, Boundness::Bound); + } + } + + // `__slots__` is a symbol with special behavior in Python's runtime. It can be + // modified externally, but those changes do not take effect. We therefore issue + // a diagnostic if we see it being modified externally. In type inference, we + // can assign a "narrow" type to it even if it is not *declared*. This means, we + // do not have to call [`widen_type_for_undeclared_public_symbol`]. + // + // `TYPE_CHECKING` is a special variable that should only be assigned `False` + // at runtime, but is always considered `True` in type checking. + // See mdtest/known_constants.md#user-defined-type_checking for details. + let is_considered_non_modifiable = place_table(db, scope) + .place_expr(place_id) + .expr + .is_name_and(|name| matches!(name, "__slots__" | "TYPE_CHECKING")); + + if scope.file(db).is_stub(db) { + // We generally trust module-level undeclared places in stubs and do not union + // with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in + // stubs would result in `IOError` being a union of `OSError` and `Unknown`, which + // leads to all sorts of downstream problems. Similarly, type variables are often + // defined as `_T = TypeVar("_T")`, without being declared. + + inferred.into() + } else { + widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable) + .into() + } + } + // Place has conflicting declared types + Err((declared, _)) => { + // Intentionally ignore conflicting declared types; that's not our problem, + // it's the problem of the module we are importing from. + Place::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) + } + } + + // TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness + // currently only depends on bindings, and ignores declarations. This is inconsistent, since + // we only look at bindings if the place may be undeclared. Consider the following example: + // ```py + // x: int + // + // if flag: + // y: int + // else + // y = 3 + // ``` + // If we import from this module, we will currently report `x` as a definitely-bound place + // (even though it has no bindings at all!) but report `y` as possibly-unbound (even though + // every path has either a binding or a declaration for it.) +} + +/// Implementation of [`symbol`]. +fn symbol_impl<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + name: &str, + requires_explicit_reexport: RequiresExplicitReExport, + considered_definitions: ConsideredDefinitions, +) -> PlaceAndQualifiers<'db> { + let _span = tracing::trace_span!("symbol", ?name).entered(); + + if name == "platform" + && file_to_module(db, scope.file(db)) + .is_some_and(|module| module.is_known(KnownModule::Sys)) + { + match Program::get(db).python_platform(db) { + crate::PythonPlatform::Identifier(platform) => { + return Place::bound(Type::string_literal(db, platform.as_str())).into(); + } + crate::PythonPlatform::All => { + // Fall through to the looked up type + } + } + } + + place_table(db, scope) + .place_id_by_name(name) + .map(|symbol| { + place_by_id( + db, + scope, + symbol, + requires_explicit_reexport, + considered_definitions, + ) + }) + .unwrap_or_default() +} + +/// Implementation of [`place`]. +fn place_impl<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + expr: &PlaceExpr, + requires_explicit_reexport: RequiresExplicitReExport, + considered_definitions: ConsideredDefinitions, +) -> PlaceAndQualifiers<'db> { + let _span = tracing::trace_span!("place", ?expr).entered(); + + place_table(db, scope) + .place_id_by_expr(expr) + .map(|place| { + place_by_id( + db, + scope, + place, + requires_explicit_reexport, + considered_definitions, + ) + }) + .unwrap_or_default() +} + +/// Implementation of [`place_from_bindings`]. +/// +/// ## Implementation Note +/// This function gets called cross-module. It, therefore, shouldn't +/// access any AST nodes from the file containing the declarations. +fn place_from_bindings_impl<'db>( + db: &'db dyn Db, + bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, + requires_explicit_reexport: RequiresExplicitReExport, +) -> Place<'db> { + let predicates = bindings_with_constraints.predicates; + let reachability_constraints = bindings_with_constraints.reachability_constraints; + let boundness_analysis = bindings_with_constraints.boundness_analysis; + let mut bindings_with_constraints = bindings_with_constraints.peekable(); + + let is_non_exported = |binding: Definition<'db>| { + requires_explicit_reexport.is_yes() && !is_reexported(db, binding) + }; + + let unbound_reachability_constraint = match bindings_with_constraints.peek() { + Some(BindingWithConstraints { + binding, + reachability_constraint, + narrowing_constraint: _, + }) if binding.is_undefined_or(is_non_exported) => Some(*reachability_constraint), + _ => None, + }; + let mut deleted_reachability = Truthiness::AlwaysFalse; + + // Evaluate this lazily because we don't always need it (for example, if there are no visible + // bindings at all, we don't need it), and it can cause us to evaluate reachability constraint + // expressions, which is extra work and can lead to cycles. + let unbound_visibility = || { + unbound_reachability_constraint.map(|reachability_constraint| { + reachability_constraints.evaluate(db, predicates, reachability_constraint) + }) + }; + + let mut types = bindings_with_constraints.filter_map( + |BindingWithConstraints { + binding, + narrowing_constraint, + reachability_constraint, + }| { + let binding = match binding { + DefinitionState::Defined(binding) => binding, + DefinitionState::Undefined => { + return None; + } + DefinitionState::Deleted => { + deleted_reachability = deleted_reachability.or( + reachability_constraints.evaluate(db, predicates, reachability_constraint) + ); + return None; + } + }; + + if is_non_exported(binding) { + return None; + } + + let static_reachability = + reachability_constraints.evaluate(db, predicates, reachability_constraint); + + if static_reachability.is_always_false() { + // If the static reachability evaluates to false, the binding is either not reachable + // from the start of the scope, or there is no control flow path from that binding to + // the use of the place that we are investigating. There are three interesting cases + // to consider: + // + // ```py + // def f1(): + // if False: + // x = 1 + // use(x) + // + // def f2(): + // y = 1 + // return + // use(y) + // + // def f3(flag: bool): + // if flag: + // z = 1 + // else: + // z = 2 + // return + // use(z) + // ``` + // + // In the first case, there is a single binding for `x`, but it is not reachable from + // the start of the scope. However, the use of `x` is reachable (`unbound_reachability` + // is not always-false). This means that `x` is unbound and we should return `None`. + // + // In the second case, the binding of `y` is reachable, but there is no control flow + // path from the beginning of the scope, through that binding, to the use of `y` that + // we are investigating. There is also no control flow path from the start of the + // scope, through the implicit `y = ` binding, to the use of `y`. This means + // that `unbound_reachability` is always false. Since there are no other bindings, no + // control flow path can reach this use of `y`, implying that we are in unreachable + // section of code. We return `Never` in order to silence the `unresolve-reference` + // diagnostic that would otherwise be emitted at the use of `y`. + // + // In the third case, we have two bindings for `z`. The first one is visible (there + // is a path of control flow from the start of the scope, through that binding, to + // the use of `z`). So we consider the case that we now encounter the second binding + // `z = 2`, which is not visible due to the early return. The `z = ` binding + // is not live (shadowed by the other bindings), so `unbound_reachability` is `None`. + // Here, we are *not* in an unreachable section of code. However, it is still okay to + // return `Never` in this case, because we will union the types of all bindings, and + // `Never` will be eliminated automatically. + + if unbound_visibility().is_none_or(Truthiness::is_always_false) { + return Some(Type::Never); + } + return None; + } + + let binding_ty = binding_type(db, binding); + Some(narrowing_constraint.narrow(db, binding_ty, binding.place(db))) + }, + ); + + if let Some(first) = types.next() { + let ty = if let Some(second) = types.next() { + let mut builder = PublicTypeBuilder::new(db); + builder.add(first); + builder.add(second); + + for ty in types { + builder.add(ty); + } + + builder.build() + } else { + first + }; + + let boundness = match boundness_analysis { + BoundnessAnalysis::AssumeBound => Boundness::Bound, + BoundnessAnalysis::BasedOnUnboundVisibility => match unbound_visibility() { + Some(Truthiness::AlwaysTrue) => { + unreachable!( + "If we have at least one binding, the implicit `unbound` binding should not be definitely visible" + ) + } + Some(Truthiness::AlwaysFalse) | None => Boundness::Bound, + Some(Truthiness::Ambiguous) => Boundness::PossiblyUnbound, + }, + }; + + match deleted_reachability { + Truthiness::AlwaysFalse => Place::Type(ty, boundness), + Truthiness::AlwaysTrue => Place::Unbound, + Truthiness::Ambiguous => Place::Type(ty, Boundness::PossiblyUnbound), + } + } else { + Place::Unbound + } +} + +/// Accumulates types from multiple bindings or declarations, and eventually builds a +/// union type from them. +/// +/// `@overload`ed function literal types are discarded if they are immediately followed +/// by their implementation. This is to ensure that we do not merge all of them into the +/// union type. The last one will include the other overloads already. +struct PublicTypeBuilder<'db> { + db: &'db dyn Db, + queue: Option>, + builder: UnionBuilder<'db>, +} + +impl<'db> PublicTypeBuilder<'db> { + fn new(db: &'db dyn Db) -> Self { + PublicTypeBuilder { + db, + queue: None, + builder: UnionBuilder::new(db), + } + } + + fn add_to_union(&mut self, element: Type<'db>) { + self.builder.add_in_place(element); + } + + fn drain_queue(&mut self) { + if let Some(queued_element) = self.queue.take() { + self.add_to_union(queued_element); + } + } + + fn add(&mut self, element: Type<'db>) -> bool { + match element { + Type::FunctionLiteral(function) => { + if function + .literal(self.db) + .last_definition(self.db) + .is_overload(self.db) + { + self.queue = Some(element); + false + } else { + self.queue = None; + self.add_to_union(element); + true + } + } + _ => { + self.drain_queue(); + self.add_to_union(element); + true + } + } + } + + fn build(mut self) -> Type<'db> { + self.drain_queue(); + self.builder.build() + } +} + +/// Accumulates multiple (potentially conflicting) declared types and type qualifiers, +/// and eventually builds a union from them. +struct DeclaredTypeBuilder<'db> { + inner: PublicTypeBuilder<'db>, + qualifiers: TypeQualifiers, + first_type: Option>, + conflicting_types: FxOrderSet>, +} + +impl<'db> DeclaredTypeBuilder<'db> { + fn new(db: &'db dyn Db) -> Self { + DeclaredTypeBuilder { + inner: PublicTypeBuilder::new(db), + qualifiers: TypeQualifiers::empty(), + first_type: None, + conflicting_types: FxOrderSet::default(), + } + } + + fn add(&mut self, element: TypeAndQualifiers<'db>) { + let element_ty = element.inner_type(); + + if self.inner.add(element_ty) { + if let Some(first_ty) = self.first_type { + if !first_ty.is_equivalent_to(self.inner.db, element_ty) { + self.conflicting_types.insert(element_ty); + } + } else { + self.first_type = Some(element_ty); + } + } + + self.qualifiers = self.qualifiers.union(element.qualifiers()); + } + + fn build(mut self) -> DeclaredTypeAndConflictingTypes<'db> { + if !self.conflicting_types.is_empty() { + self.conflicting_types.insert_before( + 0, + self.first_type + .expect("there must be a first type if there are conflicting types"), + ); + } + + ( + TypeAndQualifiers::new(self.inner.build(), self.qualifiers), + self.conflicting_types.into_boxed_slice(), + ) + } +} + +/// Implementation of [`place_from_declarations`]. +/// +/// ## Implementation Note +/// This function gets called cross-module. It, therefore, shouldn't +/// access any AST nodes from the file containing the declarations. +fn place_from_declarations_impl<'db>( + db: &'db dyn Db, + declarations: DeclarationsIterator<'_, 'db>, + requires_explicit_reexport: RequiresExplicitReExport, +) -> PlaceFromDeclarationsResult<'db> { + let predicates = declarations.predicates; + let reachability_constraints = declarations.reachability_constraints; + let boundness_analysis = declarations.boundness_analysis; + let mut declarations = declarations.peekable(); + + let is_non_exported = |declaration: Definition<'db>| { + requires_explicit_reexport.is_yes() && !is_reexported(db, declaration) + }; + + let undeclared_reachability = match declarations.peek() { + Some(DeclarationWithConstraint { + declaration, + reachability_constraint, + }) if declaration.is_undefined_or(is_non_exported) => { + reachability_constraints.evaluate(db, predicates, *reachability_constraint) + } + _ => Truthiness::AlwaysFalse, + }; + + let mut all_declarations_definitely_reachable = true; + + let mut types = declarations.filter_map( + |DeclarationWithConstraint { + declaration, + reachability_constraint, + }| { + let DefinitionState::Defined(declaration) = declaration else { + return None; + }; + + if is_non_exported(declaration) { + return None; + } + + let static_reachability = + reachability_constraints.evaluate(db, predicates, reachability_constraint); + + if static_reachability.is_always_false() { + None + } else { + all_declarations_definitely_reachable = + all_declarations_definitely_reachable && static_reachability.is_always_true(); + + Some(declaration_type(db, declaration)) + } + }, + ); + + if let Some(first) = types.next() { + let declared = if let Some(second) = types.next() { + let mut builder = DeclaredTypeBuilder::new(db); + builder.add(first); + builder.add(second); + for element in types { + builder.add(element); + } + let (union, conflicting) = builder.build(); + + if !conflicting.is_empty() { + return Err((union, conflicting)); + } + + union + } else { + first + }; + + let boundness = match boundness_analysis { + BoundnessAnalysis::AssumeBound => { + if all_declarations_definitely_reachable { + Boundness::Bound + } else { + // For declarations, it is important to consider the possibility that they might only + // be bound in one control flow path, while the other path contains a binding. In order + // to even consider the bindings as well in `place_by_id`, we return `PossiblyUnbound` + // here. + Boundness::PossiblyUnbound + } + } + BoundnessAnalysis::BasedOnUnboundVisibility => match undeclared_reachability { + Truthiness::AlwaysTrue => { + unreachable!( + "If we have at least one declaration, the implicit `unbound` binding should not be definitely visible" + ) + } + Truthiness::AlwaysFalse => Boundness::Bound, + Truthiness::Ambiguous => Boundness::PossiblyUnbound, + }, + }; + + Ok(Place::Type(declared.inner_type(), boundness).with_qualifiers(declared.qualifiers())) + } else { + Ok(Place::Unbound.into()) + } +} + +// Returns `true` if the `definition` is re-exported. +// +// This will first check if the definition is using the "redundant alias" pattern like `import foo +// as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being +// exported via `__all__`. +fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { + // This information is computed by the semantic index builder. + if definition.is_reexported(db) { + return true; + } + // At this point, the definition should either be an `import` or `from ... import` statement. + // This is because the default value of `is_reexported` is `true` for any other kind of + // definition. + let Some(all_names) = dunder_all_names(db, definition.file(db)) else { + return false; + }; + let table = place_table(db, definition.scope(db)); + let symbol_name = table.place_expr(definition.place(db)).expect_name(); + all_names.contains(symbol_name) +} + +mod implicit_globals { + use ruff_python_ast as ast; + + use crate::db::Db; + use crate::place::PlaceAndQualifiers; + use crate::semantic_index::place::PlaceExpr; + use crate::semantic_index::{self, place_table, use_def_map}; + use crate::types::{KnownClass, Type}; + + use super::{Place, PlaceFromDeclarationsResult, place_from_declarations}; + + pub(crate) fn module_type_implicit_global_declaration<'db>( + db: &'db dyn Db, + expr: &PlaceExpr, + ) -> PlaceFromDeclarationsResult<'db> { + if !module_type_symbols(db) + .iter() + .any(|module_type_member| Some(module_type_member) == expr.as_name()) + { + return Ok(Place::Unbound.into()); + } + let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db) + else { + return Ok(Place::Unbound.into()); + }; + let module_type_scope = module_type_class.body_scope(db); + let place_table = place_table(db, module_type_scope); + let Some(place_id) = place_table.place_id_by_expr(expr) else { + return Ok(Place::Unbound.into()); + }; + place_from_declarations( + db, + use_def_map(db, module_type_scope).end_of_scope_declarations(place_id), + ) + } + + /// Looks up the type of an "implicit global symbol". Returns [`Place::Unbound`] if + /// `name` is not present as an implicit symbol in module-global namespaces. + /// + /// Implicit global symbols are symbols such as `__doc__`, `__name__`, and `__file__` + /// that are implicitly defined in every module's global scope. Because their type is + /// always the same, we simply look these up as instance attributes on `types.ModuleType`. + /// + /// Note that this function should only be used as a fallback if a symbol is being looked + /// up in the global scope **from within the same file**. If the symbol is being looked up + /// from outside the file (e.g. via imports), use [`super::imported_symbol`] (or fallback logic + /// like the logic used in that function) instead. The reason is that this function returns + /// [`Place::Unbound`] for `__init__` and `__dict__` (which cannot be found in globals if + /// the lookup is being done from the same file) -- but these symbols *are* available in the + /// global scope if they're being imported **from a different file**. + pub(crate) fn module_type_implicit_global_symbol<'db>( + db: &'db dyn Db, + name: &str, + ) -> PlaceAndQualifiers<'db> { + // We special-case `__file__` here because we know that for an internal implicit global + // lookup in a Python module, it is always a string, even though typeshed says `str | + // None`. + if name == "__file__" { + Place::bound(KnownClass::Str.to_instance(db)).into() + } else if name == "__builtins__" { + Place::bound(Type::any()).into() + } else if name == "__debug__" { + Place::bound(KnownClass::Bool.to_instance(db)).into() + } + // In general we wouldn't check to see whether a symbol exists on a class before doing the + // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance + // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` + // when absolutely necessary is that this function is used in a very hot path (name resolution + // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. + else if module_type_symbols(db) + .iter() + .any(|module_type_member| &**module_type_member == name) + { + KnownClass::ModuleType.to_instance(db).member(db, name) + } else { + Place::Unbound.into() + } + } + + /// An internal micro-optimisation for `module_type_implicit_global_symbol`. + /// + /// This function returns a list of the symbols that typeshed declares in the + /// body scope of the stub for the class `types.ModuleType`. + /// + /// The returned list excludes the attributes `__dict__` and `__init__`. These are very + /// special members that can be accessed as attributes on the module when imported, + /// but cannot be accessed as globals *inside* the module. + /// + /// The list also excludes `__getattr__`. `__getattr__` is even more special: it doesn't + /// exist at runtime, but typeshed includes it to reduce false positives associated with + /// functions that dynamically import modules and return `Instance(types.ModuleType)`. + /// We should ignore it for any known module-literal type. + /// + /// Conceptually this function could be a `Set` rather than a list, + /// but the number of symbols declared in this scope is likely to be very small, + /// so the cost of hashing the names is likely to be more expensive than it's worth. + #[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] + fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { + let Some(module_type) = KnownClass::ModuleType + .to_class_literal(db) + .into_class_literal() + else { + // The most likely way we get here is if a user specified a `--custom-typeshed-dir` + // without a `types.pyi` stub in the `stdlib/` directory + return smallvec::SmallVec::default(); + }; + + let module_type_scope = module_type.body_scope(db); + let module_type_symbol_table = place_table(db, module_type_scope); + + module_type_symbol_table + .places() + .filter(|place| place.is_declared() && place.is_name()) + .map(semantic_index::place::PlaceExprWithFlags::expect_name) + .filter(|symbol_name| { + !matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__") + }) + .cloned() + .collect() + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::db::tests::setup_db; + + #[test] + fn module_type_symbols_includes_declared_types_but_not_referenced_types() { + let db = setup_db(); + let symbol_names = module_type_symbols(&db); + + let dunder_name_symbol_name = ast::name::Name::new_static("__name__"); + assert!(symbol_names.contains(&dunder_name_symbol_name)); + + let property_symbol_name = ast::name::Name::new_static("property"); + assert!(!symbol_names.contains(&property_symbol_name)); + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum RequiresExplicitReExport { + Yes, + No, +} + +impl RequiresExplicitReExport { + const fn is_yes(self) -> bool { + matches!(self, RequiresExplicitReExport::Yes) + } +} + +/// Specifies which definitions should be considered when looking up a place. +/// +/// In the example below, the `EndOfScope` variant would consider the `x = 2` and `x = 3` definitions, +/// while the `AllReachable` variant would also consider the `x = 1` definition. +/// ```py +/// def _(): +/// x = 1 +/// +/// x = 2 +/// +/// if flag(): +/// x = 3 +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub(crate) enum ConsideredDefinitions { + /// Consider only the definitions that are "live" at the end of the scope, i.e. those + /// that have not been shadowed or deleted. + EndOfScope, + /// Consider all definitions that are reachable from the start of the scope. + AllReachable, +} + +/// Specifies how the boundness of a place should be determined. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub(crate) enum BoundnessAnalysis { + /// The place is always considered bound. + AssumeBound, + /// The boundness of the place is determined based on the visibility of the implicit + /// `unbound` binding. In the example below, when analyzing the visibility of the + /// `x = ` binding from the position of the end of the scope, it would be + /// `Truthiness::Ambiguous`, because it could either be visible or not, depending on the + /// `flag()` return value. This would result in a `Boundness::PossiblyUnbound` for `x`. + /// + /// ```py + /// x = + /// + /// if flag(): + /// x = 1 + /// ``` + BasedOnUnboundVisibility, +} + +/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred` +/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is +/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses +/// of symbols that have no declared type. +fn widen_type_for_undeclared_public_symbol<'db>( + db: &'db dyn Db, + inferred: Place<'db>, + is_considered_non_modifiable: bool, +) -> Place<'db> { + // We special-case known-instance types here since symbols like `typing.Any` are typically + // not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as + // such. + let is_known_instance = inferred + .ignore_possibly_unbound() + .is_some_and(|ty| matches!(ty, Type::SpecialForm(_) | Type::KnownInstance(_))); + + if is_considered_non_modifiable || is_known_instance { + inferred + } else { + inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::tests::setup_db; + + #[test] + fn test_symbol_or_fall_back_to() { + use Boundness::{Bound, PossiblyUnbound}; + + let db = setup_db(); + let ty1 = Type::IntLiteral(1); + let ty2 = Type::IntLiteral(2); + + let unbound = || Place::Unbound.with_qualifiers(TypeQualifiers::empty()); + + let possibly_unbound_ty1 = + || Place::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); + let possibly_unbound_ty2 = + || Place::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty()); + + let bound_ty1 = || Place::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty()); + let bound_ty2 = || Place::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty()); + + // Start from an unbound symbol + assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound()); + assert_eq!( + unbound().or_fall_back_to(&db, possibly_unbound_ty1), + possibly_unbound_ty1() + ); + assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1()); + + // Start from a possibly unbound symbol + assert_eq!( + possibly_unbound_ty1().or_fall_back_to(&db, unbound), + possibly_unbound_ty1() + ); + assert_eq!( + possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), + Place::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into() + ); + assert_eq!( + possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2), + Place::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into() + ); + + // Start from a definitely bound symbol + assert_eq!(bound_ty1().or_fall_back_to(&db, unbound), bound_ty1()); + assert_eq!( + bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), + bound_ty1() + ); + assert_eq!(bound_ty1().or_fall_back_to(&db, bound_ty2), bound_ty1()); + } + + #[track_caller] + fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Place<'db>) { + assert!(matches!( + symbol, + Place::Type(Type::NominalInstance(_), Boundness::Bound) + )); + assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); + } + + #[test] + fn implicit_builtin_globals() { + let db = setup_db(); + assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").place); + } + + #[test] + fn implicit_typing_globals() { + let db = setup_db(); + assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").place); + } + + #[test] + fn implicit_typing_extensions_globals() { + let db = setup_db(); + assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").place); + } + + #[test] + fn implicit_sys_globals() { + let db = setup_db(); + assert_bound_string_symbol( + &db, + known_module_symbol(&db, KnownModule::Sys, "__name__").place, + ); + } +} diff --git a/crates/ty_python_semantic/src/program.rs b/crates/ty_python_semantic/src/program.rs new file mode 100644 index 0000000000000..64c42522614c2 --- /dev/null +++ b/crates/ty_python_semantic/src/program.rs @@ -0,0 +1,204 @@ +use std::sync::Arc; + +use crate::Db; +use crate::module_resolver::{SearchPathValidationError, SearchPaths}; +use crate::python_platform::PythonPlatform; + +use ruff_db::diagnostic::Span; +use ruff_db::files::system_path_to_file; +use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_db::vendored::VendoredFileSystem; +use ruff_python_ast::PythonVersion; +use ruff_text_size::TextRange; +use salsa::Durability; +use salsa::Setter; + +#[salsa::input(singleton)] +pub struct Program { + #[returns(ref)] + pub python_version_with_source: PythonVersionWithSource, + + #[returns(ref)] + pub python_platform: PythonPlatform, + + #[returns(ref)] + pub(crate) search_paths: SearchPaths, +} + +impl Program { + pub fn init_or_update(db: &mut dyn Db, settings: ProgramSettings) -> Self { + match Self::try_get(db) { + Some(program) => { + program.update_from_settings(db, settings); + program + } + None => Self::from_settings(db, settings), + } + } + + pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self { + let ProgramSettings { + python_version, + python_platform, + search_paths, + } = settings; + + search_paths.try_register_static_roots(db); + + Program::builder(python_version, python_platform, search_paths) + .durability(Durability::HIGH) + .new(db) + } + + pub fn python_version(self, db: &dyn Db) -> PythonVersion { + self.python_version_with_source(db).version + } + + pub fn update_from_settings(self, db: &mut dyn Db, settings: ProgramSettings) { + let ProgramSettings { + python_version, + python_platform, + search_paths, + } = settings; + + if self.search_paths(db) != &search_paths { + tracing::debug!("Updating search paths"); + search_paths.try_register_static_roots(db); + self.set_search_paths(db).to(search_paths); + } + + if &python_platform != self.python_platform(db) { + tracing::debug!("Updating python platform: `{python_platform:?}`"); + self.set_python_platform(db).to(python_platform); + } + + if &python_version != self.python_version_with_source(db) { + tracing::debug!( + "Updating python version: Python {version}", + version = python_version.version + ); + self.set_python_version_with_source(db).to(python_version); + } + } + + pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> { + self.search_paths(db).custom_stdlib() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProgramSettings { + pub python_version: PythonVersionWithSource, + pub python_platform: PythonPlatform, + pub search_paths: SearchPaths, +} + +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub enum PythonVersionSource { + /// Value loaded from a project's configuration file. + ConfigFile(PythonVersionFileSource), + + /// Value loaded from the `pyvenv.cfg` file of the virtual environment. + /// The virtual environment might have been configured, activated or inferred. + PyvenvCfgFile(PythonVersionFileSource), + + /// Value inferred from the layout of the Python installation. + /// + /// This only ever applies on Unix. On Unix, the `site-packages` directory + /// will always be at `sys.prefix/lib/pythonX.Y/site-packages`, + /// so we can infer the Python version from the parent directory of `site-packages`. + InstallationDirectoryLayout { site_packages_parent_dir: Box }, + + /// 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, + + /// We fell back to a default value because the value was not specified via the CLI or a config file. + #[default] + Default, +} + +/// Information regarding the file and [`TextRange`] of the configuration +/// from which we inferred the Python version. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PythonVersionFileSource { + path: Arc, + range: Option, +} + +impl PythonVersionFileSource { + pub fn new(path: Arc, range: Option) -> Self { + Self { path, range } + } + + /// Attempt to resolve a [`Span`] that corresponds to the location of + /// the configuration setting that specified the Python version. + /// + /// Useful for subdiagnostics when informing the user + /// what the inferred Python version of their project is. + pub(crate) fn span(&self, db: &dyn Db) -> Option { + let file = system_path_to_file(db, &*self.path).ok()?; + Some(Span::from(file).with_optional_range(self.range)) + } +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct PythonVersionWithSource { + pub version: PythonVersion, + pub source: PythonVersionSource, +} + +impl Default for PythonVersionWithSource { + fn default() -> Self { + Self { + version: PythonVersion::latest_ty(), + source: PythonVersionSource::Default, + } + } +} + +/// Configures the search paths for module resolution. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct SearchPathSettings { + /// List of user-provided paths that should take first priority in the module resolution. + /// Examples in other type checkers are mypy's MYPYPATH environment variable, + /// or pyright's stubPath configuration setting. + pub extra_paths: Vec, + + /// The root of the project, used for finding first-party modules. + pub src_roots: Vec, + + /// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types. + /// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, + /// bundled as a zip file in the binary + pub custom_typeshed: Option, + + /// List of site packages paths to use. + pub site_packages_paths: Vec, +} + +impl SearchPathSettings { + pub fn new(src_roots: Vec) -> Self { + Self { + src_roots, + ..SearchPathSettings::empty() + } + } + + pub fn empty() -> Self { + SearchPathSettings { + src_roots: vec![], + extra_paths: vec![], + custom_typeshed: None, + site_packages_paths: vec![], + } + } + + pub fn to_search_paths( + &self, + system: &dyn System, + vendored: &VendoredFileSystem, + ) -> Result { + SearchPaths::from_settings(self, system, vendored) + } +} diff --git a/crates/ty_python_semantic/src/pull_types.rs b/crates/ty_python_semantic/src/pull_types.rs new file mode 100644 index 0000000000000..d2c1317b9ffff --- /dev/null +++ b/crates/ty_python_semantic/src/pull_types.rs @@ -0,0 +1,134 @@ +//! A utility visitor for testing, which attempts to "pull a type" for ever sub-node in a given AST. +//! +//! This is used in the "corpus" and (indirectly) the "mdtest" integration tests for this crate. +//! (Mdtest uses the `pull_types` function via the `ty_test` crate.) + +use crate::{Db, HasType, SemanticModel}; +use ruff_db::{files::File, parsed::parsed_module}; +use ruff_python_ast::{ + self as ast, visitor::source_order, visitor::source_order::SourceOrderVisitor, +}; + +pub fn pull_types(db: &dyn Db, file: File) { + let mut visitor = PullTypesVisitor::new(db, file); + + let ast = parsed_module(db, file).load(db); + + visitor.visit_body(ast.suite()); +} + +struct PullTypesVisitor<'db> { + model: SemanticModel<'db>, +} + +impl<'db> PullTypesVisitor<'db> { + fn new(db: &'db dyn Db, file: File) -> Self { + Self { + model: SemanticModel::new(db, file), + } + } + + fn visit_target(&mut self, target: &ast::Expr) { + match target { + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + for element in elts { + self.visit_target(element); + } + } + _ => self.visit_expr(target), + } + } +} + +impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> { + fn visit_stmt(&mut self, stmt: &ast::Stmt) { + match stmt { + ast::Stmt::FunctionDef(function) => { + let _ty = function.inferred_type(&self.model); + } + ast::Stmt::ClassDef(class) => { + let _ty = class.inferred_type(&self.model); + } + ast::Stmt::Assign(assign) => { + for target in &assign.targets { + self.visit_target(target); + } + self.visit_expr(&assign.value); + return; + } + ast::Stmt::For(for_stmt) => { + self.visit_target(&for_stmt.target); + self.visit_expr(&for_stmt.iter); + self.visit_body(&for_stmt.body); + self.visit_body(&for_stmt.orelse); + return; + } + ast::Stmt::With(with_stmt) => { + for item in &with_stmt.items { + if let Some(target) = &item.optional_vars { + self.visit_target(target); + } + self.visit_expr(&item.context_expr); + } + + self.visit_body(&with_stmt.body); + return; + } + ast::Stmt::AnnAssign(_) + | ast::Stmt::Return(_) + | ast::Stmt::Delete(_) + | ast::Stmt::AugAssign(_) + | ast::Stmt::TypeAlias(_) + | ast::Stmt::While(_) + | ast::Stmt::If(_) + | ast::Stmt::Match(_) + | ast::Stmt::Raise(_) + | ast::Stmt::Try(_) + | ast::Stmt::Assert(_) + | ast::Stmt::Import(_) + | ast::Stmt::ImportFrom(_) + | ast::Stmt::Global(_) + | ast::Stmt::Nonlocal(_) + | ast::Stmt::Expr(_) + | ast::Stmt::Pass(_) + | ast::Stmt::Break(_) + | ast::Stmt::Continue(_) + | ast::Stmt::IpyEscapeCommand(_) => {} + } + + source_order::walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, expr: &ast::Expr) { + let _ty = expr.inferred_type(&self.model); + + source_order::walk_expr(self, expr); + } + + fn visit_comprehension(&mut self, comprehension: &ast::Comprehension) { + self.visit_expr(&comprehension.iter); + self.visit_target(&comprehension.target); + for if_expr in &comprehension.ifs { + self.visit_expr(if_expr); + } + } + + fn visit_parameter(&mut self, parameter: &ast::Parameter) { + let _ty = parameter.inferred_type(&self.model); + + source_order::walk_parameter(self, parameter); + } + + fn visit_parameter_with_default(&mut self, parameter_with_default: &ast::ParameterWithDefault) { + let _ty = parameter_with_default.inferred_type(&self.model); + + source_order::walk_parameter_with_default(self, parameter_with_default); + } + + fn visit_alias(&mut self, alias: &ast::Alias) { + let _ty = alias.inferred_type(&self.model); + + source_order::walk_alias(self, alias); + } +} diff --git a/crates/ty_python_semantic/src/python_platform.rs b/crates/ty_python_semantic/src/python_platform.rs new file mode 100644 index 0000000000000..573165f82edab --- /dev/null +++ b/crates/ty_python_semantic/src/python_platform.rs @@ -0,0 +1,134 @@ +use std::fmt::{Display, Formatter}; + +/// The target platform to assume when resolving types. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize, ruff_macros::RustDoc), + serde(rename_all = "kebab-case") +)] +pub enum PythonPlatform { + /// Do not make any assumptions about the target platform. + All, + + /// Assume a specific target platform like `linux`, `darwin` or `win32`. + /// + /// We use a string (instead of individual enum variants), as the set of possible platforms + /// may change over time. See for + /// some known platform identifiers. + #[cfg_attr(feature = "serde", serde(untagged))] + Identifier(String), +} + +impl From for PythonPlatform { + fn from(platform: String) -> Self { + match platform.as_str() { + "all" => PythonPlatform::All, + _ => PythonPlatform::Identifier(platform.to_string()), + } + } +} + +impl Display for PythonPlatform { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PythonPlatform::All => f.write_str("all"), + PythonPlatform::Identifier(name) => f.write_str(name), + } + } +} + +impl Default for PythonPlatform { + fn default() -> Self { + if cfg!(target_os = "windows") { + PythonPlatform::Identifier("win32".to_string()) + } else if cfg!(target_os = "macos") { + PythonPlatform::Identifier("darwin".to_string()) + } else if cfg!(target_os = "android") { + PythonPlatform::Identifier("android".to_string()) + } else if cfg!(target_os = "ios") { + PythonPlatform::Identifier("ios".to_string()) + } else { + PythonPlatform::Identifier("linux".to_string()) + } + } +} + +#[cfg(feature = "schemars")] +mod schema { + use crate::PythonPlatform; + use ruff_db::RustDoc; + use schemars::_serde_json::Value; + use schemars::JsonSchema; + use schemars::r#gen::SchemaGenerator; + use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation}; + + impl JsonSchema for PythonPlatform { + fn schema_name() -> String { + "PythonPlatform".to_string() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + // Hard code some well known values, but allow any other string as well. + subschemas: Some(Box::new(SubschemaValidation { + any_of: Some(vec![ + Schema::Object(SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + ..SchemaObject::default() + }), + // Promote well-known values for better auto-completion. + // Using `const` over `enumValues` as recommended [here](https://github.com/SchemaStore/schemastore/blob/master/CONTRIBUTING.md#documenting-enums). + Schema::Object(SchemaObject { + const_value: Some(Value::String("all".to_string())), + metadata: Some(Box::new(Metadata { + description: Some( + "Do not make any assumptions about the target platform." + .to_string(), + ), + ..Metadata::default() + })), + + ..SchemaObject::default() + }), + Schema::Object(SchemaObject { + const_value: Some(Value::String("darwin".to_string())), + metadata: Some(Box::new(Metadata { + description: Some("Darwin".to_string()), + ..Metadata::default() + })), + + ..SchemaObject::default() + }), + Schema::Object(SchemaObject { + const_value: Some(Value::String("linux".to_string())), + metadata: Some(Box::new(Metadata { + description: Some("Linux".to_string()), + ..Metadata::default() + })), + + ..SchemaObject::default() + }), + Schema::Object(SchemaObject { + const_value: Some(Value::String("win32".to_string())), + metadata: Some(Box::new(Metadata { + description: Some("Windows".to_string()), + ..Metadata::default() + })), + + ..SchemaObject::default() + }), + ]), + + ..SubschemaValidation::default() + })), + metadata: Some(Box::new(Metadata { + description: Some(::rust_doc().to_string()), + ..Metadata::default() + })), + + ..SchemaObject::default() + }) + } + } +} diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs new file mode 100644 index 0000000000000..b6e6755f0f75a --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -0,0 +1,1559 @@ +use std::iter::FusedIterator; +use std::sync::Arc; + +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_index::{IndexSlice, IndexVec}; + +use ruff_python_parser::semantic_errors::SemanticSyntaxError; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa::Update; +use salsa::plumbing::AsId; + +use crate::Db; +use crate::module_name::ModuleName; +use crate::node_key::NodeKey; +use crate::semantic_index::ast_ids::AstIds; +use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; +use crate::semantic_index::builder::SemanticIndexBuilder; +use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions}; +use crate::semantic_index::expression::Expression; +use crate::semantic_index::narrowing_constraints::ScopedNarrowingConstraint; +use crate::semantic_index::place::{ + FileScopeId, NodeWithScopeKey, NodeWithScopeRef, PlaceExpr, PlaceTable, Scope, ScopeId, + ScopeKind, ScopedPlaceId, +}; +use crate::semantic_index::use_def::{EagerSnapshotKey, ScopedEagerSnapshotId, UseDefMap}; +use crate::util::get_size::untracked_arc_size; + +pub mod ast_ids; +mod builder; +pub mod definition; +pub mod expression; +pub(crate) mod narrowing_constraints; +pub mod place; +pub(crate) mod predicate; +mod re_exports; +mod reachability_constraints; +mod use_def; + +pub(crate) use self::use_def::{ + ApplicableConstraints, BindingWithConstraints, BindingWithConstraintsIterator, + DeclarationWithConstraint, DeclarationsIterator, +}; + +type PlaceSet = hashbrown::HashTable; + +/// Returns the semantic index for `file`. +/// +/// Prefer using [`symbol_table`] when working with symbols from a single scope. +#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { + let _span = tracing::trace_span!("semantic_index", ?file).entered(); + + let module = parsed_module(db, file).load(db); + + SemanticIndexBuilder::new(db, file, &module).build() +} + +/// Returns the place table for a specific `scope`. +/// +/// Using [`place_table`] over [`semantic_index`] has the advantage that +/// Salsa can avoid invalidating dependent queries if this scope's place table +/// is unchanged. +#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { + let file = scope.file(db); + let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?file).entered(); + let index = semantic_index(db, file); + + index.place_table(scope.file_scope_id(db)) +} + +/// Returns the set of modules that are imported anywhere in `file`. +/// +/// This set only considers `import` statements, not `from...import` statements, because: +/// +/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is +/// therefore imported) without looking outside the content of this file. (We could turn this +/// into a _potentially_ imported modules set, but that would change how it's used in our type +/// inference logic.) +/// +/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without +/// knowing the name of the current module, and whether it's a package. +#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc> { + semantic_index(db, file).imported_modules.clone() +} + +/// Returns the use-def map for a specific `scope`. +/// +/// Using [`use_def_map`] over [`semantic_index`] has the advantage that +/// Salsa can avoid invalidating dependent queries if this scope's use-def map +/// is unchanged. +#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> ArcUseDefMap<'db> { + let file = scope.file(db); + let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?file).entered(); + let index = semantic_index(db, file); + + index.use_def_map(scope.file_scope_id(db)) +} + +/// Returns all attribute assignments (and their method scope IDs) with a symbol name matching +/// the one given for a specific class body scope. +/// +/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it +/// introduces a direct dependency on that file's AST. +pub(crate) fn attribute_assignments<'db, 's>( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &'s str, +) -> impl Iterator, FileScopeId)> + use<'s, 'db> { + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + + attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { + let place_table = index.place_table(function_scope_id); + let place = place_table.place_id_by_instance_attribute_name(name)?; + let use_def = &index.use_def_maps[function_scope_id]; + Some(( + use_def.inner.all_reachable_bindings(place), + function_scope_id, + )) + }) +} + +/// Returns all attribute declarations (and their method scope IDs) with a symbol name matching +/// the one given for a specific class body scope. +/// +/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it +/// introduces a direct dependency on that file's AST. +pub(crate) fn attribute_declarations<'db, 's>( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &'s str, +) -> impl Iterator, FileScopeId)> + use<'s, 'db> { + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + + attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { + let place_table = index.place_table(function_scope_id); + let place = place_table.place_id_by_instance_attribute_name(name)?; + let use_def = &index.use_def_maps[function_scope_id]; + Some(( + use_def.inner.all_reachable_declarations(place), + function_scope_id, + )) + }) +} + +/// Returns all attribute assignments as scope IDs for a specific class body scope. +/// +/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it +/// introduces a direct dependency on that file's AST. +pub(crate) fn attribute_scopes<'db, 's>( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, +) -> impl Iterator + use<'s, 'db> { + let file = class_body_scope.file(db); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let class_scope_id = class_body_scope.file_scope_id(db); + + ChildrenIter::new(index, class_scope_id).filter_map(move |(child_scope_id, scope)| { + let (function_scope_id, function_scope) = + if scope.node().scope_kind() == ScopeKind::Annotation { + // This could be a generic method with a type-params scope. + // Go one level deeper to find the function scope. The first + // descendant is the (potential) function scope. + let function_scope_id = scope.descendants().start; + (function_scope_id, index.scope(function_scope_id)) + } else { + (child_scope_id, scope) + }; + + function_scope.node().as_function(&module)?; + Some(function_scope_id) + }) +} + +/// Returns the module global scope of `file`. +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { + let _span = tracing::trace_span!("global_scope", ?file).entered(); + + FileScopeId::global().to_scope_id(db, file) +} + +pub(crate) enum EagerSnapshotResult<'map, 'db> { + FoundConstraint(ScopedNarrowingConstraint), + FoundBindings(BindingWithConstraintsIterator<'map, 'db>), + NotFound, + NoLongerInEagerContext, +} + +/// The place tables and use-def maps for all scopes in a file. +#[derive(Debug, Update, get_size2::GetSize)] +pub(crate) struct SemanticIndex<'db> { + /// List of all place tables in this file, indexed by scope. + place_tables: IndexVec>, + + /// List of all scopes in this file. + scopes: IndexVec, + + /// Map expressions to their corresponding scope. + scopes_by_expression: FxHashMap, + + /// Map from a node creating a definition to its definition. + definitions_by_node: FxHashMap>, + + /// Map from a standalone expression to its [`Expression`] ingredient. + expressions_by_node: FxHashMap>, + + /// Map from nodes that create a scope to the scope they create. + scopes_by_node: FxHashMap, + + /// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`]. + scope_ids_by_scope: IndexVec>, + + /// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains. + globals_by_scope: FxHashMap>, + + /// Use-def map for each scope in this file. + use_def_maps: IndexVec>, + + /// Lookup table to map between node ids and ast nodes. + /// + /// Note: We should not depend on this map when analysing other files or + /// changing a file invalidates all dependents. + ast_ids: IndexVec, + + /// The set of modules that are imported anywhere within this file. + imported_modules: Arc>, + + /// Flags about the global scope (code usage impacting inference) + has_future_annotations: bool, + + /// Map of all of the eager snapshots that appear in this file. + eager_snapshots: FxHashMap, + + /// List of all semantic syntax errors in this file. + semantic_syntax_errors: Vec, + + /// Set of all generator functions in this file. + generator_functions: FxHashSet, +} + +impl<'db> SemanticIndex<'db> { + /// Returns the place table for a specific scope. + /// + /// Use the Salsa cached [`place_table()`] query if you only need the + /// place table for a single scope. + #[track_caller] + pub(super) fn place_table(&self, scope_id: FileScopeId) -> Arc { + self.place_tables[scope_id].clone() + } + + /// Returns the use-def map for a specific scope. + /// + /// Use the Salsa cached [`use_def_map()`] query if you only need the + /// use-def map for a single scope. + #[track_caller] + pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> ArcUseDefMap<'_> { + self.use_def_maps[scope_id].clone() + } + + #[track_caller] + pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds { + &self.ast_ids[scope_id] + } + + /// Returns the ID of the `expression`'s enclosing scope. + #[track_caller] + pub(crate) fn expression_scope_id( + &self, + expression: impl Into, + ) -> FileScopeId { + self.scopes_by_expression[&expression.into()] + } + + /// Returns the ID of the `expression`'s enclosing scope. + pub(crate) fn try_expression_scope_id( + &self, + expression: impl Into, + ) -> Option { + self.scopes_by_expression.get(&expression.into()).copied() + } + + /// Returns the [`Scope`] of the `expression`'s enclosing scope. + #[allow(unused)] + #[track_caller] + pub(crate) fn expression_scope(&self, expression: impl Into) -> &Scope { + &self.scopes[self.expression_scope_id(expression)] + } + + /// Returns the [`Scope`] with the given id. + #[track_caller] + pub(crate) fn scope(&self, id: FileScopeId) -> &Scope { + &self.scopes[id] + } + + pub(crate) fn scope_ids(&self) -> impl Iterator { + self.scope_ids_by_scope.iter().copied() + } + + pub(crate) fn symbol_is_global_in_scope( + &self, + symbol: ScopedPlaceId, + scope: FileScopeId, + ) -> bool { + self.globals_by_scope + .get(&scope) + .is_some_and(|globals| globals.contains(&symbol)) + } + + /// Returns the id of the parent scope. + pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option { + let scope = self.scope(scope_id); + scope.parent() + } + + /// Returns the parent scope of `scope_id`. + #[expect(unused)] + #[track_caller] + pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> { + Some(&self.scopes[self.parent_scope_id(scope_id)?]) + } + + fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { + self.parent_scope_id(scope_id) + .is_none_or(|parent_scope_id| { + if !self.is_scope_reachable(db, parent_scope_id) { + return false; + } + + let parent_use_def = self.use_def_map(parent_scope_id); + let reachability = self.scope(scope_id).reachability(); + + parent_use_def.is_reachable(db, reachability) + }) + } + + /// Returns true if a given AST node is reachable from the start of the scope. For example, + /// in the following code, expression `2` is reachable, but expressions `1` and `3` are not: + /// ```py + /// def f(): + /// x = 1 + /// if False: + /// x # 1 + /// x # 2 + /// return + /// x # 3 + /// ``` + pub(crate) fn is_node_reachable( + &self, + db: &'db dyn crate::Db, + scope_id: FileScopeId, + node_key: NodeKey, + ) -> bool { + self.is_scope_reachable(db, scope_id) + && self.use_def_map(scope_id).is_node_reachable(db, node_key) + } + + /// Returns an iterator over the descendent scopes of `scope`. + #[allow(unused)] + pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter { + DescendantsIter::new(self, scope) + } + + /// Returns an iterator over the direct child scopes of `scope`. + #[allow(unused)] + pub(crate) fn child_scopes(&self, scope: FileScopeId) -> ChildrenIter { + ChildrenIter::new(self, scope) + } + + /// Returns an iterator over all ancestors of `scope`, starting with `scope` itself. + pub(crate) fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter { + AncestorsIter::new(self, scope) + } + + /// 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 definitions( + &self, + definition_key: impl Into, + ) -> &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 + std::fmt::Debug + Copy, + ) -> Definition<'db> { + 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. + /// Panics if we have no expression ingredient for that node. We can only call this method for + /// standalone-inferable expressions, which we call `add_standalone_expression` for in + /// [`SemanticIndexBuilder`]. + #[track_caller] + pub(crate) fn expression( + &self, + expression_key: impl Into, + ) -> Expression<'db> { + self.expressions_by_node[&expression_key.into()] + } + + pub(crate) fn try_expression( + &self, + expression_key: impl Into, + ) -> Option> { + self.expressions_by_node + .get(&expression_key.into()) + .copied() + } + + pub(crate) fn is_standalone_expression( + &self, + expression_key: impl Into, + ) -> bool { + self.expressions_by_node + .contains_key(&expression_key.into()) + } + + /// 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 { + self.scopes_by_node[&node.node_key()] + } + + /// Checks if there is an import of `__future__.annotations` in the global scope, which affects + /// the logic for type inference. + pub(super) fn has_future_annotations(&self) -> bool { + self.has_future_annotations + } + + /// Returns + /// * `NoLongerInEagerContext` if the nested scope is no longer in an eager context + /// (that is, not every scope that will be traversed is eager). + /// * an iterator of bindings for a particular nested eager scope reference if the bindings exist. + /// * a narrowing constraint if there are no bindings, but there is a narrowing constraint for an outer scope symbol. + /// * `NotFound` if the narrowing constraint / bindings do not exist in the nested eager scope. + pub(crate) fn eager_snapshot( + &self, + enclosing_scope: FileScopeId, + expr: &PlaceExpr, + nested_scope: FileScopeId, + ) -> EagerSnapshotResult<'_, 'db> { + for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) { + if ancestor_scope_id == enclosing_scope { + break; + } + if !ancestor_scope.is_eager() { + return EagerSnapshotResult::NoLongerInEagerContext; + } + } + let Some(place_id) = self.place_tables[enclosing_scope].place_id_by_expr(expr) else { + return EagerSnapshotResult::NotFound; + }; + let key = EagerSnapshotKey { + enclosing_scope, + enclosing_place: place_id, + nested_scope, + }; + let Some(id) = self.eager_snapshots.get(&key) else { + return EagerSnapshotResult::NotFound; + }; + self.use_def_maps[enclosing_scope].inner.eager_snapshot(*id) + } + + pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] { + &self.semantic_syntax_errors + } +} + +#[derive(Debug, PartialEq, Eq, Clone, salsa::Update, get_size2::GetSize)] +pub(crate) struct ArcUseDefMap<'db> { + #[get_size(size_fn = untracked_arc_size)] + inner: Arc>, +} + +impl<'db> std::ops::Deref for ArcUseDefMap<'db> { + type Target = UseDefMap<'db>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<'db> ArcUseDefMap<'db> { + pub(crate) fn new(inner: UseDefMap<'db>) -> Self { + Self { + inner: Arc::new(inner), + } + } +} + +pub struct AncestorsIter<'a> { + scopes: &'a IndexSlice, + next_id: Option, +} + +impl<'a> AncestorsIter<'a> { + fn new(module_table: &'a SemanticIndex, start: FileScopeId) -> Self { + Self { + scopes: &module_table.scopes, + next_id: Some(start), + } + } +} + +impl<'a> Iterator for AncestorsIter<'a> { + type Item = (FileScopeId, &'a Scope); + + fn next(&mut self) -> Option { + let current_id = self.next_id?; + let current = &self.scopes[current_id]; + self.next_id = current.parent(); + + Some((current_id, current)) + } +} + +impl FusedIterator for AncestorsIter<'_> {} + +pub struct DescendantsIter<'a> { + next_id: FileScopeId, + descendants: std::slice::Iter<'a, Scope>, +} + +impl<'a> DescendantsIter<'a> { + fn new(index: &'a SemanticIndex, scope_id: FileScopeId) -> Self { + let scope = &index.scopes[scope_id]; + let scopes = &index.scopes[scope.descendants()]; + + Self { + next_id: scope_id + 1, + descendants: scopes.iter(), + } + } +} + +impl<'a> Iterator for DescendantsIter<'a> { + type Item = (FileScopeId, &'a Scope); + + fn next(&mut self) -> Option { + let descendant = self.descendants.next()?; + let id = self.next_id; + self.next_id = self.next_id + 1; + + Some((id, descendant)) + } + + fn size_hint(&self) -> (usize, Option) { + self.descendants.size_hint() + } +} + +impl FusedIterator for DescendantsIter<'_> {} + +impl ExactSizeIterator for DescendantsIter<'_> {} + +pub struct ChildrenIter<'a> { + parent: FileScopeId, + descendants: DescendantsIter<'a>, +} + +impl<'a> ChildrenIter<'a> { + pub(crate) fn new(module_index: &'a SemanticIndex, parent: FileScopeId) -> Self { + let descendants = DescendantsIter::new(module_index, parent); + + Self { + parent, + descendants, + } + } +} + +impl<'a> Iterator for ChildrenIter<'a> { + type Item = (FileScopeId, &'a Scope); + + fn next(&mut self) -> Option { + self.descendants + .find(|(_, scope)| scope.parent() == Some(self.parent)) + } +} + +impl FusedIterator for ChildrenIter<'_> {} + +#[cfg(test)] +mod tests { + use ruff_db::files::{File, system_path_to_file}; + use ruff_db::parsed::{ParsedModuleRef, parsed_module}; + use ruff_python_ast::{self as ast}; + use ruff_text_size::{Ranged, TextRange}; + + use crate::Db; + use crate::db::tests::{TestDb, TestDbBuilder}; + use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId}; + use crate::semantic_index::definition::{Definition, DefinitionKind}; + use crate::semantic_index::place::{FileScopeId, PlaceTable, Scope, ScopeKind, ScopedPlaceId}; + use crate::semantic_index::use_def::UseDefMap; + use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map}; + + impl UseDefMap<'_> { + fn first_public_binding(&self, symbol: ScopedPlaceId) -> Option> { + self.end_of_scope_bindings(symbol) + .find_map(|constrained_binding| constrained_binding.binding.definition()) + } + + fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option> { + self.bindings_at_use(use_id) + .find_map(|constrained_binding| constrained_binding.binding.definition()) + } + } + + struct TestCase { + db: TestDb, + file: File, + } + + fn test_case(content: &str) -> TestCase { + const FILENAME: &str = "test.py"; + + let db = TestDbBuilder::new() + .with_file(FILENAME, content) + .build() + .unwrap(); + + let file = system_path_to_file(&db, FILENAME).unwrap(); + + TestCase { db, file } + } + + fn names(table: &PlaceTable) -> Vec { + table + .places() + .filter_map(|expr| Some(expr.as_name()?.to_string())) + .collect() + } + + #[test] + fn empty() { + let TestCase { db, file } = test_case(""); + let global_table = place_table(&db, global_scope(&db, file)); + + let global_names = names(global_table); + + assert_eq!(global_names, Vec::<&str>::new()); + } + + #[test] + fn simple() { + let TestCase { db, file } = test_case("x"); + let global_table = place_table(&db, global_scope(&db, file)); + + assert_eq!(names(global_table), vec!["x"]); + } + + #[test] + fn annotation_only() { + let TestCase { db, file } = test_case("x: int"); + let global_table = place_table(&db, global_scope(&db, file)); + + assert_eq!(names(global_table), vec!["int", "x"]); + // TODO record definition + } + + #[test] + fn import() { + let TestCase { db, file } = test_case("import foo"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(names(global_table), vec!["foo"]); + let foo = global_table.place_id_by_name("foo").unwrap(); + + let use_def = use_def_map(&db, scope); + let binding = use_def.first_public_binding(foo).unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Import(_))); + } + + #[test] + fn import_sub() { + let TestCase { db, file } = test_case("import foo.bar"); + let global_table = place_table(&db, global_scope(&db, file)); + + assert_eq!(names(global_table), vec!["foo"]); + } + + #[test] + fn import_as() { + let TestCase { db, file } = test_case("import foo.bar as baz"); + let global_table = place_table(&db, global_scope(&db, file)); + + assert_eq!(names(global_table), vec!["baz"]); + } + + #[test] + fn import_from() { + let TestCase { db, file } = test_case("from bar import foo"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(names(global_table), vec!["foo"]); + assert!( + global_table + .place_by_name("foo") + .is_some_and(|symbol| { symbol.is_bound() && !symbol.is_used() }), + "symbols that are defined get the defined flag" + ); + + let use_def = use_def_map(&db, scope); + let binding = use_def + .first_public_binding( + global_table + .place_id_by_name("foo") + .expect("symbol to exist"), + ) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::ImportFrom(_))); + } + + #[test] + fn assign() { + let TestCase { db, file } = test_case("x = foo"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(names(global_table), vec!["foo", "x"]); + assert!( + global_table + .place_by_name("foo") + .is_some_and(|symbol| { !symbol.is_bound() && symbol.is_used() }), + "a symbol used but not bound in a scope should have only the used flag" + ); + let use_def = use_def_map(&db, scope); + let binding = use_def + .first_public_binding(global_table.place_id_by_name("x").expect("symbol exists")) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); + } + + #[test] + fn augmented_assignment() { + let TestCase { db, file } = test_case("x += 1"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(names(global_table), vec!["x"]); + + let use_def = use_def_map(&db, scope); + let binding = use_def + .first_public_binding(global_table.place_id_by_name("x").unwrap()) + .unwrap(); + + assert!(matches!( + binding.kind(&db), + DefinitionKind::AugmentedAssignment(_) + )); + } + + #[test] + fn class_scope() { + let TestCase { db, file } = test_case( + " +class C: + x = 1 +y = 2 +", + ); + let global_table = place_table(&db, global_scope(&db, file)); + + assert_eq!(names(global_table), vec!["C", "y"]); + + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + + let [(class_scope_id, class_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope") + }; + assert_eq!(class_scope.kind(), ScopeKind::Class); + assert_eq!( + class_scope_id.to_scope_id(&db, file).name(&db, &module), + "C" + ); + + let class_table = index.place_table(class_scope_id); + assert_eq!(names(&class_table), vec!["x"]); + + let use_def = index.use_def_map(class_scope_id); + let binding = use_def + .first_public_binding(class_table.place_id_by_name("x").expect("symbol exists")) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); + } + + #[test] + fn function_scope() { + let TestCase { db, file } = test_case( + " +def func(): + x = 1 +y = 2 +", + ); + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["func", "y"]); + + let [(function_scope_id, function_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope") + }; + assert_eq!(function_scope.kind(), ScopeKind::Function); + assert_eq!( + function_scope_id.to_scope_id(&db, file).name(&db, &module), + "func" + ); + + let function_table = index.place_table(function_scope_id); + assert_eq!(names(&function_table), vec!["x"]); + + let use_def = index.use_def_map(function_scope_id); + let binding = use_def + .first_public_binding(function_table.place_id_by_name("x").expect("symbol exists")) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_))); + } + + #[test] + fn function_parameter_symbols() { + let TestCase { db, file } = test_case( + " +def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): + pass +", + ); + + let index = semantic_index(&db, file); + let global_table = place_table(&db, global_scope(&db, file)); + + assert_eq!(names(global_table), vec!["str", "int", "f"]); + + let [(function_scope_id, _function_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("Expected a function scope") + }; + + let function_table = index.place_table(function_scope_id); + assert_eq!( + names(&function_table), + vec!["a", "b", "c", "d", "args", "kwargs"], + ); + + let use_def = index.use_def_map(function_scope_id); + for name in ["a", "b", "c", "d"] { + let binding = use_def + .first_public_binding( + function_table + .place_id_by_name(name) + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_))); + } + let args_binding = use_def + .first_public_binding( + function_table + .place_id_by_name("args") + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!( + args_binding.kind(&db), + DefinitionKind::VariadicPositionalParameter(_) + )); + let kwargs_binding = use_def + .first_public_binding( + function_table + .place_id_by_name("kwargs") + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!( + kwargs_binding.kind(&db), + DefinitionKind::VariadicKeywordParameter(_) + )); + } + + #[test] + fn lambda_parameter_symbols() { + let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None"); + + let index = semantic_index(&db, file); + let global_table = place_table(&db, global_scope(&db, file)); + + assert!(names(global_table).is_empty()); + + let [(lambda_scope_id, _lambda_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("Expected a lambda scope") + }; + + let lambda_table = index.place_table(lambda_scope_id); + assert_eq!( + names(&lambda_table), + vec!["a", "b", "c", "d", "args", "kwargs"], + ); + + let use_def = index.use_def_map(lambda_scope_id); + for name in ["a", "b", "c", "d"] { + let binding = use_def + .first_public_binding(lambda_table.place_id_by_name(name).expect("symbol exists")) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_))); + } + let args_binding = use_def + .first_public_binding( + lambda_table + .place_id_by_name("args") + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!( + args_binding.kind(&db), + DefinitionKind::VariadicPositionalParameter(_) + )); + let kwargs_binding = use_def + .first_public_binding( + lambda_table + .place_id_by_name("kwargs") + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!( + kwargs_binding.kind(&db), + DefinitionKind::VariadicKeywordParameter(_) + )); + } + + /// Test case to validate that the comprehension scope is correctly identified and that the target + /// variable is defined only in the comprehension scope and not in the global scope. + #[test] + fn comprehension_scope() { + let TestCase { db, file } = test_case( + " +[x for x, y in iter1] +", + ); + + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["iter1"]); + + let [(comprehension_scope_id, comprehension_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope") + }; + + assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); + assert_eq!( + comprehension_scope_id + .to_scope_id(&db, file) + .name(&db, &module), + "" + ); + + let comprehension_symbol_table = index.place_table(comprehension_scope_id); + + assert_eq!(names(&comprehension_symbol_table), vec!["x", "y"]); + + let use_def = index.use_def_map(comprehension_scope_id); + for name in ["x", "y"] { + let binding = use_def + .first_public_binding( + comprehension_symbol_table + .place_id_by_name(name) + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!( + binding.kind(&db), + DefinitionKind::Comprehension(_) + )); + } + } + + /// Test case to validate that the `x` variable used in the comprehension is referencing the + /// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one. + #[test] + fn multiple_generators() { + let TestCase { db, file } = test_case( + " +[x for x in iter1 for x in iter2] +", + ); + + let index = semantic_index(&db, file); + let [(comprehension_scope_id, _)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope") + }; + + let use_def = index.use_def_map(comprehension_scope_id); + + let module = parsed_module(&db, file).load(&db); + let syntax = module.syntax(); + let element = syntax.body[0] + .as_expr_stmt() + .unwrap() + .value + .as_list_comp_expr() + .unwrap() + .elt + .as_name_expr() + .unwrap(); + let element_use_id = + element.scoped_use_id(&db, comprehension_scope_id.to_scope_id(&db, file)); + + let binding = use_def.first_binding_at_use(element_use_id).unwrap(); + let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else { + panic!("expected generator definition") + }; + let target = comprehension.target(&module); + let name = target.as_name_expr().unwrap().id().as_str(); + + assert_eq!(name, "x"); + assert_eq!(target.range(), TextRange::new(23.into(), 24.into())); + } + + /// Test case to validate that the nested comprehension creates a new scope which is a child of + /// the outer comprehension scope and the variables are correctly defined in the respective + /// scopes. + #[test] + fn nested_generators() { + let TestCase { db, file } = test_case( + " +[{x for x in iter2} for y in iter1] +", + ); + + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["iter1"]); + + let [(comprehension_scope_id, comprehension_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope") + }; + + assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); + assert_eq!( + comprehension_scope_id + .to_scope_id(&db, file) + .name(&db, &module), + "" + ); + + let comprehension_symbol_table = index.place_table(comprehension_scope_id); + + assert_eq!(names(&comprehension_symbol_table), vec!["y", "iter2"]); + + let [(inner_comprehension_scope_id, inner_comprehension_scope)] = index + .child_scopes(comprehension_scope_id) + .collect::>()[..] + else { + panic!("expected one inner generator scope") + }; + + assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension); + assert_eq!( + inner_comprehension_scope_id + .to_scope_id(&db, file) + .name(&db, &module), + "" + ); + + let inner_comprehension_symbol_table = index.place_table(inner_comprehension_scope_id); + + assert_eq!(names(&inner_comprehension_symbol_table), vec!["x"]); + } + + #[test] + fn with_item_definition() { + let TestCase { db, file } = test_case( + " +with item1 as x, item2 as y: + pass +", + ); + + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["item1", "x", "item2", "y"]); + + let use_def = index.use_def_map(FileScopeId::global()); + for name in ["x", "y"] { + let binding = use_def + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) + .expect("Expected with item definition for {name}"); + assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_))); + } + } + + #[test] + fn with_item_unpacked_definition() { + let TestCase { db, file } = test_case( + " +with context() as (x, y): + pass +", + ); + + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["context", "x", "y"]); + + let use_def = index.use_def_map(FileScopeId::global()); + for name in ["x", "y"] { + let binding = use_def + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) + .expect("Expected with item definition for {name}"); + assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_))); + } + } + + #[test] + fn dupes() { + let TestCase { db, file } = test_case( + " +def func(): + x = 1 +def func(): + y = 2 +", + ); + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["func"]); + let [ + (func_scope1_id, func_scope_1), + (func_scope2_id, func_scope_2), + ] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected two child scopes"); + }; + + assert_eq!(func_scope_1.kind(), ScopeKind::Function); + + assert_eq!( + func_scope1_id.to_scope_id(&db, file).name(&db, &module), + "func" + ); + assert_eq!(func_scope_2.kind(), ScopeKind::Function); + assert_eq!( + func_scope2_id.to_scope_id(&db, file).name(&db, &module), + "func" + ); + + let func1_table = index.place_table(func_scope1_id); + let func2_table = index.place_table(func_scope2_id); + assert_eq!(names(&func1_table), vec!["x"]); + assert_eq!(names(&func2_table), vec!["y"]); + + let use_def = index.use_def_map(FileScopeId::global()); + let binding = use_def + .first_public_binding( + global_table + .place_id_by_name("func") + .expect("symbol exists"), + ) + .unwrap(); + assert!(matches!(binding.kind(&db), DefinitionKind::Function(_))); + } + + #[test] + fn generic_function() { + let TestCase { db, file } = test_case( + " +def func[T](): + x = 1 +", + ); + + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["func"]); + + let [(ann_scope_id, ann_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope"); + }; + + assert_eq!(ann_scope.kind(), ScopeKind::Annotation); + assert_eq!( + ann_scope_id.to_scope_id(&db, file).name(&db, &module), + "func" + ); + let ann_table = index.place_table(ann_scope_id); + assert_eq!(names(&ann_table), vec!["T"]); + + let [(func_scope_id, func_scope)] = + index.child_scopes(ann_scope_id).collect::>()[..] + else { + panic!("expected one child scope"); + }; + assert_eq!(func_scope.kind(), ScopeKind::Function); + assert_eq!( + func_scope_id.to_scope_id(&db, file).name(&db, &module), + "func" + ); + let func_table = index.place_table(func_scope_id); + assert_eq!(names(&func_table), vec!["x"]); + } + + #[test] + fn generic_class() { + let TestCase { db, file } = test_case( + " +class C[T]: + x = 1 +", + ); + + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + let global_table = index.place_table(FileScopeId::global()); + + assert_eq!(names(&global_table), vec!["C"]); + + let [(ann_scope_id, ann_scope)] = index + .child_scopes(FileScopeId::global()) + .collect::>()[..] + else { + panic!("expected one child scope"); + }; + + assert_eq!(ann_scope.kind(), ScopeKind::Annotation); + assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C"); + let ann_table = index.place_table(ann_scope_id); + assert_eq!(names(&ann_table), vec!["T"]); + assert!( + ann_table + .place_by_name("T") + .is_some_and(|s| s.is_bound() && !s.is_used()), + "type parameters are defined by the scope that introduces them" + ); + + let [(class_scope_id, class_scope)] = + index.child_scopes(ann_scope_id).collect::>()[..] + else { + panic!("expected one child scope"); + }; + + assert_eq!(class_scope.kind(), ScopeKind::Class); + assert_eq!( + class_scope_id.to_scope_id(&db, file).name(&db, &module), + "C" + ); + assert_eq!(names(&index.place_table(class_scope_id)), vec!["x"]); + } + + #[test] + fn reachability_trivial() { + let TestCase { db, file } = test_case("x = 1; x"); + let module = parsed_module(&db, file).load(&db); + let scope = global_scope(&db, file); + let ast = module.syntax(); + let ast::Stmt::Expr(ast::StmtExpr { + value: x_use_expr, .. + }) = &ast.body[1] + else { + panic!("should be an expr") + }; + let ast::Expr::Name(x_use_expr_name) = x_use_expr.as_ref() else { + panic!("expected a Name"); + }; + let x_use_id = x_use_expr_name.scoped_use_id(&db, scope); + let use_def = use_def_map(&db, scope); + let binding = use_def.first_binding_at_use(x_use_id).unwrap(); + let DefinitionKind::Assignment(assignment) = binding.kind(&db) else { + panic!("should be an assignment definition") + }; + let ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(num), + .. + }) = assignment.value(&module) + else { + panic!("should be a number literal") + }; + assert_eq!(*num, 1); + } + + #[test] + fn expression_scope() { + let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4"); + + let index = semantic_index(&db, file); + let module = parsed_module(&db, file).load(&db); + let ast = module.syntax(); + + let x_stmt = ast.body[0].as_assign_stmt().unwrap(); + let x = &x_stmt.targets[0]; + + assert_eq!(index.expression_scope(x).kind(), ScopeKind::Module); + assert_eq!(index.expression_scope_id(x), FileScopeId::global()); + + let def = ast.body[1].as_function_def_stmt().unwrap(); + let y_stmt = def.body[0].as_assign_stmt().unwrap(); + let y = &y_stmt.targets[0]; + + assert_eq!(index.expression_scope(y).kind(), ScopeKind::Function); + } + + #[test] + fn scope_iterators() { + fn scope_names<'a, 'db>( + scopes: impl Iterator, + db: &'db dyn Db, + file: File, + module: &'a ParsedModuleRef, + ) -> Vec<&'a str> { + scopes + .into_iter() + .map(|(scope_id, _)| scope_id.to_scope_id(db, file).name(db, module)) + .collect() + } + + let TestCase { db, file } = test_case( + r" +class Test: + def foo(): + def bar(): + ... + def baz(): + pass + +def x(): + pass", + ); + + let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, file); + + let descendants = index.descendent_scopes(FileScopeId::global()); + assert_eq!( + scope_names(descendants, &db, file, &module), + vec!["Test", "foo", "bar", "baz", "x"] + ); + + let children = index.child_scopes(FileScopeId::global()); + assert_eq!(scope_names(children, &db, file, &module), vec!["Test", "x"]); + + let test_class = index.child_scopes(FileScopeId::global()).next().unwrap().0; + let test_child_scopes = index.child_scopes(test_class); + assert_eq!( + scope_names(test_child_scopes, &db, file, &module), + vec!["foo", "baz"] + ); + + let bar_scope = index + .descendent_scopes(FileScopeId::global()) + .nth(2) + .unwrap() + .0; + let ancestors = index.ancestor_scopes(bar_scope); + + assert_eq!( + scope_names(ancestors, &db, file, &module), + vec!["bar", "foo", "Test", ""] + ); + } + + #[test] + fn match_stmt() { + let TestCase { db, file } = test_case( + " +match subject: + case a: ... + case [b, c, *d]: ... + case e as f: ... + case {'x': g, **h}: ... + case Foo(i, z=j): ... + case k | l: ... + case _: ... +", + ); + + let global_scope_id = global_scope(&db, file); + let global_table = place_table(&db, global_scope_id); + + assert!(global_table.place_by_name("Foo").unwrap().is_used()); + assert_eq!( + names(global_table), + vec![ + "subject", "a", "b", "c", "d", "e", "f", "g", "h", "Foo", "i", "j", "k", "l" + ] + ); + + let use_def = use_def_map(&db, global_scope_id); + for (name, expected_index) in [ + ("a", 0), + ("b", 0), + ("c", 1), + ("d", 2), + ("e", 0), + ("f", 1), + ("g", 0), + ("h", 1), + ("i", 0), + ("j", 1), + ("k", 0), + ("l", 1), + ] { + let binding = use_def + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) + .expect("Expected with item definition for {name}"); + if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) { + assert_eq!(pattern.index(), expected_index); + } else { + panic!("Expected match pattern definition for {name}"); + } + } + } + + #[test] + fn nested_match_case() { + let TestCase { db, file } = test_case( + " +match 1: + case first: + match 2: + case second: + pass +", + ); + + let global_scope_id = global_scope(&db, file); + let global_table = place_table(&db, global_scope_id); + + assert_eq!(names(global_table), vec!["first", "second"]); + + let use_def = use_def_map(&db, global_scope_id); + for (name, expected_index) in [("first", 0), ("second", 0)] { + let binding = use_def + .first_public_binding(global_table.place_id_by_name(name).expect("symbol exists")) + .expect("Expected with item definition for {name}"); + if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) { + assert_eq!(pattern.index(), expected_index); + } else { + panic!("Expected match pattern definition for {name}"); + } + } + } + + #[test] + fn for_loops_single_assignment() { + let TestCase { db, file } = test_case("for x in a: pass"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(&names(global_table), &["a", "x"]); + + let use_def = use_def_map(&db, scope); + let binding = use_def + .first_public_binding(global_table.place_id_by_name("x").unwrap()) + .unwrap(); + + assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); + } + + #[test] + fn for_loops_simple_unpacking() { + let TestCase { db, file } = test_case("for (x, y) in a: pass"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(&names(global_table), &["a", "x", "y"]); + + let use_def = use_def_map(&db, scope); + let x_binding = use_def + .first_public_binding(global_table.place_id_by_name("x").unwrap()) + .unwrap(); + let y_binding = use_def + .first_public_binding(global_table.place_id_by_name("y").unwrap()) + .unwrap(); + + assert!(matches!(x_binding.kind(&db), DefinitionKind::For(_))); + assert!(matches!(y_binding.kind(&db), DefinitionKind::For(_))); + } + + #[test] + fn for_loops_complex_unpacking() { + let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass"); + let scope = global_scope(&db, file); + let global_table = place_table(&db, scope); + + assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]); + + let use_def = use_def_map(&db, scope); + let binding = use_def + .first_public_binding(global_table.place_id_by_name("a").unwrap()) + .unwrap(); + + assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs new file mode 100644 index 0000000000000..6d12d2291612e --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs @@ -0,0 +1,144 @@ +use rustc_hash::FxHashMap; + +use ruff_index::newtype_index; +use ruff_python_ast as ast; +use ruff_python_ast::ExprRef; + +use crate::Db; +use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; +use crate::semantic_index::place::ScopeId; +use crate::semantic_index::semantic_index; + +/// AST ids for a single scope. +/// +/// The motivation for building the AST ids per scope isn't about reducing invalidation because +/// the struct changes whenever the parsed AST changes. Instead, it's mainly that we can +/// build the AST ids struct when building the place table and also keep the property that +/// IDs of outer scopes are unaffected by changes in inner scopes. +/// +/// For example, we don't want that adding new statements to `foo` changes the statement id of `x = foo()` in: +/// +/// ```python +/// def foo(): +/// return 5 +/// +/// x = foo() +/// ``` +#[derive(Debug, salsa::Update, get_size2::GetSize)] +pub(crate) struct AstIds { + /// Maps expressions which "use" a place (that is, [`ast::ExprName`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`]) to a use id. + uses_map: FxHashMap, +} + +impl AstIds { + fn use_id(&self, key: impl Into) -> ScopedUseId { + self.uses_map[&key.into()] + } +} + +fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { + semantic_index(db, scope.file(db)).ast_ids(scope.file_scope_id(db)) +} + +/// Uniquely identifies a use of a name in a [`crate::semantic_index::place::FileScopeId`]. +#[newtype_index] +#[derive(get_size2::GetSize)] +pub struct ScopedUseId; + +pub trait HasScopedUseId { + /// Returns the ID that uniquely identifies the use in `scope`. + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId; +} + +impl HasScopedUseId for ast::Identifier { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let ast_ids = ast_ids(db, scope); + ast_ids.use_id(self) + } +} + +impl HasScopedUseId for ast::ExprName { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let expression_ref = ExprRef::from(self); + expression_ref.scoped_use_id(db, scope) + } +} + +impl HasScopedUseId for ast::ExprAttribute { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let expression_ref = ExprRef::from(self); + expression_ref.scoped_use_id(db, scope) + } +} + +impl HasScopedUseId for ast::ExprSubscript { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let expression_ref = ExprRef::from(self); + expression_ref.scoped_use_id(db, scope) + } +} + +impl HasScopedUseId for ast::ExprRef<'_> { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let ast_ids = ast_ids(db, scope); + ast_ids.use_id(*self) + } +} + +#[derive(Debug, Default)] +pub(super) struct AstIdsBuilder { + uses_map: FxHashMap, +} + +impl AstIdsBuilder { + /// Adds `expr` to the use ids map and returns its id. + pub(super) fn record_use(&mut self, expr: impl Into) -> ScopedUseId { + let use_id = self.uses_map.len().into(); + + self.uses_map.insert(expr.into(), use_id); + + use_id + } + + pub(super) fn finish(mut self) -> AstIds { + self.uses_map.shrink_to_fit(); + + AstIds { + uses_map: self.uses_map, + } + } +} + +/// Node key that can only be constructed for expressions. +pub(crate) mod node_key { + use ruff_python_ast as ast; + + use crate::node_key::NodeKey; + + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update, get_size2::GetSize)] + pub(crate) struct ExpressionNodeKey(NodeKey); + + impl From> for ExpressionNodeKey { + fn from(value: ast::ExprRef<'_>) -> Self { + Self(NodeKey::from_node(value)) + } + } + + impl From<&ast::Expr> for ExpressionNodeKey { + fn from(value: &ast::Expr) -> Self { + Self(NodeKey::from_node(value)) + } + } + + impl From<&ast::ExprCall> for ExpressionNodeKey { + fn from(value: &ast::ExprCall) -> Self { + Self(NodeKey::from_node(value)) + } + } + + impl From<&ast::Identifier> for ExpressionNodeKey { + fn from(value: &ast::Identifier) -> Self { + Self(NodeKey::from_node(value)) + } + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs new file mode 100644 index 0000000000000..4d2cb57c8433f --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -0,0 +1,2587 @@ +use std::cell::{OnceCell, RefCell}; +use std::sync::Arc; + +use except_handlers::TryNodeContextStackManager; +use rustc_hash::{FxHashMap, FxHashSet}; + +use ruff_db::files::File; +use ruff_db::parsed::ParsedModuleRef; +use ruff_db::source::{SourceText, source_text}; +use ruff_index::IndexVec; +use ruff_python_ast::name::Name; +use ruff_python_ast::visitor::{Visitor, walk_expr, walk_pattern, walk_stmt}; +use ruff_python_ast::{self as ast, PySourceType, PythonVersion}; +use ruff_python_parser::semantic_errors::{ + SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, +}; +use ruff_text_size::TextRange; + +use crate::ast_node_ref::AstNodeRef; +use crate::module_name::ModuleName; +use crate::module_resolver::resolve_module; +use crate::node_key::NodeKey; +use crate::semantic_index::ast_ids::AstIdsBuilder; +use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; +use crate::semantic_index::definition::{ + AnnotatedAssignmentDefinitionNodeRef, AssignmentDefinitionNodeRef, + ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionNodeKey, + DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef, + ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, + StarImportDefinitionNodeRef, WithItemDefinitionNodeRef, +}; +use crate::semantic_index::expression::{Expression, ExpressionKind}; +use crate::semantic_index::place::{ + FileScopeId, NodeWithScopeKey, NodeWithScopeKind, NodeWithScopeRef, PlaceExpr, + PlaceExprWithFlags, PlaceTableBuilder, Scope, ScopeId, ScopeKind, ScopedPlaceId, +}; +use crate::semantic_index::predicate::{ + CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, + PredicateOrLiteral, ScopedPredicateId, StarImportPlaceholderPredicate, +}; +use crate::semantic_index::re_exports::exported_names; +use crate::semantic_index::reachability_constraints::{ + ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId, +}; +use crate::semantic_index::use_def::{ + EagerSnapshotKey, FlowSnapshot, ScopedEagerSnapshotId, UseDefMapBuilder, +}; +use crate::semantic_index::{ArcUseDefMap, SemanticIndex}; +use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; +use crate::{Db, Program}; + +mod except_handlers; + +#[derive(Clone, Debug, Default)] +struct Loop { + /// Flow states at each `break` in the current loop. + break_states: Vec, +} + +impl Loop { + fn push_break(&mut self, state: FlowSnapshot) { + self.break_states.push(state); + } +} + +struct ScopeInfo { + file_scope_id: FileScopeId, + /// Current loop state; None if we are not currently visiting a loop + current_loop: Option, +} + +pub(super) struct SemanticIndexBuilder<'db, 'ast> { + // Builder state + db: &'db dyn Db, + file: File, + source_type: PySourceType, + module: &'ast ParsedModuleRef, + scope_stack: Vec, + /// The assignments we're currently visiting, with + /// the most recent visit at the end of the Vec + current_assignments: Vec>, + /// The match case we're currently visiting. + current_match_case: Option>, + /// The name of the first function parameter of the innermost function that we're currently visiting. + current_first_parameter_name: Option<&'ast str>, + + /// Per-scope contexts regarding nested `try`/`except` statements + try_node_context_stack_manager: TryNodeContextStackManager, + + /// Flags about the file's global scope + has_future_annotations: bool, + + // Used for checking semantic syntax errors + python_version: PythonVersion, + source_text: OnceCell, + semantic_checker: SemanticSyntaxChecker, + + // Semantic Index fields + scopes: IndexVec, + scope_ids_by_scope: IndexVec>, + place_tables: IndexVec, + ast_ids: IndexVec, + use_def_maps: IndexVec>, + scopes_by_node: FxHashMap, + scopes_by_expression: FxHashMap, + globals_by_scope: FxHashMap>, + definitions_by_node: FxHashMap>, + expressions_by_node: FxHashMap>, + imported_modules: FxHashSet, + /// Hashset of all [`FileScopeId`]s that correspond to [generator functions]. + /// + /// [generator functions]: https://docs.python.org/3/glossary.html#term-generator + generator_functions: FxHashSet, + eager_snapshots: FxHashMap, + /// Errors collected by the `semantic_checker`. + semantic_syntax_errors: RefCell>, +} + +impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { + pub(super) fn new(db: &'db dyn Db, file: File, module_ref: &'ast ParsedModuleRef) -> Self { + let mut builder = Self { + db, + file, + source_type: file.source_type(db), + module: module_ref, + scope_stack: Vec::new(), + current_assignments: vec![], + current_match_case: None, + current_first_parameter_name: None, + try_node_context_stack_manager: TryNodeContextStackManager::default(), + + has_future_annotations: false, + + scopes: IndexVec::new(), + place_tables: IndexVec::new(), + ast_ids: IndexVec::new(), + scope_ids_by_scope: IndexVec::new(), + use_def_maps: IndexVec::new(), + + scopes_by_expression: FxHashMap::default(), + scopes_by_node: FxHashMap::default(), + definitions_by_node: FxHashMap::default(), + expressions_by_node: FxHashMap::default(), + globals_by_scope: FxHashMap::default(), + + imported_modules: FxHashSet::default(), + generator_functions: FxHashSet::default(), + + eager_snapshots: FxHashMap::default(), + + python_version: Program::get(db).python_version(db), + source_text: OnceCell::new(), + semantic_checker: SemanticSyntaxChecker::default(), + semantic_syntax_errors: RefCell::default(), + }; + + builder.push_scope_with_parent( + NodeWithScopeRef::Module, + None, + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + + builder + } + + fn current_scope_info(&self) -> &ScopeInfo { + self.scope_stack + .last() + .expect("SemanticIndexBuilder should have created a root scope") + } + + fn current_scope_info_mut(&mut self) -> &mut ScopeInfo { + self.scope_stack + .last_mut() + .expect("SemanticIndexBuilder should have created a root scope") + } + + fn current_scope(&self) -> FileScopeId { + self.current_scope_info().file_scope_id + } + + /// 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 + /// function body. + fn is_method_of_class(&self) -> Option { + let mut scopes_rev = self.scope_stack.iter().rev(); + let current = scopes_rev.next()?; + + if self.scopes[current.file_scope_id].kind() != ScopeKind::Function { + return None; + } + + let parent = scopes_rev.next()?; + + match self.scopes[parent.file_scope_id].kind() { + ScopeKind::Class => Some(parent.file_scope_id), + ScopeKind::Annotation => { + // If the function is generic, the parent scope is an annotation scope. + // In this case, we need to go up one level higher to find the class scope. + let grandparent = scopes_rev.next()?; + + if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class { + Some(grandparent.file_scope_id) + } else { + None + } + } + _ => None, + } + } + + /// Push a new loop, returning the outer loop, if any. + fn push_loop(&mut self) -> Option { + self.current_scope_info_mut() + .current_loop + .replace(Loop::default()) + } + + /// Pop a loop, replacing with the previous saved outer loop, if any. + fn pop_loop(&mut self, outer_loop: Option) -> Loop { + std::mem::replace(&mut self.current_scope_info_mut().current_loop, outer_loop) + .expect("pop_loop() should not be called without a prior push_loop()") + } + + fn current_loop_mut(&mut self) -> Option<&mut Loop> { + self.current_scope_info_mut().current_loop.as_mut() + } + + fn push_scope(&mut self, node: NodeWithScopeRef) { + let parent = self.current_scope(); + let reachability = self.current_use_def_map().reachability; + self.push_scope_with_parent(node, Some(parent), reachability); + } + + fn push_scope_with_parent( + &mut self, + node: NodeWithScopeRef, + parent: Option, + reachability: ScopedReachabilityConstraintId, + ) { + let children_start = self.scopes.next_index() + 1; + + // Note `node` is guaranteed to be a child of `self.module` + let node_with_kind = node.to_kind(self.module); + + let scope = Scope::new( + parent, + node_with_kind, + children_start..children_start, + reachability, + ); + let is_class_scope = scope.kind().is_class(); + self.try_node_context_stack_manager.enter_nested_scope(); + + let file_scope_id = self.scopes.push(scope); + self.place_tables.push(PlaceTableBuilder::default()); + self.use_def_maps + .push(UseDefMapBuilder::new(is_class_scope)); + let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default()); + + let scope_id = ScopeId::new(self.db, self.file, file_scope_id); + + self.scope_ids_by_scope.push(scope_id); + let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id); + debug_assert_eq!(previous, None); + + debug_assert_eq!(ast_id_scope, file_scope_id); + + self.scope_stack.push(ScopeInfo { + file_scope_id, + current_loop: None, + }); + } + + fn pop_scope(&mut self) -> FileScopeId { + self.try_node_context_stack_manager.exit_scope(); + + let ScopeInfo { + file_scope_id: popped_scope_id, + .. + } = self + .scope_stack + .pop() + .expect("Root scope should be present"); + + let children_end = self.scopes.next_index(); + let popped_scope = &mut self.scopes[popped_scope_id]; + popped_scope.extend_descendants(children_end); + + if !popped_scope.is_eager() { + return popped_scope_id; + } + + // If the scope that we just popped off is an eager scope, we need to "lock" our view of + // which bindings reach each of the uses in the scope. Loop through each enclosing scope, + // looking for any that bind each place. + // TODO: Bindings in eager nested scopes also need to be recorded. For example: + // ```python + // class C: + // x: int | None = None + // c = C() + // class _: + // c.x = 1 + // reveal_type(c.x) # revealed: Literal[1] + // ``` + for enclosing_scope_info in self.scope_stack.iter().rev() { + let enclosing_scope_id = enclosing_scope_info.file_scope_id; + let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind(); + let enclosing_place_table = &self.place_tables[enclosing_scope_id]; + + for nested_place in self.place_tables[popped_scope_id].places() { + // Skip this place if this enclosing scope doesn't contain any bindings for it. + // Note that even if this place is bound in the popped scope, + // it may refer to the enclosing scope bindings + // so we also need to snapshot the bindings of the enclosing scope. + + let Some(enclosing_place_id) = + enclosing_place_table.place_id_by_expr(&nested_place.expr) + else { + continue; + }; + let enclosing_place = enclosing_place_table.place_expr(enclosing_place_id); + + // Snapshot the state of this place that are visible at this point in this + // enclosing scope. + let key = EagerSnapshotKey { + enclosing_scope: enclosing_scope_id, + enclosing_place: enclosing_place_id, + nested_scope: popped_scope_id, + }; + let eager_snapshot = self.use_def_maps[enclosing_scope_id].snapshot_eager_state( + enclosing_place_id, + enclosing_scope_kind, + enclosing_place, + ); + self.eager_snapshots.insert(key, eager_snapshot); + } + + // Lazy scopes are "sticky": once we see a lazy scope we stop doing lookups + // eagerly, even if we would encounter another eager enclosing scope later on. + // Also, narrowing constraints outside a lazy scope are not applicable. + // TODO: If the place has never been rewritten, they are applicable. + if !enclosing_scope_kind.is_eager() { + break; + } + } + + popped_scope_id + } + + fn current_place_table(&mut self) -> &mut PlaceTableBuilder { + let scope_id = self.current_scope(); + &mut self.place_tables[scope_id] + } + + fn current_use_def_map_mut(&mut self) -> &mut UseDefMapBuilder<'db> { + let scope_id = self.current_scope(); + &mut self.use_def_maps[scope_id] + } + + fn current_use_def_map(&self) -> &UseDefMapBuilder<'db> { + let scope_id = self.current_scope(); + &self.use_def_maps[scope_id] + } + + fn current_reachability_constraints_mut(&mut self) -> &mut ReachabilityConstraintsBuilder { + let scope_id = self.current_scope(); + &mut self.use_def_maps[scope_id].reachability_constraints + } + + fn current_ast_ids(&mut self) -> &mut AstIdsBuilder { + let scope_id = self.current_scope(); + &mut self.ast_ids[scope_id] + } + + fn flow_snapshot(&self) -> FlowSnapshot { + self.current_use_def_map().snapshot() + } + + fn flow_restore(&mut self, state: FlowSnapshot) { + self.current_use_def_map_mut().restore(state); + } + + fn flow_merge(&mut self, state: FlowSnapshot) { + self.current_use_def_map_mut().merge(state); + } + + /// Add a symbol to the place table and the use-def map. + /// Return the [`ScopedPlaceId`] that uniquely identifies the symbol in both. + fn add_symbol(&mut self, name: Name) -> ScopedPlaceId { + let (place_id, added) = self.current_place_table().add_symbol(name); + if added { + self.current_use_def_map_mut().add_place(place_id); + } + place_id + } + + /// Add a place to the place table and the use-def map. + /// Return the [`ScopedPlaceId`] that uniquely identifies the place in both. + fn add_place(&mut self, place_expr: PlaceExprWithFlags) -> ScopedPlaceId { + let (place_id, added) = self.current_place_table().add_place(place_expr); + if added { + self.current_use_def_map_mut().add_place(place_id); + } + place_id + } + + fn mark_place_bound(&mut self, id: ScopedPlaceId) { + self.current_place_table().mark_place_bound(id); + } + + fn mark_place_declared(&mut self, id: ScopedPlaceId) { + self.current_place_table().mark_place_declared(id); + } + + fn mark_place_used(&mut self, id: ScopedPlaceId) { + self.current_place_table().mark_place_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, + place: ScopedPlaceId, + definition_node: impl Into> + std::fmt::Debug + Copy, + ) -> Definition<'db> { + let (definition, num_definitions) = self.push_additional_definition(place, definition_node); + debug_assert_eq!( + num_definitions, 1, + "Attempted to create multiple `Definition`s associated with AST node {definition_node:?}" + ); + definition + } + + fn delete_associated_bindings(&mut self, place: ScopedPlaceId) { + let scope = self.current_scope(); + // Don't delete associated bindings if the scope is a class scope & place is a name (it's never visible to nested scopes) + if self.scopes[scope].kind() == ScopeKind::Class + && self.place_tables[scope].place_expr(place).is_name() + { + return; + } + for associated_place in self.place_tables[scope].associated_place_ids(place) { + let is_place_name = self.place_tables[scope] + .place_expr(associated_place) + .is_name(); + self.use_def_maps[scope].delete_binding(associated_place, is_place_name); + } + } + + fn delete_binding(&mut self, place: ScopedPlaceId) { + let is_place_name = self.current_place_table().place_expr(place).is_name(); + self.current_use_def_map_mut() + .delete_binding(place, is_place_name); + } + + /// 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, + place: ScopedPlaceId, + definition_node: impl Into>, + ) -> (Definition<'db>, usize) { + let definition_node: DefinitionNodeRef<'ast, 'db> = definition_node.into(); + + // Note `definition_node` is guaranteed to be a child of `self.module` + let kind = definition_node.into_owned(self.module); + + let category = kind.category(self.source_type.is_stub(), self.module); + let is_reexported = kind.is_reexported(); + + let definition: Definition<'db> = Definition::new( + self.db, + self.file, + self.current_scope(), + place, + kind, + is_reexported, + ); + + 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_place_bound(place); + } + if category.is_declaration() { + self.mark_place_declared(place); + } + + let is_place_name = self.current_place_table().place_expr(place).is_name(); + let use_def = self.current_use_def_map_mut(); + match category { + DefinitionCategory::DeclarationAndBinding => { + use_def.record_declaration_and_binding(place, definition, is_place_name); + self.delete_associated_bindings(place); + } + DefinitionCategory::Declaration => use_def.record_declaration(place, definition), + DefinitionCategory::Binding => { + use_def.record_binding(place, definition, is_place_name); + self.delete_associated_bindings(place); + } + } + + let mut try_node_stack_manager = std::mem::take(&mut self.try_node_context_stack_manager); + try_node_stack_manager.record_definition(self); + self.try_node_context_stack_manager = try_node_stack_manager; + + (definition, num_definitions) + } + + fn record_expression_narrowing_constraint( + &mut self, + precide_node: &ast::Expr, + ) -> PredicateOrLiteral<'db> { + let predicate = self.build_predicate(precide_node); + self.record_narrowing_constraint(predicate); + predicate + } + + fn build_predicate(&mut self, predicate_node: &ast::Expr) -> PredicateOrLiteral<'db> { + // Some commonly used test expressions are eagerly evaluated as `true` + // or `false` here for performance reasons. This list does not need to + // be exhaustive. More complex expressions will still evaluate to the + // correct value during type-checking. + fn resolve_to_literal(node: &ast::Expr) -> Option { + match node { + ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => Some(*value), + ast::Expr::Name(ast::ExprName { id, .. }) if id == "TYPE_CHECKING" => Some(true), + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(n), + .. + }) => Some(*n != 0), + ast::Expr::EllipsisLiteral(_) => Some(true), + ast::Expr::NoneLiteral(_) => Some(false), + ast::Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::Not, + operand, + .. + }) => Some(!resolve_to_literal(operand)?), + _ => None, + } + } + + let expression = self.add_standalone_expression(predicate_node); + + match resolve_to_literal(predicate_node) { + Some(literal) => PredicateOrLiteral::Literal(literal), + None => PredicateOrLiteral::Predicate(Predicate { + node: PredicateNode::Expression(expression), + is_positive: true, + }), + } + } + + /// Adds a new predicate to the list of all predicates, but does not record it. Returns the + /// predicate ID for later recording using + /// [`SemanticIndexBuilder::record_narrowing_constraint_id`]. + fn add_predicate(&mut self, predicate: PredicateOrLiteral<'db>) -> ScopedPredicateId { + self.current_use_def_map_mut().add_predicate(predicate) + } + + /// Negates a predicate and adds it to the list of all predicates, does not record it. + fn add_negated_predicate(&mut self, predicate: PredicateOrLiteral<'db>) -> ScopedPredicateId { + self.current_use_def_map_mut() + .add_predicate(predicate.negated()) + } + + /// Records a previously added narrowing constraint by adding it to all live bindings. + fn record_narrowing_constraint_id(&mut self, predicate: ScopedPredicateId) { + self.current_use_def_map_mut() + .record_narrowing_constraint(predicate); + } + + /// Adds and records a narrowing constraint, i.e. adds it to all live bindings. + fn record_narrowing_constraint(&mut self, predicate: PredicateOrLiteral<'db>) { + let use_def = self.current_use_def_map_mut(); + let predicate_id = use_def.add_predicate(predicate); + use_def.record_narrowing_constraint(predicate_id); + } + + /// Negates the given predicate and then adds it as a narrowing constraint to all live + /// bindings. + fn record_negated_narrowing_constraint( + &mut self, + predicate: PredicateOrLiteral<'db>, + ) -> ScopedPredicateId { + let id = self.add_negated_predicate(predicate); + self.record_narrowing_constraint_id(id); + id + } + + /// Records that all remaining statements in the current block are unreachable. + fn mark_unreachable(&mut self) { + self.current_use_def_map_mut().mark_unreachable(); + } + + /// Records a reachability constraint that always evaluates to "ambiguous". + fn record_ambiguous_reachability(&mut self) { + self.current_use_def_map_mut() + .record_reachability_constraint(ScopedReachabilityConstraintId::AMBIGUOUS); + } + + /// Record a constraint that affects the reachability of the current position in the semantic + /// index analysis. For example, if we encounter a `if test:` branch, we immediately record + /// a `test` constraint, because if `test` later (during type checking) evaluates to `False`, + /// we know that all statements that follow in this path of control flow will be unreachable. + fn record_reachability_constraint( + &mut self, + predicate: PredicateOrLiteral<'db>, + ) -> ScopedReachabilityConstraintId { + let predicate_id = self.add_predicate(predicate); + self.record_reachability_constraint_id(predicate_id) + } + + /// Similar to [`Self::record_reachability_constraint`], but takes a [`ScopedPredicateId`]. + fn record_reachability_constraint_id( + &mut self, + predicate_id: ScopedPredicateId, + ) -> ScopedReachabilityConstraintId { + let reachability_constraint = self + .current_reachability_constraints_mut() + .add_atom(predicate_id); + + self.current_use_def_map_mut() + .record_reachability_constraint(reachability_constraint); + reachability_constraint + } + + /// Record the negation of a given reachability constraint. + fn record_negated_reachability_constraint( + &mut self, + reachability_constraint: ScopedReachabilityConstraintId, + ) { + let negated_constraint = self + .current_reachability_constraints_mut() + .add_not_constraint(reachability_constraint); + self.current_use_def_map_mut() + .record_reachability_constraint(negated_constraint); + } + + fn push_assignment(&mut self, assignment: CurrentAssignment<'ast, 'db>) { + self.current_assignments.push(assignment); + } + + fn pop_assignment(&mut self) { + let popped_assignment = self.current_assignments.pop(); + debug_assert!(popped_assignment.is_some()); + } + + fn current_assignment(&self) -> Option> { + self.current_assignments.last().copied() + } + + fn current_assignment_mut(&mut self) -> Option<&mut CurrentAssignment<'ast, 'db>> { + self.current_assignments.last_mut() + } + + fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> { + match pattern { + ast::Pattern::MatchValue(pattern) => { + let value = self.add_standalone_expression(&pattern.value); + PatternPredicateKind::Value(value) + } + ast::Pattern::MatchSingleton(singleton) => { + PatternPredicateKind::Singleton(singleton.value) + } + ast::Pattern::MatchClass(pattern) => { + let cls = self.add_standalone_expression(&pattern.cls); + PatternPredicateKind::Class(cls) + } + ast::Pattern::MatchOr(pattern) => { + let predicates = pattern + .patterns + .iter() + .map(|pattern| self.predicate_kind(pattern)) + .collect(); + PatternPredicateKind::Or(predicates) + } + _ => PatternPredicateKind::Unsupported, + } + } + + fn add_pattern_narrowing_constraint( + &mut self, + subject: Expression<'db>, + pattern: &ast::Pattern, + guard: Option<&ast::Expr>, + ) -> PredicateOrLiteral<'db> { + // This is called for the top-level pattern of each match arm. We need to create a + // standalone expression for each arm of a match statement, since they can introduce + // constraints on the match subject. (Or more accurately, for the match arm's pattern, + // since its the pattern that introduces any constraints, not the body.) Ideally, that + // standalone expression would wrap the match arm's pattern as a whole. But a standalone + // expression can currently only wrap an ast::Expr, which patterns are not. So, we need to + // choose an Expr that can “stand in” for the pattern, which we can wrap in a standalone + // expression. + // + // See the comment in TypeInferenceBuilder::infer_match_pattern for more details. + + let kind = self.predicate_kind(pattern); + let guard = guard.map(|guard| self.add_standalone_expression(guard)); + + let pattern_predicate = PatternPredicate::new( + self.db, + self.file, + self.current_scope(), + subject, + kind, + guard, + ); + let predicate = PredicateOrLiteral::Predicate(Predicate { + node: PredicateNode::Pattern(pattern_predicate), + is_positive: true, + }); + self.record_narrowing_constraint(predicate); + predicate + } + + /// Record an expression that needs to be a Salsa ingredient, because we need to infer its type + /// standalone (type narrowing tests, RHS of an assignment.) + fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> { + self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal, None) + } + + /// Record an expression that is immediately assigned to a target, and that needs to be a Salsa + /// ingredient, because we need to infer its type standalone (type narrowing tests, RHS of an + /// assignment.) + fn add_standalone_assigned_expression( + &mut self, + expression_node: &ast::Expr, + assigned_to: &ast::StmtAssign, + ) -> Expression<'db> { + self.add_standalone_expression_impl( + expression_node, + ExpressionKind::Normal, + Some(assigned_to), + ) + } + + /// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a + /// *type* expression, which makes sure that it will later be inferred as such. + fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> { + self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression, None) + } + + fn add_standalone_expression_impl( + &mut self, + expression_node: &ast::Expr, + expression_kind: ExpressionKind, + assigned_to: Option<&ast::StmtAssign>, + ) -> Expression<'db> { + let expression = Expression::new( + self.db, + self.file, + self.current_scope(), + AstNodeRef::new(self.module, expression_node), + assigned_to.map(|assigned_to| AstNodeRef::new(self.module, assigned_to)), + expression_kind, + ); + self.expressions_by_node + .insert(expression_node.into(), expression); + expression + } + + fn with_type_params( + &mut self, + with_scope: NodeWithScopeRef, + type_params: Option<&'ast ast::TypeParams>, + nested: impl FnOnce(&mut Self) -> FileScopeId, + ) -> FileScopeId { + if let Some(type_params) = type_params { + self.push_scope(with_scope); + + for type_param in &type_params.type_params { + let (name, bound, default) = match type_param { + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { + range: _, + node_index: _, + name, + bound, + default, + }) => (name, bound, default), + ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { + name, default, .. + }) => (name, &None, default), + ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { + name, + default, + .. + }) => (name, &None, default), + }; + let symbol = self.add_symbol(name.id.clone()); + // TODO create Definition for PEP 695 typevars + // note that the "bound" on the typevar is a totally different thing than whether + // or not a name is "bound" by a typevar declaration; the latter is always true. + self.mark_place_bound(symbol); + self.mark_place_declared(symbol); + if let Some(bounds) = bound { + self.visit_expr(bounds); + } + if let Some(default) = default { + self.visit_expr(default); + } + match type_param { + ast::TypeParam::TypeVar(node) => self.add_definition(symbol, node), + ast::TypeParam::ParamSpec(node) => self.add_definition(symbol, node), + ast::TypeParam::TypeVarTuple(node) => self.add_definition(symbol, node), + }; + } + } + + let nested_scope = nested(self); + + if type_params.is_some() { + self.pop_scope(); + } + + nested_scope + } + + /// This method does several things: + /// - It pushes a new scope onto the stack for visiting + /// a list/dict/set comprehension or generator expression + /// - Inside that scope, it visits a list of [`Comprehension`] nodes, + /// assumed to be the "generators" that compose a comprehension + /// (that is, the `for x in y` and `for y in z` parts of `x for x in y for y in z`). + /// - Inside that scope, it also calls a closure for visiting the outer `elt` + /// of a list/dict/set comprehension or generator expression + /// - It then pops the new scope off the stack + /// + /// [`Comprehension`]: ast::Comprehension + fn with_generators_scope( + &mut self, + scope: NodeWithScopeRef, + generators: &'ast [ast::Comprehension], + visit_outer_elt: impl FnOnce(&mut Self), + ) { + let mut generators_iter = generators.iter(); + + let Some(generator) = generators_iter.next() else { + unreachable!("Expression must contain at least one generator"); + }; + + // The `iter` of the first generator is evaluated in the outer scope, while all subsequent + // nodes are evaluated in the inner scope. + let value = self.add_standalone_expression(&generator.iter); + self.visit_expr(&generator.iter); + self.push_scope(scope); + + self.add_unpackable_assignment( + &Unpackable::Comprehension { + node: generator, + first: true, + }, + &generator.target, + value, + ); + + for if_expr in &generator.ifs { + self.visit_expr(if_expr); + self.record_expression_narrowing_constraint(if_expr); + } + + for generator in generators_iter { + let value = self.add_standalone_expression(&generator.iter); + self.visit_expr(&generator.iter); + + self.add_unpackable_assignment( + &Unpackable::Comprehension { + node: generator, + first: false, + }, + &generator.target, + value, + ); + + for if_expr in &generator.ifs { + self.visit_expr(if_expr); + self.record_expression_narrowing_constraint(if_expr); + } + } + + visit_outer_elt(self); + self.pop_scope(); + } + + fn declare_parameters(&mut self, parameters: &'ast ast::Parameters) { + for parameter in parameters.iter_non_variadic_params() { + self.declare_parameter(parameter); + } + if let Some(vararg) = parameters.vararg.as_ref() { + let symbol = self.add_symbol(vararg.name.id().clone()); + self.add_definition( + symbol, + DefinitionNodeRef::VariadicPositionalParameter(vararg), + ); + } + if let Some(kwarg) = parameters.kwarg.as_ref() { + let symbol = self.add_symbol(kwarg.name.id().clone()); + self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg)); + } + } + + fn declare_parameter(&mut self, parameter: &'ast ast::ParameterWithDefault) { + let symbol = self.add_symbol(parameter.name().id().clone()); + + let definition = self.add_definition(symbol, parameter); + + // 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(), + Definitions::single(definition), + ); + debug_assert_eq!(existing_definition, None); + } + + /// Add an unpackable assignment for the given [`Unpackable`]. + /// + /// This method handles assignments that can contain unpacking like assignment statements, + /// for statements, etc. + fn add_unpackable_assignment( + &mut self, + unpackable: &Unpackable<'ast>, + target: &'ast ast::Expr, + value: Expression<'db>, + ) { + // We only handle assignments to names and unpackings here, other targets like + // attribute and subscript are handled separately as they don't create a new + // definition. + + let current_assignment = match target { + ast::Expr::List(_) | ast::Expr::Tuple(_) => { + if matches!(unpackable, Unpackable::Comprehension { .. }) { + debug_assert_eq!( + self.scopes[self.current_scope()].node().scope_kind(), + ScopeKind::Comprehension + ); + } + // The first iterator of the comprehension is evaluated in the outer scope, while all subsequent + // nodes are evaluated in the inner scope. + // SAFETY: The current scope is the comprehension, and the comprehension scope must have a parent scope. + let value_file_scope = + if let Unpackable::Comprehension { first: true, .. } = unpackable { + self.scope_stack + .iter() + .rev() + .nth(1) + .expect("The comprehension scope must have a parent scope") + .file_scope_id + } else { + self.current_scope() + }; + let unpack = Some(Unpack::new( + self.db, + self.file, + value_file_scope, + self.current_scope(), + // Note `target` belongs to the `self.module` tree + AstNodeRef::new(self.module, target), + UnpackValue::new(unpackable.kind(), value), + )); + Some(unpackable.as_current_assignment(unpack)) + } + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { + Some(unpackable.as_current_assignment(None)) + } + _ => None, + }; + + if let Some(current_assignment) = current_assignment { + self.push_assignment(current_assignment); + } + + self.visit_expr(target); + + if current_assignment.is_some() { + // Only need to pop in the case where we pushed something + self.pop_assignment(); + } + } + + pub(super) fn build(mut self) -> SemanticIndex<'db> { + self.visit_body(self.module.suite()); + + // Pop the root scope + self.pop_scope(); + assert!(self.scope_stack.is_empty()); + + assert_eq!(&self.current_assignments, &[]); + + let mut place_tables: IndexVec<_, _> = self + .place_tables + .into_iter() + .map(|builder| Arc::new(builder.finish())) + .collect(); + + let mut use_def_maps: IndexVec<_, _> = self + .use_def_maps + .into_iter() + .map(|builder| ArcUseDefMap::new(builder.finish())) + .collect(); + + let mut ast_ids: IndexVec<_, _> = self + .ast_ids + .into_iter() + .map(super::ast_ids::AstIdsBuilder::finish) + .collect(); + + self.scopes.shrink_to_fit(); + place_tables.shrink_to_fit(); + use_def_maps.shrink_to_fit(); + ast_ids.shrink_to_fit(); + self.scopes_by_expression.shrink_to_fit(); + self.definitions_by_node.shrink_to_fit(); + + self.scope_ids_by_scope.shrink_to_fit(); + self.scopes_by_node.shrink_to_fit(); + self.generator_functions.shrink_to_fit(); + self.eager_snapshots.shrink_to_fit(); + self.globals_by_scope.shrink_to_fit(); + + SemanticIndex { + place_tables, + scopes: self.scopes, + definitions_by_node: self.definitions_by_node, + expressions_by_node: self.expressions_by_node, + scope_ids_by_scope: self.scope_ids_by_scope, + globals_by_scope: self.globals_by_scope, + ast_ids, + scopes_by_expression: self.scopes_by_expression, + scopes_by_node: self.scopes_by_node, + use_def_maps, + imported_modules: Arc::new(self.imported_modules), + has_future_annotations: self.has_future_annotations, + eager_snapshots: self.eager_snapshots, + semantic_syntax_errors: self.semantic_syntax_errors.into_inner(), + generator_functions: self.generator_functions, + } + } + + fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Self)) { + let mut checker = std::mem::take(&mut self.semantic_checker); + f(&mut checker, self); + self.semantic_checker = checker; + } + + fn source_text(&self) -> &SourceText { + self.source_text + .get_or_init(|| source_text(self.db, self.file)) + } +} + +impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { + fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) { + self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context)); + + match stmt { + ast::Stmt::FunctionDef(function_def) => { + let ast::StmtFunctionDef { + decorator_list, + parameters, + type_params, + name, + returns, + body, + is_async: _, + range: _, + node_index: _, + } = function_def; + for decorator in decorator_list { + self.visit_decorator(decorator); + } + + self.with_type_params( + NodeWithScopeRef::FunctionTypeParameters(function_def), + type_params.as_deref(), + |builder| { + builder.visit_parameters(parameters); + if let Some(returns) = returns { + builder.visit_annotation(returns); + } + + builder.push_scope(NodeWithScopeRef::Function(function_def)); + + builder.declare_parameters(parameters); + + let mut first_parameter_name = parameters + .iter_non_variadic_params() + .next() + .map(|first_param| first_param.parameter.name.id().as_str()); + std::mem::swap( + &mut builder.current_first_parameter_name, + &mut first_parameter_name, + ); + + builder.visit_body(body); + + builder.current_first_parameter_name = first_parameter_name; + builder.pop_scope() + }, + ); + // The default value of the parameters needs to be evaluated in the + // enclosing scope. + for default in parameters + .iter_non_variadic_params() + .filter_map(|param| param.default.as_deref()) + { + self.visit_expr(default); + } + // The symbol for the function name itself has to be evaluated + // at the end to match the runtime evaluation of parameter defaults + // and return-type annotations. + let symbol = self.add_symbol(name.id.clone()); + + // Record a use of the function name in the scope that it is defined in, so that it + // can be used to find previously defined functions with the same name. This is + // used to collect all the overloaded definitions of a function. This needs to be + // done on the `Identifier` node as opposed to `ExprName` because that's what the + // AST uses. + self.mark_place_used(symbol); + let use_id = self.current_ast_ids().record_use(name); + self.current_use_def_map_mut() + .record_use(symbol, use_id, NodeKey::from_node(name)); + + self.add_definition(symbol, function_def); + } + ast::Stmt::ClassDef(class) => { + for decorator in &class.decorator_list { + self.visit_decorator(decorator); + } + + self.with_type_params( + NodeWithScopeRef::ClassTypeParameters(class), + class.type_params.as_deref(), + |builder| { + if let Some(arguments) = &class.arguments { + builder.visit_arguments(arguments); + } + + builder.push_scope(NodeWithScopeRef::Class(class)); + builder.visit_body(&class.body); + + 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( + type_alias + .name + .as_name_expr() + .map(|name| name.id.clone()) + .unwrap_or("".into()), + ); + self.add_definition(symbol, type_alias); + self.visit_expr(&type_alias.name); + + self.with_type_params( + NodeWithScopeRef::TypeAliasTypeParameters(type_alias), + type_alias.type_params.as_deref(), + |builder| { + builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias)); + builder.visit_expr(&type_alias.value); + builder.pop_scope() + }, + ); + } + ast::Stmt::Import(node) => { + self.current_use_def_map_mut() + .record_node_reachability(NodeKey::from_node(node)); + + for (alias_index, alias) in node.names.iter().enumerate() { + // Mark the imported module, and all of its parents, as being imported in this + // file. + if let Some(module_name) = ModuleName::new(&alias.name) { + self.imported_modules.extend(module_name.ancestors()); + } + + let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname { + (asname.id.clone(), asname.id == alias.name.id) + } else { + (Name::new(alias.name.id.split('.').next().unwrap()), false) + }; + + let symbol = self.add_symbol(symbol_name); + self.add_definition( + symbol, + ImportDefinitionNodeRef { + node, + alias_index, + is_reexported, + }, + ); + } + } + ast::Stmt::ImportFrom(node) => { + self.current_use_def_map_mut() + .record_node_reachability(NodeKey::from_node(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` 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.in_module_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; + }; + + let Some(referenced_module) = module.file() else { + continue; + }; + + // In order to understand the reachability of definitions created by a `*` import, + // we need to know the reachability of the global-scope definitions in the + // `referenced_module` the symbols imported from. Much like predicates for `if` + // statements can only have their reachability constraints resolved at type-inference + // time, the reachability of these global-scope definitions in the external module + // cannot be resolved at this point. As such, we essentially model each definition + // stemming from a `from exporter *` import as something like: + // + // ```py + // if : + // from exporter import name + // ``` + // + // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. + for export in exported_names(self.db, referenced_module) { + let symbol_id = self.add_symbol(export.clone()); + let node_ref = StarImportDefinitionNodeRef { + node, + place_id: symbol_id, + }; + let star_import = StarImportPlaceholderPredicate::new( + self.db, + self.file, + symbol_id, + referenced_module, + ); + + let star_import_predicate = self.add_predicate(star_import.into()); + + let pre_definition = + self.current_use_def_map().single_place_snapshot(symbol_id); + let pre_definition_reachability = + self.current_use_def_map().reachability; + + // Temporarily modify the reachability to include the star import predicate, + // in order for the new definition to pick it up. + let reachability_constraints = + &mut self.current_use_def_map_mut().reachability_constraints; + let star_import_reachability = + reachability_constraints.add_atom(star_import_predicate); + let definition_reachability = reachability_constraints + .add_and_constraint( + pre_definition_reachability, + star_import_reachability, + ); + self.current_use_def_map_mut().reachability = definition_reachability; + + self.push_additional_definition(symbol_id, node_ref); + + self.current_use_def_map_mut() + .record_and_negate_star_import_reachability_constraint( + star_import_reachability, + symbol_id, + pre_definition, + ); + + // Restore the reachability to its pre-definition state + self.current_use_def_map_mut().reachability = + pre_definition_reachability; + } + + continue; + } + + let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname { + (&asname.id, asname.id == alias.name.id) + } else { + (&alias.name.id, false) + }; + + // Look for imports `from __future__ import annotations`, ignore `as ...` + // We intentionally don't enforce the rules about location of `__future__` + // imports here, we assume the user's intent was to apply the `__future__` + // import, so we still check using it (and will also emit a diagnostic about a + // miss-placed `__future__` import.) + self.has_future_annotations |= alias.name.id == "annotations" + && node.module.as_deref() == Some("__future__"); + + let symbol = self.add_symbol(symbol_name.clone()); + + self.add_definition( + symbol, + ImportFromDefinitionNodeRef { + node, + alias_index, + is_reexported, + }, + ); + } + } + + ast::Stmt::Assert(ast::StmtAssert { + test, + msg, + range: _, + node_index: _, + }) => { + // We model an `assert test, msg` statement here. Conceptually, we can think of + // this as being equivalent to the following: + // + // ```py + // if not test: + // msg + // + // + // + // ``` + // + // Importantly, the `msg` expression is only evaluated if the `test` expression is + // falsy. This is why we apply the negated `test` predicate as a narrowing and + // reachability constraint on the `msg` expression. + // + // The other important part is the ``. This lets us skip the usual merging of + // flow states and simplification of reachability constraints, since there is no way + // of getting out of that `msg` branch. We simply restore to the post-test state. + + self.visit_expr(test); + let predicate = self.build_predicate(test); + + if let Some(msg) = msg { + let post_test = self.flow_snapshot(); + let negated_predicate = predicate.negated(); + self.record_narrowing_constraint(negated_predicate); + self.record_reachability_constraint(negated_predicate); + self.visit_expr(msg); + self.flow_restore(post_test); + } + + self.record_narrowing_constraint(predicate); + self.record_reachability_constraint(predicate); + } + + ast::Stmt::Assign(node) => { + debug_assert_eq!(&self.current_assignments, &[]); + + self.visit_expr(&node.value); + let value = self.add_standalone_assigned_expression(&node.value, node); + + for target in &node.targets { + self.add_unpackable_assignment(&Unpackable::Assign(node), target, value); + } + } + ast::Stmt::AnnAssign(node) => { + debug_assert_eq!(&self.current_assignments, &[]); + self.visit_expr(&node.annotation); + if let Some(value) = &node.value { + self.visit_expr(value); + } + + // See https://docs.python.org/3/library/ast.html#ast.AnnAssign + if matches!( + *node.target, + ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_) + ) { + self.push_assignment(node.into()); + self.visit_expr(&node.target); + self.pop_assignment(); + } else { + self.visit_expr(&node.target); + } + } + ast::Stmt::AugAssign( + aug_assign @ ast::StmtAugAssign { + range: _, + node_index: _, + target, + op, + value, + }, + ) => { + debug_assert_eq!(&self.current_assignments, &[]); + self.visit_expr(value); + + match &**target { + ast::Expr::Name(ast::ExprName { id, .. }) + if id == "__all__" && op.is_add() && self.in_module_scope() => + { + if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = + &**value + { + if attr == "__all__" { + self.add_standalone_expression(value); + } + } + + self.push_assignment(aug_assign.into()); + self.visit_expr(target); + self.pop_assignment(); + } + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { + self.push_assignment(aug_assign.into()); + self.visit_expr(target); + self.pop_assignment(); + } + _ => { + self.visit_expr(target); + } + } + } + ast::Stmt::If(node) => { + self.visit_expr(&node.test); + let mut no_branch_taken = self.flow_snapshot(); + let mut last_predicate = self.record_expression_narrowing_constraint(&node.test); + let mut last_reachability_constraint = + self.record_reachability_constraint(last_predicate); + self.visit_body(&node.body); + + let mut post_clauses: Vec = vec![]; + let elif_else_clauses = node + .elif_else_clauses + .iter() + .map(|clause| (clause.test.as_ref(), clause.body.as_slice())); + let has_else = node + .elif_else_clauses + .last() + .is_some_and(|clause| clause.test.is_none()); + let elif_else_clauses = elif_else_clauses.chain(if has_else { + // if there's an `else` clause already, we don't need to add another + None + } else { + // if there's no `else` branch, we should add a no-op `else` branch + Some((None, Default::default())) + }); + for (clause_test, clause_body) in elif_else_clauses { + // snapshot after every block except the last; the last one will just become + // the state that we merge the other snapshots into + post_clauses.push(self.flow_snapshot()); + // we can only take an elif/else branch if none of the previous ones were + // taken + self.flow_restore(no_branch_taken.clone()); + + self.record_negated_narrowing_constraint(last_predicate); + self.record_negated_reachability_constraint(last_reachability_constraint); + + if let Some(elif_test) = clause_test { + self.visit_expr(elif_test); + // A test expression is evaluated whether the branch is taken or not + no_branch_taken = self.flow_snapshot(); + + last_predicate = self.record_expression_narrowing_constraint(elif_test); + + last_reachability_constraint = + self.record_reachability_constraint(last_predicate); + } + + self.visit_body(clause_body); + } + + for post_clause_state in post_clauses { + self.flow_merge(post_clause_state); + } + } + ast::Stmt::While(ast::StmtWhile { + test, + body, + orelse, + range: _, + node_index: _, + }) => { + self.visit_expr(test); + + let pre_loop = self.flow_snapshot(); + let predicate = self.record_expression_narrowing_constraint(test); + self.record_reachability_constraint(predicate); + + let outer_loop = self.push_loop(); + self.visit_body(body); + let this_loop = self.pop_loop(outer_loop); + + // We execute the `else` branch once the condition evaluates to false. This could + // happen without ever executing the body, if the condition is false the first time + // it's tested. Or it could happen if a _later_ evaluation of the condition yields + // false. So we merge in the pre-loop state here into the post-body state: + + self.flow_merge(pre_loop); + + // The `else` branch can only be reached if the loop condition *can* be false. To + // model this correctly, we need a second copy of the while condition constraint, + // since the first and later evaluations might produce different results. We would + // otherwise simplify `predicate AND ~predicate` to `False`. + let later_predicate_id = self.current_use_def_map_mut().add_predicate(predicate); + let later_reachability_constraint = self + .current_reachability_constraints_mut() + .add_atom(later_predicate_id); + self.record_negated_reachability_constraint(later_reachability_constraint); + + self.record_negated_narrowing_constraint(predicate); + + self.visit_body(orelse); + + // Breaking out of a while loop bypasses the `else` clause, so merge in the break + // states after visiting `else`. + for break_state in this_loop.break_states { + self.flow_merge(break_state); + } + } + ast::Stmt::With(ast::StmtWith { + items, + body, + is_async, + .. + }) => { + for item @ ast::WithItem { + range: _, + node_index: _, + context_expr, + optional_vars, + } in items + { + self.visit_expr(context_expr); + if let Some(optional_vars) = optional_vars.as_deref() { + let context_manager = self.add_standalone_expression(context_expr); + self.add_unpackable_assignment( + &Unpackable::WithItem { + item, + is_async: *is_async, + }, + optional_vars, + context_manager, + ); + } + } + self.visit_body(body); + } + + ast::Stmt::For( + for_stmt @ ast::StmtFor { + range: _, + node_index: _, + is_async: _, + target, + iter, + body, + orelse, + }, + ) => { + debug_assert_eq!(&self.current_assignments, &[]); + + let iter_expr = self.add_standalone_expression(iter); + self.visit_expr(iter); + + self.record_ambiguous_reachability(); + + let pre_loop = self.flow_snapshot(); + + self.add_unpackable_assignment(&Unpackable::For(for_stmt), target, iter_expr); + + let outer_loop = self.push_loop(); + self.visit_body(body); + let this_loop = self.pop_loop(outer_loop); + + // We may execute the `else` clause without ever executing the body, so merge in + // the pre-loop state before visiting `else`. + self.flow_merge(pre_loop); + self.visit_body(orelse); + + // Breaking out of a `for` loop bypasses the `else` clause, so merge in the break + // states after visiting `else`. + for break_state in this_loop.break_states { + self.flow_merge(break_state); + } + } + ast::Stmt::Match(ast::StmtMatch { + subject, + cases, + range: _, + node_index: _, + }) => { + debug_assert_eq!(self.current_match_case, None); + + let subject_expr = self.add_standalone_expression(subject); + self.visit_expr(subject); + if cases.is_empty() { + return; + } + + let mut no_case_matched = self.flow_snapshot(); + + let has_catchall = cases + .last() + .is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard()); + + let mut post_case_snapshots = vec![]; + let mut match_predicate; + + for (i, case) in cases.iter().enumerate() { + self.current_match_case = Some(CurrentMatchCase::new(&case.pattern)); + self.visit_pattern(&case.pattern); + self.current_match_case = None; + // unlike in [Stmt::If], we don't reset [no_case_matched] + // here because the effects of visiting a pattern is binding + // symbols, and this doesn't occur unless the pattern + // actually matches + match_predicate = self.add_pattern_narrowing_constraint( + subject_expr, + &case.pattern, + case.guard.as_deref(), + ); + let reachability_constraint = + self.record_reachability_constraint(match_predicate); + + let match_success_guard_failure = case.guard.as_ref().map(|guard| { + let guard_expr = self.add_standalone_expression(guard); + // We could also add the guard expression as a reachability constraint, but + // it seems unlikely that both the case predicate as well as the guard are + // statically known conditions, so we currently don't model that. + self.record_ambiguous_reachability(); + self.visit_expr(guard); + let post_guard_eval = self.flow_snapshot(); + let predicate = PredicateOrLiteral::Predicate(Predicate { + node: PredicateNode::Expression(guard_expr), + is_positive: true, + }); + self.record_negated_narrowing_constraint(predicate); + let match_success_guard_failure = self.flow_snapshot(); + self.flow_restore(post_guard_eval); + self.record_narrowing_constraint(predicate); + match_success_guard_failure + }); + + self.visit_body(&case.body); + + post_case_snapshots.push(self.flow_snapshot()); + + if i != cases.len() - 1 || !has_catchall { + // We need to restore the state after each case, but not after the last + // one. The last one will just become the state that we merge the other + // snapshots into. + self.flow_restore(no_case_matched.clone()); + self.record_negated_narrowing_constraint(match_predicate); + self.record_negated_reachability_constraint(reachability_constraint); + if let Some(match_success_guard_failure) = match_success_guard_failure { + self.flow_merge(match_success_guard_failure); + } else { + assert!(case.guard.is_none()); + } + } else { + debug_assert!(match_success_guard_failure.is_none()); + debug_assert!(case.guard.is_none()); + } + + no_case_matched = self.flow_snapshot(); + } + + for post_clause_state in post_case_snapshots { + self.flow_merge(post_clause_state); + } + } + ast::Stmt::Try(ast::StmtTry { + body, + handlers, + orelse, + finalbody, + is_star, + range: _, + node_index: _, + }) => { + self.record_ambiguous_reachability(); + + // Save the state prior to visiting any of the `try` block. + // + // Potentially none of the `try` block could have been executed prior to executing + // the `except` block(s) and/or the `finally` block. + // We will merge this state with all of the intermediate + // states during the `try` block before visiting those suites. + let pre_try_block_state = self.flow_snapshot(); + + self.try_node_context_stack_manager.push_context(); + + // Visit the `try` block! + self.visit_body(body); + + let mut post_except_states = vec![]; + + // Take a record also of all the intermediate states we encountered + // while visiting the `try` block + let try_block_snapshots = self.try_node_context_stack_manager.pop_context(); + + if !handlers.is_empty() { + // Save the state immediately *after* visiting the `try` block + // but *before* we prepare for visiting the `except` block(s). + // + // We will revert to this state prior to visiting the `else` block, + // as there necessarily must have been 0 `except` blocks executed + // if we hit the `else` block. + let post_try_block_state = self.flow_snapshot(); + + // Prepare for visiting the `except` block(s) + self.flow_restore(pre_try_block_state); + for state in try_block_snapshots { + self.flow_merge(state); + } + + let pre_except_state = self.flow_snapshot(); + let num_handlers = handlers.len(); + + for (i, except_handler) in handlers.iter().enumerate() { + let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler; + let ast::ExceptHandlerExceptHandler { + name: symbol_name, + type_: handled_exceptions, + body: handler_body, + range: _, + node_index: _, + } = except_handler; + + if let Some(handled_exceptions) = handled_exceptions { + self.visit_expr(handled_exceptions); + } + + // If `handled_exceptions` above was `None`, it's something like `except as e:`, + // which is invalid syntax. However, it's still pretty obvious here that the user + // *wanted* `e` to be bound, so we should still create a definition here nonetheless. + let symbol = if let Some(symbol_name) = symbol_name { + let symbol = self.add_symbol(symbol_name.id.clone()); + + self.add_definition( + symbol, + DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef { + handler: except_handler, + is_star: *is_star, + }), + ); + Some(symbol) + } else { + None + }; + + self.visit_body(handler_body); + // The caught exception is cleared at the end of the except clause + if let Some(symbol) = symbol { + self.delete_binding(symbol); + } + // Each `except` block is mutually exclusive with all other `except` blocks. + post_except_states.push(self.flow_snapshot()); + + // It's unnecessary to do the `self.flow_restore()` call for the final except handler, + // as we'll immediately call `self.flow_restore()` to a different state + // as soon as this loop over the handlers terminates. + if i < (num_handlers - 1) { + self.flow_restore(pre_except_state.clone()); + } + } + + // If we get to the `else` block, we know that 0 of the `except` blocks can have been executed, + // and the entire `try` block must have been executed: + self.flow_restore(post_try_block_state); + } + + self.visit_body(orelse); + + for post_except_state in post_except_states { + self.flow_merge(post_except_state); + } + + // TODO: there's lots of complexity here that isn't yet handled by our model. + // In order to accurately model the semantics of `finally` suites, we in fact need to visit + // the suite twice: once under the (current) assumption that either the `try + else` suite + // ran to completion or exactly one `except` branch ran to completion, and then again under + // the assumption that potentially none of the branches ran to completion and we in fact + // jumped from a `try`, `else` or `except` branch straight into the `finally` branch. + // This requires rethinking some fundamental assumptions semantic indexing makes. + // For more details, see: + // - https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d + // - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702 + self.visit_body(finalbody); + } + + ast::Stmt::Raise(_) | ast::Stmt::Return(_) | ast::Stmt::Continue(_) => { + walk_stmt(self, stmt); + // Everything in the current block after a terminal statement is unreachable. + self.mark_unreachable(); + } + + ast::Stmt::Break(_) => { + let snapshot = self.flow_snapshot(); + if let Some(current_loop) = self.current_loop_mut() { + current_loop.push_break(snapshot); + } + // Everything in the current block after a terminal statement is unreachable. + self.mark_unreachable(); + } + ast::Stmt::Global(ast::StmtGlobal { + range: _, + node_index: _, + names, + }) => { + for name in names { + let symbol_id = self.add_symbol(name.id.clone()); + let symbol_table = self.current_place_table(); + let symbol = symbol_table.place_expr(symbol_id); + if symbol.is_bound() || symbol.is_declared() || symbol.is_used() { + self.report_semantic_error(SemanticSyntaxError { + kind: SemanticSyntaxErrorKind::LoadBeforeGlobalDeclaration { + name: name.to_string(), + start: name.range.start(), + }, + range: name.range, + python_version: self.python_version, + }); + } + let scope_id = self.current_scope(); + self.globals_by_scope + .entry(scope_id) + .or_default() + .insert(symbol_id); + } + walk_stmt(self, stmt); + } + ast::Stmt::Delete(ast::StmtDelete { + targets, + range: _, + node_index: _, + }) => { + // We will check the target expressions and then delete them. + walk_stmt(self, stmt); + for target in targets { + if let Ok(target) = PlaceExpr::try_from(target) { + let place_id = self.add_place(PlaceExprWithFlags::new(target)); + self.current_place_table().mark_place_used(place_id); + self.delete_binding(place_id); + } + } + } + ast::Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) => { + if self.in_module_scope() { + if let Some(expr) = dunder_all_extend_argument(value) { + self.add_standalone_expression(expr); + } + } + + self.visit_expr(value); + + // If the statement is a call, it could possibly be a call to a function + // marked with `NoReturn` (for example, `sys.exit()`). In this case, we use a special + // kind of constraint to mark the following code as unreachable. + // + // Ideally, these constraints should be added for every call expression, even those in + // sub-expressions and in the module-level scope. But doing so makes the number of + // such constraints so high that it significantly degrades performance. We thus cut + // scope here and add these constraints only at statement level function calls, + // like `sys.exit()`, and not within sub-expression like `3 + sys.exit()` etc. + // + // We also only add these inside function scopes, since considering module-level + // constraints can affect the the type of imported symbols, leading to a lot more + // work in third-party code. + if let ast::Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { + if !self.source_type.is_stub() && self.in_function_scope() { + let callable = self.add_standalone_expression(func); + let call_expr = self.add_standalone_expression(value.as_ref()); + + let predicate = Predicate { + node: PredicateNode::ReturnsNever(CallableAndCallExpr { + callable, + call_expr, + }), + is_positive: false, + }; + self.record_reachability_constraint(PredicateOrLiteral::Predicate( + predicate, + )); + } + } + } + _ => { + walk_stmt(self, stmt); + } + } + } + + fn visit_expr(&mut self, expr: &'ast ast::Expr) { + self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context)); + + self.scopes_by_expression + .insert(expr.into(), self.current_scope()); + + let node_key = NodeKey::from_node(expr); + + match expr { + ast::Expr::Name(ast::ExprName { ctx, .. }) + | ast::Expr::Attribute(ast::ExprAttribute { ctx, .. }) + | ast::Expr::Subscript(ast::ExprSubscript { ctx, .. }) => { + if let Ok(place_expr) = PlaceExpr::try_from(expr) { + let mut place_expr = PlaceExprWithFlags::new(place_expr); + if self.is_method_of_class().is_some() + && place_expr.is_instance_attribute_candidate() + { + // We specifically mark attribute assignments to the first parameter of a method, + // i.e. typically `self` or `cls`. + let accessed_object_refers_to_first_parameter = self + .current_first_parameter_name + .is_some_and(|fst| place_expr.expr.root_name() == fst); + + if accessed_object_refers_to_first_parameter && place_expr.is_member() { + place_expr.mark_instance_attribute(); + } + } + + let (is_use, is_definition) = match (ctx, self.current_assignment()) { + (ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => { + // For augmented assignment, the target expression is also used. + (true, true) + } + (ast::ExprContext::Load, _) => (true, false), + (ast::ExprContext::Store, _) => (false, true), + (ast::ExprContext::Del, _) => (true, true), + (ast::ExprContext::Invalid, _) => (false, false), + }; + let place_id = self.add_place(place_expr); + + if is_use { + self.mark_place_used(place_id); + let use_id = self.current_ast_ids().record_use(expr); + self.current_use_def_map_mut() + .record_use(place_id, use_id, node_key); + } + + if is_definition { + match self.current_assignment() { + Some(CurrentAssignment::Assign { node, unpack }) => { + self.add_definition( + place_id, + AssignmentDefinitionNodeRef { + unpack, + value: &node.value, + target: expr, + }, + ); + } + Some(CurrentAssignment::AnnAssign(ann_assign)) => { + self.add_standalone_type_expression(&ann_assign.annotation); + self.add_definition( + place_id, + AnnotatedAssignmentDefinitionNodeRef { + node: ann_assign, + annotation: &ann_assign.annotation, + value: ann_assign.value.as_deref(), + target: expr, + }, + ); + } + Some(CurrentAssignment::AugAssign(aug_assign)) => { + self.add_definition(place_id, aug_assign); + } + Some(CurrentAssignment::For { node, unpack }) => { + self.add_definition( + place_id, + ForStmtDefinitionNodeRef { + unpack, + iterable: &node.iter, + target: expr, + is_async: node.is_async, + }, + ); + } + Some(CurrentAssignment::Named(named)) => { + // TODO(dhruvmanila): If the current scope is a comprehension, then the + // named expression is implicitly nonlocal. This is yet to be + // implemented. + self.add_definition(place_id, named); + } + Some(CurrentAssignment::Comprehension { + unpack, + node, + first, + }) => { + self.add_definition( + place_id, + ComprehensionDefinitionNodeRef { + unpack, + iterable: &node.iter, + target: expr, + first, + is_async: node.is_async, + }, + ); + } + Some(CurrentAssignment::WithItem { + item, + is_async, + unpack, + }) => { + self.add_definition( + place_id, + WithItemDefinitionNodeRef { + unpack, + context_expr: &item.context_expr, + target: expr, + is_async, + }, + ); + } + None => {} + } + } + + if let Some(unpack_position) = self + .current_assignment_mut() + .and_then(CurrentAssignment::unpack_position_mut) + { + *unpack_position = UnpackPosition::Other; + } + } + + // Track reachability of attribute expressions to silence `unresolved-attribute` + // diagnostics in unreachable code. + if expr.is_attribute_expr() { + self.current_use_def_map_mut() + .record_node_reachability(node_key); + } + + walk_expr(self, expr); + } + ast::Expr::Named(node) => { + // TODO walrus in comprehensions is implicitly nonlocal + self.visit_expr(&node.value); + + // See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements + if node.target.is_name_expr() { + self.push_assignment(node.into()); + self.visit_expr(&node.target); + self.pop_assignment(); + } else { + self.visit_expr(&node.target); + } + } + ast::Expr::Lambda(lambda) => { + if let Some(parameters) = &lambda.parameters { + // The default value of the parameters needs to be evaluated in the + // enclosing scope. + for default in parameters + .iter_non_variadic_params() + .filter_map(|param| param.default.as_deref()) + { + self.visit_expr(default); + } + self.visit_parameters(parameters); + } + self.push_scope(NodeWithScopeRef::Lambda(lambda)); + + // Add symbols and definitions for the parameters to the lambda scope. + if let Some(parameters) = lambda.parameters.as_ref() { + self.declare_parameters(parameters); + } + + self.visit_expr(lambda.body.as_ref()); + self.pop_scope(); + } + ast::Expr::If(ast::ExprIf { + body, test, orelse, .. + }) => { + self.visit_expr(test); + let pre_if = self.flow_snapshot(); + let predicate = self.record_expression_narrowing_constraint(test); + let reachability_constraint = self.record_reachability_constraint(predicate); + self.visit_expr(body); + let post_body = self.flow_snapshot(); + self.flow_restore(pre_if); + + self.record_negated_narrowing_constraint(predicate); + self.record_negated_reachability_constraint(reachability_constraint); + self.visit_expr(orelse); + self.flow_merge(post_body); + } + ast::Expr::ListComp( + list_comprehension @ ast::ExprListComp { + elt, generators, .. + }, + ) => { + self.with_generators_scope( + NodeWithScopeRef::ListComprehension(list_comprehension), + generators, + |builder| builder.visit_expr(elt), + ); + } + ast::Expr::SetComp( + set_comprehension @ ast::ExprSetComp { + elt, generators, .. + }, + ) => { + self.with_generators_scope( + NodeWithScopeRef::SetComprehension(set_comprehension), + generators, + |builder| builder.visit_expr(elt), + ); + } + ast::Expr::Generator( + generator @ ast::ExprGenerator { + elt, generators, .. + }, + ) => { + self.with_generators_scope( + NodeWithScopeRef::GeneratorExpression(generator), + generators, + |builder| builder.visit_expr(elt), + ); + } + ast::Expr::DictComp( + dict_comprehension @ ast::ExprDictComp { + key, + value, + generators, + .. + }, + ) => { + self.with_generators_scope( + NodeWithScopeRef::DictComprehension(dict_comprehension), + generators, + |builder| { + builder.visit_expr(key); + builder.visit_expr(value); + }, + ); + } + ast::Expr::BoolOp(ast::ExprBoolOp { + values, + range: _, + node_index: _, + op, + }) => { + let mut snapshots = vec![]; + let mut reachability_constraints = vec![]; + + for (index, value) in values.iter().enumerate() { + for id in &reachability_constraints { + self.current_use_def_map_mut() + .record_reachability_constraint(*id); // TODO: nicer API + } + + self.visit_expr(value); + + // For the last value, we don't need to model control flow. There is no short-circuiting + // anymore. + if index < values.len() - 1 { + let predicate = self.build_predicate(value); + let predicate_id = match op { + ast::BoolOp::And => self.add_predicate(predicate), + ast::BoolOp::Or => self.add_negated_predicate(predicate), + }; + let reachability_constraint = self + .current_reachability_constraints_mut() + .add_atom(predicate_id); + + let after_expr = self.flow_snapshot(); + + // We first model the short-circuiting behavior. We take the short-circuit + // path here if all of the previous short-circuit paths were not taken, so + // we record all previously existing reachability constraints, and negate the + // one for the current expression. + + self.record_negated_reachability_constraint(reachability_constraint); + snapshots.push(self.flow_snapshot()); + + // Then we model the non-short-circuiting behavior. Here, we need to delay + // the application of the reachability constraint until after the expression + // has been evaluated, so we only push it onto the stack here. + self.flow_restore(after_expr); + self.record_narrowing_constraint_id(predicate_id); + reachability_constraints.push(reachability_constraint); + } + } + + for snapshot in snapshots { + self.flow_merge(snapshot); + } + } + ast::Expr::StringLiteral(_) => { + // Track reachability of string literals, as they could be a stringified annotation + // with child expressions whose reachability we are interested in. + self.current_use_def_map_mut() + .record_node_reachability(node_key); + + walk_expr(self, expr); + } + ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => { + let scope = self.current_scope(); + if self.scopes[scope].kind() == ScopeKind::Function { + self.generator_functions.insert(scope); + } + walk_expr(self, expr); + } + _ => { + walk_expr(self, expr); + } + } + } + + fn visit_parameters(&mut self, parameters: &'ast ast::Parameters) { + // Intentionally avoid walking default expressions, as we handle them in the enclosing + // scope. + for parameter in parameters.iter().map(ast::AnyParameterRef::as_parameter) { + self.visit_parameter(parameter); + } + } + + fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) { + if let ast::Pattern::MatchStar(ast::PatternMatchStar { + name: Some(name), + range: _, + node_index: _, + }) = pattern + { + let symbol = self.add_symbol(name.id().clone()); + let state = self.current_match_case.as_ref().unwrap(); + self.add_definition( + symbol, + MatchPatternDefinitionNodeRef { + pattern: state.pattern, + identifier: name, + index: state.index, + }, + ); + } + + walk_pattern(self, pattern); + + if let ast::Pattern::MatchAs(ast::PatternMatchAs { + name: Some(name), .. + }) + | ast::Pattern::MatchMapping(ast::PatternMatchMapping { + rest: Some(name), .. + }) = pattern + { + let symbol = self.add_symbol(name.id().clone()); + let state = self.current_match_case.as_ref().unwrap(); + self.add_definition( + symbol, + MatchPatternDefinitionNodeRef { + pattern: state.pattern, + identifier: name, + index: state.index, + }, + ); + } + + self.current_match_case.as_mut().unwrap().index += 1; + } +} + +impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { + fn future_annotations_or_stub(&self) -> bool { + self.has_future_annotations + } + + fn python_version(&self) -> PythonVersion { + self.python_version + } + + fn source(&self) -> &str { + self.source_text().as_str() + } + + // We handle the one syntax error that relies on this method (`LoadBeforeGlobalDeclaration`) + // directly in `visit_stmt`, so this just returns a placeholder value. + fn global(&self, _name: &str) -> Option { + None + } + + fn in_async_context(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + match scope.kind() { + ScopeKind::Class | ScopeKind::Lambda => return false, + ScopeKind::Function => { + return scope.node().expect_function(self.module).is_async; + } + ScopeKind::Comprehension + | ScopeKind::Module + | ScopeKind::TypeAlias + | ScopeKind::Annotation => {} + } + } + false + } + + fn in_await_allowed_context(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + match scope.kind() { + ScopeKind::Class => return false, + ScopeKind::Function | ScopeKind::Lambda => return true, + ScopeKind::Comprehension + | ScopeKind::Module + | ScopeKind::TypeAlias + | ScopeKind::Annotation => {} + } + } + false + } + + fn in_yield_allowed_context(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + match scope.kind() { + ScopeKind::Class | ScopeKind::Comprehension => return false, + ScopeKind::Function | ScopeKind::Lambda => return true, + ScopeKind::Module | ScopeKind::TypeAlias | ScopeKind::Annotation => {} + } + } + false + } + + fn in_sync_comprehension(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + let generators = match scope.node() { + NodeWithScopeKind::ListComprehension(node) => &node.node(self.module).generators, + NodeWithScopeKind::SetComprehension(node) => &node.node(self.module).generators, + NodeWithScopeKind::DictComprehension(node) => &node.node(self.module).generators, + _ => continue, + }; + if generators + .iter() + .all(|comprehension| !comprehension.is_async) + { + return true; + } + } + false + } + + fn in_module_scope(&self) -> bool { + self.scope_stack.len() == 1 + } + + fn in_function_scope(&self) -> bool { + let kind = self.scopes[self.current_scope()].kind(); + matches!(kind, ScopeKind::Function | ScopeKind::Lambda) + } + + fn in_generator_scope(&self) -> bool { + matches!( + self.scopes[self.current_scope()].node(), + NodeWithScopeKind::GeneratorExpression(_) + ) + } + + fn in_notebook(&self) -> bool { + self.source_text().is_notebook() + } + + fn report_semantic_error(&self, error: SemanticSyntaxError) { + if self.db.is_file_open(self.file) { + self.semantic_syntax_errors.borrow_mut().push(error); + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum CurrentAssignment<'ast, 'db> { + Assign { + node: &'ast ast::StmtAssign, + unpack: Option<(UnpackPosition, Unpack<'db>)>, + }, + AnnAssign(&'ast ast::StmtAnnAssign), + AugAssign(&'ast ast::StmtAugAssign), + For { + node: &'ast ast::StmtFor, + unpack: Option<(UnpackPosition, Unpack<'db>)>, + }, + Named(&'ast ast::ExprNamed), + Comprehension { + node: &'ast ast::Comprehension, + first: bool, + unpack: Option<(UnpackPosition, Unpack<'db>)>, + }, + WithItem { + item: &'ast ast::WithItem, + is_async: bool, + unpack: Option<(UnpackPosition, Unpack<'db>)>, + }, +} + +impl CurrentAssignment<'_, '_> { + fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> { + match self { + Self::Assign { unpack, .. } + | Self::For { unpack, .. } + | Self::WithItem { unpack, .. } + | Self::Comprehension { unpack, .. } => unpack.as_mut().map(|(position, _)| position), + Self::AnnAssign(_) | Self::AugAssign(_) | Self::Named(_) => None, + } + } +} + +impl<'ast> From<&'ast ast::StmtAnnAssign> for CurrentAssignment<'ast, '_> { + fn from(value: &'ast ast::StmtAnnAssign) -> Self { + Self::AnnAssign(value) + } +} + +impl<'ast> From<&'ast ast::StmtAugAssign> for CurrentAssignment<'ast, '_> { + fn from(value: &'ast ast::StmtAugAssign) -> Self { + Self::AugAssign(value) + } +} + +impl<'ast> From<&'ast ast::ExprNamed> for CurrentAssignment<'ast, '_> { + fn from(value: &'ast ast::ExprNamed) -> Self { + Self::Named(value) + } +} + +#[derive(Debug, PartialEq)] +struct CurrentMatchCase<'ast> { + /// The pattern that's part of the current match case. + pattern: &'ast ast::Pattern, + + /// The index of the sub-pattern that's being currently visited within the pattern. + /// + /// For example: + /// ```py + /// match subject: + /// case a as b: ... + /// case [a, b]: ... + /// case a | b: ... + /// ``` + /// + /// In all of the above cases, the index would be 0 for `a` and 1 for `b`. + index: u32, +} + +impl<'a> CurrentMatchCase<'a> { + fn new(pattern: &'a ast::Pattern) -> Self { + Self { pattern, index: 0 } + } +} + +enum Unpackable<'ast> { + Assign(&'ast ast::StmtAssign), + For(&'ast ast::StmtFor), + WithItem { + item: &'ast ast::WithItem, + is_async: bool, + }, + Comprehension { + first: bool, + node: &'ast ast::Comprehension, + }, +} + +impl<'ast> Unpackable<'ast> { + const fn kind(&self) -> UnpackKind { + match self { + Unpackable::Assign(_) => UnpackKind::Assign, + Unpackable::For(_) | Unpackable::Comprehension { .. } => UnpackKind::Iterable, + Unpackable::WithItem { .. } => UnpackKind::ContextManager, + } + } + + fn as_current_assignment<'db>( + &self, + unpack: Option>, + ) -> CurrentAssignment<'ast, 'db> { + let unpack = unpack.map(|unpack| (UnpackPosition::First, unpack)); + match self { + Unpackable::Assign(stmt) => CurrentAssignment::Assign { node: stmt, unpack }, + Unpackable::For(stmt) => CurrentAssignment::For { node: stmt, unpack }, + Unpackable::WithItem { item, is_async } => CurrentAssignment::WithItem { + item, + is_async: *is_async, + unpack, + }, + Unpackable::Comprehension { node, first } => CurrentAssignment::Comprehension { + node, + first: *first, + unpack, + }, + } + } +} + +/// Returns the single argument to `__all__.extend()`, if it is a call to `__all__.extend()` +/// where it looks like the argument might be a `submodule.__all__` expression. +/// Else, returns `None`. +fn dunder_all_extend_argument(value: &ast::Expr) -> Option<&ast::Expr> { + let ast::ExprCall { + func, + arguments: + ast::Arguments { + args, + keywords, + range: _, + node_index: _, + }, + .. + } = value.as_call_expr()?; + + let ast::ExprAttribute { value, attr, .. } = func.as_attribute_expr()?; + + let ast::ExprName { id, .. } = value.as_name_expr()?; + + if id != "__all__" { + return None; + } + + if attr != "extend" { + return None; + } + + if !keywords.is_empty() { + return None; + } + + let [single_argument] = &**args else { + return None; + }; + + let ast::ExprAttribute { value, attr, .. } = single_argument.as_attribute_expr()?; + + (attr == "__all__").then_some(value) +} diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder/except_handlers.rs b/crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/builder/except_handlers.rs rename to crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs new file mode 100644 index 0000000000000..aa9d3368008f2 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -0,0 +1,1179 @@ +use std::ops::Deref; + +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_python_ast as ast; +use ruff_text_size::{Ranged, TextRange}; + +use crate::Db; +use crate::ast_node_ref::AstNodeRef; +use crate::node_key::NodeKey; +use crate::semantic_index::place::{FileScopeId, ScopeId, ScopedPlaceId}; +use crate::unpack::{Unpack, UnpackPosition}; + +/// A definition of a place. +/// +/// ## ID stability +/// The `Definition`'s ID is stable when the only field that change is its `kind` (AST node). +/// +/// The `Definition` changes when the `file`, `scope`, or `place` change. This can be +/// because a new scope gets inserted before the `Definition` or a new place is inserted +/// before this `Definition`. However, the ID can be considered stable and it is okay to use +/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs. +#[salsa::tracked(debug)] +pub struct Definition<'db> { + /// The file in which the definition occurs. + pub(crate) file: File, + + /// The scope in which the definition occurs. + pub(crate) file_scope: FileScopeId, + + /// The place ID of the definition. + pub(crate) place: ScopedPlaceId, + + /// WARNING: Only access this field when doing type inference for the same + /// file as where `Definition` is defined to avoid cross-file query dependencies. + #[no_eq] + #[returns(ref)] + #[tracked] + pub(crate) kind: DefinitionKind<'db>, + + /// This is a dedicated field to avoid accessing `kind` to compute this value. + pub(crate) is_reexported: bool, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for Definition<'_> {} + +impl<'db> Definition<'db> { + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.file_scope(db).to_scope_id(db, self.file(db)) + } + + pub fn full_range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> FileRange { + FileRange::new(self.file(db), self.kind(db).full_range(module)) + } + + pub fn focus_range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> FileRange { + FileRange::new(self.file(db), self.kind(db).target_range(module)) + } + + /// Extract a docstring from this definition, if applicable. + /// This method returns a docstring for function and class definitions. + /// The docstring is extracted from the first statement in the body if it's a string literal. + pub fn docstring(self, db: &'db dyn Db) -> Option { + let file = self.file(db); + let module = parsed_module(db, file).load(db); + let kind = self.kind(db); + + match kind { + DefinitionKind::Function(function_def) => { + let function_node = function_def.node(&module); + docstring_from_body(&function_node.body) + .map(|docstring_expr| docstring_expr.value.to_str().to_owned()) + } + DefinitionKind::Class(class_def) => { + let class_node = class_def.node(&module); + docstring_from_body(&class_node.body) + .map(|docstring_expr| docstring_expr.value.to_str().to_owned()) + } + _ => None, + } + } +} + +/// Extract a docstring from a function or class body. +fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> { + let stmt = body.first()?; + // Require the docstring to be a standalone expression. + let ast::Stmt::Expr(ast::StmtExpr { + value, + range: _, + node_index: _, + }) = stmt + else { + return None; + }; + // Only match string literals. + value.as_string_literal_expr() +} + +/// One or more [`Definition`]s. +#[derive(Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub struct Definitions<'db> { + definitions: smallvec::SmallVec<[Definition<'db>; 1]>, +} + +impl<'db> Definitions<'db> { + pub(crate) fn single(definition: Definition<'db>) -> Self { + Self { + definitions: smallvec::smallvec![definition], + } + } + + pub(crate) fn push(&mut self, definition: Definition<'db>) { + self.definitions.push(definition); + } +} + +impl<'db> Deref for Definitions<'db> { + type Target = [Definition<'db>]; + + fn deref(&self) -> &Self::Target { + &self.definitions + } +} + +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.definitions.iter() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) enum DefinitionState<'db> { + Defined(Definition<'db>), + /// Represents the implicit "unbound"/"undeclared" definition of every place. + Undefined, + /// Represents a definition that has been deleted. + /// This used when an attribute/subscript definition (such as `x.y = ...`, `x[0] = ...`) becomes obsolete due to a reassignment of the root place. + Deleted, +} + +impl<'db> DefinitionState<'db> { + pub(crate) fn is_defined_and(self, f: impl Fn(Definition<'db>) -> bool) -> bool { + matches!(self, DefinitionState::Defined(def) if f(def)) + } + + pub(crate) fn is_undefined_or(self, f: impl Fn(Definition<'db>) -> bool) -> bool { + matches!(self, DefinitionState::Undefined) + || matches!(self, DefinitionState::Defined(def) if f(def)) + } + + #[allow(unused)] + pub(crate) fn definition(self) -> Option> { + match self { + DefinitionState::Defined(def) => Some(def), + DefinitionState::Deleted | DefinitionState::Undefined => None, + } + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) enum DefinitionNodeRef<'ast, 'db> { + Import(ImportDefinitionNodeRef<'ast>), + ImportFrom(ImportFromDefinitionNodeRef<'ast>), + ImportStar(StarImportDefinitionNodeRef<'ast>), + For(ForStmtDefinitionNodeRef<'ast, 'db>), + Function(&'ast ast::StmtFunctionDef), + Class(&'ast ast::StmtClassDef), + TypeAlias(&'ast ast::StmtTypeAlias), + NamedExpression(&'ast ast::ExprNamed), + Assignment(AssignmentDefinitionNodeRef<'ast, 'db>), + AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef<'ast>), + AugmentedAssignment(&'ast ast::StmtAugAssign), + Comprehension(ComprehensionDefinitionNodeRef<'ast, 'db>), + VariadicPositionalParameter(&'ast ast::Parameter), + VariadicKeywordParameter(&'ast ast::Parameter), + Parameter(&'ast ast::ParameterWithDefault), + WithItem(WithItemDefinitionNodeRef<'ast, 'db>), + MatchPattern(MatchPatternDefinitionNodeRef<'ast>), + ExceptHandler(ExceptHandlerDefinitionNodeRef<'ast>), + TypeVar(&'ast ast::TypeParamTypeVar), + ParamSpec(&'ast ast::TypeParamParamSpec), + TypeVarTuple(&'ast ast::TypeParamTypeVarTuple), +} + +impl<'ast> From<&'ast ast::StmtFunctionDef> for DefinitionNodeRef<'ast, '_> { + fn from(node: &'ast ast::StmtFunctionDef) -> Self { + Self::Function(node) + } +} + +impl<'ast> From<&'ast ast::StmtClassDef> for DefinitionNodeRef<'ast, '_> { + fn from(node: &'ast ast::StmtClassDef) -> Self { + Self::Class(node) + } +} + +impl<'ast> From<&'ast ast::StmtTypeAlias> for DefinitionNodeRef<'ast, '_> { + fn from(node: &'ast ast::StmtTypeAlias) -> Self { + Self::TypeAlias(node) + } +} + +impl<'ast> From<&'ast ast::ExprNamed> for DefinitionNodeRef<'ast, '_> { + fn from(node: &'ast ast::ExprNamed) -> Self { + Self::NamedExpression(node) + } +} + +impl<'ast> From<&'ast ast::StmtAugAssign> for DefinitionNodeRef<'ast, '_> { + fn from(node: &'ast ast::StmtAugAssign) -> Self { + Self::AugmentedAssignment(node) + } +} + +impl<'ast> From<&'ast ast::TypeParamTypeVar> for DefinitionNodeRef<'ast, '_> { + fn from(value: &'ast ast::TypeParamTypeVar) -> Self { + Self::TypeVar(value) + } +} + +impl<'ast> From<&'ast ast::TypeParamParamSpec> for DefinitionNodeRef<'ast, '_> { + fn from(value: &'ast ast::TypeParamParamSpec) -> Self { + Self::ParamSpec(value) + } +} + +impl<'ast> From<&'ast ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'ast, '_> { + fn from(value: &'ast ast::TypeParamTypeVarTuple) -> Self { + Self::TypeVarTuple(value) + } +} + +impl<'ast> From> for DefinitionNodeRef<'ast, '_> { + fn from(node_ref: ImportDefinitionNodeRef<'ast>) -> Self { + Self::Import(node_ref) + } +} + +impl<'ast> From> for DefinitionNodeRef<'ast, '_> { + fn from(node_ref: ImportFromDefinitionNodeRef<'ast>) -> Self { + Self::ImportFrom(node_ref) + } +} + +impl<'ast, 'db> From> for DefinitionNodeRef<'ast, 'db> { + fn from(value: ForStmtDefinitionNodeRef<'ast, 'db>) -> Self { + Self::For(value) + } +} + +impl<'ast, 'db> From> for DefinitionNodeRef<'ast, 'db> { + fn from(node_ref: AssignmentDefinitionNodeRef<'ast, 'db>) -> Self { + Self::Assignment(node_ref) + } +} + +impl<'ast> From> for DefinitionNodeRef<'ast, '_> { + fn from(node_ref: AnnotatedAssignmentDefinitionNodeRef<'ast>) -> Self { + Self::AnnotatedAssignment(node_ref) + } +} + +impl<'ast, 'db> From> for DefinitionNodeRef<'ast, 'db> { + fn from(node_ref: WithItemDefinitionNodeRef<'ast, 'db>) -> Self { + Self::WithItem(node_ref) + } +} + +impl<'ast, 'db> From> for DefinitionNodeRef<'ast, 'db> { + fn from(node: ComprehensionDefinitionNodeRef<'ast, 'db>) -> Self { + Self::Comprehension(node) + } +} + +impl<'ast> From<&'ast ast::ParameterWithDefault> for DefinitionNodeRef<'ast, '_> { + fn from(node: &'ast ast::ParameterWithDefault) -> Self { + Self::Parameter(node) + } +} + +impl<'ast> From> for DefinitionNodeRef<'ast, '_> { + fn from(node: MatchPatternDefinitionNodeRef<'ast>) -> Self { + Self::MatchPattern(node) + } +} + +impl<'ast> From> for DefinitionNodeRef<'ast, '_> { + fn from(node: StarImportDefinitionNodeRef<'ast>) -> Self { + Self::ImportStar(node) + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct ImportDefinitionNodeRef<'ast> { + pub(crate) node: &'ast ast::StmtImport, + pub(crate) alias_index: usize, + pub(crate) is_reexported: bool, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct StarImportDefinitionNodeRef<'ast> { + pub(crate) node: &'ast ast::StmtImportFrom, + pub(crate) place_id: ScopedPlaceId, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct ImportFromDefinitionNodeRef<'ast> { + pub(crate) node: &'ast ast::StmtImportFrom, + pub(crate) alias_index: usize, + pub(crate) is_reexported: bool, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct AssignmentDefinitionNodeRef<'ast, 'db> { + pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub(crate) value: &'ast ast::Expr, + pub(crate) target: &'ast ast::Expr, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct AnnotatedAssignmentDefinitionNodeRef<'ast> { + pub(crate) node: &'ast ast::StmtAnnAssign, + pub(crate) annotation: &'ast ast::Expr, + pub(crate) value: Option<&'ast ast::Expr>, + pub(crate) target: &'ast ast::Expr, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct WithItemDefinitionNodeRef<'ast, 'db> { + pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub(crate) context_expr: &'ast ast::Expr, + pub(crate) target: &'ast ast::Expr, + pub(crate) is_async: bool, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct ForStmtDefinitionNodeRef<'ast, 'db> { + pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub(crate) iterable: &'ast ast::Expr, + pub(crate) target: &'ast ast::Expr, + pub(crate) is_async: bool, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct ExceptHandlerDefinitionNodeRef<'ast> { + pub(crate) handler: &'ast ast::ExceptHandlerExceptHandler, + pub(crate) is_star: bool, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct ComprehensionDefinitionNodeRef<'ast, 'db> { + pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>, + pub(crate) iterable: &'ast ast::Expr, + pub(crate) target: &'ast ast::Expr, + pub(crate) first: bool, + pub(crate) is_async: bool, +} + +#[derive(Copy, Clone, Debug)] +pub(crate) struct MatchPatternDefinitionNodeRef<'ast> { + /// The outermost pattern node in which the identifier being defined occurs. + pub(crate) pattern: &'ast ast::Pattern, + /// The identifier being defined. + pub(crate) identifier: &'ast ast::Identifier, + /// The index of the identifier in the pattern when visiting the `pattern` node in evaluation + /// order. + pub(crate) index: u32, +} + +impl<'db> DefinitionNodeRef<'_, 'db> { + pub(super) fn into_owned(self, parsed: &ParsedModuleRef) -> DefinitionKind<'db> { + match self { + DefinitionNodeRef::Import(ImportDefinitionNodeRef { + node, + alias_index, + is_reexported, + }) => DefinitionKind::Import(ImportDefinitionKind { + node: AstNodeRef::new(parsed, node), + alias_index, + is_reexported, + }), + + DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef { + node, + alias_index, + is_reexported, + }) => DefinitionKind::ImportFrom(ImportFromDefinitionKind { + node: AstNodeRef::new(parsed, node), + alias_index, + is_reexported, + }), + DefinitionNodeRef::ImportStar(star_import) => { + let StarImportDefinitionNodeRef { node, place_id } = star_import; + DefinitionKind::StarImport(StarImportDefinitionKind { + node: AstNodeRef::new(parsed, node), + place_id, + }) + } + DefinitionNodeRef::Function(function) => { + DefinitionKind::Function(AstNodeRef::new(parsed, function)) + } + DefinitionNodeRef::Class(class) => { + DefinitionKind::Class(AstNodeRef::new(parsed, class)) + } + DefinitionNodeRef::TypeAlias(type_alias) => { + DefinitionKind::TypeAlias(AstNodeRef::new(parsed, type_alias)) + } + DefinitionNodeRef::NamedExpression(named) => { + DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named)) + } + DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef { + unpack, + value, + target, + }) => DefinitionKind::Assignment(AssignmentDefinitionKind { + target_kind: TargetKind::from(unpack), + value: AstNodeRef::new(parsed, value), + target: AstNodeRef::new(parsed, target), + }), + DefinitionNodeRef::AnnotatedAssignment(AnnotatedAssignmentDefinitionNodeRef { + node: _, + annotation, + value, + target, + }) => DefinitionKind::AnnotatedAssignment(AnnotatedAssignmentDefinitionKind { + target: AstNodeRef::new(parsed, target), + annotation: AstNodeRef::new(parsed, annotation), + value: value.map(|v| AstNodeRef::new(parsed, v)), + }), + DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => { + DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment)) + } + DefinitionNodeRef::For(ForStmtDefinitionNodeRef { + unpack, + iterable, + target, + is_async, + }) => DefinitionKind::For(ForStmtDefinitionKind { + target_kind: TargetKind::from(unpack), + iterable: AstNodeRef::new(parsed, iterable), + target: AstNodeRef::new(parsed, target), + is_async, + }), + DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef { + unpack, + iterable, + target, + first, + is_async, + }) => DefinitionKind::Comprehension(ComprehensionDefinitionKind { + target_kind: TargetKind::from(unpack), + iterable: AstNodeRef::new(parsed, iterable), + target: AstNodeRef::new(parsed, target), + first, + is_async, + }), + DefinitionNodeRef::VariadicPositionalParameter(parameter) => { + DefinitionKind::VariadicPositionalParameter(AstNodeRef::new(parsed, parameter)) + } + DefinitionNodeRef::VariadicKeywordParameter(parameter) => { + DefinitionKind::VariadicKeywordParameter(AstNodeRef::new(parsed, parameter)) + } + DefinitionNodeRef::Parameter(parameter) => { + DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter)) + } + DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef { + unpack, + context_expr, + target, + is_async, + }) => DefinitionKind::WithItem(WithItemDefinitionKind { + target_kind: TargetKind::from(unpack), + context_expr: AstNodeRef::new(parsed, context_expr), + target: AstNodeRef::new(parsed, target), + is_async, + }), + DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef { + pattern, + identifier, + index, + }) => DefinitionKind::MatchPattern(MatchPatternDefinitionKind { + pattern: AstNodeRef::new(parsed, pattern), + identifier: AstNodeRef::new(parsed, identifier), + index, + }), + DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef { + handler, + is_star, + }) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind { + handler: AstNodeRef::new(parsed, handler), + is_star, + }), + DefinitionNodeRef::TypeVar(node) => { + DefinitionKind::TypeVar(AstNodeRef::new(parsed, node)) + } + DefinitionNodeRef::ParamSpec(node) => { + DefinitionKind::ParamSpec(AstNodeRef::new(parsed, node)) + } + DefinitionNodeRef::TypeVarTuple(node) => { + DefinitionKind::TypeVarTuple(AstNodeRef::new(parsed, node)) + } + } + } + + pub(super) fn key(self) -> DefinitionNodeKey { + match self { + Self::Import(ImportDefinitionNodeRef { + node, + alias_index, + is_reexported: _, + }) => (&node.names[alias_index]).into(), + Self::ImportFrom(ImportFromDefinitionNodeRef { + node, + 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, place_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(), + Self::NamedExpression(node) => node.into(), + Self::Assignment(AssignmentDefinitionNodeRef { + value: _, + unpack: _, + target, + }) => DefinitionNodeKey(NodeKey::from_node(target)), + Self::AnnotatedAssignment(ann_assign) => ann_assign.node.into(), + Self::AugmentedAssignment(node) => node.into(), + Self::For(ForStmtDefinitionNodeRef { + target, + iterable: _, + unpack: _, + is_async: _, + }) => DefinitionNodeKey(NodeKey::from_node(target)), + Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => { + DefinitionNodeKey(NodeKey::from_node(target)) + } + Self::VariadicPositionalParameter(node) => node.into(), + Self::VariadicKeywordParameter(node) => node.into(), + Self::Parameter(node) => node.into(), + Self::WithItem(WithItemDefinitionNodeRef { + context_expr: _, + unpack: _, + is_async: _, + target, + }) => DefinitionNodeKey(NodeKey::from_node(target)), + Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => { + identifier.into() + } + Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(), + Self::TypeVar(node) => node.into(), + Self::ParamSpec(node) => node.into(), + Self::TypeVarTuple(node) => node.into(), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum DefinitionCategory { + /// A Definition which binds a value to a name (e.g. `x = 1`). + Binding, + /// A Definition which declares the upper-bound of acceptable types for this name (`x: int`). + Declaration, + /// A Definition which both declares a type and binds a value (e.g. `x: int = 1`). + DeclarationAndBinding, +} + +impl DefinitionCategory { + /// True if this definition establishes a "declared type" for the place. + /// + /// If so, any assignments reached by this definition are in error if they assign a value of a + /// type not assignable to the declared type. + /// + /// Annotations establish a declared type. So do function and class definitions, and imports. + pub(crate) fn is_declaration(self) -> bool { + matches!( + self, + DefinitionCategory::Declaration | DefinitionCategory::DeclarationAndBinding + ) + } + + /// True if this definition assigns a value to the place. + /// + /// False only for annotated assignments without a RHS. + pub(crate) fn is_binding(self) -> bool { + matches!( + self, + DefinitionCategory::Binding | DefinitionCategory::DeclarationAndBinding + ) + } +} + +/// The kind of a definition. +/// +/// ## Usage in salsa tracked structs +/// +/// [`DefinitionKind`] fields in salsa tracked structs should be tracked (attributed with `#[tracked]`) +/// because the kind is a thin wrapper around [`AstNodeRef`]. See the [`AstNodeRef`] documentation +/// for an in-depth explanation of why this is necessary. +#[derive(Clone, Debug)] +pub enum DefinitionKind<'db> { + Import(ImportDefinitionKind), + ImportFrom(ImportFromDefinitionKind), + StarImport(StarImportDefinitionKind), + Function(AstNodeRef), + Class(AstNodeRef), + TypeAlias(AstNodeRef), + NamedExpression(AstNodeRef), + Assignment(AssignmentDefinitionKind<'db>), + AnnotatedAssignment(AnnotatedAssignmentDefinitionKind), + AugmentedAssignment(AstNodeRef), + For(ForStmtDefinitionKind<'db>), + Comprehension(ComprehensionDefinitionKind<'db>), + VariadicPositionalParameter(AstNodeRef), + VariadicKeywordParameter(AstNodeRef), + Parameter(AstNodeRef), + WithItem(WithItemDefinitionKind<'db>), + MatchPattern(MatchPatternDefinitionKind), + ExceptHandler(ExceptHandlerDefinitionKind), + TypeVar(AstNodeRef), + ParamSpec(AstNodeRef), + TypeVarTuple(AstNodeRef), +} + +impl DefinitionKind<'_> { + pub(crate) fn is_reexported(&self) -> bool { + match self { + DefinitionKind::Import(import) => import.is_reexported(), + DefinitionKind::ImportFrom(import) => import.is_reexported(), + _ => true, + } + } + + pub(crate) const fn as_star_import(&self) -> Option<&StarImportDefinitionKind> { + match self { + DefinitionKind::StarImport(import) => Some(import), + _ => None, + } + } + + pub(crate) fn is_import(&self) -> bool { + matches!( + self, + DefinitionKind::Import(_) + | DefinitionKind::ImportFrom(_) + | DefinitionKind::StarImport(_) + ) + } + + /// Returns the [`TextRange`] of the definition target. + /// + /// A definition target would mainly be the node representing the place being defined i.e., + /// [`ast::ExprName`], [`ast::Identifier`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`] but could also be other nodes. + pub(crate) fn target_range(&self, module: &ParsedModuleRef) -> TextRange { + match self { + DefinitionKind::Import(import) => import.alias(module).range(), + DefinitionKind::ImportFrom(import) => import.alias(module).range(), + DefinitionKind::StarImport(import) => import.alias(module).range(), + DefinitionKind::Function(function) => function.node(module).name.range(), + DefinitionKind::Class(class) => class.node(module).name.range(), + DefinitionKind::TypeAlias(type_alias) => type_alias.node(module).name.range(), + DefinitionKind::NamedExpression(named) => named.node(module).target.range(), + DefinitionKind::Assignment(assignment) => assignment.target.node(module).range(), + DefinitionKind::AnnotatedAssignment(assign) => assign.target.node(module).range(), + DefinitionKind::AugmentedAssignment(aug_assign) => { + aug_assign.node(module).target.range() + } + DefinitionKind::For(for_stmt) => for_stmt.target.node(module).range(), + DefinitionKind::Comprehension(comp) => comp.target(module).range(), + DefinitionKind::VariadicPositionalParameter(parameter) => { + parameter.node(module).name.range() + } + DefinitionKind::VariadicKeywordParameter(parameter) => { + parameter.node(module).name.range() + } + DefinitionKind::Parameter(parameter) => parameter.node(module).parameter.name.range(), + DefinitionKind::WithItem(with_item) => with_item.target.node(module).range(), + DefinitionKind::MatchPattern(match_pattern) => { + match_pattern.identifier.node(module).range() + } + DefinitionKind::ExceptHandler(handler) => handler.node(module).range(), + DefinitionKind::TypeVar(type_var) => type_var.node(module).name.range(), + DefinitionKind::ParamSpec(param_spec) => param_spec.node(module).name.range(), + DefinitionKind::TypeVarTuple(type_var_tuple) => { + type_var_tuple.node(module).name.range() + } + } + } + + /// Returns the [`TextRange`] of the entire definition. + pub(crate) fn full_range(&self, module: &ParsedModuleRef) -> TextRange { + match self { + DefinitionKind::Import(import) => import.alias(module).range(), + DefinitionKind::ImportFrom(import) => import.alias(module).range(), + DefinitionKind::StarImport(import) => import.import(module).range(), + DefinitionKind::Function(function) => function.node(module).range(), + DefinitionKind::Class(class) => class.node(module).range(), + DefinitionKind::TypeAlias(type_alias) => type_alias.node(module).range(), + DefinitionKind::NamedExpression(named) => named.node(module).range(), + DefinitionKind::Assignment(assign) => { + let target_range = assign.target.node(module).range(); + let value_range = assign.value.node(module).range(); + target_range.cover(value_range) + } + DefinitionKind::AnnotatedAssignment(assign) => { + let target_range = assign.target.node(module).range(); + if let Some(ref value) = assign.value { + let value_range = value.node(module).range(); + target_range.cover(value_range) + } else { + target_range + } + } + DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.node(module).range(), + DefinitionKind::For(for_stmt) => for_stmt.target.node(module).range(), + DefinitionKind::Comprehension(comp) => comp.target(module).range(), + DefinitionKind::VariadicPositionalParameter(parameter) => { + parameter.node(module).range() + } + DefinitionKind::VariadicKeywordParameter(parameter) => parameter.node(module).range(), + DefinitionKind::Parameter(parameter) => parameter.node(module).parameter.range(), + DefinitionKind::WithItem(with_item) => with_item.target.node(module).range(), + DefinitionKind::MatchPattern(match_pattern) => { + match_pattern.identifier.node(module).range() + } + DefinitionKind::ExceptHandler(handler) => handler.node(module).range(), + DefinitionKind::TypeVar(type_var) => type_var.node(module).range(), + DefinitionKind::ParamSpec(param_spec) => param_spec.node(module).range(), + DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.node(module).range(), + } + } + + pub(crate) fn category(&self, in_stub: bool, module: &ParsedModuleRef) -> DefinitionCategory { + match self { + // functions, classes, and imports always bind, and we consider them declarations + DefinitionKind::Function(_) + | DefinitionKind::Class(_) + | DefinitionKind::TypeAlias(_) + | DefinitionKind::Import(_) + | DefinitionKind::ImportFrom(_) + | DefinitionKind::StarImport(_) + | DefinitionKind::TypeVar(_) + | DefinitionKind::ParamSpec(_) + | DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding, + // a parameter always binds a value, but is only a declaration if annotated + DefinitionKind::VariadicPositionalParameter(parameter) + | DefinitionKind::VariadicKeywordParameter(parameter) => { + if parameter.node(module).annotation.is_some() { + DefinitionCategory::DeclarationAndBinding + } else { + DefinitionCategory::Binding + } + } + // presence of a default is irrelevant, same logic as for a no-default parameter + DefinitionKind::Parameter(parameter_with_default) => { + if parameter_with_default + .node(module) + .parameter + .annotation + .is_some() + { + DefinitionCategory::DeclarationAndBinding + } else { + DefinitionCategory::Binding + } + } + // Annotated assignment is always a declaration. It is also a binding if there is a RHS + // or if we are in a stub file. Unfortunately, it is common for stubs to omit even an `...` value placeholder. + DefinitionKind::AnnotatedAssignment(ann_assign) => { + if in_stub || ann_assign.value.is_some() { + DefinitionCategory::DeclarationAndBinding + } else { + DefinitionCategory::Declaration + } + } + // all of these bind values without declaring a type + DefinitionKind::NamedExpression(_) + | DefinitionKind::Assignment(_) + | DefinitionKind::AugmentedAssignment(_) + | DefinitionKind::For(_) + | DefinitionKind::Comprehension(_) + | DefinitionKind::WithItem(_) + | DefinitionKind::MatchPattern(_) + | DefinitionKind::ExceptHandler(_) => DefinitionCategory::Binding, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Hash)] +pub(crate) enum TargetKind<'db> { + Sequence(UnpackPosition, Unpack<'db>), + /// Name, attribute, or subscript. + Single, +} + +impl<'db> From)>> for TargetKind<'db> { + fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self { + match value { + Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack), + None => TargetKind::Single, + } + } +} + +#[derive(Clone, Debug)] +pub struct StarImportDefinitionKind { + node: AstNodeRef, + place_id: ScopedPlaceId, +} + +impl StarImportDefinitionKind { + pub(crate) fn import<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::StmtImportFrom { + self.node.node(module) + } + + pub(crate) fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast 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(module) + .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 place_id(&self) -> ScopedPlaceId { + self.place_id + } +} + +#[derive(Clone, Debug)] +pub struct MatchPatternDefinitionKind { + pattern: AstNodeRef, + identifier: AstNodeRef, + index: u32, +} + +impl MatchPatternDefinitionKind { + pub(crate) fn pattern<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Pattern { + self.pattern.node(module) + } + + pub(crate) fn index(&self) -> u32 { + self.index + } +} + +/// Note that the elements of a comprehension can be in different scopes. +/// If the definition target of a comprehension is a name, it is in the comprehension's scope. +/// But if the target is an attribute or subscript, its definition is not in the comprehension's scope; +/// it is in the scope in which the root variable is bound. +/// TODO: currently we don't model this correctly and simply assume that it is in a scope outside the comprehension. +#[derive(Clone, Debug)] +pub struct ComprehensionDefinitionKind<'db> { + target_kind: TargetKind<'db>, + iterable: AstNodeRef, + target: AstNodeRef, + first: bool, + is_async: bool, +} + +impl<'db> ComprehensionDefinitionKind<'db> { + pub(crate) fn iterable<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.iterable.node(module) + } + + pub(crate) fn target_kind(&self) -> TargetKind<'db> { + self.target_kind + } + + pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.target.node(module) + } + + pub(crate) fn is_first(&self) -> bool { + self.first + } + + pub(crate) fn is_async(&self) -> bool { + self.is_async + } +} + +#[derive(Clone, Debug)] +pub struct ImportDefinitionKind { + node: AstNodeRef, + alias_index: usize, + is_reexported: bool, +} + +impl ImportDefinitionKind { + pub(crate) fn import<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::StmtImport { + self.node.node(module) + } + + pub(crate) fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { + &self.node.node(module).names[self.alias_index] + } + + pub(crate) fn is_reexported(&self) -> bool { + self.is_reexported + } +} + +#[derive(Clone, Debug)] +pub struct ImportFromDefinitionKind { + node: AstNodeRef, + alias_index: usize, + is_reexported: bool, +} + +impl ImportFromDefinitionKind { + pub(crate) fn import<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::StmtImportFrom { + self.node.node(module) + } + + pub(crate) fn alias<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Alias { + &self.node.node(module).names[self.alias_index] + } + + pub(crate) fn is_reexported(&self) -> bool { + self.is_reexported + } +} + +#[derive(Clone, Debug)] +pub struct AssignmentDefinitionKind<'db> { + target_kind: TargetKind<'db>, + value: AstNodeRef, + target: AstNodeRef, +} + +impl<'db> AssignmentDefinitionKind<'db> { + pub(crate) fn target_kind(&self) -> TargetKind<'db> { + self.target_kind + } + + pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.value.node(module) + } + + pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.target.node(module) + } +} + +#[derive(Clone, Debug)] +pub struct AnnotatedAssignmentDefinitionKind { + annotation: AstNodeRef, + value: Option>, + target: AstNodeRef, +} + +impl AnnotatedAssignmentDefinitionKind { + pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> { + self.value.as_ref().map(|value| value.node(module)) + } + + pub(crate) fn annotation<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.annotation.node(module) + } + + pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.target.node(module) + } +} + +#[derive(Clone, Debug)] +pub struct WithItemDefinitionKind<'db> { + target_kind: TargetKind<'db>, + context_expr: AstNodeRef, + target: AstNodeRef, + is_async: bool, +} + +impl<'db> WithItemDefinitionKind<'db> { + pub(crate) fn context_expr<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.context_expr.node(module) + } + + pub(crate) fn target_kind(&self) -> TargetKind<'db> { + self.target_kind + } + + pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.target.node(module) + } + + pub(crate) const fn is_async(&self) -> bool { + self.is_async + } +} + +#[derive(Clone, Debug)] +pub struct ForStmtDefinitionKind<'db> { + target_kind: TargetKind<'db>, + iterable: AstNodeRef, + target: AstNodeRef, + is_async: bool, +} + +impl<'db> ForStmtDefinitionKind<'db> { + pub(crate) fn iterable<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.iterable.node(module) + } + + pub(crate) fn target_kind(&self) -> TargetKind<'db> { + self.target_kind + } + + pub(crate) fn target<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Expr { + self.target.node(module) + } + + pub(crate) const fn is_async(&self) -> bool { + self.is_async + } +} + +#[derive(Clone, Debug)] +pub struct ExceptHandlerDefinitionKind { + handler: AstNodeRef, + is_star: bool, +} + +impl ExceptHandlerDefinitionKind { + pub(crate) fn node<'ast>( + &self, + module: &'ast ParsedModuleRef, + ) -> &'ast ast::ExceptHandlerExceptHandler { + self.handler.node(module) + } + + pub(crate) fn handled_exceptions<'ast>( + &self, + module: &'ast ParsedModuleRef, + ) -> Option<&'ast ast::Expr> { + self.node(module).type_.as_deref() + } + + pub(crate) fn is_star(&self) -> bool { + self.is_star + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update, get_size2::GetSize)] +pub(crate) struct DefinitionNodeKey(NodeKey); + +impl From<&ast::Alias> for DefinitionNodeKey { + fn from(node: &ast::Alias) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::StmtFunctionDef> for DefinitionNodeKey { + fn from(node: &ast::StmtFunctionDef) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::StmtClassDef> for DefinitionNodeKey { + fn from(node: &ast::StmtClassDef) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::StmtTypeAlias> for DefinitionNodeKey { + fn from(node: &ast::StmtTypeAlias) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::ExprName> for DefinitionNodeKey { + fn from(node: &ast::ExprName) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::ExprAttribute> for DefinitionNodeKey { + fn from(node: &ast::ExprAttribute) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::ExprSubscript> for DefinitionNodeKey { + fn from(node: &ast::ExprSubscript) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::ExprNamed> for DefinitionNodeKey { + fn from(node: &ast::ExprNamed) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::StmtAnnAssign> for DefinitionNodeKey { + fn from(node: &ast::StmtAnnAssign) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::StmtAugAssign> for DefinitionNodeKey { + fn from(node: &ast::StmtAugAssign) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::Parameter> for DefinitionNodeKey { + fn from(node: &ast::Parameter) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From<&ast::ParameterWithDefault> for DefinitionNodeKey { + fn from(node: &ast::ParameterWithDefault) -> Self { + Self(NodeKey::from_node(node)) + } +} + +impl From> for DefinitionNodeKey { + fn from(value: ast::AnyParameterRef) -> Self { + Self(match value { + ast::AnyParameterRef::Variadic(node) => NodeKey::from_node(node), + ast::AnyParameterRef::NonVariadic(node) => NodeKey::from_node(node), + }) + } +} + +impl From<&ast::Identifier> for DefinitionNodeKey { + fn from(identifier: &ast::Identifier) -> Self { + Self(NodeKey::from_node(identifier)) + } +} + +impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey { + fn from(handler: &ast::ExceptHandlerExceptHandler) -> Self { + Self(NodeKey::from_node(handler)) + } +} + +impl From<&ast::TypeParamTypeVar> for DefinitionNodeKey { + fn from(value: &ast::TypeParamTypeVar) -> Self { + Self(NodeKey::from_node(value)) + } +} + +impl From<&ast::TypeParamParamSpec> for DefinitionNodeKey { + fn from(value: &ast::TypeParamParamSpec) -> Self { + Self(NodeKey::from_node(value)) + } +} + +impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey { + fn from(value: &ast::TypeParamTypeVarTuple) -> Self { + Self(NodeKey::from_node(value)) + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/expression.rs b/crates/ty_python_semantic/src/semantic_index/expression.rs new file mode 100644 index 0000000000000..99200c89cc826 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/expression.rs @@ -0,0 +1,78 @@ +use crate::ast_node_ref::AstNodeRef; +use crate::db::Db; +use crate::semantic_index::place::{FileScopeId, ScopeId}; +use ruff_db::files::File; +use ruff_db::parsed::ParsedModuleRef; +use ruff_python_ast as ast; +use salsa; + +/// Whether or not this expression should be inferred as a normal expression or +/// a type expression. For example, in `self.x: = `, the +/// `` is inferred as a type expression, while `` is inferred +/// as a normal expression. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum ExpressionKind { + Normal, + TypeExpression, +} + +/// An independently type-inferable expression. +/// +/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment. +/// +/// ## Module-local type +/// This type should not be used as part of any cross-module API because +/// it holds a reference to the AST node. Range-offset changes +/// then propagate through all usages, and deserialization requires +/// reparsing the entire module. +/// +/// E.g. don't use this type in: +/// +/// * a return type of a cross-module query +/// * a field of a type that is a return type of a cross-module query +/// * an argument of a cross-module query +#[salsa::tracked(debug)] +pub(crate) struct Expression<'db> { + /// The file in which the expression occurs. + pub(crate) file: File, + + /// The scope in which the expression occurs. + pub(crate) file_scope: FileScopeId, + + /// The expression node. + #[no_eq] + #[tracked] + #[returns(ref)] + pub(crate) _node_ref: AstNodeRef, + + /// An assignment statement, if this expression is immediately used as the rhs of that + /// assignment. + /// + /// (Note that this is the _immediately_ containing assignment — if a complex expression is + /// assigned to some target, only the outermost expression node has this set. The inner + /// expressions are used to build up the assignment result, and are not "immediately assigned" + /// to the target, and so have `None` for this field.) + #[no_eq] + #[tracked] + pub(crate) assigned_to: Option>, + + /// Should this expression be inferred as a normal expression or a type expression? + pub(crate) kind: ExpressionKind, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for Expression<'_> {} + +impl<'db> Expression<'db> { + pub(crate) fn node_ref<'ast>( + self, + db: &'db dyn Db, + parsed: &'ast ParsedModuleRef, + ) -> &'ast ast::Expr { + self._node_ref(db).node(parsed) + } + + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.file_scope(db).to_scope_id(db, self.file(db)) + } +} diff --git a/crates/red_knot_python_semantic/src/semantic_index/narrowing_constraints.rs b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs similarity index 89% rename from crates/red_knot_python_semantic/src/semantic_index/narrowing_constraints.rs rename to crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs index 6ed80e7ebf2b1..54155a6ff3086 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/narrowing_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs @@ -1,7 +1,7 @@ //! # Narrowing constraints //! //! When building a semantic index for a file, we associate each binding with a _narrowing -//! constraint_, which constrains the type of the binding's symbol. Note that a binding can be +//! constraint_, which constrains the type of the binding's place. Note that a binding can be //! associated with a different narrowing constraint at different points in a file. See the //! [`use_def`][crate::semantic_index::use_def] module for more details. //! @@ -29,24 +29,33 @@ //! [`Predicate`]: crate::semantic_index::predicate::Predicate use crate::list::{List, ListBuilder, ListSetReverseIterator, ListStorage}; +use crate::semantic_index::ast_ids::ScopedUseId; +use crate::semantic_index::place::FileScopeId; use crate::semantic_index::predicate::ScopedPredicateId; /// A narrowing constraint associated with a live binding. /// -/// A constraint is a list of [`Predicate`]s that each constrain the type of the binding's symbol. +/// A constraint is a list of [`Predicate`]s that each constrain the type of the binding's place. /// /// [`Predicate`]: crate::semantic_index::predicate::Predicate pub(crate) type ScopedNarrowingConstraint = List; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ConstraintKey { + NarrowingConstraint(ScopedNarrowingConstraint), + EagerNestedScope(FileScopeId), + UseId(ScopedUseId), +} + /// One of the [`Predicate`]s in a narrowing constraint, which constraints the type of the -/// binding's symbol. +/// binding's place. /// /// Note that those [`Predicate`]s are stored in [their own per-scope /// arena][crate::semantic_index::predicate::Predicates], so internally we use a /// [`ScopedPredicateId`] to refer to the underlying predicate. /// /// [`Predicate`]: crate::semantic_index::predicate::Predicate -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, get_size2::GetSize)] pub(crate) struct ScopedNarrowingConstraintPredicate(ScopedPredicateId); impl ScopedNarrowingConstraintPredicate { @@ -63,7 +72,7 @@ impl From for ScopedNarrowingConstraintPredicate { } /// A collection of narrowing constraints for a given scope. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, get_size2::GetSize)] pub(crate) struct NarrowingConstraints { lists: ListStorage, } diff --git a/crates/ty_python_semantic/src/semantic_index/place.rs b/crates/ty_python_semantic/src/semantic_index/place.rs new file mode 100644 index 0000000000000..5f0d281a8c615 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/place.rs @@ -0,0 +1,1035 @@ +use std::convert::Infallible; +use std::hash::{Hash, Hasher}; +use std::ops::Range; + +use bitflags::bitflags; +use hashbrown::hash_table::Entry; +use ruff_db::files::File; +use ruff_db::parsed::ParsedModuleRef; +use ruff_index::{IndexVec, newtype_index}; +use ruff_python_ast as ast; +use ruff_python_ast::name::Name; +use rustc_hash::FxHasher; +use smallvec::{SmallVec, smallvec}; + +use crate::Db; +use crate::ast_node_ref::AstNodeRef; +use crate::node_key::NodeKey; +use crate::semantic_index::reachability_constraints::ScopedReachabilityConstraintId; +use crate::semantic_index::{PlaceSet, SemanticIndex, semantic_index}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub(crate) enum PlaceExprSubSegment { + /// A member access, e.g. `.y` in `x.y` + Member(ast::name::Name), + /// An integer-based index access, e.g. `[1]` in `x[1]` + IntSubscript(ast::Int), + /// A string-based index access, e.g. `["foo"]` in `x["foo"]` + StringSubscript(String), +} + +impl PlaceExprSubSegment { + pub(crate) fn as_member(&self) -> Option<&ast::name::Name> { + match self { + PlaceExprSubSegment::Member(name) => Some(name), + _ => None, + } + } +} + +/// An expression that can be the target of a `Definition`. +#[derive(Eq, PartialEq, Debug, get_size2::GetSize)] +pub struct PlaceExpr { + root_name: Name, + sub_segments: SmallVec<[PlaceExprSubSegment; 1]>, +} + +impl std::fmt::Display for PlaceExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.root_name)?; + for segment in &self.sub_segments { + match segment { + PlaceExprSubSegment::Member(name) => write!(f, ".{name}")?, + PlaceExprSubSegment::IntSubscript(int) => write!(f, "[{int}]")?, + PlaceExprSubSegment::StringSubscript(string) => write!(f, "[\"{string}\"]")?, + } + } + Ok(()) + } +} + +impl TryFrom<&ast::name::Name> for PlaceExpr { + type Error = Infallible; + + fn try_from(name: &ast::name::Name) -> Result { + Ok(PlaceExpr::name(name.clone())) + } +} + +impl TryFrom for PlaceExpr { + type Error = Infallible; + + fn try_from(name: ast::name::Name) -> Result { + Ok(PlaceExpr::name(name)) + } +} + +impl TryFrom<&ast::ExprAttribute> for PlaceExpr { + type Error = (); + + fn try_from(attr: &ast::ExprAttribute) -> Result { + let mut place = PlaceExpr::try_from(&*attr.value)?; + place + .sub_segments + .push(PlaceExprSubSegment::Member(attr.attr.id.clone())); + Ok(place) + } +} + +impl TryFrom for PlaceExpr { + type Error = (); + + fn try_from(attr: ast::ExprAttribute) -> Result { + let mut place = PlaceExpr::try_from(&*attr.value)?; + place + .sub_segments + .push(PlaceExprSubSegment::Member(attr.attr.id)); + Ok(place) + } +} + +impl TryFrom<&ast::ExprSubscript> for PlaceExpr { + type Error = (); + + fn try_from(subscript: &ast::ExprSubscript) -> Result { + let mut place = PlaceExpr::try_from(&*subscript.value)?; + match &*subscript.slice { + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(index), + .. + }) => { + place + .sub_segments + .push(PlaceExprSubSegment::IntSubscript(index.clone())); + } + ast::Expr::StringLiteral(string) => { + place + .sub_segments + .push(PlaceExprSubSegment::StringSubscript( + string.value.to_string(), + )); + } + _ => { + return Err(()); + } + } + Ok(place) + } +} + +impl TryFrom for PlaceExpr { + type Error = (); + + fn try_from(subscript: ast::ExprSubscript) -> Result { + PlaceExpr::try_from(&subscript) + } +} + +impl TryFrom<&ast::Expr> for PlaceExpr { + type Error = (); + + fn try_from(expr: &ast::Expr) -> Result { + match expr { + ast::Expr::Name(name) => Ok(PlaceExpr::name(name.id.clone())), + ast::Expr::Attribute(attr) => PlaceExpr::try_from(attr), + ast::Expr::Subscript(subscript) => PlaceExpr::try_from(subscript), + _ => Err(()), + } + } +} + +impl TryFrom> for PlaceExpr { + type Error = (); + + fn try_from(expr: ast::ExprRef) -> Result { + match expr { + ast::ExprRef::Name(name) => Ok(PlaceExpr::name(name.id.clone())), + ast::ExprRef::Attribute(attr) => PlaceExpr::try_from(attr), + ast::ExprRef::Subscript(subscript) => PlaceExpr::try_from(subscript), + _ => Err(()), + } + } +} + +impl PlaceExpr { + pub(crate) fn name(name: Name) -> Self { + Self { + root_name: name, + sub_segments: smallvec![], + } + } + + pub(crate) fn root_name(&self) -> &Name { + &self.root_name + } + + pub(crate) fn sub_segments(&self) -> &[PlaceExprSubSegment] { + &self.sub_segments + } + + pub(crate) fn as_name(&self) -> Option<&Name> { + if self.is_name() { + Some(&self.root_name) + } else { + None + } + } + + /// Assumes that the place expression is a name. + #[track_caller] + pub(crate) fn expect_name(&self) -> &Name { + debug_assert_eq!(self.sub_segments.len(), 0); + &self.root_name + } + + /// Is the place just a name? + pub fn is_name(&self) -> bool { + self.sub_segments.is_empty() + } + + pub fn is_name_and(&self, f: impl FnOnce(&str) -> bool) -> bool { + self.is_name() && f(&self.root_name) + } + + /// Does the place expression have the form `.member`? + pub fn is_member(&self) -> bool { + self.sub_segments + .last() + .is_some_and(|last| last.as_member().is_some()) + } + + fn root_exprs(&self) -> RootExprs<'_> { + RootExprs { + expr_ref: self.into(), + len: self.sub_segments.len(), + } + } +} + +/// A [`PlaceExpr`] with flags, e.g. whether it is used, bound, an instance attribute, etc. +#[derive(Eq, PartialEq, Debug, get_size2::GetSize)] +pub struct PlaceExprWithFlags { + pub(crate) expr: PlaceExpr, + flags: PlaceFlags, +} + +impl std::fmt::Display for PlaceExprWithFlags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.expr.fmt(f) + } +} + +impl PlaceExprWithFlags { + pub(crate) fn new(expr: PlaceExpr) -> Self { + PlaceExprWithFlags { + expr, + flags: PlaceFlags::empty(), + } + } + + fn name(name: Name) -> Self { + PlaceExprWithFlags { + expr: PlaceExpr::name(name), + flags: PlaceFlags::empty(), + } + } + + fn insert_flags(&mut self, flags: PlaceFlags) { + self.flags.insert(flags); + } + + pub(super) fn mark_instance_attribute(&mut self) { + self.flags.insert(PlaceFlags::IS_INSTANCE_ATTRIBUTE); + } + + /// If the place expression has the form `.` + /// (meaning it *may* be an instance attribute), + /// return `Some()`. Else, return `None`. + /// + /// This method is internal to the semantic-index submodule. + /// It *only* checks that the AST structure of the `Place` is + /// correct. It does not check whether the `Place` actually occurred in + /// a method context, or whether the `` actually refers to the first + /// parameter of the method (i.e. `self`). To answer those questions, + /// use [`Self::as_instance_attribute`]. + pub(super) fn as_instance_attribute_candidate(&self) -> Option<&Name> { + if self.expr.sub_segments.len() == 1 { + self.expr.sub_segments[0].as_member() + } else { + None + } + } + + /// Return `true` if the place expression has the form `.`, + /// indicating that it *may* be an instance attribute if we are in a method context. + /// + /// This method is internal to the semantic-index submodule. + /// It *only* checks that the AST structure of the `Place` is + /// correct. It does not check whether the `Place` actually occurred in + /// a method context, or whether the `` actually refers to the first + /// parameter of the method (i.e. `self`). To answer those questions, + /// use [`Self::is_instance_attribute`]. + pub(super) fn is_instance_attribute_candidate(&self) -> bool { + self.as_instance_attribute_candidate().is_some() + } + + /// Does the place expression have the form `self.{name}` (`self` is the first parameter of the method)? + pub(super) fn is_instance_attribute_named(&self, name: &str) -> bool { + self.as_instance_attribute().map(Name::as_str) == Some(name) + } + + /// Return `Some()` if the place expression is an instance attribute. + pub(crate) fn as_instance_attribute(&self) -> Option<&Name> { + if self.is_instance_attribute() { + debug_assert!(self.as_instance_attribute_candidate().is_some()); + self.as_instance_attribute_candidate() + } else { + None + } + } + + /// Is the place an instance attribute? + pub(crate) fn is_instance_attribute(&self) -> bool { + let is_instance_attribute = self.flags.contains(PlaceFlags::IS_INSTANCE_ATTRIBUTE); + if is_instance_attribute { + debug_assert!(self.is_instance_attribute_candidate()); + } + is_instance_attribute + } + + pub(crate) fn is_name(&self) -> bool { + self.expr.is_name() + } + + pub(crate) fn is_member(&self) -> bool { + self.expr.is_member() + } + + /// Is the place used in its containing scope? + pub fn is_used(&self) -> bool { + self.flags.contains(PlaceFlags::IS_USED) + } + + /// Is the place defined in its containing scope? + pub fn is_bound(&self) -> bool { + self.flags.contains(PlaceFlags::IS_BOUND) + } + + /// Is the place declared in its containing scope? + pub fn is_declared(&self) -> bool { + self.flags.contains(PlaceFlags::IS_DECLARED) + } + + pub(crate) fn as_name(&self) -> Option<&Name> { + self.expr.as_name() + } + + pub(crate) fn expect_name(&self) -> &Name { + self.expr.expect_name() + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] +pub struct PlaceExprRef<'a> { + pub(crate) root_name: &'a Name, + /// Sub-segments is empty for a simple target (e.g. `foo`). + pub(crate) sub_segments: &'a [PlaceExprSubSegment], +} + +impl PartialEq for PlaceExprRef<'_> { + fn eq(&self, other: &PlaceExpr) -> bool { + self.root_name == &other.root_name && self.sub_segments == &other.sub_segments[..] + } +} + +impl PartialEq> for PlaceExpr { + fn eq(&self, other: &PlaceExprRef<'_>) -> bool { + &self.root_name == other.root_name && &self.sub_segments[..] == other.sub_segments + } +} + +impl<'e> From<&'e PlaceExpr> for PlaceExprRef<'e> { + fn from(expr: &'e PlaceExpr) -> Self { + PlaceExprRef { + root_name: &expr.root_name, + sub_segments: &expr.sub_segments, + } + } +} + +struct RootExprs<'e> { + expr_ref: PlaceExprRef<'e>, + len: usize, +} + +impl<'e> Iterator for RootExprs<'e> { + type Item = PlaceExprRef<'e>; + + fn next(&mut self) -> Option { + if self.len == 0 { + return None; + } + self.len -= 1; + Some(PlaceExprRef { + root_name: self.expr_ref.root_name, + sub_segments: &self.expr_ref.sub_segments[..self.len], + }) + } +} + +bitflags! { + /// Flags that can be queried to obtain information about a place in a given scope. + /// + /// See the doc-comment at the top of [`super::use_def`] for explanations of what it + /// means for a place to be *bound* as opposed to *declared*. + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + struct PlaceFlags: u8 { + const IS_USED = 1 << 0; + const IS_BOUND = 1 << 1; + const IS_DECLARED = 1 << 2; + /// TODO: This flag is not yet set by anything + const MARKED_GLOBAL = 1 << 3; + /// TODO: This flag is not yet set by anything + const MARKED_NONLOCAL = 1 << 4; + const IS_INSTANCE_ATTRIBUTE = 1 << 5; + } +} + +impl get_size2::GetSize for PlaceFlags {} + +/// ID that uniquely identifies a place in a file. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct FilePlaceId { + scope: FileScopeId, + scoped_place_id: ScopedPlaceId, +} + +impl FilePlaceId { + pub fn scope(self) -> FileScopeId { + self.scope + } + + pub(crate) fn scoped_place_id(self) -> ScopedPlaceId { + self.scoped_place_id + } +} + +impl From for ScopedPlaceId { + fn from(val: FilePlaceId) -> Self { + val.scoped_place_id() + } +} + +/// ID that uniquely identifies a place inside a [`Scope`]. +#[newtype_index] +#[derive(salsa::Update, get_size2::GetSize)] +pub struct ScopedPlaceId; + +/// A cross-module identifier of a scope that can be used as a salsa query parameter. +#[salsa::tracked(debug)] +pub struct ScopeId<'db> { + pub file: File, + + pub file_scope_id: FileScopeId, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for ScopeId<'_> {} + +impl<'db> ScopeId<'db> { + pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { + self.node(db).scope_kind().is_function_like() + } + + pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { + self.node(db).scope_kind().is_type_parameter() + } + + pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind { + self.scope(db).node() + } + + pub(crate) fn scope(self, db: &dyn Db) -> &Scope { + semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) + } + + #[cfg(test)] + pub(crate) fn name<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast str { + match self.node(db) { + NodeWithScopeKind::Module => "", + NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => { + class.node(module).name.as_str() + } + NodeWithScopeKind::Function(function) + | NodeWithScopeKind::FunctionTypeParameters(function) => { + function.node(module).name.as_str() + } + NodeWithScopeKind::TypeAlias(type_alias) + | NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias + .node(module) + .name + .as_name_expr() + .map(|name| name.id.as_str()) + .unwrap_or(""), + NodeWithScopeKind::Lambda(_) => "", + NodeWithScopeKind::ListComprehension(_) => "", + NodeWithScopeKind::SetComprehension(_) => "", + NodeWithScopeKind::DictComprehension(_) => "", + NodeWithScopeKind::GeneratorExpression(_) => "", + } + } +} + +/// ID that uniquely identifies a scope inside of a module. +#[newtype_index] +#[derive(salsa::Update, get_size2::GetSize)] +pub struct FileScopeId; + +impl FileScopeId { + /// Returns the scope id of the module-global scope. + pub fn global() -> Self { + FileScopeId::from_u32(0) + } + + pub fn is_global(self) -> bool { + self == FileScopeId::global() + } + + pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { + let index = semantic_index(db, file); + index.scope_ids_by_scope[self] + } + + pub(crate) fn is_generator_function(self, index: &SemanticIndex) -> bool { + index.generator_functions.contains(&self) + } +} + +#[derive(Debug, salsa::Update, get_size2::GetSize)] +pub struct Scope { + parent: Option, + node: NodeWithScopeKind, + descendants: Range, + reachability: ScopedReachabilityConstraintId, +} + +impl Scope { + pub(super) fn new( + parent: Option, + node: NodeWithScopeKind, + descendants: Range, + reachability: ScopedReachabilityConstraintId, + ) -> Self { + Scope { + parent, + node, + descendants, + reachability, + } + } + + pub fn parent(&self) -> Option { + self.parent + } + + pub fn node(&self) -> &NodeWithScopeKind { + &self.node + } + + pub fn kind(&self) -> ScopeKind { + self.node().scope_kind() + } + + pub fn descendants(&self) -> Range { + self.descendants.clone() + } + + pub(super) fn extend_descendants(&mut self, children_end: FileScopeId) { + self.descendants = self.descendants.start..children_end; + } + + pub(crate) fn is_eager(&self) -> bool { + self.kind().is_eager() + } + + pub(crate) fn reachability(&self) -> ScopedReachabilityConstraintId { + self.reachability + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ScopeKind { + Module, + Annotation, + Class, + Function, + Lambda, + Comprehension, + TypeAlias, +} + +impl ScopeKind { + pub(crate) const fn is_eager(self) -> bool { + match self { + ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true, + ScopeKind::Annotation + | ScopeKind::Function + | ScopeKind::Lambda + | ScopeKind::TypeAlias => false, + } + } + + pub(crate) const fn is_function_like(self) -> bool { + // Type parameter scopes behave like function scopes in terms of name resolution; CPython + // place table also uses the term "function-like" for these scopes. + matches!( + self, + ScopeKind::Annotation + | ScopeKind::Function + | ScopeKind::Lambda + | ScopeKind::TypeAlias + | ScopeKind::Comprehension + ) + } + + pub(crate) const fn is_class(self) -> bool { + matches!(self, ScopeKind::Class) + } + + pub(crate) const fn is_type_parameter(self) -> bool { + matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) + } + + pub(crate) const fn is_non_lambda_function(self) -> bool { + matches!(self, ScopeKind::Function) + } +} + +/// [`PlaceExpr`] table for a specific [`Scope`]. +#[derive(Default, get_size2::GetSize)] +pub struct PlaceTable { + /// The place expressions in this scope. + places: IndexVec, + + /// The set of places. + place_set: PlaceSet, +} + +impl PlaceTable { + fn shrink_to_fit(&mut self) { + self.places.shrink_to_fit(); + } + + pub(crate) fn place_expr(&self, place_id: impl Into) -> &PlaceExprWithFlags { + &self.places[place_id.into()] + } + + /// Iterate over the "root" expressions of the place (e.g. `x.y.z`, `x.y`, `x` for `x.y.z[0]`). + pub(crate) fn root_place_exprs( + &self, + place_expr: &PlaceExpr, + ) -> impl Iterator { + place_expr + .root_exprs() + .filter_map(|place_expr| self.place_by_expr(place_expr)) + } + + #[expect(unused)] + pub(crate) fn place_ids(&self) -> impl Iterator { + self.places.indices() + } + + pub fn places(&self) -> impl Iterator { + self.places.iter() + } + + pub fn symbols(&self) -> impl Iterator { + self.places().filter(|place_expr| place_expr.is_name()) + } + + pub fn instance_attributes(&self) -> impl Iterator { + self.places() + .filter_map(|place_expr| place_expr.as_instance_attribute()) + } + + /// Returns the place named `name`. + #[allow(unused)] // used in tests + pub(crate) fn place_by_name(&self, name: &str) -> Option<&PlaceExprWithFlags> { + let id = self.place_id_by_name(name)?; + Some(self.place_expr(id)) + } + + /// Returns the flagged place. + pub(crate) fn place_by_expr<'e>( + &self, + place_expr: impl Into>, + ) -> Option<&PlaceExprWithFlags> { + let id = self.place_id_by_expr(place_expr)?; + Some(self.place_expr(id)) + } + + /// Returns the [`ScopedPlaceId`] of the place named `name`. + pub(crate) fn place_id_by_name(&self, name: &str) -> Option { + self.place_set + .find(Self::hash_name(name), |id| { + self.place_expr(*id).as_name().map(Name::as_str) == Some(name) + }) + .copied() + } + + /// Returns the [`ScopedPlaceId`] of the place expression. + pub(crate) fn place_id_by_expr<'e>( + &self, + place_expr: impl Into>, + ) -> Option { + let place_expr = place_expr.into(); + self.place_set + .find(Self::hash_place_expr(place_expr), |id| { + self.place_expr(*id).expr == place_expr + }) + .copied() + } + + pub(crate) fn place_id_by_instance_attribute_name(&self, name: &str) -> Option { + self.places + .indices() + .find(|id| self.places[*id].is_instance_attribute_named(name)) + } + + fn hash_name(name: &str) -> u64 { + let mut hasher = FxHasher::default(); + name.hash(&mut hasher); + hasher.finish() + } + + fn hash_place_expr<'e>(place_expr: impl Into>) -> u64 { + let place_expr: PlaceExprRef = place_expr.into(); + + let mut hasher = FxHasher::default(); + + // Special case for simple names (e.g. "foo"). Only hash the name so + // that a lookup by name can find it (see `place_by_name`). + if place_expr.sub_segments.is_empty() { + place_expr.root_name.as_str().hash(&mut hasher); + } else { + place_expr.hash(&mut hasher); + } + hasher.finish() + } +} + +impl PartialEq for PlaceTable { + fn eq(&self, other: &Self) -> bool { + // We don't need to compare the place_set because the place is already captured in `PlaceExpr`. + self.places == other.places + } +} + +impl Eq for PlaceTable {} + +impl std::fmt::Debug for PlaceTable { + /// Exclude the `place_set` field from the debug output. + /// It's very noisy and not useful for debugging. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PlaceTable") + .field(&self.places) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Default)] +pub(super) struct PlaceTableBuilder { + table: PlaceTable, + + associated_place_ids: IndexVec>, +} + +impl PlaceTableBuilder { + pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedPlaceId, bool) { + let hash = PlaceTable::hash_name(&name); + let entry = self.table.place_set.entry( + hash, + |id| self.table.places[*id].as_name() == Some(&name), + |id| PlaceTable::hash_place_expr(&self.table.places[*id].expr), + ); + + match entry { + Entry::Occupied(entry) => (*entry.get(), false), + Entry::Vacant(entry) => { + let symbol = PlaceExprWithFlags::name(name); + + let id = self.table.places.push(symbol); + entry.insert(id); + let new_id = self.associated_place_ids.push(vec![]); + debug_assert_eq!(new_id, id); + (id, true) + } + } + } + + pub(super) fn add_place(&mut self, place_expr: PlaceExprWithFlags) -> (ScopedPlaceId, bool) { + let hash = PlaceTable::hash_place_expr(&place_expr.expr); + let entry = self.table.place_set.entry( + hash, + |id| self.table.places[*id].expr == place_expr.expr, + |id| PlaceTable::hash_place_expr(&self.table.places[*id].expr), + ); + + match entry { + Entry::Occupied(entry) => (*entry.get(), false), + Entry::Vacant(entry) => { + let id = self.table.places.push(place_expr); + entry.insert(id); + let new_id = self.associated_place_ids.push(vec![]); + debug_assert_eq!(new_id, id); + for root in self.table.places[id].expr.root_exprs() { + if let Some(root_id) = self.table.place_id_by_expr(root) { + self.associated_place_ids[root_id].push(id); + } + } + (id, true) + } + } + } + + pub(super) fn mark_place_bound(&mut self, id: ScopedPlaceId) { + self.table.places[id].insert_flags(PlaceFlags::IS_BOUND); + } + + pub(super) fn mark_place_declared(&mut self, id: ScopedPlaceId) { + self.table.places[id].insert_flags(PlaceFlags::IS_DECLARED); + } + + pub(super) fn mark_place_used(&mut self, id: ScopedPlaceId) { + self.table.places[id].insert_flags(PlaceFlags::IS_USED); + } + + pub(super) fn places(&self) -> impl Iterator { + self.table.places() + } + + pub(super) fn place_id_by_expr(&self, place_expr: &PlaceExpr) -> Option { + self.table.place_id_by_expr(place_expr) + } + + pub(super) fn place_expr(&self, place_id: impl Into) -> &PlaceExprWithFlags { + self.table.place_expr(place_id) + } + + /// Returns the place IDs associated with the place (e.g. `x.y`, `x.y.z`, `x.y.z[0]` for `x`). + pub(super) fn associated_place_ids( + &self, + place: ScopedPlaceId, + ) -> impl Iterator { + self.associated_place_ids[place].iter().copied() + } + + pub(super) fn finish(mut self) -> PlaceTable { + self.table.shrink_to_fit(); + self.table + } +} + +/// Reference to a node that introduces a new scope. +#[derive(Copy, Clone, Debug)] +pub(crate) enum NodeWithScopeRef<'a> { + Module, + Class(&'a ast::StmtClassDef), + Function(&'a ast::StmtFunctionDef), + Lambda(&'a ast::ExprLambda), + FunctionTypeParameters(&'a ast::StmtFunctionDef), + ClassTypeParameters(&'a ast::StmtClassDef), + TypeAlias(&'a ast::StmtTypeAlias), + TypeAliasTypeParameters(&'a ast::StmtTypeAlias), + ListComprehension(&'a ast::ExprListComp), + SetComprehension(&'a ast::ExprSetComp), + DictComprehension(&'a ast::ExprDictComp), + GeneratorExpression(&'a ast::ExprGenerator), +} + +impl NodeWithScopeRef<'_> { + /// Converts the unowned reference to an owned [`NodeWithScopeKind`]. + /// + /// Note that node wrapped by `self` must be a child of `module`. + pub(super) fn to_kind(self, module: &ParsedModuleRef) -> NodeWithScopeKind { + match self { + NodeWithScopeRef::Module => NodeWithScopeKind::Module, + NodeWithScopeRef::Class(class) => { + NodeWithScopeKind::Class(AstNodeRef::new(module, class)) + } + NodeWithScopeRef::Function(function) => { + NodeWithScopeKind::Function(AstNodeRef::new(module, function)) + } + NodeWithScopeRef::TypeAlias(type_alias) => { + NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias)) + } + NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { + NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias)) + } + NodeWithScopeRef::Lambda(lambda) => { + NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda)) + } + NodeWithScopeRef::FunctionTypeParameters(function) => { + NodeWithScopeKind::FunctionTypeParameters(AstNodeRef::new(module, function)) + } + NodeWithScopeRef::ClassTypeParameters(class) => { + NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class)) + } + NodeWithScopeRef::ListComprehension(comprehension) => { + NodeWithScopeKind::ListComprehension(AstNodeRef::new(module, comprehension)) + } + NodeWithScopeRef::SetComprehension(comprehension) => { + NodeWithScopeKind::SetComprehension(AstNodeRef::new(module, comprehension)) + } + NodeWithScopeRef::DictComprehension(comprehension) => { + NodeWithScopeKind::DictComprehension(AstNodeRef::new(module, comprehension)) + } + NodeWithScopeRef::GeneratorExpression(generator) => { + NodeWithScopeKind::GeneratorExpression(AstNodeRef::new(module, generator)) + } + } + } + + pub(crate) fn node_key(self) -> NodeWithScopeKey { + match self { + NodeWithScopeRef::Module => NodeWithScopeKey::Module, + NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), + NodeWithScopeRef::Function(function) => { + NodeWithScopeKey::Function(NodeKey::from_node(function)) + } + NodeWithScopeRef::Lambda(lambda) => { + NodeWithScopeKey::Lambda(NodeKey::from_node(lambda)) + } + NodeWithScopeRef::FunctionTypeParameters(function) => { + NodeWithScopeKey::FunctionTypeParameters(NodeKey::from_node(function)) + } + NodeWithScopeRef::ClassTypeParameters(class) => { + NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class)) + } + NodeWithScopeRef::TypeAlias(type_alias) => { + NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias)) + } + NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => { + NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias)) + } + NodeWithScopeRef::ListComprehension(comprehension) => { + NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension)) + } + NodeWithScopeRef::SetComprehension(comprehension) => { + NodeWithScopeKey::SetComprehension(NodeKey::from_node(comprehension)) + } + NodeWithScopeRef::DictComprehension(comprehension) => { + NodeWithScopeKey::DictComprehension(NodeKey::from_node(comprehension)) + } + NodeWithScopeRef::GeneratorExpression(generator) => { + NodeWithScopeKey::GeneratorExpression(NodeKey::from_node(generator)) + } + } + } +} + +/// Node that introduces a new scope. +#[derive(Clone, Debug, salsa::Update, get_size2::GetSize)] +pub enum NodeWithScopeKind { + Module, + Class(AstNodeRef), + ClassTypeParameters(AstNodeRef), + Function(AstNodeRef), + FunctionTypeParameters(AstNodeRef), + TypeAliasTypeParameters(AstNodeRef), + TypeAlias(AstNodeRef), + Lambda(AstNodeRef), + ListComprehension(AstNodeRef), + SetComprehension(AstNodeRef), + DictComprehension(AstNodeRef), + GeneratorExpression(AstNodeRef), +} + +impl NodeWithScopeKind { + pub(crate) const fn scope_kind(&self) -> ScopeKind { + match self { + Self::Module => ScopeKind::Module, + Self::Class(_) => ScopeKind::Class, + Self::Function(_) => ScopeKind::Function, + Self::Lambda(_) => ScopeKind::Lambda, + Self::FunctionTypeParameters(_) + | Self::ClassTypeParameters(_) + | Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation, + Self::TypeAlias(_) => ScopeKind::TypeAlias, + Self::ListComprehension(_) + | Self::SetComprehension(_) + | Self::DictComprehension(_) + | Self::GeneratorExpression(_) => ScopeKind::Comprehension, + } + } + + pub fn expect_class<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { + match self { + Self::Class(class) => class.node(module), + _ => panic!("expected class"), + } + } + + pub(crate) fn as_class<'ast>( + &self, + module: &'ast ParsedModuleRef, + ) -> Option<&'ast ast::StmtClassDef> { + match self { + Self::Class(class) => Some(class.node(module)), + _ => None, + } + } + + pub fn expect_function<'ast>( + &self, + module: &'ast ParsedModuleRef, + ) -> &'ast ast::StmtFunctionDef { + self.as_function(module).expect("expected function") + } + + pub fn expect_type_alias<'ast>( + &self, + module: &'ast ParsedModuleRef, + ) -> &'ast ast::StmtTypeAlias { + match self { + Self::TypeAlias(type_alias) => type_alias.node(module), + _ => panic!("expected type alias"), + } + } + + pub fn as_function<'ast>( + &self, + module: &'ast ParsedModuleRef, + ) -> Option<&'ast ast::StmtFunctionDef> { + match self { + Self::Function(function) => Some(function.node(module)), + _ => None, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] +pub(crate) enum NodeWithScopeKey { + Module, + Class(NodeKey), + ClassTypeParameters(NodeKey), + Function(NodeKey), + FunctionTypeParameters(NodeKey), + TypeAlias(NodeKey), + TypeAliasTypeParameters(NodeKey), + Lambda(NodeKey), + ListComprehension(NodeKey), + SetComprehension(NodeKey), + DictComprehension(NodeKey), + GeneratorExpression(NodeKey), +} diff --git a/crates/ty_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs new file mode 100644 index 0000000000000..3b67e5871e90e --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/predicate.rs @@ -0,0 +1,228 @@ +//! _Predicates_ are Python expressions whose runtime values can affect type inference. +//! +//! We currently use predicates in two places: +//! +//! - [_Narrowing constraints_][crate::semantic_index::narrowing_constraints] constrain the type of +//! a binding that is visible at a particular use. +//! - [_Reachability constraints_][crate::semantic_index::reachability_constraints] determine the +//! static reachability of a binding, and the reachability of a statement or expression. + +use ruff_db::files::File; +use ruff_index::{Idx, IndexVec}; +use ruff_python_ast::Singleton; + +use crate::db::Db; +use crate::semantic_index::expression::Expression; +use crate::semantic_index::global_scope; +use crate::semantic_index::place::{FileScopeId, ScopeId, ScopedPlaceId}; + +// A scoped identifier for each `Predicate` in a scope. +#[derive(Clone, Debug, Copy, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)] +pub(crate) struct ScopedPredicateId(u32); + +impl ScopedPredicateId { + /// A special ID that is used for an "always true" predicate. + pub(crate) const ALWAYS_TRUE: ScopedPredicateId = ScopedPredicateId(0xffff_ffff); + + /// A special ID that is used for an "always false" predicate. + pub(crate) const ALWAYS_FALSE: ScopedPredicateId = ScopedPredicateId(0xffff_fffe); + + const SMALLEST_TERMINAL: ScopedPredicateId = Self::ALWAYS_FALSE; + + fn is_terminal(self) -> bool { + self >= Self::SMALLEST_TERMINAL + } + + #[cfg(test)] + pub(crate) fn as_u32(self) -> u32 { + self.0 + } +} + +impl Idx for ScopedPredicateId { + #[inline] + fn new(value: usize) -> Self { + assert!(value <= (Self::SMALLEST_TERMINAL.0 as usize)); + #[expect(clippy::cast_possible_truncation)] + Self(value as u32) + } + + #[inline] + fn index(self) -> usize { + debug_assert!(!self.is_terminal()); + self.0 as usize + } +} + +// A collection of predicates for a given scope. +pub(crate) type Predicates<'db> = IndexVec>; + +#[derive(Debug, Default)] +pub(crate) struct PredicatesBuilder<'db> { + predicates: IndexVec>, +} + +impl<'db> PredicatesBuilder<'db> { + /// Adds a predicate. Note that we do not deduplicate predicates. If you add a `Predicate` + /// more than once, you will get distinct `ScopedPredicateId`s for each one. (This lets you + /// model predicates that might evaluate to different values at different points of execution.) + pub(crate) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { + self.predicates.push(predicate) + } + + pub(crate) fn build(mut self) -> Predicates<'db> { + self.predicates.shrink_to_fit(); + self.predicates + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct Predicate<'db> { + pub(crate) node: PredicateNode<'db>, + pub(crate) is_positive: bool, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) enum PredicateOrLiteral<'db> { + Literal(bool), + Predicate(Predicate<'db>), +} + +impl PredicateOrLiteral<'_> { + pub(crate) fn negated(self) -> Self { + match self { + PredicateOrLiteral::Literal(value) => PredicateOrLiteral::Literal(!value), + PredicateOrLiteral::Predicate(Predicate { node, is_positive }) => { + PredicateOrLiteral::Predicate(Predicate { + node, + is_positive: !is_positive, + }) + } + } + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct CallableAndCallExpr<'db> { + pub(crate) callable: Expression<'db>, + pub(crate) call_expr: Expression<'db>, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) enum PredicateNode<'db> { + Expression(Expression<'db>), + ReturnsNever(CallableAndCallExpr<'db>), + Pattern(PatternPredicate<'db>), + StarImportPlaceholder(StarImportPlaceholderPredicate<'db>), +} + +/// Pattern kinds for which we support type narrowing and/or static reachability analysis. +#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)] +pub(crate) enum PatternPredicateKind<'db> { + Singleton(Singleton), + Value(Expression<'db>), + Or(Vec>), + Class(Expression<'db>), + Unsupported, +} + +#[salsa::tracked(debug)] +pub(crate) struct PatternPredicate<'db> { + pub(crate) file: File, + + pub(crate) file_scope: FileScopeId, + + pub(crate) subject: Expression<'db>, + + #[returns(ref)] + pub(crate) kind: PatternPredicateKind<'db>, + + pub(crate) guard: Option>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for PatternPredicate<'_> {} + +impl<'db> PatternPredicate<'db> { + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.file_scope(db).to_scope_id(db, self.file(db)) + } +} + +/// A "placeholder predicate" that is used to model the fact that the boundness of a +/// (possible) definition or declaration caused by a `*` import cannot be fully determined +/// until type-inference time. This is essentially the same as a standard reachability constraint, +/// so we reuse the [`Predicate`] infrastructure to model it. +/// +/// To illustrate, say we have a module `exporter.py` like so: +/// +/// ```py +/// if : +/// class A: ... +/// ``` +/// +/// and we have a module `importer.py` like so: +/// +/// ```py +/// A = 1 +/// +/// from importer import * +/// ``` +/// +/// Since we cannot know whether or not is true at semantic-index time, +/// we record a definition for `A` in `b.py` as a result of the `from a import *` +/// statement, but place a predicate on it to record the fact that we don't yet +/// know whether this definition will be visible from all control-flow paths or not. +/// Essentially, we model `b.py` as something similar to this: +/// +/// ```py +/// A = 1 +/// +/// if : +/// from a import A +/// ``` +/// +/// At type-check time, the placeholder predicate for the `A` definition is evaluated by +/// attempting to resolve the `A` symbol in `a.py`'s global namespace: +/// - If it resolves to a definitely bound symbol, then the predicate resolves to [`Truthiness::AlwaysTrue`] +/// - If it resolves to an unbound symbol, then the predicate resolves to [`Truthiness::AlwaysFalse`] +/// - If it resolves to a possibly bound symbol, then the predicate resolves to [`Truthiness::Ambiguous`] +/// +/// [Truthiness]: [crate::types::Truthiness] +#[salsa::tracked(debug)] +pub(crate) struct StarImportPlaceholderPredicate<'db> { + pub(crate) importing_file: File, + + /// Each symbol imported by a `*` import has a separate predicate associated with it: + /// this field identifies which symbol that is. + /// + /// Note that a [`ScopedPlaceId`] is only meaningful if you also know the scope + /// it is relative to. For this specific struct, however, there's no need to store a + /// separate field to hold the ID of the scope. `StarImportPredicate`s are only created + /// for valid `*`-import definitions, and valid `*`-import definitions can only ever + /// exist in the global scope; thus, we know that the `symbol_id` here will be relative + /// to the global scope of the importing file. + pub(crate) symbol_id: ScopedPlaceId, + + pub(crate) referenced_file: File, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for StarImportPlaceholderPredicate<'_> {} + +impl<'db> StarImportPlaceholderPredicate<'db> { + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + // See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]: + // valid `*`-import definitions can only take place in the global scope. + global_scope(db, self.importing_file(db)) + } +} + +impl<'db> From> for PredicateOrLiteral<'db> { + fn from(predicate: StarImportPlaceholderPredicate<'db>) -> Self { + PredicateOrLiteral::Predicate(Predicate { + node: PredicateNode::StarImportPlaceholder(predicate), + is_positive: true, + }) + } +} diff --git a/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs b/crates/ty_python_semantic/src/semantic_index/re_exports.rs similarity index 91% rename from crates/red_knot_python_semantic/src/semantic_index/re_exports.rs rename to crates/ty_python_semantic/src/semantic_index/re_exports.rs index f16819f32980f..70f76c370755a 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/ty_python_semantic/src/semantic_index/re_exports.rs @@ -24,11 +24,11 @@ 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}, + visitor::{Visitor, walk_expr, walk_pattern, walk_stmt}, }; use rustc_hash::FxHashMap; -use crate::{module_name::ModuleName, resolve_module, Db}; +use crate::{Db, module_name::ModuleName, resolve_module}; fn exports_cycle_recover( _db: &dyn Db, @@ -43,9 +43,9 @@ fn exports_cycle_initial(_db: &dyn Db, _file: File) -> Box<[Name]> { Box::default() } -#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)] +#[salsa::tracked(returns(deref), cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { - let module = parsed_module(db.upcast(), file); + let module = parsed_module(db, file).load(db); let mut finder = ExportFinder::new(db, file); finder.visit_body(module.suite()); finder.resolve_exports() @@ -64,7 +64,7 @@ impl<'db> ExportFinder<'db> { Self { db, file, - visiting_stub_file: file.is_stub(db.upcast()), + visiting_stub_file: file.is_stub(db), exports: FxHashMap::default(), dunder_all: DunderAll::NotPresent, } @@ -104,6 +104,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { name, asname, range: _, + node_index: _, } = alias; let name = &name.id; @@ -126,6 +127,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { pattern, name, range: _, + node_index: _, }) => { if let Some(pattern) = pattern { self.visit_pattern(pattern); @@ -145,6 +147,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { rest, keys: _, range: _, + node_index: _, }) => { for pattern in patterns { self.visit_pattern(pattern); @@ -153,7 +156,11 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { self.possibly_add_export(&rest.id, PossibleExportKind::Normal); } } - ast::Pattern::MatchStar(ast::PatternMatchStar { name, range: _ }) => { + ast::Pattern::MatchStar(ast::PatternMatchStar { + name, + range: _, + node_index: _, + }) => { if let Some(name) = name { self.possibly_add_export(&name.id, PossibleExportKind::Normal); } @@ -176,6 +183,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { 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: _, + node_index: _, }) => { self.possibly_add_export(&name.id, PossibleExportKind::Normal); for decorator in decorator_list { @@ -194,6 +202,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { 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: _, + node_index: _, is_async: _, }) => { self.possibly_add_export(&name.id, PossibleExportKind::Normal); @@ -212,6 +221,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { annotation, simple: _, range: _, + node_index: _, }) => { if value.is_some() || self.visiting_stub_file { self.visit_expr(target); @@ -227,6 +237,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { type_params: _, value: _, range: _, + node_index: _, }) => { self.visit_expr(name); // Neither walrus expressions nor statements cannot appear in type aliases; @@ -244,7 +255,12 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { .ok() .and_then(|module_name| resolve_module(self.db, &module_name)) .iter() - .flat_map(|module| exported_names(self.db, module.file())) + .flat_map(|module| { + module + .file() + .map(|file| exported_names(self.db, file)) + .unwrap_or_default() + }) { self.possibly_add_export(export, PossibleExportKind::Normal); } @@ -281,7 +297,12 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { fn visit_expr(&mut self, expr: &'db ast::Expr) { match expr { - ast::Expr::Name(ast::ExprName { id, ctx, range: _ }) => { + ast::Expr::Name(ast::ExprName { + id, + ctx, + range: _, + node_index: _, + }) => { if ctx.is_store() { self.possibly_add_export(id, PossibleExportKind::Normal); } @@ -320,6 +341,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { | ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) | ast::Expr::FString(_) + | ast::Expr::TString(_) | ast::Expr::Tuple(_) | ast::Expr::List(_) | ast::Expr::Slice(_) @@ -353,11 +375,13 @@ impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> { target, value: _, range: _, + node_index: _, }) => { if let ast::Expr::Name(ast::ExprName { id, ctx: ast::ExprContext::Store, range: _, + node_index: _, }) = &**target { self.export_finder @@ -384,6 +408,7 @@ impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> { | ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) | ast::Expr::FString(_) + | ast::Expr::TString(_) | ast::Expr::Tuple(_) | ast::Expr::List(_) | ast::Expr::Slice(_) diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs new file mode 100644 index 0000000000000..9781ebc8e0da0 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -0,0 +1,773 @@ +//! # Reachability constraints +//! +//! During semantic index building, we record so-called reachability constraints that keep track +//! of a set of conditions that need to apply in order for a certain statement or expression to +//! be reachable from the start of the scope. As an example, consider the following situation where +//! we have just processed two `if`-statements: +//! ```py +//! if test: +//! +//! ``` +//! In this case, we would record a reachability constraint of `test`, which would later allow us +//! to re-analyze the control flow during type-checking, once we actually know the static truthiness +//! of `test`. When evaluating a constraint, there are three possible outcomes: always true, always +//! false, or ambiguous. For a simple constraint like this, always-true and always-false correspond +//! to the case in which we can infer that the type of `test` is `Literal[True]` or `Literal[False]`. +//! In any other case, like if the type of `test` is `bool` or `Unknown`, we can not statically +//! determine whether `test` is truthy or falsy, so the outcome would be "ambiguous". +//! +//! +//! ## Sequential constraints (ternary AND) +//! +//! Whenever control flow branches, we record reachability constraints. If we already have a +//! constraint, we create a new one using a ternary AND operation. Consider the following example: +//! ```py +//! if test1: +//! if test2: +//! +//! ``` +//! Here, we would accumulate a reachability constraint of `test1 AND test2`. We can statically +//! determine that this position is *always* reachable only if both `test1` and `test2` are +//! always true. On the other hand, we can statically determine that this position is *never* +//! reachable if *either* `test1` or `test2` is always false. In any other case, we can not +//! determine whether this position is reachable or not, so the outcome is "ambiguous". This +//! corresponds to a ternary *AND* operation in [Kleene] logic: +//! +//! ```text +//! | AND | always-false | ambiguous | always-true | +//! |--------------|--------------|--------------|--------------| +//! | always false | always-false | always-false | always-false | +//! | ambiguous | always-false | ambiguous | ambiguous | +//! | always true | always-false | ambiguous | always-true | +//! ``` +//! +//! +//! ## Merged constraints (ternary OR) +//! +//! We also need to consider the case where control flow merges again. Consider a case like this: +//! ```py +//! def _(): +//! if test1: +//! pass +//! elif test2: +//! pass +//! else: +//! return +//! +//! +//! ``` +//! Here, the first branch has a `test1` constraint, and the second branch has a `test2` constraint. +//! The third branch ends in a terminal statement [^1]. When we merge control flow, we need to consider +//! the reachability through either the first or the second branch. The current position is only +//! *definitely* unreachable if both `test1` and `test2` are always false. It is definitely +//! reachable if *either* `test1` or `test2` is always true. In any other case, we can not statically +//! determine whether it is reachable or not. This operation corresponds to a ternary *OR* operation: +//! +//! ```text +//! | OR | always-false | ambiguous | always-true | +//! |--------------|--------------|--------------|--------------| +//! | always false | always-false | ambiguous | always-true | +//! | ambiguous | ambiguous | ambiguous | always-true | +//! | always true | always-true | always-true | always-true | +//! ``` +//! +//! [^1]: What's actually happening here is that we merge all three branches using a ternary OR. The +//! third branch has a reachability constraint of `always-false`, and `t OR always-false` is equal +//! to `t` (see first column in that table), so it was okay to omit the third branch in the discussion +//! above. +//! +//! +//! ## Negation +//! +//! Control flow elements like `if-elif-else` or `match` statements can also lead to negated +//! constraints. For example, we record a constraint of `~test` for the `else` branch here: +//! ```py +//! if test: +//! pass +//! else: +//! +//! ``` +//! +//! ## Explicit ambiguity +//! +//! In some cases, we explicitly record an “ambiguous” constraint. We do this when branching on +//! something that we can not (or intentionally do not want to) analyze statically. `for` loops are +//! one example: +//! ```py +//! def _(): +//! for _ in range(2): +//! return +//! +//! +//! ``` +//! If we would not record any constraints at the branching point, we would have an `always-true` +//! reachability for the no-loop branch, and a `always-false` reachability for the branch which enters +//! the loop. Merging those would lead to a reachability of `always-true OR always-false = always-true`, +//! i.e. we would consider the end of the scope to be unconditionally reachable, which is not correct. +//! +//! Recording an ambiguous constraint at the branching point modifies the constraints in both branches to +//! `always-true AND ambiguous = ambiguous` and `always-false AND ambiguous = always-false`, respectively. +//! Merging these two using OR correctly leads to `ambiguous` for the end-of-scope reachability. +//! +//! +//! ## Reachability constraints and bindings +//! +//! To understand how reachability constraints apply to bindings in particular, consider the following +//! example: +//! ```py +//! x = # not a live binding for the use of x below, shadowed by `x = 1` +//! y = # reachability constraint: ~test +//! +//! x = 1 # reachability constraint: ~test +//! if test: +//! x = 2 # reachability constraint: test +//! +//! y = 2 # reachability constraint: test +//! +//! use(x) +//! use(y) +//! ``` +//! Both the type and the boundness of `x` and `y` are affected by reachability constraints: +//! +//! ```text +//! | `test` truthiness | type of `x` | boundness of `y` | +//! |-------------------|-----------------|------------------| +//! | always false | `Literal[1]` | unbound | +//! | ambiguous | `Literal[1, 2]` | possibly unbound | +//! | always true | `Literal[2]` | bound | +//! ``` +//! +//! To achieve this, we apply reachability constraints retroactively to bindings that came before +//! the branching point. In the example above, the `x = 1` binding has a `test` constraint in the +//! `if` branch, and a `~test` constraint in the implicit `else` branch. Since it is shadowed by +//! `x = 2` in the `if` branch, we are only left with the `~test` constraint after control flow +//! has merged again. +//! +//! For live bindings, the reachability constraint therefore refers to the following question: +//! Is the binding reachable from the start of the scope, and is there a control flow path from +//! that binding to a use of that symbol at the current position? +//! +//! In the example above, `x = 1` is always reachable, but that binding can only reach the use of +//! `x` at the current position if `test` is falsy. +//! +//! To handle boundness correctly, we also add implicit `y = ` bindings at the start of +//! the scope. This allows us to determine whether a symbol is definitely bound (if that implicit +//! `y = ` binding is not visible), possibly unbound (if the reachability constraint +//! evaluates to `Ambiguous`), or definitely unbound (in case the `y = ` binding is +//! always visible). +//! +//! +//! ### Representing formulas +//! +//! Given everything above, we can represent a reachability constraint as a _ternary formula_. This +//! is like a boolean formula (which maps several true/false variables to a single true/false +//! result), but which allows the third "ambiguous" value in addition to "true" and "false". +//! +//! [_Binary decision diagrams_][bdd] (BDDs) are a common way to represent boolean formulas when +//! doing program analysis. We extend this to a _ternary decision diagram_ (TDD) to support +//! ambiguous values. +//! +//! A TDD is a graph, and a ternary formula is represented by a node in this graph. There are three +//! possible leaf nodes representing the "true", "false", and "ambiguous" constant functions. +//! Interior nodes consist of a ternary variable to evaluate, and outgoing edges for whether the +//! variable evaluates to true, false, or ambiguous. +//! +//! Our TDDs are _reduced_ and _ordered_ (as is typical for BDDs). +//! +//! An ordered TDD means that variables appear in the same order in all paths within the graph. +//! +//! A reduced TDD means two things: First, we intern the graph nodes, so that we only keep a single +//! copy of interior nodes with the same contents. Second, we eliminate any nodes that are "noops", +//! where the "true" and "false" outgoing edges lead to the same node. (This implies that it +//! doesn't matter what value that variable has when evaluating the formula, and we can leave it +//! out of the evaluation chain completely.) +//! +//! Reduced and ordered decision diagrams are _normal forms_, which means that two equivalent +//! formulas (which have the same outputs for every combination of inputs) are represented by +//! exactly the same graph node. (Because of interning, this is not _equal_ nodes, but _identical_ +//! ones.) That means that we can compare formulas for equivalence in constant time, and in +//! particular, can check whether a reachability constraint is statically always true or false, +//! regardless of any Python program state, by seeing if the constraint's formula is the "true" or +//! "false" leaf node. +//! +//! [Kleene]: +//! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram + +use std::cmp::Ordering; + +use ruff_index::{Idx, IndexVec}; +use rustc_hash::FxHashMap; + +use crate::Db; +use crate::dunder_all::dunder_all_names; +use crate::place::{RequiresExplicitReExport, imported_symbol}; +use crate::semantic_index::expression::Expression; +use crate::semantic_index::place_table; +use crate::semantic_index::predicate::{ + CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, + Predicates, ScopedPredicateId, +}; +use crate::types::{Truthiness, Type, infer_expression_type}; + +/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula +/// is just like a boolean formula, but with `Ambiguous` as a third potential result. See the +/// module documentation for more details.) +/// +/// The primitive atoms of the formula are [`Predicate`]s, which express some property of the +/// runtime state of the code that we are analyzing. +/// +/// We assume that each atom has a stable value each time that the formula is evaluated. An atom +/// that resolves to `Ambiguous` might be true or false, and we can't tell which — but within that +/// evaluation, we assume that the atom has the _same_ unknown value each time it appears. That +/// allows us to perform simplifications like `A ∨ !A → true` and `A ∧ !A → false`. +/// +/// That means that when you are constructing a formula, you might need to create distinct atoms +/// for a particular [`Predicate`], if your formula needs to consider how a particular runtime +/// property might be different at different points in the execution of the program. +/// +/// reachability constraints are normalized, so equivalent constraints are guaranteed to have equal +/// IDs. +#[derive(Clone, Copy, Eq, Hash, PartialEq, get_size2::GetSize)] +pub(crate) struct ScopedReachabilityConstraintId(u32); + +impl std::fmt::Debug for ScopedReachabilityConstraintId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut f = f.debug_tuple("ScopedReachabilityConstraintId"); + match *self { + // We use format_args instead of rendering the strings directly so that we don't get + // any quotes in the output: ScopedReachabilityConstraintId(AlwaysTrue) instead of + // ScopedReachabilityConstraintId("AlwaysTrue"). + ALWAYS_TRUE => f.field(&format_args!("AlwaysTrue")), + AMBIGUOUS => f.field(&format_args!("Ambiguous")), + ALWAYS_FALSE => f.field(&format_args!("AlwaysFalse")), + _ => f.field(&self.0), + }; + f.finish() + } +} + +// Internal details: +// +// There are 3 terminals, with hard-coded constraint IDs: true, ambiguous, and false. +// +// _Atoms_ are the underlying Predicates, which are the variables that are evaluated by the +// ternary function. +// +// _Interior nodes_ provide the TDD structure for the formula. Interior nodes are stored in an +// arena Vec, with the constraint ID providing an index into the arena. + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +struct InteriorNode { + /// A "variable" that is evaluated as part of a TDD ternary function. For reachability + /// constraints, this is a `Predicate` that represents some runtime property of the Python + /// code that we are evaluating. + atom: ScopedPredicateId, + if_true: ScopedReachabilityConstraintId, + if_ambiguous: ScopedReachabilityConstraintId, + if_false: ScopedReachabilityConstraintId, +} + +impl ScopedReachabilityConstraintId { + /// A special ID that is used for an "always true" / "always visible" constraint. + pub(crate) const ALWAYS_TRUE: ScopedReachabilityConstraintId = + ScopedReachabilityConstraintId(0xffff_ffff); + + /// A special ID that is used for an ambiguous constraint. + pub(crate) const AMBIGUOUS: ScopedReachabilityConstraintId = + ScopedReachabilityConstraintId(0xffff_fffe); + + /// A special ID that is used for an "always false" / "never visible" constraint. + pub(crate) const ALWAYS_FALSE: ScopedReachabilityConstraintId = + ScopedReachabilityConstraintId(0xffff_fffd); + + fn is_terminal(self) -> bool { + self.0 >= SMALLEST_TERMINAL.0 + } +} + +impl Idx for ScopedReachabilityConstraintId { + #[inline] + fn new(value: usize) -> Self { + assert!(value <= (SMALLEST_TERMINAL.0 as usize)); + #[expect(clippy::cast_possible_truncation)] + Self(value as u32) + } + + #[inline] + fn index(self) -> usize { + debug_assert!(!self.is_terminal()); + self.0 as usize + } +} + +// Rebind some constants locally so that we don't need as many qualifiers below. +const ALWAYS_TRUE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::ALWAYS_TRUE; +const AMBIGUOUS: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::AMBIGUOUS; +const ALWAYS_FALSE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId::ALWAYS_FALSE; +const SMALLEST_TERMINAL: ScopedReachabilityConstraintId = ALWAYS_FALSE; + +/// A collection of reachability constraints for a given scope. +#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct ReachabilityConstraints { + interiors: IndexVec, +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub(crate) struct ReachabilityConstraintsBuilder { + interiors: IndexVec, + interior_cache: FxHashMap, + not_cache: FxHashMap, + and_cache: FxHashMap< + ( + ScopedReachabilityConstraintId, + ScopedReachabilityConstraintId, + ), + ScopedReachabilityConstraintId, + >, + or_cache: FxHashMap< + ( + ScopedReachabilityConstraintId, + ScopedReachabilityConstraintId, + ), + ScopedReachabilityConstraintId, + >, +} + +impl ReachabilityConstraintsBuilder { + pub(crate) fn build(self) -> ReachabilityConstraints { + ReachabilityConstraints { + interiors: self.interiors, + } + } + + /// Returns whether `a` or `b` has a "larger" atom. TDDs are ordered such that interior nodes + /// can only have edges to "larger" nodes. Terminals are considered to have a larger atom than + /// any internal node, since they are leaf nodes. + fn cmp_atoms( + &self, + a: ScopedReachabilityConstraintId, + b: ScopedReachabilityConstraintId, + ) -> Ordering { + if a == b || (a.is_terminal() && b.is_terminal()) { + Ordering::Equal + } else if a.is_terminal() { + Ordering::Greater + } else if b.is_terminal() { + Ordering::Less + } else { + self.interiors[a].atom.cmp(&self.interiors[b].atom) + } + } + + /// Adds an interior node, ensuring that we always use the same reachability constraint ID for + /// equal nodes. + fn add_interior(&mut self, node: InteriorNode) -> ScopedReachabilityConstraintId { + // If the true and false branches lead to the same node, we can override the ambiguous + // branch to go there too. And this node is then redundant and can be reduced. + if node.if_true == node.if_false { + return node.if_true; + } + + *self + .interior_cache + .entry(node) + .or_insert_with(|| self.interiors.push(node)) + } + + /// Adds a new reachability constraint that checks a single [`Predicate`]. + /// + /// [`ScopedPredicateId`]s are the “variables” that are evaluated by a TDD. A TDD variable has + /// the same value no matter how many times it appears in the ternary formula that the TDD + /// represents. + /// + /// However, we sometimes have to model how a `Predicate` can have a different runtime + /// value at different points in the execution of the program. To handle this, you can take + /// advantage of the fact that the [`Predicates`] arena does not deduplicate `Predicate`s. + /// You can add a `Predicate` multiple times, yielding different `ScopedPredicateId`s, which + /// you can then create separate TDD atoms for. + pub(crate) fn add_atom( + &mut self, + predicate: ScopedPredicateId, + ) -> ScopedReachabilityConstraintId { + if predicate == ScopedPredicateId::ALWAYS_FALSE { + ScopedReachabilityConstraintId::ALWAYS_FALSE + } else if predicate == ScopedPredicateId::ALWAYS_TRUE { + ScopedReachabilityConstraintId::ALWAYS_TRUE + } else { + self.add_interior(InteriorNode { + atom: predicate, + if_true: ALWAYS_TRUE, + if_ambiguous: AMBIGUOUS, + if_false: ALWAYS_FALSE, + }) + } + } + + /// Adds a new reachability constraint that is the ternary NOT of an existing one. + pub(crate) fn add_not_constraint( + &mut self, + a: ScopedReachabilityConstraintId, + ) -> ScopedReachabilityConstraintId { + if a == ALWAYS_TRUE { + return ALWAYS_FALSE; + } else if a == AMBIGUOUS { + return AMBIGUOUS; + } else if a == ALWAYS_FALSE { + return ALWAYS_TRUE; + } + + if let Some(cached) = self.not_cache.get(&a) { + return *cached; + } + let a_node = self.interiors[a]; + let if_true = self.add_not_constraint(a_node.if_true); + let if_ambiguous = self.add_not_constraint(a_node.if_ambiguous); + let if_false = self.add_not_constraint(a_node.if_false); + let result = self.add_interior(InteriorNode { + atom: a_node.atom, + if_true, + if_ambiguous, + if_false, + }); + self.not_cache.insert(a, result); + result + } + + /// Adds a new reachability constraint that is the ternary OR of two existing ones. + pub(crate) fn add_or_constraint( + &mut self, + a: ScopedReachabilityConstraintId, + b: ScopedReachabilityConstraintId, + ) -> ScopedReachabilityConstraintId { + match (a, b) { + (ALWAYS_TRUE, _) | (_, ALWAYS_TRUE) => return ALWAYS_TRUE, + (ALWAYS_FALSE, other) | (other, ALWAYS_FALSE) => return other, + (AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS, + _ => {} + } + + // OR is commutative, which lets us halve the cache requirements + let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) }; + if let Some(cached) = self.or_cache.get(&(a, b)) { + return *cached; + } + + let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) { + Ordering::Equal => { + let a_node = self.interiors[a]; + let b_node = self.interiors[b]; + let if_true = self.add_or_constraint(a_node.if_true, b_node.if_true); + let if_false = self.add_or_constraint(a_node.if_false, b_node.if_false); + let if_ambiguous = if if_true == if_false { + if_true + } else { + self.add_or_constraint(a_node.if_ambiguous, b_node.if_ambiguous) + }; + (a_node.atom, if_true, if_ambiguous, if_false) + } + Ordering::Less => { + let a_node = self.interiors[a]; + let if_true = self.add_or_constraint(a_node.if_true, b); + let if_false = self.add_or_constraint(a_node.if_false, b); + let if_ambiguous = if if_true == if_false { + if_true + } else { + self.add_or_constraint(a_node.if_ambiguous, b) + }; + (a_node.atom, if_true, if_ambiguous, if_false) + } + Ordering::Greater => { + let b_node = self.interiors[b]; + let if_true = self.add_or_constraint(a, b_node.if_true); + let if_false = self.add_or_constraint(a, b_node.if_false); + let if_ambiguous = if if_true == if_false { + if_true + } else { + self.add_or_constraint(a, b_node.if_ambiguous) + }; + (b_node.atom, if_true, if_ambiguous, if_false) + } + }; + + let result = self.add_interior(InteriorNode { + atom, + if_true, + if_ambiguous, + if_false, + }); + self.or_cache.insert((a, b), result); + result + } + + /// Adds a new reachability constraint that is the ternary AND of two existing ones. + pub(crate) fn add_and_constraint( + &mut self, + a: ScopedReachabilityConstraintId, + b: ScopedReachabilityConstraintId, + ) -> ScopedReachabilityConstraintId { + match (a, b) { + (ALWAYS_FALSE, _) | (_, ALWAYS_FALSE) => return ALWAYS_FALSE, + (ALWAYS_TRUE, other) | (other, ALWAYS_TRUE) => return other, + (AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS, + _ => {} + } + + // AND is commutative, which lets us halve the cache requirements + let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) }; + if let Some(cached) = self.and_cache.get(&(a, b)) { + return *cached; + } + + let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) { + Ordering::Equal => { + let a_node = self.interiors[a]; + let b_node = self.interiors[b]; + let if_true = self.add_and_constraint(a_node.if_true, b_node.if_true); + let if_false = self.add_and_constraint(a_node.if_false, b_node.if_false); + let if_ambiguous = if if_true == if_false { + if_true + } else { + self.add_and_constraint(a_node.if_ambiguous, b_node.if_ambiguous) + }; + (a_node.atom, if_true, if_ambiguous, if_false) + } + Ordering::Less => { + let a_node = self.interiors[a]; + let if_true = self.add_and_constraint(a_node.if_true, b); + let if_false = self.add_and_constraint(a_node.if_false, b); + let if_ambiguous = if if_true == if_false { + if_true + } else { + self.add_and_constraint(a_node.if_ambiguous, b) + }; + (a_node.atom, if_true, if_ambiguous, if_false) + } + Ordering::Greater => { + let b_node = self.interiors[b]; + let if_true = self.add_and_constraint(a, b_node.if_true); + let if_false = self.add_and_constraint(a, b_node.if_false); + let if_ambiguous = if if_true == if_false { + if_true + } else { + self.add_and_constraint(a, b_node.if_ambiguous) + }; + (b_node.atom, if_true, if_ambiguous, if_false) + } + }; + + let result = self.add_interior(InteriorNode { + atom, + if_true, + if_ambiguous, + if_false, + }); + self.and_cache.insert((a, b), result); + result + } +} + +impl ReachabilityConstraints { + /// Analyze the statically known reachability for a given constraint. + pub(crate) fn evaluate<'db>( + &self, + db: &'db dyn Db, + predicates: &Predicates<'db>, + mut id: ScopedReachabilityConstraintId, + ) -> Truthiness { + loop { + let node = match id { + ALWAYS_TRUE => return Truthiness::AlwaysTrue, + AMBIGUOUS => return Truthiness::Ambiguous, + ALWAYS_FALSE => return Truthiness::AlwaysFalse, + _ => self.interiors[id], + }; + let predicate = &predicates[node.atom]; + match Self::analyze_single(db, predicate) { + Truthiness::AlwaysTrue => id = node.if_true, + Truthiness::Ambiguous => id = node.if_ambiguous, + Truthiness::AlwaysFalse => id = node.if_false, + } + } + } + + fn analyze_single_pattern_predicate_kind<'db>( + db: &'db dyn Db, + predicate_kind: &PatternPredicateKind<'db>, + subject: Expression<'db>, + ) -> Truthiness { + match predicate_kind { + PatternPredicateKind::Value(value) => { + let subject_ty = infer_expression_type(db, subject); + let value_ty = infer_expression_type(db, *value); + + if subject_ty.is_single_valued(db) { + Truthiness::from(subject_ty.is_equivalent_to(db, value_ty)) + } else { + Truthiness::Ambiguous + } + } + PatternPredicateKind::Singleton(singleton) => { + let subject_ty = infer_expression_type(db, subject); + + let singleton_ty = match singleton { + ruff_python_ast::Singleton::None => Type::none(db), + ruff_python_ast::Singleton::True => Type::BooleanLiteral(true), + ruff_python_ast::Singleton::False => Type::BooleanLiteral(false), + }; + + debug_assert!(singleton_ty.is_singleton(db)); + + if subject_ty.is_equivalent_to(db, singleton_ty) { + Truthiness::AlwaysTrue + } else if subject_ty.is_disjoint_from(db, singleton_ty) { + Truthiness::AlwaysFalse + } else { + Truthiness::Ambiguous + } + } + PatternPredicateKind::Or(predicates) => { + use std::ops::ControlFlow; + let (ControlFlow::Break(truthiness) | ControlFlow::Continue(truthiness)) = + predicates + .iter() + .map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject)) + // this is just a "max", but with a slight optimization: `AlwaysTrue` is the "greatest" possible element, so we short-circuit if we get there + .try_fold(Truthiness::AlwaysFalse, |acc, next| match (acc, next) { + (Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => { + ControlFlow::Break(Truthiness::AlwaysTrue) + } + (Truthiness::Ambiguous, _) | (_, Truthiness::Ambiguous) => { + ControlFlow::Continue(Truthiness::Ambiguous) + } + (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => { + ControlFlow::Continue(Truthiness::AlwaysFalse) + } + }); + truthiness + } + PatternPredicateKind::Class(class_expr) => { + let subject_ty = infer_expression_type(db, subject); + let class_ty = infer_expression_type(db, *class_expr).to_instance(db); + + class_ty.map_or(Truthiness::Ambiguous, |class_ty| { + if subject_ty.is_subtype_of(db, class_ty) { + Truthiness::AlwaysTrue + } else if subject_ty.is_disjoint_from(db, class_ty) { + Truthiness::AlwaysFalse + } else { + Truthiness::Ambiguous + } + }) + } + PatternPredicateKind::Unsupported => Truthiness::Ambiguous, + } + } + + fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness { + let truthiness = Self::analyze_single_pattern_predicate_kind( + db, + predicate.kind(db), + predicate.subject(db), + ); + + if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() { + // Fall back to ambiguous, the guard might change the result. + // TODO: actually analyze guard truthiness + Truthiness::Ambiguous + } else { + truthiness + } + } + + fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { + match predicate.node { + PredicateNode::Expression(test_expr) => { + let ty = infer_expression_type(db, test_expr); + ty.bool(db).negate_if(!predicate.is_positive) + } + PredicateNode::ReturnsNever(CallableAndCallExpr { + callable, + call_expr, + }) => { + // We first infer just the type of the callable. In the most likely case that the + // function is not marked with `NoReturn`, or that it always returns `NoReturn`, + // doing so allows us to avoid the more expensive work of inferring the entire call + // expression (which could involve inferring argument types to possibly run the overload + // selection algorithm). + // Avoiding this on the happy-path is important because these constraints can be + // very large in number, since we add them on all statement level function calls. + let ty = infer_expression_type(db, callable); + + let overloads_iterator = + if let Some(Type::Callable(callable)) = ty.into_callable(db) { + callable.signatures(db).overloads.iter() + } else { + return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); + }; + + let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator + .fold((true, true), |(none, all), overload| { + let overload_returns_never = + overload.return_ty.is_some_and(|return_type| { + return_type.is_equivalent_to(db, Type::Never) + }); + + ( + none && !overload_returns_never, + all && overload_returns_never, + ) + }); + + if no_overloads_return_never { + Truthiness::AlwaysFalse + } else if all_overloads_return_never { + Truthiness::AlwaysTrue + } else { + let call_expr_ty = infer_expression_type(db, call_expr); + if call_expr_ty.is_equivalent_to(db, Type::Never) { + Truthiness::AlwaysTrue + } else { + Truthiness::AlwaysFalse + } + } + .negate_if(!predicate.is_positive) + } + PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner), + PredicateNode::StarImportPlaceholder(star_import) => { + let place_table = place_table(db, star_import.scope(db)); + let symbol_name = place_table + .place_expr(star_import.symbol_id(db)) + .expect_name(); + let referenced_file = star_import.referenced_file(db); + + let requires_explicit_reexport = match dunder_all_names(db, referenced_file) { + Some(all_names) => { + if all_names.contains(symbol_name) { + Some(RequiresExplicitReExport::No) + } else { + tracing::trace!( + "Symbol `{}` (via star import) not found in `__all__` of `{}`", + symbol_name, + referenced_file.path(db) + ); + return Truthiness::AlwaysFalse; + } + } + None => None, + }; + + match imported_symbol(db, referenced_file, symbol_name, requires_explicit_reexport) + .place + { + crate::place::Place::Type(_, crate::place::Boundness::Bound) => { + Truthiness::AlwaysTrue + } + crate::place::Place::Type(_, crate::place::Boundness::PossiblyUnbound) => { + Truthiness::Ambiguous + } + crate::place::Place::Unbound => Truthiness::AlwaysFalse, + } + } + } + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs new file mode 100644 index 0000000000000..33dc7d8989bc3 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -0,0 +1,1146 @@ +//! First, some terminology: +//! +//! * A "place" is semantically a location where a value can be read or written, and syntactically, +//! an expression that can be the target of an assignment, e.g. `x`, `x[0]`, `x.y`. (The term is +//! borrowed from Rust). In Python syntax, an expression like `f().x` is also allowed as the +//! target so it can be called a place, but we do not record declarations / bindings like `f().x: +//! int`, `f().x = ...`. Type checking itself can be done by recording only assignments to names, +//! but in order to perform type narrowing by attribute/subscript assignments, they must also be +//! recorded. +//! +//! * A "binding" gives a new value to a place. This includes many different Python statements +//! (assignment statements of course, but also imports, `def` and `class` statements, `as` +//! clauses in `with` and `except` statements, match patterns, and others) and even one +//! expression kind (named expressions). It notably does not include annotated assignment +//! statements without a right-hand side value; these do not assign any new value to the place. +//! We consider function parameters to be bindings as well, since (from the perspective of the +//! function's internal scope), a function parameter begins the scope bound to a value. +//! +//! * A "declaration" establishes an upper bound type for the values that a variable may be +//! permitted to take on. Annotated assignment statements (with or without an RHS value) are +//! declarations; annotated function parameters are also declarations. We consider `def` and +//! `class` statements to also be declarations, so as to prohibit accidentally shadowing them. +//! +//! Annotated assignments with a right-hand side, and annotated function parameters, are both +//! bindings and declarations. +//! +//! We use [`Definition`] as the universal term (and Salsa tracked struct) encompassing both +//! bindings and declarations. (This sacrifices a bit of type safety in exchange for improved +//! performance via fewer Salsa tracked structs and queries, since most declarations -- typed +//! parameters and annotated assignments with RHS -- are both bindings and declarations.) +//! +//! At any given use of a variable, we can ask about both its "declared type" and its "inferred +//! type". These may be different, but the inferred type must always be assignable to the declared +//! type; that is, the declared type is always wider, and the inferred type may be more precise. If +//! we see an invalid assignment, we emit a diagnostic and abandon our inferred type, deferring to +//! the declared type (this allows an explicit annotation to override bad inference, without a +//! cast), maintaining the invariant. +//! +//! The **inferred type** represents the most precise type we believe encompasses all possible +//! values for the variable at a given use. It is based on a union of the bindings which can reach +//! that use through some control flow path, and the narrowing constraints that control flow must +//! have passed through between the binding and the use. For example, in this code: +//! +//! ```python +//! x = 1 if flag else None +//! if x is not None: +//! use(x) +//! ``` +//! +//! For the use of `x` on the third line, the inferred type should be `Literal[1]`. This is based +//! on the binding on the first line, which assigns the type `Literal[1] | None`, and the narrowing +//! constraint on the second line, which rules out the type `None`, since control flow must pass +//! through this constraint to reach the use in question. +//! +//! The **declared type** represents the code author's declaration (usually through a type +//! annotation) that a given variable should not be assigned any type outside the declared type. In +//! our model, declared types are also control-flow-sensitive; we allow the code author to +//! explicitly redeclare the same variable with a different type. So for a given binding of a +//! variable, we will want to ask which declarations of that variable can reach that binding, in +//! order to determine whether the binding is permitted, or should be a type error. For example: +//! +//! ```python +//! from pathlib import Path +//! def f(path: str): +//! path: Path = Path(path) +//! ``` +//! +//! In this function, the initial declared type of `path` is `str`, meaning that the assignment +//! `path = Path(path)` would be a type error, since it assigns to `path` a value whose type is not +//! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment +//! of the wrong type to a variable. +//! +//! But in some cases it is useful to "shadow" or "redeclare" a variable with a new type, and we +//! permit this, as long as it is done with an explicit re-annotation. So `path: Path = +//! Path(path)`, with the explicit `: Path` annotation, is permitted. +//! +//! The general rule is that whatever declaration(s) can reach a given binding determine the +//! validity of that binding. If there is a path in which the place is not declared, that is a +//! declaration of `Unknown`. If multiple declarations can reach a binding, we union them, but by +//! default we also issue a type error, since this implicit union of declared types may hide an +//! error. +//! +//! To support type inference, we build a map from each use of a place to the bindings live at +//! that use, and the type narrowing constraints that apply to each binding. +//! +//! Let's take this code sample: +//! +//! ```python +//! x = 1 +//! x = 2 +//! y = x +//! if flag: +//! x = 3 +//! else: +//! x = 4 +//! z = x +//! ``` +//! +//! In this snippet, we have four bindings of `x` (the statements assigning `1`, `2`, `3`, and `4` +//! to it), and two uses of `x` (the `y = x` and `z = x` assignments). The first binding of `x` +//! does not reach any use, because it's immediately replaced by the second binding, before any use +//! happens. (A linter could thus flag the statement `x = 1` as likely superfluous.) +//! +//! The first use of `x` has one live binding: the assignment `x = 2`. +//! +//! Things get a bit more complex when we have branches. We will definitely take either the `if` or +//! the `else` branch. Thus, the second use of `x` has two live bindings: `x = 3` and `x = 4`. The +//! `x = 2` assignment is no longer visible, because it must be replaced by either `x = 3` or `x = +//! 4`, no matter which branch was taken. We don't know which branch was taken, so we must consider +//! both bindings as live, which means eventually we would (in type inference) look at these two +//! bindings and infer a type of `Literal[3, 4]` -- the union of `Literal[3]` and `Literal[4]` -- +//! for the second use of `x`. +//! +//! So that's one question our use-def map needs to answer: given a specific use of a place, which +//! binding(s) can reach that use. In [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number +//! all uses (that means a `Name`/`ExprAttribute`/`ExprSubscript` node with `Load` context) +//! so we have a `ScopedUseId` to efficiently represent each use. +//! +//! We also need to know, for a given definition of a place, what type narrowing constraints apply +//! to it. For instance, in this code sample: +//! +//! ```python +//! x = 1 if flag else None +//! if x is not None: +//! use(x) +//! ``` +//! +//! At the use of `x`, the live binding of `x` is `1 if flag else None`, which would infer as the +//! type `Literal[1] | None`. But the constraint `x is not None` dominates this use, which means we +//! can rule out the possibility that `x` is `None` here, which should give us the type +//! `Literal[1]` for this use. +//! +//! For declared types, we need to be able to answer the question "given a binding to a place, +//! which declarations of that place can reach the binding?" This allows us to emit a diagnostic +//! if the binding is attempting to bind a value of a type that is not assignable to the declared +//! type for that place, at that point in control flow. +//! +//! We also need to know, given a declaration of a place, what the inferred type of that place is +//! at that point. This allows us to emit a diagnostic in a case like `x = "foo"; x: int`. The +//! binding `x = "foo"` occurs before the declaration `x: int`, so according to our +//! control-flow-sensitive interpretation of declarations, the assignment is not an error. But the +//! declaration is an error, since it would violate the "inferred type must be assignable to +//! declared type" rule. +//! +//! Another case we need to handle is when a place is referenced from a different scope (for +//! example, an import or a nonlocal reference). We call this "public" use of a place. For public +//! use of a place, we prefer the declared type, if there are any declarations of that place; if +//! not, we fall back to the inferred type. So we also need to know which declarations and bindings +//! can reach the end of the scope. +//! +//! Technically, public use of a place could occur from any point in control flow of the scope +//! where the place is defined (via inline imports and import cycles, in the case of an import, or +//! via a function call partway through the local scope that ends up using a place from the scope +//! via a global or nonlocal reference.) But modeling this fully accurately requires whole-program +//! analysis that isn't tractable for an efficient analysis, since it means a given place could +//! have a different type every place it's referenced throughout the program, depending on the +//! shape of arbitrarily-sized call/import graphs. So we follow other Python type checkers in +//! making the simplifying assumption that usually the scope will finish execution before its +//! places are made visible to other scopes; for instance, most imports will import from a +//! complete module, not a partially-executed module. (We may want to get a little smarter than +//! this in the future for some closures, but for now this is where we start.) +//! +//! The data structure we build to answer these questions is the `UseDefMap`. It has a +//! `bindings_by_use` vector of [`Bindings`] indexed by [`ScopedUseId`], a +//! `declarations_by_binding` vector of [`Declarations`] indexed by [`ScopedDefinitionId`], a +//! `bindings_by_declaration` vector of [`Bindings`] indexed by [`ScopedDefinitionId`], and +//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedPlaceId`]. The values in +//! each of these vectors are (in principle) a list of live bindings at that use/definition, or at +//! the end of the scope for that place, with a list of the dominating constraints for each +//! binding. +//! +//! In order to avoid vectors-of-vectors-of-vectors and all the allocations that would entail, we +//! don't actually store these "list of visible definitions" as a vector of [`Definition`]. +//! Instead, [`Bindings`] and [`Declarations`] are structs which use bit-sets to track +//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and +//! [`ScopedPredicateId`], which are indices into the `all_definitions` and `predicates` +//! indexvecs in the [`UseDefMap`]. +//! +//! There is another special kind of possible "definition" for a place: there might be a path from +//! the scope entry to a given use in which the place is never bound. We model this with a special +//! "unbound/undeclared" definition (a [`DefinitionState::Undefined`] entry at the start of the +//! `all_definitions` vector). If that sentinel definition is present in the live bindings at a +//! given use, it means that there is a possible path through control flow in which that place is +//! unbound. Similarly, if that sentinel is present in the live declarations, it means that the +//! place is (possibly) undeclared. +//! +//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and +//! constraint as they are encountered by the +//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder) AST visit. For +//! each place, the builder tracks the `PlaceState` (`Bindings` and `Declarations`) for that place. +//! When we hit a use or definition of a place, we record the necessary parts of the current state +//! for that place that we need for that use or definition. When we reach the end of the scope, it +//! records the state for each place as the public definitions of that place. +//! +//! ```python +//! x = 1 +//! x = 2 +//! y = x +//! if flag: +//! x = 3 +//! else: +//! x = 4 +//! z = x +//! ``` +//! +//! Let's walk through the above example. Initially we do not have any record of `x`. When we add +//! the new place (before we process the first binding), we create a new undefined `PlaceState` +//! which has a single live binding (the "unbound" definition) and a single live declaration (the +//! "undeclared" definition). When we see `x = 1`, we record that as the sole live binding of `x`. +//! The "unbound" binding is no longer visible. Then we see `x = 2`, and we replace `x = 1` as the +//! sole live binding of `x`. When we get to `y = x`, we record that the live bindings for that use +//! of `x` are just the `x = 2` definition. +//! +//! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will +//! happen regardless. Then we take a pre-branch snapshot of the current state for all places, +//! which we'll need later. Then we record `flag` as a possible constraint on the current binding +//! (`x = 2`), and go ahead and visit the `if` body. When we see `x = 3`, it replaces `x = 2` +//! (constrained by `flag`) as the sole live binding of `x`. At the end of the `if` body, we take +//! another snapshot of the current place state; we'll call this the post-if-body snapshot. +//! +//! Now we need to visit the `else` clause. The conditions when entering the `else` clause should +//! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test +//! failed and we didn't execute the `if` body. So we first reset the builder to the pre-if state, +//! using the snapshot we took previously (meaning we now have `x = 2` as the sole binding for `x` +//! again), and record a *negative* `flag` constraint for all live bindings (`x = 2`). We then +//! visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding of `x`. +//! +//! Now we reach the end of the if/else, and want to visit the following code. The state here needs +//! to reflect that we might have gone through the `if` branch, or we might have gone through the +//! `else` branch, and we don't know which. So we need to "merge" our current builder state +//! (reflecting the end-of-else state, with `x = 4` as the only live binding) with our post-if-body +//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now +//! have two live bindings of `x`: `x = 3` and `x = 4`. +//! +//! Another piece of information that the `UseDefMap` needs to provide are reachability constraints. +//! See [`reachability_constraints.rs`] for more details, in particular how they apply to bindings. +//! +//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a +//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in +//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it +//! visits a `StmtIf` node. + +use ruff_index::{IndexVec, newtype_index}; +use rustc_hash::FxHashMap; + +use self::place_state::{ + Bindings, Declarations, EagerSnapshot, LiveBindingsIterator, LiveDeclaration, + LiveDeclarationsIterator, PlaceState, ScopedDefinitionId, +}; +use crate::node_key::NodeKey; +use crate::place::BoundnessAnalysis; +use crate::semantic_index::ast_ids::ScopedUseId; +use crate::semantic_index::definition::{Definition, DefinitionState}; +use crate::semantic_index::narrowing_constraints::{ + ConstraintKey, NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator, +}; +use crate::semantic_index::place::{ + FileScopeId, PlaceExpr, PlaceExprWithFlags, ScopeKind, ScopedPlaceId, +}; +use crate::semantic_index::predicate::{ + Predicate, PredicateOrLiteral, Predicates, PredicatesBuilder, ScopedPredicateId, +}; +use crate::semantic_index::reachability_constraints::{ + ReachabilityConstraints, ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId, +}; +use crate::semantic_index::use_def::place_state::PreviousDefinitions; +use crate::semantic_index::{EagerSnapshotResult, SemanticIndex}; +use crate::types::{IntersectionBuilder, Truthiness, Type, infer_narrowing_constraint}; + +mod place_state; + +/// Applicable definitions and constraints for every use of a name. +#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct UseDefMap<'db> { + /// Array of [`Definition`] in this scope. Only the first entry should be [`DefinitionState::Undefined`]; + /// this represents the implicit "unbound"/"undeclared" definition of every place. + all_definitions: IndexVec>, + + /// Array of predicates in this scope. + predicates: Predicates<'db>, + + /// Array of narrowing constraints in this scope. + narrowing_constraints: NarrowingConstraints, + + /// Array of reachability constraints in this scope. + reachability_constraints: ReachabilityConstraints, + + /// [`Bindings`] reaching a [`ScopedUseId`]. + bindings_by_use: IndexVec, + + /// Tracks whether or not a given AST node is reachable from the start of the scope. + node_reachability: FxHashMap, + + /// If the definition is a binding (only) -- `x = 1` for example -- then we need + /// [`Declarations`] to know whether this binding is permitted by the live declarations. + /// + /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then + /// we don't actually need anything here, all we'll need to validate is that our own RHS is a + /// valid assignment to our own annotation. + declarations_by_binding: FxHashMap, Declarations>, + + /// If the definition is a declaration (only) -- `x: int` for example -- then we need + /// [`Bindings`] to know whether this declaration is consistent with the previously + /// inferred type. + /// + /// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then + /// we don't actually need anything here, all we'll need to validate is that our own RHS is a + /// valid assignment to our own annotation. + /// + /// If we see a binding to a `Final`-qualified symbol, we also need this map to find previous + /// bindings to that symbol. If there are any, the assignment is invalid. + bindings_by_definition: FxHashMap, Bindings>, + + /// [`PlaceState`] visible at end of scope for each place. + end_of_scope_places: IndexVec, + + /// All potentially reachable bindings and declarations, for each place. + reachable_definitions: IndexVec, + + /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested + /// eager scope. + eager_snapshots: EagerSnapshots, + + /// Whether or not the end of the scope is reachable. + /// + /// This is used to check if the function can implicitly return `None`. + /// For example: + /// ```py + /// def f(cond: bool) -> int | None: + /// if cond: + /// return 1 + /// + /// def g() -> int: + /// if True: + /// return 1 + /// ``` + /// + /// Function `f` may implicitly return `None`, but `g` cannot. + /// + /// This is used by [`UseDefMap::can_implicitly_return_none`]. + end_of_scope_reachability: ScopedReachabilityConstraintId, +} + +pub(crate) enum ApplicableConstraints<'map, 'db> { + UnboundBinding(ConstraintsIterator<'map, 'db>), + ConstrainedBindings(BindingWithConstraintsIterator<'map, 'db>), +} + +impl<'db> UseDefMap<'db> { + pub(crate) fn bindings_at_use( + &self, + use_id: ScopedUseId, + ) -> BindingWithConstraintsIterator<'_, 'db> { + self.bindings_iterator( + &self.bindings_by_use[use_id], + BoundnessAnalysis::BasedOnUnboundVisibility, + ) + } + + pub(crate) fn applicable_constraints( + &self, + constraint_key: ConstraintKey, + enclosing_scope: FileScopeId, + expr: &PlaceExpr, + index: &'db SemanticIndex, + ) -> ApplicableConstraints<'_, 'db> { + match constraint_key { + ConstraintKey::NarrowingConstraint(constraint) => { + ApplicableConstraints::UnboundBinding(ConstraintsIterator { + predicates: &self.predicates, + constraint_ids: self.narrowing_constraints.iter_predicates(constraint), + }) + } + ConstraintKey::EagerNestedScope(nested_scope) => { + let EagerSnapshotResult::FoundBindings(bindings) = + index.eager_snapshot(enclosing_scope, expr, nested_scope) + else { + unreachable!( + "The result of `SemanticIndex::eager_snapshot` must be `FoundBindings`" + ) + }; + ApplicableConstraints::ConstrainedBindings(bindings) + } + ConstraintKey::UseId(use_id) => { + ApplicableConstraints::ConstrainedBindings(self.bindings_at_use(use_id)) + } + } + } + + pub(super) fn is_reachable( + &self, + db: &dyn crate::Db, + reachability: ScopedReachabilityConstraintId, + ) -> bool { + self.reachability_constraints + .evaluate(db, &self.predicates, reachability) + .may_be_true() + } + + /// Check whether or not a given expression is reachable from the start of the scope. This + /// is a local analysis which does not capture the possibility that the entire scope might + /// be unreachable. Use [`super::SemanticIndex::is_node_reachable`] for the global + /// analysis. + #[track_caller] + pub(super) fn is_node_reachable(&self, db: &dyn crate::Db, node_key: NodeKey) -> bool { + self + .reachability_constraints + .evaluate( + db, + &self.predicates, + *self + .node_reachability + .get(&node_key) + .expect("`is_node_reachable` should only be called on AST nodes with recorded reachability"), + ) + .may_be_true() + } + + pub(crate) fn end_of_scope_bindings( + &self, + place: ScopedPlaceId, + ) -> BindingWithConstraintsIterator<'_, 'db> { + self.bindings_iterator( + self.end_of_scope_places[place].bindings(), + BoundnessAnalysis::BasedOnUnboundVisibility, + ) + } + + pub(crate) fn all_reachable_bindings( + &self, + place: ScopedPlaceId, + ) -> BindingWithConstraintsIterator<'_, 'db> { + self.bindings_iterator( + &self.reachable_definitions[place].bindings, + BoundnessAnalysis::AssumeBound, + ) + } + + pub(crate) fn eager_snapshot( + &self, + eager_bindings: ScopedEagerSnapshotId, + ) -> EagerSnapshotResult<'_, 'db> { + match self.eager_snapshots.get(eager_bindings) { + Some(EagerSnapshot::Constraint(constraint)) => { + EagerSnapshotResult::FoundConstraint(*constraint) + } + Some(EagerSnapshot::Bindings(bindings)) => EagerSnapshotResult::FoundBindings( + self.bindings_iterator(bindings, BoundnessAnalysis::BasedOnUnboundVisibility), + ), + None => EagerSnapshotResult::NotFound, + } + } + + pub(crate) fn bindings_at_definition( + &self, + definition: Definition<'db>, + ) -> BindingWithConstraintsIterator<'_, 'db> { + self.bindings_iterator( + &self.bindings_by_definition[&definition], + BoundnessAnalysis::BasedOnUnboundVisibility, + ) + } + + pub(crate) fn declarations_at_binding( + &self, + binding: Definition<'db>, + ) -> DeclarationsIterator<'_, 'db> { + self.declarations_iterator( + &self.declarations_by_binding[&binding], + BoundnessAnalysis::BasedOnUnboundVisibility, + ) + } + + pub(crate) fn end_of_scope_declarations<'map>( + &'map self, + place: ScopedPlaceId, + ) -> DeclarationsIterator<'map, 'db> { + let declarations = self.end_of_scope_places[place].declarations(); + self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) + } + + pub(crate) fn all_reachable_declarations( + &self, + place: ScopedPlaceId, + ) -> DeclarationsIterator<'_, 'db> { + let declarations = &self.reachable_definitions[place].declarations; + self.declarations_iterator(declarations, BoundnessAnalysis::AssumeBound) + } + + pub(crate) fn all_end_of_scope_declarations<'map>( + &'map self, + ) -> impl Iterator)> + 'map { + (0..self.end_of_scope_places.len()) + .map(ScopedPlaceId::from_usize) + .map(|place_id| (place_id, self.end_of_scope_declarations(place_id))) + } + + pub(crate) fn all_end_of_scope_bindings<'map>( + &'map self, + ) -> impl Iterator)> + 'map + { + (0..self.end_of_scope_places.len()) + .map(ScopedPlaceId::from_usize) + .map(|place_id| (place_id, self.end_of_scope_bindings(place_id))) + } + + /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. + pub(crate) fn can_implicitly_return_none(&self, db: &dyn crate::Db) -> bool { + !self + .reachability_constraints + .evaluate(db, &self.predicates, self.end_of_scope_reachability) + .is_always_false() + } + + pub(crate) fn is_declaration_reachable( + &self, + db: &dyn crate::Db, + declaration: &DeclarationWithConstraint<'db>, + ) -> Truthiness { + self.reachability_constraints.evaluate( + db, + &self.predicates, + declaration.reachability_constraint, + ) + } + + pub(crate) fn is_binding_reachable( + &self, + db: &dyn crate::Db, + binding: &BindingWithConstraints<'_, 'db>, + ) -> Truthiness { + self.reachability_constraints.evaluate( + db, + &self.predicates, + binding.reachability_constraint, + ) + } + + fn bindings_iterator<'map>( + &'map self, + bindings: &'map Bindings, + boundness_analysis: BoundnessAnalysis, + ) -> BindingWithConstraintsIterator<'map, 'db> { + BindingWithConstraintsIterator { + all_definitions: &self.all_definitions, + predicates: &self.predicates, + narrowing_constraints: &self.narrowing_constraints, + reachability_constraints: &self.reachability_constraints, + boundness_analysis, + inner: bindings.iter(), + } + } + + fn declarations_iterator<'map>( + &'map self, + declarations: &'map Declarations, + boundness_analysis: BoundnessAnalysis, + ) -> DeclarationsIterator<'map, 'db> { + DeclarationsIterator { + all_definitions: &self.all_definitions, + predicates: &self.predicates, + reachability_constraints: &self.reachability_constraints, + boundness_analysis, + inner: declarations.iter(), + } + } +} + +/// Uniquely identifies a snapshot of a place state that can be used to resolve a reference in a +/// nested eager scope. +/// +/// An eager scope has its entire body executed immediately at the location where it is defined. +/// For any free references in the nested scope, we use the bindings that are visible at the point +/// where the nested scope is defined, instead of using the public type of the place. +/// +/// There is a unique ID for each distinct [`EagerSnapshotKey`] in the file. +#[newtype_index] +#[derive(get_size2::GetSize)] +pub(crate) struct ScopedEagerSnapshotId; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub(crate) struct EagerSnapshotKey { + /// The enclosing scope containing the bindings + pub(crate) enclosing_scope: FileScopeId, + /// The referenced place (in the enclosing scope) + pub(crate) enclosing_place: ScopedPlaceId, + /// The nested eager scope containing the reference + pub(crate) nested_scope: FileScopeId, +} + +/// A snapshot of place states that can be used to resolve a reference in a nested eager scope. +type EagerSnapshots = IndexVec; + +#[derive(Debug)] +pub(crate) struct BindingWithConstraintsIterator<'map, 'db> { + all_definitions: &'map IndexVec>, + pub(crate) predicates: &'map Predicates<'db>, + pub(crate) narrowing_constraints: &'map NarrowingConstraints, + pub(crate) reachability_constraints: &'map ReachabilityConstraints, + pub(crate) boundness_analysis: BoundnessAnalysis, + inner: LiveBindingsIterator<'map>, +} + +impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> { + type Item = BindingWithConstraints<'map, 'db>; + + fn next(&mut self) -> Option { + let predicates = self.predicates; + let narrowing_constraints = self.narrowing_constraints; + + self.inner + .next() + .map(|live_binding| BindingWithConstraints { + binding: self.all_definitions[live_binding.binding], + narrowing_constraint: ConstraintsIterator { + predicates, + constraint_ids: narrowing_constraints + .iter_predicates(live_binding.narrowing_constraint), + }, + reachability_constraint: live_binding.reachability_constraint, + }) + } +} + +impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {} + +pub(crate) struct BindingWithConstraints<'map, 'db> { + pub(crate) binding: DefinitionState<'db>, + pub(crate) narrowing_constraint: ConstraintsIterator<'map, 'db>, + pub(crate) reachability_constraint: ScopedReachabilityConstraintId, +} + +pub(crate) struct ConstraintsIterator<'map, 'db> { + predicates: &'map Predicates<'db>, + constraint_ids: NarrowingConstraintsIterator<'map>, +} + +impl<'db> Iterator for ConstraintsIterator<'_, 'db> { + type Item = Predicate<'db>; + + fn next(&mut self) -> Option { + self.constraint_ids + .next() + .map(|narrowing_constraint| self.predicates[narrowing_constraint.predicate()]) + } +} + +impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {} + +impl<'db> ConstraintsIterator<'_, 'db> { + pub(crate) fn narrow( + self, + db: &'db dyn crate::Db, + base_ty: Type<'db>, + place: ScopedPlaceId, + ) -> Type<'db> { + let constraint_tys: Vec<_> = self + .filter_map(|constraint| infer_narrowing_constraint(db, constraint, place)) + .collect(); + + if constraint_tys.is_empty() { + base_ty + } else { + constraint_tys + .into_iter() + .rev() + .fold( + IntersectionBuilder::new(db).add_positive(base_ty), + IntersectionBuilder::add_positive, + ) + .build() + } + } +} + +#[derive(Clone)] +pub(crate) struct DeclarationsIterator<'map, 'db> { + all_definitions: &'map IndexVec>, + pub(crate) predicates: &'map Predicates<'db>, + pub(crate) reachability_constraints: &'map ReachabilityConstraints, + pub(crate) boundness_analysis: BoundnessAnalysis, + inner: LiveDeclarationsIterator<'map>, +} + +pub(crate) struct DeclarationWithConstraint<'db> { + pub(crate) declaration: DefinitionState<'db>, + pub(crate) reachability_constraint: ScopedReachabilityConstraintId, +} + +impl<'db> Iterator for DeclarationsIterator<'_, 'db> { + type Item = DeclarationWithConstraint<'db>; + + fn next(&mut self) -> Option { + self.inner.next().map( + |LiveDeclaration { + declaration, + reachability_constraint, + }| { + DeclarationWithConstraint { + declaration: self.all_definitions[*declaration], + reachability_constraint: *reachability_constraint, + } + }, + ) + } +} + +impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {} + +#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +struct ReachableDefinitions { + bindings: Bindings, + declarations: Declarations, +} + +/// A snapshot of the definitions and constraints state at a particular point in control flow. +#[derive(Clone, Debug)] +pub(super) struct FlowSnapshot { + place_states: IndexVec, + reachability: ScopedReachabilityConstraintId, +} + +#[derive(Debug)] +pub(super) struct UseDefMapBuilder<'db> { + /// Append-only array of [`DefinitionState`]. + all_definitions: IndexVec>, + + /// Builder of predicates. + pub(super) predicates: PredicatesBuilder<'db>, + + /// Builder of narrowing constraints. + pub(super) narrowing_constraints: NarrowingConstraintsBuilder, + + /// Builder of reachability constraints. + pub(super) reachability_constraints: ReachabilityConstraintsBuilder, + + /// Live bindings at each so-far-recorded use. + bindings_by_use: IndexVec, + + /// Tracks whether or not the current point in control flow is reachable from the + /// start of the scope. + pub(super) reachability: ScopedReachabilityConstraintId, + + /// Tracks whether or not a given AST node is reachable from the start of the scope. + node_reachability: FxHashMap, + + /// Live declarations for each so-far-recorded binding. + declarations_by_binding: FxHashMap, Declarations>, + + /// Live bindings for each so-far-recorded definition. + bindings_by_definition: FxHashMap, Bindings>, + + /// Currently live bindings and declarations for each place. + place_states: IndexVec, + + /// All potentially reachable bindings and declarations, for each place. + reachable_definitions: IndexVec, + + /// Snapshots of place states in this scope that can be used to resolve a reference in a + /// nested eager scope. + eager_snapshots: EagerSnapshots, + + /// Is this a class scope? + is_class_scope: bool, +} + +impl<'db> UseDefMapBuilder<'db> { + pub(super) fn new(is_class_scope: bool) -> Self { + Self { + all_definitions: IndexVec::from_iter([DefinitionState::Undefined]), + predicates: PredicatesBuilder::default(), + narrowing_constraints: NarrowingConstraintsBuilder::default(), + reachability_constraints: ReachabilityConstraintsBuilder::default(), + bindings_by_use: IndexVec::new(), + reachability: ScopedReachabilityConstraintId::ALWAYS_TRUE, + node_reachability: FxHashMap::default(), + declarations_by_binding: FxHashMap::default(), + bindings_by_definition: FxHashMap::default(), + place_states: IndexVec::new(), + reachable_definitions: IndexVec::new(), + eager_snapshots: EagerSnapshots::default(), + is_class_scope, + } + } + pub(super) fn mark_unreachable(&mut self) { + self.reachability = ScopedReachabilityConstraintId::ALWAYS_FALSE; + + for state in &mut self.place_states { + state.record_reachability_constraint( + &mut self.reachability_constraints, + ScopedReachabilityConstraintId::ALWAYS_FALSE, + ); + } + } + + pub(super) fn add_place(&mut self, place: ScopedPlaceId) { + let new_place = self + .place_states + .push(PlaceState::undefined(self.reachability)); + debug_assert_eq!(place, new_place); + let new_place = self.reachable_definitions.push(ReachableDefinitions { + bindings: Bindings::unbound(self.reachability), + declarations: Declarations::undeclared(self.reachability), + }); + debug_assert_eq!(place, new_place); + } + + pub(super) fn record_binding( + &mut self, + place: ScopedPlaceId, + binding: Definition<'db>, + is_place_name: bool, + ) { + self.bindings_by_definition + .insert(binding, self.place_states[place].bindings().clone()); + + let def_id = self.all_definitions.push(DefinitionState::Defined(binding)); + let place_state = &mut self.place_states[place]; + self.declarations_by_binding + .insert(binding, place_state.declarations().clone()); + place_state.record_binding( + def_id, + self.reachability, + self.is_class_scope, + is_place_name, + ); + + self.reachable_definitions[place].bindings.record_binding( + def_id, + self.reachability, + self.is_class_scope, + is_place_name, + PreviousDefinitions::AreKept, + ); + } + + pub(super) fn add_predicate( + &mut self, + predicate: PredicateOrLiteral<'db>, + ) -> ScopedPredicateId { + match predicate { + PredicateOrLiteral::Predicate(predicate) => self.predicates.add_predicate(predicate), + PredicateOrLiteral::Literal(true) => ScopedPredicateId::ALWAYS_TRUE, + PredicateOrLiteral::Literal(false) => ScopedPredicateId::ALWAYS_FALSE, + } + } + + pub(super) fn record_narrowing_constraint(&mut self, predicate: ScopedPredicateId) { + if predicate == ScopedPredicateId::ALWAYS_TRUE + || predicate == ScopedPredicateId::ALWAYS_FALSE + { + // No need to record a narrowing constraint for `True` or `False`. + return; + } + + let narrowing_constraint = predicate.into(); + for state in &mut self.place_states { + state + .record_narrowing_constraint(&mut self.narrowing_constraints, narrowing_constraint); + } + } + + /// Snapshot the state of a single place at the current point in control flow. + /// + /// This is only used for `*`-import reachability constraints, which are handled differently + /// to most other reachability constraints. See the doc-comment for + /// [`Self::record_and_negate_star_import_reachability_constraint`] for more details. + pub(super) fn single_place_snapshot(&self, place: ScopedPlaceId) -> PlaceState { + self.place_states[place].clone() + } + + /// This method exists solely for handling `*`-import reachability constraints. + /// + /// The reason why we add reachability constraints for [`Definition`]s created by `*` imports + /// is laid out in the doc-comment for `StarImportPlaceholderPredicate`. But treating these + /// reachability constraints in the use-def map the same way as all other reachability constraints + /// was shown to lead to [significant regressions] for small codebases where typeshed + /// dominates. (Although `*` imports are not common generally, they are used in several + /// important places by typeshed.) + /// + /// To solve these regressions, it was observed that we could do significantly less work for + /// `*`-import definitions. We do a number of things differently here to our normal handling of + /// reachability constraints: + /// + /// - We only apply and negate the reachability constraints to a single symbol, rather than to + /// all symbols. This is possible here because, unlike most definitions, we know in advance that + /// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly + /// which definition it is. + /// + /// - We only snapshot the state for a single place prior to the definition, rather than doing + /// expensive calls to [`Self::snapshot`]. Again, this is possible because we know + /// that only a single definition occurs inside the "if-predicate-true" predicate branch. + /// + /// - Normally we take care to check whether an "if-predicate-true" branch or an + /// "if-predicate-false" branch contains a terminal statement: these can affect the reachability + /// of symbols defined inside either branch. However, in the case of `*`-import definitions, + /// this is unnecessary (and therefore not done in this method), since we know that a `*`-import + /// predicate cannot create a terminal statement inside either branch. + /// + /// [significant regressions]: https://github.com/astral-sh/ruff/pull/17286#issuecomment-2786755746 + pub(super) fn record_and_negate_star_import_reachability_constraint( + &mut self, + reachability_id: ScopedReachabilityConstraintId, + symbol: ScopedPlaceId, + pre_definition_state: PlaceState, + ) { + let negated_reachability_id = self + .reachability_constraints + .add_not_constraint(reachability_id); + + let mut post_definition_state = + std::mem::replace(&mut self.place_states[symbol], pre_definition_state); + + post_definition_state + .record_reachability_constraint(&mut self.reachability_constraints, reachability_id); + + self.place_states[symbol].record_reachability_constraint( + &mut self.reachability_constraints, + negated_reachability_id, + ); + + self.place_states[symbol].merge( + post_definition_state, + &mut self.narrowing_constraints, + &mut self.reachability_constraints, + ); + } + + pub(super) fn record_reachability_constraint( + &mut self, + constraint: ScopedReachabilityConstraintId, + ) { + self.reachability = self + .reachability_constraints + .add_and_constraint(self.reachability, constraint); + + for state in &mut self.place_states { + state.record_reachability_constraint(&mut self.reachability_constraints, constraint); + } + } + + pub(super) fn record_declaration( + &mut self, + place: ScopedPlaceId, + declaration: Definition<'db>, + ) { + let def_id = self + .all_definitions + .push(DefinitionState::Defined(declaration)); + let place_state = &mut self.place_states[place]; + self.bindings_by_definition + .insert(declaration, place_state.bindings().clone()); + place_state.record_declaration(def_id, self.reachability); + + self.reachable_definitions[place] + .declarations + .record_declaration(def_id, self.reachability, PreviousDefinitions::AreKept); + } + + pub(super) fn record_declaration_and_binding( + &mut self, + place: ScopedPlaceId, + definition: Definition<'db>, + is_place_name: bool, + ) { + // We don't need to store anything in self.bindings_by_declaration or + // self.declarations_by_binding. + let def_id = self + .all_definitions + .push(DefinitionState::Defined(definition)); + let place_state = &mut self.place_states[place]; + place_state.record_declaration(def_id, self.reachability); + place_state.record_binding( + def_id, + self.reachability, + self.is_class_scope, + is_place_name, + ); + + self.reachable_definitions[place] + .declarations + .record_declaration(def_id, self.reachability, PreviousDefinitions::AreKept); + self.reachable_definitions[place].bindings.record_binding( + def_id, + self.reachability, + self.is_class_scope, + is_place_name, + PreviousDefinitions::AreKept, + ); + } + + pub(super) fn delete_binding(&mut self, place: ScopedPlaceId, is_place_name: bool) { + let def_id = self.all_definitions.push(DefinitionState::Deleted); + let place_state = &mut self.place_states[place]; + place_state.record_binding( + def_id, + self.reachability, + self.is_class_scope, + is_place_name, + ); + } + + pub(super) fn record_use( + &mut self, + place: ScopedPlaceId, + use_id: ScopedUseId, + node_key: NodeKey, + ) { + // We have a use of a place; clone the current bindings for that place, and record them + // as the live bindings for this use. + let new_use = self + .bindings_by_use + .push(self.place_states[place].bindings().clone()); + debug_assert_eq!(use_id, new_use); + + // Track reachability of all uses of places to silence `unresolved-reference` + // diagnostics in unreachable code. + self.record_node_reachability(node_key); + } + + pub(super) fn record_node_reachability(&mut self, node_key: NodeKey) { + self.node_reachability.insert(node_key, self.reachability); + } + + pub(super) fn snapshot_eager_state( + &mut self, + enclosing_place: ScopedPlaceId, + scope: ScopeKind, + enclosing_place_expr: &PlaceExprWithFlags, + ) -> ScopedEagerSnapshotId { + // Names bound in class scopes are never visible to nested scopes (but attributes/subscripts are visible), + // so we never need to save eager scope bindings in a class scope. + if (scope.is_class() && enclosing_place_expr.is_name()) || !enclosing_place_expr.is_bound() + { + self.eager_snapshots.push(EagerSnapshot::Constraint( + self.place_states[enclosing_place] + .bindings() + .unbound_narrowing_constraint(), + )) + } else { + self.eager_snapshots.push(EagerSnapshot::Bindings( + self.place_states[enclosing_place].bindings().clone(), + )) + } + } + + /// Take a snapshot of the current visible-places state. + pub(super) fn snapshot(&self) -> FlowSnapshot { + FlowSnapshot { + place_states: self.place_states.clone(), + reachability: self.reachability, + } + } + + /// Restore the current builder places state to the given snapshot. + pub(super) fn restore(&mut self, snapshot: FlowSnapshot) { + // We never remove places from `place_states` (it's an IndexVec, and the place + // IDs must line up), so the current number of known places must always be equal to or + // greater than the number of known places in a previously-taken snapshot. + let num_places = self.place_states.len(); + debug_assert!(num_places >= snapshot.place_states.len()); + + // Restore the current visible-definitions state to the given snapshot. + self.place_states = snapshot.place_states; + self.reachability = snapshot.reachability; + + // If the snapshot we are restoring is missing some places we've recorded since, we need + // to fill them in so the place IDs continue to line up. Since they don't exist in the + // snapshot, the correct state to fill them in with is "undefined". + self.place_states + .resize(num_places, PlaceState::undefined(self.reachability)); + } + + /// Merge the given snapshot into the current state, reflecting that we might have taken either + /// path to get here. The new state for each place should include definitions from both the + /// prior state and the snapshot. + pub(super) fn merge(&mut self, snapshot: FlowSnapshot) { + // As an optimization, if we know statically that either of the snapshots is always + // unreachable, we can leave it out of the merged result entirely. Note that we cannot + // perform any type inference at this point, so this is largely limited to unreachability + // via terminal statements. If a flow's reachability depends on an expression in the code, + // we will include the flow in the merged result; the reachability constraints of its + // bindings will include this reachability condition, so that later during type inference, + // we can determine whether any particular binding is non-visible due to unreachability. + if snapshot.reachability == ScopedReachabilityConstraintId::ALWAYS_FALSE { + return; + } + if self.reachability == ScopedReachabilityConstraintId::ALWAYS_FALSE { + self.restore(snapshot); + return; + } + + // We never remove places from `place_states` (it's an IndexVec, and the place + // IDs must line up), so the current number of known places must always be equal to or + // greater than the number of known places in a previously-taken snapshot. + debug_assert!(self.place_states.len() >= snapshot.place_states.len()); + + let mut snapshot_definitions_iter = snapshot.place_states.into_iter(); + for current in &mut self.place_states { + if let Some(snapshot) = snapshot_definitions_iter.next() { + current.merge( + snapshot, + &mut self.narrowing_constraints, + &mut self.reachability_constraints, + ); + } else { + current.merge( + PlaceState::undefined(snapshot.reachability), + &mut self.narrowing_constraints, + &mut self.reachability_constraints, + ); + // Place not present in snapshot, so it's unbound/undeclared from that path. + } + } + + self.reachability = self + .reachability_constraints + .add_or_constraint(self.reachability, snapshot.reachability); + } + + pub(super) fn finish(mut self) -> UseDefMap<'db> { + self.all_definitions.shrink_to_fit(); + self.place_states.shrink_to_fit(); + self.reachable_definitions.shrink_to_fit(); + self.bindings_by_use.shrink_to_fit(); + self.node_reachability.shrink_to_fit(); + self.declarations_by_binding.shrink_to_fit(); + self.bindings_by_definition.shrink_to_fit(); + self.eager_snapshots.shrink_to_fit(); + + UseDefMap { + all_definitions: self.all_definitions, + predicates: self.predicates.build(), + narrowing_constraints: self.narrowing_constraints.build(), + reachability_constraints: self.reachability_constraints.build(), + bindings_by_use: self.bindings_by_use, + node_reachability: self.node_reachability, + end_of_scope_places: self.place_states, + reachable_definitions: self.reachable_definitions, + declarations_by_binding: self.declarations_by_binding, + bindings_by_definition: self.bindings_by_definition, + eager_snapshots: self.eager_snapshots, + end_of_scope_reachability: self.reachability, + } + } +} diff --git a/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs new file mode 100644 index 0000000000000..116dbece85aa7 --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_index/use_def/place_state.rs @@ -0,0 +1,696 @@ +//! Track live bindings per place, applicable constraints per binding, and live declarations. +//! +//! These data structures operate entirely on scope-local newtype-indices for definitions and +//! constraints, referring to their location in the `all_definitions` and `all_constraints` +//! indexvecs in [`super::UseDefMapBuilder`]. +//! +//! We need to track arbitrary associations between bindings and constraints, not just a single set +//! of currently dominating constraints (where "dominating" means "control flow must have passed +//! through it to reach this point"), because we can have dominating constraints that apply to some +//! bindings but not others, as in this code: +//! +//! ```python +//! x = 1 if flag else None +//! if x is not None: +//! if flag2: +//! x = 2 if flag else None +//! x +//! ``` +//! +//! The `x is not None` constraint dominates the final use of `x`, but it applies only to the first +//! binding of `x`, not the second, so `None` is a possible value for `x`. +//! +//! And we can't just track, for each binding, an index into a list of dominating constraints, +//! either, because we can have bindings which are still visible, but subject to constraints that +//! are no longer dominating, as in this code: +//! +//! ```python +//! x = 0 +//! if flag1: +//! x = 1 if flag2 else None +//! assert x is not None +//! x +//! ``` +//! +//! From the point of view of the final use of `x`, the `x is not None` constraint no longer +//! dominates, but it does dominate the `x = 1 if flag2 else None` binding, so we have to keep +//! track of that. +//! +//! The data structures use `IndexVec` arenas to store all data compactly and contiguously, while +//! supporting very cheap clones. +//! +//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very +//! similar to tracking live bindings. + +use itertools::{EitherOrBoth, Itertools}; +use ruff_index::newtype_index; +use smallvec::{SmallVec, smallvec}; + +use crate::semantic_index::narrowing_constraints::{ + NarrowingConstraintsBuilder, ScopedNarrowingConstraint, ScopedNarrowingConstraintPredicate, +}; +use crate::semantic_index::reachability_constraints::{ + ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId, +}; + +/// A newtype-index for a definition in a particular scope. +#[newtype_index] +#[derive(Ord, PartialOrd, get_size2::GetSize)] +pub(super) struct ScopedDefinitionId; + +impl ScopedDefinitionId { + /// A special ID that is used to describe an implicit start-of-scope state. When + /// we see that this definition is live, we know that the place is (possibly) + /// unbound or undeclared at a given usage site. + /// When creating a use-def-map builder, we always add an empty `DefinitionState::Undefined` definition + /// at index 0, so this ID is always present. + pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0); + + fn is_unbound(self) -> bool { + self == Self::UNBOUND + } +} + +/// Can keep inline this many live bindings or declarations per place at a given time; more will +/// go to heap. +const INLINE_DEFINITIONS_PER_PLACE: usize = 4; + +/// Live declarations for a single place at some point in control flow, with their +/// corresponding reachability constraints. +#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) struct Declarations { + /// A list of live declarations for this place, sorted by their `ScopedDefinitionId` + live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_PLACE]>, +} + +/// One of the live declarations for a single place at some point in control flow. +#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +pub(super) struct LiveDeclaration { + pub(super) declaration: ScopedDefinitionId, + pub(super) reachability_constraint: ScopedReachabilityConstraintId, +} + +pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>; + +#[derive(Clone, Copy, Debug)] +pub(super) enum PreviousDefinitions { + AreShadowed, + AreKept, +} + +impl PreviousDefinitions { + pub(super) fn are_shadowed(self) -> bool { + matches!(self, PreviousDefinitions::AreShadowed) + } +} + +impl Declarations { + pub(super) fn undeclared(reachability_constraint: ScopedReachabilityConstraintId) -> Self { + let initial_declaration = LiveDeclaration { + declaration: ScopedDefinitionId::UNBOUND, + reachability_constraint, + }; + Self { + live_declarations: smallvec![initial_declaration], + } + } + + /// Record a newly-encountered declaration for this place. + pub(super) fn record_declaration( + &mut self, + declaration: ScopedDefinitionId, + reachability_constraint: ScopedReachabilityConstraintId, + previous_definitions: PreviousDefinitions, + ) { + if previous_definitions.are_shadowed() { + // The new declaration replaces all previous live declaration in this path. + self.live_declarations.clear(); + } + self.live_declarations.push(LiveDeclaration { + declaration, + reachability_constraint, + }); + } + + /// Add given reachability constraint to all live declarations. + pub(super) fn record_reachability_constraint( + &mut self, + reachability_constraints: &mut ReachabilityConstraintsBuilder, + constraint: ScopedReachabilityConstraintId, + ) { + for declaration in &mut self.live_declarations { + declaration.reachability_constraint = reachability_constraints + .add_and_constraint(declaration.reachability_constraint, constraint); + } + } + + /// Return an iterator over live declarations for this place. + pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> { + self.live_declarations.iter() + } + + fn merge(&mut self, b: Self, reachability_constraints: &mut ReachabilityConstraintsBuilder) { + let a = std::mem::take(self); + + // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that + // the merged `live_declarations` vec remains sorted. If a definition is found in both `a` + // and `b`, we compose the constraints from the two paths in an appropriate way + // (intersection for narrowing constraints; ternary OR for reachability constraints). If a + // definition is found in only one path, it is used as-is. + let a = a.live_declarations.into_iter(); + let b = b.live_declarations.into_iter(); + for zipped in a.merge_join_by(b, |a, b| a.declaration.cmp(&b.declaration)) { + match zipped { + EitherOrBoth::Both(a, b) => { + let reachability_constraint = reachability_constraints + .add_or_constraint(a.reachability_constraint, b.reachability_constraint); + self.live_declarations.push(LiveDeclaration { + declaration: a.declaration, + reachability_constraint, + }); + } + + EitherOrBoth::Left(declaration) | EitherOrBoth::Right(declaration) => { + self.live_declarations.push(declaration); + } + } + } + } +} + +/// A snapshot of a place state that can be used to resolve a reference in a nested eager scope. +/// If there are bindings in a (non-class) scope , they are stored in `Bindings`. +/// Even if it's a class scope (class variables are not visible to nested scopes) or there are no +/// bindings, the current narrowing constraint is necessary for narrowing, so it's stored in +/// `Constraint`. +#[derive(Clone, Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) enum EagerSnapshot { + Constraint(ScopedNarrowingConstraint), + Bindings(Bindings), +} + +/// Live bindings for a single place at some point in control flow. Each live binding comes +/// with a set of narrowing constraints and a reachability constraint. +#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) struct Bindings { + /// The narrowing constraint applicable to the "unbound" binding, if we need access to it even + /// when it's not visible. This happens in class scopes, where local name bindings are not visible + /// to nested scopes, but we still need to know what narrowing constraints were applied to the + /// "unbound" binding. + unbound_narrowing_constraint: Option, + /// A list of live bindings for this place, sorted by their `ScopedDefinitionId` + live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_PLACE]>, +} + +impl Bindings { + pub(super) fn unbound_narrowing_constraint(&self) -> ScopedNarrowingConstraint { + self.unbound_narrowing_constraint + .unwrap_or(self.live_bindings[0].narrowing_constraint) + } +} + +/// One of the live bindings for a single place at some point in control flow. +#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +pub(super) struct LiveBinding { + pub(super) binding: ScopedDefinitionId, + pub(super) narrowing_constraint: ScopedNarrowingConstraint, + pub(super) reachability_constraint: ScopedReachabilityConstraintId, +} + +pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>; + +impl Bindings { + pub(super) fn unbound(reachability_constraint: ScopedReachabilityConstraintId) -> Self { + let initial_binding = LiveBinding { + binding: ScopedDefinitionId::UNBOUND, + narrowing_constraint: ScopedNarrowingConstraint::empty(), + reachability_constraint, + }; + Self { + unbound_narrowing_constraint: None, + live_bindings: smallvec![initial_binding], + } + } + + /// Record a newly-encountered binding for this place. + pub(super) fn record_binding( + &mut self, + binding: ScopedDefinitionId, + reachability_constraint: ScopedReachabilityConstraintId, + is_class_scope: bool, + is_place_name: bool, + previous_definitions: PreviousDefinitions, + ) { + // If we are in a class scope, and the unbound name binding was previously visible, but we will + // now replace it, record the narrowing constraints on it: + if is_class_scope && is_place_name && self.live_bindings[0].binding.is_unbound() { + self.unbound_narrowing_constraint = Some(self.live_bindings[0].narrowing_constraint); + } + // The new binding replaces all previous live bindings in this path, and has no + // constraints. + if previous_definitions.are_shadowed() { + self.live_bindings.clear(); + } + self.live_bindings.push(LiveBinding { + binding, + narrowing_constraint: ScopedNarrowingConstraint::empty(), + reachability_constraint, + }); + } + + /// Add given constraint to all live bindings. + pub(super) fn record_narrowing_constraint( + &mut self, + narrowing_constraints: &mut NarrowingConstraintsBuilder, + predicate: ScopedNarrowingConstraintPredicate, + ) { + for binding in &mut self.live_bindings { + binding.narrowing_constraint = narrowing_constraints + .add_predicate_to_constraint(binding.narrowing_constraint, predicate); + } + } + + /// Add given reachability constraint to all live bindings. + pub(super) fn record_reachability_constraint( + &mut self, + reachability_constraints: &mut ReachabilityConstraintsBuilder, + constraint: ScopedReachabilityConstraintId, + ) { + for binding in &mut self.live_bindings { + binding.reachability_constraint = reachability_constraints + .add_and_constraint(binding.reachability_constraint, constraint); + } + } + + /// Iterate over currently live bindings for this place + pub(super) fn iter(&self) -> LiveBindingsIterator<'_> { + self.live_bindings.iter() + } + + fn merge( + &mut self, + b: Self, + narrowing_constraints: &mut NarrowingConstraintsBuilder, + reachability_constraints: &mut ReachabilityConstraintsBuilder, + ) { + let a = std::mem::take(self); + + if let Some((a, b)) = a + .unbound_narrowing_constraint + .zip(b.unbound_narrowing_constraint) + { + self.unbound_narrowing_constraint = + Some(narrowing_constraints.intersect_constraints(a, b)); + } + + // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that + // the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and + // `b`, we compose the constraints from the two paths in an appropriate way (intersection + // for narrowing constraints; ternary OR for reachability constraints). If a definition is + // found in only one path, it is used as-is. + let a = a.live_bindings.into_iter(); + let b = b.live_bindings.into_iter(); + for zipped in a.merge_join_by(b, |a, b| a.binding.cmp(&b.binding)) { + match zipped { + EitherOrBoth::Both(a, b) => { + // If the same definition is visible through both paths, any constraint + // that applies on only one path is irrelevant to the resulting type from + // unioning the two paths, so we intersect the constraints. + let narrowing_constraint = narrowing_constraints + .intersect_constraints(a.narrowing_constraint, b.narrowing_constraint); + + // For reachability constraints, we merge them using a ternary OR operation: + let reachability_constraint = reachability_constraints + .add_or_constraint(a.reachability_constraint, b.reachability_constraint); + + self.live_bindings.push(LiveBinding { + binding: a.binding, + narrowing_constraint, + reachability_constraint, + }); + } + + EitherOrBoth::Left(binding) | EitherOrBoth::Right(binding) => { + self.live_bindings.push(binding); + } + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +pub(in crate::semantic_index) struct PlaceState { + declarations: Declarations, + bindings: Bindings, +} + +impl PlaceState { + /// Return a new [`PlaceState`] representing an unbound, undeclared place. + pub(super) fn undefined(reachability: ScopedReachabilityConstraintId) -> Self { + Self { + declarations: Declarations::undeclared(reachability), + bindings: Bindings::unbound(reachability), + } + } + + /// Record a newly-encountered binding for this place. + pub(super) fn record_binding( + &mut self, + binding_id: ScopedDefinitionId, + reachability_constraint: ScopedReachabilityConstraintId, + is_class_scope: bool, + is_place_name: bool, + ) { + debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND); + self.bindings.record_binding( + binding_id, + reachability_constraint, + is_class_scope, + is_place_name, + PreviousDefinitions::AreShadowed, + ); + } + + /// Add given constraint to all live bindings. + pub(super) fn record_narrowing_constraint( + &mut self, + narrowing_constraints: &mut NarrowingConstraintsBuilder, + constraint: ScopedNarrowingConstraintPredicate, + ) { + self.bindings + .record_narrowing_constraint(narrowing_constraints, constraint); + } + + /// Add given reachability constraint to all live bindings. + pub(super) fn record_reachability_constraint( + &mut self, + reachability_constraints: &mut ReachabilityConstraintsBuilder, + constraint: ScopedReachabilityConstraintId, + ) { + self.bindings + .record_reachability_constraint(reachability_constraints, constraint); + self.declarations + .record_reachability_constraint(reachability_constraints, constraint); + } + + /// Record a newly-encountered declaration of this place. + pub(super) fn record_declaration( + &mut self, + declaration_id: ScopedDefinitionId, + reachability_constraint: ScopedReachabilityConstraintId, + ) { + self.declarations.record_declaration( + declaration_id, + reachability_constraint, + PreviousDefinitions::AreShadowed, + ); + } + + /// Merge another [`PlaceState`] into this one. + pub(super) fn merge( + &mut self, + b: PlaceState, + narrowing_constraints: &mut NarrowingConstraintsBuilder, + reachability_constraints: &mut ReachabilityConstraintsBuilder, + ) { + self.bindings + .merge(b.bindings, narrowing_constraints, reachability_constraints); + self.declarations + .merge(b.declarations, reachability_constraints); + } + + pub(super) fn bindings(&self) -> &Bindings { + &self.bindings + } + + pub(super) fn declarations(&self) -> &Declarations { + &self.declarations + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ruff_index::Idx; + + use crate::semantic_index::predicate::ScopedPredicateId; + + #[track_caller] + fn assert_bindings( + narrowing_constraints: &NarrowingConstraintsBuilder, + place: &PlaceState, + expected: &[&str], + ) { + let actual = place + .bindings() + .iter() + .map(|live_binding| { + let def_id = live_binding.binding; + let def = if def_id == ScopedDefinitionId::UNBOUND { + "unbound".into() + } else { + def_id.as_u32().to_string() + }; + let predicates = narrowing_constraints + .iter_predicates(live_binding.narrowing_constraint) + .map(|idx| idx.as_u32().to_string()) + .collect::>() + .join(", "); + format!("{def}<{predicates}>") + }) + .collect::>(); + assert_eq!(actual, expected); + } + + #[track_caller] + pub(crate) fn assert_declarations(place: &PlaceState, expected: &[&str]) { + let actual = place + .declarations() + .iter() + .map( + |LiveDeclaration { + declaration, + reachability_constraint: _, + }| { + if *declaration == ScopedDefinitionId::UNBOUND { + "undeclared".into() + } else { + declaration.as_u32().to_string() + } + }, + ) + .collect::>(); + assert_eq!(actual, expected); + } + + #[test] + fn unbound() { + let narrowing_constraints = NarrowingConstraintsBuilder::default(); + let sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + + assert_bindings(&narrowing_constraints, &sym, &["unbound<>"]); + } + + #[test] + fn with() { + let narrowing_constraints = NarrowingConstraintsBuilder::default(); + let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym.record_binding( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + + assert_bindings(&narrowing_constraints, &sym, &["1<>"]); + } + + #[test] + fn record_constraint() { + let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); + let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym.record_binding( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + let predicate = ScopedPredicateId::new(0).into(); + sym.record_narrowing_constraint(&mut narrowing_constraints, predicate); + + assert_bindings(&narrowing_constraints, &sym, &["1<0>"]); + } + + #[test] + fn merge() { + let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); + let mut reachability_constraints = ReachabilityConstraintsBuilder::default(); + + // merging the same definition with the same constraint keeps the constraint + let mut sym1a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym1a.record_binding( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + let predicate = ScopedPredicateId::new(0).into(); + sym1a.record_narrowing_constraint(&mut narrowing_constraints, predicate); + + let mut sym1b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym1b.record_binding( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + let predicate = ScopedPredicateId::new(0).into(); + sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); + + sym1a.merge( + sym1b, + &mut narrowing_constraints, + &mut reachability_constraints, + ); + let mut sym1 = sym1a; + assert_bindings(&narrowing_constraints, &sym1, &["1<0>"]); + + // merging the same definition with differing constraints drops all constraints + let mut sym2a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym2a.record_binding( + ScopedDefinitionId::from_u32(2), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + let predicate = ScopedPredicateId::new(1).into(); + sym2a.record_narrowing_constraint(&mut narrowing_constraints, predicate); + + let mut sym1b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym1b.record_binding( + ScopedDefinitionId::from_u32(2), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + let predicate = ScopedPredicateId::new(2).into(); + sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); + + sym2a.merge( + sym1b, + &mut narrowing_constraints, + &mut reachability_constraints, + ); + let sym2 = sym2a; + assert_bindings(&narrowing_constraints, &sym2, &["2<>"]); + + // merging a constrained definition with unbound keeps both + let mut sym3a = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym3a.record_binding( + ScopedDefinitionId::from_u32(3), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + false, + true, + ); + let predicate = ScopedPredicateId::new(3).into(); + sym3a.record_narrowing_constraint(&mut narrowing_constraints, predicate); + + let sym2b = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + + sym3a.merge( + sym2b, + &mut narrowing_constraints, + &mut reachability_constraints, + ); + let sym3 = sym3a; + assert_bindings(&narrowing_constraints, &sym3, &["unbound<>", "3<3>"]); + + // merging different definitions keeps them each with their existing constraints + sym1.merge( + sym3, + &mut narrowing_constraints, + &mut reachability_constraints, + ); + let sym = sym1; + assert_bindings(&narrowing_constraints, &sym, &["unbound<>", "1<0>", "3<3>"]); + } + + #[test] + fn no_declaration() { + let sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + + assert_declarations(&sym, &["undeclared"]); + } + + #[test] + fn record_declaration() { + let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym.record_declaration( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + + assert_declarations(&sym, &["1"]); + } + + #[test] + fn record_declaration_override() { + let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym.record_declaration( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + sym.record_declaration( + ScopedDefinitionId::from_u32(2), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + + assert_declarations(&sym, &["2"]); + } + + #[test] + fn record_declaration_merge() { + let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); + let mut reachability_constraints = ReachabilityConstraintsBuilder::default(); + let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym.record_declaration( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + + let mut sym2 = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym2.record_declaration( + ScopedDefinitionId::from_u32(2), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + + sym.merge( + sym2, + &mut narrowing_constraints, + &mut reachability_constraints, + ); + + assert_declarations(&sym, &["1", "2"]); + } + + #[test] + fn record_declaration_merge_partial_undeclared() { + let mut narrowing_constraints = NarrowingConstraintsBuilder::default(); + let mut reachability_constraints = ReachabilityConstraintsBuilder::default(); + let mut sym = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + sym.record_declaration( + ScopedDefinitionId::from_u32(1), + ScopedReachabilityConstraintId::ALWAYS_TRUE, + ); + + let sym2 = PlaceState::undefined(ScopedReachabilityConstraintId::ALWAYS_TRUE); + + sym.merge( + sym2, + &mut narrowing_constraints, + &mut reachability_constraints, + ); + + assert_declarations(&sym, &["undeclared", "1"]); + } +} diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs new file mode 100644 index 0000000000000..e26992a39a9de --- /dev/null +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -0,0 +1,473 @@ +use ruff_db::files::{File, FilePath}; +use ruff_db::source::line_index; +use ruff_python_ast as ast; +use ruff_python_ast::{Expr, ExprRef, name::Name}; +use ruff_source_file::LineIndex; + +use crate::Db; +use crate::module_name::ModuleName; +use crate::module_resolver::{KnownModule, Module, resolve_module}; +use crate::semantic_index::place::FileScopeId; +use crate::semantic_index::semantic_index; +use crate::types::ide_support::all_declarations_and_bindings; +use crate::types::{Type, binding_type, infer_scope_types}; + +pub struct SemanticModel<'db> { + db: &'db dyn Db, + file: File, +} + +impl<'db> SemanticModel<'db> { + pub fn new(db: &'db dyn Db, file: File) -> Self { + Self { db, file } + } + + // TODO we don't actually want to expose the Db directly to lint rules, but we need to find a + // solution for exposing information from types + pub fn db(&self) -> &dyn Db { + self.db + } + + pub fn file_path(&self) -> &FilePath { + self.file.path(self.db) + } + + pub fn line_index(&self) -> LineIndex { + line_index(self.db, self.file) + } + + pub fn resolve_module(&self, module_name: &ModuleName) -> Option { + resolve_module(self.db, module_name) + } + + /// Returns completions for symbols available in a `from module import ` context. + pub fn import_completions( + &self, + import: &ast::StmtImportFrom, + _name: Option, + ) -> Vec> { + let module_name = match ModuleName::from_import_statement(self.db, self.file, import) { + Ok(module_name) => module_name, + Err(err) => { + tracing::debug!( + "Could not extract module name from `{module:?}` with level {level}: {err:?}", + module = import.module, + level = import.level, + ); + return vec![]; + } + }; + self.module_completions(&module_name) + } + + /// Returns completions for symbols available in the given module as if + /// it were imported by this model's `File`. + fn module_completions(&self, module_name: &ModuleName) -> Vec> { + let Some(module) = resolve_module(self.db, module_name) else { + tracing::debug!("Could not resolve module from `{module_name:?}`"); + return vec![]; + }; + let ty = Type::module_literal(self.db, self.file, &module); + let builtin = module.is_known(KnownModule::Builtins); + crate::types::all_members(self.db, ty) + .into_iter() + .map(|member| Completion { + name: member.name, + ty: member.ty, + builtin, + }) + .collect() + } + + /// Returns completions for symbols available in a `object.` context. + pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec> { + let ty = node.value.inferred_type(self); + crate::types::all_members(self.db, ty) + .into_iter() + .map(|member| Completion { + name: member.name, + ty: member.ty, + builtin: false, + }) + .collect() + } + + /// Returns completions for symbols available in the scope containing the + /// given expression. + /// + /// If a scope could not be determined, then completions for the global + /// scope of this model's `File` are returned. + pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec> { + let index = semantic_index(self.db, self.file); + + // TODO: We currently use `try_expression_scope_id` here as a hotfix for [1]. + // Revert this to use `expression_scope_id` once a proper fix is in place. + // + // [1] https://github.com/astral-sh/ty/issues/572 + let Some(file_scope) = (match node { + ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), + node => match node.as_expr_ref() { + // If we couldn't identify a specific + // expression that we're in, then just + // fall back to the global scope. + None => Some(FileScopeId::global()), + Some(expr) => index.try_expression_scope_id(expr), + }, + }) else { + return vec![]; + }; + let mut completions = vec![]; + for (file_scope, _) in index.ancestor_scopes(file_scope) { + completions.extend( + all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) + .map(|member| Completion { + name: member.name, + ty: member.ty, + builtin: false, + }), + ); + } + // Builtins are available in all scopes. + let builtins = ModuleName::new("builtins").expect("valid module name"); + completions.extend(self.module_completions(&builtins)); + completions + } +} + +/// A classification of symbol names. +/// +/// The ordering here is used for sorting completions. +/// +/// This sorts "normal" names first, then dunder names and finally +/// single-underscore names. This matches the order of the variants defined for +/// this enum, which is in turn picked up by the derived trait implementation +/// for `Ord`. +#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +pub enum NameKind { + Normal, + Dunder, + Sunder, +} + +impl NameKind { + pub fn classify(name: &Name) -> NameKind { + // Dunder needs a prefix and suffix double underscore. + // When there's only a prefix double underscore, this + // results in explicit name mangling. We let that be + // classified as-if they were single underscore names. + // + // Ref: + if name.starts_with("__") && name.ends_with("__") { + NameKind::Dunder + } else if name.starts_with('_') { + NameKind::Sunder + } else { + NameKind::Normal + } + } +} + +/// A suggestion for code completion. +#[derive(Clone, Debug)] +pub struct Completion<'db> { + /// The label shown to the user for this suggestion. + pub name: Name, + /// The type of this completion. + pub ty: Type<'db>, + /// Whether this suggestion came from builtins or not. + /// + /// At time of writing (2025-06-26), this information + /// doesn't make it into the LSP response. Instead, we + /// use it mainly in tests so that we can write less + /// noisy tests. + pub builtin: bool, +} + +impl<'db> Completion<'db> { + /// Returns the "kind" of this completion. + /// + /// This is meant to be a very general classification of this completion. + /// Typically, this is communicated from the LSP server to a client, and + /// the client uses this information to help improve the UX (perhaps by + /// assigning an icon of some kind to the completion). + pub fn kind(&self, db: &'db dyn Db) -> Option { + fn imp<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { + Some(match ty { + Type::FunctionLiteral(_) + | Type::DataclassDecorator(_) + | Type::WrapperDescriptor(_) + | Type::DataclassTransformer(_) + | Type::Callable(_) => CompletionKind::Function, + Type::BoundMethod(_) | Type::MethodWrapper(_) => CompletionKind::Method, + Type::ModuleLiteral(_) => CompletionKind::Module, + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { + CompletionKind::Class + } + // This is a little weird for "struct." I'm mostly interpreting + // "struct" here as a more general "object." ---AG + Type::NominalInstance(_) + | Type::PropertyInstance(_) + | Type::Tuple(_) + | Type::BoundSuper(_) => CompletionKind::Struct, + Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::TypeIs(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) => CompletionKind::Value, + Type::ProtocolInstance(_) => CompletionKind::Interface, + Type::TypeVar(_) => CompletionKind::TypeParameter, + Type::Union(union) => union.elements(db).iter().find_map(|&ty| imp(db, ty))?, + Type::Intersection(intersection) => { + intersection.iter_positive(db).find_map(|ty| imp(db, ty))? + } + Type::Dynamic(_) + | Type::Never + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy => return None, + }) + } + imp(db, self.ty) + } +} + +/// The "kind" of a completion. +/// +/// This is taken directly from the LSP completion specification: +/// +/// +/// The idea here is that `Completion::kind` defines the mapping to this from +/// `Type` (and possibly other information), which might be interesting and +/// contentious. Then the outer edges map this to the LSP types, which is +/// expected to be mundane and boring. +#[derive(Clone, Copy, Debug)] +pub enum CompletionKind { + Text, + Method, + Function, + Constructor, + Field, + Variable, + Class, + Interface, + Module, + Property, + Unit, + Value, + Enum, + Keyword, + Snippet, + Color, + File, + Reference, + Folder, + EnumMember, + Constant, + Struct, + Event, + Operator, + TypeParameter, +} + +pub trait HasType { + /// Returns the inferred type of `self`. + /// + /// ## Panics + /// May panic if `self` is from another file than `model`. + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>; +} + +impl HasType for ast::ExprRef<'_> { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + let index = semantic_index(model.db, model.file); + let file_scope = index.expression_scope_id(*self); + let scope = file_scope.to_scope_id(model.db, model.file); + + infer_scope_types(model.db, scope).expression_type(*self) + } +} + +macro_rules! impl_expression_has_type { + ($ty: ty) => { + impl HasType for $ty { + #[inline] + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + let expression_ref = ExprRef::from(self); + expression_ref.inferred_type(model) + } + } + }; +} + +impl_expression_has_type!(ast::ExprBoolOp); +impl_expression_has_type!(ast::ExprNamed); +impl_expression_has_type!(ast::ExprBinOp); +impl_expression_has_type!(ast::ExprUnaryOp); +impl_expression_has_type!(ast::ExprLambda); +impl_expression_has_type!(ast::ExprIf); +impl_expression_has_type!(ast::ExprDict); +impl_expression_has_type!(ast::ExprSet); +impl_expression_has_type!(ast::ExprListComp); +impl_expression_has_type!(ast::ExprSetComp); +impl_expression_has_type!(ast::ExprDictComp); +impl_expression_has_type!(ast::ExprGenerator); +impl_expression_has_type!(ast::ExprAwait); +impl_expression_has_type!(ast::ExprYield); +impl_expression_has_type!(ast::ExprYieldFrom); +impl_expression_has_type!(ast::ExprCompare); +impl_expression_has_type!(ast::ExprCall); +impl_expression_has_type!(ast::ExprFString); +impl_expression_has_type!(ast::ExprTString); +impl_expression_has_type!(ast::ExprStringLiteral); +impl_expression_has_type!(ast::ExprBytesLiteral); +impl_expression_has_type!(ast::ExprNumberLiteral); +impl_expression_has_type!(ast::ExprBooleanLiteral); +impl_expression_has_type!(ast::ExprNoneLiteral); +impl_expression_has_type!(ast::ExprEllipsisLiteral); +impl_expression_has_type!(ast::ExprAttribute); +impl_expression_has_type!(ast::ExprSubscript); +impl_expression_has_type!(ast::ExprStarred); +impl_expression_has_type!(ast::ExprName); +impl_expression_has_type!(ast::ExprList); +impl_expression_has_type!(ast::ExprTuple); +impl_expression_has_type!(ast::ExprSlice); +impl_expression_has_type!(ast::ExprIpyEscapeCommand); + +impl HasType for ast::Expr { + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + match self { + Expr::BoolOp(inner) => inner.inferred_type(model), + Expr::Named(inner) => inner.inferred_type(model), + Expr::BinOp(inner) => inner.inferred_type(model), + Expr::UnaryOp(inner) => inner.inferred_type(model), + Expr::Lambda(inner) => inner.inferred_type(model), + Expr::If(inner) => inner.inferred_type(model), + Expr::Dict(inner) => inner.inferred_type(model), + Expr::Set(inner) => inner.inferred_type(model), + Expr::ListComp(inner) => inner.inferred_type(model), + Expr::SetComp(inner) => inner.inferred_type(model), + Expr::DictComp(inner) => inner.inferred_type(model), + Expr::Generator(inner) => inner.inferred_type(model), + Expr::Await(inner) => inner.inferred_type(model), + Expr::Yield(inner) => inner.inferred_type(model), + Expr::YieldFrom(inner) => inner.inferred_type(model), + Expr::Compare(inner) => inner.inferred_type(model), + Expr::Call(inner) => inner.inferred_type(model), + Expr::FString(inner) => inner.inferred_type(model), + Expr::TString(inner) => inner.inferred_type(model), + Expr::StringLiteral(inner) => inner.inferred_type(model), + Expr::BytesLiteral(inner) => inner.inferred_type(model), + Expr::NumberLiteral(inner) => inner.inferred_type(model), + Expr::BooleanLiteral(inner) => inner.inferred_type(model), + Expr::NoneLiteral(inner) => inner.inferred_type(model), + Expr::EllipsisLiteral(inner) => inner.inferred_type(model), + Expr::Attribute(inner) => inner.inferred_type(model), + Expr::Subscript(inner) => inner.inferred_type(model), + Expr::Starred(inner) => inner.inferred_type(model), + Expr::Name(inner) => inner.inferred_type(model), + Expr::List(inner) => inner.inferred_type(model), + Expr::Tuple(inner) => inner.inferred_type(model), + Expr::Slice(inner) => inner.inferred_type(model), + Expr::IpyEscapeCommand(inner) => inner.inferred_type(model), + } + } +} + +macro_rules! impl_binding_has_ty { + ($ty: ty) => { + impl HasType for $ty { + #[inline] + fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { + let index = semantic_index(model.db, model.file); + let binding = index.expect_single_definition(self); + binding_type(model.db, binding) + } + } + }; +} + +impl_binding_has_ty!(ast::StmtFunctionDef); +impl_binding_has_ty!(ast::StmtClassDef); +impl_binding_has_ty!(ast::Parameter); +impl_binding_has_ty!(ast::ParameterWithDefault); +impl_binding_has_ty!(ast::ExceptHandlerExceptHandler); + +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; + use ruff_db::parsed::parsed_module; + + use crate::db::tests::TestDbBuilder; + use crate::{HasType, SemanticModel}; + + #[test] + fn function_type() -> anyhow::Result<()> { + let db = TestDbBuilder::new() + .with_file("/src/foo.py", "def test(): pass") + .build()?; + + let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); + + let ast = parsed_module(&db, foo).load(&db); + + let function = ast.suite()[0].as_function_def_stmt().unwrap(); + let model = SemanticModel::new(&db, foo); + let ty = function.inferred_type(&model); + + assert!(ty.is_function_literal()); + + Ok(()) + } + + #[test] + fn class_type() -> anyhow::Result<()> { + let db = TestDbBuilder::new() + .with_file("/src/foo.py", "class Test: pass") + .build()?; + + let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); + + let ast = parsed_module(&db, foo).load(&db); + + let class = ast.suite()[0].as_class_def_stmt().unwrap(); + let model = SemanticModel::new(&db, foo); + let ty = class.inferred_type(&model); + + assert!(ty.is_class_literal()); + + Ok(()) + } + + #[test] + fn alias_type() -> anyhow::Result<()> { + let db = TestDbBuilder::new() + .with_file("/src/foo.py", "class Test: pass") + .with_file("/src/bar.py", "from foo import Test") + .build()?; + + let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); + + let ast = parsed_module(&db, bar).load(&db); + + let import = ast.suite()[0].as_import_from_stmt().unwrap(); + let alias = &import.names[0]; + let model = SemanticModel::new(&db, bar); + let ty = alias.inferred_type(&model); + + assert!(ty.is_class_literal()); + + Ok(()) + } +} diff --git a/crates/ty_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs new file mode 100644 index 0000000000000..a6b2b8214c65a --- /dev/null +++ b/crates/ty_python_semantic/src/site_packages.rs @@ -0,0 +1,1847 @@ +//! Utilities for finding the `site-packages` directory, +//! into which third-party packages are installed. +//! +//! The routines exposed by this module have different behaviour depending +//! on the platform of the *host machine*, which may be +//! different from the *target platform for type checking*. (A user +//! might be running ty on a Windows machine, but might +//! reasonably ask us to type-check code assuming that the code runs +//! on Linux.) + +use std::io; +use std::num::NonZeroUsize; +use std::ops::Deref; +use std::str::FromStr; +use std::{fmt, sync::Arc}; + +use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource}; +use camino::Utf8Component; +use indexmap::IndexSet; +use ruff_annotate_snippets::{Level, Renderer, Snippet}; +use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_python_ast::PythonVersion; +use ruff_python_trivia::Cursor; +use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; +use ruff_text_size::{TextLen, TextRange}; +use ty_static::EnvVars; + +type SitePackagesDiscoveryResult = Result; + +/// An ordered, deduplicated set of `site-packages` search paths. +/// +/// Most environments will only have one `site-packages` directory. +/// Some virtual environments created with `--system-site-packages` +/// will also have the system installation's `site-packages` packages +/// available, however. Ephemeral environments created with `uv` in +/// `uv run --with` invocations, meanwhile, "extend" a parent environment +/// (which could be another virtual environment or a system installation, +/// and which could itself have multiple `site-packages` directories). +/// +/// We use an `IndexSet` here to guard against the (very remote) +/// possibility that an environment might somehow be marked as being +/// both a `--system-site-packages` virtual environment *and* an +/// ephemeral environment that extends the system environment. If this +/// were the case, the system environment's `site-packages` directory +/// *might* be added to the `SitePackagesPaths` twice, but we wouldn't +/// want duplicates to appear in this set. +#[derive(Debug, PartialEq, Eq, Default)] +pub struct SitePackagesPaths(IndexSet); + +impl SitePackagesPaths { + fn single(path: SystemPathBuf) -> Self { + Self(IndexSet::from([path])) + } + + fn insert(&mut self, path: SystemPathBuf) { + self.0.insert(path); + } + + fn extend(&mut self, other: Self) { + self.0.extend(other.0); + } + + /// Tries to detect the version from the layout of the `site-packages` directory. + pub fn python_version_from_layout(&self) -> Option { + if cfg!(windows) { + // The path to `site-packages` on Unix is + // `/lib/pythonX.Y/site-packages`, + // but on Windows it's `/Lib/site-packages`. + return None; + } + + let primary_site_packages = self.0.first()?; + + let mut site_packages_ancestor_components = + primary_site_packages.components().rev().skip(1).map(|c| { + // This should have all been validated in `site_packages.rs` + // when we resolved the search paths for the project. + debug_assert!( + matches!(c, Utf8Component::Normal(_)), + "Unexpected component in site-packages path `{c:?}` \ + (expected `site-packages` to be an absolute path with symlinks resolved, \ + located at `/lib/pythonX.Y/site-packages`)" + ); + + c.as_str() + }); + + let parent_component = site_packages_ancestor_components.next()?; + + if site_packages_ancestor_components.next()? != "lib" { + return None; + } + + let version = parent_component + .strip_prefix("python") + .or_else(|| parent_component.strip_prefix("pypy"))? + .trim_end_matches('t'); + + let version = PythonVersion::from_str(version).ok()?; + let source = PythonVersionSource::InstallationDirectoryLayout { + site_packages_parent_dir: Box::from(parent_component), + }; + + Some(PythonVersionWithSource { version, source }) + } + + pub fn into_vec(self) -> Vec { + self.0.into_iter().collect() + } +} + +impl FromIterator for SitePackagesPaths { + fn from_iter>(iter: T) -> Self { + Self(IndexSet::from_iter(iter)) + } +} + +impl IntoIterator for SitePackagesPaths { + type Item = SystemPathBuf; + type IntoIter = indexmap::set::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl PartialEq<&[SystemPathBuf]> for SitePackagesPaths { + fn eq(&self, other: &&[SystemPathBuf]) -> bool { + self.0.as_slice() == *other + } +} + +#[derive(Debug)] +pub enum PythonEnvironment { + Virtual(VirtualEnvironment), + System(SystemEnvironment), +} + +impl PythonEnvironment { + pub fn discover( + project_root: &SystemPath, + system: &dyn System, + ) -> Result, SitePackagesDiscoveryError> { + fn resolve_environment( + system: &dyn System, + path: &SystemPath, + origin: SysPrefixPathOrigin, + ) -> Result { + tracing::debug!("Resolving {origin}: {path}"); + PythonEnvironment::new(path, origin, system) + } + + if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) { + return resolve_environment( + system, + SystemPath::new(&virtual_env), + SysPrefixPathOrigin::VirtualEnvVar, + ) + .map(Some); + } + + if let Ok(conda_env) = system.env_var(EnvVars::CONDA_PREFIX) { + return resolve_environment( + system, + SystemPath::new(&conda_env), + SysPrefixPathOrigin::CondaPrefixVar, + ) + .map(Some); + } + + tracing::debug!("Discovering virtual environment in `{project_root}`"); + let virtual_env_directory = project_root.join(".venv"); + + match PythonEnvironment::new( + &virtual_env_directory, + SysPrefixPathOrigin::LocalVenv, + system, + ) { + Ok(environment) => return Ok(Some(environment)), + Err(err) => { + if system.is_directory(&virtual_env_directory) { + tracing::debug!( + "Ignoring automatically detected virtual environment at `{}`: {}", + &virtual_env_directory, + err + ); + } + } + } + + Ok(None) + } + + pub fn new( + path: impl AsRef, + origin: SysPrefixPathOrigin, + system: &dyn System, + ) -> SitePackagesDiscoveryResult { + let path = SysPrefixPath::new(path.as_ref(), origin, system)?; + + // Attempt to inspect as a virtual environment first + match VirtualEnvironment::new(path, system) { + Ok(venv) => Ok(Self::Virtual(venv)), + // If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment + Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(path, _)) + if !path.origin.must_be_virtual_env() => + { + Ok(Self::System(SystemEnvironment::new(path))) + } + Err(err) => Err(err), + } + } + + /// Returns the Python version that was used to create this environment + /// (will only be available for virtual environments that specify + /// the metadata in their `pyvenv.cfg` files). + pub fn python_version_from_metadata(&self) -> Option<&PythonVersionWithSource> { + match self { + Self::Virtual(venv) => venv.version.as_ref(), + Self::System(_) => None, + } + } + + pub fn site_packages_paths( + &self, + system: &dyn System, + ) -> SitePackagesDiscoveryResult { + match self { + Self::Virtual(env) => env.site_packages_directories(system), + Self::System(env) => env.site_packages_directories(system), + } + } +} + +/// The Python runtime that produced the venv. +/// +/// We only need to distinguish cases that change the on-disk layout. +/// Everything else can be treated like CPython. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub(crate) enum PythonImplementation { + CPython, + PyPy, + GraalPy, + /// Fallback when the value is missing or unrecognised. + /// We treat it like CPython but keep the information for diagnostics. + #[default] + Unknown, +} + +impl PythonImplementation { + /// Return the relative path from `sys.prefix` to the `site-packages` directory + /// if this is a known implementation. Return `None` if this is an unknown implementation. + fn relative_site_packages_path(self, version: Option) -> Option { + match self { + Self::CPython | Self::GraalPy => { + version.map(|version| format!("lib/python{version}/site-packages")) + } + Self::PyPy => version.map(|version| format!("lib/pypy{version}/site-packages")), + Self::Unknown => None, + } + } +} + +/// Abstraction for a Python virtual environment. +/// +/// Most of this information is derived from the virtual environment's `pyvenv.cfg` file. +/// The format of this file is not defined anywhere, and exactly which keys are present +/// depends on the tool that was used to create the virtual environment. +#[derive(Debug)] +pub struct VirtualEnvironment { + root_path: SysPrefixPath, + base_executable_home_path: PythonHomePath, + include_system_site_packages: bool, + + /// The version of the Python executable that was used to create this virtual environment. + /// + /// The Python version is encoded under different keys and in different formats + /// by different virtual-environment creation tools, + /// and the key is never read by the standard-library `site.py` module, + /// so it's possible that we might not be able to find this information + /// in an acceptable format under any of the keys we expect. + /// This field will be `None` if so. + version: Option, + implementation: PythonImplementation, + + /// If this virtual environment was created using uv, + /// it may be an "ephemeral" virtual environment that dynamically adds the `site-packages` + /// directories of its parent environment to `sys.path` at runtime. + /// Newer versions of uv record the parent environment in the `pyvenv.cfg` file; + /// we'll want to add the `site-packages` directories of the parent environment + /// as search paths as well as the `site-packages` directories of this virtual environment. + parent_environment: Option>, +} + +impl VirtualEnvironment { + pub(crate) fn new( + path: SysPrefixPath, + system: &dyn System, + ) -> SitePackagesDiscoveryResult { + let pyvenv_cfg_path = path.join("pyvenv.cfg"); + tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'"); + + let pyvenv_cfg = match system.read_to_string(&pyvenv_cfg_path) { + Ok(pyvenv_cfg) => pyvenv_cfg, + Err(err) => return Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(path, err)), + }; + + let parsed_pyvenv_cfg = + PyvenvCfgParser::new(&pyvenv_cfg) + .parse() + .map_err(|pyvenv_parse_error| { + SitePackagesDiscoveryError::PyvenvCfgParseError( + pyvenv_cfg_path.clone(), + pyvenv_parse_error, + ) + })?; + + let RawPyvenvCfg { + include_system_site_packages, + base_executable_home_path, + version, + implementation, + created_with_uv, + parent_environment, + } = parsed_pyvenv_cfg; + + // The `home` key is read by the standard library's `site.py` module, + // so if it's missing from the `pyvenv.cfg` file + // (or the provided value is invalid), + // it's reasonable to consider the virtual environment irredeemably broken. + let Some(base_executable_home_path) = base_executable_home_path else { + return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( + pyvenv_cfg_path, + PyvenvCfgParseErrorKind::NoHomeKey, + )); + }; + + let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system) + .map_err(|io_err| { + SitePackagesDiscoveryError::PyvenvCfgParseError( + pyvenv_cfg_path.clone(), + PyvenvCfgParseErrorKind::InvalidHomeValue(io_err), + ) + })?; + + // Since the `extends-environment` key is nonstandard, + // for now we only trust it if the virtual environment was created with `uv`. + let parent_environment = if created_with_uv { + parent_environment + .and_then(|sys_prefix| { + PythonEnvironment::new(sys_prefix, SysPrefixPathOrigin::DerivedFromPyvenvCfg, system) + .inspect_err(|err| { + tracing::warn!( + "Failed to resolve the parent environment of this ephemeral uv virtual environment \ + from the `extends-environment` value specified in the `pyvenv.cfg` file at {pyvenv_cfg_path}. \ + Imports will not be resolved correctly if they refer to packages installed into the parent \ + environment. Underlying error: {err}", + ); + }) + .ok() + }) + .map(Box::new) + } else { + None + }; + + // but the `version`/`version_info` key is not read by the standard library, + // and is provided under different keys depending on which virtual-environment creation tool + // created the `pyvenv.cfg` file. Lenient parsing is appropriate here: + // the file isn't really *invalid* if it doesn't have this key, + // or if the value doesn't parse according to our expectations. + let version = version.and_then(|(version_string, range)| { + let mut version_info_parts = version_string.split('.'); + let (major, minor) = (version_info_parts.next()?, version_info_parts.next()?); + let version = PythonVersion::try_from((major, minor)).ok()?; + let source = PythonVersionSource::PyvenvCfgFile(PythonVersionFileSource::new( + Arc::new(pyvenv_cfg_path), + Some(range), + )); + Some(PythonVersionWithSource { version, source }) + }); + + let metadata = Self { + root_path: path, + base_executable_home_path, + include_system_site_packages, + version, + implementation, + parent_environment, + }; + + tracing::trace!("Resolved metadata for virtual environment: {metadata:?}"); + Ok(metadata) + } + + /// Return a list of `site-packages` directories that are available from this virtual environment + /// + /// See the documentation for [`site_packages_directory_from_sys_prefix`] for more details. + pub(crate) fn site_packages_directories( + &self, + system: &dyn System, + ) -> SitePackagesDiscoveryResult { + let VirtualEnvironment { + root_path, + base_executable_home_path, + include_system_site_packages, + implementation, + version, + parent_environment, + } = self; + + let version = version.as_ref().map(|v| v.version); + + let mut site_packages_directories = SitePackagesPaths::single( + site_packages_directory_from_sys_prefix(root_path, version, *implementation, system)?, + ); + + if let Some(parent_env_site_packages) = parent_environment.as_deref() { + match parent_env_site_packages.site_packages_paths(system) { + Ok(parent_environment_site_packages) => { + site_packages_directories.extend(parent_environment_site_packages); + } + Err(err) => { + tracing::warn!( + "Failed to resolve the site-packages directories of this ephemeral uv virtual environment's \ + parent environment. Imports will not be resolved correctly if they refer to packages installed \ + into the parent environment. Underlying error: {err}" + ); + } + } + } + + if *include_system_site_packages { + let system_sys_prefix = + SysPrefixPath::from_executable_home_path(base_executable_home_path); + + // If we fail to resolve the `sys.prefix` path from the base executable home path, + // or if we fail to resolve the `site-packages` from the `sys.prefix` path, + // we should probably print a warning but *not* abort type checking + if let Some(sys_prefix_path) = system_sys_prefix { + match site_packages_directory_from_sys_prefix( + &sys_prefix_path, + version, + *implementation, + system, + ) { + Ok(site_packages_directory) => { + site_packages_directories.insert(site_packages_directory); + } + Err(error) => tracing::warn!( + "{error}. System site-packages will not be used for module resolution." + ), + } + } else { + tracing::warn!( + "Failed to resolve `sys.prefix` of the system Python installation \ +from the `home` value in the `pyvenv.cfg` file at `{}`. \ +System site-packages will not be used for module resolution.", + root_path.join("pyvenv.cfg") + ); + } + } + + tracing::debug!( + "Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}" + ); + Ok(site_packages_directories) + } +} + +/// A parser for `pyvenv.cfg` files: metadata files for virtual environments. +/// +/// Note that a `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax! +/// +/// See also: +#[derive(Debug)] +struct PyvenvCfgParser<'s> { + source: &'s str, + cursor: Cursor<'s>, + line_number: NonZeroUsize, + data: RawPyvenvCfg<'s>, +} + +impl<'s> PyvenvCfgParser<'s> { + fn new(source: &'s str) -> Self { + Self { + source, + cursor: Cursor::new(source), + line_number: NonZeroUsize::new(1).unwrap(), + data: RawPyvenvCfg::default(), + } + } + + /// Parse the `pyvenv.cfg` file and return the parsed data. + fn parse(mut self) -> Result, PyvenvCfgParseErrorKind> { + while !self.cursor.is_eof() { + self.parse_line()?; + self.line_number = self.line_number.checked_add(1).unwrap(); + } + Ok(self.data) + } + + /// Parse a single line of the `pyvenv.cfg` file and advance the cursor + /// to the beginning of the next line. + fn parse_line(&mut self) -> Result<(), PyvenvCfgParseErrorKind> { + let PyvenvCfgParser { + source, + cursor, + line_number, + data, + } = self; + + let line_number = *line_number; + + cursor.eat_while(|c| c.is_whitespace() && c != '\n'); + + let key_start = cursor.offset(); + cursor.eat_while(|c| !matches!(c, '\n' | '=')); + let key_end = cursor.offset(); + + if !cursor.eat_char('=') { + // Skip over any lines that do not contain '=' characters, same as the CPython stdlib + // + cursor.eat_char('\n'); + return Ok(()); + } + + let key = source[TextRange::new(key_start, key_end)].trim(); + + cursor.eat_while(|c| c.is_whitespace() && c != '\n'); + let value_start = cursor.offset(); + cursor.eat_while(|c| c != '\n'); + let value = source[TextRange::new(value_start, cursor.offset())].trim(); + cursor.eat_char('\n'); + + if value.is_empty() { + return Err(PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }); + } + + match key { + "include-system-site-packages" => { + data.include_system_site_packages = value.eq_ignore_ascii_case("true"); + } + "home" => data.base_executable_home_path = Some(value), + // `virtualenv` and `uv` call this key `version_info`, + // but the stdlib venv module calls it `version` + "version" | "version_info" => { + let version_range = TextRange::at(value_start, value.text_len()); + data.version = Some((value, version_range)); + } + "implementation" => { + data.implementation = match value.to_ascii_lowercase().as_str() { + "cpython" => PythonImplementation::CPython, + "graalvm" => PythonImplementation::GraalPy, + "pypy" => PythonImplementation::PyPy, + _ => PythonImplementation::Unknown, + }; + } + "uv" => data.created_with_uv = true, + "extends-environment" => data.parent_environment = Some(value), + "" => { + return Err(PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }); + } + _ => {} + } + + Ok(()) + } +} + +/// A `key:value` mapping derived from parsing a `pyvenv.cfg` file. +/// +/// This data contained within is still mostly raw and unvalidated. +#[derive(Debug, Default)] +struct RawPyvenvCfg<'s> { + include_system_site_packages: bool, + base_executable_home_path: Option<&'s str>, + version: Option<(&'s str, TextRange)>, + implementation: PythonImplementation, + created_with_uv: bool, + parent_environment: Option<&'s str>, +} + +/// A Python environment that is _not_ a virtual environment. +/// +/// This environment may or may not be one that is managed by the operating system itself, e.g., +/// this captures both Homebrew-installed Python versions and the bundled macOS Python installation. +#[derive(Debug)] +pub struct SystemEnvironment { + root_path: SysPrefixPath, +} + +impl SystemEnvironment { + /// Create a new system environment from the given path. + /// + /// At this time, there is no eager validation and this is infallible. Instead, validation + /// will occur in [`site_packages_directory_from_sys_prefix`] — which will fail if there is not + /// a Python environment at the given path. + pub(crate) fn new(path: SysPrefixPath) -> Self { + Self { root_path: path } + } + + /// Return a list of `site-packages` directories that are available from this environment. + /// + /// See the documentation for [`site_packages_directory_from_sys_prefix`] for more details. + pub(crate) fn site_packages_directories( + &self, + system: &dyn System, + ) -> SitePackagesDiscoveryResult { + let SystemEnvironment { root_path } = self; + + let site_packages_directories = + SitePackagesPaths::single(site_packages_directory_from_sys_prefix( + root_path, + None, + PythonImplementation::Unknown, + system, + )?); + + tracing::debug!( + "Resolved site-packages directories for this environment are: {site_packages_directories:?}" + ); + Ok(site_packages_directories) + } +} + +/// Enumeration of ways in which `site-packages` discovery can fail. +#[derive(Debug)] +pub enum SitePackagesDiscoveryError { + /// `site-packages` discovery failed because the provided path couldn't be canonicalized. + CanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, io::Error), + + /// `site-packages` discovery failed because the provided path doesn't appear to point to + /// a Python executable or a `sys.prefix` directory. + PathNotExecutableOrDirectory(SystemPathBuf, SysPrefixPathOrigin, Option), + + /// `site-packages` discovery failed because the [`SysPrefixPathOrigin`] indicated that + /// the provided path should point to the `sys.prefix` of a virtual environment, + /// but there was no file at `/pyvenv.cfg`. + NoPyvenvCfgFile(SysPrefixPath, io::Error), + + /// `site-packages` discovery failed because the `pyvenv.cfg` file could not be parsed. + PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind), + + /// `site-packages` discovery failed because we're on a Unix system, + /// we weren't able to figure out from the `pyvenv.cfg` file exactly where `site-packages` + /// would be relative to the `sys.prefix` path, and we tried to fallback to iterating + /// through the `/lib` directory looking for a `site-packages` directory, + /// but we came across some I/O error while trying to do so. + CouldNotReadLibDirectory(SysPrefixPath, io::Error), + + /// We looked everywhere we could think of for the `site-packages` directory, + /// but none could be found despite our best endeavours. + NoSitePackagesDirFound(SysPrefixPath), +} + +impl std::error::Error for SitePackagesDiscoveryError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::CanonicalizationError(_, _, io_err) => Some(io_err), + Self::PathNotExecutableOrDirectory(_, _, io_err) => { + io_err.as_ref().map(|e| e as &dyn std::error::Error) + } + Self::NoPyvenvCfgFile(_, io_err) => Some(io_err), + Self::PyvenvCfgParseError(_, _) => None, + Self::CouldNotReadLibDirectory(_, io_err) => Some(io_err), + Self::NoSitePackagesDirFound(_) => None, + } + } +} + +impl std::fmt::Display for SitePackagesDiscoveryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CanonicalizationError(given_path, origin, _) => { + display_error(f, origin, given_path, "Failed to canonicalize", None) + } + Self::PathNotExecutableOrDirectory(path, origin, _) => { + let thing = if origin.must_point_directly_to_sys_prefix() { + "directory on disk" + } else { + "Python executable or a directory on disk" + }; + display_error( + f, + origin, + path, + &format!("Invalid {origin}"), + Some(&format!("does not point to a {thing}")), + ) + } + Self::NoPyvenvCfgFile(SysPrefixPath { inner, origin }, _) => display_error( + f, + origin, + inner, + &format!("Invalid {origin}"), + Some("points to a broken venv with no pyvenv.cfg file"), + ), + Self::PyvenvCfgParseError(path, kind) => { + write!( + f, + "Failed to parse the `pyvenv.cfg` file at `{path}` because {kind}" + ) + } + Self::CouldNotReadLibDirectory(SysPrefixPath { inner, origin }, _) => display_error( + f, + origin, + inner, + "Failed to iterate over the contents of the `lib` directory of the Python installation", + None, + ), + Self::NoSitePackagesDirFound(SysPrefixPath { inner, origin }) => display_error( + f, + origin, + inner, + &format!("Invalid {origin}"), + Some( + "Could not find a `site-packages` directory for this Python installation/executable", + ), + ), + } + } +} + +fn display_error( + f: &mut std::fmt::Formatter<'_>, + sys_prefix_origin: &SysPrefixPathOrigin, + given_path: &SystemPath, + primary_message: &str, + secondary_message: Option<&str>, +) -> std::fmt::Result { + let fallback: &mut dyn FnMut() -> std::fmt::Result = &mut || { + f.write_str(primary_message)?; + write!(f, " `{given_path}`")?; + if let Some(secondary_message) = secondary_message { + f.write_str(": ")?; + f.write_str(secondary_message)?; + } + Ok(()) + }; + + let SysPrefixPathOrigin::ConfigFileSetting(config_file_path, Some(setting_range)) = + sys_prefix_origin + else { + return fallback(); + }; + + let Ok(config_file_source) = std::fs::read_to_string((**config_file_path).as_ref()) else { + return fallback(); + }; + + let index = LineIndex::from_source_text(&config_file_source); + let source = SourceCode::new(&config_file_source, &index); + + let primary_message = format!( + "{primary_message} + +--> Invalid setting in configuration file `{config_file_path}`" + ); + + let start_index = source.line_index(setting_range.start()).saturating_sub(2); + let end_index = source + .line_index(setting_range.end()) + .saturating_add(2) + .min(OneIndexed::from_zero_indexed(source.line_count())); + + let start_offset = source.line_start(start_index); + let end_offset = source.line_end(end_index); + + let mut annotation = Level::Error.span((setting_range - start_offset).into()); + + if let Some(secondary_message) = secondary_message { + annotation = annotation.label(secondary_message); + } + + let snippet = Snippet::source(&config_file_source[TextRange::new(start_offset, end_offset)]) + .annotation(annotation) + .line_start(start_index.get()) + .fold(false); + + let message = Level::None.title(&primary_message).snippet(snippet); + + let renderer = if colored::control::SHOULD_COLORIZE.should_colorize() { + Renderer::styled() + } else { + Renderer::plain() + }; + let renderer = renderer.cut_indicator("…"); + + writeln!(f, "{}", renderer.render(message)) +} + +/// The various ways in which parsing a `pyvenv.cfg` file could fail +#[derive(Debug)] +pub enum PyvenvCfgParseErrorKind { + MalformedKeyValuePair { line_number: NonZeroUsize }, + NoHomeKey, + InvalidHomeValue(io::Error), +} + +impl fmt::Display for PyvenvCfgParseErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MalformedKeyValuePair { line_number } => write!( + f, + "line {line_number} has a malformed ` = ` pair" + ), + Self::NoHomeKey => f.write_str("the file does not have a `home` key"), + Self::InvalidHomeValue(io_err) => { + write!( + f, + "the following error was encountered \ +when trying to resolve the `home` value to a directory on disk: {io_err}" + ) + } + } + } +} + +/// Attempt to retrieve the `site-packages` directory +/// associated with a given Python installation. +/// +/// The location of the `site-packages` directory can vary according to the +/// Python version that this installation represents. The Python version may +/// or may not be known at this point, which is why the `python_version` +/// parameter is an `Option`. +fn site_packages_directory_from_sys_prefix( + sys_prefix_path: &SysPrefixPath, + python_version: Option, + implementation: PythonImplementation, + system: &dyn System, +) -> SitePackagesDiscoveryResult { + tracing::debug!( + "Searching for site-packages directory in sys.prefix {}", + sys_prefix_path.inner + ); + + if cfg!(target_os = "windows") { + let site_packages = sys_prefix_path.join(r"Lib\site-packages"); + return system + .is_directory(&site_packages) + .then_some(site_packages) + .ok_or(SitePackagesDiscoveryError::NoSitePackagesDirFound( + sys_prefix_path.to_owned(), + )); + } + + // In the Python standard library's `site.py` module (used for finding `site-packages` + // at runtime), we can find this in [the non-Windows branch]: + // + // ```py + // libdirs = [sys.platlibdir] + // if sys.platlibdir != "lib": + // libdirs.append("lib") + // ``` + // + // Pyright therefore searches for both a `lib/python3.X/site-packages` directory + // and a `lib64/python3.X/site-packages` directory on non-MacOS Unix systems, + // since `sys.platlibdir` can sometimes be equal to `"lib64"`. + // + // However, we only care about the `site-packages` directory insofar as it allows + // us to discover Python source code that can be used for inferring type + // information regarding third-party dependencies. That means that we don't need + // to care about any possible `lib64/site-packages` directories, since + // [the `sys`-module documentation] states that `sys.platlibdir` is *only* ever + // used for C extensions, never for pure-Python modules. + // + // [the non-Windows branch]: https://github.com/python/cpython/blob/a8be8fc6c4682089be45a87bd5ee1f686040116c/Lib/site.py#L401-L410 + // [the `sys`-module documentation]: https://docs.python.org/3/library/sys.html#sys.platlibdir + + // If we were able to figure out what Python version this installation is, + // we should be able to avoid iterating through all items in the `lib/` directory: + if let Some(expected_relative_path) = implementation.relative_site_packages_path(python_version) + { + let expected_absolute_path = sys_prefix_path.join(expected_relative_path); + if system.is_directory(&expected_absolute_path) { + return Ok(expected_absolute_path); + } + + // CPython free-threaded (3.13+) variant: pythonXYt + if matches!(implementation, PythonImplementation::CPython) + && python_version.is_some_and(PythonVersion::free_threaded_build_available) + { + let alternative_path = sys_prefix_path.join(format!( + "lib/python{}t/site-packages", + python_version.unwrap() + )); + if system.is_directory(&alternative_path) { + return Ok(alternative_path); + } + } + } + + // Either we couldn't figure out the version before calling this function + // (e.g., from a `pyvenv.cfg` file if this was a venv), + // or we couldn't find a `site-packages` folder at the expected location given + // the parsed version + // + // Note: the `python3.x` part of the `site-packages` path can't be computed from + // the `--python-version` the user has passed, as they might be running Python 3.12 locally + // even if they've requested that we type check their code "as if" they're running 3.8. + for entry_result in system + .read_directory(&sys_prefix_path.join("lib")) + .map_err(|io_err| { + SitePackagesDiscoveryError::CouldNotReadLibDirectory(sys_prefix_path.to_owned(), io_err) + })? + { + let Ok(entry) = entry_result else { + continue; + }; + + if !entry.file_type().is_directory() { + continue; + } + + let mut path = entry.into_path(); + + let name = path + .file_name() + .expect("File name to be non-null because path is guaranteed to be a child of `lib`"); + + if !(name.starts_with("python3.") || name.starts_with("pypy3.")) { + continue; + } + + path.push("site-packages"); + if system.is_directory(&path) { + return Ok(path); + } + } + Err(SitePackagesDiscoveryError::NoSitePackagesDirFound( + sys_prefix_path.to_owned(), + )) +} + +/// A path that represents the value of [`sys.prefix`] at runtime in Python +/// for a given Python executable. +/// +/// For the case of a virtual environment, where a +/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to +/// the virtual environment the Python binary lies inside, i.e. `/.venv`, +/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`. +/// System Python installations generally work the same way: if a system +/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix` +/// will be `/opt/homebrew`, and `site-packages` will be at +/// `/opt/homebrew/lib/python3.X/site-packages`. +/// +/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SysPrefixPath { + inner: SystemPathBuf, + origin: SysPrefixPathOrigin, +} + +impl SysPrefixPath { + fn new( + unvalidated_path: &SystemPath, + origin: SysPrefixPathOrigin, + system: &dyn System, + ) -> SitePackagesDiscoveryResult { + let sys_prefix = if !origin.must_point_directly_to_sys_prefix() + && system.is_file(unvalidated_path) + && unvalidated_path + .file_name() + .is_some_and(|name| name.starts_with("python")) + { + // It looks like they passed us a path to a Python executable, e.g. `.venv/bin/python3`. + // Try to figure out the `sys.prefix` value from the Python executable. + let sys_prefix = if cfg!(windows) { + // On Windows, the relative path to the Python executable from `sys.prefix` + // is different depending on whether it's a virtual environment or a system installation. + // System installations have their executable at `/python.exe`, + // whereas virtual environments have their executable at `/Scripts/python.exe`. + unvalidated_path.parent().and_then(|parent| { + if parent.file_name() == Some("Scripts") { + parent.parent() + } else { + Some(parent) + } + }) + } else { + // On Unix, `sys.prefix` is always the grandparent directory of the Python executable, + // regardless of whether it's a virtual environment or a system installation. + unvalidated_path.ancestors().nth(2) + }; + let Some(sys_prefix) = sys_prefix else { + return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory( + unvalidated_path.to_path_buf(), + origin, + None, + )); + }; + sys_prefix + } else { + unvalidated_path + }; + + // It's important to resolve symlinks here rather than simply making the path absolute, + // since system Python installations often only put symlinks in the "expected" + // locations for `home` and `site-packages` + let sys_prefix = match system.canonicalize_path(sys_prefix) { + Ok(path) => path, + Err(io_err) => { + let unvalidated_path = unvalidated_path.to_path_buf(); + let err = if io_err.kind() == io::ErrorKind::NotFound { + SitePackagesDiscoveryError::PathNotExecutableOrDirectory( + unvalidated_path, + origin, + Some(io_err), + ) + } else { + SitePackagesDiscoveryError::CanonicalizationError( + unvalidated_path, + origin, + io_err, + ) + }; + return Err(err); + } + }; + + if !system.is_directory(&sys_prefix) { + return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory( + unvalidated_path.to_path_buf(), + origin, + None, + )); + } + + Ok(Self { + inner: sys_prefix, + origin, + }) + } + fn from_executable_home_path(path: &PythonHomePath) -> Option { + // No need to check whether `path.parent()` is a directory: + // the parent of a canonicalised path that is known to exist + // is guaranteed to be a directory. + if cfg!(target_os = "windows") { + Some(Self { + inner: path.to_path_buf(), + origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg, + }) + } else { + path.parent().map(|path| Self { + inner: path.to_path_buf(), + origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg, + }) + } + } +} + +impl Deref for SysPrefixPath { + type Target = SystemPath; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +/// Enumeration of sources a `sys.prefix` path can come from. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SysPrefixPathOrigin { + /// The `sys.prefix` path came from a configuration file setting: `pyproject.toml` or `ty.toml` + ConfigFileSetting(Arc, Option), + /// The `sys.prefix` path came from a `--python` CLI flag + PythonCliFlag, + /// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable + VirtualEnvVar, + /// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable + CondaPrefixVar, + /// The `sys.prefix` path was derived from a value in a `pyvenv.cfg` file: + /// either the value associated with the `home` key + /// or the value associated with the `extends-environment` key + DerivedFromPyvenvCfg, + /// A `.venv` directory was found in the current working directory, + /// and the `sys.prefix` path is the path to that virtual environment. + LocalVenv, +} + +impl SysPrefixPathOrigin { + /// Whether the given `sys.prefix` path must be a virtual environment (rather than a system + /// Python environment). + pub(crate) const fn must_be_virtual_env(&self) -> bool { + match self { + Self::LocalVenv | Self::VirtualEnvVar => true, + Self::ConfigFileSetting(..) + | Self::PythonCliFlag + | Self::DerivedFromPyvenvCfg + | Self::CondaPrefixVar => false, + } + } + + /// Whether paths with this origin always point directly to the `sys.prefix` directory. + /// + /// Some variants can point either directly to `sys.prefix` or to a Python executable inside + /// the `sys.prefix` directory, e.g. the `--python` CLI flag. + pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool { + match self { + Self::PythonCliFlag | Self::ConfigFileSetting(..) => false, + Self::VirtualEnvVar + | Self::CondaPrefixVar + | Self::DerivedFromPyvenvCfg + | Self::LocalVenv => true, + } + } +} + +impl std::fmt::Display for SysPrefixPathOrigin { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::PythonCliFlag => f.write_str("`--python` argument"), + Self::ConfigFileSetting(_, _) => f.write_str("`environment.python` setting"), + Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"), + Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"), + Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"), + Self::LocalVenv => f.write_str("local virtual environment"), + } + } +} + +/// The value given by the `home` key in `pyvenv.cfg` files. +/// +/// This is equivalent to `{sys_prefix_path}/bin`, and points +/// to a directory in which a Python executable can be found. +/// Confusingly, it is *not* the same as the [`PYTHONHOME`] +/// environment variable that Python provides! However, it's +/// consistent among all mainstream creators of Python virtual +/// environments (the stdlib Python `venv` module, the third-party +/// `virtualenv` library, and `uv`), was specified by +/// [the original PEP adding the `venv` module], +/// and it's one of the few fields that's read by the Python +/// standard library's `site.py` module. +/// +/// Although it doesn't appear to be specified anywhere, +/// all existing virtual environment tools always use an absolute path +/// for the `home` value, and the Python standard library also assumes +/// that the `home` value will be an absolute path. +/// +/// Other values, such as the path to the Python executable or the +/// base-executable `sys.prefix` value, are either only provided in +/// `pyvenv.cfg` files by some virtual-environment creators, +/// or are included under different keys depending on which +/// virtual-environment creation tool you've used. +/// +/// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME +/// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/ +#[derive(Debug, PartialEq, Eq)] +struct PythonHomePath(SystemPathBuf); + +impl PythonHomePath { + fn new(path: impl AsRef, system: &dyn System) -> io::Result { + let path = path.as_ref(); + // It's important to resolve symlinks here rather than simply making the path absolute, + // since system Python installations often only put symlinks in the "expected" + // locations for `home` and `site-packages` + let canonicalized = system.canonicalize_path(path)?; + system + .is_directory(&canonicalized) + .then_some(Self(canonicalized)) + .ok_or_else(|| io::Error::other("not a directory")) + } +} + +impl Deref for PythonHomePath { + type Target = SystemPath; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for PythonHomePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "`home` location `{}`", self.0) + } +} + +impl PartialEq for PythonHomePath { + fn eq(&self, other: &SystemPath) -> bool { + &*self.0 == other + } +} + +impl PartialEq for PythonHomePath { + fn eq(&self, other: &SystemPathBuf) -> bool { + self == &**other + } +} + +#[cfg(test)] +mod tests { + use ruff_db::system::TestSystem; + + use super::*; + + impl PythonEnvironment { + fn expect_venv(self) -> VirtualEnvironment { + match self { + Self::Virtual(venv) => venv, + Self::System(_) => panic!("Expected a virtual environment"), + } + } + } + + #[derive(Default)] + struct VirtualEnvironmentTestCase { + system_site_packages: bool, + pyvenv_cfg_version_field: Option<&'static str>, + command_field: Option<&'static str>, + implementation_field: Option<&'static str>, + } + + struct PythonEnvironmentTestCase { + system: TestSystem, + minor_version: u8, + free_threaded: bool, + origin: SysPrefixPathOrigin, + virtual_env: Option, + } + + impl PythonEnvironmentTestCase { + /// Builds a mock environment, and returns the path to the environment root. + fn build(&self) -> SystemPathBuf { + let PythonEnvironmentTestCase { + system, + minor_version, + free_threaded, + origin: _, + virtual_env, + } = self; + let memory_fs = system.memory_file_system(); + let unix_site_packages = if *free_threaded { + format!("lib/python3.{minor_version}t/site-packages") + } else { + format!("lib/python3.{minor_version}/site-packages") + }; + + let system_install_sys_prefix = + SystemPathBuf::from(&*format!("/Python3.{minor_version}")); + let (system_home_path, system_exe_path, system_site_packages_path) = + if cfg!(target_os = "windows") { + let system_home_path = system_install_sys_prefix.clone(); + let system_exe_path = system_home_path.join("python.exe"); + let system_site_packages_path = + system_install_sys_prefix.join(r"Lib\site-packages"); + (system_home_path, system_exe_path, system_site_packages_path) + } else { + let system_home_path = system_install_sys_prefix.join("bin"); + let system_exe_path = system_home_path.join("python"); + let system_site_packages_path = + system_install_sys_prefix.join(&unix_site_packages); + (system_home_path, system_exe_path, system_site_packages_path) + }; + memory_fs.write_file_all(system_exe_path, "").unwrap(); + memory_fs + .create_directory_all(&system_site_packages_path) + .unwrap(); + + let Some(VirtualEnvironmentTestCase { + pyvenv_cfg_version_field, + system_site_packages, + command_field, + implementation_field, + }) = virtual_env + else { + return system_install_sys_prefix; + }; + + let venv_sys_prefix = SystemPathBuf::from("/.venv"); + let (venv_exe, site_packages_path) = if cfg!(target_os = "windows") { + ( + venv_sys_prefix.join(r"Scripts\python.exe"), + venv_sys_prefix.join(r"Lib\site-packages"), + ) + } else { + ( + venv_sys_prefix.join("bin/python"), + venv_sys_prefix.join(&unix_site_packages), + ) + }; + memory_fs.write_file_all(&venv_exe, "").unwrap(); + memory_fs.create_directory_all(&site_packages_path).unwrap(); + + let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg"); + let mut pyvenv_cfg_contents = format!("home = {system_home_path}\n"); + if let Some(version_field) = pyvenv_cfg_version_field { + pyvenv_cfg_contents.push_str(version_field); + pyvenv_cfg_contents.push('\n'); + } + if let Some(command_field) = command_field { + pyvenv_cfg_contents.push_str(command_field); + pyvenv_cfg_contents.push('\n'); + } + if let Some(implementation_field) = implementation_field { + pyvenv_cfg_contents.push_str(implementation_field); + pyvenv_cfg_contents.push('\n'); + } + + // Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive: + if *system_site_packages { + pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n"); + } + memory_fs + .write_file_all(pyvenv_cfg_path, &pyvenv_cfg_contents) + .unwrap(); + + venv_sys_prefix + } + + #[track_caller] + fn err(self) -> SitePackagesDiscoveryError { + PythonEnvironment::new(self.build(), self.origin, &self.system) + .expect_err("Expected environment construction to fail") + } + + #[track_caller] + fn run(self) -> PythonEnvironment { + let env_path = self.build(); + let env = PythonEnvironment::new(env_path.clone(), self.origin.clone(), &self.system) + .expect("Expected environment construction to succeed"); + + let expect_virtual_env = self.virtual_env.is_some(); + match &env { + PythonEnvironment::Virtual(venv) if expect_virtual_env => { + self.assert_virtual_environment(venv, &env_path); + } + PythonEnvironment::Virtual(venv) => { + panic!( + "Expected a system environment, but got a virtual environment: {venv:?}" + ); + } + PythonEnvironment::System(env) if !expect_virtual_env => { + self.assert_system_environment(env, &env_path); + } + PythonEnvironment::System(env) => { + panic!("Expected a virtual environment, but got a system environment: {env:?}"); + } + } + env + } + + #[track_caller] + fn assert_virtual_environment( + &self, + venv: &VirtualEnvironment, + expected_env_path: &SystemPathBuf, + ) { + let self_venv = self.virtual_env.as_ref().expect( + "`assert_virtual_environment` should only be used when `virtual_env` is populated", + ); + + assert_eq!( + venv.root_path, + SysPrefixPath { + inner: self.system.canonicalize_path(expected_env_path).unwrap(), + origin: self.origin.clone(), + } + ); + assert_eq!( + venv.include_system_site_packages, + self_venv.system_site_packages + ); + + if self_venv.pyvenv_cfg_version_field.is_some() { + assert_eq!( + venv.version.as_ref().map(|v| v.version), + Some(PythonVersion { + major: 3, + minor: self.minor_version + }) + ); + } else { + assert_eq!(venv.version, None); + } + + let expected_home = if cfg!(target_os = "windows") { + SystemPathBuf::from(&*format!(r"\Python3.{}", self.minor_version)) + } else { + SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version)) + }; + assert_eq!(venv.base_executable_home_path, expected_home); + + let site_packages_directories = venv.site_packages_directories(&self.system).unwrap(); + let expected_venv_site_packages = if cfg!(target_os = "windows") { + SystemPathBuf::from(r"\.venv\Lib\site-packages") + } else if self.free_threaded { + SystemPathBuf::from(&*format!( + "/.venv/lib/python3.{}t/site-packages", + self.minor_version + )) + } else { + SystemPathBuf::from(&*format!( + "/.venv/lib/python3.{}/site-packages", + self.minor_version + )) + }; + + let expected_system_site_packages = self.expected_system_site_packages(); + + if self_venv.system_site_packages { + assert_eq!( + site_packages_directories, + [expected_venv_site_packages, expected_system_site_packages].as_slice() + ); + } else { + assert_eq!( + &site_packages_directories.into_iter().next().unwrap(), + &expected_venv_site_packages + ); + } + } + + #[track_caller] + fn assert_system_environment( + &self, + env: &SystemEnvironment, + expected_env_path: &SystemPathBuf, + ) { + assert!( + self.virtual_env.is_none(), + "`assert_system_environment` should only be used when `virtual_env` is not populated" + ); + + assert_eq!( + env.root_path, + SysPrefixPath { + inner: self.system.canonicalize_path(expected_env_path).unwrap(), + origin: self.origin.clone(), + } + ); + + let site_packages_directories = env.site_packages_directories(&self.system).unwrap(); + let expected_site_packages = self.expected_system_site_packages(); + assert_eq!( + site_packages_directories, + std::slice::from_ref(&expected_site_packages) + ); + } + + fn expected_system_site_packages(&self) -> SystemPathBuf { + let minor_version = self.minor_version; + if cfg!(target_os = "windows") { + SystemPathBuf::from(&*format!(r"\Python3.{minor_version}\Lib\site-packages")) + } else if self.free_threaded { + SystemPathBuf::from(&*format!( + "/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages" + )) + } else { + SystemPathBuf::from(&*format!( + "/Python3.{minor_version}/lib/python3.{minor_version}/site-packages" + )) + } + } + } + + #[test] + fn can_find_site_packages_directory_no_virtual_env() { + // Shouldn't be converted to an mdtest because mdtest automatically creates a + // pyvenv.cfg file for you if it sees you creating a `site-packages` directory. + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 12, + free_threaded: false, + origin: SysPrefixPathOrigin::PythonCliFlag, + virtual_env: None, + }; + test.run(); + } + + #[test] + fn can_find_site_packages_directory_no_virtual_env_freethreaded() { + // Shouldn't be converted to an mdtest because mdtest automatically creates a + // pyvenv.cfg file for you if it sees you creating a `site-packages` directory. + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::PythonCliFlag, + virtual_env: None, + }; + test.run(); + } + + #[test] + fn cannot_find_site_packages_directory_no_virtual_env_at_origin_virtual_env_var() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: false, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: None, + }; + let err = test.err(); + assert!( + matches!(err, SitePackagesDiscoveryError::NoPyvenvCfgFile(..)), + "Got {err:?}", + ); + } + + #[test] + fn cannot_find_site_packages_directory_no_virtual_env_at_origin_local_venv() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: false, + origin: SysPrefixPathOrigin::LocalVenv, + virtual_env: None, + }; + let err = test.err(); + assert!( + matches!(err, SitePackagesDiscoveryError::NoPyvenvCfgFile(..)), + "Got {err:?}", + ); + } + + #[test] + fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() { + // Shouldn't be converted to an mdtest because we want to assert + // that we parsed the `version` field correctly in `test.run()`. + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 12, + free_threaded: false, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + pyvenv_cfg_version_field: Some("version = 3.12"), + ..VirtualEnvironmentTestCase::default() + }), + }; + test.run(); + } + + #[test] + fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() { + // Shouldn't be converted to an mdtest because we want to assert + // that we parsed the `version` field correctly in `test.run()`. + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 12, + free_threaded: false, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + pyvenv_cfg_version_field: Some("version_info = 3.12"), + ..VirtualEnvironmentTestCase::default() + }), + }; + test.run(); + } + + #[test] + fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() { + // Shouldn't be converted to an mdtest because we want to assert + // that we parsed the `version` field correctly in `test.run()`. + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 12, + free_threaded: false, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"), + ..VirtualEnvironmentTestCase::default() + }), + }; + test.run(); + } + + #[test] + fn can_find_site_packages_directory_freethreaded_build() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + pyvenv_cfg_version_field: Some("version_info = 3.13"), + ..VirtualEnvironmentTestCase::default() + }), + }; + test.run(); + } + + #[test] + fn finds_system_site_packages() { + // Can't be converted to an mdtest because the system installation's `sys.prefix` + // path is at a different location relative to the `pyvenv.cfg` file's `home` value + // on Windows. + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + system_site_packages: true, + pyvenv_cfg_version_field: Some("version_info = 3.13"), + ..VirtualEnvironmentTestCase::default() + }), + }; + test.run(); + } + + #[test] + fn detects_pypy_implementation() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + implementation_field: Some("implementation = PyPy"), + ..VirtualEnvironmentTestCase::default() + }), + }; + let venv = test.run().expect_venv(); + assert_eq!(venv.implementation, PythonImplementation::PyPy); + } + + #[test] + fn detects_cpython_implementation() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + implementation_field: Some("implementation = CPython"), + ..VirtualEnvironmentTestCase::default() + }), + }; + let venv = test.run().expect_venv(); + assert_eq!(venv.implementation, PythonImplementation::CPython); + } + + #[test] + fn detects_graalpy_implementation() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase { + implementation_field: Some("implementation = GraalVM"), + ..VirtualEnvironmentTestCase::default() + }), + }; + let venv = test.run().expect_venv(); + assert_eq!(venv.implementation, PythonImplementation::GraalPy); + } + + #[test] + fn detects_unknown_implementation() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 13, + free_threaded: true, + origin: SysPrefixPathOrigin::VirtualEnvVar, + virtual_env: Some(VirtualEnvironmentTestCase::default()), + }; + let venv = test.run().expect_venv(); + assert_eq!(venv.implementation, PythonImplementation::Unknown); + } + + #[test] + fn reject_env_that_does_not_exist() { + let system = TestSystem::default(); + assert!(matches!( + PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system), + Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(..)) + )); + } + + #[test] + fn reject_env_that_is_not_a_directory() { + let system = TestSystem::default(); + system + .memory_file_system() + .write_file_all("/env", "") + .unwrap(); + assert!(matches!( + PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system), + Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(..)) + )); + } + + #[test] + fn cannot_read_lib_directory() { + let system = TestSystem::default(); + system + .memory_file_system() + .create_directory_all("/env") + .unwrap(); + // Environment creation succeeds, but site-packages retrieval fails reading the `lib` + // directory + let env = + PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system).unwrap(); + let site_packages = env.site_packages_paths(&system); + if cfg!(unix) { + assert!( + matches!( + site_packages, + Err(SitePackagesDiscoveryError::CouldNotReadLibDirectory(..)), + ), + "Got {site_packages:?}", + ); + } else { + // On Windows, we look for `Lib/site-packages` directly instead of listing the entries + // of `lib/...` — so we don't see the intermediate failure + assert!( + matches!( + site_packages, + Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(..)), + ), + "Got {site_packages:?}", + ); + } + } + + #[test] + fn cannot_find_site_packages_directory() { + let system = TestSystem::default(); + if cfg!(unix) { + system + .memory_file_system() + .create_directory_all("/env/lib") + .unwrap(); + } else { + system + .memory_file_system() + .create_directory_all("/env/Lib") + .unwrap(); + } + // Environment creation succeeds, but site-packages retrieval fails + let env = + PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system).unwrap(); + let site_packages = env.site_packages_paths(&system); + assert!( + matches!( + site_packages, + Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(..)), + ), + "Got {site_packages:?}", + ); + } + + #[test] + fn parsing_pyvenv_cfg_with_key_but_no_value_fails() { + let system = TestSystem::default(); + let memory_fs = system.memory_file_system(); + let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); + memory_fs + .write_file_all(&pyvenv_cfg_path, "home =") + .unwrap(); + let venv_result = + PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); + assert!(matches!( + venv_result, + Err(SitePackagesDiscoveryError::PyvenvCfgParseError( + path, + PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number } + )) + if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1) + )); + } + + #[test] + fn parsing_pyvenv_cfg_with_value_but_no_key_fails() { + let system = TestSystem::default(); + let memory_fs = system.memory_file_system(); + let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); + memory_fs + .write_file_all(&pyvenv_cfg_path, "= whatever") + .unwrap(); + let venv_result = + PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); + assert!(matches!( + venv_result, + Err(SitePackagesDiscoveryError::PyvenvCfgParseError( + path, + PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number } + )) + if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1) + )); + } + + #[test] + fn parsing_pyvenv_cfg_with_no_home_key_fails() { + let system = TestSystem::default(); + let memory_fs = system.memory_file_system(); + let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); + memory_fs.write_file_all(&pyvenv_cfg_path, "").unwrap(); + let venv_result = + PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); + assert!(matches!( + venv_result, + Err(SitePackagesDiscoveryError::PyvenvCfgParseError( + path, + PyvenvCfgParseErrorKind::NoHomeKey + )) + if path == pyvenv_cfg_path + )); + } + + #[test] + fn parsing_pyvenv_cfg_with_invalid_home_key_fails() { + let system = TestSystem::default(); + let memory_fs = system.memory_file_system(); + let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); + memory_fs + .write_file_all(&pyvenv_cfg_path, "home = foo") + .unwrap(); + let venv_result = + PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); + assert!(matches!( + venv_result, + Err(SitePackagesDiscoveryError::PyvenvCfgParseError( + path, + PyvenvCfgParseErrorKind::InvalidHomeValue(_) + )) + if path == pyvenv_cfg_path + )); + } + + #[test] + fn pyvenv_cfg_with_carriage_return_line_endings_parses() { + let pyvenv_cfg = "home = /somewhere/python\r\nversion_info = 3.13\r\nimplementation = PyPy"; + let parsed = PyvenvCfgParser::new(pyvenv_cfg).parse().unwrap(); + assert_eq!(parsed.base_executable_home_path, Some("/somewhere/python")); + let version = parsed.version.unwrap(); + assert_eq!(version.0, "3.13"); + assert_eq!(&pyvenv_cfg[version.1], version.0); + assert_eq!(parsed.implementation, PythonImplementation::PyPy); + } + + #[test] + fn pyvenv_cfg_with_strange_whitespace_parses() { + let pyvenv_cfg = " home= /a path with whitespace/python\t \t \nversion_info = 3.13 \n\n\n\nimplementation =PyPy"; + let parsed = PyvenvCfgParser::new(pyvenv_cfg).parse().unwrap(); + assert_eq!( + parsed.base_executable_home_path, + Some("/a path with whitespace/python") + ); + let version = parsed.version.unwrap(); + assert_eq!(version.0, "3.13"); + assert_eq!(&pyvenv_cfg[version.1], version.0); + assert_eq!(parsed.implementation, PythonImplementation::PyPy); + } +} diff --git a/crates/red_knot_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs similarity index 93% rename from crates/red_knot_python_semantic/src/suppression.rs rename to crates/ty_python_semantic/src/suppression.rs index ebfc62afd803b..cacdc41ec3984 100644 --- a/crates/red_knot_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -1,12 +1,12 @@ use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus}; use crate::types::TypeCheckDiagnostics; -use crate::{declare_lint, lint::LintId, Db}; +use crate::{Db, declare_lint, lint::LintId}; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Span}; use ruff_db::{files::File, parsed::parsed_module, source::source_text}; use ruff_python_parser::TokenKind; use ruff_python_trivia::Cursor; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use smallvec::{smallvec, SmallVec}; +use smallvec::{SmallVec, smallvec}; use std::error::Error; use std::fmt; use std::fmt::Formatter; @@ -14,7 +14,7 @@ use thiserror::Error; declare_lint! { /// ## What it does - /// Checks for `type: ignore` or `knot: ignore` directives that are no longer applicable. + /// Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable. /// /// ## Why is this bad? /// A `type: ignore` directive that no longer matches any diagnostic violations is likely @@ -22,7 +22,7 @@ declare_lint! { /// /// ## Examples /// ```py - /// a = 20 / 2 # knot: ignore[division-by-zero] + /// a = 20 / 2 # ty: ignore[division-by-zero] /// ``` /// /// Use instead: @@ -33,30 +33,30 @@ declare_lint! { pub(crate) static UNUSED_IGNORE_COMMENT = { summary: "detects unused `type: ignore` comments", status: LintStatus::preview("1.0.0"), - default_level: Level::Warn, + default_level: Level::Ignore, } } declare_lint! { /// ## What it does - /// Checks for `knot: ignore[code]` where `code` isn't a known lint rule. + /// Checks for `ty: ignore[code]` where `code` isn't a known lint rule. /// /// ## Why is this bad? - /// A `knot: ignore[code]` directive with a `code` that doesn't match + /// A `ty: ignore[code]` directive with a `code` that doesn't match /// any known rule will not suppress any type errors, and is probably a mistake. /// /// ## Examples /// ```py - /// a = 20 / 0 # knot: ignore[division-by-zer] + /// a = 20 / 0 # ty: ignore[division-by-zer] /// ``` /// /// Use instead: /// /// ```py - /// a = 20 / 0 # knot: ignore[division-by-zero] + /// a = 20 / 0 # ty: ignore[division-by-zero] /// ``` pub(crate) static UNKNOWN_RULE = { - summary: "detects `knot: ignore` comments that reference unknown rules", + summary: "detects `ty: ignore` comments that reference unknown rules", status: LintStatus::preview("1.0.0"), default_level: Level::Warn, } @@ -64,7 +64,7 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect. + /// Checks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect. /// /// ## Why is this bad? /// A syntactically incorrect ignore comment is probably a mistake and is useless. @@ -86,10 +86,10 @@ declare_lint! { } } -#[salsa::tracked(return_ref)] +#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)] pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { - let parsed = parsed_module(db.upcast(), file); - let source = source_text(db.upcast(), file); + let parsed = parsed_module(db, file).load(db); + let source = source_text(db, file); let mut builder = SuppressionsBuilder::new(&source, db.lint_registry()); let mut line_start = TextSize::default(); @@ -141,7 +141,7 @@ pub(crate) fn check_suppressions(db: &dyn Db, file: File, diagnostics: &mut Type check_unused_suppressions(&mut context); } -/// Checks for `knot: ignore` comments that reference unknown rules. +/// Checks for `ty: ignore` comments that reference unknown rules. fn check_unknown_rule(context: &mut CheckSuppressionsContext) { if context.is_lint_disabled(&UNKNOWN_RULE) { return; @@ -241,7 +241,7 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) { // This looks silly but it's necessary to check again if a `unused-ignore-comment` is indeed unused // in case the "unused" directive comes after it: // ```py - // a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero] + // a = 10 / 2 # ty: ignore[unused-ignore-comment, division-by-zero] // ``` if context.diagnostics.is_used(suppression.id()) { continue; @@ -290,7 +290,10 @@ impl<'a> CheckSuppressionsContext<'a> { } fn is_lint_disabled(&self, lint: &'static LintMetadata) -> bool { - !self.db.rule_selection().is_enabled(LintId::of(lint)) + !self + .db + .rule_selection(self.file) + .is_enabled(LintId::of(lint)) } fn report_lint( @@ -315,7 +318,7 @@ impl<'a> CheckSuppressionsContext<'a> { range: TextRange, message: fmt::Arguments, ) { - let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else { + let Some(severity) = self.db.rule_selection(self.file).severity(LintId::of(lint)) else { return; }; @@ -328,7 +331,7 @@ impl<'a> CheckSuppressionsContext<'a> { } /// The suppressions of a single file. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, get_size2::GetSize)] pub(crate) struct Suppressions { /// Suppressions that apply to the entire file. /// @@ -416,12 +419,12 @@ impl<'a> IntoIterator for &'a Suppressions { } } -/// A `type: ignore` or `knot: ignore` suppression. +/// A `type: ignore` or `ty: ignore` suppression. /// /// Suppression comments that suppress multiple codes /// create multiple suppressions: one for every code. /// They all share the same `comment_range`. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] pub(crate) struct Suppression { target: SuppressionTarget, kind: SuppressionKind, @@ -463,10 +466,10 @@ impl Suppression { /// The wrapped `TextRange` is the suppression's range. /// This is unique enough because it is its exact /// location in the source. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] pub(crate) struct FileSuppressionId(TextRange); -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)] enum SuppressionTarget { /// Suppress all lints All, @@ -474,7 +477,7 @@ enum SuppressionTarget { /// Suppress the lint with the given id Lint(LintId), - /// Suppresses no lint, e.g. `knot: ignore[]` + /// Suppresses no lint, e.g. `ty: ignore[]` Empty, } @@ -577,7 +580,7 @@ impl<'a> SuppressionsBuilder<'a> { }); } - // `knot: ignore[]` + // `ty: ignore[]` Some([]) => { self.line.push(Suppression { target: SuppressionTarget::Empty, @@ -588,7 +591,7 @@ impl<'a> SuppressionsBuilder<'a> { }); } - // `knot: ignore[a, b]` + // `ty: ignore[a, b]` Some(codes) => { for code_range in codes { let code = &self.source[*code_range]; @@ -625,7 +628,7 @@ impl<'a> SuppressionsBuilder<'a> { } /// Suppression for an unknown lint rule. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, get_size2::GetSize)] struct UnknownSuppression { /// The range of the code. range: TextRange, @@ -636,7 +639,7 @@ struct UnknownSuppression { reason: GetLintError, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, get_size2::GetSize)] struct InvalidSuppression { kind: SuppressionKind, error: ParseError, @@ -695,8 +698,8 @@ impl<'src> SuppressionParser<'src> { fn eat_kind(&mut self) -> Option { let kind = if self.cursor.as_str().starts_with("type") { SuppressionKind::TypeIgnore - } else if self.cursor.as_str().starts_with("knot") { - SuppressionKind::Knot + } else if self.cursor.as_str().starts_with("ty") { + SuppressionKind::Ty } else { return None; }; @@ -737,7 +740,7 @@ impl<'src> SuppressionParser<'src> { self.eat_whitespace(); - // `knot: ignore[]` or `knot: ignore[a,]` + // `ty: ignore[]` or `ty: ignore[a,]` if self.cursor.eat_char(']') { break Ok(Some(codes)); } @@ -757,7 +760,7 @@ impl<'src> SuppressionParser<'src> { if self.cursor.eat_char(']') { break Ok(Some(codes)); } - // `knot: ignore[a b] + // `ty: ignore[a b] return self.syntax_error(ParseErrorKind::CodesMissingComma(kind)); } } @@ -840,10 +843,10 @@ struct SuppressionComment { codes: Option>, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)] enum SuppressionKind { TypeIgnore, - Knot, + Ty, } impl SuppressionKind { @@ -854,7 +857,7 @@ impl SuppressionKind { fn len_utf8(self) -> usize { match self { SuppressionKind::TypeIgnore => "type".len(), - SuppressionKind::Knot => "knot".len(), + SuppressionKind::Ty => "ty".len(), } } } @@ -863,12 +866,12 @@ impl fmt::Display for SuppressionKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { SuppressionKind::TypeIgnore => f.write_str("type: ignore"), - SuppressionKind::Knot => f.write_str("knot: ignore"), + SuppressionKind::Ty => f.write_str("ty: ignore"), } } } -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Eq, PartialEq, Clone, get_size2::GetSize)] struct ParseError { kind: ParseErrorKind, @@ -890,7 +893,7 @@ impl fmt::Display for ParseError { impl Error for ParseError {} -#[derive(Debug, Eq, PartialEq, Clone, Error)] +#[derive(Debug, Eq, PartialEq, Clone, Error, get_size2::GetSize)] enum ParseErrorKind { /// The comment isn't a suppression comment. #[error("not a suppression comment")] @@ -911,11 +914,11 @@ enum ParseErrorKind { #[error("expected a comma separating the rule codes")] CodesMissingComma(SuppressionKind), - /// `knot: ignore[*.*]` + /// `ty: ignore[*.*]` #[error("expected a alphanumeric character or `-` or `_` as code")] InvalidCode(SuppressionKind), - /// `knot: ignore[a, b` + /// `ty: ignore[a, b` #[error("expected a closing bracket")] CodesMissingClosingBracket(SuppressionKind), } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs new file mode 100644 index 0000000000000..ed37dcc0c6b91 --- /dev/null +++ b/crates/ty_python_semantic/src/types.rs @@ -0,0 +1,8749 @@ +use infer::nearest_enclosing_class; +use itertools::{Either, Itertools}; +use ruff_db::parsed::parsed_module; + +use std::slice::Iter; + +use bitflags::bitflags; +use call::{CallDunderError, CallError, CallErrorKind}; +use context::InferContext; +use diagnostic::{ + INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, POSSIBLY_UNBOUND_IMPLICIT_CALL, + UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, +}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; +use ruff_db::files::File; +use ruff_python_ast::name::Name; +use ruff_python_ast::{self as ast, AnyNodeRef}; +use ruff_text_size::{Ranged, TextRange}; +use type_ordering::union_or_intersection_elements_ordering; + +pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; +pub(crate) use self::cyclic::{PairVisitor, TypeTransformer}; +pub use self::diagnostic::TypeCheckDiagnostics; +pub(crate) use self::diagnostic::register_lints; +pub(crate) use self::infer::{ + infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, + infer_scope_types, +}; +pub(crate) use self::signatures::{CallableSignature, Signature}; +pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; +use crate::module_name::ModuleName; +use crate::module_resolver::{KnownModule, resolve_module}; +use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol}; +use crate::semantic_index::definition::Definition; +use crate::semantic_index::place::{ScopeId, ScopedPlaceId}; +use crate::semantic_index::{imported_modules, place_table, semantic_index}; +use crate::suppression::check_suppressions; +use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallableBinding}; +pub(crate) use crate::types::class_base::ClassBase; +use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; +use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; +use crate::types::function::{ + DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction, +}; +use crate::types::generics::{ + GenericContext, PartialSpecialization, Specialization, walk_generic_context, + walk_partial_specialization, walk_specialization, +}; +pub use crate::types::ide_support::{ + CallSignatureDetails, all_members, call_signature_details, definition_kind_for_name, +}; +use crate::types::infer::infer_unpack_types; +use crate::types::mro::{Mro, MroError, MroIterator}; +pub(crate) use crate::types::narrow::infer_narrowing_constraint; +use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature}; +use crate::types::tuple::{TupleSpec, TupleType}; +pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic; +use crate::{Db, FxOrderSet, Module, Program}; +pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; +use instance::Protocol; +pub use instance::{NominalInstanceType, ProtocolInstanceType}; +pub use special_form::SpecialFormType; + +mod builder; +mod call; +mod class; +mod class_base; +mod context; +mod cyclic; +mod diagnostic; +mod display; +mod function; +mod generics; +pub(crate) mod ide_support; +mod infer; +mod instance; +mod mro; +mod narrow; +mod protocol_class; +mod signatures; +mod special_form; +mod string_annotation; +mod subclass_of; +mod tuple; +mod type_ordering; +mod unpacker; +mod visitor; + +mod definition; +#[cfg(test)] +mod property_tests; + +pub fn check_types(db: &dyn Db, file: File) -> Vec { + let _span = tracing::trace_span!("check_types", ?file).entered(); + + tracing::debug!("Checking file '{path}'", path = file.path(db)); + + let index = semantic_index(db, file); + let mut diagnostics = TypeCheckDiagnostics::default(); + + for scope_id in index.scope_ids() { + let result = infer_scope_types(db, scope_id); + diagnostics.extend(result.diagnostics()); + } + + diagnostics.extend_diagnostics( + index + .semantic_syntax_errors() + .iter() + .map(|error| Diagnostic::invalid_syntax(file, error, error)), + ); + + check_suppressions(db, file, &mut diagnostics); + + diagnostics.into_vec() +} + +/// Infer the type of a binding. +pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> { + let inference = infer_definition_types(db, definition); + inference.binding_type(definition) +} + +/// Infer the type of a declaration. +pub(crate) fn declaration_type<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> TypeAndQualifiers<'db> { + let inference = infer_definition_types(db, definition); + inference.declaration_type(definition) +} + +/// Infer the type of a (possibly deferred) sub-expression of a [`Definition`]. +/// +/// Supports expressions that are evaluated within a type-params sub-scope. +/// +/// ## Panics +/// If the given expression is not a sub-expression of the given [`Definition`]. +fn definition_expression_type<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + expression: &ast::Expr, +) -> Type<'db> { + let file = definition.file(db); + let index = semantic_index(db, file); + let file_scope = index.expression_scope_id(expression); + let scope = file_scope.to_scope_id(db, file); + if scope == definition.scope(db) { + // expression is in the definition scope + let inference = infer_definition_types(db, definition); + if let Some(ty) = inference.try_expression_type(expression) { + ty + } else { + infer_deferred_types(db, definition).expression_type(expression) + } + } else { + // expression is in a type-params sub-scope + infer_scope_types(db, scope).expression_type(expression) + } +} + +/// The descriptor protocol distinguishes two kinds of descriptors. Non-data descriptors +/// define a `__get__` method, while data descriptors additionally define a `__set__` +/// method or a `__delete__` method. This enum is used to categorize attributes into two +/// groups: (1) data descriptors and (2) normal attributes or non-data descriptors. +#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) enum AttributeKind { + DataDescriptor, + NormalOrNonDataDescriptor, +} + +impl AttributeKind { + const fn is_data(self) -> bool { + matches!(self, Self::DataDescriptor) + } +} + +/// This enum is used to control the behavior of the descriptor protocol implementation. +/// When invoked on a class object, the fallback type (a class attribute) can shadow a +/// non-data descriptor of the meta-type (the class's metaclass). However, this is not +/// true for instances. When invoked on an instance, the fallback type (an attribute on +/// the instance) can not completely shadow a non-data descriptor of the meta-type (the +/// class), because we do not currently attempt to statically infer if an instance +/// attribute is definitely defined (i.e. to check whether a particular method has been +/// called). +#[derive(Clone, Debug, Copy, PartialEq)] +enum InstanceFallbackShadowsNonDataDescriptor { + Yes, + No, +} + +bitflags! { + #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)] + pub(crate) struct MemberLookupPolicy: u8 { + /// Dunder methods are looked up on the meta-type of a type without potentially falling + /// back on attributes on the type itself. For example, when implicitly invoked on an + /// instance, dunder methods are not looked up as instance attributes. And when invoked + /// on a class, dunder methods are only looked up on the metaclass, not the class itself. + /// + /// All other attributes use the `WithInstanceFallback` policy. + /// + /// If this flag is set - look up the attribute on the meta-type only. + const NO_INSTANCE_FALLBACK = 1 << 0; + + /// When looking up an attribute on a class, we sometimes need to avoid + /// looking up attributes defined on the `object` class. Usually because + /// typeshed doesn't properly encode runtime behavior (e.g. see how `__new__` & `__init__` + /// are handled during class creation). + /// + /// If this flag is set - exclude attributes defined on `object` when looking up attributes. + const MRO_NO_OBJECT_FALLBACK = 1 << 1; + + /// When looking up an attribute on a class, we sometimes need to avoid + /// looking up attributes defined on `type` if this is the metaclass of the class. + /// + /// This is similar to no object fallback above + const META_CLASS_NO_TYPE_FALLBACK = 1 << 2; + } +} + +impl MemberLookupPolicy { + /// Only look up the attribute on the meta-type. + /// + /// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance + /// if the meta-type attribute is not found or if the meta-type attribute is not a data + /// descriptor. + pub(crate) const fn no_instance_fallback(self) -> bool { + self.contains(Self::NO_INSTANCE_FALLBACK) + } + + /// Exclude attributes defined on `object` when looking up attributes. + pub(crate) const fn mro_no_object_fallback(self) -> bool { + self.contains(Self::MRO_NO_OBJECT_FALLBACK) + } + + /// Exclude attributes defined on `type` when looking up meta-class-attributes. + pub(crate) const fn meta_class_no_type_fallback(self) -> bool { + self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) + } +} + +impl Default for MemberLookupPolicy { + fn default() -> Self { + Self::empty() + } +} + +fn member_lookup_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &PlaceAndQualifiers<'db>, + _count: u32, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn member_lookup_cycle_initial<'db>( + _db: &'db dyn Db, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> PlaceAndQualifiers<'db> { + Place::bound(Type::Never).into() +} + +fn class_lookup_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &PlaceAndQualifiers<'db>, + _count: u32, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn class_lookup_cycle_initial<'db>( + _db: &'db dyn Db, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> PlaceAndQualifiers<'db> { + Place::bound(Type::Never).into() +} + +/// Meta data for `Type::Todo`, which represents a known limitation in ty. +#[cfg(debug_assertions)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] +pub struct TodoType(pub &'static str); + +#[cfg(debug_assertions)] +impl std::fmt::Display for TodoType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({msg})", msg = self.0) + } +} + +#[cfg(not(debug_assertions))] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] +pub struct TodoType; + +#[cfg(not(debug_assertions))] +impl std::fmt::Display for TodoType { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +/// Create a `Type::Todo` variant to represent a known limitation in the type system. +/// +/// It can be created by specifying a custom message: `todo_type!("PEP 604 not supported")`. +#[cfg(debug_assertions)] +macro_rules! todo_type { + ($message:literal) => {{ + const _: () = { + let s = $message; + + if !s.is_ascii() { + panic!("todo_type! message must be ASCII"); + } + + let bytes = s.as_bytes(); + let mut i = 0; + while i < bytes.len() { + // Check each byte for '(' or ')' + let ch = bytes[i]; + + assert!( + !40u8.eq_ignore_ascii_case(&ch) && !41u8.eq_ignore_ascii_case(&ch), + "todo_type! message must not contain parentheses", + ); + i += 1; + } + }; + $crate::types::Type::Dynamic($crate::types::DynamicType::Todo($crate::types::TodoType( + $message, + ))) + }}; + ($message:ident) => { + $crate::types::Type::Dynamic($crate::types::DynamicType::Todo($crate::types::TodoType( + $message, + ))) + }; +} + +#[cfg(not(debug_assertions))] +macro_rules! todo_type { + () => { + $crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType)) + }; + ($message:literal) => { + $crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType)) + }; + ($message:ident) => { + $crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType)) + }; +} + +pub use crate::types::definition::TypeDefinition; +pub(crate) use todo_type; + +/// Represents an instance of `builtins.property`. +/// +/// # Ordering +/// Ordering is based on the property instance's salsa-assigned id and not on its values. +/// The id may change between runs, or when the property instance was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct PropertyInstanceType<'db> { + getter: Option>, + setter: Option>, +} + +fn walk_property_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + property: PropertyInstanceType<'db>, + visitor: &mut V, +) { + if let Some(getter) = property.getter(db) { + visitor.visit_type(db, getter); + } + if let Some(setter) = property.setter(db) { + visitor.visit_type(db, setter); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for PropertyInstanceType<'_> {} + +impl<'db> PropertyInstanceType<'db> { + fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + let getter = self + .getter(db) + .map(|ty| ty.apply_type_mapping(db, type_mapping)); + let setter = self + .setter(db) + .map(|ty| ty.apply_type_mapping(db, type_mapping)); + Self::new(db, getter, setter) + } + + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + Self::new( + db, + self.getter(db).map(|ty| ty.normalized_impl(db, visitor)), + self.setter(db).map(|ty| ty.normalized_impl(db, visitor)), + ) + } + + fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + if let Some(ty) = self.getter(db) { + ty.find_legacy_typevars(db, typevars); + } + if let Some(ty) = self.setter(db) { + ty.find_legacy_typevars(db, typevars); + } + } + + fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::new( + db, + self.getter(db).map(|ty| ty.materialize(db, variance)), + self.setter(db).map(|ty| ty.materialize(db, variance)), + ) + } +} + +bitflags! { + /// Used for the return type of `dataclass(…)` calls. Keeps track of the arguments + /// that were passed in. For the precise meaning of the fields, see [1]. + /// + /// [1]: https://docs.python.org/3/library/dataclasses.html + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct DataclassParams: u16 { + const INIT = 0b0000_0000_0001; + const REPR = 0b0000_0000_0010; + const EQ = 0b0000_0000_0100; + const ORDER = 0b0000_0000_1000; + const UNSAFE_HASH = 0b0000_0001_0000; + const FROZEN = 0b0000_0010_0000; + const MATCH_ARGS = 0b0000_0100_0000; + const KW_ONLY = 0b0000_1000_0000; + const SLOTS = 0b0001_0000_0000; + const WEAKREF_SLOT = 0b0010_0000_0000; + } +} + +impl get_size2::GetSize for DataclassParams {} + +impl Default for DataclassParams { + fn default() -> Self { + Self::INIT | Self::REPR | Self::EQ | Self::MATCH_ARGS + } +} + +impl From for DataclassParams { + fn from(params: DataclassTransformerParams) -> Self { + let mut result = Self::default(); + + result.set( + Self::EQ, + params.contains(DataclassTransformerParams::EQ_DEFAULT), + ); + result.set( + Self::ORDER, + params.contains(DataclassTransformerParams::ORDER_DEFAULT), + ); + result.set( + Self::KW_ONLY, + params.contains(DataclassTransformerParams::KW_ONLY_DEFAULT), + ); + result.set( + Self::FROZEN, + params.contains(DataclassTransformerParams::FROZEN_DEFAULT), + ); + + result + } +} + +/// Representation of a type: a set of possible values at runtime. +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub enum Type<'db> { + /// The dynamic type: a statically unknown set of values + Dynamic(DynamicType), + /// The empty set of values + Never, + /// A specific function object + FunctionLiteral(FunctionType<'db>), + /// Represents a callable `instance.method` where `instance` is an instance of a class + /// and `method` is a method (of that class). + /// + /// See [`BoundMethodType`] for more information. + /// + /// TODO: consider replacing this with `Callable & Instance(MethodType)`? + /// I.e. if we have a method `def f(self, x: int) -> str`, and see it being called as + /// `instance.f`, we could partially apply (and check) the `instance` argument against + /// the `self` parameter, and return a `MethodType & Callable[[int], str]`. + /// One drawback would be that we could not show the bound instance when that type is displayed. + BoundMethod(BoundMethodType<'db>), + /// Represents a specific instance of `types.MethodWrapperType`. + /// + /// TODO: consider replacing this with `Callable & types.MethodWrapperType` type? + /// Requires `Callable` to be able to represent overloads, e.g. `types.FunctionType.__get__` has + /// this behaviour when a method is accessed on a class vs an instance: + /// + /// ```txt + /// * (None, type) -> Literal[function_on_which_it_was_called] + /// * (object, type | None) -> BoundMethod[instance, function_on_which_it_was_called] + /// ``` + MethodWrapper(MethodWrapperKind<'db>), + /// Represents a specific instance of `types.WrapperDescriptorType`. + /// + /// TODO: Similar to above, this could eventually be replaced by a generic `Callable` + /// type. We currently add this as a separate variant because `FunctionType.__get__` + /// is an overloaded method and we do not support `@overload` yet. + WrapperDescriptor(WrapperDescriptorKind), + /// A special callable that is returned by a `dataclass(…)` call. It is usually + /// used as a decorator. Note that this is only used as a return type for actual + /// `dataclass` calls, not for the argumentless `@dataclass` decorator. + DataclassDecorator(DataclassParams), + /// A special callable that is returned by a `dataclass_transform(…)` call. + DataclassTransformer(DataclassTransformerParams), + /// The type of an arbitrary callable object with a certain specified signature. + Callable(CallableType<'db>), + /// A specific module object + ModuleLiteral(ModuleLiteralType<'db>), + /// A specific class object + ClassLiteral(ClassLiteral<'db>), + /// A specialization of a generic class + GenericAlias(GenericAlias<'db>), + /// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`. + SubclassOf(SubclassOfType<'db>), + /// The set of Python objects with the given class in their __class__'s method resolution order. + /// Construct this variant using the `Type::instance` constructor function. + NominalInstance(NominalInstanceType<'db>), + /// The set of Python objects that conform to the interface described by a given protocol. + /// Construct this variant using the `Type::instance` constructor function. + ProtocolInstance(ProtocolInstanceType<'db>), + /// A single Python object that requires special treatment in the type system, + /// and which exists at a location that can be known prior to any analysis by ty. + SpecialForm(SpecialFormType), + /// Singleton types that are heavily special-cased by ty, and which are usually + /// created as a result of some runtime operation (e.g. a type-alias statement, + /// a typevar definition, or `Generic[T]` in a class's bases list). + KnownInstance(KnownInstanceType<'db>), + /// An instance of `builtins.property` + PropertyInstance(PropertyInstanceType<'db>), + /// The set of objects in any of the types in the union + Union(UnionType<'db>), + /// The set of objects in all of the types in the intersection + Intersection(IntersectionType<'db>), + /// Represents objects whose `__bool__` method is deterministic: + /// - `AlwaysTruthy`: `__bool__` always returns `True` + /// - `AlwaysFalsy`: `__bool__` always returns `False` + AlwaysTruthy, + AlwaysFalsy, + /// An integer literal + IntLiteral(i64), + /// A boolean literal, either `True` or `False`. + BooleanLiteral(bool), + /// A string literal whose value is known + StringLiteral(StringLiteralType<'db>), + /// A string known to originate only from literal values, but whose value is not known (unlike + /// `StringLiteral` above). + LiteralString, + /// A bytes literal + BytesLiteral(BytesLiteralType<'db>), + /// An instance of the builtin `tuple` class. + /// TODO: Consider removing this in favor of `NominalInstance`. This is currently stored as a + /// separate variant partly for historical reasons, and partly to allow us to easily + /// distinguish tuples since they occur so often. + Tuple(TupleType<'db>), + /// An instance of a typevar in a generic class or function. When the generic class or function + /// is specialized, we will replace this typevar with its specialization. + TypeVar(TypeVarInstance<'db>), + /// A bound super object like `super()` or `super(A, A())` + /// This type doesn't handle an unbound super object like `super(A)`; for that we just use + /// a `Type::NominalInstance` of `builtins.super`. + BoundSuper(BoundSuperType<'db>), + /// A subtype of `bool` that allows narrowing in both positive and negative cases. + TypeIs(TypeIsType<'db>), +} + +#[salsa::tracked] +impl<'db> Type<'db> { + pub const fn any() -> Self { + Self::Dynamic(DynamicType::Any) + } + + pub const fn unknown() -> Self { + Self::Dynamic(DynamicType::Unknown) + } + + pub fn object(db: &'db dyn Db) -> Self { + KnownClass::Object.to_instance(db) + } + + pub const fn is_unknown(&self) -> bool { + matches!(self, Type::Dynamic(DynamicType::Unknown)) + } + + pub const fn is_never(&self) -> bool { + matches!(self, Type::Never) + } + + /// Returns `true` if `self` is [`Type::Callable`]. + pub const fn is_callable_type(&self) -> bool { + matches!(self, Type::Callable(..)) + } + + fn is_none(&self, db: &'db dyn Db) -> bool { + self.into_nominal_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) + } + + fn is_bool(&self, db: &'db dyn Db) -> bool { + self.into_nominal_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::Bool)) + } + + pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { + self.into_nominal_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) + } + + pub fn is_object(&self, db: &'db dyn Db) -> bool { + self.into_nominal_instance() + .is_some_and(|instance| instance.class.is_object(db)) + } + + pub const fn is_todo(&self) -> bool { + matches!(self, Type::Dynamic(DynamicType::Todo(_))) + } + + pub const fn is_generic_alias(&self) -> bool { + matches!(self, Type::GenericAlias(_)) + } + + const fn is_dynamic(&self) -> bool { + matches!(self, Type::Dynamic(_)) + } + + /// Returns the top materialization (or upper bound materialization) of this type, which is the + /// most general form of the type that is fully static. + #[must_use] + pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { + self.materialize(db, TypeVarVariance::Covariant) + } + + /// Returns the bottom materialization (or lower bound materialization) of this type, which is + /// the most specific form of the type that is fully static. + #[must_use] + pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { + self.materialize(db, TypeVarVariance::Contravariant) + } + + /// Returns the materialization of this type depending on the given `variance`. + /// + /// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of + /// the dynamic types (`Any`, `Unknown`, `Todo`) replaced as follows: + /// + /// - In covariant position, it's replaced with `object` + /// - In contravariant position, it's replaced with `Never` + /// - In invariant position, it's replaced with an unresolved type variable + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> { + match self { + Type::Dynamic(_) => match variance { + // TODO: For an invariant position, e.g. `list[Any]`, it should be replaced with an + // existential type representing "all lists, containing any type." We currently + // represent this by replacing `Any` in invariant position with an unresolved type + // variable. + TypeVarVariance::Invariant => Type::TypeVar(TypeVarInstance::new( + db, + Name::new_static("T_all"), + None, + None, + variance, + None, + TypeVarKind::Pep695, + )), + TypeVarVariance::Covariant => Type::object(db), + TypeVarVariance::Contravariant => Type::Never, + TypeVarVariance::Bivariant => unreachable!(), + }, + + Type::Never + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::ClassLiteral(_) + | Type::BoundSuper(_) => *self, + + Type::PropertyInstance(property_instance) => { + Type::PropertyInstance(property_instance.materialize(db, variance)) + } + + Type::FunctionLiteral(_) | Type::BoundMethod(_) => { + // TODO: Subtyping between function / methods with a callable accounts for the + // signature (parameters and return type), so we might need to do something here + *self + } + + Type::NominalInstance(nominal_instance_type) => { + Type::NominalInstance(nominal_instance_type.materialize(db, variance)) + } + Type::GenericAlias(generic_alias) => { + Type::GenericAlias(generic_alias.materialize(db, variance)) + } + Type::Callable(callable_type) => { + Type::Callable(callable_type.materialize(db, variance)) + } + Type::SubclassOf(subclass_of_type) => subclass_of_type.materialize(db, variance), + Type::ProtocolInstance(protocol_instance_type) => { + // TODO: Add tests for this once subtyping/assignability is implemented for + // protocols. It _might_ require changing the logic here because: + // + // > Subtyping for protocol instances involves taking account of the fact that + // > read-only property members, and method members, on protocols act covariantly; + // > write-only property members act contravariantly; and read/write attribute + // > members on protocols act invariantly + Type::ProtocolInstance(protocol_instance_type.materialize(db, variance)) + } + Type::Union(union_type) => union_type.map(db, |ty| ty.materialize(db, variance)), + Type::Intersection(intersection_type) => IntersectionBuilder::new(db) + .positive_elements( + intersection_type + .positive(db) + .iter() + .map(|ty| ty.materialize(db, variance)), + ) + .negative_elements( + intersection_type + .negative(db) + .iter() + .map(|ty| ty.materialize(db, variance.flip())), + ) + .build(), + Type::Tuple(tuple_type) => Type::tuple(tuple_type.materialize(db, variance)), + Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)), + Type::TypeIs(type_is) => { + type_is.with_type(db, type_is.return_type(db).materialize(db, variance)) + } + } + } + + pub const fn into_class_literal(self) -> Option> { + match self { + Type::ClassLiteral(class_type) => Some(class_type), + _ => None, + } + } + + #[track_caller] + pub fn expect_class_literal(self) -> ClassLiteral<'db> { + self.into_class_literal() + .expect("Expected a Type::ClassLiteral variant") + } + + pub const fn is_subclass_of(&self) -> bool { + matches!(self, Type::SubclassOf(..)) + } + + pub const fn is_class_literal(&self) -> bool { + matches!(self, Type::ClassLiteral(..)) + } + + pub(crate) const fn into_tuple(self) -> Option> { + match self { + Type::Tuple(tuple_type) => Some(tuple_type), + _ => None, + } + } + + /// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`. + /// Since a `ClassType` must be specialized, apply the default specialization to any + /// unspecialized generic class literal. + pub fn to_class_type(self, db: &'db dyn Db) -> Option> { + match self { + Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)), + Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), + _ => None, + } + } + + #[track_caller] + pub fn expect_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + self.to_class_type(db) + .expect("Expected a Type::GenericAlias or Type::ClassLiteral variant") + } + + pub fn is_class_type(&self, db: &'db dyn Db) -> bool { + match self { + Type::ClassLiteral(class) if class.generic_context(db).is_none() => true, + Type::GenericAlias(_) => true, + _ => false, + } + } + + pub const fn is_property_instance(&self) -> bool { + matches!(self, Type::PropertyInstance(..)) + } + + pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: &Module) -> Self { + Self::ModuleLiteral(ModuleLiteralType::new( + db, + submodule, + submodule.kind().is_package().then_some(importing_file), + )) + } + + pub const fn into_module_literal(self) -> Option> { + match self { + Type::ModuleLiteral(module) => Some(module), + _ => None, + } + } + + #[track_caller] + pub fn expect_module_literal(self) -> ModuleLiteralType<'db> { + self.into_module_literal() + .expect("Expected a Type::ModuleLiteral variant") + } + + pub const fn into_union(self) -> Option> { + match self { + Type::Union(union_type) => Some(union_type), + _ => None, + } + } + + #[track_caller] + pub fn expect_union(self) -> UnionType<'db> { + self.into_union().expect("Expected a Type::Union variant") + } + + pub const fn is_union(&self) -> bool { + matches!(self, Type::Union(..)) + } + + pub const fn into_intersection(self) -> Option> { + match self { + Type::Intersection(intersection_type) => Some(intersection_type), + _ => None, + } + } + + #[track_caller] + pub fn expect_intersection(self) -> IntersectionType<'db> { + self.into_intersection() + .expect("Expected a Type::Intersection variant") + } + + pub const fn into_function_literal(self) -> Option> { + match self { + Type::FunctionLiteral(function_type) => Some(function_type), + _ => None, + } + } + + #[track_caller] + pub fn expect_function_literal(self) -> FunctionType<'db> { + self.into_function_literal() + .expect("Expected a Type::FunctionLiteral variant") + } + + pub const fn is_function_literal(&self) -> bool { + matches!(self, Type::FunctionLiteral(..)) + } + + pub const fn is_bound_method(&self) -> bool { + matches!(self, Type::BoundMethod(..)) + } + + pub fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { + self.into_union().is_some_and(|union| { + union + .elements(db) + .iter() + .all(|ty| ty.is_single_valued(db) || ty.is_bool(db) || ty.is_literal_string()) + }) || self.is_bool(db) + || self.is_literal_string() + } + + pub const fn into_int_literal(self) -> Option { + match self { + Type::IntLiteral(value) => Some(value), + _ => None, + } + } + + pub fn into_string_literal(self) -> Option> { + match self { + Type::StringLiteral(string_literal) => Some(string_literal), + _ => None, + } + } + + pub fn is_string_literal(&self) -> bool { + matches!(self, Type::StringLiteral(..)) + } + + #[track_caller] + pub fn expect_int_literal(self) -> i64 { + self.into_int_literal() + .expect("Expected a Type::IntLiteral variant") + } + + pub const fn is_boolean_literal(&self) -> bool { + matches!(self, Type::BooleanLiteral(..)) + } + + pub const fn is_literal_string(&self) -> bool { + matches!(self, Type::LiteralString) + } + + pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { + Self::StringLiteral(StringLiteralType::new(db, string)) + } + + pub fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { + Self::BytesLiteral(BytesLiteralType::new(db, bytes)) + } + + #[must_use] + pub fn negate(&self, db: &'db dyn Db) -> Type<'db> { + IntersectionBuilder::new(db).add_negative(*self).build() + } + + #[must_use] + pub fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { + if yes { self.negate(db) } else { *self } + } + + /// Returns the fallback instance type that a literal is an instance of, or `None` if the type + /// is not a literal. + pub fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { + // There are other literal types that could conceivable be included here: class literals + // falling back to `type[X]`, for instance. For now, there is not much rigorous thought put + // into what's included vs not; this is just an empirical choice that makes our ecosystem + // report look better until we have proper bidirectional type inference. + match self { + Type::StringLiteral(_) | Type::LiteralString => Some(KnownClass::Str.to_instance(db)), + Type::BooleanLiteral(_) => Some(KnownClass::Bool.to_instance(db)), + Type::IntLiteral(_) => Some(KnownClass::Int.to_instance(db)), + Type::BytesLiteral(_) => Some(KnownClass::Bytes.to_instance(db)), + Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)), + _ => None, + } + } + + /// Return a "normalized" version of `self` that ensures that equivalent types have the same Salsa ID. + /// + /// A normalized type: + /// - Has all unions and intersections sorted according to a canonical order, + /// no matter how "deeply" a union/intersection may be nested. + /// - Strips the names of positional-only parameters and variadic parameters from `Callable` types, + /// as these are irrelevant to whether a callable type `X` is equivalent to a callable type `Y`. + /// - Strips the types of default values from parameters in `Callable` types: only whether a parameter + /// *has* or *does not have* a default value is relevant to whether two `Callable` types are equivalent. + /// - Converts class-based protocols into synthesized protocols + #[must_use] + pub fn normalized(self, db: &'db dyn Db) -> Self { + let mut visitor = TypeTransformer::default(); + self.normalized_impl(db, &mut visitor) + } + + #[must_use] + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + match self { + Type::Union(union) => { + visitor.visit(self, |v| Type::Union(union.normalized_impl(db, v))) + } + Type::Intersection(intersection) => visitor.visit(self, |v| { + Type::Intersection(intersection.normalized_impl(db, v)) + }), + Type::Tuple(tuple) => { + visitor.visit(self, |v| Type::tuple(tuple.normalized_impl(db, v))) + } + Type::Callable(callable) => { + visitor.visit(self, |v| Type::Callable(callable.normalized_impl(db, v))) + } + Type::ProtocolInstance(protocol) => { + visitor.visit(self, |v| protocol.normalized_impl(db, v)) + } + Type::NominalInstance(instance) => visitor.visit(self, |v| { + Type::NominalInstance(instance.normalized_impl(db, v)) + }), + Type::FunctionLiteral(function) => visitor.visit(self, |v| { + Type::FunctionLiteral(function.normalized_impl(db, v)) + }), + Type::PropertyInstance(property) => visitor.visit(self, |v| { + Type::PropertyInstance(property.normalized_impl(db, v)) + }), + Type::MethodWrapper(method_kind) => visitor.visit(self, |v| { + Type::MethodWrapper(method_kind.normalized_impl(db, v)) + }), + Type::BoundMethod(method) => { + visitor.visit(self, |v| Type::BoundMethod(method.normalized_impl(db, v))) + } + Type::BoundSuper(bound_super) => visitor.visit(self, |v| { + Type::BoundSuper(bound_super.normalized_impl(db, v)) + }), + Type::GenericAlias(generic) => { + visitor.visit(self, |v| Type::GenericAlias(generic.normalized_impl(db, v))) + } + Type::SubclassOf(subclass_of) => visitor.visit(self, |v| { + Type::SubclassOf(subclass_of.normalized_impl(db, v)) + }), + Type::TypeVar(typevar) => { + visitor.visit(self, |v| Type::TypeVar(typevar.normalized_impl(db, v))) + } + Type::KnownInstance(known_instance) => visitor.visit(self, |v| { + Type::KnownInstance(known_instance.normalized_impl(db, v)) + }), + Type::TypeIs(type_is) => visitor.visit(self, |v| { + type_is.with_type(db, type_is.return_type(db).normalized_impl(db, v)) + }), + Type::Dynamic(dynamic) => Type::Dynamic(dynamic.normalized()), + Type::LiteralString + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::BooleanLiteral(_) + | Type::BytesLiteral(_) + | Type::StringLiteral(_) + | Type::Never + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::SpecialForm(_) + | Type::IntLiteral(_) => self, + } + } + + /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for + /// any `T` of this type. + /// + /// This is true for fully static types, but also for some types that may not be fully static. + /// For example, a `ClassLiteral` may inherit `Any`, but its subtyping is still reflexive. + /// + /// This method may have false negatives, but it should not have false positives. It should be + /// a cheap shallow check, not an exhaustive recursive check. + fn subtyping_is_always_reflexive(self) -> bool { + match self { + Type::Never + | Type::FunctionLiteral(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(..) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::PropertyInstance(_) + // might inherit `Any`, but subtyping is still reflexive + | Type::ClassLiteral(_) => true, + Type::Dynamic(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::Union(_) + | Type::Intersection(_) + | Type::Callable(_) + | Type::Tuple(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) + | Type::TypeIs(_) => false, + } + } + + pub(crate) fn into_callable(self, db: &'db dyn Db) -> Option> { + match self { + Type::Callable(_) => Some(self), + + Type::Dynamic(_) => Some(CallableType::single(db, Signature::dynamic(self))), + + Type::FunctionLiteral(function_literal) => { + Some(Type::Callable(function_literal.into_callable_type(db))) + } + Type::BoundMethod(bound_method) => { + Some(Type::Callable(bound_method.into_callable_type(db))) + } + + Type::NominalInstance(_) | Type::ProtocolInstance(_) => { + let call_symbol = self + .member_lookup_with_policy( + db, + Name::new_static("__call__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place; + + if let Place::Type(ty, Boundness::Bound) = call_symbol { + ty.into_callable(db) + } else { + None + } + } + Type::ClassLiteral(class_literal) => { + Some(ClassType::NonGeneric(class_literal).into_callable(db)) + } + + Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), + + // TODO: This is unsound so in future we can consider an opt-in option to disable it. + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Class(class) => Some(class.into_callable(db)), + SubclassOfInner::Dynamic(dynamic) => Some(CallableType::single( + db, + Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))), + )), + }, + + Type::Union(union) => union.try_map(db, |element| element.into_callable(db)), + + Type::Never + | Type::DataclassTransformer(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::Tuple(_) + | Type::TypeIs(_) => None, + + // TODO + Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::ModuleLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) => None, + } + } + /// Return true if this type is a [subtype of] type `target`. + /// + /// For fully static types, this means that the set of objects represented by `self` is a + /// subset of the objects represented by `target`. + /// + /// For gradual types, it means that the union of all possible sets of values represented by + /// `self` (the "top materialization" of `self`) is a subtype of the intersection of all + /// possible sets of values represented by `target` (the "bottom materialization" of + /// `target`). In other words, for all possible pairs of materializations `self'` and + /// `target'`, `self'` is always a subtype of `target'`. + /// + /// Note that this latter expansion of the subtyping relation to non-fully-static types is not + /// described in the typing spec, but the primary use of the subtyping relation is for + /// simplifying unions and intersections, and this expansion to gradual types is sound and + /// allows us to better simplify many unions and intersections. This definition does mean the + /// subtyping relation is not reflexive for non-fully-static types (e.g. `Any` is not a subtype + /// of `Any`). + /// + /// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence + /// + /// There would be an even more general definition of subtyping for gradual types, allowing a + /// type `S` to be a subtype of a type `T` if the top materialization of `S` (`S+`) is a + /// subtype of `T+`, and the bottom materialization of `S` (`S-`) is a subtype of `T-`. This + /// definition is attractive in that it would restore reflexivity of subtyping for all types, + /// and would mean that gradual equivalence of `S` and `T` could be defined simply as `S <: T + /// && T <: S`. It would also be sound, in that simplifying unions or intersections according + /// to this definition of subtyping would still result in an equivalent type. + /// + /// Unfortunately using this definition would break transitivity of subtyping when both nominal + /// and structural types are involved, because Liskov enforcement for nominal types is based on + /// assignability, so we can have class `A` with method `def meth(self) -> Any` and a subclass + /// `B(A)` with method `def meth(self) -> int`. In this case, `A` would be a subtype of a + /// protocol `P` with method `def meth(self) -> Any`, but `B` would not be a subtype of `P`, + /// and yet `B` is (by nominal subtyping) a subtype of `A`, so we would have `B <: A` and `A <: + /// P`, but not `B <: P`. Losing transitivity of subtyping is not tenable (it makes union and + /// intersection simplification dependent on the order in which elements are added), so we do + /// not use this more general definition of subtyping. + pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + self.has_relation_to(db, target, TypeRelation::Subtyping) + } + + /// Return true if this type is [assignable to] type `target`. + /// + /// [assignable to]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation + pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + self.has_relation_to(db, target, TypeRelation::Assignability) + } + + fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool { + // Subtyping implies assignability, so if subtyping is reflexive and the two types are + // equal, it is both a subtype and assignable. Assignability is always reflexive. + // + // Note that we could do a full equivalence check here, but that would be both expensive + // and unnecessary. This early return is only an optimisation. + if (relation.is_assignability() || self.subtyping_is_always_reflexive()) && self == target { + return true; + } + + match (self, target) { + // Everything is a subtype of `object`. + (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true, + + // `Never` is the bottom type, the empty set. + // It is a subtype of all other types. + (Type::Never, _) => true, + + // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were + // handled above. It's always assignable, though. + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => relation.is_assignability(), + + // In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied: + // 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`. + // TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`. + // 2. `T` is a constrained TypeVar and all of `T`'s constraints are subtypes of `S`. + // + // However, there is one exception to this general rule: for any given typevar `T`, + // `T` will always be a subtype of any union containing `T`. + // A similar rule applies in reverse to intersection types. + (Type::TypeVar(_), Type::Union(union)) if union.elements(db).contains(&self) => true, + (Type::Intersection(intersection), Type::TypeVar(_)) + if intersection.positive(db).contains(&target) => + { + true + } + (Type::Intersection(intersection), Type::TypeVar(_)) + if intersection.negative(db).contains(&target) => + { + false + } + + // Two identical typevars must always solve to the same type, so they are always + // subtypes of each other and assignable to each other. + // + // Note that this is not handled by the early return at the beginning of this method, + // since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive. + (Type::TypeVar(lhs_typevar), Type::TypeVar(rhs_typevar)) + if lhs_typevar == rhs_typevar => + { + true + } + + // A fully static typevar is a subtype of its upper bound, and to something similar to + // the union of its constraints. An unbound, unconstrained, fully static typevar has an + // implicit upper bound of `object` (which is handled above). + (Type::TypeVar(typevar), _) if typevar.bound_or_constraints(db).is_some() => { + match typevar.bound_or_constraints(db) { + None => unreachable!(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.has_relation_to(db, target, relation) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.has_relation_to(db, target, relation)), + } + } + + // If the typevar is constrained, there must be multiple constraints, and the typevar + // might be specialized to any one of them. However, the constraints do not have to be + // disjoint, which means an lhs type might be a subtype of all of the constraints. + (_, Type::TypeVar(typevar)) + if typevar.constraints(db).is_some_and(|constraints| { + constraints + .iter() + .all(|constraint| self.has_relation_to(db, *constraint, relation)) + }) => + { + true + } + + // `Never` is the bottom type, the empty set. + // Other than one unlikely edge case (TypeVars bound to `Never`), + // no other type is a subtype of or assignable to `Never`. + (_, Type::Never) => false, + + (Type::Union(union), _) => union + .elements(db) + .iter() + .all(|&elem_ty| elem_ty.has_relation_to(db, target, relation)), + + (_, Type::Union(union)) => union + .elements(db) + .iter() + .any(|&elem_ty| self.has_relation_to(db, elem_ty, relation)), + + // If both sides are intersections we need to handle the right side first + // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, + // but none of A, B, or C is a subtype of (A & B). + (_, Type::Intersection(intersection)) => { + intersection + .positive(db) + .iter() + .all(|&pos_ty| self.has_relation_to(db, pos_ty, relation)) + && intersection + .negative(db) + .iter() + .all(|&neg_ty| self.is_disjoint_from(db, neg_ty)) + } + + (Type::Intersection(intersection), _) => intersection + .positive(db) + .iter() + .any(|&elem_ty| elem_ty.has_relation_to(db, target, relation)), + + // Other than the special cases checked above, no other types are a subtype of a + // typevar, since there's no guarantee what type the typevar will be specialized to. + // (If the typevar is bounded, it might be specialized to a smaller type than the + // bound. This is true even if the bound is a final class, since the typevar can still + // be specialized to `Never`.) + (_, Type::TypeVar(_)) => false, + + // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. + // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. + (left, Type::AlwaysFalsy) => left.bool(db).is_always_false(), + (left, Type::AlwaysTruthy) => left.bool(db).is_always_true(), + // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). + (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { + target.is_equivalent_to(db, Type::object(db)) + } + + // These clauses handle type variants that include function literals. A function + // literal is the subtype of itself, and not of any other function literal. However, + // our representation of a function literal includes any specialization that should be + // applied to the signature. Different specializations of the same function literal are + // only subtypes of each other if they result in the same signature. + (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { + self_function.has_relation_to(db, target_function, relation) + } + (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { + self_method.has_relation_to(db, target_method, relation) + } + (Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => { + self_method.has_relation_to(db, target_method, relation) + } + + // No literal type is a subtype of any other literal type, unless they are the same + // type (which is handled above). This case is not necessary from a correctness + // perspective (the fallback cases below will handle it correctly), but it is important + // for performance of simplifying large unions of literal types. + ( + Type::StringLiteral(_) + | Type::IntLiteral(_) + | Type::BytesLiteral(_) + | Type::ClassLiteral(_) + | Type::FunctionLiteral(_) + | Type::ModuleLiteral(_), + Type::StringLiteral(_) + | Type::IntLiteral(_) + | Type::BytesLiteral(_) + | Type::ClassLiteral(_) + | Type::FunctionLiteral(_) + | Type::ModuleLiteral(_), + ) => false, + + (Type::Callable(self_callable), Type::Callable(other_callable)) => { + self_callable.has_relation_to(db, other_callable, relation) + } + + (_, Type::Callable(_)) => self + .into_callable(db) + .is_some_and(|callable| callable.has_relation_to(db, target, relation)), + + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.has_relation_to(db, right, relation) + } + // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. + (Type::ProtocolInstance(_), _) => false, + (_, Type::ProtocolInstance(protocol)) => { + self.satisfies_protocol(db, protocol, relation) + } + + // All `StringLiteral` types are a subtype of `LiteralString`. + (Type::StringLiteral(_), Type::LiteralString) => true, + + // Except for the special `LiteralString` case above, + // most `Literal` types delegate to their instance fallbacks + // unless `self` is exactly equivalent to `target` (handled above) + ( + Type::StringLiteral(_) + | Type::LiteralString + | Type::BooleanLiteral(_) + | Type::IntLiteral(_) + | Type::BytesLiteral(_) + | Type::ModuleLiteral(_), + _, + ) => (self.literal_fallback_instance(db)) + .is_some_and(|instance| instance.has_relation_to(db, target, relation)), + + // A `FunctionLiteral` type is a single-valued type like the other literals handled above, + // so it also, for now, just delegates to its instance fallback. + (Type::FunctionLiteral(_), _) => KnownClass::FunctionType + .to_instance(db) + .has_relation_to(db, target, relation), + + // The same reasoning applies for these special callable types: + (Type::BoundMethod(_), _) => KnownClass::MethodType + .to_instance(db) + .has_relation_to(db, target, relation), + (Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType + .to_instance(db) + .has_relation_to(db, target, relation), + (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType + .to_instance(db) + .has_relation_to(db, target, relation), + + (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { + // TODO: Implement subtyping using an equivalent `Callable` type. + false + } + + // `TypeIs` is invariant. + (Type::TypeIs(left), Type::TypeIs(right)) => { + left.return_type(db) + .has_relation_to(db, right.return_type(db), relation) + && right + .return_type(db) + .has_relation_to(db, left.return_type(db), relation) + } + + // `TypeIs[T]` is a subtype of `bool`. + (Type::TypeIs(_), _) => KnownClass::Bool + .to_instance(db) + .has_relation_to(db, target, relation), + + // Function-like callables are subtypes of `FunctionType` + (Type::Callable(callable), _) + if callable.is_function_like(db) + && KnownClass::FunctionType + .to_instance(db) + .has_relation_to(db, target, relation) => + { + true + } + + (Type::Callable(_), _) => false, + + (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { + self_tuple.has_relation_to(db, target_tuple, relation) + } + + (Type::Tuple(self_tuple), Type::NominalInstance(target_instance)) => { + self_tuple.to_class_type(db).is_some_and(|self_class| { + self_class.has_relation_to(db, target_instance.class, relation) + }) + } + (Type::NominalInstance(self_instance), Type::Tuple(target_tuple)) => { + target_tuple.to_class_type(db).is_some_and(|target_class| { + self_instance + .class + .has_relation_to(db, target_class, relation) + }) + } + (Type::Tuple(_), _) => false, + + (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), + (Type::BoundSuper(_), _) => KnownClass::Super + .to_instance(db) + .has_relation_to(db, target, relation), + + // `Literal[]` is a subtype of `type[B]` if `C` is a subclass of `B`, + // since `type[B]` describes all possible runtime subclasses of the class object `B`. + (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty + .subclass_of() + .into_class() + .map(|subclass_of_class| { + ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation) + }) + .unwrap_or(relation.is_assignability()), + (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty + .subclass_of() + .into_class() + .map(|subclass_of_class| { + ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation) + }) + .unwrap_or(relation.is_assignability()), + + // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? + (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { + self_subclass_ty.has_relation_to(db, target_subclass_ty, relation) + } + + // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. + // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object + // is an instance of its metaclass `abc.ABCMeta`. + (Type::ClassLiteral(class), _) => class + .metaclass_instance_type(db) + .has_relation_to(db, target, relation), + (Type::GenericAlias(alias), _) => ClassType::from(alias) + .metaclass_instance_type(db) + .has_relation_to(db, target, relation), + + // `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]` + (Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => { + KnownClass::Type + .to_instance(db) + .has_relation_to(db, other, relation) + || (relation.is_assignability() + && other.has_relation_to(db, KnownClass::Type.to_instance(db), relation)) + } + + // Any `type[...]` type is assignable to `type[Any]` + (other, Type::SubclassOf(subclass_of_ty)) + if subclass_of_ty.is_dynamic() && relation.is_assignability() => + { + other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) + } + + // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses + // of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str` + // is an instance of `type`, and so all possible subclasses of `str` will also be instances of `type`. + // + // Similarly `type[enum.Enum]` is a subtype of `enum.EnumMeta` because `enum.Enum` + // is an instance of `enum.EnumMeta`. `type[Any]` and `type[Unknown]` do not participate in subtyping, + // however, as they are not fully static types. + (Type::SubclassOf(subclass_of_ty), _) => subclass_of_ty + .subclass_of() + .into_class() + .map(|class| class.metaclass_instance_type(db)) + .unwrap_or_else(|| KnownClass::Type.to_instance(db)) + .has_relation_to(db, target, relation), + + // For example: `Type::SpecialForm(SpecialFormType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`, + // because `Type::SpecialForm(SpecialFormType::Type)` is a set with exactly one runtime value in it + // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime. + (Type::SpecialForm(left), right) => left + .instance_fallback(db) + .has_relation_to(db, right, relation), + + (Type::KnownInstance(left), right) => left + .instance_fallback(db) + .has_relation_to(db, right, relation), + + // `bool` is a subtype of `int`, because `bool` subclasses `int`, + // which means that all instances of `bool` are also instances of `int` + (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { + self_instance.has_relation_to(db, target_instance, relation) + } + + (Type::PropertyInstance(_), _) => KnownClass::Property + .to_instance(db) + .has_relation_to(db, target, relation), + (_, Type::PropertyInstance(_)) => { + self.has_relation_to(db, KnownClass::Property.to_instance(db), relation) + } + + // Other than the special cases enumerated above, `Instance` types and typevars are + // never subtypes of any other variants + (Type::NominalInstance(_) | Type::TypeVar(_), _) => false, + } + } + + /// Return true if this type is [equivalent to] type `other`. + /// + /// Two equivalent types represent the same sets of values. + /// + /// > Two gradual types `A` and `B` are equivalent + /// > (that is, the same gradual type, not merely consistent with one another) + /// > if and only if all materializations of `A` are also materializations of `B`, + /// > and all materializations of `B` are also materializations of `A`. + /// > + /// > — [Summary of type relations] + /// + /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { + if self == other { + return true; + } + + match (self, other) { + (Type::Dynamic(_), Type::Dynamic(_)) => true, + + (Type::SubclassOf(first), Type::SubclassOf(second)) => { + match (first.subclass_of(), second.subclass_of()) { + (first, second) if first == second => true, + (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => true, + _ => false, + } + } + + (Type::NominalInstance(first), Type::NominalInstance(second)) => { + first.is_equivalent_to(db, second) + } + + (Type::Tuple(first), Type::Tuple(second)) => first.is_equivalent_to(db, second), + + (Type::Union(first), Type::Union(second)) => first.is_equivalent_to(db, second), + + (Type::Intersection(first), Type::Intersection(second)) => { + first.is_equivalent_to(db, second) + } + + (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { + self_function.is_equivalent_to(db, target_function) + } + (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { + self_method.is_equivalent_to(db, target_method) + } + (Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => { + self_method.is_equivalent_to(db, target_method) + } + (Type::Callable(first), Type::Callable(second)) => first.is_equivalent_to(db, second), + + (Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => { + first.is_equivalent_to(db, second) + } + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class.is_object(db) && protocol.normalized(db) == nominal + } + _ => false, + } + } + + /// Return true if this type and `other` have no common elements. + /// + /// Note: This function aims to have no false positives, but might return + /// wrong `false` answers in some cases. + pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { + let mut visitor = PairVisitor::new(false); + self.is_disjoint_from_impl(db, other, &mut visitor) + } + + pub(crate) fn is_disjoint_from_impl( + self, + db: &'db dyn Db, + other: Type<'db>, + visitor: &mut PairVisitor<'db>, + ) -> bool { + fn any_protocol_members_absent_or_disjoint<'db>( + db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, + other: Type<'db>, + visitor: &mut PairVisitor<'db>, + ) -> bool { + protocol.interface(db).members(db).any(|member| { + other + .member(db, member.name()) + .place + .ignore_possibly_unbound() + .is_none_or(|attribute_type| { + member.has_disjoint_type_from(db, attribute_type, visitor) + }) + }) + } + + match (self, other) { + (Type::Never, _) | (_, Type::Never) => true, + + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false, + + // A typevar is never disjoint from itself, since all occurrences of the typevar must + // be specialized to the same type. (This is an important difference between typevars + // and `Any`!) Different typevars might be disjoint, depending on their bounds and + // constraints, which are handled below. + (Type::TypeVar(self_typevar), Type::TypeVar(other_typevar)) + if self_typevar == other_typevar => + { + false + } + + (tvar @ Type::TypeVar(_), Type::Intersection(intersection)) + | (Type::Intersection(intersection), tvar @ Type::TypeVar(_)) + if intersection.negative(db).contains(&tvar) => + { + true + } + + // An unbounded typevar is never disjoint from any other type, since it might be + // specialized to any type. A bounded typevar is not disjoint from its bound, and is + // only disjoint from other types if its bound is. A constrained typevar is disjoint + // from a type if all of its constraints are. + (Type::TypeVar(typevar), other) | (other, Type::TypeVar(typevar)) => { + match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.is_disjoint_from_impl(db, other, visitor) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_disjoint_from_impl(db, other, visitor)), + } + } + + (Type::Union(union), other) | (other, Type::Union(union)) => union + .elements(db) + .iter() + .all(|e| e.is_disjoint_from_impl(db, other, visitor)), + + // If we have two intersections, we test the positive elements of each one against the other intersection + // Negative elements need a positive element on the other side in order to be disjoint. + // This is similar to what would happen if we tried to build a new intersection that combines the two + (Type::Intersection(self_intersection), Type::Intersection(other_intersection)) => { + self_intersection + .positive(db) + .iter() + .any(|p| p.is_disjoint_from_impl(db, other, visitor)) + || other_intersection + .positive(db) + .iter() + .any(|p: &Type<'_>| p.is_disjoint_from_impl(db, self, visitor)) + } + + (Type::Intersection(intersection), other) + | (other, Type::Intersection(intersection)) => { + intersection + .positive(db) + .iter() + .any(|p| p.is_disjoint_from_impl(db, other, visitor)) + // A & B & Not[C] is disjoint from C + || intersection + .negative(db) + .iter() + .any(|&neg_ty| other.is_subtype_of(db, neg_ty)) + } + + // any single-valued type is disjoint from another single-valued type + // iff the two types are nonequal + ( + left @ (Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::FunctionLiteral(..) + | Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::ModuleLiteral(..) + | Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..)), + right @ (Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::FunctionLiteral(..) + | Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::ModuleLiteral(..) + | Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..)), + ) => left != right, + + // One tuple type can be a subtype of another tuple type, + // but we know for sure that any given tuple type is disjoint from all single-valued types + ( + Type::Tuple(..), + Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::ModuleLiteral(..) + | Type::BooleanLiteral(..) + | Type::BytesLiteral(..) + | Type::FunctionLiteral(..) + | Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::DataclassDecorator(..) + | Type::DataclassTransformer(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::LiteralString, + ) + | ( + Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::ModuleLiteral(..) + | Type::BooleanLiteral(..) + | Type::BytesLiteral(..) + | Type::FunctionLiteral(..) + | Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::DataclassDecorator(..) + | Type::DataclassTransformer(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::LiteralString, + Type::Tuple(..), + ) => true, + + ( + Type::SubclassOf(_), + Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..) + | Type::FunctionLiteral(..) + | Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::ModuleLiteral(..), + ) + | ( + Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::LiteralString + | Type::BytesLiteral(..) + | Type::FunctionLiteral(..) + | Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::ModuleLiteral(..), + Type::SubclassOf(_), + ) => true, + + (Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => { + // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. + // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. + ty.bool(db).is_always_false() + } + (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { + // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. + ty.bool(db).is_always_true() + } + + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_disjoint_from_impl(db, right, visitor) + } + + (Type::ProtocolInstance(protocol), Type::SpecialForm(special_form)) + | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => { + any_protocol_members_absent_or_disjoint(db, protocol, special_form.instance_fallback(db), visitor) + } + + (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) + | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { + any_protocol_members_absent_or_disjoint(db, protocol, known_instance.instance_fallback(db), visitor) + } + + // The absence of a protocol member on one of these types guarantees + // that the type will be disjoint from the protocol, + // but the type will not be disjoint from the protocol if it has a member + // that is of the correct type but is possibly unbound. + // If accessing a member on this type returns a possibly unbound `Place`, + // the type will not be a subtype of the protocol but it will also not be + // disjoint from the protocol, since there are possible subtypes of the type + // that could satisfy the protocol. + // + // ```py + // class Foo: + // if coinflip(): + // X = 42 + // + // class HasX(Protocol): + // @property + // def x(self) -> int: ... + // + // # `TypeOf[Foo]` (a class-literal type) is not a subtype of `HasX`, + // # but `TypeOf[Foo]` & HasX` should not simplify to `Never`, + // # or this branch would be incorrectly understood to be unreachable, + // # since we would understand the type of `Foo` in this branch to be + // # `TypeOf[Foo] & HasX` due to `hasattr()` narrowing. + // + // if hasattr(Foo, "X"): + // print(Foo.X) + // ``` + ( + ty @ (Type::LiteralString + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::BooleanLiteral(..) + | Type::ClassLiteral(..) + | Type::FunctionLiteral(..) + | Type::ModuleLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..)), + Type::ProtocolInstance(protocol), + ) + | ( + Type::ProtocolInstance(protocol), + ty @ (Type::LiteralString + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::BooleanLiteral(..) + | Type::ClassLiteral(..) + | Type::FunctionLiteral(..) + | Type::ModuleLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..)), + ) => any_protocol_members_absent_or_disjoint(db, protocol, ty, visitor), + + // This is the same as the branch above -- + // once guard patterns are stabilised, it could be unified with that branch + // () + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) + if n.class.is_final(db) => + { + any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor) + } + + (Type::ProtocolInstance(protocol), other) + | (other, Type::ProtocolInstance(protocol)) => { + protocol.interface(db).members(db).any(|member| { + matches!( + other.member(db, member.name()).place, + Place::Type(attribute_type, _) if member.has_disjoint_type_from(db, attribute_type, visitor) + ) + }) + } + + (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) + | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { + match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => false, + SubclassOfInner::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), + } + } + + (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) + | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { + match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => false, + SubclassOfInner::Class(class_a) => { + !ClassType::from(alias_b).is_subclass_of(db, class_a) + } + } + } + + (Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from_impl(db, right), + + // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, + // so although the type is dynamic we can still determine disjointedness in some situations + (Type::SubclassOf(subclass_of_ty), other) + | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => { + KnownClass::Type.to_instance(db).is_disjoint_from_impl(db, other, visitor) + } + SubclassOfInner::Class(class) => class + .metaclass_instance_type(db) + .is_disjoint_from_impl(db, other, visitor), + }, + + (Type::SpecialForm(special_form), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { + !special_form.is_instance_of(db, instance.class) + } + + (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { + !known_instance.is_instance_of(db, instance.class) + } + + (Type::SpecialForm(special_form), Type::Tuple(tuple)) + | (Type::Tuple(tuple), Type::SpecialForm(special_form)) => tuple + .to_class_type(db) + .is_some_and(|tuple_class| !special_form.is_instance_of(db, tuple_class)), + + (Type::KnownInstance(known_instance), Type::Tuple(tuple)) + | (Type::Tuple(tuple), Type::KnownInstance(known_instance)) => tuple + .to_class_type(db) + .is_some_and(|tuple_class| !known_instance.is_instance_of(db, tuple_class)), + + (Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => { + // A `Type::BooleanLiteral()` must be an instance of exactly `bool` + // (it cannot be an instance of a `bool` subclass) + !KnownClass::Bool.is_subclass_of(db, instance.class) + } + + (Type::BooleanLiteral(..) | Type::TypeIs(_), _) + | (_, Type::BooleanLiteral(..) | Type::TypeIs(_)) => true, + + (Type::IntLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::IntLiteral(..)) => { + // A `Type::IntLiteral()` must be an instance of exactly `int` + // (it cannot be an instance of an `int` subclass) + !KnownClass::Int.is_subclass_of(db, instance.class) + } + + (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, + + (Type::StringLiteral(..), Type::LiteralString) + | (Type::LiteralString, Type::StringLiteral(..)) => false, + + (Type::StringLiteral(..) | Type::LiteralString, Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => { + // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` + // (it cannot be an instance of a `str` subclass) + !KnownClass::Str.is_subclass_of(db, instance.class) + } + + (Type::LiteralString, Type::LiteralString) => false, + (Type::LiteralString, _) | (_, Type::LiteralString) => true, + + (Type::BytesLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::BytesLiteral(..)) => { + // A `Type::BytesLiteral()` must be an instance of exactly `bytes` + // (it cannot be an instance of a `bytes` subclass) + !KnownClass::Bytes.is_subclass_of(db, instance.class) + } + + // A class-literal type `X` is always disjoint from an instance type `Y`, + // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, + // where `Z` is `X`'s metaclass. + (Type::ClassLiteral(class), instance @ Type::NominalInstance(_)) + | (instance @ Type::NominalInstance(_), Type::ClassLiteral(class)) => !class + .metaclass_instance_type(db) + .is_subtype_of(db, instance), + (Type::GenericAlias(alias), instance @ Type::NominalInstance(_)) + | (instance @ Type::NominalInstance(_), Type::GenericAlias(alias)) => { + !ClassType::from(alias) + .metaclass_instance_type(db) + .is_subtype_of(db, instance) + } + + (Type::FunctionLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => { + // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` + // (it cannot be an instance of a `types.FunctionType` subclass) + !KnownClass::FunctionType.is_subclass_of(db, instance.class) + } + + (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType + .to_instance(db) + .is_disjoint_from_impl(db, other, visitor), + + (Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => { + KnownClass::MethodWrapperType + .to_instance(db) + .is_disjoint_from_impl(db, other, visitor) + } + + (Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .is_disjoint_from_impl(db, other, visitor) + } + + (Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_)) + | (Type::Callable(_), Type::FunctionLiteral(_)) => { + // No two callable types are ever disjoint because + // `(*args: object, **kwargs: object) -> Never` is a subtype of all fully static + // callable types. + false + } + + (Type::Callable(_), Type::StringLiteral(_) | Type::BytesLiteral(_)) + | (Type::StringLiteral(_) | Type::BytesLiteral(_), Type::Callable(_)) => { + // A callable type is disjoint from other literal types. For example, + // `Type::StringLiteral` must be an instance of exactly `str`, not a subclass + // of `str`, and `str` is not callable. The same applies to other literal types. + true + } + + (Type::Callable(_), Type::SpecialForm(special_form)) + | (Type::SpecialForm(special_form), Type::Callable(_)) => { + // A callable type is disjoint from special form types, except for special forms + // that are callable (like TypedDict and collection constructors). + // Most special forms are type constructors/annotations (like `typing.Literal`, + // `typing.Union`, etc.) that are subscripted, not called. + !special_form.is_callable() + } + + ( + Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), + instance @ Type::NominalInstance(NominalInstanceType { class, .. }), + ) + | ( + instance @ Type::NominalInstance(NominalInstanceType { class, .. }), + Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), + ) if class.is_final(db) => instance + .member_lookup_with_policy( + db, + Name::new_static("__call__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + .ignore_possibly_unbound() + .is_none_or(|dunder_call| { + !dunder_call.is_assignable_to(db, CallableType::unknown(db)) + }), + + ( + Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), + _, + ) + | ( + _, + Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), + ) => { + // TODO: Implement disjointness for general callable type with other types + false + } + + (Type::ModuleLiteral(..), other @ Type::NominalInstance(..)) + | (other @ Type::NominalInstance(..), Type::ModuleLiteral(..)) => { + // Modules *can* actually be instances of `ModuleType` subclasses + other.is_disjoint_from_impl(db, KnownClass::ModuleType.to_instance(db), visitor) + } + + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.is_disjoint_from_impl(db, right) + } + + (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { + tuple.is_disjoint_from_impl(db, other_tuple, visitor) + } + + (Type::Tuple(tuple), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::Tuple(tuple)) => { + tuple.to_class_type(db).is_some_and(|tuple_class| { + !instance.class.could_coexist_in_mro_with(db, tuple_class) + }) + } + + (Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => { + KnownClass::Property + .to_instance(db) + .is_disjoint_from_impl(db, other, visitor) + } + + (Type::BoundSuper(_), Type::BoundSuper(_)) => !self.is_equivalent_to(db, other), + (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => KnownClass::Super + .to_instance(db) + .is_disjoint_from_impl(db, other, visitor), + } + } + + /// Return true if there is just a single inhabitant for this type. + /// + /// Note: This function aims to have no false positives, but might return `false` + /// for more complicated types that are actually singletons. + pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool { + match self { + Type::Dynamic(_) + | Type::Never + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::LiteralString => { + // Note: The literal types included in this pattern are not true singletons. + // There can be multiple Python objects (at different memory locations) that + // are both of type Literal[345], for example. + false + } + + Type::ProtocolInstance(..) => { + // It *might* be possible to have a singleton protocol-instance type...? + // + // E.g.: + // + // ```py + // from typing import Protocol, Callable + // + // class WeirdAndWacky(Protocol): + // @property + // def __class__(self) -> Callable[[], None]: ... + // ``` + // + // `WeirdAndWacky` only has a single possible inhabitant: `None`! + // It is thus a singleton type. + // However, going out of our way to recognise it as such is probably not worth it. + // Such cases should anyway be exceedingly rare and/or contrived. + false + } + + // An unbounded, unconstrained typevar is not a singleton, because it can be + // specialized to a non-singleton type. A bounded typevar is not a singleton, even if + // the bound is a final singleton class, since it can still be specialized to `Never`. + // A constrained typevar is a singleton if all of its constraints are singletons. (Note + // that you cannot specialize a constrained typevar to a subtype of a constraint.) + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_singleton(db)), + }, + + // We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton. + Type::SubclassOf(..) => false, + Type::BoundSuper(..) => false, + Type::BooleanLiteral(_) + | Type::FunctionLiteral(..) + | Type::WrapperDescriptor(..) + | Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::ModuleLiteral(..) => true, + Type::SpecialForm(special_form) => { + // Nearly all `SpecialForm` types are singletons, but if a symbol could validly + // originate from either `typing` or `typing_extensions` then this is not guaranteed. + // E.g. `typing.TypeGuard` is equivalent to `typing_extensions.TypeGuard`, so both are treated + // as inhabiting the type `SpecialFormType::TypeGuard` in our model, but they are actually + // distinct symbols at different memory addresses at runtime. + !(special_form.check_module(KnownModule::Typing) + && special_form.check_module(KnownModule::TypingExtensions)) + } + Type::KnownInstance(_) => false, + Type::Callable(_) => { + // A callable type is never a singleton because for any given signature, + // there could be any number of distinct objects that are all callable with that + // signature. + false + } + Type::BoundMethod(..) => { + // `BoundMethod` types are single-valued types, but not singleton types: + // ```pycon + // >>> class Foo: + // ... def bar(self): pass + // >>> f = Foo() + // >>> f.bar is f.bar + // False + // ``` + false + } + Type::MethodWrapper(_) => { + // Just a special case of `BoundMethod` really + // (this variant represents `f.__get__`, where `f` is any function) + false + } + Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false, + Type::NominalInstance(instance) => instance.is_singleton(db), + Type::PropertyInstance(_) => false, + Type::Tuple(..) => { + // The empty tuple is a singleton on CPython and PyPy, but not on other Python + // implementations such as GraalPy. Its *use* as a singleton is discouraged and + // should not be relied on for type narrowing, so we do not treat it as one. + // See: + // https://docs.python.org/3/reference/expressions.html#parenthesized-forms + false + } + Type::Union(..) => { + // A single-element union, where the sole element was a singleton, would itself + // be a singleton type. However, unions with length < 2 should never appear in + // our model due to [`UnionBuilder::build`]. + false + } + Type::Intersection(..) => { + // Here, we assume that all intersection types that are singletons would have + // been reduced to a different form via [`IntersectionBuilder::build`] by now. + // For example: + // + // bool & ~Literal[False] = Literal[True] + // None & (None | int) = None | None & int = None + // + false + } + Type::AlwaysTruthy | Type::AlwaysFalsy => false, + Type::TypeIs(type_is) => type_is.is_bound(db), + } + } + + /// Return true if this type is non-empty and all inhabitants of this type compare equal. + pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { + match self { + Type::FunctionLiteral(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::ModuleLiteral(..) + | Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..) + | Type::BooleanLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..) => true, + + Type::ProtocolInstance(..) => { + // See comment in the `Type::ProtocolInstance` branch for `Type::is_singleton`. + false + } + + // An unbounded, unconstrained typevar is not single-valued, because it can be + // specialized to a multiple-valued type. A bounded typevar is not single-valued, even + // if the bound is a final single-valued class, since it can still be specialized to + // `Never`. A constrained typevar is single-valued if all of its constraints are + // single-valued. (Note that you cannot specialize a constrained typevar to a subtype + // of a constraint.) + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => false, + Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .elements(db) + .iter() + .all(|constraint| constraint.is_single_valued(db)), + }, + + Type::SubclassOf(..) => { + // TODO: Same comment as above for `is_singleton` + false + } + + Type::Tuple(tuple) => tuple.is_single_valued(db), + Type::NominalInstance(instance) => instance.is_single_valued(db), + + Type::BoundSuper(_) => { + // At runtime two super instances never compare equal, even if their arguments are identical. + false + } + + Type::TypeIs(type_is) => type_is.is_bound(db), + + Type::Dynamic(_) + | Type::Never + | Type::Union(..) + | Type::Intersection(..) + | Type::LiteralString + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::Callable(_) + | Type::PropertyInstance(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) => false, + } + } + + /// This function is roughly equivalent to `find_name_in_mro` as defined in the [descriptor guide] or + /// [`_PyType_Lookup`] in CPython's `Objects/typeobject.c`. It should typically be called through + /// [Type::class_member], unless it is known that `self` is a class-like type. This function returns + /// `None` if called on an instance-like type. + /// + /// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance + /// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223 + fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option> { + self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default()) + } + + fn find_name_in_mro_with_policy( + &self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> Option> { + match self { + Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| { + elem.find_name_in_mro_with_policy(db, name, policy) + // If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class + // elements instead of short-circuiting the whole result to `None`. We would need a more detailed + // return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is + // not a problem. + .unwrap_or_default() + })), + Type::Intersection(inter) => { + Some(inter.map_with_boundness_and_qualifiers(db, |elem| { + elem.find_name_in_mro_with_policy(db, name, policy) + // Fall back to Unbound, similar to the union case (see above). + .unwrap_or_default() + })) + } + + Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()), + + Type::ClassLiteral(class) => { + match (class.known(db), name) { + (Some(KnownClass::FunctionType), "__get__") => Some( + Place::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::FunctionTypeDunderGet, + )) + .into(), + ), + (Some(KnownClass::FunctionType), "__set__" | "__delete__") => { + // Hard code this knowledge, as we look up `__set__` and `__delete__` on `FunctionType` often. + Some(Place::Unbound.into()) + } + (Some(KnownClass::Property), "__get__") => Some( + Place::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderGet, + )) + .into(), + ), + (Some(KnownClass::Property), "__set__") => Some( + Place::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderSet, + )) + .into(), + ), + + _ => Some(class.class_member(db, name, policy)), + } + } + + Type::GenericAlias(alias) => { + Some(ClassType::from(*alias).class_member(db, name, policy)) + } + + Type::SubclassOf(subclass_of_ty) => { + subclass_of_ty.find_name_in_mro_with_policy(db, name, policy) + } + + // Note: `super(pivot, owner).__class__` is `builtins.super`, not the owner's class. + // `BoundSuper` should look up the name in the MRO of `builtins.super`. + Type::BoundSuper(_) => KnownClass::Super + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy), + + // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, + // i.e. Type::NominalInstance(type). So looking up a name in the MRO of + // `Type::NominalInstance(type)` is equivalent to looking up the name in the + // MRO of the class `object`. + Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Type) => { + if policy.mro_no_object_fallback() { + Some(Place::Unbound.into()) + } else { + KnownClass::Object + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + } + } + + Type::FunctionLiteral(_) + | Type::Callable(_) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::Tuple(_) + | Type::TypeVar(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::PropertyInstance(_) + | Type::TypeIs(_) => None, + } + } + + #[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] + #[allow(unused_variables)] + // If we choose name `_unit`, the macro will generate code that uses `_unit`, causing clippy to fail. + fn lookup_dunder_new(self, db: &'db dyn Db, unit: ()) -> Option> { + self.find_name_in_mro_with_policy( + db, + "__new__", + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + } + + /// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes + /// when called on an instance-like type, and metaclass attributes when called on a class-like type. + /// + /// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling + /// of union and intersection types. + fn class_member(self, db: &'db dyn Db, name: Name) -> PlaceAndQualifiers<'db> { + self.class_member_with_policy(db, name, MemberLookupPolicy::default()) + } + + #[salsa::tracked(cycle_fn=class_lookup_cycle_recover, cycle_initial=class_lookup_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + fn class_member_with_policy( + self, + db: &'db dyn Db, + name: Name, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + tracing::trace!("class_member: {}.{}", self.display(db), name); + match self { + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { + elem.class_member_with_policy(db, name.clone(), policy) + }), + Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| { + elem.class_member_with_policy(db, name.clone(), policy) + }), + // TODO: Once `to_meta_type` for the synthesized protocol is fully implemented, this handling should be removed. + Type::ProtocolInstance(ProtocolInstanceType { + inner: Protocol::Synthesized(_), + .. + }) => self.instance_member(db, &name), + _ => self + .to_meta_type(db) + .find_name_in_mro_with_policy(db, name.as_str(), policy) + .expect( + "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", + ), + } + } + + /// This function roughly corresponds to looking up an attribute in the `__dict__` of an object. + /// For instance-like types, this goes through the classes MRO and discovers attribute assignments + /// in methods, as well as class-body declarations that we consider to be evidence for the presence + /// of an instance attribute. + /// + /// For example, an instance of the following class has instance members `a` and `b`, but `c` is + /// just a class attribute that would not be discovered by this method: + /// ```py + /// class C: + /// a: int + /// + /// c = 1 + /// + /// def __init__(self): + /// self.b: str = "a" + /// ``` + fn instance_member(&self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + match self { + Type::Union(union) => { + union.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)) + } + + Type::Intersection(intersection) => intersection + .map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)), + + Type::Dynamic(_) | Type::Never => Place::bound(self).into(), + + Type::NominalInstance(instance) => instance.class.instance_member(db, name), + + Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), + + Type::FunctionLiteral(_) => KnownClass::FunctionType + .to_instance(db) + .instance_member(db, name), + + Type::BoundMethod(_) => KnownClass::MethodType + .to_instance(db) + .instance_member(db, name), + Type::MethodWrapper(_) => KnownClass::MethodWrapperType + .to_instance(db) + .instance_member(db, name), + Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType + .to_instance(db) + .instance_member(db, name), + Type::DataclassDecorator(_) => KnownClass::FunctionType + .to_instance(db) + .instance_member(db, name), + Type::Callable(_) | Type::DataclassTransformer(_) => { + KnownClass::Object.to_instance(db).instance_member(db, name) + } + + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => KnownClass::Object.to_instance(db).instance_member(db, name), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.instance_member(db, name) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints + .map_with_boundness_and_qualifiers(db, |constraint| { + constraint.instance_member(db, name) + }), + }, + + Type::IntLiteral(_) => KnownClass::Int.to_instance(db).instance_member(db, name), + Type::BooleanLiteral(_) | Type::TypeIs(_) => { + KnownClass::Bool.to_instance(db).instance_member(db, name) + } + Type::StringLiteral(_) | Type::LiteralString => { + KnownClass::Str.to_instance(db).instance_member(db, name) + } + Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name), + Type::Tuple(tuple) => tuple + .to_class_type(db) + .map(|class| class.instance_member(db, name)) + .unwrap_or(Place::Unbound.into()), + + Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name), + Type::ModuleLiteral(_) => KnownClass::ModuleType + .to_instance(db) + .instance_member(db, name), + + Type::SpecialForm(_) | Type::KnownInstance(_) => Place::Unbound.into(), + + Type::PropertyInstance(_) => KnownClass::Property + .to_instance(db) + .instance_member(db, name), + + // Note: `super(pivot, owner).__dict__` refers to the `__dict__` of the `builtins.super` instance, + // not that of the owner. + // This means we should only look up instance members defined on the `builtins.super()` instance itself. + // If you want to look up a member in the MRO of the `super`'s owner, + // refer to [`Type::member`] instead. + Type::BoundSuper(_) => KnownClass::Super.to_instance(db).instance_member(db, name), + + // TODO: we currently don't model the fact that class literals and subclass-of types have + // a `__dict__` that is filled with class level attributes. Modeling this is currently not + // required, as `instance_member` is only called for instance-like types through `member`, + // but we might want to add this in the future. + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { + Place::Unbound.into() + } + } + } + + /// Access an attribute of this type without invoking the descriptor protocol. This + /// method corresponds to `inspect.getattr_static(, name)`. + /// + /// See also: [`Type::member`] + fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> { + if let Type::ModuleLiteral(module) = self { + module.static_member(db, name).place + } else if let place @ Place::Type(_, _) = self.class_member(db, name.into()).place { + place + } else if let Some(place @ Place::Type(_, _)) = + self.find_name_in_mro(db, name).map(|inner| inner.place) + { + place + } else { + self.instance_member(db, name).place + } + } + + /// Look up `__get__` on the meta-type of self, and call it with the arguments `self`, `instance`, + /// and `owner`. `__get__` is different than other dunder methods in that it is not looked up using + /// the descriptor protocol itself. + /// + /// In addition to the return type of `__get__`, this method also returns the *kind* of attribute + /// that `self` represents: (1) a data descriptor or (2) a non-data descriptor / normal attribute. + /// + /// If `__get__` is not defined on the meta-type, this method returns `None`. + #[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] + pub(crate) fn try_call_dunder_get( + self, + db: &'db dyn Db, + instance: Type<'db>, + owner: Type<'db>, + ) -> Option<(Type<'db>, AttributeKind)> { + tracing::trace!( + "try_call_dunder_get: {}, {}, {}", + self.display(db), + instance.display(db), + owner.display(db) + ); + match self { + Type::Callable(callable) if callable.is_function_like(db) => { + // For "function-like" callables, model the the behavior of `FunctionType.__get__`. + // + // It is a shortcut to model this in `try_call_dunder_get`. If we want to be really precise, + // we should instead return a new method-wrapper type variant for the synthesized `__get__` + // method of these synthesized functions. The method-wrapper would then be returned from + // `find_name_in_mro` when called on function-like `Callable`s. This would allow us to + // correctly model the behavior of *explicit* `SomeDataclass.__init__.__get__` calls. + return if instance.is_none(db) { + Some((self, AttributeKind::NormalOrNonDataDescriptor)) + } else { + Some(( + callable.bind_self(db), + AttributeKind::NormalOrNonDataDescriptor, + )) + }; + } + _ => {} + } + + let descr_get = self.class_member(db, "__get__".into()).place; + + if let Place::Type(descr_get, descr_get_boundness) = descr_get { + let return_ty = descr_get + .try_call(db, &CallArgumentTypes::positional([self, instance, owner])) + .map(|bindings| { + if descr_get_boundness == Boundness::Bound { + bindings.return_type(db) + } else { + UnionType::from_elements(db, [bindings.return_type(db), self]) + } + }) + .ok()?; + + let descriptor_kind = if self.is_data_descriptor(db) { + AttributeKind::DataDescriptor + } else { + AttributeKind::NormalOrNonDataDescriptor + }; + + Some((return_ty, descriptor_kind)) + } else { + None + } + } + + /// Look up `__get__` on the meta-type of `attribute`, and call it with `attribute`, `instance`, + /// and `owner` as arguments. This method exists as a separate step as we need to handle unions + /// and intersections explicitly. + fn try_call_dunder_get_on_attribute( + db: &'db dyn Db, + attribute: PlaceAndQualifiers<'db>, + instance: Type<'db>, + owner: Type<'db>, + ) -> (PlaceAndQualifiers<'db>, AttributeKind) { + match attribute { + // This branch is not strictly needed, but it short-circuits the lookup of various dunder + // methods and calls that would otherwise be made. + // + // Note that attribute accesses on dynamic types always succeed. For this reason, they also + // have `__get__`, `__set__`, and `__delete__` methods and are therefore considered to be + // data descriptors. + // + // The same is true for `Never`. + PlaceAndQualifiers { + place: Place::Type(Type::Dynamic(_) | Type::Never, _), + qualifiers: _, + } => (attribute, AttributeKind::DataDescriptor), + + PlaceAndQualifiers { + place: Place::Type(Type::Union(union), boundness), + qualifiers, + } => ( + union + .map_with_boundness(db, |elem| { + Place::Type( + elem.try_call_dunder_get(db, instance, owner) + .map_or(*elem, |(ty, _)| ty), + boundness, + ) + }) + .with_qualifiers(qualifiers), + // TODO: avoid the duplication here: + if union.elements(db).iter().all(|elem| { + elem.try_call_dunder_get(db, instance, owner) + .is_some_and(|(_, kind)| kind.is_data()) + }) { + AttributeKind::DataDescriptor + } else { + AttributeKind::NormalOrNonDataDescriptor + }, + ), + + PlaceAndQualifiers { + place: Place::Type(Type::Intersection(intersection), boundness), + qualifiers, + } => ( + intersection + .map_with_boundness(db, |elem| { + Place::Type( + elem.try_call_dunder_get(db, instance, owner) + .map_or(*elem, |(ty, _)| ty), + boundness, + ) + }) + .with_qualifiers(qualifiers), + // TODO: Discover data descriptors in intersections. + AttributeKind::NormalOrNonDataDescriptor, + ), + + PlaceAndQualifiers { + place: Place::Type(attribute_ty, boundness), + qualifiers: _, + } => { + if let Some((return_ty, attribute_kind)) = + attribute_ty.try_call_dunder_get(db, instance, owner) + { + (Place::Type(return_ty, boundness).into(), attribute_kind) + } else { + (attribute, AttributeKind::NormalOrNonDataDescriptor) + } + } + + _ => (attribute, AttributeKind::NormalOrNonDataDescriptor), + } + } + + /// Returns whether this type is a data descriptor, i.e. defines `__set__` or `__delete__`. + /// If this type is a union, requires all elements of union to be data descriptors. + pub(crate) fn is_data_descriptor(self, d: &'db dyn Db) -> bool { + self.is_data_descriptor_impl(d, false) + } + + /// Returns whether this type may be a data descriptor. + /// If this type is a union, returns true if _any_ element is a data descriptor. + pub(crate) fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { + self.is_data_descriptor_impl(d, true) + } + + fn is_data_descriptor_impl(self, db: &'db dyn Db, any_of_union: bool) -> bool { + match self { + Type::Dynamic(_) | Type::Never | Type::PropertyInstance(_) => true, + Type::Union(union) if any_of_union => union + .elements(db) + .iter() + // Types of instance attributes that are not explicitly typed are unioned with `Unknown`, it should be excluded when checking. + .filter(|ty| !ty.is_unknown()) + .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + Type::Union(union) => union + .elements(db) + .iter() + .all(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + Type::Intersection(intersection) => intersection + .iter_positive(db) + .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + _ => { + !self.class_member(db, "__set__".into()).place.is_unbound() + || !self + .class_member(db, "__delete__".into()) + .place + .is_unbound() + } + } + } + + /// Implementation of the descriptor protocol. + /// + /// This method roughly performs the following steps: + /// + /// - Look up the attribute `name` on the meta-type of `self`. Call the result `meta_attr`. + /// - Call `__get__` on the meta-type of `meta_attr`, if it exists. If the call succeeds, + /// replace `meta_attr` with the result of the call. Also check if `meta_attr` is a *data* + /// descriptor by testing if `__set__` or `__delete__` exist. + /// - If `meta_attr` is a data descriptor, return it. + /// - Otherwise, if `fallback` is bound, return `fallback`. + /// - Otherwise, return `meta_attr`. + /// + /// In addition to that, we also handle various cases of possibly-unbound symbols and fall + /// back to lower-precedence stages of the descriptor protocol by building union types. + fn invoke_descriptor_protocol( + self, + db: &'db dyn Db, + name: &str, + fallback: PlaceAndQualifiers<'db>, + policy: InstanceFallbackShadowsNonDataDescriptor, + member_policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + let ( + PlaceAndQualifiers { + place: meta_attr, + qualifiers: meta_attr_qualifiers, + }, + meta_attr_kind, + ) = Self::try_call_dunder_get_on_attribute( + db, + self.class_member_with_policy(db, name.into(), member_policy), + self, + self.to_meta_type(db), + ); + + let PlaceAndQualifiers { + place: fallback, + qualifiers: fallback_qualifiers, + } = fallback; + + match (meta_attr, meta_attr_kind, fallback) { + // The fallback type is unbound, so we can just return `meta_attr` unconditionally, + // no matter if it's data descriptor, a non-data descriptor, or a normal attribute. + (meta_attr @ Place::Type(_, _), _, Place::Unbound) => { + meta_attr.with_qualifiers(meta_attr_qualifiers) + } + + // `meta_attr` is the return type of a data descriptor and definitely bound, so we + // return it. + (meta_attr @ Place::Type(_, Boundness::Bound), AttributeKind::DataDescriptor, _) => { + meta_attr.with_qualifiers(meta_attr_qualifiers) + } + + // `meta_attr` is the return type of a data descriptor, but the attribute on the + // meta-type is possibly-unbound. This means that we "fall through" to the next + // stage of the descriptor protocol and union with the fallback type. + ( + Place::Type(meta_attr_ty, Boundness::PossiblyUnbound), + AttributeKind::DataDescriptor, + Place::Type(fallback_ty, fallback_boundness), + ) => Place::Type( + UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), + fallback_boundness, + ) + .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + + // `meta_attr` is *not* a data descriptor. This means that the `fallback` type has + // now the highest priority. However, we only return the pure `fallback` type if the + // policy allows it. When invoked on class objects, the policy is set to `Yes`, which + // means that class-level attributes (the fallback) can shadow non-data descriptors + // on metaclasses. However, for instances, the policy is set to `No`, because we do + // allow instance-level attributes to shadow class-level non-data descriptors. This + // would require us to statically infer if an instance attribute is always set, which + // is something we currently don't attempt to do. + ( + Place::Type(_, _), + AttributeKind::NormalOrNonDataDescriptor, + fallback @ Place::Type(_, Boundness::Bound), + ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => { + fallback.with_qualifiers(fallback_qualifiers) + } + + // `meta_attr` is *not* a data descriptor. The `fallback` symbol is either possibly + // unbound or the policy argument is `No`. In both cases, the `fallback` type does + // not completely shadow the non-data descriptor, so we build a union of the two. + ( + Place::Type(meta_attr_ty, meta_attr_boundness), + AttributeKind::NormalOrNonDataDescriptor, + Place::Type(fallback_ty, fallback_boundness), + ) => Place::Type( + UnionType::from_elements(db, [meta_attr_ty, fallback_ty]), + meta_attr_boundness.max(fallback_boundness), + ) + .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + + // If the attribute is not found on the meta-type, we simply return the fallback. + (Place::Unbound, _, fallback) => fallback.with_qualifiers(fallback_qualifiers), + } + } + + /// Access an attribute of this type, potentially invoking the descriptor protocol. + /// Corresponds to `getattr(, name)`. + /// + /// See also: [`Type::static_member`] + /// + /// TODO: We should return a `Result` here to handle errors that can appear during attribute + /// lookup, like a failed `__get__` call on a descriptor. + #[must_use] + pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default()) + } + + /// Similar to [`Type::member`], but allows the caller to specify what policy should be used + /// when looking up attributes. See [`MemberLookupPolicy`] for more information. + #[salsa::tracked(cycle_fn=member_lookup_cycle_recover, cycle_initial=member_lookup_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + fn member_lookup_with_policy( + self, + db: &'db dyn Db, + name: Name, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name); + if name == "__class__" { + return Place::bound(self.to_meta_type(db)).into(); + } + + let name_str = name.as_str(); + + match self { + Type::Union(union) => union + .map_with_boundness(db, |elem| { + elem.member_lookup_with_policy(db, name_str.into(), policy) + .place + }) + .into(), + + Type::Intersection(intersection) => intersection + .map_with_boundness(db, |elem| { + elem.member_lookup_with_policy(db, name_str.into(), policy) + .place + }) + .into(), + + Type::Dynamic(..) | Type::Never => Place::bound(self).into(), + + Type::FunctionLiteral(function) if name == "__get__" => Place::bound( + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)), + ) + .into(), + Type::FunctionLiteral(function) if name == "__call__" => Place::bound( + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)), + ) + .into(), + Type::PropertyInstance(property) if name == "__get__" => Place::bound( + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)), + ) + .into(), + Type::PropertyInstance(property) if name == "__set__" => Place::bound( + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)), + ) + .into(), + Type::StringLiteral(literal) if name == "startswith" => Place::bound( + Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)), + ) + .into(), + + Type::ClassLiteral(class) + if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => + { + Place::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::FunctionTypeDunderGet, + )) + .into() + } + Type::ClassLiteral(class) + if name == "__get__" && class.is_known(db, KnownClass::Property) => + { + Place::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderGet, + )) + .into() + } + Type::ClassLiteral(class) + if name == "__set__" && class.is_known(db, KnownClass::Property) => + { + Place::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderSet, + )) + .into() + } + Type::BoundMethod(bound_method) => match name_str { + "__self__" => Place::bound(bound_method.self_instance(db)).into(), + "__func__" => Place::bound(Type::FunctionLiteral(bound_method.function(db))).into(), + _ => { + KnownClass::MethodType + .to_instance(db) + .member_lookup_with_policy(db, name.clone(), policy) + .or_fall_back_to(db, || { + // If an attribute is not available on the bound method object, + // it will be looked up on the underlying function object: + Type::FunctionLiteral(bound_method.function(db)) + .member_lookup_with_policy(db, name, policy) + }) + } + }, + Type::MethodWrapper(_) => KnownClass::MethodWrapperType + .to_instance(db) + .member_lookup_with_policy(db, name, policy), + Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType + .to_instance(db) + .member_lookup_with_policy(db, name, policy), + Type::DataclassDecorator(_) => KnownClass::FunctionType + .to_instance(db) + .member_lookup_with_policy(db, name, policy), + + Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => { + Place::bound(self).into() + } + + Type::Callable(callable) if callable.is_function_like(db) => KnownClass::FunctionType + .to_instance(db) + .member_lookup_with_policy(db, name, policy), + + Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object + .to_instance(db) + .member_lookup_with_policy(db, name, policy), + + Type::NominalInstance(instance) + if matches!(name.as_str(), "major" | "minor") + && instance.class.is_known(db, KnownClass::VersionInfo) => + { + let python_version = Program::get(db).python_version(db); + let segment = if name == "major" { + python_version.major + } else { + python_version.minor + }; + Place::bound(Type::IntLiteral(segment.into())).into() + } + + Type::PropertyInstance(property) if name == "fget" => { + Place::bound(property.getter(db).unwrap_or(Type::none(db))).into() + } + Type::PropertyInstance(property) if name == "fset" => { + Place::bound(property.setter(db).unwrap_or(Type::none(db))).into() + } + + Type::IntLiteral(_) if matches!(name_str, "real" | "numerator") => { + Place::bound(self).into() + } + + Type::BooleanLiteral(bool_value) if matches!(name_str, "real" | "numerator") => { + Place::bound(Type::IntLiteral(i64::from(bool_value))).into() + } + + Type::ModuleLiteral(module) => module.static_member(db, name_str), + + _ if policy.no_instance_fallback() => self.invoke_descriptor_protocol( + db, + name_str, + Place::Unbound.into(), + InstanceFallbackShadowsNonDataDescriptor::No, + policy, + ), + + Type::NominalInstance(..) + | Type::ProtocolInstance(..) + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::LiteralString + | Type::Tuple(..) + | Type::TypeVar(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..) + | Type::PropertyInstance(..) + | Type::FunctionLiteral(..) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::TypeIs(..) => { + let fallback = self.instance_member(db, name_str); + + let result = self.invoke_descriptor_protocol( + db, + name_str, + fallback, + InstanceFallbackShadowsNonDataDescriptor::No, + policy, + ); + + let custom_getattr_result = || { + // Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with + // dynamic imports. We explicitly hide it here to prevent arbitrary attributes + // from being available on modules. Same for `types.GenericAlias` - its + // `__getattr__` method will delegate to `__origin__` to allow looking up + // attributes on the original type. But in typeshed its return type is `Any`. + // It will need a special handling, so it remember the origin type to properly + // resolve the attribute. + if matches!( + self.into_nominal_instance() + .and_then(|instance| instance.class.known(db)), + Some(KnownClass::ModuleType | KnownClass::GenericAlias) + ) { + return Place::Unbound.into(); + } + + self.try_call_dunder( + db, + "__getattr__", + CallArgumentTypes::positional([Type::StringLiteral( + StringLiteralType::new(db, Box::from(name.as_str())), + )]), + ) + .map(|outcome| Place::bound(outcome.return_type(db))) + // TODO: Handle call errors here. + .unwrap_or(Place::Unbound) + .into() + }; + + let custom_getattribute_result = || { + // Avoid cycles when looking up `__getattribute__` + if "__getattribute__" == name.as_str() { + return Place::Unbound.into(); + } + + // Typeshed has a `__getattribute__` method defined on `builtins.object` so we + // explicitly hide it here using `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK`. + self.try_call_dunder_with_policy( + db, + "__getattribute__", + &mut CallArgumentTypes::positional([Type::StringLiteral( + StringLiteralType::new(db, Box::from(name.as_str())), + )]), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + .map(|outcome| Place::bound(outcome.return_type(db))) + // TODO: Handle call errors here. + .unwrap_or(Place::Unbound) + .into() + }; + + match result { + member @ PlaceAndQualifiers { + place: Place::Type(_, Boundness::Bound), + qualifiers: _, + } => member, + member @ PlaceAndQualifiers { + place: Place::Type(_, Boundness::PossiblyUnbound), + qualifiers: _, + } => member + .or_fall_back_to(db, custom_getattribute_result) + .or_fall_back_to(db, custom_getattr_result), + PlaceAndQualifiers { + place: Place::Unbound, + qualifiers: _, + } => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result), + } + } + + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { + let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect( + "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", + ); + + if name == "__mro__" { + return class_attr_plain; + } + + if self.is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) { + return PlaceAndQualifiers::todo("Attribute access on enum classes"); + } + + let class_attr_fallback = Self::try_call_dunder_get_on_attribute( + db, + class_attr_plain, + Type::none(db), + self, + ) + .0; + + self.invoke_descriptor_protocol( + db, + name_str, + class_attr_fallback, + InstanceFallbackShadowsNonDataDescriptor::Yes, + policy, + ) + } + + // Unlike other objects, `super` has a unique member lookup behavior. + // It's simpler than other objects: + // + // 1. Search for the attribute in the MRO, starting just after the pivot class. + // 2. If the attribute is a descriptor, invoke its `__get__` method. + Type::BoundSuper(bound_super) => { + let owner_attr = bound_super.find_name_in_mro_after_pivot(db, name_str, policy); + + bound_super + .try_call_dunder_get_on_attribute(db, owner_attr.clone()) + .unwrap_or(owner_attr) + } + } + } + + /// Resolves the boolean value of the type and falls back to [`Truthiness::Ambiguous`] if the type doesn't implement `__bool__` correctly. + /// + /// This method should only be used outside type checking or when evaluating if a type + /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. + /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. + pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { + self.try_bool_impl(db, true) + .unwrap_or_else(|err| err.fallback_truthiness()) + } + + /// Resolves the boolean value of a type. + /// + /// This is used to determine the value that would be returned + /// when `bool(x)` is called on an object `x`. + /// + /// Returns an error if the type doesn't implement `__bool__` correctly. + pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { + self.try_bool_impl(db, false) + } + + /// Resolves the boolean value of a type. + /// + /// Setting `allow_short_circuit` to `true` allows the implementation to + /// early return if the bool value of any union variant is `Truthiness::Ambiguous`. + /// Early returning shows a 1-2% perf improvement on our benchmarks because + /// `bool` (which doesn't care about errors) is used heavily when evaluating statically known branches. + /// + /// An alternative to this flag is to implement a trait similar to Rust's `Try` trait. + /// The advantage of that is that it would allow collecting the errors as well. However, + /// it is significantly more complex and duplicating the logic into `bool` without the error + /// handling didn't show any significant performance difference to when using the `allow_short_circuit` flag. + #[inline] + fn try_bool_impl( + &self, + db: &'db dyn Db, + allow_short_circuit: bool, + ) -> Result> { + let type_to_truthiness = |ty| { + if let Type::BooleanLiteral(bool_val) = ty { + Truthiness::from(bool_val) + } else { + Truthiness::Ambiguous + } + }; + + let try_dunder_bool = || { + // We only check the `__bool__` method for truth testing, even though at + // runtime there is a fallback to `__len__`, since `__bool__` takes precedence + // and a subclass could add a `__bool__` method. + + match self.try_call_dunder(db, "__bool__", CallArgumentTypes::none()) { + Ok(outcome) => { + let return_type = outcome.return_type(db); + if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + // The type has a `__bool__` method, but it doesn't return a + // boolean. + return Err(BoolError::IncorrectReturnType { + return_type, + not_boolable_type: *self, + }); + } + Ok(type_to_truthiness(return_type)) + } + + Err(CallDunderError::PossiblyUnbound(outcome)) => { + let return_type = outcome.return_type(db); + if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + // The type has a `__bool__` method, but it doesn't return a + // boolean. + return Err(BoolError::IncorrectReturnType { + return_type: outcome.return_type(db), + not_boolable_type: *self, + }); + } + + // Don't trust possibly unbound `__bool__` method. + Ok(Truthiness::Ambiguous) + } + + Err(CallDunderError::MethodNotAvailable) => Ok(Truthiness::Ambiguous), + Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => { + Err(BoolError::IncorrectArguments { + truthiness: type_to_truthiness(bindings.return_type(db)), + not_boolable_type: *self, + }) + } + Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => { + Err(BoolError::NotCallable { + not_boolable_type: *self, + }) + } + Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => { + Err(BoolError::Other { + not_boolable_type: *self, + }) + } + } + }; + + let try_union = |union: UnionType<'db>| { + let mut truthiness = None; + let mut all_not_callable = true; + let mut has_errors = false; + + for element in union.elements(db) { + let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) { + Ok(truthiness) => truthiness, + Err(err) => { + has_errors = true; + all_not_callable &= matches!(err, BoolError::NotCallable { .. }); + err.fallback_truthiness() + } + }; + + truthiness.get_or_insert(element_truthiness); + + if Some(element_truthiness) != truthiness { + truthiness = Some(Truthiness::Ambiguous); + + if allow_short_circuit { + return Ok(Truthiness::Ambiguous); + } + } + } + + if has_errors { + if all_not_callable { + return Err(BoolError::NotCallable { + not_boolable_type: *self, + }); + } + return Err(BoolError::Union { + union, + truthiness: truthiness.unwrap_or(Truthiness::Ambiguous), + }); + } + Ok(truthiness.unwrap_or(Truthiness::Ambiguous)) + }; + + let truthiness = match self { + Type::Dynamic(_) + | Type::Never + | Type::Callable(_) + | Type::LiteralString + | Type::TypeIs(_) => Truthiness::Ambiguous, + + Type::FunctionLiteral(_) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::PropertyInstance(_) + | Type::BoundSuper(_) + | Type::KnownInstance(_) + | Type::SpecialForm(_) + | Type::AlwaysTruthy => Truthiness::AlwaysTrue, + + Type::AlwaysFalsy => Truthiness::AlwaysFalse, + + Type::ClassLiteral(class) => class + .metaclass_instance_type(db) + .try_bool_impl(db, allow_short_circuit)?, + Type::GenericAlias(alias) => ClassType::from(*alias) + .metaclass_instance_type(db) + .try_bool_impl(db, allow_short_circuit)?, + + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, + SubclassOfInner::Class(class) => { + Type::from(class).try_bool_impl(db, allow_short_circuit)? + } + }, + + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => Truthiness::Ambiguous, + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.try_bool_impl(db, allow_short_circuit)? + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + try_union(constraints)? + } + }, + + Type::NominalInstance(instance) => match instance.class.known(db) { + Some(known_class) => known_class.bool(), + None => try_dunder_bool()?, + }, + + Type::ProtocolInstance(_) => try_dunder_bool()?, + + Type::Union(union) => try_union(*union)?, + + Type::Intersection(_) => { + // TODO + Truthiness::Ambiguous + } + + Type::IntLiteral(num) => Truthiness::from(*num != 0), + Type::BooleanLiteral(bool) => Truthiness::from(*bool), + Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), + Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), + Type::Tuple(tuple) => match tuple.tuple(db).len().size_hint() { + // The tuple type is AlwaysFalse if it contains only the empty tuple + (_, Some(0)) => Truthiness::AlwaysFalse, + // The tuple type is AlwaysTrue if its inhabitants must always have length >=1 + (minimum, _) if minimum > 0 => Truthiness::AlwaysTrue, + // The tuple type is Ambiguous if its inhabitants could be of any length + _ => Truthiness::Ambiguous, + }, + }; + + Ok(truthiness) + } + + /// Return the type of `len()` on a type if it is known more precisely than `int`, + /// or `None` otherwise. + /// + /// In the second case, the return type of `len()` in `typeshed` (`int`) + /// is used as a fallback. + fn len(&self, db: &'db dyn Db) -> Option> { + fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { + match ty { + // TODO: Emit diagnostic for non-integers and negative integers + Type::IntLiteral(value) => (value >= 0).then_some(ty), + Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())), + Type::Union(union) => { + union.try_map(db, |element| non_negative_int_literal(db, *element)) + } + _ => None, + } + } + + let usize_len = match self { + Type::BytesLiteral(bytes) => Some(bytes.python_len(db)), + Type::StringLiteral(string) => Some(string.python_len(db)), + Type::Tuple(tuple) => match tuple.tuple(db) { + TupleSpec::Fixed(tuple) => Some(tuple.len()), + TupleSpec::Variable(_) => None, + }, + + _ => None, + }; + + if let Some(usize_len) = usize_len { + return usize_len.try_into().ok().map(Type::IntLiteral); + } + + let return_ty = match self.try_call_dunder(db, "__len__", CallArgumentTypes::none()) { + Ok(bindings) => bindings.return_type(db), + Err(CallDunderError::PossiblyUnbound(bindings)) => bindings.return_type(db), + + // TODO: emit a diagnostic + Err(CallDunderError::MethodNotAvailable) => return None, + Err(CallDunderError::CallError(_, bindings)) => bindings.return_type(db), + }; + + non_negative_int_literal(db, return_ty) + } + + /// Returns a [`Bindings`] that can be used to analyze a call to this type. You must call + /// [`match_parameters`][Bindings::match_parameters] and [`check_types`][Bindings::check_types] + /// to fully analyze a particular call site. + /// + /// Note that we return a [`Bindings`] for all types, even if the type is not callable. + /// "Callable" can be subtle for a union type, since some union elements might be callable and + /// some not. A union is callable if every element type is callable — but even then, the + /// elements might be inconsistent, such that there's no argument list that's valid for all + /// elements. It's usually best to only worry about "callability" relative to a particular + /// argument list, via [`try_call`][Self::try_call] and [`CallErrorKind::NotCallable`]. + fn bindings(self, db: &'db dyn Db) -> Bindings<'db> { + match self { + Type::Callable(callable) => { + CallableBinding::from_overloads(self, callable.signatures(db).iter().cloned()) + .into() + } + + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => CallableBinding::not_callable(self).into(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.bindings(db), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => Bindings::from_union( + self, + constraints.elements(db).iter().map(|ty| ty.bindings(db)), + ), + }, + + Type::BoundMethod(bound_method) => { + let signature = bound_method.function(db).signature(db); + CallableBinding::from_overloads(self, signature.overloads.iter().cloned()) + .with_bound_type(bound_method.self_instance(db)) + .into() + } + + Type::MethodWrapper( + MethodWrapperKind::FunctionTypeDunderGet(_) + | MethodWrapperKind::PropertyDunderGet(_), + ) => { + // Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`. + // This is required because we need to return more precise types than what the signature in + // typeshed provides: + // + // ```py + // class FunctionType: + // # ... + // @overload + // def __get__(self, instance: None, owner: type, /) -> FunctionType: ... + // @overload + // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... + // ``` + // + // For `builtins.property.__get__`, we use the same signature. The return types are not + // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`. + + CallableBinding::from_overloads( + self, + [ + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(KnownClass::Type.to_instance(db)), + ]), + None, + ), + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object(db)), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::Type.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ]), + None, + ), + ], + ) + .into() + } + + Type::WrapperDescriptor( + kind @ (WrapperDescriptorKind::FunctionTypeDunderGet + | WrapperDescriptorKind::PropertyDunderGet), + ) => { + // Here, we also model `types.FunctionType.__get__` (or builtins.property.__get__), + // but now we consider a call to this as a function, i.e. we also expect the `self` + // argument to be passed in. + + // TODO: Consider merging this signature with the one in the previous match clause, + // since the previous one is just this signature with the `self` parameters + // removed. + let descriptor = match kind { + WrapperDescriptorKind::FunctionTypeDunderGet => { + KnownClass::FunctionType.to_instance(db) + } + WrapperDescriptorKind::PropertyDunderGet => { + KnownClass::Property.to_instance(db) + } + WrapperDescriptorKind::PropertyDunderSet => { + unreachable!("Not part of outer match pattern") + } + }; + CallableBinding::from_overloads( + self, + [ + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(descriptor), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(KnownClass::Type.to_instance(db)), + ]), + None, + ), + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(descriptor), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object(db)), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::Type.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ]), + None, + ), + ], + ) + .into() + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object(db)), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::object(db)), + ]), + None, + ), + ) + .into(), + + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(KnownClass::Property.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object(db)), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::object(db)), + ]), + None, + ), + ) + .into(), + + Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("prefix"))) + .with_annotated_type(UnionType::from_elements( + db, + [ + KnownClass::Str.to_instance(db), + TupleType::homogeneous(db, KnownClass::Str.to_instance(db)), + ], + )), + Parameter::positional_only(Some(Name::new_static("start"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("end"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ]), + Some(KnownClass::Bool.to_instance(db)), + ), + ) + .into(), + + // TODO: We should probably also check the original return type of the function + // that was decorated with `@dataclass_transform`, to see if it is consistent with + // with what we configure here. + Type::DataclassTransformer(_) => Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static("func"))) + .with_annotated_type(Type::object(db))]), + None, + ), + ) + .into(), + + Type::FunctionLiteral(function_type) => match function_type.known(db) { + Some( + KnownFunction::IsEquivalentTo + | KnownFunction::IsSubtypeOf + | KnownFunction::IsAssignableTo + | KnownFunction::IsDisjointFrom, + ) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("a"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("b"))) + .type_form() + .with_annotated_type(Type::any()), + ]), + Some(KnownClass::Bool.to_instance(db)), + ), + ) + .into(), + + Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => { + Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static( + "a", + ))) + .type_form() + .with_annotated_type(Type::any())]), + Some(KnownClass::Bool.to_instance(db)), + ), + ) + .into() + } + + Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => { + Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static( + "type", + ))) + .type_form() + .with_annotated_type(Type::any())]), + Some(Type::any()), + ), + ) + .into() + } + + Some(KnownFunction::AssertType) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("type"))) + .type_form() + .with_annotated_type(Type::any()), + ]), + Some(Type::none(db)), + ), + ) + .into(), + + Some(KnownFunction::AssertNever) => { + Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static( + "arg", + ))) + // We need to set the type to `Any` here (instead of `Never`), + // in order for every `assert_never` call to pass the argument + // check. If we set it to `Never`, we'll get invalid-argument-type + // errors instead of `type-assertion-failure` errors. + .with_annotated_type(Type::any())]), + Some(Type::none(db)), + ), + ) + .into() + } + + Some(KnownFunction::Cast) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("typ")) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_or_keyword(Name::new_static("val")) + .with_annotated_type(Type::any()), + ]), + Some(Type::any()), + ), + ) + .into(), + + Some(KnownFunction::Dataclass) => { + CallableBinding::from_overloads( + self, + [ + // def dataclass(cls: None, /) -> Callable[[type[_T]], type[_T]]: ... + Signature::new( + Parameters::new([Parameter::positional_only(Some( + Name::new_static("cls"), + )) + .with_annotated_type(Type::none(db))]), + None, + ), + // def dataclass(cls: type[_T], /) -> type[_T]: ... + Signature::new( + Parameters::new([Parameter::positional_only(Some( + Name::new_static("cls"), + )) + .with_annotated_type(KnownClass::Type.to_instance(db))]), + None, + ), + // TODO: make this overload Python-version-dependent + + // def dataclass( + // *, + // init: bool = True, + // repr: bool = True, + // eq: bool = True, + // order: bool = False, + // unsafe_hash: bool = False, + // frozen: bool = False, + // match_args: bool = True, + // kw_only: bool = False, + // slots: bool = False, + // weakref_slot: bool = False, + // ) -> Callable[[type[_T]], type[_T]]: ... + Signature::new( + Parameters::new([ + Parameter::keyword_only(Name::new_static("init")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("repr")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("eq")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("order")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("unsafe_hash")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("frozen")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("match_args")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("kw_only")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("slots")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("weakref_slot")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + ]), + None, + ), + ], + ) + .into() + } + + _ => CallableBinding::from_overloads( + self, + function_type.signature(db).overloads.iter().cloned(), + ) + .into(), + }, + + Type::ClassLiteral(class) => match class.known(db) { + // TODO: Ideally we'd use `try_call_constructor` for all constructor calls. + // Currently we don't for a few special known types, either because their + // constructors are defined with overloads, or because we want to special case + // their return type beyond what typeshed provides (though this support could + // likely be moved into the `try_call_constructor` path). Once we support + // overloads, re-evaluate the need for these arms. + Some(KnownClass::Bool) => { + // ```py + // class bool(int): + // def __new__(cls, o: object = ..., /) -> Self: ... + // ``` + Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static( + "o", + ))) + .with_annotated_type(Type::any()) + .with_default_type(Type::BooleanLiteral(false))]), + Some(KnownClass::Bool.to_instance(db)), + ), + ) + .into() + } + + Some(KnownClass::Str) => { + // ```py + // class str(Sequence[str]): + // @overload + // def __new__(cls, object: object = ...) -> Self: ... + // @overload + // def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ... + // ``` + CallableBinding::from_overloads( + self, + [ + Signature::new( + Parameters::new([Parameter::positional_or_keyword( + Name::new_static("object"), + ) + .with_annotated_type(Type::object(db)) + .with_default_type(Type::string_literal(db, ""))]), + Some(KnownClass::Str.to_instance(db)), + ), + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("object")) + // TODO: Should be `ReadableBuffer` instead of this union type: + .with_annotated_type(UnionType::from_elements( + db, + [ + KnownClass::Bytes.to_instance(db), + KnownClass::Bytearray.to_instance(db), + ], + )) + .with_default_type(Type::bytes_literal(db, b"")), + Parameter::positional_or_keyword(Name::new_static("encoding")) + .with_annotated_type(KnownClass::Str.to_instance(db)) + .with_default_type(Type::string_literal(db, "utf-8")), + Parameter::positional_or_keyword(Name::new_static("errors")) + .with_annotated_type(KnownClass::Str.to_instance(db)) + .with_default_type(Type::string_literal(db, "strict")), + ]), + Some(KnownClass::Str.to_instance(db)), + ), + ], + ) + .into() + } + + Some(KnownClass::Type) => { + let str_instance = KnownClass::Str.to_instance(db); + let type_instance = KnownClass::Type.to_instance(db); + + // ```py + // class type: + // @overload + // def __init__(self, o: object, /) -> None: ... + // @overload + // def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... + // ``` + CallableBinding::from_overloads( + self, + [ + Signature::new( + Parameters::new([Parameter::positional_only(Some( + Name::new_static("o"), + )) + .with_annotated_type(Type::any())]), + Some(type_instance), + ), + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("name"))) + .with_annotated_type(str_instance), + Parameter::positional_only(Some(Name::new_static("bases"))) + .with_annotated_type(TupleType::homogeneous( + db, + type_instance, + )), + Parameter::positional_only(Some(Name::new_static("dict"))) + .with_annotated_type( + KnownClass::Dict.to_specialized_instance( + db, + [str_instance, Type::any()], + ), + ), + ]), + Some(type_instance), + ), + ], + ) + .into() + } + + Some(KnownClass::NamedTuple) => { + Binding::single(self, Signature::todo("functional `NamedTuple` syntax")).into() + } + + Some(KnownClass::Object) => { + // ```py + // class object: + // def __init__(self) -> None: ... + // def __new__(cls) -> Self: ... + // ``` + Binding::single( + self, + Signature::new( + Parameters::empty(), + Some(KnownClass::Object.to_instance(db)), + ), + ) + .into() + } + + Some(KnownClass::Enum) => { + Binding::single(self, Signature::todo("functional `Enum` syntax")).into() + } + + Some(KnownClass::Super) => { + // ```py + // class super: + // @overload + // def __init__(self, t: Any, obj: Any, /) -> None: ... + // @overload + // def __init__(self, t: Any, /) -> None: ... + // @overload + // def __init__(self) -> None: ... + // ``` + CallableBinding::from_overloads( + self, + [ + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("t"))) + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("obj"))) + .with_annotated_type(Type::any()), + ]), + Some(KnownClass::Super.to_instance(db)), + ), + Signature::new( + Parameters::new([Parameter::positional_only(Some( + Name::new_static("t"), + )) + .with_annotated_type(Type::any())]), + Some(KnownClass::Super.to_instance(db)), + ), + Signature::new( + Parameters::empty(), + Some(KnownClass::Super.to_instance(db)), + ), + ], + ) + .into() + } + + Some(KnownClass::TypeVar) => { + // ```py + // class TypeVar: + // def __new__( + // cls, + // name: str, + // *constraints: Any, + // bound: Any | None = None, + // contravariant: bool = False, + // covariant: bool = False, + // infer_variance: bool = False, + // default: Any = ..., + // ) -> Self: ... + // ``` + Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(Type::LiteralString), + Parameter::variadic(Name::new_static("constraints")) + .type_form() + .with_annotated_type(Type::any()), + Parameter::keyword_only(Name::new_static("bound")) + .type_form() + .with_annotated_type(UnionType::from_elements( + db, + [Type::any(), Type::none(db)], + )) + .with_default_type(Type::none(db)), + Parameter::keyword_only(Name::new_static("default")) + .type_form() + .with_annotated_type(Type::any()) + .with_default_type(KnownClass::NoneType.to_instance(db)), + Parameter::keyword_only(Name::new_static("contravariant")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("covariant")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("infer_variance")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + ]), + Some(KnownClass::TypeVar.to_instance(db)), + ), + ) + .into() + } + + Some(KnownClass::TypeAliasType) => { + // ```py + // def __new__( + // cls, + // name: str, + // value: Any, + // *, + // type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = () + // ) -> Self: ... + // ``` + Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_or_keyword(Name::new_static("value")) + .with_annotated_type(Type::any()) + .type_form(), + Parameter::keyword_only(Name::new_static("type_params")) + .with_annotated_type(TupleType::homogeneous( + db, + UnionType::from_elements( + db, + [ + KnownClass::TypeVar.to_instance(db), + KnownClass::ParamSpec.to_instance(db), + KnownClass::TypeVarTuple.to_instance(db), + ], + ), + )) + .with_default_type(TupleType::empty(db)), + ]), + None, + ), + ) + .into() + } + + Some(KnownClass::Property) => { + let getter_signature = Signature::new( + Parameters::new([ + Parameter::positional_only(None).with_annotated_type(Type::any()) + ]), + Some(Type::any()), + ); + let setter_signature = Signature::new( + Parameters::new([ + Parameter::positional_only(None).with_annotated_type(Type::any()), + Parameter::positional_only(None).with_annotated_type(Type::any()), + ]), + Some(Type::none(db)), + ); + let deleter_signature = Signature::new( + Parameters::new([ + Parameter::positional_only(None).with_annotated_type(Type::any()) + ]), + Some(Type::any()), + ); + + Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("fget")) + .with_annotated_type(UnionType::from_elements( + db, + [ + CallableType::single(db, getter_signature), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("fset")) + .with_annotated_type(UnionType::from_elements( + db, + [ + CallableType::single(db, setter_signature), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("fdel")) + .with_annotated_type(UnionType::from_elements( + db, + [ + CallableType::single(db, deleter_signature), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("doc")) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::Str.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ]), + None, + ), + ) + .into() + } + + Some(KnownClass::Tuple) => { + let object = Type::object(db); + + // ```py + // class tuple: + // @overload + // def __new__(cls) -> tuple[()]: ... + // @overload + // def __new__(cls, iterable: Iterable[object]) -> tuple[object, ...]: ... + // ``` + CallableBinding::from_overloads( + self, + [ + Signature::new(Parameters::empty(), Some(TupleType::empty(db))), + Signature::new( + Parameters::new([Parameter::positional_only(Some( + Name::new_static("iterable"), + )) + .with_annotated_type( + KnownClass::Iterable.to_specialized_instance(db, [object]), + )]), + Some(TupleType::homogeneous(db, object)), + ), + ], + ) + .into() + } + + // Most class literal constructor calls are handled by `try_call_constructor` and + // not via getting the signature here. This signature can still be used in some + // cases (e.g. evaluating callable subtyping). TODO improve this definition + // (intersection of `__new__` and `__init__` signatures? and respect metaclass + // `__call__`). + _ => Binding::single( + self, + Signature::new_generic( + class.generic_context(db), + Parameters::gradual_form(), + self.to_instance(db), + ), + ) + .into(), + }, + + Type::SpecialForm(SpecialFormType::TypedDict) => { + Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("typename"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("fields"))) + .with_annotated_type(KnownClass::Dict.to_instance(db)) + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("total")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + // Future compatibility, in case new keyword arguments will be added: + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::any()), + ]), + None, + ), + ) + .into() + } + + Type::GenericAlias(alias) => { + let instantiated = Type::instance(db, ClassType::from(alias)); + + let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) { + // ```py + // class tuple: + // @overload + // def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ... + // @overload + // def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ... + // ``` + let spec = alias.specialization(db).tuple(db); + let mut parameter = + Parameter::positional_only(Some(Name::new_static("iterable"))) + .with_annotated_type(instantiated); + if matches!(spec.len().maximum(), Some(0)) { + parameter = parameter.with_default_type(TupleType::empty(db)); + } + Parameters::new([parameter]) + } else { + Parameters::gradual_form() + }; + // TODO annotated return type on `__new__` or metaclass `__call__` + // TODO check call vs signatures of `__new__` and/or `__init__` + Binding::single(self, Signature::new(parameters, Some(instantiated))).into() + } + + Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { + SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).bindings(db), + + // Most type[] constructor calls are handled by `try_call_constructor` and not via + // getting the signature here. This signature can still be used in some cases (e.g. + // evaluating callable subtyping). TODO improve this definition (intersection of + // `__new__` and `__init__` signatures? and respect metaclass `__call__`). + SubclassOfInner::Class(class) => Type::from(class).bindings(db), + }, + + Type::NominalInstance(_) | Type::ProtocolInstance(_) => { + // Note that for objects that have a (possibly not callable!) `__call__` attribute, + // we will get the signature of the `__call__` attribute, but will pass in the type + // of the original object as the "callable type". That ensures that we get errors + // like "`X` is not callable" instead of "`` is not + // callable". + match self + .member_lookup_with_policy( + db, + Name::new_static("__call__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Type(dunder_callable, boundness) => { + let mut bindings = dunder_callable.bindings(db); + bindings.replace_callable_type(dunder_callable, self); + if boundness == Boundness::PossiblyUnbound { + bindings.set_dunder_call_is_possibly_unbound(); + } + bindings + } + Place::Unbound => CallableBinding::not_callable(self).into(), + } + } + + // Dynamic types are callable, and the return type is the same dynamic type. Similarly, + // `Never` is always callable and returns `Never`. + Type::Dynamic(_) | Type::Never => { + Binding::single(self, Signature::dynamic(self)).into() + } + + // Note that this correctly returns `None` if none of the union elements are callable. + Type::Union(union) => Bindings::from_union( + self, + union + .elements(db) + .iter() + .map(|element| element.bindings(db)), + ), + + Type::Intersection(_) => { + Binding::single(self, Signature::todo("Type::Intersection.call()")).into() + } + + // TODO: these are actually callable + Type::MethodWrapper(_) | Type::DataclassDecorator(_) => { + CallableBinding::not_callable(self).into() + } + + // TODO: some `SpecialForm`s are callable (e.g. TypedDicts) + Type::SpecialForm(_) => CallableBinding::not_callable(self).into(), + + Type::PropertyInstance(_) + | Type::KnownInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::BooleanLiteral(_) + | Type::LiteralString + | Type::Tuple(_) + | Type::BoundSuper(_) + | Type::ModuleLiteral(_) + | Type::TypeIs(_) => CallableBinding::not_callable(self).into(), + } + } + + /// Calls `self`. Returns a [`CallError`] if `self` is (always or possibly) not callable, or if + /// the arguments are not compatible with the formal parameters. + /// + /// You get back a [`Bindings`] for both successful and unsuccessful calls. + /// It contains information about which formal parameters each argument was matched to, + /// and about any errors matching arguments and parameters. + fn try_call( + self, + db: &'db dyn Db, + argument_types: &CallArgumentTypes<'_, 'db>, + ) -> Result, CallError<'db>> { + self.bindings(db) + .match_parameters(argument_types) + .check_types(db, argument_types) + } + + /// Look up a dunder method on the meta-type of `self` and call it. + /// + /// Returns an `Err` if the dunder method can't be called, + /// or the given arguments are not valid. + fn try_call_dunder( + self, + db: &'db dyn Db, + name: &str, + mut argument_types: CallArgumentTypes<'_, 'db>, + ) -> Result, CallDunderError<'db>> { + self.try_call_dunder_with_policy( + db, + name, + &mut argument_types, + MemberLookupPolicy::default(), + ) + } + + /// Same as `try_call_dunder`, but allows specifying a policy for the member lookup. In + /// particular, this allows to specify `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK` to avoid + /// looking up dunder methods on `object`, which is needed for functions like `__init__`, + /// `__new__`, or `__setattr__`. + /// + /// Note that `NO_INSTANCE_FALLBACK` is always added to the policy, since implicit calls to + /// dunder methods never access instance members. + fn try_call_dunder_with_policy( + self, + db: &'db dyn Db, + name: &str, + argument_types: &mut CallArgumentTypes<'_, 'db>, + policy: MemberLookupPolicy, + ) -> Result, CallDunderError<'db>> { + // Implicit calls to dunder methods never access instance members, so we pass + // `NO_INSTANCE_FALLBACK` here in addition to other policies: + match self + .member_lookup_with_policy( + db, + name.into(), + policy | MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Type(dunder_callable, boundness) => { + let bindings = dunder_callable + .bindings(db) + .match_parameters(argument_types) + .check_types(db, argument_types)?; + if boundness == Boundness::PossiblyUnbound { + return Err(CallDunderError::PossiblyUnbound(Box::new(bindings))); + } + Ok(bindings) + } + Place::Unbound => Err(CallDunderError::MethodNotAvailable), + } + } + + /// Returns the element type when iterating over `self`. + /// + /// This method should only be used outside of type checking because it omits any errors. + /// For type checking, use [`try_iterate`](Self::try_iterate) instead. + fn iterate(self, db: &'db dyn Db) -> Type<'db> { + self.try_iterate(db) + .unwrap_or_else(|err| err.fallback_element_type(db)) + } + + /// Given the type of an object that is iterated over in some way, + /// return the type of objects that are yielded by that iteration. + /// + /// E.g., for the following loop, given the type of `x`, infer the type of `y`: + /// ```python + /// for y in x: + /// pass + /// ``` + fn try_iterate(self, db: &'db dyn Db) -> Result, IterationError<'db>> { + if let Type::Tuple(tuple_type) = self { + return Ok(UnionType::from_elements( + db, + tuple_type.tuple(db).all_elements(), + )); + } + + if let Type::GenericAlias(alias) = self { + if alias.origin(db).is_known(db, KnownClass::Tuple) { + return Ok(todo_type!("*tuple[] annotations")); + } + } + + let try_call_dunder_getitem = || { + self.try_call_dunder( + db, + "__getitem__", + CallArgumentTypes::positional([KnownClass::Int.to_instance(db)]), + ) + .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) + }; + + let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { + iterator + .try_call_dunder(db, "__next__", CallArgumentTypes::none()) + .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) + }; + + let dunder_iter_result = self + .try_call_dunder(db, "__iter__", CallArgumentTypes::none()) + .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); + + match dunder_iter_result { + Ok(iterator) => { + // `__iter__` is definitely bound and calling it succeeds. + // See what calling `__next__` on the object returned by `__iter__` gives us... + try_call_dunder_next_on_iterator(iterator).map_err(|dunder_next_error| { + IterationError::IterReturnsInvalidIterator { + iterator, + dunder_next_error, + } + }) + } + + // `__iter__` is possibly unbound... + Err(CallDunderError::PossiblyUnbound(dunder_iter_outcome)) => { + let iterator = dunder_iter_outcome.return_type(db); + + match try_call_dunder_next_on_iterator(iterator) { + Ok(dunder_next_return) => { + try_call_dunder_getitem() + .map(|dunder_getitem_return_type| { + // If `__iter__` is possibly unbound, + // but it returns an object that has a bound and valid `__next__` method, + // *and* the object has a bound and valid `__getitem__` method, + // we infer a union of the type returned by the `__next__` method + // and the type returned by the `__getitem__` method. + // + // No diagnostic is emitted; iteration will always succeed! + UnionType::from_elements( + db, + [dunder_next_return, dunder_getitem_return_type], + ) + }) + .map_err(|dunder_getitem_error| { + IterationError::PossiblyUnboundIterAndGetitemError { + dunder_next_return, + dunder_getitem_error, + } + }) + } + + Err(dunder_next_error) => Err(IterationError::IterReturnsInvalidIterator { + iterator, + dunder_next_error, + }), + } + } + + // `__iter__` is definitely bound but it can't be called with the expected arguments + Err(CallDunderError::CallError(kind, bindings)) => { + Err(IterationError::IterCallError(kind, bindings)) + } + + // There's no `__iter__` method. Try `__getitem__` instead... + Err(CallDunderError::MethodNotAvailable) => { + try_call_dunder_getitem().map_err(|dunder_getitem_error| { + IterationError::UnboundIterAndGetitemError { + dunder_getitem_error, + } + }) + } + } + } + + /// Returns the type bound from a context manager with type `self`. + /// + /// This method should only be used outside of type checking because it omits any errors. + /// For type checking, use [`try_enter`](Self::try_enter) instead. + fn enter(self, db: &'db dyn Db) -> Type<'db> { + self.try_enter(db) + .unwrap_or_else(|err| err.fallback_enter_type(db)) + } + + /// Given the type of an object that is used as a context manager (i.e. in a `with` statement), + /// return the return type of its `__enter__` method, which is bound to any potential targets. + /// + /// E.g., for the following `with` statement, given the type of `x`, infer the type of `y`: + /// ```python + /// with x as y: + /// pass + /// ``` + fn try_enter(self, db: &'db dyn Db) -> Result, ContextManagerError<'db>> { + let enter = self.try_call_dunder(db, "__enter__", CallArgumentTypes::none()); + let exit = self.try_call_dunder( + db, + "__exit__", + CallArgumentTypes::positional([Type::none(db), Type::none(db), Type::none(db)]), + ); + + // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). + match (enter, exit) { + (Ok(enter), Ok(_)) => Ok(enter.return_type(db)), + (Ok(enter), Err(exit_error)) => Err(ContextManagerError::Exit { + enter_return_type: enter.return_type(db), + exit_error, + }), + // TODO: Use the `exit_ty` to determine if any raised exception is suppressed. + (Err(enter_error), Ok(_)) => Err(ContextManagerError::Enter(enter_error)), + (Err(enter_error), Err(exit_error)) => Err(ContextManagerError::EnterAndExit { + enter_error, + exit_error, + }), + } + } + + /// Given a class literal or non-dynamic SubclassOf type, try calling it (creating an instance) + /// and return the resulting instance type. + /// + /// Models `type.__call__` behavior. + /// TODO: model metaclass `__call__`. + /// + /// E.g., for the following code, infer the type of `Foo()`: + /// ```python + /// class Foo: + /// pass + /// + /// Foo() + /// ``` + fn try_call_constructor( + self, + db: &'db dyn Db, + argument_types: CallArgumentTypes<'_, 'db>, + ) -> Result, ConstructorCallError<'db>> { + debug_assert!(matches!( + self, + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) + )); + + // If we are trying to construct a non-specialized generic class, we should use the + // constructor parameters to try to infer the class specialization. To do this, we need to + // tweak our member lookup logic a bit. Normally, when looking up a class or instance + // member, we first apply the class's default specialization, and apply that specialization + // to the type of the member. To infer a specialization from the argument types, we need to + // have the class's typevars still in the method signature when we attempt to call it. To + // do this, we instead use the _identity_ specialization, which maps each of the class's + // generic typevars to itself. + let (generic_origin, generic_context, self_type) = + match self { + Type::ClassLiteral(class) => match class.generic_context(db) { + Some(generic_context) => ( + Some(class), + Some(generic_context), + Type::from(class.apply_specialization(db, |_| { + generic_context.identity_specialization(db) + })), + ), + _ => (None, None, self), + }, + _ => (None, None, self), + }; + + // As of now we do not model custom `__call__` on meta-classes, so the code below + // only deals with interplay between `__new__` and `__init__` methods. + // The logic is roughly as follows: + // 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always + // present), we call it and analyze outcome. We then analyze `__init__` call, but only + // if it is defined somewhere except object. This is because `object.__init__` + // allows arbitrary arguments if and only if `__new__` is defined, but typeshed + // defines `__init__` for `object` with no arguments. + // 2. If `__new__` is not found, we call `__init__`. Here, we allow it to fallback all + // the way to `object` (single `self` argument call). This time it is correct to + // fallback to `object.__init__`, since it will indeed check that no arguments are + // passed. + // + // Note that we currently ignore `__new__` return type, since we do not yet support `Self` + // and most builtin classes use it as return type annotation. We always return the instance + // type. + + // Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must + // avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on + // a class, metaclass attribute would take precedence. But by avoiding `__new__` on + // `object` we would inadvertently unhide `__new__` on `type`, which is not what we want. + // An alternative might be to not skip `object.__new__` but instead mark it such that it's + // easy to check if that's the one we found? + // Note that `__new__` is a static method, so we must inject the `cls` argument. + let new_method = self_type.lookup_dunder_new(db, ()); + let new_call_outcome = new_method.and_then(|new_method| { + match new_method.place.try_call_dunder_get(db, self_type) { + Place::Type(new_method, boundness) => { + let result = + new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref()); + if boundness == Boundness::PossiblyUnbound { + Some(Err(DunderNewCallError::PossiblyUnbound(result.err()))) + } else { + Some(result.map_err(DunderNewCallError::CallError)) + } + } + Place::Unbound => None, + } + }); + + // Construct an instance type that we can use to look up the `__init__` instance method. + // This performs the same logic as `Type::to_instance`, except for generic class literals. + // TODO: we should use the actual return type of `__new__` to determine the instance type + let init_ty = self_type + .to_instance(db) + .expect("type should be convertible to instance type"); + + let init_call_outcome = if new_call_outcome.is_none() + || !init_ty + .member_lookup_with_policy( + db, + "__init__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + .place + .is_unbound() + { + Some(init_ty.try_call_dunder(db, "__init__", argument_types)) + } else { + None + }; + + // Note that we use `self` here, not `self_type`, so that if constructor argument inference + // fails, we fail back to the default specialization. + let instance_ty = self + .to_instance(db) + .expect("type should be convertible to instance type"); + + match (generic_origin, new_call_outcome, init_call_outcome) { + // All calls are successful or not called at all + ( + Some(generic_origin), + new_call_outcome @ (None | Some(Ok(_))), + init_call_outcome @ (None | Some(Ok(_))), + ) => { + fn combine_specializations<'db>( + db: &'db dyn Db, + s1: Option>, + s2: Option>, + ) -> Option> { + match (s1, s2) { + (None, None) => None, + (Some(s), None) | (None, Some(s)) => Some(s), + (Some(s1), Some(s2)) => Some(s1.combine(db, s2)), + } + } + + let new_specialization = new_call_outcome + .and_then(Result::ok) + .as_ref() + .and_then(Bindings::single_element) + .into_iter() + .flat_map(CallableBinding::matching_overloads) + .next() + .and_then(|(_, binding)| binding.inherited_specialization()) + .filter(|specialization| { + Some(specialization.generic_context(db)) == generic_context + }); + let init_specialization = init_call_outcome + .and_then(Result::ok) + .as_ref() + .and_then(Bindings::single_element) + .into_iter() + .flat_map(CallableBinding::matching_overloads) + .next() + .and_then(|(_, binding)| binding.inherited_specialization()) + .filter(|specialization| { + Some(specialization.generic_context(db)) == generic_context + }); + let specialization = + combine_specializations(db, new_specialization, init_specialization); + let specialized = specialization + .map(|specialization| { + Type::instance( + db, + generic_origin.apply_specialization(db, |_| specialization), + ) + }) + .unwrap_or(instance_ty); + Ok(specialized) + } + + (None, None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty), + + (_, None | Some(Ok(_)), Some(Err(error))) => { + // no custom `__new__` or it was called and succeeded, but `__init__` failed. + Err(ConstructorCallError::Init(instance_ty, error)) + } + (_, Some(Err(error)), None | Some(Ok(_))) => { + // custom `__new__` was called and failed, but init is ok + Err(ConstructorCallError::New(instance_ty, error)) + } + (_, Some(Err(new_error)), Some(Err(init_error))) => { + // custom `__new__` was called and failed, and `__init__` is also not ok + Err(ConstructorCallError::NewAndInit( + instance_ty, + new_error, + init_error, + )) + } + } + } + + #[must_use] + pub fn to_instance(&self, db: &'db dyn Db) -> Option> { + match self { + Type::Dynamic(_) | Type::Never => Some(*self), + Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))), + Type::GenericAlias(alias) => Some(Type::instance(db, ClassType::from(*alias))), + Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance(db)), + Type::Union(union) => union.to_instance(db), + // If there is no bound or constraints on a typevar `T`, `T: object` implicitly, which + // has no instance type. Otherwise, synthesize a typevar with bound or constraints + // mapped through `to_instance`. + Type::TypeVar(typevar) => { + let bound_or_constraints = match typevar.bound_or_constraints(db)? { + TypeVarBoundOrConstraints::UpperBound(upper_bound) => { + TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints( + constraints.to_instance(db)?.into_union()?, + ) + } + }; + Some(Type::TypeVar(TypeVarInstance::new( + db, + Name::new(format!("{}'instance", typevar.name(db))), + None, + Some(bound_or_constraints), + typevar.variance(db), + None, + typevar.kind(db), + ))) + } + Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance")), + Type::BooleanLiteral(_) + | Type::BytesLiteral(_) + | Type::FunctionLiteral(_) + | Type::Callable(..) + | Type::MethodWrapper(_) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::ModuleLiteral(_) + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::Tuple(_) + | Type::LiteralString + | Type::BoundSuper(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::TypeIs(_) => None, + } + } + + /// If we see a value of this type used as a type expression, what type does it name? + /// + /// For example, the builtin `int` as a value expression is of type + /// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type + /// expression, it names the type `Type::NominalInstance(builtins.int)`, that is, all objects whose + /// `__class__` is `int`. + /// + /// The `scope_id` argument must always be a scope from the file we are currently inferring, so + /// as to avoid cross-module AST dependency. + pub(crate) fn in_type_expression( + &self, + db: &'db dyn Db, + scope_id: ScopeId<'db>, + ) -> Result, InvalidTypeExpressionError<'db>> { + match self { + // Special cases for `float` and `complex` + // https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex + Type::ClassLiteral(class) => { + let ty = match class.known(db) { + Some(KnownClass::Any) => Type::any(), + Some(KnownClass::Complex) => UnionType::from_elements( + db, + [ + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + KnownClass::Complex.to_instance(db), + ], + ), + Some(KnownClass::Float) => UnionType::from_elements( + db, + [ + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + ], + ), + _ => Type::instance(db, class.default_specialization(db)), + }; + Ok(ty) + } + Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))), + + Type::SubclassOf(_) + | Type::BooleanLiteral(_) + | Type::BytesLiteral(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::IntLiteral(_) + | Type::LiteralString + | Type::ModuleLiteral(_) + | Type::StringLiteral(_) + | Type::Tuple(_) + | Type::TypeVar(_) + | Type::Callable(_) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::Never + | Type::FunctionLiteral(_) + | Type::BoundSuper(_) + | Type::ProtocolInstance(_) + | Type::PropertyInstance(_) + | Type::TypeIs(_) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType( + *self, scope_id + )], + fallback_type: Type::unknown(), + }), + + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)), + KnownInstanceType::TypeVar(typevar) => Ok(Type::TypeVar(*typevar)), + KnownInstanceType::SubscriptedProtocol(_) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], + fallback_type: Type::unknown(), + }), + KnownInstanceType::SubscriptedGeneric(_) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), + }, + + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Never | SpecialFormType::NoReturn => Ok(Type::Never), + SpecialFormType::LiteralString => Ok(Type::LiteralString), + SpecialFormType::Unknown => Ok(Type::unknown()), + SpecialFormType::AlwaysTruthy => Ok(Type::AlwaysTruthy), + SpecialFormType::AlwaysFalsy => Ok(Type::AlwaysFalsy), + + // We treat `typing.Type` exactly the same as `builtins.type`: + SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormType::Tuple => Ok(TupleType::homogeneous(db, Type::unknown())), + + // Legacy `typing` aliases + SpecialFormType::List => Ok(KnownClass::List.to_instance(db)), + SpecialFormType::Dict => Ok(KnownClass::Dict.to_instance(db)), + SpecialFormType::Set => Ok(KnownClass::Set.to_instance(db)), + SpecialFormType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)), + SpecialFormType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)), + SpecialFormType::Counter => Ok(KnownClass::Counter.to_instance(db)), + SpecialFormType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)), + SpecialFormType::Deque => Ok(KnownClass::Deque.to_instance(db)), + SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), + + // TODO: Use an opt-in rule for a bare `Callable` + SpecialFormType::Callable => Ok(CallableType::unknown(db)), + + SpecialFormType::TypingSelf => { + let module = parsed_module(db, scope_id.file(db)).load(db); + let index = semantic_index(db, scope_id.file(db)); + let Some(class) = nearest_enclosing_class(db, index, scope_id, &module) else { + return Err(InvalidTypeExpressionError { + fallback_type: Type::unknown(), + invalid_expressions: smallvec::smallvec![ + InvalidTypeExpression::InvalidType(*self, scope_id) + ], + }); + }; + let instance = Type::ClassLiteral(class).to_instance(db).expect( + "nearest_enclosing_class must return type that can be instantiated", + ); + Ok(Type::TypeVar(TypeVarInstance::new( + db, + ast::name::Name::new("Self"), + Some(class.definition(db)), + Some(TypeVarBoundOrConstraints::UpperBound(instance)), + TypeVarVariance::Invariant, + None, + TypeVarKind::Legacy, + ))) + } + SpecialFormType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")), + SpecialFormType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")), + + SpecialFormType::Literal + | SpecialFormType::Union + | SpecialFormType::Intersection => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![ + InvalidTypeExpression::RequiresArguments(*self) + ], + fallback_type: Type::unknown(), + }), + + SpecialFormType::Protocol => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], + fallback_type: Type::unknown(), + }), + SpecialFormType::Generic => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), + + SpecialFormType::Optional + | SpecialFormType::Not + | SpecialFormType::TypeOf + | SpecialFormType::TypeIs + | SpecialFormType::TypeGuard + | SpecialFormType::Unpack + | SpecialFormType::CallableTypeOf => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![ + InvalidTypeExpression::RequiresOneArgument(*self) + ], + fallback_type: Type::unknown(), + }), + + SpecialFormType::Annotated | SpecialFormType::Concatenate => { + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![ + InvalidTypeExpression::RequiresTwoArguments(*self) + ], + fallback_type: Type::unknown(), + }) + } + + SpecialFormType::ClassVar | SpecialFormType::Final => { + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![ + InvalidTypeExpression::TypeQualifier(*special_form) + ], + fallback_type: Type::unknown(), + }) + } + + SpecialFormType::ReadOnly + | SpecialFormType::NotRequired + | SpecialFormType::Required => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![ + InvalidTypeExpression::TypeQualifierRequiresOneArgument(*special_form) + ], + fallback_type: Type::unknown(), + }), + }, + + Type::Union(union) => { + let mut builder = UnionBuilder::new(db); + let mut invalid_expressions = smallvec::SmallVec::default(); + for element in union.elements(db) { + match element.in_type_expression(db, scope_id) { + Ok(type_expr) => builder = builder.add(type_expr), + Err(InvalidTypeExpressionError { + fallback_type, + invalid_expressions: new_invalid_expressions, + }) => { + invalid_expressions.extend(new_invalid_expressions); + builder = builder.add(fallback_type); + } + } + } + if invalid_expressions.is_empty() { + Ok(builder.build()) + } else { + Err(InvalidTypeExpressionError { + fallback_type: builder.build(), + invalid_expressions, + }) + } + } + + Type::Dynamic(_) => Ok(*self), + + Type::NominalInstance(instance) => match instance.class.known(db) { + Some(KnownClass::TypeVar) => Ok(todo_type!( + "Support for `typing.TypeVar` instances in type expressions" + )), + Some( + KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs, + ) => Ok(todo_type!("Support for `typing.ParamSpec`")), + Some(KnownClass::TypeVarTuple) => Ok(todo_type!( + "Support for `typing.TypeVarTuple` instances in type expressions" + )), + Some(KnownClass::NewType) => Ok(todo_type!( + "Support for `typing.NewType` instances in type expressions" + )), + Some(KnownClass::GenericAlias) => Ok(todo_type!( + "Support for `typing.GenericAlias` instances in type expressions" + )), + Some(KnownClass::UnionType) => Ok(todo_type!( + "Support for `types.UnionType` instances in type expressions" + )), + _ => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType( + *self, scope_id + )], + fallback_type: Type::unknown(), + }), + }, + + Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), + } + } + + /// The type `NoneType` / `None` + pub fn none(db: &'db dyn Db) -> Type<'db> { + KnownClass::NoneType.to_instance(db) + } + + /// Return the type of `tuple(sys.version_info)`. + /// + /// This is not exactly the type that `sys.version_info` has at runtime, + /// but it's a useful fallback for us in order to infer `Literal` types from `sys.version_info` comparisons. + fn version_info_tuple(db: &'db dyn Db) -> Self { + let python_version = Program::get(db).python_version(db); + let int_instance_ty = KnownClass::Int.to_instance(db); + + // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) + let release_level_ty = { + let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"] + .iter() + .map(|level| Type::string_literal(db, level)) + .collect(); + + // For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`; + // those techniques ensure that union elements are deduplicated and unions are eagerly simplified + // into other types where necessary. Here, however, we know that there are no duplicates + // in this union, so it's probably more efficient to use `UnionType::new()` directly. + Type::Union(UnionType::new(db, elements)) + }; + + TupleType::from_elements( + db, + [ + Type::IntLiteral(python_version.major.into()), + Type::IntLiteral(python_version.minor.into()), + int_instance_ty, + release_level_ty, + int_instance_ty, + ], + ) + } + + /// Given a type that is assumed to represent an instance of a class, + /// return a type that represents that class itself. + #[must_use] + pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { + match self { + Type::Never => Type::Never, + Type::NominalInstance(instance) => instance.to_meta_type(db), + Type::KnownInstance(known_instance) => known_instance.to_meta_type(db), + Type::SpecialForm(special_form) => special_form.to_meta_type(db), + Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), + Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), + Type::BooleanLiteral(_) | Type::TypeIs(_) => KnownClass::Bool.to_class_literal(db), + Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db), + Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), + Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), + Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), + Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db), + Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), + Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db), + Type::Callable(callable) if callable.is_function_like(db) => { + KnownClass::FunctionType.to_class_literal(db) + } + Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), + Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), + Type::Tuple(tuple) => tuple + .to_class_type(db) + .map(Type::from) + .unwrap_or_else(Type::unknown), + + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { + None => KnownClass::Type.to_instance(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // TODO: If we add a proper `OneOf` connector, we should use that here instead + // of union. (Using a union here doesn't break anything, but it is imprecise.) + constraints.map(db, |constraint| constraint.to_meta_type(db)) + } + }, + + Type::ClassLiteral(class) => class.metaclass(db), + Type::GenericAlias(alias) => ClassType::from(*alias).metaclass(db), + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => *self, + SubclassOfInner::Class(class) => SubclassOfType::from( + db, + SubclassOfInner::try_from_type(db, class.metaclass(db)) + .unwrap_or(SubclassOfInner::unknown()), + ), + }, + + Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), + Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(*dynamic)), + // TODO intersections + Type::Intersection(_) => SubclassOfType::from( + db, + SubclassOfInner::try_from_type(db, todo_type!("Intersection meta-type")) + .expect("Type::Todo should be a valid `SubclassOfInner`"), + ), + Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), + Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), + Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), + } + } + + #[must_use] + pub fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Type<'db> { + if let Some(specialization) = specialization { + self.apply_specialization(db, specialization) + } else { + self + } + } + + /// Applies a specialization to this type, replacing any typevars with the types that they are + /// specialized to. + /// + /// Note that this does not specialize generic classes, functions, or type aliases! That is a + /// different operation that is performed explicitly (via a subscript operation), or implicitly + /// via a call to the generic object. + #[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] + pub fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Type<'db> { + self.apply_type_mapping(db, &TypeMapping::Specialization(specialization)) + } + + fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Type<'db> { + match self { + Type::TypeVar(typevar) => match type_mapping { + TypeMapping::Specialization(specialization) => { + specialization.get(db, typevar).unwrap_or(self) + } + TypeMapping::PartialSpecialization(partial) => { + partial.get(db, typevar).unwrap_or(self) + } + TypeMapping::PromoteLiterals => self, + } + + Type::FunctionLiteral(function) => { + Type::FunctionLiteral(function.with_type_mapping(db, type_mapping)) + } + + Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( + db, + method.function(db).with_type_mapping(db, type_mapping), + method.self_instance(db).apply_type_mapping(db, type_mapping), + )), + + Type::NominalInstance(instance) => Type::NominalInstance( + instance.apply_type_mapping(db, type_mapping), + ), + + Type::ProtocolInstance(instance) => { + Type::ProtocolInstance(instance.apply_type_mapping(db, type_mapping)) + } + + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet( + function.with_type_mapping(db, type_mapping), + )) + } + + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall( + function.with_type_mapping(db, type_mapping), + )) + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet( + property.apply_type_mapping(db, type_mapping), + )) + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet( + property.apply_type_mapping(db, type_mapping), + )) + } + + Type::Callable(callable) => { + Type::Callable(callable.apply_type_mapping(db, type_mapping)) + } + + Type::GenericAlias(generic) => { + Type::GenericAlias(generic.apply_type_mapping(db, type_mapping)) + } + + Type::SubclassOf(subclass_of) => Type::SubclassOf( + subclass_of.apply_type_mapping(db, type_mapping), + ), + + Type::PropertyInstance(property) => { + Type::PropertyInstance(property.apply_type_mapping(db, type_mapping)) + } + + Type::Union(union) => union.map(db, |element| { + element.apply_type_mapping(db, type_mapping) + }), + Type::Intersection(intersection) => { + let mut builder = IntersectionBuilder::new(db); + for positive in intersection.positive(db) { + builder = + builder.add_positive(positive.apply_type_mapping(db, type_mapping)); + } + for negative in intersection.negative(db) { + builder = + builder.add_negative(negative.apply_type_mapping(db, type_mapping)); + } + builder.build() + } + Type::Tuple(tuple) => Type::tuple(tuple.apply_type_mapping(db, type_mapping)), + + Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)), + + Type::ModuleLiteral(_) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::LiteralString + | Type::StringLiteral(_) + | Type::BytesLiteral(_) => match type_mapping { + TypeMapping::Specialization(_) | + TypeMapping::PartialSpecialization(_) => self, + TypeMapping::PromoteLiterals => self.literal_fallback_instance(db) + .expect("literal type should have fallback instance type"), + } + + Type::Dynamic(_) + | Type::Never + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + // A non-generic class never needs to be specialized. A generic class is specialized + // explicitly (via a subscript expression) or implicitly (via a call), and not because + // some other generic context's specialization is applied to it. + | Type::ClassLiteral(_) + | Type::BoundSuper(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) => self, + } + } + + /// Locates any legacy `TypeVar`s in this type, and adds them to a set. This is used to build + /// up a generic context from any legacy `TypeVar`s that appear in a function parameter list or + /// `Generic` specialization. + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + Type::TypeVar(typevar) => { + if typevar.is_legacy(db) { + typevars.insert(typevar); + } + } + + Type::FunctionLiteral(function) => function.find_legacy_typevars(db, typevars), + + Type::BoundMethod(method) => { + method.self_instance(db).find_legacy_typevars(db, typevars); + method.function(db).find_legacy_typevars(db, typevars); + } + + Type::MethodWrapper( + MethodWrapperKind::FunctionTypeDunderGet(function) + | MethodWrapperKind::FunctionTypeDunderCall(function), + ) => { + function.find_legacy_typevars(db, typevars); + } + + Type::MethodWrapper( + MethodWrapperKind::PropertyDunderGet(property) + | MethodWrapperKind::PropertyDunderSet(property), + ) => { + property.find_legacy_typevars(db, typevars); + } + + Type::Callable(callable) => { + callable.find_legacy_typevars(db, typevars); + } + + Type::PropertyInstance(property) => { + property.find_legacy_typevars(db, typevars); + } + + Type::Union(union) => { + for element in union.iter(db) { + element.find_legacy_typevars(db, typevars); + } + } + Type::Intersection(intersection) => { + for positive in intersection.positive(db) { + positive.find_legacy_typevars(db, typevars); + } + for negative in intersection.negative(db) { + negative.find_legacy_typevars(db, typevars); + } + } + + Type::Tuple(tuple) => { + tuple.find_legacy_typevars(db, typevars); + } + + Type::GenericAlias(alias) => { + alias.find_legacy_typevars(db, typevars); + } + + Type::NominalInstance(instance) => { + instance.find_legacy_typevars(db, typevars); + } + + Type::ProtocolInstance(instance) => { + instance.find_legacy_typevars(db, typevars); + } + + Type::SubclassOf(subclass_of) => { + subclass_of.find_legacy_typevars(db, typevars); + } + + Type::TypeIs(type_is) => { + type_is.return_type(db).find_legacy_typevars(db, typevars); + } + + Type::Dynamic(_) + | Type::Never + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::LiteralString + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::BoundSuper(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) => {} + } + } + + /// Return the string representation of this type when converted to string as it would be + /// provided by the `__str__` method. + /// + /// When not available, this should fall back to the value of `[Type::repr]`. + /// Note: this method is used in the builtins `format`, `print`, `str.format` and `f-strings`. + #[must_use] + pub fn str(&self, db: &'db dyn Db) -> Type<'db> { + match self { + Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), + Type::StringLiteral(_) | Type::LiteralString => *self, + Type::SpecialForm(special_form) => Type::string_literal(db, special_form.repr()), + Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new( + db, + known_instance.repr(db).to_string().into_boxed_str(), + )), + // TODO: handle more complex types + _ => KnownClass::Str.to_instance(db), + } + } + + /// Return the string representation of this type as it would be provided by the `__repr__` + /// method at runtime. + #[must_use] + pub fn repr(&self, db: &'db dyn Db) -> Type<'db> { + match self { + Type::IntLiteral(number) => Type::string_literal(db, &number.to_string()), + Type::BooleanLiteral(true) => Type::string_literal(db, "True"), + Type::BooleanLiteral(false) => Type::string_literal(db, "False"), + Type::StringLiteral(literal) => { + Type::string_literal(db, &format!("'{}'", literal.value(db).escape_default())) + } + Type::LiteralString => Type::LiteralString, + Type::SpecialForm(special_form) => Type::string_literal(db, special_form.repr()), + Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new( + db, + known_instance.repr(db).to_string().into_boxed_str(), + )), + // TODO: handle more complex types + _ => KnownClass::Str.to_instance(db), + } + } + + /// Returns where this type is defined. + /// + /// It's the foundation for the editor's "Go to type definition" feature + /// where the user clicks on a value and it takes them to where the value's type is defined. + /// + /// This method returns `None` for unions and intersections because how these + /// should be handled, especially when some variants don't have definitions, is + /// specific to the call site. + pub fn definition(&self, db: &'db dyn Db) -> Option> { + match self { + Self::BoundMethod(method) => { + Some(TypeDefinition::Function(method.function(db).definition(db))) + } + Self::FunctionLiteral(function) => { + Some(TypeDefinition::Function(function.definition(db))) + } + Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))), + Self::ClassLiteral(class_literal) => { + Some(TypeDefinition::Class(class_literal.definition(db))) + } + Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), + Self::NominalInstance(instance) => { + Some(TypeDefinition::Class(instance.class.definition(db))) + } + Self::KnownInstance(instance) => match instance { + KnownInstanceType::TypeVar(var) => { + Some(TypeDefinition::TypeVar(var.definition(db)?)) + } + KnownInstanceType::TypeAliasType(type_alias) => { + type_alias.definition(db).map(TypeDefinition::TypeAlias) + } + _ => None, + }, + + Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { + SubclassOfInner::Class(class) => Some(TypeDefinition::Class(class.definition(db))), + SubclassOfInner::Dynamic(_) => None, + }, + + Self::StringLiteral(_) + | Self::BooleanLiteral(_) + | Self::LiteralString + | Self::IntLiteral(_) + | Self::BytesLiteral(_) + | Self::MethodWrapper(_) + | Self::WrapperDescriptor(_) + | Self::DataclassDecorator(_) + | Self::DataclassTransformer(_) + | Self::PropertyInstance(_) + | Self::BoundSuper(_) + | Self::Tuple(_) => self.to_meta_type(db).definition(db), + + Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db)?)), + + Self::ProtocolInstance(protocol) => match protocol.inner { + Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))), + Protocol::Synthesized(_) => None, + }, + + Self::Union(_) | Self::Intersection(_) => None, + + // These types have no definition + Self::Dynamic(_) + | Self::Never + | Self::Callable(_) + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::SpecialForm(_) + | Self::TypeIs(_) => None, + } + } + + /// Returns a tuple of two spans. The first is + /// the span for the identifier of the function + /// definition for `self`. The second is + /// the span for the parameter in the function + /// definition for `self`. + /// + /// If there are no meaningful spans, then this + /// returns `None`. For example, when this type + /// isn't callable. + /// + /// When `parameter_index` is `None`, then the + /// second span returned covers the entire parameter + /// list. + /// + /// # Performance + /// + /// Note that this may introduce cross-module + /// dependencies. This can have an impact on + /// the effectiveness of incremental caching + /// and should therefore be used judiciously. + /// + /// An example of a good use case is to improve + /// a diagnostic. + fn parameter_span( + &self, + db: &'db dyn Db, + parameter_index: Option, + ) -> Option<(Span, Span)> { + match *self { + Type::FunctionLiteral(function) => function.parameter_span(db, parameter_index), + Type::BoundMethod(bound_method) => bound_method + .function(db) + .parameter_span(db, parameter_index), + _ => None, + } + } + + /// Returns a collection of useful spans for a + /// function signature. These are useful for + /// creating annotations on diagnostics. + /// + /// If there are no meaningful spans, then this + /// returns `None`. For example, when this type + /// isn't callable. + /// + /// # Performance + /// + /// Note that this may introduce cross-module + /// dependencies. This can have an impact on + /// the effectiveness of incremental caching + /// and should therefore be used judiciously. + /// + /// An example of a good use case is to improve + /// a diagnostic. + fn function_spans(&self, db: &'db dyn Db) -> Option { + match *self { + Type::FunctionLiteral(function) => function.spans(db), + Type::BoundMethod(bound_method) => bound_method.function(db).spans(db), + _ => None, + } + } + + pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> { + match self { + Type::GenericAlias(generic) => Some(generic.origin(db)), + Type::NominalInstance(instance) => { + if let ClassType::Generic(generic) = instance.class { + Some(generic.origin(db)) + } else { + None + } + } + _ => None, + } + } +} + +impl<'db> From<&Type<'db>> for Type<'db> { + fn from(value: &Type<'db>) -> Self { + *value + } +} + +/// A mapping that can be applied to a type, producing another type. This is applied inductively to +/// the components of complex types. +/// +/// This is represented as an enum (with some variants using `Cow`), and not an `FnMut` trait, +/// since we sometimes have to apply type mappings lazily (e.g., to the signature of a function +/// literal). +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum TypeMapping<'a, 'db> { + /// Applies a specialization to the type + Specialization(Specialization<'db>), + /// Applies a partial specialization to the type + PartialSpecialization(PartialSpecialization<'a, 'db>), + /// Promotes any literal types to their corresponding instance types (e.g. `Literal["string"]` + /// to `str`) + PromoteLiterals, +} + +fn walk_type_mapping<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + mapping: &TypeMapping<'_, 'db>, + visitor: &mut V, +) { + match mapping { + TypeMapping::Specialization(specialization) => { + walk_specialization(db, *specialization, visitor); + } + TypeMapping::PartialSpecialization(specialization) => { + walk_partial_specialization(db, specialization, visitor); + } + TypeMapping::PromoteLiterals => {} + } +} + +impl<'db> TypeMapping<'_, 'db> { + fn to_owned(&self) -> TypeMapping<'db, 'db> { + match self { + TypeMapping::Specialization(specialization) => { + TypeMapping::Specialization(*specialization) + } + TypeMapping::PartialSpecialization(partial) => { + TypeMapping::PartialSpecialization(partial.to_owned()) + } + TypeMapping::PromoteLiterals => TypeMapping::PromoteLiterals, + } + } + + fn normalized_impl(&self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + match self { + TypeMapping::Specialization(specialization) => { + TypeMapping::Specialization(specialization.normalized_impl(db, visitor)) + } + TypeMapping::PartialSpecialization(partial) => { + TypeMapping::PartialSpecialization(partial.normalized_impl(db, visitor)) + } + TypeMapping::PromoteLiterals => TypeMapping::PromoteLiterals, + } + } +} + +/// Singleton types that are heavily special-cased by ty. Despite its name, +/// quite a different type to [`NominalInstanceType`]. +/// +/// In many ways, this enum behaves similarly to [`SpecialFormType`]. +/// Unlike instances of that variant, however, `Type::KnownInstance`s do not exist +/// at a location that can be known prior to any analysis by ty, and each variant +/// of `KnownInstanceType` can have multiple instances (as, unlike `SpecialFormType`, +/// `KnownInstanceType` variants can hold associated data). Instances of this type +/// are generally created by operations at runtime in some way, such as a type alias +/// statement, a typevar definition, or an instance of `Generic[T]` in a class's +/// bases list. +/// +/// # Ordering +/// +/// Ordering between variants is stable and should be the same between runs. +/// Ordering within variants is based on the wrapped data's salsa-assigned id and not on its values. +/// The id may change between runs, or when e.g. a `TypeVarInstance` was garbage-collected and recreated. +#[derive( + Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, Ord, PartialOrd, get_size2::GetSize, +)] +pub enum KnownInstanceType<'db> { + /// The type of `Protocol[T]`, `Protocol[U, S]`, etc -- usually only found in a class's bases list. + /// + /// Note that unsubscripted `Protocol` is represented by [`SpecialFormType::Protocol`], not this type. + SubscriptedProtocol(GenericContext<'db>), + + /// The type of `Generic[T]`, `Generic[U, S]`, etc -- usually only found in a class's bases list. + /// + /// Note that unsubscripted `Generic` is represented by [`SpecialFormType::Generic`], not this type. + SubscriptedGeneric(GenericContext<'db>), + + /// A single instance of `typing.TypeVar` + TypeVar(TypeVarInstance<'db>), + + /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) + TypeAliasType(TypeAliasType<'db>), +} + +fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + known_instance: KnownInstanceType<'db>, + visitor: &mut V, +) { + match known_instance { + KnownInstanceType::SubscriptedProtocol(context) + | KnownInstanceType::SubscriptedGeneric(context) => { + walk_generic_context(db, context, visitor); + } + KnownInstanceType::TypeVar(typevar) => { + visitor.visit_type_var_type(db, typevar); + } + KnownInstanceType::TypeAliasType(type_alias) => { + visitor.visit_type_alias_type(db, type_alias); + } + } +} + +impl<'db> KnownInstanceType<'db> { + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + match self { + Self::SubscriptedProtocol(context) => { + Self::SubscriptedProtocol(context.normalized_impl(db, visitor)) + } + Self::SubscriptedGeneric(context) => { + Self::SubscriptedGeneric(context.normalized_impl(db, visitor)) + } + Self::TypeVar(typevar) => Self::TypeVar(typevar.normalized_impl(db, visitor)), + Self::TypeAliasType(type_alias) => { + Self::TypeAliasType(type_alias.normalized_impl(db, visitor)) + } + } + } + + const fn class(self) -> KnownClass { + match self { + Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, + Self::TypeVar(_) => KnownClass::TypeVar, + Self::TypeAliasType(_) => KnownClass::TypeAliasType, + } + } + + fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + self.class().to_class_literal(db) + } + + /// Return the instance type which this type is a subtype of. + /// + /// For example, an alias created using the `type` statement is an instance of + /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)` + /// returns `Type::NominalInstance(NominalInstanceType { class: })`. + fn instance_fallback(self, db: &dyn Db) -> Type { + self.class().to_instance(db) + } + + /// Return `true` if this symbol is an instance of `class`. + fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { + self.class().is_subclass_of(db, class) + } + + /// Return the repr of the symbol at runtime + fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { + struct KnownInstanceRepr<'db> { + known_instance: KnownInstanceType<'db>, + db: &'db dyn Db, + } + + impl std::fmt::Display for KnownInstanceRepr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.known_instance { + KnownInstanceType::SubscriptedProtocol(generic_context) => { + f.write_str("typing.Protocol")?; + generic_context.display(self.db).fmt(f) + } + KnownInstanceType::SubscriptedGeneric(generic_context) => { + f.write_str("typing.Generic")?; + generic_context.display(self.db).fmt(f) + } + KnownInstanceType::TypeAliasType(_) => f.write_str("typing.TypeAliasType"), + // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render + // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll + // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. + KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"), + } + } + } + + KnownInstanceRepr { + known_instance: self, + db, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub enum DynamicType { + /// An explicitly annotated `typing.Any` + Any, + /// An unannotated value, or a dynamic type resulting from an error + Unknown, + /// Temporary type for symbols that can't be inferred yet because of missing implementations. + /// + /// This variant should eventually be removed once ty is spec-compliant. + /// + /// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the + /// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not + /// `Todo` would change the output type. + /// + /// This variant should be created with the `todo_type!` macro. + Todo(TodoType), + /// A special Todo-variant for PEP-695 `ParamSpec` types. A temporary variant to detect and special- + /// case the handling of these types in `Callable` annotations. + TodoPEP695ParamSpec, +} + +impl DynamicType { + #[expect(clippy::unused_self)] + fn normalized(self) -> Self { + Self::Any + } +} + +impl std::fmt::Display for DynamicType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DynamicType::Any => f.write_str("Any"), + DynamicType::Unknown => f.write_str("Unknown"), + // `DynamicType::Todo`'s display should be explicit that is not a valid display of + // any other type + DynamicType::Todo(todo) => write!(f, "@Todo{todo}"), + DynamicType::TodoPEP695ParamSpec => { + if cfg!(debug_assertions) { + f.write_str("@Todo(ParamSpec)") + } else { + f.write_str("@Todo") + } + } + } + } +} + +bitflags! { + /// Type qualifiers that appear in an annotation expression. + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash)] + pub(crate) struct TypeQualifiers: u8 { + /// `typing.ClassVar` + const CLASS_VAR = 1 << 0; + /// `typing.Final` + const FINAL = 1 << 1; + } +} + +impl get_size2::GetSize for TypeQualifiers {} + +/// When inferring the type of an annotation expression, we can also encounter type qualifiers +/// such as `ClassVar` or `Final`. These do not affect the inferred type itself, but rather +/// control how a particular place can be accessed or modified. This struct holds a type and +/// a set of type qualifiers. +/// +/// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the +/// qualifier `ClassVar`. +#[derive(Clone, Debug, Copy, Eq, PartialEq, salsa::Update, get_size2::GetSize)] +pub(crate) struct TypeAndQualifiers<'db> { + inner: Type<'db>, + qualifiers: TypeQualifiers, +} + +impl<'db> TypeAndQualifiers<'db> { + pub(crate) fn new(inner: Type<'db>, qualifiers: TypeQualifiers) -> Self { + Self { inner, qualifiers } + } + + /// Constructor that creates a [`TypeAndQualifiers`] instance with type `Unknown` and no qualifiers. + pub(crate) fn unknown() -> Self { + Self { + inner: Type::unknown(), + qualifiers: TypeQualifiers::empty(), + } + } + + /// Forget about type qualifiers and only return the inner type. + pub(crate) fn inner_type(&self) -> Type<'db> { + self.inner + } + + /// Insert/add an additional type qualifier. + pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) { + self.qualifiers |= qualifier; + } + + /// Return the set of type qualifiers. + pub(crate) fn qualifiers(&self) -> TypeQualifiers { + self.qualifiers + } +} + +impl<'db> From> for TypeAndQualifiers<'db> { + fn from(inner: Type<'db>) -> Self { + Self { + inner, + qualifiers: TypeQualifiers::empty(), + } + } +} + +/// Error struct providing information on type(s) that were deemed to be invalid +/// in a type expression context, and the type we should therefore fallback to +/// for the problematic type expression. +#[derive(Debug, PartialEq, Eq)] +pub struct InvalidTypeExpressionError<'db> { + fallback_type: Type<'db>, + invalid_expressions: smallvec::SmallVec<[InvalidTypeExpression<'db>; 1]>, +} + +impl<'db> InvalidTypeExpressionError<'db> { + fn into_fallback_type( + self, + context: &InferContext, + node: &ast::Expr, + is_reachable: bool, + ) -> Type<'db> { + let InvalidTypeExpressionError { + fallback_type, + invalid_expressions, + } = self; + if is_reachable { + for error in invalid_expressions { + let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else { + continue; + }; + let diagnostic = builder.into_diagnostic(error.reason(context.db())); + error.add_subdiagnostics(context.db(), diagnostic); + } + } + fallback_type + } +} + +/// Enumeration of various types that are invalid in type-expression contexts +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum InvalidTypeExpression<'db> { + /// Some types always require exactly one argument when used in a type expression + RequiresOneArgument(Type<'db>), + /// Some types always require at least one argument when used in a type expression + RequiresArguments(Type<'db>), + /// Some types always require at least two arguments when used in a type expression + RequiresTwoArguments(Type<'db>), + /// The `Protocol` class is invalid in type expressions + Protocol, + /// Same for `Generic` + Generic, + /// Type qualifiers are always invalid in *type expressions*, + /// but these ones are okay with 0 arguments in *annotation expressions* + TypeQualifier(SpecialFormType), + /// Type qualifiers that are invalid in type expressions, + /// and which would require exactly one argument even if they appeared in an annotation expression + TypeQualifierRequiresOneArgument(SpecialFormType), + /// Some types are always invalid in type expressions + InvalidType(Type<'db>, ScopeId<'db>), +} + +impl<'db> InvalidTypeExpression<'db> { + const fn reason(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { + struct Display<'db> { + error: InvalidTypeExpression<'db>, + db: &'db dyn Db, + } + + impl std::fmt::Display for Display<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.error { + InvalidTypeExpression::RequiresOneArgument(ty) => write!( + f, + "`{ty}` requires exactly one argument when used in a type expression", + ty = ty.display(self.db) + ), + InvalidTypeExpression::RequiresArguments(ty) => write!( + f, + "`{ty}` requires at least one argument when used in a type expression", + ty = ty.display(self.db) + ), + InvalidTypeExpression::RequiresTwoArguments(ty) => write!( + f, + "`{ty}` requires at least two arguments when used in a type expression", + ty = ty.display(self.db) + ), + InvalidTypeExpression::Protocol => { + f.write_str("`typing.Protocol` is not allowed in type expressions") + } + InvalidTypeExpression::Generic => { + f.write_str("`typing.Generic` is not allowed in type expressions") + } + InvalidTypeExpression::TypeQualifier(qualifier) => write!( + f, + "Type qualifier `{qualifier}` is not allowed in type expressions \ + (only in annotation expressions)", + ), + InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!( + f, + "Type qualifier `{qualifier}` is not allowed in type expressions \ + (only in annotation expressions, and only with exactly one argument)", + ), + InvalidTypeExpression::InvalidType(ty, _) => write!( + f, + "Variable of type `{ty}` is not allowed in a type expression", + ty = ty.display(self.db) + ), + } + } + } + + Display { error: self, db } + } + + fn add_subdiagnostics(self, db: &'db dyn Db, mut diagnostic: LintDiagnosticGuard) { + let InvalidTypeExpression::InvalidType(ty, scope) = self else { + return; + }; + let Type::ModuleLiteral(module_type) = ty else { + return; + }; + let module = module_type.module(db); + let Some(module_name_final_part) = module.name().components().next_back() else { + return; + }; + let Some(module_member_with_same_name) = ty + .member(db, module_name_final_part) + .place + .ignore_possibly_unbound() + else { + return; + }; + if module_member_with_same_name + .in_type_expression(db, scope) + .is_err() + { + return; + } + + // TODO: showing a diff (and even having an autofix) would be even better + diagnostic.info(format_args!( + "Did you mean to use the module's member \ + `{module_name_final_part}.{module_name_final_part}` instead?" + )); + } +} + +/// Whether this typecar was created via the legacy `TypeVar` constructor, or using PEP 695 syntax. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum TypeVarKind { + Legacy, + Pep695, +} + +/// Data regarding a single type variable. +/// +/// This is referenced by `KnownInstanceType::TypeVar` (to represent the singleton type of the +/// runtime `typing.TypeVar` object itself), and by `Type::TypeVar` to represent the type that this +/// typevar represents as an annotation: that is, an unknown set of objects, constrained by the +/// upper-bound/constraints on this type var, defaulting to the default type of this type var when +/// not otherwise bound to a type. +/// +/// # Ordering +/// Ordering is based on the type var instance's salsa-assigned id and not on its values. +/// The id may change between runs, or when the type var instance was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct TypeVarInstance<'db> { + /// The name of this TypeVar (e.g. `T`) + #[returns(ref)] + name: ast::name::Name, + + /// The type var's definition (None if synthesized) + pub definition: Option>, + + /// The upper bound or constraint on the type of this TypeVar + bound_or_constraints: Option>, + + /// The variance of the TypeVar + variance: TypeVarVariance, + + /// The default type for this TypeVar + default_ty: Option>, + + pub kind: TypeVarKind, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypeVarInstance<'_> {} + +fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_var: TypeVarInstance<'db>, + visitor: &mut V, +) { + if let Some(bounds) = type_var.bound_or_constraints(db) { + walk_type_var_bounds(db, bounds, visitor); + } + if let Some(default_type) = type_var.default_ty(db) { + visitor.visit_type(db, default_type); + } +} + +impl<'db> TypeVarInstance<'db> { + pub(crate) fn is_legacy(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), TypeVarKind::Legacy) + } + + pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { + if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { + Some(ty) + } else { + None + } + } + + pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { + if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { + Some(tuple.elements(db)) + } else { + None + } + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self::new( + db, + self.name(db), + self.definition(db), + self.bound_or_constraints(db) + .map(|b| b.normalized_impl(db, visitor)), + self.variance(db), + self.default_ty(db).map(|d| d.normalized_impl(db, visitor)), + self.kind(db), + ) + } + + fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::new( + db, + self.name(db), + self.definition(db), + self.bound_or_constraints(db) + .map(|b| b.materialize(db, variance)), + self.variance(db), + self.default_ty(db), + self.kind(db), + ) + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)] +pub enum TypeVarVariance { + Invariant, + Covariant, + Contravariant, + Bivariant, +} + +impl TypeVarVariance { + /// Flips the polarity of the variance. + /// + /// Covariant becomes contravariant, contravariant becomes covariant, others remain unchanged. + pub(crate) const fn flip(self) -> Self { + match self { + TypeVarVariance::Invariant => TypeVarVariance::Invariant, + TypeVarVariance::Covariant => TypeVarVariance::Contravariant, + TypeVarVariance::Contravariant => TypeVarVariance::Covariant, + TypeVarVariance::Bivariant => TypeVarVariance::Bivariant, + } + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)] +pub enum TypeVarBoundOrConstraints<'db> { + UpperBound(Type<'db>), + Constraints(UnionType<'db>), +} + +fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + bounds: TypeVarBoundOrConstraints<'db>, + visitor: &mut V, +) { + match bounds { + TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound), + TypeVarBoundOrConstraints::Constraints(constraints) => { + visitor.visit_union_type(db, constraints); + } + } +} + +impl<'db> TypeVarBoundOrConstraints<'db> { + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + match self { + TypeVarBoundOrConstraints::UpperBound(bound) => { + TypeVarBoundOrConstraints::UpperBound(bound.normalized_impl(db, visitor)) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints(constraints.normalized_impl(db, visitor)) + } + } + } + + fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + match self { + TypeVarBoundOrConstraints::UpperBound(bound) => { + TypeVarBoundOrConstraints::UpperBound(bound.materialize(db, variance)) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints(UnionType::new( + db, + constraints + .elements(db) + .iter() + .map(|ty| ty.materialize(db, variance)) + .collect::>() + .into_boxed_slice(), + )) + } + } + } +} + +/// Error returned if a type is not (or may not be) a context manager. +#[derive(Debug)] +enum ContextManagerError<'db> { + Enter(CallDunderError<'db>), + Exit { + enter_return_type: Type<'db>, + exit_error: CallDunderError<'db>, + }, + EnterAndExit { + enter_error: CallDunderError<'db>, + exit_error: CallDunderError<'db>, + }, +} + +impl<'db> ContextManagerError<'db> { + fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { + self.enter_type(db).unwrap_or(Type::unknown()) + } + + /// Returns the `__enter__` return type if it is known, + /// or `None` if the type never has a callable `__enter__` attribute + fn enter_type(&self, db: &'db dyn Db) -> Option> { + match self { + Self::Exit { + enter_return_type, + exit_error: _, + } => Some(*enter_return_type), + Self::Enter(enter_error) + | Self::EnterAndExit { + enter_error, + exit_error: _, + } => match enter_error { + CallDunderError::PossiblyUnbound(call_outcome) => { + Some(call_outcome.return_type(db)) + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => None, + CallDunderError::CallError(_, bindings) => Some(bindings.return_type(db)), + CallDunderError::MethodNotAvailable => None, + }, + } + } + + fn report_diagnostic( + &self, + context: &InferContext<'db, '_>, + context_expression_type: Type<'db>, + context_expression_node: ast::AnyNodeRef, + ) { + let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node) + else { + return; + }; + + let format_call_dunder_error = |call_dunder_error: &CallDunderError<'db>, name: &str| { + match call_dunder_error { + CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"), + CallDunderError::PossiblyUnbound(_) => { + format!("the method `{name}` is possibly unbound") + } + // TODO: Use more specific error messages for the different error cases. + // E.g. hint toward the union variant that doesn't correctly implement enter, + // distinguish between a not callable `__enter__` attribute and a wrong signature. + CallDunderError::CallError(_, _) => { + format!("it does not correctly implement `{name}`") + } + } + }; + + let format_call_dunder_errors = |error_a: &CallDunderError<'db>, + name_a: &str, + error_b: &CallDunderError<'db>, + name_b: &str| { + match (error_a, error_b) { + (CallDunderError::PossiblyUnbound(_), CallDunderError::PossiblyUnbound(_)) => { + format!("the methods `{name_a}` and `{name_b}` are possibly unbound") + } + (CallDunderError::MethodNotAvailable, CallDunderError::MethodNotAvailable) => { + format!("it does not implement `{name_a}` and `{name_b}`") + } + (CallDunderError::CallError(_, _), CallDunderError::CallError(_, _)) => { + format!("it does not correctly implement `{name_a}` or `{name_b}`") + } + (_, _) => format!( + "{format_a}, and {format_b}", + format_a = format_call_dunder_error(error_a, name_a), + format_b = format_call_dunder_error(error_b, name_b) + ), + } + }; + + let db = context.db(); + + let formatted_errors = match self { + Self::Exit { + enter_return_type: _, + exit_error, + } => format_call_dunder_error(exit_error, "__exit__"), + Self::Enter(enter_error) => format_call_dunder_error(enter_error, "__enter__"), + Self::EnterAndExit { + enter_error, + exit_error, + } => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"), + }; + + let mut diag = builder.into_diagnostic( + format_args!( + "Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}", + context_expression = context_expression_type.display(db) + ), + ); + + // If `__aenter__` and `__aexit__` are available, the user may have intended to use `async with` instead of `with`: + if let ( + Ok(_) | Err(CallDunderError::CallError(..)), + Ok(_) | Err(CallDunderError::CallError(..)), + ) = ( + context_expression_type.try_call_dunder(db, "__aenter__", CallArgumentTypes::none()), + context_expression_type.try_call_dunder( + db, + "__aexit__", + CallArgumentTypes::positional([Type::unknown(), Type::unknown(), Type::unknown()]), + ), + ) { + diag.info(format_args!( + "Objects of type `{context_expression}` can be used as async context managers", + context_expression = context_expression_type.display(db) + )); + diag.info("Consider using `async with` here"); + } + } +} + +/// Error returned if a type is not (or may not be) iterable. +#[derive(Debug)] +enum IterationError<'db> { + /// The object being iterated over has a bound `__iter__` method, + /// but calling it with the expected arguments results in an error. + IterCallError(CallErrorKind, Box>), + + /// The object being iterated over has a bound `__iter__` method that can be called + /// with the expected types, but it returns an object that is not a valid iterator. + IterReturnsInvalidIterator { + /// The type of the object returned by the `__iter__` method. + iterator: Type<'db>, + /// The error we encountered when we tried to call `__next__` on the type + /// returned by `__iter__` + dunder_next_error: CallDunderError<'db>, + }, + + /// The object being iterated over has a bound `__iter__` method that returns a + /// valid iterator. However, the `__iter__` method is possibly unbound, and there + /// either isn't a `__getitem__` method to fall back to, or calling the `__getitem__` + /// method returns some kind of error. + PossiblyUnboundIterAndGetitemError { + /// The type of the object returned by the `__next__` method on the iterator. + /// (The iterator being the type returned by the `__iter__` method on the iterable.) + dunder_next_return: Type<'db>, + /// The error we encountered when we tried to call `__getitem__` on the iterable. + dunder_getitem_error: CallDunderError<'db>, + }, + + /// The object being iterated over doesn't have an `__iter__` method. + /// It also either doesn't have a `__getitem__` method to fall back to, + /// or calling the `__getitem__` method returns some kind of error. + UnboundIterAndGetitemError { + dunder_getitem_error: CallDunderError<'db>, + }, +} + +impl<'db> IterationError<'db> { + fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { + self.element_type(db).unwrap_or(Type::unknown()) + } + + /// Returns the element type if it is known, or `None` if the type is never iterable. + fn element_type(&self, db: &'db dyn Db) -> Option> { + match self { + Self::IterReturnsInvalidIterator { + dunder_next_error, .. + } => dunder_next_error.return_type(db), + + Self::IterCallError(_, dunder_iter_bindings) => dunder_iter_bindings + .return_type(db) + .try_call_dunder(db, "__next__", CallArgumentTypes::none()) + .map(|dunder_next_outcome| Some(dunder_next_outcome.return_type(db))) + .unwrap_or_else(|dunder_next_call_error| dunder_next_call_error.return_type(db)), + + Self::PossiblyUnboundIterAndGetitemError { + dunder_next_return, + dunder_getitem_error, + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => Some(*dunder_next_return), + CallDunderError::PossiblyUnbound(dunder_getitem_outcome) => { + Some(UnionType::from_elements( + db, + [*dunder_next_return, dunder_getitem_outcome.return_type(db)], + )) + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + Some(*dunder_next_return) + } + CallDunderError::CallError(_, dunder_getitem_bindings) => { + let dunder_getitem_return = dunder_getitem_bindings.return_type(db); + let elements = [*dunder_next_return, dunder_getitem_return]; + Some(UnionType::from_elements(db, elements)) + } + }, + + Self::UnboundIterAndGetitemError { + dunder_getitem_error, + } => dunder_getitem_error.return_type(db), + } + } + + /// Reports the diagnostic for this error. + fn report_diagnostic( + &self, + context: &InferContext<'db, '_>, + iterable_type: Type<'db>, + iterable_node: ast::AnyNodeRef, + ) { + /// A little helper type for emitting a diagnostic + /// based on the variant of iteration error. + struct Reporter<'a> { + db: &'a dyn Db, + builder: LintDiagnosticGuardBuilder<'a, 'a>, + iterable_type: Type<'a>, + } + + impl<'a> Reporter<'a> { + /// Emit a diagnostic that is certain that `iterable_type` is not iterable. + /// + /// `because` should explain why `iterable_type` is not iterable. + #[expect(clippy::wrong_self_convention)] + fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { + let mut diag = self.builder.into_diagnostic(format_args!( + "Object of type `{iterable_type}` is not iterable", + iterable_type = self.iterable_type.display(self.db), + )); + diag.info(because); + diag + } + + /// Emit a diagnostic that is uncertain that `iterable_type` is not iterable. + /// + /// `because` should explain why `iterable_type` is likely not iterable. + fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { + let mut diag = self.builder.into_diagnostic(format_args!( + "Object of type `{iterable_type}` may not be iterable", + iterable_type = self.iterable_type.display(self.db), + )); + diag.info(because); + diag + } + } + + let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { + return; + }; + let db = context.db(); + let reporter = Reporter { + db, + builder, + iterable_type, + }; + + // TODO: for all of these error variants, the "explanation" for the diagnostic + // (everything after the "because") should really be presented as a "help:", "note", + // or similar, rather than as part of the same sentence as the error message. + match self { + Self::IterCallError(CallErrorKind::NotCallable, bindings) => { + reporter.is_not(format_args!( + "Its `__iter__` attribute has type `{dunder_iter_type}`, which is not callable", + dunder_iter_type = bindings.callable_type().display(db), + )); + } + Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not(format_args!( + "Its `__iter__` attribute (with type `{dunder_iter_type}`) \ + may not be callable", + dunder_iter_type = bindings.callable_type().display(db), + )); + } + Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) => { + reporter.may_not(format_args!( + "Its `__iter__` attribute (with type `{dunder_iter_type}`) \ + may not be callable", + dunder_iter_type = bindings.callable_type().display(db), + )); + } + Self::IterCallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => { + reporter + .is_not("Its `__iter__` method has an invalid signature") + .info("Expected signature `def __iter__(self): ...`"); + } + Self::IterCallError(CallErrorKind::BindingError, bindings) => { + let mut diag = + reporter.may_not("Its `__iter__` method may have an invalid signature"); + diag.info(format_args!( + "Type of `__iter__` is `{dunder_iter_type}`", + dunder_iter_type = bindings.callable_type().display(db), + )); + diag.info("Expected signature for `__iter__` is `def __iter__(self): ...`"); + } + + Self::IterReturnsInvalidIterator { + iterator, + dunder_next_error, + } => match dunder_next_error { + CallDunderError::MethodNotAvailable => { + reporter.is_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has no `__next__` method", + iterator_type = iterator.display(db), + )); + } + CallDunderError::PossiblyUnbound(_) => { + reporter.may_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which may not have a `__next__` method", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + reporter.is_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has a `__next__` attribute that is not callable", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => { + reporter.may_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has a `__next__` attribute that may not be callable", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .is_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has an invalid `__next__` method", + iterator_type = iterator.display(db), + )) + .info("Expected signature for `__next__` is `def __next__(self): ...`"); + } + CallDunderError::CallError(CallErrorKind::BindingError, _) => { + reporter + .may_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which may have an invalid `__next__` method", + iterator_type = iterator.display(db), + )) + .info("Expected signature for `__next__` is `def __next__(self): ...`)"); + } + }, + + Self::PossiblyUnboundIterAndGetitemError { + dunder_getitem_error, + .. + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => { + reporter.may_not( + "It may not have an `__iter__` method \ + and it doesn't have a `__getitem__` method", + ); + } + CallDunderError::PossiblyUnbound(_) => { + reporter + .may_not("It may not have an `__iter__` method or a `__getitem__` method"); + } + CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { + reporter.may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute may not be callable", + ); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { + reporter.may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \ + may not be callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .may_not( + "It may not have an `__iter__` method \ + and its `__getitem__` method has an incorrect signature \ + for the old-style iteration protocol", + ) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { + reporter + .may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` method (with type `{dunder_getitem_type}`) \ + may have an incorrect signature for the old-style iteration protocol", + dunder_getitem_type = bindings.callable_type().display(db), + )) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + }, + + Self::UnboundIterAndGetitemError { + dunder_getitem_error, + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => { + reporter + .is_not("It doesn't have an `__iter__` method or a `__getitem__` method"); + } + CallDunderError::PossiblyUnbound(_) => { + reporter.is_not( + "It has no `__iter__` method and it may not have a `__getitem__` method", + ); + } + CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { + reporter.is_not(format_args!( + "It has no `__iter__` method and \ + its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not( + "It has no `__iter__` method and its `__getitem__` attribute \ + may not be callable", + ); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { + reporter.may_not( + "It has no `__iter__` method and its `__getitem__` attribute is invalid", + ).info(format_args!( + "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .is_not( + "It has no `__iter__` method and \ + its `__getitem__` method has an incorrect signature \ + for the old-style iteration protocol", + ) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { + reporter + .may_not(format_args!( + "It has no `__iter__` method and \ + its `__getitem__` method (with type `{dunder_getitem_type}`) \ + may have an incorrect signature for the old-style iteration protocol", + dunder_getitem_type = bindings.callable_type().display(db), + )) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum BoolError<'db> { + /// The type has a `__bool__` attribute but it can't be called. + NotCallable { not_boolable_type: Type<'db> }, + + /// The type has a callable `__bool__` attribute, but it isn't callable + /// with the given arguments. + IncorrectArguments { + not_boolable_type: Type<'db>, + truthiness: Truthiness, + }, + + /// The type has a `__bool__` method, is callable with the given arguments, + /// but the return type isn't assignable to `bool`. + IncorrectReturnType { + not_boolable_type: Type<'db>, + return_type: Type<'db>, + }, + + /// A union type doesn't implement `__bool__` correctly. + Union { + union: UnionType<'db>, + truthiness: Truthiness, + }, + + /// Any other reason why the type can't be converted to a bool. + /// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or + /// because `__bool__` points to a type that has a possibly unbound `__call__` method. + Other { not_boolable_type: Type<'db> }, +} + +impl<'db> BoolError<'db> { + pub(super) fn fallback_truthiness(&self) -> Truthiness { + match self { + BoolError::NotCallable { .. } + | BoolError::IncorrectReturnType { .. } + | BoolError::Other { .. } => Truthiness::Ambiguous, + BoolError::IncorrectArguments { truthiness, .. } + | BoolError::Union { truthiness, .. } => *truthiness, + } + } + + fn not_boolable_type(&self) -> Type<'db> { + match self { + BoolError::NotCallable { + not_boolable_type, .. + } + | BoolError::IncorrectArguments { + not_boolable_type, .. + } + | BoolError::Other { not_boolable_type } + | BoolError::IncorrectReturnType { + not_boolable_type, .. + } => *not_boolable_type, + BoolError::Union { union, .. } => Type::Union(*union), + } + } + + pub(super) fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) { + self.report_diagnostic_impl(context, condition.range()); + } + + fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { + let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else { + return; + }; + match self { + Self::IncorrectArguments { + not_boolable_type, .. + } => { + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`", + not_boolable_type.display(context.db()) + )); + let mut sub = SubDiagnostic::new( + Severity::Info, + "`__bool__` methods must only have a `self` parameter", + ); + if let Some((func_span, parameter_span)) = not_boolable_type + .member(context.db(), "__bool__") + .into_lookup_result() + .ok() + .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) + { + sub.annotate( + Annotation::primary(parameter_span).message("Incorrect parameters"), + ); + sub.annotate(Annotation::secondary(func_span).message("Method defined here")); + } + diag.sub(sub); + } + Self::IncorrectReturnType { + not_boolable_type, + return_type, + } => { + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{not_boolable}`", + not_boolable = not_boolable_type.display(context.db()), + )); + let mut sub = SubDiagnostic::new( + Severity::Info, + format_args!( + "`{return_type}` is not assignable to `bool`", + return_type = return_type.display(context.db()), + ), + ); + if let Some((func_span, return_type_span)) = not_boolable_type + .member(context.db(), "__bool__") + .into_lookup_result() + .ok() + .and_then(|quals| quals.inner_type().function_spans(context.db())) + .and_then(|spans| Some((spans.name, spans.return_type?))) + { + sub.annotate( + Annotation::primary(return_type_span).message("Incorrect return type"), + ); + sub.annotate(Annotation::secondary(func_span).message("Method defined here")); + } + diag.sub(sub); + } + Self::NotCallable { not_boolable_type } => { + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`", + not_boolable_type.display(context.db()) + )); + let sub = SubDiagnostic::new( + Severity::Info, + format_args!( + "`__bool__` on `{}` must be callable", + not_boolable_type.display(context.db()) + ), + ); + // TODO: It would be nice to create an annotation here for + // where `__bool__` is defined. At time of writing, I couldn't + // figure out a straight-forward way of doing this. ---AG + diag.sub(sub); + } + Self::Union { union, .. } => { + let first_error = union + .elements(context.db()) + .iter() + .find_map(|element| element.try_bool(context.db()).err()) + .unwrap(); + + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for union `{}` \ + because `{}` doesn't implement `__bool__` correctly", + Type::Union(*union).display(context.db()), + first_error.not_boolable_type().display(context.db()), + )); + } + + Self::Other { not_boolable_type } => { + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`; \ + it incorrectly implements `__bool__`", + not_boolable_type.display(context.db()) + )); + } + } + } +} + +/// Represents possibly failure modes of implicit `__new__` calls. +#[derive(Debug)] +enum DunderNewCallError<'db> { + /// The call to `__new__` failed. + CallError(CallError<'db>), + /// The `__new__` method could be unbound. If the call to the + /// method has also failed, this variant also includes the + /// corresponding `CallError`. + PossiblyUnbound(Option>), +} + +/// Error returned if a class instantiation call failed +#[derive(Debug)] +enum ConstructorCallError<'db> { + Init(Type<'db>, CallDunderError<'db>), + New(Type<'db>, DunderNewCallError<'db>), + NewAndInit(Type<'db>, DunderNewCallError<'db>, CallDunderError<'db>), +} + +impl<'db> ConstructorCallError<'db> { + fn return_type(&self) -> Type<'db> { + match self { + Self::Init(ty, _) => *ty, + Self::New(ty, _) => *ty, + Self::NewAndInit(ty, _, _) => *ty, + } + } + + fn report_diagnostic( + &self, + context: &InferContext<'db, '_>, + context_expression_type: Type<'db>, + context_expression_node: ast::AnyNodeRef, + ) { + let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { + CallDunderError::MethodNotAvailable => { + if let Some(builder) = + context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, context_expression_node) + { + // If we are using vendored typeshed, it should be impossible to have missing + // or unbound `__init__` method on a class, as all classes have `object` in MRO. + // Thus the following may only trigger if a custom typeshed is used. + builder.into_diagnostic(format_args!( + "`__init__` method is missing on type `{}`. \ + Make sure your `object` in typeshed has its definition.", + context_expression_type.display(context.db()), + )); + } + } + CallDunderError::PossiblyUnbound(bindings) => { + if let Some(builder) = + context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, context_expression_node) + { + builder.into_diagnostic(format_args!( + "Method `__init__` on type `{}` is possibly unbound.", + context_expression_type.display(context.db()), + )); + } + + bindings.report_diagnostics(context, context_expression_node); + } + CallDunderError::CallError(_, bindings) => { + bindings.report_diagnostics(context, context_expression_node); + } + }; + + let report_new_error = |error: &DunderNewCallError<'db>| match error { + DunderNewCallError::PossiblyUnbound(call_error) => { + if let Some(builder) = + context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, context_expression_node) + { + builder.into_diagnostic(format_args!( + "Method `__new__` on type `{}` is possibly unbound.", + context_expression_type.display(context.db()), + )); + } + + if let Some(CallError(_kind, bindings)) = call_error { + bindings.report_diagnostics(context, context_expression_node); + } + } + DunderNewCallError::CallError(CallError(_kind, bindings)) => { + bindings.report_diagnostics(context, context_expression_node); + } + }; + + match self { + Self::Init(_, init_call_dunder_error) => { + report_init_error(init_call_dunder_error); + } + Self::New(_, new_call_error) => { + report_new_error(new_call_error); + } + Self::NewAndInit(_, new_call_error, init_call_dunder_error) => { + report_new_error(new_call_error); + report_init_error(init_call_dunder_error); + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum TypeRelation { + Subtyping, + Assignability, +} + +impl TypeRelation { + pub(crate) const fn is_assignability(self) -> bool { + matches!(self, TypeRelation::Assignability) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Truthiness { + /// For an object `x`, `bool(x)` will always return `True` + AlwaysTrue, + /// For an object `x`, `bool(x)` will always return `False` + AlwaysFalse, + /// For an object `x`, `bool(x)` could return either `True` or `False` + Ambiguous, +} + +impl Truthiness { + pub(crate) const fn is_ambiguous(self) -> bool { + matches!(self, Truthiness::Ambiguous) + } + + pub(crate) const fn is_always_false(self) -> bool { + matches!(self, Truthiness::AlwaysFalse) + } + + pub(crate) const fn may_be_true(self) -> bool { + !self.is_always_false() + } + + pub(crate) const fn is_always_true(self) -> bool { + matches!(self, Truthiness::AlwaysTrue) + } + + pub(crate) const fn negate(self) -> Self { + match self { + Self::AlwaysTrue => Self::AlwaysFalse, + Self::AlwaysFalse => Self::AlwaysTrue, + Self::Ambiguous => Self::Ambiguous, + } + } + + pub(crate) const fn negate_if(self, condition: bool) -> Self { + if condition { self.negate() } else { self } + } + + pub(crate) fn and(self, other: Self) -> Self { + match (self, other) { + (Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue, + (Truthiness::AlwaysFalse, _) | (_, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse, + _ => Truthiness::Ambiguous, + } + } + + pub(crate) fn or(self, other: Self) -> Self { + match (self, other) { + (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => Truthiness::AlwaysFalse, + (Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => Truthiness::AlwaysTrue, + _ => Truthiness::Ambiguous, + } + } + + fn into_type(self, db: &dyn Db) -> Type { + match self { + Self::AlwaysTrue => Type::BooleanLiteral(true), + Self::AlwaysFalse => Type::BooleanLiteral(false), + Self::Ambiguous => KnownClass::Bool.to_instance(db), + } + } +} + +impl From for Truthiness { + fn from(value: bool) -> Self { + if value { + Truthiness::AlwaysTrue + } else { + Truthiness::AlwaysFalse + } + } +} + +/// This type represents bound method objects that are created when a method is accessed +/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates +/// a bound method object that represents the `Path.touch` method which is bound to the +/// instance `Path("a.txt")`. +/// +/// # Ordering +/// Ordering is based on the bounded method's salsa-assigned id and not on its values. +/// The id may change between runs, or when the bounded method was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct BoundMethodType<'db> { + /// The function that is being bound. Corresponds to the `__func__` attribute on a + /// bound method object + pub(crate) function: FunctionType<'db>, + /// The instance on which this method has been called. Corresponds to the `__self__` + /// attribute on a bound method object + self_instance: Type<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for BoundMethodType<'_> {} + +fn walk_bound_method_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + method: BoundMethodType<'db>, + visitor: &mut V, +) { + visitor.visit_function_type(db, method.function(db)); + visitor.visit_type(db, method.self_instance(db)); +} + +impl<'db> BoundMethodType<'db> { + pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { + CallableType::new( + db, + CallableSignature::from_overloads( + self.function(db) + .signature(db) + .overloads + .iter() + .map(signatures::Signature::bind_self), + ), + false, + ) + } + + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + Self::new( + db, + self.function(db).normalized_impl(db, visitor), + self.self_instance(db).normalized_impl(db, visitor), + ) + } + + fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool { + // A bound method is a typically a subtype of itself. However, we must explicitly verify + // the subtyping of the underlying function signatures (since they might be specialized + // differently), and of the bound self parameter (taking care that parameters, including a + // bound self parameter, are contravariant.) + self.function(db) + .has_relation_to(db, other.function(db), relation) + && other + .self_instance(db) + .has_relation_to(db, self.self_instance(db), relation) + } + + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.function(db).is_equivalent_to(db, other.function(db)) + && other + .self_instance(db) + .is_equivalent_to(db, self.self_instance(db)) + } +} + +/// This type represents the set of all callable objects with a certain, possibly overloaded, +/// signature. +/// +/// It can be written in type expressions using `typing.Callable`. `lambda` expressions are +/// inferred directly as `CallableType`s; all function-literal types are subtypes of a +/// `CallableType`. +/// +/// # Ordering +/// Ordering is based on the callable type's salsa-assigned id and not on its values. +/// The id may change between runs, or when the callable type was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct CallableType<'db> { + #[returns(ref)] + pub(crate) signatures: CallableSignature<'db>, + + /// We use `CallableType` to represent function-like objects, like the synthesized methods + /// of dataclasses or NamedTuples. These callables act like real functions when accessed + /// as attributes on instances, i.e. they bind `self`. + is_function_like: bool, +} + +pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + ty: CallableType<'db>, + visitor: &mut V, +) { + for signature in &ty.signatures(db).overloads { + walk_signature(db, signature, visitor); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for CallableType<'_> {} + +impl<'db> CallableType<'db> { + /// Create a callable type with a single non-overloaded signature. + pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::new( + db, + CallableSignature::single(signature), + false, + )) + } + + /// Create a non-overloaded, function-like callable type with a single signature. + /// + /// A function-like callable will bind `self` when accessed as an attribute on an instance. + pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::new( + db, + CallableSignature::single(signature), + true, + )) + } + + /// Create a callable type which accepts any parameters and returns an `Unknown` type. + pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> { + Self::single(db, Signature::unknown()) + } + + pub(crate) fn bind_self(self, db: &'db dyn Db) -> Type<'db> { + Type::Callable(CallableType::new( + db, + self.signatures(db).bind_self(), + false, + )) + } + + fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + CallableType::new( + db, + self.signatures(db).materialize(db, variance), + self.is_function_like(db), + ) + } + + /// Create a callable type which represents a fully-static "bottom" callable. + /// + /// Specifically, this represents a callable type with a single signature: + /// `(*args: object, **kwargs: object) -> Never`. + #[cfg(test)] + pub(crate) fn bottom(db: &'db dyn Db) -> Type<'db> { + Self::single(db, Signature::bottom(db)) + } + + /// Return a "normalized" version of this `Callable` type. + /// + /// See [`Type::normalized`] for more details. + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + CallableType::new( + db, + self.signatures(db).normalized_impl(db, visitor), + self.is_function_like(db), + ) + } + + fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + CallableType::new( + db, + self.signatures(db).apply_type_mapping(db, type_mapping), + self.is_function_like(db), + ) + } + + fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.signatures(db).find_legacy_typevars(db, typevars); + } + + /// Check whether this callable type has the given relation to another callable type. + /// + /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. + fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool { + if other.is_function_like(db) && !self.is_function_like(db) { + return false; + } + self.signatures(db) + .has_relation_to(db, other.signatures(db), relation) + } + + /// Check whether this callable type is equivalent to another callable type. + /// + /// See [`Type::is_equivalent_to`] for more details. + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + if self == other { + return true; + } + + self.is_function_like(db) == other.is_function_like(db) + && self + .signatures(db) + .is_equivalent_to(db, other.signatures(db)) + } +} + +/// Represents a specific instance of `types.MethodWrapperType` +#[derive( + Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update, get_size2::GetSize, +)] +pub enum MethodWrapperKind<'db> { + /// Method wrapper for `some_function.__get__` + FunctionTypeDunderGet(FunctionType<'db>), + /// Method wrapper for `some_function.__call__` + FunctionTypeDunderCall(FunctionType<'db>), + /// Method wrapper for `some_property.__get__` + PropertyDunderGet(PropertyInstanceType<'db>), + /// Method wrapper for `some_property.__set__` + PropertyDunderSet(PropertyInstanceType<'db>), + /// Method wrapper for `str.startswith`. + /// We treat this method specially because we want to be able to infer precise Boolean + /// literal return types if the instance and the prefix are both string literals, and + /// this allows us to understand statically known branches for common tests such as + /// `if sys.platform.startswith("freebsd")`. + StrStartswith(StringLiteralType<'db>), +} + +pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + method_wrapper: MethodWrapperKind<'db>, + visitor: &mut V, +) { + match method_wrapper { + MethodWrapperKind::FunctionTypeDunderGet(function) => { + visitor.visit_function_type(db, function); + } + MethodWrapperKind::FunctionTypeDunderCall(function) => { + visitor.visit_function_type(db, function); + } + MethodWrapperKind::PropertyDunderGet(property) => { + visitor.visit_property_instance_type(db, property); + } + MethodWrapperKind::PropertyDunderSet(property) => { + visitor.visit_property_instance_type(db, property); + } + MethodWrapperKind::StrStartswith(string_literal) => { + visitor.visit_type(db, Type::StringLiteral(string_literal)); + } + } +} + +impl<'db> MethodWrapperKind<'db> { + fn has_relation_to(self, db: &'db dyn Db, other: Self, relation: TypeRelation) -> bool { + match (self, other) { + ( + MethodWrapperKind::FunctionTypeDunderGet(self_function), + MethodWrapperKind::FunctionTypeDunderGet(other_function), + ) => self_function.has_relation_to(db, other_function, relation), + + ( + MethodWrapperKind::FunctionTypeDunderCall(self_function), + MethodWrapperKind::FunctionTypeDunderCall(other_function), + ) => self_function.has_relation_to(db, other_function, relation), + + (MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_)) + | (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_)) + | (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => { + self == other + } + + ( + MethodWrapperKind::FunctionTypeDunderGet(_) + | MethodWrapperKind::FunctionTypeDunderCall(_) + | MethodWrapperKind::PropertyDunderGet(_) + | MethodWrapperKind::PropertyDunderSet(_) + | MethodWrapperKind::StrStartswith(_), + MethodWrapperKind::FunctionTypeDunderGet(_) + | MethodWrapperKind::FunctionTypeDunderCall(_) + | MethodWrapperKind::PropertyDunderGet(_) + | MethodWrapperKind::PropertyDunderSet(_) + | MethodWrapperKind::StrStartswith(_), + ) => false, + } + } + + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + match (self, other) { + ( + MethodWrapperKind::FunctionTypeDunderGet(self_function), + MethodWrapperKind::FunctionTypeDunderGet(other_function), + ) => self_function.is_equivalent_to(db, other_function), + + ( + MethodWrapperKind::FunctionTypeDunderCall(self_function), + MethodWrapperKind::FunctionTypeDunderCall(other_function), + ) => self_function.is_equivalent_to(db, other_function), + + (MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_)) + | (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_)) + | (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => { + self == other + } + + ( + MethodWrapperKind::FunctionTypeDunderGet(_) + | MethodWrapperKind::FunctionTypeDunderCall(_) + | MethodWrapperKind::PropertyDunderGet(_) + | MethodWrapperKind::PropertyDunderSet(_) + | MethodWrapperKind::StrStartswith(_), + MethodWrapperKind::FunctionTypeDunderGet(_) + | MethodWrapperKind::FunctionTypeDunderCall(_) + | MethodWrapperKind::PropertyDunderGet(_) + | MethodWrapperKind::PropertyDunderSet(_) + | MethodWrapperKind::StrStartswith(_), + ) => false, + } + } + + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + match self { + MethodWrapperKind::FunctionTypeDunderGet(function) => { + MethodWrapperKind::FunctionTypeDunderGet(function.normalized_impl(db, visitor)) + } + MethodWrapperKind::FunctionTypeDunderCall(function) => { + MethodWrapperKind::FunctionTypeDunderCall(function.normalized_impl(db, visitor)) + } + MethodWrapperKind::PropertyDunderGet(property) => { + MethodWrapperKind::PropertyDunderGet(property.normalized_impl(db, visitor)) + } + MethodWrapperKind::PropertyDunderSet(property) => { + MethodWrapperKind::PropertyDunderSet(property.normalized_impl(db, visitor)) + } + MethodWrapperKind::StrStartswith(_) => self, + } + } +} + +/// Represents a specific instance of `types.WrapperDescriptorType` +#[derive( + Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update, get_size2::GetSize, +)] +pub enum WrapperDescriptorKind { + /// `FunctionType.__get__` + FunctionTypeDunderGet, + /// `property.__get__` + PropertyDunderGet, + /// `property.__set__` + PropertyDunderSet, +} + +/// # Ordering +/// Ordering is based on the module literal's salsa-assigned id and not on its values. +/// The id may change between runs, or when the module literal was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct ModuleLiteralType<'db> { + /// The imported module. + pub module: Module, + + /// The file in which this module was imported. + /// + /// If the module is a module that could have submodules (a package), + /// we need this in order to know which submodules should be attached to it as attributes + /// (because the submodules were also imported in this file). For a package, this should + /// therefore always be `Some()`. If the module is not a package, however, this should + /// always be `None`: this helps reduce memory usage (the information is redundant for + /// single-file modules), and ensures that two module-literal types that both refer to + /// the same underlying single-file module are understood by ty as being equivalent types + /// in all situations. + _importing_file: Option, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for ModuleLiteralType<'_> {} + +impl<'db> ModuleLiteralType<'db> { + fn importing_file(self, db: &'db dyn Db) -> Option { + debug_assert_eq!( + self._importing_file(db).is_some(), + self.module(db).kind().is_package() + ); + self._importing_file(db) + } + + fn available_submodule_attributes(&self, db: &'db dyn Db) -> impl Iterator { + self.importing_file(db) + .into_iter() + .flat_map(|file| imported_modules(db, file)) + .filter_map(|submodule_name| submodule_name.relative_to(self.module(db).name())) + .filter_map(|relative_submodule| relative_submodule.components().next().map(Name::from)) + } + + fn resolve_submodule(self, db: &'db dyn Db, name: &str) -> Option> { + let importing_file = self.importing_file(db)?; + let relative_submodule_name = ModuleName::new(name)?; + let mut absolute_submodule_name = self.module(db).name().clone(); + absolute_submodule_name.extend(&relative_submodule_name); + let submodule = resolve_module(db, &absolute_submodule_name)?; + Some(Type::module_literal(db, importing_file, &submodule)) + } + + fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + // `__dict__` is a very special member that is never overridden by module globals; + // we should always look it up directly as an attribute on `types.ModuleType`, + // never in the global scope of the module. + if name == "__dict__" { + return KnownClass::ModuleType + .to_instance(db) + .member(db, "__dict__"); + } + + // If the file that originally imported the module has also imported a submodule + // named `name`, then the result is (usually) that submodule, even if the module + // also defines a (non-module) symbol with that name. + // + // Note that technically, either the submodule or the non-module symbol could take + // priority, depending on the ordering of when the submodule is loaded relative to + // the parent module's `__init__.py` file being evaluated. That said, we have + // chosen to always have the submodule take priority. (This matches pyright's + // current behavior, but is the opposite of mypy's current behavior.) + if self.available_submodule_attributes(db).contains(name) { + if let Some(submodule) = self.resolve_submodule(db, name) { + return Place::bound(submodule).into(); + } + } + + self.module(db) + .file() + .map(|file| imported_symbol(db, file, name, None)) + .unwrap_or_default() + } +} + +/// # Ordering +/// Ordering is based on the type alias's salsa-assigned id and not on its values. +/// The id may change between runs, or when the alias was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct PEP695TypeAliasType<'db> { + #[returns(ref)] + pub name: ast::name::Name, + + rhs_scope: ScopeId<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for PEP695TypeAliasType<'_> {} + +fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: PEP695TypeAliasType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, type_alias.value_type(db)); +} + +#[salsa::tracked] +impl<'db> PEP695TypeAliasType<'db> { + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let scope = self.rhs_scope(db); + let module = parsed_module(db, scope.file(db)).load(db); + let type_alias_stmt_node = scope.node(db).expect_type_alias(&module); + + semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) + } + + #[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + let scope = self.rhs_scope(db); + let module = parsed_module(db, scope.file(db)).load(db); + let type_alias_stmt_node = scope.node(db).expect_type_alias(&module); + let definition = self.definition(db); + definition_expression_type(db, definition, &type_alias_stmt_node.value) + } + + fn normalized_impl(self, _db: &'db dyn Db, _visitor: &mut TypeTransformer<'db>) -> Self { + self + } +} + +/// # Ordering +/// Ordering is based on the type alias's salsa-assigned id and not on its values. +/// The id may change between runs, or when the alias was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct BareTypeAliasType<'db> { + #[returns(ref)] + pub name: ast::name::Name, + pub definition: Option>, + pub value: Type<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for BareTypeAliasType<'_> {} + +fn walk_bare_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: BareTypeAliasType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, type_alias.value(db)); +} + +impl<'db> BareTypeAliasType<'db> { + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + Self::new( + db, + self.name(db), + self.definition(db), + self.value(db).normalized_impl(db, visitor), + ) + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update, get_size2::GetSize, +)] +pub enum TypeAliasType<'db> { + PEP695(PEP695TypeAliasType<'db>), + Bare(BareTypeAliasType<'db>), +} + +fn walk_type_alias_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + type_alias: TypeAliasType<'db>, + visitor: &mut V, +) { + match type_alias { + TypeAliasType::PEP695(type_alias) => { + walk_pep_695_type_alias(db, type_alias, visitor); + } + TypeAliasType::Bare(type_alias) => { + walk_bare_type_alias(db, type_alias, visitor); + } + } +} + +impl<'db> TypeAliasType<'db> { + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + match self { + TypeAliasType::PEP695(type_alias) => { + TypeAliasType::PEP695(type_alias.normalized_impl(db, visitor)) + } + TypeAliasType::Bare(type_alias) => { + TypeAliasType::Bare(type_alias.normalized_impl(db, visitor)) + } + } + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.name(db), + TypeAliasType::Bare(type_alias) => type_alias.name(db), + } + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + match self { + TypeAliasType::PEP695(type_alias) => Some(type_alias.definition(db)), + TypeAliasType::Bare(type_alias) => type_alias.definition(db), + } + } + + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), + TypeAliasType::Bare(type_alias) => type_alias.value(db), + } + } +} + +/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) struct MetaclassCandidate<'db> { + metaclass: ClassType<'db>, + explicit_metaclass_of: ClassLiteral<'db>, +} + +#[salsa::interned(debug)] +pub struct UnionType<'db> { + /// The union type includes values in any of these types. + #[returns(deref)] + pub elements: Box<[Type<'db>]>, +} + +pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + union: UnionType<'db>, + visitor: &mut V, +) { + for element in union.elements(db) { + visitor.visit_type(db, *element); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for UnionType<'_> {} + +impl<'db> UnionType<'db> { + /// Create a union from a list of elements + /// (which may be eagerly simplified into a different variant of [`Type`] altogether). + pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(element.into()) + }) + .build() + } + + /// A fallible version of [`UnionType::from_elements`]. + /// + /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. + /// As soon as a `None` element in the iterable is encountered, + /// the function short-circuits and returns `None`. + pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> + where + I: IntoIterator>, + T: Into>, + { + let mut builder = UnionBuilder::new(db); + for element in elements { + builder = builder.add(element?.into()); + } + Some(builder.build()) + } + + /// Apply a transformation function to all elements of the union, + /// and create a new union from the resulting set of types. + pub fn map( + &self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + Self::from_elements(db, self.elements(db).iter().map(transform_fn)) + } + + /// A fallible version of [`UnionType::map`]. + /// + /// For each element in `self`, `transform_fn` is called on that element. + /// If `transform_fn` returns `Some()` for all elements in `self`, + /// the result of unioning all transformed elements is returned. + /// As soon as `transform_fn` returns `None` for an element, however, + /// the function short-circuits and returns `None`. + pub(crate) fn try_map( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Option>, + ) -> Option> { + Self::try_from_elements(db, self.elements(db).iter().map(transform_fn)) + } + + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { + self.try_map(db, |element| element.to_instance(db)) + } + + pub(crate) fn filter( + self, + db: &'db dyn Db, + filter_fn: impl FnMut(&&Type<'db>) -> bool, + ) -> Type<'db> { + Self::from_elements(db, self.elements(db).iter().filter(filter_fn)) + } + + pub fn iter(&self, db: &'db dyn Db) -> Iter> { + self.elements(db).iter() + } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { + let mut builder = UnionBuilder::new(db); + + let mut all_unbound = true; + let mut possibly_unbound = false; + for ty in self.elements(db) { + let ty_member = transform_fn(ty); + match ty_member { + Place::Unbound => { + possibly_unbound = true; + } + Place::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + + if all_unbound { + Place::Unbound + } else { + Place::Type( + builder.build(), + if possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + } + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + let mut builder = UnionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut all_unbound = true; + let mut possibly_unbound = false; + for ty in self.elements(db) { + let PlaceAndQualifiers { + place: ty_member, + qualifiers: new_qualifiers, + } = transform_fn(ty); + qualifiers |= new_qualifiers; + match ty_member { + Place::Unbound => { + possibly_unbound = true; + } + Place::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + PlaceAndQualifiers { + place: if all_unbound { + Place::Unbound + } else { + Place::Type( + builder.build(), + if possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + }, + qualifiers, + } + } + + /// Create a new union type with the elements normalized. + /// + /// See [`Type::normalized`] for more details. + #[must_use] + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + self.normalized_impl(db, &mut TypeTransformer::default()) + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + let mut new_elements: Vec> = self + .elements(db) + .iter() + .map(|element| element.normalized_impl(db, visitor)) + .collect(); + new_elements.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); + UnionType::new(db, new_elements.into_boxed_slice()) + } + + /// Return `true` if `self` represents the exact same sets of possible runtime objects as `other` + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + if self == other { + return true; + } + + let self_elements = self.elements(db); + let other_elements = other.elements(db); + + if self_elements.len() != other_elements.len() { + return false; + } + + let sorted_self = self.normalized(db); + + if sorted_self == other { + return true; + } + + sorted_self == other.normalized(db) + } +} + +#[salsa::interned(debug)] +pub struct IntersectionType<'db> { + /// The intersection type includes only values in all of these types. + #[returns(ref)] + positive: FxOrderSet>, + + /// The intersection type does not include any value in any of these types. + /// + /// Negation types aren't expressible in annotations, and are most likely to arise from type + /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them + /// directly in intersections rather than as a separate type. + #[returns(ref)] + negative: FxOrderSet>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for IntersectionType<'_> {} + +pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + intersection: IntersectionType<'db>, + visitor: &mut V, +) { + for element in intersection.positive(db) { + visitor.visit_type(db, *element); + } + for element in intersection.negative(db) { + visitor.visit_type(db, *element); + } +} + +impl<'db> IntersectionType<'db> { + /// Return a new `IntersectionType` instance with the positive and negative types sorted + /// according to a canonical ordering, and other normalizations applied to each element as applicable. + /// + /// See [`Type::normalized`] for more details. + #[must_use] + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + let mut visitor = TypeTransformer::default(); + self.normalized_impl(db, &mut visitor) + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + fn normalized_set<'db>( + db: &'db dyn Db, + elements: &FxOrderSet>, + visitor: &mut TypeTransformer<'db>, + ) -> FxOrderSet> { + let mut elements: FxOrderSet> = elements + .iter() + .map(|ty| ty.normalized_impl(db, visitor)) + .collect(); + + elements.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); + elements + } + + IntersectionType::new( + db, + normalized_set(db, self.positive(db), visitor), + normalized_set(db, self.negative(db), visitor), + ) + } + + /// Return `true` if `self` represents exactly the same set of possible runtime objects as `other` + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + if self == other { + return true; + } + + let self_positive = self.positive(db); + let other_positive = other.positive(db); + + if self_positive.len() != other_positive.len() { + return false; + } + + let self_negative = self.negative(db); + let other_negative = other.negative(db); + + if self_negative.len() != other_negative.len() { + return false; + } + + let sorted_self = self.normalized(db); + + if sorted_self == other { + return true; + } + + sorted_self == other.normalized(db) + } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { + if !self.negative(db).is_empty() { + return Place::todo("map_with_boundness: intersections with negative contributions"); + } + + let mut builder = IntersectionBuilder::new(db); + + let mut all_unbound = true; + let mut any_definitely_bound = false; + for ty in self.positive(db) { + let ty_member = transform_fn(ty); + match ty_member { + Place::Unbound => {} + Place::Type(ty_member, member_boundness) => { + all_unbound = false; + if member_boundness == Boundness::Bound { + any_definitely_bound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + if all_unbound { + Place::Unbound + } else { + Place::Type( + builder.build(), + if any_definitely_bound { + Boundness::Bound + } else { + Boundness::PossiblyUnbound + }, + ) + } + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + if !self.negative(db).is_empty() { + return Place::todo("map_with_boundness: intersections with negative contributions") + .into(); + } + + let mut builder = IntersectionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut any_unbound = false; + let mut any_possibly_unbound = false; + for ty in self.positive(db) { + let PlaceAndQualifiers { + place: member, + qualifiers: new_qualifiers, + } = transform_fn(ty); + qualifiers |= new_qualifiers; + match member { + Place::Unbound => { + any_unbound = true; + } + Place::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + any_possibly_unbound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + PlaceAndQualifiers { + place: if any_unbound { + Place::Unbound + } else { + Place::Type( + builder.build(), + if any_possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + }, + qualifiers, + } + } + + pub fn iter_positive(&self, db: &'db dyn Db) -> impl Iterator> { + self.positive(db).iter().copied() + } + + pub fn has_one_element(&self, db: &'db dyn Db) -> bool { + (self.positive(db).len() + self.negative(db).len()) == 1 + } +} + +/// # Ordering +/// Ordering is based on the string literal's salsa-assigned id and not on its value. +/// The id may change between runs, or when the string literal was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct StringLiteralType<'db> { + #[returns(deref)] + value: Box, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for StringLiteralType<'_> {} + +impl<'db> StringLiteralType<'db> { + /// The length of the string, as would be returned by Python's `len()`. + pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { + self.value(db).chars().count() + } + + /// Return an iterator over each character in the string literal. + /// as would be returned by Python's `iter()`. + pub(crate) fn iter_each_char(self, db: &'db dyn Db) -> impl Iterator { + self.value(db) + .chars() + .map(|c| StringLiteralType::new(db, c.to_string().as_str())) + } +} + +/// # Ordering +/// Ordering is based on the byte literal's salsa-assigned id and not on its value. +/// The id may change between runs, or when the byte literal was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct BytesLiteralType<'db> { + #[returns(deref)] + value: Box<[u8]>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for BytesLiteralType<'_> {} + +impl<'db> BytesLiteralType<'db> { + pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { + self.value(db).len() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum BoundSuperError<'db> { + InvalidPivotClassType { + pivot_class: Type<'db>, + }, + FailingConditionCheck { + pivot_class: Type<'db>, + owner: Type<'db>, + }, + UnavailableImplicitArguments, +} + +impl BoundSuperError<'_> { + pub(super) fn report_diagnostic(&self, context: &InferContext, node: AnyNodeRef) { + match self { + BoundSuperError::InvalidPivotClassType { pivot_class } => { + if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + builder.into_diagnostic(format_args!( + "`{pivot_class}` is not a valid class", + pivot_class = pivot_class.display(context.db()), + )); + } + } + BoundSuperError::FailingConditionCheck { pivot_class, owner } => { + if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + builder.into_diagnostic(format_args!( + "`{owner}` is not an instance or subclass of \ + `{pivot_class}` in `super({pivot_class}, {owner})` call", + pivot_class = pivot_class.display(context.db()), + owner = owner.display(context.db()), + )); + } + } + BoundSuperError::UnavailableImplicitArguments => { + if let Some(builder) = + context.report_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, node) + { + builder.into_diagnostic(format_args!( + "Cannot determine implicit arguments for 'super()' in this context", + )); + } + } + } + } +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum SuperOwnerKind<'db> { + Dynamic(DynamicType), + Class(ClassType<'db>), + Instance(NominalInstanceType<'db>), +} + +impl<'db> SuperOwnerKind<'db> { + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + match self { + SuperOwnerKind::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic.normalized()), + SuperOwnerKind::Class(class) => { + SuperOwnerKind::Class(class.normalized_impl(db, visitor)) + } + SuperOwnerKind::Instance(instance) => { + SuperOwnerKind::Instance(instance.normalized_impl(db, visitor)) + } + } + } + + fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { + match self { + SuperOwnerKind::Dynamic(dynamic) => { + Either::Left(ClassBase::Dynamic(dynamic).mro(db, None)) + } + SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), + SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)), + } + } + + fn into_type(self) -> Type<'db> { + match self { + SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic), + SuperOwnerKind::Class(class) => class.into(), + SuperOwnerKind::Instance(instance) => instance.into(), + } + } + + fn into_class(self) -> Option> { + match self { + SuperOwnerKind::Dynamic(_) => None, + SuperOwnerKind::Class(class) => Some(class), + SuperOwnerKind::Instance(instance) => Some(instance.class), + } + } + + fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + match ty { + Type::Dynamic(dynamic) => Some(SuperOwnerKind::Dynamic(dynamic)), + Type::ClassLiteral(class_literal) => Some(SuperOwnerKind::Class( + class_literal.apply_optional_specialization(db, None), + )), + Type::NominalInstance(instance) => Some(SuperOwnerKind::Instance(instance)), + Type::BooleanLiteral(_) => { + SuperOwnerKind::try_from_type(db, KnownClass::Bool.to_instance(db)) + } + Type::IntLiteral(_) => { + SuperOwnerKind::try_from_type(db, KnownClass::Int.to_instance(db)) + } + Type::StringLiteral(_) => { + SuperOwnerKind::try_from_type(db, KnownClass::Str.to_instance(db)) + } + Type::LiteralString => { + SuperOwnerKind::try_from_type(db, KnownClass::Str.to_instance(db)) + } + Type::BytesLiteral(_) => { + SuperOwnerKind::try_from_type(db, KnownClass::Bytes.to_instance(db)) + } + Type::SpecialForm(special_form) => { + SuperOwnerKind::try_from_type(db, special_form.instance_fallback(db)) + } + _ => None, + } + } +} + +impl<'db> From> for Type<'db> { + fn from(owner: SuperOwnerKind<'db>) -> Self { + match owner { + SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic), + SuperOwnerKind::Class(class) => class.into(), + SuperOwnerKind::Instance(instance) => instance.into(), + } + } +} + +/// Represent a bound super object like `super(PivotClass, owner)` +#[salsa::interned(debug)] +pub struct BoundSuperType<'db> { + pub pivot_class: ClassBase<'db>, + pub owner: SuperOwnerKind<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for BoundSuperType<'_> {} + +fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + bound_super: BoundSuperType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, bound_super.pivot_class(db).into()); + visitor.visit_type(db, bound_super.owner(db).into_type()); +} + +impl<'db> BoundSuperType<'db> { + /// Attempts to build a `Type::BoundSuper` based on the given `pivot_class` and `owner`. + /// + /// This mimics the behavior of Python's built-in `super(pivot, owner)` at runtime. + /// - `super(pivot, owner_class)` is valid only if `issubclass(owner_class, pivot)` + /// - `super(pivot, owner_instance)` is valid only if `isinstance(owner_instance, pivot)` + /// + /// However, the checking is skipped when any of the arguments is a dynamic type. + fn build( + db: &'db dyn Db, + pivot_class_type: Type<'db>, + owner_type: Type<'db>, + ) -> Result, BoundSuperError<'db>> { + if let Type::Union(union) = owner_type { + return Ok(UnionType::from_elements( + db, + union + .elements(db) + .iter() + .map(|ty| BoundSuperType::build(db, pivot_class_type, *ty)) + .collect::, _>>()?, + )); + } + + let pivot_class = ClassBase::try_from_type(db, pivot_class_type).ok_or({ + BoundSuperError::InvalidPivotClassType { + pivot_class: pivot_class_type, + } + })?; + + let owner = SuperOwnerKind::try_from_type(db, owner_type) + .and_then(|owner| { + let Some(pivot_class) = pivot_class.into_class() else { + return Some(owner); + }; + let Some(owner_class) = owner.into_class() else { + return Some(owner); + }; + if owner_class.is_subclass_of(db, pivot_class) { + Some(owner) + } else { + None + } + }) + .ok_or(BoundSuperError::FailingConditionCheck { + pivot_class: pivot_class_type, + owner: owner_type, + })?; + + Ok(Type::BoundSuper(BoundSuperType::new( + db, + pivot_class, + owner, + ))) + } + + /// Skips elements in the MRO up to and including the pivot class. + /// + /// If the pivot class is a dynamic type, its MRO can't be determined, + /// so we fall back to using the MRO of `DynamicType::Unknown`. + fn skip_until_after_pivot( + self, + db: &'db dyn Db, + mro_iter: impl Iterator>, + ) -> impl Iterator> { + let Some(pivot_class) = self.pivot_class(db).into_class() else { + return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db, None)); + }; + + let mut pivot_found = false; + + Either::Right(mro_iter.skip_while(move |superclass| { + if pivot_found { + false + } else if Some(pivot_class) == superclass.into_class() { + pivot_found = true; + true + } else { + true + } + })) + } + + /// Tries to call `__get__` on the attribute. + /// The arguments passed to `__get__` depend on whether the owner is an instance or a class. + /// See the `CPython` implementation for reference: + /// + fn try_call_dunder_get_on_attribute( + self, + db: &'db dyn Db, + attribute: PlaceAndQualifiers<'db>, + ) -> Option> { + let owner = self.owner(db); + + match owner { + // If the owner is a dynamic type, we can't tell whether it's a class or an instance. + // Also, invoking a descriptor on a dynamic attribute is meaningless, so we don't handle this. + SuperOwnerKind::Dynamic(_) => None, + SuperOwnerKind::Class(_) => Some( + Type::try_call_dunder_get_on_attribute( + db, + attribute, + Type::none(db), + owner.into_type(), + ) + .0, + ), + SuperOwnerKind::Instance(_) => Some( + Type::try_call_dunder_get_on_attribute( + db, + attribute, + owner.into_type(), + owner.into_type().to_meta_type(db), + ) + .0, + ), + } + } + + /// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the + /// pivot class in the MRO, based on the `owner` type instead of the `super` type. + fn find_name_in_mro_after_pivot( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + let owner = self.owner(db); + let class = match owner { + SuperOwnerKind::Dynamic(_) => { + return owner + .into_type() + .find_name_in_mro_with_policy(db, name, policy) + .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"); + } + SuperOwnerKind::Class(class) => class, + SuperOwnerKind::Instance(instance) => instance.class, + }; + + let (class_literal, _) = class.class_literal(db); + // TODO properly support super() with generic types + // * requires a fix for https://github.com/astral-sh/ruff/issues/17432 + // * also requires understanding how we should handle cases like this: + // ```python + // b_int: B[int] + // b_unknown: B + // + // super(B, b_int) + // super(B[int], b_unknown) + // ``` + match class_literal.generic_context(db) { + Some(_) => Place::bound(todo_type!("super in generic class")).into(), + None => class_literal.class_member_from_mro( + db, + name, + policy, + self.skip_until_after_pivot(db, owner.iter_mro(db)), + ), + } + } + + pub(super) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self::new( + db, + self.pivot_class(db).normalized_impl(db, visitor), + self.owner(db).normalized_impl(db, visitor), + ) + } +} + +#[salsa::interned(debug)] +pub struct TypeIsType<'db> { + return_type: Type<'db>, + /// The ID of the scope to which the place belongs + /// and the ID of the place itself within that scope. + place_info: Option<(ScopeId<'db>, ScopedPlaceId)>, +} + +fn walk_typeis_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + typeis_type: TypeIsType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, typeis_type.return_type(db)); +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypeIsType<'_> {} + +impl<'db> TypeIsType<'db> { + pub fn place_name(self, db: &'db dyn Db) -> Option { + let (scope, place) = self.place_info(db)?; + let table = place_table(db, scope); + + Some(format!("{}", table.place_expr(place))) + } + + pub fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + Type::TypeIs(Self::new(db, ty, None)) + } + + pub fn bound( + db: &'db dyn Db, + return_type: Type<'db>, + scope: ScopeId<'db>, + place: ScopedPlaceId, + ) -> Type<'db> { + Type::TypeIs(Self::new(db, return_type, Some((scope, place)))) + } + + #[must_use] + pub fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> { + Self::bound(db, self.return_type(db), scope, place) + } + + #[must_use] + pub fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + Type::TypeIs(Self::new(db, ty, self.place_info(db))) + } + + pub fn is_bound(&self, db: &'db dyn Db) -> bool { + self.place_info(db).is_some() + } + + pub fn is_unbound(&self, db: &'db dyn Db) -> bool { + self.place_info(db).is_none() + } +} + +// Make sure that the `Type` enum does not grow unexpectedly. +#[cfg(not(debug_assertions))] +#[cfg(target_pointer_width = "64")] +static_assertions::assert_eq_size!(Type, [u8; 16]); + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::db::tests::{TestDbBuilder, setup_db}; + use crate::place::{global_symbol, typing_extensions_symbol, typing_symbol}; + use ruff_db::files::system_path_to_file; + use ruff_db::parsed::parsed_module; + use ruff_db::system::DbWithWritableSystem as _; + use ruff_db::testing::assert_function_query_was_not_run; + use ruff_python_ast::PythonVersion; + use test_case::test_case; + + /// Explicitly test for Python version <3.13 and >=3.13, to ensure that + /// the fallback to `typing_extensions` is working correctly. + /// See [`KnownClass::canonical_module`] for more information. + #[test_case(PythonVersion::PY312)] + #[test_case(PythonVersion::PY313)] + fn no_default_type_is_singleton(python_version: PythonVersion) { + let db = TestDbBuilder::new() + .with_python_version(python_version) + .build() + .unwrap(); + + let no_default = KnownClass::NoDefaultType.to_instance(&db); + + assert!(no_default.is_singleton(&db)); + } + + #[test] + fn typing_vs_typeshed_no_default() { + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY313) + .build() + .unwrap(); + + let typing_no_default = typing_symbol(&db, "NoDefault").place.expect_type(); + let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") + .place + .expect_type(); + + assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); + assert_eq!( + typing_extensions_no_default.display(&db).to_string(), + "NoDefault" + ); + } + + /// Inferring the result of a call-expression shouldn't need to re-run after + /// a trivial change to the function's file (e.g. by adding a docstring to the function). + #[test] + fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { + let mut db = setup_db(); + + db.write_dedented( + "src/foo.py", + r#" + def foo() -> int: + return 5 + "#, + )?; + db.write_dedented( + "src/bar.py", + r#" + from foo import foo + + a = foo() + "#, + )?; + + let bar = system_path_to_file(&db, "src/bar.py")?; + let a = global_symbol(&db, bar, "a").place; + + assert_eq!( + a.expect_type(), + UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)]) + ); + + // Add a docstring to foo to trigger a re-run. + // The bar-call site of foo should not be re-run because of that + db.write_dedented( + "src/foo.py", + r#" + def foo() -> int: + "Computes a value" + return 5 + "#, + )?; + db.clear_salsa_events(); + + let a = global_symbol(&db, bar, "a").place; + + assert_eq!( + a.expect_type(), + UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)]) + ); + let events = db.take_salsa_events(); + + let module = parsed_module(&db, bar).load(&db); + let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; + let foo_call = semantic_index(&db, bar).expression(call); + + assert_function_query_was_not_run(&db, infer_expression_types, foo_call, &events); + + Ok(()) + } + + /// All other tests also make sure that `Type::Todo` works as expected. This particular + /// test makes sure that we handle `Todo` types correctly, even if they originate from + /// different sources. + #[test] + fn todo_types() { + let db = setup_db(); + + let todo1 = todo_type!("1"); + let todo2 = todo_type!("2"); + + let int = KnownClass::Int.to_instance(&db); + + assert!(int.is_assignable_to(&db, todo1)); + + assert!(todo1.is_assignable_to(&db, int)); + + // We lose information when combining several `Todo` types. This is an + // acknowledged limitation of the current implementation. We can not + // easily store the meta information of several `Todo`s in a single + // variant, as `TodoType` needs to implement `Copy`, meaning it can't + // contain `Vec`/`Box`/etc., and can't be boxed itself. + // + // Lifting this restriction would require us to intern `TodoType` in + // salsa, but that would mean we would have to pass in `db` everywhere. + + // A union of several `Todo` types collapses to a single `Todo` type: + assert!(UnionType::from_elements(&db, vec![todo1, todo2]).is_todo()); + + // And similar for intersection types: + assert!( + IntersectionBuilder::new(&db) + .add_positive(todo1) + .add_positive(todo2) + .build() + .is_todo() + ); + assert!( + IntersectionBuilder::new(&db) + .add_positive(todo1) + .add_negative(todo2) + .build() + .is_todo() + ); + } +} diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs new file mode 100644 index 0000000000000..70876773225d0 --- /dev/null +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -0,0 +1,1047 @@ +//! Smart builders for union and intersection types. +//! +//! Invariants we maintain here: +//! * No single-element union types (should just be the contained type instead.) +//! * No single-positive-element intersection types. Single-negative-element are OK, we don't +//! have a standalone negation type so there's no other representation for this. +//! * The same type should never appear more than once in a union or intersection. (This should +//! be expanded to cover subtyping -- see below -- but for now we only implement it for type +//! identity.) +//! * Disjunctive normal form (DNF): the tree of unions and intersections can never be deeper +//! than a union-of-intersections. Unions cannot contain other unions (the inner union just +//! flattens into the outer one), intersections cannot contain other intersections (also +//! flattens), and intersections cannot contain unions (the intersection distributes over the +//! union, inverting it into a union-of-intersections). +//! * No type in a union can be a subtype of any other type in the union (just eliminate the +//! subtype from the union). +//! * No type in an intersection can be a supertype of any other type in the intersection (just +//! eliminate the supertype from the intersection). +//! * An intersection containing two non-overlapping types simplifies to [`Type::Never`]. +//! +//! The implication of these invariants is that a [`UnionBuilder`] does not necessarily build a +//! [`Type::Union`]. For example, if only one type is added to the [`UnionBuilder`], `build()` will +//! just return that type directly. The same is true for [`IntersectionBuilder`]; for example, if a +//! union type is added to the intersection, it will distribute and [`IntersectionBuilder::build`] +//! may end up returning a [`Type::Union`] of intersections. +//! +//! ## Performance +//! +//! In practice, there are two kinds of unions found in the wild: relatively-small unions made up +//! of normal user types (classes, etc), and large unions made up of literals, which can occur via +//! large enums (not yet implemented) or from string/integer/bytes literals, which can grow due to +//! literal arithmetic or operations on literal strings/bytes. For normal unions, it's most +//! efficient to just store the member types in a vector, and do O(n^2) `is_subtype_of` checks to +//! maintain the union in simplified form. But literal unions can grow to a size where this becomes +//! a performance problem. For this reason, we group literal types in `UnionBuilder`. Since every +//! different string literal type shares exactly the same possible super-types, and none of them +//! are subtypes of each other (unless exactly the same literal type), we can avoid many +//! unnecessary `is_subtype_of` checks. + +use crate::types::{ + BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type, + TypeVarBoundOrConstraints, UnionType, +}; +use crate::{Db, FxOrderSet}; +use smallvec::SmallVec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LiteralKind { + Int, + String, + Bytes, +} + +impl<'db> Type<'db> { + /// Return `true` if this type can be a supertype of some literals of `kind` and not others. + fn splits_literals(self, db: &'db dyn Db, kind: LiteralKind) -> bool { + match (self, kind) { + (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => true, + (Type::StringLiteral(_), LiteralKind::String) => true, + (Type::BytesLiteral(_), LiteralKind::Bytes) => true, + (Type::IntLiteral(_), LiteralKind::Int) => true, + (Type::Intersection(intersection), _) => { + intersection + .positive(db) + .iter() + .any(|ty| ty.splits_literals(db, kind)) + || intersection + .negative(db) + .iter() + .any(|ty| ty.splits_literals(db, kind)) + } + (Type::Union(union), _) => union + .elements(db) + .iter() + .any(|ty| ty.splits_literals(db, kind)), + _ => false, + } + } +} + +#[derive(Debug)] +enum UnionElement<'db> { + IntLiterals(FxOrderSet), + StringLiterals(FxOrderSet>), + BytesLiterals(FxOrderSet>), + Type(Type<'db>), +} + +impl<'db> UnionElement<'db> { + /// Try reducing this `UnionElement` given the presence in the same union of `other_type`. + fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> { + match self { + UnionElement::IntLiterals(literals) => { + if other_type.splits_literals(db, LiteralKind::Int) { + let mut collapse = false; + let mut ignore = false; + let negated = other_type.negate(db); + literals.retain(|literal| { + let ty = Type::IntLiteral(*literal); + if negated.is_subtype_of(db, ty) { + collapse = true; + } + if other_type.is_subtype_of(db, ty) { + ignore = true; + } + !ty.is_subtype_of(db, other_type) + }); + if ignore { + ReduceResult::Ignore + } else if collapse { + ReduceResult::CollapseToObject + } else { + ReduceResult::KeepIf(!literals.is_empty()) + } + } else { + ReduceResult::KeepIf( + !Type::IntLiteral(literals[0]).is_subtype_of(db, other_type), + ) + } + } + UnionElement::StringLiterals(literals) => { + if other_type.splits_literals(db, LiteralKind::String) { + let mut collapse = false; + let mut ignore = false; + let negated = other_type.negate(db); + literals.retain(|literal| { + let ty = Type::StringLiteral(*literal); + if negated.is_subtype_of(db, ty) { + collapse = true; + } + if other_type.is_subtype_of(db, ty) { + ignore = true; + } + !ty.is_subtype_of(db, other_type) + }); + if ignore { + ReduceResult::Ignore + } else if collapse { + ReduceResult::CollapseToObject + } else { + ReduceResult::KeepIf(!literals.is_empty()) + } + } else { + ReduceResult::KeepIf( + !Type::StringLiteral(literals[0]).is_subtype_of(db, other_type), + ) + } + } + UnionElement::BytesLiterals(literals) => { + if other_type.splits_literals(db, LiteralKind::Bytes) { + let mut collapse = false; + let mut ignore = false; + let negated = other_type.negate(db); + literals.retain(|literal| { + let ty = Type::BytesLiteral(*literal); + if negated.is_subtype_of(db, ty) { + collapse = true; + } + if other_type.is_subtype_of(db, ty) { + ignore = true; + } + !ty.is_subtype_of(db, other_type) + }); + if ignore { + ReduceResult::Ignore + } else if collapse { + ReduceResult::CollapseToObject + } else { + ReduceResult::KeepIf(!literals.is_empty()) + } + } else { + ReduceResult::KeepIf( + !Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type), + ) + } + } + UnionElement::Type(existing) => ReduceResult::Type(*existing), + } + } +} + +enum ReduceResult<'db> { + /// Reduction of this `UnionElement` is complete; keep it in the union if the nested + /// boolean is true, eliminate it from the union if false. + KeepIf(bool), + /// Collapse this entire union to `object`. + CollapseToObject, + /// The new element is a subtype of an existing part of the `UnionElement`, ignore it. + Ignore, + /// The given `Type` can stand-in for the entire `UnionElement` for further union + /// simplification checks. + Type(Type<'db>), +} + +// TODO increase this once we extend `UnionElement` throughout all union/intersection +// representations, so that we can make large unions of literals fast in all operations. +const MAX_UNION_LITERALS: usize = 200; + +pub(crate) struct UnionBuilder<'db> { + elements: Vec>, + db: &'db dyn Db, +} + +impl<'db> UnionBuilder<'db> { + pub(crate) fn new(db: &'db dyn Db) -> Self { + Self { + db, + elements: vec![], + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + /// Collapse the union to a single type: `object`. + fn collapse_to_object(&mut self) { + self.elements.clear(); + self.elements + .push(UnionElement::Type(Type::object(self.db))); + } + + /// Adds a type to this union. + pub(crate) fn add(mut self, ty: Type<'db>) -> Self { + self.add_in_place(ty); + self + } + + /// Adds a type to this union. + pub(crate) fn add_in_place(&mut self, ty: Type<'db>) { + match ty { + Type::Union(union) => { + let new_elements = union.elements(self.db); + self.elements.reserve(new_elements.len()); + for element in new_elements { + self.add_in_place(*element); + } + } + // Adding `Never` to a union is a no-op. + Type::Never => {} + // If adding a string literal, look for an existing `UnionElement::StringLiterals` to + // add it to, or an existing element that is a super-type of string literals, which + // means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals` + // containing it. + Type::StringLiteral(literal) => { + let mut found = None; + let mut to_remove = None; + let ty_negated = ty.negate(self.db); + for (index, element) in self.elements.iter_mut().enumerate() { + match element { + UnionElement::StringLiterals(literals) => { + if literals.len() >= MAX_UNION_LITERALS { + let replace_with = KnownClass::Str.to_instance(self.db); + self.add_in_place(replace_with); + return; + } + found = Some(literals); + continue; + } + UnionElement::Type(existing) => { + if ty.is_subtype_of(self.db, *existing) { + return; + } + if existing.is_subtype_of(self.db, ty) { + to_remove = Some(index); + } + if ty_negated.is_subtype_of(self.db, *existing) { + // The type that includes both this new element, and its negation + // (or a supertype of its negation), must be simply `object`. + self.collapse_to_object(); + return; + } + } + _ => {} + } + } + if let Some(found) = found { + found.insert(literal); + } else { + self.elements + .push(UnionElement::StringLiterals(FxOrderSet::from_iter([ + literal, + ]))); + } + if let Some(index) = to_remove { + self.elements.swap_remove(index); + } + } + // Same for bytes literals as for string literals, above. + Type::BytesLiteral(literal) => { + let mut found = None; + let mut to_remove = None; + let ty_negated = ty.negate(self.db); + for (index, element) in self.elements.iter_mut().enumerate() { + match element { + UnionElement::BytesLiterals(literals) => { + if literals.len() >= MAX_UNION_LITERALS { + let replace_with = KnownClass::Bytes.to_instance(self.db); + self.add_in_place(replace_with); + return; + } + found = Some(literals); + continue; + } + UnionElement::Type(existing) => { + if ty.is_subtype_of(self.db, *existing) { + return; + } + if existing.is_subtype_of(self.db, ty) { + to_remove = Some(index); + } + if ty_negated.is_subtype_of(self.db, *existing) { + // The type that includes both this new element, and its negation + // (or a supertype of its negation), must be simply `object`. + self.collapse_to_object(); + return; + } + } + _ => {} + } + } + if let Some(found) = found { + found.insert(literal); + } else { + self.elements + .push(UnionElement::BytesLiterals(FxOrderSet::from_iter([ + literal, + ]))); + } + if let Some(index) = to_remove { + self.elements.swap_remove(index); + } + } + // And same for int literals as well. + Type::IntLiteral(literal) => { + let mut found = None; + let mut to_remove = None; + let ty_negated = ty.negate(self.db); + for (index, element) in self.elements.iter_mut().enumerate() { + match element { + UnionElement::IntLiterals(literals) => { + if literals.len() >= MAX_UNION_LITERALS { + let replace_with = KnownClass::Int.to_instance(self.db); + self.add_in_place(replace_with); + return; + } + found = Some(literals); + continue; + } + UnionElement::Type(existing) => { + if ty.is_subtype_of(self.db, *existing) { + return; + } + if existing.is_subtype_of(self.db, ty) { + to_remove = Some(index); + } + if ty_negated.is_subtype_of(self.db, *existing) { + // The type that includes both this new element, and its negation + // (or a supertype of its negation), must be simply `object`. + self.collapse_to_object(); + return; + } + } + _ => {} + } + } + if let Some(found) = found { + found.insert(literal); + } else { + self.elements + .push(UnionElement::IntLiterals(FxOrderSet::from_iter([literal]))); + } + if let Some(index) = to_remove { + self.elements.swap_remove(index); + } + } + // Adding `object` to a union results in `object`. + ty if ty.is_object(self.db) => { + self.collapse_to_object(); + } + _ => { + let bool_pair = if let Type::BooleanLiteral(b) = ty { + Some(Type::BooleanLiteral(!b)) + } else { + None + }; + + let mut to_remove = SmallVec::<[usize; 2]>::new(); + let ty_negated = ty.negate(self.db); + + for (index, element) in self.elements.iter_mut().enumerate() { + let element_type = match element.try_reduce(self.db, ty) { + ReduceResult::KeepIf(keep) => { + if !keep { + to_remove.push(index); + } + continue; + } + ReduceResult::Type(ty) => ty, + ReduceResult::CollapseToObject => { + self.collapse_to_object(); + return; + } + ReduceResult::Ignore => { + return; + } + }; + if Some(element_type) == bool_pair { + self.add_in_place(KnownClass::Bool.to_instance(self.db)); + return; + } + + if ty.is_equivalent_to(self.db, element_type) + || ty.is_subtype_of(self.db, element_type) + { + return; + } else if element_type.is_subtype_of(self.db, ty) { + to_remove.push(index); + } else if ty_negated.is_subtype_of(self.db, element_type) { + // We add `ty` to the union. We just checked that `~ty` is a subtype of an + // existing `element`. This also means that `~ty | ty` is a subtype of + // `element | ty`, because both elements in the first union are subtypes of + // the corresponding elements in the second union. But `~ty | ty` is just + // `object`. Since `object` is a subtype of `element | ty`, we can only + // conclude that `element | ty` must be `object` (object has no other + // supertypes). This means we can simplify the whole union to just + // `object`, since all other potential elements would also be subtypes of + // `object`. + self.collapse_to_object(); + return; + } + } + if let Some((&first, rest)) = to_remove.split_first() { + self.elements[first] = UnionElement::Type(ty); + // 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(UnionElement::Type(ty)); + } + } + } + } + + pub(crate) fn build(self) -> Type<'db> { + self.try_build().unwrap_or(Type::Never) + } + + pub(crate) fn try_build(self) -> Option> { + let mut types = vec![]; + for element in self.elements { + match element { + UnionElement::IntLiterals(literals) => { + types.extend(literals.into_iter().map(Type::IntLiteral)); + } + UnionElement::StringLiterals(literals) => { + types.extend(literals.into_iter().map(Type::StringLiteral)); + } + UnionElement::BytesLiterals(literals) => { + types.extend(literals.into_iter().map(Type::BytesLiteral)); + } + UnionElement::Type(ty) => types.push(ty), + } + } + match types.len() { + 0 => None, + 1 => Some(types[0]), + _ => Some(Type::Union(UnionType::new( + self.db, + types.into_boxed_slice(), + ))), + } + } +} + +#[derive(Clone)] +pub(crate) struct IntersectionBuilder<'db> { + // Really this builds a union-of-intersections, because we always keep our set-theoretic types + // in disjunctive normal form (DNF), a union of intersections. In the simplest case there's + // just a single intersection in this vector, and we are building a single intersection type, + // but if a union is added to the intersection, we'll distribute ourselves over that union and + // create a union of intersections. + intersections: Vec>, + db: &'db dyn Db, +} + +impl<'db> IntersectionBuilder<'db> { + pub(crate) fn new(db: &'db dyn Db) -> Self { + Self { + db, + intersections: vec![InnerIntersectionBuilder::default()], + } + } + + fn empty(db: &'db dyn Db) -> Self { + Self { + db, + intersections: vec![], + } + } + + pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self { + if let Type::Union(union) = ty { + // Distribute ourself over this union: for each union element, clone ourself and + // intersect with that union element, then create a new union-of-intersections with all + // of those sub-intersections in it. E.g. if `self` is a simple intersection `T1 & T2` + // and we add `T3 | T4` to the intersection, we don't get `T1 & T2 & (T3 | T4)` (that's + // not in DNF), we distribute the union and get `(T1 & T3) | (T2 & T3) | (T1 & T4) | + // (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)` + // and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 & + // T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea. + union + .elements(self.db) + .iter() + .map(|elem| self.clone().add_positive(*elem)) + .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { + builder.intersections.extend(sub.intersections); + builder + }) + } else { + // If we are already a union-of-intersections, distribute the new intersected element + // across all of those intersections. + for inner in &mut self.intersections { + inner.add_positive(self.db, ty); + } + self + } + } + + pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self { + // See comments above in `add_positive`; this is just the negated version. + if let Type::Union(union) = ty { + for elem in union.elements(self.db) { + self = self.add_negative(*elem); + } + self + } else if let Type::Intersection(intersection) = ty { + // (A | B) & ~(C & ~D) + // -> (A | B) & (~C | D) + // -> ((A | B) & ~C) | ((A | B) & D) + // i.e. if we have an intersection of positive constraints C + // and negative constraints D, then our new intersection + // is (existing & ~C) | (existing & D) + + let positive_side = intersection + .positive(self.db) + .iter() + // we negate all the positive constraints while distributing + .map(|elem| self.clone().add_negative(*elem)); + + let negative_side = intersection + .negative(self.db) + .iter() + // all negative constraints end up becoming positive constraints + .map(|elem| self.clone().add_positive(*elem)); + + positive_side.chain(negative_side).fold( + IntersectionBuilder::empty(self.db), + |mut builder, sub| { + builder.intersections.extend(sub.intersections); + builder + }, + ) + } else { + for inner in &mut self.intersections { + inner.add_negative(self.db, ty); + } + self + } + } + + pub(crate) fn positive_elements(mut self, elements: I) -> Self + where + I: IntoIterator, + T: Into>, + { + for element in elements { + self = self.add_positive(element.into()); + } + self + } + + pub(crate) fn negative_elements(mut self, elements: I) -> Self + where + I: IntoIterator, + T: Into>, + { + for element in elements { + self = self.add_negative(element.into()); + } + self + } + + pub(crate) fn build(mut self) -> Type<'db> { + // Avoid allocating the UnionBuilder unnecessarily if we have just one intersection: + if self.intersections.len() == 1 { + self.intersections.pop().unwrap().build(self.db) + } else { + UnionType::from_elements( + self.db, + self.intersections + .into_iter() + .map(|inner| inner.build(self.db)), + ) + } + } +} + +#[derive(Debug, Clone, Default)] +struct InnerIntersectionBuilder<'db> { + positive: FxOrderSet>, + negative: FxOrderSet>, +} + +impl<'db> InnerIntersectionBuilder<'db> { + /// Adds a positive type to this intersection. + fn add_positive(&mut self, db: &'db dyn Db, mut new_positive: Type<'db>) { + match new_positive { + // `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` + Type::AlwaysTruthy if self.positive.contains(&Type::LiteralString) => { + self.add_negative(db, Type::string_literal(db, "")); + } + // `LiteralString & AlwaysFalsy` -> `Literal[""]` + Type::AlwaysFalsy if self.positive.swap_remove(&Type::LiteralString) => { + self.add_positive(db, Type::string_literal(db, "")); + } + // `AlwaysTruthy & LiteralString` -> `LiteralString & ~Literal[""]` + Type::LiteralString if self.positive.swap_remove(&Type::AlwaysTruthy) => { + self.add_positive(db, Type::LiteralString); + self.add_negative(db, Type::string_literal(db, "")); + } + // `AlwaysFalsy & LiteralString` -> `Literal[""]` + Type::LiteralString if self.positive.swap_remove(&Type::AlwaysFalsy) => { + self.add_positive(db, Type::string_literal(db, "")); + } + // `LiteralString & ~AlwaysTruthy` -> `LiteralString & AlwaysFalsy` -> `Literal[""]` + Type::LiteralString if self.negative.swap_remove(&Type::AlwaysTruthy) => { + self.add_positive(db, Type::string_literal(db, "")); + } + // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` + Type::LiteralString if self.negative.swap_remove(&Type::AlwaysFalsy) => { + self.add_positive(db, Type::LiteralString); + self.add_negative(db, Type::string_literal(db, "")); + } + // `(A & B & ~C) & (D & E & ~F)` -> `A & B & D & E & ~C & ~F` + Type::Intersection(other) => { + for pos in other.positive(db) { + self.add_positive(db, *pos); + } + for neg in other.negative(db) { + self.add_negative(db, *neg); + } + } + _ => { + let known_instance = new_positive + .into_nominal_instance() + .and_then(|instance| instance.class.known(db)); + + if known_instance == Some(KnownClass::Object) { + // `object & T` -> `T`; it is always redundant to add `object` to an intersection + return; + } + + let addition_is_bool_instance = known_instance == Some(KnownClass::Bool); + + for (index, existing_positive) in self.positive.iter().enumerate() { + match existing_positive { + // `AlwaysTruthy & bool` -> `Literal[True]` + Type::AlwaysTruthy if addition_is_bool_instance => { + new_positive = Type::BooleanLiteral(true); + } + // `AlwaysFalsy & bool` -> `Literal[False]` + Type::AlwaysFalsy if addition_is_bool_instance => { + new_positive = Type::BooleanLiteral(false); + } + Type::NominalInstance(instance) + if instance.class.is_known(db, KnownClass::Bool) => + { + match new_positive { + // `bool & AlwaysTruthy` -> `Literal[True]` + Type::AlwaysTruthy => { + new_positive = Type::BooleanLiteral(true); + } + // `bool & AlwaysFalsy` -> `Literal[False]` + Type::AlwaysFalsy => { + new_positive = Type::BooleanLiteral(false); + } + _ => continue, + } + } + _ => continue, + } + self.positive.swap_remove_index(index); + break; + } + + if addition_is_bool_instance { + for (index, existing_negative) in self.negative.iter().enumerate() { + match existing_negative { + // `bool & ~Literal[False]` -> `Literal[True]` + // `bool & ~Literal[True]` -> `Literal[False]` + Type::BooleanLiteral(bool_value) => { + new_positive = Type::BooleanLiteral(!bool_value); + } + // `bool & ~AlwaysTruthy` -> `Literal[False]` + Type::AlwaysTruthy => { + new_positive = Type::BooleanLiteral(false); + } + // `bool & ~AlwaysFalsy` -> `Literal[True]` + Type::AlwaysFalsy => { + new_positive = Type::BooleanLiteral(true); + } + _ => continue, + } + self.negative.swap_remove_index(index); + break; + } + } + + let mut to_remove = SmallVec::<[usize; 1]>::new(); + for (index, existing_positive) in self.positive.iter().enumerate() { + // S & T = S if S <: T + if existing_positive.is_subtype_of(db, new_positive) + || existing_positive.is_equivalent_to(db, new_positive) + { + return; + } + // same rule, reverse order + if new_positive.is_subtype_of(db, *existing_positive) { + to_remove.push(index); + } + // A & B = Never if A and B are disjoint + if new_positive.is_disjoint_from(db, *existing_positive) { + *self = Self::default(); + self.positive.insert(Type::Never); + return; + } + } + for index in to_remove.into_iter().rev() { + self.positive.swap_remove_index(index); + } + + let mut to_remove = SmallVec::<[usize; 1]>::new(); + for (index, existing_negative) in self.negative.iter().enumerate() { + // S & ~T = Never if S <: T + if new_positive.is_subtype_of(db, *existing_negative) { + *self = Self::default(); + self.positive.insert(Type::Never); + return; + } + // A & ~B = A if A and B are disjoint + if existing_negative.is_disjoint_from(db, new_positive) { + to_remove.push(index); + } + } + for index in to_remove.into_iter().rev() { + self.negative.swap_remove_index(index); + } + + self.positive.insert(new_positive); + } + } + } + + /// Adds a negative type to this intersection. + fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) { + let contains_bool = || { + self.positive + .iter() + .filter_map(|ty| ty.into_nominal_instance()) + .filter_map(|instance| instance.class.known(db)) + .any(KnownClass::is_bool) + }; + + match new_negative { + Type::Intersection(inter) => { + for pos in inter.positive(db) { + self.add_negative(db, *pos); + } + for neg in inter.negative(db) { + self.add_positive(db, *neg); + } + } + Type::Never => { + // Adding ~Never to an intersection is a no-op. + } + Type::NominalInstance(instance) if instance.class.is_object(db) => { + // Adding ~object to an intersection results in Never. + *self = Self::default(); + self.positive.insert(Type::Never); + } + ty @ Type::Dynamic(_) => { + // Adding any of these types to the negative side of an intersection + // is equivalent to adding it to the positive side. We do this to + // simplify the representation. + self.add_positive(db, ty); + } + // `bool & ~AlwaysTruthy` -> `bool & Literal[False]` + // `bool & ~Literal[True]` -> `bool & Literal[False]` + Type::AlwaysTruthy | Type::BooleanLiteral(true) if contains_bool() => { + self.add_positive(db, Type::BooleanLiteral(false)); + } + // `LiteralString & ~AlwaysTruthy` -> `LiteralString & Literal[""]` + Type::AlwaysTruthy if self.positive.contains(&Type::LiteralString) => { + self.add_positive(db, Type::string_literal(db, "")); + } + // `bool & ~AlwaysFalsy` -> `bool & Literal[True]` + // `bool & ~Literal[False]` -> `bool & Literal[True]` + Type::AlwaysFalsy | Type::BooleanLiteral(false) if contains_bool() => { + self.add_positive(db, Type::BooleanLiteral(true)); + } + // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` + Type::AlwaysFalsy if self.positive.contains(&Type::LiteralString) => { + self.add_negative(db, Type::string_literal(db, "")); + } + _ => { + let mut to_remove = SmallVec::<[usize; 1]>::new(); + for (index, existing_negative) in self.negative.iter().enumerate() { + // ~S & ~T = ~T if S <: T + if existing_negative.is_subtype_of(db, new_negative) + || existing_negative.is_equivalent_to(db, new_negative) + { + to_remove.push(index); + } + // same rule, reverse order + if new_negative.is_subtype_of(db, *existing_negative) { + return; + } + } + for index in to_remove.into_iter().rev() { + self.negative.swap_remove_index(index); + } + + for existing_positive in &self.positive { + // S & ~T = Never if S <: T + if existing_positive.is_subtype_of(db, new_negative) { + *self = Self::default(); + self.positive.insert(Type::Never); + return; + } + // A & ~B = A if A and B are disjoint + if existing_positive.is_disjoint_from(db, new_negative) { + return; + } + } + + self.negative.insert(new_negative); + } + } + } + + /// Tries to simplify any constrained typevars in the intersection: + /// + /// - If the intersection contains a positive entry for exactly one of the constraints, we can + /// remove the typevar (effectively replacing it with that one positive constraint). + /// + /// - If the intersection contains negative entries for all but one of the constraints, we can + /// remove the negative constraints and replace the typevar with the remaining positive + /// constraint. + /// + /// - If the intersection contains negative entries for all of the constraints, the overall + /// intersection is `Never`. + fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) { + let mut to_add = SmallVec::<[Type<'db>; 1]>::new(); + let mut positive_to_remove = SmallVec::<[usize; 1]>::new(); + + for (typevar_index, ty) in self.positive.iter().enumerate() { + let Type::TypeVar(typevar) = ty else { + continue; + }; + let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + typevar.bound_or_constraints(db) + else { + continue; + }; + + // Determine which constraints appear as positive entries in the intersection. Note + // that we shouldn't have duplicate entries in the positive or negative lists, so we + // don't need to worry about finding any particular constraint more than once. + let constraints = constraints.elements(db); + let mut positive_constraint_count = 0; + for positive in &self.positive { + // This linear search should be fine as long as we don't encounter typevars with + // thousands of constraints. + positive_constraint_count += constraints + .iter() + .filter(|c| c.is_subtype_of(db, *positive)) + .count(); + } + + // If precisely one constraint appears as a positive element, we can replace the + // typevar with that positive constraint. + if positive_constraint_count == 1 { + positive_to_remove.push(typevar_index); + continue; + } + + // Determine which constraints appear as negative entries in the intersection. + let mut to_remove = Vec::with_capacity(constraints.len()); + let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect(); + for (negative_index, negative) in self.negative.iter().enumerate() { + // This linear search should be fine as long as we don't encounter typevars with + // thousands of constraints. + let matching_constraints = constraints + .iter() + .enumerate() + .filter(|(_, c)| c.is_subtype_of(db, *negative)); + for (constraint_index, _) in matching_constraints { + to_remove.push(negative_index); + remaining_constraints[constraint_index] = None; + } + } + + let mut iter = remaining_constraints.into_iter().flatten(); + let Some(remaining_constraint) = iter.next() else { + // All of the typevar constraints have been removed, so the entire intersection is + // `Never`. + *self = Self::default(); + self.positive.insert(Type::Never); + return; + }; + + let more_than_one_remaining_constraint = iter.next().is_some(); + if more_than_one_remaining_constraint { + // This typevar cannot be simplified. + continue; + } + + // Only one typevar constraint remains. Remove all of the negative constraints, and + // replace the typevar itself with the remaining positive constraint. + to_add.push(remaining_constraint); + positive_to_remove.push(typevar_index); + } + + // We don't need to sort the positive list, since we only append to it in increasing order. + for index in positive_to_remove.into_iter().rev() { + self.positive.swap_remove_index(index); + } + + for remaining_constraint in to_add { + self.add_positive(db, remaining_constraint); + } + } + + fn build(mut self, db: &'db dyn Db) -> Type<'db> { + self.simplify_constrained_typevars(db); + match (self.positive.len(), self.negative.len()) { + (0, 0) => Type::object(db), + (1, 0) => self.positive[0], + _ => { + self.positive.shrink_to_fit(); + self.negative.shrink_to_fit(); + Type::Intersection(IntersectionType::new(db, self.positive, self.negative)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{IntersectionBuilder, Type, UnionBuilder, UnionType}; + + use crate::db::tests::setup_db; + use crate::types::{KnownClass, Truthiness}; + + use test_case::test_case; + + #[test] + fn build_union_no_elements() { + let db = setup_db(); + + let empty_union = UnionBuilder::new(&db).build(); + assert_eq!(empty_union, Type::Never); + } + + #[test] + fn build_union_single_element() { + let db = setup_db(); + + let t0 = Type::IntLiteral(0); + let union = UnionType::from_elements(&db, [t0]); + assert_eq!(union, t0); + } + + #[test] + fn build_union_two_elements() { + let db = setup_db(); + + let t0 = Type::IntLiteral(0); + let t1 = Type::IntLiteral(1); + let union = UnionType::from_elements(&db, [t0, t1]).expect_union(); + + assert_eq!(union.elements(&db), &[t0, t1]); + } + + #[test] + fn build_intersection_empty_intersection_equals_object() { + let db = setup_db(); + + let intersection = IntersectionBuilder::new(&db).build(); + assert_eq!(intersection, Type::object(&db)); + } + + #[test_case(Type::BooleanLiteral(true))] + #[test_case(Type::BooleanLiteral(false))] + #[test_case(Type::AlwaysTruthy)] + #[test_case(Type::AlwaysFalsy)] + fn build_intersection_simplify_split_bool(t_splitter: Type) { + let db = setup_db(); + let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue; + + // We add t_object in various orders (in first or second position) in + // the tests below to ensure that the boolean simplification eliminates + // everything from the intersection, not just `bool`. + let t_object = Type::object(&db); + let t_bool = KnownClass::Bool.to_instance(&db); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_object) + .add_positive(t_bool) + .add_negative(t_splitter) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_bool) + .add_positive(t_object) + .add_negative(t_splitter) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); + + let ty = IntersectionBuilder::new(&db) + .add_positive(t_object) + .add_negative(t_splitter) + .add_positive(t_bool) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); + + let ty = IntersectionBuilder::new(&db) + .add_negative(t_splitter) + .add_positive(t_object) + .add_positive(t_bool) + .build(); + assert_eq!(ty, Type::BooleanLiteral(!bool_value)); + } +} diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs similarity index 95% rename from crates/red_knot_python_semantic/src/types/call.rs rename to crates/ty_python_semantic/src/types/call.rs index 27c10e5432a0d..f77cc429a5731 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -1,11 +1,11 @@ use super::context::InferContext; -use super::{CallableSignature, Signature, Signatures, Type}; +use super::{Signature, Type}; use crate::Db; mod arguments; -mod bind; +pub(crate) mod bind; pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments}; -pub(super) use bind::{Bindings, CallableBinding}; +pub(super) use bind::{Binding, Bindings, CallableBinding}; /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was /// unsuccessful. diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs new file mode 100644 index 0000000000000..77a4eb3976eca --- /dev/null +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -0,0 +1,369 @@ +use std::borrow::Cow; +use std::ops::{Deref, DerefMut}; + +use itertools::{Either, Itertools}; +use ruff_python_ast as ast; + +use crate::Db; +use crate::types::KnownClass; +use crate::types::tuple::{TupleSpec, TupleType}; + +use super::Type; + +/// Arguments for a single call, in source order. +#[derive(Clone, Debug, Default)] +pub(crate) struct CallArguments<'a>(Vec>); + +impl<'a> CallArguments<'a> { + /// Create `CallArguments` from AST arguments + pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self { + arguments + .arguments_source_order() + .map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(arg) => match arg { + ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, + _ => Argument::Positional, + }, + ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { + if let Some(arg) = arg { + Argument::Keyword(&arg.id) + } else { + Argument::Keywords + } + } + }) + .collect() + } + + /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front + /// of this argument list. (If `bound_self` is none, we return the argument list + /// unmodified.) + pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { + if bound_self.is_some() { + let arguments = std::iter::once(Argument::Synthetic) + .chain(self.0.iter().copied()) + .collect(); + Cow::Owned(CallArguments(arguments)) + } else { + Cow::Borrowed(self) + } + } + + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + pub(crate) fn iter(&self) -> impl Iterator> + '_ { + self.0.iter().copied() + } +} + +impl<'a> FromIterator> for CallArguments<'a> { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +#[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, + /// A positional argument. + Positional, + /// A starred positional argument (e.g. `*args`). + Variadic, + /// A keyword argument (e.g. `a=1`). + Keyword(&'a str), + /// The double-starred keywords argument (e.g. `**kwargs`). + Keywords, +} + +/// Arguments for a single call, in source order, along with inferred types for each argument. +#[derive(Clone, Debug, Default)] +pub(crate) struct CallArgumentTypes<'a, 'db> { + arguments: CallArguments<'a>, + types: Vec>, +} + +impl<'a, 'db> CallArgumentTypes<'a, 'db> { + /// Create a [`CallArgumentTypes`] with no arguments. + pub(crate) fn none() -> Self { + Self::default() + } + + /// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument + /// types. + pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { + let types: Vec<_> = positional_tys.into_iter().collect(); + let arguments = CallArguments(vec![Argument::Positional; types.len()]); + 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(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 } + } + + pub(crate) fn types(&self) -> &[Type<'db>] { + &self.types + } + + /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front + /// of this argument list. (If `bound_self` is none, we return the argument list + /// unmodified.) + pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { + if let Some(bound_self) = bound_self { + let arguments = CallArguments( + std::iter::once(Argument::Synthetic) + .chain(self.arguments.0.iter().copied()) + .collect(), + ); + let types = std::iter::once(bound_self) + .chain(self.types.iter().copied()) + .collect(); + Cow::Owned(CallArgumentTypes { arguments, types }) + } else { + Cow::Borrowed(self) + } + } + + pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { + self.arguments.iter().zip(self.types.iter().copied()) + } + + /// Returns an iterator on performing [argument type expansion]. + /// + /// Each element of the iterator represents a set of argument lists, where each argument list + /// contains the same arguments, but with one or more of the argument types expanded. + /// + /// [argument type expansion]: https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion + pub(crate) fn expand(&self, db: &'db dyn Db) -> impl Iterator>>> + '_ { + /// Represents the state of the expansion process. + /// + /// This is useful to avoid cloning the initial types vector if none of the types can be + /// expanded. + enum State<'a, 'db> { + Initial(&'a Vec>), + Expanded(Vec>>), + } + + impl<'db> State<'_, 'db> { + fn len(&self) -> usize { + match self { + State::Initial(_) => 1, + State::Expanded(expanded) => expanded.len(), + } + } + + fn iter(&self) -> impl Iterator>> + '_ { + match self { + State::Initial(types) => std::slice::from_ref(*types).iter(), + State::Expanded(expanded) => expanded.iter(), + } + } + } + + let mut index = 0; + + std::iter::successors(Some(State::Initial(&self.types)), move |previous| { + // Find the next type that can be expanded. + let expanded_types = loop { + let arg_type = self.types.get(index)?; + if let Some(expanded_types) = expand_type(db, *arg_type) { + break expanded_types; + } + index += 1; + }; + + let mut expanded_arg_types = Vec::with_capacity(expanded_types.len() * previous.len()); + + for pre_expanded_types in previous.iter() { + for subtype in &expanded_types { + let mut new_expanded_types = pre_expanded_types.clone(); + new_expanded_types[index] = *subtype; + expanded_arg_types.push(new_expanded_types); + } + } + + // Increment the index to move to the next argument type for the next iteration. + index += 1; + + Some(State::Expanded(expanded_arg_types)) + }) + .skip(1) // Skip the initial state, which has no expanded types. + .map(|state| match state { + State::Initial(_) => unreachable!("initial state should be skipped"), + State::Expanded(expanded) => expanded, + }) + } +} + +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 + } +} + +/// Expands a type into its possible subtypes, if applicable. +/// +/// Returns [`None`] if the type cannot be expanded. +fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { + // TODO: Expand enums to their variants + match ty { + Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Bool) => { + Some(vec![ + Type::BooleanLiteral(true), + Type::BooleanLiteral(false), + ]) + } + Type::Tuple(tuple_type) => { + // Note: This should only account for tuples of known length, i.e., `tuple[bool, ...]` + // should not be expanded here. + let tuple = tuple_type.tuple(db); + if !matches!(tuple, TupleSpec::Fixed(_)) { + return None; + } + let expanded = tuple + .all_elements() + .map(|element| { + if let Some(expanded) = expand_type(db, *element) { + Either::Left(expanded.into_iter()) + } else { + Either::Right(std::iter::once(*element)) + } + }) + .multi_cartesian_product() + .map(|types| TupleType::from_elements(db, types)) + .collect::>(); + if expanded.len() == 1 { + // There are no elements in the tuple type that can be expanded. + None + } else { + Some(expanded) + } + } + Type::Union(union) => Some(union.iter(db).copied().collect()), + // We don't handle `type[A | B]` here because it's already stored in the expanded form + // i.e., `type[A] | type[B]` which is handled by the `Type::Union` case. + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::db::tests::setup_db; + use crate::types::tuple::TupleType; + use crate::types::{KnownClass, Type, UnionType}; + + use super::expand_type; + + #[test] + fn expand_union_type() { + let db = setup_db(); + let types = [ + KnownClass::Int.to_instance(&db), + KnownClass::Str.to_instance(&db), + KnownClass::Bytes.to_instance(&db), + ]; + let union_type = UnionType::from_elements(&db, types); + let expanded = expand_type(&db, union_type).unwrap(); + assert_eq!(expanded.len(), types.len()); + assert_eq!(expanded, types); + } + + #[test] + fn expand_bool_type() { + let db = setup_db(); + let bool_instance = KnownClass::Bool.to_instance(&db); + let expanded = expand_type(&db, bool_instance).unwrap(); + let expected_types = [Type::BooleanLiteral(true), Type::BooleanLiteral(false)]; + assert_eq!(expanded.len(), expected_types.len()); + assert_eq!(expanded, expected_types); + } + + #[test] + fn expand_tuple_type() { + let db = setup_db(); + + let int_ty = KnownClass::Int.to_instance(&db); + let str_ty = KnownClass::Str.to_instance(&db); + let bytes_ty = KnownClass::Bytes.to_instance(&db); + let bool_ty = KnownClass::Bool.to_instance(&db); + let true_ty = Type::BooleanLiteral(true); + let false_ty = Type::BooleanLiteral(false); + + // Empty tuple + let empty_tuple = TupleType::empty(&db); + let expanded = expand_type(&db, empty_tuple); + assert!(expanded.is_none()); + + // None of the elements can be expanded. + let tuple_type1 = TupleType::from_elements(&db, [int_ty, str_ty]); + let expanded = expand_type(&db, tuple_type1); + assert!(expanded.is_none()); + + // All elements can be expanded. + let tuple_type2 = TupleType::from_elements( + &db, + [ + bool_ty, + UnionType::from_elements(&db, [int_ty, str_ty, bytes_ty]), + ], + ); + let expected_types = [ + TupleType::from_elements(&db, [true_ty, int_ty]), + TupleType::from_elements(&db, [true_ty, str_ty]), + TupleType::from_elements(&db, [true_ty, bytes_ty]), + TupleType::from_elements(&db, [false_ty, int_ty]), + TupleType::from_elements(&db, [false_ty, str_ty]), + TupleType::from_elements(&db, [false_ty, bytes_ty]), + ]; + let expanded = expand_type(&db, tuple_type2).unwrap(); + assert_eq!(expanded, expected_types); + + // Mixed set of elements where some can be expanded while others cannot be. + let tuple_type3 = TupleType::from_elements( + &db, + [ + bool_ty, + int_ty, + UnionType::from_elements(&db, [str_ty, bytes_ty]), + str_ty, + ], + ); + let expected_types = [ + TupleType::from_elements(&db, [true_ty, int_ty, str_ty, str_ty]), + TupleType::from_elements(&db, [true_ty, int_ty, bytes_ty, str_ty]), + TupleType::from_elements(&db, [false_ty, int_ty, str_ty, str_ty]), + TupleType::from_elements(&db, [false_ty, int_ty, bytes_ty, str_ty]), + ]; + let expanded = expand_type(&db, tuple_type3).unwrap(); + assert_eq!(expanded, expected_types); + + // Variable-length tuples are not expanded. + let variable_length_tuple = TupleType::mixed( + &db, + [bool_ty], + int_ty, + [UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty], + ); + let expanded = expand_type(&db, variable_length_tuple); + assert!(expanded.is_none()); + } +} diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs new file mode 100644 index 0000000000000..18cb5817733ff --- /dev/null +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -0,0 +1,2868 @@ +//! When analyzing a call site, we create _bindings_, which match and type-check the actual +//! arguments against the parameters of the callable. Like with +//! [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::collections::HashSet; +use std::fmt; + +use itertools::Itertools; +use ruff_db::parsed::parsed_module; +use smallvec::{SmallVec, smallvec}; + +use super::{ + Argument, CallArgumentTypes, CallArguments, CallError, CallErrorKind, InferContext, Signature, + Type, +}; +use crate::db::Db; +use crate::dunder_all::dunder_all_names; +use crate::place::{Boundness, Place}; +use crate::types::diagnostic::{ + 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::function::{ + DataclassTransformerParams, FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral, +}; +use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError}; +use crate::types::signatures::{Parameter, ParameterForm, Parameters}; +use crate::types::tuple::TupleType; +use crate::types::{ + BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType, + MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, + WrapperDescriptorKind, ide_support, todo_type, +}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; +use ruff_python_ast as ast; + +/// Binding information for a possible union of callables. At a call site, the arguments must be +/// 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)] +pub(crate) struct Bindings<'db> { + /// The type that is (hopefully) callable. + callable_type: Type<'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]>, + + conflicting_forms: Box<[bool]>, +} + +impl<'db> Bindings<'db> { + /// Creates a new `Bindings` from an iterator of [`Bindings`]s. Panics if the iterator is + /// empty. + pub(crate) fn from_union(callable_type: Type<'db>, elements: I) -> Self + where + I: IntoIterator>, + { + let elements: SmallVec<_> = elements + .into_iter() + .flat_map(|s| s.elements.into_iter()) + .collect(); + assert!(!elements.is_empty()); + Self { + callable_type, + elements, + argument_forms: Box::from([]), + conflicting_forms: Box::from([]), + } + } + + pub(crate) fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) { + if self.callable_type == before { + self.callable_type = after; + } + for binding in &mut self.elements { + binding.replace_callable_type(before, after); + } + } + + pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) { + for binding in &mut self.elements { + binding.dunder_call_is_possibly_unbound = true; + } + } + + /// Match the arguments of a call site against the parameters of a collection of possibly + /// unioned, possibly overloaded signatures. + /// + /// 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(mut self, arguments: &CallArguments<'_>) -> Self { + let mut argument_forms = vec![None; arguments.len()]; + let mut conflicting_forms = vec![false; arguments.len()]; + for binding in &mut self.elements { + binding.match_parameters(arguments, &mut argument_forms, &mut conflicting_forms); + } + self.argument_forms = argument_forms.into(); + self.conflicting_forms = conflicting_forms.into(); + self + } + + /// 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 check_types( + mut self, + db: &'db dyn Db, + argument_types: &CallArgumentTypes<'_, 'db>, + ) -> Result> { + for element in &mut self.elements { + element.check_types(db, argument_types); + } + + self.evaluate_known_cases(db); + + // In order of precedence: + // + // - If every union element is Ok, then the union is too. + // - If any element has a BindingError, the union has a BindingError. + // - If every element is NotCallable, then the union is also NotCallable. + // - Otherwise, the elements are some mixture of Ok, NotCallable, and PossiblyNotCallable. + // The union as a whole is PossiblyNotCallable. + // + // For example, the union type `Callable[[int], int] | None` may not be callable at all, + // because the `None` element in this union has no `__call__` method. + // + // On the other hand, the union type `Callable[[int], int] | Callable[[str], str]` is + // always *callable*, but it would produce a `BindingError` if an inhabitant of this type + // was called with a single `int` argument passed in. That's because the second element in + // the union doesn't accept an `int` when it's called: it only accepts a `str`. + let mut all_ok = true; + let mut any_binding_error = false; + let mut all_not_callable = true; + 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)); + } + + if all_ok { + Ok(self) + } else if any_binding_error { + Err(CallError(CallErrorKind::BindingError, Box::new(self))) + } else if all_not_callable { + Err(CallError(CallErrorKind::NotCallable, Box::new(self))) + } else { + Err(CallError( + CallErrorKind::PossiblyNotCallable, + Box::new(self), + )) + } + } + + pub(crate) fn is_single(&self) -> bool { + self.elements.len() == 1 + } + + pub(crate) fn single_element(&self) -> Option<&CallableBinding<'db>> { + match self.elements.as_slice() { + [element] => Some(element), + _ => None, + } + } + + pub(crate) fn callable_type(&self) -> Type<'db> { + self.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`. + pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + if let [binding] = self.elements.as_slice() { + return binding.return_type(); + } + UnionType::from_elements(db, self.into_iter().map(CallableBinding::return_type)) + } + + /// Report diagnostics for all of the errors that occurred when trying to match actual + /// arguments to formal parameters. If the callable is a union, or has multiple overloads, we + /// report a single diagnostic if we couldn't match any union element or overload. + /// TODO: Update this to add subdiagnostics about how we failed to match each union element and + /// overload. + pub(crate) fn report_diagnostics( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + ) { + // If all union elements are not callable, report that the union as a whole is not + // callable. + if self.into_iter().all(|b| !b.is_callable()) { + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + builder.into_diagnostic(format_args!( + "Object of type `{}` is not callable", + self.callable_type().display(context.db()) + )); + } + return; + } + + for (index, conflicting_form) in self.conflicting_forms.iter().enumerate() { + if *conflicting_form { + let node = BindingError::get_node(node, Some(index)); + if let Some(builder) = context.report_lint(&CONFLICTING_ARGUMENT_FORMS, node) { + builder.into_diagnostic( + "Argument is used as both a value and a type form in call", + ); + } + } + } + + // If this is not a union, then report a diagnostic for any + // errors as normal. + if let Some(binding) = self.single_element() { + binding.report_diagnostics(context, node, None); + return; + } + + for binding in self { + let union_diag = UnionDiagnostic { + callable_type: self.callable_type(), + binding, + }; + binding.report_diagnostics(context, node, Some(&union_diag)); + } + } + + /// 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) { + let to_bool = |ty: &Option>, default: bool| -> bool { + if let Some(Type::BooleanLiteral(value)) = ty { + *value + } else { + // TODO: emit a diagnostic if we receive `bool` + default + } + }; + + // Each special case listed here should have a corresponding clause in `Type::bindings`. + for binding in &mut self.elements { + let binding_type = binding.callable_type; + for (overload_index, overload) in binding.matching_overloads_mut() { + match binding_type { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { + match overload.parameter_types() { + [_, Some(owner)] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new(db, function, *owner), + )); + } + [Some(instance), None] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new( + db, + function, + instance.to_meta_type(db), + ), + )); + } + _ => {} + } + } else if function.has_known_decorator(db, FunctionDecorators::STATICMETHOD) + { + overload.set_return_type(Type::FunctionLiteral(function)); + } 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::BoundMethod(BoundMethodType::new( + db, function, *first, + ))); + } + } + } + + Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => { + if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] = + overload.parameter_types() + { + if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { + match overload.parameter_types() { + [_, _, Some(owner)] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new(db, *function, *owner), + )); + } + + [_, Some(instance), None] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new( + db, + *function, + instance.to_meta_type(db), + ), + )); + } + + _ => {} + } + } else if function + .has_known_decorator(db, FunctionDecorators::STATICMETHOD) + { + overload.set_return_type(*function_ty); + } else { + match overload.parameter_types() { + [_, Some(instance), _] if instance.is_none(db) => { + overload.set_return_type(*function_ty); + } + [_, Some(instance), _] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new(db, *function, *instance), + )); + } + + _ => {} + } + } + } + } + + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => { + match overload.parameter_types() { + [ + Some(property @ Type::PropertyInstance(_)), + Some(instance), + .., + ] if instance.is_none(db) => { + overload.set_return_type(*property); + } + [ + Some(Type::PropertyInstance(property)), + Some(Type::KnownInstance(KnownInstanceType::TypeAliasType( + type_alias, + ))), + .., + ] if property.getter(db).is_some_and(|getter| { + getter + .into_function_literal() + .is_some_and(|f| f.name(db) == "__name__") + }) => + { + overload + .set_return_type(Type::string_literal(db, type_alias.name(db))); + } + [ + Some(Type::PropertyInstance(property)), + Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), + .., + ] => { + match property + .getter(db) + .and_then(Type::into_function_literal) + .map(|f| f.name(db).as_str()) + { + Some("__name__") => { + overload.set_return_type(Type::string_literal( + db, + typevar.name(db), + )); + } + Some("__bound__") => { + overload.set_return_type( + typevar + .upper_bound(db) + .unwrap_or_else(|| Type::none(db)), + ); + } + Some("__constraints__") => { + overload.set_return_type(TupleType::from_elements( + db, + typevar.constraints(db).into_iter().flatten().copied(), + )); + } + Some("__default__") => { + overload.set_return_type( + typevar.default_ty(db).unwrap_or_else(|| { + KnownClass::NoDefaultType.to_instance(db) + }), + ); + } + _ => {} + } + } + [Some(Type::PropertyInstance(property)), Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call(db, &CallArgumentTypes::positional([*instance])) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); + } else { + overload.errors.push(BindingError::InternalCallError( + "calling the getter failed", + )); + overload.set_return_type(Type::unknown()); + } + } else { + overload.errors.push(BindingError::InternalCallError( + "property has no getter", + )); + overload.set_return_type(Type::Never); + } + } + _ => {} + } + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { + match overload.parameter_types() { + [Some(instance), ..] if instance.is_none(db) => { + overload.set_return_type(Type::PropertyInstance(property)); + } + [Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call(db, &CallArgumentTypes::positional([*instance])) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); + } else { + overload.errors.push(BindingError::InternalCallError( + "calling the getter failed", + )); + overload.set_return_type(Type::unknown()); + } + } else { + overload.set_return_type(Type::Never); + overload.errors.push(BindingError::InternalCallError( + "property has no getter", + )); + } + } + _ => {} + } + } + + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { + if let [ + Some(Type::PropertyInstance(property)), + Some(instance), + Some(value), + .., + ] = overload.parameter_types() + { + if let Some(setter) = property.setter(db) { + if let Err(_call_error) = setter.try_call( + db, + &CallArgumentTypes::positional([*instance, *value]), + ) { + overload.errors.push(BindingError::InternalCallError( + "calling the setter failed", + )); + } + } else { + overload.errors.push(BindingError::InternalCallError( + "property has no setter", + )); + } + } + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { + if let [Some(instance), Some(value), ..] = overload.parameter_types() { + if let Some(setter) = property.setter(db) { + if let Err(_call_error) = setter.try_call( + db, + &CallArgumentTypes::positional([*instance, *value]), + ) { + overload.errors.push(BindingError::InternalCallError( + "calling the setter failed", + )); + } + } else { + overload.errors.push(BindingError::InternalCallError( + "property has no setter", + )); + } + } + } + + Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)) => { + if let [Some(Type::StringLiteral(prefix)), None, None] = + overload.parameter_types() + { + overload.set_return_type(Type::BooleanLiteral( + literal.value(db).starts_with(prefix.value(db)), + )); + } + } + + Type::DataclassTransformer(params) => { + if let [Some(Type::FunctionLiteral(function))] = overload.parameter_types() + { + overload.set_return_type(Type::FunctionLiteral( + function.with_dataclass_transformer_params(db, params), + )); + } + } + + Type::BoundMethod(bound_method) + if bound_method.self_instance(db).is_property_instance() => + { + match bound_method.function(db).name(db).as_str() { + "setter" => { + if let [Some(_), Some(setter)] = overload.parameter_types() { + let mut ty_property = bound_method.self_instance(db); + if let Type::PropertyInstance(property) = ty_property { + ty_property = + Type::PropertyInstance(PropertyInstanceType::new( + db, + property.getter(db), + Some(*setter), + )); + } + overload.set_return_type(ty_property); + } + } + "getter" => { + if let [Some(_), Some(getter)] = overload.parameter_types() { + let mut ty_property = bound_method.self_instance(db); + if let Type::PropertyInstance(property) = ty_property { + ty_property = + Type::PropertyInstance(PropertyInstanceType::new( + db, + Some(*getter), + property.setter(db), + )); + } + overload.set_return_type(ty_property); + } + } + "deleter" => { + // TODO: we do not store deleters yet + let ty_property = bound_method.self_instance(db); + overload.set_return_type(ty_property); + } + _ => { + // Fall back to typeshed stubs for all other methods + } + } + } + + 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::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::GenericContext) => { + if let [Some(ty)] = overload.parameter_types() { + // TODO: Handle generic functions, and unions/intersections of + // generic types + overload.set_return_type(match ty { + Type::ClassLiteral(class) => match class.generic_context(db) { + Some(generic_context) => TupleType::from_elements( + db, + generic_context + .variables(db) + .iter() + .map(|typevar| Type::TypeVar(*typevar)), + ), + None => Type::none(db), + }, + + _ => Type::none(db), + }); + } + } + + Some(KnownFunction::DunderAllNames) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(match ty { + Type::ModuleLiteral(module_literal) => { + let all_names = module_literal + .module(db) + .file() + .map(|file| dunder_all_names(db, file)) + .unwrap_or_default(); + match all_names { + Some(names) => { + let mut names = names.iter().collect::>(); + names.sort(); + TupleType::from_elements( + db, + names.iter().map(|name| { + Type::string_literal(db, name.as_str()) + }), + ) + } + None => Type::none(db), + } + } + _ => Type::none(db), + }); + } + } + + Some(KnownFunction::AllMembers) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(TupleType::from_elements( + db, + ide_support::all_members(db, *ty) + .into_iter() + .sorted() + .map(|member| Type::string_literal(db, &member.name)), + )); + } + } + + Some(KnownFunction::TopMaterialization) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(ty.top_materialization(db)); + } + } + + Some(KnownFunction::BottomMaterialization) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(ty.bottom_materialization(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::IsProtocol) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty.into_class_literal() + .is_some_and(|class| class.is_protocol(db)), + )); + } + } + + Some(KnownFunction::GetProtocolMembers) => { + if let [Some(Type::ClassLiteral(class))] = overload.parameter_types() { + if let Some(protocol_class) = class.into_protocol_class(db) { + let member_names = protocol_class + .interface(db) + .members(db) + .map(|member| Type::string_literal(db, member.name())); + let specialization = UnionType::from_elements(db, member_names); + overload.set_return_type( + KnownClass::FrozenSet + .to_specialized_instance(db, [specialization]), + ); + } + } + } + + 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)) { + Place::Type(ty, Boundness::Bound) => { + if ty.is_dynamic() { + // Here, we attempt to model the fact that an attribute lookup on + // a dynamic type could fail + + union_with_default(ty) + } else { + ty + } + } + Place::Type(ty, Boundness::PossiblyUnbound) => { + union_with_default(ty) + } + Place::Unbound => default, + }, + ); + } + + Some(KnownFunction::Dataclass) => { + if let [ + init, + repr, + eq, + order, + unsafe_hash, + frozen, + match_args, + kw_only, + slots, + weakref_slot, + ] = overload.parameter_types() + { + let mut params = DataclassParams::empty(); + + if to_bool(init, true) { + params |= DataclassParams::INIT; + } + if to_bool(repr, true) { + params |= DataclassParams::REPR; + } + if to_bool(eq, true) { + params |= DataclassParams::EQ; + } + if to_bool(order, false) { + params |= DataclassParams::ORDER; + } + if to_bool(unsafe_hash, false) { + params |= DataclassParams::UNSAFE_HASH; + } + if to_bool(frozen, false) { + params |= DataclassParams::FROZEN; + } + if to_bool(match_args, true) { + params |= DataclassParams::MATCH_ARGS; + } + if to_bool(kw_only, false) { + params |= DataclassParams::KW_ONLY; + } + if to_bool(slots, false) { + params |= DataclassParams::SLOTS; + } + if to_bool(weakref_slot, false) { + params |= DataclassParams::WEAKREF_SLOT; + } + + overload.set_return_type(Type::DataclassDecorator(params)); + } + + // `dataclass` being used as a non-decorator + if let [Some(Type::ClassLiteral(class_literal))] = + overload.parameter_types() + { + let params = DataclassParams::default(); + overload.set_return_type(Type::from(ClassLiteral::new( + db, + class_literal.name(db), + class_literal.body_scope(db), + class_literal.known(db), + Some(params), + class_literal.dataclass_transformer_params(db), + ))); + } + } + + Some(KnownFunction::DataclassTransform) => { + if let [ + eq_default, + order_default, + kw_only_default, + frozen_default, + _field_specifiers, + _kwargs, + ] = overload.parameter_types() + { + let mut params = DataclassTransformerParams::empty(); + + if to_bool(eq_default, true) { + params |= DataclassTransformerParams::EQ_DEFAULT; + } + if to_bool(order_default, false) { + params |= DataclassTransformerParams::ORDER_DEFAULT; + } + if to_bool(kw_only_default, false) { + params |= DataclassTransformerParams::KW_ONLY_DEFAULT; + } + if to_bool(frozen_default, false) { + params |= DataclassTransformerParams::FROZEN_DEFAULT; + } + + overload.set_return_type(Type::DataclassTransformer(params)); + } + } + + _ => { + // Ideally, either the implementation, or exactly one of the overloads + // of the function can have the dataclass_transform decorator applied. + // However, we do not yet enforce this, and in the case of multiple + // applications of the decorator, we will only consider the last one + // for the return value, since the prior ones will be over-written. + let return_type = function_type + .iter_overloads_and_implementation(db) + .filter_map(|function_overload| { + function_overload.dataclass_transformer_params(db).map( + |params| { + // This is a call to a custom function that was decorated with `@dataclass_transformer`. + // If this function was called with a keyword argument like `order=False`, we extract + // the argument type and overwrite the corresponding flag in `dataclass_params` after + // constructing them from the `dataclass_transformer`-parameter defaults. + + let mut dataclass_params = + DataclassParams::from(params); + + if let Some(Some(Type::BooleanLiteral(order))) = + overload + .signature + .parameters() + .keyword_by_name("order") + .map(|(idx, _)| idx) + .and_then(|idx| { + overload.parameter_types().get(idx) + }) + { + dataclass_params + .set(DataclassParams::ORDER, *order); + } + + Type::DataclassDecorator(dataclass_params) + }, + ) + }) + .last(); + + if let Some(return_type) = return_type { + overload.set_return_type(return_type); + } + } + }, + + Type::ClassLiteral(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)); + } + } + + Some(KnownClass::Property) => { + if let [getter, setter, ..] = overload.parameter_types() { + overload.set_return_type(Type::PropertyInstance( + PropertyInstanceType::new(db, *getter, *setter), + )); + } + } + + Some(KnownClass::Tuple) if overload_index == 1 => { + // `tuple(range(42))` => `tuple[int, ...]` + // BUT `tuple((1, 2))` => `tuple[Literal[1], Literal[2]]` rather than `tuple[Literal[1, 2], ...]` + if let [Some(argument)] = overload.parameter_types() { + let overridden_return = + argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| { + // Some awkward special handling is required here because of the fact + // that calling `try_iterate()` on `Never` returns `Never`, + // but `tuple[Never, ...]` eagerly simplifies to `tuple[()]`, + // which will cause us to emit false positives if we index into the tuple. + // Using `tuple[Unknown, ...]` avoids these false positives. + let specialization = if argument.is_never() { + Type::unknown() + } else { + argument.try_iterate(db).expect( + "try_iterate() should not fail on a type \ + assignable to `Iterable`", + ) + }; + TupleType::homogeneous(db, specialization) + }); + overload.set_return_type(overridden_return); + } + } + + _ => {} + }, + + Type::SpecialForm(SpecialFormType::TypedDict) => { + overload.set_return_type(todo_type!("TypedDict")); + } + + // Not a special case + _ => {} + } + } + } + } +} + +impl<'a, 'db> IntoIterator for &'a Bindings<'db> { + type Item = &'a CallableBinding<'db>; + type IntoIter = std::slice::Iter<'a, CallableBinding<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.elements.iter() + } +} + +impl<'a, 'db> IntoIterator for &'a mut Bindings<'db> { + type Item = &'a mut CallableBinding<'db>; + type IntoIter = std::slice::IterMut<'a, CallableBinding<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.elements.iter_mut() + } +} + +impl<'db> From> for Bindings<'db> { + fn from(from: CallableBinding<'db>) -> Bindings<'db> { + Bindings { + callable_type: from.callable_type, + elements: smallvec![from], + argument_forms: Box::from([]), + conflicting_forms: Box::from([]), + } + } +} + +impl<'db> From> for Bindings<'db> { + fn from(from: Binding<'db>) -> Bindings<'db> { + let callable_type = from.callable_type; + let signature_type = from.signature_type; + let callable_binding = CallableBinding { + callable_type, + signature_type, + dunder_call_is_possibly_unbound: false, + bound_type: None, + overload_call_return_type: None, + matching_overload_index: None, + overloads: smallvec![from], + }; + Bindings { + callable_type, + elements: smallvec![callable_binding], + argument_forms: Box::from([]), + conflicting_forms: Box::from([]), + } + } +} + +/// Binding information for a single callable. If the callable is overloaded, there is a separate +/// [`Binding`] for each overload. +/// +/// For a successful binding, each argument is mapped to one of the callable's formal parameters. +/// If the callable has multiple overloads, the first one that matches is used as the overall +/// binding match. +/// +/// If the arguments cannot be matched to formal parameters, we store information about the +/// specific errors that occurred when trying to match them up. If the callable has multiple +/// overloads, we store this error information for each overload. +#[derive(Debug)] +pub(crate) struct CallableBinding<'db> { + /// The type that is (hopefully) callable. + pub(crate) callable_type: Type<'db>, + + /// The type we'll use for error messages referring to details of the called signature. For + /// calls to functions this will be the same as `callable_type`; for other callable instances + /// it may be a `__call__` method. + pub(crate) signature_type: Type<'db>, + + /// If this is a callable object (i.e. called via a `__call__` method), the boundness of + /// that call method. + pub(crate) dunder_call_is_possibly_unbound: bool, + + /// The type of the bound `self` or `cls` parameter if this signature is for a bound method. + pub(crate) bound_type: Option>, + + /// The return type of this overloaded callable. + /// + /// This is [`Some`] only in the following cases: + /// 1. Argument type expansion was performed and one of the expansions evaluated successfully + /// for all of the argument lists, or + /// 2. Overload call evaluation was ambiguous, meaning that multiple overloads matched the + /// argument lists, but they all had different return types + /// + /// For (1), the final return type is the union of all the return types of the matched + /// overloads for the expanded argument lists. + /// + /// For (2), the final return type is [`Unknown`]. + /// + /// [`Unknown`]: crate::types::DynamicType::Unknown + overload_call_return_type: Option>, + + /// The index of the overload that matched for this overloaded callable. + /// + /// This is [`Some`] only for step 1 and 4 of the [overload call evaluation algorithm][1]. + /// + /// The main use of this field is to surface the diagnostics for a matching overload directly + /// instead of using the `no-matching-overload` diagnostic. This is mentioned in the spec: + /// + /// > If only one candidate overload remains, it is the winning match. Evaluate it as if it + /// > were a non-overloaded function call and stop. + /// + /// Other steps of the algorithm do not set this field because this use case isn't relevant for + /// them. + /// + /// [1]: https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation + matching_overload_index: Option, + + /// The bindings of each overload of this callable. Will be empty if the type is not callable. + /// + /// By using `SmallVec`, we avoid an extra heap allocation for the common case of a + /// non-overloaded callable. + overloads: SmallVec<[Binding<'db>; 1]>, +} + +impl<'db> CallableBinding<'db> { + pub(crate) fn from_overloads( + signature_type: Type<'db>, + overloads: impl IntoIterator>, + ) -> Self { + let overloads = overloads + .into_iter() + .map(|signature| Binding::single(signature_type, signature)) + .collect(); + Self { + callable_type: signature_type, + signature_type, + dunder_call_is_possibly_unbound: false, + bound_type: None, + overload_call_return_type: None, + matching_overload_index: None, + overloads, + } + } + + pub(crate) fn not_callable(signature_type: Type<'db>) -> Self { + Self { + callable_type: signature_type, + signature_type, + dunder_call_is_possibly_unbound: false, + bound_type: None, + overload_call_return_type: None, + matching_overload_index: None, + overloads: smallvec![], + } + } + + pub(crate) fn with_bound_type(mut self, bound_type: Type<'db>) -> Self { + self.bound_type = Some(bound_type); + self + } + + fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) { + if self.callable_type == before { + self.callable_type = after; + } + for binding in &mut self.overloads { + binding.replace_callable_type(before, after); + } + } + + fn match_parameters( + &mut self, + arguments: &CallArguments<'_>, + argument_forms: &mut [Option], + conflicting_forms: &mut [bool], + ) { + // If this callable is a bound method, prepend the self instance onto the arguments list + // before checking. + let arguments = arguments.with_self(self.bound_type); + + for overload in &mut self.overloads { + overload.match_parameters(arguments.as_ref(), argument_forms, conflicting_forms); + } + } + + fn check_types(&mut self, db: &'db dyn Db, argument_types: &CallArgumentTypes<'_, 'db>) { + // If this callable is a bound method, prepend the self instance onto the arguments list + // before checking. + let argument_types = argument_types.with_self(self.bound_type); + + // Step 1: Check the result of the arity check which is done by `match_parameters` + let matching_overload_indexes = match self.matching_overload_index() { + MatchingOverloadIndex::None => { + // If no candidate overloads remain from the arity check, we can stop here. We + // still perform type checking for non-overloaded function to provide better user + // experience. + if let [overload] = self.overloads.as_mut_slice() { + overload.check_types(db, argument_types.as_ref(), argument_types.types()); + } + return; + } + MatchingOverloadIndex::Single(index) => { + // If only one candidate overload remains, it is the winning match. Evaluate it as + // a regular (non-overloaded) call. + self.matching_overload_index = Some(index); + self.overloads[index].check_types( + db, + argument_types.as_ref(), + argument_types.types(), + ); + return; + } + MatchingOverloadIndex::Multiple(indexes) => { + // If two or more candidate overloads remain, proceed to step 2. + indexes + } + }; + + let snapshotter = CallableBindingSnapshotter::new(matching_overload_indexes); + + // State of the bindings _before_ evaluating (type checking) the matching overloads using + // the non-expanded argument types. + let pre_evaluation_snapshot = snapshotter.take(self); + + // Step 2: Evaluate each remaining overload as a regular (non-overloaded) call to determine + // whether it is compatible with the supplied argument list. + for (_, overload) in self.matching_overloads_mut() { + overload.check_types(db, argument_types.as_ref(), argument_types.types()); + } + + match self.matching_overload_index() { + MatchingOverloadIndex::None => { + // If all overloads result in errors, proceed to step 3. + } + MatchingOverloadIndex::Single(_) => { + // If only one overload evaluates without error, it is the winning match. + return; + } + MatchingOverloadIndex::Multiple(indexes) => { + // If two or more candidate overloads remain, proceed to step 4. + // TODO: Step 4 + + // Step 5 + self.filter_overloads_using_any_or_unknown(db, argument_types.types(), &indexes); + + // We're returning here because this shouldn't lead to argument type expansion. + return; + } + } + + // Step 3: Perform "argument type expansion". Reference: + // https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion + let mut expansions = argument_types.expand(db).peekable(); + + if expansions.peek().is_none() { + // Return early if there are no argument types to expand. + return; + } + + // State of the bindings _after_ evaluating (type checking) the matching overloads using + // the non-expanded argument types. + let post_evaluation_snapshot = snapshotter.take(self); + + // Restore the bindings state to the one prior to the type checking step in preparation + // for evaluating the expanded argument lists. + snapshotter.restore(self, pre_evaluation_snapshot); + + for expanded_argument_lists in expansions { + // This is the merged state of the bindings after evaluating all of the expanded + // argument lists. This will be the final state to restore the bindings to if all of + // the expanded argument lists evaluated successfully. + let mut merged_evaluation_state: Option> = None; + + let mut return_types = Vec::new(); + + for expanded_argument_types in &expanded_argument_lists { + let pre_evaluation_snapshot = snapshotter.take(self); + + for (_, overload) in self.matching_overloads_mut() { + overload.check_types(db, argument_types.as_ref(), expanded_argument_types); + } + + let return_type = match self.matching_overload_index() { + MatchingOverloadIndex::None => None, + MatchingOverloadIndex::Single(index) => { + Some(self.overloads[index].return_type()) + } + MatchingOverloadIndex::Multiple(matching_overload_indexes) => { + // TODO: Step 4 + + self.filter_overloads_using_any_or_unknown( + db, + expanded_argument_types, + &matching_overload_indexes, + ); + + Some(self.return_type()) + } + }; + + // This split between initializing and updating the merged evaluation state is + // required because otherwise it's difficult to differentiate between the + // following: + // 1. An initial unmatched overload becomes a matched overload when evaluating the + // first argument list + // 2. An unmatched overload after evaluating the first argument list becomes a + // matched overload when evaluating the second argument list + if let Some(merged_evaluation_state) = merged_evaluation_state.as_mut() { + merged_evaluation_state.update(self); + } else { + merged_evaluation_state = Some(snapshotter.take(self)); + } + + // Restore the bindings state before evaluating the next argument list. + snapshotter.restore(self, pre_evaluation_snapshot); + + if let Some(return_type) = return_type { + return_types.push(return_type); + } else { + // No need to check the remaining argument lists if the current argument list + // doesn't evaluate successfully. Move on to expanding the next argument type. + break; + } + } + + if return_types.len() == expanded_argument_lists.len() { + // Restore the bindings state to the one that merges the bindings state evaluating + // each of the expanded argument list. + // + // Note that this needs to happen *before* setting the return type, because this + // will restore the return type to the one before argument type expansion. + if let Some(merged_evaluation_state) = merged_evaluation_state { + snapshotter.restore(self, merged_evaluation_state); + } + + // If the number of return types is equal to the number of expanded argument lists, + // they all evaluated successfully. So, we need to combine their return types by + // union to determine the final return type. + self.overload_call_return_type = + Some(OverloadCallReturnType::ArgumentTypeExpansion( + UnionType::from_elements(db, return_types), + )); + + return; + } + } + + // If the type expansion didn't yield any successful return type, we need to restore the + // bindings state back to the one after the type checking step using the non-expanded + // argument types. This is necessary because we restore the state to the pre-evaluation + // snapshot when processing the expanded argument lists. + snapshotter.restore(self, post_evaluation_snapshot); + } + + /// Filter overloads based on [`Any`] or [`Unknown`] argument types. + /// + /// This is the step 5 of the [overload call evaluation algorithm][1]. + /// + /// The filtering works on the remaining overloads that are present at the + /// `matching_overload_indexes` and are filtered out by marking them as unmatched overloads + /// using the [`mark_as_unmatched_overload`] method. + /// + /// [`Any`]: crate::types::DynamicType::Any + /// [`Unknown`]: crate::types::DynamicType::Unknown + /// [`mark_as_unmatched_overload`]: Binding::mark_as_unmatched_overload + /// [1]: https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation + fn filter_overloads_using_any_or_unknown( + &mut self, + db: &'db dyn Db, + argument_types: &[Type<'db>], + matching_overload_indexes: &[usize], + ) { + // These are the parameter indexes that matches the arguments that participate in the + // filtering process. + // + // The parameter types at these indexes have at least one overload where the type isn't + // gradual equivalent to the parameter types at the same index for other overloads. + let mut participating_parameter_indexes = HashSet::new(); + + // These only contain the top materialized argument types for the corresponding + // participating parameter indexes. + let mut top_materialized_argument_types = vec![]; + + for (argument_index, argument_type) in argument_types.iter().enumerate() { + let mut first_parameter_type: Option> = None; + let mut participating_parameter_index = None; + + for overload_index in matching_overload_indexes { + let overload = &self.overloads[*overload_index]; + let Some(parameter_index) = overload.argument_parameters[argument_index] else { + // There is no parameter for this argument in this overload. + break; + }; + // TODO: For an unannotated `self` / `cls` parameter, the type should be + // `typing.Self` / `type[typing.Self]` + let current_parameter_type = overload.signature.parameters()[parameter_index] + .annotated_type() + .unwrap_or(Type::unknown()); + if let Some(first_parameter_type) = first_parameter_type { + if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { + participating_parameter_index = Some(parameter_index); + break; + } + } else { + first_parameter_type = Some(current_parameter_type); + } + } + + if let Some(parameter_index) = participating_parameter_index { + participating_parameter_indexes.insert(parameter_index); + top_materialized_argument_types.push(argument_type.top_materialization(db)); + } + } + + let top_materialized_argument_type = + TupleType::from_elements(db, top_materialized_argument_types); + + // A flag to indicate whether we've found the overload that makes the remaining overloads + // unmatched for the given argument types. + let mut filter_remaining_overloads = false; + + for (upto, current_index) in matching_overload_indexes.iter().enumerate() { + if filter_remaining_overloads { + self.overloads[*current_index].mark_as_unmatched_overload(); + continue; + } + let mut parameter_types = Vec::with_capacity(argument_types.len()); + for argument_index in 0..argument_types.len() { + // The parameter types at the current argument index. + let mut current_parameter_types = vec![]; + for overload_index in &matching_overload_indexes[..=upto] { + let overload = &self.overloads[*overload_index]; + let Some(parameter_index) = overload.argument_parameters[argument_index] else { + // There is no parameter for this argument in this overload. + continue; + }; + if !participating_parameter_indexes.contains(¶meter_index) { + // This parameter doesn't participate in the filtering process. + continue; + } + // TODO: For an unannotated `self` / `cls` parameter, the type should be + // `typing.Self` / `type[typing.Self]` + let parameter_type = overload.signature.parameters()[parameter_index] + .annotated_type() + .unwrap_or(Type::unknown()); + current_parameter_types.push(parameter_type); + } + if current_parameter_types.is_empty() { + continue; + } + parameter_types.push(UnionType::from_elements(db, current_parameter_types)); + } + if top_materialized_argument_type + .is_assignable_to(db, TupleType::from_elements(db, parameter_types)) + { + filter_remaining_overloads = true; + } + } + + // Once this filtering process is applied for all arguments, examine the return types of + // the remaining overloads. If the resulting return types for all remaining overloads are + // equivalent, proceed to step 6. + let are_return_types_equivalent_for_all_matching_overloads = { + let mut matching_overloads = self.matching_overloads(); + if let Some(first_overload_return_type) = matching_overloads + .next() + .map(|(_, overload)| overload.return_type()) + { + matching_overloads.all(|(_, overload)| { + overload + .return_type() + .is_equivalent_to(db, first_overload_return_type) + }) + } else { + // No matching overload + true + } + }; + + if !are_return_types_equivalent_for_all_matching_overloads { + // Overload matching is ambiguous. + self.overload_call_return_type = Some(OverloadCallReturnType::Ambiguous); + } + } + + fn as_result(&self) -> Result<(), CallErrorKind> { + if !self.is_callable() { + return Err(CallErrorKind::NotCallable); + } + + if self.has_binding_errors() { + return Err(CallErrorKind::BindingError); + } + + if self.dunder_call_is_possibly_unbound { + return Err(CallErrorKind::PossiblyNotCallable); + } + + Ok(()) + } + + fn is_callable(&self) -> bool { + !self.overloads.is_empty() + } + + /// Returns whether there were any errors binding this call site. If the callable has multiple + /// overloads, they must _all_ have errors. + pub(crate) fn has_binding_errors(&self) -> bool { + self.matching_overloads().next().is_none() + } + + /// Returns the index of the matching overload in the form of [`MatchingOverloadIndex`]. + fn matching_overload_index(&self) -> MatchingOverloadIndex { + let mut matching_overloads = self.matching_overloads(); + match matching_overloads.next() { + None => MatchingOverloadIndex::None, + Some((first, _)) => { + if let Some((second, _)) = matching_overloads.next() { + let mut indexes = vec![first, second]; + for (index, _) in matching_overloads { + indexes.push(index); + } + MatchingOverloadIndex::Multiple(indexes) + } else { + MatchingOverloadIndex::Single(first) + } + } + } + } + + /// Returns an iterator over all the overloads that matched for this call binding. + pub(crate) fn matching_overloads(&self) -> impl Iterator)> { + self.overloads + .iter() + .enumerate() + .filter(|(_, overload)| overload.as_result().is_ok()) + } + + /// Returns an iterator over all the mutable overloads that matched for this call binding. + pub(crate) fn matching_overloads_mut( + &mut self, + ) -> impl Iterator)> { + self.overloads + .iter_mut() + .enumerate() + .filter(|(_, overload)| overload.as_result().is_ok()) + } + + /// Returns the return type of this call. + /// + /// For a valid call, this is the return type of either a successful argument type expansion of + /// an overloaded function, or the return type of the first overload that the arguments matched + /// against. + /// + /// For an invalid call to a non-overloaded function, this is the return type of the function. + /// + /// For an invalid call to an overloaded function, we return `Type::unknown`, since we cannot + /// make any useful conclusions about which overload was intended to be called. + pub(crate) fn return_type(&self) -> Type<'db> { + if let Some(overload_call_return_type) = self.overload_call_return_type { + return match overload_call_return_type { + OverloadCallReturnType::ArgumentTypeExpansion(return_type) => return_type, + OverloadCallReturnType::Ambiguous => Type::unknown(), + }; + } + if let Some((_, first_overload)) = self.matching_overloads().next() { + return first_overload.return_type(); + } + if let [overload] = self.overloads.as_slice() { + return overload.return_type(); + } + Type::unknown() + } + + fn report_diagnostics( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + union_diag: Option<&UnionDiagnostic<'_, '_>>, + ) { + if !self.is_callable() { + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Object of type `{}` is not callable", + self.callable_type.display(context.db()), + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + return; + } + + if self.dunder_call_is_possibly_unbound { + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Object of type `{}` is not callable (possibly unbound `__call__` method)", + self.callable_type.display(context.db()), + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + return; + } + + match self.overloads.as_slice() { + [] => {} + [overload] => { + let callable_description = + CallableDescription::new(context.db(), self.signature_type); + overload.report_diagnostics( + context, + node, + self.signature_type, + callable_description.as_ref(), + union_diag, + None, + ); + } + _overloads => { + // TODO: This should probably be adapted to handle more + // types of callables[1]. At present, it just handles + // standard function and method calls. + // + // [1]: https://github.com/astral-sh/ty/issues/274#issuecomment-2881856028 + let function_type_and_kind = match self.signature_type { + Type::FunctionLiteral(function) => Some((FunctionKind::Function, function)), + Type::BoundMethod(bound_method) => Some(( + FunctionKind::BoundMethod, + bound_method.function(context.db()), + )), + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + Some((FunctionKind::MethodWrapper, function)) + } + _ => None, + }; + + // If there is a single matching overload, the diagnostics should be reported + // directly for that overload. + if let Some(matching_overload_index) = self.matching_overload_index { + let callable_description = + CallableDescription::new(context.db(), self.signature_type); + let matching_overload = + function_type_and_kind.map(|(kind, function)| MatchingOverloadLiteral { + index: matching_overload_index, + kind, + function, + }); + self.overloads[matching_overload_index].report_diagnostics( + context, + node, + self.signature_type, + callable_description.as_ref(), + union_diag, + matching_overload.as_ref(), + ); + return; + } + + let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, node) else { + return; + }; + let callable_description = + CallableDescription::new(context.db(), self.callable_type); + let mut diag = builder.into_diagnostic(format_args!( + "No overload{} matches arguments", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + if let Some((kind, function)) = function_type_and_kind { + let (overloads, implementation) = + function.overloads_and_implementation(context.db()); + + if let Some(spans) = overloads + .first() + .and_then(|overload| overload.spans(context.db())) + { + let mut sub = + SubDiagnostic::new(Severity::Info, "First overload defined here"); + sub.annotate(Annotation::primary(spans.signature)); + diag.sub(sub); + } + + diag.info(format_args!( + "Possible overloads for {kind} `{}`:", + function.name(context.db()) + )); + + for overload in overloads.iter().take(MAXIMUM_OVERLOADS) { + diag.info(format_args!( + " {}", + overload.signature(context.db(), None).display(context.db()) + )); + } + if overloads.len() > MAXIMUM_OVERLOADS { + diag.info(format_args!( + "... omitted {remaining} overloads", + remaining = overloads.len() - MAXIMUM_OVERLOADS + )); + } + + if let Some(spans) = + implementation.and_then(|function| function.spans(context.db())) + { + let mut sub = SubDiagnostic::new( + Severity::Info, + "Overload implementation defined here", + ); + sub.annotate(Annotation::primary(spans.signature)); + diag.sub(sub); + } + } + + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } + } +} + +impl<'a, 'db> IntoIterator for &'a CallableBinding<'db> { + type Item = &'a Binding<'db>; + type IntoIter = std::slice::Iter<'a, Binding<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.overloads.iter() + } +} + +#[derive(Debug, Copy, Clone)] +enum OverloadCallReturnType<'db> { + ArgumentTypeExpansion(Type<'db>), + Ambiguous, +} + +#[derive(Debug)] +enum MatchingOverloadIndex { + /// No matching overloads found. + None, + + /// Exactly one matching overload found at the given index. + Single(usize), + + /// Multiple matching overloads found at the given indexes. + Multiple(Vec), +} + +struct ArgumentMatcher<'a, 'db> { + parameters: &'a Parameters<'db>, + argument_forms: &'a mut [Option], + conflicting_forms: &'a mut [bool], + errors: &'a mut Vec>, + + /// The parameter that each argument is matched with. + argument_parameters: Vec>, + /// Whether each parameter has been matched with an argument. + parameter_matched: Vec, + next_positional: usize, + first_excess_positional: Option, + num_synthetic_args: usize, +} + +impl<'a, 'db> ArgumentMatcher<'a, 'db> { + fn new( + arguments: &CallArguments, + parameters: &'a Parameters<'db>, + argument_forms: &'a mut [Option], + conflicting_forms: &'a mut [bool], + errors: &'a mut Vec>, + ) -> Self { + Self { + parameters, + argument_forms, + conflicting_forms, + errors, + argument_parameters: vec![None; arguments.len()], + parameter_matched: vec![false; parameters.len()], + next_positional: 0, + first_excess_positional: None, + num_synthetic_args: 0, + } + } + + fn get_argument_index(&self, argument_index: usize) -> Option { + if argument_index >= self.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 - self.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 + } + } + + fn assign_argument( + &mut self, + argument_index: usize, + argument: Argument<'a>, + parameter_index: usize, + parameter: &Parameter<'db>, + positional: bool, + ) { + if !matches!(argument, Argument::Synthetic) { + if let Some(existing) = self.argument_forms[argument_index - self.num_synthetic_args] + .replace(parameter.form) + { + if existing != parameter.form { + self.conflicting_forms[argument_index - self.num_synthetic_args] = true; + } + } + } + if self.parameter_matched[parameter_index] { + if !parameter.is_variadic() && !parameter.is_keyword_variadic() { + self.errors.push(BindingError::ParameterAlreadyAssigned { + argument_index: self.get_argument_index(argument_index), + parameter: ParameterContext::new(parameter, parameter_index, positional), + }); + } + } + self.argument_parameters[argument_index] = Some(parameter_index); + self.parameter_matched[parameter_index] = true; + } + + fn match_positional( + &mut self, + argument_index: usize, + argument: Argument<'a>, + ) -> Result<(), ()> { + if matches!(argument, Argument::Synthetic) { + self.num_synthetic_args += 1; + } + let Some((parameter_index, parameter)) = self + .parameters + .get_positional(self.next_positional) + .map(|param| (self.next_positional, param)) + .or_else(|| self.parameters.variadic()) + else { + self.first_excess_positional.get_or_insert(argument_index); + self.next_positional += 1; + return Err(()); + }; + self.next_positional += 1; + self.assign_argument( + argument_index, + argument, + parameter_index, + parameter, + !parameter.is_variadic(), + ); + Ok(()) + } + + fn match_keyword( + &mut self, + argument_index: usize, + argument: Argument<'a>, + name: &str, + ) -> Result<(), ()> { + let Some((parameter_index, parameter)) = self + .parameters + .keyword_by_name(name) + .or_else(|| self.parameters.keyword_variadic()) + else { + self.errors.push(BindingError::UnknownArgument { + argument_name: ast::name::Name::new(name), + argument_index: self.get_argument_index(argument_index), + }); + return Err(()); + }; + self.assign_argument(argument_index, argument, parameter_index, parameter, false); + Ok(()) + } + + fn finish(self) -> Box<[Option]> { + if let Some(first_excess_argument_index) = self.first_excess_positional { + self.errors.push(BindingError::TooManyPositionalArguments { + first_excess_argument_index: self.get_argument_index(first_excess_argument_index), + expected_positional_count: self.parameters.positional().count(), + provided_positional_count: self.next_positional, + }); + } + + let mut missing = vec![]; + for (index, matched) in self.parameter_matched.iter().copied().enumerate() { + if !matched { + let param = &self.parameters[index]; + if param.is_variadic() + || param.is_keyword_variadic() + || param.default_type().is_some() + { + // variadic/keywords and defaulted arguments are not required + continue; + } + missing.push(ParameterContext::new(param, index, false)); + } + } + if !missing.is_empty() { + self.errors.push(BindingError::MissingArguments { + parameters: ParameterContexts(missing), + }); + } + + self.argument_parameters.into_boxed_slice() + } +} + +struct ArgumentTypeChecker<'a, 'db> { + db: &'db dyn Db, + signature: &'a Signature<'db>, + arguments: &'a CallArguments<'a>, + argument_types: &'a [Type<'db>], + argument_parameters: &'a [Option], + parameter_tys: &'a mut [Option>], + errors: &'a mut Vec>, + + specialization: Option>, + inherited_specialization: Option>, +} + +impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { + fn new( + db: &'db dyn Db, + signature: &'a Signature<'db>, + arguments: &'a CallArguments<'a>, + argument_types: &'a [Type<'db>], + argument_parameters: &'a [Option], + parameter_tys: &'a mut [Option>], + errors: &'a mut Vec>, + ) -> Self { + Self { + db, + signature, + arguments, + argument_types, + argument_parameters, + parameter_tys, + errors, + specialization: None, + inherited_specialization: None, + } + } + + fn enumerate_argument_types( + &self, + ) -> impl Iterator, Argument<'a>, Type<'db>)> + 'a { + let mut iter = (self.arguments.iter()) + .zip(self.argument_types.iter().copied()) + .enumerate(); + let mut num_synthetic_args = 0; + std::iter::from_fn(move || { + let (argument_index, (argument, argument_type)) = iter.next()?; + let adjusted_argument_index = if matches!(argument, Argument::Synthetic) { + // If 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 + num_synthetic_args += 1; + None + } else { + // 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) + }; + Some(( + argument_index, + adjusted_argument_index, + argument, + argument_type, + )) + }) + } + + fn infer_specialization(&mut self) { + if self.signature.generic_context.is_none() + && self.signature.inherited_generic_context.is_none() + { + return; + } + + let parameters = self.signature.parameters(); + let mut builder = SpecializationBuilder::new(self.db); + for (argument_index, adjusted_argument_index, _, argument_type) in + self.enumerate_argument_types() + { + 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]; + let Some(expected_type) = parameter.annotated_type() else { + continue; + }; + if let Err(error) = builder.infer(expected_type, argument_type) { + self.errors.push(BindingError::SpecializationError { + error, + argument_index: adjusted_argument_index, + }); + } + } + self.specialization = self.signature.generic_context.map(|gc| builder.build(gc)); + self.inherited_specialization = self.signature.inherited_generic_context.map(|gc| { + // The inherited generic context is used when inferring the specialization of a generic + // class from a constructor call. In this case (only), we promote any typevars that are + // inferred as a literal to the corresponding instance type. + builder + .build(gc) + .apply_type_mapping(self.db, &TypeMapping::PromoteLiterals) + }); + } + + fn check_argument_type( + &mut self, + argument_index: usize, + adjusted_argument_index: Option, + argument: Argument<'a>, + mut argument_type: Type<'db>, + ) { + 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. + return; + }; + let parameters = self.signature.parameters(); + let parameter = ¶meters[parameter_index]; + if let Some(mut expected_ty) = parameter.annotated_type() { + if let Some(specialization) = self.specialization { + argument_type = argument_type.apply_specialization(self.db, specialization); + expected_ty = expected_ty.apply_specialization(self.db, specialization); + } + if let Some(inherited_specialization) = self.inherited_specialization { + argument_type = + argument_type.apply_specialization(self.db, inherited_specialization); + expected_ty = expected_ty.apply_specialization(self.db, inherited_specialization); + } + if !argument_type.is_assignable_to(self.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: adjusted_argument_index, + 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(self.db, [existing, argument_type]); + self.parameter_tys[parameter_index] = Some(union); + } + } + + fn check_argument_types(&mut self) { + for (argument_index, adjusted_argument_index, argument, argument_type) in + self.enumerate_argument_types() + { + self.check_argument_type( + argument_index, + adjusted_argument_index, + argument, + argument_type, + ); + } + } + + fn finish(self) -> (Option>, Option>) { + (self.specialization, self.inherited_specialization) + } +} + +/// Binding information for one of the overloads of a callable. +#[derive(Debug)] +pub(crate) struct Binding<'db> { + pub(crate) signature: Signature<'db>, + + /// The type that is (hopefully) callable. + pub(crate) callable_type: Type<'db>, + + /// The type we'll use for error messages referring to details of the called signature. For + /// calls to functions this will be the same as `callable_type`; for other callable instances + /// it may be a `__call__` method. + pub(crate) signature_type: Type<'db>, + + /// Return type of the call. + return_ty: Type<'db>, + + /// The specialization that was inferred from the argument types, if the callable is generic. + specialization: Option>, + + /// The specialization that was inferred for a class method's containing generic class, if it + /// is being used to infer a specialization for the class. + inherited_specialization: Option>, + + /// 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]>, + + /// Bound types for parameters, in parameter source order, or `None` if no argument was matched + /// to that parameter. + parameter_tys: Box<[Option>]>, + + /// Call binding errors, if any. + errors: Vec>, +} + +impl<'db> Binding<'db> { + pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Binding<'db> { + Binding { + signature, + callable_type: signature_type, + signature_type, + return_ty: Type::unknown(), + specialization: None, + inherited_specialization: None, + argument_parameters: Box::from([]), + parameter_tys: Box::from([]), + errors: vec![], + } + } + + fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) { + if self.callable_type == before { + self.callable_type = after; + } + } + + pub(crate) fn match_parameters( + &mut self, + arguments: &CallArguments<'_>, + argument_forms: &mut [Option], + conflicting_forms: &mut [bool], + ) { + let parameters = self.signature.parameters(); + let mut matcher = ArgumentMatcher::new( + arguments, + parameters, + argument_forms, + conflicting_forms, + &mut self.errors, + ); + for (argument_index, argument) in arguments.iter().enumerate() { + match argument { + Argument::Positional | Argument::Synthetic => { + let _ = matcher.match_positional(argument_index, argument); + } + Argument::Keyword(name) => { + let _ = matcher.match_keyword(argument_index, argument, name); + } + Argument::Variadic | Argument::Keywords => { + // TODO + continue; + } + } + } + self.return_ty = self.signature.return_ty.unwrap_or(Type::unknown()); + self.parameter_tys = vec![None; parameters.len()].into_boxed_slice(); + self.argument_parameters = matcher.finish(); + } + + fn check_types( + &mut self, + db: &'db dyn Db, + arguments: &CallArguments<'_>, + argument_types: &[Type<'db>], + ) { + let mut checker = ArgumentTypeChecker::new( + db, + &self.signature, + arguments, + argument_types, + &self.argument_parameters, + &mut self.parameter_tys, + &mut self.errors, + ); + + // If this overload is generic, first see if we can infer a specialization of the function + // from the arguments that were passed in. + checker.infer_specialization(); + + checker.check_argument_types(); + (self.specialization, self.inherited_specialization) = checker.finish(); + if let Some(specialization) = self.specialization { + self.return_ty = self.return_ty.apply_specialization(db, specialization); + } + if let Some(inherited_specialization) = self.inherited_specialization { + self.return_ty = self + .return_ty + .apply_specialization(db, inherited_specialization); + } + } + + pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { + self.return_ty = return_ty; + } + + pub(crate) fn return_type(&self) -> Type<'db> { + self.return_ty + } + + pub(crate) fn inherited_specialization(&self) -> Option> { + self.inherited_specialization + } + + /// Returns the bound types for each parameter, in parameter source order, or `None` if no + /// argument was matched to that parameter. + pub(crate) fn parameter_types(&self) -> &[Option>] { + &self.parameter_tys + } + + pub(crate) fn arguments_for_parameter<'a>( + &'a self, + argument_types: &'a CallArgumentTypes<'a, 'db>, + parameter_index: usize, + ) -> impl Iterator, Type<'db>)> + 'a { + argument_types + .iter() + .zip(&self.argument_parameters) + .filter(move |(_, argument_parameter)| { + argument_parameter.is_some_and(|ap| ap == parameter_index) + }) + .map(|(arg_and_type, _)| arg_and_type) + } + + /// Mark this overload binding as an unmatched overload. + fn mark_as_unmatched_overload(&mut self) { + self.errors.push(BindingError::UnmatchedOverload); + } + + fn report_diagnostics( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + callable_ty: Type<'db>, + callable_description: Option<&CallableDescription>, + union_diag: Option<&UnionDiagnostic<'_, '_>>, + matching_overload: Option<&MatchingOverloadLiteral<'db>>, + ) { + for error in &self.errors { + error.report_diagnostic( + context, + node, + callable_ty, + callable_description, + union_diag, + matching_overload, + ); + } + } + + fn as_result(&self) -> Result<(), CallErrorKind> { + if !self.errors.is_empty() { + return Err(CallErrorKind::BindingError); + } + Ok(()) + } + + fn snapshot(&self) -> BindingSnapshot<'db> { + BindingSnapshot { + return_ty: self.return_ty, + specialization: self.specialization, + inherited_specialization: self.inherited_specialization, + argument_parameters: self.argument_parameters.clone(), + parameter_tys: self.parameter_tys.clone(), + errors: self.errors.clone(), + } + } + + fn restore(&mut self, snapshot: BindingSnapshot<'db>) { + let BindingSnapshot { + return_ty, + specialization, + inherited_specialization, + argument_parameters, + parameter_tys, + errors, + } = snapshot; + + self.return_ty = return_ty; + self.specialization = specialization; + self.inherited_specialization = inherited_specialization; + self.argument_parameters = argument_parameters; + self.parameter_tys = parameter_tys; + self.errors = errors; + } + + /// Returns a vector where each index corresponds to an argument position, + /// and the value is the parameter index that argument maps to (if any). + pub(crate) fn argument_to_parameter_mapping(&self) -> &[Option] { + &self.argument_parameters + } +} + +#[derive(Clone, Debug)] +struct BindingSnapshot<'db> { + return_ty: Type<'db>, + specialization: Option>, + inherited_specialization: Option>, + argument_parameters: Box<[Option]>, + parameter_tys: Box<[Option>]>, + errors: Vec>, +} + +#[derive(Clone, Debug)] +struct CallableBindingSnapshot<'db> { + overload_return_type: Option>, + + /// Represents the snapshot of the matched overload bindings. + /// + /// The reason that this only contains the matched overloads are: + /// 1. Avoid creating snapshots for the overloads that have been filtered by the arity check + /// 2. Avoid duplicating errors when merging the snapshots on a successful evaluation of all + /// the expanded argument lists + matching_overloads: Vec<(usize, BindingSnapshot<'db>)>, +} + +impl<'db> CallableBindingSnapshot<'db> { + /// Update the state of the matched overload bindings in this snapshot with the current + /// state in the given `binding`. + fn update(&mut self, binding: &CallableBinding<'db>) { + // Here, the `snapshot` is the state of this binding for the previous argument list and + // `binding` would contain the state after evaluating the current argument list. + for (snapshot, binding) in self + .matching_overloads + .iter_mut() + .map(|(index, snapshot)| (snapshot, &binding.overloads[*index])) + { + if binding.errors.is_empty() { + // If the binding has no errors, this means that the current argument list was + // evaluated successfully and this is the matching overload. + // + // Clear the errors from the snapshot of this overload to signal this change ... + snapshot.errors.clear(); + + // ... and update the snapshot with the current state of the binding. + snapshot.return_ty = binding.return_ty; + snapshot.specialization = binding.specialization; + snapshot.inherited_specialization = binding.inherited_specialization; + snapshot + .argument_parameters + .clone_from(&binding.argument_parameters); + snapshot.parameter_tys.clone_from(&binding.parameter_tys); + } + + // If the errors in the snapshot was empty, then this binding is the matching overload + // for a previously evaluated argument list. This means that we don't need to change + // any information for an already matched overload binding. + // + // If it does have errors, we could extend it with the errors from evaluating the + // current argument list. Arguably, this isn't required, since the errors in the + // snapshot should already signal that this is an unmatched overload which is why we + // don't do it. Similarly, due to this being an unmatched overload, there's no point in + // updating the binding state. + } + } +} + +/// A helper to take snapshots of the matched overload bindings for the current state of the +/// bindings. +struct CallableBindingSnapshotter(Vec); + +impl CallableBindingSnapshotter { + /// Creates a new snapshotter for the given indexes of the matched overloads. + fn new(indexes: Vec) -> Self { + debug_assert!(indexes.len() > 1); + CallableBindingSnapshotter(indexes) + } + + /// Takes a snapshot of the current state of the matched overload bindings. + /// + /// # Panics + /// + /// Panics if the indexes of the matched overloads are not valid for the given binding. + fn take<'db>(&self, binding: &CallableBinding<'db>) -> CallableBindingSnapshot<'db> { + CallableBindingSnapshot { + overload_return_type: binding.overload_call_return_type, + matching_overloads: self + .0 + .iter() + .map(|index| (*index, binding.overloads[*index].snapshot())) + .collect(), + } + } + + /// Restores the state of the matched overload bindings from the given snapshot. + fn restore<'db>( + &self, + binding: &mut CallableBinding<'db>, + snapshot: CallableBindingSnapshot<'db>, + ) { + debug_assert_eq!(self.0.len(), snapshot.matching_overloads.len()); + binding.overload_call_return_type = snapshot.overload_return_type; + for (index, snapshot) in snapshot.matching_overloads { + binding.overloads[index].restore(snapshot); + } + } +} + +/// Describes a callable for the purposes of diagnostics. +#[derive(Debug)] +pub(crate) struct CallableDescription<'a> { + name: &'a str, + kind: &'a str, +} + +impl<'db> CallableDescription<'db> { + fn new(db: &'db dyn Db, callable_type: Type<'db>) -> Option> { + match callable_type { + Type::FunctionLiteral(function) => Some(CallableDescription { + kind: "function", + name: function.name(db), + }), + Type::ClassLiteral(class_type) => Some(CallableDescription { + kind: "class", + name: class_type.name(db), + }), + Type::BoundMethod(bound_method) => Some(CallableDescription { + kind: "bound method", + name: bound_method.function(db).name(db), + }), + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + Some(CallableDescription { + kind: "method wrapper `__get__` of function", + name: function.name(db), + }) + } + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { + Some(CallableDescription { + kind: "method wrapper", + name: "`__get__` of property", + }) + } + Type::WrapperDescriptor(kind) => Some(CallableDescription { + kind: "wrapper descriptor", + name: match kind { + WrapperDescriptorKind::FunctionTypeDunderGet => "FunctionType.__get__", + WrapperDescriptorKind::PropertyDunderGet => "property.__get__", + WrapperDescriptorKind::PropertyDunderSet => "property.__set__", + }, + }), + _ => None, + } + } +} + +/// Information needed to emit a diagnostic regarding a parameter. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ParameterContext { + name: Option, + index: usize, + + /// Was the argument for this parameter passed positionally, and matched to a non-variadic + /// positional parameter? (If so, we will provide the index in the diagnostic, not just the + /// name.) + positional: bool, +} + +impl ParameterContext { + fn new(parameter: &Parameter, index: usize, positional: bool) -> Self { + Self { + name: parameter.display_name(), + index, + positional, + } + } +} + +impl std::fmt::Display for ParameterContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(name) = &self.name { + if self.positional { + write!(f, "{} (`{name}`)", self.index + 1) + } else { + write!(f, "`{name}`") + } + } else { + write!(f, "{}", self.index + 1) + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ParameterContexts(Vec); + +impl std::fmt::Display for ParameterContexts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut iter = self.0.iter(); + if let Some(first) = iter.next() { + write!(f, "{first}")?; + for param in iter { + f.write_str(", ")?; + write!(f, "{param}")?; + } + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum BindingError<'db> { + /// The type of an argument is not assignable to the annotated type of its corresponding + /// parameter. + InvalidArgumentType { + parameter: ParameterContext, + argument_index: Option, + expected_ty: Type<'db>, + provided_ty: Type<'db>, + }, + /// One or more required parameters (that is, with no default) is not supplied by any argument. + MissingArguments { parameters: ParameterContexts }, + /// A call argument can't be matched to any parameter. + UnknownArgument { + argument_name: ast::name::Name, + argument_index: Option, + }, + /// More positional arguments are provided in the call than can be handled by the signature. + TooManyPositionalArguments { + first_excess_argument_index: Option, + expected_positional_count: usize, + provided_positional_count: usize, + }, + /// Multiple arguments were provided for a single parameter. + ParameterAlreadyAssigned { + argument_index: Option, + parameter: ParameterContext, + }, + /// An inferred specialization was invalid. + SpecializationError { + error: SpecializationError<'db>, + argument_index: Option, + }, + /// The call itself might be well constructed, but an error occurred while evaluating the call. + /// We use this variant to report errors in `property.__get__` and `property.__set__`, which + /// can occur when the call to the underlying getter/setter fails. + InternalCallError(&'static str), + /// This overload binding of the callable does not match the arguments. + // TODO: We could expand this with an enum to specify why the overload is unmatched. + UnmatchedOverload, +} + +impl<'db> BindingError<'db> { + fn report_diagnostic( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + callable_ty: Type<'db>, + callable_description: Option<&CallableDescription>, + union_diag: Option<&UnionDiagnostic<'_, '_>>, + matching_overload: Option<&MatchingOverloadLiteral<'_>>, + ) { + match self { + Self::InvalidArgumentType { + parameter, + argument_index, + expected_ty, + provided_ty, + } => { + let range = Self::get_node(node, *argument_index); + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { + return; + }; + + let provided_ty_display = provided_ty.display(context.db()); + let expected_ty_display = expected_ty.display(context.db()); + + let mut diag = builder.into_diagnostic(format_args!( + "Argument{} is incorrect", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" to {kind} `{name}`") + } else { + String::new() + } + )); + diag.set_primary_message(format_args!( + "Expected `{expected_ty_display}`, found `{provided_ty_display}`" + )); + + if let Some(matching_overload) = matching_overload { + if let Some((name_span, parameter_span)) = + matching_overload.get(context.db()).and_then(|overload| { + overload.parameter_span(context.db(), Some(parameter.index)) + }) + { + let mut sub = + SubDiagnostic::new(Severity::Info, "Matching overload defined here"); + sub.annotate(Annotation::primary(name_span)); + sub.annotate( + Annotation::secondary(parameter_span) + .message("Parameter declared here"), + ); + diag.sub(sub); + diag.info(format_args!( + "Non-matching overloads for {} `{}`:", + matching_overload.kind, + matching_overload.function.name(context.db()) + )); + let (overloads, _) = matching_overload + .function + .overloads_and_implementation(context.db()); + for (overload_index, overload) in + overloads.iter().enumerate().take(MAXIMUM_OVERLOADS) + { + if overload_index == matching_overload.index { + continue; + } + diag.info(format_args!( + " {}", + overload.signature(context.db(), None).display(context.db()) + )); + } + if overloads.len() > MAXIMUM_OVERLOADS { + diag.info(format_args!( + "... omitted {remaining} overloads", + remaining = overloads.len() - MAXIMUM_OVERLOADS + )); + } + } + } else if let Some((name_span, parameter_span)) = + callable_ty.parameter_span(context.db(), Some(parameter.index)) + { + let mut sub = SubDiagnostic::new(Severity::Info, "Function defined here"); + sub.annotate(Annotation::primary(name_span)); + sub.annotate( + Annotation::secondary(parameter_span).message("Parameter declared here"), + ); + diag.sub(sub); + } + + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + + Self::TooManyPositionalArguments { + first_excess_argument_index, + expected_positional_count, + provided_positional_count, + } => { + let node = Self::get_node(node, *first_excess_argument_index); + if let Some(builder) = context.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Too many positional arguments{}: expected \ + {expected_positional_count}, got {provided_positional_count}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" to {kind} `{name}`") + } else { + String::new() + } + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } + + Self::MissingArguments { parameters } => { + if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) { + let s = if parameters.0.len() == 1 { "" } else { "s" }; + let mut diag = builder.into_diagnostic(format_args!( + "No argument{s} provided for required parameter{s} {parameters}{}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } + + Self::UnknownArgument { + argument_name, + argument_index, + } => { + let node = Self::get_node(node, *argument_index); + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Argument `{argument_name}` does not match any known parameter{}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } + + Self::ParameterAlreadyAssigned { + argument_index, + parameter, + } => { + let node = Self::get_node(node, *argument_index); + if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Multiple values provided for parameter {parameter}{}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } + + Self::SpecializationError { + error, + argument_index, + } => { + let range = Self::get_node(node, *argument_index); + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { + return; + }; + + let typevar = error.typevar(); + let argument_type = error.argument_type(); + let argument_ty_display = argument_type.display(context.db()); + + let mut diag = builder.into_diagnostic(format_args!( + "Argument{} is incorrect", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" to {kind} `{name}`") + } else { + String::new() + } + )); + diag.set_primary_message(format_args!( + "Argument type `{argument_ty_display}` does not satisfy {} of type variable `{}`", + match error { + SpecializationError::MismatchedBound {..} => "upper bound", + SpecializationError::MismatchedConstraint {..} => "constraints", + }, + typevar.name(context.db()), + )); + + if let Some(typevar_definition) = typevar.definition(context.db()) { + let module = parsed_module(context.db(), typevar_definition.file(context.db())) + .load(context.db()); + let typevar_range = typevar_definition.full_range(context.db(), &module); + let mut sub = SubDiagnostic::new(Severity::Info, "Type variable defined here"); + sub.annotate(Annotation::primary(typevar_range.into())); + diag.sub(sub); + } + + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + + Self::InternalCallError(reason) => { + let node = Self::get_node(node, None); + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + let mut diag = builder.into_diagnostic(format_args!( + "Call{} failed: {reason}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } + } + } + + Self::UnmatchedOverload => {} + } + } + + fn get_node(node: ast::AnyNodeRef, argument_index: Option) -> ast::AnyNodeRef { + // If we have a Call node and an argument index, report the diagnostic on the correct + // argument node; otherwise, report it on the entire provided node. + match (node, argument_index) { + (ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => { + match call_node + .arguments + .arguments_source_order() + .nth(argument_index) + .expect("argument index should not be out of range") + { + ast::ArgOrKeyword::Arg(expr) => expr.into(), + ast::ArgOrKeyword::Keyword(keyword) => keyword.into(), + } + } + _ => node, + } + } +} + +/// Contains additional context for union specific diagnostics. +/// +/// This is used when a function call is inconsistent with one or more variants +/// of a union. This can be used to attach sub-diagnostics that clarify that +/// the error is part of a union. +struct UnionDiagnostic<'b, 'db> { + /// The type of the union. + callable_type: Type<'db>, + /// The specific binding that failed. + binding: &'b CallableBinding<'db>, +} + +impl UnionDiagnostic<'_, '_> { + /// Adds context about any relevant union function types to the given + /// diagnostic. + fn add_union_context(&self, db: &'_ dyn Db, diag: &mut Diagnostic) { + let sub = SubDiagnostic::new( + Severity::Info, + format_args!( + "Union variant `{callable_ty}` is incompatible with this call site", + callable_ty = self.binding.callable_type.display(db), + ), + ); + diag.sub(sub); + + let sub = SubDiagnostic::new( + Severity::Info, + format_args!( + "Attempted to call union type `{}`", + self.callable_type.display(db) + ), + ); + diag.sub(sub); + } +} + +/// Represents the matching overload of a function literal that was found via the overload call +/// evaluation algorithm. +struct MatchingOverloadLiteral<'db> { + /// The position of the matching overload in the list of overloads. + index: usize, + /// The kind of function this overload is for. + kind: FunctionKind, + /// The function literal that this overload belongs to. + /// + /// This is used to retrieve the overload at the given index. + function: FunctionType<'db>, +} + +impl<'db> MatchingOverloadLiteral<'db> { + /// Returns the [`OverloadLiteral`] representing this matching overload. + fn get(&self, db: &'db dyn Db) -> Option> { + let (overloads, _) = self.function.overloads_and_implementation(db); + + // TODO: This should actually be safe to index directly but isn't so as of this writing. + // The main reason is that we've custom overload signatures that are constructed manually + // and does not belong to any file. For example, the `__get__` method of a function literal + // has a custom overloaded signature. So, when we try to retrieve the actual overloads + // above, we get an empty list of overloads because the implementation of that method + // relies on it existing in the file. + overloads.get(self.index).copied() + } +} + +#[derive(Clone, Copy, Debug)] +enum FunctionKind { + Function, + BoundMethod, + MethodWrapper, +} + +impl fmt::Display for FunctionKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FunctionKind::Function => write!(f, "function"), + FunctionKind::BoundMethod => write!(f, "bound method"), + FunctionKind::MethodWrapper => write!(f, "method wrapper `__get__` of function"), + } + } +} + +// When the number of unmatched overloads exceeds this number, we stop printing them to avoid +// excessive output. +// +// An example of a routine with many many overloads: +// https://github.com/henribru/google-api-python-client-stubs/blob/master/googleapiclient-stubs/discovery.pyi +const MAXIMUM_OVERLOADS: usize = 50; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs new file mode 100644 index 0000000000000..6fadfa7df9a5f --- /dev/null +++ b/crates/ty_python_semantic/src/types/class.rs @@ -0,0 +1,3782 @@ +use std::hash::BuildHasherDefault; +use std::sync::{LazyLock, Mutex}; + +use super::TypeVarVariance; +use super::{ + IntersectionBuilder, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType, + SubclassOfType, Truthiness, Type, TypeQualifiers, + class_base::ClassBase, + function::{FunctionDecorators, FunctionType}, + infer_expression_type, infer_unpack_types, +}; +use crate::semantic_index::definition::{Definition, DefinitionState}; +use crate::semantic_index::place::NodeWithScopeKind; +use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex, attribute_declarations}; +use crate::types::context::InferContext; +use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE}; +use crate::types::function::{DataclassTransformerParams, KnownFunction}; +use crate::types::generics::{GenericContext, Specialization, walk_specialization}; +use crate::types::infer::nearest_enclosing_class; +use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; +use crate::types::tuple::TupleType; +use crate::types::{ + BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, + KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeTransformer, + TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, infer_definition_types, +}; +use crate::{ + Db, FxOrderSet, KnownModule, Program, + module_resolver::file_to_module, + place::{ + Boundness, LookupError, LookupResult, Place, PlaceAndQualifiers, class_symbol, + known_module_symbol, place_from_bindings, place_from_declarations, + }, + semantic_index::{ + attribute_assignments, + definition::{DefinitionKind, TargetKind}, + place::ScopeId, + place_table, semantic_index, use_def_map, + }, + types::{ + CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType, + definition_expression_type, + }, +}; +use indexmap::IndexSet; +use itertools::Itertools as _; +use ruff_db::diagnostic::Span; +use ruff_db::files::File; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_python_ast::name::Name; +use ruff_python_ast::{self as ast, PythonVersion}; +use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::{FxHashSet, FxHasher}; + +type FxOrderMap = ordermap::map::OrderMap>; + +fn explicit_bases_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &[Type<'db>], + _count: u32, + _self: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction]>> { + salsa::CycleRecoveryAction::Iterate +} + +fn explicit_bases_cycle_initial<'db>( + _db: &'db dyn Db, + _self: ClassLiteral<'db>, +) -> Box<[Type<'db>]> { + Box::default() +} + +#[expect(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] +fn inheritance_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Option, + _count: u32, + _self: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn inheritance_cycle_initial<'db>( + _db: &'db dyn Db, + _self: ClassLiteral<'db>, +) -> Option { + None +} + +fn try_mro_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Result, MroError<'db>>, + _count: u32, + _self: ClassLiteral<'db>, + _specialization: Option>, +) -> salsa::CycleRecoveryAction, MroError<'db>>> { + salsa::CycleRecoveryAction::Iterate +} + +fn try_mro_cycle_initial<'db>( + db: &'db dyn Db, + self_: ClassLiteral<'db>, + specialization: Option>, +) -> Result, MroError<'db>> { + Err(MroError::cycle( + db, + self_.apply_optional_specialization(db, specialization), + )) +} + +fn try_metaclass_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Result<(Type<'db>, Option), MetaclassError<'db>>, + _count: u32, + _self: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction< + Result<(Type<'db>, Option), MetaclassError<'db>>, +> { + salsa::CycleRecoveryAction::Iterate +} + +#[allow(clippy::unnecessary_wraps)] +fn try_metaclass_cycle_initial<'db>( + _db: &'db dyn Db, + _self_: ClassLiteral<'db>, +) -> Result<(Type<'db>, Option), MetaclassError<'db>> { + Err(MetaclassError { + kind: MetaclassErrorKind::Cycle, + }) +} + +/// A category of classes with code generation capabilities (with synthesized methods). +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum CodeGeneratorKind { + /// Classes decorated with `@dataclass` or similar dataclass-like decorators + DataclassLike, + /// Classes inheriting from `typing.NamedTuple` + NamedTuple, +} + +impl CodeGeneratorKind { + pub(crate) fn from_class(db: &dyn Db, class: ClassLiteral<'_>) -> Option { + if CodeGeneratorKind::DataclassLike.matches(db, class) { + Some(CodeGeneratorKind::DataclassLike) + } else if CodeGeneratorKind::NamedTuple.matches(db, class) { + Some(CodeGeneratorKind::NamedTuple) + } else { + None + } + } + + fn matches<'db>(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { + match self { + Self::DataclassLike => { + class.dataclass_params(db).is_some() + || class + .try_metaclass(db) + .is_ok_and(|(_, transformer_params)| transformer_params.is_some()) + } + Self::NamedTuple => class.explicit_bases(db).iter().any(|base| { + base.into_class_literal() + .is_some_and(|c| c.is_known(db, KnownClass::NamedTuple)) + }), + } + } +} + +/// A specialization of a generic class with a particular assignment of types to typevars. +/// +/// # Ordering +/// Ordering is based on the generic aliases's salsa-assigned id and not on its values. +/// The id may change between runs, or when the alias was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct GenericAlias<'db> { + pub(crate) origin: ClassLiteral<'db>, + pub(crate) specialization: Specialization<'db>, +} + +pub(super) fn walk_generic_alias<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + alias: GenericAlias<'db>, + visitor: &mut V, +) { + walk_specialization(db, alias.specialization(db), visitor); +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for GenericAlias<'_> {} + +impl<'db> GenericAlias<'db> { + pub(super) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self::new( + db, + self.origin(db), + self.specialization(db).normalized_impl(db, visitor), + ) + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::new( + db, + self.origin(db), + self.specialization(db).materialize(db, variance), + ) + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + self.origin(db).definition(db) + } + + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self::new( + db, + self.origin(db), + self.specialization(db).apply_type_mapping(db, type_mapping), + ) + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + // A tuple's specialization will include all of its element types, so we don't need to also + // look in `self.tuple`. + self.specialization(db).find_legacy_typevars(db, typevars); + } +} + +impl<'db> From> for Type<'db> { + fn from(alias: GenericAlias<'db>) -> Type<'db> { + Type::GenericAlias(alias) + } +} + +/// Represents a class type, which might be a non-generic class, or a specialization of a generic +/// class. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + salsa::Supertype, + salsa::Update, + get_size2::GetSize, +)] +pub enum ClassType<'db> { + NonGeneric(ClassLiteral<'db>), + Generic(GenericAlias<'db>), +} + +#[salsa::tracked] +impl<'db> ClassType<'db> { + pub(super) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => Self::Generic(generic.normalized_impl(db, visitor)), + } + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => Self::Generic(generic.materialize(db, variance)), + } + } + + pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + match self { + Self::NonGeneric(class) => class.has_pep_695_type_params(db), + Self::Generic(generic) => generic.origin(db).has_pep_695_type_params(db), + } + } + + /// Returns the class literal and specialization for this class. For a non-generic class, this + /// is the class itself. For a generic alias, this is the alias's origin. + pub(crate) fn class_literal( + self, + db: &'db dyn Db, + ) -> (ClassLiteral<'db>, Option>) { + match self { + Self::NonGeneric(non_generic) => (non_generic, None), + Self::Generic(generic) => (generic.origin(db), Some(generic.specialization(db))), + } + } + + /// Returns the class literal and specialization for this class, with an additional + /// specialization applied if the class is generic. + pub(crate) fn class_literal_specialized( + self, + db: &'db dyn Db, + additional_specialization: Option>, + ) -> (ClassLiteral<'db>, Option>) { + match self { + Self::NonGeneric(non_generic) => (non_generic, None), + Self::Generic(generic) => ( + generic.origin(db), + Some( + generic + .specialization(db) + .apply_optional_specialization(db, additional_specialization), + ), + ), + } + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + let (class_literal, _) = self.class_literal(db); + class_literal.name(db) + } + + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + let (class_literal, _) = self.class_literal(db); + class_literal.known(db) + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let (class_literal, _) = self.class_literal(db); + class_literal.definition(db) + } + + /// Return `Some` if this class is known to be a [`SolidBase`], or `None` if it is not. + pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option> { + self.class_literal(db).0.as_solid_base(db) + } + + /// Return `true` if this class represents `known_class` + pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + self.known(db) == Some(known_class) + } + + /// Return `true` if this class represents the builtin class `object` + pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Object) + } + + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => Self::Generic(generic.apply_type_mapping(db, type_mapping)), + } + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + Self::NonGeneric(_) => {} + Self::Generic(generic) => generic.find_legacy_typevars(db, typevars), + } + } + + /// Iterate over the [method resolution order] ("MRO") of the class. + /// + /// If the MRO could not be accurately resolved, this method falls back to iterating + /// over an MRO that has the class directly inheriting from `Unknown`. Use + /// [`ClassLiteral::try_mro`] if you need to distinguish between the success and failure + /// cases rather than simply iterating over the inferred resolution order for the class. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(super) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { + let (class_literal, specialization) = self.class_literal(db); + class_literal.iter_mro(db, specialization) + } + + /// Iterate over the method resolution order ("MRO") of the class, optionally applying an + /// additional specialization to it if the class is generic. + pub(super) fn iter_mro_specialized( + self, + db: &'db dyn Db, + additional_specialization: Option>, + ) -> MroIterator<'db> { + let (class_literal, specialization) = + self.class_literal_specialized(db, additional_specialization); + class_literal.iter_mro(db, specialization) + } + + /// Is this class final? + pub(super) fn is_final(self, db: &'db dyn Db) -> bool { + let (class_literal, _) = self.class_literal(db); + class_literal.is_final(db) + } + + /// Return `true` if `other` is present in this class's MRO. + pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + self.has_relation_to(db, other, TypeRelation::Subtyping) + } + + pub(super) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + self.iter_mro(db).any(|base| { + match base { + ClassBase::Dynamic(_) => match relation { + TypeRelation::Subtyping => other.is_object(db), + TypeRelation::Assignability => !other.is_final(db), + }, + + // Protocol and Generic are not represented by a ClassType. + ClassBase::Protocol | ClassBase::Generic => false, + + ClassBase::Class(base) => match (base, other) { + (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other, + (ClassType::Generic(base), ClassType::Generic(other)) => { + base.origin(db) == other.origin(db) + && base.specialization(db).has_relation_to( + db, + other.specialization(db), + relation, + ) + } + (ClassType::Generic(_), ClassType::NonGeneric(_)) + | (ClassType::NonGeneric(_), ClassType::Generic(_)) => false, + }, + } + }) + } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + if self == other { + return true; + } + + match (self, other) { + // A non-generic class is never equivalent to a generic class. + // Two non-generic classes are only equivalent if they are equal (handled above). + (ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false, + + (ClassType::Generic(this), ClassType::Generic(other)) => { + this.origin(db) == other.origin(db) + && this + .specialization(db) + .is_equivalent_to(db, other.specialization(db)) + } + } + } + + /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. + pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + let (class_literal, specialization) = self.class_literal(db); + class_literal + .metaclass(db) + .apply_optional_specialization(db, specialization) + } + + /// Return the [`SolidBase`] that appears first in the MRO of this class. + /// + /// Returns `None` if this class does not have any solid bases in its MRO. + pub(super) fn nearest_solid_base(self, db: &'db dyn Db) -> Option> { + self.iter_mro(db) + .filter_map(ClassBase::into_class) + .find_map(|base| base.as_solid_base(db)) + } + + /// Return `true` if this class could coexist in an MRO with `other`. + /// + /// For two given classes `A` and `B`, it is often possible to say for sure + /// that there could never exist any class `C` that inherits from both `A` and `B`. + /// In these situations, this method returns `false`; in all others, it returns `true`. + pub(super) fn could_coexist_in_mro_with(self, db: &'db dyn Db, other: Self) -> bool { + if self == other { + return true; + } + + // Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call. + if self.is_final(db) { + return self.is_subclass_of(db, other); + } + if other.is_final(db) { + return other.is_subclass_of(db, self); + } + + // Two solid bases can only coexist in an MRO if one is a subclass of the other. + if self.nearest_solid_base(db).is_some_and(|solid_base_1| { + other.nearest_solid_base(db).is_some_and(|solid_base_2| { + !solid_base_1.could_coexist_in_mro_with(db, &solid_base_2) + }) + }) { + return false; + } + + // Check to see whether the metaclasses of `self` and `other` are disjoint. + // Avoid this check if the metaclass of either `self` or `other` is `type`, + // however, since we end up with infinite recursion in that case due to the fact + // that `type` is its own metaclass (and we know that `type` can coexist in an MRO + // with any other arbitrary class, anyway). + let type_class = KnownClass::Type.to_class_literal(db); + let self_metaclass = self.metaclass(db); + if self_metaclass == type_class { + return true; + } + let other_metaclass = other.metaclass(db); + if other_metaclass == type_class { + return true; + } + let Some(self_metaclass_instance) = self_metaclass.to_instance(db) else { + return true; + }; + let Some(other_metaclass_instance) = other_metaclass.to_instance(db) else { + return true; + }; + if self_metaclass_instance.is_disjoint_from(db, other_metaclass_instance) { + return false; + } + + true + } + + /// Return a type representing "the set of all instances of the metaclass of this class". + pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + self + .metaclass(db) + .to_instance(db) + .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + } + + /// Returns the class member of this class named `name`. + /// + /// The member resolves to a member on the class itself or any of its proper superclasses. + /// + /// TODO: Should this be made private...? + pub(super) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + let (class_literal, specialization) = self.class_literal(db); + class_literal.class_member_inner(db, specialization, name, policy) + } + + /// Returns the inferred type of the class member named `name`. Only bound members + /// or those marked as ClassVars are considered. + /// + /// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope + /// directly. Use [`ClassType::class_member`] if you require a method that will + /// traverse through the MRO until it finds the member. + pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + let (class_literal, specialization) = self.class_literal(db); + class_literal + .own_class_member(db, specialization, name) + .map_type(|ty| ty.apply_optional_specialization(db, specialization)) + } + + /// Look up an instance attribute (available in `__dict__`) of the given name. + /// + /// See [`Type::instance_member`] for more details. + pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + let (class_literal, specialization) = self.class_literal(db); + class_literal + .instance_member(db, specialization, name) + .map_type(|ty| ty.apply_optional_specialization(db, specialization)) + } + + /// A helper function for `instance_member` that looks up the `name` attribute only on + /// this class, not on its superclasses. + fn own_instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + let (class_literal, specialization) = self.class_literal(db); + class_literal + .own_instance_member(db, name) + .map_type(|ty| ty.apply_optional_specialization(db, specialization)) + } + + /// Return a callable type (or union of callable types) that represents the callable + /// constructor signature of this class. + pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> { + let self_ty = Type::from(self); + let metaclass_dunder_call_function_symbol = self_ty + .member_lookup_with_policy( + db, + "__call__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .place; + + if let Place::Type(Type::BoundMethod(metaclass_dunder_call_function), _) = + metaclass_dunder_call_function_symbol + { + // TODO: this intentionally diverges from step 1 in + // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable + // by always respecting the signature of the metaclass `__call__`, rather than + // using a heuristic which makes unwarranted assumptions to sometimes ignore it. + return Type::Callable(metaclass_dunder_call_function.into_callable_type(db)); + } + + let dunder_new_function_symbol = self_ty + .member_lookup_with_policy( + db, + "__new__".into(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + .place; + + let dunder_new_function = + if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) = + dunder_new_function_symbol + { + // Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class, + // then we should ignore the `__init__` and just return the `__new__` method. + let returns_non_subclass = + dunder_new_function + .signature(db) + .overloads + .iter() + .any(|signature| { + signature.return_ty.is_some_and(|return_ty| { + !return_ty.is_assignable_to( + db, + self_ty + .to_instance(db) + .expect("ClassType should be instantiable"), + ) + }) + }); + + let dunder_new_bound_method = + dunder_new_function.into_bound_method_type(db, self_ty); + + if returns_non_subclass { + return dunder_new_bound_method; + } + Some(dunder_new_bound_method) + } else { + None + }; + + let dunder_init_function_symbol = self_ty + .member_lookup_with_policy( + db, + "__init__".into(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .place; + + let correct_return_type = self_ty.to_instance(db).unwrap_or_else(Type::unknown); + + // If the class defines an `__init__` method, then we synthesize a callable type with the + // same parameters as the `__init__` method after it is bound, and with the return type of + // the concrete type of `Self`. + let synthesized_dunder_init_callable = + if let Place::Type(ty, _) = dunder_init_function_symbol { + let signature = match ty { + Type::FunctionLiteral(dunder_init_function) => { + Some(dunder_init_function.signature(db)) + } + Type::Callable(callable) => Some(callable.signatures(db)), + _ => None, + }; + + if let Some(signature) = signature { + let synthesized_signature = |signature: &Signature<'db>| { + Signature::new(signature.parameters().clone(), Some(correct_return_type)) + .with_definition(signature.definition()) + .bind_self() + }; + + let synthesized_dunder_init_signature = CallableSignature::from_overloads( + signature.overloads.iter().map(synthesized_signature), + ); + + Some(Type::Callable(CallableType::new( + db, + synthesized_dunder_init_signature, + true, + ))) + } else { + None + } + } else { + None + }; + + match (dunder_new_function, synthesized_dunder_init_callable) { + (Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => { + UnionType::from_elements( + db, + vec![dunder_new_function, synthesized_dunder_init_callable], + ) + } + (Some(constructor), None) | (None, Some(constructor)) => constructor, + (None, None) => { + // If no `__new__` or `__init__` method is found, then we fall back to looking for + // an `object.__new__` method. + let new_function_symbol = self_ty + .member_lookup_with_policy( + db, + "__new__".into(), + MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .place; + + if let Place::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { + new_function.into_bound_method_type(db, self_ty) + } else { + // Fallback if no `object.__new__` is found. + CallableType::single( + db, + Signature::new(Parameters::empty(), Some(correct_return_type)), + ) + } + } + } + } +} + +impl<'db> From> for ClassType<'db> { + fn from(generic: GenericAlias<'db>) -> ClassType<'db> { + ClassType::Generic(generic) + } +} + +impl<'db> From> for Type<'db> { + fn from(class: ClassType<'db>) -> Type<'db> { + match class { + ClassType::NonGeneric(non_generic) => non_generic.into(), + ClassType::Generic(generic) => generic.into(), + } + } +} + +/// A filter that describes which methods are considered when looking for implicit attribute assignments +/// in [`ClassLiteral::implicit_attribute`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) enum MethodDecorator { + None, + ClassMethod, + StaticMethod, +} + +impl MethodDecorator { + fn try_from_fn_type(db: &dyn Db, fn_type: FunctionType) -> Result { + match ( + fn_type.has_known_decorator(db, FunctionDecorators::CLASSMETHOD), + fn_type.has_known_decorator(db, FunctionDecorators::STATICMETHOD), + ) { + (true, true) => Err(()), // A method can't be static and class method at the same time. + (true, false) => Ok(Self::ClassMethod), + (false, true) => Ok(Self::StaticMethod), + (false, false) => Ok(Self::None), + } + } +} + +/// Representation of a class definition statement in the AST: either a non-generic class, or a +/// generic class that has not been specialized. +/// +/// This does not in itself represent a type, but can be transformed into a [`ClassType`] that +/// does. (For generic classes, this requires specializing its generic context.) +/// +/// # Ordering +/// Ordering is based on the class's id assigned by salsa and not on the class literal's values. +/// The id may change between runs, or when the class literal was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct ClassLiteral<'db> { + /// Name of the class at definition + #[returns(ref)] + pub(crate) name: ast::name::Name, + + pub(crate) body_scope: ScopeId<'db>, + + pub(crate) known: Option, + + pub(crate) dataclass_params: Option, + pub(crate) dataclass_transformer_params: Option, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for ClassLiteral<'_> {} + +#[expect(clippy::trivially_copy_pass_by_ref, clippy::ref_option)] +fn pep695_generic_context_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Option>, + _count: u32, + _self: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction>> { + salsa::CycleRecoveryAction::Iterate +} + +fn pep695_generic_context_cycle_initial<'db>( + _db: &'db dyn Db, + _self: ClassLiteral<'db>, +) -> Option> { + None +} + +#[salsa::tracked] +impl<'db> ClassLiteral<'db> { + /// Return `true` if this class represents `known_class` + pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { + self.known(db) == Some(known_class) + } + + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code + // the knowledge that this class is not generic. + if self.is_known(db, KnownClass::VersionInfo) { + return None; + } + + // We've already verified that the class literal does not contain both a PEP-695 generic + // scope and a `typing.Generic` base class. + // + // Note that if a class has an explicit legacy generic context (by inheriting from + // `typing.Generic`), and also an implicit one (by inheriting from other generic classes, + // specialized by typevars), the explicit one takes precedence. + self.pep695_generic_context(db) + .or_else(|| self.legacy_generic_context(db)) + .or_else(|| self.inherited_legacy_generic_context(db)) + } + + pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + self.pep695_generic_context(db).is_some() + } + + #[salsa::tracked(cycle_fn=pep695_generic_context_cycle_recover, cycle_initial=pep695_generic_context_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { + let scope = self.body_scope(db); + let parsed = parsed_module(db, scope.file(db)).load(db); + let class_def_node = scope.node(db).expect_class(&parsed); + class_def_node.type_params.as_ref().map(|type_params| { + let index = semantic_index(db, scope.file(db)); + GenericContext::from_type_params(db, index, type_params) + }) + } + + pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { + self.explicit_bases(db).iter().find_map(|base| match base { + Type::KnownInstance( + KnownInstanceType::SubscriptedGeneric(generic_context) + | KnownInstanceType::SubscriptedProtocol(generic_context), + ) => Some(*generic_context), + _ => None, + }) + } + + pub(crate) fn inherited_legacy_generic_context( + self, + db: &'db dyn Db, + ) -> Option> { + GenericContext::from_base_classes( + db, + self.explicit_bases(db) + .iter() + .copied() + .filter(|ty| matches!(ty, Type::GenericAlias(_))), + ) + } + + fn file(self, db: &dyn Db) -> File { + self.body_scope(db).file(db) + } + + /// Return the original [`ast::StmtClassDef`] node associated with this class + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { + let scope = self.body_scope(db); + scope.node(db).expect_class(module) + } + + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let body_scope = self.body_scope(db); + let module = parsed_module(db, body_scope.file(db)).load(db); + let index = semantic_index(db, body_scope.file(db)); + index.expect_single_definition(body_scope.node(db).expect_class(&module)) + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> ClassType<'db> { + match self.generic_context(db) { + None => ClassType::NonGeneric(self), + Some(generic_context) => { + let specialization = f(generic_context); + ClassType::Generic(GenericAlias::new(db, self, specialization)) + } + } + } + + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + specialization.unwrap_or_else(|| generic_context.default_specialization(db)) + }) + } + + /// Returns the default specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// applies the default specialization to the class's typevars. + pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.default_specialization(db) + }) + } + + /// Returns the unknown specialization of this class. For non-generic classes, the class is + /// returned unchanged. For a non-specialized generic class, we return a generic alias that + /// maps each of the class's typevars to `Unknown`. + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context.unknown_specialization(db) + }) + } + + /// Return an iterator over the inferred types of this class's *explicit* bases. + /// + /// Note that any class (except for `object`) that has no explicit + /// bases will implicitly inherit from `object` at runtime. Nonetheless, + /// this method does *not* include `object` in the bases it iterates over. + /// + /// ## Why is this a salsa query? + /// + /// This is a salsa query to short-circuit the invalidation + /// when the class's AST node changes. + /// + /// Were this not a salsa query, then the calling query + /// would depend on the class's AST and rerun for every change in that file. + #[salsa::tracked(returns(deref), cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + tracing::trace!("ClassLiteral::explicit_bases_query: {}", self.name(db)); + + let module = parsed_module(db, self.file(db)).load(db); + let class_stmt = self.node(db, &module); + let class_definition = + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + + class_stmt + .bases() + .iter() + .map(|base_node| definition_expression_type(db, class_definition, base_node)) + .collect() + } + + /// Return `Some()` if this class is known to be a [`SolidBase`], or `None` if it is not. + pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option> { + if let Some(known_class) = self.known(db) { + known_class + .is_solid_base() + .then_some(SolidBase::hard_coded(self)) + } else if SlotsKind::from(db, self) == SlotsKind::NotEmpty { + Some(SolidBase::due_to_dunder_slots(self)) + } else { + None + } + } + + /// Iterate over this class's explicit bases, filtering out any bases that are not class + /// objects, and applying default specialization to any unspecialized generic class literals. + fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + self.explicit_bases(db) + .iter() + .copied() + .filter_map(|ty| ty.to_class_type(db)) + } + + /// Determine if this class is a protocol. + /// + /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method, + /// which hardcodes knowledge about certain special-cased classes. See the docs on + /// that method for why we do this rather than relying on generalised logic for all + /// classes, including the special-cased ones that are included in the [`KnownClass`] + /// enum. + pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { + self.known(db) + .map(KnownClass::is_protocol) + .unwrap_or_else(|| { + // Iterate through the last three bases of the class + // searching for `Protocol` or `Protocol[]` in the bases list. + // + // If `Protocol` is present in the bases list of a valid protocol class, it must either: + // + // - be the last base + // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) + // - OR be the last-but-two base (with the penultimate base being `Generic[]` + // and the final base being `object`) + self.explicit_bases(db).iter().rev().take(3).any(|base| { + matches!( + base, + Type::SpecialForm(SpecialFormType::Protocol) + | Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)) + ) + }) + }) + } + + /// Determine if this is an abstract class. + pub(super) fn is_abstract(self, db: &'db dyn Db) -> bool { + self.metaclass(db) + .into_class_literal() + .is_some_and(|metaclass| metaclass.is_known(db, KnownClass::ABCMeta)) + } + + /// Return the types of the decorators on this class + #[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)] + fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + tracing::trace!("ClassLiteral::decorators: {}", self.name(db)); + + let module = parsed_module(db, self.file(db)).load(db); + + let class_stmt = self.node(db, &module); + if class_stmt.decorator_list.is_empty() { + return Box::new([]); + } + + let class_definition = + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + + class_stmt + .decorator_list + .iter() + .map(|decorator_node| { + definition_expression_type(db, class_definition, &decorator_node.expression) + }) + .collect() + } + + pub(super) fn known_function_decorators( + self, + db: &'db dyn Db, + ) -> impl Iterator + 'db { + self.decorators(db) + .iter() + .filter_map(|deco| deco.into_function_literal()) + .filter_map(|decorator| decorator.known(db)) + } + + /// Is this class final? + pub(super) fn is_final(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::Final) + } + + /// Attempt to resolve the [method resolution order] ("MRO") for this class. + /// If the MRO is unresolvable, return an error indicating why the class's MRO + /// cannot be accurately determined. The error returned contains a fallback MRO + /// that will be used instead for the purposes of type inference. + /// + /// The MRO is the tuple of classes that can be retrieved as the `__mro__` + /// attribute on a class at runtime. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + #[salsa::tracked(returns(as_ref), cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + pub(super) fn try_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Result, MroError<'db>> { + tracing::trace!("ClassLiteral::try_mro: {}", self.name(db)); + Mro::of_class(db, self, specialization) + } + + /// Iterate over the [method resolution order] ("MRO") of the class. + /// + /// If the MRO could not be accurately resolved, this method falls back to iterating + /// over an MRO that has the class directly inheriting from `Unknown`. Use + /// [`ClassLiteral::try_mro`] if you need to distinguish between the success and failure + /// cases rather than simply iterating over the inferred resolution order for the class. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(super) fn iter_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> MroIterator<'db> { + MroIterator::new(db, self, specialization) + } + + /// Return `true` if `other` is present in this class's MRO. + pub(super) fn is_subclass_of( + self, + db: &'db dyn Db, + specialization: Option>, + other: ClassType<'db>, + ) -> bool { + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. + self.iter_mro(db, specialization) + .contains(&ClassBase::Class(other)) + } + + /// Return the explicit `metaclass` of this class, if one is defined. + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn explicit_metaclass(self, db: &'db dyn Db, module: &ParsedModuleRef) -> Option> { + let class_stmt = self.node(db, module); + let metaclass_node = &class_stmt + .arguments + .as_ref()? + .find_keyword("metaclass")? + .value; + + let class_definition = self.definition(db); + + Some(definition_expression_type( + db, + class_definition, + metaclass_node, + )) + } + + /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. + pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + self.try_metaclass(db) + .map(|(ty, _)| ty) + .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) + } + + /// Return a type representing "the set of all instances of the metaclass of this class". + pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + self + .metaclass(db) + .to_instance(db) + .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + } + + /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. + #[salsa::tracked( + cycle_fn=try_metaclass_cycle_recover, + cycle_initial=try_metaclass_cycle_initial, + heap_size=get_size2::GetSize::get_heap_size, + )] + pub(super) fn try_metaclass( + self, + db: &'db dyn Db, + ) -> Result<(Type<'db>, Option), MetaclassError<'db>> { + tracing::trace!("ClassLiteral::try_metaclass: {}", self.name(db)); + + // Identify the class's own metaclass (or take the first base class's metaclass). + let mut base_classes = self.fully_static_explicit_bases(db).peekable(); + + if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() { + // We emit diagnostics for cyclic class definitions elsewhere. + // Avoid attempting to infer the metaclass if the class is cyclically defined. + return Ok((SubclassOfType::subclass_of_unknown(), None)); + } + + if self.try_mro(db, None).is_err_and(MroError::is_cycle) { + return Ok((SubclassOfType::subclass_of_unknown(), None)); + } + + let module = parsed_module(db, self.file(db)).load(db); + + let explicit_metaclass = self.explicit_metaclass(db, &module); + let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass { + (metaclass, self) + } else if let Some(base_class) = base_classes.next() { + let (base_class_literal, _) = base_class.class_literal(db); + (base_class.metaclass(db), base_class_literal) + } else { + (KnownClass::Type.to_class_literal(db), self) + }; + + let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { + MetaclassCandidate { + metaclass: metaclass_ty, + explicit_metaclass_of: class_metaclass_was_from, + } + } else { + let name = Type::string_literal(db, self.name(db)); + let bases = TupleType::from_elements(db, self.explicit_bases(db).iter().copied()); + let namespace = KnownClass::Dict + .to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]); + + // TODO: Other keyword arguments? + let arguments = CallArgumentTypes::positional([name, bases, namespace]); + + 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()), + }), + + // TODO we should also check for binding errors that would indicate the metaclass + // does not accept the right arguments + Err(CallError(CallErrorKind::BindingError, bindings)) => { + Ok(bindings.return_type(db)) + } + + Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { + kind: MetaclassErrorKind::PartlyNotCallable(metaclass), + }), + }; + + return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); + }; + + // Reconcile all base classes' metaclasses with the candidate metaclass. + // + // See: + // - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass + // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 + for base_class in base_classes { + let metaclass = base_class.metaclass(db); + let Some(metaclass) = metaclass.to_class_type(db) else { + continue; + }; + if metaclass.is_subclass_of(db, candidate.metaclass) { + let (base_class_literal, _) = base_class.class_literal(db); + candidate = MetaclassCandidate { + metaclass, + explicit_metaclass_of: base_class_literal, + }; + continue; + } + if candidate.metaclass.is_subclass_of(db, metaclass) { + continue; + } + let (base_class_literal, _) = base_class.class_literal(db); + return Err(MetaclassError { + kind: MetaclassErrorKind::Conflict { + candidate1: candidate, + candidate2: MetaclassCandidate { + metaclass, + explicit_metaclass_of: base_class_literal, + }, + candidate1_is_base_class: explicit_metaclass.is_none(), + }, + }); + } + + let (metaclass_literal, _) = candidate.metaclass.class_literal(db); + Ok(( + candidate.metaclass.into(), + metaclass_literal.dataclass_transformer_params(db), + )) + } + + /// Returns the class member of this class named `name`. + /// + /// The member resolves to a member on the class itself or any of its proper superclasses. + /// + /// TODO: Should this be made private...? + pub(super) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + self.class_member_inner(db, None, name, policy) + } + + fn class_member_inner( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + if name == "__mro__" { + let tuple_elements = self.iter_mro(db, specialization).map(Type::from); + return Place::bound(TupleType::from_elements(db, tuple_elements)).into(); + } + + self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) + } + + pub(super) fn class_member_from_mro( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + mro_iter: impl Iterator>, + ) -> PlaceAndQualifiers<'db> { + // If we encounter a dynamic type in this class's MRO, we'll save that dynamic type + // in this variable. After we've traversed the MRO, we'll either: + // (1) Use that dynamic type as the type for this attribute, + // if no other classes in the MRO define the attribute; or, + // (2) Intersect that dynamic type with the type of the attribute + // from the non-dynamic members of the class's MRO. + let mut dynamic_type_to_intersect_with: Option> = None; + + let mut lookup_result: LookupResult<'db> = + Err(LookupError::Unbound(TypeQualifiers::empty())); + + for superclass in mro_iter { + match superclass { + ClassBase::Generic | ClassBase::Protocol => { + // Skip over these very special class bases that aren't really classes. + } + ClassBase::Dynamic(_) => { + // Note: calling `Type::from(superclass).member()` would be incorrect here. + // What we'd really want is a `Type::Any.own_class_member()` method, + // but adding such a method wouldn't make much sense -- it would always return `Any`! + dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass)); + } + ClassBase::Class(class) => { + if class.is_known(db, KnownClass::Object) + // Only exclude `object` members if this is not an `object` class itself + && (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object)) + { + continue; + } + + if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback() + { + continue; + } + + lookup_result = lookup_result.or_else(|lookup_error| { + lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) + }); + } + } + if lookup_result.is_ok() { + break; + } + } + + match ( + PlaceAndQualifiers::from(lookup_result), + dynamic_type_to_intersect_with, + ) { + (symbol_and_qualifiers, None) => symbol_and_qualifiers, + + ( + PlaceAndQualifiers { + place: Place::Type(ty, _), + qualifiers, + }, + Some(dynamic_type), + ) => Place::bound( + IntersectionBuilder::new(db) + .add_positive(ty) + .add_positive(dynamic_type) + .build(), + ) + .with_qualifiers(qualifiers), + + ( + PlaceAndQualifiers { + place: Place::Unbound, + qualifiers, + }, + Some(dynamic_type), + ) => Place::bound(dynamic_type).with_qualifiers(qualifiers), + } + } + + /// Returns the inferred type of the class member named `name`. Only bound members + /// or those marked as ClassVars are considered. + /// + /// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope + /// directly. Use [`ClassLiteral::class_member`] if you require a method that will + /// traverse through the MRO until it finds the member. + pub(super) fn own_class_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() { + // Make this class look like a subclass of the `DataClassInstance` protocol + return Place::bound(KnownClass::Dict.to_specialized_instance( + db, + [ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, [Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR); + } + + let body_scope = self.body_scope(db); + let symbol = class_symbol(db, body_scope, name).map_type(|ty| { + // The `__new__` and `__init__` members of a non-specialized generic class are handled + // specially: they inherit the generic context of their class. That lets us treat them + // as generic functions when constructing the class, and infer the specialization of + // the class from the arguments that are passed in. + // + // We might decide to handle other class methods the same way, having them inherit the + // class's generic context, and performing type inference on calls to them to determine + // the specialization of the class. If we do that, we would update this to also apply + // to any method with a `@classmethod` decorator. (`__init__` would remain a special + // case, since it's an _instance_ method where we don't yet know the generic class's + // specialization.) + match (self.generic_context(db), ty, specialization, name) { + ( + Some(generic_context), + Type::FunctionLiteral(function), + Some(_), + "__new__" | "__init__", + ) => Type::FunctionLiteral( + function.with_inherited_generic_context(db, generic_context), + ), + _ => ty, + } + }); + + if symbol.place.is_unbound() { + if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name) + { + return Place::bound(synthesized_member).into(); + } + // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. + return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod) + .into(); + } + symbol + } + + /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or + /// a synthesized `__new__` method for a `NamedTuple`. + fn own_synthesized_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> Option> { + let dataclass_params = self.dataclass_params(db); + let has_dataclass_param = + |param| dataclass_params.is_some_and(|params| params.contains(param)); + + let field_policy = CodeGeneratorKind::from_class(db, self)?; + + let signature_from_fields = |mut parameters: Vec<_>| { + let mut kw_only_field_seen = false; + for (name, (mut attr_ty, mut default_ty)) in + self.fields(db, specialization, field_policy) + { + if attr_ty + .into_nominal_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::KwOnly)) + { + // Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized + // `__init__` method; they are used to indicate that the following parameters are + // keyword-only. + kw_only_field_seen = true; + continue; + } + + let dunder_set = attr_ty.class_member(db, "__set__".into()); + if let Place::Type(dunder_set, Boundness::Bound) = dunder_set.place { + // The descriptor handling below is guarded by this not-dynamic check, because + // dynamic types like `Any` are valid (data) descriptors: since they have all + // possible attributes, they also have a (callable) `__set__` method. The + // problem is that we can't determine the type of the value parameter this way. + // Instead, we want to use the dynamic type itself in this case, so we skip the + // special descriptor handling. + if !dunder_set.is_dynamic() { + // This type of this attribute is a data descriptor. Instead of overwriting the + // descriptor attribute, data-classes will (implicitly) call the `__set__` method + // of the descriptor. This means that the synthesized `__init__` parameter for + // this attribute is determined by possible `value` parameter types with which + // the `__set__` method can be called. We build a union of all possible options + // to account for possible overloads. + let mut value_types = UnionBuilder::new(db); + for binding in &dunder_set.bindings(db) { + for overload in binding { + if let Some(value_param) = + overload.signature.parameters().get_positional(2) + { + value_types = value_types.add( + value_param.annotated_type().unwrap_or_else(Type::unknown), + ); + } else if overload.signature.parameters().is_gradual() { + value_types = value_types.add(Type::unknown()); + } + } + } + attr_ty = value_types.build(); + + // The default value of the attribute is *not* determined by the right hand side + // of the class-body assignment. Instead, the runtime invokes `__get__` on the + // descriptor, as if it had been called on the class itself, i.e. it passes `None` + // for the `instance` argument. + + if let Some(ref mut default_ty) = default_ty { + *default_ty = default_ty + .try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self)) + .map(|(return_ty, _)| return_ty) + .unwrap_or_else(Type::unknown); + } + } + } + + let mut parameter = if kw_only_field_seen { + Parameter::keyword_only(name) + } else { + Parameter::positional_or_keyword(name) + } + .with_annotated_type(attr_ty); + + if let Some(default_ty) = default_ty { + parameter = parameter.with_default_type(default_ty); + } + + parameters.push(parameter); + } + + let mut signature = Signature::new(Parameters::new(parameters), Some(Type::none(db))); + signature.inherited_generic_context = self.generic_context(db); + Some(CallableType::function_like(db, signature)) + }; + + match (field_policy, name) { + (CodeGeneratorKind::DataclassLike, "__init__") => { + let has_synthesized_dunder_init = has_dataclass_param(DataclassParams::INIT) + || self + .try_metaclass(db) + .is_ok_and(|(_, transformer_params)| transformer_params.is_some()); + + if !has_synthesized_dunder_init { + return None; + } + + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(Type::instance( + db, + self.apply_optional_specialization(db, specialization), + )); + signature_from_fields(vec![self_parameter]) + } + (CodeGeneratorKind::NamedTuple, "__new__") => { + let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) + .with_annotated_type(KnownClass::Type.to_instance(db)); + signature_from_fields(vec![cls_parameter]) + } + (CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => { + if !has_dataclass_param(DataclassParams::ORDER) { + return None; + } + + let signature = Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(Type::instance( + db, + self.apply_optional_specialization(db, specialization), + )), + Parameter::positional_or_keyword(Name::new_static("other")) + // TODO: could be `Self`. + .with_annotated_type(Type::instance( + db, + self.apply_optional_specialization(db, specialization), + )), + ]), + Some(KnownClass::Bool.to_instance(db)), + ); + + Some(CallableType::function_like(db, signature)) + } + (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { + KnownClass::NamedTupleFallback + .to_class_literal(db) + .into_class_literal()? + .own_class_member(db, None, name) + .place + .ignore_possibly_unbound() + } + _ => None, + } + } + + /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. + /// + /// See [`ClassLiteral::own_fields`] for more details. + pub(crate) fn fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind, + ) -> FxOrderMap, Option>)> { + if field_policy == CodeGeneratorKind::NamedTuple { + // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the + // fields of this class only. + return self.own_fields(db); + } + + let matching_classes_in_mro: Vec<_> = self + .iter_mro(db, specialization) + .filter_map(|superclass| { + if let Some(class) = superclass.into_class() { + let class_literal = class.class_literal(db).0; + if field_policy.matches(db, class_literal) { + Some(class_literal) + } else { + None + } + } else { + None + } + }) + // We need to collect into a `Vec` here because we iterate the MRO in reverse order + .collect(); + + matching_classes_in_mro + .into_iter() + .rev() + .flat_map(|class| class.own_fields(db)) + // We collect into a FxOrderMap here to deduplicate attributes + .collect() + } + + /// Returns a list of all annotated attributes defined in the body of this class. This is similar + /// to the `__annotations__` attribute at runtime, but also contains default values. + /// + /// For a class body like + /// ```py + /// @dataclass + /// class C: + /// x: int + /// y: str = "a" + /// ``` + /// we return a map `{"x": (int, None), "y": (str, Some(Literal["a"]))}`. + fn own_fields(self, db: &'db dyn Db) -> FxOrderMap, Option>)> { + let mut attributes = FxOrderMap::default(); + + let class_body_scope = self.body_scope(db); + let table = place_table(db, class_body_scope); + + let use_def = use_def_map(db, class_body_scope); + for (place_id, declarations) in use_def.all_end_of_scope_declarations() { + // Here, we exclude all declarations that are not annotated assignments. We need this because + // things like function definitions and nested classes would otherwise be considered dataclass + // fields. The check is too broad in the sense that it also excludes (weird) constructs where + // a symbol would have multiple declarations, one of which is an annotated assignment. If we + // want to improve this, we could instead pass a definition-kind filter to the use-def map + // query, or to the `symbol_from_declarations` call below. Doing so would potentially require + // us to generate a union of `__init__` methods. + if !declarations + .clone() + .all(|DeclarationWithConstraint { declaration, .. }| { + declaration.is_undefined_or(|declaration| { + matches!( + declaration.kind(db), + DefinitionKind::AnnotatedAssignment(..) + ) + }) + }) + { + continue; + } + + let place_expr = table.place_expr(place_id); + + if let Ok(attr) = place_from_declarations(db, declarations) { + if attr.is_class_var() { + continue; + } + + if let Some(attr_ty) = attr.place.ignore_possibly_unbound() { + let bindings = use_def.end_of_scope_bindings(place_id); + let default_ty = place_from_bindings(db, bindings).ignore_possibly_unbound(); + + attributes.insert(place_expr.expect_name().clone(), (attr_ty, default_ty)); + } + } + } + + attributes + } + + /// Look up an instance attribute (available in `__dict__`) of the given name. + /// + /// See [`Type::instance_member`] for more details. + pub(super) fn instance_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + let mut union = UnionBuilder::new(db); + let mut union_qualifiers = TypeQualifiers::empty(); + + for superclass in self.iter_mro(db, specialization) { + match superclass { + ClassBase::Generic | ClassBase::Protocol => { + // Skip over these very special class bases that aren't really classes. + } + ClassBase::Dynamic(_) => { + return PlaceAndQualifiers::todo( + "instance attribute on class with dynamic base", + ); + } + ClassBase::Class(class) => { + if let member @ PlaceAndQualifiers { + place: Place::Type(ty, boundness), + qualifiers, + } = class.own_instance_member(db, name) + { + // TODO: We could raise a diagnostic here if there are conflicting type qualifiers + union_qualifiers |= qualifiers; + + if boundness == Boundness::Bound { + if union.is_empty() { + // Short-circuit, no need to allocate inside the union builder + return member; + } + + return Place::bound(union.add(ty).build()) + .with_qualifiers(union_qualifiers); + } + + // If we see a possibly-unbound symbol, we need to keep looking + // higher up in the MRO. + union = union.add(ty); + } + } + } + } + + if union.is_empty() { + Place::Unbound.with_qualifiers(TypeQualifiers::empty()) + } else { + // If we have reached this point, we know that we have only seen possibly-unbound places. + // This means that the final result is still possibly-unbound. + + Place::Type(union.build(), Boundness::PossiblyUnbound).with_qualifiers(union_qualifiers) + } + } + + /// Tries to find declarations/bindings of an attribute named `name` that are only + /// "implicitly" defined (`self.x = …`, `cls.x = …`) in a method of the class that + /// corresponds to `class_body_scope`. The `target_method_decorator` parameter is + /// used to skip methods that do not have the expected decorator. + fn implicit_attribute( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: &str, + target_method_decorator: MethodDecorator, + ) -> Place<'db> { + // If we do not see any declarations of an attribute, neither in the class body nor in + // any method, we build a union of `Unknown` with the inferred types of all bindings of + // that attribute. We include `Unknown` in that union to account for the fact that the + // attribute might be externally modified. + let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown()); + + let mut is_attribute_bound = false; + + let file = class_body_scope.file(db); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let class_map = use_def_map(db, class_body_scope); + let class_table = place_table(db, class_body_scope); + + let is_valid_scope = |method_scope: ScopeId<'db>| { + if let Some(method_def) = method_scope.node(db).as_function(&module) { + let method_name = method_def.name.as_str(); + if let Place::Type(Type::FunctionLiteral(method_type), _) = + class_symbol(db, class_body_scope, method_name).place + { + let method_decorator = MethodDecorator::try_from_fn_type(db, method_type); + if method_decorator != Ok(target_method_decorator) { + return false; + } + } + } + true + }; + + // First check declarations + for (attribute_declarations, method_scope_id) in + attribute_declarations(db, class_body_scope, name) + { + let method_scope = method_scope_id.to_scope_id(db, file); + if !is_valid_scope(method_scope) { + continue; + } + + for attribute_declaration in attribute_declarations { + let DefinitionState::Defined(decl) = attribute_declaration.declaration else { + continue; + }; + + let DefinitionKind::AnnotatedAssignment(annotated) = decl.kind(db) else { + continue; + }; + + if use_def_map(db, method_scope) + .is_declaration_reachable(db, &attribute_declaration) + .is_always_false() + { + continue; + } + + let annotation_ty = + infer_expression_type(db, index.expression(annotated.annotation(&module))); + + return Place::bound(annotation_ty); + } + } + + for (attribute_assignments, method_scope_id) in + attribute_assignments(db, class_body_scope, name) + { + let method_scope = method_scope_id.to_scope_id(db, file); + if !is_valid_scope(method_scope) { + continue; + } + + let method_map = use_def_map(db, method_scope); + + // The attribute assignment inherits the reachability of the method which contains it + let is_method_reachable = + if let Some(method_def) = method_scope.node(db).as_function(&module) { + let method = index.expect_single_definition(method_def); + let method_place = class_table.place_id_by_name(&method_def.name).unwrap(); + class_map + .all_reachable_bindings(method_place) + .find_map(|bind| { + (bind.binding.is_defined_and(|def| def == method)) + .then(|| class_map.is_binding_reachable(db, &bind)) + }) + .unwrap_or(Truthiness::AlwaysFalse) + } else { + Truthiness::AlwaysFalse + }; + if is_method_reachable.is_always_false() { + continue; + } + + // Storage for the implicit `DefinitionState::Undefined` binding. If present, it + // will be the first binding in the `attribute_assignments` iterator. + let mut unbound_binding = None; + + for attribute_assignment in attribute_assignments { + if let DefinitionState::Undefined = attribute_assignment.binding { + // Store the implicit unbound binding here so that we can delay the + // computation of `unbound_reachability` to the point when we actually + // need it. This is an optimization for the common case where the + // `unbound` binding is the only binding of the `name` attribute, + // i.e. if there is no `self.name = …` assignment in this method. + unbound_binding = Some(attribute_assignment); + continue; + } + + let DefinitionState::Defined(binding) = attribute_assignment.binding else { + continue; + }; + match method_map + .is_binding_reachable(db, &attribute_assignment) + .and(is_method_reachable) + { + Truthiness::AlwaysTrue | Truthiness::Ambiguous => { + is_attribute_bound = true; + } + Truthiness::AlwaysFalse => { + continue; + } + } + + // There is at least one attribute assignment that may be reachable, so if `unbound_reachability` is + // always false then this attribute is considered bound. + // TODO: this is incomplete logic since the attributes bound after termination are considered reachable. + let unbound_reachability = unbound_binding + .as_ref() + .map(|binding| method_map.is_binding_reachable(db, binding)) + .unwrap_or(Truthiness::AlwaysFalse); + + if unbound_reachability + .negate() + .and(is_method_reachable) + .is_always_true() + { + is_attribute_bound = true; + } + + match binding.kind(db) { + DefinitionKind::AnnotatedAssignment(ann_assign) => { + // We found an annotated assignment of one of the following forms (using 'self' in these + // examples, but we support arbitrary names for the first parameters of methods): + // + // self.name: + // self.name: = … + + let annotation_ty = infer_expression_type( + db, + index.expression(ann_assign.annotation(&module)), + ); + + // TODO: check if there are conflicting declarations + if is_attribute_bound { + return Place::bound(annotation_ty); + } + unreachable!( + "If the attribute assignments are all invisible, inference of their types should be skipped" + ); + } + DefinitionKind::Assignment(assign) => { + match assign.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // .., self.name, .. = + // (.., self.name, ..) = + // [.., self.name, ..] = + + let unpacked = infer_unpack_types(db, unpack); + + let inferred_ty = unpacked.expression_type(assign.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an un-annotated attribute assignment of the form: + // + // self.name = + + let inferred_ty = infer_expression_type( + db, + index.expression(assign.value(&module)), + ); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::For(for_stmt) => { + match for_stmt.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // for .., self.name, .. in : + + let unpacked = infer_unpack_types(db, unpack); + let inferred_ty = + unpacked.expression_type(for_stmt.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // for self.name in : + + let iterable_ty = infer_expression_type( + db, + index.expression(for_stmt.iterable(&module)), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = iterable_ty.iterate(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::WithItem(with_item) => { + match with_item.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // with as .., self.name, ..: + + let unpacked = infer_unpack_types(db, unpack); + let inferred_ty = + unpacked.expression_type(with_item.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // with as self.name: + + let context_ty = infer_expression_type( + db, + index.expression(with_item.context_expr(&module)), + ); + let inferred_ty = context_ty.enter(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::Comprehension(comprehension) => { + match comprehension.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // [... for .., self.name, .. in ] + + let unpacked = infer_unpack_types(db, unpack); + + let inferred_ty = + unpacked.expression_type(comprehension.target(&module)); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::Single => { + // We found an attribute assignment like: + // + // [... for self.name in ] + + let iterable_ty = infer_expression_type( + db, + index.expression(comprehension.iterable(&module)), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = iterable_ty.iterate(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } + } + DefinitionKind::AugmentedAssignment(_) => { + // TODO: + } + DefinitionKind::NamedExpression(_) => { + // A named expression whose target is an attribute is syntactically prohibited + } + _ => {} + } + } + } + + if is_attribute_bound { + Place::bound(union_of_inferred_types.build()) + } else { + Place::Unbound + } + } + + /// A helper function for `instance_member` that looks up the `name` attribute only on + /// this class, not on its superclasses. + pub(crate) fn own_instance_member( + self, + db: &'db dyn Db, + name: &str, + ) -> PlaceAndQualifiers<'db> { + // TODO: There are many things that are not yet implemented here: + // - `typing.Final` + // - Proper diagnostics + + let body_scope = self.body_scope(db); + let table = place_table(db, body_scope); + + if let Some(place_id) = table.place_id_by_name(name) { + let use_def = use_def_map(db, body_scope); + + let declarations = use_def.end_of_scope_declarations(place_id); + let declared_and_qualifiers = place_from_declarations(db, declarations); + + match declared_and_qualifiers { + Ok(PlaceAndQualifiers { + place: mut declared @ Place::Type(declared_ty, declaredness), + qualifiers, + }) => { + // For the purpose of finding instance attributes, ignore `ClassVar` + // declarations: + if qualifiers.contains(TypeQualifiers::CLASS_VAR) { + declared = Place::Unbound; + } + + // The attribute is declared in the class body. + + let bindings = use_def.end_of_scope_bindings(place_id); + let inferred = place_from_bindings(db, bindings); + let has_binding = !inferred.is_unbound(); + + if has_binding { + // The attribute is declared and bound in the class body. + + if let Some(implicit_ty) = + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None) + .ignore_possibly_unbound() + { + if declaredness == Boundness::Bound { + // If a symbol is definitely declared, and we see + // attribute assignments in methods of the class, + // we trust the declared type. + declared.with_qualifiers(qualifiers) + } else { + Place::Type( + UnionType::from_elements(db, [declared_ty, implicit_ty]), + declaredness, + ) + .with_qualifiers(qualifiers) + } + } else { + // The symbol is declared and bound in the class body, + // but we did not find any attribute assignments in + // methods of the class. This means that the attribute + // has a class-level default value, but it would not be + // found in a `__dict__` lookup. + + Place::Unbound.into() + } + } else { + // The attribute is declared but not bound in the class body. + // We take this as a sign that this is intended to be a pure + // instance attribute, and we trust the declared type, unless + // it is possibly-undeclared. In the latter case, we also + // union with the inferred type from attribute assignments. + + if declaredness == Boundness::Bound { + declared.with_qualifiers(qualifiers) + } else { + if let Some(implicit_ty) = Self::implicit_attribute( + db, + body_scope, + name, + MethodDecorator::None, + ) + .ignore_possibly_unbound() + { + Place::Type( + UnionType::from_elements(db, [declared_ty, implicit_ty]), + declaredness, + ) + .with_qualifiers(qualifiers) + } else { + declared.with_qualifiers(qualifiers) + } + } + } + } + + Ok(PlaceAndQualifiers { + place: Place::Unbound, + qualifiers: _, + }) => { + // The attribute is not *declared* in the class body. It could still be declared/bound + // in a method. + + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None).into() + } + Err((declared, _conflicting_declarations)) => { + // There are conflicting declarations for this attribute in the class body. + Place::bound(declared.inner_type()).with_qualifiers(declared.qualifiers()) + } + } + } else { + // This attribute is neither declared nor bound in the class body. + // It could still be implicitly defined in a method. + + Self::implicit_attribute(db, body_scope, name, MethodDecorator::None).into() + } + } + + /// Return this class' involvement in an inheritance cycle, if any. + /// + /// A class definition like this will fail at runtime, + /// but we must be resilient to it or we could panic. + #[salsa::tracked(cycle_fn=inheritance_cycle_recover, cycle_initial=inheritance_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + pub(super) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { + /// Return `true` if the class is cyclically defined. + /// + /// Also, populates `visited_classes` with all base classes of `self`. + fn is_cyclically_defined_recursive<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + classes_on_stack: &mut IndexSet>, + visited_classes: &mut IndexSet>, + ) -> bool { + let mut result = false; + for explicit_base in class.explicit_bases(db) { + let explicit_base_class_literal = match explicit_base { + Type::ClassLiteral(class_literal) => *class_literal, + Type::GenericAlias(generic_alias) => generic_alias.origin(db), + _ => continue, + }; + if !classes_on_stack.insert(explicit_base_class_literal) { + return true; + } + + if visited_classes.insert(explicit_base_class_literal) { + // If we find a cycle, keep searching to check if we can reach the starting class. + result |= is_cyclically_defined_recursive( + db, + explicit_base_class_literal, + classes_on_stack, + visited_classes, + ); + } + classes_on_stack.pop(); + } + result + } + + tracing::trace!("Class::inheritance_cycle: {}", self.name(db)); + + let visited_classes = &mut IndexSet::new(); + if !is_cyclically_defined_recursive(db, self, &mut IndexSet::new(), visited_classes) { + None + } else if visited_classes.contains(&self) { + Some(InheritanceCycle::Participant) + } else { + Some(InheritanceCycle::Inherited) + } + } + + /// Returns a [`Span`] with the range of the class's header. + /// + /// See [`Self::header_range`] for more details. + pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + Span::from(self.file(db)).with_range(self.header_range(db)) + } + + /// Returns the range of the class's "header": the class name + /// and any arguments passed to the `class` statement. E.g. + /// + /// ```ignore + /// class Foo(Bar, metaclass=Baz): ... + /// ^^^^^^^^^^^^^^^^^^^^^^^ + /// ``` + pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { + let class_scope = self.body_scope(db); + let module = parsed_module(db, class_scope.file(db)).load(db); + let class_node = class_scope.node(db).expect_class(&module); + let class_name = &class_node.name; + TextRange::new( + class_name.start(), + class_node + .arguments + .as_deref() + .map(Ranged::end) + .unwrap_or_else(|| class_name.end()), + ) + } +} + +impl<'db> From> for Type<'db> { + fn from(class: ClassLiteral<'db>) -> Type<'db> { + Type::ClassLiteral(class) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] +pub(super) enum InheritanceCycle { + /// The class is cyclically defined and is a participant in the cycle. + /// i.e., it inherits either directly or indirectly from itself. + Participant, + /// The class inherits from a class that is a `Participant` in an inheritance cycle, + /// but is not itself a participant. + Inherited, +} + +impl InheritanceCycle { + pub(super) const fn is_participant(self) -> bool { + matches!(self, InheritanceCycle::Participant) + } +} + +/// CPython internally considers a class a "solid base" if it has an atypical instance memory layout, +/// with additional memory "slots" for each instance, besides the default object metadata and an +/// attribute dictionary. A "solid base" can be a class defined in a C extension which defines C-level +/// instance slots, or a Python class that defines non-empty `__slots__`. +/// +/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other. Knowing if +/// a class is "solid base" or not is therefore valuable for inferring whether two instance types or +/// two subclass-of types are disjoint from each other. It also allows us to detect possible +/// `TypeError`s resulting from class definitions. +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +pub(super) struct SolidBase<'db> { + pub(super) class: ClassLiteral<'db>, + pub(super) kind: SolidBaseKind, +} + +impl<'db> SolidBase<'db> { + /// Creates a [`SolidBase`] instance where we know the class is a solid base + /// because it is special-cased by ty. + fn hard_coded(class: ClassLiteral<'db>) -> Self { + Self { + class, + kind: SolidBaseKind::HardCoded, + } + } + + /// Creates a [`SolidBase`] instance where we know the class is a solid base + /// because of its `__slots__` definition. + fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self { + Self { + class, + kind: SolidBaseKind::DefinesSlots, + } + } + + /// Two solid bases can only coexist in a class's MRO if one is a subclass of the other + fn could_coexist_in_mro_with(&self, db: &'db dyn Db, other: &Self) -> bool { + self == other + || self + .class + .is_subclass_of(db, None, other.class.default_specialization(db)) + || other + .class + .is_subclass_of(db, None, self.class.default_specialization(db)) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(super) enum SolidBaseKind { + /// We know the class is a solid base because of some hardcoded knowledge in ty. + HardCoded, + /// We know the class is a solid base because it has a non-empty `__slots__` definition. + DefinesSlots, +} + +/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow +/// for easier syntax when interacting with very common classes. +/// +/// Feel free to expand this enum if you ever find yourself using the same class in multiple +/// places. +/// Note: good candidates are any classes in `[crate::module_resolver::module::KnownModule]` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(test, derive(strum_macros::EnumIter))] +pub enum KnownClass { + // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` + // and grep for the symbol name in any `.pyi` file. + + // Builtins + Bool, + Object, + Bytes, + Bytearray, + Type, + Int, + Float, + Complex, + Str, + List, + Tuple, + Set, + FrozenSet, + Dict, + Slice, + Property, + BaseException, + Exception, + BaseExceptionGroup, + ExceptionGroup, + Staticmethod, + Classmethod, + Super, + // enum + Enum, + // abc + ABCMeta, + // Types + GenericAlias, + ModuleType, + FunctionType, + MethodType, + MethodWrapperType, + WrapperDescriptorType, + UnionType, + GeneratorType, + AsyncGeneratorType, + // Typeshed + NoneType, // Part of `types` for Python >= 3.10 + // Typing + Any, + StdlibAlias, + SpecialForm, + TypeVar, + ParamSpec, + ParamSpecArgs, + ParamSpecKwargs, + TypeVarTuple, + TypeAliasType, + NoDefaultType, + NamedTuple, + NewType, + SupportsIndex, + Iterable, + // Collections + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, + // sys + VersionInfo, + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + EllipsisType, + NotImplementedType, + // dataclasses + Field, + KwOnly, + // _typeshed._type_checker_internals + NamedTupleFallback, +} + +impl KnownClass { + pub(crate) const fn is_bool(self) -> bool { + matches!(self, Self::Bool) + } + + pub(crate) const fn is_special_form(self) -> bool { + matches!(self, Self::SpecialForm) + } + + /// Determine whether instances of this class are always truthy, always falsy, + /// or have an ambiguous truthiness. + pub(crate) const fn bool(self) -> Truthiness { + match self { + // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` + // variant if the class's `__bool__` method always returns the same thing *and* the + // class is `@final`. + // + // E.g. `ModuleType.__bool__` always returns `True`, but `ModuleType` is not `@final`. + // Equally, `range` is `@final`, but its `__bool__` method can return `False`. + Self::EllipsisType + | Self::NoDefaultType + | Self::MethodType + | Self::Slice + | Self::FunctionType + | Self::VersionInfo + | Self::TypeAliasType + | Self::TypeVar + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Super + | Self::WrapperDescriptorType + | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::MethodWrapperType => Truthiness::AlwaysTrue, + + Self::NoneType => Truthiness::AlwaysFalse, + + Self::Any + | Self::BaseException + | Self::Exception + | Self::ExceptionGroup + | Self::Object + | Self::OrderedDict + | Self::BaseExceptionGroup + | Self::Bool + | Self::Str + | Self::List + | Self::GenericAlias + | Self::NewType + | Self::StdlibAlias + | Self::SupportsIndex + | Self::Set + | Self::Tuple + | Self::Int + | Self::Type + | Self::Bytes + | Self::Bytearray + | Self::FrozenSet + | Self::Property + | Self::SpecialForm + | Self::Dict + | Self::ModuleType + | Self::ChainMap + | Self::Complex + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::Float + | Self::Enum + | Self::ABCMeta + | Self::Iterable + // Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue + | Self::NamedTuple + // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 + // and raises a `TypeError` in Python >=3.14 + // (see https://docs.python.org/3/library/constants.html#NotImplemented) + | Self::NotImplementedType + | Self::Staticmethod + | Self::Classmethod + | Self::Field + | Self::KwOnly + | Self::NamedTupleFallback => Truthiness::Ambiguous, + } + } + + /// Return `true` if this class is a [`SolidBase`] + const fn is_solid_base(self) -> bool { + match self { + Self::Object => false, + + // Most non-`@final` builtins (other than `object`) are solid bases. + Self::Set + | Self::FrozenSet + | Self::BaseException + | Self::Bytearray + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Dict + | Self::Slice + | Self::Property + | Self::Staticmethod + | Self::Classmethod + | Self::Type + | Self::ModuleType + | Self::Super + | Self::GenericAlias + | Self::Deque + | Self::Bytes => true, + + // It doesn't really make sense to ask the question for `@final` types, + // since these are "more than solid bases". But we'll anyway infer a `@final` + // class as being disjoint from a class that doesn't appear in its MRO, + // and we'll anyway complain if we see a class definition that includes a + // `@final` class in its bases. We therefore return `false` here to avoid + // unnecessary duplicate diagnostics elsewhere. + Self::TypeVarTuple + | Self::TypeAliasType + | Self::UnionType + | Self::NoDefaultType + | Self::MethodType + | Self::MethodWrapperType + | Self::FunctionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::StdlibAlias + | Self::SpecialForm + | Self::TypeVar + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::WrapperDescriptorType + | Self::EllipsisType + | Self::NotImplementedType + | Self::KwOnly + | Self::VersionInfo + | Self::Bool + | Self::NoneType => false, + + // Anything with a *runtime* MRO (N.B. sometimes different from the MRO that typeshed gives!) + // with length >2, or anything that is implemented in pure Python, is not a solid base. + Self::ABCMeta + | Self::Any + | Self::Enum + | Self::ChainMap + | Self::Exception + | Self::ExceptionGroup + | Self::Field + | Self::SupportsIndex + | Self::NamedTuple + | Self::NamedTupleFallback + | Self::Counter + | Self::DefaultDict + | Self::OrderedDict + | Self::NewType + | Self::Iterable + | Self::BaseExceptionGroup => false, + } + } + + /// Return `true` if this class is a protocol class. + /// + /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; + /// instead, we'd just look at the bases for these classes, as we do for + /// all other classes. However, the special casing here helps us out in + /// two important ways: + /// + /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`" + /// and "instance of `sys._version_info`". These types are constructed very early + /// on, but it causes problems if we attempt to infer the types of their bases + /// too soon. + /// 2. It's probably more performant. + const fn is_protocol(self) -> bool { + match self { + Self::SupportsIndex | Self::Iterable => true, + + Self::Any + | Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::FrozenSet + | Self::Str + | Self::Set + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::GenericAlias + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::NoneType + | Self::SpecialForm + | Self::TypeVar + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::TypeAliasType + | Self::NoDefaultType + | Self::NamedTuple + | Self::NewType + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::Enum + | Self::ABCMeta + | Self::Super + | Self::StdlibAlias + | Self::VersionInfo + | Self::EllipsisType + | Self::NotImplementedType + | Self::UnionType + | Self::Field + | Self::KwOnly + | Self::NamedTupleFallback => false, + } + } + + pub(crate) fn name(self, db: &dyn Db) -> &'static str { + match self { + Self::Any => "Any", + Self::Bool => "bool", + Self::Object => "object", + Self::Bytes => "bytes", + Self::Bytearray => "bytearray", + Self::Tuple => "tuple", + Self::Int => "int", + Self::Float => "float", + Self::Complex => "complex", + Self::FrozenSet => "frozenset", + Self::Str => "str", + Self::Set => "set", + Self::Dict => "dict", + Self::List => "list", + Self::Type => "type", + Self::Slice => "slice", + Self::Property => "property", + Self::BaseException => "BaseException", + Self::BaseExceptionGroup => "BaseExceptionGroup", + Self::Exception => "Exception", + Self::ExceptionGroup => "ExceptionGroup", + Self::Staticmethod => "staticmethod", + Self::Classmethod => "classmethod", + Self::GenericAlias => "GenericAlias", + Self::ModuleType => "ModuleType", + Self::FunctionType => "FunctionType", + Self::MethodType => "MethodType", + Self::UnionType => "UnionType", + Self::MethodWrapperType => "MethodWrapperType", + Self::WrapperDescriptorType => "WrapperDescriptorType", + Self::GeneratorType => "GeneratorType", + Self::AsyncGeneratorType => "AsyncGeneratorType", + Self::NamedTuple => "NamedTuple", + Self::NoneType => "NoneType", + Self::SpecialForm => "_SpecialForm", + Self::TypeVar => "TypeVar", + Self::ParamSpec => "ParamSpec", + Self::ParamSpecArgs => "ParamSpecArgs", + Self::ParamSpecKwargs => "ParamSpecKwargs", + Self::TypeVarTuple => "TypeVarTuple", + Self::TypeAliasType => "TypeAliasType", + Self::NoDefaultType => "_NoDefaultType", + Self::NewType => "NewType", + Self::SupportsIndex => "SupportsIndex", + Self::ChainMap => "ChainMap", + Self::Counter => "Counter", + Self::DefaultDict => "defaultdict", + Self::Deque => "deque", + Self::OrderedDict => "OrderedDict", + Self::Enum => "Enum", + Self::ABCMeta => "ABCMeta", + Self::Super => "super", + Self::Iterable => "Iterable", + // For example, `typing.List` is defined as `List = _Alias()` in typeshed + Self::StdlibAlias => "_Alias", + // This is the name the type of `sys.version_info` has in typeshed, + // which is different to what `type(sys.version_info).__name__` is at runtime. + // (At runtime, `type(sys.version_info).__name__ == "version_info"`, + // which is impossible to replicate in the stubs since the sole instance of the class + // also has that name in the `sys` module.) + Self::VersionInfo => "_version_info", + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + "EllipsisType" + } else { + "ellipsis" + } + } + Self::NotImplementedType => "_NotImplementedType", + Self::Field => "Field", + Self::KwOnly => "KW_ONLY", + Self::NamedTupleFallback => "NamedTupleFallback", + } + } + + pub(super) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { + struct KnownClassDisplay<'db> { + db: &'db dyn Db, + class: KnownClass, + } + + impl std::fmt::Display for KnownClassDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let KnownClassDisplay { + class: known_class, + db, + } = *self; + write!( + f, + "{module}.{class}", + module = known_class.canonical_module(db), + class = known_class.name(db) + ) + } + } + + KnownClassDisplay { db, class: self } + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing all possible instances of the class. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn to_instance(self, db: &dyn Db) -> Type { + self.to_class_literal(db) + .to_class_type(db) + .map(|class| Type::instance(db, class)) + .unwrap_or_else(Type::unknown) + } + + /// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`] + /// representing a specialization of that class. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + pub(crate) fn to_specialized_class_type<'db>( + self, + db: &'db dyn Db, + specialization: impl IntoIterator>, + ) -> Option> { + let Type::ClassLiteral(class_literal) = self.to_class_literal(db) else { + return None; + }; + let generic_context = class_literal.generic_context(db)?; + + let types = specialization.into_iter().collect::>(); + if types.len() != generic_context.len(db) { + // a cache of the `KnownClass`es that we have already seen mismatched-arity + // specializations for (and therefore that we've already logged a warning for) + static MESSAGES: LazyLock>> = LazyLock::new(Mutex::default); + if MESSAGES.lock().unwrap().insert(self) { + tracing::info!( + "Wrong number of types when specializing {}. \ + Falling back to default specialization for the symbol instead.", + self.display(db) + ); + } + return Some(class_literal.default_specialization(db)); + } + + Some(class_literal.apply_specialization(db, |_| generic_context.specialize(db, types))) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing all possible instances of the generic class with a specialization. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + pub(crate) fn to_specialized_instance<'db>( + self, + db: &'db dyn Db, + specialization: impl IntoIterator>, + ) -> Type<'db> { + self.to_specialized_class_type(db, specialization) + .and_then(|class_type| Type::from(class_type).to_instance(db)) + .unwrap_or_else(Type::unknown) + } + + /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// Return an error if the symbol cannot be found in the expected typeshed module, + /// or if the symbol is not a class definition, or if the symbol is possibly unbound. + fn try_to_class_literal_without_logging( + self, + db: &dyn Db, + ) -> Result { + let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place; + match symbol { + Place::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal), + Place::Type(Type::ClassLiteral(class_literal), Boundness::PossiblyUnbound) => { + Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }) + } + Place::Type(found_type, _) => { + Err(KnownClassLookupError::SymbolNotAClass { found_type }) + } + Place::Unbound => Err(KnownClassLookupError::ClassNotFound), + } + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option { + // a cache of the `KnownClass`es that we have already failed to lookup in typeshed + // (and therefore that we've already logged a warning for) + static MESSAGES: LazyLock>> = LazyLock::new(Mutex::default); + + self.try_to_class_literal_without_logging(db) + .or_else(|lookup_error| { + if MESSAGES.lock().unwrap().insert(self) { + if matches!( + lookup_error, + KnownClassLookupError::ClassPossiblyUnbound { .. } + ) { + tracing::info!("{}", lookup_error.display(db, self)); + } else { + tracing::info!( + "{}. Falling back to `Unknown` for the symbol instead.", + lookup_error.display(db, self) + ); + } + } + + match lookup_error { + KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { + Ok(class_literal) + } + KnownClassLookupError::ClassNotFound { .. } + | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), + } + }) + .ok() + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type { + self.try_to_class_literal(db) + .map(Type::ClassLiteral) + .unwrap_or_else(Type::unknown) + } + + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing that class and all possible subclasses of the class. + /// + /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. + pub(crate) fn to_subclass_of(self, db: &dyn Db) -> Type { + self.to_class_literal(db) + .to_class_type(db) + .map(|class| SubclassOfType::from(db, class)) + .unwrap_or_else(SubclassOfType::subclass_of_unknown) + } + + /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, + /// *and* `class` is a subclass of `other`. + pub(super) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + self.try_to_class_literal_without_logging(db) + .is_ok_and(|class| class.is_subclass_of(db, None, other)) + } + + /// Return the module in which we should look up the definition for this class + fn canonical_module(self, db: &dyn Db) -> KnownModule { + match self { + Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::Slice + | Self::Super + | Self::Property => KnownModule::Builtins, + Self::VersionInfo => KnownModule::Sys, + Self::ABCMeta => KnownModule::Abc, + Self::Enum => KnownModule::Enum, + Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::MethodWrapperType + | Self::UnionType + | Self::WrapperDescriptorType => KnownModule::Types, + Self::NoneType => KnownModule::Typeshed, + Self::Any + | Self::SpecialForm + | Self::TypeVar + | Self::NamedTuple + | Self::StdlibAlias + | Self::Iterable + | Self::SupportsIndex => KnownModule::Typing, + Self::TypeAliasType + | Self::TypeVarTuple + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::NewType => KnownModule::TypingExtensions, + Self::NoDefaultType => { + let python_version = Program::get(db).python_version(db); + + // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` + // singleton, but not for `typing._NoDefaultType`. So we need to switch + // to `typing._NoDefaultType` for newer versions: + if python_version >= PythonVersion::PY313 { + KnownModule::Typing + } else { + KnownModule::TypingExtensions + } + } + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Types + } else { + KnownModule::Builtins + } + } + Self::NotImplementedType => KnownModule::Builtins, + Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict => KnownModule::Collections, + Self::Field => KnownModule::Dataclasses, + Self::KwOnly => KnownModule::Dataclasses, + Self::NamedTupleFallback => KnownModule::TypeCheckerInternals, + } + } + + /// Return true if all instances of this `KnownClass` compare equal. + pub(super) const fn is_single_valued(self) -> bool { + match self { + Self::NoneType + | Self::NoDefaultType + | Self::VersionInfo + | Self::EllipsisType + | Self::TypeAliasType + | Self::UnionType + | Self::NotImplementedType => true, + + Self::Any + | Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::Slice + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::SpecialForm + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::SupportsIndex + | Self::StdlibAlias + | Self::TypeVar + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Enum + | Self::ABCMeta + | Self::Super + | Self::NamedTuple + | Self::NewType + | Self::Field + | Self::KwOnly + | Self::Iterable + | Self::NamedTupleFallback => false, + } + } + + /// Is this class a singleton class? + /// + /// A singleton class is a class where it is known that only one instance can ever exist at runtime. + pub(super) const fn is_singleton(self) -> bool { + match self { + Self::NoneType + | Self::EllipsisType + | Self::NoDefaultType + | Self::VersionInfo + | Self::TypeAliasType + | Self::NotImplementedType => true, + + Self::Any + | Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Property + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::SpecialForm + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::StdlibAlias + | Self::SupportsIndex + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Exception + | Self::ExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::TypeVar + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::Enum + | Self::ABCMeta + | Self::Super + | Self::UnionType + | Self::NamedTuple + | Self::NewType + | Self::Field + | Self::KwOnly + | Self::Iterable + | Self::NamedTupleFallback => false, + } + } + + pub(super) fn try_from_file_and_name( + db: &dyn Db, + file: File, + class_name: &str, + ) -> Option { + // We assert that this match is exhaustive over the right-hand side in the unit test + // `known_class_roundtrip_from_str()` + let candidate = match class_name { + "Any" => Self::Any, + "bool" => Self::Bool, + "object" => Self::Object, + "bytes" => Self::Bytes, + "bytearray" => Self::Bytearray, + "tuple" => Self::Tuple, + "type" => Self::Type, + "int" => Self::Int, + "float" => Self::Float, + "complex" => Self::Complex, + "str" => Self::Str, + "set" => Self::Set, + "frozenset" => Self::FrozenSet, + "dict" => Self::Dict, + "list" => Self::List, + "slice" => Self::Slice, + "property" => Self::Property, + "BaseException" => Self::BaseException, + "BaseExceptionGroup" => Self::BaseExceptionGroup, + "Exception" => Self::Exception, + "ExceptionGroup" => Self::ExceptionGroup, + "staticmethod" => Self::Staticmethod, + "classmethod" => Self::Classmethod, + "GenericAlias" => Self::GenericAlias, + "NoneType" => Self::NoneType, + "ModuleType" => Self::ModuleType, + "GeneratorType" => Self::GeneratorType, + "AsyncGeneratorType" => Self::AsyncGeneratorType, + "FunctionType" => Self::FunctionType, + "MethodType" => Self::MethodType, + "UnionType" => Self::UnionType, + "MethodWrapperType" => Self::MethodWrapperType, + "WrapperDescriptorType" => Self::WrapperDescriptorType, + "NamedTuple" => Self::NamedTuple, + "NewType" => Self::NewType, + "TypeAliasType" => Self::TypeAliasType, + "TypeVar" => Self::TypeVar, + "Iterable" => Self::Iterable, + "ParamSpec" => Self::ParamSpec, + "ParamSpecArgs" => Self::ParamSpecArgs, + "ParamSpecKwargs" => Self::ParamSpecKwargs, + "TypeVarTuple" => Self::TypeVarTuple, + "ChainMap" => Self::ChainMap, + "Counter" => Self::Counter, + "defaultdict" => Self::DefaultDict, + "deque" => Self::Deque, + "OrderedDict" => Self::OrderedDict, + "_Alias" => Self::StdlibAlias, + "_SpecialForm" => Self::SpecialForm, + "_NoDefaultType" => Self::NoDefaultType, + "SupportsIndex" => Self::SupportsIndex, + "Enum" => Self::Enum, + "ABCMeta" => Self::ABCMeta, + "super" => Self::Super, + "_version_info" => Self::VersionInfo, + "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { + Self::EllipsisType + } + "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + Self::EllipsisType + } + "_NotImplementedType" => Self::NotImplementedType, + "Field" => Self::Field, + "KW_ONLY" => Self::KwOnly, + "NamedTupleFallback" => Self::NamedTupleFallback, + _ => return None, + }; + + candidate + .check_module(db, file_to_module(db, file)?.known()?) + .then_some(candidate) + } + + /// Return `true` if the module of `self` matches `module` + fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { + match self { + Self::Any + | Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Type + | Self::Int + | Self::Float + | Self::Complex + | Self::Str + | Self::List + | Self::Tuple + | Self::Set + | Self::FrozenSet + | Self::Dict + | Self::Slice + | Self::Property + | Self::GenericAlias + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will + | Self::ModuleType + | Self::VersionInfo + | Self::BaseException + | Self::Exception + | Self::ExceptionGroup + | Self::EllipsisType + | Self::BaseExceptionGroup + | Self::Staticmethod + | Self::Classmethod + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::Enum + | Self::ABCMeta + | Self::Super + | Self::NotImplementedType + | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType + | Self::WrapperDescriptorType + | Self::Field + | Self::KwOnly + | Self::NamedTupleFallback => module == self.canonical_module(db), + Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), + Self::SpecialForm + | Self::TypeVar + | Self::TypeAliasType + | Self::NoDefaultType + | Self::SupportsIndex + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::NamedTuple + | Self::Iterable + | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), + } + } + + /// Evaluate a call to this known class, emit any diagnostics that are necessary + /// as a result of the call, and return the type that results from the call. + pub(super) fn check_call<'db>( + self, + context: &InferContext<'db, '_>, + index: &SemanticIndex<'db>, + overload_binding: &Binding<'db>, + call_argument_types: &CallArgumentTypes<'_, 'db>, + call_expression: &ast::ExprCall, + ) -> Option> { + let db = context.db(); + let scope = context.scope(); + let module = context.module(); + + match self { + KnownClass::Super => { + // Handle the case where `super()` is called with no arguments. + // In this case, we need to infer the two arguments: + // 1. The nearest enclosing class + // 2. The first parameter of the current function (typically `self` or `cls`) + match overload_binding.parameter_types() { + [] => { + let Some(enclosing_class) = + nearest_enclosing_class(db, index, scope, module) + else { + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic(context, call_expression.into()); + return Some(Type::unknown()); + }; + + // The type of the first parameter if the given scope is function-like (i.e. function or lambda). + // `None` if the scope is not function-like, or has no parameters. + let first_param = match scope.node(db) { + NodeWithScopeKind::Function(f) => { + f.node(module).parameters.iter().next() + } + NodeWithScopeKind::Lambda(l) => l + .node(module) + .parameters + .as_ref() + .into_iter() + .flatten() + .next(), + _ => None, + }; + + let Some(first_param) = first_param else { + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic(context, call_expression.into()); + return Some(Type::unknown()); + }; + + let definition = index.expect_single_definition(first_param); + let first_param = + infer_definition_types(db, definition).binding_type(definition); + + let bound_super = BoundSuperType::build( + db, + Type::ClassLiteral(enclosing_class), + first_param, + ) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); + + Some(bound_super) + } + [Some(pivot_class_type), Some(owner_type)] => { + let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); + + Some(bound_super) + } + _ => None, + } + } + + KnownClass::TypeVar => { + let assigned_to = index + .try_expression(ast::ExprRef::from(call_expression)) + .and_then(|expr| expr.assigned_to(db)); + + let Some(target) = assigned_to.as_ref().and_then(|assigned_to| { + match assigned_to.node(module).targets.as_slice() { + [ast::Expr::Name(target)] => Some(target), + _ => None, + } + }) else { + let builder = + context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; + builder.into_diagnostic( + "A legacy `typing.TypeVar` must be immediately assigned to a variable", + ); + return None; + }; + + let [ + Some(name_param), + constraints, + bound, + default, + contravariant, + covariant, + _infer_variance, + ] = overload_binding.parameter_types() + else { + return None; + }; + + let covariant = covariant + .map(|ty| ty.bool(db)) + .unwrap_or(Truthiness::AlwaysFalse); + + let contravariant = contravariant + .map(|ty| ty.bool(db)) + .unwrap_or(Truthiness::AlwaysFalse); + + let variance = match (contravariant, covariant) { + (Truthiness::Ambiguous, _) => { + let builder = + context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; + builder.into_diagnostic( + "The `contravariant` parameter of a legacy `typing.TypeVar` \ + cannot have an ambiguous value", + ); + return None; + } + (_, Truthiness::Ambiguous) => { + let builder = + context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; + builder.into_diagnostic( + "The `covariant` parameter of a legacy `typing.TypeVar` \ + cannot have an ambiguous value", + ); + return None; + } + (Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => { + let builder = + context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; + builder.into_diagnostic( + "A legacy `typing.TypeVar` cannot be both covariant and contravariant", + ); + return None; + } + (Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => { + TypeVarVariance::Contravariant + } + (Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => TypeVarVariance::Covariant, + (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => { + TypeVarVariance::Invariant + } + }; + + let name_param = name_param.into_string_literal().map(|name| name.value(db)); + + if name_param.is_none_or(|name_param| name_param != target.id) { + let builder = + context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)?; + builder.into_diagnostic(format_args!( + "The name of a legacy `typing.TypeVar`{} must match \ + the name of the variable it is assigned to (`{}`)", + if let Some(name_param) = name_param { + format!(" (`{name_param}`)") + } else { + String::new() + }, + target.id, + )); + return None; + } + + let bound_or_constraint = match (bound, constraints) { + (Some(bound), None) => Some(TypeVarBoundOrConstraints::UpperBound(*bound)), + + (None, Some(_constraints)) => { + // We don't use UnionType::from_elements or UnionBuilder here, + // because we don't want to simplify the list of constraints like + // we do with the elements of an actual union type. + // TODO: Consider using a new `OneOfType` connective here instead, + // since that more accurately represents the actual semantics of + // typevar constraints. + let elements = UnionType::new( + db, + overload_binding + .arguments_for_parameter(call_argument_types, 1) + .map(|(_, ty)| ty) + .collect::>(), + ); + Some(TypeVarBoundOrConstraints::Constraints(elements)) + } + + // TODO: Emit a diagnostic that TypeVar cannot be both bounded and + // constrained + (Some(_), Some(_)) => return None, + + (None, None) => None, + }; + + let containing_assignment = index.expect_single_definition(target); + Some(Type::KnownInstance(KnownInstanceType::TypeVar( + TypeVarInstance::new( + db, + target.id.clone(), + Some(containing_assignment), + bound_or_constraint, + variance, + *default, + TypeVarKind::Legacy, + ), + ))) + } + + KnownClass::TypeAliasType => { + let assigned_to = index + .try_expression(ast::ExprRef::from(call_expression)) + .and_then(|expr| expr.assigned_to(db)); + + let containing_assignment = assigned_to.as_ref().and_then(|assigned_to| { + match assigned_to.node(module).targets.as_slice() { + [ast::Expr::Name(target)] => Some(index.expect_single_definition(target)), + _ => None, + } + }); + + let [Some(name), Some(value), ..] = overload_binding.parameter_types() else { + return None; + }; + + name.into_string_literal() + .map(|name| { + Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::Bare( + BareTypeAliasType::new( + db, + ast::name::Name::new(name.value(db)), + containing_assignment, + value, + ), + ))) + }) + .or_else(|| { + let builder = + context.report_lint(&INVALID_TYPE_ALIAS_TYPE, call_expression)?; + builder.into_diagnostic( + "The name of a `typing.TypeAlias` must be a string literal", + ); + None + }) + } + + _ => None, + } + } +} + +/// Enumeration of ways in which looking up a [`KnownClass`] in typeshed could fail. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum KnownClassLookupError<'db> { + /// There is no symbol by that name in the expected typeshed module. + ClassNotFound, + /// There is a symbol by that name in the expected typeshed module, + /// but it's not a class. + SymbolNotAClass { found_type: Type<'db> }, + /// There is a symbol by that name in the expected typeshed module, + /// and it's a class definition, but it's possibly unbound. + ClassPossiblyUnbound { class_literal: ClassLiteral<'db> }, +} + +impl<'db> KnownClassLookupError<'db> { + fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { + struct ErrorDisplay<'db> { + db: &'db dyn Db, + class: KnownClass, + error: KnownClassLookupError<'db>, + } + + impl std::fmt::Display for ErrorDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ErrorDisplay { db, class, error } = *self; + + let class = class.display(db); + let python_version = Program::get(db).python_version(db); + + match error { + KnownClassLookupError::ClassNotFound => write!( + f, + "Could not find class `{class}` in typeshed on Python {python_version}", + ), + KnownClassLookupError::SymbolNotAClass { found_type } => write!( + f, + "Error looking up `{class}` in typeshed: expected to find a class definition \ + on Python {python_version}, but found a symbol of type `{found_type}` instead", + found_type = found_type.display(db), + ), + KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( + f, + "Error looking up `{class}` in typeshed on Python {python_version}: \ + expected to find a fully bound symbol, but found one that is possibly unbound", + ), + } + } + } + + ErrorDisplay { + db, + class, + error: *self, + } + } +} + +pub(crate) struct SliceLiteral { + pub(crate) start: Option, + pub(crate) stop: Option, + pub(crate) step: Option, +} + +impl<'db> Type<'db> { + /// If this type represents a valid slice literal, returns a [`SliceLiteral`] describing it. + /// Otherwise returns `None`. + /// + /// The type must be a specialization of the `slice` builtin type, where the specialized + /// typevars are statically known integers or `None`. + pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option { + let ClassType::Generic(alias) = self.into_nominal_instance()?.class else { + return None; + }; + if !alias.origin(db).is_known(db, KnownClass::Slice) { + return None; + } + let [start, stop, step] = alias.specialization(db).types(db) else { + return None; + }; + + let to_u32 = |ty: &Type<'db>| match ty { + Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(), + Type::BooleanLiteral(b) => Some(Some(i32::from(*b))), + Type::NominalInstance(instance) + if instance.class.is_known(db, KnownClass::NoneType) => + { + Some(None) + } + _ => None, + }; + Some(SliceLiteral { + start: to_u32(start)?, + stop: to_u32(stop)?, + step: to_u32(step)?, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) struct MetaclassError<'db> { + kind: MetaclassErrorKind<'db>, +} + +impl<'db> MetaclassError<'db> { + /// Return an [`MetaclassErrorKind`] variant describing why we could not resolve the metaclass for this class. + pub(super) fn reason(&self) -> &MetaclassErrorKind<'db> { + &self.kind + } +} + +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) enum MetaclassErrorKind<'db> { + /// The class has incompatible metaclasses in its inheritance hierarchy. + /// + /// The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all + /// its bases. + Conflict { + /// `candidate1` will either be the explicit `metaclass=` keyword in the class definition, + /// or the inferred metaclass of a base class + candidate1: MetaclassCandidate<'db>, + + /// `candidate2` will always be the inferred metaclass of a base class + candidate2: MetaclassCandidate<'db>, + + /// Flag to indicate whether `candidate1` is the explicit `metaclass=` keyword or the + /// inferred metaclass of a base class. This helps us give better error messages in diagnostics. + candidate1_is_base_class: bool, + }, + /// The metaclass is not callable + NotCallable(Type<'db>), + /// The metaclass is of a union type whose some members are not callable + PartlyNotCallable(Type<'db>), + /// A cycle was encountered attempting to determine the metaclass + Cycle, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum SlotsKind { + /// `__slots__` is not found in the class. + NotSpecified, + /// `__slots__` is defined but empty: `__slots__ = ()`. + Empty, + /// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`. + NotEmpty, + /// `__slots__` is defined but its value is dynamic: + /// * `__slots__ = tuple(a for a in b)` + /// * `__slots__ = ["a", "b"]` + Dynamic, +} + +impl SlotsKind { + fn from(db: &dyn Db, base: ClassLiteral) -> Self { + let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place + else { + return Self::NotSpecified; + }; + + if matches!(bound, Boundness::PossiblyUnbound) { + return Self::Dynamic; + } + + match slots_ty { + // __slots__ = ("a", "b") + Type::Tuple(tuple) => { + let tuple = tuple.tuple(db); + if tuple.is_variadic() { + Self::Dynamic + } else if tuple.is_empty() { + Self::Empty + } else { + Self::NotEmpty + } + } + + // __slots__ = "abc" # Same as `("abc",)` + Type::StringLiteral(_) => Self::NotEmpty, + + _ => Self::Dynamic, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::tests::setup_db; + use crate::module_resolver::resolve_module; + use crate::{PythonVersionSource, PythonVersionWithSource}; + use salsa::Setter; + use strum::IntoEnumIterator; + + #[test] + fn known_class_roundtrip_from_str() { + let db = setup_db(); + for class in KnownClass::iter() { + let class_name = class.name(&db); + let class_module = resolve_module(&db, &class.canonical_module(&db).name()).unwrap(); + + assert_eq!( + KnownClass::try_from_file_and_name(&db, class_module.file().unwrap(), class_name), + Some(class), + "`KnownClass::candidate_from_str` appears to be missing a case for `{class_name}`" + ); + } + } + + #[test] + fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { + let mut db = setup_db(); + + Program::get(&db) + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: PythonVersion::latest_ty(), + source: PythonVersionSource::default(), + }); + + for class in KnownClass::iter() { + assert_ne!( + class.to_instance(&db), + Type::unknown(), + "Unexpectedly fell back to `Unknown` for `{class:?}`" + ); + } + } + + #[test] + fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { + let mut db = setup_db(); + + for class in KnownClass::iter() { + let version_added = match class { + KnownClass::UnionType => PythonVersion::PY310, + KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311, + KnownClass::GenericAlias => PythonVersion::PY39, + KnownClass::KwOnly => PythonVersion::PY310, + _ => PythonVersion::PY37, + }; + + Program::get(&db) + .set_python_version_with_source(&mut db) + .to(PythonVersionWithSource { + version: version_added, + source: PythonVersionSource::default(), + }); + + assert_ne!( + class.to_instance(&db), + Type::unknown(), + "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" + ); + } + } +} diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs new file mode 100644 index 0000000000000..ce5686e9c1ed0 --- /dev/null +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -0,0 +1,353 @@ +use crate::Db; +use crate::types::generics::Specialization; +use crate::types::{ + ClassType, DynamicType, KnownClass, KnownInstanceType, MroError, MroIterator, SpecialFormType, + Type, TypeMapping, TypeTransformer, todo_type, +}; + +/// Enumeration of the possible kinds of types we allow in class bases. +/// +/// This is much more limited than the [`Type`] enum: all types that would be invalid to have as a +/// class base are transformed into [`ClassBase::unknown()`] +/// +/// Note that a non-specialized generic class _cannot_ be a class base. When we see a +/// non-specialized generic class in any type expression (including the list of base classes), we +/// automatically construct the default specialization for that class. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub enum ClassBase<'db> { + Dynamic(DynamicType), + Class(ClassType<'db>), + /// Although `Protocol` is not a class in typeshed's stubs, it is at runtime, + /// and can appear in the MRO of a class. + Protocol, + /// Bare `Generic` cannot be subclassed directly in user code, + /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, + /// `Protocol[T]`, or bare `Protocol`. + Generic, +} + +impl<'db> ClassBase<'db> { + pub(crate) const fn unknown() -> Self { + Self::Dynamic(DynamicType::Unknown) + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + match self { + Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), + Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), + Self::Protocol | Self::Generic => self, + } + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { + match self { + ClassBase::Class(class) => class.name(db), + ClassBase::Dynamic(DynamicType::Any) => "Any", + ClassBase::Dynamic(DynamicType::Unknown) => "Unknown", + ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec) => "@Todo", + ClassBase::Protocol => "Protocol", + ClassBase::Generic => "Generic", + } + } + + /// Return a `ClassBase` representing the class `builtins.object` + pub(super) fn object(db: &'db dyn Db) -> Self { + KnownClass::Object + .to_class_literal(db) + .to_class_type(db) + .map_or(Self::unknown(), Self::Class) + } + + /// Attempt to resolve `ty` into a `ClassBase`. + /// + /// Return `None` if `ty` is not an acceptable type for a class base. + pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + match ty { + Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), + Type::ClassLiteral(literal) => { + if literal.is_known(db, KnownClass::Any) { + Some(Self::Dynamic(DynamicType::Any)) + } else if literal.is_known(db, KnownClass::NamedTuple) { + // TODO: Figure out the tuple spec for the named tuple + Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db)) + } else { + Some(Self::Class(literal.default_specialization(db))) + } + } + Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), + Type::NominalInstance(instance) + if instance.class.is_known(db, KnownClass::GenericAlias) => + { + Self::try_from_type(db, todo_type!("GenericAlias instance")) + } + Type::SubclassOf(subclass_of) => subclass_of + .subclass_of() + .into_dynamic() + .map(ClassBase::Dynamic), + Type::Intersection(inter) => { + let valid_element = inter + .positive(db) + .iter() + .find_map(|elem| ClassBase::try_from_type(db, *elem))?; + + if ty.is_disjoint_from(db, KnownClass::Type.to_instance(db)) { + None + } else { + Some(valid_element) + } + } + Type::Union(union) => { + // We do not support full unions of MROs (yet). Until we do, + // support the cases where one of the types in the union is + // a dynamic type such as `Any` or `Unknown`, and all other + // types *would be* valid class bases. In this case, we can + // "fold" the other potential bases into the dynamic type, + // and return `Any`/`Unknown` as the class base to prevent + // invalid-base diagnostics and further downstream errors. + let Some(Type::Dynamic(dynamic)) = union + .elements(db) + .iter() + .find(|elem| matches!(elem, Type::Dynamic(_))) + else { + return None; + }; + + if union + .elements(db) + .iter() + .all(|elem| ClassBase::try_from_type(db, *elem).is_some()) + { + Some(ClassBase::Dynamic(*dynamic)) + } else { + None + } + } + Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`? + + // This likely means that we're in unreachable code, + // in which case we want to treat `Never` in a forgiving way and silence diagnostics + Type::Never => Some(ClassBase::unknown()), + + Type::PropertyInstance(_) + | Type::BooleanLiteral(_) + | Type::FunctionLiteral(_) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::BytesLiteral(_) + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::Tuple(_) + | Type::ModuleLiteral(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) + | Type::ProtocolInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::TypeIs(_) => None, + + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), + KnownInstanceType::SubscriptedProtocol(_) => Some(Self::Protocol), + KnownInstanceType::TypeAliasType(_) | KnownInstanceType::TypeVar(_) => None, + }, + + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Annotated + | SpecialFormType::Literal + | SpecialFormType::LiteralString + | SpecialFormType::Union + | SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::Final + | SpecialFormType::NotRequired + | SpecialFormType::TypeGuard + | SpecialFormType::TypeIs + | SpecialFormType::TypingSelf + | SpecialFormType::Unpack + | SpecialFormType::ClassVar + | SpecialFormType::Concatenate + | SpecialFormType::Required + | SpecialFormType::TypeAlias + | SpecialFormType::ReadOnly + | SpecialFormType::Optional + | SpecialFormType::Not + | SpecialFormType::Intersection + | SpecialFormType::TypeOf + | SpecialFormType::CallableTypeOf + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy => None, + + SpecialFormType::Unknown => Some(Self::unknown()), + + SpecialFormType::Protocol => Some(Self::Protocol), + SpecialFormType::Generic => Some(Self::Generic), + + // TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO + SpecialFormType::Dict => { + Self::try_from_type(db, KnownClass::Dict.to_class_literal(db)) + } + SpecialFormType::List => { + Self::try_from_type(db, KnownClass::List.to_class_literal(db)) + } + SpecialFormType::Type => { + Self::try_from_type(db, KnownClass::Type.to_class_literal(db)) + } + SpecialFormType::Tuple => { + Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db)) + } + SpecialFormType::Set => { + Self::try_from_type(db, KnownClass::Set.to_class_literal(db)) + } + SpecialFormType::FrozenSet => { + Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db)) + } + SpecialFormType::ChainMap => { + Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db)) + } + SpecialFormType::Counter => { + Self::try_from_type(db, KnownClass::Counter.to_class_literal(db)) + } + SpecialFormType::DefaultDict => { + Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db)) + } + SpecialFormType::Deque => { + Self::try_from_type(db, KnownClass::Deque.to_class_literal(db)) + } + SpecialFormType::OrderedDict => { + Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) + } + SpecialFormType::TypedDict => Self::try_from_type(db, todo_type!("TypedDict")), + SpecialFormType::Callable => { + Self::try_from_type(db, todo_type!("Support for Callable as a base class")) + } + }, + } + } + + pub(super) fn into_class(self) -> Option> { + match self { + Self::Class(class) => Some(class), + Self::Dynamic(_) | Self::Generic | Self::Protocol => None, + } + } + + fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + match self { + Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)), + Self::Dynamic(_) | Self::Generic | Self::Protocol => self, + } + } + + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Self { + if let Some(specialization) = specialization { + self.apply_type_mapping(db, &TypeMapping::Specialization(specialization)) + } else { + self + } + } + + pub(super) fn has_cyclic_mro(self, db: &'db dyn Db) -> bool { + match self { + ClassBase::Class(class) => { + let (class_literal, specialization) = class.class_literal(db); + class_literal + .try_mro(db, specialization) + .is_err_and(MroError::is_cycle) + } + ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false, + } + } + + /// Iterate over the MRO of this base + pub(super) fn mro( + self, + db: &'db dyn Db, + additional_specialization: Option>, + ) -> impl Iterator> { + match self { + ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), + ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self), + ClassBase::Class(class) => { + ClassBaseMroIterator::from_class(db, class, additional_specialization) + } + } + } +} + +impl<'db> From> for ClassBase<'db> { + fn from(value: ClassType<'db>) -> Self { + ClassBase::Class(value) + } +} + +impl<'db> From> for Type<'db> { + fn from(value: ClassBase<'db>) -> Self { + match value { + ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), + ClassBase::Class(class) => class.into(), + ClassBase::Protocol => Type::SpecialForm(SpecialFormType::Protocol), + ClassBase::Generic => Type::SpecialForm(SpecialFormType::Generic), + } + } +} + +impl<'db> From<&ClassBase<'db>> for Type<'db> { + fn from(value: &ClassBase<'db>) -> Self { + Self::from(*value) + } +} + +/// An iterator over the MRO of a class base. +enum ClassBaseMroIterator<'db> { + Length2(core::array::IntoIter, 2>), + Length3(core::array::IntoIter, 3>), + FromClass(MroIterator<'db>), +} + +impl<'db> ClassBaseMroIterator<'db> { + /// Iterate over an MRO of length 2 that consists of `first_element` and then `object`. + fn length_2(db: &'db dyn Db, first_element: ClassBase<'db>) -> Self { + ClassBaseMroIterator::Length2([first_element, ClassBase::object(db)].into_iter()) + } + + /// Iterate over an MRO of length 3 that consists of `first_element`, then `second_element`, then `object`. + fn length_3(db: &'db dyn Db, element_1: ClassBase<'db>, element_2: ClassBase<'db>) -> Self { + ClassBaseMroIterator::Length3([element_1, element_2, ClassBase::object(db)].into_iter()) + } + + /// Iterate over the MRO of an arbitrary class. The MRO may be of any length. + fn from_class( + db: &'db dyn Db, + class: ClassType<'db>, + additional_specialization: Option>, + ) -> Self { + ClassBaseMroIterator::FromClass(class.iter_mro_specialized(db, additional_specialization)) + } +} + +impl<'db> Iterator for ClassBaseMroIterator<'db> { + type Item = ClassBase<'db>; + + fn next(&mut self) -> Option { + match self { + Self::Length2(iter) => iter.next(), + Self::Length3(iter) => iter.next(), + Self::FromClass(iter) => iter.next(), + } + } +} + +impl std::iter::FusedIterator for ClassBaseMroIterator<'_> {} diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs similarity index 85% rename from crates/red_knot_python_semantic/src/types/context.rs rename to crates/ty_python_semantic/src/types/context.rs index 5f92be04ab26f..fb4d449c548e5 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -1,21 +1,25 @@ use std::fmt; use drop_bomb::DebugDropBomb; +use ruff_db::diagnostic::{DiagnosticTag, SubDiagnostic}; +use ruff_db::parsed::ParsedModuleRef; use ruff_db::{ diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span}, files::File, }; use ruff_text_size::{Ranged, TextRange}; -use super::{binding_type, Type, TypeCheckDiagnostics}; +use super::{Type, TypeCheckDiagnostics, binding_type}; -use crate::semantic_index::symbol::ScopeId; +use crate::lint::LintSource; +use crate::semantic_index::place::ScopeId; +use crate::semantic_index::semantic_index; +use crate::types::function::FunctionDecorators; use crate::{ + Db, lint::{LintId, LintMetadata}, suppression::suppressions, - Db, }; -use crate::{semantic_index::semantic_index, types::FunctionDecorators}; /// Context for inferring the types of a single file. /// @@ -29,24 +33,28 @@ use crate::{semantic_index::semantic_index, types::FunctionDecorators}; /// It's important that the context is explicitly consumed before dropping by calling /// [`InferContext::finish`] and the returned diagnostics must be stored /// on the current [`TypeInference`](super::infer::TypeInference) result. -pub(crate) struct InferContext<'db> { +pub(crate) struct InferContext<'db, 'ast> { db: &'db dyn Db, scope: ScopeId<'db>, file: File, + module: &'ast ParsedModuleRef, diagnostics: std::cell::RefCell, no_type_check: InNoTypeCheck, bomb: DebugDropBomb, } -impl<'db> InferContext<'db> { - pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self { +impl<'db, 'ast> InferContext<'db, 'ast> { + pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>, module: &'ast ParsedModuleRef) -> Self { Self { db, scope, + module, file: scope.file(db), diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()), no_type_check: InNoTypeCheck::default(), - bomb: DebugDropBomb::new("`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics."), + bomb: DebugDropBomb::new( + "`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics.", + ), } } @@ -55,6 +63,15 @@ impl<'db> InferContext<'db> { self.file } + /// The module for which the types are inferred. + pub(crate) fn module(&self) -> &'ast ParsedModuleRef { + self.module + } + + pub(crate) fn scope(&self) -> ScopeId<'db> { + self.scope + } + /// Create a span with the range of the given expression /// in the file being currently type checked. /// @@ -65,6 +82,14 @@ impl<'db> InferContext<'db> { Span::from(self.file()).with_range(ranged.range()) } + /// Create a secondary annotation attached to the range of the given value in + /// the file currently being type checked. + /// + /// The annotation returned has no message attached to it. + pub(crate) fn secondary(&self, ranged: T) -> Annotation { + Annotation::secondary(self.span(ranged)) + } + pub(crate) fn db(&self) -> &'db dyn Db { self.db } @@ -73,22 +98,6 @@ impl<'db> InferContext<'db> { self.diagnostics.get_mut().extend(other); } - /// Reports a lint located at `ranged`. - pub(super) fn report_lint_old( - &self, - lint: &'static LintMetadata, - ranged: T, - message: fmt::Arguments, - ) where - T: Ranged, - { - let Some(builder) = self.report_lint(lint, ranged) else { - return; - }; - let mut diag = builder.into_diagnostic(""); - diag.set_primary_message(message); - } - /// Optionally return a builder for a lint diagnostic guard. /// /// If the current context believes a diagnostic should be reported for @@ -163,7 +172,7 @@ impl<'db> InferContext<'db> { // Inspect all ancestor function scopes by walking bottom up and infer the function's type. let mut function_scope_tys = index .ancestor_scopes(scope_id) - .filter_map(|(_, scope)| scope.node().as_function()) + .filter_map(|(_, scope)| scope.node().as_function(self.module())) .map(|node| binding_type(self.db, index.expect_single_definition(node))) .filter_map(Type::into_function_literal); @@ -178,7 +187,7 @@ impl<'db> InferContext<'db> { /// Are we currently inferring types in a stub file? pub(crate) fn in_stub(&self) -> bool { - self.file.is_stub(self.db().upcast()) + self.file.is_stub(self.db()) } #[must_use] @@ -190,7 +199,7 @@ impl<'db> InferContext<'db> { } } -impl fmt::Debug for InferContext<'_> { +impl fmt::Debug for InferContext<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("TyContext") .field("file", &self.file) @@ -224,11 +233,13 @@ pub(crate) enum InNoTypeCheck { /// will attach a message to the primary span on the diagnostic. pub(super) struct LintDiagnosticGuard<'db, 'ctx> { /// The typing context. - ctx: &'ctx InferContext<'db>, + ctx: &'ctx InferContext<'db, 'ctx>, /// The diagnostic that we want to report. /// /// This is always `Some` until the `Drop` impl. diag: Option, + + source: LintSource, } impl LintDiagnosticGuard<'_, '_> { @@ -267,6 +278,21 @@ impl LintDiagnosticGuard<'_, '_> { let ann = self.primary_annotation_mut().unwrap(); ann.set_message(message); } + + /// Adds a tag on the primary annotation for this diagnostic. + /// + /// This tag is associated with the primary annotation created + /// for every `Diagnostic` that uses the `LintDiagnosticGuard` API. + /// Specifically, the annotation is derived from the `TextRange` given to + /// the `InferContext::report_lint` API. + /// + /// Callers can add additional primary or secondary annotations via the + /// `DerefMut` trait implementation to a `Diagnostic`. + #[expect(dead_code)] + pub(super) fn add_primary_tag(&mut self, tag: DiagnosticTag) { + let ann = self.primary_annotation_mut().unwrap(); + ann.push_tag(tag); + } } impl std::ops::Deref for LintDiagnosticGuard<'_, '_> { @@ -302,7 +328,22 @@ impl Drop for LintDiagnosticGuard<'_, '_> { // OK because the only way `self.diag` is `None` // is via this impl, which can only run at most // once. - let diag = self.diag.take().unwrap(); + let mut diag = self.diag.take().unwrap(); + + diag.sub(SubDiagnostic::new( + Severity::Info, + match self.source { + LintSource::Default => format!("rule `{}` is enabled by default", diag.id()), + LintSource::Cli => format!("rule `{}` was selected on the command line", diag.id()), + LintSource::File => { + format!( + "rule `{}` was selected in the configuration file", + diag.id() + ) + } + }, + )); + self.ctx.diagnostics.borrow_mut().push(diag); } } @@ -334,15 +375,16 @@ impl Drop for LintDiagnosticGuard<'_, '_> { /// it is known that the diagnostic should not be reported. This can happen /// when the diagnostic is disabled or suppressed (among other reasons). pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> { - ctx: &'ctx InferContext<'db>, + ctx: &'ctx InferContext<'db, 'ctx>, id: DiagnosticId, severity: Severity, + source: LintSource, primary_span: Span, } impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { fn new( - ctx: &'ctx InferContext<'db>, + ctx: &'ctx InferContext<'db, 'ctx>, lint: &'static LintMetadata, range: TextRange, ) -> Option> { @@ -363,7 +405,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { let lint_id = LintId::of(lint); // Skip over diagnostics if the rule // is disabled. - let severity = ctx.db.rule_selection().severity(lint_id)?; + let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint_id)?; // If we're not in type checking mode, // we can bail now. if ctx.is_in_no_type_check() { @@ -382,6 +424,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { ctx, id, severity, + source, primary_span, }) } @@ -396,7 +439,6 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - #[must_use] pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, @@ -410,6 +452,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { diag.annotate(Annotation::primary(self.primary_span.clone())); LintDiagnosticGuard { ctx: self.ctx, + source: self.source, diag: Some(diag), } } @@ -431,7 +474,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// if either is violated, then the `Drop` impl on `DiagnosticGuard` will /// panic. pub(super) struct DiagnosticGuard<'db, 'ctx> { - ctx: &'ctx InferContext<'db>, + ctx: &'ctx InferContext<'db, 'ctx>, /// The diagnostic that we want to report. /// /// This is always `Some` until the `Drop` impl. @@ -467,16 +510,26 @@ impl std::ops::DerefMut for DiagnosticGuard<'_, '_> { /// /// # Panics /// -/// This panics when the the underlying diagnostic lacks a primary +/// This panics when the underlying diagnostic lacks a primary /// annotation, or if it has one and its file doesn't match the file /// being type checked. impl Drop for DiagnosticGuard<'_, '_> { fn drop(&mut self) { + if std::thread::panicking() { + // Don't submit diagnostics when panicking because they might be incomplete. + return; + } + // OK because the only way `self.diag` is `None` // is via this impl, which can only run at most // once. let diag = self.diag.take().unwrap(); + if std::thread::panicking() { + // Don't submit diagnostics when panicking because they might be incomplete. + return; + } + let Some(ann) = diag.primary_annotation() else { panic!( "All diagnostics reported by `InferContext` must have a \ @@ -486,7 +539,7 @@ impl Drop for DiagnosticGuard<'_, '_> { }; let expected_file = self.ctx.file(); - let got_file = ann.get_span().file(); + let got_file = ann.get_span().expect_ty_file(); assert_eq!( expected_file, got_file, @@ -509,14 +562,14 @@ impl Drop for DiagnosticGuard<'_, '_> { /// minimal amount of information with which to construct a diagnostic) before /// one can mutate the diagnostic. pub(super) struct DiagnosticGuardBuilder<'db, 'ctx> { - ctx: &'ctx InferContext<'db>, + ctx: &'ctx InferContext<'db, 'ctx>, id: DiagnosticId, severity: Severity, } impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> { fn new( - ctx: &'ctx InferContext<'db>, + ctx: &'ctx InferContext<'db, 'ctx>, id: DiagnosticId, severity: Severity, ) -> Option> { @@ -533,7 +586,6 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - #[must_use] pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs new file mode 100644 index 0000000000000..2d1927d16362e --- /dev/null +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -0,0 +1,62 @@ +use rustc_hash::FxHashMap; + +use crate::FxIndexSet; +use crate::types::Type; +use std::cmp::Eq; +use std::hash::Hash; + +pub(crate) type TypeTransformer<'db> = CycleDetector, Type<'db>>; + +impl Default for TypeTransformer<'_> { + fn default() -> Self { + // TODO: proper recursive type handling + + // This must be Any, not e.g. a todo type, because Any is the normalized form of the + // dynamic type (that is, todo types are normalized to Any). + CycleDetector::new(Type::any()) + } +} + +pub(crate) type PairVisitor<'db> = CycleDetector<(Type<'db>, Type<'db>), bool>; + +#[derive(Debug)] +pub(crate) struct CycleDetector { + /// If the type we're visiting is present in `seen`, + /// it indicates that we've hit a cycle (due to a recursive type); + /// we need to immediately short circuit the whole operation and return the fallback value. + /// That's why we pop items off the end of `seen` after we've visited them. + seen: FxIndexSet, + + /// Unlike `seen`, this field is a pure performance optimisation (and an essential one). + /// If the type we're trying to normalize is present in `cache`, it doesn't necessarily mean we've hit a cycle: + /// it just means that we've already visited this inner type as part of a bigger call chain we're currently in. + /// Since this cache is just a performance optimisation, it doesn't make sense to pop items off the end of the + /// cache after they've been visited (it would sort-of defeat the point of a cache if we did!) + cache: FxHashMap, + + fallback: R, +} + +impl CycleDetector { + pub(crate) fn new(fallback: R) -> Self { + CycleDetector { + seen: FxIndexSet::default(), + cache: FxHashMap::default(), + fallback, + } + } + + pub(crate) fn visit(&mut self, item: T, func: impl FnOnce(&mut Self) -> R) -> R { + if !self.seen.insert(item) { + return self.fallback; + } + if let Some(ty) = self.cache.get(&item) { + self.seen.pop(); + return *ty; + } + let ret = func(self); + self.cache.insert(item, ret); + self.seen.pop(); + ret + } +} diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs new file mode 100644 index 0000000000000..a81fb0f925ec5 --- /dev/null +++ b/crates/ty_python_semantic/src/types/definition.rs @@ -0,0 +1,47 @@ +use crate::semantic_index::definition::Definition; +use crate::{Db, Module}; +use ruff_db::files::FileRange; +use ruff_db::parsed::parsed_module; +use ruff_db::source::source_text; +use ruff_text_size::{TextLen, TextRange}; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum TypeDefinition<'db> { + Module(Module), + Class(Definition<'db>), + Function(Definition<'db>), + TypeVar(Definition<'db>), + TypeAlias(Definition<'db>), +} + +impl TypeDefinition<'_> { + pub fn focus_range(&self, db: &dyn Db) -> Option { + match self { + Self::Module(_) => None, + Self::Class(definition) + | Self::Function(definition) + | Self::TypeVar(definition) + | Self::TypeAlias(definition) => { + let module = parsed_module(db, definition.file(db)).load(db); + Some(definition.focus_range(db, &module)) + } + } + } + + pub fn full_range(&self, db: &dyn Db) -> Option { + match self { + Self::Module(module) => { + let file = module.file()?; + let source = source_text(db, file); + Some(FileRange::new(file, TextRange::up_to(source.text_len()))) + } + Self::Class(definition) + | Self::Function(definition) + | Self::TypeVar(definition) + | Self::TypeAlias(definition) => { + let module = parsed_module(db, definition.file(db)).load(db); + Some(definition.full_range(db, &module)) + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs new file mode 100644 index 0000000000000..e3cf81f75fb45 --- /dev/null +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -0,0 +1,2477 @@ +use super::call::CallErrorKind; +use super::context::InferContext; +use super::mro::DuplicateBaseError; +use super::{ + CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass, + add_inferred_python_version_hint_to_diagnostic, +}; +use crate::lint::{Level, LintRegistryBuilder, LintStatus}; +use crate::suppression::FileSuppressionId; +use crate::types::LintDiagnosticGuard; +use crate::types::class::{SolidBase, SolidBaseKind}; +use crate::types::function::KnownFunction; +use crate::types::string_annotation::{ + BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION, + IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, + RAW_STRING_TYPE_ANNOTATION, +}; +use crate::types::tuple::TupleType; +use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral}; +use crate::util::diagnostics::format_enumeration; +use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint}; +use itertools::Itertools; +use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; +use ruff_python_ast::{self as ast, AnyNodeRef}; +use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::FxHashSet; +use std::fmt::Formatter; + +/// Registers all known type check lints. +pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { + registry.register_lint(&CALL_NON_CALLABLE); + registry.register_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL); + registry.register_lint(&CONFLICTING_ARGUMENT_FORMS); + registry.register_lint(&CONFLICTING_DECLARATIONS); + registry.register_lint(&CONFLICTING_METACLASS); + registry.register_lint(&CYCLIC_CLASS_DEFINITION); + registry.register_lint(&DIVISION_BY_ZERO); + registry.register_lint(&DUPLICATE_BASE); + registry.register_lint(&DUPLICATE_KW_ONLY); + registry.register_lint(&INSTANCE_LAYOUT_CONFLICT); + registry.register_lint(&INCONSISTENT_MRO); + registry.register_lint(&INDEX_OUT_OF_BOUNDS); + registry.register_lint(&INVALID_ARGUMENT_TYPE); + registry.register_lint(&INVALID_RETURN_TYPE); + registry.register_lint(&INVALID_ASSIGNMENT); + registry.register_lint(&INVALID_BASE); + registry.register_lint(&INVALID_CONTEXT_MANAGER); + registry.register_lint(&INVALID_DECLARATION); + registry.register_lint(&INVALID_EXCEPTION_CAUGHT); + registry.register_lint(&INVALID_GENERIC_CLASS); + registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); + registry.register_lint(&INVALID_TYPE_ALIAS_TYPE); + registry.register_lint(&INVALID_METACLASS); + registry.register_lint(&INVALID_OVERLOAD); + registry.register_lint(&INVALID_PARAMETER_DEFAULT); + registry.register_lint(&INVALID_PROTOCOL); + registry.register_lint(&INVALID_RAISE); + registry.register_lint(&INVALID_SUPER_ARGUMENT); + registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT); + registry.register_lint(&INVALID_TYPE_FORM); + registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION); + registry.register_lint(&INVALID_TYPE_GUARD_CALL); + registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS); + registry.register_lint(&MISSING_ARGUMENT); + registry.register_lint(&NO_MATCHING_OVERLOAD); + registry.register_lint(&NON_SUBSCRIPTABLE); + registry.register_lint(&NOT_ITERABLE); + registry.register_lint(&UNSUPPORTED_BOOL_CONVERSION); + registry.register_lint(&PARAMETER_ALREADY_ASSIGNED); + registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE); + registry.register_lint(&POSSIBLY_UNBOUND_IMPORT); + registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE); + registry.register_lint(&SUBCLASS_OF_FINAL_CLASS); + registry.register_lint(&TYPE_ASSERTION_FAILURE); + registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS); + registry.register_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS); + registry.register_lint(&UNDEFINED_REVEAL); + registry.register_lint(&UNKNOWN_ARGUMENT); + registry.register_lint(&UNRESOLVED_ATTRIBUTE); + registry.register_lint(&UNRESOLVED_IMPORT); + registry.register_lint(&UNRESOLVED_REFERENCE); + registry.register_lint(&UNSUPPORTED_BASE); + registry.register_lint(&UNSUPPORTED_OPERATOR); + registry.register_lint(&ZERO_STEPSIZE_IN_SLICE); + registry.register_lint(&STATIC_ASSERT_ERROR); + registry.register_lint(&INVALID_ATTRIBUTE_ACCESS); + registry.register_lint(&REDUNDANT_CAST); + + // String annotations + registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION); + registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION); + registry.register_lint(&FSTRING_TYPE_ANNOTATION); + registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION); + registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION); + registry.register_lint(&RAW_STRING_TYPE_ANNOTATION); +} + +declare_lint! { + /// ## What it does + /// Checks for calls to non-callable objects. + /// + /// ## Why is this bad? + /// Calling a non-callable object will raise a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// 4() # TypeError: 'int' object is not callable + /// ``` + pub(crate) static CALL_NON_CALLABLE = { + summary: "detects calls to non-callable objects", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for implicit calls to possibly unbound methods. + /// + /// ## Why is this bad? + /// Expressions such as `x[y]` and `x * y` call methods + /// under the hood (`__getitem__` and `__mul__` respectively). + /// Calling an unbound method will raise an `AttributeError` at runtime. + /// + /// ## Examples + /// ```python + /// import datetime + /// + /// class A: + /// if datetime.date.today().weekday() != 6: + /// def __getitem__(self, v): ... + /// + /// A()[0] # TypeError: 'A' object is not subscriptable + /// ``` + pub(crate) static POSSIBLY_UNBOUND_IMPLICIT_CALL = { + summary: "detects implicit calls to possibly unbound methods", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + +declare_lint! { + /// ## What it does + /// Checks whether an argument is used as both a value and a type form in a call. + /// + /// ## Why is this bad? + /// Such calls have confusing semantics and often indicate a logic error. + /// + /// ## Examples + /// ```python + /// from typing import reveal_type + /// from ty_extensions import is_singleton + /// + /// if flag: + /// f = repr # Expects a value + /// else: + /// f = is_singleton # Expects a type form + /// + /// f(int) # error + /// ``` + 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! { + /// ## What it does + /// Checks whether a variable has been declared as two conflicting types. + /// + /// ## Why is this bad + /// A variable with two conflicting declarations likely indicates a mistake. + /// Moreover, it could lead to incorrect or ill-defined type inference for + /// other code that relies on these variables. + /// + /// ## Examples + /// ```python + /// if b: + /// a: int + /// else: + /// a: str + /// + /// a = 1 + /// ``` + pub(crate) static CONFLICTING_DECLARATIONS = { + summary: "detects conflicting declarations", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for class definitions where the metaclass of the class + /// being created would not be a subclass of the metaclasses of + /// all the class's bases. + /// + /// ## Why is it bad? + /// Such a class definition raises a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// class M1(type): ... + /// class M2(type): ... + /// class A(metaclass=M1): ... + /// class B(metaclass=M2): ... + /// + /// # TypeError: metaclass conflict + /// class C(A, B): ... + /// ``` + pub(crate) static CONFLICTING_METACLASS = { + summary: "detects conflicting metaclasses", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for class definitions in stub files that inherit + /// (directly or indirectly) from themselves. + /// + /// ## Why is it bad? + /// Although forward references are natively supported in stub files, + /// inheritance cycles are still disallowed, as it is impossible to + /// resolve a consistent [method resolution order] for a class that + /// inherits from itself. + /// + /// ## Examples + /// ```python + /// # foo.pyi + /// class A(B): ... + /// class B(A): ... + /// ``` + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) static CYCLIC_CLASS_DEFINITION = { + summary: "detects cyclic class definitions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// It detects division by zero. + /// + /// ## Why is this bad? + /// Dividing by zero raises a `ZeroDivisionError` at runtime. + /// + /// ## Examples + /// ```python + /// 5 / 0 + /// ``` + pub(crate) static DIVISION_BY_ZERO = { + summary: "detects division by zero", + status: LintStatus::preview("1.0.0"), + default_level: Level::Ignore, + } +} + +declare_lint! { + /// ## What it does + /// Checks for class definitions with duplicate bases. + /// + /// ## Why is this bad? + /// Class definitions with duplicate bases raise `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// class A: ... + /// + /// # TypeError: duplicate base class + /// class B(A, A): ... + /// ``` + pub(crate) static DUPLICATE_BASE = { + summary: "detects class definitions with duplicate bases", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for dataclass definitions with more than one field + /// annotated with `KW_ONLY`. + /// + /// ## Why is this bad? + /// `dataclasses.KW_ONLY` is a special marker used to + /// emulate the `*` syntax in normal signatures. + /// It can only be used once per dataclass. + /// + /// Attempting to annotate two different fields with + /// it will lead to a runtime error. + /// + /// ## Examples + /// ```python + /// from dataclasses import dataclass, KW_ONLY + /// + /// @dataclass + /// class A: # Crash at runtime + /// b: int + /// _1: KW_ONLY + /// c: str + /// _2: KW_ONLY + /// d: bytes + /// ``` + pub(crate) static DUPLICATE_KW_ONLY = { + summary: "detects dataclass definitions with more than one usage of `KW_ONLY`", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for classes definitions which will fail at runtime due to + /// "instance memory layout conflicts". + /// + /// This error is usually caused by attempting to combine multiple classes + /// that define non-empty `__slots__` in a class's [Method Resolution Order] + /// (MRO), or by attempting to combine multiple builtin classes in a class's + /// MRO. + /// + /// ## Why is this bad? + /// Inheriting from bases with conflicting instance memory layouts + /// will lead to a `TypeError` at runtime. + /// + /// An instance memory layout conflict occurs when CPython cannot determine + /// the memory layout instances of a class should have, because the instance + /// memory layout of one of its bases conflicts with the instance memory layout + /// of one or more of its other bases. + /// + /// For example, if a Python class defines non-empty `__slots__`, this will + /// impact the memory layout of instances of that class. Multiple inheritance + /// from more than one different class defining non-empty `__slots__` is not + /// allowed: + /// + /// ```python + /// class A: + /// __slots__ = ("a", "b") + /// + /// class B: + /// __slots__ = ("a", "b") # Even if the values are the same + /// + /// # TypeError: multiple bases have instance lay-out conflict + /// class C(A, B): ... + /// ``` + /// + /// An instance layout conflict can also be caused by attempting to use + /// multiple inheritance with two builtin classes, due to the way that these + /// classes are implemented in a CPython C extension: + /// + /// ```python + /// class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict + /// ``` + /// + /// Note that pure-Python classes with no `__slots__`, or pure-Python classes + /// with empty `__slots__`, are always compatible: + /// + /// ```python + /// class A: ... + /// class B: + /// __slots__ = () + /// class C: + /// __slots__ = ("a", "b") + /// + /// # fine + /// class D(A, B, C): ... + /// ``` + /// + /// ## Known problems + /// Classes that have "dynamic" definitions of `__slots__` (definitions do not consist + /// of string literals, or tuples of string literals) are not currently considered solid + /// bases by ty. + /// + /// Additionally, this check is not exhaustive: many C extensions (including several in + /// the standard library) define classes that use extended memory layouts and thus cannot + /// coexist in a single MRO. Since it is currently not possible to represent this fact in + /// stub files, having a full knowledge of these classes is also impossible. When it comes + /// to classes that do not define `__slots__` at the Python level, therefore, ty, currently + /// only hard-codes a number of cases where it knows that a class will produce instances with + /// an atypical memory layout. + /// + /// ## Further reading + /// - [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) + /// - [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order) + /// + /// [Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) static INSTANCE_LAYOUT_CONFLICT = { + summary: "detects class definitions that raise `TypeError` due to instance layout conflict", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for invalidly defined protocol classes. + /// + /// ## Why is this bad? + /// An invalidly defined protocol class may lead to the type checker inferring + /// unexpected things. It may also lead to `TypeError`s at runtime. + /// + /// ## Examples + /// A `Protocol` class cannot inherit from a non-`Protocol` class; + /// this raises a `TypeError` at runtime: + /// + /// ```pycon + /// >>> from typing import Protocol + /// >>> class Foo(int, Protocol): ... + /// ... + /// Traceback (most recent call last): + /// File "", line 1, in + /// class Foo(int, Protocol): ... + /// TypeError: Protocols can only inherit from other protocols, got + /// ``` + pub(crate) static INVALID_PROTOCOL = { + summary: "detects invalid protocol class definitions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for classes with an inconsistent [method resolution order] (MRO). + /// + /// ## Why is this bad? + /// Classes with an inconsistent MRO will raise a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// class A: ... + /// class B(A): ... + /// + /// # TypeError: Cannot create a consistent method resolution order + /// class C(A, B): ... + /// ``` + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) static INCONSISTENT_MRO = { + summary: "detects class definitions with an inconsistent MRO", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for attempts to use an out of bounds index to get an item from + /// a container. + /// + /// ## Why is this bad? + /// Using an out of bounds index will raise an `IndexError` at runtime. + /// + /// ## Examples + /// ```python + /// t = (0, 1, 2) + /// t[3] # IndexError: tuple index out of range + /// ``` + pub(crate) static INDEX_OUT_OF_BOUNDS = { + summary: "detects index out of bounds errors", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Detects call arguments whose type is not assignable to the corresponding typed parameter. + /// + /// ## Why is this bad? + /// Passing an argument of a type the function (or callable object) does not accept violates + /// the expectations of the function author and may cause unexpected runtime errors within the + /// body of the function. + /// + /// ## Examples + /// ```python + /// def func(x: int): ... + /// func("foo") # error: [invalid-argument-type] + /// ``` + pub(crate) static INVALID_ARGUMENT_TYPE = { + summary: "detects call arguments whose type is not assignable to the corresponding typed parameter", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Detects returned values that can't be assigned to the function's annotated return type. + /// + /// ## Why is this bad? + /// Returning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function. + /// + /// ## Examples + /// ```python + /// def func() -> int: + /// return "a" # error: [invalid-return-type] + /// ``` + pub(crate) static INVALID_RETURN_TYPE = { + summary: "detects returned values that can't be assigned to the function's annotated return type", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for assignments where the type of the value + /// is not [assignable to] the type of the assignee. + /// + /// ## Why is this bad? + /// Such assignments break the rules of the type system and + /// weaken a type checker's ability to accurately reason about your code. + /// + /// ## Examples + /// ```python + /// a: int = '' + /// ``` + /// + /// [assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable + pub(crate) static INVALID_ASSIGNMENT = { + summary: "detects invalid assignments", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for class definitions that have bases which are not instances of `type`. + /// + /// ## Why is this bad? + /// Class definitions with bases like this will lead to `TypeError` being raised at runtime. + /// + /// ## Examples + /// ```python + /// class A(42): ... # error: [invalid-base] + /// ``` + pub(crate) static INVALID_BASE = { + summary: "detects class bases that will cause the class definition to raise an exception at runtime", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for class definitions that have bases which are unsupported by ty. + /// + /// ## Why is this bad? + /// If a class has a base that is an instance of a complex type such as a union type, + /// ty will not be able to resolve the [method resolution order] (MRO) for the class. + /// This will lead to an inferior understanding of your codebase and unpredictable + /// type-checking behavior. + /// + /// ## Examples + /// ```python + /// import datetime + /// + /// class A: ... + /// class B: ... + /// + /// if datetime.date.today().weekday() != 6: + /// C = A + /// else: + /// C = B + /// + /// class D(C): ... # error: [unsupported-base] + /// ``` + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(crate) static UNSUPPORTED_BASE = { + summary: "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + +declare_lint! { + /// ## What it does + /// Checks for expressions used in `with` statements + /// that do not implement the context manager protocol. + /// + /// ## Why is this bad? + /// Such a statement will raise `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// # TypeError: 'int' object does not support the context manager protocol + /// with 1: + /// print(2) + /// ``` + pub(crate) static INVALID_CONTEXT_MANAGER = { + summary: "detects expressions used in with statements that don't implement the context manager protocol", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for declarations where the inferred type of an existing symbol + /// is not [assignable to] its post-hoc declared type. + /// + /// ## Why is this bad? + /// Such declarations break the rules of the type system and + /// weaken a type checker's ability to accurately reason about your code. + /// + /// ## Examples + /// ```python + /// a = 1 + /// a: str + /// ``` + /// + /// [assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable + pub(crate) static INVALID_DECLARATION = { + summary: "detects invalid declarations", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for exception handlers that catch non-exception classes. + /// + /// ## Why is this bad? + /// Catching classes that do not inherit from `BaseException` will raise a TypeError at runtime. + /// + /// ## Example + /// ```python + /// try: + /// 1 / 0 + /// except 1: + /// ... + /// ``` + /// + /// Use instead: + /// ```python + /// try: + /// 1 / 0 + /// except ZeroDivisionError: + /// ... + /// ``` + /// + /// ## References + /// - [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) + /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) + /// + /// ## Ruff rule + /// This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes) + pub(crate) static INVALID_EXCEPTION_CAUGHT = { + summary: "detects exception handlers that catch classes that do not inherit from `BaseException`", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for the creation of invalid generic classes + /// + /// ## Why is this bad? + /// There are several requirements that you must follow when defining a generic class. + /// + /// ## Examples + /// ```python + /// from typing import Generic, TypeVar + /// + /// T = TypeVar("T") # okay + /// + /// # error: class uses both PEP-695 syntax and legacy syntax + /// class C[U](Generic[T]): ... + /// ``` + /// + /// ## References + /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + pub(crate) static INVALID_GENERIC_CLASS = { + summary: "detects invalid generic classes", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for the creation of invalid legacy `TypeVar`s + /// + /// ## Why is this bad? + /// There are several requirements that you must follow when creating a legacy `TypeVar`. + /// + /// ## Examples + /// ```python + /// from typing import TypeVar + /// + /// T = TypeVar("T") # okay + /// Q = TypeVar("S") # error: TypeVar name must match the variable it's assigned to + /// T = TypeVar("T") # error: TypeVars should not be redefined + /// + /// # error: TypeVar must be immediately assigned to a variable + /// def f(t: TypeVar("U")): ... + /// ``` + /// + /// ## References + /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + pub(crate) static INVALID_LEGACY_TYPE_VARIABLE = { + summary: "detects invalid legacy type variables", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for the creation of invalid `TypeAliasType`s + /// + /// ## Why is this bad? + /// There are several requirements that you must follow when creating a `TypeAliasType`. + /// + /// ## Examples + /// ```python + /// from typing import TypeAliasType + /// + /// IntOrStr = TypeAliasType("IntOrStr", int | str) # okay + /// NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal + /// ``` + pub(crate) static INVALID_TYPE_ALIAS_TYPE = { + summary: "detects invalid TypeAliasType definitions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for arguments to `metaclass=` that are invalid. + /// + /// ## Why is this bad? + /// Python allows arbitrary expressions to be used as the argument to `metaclass=`. + /// These expressions, however, need to be callable and accept the same arguments + /// as `type.__new__`. + /// + /// ## Example + /// + /// ```python + /// def f(): ... + /// + /// # TypeError: f() takes 0 positional arguments but 3 were given + /// class B(metaclass=f): ... + /// ``` + /// + /// ## References + /// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses) + pub(crate) static INVALID_METACLASS = { + summary: "detects invalid `metaclass=` arguments", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for various invalid `@overload` usages. + /// + /// ## Why is this bad? + /// The `@overload` decorator is used to define functions and methods that accepts different + /// combinations of arguments and return different types based on the arguments passed. This is + /// mainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type + /// checker may not be able to provide correct type information. + /// + /// ## Example + /// + /// Defining only one overload: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo(x: int) -> int: ... + /// def foo(x: int | None) -> int | None: + /// return x + /// ``` + /// + /// Or, not providing an implementation for the overloaded definition: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo() -> None: ... + /// @overload + /// def foo(x: int) -> int: ... + /// ``` + /// + /// ## References + /// - [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload) + pub(crate) static INVALID_OVERLOAD = { + summary: "detects invalid `@overload` usages", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for default values that can't be + /// assigned to the parameter's annotated type. + /// + /// ## Why is this bad? + /// This breaks the rules of the type system and + /// weakens a type checker's ability to accurately reason about your code. + /// + /// ## Examples + /// ```python + /// def f(a: int = ''): ... + /// ``` + pub(crate) static INVALID_PARAMETER_DEFAULT = { + summary: "detects default values that can't be assigned to the parameter's annotated type", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// Checks for `raise` statements that raise non-exceptions or use invalid + /// causes for their raised exceptions. + /// + /// ## Why is this bad? + /// Only subclasses or instances of `BaseException` can be raised. + /// For an exception's cause, the same rules apply, except that `None` is also + /// permitted. Violating these rules results in a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// def f(): + /// try: + /// something() + /// except NameError: + /// raise "oops!" from f + /// + /// def g(): + /// raise NotImplemented from 42 + /// ``` + /// + /// Use instead: + /// ```python + /// def f(): + /// try: + /// something() + /// except NameError as e: + /// raise RuntimeError("oops!") from e + /// + /// def g(): + /// raise NotImplementedError from None + /// ``` + /// + /// ## References + /// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise) + /// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) + pub(crate) static INVALID_RAISE = { + summary: "detects `raise` statements that raise invalid exceptions or use invalid causes", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Detects `super()` calls where: + /// - the first argument is not a valid class literal, or + /// - the second argument is not an instance or subclass of the first argument. + /// + /// ## Why is this bad? + /// `super(type, obj)` expects: + /// - the first argument to be a class, + /// - and the second argument to satisfy one of the following: + /// - `isinstance(obj, type)` is `True` + /// - `issubclass(obj, type)` is `True` + /// + /// Violating this relationship will raise a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// class A: + /// ... + /// class B(A): + /// ... + /// + /// super(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)` + /// + /// super(A(), B()) # error: `A()` is not a class + /// + /// super(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)` + /// super(B, A) # error: `A` does not satisfy `issubclass(A, B)` + /// ``` + /// + /// ## References + /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) + pub(crate) static INVALID_SUPER_ARGUMENT = { + summary: "detects invalid arguments for `super()`", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an + /// annotation not assignable from `bool`. + /// + /// ## Why is this bad? + /// The name `TYPE_CHECKING` is reserved for a flag that can be used to provide conditional + /// code seen only by the type checker, and not at runtime. Normally this flag is imported from + /// `typing` or `typing_extensions`, but it can also be defined locally. If defined locally, it + /// must be assigned the value `False` at runtime; the type checker will consider its value to + /// be `True`. If annotated, it must be annotated as a type that can accept `bool` values. + /// + /// ## Examples + /// ```python + /// TYPE_CHECKING: str + /// TYPE_CHECKING = '' + /// ``` + pub(crate) static INVALID_TYPE_CHECKING_CONSTANT = { + summary: "detects invalid `TYPE_CHECKING` constant assignments", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for expressions that are used as [type expressions] + /// but cannot validly be interpreted as such. + /// + /// ## Why is this bad? + /// Such expressions cannot be understood by ty. + /// In some cases, they might raise errors at runtime. + /// + /// ## Examples + /// ```python + /// from typing import Annotated + /// + /// a: type[1] # `1` is not a type + /// b: Annotated[int] # `Annotated` expects at least two arguments + /// ``` + /// [type expressions]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions + pub(crate) static INVALID_TYPE_FORM = { + summary: "detects invalid type forms", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for type guard functions without + /// a first non-self-like non-keyword-only non-variadic parameter. + /// + /// ## Why is this bad? + /// Type narrowing functions must accept at least one positional argument + /// (non-static methods must accept another in addition to `self`/`cls`). + /// + /// Extra parameters/arguments are allowed but do not affect narrowing. + /// + /// ## Examples + /// ```python + /// from typing import TypeIs + /// + /// def f() -> TypeIs[int]: ... # Error, no parameter + /// def f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed + /// def f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments + /// class C: + /// def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self` + /// ``` + pub(crate) static INVALID_TYPE_GUARD_DEFINITION = { + summary: "detects malformed type guard functions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for type guard function calls without a valid target. + /// + /// ## Why is this bad? + /// The first non-keyword non-variadic argument to a type guard function + /// is its target and must map to a symbol. + /// + /// Starred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like + /// expressions are invalid as narrowing targets. + /// + /// ## Examples + /// ```python + /// from typing import TypeIs + /// + /// def f(v: object) -> TypeIs[int]: ... + /// + /// f() # Error + /// f(*a) # Error + /// f(10) # Error + /// ``` + pub(crate) static INVALID_TYPE_GUARD_CALL = { + summary: "detects type guard function calls that has no narrowing effect", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for constrained [type variables] with only one constraint. + /// + /// ## Why is this bad? + /// A constrained type variable must have at least two constraints. + /// + /// ## Examples + /// ```python + /// from typing import TypeVar + /// + /// T = TypeVar('T', str) # invalid constrained TypeVar + /// ``` + /// + /// Use instead: + /// ```python + /// T = TypeVar('T', str, int) # valid constrained TypeVar + /// # or + /// T = TypeVar('T', bound=str) # valid bound TypeVar + /// ``` + /// + /// [type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar + pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = { + summary: "detects invalid type variable constraints", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for missing required arguments in a call. + /// + /// ## Why is this bad? + /// Failing to provide a required argument will raise a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// def func(x: int): ... + /// func() # TypeError: func() missing 1 required positional argument: 'x' + /// ``` + pub(crate) static MISSING_ARGUMENT = { + summary: "detects missing required arguments in a call", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for calls to an overloaded function that do not match any of the overloads. + /// + /// ## Why is this bad? + /// Failing to provide the correct arguments to one of the overloads will raise a `TypeError` + /// at runtime. + /// + /// ## Examples + /// ```python + /// @overload + /// def func(x: int): ... + /// @overload + /// def func(x: bool): ... + /// func("string") # error: [no-matching-overload] + /// ``` + pub(crate) static NO_MATCHING_OVERLOAD = { + summary: "detects calls that do not match any overload", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for subscripting objects that do not support subscripting. + /// + /// ## Why is this bad? + /// Subscripting an object that does not support it will raise a `TypeError` at runtime. + /// + /// ## Examples + /// ```python + /// 4[1] # TypeError: 'int' object is not subscriptable + /// ``` + pub(crate) static NON_SUBSCRIPTABLE = { + summary: "detects subscripting objects that do not support subscripting", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for objects that are not iterable but are used in a context that requires them to be. + /// + /// ## Why is this bad? + /// Iterating over an object that is not iterable will raise a `TypeError` at runtime. + /// + /// ## Examples + /// + /// ```python + /// for i in 34: # TypeError: 'int' object is not iterable + /// pass + /// ``` + pub(crate) static NOT_ITERABLE = { + summary: "detects iteration over an object that is not iterable", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for bool conversions where the object doesn't correctly implement `__bool__`. + /// + /// ## Why is this bad? + /// If an exception is raised when you attempt to evaluate the truthiness of an object, + /// using the object in a boolean context will fail at runtime. + /// + /// ## Examples + /// + /// ```python + /// class NotBoolable: + /// __bool__ = None + /// + /// b1 = NotBoolable() + /// b2 = NotBoolable() + /// + /// if b1: # exception raised here + /// pass + /// + /// b1 and b2 # exception raised here + /// not b1 # exception raised here + /// b1 < b2 < b1 # exception raised here + /// ``` + pub(crate) static UNSUPPORTED_BOOL_CONVERSION = { + summary: "detects boolean conversion where the object incorrectly implements `__bool__`", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for calls which provide more than one argument for a single parameter. + /// + /// ## Why is this bad? + /// Providing multiple values for a single parameter will raise a `TypeError` at runtime. + /// + /// ## Examples + /// + /// ```python + /// def f(x: int) -> int: ... + /// + /// f(1, x=2) # Error raised here + /// ``` + pub(crate) static PARAMETER_ALREADY_ASSIGNED = { + summary: "detects multiple arguments for the same parameter", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for possibly unbound attributes. + /// + /// ## Why is this bad? + /// Attempting to access an unbound attribute will raise an `AttributeError` at runtime. + /// + /// ## Examples + /// ```python + /// class A: + /// if b: + /// c = 0 + /// + /// A.c # AttributeError: type object 'A' has no attribute 'c' + /// ``` + pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = { + summary: "detects references to possibly unbound attributes", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + +declare_lint! { + /// ## What it does + /// Checks for imports of symbols that may be unbound. + /// + /// ## Why is this bad? + /// Importing an unbound module or name will raise a `ModuleNotFoundError` + /// or `ImportError` at runtime. + /// + /// ## Examples + /// ```python + /// # module.py + /// import datetime + /// + /// if datetime.date.today().weekday() != 6: + /// a = 1 + /// + /// # main.py + /// from module import a # ImportError: cannot import name 'a' from 'module' + /// ``` + pub(crate) static POSSIBLY_UNBOUND_IMPORT = { + summary: "detects possibly unbound imports", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + +declare_lint! { + /// ## What it does + /// Checks for references to names that are possibly not defined. + /// + /// ## Why is this bad? + /// Using an undefined variable will raise a `NameError` at runtime. + /// + /// ## Example + /// + /// ```python + /// for i in range(0): + /// x = i + /// + /// print(x) # NameError: name 'x' is not defined + /// ``` + pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = { + summary: "detects references to possibly undefined names", + status: LintStatus::preview("1.0.0"), + default_level: Level::Ignore, + } +} + +declare_lint! { + /// ## What it does + /// Checks for classes that subclass final classes. + /// + /// ## Why is this bad? + /// Decorating a class with `@final` declares to the type checker that it should not be subclassed. + /// + /// ## Example + /// + /// ```python + /// from typing import final + /// + /// @final + /// class A: ... + /// class B(A): ... # Error raised here + /// ``` + pub(crate) static SUBCLASS_OF_FINAL_CLASS = { + summary: "detects subclasses of final classes", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for `assert_type()` and `assert_never()` calls where the actual type + /// is not the same as the asserted type. + /// + /// ## Why is this bad? + /// `assert_type()` allows confirming the inferred type of a certain value. + /// + /// ## Example + /// + /// ```python + /// def _(x: int): + /// assert_type(x, int) # fine + /// assert_type(x, str) # error: Actual type does not match asserted type + /// ``` + pub(crate) static TYPE_ASSERTION_FAILURE = { + summary: "detects failed type assertions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for calls that pass more positional arguments than the callable can accept. + /// + /// ## Why is this bad? + /// Passing too many positional arguments will raise `TypeError` at runtime. + /// + /// ## Example + /// + /// ```python + /// def f(): ... + /// + /// f("foo") # Error raised here + /// ``` + pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = { + summary: "detects calls passing too many positional arguments", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Detects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable. + /// + /// ## Why is this bad? + /// When `super()` is used without arguments, Python tries to find two things: + /// the nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls). + /// If either of these is missing, the call will fail at runtime with a `RuntimeError`. + /// + /// ## Examples + /// ```python + /// super() # error: no enclosing class or function found + /// + /// def func(): + /// super() # error: no enclosing class or first argument exists + /// + /// class A: + /// f = super() # error: no enclosing function to provide the first argument + /// + /// def method(self): + /// def nested(): + /// super() # error: first argument does not exist in this nested function + /// + /// lambda: super() # error: first argument does not exist in this lambda + /// + /// (super() for _ in range(10)) # error: argument is not available in generator expression + /// + /// super() # okay! both enclosing class and first argument are available + /// ``` + /// + /// ## References + /// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super) + pub(crate) static UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS = { + summary: "detects invalid `super()` calls where implicit arguments are unavailable.", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for calls to `reveal_type` without importing it. + /// + /// ## Why is this bad? + /// Using `reveal_type` without importing it will raise a `NameError` at runtime. + /// + /// ## Examples + /// ```python + /// reveal_type(1) # NameError: name 'reveal_type' is not defined + /// ``` + pub(crate) static UNDEFINED_REVEAL = { + summary: "detects usages of `reveal_type` without importing it", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + +declare_lint! { + /// ## What it does + /// Checks for keyword arguments in calls that don't match any parameter of the callable. + /// + /// ## Why is this bad? + /// Providing an unknown keyword argument will raise `TypeError` at runtime. + /// + /// ## Example + /// + /// ```python + /// def f(x: int) -> int: ... + /// + /// f(x=1, y=2) # Error raised here + /// ``` + pub(crate) static UNKNOWN_ARGUMENT = { + summary: "detects unknown keyword arguments in calls", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for unresolved attributes. + /// + /// ## Why is this bad? + /// Accessing an unbound attribute will raise an `AttributeError` at runtime. + /// An unresolved attribute is not guaranteed to exist from the type alone, + /// so this could also indicate that the object is not of the type that the user expects. + /// + /// ## Examples + /// ```python + /// class A: ... + /// + /// A().foo # AttributeError: 'A' object has no attribute 'foo' + /// ``` + pub(crate) static UNRESOLVED_ATTRIBUTE = { + summary: "detects references to unresolved attributes", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for import statements for which the module cannot be resolved. + /// + /// ## Why is this bad? + /// Importing a module that cannot be resolved will raise a `ModuleNotFoundError` + /// at runtime. + /// + /// ## Examples + /// ```python + /// import foo # ModuleNotFoundError: No module named 'foo' + /// ``` + pub(crate) static UNRESOLVED_IMPORT = { + summary: "detects unresolved imports", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for references to names that are not defined. + /// + /// ## Why is this bad? + /// Using an undefined variable will raise a `NameError` at runtime. + /// + /// ## Example + /// + /// ```python + /// print(x) # NameError: name 'x' is not defined + /// ``` + pub(crate) static UNRESOLVED_REFERENCE = { + summary: "detects references to names that are not defined", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for binary expressions, comparisons, and unary expressions where + /// the operands don't support the operator. + /// + /// ## Why is this bad? + /// Attempting to use an unsupported operator will raise a `TypeError` at + /// runtime. + /// + /// ## Examples + /// ```python + /// class A: ... + /// + /// A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' + /// ``` + pub(crate) static UNSUPPORTED_OPERATOR = { + summary: "detects binary, unary, or comparison expressions where the operands don't support the operator", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for step size 0 in slices. + /// + /// ## Why is this bad? + /// A slice with a step size of zero will raise a `ValueError` at runtime. + /// + /// ## Examples + /// ```python + /// l = list(range(10)) + /// l[1:10:0] # ValueError: slice step cannot be zero + /// ``` + pub(crate) static ZERO_STEPSIZE_IN_SLICE = { + summary: "detects a slice step size of zero", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Makes sure that the argument of `static_assert` is statically known to be true. + /// + /// ## Why is this bad? + /// A `static_assert` call represents an explicit request from the user + /// for the type checker to emit an error if the argument cannot be verified + /// to evaluate to `True` in a boolean context. + /// + /// ## Examples + /// ```python + /// from ty_extensions import static_assert + /// + /// static_assert(1 + 1 == 3) # error: evaluates to `False` + /// + /// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness + /// ``` + pub(crate) static STATIC_ASSERT_ERROR = { + summary: "Failed static assertion", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for assignments to class variables from instances + /// and assignments to instance variables from its class. + /// + /// ## Why is this bad? + /// Incorrect assignments break the rules of the type system and + /// weaken a type checker's ability to accurately reason about your code. + /// + /// ## Examples + /// ```python + /// class C: + /// class_var: ClassVar[int] = 1 + /// instance_var: int + /// + /// C.class_var = 3 # okay + /// C().class_var = 3 # error: Cannot assign to class variable + /// + /// C().instance_var = 3 # okay + /// C.instance_var = 3 # error: Cannot assign to instance variable + /// ``` + pub(crate) static INVALID_ATTRIBUTE_ACCESS = { + summary: "Invalid attribute access", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Detects redundant `cast` calls where the value already has the target type. + /// + /// ## Why is this bad? + /// These casts have no effect and can be removed. + /// + /// ## Example + /// ```python + /// def f() -> int: + /// return 10 + /// + /// cast(int, f()) # Redundant + /// ``` + pub(crate) static REDUNDANT_CAST = { + summary: "detects redundant `cast` calls", + status: LintStatus::preview("1.0.0"), + default_level: Level::Warn, + } +} + +/// A collection of type check diagnostics. +#[derive(Default, Eq, PartialEq, get_size2::GetSize)] +pub struct TypeCheckDiagnostics { + diagnostics: Vec, + used_suppressions: FxHashSet, +} + +impl TypeCheckDiagnostics { + pub(crate) fn push(&mut self, diagnostic: Diagnostic) { + self.diagnostics.push(diagnostic); + } + + pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) { + self.diagnostics.extend_from_slice(&other.diagnostics); + self.used_suppressions.extend(&other.used_suppressions); + } + + pub(super) fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator) { + self.diagnostics.extend(diagnostics); + } + + pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) { + self.used_suppressions.insert(suppression_id); + } + + pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool { + self.used_suppressions.contains(&suppression_id) + } + + pub(crate) fn used_len(&self) -> usize { + self.used_suppressions.len() + } + + pub(crate) fn shrink_to_fit(&mut self) { + self.used_suppressions.shrink_to_fit(); + self.diagnostics.shrink_to_fit(); + } + + pub(crate) fn into_vec(self) -> Vec { + self.diagnostics + } + + pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> { + self.diagnostics.iter() + } +} + +impl std::fmt::Debug for TypeCheckDiagnostics { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.diagnostics.fmt(f) + } +} + +impl IntoIterator for TypeCheckDiagnostics { + type Item = Diagnostic; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.diagnostics.into_iter() + } +} + +impl<'a> IntoIterator for &'a TypeCheckDiagnostics { + type Item = &'a Diagnostic; + type IntoIter = std::slice::Iter<'a, Diagnostic>; + + fn into_iter(self) -> Self::IntoIter { + self.diagnostics.iter() + } +} + +/// Emit a diagnostic declaring that an index is out of bounds for a tuple. +pub(super) fn report_index_out_of_bounds( + context: &InferContext, + kind: &'static str, + node: AnyNodeRef, + tuple_ty: Type, + length: impl std::fmt::Display, + index: i64, +) { + let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Index {index} is out of bounds for {kind} `{}` with length {length}", + tuple_ty.display(context.db()) + )); +} + +/// Emit a diagnostic declaring that a type does not support subscripting. +pub(super) fn report_non_subscriptable( + context: &InferContext, + node: AnyNodeRef, + non_subscriptable_ty: Type, + method: &str, +) { + let Some(builder) = context.report_lint(&NON_SUBSCRIPTABLE, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot subscript object of type `{}` with no `{method}` method", + non_subscriptable_ty.display(context.db()) + )); +} + +pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) { + let Some(builder) = context.report_lint(&ZERO_STEPSIZE_IN_SLICE, node) else { + return; + }; + builder.into_diagnostic("Slice step size can not be zero"); +} + +fn report_invalid_assignment_with_message( + context: &InferContext, + node: AnyNodeRef, + target_ty: Type, + message: std::fmt::Arguments, +) { + let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, node) else { + return; + }; + match target_ty { + Type::ClassLiteral(class) => { + let mut diag = builder.into_diagnostic(format_args!( + "Implicit shadowing of class `{}`", + class.name(context.db()), + )); + diag.info("Annotate to make it explicit if this is intentional"); + } + Type::FunctionLiteral(function) => { + let mut diag = builder.into_diagnostic(format_args!( + "Implicit shadowing of function `{}`", + function.name(context.db()), + )); + diag.info("Annotate to make it explicit if this is intentional"); + } + _ => { + builder.into_diagnostic(message); + } + } +} + +pub(super) fn report_invalid_assignment( + context: &InferContext, + node: AnyNodeRef, + target_ty: Type, + source_ty: Type, +) { + report_invalid_assignment_with_message( + context, + node, + target_ty, + format_args!( + "Object of type `{}` is not assignable to `{}`", + source_ty.display(context.db()), + target_ty.display(context.db()), + ), + ); +} + +pub(super) fn report_invalid_attribute_assignment( + context: &InferContext, + node: AnyNodeRef, + target_ty: Type, + source_ty: Type, + attribute_name: &'_ str, +) { + report_invalid_assignment_with_message( + context, + node, + target_ty, + format_args!( + "Object of type `{}` is not assignable to attribute `{attribute_name}` of type `{}`", + source_ty.display(context.db()), + target_ty.display(context.db()), + ), + ); +} + +pub(super) fn report_invalid_return_type( + context: &InferContext, + object_range: impl Ranged, + return_type_range: impl Ranged, + expected_ty: Type, + actual_ty: Type, +) { + let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, object_range) else { + return; + }; + + let return_type_span = context.span(return_type_range); + + let mut diag = builder.into_diagnostic("Return type does not match returned value"); + diag.set_primary_message(format_args!( + "expected `{expected_ty}`, found `{actual_ty}`", + expected_ty = expected_ty.display(context.db()), + actual_ty = actual_ty.display(context.db()), + )); + diag.annotate( + Annotation::secondary(return_type_span).message(format_args!( + "Expected `{expected_ty}` because of return type", + expected_ty = expected_ty.display(context.db()), + )), + ); +} + +pub(super) fn report_invalid_generator_function_return_type( + context: &InferContext, + return_type_range: TextRange, + inferred_return: KnownClass, + expected_ty: Type, +) { + let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, return_type_range) else { + return; + }; + + let mut diag = builder.into_diagnostic("Return type does not match returned value"); + let inferred_ty = inferred_return.display(context.db()); + diag.set_primary_message(format_args!( + "expected `{expected_ty}`, found `{inferred_ty}`", + expected_ty = expected_ty.display(context.db()), + )); + + let (description, link) = if inferred_return == KnownClass::AsyncGeneratorType { + ( + "an async generator function", + "https://docs.python.org/3/glossary.html#term-asynchronous-generator", + ) + } else { + ( + "a generator function", + "https://docs.python.org/3/glossary.html#term-generator", + ) + }; + + diag.info(format_args!( + "Function is inferred as returning `{inferred_ty}` because it is {description}" + )); + diag.info(format_args!("See {link} for more details")); +} + +pub(super) fn report_implicit_return_type( + context: &InferContext, + range: impl Ranged, + expected_ty: Type, + has_empty_body: bool, + enclosing_class_of_method: Option, + no_return: bool, +) { + let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, range) else { + return; + }; + let db = context.db(); + + // If no return statement is defined in the function, then the function always returns `None` + let mut diagnostic = if no_return { + let mut diag = builder.into_diagnostic(format_args!( + "Function always implicitly returns `None`, which is not assignable to return type `{}`", + expected_ty.display(db), + )); + diag.info( + "Consider changing the return annotation to `-> None` or adding a `return` statement", + ); + diag + } else { + builder.into_diagnostic(format_args!( + "Function can implicitly return `None`, which is not assignable to return type `{}`", + expected_ty.display(db), + )) + }; + if !has_empty_body { + return; + } + diagnostic.info( + "Only functions in stub files, methods on protocol classes, \ + or methods with `@abstractmethod` are permitted to have empty bodies", + ); + let Some(class) = enclosing_class_of_method else { + return; + }; + if class.iter_mro(db, None).contains(&ClassBase::Protocol) { + diagnostic.info(format_args!( + "Class `{}` has `typing.Protocol` in its MRO, but it is not a protocol class", + class.name(db) + )); + + let mut sub_diagnostic = SubDiagnostic::new( + Severity::Info, + "Only classes that directly inherit from `typing.Protocol` \ + or `typing_extensions.Protocol` are considered protocol classes", + ); + sub_diagnostic.annotate( + Annotation::primary(class.header_span(db)).message(format_args!( + "`Protocol` not present in `{class}`'s immediate bases", + class = class.name(db) + )), + ); + diagnostic.sub(sub_diagnostic); + + diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#"); + } +} + +pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) { + let Some(builder) = context.report_lint(&INVALID_TYPE_CHECKING_CONSTANT, node) else { + return; + }; + builder.into_diagnostic( + "The name TYPE_CHECKING is reserved for use as a flag; only False can be assigned to it", + ); +} + +pub(super) fn report_possibly_unresolved_reference( + context: &InferContext, + expr_name_node: &ast::ExprName, +) { + let Some(builder) = context.report_lint(&POSSIBLY_UNRESOLVED_REFERENCE, expr_name_node) else { + return; + }; + + let ast::ExprName { id, .. } = expr_name_node; + builder.into_diagnostic(format_args!("Name `{id}` used when possibly not defined")); +} + +pub(super) fn report_possibly_unbound_attribute( + context: &InferContext, + target: &ast::ExprAttribute, + attribute: &str, + object_ty: Type, +) { + let Some(builder) = context.report_lint(&POSSIBLY_UNBOUND_ATTRIBUTE, target) else { + return; + }; + builder.into_diagnostic(format_args!( + "Attribute `{attribute}` on type `{}` is possibly unbound", + object_ty.display(context.db()), + )); +} + +pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { + let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot catch object of type `{}` in an exception handler \ + (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", + ty.display(context.db()) + )); +} + +pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) { + let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)", + ty.display(context.db()) + )); +} + +pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { + let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot use object of type `{}` as exception cause \ + (must be a `BaseException` subclass or instance or `None`)", + ty.display(context.db()) + )); +} + +pub(crate) fn report_instance_layout_conflict( + context: &InferContext, + class: ClassLiteral, + node: &ast::StmtClassDef, + solid_bases: &IncompatibleBases, +) { + debug_assert!(solid_bases.len() > 1); + + let db = context.db(); + + let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, class.header_range(db)) + else { + return; + }; + + let mut diagnostic = builder + .into_diagnostic("Class will raise `TypeError` at runtime due to incompatible bases"); + + diagnostic.set_primary_message(format_args!( + "Bases {} cannot be combined in multiple inheritance", + solid_bases.describe_problematic_class_bases(db) + )); + + let mut subdiagnostic = SubDiagnostic::new( + Severity::Info, + "Two classes cannot coexist in a class's MRO if their instances \ + have incompatible memory layouts", + ); + + for (solid_base, solid_base_info) in solid_bases { + let IncompatibleBaseInfo { + node_index, + originating_base, + } = solid_base_info; + + let span = context.span(&node.bases()[*node_index]); + let mut annotation = Annotation::secondary(span.clone()); + if solid_base.class == *originating_base { + match solid_base.kind { + SolidBaseKind::DefinesSlots => { + annotation = annotation.message(format_args!( + "`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`", + base = originating_base.name(db) + )); + } + SolidBaseKind::HardCoded => { + annotation = annotation.message(format_args!( + "`{base}` instances have a distinct memory layout because of the way `{base}` \ + is implemented in a C extension", + base = originating_base.name(db) + )); + } + } + subdiagnostic.annotate(annotation); + } else { + annotation = annotation.message(format_args!( + "`{base}` instances have a distinct memory layout \ + because `{base}` inherits from `{solid_base}`", + base = originating_base.name(db), + solid_base = solid_base.class.name(db) + )); + subdiagnostic.annotate(annotation); + + let mut additional_annotation = Annotation::secondary(span); + + additional_annotation = match solid_base.kind { + SolidBaseKind::DefinesSlots => additional_annotation.message(format_args!( + "`{solid_base}` instances have a distinct memory layout because `{solid_base}` \ + defines non-empty `__slots__`", + solid_base = solid_base.class.name(db), + )), + + SolidBaseKind::HardCoded => additional_annotation.message(format_args!( + "`{solid_base}` instances have a distinct memory layout \ + because of the way `{solid_base}` is implemented in a C extension", + solid_base = solid_base.class.name(db), + )), + }; + + subdiagnostic.annotate(additional_annotation); + } + } + + diagnostic.sub(subdiagnostic); +} + +/// Information regarding the conflicting solid bases a class is inferred to have in its MRO. +/// +/// For each solid base, we record information about which element in the class's bases list +/// caused the solid base to be included in the class's MRO. +/// +/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting solid bases +/// are reported in a stable order. +#[derive(Debug, Default)] +pub(super) struct IncompatibleBases<'db>(FxIndexMap, IncompatibleBaseInfo<'db>>); + +impl<'db> IncompatibleBases<'db> { + pub(super) fn insert( + &mut self, + base: SolidBase<'db>, + node_index: usize, + class: ClassLiteral<'db>, + ) { + let info = IncompatibleBaseInfo { + node_index, + originating_base: class, + }; + self.0.insert(base, info); + } + + /// List the problematic class bases in a human-readable format. + fn describe_problematic_class_bases(&self, db: &dyn Db) -> String { + let bad_base_names = self.0.values().map(|info| info.originating_base.name(db)); + + format_enumeration(bad_base_names) + } + + pub(super) fn len(&self) -> usize { + self.0.len() + } + + /// Two solid bases are allowed to coexist in an MRO if one is a subclass of the other. + /// This method therefore removes any entry in `self` that is a subclass of one or more + /// other entries also contained in `self`. + pub(super) fn remove_redundant_entries(&mut self, db: &'db dyn Db) { + self.0 = self + .0 + .iter() + .filter(|(solid_base, _)| { + self.0 + .keys() + .filter(|other_base| other_base != solid_base) + .all(|other_base| { + !solid_base.class.is_subclass_of( + db, + None, + other_base.class.default_specialization(db), + ) + }) + }) + .map(|(base, info)| (*base, *info)) + .collect(); + } +} + +impl<'a, 'db> IntoIterator for &'a IncompatibleBases<'db> { + type Item = (&'a SolidBase<'db>, &'a IncompatibleBaseInfo<'db>); + type IntoIter = indexmap::map::Iter<'a, SolidBase<'db>, IncompatibleBaseInfo<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +/// Information about which class base the "solid base" stems from +#[derive(Debug, Copy, Clone)] +pub(super) struct IncompatibleBaseInfo<'db> { + /// The index of the problematic base in the [`ast::StmtClassDef`]'s bases list. + node_index: usize, + + /// The base class in the [`ast::StmtClassDef`]'s bases list that caused + /// the solid base to be included in the class's MRO. + /// + /// This won't necessarily be the same class as the `SolidBase`'s class, + /// as the `SolidBase` may have found its way into the class's MRO by dint of it being a + /// superclass of one of the classes in the class definition's bases list. + originating_base: ClassLiteral<'db>, +} + +pub(crate) fn report_invalid_arguments_to_annotated( + context: &InferContext, + subscript: &ast::ExprSubscript, +) { + let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else { + return; + }; + builder.into_diagnostic(format_args!( + "Special form `{}` expected at least 2 arguments \ + (one type and at least one metadata element)", + SpecialFormType::Annotated + )); +} + +pub(crate) fn report_invalid_argument_number_to_special_form( + context: &InferContext, + subscript: &ast::ExprSubscript, + special_form: SpecialFormType, + received_arguments: usize, + expected_arguments: u8, +) { + let noun = if expected_arguments == 1 { + "type argument" + } else { + "type arguments" + }; + if let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "Special form `{special_form}` expected exactly {expected_arguments} {noun}, \ + got {received_arguments}", + )); + } +} + +pub(crate) fn report_bad_argument_to_get_protocol_members( + context: &InferContext, + call: &ast::ExprCall, + class: ClassLiteral, +) { + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { + return; + }; + let db = context.db(); + let mut diagnostic = builder.into_diagnostic("Invalid argument to `get_protocol_members`"); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic.info("Only protocol classes can be passed to `get_protocol_members`"); + + let mut class_def_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!( + "`{}` is declared here, but it is not a protocol class:", + class.name(db) + ), + ); + class_def_diagnostic.annotate(Annotation::primary(class.header_span(db))); + diagnostic.sub(class_def_diagnostic); + + diagnostic.info( + "A class is only a protocol class if it directly inherits \ + from `typing.Protocol` or `typing_extensions.Protocol`", + ); + // TODO the typing spec isn't really designed as user-facing documentation, + // but there isn't really any user-facing documentation that covers this specific issue well + // (it's not described well in the CPython docs; and PEP-544 is a snapshot of a decision taken + // years ago rather than up-to-date documentation). We should either write our own docs + // describing this well or contribute to type-checker-agnostic docs somewhere and link to those. + diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#"); +} + +pub(crate) fn report_invalid_arguments_to_callable( + context: &InferContext, + subscript: &ast::ExprSubscript, +) { + let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else { + return; + }; + builder.into_diagnostic(format_args!( + "Special form `{}` expected exactly two arguments (parameter types and return type)", + SpecialFormType::Callable + )); +} + +pub(crate) fn add_type_expression_reference_link<'db, 'ctx>( + mut diag: LintDiagnosticGuard<'db, 'ctx>, +) -> LintDiagnosticGuard<'db, 'ctx> { + diag.info("See the following page for a reference on valid type expressions:"); + diag.info( + "https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions", + ); + diag +} + +pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( + context: &InferContext, + call: &ast::ExprCall, + protocol: ProtocolClassLiteral, + function: KnownFunction, +) { + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { + return; + }; + let db = context.db(); + let class_name = protocol.name(db); + let function_name: &'static str = function.into(); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Class `{class_name}` cannot be used as the second argument to `{function_name}`", + )); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + + let mut class_def_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!( + "`{class_name}` is declared as a protocol class, \ + but it is not declared as runtime-checkable" + ), + ); + class_def_diagnostic.annotate( + Annotation::primary(protocol.header_span(db)) + .message(format_args!("`{class_name}` declared here")), + ); + diagnostic.sub(class_def_diagnostic); + + diagnostic.info(format_args!( + "A protocol class can only be used in `{function_name}` checks if it is decorated \ + with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable`" + )); + diagnostic.info("See https://docs.python.org/3/library/typing.html#typing.runtime_checkable"); +} + +pub(crate) fn report_attempted_protocol_instantiation( + context: &InferContext, + call: &ast::ExprCall, + protocol: ProtocolClassLiteral, +) { + let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, call) else { + return; + }; + let db = context.db(); + let class_name = protocol.name(db); + let mut diagnostic = + builder.into_diagnostic(format_args!("Cannot instantiate class `{class_name}`",)); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + + let mut class_def_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!("Protocol classes cannot be instantiated"), + ); + class_def_diagnostic.annotate( + Annotation::primary(protocol.header_span(db)) + .message(format_args!("`{class_name}` declared as a protocol here")), + ); + diagnostic.sub(class_def_diagnostic); +} + +pub(crate) fn report_duplicate_bases( + context: &InferContext, + class: ClassLiteral, + duplicate_base_error: &DuplicateBaseError, + bases_list: &[ast::Expr], +) { + let db = context.db(); + + let Some(builder) = context.report_lint(&DUPLICATE_BASE, class.header_range(db)) else { + return; + }; + + let DuplicateBaseError { + duplicate_base, + first_index, + later_indices, + } = duplicate_base_error; + + let duplicate_name = duplicate_base.name(db); + + let mut diagnostic = + builder.into_diagnostic(format_args!("Duplicate base class `{duplicate_name}`",)); + + let mut sub_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!( + "The definition of class `{}` will raise `TypeError` at runtime", + class.name(db) + ), + ); + sub_diagnostic.annotate( + Annotation::secondary(context.span(&bases_list[*first_index])).message(format_args!( + "Class `{duplicate_name}` first included in bases list here" + )), + ); + for index in later_indices { + sub_diagnostic.annotate( + Annotation::primary(context.span(&bases_list[*index])) + .message(format_args!("Class `{duplicate_name}` later repeated here")), + ); + } + + diagnostic.sub(sub_diagnostic); +} + +pub(crate) fn report_invalid_or_unsupported_base( + context: &InferContext, + base_node: &ast::Expr, + base_type: Type, + class: ClassLiteral, +) { + let db = context.db(); + let instance_of_type = KnownClass::Type.to_instance(db); + + if base_type.is_assignable_to(db, instance_of_type) { + report_unsupported_base(context, base_node, base_type, class); + return; + } + + let tuple_of_types = TupleType::homogeneous(db, instance_of_type); + + let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| { + diagnostic.info( + "An instance type is only a valid class base \ + if it has a valid `__mro_entries__` method", + ); + }; + + match base_type.try_call_dunder( + db, + "__mro_entries__", + CallArgumentTypes::positional([tuple_of_types]), + ) { + Ok(ret) => { + if ret.return_type(db).is_assignable_to(db, tuple_of_types) { + report_unsupported_base(context, base_node, base_type, class); + } else { + let Some(mut diagnostic) = + report_invalid_base(context, base_node, base_type, class) + else { + return; + }; + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` method, but it does not return a tuple of types", + base_type.display(db) + )); + } + } + Err(mro_entries_call_error) => { + let Some(mut diagnostic) = report_invalid_base(context, base_node, base_type, class) + else { + return; + }; + + match mro_entries_call_error { + CallDunderError::MethodNotAvailable => {} + CallDunderError::PossiblyUnbound(_) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` attribute, but it is possibly unbound", + base_type.display(db) + )); + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` attribute, but it is not callable", + base_type.display(db) + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, _) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` method, \ + but it cannot be called with the expected arguments", + base_type.display(db) + )); + diagnostic.info( + "Expected a signature at least as permissive as \ + `def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]`" + ); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => { + explain_mro_entries(&mut diagnostic); + diagnostic.info(format_args!( + "Type `{}` has an `__mro_entries__` method, \ + but it may not be callable", + base_type.display(db) + )); + } + } + } + } +} + +fn report_unsupported_base( + context: &InferContext, + base_node: &ast::Expr, + base_type: Type, + class: ClassLiteral, +) { + let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else { + return; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Unsupported class base with type `{}`", + base_type.display(context.db()) + )); + diagnostic.info(format_args!( + "ty cannot resolve a consistent MRO for class `{}` due to this base", + class.name(context.db()) + )); + diagnostic.info("Only class objects or `Any` are supported as class bases"); +} + +fn report_invalid_base<'ctx, 'db>( + context: &'ctx InferContext<'db, '_>, + base_node: &ast::Expr, + base_type: Type<'db>, + class: ClassLiteral<'db>, +) -> Option> { + let builder = context.report_lint(&INVALID_BASE, base_node)?; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid class base with type `{}`", + base_type.display(context.db()) + )); + diagnostic.info(format_args!( + "Definition of class `{}` will raise `TypeError` at runtime", + class.name(context.db()) + )); + Some(diagnostic) +} + +/// This function receives an unresolved `from foo import bar` import, +/// where `foo` can be resolved to a module but that module does not +/// have a `bar` member or submodule. +/// +/// If the `foo` module originates from the standard library and `foo.bar` +/// *does* exist as a submodule in the standard library on *other* Python +/// versions, we add a hint to the diagnostic that the user may have +/// misconfigured their Python version. +pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( + db: &dyn Db, + mut diagnostic: LintDiagnosticGuard, + full_submodule_name: &ModuleName, + parent_module: &Module, +) { + let Some(search_path) = parent_module.search_path() else { + return; + }; + + if !search_path.is_standard_library() { + return; + } + + let program = Program::get(db); + let typeshed_versions = program.search_paths(db).typeshed_versions(); + + let Some(version_range) = typeshed_versions.exact(full_submodule_name) else { + return; + }; + + let python_version = program.python_version(db); + if version_range.contains(python_version) { + return; + } + + diagnostic.info(format_args!( + "The stdlib module `{module_name}` only has a `{name}` \ + submodule on Python {version_range}", + module_name = parent_module.name(), + name = full_submodule_name + .components() + .next_back() + .expect("A `ModuleName` always has at least one component"), + version_range = version_range.diagnostic_display(), + )); + + add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules"); +} diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs new file mode 100644 index 0000000000000..a2f0c380e8a05 --- /dev/null +++ b/crates/ty_python_semantic/src/types/display.rs @@ -0,0 +1,1199 @@ +//! Display implementations for types. + +use std::fmt::{self, Display, Formatter, Write}; + +use ruff_db::display::FormatterJoinExtension; +use ruff_python_ast::str::{Quote, TripleQuotes}; +use ruff_python_literal::escape::AsciiEscape; +use ruff_text_size::{TextRange, TextSize}; + +use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; +use crate::types::function::{FunctionType, OverloadLiteral}; +use crate::types::generics::{GenericContext, Specialization}; +use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; +use crate::types::tuple::TupleSpec; +use crate::types::{ + CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType, + SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, + WrapperDescriptorKind, +}; +use crate::{Db, FxOrderSet}; + +impl<'db> Type<'db> { + pub fn display(&self, db: &'db dyn Db) -> DisplayType { + DisplayType { ty: self, db } + } + fn representation(self, db: &'db dyn Db) -> DisplayRepresentation<'db> { + DisplayRepresentation { db, ty: self } + } +} + +#[derive(Copy, Clone)] +pub struct DisplayType<'db> { + ty: &'db Type<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let representation = self.ty.representation(self.db); + match self.ty { + Type::ClassLiteral(literal) if literal.is_known(self.db, KnownClass::Any) => { + write!(f, "typing.Any") + } + Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) => { + write!(f, "Literal[{representation}]") + } + _ => representation.fmt(f), + } + } +} + +impl fmt::Debug for DisplayType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +/// Writes the string representation of a type, which is the value displayed either as +/// `Literal[]` or `Literal[, ]` for literal types or as `` for +/// non literals +struct DisplayRepresentation<'db> { + ty: Type<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayRepresentation<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.ty { + Type::Dynamic(dynamic) => dynamic.fmt(f), + Type::Never => f.write_str("Never"), + Type::NominalInstance(instance) => { + match (instance.class, instance.class.known(self.db)) { + (_, Some(KnownClass::NoneType)) => f.write_str("None"), + (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), + (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), + (ClassType::Generic(alias), _) => alias.display(self.db).fmt(f), + } + } + Type::ProtocolInstance(protocol) => match protocol.inner { + Protocol::FromClass(ClassType::NonGeneric(class)) => { + f.write_str(class.name(self.db)) + } + Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f), + Protocol::Synthesized(synthetic) => { + f.write_str("') + } + }, + Type::PropertyInstance(_) => f.write_str("property"), + Type::ModuleLiteral(module) => { + write!(f, "", module.module(self.db).name()) + } + Type::ClassLiteral(class) => { + write!(f, "", class.name(self.db)) + } + Type::GenericAlias(generic) => write!(f, "", generic.display(self.db)), + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + // Only show the bare class name here; ClassBase::display would render this as + // type[] instead of type[Foo]. + SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)), + SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), + }, + Type::SpecialForm(special_form) => special_form.fmt(f), + Type::KnownInstance(known_instance) => known_instance.repr(self.db).fmt(f), + Type::FunctionLiteral(function) => function.display(self.db).fmt(f), + Type::Callable(callable) => callable.display(self.db).fmt(f), + Type::BoundMethod(bound_method) => { + let function = bound_method.function(self.db); + + // TODO: use the specialization from the method. Similar to the comment above + // about the function specialization, + + match function.signature(self.db).overloads.as_slice() { + [signature] => { + write!( + f, + "bound method {instance}.{method}{signature}", + method = function.name(self.db), + instance = bound_method.self_instance(self.db).display(self.db), + signature = signature.bind_self().display(self.db) + ) + } + signatures => { + // TODO: How to display overloads? + f.write_str("Overload[")?; + let mut join = f.join(", "); + for signature in signatures { + join.entry(&signature.bind_self().display(self.db)); + } + f.write_str("]") + } + } + } + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + write!( + f, + "", + function = function.name(self.db), + ) + } + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => { + write!( + f, + "", + function = function.name(self.db), + ) + } + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { + write!(f, "",) + } + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => { + write!(f, "",) + } + Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => { + write!(f, "",) + } + Type::WrapperDescriptor(kind) => { + let (method, object) = match kind { + WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), + WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"), + WrapperDescriptorKind::PropertyDunderSet => ("__set__", "property"), + }; + write!(f, "") + } + Type::DataclassDecorator(_) => { + f.write_str("") + } + Type::DataclassTransformer(_) => { + f.write_str("") + } + Type::Union(union) => union.display(self.db).fmt(f), + Type::Intersection(intersection) => intersection.display(self.db).fmt(f), + Type::IntLiteral(n) => n.fmt(f), + Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }), + Type::StringLiteral(string) => string.display(self.db).fmt(f), + Type::LiteralString => f.write_str("LiteralString"), + Type::BytesLiteral(bytes) => { + let escape = AsciiEscape::with_preferred_quote(bytes.value(self.db), Quote::Double); + + escape.bytes_repr(TripleQuotes::No).write(f) + } + Type::Tuple(specialization) => specialization.tuple(self.db).display(self.db).fmt(f), + Type::TypeVar(typevar) => f.write_str(typevar.name(self.db)), + Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), + Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), + Type::BoundSuper(bound_super) => { + write!( + f, + "", + pivot = Type::from(bound_super.pivot_class(self.db)).display(self.db), + owner = bound_super.owner(self.db).into_type().display(self.db) + ) + } + Type::TypeIs(type_is) => { + f.write_str("TypeIs[")?; + type_is.return_type(self.db).display(self.db).fmt(f)?; + if let Some(name) = type_is.place_name(self.db) { + f.write_str(" @ ")?; + f.write_str(&name)?; + } + f.write_str("]") + } + } + } +} + +impl<'db> TupleSpec<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayTuple<'db> { + DisplayTuple { tuple: self, db } + } +} + +pub(crate) struct DisplayTuple<'db> { + tuple: &'db TupleSpec<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayTuple<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("tuple[")?; + match self.tuple { + TupleSpec::Fixed(tuple) => { + let elements = tuple.elements_slice(); + if elements.is_empty() { + f.write_str("()")?; + } else { + elements.display(self.db).fmt(f)?; + } + } + + // Decoder key for which snippets of text need to be included depending on whether + // the tuple contains a prefix and/or suffix: + // + // tuple[ yyy, ... ] + // tuple[xxx, *tuple[yyy, ...] ] + // tuple[xxx, *tuple[yyy, ...], zzz] + // tuple[ *tuple[yyy, ...], zzz] + // PPPPPPPPPPPP P + // SSSSSSS SSSSSS + // + // (Anything that appears above only a P is included only if there's a prefix; anything + // above only an S is included only if there's a suffix; anything about both a P and an + // S is included if there is either a prefix or a suffix. The initial `tuple[` and + // trailing `]` are printed elsewhere. The `yyy, ...` is printed no matter what.) + TupleSpec::Variable(tuple) => { + if !tuple.prefix.is_empty() { + tuple.prefix.display(self.db).fmt(f)?; + f.write_str(", ")?; + } + if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { + f.write_str("*tuple[")?; + } + tuple.variable.display(self.db).fmt(f)?; + f.write_str(", ...")?; + if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { + f.write_str("]")?; + } + if !tuple.suffix.is_empty() { + f.write_str(", ")?; + tuple.suffix.display(self.db).fmt(f)?; + } + } + } + f.write_str("]") + } +} + +impl<'db> OverloadLiteral<'db> { + // Not currently used, but useful for debugging. + #[expect(dead_code)] + pub(crate) fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { + DisplayOverloadLiteral { literal: self, db } + } +} + +pub(crate) struct DisplayOverloadLiteral<'db> { + literal: OverloadLiteral<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayOverloadLiteral<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let signature = self.literal.signature(self.db, None); + write!( + f, + "def {name}{signature}", + name = self.literal.name(self.db), + signature = signature.display(self.db) + ) + } +} + +impl<'db> FunctionType<'db> { + pub(crate) fn display(self, db: &'db dyn Db) -> DisplayFunctionType<'db> { + DisplayFunctionType { ty: self, db } + } +} + +pub(crate) struct DisplayFunctionType<'db> { + ty: FunctionType<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayFunctionType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let signature = self.ty.signature(self.db); + + // TODO: We should consider adding the type parameters to the signature of a generic + // function, i.e. `def foo[T](x: T) -> T`. + + match signature.overloads.as_slice() { + [signature] => { + write!( + f, + "def {name}{signature}", + name = self.ty.name(self.db), + signature = signature.display(self.db) + ) + } + signatures => { + // TODO: How to display overloads? + f.write_str("Overload[")?; + let mut join = f.join(", "); + for signature in signatures { + join.entry(&signature.display(self.db)); + } + f.write_str("]") + } + } + } +} + +impl<'db> GenericAlias<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { + DisplayGenericAlias { + origin: self.origin(db), + specialization: self.specialization(db), + db, + } + } +} + +pub(crate) struct DisplayGenericAlias<'db> { + origin: ClassLiteral<'db>, + specialization: Specialization<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayGenericAlias<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.origin.is_known(self.db, KnownClass::Tuple) { + self.specialization.tuple(self.db).display(self.db).fmt(f) + } else { + write!( + f, + "{origin}{specialization}", + origin = self.origin.name(self.db), + specialization = self.specialization.display_short( + self.db, + TupleSpecialization::from_class(self.db, self.origin) + ), + ) + } + } +} + +impl<'db> GenericContext<'db> { + pub fn display(&'db self, db: &'db dyn Db) -> DisplayGenericContext<'db> { + DisplayGenericContext { + typevars: self.variables(db), + db, + } + } +} + +pub struct DisplayGenericContext<'db> { + typevars: &'db FxOrderSet>, + db: &'db dyn Db, +} + +impl Display for DisplayGenericContext<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char('[')?; + for (idx, var) in self.typevars.iter().enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + f.write_str(var.name(self.db))?; + match var.bound_or_constraints(self.db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + write!(f, ": {}", bound.display(self.db))?; + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + f.write_str(": (")?; + for (idx, constraint) in constraints.iter(self.db).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + constraint.display(self.db).fmt(f)?; + } + f.write_char(')')?; + } + None => {} + } + if let Some(default_type) = var.default_ty(self.db) { + write!(f, " = {}", default_type.display(self.db))?; + } + } + f.write_char(']') + } +} + +impl<'db> Specialization<'db> { + /// Renders the specialization in full, e.g. `{T = int, U = str}`. + pub fn display( + &'db self, + db: &'db dyn Db, + tuple_specialization: TupleSpecialization, + ) -> DisplaySpecialization<'db> { + DisplaySpecialization { + typevars: self.generic_context(db).variables(db), + types: self.types(db), + db, + full: true, + tuple_specialization, + } + } + + /// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`. + pub fn display_short( + &'db self, + db: &'db dyn Db, + tuple_specialization: TupleSpecialization, + ) -> DisplaySpecialization<'db> { + DisplaySpecialization { + typevars: self.generic_context(db).variables(db), + types: self.types(db), + db, + full: false, + tuple_specialization, + } + } +} + +pub struct DisplaySpecialization<'db> { + typevars: &'db FxOrderSet>, + types: &'db [Type<'db>], + db: &'db dyn Db, + full: bool, + tuple_specialization: TupleSpecialization, +} + +impl Display for DisplaySpecialization<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.full { + f.write_char('{')?; + for (idx, (var, ty)) in self.typevars.iter().zip(self.types).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + write!(f, "{} = {}", var.name(self.db), ty.display(self.db))?; + } + f.write_char('}') + } else { + f.write_char('[')?; + for (idx, (_, ty)) in self.typevars.iter().zip(self.types).enumerate() { + if idx > 0 { + f.write_str(", ")?; + } + ty.display(self.db).fmt(f)?; + } + if self.tuple_specialization.is_yes() { + f.write_str(", ...")?; + } + f.write_char(']') + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TupleSpecialization { + Yes, + No, +} + +impl TupleSpecialization { + const fn is_yes(self) -> bool { + matches!(self, Self::Yes) + } + + fn from_class(db: &dyn Db, class: ClassLiteral) -> Self { + if class.is_known(db, KnownClass::Tuple) { + Self::Yes + } else { + Self::No + } + } +} + +impl<'db> CallableType<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayCallableType<'db> { + DisplayCallableType { + signatures: self.signatures(db), + db, + } + } +} + +pub(crate) struct DisplayCallableType<'db> { + signatures: &'db CallableSignature<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayCallableType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.signatures.overloads.as_slice() { + [signature] => signature.display(self.db).fmt(f), + signatures => { + // TODO: How to display overloads? + f.write_str("Overload[")?; + let mut join = f.join(", "); + for signature in signatures { + join.entry(&signature.display(self.db)); + } + join.finish()?; + f.write_char(']') + } + } + } +} + +impl<'db> Signature<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> { + DisplaySignature { + parameters: self.parameters(), + return_ty: self.return_ty, + db, + } + } +} + +pub(crate) struct DisplaySignature<'db> { + parameters: &'db Parameters<'db>, + return_ty: Option>, + db: &'db dyn Db, +} + +impl DisplaySignature<'_> { + /// Get detailed display information including component ranges + pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails { + let mut writer = SignatureWriter::Details(SignatureDetailsWriter::new()); + self.write_signature(&mut writer).unwrap(); + + match writer { + SignatureWriter::Details(details) => details.finish(), + SignatureWriter::Formatter(_) => unreachable!("Expected Details variant"), + } + } + + /// Internal method to write signature with the signature writer + fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result { + // Opening parenthesis + writer.write_char('(')?; + + if self.parameters.is_gradual() { + // We represent gradual form as `...` in the signature, internally the parameters still + // contain `(*args, **kwargs)` parameters. + writer.write_str("...")?; + } else { + let mut star_added = false; + let mut needs_slash = false; + let mut first = true; + + for parameter in self.parameters.as_slice() { + // Handle special separators + if !star_added && parameter.is_keyword_only() { + if !first { + writer.write_str(", ")?; + } + writer.write_char('*')?; + star_added = true; + first = false; + } + if parameter.is_positional_only() { + needs_slash = true; + } else if needs_slash { + if !first { + writer.write_str(", ")?; + } + writer.write_char('/')?; + needs_slash = false; + first = false; + } + + // Add comma before parameter if not first + if !first { + writer.write_str(", ")?; + } + + // Write parameter with range tracking + let param_name = parameter.display_name(); + writer.write_parameter(¶meter.display(self.db), param_name.as_deref())?; + + first = false; + } + + if needs_slash { + if !first { + writer.write_str(", ")?; + } + writer.write_char('/')?; + } + } + + // Closing parenthesis + writer.write_char(')')?; + + // Return type + let return_ty = self.return_ty.unwrap_or_else(Type::unknown); + writer.write_return_type(&return_ty.display(self.db))?; + + Ok(()) + } +} + +impl Display for DisplaySignature<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut writer = SignatureWriter::Formatter(f); + self.write_signature(&mut writer) + } +} + +/// Writer for building signature strings with different output targets +enum SignatureWriter<'a, 'b> { + /// Write directly to a formatter (for Display trait) + Formatter(&'a mut Formatter<'b>), + /// Build a string with range tracking (for `to_string_parts`) + Details(SignatureDetailsWriter), +} + +/// Writer that builds a string with range tracking +struct SignatureDetailsWriter { + label: String, + parameter_ranges: Vec, + parameter_names: Vec, +} + +impl SignatureDetailsWriter { + fn new() -> Self { + Self { + label: String::new(), + parameter_ranges: Vec::new(), + parameter_names: Vec::new(), + } + } + + fn finish(self) -> SignatureDisplayDetails { + SignatureDisplayDetails { + label: self.label, + parameter_ranges: self.parameter_ranges, + parameter_names: self.parameter_names, + } + } +} + +impl SignatureWriter<'_, '_> { + fn write_char(&mut self, c: char) -> fmt::Result { + match self { + SignatureWriter::Formatter(f) => f.write_char(c), + SignatureWriter::Details(details) => { + details.label.push(c); + Ok(()) + } + } + } + + fn write_str(&mut self, s: &str) -> fmt::Result { + match self { + SignatureWriter::Formatter(f) => f.write_str(s), + SignatureWriter::Details(details) => { + details.label.push_str(s); + Ok(()) + } + } + } + + fn write_parameter(&mut self, param: &T, param_name: Option<&str>) -> fmt::Result { + match self { + SignatureWriter::Formatter(f) => param.fmt(f), + SignatureWriter::Details(details) => { + let param_start = details.label.len(); + let param_display = param.to_string(); + details.label.push_str(¶m_display); + + // Use TextSize::try_from for safe conversion, falling back to empty range on overflow + let start = TextSize::try_from(param_start).unwrap_or_default(); + let length = TextSize::try_from(param_display.len()).unwrap_or_default(); + details.parameter_ranges.push(TextRange::at(start, length)); + + // Store the parameter name if available + if let Some(name) = param_name { + details.parameter_names.push(name.to_string()); + } else { + details.parameter_names.push(String::new()); + } + + Ok(()) + } + } + } + + fn write_return_type(&mut self, return_ty: &T) -> fmt::Result { + match self { + SignatureWriter::Formatter(f) => write!(f, " -> {return_ty}"), + SignatureWriter::Details(details) => { + let return_display = format!(" -> {return_ty}"); + details.label.push_str(&return_display); + Ok(()) + } + } + } +} + +/// Details about signature display components, including ranges for parameters and return type +#[derive(Debug, Clone)] +pub(crate) struct SignatureDisplayDetails { + /// The full signature string + pub label: String, + /// Ranges for each parameter within the label + pub parameter_ranges: Vec, + /// Names of the parameters in order + pub parameter_names: Vec, +} + +impl<'db> Parameter<'db> { + fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> { + DisplayParameter { param: self, db } + } +} + +struct DisplayParameter<'db> { + param: &'db Parameter<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayParameter<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Some(name) = self.param.display_name() { + f.write_str(&name)?; + if let Some(annotated_type) = self.param.annotated_type() { + write!(f, ": {}", annotated_type.display(self.db))?; + } + // Default value can only be specified if `name` is given. + if let Some(default_ty) = self.param.default_type() { + if self.param.annotated_type().is_some() { + write!(f, " = {}", default_ty.display(self.db))?; + } else { + write!(f, "={}", default_ty.display(self.db))?; + } + } + } else if let Some(ty) = self.param.annotated_type() { + // This case is specifically for the `Callable` signature where name and default value + // cannot be provided. + ty.display(self.db).fmt(f)?; + } + Ok(()) + } +} + +impl<'db> UnionType<'db> { + fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> { + DisplayUnionType { db, ty: self } + } +} + +struct DisplayUnionType<'db> { + ty: &'db UnionType<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayUnionType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + fn is_condensable(ty: Type<'_>) -> bool { + matches!( + ty, + Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::BooleanLiteral(_) + ) + } + + let elements = self.ty.elements(self.db); + + let condensed_types = elements + .iter() + .copied() + .filter(|element| is_condensable(*element)) + .collect::>(); + + let mut join = f.join(" | "); + + let mut condensed_types = Some(condensed_types); + for element in elements { + if is_condensable(*element) { + if let Some(condensed_types) = condensed_types.take() { + join.entry(&DisplayLiteralGroup { + literals: condensed_types, + db: self.db, + }); + } + } else { + join.entry(&DisplayMaybeParenthesizedType { + ty: *element, + db: self.db, + }); + } + } + + join.finish()?; + + Ok(()) + } +} + +impl fmt::Debug for DisplayUnionType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +struct DisplayLiteralGroup<'db> { + literals: Vec>, + db: &'db dyn Db, +} + +impl Display for DisplayLiteralGroup<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("Literal[")?; + f.join(", ") + .entries(self.literals.iter().map(|ty| ty.representation(self.db))) + .finish()?; + f.write_str("]") + } +} + +impl<'db> IntersectionType<'db> { + fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> { + DisplayIntersectionType { db, ty: self } + } +} + +struct DisplayIntersectionType<'db> { + ty: &'db IntersectionType<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayIntersectionType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let tys = self + .ty + .positive(self.db) + .iter() + .map(|&ty| DisplayMaybeNegatedType { + ty, + db: self.db, + negated: false, + }) + .chain( + self.ty + .negative(self.db) + .iter() + .map(|&ty| DisplayMaybeNegatedType { + ty, + db: self.db, + negated: true, + }), + ); + f.join(" & ").entries(tys).finish() + } +} + +impl fmt::Debug for DisplayIntersectionType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +struct DisplayMaybeNegatedType<'db> { + ty: Type<'db>, + db: &'db dyn Db, + negated: bool, +} + +impl Display for DisplayMaybeNegatedType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.negated { + f.write_str("~")?; + } + 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 { + let write_parentheses = |f: &mut Formatter<'_>| write!(f, "({})", self.ty.display(self.db)); + match self.ty { + Type::Callable(_) + | Type::MethodWrapper(_) + | Type::FunctionLiteral(_) + | Type::BoundMethod(_) + | Type::Union(_) => write_parentheses(f), + Type::Intersection(intersection) if !intersection.has_one_element(self.db) => { + write_parentheses(f) + } + _ => self.ty.display(self.db).fmt(f), + } + } +} + +pub(crate) trait TypeArrayDisplay<'db> { + fn display(&self, db: &'db dyn Db) -> DisplayTypeArray; +} + +impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> { + fn display(&self, db: &'db dyn Db) -> DisplayTypeArray { + DisplayTypeArray { types: self, db } + } +} + +impl<'db> TypeArrayDisplay<'db> for Vec> { + fn display(&self, db: &'db dyn Db) -> DisplayTypeArray { + DisplayTypeArray { types: self, db } + } +} + +impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { + fn display(&self, db: &'db dyn Db) -> DisplayTypeArray { + DisplayTypeArray { types: self, db } + } +} + +pub(crate) struct DisplayTypeArray<'b, 'db> { + types: &'b [Type<'db>], + db: &'db dyn Db, +} + +impl Display for DisplayTypeArray<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.join(", ") + .entries(self.types.iter().map(|ty| ty.display(self.db))) + .finish() + } +} + +impl<'db> StringLiteralType<'db> { + fn display(&'db self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> { + DisplayStringLiteralType { db, ty: self } + } +} + +struct DisplayStringLiteralType<'db> { + ty: &'db StringLiteralType<'db>, + db: &'db dyn Db, +} + +impl Display for DisplayStringLiteralType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let value = self.ty.value(self.db); + f.write_char('"')?; + for ch in value.chars() { + match ch { + // `escape_debug` will escape even single quotes, which is not necessary for our + // use case as we are already using double quotes to wrap the string. + '\'' => f.write_char('\''), + _ => ch.escape_debug().fmt(f), + }?; + } + f.write_char('"') + } +} + +#[cfg(test)] +mod tests { + use ruff_python_ast::name::Name; + + use crate::Db; + use crate::db::tests::setup_db; + use crate::place::typing_extensions_symbol; + use crate::types::{KnownClass, Parameter, Parameters, Signature, StringLiteralType, Type}; + + #[test] + fn string_literal_display() { + let db = setup_db(); + + assert_eq!( + Type::StringLiteral(StringLiteralType::new(&db, r"\n")) + .display(&db) + .to_string(), + r#"Literal["\\n"]"# + ); + assert_eq!( + Type::StringLiteral(StringLiteralType::new(&db, "'")) + .display(&db) + .to_string(), + r#"Literal["'"]"# + ); + assert_eq!( + Type::StringLiteral(StringLiteralType::new(&db, r#"""#)) + .display(&db) + .to_string(), + r#"Literal["\""]"# + ); + } + + #[test] + fn synthesized_protocol_display() { + let db = setup_db(); + + // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one. + let supports_index_synthesized = KnownClass::SupportsIndex.to_instance(&db).normalized(&db); + assert_eq!( + supports_index_synthesized.display(&db).to_string(), + "" + ); + + let iterator_synthesized = typing_extensions_symbol(&db, "Iterator") + .place + .ignore_possibly_unbound() + .unwrap() + .to_instance(&db) + .unwrap() + .normalized(&db); // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one. + + assert_eq!( + iterator_synthesized.display(&db).to_string(), + "" + ); + } + + fn display_signature<'db>( + db: &dyn Db, + parameters: impl IntoIterator>, + return_ty: Option>, + ) -> String { + Signature::new(Parameters::new(parameters), return_ty) + .display(db) + .to_string() + } + + #[test] + fn signature_display() { + let db = setup_db(); + + // Empty parameters with no return type. + assert_eq!(display_signature(&db, [], None), "() -> Unknown"); + + // Empty parameters with a return type. + assert_eq!( + display_signature(&db, [], Some(Type::none(&db))), + "() -> None" + ); + + // Single parameter type (no name) with a return type. + assert_eq!( + display_signature( + &db, + [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], + Some(Type::none(&db)) + ), + "(None, /) -> None" + ); + + // Two parameters where one has annotation and the other doesn't. + assert_eq!( + display_signature( + &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)) + ), + "(x=int, y: str = str) -> None" + ); + + // All positional only parameters. + assert_eq!( + display_signature( + &db, + [ + Parameter::positional_only(Some(Name::new_static("x"))), + Parameter::positional_only(Some(Name::new_static("y"))), + ], + Some(Type::none(&db)) + ), + "(x, y, /) -> None" + ); + + // Positional-only parameters mixed with non-positional-only parameters. + assert_eq!( + display_signature( + &db, + [ + Parameter::positional_only(Some(Name::new_static("x"))), + Parameter::positional_or_keyword(Name::new_static("y")), + ], + Some(Type::none(&db)) + ), + "(x, /, y) -> None" + ); + + // All keyword-only parameters. + assert_eq!( + display_signature( + &db, + [ + Parameter::keyword_only(Name::new_static("x")), + Parameter::keyword_only(Name::new_static("y")), + ], + Some(Type::none(&db)) + ), + "(*, x, y) -> None" + ); + + // Keyword-only parameters mixed with non-keyword-only parameters. + assert_eq!( + display_signature( + &db, + [ + Parameter::positional_or_keyword(Name::new_static("x")), + Parameter::keyword_only(Name::new_static("y")), + ], + Some(Type::none(&db)) + ), + "(x, *, y) -> None" + ); + + // A mix of all parameter kinds. + assert_eq!( + display_signature( + &db, + [ + 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)) + ), + "(a, b: int, c=Literal[1], d: int = Literal[2], \ + /, e=Literal[3], f: int = Literal[4], *args: object, \ + *, g=Literal[5], h: int = Literal[6], **kwargs: str) -> bytes" + ); + } +} diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs new file mode 100644 index 0000000000000..189ea9dd12793 --- /dev/null +++ b/crates/ty_python_semantic/src/types/function.rs @@ -0,0 +1,1320 @@ +//! Contains representations of function literals. There are several complicating factors: +//! +//! - Functions can be generic, and can have specializations applied to them. These are not the +//! same thing! For instance, a method of a generic class might not itself be generic, but it can +//! still have the class's specialization applied to it. +//! +//! - Functions can be overloaded, and each overload can be independently generic or not, with +//! different sets of typevars for different generic overloads. In some cases we need to consider +//! each overload separately; in others we need to consider all of the overloads (and any +//! implementation) as a single collective entity. +//! +//! - Certain “known” functions need special treatment — for instance, inferring a special return +//! type, or raising custom diagnostics. +//! +//! - TODO: Some functions don't correspond to a function definition in the AST, and are instead +//! synthesized as we mimic the behavior of the Python interpreter. Even though they are +//! synthesized, and are “implemented” as Rust code, they are still functions from the POV of the +//! rest of the type system. +//! +//! Given these constraints, we have the following representation: a function is a list of one or +//! more overloads, with zero or more specializations (more specifically, “type mappings”) applied +//! to it. [`FunctionType`] is the outermost type, which is what [`Type::FunctionLiteral`] wraps. +//! It contains the list of type mappings to apply. It wraps a [`FunctionLiteral`], which collects +//! together all of the overloads (and implementation) of an overloaded function. An +//! [`OverloadLiteral`] represents an individual function definition in the AST — that is, each +//! overload (and implementation) of an overloaded function, or the single definition of a +//! non-overloaded function. +//! +//! Technically, each `FunctionLiteral` wraps a particular overload and all _previous_ overloads. +//! So it's only true that it wraps _all_ overloads if you are looking at the last definition. For +//! instance, in +//! +//! ```py +//! @overload +//! def f(x: int) -> None: ... +//! # <-- 1 +//! +//! @overload +//! def f(x: str) -> None: ... +//! # <-- 2 +//! +//! def f(x): pass +//! # <-- 3 +//! ``` +//! +//! resolving `f` at each of the three numbered positions will give you a `FunctionType`, which +//! wraps a `FunctionLiteral`, which contain `OverloadLiteral`s only for the definitions that +//! appear before that position. We rely on the fact that later definitions shadow earlier ones, so +//! the public type of `f` is resolved at position 3, correctly giving you all of the overloads +//! (and the implementation). + +use std::str::FromStr; + +use bitflags::bitflags; +use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_python_ast as ast; +use ruff_text_size::Ranged; + +use crate::module_resolver::{KnownModule, file_to_module}; +use crate::place::{Boundness, Place, place_from_bindings}; +use crate::semantic_index::ast_ids::HasScopedUseId; +use crate::semantic_index::definition::Definition; +use crate::semantic_index::place::ScopeId; +use crate::semantic_index::semantic_index; +use crate::types::context::InferContext; +use crate::types::diagnostic::{ + REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, + report_bad_argument_to_get_protocol_members, + report_runtime_check_against_non_runtime_checkable_protocol, +}; +use crate::types::generics::{GenericContext, walk_generic_context}; +use crate::types::narrow::ClassInfoConstraintFunction; +use crate::types::signatures::{CallableSignature, Signature}; +use crate::types::visitor::any_over_type; +use crate::types::{ + BoundMethodType, CallableType, DynamicType, KnownClass, Type, TypeMapping, TypeRelation, + TypeTransformer, TypeVarInstance, walk_type_mapping, +}; +use crate::{Db, FxOrderSet, ModuleName, resolve_module}; + +/// A collection of useful spans for annotating functions. +/// +/// This can be retrieved via `FunctionType::spans` or +/// `Type::function_spans`. +pub(crate) struct FunctionSpans { + /// The span of the entire function "signature." This includes + /// the name, parameter list and return type (if present). + pub(crate) signature: Span, + /// The span of the function name. i.e., `foo` in `def foo(): ...`. + pub(crate) name: Span, + /// The span of the parameter list, including the opening and + /// closing parentheses. + #[expect(dead_code)] + pub(crate) parameters: Span, + /// The span of the annotated return type, if present. + pub(crate) return_type: Option, +} + +bitflags! { + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Hash)] + pub struct FunctionDecorators: u8 { + /// `@classmethod` + const CLASSMETHOD = 1 << 0; + /// `@typing.no_type_check` + const NO_TYPE_CHECK = 1 << 1; + /// `@typing.overload` + const OVERLOAD = 1 << 2; + /// `@abc.abstractmethod` + const ABSTRACT_METHOD = 1 << 3; + /// `@typing.final` + const FINAL = 1 << 4; + /// `@staticmethod` + const STATICMETHOD = 1 << 5; + /// `@typing.override` + const OVERRIDE = 1 << 6; + } +} + +impl FunctionDecorators { + pub(super) fn from_decorator_type(db: &dyn Db, decorator_type: Type) -> Self { + match decorator_type { + Type::FunctionLiteral(function) => match function.known(db) { + Some(KnownFunction::NoTypeCheck) => FunctionDecorators::NO_TYPE_CHECK, + Some(KnownFunction::Overload) => FunctionDecorators::OVERLOAD, + Some(KnownFunction::AbstractMethod) => FunctionDecorators::ABSTRACT_METHOD, + Some(KnownFunction::Final) => FunctionDecorators::FINAL, + Some(KnownFunction::Override) => FunctionDecorators::OVERRIDE, + _ => FunctionDecorators::empty(), + }, + Type::ClassLiteral(class) => match class.known(db) { + Some(KnownClass::Classmethod) => FunctionDecorators::CLASSMETHOD, + Some(KnownClass::Staticmethod) => FunctionDecorators::STATICMETHOD, + _ => FunctionDecorators::empty(), + }, + _ => FunctionDecorators::empty(), + } + } + + pub(super) fn from_decorator_types<'db>( + db: &'db dyn Db, + types: impl IntoIterator>, + ) -> Self { + types + .into_iter() + .fold(FunctionDecorators::empty(), |acc, ty| { + acc | FunctionDecorators::from_decorator_type(db, ty) + }) + } +} + +bitflags! { + /// Used for the return type of `dataclass_transform(…)` calls. Keeps track of the + /// arguments that were passed in. For the precise meaning of the fields, see [1]. + /// + /// [1]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] + pub struct DataclassTransformerParams: u8 { + const EQ_DEFAULT = 1 << 0; + const ORDER_DEFAULT = 1 << 1; + const KW_ONLY_DEFAULT = 1 << 2; + const FROZEN_DEFAULT = 1 << 3; + } +} + +impl get_size2::GetSize for DataclassTransformerParams {} + +impl Default for DataclassTransformerParams { + fn default() -> Self { + Self::EQ_DEFAULT + } +} + +/// Representation of a function definition in the AST: either a non-generic function, or a generic +/// function that has not been specialized. +/// +/// If a function has multiple overloads, each overload is represented by a separate function +/// definition in the AST, and is therefore a separate `OverloadLiteral` instance. +/// +/// # Ordering +/// Ordering is based on the function's id assigned by salsa and not on the function literal's +/// values. The id may change between runs, or when the function literal was garbage collected and +/// recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct OverloadLiteral<'db> { + /// Name of the function at definition. + #[returns(ref)] + pub name: ast::name::Name, + + /// Is this a function that we special-case somehow? If so, which one? + pub(crate) known: Option, + + /// The scope that's created by the function, in which the function body is evaluated. + pub(crate) body_scope: ScopeId<'db>, + + /// A set of special decorators that were applied to this function + pub(crate) decorators: FunctionDecorators, + + /// The arguments to `dataclass_transformer`, if this function was annotated + /// with `@dataclass_transformer(...)`. + pub(crate) dataclass_transformer_params: Option, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for OverloadLiteral<'_> {} + +#[salsa::tracked] +impl<'db> OverloadLiteral<'db> { + fn with_dataclass_transformer_params( + self, + db: &'db dyn Db, + params: DataclassTransformerParams, + ) -> Self { + Self::new( + db, + self.name(db).clone(), + self.known(db), + self.body_scope(db), + self.decorators(db), + Some(params), + ) + } + + fn file(self, db: &'db dyn Db) -> File { + // NOTE: Do not use `self.definition(db).file(db)` here, as that could create a + // cross-module dependency on the full AST. + self.body_scope(db).file(db) + } + + pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + self.decorators(db).contains(decorator) + } + + pub(crate) fn is_overload(self, db: &dyn Db) -> bool { + self.has_known_decorator(db, FunctionDecorators::OVERLOAD) + } + + fn node<'ast>( + self, + db: &dyn Db, + file: File, + module: &'ast ParsedModuleRef, + ) -> &'ast ast::StmtFunctionDef { + debug_assert_eq!( + file, + self.file(db), + "OverloadLiteral::node() must be called with the same file as the one where \ + the function is defined." + ); + + self.body_scope(db).node(db).expect_function(module) + } + + /// Returns the [`FileRange`] of the function's name. + pub(crate) fn focus_range(self, db: &dyn Db, module: &ParsedModuleRef) -> FileRange { + FileRange::new( + self.file(db), + self.body_scope(db) + .node(db) + .expect_function(module) + .name + .range, + ) + } + + /// Returns the [`Definition`] of this function. + /// + /// ## Warning + /// + /// This uses the semantic index to find the definition of the function. This means that if the + /// calling query is not in the same file as this function is defined in, then this will create + /// a cross-module dependency directly on the full AST which will lead to cache + /// over-invalidation. + fn definition(self, db: &'db dyn Db) -> Definition<'db> { + let body_scope = self.body_scope(db); + let module = parsed_module(db, self.file(db)).load(db); + let index = semantic_index(db, body_scope.file(db)); + index.expect_single_definition(body_scope.node(db).expect_function(&module)) + } + + /// Returns the overload immediately before this one in the AST. Returns `None` if there is no + /// previous overload. + fn previous_overload(self, db: &'db dyn Db) -> Option> { + // The semantic model records a use for each function on the name node. This is used + // here to get the previous function definition with the same name. + let scope = self.definition(db).scope(db); + let module = parsed_module(db, self.file(db)).load(db); + let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); + let use_id = self + .body_scope(db) + .node(db) + .expect_function(&module) + .name + .scoped_use_id(db, scope); + + let Place::Type(Type::FunctionLiteral(previous_type), Boundness::Bound) = + place_from_bindings(db, use_def.bindings_at_use(use_id)) + else { + return None; + }; + + let previous_literal = previous_type.literal(db); + let previous_overload = previous_literal.last_definition(db); + if !previous_overload.is_overload(db) { + return None; + } + + Some(previous_literal) + } + + /// Typed internally-visible signature for this function. + /// + /// This represents the annotations on the function itself, unmodified by decorators and + /// overloads. + /// + /// ## Warning + /// + /// This uses the semantic index to find the definition of the function. This means that if the + /// calling query is not in the same file as this function is defined in, then this will create + /// a cross-module dependency directly on the full AST which will lead to cache + /// over-invalidation. + pub(crate) fn signature( + self, + db: &'db dyn Db, + inherited_generic_context: Option>, + ) -> Signature<'db> { + let scope = self.body_scope(db); + let module = parsed_module(db, self.file(db)).load(db); + let function_stmt_node = scope.node(db).expect_function(&module); + let definition = self.definition(db); + let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| { + let index = semantic_index(db, scope.file(db)); + GenericContext::from_type_params(db, index, type_params) + }); + Signature::from_function( + db, + generic_context, + inherited_generic_context, + definition, + function_stmt_node, + ) + } + + pub(crate) fn parameter_span( + self, + db: &'db dyn Db, + parameter_index: Option, + ) -> Option<(Span, Span)> { + let function_scope = self.body_scope(db); + let span = Span::from(function_scope.file(db)); + let node = function_scope.node(db); + let module = parsed_module(db, self.file(db)).load(db); + let func_def = node.as_function(&module)?; + let range = parameter_index + .and_then(|parameter_index| { + func_def + .parameters + .iter() + .nth(parameter_index) + .map(|param| param.range()) + }) + .unwrap_or(func_def.parameters.range); + let name_span = span.clone().with_range(func_def.name.range); + let parameter_span = span.with_range(range); + Some((name_span, parameter_span)) + } + + pub(crate) fn spans(self, db: &'db dyn Db) -> Option { + let function_scope = self.body_scope(db); + let span = Span::from(function_scope.file(db)); + let node = function_scope.node(db); + let module = parsed_module(db, self.file(db)).load(db); + let func_def = node.as_function(&module)?; + let return_type_range = func_def.returns.as_ref().map(|returns| returns.range()); + let mut signature = func_def.name.range.cover(func_def.parameters.range); + if let Some(return_type_range) = return_type_range { + signature = signature.cover(return_type_range); + } + Some(FunctionSpans { + signature: span.clone().with_range(signature), + name: span.clone().with_range(func_def.name.range), + parameters: span.clone().with_range(func_def.parameters.range), + return_type: return_type_range.map(|range| span.clone().with_range(range)), + }) + } +} + +/// Representation of a function definition in the AST, along with any previous overloads of the +/// function. Each overload can be separately generic or not, and each generic overload uses +/// distinct typevars. +/// +/// # Ordering +/// Ordering is based on the function's id assigned by salsa and not on the function literal's +/// values. The id may change between runs, or when the function literal was garbage collected and +/// recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct FunctionLiteral<'db> { + pub(crate) last_definition: OverloadLiteral<'db>, + + /// The inherited generic context, if this function is a constructor method (`__new__` or + /// `__init__`) being used to infer the specialization of its generic class. If any of the + /// method's overloads are themselves generic, this is in addition to those per-overload + /// generic contexts (which are created lazily in [`OverloadLiteral::signature`]). + /// + /// If the function is not a constructor method, this field will always be `None`. + /// + /// If the function is a constructor method, we will end up creating two `FunctionLiteral` + /// instances for it. The first is created in [`TypeInferenceBuilder`][infer] when we encounter + /// the function definition during type inference. At this point, we don't yet know if the + /// function is a constructor method, so we create a `FunctionLiteral` with `None` for this + /// field. + /// + /// If at some point we encounter a call expression, which invokes the containing class's + /// constructor, as will create a _new_ `FunctionLiteral` instance for the function, with this + /// field [updated][] to contain the containing class's generic context. + /// + /// [infer]: crate::types::infer::TypeInferenceBuilder::infer_function_definition + /// [updated]: crate::types::class::ClassLiteral::own_class_member + inherited_generic_context: Option>, +} + +fn walk_function_literal<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + function: FunctionLiteral<'db>, + visitor: &mut V, +) { + if let Some(context) = function.inherited_generic_context(db) { + walk_generic_context(db, context, visitor); + } +} + +#[salsa::tracked] +impl<'db> FunctionLiteral<'db> { + fn with_inherited_generic_context( + self, + db: &'db dyn Db, + inherited_generic_context: GenericContext<'db>, + ) -> Self { + // A function cannot inherit more than one generic context from its containing class. + debug_assert!(self.inherited_generic_context(db).is_none()); + Self::new( + db, + self.last_definition(db), + Some(inherited_generic_context), + ) + } + + fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + // All of the overloads of a function literal should have the same name. + self.last_definition(db).name(db) + } + + fn known(self, db: &'db dyn Db) -> Option { + // Whether a function is known is based on its name (and its containing module's name), so + // all overloads should be known (or not) equivalently. + self.last_definition(db).known(db) + } + + fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + self.iter_overloads_and_implementation(db) + .any(|overload| overload.decorators(db).contains(decorator)) + } + + fn definition(self, db: &'db dyn Db) -> Definition<'db> { + self.last_definition(db).definition(db) + } + + fn parameter_span( + self, + db: &'db dyn Db, + parameter_index: Option, + ) -> Option<(Span, Span)> { + self.last_definition(db).parameter_span(db, parameter_index) + } + + fn spans(self, db: &'db dyn Db) -> Option { + self.last_definition(db).spans(db) + } + + #[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)] + fn overloads_and_implementation( + self, + db: &'db dyn Db, + ) -> (Box<[OverloadLiteral<'db>]>, Option>) { + let self_overload = self.last_definition(db); + let mut current = self_overload; + let mut overloads = vec![]; + + while let Some(previous) = current.previous_overload(db) { + let overload = previous.last_definition(db); + overloads.push(overload); + current = overload; + } + + // Overloads are inserted in reverse order, from bottom to top. + overloads.reverse(); + + let implementation = if self_overload.is_overload(db) { + overloads.push(self_overload); + None + } else { + Some(self_overload) + }; + + (overloads.into_boxed_slice(), implementation) + } + + fn iter_overloads_and_implementation( + self, + db: &'db dyn Db, + ) -> impl Iterator> + 'db { + let (implementation, overloads) = self.overloads_and_implementation(db); + overloads.iter().chain(implementation).copied() + } + + /// Typed externally-visible signature for this function. + /// + /// This is the signature as seen by external callers, possibly modified by decorators and/or + /// overloaded. + /// + /// ## Warning + /// + /// This uses the semantic index to find the definition of the function. This means that if the + /// calling query is not in the same file as this function is defined in, then this will create + /// a cross-module dependency directly on the full AST which will lead to cache + /// over-invalidation. + fn signature<'a>( + self, + db: &'db dyn Db, + type_mappings: &'a [TypeMapping<'a, 'db>], + ) -> CallableSignature<'db> + where + 'db: 'a, + { + // We only include an implementation (i.e. a definition not decorated with `@overload`) if + // it's the only definition. + let inherited_generic_context = self.inherited_generic_context(db); + let (overloads, implementation) = self.overloads_and_implementation(db); + if let Some(implementation) = implementation { + if overloads.is_empty() { + return CallableSignature::single(type_mappings.iter().fold( + implementation.signature(db, inherited_generic_context), + |ty, mapping| ty.apply_type_mapping(db, mapping), + )); + } + } + + CallableSignature::from_overloads(overloads.iter().map(|overload| { + type_mappings.iter().fold( + overload.signature(db, inherited_generic_context), + |ty, mapping| ty.apply_type_mapping(db, mapping), + ) + })) + } + + fn normalized_impl(self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + let context = self + .inherited_generic_context(db) + .map(|ctx| ctx.normalized_impl(db, visitor)); + Self::new(db, self.last_definition(db), context) + } +} + +/// Represents a function type, which might be a non-generic function, or a specialization of a +/// generic function. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct FunctionType<'db> { + pub(crate) literal: FunctionLiteral<'db>, + + /// Type mappings that should be applied to the function's parameter and return types. This + /// might include specializations of enclosing generic contexts (e.g. for non-generic methods + /// of a specialized generic class). + #[returns(deref)] + type_mappings: Box<[TypeMapping<'db, 'db>]>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for FunctionType<'_> {} + +pub(super) fn walk_function_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + function: FunctionType<'db>, + visitor: &mut V, +) { + walk_function_literal(db, function.literal(db), visitor); + for mapping in function.type_mappings(db) { + walk_type_mapping(db, mapping, visitor); + } +} + +#[salsa::tracked] +impl<'db> FunctionType<'db> { + pub(crate) fn with_inherited_generic_context( + self, + db: &'db dyn Db, + inherited_generic_context: GenericContext<'db>, + ) -> Self { + let literal = self + .literal(db) + .with_inherited_generic_context(db, inherited_generic_context); + Self::new(db, literal, self.type_mappings(db)) + } + + pub(crate) fn with_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + let type_mappings: Box<[_]> = self + .type_mappings(db) + .iter() + .cloned() + .chain(std::iter::once(type_mapping.to_owned())) + .collect(); + Self::new(db, self.literal(db), type_mappings) + } + + pub(crate) fn with_dataclass_transformer_params( + self, + db: &'db dyn Db, + params: DataclassTransformerParams, + ) -> Self { + // A decorator only applies to the specific overload that it is attached to, not to all + // previous overloads. + let literal = self.literal(db); + let last_definition = literal + .last_definition(db) + .with_dataclass_transformer_params(db, params); + let literal = + FunctionLiteral::new(db, last_definition, literal.inherited_generic_context(db)); + Self::new(db, literal, self.type_mappings(db)) + } + + /// Returns the [`File`] in which this function is defined. + pub(crate) fn file(self, db: &'db dyn Db) -> File { + self.literal(db).last_definition(db).file(db) + } + + /// Returns the AST node for this function. + pub(crate) fn node<'ast>( + self, + db: &dyn Db, + file: File, + module: &'ast ParsedModuleRef, + ) -> &'ast ast::StmtFunctionDef { + self.literal(db).last_definition(db).node(db, file, module) + } + + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + self.literal(db).name(db) + } + + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + self.literal(db).known(db) + } + + pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { + self.known(db) == Some(known_function) + } + + /// Returns if any of the overloads of this function have a particular decorator. + /// + /// Some decorators are expected to appear on every overload; others are expected to appear + /// only the implementation or first overload. This method does not check either of those + /// conditions. + pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + self.literal(db).has_known_decorator(db, decorator) + } + + /// Returns the [`Definition`] of the implementation or first overload of this function. + /// + /// ## Warning + /// + /// This uses the semantic index to find the definition of the function. This means that if the + /// calling query is not in the same file as this function is defined in, then this will create + /// a cross-module dependency directly on the full AST which will lead to cache + /// over-invalidation. + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + self.literal(db).definition(db) + } + + /// Returns a tuple of two spans. The first is + /// the span for the identifier of the function + /// definition for `self`. The second is + /// the span for the parameter in the function + /// definition for `self`. + /// + /// If there are no meaningful spans, then this + /// returns `None`. For example, when this type + /// isn't callable. + /// + /// When `parameter_index` is `None`, then the + /// second span returned covers the entire parameter + /// list. + /// + /// # Performance + /// + /// Note that this may introduce cross-module + /// dependencies. This can have an impact on + /// the effectiveness of incremental caching + /// and should therefore be used judiciously. + /// + /// An example of a good use case is to improve + /// a diagnostic. + pub(crate) fn parameter_span( + self, + db: &'db dyn Db, + parameter_index: Option, + ) -> Option<(Span, Span)> { + self.literal(db).parameter_span(db, parameter_index) + } + + /// Returns a collection of useful spans for a + /// function signature. These are useful for + /// creating annotations on diagnostics. + /// + /// # Performance + /// + /// Note that this may introduce cross-module + /// dependencies. This can have an impact on + /// the effectiveness of incremental caching + /// and should therefore be used judiciously. + /// + /// An example of a good use case is to improve + /// a diagnostic. + pub(crate) fn spans(self, db: &'db dyn Db) -> Option { + self.literal(db).spans(db) + } + + /// Returns all of the overload signatures and the implementation definition, if any, of this + /// function. The overload signatures will be in source order. + pub(crate) fn overloads_and_implementation( + self, + db: &'db dyn Db, + ) -> &'db (Box<[OverloadLiteral<'db>]>, Option>) { + self.literal(db).overloads_and_implementation(db) + } + + /// Returns an iterator of all of the definitions of this function, including both overload + /// signatures and any implementation, all in source order. + pub(crate) fn iter_overloads_and_implementation( + self, + db: &'db dyn Db, + ) -> impl Iterator> + 'db { + self.literal(db).iter_overloads_and_implementation(db) + } + + /// Typed externally-visible signature for this function. + /// + /// This is the signature as seen by external callers, possibly modified by decorators and/or + /// overloaded. + /// + /// ## Why is this a salsa query? + /// + /// This is a salsa query to short-circuit the invalidation + /// when the function's AST node changes. + /// + /// Were this not a salsa query, then the calling query + /// would depend on the function's AST and rerun for every change in that file. + #[salsa::tracked(returns(ref), cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] + pub(crate) fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { + self.literal(db).signature(db, self.type_mappings(db)) + } + + /// Convert the `FunctionType` into a [`CallableType`]. + pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { + CallableType::new(db, self.signature(db), false) + } + + /// Convert the `FunctionType` into a [`Type::BoundMethod`]. + pub(crate) fn into_bound_method_type( + self, + db: &'db dyn Db, + self_instance: Type<'db>, + ) -> Type<'db> { + Type::BoundMethod(BoundMethodType::new(db, self, self_instance)) + } + + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + match relation { + TypeRelation::Subtyping => self.is_subtype_of(db, other), + TypeRelation::Assignability => self.is_assignable_to(db, other), + } + } + + pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + // A function type is the subtype of itself, and not of any other function type. However, + // our representation of a function type includes any specialization that should be applied + // to the signature. Different specializations of the same function type are only subtypes + // of each other if they result in subtype signatures. + if self.normalized(db) == other.normalized(db) { + return true; + } + if self.literal(db) != other.literal(db) { + return false; + } + let self_signature = self.signature(db); + let other_signature = other.signature(db); + self_signature.is_subtype_of(db, other_signature) + } + + pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { + // A function type is assignable to itself, and not to any other function type. However, + // our representation of a function type includes any specialization that should be applied + // to the signature. Different specializations of the same function type are only + // assignable to each other if they result in assignable signatures. + self.literal(db) == other.literal(db) + && self.signature(db).is_assignable_to(db, other.signature(db)) + } + + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + if self.normalized(db) == other.normalized(db) { + return true; + } + if self.literal(db) != other.literal(db) { + return false; + } + let self_signature = self.signature(db); + let other_signature = other.signature(db); + self_signature.is_equivalent_to(db, other_signature) + } + + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + let signatures = self.signature(db); + for signature in &signatures.overloads { + signature.find_legacy_typevars(db, typevars); + } + } + + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + let mut visitor = TypeTransformer::default(); + self.normalized_impl(db, &mut visitor) + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + let mappings: Box<_> = self + .type_mappings(db) + .iter() + .map(|mapping| mapping.normalized_impl(db, visitor)) + .collect(); + Self::new(db, self.literal(db).normalized_impl(db, visitor), mappings) + } +} + +fn signature_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &CallableSignature<'db>, + _count: u32, + _function: FunctionType<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn signature_cycle_initial<'db>( + db: &'db dyn Db, + _function: FunctionType<'db>, +) -> CallableSignature<'db> { + CallableSignature::single(Signature::bottom(db)) +} + +/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might +/// have special behavior. +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString, strum_macros::IntoStaticStr, +)] +#[strum(serialize_all = "snake_case")] +#[cfg_attr(test, derive(strum_macros::EnumIter))] +pub enum KnownFunction { + /// `builtins.isinstance` + #[strum(serialize = "isinstance")] + IsInstance, + /// `builtins.issubclass` + #[strum(serialize = "issubclass")] + IsSubclass, + /// `builtins.hasattr` + #[strum(serialize = "hasattr")] + HasAttr, + /// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type` + RevealType, + /// `builtins.len` + Len, + /// `builtins.repr` + Repr, + /// `builtins.__import__`, which returns the top-level module. + #[strum(serialize = "__import__")] + DunderImport, + /// `importlib.import_module`, which returns the submodule. + ImportModule, + + /// `typing(_extensions).final` + Final, + + /// [`typing(_extensions).no_type_check`](https://typing.python.org/en/latest/spec/directives.html#no-type-check) + NoTypeCheck, + + /// `typing(_extensions).assert_type` + AssertType, + /// `typing(_extensions).assert_never` + AssertNever, + /// `typing(_extensions).cast` + Cast, + /// `typing(_extensions).overload` + Overload, + /// `typing(_extensions).override` + Override, + /// `typing(_extensions).is_protocol` + IsProtocol, + /// `typing(_extensions).get_protocol_members` + GetProtocolMembers, + /// `typing(_extensions).runtime_checkable` + RuntimeCheckable, + /// `typing(_extensions).dataclass_transform` + DataclassTransform, + + /// `abc.abstractmethod` + #[strum(serialize = "abstractmethod")] + AbstractMethod, + + /// `dataclasses.dataclass` + Dataclass, + + /// `inspect.getattr_static` + GetattrStatic, + + /// `ty_extensions.static_assert` + StaticAssert, + /// `ty_extensions.is_equivalent_to` + IsEquivalentTo, + /// `ty_extensions.is_subtype_of` + IsSubtypeOf, + /// `ty_extensions.is_assignable_to` + IsAssignableTo, + /// `ty_extensions.is_disjoint_from` + IsDisjointFrom, + /// `ty_extensions.is_singleton` + IsSingleton, + /// `ty_extensions.is_single_valued` + IsSingleValued, + /// `ty_extensions.generic_context` + GenericContext, + /// `ty_extensions.dunder_all_names` + DunderAllNames, + /// `ty_extensions.all_members` + AllMembers, + /// `ty_extensions.top_materialization` + TopMaterialization, + /// `ty_extensions.bottom_materialization` + BottomMaterialization, +} + +impl KnownFunction { + pub fn into_classinfo_constraint_function(self) -> Option { + match self { + Self::IsInstance => Some(ClassInfoConstraintFunction::IsInstance), + Self::IsSubclass => Some(ClassInfoConstraintFunction::IsSubclass), + _ => None, + } + } + + pub(crate) fn try_from_definition_and_name<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + name: &str, + ) -> Option { + let candidate = Self::from_str(name).ok()?; + candidate + .check_module(file_to_module(db, definition.file(db))?.known()?) + .then_some(candidate) + } + + /// Return `true` if `self` is defined in `module` at runtime. + const fn check_module(self, module: KnownModule) -> bool { + match self { + Self::IsInstance + | Self::IsSubclass + | Self::HasAttr + | Self::Len + | Self::Repr + | Self::DunderImport => module.is_builtins(), + Self::AssertType + | Self::AssertNever + | Self::Cast + | Self::Overload + | Self::Override + | Self::RevealType + | Self::Final + | Self::IsProtocol + | Self::GetProtocolMembers + | Self::RuntimeCheckable + | Self::DataclassTransform + | Self::NoTypeCheck => { + matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) + } + Self::AbstractMethod => { + matches!(module, KnownModule::Abc) + } + Self::Dataclass => { + matches!(module, KnownModule::Dataclasses) + } + Self::GetattrStatic => module.is_inspect(), + Self::IsAssignableTo + | Self::IsDisjointFrom + | Self::IsEquivalentTo + | Self::IsSingleValued + | Self::IsSingleton + | Self::IsSubtypeOf + | Self::TopMaterialization + | Self::BottomMaterialization + | Self::GenericContext + | Self::DunderAllNames + | Self::StaticAssert + | Self::AllMembers => module.is_ty_extensions(), + Self::ImportModule => module.is_importlib(), + } + } + + /// Evaluate a call to this known function, and emit any diagnostics that are necessary + /// as a result of the call. + pub(super) fn check_call<'db>( + self, + context: &InferContext<'db, '_>, + parameter_types: &[Option>], + call_expression: &ast::ExprCall, + file: File, + ) -> Option> { + let db = context.db(); + + match self { + KnownFunction::RevealType => { + let [Some(revealed_type)] = parameter_types else { + return None; + }; + let builder = + context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)?; + let mut diag = builder.into_diagnostic("Revealed type"); + let span = context.span(&call_expression.arguments.args[0]); + diag.annotate( + Annotation::primary(span) + .message(format_args!("`{}`", revealed_type.display(db))), + ); + None + } + KnownFunction::AssertType => { + let [Some(actual_ty), Some(asserted_ty)] = parameter_types else { + return None; + }; + + if actual_ty.is_equivalent_to(db, *asserted_ty) { + return None; + } + let builder = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)?; + + let mut diagnostic = builder.into_diagnostic(format_args!( + "Argument does not have asserted type `{}`", + asserted_ty.display(db), + )); + + diagnostic.annotate( + Annotation::secondary(context.span(&call_expression.arguments.args[0])) + .message(format_args!( + "Inferred type of argument is `{}`", + actual_ty.display(db), + )), + ); + + diagnostic.info(format_args!( + "`{asserted_type}` and `{inferred_type}` are not equivalent types", + asserted_type = asserted_ty.display(db), + inferred_type = actual_ty.display(db), + )); + + None + } + KnownFunction::AssertNever => { + let [Some(actual_ty)] = parameter_types else { + return None; + }; + if actual_ty.is_equivalent_to(db, Type::Never) { + return None; + } + let builder = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression)?; + + let mut diagnostic = + builder.into_diagnostic("Argument does not have asserted type `Never`"); + diagnostic.annotate( + Annotation::secondary(context.span(&call_expression.arguments.args[0])) + .message(format_args!( + "Inferred type of argument is `{}`", + actual_ty.display(db) + )), + ); + diagnostic.info(format_args!( + "`Never` and `{inferred_type}` are not equivalent types", + inferred_type = actual_ty.display(db), + )); + + None + } + KnownFunction::StaticAssert => { + let [Some(parameter_ty), message] = parameter_types else { + return None; + }; + let truthiness = match parameter_ty.try_bool(db) { + Ok(truthiness) => truthiness, + Err(err) => { + let condition = call_expression + .arguments + .find_argument("condition", 0) + .map(|argument| match argument { + ruff_python_ast::ArgOrKeyword::Arg(expr) => { + ast::AnyNodeRef::from(expr) + } + ruff_python_ast::ArgOrKeyword::Keyword(keyword) => { + ast::AnyNodeRef::from(keyword) + } + }) + .unwrap_or(ast::AnyNodeRef::from(call_expression)); + + err.report_diagnostic(context, condition); + + return None; + } + }; + + let builder = context.report_lint(&STATIC_ASSERT_ERROR, call_expression)?; + if truthiness.is_always_true() { + return None; + } + if let Some(message) = message + .and_then(Type::into_string_literal) + .map(|s| s.value(db)) + { + builder.into_diagnostic(format_args!("Static assertion error: {message}")); + } else if *parameter_ty == Type::BooleanLiteral(false) { + builder + .into_diagnostic("Static assertion error: argument evaluates to `False`"); + } else if truthiness.is_always_false() { + builder.into_diagnostic(format_args!( + "Static assertion error: argument of type `{parameter_ty}` \ + is statically known to be falsy", + parameter_ty = parameter_ty.display(db) + )); + } else { + builder.into_diagnostic(format_args!( + "Static assertion error: argument of type `{parameter_ty}` \ + has an ambiguous static truthiness", + parameter_ty = parameter_ty.display(db) + )); + } + + None + } + KnownFunction::Cast => { + let [Some(casted_type), Some(source_type)] = parameter_types else { + return None; + }; + let contains_unknown_or_todo = + |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any); + if source_type.is_equivalent_to(db, *casted_type) + && !any_over_type(db, *source_type, &contains_unknown_or_todo) + && !any_over_type(db, *casted_type, &contains_unknown_or_todo) + { + let builder = context.report_lint(&REDUNDANT_CAST, call_expression)?; + builder.into_diagnostic(format_args!( + "Value is already of type `{}`", + casted_type.display(db), + )); + } + None + } + KnownFunction::GetProtocolMembers => { + let [Some(Type::ClassLiteral(class))] = parameter_types else { + return None; + }; + if class.is_protocol(db) { + return None; + } + report_bad_argument_to_get_protocol_members(context, call_expression, *class); + None + } + KnownFunction::IsInstance | KnownFunction::IsSubclass => { + let [_, Some(Type::ClassLiteral(class))] = parameter_types else { + return None; + }; + let protocol_class = class.into_protocol_class(db)?; + if protocol_class.is_runtime_checkable(db) { + return None; + } + report_runtime_check_against_non_runtime_checkable_protocol( + context, + call_expression, + protocol_class, + self, + ); + None + } + known @ (KnownFunction::DunderImport | KnownFunction::ImportModule) => { + let [Some(Type::StringLiteral(full_module_name)), rest @ ..] = parameter_types + else { + return None; + }; + + if rest.iter().any(Option::is_some) { + return None; + } + + let module_name = full_module_name.value(db); + + if known == KnownFunction::DunderImport && module_name.contains('.') { + // `__import__("collections.abc")` returns the `collections` module. + // `importlib.import_module("collections.abc")` returns the `collections.abc` module. + // ty doesn't have a way to represent the return type of the former yet. + // https://github.com/astral-sh/ruff/pull/19008#discussion_r2173481311 + return None; + } + + let module_name = ModuleName::new(module_name)?; + let module = resolve_module(db, &module_name)?; + + Some(Type::module_literal(db, file, &module)) + } + + _ => None, + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use strum::IntoEnumIterator; + + use super::*; + use crate::db::tests::setup_db; + use crate::place::known_module_symbol; + + #[test] + fn known_function_roundtrip_from_str() { + let db = setup_db(); + + for function in KnownFunction::iter() { + let function_name: &'static str = function.into(); + + let module = match function { + KnownFunction::Len + | KnownFunction::Repr + | KnownFunction::IsInstance + | KnownFunction::HasAttr + | KnownFunction::IsSubclass + | KnownFunction::DunderImport => KnownModule::Builtins, + + KnownFunction::AbstractMethod => KnownModule::Abc, + + KnownFunction::Dataclass => KnownModule::Dataclasses, + + KnownFunction::GetattrStatic => KnownModule::Inspect, + + KnownFunction::Cast + | KnownFunction::Final + | KnownFunction::Overload + | KnownFunction::Override + | KnownFunction::RevealType + | KnownFunction::AssertType + | KnownFunction::AssertNever + | KnownFunction::IsProtocol + | KnownFunction::GetProtocolMembers + | KnownFunction::RuntimeCheckable + | KnownFunction::DataclassTransform + | KnownFunction::NoTypeCheck => KnownModule::TypingExtensions, + + KnownFunction::IsSingleton + | KnownFunction::IsSubtypeOf + | KnownFunction::GenericContext + | KnownFunction::DunderAllNames + | KnownFunction::StaticAssert + | KnownFunction::IsDisjointFrom + | KnownFunction::IsSingleValued + | KnownFunction::IsAssignableTo + | KnownFunction::IsEquivalentTo + | KnownFunction::TopMaterialization + | KnownFunction::BottomMaterialization + | KnownFunction::AllMembers => KnownModule::TyExtensions, + + KnownFunction::ImportModule => KnownModule::ImportLib, + }; + + let function_definition = known_module_symbol(&db, module, function_name) + .place + .expect_type() + .expect_function_literal() + .definition(&db); + + assert_eq!( + KnownFunction::try_from_definition_and_name( + &db, + function_definition, + function_name + ), + Some(function), + "The strum `EnumString` implementation appears to be incorrect for `{function_name}`" + ); + } + } +} diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs new file mode 100644 index 0000000000000..e6714330d9485 --- /dev/null +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -0,0 +1,815 @@ +use std::borrow::Cow; + +use ruff_python_ast as ast; +use rustc_hash::FxHashMap; + +use crate::semantic_index::SemanticIndex; +use crate::types::class::ClassType; +use crate::types::class_base::ClassBase; +use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType}; +use crate::types::signatures::{Parameter, Parameters, Signature}; +use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::{ + KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, + TypeVarInstance, TypeVarVariance, UnionType, declaration_type, +}; +use crate::{Db, FxOrderSet}; + +/// A list of formal type variables for a generic function, class, or type alias. +/// +/// TODO: Handle nested generic contexts better, with actual parent links to the lexically +/// containing context. +/// +/// # Ordering +/// Ordering is based on the context's salsa-assigned id and not on its values. +/// The id may change between runs, or when the context was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct GenericContext<'db> { + #[returns(ref)] + pub(crate) variables: FxOrderSet>, +} + +pub(super) fn walk_generic_context<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + context: GenericContext<'db>, + visitor: &mut V, +) { + for typevar in context.variables(db) { + visitor.visit_type_var_type(db, *typevar); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for GenericContext<'_> {} + +impl<'db> GenericContext<'db> { + /// Creates a generic context from a list of PEP-695 type parameters. + pub(crate) fn from_type_params( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + type_params_node: &ast::TypeParams, + ) -> Self { + let variables: FxOrderSet<_> = type_params_node + .iter() + .filter_map(|type_param| Self::variable_from_type_param(db, index, type_param)) + .collect(); + Self::new(db, variables) + } + + fn variable_from_type_param( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + type_param_node: &ast::TypeParam, + ) -> Option> { + match type_param_node { + ast::TypeParam::TypeVar(node) => { + let definition = index.expect_single_definition(node); + let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = + declaration_type(db, definition).inner_type() + else { + return None; + }; + Some(typevar) + } + // TODO: Support these! + ast::TypeParam::ParamSpec(_) => None, + ast::TypeParam::TypeVarTuple(_) => None, + } + } + + /// Creates a generic context from the legacy `TypeVar`s that appear in a function parameter + /// list. + pub(crate) fn from_function_params( + db: &'db dyn Db, + parameters: &Parameters<'db>, + return_type: Option>, + ) -> Option { + let mut variables = FxOrderSet::default(); + for param in parameters { + if let Some(ty) = param.annotated_type() { + ty.find_legacy_typevars(db, &mut variables); + } + if let Some(ty) = param.default_type() { + ty.find_legacy_typevars(db, &mut variables); + } + } + if let Some(ty) = return_type { + ty.find_legacy_typevars(db, &mut variables); + } + if variables.is_empty() { + return None; + } + Some(Self::new(db, variables)) + } + + /// Creates a generic context from the legacy `TypeVar`s that appear in class's base class + /// list. + pub(crate) fn from_base_classes( + db: &'db dyn Db, + bases: impl Iterator>, + ) -> Option { + let mut variables = FxOrderSet::default(); + for base in bases { + base.find_legacy_typevars(db, &mut variables); + } + if variables.is_empty() { + return None; + } + Some(Self::new(db, variables)) + } + + pub(crate) fn len(self, db: &'db dyn Db) -> usize { + self.variables(db).len() + } + + pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { + let parameters = Parameters::new( + self.variables(db) + .iter() + .map(|typevar| Self::parameter_from_typevar(db, *typevar)), + ); + Signature::new(parameters, None) + } + + fn parameter_from_typevar(db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Parameter<'db> { + let mut parameter = Parameter::positional_only(Some(typevar.name(db).clone())); + match typevar.bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + // TODO: This should be a type form. + parameter = parameter.with_annotated_type(bound); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + // TODO: This should be a new type variant where only these exact types are + // assignable, and not subclasses of them, nor a union of them. + parameter = parameter + .with_annotated_type(UnionType::from_elements(db, constraints.iter(db))); + } + None => {} + } + if let Some(default_ty) = typevar.default_ty(db) { + parameter = parameter.with_default_type(default_ty); + } + parameter + } + + pub(crate) fn default_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + self.specialize_partial(db, &vec![None; self.variables(db).len()]) + } + + pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + let types = self + .variables(db) + .iter() + .map(|typevar| Type::TypeVar(*typevar)) + .collect(); + self.specialize(db, types) + } + + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { + let types = vec![Type::unknown(); self.variables(db).len()]; + self.specialize(db, types.into()) + } + + pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool { + self.variables(db).is_subset(other.variables(db)) + } + + /// Creates a specialization of this generic context. Panics if the length of `types` does not + /// match the number of typevars in the generic context. You must provide a specific type for + /// each typevar; no defaults are used. (Use [`specialize_partial`](Self::specialize_partial) + /// if you might not have types for every typevar.) + pub(crate) fn specialize( + self, + db: &'db dyn Db, + types: Box<[Type<'db>]>, + ) -> Specialization<'db> { + assert!(self.variables(db).len() == types.len()); + Specialization::new(db, self, types, None) + } + + /// Creates a specialization of this generic context for the `tuple` class. + pub(crate) fn specialize_tuple( + self, + db: &'db dyn Db, + tuple: TupleType<'db>, + ) -> Specialization<'db> { + let element_type = UnionType::from_elements(db, tuple.tuple(db).all_elements()); + Specialization::new(db, self, Box::from([element_type]), Some(tuple)) + } + + /// Creates a specialization of this generic context. Panics if the length of `types` does not + /// match the number of typevars in the generic context. If any provided type is `None`, we + /// will use the corresponding typevar's default type. + pub(crate) fn specialize_partial( + self, + db: &'db dyn Db, + types: &[Option>], + ) -> Specialization<'db> { + let variables = self.variables(db); + assert!(variables.len() == types.len()); + + // Typevars can have other typevars as their default values, e.g. + // + // ```py + // class C[T, U = T]: ... + // ``` + // + // If there is a mapping for `T`, we want to map `U` to that type, not to `T`. To handle + // this, we repeatedly apply the specialization to itself, until we reach a fixed point. + let mut expanded = vec![Type::unknown(); types.len()]; + for (idx, (ty, typevar)) in types.iter().zip(variables).enumerate() { + if let Some(ty) = ty { + expanded[idx] = *ty; + continue; + } + + let Some(default) = typevar.default_ty(db) else { + continue; + }; + + // Typevars are only allowed to refer to _earlier_ typevars in their defaults. (This is + // statically enforced for PEP-695 contexts, and is explicitly called out as a + // requirement for legacy contexts.) + let partial = PartialSpecialization { + generic_context: self, + types: Cow::Borrowed(&expanded[0..idx]), + }; + let default = + default.apply_type_mapping(db, &TypeMapping::PartialSpecialization(partial)); + expanded[idx] = default; + } + + Specialization::new(db, self, expanded.into_boxed_slice(), None) + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + let variables: FxOrderSet<_> = self + .variables(db) + .iter() + .map(|ty| ty.normalized_impl(db, visitor)) + .collect(); + Self::new(db, variables) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum LegacyGenericBase { + Generic, + Protocol, +} + +impl LegacyGenericBase { + const fn as_str(self) -> &'static str { + match self { + Self::Generic => "Generic", + Self::Protocol => "Protocol", + } + } +} + +impl std::fmt::Display for LegacyGenericBase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// An assignment of a specific type to each type variable in a generic scope. +/// +/// TODO: Handle nested specializations better, with actual parent links to the specialization of +/// the lexically containing context. +#[salsa::interned(debug)] +pub struct Specialization<'db> { + pub(crate) generic_context: GenericContext<'db>, + #[returns(deref)] + pub(crate) types: Box<[Type<'db>]>, + + /// For specializations of `tuple`, we also store more detailed information about the tuple's + /// elements, above what the class's (single) typevar can represent. + tuple_inner: Option>, +} + +pub(super) fn walk_specialization<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + specialization: Specialization<'db>, + visitor: &mut V, +) { + walk_generic_context(db, specialization.generic_context(db), visitor); + for ty in specialization.types(db) { + visitor.visit_type(db, *ty); + } + if let Some(tuple) = specialization.tuple_inner(db) { + visitor.visit_tuple_type(db, tuple); + } +} + +impl<'db> Specialization<'db> { + /// Returns the tuple spec for a specialization of the `tuple` class. + pub(crate) fn tuple(self, db: &'db dyn Db) -> &'db TupleSpec<'db> { + if let Some(tuple) = self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) { + return tuple; + } + if let [element_type] = self.types(db) { + if let Some(tuple) = TupleType::new(db, TupleSpec::homogeneous(*element_type)) { + return tuple.tuple(db); + } + } + TupleType::new(db, TupleSpec::homogeneous(Type::unknown())) + .expect("tuple[Unknown, ...] should never contain Never") + .tuple(db) + } + + /// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this + /// mapping. + pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option> { + let index = self + .generic_context(db) + .variables(db) + .get_index_of(&typevar)?; + self.types(db).get(index).copied() + } + + /// Applies a specialization to this specialization. This is used, for instance, when a generic + /// class inherits from a generic alias: + /// + /// ```py + /// class A[T]: ... + /// class B[U](A[U]): ... + /// ``` + /// + /// `B` is a generic class, whose MRO includes the generic alias `A[U]`, which specializes `A` + /// with the specialization `{T: U}`. If `B` is specialized to `B[int]`, with specialization + /// `{U: int}`, we can apply the second specialization to the first, resulting in `T: int`. + /// That lets us produce the generic alias `A[int]`, which is the corresponding entry in the + /// MRO of `B[int]`. + pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self { + self.apply_type_mapping(db, &TypeMapping::Specialization(other)) + } + + pub(crate) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + let types: Box<[_]> = self + .types(db) + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)) + .collect(); + let tuple_inner = self + .tuple_inner(db) + .and_then(|tuple| tuple.apply_type_mapping(db, type_mapping)); + Specialization::new(db, self.generic_context(db), types, tuple_inner) + } + + /// Applies an optional specialization to this specialization. + pub(crate) fn apply_optional_specialization( + self, + db: &'db dyn Db, + other: Option>, + ) -> Self { + if let Some(other) = other { + self.apply_specialization(db, other) + } else { + self + } + } + + /// Combines two specializations of the same generic context. If either specialization maps a + /// typevar to `Type::Unknown`, the other specialization's mapping is used. If both map the + /// typevar to a known type, those types are unioned together. + /// + /// Panics if the two specializations are not for the same generic context. + pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { + let generic_context = self.generic_context(db); + assert!(other.generic_context(db) == generic_context); + // TODO special-casing Unknown to mean "no mapping" is not right here, and can give + // confusing/wrong results in cases where there was a mapping found for a typevar, and it + // was of type Unknown. We should probably add a bitset or similar to Specialization that + // explicitly tells us which typevars are mapped. + let types: Box<[_]> = self + .types(db) + .iter() + .zip(other.types(db)) + .map(|(self_type, other_type)| match (self_type, other_type) { + (unknown, known) | (known, unknown) if unknown.is_unknown() => *known, + _ => UnionType::from_elements(db, [self_type, other_type]), + }) + .collect(); + // TODO: Combine the tuple specs too + Specialization::new(db, self.generic_context(db), types, None) + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + let types: Box<[_]> = self + .types(db) + .iter() + .map(|ty| ty.normalized_impl(db, visitor)) + .collect(); + let tuple_inner = self + .tuple_inner(db) + .and_then(|tuple| tuple.normalized_impl(db, visitor)); + let context = self.generic_context(db).normalized_impl(db, visitor); + Self::new(db, context, types, tuple_inner) + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + let types: Box<[_]> = self + .generic_context(db) + .variables(db) + .into_iter() + .zip(self.types(db)) + .map(|(typevar, vartype)| { + let variance = match typevar.variance(db) { + TypeVarVariance::Invariant => TypeVarVariance::Invariant, + TypeVarVariance::Covariant => variance, + TypeVarVariance::Contravariant => variance.flip(), + TypeVarVariance::Bivariant => unreachable!(), + }; + vartype.materialize(db, variance) + }) + .collect(); + let tuple_inner = self.tuple_inner(db).and_then(|tuple| { + // Tuples are immutable, so tuple element types are always in covariant position. + tuple.materialize(db, variance) + }); + Specialization::new(db, self.generic_context(db), types, tuple_inner) + } + + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db)) + { + return self_tuple.has_relation_to(db, other_tuple, relation); + } + + for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if self_type.is_dynamic() || other_type.is_dynamic() { + match relation { + TypeRelation::Assignability => continue, + TypeRelation::Subtyping => return false, + } + } + + // Subtyping/assignability of each type in the specialization depends on the variance + // of the corresponding typevar: + // - covariant: verify that self_type <: other_type + // - contravariant: verify that other_type <: self_type + // - invariant: verify that self_type <: other_type AND other_type <: self_type + // - bivariant: skip, can't make subtyping/assignability false + let compatible = match typevar.variance(db) { + TypeVarVariance::Invariant => match relation { + TypeRelation::Subtyping => self_type.is_equivalent_to(db, *other_type), + TypeRelation::Assignability => { + self_type.is_assignable_to(db, *other_type) + && other_type.is_assignable_to(db, *self_type) + } + }, + TypeVarVariance::Covariant => self_type.has_relation_to(db, *other_type, relation), + TypeVarVariance::Contravariant => { + other_type.has_relation_to(db, *self_type, relation) + } + TypeVarVariance::Bivariant => true, + }; + if !compatible { + return false; + } + } + + true + } + + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + // Equivalence of each type in the specialization depends on the variance of the + // corresponding typevar: + // - covariant: verify that self_type == other_type + // - contravariant: verify that other_type == self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make equivalence false + let compatible = match typevar.variance(db) { + TypeVarVariance::Invariant + | TypeVarVariance::Covariant + | TypeVarVariance::Contravariant => self_type.is_equivalent_to(db, *other_type), + TypeVarVariance::Bivariant => true, + }; + if !compatible { + return false; + } + } + + true + } + + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for ty in self.types(db) { + ty.find_legacy_typevars(db, typevars); + } + } +} + +/// A mapping between type variables and types. +/// +/// You will usually use [`Specialization`] instead of this type. This type is used when we need to +/// substitute types for type variables before we have fully constructed a [`Specialization`]. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct PartialSpecialization<'a, 'db> { + generic_context: GenericContext<'db>, + types: Cow<'a, [Type<'db>]>, +} + +pub(super) fn walk_partial_specialization<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + specialization: &PartialSpecialization<'_, 'db>, + visitor: &mut V, +) { + walk_generic_context(db, specialization.generic_context, visitor); + for ty in &*specialization.types { + visitor.visit_type(db, *ty); + } +} + +impl<'db> PartialSpecialization<'_, 'db> { + /// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this + /// mapping. + pub(crate) fn get(&self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option> { + let index = self.generic_context.variables(db).get_index_of(&typevar)?; + self.types.get(index).copied() + } + + pub(crate) fn to_owned(&self) -> PartialSpecialization<'db, 'db> { + PartialSpecialization { + generic_context: self.generic_context, + types: Cow::from(self.types.clone().into_owned()), + } + } + + pub(crate) fn normalized_impl( + &self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> PartialSpecialization<'db, 'db> { + let generic_context = self.generic_context.normalized_impl(db, visitor); + let types: Cow<_> = self + .types + .iter() + .map(|ty| ty.normalized_impl(db, visitor)) + .collect(); + + PartialSpecialization { + generic_context, + types, + } + } +} + +/// Performs type inference between parameter annotations and argument types, producing a +/// specialization of a generic function. +pub(crate) struct SpecializationBuilder<'db> { + db: &'db dyn Db, + types: FxHashMap, Type<'db>>, +} + +impl<'db> SpecializationBuilder<'db> { + pub(crate) fn new(db: &'db dyn Db) -> Self { + Self { + db, + types: FxHashMap::default(), + } + } + + pub(crate) fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> { + let types: Box<[_]> = generic_context + .variables(self.db) + .iter() + .map(|variable| { + self.types + .get(variable) + .copied() + .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) + }) + .collect(); + // TODO Infer the tuple spec for a tuple type + Specialization::new(self.db, generic_context, types, None) + } + + fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) { + self.types + .entry(typevar) + .and_modify(|existing| { + *existing = UnionType::from_elements(self.db, [*existing, ty]); + }) + .or_insert(ty); + } + + pub(crate) fn infer( + &mut self, + formal: Type<'db>, + actual: Type<'db>, + ) -> Result<(), SpecializationError<'db>> { + // If the actual type is a subtype of the formal type, then return without adding any new + // type mappings. (Note that if the formal type contains any typevars, this check will + // fail, since no non-typevar types are assignable to a typevar. Also note that we are + // checking _subtyping_, not _assignability_, so that we do specialize typevars to dynamic + // argument types; and we have a special case for `Never`, which is a subtype of all types, + // but which we also do want as a specialization candidate.) + // + // In particular, this handles a case like + // + // ```py + // def f[T](t: T | None): ... + // + // f(None) + // ``` + // + // without specializing `T` to `None`. + if !matches!(formal, Type::ProtocolInstance(_)) + && !actual.is_never() + && actual.is_subtype_of(self.db, formal) + { + return Ok(()); + } + + match (formal, actual) { + (Type::TypeVar(typevar), ty) | (ty, Type::TypeVar(typevar)) => { + match typevar.bound_or_constraints(self.db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + if !ty.is_assignable_to(self.db, bound) { + return Err(SpecializationError::MismatchedBound { + typevar, + argument: ty, + }); + } + self.add_type_mapping(typevar, ty); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + for constraint in constraints.iter(self.db) { + if ty.is_assignable_to(self.db, *constraint) { + self.add_type_mapping(typevar, *constraint); + return Ok(()); + } + } + return Err(SpecializationError::MismatchedConstraint { + typevar, + argument: ty, + }); + } + _ => { + self.add_type_mapping(typevar, ty); + } + } + } + + (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { + let formal_tuple = formal_tuple.tuple(self.db); + let actual_tuple = actual_tuple.tuple(self.db); + match (formal_tuple, actual_tuple) { + (TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => { + if formal_tuple.len() == actual_tuple.len() { + for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) { + self.infer(*formal_element, *actual_element)?; + } + } + } + + // TODO: Infer specializations of variable-length tuples + (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) => {} + } + } + + ( + Type::NominalInstance(NominalInstanceType { + class: ClassType::Generic(formal_alias), + .. + }) + // TODO: This will only handle classes that explicit implement a generic protocol + // by listing it as a base class. To handle classes that implicitly implement a + // generic protocol, we will need to check the types of the protocol members to be + // able to infer the specialization of the protocol that the class implements. + | Type::ProtocolInstance(ProtocolInstanceType { + inner: Protocol::FromClass(ClassType::Generic(formal_alias)), + .. + }), + Type::NominalInstance(NominalInstanceType { + class: actual_class, + .. + }), + ) => { + let formal_origin = formal_alias.origin(self.db); + for base in actual_class.iter_mro(self.db) { + let ClassBase::Class(ClassType::Generic(base_alias)) = base else { + continue; + }; + if formal_origin != base_alias.origin(self.db) { + continue; + } + let formal_specialization = formal_alias.specialization(self.db).types(self.db); + let base_specialization = base_alias.specialization(self.db).types(self.db); + for (formal_ty, base_ty) in + formal_specialization.iter().zip(base_specialization) + { + self.infer(*formal_ty, *base_ty)?; + } + return Ok(()); + } + } + + (Type::Union(formal), _) => { + // TODO: We haven't implemented a full unification solver yet. If typevars appear + // in multiple union elements, we ideally want to express that _only one_ of them + // needs to match, and that we should infer the smallest type mapping that allows + // that. + // + // For now, we punt on handling multiple typevar elements. Instead, if _precisely + // one_ union element _is_ a typevar (not _contains_ a typevar), then we go ahead + // and add a mapping between that typevar and the actual type. (Note that we've + // already handled above the case where the actual is assignable to a _non-typevar_ + // union element.) + let mut typevars = formal.iter(self.db).filter_map(|ty| match ty { + Type::TypeVar(typevar) => Some(*typevar), + _ => None, + }); + let typevar = typevars.next(); + let additional_typevars = typevars.next(); + if let (Some(typevar), None) = (typevar, additional_typevars) { + self.add_type_mapping(typevar, actual); + } + } + + (Type::Intersection(formal), _) => { + // The actual type must be assignable to every (positive) element of the + // formal intersection, so we must infer type mappings for each of them. (The + // actual type must also be disjoint from every negative element of the + // intersection, but that doesn't help us infer any type mappings.) + for positive in formal.iter_positive(self.db) { + self.infer(positive, actual)?; + } + } + + // TODO: Add more forms that we can structurally induct into: type[C], callables + _ => {} + } + + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum SpecializationError<'db> { + MismatchedBound { + typevar: TypeVarInstance<'db>, + argument: Type<'db>, + }, + MismatchedConstraint { + typevar: TypeVarInstance<'db>, + argument: Type<'db>, + }, +} + +impl<'db> SpecializationError<'db> { + pub(crate) fn typevar(&self) -> TypeVarInstance<'db> { + match self { + Self::MismatchedBound { typevar, .. } => *typevar, + Self::MismatchedConstraint { typevar, .. } => *typevar, + } + } + + pub(crate) fn argument_type(&self) -> Type<'db> { + match self { + Self::MismatchedBound { argument, .. } => *argument, + Self::MismatchedConstraint { argument, .. } => *argument, + } + } +} diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs new file mode 100644 index 0000000000000..3df5a4bb914d4 --- /dev/null +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -0,0 +1,429 @@ +use std::cmp::Ordering; + +use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations}; +use crate::semantic_index::definition::Definition; +use crate::semantic_index::definition::DefinitionKind; +use crate::semantic_index::place::ScopeId; +use crate::semantic_index::{ + attribute_scopes, global_scope, place_table, semantic_index, use_def_map, +}; +use crate::types::call::CallArguments; +use crate::types::signatures::Signature; +use crate::types::{ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type}; +use crate::{Db, HasType, NameKind, SemanticModel}; +use ruff_db::files::File; +use ruff_python_ast as ast; +use ruff_python_ast::name::Name; +use ruff_text_size::TextRange; +use rustc_hash::FxHashSet; + +pub(crate) fn all_declarations_and_bindings<'db>( + db: &'db dyn Db, + scope_id: ScopeId<'db>, +) -> impl Iterator> + 'db { + let use_def_map = use_def_map(db, scope_id); + let table = place_table(db, scope_id); + + use_def_map + .all_end_of_scope_declarations() + .filter_map(move |(symbol_id, declarations)| { + place_from_declarations(db, declarations) + .ok() + .and_then(|result| { + result.place.ignore_possibly_unbound().and_then(|ty| { + table + .place_expr(symbol_id) + .as_name() + .cloned() + .map(|name| Member { name, ty }) + }) + }) + }) + .chain( + use_def_map + .all_end_of_scope_bindings() + .filter_map(move |(symbol_id, bindings)| { + place_from_bindings(db, bindings) + .ignore_possibly_unbound() + .and_then(|ty| { + table + .place_expr(symbol_id) + .as_name() + .cloned() + .map(|name| Member { name, ty }) + }) + }), + ) +} + +struct AllMembers<'db> { + members: FxHashSet>, +} + +impl<'db> AllMembers<'db> { + fn of(db: &'db dyn Db, ty: Type<'db>) -> Self { + let mut all_members = Self { + members: FxHashSet::default(), + }; + all_members.extend_with_type(db, ty); + all_members + } + + fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { + match ty { + Type::Union(union) => self.members.extend( + union + .elements(db) + .iter() + .map(|ty| AllMembers::of(db, *ty).members) + .reduce(|acc, members| acc.intersection(&members).cloned().collect()) + .unwrap_or_default(), + ), + + Type::Intersection(intersection) => self.members.extend( + intersection + .positive(db) + .iter() + .map(|ty| AllMembers::of(db, *ty).members) + .reduce(|acc, members| acc.union(&members).cloned().collect()) + .unwrap_or_default(), + ), + + Type::NominalInstance(instance) => { + let (class_literal, _specialization) = instance.class.class_literal(db); + self.extend_with_instance_members(db, class_literal); + } + + Type::ClassLiteral(class_literal) => { + self.extend_with_class_members(db, class_literal); + + if let Type::ClassLiteral(meta_class_literal) = ty.to_meta_type(db) { + self.extend_with_class_members(db, meta_class_literal); + } + } + + Type::GenericAlias(generic_alias) => { + let class_literal = generic_alias.origin(db); + self.extend_with_class_members(db, class_literal); + } + + Type::SubclassOf(subclass_of_type) => { + if let Some(class_literal) = subclass_of_type.subclass_of().into_class() { + self.extend_with_class_members(db, class_literal.class_literal(db).0); + } + } + + Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy | Type::AlwaysFalsy => {} + + Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::LiteralString + | Type::Tuple(_) + | Type::PropertyInstance(_) + | Type::FunctionLiteral(_) + | Type::BoundMethod(_) + | Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::Callable(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) + | Type::TypeIs(_) => match ty.to_meta_type(db) { + Type::ClassLiteral(class_literal) => { + self.extend_with_class_members(db, class_literal); + } + Type::GenericAlias(generic_alias) => { + let class_literal = generic_alias.origin(db); + self.extend_with_class_members(db, class_literal); + } + _ => {} + }, + + Type::ModuleLiteral(literal) => { + self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); + let module = literal.module(db); + + let Some(file) = module.file() else { + return; + }; + + let module_scope = global_scope(db, file); + let use_def_map = use_def_map(db, module_scope); + let place_table = place_table(db, module_scope); + + for (symbol_id, _) in use_def_map.all_end_of_scope_declarations() { + let Some(symbol_name) = place_table.place_expr(symbol_id).as_name() else { + continue; + }; + let Place::Type(ty, _) = imported_symbol(db, file, symbol_name, None).place + else { + continue; + }; + + // Filter private symbols from stubs if they appear to be internal types + let is_stub_file = file.path(db).extension() == Some("pyi"); + let is_private_symbol = match NameKind::classify(symbol_name) { + NameKind::Dunder | NameKind::Normal => false, + NameKind::Sunder => true, + }; + if is_private_symbol && is_stub_file { + match ty { + Type::NominalInstance(instance) + if matches!( + instance.class.known(db), + Some( + KnownClass::TypeVar + | KnownClass::TypeVarTuple + | KnownClass::ParamSpec + ) + ) => + { + continue; + } + Type::ClassLiteral(class) if class.is_protocol(db) => continue, + Type::KnownInstance( + KnownInstanceType::TypeVar(_) | KnownInstanceType::TypeAliasType(_), + ) => continue, + _ => {} + } + } + + self.members.insert(Member { + name: place_table.place_expr(symbol_id).expect_name().clone(), + ty, + }); + } + + self.members + .extend(literal.available_submodule_attributes(db).filter_map( + |submodule_name| { + let ty = literal.resolve_submodule(db, &submodule_name)?; + let name = submodule_name.clone(); + Some(Member { name, ty }) + }, + )); + } + } + } + + fn extend_with_class_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) { + for parent in class_literal + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .map(|class| class.class_literal(db).0) + { + let parent_ty = Type::ClassLiteral(parent); + let parent_scope = parent.body_scope(db); + for Member { name, .. } in all_declarations_and_bindings(db, parent_scope) { + let result = parent_ty.member(db, name.as_str()); + let Some(ty) = result.place.ignore_possibly_unbound() else { + continue; + }; + self.members.insert(Member { name, ty }); + } + } + } + + fn extend_with_instance_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) { + for parent in class_literal + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .map(|class| class.class_literal(db).0) + { + let parent_instance = Type::instance(db, parent.default_specialization(db)); + let class_body_scope = parent.body_scope(db); + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + for function_scope_id in attribute_scopes(db, class_body_scope) { + let place_table = index.place_table(function_scope_id); + for place_expr in place_table.places() { + let Some(name) = place_expr.as_instance_attribute() else { + continue; + }; + let result = parent_instance.member(db, name.as_str()); + let Some(ty) = result.place.ignore_possibly_unbound() else { + continue; + }; + self.members.insert(Member { + name: name.clone(), + ty, + }); + } + } + + // This is very similar to `extend_with_class_members`, + // but uses the type of the class instance to query the + // class member. This gets us the right type for each + // member, e.g., `SomeClass.__delattr__` is not a bound + // method, but `instance_of_SomeClass.__delattr__` is. + for Member { name, .. } in all_declarations_and_bindings(db, class_body_scope) { + let result = parent_instance.member(db, name.as_str()); + let Some(ty) = result.place.ignore_possibly_unbound() else { + continue; + }; + self.members.insert(Member { name, ty }); + } + } + } +} + +/// A member of a type. +/// +/// This represents a single item in (ideally) the list returned by +/// `dir(object)`. +/// +/// The equality, comparison and hashing traits implemented for +/// this type are done so by taking only the name into account. At +/// present, this is because we assume the name is enough to uniquely +/// identify each attribute on an object. This is perhaps complicated +/// by overloads, but they only get represented by one member for +/// now. Moreover, it is convenient to be able to sort collections of +/// members, and a `Type` currently (as of 2025-07-09) has no way to do +/// ordered comparisons. +#[derive(Clone, Debug)] +pub struct Member<'db> { + pub name: Name, + pub ty: Type<'db>, +} + +impl std::hash::Hash for Member<'_> { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl Eq for Member<'_> {} + +impl<'db> PartialEq for Member<'db> { + fn eq(&self, rhs: &Member<'db>) -> bool { + self.name == rhs.name + } +} + +impl<'db> Ord for Member<'db> { + fn cmp(&self, rhs: &Member<'db>) -> Ordering { + self.name.cmp(&rhs.name) + } +} + +impl<'db> PartialOrd for Member<'db> { + fn partial_cmp(&self, rhs: &Member<'db>) -> Option { + Some(self.cmp(rhs)) + } +} + +/// List all members of a given type: anything that would be valid when accessed +/// as an attribute on an object of the given type. +pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { + AllMembers::of(db, ty).members +} + +/// Get the primary definition kind for a name expression within a specific file. +/// Returns the first definition kind that is reachable for this name in its scope. +/// This is useful for IDE features like semantic tokens. +pub fn definition_kind_for_name<'db>( + db: &'db dyn Db, + file: File, + name: &ast::ExprName, +) -> Option> { + let index = semantic_index(db, file); + let name_str = name.id.as_str(); + + // Get the scope for this name expression + let file_scope = index.try_expression_scope_id(&ast::Expr::Name(name.clone()))?; + + // Get the place table for this scope + let place_table = index.place_table(file_scope); + + // Look up the place by name + let place_id = place_table.place_id_by_name(name_str)?; + + // Get the use-def map and look up definitions for this place + let use_def_map = index.use_def_map(file_scope); + let declarations = use_def_map.all_reachable_declarations(place_id); + + // Find the first valid definition and return its kind + for declaration in declarations { + if let Some(def) = declaration.declaration.definition() { + return Some(def.kind(db).clone()); + } + } + + None +} + +/// Details about a callable signature for IDE support. +#[derive(Debug, Clone)] +pub struct CallSignatureDetails<'db> { + /// The signature itself + pub signature: Signature<'db>, + + /// The display label for this signature (e.g., "(param1: str, param2: int) -> str") + pub label: String, + + /// Label offsets for each parameter in the signature string. + /// Each range specifies the start position and length of a parameter label + /// within the full signature string. + pub parameter_label_offsets: Vec, + + /// The names of the parameters in the signature, in order. + /// This provides easy access to parameter names for documentation lookup. + pub parameter_names: Vec, + + /// The definition where this callable was originally defined (useful for + /// extracting docstrings). + pub definition: Option>, + + /// Mapping from argument indices to parameter indices. This helps + /// determine which parameter corresponds to which argument position. + pub argument_to_parameter_mapping: Vec>, +} + +/// Extract signature details from a function call expression. +/// This function analyzes the callable being invoked and returns zero or more +/// `CallSignatureDetails` objects, each representing one possible signature +/// (in case of overloads or union types). +pub fn call_signature_details<'db>( + db: &'db dyn Db, + file: File, + call_expr: &ast::ExprCall, +) -> Vec> { + let model = SemanticModel::new(db, file); + let func_type = call_expr.func.inferred_type(&model); + + // Use into_callable to handle all the complex type conversions + if let Some(callable_type) = func_type.into_callable(db) { + let call_arguments = CallArguments::from_arguments(&call_expr.arguments); + let bindings = callable_type.bindings(db).match_parameters(&call_arguments); + + // Extract signature details from all callable bindings + bindings + .into_iter() + .flat_map(std::iter::IntoIterator::into_iter) + .map(|binding| { + let signature = &binding.signature; + let display_details = signature.display(db).to_string_parts(); + let parameter_label_offsets = display_details.parameter_ranges.clone(); + let parameter_names = display_details.parameter_names.clone(); + + CallSignatureDetails { + signature: signature.clone(), + label: display_details.label, + parameter_label_offsets, + parameter_names, + definition: signature.definition(), + argument_to_parameter_mapping: binding.argument_to_parameter_mapping().to_vec(), + } + }) + .collect() + } else { + // Type is not callable, return empty signatures + vec![] + } +} diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs new file mode 100644 index 0000000000000..fff33b7826a92 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -0,0 +1,10619 @@ +//! We have Salsa queries for inferring types at three different granularities: scope-level, +//! definition-level, and expression-level. +//! +//! Scope-level inference is for when we are actually checking a file, and need to check types for +//! everything in that file's scopes, or give a linter access to types of arbitrary expressions +//! (via the [`HasType`](crate::semantic_model::HasType) trait). +//! +//! Definition-level inference allows us to look up the types of places in other scopes (e.g. for +//! imports) with the minimum inference necessary, so that if we're looking up one place from a +//! very large module, we can avoid a bunch of unnecessary work. Definition-level inference also +//! allows us to handle import cycles without getting into a cycle of scope-level inference +//! queries. +//! +//! The expression-level inference query is needed in only a few cases. Since some assignments can +//! have multiple targets (via `x = y = z` or unpacking `(x, y) = z`, they can be associated with +//! multiple definitions (one per assigned place). In order to avoid inferring the type of the +//! right-hand side once per definition, we infer it as a standalone query, so its result will be +//! cached by Salsa. We also need the expression-level query for inferring types in type guard +//! expressions (e.g. the test clause of an `if` statement.) +//! +//! Inferring types at any of the three region granularities returns a [`TypeInference`], which +//! holds types for every [`Definition`] and expression within the inferred region. +//! +//! Some type expressions can require deferred evaluation. This includes all type expressions in +//! stub files, or annotation expressions in modules with `from __future__ import annotations`, or +//! stringified annotations. We have a fourth Salsa query for inferring the deferred types +//! associated with a particular definition. Scope-level inference infers deferred types for all +//! definitions once the rest of the types in the scope have been inferred. +//! +//! Many of our type inference Salsa queries implement cycle recovery via fixed-point iteration. In +//! general, they initiate fixed-point iteration by returning a `TypeInference` that returns +//! `Type::Never` for all expressions, bindings, and declarations, and then they continue iterating +//! 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 itertools::{Either, Itertools}; +use ruff_db::files::File; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_python_ast::visitor::{Visitor, walk_expr}; +use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, PythonVersion}; +use ruff_python_stdlib::builtins::version_builtin_was_added; +use ruff_text_size::{Ranged, TextRange}; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa; +use salsa::plumbing::AsId; + +use super::context::{InNoTypeCheck, InferContext}; +use super::diagnostic::{ + INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, SUBCLASS_OF_FINAL_CLASS, + hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, + report_duplicate_bases, report_index_out_of_bounds, report_invalid_exception_caught, + report_invalid_exception_cause, report_invalid_exception_raised, + report_invalid_or_unsupported_base, report_invalid_type_checking_constant, + report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero, +}; +use super::generics::LegacyGenericBase; +use super::string_annotation::{ + BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, +}; +use super::subclass_of::SubclassOfInner; +use super::{ClassBase, NominalInstanceType, add_inferred_python_version_hint_to_diagnostic}; +use crate::module_name::{ModuleName, ModuleNameResolutionError}; +use crate::module_resolver::resolve_module; +use crate::node_key::NodeKey; +use crate::place::{ + Boundness, ConsideredDefinitions, LookupError, Place, PlaceAndQualifiers, + builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol, + module_type_implicit_global_declaration, module_type_implicit_global_symbol, place, + place_from_bindings, place_from_declarations, typing_extensions_symbol, +}; +use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; +use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId}; +use crate::semantic_index::definition::{ + AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, + Definition, DefinitionKind, DefinitionNodeKey, DefinitionState, ExceptHandlerDefinitionKind, + ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind, +}; +use crate::semantic_index::expression::{Expression, ExpressionKind}; +use crate::semantic_index::narrowing_constraints::ConstraintKey; +use crate::semantic_index::place::{ + FileScopeId, NodeWithScopeKind, NodeWithScopeRef, PlaceExpr, ScopeId, ScopeKind, ScopedPlaceId, +}; +use crate::semantic_index::{ + ApplicableConstraints, EagerSnapshotResult, SemanticIndex, place_table, semantic_index, +}; +use crate::types::call::{Binding, Bindings, CallArgumentTypes, CallArguments, CallError}; +use crate::types::class::{CodeGeneratorKind, MetaclassErrorKind, SliceLiteral}; +use crate::types::diagnostic::{ + self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, + CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, + INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, + INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, + INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, + POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, + UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, + UNSUPPORTED_OPERATOR, report_implicit_return_type, report_instance_layout_conflict, + report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated, + report_invalid_arguments_to_callable, report_invalid_assignment, + report_invalid_attribute_assignment, report_invalid_generator_function_return_type, + report_invalid_return_type, report_possibly_unbound_attribute, +}; +use crate::types::function::{ + FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, +}; +use crate::types::generics::GenericContext; +use crate::types::mro::MroErrorKind; +use crate::types::signatures::{CallableSignature, Signature}; +use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::unpacker::{UnpackResult, Unpacker}; +use crate::types::{ + CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, + IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, + MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, + Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, + todo_type, +}; +use crate::unpack::{Unpack, UnpackPosition}; +use crate::util::diagnostics::format_enumeration; +use crate::util::subscript::{PyIndex, PySlice}; +use crate::{Db, FxOrderSet, Program}; + +/// 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 +/// scope. +#[salsa::tracked(returns(ref), cycle_fn=scope_cycle_recover, cycle_initial=scope_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> TypeInference<'db> { + let file = scope.file(db); + let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?file).entered(); + + let module = parsed_module(db, file).load(db); + + // Using the index here is fine because the code below depends on the AST anyway. + // The isolation of the query is by the return inferred types. + let index = semantic_index(db, file); + + TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope), index, &module).finish() +} + +fn scope_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &TypeInference<'db>, + _count: u32, + _scope: ScopeId<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> TypeInference<'db> { + TypeInference::cycle_fallback(scope, Type::Never) +} + +/// Infer all types for a [`Definition`] (including sub-expressions). +/// Use when resolving a place use or public type of a place. +#[salsa::tracked(returns(ref), cycle_fn=definition_cycle_recover, cycle_initial=definition_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn infer_definition_types<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> TypeInference<'db> { + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let _span = tracing::trace_span!( + "infer_definition_types", + range = ?definition.kind(db).target_range(&module), + ?file + ) + .entered(); + + let index = semantic_index(db, file); + + TypeInferenceBuilder::new(db, InferenceRegion::Definition(definition), index, &module).finish() +} + +fn definition_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &TypeInference<'db>, + _count: u32, + _definition: Definition<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn definition_cycle_initial<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> TypeInference<'db> { + TypeInference::cycle_fallback(definition.scope(db), Type::Never) +} + +/// Infer types for all deferred type expressions in a [`Definition`]. +/// +/// Deferred expressions are type expressions (annotations, base classes, aliases...) in a stub +/// file, or in a file with `from __future__ import annotations`, or stringified annotations. +#[salsa::tracked(returns(ref), cycle_fn=deferred_cycle_recover, cycle_initial=deferred_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn infer_deferred_types<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> TypeInference<'db> { + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let _span = tracing::trace_span!( + "infer_deferred_types", + definition = ?definition.as_id(), + range = ?definition.kind(db).target_range(&module), + ?file + ) + .entered(); + + let index = semantic_index(db, file); + + TypeInferenceBuilder::new(db, InferenceRegion::Deferred(definition), index, &module).finish() +} + +fn deferred_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &TypeInference<'db>, + _count: u32, + _definition: Definition<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn deferred_cycle_initial<'db>(db: &'db dyn Db, definition: Definition<'db>) -> TypeInference<'db> { + TypeInference::cycle_fallback(definition.scope(db), Type::Never) +} + +/// Infer all types for an [`Expression`] (including sub-expressions). +/// Use rarely; only for cases where we'd otherwise risk double-inferring an expression: RHS of an +/// assignment, which might be unpacking/multi-target and thus part of multiple definitions, or a +/// type narrowing guard expression (e.g. if statement test node). +#[salsa::tracked(returns(ref), cycle_fn=expression_cycle_recover, cycle_initial=expression_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn infer_expression_types<'db>( + db: &'db dyn Db, + expression: Expression<'db>, +) -> TypeInference<'db> { + let file = expression.file(db); + let module = parsed_module(db, file).load(db); + let _span = tracing::trace_span!( + "infer_expression_types", + expression = ?expression.as_id(), + range = ?expression.node_ref(db, &module).range(), + ?file + ) + .entered(); + + let index = semantic_index(db, file); + + TypeInferenceBuilder::new(db, InferenceRegion::Expression(expression), index, &module).finish() +} + +fn expression_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &TypeInference<'db>, + _count: u32, + _expression: Expression<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn expression_cycle_initial<'db>( + db: &'db dyn Db, + expression: Expression<'db>, +) -> TypeInference<'db> { + TypeInference::cycle_fallback(expression.scope(db), Type::Never) +} + +/// Infers the type of an `expression` that is guaranteed to be in the same file as the calling query. +/// +/// This is a small helper around [`infer_expression_types()`] to reduce the boilerplate. +/// Use [`infer_expression_type()`] if it isn't guaranteed that `expression` is in the same file to +/// avoid cross-file query dependencies. +pub(super) fn infer_same_file_expression_type<'db>( + db: &'db dyn Db, + expression: Expression<'db>, + parsed: &ParsedModuleRef, +) -> Type<'db> { + let inference = infer_expression_types(db, expression); + inference.expression_type(expression.node_ref(db, parsed)) +} + +/// Infers the type of an expression where the expression might come from another file. +/// +/// Use this over [`infer_expression_types`] if the expression might come from another file than the +/// enclosing query to avoid cross-file query dependencies. +/// +/// Use [`infer_same_file_expression_type`] if it is guaranteed that `expression` is in the same +/// to avoid unnecessary salsa ingredients. This is normally the case inside the `TypeInferenceBuilder`. +#[salsa::tracked(cycle_fn=single_expression_cycle_recover, cycle_initial=single_expression_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(crate) fn infer_expression_type<'db>( + db: &'db dyn Db, + expression: Expression<'db>, +) -> Type<'db> { + let file = expression.file(db); + let module = parsed_module(db, file).load(db); + + // It's okay to call the "same file" version here because we're inside a salsa query. + infer_same_file_expression_type(db, expression, &module) +} + +fn single_expression_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Type<'db>, + _count: u32, + _expression: Expression<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn single_expression_cycle_initial<'db>( + _db: &'db dyn Db, + _expression: Expression<'db>, +) -> Type<'db> { + Type::Never +} + +/// Infer the types for an [`Unpack`] operation. +/// +/// This infers the expression type and performs structural match against the target expression +/// involved in an unpacking operation. It returns a result-like object that can be used to get the +/// type of the variables involved in this unpacking along with any violations that are detected +/// during this unpacking. +#[salsa::tracked(returns(ref), cycle_fn=unpack_cycle_recover, cycle_initial=unpack_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> { + let file = unpack.file(db); + let module = parsed_module(db, file).load(db); + let _span = tracing::trace_span!("infer_unpack_types", range=?unpack.range(db, &module), ?file) + .entered(); + + let mut unpacker = Unpacker::new(db, unpack.target_scope(db), &module); + unpacker.unpack(unpack.target(db, &module), unpack.value(db)); + unpacker.finish() +} + +fn unpack_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &UnpackResult<'db>, + _count: u32, + _unpack: Unpack<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn unpack_cycle_initial<'db>(_db: &'db dyn Db, _unpack: Unpack<'db>) -> UnpackResult<'db> { + UnpackResult::cycle_fallback(Type::Never) +} + +/// Returns the type of the nearest enclosing class for the given scope. +/// +/// This function walks up the ancestor scopes starting from the given scope, +/// and finds the closest class definition. This is different to the behaviour of +/// [`TypeInferenceBuilder::class_context_of_current_method`], which will only return +/// `Some(class)` if either the immediate parent scope is a class OR the immediate parent +/// scope is a type-parameters scope and the grandparent scope is a class. +/// +/// Returns `None` if no enclosing class is found. +pub(crate) fn nearest_enclosing_class<'db>( + db: &'db dyn Db, + semantic: &SemanticIndex<'db>, + scope: ScopeId, + parsed: &ParsedModuleRef, +) -> Option> { + semantic + .ancestor_scopes(scope.file_scope_id(db)) + .find_map(|(_, ancestor_scope)| { + let class = ancestor_scope.node().as_class(parsed)?; + let definition = semantic.expect_single_definition(class); + infer_definition_types(db, definition) + .declaration_type(definition) + .inner_type() + .into_class_literal() + }) +} + +/// A region within which we can infer types. +#[derive(Copy, Clone, Debug)] +pub(crate) enum InferenceRegion<'db> { + /// infer types for a standalone [`Expression`] + Expression(Expression<'db>), + /// infer types for a [`Definition`] + Definition(Definition<'db>), + /// infer deferred types for a [`Definition`] + Deferred(Definition<'db>), + /// infer types for an entire [`ScopeId`] + Scope(ScopeId<'db>), +} + +impl<'db> InferenceRegion<'db> { + fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + match self { + InferenceRegion::Expression(expression) => expression.scope(db), + InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => { + definition.scope(db) + } + InferenceRegion::Scope(scope) => scope, + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +struct TypeAndRange<'db> { + ty: Type<'db>, + range: TextRange, +} + +/// The inferred types for a single region. +#[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)] +pub(crate) struct TypeInference<'db> { + /// The types of every expression in this region. + expressions: FxHashMap>, + + /// The types of every binding in this region. + bindings: FxHashMap, Type<'db>>, + + /// The types and type qualifiers of every declaration in this region. + declarations: FxHashMap, TypeAndQualifiers<'db>>, + + /// The definitions that are deferred. + deferred: FxHashSet>, + + /// The diagnostics for this region. + diagnostics: TypeCheckDiagnostics, + + /// The scope this region is part of. + scope: ScopeId<'db>, + + /// The fallback type for missing expressions/bindings/declarations. + /// + /// This is used only when constructing a cycle-recovery `TypeInference`. + cycle_fallback_type: Option>, +} + +impl<'db> TypeInference<'db> { + pub(crate) fn empty(scope: ScopeId<'db>) -> Self { + Self { + expressions: FxHashMap::default(), + bindings: FxHashMap::default(), + declarations: FxHashMap::default(), + deferred: FxHashSet::default(), + diagnostics: TypeCheckDiagnostics::default(), + scope, + cycle_fallback_type: None, + } + } + + fn cycle_fallback(scope: ScopeId<'db>, cycle_fallback_type: Type<'db>) -> Self { + Self { + expressions: FxHashMap::default(), + bindings: FxHashMap::default(), + declarations: FxHashMap::default(), + deferred: FxHashSet::default(), + diagnostics: TypeCheckDiagnostics::default(), + scope, + cycle_fallback_type: Some(cycle_fallback_type), + } + } + + #[track_caller] + pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + self.try_expression_type(expression).expect( + "Failed to retrieve the inferred type for an `ast::Expr` node \ + passed to `TypeInference::expression_type()`. The `TypeInferenceBuilder` \ + should infer and store types for all `ast::Expr` nodes in any `TypeInference` \ + region it analyzes.", + ) + } + + pub(crate) fn try_expression_type( + &self, + expression: impl Into, + ) -> Option> { + self.expressions + .get(&expression.into()) + .copied() + .or(self.cycle_fallback_type) + } + + #[track_caller] + pub(crate) fn binding_type(&self, definition: Definition<'db>) -> Type<'db> { + self.bindings + .get(&definition) + .copied() + .or(self.cycle_fallback_type) + .expect( + "definition should belong to this TypeInference region and \ + TypeInferenceBuilder should have inferred a type for it", + ) + } + + #[track_caller] + pub(crate) fn declaration_type(&self, definition: Definition<'db>) -> TypeAndQualifiers<'db> { + self.declarations + .get(&definition) + .copied() + .or(self.cycle_fallback_type.map(Into::into)) + .expect( + "definition should belong to this TypeInference region and \ + TypeInferenceBuilder should have inferred a type for it", + ) + } + + pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { + &self.diagnostics + } + + fn shrink_to_fit(&mut self) { + self.expressions.shrink_to_fit(); + self.bindings.shrink_to_fit(); + self.declarations.shrink_to_fit(); + self.diagnostics.shrink_to_fit(); + self.deferred.shrink_to_fit(); + } +} + +/// Whether the intersection type is on the left or right side of the comparison. +#[derive(Debug, Clone, Copy)] +enum IntersectionOn { + Left, + Right, +} + +/// A helper to track if we already know that declared and inferred types are the same. +#[derive(Debug, Clone, PartialEq, Eq)] +enum DeclaredAndInferredType<'db> { + /// We know that both the declared and inferred types are the same. + AreTheSame(Type<'db>), + /// Declared and inferred types might be different, we need to check assignability. + MightBeDifferent { + declared_ty: TypeAndQualifiers<'db>, + inferred_ty: Type<'db>, + }, +} + +/// Builder to infer all types in a region. +/// +/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling +/// [`finish()`](TypeInferenceBuilder::finish) on it, which returns the resulting +/// [`TypeInference`]. +/// +/// There are a few different kinds of methods in the type inference builder, and the naming +/// distinctions are a bit subtle. +/// +/// The `finish` method calls [`infer_region`](TypeInferenceBuilder::infer_region), which delegates +/// to one of [`infer_region_scope`](TypeInferenceBuilder::infer_region_scope), +/// [`infer_region_definition`](TypeInferenceBuilder::infer_region_definition), or +/// [`infer_region_expression`](TypeInferenceBuilder::infer_region_expression), depending which +/// kind of [`InferenceRegion`] we are inferring types for. +/// +/// Scope inference starts with the scope body, walking all statements and expressions and +/// recording the types of each expression in the [`TypeInference`] result. Most of the methods +/// here (with names like `infer_*_statement` or `infer_*_expression` or some other node kind) take +/// a single AST node and are called as part of this AST visit. +/// +/// When the visit encounters a node which creates a [`Definition`], we look up the definition in +/// the semantic index and call the [`infer_definition_types()`] query on it, which creates another +/// [`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 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 +/// called by [`infer_region_definition`](TypeInferenceBuilder::infer_region_definition). +/// +/// So for example we have both +/// [`infer_function_definition_statement`](TypeInferenceBuilder::infer_function_definition_statement), +/// which takes just the function AST node, and +/// [`infer_function_definition`](TypeInferenceBuilder::infer_function_definition), which takes +/// both the node and the [`Definition`] id. The former is called as part of walking the AST, and +/// it just looks up the [`Definition`] for that function in the semantic index and calls +/// [`infer_definition_types()`] on it, which will create a new [`TypeInferenceBuilder`] with +/// [`InferenceRegion::Definition`], and in that builder +/// [`infer_region_definition`](TypeInferenceBuilder::infer_region_definition) will call +/// [`infer_function_definition`](TypeInferenceBuilder::infer_function_definition) to actually +/// infer a type for the definition. +/// +/// Similarly, when we encounter a standalone-inferable expression (right-hand side of an +/// assignment, type narrowing guard), we use the [`infer_expression_types()`] query to ensure we +/// don't infer its types more than once. +pub(super) struct TypeInferenceBuilder<'db, 'ast> { + context: InferContext<'db, 'ast>, + index: &'db SemanticIndex<'db>, + region: InferenceRegion<'db>, + + /// The type inference results + types: TypeInference<'db>, + + /// The returned types and their corresponding ranges of the region, if it is a function body. + return_types_and_ranges: Vec>, + + /// A set of functions that have been defined **and** called in this region. + /// + /// This is a set because the same function could be called multiple times in the same region. + /// This is mainly used in [`check_overloaded_functions`] to check an overloaded function that + /// is shadowed by a function with the same name in this scope but has been called before. For + /// example: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo() -> None: ... + /// @overload + /// def foo(x: int) -> int: ... + /// def foo(x: int | None) -> int | None: return x + /// + /// foo() # An overloaded function that was defined in this scope have been called + /// + /// def foo(x: int) -> int: + /// return x + /// ``` + /// + /// [`check_overloaded_functions`]: TypeInferenceBuilder::check_overloaded_functions + called_functions: FxHashSet>, + + /// The deferred state of inferring types of certain expressions within the region. + /// + /// This is different from [`InferenceRegion::Deferred`] which works on the entire definition + /// while this is relevant for specific expressions within the region itself and is updated + /// during the inference process. + /// + /// For example, when inferring the types of an annotated assignment, the type of an annotation + /// expression could be deferred if the file has `from __future__ import annotations` import or + /// is a stub file but we're still in a non-deferred region. + deferred_state: DeferredExpressionState, +} + +impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { + /// How big a string do we build before bailing? + /// + /// This is a fairly arbitrary number. It should be *far* more than enough + /// for most use cases, but we can reevaluate it later if useful. + const MAX_STRING_LITERAL_SIZE: usize = 4096; + + /// Creates a new builder for inferring types in a region. + pub(super) fn new( + db: &'db dyn Db, + region: InferenceRegion<'db>, + index: &'db SemanticIndex<'db>, + module: &'ast ParsedModuleRef, + ) -> Self { + let scope = region.scope(db); + + Self { + context: InferContext::new(db, scope, module), + index, + region, + return_types_and_ranges: vec![], + called_functions: FxHashSet::default(), + deferred_state: DeferredExpressionState::None, + types: TypeInference::empty(scope), + } + } + + fn extend(&mut self, inference: &TypeInference<'db>) { + debug_assert_eq!(self.types.scope, inference.scope); + + self.types.bindings.extend(inference.bindings.iter()); + self.types + .declarations + .extend(inference.declarations.iter()); + self.types.expressions.extend(inference.expressions.iter()); + self.types.deferred.extend(inference.deferred.iter()); + self.context.extend(inference.diagnostics()); + self.types.cycle_fallback_type = self + .types + .cycle_fallback_type + .or(inference.cycle_fallback_type); + } + + fn file(&self) -> File { + self.context.file() + } + + fn module(&self) -> &'ast ParsedModuleRef { + self.context.module() + } + + fn db(&self) -> &'db dyn Db { + self.context.db() + } + + fn scope(&self) -> ScopeId<'db> { + self.types.scope + } + + /// Are we currently inferring types in file with deferred types? + /// This is true for stub files and files with `__future__.annotations` + fn defer_annotations(&self) -> bool { + self.index.has_future_annotations() || self.in_stub() + } + + /// Are we currently inferring deferred types? + fn is_deferred(&self) -> bool { + matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred() + } + + /// Return the node key of the given AST node, or the key of the outermost enclosing string + /// literal, if the node originates from inside a stringified annotation. + fn enclosing_node_key(&self, node: AnyNodeRef<'_>) -> NodeKey { + match self.deferred_state { + DeferredExpressionState::InStringAnnotation(enclosing_node_key) => enclosing_node_key, + _ => NodeKey::from_node(node), + } + } + + /// Check if a given AST node is reachable. + /// + /// Note that this only works if reachability is explicitly tracked for this specific + /// type of node (see `node_reachability` in the use-def map). + fn is_reachable<'a, N>(&self, node: N) -> bool + where + N: Into>, + { + let file_scope_id = self.scope().file_scope_id(self.db()); + self.index.is_node_reachable( + self.db(), + file_scope_id, + self.enclosing_node_key(node.into()), + ) + } + + fn in_stub(&self) -> bool { + self.context.in_stub() + } + + /// Get the already-inferred type of an expression node. + /// + /// ## Panics + /// If the expression is not within this region, or if no type has yet been inferred for + /// this node. + #[track_caller] + fn expression_type(&self, expr: &ast::Expr) -> Type<'db> { + self.types.expression_type(expr) + } + + fn try_expression_type(&self, expr: &ast::Expr) -> Option> { + self.types.try_expression_type(expr) + } + + /// Get the type of an expression from any scope in the same file. + /// + /// If the expression is in the current scope, and we are inferring the entire scope, just look + /// up the expression in our own results, otherwise call [`infer_scope_types()`] for the scope + /// of the expression. + /// + /// ## Panics + /// + /// If the expression is in the current scope but we haven't yet inferred a type for it. + /// + /// Can cause query cycles if the expression is from a different scope and type inference is + /// already in progress for that scope (further up the stack). + fn file_expression_type(&self, expression: &ast::Expr) -> Type<'db> { + let file_scope = self.index.expression_scope_id(expression); + let expr_scope = file_scope.to_scope_id(self.db(), self.file()); + match self.region { + InferenceRegion::Scope(scope) if scope == expr_scope => { + self.expression_type(expression) + } + _ => infer_scope_types(self.db(), expr_scope).expression_type(expression), + } + } + + /// Infers types in the given [`InferenceRegion`]. + fn infer_region(&mut self) { + match self.region { + InferenceRegion::Scope(scope) => self.infer_region_scope(scope), + InferenceRegion::Definition(definition) => self.infer_region_definition(definition), + InferenceRegion::Deferred(definition) => self.infer_region_deferred(definition), + InferenceRegion::Expression(expression) => self.infer_region_expression(expression), + } + } + + fn infer_region_scope(&mut self, scope: ScopeId<'db>) { + let node = scope.node(self.db()); + match node { + NodeWithScopeKind::Module => { + self.infer_module(self.module().syntax()); + } + NodeWithScopeKind::Function(function) => { + self.infer_function_body(function.node(self.module())); + } + NodeWithScopeKind::Lambda(lambda) => self.infer_lambda_body(lambda.node(self.module())), + NodeWithScopeKind::Class(class) => self.infer_class_body(class.node(self.module())), + NodeWithScopeKind::ClassTypeParameters(class) => { + self.infer_class_type_params(class.node(self.module())); + } + NodeWithScopeKind::FunctionTypeParameters(function) => { + self.infer_function_type_params(function.node(self.module())); + } + NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => { + self.infer_type_alias_type_params(type_alias.node(self.module())); + } + NodeWithScopeKind::TypeAlias(type_alias) => { + self.infer_type_alias(type_alias.node(self.module())); + } + NodeWithScopeKind::ListComprehension(comprehension) => { + self.infer_list_comprehension_expression_scope(comprehension.node(self.module())); + } + NodeWithScopeKind::SetComprehension(comprehension) => { + self.infer_set_comprehension_expression_scope(comprehension.node(self.module())); + } + NodeWithScopeKind::DictComprehension(comprehension) => { + self.infer_dict_comprehension_expression_scope(comprehension.node(self.module())); + } + NodeWithScopeKind::GeneratorExpression(generator) => { + self.infer_generator_expression_scope(generator.node(self.module())); + } + } + + // Infer the deferred types for the definitions here to consider the end-of-scope + // semantics. + for definition in std::mem::take(&mut self.types.deferred) { + self.extend(infer_deferred_types(self.db(), definition)); + } + assert!( + self.types.deferred.is_empty(), + "Inferring deferred types should not add more deferred definitions" + ); + + // TODO: Only call this function when diagnostics are enabled. + self.check_class_definitions(); + self.check_overloaded_functions(node); + } + + /// Iterate over all class definitions to check that the definition will not cause an exception + /// to be raised at runtime. This needs to be done after most other types in the scope have been + /// inferred, due to the fact that base classes can be deferred. If it looks like a class + /// definition is invalid in some way, issue a diagnostic. + /// + /// Among the things we check for in this method are whether Python will be able to determine a + /// consistent "[method resolution order]" and [metaclass] for each class. + /// + /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + /// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses + fn check_class_definitions(&mut self) { + let class_definitions = self + .types + .declarations + .iter() + .filter_map(|(definition, ty)| { + // Filter out class literals that result from imports + if let DefinitionKind::Class(class) = definition.kind(self.db()) { + ty.inner_type() + .into_class_literal() + .map(|class_literal| (class_literal, class.node(self.module()))) + } else { + None + } + }); + + // Iterate through all class definitions in this scope. + for (class, class_node) in class_definitions { + // (1) Check that the class does not have a cyclic definition + if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) { + if inheritance_cycle.is_participant() { + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}` (class cannot inherit from itself)", + class.name(self.db()) + )); + } + } + // If a class is cyclically defined, that's a sufficient error to report; the + // following checks (which are all inheritance-based) aren't even relevant. + continue; + } + + let is_protocol = class.is_protocol(self.db()); + let mut solid_bases = IncompatibleBases::default(); + + // (2) Iterate through the class's explicit bases to check for various possible errors: + // - Check for inheritance from plain `Generic`, + // - Check for inheritance from a `@final` classes + // - If the class is a protocol class: check for inheritance from a non-protocol class + for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { + if let Some((class, solid_base)) = base_class + .to_class_type(self.db()) + .and_then(|class| Some((class, class.nearest_solid_base(self.db())?))) + { + solid_bases.insert(solid_base, i, class.class_literal(self.db()).0); + } + + let base_class = match base_class { + Type::SpecialForm(SpecialFormType::Generic) => { + if let Some(builder) = self + .context + .report_lint(&INVALID_BASE, &class_node.bases()[i]) + { + // Unsubscripted `Generic` can appear in the MRO of many classes, + // but it is never valid as an explicit base class in user code. + builder.into_diagnostic("Cannot inherit from plain `Generic`"); + } + continue; + } + // Note that unlike several of the other errors caught in this function, + // this does not lead to the class creation failing at runtime, + // but it is semantically invalid. + Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)) => { + if class_node.type_params.is_none() { + continue; + } + let Some(builder) = self + .context + .report_lint(&INVALID_GENERIC_CLASS, &class_node.bases()[i]) + else { + continue; + }; + builder.into_diagnostic( + "Cannot both inherit from subscripted `Protocol` \ + and use PEP 695 type variables", + ); + continue; + } + Type::ClassLiteral(class) => class, + // dynamic/unknown bases are never `@final` + _ => continue, + }; + + if is_protocol + && !(base_class.is_protocol(self.db()) + || base_class.is_known(self.db(), KnownClass::Object)) + { + if let Some(builder) = self + .context + .report_lint(&INVALID_PROTOCOL, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( + "Protocol class `{}` cannot inherit from non-protocol class `{}`", + class.name(self.db()), + base_class.name(self.db()), + )); + } + } + + if base_class.is_final(self.db()) { + if let Some(builder) = self + .context + .report_lint(&SUBCLASS_OF_FINAL_CLASS, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( + "Class `{}` cannot inherit from final class `{}`", + class.name(self.db()), + base_class.name(self.db()), + )); + } + } + } + + // (3) Check that the class's MRO is resolvable + match class.try_mro(self.db(), None) { + Err(mro_error) => match mro_error.reason() { + MroErrorKind::DuplicateBases(duplicates) => { + let base_nodes = class_node.bases(); + for duplicate in duplicates { + report_duplicate_bases(&self.context, class, duplicate, base_nodes); + } + } + MroErrorKind::InvalidBases(bases) => { + let base_nodes = class_node.bases(); + for (index, base_ty) in bases { + report_invalid_or_unsupported_base( + &self.context, + &base_nodes[*index], + *base_ty, + class, + ); + } + } + MroErrorKind::UnresolvableMro { bases_list } => { + if let Some(builder) = + self.context.report_lint(&INCONSISTENT_MRO, class_node) + { + builder.into_diagnostic(format_args!( + "Cannot create a consistent method resolution order (MRO) \ + for class `{}` with bases list `[{}]`", + class.name(self.db()), + bases_list + .iter() + .map(|base| base.display(self.db())) + .join(", ") + )); + } + } + MroErrorKind::Pep695ClassWithGenericInheritance => { + if let Some(builder) = + self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) + { + builder.into_diagnostic( + "Cannot both inherit from `typing.Generic` \ + and use PEP 695 type variables", + ); + } + } + MroErrorKind::InheritanceCycle => { + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}` (class cannot inherit from itself)", + class.name(self.db()) + )); + } + } + }, + Ok(_) => { + solid_bases.remove_redundant_entries(self.db()); + + if solid_bases.len() > 1 { + report_instance_layout_conflict( + &self.context, + class, + class_node, + &solid_bases, + ); + } + } + } + + // (4) Check that the class's metaclass can be determined without error. + if let Err(metaclass_error) = class.try_metaclass(self.db()) { + match metaclass_error.reason() { + MetaclassErrorKind::Cycle => { + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}`", + class.name(self.db()) + )); + } + } + MetaclassErrorKind::NotCallable(ty) => { + if let Some(builder) = + self.context.report_lint(&INVALID_METACLASS, class_node) + { + builder.into_diagnostic(format_args!( + "Metaclass type `{}` is not callable", + ty.display(self.db()) + )); + } + } + MetaclassErrorKind::PartlyNotCallable(ty) => { + if let Some(builder) = + self.context.report_lint(&INVALID_METACLASS, class_node) + { + builder.into_diagnostic(format_args!( + "Metaclass type `{}` is partly not callable", + ty.display(self.db()) + )); + } + } + MetaclassErrorKind::Conflict { + candidate1: + MetaclassCandidate { + metaclass: metaclass1, + explicit_metaclass_of: class1, + }, + candidate2: + MetaclassCandidate { + metaclass: metaclass2, + explicit_metaclass_of: class2, + }, + candidate1_is_base_class, + } => { + if let Some(builder) = + self.context.report_lint(&CONFLICTING_METACLASS, class_node) + { + if *candidate1_is_base_class { + builder.into_diagnostic(format_args!( + "The metaclass of a derived class (`{class}`) \ + must be a subclass of the metaclasses of all its bases, \ + but `{metaclass1}` (metaclass of base class `{base1}`) \ + and `{metaclass2}` (metaclass of base class `{base2}`) \ + have no subclass relationship", + class = class.name(self.db()), + metaclass1 = metaclass1.name(self.db()), + base1 = class1.name(self.db()), + metaclass2 = metaclass2.name(self.db()), + base2 = class2.name(self.db()), + )); + } else { + builder.into_diagnostic(format_args!( + "The metaclass of a derived class (`{class}`) \ + must be a subclass of the metaclasses of all its bases, \ + but `{metaclass_of_class}` (metaclass of `{class}`) \ + and `{metaclass_of_base}` (metaclass of base class `{base}`) \ + have no subclass relationship", + class = class.name(self.db()), + metaclass_of_class = metaclass1.name(self.db()), + metaclass_of_base = metaclass2.name(self.db()), + base = class2.name(self.db()), + )); + } + } + } + } + } + + if let (Some(legacy), Some(inherited)) = ( + class.legacy_generic_context(self.db()), + class.inherited_legacy_generic_context(self.db()), + ) { + if !inherited.is_subset_of(self.db(), legacy) { + if let Some(builder) = + self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) + { + builder.into_diagnostic( + "`Generic` base class must include all type \ + variables used in other base classes", + ); + } + } + } + + // (5) Check that a dataclass does not have more than one `KW_ONLY`. + if let Some(field_policy @ CodeGeneratorKind::DataclassLike) = + CodeGeneratorKind::from_class(self.db(), class) + { + let specialization = None; + let mut kw_only_field_names = vec![]; + + for (name, (attr_ty, _)) in class.fields(self.db(), specialization, field_policy) { + let Some(instance) = attr_ty.into_nominal_instance() else { + continue; + }; + + if !instance.class.is_known(self.db(), KnownClass::KwOnly) { + continue; + } + + kw_only_field_names.push(name); + } + + if kw_only_field_names.len() > 1 { + // TODO: The fields should be displayed in a subdiagnostic. + if let Some(builder) = self + .context + .report_lint(&DUPLICATE_KW_ONLY, &class_node.name) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Dataclass has more than one field annotated with `KW_ONLY`" + )); + + diagnostic.info(format_args!( + "`KW_ONLY` fields: {}", + kw_only_field_names + .iter() + .map(|name| format!("`{name}`")) + .join(", ") + )); + } + } + } + } + } + + /// Check the overloaded functions in this scope. + /// + /// This only checks the overloaded functions that are: + /// 1. Visible publicly at the end of this scope + /// 2. Or, defined and called in this scope + /// + /// For (1), this has the consequence of not checking an overloaded function that is being + /// shadowed by another function with the same name in this scope. + fn check_overloaded_functions(&mut self, scope: &NodeWithScopeKind) { + // Collect all the unique overloaded function places in this scope. This requires a set + // because an overloaded function uses the same place for each of the overloads and the + // implementation. + let overloaded_function_places: FxHashSet<_> = self + .types + .declarations + .iter() + .filter_map(|(definition, ty)| { + // Filter out function literals that result from anything other than a function + // definition e.g., imports which would create a cross-module AST dependency. + if !matches!(definition.kind(self.db()), DefinitionKind::Function(_)) { + return None; + } + let function = ty.inner_type().into_function_literal()?; + if function.has_known_decorator(self.db(), FunctionDecorators::OVERLOAD) { + Some(definition.place(self.db())) + } else { + None + } + }) + .collect(); + + let use_def = self + .index + .use_def_map(self.scope().file_scope_id(self.db())); + + let mut public_functions = FxHashSet::default(); + + for place in overloaded_function_places { + if let Place::Type(Type::FunctionLiteral(function), Boundness::Bound) = + place_from_bindings(self.db(), use_def.end_of_scope_bindings(place)) + { + if function.file(self.db()) != self.file() { + // If the function is not in this file, we don't need to check it. + // https://github.com/astral-sh/ruff/pull/17609#issuecomment-2839445740 + continue; + } + + // Extend the functions that we need to check with the publicly visible overloaded + // function. This is always going to be either the implementation or the last + // overload if the implementation doesn't exists. + public_functions.insert(function); + } + } + + for function in self.called_functions.union(&public_functions) { + let (overloads, implementation) = function.overloads_and_implementation(self.db()); + if overloads.is_empty() { + continue; + } + + // Check that the overloaded function has at least two overloads + if let [single_overload] = overloads.as_ref() { + let function_node = function.node(self.db(), self.file(), self.module()); + if let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloaded function `{}` requires at least two overloads", + &function_node.name + )); + diagnostic.annotate( + self.context + .secondary(single_overload.focus_range(self.db(), self.module())) + .message(format_args!("Only one overload defined here")), + ); + } + } + + // Check that the overloaded function has an implementation. Overload definitions + // within stub files, protocols, and on abstract methods within abstract base classes + // are exempt from this check. + if implementation.is_none() && !self.in_stub() { + let mut implementation_required = true; + + if let NodeWithScopeKind::Class(class_node_ref) = scope { + let class = binding_type( + self.db(), + self.index + .expect_single_definition(class_node_ref.node(self.module())), + ) + .expect_class_literal(); + + if class.is_protocol(self.db()) + || (class.is_abstract(self.db()) + && overloads.iter().all(|overload| { + overload.has_known_decorator( + self.db(), + FunctionDecorators::ABSTRACT_METHOD, + ) + })) + { + implementation_required = false; + } + } + + if implementation_required { + let function_node = function.node(self.db(), self.file(), self.module()); + if let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + { + builder.into_diagnostic(format_args!( + "Overloaded non-stub function `{}` must have an implementation", + &function_node.name + )); + } + } + } + + for (decorator, name) in [ + (FunctionDecorators::CLASSMETHOD, "classmethod"), + (FunctionDecorators::STATICMETHOD, "staticmethod"), + ] { + let mut decorator_present = false; + let mut decorator_missing = vec![]; + + for function in overloads.iter().chain(implementation.as_ref()) { + if function.has_known_decorator(self.db(), decorator) { + decorator_present = true; + } else { + decorator_missing.push(function); + } + } + + if !decorator_present { + // Both overloads and implementation does not have the decorator + continue; + } + if decorator_missing.is_empty() { + // All overloads and implementation have the decorator + continue; + } + + let function_node = function.node(self.db(), self.file(), self.module()); + if let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloaded function `{}` does not use the `@{name}` decorator \ + consistently", + &function_node.name + )); + for function in decorator_missing { + diagnostic.annotate( + self.context + .secondary(function.focus_range(self.db(), self.module())) + .message(format_args!("Missing here")), + ); + } + } + } + + for (decorator, name) in [ + (FunctionDecorators::FINAL, "final"), + (FunctionDecorators::OVERRIDE, "override"), + ] { + if let Some(implementation) = implementation { + for overload in overloads.as_ref() { + if !overload.has_known_decorator(self.db(), decorator) { + continue; + } + let function_node = function.node(self.db(), self.file(), self.module()); + let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@{name}` decorator should be applied only to the \ + overload implementation" + )); + diagnostic.annotate( + self.context + .secondary(implementation.focus_range(self.db(), self.module())) + .message(format_args!("Implementation defined here")), + ); + } + } else { + let mut overloads = overloads.iter(); + let Some(first_overload) = overloads.next() else { + continue; + }; + for overload in overloads { + if !overload.has_known_decorator(self.db(), decorator) { + continue; + } + let function_node = function.node(self.db(), self.file(), self.module()); + let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@{name}` decorator should be applied only to the \ + first overload" + )); + diagnostic.annotate( + self.context + .secondary(first_overload.focus_range(self.db(), self.module())) + .message(format_args!("First overload defined here")), + ); + } + } + } + } + } + + fn infer_region_definition(&mut self, definition: Definition<'db>) { + match definition.kind(self.db()) { + DefinitionKind::Function(function) => { + self.infer_function_definition(function.node(self.module()), definition); + } + DefinitionKind::Class(class) => { + self.infer_class_definition(class.node(self.module()), definition); + } + DefinitionKind::TypeAlias(type_alias) => { + self.infer_type_alias_definition(type_alias.node(self.module()), definition); + } + DefinitionKind::Import(import) => { + self.infer_import_definition( + import.import(self.module()), + import.alias(self.module()), + definition, + ); + } + DefinitionKind::ImportFrom(import_from) => { + self.infer_import_from_definition( + import_from.import(self.module()), + import_from.alias(self.module()), + definition, + ); + } + DefinitionKind::StarImport(import) => { + self.infer_import_from_definition( + import.import(self.module()), + import.alias(self.module()), + definition, + ); + } + DefinitionKind::Assignment(assignment) => { + self.infer_assignment_definition(assignment, definition); + } + DefinitionKind::AnnotatedAssignment(annotated_assignment) => { + self.infer_annotated_assignment_definition(annotated_assignment, definition); + } + DefinitionKind::AugmentedAssignment(augmented_assignment) => { + self.infer_augment_assignment_definition( + augmented_assignment.node(self.module()), + definition, + ); + } + DefinitionKind::For(for_statement_definition) => { + self.infer_for_statement_definition(for_statement_definition, definition); + } + DefinitionKind::NamedExpression(named_expression) => { + self.infer_named_expression_definition( + named_expression.node(self.module()), + definition, + ); + } + DefinitionKind::Comprehension(comprehension) => { + self.infer_comprehension_definition(comprehension, definition); + } + DefinitionKind::VariadicPositionalParameter(parameter) => { + self.infer_variadic_positional_parameter_definition( + parameter.node(self.module()), + definition, + ); + } + DefinitionKind::VariadicKeywordParameter(parameter) => { + self.infer_variadic_keyword_parameter_definition( + parameter.node(self.module()), + definition, + ); + } + DefinitionKind::Parameter(parameter_with_default) => { + self.infer_parameter_definition( + parameter_with_default.node(self.module()), + definition, + ); + } + DefinitionKind::WithItem(with_item_definition) => { + self.infer_with_item_definition(with_item_definition, definition); + } + DefinitionKind::MatchPattern(match_pattern) => { + self.infer_match_pattern_definition( + match_pattern.pattern(self.module()), + match_pattern.index(), + definition, + ); + } + DefinitionKind::ExceptHandler(except_handler_definition) => { + self.infer_except_handler_definition(except_handler_definition, definition); + } + DefinitionKind::TypeVar(node) => { + self.infer_typevar_definition(node.node(self.module()), definition); + } + DefinitionKind::ParamSpec(node) => { + self.infer_paramspec_definition(node.node(self.module()), definition); + } + DefinitionKind::TypeVarTuple(node) => { + self.infer_typevartuple_definition(node.node(self.module()), definition); + } + } + } + + fn infer_region_deferred(&mut self, definition: Definition<'db>) { + // N.B. We don't defer the types for an annotated assignment here because it is done in + // the same definition query. It utilizes the deferred expression state instead. + // + // This is because for partially stringified annotations like `a: tuple[int, "ForwardRef"]`, + // we need to defer the types of non-stringified expressions like `tuple` and `int` in the + // definition query while the stringified expression `"ForwardRef"` would need to deferred + // to use end-of-scope semantics. This would require custom and possibly a complex + // implementation to allow this "split" to happen. + + match definition.kind(self.db()) { + DefinitionKind::Function(function) => { + self.infer_function_deferred(function.node(self.module())); + } + DefinitionKind::Class(class) => self.infer_class_deferred(class.node(self.module())), + _ => {} + } + } + + fn infer_region_expression(&mut self, expression: Expression<'db>) { + match expression.kind(self.db()) { + ExpressionKind::Normal => { + self.infer_expression_impl(expression.node_ref(self.db(), self.module())); + } + ExpressionKind::TypeExpression => { + self.infer_type_expression(expression.node_ref(self.db(), self.module())); + } + } + } + + /// Raise a diagnostic if the given type cannot be divided by zero. + /// + /// Expects the resolved type of the left side of the binary expression. + fn check_division_by_zero( + &mut self, + node: AnyNodeRef<'_>, + op: ast::Operator, + left: Type<'db>, + ) -> bool { + match left { + Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} + Type::NominalInstance(instance) + if matches!( + instance.class.known(self.db()), + Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) + ) => {} + _ => return false, + } + + let (op, by_zero) = match op { + ast::Operator::Div => ("divide", "by zero"), + ast::Operator::FloorDiv => ("floor divide", "by zero"), + ast::Operator::Mod => ("reduce", "modulo zero"), + _ => return false, + }; + + if let Some(builder) = self.context.report_lint(&DIVISION_BY_ZERO, node) { + builder.into_diagnostic(format_args!( + "Cannot {op} object of type `{}` {by_zero}", + left.display(self.db()) + )); + } + + true + } + + fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) { + debug_assert!( + binding + .kind(self.db()) + .category(self.context.in_stub(), self.module()) + .is_binding() + ); + + let db = self.db(); + let file_scope_id = binding.file_scope(db); + let place_table = self.index.place_table(file_scope_id); + let use_def = self.index.use_def_map(file_scope_id); + let mut bound_ty = ty; + + let global_use_def_map = self.index.use_def_map(FileScopeId::global()); + let place_id = binding.place(self.db()); + let place = place_table.place_expr(place_id); + let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, place_id); + let (declarations, is_local) = if skip_non_global_scopes { + match self + .index + .place_table(FileScopeId::global()) + .place_id_by_expr(&place.expr) + { + Some(id) => (global_use_def_map.end_of_scope_declarations(id), false), + // This case is a syntax error (load before global declaration) but ignore that here + None => (use_def.declarations_at_binding(binding), true), + } + } else { + (use_def.declarations_at_binding(binding), true) + }; + + let (declared_ty, is_modifiable) = place_from_declarations(self.db(), declarations) + .and_then(|place_and_quals| { + Ok( + if matches!(place_and_quals.place, Place::Type(_, Boundness::Bound)) { + place_and_quals + } else if skip_non_global_scopes + || self.scope().file_scope_id(self.db()).is_global() + { + let module_type_declarations = + module_type_implicit_global_declaration(self.db(), &place.expr)?; + place_and_quals.or_fall_back_to(self.db(), || module_type_declarations) + } else { + place_and_quals + }, + ) + }) + .map( + |PlaceAndQualifiers { + place: resolved_place, + qualifiers, + }| { + let is_modifiable = !qualifiers.contains(TypeQualifiers::FINAL); + + if resolved_place.is_unbound() && !place_table.place_expr(place_id).is_name() { + if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { + value, attr, .. + }) = node + { + let value_type = self.infer_maybe_standalone_expression(value); + if let Place::Type(ty, Boundness::Bound) = + value_type.member(db, attr).place + { + // TODO: also consider qualifiers on the attribute + return (ty, is_modifiable); + } + } else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript { + value, + slice, + .. + }) = node + { + let value_ty = self.infer_expression(value); + let slice_ty = self.infer_expression(slice); + let result_ty = + self.infer_subscript_expression_types(value, value_ty, slice_ty); + return (result_ty, is_modifiable); + } + } + ( + resolved_place + .ignore_possibly_unbound() + .unwrap_or(Type::unknown()), + is_modifiable, + ) + }, + ) + .unwrap_or_else(|(ty, conflicting)| { + // TODO point out the conflicting declarations in the diagnostic? + let place = place_table.place_expr(binding.place(db)); + if let Some(builder) = self.context.report_lint(&CONFLICTING_DECLARATIONS, node) { + builder.into_diagnostic(format_args!( + "Conflicting declared types for `{place}`: {}", + format_enumeration(conflicting.iter().map(|ty| ty.display(db))) + )); + } + ( + ty.inner_type(), + !ty.qualifiers.contains(TypeQualifiers::FINAL), + ) + }); + + if !is_modifiable { + let mut previous_bindings = use_def.bindings_at_definition(binding); + + // An assignment to a local `Final`-qualified symbol is only an error if there are prior bindings + + let previous_definition = previous_bindings + .next() + .and_then(|r| r.binding.definition()); + + if !is_local || previous_definition.is_some() { + let place = place_table.place_expr(binding.place(db)); + if let Some(builder) = self.context.report_lint( + &INVALID_ASSIGNMENT, + binding.full_range(self.db(), self.module()), + ) { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Reassignment of `Final` symbol `{place}` is not allowed" + )); + + diagnostic.set_primary_message("Reassignment of `Final` symbol"); + + if let Some(previous_definition) = previous_definition { + // It is not very helpful to show the previous definition if it results from + // an import. Ideally, we would show the original definition in the external + // module, but that information is currently not threaded through attribute + // lookup. + if !previous_definition.kind(db).is_import() { + if let DefinitionKind::AnnotatedAssignment(assignment) = + previous_definition.kind(db) + { + let range = assignment.annotation(self.module()).range(); + diagnostic.annotate( + self.context + .secondary(range) + .message("Symbol declared as `Final` here"), + ); + } else { + let range = + previous_definition.full_range(self.db(), self.module()); + diagnostic.annotate( + self.context + .secondary(range) + .message("Symbol declared as `Final` here"), + ); + } + diagnostic.set_primary_message("Symbol later reassigned here"); + } + } + } + } + } + + if !bound_ty.is_assignable_to(db, declared_ty) { + report_invalid_assignment(&self.context, node, declared_ty, bound_ty); + // allow declarations to override inference in case of invalid assignment + bound_ty = declared_ty; + } + // In the following cases, the bound type may not be the same as the RHS value type. + if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { value, attr, .. }) = node { + let value_ty = self + .try_expression_type(value) + .unwrap_or_else(|| self.infer_maybe_standalone_expression(value)); + // If the member is a data descriptor, the RHS value may differ from the value actually assigned. + if value_ty + .class_member(db, attr.id.clone()) + .place + .ignore_possibly_unbound() + .is_some_and(|ty| ty.may_be_data_descriptor(db)) + { + bound_ty = declared_ty; + } + } else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript { value, .. }) = node { + let value_ty = self + .try_expression_type(value) + .unwrap_or_else(|| self.infer_expression(value)); + // Arbitrary `__getitem__`/`__setitem__` methods on a class do not + // necessarily guarantee that the passed-in value for `__setitem__` is stored and + // can be retrieved unmodified via `__getitem__`. Therefore, we currently only + // perform assignment-based narrowing on a few built-in classes (`list`, `dict`, + // `bytesarray`, `TypedDict` and `collections` types) where we are confident that + // this kind of narrowing can be performed soundly. This is the same approach as + // pyright. TODO: Other standard library classes may also be considered safe. Also, + // subclasses of these safe classes that do not override `__getitem__/__setitem__` + // may be considered safe. + let safe_mutable_classes = [ + KnownClass::List.to_instance(db), + KnownClass::Dict.to_instance(db), + KnownClass::Bytearray.to_instance(db), + KnownClass::DefaultDict.to_instance(db), + SpecialFormType::ChainMap.instance_fallback(db), + SpecialFormType::Counter.instance_fallback(db), + SpecialFormType::Deque.instance_fallback(db), + SpecialFormType::OrderedDict.instance_fallback(db), + SpecialFormType::TypedDict.instance_fallback(db), + ]; + if safe_mutable_classes.iter().all(|safe_mutable_class| { + !value_ty.is_equivalent_to(db, *safe_mutable_class) + && value_ty + .generic_origin(db) + .zip(safe_mutable_class.generic_origin(db)) + .is_none_or(|(l, r)| l != r) + }) { + bound_ty = declared_ty; + } + } + + self.types.bindings.insert(binding, bound_ty); + } + + /// Returns `true` if `symbol_id` should be looked up in the global scope, skipping intervening + /// local scopes. + fn skip_non_global_scopes(&self, file_scope_id: FileScopeId, symbol_id: ScopedPlaceId) -> bool { + !file_scope_id.is_global() + && self + .index + .symbol_is_global_in_scope(symbol_id, file_scope_id) + } + + fn add_declaration( + &mut self, + node: AnyNodeRef, + declaration: Definition<'db>, + ty: TypeAndQualifiers<'db>, + ) { + debug_assert!( + declaration + .kind(self.db()) + .category(self.context.in_stub(), self.module()) + .is_declaration() + ); + let use_def = self.index.use_def_map(declaration.file_scope(self.db())); + let prior_bindings = use_def.bindings_at_definition(declaration); + // unbound_ty is Never because for this check we don't care about unbound + let inferred_ty = place_from_bindings(self.db(), prior_bindings) + .with_qualifiers(TypeQualifiers::empty()) + .or_fall_back_to(self.db(), || { + // Fallback to bindings declared on `types.ModuleType` if it's a global symbol + let scope = self.scope().file_scope_id(self.db()); + let place_table = self.index.place_table(scope); + let place = place_table.place_expr(declaration.place(self.db())); + if scope.is_global() && place.is_name() { + module_type_implicit_global_symbol(self.db(), place.expect_name()) + } else { + Place::Unbound.into() + } + }) + .place + .ignore_possibly_unbound() + .unwrap_or(Type::Never); + let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { + ty + } else { + if let Some(builder) = self.context.report_lint(&INVALID_DECLARATION, node) { + builder.into_diagnostic(format_args!( + "Cannot declare type `{}` for inferred type `{}`", + ty.inner_type().display(self.db()), + inferred_ty.display(self.db()) + )); + } + TypeAndQualifiers::unknown() + }; + self.types.declarations.insert(declaration, ty); + } + + fn add_declaration_with_binding( + &mut self, + node: AnyNodeRef, + definition: Definition<'db>, + declared_and_inferred_ty: &DeclaredAndInferredType<'db>, + ) { + debug_assert!( + definition + .kind(self.db()) + .category(self.context.in_stub(), self.module()) + .is_binding() + ); + debug_assert!( + definition + .kind(self.db()) + .category(self.context.in_stub(), self.module()) + .is_declaration() + ); + + let (declared_ty, inferred_ty) = match *declared_and_inferred_ty { + DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty), + DeclaredAndInferredType::MightBeDifferent { + declared_ty, + inferred_ty, + } => { + let file_scope_id = self.scope().file_scope_id(self.db()); + if file_scope_id.is_global() { + let place_table = self.index.place_table(file_scope_id); + let place = place_table.place_expr(definition.place(self.db())); + if let Some(module_type_implicit_declaration) = + module_type_implicit_global_declaration(self.db(), &place.expr) + .ok() + .and_then(|place| place.place.ignore_possibly_unbound()) + { + let declared_type = declared_ty.inner_type(); + if !declared_type + .is_assignable_to(self.db(), module_type_implicit_declaration) + { + if let Some(builder) = + self.context.report_lint(&INVALID_DECLARATION, node) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot shadow implicit global attribute `{place}` with declaration of type `{}`", + declared_type.display(self.db()) + )); + diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`", + place, + module_type_implicit_declaration.display(self.db()) + )); + } + } + } + } + if inferred_ty.is_assignable_to(self.db(), declared_ty.inner_type()) { + (declared_ty, inferred_ty) + } else { + report_invalid_assignment( + &self.context, + node, + declared_ty.inner_type(), + inferred_ty, + ); + // if the assignment is invalid, fall back to assuming the annotation is correct + (declared_ty, declared_ty.inner_type()) + } + } + }; + self.types.declarations.insert(definition, declared_ty); + self.types.bindings.insert(definition, inferred_ty); + } + + fn add_unknown_declaration_with_binding( + &mut self, + node: AnyNodeRef, + definition: Definition<'db>, + ) { + self.add_declaration_with_binding( + node, + definition, + &DeclaredAndInferredType::AreTheSame(Type::unknown()), + ); + } + + fn record_return_type(&mut self, ty: Type<'db>, range: TextRange) { + self.return_types_and_ranges + .push(TypeAndRange { ty, range }); + } + + fn infer_module(&mut self, module: &ast::ModModule) { + self.infer_body(&module.body); + } + + fn infer_class_type_params(&mut self, class: &ast::StmtClassDef) { + let type_params = class + .type_params + .as_deref() + .expect("class type params scope without type params"); + + self.infer_type_parameters(type_params); + + if let Some(arguments) = class.arguments.as_deref() { + let call_arguments = CallArguments::from_arguments(arguments); + let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; + self.infer_argument_types(arguments, call_arguments, &argument_forms); + } + } + + fn infer_class_body(&mut self, class: &ast::StmtClassDef) { + self.infer_body(&class.body); + } + + fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { + let type_params = function + .type_params + .as_deref() + .expect("function type params scope without type params"); + + self.infer_optional_annotation_expression( + function.returns.as_deref(), + DeferredExpressionState::None, + ); + self.infer_type_parameters(type_params); + self.infer_parameters(&function.parameters); + } + + fn infer_type_alias_type_params(&mut self, type_alias: &ast::StmtTypeAlias) { + let type_params = type_alias + .type_params + .as_ref() + .expect("type alias type params scope without type params"); + + self.infer_type_parameters(type_params); + } + + fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) { + self.infer_annotation_expression(&type_alias.value, DeferredExpressionState::Deferred); + } + + /// If the current scope is a method inside an enclosing class, + /// return `Some(class)` where `class` represents the enclosing class. + /// + /// If the current scope is not a method inside an enclosing class, + /// return `None`. + /// + /// Note that this method will only return `Some` if the immediate parent scope + /// is a class scope OR the immediate parent scope is an annotation scope + /// and the grandparent scope is a class scope. This means it has different + /// behaviour to the [`nearest_enclosing_class`] function. + fn class_context_of_current_method(&self) -> Option> { + let current_scope_id = self.scope().file_scope_id(self.db()); + let current_scope = self.index.scope(current_scope_id); + if current_scope.kind() != ScopeKind::Function { + return None; + } + let parent_scope_id = current_scope.parent()?; + let parent_scope = self.index.scope(parent_scope_id); + + let class_scope = match parent_scope.kind() { + ScopeKind::Class => parent_scope, + ScopeKind::Annotation => { + let class_scope_id = parent_scope.parent()?; + let potentially_class_scope = self.index.scope(class_scope_id); + + match potentially_class_scope.kind() { + ScopeKind::Class => potentially_class_scope, + _ => return None, + } + } + _ => return None, + }; + + let class_stmt = class_scope.node().as_class(self.module())?; + let class_definition = self.index.expect_single_definition(class_stmt); + binding_type(self.db(), class_definition).into_class_literal() + } + + /// If the current scope is a (non-lambda) function, return that function's AST node. + /// + /// If the current scope is not a function (or it is a lambda function), return `None`. + fn current_function_definition(&self) -> Option<&ast::StmtFunctionDef> { + let current_scope_id = self.scope().file_scope_id(self.db()); + let current_scope = self.index.scope(current_scope_id); + if !current_scope.kind().is_non_lambda_function() { + return None; + } + current_scope.node().as_function(self.module()) + } + + fn function_decorator_types<'a>( + &'a self, + function: &'a ast::StmtFunctionDef, + ) -> impl Iterator> + 'a { + let definition = self.index.expect_single_definition(function); + + let definition_types = infer_definition_types(self.db(), definition); + + function + .decorator_list + .iter() + .map(move |decorator| definition_types.expression_type(&decorator.expression)) + } + + /// Returns `true` if the current scope is the function body scope of a function overload (that + /// is, the stub declaration decorated with `@overload`, not the implementation), or an + /// abstract method (decorated with `@abstractmethod`.) + fn in_function_overload_or_abstractmethod(&self) -> bool { + let Some(function) = self.current_function_definition() else { + return false; + }; + + self.function_decorator_types(function) + .any(|decorator_type| { + match decorator_type { + Type::FunctionLiteral(function) => matches!( + function.known(self.db()), + Some(KnownFunction::Overload | KnownFunction::AbstractMethod) + ), + Type::Never => { + // In unreachable code, we infer `Never` for decorators like `typing.overload`. + // Return `true` here to avoid false positive `invalid-return-type` lints for + // `@overload`ed functions without a body in unreachable code. + true + } + _ => false, + } + }) + } + + fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) { + // Parameters are odd: they are Definitions in the function body scope, but have no + // constituent nodes that are part of the function body. In order to get diagnostics + // merged/emitted for them, we need to explicitly infer their definitions here. + for parameter in &function.parameters { + self.infer_definition(parameter); + } + self.infer_body(&function.body); + + if let Some(returns) = function.returns.as_deref() { + fn is_stub_suite(suite: &[ast::Stmt]) -> bool { + match suite { + [ + ast::Stmt::Expr(ast::StmtExpr { value: first, .. }), + ast::Stmt::Expr(ast::StmtExpr { value: second, .. }), + .., + ] => first.is_string_literal_expr() && second.is_ellipsis_literal_expr(), + [ + ast::Stmt::Expr(ast::StmtExpr { value, .. }), + ast::Stmt::Pass(_), + .., + ] => value.is_string_literal_expr(), + [ast::Stmt::Expr(ast::StmtExpr { value, .. }), ..] => { + value.is_ellipsis_literal_expr() || value.is_string_literal_expr() + } + [ast::Stmt::Pass(_)] => true, + _ => false, + } + } + + let has_empty_body = + self.return_types_and_ranges.is_empty() && is_stub_suite(&function.body); + + let mut enclosing_class_context = None; + + if has_empty_body { + if self.in_stub() { + return; + } + if self.in_function_overload_or_abstractmethod() { + return; + } + if let Some(class) = self.class_context_of_current_method() { + enclosing_class_context = Some(class); + if class.is_protocol(self.db()) { + return; + } + } + } + + let declared_ty = self.file_expression_type(returns); + let expected_ty = match declared_ty { + Type::TypeIs(_) => KnownClass::Bool.to_instance(self.db()), + ty => ty, + }; + + let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); + if scope_id.is_generator_function(self.index) { + // TODO: `AsyncGeneratorType` and `GeneratorType` are both generic classes. + // + // If type arguments are supplied to `(Async)Iterable`, `(Async)Iterator`, + // `(Async)Generator` or `(Async)GeneratorType` in the return annotation, + // we should iterate over the `yield` expressions and `return` statements in the function + // to check that they are consistent with the type arguments provided. + let inferred_return = if function.is_async { + KnownClass::AsyncGeneratorType + } else { + KnownClass::GeneratorType + }; + + if !inferred_return + .to_instance(self.db()) + .is_assignable_to(self.db(), expected_ty) + { + report_invalid_generator_function_return_type( + &self.context, + returns.range(), + inferred_return, + declared_ty, + ); + } + return; + } + + for invalid in self + .return_types_and_ranges + .iter() + .copied() + .filter_map(|ty_range| match ty_range.ty { + // We skip `is_assignable_to` checks for `NotImplemented`, + // so we remove it beforehand. + Type::Union(union) => Some(TypeAndRange { + ty: union.filter(self.db(), |ty| !ty.is_notimplemented(self.db())), + range: ty_range.range, + }), + ty if ty.is_notimplemented(self.db()) => None, + _ => Some(ty_range), + }) + .filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), expected_ty)) + { + report_invalid_return_type( + &self.context, + invalid.range, + returns.range(), + declared_ty, + invalid.ty, + ); + } + let use_def = self.index.use_def_map(scope_id); + if use_def.can_implicitly_return_none(self.db()) + && !Type::none(self.db()).is_assignable_to(self.db(), expected_ty) + { + let no_return = self.return_types_and_ranges.is_empty(); + report_implicit_return_type( + &self.context, + returns.range(), + declared_ty, + has_empty_body, + enclosing_class_context, + no_return, + ); + } + } + } + + fn infer_body(&mut self, suite: &[ast::Stmt]) { + for statement in suite { + self.infer_statement(statement); + } + } + + fn infer_statement(&mut self, statement: &ast::Stmt) { + match statement { + ast::Stmt::FunctionDef(function) => self.infer_function_definition_statement(function), + ast::Stmt::ClassDef(class) => self.infer_class_definition_statement(class), + ast::Stmt::Expr(ast::StmtExpr { + range: _, + node_index: _, + value, + }) => { + // If this is a call expression, we would have added a `ReturnsNever` constraint, + // meaning this will be a standalone expression. + self.infer_maybe_standalone_expression(value); + } + ast::Stmt::If(if_statement) => self.infer_if_statement(if_statement), + ast::Stmt::Try(try_statement) => self.infer_try_statement(try_statement), + ast::Stmt::With(with_statement) => self.infer_with_statement(with_statement), + ast::Stmt::Match(match_statement) => self.infer_match_statement(match_statement), + ast::Stmt::Assign(assign) => self.infer_assignment_statement(assign), + ast::Stmt::AnnAssign(assign) => self.infer_annotated_assignment_statement(assign), + ast::Stmt::AugAssign(aug_assign) => { + self.infer_augmented_assignment_statement(aug_assign); + } + ast::Stmt::TypeAlias(type_statement) => self.infer_type_alias_statement(type_statement), + ast::Stmt::For(for_statement) => self.infer_for_statement(for_statement), + ast::Stmt::While(while_statement) => self.infer_while_statement(while_statement), + ast::Stmt::Import(import) => self.infer_import_statement(import), + ast::Stmt::ImportFrom(import) => self.infer_import_from_statement(import), + ast::Stmt::Assert(assert_statement) => self.infer_assert_statement(assert_statement), + ast::Stmt::Raise(raise) => self.infer_raise_statement(raise), + ast::Stmt::Return(ret) => self.infer_return_statement(ret), + ast::Stmt::Delete(delete) => self.infer_delete_statement(delete), + ast::Stmt::Break(_) + | ast::Stmt::Continue(_) + | ast::Stmt::Pass(_) + | ast::Stmt::IpyEscapeCommand(_) + | ast::Stmt::Global(_) + | ast::Stmt::Nonlocal(_) => { + // No-op + } + } + } + + fn infer_definition(&mut self, node: impl Into + std::fmt::Debug + Copy) { + let definition = self.index.expect_single_definition(node); + let result = infer_definition_types(self.db(), definition); + self.extend(result); + } + + fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) { + self.infer_definition(function); + } + + fn infer_function_definition( + &mut self, + function: &ast::StmtFunctionDef, + definition: Definition<'db>, + ) { + let ast::StmtFunctionDef { + range: _, + node_index: _, + is_async: _, + name, + type_params, + parameters, + returns, + body: _, + decorator_list, + } = function; + + let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); + let mut function_decorators = FunctionDecorators::empty(); + let mut dataclass_transformer_params = None; + + for decorator in decorator_list { + let decorator_type = self.infer_decorator(decorator); + let decorator_function_decorator = + FunctionDecorators::from_decorator_type(self.db(), decorator_type); + function_decorators |= decorator_function_decorator; + + match decorator_type { + Type::FunctionLiteral(function) => { + if let Some(KnownFunction::NoTypeCheck) = function.known(self.db()) { + // If the function is decorated with the `no_type_check` decorator, + // we need to suppress any errors that come after the decorators. + self.context.set_in_no_type_check(InNoTypeCheck::Yes); + continue; + } + } + Type::DataclassTransformer(params) => { + dataclass_transformer_params = Some(params); + } + _ => {} + } + if !decorator_function_decorator.is_empty() { + continue; + } + + decorator_types_and_nodes.push((decorator_type, decorator)); + } + + for default in parameters + .iter_non_variadic_params() + .filter_map(|param| param.default.as_deref()) + { + self.infer_expression(default); + } + + // If there are type params, parameters and returns are evaluated in that scope, that is, in + // `infer_function_type_params`, rather than here. + if type_params.is_none() { + if self.defer_annotations() { + self.types.deferred.insert(definition); + } else { + self.infer_optional_annotation_expression( + returns.as_deref(), + DeferredExpressionState::None, + ); + self.infer_parameters(parameters); + } + } + + let known_function = + KnownFunction::try_from_definition_and_name(self.db(), definition, name); + + let body_scope = self + .index + .node_scope(NodeWithScopeRef::Function(function)) + .to_scope_id(self.db(), self.file()); + + let overload_literal = OverloadLiteral::new( + self.db(), + &name.id, + known_function, + body_scope, + function_decorators, + dataclass_transformer_params, + ); + + let inherited_generic_context = None; + let function_literal = + FunctionLiteral::new(self.db(), overload_literal, inherited_generic_context); + + let type_mappings = Box::from([]); + let mut inferred_ty = Type::FunctionLiteral(FunctionType::new( + self.db(), + function_literal, + type_mappings, + )); + + for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { + inferred_ty = match decorator_ty + .try_call(self.db(), &CallArgumentTypes::positional([inferred_ty])) + .map(|bindings| bindings.return_type(self.db())) + { + Ok(return_ty) => return_ty, + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, (*decorator_node).into()); + bindings.return_type(self.db()) + } + }; + } + + self.add_declaration_with_binding( + function.into(), + definition, + &DeclaredAndInferredType::AreTheSame(inferred_ty), + ); + } + + fn infer_parameters(&mut self, parameters: &ast::Parameters) { + let ast::Parameters { + range: _, + node_index: _, + posonlyargs: _, + args: _, + vararg, + kwonlyargs: _, + kwarg, + } = parameters; + + for param_with_default in parameters.iter_non_variadic_params() { + self.infer_parameter_with_default(param_with_default); + } + if let Some(vararg) = vararg { + self.infer_parameter(vararg); + } + if let Some(kwarg) = kwarg { + self.infer_parameter(kwarg); + } + } + + fn infer_parameter_with_default(&mut self, parameter_with_default: &ast::ParameterWithDefault) { + let ast::ParameterWithDefault { + range: _, + node_index: _, + parameter, + default: _, + } = parameter_with_default; + + self.infer_optional_annotation_expression( + parameter.annotation.as_deref(), + DeferredExpressionState::None, + ); + } + + fn infer_parameter(&mut self, parameter: &ast::Parameter) { + let ast::Parameter { + range: _, + node_index: _, + name: _, + annotation, + } = parameter; + + self.infer_optional_annotation_expression( + annotation.as_deref(), + DeferredExpressionState::None, + ); + } + + /// Set initial declared type (if annotated) and inferred type for a function-parameter symbol, + /// in the function body scope. + /// + /// The declared type is the annotated type, if any, or `Unknown`. + /// + /// The inferred type is the annotated type, unioned with the type of the default value, if + /// any. If both types are fully static, this union is a no-op (it should simplify to just the + /// annotated type.) But in a case like `f(x=None)` with no annotated type, we want to infer + /// the type `Unknown | None` for `x`, not just `Unknown`, so that we can error on usage of `x` + /// that would not be valid for `None`. + /// + /// If the default-value type is not assignable to the declared (annotated) type, we ignore the + /// default-value type and just infer the annotated type; this is the same way we handle + /// assignments, and allows an explicit annotation to override a bad inference. + /// + /// Parameter definitions are odd in that they define a symbol in the function-body scope, so + /// the Definition belongs to the function body scope, but the expressions (annotation and + /// default value) both belong to outer scopes. (The default value always belongs to the outer + /// scope in which the function is defined, the annotation belongs either to the outer scope, + /// or maybe to an intervening type-params scope, if it's a generic function.) So we don't use + /// `self.infer_expression` or store any expression types here, we just use `expression_ty` to + /// get the types of the expressions from their respective scopes. + /// + /// It is safe (non-cycle-causing) to use `expression_ty` here, because an outer scope can't + /// depend on a definition from an inner scope, so we shouldn't be in-process of inferring the + /// outer scope here. + fn infer_parameter_definition( + &mut self, + parameter_with_default: &ast::ParameterWithDefault, + definition: Definition<'db>, + ) { + let ast::ParameterWithDefault { + parameter, + default, + range: _, + node_index: _, + } = parameter_with_default; + let default_ty = default + .as_ref() + .map(|default| self.file_expression_type(default)); + if let Some(annotation) = parameter.annotation.as_ref() { + let declared_ty = self.file_expression_type(annotation); + let declared_and_inferred_ty = if let Some(default_ty) = default_ty { + if default_ty.is_assignable_to(self.db(), declared_ty) { + DeclaredAndInferredType::MightBeDifferent { + declared_ty: declared_ty.into(), + inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]), + } + } else if (self.in_stub() + || self.in_function_overload_or_abstractmethod() + || self + .class_context_of_current_method() + .is_some_and(|class| class.is_protocol(self.db()))) + && default + .as_ref() + .is_some_and(|d| d.is_ellipsis_literal_expr()) + { + DeclaredAndInferredType::AreTheSame(declared_ty) + } else { + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMETER_DEFAULT, parameter_with_default) + { + builder.into_diagnostic(format_args!( + "Default value of type `{}` is not assignable \ + to annotated parameter type `{}`", + default_ty.display(self.db()), + declared_ty.display(self.db()) + )); + } + DeclaredAndInferredType::AreTheSame(declared_ty) + } + } else { + DeclaredAndInferredType::AreTheSame(declared_ty) + }; + self.add_declaration_with_binding( + parameter.into(), + definition, + &declared_and_inferred_ty, + ); + } else { + let ty = if let Some(default_ty) = default_ty { + UnionType::from_elements(self.db(), [Type::unknown(), default_ty]) + } else { + Type::unknown() + }; + self.add_binding(parameter.into(), definition, ty); + } + } + + /// Set initial declared/inferred types for a `*args` variadic positional parameter. + /// + /// The annotated type is implicitly wrapped in a homogeneous tuple. + /// + /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. + /// + /// [`infer_parameter_definition`]: Self::infer_parameter_definition + fn infer_variadic_positional_parameter_definition( + &mut self, + parameter: &ast::Parameter, + definition: Definition<'db>, + ) { + if let Some(annotation) = parameter.annotation() { + let ty = if annotation.is_starred_expr() { + todo_type!("PEP 646") + } else { + let annotated_type = self.file_expression_type(annotation); + TupleType::homogeneous(self.db(), annotated_type) + }; + + self.add_declaration_with_binding( + parameter.into(), + definition, + &DeclaredAndInferredType::AreTheSame(ty), + ); + } else { + self.add_binding( + parameter.into(), + definition, + TupleType::homogeneous(self.db(), Type::unknown()), + ); + } + } + + /// Set initial declared/inferred types for a `*args` variadic positional parameter. + /// + /// The annotated type is implicitly wrapped in a string-keyed dictionary. + /// + /// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes. + /// + /// [`infer_parameter_definition`]: Self::infer_parameter_definition + fn infer_variadic_keyword_parameter_definition( + &mut self, + parameter: &ast::Parameter, + definition: Definition<'db>, + ) { + if let Some(annotation) = parameter.annotation() { + let annotated_ty = self.file_expression_type(annotation); + let ty = KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), annotated_ty], + ); + self.add_declaration_with_binding( + parameter.into(), + definition, + &DeclaredAndInferredType::AreTheSame(ty), + ); + } else { + self.add_binding( + parameter.into(), + definition, + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ), + ); + } + } + + fn infer_class_definition_statement(&mut self, class: &ast::StmtClassDef) { + self.infer_definition(class); + } + + fn infer_class_definition( + &mut self, + class_node: &ast::StmtClassDef, + definition: Definition<'db>, + ) { + let ast::StmtClassDef { + range: _, + node_index: _, + name, + type_params, + decorator_list, + arguments: _, + body: _, + } = class_node; + + let mut dataclass_params = None; + let mut dataclass_transformer_params = None; + for decorator in decorator_list { + let decorator_ty = self.infer_decorator(decorator); + if decorator_ty + .into_function_literal() + .is_some_and(|function| function.is_known(self.db(), KnownFunction::Dataclass)) + { + dataclass_params = Some(DataclassParams::default()); + continue; + } + + if let Type::DataclassDecorator(params) = decorator_ty { + dataclass_params = Some(params); + continue; + } + + if let Type::FunctionLiteral(f) = decorator_ty { + // We do not yet detect or flag `@dataclass_transform` applied to more than one + // overload, or an overload and the implementation both. Nevertheless, this is not + // allowed. We do not try to treat the offenders intelligently -- just use the + // params of the last seen usage of `@dataclass_transform` + let params = f + .iter_overloads_and_implementation(self.db()) + .find_map(|overload| overload.dataclass_transformer_params(self.db())); + if let Some(params) = params { + dataclass_params = Some(params.into()); + continue; + } + } + + if let Type::DataclassTransformer(params) = decorator_ty { + dataclass_transformer_params = Some(params); + continue; + } + } + + let body_scope = self + .index + .node_scope(NodeWithScopeRef::Class(class_node)) + .to_scope_id(self.db(), self.file()); + + let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name); + + let class_ty = Type::from(ClassLiteral::new( + self.db(), + name.id.clone(), + body_scope, + maybe_known_class, + dataclass_params, + dataclass_transformer_params, + )); + + self.add_declaration_with_binding( + class_node.into(), + definition, + &DeclaredAndInferredType::AreTheSame(class_ty), + ); + + // if there are type parameters, then the keywords and bases are within that scope + // and we don't need to run inference here + if type_params.is_none() { + for keyword in class_node.keywords() { + self.infer_expression(&keyword.value); + } + + // Inference of bases deferred in stubs + // TODO: Only defer the references that are actually string literals, instead of + // deferring the entire class definition if a string literal occurs anywhere in the + // base class list. + if self.in_stub() || class_node.bases().iter().any(contains_string_literal) { + self.types.deferred.insert(definition); + } else { + for base in class_node.bases() { + self.infer_expression(base); + } + } + } + } + + fn infer_function_deferred(&mut self, function: &ast::StmtFunctionDef) { + self.infer_optional_annotation_expression( + function.returns.as_deref(), + DeferredExpressionState::Deferred, + ); + self.infer_parameters(function.parameters.as_ref()); + } + + fn infer_class_deferred(&mut self, class: &ast::StmtClassDef) { + for base in class.bases() { + self.infer_expression(base); + } + } + + fn infer_type_alias_definition( + &mut self, + type_alias: &ast::StmtTypeAlias, + definition: Definition<'db>, + ) { + self.infer_expression(&type_alias.name); + + let rhs_scope = self + .index + .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) + .to_scope_id(self.db(), self.file()); + + let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType( + TypeAliasType::PEP695(PEP695TypeAliasType::new( + self.db(), + &type_alias.name.as_name_expr().unwrap().id, + rhs_scope, + )), + )); + + self.add_declaration_with_binding( + type_alias.into(), + definition, + &DeclaredAndInferredType::AreTheSame(type_alias_ty), + ); + } + + fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { + let ast::StmtIf { + range: _, + node_index: _, + test, + body, + elif_else_clauses, + } = if_statement; + + let test_ty = self.infer_standalone_expression(test); + + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, &**test); + } + + self.infer_body(body); + + for clause in elif_else_clauses { + let ast::ElifElseClause { + range: _, + node_index: _, + test, + body, + } = clause; + + if let Some(test) = &test { + let test_ty = self.infer_standalone_expression(test); + + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, test); + } + } + + self.infer_body(body); + } + } + + fn infer_try_statement(&mut self, try_statement: &ast::StmtTry) { + let ast::StmtTry { + range: _, + node_index: _, + body, + handlers, + orelse, + finalbody, + is_star: _, + } = try_statement; + + self.infer_body(body); + + for handler in handlers { + let ast::ExceptHandler::ExceptHandler(handler) = handler; + let ast::ExceptHandlerExceptHandler { + type_: handled_exceptions, + name: symbol_name, + body, + range: _, + node_index: _, + } = handler; + + // If `symbol_name` is `Some()` and `handled_exceptions` is `None`, + // it's invalid syntax (something like `except as e:`). + // However, it's obvious that the user *wanted* `e` to be bound here, + // so we'll have created a definition in the semantic-index stage anyway. + if symbol_name.is_some() { + self.infer_definition(handler); + } else { + self.infer_exception(handled_exceptions.as_deref(), try_statement.is_star); + } + + self.infer_body(body); + } + + self.infer_body(orelse); + self.infer_body(finalbody); + } + + fn infer_with_statement(&mut self, with_statement: &ast::StmtWith) { + let ast::StmtWith { + range: _, + node_index: _, + is_async, + items, + body, + } = with_statement; + for item in items { + let target = item.optional_vars.as_deref(); + if let Some(target) = target { + self.infer_target(target, &item.context_expr, |builder, context_expr| { + // TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager + // but only if the target is a name. We should report a diagnostic here if the target isn't a name: + // `with not_context_manager as a.x: ... + builder + .infer_standalone_expression(context_expr) + .enter(builder.db()) + }); + } else { + // Call into the context expression inference to validate that it evaluates + // to a valid context manager. + let context_expression_ty = self.infer_expression(&item.context_expr); + self.infer_context_expression(&item.context_expr, context_expression_ty, *is_async); + self.infer_optional_expression(target); + } + } + + self.infer_body(body); + } + + fn infer_with_item_definition( + &mut self, + with_item: &WithItemDefinitionKind<'db>, + definition: Definition<'db>, + ) { + let context_expr = with_item.context_expr(self.module()); + let target = with_item.target(self.module()); + + let target_ty = if with_item.is_async() { + let _context_expr_ty = self.infer_standalone_expression(context_expr); + todo_type!("async `with` statement") + } else { + match with_item.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } + unpacked.expression_type(target) + } + TargetKind::Single => { + let context_expr_ty = self.infer_standalone_expression(context_expr); + self.infer_context_expression( + context_expr, + context_expr_ty, + with_item.is_async(), + ) + } + } + }; + + self.store_expression_type(target, target_ty); + self.add_binding(target.into(), definition, target_ty); + } + + /// Infers the type of a context expression (`with expr`) and returns the target's type + /// + /// Returns [`Type::unknown`] if the context expression doesn't implement the context manager protocol. + /// + /// ## Terminology + /// See [PEP343](https://peps.python.org/pep-0343/#standard-terminology). + fn infer_context_expression( + &mut self, + context_expression: &ast::Expr, + context_expression_type: Type<'db>, + is_async: bool, + ) -> Type<'db> { + // TODO: Handle async with statements (they use `aenter` and `aexit`) + if is_async { + return todo_type!("async `with` statement"); + } + + context_expression_type + .try_enter(self.db()) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + context_expression_type, + context_expression.into(), + ); + err.fallback_enter_type(self.db()) + }) + } + + fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { + fn extract_tuple_specialization<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { + let class = ty.into_nominal_instance()?.class; + if !class.is_known(db, KnownClass::Tuple) { + return None; + } + let ClassType::Generic(class) = class else { + return None; + }; + let specialization = class.specialization(db).types(db)[0]; + let specialization_instance = specialization.to_instance(db)?; + + specialization_instance + .is_assignable_to(db, KnownClass::BaseException.to_instance(db)) + .then_some(specialization_instance) + } + + // If there is no handled exception, it's invalid syntax; + // a diagnostic will have already been emitted + let node_ty = node.map_or(Type::unknown(), |ty| self.infer_expression(ty)); + let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); + + // If it's an `except*` handler, this won't actually be the type of the bound symbol; + // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. + let symbol_ty = if let Type::Tuple(tuple) = node_ty { + let mut builder = UnionBuilder::new(self.db()); + for element in tuple.tuple(self.db()).all_elements().copied() { + builder = builder.add( + if element.is_assignable_to(self.db(), type_base_exception) { + element.to_instance(self.db()).expect( + "`Type::to_instance()` should always return `Some()` \ + if called on a type assignable to `type[BaseException]`", + ) + } else { + if let Some(node) = node { + report_invalid_exception_caught(&self.context, node, element); + } + Type::unknown() + }, + ); + } + builder.build() + } else if node_ty.is_assignable_to(self.db(), type_base_exception) { + node_ty.to_instance(self.db()).expect( + "`Type::to_instance()` should always return `Some()` \ + if called on a type assignable to `type[BaseException]`", + ) + } else if node_ty.is_assignable_to( + self.db(), + TupleType::homogeneous(self.db(), type_base_exception), + ) { + extract_tuple_specialization(self.db(), node_ty) + .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())) + } else if node_ty.is_assignable_to( + self.db(), + UnionType::from_elements( + self.db(), + [ + type_base_exception, + TupleType::homogeneous(self.db(), type_base_exception), + ], + ), + ) { + KnownClass::BaseException.to_instance(self.db()) + } else { + if let Some(node) = node { + report_invalid_exception_caught(&self.context, node, node_ty); + } + Type::unknown() + }; + + if is_star { + let class = if symbol_ty + .is_subtype_of(self.db(), KnownClass::Exception.to_instance(self.db())) + { + KnownClass::ExceptionGroup + } else { + KnownClass::BaseExceptionGroup + }; + class.to_specialized_instance(self.db(), [symbol_ty]) + } else { + symbol_ty + } + } + + fn infer_except_handler_definition( + &mut self, + except_handler_definition: &ExceptHandlerDefinitionKind, + definition: Definition<'db>, + ) { + let symbol_ty = self.infer_exception( + except_handler_definition.handled_exceptions(self.module()), + except_handler_definition.is_star(), + ); + + self.add_binding( + except_handler_definition.node(self.module()).into(), + definition, + symbol_ty, + ); + } + + fn infer_typevar_definition( + &mut self, + node: &ast::TypeParamTypeVar, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVar { + range: _, + node_index: _, + name, + bound, + default, + } = node; + let bound_or_constraint = match bound.as_deref() { + Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { + if elts.len() < 2 { + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) + { + builder.into_diagnostic("TypeVar must have at least two constrained types"); + } + self.infer_expression(expr); + None + } else { + // We don't use UnionType::from_elements or UnionBuilder here, because we don't + // want to simplify the list of constraints like we do with the elements of an + // actual union type. + // TODO: Consider using a new `OneOfType` connective here instead, since that + // more accurately represents the actual semantics of typevar constraints. + let elements = UnionType::new( + self.db(), + elts.iter() + .map(|expr| self.infer_type_expression(expr)) + .collect::>(), + ); + let constraints = TypeVarBoundOrConstraints::Constraints(elements); + // But when we construct an actual union type for the constraint expression as + // a whole, we do use UnionType::from_elements to maintain the invariant that + // all union types are simplified. + self.store_expression_type( + expr, + UnionType::from_elements(self.db(), elements.elements(self.db())), + ); + Some(constraints) + } + } + Some(expr) => Some(TypeVarBoundOrConstraints::UpperBound( + self.infer_type_expression(expr), + )), + None => None, + }; + let default_ty = self.infer_optional_type_expression(default.as_deref()); + let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + self.db(), + name.id.clone(), + Some(definition), + bound_or_constraint, + TypeVarVariance::Invariant, // TODO: infer this + default_ty, + TypeVarKind::Pep695, + ))); + self.add_declaration_with_binding( + node.into(), + definition, + &DeclaredAndInferredType::AreTheSame(ty), + ); + } + + fn infer_paramspec_definition( + &mut self, + node: &ast::TypeParamParamSpec, + definition: Definition<'db>, + ) { + let ast::TypeParamParamSpec { + range: _, + node_index: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref()); + let pep_695_todo = Type::Dynamic(DynamicType::TodoPEP695ParamSpec); + self.add_declaration_with_binding( + node.into(), + definition, + &DeclaredAndInferredType::AreTheSame(pep_695_todo), + ); + } + + fn infer_typevartuple_definition( + &mut self, + node: &ast::TypeParamTypeVarTuple, + definition: Definition<'db>, + ) { + let ast::TypeParamTypeVarTuple { + range: _, + node_index: _, + name: _, + default, + } = node; + self.infer_optional_expression(default.as_deref()); + let pep_695_todo = todo_type!("PEP-695 TypeVarTuple definition types"); + self.add_declaration_with_binding( + node.into(), + definition, + &DeclaredAndInferredType::AreTheSame(pep_695_todo), + ); + } + + fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { + let ast::StmtMatch { + range: _, + node_index: _, + subject, + cases, + } = match_statement; + + self.infer_standalone_expression(subject); + + for case in cases { + let ast::MatchCase { + range: _, + node_index: _, + body, + pattern, + guard, + } = case; + self.infer_match_pattern(pattern); + + if let Some(guard) = guard.as_deref() { + let guard_ty = self.infer_standalone_expression(guard); + + if let Err(err) = guard_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, guard); + } + } + + self.infer_body(body); + } + } + + fn infer_match_pattern_definition( + &mut self, + pattern: &ast::Pattern, + _index: u32, + definition: Definition<'db>, + ) { + // TODO(dhruvmanila): The correct way to infer types here is to perform structural matching + // against the subject expression type (which we can query via `infer_expression_types`) + // and extract the type at the `index` position if the pattern matches. This will be + // similar to the logic in `self.infer_assignment_definition`. + self.add_binding( + pattern.into(), + definition, + todo_type!("`match` pattern definition types"), + ); + } + + fn infer_match_pattern(&mut self, pattern: &ast::Pattern) { + // We need to create a standalone expression for each arm of a match statement, since they + // can introduce constraints on the match subject. (Or more accurately, for the match arm's + // pattern, since its the pattern that introduces any constraints, not the body.) Ideally, + // that standalone expression would wrap the match arm's pattern as a whole. But a + // standalone expression can currently only wrap an ast::Expr, which patterns are not. So, + // we need to choose an Expr that can “stand in” for the pattern, which we can wrap in a + // standalone expression. + // + // That said, when inferring the type of a standalone expression, we don't have access to + // its parent or sibling nodes. That means, for instance, that in a class pattern, where + // we are currently using the class name as the standalone expression, we do not have + // access to the class pattern's arguments in the standalone expression inference scope. + // At the moment, we aren't trying to do anything with those arguments when creating a + // narrowing constraint for the pattern. But in the future, if we do, we will have to + // either wrap those arguments in their own standalone expressions, or update Expression to + // be able to wrap other AST node types besides just ast::Expr. + // + // This function is only called for the top-level pattern of a match arm, and is + // responsible for inferring the standalone expression for each supported pattern type. It + // then hands off to `infer_nested_match_pattern` for any subexpressions and subpatterns, + // where we do NOT have any additional standalone expressions to infer through. + // + // TODO(dhruvmanila): Add a Salsa query for inferring pattern types and matching against + // the subject expression: https://github.com/astral-sh/ruff/pull/13147#discussion_r1739424510 + match pattern { + ast::Pattern::MatchValue(match_value) => { + self.infer_standalone_expression(&match_value.value); + } + ast::Pattern::MatchClass(match_class) => { + let ast::PatternMatchClass { + range: _, + node_index: _, + cls, + arguments, + } = match_class; + for pattern in &arguments.patterns { + self.infer_nested_match_pattern(pattern); + } + for keyword in &arguments.keywords { + self.infer_nested_match_pattern(&keyword.pattern); + } + self.infer_standalone_expression(cls); + } + ast::Pattern::MatchOr(match_or) => { + for pattern in &match_or.patterns { + self.infer_match_pattern(pattern); + } + } + _ => { + self.infer_nested_match_pattern(pattern); + } + } + } + + fn infer_nested_match_pattern(&mut self, pattern: &ast::Pattern) { + match pattern { + ast::Pattern::MatchValue(match_value) => { + self.infer_expression(&match_value.value); + } + ast::Pattern::MatchSequence(match_sequence) => { + for pattern in &match_sequence.patterns { + self.infer_nested_match_pattern(pattern); + } + } + ast::Pattern::MatchMapping(match_mapping) => { + let ast::PatternMatchMapping { + range: _, + node_index: _, + keys, + patterns, + rest: _, + } = match_mapping; + for key in keys { + self.infer_expression(key); + } + for pattern in patterns { + self.infer_nested_match_pattern(pattern); + } + } + ast::Pattern::MatchClass(match_class) => { + let ast::PatternMatchClass { + range: _, + node_index: _, + cls, + arguments, + } = match_class; + for pattern in &arguments.patterns { + self.infer_nested_match_pattern(pattern); + } + for keyword in &arguments.keywords { + self.infer_nested_match_pattern(&keyword.pattern); + } + self.infer_expression(cls); + } + ast::Pattern::MatchAs(match_as) => { + if let Some(pattern) = &match_as.pattern { + self.infer_nested_match_pattern(pattern); + } + } + ast::Pattern::MatchOr(match_or) => { + for pattern in &match_or.patterns { + self.infer_nested_match_pattern(pattern); + } + } + ast::Pattern::MatchStar(_) | ast::Pattern::MatchSingleton(_) => {} + } + } + + fn infer_assignment_statement(&mut self, assignment: &ast::StmtAssign) { + let ast::StmtAssign { + range: _, + node_index: _, + targets, + value, + } = assignment; + + for target in targets { + self.infer_target(target, value, |builder, value_expr| { + builder.infer_standalone_expression(value_expr) + }); + } + } + + /// Infer the (definition) types involved in a `target` expression. + /// + /// This is used for assignment statements, for statements, etc. with a single or multiple + /// targets (unpacking). If `target` is an attribute expression, we check that the assignment + /// is valid. For 'target's that are definitions, this check happens elsewhere. + /// + /// The `infer_value_expr` function is used to infer the type of the `value` expression which + /// are not `Name` expressions. The returned type is the one that is eventually assigned to the + /// `target`. + fn infer_target(&mut self, target: &ast::Expr, value: &ast::Expr, infer_value_expr: F) + where + F: Fn(&mut TypeInferenceBuilder<'db, '_>, &ast::Expr) -> Type<'db>, + { + let assigned_ty = match target { + ast::Expr::Name(_) => None, + _ => Some(infer_value_expr(self, value)), + }; + self.infer_target_impl(target, assigned_ty); + } + + /// Make sure that the attribute assignment `obj.attribute = value` is valid. + /// + /// `target` is the node for the left-hand side, `object_ty` is the type of `obj`, `attribute` is + /// the name of the attribute being assigned, and `value_ty` is the type of the right-hand side of + /// the assignment. If the assignment is invalid, emit diagnostics. + fn validate_attribute_assignment( + &mut self, + target: &ast::ExprAttribute, + object_ty: Type<'db>, + attribute: &str, + value_ty: Type<'db>, + emit_diagnostics: bool, + ) -> bool { + let db = self.db(); + + let ensure_assignable_to = |attr_ty| -> bool { + let assignable = value_ty.is_assignable_to(db, attr_ty); + if !assignable && emit_diagnostics { + report_invalid_attribute_assignment( + &self.context, + target.into(), + attr_ty, + value_ty, + attribute, + ); + } + assignable + }; + + match object_ty { + Type::Union(union) => { + if union.elements(self.db()).iter().all(|elem| { + self.validate_attribute_assignment(target, *elem, attribute, value_ty, false) + }) { + true + } else { + // TODO: This is not a very helpful error message, as it does not include the underlying reason + // why the assignment is invalid. This would be a good use case for sub-diagnostics. + if emit_diagnostics { + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Object of type `{}` is not assignable \ + to attribute `{attribute}` on type `{}`", + value_ty.display(self.db()), + object_ty.display(self.db()), + )); + } + } + + false + } + } + + Type::Intersection(intersection) => { + // TODO: Handle negative intersection elements + if intersection.positive(db).iter().any(|elem| { + self.validate_attribute_assignment(target, *elem, attribute, value_ty, false) + }) { + true + } else { + if emit_diagnostics { + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: same here, see above + builder.into_diagnostic(format_args!( + "Object of type `{}` is not assignable \ + to attribute `{attribute}` on type `{}`", + value_ty.display(self.db()), + object_ty.display(self.db()), + )); + } + } + false + } + } + + // Super instances do not allow attribute assignment + Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Super) => { + if emit_diagnostics { + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { + builder.into_diagnostic(format_args!( + "Cannot assign to attribute `{attribute}` on type `{}`", + object_ty.display(self.db()), + )); + } + } + false + } + Type::BoundSuper(_) => { + if emit_diagnostics { + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { + builder.into_diagnostic(format_args!( + "Cannot assign to attribute `{attribute}` on type `{}`", + object_ty.display(self.db()), + )); + } + } + false + } + + Type::Dynamic(..) | Type::Never => true, + + Type::NominalInstance(..) + | Type::ProtocolInstance(_) + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::LiteralString + | Type::Tuple(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..) + | Type::PropertyInstance(..) + | Type::FunctionLiteral(..) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::TypeVar(..) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::TypeIs(_) => { + let is_read_only = || { + let dataclass_params = match object_ty { + Type::NominalInstance(instance) => match instance.class { + ClassType::NonGeneric(cls) => cls.dataclass_params(self.db()), + ClassType::Generic(cls) => { + cls.origin(self.db()).dataclass_params(self.db()) + } + }, + _ => None, + }; + + dataclass_params.is_some_and(|params| params.contains(DataclassParams::FROZEN)) + }; + + // First, try to call the `__setattr__` dunder method. If this is present/defined, overrides + // assigning the attributed by the normal mechanism. + let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy( + db, + "__setattr__", + &mut CallArgumentTypes::positional([ + Type::StringLiteral(StringLiteralType::new(db, Box::from(attribute))), + value_ty, + ]), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ); + + let check_setattr_return_type = |result: Bindings<'db>| -> bool { + match result.return_type(db) { + Type::Never => { + if emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to attribute `{attribute}` on type `{}` \ + whose `__setattr__` method returns `Never`/`NoReturn`", + object_ty.display(db) + )); + } + } + false + } + _ => true, + } + }; + + match setattr_dunder_call_result { + Ok(result) => check_setattr_return_type(result), + Err(CallDunderError::PossiblyUnbound(result)) => { + check_setattr_return_type(*result) + } + Err(CallDunderError::CallError(..)) => { + if emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Can not assign object of type `{}` to attribute \ + `{attribute}` on type `{}` with \ + custom `__setattr__` method.", + value_ty.display(db), + object_ty.display(db) + )); + } + } + false + } + Err(CallDunderError::MethodNotAvailable) => { + match object_ty.class_member(db, attribute.into()) { + meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => { + if emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to ClassVar `{attribute}` \ + from an instance of type `{ty}`", + ty = object_ty.display(self.db()), + )); + } + } + false + } + PlaceAndQualifiers { + place: Place::Type(meta_attr_ty, meta_attr_boundness), + qualifiers: _, + } => { + if is_read_only() { + if emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Property `{attribute}` defined in `{ty}` is read-only", + ty = object_ty.display(self.db()), + )); + } + } + false + } else { + let assignable_to_meta_attr = + if let Place::Type(meta_dunder_set, _) = + meta_attr_ty.class_member(db, "__set__".into()).place + { + let successful_call = meta_dunder_set + .try_call( + db, + &CallArgumentTypes::positional([ + meta_attr_ty, + object_ty, + value_ty, + ]), + ) + .is_ok(); + + if !successful_call && emit_diagnostics { + if let Some(builder) = self + .context + .report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed + builder.into_diagnostic(format_args!( + "Invalid assignment to data descriptor attribute \ + `{attribute}` on type `{}` with custom `__set__` method", + object_ty.display(db) + )); + } + } + + successful_call + } else { + ensure_assignable_to(meta_attr_ty) + }; + + let assignable_to_instance_attribute = + if meta_attr_boundness == Boundness::PossiblyUnbound { + let (assignable, boundness) = if let Place::Type( + instance_attr_ty, + instance_attr_boundness, + ) = + object_ty.instance_member(db, attribute).place + { + ( + ensure_assignable_to(instance_attr_ty), + instance_attr_boundness, + ) + } else { + (true, Boundness::PossiblyUnbound) + }; + + if boundness == Boundness::PossiblyUnbound { + report_possibly_unbound_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } + + assignable + } else { + true + }; + + assignable_to_meta_attr && assignable_to_instance_attribute + } + } + + PlaceAndQualifiers { + place: Place::Unbound, + .. + } => { + if let Place::Type(instance_attr_ty, instance_attr_boundness) = + object_ty.instance_member(db, attribute).place + { + if instance_attr_boundness == Boundness::PossiblyUnbound { + report_possibly_unbound_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } + + if is_read_only() { + if emit_diagnostics { + if let Some(builder) = self + .context + .report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Property `{attribute}` defined in `{ty}` is read-only", + ty = object_ty.display(self.db()), + )); + } + } + false + } else { + ensure_assignable_to(instance_attr_ty) + } + } else { + if emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + )); + } + } + + false + } + } + } + } + } + } + + Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { + match object_ty.class_member(db, attribute.into()) { + PlaceAndQualifiers { + place: Place::Type(meta_attr_ty, meta_attr_boundness), + qualifiers: _, + } => { + let assignable_to_meta_attr = if let Place::Type(meta_dunder_set, _) = + meta_attr_ty.class_member(db, "__set__".into()).place + { + let successful_call = meta_dunder_set + .try_call( + db, + &CallArgumentTypes::positional([ + meta_attr_ty, + object_ty, + value_ty, + ]), + ) + .is_ok(); + + if !successful_call && emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed + builder.into_diagnostic(format_args!( + "Invalid assignment to data descriptor attribute \ + `{attribute}` on type `{}` with custom `__set__` method", + object_ty.display(db) + )); + } + } + + successful_call + } else { + ensure_assignable_to(meta_attr_ty) + }; + + let assignable_to_class_attr = if meta_attr_boundness + == Boundness::PossiblyUnbound + { + let (assignable, boundness) = + if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty + .find_name_in_mro(db, attribute) + .expect("called on Type::ClassLiteral or Type::SubclassOf") + .place + { + (ensure_assignable_to(class_attr_ty), class_attr_boundness) + } else { + (true, Boundness::PossiblyUnbound) + }; + + if boundness == Boundness::PossiblyUnbound { + report_possibly_unbound_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } + + assignable + } else { + true + }; + + assignable_to_meta_attr && assignable_to_class_attr + } + PlaceAndQualifiers { + place: Place::Unbound, + .. + } => { + if let Place::Type(class_attr_ty, class_attr_boundness) = object_ty + .find_name_in_mro(db, attribute) + .expect("called on Type::ClassLiteral or Type::SubclassOf") + .place + { + if class_attr_boundness == Boundness::PossiblyUnbound { + report_possibly_unbound_attribute( + &self.context, + target, + attribute, + object_ty, + ); + } + + ensure_assignable_to(class_attr_ty) + } else { + let attribute_is_bound_on_instance = + object_ty.to_instance(self.db()).is_some_and(|instance| { + !instance + .instance_member(self.db(), attribute) + .place + .is_unbound() + }); + + // Attribute is declared or bound on instance. Forbid access from the class object + if emit_diagnostics { + if attribute_is_bound_on_instance { + if let Some(builder) = + self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to instance attribute \ + `{attribute}` from the class object `{ty}`", + ty = object_ty.display(self.db()), + )); + } + } else { + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + )); + } + } + } + + false + } + } + } + } + + Type::ModuleLiteral(module) => { + if let Place::Type(attr_ty, _) = module.static_member(db, attribute).place { + let assignable = value_ty.is_assignable_to(db, attr_ty); + if assignable { + true + } else { + if emit_diagnostics { + report_invalid_attribute_assignment( + &self.context, + target.into(), + attr_ty, + value_ty, + attribute, + ); + } + false + } + } else { + if emit_diagnostics { + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + )); + } + } + + false + } + } + } + } + + fn infer_target_impl(&mut self, target: &ast::Expr, assigned_ty: Option>) { + match target { + ast::Expr::Name(name) => self.infer_definition(name), + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + let mut assigned_tys = match assigned_ty { + Some(Type::Tuple(tuple)) => { + Either::Left(tuple.tuple(self.db()).all_elements().copied()) + } + Some(_) | None => Either::Right(std::iter::empty()), + }; + + for element in elts { + self.infer_target_impl(element, assigned_tys.next()); + } + } + ast::Expr::Attribute( + attr_expr @ ast::ExprAttribute { + value: object, + ctx: ExprContext::Store, + attr, + .. + }, + ) => { + self.store_expression_type(target, assigned_ty.unwrap_or(Type::unknown())); + + let object_ty = self.infer_expression(object); + + if let Some(assigned_ty) = assigned_ty { + self.validate_attribute_assignment( + attr_expr, + object_ty, + attr.id(), + assigned_ty, + true, + ); + } + } + _ => { + // TODO: Remove this once we handle all possible assignment targets. + self.infer_expression(target); + } + } + } + + fn infer_assignment_definition( + &mut self, + assignment: &AssignmentDefinitionKind<'db>, + definition: Definition<'db>, + ) { + let value = assignment.value(self.module()); + let target = assignment.target(self.module()); + + let mut target_ty = match assignment.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + // Only copy the diagnostics if this is the first assignment to avoid duplicating the + // unpack assignments. + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } + + unpacked.expression_type(target) + } + TargetKind::Single => { + let value_ty = self.infer_standalone_expression(value); + + // `TYPE_CHECKING` is a special variable that should only be assigned `False` + // at runtime, but is always considered `True` in type checking. + // See mdtest/known_constants.md#user-defined-type_checking for details. + if target.as_name_expr().map(|name| name.id.as_str()) == Some("TYPE_CHECKING") { + if !matches!( + value.as_boolean_literal_expr(), + Some(ast::ExprBooleanLiteral { value: false, .. }) + ) { + report_invalid_type_checking_constant(&self.context, target.into()); + } + Type::BooleanLiteral(true) + } else if self.in_stub() && value.is_ellipsis_literal_expr() { + Type::unknown() + } else { + value_ty + } + } + }; + + if let Some(special_form) = target.as_name_expr().and_then(|name| { + SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name.id) + }) { + target_ty = Type::SpecialForm(special_form); + } + + self.store_expression_type(target, target_ty); + self.add_binding(target.into(), definition, target_ty); + } + + fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) { + if assignment.target.is_name_expr() { + self.infer_definition(assignment); + } else { + // Non-name assignment targets are inferred as ordinary expressions, not definitions. + let ast::StmtAnnAssign { + range: _, + node_index: _, + annotation, + value, + target, + simple: _, + } = assignment; + let annotated = + self.infer_annotation_expression(annotation, DeferredExpressionState::None); + self.infer_optional_expression(value.as_deref()); + + // If we have an annotated assignment like `self.attr: int = 1`, we still need to + // do type inference on the `self.attr` target to get types for all sub-expressions. + self.infer_expression(target); + + // But here we explicitly overwrite the type for the overall `self.attr` node with + // the annotated type. We do no use `store_expression_type` here, because it checks + // that no type has been stored for the expression before. + self.types + .expressions + .insert((&**target).into(), annotated.inner_type()); + } + } + + /// Infer the types in an annotated assignment definition. + fn infer_annotated_assignment_definition( + &mut self, + assignment: &'db AnnotatedAssignmentDefinitionKind, + definition: Definition<'db>, + ) { + let annotation = assignment.annotation(self.module()); + let target = assignment.target(self.module()); + let value = assignment.value(self.module()); + + let mut declared_ty = self.infer_annotation_expression( + annotation, + DeferredExpressionState::from(self.defer_annotations()), + ); + + if target + .as_name_expr() + .is_some_and(|name| &name.id == "TYPE_CHECKING") + { + if !KnownClass::Bool + .to_instance(self.db()) + .is_assignable_to(self.db(), declared_ty.inner_type()) + { + // annotation not assignable from `bool` is an error + report_invalid_type_checking_constant(&self.context, target.into()); + } else if self.in_stub() + && value + .as_ref() + .is_none_or(|value| value.is_ellipsis_literal_expr()) + { + // stub file assigning nothing or `...` is fine + } else if !matches!( + value + .as_ref() + .and_then(|value| value.as_boolean_literal_expr()), + Some(ast::ExprBooleanLiteral { value: false, .. }) + ) { + // otherwise, assigning something other than `False` is an error + report_invalid_type_checking_constant(&self.context, target.into()); + } + declared_ty.inner = Type::BooleanLiteral(true); + } + + // Handle various singletons. + if let Type::NominalInstance(instance) = declared_ty.inner_type() { + if instance.class.is_known(self.db(), KnownClass::SpecialForm) { + if let Some(name_expr) = target.as_name_expr() { + if let Some(special_form) = SpecialFormType::try_from_file_and_name( + self.db(), + self.file(), + &name_expr.id, + ) { + declared_ty.inner = Type::SpecialForm(special_form); + } + } + } + } + + // If the target of an assignment is not one of the place expressions we support, + // then they are not definitions, so we can only be here if the target is in a form supported as a place expression. + // In this case, we can simply store types in `target` below, instead of calling `infer_expression` (which would return `Never`). + debug_assert!(PlaceExpr::try_from(target).is_ok()); + + if let Some(value) = value { + let inferred_ty = self.infer_expression(value); + let inferred_ty = if target + .as_name_expr() + .is_some_and(|name| &name.id == "TYPE_CHECKING") + { + Type::BooleanLiteral(true) + } else if self.in_stub() && value.is_ellipsis_literal_expr() { + declared_ty.inner_type() + } else { + inferred_ty + }; + self.add_declaration_with_binding( + target.into(), + definition, + &DeclaredAndInferredType::MightBeDifferent { + declared_ty, + inferred_ty, + }, + ); + + self.store_expression_type(target, inferred_ty); + } else { + if self.in_stub() { + self.add_declaration_with_binding( + target.into(), + definition, + &DeclaredAndInferredType::AreTheSame(declared_ty.inner_type()), + ); + } else { + self.add_declaration(target.into(), definition, declared_ty); + } + + self.store_expression_type(target, declared_ty.inner_type()); + } + } + + fn infer_augmented_assignment_statement(&mut self, assignment: &ast::StmtAugAssign) { + if assignment.target.is_name_expr() { + self.infer_definition(assignment); + } else { + // Non-name assignment targets are inferred as ordinary expressions, not definitions. + self.infer_augment_assignment(assignment); + } + } + + fn infer_augmented_op( + &mut self, + assignment: &ast::StmtAugAssign, + target_type: Type<'db>, + value_type: Type<'db>, + ) -> Type<'db> { + // If the target defines, e.g., `__iadd__`, infer the augmented assignment as a call to that + // dunder. + let op = assignment.op; + let db = self.db(); + + let report_unsupported_augmented_op = |ctx: &mut InferContext| { + let Some(builder) = ctx.report_lint(&UNSUPPORTED_OPERATOR, assignment) else { + return; + }; + builder.into_diagnostic(format_args!( + "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", + target_type.display(db), + value_type.display(db) + )); + }; + + // Fall back to non-augmented binary operator inference. + let mut binary_return_ty = || { + self.infer_binary_expression_type(assignment.into(), false, target_type, value_type, op) + .unwrap_or_else(|| { + report_unsupported_augmented_op(&mut self.context); + Type::unknown() + }) + }; + + match target_type { + Type::Union(union) => union.map(db, |&elem_type| { + self.infer_augmented_op(assignment, elem_type, value_type) + }), + _ => { + let call = target_type.try_call_dunder( + db, + op.in_place_dunder(), + CallArgumentTypes::positional([value_type]), + ); + + match call { + Ok(outcome) => outcome.return_type(db), + Err(CallDunderError::MethodNotAvailable) => binary_return_ty(), + Err(CallDunderError::PossiblyUnbound(outcome)) => { + UnionType::from_elements(db, [outcome.return_type(db), binary_return_ty()]) + } + Err(CallDunderError::CallError(_, bindings)) => { + report_unsupported_augmented_op(&mut self.context); + bindings.return_type(db) + } + } + } + } + } + + fn infer_augment_assignment_definition( + &mut self, + assignment: &ast::StmtAugAssign, + definition: Definition<'db>, + ) { + let target_ty = self.infer_augment_assignment(assignment); + self.add_binding(assignment.into(), definition, target_ty); + } + + fn infer_augment_assignment(&mut self, assignment: &ast::StmtAugAssign) -> Type<'db> { + let ast::StmtAugAssign { + range: _, + node_index: _, + target, + op: _, + value, + } = assignment; + + // Resolve the target type, assuming a load context. + let target_type = match &**target { + ast::Expr::Name(name) => { + let previous_value = self.infer_name_load(name); + self.store_expression_type(target, previous_value); + previous_value + } + ast::Expr::Attribute(attr) => { + let previous_value = self.infer_attribute_load(attr); + self.store_expression_type(target, previous_value); + previous_value + } + ast::Expr::Subscript(subscript) => { + let previous_value = self.infer_subscript_load(subscript); + self.store_expression_type(target, previous_value); + previous_value + } + _ => self.infer_expression(target), + }; + let value_type = self.infer_expression(value); + + self.infer_augmented_op(assignment, target_type, value_type) + } + + fn infer_type_alias_statement(&mut self, node: &ast::StmtTypeAlias) { + self.infer_definition(node); + } + + fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) { + let ast::StmtFor { + range: _, + node_index: _, + target, + iter, + body, + orelse, + is_async: _, + } = for_statement; + + self.infer_target(target, iter, |builder, iter_expr| { + // TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable + // but only if the target is a name. We should report a diagnostic here if the target isn't a name: + // `for a.x in not_iterable: ... + builder + .infer_standalone_expression(iter_expr) + .iterate(builder.db()) + }); + + self.infer_body(body); + self.infer_body(orelse); + } + + fn infer_for_statement_definition( + &mut self, + for_stmt: &ForStmtDefinitionKind<'db>, + definition: Definition<'db>, + ) { + let iterable = for_stmt.iterable(self.module()); + let target = for_stmt.target(self.module()); + + let loop_var_value_type = if for_stmt.is_async() { + let _iterable_type = self.infer_standalone_expression(iterable); + todo_type!("async iterables/iterators") + } else { + match for_stmt.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } + + unpacked.expression_type(target) + } + TargetKind::Single => { + let iterable_type = self.infer_standalone_expression(iterable); + iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, iterable.into()); + err.fallback_element_type(self.db()) + }) + } + } + }; + + self.store_expression_type(target, loop_var_value_type); + self.add_binding(target.into(), definition, loop_var_value_type); + } + + fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) { + let ast::StmtWhile { + range: _, + node_index: _, + test, + body, + orelse, + } = while_statement; + + let test_ty = self.infer_standalone_expression(test); + + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, &**test); + } + + self.infer_body(body); + self.infer_body(orelse); + } + + fn infer_import_statement(&mut self, import: &ast::StmtImport) { + let ast::StmtImport { + range: _, + node_index: _, + names, + } = import; + + for alias in names { + self.infer_definition(alias); + } + } + + fn report_unresolved_import( + &self, + import_node: AnyNodeRef<'_>, + range: TextRange, + level: u32, + module: Option<&str>, + ) { + let is_import_reachable = self.is_reachable(import_node); + + if !is_import_reachable { + return; + } + + let Some(builder) = self.context.report_lint(&UNRESOLVED_IMPORT, range) else { + return; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot resolve imported module `{}{}`", + ".".repeat(level as usize), + module.unwrap_or_default() + )); + if level == 0 { + if let Some(module_name) = module.and_then(ModuleName::new) { + let program = Program::get(self.db()); + let typeshed_versions = program.search_paths(self.db()).typeshed_versions(); + + if let Some(version_range) = typeshed_versions.exact(&module_name) { + // We know it is a stdlib module on *some* Python versions... + let python_version = program.python_version(self.db()); + if !version_range.contains(python_version) { + // ...But not on *this* Python version. + diagnostic.info(format_args!( + "The stdlib module `{module_name}` is only available on Python {version_range}", + version_range = version_range.diagnostic_display(), + )); + add_inferred_python_version_hint_to_diagnostic( + self.db(), + &mut diagnostic, + "resolving modules", + ); + return; + } + } + } + + diagnostic.info( + "make sure your Python environment is properly configured: \ + https://docs.astral.sh/ty/modules/#python-environment", + ); + } + } + + fn infer_import_definition( + &mut self, + node: &ast::StmtImport, + alias: &ast::Alias, + definition: Definition<'db>, + ) { + let ast::Alias { + range: _, + node_index: _, + name, + asname, + } = alias; + + // The name of the module being imported + let Some(full_module_name) = ModuleName::new(name) else { + tracing::debug!("Failed to resolve import due to invalid syntax"); + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + // Resolve the module being imported. + let Some(full_module_ty) = self.module_type_from_name(&full_module_name) else { + self.report_unresolved_import(node.into(), alias.range(), 0, Some(name)); + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + let binding_ty = if asname.is_some() { + // If we are renaming the imported module via an `as` clause, then we bind the resolved + // module's type to that name, even if that module is nested. + full_module_ty + } else if full_module_name.contains('.') { + // If there's no `as` clause and the imported module is nested, we're not going to bind + // the resolved module itself into the current scope; we're going to bind the top-most + // parent package of that module. + let topmost_parent_name = + ModuleName::new(full_module_name.components().next().unwrap()).unwrap(); + let Some(topmost_parent_ty) = self.module_type_from_name(&topmost_parent_name) else { + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + topmost_parent_ty + } else { + // If there's no `as` clause and the imported module isn't nested, then the imported + // module _is_ what we bind into the current scope. + full_module_ty + }; + + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::AreTheSame(binding_ty), + ); + } + + fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) { + let ast::StmtImportFrom { + range: _, + node_index: _, + module: _, + names, + level: _, + } = import; + + self.check_import_from_module_is_resolvable(import); + + for alias in names { + for definition in self.index.definitions(alias) { + self.extend(infer_definition_types(self.db(), *definition)); + } + } + } + + fn infer_assert_statement(&mut self, assert: &ast::StmtAssert) { + let ast::StmtAssert { + range: _, + node_index: _, + test, + msg, + } = assert; + + let test_ty = self.infer_standalone_expression(test); + + if let Err(err) = test_ty.try_bool(self.db()) { + err.report_diagnostic(&self.context, &**test); + } + + self.infer_optional_expression(msg.as_deref()); + } + + fn infer_raise_statement(&mut self, raise: &ast::StmtRaise) { + let ast::StmtRaise { + range: _, + node_index: _, + exc, + cause, + } = raise; + + let base_exception_type = KnownClass::BaseException.to_subclass_of(self.db()); + let base_exception_instance = KnownClass::BaseException.to_instance(self.db()); + + let can_be_raised = + UnionType::from_elements(self.db(), [base_exception_type, base_exception_instance]); + let can_be_exception_cause = + UnionType::from_elements(self.db(), [can_be_raised, Type::none(self.db())]); + + if let Some(raised) = exc { + let raised_type = self.infer_expression(raised); + + if !raised_type.is_assignable_to(self.db(), can_be_raised) { + report_invalid_exception_raised(&self.context, raised, raised_type); + } + } + + if let Some(cause) = cause { + let cause_type = self.infer_expression(cause); + + if !cause_type.is_assignable_to(self.db(), can_be_exception_cause) { + report_invalid_exception_cause(&self.context, cause, cause_type); + } + } + } + + /// 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 check_import_from_module_is_resolvable(&mut self, import_from: &ast::StmtImportFrom) { + let ast::StmtImportFrom { module, level, .. } = import_from; + + // For diagnostics, we want to highlight the unresolvable + // module and not the entire `from ... import ...` statement. + let module_ref = module + .as_ref() + .map(AnyNodeRef::from) + .unwrap_or_else(|| AnyNodeRef::from(import_from)); + let module = module.as_deref(); + + tracing::trace!( + "Resolving import statement from module `{}` into file `{}`", + 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. + return; + } + Err(ModuleNameResolutionError::TooManyDots) => { + tracing::debug!( + "Relative module resolution `{}` failed: too many leading dots", + format_import_from_module(*level, module), + ); + self.report_unresolved_import( + import_from.into(), + module_ref.range(), + *level, + module, + ); + return; + } + Err(ModuleNameResolutionError::UnknownCurrentModule) => { + tracing::debug!( + "Relative module resolution `{}` failed; could not resolve file `{}` to a module", + format_import_from_module(*level, module), + self.file().path(self.db()) + ); + self.report_unresolved_import( + import_from.into(), + module_ref.range(), + *level, + module, + ); + return; + } + }; + + if resolve_module(self.db(), &module_name).is_none() { + self.report_unresolved_import(import_from.into(), module_ref.range(), *level, module); + } + } + + fn infer_import_from_definition( + &mut self, + import_from: &ast::StmtImportFrom, + alias: &ast::Alias, + definition: Definition<'db>, + ) { + let Ok(module_name) = + ModuleName::from_import_statement(self.db(), self.file(), import_from) + else { + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + let Some(module) = resolve_module(self.db(), &module_name) else { + self.add_unknown_declaration_with_binding(alias.into(), definition); + return; + }; + + let module_ty = Type::module_literal(self.db(), self.file(), &module); + + // 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 + .place_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 + .place_expr(star_import.place_id()) + .expect_name() + } else { + &alias.name.id + }; + + // Avoid looking up attributes on a module if a module imports from itself + // (e.g. `from parent import submodule` inside the `parent` module). + let import_is_self_referential = module_ty + .into_module_literal() + .is_some_and(|module| Some(self.file()) == module.module(self.db()).file()); + + // First try loading the requested attribute from the module. + if !import_is_self_referential { + if let PlaceAndQualifiers { + place: Place::Type(ty, boundness), + qualifiers, + } = module_ty.member(self.db(), name) + { + 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. + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_UNBOUND_IMPORT, AnyNodeRef::Alias(alias)) + { + builder.into_diagnostic(format_args!( + "Member `{name}` of module `{module_name}` is possibly unbound", + )); + } + } + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::MightBeDifferent { + declared_ty: TypeAndQualifiers { + inner: ty, + qualifiers, + }, + inferred_ty: ty, + }, + ); + return; + } + } + + // Evaluate whether `X.Y` would constitute a valid submodule name, + // given a `from X import Y` statement. If it is valid, this will be `Some()`; + // else, it will be `None`. + let full_submodule_name = ModuleName::new(name).map(|final_part| { + let mut ret = module_name.clone(); + ret.extend(&final_part); + ret + }); + + // If the module doesn't bind the symbol, check if it's a submodule. This won't get + // handled by the `Type::member` call because it relies on the semantic index's + // `imported_modules` set. The semantic index does not include information about + // `from...import` statements because there are two things it cannot determine while only + // inspecting the content of the current file: + // + // - whether the imported symbol is an attribute or submodule + // - whether the containing file is in a module or a package (needed to correctly resolve + // relative imports) + // + // The first would be solvable by making it a _potentially_ imported modules set. The + // second is not. + // + // Regardless, for now, we sidestep all of that by repeating the submodule-or-attribute + // check here when inferring types for a `from...import` statement. + if let Some(submodule_type) = full_submodule_name + .as_ref() + .and_then(|submodule_name| self.module_type_from_name(submodule_name)) + { + self.add_declaration_with_binding( + alias.into(), + definition, + &DeclaredAndInferredType::AreTheSame(submodule_type), + ); + return; + } + + self.add_unknown_declaration_with_binding(alias.into(), definition); + + if &alias.name == "*" { + return; + } + + if !self.is_reachable(import_from) { + return; + } + + let Some(builder) = self + .context + .report_lint(&UNRESOLVED_IMPORT, AnyNodeRef::Alias(alias)) + else { + return; + }; + + let diagnostic = builder.into_diagnostic(format_args!( + "Module `{module_name}` has no member `{name}`" + )); + + if let Some(full_submodule_name) = full_submodule_name { + hint_if_stdlib_submodule_exists_on_other_versions( + self.db(), + diagnostic, + &full_submodule_name, + &module, + ); + } + } + + fn infer_return_statement(&mut self, ret: &ast::StmtReturn) { + if let Some(ty) = self.infer_optional_expression(ret.value.as_deref()) { + let range = ret + .value + .as_ref() + .map_or(ret.range(), |value| value.range()); + self.record_return_type(ty, range); + } else { + self.record_return_type(Type::none(self.db()), ret.range()); + } + } + + fn infer_delete_statement(&mut self, delete: &ast::StmtDelete) { + let ast::StmtDelete { + range: _, + node_index: _, + targets, + } = delete; + for target in targets { + self.infer_expression(target); + } + } + + fn module_type_from_name(&self, module_name: &ModuleName) -> Option> { + resolve_module(self.db(), module_name) + .map(|module| Type::module_literal(self.db(), self.file(), &module)) + } + + fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> { + let ast::Decorator { + range: _, + node_index: _, + expression, + } = decorator; + + self.infer_expression(expression) + } + + fn infer_argument_types<'a>( + &mut self, + ast_arguments: &ast::Arguments, + arguments: CallArguments<'a>, + argument_forms: &[Option], + ) -> 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, + ) -> 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> { + expression.map(|expr| self.infer_expression(expr)) + } + + #[track_caller] + fn infer_expression(&mut self, expression: &ast::Expr) -> Type<'db> { + debug_assert!( + !self.index.is_standalone_expression(expression), + "Calling `self.infer_expression` on a standalone-expression is not allowed because it can lead to double-inference. Use `self.infer_standalone_expression` instead." + ); + + self.infer_expression_impl(expression) + } + + fn infer_maybe_standalone_expression(&mut self, expression: &ast::Expr) -> Type<'db> { + if self.index.is_standalone_expression(expression) { + self.infer_standalone_expression(expression) + } else { + self.infer_expression(expression) + } + } + + fn infer_standalone_expression(&mut self, expression: &ast::Expr) -> Type<'db> { + let standalone_expression = self.index.expression(expression); + let types = infer_expression_types(self.db(), standalone_expression); + self.extend(types); + + // Instead of calling `self.expression_type(expr)` after extending here, we get + // the result from `types` directly because we might be in cycle recovery where + // `types.cycle_fallback_type` is `Some(fallback_ty)`, which we can retrieve by + // using `expression_type` on `types`: + types.expression_type(expression) + } + + fn infer_expression_impl(&mut self, expression: &ast::Expr) -> Type<'db> { + let ty = match expression { + ast::Expr::NoneLiteral(ast::ExprNoneLiteral { + range: _, + node_index: _, + }) => Type::none(self.db()), + ast::Expr::NumberLiteral(literal) => self.infer_number_literal_expression(literal), + ast::Expr::BooleanLiteral(literal) => self.infer_boolean_literal_expression(literal), + ast::Expr::StringLiteral(literal) => self.infer_string_literal_expression(literal), + ast::Expr::BytesLiteral(bytes_literal) => { + self.infer_bytes_literal_expression(bytes_literal) + } + ast::Expr::FString(fstring) => self.infer_fstring_expression(fstring), + ast::Expr::TString(tstring) => self.infer_tstring_expression(tstring), + ast::Expr::EllipsisLiteral(literal) => self.infer_ellipsis_literal_expression(literal), + ast::Expr::Tuple(tuple) => self.infer_tuple_expression(tuple), + ast::Expr::List(list) => self.infer_list_expression(list), + ast::Expr::Set(set) => self.infer_set_expression(set), + ast::Expr::Dict(dict) => self.infer_dict_expression(dict), + ast::Expr::Generator(generator) => self.infer_generator_expression(generator), + ast::Expr::ListComp(listcomp) => self.infer_list_comprehension_expression(listcomp), + ast::Expr::DictComp(dictcomp) => self.infer_dict_comprehension_expression(dictcomp), + ast::Expr::SetComp(setcomp) => self.infer_set_comprehension_expression(setcomp), + ast::Expr::Name(name) => self.infer_name_expression(name), + ast::Expr::Attribute(attribute) => self.infer_attribute_expression(attribute), + ast::Expr::UnaryOp(unary_op) => self.infer_unary_expression(unary_op), + ast::Expr::BinOp(binary) => self.infer_binary_expression(binary), + ast::Expr::BoolOp(bool_op) => self.infer_boolean_expression(bool_op), + ast::Expr::Compare(compare) => self.infer_compare_expression(compare), + ast::Expr::Subscript(subscript) => self.infer_subscript_expression(subscript), + ast::Expr::Slice(slice) => self.infer_slice_expression(slice), + ast::Expr::Named(named) => self.infer_named_expression(named), + ast::Expr::If(if_expression) => self.infer_if_expression(if_expression), + ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression), + ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression), + ast::Expr::Starred(starred) => self.infer_starred_expression(starred), + ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression), + ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from), + ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression), + ast::Expr::IpyEscapeCommand(_) => { + todo_type!("Ipy escape command support") + } + }; + + self.store_expression_type(expression, ty); + + ty + } + + fn store_expression_type(&mut self, expression: &ast::Expr, ty: Type<'db>) { + if self.deferred_state.in_string_annotation() { + // Avoid storing the type of expressions that are part of a string annotation because + // the expression ids don't exists in the semantic index. Instead, we'll store the type + // on the string expression itself that represents the annotation. + return; + } + let previous = self.types.expressions.insert(expression.into(), ty); + assert_eq!(previous, None); + } + + fn infer_number_literal_expression(&mut self, literal: &ast::ExprNumberLiteral) -> Type<'db> { + let ast::ExprNumberLiteral { + range: _, + node_index: _, + value, + } = literal; + let db = self.db(); + + match value { + ast::Number::Int(n) => n + .as_i64() + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + ast::Number::Float(_) => KnownClass::Float.to_instance(db), + ast::Number::Complex { .. } => KnownClass::Complex.to_instance(db), + } + } + + #[expect(clippy::unused_self)] + fn infer_boolean_literal_expression(&mut self, literal: &ast::ExprBooleanLiteral) -> Type<'db> { + let ast::ExprBooleanLiteral { + range: _, + node_index: _, + value, + } = literal; + + Type::BooleanLiteral(*value) + } + + fn infer_string_literal_expression(&mut self, literal: &ast::ExprStringLiteral) -> Type<'db> { + if literal.value.len() <= Self::MAX_STRING_LITERAL_SIZE { + Type::string_literal(self.db(), literal.value.to_str()) + } else { + Type::LiteralString + } + } + + fn infer_bytes_literal_expression(&mut self, literal: &ast::ExprBytesLiteral) -> Type<'db> { + // TODO: ignoring r/R prefixes for now, should normalize bytes values + let bytes: Vec = literal.value.bytes().collect(); + Type::bytes_literal(self.db(), &bytes) + } + + fn infer_fstring_expression(&mut self, fstring: &ast::ExprFString) -> Type<'db> { + let ast::ExprFString { + range: _, + node_index: _, + value, + } = fstring; + + let mut collector = StringPartsCollector::new(); + for part in value { + // Make sure we iter through every parts to infer all sub-expressions. The `collector` + // struct ensures we don't allocate unnecessary strings. + match part { + ast::FStringPart::Literal(literal) => { + collector.push_str(&literal.value); + } + ast::FStringPart::FString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation(expression) => { + let ast::InterpolatedElement { + range: _, + node_index: _, + expression, + debug_text: _, + conversion, + format_spec, + } = expression; + let ty = self.infer_expression(expression); + + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + + // TODO: handle format specifiers by calling a method + // (`Type::format`?) that handles the `__format__` method. + // Conversion flags should be handled before calling `__format__`. + // https://docs.python.org/3/library/string.html#format-string-syntax + if !conversion.is_none() || format_spec.is_some() { + collector.add_expression(); + } else { + if let Type::StringLiteral(literal) = ty.str(self.db()) { + collector.push_str(literal.value(self.db())); + } else { + collector.add_expression(); + } + } + } + ast::InterpolatedStringElement::Literal(literal) => { + collector.push_str(&literal.value); + } + } + } + } + } + } + collector.string_type(self.db()) + } + + fn infer_tstring_expression(&mut self, tstring: &ast::ExprTString) -> Type<'db> { + let ast::ExprTString { value, .. } = tstring; + for part in value { + match part { + ast::TStringPart::Literal(_) => {} + ast::TStringPart::FString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation(expression) => { + let ast::InterpolatedElement { + expression, + format_spec, + .. + } = expression; + self.infer_expression(expression); + + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + } + ast::InterpolatedStringElement::Literal(_) => {} + } + } + } + ast::TStringPart::TString(tstring) => { + for element in &tstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation( + tstring_interpolation_element, + ) => { + let ast::InterpolatedElement { + expression, + format_spec, + .. + } = tstring_interpolation_element; + self.infer_expression(expression); + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + } + ast::InterpolatedStringElement::Literal(_) => {} + } + } + } + } + } + todo_type!("Template") + } + + fn infer_ellipsis_literal_expression( + &mut self, + _literal: &ast::ExprEllipsisLiteral, + ) -> Type<'db> { + KnownClass::EllipsisType.to_instance(self.db()) + } + + fn infer_tuple_expression(&mut self, tuple: &ast::ExprTuple) -> Type<'db> { + let ast::ExprTuple { + range: _, + node_index: _, + elts, + ctx: _, + parenthesized: _, + } = tuple; + + // Collecting all elements is necessary to infer all sub-expressions even if some + // element types are `Never` (which leads `from_elements` to return early without + // consuming the whole iterator). + let element_types: Vec<_> = elts.iter().map(|elt| self.infer_expression(elt)).collect(); + + TupleType::from_elements(self.db(), element_types) + } + + fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> { + let ast::ExprList { + range: _, + node_index: _, + elts, + ctx: _, + } = list; + + for elt in elts { + self.infer_expression(elt); + } + + // TODO generic + KnownClass::List.to_instance(self.db()) + } + + fn infer_set_expression(&mut self, set: &ast::ExprSet) -> Type<'db> { + let ast::ExprSet { + range: _, + node_index: _, + elts, + } = set; + + for elt in elts { + self.infer_expression(elt); + } + + // TODO generic + KnownClass::Set.to_instance(self.db()) + } + + fn infer_dict_expression(&mut self, dict: &ast::ExprDict) -> Type<'db> { + let ast::ExprDict { + range: _, + node_index: _, + items, + } = dict; + + for item in items { + self.infer_optional_expression(item.key.as_ref()); + self.infer_expression(&item.value); + } + + // TODO generic + KnownClass::Dict.to_instance(self.db()) + } + + /// Infer the type of the `iter` expression of the first comprehension. + fn infer_first_comprehension_iter(&mut self, comprehensions: &[ast::Comprehension]) { + let mut comprehensions_iter = comprehensions.iter(); + let Some(first_comprehension) = comprehensions_iter.next() else { + unreachable!("Comprehension must contain at least one generator"); + }; + self.infer_standalone_expression(&first_comprehension.iter); + } + + fn infer_generator_expression(&mut self, generator: &ast::ExprGenerator) -> Type<'db> { + let ast::ExprGenerator { + range: _, + node_index: _, + elt: _, + generators, + parenthesized: _, + } = generator; + + self.infer_first_comprehension_iter(generators); + + todo_type!("generator type") + } + + fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> { + let ast::ExprListComp { + range: _, + node_index: _, + elt: _, + generators, + } = listcomp; + + self.infer_first_comprehension_iter(generators); + + todo_type!("list comprehension type") + } + + fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> { + let ast::ExprDictComp { + range: _, + node_index: _, + key: _, + value: _, + generators, + } = dictcomp; + + self.infer_first_comprehension_iter(generators); + + todo_type!("dict comprehension type") + } + + fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> { + let ast::ExprSetComp { + range: _, + node_index: _, + elt: _, + generators, + } = setcomp; + + self.infer_first_comprehension_iter(generators); + + todo_type!("set comprehension type") + } + + fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) { + let ast::ExprGenerator { + range: _, + node_index: _, + elt, + generators, + parenthesized: _, + } = generator; + + self.infer_expression(elt); + self.infer_comprehensions(generators); + } + + fn infer_list_comprehension_expression_scope(&mut self, listcomp: &ast::ExprListComp) { + let ast::ExprListComp { + range: _, + node_index: _, + elt, + generators, + } = listcomp; + + self.infer_expression(elt); + self.infer_comprehensions(generators); + } + + fn infer_dict_comprehension_expression_scope(&mut self, dictcomp: &ast::ExprDictComp) { + let ast::ExprDictComp { + range: _, + node_index: _, + key, + value, + generators, + } = dictcomp; + + self.infer_expression(key); + self.infer_expression(value); + self.infer_comprehensions(generators); + } + + fn infer_set_comprehension_expression_scope(&mut self, setcomp: &ast::ExprSetComp) { + let ast::ExprSetComp { + range: _, + node_index: _, + elt, + generators, + } = setcomp; + + self.infer_expression(elt); + self.infer_comprehensions(generators); + } + + fn infer_comprehensions(&mut self, comprehensions: &[ast::Comprehension]) { + let mut comprehensions_iter = comprehensions.iter(); + let Some(first_comprehension) = comprehensions_iter.next() else { + unreachable!("Comprehension must contain at least one generator"); + }; + self.infer_comprehension(first_comprehension, true); + for comprehension in comprehensions_iter { + self.infer_comprehension(comprehension, false); + } + } + + fn infer_comprehension(&mut self, comprehension: &ast::Comprehension, is_first: bool) { + let ast::Comprehension { + range: _, + node_index: _, + target, + iter, + ifs, + is_async: _, + } = comprehension; + + self.infer_target(target, iter, |builder, iter_expr| { + // TODO: `infer_comprehension_definition` reports a diagnostic if `iter_ty` isn't iterable + // but only if the target is a name. We should report a diagnostic here if the target isn't a name: + // `[... for a.x in not_iterable] + if is_first { + infer_same_file_expression_type( + builder.db(), + builder.index.expression(iter_expr), + builder.module(), + ) + } else { + builder.infer_standalone_expression(iter_expr) + } + .iterate(builder.db()) + }); + for expr in ifs { + self.infer_standalone_expression(expr); + } + } + + fn infer_comprehension_definition( + &mut self, + comprehension: &ComprehensionDefinitionKind<'db>, + definition: Definition<'db>, + ) { + let iterable = comprehension.iterable(self.module()); + let target = comprehension.target(self.module()); + + let mut infer_iterable_type = || { + let expression = self.index.expression(iterable); + let result = infer_expression_types(self.db(), expression); + + // Two things are different if it's the first comprehension: + // (1) We must lookup the `ScopedExpressionId` of the iterable expression in the outer scope, + // because that's the scope we visit it in in the semantic index builder + // (2) We must *not* call `self.extend()` on the result of the type inference, + // because `ScopedExpressionId`s are only meaningful within their own scope, so + // we'd add types for random wrong expressions in the current scope + if comprehension.is_first() && target.is_name_expr() { + result.expression_type(iterable) + } else { + let scope = self.types.scope; + self.types.scope = result.scope; + self.extend(result); + self.types.scope = scope; + result.expression_type(iterable) + } + }; + + let target_type = if comprehension.is_async() { + // TODO: async iterables/iterators! -- Alex + let _iterable_type = infer_iterable_type(); + todo_type!("async iterables/iterators") + } else { + match comprehension.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } + + unpacked.expression_type(target) + } + TargetKind::Single => { + let iterable_type = infer_iterable_type(); + iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, iterable.into()); + err.fallback_element_type(self.db()) + }) + } + } + }; + + self.types.expressions.insert(target.into(), target_type); + self.add_binding(target.into(), definition, target_type); + } + + 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.expect_single_definition(named); + let result = infer_definition_types(self.db(), definition); + self.extend(result); + result.binding_type(definition) + } else { + // For syntactically invalid targets, we still need to run type inference: + self.infer_expression(&named.target); + self.infer_expression(&named.value); + Type::unknown() + } + } + + fn infer_named_expression_definition( + &mut self, + named: &ast::ExprNamed, + definition: Definition<'db>, + ) -> Type<'db> { + let ast::ExprNamed { + range: _, + node_index: _, + target, + value, + } = named; + + let value_ty = self.infer_expression(value); + self.infer_expression(target); + + self.add_binding(named.into(), definition, value_ty); + + value_ty + } + + fn infer_if_expression(&mut self, if_expression: &ast::ExprIf) -> Type<'db> { + let ast::ExprIf { + range: _, + node_index: _, + test, + body, + orelse, + } = if_expression; + + let test_ty = self.infer_standalone_expression(test); + let body_ty = self.infer_expression(body); + let orelse_ty = self.infer_expression(orelse); + + match test_ty.try_bool(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, &**test); + err.fallback_truthiness() + }) { + Truthiness::AlwaysTrue => body_ty, + Truthiness::AlwaysFalse => orelse_ty, + Truthiness::Ambiguous => UnionType::from_elements(self.db(), [body_ty, orelse_ty]), + } + } + + fn infer_lambda_body(&mut self, lambda_expression: &ast::ExprLambda) { + self.infer_expression(&lambda_expression.body); + } + + fn infer_lambda_expression(&mut self, lambda_expression: &ast::ExprLambda) -> Type<'db> { + let ast::ExprLambda { + range: _, + node_index: _, + parameters, + body: _, + } = lambda_expression; + + let parameters = if let Some(parameters) = parameters { + let positional_only = parameters + .posonlyargs + .iter() + .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::>(); + let positional_or_keyword = parameters + .args + .iter() + .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::>(); + let variadic = parameters + .vararg + .as_ref() + .map(|param| Parameter::variadic(param.name().id.clone())); + let keyword_only = parameters + .kwonlyargs + .iter() + .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::>(); + let keyword_variadic = parameters + .kwarg + .as_ref() + .map(|param| Parameter::keyword_variadic(param.name().id.clone())); + + Parameters::new( + positional_only + .into_iter() + .chain(positional_or_keyword) + .chain(variadic) + .chain(keyword_only) + .chain(keyword_variadic), + ) + } else { + Parameters::empty() + }; + + // TODO: Useful inference of a lambda's return type will require a different approach, + // which does the inference of the body expression based on arguments at each call site, + // rather than eagerly computing a return type without knowing the argument types. + CallableType::function_like(self.db(), Signature::new(parameters, Some(Type::unknown()))) + } + + fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> { + let ast::ExprCall { + range: _, + node_index: _, + func, + 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 call_arguments = CallArguments::from_arguments(arguments); + + let callable_type = self.infer_maybe_standalone_expression(func); + + if let Type::FunctionLiteral(function) = callable_type { + // Make sure that the `function.definition` is only called when the function is defined + // in the same file as the one we're currently inferring the types for. This is because + // the `definition` method accesses the semantic index, which could create a + // cross-module AST dependency. + if function.file(self.db()) == self.file() + && function.definition(self.db()).scope(self.db()) == self.scope() + { + self.called_functions.insert(function); + } + } + + let class = match callable_type { + Type::ClassLiteral(class) => Some(ClassType::NonGeneric(class)), + Type::GenericAlias(generic) => Some(ClassType::Generic(generic)), + Type::SubclassOf(subclass) => subclass.subclass_of().into_class(), + _ => None, + }; + + if let Some(class) = class { + // It might look odd here that we emit an error for class-literals and generic aliases but not + // `type[]` types. But it's deliberate! The typing spec explicitly mandates that `type[]` types + // can be called even though class-literals cannot. This is because even though a protocol class + // `SomeProtocol` is always an abstract class, `type[SomeProtocol]` can be a concrete subclass of + // that protocol -- and indeed, according to the spec, type checkers must disallow abstract + // subclasses of the protocol to be passed to parameters that accept `type[SomeProtocol]`. + // . + if !callable_type.is_subclass_of() { + if let Some(protocol) = class + .class_literal(self.db()) + .0 + .into_protocol_class(self.db()) + { + report_attempted_protocol_instantiation( + &self.context, + call_expression, + protocol, + ); + } + } + + // For class literals we model the entire class instantiation logic, so it is handled + // in a separate function. For some known classes we have manual signatures defined and use + // the `try_call` path below. + // TODO: it should be possible to move these special cases into the `try_call_constructor` + // path instead, or even remove some entirely once we support overloads fully. + if !matches!( + class.known(self.db()), + Some( + KnownClass::Bool + | KnownClass::Str + | KnownClass::Type + | KnownClass::Object + | KnownClass::Property + | KnownClass::Super + | KnownClass::TypeVar + | KnownClass::NamedTuple + | KnownClass::TypeAliasType + | KnownClass::Tuple + ) + ) + // temporary special-casing for all subclasses of `enum.Enum` + // until we support the functional syntax for creating enum classes + && KnownClass::Enum + .to_class_literal(self.db()) + .to_class_type(self.db()) + .is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class)) + { + let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; + let call_argument_types = + self.infer_argument_types(arguments, call_arguments, &argument_forms); + + return callable_type + .try_call_constructor(self.db(), call_argument_types) + .unwrap_or_else(|err| { + err.report_diagnostic(&self.context, callable_type, call_expression.into()); + err.return_type() + }); + } + } + + let bindings = callable_type + .bindings(self.db()) + .match_parameters(&call_arguments); + let call_argument_types = + self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms); + + let mut bindings = match bindings.check_types(self.db(), &call_argument_types) { + Ok(bindings) => bindings, + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, call_expression.into()); + return bindings.return_type(self.db()); + } + }; + + for binding in &mut bindings { + let binding_type = binding.callable_type; + for (_, overload) in binding.matching_overloads_mut() { + match binding_type { + Type::FunctionLiteral(function_literal) => { + if let Some(known_function) = function_literal.known(self.db()) { + if let Some(return_type) = known_function.check_call( + &self.context, + overload.parameter_types(), + call_expression, + self.file(), + ) { + overload.set_return_type(return_type); + } + } + } + + Type::ClassLiteral(class) => { + let Some(known_class) = class.known(self.db()) else { + continue; + }; + let overridden_return = known_class.check_call( + &self.context, + self.index, + overload, + &call_argument_types, + call_expression, + ); + if let Some(overridden_return) = overridden_return { + overload.set_return_type(overridden_return); + } + } + _ => {} + } + } + } + + let db = self.db(); + let scope = self.scope(); + let return_ty = bindings.return_type(db); + + let find_narrowed_place = || match arguments.args.first() { + None => { + // This branch looks extraneous, especially in the face of `missing-arguments`. + // However, that lint won't be able to catch this: + // + // ```python + // def f(v: object = object()) -> TypeIs[int]: ... + // + // if f(): ... + // ``` + // + // TODO: Will this report things that is actually fine? + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_GUARD_CALL, arguments) + { + builder.into_diagnostic("Type guard call does not have a target"); + } + None + } + Some(expr) => match PlaceExpr::try_from(expr) { + Ok(place_expr) => place_table(db, scope).place_id_by_expr(&place_expr), + Err(()) => None, + }, + }; + + match return_ty { + // TODO: TypeGuard + Type::TypeIs(type_is) => match find_narrowed_place() { + Some(place) => type_is.bind(db, scope, place), + None => return_ty, + }, + _ => return_ty, + } + } + + fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { + let ast::ExprStarred { + range: _, + node_index: _, + value, + ctx: _, + } = starred; + + let iterable_type = self.infer_expression(value); + iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); + err.fallback_element_type(self.db()) + }); + + // TODO + todo_type!("starred expression") + } + + fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> { + let ast::ExprYield { + range: _, + node_index: _, + value, + } = yield_expression; + self.infer_optional_expression(value.as_deref()); + todo_type!("yield expressions") + } + + fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> { + let ast::ExprYieldFrom { + range: _, + node_index: _, + value, + } = yield_from; + + let iterable_type = self.infer_expression(value); + iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); + err.fallback_element_type(self.db()) + }); + + // TODO get type from `ReturnType` of generator + todo_type!("Generic `typing.Generator` type") + } + + fn infer_await_expression(&mut self, await_expression: &ast::ExprAwait) -> Type<'db> { + let ast::ExprAwait { + range: _, + node_index: _, + value, + } = await_expression; + self.infer_expression(value); + todo_type!("generic `typing.Awaitable` type") + } + + // Perform narrowing with applicable constraints between the current scope and the enclosing scope. + fn narrow_place_with_applicable_constraints( + &self, + expr: &PlaceExpr, + mut ty: Type<'db>, + constraint_keys: &[(FileScopeId, ConstraintKey)], + ) -> Type<'db> { + let db = self.db(); + for (enclosing_scope_file_id, constraint_key) in constraint_keys { + let use_def = self.index.use_def_map(*enclosing_scope_file_id); + let place_table = self.index.place_table(*enclosing_scope_file_id); + let place = place_table.place_id_by_expr(expr).unwrap(); + + match use_def.applicable_constraints( + *constraint_key, + *enclosing_scope_file_id, + expr, + self.index, + ) { + ApplicableConstraints::UnboundBinding(constraint) => { + ty = constraint.narrow(db, ty, place); + } + // Performs narrowing based on constrained bindings. + // This handling must be performed even if narrowing is attempted and failed using `infer_place_load`. + // The result of `infer_place_load` can be applied as is only when its boundness is `Bound`. + // For example, this handling is required in the following case: + // ```python + // class C: + // x: int | None = None + // c = C() + // # c.x: int | None = + // if c.x is None: + // c.x = 1 + // # else: c.x: int = + // # `c.x` is not definitely bound here + // reveal_type(c.x) # revealed: int + // ``` + ApplicableConstraints::ConstrainedBindings(bindings) => { + let reachability_constraints = bindings.reachability_constraints; + let predicates = bindings.predicates; + let mut union = UnionBuilder::new(db); + for binding in bindings { + let static_reachability = reachability_constraints.evaluate( + db, + predicates, + binding.reachability_constraint, + ); + if static_reachability.is_always_false() { + continue; + } + match binding.binding { + DefinitionState::Defined(definition) => { + let binding_ty = binding_type(db, definition); + union = union.add( + binding.narrowing_constraint.narrow(db, binding_ty, place), + ); + } + DefinitionState::Undefined | DefinitionState::Deleted => { + union = + union.add(binding.narrowing_constraint.narrow(db, ty, place)); + } + } + } + // If there are no visible bindings, the union becomes `Never`. + // Since an unbound binding is recorded even for an undefined place, + // this can only happen if the code is unreachable + // and therefore it is correct to set the result to `Never`. + let union = union.build(); + if union.is_assignable_to(db, ty) { + ty = union; + } + } + } + } + ty + } + + fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> { + let ast::ExprName { + range: _, + node_index: _, + id: symbol_name, + ctx: _, + } = name_node; + let Ok(expr) = PlaceExpr::try_from(symbol_name); + let db = self.db(); + + let (resolved, constraint_keys) = + self.infer_place_load(&expr, ast::ExprRef::Name(name_node)); + resolved + // Not found in the module's explicitly declared global symbols? + // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. + // These are looked up as attributes on `types.ModuleType`. + .or_fall_back_to(db, || { + module_type_implicit_global_symbol(db, symbol_name).map_type(|ty| { + self.narrow_place_with_applicable_constraints(&expr, ty, &constraint_keys) + }) + }) + // Not found in globals? Fallback to builtins + // (without infinite recursion if we're already in builtins.) + .or_fall_back_to(db, || { + if Some(self.scope()) == builtins_module_scope(db) { + Place::Unbound.into() + } else { + builtins_symbol(db, symbol_name) + } + }) + // Still not found? It might be `reveal_type`... + .or_fall_back_to(db, || { + if symbol_name == "reveal_type" { + if let Some(builder) = self.context.report_lint(&UNDEFINED_REVEAL, name_node) { + let mut diag = + builder.into_diagnostic("`reveal_type` used without importing it"); + diag.info( + "This is allowed for debugging convenience but will fail at runtime", + ); + } + typing_extensions_symbol(db, symbol_name) + } else { + Place::Unbound.into() + } + }) + .unwrap_with_diagnostic(|lookup_error| match lookup_error { + LookupError::Unbound(qualifiers) => { + self.report_unresolved_reference(name_node); + TypeAndQualifiers::new(Type::unknown(), qualifiers) + } + LookupError::PossiblyUnbound(type_when_bound) => { + if self.is_reachable(name_node) { + report_possibly_unresolved_reference(&self.context, name_node); + } + type_when_bound + } + }) + .inner_type() + } + + fn infer_local_place_load( + &self, + expr: &PlaceExpr, + expr_ref: ast::ExprRef, + ) -> (Place<'db>, Option) { + let db = self.db(); + let scope = self.scope(); + let file_scope_id = scope.file_scope_id(db); + let place_table = self.index.place_table(file_scope_id); + let use_def = self.index.use_def_map(file_scope_id); + + // If we're inferring types of deferred expressions, always treat them as public symbols + if self.is_deferred() { + let place = if let Some(place_id) = place_table.place_id_by_expr(expr) { + place_from_bindings(db, use_def.all_reachable_bindings(place_id)) + } else { + assert!( + self.deferred_state.in_string_annotation(), + "Expected the place table to create a place for every valid PlaceExpr node" + ); + Place::Unbound + }; + (place, None) + } else { + if expr_ref + .as_name_expr() + .is_some_and(|name| name.is_invalid()) + { + return (Place::Unbound, None); + } + + let use_id = expr_ref.scoped_use_id(db, scope); + let place = place_from_bindings(db, use_def.bindings_at_use(use_id)); + (place, Some(use_id)) + } + } + + /// Infer the type of a place expression from definitions, assuming a load context. + /// This method also returns the [`ConstraintKey`]s for each scope associated with `expr`, + /// which is used to narrow by condition rather than by assignment. + fn infer_place_load( + &self, + expr: &PlaceExpr, + expr_ref: ast::ExprRef, + ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { + let db = self.db(); + let scope = self.scope(); + let file_scope_id = scope.file_scope_id(db); + let place_table = self.index.place_table(file_scope_id); + + let mut constraint_keys = vec![]; + let (local_scope_place, use_id) = self.infer_local_place_load(expr, expr_ref); + if let Some(use_id) = use_id { + constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id))); + } + + let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || { + let has_bindings_in_this_scope = match place_table.place_by_expr(expr) { + Some(place_expr) => place_expr.is_bound(), + None => { + assert!( + self.deferred_state.in_string_annotation(), + "Expected the place table to create a place for every Name node" + ); + false + } + }; + + let current_file = self.file(); + + if let Some(name) = expr.as_name() { + let skip_non_global_scopes = place_table + .place_id_by_name(name) + .is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id)); + + if skip_non_global_scopes { + return global_symbol(self.db(), self.file(), name); + } + } + + // If it's a function-like scope and there is one or more binding in this scope (but + // none of those bindings are visible from where we are in the control flow), we cannot + // fallback to any bindings in enclosing scopes. As such, we can immediately short-circuit + // here and return `Place::Unbound`. + // + // This is because Python is very strict in its categorisation of whether a variable is + // a local variable or not in function-like scopes. If a variable has any bindings in a + // function-like scope, it is considered a local variable; it never references another + // scope. (At runtime, it would use the `LOAD_FAST` opcode.) + if has_bindings_in_this_scope && scope.is_function_like(db) { + return Place::Unbound.into(); + } + + for root_expr in place_table.root_place_exprs(expr) { + let mut expr_ref = expr_ref; + for _ in 0..(expr.sub_segments().len() - root_expr.expr.sub_segments().len()) { + match expr_ref { + ast::ExprRef::Attribute(attribute) => { + expr_ref = ast::ExprRef::from(&attribute.value); + } + ast::ExprRef::Subscript(subscript) => { + expr_ref = ast::ExprRef::from(&subscript.value); + } + _ => unreachable!(), + } + } + let (parent_place, _use_id) = + self.infer_local_place_load(&root_expr.expr, expr_ref); + if let Place::Type(_, _) = parent_place { + return Place::Unbound.into(); + } + } + + // Walk up parent scopes looking for a possible enclosing scope that may have a + // definition of this name visible to us (would be `LOAD_DEREF` at runtime.) + // Note that we skip the scope containing the use that we are resolving, since we + // already looked for the place there up above. + for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) { + // Class scopes are not visible to nested scopes, and we need to handle global + // scope differently (because an unbound name there falls back to builtins), so + // check only function-like scopes. + // There is one exception to this rule: type parameter scopes can see + // names defined in an immediately-enclosing class scope. + let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file); + + let is_immediately_enclosing_scope = scope.is_type_parameter(db) + && scope + .scope(db) + .parent() + .is_some_and(|parent| parent == enclosing_scope_file_id); + + // If the reference is in a nested eager scope, we need to look for the place at + // the point where the previous enclosing scope was defined, instead of at the end + // of the scope. (Note that the semantic index builder takes care of only + // registering eager bindings for nested scopes that are actually eager, and for + // enclosing scopes that actually contain bindings that we should use when + // resolving the reference.) + if !self.is_deferred() { + match self + .index + .eager_snapshot(enclosing_scope_file_id, expr, file_scope_id) + { + EagerSnapshotResult::FoundConstraint(constraint) => { + constraint_keys.push(( + enclosing_scope_file_id, + ConstraintKey::NarrowingConstraint(constraint), + )); + } + EagerSnapshotResult::FoundBindings(bindings) => { + if expr.is_name() + && !enclosing_scope_id.is_function_like(db) + && !is_immediately_enclosing_scope + { + continue; + } + let place = place_from_bindings(db, bindings).map_type(|ty| { + self.narrow_place_with_applicable_constraints( + expr, + ty, + &constraint_keys, + ) + }); + constraint_keys.push(( + enclosing_scope_file_id, + ConstraintKey::EagerNestedScope(file_scope_id), + )); + return place.into(); + } + // There are no visible bindings / constraint here. + // Don't fall back to non-eager place resolution. + EagerSnapshotResult::NotFound => { + let enclosing_place_table = + self.index.place_table(enclosing_scope_file_id); + for enclosing_root_place in enclosing_place_table.root_place_exprs(expr) + { + if enclosing_root_place.is_bound() { + if let Place::Type(_, _) = place( + db, + enclosing_scope_id, + &enclosing_root_place.expr, + ConsideredDefinitions::AllReachable, + ) + .place + { + return Place::Unbound.into(); + } + } + } + continue; + } + EagerSnapshotResult::NoLongerInEagerContext => {} + } + } + + if !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope { + continue; + } + + let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); + let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else { + continue; + }; + if enclosing_place.is_bound() { + // We can return early here, because the nearest function-like scope that + // defines a name must be the only source for the nonlocal reference (at + // runtime, it is the scope that creates the cell for our closure.) If the name + // isn't bound in that scope, we should get an unbound name, not continue + // falling back to other scopes / globals / builtins. + return place( + db, + enclosing_scope_id, + expr, + ConsideredDefinitions::AllReachable, + ) + .map_type(|ty| { + self.narrow_place_with_applicable_constraints(expr, ty, &constraint_keys) + }); + } + } + + PlaceAndQualifiers::from(Place::Unbound) + // No nonlocal binding? Check the module's explicit globals. + // Avoid infinite recursion if `self.scope` already is the module's global scope. + .or_fall_back_to(db, || { + if file_scope_id.is_global() { + return Place::Unbound.into(); + } + + if !self.is_deferred() { + match self + .index + .eager_snapshot(FileScopeId::global(), expr, file_scope_id) + { + EagerSnapshotResult::FoundConstraint(constraint) => { + constraint_keys.push(( + FileScopeId::global(), + ConstraintKey::NarrowingConstraint(constraint), + )); + } + EagerSnapshotResult::FoundBindings(bindings) => { + let place = place_from_bindings(db, bindings).map_type(|ty| { + self.narrow_place_with_applicable_constraints( + expr, + ty, + &constraint_keys, + ) + }); + constraint_keys.push(( + FileScopeId::global(), + ConstraintKey::EagerNestedScope(file_scope_id), + )); + return place.into(); + } + // There are no visible bindings / constraint here. + EagerSnapshotResult::NotFound => { + return Place::Unbound.into(); + } + EagerSnapshotResult::NoLongerInEagerContext => {} + } + } + + let Some(name) = expr.as_name() else { + return Place::Unbound.into(); + }; + + explicit_global_symbol(db, self.file(), name).map_type(|ty| { + self.narrow_place_with_applicable_constraints(expr, ty, &constraint_keys) + }) + }) + }); + + (place, constraint_keys) + } + + pub(super) fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { + if !self.is_reachable(expr_name_node) { + return; + } + + let Some(builder) = self + .context + .report_lint(&UNRESOLVED_REFERENCE, expr_name_node) + else { + return; + }; + + let ast::ExprName { id, .. } = expr_name_node; + let mut diagnostic = + builder.into_diagnostic(format_args!("Name `{id}` used when not defined")); + + // === + // Subdiagnostic (1): check to see if it was added as a builtin in a later version of Python. + // === + if let Some(version_added_to_builtins) = version_builtin_was_added(id) { + diagnostic.info(format_args!( + "`{id}` was added as a builtin in Python 3.{version_added_to_builtins}" + )); + add_inferred_python_version_hint_to_diagnostic( + self.db(), + &mut diagnostic, + "resolving types", + ); + } + + // === + // Subdiagnostic (2): + // - If it's an instance method, check to see if it's available as an attribute on `self`; + // - If it's a classmethod, check to see if it's available as an attribute on `cls` + // === + let Some(current_function) = self.current_function_definition() else { + return; + }; + + let function_parameters = &*current_function.parameters; + + // `self`/`cls` can't be a keyword-only parameter. + if function_parameters.posonlyargs.is_empty() && function_parameters.args.is_empty() { + return; + } + + let Some(first_parameter) = function_parameters.iter_non_variadic_params().next() else { + return; + }; + + let Some(class) = self.class_context_of_current_method() else { + return; + }; + + let first_parameter_name = first_parameter.name(); + + let function_decorators = FunctionDecorators::from_decorator_types( + self.db(), + self.function_decorator_types(current_function), + ); + + let attribute_exists = if function_decorators.contains(FunctionDecorators::CLASSMETHOD) { + if function_decorators.contains(FunctionDecorators::STATICMETHOD) { + return; + } + !Type::instance(self.db(), class.default_specialization(self.db())) + .class_member(self.db(), id.clone()) + .place + .is_unbound() + } else if !function_decorators.contains(FunctionDecorators::STATICMETHOD) { + !Type::instance(self.db(), class.default_specialization(self.db())) + .member(self.db(), id) + .place + .is_unbound() + } else { + false + }; + + if attribute_exists { + diagnostic.info(format_args!( + "An attribute `{id}` is available: consider using `{first_parameter_name}.{id}`" + )); + } + } + + fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> { + match name.ctx { + ExprContext::Load => self.infer_name_load(name), + ExprContext::Store => Type::Never, + ExprContext::Del => { + self.infer_name_load(name); + Type::Never + } + ExprContext::Invalid => Type::unknown(), + } + } + + fn narrow_expr_with_applicable_constraints<'r>( + &self, + target: impl Into>, + target_ty: Type<'db>, + constraint_keys: &[(FileScopeId, ConstraintKey)], + ) -> Type<'db> { + let target = target.into(); + + if let Ok(place_expr) = PlaceExpr::try_from(target) { + self.narrow_place_with_applicable_constraints(&place_expr, target_ty, constraint_keys) + } else { + target_ty + } + } + + /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context. + fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { + let ast::ExprAttribute { + value, + attr, + range: _, + node_index: _, + ctx: _, + } = attribute; + + let value_type = self.infer_maybe_standalone_expression(value); + let db = self.db(); + let mut constraint_keys = vec![]; + + let mut assigned_type = None; + if let Ok(place_expr) = PlaceExpr::try_from(attribute) { + let (resolved, keys) = + self.infer_place_load(&place_expr, ast::ExprRef::Attribute(attribute)); + constraint_keys.extend(keys); + if let Place::Type(ty, Boundness::Bound) = resolved.place { + assigned_type = Some(ty); + } + } + + let resolved_type = value_type + .member(db, &attr.id) + .map_type(|ty| self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys)) + .unwrap_with_diagnostic(|lookup_error| match lookup_error { + LookupError::Unbound(_) => { + let report_unresolved_attribute = self.is_reachable(attribute); + + if report_unresolved_attribute { + let bound_on_instance = match value_type { + Type::ClassLiteral(class) => { + !class.instance_member(db, None, attr).place.is_unbound() + } + Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { + match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => { + !class.instance_member(db, attr).place.is_unbound() + } + SubclassOfInner::Dynamic(_) => unreachable!( + "Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol" + ), + } + } + _ => false, + }; + + if let Some(builder) = self + .context + .report_lint(&UNRESOLVED_ATTRIBUTE, attribute) + { + if bound_on_instance { + builder.into_diagnostic( + format_args!( + "Attribute `{}` can only be accessed on instances, \ + not on the class object `{}` itself.", + attr.id, + value_type.display(db) + ), + ); + } else { + builder.into_diagnostic( + format_args!( + "Type `{}` has no attribute `{}`", + value_type.display(db), + attr.id + ), + ); + } + } + } + + Type::unknown().into() + } + LookupError::PossiblyUnbound(type_when_bound) => { + report_possibly_unbound_attribute( + &self.context, + attribute, + &attr.id, + value_type, + ); + + type_when_bound + } + }) + .inner_type(); + // Even if we can obtain the attribute type based on the assignments, we still perform default type inference + // (to report errors). + assigned_type.unwrap_or(resolved_type) + } + + fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { + let ast::ExprAttribute { + value, + attr: _, + range: _, + node_index: _, + ctx, + } = attribute; + + match ctx { + ExprContext::Load => self.infer_attribute_load(attribute), + ExprContext::Store => { + self.infer_expression(value); + Type::Never + } + ExprContext::Del => { + self.infer_attribute_load(attribute); + Type::Never + } + ExprContext::Invalid => { + self.infer_expression(value); + Type::unknown() + } + } + } + + fn infer_unary_expression(&mut self, unary: &ast::ExprUnaryOp) -> Type<'db> { + let ast::ExprUnaryOp { + range: _, + node_index: _, + op, + operand, + } = unary; + + let operand_type = self.infer_expression(operand); + + match (op, operand_type) { + (_, Type::Dynamic(_)) => operand_type, + (_, Type::Never) => Type::Never, + + (ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), + (ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), + (ast::UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value), + + (ast::UnaryOp::UAdd, Type::BooleanLiteral(bool)) => Type::IntLiteral(i64::from(bool)), + (ast::UnaryOp::USub, Type::BooleanLiteral(bool)) => Type::IntLiteral(-i64::from(bool)), + (ast::UnaryOp::Invert, Type::BooleanLiteral(bool)) => { + Type::IntLiteral(!i64::from(bool)) + } + + (ast::UnaryOp::Not, ty) => ty + .try_bool(self.db()) + .unwrap_or_else(|err| { + err.report_diagnostic(&self.context, unary); + err.fallback_truthiness() + }) + .negate() + .into_type(self.db()), + ( + op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert), + Type::FunctionLiteral(_) + | Type::Callable(..) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::BoundMethod(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Union(_) + | Type::Intersection(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::Tuple(_) + | Type::BoundSuper(_) + | Type::TypeVar(_) + | Type::TypeIs(_), + ) => { + let unary_dunder_method = match op { + ast::UnaryOp::Invert => "__invert__", + ast::UnaryOp::UAdd => "__pos__", + ast::UnaryOp::USub => "__neg__", + ast::UnaryOp::Not => { + unreachable!("Not operator is handled in its own case"); + } + }; + + match operand_type.try_call_dunder( + self.db(), + unary_dunder_method, + CallArgumentTypes::none(), + ) { + Ok(outcome) => outcome.return_type(self.db()), + Err(e) => { + if let Some(builder) = + self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) + { + builder.into_diagnostic(format_args!( + "Unary operator `{op}` is unsupported for type `{}`", + operand_type.display(self.db()), + )); + } + e.fallback_return_type(self.db()) + } + } + } + } + } + + fn infer_binary_expression(&mut self, binary: &ast::ExprBinOp) -> Type<'db> { + let ast::ExprBinOp { + left, + op, + right, + range: _, + node_index: _, + } = binary; + + let left_ty = self.infer_expression(left); + let right_ty = self.infer_expression(right); + + self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op) + .unwrap_or_else(|| { + let db = self.db(); + + if let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) { + let mut diag = builder.into_diagnostic(format_args!( + "Operator `{op}` is unsupported between objects of type `{}` and `{}`", + left_ty.display(db), + right_ty.display(db) + )); + + if op == &ast::Operator::BitOr + && (left_ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) + || right_ty.is_subtype_of(db, KnownClass::Type.to_instance(db))) + && Program::get(db).python_version(db) < PythonVersion::PY310 + { + diag.info( + "Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later", + ); + add_inferred_python_version_hint_to_diagnostic(db, &mut diag, "resolving types"); + } + } + Type::unknown() + }) + } + + fn infer_binary_expression_type( + &mut self, + node: AnyNodeRef<'_>, + mut emitted_division_by_zero_diagnostic: bool, + left_ty: Type<'db>, + right_ty: Type<'db>, + op: ast::Operator, + ) -> Option> { + // Check for division by zero; this doesn't change the inferred type for the expression, but + // may emit a diagnostic + if !emitted_division_by_zero_diagnostic + && matches!( + (op, right_ty), + ( + ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod, + Type::IntLiteral(0) | Type::BooleanLiteral(false) + ) + ) + { + emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty); + } + + match (left_ty, right_ty, op) { + (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(self.db(), |lhs_element| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + *lhs_element, + rhs, + op, + ) + }), + (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(self.db(), |rhs_element| { + self.infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + lhs, + *rhs_element, + op, + ) + }), + + // Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future, + // the result would then become Any or Unknown, respectively). + (any @ Type::Dynamic(DynamicType::Any), _, _) + | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), + (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) + | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), + ( + todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec), + _, + _, + ) + | ( + _, + todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec), + _, + ) => Some(todo), + (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( + n.checked_add(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), + ), + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Sub) => Some( + n.checked_sub(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), + ), + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mult) => Some( + n.checked_mul(m) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), + ), + + (Type::IntLiteral(_), Type::IntLiteral(_), ast::Operator::Div) => { + Some(KnownClass::Float.to_instance(self.db())) + } + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::FloorDiv) => Some({ + let mut q = n.checked_div(m); + let r = n.checked_rem(m); + // Division works differently in Python than in Rust. If the result is negative and + // there is a remainder, the division rounds down (instead of towards zero): + if n.is_negative() != m.is_negative() && r.unwrap_or(0) != 0 { + q = q.map(|q| q - 1); + } + q.map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) + }), + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Mod) => Some({ + let mut r = n.checked_rem(m); + // Division works differently in Python than in Rust. If the result is negative and + // there is a remainder, the division rounds down (instead of towards zero). Adjust + // the remainder to compensate so that q * m + r == n: + if n.is_negative() != m.is_negative() && r.unwrap_or(0) != 0 { + r = r.map(|x| x + m); + } + r.map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) + }), + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => Some({ + if m < 0 { + KnownClass::Float.to_instance(self.db()) + } else { + u32::try_from(m) + .ok() + .and_then(|m| n.checked_pow(m)) + .map(Type::IntLiteral) + .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())) + } + }), + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::BitOr) => { + Some(Type::IntLiteral(n | m)) + } + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::BitAnd) => { + Some(Type::IntLiteral(n & m)) + } + + (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::BitXor) => { + Some(Type::IntLiteral(n ^ m)) + } + + (Type::BytesLiteral(lhs), Type::BytesLiteral(rhs), ast::Operator::Add) => { + let bytes = [lhs.value(self.db()), rhs.value(self.db())].concat(); + Some(Type::bytes_literal(self.db(), &bytes)) + } + + (Type::StringLiteral(lhs), Type::StringLiteral(rhs), ast::Operator::Add) => { + let lhs_value = lhs.value(self.db()).to_string(); + let rhs_value = rhs.value(self.db()); + let ty = if lhs_value.len() + rhs_value.len() <= Self::MAX_STRING_LITERAL_SIZE { + Type::string_literal(self.db(), &(lhs_value + rhs_value)) + } else { + Type::LiteralString + }; + Some(ty) + } + + ( + Type::StringLiteral(_) | Type::LiteralString, + Type::StringLiteral(_) | Type::LiteralString, + ast::Operator::Add, + ) => Some(Type::LiteralString), + + (Type::StringLiteral(s), Type::IntLiteral(n), ast::Operator::Mult) + | (Type::IntLiteral(n), Type::StringLiteral(s), ast::Operator::Mult) => { + let ty = if n < 1 { + Type::string_literal(self.db(), "") + } else if let Ok(n) = usize::try_from(n) { + if n.checked_mul(s.value(self.db()).len()) + .is_some_and(|new_length| new_length <= Self::MAX_STRING_LITERAL_SIZE) + { + let new_literal = s.value(self.db()).repeat(n); + Type::string_literal(self.db(), &new_literal) + } else { + Type::LiteralString + } + } else { + Type::LiteralString + }; + Some(ty) + } + + (Type::LiteralString, Type::IntLiteral(n), ast::Operator::Mult) + | (Type::IntLiteral(n), Type::LiteralString, ast::Operator::Mult) => { + let ty = if n < 1 { + Type::string_literal(self.db(), "") + } else { + Type::LiteralString + }; + Some(ty) + } + + (Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => { + Some(Type::BooleanLiteral(b1 | b2)) + } + + (Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitAnd) => { + Some(Type::BooleanLiteral(b1 & b2)) + } + + (Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitXor) => { + Some(Type::BooleanLiteral(b1 ^ b2)) + } + + (Type::BooleanLiteral(b1), Type::BooleanLiteral(_) | Type::IntLiteral(_), op) => self + .infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + Type::IntLiteral(i64::from(b1)), + right_ty, + op, + ), + (Type::IntLiteral(_), Type::BooleanLiteral(b2), op) => self + .infer_binary_expression_type( + node, + emitted_division_by_zero_diagnostic, + left_ty, + Type::IntLiteral(i64::from(b2)), + op, + ), + + (Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => { + Some(Type::tuple(TupleType::new( + self.db(), + lhs.tuple(self.db()).concat(self.db(), rhs.tuple(self.db())), + ))) + } + + // We've handled all of the special cases that we support for literals, so we need to + // fall back on looking for dunder methods on one of the operand types. + ( + Type::FunctionLiteral(_) + | Type::BooleanLiteral(_) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::Tuple(_) + | Type::BoundSuper(_) + | Type::TypeVar(_) + | Type::TypeIs(_), + Type::FunctionLiteral(_) + | Type::BooleanLiteral(_) + | Type::Callable(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::Tuple(_) + | Type::BoundSuper(_) + | Type::TypeVar(_) + | Type::TypeIs(_), + op, + ) => { + // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from + // the Python spec [1] is: + // + // - If rhs is a (proper) subclass of lhs, and it provides a different + // implementation of __rop__, use that. + // - Otherwise, if lhs implements __op__, use that. + // - Otherwise, if lhs and rhs are different types, and rhs implements __rop__, + // use that. + // + // [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__ + + // Technically we don't have to check left_ty != right_ty here, since if the types + // are the same, they will trivially have the same implementation of the reflected + // dunder, and so we'll fail the inner check. But the type equality check will be + // faster for the common case, and allow us to skip the (two) class member lookups. + let left_class = left_ty.to_meta_type(self.db()); + let right_class = right_ty.to_meta_type(self.db()); + if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) { + let reflected_dunder = op.reflected_dunder(); + let rhs_reflected = right_class.member(self.db(), reflected_dunder).place; + // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible + // Bindings together + if !rhs_reflected.is_unbound() + && rhs_reflected != left_class.member(self.db(), reflected_dunder).place + { + return right_ty + .try_call_dunder( + self.db(), + reflected_dunder, + CallArgumentTypes::positional([left_ty]), + ) + .map(|outcome| outcome.return_type(self.db())) + .or_else(|_| { + left_ty + .try_call_dunder( + self.db(), + op.dunder(), + CallArgumentTypes::positional([right_ty]), + ) + .map(|outcome| outcome.return_type(self.db())) + }) + .ok(); + } + } + + let call_on_left_instance = left_ty + .try_call_dunder( + self.db(), + op.dunder(), + CallArgumentTypes::positional([right_ty]), + ) + .map(|outcome| outcome.return_type(self.db())) + .ok(); + + call_on_left_instance.or_else(|| { + if left_ty == right_ty { + None + } else { + right_ty + .try_call_dunder( + self.db(), + op.reflected_dunder(), + CallArgumentTypes::positional([left_ty]), + ) + .map(|outcome| outcome.return_type(self.db())) + .ok() + } + }) + } + } + } + + fn infer_boolean_expression(&mut self, bool_op: &ast::ExprBoolOp) -> Type<'db> { + let ast::ExprBoolOp { + range: _, + node_index: _, + op, + values, + } = bool_op; + self.infer_chained_boolean_types( + *op, + values.iter().enumerate(), + |builder, (index, value)| { + let ty = if index == values.len() - 1 { + builder.infer_expression(value) + } else { + builder.infer_standalone_expression(value) + }; + + (ty, value.range()) + }, + ) + } + + /// Computes the output of a chain of (one) boolean operation, consuming as input an iterator + /// of operations and calling the `infer_ty` for each to infer their types. + /// The iterator is consumed even if the boolean evaluation can be short-circuited, + /// in order to ensure the invariant that all expressions are evaluated when inferring types. + fn infer_chained_boolean_types( + &mut self, + op: ast::BoolOp, + operations: Iterator, + infer_ty: F, + ) -> Type<'db> + where + Iterator: IntoIterator, + F: Fn(&mut Self, Item) -> (Type<'db>, TextRange), + { + let mut done = false; + let db = self.db(); + + let elements = operations + .into_iter() + .with_position() + .map(|(position, item)| { + let (ty, range) = infer_ty(self, item); + + let is_last = matches!( + position, + itertools::Position::Last | itertools::Position::Only + ); + + if is_last { + if done { Type::Never } else { ty } + } else { + let truthiness = ty.try_bool(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, range); + err.fallback_truthiness() + }); + + if done { + return Type::Never; + } + + match (truthiness, op) { + (Truthiness::AlwaysTrue, ast::BoolOp::And) => Type::Never, + (Truthiness::AlwaysFalse, ast::BoolOp::Or) => Type::Never, + + (Truthiness::AlwaysFalse, ast::BoolOp::And) + | (Truthiness::AlwaysTrue, ast::BoolOp::Or) => { + done = true; + ty + } + + (Truthiness::Ambiguous, _) => IntersectionBuilder::new(db) + .add_positive(ty) + .add_negative(match op { + ast::BoolOp::And => Type::AlwaysTruthy, + ast::BoolOp::Or => Type::AlwaysFalsy, + }) + .build(), + } + } + }); + + UnionType::from_elements(db, elements) + } + + fn infer_compare_expression(&mut self, compare: &ast::ExprCompare) -> Type<'db> { + let ast::ExprCompare { + range: _, + node_index: _, + left, + ops, + comparators, + } = compare; + + self.infer_expression(left); + + // https://docs.python.org/3/reference/expressions.html#comparisons + // > Formally, if `a, b, c, …, y, z` are expressions and `op1, op2, …, opN` are comparison + // > operators, then `a op1 b op2 c ... y opN z` is equivalent to `a op1 b and b op2 c and + // ... > y opN z`, except that each expression is evaluated at most once. + // + // As some operators (==, !=, <, <=, >, >=) *can* return an arbitrary type, the logic below + // is shared with the one in `infer_binary_type_comparison`. + self.infer_chained_boolean_types( + ast::BoolOp::And, + std::iter::once(&**left) + .chain(comparators) + .tuple_windows::<(_, _)>() + .zip(ops), + |builder, ((left, right), op)| { + let left_ty = builder.expression_type(left); + let right_ty = builder.infer_expression(right); + + let range = TextRange::new(left.start(), right.end()); + + let ty = builder + .infer_binary_type_comparison(left_ty, *op, right_ty, range) + .unwrap_or_else(|error| { + if let Some(diagnostic_builder) = + builder.context.report_lint(&UNSUPPORTED_OPERATOR, range) + { + // Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome) + diagnostic_builder.into_diagnostic(format_args!( + "Operator `{}` is not supported for types `{}` and `{}`{}", + error.op, + error.left_ty.display(builder.db()), + error.right_ty.display(builder.db()), + if (left_ty, right_ty) == (error.left_ty, error.right_ty) { + String::new() + } else { + format!( + ", in comparing `{}` with `{}`", + left_ty.display(builder.db()), + right_ty.display(builder.db()) + ) + } + )); + } + + match op { + // `in, not in, is, is not` always return bool instances + ast::CmpOp::In + | ast::CmpOp::NotIn + | ast::CmpOp::Is + | ast::CmpOp::IsNot => KnownClass::Bool.to_instance(builder.db()), + // Other operators can return arbitrary types + _ => Type::unknown(), + } + }); + + (ty, range) + }, + ) + } + + fn infer_binary_intersection_type_comparison( + &mut self, + intersection: IntersectionType<'db>, + op: ast::CmpOp, + other: Type<'db>, + intersection_on: IntersectionOn, + range: TextRange, + ) -> Result, CompareUnsupportedError<'db>> { + enum State<'db> { + // We have not seen any positive elements (yet) + NoPositiveElements, + // The operator was unsupported on all elements that we have seen so far. + // Contains the first error we encountered. + UnsupportedOnAllElements(CompareUnsupportedError<'db>), + // The operator was supported on at least one positive element. + Supported, + } + + // If a comparison yields a definitive true/false answer on a (positive) part + // of an intersection type, it will also yield a definitive answer on the full + // intersection type, which is even more specific. + for pos in intersection.positive(self.db()) { + let result = match intersection_on { + IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other, range), + IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos, range), + }; + + if let Ok(Type::BooleanLiteral(_)) = result { + return result; + } + } + + // For negative contributions to the intersection type, there are only a few + // special cases that allow us to narrow down the result type of the comparison. + for neg in intersection.negative(self.db()) { + let result = match intersection_on { + IntersectionOn::Left => self + .infer_binary_type_comparison(*neg, op, other, range) + .ok(), + IntersectionOn::Right => self + .infer_binary_type_comparison(other, op, *neg, range) + .ok(), + }; + + match (op, result) { + (ast::CmpOp::Is, Some(Type::BooleanLiteral(true))) => { + return Ok(Type::BooleanLiteral(false)); + } + (ast::CmpOp::IsNot, Some(Type::BooleanLiteral(false))) => { + return Ok(Type::BooleanLiteral(true)); + } + _ => {} + } + } + + // If none of the simplifications above apply, we still need to return *some* + // result type for the comparison 'T_inter `op` T_other' (or reversed), where + // + // T_inter = P1 & P2 & ... & Pn & ~N1 & ~N2 & ... & ~Nm + // + // is the intersection type. If f(T) is the function that computes the result + // type of a `op`-comparison with `T_other`, we are interested in f(T_inter). + // Since we can't compute it exactly, we return the following approximation: + // + // f(T_inter) = f(P1) & f(P2) & ... & f(Pn) + // + // The reason for this is the following: In general, for any function 'f', the + // set f(A) & f(B) is *larger than or equal to* the set f(A & B). This means + // that we will return a type that is possibly wider than it could be, but + // never wrong. + // + // However, we do have to leave out the negative contributions. If we were to + // add a contribution like ~f(N1), we would potentially infer result types + // that are too narrow. + // + // As an example for this, consider the intersection type `int & ~Literal[1]`. + // If 'f' would be the `==`-comparison with 2, we obviously can't tell if that + // answer would be true or false, so we need to return `bool`. And indeed, we + // we have (glossing over notational details): + // + // f(int & ~1) + // = f({..., -1, 0, 2, 3, ...}) + // = {..., False, False, True, False, ...} + // = bool + // + // On the other hand, if we were to compute + // + // f(int) & ~f(1) + // = bool & ~False + // = True + // + // we would get a result type `Literal[True]` which is too narrow. + // + let mut builder = IntersectionBuilder::new(self.db()); + + builder = builder.add_positive(KnownClass::Bool.to_instance(self.db())); + + let mut state = State::NoPositiveElements; + + for pos in intersection.positive(self.db()) { + let result = match intersection_on { + IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other, range), + IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos, range), + }; + + match result { + Ok(ty) => { + state = State::Supported; + builder = builder.add_positive(ty); + } + Err(error) => { + match state { + State::NoPositiveElements => { + // This is the first positive element, but the operation is not supported. + // Store the error and continue. + state = State::UnsupportedOnAllElements(error); + } + State::UnsupportedOnAllElements(_) => { + // We already have an error stored, and continue to see elements on which + // the operator is not supported. Continue with the same state (only keep + // the first error). + } + State::Supported => { + // We previously saw a positive element that supported the operator, + // so the overall operation is still supported. + } + } + } + } + } + + match state { + State::Supported => Ok(builder.build()), + State::NoPositiveElements => { + // We didn't see any positive elements, check if the operation is supported on `object`: + match intersection_on { + IntersectionOn::Left => { + self.infer_binary_type_comparison(Type::object(self.db()), op, other, range) + } + IntersectionOn::Right => { + self.infer_binary_type_comparison(other, op, Type::object(self.db()), range) + } + } + } + State::UnsupportedOnAllElements(error) => Err(error), + } + } + + /// Infers the type of a binary comparison (e.g. 'left == right'). See + /// `infer_compare_expression` for the higher level logic dealing with multi-comparison + /// expressions. + /// + /// If the operation is not supported, return None (we need upstream context to emit a + /// diagnostic). + fn infer_binary_type_comparison( + &mut self, + left: Type<'db>, + op: ast::CmpOp, + right: Type<'db>, + range: TextRange, + ) -> Result, CompareUnsupportedError<'db>> { + let is_str_literal_in_tuple = |literal: Type<'db>, tuple: TupleType<'db>| { + // Protect against doing a lot of work for pathologically large + // tuples. + // + // Ref: https://github.com/astral-sh/ruff/pull/18251#discussion_r2115909311 + let (minimum_length, _) = tuple.tuple(self.db()).len().size_hint(); + if minimum_length > 1 << 12 { + return None; + } + + let mut definitely_true = false; + let mut definitely_false = true; + for element in tuple.tuple(self.db()).all_elements().copied() { + if element.is_string_literal() { + if literal == element { + definitely_true = true; + definitely_false = false; + } + } else if !literal.is_disjoint_from(self.db(), element) { + definitely_false = false; + } + } + + if definitely_true { + Some(true) + } else if definitely_false { + Some(false) + } else { + None + } + }; + + // Note: identity (is, is not) for equal builtin types is unreliable and not part of the + // language spec. + // - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal + // - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal + match (left, right) { + (Type::Union(union), other) => { + let mut builder = UnionBuilder::new(self.db()); + for element in union.elements(self.db()) { + builder = + builder.add(self.infer_binary_type_comparison(*element, op, other, range)?); + } + Ok(builder.build()) + } + (other, Type::Union(union)) => { + let mut builder = UnionBuilder::new(self.db()); + for element in union.elements(self.db()) { + builder = + builder.add(self.infer_binary_type_comparison(other, op, *element, range)?); + } + Ok(builder.build()) + } + + (Type::Intersection(intersection), right) => self + .infer_binary_intersection_type_comparison( + intersection, + op, + right, + IntersectionOn::Left, + range, + ), + (left, Type::Intersection(intersection)) => self + .infer_binary_intersection_type_comparison( + intersection, + op, + left, + IntersectionOn::Right, + range, + ), + + (Type::IntLiteral(n), Type::IntLiteral(m)) => match op { + ast::CmpOp::Eq => Ok(Type::BooleanLiteral(n == m)), + ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(n != m)), + ast::CmpOp::Lt => Ok(Type::BooleanLiteral(n < m)), + ast::CmpOp::LtE => Ok(Type::BooleanLiteral(n <= m)), + ast::CmpOp::Gt => Ok(Type::BooleanLiteral(n > m)), + ast::CmpOp::GtE => Ok(Type::BooleanLiteral(n >= m)), + // We cannot say that two equal int Literals will return True from an `is` or `is not` comparison. + // Even if they are the same value, they may not be the same object. + ast::CmpOp::Is => { + if n == m { + Ok(KnownClass::Bool.to_instance(self.db())) + } else { + Ok(Type::BooleanLiteral(false)) + } + } + ast::CmpOp::IsNot => { + if n == m { + Ok(KnownClass::Bool.to_instance(self.db())) + } else { + Ok(Type::BooleanLiteral(true)) + } + } + // Undefined for (int, int) + ast::CmpOp::In | ast::CmpOp::NotIn => Err(CompareUnsupportedError { + op, + left_ty: left, + right_ty: right, + }), + }, + (Type::IntLiteral(_), Type::NominalInstance(_)) => self.infer_binary_type_comparison( + KnownClass::Int.to_instance(self.db()), + op, + right, + range, + ), + (Type::NominalInstance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison( + left, + op, + KnownClass::Int.to_instance(self.db()), + range, + ), + + // Booleans are coded as integers (False = 0, True = 1) + (Type::IntLiteral(n), Type::BooleanLiteral(b)) => self.infer_binary_type_comparison( + Type::IntLiteral(n), + op, + Type::IntLiteral(i64::from(b)), + range, + ), + (Type::BooleanLiteral(b), Type::IntLiteral(m)) => self.infer_binary_type_comparison( + Type::IntLiteral(i64::from(b)), + op, + Type::IntLiteral(m), + range, + ), + (Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => self + .infer_binary_type_comparison( + Type::IntLiteral(i64::from(a)), + op, + Type::IntLiteral(i64::from(b)), + range, + ), + + (Type::StringLiteral(salsa_s1), Type::StringLiteral(salsa_s2)) => { + let s1 = salsa_s1.value(self.db()); + let s2 = salsa_s2.value(self.db()); + match op { + ast::CmpOp::Eq => Ok(Type::BooleanLiteral(s1 == s2)), + ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(s1 != s2)), + ast::CmpOp::Lt => Ok(Type::BooleanLiteral(s1 < s2)), + ast::CmpOp::LtE => Ok(Type::BooleanLiteral(s1 <= s2)), + ast::CmpOp::Gt => Ok(Type::BooleanLiteral(s1 > s2)), + ast::CmpOp::GtE => Ok(Type::BooleanLiteral(s1 >= s2)), + ast::CmpOp::In => Ok(Type::BooleanLiteral(s2.contains(s1))), + ast::CmpOp::NotIn => Ok(Type::BooleanLiteral(!s2.contains(s1))), + ast::CmpOp::Is => { + if s1 == s2 { + Ok(KnownClass::Bool.to_instance(self.db())) + } else { + Ok(Type::BooleanLiteral(false)) + } + } + ast::CmpOp::IsNot => { + if s1 == s2 { + Ok(KnownClass::Bool.to_instance(self.db())) + } else { + Ok(Type::BooleanLiteral(true)) + } + } + } + } + (Type::StringLiteral(_), Type::Tuple(tuple)) if op == ast::CmpOp::In => { + if let Some(answer) = is_str_literal_in_tuple(left, tuple) { + return Ok(Type::BooleanLiteral(answer)); + } + + self.infer_binary_type_comparison( + KnownClass::Str.to_instance(self.db()), + op, + right, + range, + ) + } + (Type::StringLiteral(_), Type::Tuple(tuple)) if op == ast::CmpOp::NotIn => { + if let Some(answer) = is_str_literal_in_tuple(left, tuple) { + return Ok(Type::BooleanLiteral(!answer)); + } + + self.infer_binary_type_comparison( + KnownClass::Str.to_instance(self.db()), + op, + right, + range, + ) + } + (Type::StringLiteral(_), _) => self.infer_binary_type_comparison( + KnownClass::Str.to_instance(self.db()), + op, + right, + range, + ), + (_, Type::StringLiteral(_)) => self.infer_binary_type_comparison( + left, + op, + KnownClass::Str.to_instance(self.db()), + range, + ), + + (Type::LiteralString, _) => self.infer_binary_type_comparison( + KnownClass::Str.to_instance(self.db()), + op, + right, + range, + ), + (_, Type::LiteralString) => self.infer_binary_type_comparison( + left, + op, + KnownClass::Str.to_instance(self.db()), + range, + ), + + (Type::BytesLiteral(salsa_b1), Type::BytesLiteral(salsa_b2)) => { + let b1 = salsa_b1.value(self.db()); + let b2 = salsa_b2.value(self.db()); + match op { + ast::CmpOp::Eq => Ok(Type::BooleanLiteral(b1 == b2)), + ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(b1 != b2)), + ast::CmpOp::Lt => Ok(Type::BooleanLiteral(b1 < b2)), + ast::CmpOp::LtE => Ok(Type::BooleanLiteral(b1 <= b2)), + ast::CmpOp::Gt => Ok(Type::BooleanLiteral(b1 > b2)), + ast::CmpOp::GtE => Ok(Type::BooleanLiteral(b1 >= b2)), + ast::CmpOp::In => { + Ok(Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_some())) + } + ast::CmpOp::NotIn => { + Ok(Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_none())) + } + ast::CmpOp::Is => { + if b1 == b2 { + Ok(KnownClass::Bool.to_instance(self.db())) + } else { + Ok(Type::BooleanLiteral(false)) + } + } + ast::CmpOp::IsNot => { + if b1 == b2 { + Ok(KnownClass::Bool.to_instance(self.db())) + } else { + Ok(Type::BooleanLiteral(true)) + } + } + } + } + (Type::BytesLiteral(_), _) => self.infer_binary_type_comparison( + KnownClass::Bytes.to_instance(self.db()), + op, + right, + range, + ), + (_, Type::BytesLiteral(_)) => self.infer_binary_type_comparison( + left, + op, + KnownClass::Bytes.to_instance(self.db()), + range, + ), + (Type::Tuple(_), Type::NominalInstance(instance)) + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + { + self.infer_binary_type_comparison( + left, + op, + Type::version_info_tuple(self.db()), + range, + ) + } + (Type::NominalInstance(instance), Type::Tuple(_)) + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + { + self.infer_binary_type_comparison( + Type::version_info_tuple(self.db()), + op, + right, + range, + ) + } + (Type::Tuple(lhs), Type::Tuple(rhs)) => { + let lhs_tuple = lhs.tuple(self.db()); + let rhs_tuple = rhs.tuple(self.db()); + + let mut tuple_rich_comparison = + |op| self.infer_tuple_rich_comparison(lhs_tuple, op, rhs_tuple, range); + + match op { + ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In | ast::CmpOp::NotIn => { + let mut any_eq = false; + let mut any_ambiguous = false; + + for ty in rhs_tuple.all_elements().copied() { + let eq_result = self.infer_binary_type_comparison( + Type::Tuple(lhs), + ast::CmpOp::Eq, + ty, + range, + ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); + + match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for different union variants. Instead, this is just for us to + // evaluate a possibly truthy value to `false` or `true`. + ty => match ty.bool(self.db()) { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, + }, + } + } + + if any_eq { + Ok(Type::BooleanLiteral(op.is_in())) + } else if !any_ambiguous { + Ok(Type::BooleanLiteral(op.is_not_in())) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) + } + } + ast::CmpOp::Is | ast::CmpOp::IsNot => { + // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` + // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` + let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( + "infer_binary_type_comparison should never return None for `CmpOp::Eq`", + ); + + Ok(match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for `is` and `is not` comparisons. This is an implementation detail + // for how we determine the truthiness of a type. + ty => match ty.bool(self.db()) { + Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), + _ => KnownClass::Bool.to_instance(self.db()), + }, + }) + } + } + } + + // Lookup the rich comparison `__dunder__` methods + _ => { + let rich_comparison = |op| self.infer_rich_comparison(left, right, op); + let membership_test_comparison = |op, range: TextRange| { + self.infer_membership_test_comparison(left, right, op, range) + }; + match op { + ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In => { + membership_test_comparison(MembershipTestCompareOperator::In, range) + } + ast::CmpOp::NotIn => { + membership_test_comparison(MembershipTestCompareOperator::NotIn, range) + } + ast::CmpOp::Is => { + if left.is_disjoint_from(self.db(), right) { + Ok(Type::BooleanLiteral(false)) + } else if left.is_singleton(self.db()) + && left.is_equivalent_to(self.db(), right) + { + Ok(Type::BooleanLiteral(true)) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) + } + } + ast::CmpOp::IsNot => { + if left.is_disjoint_from(self.db(), right) { + Ok(Type::BooleanLiteral(true)) + } else if left.is_singleton(self.db()) + && left.is_equivalent_to(self.db(), right) + { + Ok(Type::BooleanLiteral(false)) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) + } + } + } + } + } + } + + /// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their + /// behaviour can be edited for classes by implementing corresponding dunder methods. + /// This function performs rich comparison between two types and returns the resulting type. + /// see `` + fn infer_rich_comparison( + &self, + left: Type<'db>, + right: Type<'db>, + op: RichCompareOperator, + ) -> Result, CompareUnsupportedError<'db>> { + let db = self.db(); + // The following resource has details about the rich comparison algorithm: + // https://snarky.ca/unravelling-rich-comparison-operators/ + let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| { + left.try_call_dunder(db, op.dunder(), CallArgumentTypes::positional([right])) + .map(|outcome| outcome.return_type(db)) + .ok() + }; + + // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. + if left != right && right.is_subtype_of(db, left) { + call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) + } else { + call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) + } + .or_else(|| { + // When no appropriate method returns any value other than NotImplemented, + // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. + // refer to `` + if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) { + Some(KnownClass::Bool.to_instance(db)) + } else { + None + } + }) + .ok_or_else(|| CompareUnsupportedError { + op: op.into(), + left_ty: left, + right_ty: right, + }) + } + + /// Performs a membership test (`in` and `not in`) between two instances and returns the resulting type, or `None` if the test is unsupported. + /// The behavior can be customized in Python by implementing `__contains__`, `__iter__`, or `__getitem__` methods. + /// See `` + /// and `` + fn infer_membership_test_comparison( + &self, + left: Type<'db>, + right: Type<'db>, + op: MembershipTestCompareOperator, + range: TextRange, + ) -> Result, CompareUnsupportedError<'db>> { + let db = self.db(); + + let contains_dunder = right.class_member(db, "__contains__".into()).place; + let compare_result_opt = match contains_dunder { + Place::Type(contains_dunder, Boundness::Bound) => { + // If `__contains__` is available, it is used directly for the membership test. + contains_dunder + .try_call(db, &CallArgumentTypes::positional([right, left])) + .map(|bindings| bindings.return_type(db)) + .ok() + } + _ => { + // iteration-based membership test + right + .try_iterate(db) + .map(|_| KnownClass::Bool.to_instance(db)) + .ok() + } + }; + + compare_result_opt + .map(|ty| { + if matches!(ty, Type::Dynamic(DynamicType::Todo(_))) { + return ty; + } + + let truthiness = ty.try_bool(db).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, range); + err.fallback_truthiness() + }); + + match op { + MembershipTestCompareOperator::In => truthiness.into_type(db), + MembershipTestCompareOperator::NotIn => truthiness.negate().into_type(db), + } + }) + .ok_or_else(|| CompareUnsupportedError { + op: op.into(), + left_ty: left, + right_ty: right, + }) + } + + /// Simulates rich comparison between tuples and returns the inferred result. + /// This performs a lexicographic comparison, returning a union of all possible return types that could result from the comparison. + /// + /// basically it's based on cpython's `tuple_richcompare` + /// see `` + fn infer_tuple_rich_comparison( + &mut self, + left: &TupleSpec<'db>, + op: RichCompareOperator, + right: &TupleSpec<'db>, + range: TextRange, + ) -> Result, CompareUnsupportedError<'db>> { + // If either tuple is variable length, we can make no assumptions about the relative + // lengths of the tuples, and therefore neither about how they compare lexicographically. + // TODO: Consider comparing the prefixes of the tuples, since that could give a comparison + // result regardless of how long the variable-length tuple is. + let (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) = (left, right) else { + return Ok(Type::unknown()); + }; + + let left_iter = left.elements().copied(); + let right_iter = right.elements().copied(); + + let mut builder = UnionBuilder::new(self.db()); + + for (l_ty, r_ty) in left_iter.zip(right_iter) { + let pairwise_eq_result = self + .infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty, range) + .expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); + + match pairwise_eq_result + .try_bool(self.db()) + .unwrap_or_else(|err| { + // TODO: We should, whenever possible, pass the range of the left and right elements + // instead of the range of the whole tuple. + err.report_diagnostic(&self.context, range); + err.fallback_truthiness() + }) { + // - AlwaysTrue : Continue to the next pair for lexicographic comparison + Truthiness::AlwaysTrue => continue, + // - AlwaysFalse: + // Lexicographic comparisons will always terminate with this pair. + // Complete the comparison and return the result. + // - Ambiguous: + // Lexicographic comparisons might continue to the next pair (if eq_result is true), + // or terminate here (if eq_result is false). + // To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder. + eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => { + let pairwise_compare_result = match op { + RichCompareOperator::Lt + | RichCompareOperator::Le + | RichCompareOperator::Gt + | RichCompareOperator::Ge => { + self.infer_binary_type_comparison(l_ty, op.into(), r_ty, range)? + } + // For `==` and `!=`, we already figure out the result from `pairwise_eq_result` + // NOTE: The CPython implementation does not account for non-boolean return types + // or cases where `!=` is not the negation of `==`, we also do not consider these cases. + RichCompareOperator::Eq => Type::BooleanLiteral(false), + RichCompareOperator::Ne => Type::BooleanLiteral(true), + }; + + builder = builder.add(pairwise_compare_result); + + if eq_truthiness.is_ambiguous() { + continue; + } + + return Ok(builder.build()); + } + } + } + + // if no more items to compare, we just compare sizes + let (left_len, right_len) = (left.len(), right.len()); + + builder = builder.add(Type::BooleanLiteral(match op { + RichCompareOperator::Eq => left_len == right_len, + RichCompareOperator::Ne => left_len != right_len, + RichCompareOperator::Lt => left_len < right_len, + RichCompareOperator::Le => left_len <= right_len, + RichCompareOperator::Gt => left_len > right_len, + RichCompareOperator::Ge => left_len >= right_len, + })); + + Ok(builder.build()) + } + + fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + let ast::ExprSubscript { + value, + slice, + range: _, + node_index: _, + ctx, + } = subscript; + + match ctx { + ExprContext::Load => self.infer_subscript_load(subscript), + ExprContext::Store => { + let value_ty = self.infer_expression(value); + let slice_ty = self.infer_expression(slice); + self.infer_subscript_expression_types(value, value_ty, slice_ty); + Type::Never + } + ExprContext::Del => { + self.infer_subscript_load(subscript); + Type::Never + } + ExprContext::Invalid => { + let value_ty = self.infer_expression(value); + let slice_ty = self.infer_expression(slice); + self.infer_subscript_expression_types(value, value_ty, slice_ty); + Type::unknown() + } + } + } + + fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + let ast::ExprSubscript { + range: _, + node_index: _, + value, + slice, + ctx: _, + } = subscript; + let value_ty = self.infer_expression(value); + let mut constraint_keys = vec![]; + + // If `value` is a valid reference, we attempt type narrowing by assignment. + if !value_ty.is_unknown() { + if let Ok(expr) = PlaceExpr::try_from(subscript) { + let (place, keys) = + self.infer_place_load(&expr, ast::ExprRef::Subscript(subscript)); + constraint_keys.extend(keys); + if let Place::Type(ty, Boundness::Bound) = place.place { + // Even if we can obtain the subscript type based on the assignments, we still perform default type inference + // (to store the expression type and to report errors). + let slice_ty = self.infer_expression(slice); + self.infer_subscript_expression_types(value, value_ty, slice_ty); + return ty; + } + } + } + + // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the + // subscript inference logic and treat this as an explicit specialization. + // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return + // this callable as the `__class_getitem__` method on `type`. That probably requires + // updating all of the subscript logic below to use custom callables for all of the _other_ + // special cases, too. + if let Type::ClassLiteral(class) = value_ty { + if class.is_known(self.db(), KnownClass::Tuple) { + return self + .infer_tuple_type_expression(slice) + .to_meta_type(self.db()); + } + if let Some(generic_context) = class.generic_context(self.db()) { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + class, + generic_context, + ); + } + } + if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { + return self + .infer_tuple_type_expression(slice) + .to_meta_type(self.db()); + } + + let slice_ty = self.infer_expression(slice); + let result_ty = self.infer_subscript_expression_types(value, value_ty, slice_ty); + self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) + } + + fn infer_explicit_class_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_class: ClassLiteral<'db>, + generic_context: GenericContext<'db>, + ) -> Type<'db> { + let slice_node = subscript.slice.as_ref(); + let call_argument_types = match slice_node { + ast::Expr::Tuple(tuple) => { + let arguments = CallArgumentTypes::positional( + tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), + ); + self.store_expression_type( + slice_node, + TupleType::from_elements(self.db(), arguments.iter().map(|(_, ty)| ty)), + ); + arguments + } + _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), + }; + let binding = Binding::single(value_ty, generic_context.signature(self.db())); + let bindings = match Bindings::from(binding) + .match_parameters(&call_argument_types) + .check_types(self.db(), &call_argument_types) + { + Ok(bindings) => bindings, + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, subscript.into()); + return Type::unknown(); + } + }; + let callable = bindings + .into_iter() + .next() + .expect("valid bindings should have one callable"); + let (_, overload) = callable + .matching_overloads() + .next() + .expect("valid bindings should have matching overload"); + Type::from(generic_class.apply_specialization(self.db(), |_| { + generic_context.specialize_partial(self.db(), overload.parameter_types()) + })) + } + + fn infer_subscript_expression_types( + &self, + value_node: &ast::Expr, + value_ty: Type<'db>, + slice_ty: Type<'db>, + ) -> Type<'db> { + match (value_ty, slice_ty, slice_ty.slice_literal(self.db())) { + (Type::NominalInstance(instance), _, _) + if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + { + self.infer_subscript_expression_types( + value_node, + Type::version_info_tuple(self.db()), + slice_ty, + ) + } + + (Type::Union(union), _, _) => union.map(self.db(), |element| { + self.infer_subscript_expression_types(value_node, *element, slice_ty) + }), + + // TODO: we can map over the intersection and fold the results back into an intersection, + // but we need to make sure we avoid emitting a diagnostic if one positive element has a `__getitem__` + // method but another does not. This means `infer_subscript_expression_types` + // needs to return a `Result` rather than eagerly emitting diagnostics. + (Type::Intersection(_), _, _) => { + todo_type!("Subscript expressions on intersections") + } + + // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` + (Type::Tuple(tuple_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => { + let tuple = tuple_ty.tuple(self.db()); + tuple + .py_index( + self.db(), + i32::try_from(int).expect("checked in branch arm"), + ) + .unwrap_or_else(|_| { + report_index_out_of_bounds( + &self.context, + "tuple", + value_node.into(), + value_ty, + tuple.len().display_minimum(), + int, + ); + Type::unknown() + }) + } + + // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` + (Type::Tuple(tuple_ty), _, Some(SliceLiteral { start, stop, step })) => { + let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else { + return todo_type!("slice into variable-length tuple"); + }; + + if let Ok(new_elements) = tuple.py_slice(self.db(), start, stop, step) { + TupleType::from_elements(self.db(), new_elements.copied()) + } else { + report_slice_step_size_zero(&self.context, value_node.into()); + Type::unknown() + } + } + + // Ex) Given `"value"[1]`, return `"a"` + (Type::StringLiteral(literal_ty), Type::IntLiteral(int), _) + if i32::try_from(int).is_ok() => + { + let literal_value = literal_ty.value(self.db()); + (&mut literal_value.chars()) + .py_index( + self.db(), + i32::try_from(int).expect("checked in branch arm"), + ) + .map(|ch| Type::string_literal(self.db(), &ch.to_string())) + .unwrap_or_else(|_| { + report_index_out_of_bounds( + &self.context, + "string", + value_node.into(), + value_ty, + literal_value.chars().count(), + int, + ); + Type::unknown() + }) + } + + // Ex) Given `"value"[1:3]`, return `"al"` + (Type::StringLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => { + let literal_value = literal_ty.value(self.db()); + + let chars: Vec<_> = literal_value.chars().collect(); + + if let Ok(new_chars) = chars.py_slice(self.db(), start, stop, step) { + let literal: String = new_chars.collect(); + Type::string_literal(self.db(), &literal) + } else { + report_slice_step_size_zero(&self.context, value_node.into()); + Type::unknown() + } + } + + // Ex) Given `b"value"[1]`, return `97` (i.e., `ord(b"a")`) + (Type::BytesLiteral(literal_ty), Type::IntLiteral(int), _) + if i32::try_from(int).is_ok() => + { + let literal_value = literal_ty.value(self.db()); + literal_value + .py_index( + self.db(), + i32::try_from(int).expect("checked in branch arm"), + ) + .map(|byte| Type::IntLiteral((*byte).into())) + .unwrap_or_else(|_| { + report_index_out_of_bounds( + &self.context, + "bytes literal", + value_node.into(), + value_ty, + literal_value.len(), + int, + ); + Type::unknown() + }) + } + + // Ex) Given `b"value"[1:3]`, return `b"al"` + (Type::BytesLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => { + let literal_value = literal_ty.value(self.db()); + + if let Ok(new_bytes) = literal_value.py_slice(self.db(), start, stop, step) { + let new_bytes: Vec = new_bytes.copied().collect(); + Type::bytes_literal(self.db(), &new_bytes) + } else { + report_slice_step_size_zero(&self.context, value_node.into()); + Type::unknown() + } + } + + // Ex) Given `"value"[True]`, return `"a"` + ( + Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_), + Type::BooleanLiteral(bool), + _, + ) => self.infer_subscript_expression_types( + value_node, + value_ty, + Type::IntLiteral(i64::from(bool)), + ), + + (Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => { + let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else { + // TODO: emit a diagnostic + return Type::unknown(); + }; + self.legacy_generic_class_context( + value_node, + typevars.elements_slice(), + LegacyGenericBase::Protocol, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))) + .unwrap_or_else(Type::unknown) + } + + (Type::SpecialForm(SpecialFormType::Protocol), typevar, _) => self + .legacy_generic_class_context( + value_node, + std::slice::from_ref(&typevar), + LegacyGenericBase::Protocol, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))) + .unwrap_or_else(Type::unknown), + + (Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)), _, _) => { + // TODO: emit a diagnostic + todo_type!("doubly-specialized typing.Protocol") + } + + (Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => { + let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else { + // TODO: emit a diagnostic + return Type::unknown(); + }; + self.legacy_generic_class_context( + value_node, + typevars.elements_slice(), + LegacyGenericBase::Generic, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))) + .unwrap_or_else(Type::unknown) + } + + (Type::SpecialForm(SpecialFormType::Generic), typevar, _) => self + .legacy_generic_class_context( + value_node, + std::slice::from_ref(&typevar), + LegacyGenericBase::Generic, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))) + .unwrap_or_else(Type::unknown), + + (Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)), _, _) => { + // TODO: emit a diagnostic + todo_type!("doubly-specialized typing.Generic") + } + + (Type::SpecialForm(special_form), _, _) if special_form.class().is_special_form() => { + todo_type!("Inference of subscript on special form") + } + + (Type::KnownInstance(known_instance), _, _) + if known_instance.class().is_special_form() => + { + todo_type!("Inference of subscript on special form") + } + + (value_ty, slice_ty, _) => { + // If the class defines `__getitem__`, return its return type. + // + // See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem + match value_ty.try_call_dunder( + self.db(), + "__getitem__", + CallArgumentTypes::positional([slice_ty]), + ) { + Ok(outcome) => return outcome.return_type(self.db()), + Err(err @ CallDunderError::PossiblyUnbound { .. }) => { + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__getitem__` of type `{}` is possibly unbound", + value_ty.display(self.db()), + )); + } + + return err.fallback_return_type(self.db()); + } + Err(CallDunderError::CallError(_, bindings)) => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__getitem__` of type `{}` \ + is not callable on object of type `{}`", + bindings.callable_type().display(self.db()), + value_ty.display(self.db()), + )); + } + + return bindings.return_type(self.db()); + } + Err(CallDunderError::MethodNotAvailable) => { + // try `__class_getitem__` + } + } + + // Otherwise, if the value is itself a class and defines `__class_getitem__`, + // return its return type. + // + // TODO: lots of classes are only subscriptable at runtime on Python 3.9+, + // *but* we should also allow them to be subscripted in stubs + // (and in annotations if `from __future__ import annotations` is enabled), + // even if the target version is Python 3.8 or lower, + // despite the fact that there will be no corresponding `__class_getitem__` + // method in these `sys.version_info` branches. + if value_ty.is_subtype_of(self.db(), KnownClass::Type.to_instance(self.db())) { + let dunder_class_getitem_method = + value_ty.member(self.db(), "__class_getitem__").place; + + match dunder_class_getitem_method { + Place::Unbound => {} + Place::Type(ty, boundness) => { + if boundness == Boundness::PossiblyUnbound { + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__class_getitem__` of type `{}` \ + is possibly unbound", + value_ty.display(self.db()), + )); + } + } + + match ty.try_call( + self.db(), + &CallArgumentTypes::positional([value_ty, slice_ty]), + ) { + Ok(bindings) => return bindings.return_type(self.db()), + Err(CallError(_, bindings)) => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__class_getitem__` of type `{}` \ + is not callable on object of type `{}`", + bindings.callable_type().display(self.db()), + value_ty.display(self.db()), + )); + } + return bindings.return_type(self.db()); + } + } + } + } + + if let Type::ClassLiteral(class) = value_ty { + if class.is_known(self.db(), KnownClass::Type) { + return KnownClass::GenericAlias.to_instance(self.db()); + } + + if class.generic_context(self.db()).is_some() { + // TODO: specialize the generic class using these explicit type + // variable assignments. This branch is only encountered when an + // explicit class specialization appears inside of some other subscript + // expression, e.g. `tuple[list[int], ...]`. We have already inferred + // the type of the outer subscript slice as a value expression, which + // means we can't re-infer the inner specialization here as a type + // expression. + return value_ty; + } + } + + // TODO: properly handle old-style generics; get rid of this temporary hack + if !value_ty.into_class_literal().is_some_and(|class| { + class + .iter_mro(self.db(), None) + .contains(&ClassBase::Generic) + }) { + report_non_subscriptable( + &self.context, + value_node.into(), + value_ty, + "__class_getitem__", + ); + } + } else { + report_non_subscriptable( + &self.context, + value_node.into(), + value_ty, + "__getitem__", + ); + } + + Type::unknown() + } + } + } + + fn legacy_generic_class_context( + &self, + value_node: &ast::Expr, + typevars: &[Type<'db>], + origin: LegacyGenericBase, + ) -> Option> { + let typevars: Option> = typevars + .iter() + .map(|typevar| match typevar { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(*typevar), + _ => { + if let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) + { + builder.into_diagnostic(format_args!( + "`{}` is not a valid argument to `{origin}`", + typevar.display(self.db()), + )); + } + None + } + }) + .collect(); + typevars.map(|typevars| GenericContext::new(self.db(), typevars)) + } + + fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { + enum SliceArg<'db> { + Arg(Type<'db>), + Unsupported, + } + + let ast::ExprSlice { + range: _, + node_index: _, + lower, + upper, + step, + } = slice; + + let ty_lower = self.infer_optional_expression(lower.as_deref()); + let ty_upper = self.infer_optional_expression(upper.as_deref()); + let ty_step = self.infer_optional_expression(step.as_deref()); + + let type_to_slice_argument = |ty: Option>| match ty { + Some(ty @ (Type::IntLiteral(_) | Type::BooleanLiteral(_))) => SliceArg::Arg(ty), + Some(ty @ Type::NominalInstance(instance)) + if instance.class.is_known(self.db(), KnownClass::NoneType) => + { + SliceArg::Arg(ty) + } + None => SliceArg::Arg(Type::none(self.db())), + _ => SliceArg::Unsupported, + }; + + match ( + type_to_slice_argument(ty_lower), + type_to_slice_argument(ty_upper), + type_to_slice_argument(ty_step), + ) { + (SliceArg::Arg(lower), SliceArg::Arg(upper), SliceArg::Arg(step)) => { + KnownClass::Slice.to_specialized_instance(self.db(), [lower, upper, step]) + } + _ => KnownClass::Slice.to_instance(self.db()), + } + } + + fn infer_type_parameters(&mut self, type_parameters: &ast::TypeParams) { + let ast::TypeParams { + range: _, + node_index: _, + type_params, + } = type_parameters; + for type_param in type_params { + match type_param { + ast::TypeParam::TypeVar(node) => self.infer_definition(node), + ast::TypeParam::ParamSpec(node) => self.infer_definition(node), + ast::TypeParam::TypeVarTuple(node) => self.infer_definition(node), + } + } + } + + pub(super) fn finish(mut self) -> TypeInference<'db> { + self.infer_region(); + self.types.diagnostics = self.context.finish(); + self.types.shrink_to_fit(); + self.types + } +} + +/// Annotation expressions. +impl<'db> TypeInferenceBuilder<'db, '_> { + /// Infer the type of an annotation expression with the given [`DeferredExpressionState`]. + fn infer_annotation_expression( + &mut self, + annotation: &ast::Expr, + deferred_state: DeferredExpressionState, + ) -> TypeAndQualifiers<'db> { + let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state); + let annotation_ty = self.infer_annotation_expression_impl(annotation); + self.deferred_state = previous_deferred_state; + annotation_ty + } + + /// Similar to [`infer_annotation_expression`], but accepts an optional annotation expression + /// and returns [`None`] if the annotation is [`None`]. + /// + /// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression + fn infer_optional_annotation_expression( + &mut self, + annotation: Option<&ast::Expr>, + deferred_state: DeferredExpressionState, + ) -> Option> { + annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state)) + } + + /// Implementation of [`infer_annotation_expression`]. + /// + /// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression + fn infer_annotation_expression_impl( + &mut self, + annotation: &ast::Expr, + ) -> TypeAndQualifiers<'db> { + // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression + let annotation_ty = match annotation { + // String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations + ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string), + + // Annotation expressions also get special handling for `*args` and `**kwargs`. + ast::Expr::Starred(starred) => self.infer_starred_expression(starred).into(), + + ast::Expr::BytesLiteral(bytes) => { + if let Some(builder) = self + .context + .report_lint(&BYTE_STRING_TYPE_ANNOTATION, bytes) + { + builder.into_diagnostic("Type expressions cannot use bytes literal"); + } + TypeAndQualifiers::unknown() + } + + ast::Expr::FString(fstring) => { + if let Some(builder) = self.context.report_lint(&FSTRING_TYPE_ANNOTATION, fstring) { + builder.into_diagnostic("Type expressions cannot use f-strings"); + } + self.infer_fstring_expression(fstring); + TypeAndQualifiers::unknown() + } + + ast::Expr::Name(name) => match name.ctx { + ast::ExprContext::Load => { + let name_expr_ty = self.infer_name_expression(name); + match name_expr_ty { + Type::SpecialForm(SpecialFormType::ClassVar) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR) + } + Type::SpecialForm(SpecialFormType::Final) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL) + } + _ => name_expr_ty + .in_type_expression(self.db(), self.scope()) + .unwrap_or_else(|error| { + error.into_fallback_type( + &self.context, + annotation, + self.is_reachable(annotation), + ) + }) + .into(), + } + } + ast::ExprContext::Invalid => TypeAndQualifiers::unknown(), + ast::ExprContext::Store | ast::ExprContext::Del => { + todo_type!("Name expression annotation in Store/Del context").into() + } + }, + + ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => { + let value_ty = self.infer_expression(value); + + let slice = &**slice; + + match value_ty { + Type::SpecialForm(SpecialFormType::Annotated) => { + // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but + // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly + // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). + if let ast::Expr::Tuple(ast::ExprTuple { + elts: arguments, .. + }) = slice + { + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); + } + + if let [inner_annotation, metadata @ ..] = &arguments[..] { + for element in metadata { + self.infer_expression(element); + } + + let inner_annotation_ty = + self.infer_annotation_expression_impl(inner_annotation); + + self.store_expression_type(slice, inner_annotation_ty.inner_type()); + inner_annotation_ty + } else { + for argument in arguments { + self.infer_expression(argument); + } + self.store_expression_type(slice, Type::unknown()); + TypeAndQualifiers::unknown() + } + } else { + report_invalid_arguments_to_annotated(&self.context, subscript); + self.infer_annotation_expression_impl(slice) + } + } + Type::SpecialForm( + type_qualifier @ (SpecialFormType::ClassVar | SpecialFormType::Final), + ) => { + let arguments = if let ast::Expr::Tuple(tuple) = slice { + &*tuple.elts + } else { + std::slice::from_ref(slice) + }; + let num_arguments = arguments.len(); + let type_and_qualifiers = if num_arguments == 1 { + let mut type_and_qualifiers = + self.infer_annotation_expression_impl(slice); + match type_qualifier { + SpecialFormType::ClassVar => { + type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); + } + SpecialFormType::Final => { + type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL); + } + _ => unreachable!(), + } + type_and_qualifiers + } else { + for element in arguments { + self.infer_annotation_expression_impl(element); + } + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "Type qualifier `{type_qualifier}` expected exactly 1 argument, \ + got {num_arguments}", + )); + } + Type::unknown().into() + }; + if slice.is_tuple_expr() { + self.store_expression_type(slice, type_and_qualifiers.inner_type()); + } + type_and_qualifiers + } + _ => self + .infer_subscript_type_expression_no_store(subscript, slice, value_ty) + .into(), + } + } + + // All other annotation expressions are (possibly) valid type expressions, so handle + // them there instead. + type_expr => self.infer_type_expression_no_store(type_expr).into(), + }; + + self.store_expression_type(annotation, annotation_ty.inner_type()); + + annotation_ty + } + + /// Infer the type of a string annotation expression. + fn infer_string_annotation_expression( + &mut self, + string: &ast::ExprStringLiteral, + ) -> TypeAndQualifiers<'db> { + match parse_string_annotation(&self.context, string) { + Some(parsed) => { + // String annotations are always evaluated in the deferred context. + self.infer_annotation_expression( + parsed.expr(), + DeferredExpressionState::InStringAnnotation( + self.enclosing_node_key(string.into()), + ), + ) + } + None => TypeAndQualifiers::unknown(), + } + } +} + +/// Type expressions +impl<'db> TypeInferenceBuilder<'db, '_> { + /// Infer the type of a type expression. + fn infer_type_expression(&mut self, expression: &ast::Expr) -> Type<'db> { + let ty = self.infer_type_expression_no_store(expression); + self.store_expression_type(expression, ty); + ty + } + + /// Similar to [`infer_type_expression`], but accepts an optional type expression and returns + /// [`None`] if the expression is [`None`]. + /// + /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression + fn infer_optional_type_expression( + &mut self, + expression: Option<&ast::Expr>, + ) -> Option> { + expression.map(|expr| self.infer_type_expression(expr)) + } + + /// Similar to [`infer_type_expression`], but accepts a [`DeferredExpressionState`]. + /// + /// [`infer_type_expression`]: TypeInferenceBuilder::infer_type_expression + fn infer_type_expression_with_state( + &mut self, + expression: &ast::Expr, + deferred_state: DeferredExpressionState, + ) -> Type<'db> { + let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state); + let annotation_ty = self.infer_type_expression(expression); + self.deferred_state = previous_deferred_state; + annotation_ty + } + + fn report_invalid_type_expression( + &self, + expression: &ast::Expr, + message: std::fmt::Arguments, + ) -> Option { + self.context + .report_lint(&INVALID_TYPE_FORM, expression) + .map(|builder| { + diagnostic::add_type_expression_reference_link(builder.into_diagnostic(message)) + }) + } + + /// Infer the type of a type expression without storing the result. + fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> { + // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression + match expression { + ast::Expr::Name(name) => match name.ctx { + ast::ExprContext::Load => self + .infer_name_expression(name) + .in_type_expression(self.db(), self.scope()) + .unwrap_or_else(|error| { + error.into_fallback_type( + &self.context, + expression, + self.is_reachable(expression), + ) + }), + ast::ExprContext::Invalid => Type::unknown(), + ast::ExprContext::Store | ast::ExprContext::Del => { + todo_type!("Name expression annotation in Store/Del context") + } + }, + + ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx { + ast::ExprContext::Load => self + .infer_attribute_expression(attribute_expression) + .in_type_expression(self.db(), self.scope()) + .unwrap_or_else(|error| { + error.into_fallback_type( + &self.context, + expression, + self.is_reachable(expression), + ) + }), + ast::ExprContext::Invalid => Type::unknown(), + ast::ExprContext::Store | ast::ExprContext::Del => { + todo_type!("Attribute expression annotation in Store/Del context") + } + }, + + ast::Expr::NoneLiteral(_literal) => Type::none(self.db()), + + // https://typing.python.org/en/latest/spec/annotations.html#string-annotations + ast::Expr::StringLiteral(string) => self.infer_string_type_expression(string), + + ast::Expr::Subscript(subscript) => { + let ast::ExprSubscript { + value, + slice, + ctx: _, + range: _, + node_index: _, + } = subscript; + + let value_ty = self.infer_expression(value); + + self.infer_subscript_type_expression_no_store(subscript, slice, value_ty) + } + + ast::Expr::BinOp(binary) => { + match binary.op { + // PEP-604 unions are okay, e.g., `int | str` + ast::Operator::BitOr => { + let left_ty = self.infer_type_expression(&binary.left); + let right_ty = self.infer_type_expression(&binary.right); + UnionType::from_elements(self.db(), [left_ty, right_ty]) + } + // anything else is an invalid annotation: + op => { + self.infer_binary_expression(binary); + if let Some(mut diag) = self.report_invalid_type_expression( + expression, + format_args!( + "Invalid binary operator `{}` in type annotation", + op.as_str() + ), + ) { + diag.info("Did you mean to use `|`?"); + } + Type::unknown() + } + } + } + + // Avoid inferring the types of invalid type expressions that have been parsed from a + // string annotation, as they are not present in the semantic index. + _ if self.deferred_state.in_string_annotation() => Type::unknown(), + + // ===================================================================================== + // Forms which are invalid in the context of annotation expressions: we infer their + // nested expressions as normal expressions, but the type of the top-level expression is + // always `Type::unknown` in these cases. + // ===================================================================================== + + // TODO: add a subdiagnostic linking to type-expression grammar + // and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]` + ast::Expr::BytesLiteral(_) => { + self.report_invalid_type_expression( + expression, + format_args!( + "Bytes literals are not allowed in this context in a type expression" + ), + ); + Type::unknown() + } + + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(_), + .. + }) => { + self.report_invalid_type_expression( + expression, + format_args!( + "Int literals are not allowed in this context in a type expression" + ), + ); + + Type::unknown() + } + + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Float(_), + .. + }) => { + self.report_invalid_type_expression( + expression, + format_args!("Float literals are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Complex { .. }, + .. + }) => { + self.report_invalid_type_expression( + expression, + format_args!("Complex literals are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::BooleanLiteral(_) => { + self.report_invalid_type_expression( + expression, + format_args!( + "Boolean literals are not allowed in this context in a type expression" + ), + ); + Type::unknown() + } + + ast::Expr::List(list) => { + let db = self.db(); + + let inner_types: Vec> = list + .iter() + .map(|element| self.infer_type_expression(element)) + .collect(); + + if let Some(mut diagnostic) = self.report_invalid_type_expression( + expression, + format_args!( + "List literals are not allowed in this context in a type expression" + ), + ) { + if !inner_types.iter().any(|ty| { + matches!( + ty, + Type::Dynamic(DynamicType::Todo(_) | DynamicType::Unknown) + ) + }) { + let hinted_type = if list.len() == 1 { + KnownClass::List.to_specialized_instance(db, inner_types) + } else { + TupleType::from_elements(db, inner_types) + }; + + diagnostic.set_primary_message(format_args!( + "Did you mean `{}`?", + hinted_type.display(self.db()), + )); + } + } + Type::unknown() + } + + ast::Expr::Tuple(tuple) => { + let inner_types: Vec> = tuple + .elts + .iter() + .map(|expr| self.infer_type_expression(expr)) + .collect(); + + if tuple.parenthesized { + if let Some(mut diagnostic) = self.report_invalid_type_expression( + expression, + format_args!( + "Tuple literals are not allowed in this context in a type expression" + ), + ) { + if !inner_types.iter().any(|ty| { + matches!( + ty, + Type::Dynamic(DynamicType::Todo(_) | DynamicType::Unknown) + ) + }) { + let hinted_type = TupleType::from_elements(self.db(), inner_types); + diagnostic.set_primary_message(format_args!( + "Did you mean `{}`?", + hinted_type.display(self.db()), + )); + } + } + } + Type::unknown() + } + + ast::Expr::BoolOp(bool_op) => { + self.infer_boolean_expression(bool_op); + self.report_invalid_type_expression( + expression, + format_args!("Boolean operations are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Named(named) => { + self.infer_named_expression(named); + self.report_invalid_type_expression( + expression, + format_args!("Named expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::UnaryOp(unary) => { + self.infer_unary_expression(unary); + self.report_invalid_type_expression( + expression, + format_args!("Unary operations are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Lambda(lambda_expression) => { + self.infer_lambda_expression(lambda_expression); + self.report_invalid_type_expression( + expression, + format_args!("`lambda` expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::If(if_expression) => { + self.infer_if_expression(if_expression); + self.report_invalid_type_expression( + expression, + format_args!("`if` expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Dict(dict) => { + self.infer_dict_expression(dict); + self.report_invalid_type_expression( + expression, + format_args!("Dict literals are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Set(set) => { + self.infer_set_expression(set); + self.report_invalid_type_expression( + expression, + format_args!("Set literals are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::DictComp(dictcomp) => { + self.infer_dict_comprehension_expression(dictcomp); + self.report_invalid_type_expression( + expression, + format_args!("Dict comprehensions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::ListComp(listcomp) => { + self.infer_list_comprehension_expression(listcomp); + self.report_invalid_type_expression( + expression, + format_args!("List comprehensions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::SetComp(setcomp) => { + self.infer_set_comprehension_expression(setcomp); + self.report_invalid_type_expression( + expression, + format_args!("Set comprehensions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Generator(generator) => { + self.infer_generator_expression(generator); + self.report_invalid_type_expression( + expression, + format_args!("Generator expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Await(await_expression) => { + self.infer_await_expression(await_expression); + self.report_invalid_type_expression( + expression, + format_args!("`await` expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Yield(yield_expression) => { + self.infer_yield_expression(yield_expression); + self.report_invalid_type_expression( + expression, + format_args!("`yield` expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::YieldFrom(yield_from) => { + self.infer_yield_from_expression(yield_from); + self.report_invalid_type_expression( + expression, + format_args!("`yield from` expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Compare(compare) => { + self.infer_compare_expression(compare); + self.report_invalid_type_expression( + expression, + format_args!("Comparison expressions are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Call(call_expr) => { + self.infer_call_expression(call_expr); + self.report_invalid_type_expression( + expression, + format_args!("Function calls are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::FString(fstring) => { + self.infer_fstring_expression(fstring); + self.report_invalid_type_expression( + expression, + format_args!("F-strings are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::TString(tstring) => { + self.infer_tstring_expression(tstring); + self.report_invalid_type_expression( + expression, + format_args!("T-strings are not allowed in type expressions"), + ); + Type::unknown() + } + + ast::Expr::Slice(slice) => { + self.infer_slice_expression(slice); + self.report_invalid_type_expression( + expression, + format_args!("Slices are not allowed in type expressions"), + ); + Type::unknown() + } + + // ================================================================================= + // Branches where we probably should emit diagnostics in some context, but don't yet + // ================================================================================= + ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"), + + ast::Expr::EllipsisLiteral(_) => { + todo_type!("ellipsis literal in type expression") + } + + ast::Expr::Starred(starred) => self.infer_starred_type_expression(starred), + } + } + + fn infer_starred_type_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { + let ast::ExprStarred { + range: _, + node_index: _, + value, + ctx: _, + } = starred; + + let starred_type = self.infer_type_expression(value); + if let Type::Tuple(_) = starred_type { + starred_type + } else { + todo_type!("PEP 646") + } + } + + fn infer_subscript_type_expression_no_store( + &mut self, + subscript: &ast::ExprSubscript, + slice: &ast::Expr, + value_ty: Type<'db>, + ) -> Type<'db> { + match value_ty { + Type::ClassLiteral(class_literal) => match class_literal.known(self.db()) { + Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice), + Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice), + _ => self.infer_subscript_type_expression(subscript, value_ty), + }, + _ => self.infer_subscript_type_expression(subscript, value_ty), + } + } + + /// Infer the type of a string type expression. + fn infer_string_type_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> { + match parse_string_annotation(&self.context, string) { + Some(parsed) => { + // String annotations are always evaluated in the deferred context. + self.infer_type_expression_with_state( + parsed.expr(), + DeferredExpressionState::InStringAnnotation( + self.enclosing_node_key(string.into()), + ), + ) + } + None => Type::unknown(), + } + } + + /// Given the slice of a `tuple[]` annotation, return the type that the annotation represents + fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> { + /// In most cases, if a subelement of the tuple is inferred as `Todo`, + /// we should only infer `Todo` for that specific subelement. + /// Certain specific AST nodes can however change the meaning of the entire tuple, + /// however: for example, `tuple[int, ...]` or `tuple[int, *tuple[str, ...]]` are a + /// homogeneous tuple and a partly homogeneous tuple (respectively) due to the `...` + /// and the starred expression (respectively), Neither is supported by us right now, + /// so we should infer `Todo` for the *entire* tuple if we encounter one of those elements. + fn element_could_alter_type_of_whole_tuple( + element: &ast::Expr, + element_ty: Type, + builder: &mut TypeInferenceBuilder, + ) -> bool { + if !element_ty.is_todo() { + return false; + } + + match element { + ast::Expr::Starred(_) => !matches!(element_ty, Type::Tuple(_)), + ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => { + let value_ty = if builder.deferred_state.in_string_annotation() { + // Using `.expression_type` does not work in string annotations, because + // we do not store types for sub-expressions. Re-infer the type here. + builder.infer_expression(value) + } else { + builder.expression_type(value) + }; + + value_ty == Type::SpecialForm(SpecialFormType::Unpack) + } + _ => false, + } + } + + // TODO: PEP 646 + match tuple_slice { + ast::Expr::Tuple(elements) => { + if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts { + self.infer_expression(ellipsis); + let result = + TupleType::homogeneous(self.db(), self.infer_type_expression(element)); + self.store_expression_type(tuple_slice, result); + return result; + } + + let mut element_types = TupleSpec::with_capacity(elements.len()); + + // Whether to infer `Todo` for the whole tuple + // (see docstring for `element_could_alter_type_of_whole_tuple`) + let mut return_todo = false; + + for element in elements { + let element_ty = self.infer_type_expression(element); + return_todo |= + element_could_alter_type_of_whole_tuple(element, element_ty, self); + if let ast::Expr::Starred(_) = element { + if let Type::Tuple(inner_tuple) = element_ty { + element_types = + element_types.concat(self.db(), inner_tuple.tuple(self.db())); + } else { + // TODO: emit a diagnostic + } + } else { + element_types.push(element_ty); + } + } + + let ty = if return_todo { + todo_type!("PEP 646") + } else { + Type::tuple(TupleType::new(self.db(), element_types)) + }; + + // Here, we store the type for the inner `int, str` tuple-expression, + // while the type for the outer `tuple[int, str]` slice-expression is + // stored in the surrounding `infer_type_expression` call: + self.store_expression_type(tuple_slice, ty); + + ty + } + single_element => { + let single_element_ty = self.infer_type_expression(single_element); + if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self) + { + todo_type!("PEP 646") + } else { + TupleType::from_elements(self.db(), std::iter::once(single_element_ty)) + } + } + } + } + + /// Given the slice of a `type[]` annotation, return the type that the annotation represents + fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> { + match slice { + ast::Expr::Name(_) | ast::Expr::Attribute(_) => { + let name_ty = self.infer_expression(slice); + match name_ty { + Type::ClassLiteral(class_literal) => { + if class_literal.is_known(self.db(), KnownClass::Any) { + SubclassOfType::subclass_of_any() + } else { + SubclassOfType::from( + self.db(), + class_literal.default_specialization(self.db()), + ) + } + } + Type::SpecialForm(SpecialFormType::Unknown) => { + SubclassOfType::subclass_of_unknown() + } + _ => todo_type!("unsupported type[X] special form"), + } + } + ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => { + let union_ty = UnionType::from_elements( + self.db(), + [ + self.infer_subclass_of_type_expression(&binary.left), + self.infer_subclass_of_type_expression(&binary.right), + ], + ); + self.store_expression_type(slice, union_ty); + + union_ty + } + ast::Expr::Tuple(_) => { + self.infer_type_expression(slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, slice) { + builder.into_diagnostic("type[...] must have exactly one type argument"); + } + Type::unknown() + } + ast::Expr::Subscript(ast::ExprSubscript { + value, + slice: parameters, + .. + }) => { + let parameters_ty = match self.infer_expression(value) { + Type::SpecialForm(SpecialFormType::Union) => match &**parameters { + ast::Expr::Tuple(tuple) => { + let ty = UnionType::from_elements( + self.db(), + tuple + .iter() + .map(|element| self.infer_subclass_of_type_expression(element)), + ); + self.store_expression_type(parameters, ty); + ty + } + _ => self.infer_subclass_of_type_expression(parameters), + }, + _ => { + self.infer_type_expression(parameters); + todo_type!("unsupported nested subscript in type[X]") + } + }; + self.store_expression_type(slice, parameters_ty); + parameters_ty + } + // TODO: subscripts, etc. + _ => { + self.infer_type_expression(slice); + todo_type!("unsupported type[X] special form") + } + } + } + + fn infer_subscript_type_expression( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + ) -> Type<'db> { + let ast::ExprSubscript { + range: _, + node_index: _, + value: _, + slice, + ctx: _, + } = subscript; + + match value_ty { + Type::Never => { + // This case can be entered when we use a type annotation like `Literal[1]` + // in unreachable code, since we infer `Never` for `Literal`. We call + // `infer_expression` (instead of `infer_type_expression`) here to avoid + // false-positive `invalid-type-form` diagnostics (`1` is not a valid type + // expression). + self.infer_expression(&subscript.slice); + Type::unknown() + } + Type::ClassLiteral(literal) if literal.is_known(self.db(), KnownClass::Any) => { + self.infer_expression(slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic("Type `typing.Any` expected no type parameter"); + } + Type::unknown() + } + Type::SpecialForm(special_form) => { + self.infer_parameterized_special_form_type_expression(subscript, special_form) + } + Type::KnownInstance(known_instance) => match known_instance { + KnownInstanceType::SubscriptedProtocol(_) => { + self.infer_type_expression(&subscript.slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.Protocol` is not allowed in type expressions", + )); + } + Type::unknown() + } + KnownInstanceType::SubscriptedGeneric(_) => { + self.infer_type_expression(&subscript.slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.Generic` is not allowed in type expressions", + )); + } + Type::unknown() + } + KnownInstanceType::TypeVar(_) => { + self.infer_type_expression(&subscript.slice); + todo_type!("TypeVar annotations") + } + KnownInstanceType::TypeAliasType(_) => { + self.infer_type_expression(&subscript.slice); + todo_type!("Generic PEP-695 type alias") + } + }, + Type::Dynamic(DynamicType::Todo(_)) => { + self.infer_type_expression(slice); + value_ty + } + Type::ClassLiteral(class) => { + match class.generic_context(self.db()) { + Some(generic_context) => { + let specialized_class = self.infer_explicit_class_specialization( + subscript, + value_ty, + class, + generic_context, + ); + specialized_class + .in_type_expression(self.db(), self.scope()) + .unwrap_or(Type::unknown()) + } + None => { + // TODO: Once we know that e.g. `list` is generic, emit a diagnostic if you try to + // specialize a non-generic class. + self.infer_type_expression(slice); + todo_type!("specialized non-generic class") + } + } + } + _ => { + // TODO: Emit a diagnostic once we've implemented all valid subscript type + // expressions. + self.infer_type_expression(slice); + todo_type!("unknown type subscript") + } + } + } + + fn infer_parameterized_legacy_typing_alias( + &mut self, + subscript_node: &ast::ExprSubscript, + expected_arg_count: usize, + alias: SpecialFormType, + class: KnownClass, + ) -> Type<'db> { + let arguments = &*subscript_node.slice; + let args = if let ast::Expr::Tuple(t) = arguments { + &*t.elts + } else { + std::slice::from_ref(arguments) + }; + if args.len() != expected_arg_count { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) { + let noun = if expected_arg_count == 1 { + "argument" + } else { + "arguments" + }; + builder.into_diagnostic(format_args!( + "Legacy alias `{alias}` expected exactly {expected_arg_count} {noun}, \ + got {}", + args.len() + )); + } + } + let ty = class.to_specialized_instance( + self.db(), + args.iter().map(|node| self.infer_type_expression(node)), + ); + if arguments.is_tuple_expr() { + self.store_expression_type(arguments, ty); + } + ty + } + + fn infer_parameterized_special_form_type_expression( + &mut self, + subscript: &ast::ExprSubscript, + special_form: SpecialFormType, + ) -> Type<'db> { + let db = self.db(); + let arguments_slice = &*subscript.slice; + match special_form { + SpecialFormType::Annotated => { + let ast::Expr::Tuple(ast::ExprTuple { + elts: arguments, .. + }) = arguments_slice + else { + report_invalid_arguments_to_annotated(&self.context, subscript); + + // `Annotated[]` with less than two arguments is an error at runtime. + // However, we still treat `Annotated[T]` as `T` here for the purpose of + // giving better diagnostics later on. + // Pyright also does this. Mypy doesn't; it falls back to `Any` instead. + return self.infer_type_expression(arguments_slice); + }; + + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); + } + + let [type_expr, metadata @ ..] = &arguments[..] else { + for argument in arguments { + self.infer_expression(argument); + } + self.store_expression_type(arguments_slice, Type::unknown()); + return Type::unknown(); + }; + + for element in metadata { + self.infer_expression(element); + } + + let ty = self.infer_type_expression(type_expr); + self.store_expression_type(arguments_slice, ty); + ty + } + SpecialFormType::Literal => match self.infer_literal_parameter_type(arguments_slice) { + Ok(ty) => ty, + Err(nodes) => { + for node in nodes { + let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ + a literal value (int, bool, str, or bytes), or an enum value", + ); + } + Type::unknown() + } + }, + SpecialFormType::Optional => { + let param_type = self.infer_type_expression(arguments_slice); + UnionType::from_elements(db, [param_type, Type::none(db)]) + } + SpecialFormType::Union => match arguments_slice { + ast::Expr::Tuple(t) => { + let union_ty = UnionType::from_elements( + db, + t.iter().map(|elt| self.infer_type_expression(elt)), + ); + self.store_expression_type(arguments_slice, union_ty); + union_ty + } + _ => self.infer_type_expression(arguments_slice), + }, + SpecialFormType::Callable => { + let mut arguments = match arguments_slice { + ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), + _ => { + self.infer_callable_parameter_types(arguments_slice); + Either::Right(std::iter::empty::<&ast::Expr>()) + } + }; + + let first_argument = arguments.next(); + + let parameters = + first_argument.and_then(|arg| self.infer_callable_parameter_types(arg)); + + let return_type = arguments.next().map(|arg| self.infer_type_expression(arg)); + + let correct_argument_number = if let Some(third_argument) = arguments.next() { + self.infer_type_expression(third_argument); + for argument in arguments { + self.infer_type_expression(argument); + } + false + } else { + return_type.is_some() + }; + + if !correct_argument_number { + report_invalid_arguments_to_callable(&self.context, subscript); + } + + let callable_type = if let (Some(parameters), Some(return_type), true) = + (parameters, return_type, correct_argument_number) + { + CallableType::single(db, Signature::new(parameters, Some(return_type))) + } else { + CallableType::unknown(db) + }; + + // `Signature` / `Parameters` are not a `Type` variant, so we're storing + // the outer callable type on these expressions instead. + self.store_expression_type(arguments_slice, callable_type); + if let Some(first_argument) = first_argument { + self.store_expression_type(first_argument, callable_type); + } + + callable_type + } + + // Type API special forms + SpecialFormType::Not => { + let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { + &*tuple.elts + } else { + std::slice::from_ref(arguments_slice) + }; + let num_arguments = arguments.len(); + let negated_type = if num_arguments == 1 { + self.infer_type_expression(&arguments[0]).negate(db) + } else { + for argument in arguments { + self.infer_type_expression(argument); + } + report_invalid_argument_number_to_special_form( + &self.context, + subscript, + special_form, + num_arguments, + 1, + ); + Type::unknown() + }; + if arguments_slice.is_tuple_expr() { + self.store_expression_type(arguments_slice, negated_type); + } + negated_type + } + SpecialFormType::Intersection => { + let elements = match arguments_slice { + ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()), + element => Either::Right(std::iter::once(element)), + }; + + let ty = elements + .fold(IntersectionBuilder::new(db), |builder, element| { + builder.add_positive(self.infer_type_expression(element)) + }) + .build(); + + if matches!(arguments_slice, ast::Expr::Tuple(_)) { + self.store_expression_type(arguments_slice, ty); + } + ty + } + SpecialFormType::TypeOf => { + let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { + &*tuple.elts + } else { + std::slice::from_ref(arguments_slice) + }; + let num_arguments = arguments.len(); + let type_of_type = if num_arguments == 1 { + // N.B. This uses `infer_expression` rather than `infer_type_expression` + self.infer_expression(&arguments[0]) + } else { + for argument in arguments { + self.infer_type_expression(argument); + } + report_invalid_argument_number_to_special_form( + &self.context, + subscript, + special_form, + num_arguments, + 1, + ); + Type::unknown() + }; + if arguments_slice.is_tuple_expr() { + self.store_expression_type(arguments_slice, type_of_type); + } + type_of_type + } + + SpecialFormType::CallableTypeOf => { + let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { + &*tuple.elts + } else { + std::slice::from_ref(arguments_slice) + }; + let num_arguments = arguments.len(); + + if num_arguments != 1 { + for argument in arguments { + self.infer_expression(argument); + } + report_invalid_argument_number_to_special_form( + &self.context, + subscript, + special_form, + num_arguments, + 1, + ); + if arguments_slice.is_tuple_expr() { + self.store_expression_type(arguments_slice, Type::unknown()); + } + return Type::unknown(); + } + + let argument_type = self.infer_expression(&arguments[0]); + let bindings = argument_type.bindings(db); + + // SAFETY: This is enforced by the constructor methods on `Bindings` even in + // the case of a non-callable union. + let callable_binding = bindings + .into_iter() + .next() + .expect("`Bindings` should have at least one `CallableBinding`"); + + let mut signature_iter = callable_binding.into_iter().map(|binding| { + if argument_type.is_bound_method() { + binding.signature.bind_self() + } else { + binding.signature.clone() + } + }); + + let Some(signature) = signature_iter.next() else { + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_FORM, arguments_slice) + { + builder.into_diagnostic(format_args!( + "Expected the first argument to `{special_form}` \ + to be a callable object, \ + but got an object of type `{actual_type}`", + actual_type = argument_type.display(db) + )); + } + if arguments_slice.is_tuple_expr() { + self.store_expression_type(arguments_slice, Type::unknown()); + } + return Type::unknown(); + }; + + let signature = CallableSignature::from_overloads( + std::iter::once(signature).chain(signature_iter), + ); + let callable_type_of = Type::Callable(CallableType::new(db, signature, false)); + if arguments_slice.is_tuple_expr() { + self.store_expression_type(arguments_slice, callable_type_of); + } + callable_type_of + } + + SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::ChainMap, + KnownClass::ChainMap, + ), + SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::OrderedDict, + KnownClass::OrderedDict, + ), + SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::Dict, + KnownClass::Dict, + ), + SpecialFormType::List => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::List, + KnownClass::List, + ), + SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias( + subscript, + 2, + SpecialFormType::DefaultDict, + KnownClass::DefaultDict, + ), + SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::Counter, + KnownClass::Counter, + ), + SpecialFormType::Set => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::Set, + KnownClass::Set, + ), + SpecialFormType::FrozenSet => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::FrozenSet, + KnownClass::FrozenSet, + ), + SpecialFormType::Deque => self.infer_parameterized_legacy_typing_alias( + subscript, + 1, + SpecialFormType::Deque, + KnownClass::Deque, + ), + + SpecialFormType::ReadOnly => { + self.infer_type_expression(arguments_slice); + todo_type!("`ReadOnly[]` type qualifier") + } + SpecialFormType::NotRequired => { + self.infer_type_expression(arguments_slice); + todo_type!("`NotRequired[]` type qualifier") + } + SpecialFormType::ClassVar | SpecialFormType::Final => { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let diag = builder.into_diagnostic(format_args!( + "Type qualifier `{special_form}` is not allowed in type expressions \ + (only in annotation expressions)", + )); + diagnostic::add_type_expression_reference_link(diag); + } + self.infer_type_expression(arguments_slice) + } + SpecialFormType::Required => { + self.infer_type_expression(arguments_slice); + todo_type!("`Required[]` type qualifier") + } + SpecialFormType::TypeIs => match arguments_slice { + ast::Expr::Tuple(_) => { + self.infer_type_expression(arguments_slice); + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let diag = builder.into_diagnostic(format_args!( + "Special form `{}` expected exactly one type parameter", + special_form.repr() + )); + diagnostic::add_type_expression_reference_link(diag); + } + + Type::unknown() + } + _ => TypeIsType::unbound(self.db(), self.infer_type_expression(arguments_slice)), + }, + SpecialFormType::TypeGuard => { + self.infer_type_expression(arguments_slice); + todo_type!("`TypeGuard[]` special form") + } + SpecialFormType::Concatenate => { + let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { + &*tuple.elts + } else { + std::slice::from_ref(arguments_slice) + }; + for argument in arguments { + self.infer_type_expression(argument); + } + let num_arguments = arguments.len(); + let inferred_type = if num_arguments < 2 { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "Special form `{special_form}` expected at least 2 parameters but got {num_arguments}", + )); + } + Type::unknown() + } else { + todo_type!("`Concatenate[]` special form") + }; + if arguments_slice.is_tuple_expr() { + self.store_expression_type(arguments_slice, inferred_type); + } + inferred_type + } + SpecialFormType::Unpack => { + self.infer_type_expression(arguments_slice); + todo_type!("`Unpack[]` special form") + } + SpecialFormType::NoReturn + | SpecialFormType::Never + | SpecialFormType::AlwaysTruthy + | SpecialFormType::AlwaysFalsy => { + self.infer_type_expression(arguments_slice); + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "Type `{special_form}` expected no type parameter", + )); + } + Type::unknown() + } + SpecialFormType::TypingSelf + | SpecialFormType::TypeAlias + | SpecialFormType::TypedDict + | SpecialFormType::Unknown => { + self.infer_type_expression(arguments_slice); + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "Special form `{special_form}` expected no type parameter", + )); + } + Type::unknown() + } + SpecialFormType::LiteralString => { + self.infer_type_expression(arguments_slice); + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let mut diag = builder.into_diagnostic(format_args!( + "Type `{special_form}` expected no type parameter", + )); + diag.info("Did you mean to use `Literal[...]` instead?"); + } + Type::unknown() + } + SpecialFormType::Type => self.infer_subclass_of_type_expression(arguments_slice), + SpecialFormType::Tuple => self.infer_tuple_type_expression(arguments_slice), + SpecialFormType::Generic | SpecialFormType::Protocol => { + self.infer_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`{special_form}` is not allowed in type expressions", + )); + } + Type::unknown() + } + } + } + + fn infer_literal_parameter_type<'param>( + &mut self, + parameters: &'param ast::Expr, + ) -> Result, Vec<&'param ast::Expr>> { + Ok(match parameters { + // TODO handle type aliases + ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + let value_ty = self.infer_expression(value); + if matches!(value_ty, Type::SpecialForm(SpecialFormType::Literal)) { + let ty = self.infer_literal_parameter_type(slice)?; + + // This branch deals with annotations such as `Literal[Literal[1]]`. + // Here, we store the type for the inner `Literal[1]` expression: + self.store_expression_type(parameters, ty); + ty + } else { + self.store_expression_type(parameters, Type::unknown()); + + return Err(vec![parameters]); + } + } + ast::Expr::Tuple(tuple) if !tuple.parenthesized => { + let mut errors = vec![]; + let mut builder = UnionBuilder::new(self.db()); + for elt in tuple { + match self.infer_literal_parameter_type(elt) { + Ok(ty) => { + builder = builder.add(ty); + } + Err(nodes) => { + errors.extend(nodes); + } + } + } + if errors.is_empty() { + let union_type = builder.build(); + + // This branch deals with annotations such as `Literal[1, 2]`. Here, we + // store the type for the inner `1, 2` tuple-expression: + self.store_expression_type(parameters, union_type); + + union_type + } else { + self.store_expression_type(parameters, Type::unknown()); + + return Err(errors); + } + } + + literal @ (ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::BooleanLiteral(_) + | ast::Expr::NoneLiteral(_)) => self.infer_expression(literal), + literal @ ast::Expr::NumberLiteral(number) if number.value.is_int() => { + self.infer_expression(literal) + } + // For enum values + ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => { + let value_ty = self.infer_expression(value); + // TODO: Check that value type is enum otherwise return None + let ty = value_ty + .member(self.db(), &attr.id) + .place + .ignore_possibly_unbound() + .unwrap_or(Type::unknown()); + self.store_expression_type(parameters, ty); + ty + } + // for negative and positive numbers + ast::Expr::UnaryOp(u) + if matches!(u.op, ast::UnaryOp::USub | ast::UnaryOp::UAdd) + && u.operand.is_number_literal_expr() => + { + let ty = self.infer_unary_expression(u); + self.store_expression_type(parameters, ty); + ty + } + _ => { + self.infer_expression(parameters); + return Err(vec![parameters]); + } + }) + } + + /// Infer the first argument to a `typing.Callable` type expression and returns the + /// corresponding [`Parameters`]. + /// + /// It returns `None` if the argument is invalid i.e., not a list of types, parameter + /// specification, `typing.Concatenate`, or `...`. + fn infer_callable_parameter_types( + &mut self, + parameters: &ast::Expr, + ) -> Option> { + match parameters { + ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { .. }) => { + return Some(Parameters::gradual_form()); + } + ast::Expr::List(ast::ExprList { elts: params, .. }) => { + let mut parameter_types = Vec::with_capacity(params.len()); + + // Whether to infer `Todo` for the parameters + let mut return_todo = false; + + for param in params { + let param_type = self.infer_type_expression(param); + // This is similar to what we currently do for inferring tuple type expression. + // We currently infer `Todo` for the parameters to avoid invalid diagnostics + // when trying to check for assignability or any other relation. For example, + // `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported. + return_todo |= param_type.is_todo() + && matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_)); + parameter_types.push(param_type); + } + + return Some(if return_todo { + // TODO: `Unpack` + Parameters::todo() + } else { + Parameters::new(parameter_types.iter().map(|param_type| { + Parameter::positional_only(None).with_annotated_type(*param_type) + })) + }); + } + ast::Expr::Subscript(subscript) => { + let value_ty = self.infer_expression(&subscript.value); + self.infer_subscript_type_expression(subscript, value_ty); + // TODO: Support `Concatenate[...]` + return Some(Parameters::todo()); + } + ast::Expr::Name(name) => { + if name.is_invalid() { + // This is a special case to avoid raising the error suggesting what the first + // argument should be. This only happens when there's already a syntax error like + // `Callable[]`. + return None; + } + match self.infer_name_load(name) { + Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => { + return Some(Parameters::todo()); + } + Type::NominalInstance(NominalInstanceType { class, .. }) + if class.is_known(self.db(), KnownClass::ParamSpec) => + { + return Some(Parameters::todo()); + } + _ => {} + } + } + _ => {} + } + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameters) { + let diag = builder.into_diagnostic(format_args!( + "The first argument to `Callable` must be either a list of types, \ + ParamSpec, Concatenate, or `...`", + )); + diagnostic::add_type_expression_reference_link(diag); + } + None + } +} + +/// The deferred state of a specific expression in an inference region. +#[derive(Default, Debug, Clone, Copy)] +enum DeferredExpressionState { + /// The expression is not deferred. + #[default] + None, + + /// The expression is deferred. + /// + /// In the following example, + /// ```py + /// from __future__ import annotation + /// + /// a: tuple[int, "ForwardRef"] = ... + /// ``` + /// + /// The expression `tuple` and `int` are deferred but `ForwardRef` (after parsing) is both + /// deferred and in a string annotation context. + Deferred, + + /// The expression is in a string annotation context. + /// + /// This is required to differentiate between a deferred annotation and a string annotation. + /// The former can occur when there's a `from __future__ import annotations` statement or we're + /// in a stub file. + /// + /// In the following example, + /// ```py + /// a: "List[int]" = ... + /// b: tuple[int, "ForwardRef"] = ... + /// ``` + /// + /// The annotation of `a` is completely inside a string while for `b`, it's only partially + /// stringified. + /// + /// This variant wraps a [`NodeKey`] that allows us to retrieve the original + /// [`ast::ExprStringLiteral`] node which created the string annotation. + InStringAnnotation(NodeKey), +} + +impl DeferredExpressionState { + const fn is_deferred(self) -> bool { + matches!( + self, + DeferredExpressionState::Deferred | DeferredExpressionState::InStringAnnotation(_) + ) + } + + const fn in_string_annotation(self) -> bool { + matches!(self, DeferredExpressionState::InStringAnnotation(_)) + } +} + +impl From for DeferredExpressionState { + fn from(value: bool) -> Self { + if value { + DeferredExpressionState::Deferred + } else { + DeferredExpressionState::None + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RichCompareOperator { + Eq, + Ne, + Gt, + Ge, + Lt, + Le, +} + +impl From for ast::CmpOp { + fn from(value: RichCompareOperator) -> Self { + match value { + RichCompareOperator::Eq => ast::CmpOp::Eq, + RichCompareOperator::Ne => ast::CmpOp::NotEq, + RichCompareOperator::Lt => ast::CmpOp::Lt, + RichCompareOperator::Le => ast::CmpOp::LtE, + RichCompareOperator::Gt => ast::CmpOp::Gt, + RichCompareOperator::Ge => ast::CmpOp::GtE, + } + } +} + +impl RichCompareOperator { + #[must_use] + const fn dunder(self) -> &'static str { + match self { + RichCompareOperator::Eq => "__eq__", + RichCompareOperator::Ne => "__ne__", + RichCompareOperator::Lt => "__lt__", + RichCompareOperator::Le => "__le__", + RichCompareOperator::Gt => "__gt__", + RichCompareOperator::Ge => "__ge__", + } + } + + #[must_use] + const fn reflect(self) -> Self { + match self { + RichCompareOperator::Eq => RichCompareOperator::Eq, + RichCompareOperator::Ne => RichCompareOperator::Ne, + RichCompareOperator::Lt => RichCompareOperator::Gt, + RichCompareOperator::Le => RichCompareOperator::Ge, + RichCompareOperator::Gt => RichCompareOperator::Lt, + RichCompareOperator::Ge => RichCompareOperator::Le, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MembershipTestCompareOperator { + In, + NotIn, +} + +impl From for ast::CmpOp { + fn from(value: MembershipTestCompareOperator) -> Self { + match value { + MembershipTestCompareOperator::In => ast::CmpOp::In, + MembershipTestCompareOperator::NotIn => ast::CmpOp::NotIn, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CompareUnsupportedError<'db> { + op: ast::CmpOp, + left_ty: Type<'db>, + right_ty: Type<'db>, +} + +fn format_import_from_module(level: u32, module: Option<&str>) -> String { + format!( + "{}{}", + ".".repeat(level as usize), + module.unwrap_or_default() + ) +} + +/// 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. +/// +/// If the formatted string contains an expression (with a representation unknown at compile time), +/// infers an instance of `builtins.str`. +#[derive(Debug)] +struct StringPartsCollector { + concatenated: Option, + expression: bool, +} + +impl StringPartsCollector { + fn new() -> Self { + Self { + concatenated: Some(String::new()), + expression: false, + } + } + + fn push_str(&mut self, literal: &str) { + if let Some(mut concatenated) = self.concatenated.take() { + if concatenated.len().saturating_add(literal.len()) + <= TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + { + concatenated.push_str(literal); + self.concatenated = Some(concatenated); + } else { + self.concatenated = None; + } + } + } + + fn add_expression(&mut self) { + self.concatenated = None; + self.expression = true; + } + + fn string_type(self, db: &dyn Db) -> Type { + if self.expression { + KnownClass::Str.to_instance(db) + } else if let Some(concatenated) = self.concatenated { + Type::string_literal(db, &concatenated) + } else { + Type::LiteralString + } + } +} + +fn contains_string_literal(expr: &ast::Expr) -> bool { + struct ContainsStringLiteral(bool); + + impl<'a> Visitor<'a> for ContainsStringLiteral { + fn visit_expr(&mut self, expr: &'a ast::Expr) { + self.0 |= matches!(expr, ast::Expr::StringLiteral(_)); + walk_expr(self, expr); + } + } + + let mut visitor = ContainsStringLiteral(false); + visitor.visit_expr(expr); + visitor.0 +} + +#[cfg(test)] +mod tests { + use crate::db::tests::{TestDb, setup_db}; + use crate::place::{global_symbol, symbol}; + use crate::semantic_index::definition::Definition; + use crate::semantic_index::place::FileScopeId; + use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map}; + use crate::types::check_types; + use ruff_db::diagnostic::Diagnostic; + use ruff_db::files::{File, system_path_to_file}; + use ruff_db::system::DbWithWritableSystem as _; + use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; + + use super::*; + + #[track_caller] + fn get_symbol<'db>( + db: &'db TestDb, + file_name: &str, + scopes: &[&str], + symbol_name: &str, + ) -> Place<'db> { + let file = system_path_to_file(db, file_name).expect("file to exist"); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let mut file_scope_id = FileScopeId::global(); + let mut scope = file_scope_id.to_scope_id(db, file); + for expected_scope_name in scopes { + file_scope_id = index + .child_scopes(file_scope_id) + .next() + .unwrap_or_else(|| panic!("scope of {expected_scope_name}")) + .0; + scope = file_scope_id.to_scope_id(db, file); + assert_eq!(scope.name(db, &module), *expected_scope_name); + } + + symbol(db, scope, symbol_name, ConsideredDefinitions::EndOfScope).place + } + + #[track_caller] + fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { + let messages: Vec<&str> = diagnostics + .iter() + .map(Diagnostic::primary_message) + .collect(); + assert_eq!(&messages, expected); + } + + #[track_caller] + fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { + let file = system_path_to_file(db, filename).unwrap(); + let diagnostics = check_types(db, file); + + assert_diagnostic_messages(&diagnostics, expected); + } + + #[test] + fn not_literal_string() -> anyhow::Result<()> { + let mut db = setup_db(); + let content = format!( + r#" + from typing_extensions import Literal, assert_type + + assert_type(not "{y}", bool) + assert_type(not 10*"{y}", bool) + assert_type(not "{y}"*10, bool) + assert_type(not 0*"{y}", Literal[True]) + assert_type(not (-100)*"{y}", Literal[True]) + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + ); + db.write_dedented("src/a.py", &content)?; + + assert_file_diagnostics(&db, "src/a.py", &[]); + + Ok(()) + } + + #[test] + fn multiplied_string() -> anyhow::Result<()> { + let mut db = setup_db(); + let content = format!( + r#" + from typing_extensions import Literal, LiteralString, assert_type + + assert_type(2 * "hello", Literal["hellohello"]) + assert_type("goodbye" * 3, Literal["goodbyegoodbyegoodbye"]) + assert_type("a" * {y}, Literal["{a_repeated}"]) + assert_type({z} * "b", LiteralString) + assert_type(0 * "hello", Literal[""]) + assert_type(-3 * "hello", Literal[""]) + "#, + y = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE, + z = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1, + a_repeated = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE), + ); + db.write_dedented("src/a.py", &content)?; + + assert_file_diagnostics(&db, "src/a.py", &[]); + + Ok(()) + } + + #[test] + fn multiplied_literal_string() -> anyhow::Result<()> { + let mut db = setup_db(); + let content = format!( + r#" + from typing_extensions import Literal, LiteralString, assert_type + + assert_type("{y}", LiteralString) + assert_type(10*"{y}", LiteralString) + assert_type("{y}"*10, LiteralString) + assert_type(0*"{y}", Literal[""]) + assert_type((-100)*"{y}", Literal[""]) + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + ); + db.write_dedented("src/a.py", &content)?; + + assert_file_diagnostics(&db, "src/a.py", &[]); + + Ok(()) + } + + #[test] + fn truncated_string_literals_become_literal_string() -> anyhow::Result<()> { + let mut db = setup_db(); + let content = format!( + r#" + from typing_extensions import LiteralString, assert_type + + assert_type("{y}", LiteralString) + assert_type("a" + "{z}", LiteralString) + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + z = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE), + ); + db.write_dedented("src/a.py", &content)?; + + assert_file_diagnostics(&db, "src/a.py", &[]); + + Ok(()) + } + + #[test] + fn adding_string_literals_and_literal_string() -> anyhow::Result<()> { + let mut db = setup_db(); + let content = format!( + r#" + from typing_extensions import LiteralString, assert_type + + assert_type("{y}", LiteralString) + assert_type("{y}" + "a", LiteralString) + assert_type("a" + "{y}", LiteralString) + assert_type("{y}" + "{y}", LiteralString) + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + ); + db.write_dedented("src/a.py", &content)?; + + assert_file_diagnostics(&db, "src/a.py", &[]); + + Ok(()) + } + + #[test] + fn pep695_type_params() { + let mut db = setup_db(); + + db.write_dedented( + "src/a.py", + " + def f[T, U: A, V: (A, B), W = A, X: A = A1, Y: (int,)](): + pass + + class A: ... + class B: ... + class A1(A): ... + ", + ) + .unwrap(); + + let check_typevar = |var: &'static str, + upper_bound: Option<&'static str>, + constraints: Option<&[&'static str]>, + default: Option<&'static str>| { + let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type(); + assert_eq!(var_ty.display(&db).to_string(), "typing.TypeVar"); + + let expected_name_ty = format!(r#"Literal["{var}"]"#); + let name_ty = var_ty.member(&db, "__name__").place.expect_type(); + assert_eq!(name_ty.display(&db).to_string(), expected_name_ty); + + let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = var_ty else { + panic!("expected TypeVar"); + }; + + assert_eq!( + typevar + .upper_bound(&db) + .map(|ty| ty.display(&db).to_string()), + upper_bound.map(std::borrow::ToOwned::to_owned) + ); + assert_eq!( + typevar.constraints(&db).map(|tys| tys + .iter() + .map(|ty| ty.display(&db).to_string()) + .collect::>()), + constraints.map(|strings| strings + .iter() + .map(std::string::ToString::to_string) + .collect::>()) + ); + assert_eq!( + typevar + .default_ty(&db) + .map(|ty| ty.display(&db).to_string()), + default.map(std::borrow::ToOwned::to_owned) + ); + }; + + check_typevar("T", None, None, None); + check_typevar("U", Some("A"), None, None); + check_typevar("V", None, Some(&["A", "B"]), None); + check_typevar("W", None, None, Some("A")); + check_typevar("X", Some("A"), None, Some("A1")); + + // a typevar with less than two constraints is treated as unconstrained + check_typevar("Y", None, None, None); + } + + /// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing + /// reachability-constraint checks in that scope. + #[test] + fn unbound_symbol_no_reachability_constraint_check() { + let mut db = setup_db(); + + // If the bug we are testing for is not fixed, what happens is that when inferring the + // `flag: bool = True` definitions, we look up `bool` as a deferred name (thus from end of + // scope), and because of the early return its "unbound" binding has a reachability + // constraint of `~flag`, which we evaluate, meaning we have to evaluate the definition of + // `flag` -- and we are in a cycle. With the fix, we short-circuit evaluating reachability + // constraints on "unbound" if a symbol is otherwise not bound. + db.write_dedented( + "src/a.py", + " + from __future__ import annotations + + def f(): + flag: bool = True + if flag: + return True + ", + ) + .unwrap(); + + db.clear_salsa_events(); + assert_file_diagnostics(&db, "src/a.py", &[]); + let events = db.take_salsa_events(); + let cycles = salsa::attach(&db, || { + events + .iter() + .filter_map(|event| { + if let salsa::EventKind::WillIterateCycle { database_key, .. } = event.kind { + Some(format!("{database_key:?}")) + } else { + None + } + }) + .collect::>() + }); + let expected: Vec = vec![]; + assert_eq!(cycles, expected); + } + + // Incremental inference tests + #[track_caller] + fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> { + let scope = global_scope(db, file); + use_def_map(db, scope) + .end_of_scope_bindings(place_table(db, scope).place_id_by_name(name).unwrap()) + .find_map(|b| b.binding.definition()) + .expect("no binding found") + } + + #[test] + fn dependency_public_symbol_type_change() -> anyhow::Result<()> { + let mut db = setup_db(); + + db.write_files([ + ("/src/a.py", "from foo import x"), + ("/src/foo.py", "x: int = 10\ndef foo(): ..."), + ])?; + + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + let x_ty = global_symbol(&db, a, "x").place.expect_type(); + + assert_eq!(x_ty.display(&db).to_string(), "int"); + + // Change `x` to a different value + db.write_file("/src/foo.py", "x: bool = True\ndef foo(): ...")?; + + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + + let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); + + assert_eq!(x_ty_2.display(&db).to_string(), "bool"); + + Ok(()) + } + + #[test] + fn dependency_internal_symbol_change() -> anyhow::Result<()> { + let mut db = setup_db(); + + db.write_files([ + ("/src/a.py", "from foo import x"), + ("/src/foo.py", "x: int = 10\ndef foo(): y = 1"), + ])?; + + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + let x_ty = global_symbol(&db, a, "x").place.expect_type(); + + assert_eq!(x_ty.display(&db).to_string(), "int"); + + db.write_file("/src/foo.py", "x: int = 10\ndef foo(): pass")?; + + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + + db.clear_salsa_events(); + + let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); + + assert_eq!(x_ty_2.display(&db).to_string(), "int"); + + let events = db.take_salsa_events(); + + assert_function_query_was_not_run( + &db, + infer_definition_types, + first_public_binding(&db, a, "x"), + &events, + ); + + Ok(()) + } + + #[test] + fn dependency_unrelated_symbol() -> anyhow::Result<()> { + let mut db = setup_db(); + + db.write_files([ + ("/src/a.py", "from foo import x"), + ("/src/foo.py", "x: int = 10\ny: bool = True"), + ])?; + + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + let x_ty = global_symbol(&db, a, "x").place.expect_type(); + + assert_eq!(x_ty.display(&db).to_string(), "int"); + + db.write_file("/src/foo.py", "x: int = 10\ny: bool = False")?; + + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + + db.clear_salsa_events(); + + let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); + + assert_eq!(x_ty_2.display(&db).to_string(), "int"); + + let events = db.take_salsa_events(); + + assert_function_query_was_not_run( + &db, + infer_definition_types, + first_public_binding(&db, a, "x"), + &events, + ); + Ok(()) + } + + #[test] + fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { + fn x_rhs_expression(db: &TestDb) -> Expression<'_> { + let file_main = system_path_to_file(db, "/src/main.py").unwrap(); + let ast = parsed_module(db, file_main).load(db); + // Get the second statement in `main.py` (x = …) and extract the expression + // node on the right-hand side: + let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; + + let index = semantic_index(db, file_main); + index.expression(x_rhs_node.as_ref()) + } + + let mut db = setup_db(); + + db.write_dedented( + "/src/mod.py", + r#" + class C: + def f(self): + self.attr: int | None = None + "#, + )?; + db.write_dedented( + "/src/main.py", + r#" + from mod import C + x = C().attr + "#, + )?; + + let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None"); + + // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred + db.write_dedented( + "/src/mod.py", + r#" + class C: + def f(self): + self.attr: str | None = None + "#, + )?; + + let events = { + db.clear_salsa_events(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); + db.take_salsa_events() + }; + assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events); + + // Add a comment; this should not trigger the type of `x` to be re-inferred + db.write_dedented( + "/src/mod.py", + r#" + class C: + def f(self): + # a comment! + self.attr: str | None = None + "#, + )?; + + let events = { + db.clear_salsa_events(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); + db.take_salsa_events() + }; + + assert_function_query_was_not_run( + &db, + infer_expression_types, + x_rhs_expression(&db), + &events, + ); + + Ok(()) + } + + /// This test verifies that changing a class's declaration in a non-meaningful way (e.g. by adding a comment) + /// doesn't trigger type inference for expressions that depend on the class's members. + #[test] + fn dependency_own_instance_member() -> anyhow::Result<()> { + fn x_rhs_expression(db: &TestDb) -> Expression<'_> { + let file_main = system_path_to_file(db, "/src/main.py").unwrap(); + let ast = parsed_module(db, file_main).load(db); + // Get the second statement in `main.py` (x = …) and extract the expression + // node on the right-hand side: + let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; + + let index = semantic_index(db, file_main); + index.expression(x_rhs_node.as_ref()) + } + + let mut db = setup_db(); + + db.write_dedented( + "/src/mod.py", + r#" + class C: + if random.choice([True, False]): + attr: int = 42 + else: + attr: None = None + "#, + )?; + db.write_dedented( + "/src/main.py", + r#" + from mod import C + x = C().attr + "#, + )?; + + let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None"); + + // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred + db.write_dedented( + "/src/mod.py", + r#" + class C: + if random.choice([True, False]): + attr: str = "42" + else: + attr: None = None + "#, + )?; + + let events = { + db.clear_salsa_events(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); + db.take_salsa_events() + }; + assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events); + + // Add a comment; this should not trigger the type of `x` to be re-inferred + db.write_dedented( + "/src/mod.py", + r#" + class C: + # comment + if random.choice([True, False]): + attr: str = "42" + else: + attr: None = None + "#, + )?; + + let events = { + db.clear_salsa_events(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None"); + db.take_salsa_events() + }; + + assert_function_query_was_not_run( + &db, + infer_expression_types, + x_rhs_expression(&db), + &events, + ); + + Ok(()) + } + + #[test] + fn dependency_implicit_class_member() -> anyhow::Result<()> { + fn x_rhs_expression(db: &TestDb) -> Expression<'_> { + let file_main = system_path_to_file(db, "/src/main.py").unwrap(); + let ast = parsed_module(db, file_main).load(db); + // Get the third statement in `main.py` (x = …) and extract the expression + // node on the right-hand side: + let x_rhs_node = &ast.syntax().body[2].as_assign_stmt().unwrap().value; + + let index = semantic_index(db, file_main); + index.expression(x_rhs_node.as_ref()) + } + + let mut db = setup_db(); + + db.write_dedented( + "/src/mod.py", + r#" + class C: + def __init__(self): + self.instance_attr: str = "24" + + @classmethod + def method(cls): + cls.class_attr: int = 42 + "#, + )?; + db.write_dedented( + "/src/main.py", + r#" + from mod import C + C.method() + x = C().class_attr + "#, + )?; + + let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int"); + + // Change the type of `class_attr` to `str`; this should trigger the type of `x` to be re-inferred + db.write_dedented( + "/src/mod.py", + r#" + class C: + def __init__(self): + self.instance_attr: str = "24" + + @classmethod + def method(cls): + cls.class_attr: str = "42" + "#, + )?; + + let events = { + db.clear_salsa_events(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str"); + db.take_salsa_events() + }; + assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events); + + // Add a comment; this should not trigger the type of `x` to be re-inferred + db.write_dedented( + "/src/mod.py", + r#" + class C: + def __init__(self): + self.instance_attr: str = "24" + + @classmethod + def method(cls): + # comment + cls.class_attr: str = "42" + "#, + )?; + + let events = { + db.clear_salsa_events(); + let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); + assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str"); + db.take_salsa_events() + }; + + assert_function_query_was_not_run( + &db, + infer_expression_types, + x_rhs_expression(&db), + &events, + ); + + Ok(()) + } +} diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs new file mode 100644 index 0000000000000..c92885adeb60e --- /dev/null +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -0,0 +1,426 @@ +//! Instance types: both nominal and structural. + +use std::marker::PhantomData; + +use super::protocol_class::ProtocolInterface; +use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance}; +use crate::place::PlaceAndQualifiers; +use crate::types::cyclic::PairVisitor; +use crate::types::protocol_class::walk_protocol_interface; +use crate::types::tuple::TupleType; +use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance}; +use crate::{Db, FxOrderSet}; + +pub(super) use synthesized_protocol::SynthesizedProtocolType; + +impl<'db> Type<'db> { + pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { + match (class, class.known(db)) { + (_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any), + (ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => { + TupleType::homogeneous(db, Type::unknown()) + } + (ClassType::Generic(alias), Some(KnownClass::Tuple)) => { + Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db))) + } + _ if class.class_literal(db).0.is_protocol(db) => { + Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) + } + _ => Self::NominalInstance(NominalInstanceType::from_class(class)), + } + } + + pub(crate) const fn into_nominal_instance(self) -> Option> { + match self { + Type::NominalInstance(instance_type) => Some(instance_type), + _ => None, + } + } + + /// Synthesize a protocol instance type with a given set of read-only property members. + pub(super) fn protocol_with_readonly_members<'a, M>(db: &'db dyn Db, members: M) -> Self + where + M: IntoIterator)>, + { + Self::ProtocolInstance(ProtocolInstanceType::synthesized( + SynthesizedProtocolType::new( + db, + ProtocolInterface::with_property_members(db, members), + &mut TypeTransformer::default(), + ), + )) + } + + /// Return `true` if `self` conforms to the interface described by `protocol`. + pub(super) fn satisfies_protocol( + self, + db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, + relation: TypeRelation, + ) -> bool { + protocol + .inner + .interface(db) + .members(db) + .all(|member| member.is_satisfied_by(db, self, relation)) + } +} + +/// A type representing the set of runtime objects which are instances of a certain nominal class. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] +pub struct NominalInstanceType<'db> { + pub(super) class: ClassType<'db>, + + // Keep this field private, so that the only way of constructing `NominalInstanceType` instances + // is through the `Type::instance` constructor function. + _phantom: PhantomData<()>, +} + +pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + nominal: NominalInstanceType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, nominal.class.into()); +} + +impl<'db> NominalInstanceType<'db> { + // Keep this method private, so that the only way of constructing `NominalInstanceType` + // instances is through the `Type::instance` constructor function. + fn from_class(class: ClassType<'db>) -> Self { + Self { + class, + _phantom: PhantomData, + } + } + + pub(super) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self::from_class(self.class.normalized_impl(db, visitor)) + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::from_class(self.class.materialize(db, variance)) + } + + pub(super) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + self.class.has_relation_to(db, other.class, relation) + } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.class.is_equivalent_to(db, other.class) + } + + pub(super) fn is_disjoint_from_impl(self, db: &'db dyn Db, other: Self) -> bool { + !self.class.could_coexist_in_mro_with(db, other.class) + } + + pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool { + self.class.known(db).is_some_and(KnownClass::is_singleton) + } + + pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { + self.class + .known(db) + .is_some_and(KnownClass::is_single_valued) + } + + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + SubclassOfType::from(db, self.class) + } + + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self::from_class(self.class.apply_type_mapping(db, type_mapping)) + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.class.find_legacy_typevars(db, typevars); + } +} + +impl<'db> From> for Type<'db> { + fn from(value: NominalInstanceType<'db>) -> Self { + Self::NominalInstance(value) + } +} + +/// A `ProtocolInstanceType` represents the set of all possible runtime objects +/// that conform to the interface described by a certain protocol. +#[derive( + Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, +)] +pub struct ProtocolInstanceType<'db> { + pub(super) inner: Protocol<'db>, + + // Keep the inner field here private, + // so that the only way of constructing `ProtocolInstanceType` instances + // is through the `Type::instance` constructor function. + _phantom: PhantomData<()>, +} + +pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, + visitor: &mut V, +) { + walk_protocol_interface(db, protocol.inner.interface(db), visitor); +} + +impl<'db> ProtocolInstanceType<'db> { + // Keep this method private, so that the only way of constructing `ProtocolInstanceType` + // instances is through the `Type::instance` constructor function. + fn from_class(class: ClassType<'db>) -> Self { + Self { + inner: Protocol::FromClass(class), + _phantom: PhantomData, + } + } + + // Keep this method private, so that the only way of constructing `ProtocolInstanceType` + // instances is through the `Type::instance` constructor function. + fn synthesized(synthesized: SynthesizedProtocolType<'db>) -> Self { + Self { + inner: Protocol::Synthesized(synthesized), + _phantom: PhantomData, + } + } + + /// Return the meta-type of this protocol-instance type. + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + match self.inner { + Protocol::FromClass(class) => SubclassOfType::from(db, class), + + // TODO: we can and should do better here. + // + // This is supported by mypy, and should be supported by us as well. + // We'll need to come up with a better solution for the meta-type of + // synthesized protocols to solve this: + // + // ```py + // from typing import Callable + // + // def foo(x: Callable[[], int]) -> None: + // reveal_type(type(x)) # mypy: "type[def (builtins.int) -> builtins.str]" + // reveal_type(type(x).__call__) # mypy: "def (*args: Any, **kwds: Any) -> Any" + // ``` + Protocol::Synthesized(_) => KnownClass::Type.to_instance(db), + } + } + + /// Return a "normalized" version of this `Protocol` type. + /// + /// See [`Type::normalized`] for more details. + pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> { + let mut visitor = TypeTransformer::default(); + self.normalized_impl(db, &mut visitor) + } + + /// Return a "normalized" version of this `Protocol` type. + /// + /// See [`Type::normalized`] for more details. + pub(super) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Type<'db> { + let object = KnownClass::Object.to_instance(db); + if object.satisfies_protocol(db, self, TypeRelation::Subtyping) { + return object; + } + match self.inner { + Protocol::FromClass(_) => Type::ProtocolInstance(Self::synthesized( + SynthesizedProtocolType::new(db, self.inner.interface(db), visitor), + )), + Protocol::Synthesized(_) => Type::ProtocolInstance(self), + } + } + + /// Return `true` if this protocol type has the given type relation to the protocol `other`. + /// + /// TODO: consider the types of the members as well as their existence + pub(super) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + _relation: TypeRelation, + ) -> bool { + other + .inner + .interface(db) + .is_sub_interface_of(db, self.inner.interface(db)) + } + + /// Return `true` if this protocol type is equivalent to the protocol `other`. + /// + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + if self == other { + return true; + } + let self_normalized = self.normalized(db); + if self_normalized == Type::ProtocolInstance(other) { + return true; + } + self_normalized == other.normalized(db) + } + + /// Return `true` if this protocol type is disjoint from the protocol `other`. + /// + /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` + /// have a member with the same name but disjoint types + #[expect(clippy::unused_self)] + pub(super) fn is_disjoint_from_impl( + self, + _db: &'db dyn Db, + _other: Self, + _visitor: &mut PairVisitor<'db>, + ) -> bool { + false + } + + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + match self.inner { + Protocol::FromClass(class) => class.instance_member(db, name), + Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name), + } + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + match self.inner { + // TODO: This should also materialize via `class.materialize(db, variance)` + Protocol::FromClass(class) => Self::from_class(class), + Protocol::Synthesized(synthesized) => { + Self::synthesized(synthesized.materialize(db, variance)) + } + } + } + + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + match self.inner { + Protocol::FromClass(class) => { + Self::from_class(class.apply_type_mapping(db, type_mapping)) + } + Protocol::Synthesized(synthesized) => { + Self::synthesized(synthesized.apply_type_mapping(db, type_mapping)) + } + } + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self.inner { + Protocol::FromClass(class) => { + class.find_legacy_typevars(db, typevars); + } + Protocol::Synthesized(synthesized) => { + synthesized.find_legacy_typevars(db, typevars); + } + } + } + + pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + self.inner.interface(db) + } +} + +/// An enumeration of the two kinds of protocol types: those that originate from a class +/// definition in source code, and those that are synthesized from a set of members. +#[derive( + Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, +)] +pub(super) enum Protocol<'db> { + FromClass(ClassType<'db>), + Synthesized(SynthesizedProtocolType<'db>), +} + +impl<'db> Protocol<'db> { + /// Return the members of this protocol type + fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + match self { + Self::FromClass(class) => class + .class_literal(db) + .0 + .into_protocol_class(db) + .expect("Protocol class literal should be a protocol class") + .interface(db), + Self::Synthesized(synthesized) => synthesized.interface(), + } + } +} + +mod synthesized_protocol { + use crate::types::protocol_class::ProtocolInterface; + use crate::types::{TypeMapping, TypeTransformer, TypeVarInstance, TypeVarVariance}; + use crate::{Db, FxOrderSet}; + + /// A "synthesized" protocol type that is dissociated from a class definition in source code. + /// + /// Two synthesized protocol types with the same members will share the same Salsa ID, + /// making them easy to compare for equivalence. A synthesized protocol type is therefore + /// returned by [`super::ProtocolInstanceType::normalized`] so that two protocols with the same members + /// will be understood as equivalent even in the context of differently ordered unions or intersections. + /// + /// The constructor method of this type maintains the invariant that a synthesized protocol type + /// is always constructed from a *normalized* protocol interface. + #[derive( + Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize, + )] + pub(in crate::types) struct SynthesizedProtocolType<'db>(ProtocolInterface<'db>); + + impl<'db> SynthesizedProtocolType<'db> { + pub(super) fn new( + db: &'db dyn Db, + interface: ProtocolInterface<'db>, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self(interface.normalized_impl(db, visitor)) + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self(self.0.materialize(db, variance)) + } + + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self(self.0.specialized_and_normalized(db, type_mapping)) + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.0.find_legacy_typevars(db, typevars); + } + + pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> { + self.0 + } + } +} diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs new file mode 100644 index 0000000000000..632a513b7beed --- /dev/null +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -0,0 +1,536 @@ +use std::collections::VecDeque; +use std::ops::Deref; + +use indexmap::IndexMap; +use rustc_hash::FxBuildHasher; + +use crate::Db; +use crate::types::class_base::ClassBase; +use crate::types::generics::Specialization; +use crate::types::{ClassLiteral, ClassType, KnownInstanceType, SpecialFormType, Type}; + +/// The inferred method resolution order of a given class. +/// +/// An MRO cannot contain non-specialized generic classes. (This is why [`ClassBase`] contains a +/// [`ClassType`], not a [`ClassLiteral`].) Any generic classes in a base class list are always +/// specialized — either because the class is explicitly specialized if there is a subscript +/// expression, or because we create the default specialization if there isn't. +/// +/// The MRO of a non-specialized generic class can contain generic classes that are specialized +/// with a typevar from the inheriting class. When the inheriting class is specialized, the MRO of +/// the resulting generic alias will substitute those type variables accordingly. For instance, in +/// the following example, the MRO of `D[int]` includes `C[int]`, and the MRO of `D[U]` includes +/// `C[U]` (which is a generic alias, not a non-specialized generic class): +/// +/// ```py +/// class C[T]: ... +/// class D[U](C[U]): ... +/// ``` +/// +/// See [`ClassType::iter_mro`] for more details. +#[derive(PartialEq, Eq, Clone, Debug, salsa::Update, get_size2::GetSize)] +pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>); + +impl<'db> Mro<'db> { + /// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of + /// base classes in the class definition, this operation is performed on a [class + /// literal][ClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a + /// class type, but this is done by first getting the MRO of the underlying class literal, and + /// specializing each base class as needed if the class type is a generic alias.) + /// + /// In the event that a possible list of bases would (or could) lead to a `TypeError` being + /// raised at runtime due to an unresolvable MRO, we infer the MRO of the class as being `[, Unknown, object]`. This seems most likely to reduce the possibility of + /// cascading errors elsewhere. (For a generic class, the first entry in this fallback MRO uses + /// the default specialization of the class's type variables.) + /// + /// (We emit a diagnostic warning about the runtime `TypeError` in + /// [`super::infer::TypeInferenceBuilder::infer_region_scope`].) + pub(super) fn of_class( + db: &'db dyn Db, + class_literal: ClassLiteral<'db>, + specialization: Option>, + ) -> Result> { + let class = class_literal.apply_optional_specialization(db, specialization); + Self::of_class_impl(db, class, class_literal.explicit_bases(db), specialization) + .map_err(|err| err.into_mro_error(db, class)) + } + + pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { + Self::from([ + ClassBase::Class(class), + ClassBase::unknown(), + ClassBase::object(db), + ]) + } + + fn of_class_impl( + db: &'db dyn Db, + class: ClassType<'db>, + original_bases: &[Type<'db>], + specialization: Option>, + ) -> Result> { + /// Possibly add `Generic` to the resolved bases list. + /// + /// This function is called in two cases: + /// - If we encounter a subscripted `Generic` in the original bases list + /// (`Generic[T]` or similar) + /// - If the class has PEP-695 type parameters, + /// `Generic` is [implicitly appended] to the bases list at runtime + /// + /// Whether or not `Generic` is added to the bases list depends on: + /// - Whether `Protocol` is present in the original bases list + /// - Whether any of the bases yet to be visited in the original bases list + /// is a generic alias (which would therefore have `Generic` in its MRO) + /// + /// This function emulates the behavior of `typing._GenericAlias.__mro_entries__` at + /// . + /// + /// [implicitly inherits]: https://docs.python.org/3/reference/compound_stmts.html#generic-classes + fn maybe_add_generic<'db>( + resolved_bases: &mut Vec>, + original_bases: &[Type<'db>], + remaining_bases: &[Type<'db>], + ) { + if original_bases.contains(&Type::SpecialForm(SpecialFormType::Protocol)) { + return; + } + if remaining_bases.iter().any(Type::is_generic_alias) { + return; + } + resolved_bases.push(ClassBase::Generic); + } + + match original_bases { + // `builtins.object` is the special case: + // the only class in Python that has an MRO with length <2 + [] if class.is_object(db) => Ok(Self::from([ + // object is not generic, so the default specialization should be a no-op + ClassBase::Class(class), + ])), + + // All other classes in Python have an MRO with length >=2. + // Even if a class has no explicit base classes, + // it will implicitly inherit from `object` at runtime; + // `object` will appear in the class's `__bases__` list and `__mro__`: + // + // ```pycon + // >>> class Foo: ... + // ... + // >>> Foo.__bases__ + // (,) + // >>> Foo.__mro__ + // (, ) + // ``` + [] => { + // e.g. `class Foo[T]: ...` implicitly has `Generic` inserted into its bases + if class.has_pep_695_type_params(db) { + Ok(Self::from([ + ClassBase::Class(class), + ClassBase::Generic, + ClassBase::object(db), + ])) + } else { + Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)])) + } + } + + // Fast path for a class that has only a single explicit base. + // + // This *could* theoretically be handled by the final branch below, + // but it's a common case (i.e., worth optimizing for), + // and the `c3_merge` function requires lots of allocations. + [single_base] + if !class.has_pep_695_type_params(db) + && !matches!( + single_base, + Type::GenericAlias(_) + | Type::KnownInstance( + KnownInstanceType::SubscriptedGeneric(_) + | KnownInstanceType::SubscriptedProtocol(_) + ) + ) => + { + ClassBase::try_from_type(db, *single_base).map_or_else( + || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))), + |single_base| { + if single_base.has_cyclic_mro(db) { + Err(MroErrorKind::InheritanceCycle) + } else { + Ok(std::iter::once(ClassBase::Class(class)) + .chain(single_base.mro(db, specialization)) + .collect()) + } + }, + ) + } + + // The class has multiple explicit bases. + // + // We'll fallback to a full implementation of the C3-merge algorithm to determine + // what MRO Python will give this class at runtime + // (if an MRO is indeed resolvable at all!) + _ => { + let mut resolved_bases = vec![]; + let mut invalid_bases = vec![]; + + for (i, base) in original_bases.iter().enumerate() { + // Note that we emit a diagnostic for inheriting from bare (unsubscripted) `Generic` elsewhere + // (see `infer::TypeInferenceBuilder::check_class_definitions`), + // which is why we only care about `KnownInstanceType::Generic(Some(_))`, + // not `KnownInstanceType::Generic(None)`. + if let Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)) = base { + maybe_add_generic( + &mut resolved_bases, + original_bases, + &original_bases[i + 1..], + ); + } else { + match ClassBase::try_from_type(db, *base) { + Some(valid_base) => resolved_bases.push(valid_base), + None => invalid_bases.push((i, *base)), + } + } + } + + if !invalid_bases.is_empty() { + return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())); + } + + // `Generic` is implicitly added to the bases list of a class that has PEP-695 type parameters + // (documented at https://docs.python.org/3/reference/compound_stmts.html#generic-classes) + if class.has_pep_695_type_params(db) { + maybe_add_generic(&mut resolved_bases, original_bases, &[]); + } + + let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])]; + for base in &resolved_bases { + if base.has_cyclic_mro(db) { + return Err(MroErrorKind::InheritanceCycle); + } + seqs.push(base.mro(db, specialization).collect()); + } + seqs.push( + resolved_bases + .iter() + .map(|base| base.apply_optional_specialization(db, specialization)) + .collect(), + ); + + if let Some(mro) = c3_merge(seqs) { + return Ok(mro); + } + + // We now know that the MRO is unresolvable through the C3-merge algorithm. + // The rest of this function is dedicated to figuring out the best error message + // to report to the user. + + if class.has_pep_695_type_params(db) + && original_bases.iter().any(|base| { + matches!( + base, + Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)) + | Type::SpecialForm(SpecialFormType::Generic) + ) + }) + { + return Err(MroErrorKind::Pep695ClassWithGenericInheritance); + } + + let mut duplicate_dynamic_bases = false; + + let duplicate_bases: Vec> = { + let mut base_to_indices: IndexMap, Vec, FxBuildHasher> = + IndexMap::default(); + + // We need to iterate over `original_bases` here rather than `resolved_bases` + // so that we get the correct index of the duplicate bases if there were any + // (`resolved_bases` may be a longer list than `original_bases`!). However, we + // need to use a `ClassBase` rather than a `Type` as the key type for the + // `base_to_indices` map so that a class such as + // `class Foo(Protocol[T], Protocol): ...` correctly causes us to emit a + // `duplicate-base` diagnostic (matching the runtime behaviour) rather than an + // `inconsistent-mro` diagnostic (which would be accurate -- but not nearly as + // precise!). + for (index, base) in original_bases.iter().enumerate() { + let Some(base) = ClassBase::try_from_type(db, *base) else { + continue; + }; + base_to_indices.entry(base).or_default().push(index); + } + + let mut errors = vec![]; + + for (base, indices) in base_to_indices { + let Some((first_index, later_indices)) = indices.split_first() else { + continue; + }; + if later_indices.is_empty() { + continue; + } + match base { + ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => { + errors.push(DuplicateBaseError { + duplicate_base: base, + first_index: *first_index, + later_indices: later_indices.iter().copied().collect(), + }); + } + ClassBase::Dynamic(_) => duplicate_dynamic_bases = true, + } + } + + errors + }; + + if duplicate_bases.is_empty() { + if duplicate_dynamic_bases { + Ok(Mro::from_error(db, class)) + } else { + Err(MroErrorKind::UnresolvableMro { + bases_list: original_bases.iter().copied().collect(), + }) + } + } else { + Err(MroErrorKind::DuplicateBases( + duplicate_bases.into_boxed_slice(), + )) + } + } + } + } +} + +impl<'db, const N: usize> From<[ClassBase<'db>; N]> for Mro<'db> { + fn from(value: [ClassBase<'db>; N]) -> Self { + Self(Box::from(value)) + } +} + +impl<'db> From>> for Mro<'db> { + fn from(value: Vec>) -> Self { + Self(value.into_boxed_slice()) + } +} + +impl<'db> Deref for Mro<'db> { + type Target = [ClassBase<'db>]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'db> FromIterator> for Mro<'db> { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// Iterator that yields elements of a class's MRO. +/// +/// We avoid materialising the *full* MRO unless it is actually necessary: +/// - Materialising the full MRO is expensive +/// - We need to do it for every class in the code that we're checking, as we need to make sure +/// that there are no class definitions in the code we're checking that would cause an +/// exception to be raised at runtime. But the same does *not* necessarily apply for every class +/// in third-party and stdlib dependencies: we never emit diagnostics about non-first-party code. +/// - However, we *do* need to resolve attribute accesses on classes/instances from +/// third-party and stdlib dependencies. That requires iterating over the MRO of third-party/stdlib +/// classes, but not necessarily the *whole* MRO: often just the first element is enough. +/// Luckily we know that for any class `X`, the first element of `X`'s MRO will always be `X` itself. +/// We can therefore avoid resolving the full MRO for many third-party/stdlib classes while still +/// being faithful to the runtime semantics. +/// +/// Even for first-party code, where we will have to resolve the MRO for every class we encounter, +/// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the +/// Salsa-tracked [`ClassLiteral::try_mro`] method unless it's absolutely necessary. +pub(super) struct MroIterator<'db> { + db: &'db dyn Db, + + /// The class whose MRO we're iterating over + class: ClassLiteral<'db>, + + /// The specialization to apply to each MRO element, if any + specialization: Option>, + + /// Whether or not we've already yielded the first element of the MRO + first_element_yielded: bool, + + /// Iterator over all elements of the MRO except the first. + /// + /// The full MRO is expensive to materialize, so this field is `None` + /// unless we actually *need* to iterate past the first element of the MRO, + /// at which point it is lazily materialized. + subsequent_elements: Option>>, +} + +impl<'db> MroIterator<'db> { + pub(super) fn new( + db: &'db dyn Db, + class: ClassLiteral<'db>, + specialization: Option>, + ) -> Self { + Self { + db, + class, + specialization, + first_element_yielded: false, + subsequent_elements: None, + } + } + + /// Materialize the full MRO of the class. + /// Return an iterator over that MRO which skips the first element of the MRO. + fn full_mro_except_first_element(&mut self) -> impl Iterator> + '_ { + self.subsequent_elements + .get_or_insert_with(|| { + let mut full_mro_iter = match self.class.try_mro(self.db, self.specialization) { + Ok(mro) => mro.iter(), + Err(error) => error.fallback_mro().iter(), + }; + full_mro_iter.next(); + full_mro_iter + }) + .copied() + } +} + +impl<'db> Iterator for MroIterator<'db> { + type Item = ClassBase<'db>; + + fn next(&mut self) -> Option { + if !self.first_element_yielded { + self.first_element_yielded = true; + return Some(ClassBase::Class( + self.class + .apply_optional_specialization(self.db, self.specialization), + )); + } + self.full_mro_except_first_element().next() + } +} + +impl std::iter::FusedIterator for MroIterator<'_> {} + +#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) struct MroError<'db> { + kind: MroErrorKind<'db>, + fallback_mro: Mro<'db>, +} + +impl<'db> MroError<'db> { + /// Construct an MRO error of kind `InheritanceCycle`. + pub(super) fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self { + MroErrorKind::InheritanceCycle.into_mro_error(db, class) + } + + pub(super) fn is_cycle(&self) -> bool { + matches!(self.kind, MroErrorKind::InheritanceCycle) + } + + /// Return an [`MroErrorKind`] variant describing why we could not resolve the MRO for this class. + pub(super) fn reason(&self) -> &MroErrorKind<'db> { + &self.kind + } + + /// Return the fallback MRO we should infer for this class during type inference + /// (since accurate resolution of its "true" MRO was impossible) + pub(super) fn fallback_mro(&self) -> &Mro<'db> { + &self.fallback_mro + } +} + +/// Possible ways in which attempting to resolve the MRO of a class might fail. +#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) enum MroErrorKind<'db> { + /// The class inherits from one or more invalid bases. + /// + /// To avoid excessive complexity in our implementation, + /// we only permit classes to inherit from class-literal types, + /// `Todo`, `Unknown` or `Any`. Anything else results in us + /// emitting a diagnostic. + /// + /// This variant records the indices and types of class bases + /// that we deem to be invalid. The indices are the indices of nodes + /// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node. + /// Each index is the index of a node representing an invalid base. + InvalidBases(Box<[(usize, Type<'db>)]>), + + /// The class has one or more duplicate bases. + /// See [`DuplicateBaseError`] for more details. + DuplicateBases(Box<[DuplicateBaseError<'db>]>), + + /// The class uses PEP-695 parameters and also inherits from `Generic[]`. + Pep695ClassWithGenericInheritance, + + /// A cycle was encountered resolving the class' bases. + InheritanceCycle, + + /// The MRO is otherwise unresolvable through the C3-merge algorithm. + /// + /// See [`c3_merge`] for more details. + UnresolvableMro { bases_list: Box<[Type<'db>]> }, +} + +impl<'db> MroErrorKind<'db> { + pub(super) fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> MroError<'db> { + MroError { + kind: self, + fallback_mro: Mro::from_error(db, class), + } + } +} + +/// Error recording the fact that a class definition was found to have duplicate bases. +#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(super) struct DuplicateBaseError<'db> { + /// The base that is duplicated in the class's bases list. + pub(super) duplicate_base: ClassBase<'db>, + /// The index of the first occurrence of the base in the class's bases list. + pub(super) first_index: usize, + /// The indices of the base's later occurrences in the class's bases list. + pub(super) later_indices: Box<[usize]>, +} + +/// Implementation of the [C3-merge algorithm] for calculating a Python class's +/// [method resolution order]. +/// +/// [C3-merge algorithm]: https://docs.python.org/3/howto/mro.html#python-2-3-mro +/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order +fn c3_merge(mut sequences: Vec>) -> Option { + // Most MROs aren't that long... + let mut mro = Vec::with_capacity(8); + + loop { + sequences.retain(|sequence| !sequence.is_empty()); + + if sequences.is_empty() { + return Some(Mro::from(mro)); + } + + // If the candidate exists "deeper down" in the inheritance hierarchy, + // we should refrain from adding it to the MRO for now. Add the first candidate + // for which this does not hold true. If this holds true for all candidates, + // return `None`; it will be impossible to find a consistent MRO for the class + // with the given bases. + let mro_entry = sequences.iter().find_map(|outer_sequence| { + let candidate = outer_sequence[0]; + + let not_head = sequences + .iter() + .all(|sequence| sequence.iter().skip(1).all(|base| base != &candidate)); + + not_head.then_some(candidate) + })?; + + mro.push(mro_entry); + + // Make sure we don't try to add the candidate to the MRO twice: + for sequence in &mut sequences { + if sequence[0] == mro_entry { + sequence.pop_front(); + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs new file mode 100644 index 0000000000000..20a912c72d153 --- /dev/null +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -0,0 +1,981 @@ +use crate::Db; +use crate::semantic_index::expression::Expression; +use crate::semantic_index::place::{PlaceExpr, PlaceTable, ScopeId, ScopedPlaceId}; +use crate::semantic_index::place_table; +use crate::semantic_index::predicate::{ + CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, +}; +use crate::types::function::KnownFunction; +use crate::types::infer::infer_same_file_expression_type; +use crate::types::{ + ClassLiteral, ClassType, IntersectionBuilder, KnownClass, SubclassOfInner, SubclassOfType, + Truthiness, Type, TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, +}; + +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_python_stdlib::identifiers::is_identifier; + +use itertools::Itertools; +use ruff_python_ast as ast; +use ruff_python_ast::{BoolOp, ExprBoolOp}; +use rustc_hash::FxHashMap; +use std::collections::hash_map::Entry; + +use super::UnionType; + +/// Return the type constraint that `test` (if true) would place on `symbol`, if any. +/// +/// For example, if we have this code: +/// +/// ```python +/// y = 1 if flag else None +/// x = 1 if flag else None +/// if x is not None: +/// ... +/// ``` +/// +/// The `test` expression `x is not None` places the constraint "not None" on the definition of +/// `x`, so in that case we'd return `Some(Type::Intersection(negative=[Type::None]))`. +/// +/// But if we called this with the same `test` expression, but the `symbol` of `y`, no +/// constraint is applied to that symbol, so we'd just return `None`. +pub(crate) fn infer_narrowing_constraint<'db>( + db: &'db dyn Db, + predicate: Predicate<'db>, + place: ScopedPlaceId, +) -> Option> { + let constraints = match predicate.node { + PredicateNode::Expression(expression) => { + if predicate.is_positive { + all_narrowing_constraints_for_expression(db, expression) + } else { + all_negative_narrowing_constraints_for_expression(db, expression) + } + } + PredicateNode::Pattern(pattern) => { + if predicate.is_positive { + all_narrowing_constraints_for_pattern(db, pattern) + } else { + all_negative_narrowing_constraints_for_pattern(db, pattern) + } + } + PredicateNode::ReturnsNever(_) => return None, + PredicateNode::StarImportPlaceholder(_) => return None, + }; + if let Some(constraints) = constraints { + constraints.get(&place).copied() + } else { + None + } +} + +#[salsa::tracked(returns(as_ref), heap_size=get_size2::GetSize::get_heap_size)] +fn all_narrowing_constraints_for_pattern<'db>( + db: &'db dyn Db, + pattern: PatternPredicate<'db>, +) -> Option> { + let module = parsed_module(db, pattern.file(db)).load(db); + NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Pattern(pattern), true).finish() +} + +#[salsa::tracked( + returns(as_ref), + cycle_fn=constraints_for_expression_cycle_recover, + cycle_initial=constraints_for_expression_cycle_initial, + heap_size=get_size2::GetSize::get_heap_size, +)] +fn all_narrowing_constraints_for_expression<'db>( + db: &'db dyn Db, + expression: Expression<'db>, +) -> Option> { + let module = parsed_module(db, expression.file(db)).load(db); + NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Expression(expression), true) + .finish() +} + +#[salsa::tracked( + returns(as_ref), + cycle_fn=negative_constraints_for_expression_cycle_recover, + cycle_initial=negative_constraints_for_expression_cycle_initial, + heap_size=get_size2::GetSize::get_heap_size, +)] +fn all_negative_narrowing_constraints_for_expression<'db>( + db: &'db dyn Db, + expression: Expression<'db>, +) -> Option> { + let module = parsed_module(db, expression.file(db)).load(db); + NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Expression(expression), false) + .finish() +} + +#[salsa::tracked(returns(as_ref), heap_size=get_size2::GetSize::get_heap_size)] +fn all_negative_narrowing_constraints_for_pattern<'db>( + db: &'db dyn Db, + pattern: PatternPredicate<'db>, +) -> Option> { + let module = parsed_module(db, pattern.file(db)).load(db); + NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Pattern(pattern), false).finish() +} + +#[expect(clippy::ref_option)] +fn constraints_for_expression_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Option>, + _count: u32, + _expression: Expression<'db>, +) -> salsa::CycleRecoveryAction>> { + salsa::CycleRecoveryAction::Iterate +} + +fn constraints_for_expression_cycle_initial<'db>( + _db: &'db dyn Db, + _expression: Expression<'db>, +) -> Option> { + None +} + +#[expect(clippy::ref_option)] +fn negative_constraints_for_expression_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Option>, + _count: u32, + _expression: Expression<'db>, +) -> salsa::CycleRecoveryAction>> { + salsa::CycleRecoveryAction::Iterate +} + +fn negative_constraints_for_expression_cycle_initial<'db>( + _db: &'db dyn Db, + _expression: Expression<'db>, +) -> Option> { + None +} + +/// Functions that can be used to narrow the type of a first argument using a "classinfo" second argument. +/// +/// A "classinfo" argument is either a class or a tuple of classes, or a tuple of tuples of classes +/// (etc. for arbitrary levels of recursion) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ClassInfoConstraintFunction { + /// `builtins.isinstance` + IsInstance, + /// `builtins.issubclass` + IsSubclass, +} + +impl ClassInfoConstraintFunction { + /// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`. + /// + /// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604 + /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. + fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { + let constraint_fn = |class: ClassLiteral<'db>| match self { + ClassInfoConstraintFunction::IsInstance => { + Type::instance(db, class.default_specialization(db)) + } + ClassInfoConstraintFunction::IsSubclass => { + SubclassOfType::from(db, class.default_specialization(db)) + } + }; + + match classinfo { + Type::Tuple(tuple) => UnionType::try_from_elements( + db, + tuple + .tuple(db) + .all_elements() + .copied() + .map(|element| self.generate_constraint(db, element)), + ), + Type::ClassLiteral(class_literal) => { + // At runtime (on Python 3.11+), this will return `True` for classes that actually + // do inherit `typing.Any` and `False` otherwise. We could accurately model that? + if class_literal.is_known(db, KnownClass::Any) { + None + } else { + Some(constraint_fn(class_literal)) + } + } + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Class(ClassType::NonGeneric(class)) => Some(constraint_fn(class)), + // It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`, + // e.g. `isinstance(x, list[int])` fails at runtime. + SubclassOfInner::Class(ClassType::Generic(_)) => None, + SubclassOfInner::Dynamic(dynamic) => Some(Type::Dynamic(dynamic)), + }, + Type::Dynamic(_) => Some(classinfo), + Type::Intersection(intersection) => { + if intersection.negative(db).is_empty() { + let mut builder = IntersectionBuilder::new(db); + for element in intersection.positive(db) { + builder = builder.add_positive(self.generate_constraint(db, *element)?); + } + Some(builder.build()) + } else { + // TODO: can we do better here? + None + } + } + Type::Union(union) => { + union.try_map(db, |element| self.generate_constraint(db, *element)) + } + Type::TypeVar(type_var) => match type_var.bound_or_constraints(db)? { + TypeVarBoundOrConstraints::UpperBound(bound) => self.generate_constraint(db, bound), + TypeVarBoundOrConstraints::Constraints(constraints) => { + self.generate_constraint(db, Type::Union(constraints)) + } + }, + + // It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`, + // e.g. `isinstance(x, list[int])` fails at runtime. + Type::GenericAlias(_) => None, + + Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::BooleanLiteral(_) + | Type::BoundMethod(_) + | Type::BoundSuper(_) + | Type::BytesLiteral(_) + | Type::Callable(_) + | Type::DataclassDecorator(_) + | Type::Never + | Type::MethodWrapper(_) + | Type::ModuleLiteral(_) + | Type::FunctionLiteral(_) + | Type::ProtocolInstance(_) + | Type::PropertyInstance(_) + | Type::SpecialForm(_) + | Type::NominalInstance(_) + | Type::LiteralString + | Type::StringLiteral(_) + | Type::IntLiteral(_) + | Type::KnownInstance(_) + | Type::TypeIs(_) + | Type::WrapperDescriptor(_) + | Type::DataclassTransformer(_) => None, + } + } +} + +type NarrowingConstraints<'db> = FxHashMap>; + +fn merge_constraints_and<'db>( + into: &mut NarrowingConstraints<'db>, + from: NarrowingConstraints<'db>, + db: &'db dyn Db, +) { + for (key, value) in from { + match into.entry(key) { + Entry::Occupied(mut entry) => { + *entry.get_mut() = IntersectionBuilder::new(db) + .add_positive(*entry.get()) + .add_positive(value) + .build(); + } + Entry::Vacant(entry) => { + entry.insert(value); + } + } + } +} + +fn merge_constraints_or<'db>( + into: &mut NarrowingConstraints<'db>, + from: &NarrowingConstraints<'db>, + db: &'db dyn Db, +) { + for (key, value) in from { + match into.entry(*key) { + Entry::Occupied(mut entry) => { + *entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build(); + } + Entry::Vacant(entry) => { + entry.insert(Type::object(db)); + } + } + } + for (key, value) in into.iter_mut() { + if !from.contains_key(key) { + *value = Type::object(db); + } + } +} + +fn negate_if<'db>(constraints: &mut NarrowingConstraints<'db>, db: &'db dyn Db, yes: bool) { + for (_place, ty) in constraints.iter_mut() { + *ty = ty.negate_if(db, yes); + } +} + +fn place_expr(expr: &ast::Expr) -> Option { + match expr { + ast::Expr::Name(name) => Some(PlaceExpr::name(name.id.clone())), + ast::Expr::Attribute(attr) => PlaceExpr::try_from(attr).ok(), + ast::Expr::Subscript(subscript) => PlaceExpr::try_from(subscript).ok(), + ast::Expr::Named(named) => PlaceExpr::try_from(named.target.as_ref()).ok(), + _ => None, + } +} + +struct NarrowingConstraintsBuilder<'db, 'ast> { + db: &'db dyn Db, + module: &'ast ParsedModuleRef, + predicate: PredicateNode<'db>, + is_positive: bool, +} + +impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { + fn new( + db: &'db dyn Db, + module: &'ast ParsedModuleRef, + predicate: PredicateNode<'db>, + is_positive: bool, + ) -> Self { + Self { + db, + module, + predicate, + is_positive, + } + } + + fn finish(mut self) -> Option> { + let constraints: Option> = match self.predicate { + PredicateNode::Expression(expression) => { + self.evaluate_expression_predicate(expression, self.is_positive) + } + PredicateNode::Pattern(pattern) => { + self.evaluate_pattern_predicate(pattern, self.is_positive) + } + PredicateNode::ReturnsNever(_) => return None, + PredicateNode::StarImportPlaceholder(_) => return None, + }; + if let Some(mut constraints) = constraints { + constraints.shrink_to_fit(); + Some(constraints) + } else { + None + } + } + + fn evaluate_expression_predicate( + &mut self, + expression: Expression<'db>, + is_positive: bool, + ) -> Option> { + let expression_node = expression.node_ref(self.db, self.module); + self.evaluate_expression_node_predicate(expression_node, expression, is_positive) + } + + fn evaluate_expression_node_predicate( + &mut self, + expression_node: &ruff_python_ast::Expr, + expression: Expression<'db>, + is_positive: bool, + ) -> Option> { + match expression_node { + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { + self.evaluate_simple_expr(expression_node, is_positive) + } + ast::Expr::Compare(expr_compare) => { + self.evaluate_expr_compare(expr_compare, expression, is_positive) + } + ast::Expr::Call(expr_call) => { + self.evaluate_expr_call(expr_call, expression, is_positive) + } + ast::Expr::UnaryOp(unary_op) if unary_op.op == ast::UnaryOp::Not => { + self.evaluate_expression_node_predicate(&unary_op.operand, expression, !is_positive) + } + ast::Expr::BoolOp(bool_op) => self.evaluate_bool_op(bool_op, expression, is_positive), + ast::Expr::Named(expr_named) => self.evaluate_expr_named(expr_named, is_positive), + _ => None, + } + } + + fn evaluate_pattern_predicate_kind( + &mut self, + pattern_predicate_kind: &PatternPredicateKind<'db>, + subject: Expression<'db>, + ) -> Option> { + match pattern_predicate_kind { + PatternPredicateKind::Singleton(singleton) => { + self.evaluate_match_pattern_singleton(subject, *singleton) + } + PatternPredicateKind::Class(cls) => self.evaluate_match_pattern_class(subject, *cls), + PatternPredicateKind::Value(expr) => self.evaluate_match_pattern_value(subject, *expr), + PatternPredicateKind::Or(predicates) => { + self.evaluate_match_pattern_or(subject, predicates) + } + PatternPredicateKind::Unsupported => None, + } + } + + fn evaluate_pattern_predicate( + &mut self, + pattern: PatternPredicate<'db>, + is_positive: bool, + ) -> Option> { + let subject = pattern.subject(self.db); + self.evaluate_pattern_predicate_kind(pattern.kind(self.db), subject) + .map(|mut constraints| { + negate_if(&mut constraints, self.db, !is_positive); + constraints + }) + } + + fn places(&self) -> &'db PlaceTable { + place_table(self.db, self.scope()) + } + + fn scope(&self) -> ScopeId<'db> { + match self.predicate { + PredicateNode::Expression(expression) => expression.scope(self.db), + PredicateNode::Pattern(pattern) => pattern.scope(self.db), + PredicateNode::ReturnsNever(CallableAndCallExpr { callable, .. }) => { + callable.scope(self.db) + } + PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db), + } + } + + #[track_caller] + fn expect_place(&self, place_expr: &PlaceExpr) -> ScopedPlaceId { + self.places() + .place_id_by_expr(place_expr) + .expect("We should always have a place for every `PlaceExpr`") + } + + fn evaluate_simple_expr( + &mut self, + expr: &ast::Expr, + is_positive: bool, + ) -> Option> { + let target = place_expr(expr)?; + let place = self.expect_place(&target); + + let ty = if is_positive { + Type::AlwaysFalsy.negate(self.db) + } else { + Type::AlwaysTruthy.negate(self.db) + }; + + Some(NarrowingConstraints::from_iter([(place, ty)])) + } + + fn evaluate_expr_named( + &mut self, + expr_named: &ast::ExprNamed, + is_positive: bool, + ) -> Option> { + self.evaluate_simple_expr(&expr_named.target, is_positive) + } + + fn evaluate_expr_eq(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + // We can only narrow on equality checks against single-valued types. + if rhs_ty.is_single_valued(self.db) || rhs_ty.is_union_of_single_valued(self.db) { + // The fully-general (and more efficient) approach here would be to introduce a + // `NeverEqualTo` type that can wrap a single-valued type, and then simply return + // `~NeverEqualTo(rhs_ty)` here and let union/intersection builder sort it out. This is + // how we handle `AlwaysTruthy` and `AlwaysFalsy`. But this means we have to deal with + // this type everywhere, and possibly have it show up unsimplified in some cases, and + // so we instead prefer to just do the simplification here. (Another hybrid option that + // would be similar to this, but more efficient, would be to allow narrowing to return + // something that is not a type, and handle this not-a-type in `symbol_from_bindings`, + // instead of intersecting with a type.) + + // Return `true` if it is possible for any two inhabitants of the given types to + // compare equal to each other; otherwise return `false`. + fn could_compare_equal<'db>( + db: &'db dyn Db, + left_ty: Type<'db>, + right_ty: Type<'db>, + ) -> bool { + if !left_ty.is_disjoint_from(db, right_ty) { + // If types overlap, they have inhabitants in common; it's definitely possible + // for an object to compare equal to itself. + return true; + } + match (left_ty, right_ty) { + // In order to be sure a union type cannot compare equal to another type, it + // must be true that no element of the union can compare equal to that type. + (Type::Union(union), _) => union + .elements(db) + .iter() + .any(|ty| could_compare_equal(db, *ty, right_ty)), + (_, Type::Union(union)) => union + .elements(db) + .iter() + .any(|ty| could_compare_equal(db, left_ty, *ty)), + // Boolean literals and int literals are disjoint, and single valued, and yet + // `True == 1` and `False == 0`. + (Type::BooleanLiteral(b), Type::IntLiteral(i)) + | (Type::IntLiteral(i), Type::BooleanLiteral(b)) => i64::from(b) == i, + // Other than the above cases, two single-valued disjoint types cannot compare + // equal. + _ => !(left_ty.is_single_valued(db) && right_ty.is_single_valued(db)), + } + } + + // Return `true` if `lhs_ty` consists only of `LiteralString` and types that cannot + // compare equal to `rhs_ty`. + fn can_narrow_to_rhs<'db>( + db: &'db dyn Db, + lhs_ty: Type<'db>, + rhs_ty: Type<'db>, + ) -> bool { + match lhs_ty { + Type::Union(union) => union + .elements(db) + .iter() + .all(|ty| can_narrow_to_rhs(db, *ty, rhs_ty)), + // Either `rhs_ty` is a string literal, in which case we can narrow to it (no + // other string literal could compare equal to it), or it is not a string + // literal, in which case (given that it is single-valued), LiteralString + // cannot compare equal to it. + Type::LiteralString => true, + _ => !could_compare_equal(db, lhs_ty, rhs_ty), + } + } + + // Filter `ty` to just the types that cannot be equal to `rhs_ty`. + fn filter_to_cannot_be_equal<'db>( + db: &'db dyn Db, + ty: Type<'db>, + rhs_ty: Type<'db>, + ) -> Type<'db> { + match ty { + Type::Union(union) => { + union.map(db, |ty| filter_to_cannot_be_equal(db, *ty, rhs_ty)) + } + // Treat `bool` as `Literal[True, False]`. + Type::NominalInstance(instance) + if instance.class.is_known(db, KnownClass::Bool) => + { + UnionType::from_elements( + db, + [Type::BooleanLiteral(true), Type::BooleanLiteral(false)] + .into_iter() + .map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)), + ) + } + _ => { + if ty.is_single_valued(db) && !could_compare_equal(db, ty, rhs_ty) { + ty + } else { + Type::Never + } + } + } + } + Some(if can_narrow_to_rhs(self.db, lhs_ty, rhs_ty) { + rhs_ty + } else { + filter_to_cannot_be_equal(self.db, lhs_ty, rhs_ty).negate(self.db) + }) + } else { + None + } + } + + fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + match (lhs_ty, rhs_ty) { + (Type::NominalInstance(instance), Type::IntLiteral(i)) + if instance.class.is_known(self.db, KnownClass::Bool) => + { + if i == 0 { + Some(Type::BooleanLiteral(false).negate(self.db)) + } else if i == 1 { + Some(Type::BooleanLiteral(true).negate(self.db)) + } else { + None + } + } + (_, Type::BooleanLiteral(b)) => Some( + UnionType::from_elements(self.db, [rhs_ty, Type::IntLiteral(i64::from(b))]) + .negate(self.db), + ), + _ if rhs_ty.is_single_valued(self.db) => Some(rhs_ty.negate(self.db)), + _ => None, + } + } + + fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { + match rhs_ty { + Type::Tuple(rhs_tuple) => Some(UnionType::from_elements( + self.db, + rhs_tuple.tuple(self.db).all_elements(), + )), + + Type::StringLiteral(string_literal) => Some(UnionType::from_elements( + self.db, + string_literal + .iter_each_char(self.db) + .map(Type::StringLiteral), + )), + + _ => None, + } + } else { + None + } + } + + fn evaluate_expr_compare_op( + &mut self, + lhs_ty: Type<'db>, + rhs_ty: Type<'db>, + op: ast::CmpOp, + ) -> Option> { + match op { + ast::CmpOp::IsNot => { + if rhs_ty.is_singleton(self.db) { + let ty = IntersectionBuilder::new(self.db) + .add_negative(rhs_ty) + .build(); + Some(ty) + } else { + // Non-singletons cannot be safely narrowed using `is not` + None + } + } + ast::CmpOp::Is => Some(rhs_ty), + ast::CmpOp::Eq => self.evaluate_expr_eq(lhs_ty, rhs_ty), + ast::CmpOp::NotEq => self.evaluate_expr_ne(lhs_ty, rhs_ty), + ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty), + ast::CmpOp::NotIn => self + .evaluate_expr_in(lhs_ty, rhs_ty) + .map(|ty| ty.negate(self.db)), + _ => None, + } + } + + fn evaluate_expr_compare( + &mut self, + expr_compare: &ast::ExprCompare, + expression: Expression<'db>, + is_positive: bool, + ) -> Option> { + fn is_narrowing_target_candidate(expr: &ast::Expr) -> bool { + matches!( + expr, + ast::Expr::Name(_) + | ast::Expr::Attribute(_) + | ast::Expr::Subscript(_) + | ast::Expr::Call(_) + | ast::Expr::Named(_) + ) + } + + let ast::ExprCompare { + range: _, + node_index: _, + left, + ops, + comparators, + } = expr_compare; + + // Performance optimization: early return if there are no potential narrowing targets. + if !is_narrowing_target_candidate(left) + && comparators + .iter() + .all(|c| !is_narrowing_target_candidate(c)) + { + return None; + } + + if !is_positive && comparators.len() > 1 { + // We can't negate a constraint made by a multi-comparator expression, since we can't + // know which comparison part is the one being negated. + // For example, the negation of `x is 1 is y is 2`, would be `(x is not 1) or (y is not 1) or (y is not 2)` + // and that requires cross-symbol constraints, which we don't support yet. + return None; + } + + let inference = infer_expression_types(self.db, expression); + + let comparator_tuples = std::iter::once(&**left) + .chain(comparators) + .tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>(); + let mut constraints = NarrowingConstraints::default(); + + let mut last_rhs_ty: Option = None; + + for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) { + let lhs_ty = last_rhs_ty.unwrap_or_else(|| inference.expression_type(left)); + let rhs_ty = inference.expression_type(right); + last_rhs_ty = Some(rhs_ty); + + match left { + ast::Expr::Name(_) + | ast::Expr::Attribute(_) + | ast::Expr::Subscript(_) + | ast::Expr::Named(_) => { + if let Some(left) = place_expr(left) { + let op = if is_positive { *op } else { op.negate() }; + + if let Some(ty) = self.evaluate_expr_compare_op(lhs_ty, rhs_ty, op) { + let place = self.expect_place(&left); + constraints.insert(place, ty); + } + } + } + ast::Expr::Call(ast::ExprCall { + range: _, + node_index: _, + func: callable, + arguments: + ast::Arguments { + args, + keywords, + range: _, + node_index: _, + }, + }) if keywords.is_empty() => { + let rhs_class = match rhs_ty { + Type::ClassLiteral(class) => class, + Type::GenericAlias(alias) => alias.origin(self.db), + _ => { + continue; + } + }; + + let target = match &**args { + [first] => match place_expr(first) { + Some(target) => target, + None => continue, + }, + _ => continue, + }; + + let is_valid_constraint = if is_positive { + op == &ast::CmpOp::Is + } else { + op == &ast::CmpOp::IsNot + }; + + if !is_valid_constraint { + continue; + } + + let callable_type = inference.expression_type(&**callable); + + if callable_type + .into_class_literal() + .is_some_and(|c| c.is_known(self.db, KnownClass::Type)) + { + let place = self.expect_place(&target); + constraints.insert( + place, + Type::instance(self.db, rhs_class.unknown_specialization(self.db)), + ); + } + } + _ => {} + } + } + Some(constraints) + } + + fn evaluate_expr_call( + &mut self, + expr_call: &ast::ExprCall, + expression: Expression<'db>, + is_positive: bool, + ) -> Option> { + let inference = infer_expression_types(self.db, expression); + + let callable_ty = inference.expression_type(&*expr_call.func); + + // TODO: add support for PEP 604 union types on the right hand side of `isinstance` + // and `issubclass`, for example `isinstance(x, str | (int | float))`. + match callable_ty { + Type::FunctionLiteral(function_type) + if matches!( + function_type.known(self.db), + None | Some(KnownFunction::RevealType) + ) => + { + let return_ty = inference.expression_type(expr_call); + + let (guarded_ty, place) = match return_ty { + // TODO: TypeGuard + Type::TypeIs(type_is) => { + let (_, place) = type_is.place_info(self.db)?; + (type_is.return_type(self.db), place) + } + _ => return None, + }; + + Some(NarrowingConstraints::from_iter([( + place, + guarded_ty.negate_if(self.db, !is_positive), + )])) + } + Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => { + let [first_arg, second_arg] = &*expr_call.arguments.args else { + return None; + }; + let first_arg = place_expr(first_arg)?; + let function = function_type.known(self.db)?; + let place = self.expect_place(&first_arg); + + if function == KnownFunction::HasAttr { + let attr = inference + .expression_type(second_arg) + .into_string_literal()? + .value(self.db); + + if !is_identifier(attr) { + return None; + } + + // Since `hasattr` only checks if an attribute is readable, + // the type of the protocol member should be a read-only property that returns `object`. + let constraint = Type::protocol_with_readonly_members( + self.db, + [(attr, Type::object(self.db))], + ); + + return Some(NarrowingConstraints::from_iter([( + place, + constraint.negate_if(self.db, !is_positive), + )])); + } + + let function = function.into_classinfo_constraint_function()?; + + let class_info_ty = inference.expression_type(second_arg); + + function + .generate_constraint(self.db, class_info_ty) + .map(|constraint| { + NarrowingConstraints::from_iter([( + place, + constraint.negate_if(self.db, !is_positive), + )]) + }) + } + // for the expression `bool(E)`, we further narrow the type based on `E` + Type::ClassLiteral(class_type) + if expr_call.arguments.args.len() == 1 + && expr_call.arguments.keywords.is_empty() + && class_type.is_known(self.db, KnownClass::Bool) => + { + self.evaluate_expression_node_predicate( + &expr_call.arguments.args[0], + expression, + is_positive, + ) + } + _ => None, + } + } + + fn evaluate_match_pattern_singleton( + &mut self, + subject: Expression<'db>, + singleton: ast::Singleton, + ) -> Option> { + let subject = place_expr(subject.node_ref(self.db, self.module))?; + let place = self.expect_place(&subject); + + let ty = match singleton { + ast::Singleton::None => Type::none(self.db), + ast::Singleton::True => Type::BooleanLiteral(true), + ast::Singleton::False => Type::BooleanLiteral(false), + }; + Some(NarrowingConstraints::from_iter([(place, ty)])) + } + + fn evaluate_match_pattern_class( + &mut self, + subject: Expression<'db>, + cls: Expression<'db>, + ) -> Option> { + let subject = place_expr(subject.node_ref(self.db, self.module))?; + let place = self.expect_place(&subject); + + let ty = infer_same_file_expression_type(self.db, cls, self.module).to_instance(self.db)?; + + Some(NarrowingConstraints::from_iter([(place, ty)])) + } + + fn evaluate_match_pattern_value( + &mut self, + subject: Expression<'db>, + value: Expression<'db>, + ) -> Option> { + let subject = place_expr(subject.node_ref(self.db, self.module))?; + let place = self.expect_place(&subject); + + let ty = infer_same_file_expression_type(self.db, value, self.module); + Some(NarrowingConstraints::from_iter([(place, ty)])) + } + + fn evaluate_match_pattern_or( + &mut self, + subject: Expression<'db>, + predicates: &Vec>, + ) -> Option> { + let db = self.db; + + predicates + .iter() + .filter_map(|predicate| self.evaluate_pattern_predicate_kind(predicate, subject)) + .reduce(|mut constraints, constraints_| { + merge_constraints_or(&mut constraints, &constraints_, db); + constraints + }) + } + + fn evaluate_bool_op( + &mut self, + expr_bool_op: &ExprBoolOp, + expression: Expression<'db>, + is_positive: bool, + ) -> Option> { + let inference = infer_expression_types(self.db, expression); + let mut sub_constraints = expr_bool_op + .values + .iter() + // filter our arms with statically known truthiness + .filter(|expr| { + inference.expression_type(*expr).bool(self.db) + != match expr_bool_op.op { + BoolOp::And => Truthiness::AlwaysTrue, + BoolOp::Or => Truthiness::AlwaysFalse, + } + }) + .map(|sub_expr| { + self.evaluate_expression_node_predicate(sub_expr, expression, is_positive) + }) + .collect::>(); + match (expr_bool_op.op, is_positive) { + (BoolOp::And, true) | (BoolOp::Or, false) => { + let mut aggregation: Option = None; + for sub_constraint in sub_constraints.into_iter().flatten() { + if let Some(ref mut some_aggregation) = aggregation { + merge_constraints_and(some_aggregation, sub_constraint, self.db); + } else { + aggregation = Some(sub_constraint); + } + } + aggregation + } + (BoolOp::Or, true) | (BoolOp::And, false) => { + let (first, rest) = sub_constraints.split_first_mut()?; + if let Some(first) = first { + for rest_constraint in rest { + if let Some(rest_constraint) = rest_constraint { + merge_constraints_or(first, rest_constraint, self.db); + } else { + return None; + } + } + } + first.clone() + } + } + } +} diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs new file mode 100644 index 0000000000000..8b5baa958e044 --- /dev/null +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -0,0 +1,325 @@ +//! This module contains quickcheck-based property tests for `Type`s. +//! +//! These tests are disabled by default, as they are non-deterministic and slow. You can +//! run them explicitly using: +//! +//! ```sh +//! cargo test -p ty_python_semantic -- --ignored types::property_tests::stable +//! ``` +//! +//! The number of tests (default: 100) can be controlled by setting the `QUICKCHECK_TESTS` +//! environment variable. For example: +//! +//! ```sh +//! QUICKCHECK_TESTS=10000 cargo test … +//! ``` +//! +//! If you want to run these tests for a longer period of time, it's advisable to run them +//! in release mode. As some tests are slower than others, it's advisable to run them in a +//! loop until they fail: +//! +//! ```sh +//! export QUICKCHECK_TESTS=100000 +//! while cargo test --release -p ty_python_semantic -- \ +//! --ignored types::property_tests::stable; do :; done +//! ``` +mod setup; +mod type_generation; + +use type_generation::{intersection, union}; + +/// A macro to define a property test for types. +/// +/// The `$test_name` identifier specifies the name of the test function. The `$db` identifier +/// is used to refer to the salsa database in the property to be tested. The actual property is +/// specified using the syntax: +/// +/// forall types t1, t2, ..., tn . ` +/// +/// where `t1`, `t2`, ..., `tn` are identifiers that represent arbitrary types, and `` +/// is an expression using these identifiers. +/// +macro_rules! type_property_test { + ($test_name:ident, $db:ident, forall types $($types:ident),+ . $property:expr) => { + #[quickcheck_macros::quickcheck] + #[ignore] + fn $test_name($($types: crate::types::property_tests::type_generation::Ty),+) -> bool { + let $db = &crate::types::property_tests::setup::get_cached_db(); + $(let $types = $types.into_type($db);)+ + + $property + } + }; + ($test_name:ident, $db:ident, forall fully_static_types $($types:ident),+ . $property:expr) => { + #[quickcheck_macros::quickcheck] + #[ignore] + fn $test_name($($types: crate::types::property_tests::type_generation::FullyStaticTy),+) -> bool { + let $db = &crate::types::property_tests::setup::get_cached_db(); + $(let $types = $types.into_type($db);)+ + + $property + } + }; + // A property test with a logical implication. + ($name:ident, $db:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => { + type_property_test!($name, $db, forall $typekind $($types),+ . !($premise) || ($conclusion)); + }; +} + +mod stable { + use super::union; + use crate::types::{CallableType, Type}; + + // Reflexivity: `T` is equivalent to itself. + type_property_test!( + equivalent_to_is_reflexive, db, + forall types t. t.is_equivalent_to(db, t) + ); + + // Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`. + type_property_test!( + equivalent_to_is_symmetric, db, + forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s) + ); + + // Transitivity: If `S` is equivalent to `T` and `T` is equivalent to `U`, then `S` must be equivalent to `U`. + type_property_test!( + equivalent_to_is_transitive, db, + forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u) + ); + + // `S <: T` and `T <: U` implies that `S <: U`. + type_property_test!( + subtype_of_is_transitive, db, + forall types s, t, u. s.is_subtype_of(db, t) && t.is_subtype_of(db, u) => s.is_subtype_of(db, u) + ); + + // `S <: T` and `T <: S` implies that `S` is equivalent to `T`. + type_property_test!( + subtype_of_is_antisymmetric, db, + forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t) + ); + + // `T` is not disjoint from itself, unless `T` is `Never`. + type_property_test!( + disjoint_from_is_irreflexive, db, + forall types t. t.is_disjoint_from(db, t) => t.is_never() + ); + + // `S` is disjoint from `T` implies that `T` is disjoint from `S`. + type_property_test!( + disjoint_from_is_symmetric, db, + forall types s, t. s.is_disjoint_from(db, t) == t.is_disjoint_from(db, s) + ); + + // `S <: T` implies that `S` is not disjoint from `T`, unless `S` is `Never`. + type_property_test!( + subtype_of_implies_not_disjoint_from, db, + forall types s, t. s.is_subtype_of(db, t) => !s.is_disjoint_from(db, t) || s.is_never() + ); + + // `S <: T` implies that `S` can be assigned to `T`. + type_property_test!( + subtype_of_implies_assignable_to, db, + forall types s, t. s.is_subtype_of(db, t) => s.is_assignable_to(db, t) + ); + + // If `T` is a singleton, it is also single-valued. + type_property_test!( + singleton_implies_single_valued, db, + forall types t. t.is_singleton(db) => t.is_single_valued(db) + ); + + // All types should be assignable to `object` + type_property_test!( + all_types_assignable_to_object, db, + forall types t. t.is_assignable_to(db, Type::object(db)) + ); + + // And all types should be subtypes of `object` + type_property_test!( + all_types_subtype_of_object, db, + forall types t. t.is_subtype_of(db, Type::object(db)) + ); + + // Never should be assignable to every type + type_property_test!( + never_assignable_to_every_type, db, + forall types t. Type::Never.is_assignable_to(db, t) + ); + + // And it should be a subtype of all types + type_property_test!( + never_subtype_of_every_type, db, + forall types t. Type::Never.is_subtype_of(db, t) + ); + + // Similar to `Never`, a "bottom" callable type should be a subtype of all callable types + type_property_test!( + bottom_callable_is_subtype_of_all_callable, db, + forall types t. t.is_callable_type() + => CallableType::bottom(db).is_subtype_of(db, t) + ); + + // `T` can be assigned to itself. + type_property_test!( + assignable_to_is_reflexive, db, + forall types t. t.is_assignable_to(db, t) + ); + + // For *any* pair of types, each of the pair should be assignable to the union of the two. + type_property_test!( + all_type_pairs_are_assignable_to_their_union, db, + forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t])) + ); + + // Only `Never` is a subtype of `Any`. + type_property_test!( + only_never_is_subtype_of_any, db, + forall types s. !s.is_equivalent_to(db, Type::Never) => !s.is_subtype_of(db, Type::any()) + ); + + // Only `object` is a supertype of `Any`. + type_property_test!( + only_object_is_supertype_of_any, db, + forall types t. !t.is_equivalent_to(db, Type::object(db)) => !Type::any().is_subtype_of(db, t) + ); + + // Equivalence is commutative. + type_property_test!( + equivalent_to_is_commutative, db, + forall types s, t. s.is_equivalent_to(db, t) == t.is_equivalent_to(db, s) + ); + + // A fully static type `T` is a subtype of itself. (This is not true for non-fully-static + // types; `Any` is not a subtype of `Any`, only `Never` is.) + type_property_test!( + subtype_of_is_reflexive_for_fully_static_types, db, + forall fully_static_types t. t.is_subtype_of(db, t) + ); + + // For any two fully static types, each type in the pair must be a subtype of their union. + // (This is clearly not true for non-fully-static types, since their subtyping is not + // reflexive.) + type_property_test!( + all_fully_static_type_pairs_are_subtype_of_their_union, db, + forall fully_static_types s, t. s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t])) + ); +} + +/// This module contains property tests that currently lead to many false positives. +/// +/// The reason for this is our insufficient understanding of equivalence of types. For +/// example, we currently consider `int | str` and `str | int` to be different types. +/// Similar issues exist for intersection types. Once this is resolved, we can move these +/// tests to the `stable` section. In the meantime, it can still be useful to run these +/// tests (using [`types::property_tests::flaky`]), to see if there are any new obvious bugs. +mod flaky { + use itertools::Itertools; + + use super::{intersection, union}; + use crate::types::{KnownClass, Type}; + + // Negating `T` twice is equivalent to `T`. + type_property_test!( + double_negation_is_identity, db, + forall types t. t.negate(db).negate(db).is_equivalent_to(db, t) + ); + + // For any fully static type `T`, `T` should be disjoint from `~T`. + // https://github.com/astral-sh/ty/issues/216 + type_property_test!( + negation_of_fully_static_types_is_disjoint, db, + forall fully_static_types t. t.negate(db).is_disjoint_from(db, t) + ); + + // For two types, their intersection must be a subtype of each type in the pair. + type_property_test!( + all_type_pairs_are_supertypes_of_their_intersection, db, + forall types s, t. + intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t) + ); + + // And the intersection of a pair of types + // should be assignable to both types of the pair. + // Currently fails due to https://github.com/astral-sh/ruff/issues/14899 + type_property_test!( + all_type_pairs_can_be_assigned_from_their_intersection, db, + forall types s, t. intersection(db, [s, t]).is_assignable_to(db, s) && intersection(db, [s, t]).is_assignable_to(db, t) + ); + + // Equal element sets of intersections implies equivalence + // flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513 + type_property_test!( + intersection_equivalence_not_order_dependent, db, + forall types s, t, u. + [s, t, u] + .into_iter() + .permutations(3) + .map(|trio_of_types| intersection(db, trio_of_types)) + .permutations(2) + .all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, vec_of_intersections[1])) + ); + + // Equal element sets of unions implies equivalence + // 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. + [s, t, u] + .into_iter() + .permutations(3) + .map(|trio_of_types| union(db, trio_of_types)) + .permutations(2) + .all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, vec_of_unions[1])) + ); + + // `S | T` is always a supertype of `S`. + // Thus, `S` is never disjoint from `S | T`. + type_property_test!( + constituent_members_of_union_is_not_disjoint_from_that_union, db, + 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/ty_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)) + ); + + // Both the top and bottom materialization tests are flaky in part due to various failures that + // it discovers in the current implementation of assignability of the types. + // TODO: Create a issue with some example failures to keep track of it + + // `T'`, the top materialization of `T`, should be assignable to `T`. + type_property_test!( + top_materialization_of_type_is_assignable_to_type, db, + forall types t. t.top_materialization(db).is_assignable_to(db, t) + ); + + // Similarly, `T'`, the bottom materialization of `T`, should also be assignable to `T`. + type_property_test!( + bottom_materialization_of_type_is_assigneble_to_type, db, + forall types t. t.bottom_materialization(db).is_assignable_to(db, t) + ); + + // Any type assignable to `Iterable[object]` should be considered iterable. + // + // Note that the inverse is not true, due to the fact that we recognize the old-style + // iteration protocol as well as the new-style iteration protocol: not all objects that + // we consider iterable are assignable to `Iterable[object]`. + type_property_test!( + all_type_assignable_to_iterable_are_iterable, db, + forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object(db)])) => t.try_iterate(db).is_ok() + ); +} diff --git a/crates/red_knot_python_semantic/src/types/property_tests/setup.rs b/crates/ty_python_semantic/src/types/property_tests/setup.rs similarity index 85% rename from crates/red_knot_python_semantic/src/types/property_tests/setup.rs rename to crates/ty_python_semantic/src/types/property_tests/setup.rs index a64ad2e46ec7b..b436c4d9e92fc 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests/setup.rs +++ b/crates/ty_python_semantic/src/types/property_tests/setup.rs @@ -1,4 +1,4 @@ -use crate::db::tests::{setup_db, TestDb}; +use crate::db::tests::{TestDb, setup_db}; use std::sync::{Arc, Mutex, OnceLock}; static CACHED_DB: OnceLock>> = OnceLock::new(); diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs new file mode 100644 index 0000000000000..1d0a651ada300 --- /dev/null +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -0,0 +1,559 @@ +use crate::db::tests::TestDb; +use crate::place::{builtins_symbol, known_module_symbol}; +use crate::types::tuple::TupleType; +use crate::types::{ + BoundMethodType, CallableType, IntersectionBuilder, KnownClass, Parameter, Parameters, + Signature, SpecialFormType, SubclassOfType, Type, UnionType, +}; +use crate::{Db, KnownModule}; +use hashbrown::HashSet; +use quickcheck::{Arbitrary, Gen}; +use ruff_python_ast::name::Name; + +/// A test representation of a type that can be transformed unambiguously into a real Type, +/// given a db. +/// +/// TODO: We should add some variants that exercise generic classes and specializations thereof. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Ty { + Never, + Unknown, + None, + Any, + IntLiteral(i64), + BooleanLiteral(bool), + StringLiteral(&'static str), + LiteralString, + BytesLiteral(&'static str), + // BuiltinInstance("str") corresponds to an instance of the builtin `str` class + BuiltinInstance(&'static str), + /// Members of the `abc` stdlib module + AbcInstance(&'static str), + AbcClassLiteral(&'static str), + TypingLiteral, + // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself + BuiltinClassLiteral(&'static str), + KnownClassInstance(KnownClass), + Union(Vec), + Intersection { + pos: Vec, + neg: Vec, + }, + FixedLengthTuple(Vec), + VariableLengthTuple(Vec, Box, Vec), + SubclassOfAny, + SubclassOfBuiltinClass(&'static str), + SubclassOfAbcClass(&'static str), + AlwaysTruthy, + AlwaysFalsy, + BuiltinsFunction(&'static str), + BuiltinsBoundMethod { + class: &'static str, + method: &'static str, + }, + Callable { + params: CallableParams, + returns: Option>, + }, + /// `unittest.mock.Mock` is interesting because it is a nominal instance type + /// where the class has `Any` in its MRO + UnittestMockInstance, + UnittestMockLiteral, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum CallableParams { + GradualForm, + List(Vec), +} + +impl CallableParams { + pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> { + match self { + CallableParams::GradualForm => Parameters::gradual_form(), + CallableParams::List(params) => Parameters::new(params.into_iter().map(|param| { + let mut parameter = match param.kind { + ParamKind::PositionalOnly => Parameter::positional_only(param.name), + ParamKind::PositionalOrKeyword => { + Parameter::positional_or_keyword(param.name.unwrap()) + } + ParamKind::Variadic => Parameter::variadic(param.name.unwrap()), + ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()), + ParamKind::KeywordVariadic => Parameter::keyword_variadic(param.name.unwrap()), + }; + if let Some(annotated_ty) = param.annotated_ty { + parameter = parameter.with_annotated_type(annotated_ty.into_type(db)); + } + if let Some(default_ty) = param.default_ty { + parameter = parameter.with_default_type(default_ty.into_type(db)); + } + parameter + })), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Param { + kind: ParamKind, + name: Option, + annotated_ty: Option, + default_ty: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParamKind { + PositionalOnly, + PositionalOrKeyword, + Variadic, + KeywordOnly, + KeywordVariadic, +} + +#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)] +fn create_bound_method<'db>( + db: &'db dyn Db, + function: Type<'db>, + builtins_class: Type<'db>, +) -> Type<'db> { + Type::BoundMethod(BoundMethodType::new( + db, + function.expect_function_literal(), + builtins_class.to_instance(db).unwrap(), + )) +} + +impl Ty { + pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + match self { + Ty::Never => Type::Never, + Ty::Unknown => Type::unknown(), + Ty::None => Type::none(db), + Ty::Any => Type::any(), + Ty::IntLiteral(n) => Type::IntLiteral(n), + Ty::StringLiteral(s) => Type::string_literal(db, s), + Ty::BooleanLiteral(b) => Type::BooleanLiteral(b), + Ty::LiteralString => Type::LiteralString, + Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), + Ty::BuiltinInstance(s) => builtins_symbol(db, s) + .place + .expect_type() + .to_instance(db) + .unwrap(), + Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) + .place + .expect_type() + .to_instance(db) + .unwrap(), + Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) + .place + .expect_type(), + Ty::UnittestMockLiteral => known_module_symbol(db, KnownModule::UnittestMock, "Mock") + .place + .expect_type(), + Ty::UnittestMockInstance => Ty::UnittestMockLiteral + .into_type(db) + .to_instance(db) + .unwrap(), + Ty::TypingLiteral => Type::SpecialForm(SpecialFormType::Literal), + Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).place.expect_type(), + Ty::KnownClassInstance(known_class) => known_class.to_instance(db), + Ty::Union(tys) => { + UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) + } + Ty::Intersection { pos, neg } => { + let mut builder = IntersectionBuilder::new(db); + for p in pos { + builder = builder.add_positive(p.into_type(db)); + } + for n in neg { + builder = builder.add_negative(n.into_type(db)); + } + builder.build() + } + Ty::FixedLengthTuple(tys) => { + let elements = tys.into_iter().map(|ty| ty.into_type(db)); + TupleType::from_elements(db, elements) + } + Ty::VariableLengthTuple(prefix, variable, suffix) => { + let prefix = prefix.into_iter().map(|ty| ty.into_type(db)); + let variable = variable.into_type(db); + let suffix = suffix.into_iter().map(|ty| ty.into_type(db)); + TupleType::mixed(db, prefix, variable, suffix) + } + Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), + Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( + db, + builtins_symbol(db, s) + .place + .expect_type() + .expect_class_literal() + .default_specialization(db), + ), + Ty::SubclassOfAbcClass(s) => SubclassOfType::from( + db, + known_module_symbol(db, KnownModule::Abc, s) + .place + .expect_type() + .expect_class_literal() + .default_specialization(db), + ), + Ty::AlwaysTruthy => Type::AlwaysTruthy, + Ty::AlwaysFalsy => Type::AlwaysFalsy, + Ty::BuiltinsFunction(name) => builtins_symbol(db, name).place.expect_type(), + Ty::BuiltinsBoundMethod { class, method } => { + let builtins_class = builtins_symbol(db, class).place.expect_type(); + let function = builtins_class.member(db, method).place.expect_type(); + + create_bound_method(db, function, builtins_class) + } + Ty::Callable { params, returns } => CallableType::single( + db, + Signature::new( + params.into_parameters(db), + returns.map(|ty| ty.into_type(db)), + ), + ), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct FullyStaticTy(Ty); + +impl FullyStaticTy { + pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + self.0.into_type(db) + } +} + +fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty { + // We could select a random integer here, but this would make it much less + // likely to explore interesting edge cases: + let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap()); + let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g)); + + // Update this if new non-fully-static types are added below. + let fully_static_index = 5; + let types = &[ + Ty::Any, + Ty::Unknown, + Ty::SubclassOfAny, + Ty::UnittestMockLiteral, + Ty::UnittestMockInstance, + // Add fully static types below, dynamic types above. + // Update `fully_static_index` above if adding new dynamic types! + Ty::Never, + Ty::None, + int_lit, + bool_lit, + Ty::StringLiteral(""), + Ty::StringLiteral("a"), + Ty::LiteralString, + Ty::BytesLiteral(""), + Ty::BytesLiteral("\x00"), + Ty::KnownClassInstance(KnownClass::Object), + Ty::KnownClassInstance(KnownClass::Str), + Ty::KnownClassInstance(KnownClass::Int), + Ty::KnownClassInstance(KnownClass::Bool), + Ty::KnownClassInstance(KnownClass::FunctionType), + Ty::KnownClassInstance(KnownClass::SpecialForm), + Ty::KnownClassInstance(KnownClass::TypeVar), + Ty::KnownClassInstance(KnownClass::TypeAliasType), + Ty::KnownClassInstance(KnownClass::NoDefaultType), + Ty::TypingLiteral, + Ty::BuiltinClassLiteral("str"), + Ty::BuiltinClassLiteral("int"), + Ty::BuiltinClassLiteral("bool"), + Ty::BuiltinClassLiteral("object"), + Ty::BuiltinInstance("type"), + Ty::AbcInstance("ABC"), + Ty::AbcInstance("ABCMeta"), + Ty::SubclassOfBuiltinClass("object"), + Ty::SubclassOfBuiltinClass("str"), + Ty::SubclassOfBuiltinClass("type"), + Ty::AbcClassLiteral("ABC"), + Ty::AbcClassLiteral("ABCMeta"), + Ty::SubclassOfAbcClass("ABC"), + Ty::SubclassOfAbcClass("ABCMeta"), + Ty::AlwaysTruthy, + Ty::AlwaysFalsy, + Ty::BuiltinsFunction("chr"), + Ty::BuiltinsFunction("ascii"), + Ty::BuiltinsBoundMethod { + class: "str", + method: "isascii", + }, + Ty::BuiltinsBoundMethod { + class: "int", + method: "bit_length", + }, + ]; + let types = if fully_static { + &types[fully_static_index..] + } else { + types + }; + g.choose(types).unwrap().clone() +} + +/// Constructs an arbitrary type. +/// +/// The `size` parameter controls the depth of the type tree. For example, +/// a simple type like `int` has a size of 0, `Union[int, str]` has a size +/// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc. +/// +/// The `fully_static` parameter, if `true`, limits generation to fully static types. +fn arbitrary_type(g: &mut Gen, size: u32, fully_static: bool) -> Ty { + if size == 0 { + arbitrary_core_type(g, fully_static) + } else { + match u32::arbitrary(g) % 6 { + 0 => arbitrary_core_type(g, fully_static), + 1 => Ty::Union( + (0..*g.choose(&[2, 3]).unwrap()) + .map(|_| arbitrary_type(g, size - 1, fully_static)) + .collect(), + ), + 2 => Ty::FixedLengthTuple( + (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1, fully_static)) + .collect(), + ), + 3 => Ty::VariableLengthTuple( + (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1, fully_static)) + .collect(), + Box::new(arbitrary_type(g, size - 1, fully_static)), + (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1, fully_static)) + .collect(), + ), + 4 => Ty::Intersection { + pos: (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1, fully_static)) + .collect(), + neg: (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1, fully_static)) + .collect(), + }, + 5 => Ty::Callable { + params: match u32::arbitrary(g) % 2 { + 0 if !fully_static => CallableParams::GradualForm, + _ => CallableParams::List(arbitrary_parameter_list(g, size, fully_static)), + }, + returns: arbitrary_annotation(g, size - 1, fully_static).map(Box::new), + }, + _ => unreachable!(), + } + } +} + +fn arbitrary_parameter_list(g: &mut Gen, size: u32, fully_static: bool) -> Vec { + let mut params: Vec = vec![]; + let mut used_names = HashSet::new(); + + // First, choose the number of parameters to generate. + for _ in 0..*g.choose(&[0, 1, 2, 3, 4, 5]).unwrap() { + // Next, choose the kind of parameters that can be generated based on the last parameter. + let next_kind = match params.last().map(|p| p.kind) { + None | Some(ParamKind::PositionalOnly) => *g + .choose(&[ + ParamKind::PositionalOnly, + ParamKind::PositionalOrKeyword, + ParamKind::Variadic, + ParamKind::KeywordOnly, + ParamKind::KeywordVariadic, + ]) + .unwrap(), + Some(ParamKind::PositionalOrKeyword) => *g + .choose(&[ + ParamKind::PositionalOrKeyword, + ParamKind::Variadic, + ParamKind::KeywordOnly, + ParamKind::KeywordVariadic, + ]) + .unwrap(), + Some(ParamKind::Variadic | ParamKind::KeywordOnly) => *g + .choose(&[ParamKind::KeywordOnly, ParamKind::KeywordVariadic]) + .unwrap(), + Some(ParamKind::KeywordVariadic) => { + // There can't be any other parameter kind after a keyword variadic parameter. + break; + } + }; + + let name = loop { + let name = if matches!(next_kind, ParamKind::PositionalOnly) { + arbitrary_optional_name(g) + } else { + Some(arbitrary_name(g)) + }; + if let Some(name) = name { + if used_names.insert(name.clone()) { + break Some(name); + } + } else { + break None; + } + }; + + params.push(Param { + kind: next_kind, + name, + annotated_ty: arbitrary_annotation(g, size, fully_static), + default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) { + None + } else { + arbitrary_optional_type(g, size, fully_static) + }, + }); + } + + params +} + +/// An arbitrary optional type, always `Some` if fully static. +fn arbitrary_annotation(g: &mut Gen, size: u32, fully_static: bool) -> Option { + if fully_static { + Some(arbitrary_type(g, size, true)) + } else { + arbitrary_optional_type(g, size, false) + } +} + +fn arbitrary_optional_type(g: &mut Gen, size: u32, fully_static: bool) -> Option { + match u32::arbitrary(g) % 2 { + 0 => None, + 1 => Some(arbitrary_type(g, size, fully_static)), + _ => unreachable!(), + } +} + +fn arbitrary_name(g: &mut Gen) -> Name { + Name::new(format!("n{}", u32::arbitrary(g) % 10)) +} + +fn arbitrary_optional_name(g: &mut Gen) -> Option { + match u32::arbitrary(g) % 2 { + 0 => None, + 1 => Some(arbitrary_name(g)), + _ => unreachable!(), + } +} + +impl Arbitrary for Ty { + fn arbitrary(g: &mut Gen) -> Ty { + const MAX_SIZE: u32 = 2; + arbitrary_type(g, MAX_SIZE, false) + } + + fn shrink(&self) -> Box> { + match self.clone() { + Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { + 0 => None, + 1 => Some(elts.into_iter().next().unwrap()), + _ => Some(Ty::Union(elts)), + })), + Ty::FixedLengthTuple(types) => { + Box::new(types.shrink().filter_map(|elts| match elts.len() { + 0 => None, + 1 => Some(elts.into_iter().next().unwrap()), + _ => Some(Ty::FixedLengthTuple(elts)), + })) + } + Ty::VariableLengthTuple(prefix, variable, suffix) => { + // We shrink the suffix first, then the prefix, then the variable-length type. + let suffix_shrunk = suffix.shrink().map({ + let prefix = prefix.clone(); + let variable = variable.clone(); + move |suffix| Ty::VariableLengthTuple(prefix.clone(), variable.clone(), suffix) + }); + let prefix_shrunk = prefix.shrink().map({ + let variable = variable.clone(); + let suffix = suffix.clone(); + move |prefix| Ty::VariableLengthTuple(prefix, variable.clone(), suffix.clone()) + }); + let variable_shrunk = variable.shrink().map({ + let prefix = prefix.clone(); + let suffix = suffix.clone(); + move |variable| { + Ty::VariableLengthTuple(prefix.clone(), variable, suffix.clone()) + } + }); + Box::new(suffix_shrunk.chain(prefix_shrunk).chain(variable_shrunk)) + } + Ty::Intersection { pos, neg } => { + // Shrinking on intersections is not exhaustive! + // + // We try to shrink the positive side or the negative side, + // but we aren't shrinking both at the same time. + // + // This should remove positive or negative constraints but + // won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink + // iteration. + // + // Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails + // so that shrinking can happen there. + let pos_orig = pos.clone(); + let neg_orig = neg.clone(); + Box::new( + // we shrink negative constraints first, as + // intersections with only negative constraints are + // more confusing + neg.shrink() + .map(move |shrunk_neg| Ty::Intersection { + pos: pos_orig.clone(), + neg: shrunk_neg, + }) + .chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection { + pos: shrunk_pos, + neg: neg_orig.clone(), + })) + .filter_map(|ty| { + if let Ty::Intersection { pos, neg } = &ty { + match (pos.len(), neg.len()) { + // an empty intersection does not mean + // anything + (0, 0) => None, + // a single positive element should be + // unwrapped + (1, 0) => Some(pos[0].clone()), + _ => Some(ty), + } + } else { + unreachable!() + } + }), + ) + } + _ => Box::new(std::iter::empty()), + } + } +} + +impl Arbitrary for FullyStaticTy { + fn arbitrary(g: &mut Gen) -> FullyStaticTy { + const MAX_SIZE: u32 = 2; + FullyStaticTy(arbitrary_type(g, MAX_SIZE, true)) + } + + fn shrink(&self) -> Box> { + Box::new(self.0.shrink().map(FullyStaticTy)) + } +} + +pub(crate) fn intersection<'db>( + db: &'db TestDb, + tys: impl IntoIterator>, +) -> Type<'db> { + let mut builder = IntersectionBuilder::new(db); + for ty in tys { + builder = builder.add_positive(ty); + } + builder.build() +} + +pub(crate) fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { + UnionType::from_elements(db, tys) +} diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs new file mode 100644 index 0000000000000..a70a0c0dee08f --- /dev/null +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -0,0 +1,543 @@ +use std::{collections::BTreeMap, ops::Deref}; + +use itertools::Itertools; + +use ruff_python_ast::name::Name; + +use crate::{ + Db, FxOrderSet, + place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, + semantic_index::{place_table, use_def_map}, + types::{ + CallableType, ClassBase, ClassLiteral, KnownFunction, PropertyInstanceType, Signature, + Type, TypeMapping, TypeQualifiers, TypeRelation, TypeTransformer, TypeVarInstance, + cyclic::PairVisitor, + signatures::{Parameter, Parameters}, + }, +}; + +use super::TypeVarVariance; + +impl<'db> ClassLiteral<'db> { + /// Returns `Some` if this is a protocol class, `None` otherwise. + pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { + self.is_protocol(db).then_some(ProtocolClassLiteral(self)) + } +} + +/// Representation of a single `Protocol` class definition. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) struct ProtocolClassLiteral<'db>(ClassLiteral<'db>); + +impl<'db> ProtocolClassLiteral<'db> { + /// Returns the protocol members of this class. + /// + /// A protocol's members define the interface declared by the protocol. + /// They therefore determine how the protocol should behave with regards to + /// assignability and subtyping. + /// + /// The list of members consists of all bindings and declarations that take place + /// in the protocol's class body, except for a list of excluded attributes which should + /// not be taken into account. (This list includes `__init__` and `__new__`, which can + /// legally be defined on protocol classes but do not constitute protocol members.) + /// + /// It is illegal for a protocol class to have any instance attributes that are not declared + /// in the protocol's class body. If any are assigned to, they are not taken into account in + /// the protocol's list of members. + pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered(); + cached_protocol_interface(db, *self) + } + + pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::RuntimeCheckable) + } +} + +impl<'db> Deref for ProtocolClassLiteral<'db> { + type Target = ClassLiteral<'db>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// The interface of a protocol: the members of that protocol, and the types of those members. +/// +/// # Ordering +/// Ordering is based on the protocol interface member's salsa-assigned id and not on its members. +/// The id may change between runs, or when the protocol instance members was garbage collected and recreated. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub(super) struct ProtocolInterface<'db> { + #[returns(ref)] + inner: BTreeMap>, +} + +impl get_size2::GetSize for ProtocolInterface<'_> {} + +pub(super) fn walk_protocol_interface<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + interface: ProtocolInterface<'db>, + visitor: &mut V, +) { + for member in interface.members(db) { + walk_protocol_member(db, &member, visitor); + } +} + +impl<'db> ProtocolInterface<'db> { + /// Synthesize a new protocol interface with the given members. + /// + /// All created members will be covariant, read-only property members + /// rather than method members or mutable attribute members. + pub(super) fn with_property_members<'a, M>(db: &'db dyn Db, members: M) -> Self + where + M: IntoIterator)>, + { + let members: BTreeMap<_, _> = members + .into_iter() + .map(|(name, ty)| { + // Synthesize a read-only property (one that has a getter but no setter) + // which returns the specified type from its getter. + let property_getter_signature = Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]), + Some(ty.normalized(db)), + ); + let property_getter = CallableType::single(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + ( + Name::new(name), + ProtocolMemberData { + qualifiers: TypeQualifiers::default(), + kind: ProtocolMemberKind::Property(property), + }, + ) + }) + .collect(); + Self::new(db, members) + } + + fn empty(db: &'db dyn Db) -> Self { + Self::new(db, BTreeMap::default()) + } + + pub(super) fn members<'a>( + self, + db: &'db dyn Db, + ) -> impl ExactSizeIterator> + where + 'db: 'a, + { + self.inner(db).iter().map(|(name, data)| ProtocolMember { + name, + kind: data.kind, + qualifiers: data.qualifiers, + }) + } + + fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option> { + self.inner(db).get(name).map(|data| ProtocolMember { + name, + kind: data.kind, + qualifiers: data.qualifiers, + }) + } + + pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + self.member_by_name(db, name) + .map(|member| PlaceAndQualifiers { + place: Place::bound(member.ty()), + qualifiers: member.qualifiers(), + }) + .unwrap_or_else(|| Type::object(db).instance_member(db, name)) + } + + /// Return `true` if if all members on `self` are also members of `other`. + /// + /// TODO: this method should consider the types of the members as well as their names. + pub(super) fn is_sub_interface_of(self, db: &'db dyn Db, other: Self) -> bool { + self.inner(db) + .keys() + .all(|member_name| other.inner(db).contains_key(member_name)) + } + + pub(super) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self::new( + db, + self.inner(db) + .iter() + .map(|(name, data)| (name.clone(), data.normalized_impl(db, visitor))) + .collect::>(), + ) + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::new( + db, + self.inner(db) + .iter() + .map(|(name, data)| (name.clone(), data.materialize(db, variance))) + .collect::>(), + ) + } + + pub(super) fn specialized_and_normalized<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self::new( + db, + self.inner(db) + .iter() + .map(|(name, data)| { + ( + name.clone(), + data.apply_type_mapping(db, type_mapping).normalized(db), + ) + }) + .collect::>(), + ) + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for data in self.inner(db).values() { + data.find_legacy_typevars(db, typevars); + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)] +pub(super) struct ProtocolMemberData<'db> { + kind: ProtocolMemberKind<'db>, + qualifiers: TypeQualifiers, +} + +impl<'db> ProtocolMemberData<'db> { + fn normalized(&self, db: &'db dyn Db) -> Self { + self.normalized_impl(db, &mut TypeTransformer::default()) + } + + fn normalized_impl(&self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + Self { + kind: self.kind.normalized_impl(db, visitor), + qualifiers: self.qualifiers, + } + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + Self { + kind: self.kind.apply_type_mapping(db, type_mapping), + qualifiers: self.qualifiers, + } + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.kind.find_legacy_typevars(db, typevars); + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self { + kind: self.kind.materialize(db, variance), + qualifiers: self.qualifiers, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash)] +enum ProtocolMemberKind<'db> { + Method(CallableType<'db>), + Property(PropertyInstanceType<'db>), + Other(Type<'db>), +} + +impl<'db> ProtocolMemberKind<'db> { + fn normalized_impl(&self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + match self { + ProtocolMemberKind::Method(callable) => { + ProtocolMemberKind::Method(callable.normalized_impl(db, visitor)) + } + ProtocolMemberKind::Property(property) => { + ProtocolMemberKind::Property(property.normalized_impl(db, visitor)) + } + ProtocolMemberKind::Other(ty) => { + ProtocolMemberKind::Other(ty.normalized_impl(db, visitor)) + } + } + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + match self { + ProtocolMemberKind::Method(callable) => { + ProtocolMemberKind::Method(callable.apply_type_mapping(db, type_mapping)) + } + ProtocolMemberKind::Property(property) => { + ProtocolMemberKind::Property(property.apply_type_mapping(db, type_mapping)) + } + ProtocolMemberKind::Other(ty) => { + ProtocolMemberKind::Other(ty.apply_type_mapping(db, type_mapping)) + } + } + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + ProtocolMemberKind::Method(callable) => callable.find_legacy_typevars(db, typevars), + ProtocolMemberKind::Property(property) => property.find_legacy_typevars(db, typevars), + ProtocolMemberKind::Other(ty) => ty.find_legacy_typevars(db, typevars), + } + } + + fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + match self { + ProtocolMemberKind::Method(callable) => { + ProtocolMemberKind::Method(callable.materialize(db, variance)) + } + ProtocolMemberKind::Property(property) => { + ProtocolMemberKind::Property(property.materialize(db, variance)) + } + ProtocolMemberKind::Other(ty) => { + ProtocolMemberKind::Other(ty.materialize(db, variance)) + } + } + } +} + +/// A single member of a protocol interface. +#[derive(Debug, PartialEq, Eq)] +pub(super) struct ProtocolMember<'a, 'db> { + name: &'a str, + kind: ProtocolMemberKind<'db>, + qualifiers: TypeQualifiers, +} + +fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + member: &ProtocolMember<'_, 'db>, + visitor: &mut V, +) { + match member.kind { + ProtocolMemberKind::Method(method) => visitor.visit_callable_type(db, method), + ProtocolMemberKind::Property(property) => { + visitor.visit_property_instance_type(db, property); + } + ProtocolMemberKind::Other(ty) => visitor.visit_type(db, ty), + } +} + +impl<'a, 'db> ProtocolMember<'a, 'db> { + pub(super) fn name(&self) -> &'a str { + self.name + } + + pub(super) fn qualifiers(&self) -> TypeQualifiers { + self.qualifiers + } + + fn ty(&self) -> Type<'db> { + match &self.kind { + ProtocolMemberKind::Method(callable) => Type::Callable(*callable), + ProtocolMemberKind::Property(property) => Type::PropertyInstance(*property), + ProtocolMemberKind::Other(ty) => *ty, + } + } + + pub(super) fn has_disjoint_type_from( + &self, + db: &'db dyn Db, + other: Type<'db>, + visitor: &mut PairVisitor<'db>, + ) -> bool { + match &self.kind { + // TODO: implement disjointness for property/method members as well as attribute members + ProtocolMemberKind::Property(_) | ProtocolMemberKind::Method(_) => false, + ProtocolMemberKind::Other(ty) => { + visitor.visit((*ty, other), |v| ty.is_disjoint_from_impl(db, other, v)) + } + } + } + + /// Return `true` if `other` contains an attribute/method/property that satisfies + /// the part of the interface defined by this protocol member. + pub(super) fn is_satisfied_by( + &self, + db: &'db dyn Db, + other: Type<'db>, + relation: TypeRelation, + ) -> bool { + let Place::Type(attribute_type, Boundness::Bound) = other.member(db, self.name).place + else { + return false; + }; + + match &self.kind { + // TODO: consider the types of the attribute on `other` for property/method members + ProtocolMemberKind::Method(_) | ProtocolMemberKind::Property(_) => true, + ProtocolMemberKind::Other(member_type) => { + member_type.has_relation_to(db, attribute_type, relation) + && attribute_type.has_relation_to(db, *member_type, relation) + } + } + } +} + +/// Returns `true` if a declaration or binding to a given name in a protocol class body +/// should be excluded from the list of protocol members of that class. +/// +/// The list of excluded members is subject to change between Python versions, +/// especially for dunders, but it probably doesn't matter *too* much if this +/// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa +/// () +fn excluded_from_proto_members(member: &str) -> bool { + matches!( + member, + "_is_protocol" + | "__non_callable_proto_members__" + | "__static_attributes__" + | "__orig_class__" + | "__match_args__" + | "__weakref__" + | "__doc__" + | "__parameters__" + | "__module__" + | "_MutableMapping__marker" + | "__slots__" + | "__dict__" + | "__new__" + | "__protocol_attrs__" + | "__init__" + | "__class_getitem__" + | "__firstlineno__" + | "__abstractmethods__" + | "__orig_bases__" + | "_is_runtime_protocol" + | "__subclasshook__" + | "__type_params__" + | "__annotations__" + | "__annotate__" + | "__annotate_func__" + | "__annotations_cache__" + ) || member.starts_with("_abc_") +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum BoundOnClass { + Yes, + No, +} + +/// Inner Salsa query for [`ProtocolClassLiteral::interface`]. +#[salsa::tracked(cycle_fn=proto_interface_cycle_recover, cycle_initial=proto_interface_cycle_initial, heap_size=get_size2::GetSize::get_heap_size)] +fn cached_protocol_interface<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, +) -> ProtocolInterface<'db> { + let mut members = BTreeMap::default(); + + for parent_protocol in class + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .filter_map(|class| class.class_literal(db).0.into_protocol_class(db)) + { + let parent_scope = parent_protocol.body_scope(db); + let use_def_map = use_def_map(db, parent_scope); + let place_table = place_table(db, parent_scope); + + members.extend( + use_def_map + .all_end_of_scope_declarations() + .flat_map(|(place_id, declarations)| { + place_from_declarations(db, declarations).map(|place| (place_id, place)) + }) + .filter_map(|(place_id, place)| { + place + .place + .ignore_possibly_unbound() + .map(|ty| (place_id, ty, place.qualifiers, BoundOnClass::No)) + }) + // Bindings in the class body that are not declared in the class body + // are not valid protocol members, and we plan to emit diagnostics for them + // elsewhere. Invalid or not, however, it's important that we still consider + // them to be protocol members. The implementation of `issubclass()` and + // `isinstance()` for runtime-checkable protocols considers them to be protocol + // members at runtime, and it's important that we accurately understand + // type narrowing that uses `isinstance()` or `issubclass()` with + // runtime-checkable protocols. + .chain(use_def_map.all_end_of_scope_bindings().filter_map( + |(place_id, bindings)| { + place_from_bindings(db, bindings) + .ignore_possibly_unbound() + .map(|ty| (place_id, ty, TypeQualifiers::default(), BoundOnClass::Yes)) + }, + )) + .filter_map(|(place_id, member, qualifiers, bound_on_class)| { + Some(( + place_table.place_expr(place_id).as_name()?, + member, + qualifiers, + bound_on_class, + )) + }) + .filter(|(name, _, _, _)| !excluded_from_proto_members(name)) + .map(|(name, ty, qualifiers, bound_on_class)| { + let kind = match (ty, bound_on_class) { + // TODO: if the getter or setter is a function literal, we should + // upcast it to a `CallableType` so that two protocols with identical property + // members are recognized as equivalent. + (Type::PropertyInstance(property), _) => { + ProtocolMemberKind::Property(property) + } + (Type::Callable(callable), BoundOnClass::Yes) + if callable.is_function_like(db) => + { + ProtocolMemberKind::Method(callable) + } + (Type::FunctionLiteral(function), BoundOnClass::Yes) => { + ProtocolMemberKind::Method(function.into_callable_type(db)) + } + _ => ProtocolMemberKind::Other(ty), + }; + + let member = ProtocolMemberData { kind, qualifiers }; + (name.clone(), member) + }), + ); + } + + ProtocolInterface::new(db, members) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn proto_interface_cycle_recover<'db>( + _db: &dyn Db, + _value: &ProtocolInterface<'db>, + _count: u32, + _class: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn proto_interface_cycle_initial<'db>( + db: &'db dyn Db, + _class: ClassLiteral<'db>, +) -> ProtocolInterface<'db> { + ProtocolInterface::empty(db) +} diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs new file mode 100644 index 0000000000000..5322f09aaf14a --- /dev/null +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -0,0 +1,1856 @@ +//! _Signatures_ describe the expected parameters and return type of a function or other callable. +//! Overloads and unions add complexity to this simple description. +//! +//! In a call expression, the type of the callable might be a union of several types. The call must +//! be compatible with _all_ of these types, since at runtime the callable might be an instance of +//! any of them. +//! +//! Each of the atomic types in the union must be callable. Each callable might be _overloaded_, +//! containing multiple _overload signatures_, each of which describes a different combination of +//! argument types and return types. For each callable type in the union, the call expression's +//! arguments must match _at least one_ overload. + +use std::{collections::HashMap, slice::Iter}; + +use itertools::EitherOrBoth; +use smallvec::{SmallVec, smallvec}; + +use super::{DynamicType, Type, TypeTransformer, TypeVarVariance, definition_expression_type}; +use crate::semantic_index::definition::Definition; +use crate::types::generics::{GenericContext, walk_generic_context}; +use crate::types::{TypeMapping, TypeRelation, TypeVarInstance, todo_type}; +use crate::{Db, FxOrderSet}; +use ruff_python_ast::{self as ast, name::Name}; + +/// The signature of a single callable. If the callable is overloaded, there is a separate +/// [`Signature`] for each overload. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub struct CallableSignature<'db> { + /// The signatures of each overload of this callable. Will be empty if the type is not + /// callable. + pub(crate) overloads: SmallVec<[Signature<'db>; 1]>, +} + +impl<'db> CallableSignature<'db> { + pub(crate) fn single(signature: Signature<'db>) -> Self { + Self { + overloads: smallvec![signature], + } + } + + /// Creates a new `CallableSignature` from an iterator of [`Signature`]s. Returns a + /// non-callable signature if the iterator is empty. + pub(crate) fn from_overloads(overloads: I) -> Self + where + I: IntoIterator>, + { + Self { + overloads: overloads.into_iter().collect(), + } + } + + pub(crate) fn iter(&self) -> std::slice::Iter<'_, Signature<'db>> { + self.overloads.iter() + } + + pub(super) fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::from_overloads( + self.overloads + .iter() + .map(|signature| signature.materialize(db, variance)), + ) + } + + pub(crate) fn normalized_impl( + &self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self::from_overloads( + self.overloads + .iter() + .map(|signature| signature.normalized_impl(db, visitor)), + ) + } + + pub(crate) fn apply_type_mapping<'a>( + &self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self::from_overloads( + self.overloads + .iter() + .map(|signature| signature.apply_type_mapping(db, type_mapping)), + ) + } + + pub(crate) fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for signature in &self.overloads { + signature.find_legacy_typevars(db, typevars); + } + } + + pub(crate) fn bind_self(&self) -> Self { + Self { + overloads: self.overloads.iter().map(Signature::bind_self).collect(), + } + } + + pub(crate) fn has_relation_to( + &self, + db: &'db dyn Db, + other: &Self, + relation: TypeRelation, + ) -> bool { + match relation { + TypeRelation::Subtyping => self.is_subtype_of(db, other), + TypeRelation::Assignability => self.is_assignable_to(db, other), + } + } + + /// Check whether this callable type is a subtype of another callable type. + /// + /// See [`Type::is_subtype_of`] for more details. + pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool { + Self::has_relation_to_impl( + db, + &self.overloads, + &other.overloads, + TypeRelation::Subtyping, + ) + } + + /// Check whether this callable type is assignable to another callable type. + /// + /// See [`Type::is_assignable_to`] for more details. + pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool { + Self::has_relation_to_impl( + db, + &self.overloads, + &other.overloads, + TypeRelation::Assignability, + ) + } + + /// Implementation of subtyping and assignability between two, possible overloaded, callable + /// types. + fn has_relation_to_impl( + db: &'db dyn Db, + self_signatures: &[Signature<'db>], + other_signatures: &[Signature<'db>], + relation: TypeRelation, + ) -> bool { + match (self_signatures, other_signatures) { + ([self_signature], [other_signature]) => { + // Base case: both callable types contain a single signature. + self_signature.has_relation_to(db, other_signature, relation) + } + + // `self` is possibly overloaded while `other` is definitely not overloaded. + (_, [_]) => self_signatures.iter().any(|self_signature| { + Self::has_relation_to_impl( + db, + std::slice::from_ref(self_signature), + other_signatures, + relation, + ) + }), + + // `self` is definitely not overloaded while `other` is possibly overloaded. + ([_], _) => other_signatures.iter().all(|other_signature| { + Self::has_relation_to_impl( + db, + self_signatures, + std::slice::from_ref(other_signature), + relation, + ) + }), + + // `self` is definitely overloaded while `other` is possibly overloaded. + (_, _) => other_signatures.iter().all(|other_signature| { + Self::has_relation_to_impl( + db, + self_signatures, + std::slice::from_ref(other_signature), + relation, + ) + }), + } + } + + /// Check whether this callable type is equivalent to another callable type. + /// + /// See [`Type::is_equivalent_to`] for more details. + pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + match (self.overloads.as_slice(), other.overloads.as_slice()) { + ([self_signature], [other_signature]) => { + // Common case: both callable types contain a single signature, use the custom + // equivalence check instead of delegating it to the subtype check. + self_signature.is_equivalent_to(db, other_signature) + } + (_, _) => { + if self == other { + return true; + } + self.is_subtype_of(db, other) && other.is_subtype_of(db, self) + } + } + } +} + +impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { + type Item = &'a Signature<'db>; + type IntoIter = std::slice::Iter<'a, Signature<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// The signature of one of the overloads of a callable. +#[derive(Clone, Debug, salsa::Update, get_size2::GetSize)] +pub struct Signature<'db> { + /// The generic context for this overload, if it is generic. + pub(crate) generic_context: Option>, + + /// The inherited generic context, if this function is a class method being used to infer the + /// specialization of its generic class. If the method is itself generic, this is in addition + /// to its own generic context. + pub(crate) inherited_generic_context: Option>, + + /// The original definition associated with this function, if available. + /// This is useful for locating and extracting docstring information for the signature. + pub(crate) definition: Option>, + + /// Parameters, in source order. + /// + /// The ordering of parameters in a valid signature must be: first positional-only parameters, + /// then positional-or-keyword, then optionally the variadic parameter, then keyword-only + /// parameters, and last, optionally the variadic keywords parameter. Parameters with defaults + /// must come after parameters without defaults. + /// + /// We may get invalid signatures, though, and need to handle them without panicking. + parameters: Parameters<'db>, + + /// Annotated return type, if any. + pub(crate) return_ty: Option>, +} + +pub(super) fn walk_signature<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + signature: &Signature<'db>, + visitor: &mut V, +) { + if let Some(generic_context) = &signature.generic_context { + walk_generic_context(db, *generic_context, visitor); + } + if let Some(inherited_generic_context) = &signature.inherited_generic_context { + walk_generic_context(db, *inherited_generic_context, visitor); + } + // By default we usually don't visit the type of the default value, + // as it isn't relevant to most things + for parameter in &signature.parameters { + if let Some(ty) = parameter.annotated_type() { + visitor.visit_type(db, ty); + } + } + if let Some(return_ty) = &signature.return_ty { + visitor.visit_type(db, *return_ty); + } +} + +impl<'db> Signature<'db> { + pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option>) -> Self { + Self { + generic_context: None, + inherited_generic_context: None, + definition: None, + parameters, + return_ty, + } + } + + pub(crate) fn new_generic( + generic_context: Option>, + parameters: Parameters<'db>, + return_ty: Option>, + ) -> Self { + Self { + generic_context, + inherited_generic_context: None, + definition: None, + parameters, + return_ty, + } + } + + /// Return a signature for a dynamic callable + pub(crate) fn dynamic(signature_type: Type<'db>) -> Self { + Signature { + generic_context: None, + inherited_generic_context: None, + definition: None, + parameters: Parameters::gradual_form(), + return_ty: Some(signature_type), + } + } + + /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo + #[allow(unused_variables)] // 'reason' only unused in debug builds + pub(crate) fn todo(reason: &'static str) -> Self { + let signature_type = todo_type!(reason); + Signature { + generic_context: None, + inherited_generic_context: None, + definition: None, + parameters: Parameters::todo(), + return_ty: Some(signature_type), + } + } + + /// Return a typed signature from a function definition. + pub(super) fn from_function( + db: &'db dyn Db, + generic_context: Option>, + inherited_generic_context: Option>, + definition: Definition<'db>, + function_node: &ast::StmtFunctionDef, + ) -> Self { + let parameters = + Parameters::from_parameters(db, definition, function_node.parameters.as_ref()); + let return_ty = function_node.returns.as_ref().map(|returns| { + if function_node.is_async { + todo_type!("generic types.CoroutineType") + } else { + definition_expression_type(db, definition, returns.as_ref()) + } + }); + let legacy_generic_context = + GenericContext::from_function_params(db, ¶meters, return_ty); + + if generic_context.is_some() && legacy_generic_context.is_some() { + // TODO: Raise a diagnostic! + } + + Self { + generic_context: generic_context.or(legacy_generic_context), + inherited_generic_context, + definition: Some(definition), + parameters, + return_ty, + } + } + + /// Returns the signature which accepts any parameters and returns an `Unknown` type. + pub(crate) fn unknown() -> Self { + Self::new(Parameters::unknown(), Some(Type::unknown())) + } + + /// Return the "bottom" signature, subtype of all other fully-static signatures. + pub(crate) fn bottom(db: &'db dyn Db) -> Self { + Self::new(Parameters::object(db), Some(Type::Never)) + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self { + generic_context: self.generic_context, + inherited_generic_context: self.inherited_generic_context, + definition: self.definition, + // Parameters are at contravariant position, so the variance is flipped. + parameters: self.parameters.materialize(db, variance.flip()), + return_ty: Some( + self.return_ty + .unwrap_or(Type::unknown()) + .materialize(db, variance), + ), + } + } + + pub(crate) fn normalized_impl( + &self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self { + generic_context: self + .generic_context + .map(|ctx| ctx.normalized_impl(db, visitor)), + inherited_generic_context: self + .inherited_generic_context + .map(|ctx| ctx.normalized_impl(db, visitor)), + definition: self.definition, + parameters: self + .parameters + .iter() + .map(|param| param.normalized_impl(db, visitor)) + .collect(), + return_ty: self + .return_ty + .map(|return_ty| return_ty.normalized_impl(db, visitor)), + } + } + + pub(crate) fn apply_type_mapping<'a>( + &self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self { + generic_context: self.generic_context, + inherited_generic_context: self.inherited_generic_context, + definition: self.definition, + parameters: self.parameters.apply_type_mapping(db, type_mapping), + return_ty: self + .return_ty + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + } + } + + pub(crate) fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for param in &self.parameters { + if let Some(ty) = param.annotated_type() { + ty.find_legacy_typevars(db, typevars); + } + if let Some(ty) = param.default_type() { + ty.find_legacy_typevars(db, typevars); + } + } + if let Some(ty) = self.return_ty { + ty.find_legacy_typevars(db, typevars); + } + } + + /// Return the parameters in this signature. + pub(crate) fn parameters(&self) -> &Parameters<'db> { + &self.parameters + } + + /// Return the definition associated with this signature, if any. + pub(crate) fn definition(&self) -> Option> { + self.definition + } + + pub(crate) fn bind_self(&self) -> Self { + Self { + generic_context: self.generic_context, + inherited_generic_context: self.inherited_generic_context, + definition: self.definition, + parameters: Parameters::new(self.parameters().iter().skip(1).cloned()), + return_ty: self.return_ty, + } + } + + /// Return `true` if `self` has exactly the same set of possible static materializations as + /// `other` (if `self` represents the same set of possible sets of possible runtime objects as + /// `other`). + pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { + let check_types = |self_type: Option>, other_type: Option>| { + self_type + .unwrap_or(Type::unknown()) + .is_equivalent_to(db, other_type.unwrap_or(Type::unknown())) + }; + + if self.parameters.is_gradual() != other.parameters.is_gradual() { + return false; + } + + if self.parameters.len() != other.parameters.len() { + return false; + } + + if !check_types(self.return_ty, other.return_ty) { + return false; + } + + for (self_parameter, other_parameter) in self.parameters.iter().zip(&other.parameters) { + match (self_parameter.kind(), other_parameter.kind()) { + ( + ParameterKind::PositionalOnly { + default_type: self_default, + .. + }, + ParameterKind::PositionalOnly { + default_type: other_default, + .. + }, + ) if self_default.is_some() == other_default.is_some() => {} + + ( + ParameterKind::PositionalOrKeyword { + name: self_name, + default_type: self_default, + }, + ParameterKind::PositionalOrKeyword { + name: other_name, + default_type: other_default, + }, + ) if self_default.is_some() == other_default.is_some() + && self_name == other_name => {} + + (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {} + + ( + ParameterKind::KeywordOnly { + name: self_name, + default_type: self_default, + }, + ParameterKind::KeywordOnly { + name: other_name, + default_type: other_default, + }, + ) if self_default.is_some() == other_default.is_some() + && self_name == other_name => {} + + (ParameterKind::KeywordVariadic { .. }, ParameterKind::KeywordVariadic { .. }) => {} + + _ => return false, + } + + if !check_types( + self_parameter.annotated_type(), + other_parameter.annotated_type(), + ) { + return false; + } + } + + true + } + + /// Implementation of subtyping and assignability for signature. + fn has_relation_to( + &self, + db: &'db dyn Db, + other: &Signature<'db>, + relation: TypeRelation, + ) -> bool { + /// A helper struct to zip two slices of parameters together that provides control over the + /// two iterators individually. It also keeps track of the current parameter in each + /// iterator. + struct ParametersZip<'a, 'db> { + current_self: Option<&'a Parameter<'db>>, + current_other: Option<&'a Parameter<'db>>, + iter_self: Iter<'a, Parameter<'db>>, + iter_other: Iter<'a, Parameter<'db>>, + } + + impl<'a, 'db> ParametersZip<'a, 'db> { + /// Move to the next parameter in both the `self` and `other` parameter iterators, + /// [`None`] if both iterators are exhausted. + fn next(&mut self) -> Option, &'a Parameter<'db>>> { + match (self.next_self(), self.next_other()) { + (Some(self_param), Some(other_param)) => { + Some(EitherOrBoth::Both(self_param, other_param)) + } + (Some(self_param), None) => Some(EitherOrBoth::Left(self_param)), + (None, Some(other_param)) => Some(EitherOrBoth::Right(other_param)), + (None, None) => None, + } + } + + /// Move to the next parameter in the `self` parameter iterator, [`None`] if the + /// iterator is exhausted. + fn next_self(&mut self) -> Option<&'a Parameter<'db>> { + self.current_self = self.iter_self.next(); + self.current_self + } + + /// Move to the next parameter in the `other` parameter iterator, [`None`] if the + /// iterator is exhausted. + fn next_other(&mut self) -> Option<&'a Parameter<'db>> { + self.current_other = self.iter_other.next(); + self.current_other + } + + /// Peek at the next parameter in the `other` parameter iterator without consuming it. + fn peek_other(&mut self) -> Option<&'a Parameter<'db>> { + self.iter_other.clone().next() + } + + /// Consumes the `ParametersZip` and returns a two-element tuple containing the + /// remaining parameters in the `self` and `other` iterators respectively. + /// + /// The returned iterators starts with the current parameter, if any, followed by the + /// remaining parameters in the respective iterators. + fn into_remaining( + self, + ) -> ( + impl Iterator>, + impl Iterator>, + ) { + ( + self.current_self.into_iter().chain(self.iter_self), + self.current_other.into_iter().chain(self.iter_other), + ) + } + } + + let check_types = |type1: Option>, type2: Option>| { + type1.unwrap_or(Type::unknown()).has_relation_to( + db, + type2.unwrap_or(Type::unknown()), + relation, + ) + }; + + // Return types are covariant. + if !check_types(self.return_ty, other.return_ty) { + return false; + } + + // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, + // **kwargs: object). + if other.parameters.is_gradual() + && self + .parameters + .variadic() + .is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db))) + && self + .parameters + .keyword_variadic() + .is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db))) + { + return true; + } + + // If either of the parameter lists is gradual (`...`), then it is assignable to and from + // any other parameter list, but not a subtype or supertype of any other parameter list. + if self.parameters.is_gradual() || other.parameters.is_gradual() { + return relation.is_assignability(); + } + + let mut parameters = ParametersZip { + current_self: None, + current_other: None, + iter_self: self.parameters.iter(), + iter_other: other.parameters.iter(), + }; + + // Collect all the standard parameters that have only been matched against a variadic + // parameter which means that the keyword variant is still unmatched. + let mut other_keywords = Vec::new(); + + loop { + let Some(next_parameter) = parameters.next() else { + // All parameters have been checked or both the parameter lists were empty. In + // either case, `self` is a subtype of `other`. + return true; + }; + + match next_parameter { + EitherOrBoth::Left(self_parameter) => match self_parameter.kind() { + ParameterKind::KeywordOnly { .. } | ParameterKind::KeywordVariadic { .. } + if !other_keywords.is_empty() => + { + // If there are any unmatched keyword parameters in `other`, they need to + // be checked against the keyword-only / keyword-variadic parameters that + // will be done after this loop. + break; + } + ParameterKind::PositionalOnly { default_type, .. } + | ParameterKind::PositionalOrKeyword { default_type, .. } + | ParameterKind::KeywordOnly { default_type, .. } => { + // For `self <: other` to be valid, if there are no more parameters in + // `other`, then the non-variadic parameters in `self` must have a default + // value. + if default_type.is_none() { + return false; + } + } + ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => { + // Variadic parameters don't have any restrictions in this context, so + // we'll just continue to the next parameter set. + } + }, + + EitherOrBoth::Right(_) => { + // If there are more parameters in `other` than in `self`, then `self` is not a + // subtype of `other`. + return false; + } + + EitherOrBoth::Both(self_parameter, other_parameter) => { + match (self_parameter.kind(), other_parameter.kind()) { + ( + ParameterKind::PositionalOnly { + default_type: self_default, + .. + } + | ParameterKind::PositionalOrKeyword { + default_type: self_default, + .. + }, + ParameterKind::PositionalOnly { + default_type: other_default, + .. + }, + ) => { + if self_default.is_none() && other_default.is_some() { + return false; + } + if !check_types( + other_parameter.annotated_type(), + self_parameter.annotated_type(), + ) { + return false; + } + } + + ( + ParameterKind::PositionalOrKeyword { + name: self_name, + default_type: self_default, + }, + ParameterKind::PositionalOrKeyword { + name: other_name, + default_type: other_default, + }, + ) => { + if self_name != other_name { + return false; + } + // The following checks are the same as positional-only parameters. + if self_default.is_none() && other_default.is_some() { + return false; + } + if !check_types( + other_parameter.annotated_type(), + self_parameter.annotated_type(), + ) { + return false; + } + } + + ( + ParameterKind::Variadic { .. }, + ParameterKind::PositionalOnly { .. } + | ParameterKind::PositionalOrKeyword { .. }, + ) => { + if !check_types( + other_parameter.annotated_type(), + self_parameter.annotated_type(), + ) { + return false; + } + + if matches!( + other_parameter.kind(), + ParameterKind::PositionalOrKeyword { .. } + ) { + other_keywords.push(other_parameter); + } + + // We've reached a variadic parameter in `self` which means there can + // be no more positional parameters after this in a valid AST. But, the + // current parameter in `other` is a positional-only which means there + // can be more positional parameters after this which could be either + // more positional-only parameters, standard parameters or a variadic + // parameter. + // + // So, any remaining positional parameters in `other` would need to be + // checked against the variadic parameter in `self`. This loop does + // that by only moving the `other` iterator forward. + loop { + let Some(other_parameter) = parameters.peek_other() else { + break; + }; + match other_parameter.kind() { + ParameterKind::PositionalOrKeyword { .. } => { + other_keywords.push(other_parameter); + } + ParameterKind::PositionalOnly { .. } + | ParameterKind::Variadic { .. } => {} + _ => { + // Any other parameter kind cannot be checked against a + // variadic parameter and is deferred to the next iteration. + break; + } + } + if !check_types( + other_parameter.annotated_type(), + self_parameter.annotated_type(), + ) { + return false; + } + parameters.next_other(); + } + } + + (ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => { + if !check_types( + other_parameter.annotated_type(), + self_parameter.annotated_type(), + ) { + return false; + } + } + + ( + _, + ParameterKind::KeywordOnly { .. } + | ParameterKind::KeywordVariadic { .. }, + ) => { + // Keyword parameters are not considered in this loop as the order of + // parameters is not important for them and so they are checked by + // doing name-based lookups. + break; + } + + _ => return false, + } + } + } + } + + // At this point, the remaining parameters in `other` are keyword-only or keyword variadic. + // But, `self` could contain any unmatched positional parameters. + let (self_parameters, other_parameters) = parameters.into_remaining(); + + // Collect all the keyword-only parameters and the unmatched standard parameters. + let mut self_keywords = HashMap::new(); + + // Type of the variadic keyword parameter in `self`. + // + // This is a nested option where the outer option represents the presence of a keyword + // variadic parameter in `self` and the inner option represents the annotated type of the + // keyword variadic parameter. + let mut self_keyword_variadic: Option>> = None; + + for self_parameter in self_parameters { + match self_parameter.kind() { + ParameterKind::KeywordOnly { name, .. } + | ParameterKind::PositionalOrKeyword { name, .. } => { + self_keywords.insert(name.clone(), self_parameter); + } + ParameterKind::KeywordVariadic { .. } => { + self_keyword_variadic = Some(self_parameter.annotated_type()); + } + ParameterKind::PositionalOnly { .. } => { + // These are the unmatched positional-only parameters in `self` from the + // previous loop. They cannot be matched against any parameter in `other` which + // only contains keyword-only and keyword-variadic parameters so the subtype + // relation is invalid. + return false; + } + ParameterKind::Variadic { .. } => {} + } + } + + for other_parameter in other_keywords.into_iter().chain(other_parameters) { + match other_parameter.kind() { + ParameterKind::KeywordOnly { + name: other_name, + default_type: other_default, + } + | ParameterKind::PositionalOrKeyword { + name: other_name, + default_type: other_default, + } => { + if let Some(self_parameter) = self_keywords.remove(other_name) { + match self_parameter.kind() { + ParameterKind::PositionalOrKeyword { + default_type: self_default, + .. + } + | ParameterKind::KeywordOnly { + default_type: self_default, + .. + } => { + if self_default.is_none() && other_default.is_some() { + return false; + } + if !check_types( + other_parameter.annotated_type(), + self_parameter.annotated_type(), + ) { + return false; + } + } + _ => unreachable!( + "`self_keywords` should only contain keyword-only or standard parameters" + ), + } + } else if let Some(self_keyword_variadic_type) = self_keyword_variadic { + if !check_types( + other_parameter.annotated_type(), + self_keyword_variadic_type, + ) { + return false; + } + } else { + return false; + } + } + ParameterKind::KeywordVariadic { .. } => { + let Some(self_keyword_variadic_type) = self_keyword_variadic else { + // For a `self <: other` relationship, if `other` has a keyword variadic + // parameter, `self` must also have a keyword variadic parameter. + return false; + }; + if !check_types(other_parameter.annotated_type(), self_keyword_variadic_type) { + return false; + } + } + _ => { + // This can only occur in case of a syntax error. + return false; + } + } + } + + // If there are still unmatched keyword parameters from `self`, then they should be + // optional otherwise the subtype relation is invalid. + for (_, self_parameter) in self_keywords { + if self_parameter.default_type().is_none() { + return false; + } + } + + true + } + + /// Create a new signature with the given definition. + pub(crate) fn with_definition(self, definition: Option>) -> Self { + Self { definition, ..self } + } +} + +// Manual implementations of PartialEq, Eq, and Hash that exclude the definition field +// since the definition is not relevant for type equality/equivalence +impl PartialEq for Signature<'_> { + fn eq(&self, other: &Self) -> bool { + self.generic_context == other.generic_context + && self.inherited_generic_context == other.inherited_generic_context + && self.parameters == other.parameters + && self.return_ty == other.return_ty + } +} + +impl Eq for Signature<'_> {} + +impl std::hash::Hash for Signature<'_> { + fn hash(&self, state: &mut H) { + self.generic_context.hash(state); + self.inherited_generic_context.hash(state); + self.parameters.hash(state); + self.return_ty.hash(state); + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) struct Parameters<'db> { + // TODO: use SmallVec here once invariance bug is fixed + value: Vec>, + + /// Whether this parameter list represents a gradual form using `...` as the only parameter. + /// + /// If this is `true`, the `value` will still contain the variadic and keyword-variadic + /// parameters. + /// + /// Per [the typing specification], any signature with a variadic and a keyword-variadic + /// argument, both annotated (explicitly or implicitly) as `Any` or `Unknown`, is considered + /// equivalent to `...`. + /// + /// The display implementation utilizes this flag to use `...` instead of displaying the + /// individual variadic and keyword-variadic parameters. + /// + /// Note: This flag can also result from invalid forms of `Callable` annotations. + /// + /// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number + /// of required positional parameters followed by a gradual form. Our representation will need + /// some adjustments to represent that. + /// + /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable + is_gradual: bool, +} + +impl<'db> Parameters<'db> { + pub(crate) fn new(parameters: impl IntoIterator>) -> Self { + let value: Vec> = parameters.into_iter().collect(); + let is_gradual = value.len() == 2 + && value + .iter() + .any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())) + && value.iter().any(|p| { + p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()) + }); + Self { value, is_gradual } + } + + /// Create an empty parameter list. + pub(crate) fn empty() -> Self { + Self { + value: Vec::new(), + is_gradual: false, + } + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + if self.is_gradual { + Parameters::object(db) + } else { + Parameters::new( + self.iter() + .map(|parameter| parameter.materialize(db, variance)), + ) + } + } + + pub(crate) fn as_slice(&self) -> &[Parameter<'db>] { + self.value.as_slice() + } + + pub(crate) const fn is_gradual(&self) -> bool { + self.is_gradual + } + + /// Return todo parameters: (*args: Todo, **kwargs: Todo) + pub(crate) fn todo() -> Self { + Self { + value: vec![ + 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: true, + } + } + + /// Return parameters that represents a gradual form using `...` as the only parameter. + /// + /// Internally, this is represented as `(*Any, **Any)` that accepts parameters of type [`Any`]. + /// + /// [`Any`]: crate::types::DynamicType::Any + pub(crate) fn gradual_form() -> Self { + Self { + value: vec![ + 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, + } + } + + /// Return parameters that represents an unknown list of parameters. + /// + /// Internally, this is represented as `(*Unknown, **Unknown)` that accepts parameters of type + /// [`Unknown`]. + /// + /// [`Unknown`]: crate::types::DynamicType::Unknown + pub(crate) fn unknown() -> Self { + Self { + value: vec![ + 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, + } + } + + /// Return parameters that represents `(*args: object, **kwargs: object)`. + pub(crate) fn object(db: &'db dyn Db) -> Self { + Self { + value: vec![ + Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object(db)), + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::object(db)), + ], + is_gradual: false, + } + } + + fn from_parameters( + db: &'db dyn Db, + definition: Definition<'db>, + parameters: &ast::Parameters, + ) -> Self { + let ast::Parameters { + posonlyargs, + args, + vararg, + kwonlyargs, + kwarg, + range: _, + node_index: _, + } = parameters; + let default_type = |param: &ast::ParameterWithDefault| { + param + .default() + .map(|default| definition_expression_type(db, definition, default)) + }; + let positional_only = posonlyargs.iter().map(|arg| { + Parameter::from_node_and_kind( + db, + definition, + &arg.parameter, + ParameterKind::PositionalOnly { + name: Some(arg.parameter.name.id.clone()), + default_type: default_type(arg), + }, + ) + }); + let positional_or_keyword = args.iter().map(|arg| { + Parameter::from_node_and_kind( + db, + definition, + &arg.parameter, + ParameterKind::PositionalOrKeyword { + name: arg.parameter.name.id.clone(), + default_type: default_type(arg), + }, + ) + }); + let variadic = vararg.as_ref().map(|arg| { + Parameter::from_node_and_kind( + db, + definition, + arg, + ParameterKind::Variadic { + name: arg.name.id.clone(), + }, + ) + }); + let keyword_only = kwonlyargs.iter().map(|arg| { + Parameter::from_node_and_kind( + db, + definition, + &arg.parameter, + ParameterKind::KeywordOnly { + name: arg.parameter.name.id.clone(), + default_type: default_type(arg), + }, + ) + }); + let keywords = kwarg.as_ref().map(|arg| { + Parameter::from_node_and_kind( + db, + definition, + arg, + ParameterKind::KeywordVariadic { + name: arg.name.id.clone(), + }, + ) + }); + Self::new( + positional_only + .chain(positional_or_keyword) + .chain(variadic) + .chain(keyword_only) + .chain(keywords), + ) + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + Self { + value: self + .value + .iter() + .map(|param| param.apply_type_mapping(db, type_mapping)) + .collect(), + is_gradual: self.is_gradual, + } + } + + pub(crate) fn len(&self) -> usize { + self.value.len() + } + + pub(crate) fn iter(&self) -> std::slice::Iter> { + self.value.iter() + } + + /// Iterate initial positional parameters, not including variadic parameter, if any. + /// + /// For a valid signature, this will be all positional parameters. In an invalid signature, + /// there could be non-initial positional parameters; effectively, we just won't consider those + /// to be positional, which is fine. + pub(crate) fn positional(&self) -> impl Iterator> { + self.iter().take_while(|param| param.is_positional()) + } + + /// Return parameter at given index, or `None` if index is out-of-range. + pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> { + self.value.get(index) + } + + /// Return positional parameter at given index, or `None` if `index` is out of range. + /// + /// Does not return variadic parameter. + pub(crate) fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> { + self.get(index) + .and_then(|parameter| parameter.is_positional().then_some(parameter)) + } + + /// Return the variadic parameter (`*args`), if any, and its index, or `None`. + pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> { + self.iter() + .enumerate() + .find(|(_, parameter)| parameter.is_variadic()) + } + + /// Return parameter (with index) for given name, or `None` if no such parameter. + /// + /// Does not return keywords (`**kwargs`) parameter. + /// + /// In an invalid signature, there could be multiple parameters with the same name; we will + /// just return the first that matches. + pub(crate) fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { + self.iter() + .enumerate() + .find(|(_, parameter)| parameter.callable_by_name(name)) + } + + /// Return the keywords parameter (`**kwargs`), if any, and its index, or `None`. + pub(crate) fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> { + self.iter() + .enumerate() + .rfind(|(_, parameter)| parameter.is_keyword_variadic()) + } +} + +impl<'db, 'a> IntoIterator for &'a Parameters<'db> { + type Item = &'a Parameter<'db>; + type IntoIter = std::slice::Iter<'a, Parameter<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.value.iter() + } +} + +impl<'db> FromIterator> for Parameters<'db> { + fn from_iter>>(iter: T) -> Self { + Self::new(iter) + } +} + +impl<'db> std::ops::Index for Parameters<'db> { + type Output = Parameter<'db>; + + fn index(&self, index: usize) -> &Self::Output { + &self.value[index] + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) struct Parameter<'db> { + /// Annotated type of the parameter. + annotated_type: Option>, + + kind: ParameterKind<'db>, + pub(crate) form: ParameterForm, +} + +impl<'db> Parameter<'db> { + pub(crate) fn positional_only(name: Option) -> 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 materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self { + annotated_type: Some( + self.annotated_type + .unwrap_or(Type::unknown()) + .materialize(db, variance), + ), + kind: self.kind.clone(), + form: self.form, + } + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + Self { + annotated_type: self + .annotated_type + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + kind: self.kind.apply_type_mapping(db, type_mapping), + form: self.form, + } + } + + /// Strip information from the parameter so that two equivalent parameters compare equal. + /// Normalize nested unions and intersections in the annotated type, if any. + /// + /// See [`Type::normalized`] for more details. + pub(crate) fn normalized_impl( + &self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + let Parameter { + annotated_type, + kind, + form, + } = self; + + // Ensure unions and intersections are ordered in the annotated type (if there is one). + // Ensure that a parameter without an annotation is treated equivalently to a parameter + // with a dynamic type as its annotation. (We must use `Any` here as all dynamic types + // normalize to `Any`.) + let annotated_type = annotated_type + .map(|ty| ty.normalized_impl(db, visitor)) + .unwrap_or_else(Type::any); + + // Ensure that parameter names are stripped from positional-only, variadic and keyword-variadic parameters. + // Ensure that we only record whether a parameter *has* a default + // (strip the precise *type* of the default from the parameter, replacing it with `Never`). + let kind = match kind { + ParameterKind::PositionalOnly { + name: _, + default_type, + } => ParameterKind::PositionalOnly { + name: None, + default_type: default_type.map(|_| Type::Never), + }, + ParameterKind::PositionalOrKeyword { name, default_type } => { + ParameterKind::PositionalOrKeyword { + name: name.clone(), + default_type: default_type.map(|_| Type::Never), + } + } + ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly { + name: name.clone(), + default_type: default_type.map(|_| Type::Never), + }, + ParameterKind::Variadic { name: _ } => ParameterKind::Variadic { + name: Name::new_static("args"), + }, + ParameterKind::KeywordVariadic { name: _ } => ParameterKind::KeywordVariadic { + name: Name::new_static("kwargs"), + }, + }; + + Self { + annotated_type: Some(annotated_type), + kind, + form: *form, + } + } + + fn from_node_and_kind( + db: &'db dyn Db, + definition: Definition<'db>, + parameter: &ast::Parameter, + kind: ParameterKind<'db>, + ) -> Self { + Self { + annotated_type: parameter + .annotation() + .map(|annotation| definition_expression_type(db, definition, annotation)), + kind, + form: ParameterForm::Value, + } + } + + /// Returns `true` if this is a keyword-only parameter. + pub(crate) fn is_keyword_only(&self) -> bool { + matches!(self.kind, ParameterKind::KeywordOnly { .. }) + } + + /// Returns `true` if this is a positional-only parameter. + pub(crate) fn is_positional_only(&self) -> bool { + matches!(self.kind, ParameterKind::PositionalOnly { .. }) + } + + /// Returns `true` if this is a variadic parameter. + pub(crate) fn is_variadic(&self) -> bool { + matches!(self.kind, ParameterKind::Variadic { .. }) + } + + /// Returns `true` if this is a keyword-variadic parameter. + pub(crate) fn is_keyword_variadic(&self) -> bool { + matches!(self.kind, ParameterKind::KeywordVariadic { .. }) + } + + /// Returns `true` if this is either a positional-only or standard (positional or keyword) + /// parameter. + pub(crate) fn is_positional(&self) -> bool { + matches!( + self.kind, + ParameterKind::PositionalOnly { .. } | ParameterKind::PositionalOrKeyword { .. } + ) + } + + pub(crate) fn callable_by_name(&self, name: &str) -> bool { + match &self.kind { + ParameterKind::PositionalOrKeyword { + name: param_name, .. + } + | ParameterKind::KeywordOnly { + name: param_name, .. + } => param_name == name, + _ => false, + } + } + + /// Annotated type of the parameter, if annotated. + pub(crate) fn annotated_type(&self) -> Option> { + self.annotated_type + } + + /// Kind of the parameter. + pub(crate) fn kind(&self) -> &ParameterKind<'db> { + &self.kind + } + + /// Name of the parameter (if it has one). + pub(crate) fn name(&self) -> Option<&ast::name::Name> { + match &self.kind { + ParameterKind::PositionalOnly { name, .. } => name.as_ref(), + ParameterKind::PositionalOrKeyword { name, .. } => Some(name), + ParameterKind::Variadic { name } => Some(name), + ParameterKind::KeywordOnly { name, .. } => Some(name), + ParameterKind::KeywordVariadic { name } => Some(name), + } + } + + /// Display name of the parameter, if it has one. + pub(crate) fn display_name(&self) -> Option { + self.name().map(|name| match self.kind { + ParameterKind::Variadic { .. } => ast::name::Name::new(format!("*{name}")), + ParameterKind::KeywordVariadic { .. } => ast::name::Name::new(format!("**{name}")), + _ => name.clone(), + }) + } + + /// Default-value type of the parameter, if any. + pub(crate) fn default_type(&self) -> Option> { + match self.kind { + ParameterKind::PositionalOnly { default_type, .. } + | ParameterKind::PositionalOrKeyword { default_type, .. } + | ParameterKind::KeywordOnly { default_type, .. } => default_type, + ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) enum ParameterKind<'db> { + /// Positional-only parameter, e.g. `def f(x, /): ...` + PositionalOnly { + /// Parameter name. + /// + /// It is possible for signatures to be defined in ways that leave positional-only parameters + /// nameless (e.g. via `Callable` annotations). + name: Option, + default_type: Option>, + }, + + /// Positional-or-keyword parameter, e.g. `def f(x): ...` + PositionalOrKeyword { + /// Parameter name. + name: Name, + default_type: Option>, + }, + + /// Variadic parameter, e.g. `def f(*args): ...` + Variadic { + /// Parameter name. + name: Name, + }, + + /// Keyword-only parameter, e.g. `def f(*, x): ...` + KeywordOnly { + /// Parameter name. + name: Name, + default_type: Option>, + }, + + /// Variadic keywords parameter, e.g. `def f(**kwargs): ...` + KeywordVariadic { + /// Parameter name. + name: Name, + }, +} + +impl<'db> ParameterKind<'db> { + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + match self { + Self::PositionalOnly { default_type, name } => Self::PositionalOnly { + default_type: default_type + .as_ref() + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + name: name.clone(), + }, + Self::PositionalOrKeyword { default_type, name } => Self::PositionalOrKeyword { + default_type: default_type + .as_ref() + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + name: name.clone(), + }, + Self::KeywordOnly { default_type, name } => Self::KeywordOnly { + default_type: default_type + .as_ref() + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + name: name.clone(), + }, + Self::Variadic { .. } | Self::KeywordVariadic { .. } => self.clone(), + } + } +} + +/// Whether a parameter is used as a value or a type form. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] +pub(crate) enum ParameterForm { + Value, + Type, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::tests::{TestDb, setup_db}; + use crate::place::global_symbol; + use crate::types::{FunctionType, KnownClass}; + use ruff_db::system::DbWithWritableSystem as _; + + #[track_caller] + fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { + let module = ruff_db::files::system_path_to_file(db, file).unwrap(); + global_symbol(db, module, "f") + .place + .expect_type() + .expect_function_literal() + } + + #[track_caller] + fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) { + assert_eq!(signature.parameters.value.as_slice(), expected); + } + + #[test] + fn empty() { + let mut db = setup_db(); + db.write_dedented("/src/a.py", "def f(): ...").unwrap(); + let func = get_function_f(&db, "/src/a.py") + .literal(&db) + .last_definition(&db); + + let sig = func.signature(&db, None); + + assert!(sig.return_ty.is_none()); + assert_params(&sig, &[]); + } + + #[test] + #[allow(clippy::many_single_char_names)] + fn full() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + " + from typing import Literal + + def f(a, b: int, c = 1, d: int = 2, /, + e = 3, f: Literal[4] = 4, *args: object, + g = 5, h: Literal[6] = 6, **kwargs: str) -> bytes: ... + ", + ) + .unwrap(); + let func = get_function_f(&db, "/src/a.py") + .literal(&db) + .last_definition(&db); + + let sig = func.signature(&db, None); + + assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes"); + assert_params( + &sig, + &[ + 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)), + ], + ); + } + + #[test] + fn not_deferred() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + " + class A: ... + class B: ... + + alias = A + + def f(a: alias): ... + + alias = B + ", + ) + .unwrap(); + let func = get_function_f(&db, "/src/a.py") + .literal(&db) + .last_definition(&db); + + let sig = func.signature(&db, None); + + let [ + Parameter { + 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_type.unwrap().display(&db).to_string(), "A"); + } + + #[test] + fn deferred_in_stub() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.pyi", + " + class A: ... + class B: ... + + alias = A + + def f(a: alias): ... + + alias = B + ", + ) + .unwrap(); + let func = get_function_f(&db, "/src/a.pyi") + .literal(&db) + .last_definition(&db); + + let sig = func.signature(&db, None); + + let [ + Parameter { + annotated_type, + kind: ParameterKind::PositionalOrKeyword { name, .. }, + .. + }, + ] = &sig.parameters.value[..] + else { + panic!("expected one positional-or-keyword parameter"); + }; + assert_eq!(name, "a"); + // Parameter resolution deferred: + assert_eq!(annotated_type.unwrap().display(&db).to_string(), "A | B"); + } + + #[test] + fn generic_not_deferred() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + " + class A: ... + class B: ... + + alias = A + + def f[T](a: alias, b: T) -> T: ... + + alias = B + ", + ) + .unwrap(); + let func = get_function_f(&db, "/src/a.py") + .literal(&db) + .last_definition(&db); + + let sig = func.signature(&db, None); + + let [ + Parameter { + annotated_type: a_annotated_ty, + kind: ParameterKind::PositionalOrKeyword { name: a_name, .. }, + .. + }, + Parameter { + annotated_type: b_annotated_ty, + kind: ParameterKind::PositionalOrKeyword { name: b_name, .. }, + .. + }, + ] = &sig.parameters.value[..] + else { + panic!("expected two positional-or-keyword parameters"); + }; + assert_eq!(a_name, "a"); + assert_eq!(b_name, "b"); + // TODO resolution should not be deferred; we should see A, not A | B + assert_eq!( + a_annotated_ty.unwrap().display(&db).to_string(), + "Unknown | A | B" + ); + assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T"); + } + + #[test] + fn generic_deferred_in_stub() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.pyi", + " + class A: ... + class B: ... + + alias = A + + def f[T](a: alias, b: T) -> T: ... + + alias = B + ", + ) + .unwrap(); + let func = get_function_f(&db, "/src/a.pyi") + .literal(&db) + .last_definition(&db); + + let sig = func.signature(&db, None); + + let [ + Parameter { + annotated_type: a_annotated_ty, + kind: ParameterKind::PositionalOrKeyword { name: a_name, .. }, + .. + }, + Parameter { + annotated_type: b_annotated_ty, + kind: ParameterKind::PositionalOrKeyword { name: b_name, .. }, + .. + }, + ] = &sig.parameters.value[..] + else { + panic!("expected two positional-or-keyword parameters"); + }; + assert_eq!(a_name, "a"); + assert_eq!(b_name, "b"); + // Parameter resolution deferred: + assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "A | B"); + assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T"); + } + + #[test] + fn external_signature_no_decorator() { + let mut db = setup_db(); + db.write_dedented( + "/src/a.py", + " + def f(a: int) -> int: ... + ", + ) + .unwrap(); + let func = get_function_f(&db, "/src/a.py"); + + let overload = func.literal(&db).last_definition(&db); + let expected_sig = overload.signature(&db, None); + + // With no decorators, internal and external signature are the same + assert_eq!( + func.signature(&db), + &CallableSignature::single(expected_sig) + ); + } +} diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs new file mode 100644 index 0000000000000..f960f3058fe9c --- /dev/null +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -0,0 +1,355 @@ +//! An enumeration of special forms in the Python type system. +//! Each of these is considered to inhabit a unique type in our model of the type system. + +use super::{ClassType, Type, class::KnownClass}; +use crate::db::Db; +use crate::module_resolver::{KnownModule, file_to_module}; +use ruff_db::files::File; +use std::str::FromStr; + +/// Enumeration of specific runtime symbols that are special enough +/// that they can each be considered to inhabit a unique type. +/// +/// # Ordering +/// +/// Ordering is stable and should be the same between runs. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + salsa::Update, + PartialOrd, + Ord, + strum_macros::EnumString, + get_size2::GetSize, +)] +pub enum SpecialFormType { + /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) + Annotated, + /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) + Literal, + /// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`) + LiteralString, + /// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`) + Optional, + /// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`) + Union, + /// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`) + NoReturn, + /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) + Never, + /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) + Tuple, + /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) + List, + /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) + Dict, + /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) + Set, + /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) + FrozenSet, + /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) + ChainMap, + /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) + Counter, + /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) + DefaultDict, + /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) + Deque, + /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) + OrderedDict, + /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) + Type, + /// The symbol `ty_extensions.Unknown` + Unknown, + /// The symbol `ty_extensions.AlwaysTruthy` + AlwaysTruthy, + /// The symbol `ty_extensions.AlwaysFalsy` + AlwaysFalsy, + /// The symbol `ty_extensions.Not` + Not, + /// The symbol `ty_extensions.Intersection` + Intersection, + /// The symbol `ty_extensions.TypeOf` + TypeOf, + /// The symbol `ty_extensions.CallableTypeOf` + CallableTypeOf, + /// The symbol `typing.Callable` + /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) + Callable, + /// The symbol `typing.Self` (which can also be found as `typing_extensions.Self`) + #[strum(serialize = "Self")] + TypingSelf, + /// The symbol `typing.Final` (which can also be found as `typing_extensions.Final`) + Final, + /// The symbol `typing.ClassVar` (which can also be found as `typing_extensions.ClassVar`) + ClassVar, + /// The symbol `typing.Concatenate` (which can also be found as `typing_extensions.Concatenate`) + Concatenate, + /// The symbol `typing.Unpack` (which can also be found as `typing_extensions.Unpack`) + Unpack, + /// The symbol `typing.Required` (which can also be found as `typing_extensions.Required`) + Required, + /// The symbol `typing.NotRequired` (which can also be found as `typing_extensions.NotRequired`) + NotRequired, + /// The symbol `typing.TypeAlias` (which can also be found as `typing_extensions.TypeAlias`) + TypeAlias, + /// The symbol `typing.TypeGuard` (which can also be found as `typing_extensions.TypeGuard`) + TypeGuard, + /// The symbol `typing.TypedDict` (which can also be found as `typing_extensions.TypedDict`) + TypedDict, + /// The symbol `typing.TypeIs` (which can also be found as `typing_extensions.TypeIs`) + TypeIs, + /// The symbol `typing.ReadOnly` (which can also be found as `typing_extensions.ReadOnly`) + ReadOnly, + + /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) + /// + /// Note that instances of subscripted `typing.Protocol` are not represented by this type; + /// see also [`super::KnownInstanceType::SubscriptedProtocol`]. + Protocol, + + /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`). + /// + /// Note that instances of subscripted `typing.Generic` are not represented by this type; + /// see also [`super::KnownInstanceType::SubscriptedGeneric`]. + Generic, +} + +impl SpecialFormType { + /// Return the [`KnownClass`] which this symbol is an instance of + pub(crate) const fn class(self) -> KnownClass { + match self { + Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Tuple + | Self::Type + | Self::TypingSelf + | Self::Final + | Self::ClassVar + | Self::Callable + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypedDict + | Self::TypeIs + | Self::TypeOf + | Self::Not + | Self::Intersection + | Self::CallableTypeOf + | Self::Protocol // actually `_ProtocolMeta` at runtime but this is what typeshed says + | Self::Generic // actually `type` at runtime but this is what typeshed says + | Self::ReadOnly => KnownClass::SpecialForm, + + Self::List + | Self::Dict + | Self::DefaultDict + | Self::Set + | Self::FrozenSet + | Self::Counter + | Self::Deque + | Self::ChainMap + | Self::OrderedDict => KnownClass::StdlibAlias, + + Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object, + } + } + + /// Return the instance type which this type is a subtype of. + /// + /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, + /// so `SpecialFormType::Literal.instance_fallback(db)` + /// returns `Type::NominalInstance(NominalInstanceType { class: })`. + pub(super) fn instance_fallback(self, db: &dyn Db) -> Type { + self.class().to_instance(db) + } + + /// Return `true` if this symbol is an instance of `class`. + pub(super) fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { + self.class().is_subclass_of(db, class) + } + + pub(super) fn try_from_file_and_name( + db: &dyn Db, + file: File, + symbol_name: &str, + ) -> Option { + let candidate = Self::from_str(symbol_name).ok()?; + candidate + .check_module(file_to_module(db, file)?.known()?) + .then_some(candidate) + } + + /// Return `true` if `module` is a module from which this `SpecialFormType` variant can validly originate. + /// + /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. + /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. + pub(super) fn check_module(self, module: KnownModule) -> bool { + match self { + Self::ClassVar + | Self::Deque + | Self::List + | Self::Dict + | Self::DefaultDict + | Self::Set + | Self::FrozenSet + | Self::Counter + | Self::ChainMap + | Self::OrderedDict + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Tuple + | Self::Type + | Self::Generic + | Self::Callable => module.is_typing(), + + Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Never + | Self::Final + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypedDict + | Self::TypeIs + | Self::TypingSelf + | Self::Protocol + | Self::ReadOnly => { + matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) + } + + Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf => module.is_ty_extensions(), + } + } + + pub(super) fn to_meta_type(self, db: &dyn Db) -> Type { + self.class().to_class_literal(db) + } + + /// Return true if this special form is callable at runtime. + /// Most special forms are not callable (they are type constructors that are subscripted), + /// but some like `TypedDict` and collection constructors can be called. + pub(super) const fn is_callable(self) -> bool { + match self { + // TypedDict can be called as a constructor to create TypedDict types + Self::TypedDict + // Collection constructors are callable + // TODO actually implement support for calling them + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict => true, + + // All other special forms are not callable + Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Tuple + | Self::List + | Self::Dict + | Self::Set + | Self::FrozenSet + | Self::Type + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf + | Self::Callable + | Self::TypingSelf + | Self::Final + | Self::ClassVar + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypeIs + | Self::ReadOnly + | Self::Protocol + | Self::Generic => false, + } + } + + /// Return the repr of the symbol at runtime + pub(super) const fn repr(self) -> &'static str { + match self { + SpecialFormType::Annotated => "typing.Annotated", + SpecialFormType::Literal => "typing.Literal", + SpecialFormType::LiteralString => "typing.LiteralString", + SpecialFormType::Optional => "typing.Optional", + SpecialFormType::Union => "typing.Union", + SpecialFormType::NoReturn => "typing.NoReturn", + SpecialFormType::Never => "typing.Never", + SpecialFormType::Tuple => "typing.Tuple", + SpecialFormType::Type => "typing.Type", + SpecialFormType::TypingSelf => "typing.Self", + SpecialFormType::Final => "typing.Final", + SpecialFormType::ClassVar => "typing.ClassVar", + SpecialFormType::Callable => "typing.Callable", + SpecialFormType::Concatenate => "typing.Concatenate", + SpecialFormType::Unpack => "typing.Unpack", + SpecialFormType::Required => "typing.Required", + SpecialFormType::NotRequired => "typing.NotRequired", + SpecialFormType::TypeAlias => "typing.TypeAlias", + SpecialFormType::TypeGuard => "typing.TypeGuard", + SpecialFormType::TypedDict => "typing.TypedDict", + SpecialFormType::TypeIs => "typing.TypeIs", + SpecialFormType::List => "typing.List", + SpecialFormType::Dict => "typing.Dict", + SpecialFormType::DefaultDict => "typing.DefaultDict", + SpecialFormType::Set => "typing.Set", + SpecialFormType::FrozenSet => "typing.FrozenSet", + SpecialFormType::Counter => "typing.Counter", + SpecialFormType::Deque => "typing.Deque", + SpecialFormType::ChainMap => "typing.ChainMap", + SpecialFormType::OrderedDict => "typing.OrderedDict", + SpecialFormType::ReadOnly => "typing.ReadOnly", + SpecialFormType::Unknown => "ty_extensions.Unknown", + SpecialFormType::AlwaysTruthy => "ty_extensions.AlwaysTruthy", + SpecialFormType::AlwaysFalsy => "ty_extensions.AlwaysFalsy", + SpecialFormType::Not => "ty_extensions.Not", + SpecialFormType::Intersection => "ty_extensions.Intersection", + SpecialFormType::TypeOf => "ty_extensions.TypeOf", + SpecialFormType::CallableTypeOf => "ty_extensions.CallableTypeOf", + SpecialFormType::Protocol => "typing.Protocol", + SpecialFormType::Generic => "typing.Generic", + } + } +} + +impl std::fmt::Display for SpecialFormType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.repr()) + } +} diff --git a/crates/ty_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs new file mode 100644 index 0000000000000..410996fd24b13 --- /dev/null +++ b/crates/ty_python_semantic/src/types/string_annotation.rs @@ -0,0 +1,180 @@ +use ruff_db::source::source_text; +use ruff_python_ast::{self as ast, ModExpression}; +use ruff_python_parser::Parsed; +use ruff_text_size::Ranged; + +use crate::declare_lint; +use crate::lint::{Level, LintStatus}; + +use super::context::InferContext; + +declare_lint! { + /// ## What it does + /// Checks for f-strings in type annotation positions. + /// + /// ## Why is this bad? + /// Static analysis tools like ty can't analyse type annotations that use f-string notation. + /// + /// ## Examples + /// ```python + /// def test(): -> f"int": + /// ... + /// ``` + /// + /// Use instead: + /// ```python + /// def test(): -> "int": + /// ... + /// ``` + pub(crate) static FSTRING_TYPE_ANNOTATION = { + summary: "detects F-strings in type annotation positions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for byte-strings in type annotation positions. + /// + /// ## Why is this bad? + /// Static analysis tools like ty can't analyse type annotations that use byte-string notation. + /// + /// ## Examples + /// ```python + /// def test(): -> b"int": + /// ... + /// ``` + /// + /// Use instead: + /// ```python + /// def test(): -> "int": + /// ... + /// ``` + pub(crate) static BYTE_STRING_TYPE_ANNOTATION = { + summary: "detects byte strings in type annotation positions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for raw-strings in type annotation positions. + /// + /// ## Why is this bad? + /// Static analysis tools like ty can't analyse type annotations that use raw-string notation. + /// + /// ## Examples + /// ```python + /// def test(): -> r"int": + /// ... + /// ``` + /// + /// Use instead: + /// ```python + /// def test(): -> "int": + /// ... + /// ``` + pub(crate) static RAW_STRING_TYPE_ANNOTATION = { + summary: "detects raw strings in type annotation positions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// ## What it does + /// Checks for implicit concatenated strings in type annotation positions. + /// + /// ## Why is this bad? + /// Static analysis tools like ty can't analyse type annotations that use implicit concatenated strings. + /// + /// ## Examples + /// ```python + /// def test(): -> "Literal[" "5" "]": + /// ... + /// ``` + /// + /// Use instead: + /// ```python + /// def test(): -> "Literal[5]": + /// ... + /// ``` + pub(crate) static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = { + summary: "detects implicit concatenated strings in type annotations", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// TODO #14889 + pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = { + summary: "detects invalid syntax in forward annotations", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +declare_lint! { + /// TODO #14889 + pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = { + summary: "detects forward type annotations with escape characters", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + +/// Parses the given expression as a string annotation. +pub(crate) fn parse_string_annotation( + context: &InferContext, + string_expr: &ast::ExprStringLiteral, +) -> Option> { + let file = context.file(); + let db = context.db(); + + let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), ?file) + .entered(); + + let source = source_text(db, file); + + if let Some(string_literal) = string_expr.as_single_part_string() { + let prefix = string_literal.flags.prefix(); + if prefix.is_raw() { + if let Some(builder) = context.report_lint(&RAW_STRING_TYPE_ANNOTATION, string_literal) + { + builder.into_diagnostic("Type expressions cannot use raw string literal"); + } + // Compare the raw contents (without quotes) of the expression with the parsed contents + // contained in the string literal. + } else if &source[string_literal.content_range()] == string_literal.as_str() { + match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) { + Ok(parsed) => return Some(parsed), + Err(parse_error) => { + if let Some(builder) = + context.report_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION, string_literal) + { + builder.into_diagnostic(format_args!( + "Syntax error in forward annotation: {}", + parse_error.error + )); + } + } + } + } else if let Some(builder) = + context.report_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, string_expr) + { + // The raw contents of the string doesn't match the parsed content. This could be the + // case for annotations that contain escape sequences. + builder.into_diagnostic("Type expressions cannot contain escape characters"); + } + } else if let Some(builder) = + context.report_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, string_expr) + { + // String is implicitly concatenated. + builder.into_diagnostic("Type expressions cannot span multiple string literals"); + } + + None +} diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs new file mode 100644 index 0000000000000..5b12ae252adbe --- /dev/null +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -0,0 +1,281 @@ +use ruff_python_ast::name::Name; + +use crate::place::PlaceAndQualifiers; +use crate::types::{ + ClassType, DynamicType, KnownClass, MemberLookupPolicy, Type, TypeMapping, TypeRelation, + TypeTransformer, TypeVarInstance, +}; +use crate::{Db, FxOrderSet}; + +use super::{TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance}; + +/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub struct SubclassOfType<'db> { + // Keep this field private, so that the only way of constructing the struct is through the `from` method. + subclass_of: SubclassOfInner<'db>, +} + +pub(super) fn walk_subclass_of_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + subclass_of: SubclassOfType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, Type::from(subclass_of.subclass_of)); +} + +impl<'db> SubclassOfType<'db> { + /// Construct a new [`Type`] instance representing a given class object (or a given dynamic type) + /// and all possible subclasses of that class object/dynamic type. + /// + /// This method does not always return a [`Type::SubclassOf`] variant. + /// If the class object is known to be a final class, + /// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type. + /// If the class object is `builtins.object`, `Type::NominalInstance()` + /// will be returned; this is no more precise, but it is exactly equivalent to `type[object]`. + /// + /// The eager normalization here means that we do not need to worry elsewhere about distinguishing + /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. + pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { + let subclass_of = subclass_of.into(); + match subclass_of { + SubclassOfInner::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), + SubclassOfInner::Class(class) => { + if class.is_final(db) { + Type::from(class) + } else { + match class.known(db) { + Some(KnownClass::Object) => KnownClass::Type.to_instance(db), + Some(KnownClass::Any) => Type::SubclassOf(Self { + subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), + }), + _ => Type::SubclassOf(Self { subclass_of }), + } + } + } + } + } + + /// Return a [`Type`] instance representing the type `type[Unknown]`. + pub(crate) const fn subclass_of_unknown() -> Type<'db> { + Type::SubclassOf(SubclassOfType { + subclass_of: SubclassOfInner::unknown(), + }) + } + + /// Return a [`Type`] instance representing the type `type[Any]`. + pub(crate) const fn subclass_of_any() -> Type<'db> { + Type::SubclassOf(SubclassOfType { + subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), + }) + } + + /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. + pub(crate) const fn subclass_of(self) -> SubclassOfInner<'db> { + self.subclass_of + } + + pub(crate) const fn is_dynamic(self) -> bool { + // Unpack `self` so that we're forced to update this method if any more fields are added in the future. + let Self { subclass_of } = self; + subclass_of.is_dynamic() + } + + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> { + match self.subclass_of { + SubclassOfInner::Dynamic(_) => match variance { + TypeVarVariance::Covariant => KnownClass::Type.to_instance(db), + TypeVarVariance::Contravariant => Type::Never, + TypeVarVariance::Invariant => { + // We need to materialize this to `type[T]` but that isn't representable so + // we instead use a type variable with an upper bound of `type`. + Type::TypeVar(TypeVarInstance::new( + db, + Name::new_static("T_all"), + None, + Some(TypeVarBoundOrConstraints::UpperBound( + KnownClass::Type.to_instance(db), + )), + variance, + None, + TypeVarKind::Pep695, + )) + } + TypeVarVariance::Bivariant => unreachable!(), + }, + SubclassOfInner::Class(_) => Type::SubclassOf(self), + } + } + + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + match self.subclass_of { + SubclassOfInner::Class(class) => Self { + subclass_of: SubclassOfInner::Class(class.apply_type_mapping(db, type_mapping)), + }, + SubclassOfInner::Dynamic(_) => self, + } + } + + pub(super) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self.subclass_of { + SubclassOfInner::Class(class) => { + class.find_legacy_typevars(db, typevars); + } + SubclassOfInner::Dynamic(_) => {} + } + } + + pub(crate) fn find_name_in_mro_with_policy( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> Option> { + Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy) + } + + /// Return `true` if `self` has a certain relation to `other`. + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: SubclassOfType<'db>, + relation: TypeRelation, + ) -> bool { + match (self.subclass_of, other.subclass_of) { + (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { + relation.is_assignability() + } + (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => { + other_class.is_object(db) || relation.is_assignability() + } + (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => relation.is_assignability(), + + // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, + // and `type[int]` describes all possible runtime subclasses of the class `int`. + // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. + (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { + self_class.has_relation_to(db, other_class, relation) + } + } + } + + /// Return` true` if `self` is a disjoint type from `other`. + /// + /// See [`Type::is_disjoint_from`] for more details. + pub(crate) fn is_disjoint_from_impl(self, db: &'db dyn Db, other: Self) -> bool { + match (self.subclass_of, other.subclass_of) { + (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false, + (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { + !self_class.could_coexist_in_mro_with(db, other_class) + } + } + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + Self { + subclass_of: self.subclass_of.normalized_impl(db, visitor), + } + } + + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + match self.subclass_of { + SubclassOfInner::Class(class) => Type::instance(db, class), + SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), + } + } +} + +/// An enumeration of the different kinds of `type[]` types that a [`SubclassOfType`] can represent: +/// +/// 1. A "subclass of a class": `type[C]` for any class object `C` +/// 2. A "subclass of a dynamic type": `type[Any]`, `type[Unknown]` and `type[@Todo]` +/// +/// In the long term, we may want to implement . +/// Doing this would allow us to get rid of this enum, +/// since `type[Any]` would be represented as `type & Any` +/// rather than using the [`Type::SubclassOf`] variant at all; +/// [`SubclassOfType`] would then be a simple wrapper around [`ClassType`]. +/// +/// Note that this enum is similar to the [`super::ClassBase`] enum, +/// but does not include the `ClassBase::Protocol` and `ClassBase::Generic` variants +/// (`type[Protocol]` and `type[Generic]` are not valid types). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) enum SubclassOfInner<'db> { + Class(ClassType<'db>), + Dynamic(DynamicType), +} + +impl<'db> SubclassOfInner<'db> { + pub(crate) const fn unknown() -> Self { + Self::Dynamic(DynamicType::Unknown) + } + + pub(crate) const fn is_dynamic(self) -> bool { + matches!(self, Self::Dynamic(_)) + } + + pub(crate) const fn into_class(self) -> Option> { + match self { + Self::Class(class) => Some(class), + Self::Dynamic(_) => None, + } + } + + pub(crate) const fn into_dynamic(self) -> Option { + match self { + Self::Class(_) => None, + Self::Dynamic(dynamic) => Some(dynamic), + } + } + + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + match self { + Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), + Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), + } + } + + pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + match ty { + Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), + Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) { + Self::Dynamic(DynamicType::Any) + } else { + Self::Class(literal.default_specialization(db)) + }), + Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), + _ => None, + } + } +} + +impl<'db> From> for SubclassOfInner<'db> { + fn from(value: ClassType<'db>) -> Self { + SubclassOfInner::Class(value) + } +} + +impl<'db> From> for Type<'db> { + fn from(value: SubclassOfInner<'db>) -> Self { + match value { + SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), + SubclassOfInner::Class(class) => class.into(), + } + } +} diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs new file mode 100644 index 0000000000000..58dcb0debbffb --- /dev/null +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -0,0 +1,1303 @@ +//! Types describing fixed- and variable-length tuples. +//! +//! At runtime, a Python tuple is a fixed-length immutable list of values. There is no restriction +//! on the types of the elements of a tuple value. In the type system, we want to model both +//! "heterogeneous" tuples that have elements of a fixed sequence of specific types, and +//! "homogeneous" tuples that have an unknown number of elements of the same single type. And in +//! fact, we want to model tuples that are a combination of the two ("mixed" tuples), with a +//! heterogeneous prefix and/or suffix, and a homogeneous portion of unknown length in between +//! those. +//! +//! The description of which elements can appear in a `tuple` is called a [`TupleSpec`]. Other +//! things besides `tuple` instances can be described by a tuple spec — for instance, the targets +//! of an unpacking assignment. A `tuple` specialization that includes `Never` as one of its +//! fixed-length elements cannot be instantiated. We reduce the entire `tuple` type down to +//! `Never`. The same is not true of tuple specs in general. (That means that it is [`TupleType`] +//! that adds that "collapse `Never`" behavior, whereas [`TupleSpec`] allows you to add any element +//! types, including `Never`.) + +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::hash::Hash; + +use itertools::{Either, EitherOrBoth, Itertools}; + +use crate::types::class::{ClassType, KnownClass}; +use crate::types::{ + Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, TypeVarVariance, + UnionBuilder, UnionType, cyclic::PairVisitor, +}; +use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; +use crate::{Db, FxOrderSet}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum TupleLength { + Fixed(usize), + Variable(usize, usize), +} + +impl TupleLength { + /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` + /// for a tuple with a variable-length portion.) + pub(crate) fn size_hint(self) -> (usize, Option) { + match self { + TupleLength::Fixed(len) => (len, Some(len)), + TupleLength::Variable(prefix, suffix) => (prefix + suffix, None), + } + } + + /// Returns the minimum length of this tuple. + pub(crate) fn minimum(self) -> usize { + match self { + TupleLength::Fixed(len) => len, + TupleLength::Variable(prefix, suffix) => prefix + suffix, + } + } + + /// Returns the maximum length of this tuple, if any. + pub(crate) fn maximum(self) -> Option { + match self { + TupleLength::Fixed(len) => Some(len), + TupleLength::Variable(_, _) => None, + } + } + + pub(crate) fn display_minimum(self) -> String { + let minimum_length = self.minimum(); + match self { + TupleLength::Fixed(_) => minimum_length.to_string(), + TupleLength::Variable(_, _) => format!("at least {minimum_length}"), + } + } + + pub(crate) fn display_maximum(self) -> String { + match self.maximum() { + Some(maximum) => maximum.to_string(), + None => "unlimited".to_string(), + } + } +} + +/// # Ordering +/// Ordering is based on the tuple's salsa-assigned id and not on its elements. +/// The id may change between runs, or when the tuple was garbage collected and recreated. +#[salsa::interned(debug, constructor=new_internal)] +#[derive(PartialOrd, Ord)] +pub struct TupleType<'db> { + #[returns(ref)] + pub(crate) tuple: TupleSpec<'db>, +} + +pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + tuple: TupleType<'db>, + visitor: &mut V, +) { + for element in tuple.tuple(db).all_elements() { + visitor.visit_type(db, *element); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TupleType<'_> {} + +impl<'db> Type<'db> { + pub(crate) fn tuple(tuple: Option>) -> Self { + let Some(tuple) = tuple else { + return Type::Never; + }; + Self::Tuple(tuple) + } +} + +impl<'db> TupleType<'db> { + pub(crate) fn new(db: &'db dyn Db, tuple_key: T) -> Option + where + T: Borrow> + Hash + salsa::plumbing::interned::Lookup>, + TupleSpec<'db>: salsa::plumbing::interned::HashEqLike, + { + // If a fixed-length (i.e., mandatory) element of the tuple is `Never`, then it's not + // possible to instantiate the tuple as a whole. + let tuple = tuple_key.borrow(); + if tuple.fixed_elements().any(Type::is_never) { + return None; + } + + // If the variable-length portion is Never, it can only be instantiated with zero elements. + // That means this isn't a variable-length tuple after all! + if let TupleSpec::Variable(tuple) = tuple { + if tuple.variable.is_never() { + let tuple = TupleSpec::Fixed(FixedLengthTuple::from_elements( + tuple.prefix.iter().chain(&tuple.suffix).copied(), + )); + return Some(TupleType::new_internal::<_, TupleSpec<'db>>(db, tuple)); + } + } + + Some(TupleType::new_internal(db, tuple_key)) + } + + pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> { + Type::tuple(TupleType::new( + db, + TupleSpec::from(FixedLengthTuple::empty()), + )) + } + + pub(crate) fn from_elements( + db: &'db dyn Db, + types: impl IntoIterator>, + ) -> Type<'db> { + Type::tuple(TupleType::new(db, TupleSpec::from_elements(types))) + } + + #[cfg(test)] + pub(crate) fn mixed( + db: &'db dyn Db, + prefix: impl IntoIterator>, + variable: Type<'db>, + suffix: impl IntoIterator>, + ) -> Type<'db> { + Type::tuple(TupleType::new( + db, + VariableLengthTuple::mixed(prefix, variable, suffix), + )) + } + + pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> { + Type::tuple(TupleType::new(db, TupleSpec::homogeneous(element))) + } + + pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option> { + KnownClass::Tuple + .try_to_class_literal(db) + .and_then(|class_literal| match class_literal.generic_context(db) { + None => Some(ClassType::NonGeneric(class_literal)), + Some(generic_context) if generic_context.variables(db).len() != 1 => None, + Some(generic_context) => Some( + class_literal + .apply_specialization(db, |_| generic_context.specialize_tuple(db, self)), + ), + }) + } + + /// Return a normalized version of `self`. + /// + /// See [`Type::normalized`] for more details. + #[must_use] + pub(crate) fn normalized_impl( + self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Option { + TupleType::new(db, self.tuple(db).normalized_impl(db, visitor)) + } + + pub(crate) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Option { + TupleType::new(db, self.tuple(db).materialize(db, variance)) + } + + pub(crate) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Option { + TupleType::new(db, self.tuple(db).apply_type_mapping(db, type_mapping)) + } + + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + self.tuple(db).find_legacy_typevars(db, typevars); + } + + pub(crate) fn has_relation_to( + self, + db: &'db dyn Db, + other: Self, + relation: TypeRelation, + ) -> bool { + self.tuple(db) + .has_relation_to(db, other.tuple(db), relation) + } + + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.tuple(db).is_equivalent_to(db, other.tuple(db)) + } + + pub(crate) fn is_disjoint_from_impl( + self, + db: &'db dyn Db, + other: Self, + visitor: &mut PairVisitor<'db>, + ) -> bool { + self.tuple(db) + .is_disjoint_from_impl(db, other.tuple(db), visitor) + } + + pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { + self.tuple(db).is_single_valued(db) + } +} + +/// A tuple spec describes the contents of a tuple type, which might be fixed- or variable-length. +/// +/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a +/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that +/// must contain an element that can't be instantiated, can't be instantiated itself). +pub(crate) type TupleSpec<'db> = Tuple>; + +/// A fixed-length tuple. +/// +/// Our tuple representation can hold instances of any Rust type. For tuples containing Python +/// types, use [`TupleSpec`], which defines some additional type-specific methods. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct FixedLengthTuple(Vec); + +impl FixedLengthTuple { + fn empty() -> Self { + Self(Vec::new()) + } + + pub(crate) fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + fn from_elements(elements: impl IntoIterator) -> Self { + Self(elements.into_iter().collect()) + } + + pub(crate) fn elements_slice(&self) -> &[T] { + &self.0 + } + + pub(crate) fn elements(&self) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.0.iter() + } + + pub(crate) fn all_elements(&self) -> impl Iterator { + self.0.iter() + } + + pub(crate) fn into_all_elements_with_kind(self) -> impl Iterator> { + self.0.into_iter().map(TupleElement::Fixed) + } + + /// Returns the length of this tuple. + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub(crate) fn push(&mut self, element: T) { + self.0.push(element); + } +} + +impl<'db> FixedLengthTuple> { + fn concat(&self, other: &Tuple>) -> Tuple> { + match other { + TupleSpec::Fixed(other) => TupleSpec::Fixed(FixedLengthTuple::from_elements( + self.elements().chain(other.elements()).copied(), + )), + + TupleSpec::Variable(other) => VariableLengthTuple::mixed( + self.elements().chain(other.prefix_elements()).copied(), + other.variable, + other.suffix_elements().copied(), + ), + } + } + + fn resize( + &self, + db: &'db dyn Db, + new_length: TupleLength, + ) -> Result>, ResizeTupleError> { + match new_length { + TupleLength::Fixed(new_length) => match self.len().cmp(&new_length) { + Ordering::Less => Err(ResizeTupleError::TooFewValues), + Ordering::Greater => Err(ResizeTupleError::TooManyValues), + Ordering::Equal => Ok(Tuple::Fixed(self.clone())), + }, + + TupleLength::Variable(prefix, suffix) => { + // The number of rhs values that will be consumed by the starred target. + let Some(variable) = self.len().checked_sub(prefix + suffix) else { + return Err(ResizeTupleError::TooFewValues); + }; + + // Extract rhs values into the prefix, then into the starred target, then into the + // suffix. + let mut elements = self.elements().copied(); + let prefix = elements.by_ref().take(prefix).collect(); + let variable = UnionType::from_elements(db, elements.by_ref().take(variable)); + let suffix = elements.by_ref().take(suffix).collect(); + Ok(Tuple::Variable(VariableLengthTuple { + prefix, + variable, + suffix, + })) + } + } + } + + #[must_use] + fn normalized_impl(&self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>) -> Self { + Self::from_elements(self.0.iter().map(|ty| ty.normalized_impl(db, visitor))) + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + Self::from_elements(self.0.iter().map(|ty| ty.materialize(db, variance))) + } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { + Self::from_elements( + self.0 + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + ) + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for ty in &self.0 { + ty.find_legacy_typevars(db, typevars); + } + } + + fn has_relation_to( + &self, + db: &'db dyn Db, + other: &Tuple>, + relation: TypeRelation, + ) -> bool { + match other { + Tuple::Fixed(other) => { + self.0.len() == other.0.len() + && (self.0.iter()) + .zip(&other.0) + .all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation)) + } + + Tuple::Variable(other) => { + // This tuple must have enough elements to match up with the other tuple's prefix + // and suffix, and each of those elements must pairwise satisfy the relation. + let mut self_iter = self.0.iter(); + for other_ty in &other.prefix { + let Some(self_ty) = self_iter.next() else { + return false; + }; + if !self_ty.has_relation_to(db, *other_ty, relation) { + return false; + } + } + for other_ty in other.suffix.iter().rev() { + let Some(self_ty) = self_iter.next_back() else { + return false; + }; + if !self_ty.has_relation_to(db, *other_ty, relation) { + return false; + } + } + + // In addition, any remaining elements in this tuple must satisfy the + // variable-length portion of the other tuple. + self_iter.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation)) + } + } + } + + fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + self.0.len() == other.0.len() + && (self.0.iter()) + .zip(&other.0) + .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) + } + + fn is_single_valued(&self, db: &'db dyn Db) -> bool { + self.0.iter().all(|ty| ty.is_single_valued(db)) + } +} + +#[allow(unsafe_code)] +unsafe impl salsa::Update for FixedLengthTuple +where + T: salsa::Update, +{ + unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { + unsafe { + let old_value = &mut *old_pointer; + Vec::maybe_update(&raw mut old_value.0, new_value.0) + } + } +} + +impl<'db> PyIndex<'db> for &FixedLengthTuple> { + type Item = Type<'db>; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + self.0.as_slice().py_index(db, index).copied() + } +} + +impl<'db> PySlice<'db> for FixedLengthTuple> { + type Item = Type<'db>; + + fn py_slice( + &'db self, + db: &'db dyn Db, + start: Option, + stop: Option, + step: Option, + ) -> Result, StepSizeZeroError> { + self.0.py_slice(db, start, stop, step) + } +} + +/// A variable-length tuple. +/// +/// The tuple can contain a fixed-length heterogeneous prefix and/or suffix. All of the elements of +/// the variable-length portion must be the same. +/// +/// Our tuple representation can hold instances of any Rust type. For tuples containing Python +/// types, use [`TupleSpec`], which defines some additional type-specific methods. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VariableLengthTuple { + pub(crate) prefix: Vec, + pub(crate) variable: T, + pub(crate) suffix: Vec, +} + +impl VariableLengthTuple { + /// Creates a new tuple spec containing zero or more elements of a given type, with no prefix + /// or suffix. + fn homogeneous(ty: T) -> Tuple { + Self::mixed([], ty, []) + } + + fn mixed( + prefix: impl IntoIterator, + variable: T, + suffix: impl IntoIterator, + ) -> Tuple { + Tuple::Variable(Self { + prefix: prefix.into_iter().collect(), + variable, + suffix: suffix.into_iter().collect(), + }) + } + + fn prefix_elements(&self) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.prefix.iter() + } + + fn suffix_elements(&self) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.suffix.iter() + } + + fn fixed_elements(&self) -> impl Iterator + '_ { + self.prefix_elements().chain(self.suffix_elements()) + } + + fn all_elements(&self) -> impl Iterator + '_ { + (self.prefix_elements()) + .chain(std::iter::once(&self.variable)) + .chain(self.suffix_elements()) + } + + fn into_all_elements_with_kind(self) -> impl Iterator> { + (self.prefix.into_iter().map(TupleElement::Prefix)) + .chain(std::iter::once(TupleElement::Variable(self.variable))) + .chain(self.suffix.into_iter().map(TupleElement::Suffix)) + } + + fn len(&self) -> TupleLength { + TupleLength::Variable(self.prefix.len(), self.suffix.len()) + } + + fn push(&mut self, element: T) { + self.suffix.push(element); + } +} + +impl<'db> VariableLengthTuple> { + /// Returns the prefix of the prenormalization of this tuple. + /// + /// This is used in our subtyping and equivalence checks below to handle different tuple types + /// that represent the same set of runtime tuple values. For instance, the following two tuple + /// types both represent "a tuple of one or more `int`s": + /// + /// ```py + /// tuple[int, *tuple[int, ...]] + /// tuple[*tuple[int, ...], int] + /// ``` + /// + /// Prenormalization rewrites both types into the former form. We arbitrarily prefer the + /// elements to appear in the prefix if they can, so we move elements from the beginning of the + /// suffix, which are equivalent to the variable-length portion, to the end of the prefix. + /// + /// Complicating matters is that we don't always want to compare with _this_ tuple's + /// variable-length portion. (When this tuple's variable-length portion is gradual — + /// `tuple[Any, ...]` — we compare with the assumption that the `Any` materializes to the other + /// tuple's variable-length portion.) + fn prenormalized_prefix_elements<'a>( + &'a self, + db: &'db dyn Db, + variable: Option>, + ) -> impl Iterator> + 'a { + let variable = variable.unwrap_or(self.variable); + self.prefix_elements() + .chain( + self.suffix_elements() + .take_while(move |element| element.is_equivalent_to(db, variable)), + ) + .copied() + } + + /// Returns the suffix of the prenormalization of this tuple. + /// + /// This is used in our subtyping and equivalence checks below to handle different tuple types + /// that represent the same set of runtime tuple values. For instance, the following two tuple + /// types both represent "a tuple of one or more `int`s": + /// + /// ```py + /// tuple[int, *tuple[int, ...]] + /// tuple[*tuple[int, ...], int] + /// ``` + /// + /// Prenormalization rewrites both types into the former form. We arbitrarily prefer the + /// elements to appear in the prefix if they can, so we move elements from the beginning of the + /// suffix, which are equivalent to the variable-length portion, to the end of the prefix. + /// + /// Complicating matters is that we don't always want to compare with _this_ tuple's + /// variable-length portion. (When this tuple's variable-length portion is gradual — + /// `tuple[Any, ...]` — we compare with the assumption that the `Any` materializes to the other + /// tuple's variable-length portion.) + fn prenormalized_suffix_elements<'a>( + &'a self, + db: &'db dyn Db, + variable: Option>, + ) -> impl Iterator> + 'a { + let variable = variable.unwrap_or(self.variable); + self.suffix_elements() + .skip_while(move |element| element.is_equivalent_to(db, variable)) + .copied() + } + + fn concat(&self, db: &'db dyn Db, other: &Tuple>) -> Tuple> { + match other { + TupleSpec::Fixed(other) => VariableLengthTuple::mixed( + self.prefix_elements().copied(), + self.variable, + self.suffix_elements().chain(other.elements()).copied(), + ), + + Tuple::Variable(other) => { + let variable = UnionType::from_elements( + db, + (self.suffix_elements().copied()) + .chain([self.variable, other.variable]) + .chain(other.prefix_elements().copied()), + ); + VariableLengthTuple::mixed( + self.prefix_elements().copied(), + variable, + other.suffix_elements().copied(), + ) + } + } + } + + fn resize( + &self, + db: &'db dyn Db, + new_length: TupleLength, + ) -> Result>, ResizeTupleError> { + match new_length { + TupleLength::Fixed(new_length) => { + // The number of elements that will get their value from our variable-length + // portion. + let Some(variable_count) = new_length.checked_sub(self.len().minimum()) else { + return Err(ResizeTupleError::TooManyValues); + }; + Ok(Tuple::Fixed(FixedLengthTuple::from_elements( + (self.prefix_elements().copied()) + .chain(std::iter::repeat_n(self.variable, variable_count)) + .chain(self.suffix_elements().copied()), + ))) + } + + TupleLength::Variable(prefix_length, suffix_length) => { + // "Overflow" are elements of our prefix/suffix that will be folded into the + // result's variable-length portion. "Underflow" are elements of the result + // prefix/suffix that will come from our variable-length portion. + let self_prefix_length = self.prefix.len(); + let prefix_underflow = prefix_length.saturating_sub(self_prefix_length); + let self_suffix_length = self.suffix.len(); + let suffix_overflow = self_suffix_length.saturating_sub(suffix_length); + let suffix_underflow = suffix_length.saturating_sub(self_suffix_length); + let prefix = (self.prefix_elements().copied().take(prefix_length)) + .chain(std::iter::repeat_n(self.variable, prefix_underflow)); + let variable = UnionType::from_elements( + db, + (self.prefix_elements().copied().skip(prefix_length)) + .chain(std::iter::once(self.variable)) + .chain(self.suffix_elements().copied().take(suffix_overflow)), + ); + let suffix = std::iter::repeat_n(self.variable, suffix_underflow) + .chain(self.suffix_elements().copied().skip(suffix_overflow)); + Ok(VariableLengthTuple::mixed(prefix, variable, suffix)) + } + } + } + + #[must_use] + fn normalized_impl( + &self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> TupleSpec<'db> { + let prefix = self + .prenormalized_prefix_elements(db, None) + .map(|ty| ty.normalized_impl(db, visitor)) + .collect::>(); + let suffix = self + .prenormalized_suffix_elements(db, None) + .map(|ty| ty.normalized_impl(db, visitor)) + .collect::>(); + let variable = self.variable.normalized_impl(db, visitor); + Self::mixed(prefix, variable, suffix) + } + + fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> TupleSpec<'db> { + Self::mixed( + self.prefix.iter().map(|ty| ty.materialize(db, variance)), + self.variable.materialize(db, variance), + self.suffix.iter().map(|ty| ty.materialize(db, variance)), + ) + } + + fn apply_type_mapping<'a>( + &self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> TupleSpec<'db> { + Self::mixed( + self.prefix + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + self.variable.apply_type_mapping(db, type_mapping), + self.suffix + .iter() + .map(|ty| ty.apply_type_mapping(db, type_mapping)), + ) + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for ty in &self.prefix { + ty.find_legacy_typevars(db, typevars); + } + self.variable.find_legacy_typevars(db, typevars); + for ty in &self.suffix { + ty.find_legacy_typevars(db, typevars); + } + } + + fn has_relation_to( + &self, + db: &'db dyn Db, + other: &Tuple>, + relation: TypeRelation, + ) -> bool { + match other { + Tuple::Fixed(other) => { + // The `...` length specifier of a variable-length tuple type is interpreted + // differently depending on the type of the variable-length elements. + // + // It typically represents the _union_ of all possible lengths. That means that a + // variable-length tuple type is not a subtype of _any_ fixed-length tuple type. + // + // However, as a special case, if the variable-length portion of the tuple is `Any` + // (or any other dynamic type), then the `...` is the _gradual choice_ of all + // possible lengths. This means that `tuple[Any, ...]` can match any tuple of any + // length. + if relation == TypeRelation::Subtyping || !matches!(self.variable, Type::Dynamic(_)) + { + return false; + } + + // In addition, the other tuple must have enough elements to match up with this + // tuple's prefix and suffix, and each of those elements must pairwise satisfy the + // relation. + let mut other_iter = other.elements().copied(); + for self_ty in self.prenormalized_prefix_elements(db, None) { + let Some(other_ty) = other_iter.next() else { + return false; + }; + if !self_ty.has_relation_to(db, other_ty, relation) { + return false; + } + } + let suffix: Vec<_> = self.prenormalized_suffix_elements(db, None).collect(); + for self_ty in suffix.iter().rev() { + let Some(other_ty) = other_iter.next_back() else { + return false; + }; + if !self_ty.has_relation_to(db, other_ty, relation) { + return false; + } + } + + true + } + + Tuple::Variable(other) => { + // When prenormalizing below, we assume that a dynamic variable-length portion of + // one tuple materializes to the variable-length portion of the other tuple. + let self_prenormalize_variable = match self.variable { + Type::Dynamic(_) => Some(other.variable), + _ => None, + }; + let other_prenormalize_variable = match other.variable { + Type::Dynamic(_) => Some(self.variable), + _ => None, + }; + + // The overlapping parts of the prefixes and suffixes must satisfy the relation. + // Any remaining parts must satisfy the relation with the other tuple's + // variable-length part. + if !self + .prenormalized_prefix_elements(db, self_prenormalize_variable) + .zip_longest( + other.prenormalized_prefix_elements(db, other_prenormalize_variable), + ) + .all(|pair| match pair { + EitherOrBoth::Both(self_ty, other_ty) => { + self_ty.has_relation_to(db, other_ty, relation) + } + EitherOrBoth::Left(self_ty) => { + self_ty.has_relation_to(db, other.variable, relation) + } + EitherOrBoth::Right(_) => { + // The rhs has a required element that the lhs is not guaranteed to + // provide. + false + } + }) + { + return false; + } + + let self_suffix: Vec<_> = self + .prenormalized_suffix_elements(db, self_prenormalize_variable) + .collect(); + let other_suffix: Vec<_> = other + .prenormalized_suffix_elements(db, other_prenormalize_variable) + .collect(); + if !(self_suffix.iter().rev()) + .zip_longest(other_suffix.iter().rev()) + .all(|pair| match pair { + EitherOrBoth::Both(self_ty, other_ty) => { + self_ty.has_relation_to(db, *other_ty, relation) + } + EitherOrBoth::Left(self_ty) => { + self_ty.has_relation_to(db, other.variable, relation) + } + EitherOrBoth::Right(_) => { + // The rhs has a required element that the lhs is not guaranteed to + // provide. + false + } + }) + { + return false; + } + + // And lastly, the variable-length portions must satisfy the relation. + self.variable.has_relation_to(db, other.variable, relation) + } + } + } + + fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + self.variable.is_equivalent_to(db, other.variable) + && (self.prenormalized_prefix_elements(db, None)) + .zip_longest(other.prenormalized_prefix_elements(db, None)) + .all(|pair| match pair { + EitherOrBoth::Both(self_ty, other_ty) => self_ty.is_equivalent_to(db, other_ty), + EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false, + }) + && (self.prenormalized_suffix_elements(db, None)) + .zip_longest(other.prenormalized_suffix_elements(db, None)) + .all(|pair| match pair { + EitherOrBoth::Both(self_ty, other_ty) => self_ty.is_equivalent_to(db, other_ty), + EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false, + }) + } +} + +#[allow(unsafe_code)] +unsafe impl salsa::Update for VariableLengthTuple +where + T: salsa::Update, +{ + unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { + let old_value = unsafe { &mut *old_pointer }; + let mut changed = false; + changed |= unsafe { Vec::maybe_update(&raw mut old_value.prefix, new_value.prefix) }; + changed |= unsafe { T::maybe_update(&raw mut old_value.variable, new_value.variable) }; + changed |= unsafe { Vec::maybe_update(&raw mut old_value.suffix, new_value.suffix) }; + changed + } +} + +impl<'db> PyIndex<'db> for &VariableLengthTuple> { + type Item = Type<'db>; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + match Nth::from_index(index) { + Nth::FromStart(index) => { + if let Some(element) = self.prefix.get(index) { + // index is small enough that it lands in the prefix of the tuple. + return Ok(*element); + } + + // index is large enough that it lands past the prefix. The tuple can always be + // large enough that it lands in the variable-length portion. It might also be + // small enough to land in the suffix. + let index_past_prefix = index - self.prefix.len() + 1; + Ok(UnionType::from_elements( + db, + std::iter::once(self.variable) + .chain(self.suffix_elements().copied().take(index_past_prefix)), + )) + } + + Nth::FromEnd(index_from_end) => { + if index_from_end < self.suffix.len() { + // index is small enough that it lands in the suffix of the tuple. + return Ok(self.suffix[self.suffix.len() - index_from_end - 1]); + } + + // index is large enough that it lands past the suffix. The tuple can always be + // large enough that it lands in the variable-length portion. It might also be + // small enough to land in the prefix. + let index_past_suffix = index_from_end - self.suffix.len() + 1; + Ok(UnionType::from_elements( + db, + (self.prefix_elements().rev().copied()) + .take(index_past_suffix) + .rev() + .chain(std::iter::once(self.variable)), + )) + } + } + } +} + +/// A tuple that might be fixed- or variable-length. +/// +/// Our tuple representation can hold instances of any Rust type. For tuples containing Python +/// types, use [`TupleSpec`], which defines some additional type-specific methods. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum Tuple { + Fixed(FixedLengthTuple), + Variable(VariableLengthTuple), +} + +impl Tuple { + pub(crate) fn homogeneous(element: T) -> Self { + VariableLengthTuple::homogeneous(element) + } + + pub(crate) fn from_elements(elements: impl IntoIterator) -> Self { + FixedLengthTuple::from_elements(elements).into() + } + + pub(crate) fn with_capacity(capacity: usize) -> Self { + Tuple::Fixed(FixedLengthTuple::with_capacity(capacity)) + } + + /// Returns an iterator of all of the fixed-length element types of this tuple. + pub(crate) fn fixed_elements(&self) -> impl Iterator + '_ { + match self { + Tuple::Fixed(tuple) => Either::Left(tuple.elements()), + Tuple::Variable(tuple) => Either::Right(tuple.fixed_elements()), + } + } + + /// Returns an iterator of all of the element types of this tuple. Does not deduplicate the + /// elements, and does not distinguish between fixed- and variable-length elements. + pub(crate) fn all_elements(&self) -> impl Iterator + '_ { + match self { + Tuple::Fixed(tuple) => Either::Left(tuple.all_elements()), + Tuple::Variable(tuple) => Either::Right(tuple.all_elements()), + } + } + + pub(crate) fn into_all_elements_with_kind(self) -> impl Iterator> { + match self { + Tuple::Fixed(tuple) => Either::Left(tuple.into_all_elements_with_kind()), + Tuple::Variable(tuple) => Either::Right(tuple.into_all_elements_with_kind()), + } + } + + pub(crate) const fn is_variadic(&self) -> bool { + matches!(self, Tuple::Variable(_)) + } + + /// Returns the length of this tuple. + pub(crate) fn len(&self) -> TupleLength { + match self { + Tuple::Fixed(tuple) => TupleLength::Fixed(tuple.len()), + Tuple::Variable(tuple) => tuple.len(), + } + } + + pub(crate) fn is_empty(&self) -> bool { + match self { + Tuple::Fixed(tuple) => tuple.is_empty(), + Tuple::Variable(_) => false, + } + } + + pub(crate) fn push(&mut self, element: T) { + match self { + Tuple::Fixed(tuple) => tuple.push(element), + Tuple::Variable(tuple) => tuple.push(element), + } + } +} + +impl<'db> Tuple> { + /// Concatenates another tuple to the end of this tuple, returning a new tuple. + pub(crate) fn concat(&self, db: &'db dyn Db, other: &Self) -> Self { + match self { + Tuple::Fixed(tuple) => tuple.concat(other), + Tuple::Variable(tuple) => tuple.concat(db, other), + } + } + + /// Resizes this tuple to a different length, if possible. If this tuple cannot satisfy the + /// desired minimum or maximum length, we return an error. If we return an `Ok` result, the + /// [`len`][Self::len] of the resulting tuple is guaranteed to be equal to `new_length`. + pub(crate) fn resize( + &self, + db: &'db dyn Db, + new_length: TupleLength, + ) -> Result { + match self { + Tuple::Fixed(tuple) => tuple.resize(db, new_length), + Tuple::Variable(tuple) => tuple.resize(db, new_length), + } + } + + pub(crate) fn normalized_impl( + &self, + db: &'db dyn Db, + visitor: &mut TypeTransformer<'db>, + ) -> Self { + match self { + Tuple::Fixed(tuple) => Tuple::Fixed(tuple.normalized_impl(db, visitor)), + Tuple::Variable(tuple) => tuple.normalized_impl(db, visitor), + } + } + + pub(crate) fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { + match self { + Tuple::Fixed(tuple) => Tuple::Fixed(tuple.materialize(db, variance)), + Tuple::Variable(tuple) => tuple.materialize(db, variance), + } + } + + pub(crate) fn apply_type_mapping<'a>( + &self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + match self { + Tuple::Fixed(tuple) => Tuple::Fixed(tuple.apply_type_mapping(db, type_mapping)), + Tuple::Variable(tuple) => tuple.apply_type_mapping(db, type_mapping), + } + } + + fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + Tuple::Fixed(tuple) => tuple.find_legacy_typevars(db, typevars), + Tuple::Variable(tuple) => tuple.find_legacy_typevars(db, typevars), + } + } + + fn has_relation_to(&self, db: &'db dyn Db, other: &Self, relation: TypeRelation) -> bool { + match self { + Tuple::Fixed(self_tuple) => self_tuple.has_relation_to(db, other, relation), + Tuple::Variable(self_tuple) => self_tuple.has_relation_to(db, other, relation), + } + } + + fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { + match (self, other) { + (Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => { + self_tuple.is_equivalent_to(db, other_tuple) + } + (Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => { + self_tuple.is_equivalent_to(db, other_tuple) + } + (Tuple::Fixed(_), Tuple::Variable(_)) | (Tuple::Variable(_), Tuple::Fixed(_)) => false, + } + } + + fn is_disjoint_from_impl( + &'db self, + db: &'db dyn Db, + other: &'db Self, + visitor: &mut PairVisitor<'db>, + ) -> bool { + // Two tuples with an incompatible number of required elements must always be disjoint. + let (self_min, self_max) = self.len().size_hint(); + let (other_min, other_max) = other.len().size_hint(); + if self_max.is_some_and(|max| max < other_min) { + return true; + } + if other_max.is_some_and(|max| max < self_min) { + return true; + } + + // If any of the required elements are pairwise disjoint, the tuples are disjoint as well. + #[allow(clippy::items_after_statements)] + fn any_disjoint<'db>( + db: &'db dyn Db, + a: impl IntoIterator>, + b: impl IntoIterator>, + visitor: &mut PairVisitor<'db>, + ) -> bool { + a.into_iter().zip(b).any(|(self_element, other_element)| { + self_element.is_disjoint_from_impl(db, *other_element, visitor) + }) + } + + match (self, other) { + (Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => { + if any_disjoint(db, self_tuple.elements(), other_tuple.elements(), visitor) { + return true; + } + } + + (Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => { + if any_disjoint( + db, + self_tuple.prefix_elements(), + other_tuple.prefix_elements(), + visitor, + ) { + return true; + } + if any_disjoint( + db, + self_tuple.suffix_elements().rev(), + other_tuple.suffix_elements().rev(), + visitor, + ) { + return true; + } + } + + (Tuple::Fixed(fixed), Tuple::Variable(variable)) + | (Tuple::Variable(variable), Tuple::Fixed(fixed)) => { + if any_disjoint(db, fixed.elements(), variable.prefix_elements(), visitor) { + return true; + } + if any_disjoint( + db, + fixed.elements().rev(), + variable.suffix_elements().rev(), + visitor, + ) { + return true; + } + } + } + + // Two pure homogeneous tuples `tuple[A, ...]` and `tuple[B, ...]` can never be + // disjoint even if A and B are disjoint, because `tuple[()]` would be assignable to + // both. + false + } + + fn is_single_valued(&self, db: &'db dyn Db) -> bool { + match self { + Tuple::Fixed(tuple) => tuple.is_single_valued(db), + Tuple::Variable(_) => false, + } + } +} + +impl From> for Tuple { + fn from(tuple: FixedLengthTuple) -> Self { + Tuple::Fixed(tuple) + } +} + +impl From> for Tuple { + fn from(tuple: VariableLengthTuple) -> Self { + Tuple::Variable(tuple) + } +} + +#[allow(unsafe_code)] +unsafe impl salsa::Update for Tuple +where + T: salsa::Update, +{ + unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { + let old_value = unsafe { &mut *old_pointer }; + match (old_value, new_value) { + (Tuple::Fixed(old), Tuple::Fixed(new)) => unsafe { + FixedLengthTuple::maybe_update(old, new) + }, + (Tuple::Variable(old), Tuple::Variable(new)) => unsafe { + VariableLengthTuple::maybe_update(old, new) + }, + (old_value, new_value) => { + *old_value = new_value; + true + } + } + } +} + +impl<'db> PyIndex<'db> for &Tuple> { + type Item = Type<'db>; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + match self { + Tuple::Fixed(tuple) => tuple.py_index(db, index), + Tuple::Variable(tuple) => tuple.py_index(db, index), + } + } +} + +pub(crate) enum TupleElement { + Fixed(T), + Prefix(T), + Variable(T), + Suffix(T), +} + +/// Unpacks tuple values in an unpacking assignment. +/// +/// You provide a [`TupleLength`] specifying how many assignment targets there are, and which one +/// (if any) is a starred target. You then call [`unpack_tuple`][TupleUnpacker::unpack_tuple] to +/// unpack the values from a rhs tuple into those targets. If the rhs is a union, call +/// `unpack_tuple` separately for each element of the union. We will automatically wrap the types +/// assigned to the starred target in `list`. +pub(crate) struct TupleUnpacker<'db> { + db: &'db dyn Db, + targets: Tuple>, +} + +impl<'db> TupleUnpacker<'db> { + pub(crate) fn new(db: &'db dyn Db, len: TupleLength) -> Self { + let new_builders = |len: usize| std::iter::repeat_with(|| UnionBuilder::new(db)).take(len); + let targets = match len { + TupleLength::Fixed(len) => { + Tuple::Fixed(FixedLengthTuple::from_elements(new_builders(len))) + } + TupleLength::Variable(prefix, suffix) => VariableLengthTuple::mixed( + new_builders(prefix), + UnionBuilder::new(db), + new_builders(suffix), + ), + }; + Self { db, targets } + } + + /// Unpacks a single rhs tuple into the target tuple that we are building. If you want to + /// unpack a single type into each target, call this method with a homogeneous tuple. + /// + /// The lengths of the targets and the rhs have to be compatible, but not necessarily + /// identical. The lengths only have to be identical if both sides are fixed-length; if either + /// side is variable-length, we will pull multiple values out of the rhs variable-length + /// portion, and assign multiple values to the starred target, as needed. + pub(crate) fn unpack_tuple( + &mut self, + values: &Tuple>, + ) -> Result<(), ResizeTupleError> { + let values = values.resize(self.db, self.targets.len())?; + match (&mut self.targets, &values) { + (Tuple::Fixed(targets), Tuple::Fixed(values)) => { + targets.unpack_tuple(values); + } + (Tuple::Variable(targets), Tuple::Variable(values)) => { + targets.unpack_tuple(self.db, values); + } + _ => panic!("should have ensured that tuples are the same length"), + } + Ok(()) + } + + /// Returns the unpacked types for each target. If you called + /// [`unpack_tuple`][TupleUnpacker::unpack_tuple] multiple times, each target type will be the + /// union of the type unpacked into that target from each of the rhs tuples. If there is a + /// starred target, we will each unpacked type in `list`. + pub(crate) fn into_types(self) -> impl Iterator> { + self.targets + .into_all_elements_with_kind() + .map(|builder| match builder { + TupleElement::Variable(builder) => builder.try_build().unwrap_or_else(|| { + KnownClass::List.to_specialized_instance(self.db, [Type::unknown()]) + }), + TupleElement::Fixed(builder) + | TupleElement::Prefix(builder) + | TupleElement::Suffix(builder) => { + builder.try_build().unwrap_or_else(Type::unknown) + } + }) + } +} + +impl<'db> FixedLengthTuple> { + fn unpack_tuple(&mut self, values: &FixedLengthTuple>) { + // We have already verified above that the two tuples have the same length. + for (target, value) in self.0.iter_mut().zip(values.elements().copied()) { + target.add_in_place(value); + } + } +} + +impl<'db> VariableLengthTuple> { + fn unpack_tuple(&mut self, db: &'db dyn Db, values: &VariableLengthTuple>) { + // We have already verified above that the two tuples have the same length. + for (target, value) in (self.prefix.iter_mut()).zip(values.prefix_elements().copied()) { + target.add_in_place(value); + } + self.variable + .add_in_place(KnownClass::List.to_specialized_instance(db, [values.variable])); + for (target, value) in (self.suffix.iter_mut()).zip(values.suffix_elements().copied()) { + target.add_in_place(value); + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum ResizeTupleError { + TooFewValues, + TooManyValues, +} diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs new file mode 100644 index 0000000000000..d4b1a5bcf4ece --- /dev/null +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -0,0 +1,276 @@ +use std::cmp::Ordering; + +use crate::db::Db; + +use super::{ + DynamicType, SuperOwnerKind, TodoType, Type, TypeIsType, class_base::ClassBase, + subclass_of::SubclassOfInner, +}; + +/// Return an [`Ordering`] that describes the canonical order in which two types should appear +/// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them +/// to be compared for equivalence. +/// +/// Two intersections are compared lexicographically. Element types in the intersection must +/// already be sorted. Two unions are never compared in this function because DNF does not permit +/// nested unions. +/// +/// ## Why not just implement [`Ord`] on [`Type`]? +/// +/// It would be fairly easy to slap `#[derive(PartialOrd, Ord)]` on [`Type`], and the ordering we +/// create here is not user-facing. However, it doesn't really "make sense" for `Type` to implement +/// [`Ord`] in terms of the semantics. There are many different ways in which you could plausibly +/// sort a list of types; this is only one (somewhat arbitrary, at times) possible ordering. +pub(super) fn union_or_intersection_elements_ordering<'db>( + db: &'db dyn Db, + left: &Type<'db>, + right: &Type<'db>, +) -> Ordering { + debug_assert_eq!( + *left, + left.normalized(db), + "`left` must be normalized before a meaningful ordering can be established" + ); + debug_assert_eq!( + *right, + right.normalized(db), + "`right` must be normalized before a meaningful ordering can be established" + ); + + if left == right { + return Ordering::Equal; + } + + match (left, right) { + (Type::Never, _) => Ordering::Less, + (_, Type::Never) => Ordering::Greater, + + (Type::LiteralString, _) => Ordering::Less, + (_, Type::LiteralString) => Ordering::Greater, + + (Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left.cmp(right), + (Type::BooleanLiteral(_), _) => Ordering::Less, + (_, Type::BooleanLiteral(_)) => Ordering::Greater, + + (Type::IntLiteral(left), Type::IntLiteral(right)) => left.cmp(right), + (Type::IntLiteral(_), _) => Ordering::Less, + (_, Type::IntLiteral(_)) => Ordering::Greater, + + (Type::StringLiteral(left), Type::StringLiteral(right)) => left.cmp(right), + (Type::StringLiteral(_), _) => Ordering::Less, + (_, Type::StringLiteral(_)) => Ordering::Greater, + + (Type::BytesLiteral(left), Type::BytesLiteral(right)) => left.cmp(right), + (Type::BytesLiteral(_), _) => Ordering::Less, + (_, Type::BytesLiteral(_)) => Ordering::Greater, + + (Type::FunctionLiteral(left), Type::FunctionLiteral(right)) => left.cmp(right), + (Type::FunctionLiteral(_), _) => Ordering::Less, + (_, Type::FunctionLiteral(_)) => Ordering::Greater, + + (Type::BoundMethod(left), Type::BoundMethod(right)) => left.cmp(right), + (Type::BoundMethod(_), _) => Ordering::Less, + (_, Type::BoundMethod(_)) => Ordering::Greater, + + (Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right), + (Type::MethodWrapper(_), _) => Ordering::Less, + (_, Type::MethodWrapper(_)) => Ordering::Greater, + + (Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right), + (Type::WrapperDescriptor(_), _) => Ordering::Less, + (_, Type::WrapperDescriptor(_)) => Ordering::Greater, + + (Type::DataclassDecorator(left), Type::DataclassDecorator(right)) => { + left.bits().cmp(&right.bits()) + } + (Type::DataclassDecorator(_), _) => Ordering::Less, + (_, Type::DataclassDecorator(_)) => Ordering::Greater, + + (Type::DataclassTransformer(left), Type::DataclassTransformer(right)) => { + left.bits().cmp(&right.bits()) + } + (Type::DataclassTransformer(_), _) => Ordering::Less, + (_, Type::DataclassTransformer(_)) => Ordering::Greater, + + (Type::Callable(left), Type::Callable(right)) => left.cmp(right), + (Type::Callable(_), _) => Ordering::Less, + (_, Type::Callable(_)) => Ordering::Greater, + + (Type::Tuple(left), Type::Tuple(right)) => left.cmp(right), + (Type::Tuple(_), _) => Ordering::Less, + (_, Type::Tuple(_)) => Ordering::Greater, + + (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => left.cmp(right), + (Type::ModuleLiteral(_), _) => Ordering::Less, + (_, Type::ModuleLiteral(_)) => Ordering::Greater, + + (Type::ClassLiteral(left), Type::ClassLiteral(right)) => left.cmp(right), + (Type::ClassLiteral(_), _) => Ordering::Less, + (_, Type::ClassLiteral(_)) => Ordering::Greater, + + (Type::GenericAlias(left), Type::GenericAlias(right)) => left.cmp(right), + (Type::GenericAlias(_), _) => Ordering::Less, + (_, Type::GenericAlias(_)) => Ordering::Greater, + + (Type::SubclassOf(left), Type::SubclassOf(right)) => { + match (left.subclass_of(), right.subclass_of()) { + (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => left.cmp(&right), + (SubclassOfInner::Class(_), _) => Ordering::Less, + (_, SubclassOfInner::Class(_)) => Ordering::Greater, + (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + } + } + + (Type::SubclassOf(_), _) => Ordering::Less, + (_, Type::SubclassOf(_)) => Ordering::Greater, + + (Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, *left, *right), + (Type::TypeIs(_), _) => Ordering::Less, + (_, Type::TypeIs(_)) => Ordering::Greater, + + (Type::NominalInstance(left), Type::NominalInstance(right)) => left.class.cmp(&right.class), + (Type::NominalInstance(_), _) => Ordering::Less, + (_, Type::NominalInstance(_)) => Ordering::Greater, + + (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { + left_proto.cmp(right_proto) + } + (Type::ProtocolInstance(_), _) => Ordering::Less, + (_, Type::ProtocolInstance(_)) => Ordering::Greater, + + (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), + (Type::TypeVar(_), _) => Ordering::Less, + (_, Type::TypeVar(_)) => Ordering::Greater, + + (Type::AlwaysTruthy, _) => Ordering::Less, + (_, Type::AlwaysTruthy) => Ordering::Greater, + + (Type::AlwaysFalsy, _) => Ordering::Less, + (_, Type::AlwaysFalsy) => Ordering::Greater, + + (Type::BoundSuper(left), Type::BoundSuper(right)) => { + (match (left.pivot_class(db), right.pivot_class(db)) { + (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), + (ClassBase::Class(_), _) => Ordering::Less, + (_, ClassBase::Class(_)) => Ordering::Greater, + + (ClassBase::Protocol, _) => Ordering::Less, + (_, ClassBase::Protocol) => Ordering::Greater, + + (ClassBase::Generic, _) => Ordering::Less, + (_, ClassBase::Generic) => Ordering::Greater, + + (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + }) + .then_with(|| match (left.owner(db), right.owner(db)) { + (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => left.cmp(&right), + (SuperOwnerKind::Class(_), _) => Ordering::Less, + (_, SuperOwnerKind::Class(_)) => Ordering::Greater, + (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { + left.class.cmp(&right.class) + } + (SuperOwnerKind::Instance(_), _) => Ordering::Less, + (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, + (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + }) + } + (Type::BoundSuper(_), _) => Ordering::Less, + (_, Type::BoundSuper(_)) => Ordering::Greater, + + (Type::SpecialForm(left), Type::SpecialForm(right)) => left.cmp(right), + (Type::SpecialForm(_), _) => Ordering::Less, + (_, Type::SpecialForm(_)) => Ordering::Greater, + + (Type::KnownInstance(left), Type::KnownInstance(right)) => left.cmp(right), + (Type::KnownInstance(_), _) => Ordering::Less, + (_, Type::KnownInstance(_)) => Ordering::Greater, + + (Type::PropertyInstance(left), Type::PropertyInstance(right)) => left.cmp(right), + (Type::PropertyInstance(_), _) => Ordering::Less, + (_, Type::PropertyInstance(_)) => Ordering::Greater, + + (Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right), + (Type::Dynamic(_), _) => Ordering::Less, + (_, Type::Dynamic(_)) => Ordering::Greater, + + (Type::Union(_), _) | (_, Type::Union(_)) => { + unreachable!("our type representation does not permit nested unions"); + } + + (Type::Intersection(left), Type::Intersection(right)) => { + // Lexicographically compare the elements of the two unequal intersections. + let left_positive = left.positive(db); + let right_positive = right.positive(db); + if left_positive.len() != right_positive.len() { + return left_positive.len().cmp(&right_positive.len()); + } + let left_negative = left.negative(db); + let right_negative = right.negative(db); + if left_negative.len() != right_negative.len() { + return left_negative.len().cmp(&right_negative.len()); + } + for (left, right) in left_positive.iter().zip(right_positive) { + let ordering = union_or_intersection_elements_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + for (left, right) in left_negative.iter().zip(right_negative) { + let ordering = union_or_intersection_elements_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + + unreachable!("Two equal, normalized intersections should share the same Salsa ID") + } + } +} + +/// Determine a canonical order for two instances of [`DynamicType`]. +fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering { + match (left, right) { + (DynamicType::Any, _) => Ordering::Less, + (_, DynamicType::Any) => Ordering::Greater, + + (DynamicType::Unknown, _) => Ordering::Less, + (_, DynamicType::Unknown) => Ordering::Greater, + + #[cfg(debug_assertions)] + (DynamicType::Todo(TodoType(left)), DynamicType::Todo(TodoType(right))) => left.cmp(right), + + #[cfg(not(debug_assertions))] + (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, + + (DynamicType::TodoPEP695ParamSpec, _) => Ordering::Less, + (_, DynamicType::TodoPEP695ParamSpec) => Ordering::Greater, + } +} + +/// Determine a canonical order for two instances of [`TypeIsType`]. +/// +/// The following criteria are considered, in order: +/// * Boundness: Unbound precedes bound +/// * Symbol name: String comparison +/// * Guarded type: [`union_or_intersection_elements_ordering`] +fn typeis_ordering(db: &dyn Db, left: TypeIsType, right: TypeIsType) -> Ordering { + let (left_ty, right_ty) = (left.return_type(db), right.return_type(db)); + + match (left.place_info(db), right.place_info(db)) { + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + + (None, None) => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), + + (Some(_), Some(_)) => match left.place_name(db).cmp(&right.place_name(db)) { + Ordering::Equal => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), + ordering => ordering, + }, + } +} diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs new file mode 100644 index 0000000000000..e3c74dd25ac56 --- /dev/null +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -0,0 +1,249 @@ +use std::borrow::Cow; + +use ruff_db::parsed::ParsedModuleRef; +use rustc_hash::FxHashMap; + +use ruff_python_ast::{self as ast, AnyNodeRef}; + +use crate::Db; +use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; +use crate::semantic_index::place::ScopeId; +use crate::types::tuple::{ResizeTupleError, Tuple, TupleLength, TupleUnpacker}; +use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types}; +use crate::unpack::{UnpackKind, UnpackValue}; + +use super::context::InferContext; +use super::diagnostic::INVALID_ASSIGNMENT; + +/// Unpacks the value expression type to their respective targets. +pub(crate) struct Unpacker<'db, 'ast> { + context: InferContext<'db, 'ast>, + targets: FxHashMap>, +} + +impl<'db, 'ast> Unpacker<'db, 'ast> { + pub(crate) fn new( + db: &'db dyn Db, + target_scope: ScopeId<'db>, + module: &'ast ParsedModuleRef, + ) -> Self { + Self { + context: InferContext::new(db, target_scope, module), + targets: FxHashMap::default(), + } + } + + fn db(&self) -> &'db dyn Db { + self.context.db() + } + + fn module(&self) -> &'ast ParsedModuleRef { + self.context.module() + } + + /// Unpack the value to the target expression. + pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) { + debug_assert!( + matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)), + "Unpacking target must be a list or tuple expression" + ); + + let value_type = infer_expression_types(self.db(), value.expression()) + .expression_type(value.expression().node_ref(self.db(), self.module())); + + let value_type = match value.kind() { + UnpackKind::Assign => { + if self.context.in_stub() + && value + .expression() + .node_ref(self.db(), self.module()) + .is_ellipsis_literal_expr() + { + Type::unknown() + } else { + value_type + } + } + UnpackKind::Iterable => value_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + value_type, + value.as_any_node_ref(self.db(), self.module()), + ); + err.fallback_element_type(self.db()) + }), + UnpackKind::ContextManager => value_type.try_enter(self.db()).unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + value_type, + value.as_any_node_ref(self.db(), self.module()), + ); + err.fallback_enter_type(self.db()) + }), + }; + + self.unpack_inner( + target, + value.as_any_node_ref(self.db(), self.module()), + value_type, + ); + } + + fn unpack_inner( + &mut self, + target: &ast::Expr, + value_expr: AnyNodeRef<'_>, + value_ty: Type<'db>, + ) { + match target { + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { + self.targets.insert(target.into(), value_ty); + } + ast::Expr::Starred(ast::ExprStarred { value, .. }) => { + self.unpack_inner(value, value_expr, value_ty); + } + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + let target_len = match elts.iter().position(ast::Expr::is_starred_expr) { + Some(starred_index) => { + TupleLength::Variable(starred_index, elts.len() - (starred_index + 1)) + } + None => TupleLength::Fixed(elts.len()), + }; + let mut unpacker = TupleUnpacker::new(self.db(), target_len); + + let unpack_types = match value_ty { + Type::Union(union_ty) => union_ty.elements(self.db()), + _ => std::slice::from_ref(&value_ty), + }; + + for ty in unpack_types.iter().copied() { + let tuple = match ty { + Type::Tuple(tuple_ty) => Cow::Borrowed(tuple_ty.tuple(self.db())), + Type::StringLiteral(string_literal_ty) => { + // We could go further and deconstruct to an array of `StringLiteral` + // with each individual character, instead of just an array of + // `LiteralString`, but there would be a cost and it's not clear that + // it's worth it. + Cow::Owned(Tuple::from_elements(std::iter::repeat_n( + Type::LiteralString, + string_literal_ty.python_len(self.db()), + ))) + } + Type::LiteralString => Cow::Owned(Tuple::homogeneous(Type::LiteralString)), + _ => { + // TODO: Update our iterator protocol machinery to return a tuple + // describing the returned values in more detail, when we can. + Cow::Owned(Tuple::homogeneous( + ty.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, ty, value_expr); + err.fallback_element_type(self.db()) + }), + )) + } + }; + + if let Err(err) = unpacker.unpack_tuple(tuple.as_ref()) { + unpacker + .unpack_tuple(&Tuple::homogeneous(Type::unknown())) + .expect("adding a homogeneous tuple should always succeed"); + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + match err { + ResizeTupleError::TooManyValues => { + let mut diag = + builder.into_diagnostic("Too many values to unpack"); + diag.set_primary_message(format_args!( + "Expected {}", + target_len.display_minimum(), + )); + diag.annotate(self.context.secondary(value_expr).message( + format_args!("Got {}", tuple.len().display_minimum()), + )); + } + ResizeTupleError::TooFewValues => { + let mut diag = + builder.into_diagnostic("Not enough values to unpack"); + diag.set_primary_message(format_args!( + "Expected {}", + target_len.display_minimum(), + )); + diag.annotate(self.context.secondary(value_expr).message( + format_args!("Got {}", tuple.len().display_maximum()), + )); + } + } + } + } + } + + // We constructed unpacker above using the length of elts, so the zip should + // consume the same number of elements from each. + for (target, value_ty) in elts.iter().zip(unpacker.into_types()) { + self.unpack_inner(target, value_expr, value_ty); + } + } + _ => {} + } + } + + pub(crate) fn finish(mut self) -> UnpackResult<'db> { + self.targets.shrink_to_fit(); + UnpackResult { + diagnostics: self.context.finish(), + targets: self.targets, + cycle_fallback_type: None, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)] +pub(crate) struct UnpackResult<'db> { + targets: FxHashMap>, + diagnostics: TypeCheckDiagnostics, + + /// The fallback type for missing expressions. + /// + /// This is used only when constructing a cycle-recovery `UnpackResult`. + cycle_fallback_type: Option>, +} + +impl<'db> UnpackResult<'db> { + /// Returns the inferred type for a given sub-expression of the left-hand side target + /// of an unpacking assignment. + /// + /// # Panics + /// + /// May panic if a scoped expression ID is passed in that does not correspond to a sub- + /// expression of the target. + #[track_caller] + pub(crate) fn expression_type(&self, expr_id: impl Into) -> Type<'db> { + self.try_expression_type(expr_id).expect( + "expression should belong to this `UnpackResult` and \ + `Unpacker` should have inferred a type for it", + ) + } + + pub(crate) fn try_expression_type( + &self, + expr: impl Into, + ) -> Option> { + self.targets + .get(&expr.into()) + .copied() + .or(self.cycle_fallback_type) + } + + /// Returns the diagnostics in this unpacking assignment. + pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { + &self.diagnostics + } + + pub(crate) fn cycle_fallback(cycle_fallback_type: Type<'db>) -> Self { + Self { + targets: FxHashMap::default(), + diagnostics: TypeCheckDiagnostics::default(), + cycle_fallback_type: Some(cycle_fallback_type), + } + } +} diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs new file mode 100644 index 0000000000000..1deaef62fca47 --- /dev/null +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -0,0 +1,275 @@ +use crate::{ + Db, FxIndexSet, + types::{ + BoundMethodType, BoundSuperType, CallableType, GenericAlias, IntersectionType, + KnownInstanceType, MethodWrapperKind, NominalInstanceType, PropertyInstanceType, + ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, TypeIsType, TypeVarInstance, + UnionType, + class::walk_generic_alias, + function::{FunctionType, walk_function_type}, + instance::{walk_nominal_instance_type, walk_protocol_instance_type}, + subclass_of::walk_subclass_of_type, + tuple::{TupleType, walk_tuple_type}, + walk_bound_method_type, walk_bound_super_type, walk_callable_type, walk_intersection_type, + walk_known_instance_type, walk_method_wrapper_type, walk_property_instance_type, + walk_type_alias_type, walk_type_var_type, walk_typeis_type, walk_union, + }, +}; + +/// A visitor trait that recurses into nested types. +/// +/// The trait does not guard against infinite recursion out of the box, +/// but it makes it easy for implementors of the trait to do so. +/// See [`any_over_type`] for an example of how to do this. +pub(crate) trait TypeVisitor<'db> { + fn visit_type(&mut self, db: &'db dyn Db, ty: Type<'db>); + + fn visit_union_type(&mut self, db: &'db dyn Db, union: UnionType<'db>) { + walk_union(db, union, self); + } + + fn visit_intersection_type(&mut self, db: &'db dyn Db, intersection: IntersectionType<'db>) { + walk_intersection_type(db, intersection, self); + } + + fn visit_tuple_type(&mut self, db: &'db dyn Db, tuple: TupleType<'db>) { + walk_tuple_type(db, tuple, self); + } + + fn visit_callable_type(&mut self, db: &'db dyn Db, callable: CallableType<'db>) { + walk_callable_type(db, callable, self); + } + + fn visit_property_instance_type( + &mut self, + db: &'db dyn Db, + property: PropertyInstanceType<'db>, + ) { + walk_property_instance_type(db, property, self); + } + + fn visit_typeis_type(&mut self, db: &'db dyn Db, type_is: TypeIsType<'db>) { + walk_typeis_type(db, type_is, self); + } + + fn visit_subclass_of_type(&mut self, db: &'db dyn Db, subclass_of: SubclassOfType<'db>) { + walk_subclass_of_type(db, subclass_of, self); + } + + fn visit_generic_alias_type(&mut self, db: &'db dyn Db, alias: GenericAlias<'db>) { + walk_generic_alias(db, alias, self); + } + + fn visit_function_type(&mut self, db: &'db dyn Db, function: FunctionType<'db>) { + walk_function_type(db, function, self); + } + + fn visit_bound_method_type(&mut self, db: &'db dyn Db, method: BoundMethodType<'db>) { + walk_bound_method_type(db, method, self); + } + + fn visit_bound_super_type(&mut self, db: &'db dyn Db, bound_super: BoundSuperType<'db>) { + walk_bound_super_type(db, bound_super, self); + } + + fn visit_nominal_instance_type(&mut self, db: &'db dyn Db, nominal: NominalInstanceType<'db>) { + walk_nominal_instance_type(db, nominal, self); + } + + fn visit_type_var_type(&mut self, db: &'db dyn Db, type_var: TypeVarInstance<'db>) { + walk_type_var_type(db, type_var, self); + } + + fn visit_protocol_instance_type( + &mut self, + db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, + ) { + walk_protocol_instance_type(db, protocol, self); + } + + fn visit_method_wrapper_type( + &mut self, + db: &'db dyn Db, + method_wrapper: MethodWrapperKind<'db>, + ) { + walk_method_wrapper_type(db, method_wrapper, self); + } + + fn visit_known_instance_type( + &mut self, + db: &'db dyn Db, + known_instance: KnownInstanceType<'db>, + ) { + walk_known_instance_type(db, known_instance, self); + } + + fn visit_type_alias_type(&mut self, db: &'db dyn Db, type_alias: TypeAliasType<'db>) { + walk_type_alias_type(db, type_alias, self); + } +} + +/// Enumeration of types that may contain other types, such as unions, intersections, and generics. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +enum NonAtomicType<'db> { + Union(UnionType<'db>), + Intersection(IntersectionType<'db>), + Tuple(TupleType<'db>), + FunctionLiteral(FunctionType<'db>), + BoundMethod(BoundMethodType<'db>), + BoundSuper(BoundSuperType<'db>), + MethodWrapper(MethodWrapperKind<'db>), + Callable(CallableType<'db>), + GenericAlias(GenericAlias<'db>), + KnownInstance(KnownInstanceType<'db>), + SubclassOf(SubclassOfType<'db>), + NominalInstance(NominalInstanceType<'db>), + PropertyInstance(PropertyInstanceType<'db>), + TypeIs(TypeIsType<'db>), + TypeVar(TypeVarInstance<'db>), + ProtocolInstance(ProtocolInstanceType<'db>), +} + +enum TypeKind<'db> { + Atomic, + NonAtomic(NonAtomicType<'db>), +} + +impl<'db> From> for TypeKind<'db> { + fn from(ty: Type<'db>) -> Self { + match ty { + Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::Never + | Type::LiteralString + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::WrapperDescriptor(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::SpecialForm(_) + | Type::Dynamic(_) => TypeKind::Atomic, + + // Non-atomic types + Type::FunctionLiteral(function) => { + TypeKind::NonAtomic(NonAtomicType::FunctionLiteral(function)) + } + Type::Intersection(intersection) => { + TypeKind::NonAtomic(NonAtomicType::Intersection(intersection)) + } + Type::Union(union) => TypeKind::NonAtomic(NonAtomicType::Union(union)), + Type::Tuple(tuple) => TypeKind::NonAtomic(NonAtomicType::Tuple(tuple)), + Type::BoundMethod(method) => TypeKind::NonAtomic(NonAtomicType::BoundMethod(method)), + Type::BoundSuper(bound_super) => { + TypeKind::NonAtomic(NonAtomicType::BoundSuper(bound_super)) + } + Type::MethodWrapper(method_wrapper) => { + TypeKind::NonAtomic(NonAtomicType::MethodWrapper(method_wrapper)) + } + Type::Callable(callable) => TypeKind::NonAtomic(NonAtomicType::Callable(callable)), + Type::GenericAlias(alias) => TypeKind::NonAtomic(NonAtomicType::GenericAlias(alias)), + Type::KnownInstance(known_instance) => { + TypeKind::NonAtomic(NonAtomicType::KnownInstance(known_instance)) + } + Type::SubclassOf(subclass_of) => { + TypeKind::NonAtomic(NonAtomicType::SubclassOf(subclass_of)) + } + Type::NominalInstance(nominal) => { + TypeKind::NonAtomic(NonAtomicType::NominalInstance(nominal)) + } + Type::ProtocolInstance(protocol) => { + TypeKind::NonAtomic(NonAtomicType::ProtocolInstance(protocol)) + } + Type::PropertyInstance(property) => { + TypeKind::NonAtomic(NonAtomicType::PropertyInstance(property)) + } + Type::TypeVar(type_var) => TypeKind::NonAtomic(NonAtomicType::TypeVar(type_var)), + Type::TypeIs(type_is) => TypeKind::NonAtomic(NonAtomicType::TypeIs(type_is)), + } + } +} + +fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + non_atomic_type: NonAtomicType<'db>, + visitor: &mut V, +) { + match non_atomic_type { + NonAtomicType::FunctionLiteral(function) => visitor.visit_function_type(db, function), + NonAtomicType::Intersection(intersection) => { + visitor.visit_intersection_type(db, intersection); + } + NonAtomicType::Union(union) => visitor.visit_union_type(db, union), + NonAtomicType::Tuple(tuple) => visitor.visit_tuple_type(db, tuple), + NonAtomicType::BoundMethod(method) => visitor.visit_bound_method_type(db, method), + NonAtomicType::BoundSuper(bound_super) => visitor.visit_bound_super_type(db, bound_super), + NonAtomicType::MethodWrapper(method_wrapper) => { + visitor.visit_method_wrapper_type(db, method_wrapper); + } + NonAtomicType::Callable(callable) => visitor.visit_callable_type(db, callable), + NonAtomicType::GenericAlias(alias) => visitor.visit_generic_alias_type(db, alias), + NonAtomicType::KnownInstance(known_instance) => { + visitor.visit_known_instance_type(db, known_instance); + } + NonAtomicType::SubclassOf(subclass_of) => visitor.visit_subclass_of_type(db, subclass_of), + NonAtomicType::NominalInstance(nominal) => visitor.visit_nominal_instance_type(db, nominal), + NonAtomicType::PropertyInstance(property) => { + visitor.visit_property_instance_type(db, property); + } + NonAtomicType::TypeIs(type_is) => visitor.visit_typeis_type(db, type_is), + NonAtomicType::TypeVar(type_var) => visitor.visit_type_var_type(db, type_var), + NonAtomicType::ProtocolInstance(protocol) => { + visitor.visit_protocol_instance_type(db, protocol); + } + } +} + +/// Return `true` if `ty`, or any of the types contained in `ty`, match the closure passed in. +/// +/// The function guards against infinite recursion +/// by keeping track of the non-atomic types it has already seen. +pub(super) fn any_over_type<'db>( + db: &'db dyn Db, + ty: Type<'db>, + query: &dyn Fn(Type<'db>) -> bool, +) -> bool { + struct AnyOverTypeVisitor<'db, 'a> { + query: &'a dyn Fn(Type<'db>) -> bool, + seen_types: FxIndexSet>, + found_matching_type: bool, + } + + impl<'db> TypeVisitor<'db> for AnyOverTypeVisitor<'db, '_> { + fn visit_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { + if self.found_matching_type { + return; + } + self.found_matching_type |= (self.query)(ty); + if self.found_matching_type { + return; + } + match TypeKind::from(ty) { + TypeKind::Atomic => {} + TypeKind::NonAtomic(non_atomic_type) => { + if !self.seen_types.insert(non_atomic_type) { + // If we have already seen this type, we can skip it. + return; + } + walk_non_atomic_type(db, non_atomic_type, self); + } + } + } + } + + let mut visitor = AnyOverTypeVisitor { + query, + seen_types: FxIndexSet::default(), + found_matching_type: false, + }; + visitor.visit_type(db, ty); + visitor.found_matching_type +} diff --git a/crates/red_knot_python_semantic/src/unpack.rs b/crates/ty_python_semantic/src/unpack.rs similarity index 75% rename from crates/red_knot_python_semantic/src/unpack.rs rename to crates/ty_python_semantic/src/unpack.rs index 4dadab3397a19..42823aa628b62 100644 --- a/crates/red_knot_python_semantic/src/unpack.rs +++ b/crates/ty_python_semantic/src/unpack.rs @@ -1,12 +1,12 @@ use ruff_db::files::File; +use ruff_db::parsed::ParsedModuleRef; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; +use crate::Db; use crate::ast_node_ref::AstNodeRef; -use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId}; use crate::semantic_index::expression::Expression; -use crate::semantic_index::symbol::{FileScopeId, ScopeId}; -use crate::Db; +use crate::semantic_index::place::{FileScopeId, ScopeId}; /// This ingredient represents a single unpacking. /// @@ -30,31 +30,39 @@ use crate::Db; pub(crate) struct Unpack<'db> { pub(crate) file: File, - pub(crate) file_scope: FileScopeId, + pub(crate) value_file_scope: FileScopeId, + + pub(crate) target_file_scope: FileScopeId, /// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target /// expression is `(a, b)`. #[no_eq] - #[return_ref] #[tracked] - pub(crate) target: AstNodeRef, + #[returns(ref)] + pub(crate) _target: AstNodeRef, /// The ingredient representing the value expression of the unpacking. For example, in /// `(a, b) = (1, 2)`, the value expression is `(1, 2)`. pub(crate) value: UnpackValue<'db>, - - count: countme::Count>, } impl<'db> Unpack<'db> { - /// Returns the scope where the unpacking is happening. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) + pub(crate) fn target<'ast>( + self, + db: &'db dyn Db, + parsed: &'ast ParsedModuleRef, + ) -> &'ast ast::Expr { + self._target(db).node(parsed) + } + + /// Returns the scope where the unpack target expression belongs to. + pub(crate) fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.target_file_scope(db).to_scope_id(db, self.file(db)) } /// Returns the range of the unpack target expression. - pub(crate) fn range(self, db: &'db dyn Db) -> TextRange { - self.target(db).range() + pub(crate) fn range(self, db: &'db dyn Db, module: &ParsedModuleRef) -> TextRange { + self.target(db, module).range() } } @@ -77,20 +85,13 @@ impl<'db> UnpackValue<'db> { self.expression } - /// Returns the [`ScopedExpressionId`] of the underlying expression. - pub(crate) fn scoped_expression_id( + /// Returns the expression as an [`AnyNodeRef`]. + pub(crate) fn as_any_node_ref<'ast>( self, db: &'db dyn Db, - scope: ScopeId<'db>, - ) -> ScopedExpressionId { - self.expression() - .node_ref(db) - .scoped_expression_id(db, scope) - } - - /// Returns the expression as an [`AnyNodeRef`]. - pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> { - self.expression().node_ref(db).node().into() + module: &'ast ParsedModuleRef, + ) -> AnyNodeRef<'ast> { + self.expression().node_ref(db, module).into() } pub(crate) const fn kind(self) -> UnpackKind { diff --git a/crates/ty_python_semantic/src/util/diagnostics.rs b/crates/ty_python_semantic/src/util/diagnostics.rs new file mode 100644 index 0000000000000..ca39583cbb15d --- /dev/null +++ b/crates/ty_python_semantic/src/util/diagnostics.rs @@ -0,0 +1,114 @@ +use crate::{Db, Program, PythonVersionWithSource}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; +use std::fmt::Write; + +/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred. +/// +/// ty can infer the Python version from various sources, such as command-line arguments, +/// configuration files, or defaults. +pub fn add_inferred_python_version_hint_to_diagnostic( + db: &dyn Db, + diagnostic: &mut Diagnostic, + action: &str, +) { + let program = Program::get(db); + let PythonVersionWithSource { version, source } = program.python_version_with_source(db); + + match source { + crate::PythonVersionSource::Cli => { + diagnostic.info(format_args!( + "Python {version} was assumed when {action} because it was specified on the command line", + )); + } + crate::PythonVersionSource::ConfigFile(source) => { + if let Some(span) = source.span(db) { + let mut sub_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!("Python {version} was assumed when {action}"), + ); + sub_diagnostic.annotate(Annotation::primary(span).message(format_args!( + "Python {version} assumed due to this configuration setting" + ))); + diagnostic.sub(sub_diagnostic); + } else { + diagnostic.info(format_args!( + "Python {version} was assumed when {action} because of your configuration file(s)", + )); + } + } + crate::PythonVersionSource::PyvenvCfgFile(source) => { + if let Some(span) = source.span(db) { + let mut sub_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!( + "Python {version} was assumed when {action} because of your virtual environment" + ), + ); + sub_diagnostic.annotate( + Annotation::primary(span) + .message("Python version inferred from virtual environment metadata file"), + ); + // TODO: it would also be nice to tell them how we resolved their virtual environment... + diagnostic.sub(sub_diagnostic); + } else { + diagnostic.info(format_args!( + "Python {version} was assumed when {action} because \ + your virtual environment's pyvenv.cfg file indicated \ + it was the Python version being used", + )); + } + diagnostic.info( + "No Python version was specified on the command line \ + or in a configuration file", + ); + } + crate::PythonVersionSource::InstallationDirectoryLayout { + site_packages_parent_dir, + } => { + // TODO: it would also be nice to tell them how we resolved this Python installation... + diagnostic.info(format_args!( + "Python {version} was assumed when {action} \ + because of the layout of your Python installation" + )); + diagnostic.info(format_args!( + "The primary `site-packages` directory of your installation was found \ + at `lib/{site_packages_parent_dir}/site-packages/`" + )); + diagnostic.info( + "No Python version was specified on the command line \ + or in a configuration file", + ); + } + crate::PythonVersionSource::Default => { + diagnostic.info(format_args!( + "Python {version} was assumed when {action} \ + because it is the newest Python version supported by ty, \ + and neither a command-line argument nor a configuration setting was provided", + )); + } + } +} + +/// Format a list of elements as a human-readable enumeration. +/// +/// Encloses every element in backticks (`1`, `2` and `3`). +pub(crate) fn format_enumeration(elements: I) -> String +where + I: IntoIterator, + IT: ExactSizeIterator + DoubleEndedIterator, + D: std::fmt::Display, +{ + let mut elements = elements.into_iter(); + debug_assert!(elements.len() >= 2); + + let final_element = elements.next_back().unwrap(); + let penultimate_element = elements.next_back().unwrap(); + + let mut buffer = String::new(); + for element in elements { + write!(&mut buffer, "`{element}`, ").ok(); + } + write!(&mut buffer, "`{penultimate_element}` and `{final_element}`").ok(); + + buffer +} diff --git a/crates/ty_python_semantic/src/util/get_size.rs b/crates/ty_python_semantic/src/util/get_size.rs new file mode 100644 index 0000000000000..d77cf5c14cb76 --- /dev/null +++ b/crates/ty_python_semantic/src/util/get_size.rs @@ -0,0 +1,15 @@ +use std::sync::Arc; + +use get_size2::GetSize; + +/// By default, `Arc: GetSize` requires `T: 'static` to enable tracking references +/// of the `Arc` and avoid double-counting. This method opts out of that behavior and +/// removes the `'static` requirement. +/// +/// This method will just return the heap-size of the inner `T`. +pub(crate) fn untracked_arc_size(arc: &Arc) -> usize +where + T: GetSize, +{ + T::get_heap_size(&**arc) +} diff --git a/crates/ty_python_semantic/src/util/mod.rs b/crates/ty_python_semantic/src/util/mod.rs new file mode 100644 index 0000000000000..5ba24ddaac9ba --- /dev/null +++ b/crates/ty_python_semantic/src/util/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod diagnostics; +pub(crate) mod get_size; +pub(crate) mod subscript; diff --git a/crates/ty_python_semantic/src/util/subscript.rs b/crates/ty_python_semantic/src/util/subscript.rs new file mode 100644 index 0000000000000..3f05384d8e1d1 --- /dev/null +++ b/crates/ty_python_semantic/src/util/subscript.rs @@ -0,0 +1,600 @@ +//! This module provides utility functions for indexing (`PyIndex`) and slicing +//! operations (`PySlice`) on iterators, following the semantics of equivalent +//! operations in Python. + +use itertools::Either; + +use crate::Db; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct OutOfBoundsError; + +pub(crate) trait PyIndex<'db> { + type Item: 'db; + + fn py_index(self, db: &'db dyn Db, index: i32) -> Result; +} + +fn from_nonnegative_i32(index: i32) -> usize { + static_assertions::const_assert!(usize::BITS >= 32); + debug_assert!(index >= 0); + + usize::try_from(index) + .expect("Should only ever pass a positive integer to `from_nonnegative_i32`") +} + +fn from_negative_i32(index: i32) -> usize { + static_assertions::const_assert!(usize::BITS >= 32); + + index.checked_neg().map(from_nonnegative_i32).unwrap_or({ + // 'checked_neg' only fails for i32::MIN. We can not + // represent -i32::MIN as a i32, but we can represent + // it as a usize, since usize is at least 32 bits. + from_nonnegative_i32(i32::MAX) + 1 + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +enum Position { + BeforeStart, + AtIndex(usize), + AfterEnd, +} + +pub(crate) enum Nth { + FromStart(usize), + FromEnd(usize), +} + +impl Nth { + pub(crate) fn from_index(index: i32) -> Self { + if index >= 0 { + Nth::FromStart(from_nonnegative_i32(index)) + } else { + Nth::FromEnd(from_negative_i32(index) - 1) + } + } + + fn to_position(&self, len: usize) -> Position { + debug_assert!(len > 0); + + match self { + Nth::FromStart(nth) => { + if *nth < len { + Position::AtIndex(*nth) + } else { + Position::AfterEnd + } + } + Nth::FromEnd(nth_rev) => { + if *nth_rev < len { + Position::AtIndex(len - 1 - *nth_rev) + } else { + Position::BeforeStart + } + } + } + } +} + +impl<'db, T> PyIndex<'db> for &'db [T] { + type Item = &'db T; + + fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<&'db T, OutOfBoundsError> { + match Nth::from_index(index) { + Nth::FromStart(nth) => self.get(nth).ok_or(OutOfBoundsError), + Nth::FromEnd(nth_rev) => (self.len().checked_sub(nth_rev + 1)) + .map(|idx| &self[idx]) + .ok_or(OutOfBoundsError), + } + } +} + +impl<'db, I: 'db, T> PyIndex<'db> for &mut T +where + T: DoubleEndedIterator, +{ + type Item = I; + + fn py_index(self, _db: &'db dyn Db, index: i32) -> Result { + match Nth::from_index(index) { + Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError), + Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct StepSizeZeroError; + +pub(crate) trait PySlice<'db> { + type Item: 'db; + + fn py_slice( + &'db self, + db: &'db dyn Db, + start: Option, + stop: Option, + step: Option, + ) -> Result, StepSizeZeroError>; +} + +impl<'db, T: 'db> PySlice<'db> for [T] { + type Item = T; + + fn py_slice( + &'db self, + _db: &'db dyn Db, + start: Option, + stop: Option, + step_int: Option, + ) -> Result, StepSizeZeroError> { + let step_int = step_int.unwrap_or(1); + if step_int == 0 { + return Err(StepSizeZeroError); + } + + let len = self.len(); + if len == 0 { + // The iterator needs to have the same type as the step>0 case below, + // so we need to use `.skip(0)`. + #[expect(clippy::iter_skip_zero)] + return Ok(Either::Left(self.iter().skip(0).take(0).step_by(1))); + } + + let to_position = |index| Nth::from_index(index).to_position(len); + + if step_int.is_positive() { + let step = from_nonnegative_i32(step_int); + + let start = start.map(to_position).unwrap_or(Position::BeforeStart); + let stop = stop.map(to_position).unwrap_or(Position::AfterEnd); + + let (skip, take, step) = if start < stop { + let skip = match start { + Position::BeforeStart => 0, + Position::AtIndex(start_index) => start_index, + Position::AfterEnd => len, + }; + + let take = match stop { + Position::BeforeStart => 0, + Position::AtIndex(stop_index) => stop_index - skip, + Position::AfterEnd => len - skip, + }; + + (skip, take, step) + } else { + (0, 0, step) + }; + + Ok(Either::Left( + self.iter().skip(skip).take(take).step_by(step), + )) + } else { + let step = from_negative_i32(step_int); + + let start = start.map(to_position).unwrap_or(Position::AfterEnd); + let stop = stop.map(to_position).unwrap_or(Position::BeforeStart); + + let (skip, take, step) = if start <= stop { + (0, 0, step) + } else { + let skip = match start { + Position::BeforeStart => len, + Position::AtIndex(start_index) => len - 1 - start_index, + Position::AfterEnd => 0, + }; + + let take = match stop { + Position::BeforeStart => len - skip, + Position::AtIndex(stop_index) => (len - 1) - skip - stop_index, + Position::AfterEnd => 0, + }; + + (skip, take, step) + }; + + Ok(Either::Right( + self.iter().rev().skip(skip).take(take).step_by(step), + )) + } + } +} + +#[cfg(test)] +#[expect(clippy::redundant_clone)] +mod tests { + use crate::Db; + use crate::db::tests::setup_db; + use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError}; + + use super::{PyIndex, PySlice}; + use itertools::assert_equal; + + #[test] + fn py_index_empty() { + let db = setup_db(); + let iter = std::iter::empty::(); + + assert_eq!(iter.clone().py_index(&db, 0), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, -1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, i32::MIN), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, i32::MAX), Err(OutOfBoundsError)); + } + + #[test] + fn py_index_single_element() { + let db = setup_db(); + let iter = ['a'].into_iter(); + + assert_eq!(iter.clone().py_index(&db, 0), Ok('a')); + assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(&db, -1), Ok('a')); + assert_eq!(iter.clone().py_index(&db, -2), Err(OutOfBoundsError)); + } + + #[test] + fn py_index_more_elements() { + let db = setup_db(); + let iter = ['a', 'b', 'c', 'd', 'e'].into_iter(); + + assert_eq!(iter.clone().py_index(&db, 0), Ok('a')); + assert_eq!(iter.clone().py_index(&db, 1), Ok('b')); + assert_eq!(iter.clone().py_index(&db, 4), Ok('e')); + assert_eq!(iter.clone().py_index(&db, 5), Err(OutOfBoundsError)); + + assert_eq!(iter.clone().py_index(&db, -1), Ok('e')); + assert_eq!(iter.clone().py_index(&db, -2), Ok('d')); + assert_eq!(iter.clone().py_index(&db, -5), Ok('a')); + assert_eq!(iter.clone().py_index(&db, -6), Err(OutOfBoundsError)); + } + + #[test] + fn py_index_uses_full_index_range() { + let db = setup_db(); + let iter = 0..=u32::MAX; + + // u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31 + assert_eq!(iter.clone().py_index(&db, i32::MIN), Ok(2u32.pow(31))); + assert_eq!(iter.clone().py_index(&db, -2), Ok(u32::MAX - 2 + 1)); + assert_eq!(iter.clone().py_index(&db, -1), Ok(u32::MAX - 1 + 1)); + + assert_eq!(iter.clone().py_index(&db, 0), Ok(0)); + assert_eq!(iter.clone().py_index(&db, 1), Ok(1)); + assert_eq!(iter.clone().py_index(&db, i32::MAX), Ok(i32::MAX as u32)); + } + + #[track_caller] + fn assert_eq_slice( + db: &dyn Db, + input: &[char; N], + start: Option, + stop: Option, + step: Option, + expected: &[char; M], + ) { + assert_equal( + input.py_slice(db, start, stop, step).unwrap(), + expected.iter(), + ); + } + + #[test] + fn py_slice_empty_input() { + let db = setup_db(); + let input = []; + + assert_eq_slice(&db, &input, None, None, None, &[]); + assert_eq_slice(&db, &input, Some(0), None, None, &[]); + assert_eq_slice(&db, &input, None, Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]); + assert_eq_slice(&db, &input, None, None, Some(-1), &[]); + assert_eq_slice(&db, &input, None, None, Some(2), &[]); + } + + #[test] + fn py_slice_single_element_input() { + let db = setup_db(); + let input = ['a']; + + assert_eq_slice(&db, &input, None, None, None, &['a']); + + assert_eq_slice(&db, &input, Some(0), None, None, &['a']); + assert_eq_slice(&db, &input, None, Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(0), Some(2), None, &['a']); + + assert_eq_slice(&db, &input, Some(-1), None, None, &['a']); + assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-1), Some(2), None, &['a']); + assert_eq_slice(&db, &input, None, Some(-1), None, &[]); + + assert_eq_slice(&db, &input, Some(-2), None, None, &['a']); + assert_eq_slice(&db, &input, Some(-2), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(-2), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(-2), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-2), Some(2), None, &['a']); + } + + #[test] + fn py_slice_nonnegative_indices() { + let db = setup_db(); + let input = ['a', 'b', 'c', 'd', 'e']; + + assert_eq_slice(&db, &input, None, Some(0), None, &[]); + assert_eq_slice(&db, &input, None, Some(1), None, &['a']); + assert_eq_slice(&db, &input, None, Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, None, None, None, &['a', 'b', 'c', 'd', 'e']); + + assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice( + &db, + &input, + Some(0), + Some(5), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice( + &db, + &input, + Some(0), + Some(6), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']); + + assert_eq_slice(&db, &input, Some(1), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(1), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(2), None, &['b']); + assert_eq_slice(&db, &input, Some(1), Some(4), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(1), None, None, &['b', 'c', 'd', 'e']); + + assert_eq_slice(&db, &input, Some(4), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(4), Some(4), None, &[]); + assert_eq_slice(&db, &input, Some(4), Some(5), None, &['e']); + assert_eq_slice(&db, &input, Some(4), Some(6), None, &['e']); + assert_eq_slice(&db, &input, Some(4), None, None, &['e']); + + assert_eq_slice(&db, &input, Some(5), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(5), Some(5), None, &[]); + assert_eq_slice(&db, &input, Some(5), Some(6), None, &[]); + assert_eq_slice(&db, &input, Some(5), None, None, &[]); + + assert_eq_slice(&db, &input, Some(6), Some(0), None, &[]); + assert_eq_slice(&db, &input, Some(6), Some(6), None, &[]); + assert_eq_slice(&db, &input, Some(6), None, None, &[]); + } + + #[test] + fn py_slice_negative_indices() { + let db = setup_db(); + let input = ['a', 'b', 'c', 'd', 'e']; + + assert_eq_slice( + &db, + &input, + Some(-6), + None, + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-6), Some(-4), None, &['a']); + assert_eq_slice(&db, &input, Some(-6), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(-6), Some(-6), None, &[]); + assert_eq_slice(&db, &input, Some(-6), Some(-10), None, &[]); + + assert_eq_slice( + &db, + &input, + Some(-5), + None, + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-5), Some(-4), None, &['a']); + assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(-5), Some(-6), None, &[]); + assert_eq_slice(&db, &input, Some(-5), Some(-10), None, &[]); + + assert_eq_slice(&db, &input, Some(-4), None, None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(-4), Some(-1), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-4), Some(-3), None, &['b']); + assert_eq_slice(&db, &input, Some(-4), Some(-4), None, &[]); + assert_eq_slice(&db, &input, Some(-4), Some(-10), None, &[]); + + assert_eq_slice(&db, &input, Some(-1), None, None, &['e']); + assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(-10), None, &[]); + + assert_eq_slice(&db, &input, None, Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, None, Some(-4), None, &['a']); + assert_eq_slice(&db, &input, None, Some(-5), None, &[]); + assert_eq_slice(&db, &input, None, Some(-6), None, &[]); + } + + #[test] + fn py_slice_mixed_positive_negative_indices() { + let db = setup_db(); + let input = ['a', 'b', 'c', 'd', 'e']; + + assert_eq_slice(&db, &input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(1), Some(-1), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(3), Some(-1), None, &['d']); + assert_eq_slice(&db, &input, Some(4), Some(-1), None, &[]); + assert_eq_slice(&db, &input, Some(5), Some(-1), None, &[]); + + assert_eq_slice(&db, &input, Some(0), Some(-4), None, &['a']); + assert_eq_slice(&db, &input, Some(1), Some(-4), None, &[]); + assert_eq_slice(&db, &input, Some(3), Some(-4), None, &[]); + + assert_eq_slice(&db, &input, Some(0), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(-5), None, &[]); + assert_eq_slice(&db, &input, Some(3), Some(-5), None, &[]); + + assert_eq_slice(&db, &input, Some(0), Some(-6), None, &[]); + assert_eq_slice(&db, &input, Some(1), Some(-6), None, &[]); + + assert_eq_slice( + &db, + &input, + Some(-6), + Some(6), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice( + &db, + &input, + Some(-6), + Some(5), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-6), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-6), Some(0), None, &[]); + + assert_eq_slice( + &db, + &input, + Some(-5), + Some(6), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice( + &db, + &input, + Some(-5), + Some(5), + None, + &['a', 'b', 'c', 'd', 'e'], + ); + assert_eq_slice(&db, &input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-5), Some(1), None, &['a']); + assert_eq_slice(&db, &input, Some(-5), Some(0), None, &[]); + + assert_eq_slice(&db, &input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&db, &input, Some(-4), Some(4), None, &['b', 'c', 'd']); + assert_eq_slice(&db, &input, Some(-4), Some(2), None, &['b']); + assert_eq_slice(&db, &input, Some(-4), Some(1), None, &[]); + assert_eq_slice(&db, &input, Some(-4), Some(0), None, &[]); + + assert_eq_slice(&db, &input, Some(-1), Some(6), None, &['e']); + assert_eq_slice(&db, &input, Some(-1), Some(5), None, &['e']); + assert_eq_slice(&db, &input, Some(-1), Some(4), None, &[]); + assert_eq_slice(&db, &input, Some(-1), Some(1), None, &[]); + } + + #[test] + fn py_slice_step_forward() { + let db = setup_db(); + // indices: 0 1 2 3 4 5 6 + let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + + // Step size zero is invalid: + assert!(matches!( + input.py_slice(&db, None, None, Some(0)), + Err(StepSizeZeroError) + )); + assert!(matches!( + input.py_slice(&db, Some(0), Some(5), Some(0)), + Err(StepSizeZeroError) + )); + assert!(matches!( + input.py_slice(&db, Some(0), Some(0), Some(0)), + Err(StepSizeZeroError) + )); + + assert_eq_slice( + &db, + &input, + Some(0), + Some(8), + Some(2), + &['a', 'c', 'e', 'g'], + ); + assert_eq_slice( + &db, + &input, + Some(0), + Some(7), + Some(2), + &['a', 'c', 'e', 'g'], + ); + assert_eq_slice(&db, &input, Some(0), Some(6), Some(2), &['a', 'c', 'e']); + assert_eq_slice(&db, &input, Some(0), Some(5), Some(2), &['a', 'c', 'e']); + assert_eq_slice(&db, &input, Some(0), Some(4), Some(2), &['a', 'c']); + assert_eq_slice(&db, &input, Some(0), Some(3), Some(2), &['a', 'c']); + assert_eq_slice(&db, &input, Some(0), Some(2), Some(2), &['a']); + assert_eq_slice(&db, &input, Some(0), Some(1), Some(2), &['a']); + assert_eq_slice(&db, &input, Some(0), Some(0), Some(2), &[]); + assert_eq_slice(&db, &input, Some(1), Some(5), Some(2), &['b', 'd']); + + assert_eq_slice(&db, &input, Some(0), Some(7), Some(3), &['a', 'd', 'g']); + assert_eq_slice(&db, &input, Some(0), Some(6), Some(3), &['a', 'd']); + + assert_eq_slice(&db, &input, Some(0), None, Some(10), &['a']); + } + + #[test] + fn py_slice_step_backward() { + let db = setup_db(); + // indices: 0 1 2 3 4 5 6 + let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + + assert_eq_slice(&db, &input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&db, &input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&db, &input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']); + assert_eq_slice(&db, &input, Some(4), Some(0), Some(-2), &['e', 'c']); + assert_eq_slice(&db, &input, Some(3), Some(0), Some(-2), &['d', 'b']); + assert_eq_slice(&db, &input, Some(2), Some(0), Some(-2), &['c']); + assert_eq_slice(&db, &input, Some(1), Some(0), Some(-2), &['b']); + assert_eq_slice(&db, &input, Some(0), Some(0), Some(-2), &[]); + + assert_eq_slice(&db, &input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']); + assert_eq_slice(&db, &input, None, None, Some(-2), &['g', 'e', 'c', 'a']); + assert_eq_slice(&db, &input, None, Some(0), Some(-2), &['g', 'e', 'c']); + + assert_eq_slice(&db, &input, Some(5), Some(1), Some(-2), &['f', 'd']); + assert_eq_slice(&db, &input, Some(5), Some(2), Some(-2), &['f', 'd']); + assert_eq_slice(&db, &input, Some(5), Some(3), Some(-2), &['f']); + assert_eq_slice(&db, &input, Some(5), Some(4), Some(-2), &['f']); + assert_eq_slice(&db, &input, Some(5), Some(5), Some(-2), &[]); + + assert_eq_slice(&db, &input, Some(6), None, Some(-3), &['g', 'd', 'a']); + assert_eq_slice(&db, &input, Some(6), Some(0), Some(-3), &['g', 'd']); + + assert_eq_slice(&db, &input, Some(7), None, Some(-10), &['g']); + + assert_eq_slice(&db, &input, Some(-6), Some(-9), Some(-1), &['b', 'a']); + assert_eq_slice(&db, &input, Some(-6), Some(-8), Some(-1), &['b', 'a']); + assert_eq_slice(&db, &input, Some(-6), Some(-7), Some(-1), &['b']); + assert_eq_slice(&db, &input, Some(-6), Some(-6), Some(-1), &[]); + + assert_eq_slice(&db, &input, Some(-7), Some(-9), Some(-1), &['a']); + + assert_eq_slice(&db, &input, Some(-8), Some(-9), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-9), Some(-9), Some(-1), &[]); + + assert_eq_slice(&db, &input, Some(-6), Some(-2), Some(-1), &[]); + assert_eq_slice(&db, &input, Some(-9), Some(-6), Some(-1), &[]); + } +} diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs new file mode 100644 index 0000000000000..799e76dd11ab4 --- /dev/null +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -0,0 +1,261 @@ +use anyhow::{Context, anyhow}; +use ruff_db::Db; +use ruff_db::files::{File, Files, system_path_to_file}; +use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; +use ruff_db::vendored::VendoredFileSystem; +use ruff_python_ast::PythonVersion; + +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::pull_types::pull_types; +use ty_python_semantic::{ + Program, ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource, + SearchPathSettings, default_lint_registry, +}; + +use test_case::test_case; + +fn get_cargo_workspace_root() -> anyhow::Result { + Ok(SystemPathBuf::from(String::from_utf8( + std::process::Command::new("cargo") + .args(["locate-project", "--workspace", "--message-format", "plain"]) + .output()? + .stdout, + )?) + .parent() + .unwrap() + .to_owned()) +} + +/// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`]) +#[test] +fn corpus_no_panic() -> anyhow::Result<()> { + let crate_root = String::from(env!("CARGO_MANIFEST_DIR")); + run_corpus_tests(&format!("{crate_root}/resources/corpus/**/*.py")) +} + +#[test] +fn parser_no_panic() -> anyhow::Result<()> { + let workspace_root = get_cargo_workspace_root()?; + run_corpus_tests(&format!( + "{workspace_root}/crates/ruff_python_parser/resources/**/*.py" + )) +} + +#[test_case("a-e")] +#[test_case("f")] +#[test_case("g-o")] +#[test_case("p")] +#[test_case("q-z")] +#[test_case("!a-z")] +fn linter_no_panic(range: &str) -> anyhow::Result<()> { + let workspace_root = get_cargo_workspace_root()?; + run_corpus_tests(&format!( + "{workspace_root}/crates/ruff_linter/resources/test/fixtures/[{range}]*/**/*.py" + )) +} + +#[test] +fn linter_stubs_no_panic() -> anyhow::Result<()> { + let workspace_root = get_cargo_workspace_root()?; + run_corpus_tests(&format!( + "{workspace_root}/crates/ruff_linter/resources/test/fixtures/**/*.pyi" + )) +} + +#[test_case("a-e")] +#[test_case("f-k")] +#[test_case("l-p")] +#[test_case("q-z")] +#[test_case("!a-z")] +fn typeshed_no_panic(range: &str) -> anyhow::Result<()> { + let workspace_root = get_cargo_workspace_root()?; + run_corpus_tests(&format!( + "{workspace_root}/crates/ty_vendored/vendor/typeshed/stdlib/[{range}]*.pyi" + )) +} + +#[expect(clippy::print_stdout)] +fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { + let root = SystemPathBuf::from("/src"); + + let mut db = CorpusDb::new(); + db.memory_file_system() + .create_directory_all(root.as_ref())?; + + let workspace_root = get_cargo_workspace_root()?; + let workspace_root = workspace_root.to_string(); + + let corpus = glob::glob(pattern).context("Failed to compile pattern")?; + + for path in corpus { + let path = path.context("Failed to glob path")?; + let path = SystemPathBuf::from_path_buf(path).map_err(|path| { + anyhow!( + "Failed to convert path '{path}' to system path", + path = path.display() + ) + })?; + + let relative_path = path.strip_prefix(&workspace_root)?; + + let (py_expected_to_fail, pyi_expected_to_fail) = KNOWN_FAILURES + .iter() + .find_map(|(path, py_fail, pyi_fail)| { + if *path == relative_path.as_str().replace('\\', "/") { + Some((*py_fail, *pyi_fail)) + } else { + None + } + }) + .unwrap_or((false, false)); + + let source = path.as_path(); + let source_filename = source.file_name().unwrap(); + + let code = std::fs::read_to_string(source) + .with_context(|| format!("Failed to read test file: {path}"))?; + + let mut check_with_file_name = |path: &SystemPath| { + db.memory_file_system().write_file_all(path, &code).unwrap(); + File::sync_path(&mut db, path); + + // this test is only asserting that we can pull every expression type without a panic + // (and some non-expressions that clearly define a single type) + let file = system_path_to_file(&db, path).unwrap(); + + let result = std::panic::catch_unwind(|| pull_types(&db, file)); + + let expected_to_fail = if path.extension().map(|e| e == "pyi").unwrap_or(false) { + pyi_expected_to_fail + } else { + py_expected_to_fail + }; + if let Err(err) = result { + if !expected_to_fail { + println!( + "Check failed for {relative_path:?}. Consider fixing it or adding it to KNOWN_FAILURES" + ); + std::panic::resume_unwind(err); + } + } else { + assert!( + !expected_to_fail, + "Expected to panic, but did not. Consider removing this path from KNOWN_FAILURES" + ); + } + + db.memory_file_system().remove_file(path).unwrap(); + file.sync(&mut db); + }; + + if source.extension() == Some("pyi") { + println!("checking {relative_path}"); + let pyi_dest = root.join(source_filename); + check_with_file_name(&pyi_dest); + } else { + println!("checking {relative_path}"); + let py_dest = root.join(source_filename); + check_with_file_name(&py_dest); + + let pyi_dest = root.join(format!("{source_filename}i")); + println!("re-checking as stub file: {pyi_dest}"); + check_with_file_name(&pyi_dest); + } + } + + Ok(()) +} + +/// Whether or not the .py/.pyi version of this file is expected to fail +#[rustfmt::skip] +const KNOWN_FAILURES: &[(&str, bool, bool)] = &[ + // Fails with too-many-cycle-iterations due to a self-referential + // type alias, see https://github.com/astral-sh/ty/issues/256 + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_34.py", true, true), +]; + +#[salsa::db] +#[derive(Clone)] +pub struct CorpusDb { + storage: salsa::Storage, + files: Files, + rule_selection: RuleSelection, + system: TestSystem, + vendored: VendoredFileSystem, +} + +impl CorpusDb { + #[expect(clippy::new_without_default)] + pub fn new() -> Self { + let db = Self { + storage: salsa::Storage::new(None), + system: TestSystem::default(), + vendored: ty_vendored::file_system().clone(), + rule_selection: RuleSelection::from_registry(default_lint_registry()), + files: Files::default(), + }; + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::latest_ty(), + source: PythonVersionSource::default(), + }, + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![]) + .to_search_paths(db.system(), db.vendored()) + .unwrap(), + }, + ); + + db + } +} + +impl DbWithTestSystem for CorpusDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } +} + +#[salsa::db] +impl ruff_db::Db for CorpusDb { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } +} + +#[salsa::db] +impl ty_python_semantic::Db for CorpusDb { + fn is_file_open(&self, file: File) -> bool { + !file.path(self).is_vendored_path() + } + + fn rule_selection(&self, _file: File) -> &RuleSelection { + &self.rule_selection + } + + fn lint_registry(&self) -> &LintRegistry { + default_lint_registry() + } +} + +#[salsa::db] +impl salsa::Database for CorpusDb {} diff --git a/crates/red_knot_python_semantic/tests/mdtest.rs b/crates/ty_python_semantic/tests/mdtest.rs similarity index 86% rename from crates/red_knot_python_semantic/tests/mdtest.rs rename to crates/ty_python_semantic/tests/mdtest.rs index b2ea0f141c268..343ded06c7ddf 100644 --- a/crates/red_knot_python_semantic/tests/mdtest.rs +++ b/crates/ty_python_semantic/tests/mdtest.rs @@ -1,13 +1,14 @@ use camino::Utf8Path; -use dir_test::{dir_test, Fixture}; -use red_knot_test::OutputFormat; +use dir_test::{Fixture, dir_test}; +use ty_static::EnvVars; +use ty_test::OutputFormat; -/// See `crates/red_knot_test/README.md` for documentation on these tests. +/// See `crates/ty_test/README.md` for documentation on these tests. #[dir_test( dir: "$CARGO_MANIFEST_DIR/resources/mdtest", glob: "**/*.md" )] -#[allow(clippy::needless_pass_by_value)] +#[expect(clippy::needless_pass_by_value)] fn mdtest(fixture: Fixture<&str>) { let absolute_fixture_path = Utf8Path::new(fixture.path()); let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); @@ -19,13 +20,13 @@ fn mdtest(fixture: Fixture<&str>) { let test_name = test_name("mdtest", absolute_fixture_path); - let output_format = if std::env::var("MDTEST_GITHUB_ANNOTATIONS_FORMAT").is_ok() { + let output_format = if std::env::var(EnvVars::MDTEST_GITHUB_ANNOTATIONS_FORMAT).is_ok() { OutputFormat::GitHub } else { OutputFormat::Cli }; - red_knot_test::run( + ty_test::run( absolute_fixture_path, relative_fixture_path, &snapshot_path, diff --git a/crates/ty_server/Cargo.toml b/crates/ty_server/Cargo.toml new file mode 100644 index 0000000000000..86c190799eca7 --- /dev/null +++ b/crates/ty_server/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "ty_server" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +ruff_db = { workspace = true, features = ["os"] } +ruff_notebook = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } + +ty_ide = { workspace = true } +ty_project = { workspace = true } +ty_python_semantic = { workspace = true } +ty_vendored = { workspace = true } + +anyhow = { workspace = true } +crossbeam = { workspace = true } +jod-thread = { workspace = true } +lsp-server = { workspace = true } +lsp-types = { workspace = true } +rustc-hash = { workspace = true } +salsa = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +shellexpand = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["chrono"] } + +[dev-dependencies] + +[target.'cfg(target_vendor = "apple")'.dependencies] +libc = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ty_server/src/document.rs b/crates/ty_server/src/document.rs new file mode 100644 index 0000000000000..fff51d2f493a1 --- /dev/null +++ b/crates/ty_server/src/document.rs @@ -0,0 +1,117 @@ +//! Types and utilities for working with text, modifying source files, and `ty <-> LSP` type conversion. + +mod location; +mod notebook; +mod range; +mod text_document; + +pub(crate) use location::ToLink; +use lsp_types::{PositionEncodingKind, Url}; + +use crate::system::AnySystemPath; +pub use notebook::NotebookDocument; +pub(crate) use range::{FileRangeExt, PositionExt, RangeExt, TextSizeExt, ToRangeExt}; +pub(crate) use text_document::DocumentVersion; +pub use text_document::TextDocument; + +/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. +// Please maintain the order from least to greatest priority for the derived `Ord` impl. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PositionEncoding { + /// UTF 16 is the encoding supported by all LSP clients. + #[default] + UTF16, + + /// Second choice because UTF32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy) + UTF32, + + /// ty's preferred encoding + UTF8, +} + +impl From for ruff_source_file::PositionEncoding { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::UTF8 => Self::Utf8, + PositionEncoding::UTF16 => Self::Utf16, + PositionEncoding::UTF32 => Self::Utf32, + } + } +} + +/// A unique document ID, derived from a URL passed as part of an LSP request. +/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. +#[derive(Clone, Debug)] +pub(crate) enum DocumentKey { + Notebook(AnySystemPath), + NotebookCell { + cell_url: Url, + notebook_path: AnySystemPath, + }, + Text(AnySystemPath), +} + +impl DocumentKey { + /// Returns the file path associated with the key. + pub(crate) fn path(&self) -> &AnySystemPath { + match self { + DocumentKey::Notebook(path) | DocumentKey::Text(path) => path, + DocumentKey::NotebookCell { notebook_path, .. } => notebook_path, + } + } + + pub(crate) fn from_path(path: AnySystemPath) -> Self { + // For text documents, we assume it's a text document unless it's a notebook file. + match path.extension() { + Some("ipynb") => Self::Notebook(path), + _ => Self::Text(path), + } + } + + /// Returns the URL for this document key. For notebook cells, returns the cell URL. + /// For other document types, converts the path to a URL. + pub(crate) fn to_url(&self) -> Option { + match self { + DocumentKey::NotebookCell { cell_url, .. } => Some(cell_url.clone()), + DocumentKey::Notebook(path) | DocumentKey::Text(path) => path.to_url(), + } + } +} + +impl std::fmt::Display for DocumentKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotebookCell { cell_url, .. } => cell_url.fmt(f), + Self::Notebook(path) | Self::Text(path) => match path { + AnySystemPath::System(system_path) => system_path.fmt(f), + AnySystemPath::SystemVirtual(virtual_path) => virtual_path.fmt(f), + }, + } + } +} + +impl From for PositionEncodingKind { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::UTF8 => PositionEncodingKind::UTF8, + PositionEncoding::UTF16 => PositionEncodingKind::UTF16, + PositionEncoding::UTF32 => PositionEncodingKind::UTF32, + } + } +} + +impl TryFrom<&PositionEncodingKind> for PositionEncoding { + type Error = (); + + fn try_from(value: &PositionEncodingKind) -> Result { + Ok(if value == &PositionEncodingKind::UTF8 { + PositionEncoding::UTF8 + } else if value == &PositionEncodingKind::UTF16 { + PositionEncoding::UTF16 + } else if value == &PositionEncodingKind::UTF32 { + PositionEncoding::UTF32 + } else { + return Err(()); + }) + } +} diff --git a/crates/ty_server/src/document/location.rs b/crates/ty_server/src/document/location.rs new file mode 100644 index 0000000000000..eb3279000e67f --- /dev/null +++ b/crates/ty_server/src/document/location.rs @@ -0,0 +1,54 @@ +use crate::PositionEncoding; +use crate::document::{FileRangeExt, ToRangeExt}; +use crate::system::file_to_url; +use lsp_types::Location; +use ruff_db::files::FileRange; +use ruff_db::source::{line_index, source_text}; +use ruff_text_size::Ranged; +use ty_ide::{Db, NavigationTarget}; + +pub(crate) trait ToLink { + fn to_location(&self, db: &dyn ty_ide::Db, encoding: PositionEncoding) -> Option; + + fn to_link( + &self, + db: &dyn ty_ide::Db, + src: Option, + encoding: PositionEncoding, + ) -> Option; +} + +impl ToLink for NavigationTarget { + fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option { + FileRange::new(self.file(), self.focus_range()).to_location(db, encoding) + } + + fn to_link( + &self, + db: &dyn Db, + src: Option, + encoding: PositionEncoding, + ) -> Option { + let file = self.file(); + let uri = file_to_url(db, file)?; + let source = source_text(db, file); + let index = line_index(db, file); + + let target_range = self.full_range().to_lsp_range(&source, &index, encoding); + let selection_range = self.focus_range().to_lsp_range(&source, &index, encoding); + + let src = src.map(|src| { + let source = source_text(db, src.file()); + let index = line_index(db, src.file()); + + src.range().to_lsp_range(&source, &index, encoding) + }); + + Some(lsp_types::LocationLink { + target_uri: uri, + target_range, + target_selection_range: selection_range, + origin_selection_range: src, + }) + } +} diff --git a/crates/red_knot_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs similarity index 98% rename from crates/red_knot_server/src/document/notebook.rs rename to crates/ty_server/src/document/notebook.rs index b01cc5dac0c3f..2616cffd70611 100644 --- a/crates/red_knot_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -92,7 +92,7 @@ impl NotebookDocument { }; ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false) - .unwrap_or_else(|err| panic!("Server notebook document could not be converted to Ruff's notebook document format: {err}")) + .unwrap_or_else(|err| panic!("Server notebook document could not be converted to ty's notebook document format: {err}")) } pub(crate) fn update( @@ -199,7 +199,6 @@ impl NotebookDocument { } /// Get the URI for a cell by its index within the cell array. - #[expect(dead_code)] pub(crate) fn cell_uri_by_index(&self, index: CellId) -> Option<&lsp_types::Url> { self.cells.get(index).map(|cell| &cell.url) } @@ -212,7 +211,7 @@ impl NotebookDocument { } /// Returns a list of cell URIs in the order they appear in the array. - pub(crate) fn urls(&self) -> impl Iterator { + pub(crate) fn cell_urls(&self) -> impl Iterator { self.cells.iter().map(|cell| &cell.url) } @@ -248,7 +247,7 @@ mod tests { use super::NotebookDocument; enum TestCellContent { - #[allow(dead_code)] + #[expect(dead_code)] Markup(String), Code(String), } diff --git a/crates/ty_server/src/document/range.rs b/crates/ty_server/src/document/range.rs new file mode 100644 index 0000000000000..1d107e5a308e2 --- /dev/null +++ b/crates/ty_server/src/document/range.rs @@ -0,0 +1,172 @@ +use super::PositionEncoding; +use super::notebook; +use crate::system::file_to_url; + +use lsp_types as types; +use lsp_types::Location; + +use ruff_db::files::FileRange; +use ruff_db::source::{line_index, source_text}; +use ruff_notebook::NotebookIndex; +use ruff_source_file::LineIndex; +use ruff_source_file::{OneIndexed, SourceLocation}; +use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::Db; + +#[expect(dead_code)] +pub(crate) struct NotebookRange { + pub(crate) cell: notebook::CellId, + pub(crate) range: types::Range, +} + +pub(crate) trait RangeExt { + fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) + -> TextRange; +} + +pub(crate) trait PositionExt { + fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize; +} + +pub(crate) trait TextSizeExt { + fn to_position( + self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> types::Position + where + Self: Sized; +} + +impl TextSizeExt for TextSize { + fn to_position( + self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> types::Position { + let source_location = index.source_location(self, text, encoding.into()); + source_location_to_position(&source_location) + } +} + +pub(crate) trait ToRangeExt { + fn to_lsp_range( + &self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> types::Range; + + #[expect(dead_code)] + fn to_notebook_range( + &self, + text: &str, + source_index: &LineIndex, + notebook_index: &NotebookIndex, + encoding: PositionEncoding, + ) -> NotebookRange; +} + +fn u32_index_to_usize(index: u32) -> usize { + usize::try_from(index).expect("u32 fits in usize") +} + +impl PositionExt for lsp_types::Position { + fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize { + index.offset( + SourceLocation { + line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)), + character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize(self.character)), + }, + text, + encoding.into(), + ) + } +} + +impl RangeExt for lsp_types::Range { + fn to_text_range( + &self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> TextRange { + TextRange::new( + self.start.to_text_size(text, index, encoding), + self.end.to_text_size(text, index, encoding), + ) + } +} + +impl ToRangeExt for TextRange { + fn to_lsp_range( + &self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> types::Range { + types::Range { + start: self.start().to_position(text, index, encoding), + end: self.end().to_position(text, index, encoding), + } + } + + fn to_notebook_range( + &self, + text: &str, + source_index: &LineIndex, + notebook_index: &NotebookIndex, + encoding: PositionEncoding, + ) -> NotebookRange { + let start = source_index.source_location(self.start(), text, encoding.into()); + let mut end = source_index.source_location(self.end(), text, encoding.into()); + let starting_cell = notebook_index.cell(start.line); + + // weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds') + // we need to move it one character back (which should place it at the end of the last line). + // we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset. + if notebook_index.cell(end.line) != starting_cell { + end.line = end.line.saturating_sub(1); + let offset = self.end().checked_sub(1.into()).unwrap_or_default(); + end.character_offset = source_index + .source_location(offset, text, encoding.into()) + .character_offset; + } + + let start = source_location_to_position(¬ebook_index.translate_source_location(&start)); + let end = source_location_to_position(¬ebook_index.translate_source_location(&end)); + + NotebookRange { + cell: starting_cell + .map(OneIndexed::to_zero_indexed) + .unwrap_or_default(), + range: types::Range { start, end }, + } + } +} + +fn source_location_to_position(location: &SourceLocation) -> types::Position { + types::Position { + line: u32::try_from(location.line.to_zero_indexed()).expect("line usize fits in u32"), + character: u32::try_from(location.character_offset.to_zero_indexed()) + .expect("character usize fits in u32"), + } +} + +pub(crate) trait FileRangeExt { + fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option; +} + +impl FileRangeExt for FileRange { + fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option { + let file = self.file(); + let uri = file_to_url(db, file)?; + let source = source_text(db, file); + let line_index = line_index(db, file); + + let range = self.range().to_lsp_range(&source, &line_index, encoding); + Some(Location { uri, range }) + } +} diff --git a/crates/red_knot_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs similarity index 97% rename from crates/red_knot_server/src/document/text_document.rs rename to crates/ty_server/src/document/text_document.rs index 77377ca937984..e5d00ff0cf176 100644 --- a/crates/red_knot_server/src/document/text_document.rs +++ b/crates/ty_server/src/document/text_document.rs @@ -82,9 +82,11 @@ impl TextDocument { new_version: DocumentVersion, encoding: PositionEncoding, ) { - if let [lsp_types::TextDocumentContentChangeEvent { - range: None, text, .. - }] = changes.as_slice() + if let [ + lsp_types::TextDocumentContentChangeEvent { + range: None, text, .. + }, + ] = changes.as_slice() { tracing::debug!("Fast path - replacing entire document"); self.modify(|contents, version| { diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs new file mode 100644 index 0000000000000..9c7ad00e4eeb5 --- /dev/null +++ b/crates/ty_server/src/lib.rs @@ -0,0 +1,54 @@ +use crate::server::{ConnectionInitializer, Server}; +use anyhow::Context; +pub use document::{NotebookDocument, PositionEncoding, TextDocument}; +pub use session::{DocumentQuery, DocumentSnapshot, Session}; +use std::num::NonZeroUsize; + +mod document; +mod logging; +mod server; +mod session; +mod system; + +pub(crate) const SERVER_NAME: &str = "ty"; +pub(crate) const DIAGNOSTIC_NAME: &str = "ty"; + +/// A common result type used in most cases where a +/// result type is needed. +pub(crate) type Result = anyhow::Result; + +pub(crate) fn version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +pub fn run_server() -> anyhow::Result<()> { + let four = NonZeroUsize::new(4).unwrap(); + + // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. + let worker_threads = std::thread::available_parallelism() + .unwrap_or(four) + .min(four); + + let (connection, io_threads) = ConnectionInitializer::stdio(); + + let server_result = Server::new(worker_threads, connection) + .context("Failed to start server")? + .run(); + + let io_result = io_threads.join(); + + let result = match (server_result, io_result) { + (Ok(()), Ok(())) => Ok(()), + (Err(server), Err(io)) => Err(server).context(format!("IO thread error: {io}")), + (Err(server), _) => Err(server), + (_, Err(io)) => Err(io).context("IO thread error"), + }; + + if let Err(err) = result.as_ref() { + tracing::warn!("Server shut down with an error: {err}"); + } else { + tracing::info!("Server shut down"); + } + + result +} diff --git a/crates/ty_server/src/logging.rs b/crates/ty_server/src/logging.rs new file mode 100644 index 0000000000000..97870753c2410 --- /dev/null +++ b/crates/ty_server/src/logging.rs @@ -0,0 +1,113 @@ +//! The logging system for `ty server`. +//! +//! Log messages are controlled by the `logLevel` setting which defaults to `"info"`. Log messages +//! are written to `stderr` by default, which should appear in the logs for most LSP clients. A +//! `logFile` path can also be specified in the settings, and output will be directed there +//! instead. +use std::sync::Arc; + +use ruff_db::system::{SystemPath, SystemPathBuf}; +use serde::Deserialize; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::Layer; +use tracing_subscriber::fmt::time::ChronoLocal; +use tracing_subscriber::fmt::writer::BoxMakeWriter; +use tracing_subscriber::layer::SubscriberExt; + +pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&SystemPath>) { + let log_file = log_file + .map(|path| { + // this expands `logFile` so that tildes and environment variables + // are replaced with their values, if possible. + if let Some(expanded) = shellexpand::full(&path.to_string()) + .ok() + .map(|path| SystemPathBuf::from(&*path)) + { + expanded + } else { + path.to_path_buf() + } + }) + .and_then(|path| { + std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path.as_std_path()) + .map_err(|err| { + #[expect(clippy::print_stderr)] + { + eprintln!("Failed to open file at {path} for logging: {err}"); + } + }) + .ok() + }); + + let logger = match log_file { + Some(file) => BoxMakeWriter::new(Arc::new(file)), + None => BoxMakeWriter::new(std::io::stderr), + }; + let is_trace_level = log_level == LogLevel::Trace; + let subscriber = tracing_subscriber::Registry::default().with( + tracing_subscriber::fmt::layer() + .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S.%f".to_string())) + .with_thread_names(is_trace_level) + .with_target(is_trace_level) + .with_ansi(false) + .with_writer(logger) + .with_filter(LogLevelFilter { filter: log_level }), + ); + + tracing::subscriber::set_global_default(subscriber) + .expect("should be able to set global default subscriber"); +} + +/// The log level for the server as provided by the client during initialization. +/// +/// The default log level is `info`. +#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] +pub(crate) enum LogLevel { + Error, + Warn, + #[default] + Info, + Debug, + Trace, +} + +impl LogLevel { + fn trace_level(self) -> tracing::Level { + match self { + Self::Error => tracing::Level::ERROR, + Self::Warn => tracing::Level::WARN, + Self::Info => tracing::Level::INFO, + Self::Debug => tracing::Level::DEBUG, + Self::Trace => tracing::Level::TRACE, + } + } +} + +/// Filters out traces which have a log level lower than the `logLevel` set by the client. +struct LogLevelFilter { + filter: LogLevel, +} + +impl tracing_subscriber::layer::Filter for LogLevelFilter { + fn enabled( + &self, + meta: &tracing::Metadata<'_>, + _: &tracing_subscriber::layer::Context<'_, S>, + ) -> bool { + let filter = if meta.target().starts_with("ty") { + self.filter.trace_level() + } else { + tracing::Level::WARN + }; + + meta.level() <= &filter + } + + fn max_level_hint(&self) -> Option { + Some(LevelFilter::from_level(self.filter.trace_level())) + } +} diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs new file mode 100644 index 0000000000000..779564a88bee6 --- /dev/null +++ b/crates/ty_server/src/server.rs @@ -0,0 +1,284 @@ +//! Scheduling, I/O, and API endpoints. + +use self::schedule::spawn_main_loop; +use crate::PositionEncoding; +use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session}; +use lsp_server::Connection; +use lsp_types::{ + ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, + InlayHintOptions, InlayHintServerCapabilities, MessageType, SemanticTokensLegend, + SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities, + SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions, +}; +use std::num::NonZeroUsize; +use std::panic::PanicHookInfo; +use std::sync::Arc; + +mod api; +mod connection; +mod main_loop; +mod schedule; + +use crate::session::client::Client; +pub(crate) use api::Error; +pub(crate) use connection::{ConnectionInitializer, ConnectionSender}; +pub(crate) use main_loop::{Action, Event, MainLoopReceiver, MainLoopSender}; + +pub(crate) type Result = std::result::Result; + +pub(crate) struct Server { + connection: Connection, + client_capabilities: ClientCapabilities, + worker_threads: NonZeroUsize, + main_loop_receiver: MainLoopReceiver, + main_loop_sender: MainLoopSender, + session: Session, +} + +impl Server { + pub(crate) fn new( + worker_threads: NonZeroUsize, + connection: ConnectionInitializer, + ) -> crate::Result { + let (id, init_params) = connection.initialize_start()?; + + let AllOptions { + global: global_options, + workspace: mut workspace_options, + } = AllOptions::from_value( + init_params + .initialization_options + .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())), + ); + + let client_capabilities = init_params.capabilities; + let position_encoding = Self::find_best_position_encoding(&client_capabilities); + let server_capabilities = + Self::server_capabilities(position_encoding, global_options.diagnostic_mode()); + + let connection = connection.initialize_finish( + id, + &server_capabilities, + crate::SERVER_NAME, + crate::version(), + )?; + + // The number 32 was chosen arbitrarily. The main goal was to have enough capacity to queue + // some responses before blocking. + let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32); + let client = Client::new(main_loop_sender.clone(), connection.sender.clone()); + + crate::logging::init_logging( + global_options.tracing.log_level.unwrap_or_default(), + global_options.tracing.log_file.as_deref(), + ); + + let mut workspace_for_url = |url: Url| { + let Some(workspace_settings) = workspace_options.as_mut() else { + return (url, ClientOptions::default()); + }; + let settings = workspace_settings.remove(&url).unwrap_or_else(|| { + tracing::warn!( + "No workspace options found for {}, using default options", + url + ); + ClientOptions::default() + }); + (url, settings) + }; + + let workspaces = init_params + .workspace_folders + .filter(|folders| !folders.is_empty()) + .map(|folders| { + folders + .into_iter() + .map(|folder| workspace_for_url(folder.uri)) + .collect() + }) + .or_else(|| { + let current_dir = std::env::current_dir().ok()?; + tracing::warn!( + "No workspace(s) were provided during initialization. \ + Using the current working directory as a default workspace: {}", + current_dir.display() + ); + let uri = Url::from_file_path(current_dir).ok()?; + Some(vec![workspace_for_url(uri)]) + }) + .ok_or_else(|| { + anyhow::anyhow!( + "Failed to get the current working directory while creating a \ + default workspace." + ) + })?; + + let workspaces = if workspaces.len() > 1 { + let first_workspace = workspaces.into_iter().next().unwrap(); + tracing::warn!( + "Multiple workspaces are not yet supported, using the first workspace: {}", + &first_workspace.0 + ); + client.show_warning_message(format_args!( + "Multiple workspaces are not yet supported, using the first workspace: {}", + &first_workspace.0, + )); + vec![first_workspace] + } else { + workspaces + }; + + Ok(Self { + connection, + worker_threads, + main_loop_receiver, + main_loop_sender, + session: Session::new( + &client_capabilities, + position_encoding, + global_options, + workspaces, + )?, + client_capabilities, + }) + } + + pub(crate) fn run(mut self) -> crate::Result<()> { + let client = Client::new( + self.main_loop_sender.clone(), + self.connection.sender.clone(), + ); + + let _panic_hook = ServerPanicHookHandler::new(client); + + spawn_main_loop(move || self.main_loop())?.join() + } + + fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding { + client_capabilities + .general + .as_ref() + .and_then(|general_capabilities| general_capabilities.position_encodings.as_ref()) + .and_then(|encodings| { + encodings + .iter() + .filter_map(|encoding| PositionEncoding::try_from(encoding).ok()) + .max() // this selects the highest priority position encoding + }) + .unwrap_or_default() + } + + fn server_capabilities( + position_encoding: PositionEncoding, + diagnostic_mode: DiagnosticMode, + ) -> ServerCapabilities { + ServerCapabilities { + position_encoding: Some(position_encoding.into()), + diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { + identifier: Some(crate::DIAGNOSTIC_NAME.into()), + inter_file_dependencies: true, + // TODO: Dynamically register for workspace diagnostics. + workspace_diagnostics: diagnostic_mode.is_workspace(), + ..Default::default() + })), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + ..Default::default() + }, + )), + type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), + hover_provider: Some(HoverProviderCapability::Simple(true)), + signature_help_provider: Some(SignatureHelpOptions { + trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), + retrigger_characters: Some(vec![")".to_string()]), + work_done_progress_options: lsp_types::WorkDoneProgressOptions::default(), + }), + inlay_hint_provider: Some(lsp_types::OneOf::Right( + InlayHintServerCapabilities::Options(InlayHintOptions::default()), + )), + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { + work_done_progress_options: WorkDoneProgressOptions::default(), + legend: SemanticTokensLegend { + token_types: ty_ide::SemanticTokenType::all() + .iter() + .map(|token_type| token_type.as_lsp_concept().into()) + .collect(), + token_modifiers: ty_ide::SemanticTokenModifier::all_names() + .iter() + .map(|&s| s.into()) + .collect(), + }, + range: Some(true), + full: Some(lsp_types::SemanticTokensFullOptions::Bool(true)), + }), + ), + completion_provider: Some(lsp_types::CompletionOptions { + trigger_characters: Some(vec!['.'.to_string()]), + ..Default::default() + }), + ..Default::default() + } + } +} + +type PanicHook = Box) + 'static + Sync + Send>; + +struct ServerPanicHookHandler { + hook: Option, + // Hold on to the strong reference for as long as the panic hook is set. + _client: Arc, +} + +impl ServerPanicHookHandler { + fn new(client: Client) -> Self { + let hook = std::panic::take_hook(); + let client = Arc::new(client); + + // Use a weak reference to the client because it must be dropped when exiting or the + // io-threads join hangs forever (because client has a reference to the connection sender). + let hook_client = Arc::downgrade(&client); + + // When we panic, try to notify the client. + std::panic::set_hook(Box::new(move |panic_info| { + use std::io::Write; + + let backtrace = std::backtrace::Backtrace::force_capture(); + tracing::error!("{panic_info}\n{backtrace}"); + + // we also need to print to stderr directly for when using `$logTrace` because + // the message won't be sent to the client. + // But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken. + let mut stderr = std::io::stderr().lock(); + writeln!(stderr, "{panic_info}\n{backtrace}").ok(); + + if let Some(client) = hook_client.upgrade() { + client.show_message( + "The ty language server exited with a panic. See the logs for more details.", + MessageType::ERROR, + ); + } + })); + + Self { + hook: Some(hook), + _client: client, + } + } +} + +impl Drop for ServerPanicHookHandler { + fn drop(&mut self) { + if std::thread::panicking() { + // Calling `std::panic::set_hook` while panicking results in a panic. + return; + } + + if let Some(hook) = self.hook.take() { + std::panic::set_hook(hook); + } + } +} diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs new file mode 100644 index 0000000000000..4b2cb86d1c4e0 --- /dev/null +++ b/crates/ty_server/src/server/api.rs @@ -0,0 +1,477 @@ +use crate::server::schedule::Task; +use crate::session::Session; +use crate::system::AnySystemPath; +use anyhow::anyhow; +use lsp_server as server; +use lsp_server::RequestId; +use lsp_types::notification::Notification; +use lsp_types::request::Request; +use std::panic::{AssertUnwindSafe, UnwindSafe}; + +mod diagnostics; +mod notifications; +mod requests; +mod semantic_tokens; +mod traits; + +use self::traits::{NotificationHandler, RequestHandler}; +use super::{Result, schedule::BackgroundSchedule}; +use crate::session::client::Client; +use ruff_db::panic::PanicError; + +/// Processes a request from the client to the server. +/// +/// The LSP specification requires that each request has exactly one response. Therefore, +/// it's crucial that all paths in this method call [`Client::respond`] exactly once. +/// The only exception to this is requests that were cancelled by the client. In this case, +/// the response was already sent by the [`notification::CancelNotificationHandler`]. +pub(super) fn request(req: server::Request) -> Task { + let id = req.id.clone(); + + match req.method.as_str() { + requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::< + requests::DocumentDiagnosticRequestHandler, + >( + req, BackgroundSchedule::Worker + ), + requests::WorkspaceDiagnosticRequestHandler::METHOD => background_request_task::< + requests::WorkspaceDiagnosticRequestHandler, + >( + req, BackgroundSchedule::Worker + ), + requests::GotoTypeDefinitionRequestHandler::METHOD => background_document_request_task::< + requests::GotoTypeDefinitionRequestHandler, + >( + req, BackgroundSchedule::Worker + ), + requests::HoverRequestHandler::METHOD => background_document_request_task::< + requests::HoverRequestHandler, + >(req, BackgroundSchedule::Worker), + requests::InlayHintRequestHandler::METHOD => background_document_request_task::< + requests::InlayHintRequestHandler, + >(req, BackgroundSchedule::Worker), + requests::SemanticTokensRequestHandler::METHOD => background_document_request_task::< + requests::SemanticTokensRequestHandler, + >(req, BackgroundSchedule::Worker), + requests::SemanticTokensRangeRequestHandler::METHOD => background_document_request_task::< + requests::SemanticTokensRangeRequestHandler, + >( + req, BackgroundSchedule::Worker + ), + requests::SignatureHelpRequestHandler::METHOD => background_document_request_task::< + requests::SignatureHelpRequestHandler, + >(req, BackgroundSchedule::Worker), + requests::CompletionRequestHandler::METHOD => background_document_request_task::< + requests::CompletionRequestHandler, + >( + req, BackgroundSchedule::LatencySensitive + ), + lsp_types::request::Shutdown::METHOD => sync_request_task::(req), + + method => { + tracing::warn!("Received request {method} which does not have a handler"); + let result: Result<()> = Err(Error::new( + anyhow!("Unknown request: {method}"), + server::ErrorCode::MethodNotFound, + )); + return Task::immediate(id, result); + } + } + .unwrap_or_else(|err| { + tracing::error!("Encountered error when routing request with ID {id}: {err}"); + + Task::sync(move |_session, client| { + client.show_error_message( + "ty failed to handle a request from the editor. Check the logs for more details.", + ); + respond_silent_error( + id, + client, + lsp_server::ResponseError { + code: err.code as i32, + message: err.to_string(), + data: None, + }, + ); + }) + }) +} + +pub(super) fn notification(notif: server::Notification) -> Task { + match notif.method.as_str() { + notifications::DidCloseTextDocumentHandler::METHOD => { + sync_notification_task::(notif) + } + notifications::DidOpenTextDocumentHandler::METHOD => { + sync_notification_task::(notif) + } + notifications::DidChangeTextDocumentHandler::METHOD => { + sync_notification_task::(notif) + } + notifications::DidOpenNotebookHandler::METHOD => { + sync_notification_task::(notif) + } + notifications::DidCloseNotebookHandler::METHOD => { + sync_notification_task::(notif) + } + notifications::DidChangeWatchedFiles::METHOD => { + sync_notification_task::(notif) + } + lsp_types::notification::Cancel::METHOD => { + sync_notification_task::(notif) + } + lsp_types::notification::SetTrace::METHOD => { + tracing::trace!("Ignoring `setTrace` notification"); + return Task::nothing(); + } + + method => { + tracing::warn!("Received notification {method} which does not have a handler."); + return Task::nothing(); + } + } + .unwrap_or_else(|err| { + tracing::error!("Encountered error when routing notification: {err}"); + Task::sync(|_session, client| { + client.show_error_message( + "ty failed to handle a notification from the editor. Check the logs for more details." + ); + }) + }) +} + +fn sync_request_task(req: server::Request) -> Result +where + <::RequestType as Request>::Params: UnwindSafe, +{ + let (id, params) = cast_request::(req)?; + Ok(Task::sync(move |session, client: &Client| { + let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); + let result = R::run(session, client, params); + respond::(&id, result, client); + })) +} + +fn background_request_task( + req: server::Request, + schedule: BackgroundSchedule, +) -> Result +where + <::RequestType as Request>::Params: UnwindSafe, +{ + let retry = R::RETRY_ON_CANCELLATION.then(|| req.clone()); + let (id, params) = cast_request::(req)?; + + Ok(Task::background(schedule, move |session: &Session| { + let cancellation_token = session + .request_queue() + .incoming() + .cancellation_token(&id) + .expect("request should have been tested for cancellation before scheduling"); + + // SAFETY: The `snapshot` is safe to move across the unwind boundary because it is not used + // after unwinding. + let snapshot = AssertUnwindSafe(session.take_session_snapshot()); + + Box::new(move |client| { + let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); + + // Test again if the request was cancelled since it was scheduled on the background task + // and, if so, return early + if cancellation_token.is_cancelled() { + tracing::trace!( + "Ignoring request id={id} method={} because it was cancelled", + R::METHOD + ); + + // We don't need to send a response here because the `cancel` notification + // handler already responded with a message. + return; + } + + let result = ruff_db::panic::catch_unwind(|| R::run(snapshot, client, params)); + + if let Some(response) = request_result_to_response::(&id, client, result, retry) { + respond::(&id, response, client); + } + }) + })) +} + +fn background_document_request_task( + req: server::Request, + schedule: BackgroundSchedule, +) -> Result +where + <::RequestType as Request>::Params: UnwindSafe, +{ + let retry = R::RETRY_ON_CANCELLATION.then(|| req.clone()); + let (id, params) = cast_request::(req)?; + + Ok(Task::background(schedule, move |session: &Session| { + let cancellation_token = session + .request_queue() + .incoming() + .cancellation_token(&id) + .expect("request should have been tested for cancellation before scheduling"); + + let url = R::document_url(¶ms).into_owned(); + + let Ok(path) = AnySystemPath::try_from_url(&url) else { + tracing::warn!("Ignoring request for invalid `{url}`"); + return Box::new(|_| {}); + }; + + let db = match &path { + AnySystemPath::System(path) => match session.project_db_for_path(path) { + Some(db) => db.clone(), + None => session.default_project_db().clone(), + }, + AnySystemPath::SystemVirtual(_) => session.default_project_db().clone(), + }; + + let Some(snapshot) = session.take_document_snapshot(url) else { + tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist"); + return Box::new(|_| {}); + }; + + Box::new(move |client| { + let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); + + // Test again if the request was cancelled since it was scheduled on the background task + // and, if so, return early + if cancellation_token.is_cancelled() { + tracing::trace!( + "Ignoring request id={id} method={} because it was cancelled", + R::METHOD + ); + + // We don't need to send a response here because the `cancel` notification + // handler already responded with a message. + return; + } + + let result = ruff_db::panic::catch_unwind(|| { + R::run_with_snapshot(&db, snapshot, client, params) + }); + + if let Some(response) = request_result_to_response::(&id, client, result, retry) { + respond::(&id, response, client); + } + }) + })) +} + +fn request_result_to_response( + id: &RequestId, + client: &Client, + result: std::result::Result< + Result<<::RequestType as Request>::Result>, + PanicError, + >, + request: Option, +) -> Option::RequestType as Request>::Result>> +where + R: traits::RetriableRequestHandler, +{ + match result { + Ok(response) => Some(response), + Err(error) => { + // Check if the request was canceled due to some modifications to the salsa database. + if error.payload.downcast_ref::().is_some() { + // If the query supports retry, re-queue the request. + // The query is still likely to succeed if the user modified any other document. + if let Some(request) = request { + tracing::trace!( + "request id={} method={} was cancelled by salsa, re-queueing for retry", + request.id, + request.method + ); + client.retry(request); + } else { + tracing::trace!( + "request id={} was cancelled by salsa, sending content modified", + id + ); + respond_silent_error(id.clone(), client, R::salsa_cancellation_error()); + } + None + } else { + Some(Err(Error { + code: lsp_server::ErrorCode::InternalError, + error: anyhow!("request handler {error}"), + })) + } + } + } +} + +fn sync_notification_task( + notif: server::Notification, +) -> Result { + let (id, params) = cast_notification::(notif)?; + Ok(Task::sync(move |session, client| { + let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); + if let Err(err) = N::run(session, client, params) { + tracing::error!("An error occurred while running {id}: {err}"); + client.show_error_message("ty encountered a problem. Check the logs for more details."); + } + })) +} + +#[expect(dead_code)] +fn background_notification_thread( + req: server::Notification, + schedule: BackgroundSchedule, +) -> Result +where + N: traits::BackgroundDocumentNotificationHandler, + <::NotificationType as Notification>::Params: UnwindSafe, +{ + let (id, params) = cast_notification::(req)?; + Ok(Task::background(schedule, move |session: &Session| { + let url = N::document_url(¶ms); + let Some(snapshot) = session.take_document_snapshot((*url).clone()) else { + tracing::debug!( + "Ignoring notification because snapshot for url `{url}` doesn't exist." + ); + return Box::new(|_| {}); + }; + Box::new(move |client| { + let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); + + let result = match ruff_db::panic::catch_unwind(|| { + N::run_with_snapshot(snapshot, client, params) + }) { + Ok(result) => result, + Err(panic) => { + tracing::error!("An error occurred while running {id}: {panic}"); + client.show_error_message( + "ty encountered a panic. Check the logs for more details.", + ); + return; + } + }; + + if let Err(err) = result { + tracing::error!("An error occurred while running {id}: {err}"); + client.show_error_message( + "ty encountered a problem. Check the logs for more details.", + ); + } + }) + })) +} + +/// Tries to cast a serialized request from the server into +/// a parameter type for a specific request handler. +/// It is *highly* recommended to not override this function in your +/// implementation. +fn cast_request( + request: server::Request, +) -> Result<( + RequestId, + <::RequestType as Request>::Params, +)> +where + Req: RequestHandler, + <::RequestType as Request>::Params: UnwindSafe, +{ + request + .extract(Req::METHOD) + .map_err(|err| match err { + json_err @ server::ExtractError::JsonError { .. } => { + anyhow::anyhow!("JSON parsing failure:\n{json_err}") + } + server::ExtractError::MethodMismatch(_) => { + unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \ + than the one whose method name was matched against earlier.") + } + }) + .with_failure_code(server::ErrorCode::InternalError) +} + +/// Sends back a response to the server, but only if the request wasn't cancelled. +fn respond( + id: &RequestId, + result: Result<<::RequestType as Request>::Result>, + client: &Client, +) where + Req: RequestHandler, +{ + if let Err(err) = &result { + tracing::error!("An error occurred with request ID {id}: {err}"); + client.show_error_message("ty encountered a problem. Check the logs for more details."); + } + client.respond(id, result); +} + +/// Sends back an error response to the server using a [`Client`] without showing a warning +/// to the user. +fn respond_silent_error(id: RequestId, client: &Client, error: lsp_server::ResponseError) { + client.respond_err(id, error); +} + +/// Tries to cast a serialized request from the server into +/// a parameter type for a specific request handler. +fn cast_notification( + notification: server::Notification, +) -> Result<( + &'static str, + <::NotificationType as Notification>::Params, +)> +where + N: NotificationHandler, +{ + Ok(( + N::METHOD, + notification + .extract(N::METHOD) + .map_err(|err| match err { + json_err @ server::ExtractError::JsonError { .. } => { + anyhow::anyhow!("JSON parsing failure:\n{json_err}") + } + server::ExtractError::MethodMismatch(_) => { + unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \ + than the one whose method name was matched against earlier.") + } + }) + .with_failure_code(server::ErrorCode::InternalError)?, + )) +} + +pub(crate) struct Error { + pub(crate) code: server::ErrorCode, + pub(crate) error: anyhow::Error, +} + +/// A trait to convert result types into the server result type, [`super::Result`]. +trait LSPResult { + fn with_failure_code(self, code: server::ErrorCode) -> super::Result; +} + +impl> LSPResult for core::result::Result { + fn with_failure_code(self, code: server::ErrorCode) -> super::Result { + self.map_err(|err| Error::new(err.into(), code)) + } +} + +impl Error { + pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { + Self { code, error: err } + } +} + +// Right now, we treat the error code as invisible data that won't +// be printed. +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs new file mode 100644 index 0000000000000..bc4020a391856 --- /dev/null +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -0,0 +1,286 @@ +use lsp_types::notification::PublishDiagnostics; +use lsp_types::{ + CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, + NumberOrString, PublishDiagnosticsParams, Range, Url, +}; +use rustc_hash::FxHashMap; + +use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic}; +use ruff_db::files::FileRange; +use ruff_db::source::{line_index, source_text}; +use ty_project::{Db, ProjectDatabase}; + +use super::LSPResult; +use crate::document::{DocumentKey, FileRangeExt, ToRangeExt}; +use crate::server::Result; +use crate::session::client::Client; +use crate::{DocumentSnapshot, PositionEncoding, Session}; + +/// Represents the diagnostics for a text document or a notebook document. +pub(super) enum Diagnostics { + TextDocument(Vec), + + /// A map of cell URLs to the diagnostics for that cell. + NotebookDocument(FxHashMap>), +} + +impl Diagnostics { + /// Returns the diagnostics for a text document. + /// + /// # Panics + /// + /// Panics if the diagnostics are for a notebook document. + pub(super) fn expect_text_document(self) -> Vec { + match self { + Diagnostics::TextDocument(diagnostics) => diagnostics, + Diagnostics::NotebookDocument(_) => { + panic!("Expected a text document diagnostics, but got notebook diagnostics") + } + } + } +} + +/// Clears the diagnostics for the document identified by `key`. +/// +/// This is done by notifying the client with an empty list of diagnostics for the document. +/// For notebook cells, this clears diagnostics for the specific cell. +/// For other document types, this clears diagnostics for the main document. +pub(super) fn clear_diagnostics(key: &DocumentKey, client: &Client) { + let Some(uri) = key.to_url() else { + // If we can't convert to URL, we can't clear diagnostics + return; + }; + + client.send_notification::(PublishDiagnosticsParams { + uri, + diagnostics: vec![], + version: None, + }); +} + +/// Publishes the diagnostics for the given document snapshot using the [publish diagnostics +/// notification]. +/// +/// This function is a no-op if the client supports pull diagnostics. +/// +/// [publish diagnostics notification]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics +pub(super) fn publish_diagnostics( + session: &Session, + key: &DocumentKey, + client: &Client, +) -> Result<()> { + if session.client_capabilities().pull_diagnostics { + return Ok(()); + } + + let Some(url) = key.to_url() else { + return Ok(()); + }; + + let path = key.path(); + + let snapshot = session + .take_document_snapshot(url.clone()) + .ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}")) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + let db = session.project_db_or_default(path); + + let Some(diagnostics) = compute_diagnostics(db, &snapshot) else { + return Ok(()); + }; + + // Sends a notification to the client with the diagnostics for the document. + let publish_diagnostics_notification = |uri: Url, diagnostics: Vec| { + client.send_notification::(PublishDiagnosticsParams { + uri, + diagnostics, + version: Some(snapshot.query().version()), + }); + }; + + match diagnostics { + Diagnostics::TextDocument(diagnostics) => { + publish_diagnostics_notification(url, diagnostics); + } + Diagnostics::NotebookDocument(cell_diagnostics) => { + for (cell_url, diagnostics) in cell_diagnostics { + publish_diagnostics_notification(cell_url, diagnostics); + } + } + } + + Ok(()) +} + +pub(super) fn compute_diagnostics( + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, +) -> Option { + let Some(file) = snapshot.file(db) else { + tracing::info!( + "No file found for snapshot for `{}`", + snapshot.query().file_url() + ); + return None; + }; + + let diagnostics = db.check_file(file); + + if let Some(notebook) = snapshot.query().as_notebook() { + let mut cell_diagnostics: FxHashMap> = FxHashMap::default(); + + // Populates all relevant URLs with an empty diagnostic list. This ensures that documents + // without diagnostics still get updated. + for cell_url in notebook.cell_urls() { + cell_diagnostics.entry(cell_url.clone()).or_default(); + } + + for (cell_index, diagnostic) in diagnostics.iter().map(|diagnostic| { + ( + // TODO: Use the cell index instead using `SourceKind` + usize::default(), + to_lsp_diagnostic(db, diagnostic, snapshot.encoding()), + ) + }) { + let Some(cell_uri) = notebook.cell_uri_by_index(cell_index) else { + tracing::warn!("Unable to find notebook cell at index {cell_index}"); + continue; + }; + cell_diagnostics + .entry(cell_uri.clone()) + .or_default() + .push(diagnostic); + } + + Some(Diagnostics::NotebookDocument(cell_diagnostics)) + } else { + Some(Diagnostics::TextDocument( + diagnostics + .iter() + .map(|diagnostic| to_lsp_diagnostic(db, diagnostic, snapshot.encoding())) + .collect(), + )) + } +} + +/// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP +/// [`Diagnostic`]. +pub(super) fn to_lsp_diagnostic( + db: &dyn Db, + diagnostic: &ruff_db::diagnostic::Diagnostic, + encoding: PositionEncoding, +) -> Diagnostic { + let range = if let Some(span) = diagnostic.primary_span() { + let file = span.expect_ty_file(); + let index = line_index(db, file); + let source = source_text(db, file); + + span.range() + .map(|range| range.to_lsp_range(&source, &index, encoding)) + .unwrap_or_default() + } else { + Range::default() + }; + + let severity = match diagnostic.severity() { + Severity::Info => DiagnosticSeverity::INFORMATION, + Severity::Warning => DiagnosticSeverity::WARNING, + Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR, + }; + + let tags = diagnostic + .primary_tags() + .map(|tags| { + tags.iter() + .map(|tag| match tag { + ruff_db::diagnostic::DiagnosticTag::Unnecessary => DiagnosticTag::UNNECESSARY, + ruff_db::diagnostic::DiagnosticTag::Deprecated => DiagnosticTag::DEPRECATED, + }) + .collect::>() + }) + .filter(|mapped_tags| !mapped_tags.is_empty()); + + let code_description = diagnostic + .id() + .is_lint() + .then(|| { + Some(CodeDescription { + href: Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id())).ok()?, + }) + }) + .flatten(); + + let mut related_information = Vec::new(); + + related_information.extend( + diagnostic + .secondary_annotations() + .filter_map(|annotation| annotation_to_related_information(db, annotation, encoding)), + ); + + for sub_diagnostic in diagnostic.sub_diagnostics() { + related_information.extend(sub_diagnostic_to_related_information( + db, + sub_diagnostic, + encoding, + )); + + related_information.extend( + sub_diagnostic + .annotations() + .iter() + .filter_map(|annotation| { + annotation_to_related_information(db, annotation, encoding) + }), + ); + } + + Diagnostic { + range, + severity: Some(severity), + tags, + code: Some(NumberOrString::String(diagnostic.id().to_string())), + code_description, + source: Some("ty".into()), + message: diagnostic.concise_message().to_string(), + related_information: Some(related_information), + data: None, + } +} + +/// Converts an [`Annotation`] to a [`DiagnosticRelatedInformation`]. +fn annotation_to_related_information( + db: &dyn Db, + annotation: &Annotation, + encoding: PositionEncoding, +) -> Option { + let span = annotation.get_span(); + + let annotation_message = annotation.get_message()?; + let range = FileRange::try_from(span).ok()?; + let location = range.to_location(db, encoding)?; + + Some(DiagnosticRelatedInformation { + location, + message: annotation_message.to_string(), + }) +} + +/// Converts a [`SubDiagnostic`] to a [`DiagnosticRelatedInformation`]. +fn sub_diagnostic_to_related_information( + db: &dyn Db, + diagnostic: &SubDiagnostic, + encoding: PositionEncoding, +) -> Option { + let primary_annotation = diagnostic.primary_annotation()?; + + let span = primary_annotation.get_span(); + let range = FileRange::try_from(span).ok()?; + let location = range.to_location(db, encoding)?; + + Some(DiagnosticRelatedInformation { + location, + message: diagnostic.concise_message().to_string(), + }) +} diff --git a/crates/ty_server/src/server/api/notifications.rs b/crates/ty_server/src/server/api/notifications.rs new file mode 100644 index 0000000000000..baeb85e8a0e2d --- /dev/null +++ b/crates/ty_server/src/server/api/notifications.rs @@ -0,0 +1,15 @@ +mod cancel; +mod did_change; +mod did_change_watched_files; +mod did_close; +mod did_close_notebook; +mod did_open; +mod did_open_notebook; + +pub(super) use cancel::CancelNotificationHandler; +pub(super) use did_change::DidChangeTextDocumentHandler; +pub(super) use did_change_watched_files::DidChangeWatchedFiles; +pub(super) use did_close::DidCloseTextDocumentHandler; +pub(super) use did_close_notebook::DidCloseNotebookHandler; +pub(super) use did_open::DidOpenTextDocumentHandler; +pub(super) use did_open_notebook::DidOpenNotebookHandler; diff --git a/crates/ty_server/src/server/api/notifications/cancel.rs b/crates/ty_server/src/server/api/notifications/cancel.rs new file mode 100644 index 0000000000000..c8f7199e75807 --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/cancel.rs @@ -0,0 +1,27 @@ +use lsp_server::RequestId; +use lsp_types::CancelParams; +use lsp_types::notification::Cancel; + +use crate::server::Result; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; + +pub(crate) struct CancelNotificationHandler; + +impl NotificationHandler for CancelNotificationHandler { + type NotificationType = Cancel; +} + +impl SyncNotificationHandler for CancelNotificationHandler { + fn run(session: &mut Session, client: &Client, params: CancelParams) -> Result<()> { + let id: RequestId = match params.id { + lsp_types::NumberOrString::Number(id) => id.into(), + lsp_types::NumberOrString::String(id) => id.into(), + }; + + client.cancel(session, id); + + Ok(()) + } +} diff --git a/crates/ty_server/src/server/api/notifications/did_change.rs b/crates/ty_server/src/server/api/notifications/did_change.rs new file mode 100644 index 0000000000000..092acc7e87967 --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_change.rs @@ -0,0 +1,59 @@ +use lsp_server::ErrorCode; +use lsp_types::notification::DidChangeTextDocument; +use lsp_types::{DidChangeTextDocumentParams, VersionedTextDocumentIdentifier}; + +use crate::server::Result; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::publish_diagnostics; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; +use crate::system::AnySystemPath; +use ty_project::watch::ChangeEvent; + +pub(crate) struct DidChangeTextDocumentHandler; + +impl NotificationHandler for DidChangeTextDocumentHandler { + type NotificationType = DidChangeTextDocument; +} + +impl SyncNotificationHandler for DidChangeTextDocumentHandler { + fn run( + session: &mut Session, + client: &Client, + params: DidChangeTextDocumentParams, + ) -> Result<()> { + let DidChangeTextDocumentParams { + text_document: VersionedTextDocumentIdentifier { uri, version }, + content_changes, + } = params; + + let Ok(key) = session.key_from_url(uri.clone()) else { + tracing::debug!("Failed to create document key from URI: {}", uri); + return Ok(()); + }; + + session + .update_text_document(&key, content_changes, version) + .with_failure_code(ErrorCode::InternalError)?; + + match key.path() { + AnySystemPath::System(path) => { + let db = match session.project_db_for_path_mut(path) { + Some(db) => db, + None => session.default_project_db_mut(), + }; + db.apply_changes(vec![ChangeEvent::file_content_changed(path.clone())], None); + } + AnySystemPath::SystemVirtual(virtual_path) => { + let db = session.default_project_db_mut(); + db.apply_changes( + vec![ChangeEvent::ChangedVirtual(virtual_path.clone())], + None, + ); + } + } + + publish_diagnostics(session, &key, client) + } +} diff --git a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs new file mode 100644 index 0000000000000..cf8822858e35f --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs @@ -0,0 +1,125 @@ +use crate::server::Result; +use crate::server::api::diagnostics::publish_diagnostics; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; +use crate::system::AnySystemPath; +use lsp_types as types; +use lsp_types::{FileChangeType, notification as notif}; +use rustc_hash::FxHashMap; +use ty_project::Db; +use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; + +pub(crate) struct DidChangeWatchedFiles; + +impl NotificationHandler for DidChangeWatchedFiles { + type NotificationType = notif::DidChangeWatchedFiles; +} + +impl SyncNotificationHandler for DidChangeWatchedFiles { + fn run( + session: &mut Session, + client: &Client, + params: types::DidChangeWatchedFilesParams, + ) -> Result<()> { + let mut events_by_db: FxHashMap<_, Vec> = FxHashMap::default(); + + for change in params.changes { + let path = match AnySystemPath::try_from_url(&change.uri) { + Ok(path) => path, + Err(err) => { + tracing::warn!( + "Failed to convert URI '{}` to system path: {err:?}", + change.uri + ); + continue; + } + }; + + let system_path = match path { + AnySystemPath::System(system) => system, + AnySystemPath::SystemVirtual(path) => { + tracing::debug!("Ignoring virtual path from change event: `{path}`"); + continue; + } + }; + + let Some(db) = session.project_db_for_path(&system_path) else { + tracing::trace!( + "Ignoring change event for `{system_path}` because it's not in any workspace" + ); + continue; + }; + + let change_event = match change.typ { + FileChangeType::CREATED => ChangeEvent::Created { + path: system_path, + kind: CreatedKind::Any, + }, + FileChangeType::CHANGED => ChangeEvent::Changed { + path: system_path, + kind: ChangedKind::Any, + }, + FileChangeType::DELETED => ChangeEvent::Deleted { + path: system_path, + kind: DeletedKind::Any, + }, + _ => { + tracing::debug!( + "Ignoring unsupported change event type: `{:?}` for {system_path}", + change.typ + ); + continue; + } + }; + + events_by_db + .entry(db.project().root(db).to_path_buf()) + .or_default() + .push(change_event); + } + + if events_by_db.is_empty() { + return Ok(()); + } + + let mut project_changed = false; + + for (root, changes) in events_by_db { + tracing::debug!("Applying changes to `{root}`"); + + // SAFETY: Only paths that are part of the workspace are registered for file watching. + // So, virtual paths and paths that are outside of a workspace does not trigger this + // notification. + let db = session.project_db_for_path_mut(&*root).unwrap(); + + let result = db.apply_changes(changes, None); + + project_changed |= result.project_changed(); + } + + let client_capabilities = session.client_capabilities(); + + if project_changed { + if client_capabilities.diagnostics_refresh { + client.send_request::( + session, + (), + |_, ()| {}, + ); + } else { + for key in session.text_document_keys() { + publish_diagnostics(session, &key, client)?; + } + } + + // TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics) + } + + if client_capabilities.inlay_refresh { + client.send_request::(session, (), |_, ()| {}); + } + + Ok(()) + } +} diff --git a/crates/ty_server/src/server/api/notifications/did_close.rs b/crates/ty_server/src/server/api/notifications/did_close.rs new file mode 100644 index 0000000000000..e7c87364aece0 --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_close.rs @@ -0,0 +1,53 @@ +use crate::server::Result; +use crate::server::api::LSPResult; +use crate::server::api::diagnostics::clear_diagnostics; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; +use crate::system::AnySystemPath; +use lsp_server::ErrorCode; +use lsp_types::DidCloseTextDocumentParams; +use lsp_types::notification::DidCloseTextDocument; +use ty_project::watch::ChangeEvent; + +pub(crate) struct DidCloseTextDocumentHandler; + +impl NotificationHandler for DidCloseTextDocumentHandler { + type NotificationType = DidCloseTextDocument; +} + +impl SyncNotificationHandler for DidCloseTextDocumentHandler { + fn run( + session: &mut Session, + client: &Client, + params: DidCloseTextDocumentParams, + ) -> Result<()> { + let Ok(key) = session.key_from_url(params.text_document.uri.clone()) else { + tracing::debug!( + "Failed to create document key from URI: {}", + params.text_document.uri + ); + return Ok(()); + }; + session + .close_document(&key) + .with_failure_code(ErrorCode::InternalError)?; + + if let AnySystemPath::SystemVirtual(virtual_path) = key.path() { + let db = session.default_project_db_mut(); + db.apply_changes( + vec![ChangeEvent::DeletedVirtual(virtual_path.clone())], + None, + ); + } + + if !session.global_settings().diagnostic_mode().is_workspace() { + // The server needs to clear the diagnostics regardless of whether the client supports + // pull diagnostics or not. This is because the client only has the capability to fetch + // the diagnostics but does not automatically clear them when a document is closed. + clear_diagnostics(&key, client); + } + + Ok(()) + } +} diff --git a/crates/ty_server/src/server/api/notifications/did_close_notebook.rs b/crates/ty_server/src/server/api/notifications/did_close_notebook.rs new file mode 100644 index 0000000000000..6916e56c32e6e --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_close_notebook.rs @@ -0,0 +1,45 @@ +use lsp_types::DidCloseNotebookDocumentParams; +use lsp_types::notification::DidCloseNotebookDocument; + +use crate::server::Result; +use crate::server::api::LSPResult; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; +use crate::system::AnySystemPath; +use ty_project::watch::ChangeEvent; + +pub(crate) struct DidCloseNotebookHandler; + +impl NotificationHandler for DidCloseNotebookHandler { + type NotificationType = DidCloseNotebookDocument; +} + +impl SyncNotificationHandler for DidCloseNotebookHandler { + fn run( + session: &mut Session, + _client: &Client, + params: DidCloseNotebookDocumentParams, + ) -> Result<()> { + let Ok(key) = session.key_from_url(params.notebook_document.uri.clone()) else { + tracing::debug!( + "Failed to create document key from URI: {}", + params.notebook_document.uri + ); + return Ok(()); + }; + session + .close_document(&key) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + if let AnySystemPath::SystemVirtual(virtual_path) = key.path() { + let db = session.default_project_db_mut(); + db.apply_changes( + vec![ChangeEvent::DeletedVirtual(virtual_path.clone())], + None, + ); + } + + Ok(()) + } +} diff --git a/crates/ty_server/src/server/api/notifications/did_open.rs b/crates/ty_server/src/server/api/notifications/did_open.rs new file mode 100644 index 0000000000000..04a4ef8f08a18 --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_open.rs @@ -0,0 +1,58 @@ +use lsp_types::notification::DidOpenTextDocument; +use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem}; + +use crate::TextDocument; +use crate::server::Result; +use crate::server::api::diagnostics::publish_diagnostics; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; +use crate::system::AnySystemPath; +use ruff_db::Db; +use ty_project::watch::ChangeEvent; + +pub(crate) struct DidOpenTextDocumentHandler; + +impl NotificationHandler for DidOpenTextDocumentHandler { + type NotificationType = DidOpenTextDocument; +} + +impl SyncNotificationHandler for DidOpenTextDocumentHandler { + fn run( + session: &mut Session, + client: &Client, + DidOpenTextDocumentParams { + text_document: + TextDocumentItem { + uri, + text, + version, + language_id, + }, + }: DidOpenTextDocumentParams, + ) -> Result<()> { + let Ok(key) = session.key_from_url(uri.clone()) else { + tracing::debug!("Failed to create document key from URI: {}", uri); + return Ok(()); + }; + + let document = TextDocument::new(text, version).with_language_id(&language_id); + session.open_text_document(key.path(), document); + + match key.path() { + AnySystemPath::System(system_path) => { + let db = match session.project_db_for_path_mut(system_path) { + Some(db) => db, + None => session.default_project_db_mut(), + }; + db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None); + } + AnySystemPath::SystemVirtual(virtual_path) => { + let db = session.default_project_db_mut(); + db.files().virtual_file(db, virtual_path); + } + } + + publish_diagnostics(session, &key, client) + } +} diff --git a/crates/ty_server/src/server/api/notifications/did_open_notebook.rs b/crates/ty_server/src/server/api/notifications/did_open_notebook.rs new file mode 100644 index 0000000000000..644046a07258b --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_open_notebook.rs @@ -0,0 +1,59 @@ +use lsp_server::ErrorCode; +use lsp_types::DidOpenNotebookDocumentParams; +use lsp_types::notification::DidOpenNotebookDocument; + +use ruff_db::Db; +use ty_project::watch::ChangeEvent; + +use crate::document::NotebookDocument; +use crate::server::Result; +use crate::server::api::LSPResult; +use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; +use crate::session::Session; +use crate::session::client::Client; +use crate::system::AnySystemPath; + +pub(crate) struct DidOpenNotebookHandler; + +impl NotificationHandler for DidOpenNotebookHandler { + type NotificationType = DidOpenNotebookDocument; +} + +impl SyncNotificationHandler for DidOpenNotebookHandler { + fn run( + session: &mut Session, + _client: &Client, + params: DidOpenNotebookDocumentParams, + ) -> Result<()> { + let Ok(path) = AnySystemPath::try_from_url(¶ms.notebook_document.uri) else { + return Ok(()); + }; + + let notebook = NotebookDocument::new( + params.notebook_document.version, + params.notebook_document.cells, + params.notebook_document.metadata.unwrap_or_default(), + params.cell_text_documents, + ) + .with_failure_code(ErrorCode::InternalError)?; + session.open_notebook_document(&path, notebook); + + match &path { + AnySystemPath::System(system_path) => { + let db = match session.project_db_for_path_mut(system_path) { + Some(db) => db, + None => session.default_project_db_mut(), + }; + db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None); + } + AnySystemPath::SystemVirtual(virtual_path) => { + let db = session.default_project_db_mut(); + db.files().virtual_file(db, virtual_path); + } + } + + // TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics + + Ok(()) + } +} diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs new file mode 100644 index 0000000000000..8c0278d57397c --- /dev/null +++ b/crates/ty_server/src/server/api/requests.rs @@ -0,0 +1,21 @@ +mod completion; +mod diagnostic; +mod goto_type_definition; +mod hover; +mod inlay_hints; +mod semantic_tokens; +mod semantic_tokens_range; +mod shutdown; +mod signature_help; +mod workspace_diagnostic; + +pub(super) use completion::CompletionRequestHandler; +pub(super) use diagnostic::DocumentDiagnosticRequestHandler; +pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; +pub(super) use hover::HoverRequestHandler; +pub(super) use inlay_hints::InlayHintRequestHandler; +pub(super) use semantic_tokens::SemanticTokensRequestHandler; +pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler; +pub(super) use shutdown::ShutdownHandler; +pub(super) use signature_help::SignatureHelpRequestHandler; +pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs new file mode 100644 index 0000000000000..53e14574e1a55 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -0,0 +1,111 @@ +use std::borrow::Cow; + +use lsp_types::request::Completion; +use lsp_types::{CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::completion; +use ty_project::ProjectDatabase; +use ty_python_semantic::CompletionKind; + +use crate::DocumentSnapshot; +use crate::document::PositionExt; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; + +pub(crate) struct CompletionRequestHandler; + +impl RequestHandler for CompletionRequestHandler { + type RequestType = Completion; +} + +impl BackgroundDocumentRequestHandler for CompletionRequestHandler { + fn document_url(params: &CompletionParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: CompletionParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + let offset = params.text_document_position.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + let completions = completion(db, file, offset); + if completions.is_empty() { + return Ok(None); + } + + let max_index_len = completions.len().saturating_sub(1).to_string().len(); + let items: Vec = completions + .into_iter() + .enumerate() + .map(|(i, comp)| { + let kind = comp.kind(db).map(ty_kind_to_lsp_kind); + CompletionItem { + label: comp.name.into(), + kind, + sort_text: Some(format!("{i:-max_index_len$}")), + ..Default::default() + } + }) + .collect(); + let response = CompletionResponse::Array(items); + Ok(Some(response)) + } +} + +impl RetriableRequestHandler for CompletionRequestHandler { + const RETRY_ON_CANCELLATION: bool = true; +} + +fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind { + // Gimme my dang globs in tight scopes! + #[allow(clippy::enum_glob_use)] + use self::CompletionKind::*; + + // ref https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind + match kind { + Text => CompletionItemKind::TEXT, + Method => CompletionItemKind::METHOD, + Function => CompletionItemKind::FUNCTION, + Constructor => CompletionItemKind::CONSTRUCTOR, + Field => CompletionItemKind::FIELD, + Variable => CompletionItemKind::VARIABLE, + Class => CompletionItemKind::CLASS, + Interface => CompletionItemKind::INTERFACE, + Module => CompletionItemKind::MODULE, + Property => CompletionItemKind::PROPERTY, + Unit => CompletionItemKind::UNIT, + Value => CompletionItemKind::VALUE, + Enum => CompletionItemKind::ENUM, + Keyword => CompletionItemKind::KEYWORD, + Snippet => CompletionItemKind::SNIPPET, + Color => CompletionItemKind::COLOR, + File => CompletionItemKind::FILE, + Reference => CompletionItemKind::REFERENCE, + Folder => CompletionItemKind::FOLDER, + EnumMember => CompletionItemKind::ENUM_MEMBER, + Constant => CompletionItemKind::CONSTANT, + Struct => CompletionItemKind::STRUCT, + Event => CompletionItemKind::EVENT, + Operator => CompletionItemKind::OPERATOR, + TypeParameter => CompletionItemKind::TYPE_PARAMETER, + } +} diff --git a/crates/ty_server/src/server/api/requests/diagnostic.rs b/crates/ty_server/src/server/api/requests/diagnostic.rs new file mode 100644 index 0000000000000..b455c3b0a4040 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/diagnostic.rs @@ -0,0 +1,61 @@ +use std::borrow::Cow; + +use lsp_types::request::DocumentDiagnosticRequest; +use lsp_types::{ + DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult, + FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport, Url, +}; + +use crate::server::Result; +use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; +use ty_project::ProjectDatabase; + +pub(crate) struct DocumentDiagnosticRequestHandler; + +impl RequestHandler for DocumentDiagnosticRequestHandler { + type RequestType = DocumentDiagnosticRequest; +} + +impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler { + fn document_url(params: &DocumentDiagnosticParams) -> Cow { + Cow::Borrowed(¶ms.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + _params: DocumentDiagnosticParams, + ) -> Result { + Ok(DocumentDiagnosticReportResult::Report( + DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { + related_documents: None, + full_document_diagnostic_report: FullDocumentDiagnosticReport { + result_id: None, + // SAFETY: Pull diagnostic requests are only called for text documents, not for + // notebook documents. + items: compute_diagnostics(db, &snapshot) + .map_or_else(Vec::new, Diagnostics::expect_text_document), + }, + }), + )) + } +} + +impl RetriableRequestHandler for DocumentDiagnosticRequestHandler { + fn salsa_cancellation_error() -> lsp_server::ResponseError { + lsp_server::ResponseError { + code: lsp_server::ErrorCode::ServerCancelled as i32, + message: "server cancelled the request".to_owned(), + data: serde_json::to_value(lsp_types::DiagnosticServerCancellationData { + retrigger_request: true, + }) + .ok(), + } + } +} diff --git a/crates/ty_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs new file mode 100644 index 0000000000000..59924a26b95d1 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -0,0 +1,76 @@ +use std::borrow::Cow; + +use lsp_types::request::{GotoTypeDefinition, GotoTypeDefinitionParams}; +use lsp_types::{GotoDefinitionResponse, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::goto_type_definition; +use ty_project::ProjectDatabase; + +use crate::DocumentSnapshot; +use crate::document::{PositionExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; + +pub(crate) struct GotoTypeDefinitionRequestHandler; + +impl RequestHandler for GotoTypeDefinitionRequestHandler { + type RequestType = GotoTypeDefinition; +} + +impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { + fn document_url(params: &GotoTypeDefinitionParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: GotoTypeDefinitionParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + let offset = params.text_document_position_params.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + + let Some(ranged) = goto_type_definition(db, file, offset) else { + return Ok(None); + }; + + if snapshot + .resolved_client_capabilities() + .type_definition_link_support + { + let src = Some(ranged.range); + let links: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_link(db, src, snapshot.encoding())) + .collect(); + + Ok(Some(GotoDefinitionResponse::Link(links))) + } else { + let locations: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_location(db, snapshot.encoding())) + .collect(); + + Ok(Some(GotoDefinitionResponse::Array(locations))) + } + } +} + +impl RetriableRequestHandler for GotoTypeDefinitionRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs new file mode 100644 index 0000000000000..bc65a583849fb --- /dev/null +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -0,0 +1,79 @@ +use std::borrow::Cow; + +use crate::DocumentSnapshot; +use crate::document::{PositionExt, ToRangeExt}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; +use lsp_types::request::HoverRequest; +use lsp_types::{HoverContents, HoverParams, MarkupContent, Url}; +use ruff_db::source::{line_index, source_text}; +use ruff_text_size::Ranged; +use ty_ide::{MarkupKind, hover}; +use ty_project::ProjectDatabase; + +pub(crate) struct HoverRequestHandler; + +impl RequestHandler for HoverRequestHandler { + type RequestType = HoverRequest; +} + +impl BackgroundDocumentRequestHandler for HoverRequestHandler { + fn document_url(params: &HoverParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: HoverParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + let offset = params.text_document_position_params.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + + let Some(range_info) = hover(db, file, offset) else { + return Ok(None); + }; + + let (markup_kind, lsp_markup_kind) = if snapshot + .resolved_client_capabilities() + .hover_prefer_markdown + { + (MarkupKind::Markdown, lsp_types::MarkupKind::Markdown) + } else { + (MarkupKind::PlainText, lsp_types::MarkupKind::PlainText) + }; + + let contents = range_info.display(db, markup_kind).to_string(); + + Ok(Some(lsp_types::Hover { + contents: HoverContents::Markup(MarkupContent { + kind: lsp_markup_kind, + value: contents, + }), + range: Some(range_info.file_range().range().to_lsp_range( + &source, + &line_index, + snapshot.encoding(), + )), + })) + } +} + +impl RetriableRequestHandler for HoverRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs new file mode 100644 index 0000000000000..aba2cd715124a --- /dev/null +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -0,0 +1,70 @@ +use std::borrow::Cow; + +use crate::DocumentSnapshot; +use crate::document::{RangeExt, TextSizeExt}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; +use lsp_types::request::InlayHintRequest; +use lsp_types::{InlayHintParams, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::inlay_hints; +use ty_project::ProjectDatabase; + +pub(crate) struct InlayHintRequestHandler; + +impl RequestHandler for InlayHintRequestHandler { + type RequestType = InlayHintRequest; +} + +impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { + fn document_url(params: &InlayHintParams) -> Cow { + Cow::Borrowed(¶ms.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: InlayHintParams, + ) -> crate::server::Result>> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let index = line_index(db, file); + let source = source_text(db, file); + + let range = params + .range + .to_text_range(&source, &index, snapshot.encoding()); + + let inlay_hints = inlay_hints(db, file, range); + + let inlay_hints = inlay_hints + .into_iter() + .map(|hint| lsp_types::InlayHint { + position: hint + .position + .to_position(&source, &index, snapshot.encoding()), + label: lsp_types::InlayHintLabel::String(hint.display(db).to_string()), + kind: Some(lsp_types::InlayHintKind::TYPE), + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + text_edits: None, + }) + .collect(); + + Ok(Some(inlay_hints)) + } +} + +impl RetriableRequestHandler for InlayHintRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/semantic_tokens.rs b/crates/ty_server/src/server/api/requests/semantic_tokens.rs new file mode 100644 index 0000000000000..b646e4dcdd0e4 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/semantic_tokens.rs @@ -0,0 +1,55 @@ +use std::borrow::Cow; + +use crate::DocumentSnapshot; +use crate::server::api::semantic_tokens::generate_semantic_tokens; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; +use lsp_types::{SemanticTokens, SemanticTokensParams, SemanticTokensResult, Url}; +use ty_project::ProjectDatabase; + +pub(crate) struct SemanticTokensRequestHandler; + +impl RequestHandler for SemanticTokensRequestHandler { + type RequestType = lsp_types::request::SemanticTokensFullRequest; +} + +impl BackgroundDocumentRequestHandler for SemanticTokensRequestHandler { + fn document_url(params: &SemanticTokensParams) -> Cow { + Cow::Borrowed(¶ms.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: SemanticTokensParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let lsp_tokens = generate_semantic_tokens( + db, + file, + None, + snapshot.encoding(), + snapshot + .resolved_client_capabilities() + .semantic_tokens_multiline_support, + ); + + Ok(Some(SemanticTokensResult::Tokens(SemanticTokens { + result_id: None, + data: lsp_tokens, + }))) + } +} + +impl RetriableRequestHandler for SemanticTokensRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs b/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs new file mode 100644 index 0000000000000..9e1c8130bb9a7 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/semantic_tokens_range.rs @@ -0,0 +1,65 @@ +use std::borrow::Cow; + +use crate::DocumentSnapshot; +use crate::document::RangeExt; +use crate::server::api::semantic_tokens::generate_semantic_tokens; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; +use lsp_types::{SemanticTokens, SemanticTokensRangeParams, SemanticTokensRangeResult, Url}; +use ruff_db::source::{line_index, source_text}; +use ty_project::ProjectDatabase; + +pub(crate) struct SemanticTokensRangeRequestHandler; + +impl RequestHandler for SemanticTokensRangeRequestHandler { + type RequestType = lsp_types::request::SemanticTokensRangeRequest; +} + +impl BackgroundDocumentRequestHandler for SemanticTokensRangeRequestHandler { + fn document_url(params: &SemanticTokensRangeParams) -> Cow { + Cow::Borrowed(¶ms.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: SemanticTokensRangeParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + + // Convert LSP range to text offsets + let requested_range = params + .range + .to_text_range(&source, &line_index, snapshot.encoding()); + + let lsp_tokens = generate_semantic_tokens( + db, + file, + Some(requested_range), + snapshot.encoding(), + snapshot + .resolved_client_capabilities() + .semantic_tokens_multiline_support, + ); + + Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens { + result_id: None, + data: lsp_tokens, + }))) + } +} + +impl RetriableRequestHandler for SemanticTokensRangeRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/shutdown.rs b/crates/ty_server/src/server/api/requests/shutdown.rs new file mode 100644 index 0000000000000..73b956168c7ec --- /dev/null +++ b/crates/ty_server/src/server/api/requests/shutdown.rs @@ -0,0 +1,17 @@ +use crate::Session; +use crate::server::api::traits::{RequestHandler, SyncRequestHandler}; +use crate::session::client::Client; + +pub(crate) struct ShutdownHandler; + +impl RequestHandler for ShutdownHandler { + type RequestType = lsp_types::request::Shutdown; +} + +impl SyncRequestHandler for ShutdownHandler { + fn run(session: &mut Session, _client: &Client, _params: ()) -> crate::server::Result<()> { + tracing::debug!("Received shutdown request, waiting for shutdown notification"); + session.set_shutdown_requested(true); + Ok(()) + } +} diff --git a/crates/ty_server/src/server/api/requests/signature_help.rs b/crates/ty_server/src/server/api/requests/signature_help.rs new file mode 100644 index 0000000000000..07f3adf669383 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/signature_help.rs @@ -0,0 +1,145 @@ +use std::borrow::Cow; + +use crate::DocumentSnapshot; +use crate::document::{PositionEncoding, PositionExt}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::client::Client; +use lsp_types::request::SignatureHelpRequest; +use lsp_types::{ + Documentation, ParameterInformation, ParameterLabel, SignatureHelp, SignatureHelpParams, + SignatureInformation, Url, +}; +use ruff_db::source::{line_index, source_text}; +use ty_ide::signature_help; +use ty_project::ProjectDatabase; + +pub(crate) struct SignatureHelpRequestHandler; + +impl RequestHandler for SignatureHelpRequestHandler { + type RequestType = SignatureHelpRequest; +} + +impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { + fn document_url(params: &SignatureHelpParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + _client: &Client, + params: SignatureHelpParams, + ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + + let Some(file) = snapshot.file(db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(db, file); + let line_index = line_index(db, file); + let offset = params.text_document_position_params.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + + // Extract signature help capabilities from the client + let resolved_capabilities = snapshot.resolved_client_capabilities(); + + let Some(signature_help_info) = signature_help(db, file, offset) else { + return Ok(None); + }; + + // Compute active parameter from the active signature + let active_parameter = signature_help_info + .active_signature + .and_then(|s| signature_help_info.signatures.get(s)) + .and_then(|sig| sig.active_parameter) + .and_then(|p| u32::try_from(p).ok()); + + // Convert from IDE types to LSP types + let signatures = signature_help_info + .signatures + .into_iter() + .map(|sig| { + let parameters = sig + .parameters + .into_iter() + .map(|param| { + let label = if resolved_capabilities.signature_label_offset_support { + // Find the parameter's offset in the signature label + if let Some(start) = sig.label.find(¶m.label) { + let encoding = snapshot.encoding(); + + // Convert byte offsets to character offsets based on negotiated encoding + let start_char_offset = match encoding { + PositionEncoding::UTF8 => start, + PositionEncoding::UTF16 => { + sig.label[..start].encode_utf16().count() + } + PositionEncoding::UTF32 => sig.label[..start].chars().count(), + }; + + let end_char_offset = match encoding { + PositionEncoding::UTF8 => start + param.label.len(), + PositionEncoding::UTF16 => sig.label + [..start + param.label.len()] + .encode_utf16() + .count(), + PositionEncoding::UTF32 => { + sig.label[..start + param.label.len()].chars().count() + } + }; + + let start_u32 = + u32::try_from(start_char_offset).unwrap_or(u32::MAX); + let end_u32 = u32::try_from(end_char_offset).unwrap_or(u32::MAX); + ParameterLabel::LabelOffsets([start_u32, end_u32]) + } else { + ParameterLabel::Simple(param.label) + } + } else { + ParameterLabel::Simple(param.label) + }; + + ParameterInformation { + label, + documentation: param.documentation.map(Documentation::String), + } + }) + .collect(); + + let active_parameter = if resolved_capabilities.signature_active_parameter_support { + sig.active_parameter.and_then(|p| u32::try_from(p).ok()) + } else { + None + }; + + SignatureInformation { + label: sig.label, + documentation: sig.documentation.map(Documentation::String), + parameters: Some(parameters), + active_parameter, + } + }) + .collect(); + + let signature_help = SignatureHelp { + signatures, + active_signature: signature_help_info + .active_signature + .and_then(|s| u32::try_from(s).ok()), + active_parameter, + }; + + Ok(Some(signature_help)) + } +} + +impl RetriableRequestHandler for SignatureHelpRequestHandler {} diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs new file mode 100644 index 0000000000000..6965c44bc0fbb --- /dev/null +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -0,0 +1,110 @@ +use std::panic::AssertUnwindSafe; + +use lsp_types::request::WorkspaceDiagnosticRequest; +use lsp_types::{ + FullDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, + WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport, + WorkspaceFullDocumentDiagnosticReport, +}; +use rustc_hash::FxHashMap; +use ty_project::CheckMode; + +use crate::server::Result; +use crate::server::api::diagnostics::to_lsp_diagnostic; +use crate::server::api::traits::{ + BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::SessionSnapshot; +use crate::session::client::Client; +use crate::system::file_to_url; + +pub(crate) struct WorkspaceDiagnosticRequestHandler; + +impl RequestHandler for WorkspaceDiagnosticRequestHandler { + type RequestType = WorkspaceDiagnosticRequest; +} + +impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { + fn run( + snapshot: AssertUnwindSafe, + _client: &Client, + _params: WorkspaceDiagnosticParams, + ) -> Result { + let index = snapshot.index(); + + if !index.global_settings().diagnostic_mode().is_workspace() { + tracing::debug!("Workspace diagnostics is disabled; returning empty report"); + return Ok(WorkspaceDiagnosticReportResult::Report( + WorkspaceDiagnosticReport { items: vec![] }, + )); + } + + let mut items = Vec::new(); + + for db in snapshot.projects() { + let diagnostics = db.check_with_mode(CheckMode::AllFiles); + + // Group diagnostics by URL + let mut diagnostics_by_url: FxHashMap> = FxHashMap::default(); + + for diagnostic in diagnostics { + if let Some(span) = diagnostic.primary_span() { + let file = span.expect_ty_file(); + let Some(url) = file_to_url(db, file) else { + tracing::debug!("Failed to convert file to URL at {}", file.path(db)); + continue; + }; + diagnostics_by_url.entry(url).or_default().push(diagnostic); + } + } + + items.reserve(diagnostics_by_url.len()); + + // Convert to workspace diagnostic report format + for (url, file_diagnostics) in diagnostics_by_url { + let version = index + .key_from_url(url.clone()) + .ok() + .and_then(|key| index.make_document_ref(&key)) + .map(|doc| i64::from(doc.version())); + + // Convert diagnostics to LSP format + let lsp_diagnostics = file_diagnostics + .into_iter() + .map(|diagnostic| { + to_lsp_diagnostic(db, &diagnostic, snapshot.position_encoding()) + }) + .collect::>(); + + items.push(WorkspaceDocumentDiagnosticReport::Full( + WorkspaceFullDocumentDiagnosticReport { + uri: url, + version, + full_document_diagnostic_report: FullDocumentDiagnosticReport { + // TODO: We don't implement result ID caching yet + result_id: None, + items: lsp_diagnostics, + }, + }, + )); + } + } + + Ok(WorkspaceDiagnosticReportResult::Report( + WorkspaceDiagnosticReport { items }, + )) + } +} + +impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler { + fn salsa_cancellation_error() -> lsp_server::ResponseError { + lsp_server::ResponseError { + code: lsp_server::ErrorCode::ServerCancelled as i32, + message: "server cancelled the request".to_owned(), + data: serde_json::to_value(lsp_types::DiagnosticServerCancellationData { + retrigger_request: true, + }) + .ok(), + } + } +} diff --git a/crates/ty_server/src/server/api/semantic_tokens.rs b/crates/ty_server/src/server/api/semantic_tokens.rs new file mode 100644 index 0000000000000..b168ef787753b --- /dev/null +++ b/crates/ty_server/src/server/api/semantic_tokens.rs @@ -0,0 +1,97 @@ +use lsp_types::SemanticToken; +use ruff_db::source::{line_index, source_text}; +use ruff_text_size::{Ranged, TextRange}; +use ty_ide::semantic_tokens; +use ty_project::ProjectDatabase; + +use crate::document::{PositionEncoding, ToRangeExt}; + +/// Common logic for generating semantic tokens, either for full document or a specific range. +/// If no range is provided, the entire file is processed. +pub(crate) fn generate_semantic_tokens( + db: &ProjectDatabase, + file: ruff_db::files::File, + range: Option, + encoding: PositionEncoding, + multiline_token_support: bool, +) -> Vec { + let source = source_text(db, file); + let line_index = line_index(db, file); + let semantic_token_data = semantic_tokens(db, file, range); + + // Convert semantic tokens to LSP format + let mut lsp_tokens = Vec::new(); + let mut prev_line = 0u32; + let mut prev_start = 0u32; + + for token in &*semantic_token_data { + let lsp_range = token.range().to_lsp_range(&source, &line_index, encoding); + let line = lsp_range.start.line; + let character = lsp_range.start.character; + + // Calculate length in the negotiated encoding + let length = if !multiline_token_support && lsp_range.start.line != lsp_range.end.line { + // Token spans multiple lines but client doesn't support it + // Clamp to the end of the current line + if let Some(line_text) = source.lines().nth(lsp_range.start.line as usize) { + let line_length_in_encoding = match encoding { + PositionEncoding::UTF8 => line_text.len().try_into().unwrap_or(u32::MAX), + PositionEncoding::UTF16 => line_text + .encode_utf16() + .count() + .try_into() + .unwrap_or(u32::MAX), + PositionEncoding::UTF32 => { + line_text.chars().count().try_into().unwrap_or(u32::MAX) + } + }; + line_length_in_encoding.saturating_sub(lsp_range.start.character) + } else { + 0 + } + } else { + // Either client supports multiline tokens or this is a single-line token + // Use the difference between start and end character positions + if lsp_range.start.line == lsp_range.end.line { + lsp_range.end.character - lsp_range.start.character + } else { + // Multiline token and client supports it - calculate full token length + let token_text = &source[token.range()]; + match encoding { + PositionEncoding::UTF8 => token_text.len().try_into().unwrap_or(u32::MAX), + PositionEncoding::UTF16 => token_text + .encode_utf16() + .count() + .try_into() + .unwrap_or(u32::MAX), + PositionEncoding::UTF32 => { + token_text.chars().count().try_into().unwrap_or(u32::MAX) + } + } + } + }; + let token_type = token.token_type as u32; + let token_modifiers = token.modifiers.bits(); + + // LSP semantic tokens are encoded as deltas + let delta_line = line - prev_line; + let delta_start = if delta_line == 0 { + character - prev_start + } else { + character + }; + + lsp_tokens.push(SemanticToken { + delta_line, + delta_start, + length, + token_type, + token_modifiers_bitset: token_modifiers, + }); + + prev_line = line; + prev_start = character; + } + + lsp_tokens +} diff --git a/crates/ty_server/src/server/api/traits.rs b/crates/ty_server/src/server/api/traits.rs new file mode 100644 index 0000000000000..68b45364dac1b --- /dev/null +++ b/crates/ty_server/src/server/api/traits.rs @@ -0,0 +1,149 @@ +//! Traits for handling requests and notifications from the LSP client. +//! +//! This module defines the trait abstractions used by the language server to handle incoming +//! requests and notifications from clients. It provides a type-safe way to implement LSP handlers +//! with different execution models (synchronous or asynchronous) and automatic retry capabilities. +//! +//! All request and notification handlers must implement the base traits [`RequestHandler`] and +//! [`NotificationHandler`], respectively, which associate them with specific LSP request or +//! notification types. These base traits are then extended by more specific traits that define +//! the execution model of the handler. +//! +//! The [`SyncRequestHandler`] and [`SyncNotificationHandler`] traits are for handlers that +//! executes synchronously on the main loop, providing mutable access to the [`Session`] that +//! contains the current state of the server. This is useful for handlers that need to modify +//! the server state such as when the content of a file changes. +//! +//! The [`BackgroundDocumentRequestHandler`] and [`BackgroundDocumentNotificationHandler`] traits +//! are for handlers that operate on a single document and can be executed on a background thread. +//! These handlers will have access to a snapshot of the document at the time of the request or +//! notification, allowing them to perform operations without blocking the main loop. There is also +//! the [`BackgroundRequestHandler`] trait for handlers that operate on the entire session, which +//! includes all the workspaces, instead of a single document and can also be executed on a +//! background thread like fetching the workspace diagnostics. +//! +//! The [`RetriableRequestHandler`] trait is a marker trait for handlers that can be retried if the +//! Salsa database is modified during execution. +//! +//! The [`SyncNotificationHandler`] is the most common trait that would be used because most +//! notifications are specific to a single document and require updating the server state. +//! Similarly, the [`BackgroundDocumentRequestHandler`] is the most common request handler that +//! would be used as most requests are document-specific and can be executed in the background. +//! +//! See the `./requests` and `./notifications` directories for concrete implementations of these +//! traits in action. + +use std::borrow::Cow; + +use std::panic::AssertUnwindSafe; + +use crate::session::client::Client; +use crate::session::{DocumentSnapshot, Session, SessionSnapshot}; + +use lsp_types::Url; +use lsp_types::notification::Notification; +use lsp_types::request::Request; +use ty_project::ProjectDatabase; + +/// A supertrait for any server request handler. +pub(super) trait RequestHandler { + type RequestType: Request; + const METHOD: &'static str = <::RequestType>::METHOD; +} + +/// A request handler that needs mutable access to the session. +/// +/// This will block the main message receiver loop, meaning that no incoming requests or +/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or +/// long-running computations. +pub(super) trait SyncRequestHandler: RequestHandler { + fn run( + session: &mut Session, + client: &Client, + params: <::RequestType as Request>::Params, + ) -> super::Result<<::RequestType as Request>::Result>; +} + +pub(super) trait RetriableRequestHandler: RequestHandler { + /// Whether this request can be cancelled if the Salsa database is modified. + const RETRY_ON_CANCELLATION: bool = false; + + /// The error to return if the request was cancelled due to a modification to the Salsa + /// database. + /// + /// By default, this returns a [`ContentModified`] error to indicate that the content of a + /// document has changed since the request was made. + /// + /// [`ContentModified`]: lsp_server::ErrorCode::ContentModified + fn salsa_cancellation_error() -> lsp_server::ResponseError { + lsp_server::ResponseError { + code: lsp_server::ErrorCode::ContentModified as i32, + message: "content modified".to_string(), + data: None, + } + } +} + +/// A request handler that can be run on a background thread. +/// +/// This handler is specific to requests that operate on a single document. +pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler { + /// Returns the URL of the document that this request handler operates on. + fn document_url( + params: &<::RequestType as Request>::Params, + ) -> Cow; + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: DocumentSnapshot, + client: &Client, + params: <::RequestType as Request>::Params, + ) -> super::Result<<::RequestType as Request>::Result>; +} + +/// A request handler that can be run on a background thread. +/// +/// Unlike [`BackgroundDocumentRequestHandler`], this handler operates on the entire session, +/// which includes all the workspaces, without being tied to a specific document. It is useful for +/// operations that require access to the entire session state, such as fetching workspace +/// diagnostics. +pub(super) trait BackgroundRequestHandler: RetriableRequestHandler { + fn run( + snapshot: AssertUnwindSafe, + client: &Client, + params: <::RequestType as Request>::Params, + ) -> super::Result<<::RequestType as Request>::Result>; +} + +/// A supertrait for any server notification handler. +pub(super) trait NotificationHandler { + type NotificationType: Notification; + const METHOD: &'static str = <::NotificationType>::METHOD; +} + +/// A notification handler that needs mutable access to the session. +/// +/// This will block the main message receiver loop, meaning that no incoming requests or +/// notifications will be handled while `run` is executing. Try to avoid doing any I/O or +/// long-running computations. +pub(super) trait SyncNotificationHandler: NotificationHandler { + fn run( + session: &mut Session, + client: &Client, + params: <::NotificationType as Notification>::Params, + ) -> super::Result<()>; +} + +/// A notification handler that can be run on a background thread. +pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler { + /// Returns the URL of the document that this notification handler operates on. + fn document_url( + params: &<::NotificationType as Notification>::Params, + ) -> Cow; + + fn run_with_snapshot( + snapshot: DocumentSnapshot, + client: &Client, + params: <::NotificationType as Notification>::Params, + ) -> super::Result<()>; +} diff --git a/crates/ty_server/src/server/connection.rs b/crates/ty_server/src/server/connection.rs new file mode 100644 index 0000000000000..f347cf8eb47a6 --- /dev/null +++ b/crates/ty_server/src/server/connection.rs @@ -0,0 +1,49 @@ +use lsp_server as lsp; + +pub(crate) type ConnectionSender = crossbeam::channel::Sender; + +/// A builder for `Connection` that handles LSP initialization. +pub(crate) struct ConnectionInitializer { + connection: lsp::Connection, +} + +impl ConnectionInitializer { + /// Create a new LSP server connection over stdin/stdout. + pub(crate) fn stdio() -> (Self, lsp::IoThreads) { + let (connection, threads) = lsp::Connection::stdio(); + (Self { connection }, threads) + } + + /// Starts the initialization process with the client by listening for an initialization request. + /// Returns a request ID that should be passed into `initialize_finish` later, + /// along with the initialization parameters that were provided. + pub(super) fn initialize_start( + &self, + ) -> crate::Result<(lsp::RequestId, lsp_types::InitializeParams)> { + let (id, params) = self.connection.initialize_start()?; + Ok((id, serde_json::from_value(params)?)) + } + + /// Finishes the initialization process with the client, + /// returning an initialized `Connection`. + pub(super) fn initialize_finish( + self, + id: lsp::RequestId, + server_capabilities: &lsp_types::ServerCapabilities, + name: &str, + version: &str, + ) -> crate::Result { + self.connection.initialize_finish( + id, + serde_json::json!({ + "capabilities": server_capabilities, + "serverInfo": { + "name": name, + "version": version + } + }), + )?; + + Ok(self.connection) + } +} diff --git a/crates/ty_server/src/server/main_loop.rs b/crates/ty_server/src/server/main_loop.rs new file mode 100644 index 0000000000000..45187a66bd501 --- /dev/null +++ b/crates/ty_server/src/server/main_loop.rs @@ -0,0 +1,312 @@ +use crate::server::schedule::Scheduler; +use crate::server::{Server, api}; +use crate::session::ClientOptions; +use crate::session::client::Client; +use anyhow::anyhow; +use crossbeam::select; +use lsp_server::Message; +use lsp_types::notification::Notification; +use lsp_types::{ + ConfigurationParams, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Url, +}; +use serde_json::Value; + +pub(crate) type MainLoopSender = crossbeam::channel::Sender; +pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver; + +impl Server { + pub(super) fn main_loop(&mut self) -> crate::Result<()> { + self.initialize(&Client::new( + self.main_loop_sender.clone(), + self.connection.sender.clone(), + )); + + let mut scheduler = Scheduler::new(self.worker_threads); + + while let Ok(next_event) = self.next_event() { + let Some(next_event) = next_event else { + anyhow::bail!("client exited without proper shutdown sequence"); + }; + + match next_event { + Event::Message(msg) => { + let Some(msg) = self.session.should_defer_message(msg) else { + continue; + }; + + let client = Client::new( + self.main_loop_sender.clone(), + self.connection.sender.clone(), + ); + + let task = match msg { + Message::Request(req) => { + self.session + .request_queue_mut() + .incoming_mut() + .register(req.id.clone(), req.method.clone()); + + if self.session.is_shutdown_requested() { + tracing::warn!( + "Received request after server shutdown was requested, discarding" + ); + client.respond_err( + req.id, + lsp_server::ResponseError { + code: lsp_server::ErrorCode::InvalidRequest as i32, + message: "Shutdown already requested".to_owned(), + data: None, + }, + ); + continue; + } + + api::request(req) + } + Message::Notification(notification) => { + if notification.method == lsp_types::notification::Exit::METHOD { + if !self.session.is_shutdown_requested() { + return Err(anyhow!( + "Received exit notification before a shutdown request" + )); + } + + tracing::debug!("Received exit notification, exiting"); + return Ok(()); + } + + api::notification(notification) + } + + // Handle the response from the client to a server request + Message::Response(response) => { + if let Some(handler) = self + .session + .request_queue_mut() + .outgoing_mut() + .complete(&response.id) + { + handler(&client, response); + } else { + tracing::error!( + "Received a response with ID {}, which was not expected", + response.id + ); + } + + continue; + } + }; + + scheduler.dispatch(task, &mut self.session, client); + } + Event::Action(action) => match action { + Action::SendResponse(response) => { + // Filter out responses for already canceled requests. + if let Some((start_time, method)) = self + .session + .request_queue_mut() + .incoming_mut() + .complete(&response.id) + { + let duration = start_time.elapsed(); + tracing::trace!(name: "message response", method, %response.id, duration = format_args!("{:0.2?}", duration)); + + self.connection.sender.send(Message::Response(response))?; + } else { + tracing::trace!( + "Ignoring response for canceled request id={}", + response.id + ); + } + } + + Action::RetryRequest(request) => { + // Never retry canceled requests. + if self + .session + .request_queue() + .incoming() + .is_pending(&request.id) + { + api::request(request); + } else { + tracing::debug!( + "Request {}/{} was cancelled, not retrying", + request.method, + request.id + ); + } + } + Action::InitializeWorkspaces(workspaces_with_options) => { + self.session.initialize_workspaces(workspaces_with_options); + } + }, + } + } + + Ok(()) + } + + /// Waits for the next message from the client or action. + /// + /// Returns `Ok(None)` if the client connection is closed. + fn next_event(&mut self) -> Result, crossbeam::channel::RecvError> { + // We can't queue those into the main loop because that could result in reordering if + // the `select` below picks a client message first. + if let Some(deferred) = self.session.take_deferred_messages() { + match &deferred { + Message::Request(req) => { + tracing::debug!("Processing deferred request `{}`", req.method); + } + Message::Notification(notification) => { + tracing::debug!("Processing deferred notification `{}`", notification.method); + } + Message::Response(response) => { + tracing::debug!("Processing deferred response `{}`", response.id); + } + } + + return Ok(Some(Event::Message(deferred))); + } + + select!( + recv(self.connection.receiver) -> msg => { + // Ignore disconnect errors, they're handled by the main loop (it will exit). + Ok(msg.ok().map(Event::Message)) + }, + recv(self.main_loop_receiver) -> event => event.map(Some), + ) + } + + fn initialize(&mut self, client: &Client) { + let urls = self + .session + .workspaces() + .urls() + .cloned() + .collect::>(); + let items = urls + .iter() + .map(|root| lsp_types::ConfigurationItem { + scope_uri: Some(root.clone()), + section: Some("ty".to_string()), + }) + .collect(); + + tracing::debug!("Requesting workspace configuration for workspaces"); + client + .send_request::( + &self.session, + ConfigurationParams { items }, + |client, result: Vec| { + tracing::debug!("Received workspace configurations, initializing workspaces"); + assert_eq!(result.len(), urls.len()); + + let workspaces_with_options: Vec<_> = urls + .into_iter() + .zip(result) + .map(|(url, value)| { + let options: ClientOptions = serde_json::from_value(value).unwrap_or_else(|err| { + tracing::warn!("Failed to deserialize workspace options for {url}: {err}. Using default options."); + ClientOptions::default() + }); + + (url, options) + }) + .collect(); + + + client.queue_action(Action::InitializeWorkspaces(workspaces_with_options)); + }, + ); + + let fs_watcher = self + .client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_watched_files?.dynamic_registration) + .unwrap_or_default(); + + if fs_watcher { + let registration = lsp_types::Registration { + id: "workspace/didChangeWatchedFiles".to_owned(), + method: "workspace/didChangeWatchedFiles".to_owned(), + register_options: Some( + serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { + watchers: vec![ + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/ty.toml".into()), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String( + "**/.gitignore".into(), + ), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/.ignore".into()), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String( + "**/pyproject.toml".into(), + ), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/*.py".into()), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/*.pyi".into()), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/*.ipynb".into()), + kind: None, + }, + ], + }) + .unwrap(), + ), + }; + let response_handler = move |_: &Client, ()| { + tracing::info!("File watcher successfully registered"); + }; + + client.send_request::( + &self.session, + lsp_types::RegistrationParams { + registrations: vec![registration], + }, + response_handler, + ); + } else { + tracing::warn!("The client does not support file system watching."); + } + } +} + +/// An action that should be performed on the main loop. +#[derive(Debug)] +pub(crate) enum Action { + /// Send a response to the client + SendResponse(lsp_server::Response), + + /// Retry a request that previously failed due to a salsa cancellation. + RetryRequest(lsp_server::Request), + + /// Initialize the workspace after the server received + /// the options from the client. + InitializeWorkspaces(Vec<(Url, ClientOptions)>), +} + +#[derive(Debug)] +pub(crate) enum Event { + /// An incoming message from the LSP client. + Message(lsp_server::Message), + + Action(Action), +} diff --git a/crates/ty_server/src/server/schedule.rs b/crates/ty_server/src/server/schedule.rs new file mode 100644 index 0000000000000..5473900f2c4c7 --- /dev/null +++ b/crates/ty_server/src/server/schedule.rs @@ -0,0 +1,74 @@ +use std::num::NonZeroUsize; + +use crate::session::Session; + +mod task; +mod thread; + +use self::{ + task::{BackgroundTaskBuilder, SyncTask}, + thread::ThreadPriority, +}; +use crate::session::client::Client; +pub(super) use task::{BackgroundSchedule, Task}; + +/// The event loop thread is actually a secondary thread that we spawn from the +/// _actual_ main thread. This secondary thread has a larger stack size +/// than some OS defaults (Windows, for example) and is also designated as +/// high-priority. +pub(crate) fn spawn_main_loop( + func: impl FnOnce() -> crate::Result<()> + Send + 'static, +) -> crate::Result>> { + // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. + const MAIN_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024; + const MAIN_THREAD_NAME: &str = "ty:main"; + Ok( + thread::Builder::new(thread::ThreadPriority::LatencySensitive) + .name(MAIN_THREAD_NAME.into()) + .stack_size(MAIN_THREAD_STACK_SIZE) + .spawn(func)?, + ) +} + +pub(crate) struct Scheduler { + fmt_pool: thread::Pool, + background_pool: thread::Pool, +} + +impl Scheduler { + pub(super) fn new(worker_threads: NonZeroUsize) -> Self { + const FMT_THREADS: usize = 1; + Self { + fmt_pool: thread::Pool::new(NonZeroUsize::try_from(FMT_THREADS).unwrap()), + background_pool: thread::Pool::new(worker_threads), + } + } + + /// Dispatches a `task` by either running it as a blocking function or + /// executing it on a background thread pool. + pub(super) fn dispatch(&mut self, task: task::Task, session: &mut Session, client: Client) { + match task { + Task::Sync(SyncTask { func }) => { + func(session, &client); + } + Task::Background(BackgroundTaskBuilder { + schedule, + builder: func, + }) => { + let static_func = func(session); + let task = move || static_func(&client); + match schedule { + BackgroundSchedule::Worker => { + self.background_pool.spawn(ThreadPriority::Worker, task); + } + BackgroundSchedule::LatencySensitive => self + .background_pool + .spawn(ThreadPriority::LatencySensitive, task), + BackgroundSchedule::Fmt => { + self.fmt_pool.spawn(ThreadPriority::LatencySensitive, task); + } + } + } + } + } +} diff --git a/crates/ty_server/src/server/schedule/task.rs b/crates/ty_server/src/server/schedule/task.rs new file mode 100644 index 0000000000000..3d4ddb3123311 --- /dev/null +++ b/crates/ty_server/src/server/schedule/task.rs @@ -0,0 +1,94 @@ +use lsp_server::RequestId; +use serde::Serialize; + +use crate::session::Session; +use crate::session::client::Client; + +type LocalFn = Box; + +type BackgroundFn = Box; + +type BackgroundFnBuilder = Box BackgroundFn>; + +/// Describes how the task should be run. +#[derive(Clone, Copy, Debug, Default)] +pub(in crate::server) enum BackgroundSchedule { + /// The task should be run on the background thread designated + /// for formatting actions. This is a high priority thread. + #[expect(dead_code)] + Fmt, + /// The task should be run on the general high-priority background + /// thread. Reserved for actions caused by the user typing (e.g.syntax highlighting). + LatencySensitive, + /// The task should be run on a regular-priority background thread. + /// The default for any request that isn't in the critical path of the user typing. + #[default] + Worker, +} + +/// A [`Task`] is a future that has not yet started, and it is the job of +/// the [`super::Scheduler`] to make that happen, via [`super::Scheduler::dispatch`]. +/// A task can either run on the main thread (in other words, the same thread as the +/// scheduler) or it can run in a background thread. The main difference between +/// the two is that background threads only have a read-only snapshot of the session, +/// while local tasks have exclusive access and can modify it as they please. Keep in mind that +/// local tasks will **block** the main event loop, so only use local tasks if you **need** +/// mutable state access or you need the absolute lowest latency possible. +pub(in crate::server) enum Task { + Background(BackgroundTaskBuilder), + Sync(SyncTask), +} + +// The reason why this isn't just a 'static background closure +// is because we need to take a snapshot of the session before sending +// this task to the background, and the inner closure can't take the session +// as an immutable reference since it's used mutably elsewhere. So instead, +// a background task is built using an outer closure that borrows the session to take a snapshot, +// that the inner closure can capture. This builder closure has a lifetime linked to the scheduler. +// When the task is dispatched, the scheduler runs the synchronous builder, which takes the session +// as a reference, to create the inner 'static closure. That closure is then moved to a background task pool. +pub(in crate::server) struct BackgroundTaskBuilder { + pub(super) schedule: BackgroundSchedule, + pub(super) builder: BackgroundFnBuilder, +} + +pub(in crate::server) struct SyncTask { + pub(super) func: LocalFn, +} + +impl Task { + /// Creates a new background task. + pub(crate) fn background(schedule: BackgroundSchedule, func: F) -> Self + where + F: FnOnce(&Session) -> Box + 'static, + { + Self::Background(BackgroundTaskBuilder { + schedule, + builder: Box::new(func), + }) + } + /// Creates a new local task. + pub(crate) fn sync(func: F) -> Self + where + F: FnOnce(&mut Session, &Client) + 'static, + { + Self::Sync(SyncTask { + func: Box::new(func), + }) + } + /// Creates a local task that immediately + /// responds with the provided `request`. + pub(crate) fn immediate(id: RequestId, result: crate::server::Result) -> Self + where + R: Serialize + Send + 'static, + { + Self::sync(move |_, client| { + client.respond(&id, result); + }) + } + + /// Creates a local task that does nothing. + pub(crate) fn nothing() -> Self { + Self::sync(move |_, _| {}) + } +} diff --git a/crates/red_knot_server/src/server/schedule/thread.rs b/crates/ty_server/src/server/schedule/thread.rs similarity index 100% rename from crates/red_knot_server/src/server/schedule/thread.rs rename to crates/ty_server/src/server/schedule/thread.rs diff --git a/crates/ty_server/src/server/schedule/thread/pool.rs b/crates/ty_server/src/server/schedule/thread/pool.rs new file mode 100644 index 0000000000000..c49d642a772c7 --- /dev/null +++ b/crates/ty_server/src/server/schedule/thread/pool.rs @@ -0,0 +1,138 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/rust-lang/rust-analyzer.git | +// | File: `crates/stdx/src/thread/pool.rs` | +// | Commit: 03b3cb6be9f21c082f4206b35c7fe7f291c94eaa | +// +------------------------------------------------------------+ +//! [`Pool`] implements a basic custom thread pool +//! inspired by the [`threadpool` crate](http://docs.rs/threadpool). +//! When you spawn a task you specify a thread priority +//! so the pool can schedule it to run on a thread with that priority. +//! rust-analyzer uses this to prioritize work based on latency requirements. +//! +//! The thread pool is implemented entirely using +//! the threading utilities in [`crate::server::schedule::thread`]. + +use crossbeam::channel::{Receiver, Sender}; +use std::panic::AssertUnwindSafe; +use std::{ + num::NonZeroUsize, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +use super::{Builder, JoinHandle, ThreadPriority}; + +pub(crate) struct Pool { + // `_handles` is never read: the field is present + // only for its `Drop` impl. + + // The worker threads exit once the channel closes; + // make sure to keep `job_sender` above `handles` + // so that the channel is actually closed + // before we join the worker threads! + job_sender: Sender, + _handles: Vec, + extant_tasks: Arc, +} + +struct Job { + requested_priority: ThreadPriority, + f: Box, +} + +impl Pool { + pub(crate) fn new(threads: NonZeroUsize) -> Pool { + // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. + const STACK_SIZE: usize = 2 * 1024 * 1024; + const INITIAL_PRIORITY: ThreadPriority = ThreadPriority::Worker; + + let threads = usize::from(threads); + + let (job_sender, job_receiver) = crossbeam::channel::bounded(std::cmp::min(threads * 2, 4)); + let extant_tasks = Arc::new(AtomicUsize::new(0)); + + let mut handles = Vec::with_capacity(threads); + for i in 0..threads { + let handle = Builder::new(INITIAL_PRIORITY) + .stack_size(STACK_SIZE) + .name(format!("ty:worker:{i}")) + .spawn({ + let extant_tasks = Arc::clone(&extant_tasks); + let job_receiver: Receiver = job_receiver.clone(); + move || { + let mut current_priority = INITIAL_PRIORITY; + for job in job_receiver { + if job.requested_priority != current_priority { + job.requested_priority.apply_to_current_thread(); + current_priority = job.requested_priority; + } + extant_tasks.fetch_add(1, Ordering::SeqCst); + + // SAFETY: it's safe to assume that `job.f` is unwind safe because we always + // abort the process if it panics. + // Panicking here ensures that we don't swallow errors and is the same as + // what rayon does. + // Any recovery should be implemented outside the thread pool (e.g. when + // dispatching requests/notifications etc). + if let Err(error) = std::panic::catch_unwind(AssertUnwindSafe(job.f)) { + if let Some(msg) = error.downcast_ref::() { + tracing::error!("Worker thread panicked with: {msg}; aborting"); + } else if let Some(msg) = error.downcast_ref::<&str>() { + tracing::error!("Worker thread panicked with: {msg}; aborting"); + } else if let Some(cancelled) = + error.downcast_ref::() + { + tracing::error!( + "Worker thread got cancelled: {cancelled}; aborting" + ); + } else { + tracing::error!( + "Worker thread panicked with: {error:?}; aborting" + ); + } + + std::process::abort(); + } + + extant_tasks.fetch_sub(1, Ordering::SeqCst); + } + } + }) + .expect("failed to spawn thread"); + + handles.push(handle); + } + + Pool { + _handles: handles, + extant_tasks, + job_sender, + } + } + + pub(crate) fn spawn(&self, priority: ThreadPriority, f: F) + where + F: FnOnce() + Send + 'static, + { + let f = Box::new(move || { + if cfg!(debug_assertions) { + priority.assert_is_used_on_current_thread(); + } + f(); + }); + + let job = Job { + requested_priority: priority, + f, + }; + self.job_sender.send(job).unwrap(); + } + + #[expect(dead_code)] + pub(super) fn len(&self) -> usize { + self.extant_tasks.load(Ordering::SeqCst) + } +} diff --git a/crates/red_knot_server/src/server/schedule/thread/priority.rs b/crates/ty_server/src/server/schedule/thread/priority.rs similarity index 97% rename from crates/red_knot_server/src/server/schedule/thread/priority.rs rename to crates/ty_server/src/server/schedule/thread/priority.rs index e6a555242fcb7..d1e0fefdad7ef 100644 --- a/crates/red_knot_server/src/server/schedule/thread/priority.rs +++ b/crates/ty_server/src/server/schedule/thread/priority.rs @@ -183,14 +183,14 @@ mod imp { QoSClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND, }; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { libc::pthread_set_qos_class_self_np(c, 0) }; if code == 0 { return; } - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; match errno { @@ -223,12 +223,16 @@ mod imp { } pub(super) fn get_current_thread_qos_class() -> Option { - #[allow(unsafe_code)] + #[expect(unsafe_code)] let current_thread = unsafe { libc::pthread_self() }; let mut qos_class_raw = libc::qos_class_t::QOS_CLASS_UNSPECIFIED; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { - libc::pthread_get_qos_class_np(current_thread, &mut qos_class_raw, std::ptr::null_mut()) + libc::pthread_get_qos_class_np( + current_thread, + &raw mut qos_class_raw, + std::ptr::null_mut(), + ) }; if code != 0 { @@ -241,7 +245,7 @@ mod imp { // ones which we cannot handle anyway // // 0: https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/src/qos.c#L171-L177 - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; unreachable!("`pthread_get_qos_class_np` failed unexpectedly (os error {errno})"); } diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs new file mode 100644 index 0000000000000..de93a5e2e86e4 --- /dev/null +++ b/crates/ty_server/src/session.rs @@ -0,0 +1,561 @@ +//! Data model, state management, and configuration resolution. + +use std::collections::{BTreeMap, VecDeque}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +use anyhow::{Context, anyhow}; +use lsp_server::Message; +use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url}; +use options::GlobalOptions; +use ruff_db::Db; +use ruff_db::files::{File, system_path_to_file}; +use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ty_project::metadata::Options; +use ty_project::{ProjectDatabase, ProjectMetadata}; + +pub(crate) use self::capabilities::ResolvedClientCapabilities; +pub use self::index::DocumentQuery; +pub(crate) use self::options::{AllOptions, ClientOptions, DiagnosticMode}; +pub(crate) use self::settings::ClientSettings; +use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; +use crate::session::request_queue::RequestQueue; +use crate::system::{AnySystemPath, LSPSystem}; +use crate::{PositionEncoding, TextDocument}; + +mod capabilities; +pub(crate) mod client; +pub(crate) mod index; +mod options; +mod request_queue; +mod settings; + +/// The global state for the LSP +pub struct Session { + /// Used to retrieve information about open documents and settings. + /// + /// This will be [`None`] when a mutable reference is held to the index via [`index_mut`] + /// to prevent the index from being accessed while it is being modified. It will be restored + /// when the mutable reference ([`MutIndexGuard`]) is dropped. + /// + /// [`index_mut`]: Session::index_mut + index: Option>, + + /// Maps workspace folders to their respective workspace. + workspaces: Workspaces, + + /// The projects across all workspaces. + projects: BTreeMap, + + default_project: ProjectDatabase, + + /// The global position encoding, negotiated during LSP initialization. + position_encoding: PositionEncoding, + + /// Tracks what LSP features the client supports and doesn't support. + resolved_client_capabilities: Arc, + + /// Tracks the pending requests between client and server. + request_queue: RequestQueue, + + /// Has the client requested the server to shutdown. + shutdown_requested: bool, + + deferred_messages: VecDeque, +} + +impl Session { + pub(crate) fn new( + client_capabilities: &ClientCapabilities, + position_encoding: PositionEncoding, + global_options: GlobalOptions, + workspace_folders: Vec<(Url, ClientOptions)>, + ) -> crate::Result { + let index = Arc::new(index::Index::new(global_options.into_settings())); + + let mut workspaces = Workspaces::default(); + for (url, options) in workspace_folders { + workspaces.register(url, options)?; + } + + let default_project = { + let system = LSPSystem::new(index.clone()); + let metadata = ProjectMetadata::from_options( + Options::default(), + system.current_directory().to_path_buf(), + None, + ) + .unwrap(); + ProjectDatabase::new(metadata, system).unwrap() + }; + + Ok(Self { + position_encoding, + workspaces, + deferred_messages: VecDeque::new(), + index: Some(index), + default_project, + projects: BTreeMap::new(), + resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( + client_capabilities, + )), + request_queue: RequestQueue::new(), + shutdown_requested: false, + }) + } + + pub(crate) fn request_queue(&self) -> &RequestQueue { + &self.request_queue + } + + pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue { + &mut self.request_queue + } + + pub(crate) fn is_shutdown_requested(&self) -> bool { + self.shutdown_requested + } + + pub(crate) fn set_shutdown_requested(&mut self, requested: bool) { + self.shutdown_requested = requested; + } + + /// The LSP specification doesn't allow configuration requests during initialization, + /// but we need access to the configuration to resolve the settings in turn to create the + /// project databases. This will become more important in the future when we support + /// persistent caching. It's then crucial that we have the correct settings to select the + /// right cache. + /// + /// We work around this by queueing up all messages that arrive between the `initialized` notification + /// and the completion of workspace initialization (which waits for the client's configuration response). + /// + /// This queuing is only necessary when registering *new* workspaces. Changes to configurations + /// don't need to go through the same process because we can update the existing + /// database in place. + /// + /// See + pub(crate) fn should_defer_message(&mut self, message: Message) -> Option { + if self.workspaces.all_initialized() { + Some(message) + } else { + match &message { + Message::Request(request) => { + tracing::debug!( + "Deferring `{}` request until all workspaces are initialized", + request.method + ); + } + Message::Response(_) => { + // We still want to get client responses even during workspace initialization. + return Some(message); + } + Message::Notification(notification) => { + tracing::debug!( + "Deferring `{}` notification until all workspaces are initialized", + notification.method + ); + } + } + + self.deferred_messages.push_back(message); + None + } + } + + pub(crate) fn workspaces(&self) -> &Workspaces { + &self.workspaces + } + + // TODO(dhruvmanila): Ideally, we should have a single method for `workspace_db_for_path_mut` + // and `default_workspace_db_mut` but the borrow checker doesn't allow that. + // https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437 + + /// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, + /// or the default project if no project is found for the path. + pub(crate) fn project_db_or_default(&self, path: &AnySystemPath) -> &ProjectDatabase { + path.as_system() + .and_then(|path| self.project_db_for_path(path)) + .unwrap_or_else(|| self.default_project_db()) + } + + /// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if + /// any. + pub(crate) fn project_db_for_path( + &self, + path: impl AsRef, + ) -> Option<&ProjectDatabase> { + self.projects + .range(..=path.as_ref().to_path_buf()) + .next_back() + .map(|(_, db)| db) + } + + /// Returns a mutable reference to the project [`ProjectDatabase`] corresponding to the given + /// path, if any. + pub(crate) fn project_db_for_path_mut( + &mut self, + path: impl AsRef, + ) -> Option<&mut ProjectDatabase> { + self.projects + .range_mut(..=path.as_ref().to_path_buf()) + .next_back() + .map(|(_, db)| db) + } + + /// Returns a reference to the default project [`ProjectDatabase`]. The default project is the + /// minimum root path in the project map. + pub(crate) fn default_project_db(&self) -> &ProjectDatabase { + &self.default_project + } + + /// Returns a mutable reference to the default project [`ProjectDatabase`]. + pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase { + &mut self.default_project + } + + fn projects_mut(&mut self) -> impl Iterator + '_ { + self.projects + .values_mut() + .chain(std::iter::once(&mut self.default_project)) + } + + pub(crate) fn key_from_url(&self, url: Url) -> crate::Result { + self.index().key_from_url(url) + } + + pub(crate) fn initialize_workspaces(&mut self, workspace_settings: Vec<(Url, ClientOptions)>) { + assert!(!self.workspaces.all_initialized()); + + for (url, options) in workspace_settings { + let Some(workspace) = self.workspaces.initialize(&url, options) else { + continue; + }; + // For now, create one project database per workspace. + // In the future, index the workspace directories to find all projects + // and create a project database for each. + let system = LSPSystem::new(self.index.as_ref().unwrap().clone()); + let system_path = workspace.root(); + + let root = system_path.to_path_buf(); + let project = ProjectMetadata::discover(&root, &system) + .context("Failed to find project configuration") + .and_then(|mut metadata| { + // TODO(dhruvmanila): Merge the client options with the project metadata options. + metadata + .apply_configuration_files(&system) + .context("Failed to apply configuration files")?; + ProjectDatabase::new(metadata, system) + .context("Failed to create project database") + }); + + // TODO(micha): Handle the case where the program settings are incorrect more gracefully. + // The easiest is to ignore those projects but to show a message to the user that we do so. + // Ignoring the projects has the effect that we'll use the default project for those files. + // The only challenge with this is that we need to register the project when the configuration + // becomes valid again. But that's a case we need to handle anyway for good mono repository support. + match project { + Ok(project) => { + self.projects.insert(root, project); + } + Err(err) => { + tracing::warn!("Failed to create project database for `{root}`: {err}",); + } + } + } + + assert!( + self.workspaces.all_initialized(), + "All workspaces should be initialized after calling `initialize_workspaces`" + ); + } + + pub(crate) fn take_deferred_messages(&mut self) -> Option { + if self.workspaces.all_initialized() { + self.deferred_messages.pop_front() + } else { + None + } + } + + /// Creates a document snapshot with the URL referencing the document to snapshot. + /// + /// Returns `None` if the url can't be converted to a document key or if the document isn't open. + pub(crate) fn take_document_snapshot(&self, url: Url) -> Option { + let key = self.key_from_url(url).ok()?; + Some(DocumentSnapshot { + resolved_client_capabilities: self.resolved_client_capabilities.clone(), + client_settings: self.index().global_settings(), + document_ref: self.index().make_document_ref(&key)?, + position_encoding: self.position_encoding, + }) + } + + /// Creates a snapshot of the current state of the [`Session`]. + pub(crate) fn take_session_snapshot(&self) -> SessionSnapshot { + SessionSnapshot { + projects: self.projects.values().cloned().collect(), + index: self.index.clone().unwrap(), + position_encoding: self.position_encoding, + } + } + + /// Iterates over the document keys for all open text documents. + pub(super) fn text_document_keys(&self) -> impl Iterator + '_ { + self.index() + .text_document_paths() + .map(|path| DocumentKey::Text(path.clone())) + } + + /// Registers a notebook document at the provided `path`. + /// If a document is already open here, it will be overwritten. + pub(crate) fn open_notebook_document( + &mut self, + path: &AnySystemPath, + document: NotebookDocument, + ) { + self.index_mut().open_notebook_document(path, document); + } + + /// Registers a text document at the provided `path`. + /// If a document is already open here, it will be overwritten. + pub(crate) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) { + self.index_mut().open_text_document(path, document); + } + + /// Updates a text document at the associated `key`. + /// + /// The document key must point to a text document, or this will throw an error. + pub(crate) fn update_text_document( + &mut self, + key: &DocumentKey, + content_changes: Vec, + new_version: DocumentVersion, + ) -> crate::Result<()> { + let position_encoding = self.position_encoding; + self.index_mut() + .update_text_document(key, content_changes, new_version, position_encoding) + } + + /// De-registers a document, specified by its key. + /// Calling this multiple times for the same document is a logic error. + pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { + self.index_mut().close_document(key)?; + Ok(()) + } + + /// Returns a reference to the index. + /// + /// # Panics + /// + /// Panics if there's a mutable reference to the index via [`index_mut`]. + /// + /// [`index_mut`]: Session::index_mut + fn index(&self) -> &index::Index { + self.index.as_ref().unwrap() + } + + /// Returns a mutable reference to the index. + /// + /// This method drops all references to the index and returns a guard that will restore the + /// references when dropped. This guard holds the only reference to the index and allows + /// modifying it. + fn index_mut(&mut self) -> MutIndexGuard { + let index = self.index.take().unwrap(); + + for db in self.projects_mut() { + // Remove the `index` from each database. This drops the count of `Arc` down to 1 + db.system_mut() + .as_any_mut() + .downcast_mut::() + .unwrap() + .take_index(); + } + + // There should now be exactly one reference to index which is self.index. + let index = Arc::into_inner(index).unwrap(); + + MutIndexGuard { + session: self, + index: Some(index), + } + } + + pub(crate) fn client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + + pub(crate) fn global_settings(&self) -> Arc { + self.index().global_settings() + } +} + +/// A guard that holds the only reference to the index and allows modifying it. +/// +/// When dropped, this guard restores all references to the index. +struct MutIndexGuard<'a> { + session: &'a mut Session, + index: Option, +} + +impl Deref for MutIndexGuard<'_> { + type Target = index::Index; + + fn deref(&self) -> &Self::Target { + self.index.as_ref().unwrap() + } +} + +impl DerefMut for MutIndexGuard<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.index.as_mut().unwrap() + } +} + +impl Drop for MutIndexGuard<'_> { + fn drop(&mut self) { + if let Some(index) = self.index.take() { + let index = Arc::new(index); + for db in self.session.projects_mut() { + db.system_mut() + .as_any_mut() + .downcast_mut::() + .unwrap() + .set_index(index.clone()); + } + + self.session.index = Some(index); + } + } +} + +/// An immutable snapshot of `Session` that references +/// a specific document. +#[derive(Debug)] +pub struct DocumentSnapshot { + resolved_client_capabilities: Arc, + client_settings: Arc, + document_ref: index::DocumentQuery, + position_encoding: PositionEncoding, +} + +impl DocumentSnapshot { + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + + pub(crate) fn query(&self) -> &index::DocumentQuery { + &self.document_ref + } + + pub(crate) fn encoding(&self) -> PositionEncoding { + self.position_encoding + } + + pub(crate) fn client_settings(&self) -> &ClientSettings { + &self.client_settings + } + + pub(crate) fn file(&self, db: &dyn Db) -> Option { + match AnySystemPath::try_from_url(self.document_ref.file_url()).ok()? { + AnySystemPath::System(path) => system_path_to_file(db, path).ok(), + AnySystemPath::SystemVirtual(virtual_path) => db + .files() + .try_virtual_file(&virtual_path) + .map(|virtual_file| virtual_file.file()), + } + } +} + +/// An immutable snapshot of the current state of [`Session`]. +pub(crate) struct SessionSnapshot { + projects: Vec, + index: Arc, + position_encoding: PositionEncoding, +} + +impl SessionSnapshot { + pub(crate) fn projects(&self) -> &[ProjectDatabase] { + &self.projects + } + + pub(crate) fn index(&self) -> &index::Index { + &self.index + } + + pub(crate) fn position_encoding(&self) -> PositionEncoding { + self.position_encoding + } +} + +#[derive(Debug, Default)] +pub(crate) struct Workspaces { + workspaces: BTreeMap, + uninitialized: usize, +} + +impl Workspaces { + pub(crate) fn register(&mut self, url: Url, options: ClientOptions) -> anyhow::Result<()> { + let path = url + .to_file_path() + .map_err(|()| anyhow!("Workspace URL is not a file or directory: {url:?}"))?; + + // Realistically I don't think this can fail because we got the path from a Url + let system_path = SystemPathBuf::from_path_buf(path) + .map_err(|_| anyhow!("Workspace URL is not valid UTF8"))?; + + self.workspaces.insert( + url, + Workspace { + options, + root: system_path, + }, + ); + + self.uninitialized += 1; + + Ok(()) + } + + pub(crate) fn initialize( + &mut self, + url: &Url, + options: ClientOptions, + ) -> Option<&mut Workspace> { + if let Some(workspace) = self.workspaces.get_mut(url) { + workspace.options = options; + self.uninitialized -= 1; + Some(workspace) + } else { + None + } + } + + pub(crate) fn urls(&self) -> impl Iterator + '_ { + self.workspaces.keys() + } + + pub(crate) fn all_initialized(&self) -> bool { + self.uninitialized == 0 + } +} + +impl<'a> IntoIterator for &'a Workspaces { + type Item = (&'a Url, &'a Workspace); + type IntoIter = std::collections::btree_map::Iter<'a, Url, Workspace>; + + fn into_iter(self) -> Self::IntoIter { + self.workspaces.iter() + } +} + +#[derive(Debug)] +pub(crate) struct Workspace { + root: SystemPathBuf, + options: ClientOptions, +} + +impl Workspace { + pub(crate) fn root(&self) -> &SystemPath { + &self.root + } +} diff --git a/crates/ty_server/src/session/capabilities.rs b/crates/ty_server/src/session/capabilities.rs new file mode 100644 index 0000000000000..e84212fe79c28 --- /dev/null +++ b/crates/ty_server/src/session/capabilities.rs @@ -0,0 +1,147 @@ +use lsp_types::{ClientCapabilities, MarkupKind}; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[expect(clippy::struct_excessive_bools)] +pub(crate) struct ResolvedClientCapabilities { + pub(crate) code_action_deferred_edit_resolution: bool, + pub(crate) apply_edit: bool, + pub(crate) document_changes: bool, + pub(crate) diagnostics_refresh: bool, + pub(crate) inlay_refresh: bool, + + /// Whether [pull diagnostics] is supported. + /// + /// [pull diagnostics]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics + pub(crate) pull_diagnostics: bool, + + /// Whether `textDocument.typeDefinition.linkSupport` is `true` + pub(crate) type_definition_link_support: bool, + + /// `true`, if the first markup kind in `textDocument.hover.contentFormat` is `Markdown` + pub(crate) hover_prefer_markdown: bool, + + /// Whether the client supports multiline semantic tokens + pub(crate) semantic_tokens_multiline_support: bool, + + /// Whether the client supports signature label offsets in signature help + pub(crate) signature_label_offset_support: bool, + + /// Whether the client supports per-signature active parameter in signature help + pub(crate) signature_active_parameter_support: bool, +} + +impl ResolvedClientCapabilities { + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { + let code_action_settings = client_capabilities + .text_document + .as_ref() + .and_then(|doc_settings| doc_settings.code_action.as_ref()); + let code_action_data_support = code_action_settings + .and_then(|code_action_settings| code_action_settings.data_support) + .unwrap_or_default(); + let code_action_edit_resolution = code_action_settings + .and_then(|code_action_settings| code_action_settings.resolve_support.as_ref()) + .is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into())); + + let apply_edit = client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.apply_edit) + .unwrap_or_default(); + + let document_changes = client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.workspace_edit.as_ref()?.document_changes) + .unwrap_or_default(); + + let declaration_link_support = client_capabilities + .text_document + .as_ref() + .and_then(|document| document.type_definition?.link_support) + .unwrap_or_default(); + + let diagnostics_refresh = client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.diagnostics.as_ref()?.refresh_support) + .unwrap_or_default(); + + let inlay_refresh = client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.inlay_hint.as_ref()?.refresh_support) + .unwrap_or_default(); + + let pull_diagnostics = client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.diagnostic.as_ref()) + .is_some(); + + let hover_prefer_markdown = client_capabilities + .text_document + .as_ref() + .and_then(|text_document| { + Some( + text_document + .hover + .as_ref()? + .content_format + .as_ref()? + .contains(&MarkupKind::Markdown), + ) + }) + .unwrap_or_default(); + + let semantic_tokens_multiline_support = client_capabilities + .text_document + .as_ref() + .and_then(|doc| doc.semantic_tokens.as_ref()) + .and_then(|semantic_tokens| semantic_tokens.multiline_token_support) + .unwrap_or(false); + + let signature_label_offset_support = client_capabilities + .text_document + .as_ref() + .and_then(|text_document| { + text_document + .signature_help + .as_ref()? + .signature_information + .as_ref()? + .parameter_information + .as_ref()? + .label_offset_support + }) + .unwrap_or_default(); + + let signature_active_parameter_support = client_capabilities + .text_document + .as_ref() + .and_then(|text_document| { + text_document + .signature_help + .as_ref()? + .signature_information + .as_ref()? + .active_parameter_support + }) + .unwrap_or_default(); + + Self { + code_action_deferred_edit_resolution: code_action_data_support + && code_action_edit_resolution, + apply_edit, + document_changes, + diagnostics_refresh, + inlay_refresh, + pull_diagnostics, + type_definition_link_support: declaration_link_support, + hover_prefer_markdown, + semantic_tokens_multiline_support, + signature_label_offset_support, + signature_active_parameter_support, + } + } +} diff --git a/crates/ty_server/src/session/client.rs b/crates/ty_server/src/session/client.rs new file mode 100644 index 0000000000000..eb8aa406b208b --- /dev/null +++ b/crates/ty_server/src/session/client.rs @@ -0,0 +1,252 @@ +use crate::Session; +use crate::server::{Action, ConnectionSender}; +use crate::server::{Event, MainLoopSender}; +use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError}; +use serde_json::Value; +use std::any::TypeId; +use std::fmt::Display; + +pub(crate) type ClientResponseHandler = Box; + +#[derive(Debug)] +pub(crate) struct Client { + /// Channel to send messages back to the main loop. + main_loop_sender: MainLoopSender, + /// Channel to send messages directly to the LSP client without going through the main loop. + /// + /// This is generally preferred because it reduces pressure on the main loop but it may not always be + /// possible if access to data on [`Session`] is required, which background tasks don't have. + client_sender: ConnectionSender, +} + +impl Client { + pub(crate) fn new(main_loop_sender: MainLoopSender, client_sender: ConnectionSender) -> Self { + Self { + main_loop_sender, + client_sender, + } + } + + /// Sends a request of kind `R` to the client, with associated parameters. + /// + /// The request is sent immediately. + /// The `response_handler` will be dispatched as soon as the client response + /// is processed on the main-loop. The handler always runs on the main-loop thread. + /// + /// # Note + /// This method takes a `session` so that we can register the pending-request + /// and send the response directly to the client. If this ever becomes too limiting (because we + /// need to send a request from somewhere where we don't have access to session), consider introducing + /// a new `send_deferred_request` method that doesn't take a session and instead sends + /// an `Action` to the main loop to send the request (the main loop has always access to session). + pub(crate) fn send_request( + &self, + session: &Session, + params: R::Params, + response_handler: impl FnOnce(&Client, R::Result) + Send + 'static, + ) where + R: lsp_types::request::Request, + { + let response_handler = Box::new(move |client: &Client, response: lsp_server::Response| { + let _span = + tracing::debug_span!("client_response", id=%response.id, method = R::METHOD) + .entered(); + + match (response.error, response.result) { + (Some(err), _) => { + tracing::error!( + "Got an error from the client (code {code}, method {method}): {message}", + code = err.code, + message = err.message, + method = R::METHOD + ); + } + (None, Some(response)) => match serde_json::from_value(response) { + Ok(response) => response_handler(client, response), + Err(error) => { + tracing::error!( + "Failed to deserialize client response (method={method}): {error}", + method = R::METHOD + ); + } + }, + (None, None) => { + if TypeId::of::() == TypeId::of::<()>() { + // We can't call `response_handler(())` directly here, but + // since we _know_ the type expected is `()`, we can use + // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, + // so this branch works in the general case but we'll only + // hit it if the concrete type is `()`, so the `unwrap()` is safe here. + response_handler(client, serde_json::from_value(Value::Null).unwrap()); + } else { + tracing::error!( + "Invalid client response: did not contain a result or error (method={method})", + method = R::METHOD + ); + } + } + } + }); + + let id = session + .request_queue() + .outgoing() + .register(response_handler); + + if let Err(err) = self + .client_sender + .send(Message::Request(lsp_server::Request { + id, + method: R::METHOD.to_string(), + params: serde_json::to_value(params).expect("Params to be serializable"), + })) + { + tracing::error!( + "Failed to send request `{}` because the client sender is closed: {err}", + R::METHOD + ); + } + } + + /// Sends a notification to the client. + pub(crate) fn send_notification(&self, params: N::Params) + where + N: lsp_types::notification::Notification, + { + let method = N::METHOD.to_string(); + + if let Err(err) = + self.client_sender + .send(lsp_server::Message::Notification(Notification::new( + method, params, + ))) + { + tracing::error!( + "Failed to send notification `{}` because the client sender is closed: {err}", + N::METHOD + ); + } + } + + /// Sends a notification without any parameters to the client. + /// + /// This is useful for notifications that don't require any data. + #[expect(dead_code)] + pub(crate) fn send_notification_no_params(&self, method: &str) { + if let Err(err) = + self.client_sender + .send(lsp_server::Message::Notification(Notification::new( + method.to_string(), + Value::Null, + ))) + { + tracing::error!( + "Failed to send notification `{method}` because the client sender is closed: {err}", + ); + } + } + + /// Sends a response to the client for a given request ID. + /// + /// The response isn't sent immediately. Instead, it's queued up in the main loop + /// and checked for cancellation (each request must have exactly one response). + pub(crate) fn respond(&self, id: &RequestId, result: crate::server::Result) + where + R: serde::Serialize, + { + let response = match result { + Ok(res) => lsp_server::Response::new_ok(id.clone(), res), + Err(crate::server::Error { code, error }) => { + lsp_server::Response::new_err(id.clone(), code as i32, error.to_string()) + } + }; + + self.main_loop_sender + .send(Event::Action(Action::SendResponse(response))) + .unwrap(); + } + + /// Sends an error response to the client for a given request ID. + /// + /// The response isn't sent immediately. Instead, it's queued up in the main loop. + pub(crate) fn respond_err(&self, id: RequestId, error: lsp_server::ResponseError) { + let response = lsp_server::Response { + id, + result: None, + error: Some(error), + }; + + self.main_loop_sender + .send(Event::Action(Action::SendResponse(response))) + .unwrap(); + } + + /// Shows a message to the user. + /// + /// This opens a pop up in VS Code showing `message`. + pub(crate) fn show_message(&self, message: impl Display, message_type: lsp_types::MessageType) { + self.send_notification::( + lsp_types::ShowMessageParams { + typ: message_type, + message: message.to_string(), + }, + ); + } + + /// Sends a request to display a warning to the client with a formatted message. The warning is + /// sent in a `window/showMessage` notification. + /// + /// Logs an error if the message could not be sent. + pub(crate) fn show_warning_message(&self, message: impl Display) { + self.show_message(message, lsp_types::MessageType::WARNING); + } + + /// Sends a request to display an error to the client with a formatted message. The error is + /// sent in a `window/showMessage` notification. + /// + /// Logs an error if the message could not be sent. + pub(crate) fn show_error_message(&self, message: impl Display) { + self.show_message(message, lsp_types::MessageType::ERROR); + } + + /// Re-queues this request after a salsa cancellation for a retry. + /// + /// The main loop will skip the retry if the client cancelled the request in the meantime. + pub(crate) fn retry(&self, request: lsp_server::Request) { + self.main_loop_sender + .send(Event::Action(Action::RetryRequest(request))) + .unwrap(); + } + + pub(crate) fn queue_action(&self, action: Action) { + self.main_loop_sender.send(Event::Action(action)).unwrap(); + } + + pub(crate) fn cancel(&self, session: &mut Session, id: RequestId) { + let method_name = session.request_queue_mut().incoming_mut().cancel(&id); + + if let Some(method_name) = method_name { + tracing::debug!("Cancelled request id={id} method={method_name}"); + let error = ResponseError { + code: ErrorCode::RequestCanceled as i32, + message: "request was cancelled by client".to_owned(), + data: None, + }; + + // Use `client_sender` here instead of `respond_err` because + // `respond_err` filters out responses for canceled requests (which we just did!). + if let Err(err) = self + .client_sender + .send(Message::Response(lsp_server::Response { + id, + result: None, + error: Some(error), + })) + { + tracing::error!( + "Failed to send cancellation response for request `{method_name}` because the client sender is closed: {err}", + ); + } + } + } +} diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs new file mode 100644 index 0000000000000..16c430ddfe42b --- /dev/null +++ b/crates/ty_server/src/session/index.rs @@ -0,0 +1,310 @@ +use std::sync::Arc; + +use lsp_types::Url; +use rustc_hash::FxHashMap; + +use crate::session::settings::ClientSettings; +use crate::{ + PositionEncoding, TextDocument, + document::{DocumentKey, DocumentVersion, NotebookDocument}, + system::AnySystemPath, +}; + +/// Stores and tracks all open documents in a session, along with their associated settings. +#[derive(Debug)] +pub(crate) struct Index { + /// Maps all document file paths to the associated document controller + documents: FxHashMap, + + /// Maps opaque cell URLs to a notebook path (document) + notebook_cells: FxHashMap, + + /// Global settings provided by the client. + global_settings: Arc, +} + +impl Index { + pub(super) fn new(global_settings: ClientSettings) -> Self { + Self { + documents: FxHashMap::default(), + notebook_cells: FxHashMap::default(), + global_settings: Arc::new(global_settings), + } + } + + pub(super) fn text_document_paths(&self) -> impl Iterator + '_ { + self.documents + .iter() + .filter_map(|(path, doc)| doc.as_text().and(Some(path))) + } + + #[expect(dead_code)] + pub(super) fn notebook_document_paths(&self) -> impl Iterator + '_ { + self.documents + .iter() + .filter(|(_, doc)| doc.as_notebook().is_some()) + .map(|(path, _)| path) + } + + pub(super) fn update_text_document( + &mut self, + key: &DocumentKey, + content_changes: Vec, + new_version: DocumentVersion, + encoding: PositionEncoding, + ) -> crate::Result<()> { + let controller = self.document_controller_for_key(key)?; + let Some(document) = controller.as_text_mut() else { + anyhow::bail!("Text document path does not point to a text document"); + }; + + if content_changes.is_empty() { + document.update_version(new_version); + return Ok(()); + } + + document.apply_changes(content_changes, new_version, encoding); + + Ok(()) + } + + pub(crate) fn key_from_url(&self, url: Url) -> crate::Result { + if let Some(notebook_path) = self.notebook_cells.get(&url) { + Ok(DocumentKey::NotebookCell { + cell_url: url, + notebook_path: notebook_path.clone(), + }) + } else { + let path = AnySystemPath::try_from_url(&url) + .map_err(|()| anyhow::anyhow!("Failed to convert URL to system path: {}", url))?; + + if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb")) + { + Ok(DocumentKey::Notebook(path)) + } else { + Ok(DocumentKey::Text(path)) + } + } + } + + #[expect(dead_code)] + pub(super) fn update_notebook_document( + &mut self, + key: &DocumentKey, + cells: Option, + metadata: Option>, + new_version: DocumentVersion, + encoding: PositionEncoding, + ) -> crate::Result<()> { + // update notebook cell index + if let Some(lsp_types::NotebookDocumentCellChangeStructure { + did_open: Some(did_open), + .. + }) = cells.as_ref().and_then(|cells| cells.structure.as_ref()) + { + let notebook_path = key.path().clone(); + + for opened_cell in did_open { + self.notebook_cells + .insert(opened_cell.uri.clone(), notebook_path.clone()); + } + // deleted notebook cells are closed via textDocument/didClose - we don't close them here. + } + + let controller = self.document_controller_for_key(key)?; + let Some(notebook) = controller.as_notebook_mut() else { + anyhow::bail!("Notebook document path does not point to a notebook document"); + }; + + notebook.update(cells, metadata, new_version, encoding)?; + Ok(()) + } + + pub(crate) fn make_document_ref(&self, key: &DocumentKey) -> Option { + let path = key.path(); + let controller = self.documents.get(path)?; + let (cell_url, file_url) = match &key { + DocumentKey::NotebookCell { + cell_url, + notebook_path, + } => (Some(cell_url.clone()), notebook_path.to_url()?), + DocumentKey::Notebook(path) | DocumentKey::Text(path) => (None, path.to_url()?), + }; + Some(controller.make_ref(cell_url, file_url)) + } + + pub(super) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) { + self.documents + .insert(path.clone(), DocumentController::new_text(document)); + } + + pub(super) fn open_notebook_document( + &mut self, + notebook_path: &AnySystemPath, + document: NotebookDocument, + ) { + for cell_url in document.cell_urls() { + self.notebook_cells + .insert(cell_url.clone(), notebook_path.clone()); + } + self.documents.insert( + notebook_path.clone(), + DocumentController::new_notebook(document), + ); + } + + pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { + // Notebook cells URIs are removed from the index here, instead of during + // `update_notebook_document`. This is because a notebook cell, as a text document, + // is requested to be `closed` by VS Code after the notebook gets updated. + // This is not documented in the LSP specification explicitly, and this assumption + // may need revisiting in the future as we support more editors with notebook support. + if let DocumentKey::NotebookCell { cell_url, .. } = key { + if self.notebook_cells.remove(cell_url).is_none() { + tracing::warn!("Tried to remove a notebook cell that does not exist: {cell_url}",); + } + return Ok(()); + } + let path = key.path(); + + let Some(_) = self.documents.remove(path) else { + anyhow::bail!("tried to close document that didn't exist at {}", key) + }; + Ok(()) + } + + pub(crate) fn global_settings(&self) -> Arc { + self.global_settings.clone() + } + + fn document_controller_for_key( + &mut self, + key: &DocumentKey, + ) -> crate::Result<&mut DocumentController> { + let path = key.path(); + let Some(controller) = self.documents.get_mut(path) else { + anyhow::bail!("Document controller not available at `{}`", key); + }; + Ok(controller) + } +} + +/// A mutable handler to an underlying document. +#[derive(Debug)] +enum DocumentController { + Text(Arc), + Notebook(Arc), +} + +impl DocumentController { + fn new_text(document: TextDocument) -> Self { + Self::Text(Arc::new(document)) + } + + fn new_notebook(document: NotebookDocument) -> Self { + Self::Notebook(Arc::new(document)) + } + + fn make_ref(&self, cell_url: Option, file_url: Url) -> DocumentQuery { + match &self { + Self::Notebook(notebook) => DocumentQuery::Notebook { + cell_url, + file_url, + notebook: notebook.clone(), + }, + Self::Text(document) => DocumentQuery::Text { + file_url, + document: document.clone(), + }, + } + } + + pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { + Some(match self { + Self::Notebook(notebook) => Arc::make_mut(notebook), + Self::Text(_) => return None, + }) + } + + pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> { + match self { + Self::Notebook(notebook) => Some(notebook), + Self::Text(_) => None, + } + } + + pub(crate) fn as_text(&self) -> Option<&TextDocument> { + match self { + Self::Text(document) => Some(document), + Self::Notebook(_) => None, + } + } + + pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> { + Some(match self { + Self::Text(document) => Arc::make_mut(document), + Self::Notebook(_) => return None, + }) + } +} + +/// A read-only query to an open document. +/// This query can 'select' a text document, full notebook, or a specific notebook cell. +/// It also includes document settings. +#[derive(Debug, Clone)] +pub enum DocumentQuery { + Text { + file_url: Url, + document: Arc, + }, + Notebook { + /// The selected notebook cell, if it exists. + cell_url: Option, + /// The URL of the notebook. + file_url: Url, + notebook: Arc, + }, +} + +impl DocumentQuery { + /// Attempts to access the underlying notebook document that this query is selecting. + pub fn as_notebook(&self) -> Option<&NotebookDocument> { + match self { + Self::Notebook { notebook, .. } => Some(notebook), + Self::Text { .. } => None, + } + } + + /// Get the version of document selected by this query. + pub(crate) fn version(&self) -> DocumentVersion { + match self { + Self::Text { document, .. } => document.version(), + Self::Notebook { notebook, .. } => notebook.version(), + } + } + + /// Get the URL for the document selected by this query. + pub(crate) fn file_url(&self) -> &Url { + match self { + Self::Text { file_url, .. } | Self::Notebook { file_url, .. } => file_url, + } + } + + /// Attempt to access the single inner text document selected by the query. + /// If this query is selecting an entire notebook document, this will return `None`. + #[expect(dead_code)] + pub(crate) fn as_single_document(&self) -> Option<&TextDocument> { + match self { + Self::Text { document, .. } => Some(document), + Self::Notebook { + notebook, + cell_url: cell_uri, + .. + } => cell_uri + .as_ref() + .and_then(|cell_uri| notebook.cell_document_by_uri(cell_uri)), + } + } +} diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs new file mode 100644 index 0000000000000..e73f49a11bb60 --- /dev/null +++ b/crates/ty_server/src/session/options.rs @@ -0,0 +1,188 @@ +use lsp_types::Url; +use ruff_db::system::SystemPathBuf; +use rustc_hash::FxHashMap; +use serde::Deserialize; + +use crate::logging::LogLevel; +use crate::session::settings::ClientSettings; + +pub(crate) type WorkspaceOptionsMap = FxHashMap; + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalOptions { + #[serde(flatten)] + client: ClientOptions, + + // These settings are only needed for tracing, and are only read from the global configuration. + // These will not be in the resolved settings. + #[serde(flatten)] + pub(crate) tracing: TracingOptions, +} + +impl GlobalOptions { + pub(crate) fn into_settings(self) -> ClientSettings { + self.client.into_settings() + } + + pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode { + self.client.diagnostic_mode.unwrap_or_default() + } +} + +/// This is a direct representation of the workspace settings schema, which inherits the schema of +/// [`ClientOptions`] and adds extra fields to describe the workspace it applies to. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct WorkspaceOptions { + #[serde(flatten)] + options: ClientOptions, + workspace: Url, +} + +/// This is a direct representation of the settings schema sent by the client. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClientOptions { + /// Settings under the `python.*` namespace in VS Code that are useful for the ty language + /// server. + python: Option, + /// Diagnostic mode for the language server. + diagnostic_mode: Option, +} + +/// Diagnostic mode for the language server. +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) enum DiagnosticMode { + /// Check only currently open files. + #[default] + OpenFilesOnly, + /// Check all files in the workspace. + Workspace, +} + +impl DiagnosticMode { + pub(crate) fn is_workspace(self) -> bool { + matches!(self, DiagnosticMode::Workspace) + } +} + +impl ClientOptions { + /// Returns the client settings that are relevant to the language server. + pub(crate) fn into_settings(self) -> ClientSettings { + ClientSettings { + disable_language_services: self + .python + .and_then(|python| python.ty) + .and_then(|ty| ty.disable_language_services) + .unwrap_or_default(), + diagnostic_mode: self.diagnostic_mode.unwrap_or_default(), + } + } +} + +// TODO(dhruvmanila): We need to mirror the "python.*" namespace on the server side but ideally it +// would be useful to instead use `workspace/configuration` instead. This would be then used to get +// all settings and not just the ones in "python.*". + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct Python { + ty: Option, +} + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct Ty { + disable_language_services: Option, +} + +/// This is a direct representation of the settings schema sent by the client. +/// Settings needed to initialize tracing. These will only be read from the global configuration. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct TracingOptions { + pub(crate) log_level: Option, + + /// Path to the log file - tildes and environment variables are supported. + pub(crate) log_file: Option, +} + +/// This is the exact schema for initialization options sent in by the client during +/// initialization. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(untagged)] +enum InitializationOptions { + #[serde(rename_all = "camelCase")] + HasWorkspaces { + #[serde(rename = "globalSettings")] + global: GlobalOptions, + #[serde(rename = "settings")] + workspace: Vec, + }, + GlobalOnly { + #[serde(default)] + settings: GlobalOptions, + }, +} + +impl Default for InitializationOptions { + fn default() -> Self { + Self::GlobalOnly { + settings: GlobalOptions::default(), + } + } +} + +/// Built from the initialization options provided by the client. +#[derive(Debug)] +pub(crate) struct AllOptions { + pub(crate) global: GlobalOptions, + /// If this is `None`, the client only passed in global settings. + pub(crate) workspace: Option, +} + +impl AllOptions { + /// Initializes the controller from the serialized initialization options. This fails if + /// `options` are not valid initialization options. + pub(crate) fn from_value(options: serde_json::Value) -> Self { + Self::from_init_options( + serde_json::from_value(options) + .map_err(|err| { + tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); + }) + .unwrap_or_default(), + ) + } + + fn from_init_options(options: InitializationOptions) -> Self { + let (global_options, workspace_options) = match options { + InitializationOptions::GlobalOnly { settings: options } => (options, None), + InitializationOptions::HasWorkspaces { + global: global_options, + workspace: workspace_options, + } => (global_options, Some(workspace_options)), + }; + + Self { + global: global_options, + workspace: workspace_options.map(|workspace_options| { + workspace_options + .into_iter() + .map(|workspace_options| { + (workspace_options.workspace, workspace_options.options) + }) + .collect() + }), + } + } +} diff --git a/crates/ty_server/src/session/request_queue.rs b/crates/ty_server/src/session/request_queue.rs new file mode 100644 index 0000000000000..60c601ae92b15 --- /dev/null +++ b/crates/ty_server/src/session/request_queue.rs @@ -0,0 +1,197 @@ +use crate::session::client::ClientResponseHandler; +use lsp_server::RequestId; +use rustc_hash::FxHashMap; +use std::cell::{Cell, OnceCell, RefCell}; +use std::fmt::Formatter; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::time::Instant; + +/// Tracks the pending requests between client and server. +pub(crate) struct RequestQueue { + incoming: Incoming, + outgoing: Outgoing, +} + +impl RequestQueue { + pub(super) fn new() -> Self { + Self { + incoming: Incoming::default(), + outgoing: Outgoing::default(), + } + } + + pub(crate) fn outgoing_mut(&mut self) -> &mut Outgoing { + &mut self.outgoing + } + + /// Returns the server to client request queue. + pub(crate) fn outgoing(&self) -> &Outgoing { + &self.outgoing + } + + /// Returns the client to server request queue. + pub(crate) fn incoming(&self) -> &Incoming { + &self.incoming + } + + pub(crate) fn incoming_mut(&mut self) -> &mut Incoming { + &mut self.incoming + } +} + +/// Requests from client -> server. +/// +/// Tracks which requests are pending. Requests that aren't registered are considered completed. +/// +/// A request is pending if: +/// +/// * it has been registered +/// * it hasn't been cancelled +/// * it hasn't been completed +/// +/// Tracking whether a request is pending is required to ensure that the server sends exactly +/// one response for every request as required by the LSP specification. +#[derive(Default, Debug)] +pub(crate) struct Incoming { + pending: FxHashMap, +} + +impl Incoming { + /// Registers a new pending request. + pub(crate) fn register(&mut self, request_id: RequestId, method: String) { + self.pending.insert(request_id, PendingRequest::new(method)); + } + + /// Cancels the pending request with the given id. + /// + /// Returns the method name if the request was still pending, `None` if it was already completed. + pub(super) fn cancel(&mut self, request_id: &RequestId) -> Option { + self.pending.remove(request_id).map(|mut pending| { + if let Some(cancellation_token) = pending.cancellation_token.take() { + cancellation_token.cancel(); + } + pending.method + }) + } + + /// Returns `true` if the request with the given id is still pending. + pub(crate) fn is_pending(&self, request_id: &RequestId) -> bool { + self.pending.contains_key(request_id) + } + + /// Returns the cancellation token for the given request id if the request is still pending. + pub(crate) fn cancellation_token( + &self, + request_id: &RequestId, + ) -> Option { + let pending = self.pending.get(request_id)?; + + Some(RequestCancellationToken::clone( + pending + .cancellation_token + .get_or_init(RequestCancellationToken::default), + )) + } + + /// Marks the request as completed. + /// + /// Returns the time when the request was registered and the request method name, or `None` if the request was not pending. + pub(crate) fn complete(&mut self, request_id: &RequestId) -> Option<(Instant, String)> { + self.pending + .remove(request_id) + .map(|pending| (pending.start_time, pending.method)) + } +} + +/// A request from the client to the server that hasn't been responded yet. +#[derive(Debug)] +struct PendingRequest { + /// The time when the request was registered. + /// + /// This does not include the time the request was queued in the main loop before it was registered. + start_time: Instant, + + /// The method name of the request. + method: String, + + /// A cancellation token to cancel this request. + /// + /// This is only initialized for background requests. Local tasks don't support cancellation (unless retried) + /// as they're processed immediately after receiving the request; Making it impossible for a + /// cancellation message to be processed before the task is completed. + cancellation_token: OnceCell, +} + +impl PendingRequest { + fn new(method: String) -> Self { + Self { + start_time: Instant::now(), + method, + cancellation_token: OnceCell::new(), + } + } +} + +/// Token to cancel a specific request. +/// +/// Can be shared between threads to check for cancellation *after* a request has been scheduled. +#[derive(Debug, Default)] +pub(crate) struct RequestCancellationToken(Arc); + +impl RequestCancellationToken { + /// Returns true if the request was cancelled. + pub(crate) fn is_cancelled(&self) -> bool { + self.0.load(std::sync::atomic::Ordering::Relaxed) + } + + /// Signals that the request should not be processed because it was cancelled. + fn cancel(&self) { + self.0.store(true, std::sync::atomic::Ordering::Relaxed); + } + + fn clone(this: &Self) -> Self { + RequestCancellationToken(this.0.clone()) + } +} + +/// Requests from server -> client. +#[derive(Default)] +pub(crate) struct Outgoing { + /// The id of the next request sent from the server to the client. + next_request_id: Cell, + + /// A map of request ids to the handlers that process the client-response. + response_handlers: RefCell>, +} + +impl Outgoing { + /// Registers a handler, returns the id for the request. + #[must_use] + pub(crate) fn register(&self, handler: ClientResponseHandler) -> RequestId { + let id = self.next_request_id.get(); + self.next_request_id.set(id + 1); + + self.response_handlers + .borrow_mut() + .insert(id.into(), handler); + id.into() + } + + /// Marks the request with the given id as complete and returns the handler to process the response. + /// + /// Returns `None` if the request was not found. + #[must_use] + pub(crate) fn complete(&mut self, request_id: &RequestId) -> Option { + self.response_handlers.get_mut().remove(request_id) + } +} + +impl std::fmt::Debug for Outgoing { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Outgoing") + .field("next_request_id", &self.next_request_id) + .field("response_handlers", &"") + .finish() + } +} diff --git a/crates/ty_server/src/session/settings.rs b/crates/ty_server/src/session/settings.rs new file mode 100644 index 0000000000000..f24cd923509b9 --- /dev/null +++ b/crates/ty_server/src/session/settings.rs @@ -0,0 +1,21 @@ +use super::options::DiagnosticMode; + +/// Resolved client settings for a specific document. These settings are meant to be +/// used directly by the server, and are *not* a 1:1 representation with how the client +/// sends them. +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub(crate) struct ClientSettings { + pub(super) disable_language_services: bool, + pub(super) diagnostic_mode: DiagnosticMode, +} + +impl ClientSettings { + pub(crate) fn is_language_services_disabled(&self) -> bool { + self.disable_language_services + } + + pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode { + self.diagnostic_mode + } +} diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs new file mode 100644 index 0000000000000..433e376847ccd --- /dev/null +++ b/crates/ty_server/src/system.rs @@ -0,0 +1,309 @@ +use std::any::Any; +use std::fmt::Display; +use std::sync::Arc; + +use lsp_types::Url; +use ruff_db::file_revision::FileRevision; +use ruff_db::files::{File, FilePath}; +use ruff_db::system::walk_directory::WalkDirectoryBuilder; +use ruff_db::system::{ + CaseSensitivity, DirectoryEntry, FileType, GlobError, Metadata, OsSystem, PatternError, Result, + System, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WritableSystem, +}; +use ruff_notebook::{Notebook, NotebookError}; +use ty_python_semantic::Db; + +use crate::DocumentQuery; +use crate::document::DocumentKey; +use crate::session::index::Index; + +/// Returns a [`Url`] for the given [`File`]. +pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option { + match file.path(db) { + FilePath::System(system) => Url::from_file_path(system.as_std_path()).ok(), + FilePath::SystemVirtual(path) => Url::parse(path.as_str()).ok(), + FilePath::Vendored(path) => { + let writable = db.system().as_writable()?; + + let system_path = SystemPathBuf::from(format!( + "vendored/typeshed/{}/{}", + // The vendored files are uniquely identified by the source commit. + ty_vendored::SOURCE_COMMIT, + path.as_str() + )); + + // Extract the vendored file onto the system. + let system_path = writable + .get_or_cache(&system_path, &|| db.vendored().read_to_string(path)) + .ok() + .flatten()?; + + Url::from_file_path(system_path.as_std_path()).ok() + } + } +} + +/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`]. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) enum AnySystemPath { + System(SystemPathBuf), + SystemVirtual(SystemVirtualPathBuf), +} + +impl AnySystemPath { + /// Converts the given [`Url`] to an [`AnySystemPath`]. + /// + /// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the + /// URL is converted to a [`SystemVirtualPathBuf`]. + /// + /// This fails in the following cases: + /// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]). + /// * If the URL is not a valid UTF-8 string. + pub(crate) fn try_from_url(url: &Url) -> std::result::Result { + if url.scheme() == "file" { + Ok(AnySystemPath::System( + SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?, + )) + } else { + Ok(AnySystemPath::SystemVirtual( + SystemVirtualPath::new(url.as_str()).to_path_buf(), + )) + } + } + + pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> { + match self { + AnySystemPath::System(system_path_buf) => Some(system_path_buf), + AnySystemPath::SystemVirtual(_) => None, + } + } + + /// Returns the extension of the path, if any. + pub(crate) fn extension(&self) -> Option<&str> { + match self { + AnySystemPath::System(system_path) => system_path.extension(), + AnySystemPath::SystemVirtual(virtual_path) => virtual_path.extension(), + } + } + + /// Converts the path to a URL. + pub(crate) fn to_url(&self) -> Option { + match self { + AnySystemPath::System(system_path) => { + Url::from_file_path(system_path.as_std_path()).ok() + } + AnySystemPath::SystemVirtual(virtual_path) => Url::parse(virtual_path.as_str()).ok(), + } + } +} + +#[derive(Debug)] +pub(crate) struct LSPSystem { + /// A read-only copy of the index where the server stores all the open documents and settings. + /// + /// This will be [`None`] when a mutable reference is held to the index via [`index_mut`] + /// method to prevent the index from being accessed while it is being modified. It will be + /// restored when the mutable reference is dropped. + /// + /// [`index_mut`]: crate::Session::index_mut + index: Option>, + + /// A system implementation that uses the local file system. + os_system: OsSystem, +} + +impl LSPSystem { + pub(crate) fn new(index: Arc) -> Self { + let cwd = std::env::current_dir().unwrap(); + let os_system = OsSystem::new(SystemPathBuf::from_path_buf(cwd).unwrap()); + + Self { + index: Some(index), + os_system, + } + } + + /// Takes the index out of the system. + pub(crate) fn take_index(&mut self) -> Option> { + self.index.take() + } + + /// Sets the index for the system. + pub(crate) fn set_index(&mut self, index: Arc) { + self.index = Some(index); + } + + /// Returns a reference to the contained index. + /// + /// # Panics + /// + /// Panics if the index is `None`. + fn index(&self) -> &Index { + self.index.as_ref().unwrap() + } + + fn make_document_ref(&self, path: AnySystemPath) -> Option { + let index = self.index(); + let key = DocumentKey::from_path(path); + index.make_document_ref(&key) + } + + fn system_path_to_document_ref(&self, path: &SystemPath) -> Option { + let any_path = AnySystemPath::System(path.to_path_buf()); + self.make_document_ref(any_path) + } + + fn system_virtual_path_to_document_ref( + &self, + path: &SystemVirtualPath, + ) -> Option { + let any_path = AnySystemPath::SystemVirtual(path.to_path_buf()); + self.make_document_ref(any_path) + } +} + +impl System for LSPSystem { + fn path_metadata(&self, path: &SystemPath) -> Result { + let document = self.system_path_to_document_ref(path); + + if let Some(document) = document { + Ok(Metadata::new( + document_revision(&document), + None, + FileType::File, + )) + } else { + self.os_system.path_metadata(path) + } + } + + fn canonicalize_path(&self, path: &SystemPath) -> Result { + self.os_system.canonicalize_path(path) + } + + fn path_exists_case_sensitive(&self, path: &SystemPath, prefix: &SystemPath) -> bool { + self.os_system.path_exists_case_sensitive(path, prefix) + } + + fn read_to_string(&self, path: &SystemPath) -> Result { + let document = self.system_path_to_document_ref(path); + + match document { + Some(DocumentQuery::Text { document, .. }) => Ok(document.contents().to_string()), + _ => self.os_system.read_to_string(path), + } + } + + fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result { + let document = self.system_path_to_document_ref(path); + + match document { + Some(DocumentQuery::Text { document, .. }) => { + Notebook::from_source_code(document.contents()) + } + Some(DocumentQuery::Notebook { notebook, .. }) => Ok(notebook.make_ruff_notebook()), + None => self.os_system.read_to_notebook(path), + } + } + + fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result { + let document = self + .system_virtual_path_to_document_ref(path) + .ok_or_else(|| virtual_path_not_found(path))?; + + if let DocumentQuery::Text { document, .. } = &document { + Ok(document.contents().to_string()) + } else { + Err(not_a_text_document(path)) + } + } + + fn read_virtual_path_to_notebook( + &self, + path: &SystemVirtualPath, + ) -> std::result::Result { + let document = self + .system_virtual_path_to_document_ref(path) + .ok_or_else(|| virtual_path_not_found(path))?; + + match document { + DocumentQuery::Text { document, .. } => Notebook::from_source_code(document.contents()), + DocumentQuery::Notebook { notebook, .. } => Ok(notebook.make_ruff_notebook()), + } + } + + fn current_directory(&self) -> &SystemPath { + self.os_system.current_directory() + } + + fn user_config_directory(&self) -> Option { + self.os_system.user_config_directory() + } + + fn cache_dir(&self) -> Option { + self.os_system.cache_dir() + } + + fn read_directory<'a>( + &'a self, + path: &SystemPath, + ) -> Result> + 'a>> { + self.os_system.read_directory(path) + } + + fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder { + self.os_system.walk_directory(path) + } + + fn glob( + &self, + pattern: &str, + ) -> std::result::Result< + Box> + '_>, + PatternError, + > { + self.os_system.glob(pattern) + } + + fn as_writable(&self) -> Option<&dyn WritableSystem> { + self.os_system.as_writable() + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn case_sensitivity(&self) -> CaseSensitivity { + self.os_system.case_sensitivity() + } + + fn env_var(&self, name: &str) -> std::result::Result { + self.os_system.env_var(name) + } +} + +fn not_a_text_document(path: impl Display) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Input is not a text document: {path}"), + ) +} + +fn virtual_path_not_found(path: impl Display) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Virtual path does not exist: {path}"), + ) +} + +/// Helper function to get the [`FileRevision`] of the given document. +fn document_revision(document: &DocumentQuery) -> FileRevision { + // The file revision is just an opaque number which doesn't have any significant meaning other + // than that the file has changed if the revisions are different. + #[expect(clippy::cast_sign_loss)] + FileRevision::new(document.version() as u128) +} diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml new file mode 100644 index 0000000000000..178df9282fbdb --- /dev/null +++ b/crates/ty_static/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ty_static" +version = "0.0.1" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +ruff_macros = { workspace = true } diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs new file mode 100644 index 0000000000000..486a01a187dd0 --- /dev/null +++ b/crates/ty_static/src/env_vars.rs @@ -0,0 +1,71 @@ +use ruff_macros::attribute_env_vars_metadata; + +/// Declares all environment variable used throughout `ty` and its crates. +pub struct EnvVars; + +#[attribute_env_vars_metadata] +impl EnvVars { + /// If set, ty will use this value as the log level for its `--verbose` output. + /// Accepts any filter compatible with the `tracing_subscriber` crate. + /// + /// For example: + /// + /// - `TY_LOG=uv=debug` is the equivalent of `-vv` to the command line + /// - `TY_LOG=trace` will enable all trace-level logging. + /// + /// See the [tracing documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax) + /// for more. + pub const TY_LOG: &'static str = "TY_LOG"; + + /// If set to `"1"` or `"true"`, ty will enable flamegraph profiling. + /// This creates a `tracing.folded` file that can be used to generate flame graphs + /// for performance analysis. + pub const TY_LOG_PROFILE: &'static str = "TY_LOG_PROFILE"; + + /// Control memory usage reporting format after ty execution. + /// + /// Accepted values: + /// + /// * `short` - Display short memory report + /// * `mypy_primer` - Display mypy_primer format and suppress workspace diagnostics + /// * `full` - Display full memory report + #[attr_hidden] + pub const TY_MEMORY_REPORT: &'static str = "TY_MEMORY_REPORT"; + + /// Specifies an upper limit for the number of tasks ty is allowed to run in parallel. + /// + /// For example, how many files should be checked in parallel. + /// This isn't the same as a thread limit. ty may spawn additional threads + /// when necessary, e.g. to watch for file system changes or a dedicated UI thread. + pub const TY_MAX_PARALLELISM: &'static str = "TY_MAX_PARALLELISM"; + + /// Used to detect an activated virtual environment. + pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; + + /// Used to detect an activated Conda environment location. + /// If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred. + pub const CONDA_PREFIX: &'static str = "CONDA_PREFIX"; + + /// Filter which tests to run in mdtest. + /// + /// Only tests whose names contain this filter string will be executed. + #[attr_hidden] + pub const MDTEST_TEST_FILTER: &'static str = "MDTEST_TEST_FILTER"; + + /// Switch mdtest output format to GitHub Actions annotations. + /// + /// If set (to any value), mdtest will output errors in GitHub Actions format. + #[attr_hidden] + pub const MDTEST_GITHUB_ANNOTATIONS_FORMAT: &'static str = "MDTEST_GITHUB_ANNOTATIONS_FORMAT"; + + // Externally defined environment variables + + /// Specifies an upper limit for the number of threads ty uses when performing work in parallel. + /// Equivalent to `TY_MAX_PARALLELISM`. + /// + /// This is a standard Rayon environment variable. + pub const RAYON_NUM_THREADS: &'static str = "RAYON_NUM_THREADS"; + + /// Path to user-level configuration directory on Unix systems. + pub const XDG_CONFIG_HOME: &'static str = "XDG_CONFIG_HOME"; +} diff --git a/crates/ty_static/src/lib.rs b/crates/ty_static/src/lib.rs new file mode 100644 index 0000000000000..153591db70828 --- /dev/null +++ b/crates/ty_static/src/lib.rs @@ -0,0 +1,3 @@ +pub use env_vars::*; + +mod env_vars; diff --git a/crates/ty_test/Cargo.toml b/crates/ty_test/Cargo.toml new file mode 100644 index 0000000000000..97bd4bea2cef5 --- /dev/null +++ b/crates/ty_test/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "ty_test" +version = "0.0.0" +publish = false +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +ruff_db = { workspace = true, features = ["os", "testing"] } +ruff_index = { workspace = true } +ruff_notebook = { workspace = true } +ruff_python_trivia = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } +ruff_python_ast = { workspace = true } +ty_python_semantic = { workspace = true, features = ["serde", "testing"] } +ty_static = { workspace = true } +ty_vendored = { workspace = true } + +anyhow = { workspace = true } +bitflags = { workspace = true } +camino = { workspace = true } +colored = { workspace = true } +insta = { workspace = true, features = ["filters"] } +memchr = { workspace = true } +regex = { workspace = true } +rustc-hash = { workspace = true } +rustc-stable-hash = { workspace = true } +salsa = { workspace = true } +smallvec = { workspace = true } +serde = { workspace = true } +tempfile = { workspace = true } +toml = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ty_test/README.md b/crates/ty_test/README.md new file mode 100644 index 0000000000000..3db1832231c9e --- /dev/null +++ b/crates/ty_test/README.md @@ -0,0 +1,538 @@ +# Writing type-checking / type-inference tests + +Any Markdown file can be a test suite. + +In order for it to be run as one, `ty_test::run` must be called with its path; see +`crates/ty_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files +under a certain directory as test suites. + +A Markdown test suite can contain any number of tests. A test consists of one or more embedded +"files", each defined by a triple-backticks fenced code block. The code block must have a tag string +specifying its language. We currently support `py` (Python files) and `pyi` (type stub files), as +well as [typeshed `VERSIONS`] files and `toml` for configuration. + +The simplest possible test suite consists of just a single test, with a single embedded file: + +````markdown +```py +reveal_type(1) # revealed: Literal[1] +``` +```` + +When running this test, the mdtest framework will write a file with these contents to the default +file path (`/src/mdtest_snippet.py`) in its in-memory file system, run a type check on that file, +and then match the resulting diagnostics with the assertions in the test. Assertions are in the form +of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, +it fails. + + + +See actual example mdtest suites in +[`crates/ty_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/ty_python_semantic/resources/mdtest). + +> [!NOTE] +> If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory, +> as with the example in `crates/ty_python_semantic/tests/mdtest.rs`, +> you will likely want to also make sure that the crate the tests are in is rebuilt every time a +> Markdown file is added or removed from the directory. See +> [`crates/ty_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/ty_python_semantic/build.rs) +> for an example of how to do this. +> +> This is because these macros generate their tests at build time rather than at runtime. +> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed, +> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild +> following the addition of the new test file. + +## Assertions + +Two kinds of assertions are supported: `# revealed:` (shown above) and `# error:`. + +### Assertion kinds + +#### revealed + +A `# revealed:` assertion should always be paired with a call to the `reveal_type` utility, which +reveals (via a diagnostic) the inferred type of its argument (which can be any expression). The text +after `# revealed:` must match exactly with the displayed form of the revealed type of that +expression. + +The `reveal_type` function can be imported from the `typing` standard library module (or, for older +Python versions, from the `typing_extensions` pseudo-standard-library module[^extensions]): + +```py +from typing import reveal_type + +reveal_type("foo") # revealed: Literal["foo"] +``` + +For convenience, type checkers also pretend that `reveal_type` is a built-in, so that this import is +not required. Using `reveal_type` without importing it issues a diagnostic warning that it was used +without importing it, in addition to the diagnostic revealing the type of the expression. + +The `# revealed:` assertion must always match a revealed-type diagnostic, and will also match the +undefined-reveal diagnostic, if present, so it's safe to use `reveal_type` in tests either with or +without importing it. (Style preference is to not import it in tests, unless specifically testing +something about the behavior of importing it.) + +#### error + +A comment beginning with `# error:` is an assertion that a type checker diagnostic will be emitted, +with text span starting on that line. The matching can be narrowed in three ways: + +- `# error: [invalid-assignment]` requires that the matched diagnostic have the rule code + `invalid-assignment`. (The square brackets are required.) +- `# error: "Some text"` requires that the diagnostic's full message contain the text `Some text`. + (The double quotes are required in the assertion comment; they are not part of the matched text.) +- `# error: 8 [rule-code]` or `# error: 8 "Some text"` additionally requires that the matched + diagnostic's text span begins on column 8 (one-indexed) of this line. + +Assertions must contain either a rule code or a contains-text, or both, and may optionally also +include a column number. They must come in order: first column, if present; then rule code, if +present; then contains-text, if present. For example, an assertion using all three would look like +`# error: 8 [invalid-assignment] "Some text"`. + +Error assertions in tests intended to test type checker semantics should primarily use rule-code +assertions, with occasional contains-text assertions where needed to disambiguate or validate some +details of the diagnostic message. + +### Assertion locations + +An assertion comment may be a line-trailing comment, in which case it applies to the line it is on: + +```py +x: str = 1 # error: [invalid-assignment] +``` + +Or it may be a comment on its own line, in which case it applies to the next line that does not +contain an assertion comment: + +```py +# error: [invalid-assignment] +x: str = 1 +``` + +Multiple assertions applying to the same line may be stacked: + +```py +# error: [invalid-assignment] +# revealed: Literal[1] +x: str = reveal_type(1) +``` + +Intervening empty lines or non-assertion comments are not allowed; an assertion stack must be one +assertion per line, immediately following each other, with the line immediately following the last +assertion as the line of source code on which the matched diagnostics are emitted. + +## Literate style + +If multiple code blocks (without an explicit path, see below) are present in a single test, they will +be merged into a single file in the order they appear in the Markdown file. This allows for tests that +interleave code and explanations: + +````markdown +# My literate test + +This first snippet here: + +```py +from typing import Literal + +def f(x: Literal[1]): + pass +``` + +will be merged with this second snippet here, i.e. `f` is defined here: + +```py +f(2) # error: [invalid-argument-type] +``` +```` + +## Diagnostic Snapshotting + +In addition to inline assertions, one can also snapshot the full diagnostic +output of a test. This is done by adding a `` directive +in the corresponding section. For example: + +````markdown +## Unresolvable module import + + + +```py +import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" +``` +```` + +The `snapshot-diagnostics` directive must appear before anything else in +the section. + +This will use `insta` to manage an external file snapshot of all diagnostic +output generated. + +Inline assertions, as described above, may be used in conjunction with diagnostic +snapshotting. + +At present, there is no way to do inline snapshotting or to request more granular +snapshotting of specific diagnostics. + +## Multi-file tests + +Some tests require multiple files, with imports from one file into another. For this purpose, +tests can specify explicit file paths in a separate line before the code block (`b.py` below): + +````markdown +```py +from b import C +reveal_type(C) # revealed: Literal[C] +``` + +`b.py`: + +```py +class C: pass +``` +```` + +Relative file names are always relative to the "workspace root", which is also an import root (that +is, the equivalent of a runtime entry on `sys.path`). + +The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but +this is a feature we will want to add in the future. + +So the above test creates two files, `/src/mdtest_snippet.py` and `/src/b.py`, and sets the workspace +root to `/src/`, allowing imports from `b.py` using the module name `b`. + +## Multi-test suites + +A single test suite (Markdown file) can contain multiple tests, by demarcating them using Markdown +header lines: + +````markdown +# Same-file invalid assignment + +```py +x: int = "foo" # error: [invalid-assignment] +``` + +# Cross-file invalid assignment + +```py +from b import y +x: int = y # error: [invalid-assignment] +``` + +`b.py`: + +```py +y = "foo" +``` +```` + +This test suite contains two tests, one named "Same-file invalid assignment" and the other named +"Cross-file invalid assignment". The first test involves only a single embedded file, and the second +test involves two embedded files. + +The tests are run independently, in independent in-memory file systems and with new ty +[Salsa](https://github.com/salsa-rs/salsa) databases. This means that each is a from-scratch run of +the type checker, with no data persisting from any previous test. + +It is possible to filter to individual tests within a single markdown file using the +`MDTEST_TEST_FILTER` environment variable. This variable will match any tests which contain the +value as a case-sensitive substring in its name. An example test name is +`unpacking.md - Unpacking - Tuple - Multiple assignment`, which contains the name of the markdown +file and its parent headers joined together with hyphens. + +## Structured test suites + +Markdown headers can also be used to group related tests within a suite: + +````markdown +# Literals + +## Numbers + +### Integer + +```py +reveal_type(1) # revealed: Literal[1] +``` + +### Float + +```py +reveal_type(1.0) # revealed: float +``` + +## Strings + +```py +reveal_type("foo") # revealed: Literal["foo"] +``` +```` + +This test suite contains three tests, named "Literals - Numbers - Integer", "Literals - Numbers - +Float", and "Literals - Strings". + +A header-demarcated section must either be a test or a grouping header; it cannot be both. That is, +a header section can either contain embedded files (making it a test), or it can contain more +deeply-nested headers (headers with more `#`), but it cannot contain both. + +## Configuration + +The test framework supports a TOML-based configuration format, which is a subset of the full ty +configuration format. This configuration can be specified in fenced code blocks with `toml` as the +language tag: + +````markdown +```toml +[environment] +python-version = "3.10" +``` +```` + +This configuration will apply to all tests in the same section, and all nested sections within that +section. Nested sections can override configurations from their parent sections. + +To enable logging in an mdtest, set `log = true` at the top level of the TOML block. +See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/ty_test/src/config.rs) +for the full list of supported configuration options. + +### Specifying a custom typeshed + +Some tests will need to override the default typeshed with custom files. The `[environment]` +configuration option `typeshed` can be used to do this: + +````markdown +```toml +[environment] +typeshed = "/typeshed" +``` +```` + +For more details, take a look at the [custom-typeshed Markdown test]. + +### Mocking a Python environment + +Mdtest supports mocking a Python environment for a specific test at an arbitrary location, again +using the `[environment]` configuration option: + +````markdown +```toml +[environment] +python = ".venv" +``` +```` + +Mdtest also makes it easy to write Python packages to the mock environment's `site-packages` +directory using the `` magic path segment. This would otherwise be hard, due +to the fact that the `site-packages` subdirectory in an environment is located at a different +relative path depending on the platform. In the following test, mdtest will write the Python file to +`.venv/Lib/site-packages/foo.py` in its in-memory filesystem used for the test if the test is being +executed on Windows, and `.venv/lib/python3.13/site-packages/foo.py` otherwise: + +````markdown +```toml +[environment] +python = ".venv" +python-version = "3.13" +``` + +`.venv//foo.py`: + +```py +X = 1 +``` +```` + +## Documentation of tests + +Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by +the test framework) between fenced code blocks. This permits natural documentation of +why a test exists, and what it intends to assert: + +````markdown +Assigning a string to a variable annotated as `int` is not permitted: + +```py +x: int = "foo" # error: [invalid-assignment] +``` +```` + +## Running the tests + +All Markdown-based tests are executed in a normal `cargo test` / `cargo run nextest` run. If you want to run the Markdown tests +*only*, you can filter the tests using `mdtest__`: + +```bash +cargo test -p ty_python_semantic -- mdtest__ +``` + +Alternatively, you can use the `mdtest.py` runner which has a watch mode that will re-run corresponding tests when Markdown files change, and recompile automatically when Rust code changes: + +```bash +uv run crates/ty_python_semantic/mdtest.py +``` + +## Planned features + +There are some designed features that we intend for the test framework to have, but have not yet +implemented: + +### Multi-line diagnostic assertions + +We may want to be able to assert that a diagnostic spans multiple lines, and to assert the columns it +begins and/or ends on. The planned syntax for this will use `<<<` and `>>>` to mark the start and end lines for +an assertion: + +```py +(3 # error: 2 [unsupported-operands] <<< + + + "foo") # error: 6 >>> +``` + +The column assertion `6` on the ending line should be optional. + +In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins +an assertion ended by `>>>>`, etc. + +### Configuring search paths and kinds + +The ty TOML configuration format hasn't been finalized, and we may want to implement +support in the test framework for configuring search paths before it is designed. If so, we can +define some configuration options for now under the `[tests]` namespace. In the future, perhaps +some of these can be replaced by real ty configuration options; some or all may also be +kept long-term as test-specific options. + +Some configuration options we will want to provide: + +- We should be able to configure the default workspace root to something other than `/src/` using a + `workspace-root` configuration option. + +- We should be able to add a third-party root using the `third-party-root` configuration option. + +- We may want to add additional configuration options for setting additional search path kinds. + +Paths for `workspace-root` and `third-party-root` must be absolute. + +Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a +non-default value using the `workspace-root` config. + +### I/O errors + +We could use an `error=` configuration option in the tag string to make an embedded file cause an +I/O error on read. + +### Asserting on full diagnostic output + +> [!NOTE] +> At present, one can opt into diagnostic snapshotting that is managed via external files. See +> the section above for more details. The feature outlined below, *inline* diagnostic snapshotting, +> is still desirable. + +The inline comment diagnostic assertions are useful for making quick, readable assertions about +diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic +output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will +want to assert on diagnostics in a file, without impacting the contents of that file by changing a +comment in it. In these cases, a Python code block in a test could be followed by a fenced code +block with language `output`; this would contain the full diagnostic output for the preceding test +file: + +````markdown +# full output + +```py +x = 1 +reveal_type(x) +``` + +This is just an example, not a proposal that ty would ever actually output diagnostics in +precisely this format: + +```output +mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]' +``` +```` + +We will want to build tooling to automatically capture and update these “full diagnostic output” +blocks, when tests are run in an update-output mode (probably specified by an environment variable.) + +By default, an `output` block will specify diagnostic output for the file +`/mdtest_snippet.py`. An `output` block can be prefixed by a +`<path>`: label as usual, to explicitly specify the Python file for which it asserts +diagnostic output. + +It is an error for an `output` block to exist, if there is no `py` or `python` block in the same +test for the same file path. + +### Incremental tests + +Some tests should validate incremental checking, by initially creating some files, checking them, +and then modifying/adding/deleting files and checking again. + +We should add the capability to create an incremental test by using the `stage=` option on some +fenced code blocks in the test: + +````markdown +# Incremental + +## modify a file + +Initial file contents: + +```py +from b import x +reveal_type(x) +``` + +`b.py`: + +```py +x = 1 +``` + +Initial expected output for the unnamed file: + +```output +/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]' +``` + +Now in our first incremental stage, modify the contents of `b.py`: + +`b.py`: + +```py stage=1 +# b.py +x = 2 +``` + +And this is our updated expected output for the unnamed file at stage 1: + +```output stage=1 +/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[2]' +``` + +(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment +diagnostic assertions for `mdtest_snippet.py` would require specifying new contents for +`mdtest_snippet.py` in stage 1, which we don't want to do in this test.) +```` + +It will be possible to provide any number of stages in an incremental test. If a stage re-specifies +a filename that was specified in a previous stage (or the initial stage), that file is modified. A +new filename appearing for the first time in a new stage will create a new file. To delete a +previously created file, specify that file with the tag `delete` in its tag string (in this case, it +is an error to provide non-empty contents). Any previously-created files that are not re-specified +in a later stage continue to exist with their previously-specified contents, and are not "touched". + +All stages should be run in order, incrementally, and then the final state should also be re-checked +cold, to validate equivalence of cold and incremental check results. + +[^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers + also, treat it as part of the standard library. + +[custom-typeshed markdown test]: ../ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +[typeshed `versions`]: https://github.com/python/typeshed/blob/c546278aae47de0b2b664973da4edb613400f6ce/stdlib/VERSIONS#L1-L18%3E diff --git a/crates/red_knot_test/src/assertion.rs b/crates/ty_test/src/assertion.rs similarity index 93% rename from crates/red_knot_test/src/assertion.rs rename to crates/ty_test/src/assertion.rs index d88c7d2b27c78..348d96ec09cbf 100644 --- a/crates/red_knot_test/src/assertion.rs +++ b/crates/ty_test/src/assertion.rs @@ -37,10 +37,10 @@ use crate::db::Db; use ruff_db::files::File; use ruff_db::parsed::parsed_module; -use ruff_db::source::{line_index, source_text, SourceText}; +use ruff_db::source::{SourceText, line_index, source_text}; use ruff_python_trivia::{CommentRanges, Cursor}; use ruff_source_file::{LineIndex, OneIndexed}; -use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use smallvec::SmallVec; use std::ops::Deref; use std::str::FromStr; @@ -57,7 +57,7 @@ impl InlineFileAssertions { pub(crate) fn from_file(db: &Db, file: File) -> Self { let source = source_text(db, file); let lines = line_index(db, file); - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file).load(db); let comment_ranges = CommentRanges::from(parsed.tokens()); Self { comment_ranges, @@ -360,11 +360,6 @@ impl<'a> ErrorAssertionParser<'a> { } } - /// Retrieves the current offset of the cursor within the source code. - fn offset(&self) -> TextSize { - self.comment_source.text_len() - self.cursor.text_len() - } - /// Consume characters in the assertion comment until we find a non-whitespace character fn skip_whitespace(&mut self) { self.cursor.eat_while(char::is_whitespace); @@ -387,9 +382,10 @@ impl<'a> ErrorAssertionParser<'a> { if rule.is_some() { return Err(ErrorAssertionParseError::ColumnNumberAfterRuleCode); } - let offset = self.offset() - TextSize::new(1); + let offset = self.cursor.offset() - TextSize::new(1); self.cursor.eat_while(|c| !c.is_whitespace()); - let column_str = &self.comment_source[TextRange::new(offset, self.offset())]; + let column_str = + &self.comment_source[TextRange::new(offset, self.cursor.offset())]; column = OneIndexed::from_str(column_str) .map(Some) .map_err(|e| ErrorAssertionParseError::BadColumnNumber(column_str, e))?; @@ -400,12 +396,14 @@ impl<'a> ErrorAssertionParser<'a> { if rule.is_some() { return Err(ErrorAssertionParseError::MultipleRuleCodes); } - let offset = self.offset(); + let offset = self.cursor.offset(); self.cursor.eat_while(|c| c != ']'); if self.cursor.is_eof() { return Err(ErrorAssertionParseError::UnclosedRuleCode); } - rule = Some(self.comment_source[TextRange::new(offset, self.offset())].trim()); + rule = Some( + self.comment_source[TextRange::new(offset, self.cursor.offset())].trim(), + ); self.cursor.bump(); } @@ -413,8 +411,8 @@ impl<'a> ErrorAssertionParser<'a> { '"' => { let comment_source = self.comment_source.trim(); return if comment_source.ends_with('"') { - let rest = - &comment_source[self.offset().to_usize()..comment_source.len() - 1]; + let rest = &comment_source + [self.cursor.offset().to_usize()..comment_source.len() - 1]; Ok(ErrorAssertion { rule, column, @@ -434,7 +432,7 @@ impl<'a> ErrorAssertionParser<'a> { unexpected => { return Err(ErrorAssertionParseError::UnexpectedCharacter { character: unexpected, - offset: self.offset().to_usize(), + offset: self.cursor.offset().to_usize(), }); } } @@ -482,20 +480,35 @@ pub(crate) enum ErrorAssertionParseError<'a> { MultipleRuleCodes, #[error("expected '\"' to be the final character in an assertion with an error message")] UnclosedMessage, - #[error("unexpected character `{character}` at offset {offset} (relative to the `:` in the assertion comment)")] + #[error( + "unexpected character `{character}` at offset {offset} (relative to the `:` in the assertion comment)" + )] UnexpectedCharacter { character: char, offset: usize }, } #[cfg(test)] mod tests { use super::*; - use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem as _; + use ruff_db::{Db as _, files::system_path_to_file}; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::OneIndexed; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings, + }; fn get_assertions(source: &str) -> InlineFileAssertions { let mut db = Db::setup(); + + let settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(Vec::new()) + .to_search_paths(db.system(), db.vendored()) + .unwrap(), + }; + Program::init_or_update(&mut db, settings); + db.write_file("/src/test.py", source).unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap(); InlineFileAssertions::from_file(&db, file) @@ -667,8 +680,10 @@ mod tests { assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2)); assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3)); - let [UnparsedAssertion::Error(error1), UnparsedAssertion::Revealed(expected_ty)] = - &line1.assertions[..] + let [ + UnparsedAssertion::Error(error1), + UnparsedAssertion::Revealed(expected_ty), + ] = &line1.assertions[..] else { panic!("expected one error assertion and one Revealed assertion"); }; diff --git a/crates/ty_test/src/config.rs b/crates/ty_test/src/config.rs new file mode 100644 index 0000000000000..bc32678449eb8 --- /dev/null +++ b/crates/ty_test/src/config.rs @@ -0,0 +1,118 @@ +//! TOML-deserializable ty configuration, similar to `ty.toml`, to be able to +//! control some configuration options from Markdown files. For now, this supports the +//! following limited structure: +//! +//! ```toml +//! log = true # or log = "ty=WARN" +//! [environment] +//! python-version = "3.10" +//! ``` + +use anyhow::Context; +use ruff_db::system::{SystemPath, SystemPathBuf}; +use ruff_python_ast::PythonVersion; +use serde::{Deserialize, Serialize}; +use ty_python_semantic::PythonPlatform; + +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub(crate) struct MarkdownTestConfig { + pub(crate) environment: Option, + + pub(crate) log: Option, + + /// The [`ruff_db::system::System`] to use for tests. + /// + /// Defaults to the case-sensitive [`ruff_db::system::InMemorySystem`]. + pub(crate) system: Option, +} + +impl MarkdownTestConfig { + pub(crate) fn from_str(s: &str) -> anyhow::Result { + toml::from_str(s).context("Error while parsing Markdown TOML config") + } + + pub(crate) fn python_version(&self) -> Option { + self.environment.as_ref()?.python_version + } + + pub(crate) fn python_platform(&self) -> Option { + self.environment.as_ref()?.python_platform.clone() + } + + pub(crate) fn typeshed(&self) -> Option<&SystemPath> { + self.environment.as_ref()?.typeshed.as_deref() + } + + pub(crate) fn extra_paths(&self) -> Option<&[SystemPathBuf]> { + self.environment.as_ref()?.extra_paths.as_deref() + } + + pub(crate) fn python(&self) -> Option<&SystemPath> { + self.environment.as_ref()?.python.as_deref() + } +} + +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub(crate) struct Environment { + /// Target Python version to assume when resolving types. + /// + /// The Python version affects allowed syntax, type definitions of the standard library, and + /// type definitions of first- and third-party modules that are conditional on the Python version. + /// + /// By default, the Python version is inferred as the lower bound of the project's + /// `requires-python` field from the `pyproject.toml`, if available. Otherwise, the latest + /// stable version supported by ty is used, which is currently 3.13. + /// + /// ty will not infer the Python version from the Python environment at this time. + pub(crate) python_version: Option, + + /// Target platform to assume when resolving types. + pub(crate) python_platform: Option, + + /// Path to a custom typeshed directory. + pub(crate) typeshed: Option, + + /// Additional search paths to consider when resolving modules. + pub(crate) extra_paths: Option>, + + /// Path to the Python environment. + /// + /// ty uses the Python environment to resolve type information and third-party dependencies. + /// + /// If a path to a Python interpreter is provided, e.g., `.venv/bin/python3`, ty will attempt to + /// find an environment two directories up from the interpreter's path, e.g., `.venv`. At this + /// time, ty does not invoke the interpreter to determine the location of the environment. This + /// means that ty will not resolve dynamic executables such as a shim. + /// + /// ty will search in the resolved environment's `site-packages` directories for type + /// information and third-party imports. + #[serde(skip_serializing_if = "Option::is_none")] + pub python: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub(crate) enum Log { + /// Enable logging with tracing when `true`. + Bool(bool), + /// Enable logging and only show filters that match the given [env-filter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) + Filter(String), +} + +/// The system to use for tests. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum SystemKind { + /// Use an in-memory system with a case-sensitive file system. + /// + /// This is recommended for all tests because it's fast. + #[default] + InMemory, + + /// Use the os system. + /// + /// This system should only be used when testing system or OS specific behavior. + Os, +} diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs new file mode 100644 index 0000000000000..76fff50e4eb65 --- /dev/null +++ b/crates/ty_test/src/db.rs @@ -0,0 +1,287 @@ +use camino::{Utf8Component, Utf8PathBuf}; +use ruff_db::Db as SourceDb; +use ruff_db::diagnostic::Severity; +use ruff_db::files::{File, Files}; +use ruff_db::system::{ + CaseSensitivity, DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath, + SystemPathBuf, WritableSystem, +}; +use ruff_db::vendored::VendoredFileSystem; +use ruff_notebook::{Notebook, NotebookError}; +use std::borrow::Cow; +use std::sync::Arc; +use tempfile::TempDir; +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::{Db as SemanticDb, Program, default_lint_registry}; + +#[salsa::db] +#[derive(Clone)] +pub(crate) struct Db { + storage: salsa::Storage, + files: Files, + system: MdtestSystem, + vendored: VendoredFileSystem, + rule_selection: Arc, +} + +impl Db { + pub(crate) fn setup() -> Self { + let rule_selection = RuleSelection::all(default_lint_registry(), Severity::Info); + + Self { + system: MdtestSystem::in_memory(), + storage: salsa::Storage::new(Some(Box::new({ + move |event| { + tracing::trace!("event: {:?}", event); + } + }))), + vendored: ty_vendored::file_system().clone(), + files: Files::default(), + rule_selection: Arc::new(rule_selection), + } + } + + pub(crate) fn use_os_system_with_temp_dir(&mut self, cwd: SystemPathBuf, temp_dir: TempDir) { + self.system.with_os(cwd, temp_dir); + Files::sync_all(self); + } + + pub(crate) fn use_in_memory_system(&mut self) { + self.system.with_in_memory(); + Files::sync_all(self); + } + + pub(crate) fn create_directory_all(&self, path: &SystemPath) -> ruff_db::system::Result<()> { + self.system.create_directory_all(path) + } +} + +#[salsa::db] +impl SourceDb for Db { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> ruff_python_ast::PythonVersion { + Program::get(self).python_version(self) + } +} + +#[salsa::db] +impl SemanticDb for Db { + fn is_file_open(&self, file: File) -> bool { + !file.path(self).is_vendored_path() + } + + fn rule_selection(&self, _file: File) -> &RuleSelection { + &self.rule_selection + } + + fn lint_registry(&self) -> &LintRegistry { + default_lint_registry() + } +} + +#[salsa::db] +impl salsa::Database for Db {} + +impl DbWithWritableSystem for Db { + type System = MdtestSystem; + fn writable_system(&self) -> &Self::System { + &self.system + } +} + +#[derive(Debug, Clone)] +pub(crate) struct MdtestSystem(Arc); + +#[derive(Debug)] +enum MdtestSystemInner { + InMemory(InMemorySystem), + Os { + os_system: OsSystem, + _temp_dir: TempDir, + }, +} + +impl MdtestSystem { + fn in_memory() -> Self { + Self(Arc::new(MdtestSystemInner::InMemory( + InMemorySystem::default(), + ))) + } + + fn as_system(&self) -> &dyn WritableSystem { + match &*self.0 { + MdtestSystemInner::InMemory(system) => system, + MdtestSystemInner::Os { os_system, .. } => os_system, + } + } + + fn with_os(&mut self, cwd: SystemPathBuf, temp_dir: TempDir) { + self.0 = Arc::new(MdtestSystemInner::Os { + os_system: OsSystem::new(cwd), + _temp_dir: temp_dir, + }); + } + + fn with_in_memory(&mut self) { + if let MdtestSystemInner::InMemory(in_memory) = &*self.0 { + in_memory.fs().remove_all(); + } else { + self.0 = Arc::new(MdtestSystemInner::InMemory(InMemorySystem::default())); + } + } + + fn normalize_path<'a>(&self, path: &'a SystemPath) -> Cow<'a, SystemPath> { + match &*self.0 { + MdtestSystemInner::InMemory(_) => Cow::Borrowed(path), + MdtestSystemInner::Os { os_system, .. } => { + // Make all paths relative to the current directory + // to avoid writing or reading from outside the temp directory. + let without_root: Utf8PathBuf = path + .components() + .skip_while(|component| { + matches!( + component, + Utf8Component::RootDir | Utf8Component::Prefix(..) + ) + }) + .collect(); + Cow::Owned(os_system.current_directory().join(&without_root)) + } + } + } +} + +impl System for MdtestSystem { + fn path_metadata( + &self, + path: &SystemPath, + ) -> ruff_db::system::Result { + self.as_system().path_metadata(&self.normalize_path(path)) + } + + fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result { + let canonicalized = self + .as_system() + .canonicalize_path(&self.normalize_path(path))?; + + if let MdtestSystemInner::Os { os_system, .. } = &*self.0 { + // Make the path relative to the current directory + Ok(canonicalized + .strip_prefix(os_system.current_directory()) + .unwrap() + .to_owned()) + } else { + Ok(canonicalized) + } + } + + fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result { + self.as_system().read_to_string(&self.normalize_path(path)) + } + + fn read_to_notebook(&self, path: &SystemPath) -> Result { + self.as_system() + .read_to_notebook(&self.normalize_path(path)) + } + + fn read_virtual_path_to_string( + &self, + path: &ruff_db::system::SystemVirtualPath, + ) -> ruff_db::system::Result { + self.as_system().read_virtual_path_to_string(path) + } + + fn read_virtual_path_to_notebook( + &self, + path: &ruff_db::system::SystemVirtualPath, + ) -> Result { + self.as_system().read_virtual_path_to_notebook(path) + } + + fn path_exists_case_sensitive(&self, path: &SystemPath, prefix: &SystemPath) -> bool { + self.as_system() + .path_exists_case_sensitive(&self.normalize_path(path), &self.normalize_path(prefix)) + } + + fn case_sensitivity(&self) -> CaseSensitivity { + self.as_system().case_sensitivity() + } + + fn current_directory(&self) -> &SystemPath { + self.as_system().current_directory() + } + + fn user_config_directory(&self) -> Option { + self.as_system().user_config_directory() + } + + fn cache_dir(&self) -> Option { + self.as_system().cache_dir() + } + + fn read_directory<'a>( + &'a self, + path: &SystemPath, + ) -> ruff_db::system::Result< + Box> + 'a>, + > { + self.as_system().read_directory(&self.normalize_path(path)) + } + + fn walk_directory( + &self, + path: &SystemPath, + ) -> ruff_db::system::walk_directory::WalkDirectoryBuilder { + self.as_system().walk_directory(&self.normalize_path(path)) + } + + fn glob( + &self, + pattern: &str, + ) -> Result< + Box> + '_>, + ruff_db::system::PatternError, + > { + self.as_system() + .glob(self.normalize_path(SystemPath::new(pattern)).as_str()) + } + + fn as_writable(&self) -> Option<&dyn WritableSystem> { + Some(self) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + +impl WritableSystem for MdtestSystem { + fn create_new_file(&self, path: &SystemPath) -> ruff_db::system::Result<()> { + self.as_system().create_new_file(&self.normalize_path(path)) + } + + fn write_file(&self, path: &SystemPath, content: &str) -> ruff_db::system::Result<()> { + self.as_system() + .write_file(&self.normalize_path(path), content) + } + + fn create_directory_all(&self, path: &SystemPath) -> ruff_db::system::Result<()> { + self.as_system() + .create_directory_all(&self.normalize_path(path)) + } +} diff --git a/crates/red_knot_test/src/diagnostic.rs b/crates/ty_test/src/diagnostic.rs similarity index 100% rename from crates/red_knot_test/src/diagnostic.rs rename to crates/ty_test/src/diagnostic.rs diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs new file mode 100644 index 0000000000000..8532bbdd0204d --- /dev/null +++ b/crates/ty_test/src/lib.rs @@ -0,0 +1,543 @@ +use crate::config::Log; +use crate::db::Db; +use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap}; +use camino::Utf8Path; +use colored::Colorize; +use config::SystemKind; +use parser as test_parser; +use ruff_db::Db as _; +use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig}; +use ruff_db::files::{File, system_path_to_file}; +use ruff_db::panic::catch_unwind; +use ruff_db::parsed::parsed_module; +use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; +use ruff_db::testing::{setup_logging, setup_logging_with_filter}; +use ruff_source_file::{LineIndex, OneIndexed}; +use std::backtrace::BacktraceStatus; +use std::fmt::Write; +use ty_python_semantic::pull_types::pull_types; +use ty_python_semantic::types::check_types; +use ty_python_semantic::{ + Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource, + PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, +}; + +mod assertion; +mod config; +mod db; +mod diagnostic; +mod matcher; +mod parser; + +use ty_static::EnvVars; + +/// Run `path` as a markdown test suite with given `title`. +/// +/// Panic on test failure, and print failure details. +#[expect(clippy::print_stdout)] +pub fn run( + absolute_fixture_path: &Utf8Path, + relative_fixture_path: &Utf8Path, + snapshot_path: &Utf8Path, + short_title: &str, + test_name: &str, + output_format: OutputFormat, +) { + let source = std::fs::read_to_string(absolute_fixture_path).unwrap(); + let suite = match test_parser::parse(short_title, &source) { + Ok(suite) => suite, + Err(err) => { + panic!("Error parsing `{absolute_fixture_path}`: {err:?}") + } + }; + + let mut db = db::Db::setup(); + + let filter = std::env::var(EnvVars::MDTEST_TEST_FILTER).ok(); + let mut any_failures = false; + for test in suite.tests() { + if filter + .as_ref() + .is_some_and(|f| !test.uncontracted_name().contains(f)) + { + continue; + } + + let _tracing = test.configuration().log.as_ref().and_then(|log| match log { + Log::Bool(enabled) => enabled.then(setup_logging), + Log::Filter(filter) => setup_logging_with_filter(filter), + }); + + if let Err(failures) = run_test(&mut db, relative_fixture_path, snapshot_path, &test) { + any_failures = true; + + if output_format.is_cli() { + println!("\n{}\n", test.name().bold().underline()); + } + + let md_index = LineIndex::from_source_text(&source); + + for test_failures in failures { + let source_map = + EmbeddedFileSourceMap::new(&md_index, test_failures.backtick_offsets); + + for (relative_line_number, failures) in test_failures.by_line.iter() { + let absolute_line_number = + source_map.to_absolute_line_number(relative_line_number); + + for failure in failures { + match output_format { + OutputFormat::Cli => { + let line_info = + format!("{relative_fixture_path}:{absolute_line_number}") + .cyan(); + println!(" {line_info} {failure}"); + } + OutputFormat::GitHub => println!( + "::error file={absolute_fixture_path},line={absolute_line_number}::{failure}" + ), + } + } + } + } + + let escaped_test_name = test.name().replace('\'', "\\'"); + + if output_format.is_cli() { + println!( + "\nTo rerun this specific test, set the environment variable: {}='{escaped_test_name}'", + EnvVars::MDTEST_TEST_FILTER, + ); + println!( + "{}='{escaped_test_name}' cargo test -p ty_python_semantic --test mdtest -- {test_name}", + EnvVars::MDTEST_TEST_FILTER, + ); + } + } + } + + println!("\n{}\n", "-".repeat(50)); + + assert!(!any_failures, "Some tests failed."); +} + +/// Defines the format in which mdtest should print an error to the terminal +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputFormat { + /// The format `cargo test` should use by default. + Cli, + /// A format that will provide annotations from GitHub Actions + /// if mdtest fails on a PR. + /// See + GitHub, +} + +impl OutputFormat { + const fn is_cli(self) -> bool { + matches!(self, OutputFormat::Cli) + } +} + +fn run_test( + db: &mut db::Db, + relative_fixture_path: &Utf8Path, + snapshot_path: &Utf8Path, + test: &parser::MarkdownTest, +) -> Result<(), Failures> { + // Initialize the system and remove all files and directories to reset the system to a clean state. + match test.configuration().system.unwrap_or_default() { + SystemKind::InMemory => { + db.use_in_memory_system(); + } + SystemKind::Os => { + let dir = tempfile::TempDir::new().expect("Creating a temporary directory to succeed"); + let root_path = dir + .path() + .canonicalize() + .expect("Canonicalizing to succeed"); + let root_path = SystemPathBuf::from_path_buf(root_path) + .expect("Temp directory to be a valid UTF8 path") + .simplified() + .to_path_buf(); + + db.use_os_system_with_temp_dir(root_path, dir); + } + } + + let project_root = SystemPathBuf::from("/src"); + db.create_directory_all(&project_root) + .expect("Creating the project root to succeed"); + + let src_path = project_root.clone(); + let custom_typeshed_path = test.configuration().typeshed(); + let python_version = test.configuration().python_version().unwrap_or_default(); + + let mut typeshed_files = vec![]; + let mut has_custom_versions_file = false; + + let test_files: Vec<_> = test + .files() + .filter_map(|embedded| { + if embedded.lang == "ignore" { + return None; + } + + assert!( + matches!(embedded.lang, "py" | "pyi" | "python" | "text" | "cfg"), + "Supported file types are: py (or python), pyi, text, cfg and ignore" + ); + + let mut full_path = embedded.full_path(&project_root); + + if let Some(relative_path_to_custom_typeshed) = custom_typeshed_path + .and_then(|typeshed| full_path.strip_prefix(typeshed.join("stdlib")).ok()) + { + if relative_path_to_custom_typeshed.as_str() == "VERSIONS" { + has_custom_versions_file = true; + } else if relative_path_to_custom_typeshed + .extension() + .is_some_and(|ext| ext == "pyi") + { + typeshed_files.push(relative_path_to_custom_typeshed.to_path_buf()); + } + } else if let Some(component_index) = full_path + .components() + .position(|c| c.as_str() == "") + { + // If the path contains ``, we need to replace it with the + // actual site-packages directory based on the Python platform and version. + let mut components = full_path.components(); + let mut new_path: SystemPathBuf = + components.by_ref().take(component_index).collect(); + if cfg!(target_os = "windows") { + new_path.extend(["Lib", "site-packages"]); + } else { + new_path.push("lib"); + new_path.push(format!("python{python_version}")); + new_path.push("site-packages"); + } + new_path.extend(components.skip(1)); + full_path = new_path; + } + + db.write_file(&full_path, &embedded.code).unwrap(); + + if !(full_path.starts_with(&src_path) && matches!(embedded.lang, "py" | "pyi")) { + // These files need to be written to the file system (above), but we don't run any checks on them. + return None; + } + + let file = system_path_to_file(db, full_path).unwrap(); + + Some(TestFile { + file, + backtick_offsets: embedded.backtick_offsets.clone(), + }) + }) + .collect(); + + // Create a custom typeshed `VERSIONS` file if none was provided. + if let Some(typeshed_path) = custom_typeshed_path { + if !has_custom_versions_file { + let versions_file = typeshed_path.join("stdlib/VERSIONS"); + let contents = typeshed_files + .iter() + .fold(String::new(), |mut content, path| { + // This is intentionally kept simple: + let module_name = path + .as_str() + .trim_end_matches(".pyi") + .trim_end_matches("/__init__") + .replace('/', "."); + let _ = writeln!(content, "{module_name}: 3.8-"); + content + }); + db.write_file(&versions_file, contents).unwrap(); + } + } + + let configuration = test.configuration(); + + let site_packages_paths = if let Some(python) = configuration.python() { + let environment = + PythonEnvironment::new(python, SysPrefixPathOrigin::PythonCliFlag, db.system()) + .expect("Python environment to point to a valid path"); + environment + .site_packages_paths(db.system()) + .expect("Python environment to be valid") + .into_vec() + } else { + vec![] + }; + + let settings = ProgramSettings { + python_version: PythonVersionWithSource { + version: python_version, + source: PythonVersionSource::Cli, + }, + python_platform: configuration + .python_platform() + .unwrap_or(PythonPlatform::Identifier("linux".to_string())), + search_paths: SearchPathSettings { + src_roots: vec![src_path], + extra_paths: configuration.extra_paths().unwrap_or_default().to_vec(), + custom_typeshed: custom_typeshed_path.map(SystemPath::to_path_buf), + site_packages_paths, + } + .to_search_paths(db.system(), db.vendored()) + .expect("Failed to resolve search path settings"), + }; + + Program::init_or_update(db, settings); + + // When snapshot testing is enabled, this is populated with + // all diagnostics. Otherwise it remains empty. + let mut snapshot_diagnostics = vec![]; + + let mut any_pull_types_failures = false; + + let mut failures: Failures = test_files + .iter() + .filter_map(|test_file| { + let pull_types_result = attempt_test( + db, + pull_types, + test_file, + "\"pull types\"", + Some( + "Note: either fix the panic or add the `` \ + directive to this test", + ), + ); + match pull_types_result { + Ok(()) => {} + Err(failures) => { + any_pull_types_failures = true; + if !test.should_skip_pulling_types() { + return Some(failures); + } + } + } + + let parsed = parsed_module(db, test_file.file).load(db); + + let mut diagnostics: Vec = parsed + .errors() + .iter() + .map(|error| Diagnostic::invalid_syntax(test_file.file, &error.error, error)) + .collect(); + + diagnostics.extend( + parsed + .unsupported_syntax_errors() + .iter() + .map(|error| Diagnostic::invalid_syntax(test_file.file, error, error)), + ); + + let mdtest_result = attempt_test(db, check_types, test_file, "run mdtest", None); + let type_diagnostics = match mdtest_result { + Ok(diagnostics) => diagnostics, + Err(failures) => return Some(failures), + }; + + diagnostics.extend(type_diagnostics); + diagnostics.sort_by(|left, right| { + left.rendering_sort_key(db) + .cmp(&right.rendering_sort_key(db)) + }); + + let failure = match matcher::match_file(db, test_file.file, &diagnostics) { + Ok(()) => None, + Err(line_failures) => Some(FileFailures { + backtick_offsets: test_file.backtick_offsets.clone(), + by_line: line_failures, + }), + }; + if test.should_snapshot_diagnostics() { + snapshot_diagnostics.extend(diagnostics); + } + + failure + }) + .collect(); + + if test.should_skip_pulling_types() && !any_pull_types_failures { + let mut by_line = matcher::FailuresByLine::default(); + by_line.push( + OneIndexed::from_zero_indexed(0), + vec![ + "Remove the `` directive from this test: pulling types \ + succeeded for all files in the test." + .to_string(), + ], + ); + let failure = FileFailures { + backtick_offsets: test_files[0].backtick_offsets.clone(), + by_line, + }; + failures.push(failure); + } + + if snapshot_diagnostics.is_empty() && test.should_snapshot_diagnostics() { + panic!( + "Test `{}` requested snapshotting diagnostics but it didn't produce any.", + test.name() + ); + } else if !snapshot_diagnostics.is_empty() { + let snapshot = + create_diagnostic_snapshot(db, relative_fixture_path, test, snapshot_diagnostics); + let name = test.name().replace(' ', "_").replace(':', "__"); + insta::with_settings!( + { + snapshot_path => snapshot_path, + input_file => name.clone(), + filters => vec![(r"\\", "/")], + prepend_module_to_snapshot => false, + }, + { insta::assert_snapshot!(name, snapshot) } + ); + } + + if failures.is_empty() { + Ok(()) + } else { + Err(failures) + } +} + +type Failures = Vec; + +/// The failures for a single file in a test by line number. +struct FileFailures { + /// Positional information about the code block(s) to reconstruct absolute line numbers. + backtick_offsets: Vec, + + /// The failures by lines in the file. + by_line: matcher::FailuresByLine, +} + +/// File in a test. +struct TestFile { + file: File, + + /// Positional information about the code block(s) to reconstruct absolute line numbers. + backtick_offsets: Vec, +} + +fn create_diagnostic_snapshot( + db: &mut db::Db, + relative_fixture_path: &Utf8Path, + test: &parser::MarkdownTest, + diagnostics: impl IntoIterator, +) -> String { + let display_config = DisplayDiagnosticConfig::default().color(false); + + let mut snapshot = String::new(); + writeln!(snapshot).unwrap(); + writeln!(snapshot, "---").unwrap(); + writeln!(snapshot, "mdtest name: {}", test.uncontracted_name()).unwrap(); + writeln!(snapshot, "mdtest path: {relative_fixture_path}").unwrap(); + writeln!(snapshot, "---").unwrap(); + writeln!(snapshot).unwrap(); + + writeln!(snapshot, "# Python source files").unwrap(); + writeln!(snapshot).unwrap(); + for file in test.files() { + writeln!(snapshot, "## {}", file.relative_path()).unwrap(); + writeln!(snapshot).unwrap(); + // Note that we don't use ```py here because the line numbering + // we add makes it invalid Python. This sacrifices syntax + // highlighting when you look at the snapshot on GitHub, + // but the line numbers are extremely useful for analyzing + // snapshots. So we keep them. + writeln!(snapshot, "```").unwrap(); + + let line_number_width = file.code.lines().count().to_string().len(); + for (i, line) in file.code.lines().enumerate() { + let line_number = i + 1; + writeln!(snapshot, "{line_number:>line_number_width$} | {line}").unwrap(); + } + writeln!(snapshot, "```").unwrap(); + writeln!(snapshot).unwrap(); + } + + writeln!(snapshot, "# Diagnostics").unwrap(); + writeln!(snapshot).unwrap(); + for (i, diag) in diagnostics.into_iter().enumerate() { + if i > 0 { + writeln!(snapshot).unwrap(); + } + writeln!(snapshot, "```").unwrap(); + write!(snapshot, "{}", diag.display(db, &display_config)).unwrap(); + writeln!(snapshot, "```").unwrap(); + } + snapshot +} + +/// Run a function over an embedded test file, catching any panics that occur in the process. +/// +/// If no panic occurs, the result of the function is returned as an `Ok()` variant. +/// +/// If a panic occurs, a nicely formatted [`FileFailures`] is returned as an `Err()` variant. +/// This will be formatted into a diagnostic message by `ty_test`. +fn attempt_test<'db, T, F>( + db: &'db Db, + test_fn: F, + test_file: &TestFile, + action: &str, + clarification: Option<&str>, +) -> Result +where + F: FnOnce(&'db dyn ty_python_semantic::Db, File) -> T + std::panic::UnwindSafe, +{ + catch_unwind(|| test_fn(db, test_file.file)).map_err(|info| { + let mut by_line = matcher::FailuresByLine::default(); + let mut messages = vec![]; + match info.location { + Some(location) => messages.push(format!( + "Attempting to {action} caused a panic at {location}" + )), + None => messages.push(format!( + "Attempting to {action} caused a panic at an unknown location", + )), + } + if let Some(clarification) = clarification { + messages.push(clarification.to_string()); + } + messages.push(String::new()); + match info.payload.as_str() { + Some(message) => messages.push(message.to_string()), + // Mimic the default panic hook's rendering of the panic payload if it's + // not a string. + None => messages.push("Box".to_string()), + } + messages.push(String::new()); + + if let Some(backtrace) = info.backtrace { + match backtrace.status() { + BacktraceStatus::Disabled => { + let msg = + "run with `RUST_BACKTRACE=1` environment variable to display a backtrace"; + messages.push(msg.to_string()); + } + BacktraceStatus::Captured => { + messages.extend(backtrace.to_string().split('\n').map(String::from)); + } + _ => {} + } + } + + if let Some(backtrace) = info.salsa_backtrace { + salsa::attach(db, || { + messages.extend(format!("{backtrace:#}").split('\n').map(String::from)); + }); + } + + by_line.push(OneIndexed::from_zero_indexed(0), messages); + + FileFailures { + backtick_offsets: test_file.backtick_offsets.clone(), + by_line, + } + }) +} diff --git a/crates/red_knot_test/src/matcher.rs b/crates/ty_test/src/matcher.rs similarity index 97% rename from crates/red_knot_test/src/matcher.rs rename to crates/ty_test/src/matcher.rs index 8a68b5483aee3..3d574242bdc14 100644 --- a/crates/red_knot_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -4,9 +4,9 @@ use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion} use crate::db::Db; use crate::diagnostic::SortedDiagnostics; use colored::Colorize; -use ruff_db::diagnostic::{Diagnostic, DiagnosticAsStrError, DiagnosticId}; +use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::files::File; -use ruff_db::source::{line_index, source_text, SourceText}; +use ruff_db::source::{SourceText, line_index, source_text}; use ruff_source_file::{LineIndex, OneIndexed}; use std::cmp::Ordering; use std::ops::Range; @@ -168,29 +168,24 @@ fn maybe_add_undefined_reveal_clarification( impl Unmatched for &Diagnostic { fn unmatched(&self) -> String { - let id = self.id(); - let id = id.as_str().unwrap_or_else(|error| match error { - DiagnosticAsStrError::Category { name, .. } => name, - }); - maybe_add_undefined_reveal_clarification( self, - format_args!(r#"[{id}] "{message}""#, message = self.concise_message()), + format_args!( + r#"[{id}] "{message}""#, + id = self.id(), + message = self.concise_message() + ), ) } } impl UnmatchedWithColumn for &Diagnostic { fn unmatched_with_column(&self, column: OneIndexed) -> String { - let id = self.id(); - let id = id.as_str().unwrap_or_else(|error| match error { - DiagnosticAsStrError::Category { name, .. } => name, - }); - maybe_add_undefined_reveal_clarification( self, format_args!( r#"{column} [{id}] "{message}""#, + id = self.id(), message = self.concise_message() ), ) @@ -263,7 +258,7 @@ impl Matcher { .and_then(|span| span.range()) .map(|range| { self.line_index - .source_location(range.start(), &self.source) + .line_column(range.start(), &self.source) .column }) .unwrap_or(OneIndexed::from_zero_indexed(0)) @@ -284,7 +279,7 @@ impl Matcher { ParsedAssertion::Error(error) => { let position = unmatched.iter().position(|diagnostic| { let lint_name_matches = !error.rule.is_some_and(|rule| { - !(diagnostic.id().is_lint_named(rule) || diagnostic.id().matches(rule)) + !(diagnostic.id().is_lint_named(rule) || diagnostic.id().as_str() == rule) }); let column_matches = error .column @@ -343,12 +338,16 @@ impl Matcher { #[cfg(test)] mod tests { use super::FailuresByLine; + use ruff_db::Db; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; - use ruff_db::files::{system_path_to_file, File}; + use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::OneIndexed; use ruff_text_size::TextRange; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings, + }; struct ExpectedDiagnostic { id: DiagnosticId, @@ -385,6 +384,16 @@ mod tests { colored::control::set_override(false); let mut db = crate::db::Db::setup(); + + let settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(Vec::new()) + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search paths settings"), + }; + Program::init_or_update(&mut db, settings); + db.write_file("/src/test.py", source).unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap(); diff --git a/crates/red_knot_test/src/parser.rs b/crates/ty_test/src/parser.rs similarity index 87% rename from crates/red_knot_test/src/parser.rs rename to crates/ty_test/src/parser.rs index 5eb3b003a120b..68e9ab5198ef4 100644 --- a/crates/red_knot_test/src/parser.rs +++ b/crates/ty_test/src/parser.rs @@ -1,14 +1,20 @@ -use std::{borrow::Cow, collections::hash_map::Entry}; +use std::{ + borrow::Cow, + collections::hash_map::Entry, + fmt::{Formatter, LowerHex, Write}, + hash::Hash, +}; use anyhow::bail; use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashMap; -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast::PySourceType; use ruff_python_trivia::Cursor; use ruff_source_file::{LineIndex, LineRanges, OneIndexed}; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustc_stable_hash::{FromStableHash, SipHasher128Hash, StableSipHasher128}; use crate::config::MarkdownTestConfig; @@ -39,6 +45,25 @@ impl<'s> MarkdownTestSuite<'s> { } } +struct Hash128([u64; 2]); + +impl FromStableHash for Hash128 { + type Hash = SipHasher128Hash; + + fn from(SipHasher128Hash(hash): SipHasher128Hash) -> Hash128 { + Hash128(hash) + } +} + +impl LowerHex for Hash128 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Self(hash) = self; + + // Only write the first half for concision + write!(f, "{:x}", hash[0]) + } +} + /// A single test inside a [`MarkdownTestSuite`]. /// /// A test is a single header section (or the implicit root section, if there are no Markdown @@ -52,22 +77,61 @@ pub(crate) struct MarkdownTest<'m, 's> { } impl<'m, 's> MarkdownTest<'m, 's> { - pub(crate) fn name(&self) -> String { - let mut name = String::new(); + const MAX_TITLE_LENGTH: usize = 20; + const ELLIPSIS: char = '\u{2026}'; + + fn contracted_title(title: &str) -> String { + if title.len() <= Self::MAX_TITLE_LENGTH { + return (*title).to_string(); + } + + format!( + "{}{}", + title + .chars() + .take(Self::MAX_TITLE_LENGTH) + .collect::(), + Self::ELLIPSIS + ) + } + + fn joined_name(&self, contracted: bool) -> String { + let mut name_fragments = vec![]; let mut parent_id = self.section.parent_id; + while let Some(next_id) = parent_id { let parent = &self.suite.sections[next_id]; + name_fragments.insert(0, parent.title); parent_id = parent.parent_id; - if !name.is_empty() { - name.insert_str(0, " - "); - } - name.insert_str(0, parent.title); } - if !name.is_empty() { - name.push_str(" - "); + + name_fragments.push(self.section.title); + + let full_name = name_fragments.join(" - "); + + if !contracted { + return full_name; } - name.push_str(self.section.title); - name + + let mut contracted_name = name_fragments + .iter() + .map(|fragment| Self::contracted_title(fragment)) + .collect::>() + .join(" - "); + + let mut hasher = StableSipHasher128::new(); + full_name.hash(&mut hasher); + let _ = write!(contracted_name, " ({:x})", hasher.finish::()); + + contracted_name + } + + pub(crate) fn uncontracted_name(&self) -> String { + self.joined_name(false) + } + + pub(crate) fn name(&self) -> String { + self.joined_name(true) } pub(crate) fn files(&self) -> impl Iterator> { @@ -79,7 +143,15 @@ impl<'m, 's> MarkdownTest<'m, 's> { } pub(super) fn should_snapshot_diagnostics(&self) -> bool { - self.section.snapshot_diagnostics + self.section + .directives + .contains(MdtestDirectives::SNAPSHOT_DIAGNOSTICS) + } + + pub(super) fn should_skip_pulling_types(&self) -> bool { + self.section + .directives + .contains(MdtestDirectives::PULL_TYPES_SKIP) } } @@ -130,7 +202,7 @@ struct Section<'s> { level: u8, parent_id: Option, config: MarkdownTestConfig, - snapshot_diagnostics: bool, + directives: MdtestDirectives, } #[newtype_index] @@ -345,7 +417,6 @@ struct Parser<'s> { explicit_path: Option<&'s str>, source: &'s str, - source_len: TextSize, /// Stack of ancestor sections. stack: SectionStack, @@ -365,7 +436,7 @@ impl<'s> Parser<'s> { level: 0, parent_id: None, config: MarkdownTestConfig::default(), - snapshot_diagnostics: false, + directives: MdtestDirectives::default(), }); Self { sections, @@ -374,7 +445,6 @@ impl<'s> Parser<'s> { cursor: Cursor::new(source), preceding_blank_lines: 0, explicit_path: None, - source_len: source.text_len(), stack: SectionStack::new(root_section_id), current_section_files: FxHashMap::default(), current_section_has_config: false, @@ -396,7 +466,7 @@ impl<'s> Parser<'s> { } } - fn skip_whitespace(&mut self) { + fn skip_non_newline_whitespace(&mut self) { self.cursor.eat_while(|c| c.is_whitespace() && c != '\n'); } @@ -410,11 +480,11 @@ impl<'s> Parser<'s> { } fn consume_until(&mut self, mut end_predicate: impl FnMut(char) -> bool) -> Option<&'s str> { - let start = self.offset().to_usize(); + let start = self.cursor.offset().to_usize(); while !self.cursor.is_eof() { if end_predicate(self.cursor.first()) { - return Some(&self.source[start..self.offset().to_usize()]); + return Some(&self.source[start..self.cursor.offset().to_usize()]); } self.cursor.bump(); } @@ -424,6 +494,7 @@ impl<'s> Parser<'s> { fn parse_impl(&mut self) -> anyhow::Result<()> { const SECTION_CONFIG_SNAPSHOT: &str = "snapshot-diagnostics"; + const SECTION_CONFIG_PULLTYPES: &str = "pull-types:skip"; const HTML_COMMENT_ALLOWLIST: &[&str] = &["blacken-docs:on", "blacken-docs:off"]; const CODE_BLOCK_END: &[u8] = b"```"; const HTML_COMMENT_END: &[u8] = b"-->"; @@ -436,10 +507,12 @@ impl<'s> Parser<'s> { { let html_comment = self.cursor.as_str()[..position].trim(); if html_comment == SECTION_CONFIG_SNAPSHOT { - self.process_snapshot_diagnostics()?; + self.process_mdtest_directive(MdtestDirective::SnapshotDiagnostics)?; + } else if html_comment == SECTION_CONFIG_PULLTYPES { + self.process_mdtest_directive(MdtestDirective::PullTypesSkip)?; } else if !HTML_COMMENT_ALLOWLIST.contains(&html_comment) { bail!( - "Unknown HTML comment `{}` -- possibly a `snapshot-diagnostics` typo? \ + "Unknown HTML comment `{}` -- possibly a typo? \ (Add to `HTML_COMMENT_ALLOWLIST` if this is a false positive)", html_comment ); @@ -473,23 +546,27 @@ impl<'s> Parser<'s> { if self.cursor.eat_char2('`', '`') { // We saw the triple-backtick beginning of a code block. - let backtick_offset_start = self.offset() - "```".text_len(); + let backtick_offset_start = self.cursor.offset() - "```".text_len(); if self.preceding_blank_lines < 1 && self.explicit_path.is_none() { - bail!("Code blocks must start on a new line and be preceded by at least one blank line."); + bail!( + "Code blocks must start on a new line and be preceded by at least one blank line." + ); } - self.skip_whitespace(); + self.skip_non_newline_whitespace(); // Parse the code block language specifier let lang = self .consume_until(|c| matches!(c, ' ' | '\n')) .unwrap_or_default(); - self.skip_whitespace(); + self.skip_non_newline_whitespace(); if !self.cursor.eat_char('\n') { - bail!("Trailing code-block metadata is not supported. Only the code block language can be specified."); + bail!( + "Trailing code-block metadata is not supported. Only the code block language can be specified." + ); } if let Some(position) = @@ -502,7 +579,7 @@ impl<'s> Parser<'s> { code = &code[..code.len() - '\n'.len_utf8()]; } - let backtick_offset_end = self.offset() - "```".text_len(); + let backtick_offset_end = self.cursor.offset() - "```".text_len(); self.process_code_block( lang, @@ -522,7 +599,7 @@ impl<'s> Parser<'s> { if let Some(path) = self.consume_until(|c| matches!(c, '`' | '\n')) { if self.cursor.eat_char('`') { - self.skip_whitespace(); + self.skip_non_newline_whitespace(); if self.cursor.eat_char(':') { self.explicit_path = Some(path); } @@ -541,7 +618,7 @@ impl<'s> Parser<'s> { self.explicit_path = None; if c.is_whitespace() { - self.skip_whitespace(); + self.skip_non_newline_whitespace(); if self.cursor.eat_char('`') && self.cursor.eat_char('`') && self.cursor.eat_char('`') @@ -570,7 +647,7 @@ impl<'s> Parser<'s> { level: header_level.try_into()?, parent_id: Some(parent), config: self.sections[parent].config.clone(), - snapshot_diagnostics: self.sections[parent].snapshot_diagnostics, + directives: self.sections[parent].directives, }; if !self.current_section_files.is_empty() { @@ -638,7 +715,9 @@ impl<'s> Parser<'s> { "py" | "python" => EmbeddedFilePath::Autogenerated(PySourceType::Python), "pyi" => EmbeddedFilePath::Autogenerated(PySourceType::Stub), "" => { - bail!("Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`"); + bail!( + "Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`" + ); } _ => { bail!( @@ -655,7 +734,9 @@ impl<'s> Parser<'s> { match self.current_section_files.entry(path.clone()) { Entry::Vacant(entry) => { if has_merged_snippets { - bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files."); + bail!( + "Merged snippets in test `{test_name}` are not allowed in the presence of other files." + ); } let index = self.files.push(EmbeddedFile { @@ -676,7 +757,9 @@ impl<'s> Parser<'s> { } if has_explicit_file_paths { - bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files."); + bail!( + "Merged snippets in test `{test_name}` are not allowed in the presence of other files." + ); } let index = *entry.get(); @@ -712,28 +795,28 @@ impl<'s> Parser<'s> { Ok(()) } - fn process_snapshot_diagnostics(&mut self) -> anyhow::Result<()> { + fn process_mdtest_directive(&mut self, directive: MdtestDirective) -> anyhow::Result<()> { if self.current_section_has_config { bail!( - "Section config to enable snapshotting diagnostics must come before \ + "Section config to enable {directive} must come before \ everything else (including TOML configuration blocks).", ); } if !self.current_section_files.is_empty() { bail!( - "Section config to enable snapshotting diagnostics must come before \ + "Section config to enable {directive} must come before \ everything else (including embedded files).", ); } let current_section = &mut self.sections[self.stack.top()]; - if current_section.snapshot_diagnostics { + if current_section.directives.has_directive_set(directive) { bail!( - "Section config to enable snapshotting diagnostics should appear \ + "Section config to enable {directive} should appear \ at most once.", ); } - current_section.snapshot_diagnostics = true; + current_section.directives.add_directive(directive); Ok(()) } @@ -747,21 +830,68 @@ impl<'s> Parser<'s> { } } - /// Retrieves the current offset of the cursor within the source code. - 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)) } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MdtestDirective { + /// A directive to enable snapshotting diagnostics. + SnapshotDiagnostics, + /// A directive to skip pull types. + PullTypesSkip, +} + +impl std::fmt::Display for MdtestDirective { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + MdtestDirective::SnapshotDiagnostics => f.write_str("snapshotting diagnostics"), + MdtestDirective::PullTypesSkip => f.write_str("skipping the pull-types visitor"), + } + } +} + +bitflags::bitflags! { + /// Directives that can be applied to a Markdown test section. + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct MdtestDirectives: u8 { + /// We should snapshot diagnostics for this section. + const SNAPSHOT_DIAGNOSTICS = 1 << 0; + /// We should skip pulling types for this section. + const PULL_TYPES_SKIP = 1 << 1; + } +} + +impl MdtestDirectives { + const fn has_directive_set(self, directive: MdtestDirective) -> bool { + match directive { + MdtestDirective::SnapshotDiagnostics => { + self.contains(MdtestDirectives::SNAPSHOT_DIAGNOSTICS) + } + MdtestDirective::PullTypesSkip => self.contains(MdtestDirectives::PULL_TYPES_SKIP), + } + } + + fn add_directive(&mut self, directive: MdtestDirective) { + match directive { + MdtestDirective::SnapshotDiagnostics => { + self.insert(MdtestDirectives::SNAPSHOT_DIAGNOSTICS); + } + MdtestDirective::PullTypesSkip => { + self.insert(MdtestDirectives::PULL_TYPES_SKIP); + } + } + } +} + #[cfg(test)] mod tests { use ruff_python_ast::PySourceType; use ruff_python_trivia::textwrap::dedent; + use insta::assert_snapshot; + use crate::parser::EmbeddedFilePath; #[test] @@ -786,7 +916,7 @@ mod tests { panic!("expected one test"); }; - assert_eq!(test.name(), "file.md"); + assert_snapshot!(test.name(), @"file.md (a8decfe8bd23e259)"); let [file] = test.files().collect::>()[..] else { panic!("expected one file"); @@ -814,7 +944,7 @@ mod tests { panic!("expected one test"); }; - assert_eq!(test.name(), "file.md"); + assert_snapshot!(test.name(), @"file.md (a8decfe8bd23e259)"); let [file] = test.files().collect::>()[..] else { panic!("expected one file"); @@ -865,9 +995,9 @@ mod tests { panic!("expected three tests"); }; - assert_eq!(test1.name(), "file.md - One"); - assert_eq!(test2.name(), "file.md - Two"); - assert_eq!(test3.name(), "file.md - Three"); + assert_snapshot!(test1.name(), @"file.md - One (9f620a533a21278)"); + assert_snapshot!(test2.name(), @"file.md - Two (1b4d4ef5a2cebbdc)"); + assert_snapshot!(test3.name(), @"file.md - Three (26479e23633dda57)"); let [file] = test1.files().collect::>()[..] else { panic!("expected one file"); @@ -935,8 +1065,8 @@ mod tests { panic!("expected two tests"); }; - assert_eq!(test1.name(), "file.md - One"); - assert_eq!(test2.name(), "file.md - Two"); + assert_snapshot!(test1.name(), @"file.md - One (9f620a533a21278)"); + assert_snapshot!(test2.name(), @"file.md - Two (1b4d4ef5a2cebbdc)"); let [main, foo] = test1.files().collect::>()[..] else { panic!("expected two files"); @@ -1331,7 +1461,7 @@ mod tests { panic!("expected one test"); }; - assert_eq!(test.name(), "file.md - A test"); + assert_snapshot!(test.name(), @"file.md - A test (1b4e27e6123dc8e7)"); } #[test] @@ -1722,7 +1852,10 @@ mod tests { ", ); let err = super::parse("file.md", &source).expect_err("Should fail to parse"); - assert_eq!(err.to_string(), "Trailing code-block metadata is not supported. Only the code block language can be specified."); + assert_eq!( + err.to_string(), + "Trailing code-block metadata is not supported. Only the code block language can be specified." + ); } #[test] @@ -1834,7 +1967,7 @@ mod tests { let err = super::parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "Unknown HTML comment `snpshotttt-digggggnosstic` -- possibly a `snapshot-diagnostics` typo? \ + "Unknown HTML comment `snpshotttt-digggggnosstic` -- possibly a typo? \ (Add to `HTML_COMMENT_ALLOWLIST` if this is a false positive)", ); } diff --git a/crates/red_knot_vendored/.gitignore b/crates/ty_vendored/.gitignore similarity index 100% rename from crates/red_knot_vendored/.gitignore rename to crates/ty_vendored/.gitignore diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml new file mode 100644 index 0000000000000..0461b834c3ccc --- /dev/null +++ b/crates/ty_vendored/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "ty_vendored" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +ruff_db = { workspace = true } +static_assertions = { workspace = true } +zip = { workspace = true } + +[build-dependencies] +path-slash = { workspace = true } +walkdir = { workspace = true } +zip = { workspace = true, features = ["zstd", "deflate"] } + +[dev-dependencies] +walkdir = { workspace = true } + +[features] +zstd = ["zip/zstd"] +deflate = ["zip/deflate"] + +[lints] +workspace = true + diff --git a/crates/ty_vendored/README.md b/crates/ty_vendored/README.md new file mode 100644 index 0000000000000..40ab2fc04f18f --- /dev/null +++ b/crates/ty_vendored/README.md @@ -0,0 +1,5 @@ +# Vendored types for the stdlib + +This crate vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/ty_vendored/vendor/typeshed`. The file `crates/ty_vendored/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to. + +The typeshed stubs are updated every two weeks via an automated PR using the `sync_typeshed.yaml` workflow in the `.github/workflows` directory. This workflow can also be triggered at any time via [workflow dispatch](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow#running-a-workflow). diff --git a/crates/ty_vendored/build.rs b/crates/ty_vendored/build.rs new file mode 100644 index 0000000000000..5c65c09848517 --- /dev/null +++ b/crates/ty_vendored/build.rs @@ -0,0 +1,103 @@ +//! Build script to package our vendored typeshed files +//! into a zip archive that can be included in the Ruff binary. +//! +//! This script should be automatically run at build time +//! whenever the script itself changes, or whenever any files +//! in `crates/ty_vendored/vendor/typeshed` change. + +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use path_slash::PathExt; +use zip::CompressionMethod; +use zip::result::ZipResult; +use zip::write::{FileOptions, ZipWriter}; + +const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed"; +const TY_EXTENSIONS_STUBS: &str = "ty_extensions/ty_extensions.pyi"; +const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip"; + +/// Recursively zip the contents of the entire typeshed directory and patch typeshed +/// on the fly to include the `ty_extensions` module. +/// +/// This routine is adapted from a recipe at +/// +fn write_zipped_typeshed_to(writer: File) -> ZipResult { + let mut zip = ZipWriter::new(writer); + + // Use deflated compression for WASM builds because compiling `zstd-sys` requires clang + // [source](https://github.com/gyscos/zstd-rs/wiki/Compile-for-WASM) which complicates the build + // by a lot. Deflated compression is slower but it shouldn't matter much for the WASM use case + // (WASM itself is already slower than a native build for a specific platform). + // We can't use `#[cfg(...)]` here because the target-arch in a build script is the + // architecture of the system running the build script and not the architecture of the build-target. + // That's why we use the `TARGET` environment variable here. + let method = if cfg!(feature = "zstd") { + CompressionMethod::Zstd + } else if cfg!(feature = "deflate") { + CompressionMethod::Deflated + } else { + CompressionMethod::Stored + }; + + let options = FileOptions::default() + .compression_method(method) + .unix_permissions(0o644); + + for entry in walkdir::WalkDir::new(TYPESHED_SOURCE_DIR) { + let dir_entry = entry.unwrap(); + let absolute_path = dir_entry.path(); + let normalized_relative_path = absolute_path + .strip_prefix(Path::new(TYPESHED_SOURCE_DIR)) + .unwrap() + .to_slash() + .expect("Unexpected non-utf8 typeshed path!"); + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if absolute_path.is_file() { + println!("adding file {absolute_path:?} as {normalized_relative_path:?} ..."); + zip.start_file(&*normalized_relative_path, options)?; + let mut f = File::open(absolute_path)?; + std::io::copy(&mut f, &mut zip).unwrap(); + + // Patch the VERSIONS file to make `ty_extensions` available + if normalized_relative_path == "stdlib/VERSIONS" { + writeln!(&mut zip, "ty_extensions: 3.0-")?; + } + } else if !normalized_relative_path.is_empty() { + // Only if not root! Avoids path spec / warning + // and mapname conversion failed error on unzip + println!("adding dir {absolute_path:?} as {normalized_relative_path:?} ..."); + zip.add_directory(normalized_relative_path, options)?; + } + } + + // Patch typeshed and add the stubs for the `ty_extensions` module + println!("adding file {TY_EXTENSIONS_STUBS} as stdlib/ty_extensions.pyi ..."); + zip.start_file("stdlib/ty_extensions.pyi", options)?; + let mut f = File::open(TY_EXTENSIONS_STUBS)?; + std::io::copy(&mut f, &mut zip).unwrap(); + + zip.finish() +} + +fn main() { + assert!( + Path::new(TYPESHED_SOURCE_DIR).is_dir(), + "Where is typeshed?" + ); + let out_dir = std::env::var("OUT_DIR").unwrap(); + + // N.B. Deliberately using `format!()` instead of `Path::join()` here, + // so that we use `/` as a path separator on all platforms. + // That enables us to load the typeshed zip at compile time in `module.rs` + // (otherwise we'd have to dynamically determine the exact path to the typeshed zip + // based on the default path separator for the specific platform we're on, + // which can't be done at compile time.) + let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}"); + + let zipped_typeshed_file = File::create(zipped_typeshed_location).unwrap(); + write_zipped_typeshed_to(zipped_typeshed_file).unwrap(); +} diff --git a/crates/ty_vendored/src/lib.rs b/crates/ty_vendored/src/lib.rs new file mode 100644 index 0000000000000..e6fd7e0a0b314 --- /dev/null +++ b/crates/ty_vendored/src/lib.rs @@ -0,0 +1,104 @@ +use ruff_db::vendored::VendoredFileSystem; +use std::sync::LazyLock; + +/// The source commit of the vendored typeshed. +pub const SOURCE_COMMIT: &str = + include_str!("../../../crates/ty_vendored/vendor/typeshed/source_commit.txt").trim_ascii_end(); + +static_assertions::const_assert_eq!(SOURCE_COMMIT.len(), 40); + +// The file path here is hardcoded in this crate's `build.rs` script. +// Luckily this crate will fail to build if this file isn't available at build time. +static TYPESHED_ZIP_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip")); + +pub fn file_system() -> &'static VendoredFileSystem { + static VENDORED_TYPESHED_STUBS: LazyLock = + LazyLock::new(|| VendoredFileSystem::new_static(TYPESHED_ZIP_BYTES).unwrap()); + &VENDORED_TYPESHED_STUBS +} + +#[cfg(test)] +mod tests { + use std::io::{self, Read}; + use std::path::Path; + + use ruff_db::vendored::VendoredPath; + + use super::*; + + #[test] + fn typeshed_zip_created_at_build_time() { + let mut typeshed_zip_archive = + zip::ZipArchive::new(io::Cursor::new(TYPESHED_ZIP_BYTES)).unwrap(); + + let mut functools_module_stub = typeshed_zip_archive + .by_name("stdlib/functools.pyi") + .unwrap(); + assert!(functools_module_stub.is_file()); + + let mut functools_module_stub_source = String::new(); + functools_module_stub + .read_to_string(&mut functools_module_stub_source) + .unwrap(); + + assert!(functools_module_stub_source.contains("def update_wrapper(")); + } + + #[test] + fn typeshed_vfs_consistent_with_vendored_stubs() { + let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap(); + let vendored_typeshed_stubs = file_system(); + + let mut empty_iterator = true; + for entry in walkdir::WalkDir::new(&vendored_typeshed_dir).min_depth(1) { + empty_iterator = false; + let entry = entry.unwrap(); + let absolute_path = entry.path(); + let file_type = entry.file_type(); + + let relative_path = absolute_path + .strip_prefix(&vendored_typeshed_dir) + .unwrap_or_else(|_| { + panic!("Expected {absolute_path:?} to be a child of {vendored_typeshed_dir:?}") + }); + + let vendored_path = <&VendoredPath>::try_from(relative_path) + .unwrap_or_else(|_| panic!("Expected {relative_path:?} to be valid UTF-8")); + + assert!( + vendored_typeshed_stubs.exists(vendored_path), + "Expected {vendored_path:?} to exist in the `VendoredFileSystem`! + + Vendored file system: + + {vendored_typeshed_stubs:#?} + " + ); + + let vendored_path_kind = vendored_typeshed_stubs + .metadata(vendored_path) + .unwrap_or_else(|_| { + panic!( + "Expected metadata for {vendored_path:?} to be retrievable from the `VendoredFileSystem! + + Vendored file system: + + {vendored_typeshed_stubs:#?} + " + ) + }) + .kind(); + + assert_eq!( + vendored_path_kind.is_directory(), + file_type.is_dir(), + "{vendored_path:?} had type {vendored_path_kind:?}, inconsistent with fs path {relative_path:?}: {file_type:?}" + ); + } + + assert!( + !empty_iterator, + "Expected there to be at least one file or directory in the vendored typeshed stubs!" + ); + } +} diff --git a/crates/ty_vendored/ty_extensions/README.md b/crates/ty_vendored/ty_extensions/README.md new file mode 100644 index 0000000000000..594f6c98faea0 --- /dev/null +++ b/crates/ty_vendored/ty_extensions/README.md @@ -0,0 +1,2 @@ +The `build.rs` copies the `ty_extensions.pyi` file in this directory into +the `vendor/typeshed/stdlib` directory. diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi new file mode 100644 index 0000000000000..158f0dcbf445d --- /dev/null +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -0,0 +1,59 @@ +from typing import Any, LiteralString, _SpecialForm + +# Special operations +def static_assert(condition: object, msg: LiteralString | None = None) -> None: ... + +# Types +Unknown = object() +AlwaysTruthy = object() +AlwaysFalsy = object() + +# Special forms +Not: _SpecialForm +Intersection: _SpecialForm +TypeOf: _SpecialForm +CallableTypeOf: _SpecialForm + +# ty treats annotations of `float` to mean `float | int`, and annotations of `complex` +# to mean `complex | float | int`. This is to support a typing-system special case [1]. +# We therefore provide `JustFloat` and `JustComplex` to represent the "bare" `float` and +# `complex` types, respectively. +# +# [1]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex +type JustFloat = TypeOf[1.0] +type JustComplex = TypeOf[1.0j] + +# Predicates on types +# +# Ideally, these would be annotated using `TypeForm`, but that has not been +# standardized yet (https://peps.python.org/pep-0747). +def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ... +def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ... +def is_assignable_to(type_target: Any, type_source: Any) -> bool: ... +def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ... +def is_singleton(type: Any) -> bool: ... +def is_single_valued(type: Any) -> bool: ... + +# Returns the generic context of a type as a tuple of typevars, or `None` if the +# type is not generic. +def generic_context(type: Any) -> Any: ... + +# Returns the `__all__` names of a module as a tuple of sorted strings, or `None` if +# either the module does not have `__all__` or it has invalid elements. +def dunder_all_names(module: Any) -> Any: ... + +# Returns the type that's an upper bound of materializing the given (gradual) type. +def top_materialization(type: Any) -> Any: ... + +# Returns the type that's a lower bound of materializing the given (gradual) type. +def bottom_materialization(type: Any) -> Any: ... + +# Returns a tuple of all members of the given object, similar to `dir(obj)` and +# `inspect.getmembers(obj)`, with at least the following differences: +# +# * `dir` and `inspect.getmembers` may use runtime mutable state to construct +# the list of attributes returned. In contrast, this routine is limited to +# static information only. +# * `dir` will respect an object's `__dir__` implementation, if present, but +# this method (currently) does not. +def all_members(obj: Any) -> tuple[str, ...]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/LICENSE b/crates/ty_vendored/vendor/typeshed/LICENSE similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/LICENSE rename to crates/ty_vendored/vendor/typeshed/LICENSE diff --git a/crates/ty_vendored/vendor/typeshed/README.md b/crates/ty_vendored/vendor/typeshed/README.md new file mode 100644 index 0000000000000..ee09529b967ac --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/README.md @@ -0,0 +1,124 @@ +# typeshed + +[![Tests](https://github.com/python/typeshed/actions/workflows/tests.yml/badge.svg)](https://github.com/python/typeshed/actions/workflows/tests.yml) +[![Chat at https://gitter.im/python/typing](https://badges.gitter.im/python/typing.svg)](https://gitter.im/python/typing?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Pull Requests Welcome](https://img.shields.io/badge/pull%20requests-welcome-brightgreen.svg)](https://github.com/python/typeshed/blob/main/CONTRIBUTING.md) + +## About + +Typeshed contains external type annotations for the Python standard library +and Python builtins, as well as third party packages as contributed by +people external to those projects. + +This data can e.g. be used for static analysis, type checking, type inference, +and autocompletion. + +For information on how to use typeshed, read below. Information for +contributors can be found in [CONTRIBUTING.md](CONTRIBUTING.md). **Please read +it before submitting pull requests; do not report issues with annotations to +the project the stubs are for, but instead report them here to typeshed.** + +Further documentation on stub files, typeshed, and Python's typing system in +general, can also be found at https://typing.readthedocs.io/en/latest/. + +Typeshed supports Python versions 3.9 to 3.14. + +## Using + +If you're just using a type checker ([mypy](https://github.com/python/mypy/), +[pyright](https://github.com/microsoft/pyright), +[pytype](https://github.com/google/pytype/), PyCharm, ...), as opposed to +developing it, you don't need to interact with the typeshed repo at +all: a copy of standard library part of typeshed is bundled with type checkers. +And type stubs for third party packages and modules you are using can +be installed from PyPI. For example, if you are using `html5lib` and `requests`, +you can install the type stubs using + +```bash +$ pip install types-html5lib types-requests +``` + +These PyPI packages follow [PEP 561](http://www.python.org/dev/peps/pep-0561/) +and are automatically released (up to once a day) by +[typeshed internal machinery](https://github.com/typeshed-internal/stub_uploader). + +Type checkers should be able to use these stub packages when installed. For more +details, see the documentation for your type checker. + +### Package versioning for third-party stubs + +Version numbers of third-party stub packages consist of at least four parts. +All parts of the stub version, except for the last part, correspond to the +version of the runtime package being stubbed. For example, if the `types-foo` +package has version `1.2.0.20240309`, this guarantees that the `types-foo` package +contains stubs targeted against `foo==1.2.*` and tested against the latest +version of `foo` matching that specifier. In this example, the final element +of the version number (20240309) indicates that the stub package was pushed on +March 9, 2024. + +At typeshed, we try to keep breaking changes to a minimum. However, due to the +nature of stubs, any version bump can introduce changes that might make your +code fail to type check. + +There are several strategies available for specifying the version of a stubs +package you're using, each with its own tradeoffs: + +1. Use the same bounds that you use for the package being stubbed. For example, + if you use `requests>=2.30.0,<2.32`, you can use + `types-requests>=2.30.0,<2.32`. This ensures that the stubs are compatible + with the package you are using, but it carries a small risk of breaking + type checking due to changes in the stubs. + + Another risk of this strategy is that stubs often lag behind + the package being stubbed. You might want to force the package being stubbed + to a certain minimum version because it fixes a critical bug, but if + correspondingly updated stubs have not been released, your type + checking results may not be fully accurate. +2. Pin the stubs to a known good version and update the pin from time to time + (either manually, or using a tool such as dependabot or renovate). + + For example, if you use `types-requests==2.31.0.1`, you can have confidence + that upgrading dependencies will not break type checking. However, you will + miss out on improvements in the stubs that could potentially improve type + checking until you update the pin. This strategy also has the risk that the + stubs you are using might become incompatible with the package being stubbed. +3. Don't pin the stubs. This is the option that demands the least work from + you when it comes to updating version pins, and has the advantage that you + will automatically benefit from improved stubs whenever a new version of the + stubs package is released. However, it carries the risk that the stubs + become incompatible with the package being stubbed. + + For example, if a new major version of the package is released, there's a + chance the stubs might be updated to reflect the new version of the runtime + package before you update the package being stubbed. + +You can also switch between the different strategies as needed. For example, +you could default to strategy (1), but fall back to strategy (2) when +a problem arises that can't easily be fixed. + +### The `_typeshed` package + +typeshed includes a package `_typeshed` as part of the standard library. +This package and its submodules contain utility types, but are not +available at runtime. For more information about how to use this package, +[see the `stdlib/_typeshed` directory](https://github.com/python/typeshed/tree/main/stdlib/_typeshed). + +## Discussion + +If you've run into behavior in the type checker that suggests the type +stubs for a given library are incorrect or incomplete, +we want to hear from you! + +Our main forum for discussion is the project's [GitHub issue +tracker](https://github.com/python/typeshed/issues). This is the right +place to start a discussion of any of the above or most any other +topic concerning the project. + +If you have general questions about typing with Python, or you need +a review of your type annotations or stubs outside of typeshed, head over to +[our discussion forum](https://github.com/python/typing/discussions). +For less formal discussion, try the typing chat room on +[gitter.im](https://gitter.im/python/typing). Some typeshed maintainers +are almost always present; feel free to find us there and we're happy +to chat. Substantive technical discussion will be directed to the +issue tracker. diff --git a/crates/ty_vendored/vendor/typeshed/source_commit.txt b/crates/ty_vendored/vendor/typeshed/source_commit.txt new file mode 100644 index 0000000000000..82a3c4c8538c3 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/source_commit.txt @@ -0,0 +1 @@ +f64707592dd3c32f756ddeebd012acb2b072aa0d diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS b/crates/ty_vendored/vendor/typeshed/stdlib/VERSIONS similarity index 96% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS rename to crates/ty_vendored/vendor/typeshed/stdlib/VERSIONS index fec56ce59e36b..8baf207ad7b85 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS +++ b/crates/ty_vendored/vendor/typeshed/stdlib/VERSIONS @@ -28,7 +28,7 @@ _bz2: 3.3- _codecs: 3.0- _collections_abc: 3.3- _compat_pickle: 3.1- -_compression: 3.5- +_compression: 3.5-3.13 _contextvars: 3.7- _csv: 3.0- _ctypes: 3.0- @@ -76,8 +76,10 @@ _warnings: 3.0- _weakref: 3.0- _weakrefset: 3.0- _winapi: 3.3- +_zstd: 3.14- abc: 3.0- aifc: 3.0-3.12 +annotationlib: 3.14- antigravity: 3.0- argparse: 3.0- array: 3.0- @@ -86,12 +88,14 @@ asynchat: 3.0-3.11 asyncio: 3.4- asyncio.exceptions: 3.8- asyncio.format_helpers: 3.7- +asyncio.graph: 3.14- asyncio.mixins: 3.10- asyncio.runners: 3.7- asyncio.staggered: 3.8- asyncio.taskgroups: 3.11- asyncio.threads: 3.9- asyncio.timeouts: 3.11- +asyncio.tools: 3.14- asyncio.trsock: 3.8- asyncore: 3.0-3.11 atexit: 3.0- @@ -117,7 +121,9 @@ collections: 3.0- collections.abc: 3.3- colorsys: 3.0- compileall: 3.0- +compression: 3.14- concurrent: 3.2- +concurrent.futures.interpreter: 3.14- configparser: 3.0- contextlib: 3.0- contextvars: 3.7- @@ -226,6 +232,7 @@ os: 3.0- ossaudiodev: 3.0-3.12 parser: 3.0-3.9 pathlib: 3.4- +pathlib.types: 3.14- pdb: 3.0- pickle: 3.0- pickletools: 3.0- @@ -278,6 +285,7 @@ ssl: 3.0- stat: 3.0- statistics: 3.4- string: 3.0- +string.templatelib: 3.14- stringprep: 3.0- struct: 3.0- subprocess: 3.0- diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/__future__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/__future__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/__future__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/__future__.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi new file mode 100644 index 0000000000000..5b0f74feb261b --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi @@ -0,0 +1 @@ +def __getattr__(name: str): ... # incomplete module diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ast.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_ast.pyi index bc0ebd9d8a0f8..00c6b357f7d80 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_ast.pyi @@ -111,13 +111,20 @@ from ast import ( from typing import Literal if sys.version_info >= (3, 12): - from ast import ParamSpec as ParamSpec, TypeVar as TypeVar, TypeVarTuple as TypeVarTuple, type_param as type_param + from ast import ( + ParamSpec as ParamSpec, + TypeAlias as TypeAlias, + TypeVar as TypeVar, + TypeVarTuple as TypeVarTuple, + type_param as type_param, + ) if sys.version_info >= (3, 11): from ast import TryStar as TryStar if sys.version_info >= (3, 10): from ast import ( + Match as Match, MatchAs as MatchAs, MatchClass as MatchClass, MatchMapping as MatchMapping, diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi index be486fddb12da..ed56f33af93af 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi @@ -103,3 +103,8 @@ def _leave_task(loop: AbstractEventLoop, task: Task[Any]) -> None: ... if sys.version_info >= (3, 12): def current_task(loop: AbstractEventLoop | None = None) -> Task[Any] | None: ... + +if sys.version_info >= (3, 14): + def future_discard_from_awaited_by(future: Future[Any], waiter: Future[Any], /) -> None: ... + def future_add_to_awaited_by(future: Future[Any], waiter: Future[Any], /) -> None: ... + def all_tasks(loop: AbstractEventLoop | None = None) -> set[Task[Any]]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_bisect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_bisect.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_bisect.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_bisect.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_blake2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_blake2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_blake2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_blake2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_bootlocale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_bootlocale.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_bootlocale.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_bootlocale.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_bz2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_bz2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_bz2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_bz2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_codecs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_codecs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_codecs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_codecs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_collections_abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_collections_abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_collections_abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_collections_abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_compression.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_compression.pyi new file mode 100644 index 0000000000000..80d38b4db824b --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_compression.pyi @@ -0,0 +1,27 @@ +# _compression is replaced by compression._common._streams on Python 3.14+ (PEP-784) + +from _typeshed import Incomplete, WriteableBuffer +from collections.abc import Callable +from io import DEFAULT_BUFFER_SIZE, BufferedIOBase, RawIOBase +from typing import Any, Protocol + +BUFFER_SIZE = DEFAULT_BUFFER_SIZE + +class _Reader(Protocol): + def read(self, n: int, /) -> bytes: ... + def seekable(self) -> bool: ... + def seek(self, n: int, /) -> Any: ... + +class BaseStream(BufferedIOBase): ... + +class DecompressReader(RawIOBase): + def __init__( + self, + fp: _Reader, + decomp_factory: Callable[..., Incomplete], + trailing_error: type[Exception] | tuple[type[Exception], ...] = (), + **decomp_args: Any, # These are passed to decomp_factory. + ) -> None: ... + def readinto(self, b: WriteableBuffer) -> int: ... + def read(self, size: int = -1) -> bytes: ... + def seek(self, offset: int, whence: int = 0) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_contextvars.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_contextvars.pyi similarity index 87% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_contextvars.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_contextvars.pyi index 33df799a768c3..e2e2e4df9d086 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_contextvars.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_contextvars.pyi @@ -1,5 +1,6 @@ +import sys from collections.abc import Callable, Iterator, Mapping -from types import GenericAlias +from types import GenericAlias, TracebackType from typing import Any, ClassVar, Generic, TypeVar, final, overload from typing_extensions import ParamSpec, Self @@ -35,6 +36,11 @@ class Token(Generic[_T]): MISSING: ClassVar[object] __hash__: ClassVar[None] # type: ignore[assignment] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + if sys.version_info >= (3, 14): + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None + ) -> None: ... def copy_context() -> Context: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_csv.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_csv.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_csv.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_csv.pyi index aa9fc538417e3..efe9ad69bd31d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_csv.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_csv.pyi @@ -2,7 +2,7 @@ import csv import sys from _typeshed import SupportsWrite from collections.abc import Iterable -from typing import Any, Final, type_check_only +from typing import Any, Final, Literal, type_check_only from typing_extensions import Self, TypeAlias __version__: Final[str] @@ -15,9 +15,10 @@ if sys.version_info >= (3, 12): QUOTE_STRINGS: Final = 4 QUOTE_NOTNULL: Final = 5 -# Ideally this would be `QUOTE_ALL | QUOTE_MINIMAL | QUOTE_NONE | QUOTE_NONNUMERIC` -# However, using literals in situations like these can cause false-positives (see #7258) -_QuotingType: TypeAlias = int +if sys.version_info >= (3, 12): + _QuotingType: TypeAlias = Literal[0, 1, 2, 3, 4, 5] +else: + _QuotingType: TypeAlias = Literal[0, 1, 2, 3] class Error(Exception): ... @@ -89,6 +90,7 @@ else: def writer( csvfile: SupportsWrite[str], + /, dialect: _DialectLike = "excel", *, delimiter: str = ",", @@ -102,6 +104,7 @@ def writer( ) -> _writer: ... def reader( csvfile: Iterable[str], + /, dialect: _DialectLike = "excel", *, delimiter: str = ",", diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ctypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_ctypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi index 4cbb030bb1362..35c77619d4749 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ctypes.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi @@ -75,6 +75,8 @@ class _CData: _objects: Mapping[Any, int] | None def __buffer__(self, flags: int, /) -> memoryview: ... def __ctypes_from_outparam__(self, /) -> Self: ... + if sys.version_info >= (3, 14): + __pointer_type__: type # this is a union of all the subclasses of _CData, which is useful because of # the methods that are present on each of those subclasses which are not present @@ -131,18 +133,23 @@ class _Pointer(_PointerLike, _CData, Generic[_CT], metaclass=_PyCPointerType): def __getitem__(self, key: slice, /) -> list[Any]: ... def __setitem__(self, key: int, value: Any, /) -> None: ... -@overload -def POINTER(type: None, /) -> type[c_void_p]: ... -@overload -def POINTER(type: type[_CT], /) -> type[_Pointer[_CT]]: ... -def pointer(obj: _CT, /) -> _Pointer[_CT]: ... +if sys.version_info < (3, 14): + @overload + def POINTER(type: None, /) -> type[c_void_p]: ... + @overload + def POINTER(type: type[_CT], /) -> type[_Pointer[_CT]]: ... + def pointer(obj: _CT, /) -> _Pointer[_CT]: ... # This class is not exposed. It calls itself _ctypes.CArgObject. @final @type_check_only class _CArgObject: ... -def byref(obj: _CData | _CDataType, offset: int = ...) -> _CArgObject: ... +if sys.version_info >= (3, 14): + def byref(obj: _CData | _CDataType, offset: int = 0, /) -> _CArgObject: ... + +else: + def byref(obj: _CData | _CDataType, offset: int = 0) -> _CArgObject: ... _ECT: TypeAlias = Callable[[_CData | _CDataType | None, CFuncPtr, tuple[_CData | _CDataType, ...]], _CDataType] _PF: TypeAlias = tuple[int] | tuple[int, str | None] | tuple[int, str | None, Any] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_curses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_curses.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi index d7820c72c090d..f21a9ca602708 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_curses.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi @@ -304,6 +304,9 @@ def has_colors() -> bool: ... if sys.version_info >= (3, 10): def has_extended_color_support() -> bool: ... +if sys.version_info >= (3, 14): + def assume_default_colors(fg: int, bg: int, /) -> None: ... + def has_ic() -> bool: ... def has_il() -> bool: ... def has_key(key: int, /) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_curses_panel.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_curses_panel.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_dbm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_dbm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_dbm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_dbm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_decimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_decimal.pyi similarity index 92% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_decimal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_decimal.pyi index 06c0197dcf07c..fd0e6e6ac0914 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_decimal.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_decimal.pyi @@ -41,6 +41,8 @@ MAX_EMAX: Final[int] MAX_PREC: Final[int] MIN_EMIN: Final[int] MIN_ETINY: Final[int] +if sys.version_info >= (3, 14): + IEEE_CONTEXT_MAX_BITS: Final[int] def setcontext(context: Context, /) -> None: ... def getcontext() -> Context: ... @@ -62,6 +64,9 @@ if sys.version_info >= (3, 11): else: def localcontext(ctx: Context | None = None) -> _ContextManager: ... +if sys.version_info >= (3, 14): + def IEEEContext(bits: int, /) -> Context: ... + DefaultContext: Context BasicContext: Context ExtendedContext: Context diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi similarity index 94% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi index 386cf20808e4f..edad50a8d8583 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi @@ -36,7 +36,10 @@ def spec_from_file_location( loader: LoaderProtocol | None = None, submodule_search_locations: list[str] | None = ..., ) -> importlib.machinery.ModuleSpec | None: ... - +@deprecated( + "Deprecated as of Python 3.6: Use site configuration instead. " + "Future versions of Python may not enable this finder by default." +) class WindowsRegistryFinder(importlib.abc.MetaPathFinder): if sys.version_info < (3, 12): @classmethod @@ -118,6 +121,13 @@ class FileLoader: class SourceFileLoader(importlib.abc.FileLoader, FileLoader, importlib.abc.SourceLoader, SourceLoader): # type: ignore[misc] # incompatible method arguments in base classes def set_data(self, path: str, data: ReadableBuffer, *, _mode: int = 0o666) -> None: ... def path_stats(self, path: str) -> Mapping[str, Any]: ... + def source_to_code( # type: ignore[override] # incompatible with InspectLoader.source_to_code + self, + data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive, + path: ReadableBuffer | StrPath, + *, + _optimize: int = -1, + ) -> types.CodeType: ... class SourcelessFileLoader(importlib.abc.FileLoader, FileLoader, _LoaderBasics): def get_code(self, fullname: str) -> types.CodeType | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_gdbm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_gdbm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_gdbm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_gdbm.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_hashlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_hashlib.pyi new file mode 100644 index 0000000000000..8b7ef52cdffdf --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_hashlib.pyi @@ -0,0 +1,126 @@ +import sys +from _typeshed import ReadableBuffer +from collections.abc import Callable +from types import ModuleType +from typing import AnyStr, Protocol, final, overload, type_check_only +from typing_extensions import Self, TypeAlias + +_DigestMod: TypeAlias = str | Callable[[], _HashObject] | ModuleType | None + +openssl_md_meth_names: frozenset[str] + +@type_check_only +class _HashObject(Protocol): + @property + def digest_size(self) -> int: ... + @property + def block_size(self) -> int: ... + @property + def name(self) -> str: ... + def copy(self) -> Self: ... + def digest(self) -> bytes: ... + def hexdigest(self) -> str: ... + def update(self, obj: ReadableBuffer, /) -> None: ... + +class HASH: + @property + def digest_size(self) -> int: ... + @property + def block_size(self) -> int: ... + @property + def name(self) -> str: ... + def copy(self) -> Self: ... + def digest(self) -> bytes: ... + def hexdigest(self) -> str: ... + def update(self, obj: ReadableBuffer, /) -> None: ... + +if sys.version_info >= (3, 10): + class UnsupportedDigestmodError(ValueError): ... + +class HASHXOF(HASH): + def digest(self, length: int) -> bytes: ... # type: ignore[override] + def hexdigest(self, length: int) -> str: ... # type: ignore[override] + +@final +class HMAC: + @property + def digest_size(self) -> int: ... + @property + def block_size(self) -> int: ... + @property + def name(self) -> str: ... + def copy(self) -> Self: ... + def digest(self) -> bytes: ... + def hexdigest(self) -> str: ... + def update(self, msg: ReadableBuffer) -> None: ... + +@overload +def compare_digest(a: ReadableBuffer, b: ReadableBuffer, /) -> bool: ... +@overload +def compare_digest(a: AnyStr, b: AnyStr, /) -> bool: ... +def get_fips_mode() -> int: ... +def hmac_new(key: bytes | bytearray, msg: ReadableBuffer = b"", digestmod: _DigestMod = None) -> HMAC: ... + +if sys.version_info >= (3, 13): + def new( + name: str, data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_md5( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha1( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha224( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha256( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha384( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha512( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha3_224( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha3_256( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha3_384( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_sha3_512( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASH: ... + def openssl_shake_128( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASHXOF: ... + def openssl_shake_256( + data: ReadableBuffer = b"", *, usedforsecurity: bool = True, string: ReadableBuffer | None = None + ) -> HASHXOF: ... + +else: + def new(name: str, string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_md5(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha1(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha224(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha384(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha512(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha3_224(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha3_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha3_384(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_sha3_512(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASH: ... + def openssl_shake_128(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ... + def openssl_shake_256(string: ReadableBuffer = b"", *, usedforsecurity: bool = True) -> HASHXOF: ... + +def hmac_digest(key: bytes | bytearray, msg: ReadableBuffer, digest: str) -> bytes: ... +def pbkdf2_hmac( + hash_name: str, password: ReadableBuffer, salt: ReadableBuffer, iterations: int, dklen: int | None = None +) -> bytes: ... +def scrypt( + password: ReadableBuffer, *, salt: ReadableBuffer, n: int, r: int, p: int, maxmem: int = 0, dklen: int = 64 +) -> bytes: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_heapq.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_heapq.pyi new file mode 100644 index 0000000000000..3363fbcd7e740 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_heapq.pyi @@ -0,0 +1,19 @@ +import sys +from typing import Any, Final, TypeVar + +_T = TypeVar("_T") # list items must be comparable + +__about__: Final[str] + +def heapify(heap: list[Any], /) -> None: ... # list items must be comparable +def heappop(heap: list[_T], /) -> _T: ... +def heappush(heap: list[_T], item: _T, /) -> None: ... +def heappushpop(heap: list[_T], item: _T, /) -> _T: ... +def heapreplace(heap: list[_T], item: _T, /) -> _T: ... + +if sys.version_info >= (3, 14): + def heapify_max(heap: list[Any], /) -> None: ... # list items must be comparable + def heappop_max(heap: list[_T], /) -> _T: ... + def heappush_max(heap: list[_T], item: _T, /) -> None: ... + def heappushpop_max(heap: list[_T], item: _T, /) -> _T: ... + def heapreplace_max(heap: list[_T], item: _T, /) -> _T: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_imp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_imp.pyi similarity index 94% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_imp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_imp.pyi index de3549a91da59..c12c26d08ba2a 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_imp.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_imp.pyi @@ -5,6 +5,8 @@ from importlib.machinery import ModuleSpec from typing import Any check_hash_based_pycs: str +if sys.version_info >= (3, 14): + pyc_magic_number_token: int def source_hash(key: int, source: ReadableBuffer) -> bytes: ... def create_builtin(spec: ModuleSpec, /) -> types.ModuleType: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_interpchannels.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_interpchannels.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_interpchannels.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_interpchannels.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_interpqueues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_interpqueues.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_interpqueues.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_interpqueues.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_interpreters.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_interpreters.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_interpreters.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_interpreters.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_io.pyi similarity index 85% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_io.pyi index 54efd31997603..c77d75287c25a 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_io.pyi @@ -88,9 +88,36 @@ class BytesIO(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] def readlines(self, size: int | None = None, /) -> list[bytes]: ... def seek(self, pos: int, whence: int = 0, /) -> int: ... -class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes - raw: RawIOBase - def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ... +class _BufferedReaderStream(Protocol): + def read(self, n: int = ..., /) -> bytes: ... + # Optional: def readall(self) -> bytes: ... + def readinto(self, b: memoryview, /) -> int | None: ... + def seek(self, pos: int, whence: int, /) -> int: ... + def tell(self) -> int: ... + def truncate(self, size: int, /) -> int: ... + def flush(self) -> object: ... + def close(self) -> object: ... + @property + def closed(self) -> bool: ... + def readable(self) -> bool: ... + def seekable(self) -> bool: ... + + # The following methods just pass through to the underlying stream. Since + # not all streams support them, they are marked as optional here, and will + # raise an AttributeError if called on a stream that does not support them. + + # @property + # def name(self) -> Any: ... # Type is inconsistent between the various I/O types. + # @property + # def mode(self) -> str: ... + # def fileno(self) -> int: ... + # def isatty(self) -> bool: ... + +_BufferedReaderStreamT = TypeVar("_BufferedReaderStreamT", bound=_BufferedReaderStream, default=_BufferedReaderStream) + +class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO, Generic[_BufferedReaderStreamT]): # type: ignore[misc] # incompatible definitions of methods in the base classes + raw: _BufferedReaderStreamT + def __init__(self, raw: _BufferedReaderStreamT, buffer_size: int = 8192) -> None: ... def peek(self, size: int = 0, /) -> bytes: ... def seek(self, target: int, whence: int = 0, /) -> int: ... def truncate(self, pos: int | None = None, /) -> int: ... @@ -111,8 +138,8 @@ class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore def peek(self, size: int = 0, /) -> bytes: ... def truncate(self, pos: int | None = None, /) -> int: ... -class BufferedRWPair(BufferedIOBase, _BufferedIOBase): - def __init__(self, reader: RawIOBase, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ... +class BufferedRWPair(BufferedIOBase, _BufferedIOBase, Generic[_BufferedReaderStreamT]): + def __init__(self, reader: _BufferedReaderStreamT, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ... def peek(self, size: int = 0, /) -> bytes: ... class _TextIOBase(_IOBase): @@ -131,8 +158,7 @@ class _TextIOBase(_IOBase): @type_check_only class _WrappedBuffer(Protocol): # "name" is wrapped by TextIOWrapper. Its type is inconsistent between - # the various I/O types, see the comments on TextIOWrapper.name and - # TextIO.name. + # the various I/O types. @property def name(self) -> Any: ... @property diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_json.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_json.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_json.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_json.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_locale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_locale.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_locale.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_locale.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_lsprof.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_lsprof.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_lsprof.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_lsprof.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_lzma.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_lzma.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_lzma.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_lzma.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_markupbase.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_markupbase.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_markupbase.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_markupbase.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_msi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_msi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_msi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_msi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_operator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_operator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_operator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_operator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_osx_support.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_osx_support.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_osx_support.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_osx_support.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_pickle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_pickle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_pickle.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi new file mode 100644 index 0000000000000..dd74e316e8990 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi @@ -0,0 +1,59 @@ +import sys +from _typeshed import StrOrBytesPath +from collections.abc import Callable, Sequence +from typing import SupportsIndex + +if sys.platform != "win32": + if sys.version_info >= (3, 14): + def fork_exec( + args: Sequence[StrOrBytesPath] | None, + executable_list: Sequence[bytes], + close_fds: bool, + pass_fds: tuple[int, ...], + cwd: str, + env: Sequence[bytes] | None, + p2cread: int, + p2cwrite: int, + c2pread: int, + c2pwrite: int, + errread: int, + errwrite: int, + errpipe_read: int, + errpipe_write: int, + restore_signals: int, + call_setsid: int, + pgid_to_set: int, + gid: SupportsIndex | None, + extra_groups: list[int] | None, + uid: SupportsIndex | None, + child_umask: int, + preexec_fn: Callable[[], None], + /, + ) -> int: ... + else: + def fork_exec( + args: Sequence[StrOrBytesPath] | None, + executable_list: Sequence[bytes], + close_fds: bool, + pass_fds: tuple[int, ...], + cwd: str, + env: Sequence[bytes] | None, + p2cread: int, + p2cwrite: int, + c2pread: int, + c2pwrite: int, + errread: int, + errwrite: int, + errpipe_read: int, + errpipe_write: int, + restore_signals: bool, + call_setsid: bool, + pgid_to_set: int, + gid: SupportsIndex | None, + extra_groups: list[int] | None, + uid: SupportsIndex | None, + child_umask: int, + preexec_fn: Callable[[], None], + allow_vfork: bool, + /, + ) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_py_abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_py_abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_py_abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_py_abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_pydecimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_pydecimal.pyi similarity index 89% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_pydecimal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_pydecimal.pyi index faff626ac0bae..a6723f749da6d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_pydecimal.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_pydecimal.pyi @@ -1,5 +1,6 @@ # This is a slight lie, the implementations aren't exactly identical # However, in all likelihood, the differences are inconsequential +import sys from _decimal import * __all__ = [ @@ -41,3 +42,6 @@ __all__ = [ "HAVE_THREADS", "HAVE_CONTEXTVAR", ] + +if sys.version_info >= (3, 14): + __all__ += ["IEEEContext", "IEEE_CONTEXT_MAX_BITS"] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_queue.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_queue.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_queue.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_queue.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_random.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_random.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_random.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_random.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_socket.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_socket.pyi similarity index 96% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_socket.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_socket.pyi index 5399f4edf0106..41fdce87ec14d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_socket.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_socket.pyi @@ -229,6 +229,29 @@ if sys.platform != "win32": IP_RECVOPTS: int IP_RECVRETOPTS: int IP_RETOPTS: int +if sys.version_info >= (3, 13) and sys.platform == "linux": + CAN_RAW_ERR_FILTER: int +if sys.version_info >= (3, 14): + IP_RECVTTL: int + + if sys.platform == "win32" or sys.platform == "linux": + IPV6_RECVERR: int + IP_RECVERR: int + SO_ORIGINAL_DST: int + + if sys.platform == "win32": + SOL_RFCOMM: int + SO_BTH_ENCRYPT: int + SO_BTH_MTU: int + SO_BTH_MTU_MAX: int + SO_BTH_MTU_MIN: int + TCP_QUICKACK: int + + if sys.platform == "linux": + IP_FREEBIND: int + IP_RECVORIGDSTADDR: int + VMADDR_CID_LOCAL: int + if sys.platform != "win32" and sys.platform != "darwin": IP_TRANSPARENT: int if sys.platform != "win32" and sys.platform != "darwin" and sys.version_info >= (3, 11): @@ -829,6 +852,11 @@ if sys.platform != "win32": def if_nameindex() -> list[tuple[int, str]]: ... def if_nametoindex(oname: str, /) -> int: ... -def if_indextoname(index: int, /) -> str: ... + +if sys.version_info >= (3, 14): + def if_indextoname(if_index: int, /) -> str: ... + +else: + def if_indextoname(index: int, /) -> str: ... CAPI: CapsuleType diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_sqlite3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_sqlite3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ssl.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_ssl.pyi index e39ab5eb6de80..7ab880e4def74 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_ssl.pyi @@ -283,6 +283,8 @@ HAS_TLSv1: bool HAS_TLSv1_1: bool HAS_TLSv1_2: bool HAS_TLSv1_3: bool +if sys.version_info >= (3, 14): + HAS_PHA: bool # version info OPENSSL_VERSION_NUMBER: int diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_stat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_stat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_stat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_stat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_struct.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_struct.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_thread.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi similarity index 96% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_thread.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi index 378ac24237572..9cfbe55b4fe31 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_thread.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi @@ -18,6 +18,8 @@ class RLock: def release(self) -> None: ... __enter__ = acquire def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ... + if sys.version_info >= (3, 14): + def locked(self) -> bool: ... if sys.version_info >= (3, 13): @final @@ -105,6 +107,9 @@ _excepthook: Callable[[_ExceptHookArgs], Any] if sys.version_info >= (3, 12): def daemon_threads_allowed() -> bool: ... +if sys.version_info >= (3, 14): + def set_name(name: str) -> None: ... + class _local: def __getattribute__(self, name: str, /) -> Any: ... def __setattr__(self, name: str, value: Any, /) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_threading_local.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_threading_local.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_tkinter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_tkinter.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_tkinter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_tkinter.pyi index 4206a2114f954..08eb00ca442bf 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_tkinter.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_tkinter.pyi @@ -77,7 +77,7 @@ class TkappType: def globalgetvar(self, *args, **kwargs): ... def globalsetvar(self, *args, **kwargs): ... def globalunsetvar(self, *args, **kwargs): ... - def interpaddr(self): ... + def interpaddr(self) -> int: ... def loadtk(self) -> None: ... def mainloop(self, threshold: int = 0, /): ... def quit(self): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/README.md b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/README.md similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/README.md rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/README.md diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi new file mode 100644 index 0000000000000..f322244016dd0 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi @@ -0,0 +1,377 @@ +# Utility types for typeshed +# +# See the README.md file in this directory for more information. + +import sys +from collections.abc import Awaitable, Callable, Iterable, Sequence, Set as AbstractSet, Sized +from dataclasses import Field +from os import PathLike +from types import FrameType, TracebackType +from typing import ( + Any, + AnyStr, + ClassVar, + Final, + Generic, + Literal, + Protocol, + SupportsFloat, + SupportsIndex, + SupportsInt, + TypeVar, + final, + overload, +) +from typing_extensions import Buffer, LiteralString, Self as _Self, TypeAlias + +_KT = TypeVar("_KT") +_KT_co = TypeVar("_KT_co", covariant=True) +_KT_contra = TypeVar("_KT_contra", contravariant=True) +_VT = TypeVar("_VT") +_VT_co = TypeVar("_VT_co", covariant=True) +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_T_contra = TypeVar("_T_contra", contravariant=True) + +# Alternative to `typing_extensions.Self`, exclusively for use with `__new__` +# in metaclasses: +# def __new__(cls: type[Self], ...) -> Self: ... +# In other cases, use `typing_extensions.Self`. +Self = TypeVar("Self") # noqa: Y001 + +# covariant version of typing.AnyStr, useful for protocols +AnyStr_co = TypeVar("AnyStr_co", str, bytes, covariant=True) # noqa: Y001 + +# For partially known annotations. Usually, fields where type annotations +# haven't been added are left unannotated, but in some situations this +# isn't possible or a type is already partially known. In cases like these, +# use Incomplete instead of Any as a marker. For example, use +# "Incomplete | None" instead of "Any | None". +Incomplete: TypeAlias = Any # stable + +# To describe a function parameter that is unused and will work with anything. +Unused: TypeAlias = object # stable + +# Marker for return types that include None, but where forcing the user to +# check for None can be detrimental. Sometimes called "the Any trick". See +# CONTRIBUTING.md for more information. +MaybeNone: TypeAlias = Any # stable + +# Used to mark arguments that default to a sentinel value. This prevents +# stubtest from complaining about the default value not matching. +# +# def foo(x: int | None = sentinel) -> None: ... +# +# In cases where the sentinel object is exported and can be used by user code, +# a construct like this is better: +# +# _SentinelType = NewType("_SentinelType", object) +# sentinel: _SentinelType +# def foo(x: int | None | _SentinelType = ...) -> None: ... +sentinel: Any + +# stable +class IdentityFunction(Protocol): + def __call__(self, x: _T, /) -> _T: ... + +# stable +class SupportsNext(Protocol[_T_co]): + def __next__(self) -> _T_co: ... + +# stable +class SupportsAnext(Protocol[_T_co]): + def __anext__(self) -> Awaitable[_T_co]: ... + +# Comparison protocols + +class SupportsDunderLT(Protocol[_T_contra]): + def __lt__(self, other: _T_contra, /) -> bool: ... + +class SupportsDunderGT(Protocol[_T_contra]): + def __gt__(self, other: _T_contra, /) -> bool: ... + +class SupportsDunderLE(Protocol[_T_contra]): + def __le__(self, other: _T_contra, /) -> bool: ... + +class SupportsDunderGE(Protocol[_T_contra]): + def __ge__(self, other: _T_contra, /) -> bool: ... + +class SupportsAllComparisons( + SupportsDunderLT[Any], SupportsDunderGT[Any], SupportsDunderLE[Any], SupportsDunderGE[Any], Protocol +): ... + +SupportsRichComparison: TypeAlias = SupportsDunderLT[Any] | SupportsDunderGT[Any] +SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichComparison) # noqa: Y001 + +# Dunder protocols + +class SupportsAdd(Protocol[_T_contra, _T_co]): + def __add__(self, x: _T_contra, /) -> _T_co: ... + +class SupportsRAdd(Protocol[_T_contra, _T_co]): + def __radd__(self, x: _T_contra, /) -> _T_co: ... + +class SupportsSub(Protocol[_T_contra, _T_co]): + def __sub__(self, x: _T_contra, /) -> _T_co: ... + +class SupportsRSub(Protocol[_T_contra, _T_co]): + def __rsub__(self, x: _T_contra, /) -> _T_co: ... + +class SupportsMul(Protocol[_T_contra, _T_co]): + def __mul__(self, x: _T_contra, /) -> _T_co: ... + +class SupportsRMul(Protocol[_T_contra, _T_co]): + def __rmul__(self, x: _T_contra, /) -> _T_co: ... + +class SupportsDivMod(Protocol[_T_contra, _T_co]): + def __divmod__(self, other: _T_contra, /) -> _T_co: ... + +class SupportsRDivMod(Protocol[_T_contra, _T_co]): + def __rdivmod__(self, other: _T_contra, /) -> _T_co: ... + +# This protocol is generic over the iterator type, while Iterable is +# generic over the type that is iterated over. +class SupportsIter(Protocol[_T_co]): + def __iter__(self) -> _T_co: ... + +# This protocol is generic over the iterator type, while AsyncIterable is +# generic over the type that is iterated over. +class SupportsAiter(Protocol[_T_co]): + def __aiter__(self) -> _T_co: ... + +class SupportsLenAndGetItem(Protocol[_T_co]): + def __len__(self) -> int: ... + def __getitem__(self, k: int, /) -> _T_co: ... + +class SupportsTrunc(Protocol): + def __trunc__(self) -> int: ... + +# Mapping-like protocols + +# stable +class SupportsItems(Protocol[_KT_co, _VT_co]): + def items(self) -> AbstractSet[tuple[_KT_co, _VT_co]]: ... + +# stable +class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): + def keys(self) -> Iterable[_KT]: ... + def __getitem__(self, key: _KT, /) -> _VT_co: ... + +# stable +class SupportsGetItem(Protocol[_KT_contra, _VT_co]): + def __getitem__(self, key: _KT_contra, /) -> _VT_co: ... + +# stable +class SupportsContainsAndGetItem(Protocol[_KT_contra, _VT_co]): + def __contains__(self, x: Any, /) -> bool: ... + def __getitem__(self, key: _KT_contra, /) -> _VT_co: ... + +# stable +class SupportsItemAccess(Protocol[_KT_contra, _VT]): + def __contains__(self, x: Any, /) -> bool: ... + def __getitem__(self, key: _KT_contra, /) -> _VT: ... + def __setitem__(self, key: _KT_contra, value: _VT, /) -> None: ... + def __delitem__(self, key: _KT_contra, /) -> None: ... + +StrPath: TypeAlias = str | PathLike[str] # stable +BytesPath: TypeAlias = bytes | PathLike[bytes] # stable +GenericPath: TypeAlias = AnyStr | PathLike[AnyStr] +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] # stable + +OpenTextModeUpdating: TypeAlias = Literal[ + "r+", + "+r", + "rt+", + "r+t", + "+rt", + "tr+", + "t+r", + "+tr", + "w+", + "+w", + "wt+", + "w+t", + "+wt", + "tw+", + "t+w", + "+tw", + "a+", + "+a", + "at+", + "a+t", + "+at", + "ta+", + "t+a", + "+ta", + "x+", + "+x", + "xt+", + "x+t", + "+xt", + "tx+", + "t+x", + "+tx", +] +OpenTextModeWriting: TypeAlias = Literal["w", "wt", "tw", "a", "at", "ta", "x", "xt", "tx"] +OpenTextModeReading: TypeAlias = Literal["r", "rt", "tr", "U", "rU", "Ur", "rtU", "rUt", "Urt", "trU", "tUr", "Utr"] +OpenTextMode: TypeAlias = OpenTextModeUpdating | OpenTextModeWriting | OpenTextModeReading +OpenBinaryModeUpdating: TypeAlias = Literal[ + "rb+", + "r+b", + "+rb", + "br+", + "b+r", + "+br", + "wb+", + "w+b", + "+wb", + "bw+", + "b+w", + "+bw", + "ab+", + "a+b", + "+ab", + "ba+", + "b+a", + "+ba", + "xb+", + "x+b", + "+xb", + "bx+", + "b+x", + "+bx", +] +OpenBinaryModeWriting: TypeAlias = Literal["wb", "bw", "ab", "ba", "xb", "bx"] +OpenBinaryModeReading: TypeAlias = Literal["rb", "br", "rbU", "rUb", "Urb", "brU", "bUr", "Ubr"] +OpenBinaryMode: TypeAlias = OpenBinaryModeUpdating | OpenBinaryModeReading | OpenBinaryModeWriting + +# stable +class HasFileno(Protocol): + def fileno(self) -> int: ... + +FileDescriptor: TypeAlias = int # stable +FileDescriptorLike: TypeAlias = int | HasFileno # stable +FileDescriptorOrPath: TypeAlias = int | StrOrBytesPath + +# stable +class SupportsRead(Protocol[_T_co]): + def read(self, length: int = ..., /) -> _T_co: ... + +# stable +class SupportsReadline(Protocol[_T_co]): + def readline(self, length: int = ..., /) -> _T_co: ... + +# stable +class SupportsNoArgReadline(Protocol[_T_co]): + def readline(self) -> _T_co: ... + +# stable +class SupportsWrite(Protocol[_T_contra]): + def write(self, s: _T_contra, /) -> object: ... + +# stable +class SupportsFlush(Protocol): + def flush(self) -> object: ... + +# Unfortunately PEP 688 does not allow us to distinguish read-only +# from writable buffers. We use these aliases for readability for now. +# Perhaps a future extension of the buffer protocol will allow us to +# distinguish these cases in the type system. +ReadOnlyBuffer: TypeAlias = Buffer # stable +# Anything that implements the read-write buffer interface. +WriteableBuffer: TypeAlias = Buffer +# Same as WriteableBuffer, but also includes read-only buffer types (like bytes). +ReadableBuffer: TypeAlias = Buffer # stable + +class SliceableBuffer(Buffer, Protocol): + def __getitem__(self, slice: slice, /) -> Sequence[int]: ... + +class IndexableBuffer(Buffer, Protocol): + def __getitem__(self, i: int, /) -> int: ... + +class SupportsGetItemBuffer(SliceableBuffer, IndexableBuffer, Protocol): + def __contains__(self, x: Any, /) -> bool: ... + @overload + def __getitem__(self, slice: slice, /) -> Sequence[int]: ... + @overload + def __getitem__(self, i: int, /) -> int: ... + +class SizedBuffer(Sized, Buffer, Protocol): ... + +ExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType] +OptExcInfo: TypeAlias = ExcInfo | tuple[None, None, None] + +# stable +if sys.version_info >= (3, 10): + from types import NoneType as NoneType +else: + # Used by type checkers for checks involving None (does not exist at runtime) + @final + class NoneType: + def __bool__(self) -> Literal[False]: ... + +# This is an internal CPython type that is like, but subtly different from, a NamedTuple +# Subclasses of this type are found in multiple modules. +# In typeshed, `structseq` is only ever used as a mixin in combination with a fixed-length `Tuple` +# See discussion at #6546 & #6560 +# `structseq` classes are unsubclassable, so are all decorated with `@final`. +class structseq(Generic[_T_co]): + n_fields: Final[int] + n_unnamed_fields: Final[int] + n_sequence_fields: Final[int] + # The first parameter will generally only take an iterable of a specific length. + # E.g. `os.uname_result` takes any iterable of length exactly 5. + # + # The second parameter will accept a dict of any kind without raising an exception, + # but only has any meaning if you supply it a dict where the keys are strings. + # https://github.com/python/typeshed/pull/6560#discussion_r767149830 + def __new__(cls, sequence: Iterable[_T_co], dict: dict[str, Any] = ...) -> _Self: ... + if sys.version_info >= (3, 13): + def __replace__(self, **kwargs: Any) -> _Self: ... + +# Superset of typing.AnyStr that also includes LiteralString +AnyOrLiteralStr = TypeVar("AnyOrLiteralStr", str, bytes, LiteralString) # noqa: Y001 + +# Represents when str or LiteralStr is acceptable. Useful for string processing +# APIs where literalness of return value depends on literalness of inputs +StrOrLiteralStr = TypeVar("StrOrLiteralStr", LiteralString, str) # noqa: Y001 + +# Objects suitable to be passed to sys.setprofile, threading.setprofile, and similar +ProfileFunction: TypeAlias = Callable[[FrameType, str, Any], object] + +# Objects suitable to be passed to sys.settrace, threading.settrace, and similar +TraceFunction: TypeAlias = Callable[[FrameType, str, Any], TraceFunction | None] + +# experimental +# Might not work as expected for pyright, see +# https://github.com/python/typeshed/pull/9362 +# https://github.com/microsoft/pyright/issues/4339 +class DataclassInstance(Protocol): + __dataclass_fields__: ClassVar[dict[str, Field[Any]]] + +# Anything that can be passed to the int/float constructors +if sys.version_info >= (3, 14): + ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex +else: + ConvertibleToInt: TypeAlias = str | ReadableBuffer | SupportsInt | SupportsIndex | SupportsTrunc +ConvertibleToFloat: TypeAlias = str | ReadableBuffer | SupportsFloat | SupportsIndex + +# A few classes updated from Foo(str, Enum) to Foo(StrEnum). This is a convenience so these +# can be accurate on all python versions without getting too wordy +if sys.version_info >= (3, 11): + from enum import StrEnum as StrEnum +else: + from enum import Enum + + class StrEnum(str, Enum): ... + +# Objects that appear in annotations or in type expressions. +# Similar to PEP 747's TypeForm but a little broader. +AnnotationForm: TypeAlias = Any + +if sys.version_info >= (3, 14): + from annotationlib import Format + + # These return annotations, which can be arbitrary objects + AnnotateFunc: TypeAlias = Callable[[Format], dict[str, AnnotationForm]] + EvaluateFunc: TypeAlias = Callable[[Format], AnnotationForm] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi new file mode 100644 index 0000000000000..feb22aae00732 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi @@ -0,0 +1,89 @@ +# Internals used by some type checkers. +# +# Don't use this module directly. It is only for type checkers to use. + +import sys +import typing_extensions +from _collections_abc import dict_items, dict_keys, dict_values +from abc import ABCMeta +from collections.abc import Awaitable, Generator, Iterable, Mapping +from typing import Any, ClassVar, Generic, TypeVar, overload +from typing_extensions import Never + +_T = TypeVar("_T") + +# Used for an undocumented mypy feature. Does not exist at runtime. +promote = object() + +# Fallback type providing methods and attributes that appear on all `TypedDict` types. +# N.B. Keep this mostly in sync with typing_extensions._TypedDict/mypy_extensions._TypedDict +class TypedDictFallback(Mapping[str, object], metaclass=ABCMeta): + __total__: ClassVar[bool] + __required_keys__: ClassVar[frozenset[str]] + __optional_keys__: ClassVar[frozenset[str]] + # __orig_bases__ sometimes exists on <3.12, but not consistently, + # so we only add it to the stub on 3.12+ + if sys.version_info >= (3, 12): + __orig_bases__: ClassVar[tuple[Any, ...]] + if sys.version_info >= (3, 13): + __readonly_keys__: ClassVar[frozenset[str]] + __mutable_keys__: ClassVar[frozenset[str]] + + def copy(self) -> typing_extensions.Self: ... + # Using Never so that only calls using mypy plugin hook that specialize the signature + # can go through. + def setdefault(self, k: Never, default: object) -> object: ... + # Mypy plugin hook for 'pop' expects that 'default' has a type variable type. + def pop(self, k: Never, default: _T = ...) -> object: ... # pyright: ignore[reportInvalidTypeVarUse] + def update(self, m: typing_extensions.Self, /) -> None: ... + def __delitem__(self, k: Never) -> None: ... + def items(self) -> dict_items[str, object]: ... + def keys(self) -> dict_keys[str, object]: ... + def values(self) -> dict_values[str, object]: ... + @overload + def __or__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... + @overload + def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ... + @overload + def __ror__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... + @overload + def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ... + # supposedly incompatible definitions of __or__ and __ior__ + def __ior__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... # type: ignore[misc] + +# Fallback type providing methods and attributes that appear on all `NamedTuple` types. +class NamedTupleFallback(tuple[Any, ...]): + _field_defaults: ClassVar[dict[str, Any]] + _fields: ClassVar[tuple[str, ...]] + # __orig_bases__ sometimes exists on <3.12, but not consistently + # So we only add it to the stub on 3.12+. + if sys.version_info >= (3, 12): + __orig_bases__: ClassVar[tuple[Any, ...]] + + @overload + def __init__(self, typename: str, fields: Iterable[tuple[str, Any]], /) -> None: ... + @overload + @typing_extensions.deprecated( + "Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15" + ) + def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ... + @classmethod + def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... + def _asdict(self) -> dict[str, Any]: ... + def _replace(self, **kwargs: Any) -> typing_extensions.Self: ... + if sys.version_info >= (3, 13): + def __replace__(self, **kwargs: Any) -> typing_extensions.Self: ... + +# Non-default variations to accommodate couroutines, and `AwaitableGenerator` having a 4th type parameter. +_S = TypeVar("_S") +_YieldT_co = TypeVar("_YieldT_co", covariant=True) +_SendT_nd_contra = TypeVar("_SendT_nd_contra", contravariant=True) +_ReturnT_nd_co = TypeVar("_ReturnT_nd_co", covariant=True) + +# The parameters correspond to Generator, but the 4th is the original type. +class AwaitableGenerator( + Awaitable[_ReturnT_nd_co], + Generator[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co], + Generic[_YieldT_co, _SendT_nd_contra, _ReturnT_nd_co, _S], + metaclass=ABCMeta, +): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_warnings.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_warnings.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_warnings.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_warnings.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_weakref.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_weakref.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_weakref.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_weakref.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_weakrefset.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_weakrefset.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_winapi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_winapi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_zstd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_zstd.pyi new file mode 100644 index 0000000000000..2730232528fc2 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_zstd.pyi @@ -0,0 +1,97 @@ +from _typeshed import ReadableBuffer +from collections.abc import Mapping +from compression.zstd import CompressionParameter, DecompressionParameter +from typing import Final, Literal, final +from typing_extensions import Self, TypeAlias + +ZSTD_CLEVEL_DEFAULT: Final = 3 +ZSTD_DStreamOutSize: Final = 131072 +ZSTD_btlazy2: Final = 6 +ZSTD_btopt: Final = 7 +ZSTD_btultra: Final = 8 +ZSTD_btultra2: Final = 9 +ZSTD_c_chainLog: Final = 103 +ZSTD_c_checksumFlag: Final = 201 +ZSTD_c_compressionLevel: Final = 100 +ZSTD_c_contentSizeFlag: Final = 200 +ZSTD_c_dictIDFlag: Final = 202 +ZSTD_c_enableLongDistanceMatching: Final = 160 +ZSTD_c_hashLog: Final = 102 +ZSTD_c_jobSize: Final = 401 +ZSTD_c_ldmBucketSizeLog: Final = 163 +ZSTD_c_ldmHashLog: Final = 161 +ZSTD_c_ldmHashRateLog: Final = 164 +ZSTD_c_ldmMinMatch: Final = 162 +ZSTD_c_minMatch: Final = 105 +ZSTD_c_nbWorkers: Final = 400 +ZSTD_c_overlapLog: Final = 402 +ZSTD_c_searchLog: Final = 104 +ZSTD_c_strategy: Final = 107 +ZSTD_c_targetLength: Final = 106 +ZSTD_c_windowLog: Final = 101 +ZSTD_d_windowLogMax: Final = 100 +ZSTD_dfast: Final = 2 +ZSTD_fast: Final = 1 +ZSTD_greedy: Final = 3 +ZSTD_lazy: Final = 4 +ZSTD_lazy2: Final = 5 + +_ZstdCompressorContinue: TypeAlias = Literal[0] +_ZstdCompressorFlushBlock: TypeAlias = Literal[1] +_ZstdCompressorFlushFrame: TypeAlias = Literal[2] + +@final +class ZstdCompressor: + CONTINUE: Final = 0 + FLUSH_BLOCK: Final = 1 + FLUSH_FRAME: Final = 2 + def __init__( + self, level: int | None = None, options: Mapping[int, int] | None = None, zstd_dict: ZstdDict | None = None + ) -> None: ... + def compress( + self, /, data: ReadableBuffer, mode: _ZstdCompressorContinue | _ZstdCompressorFlushBlock | _ZstdCompressorFlushFrame = 0 + ) -> bytes: ... + def flush(self, /, mode: _ZstdCompressorFlushBlock | _ZstdCompressorFlushFrame = 2) -> bytes: ... + def set_pledged_input_size(self, size: int | None, /) -> None: ... + @property + def last_mode(self) -> _ZstdCompressorContinue | _ZstdCompressorFlushBlock | _ZstdCompressorFlushFrame: ... + +@final +class ZstdDecompressor: + def __init__(self, zstd_dict: ZstdDict | None = None, options: Mapping[int, int] | None = None) -> None: ... + def decompress(self, /, data: ReadableBuffer, max_length: int = -1) -> bytes: ... + @property + def eof(self) -> bool: ... + @property + def needs_input(self) -> bool: ... + @property + def unused_data(self) -> bytes: ... + +@final +class ZstdDict: + def __init__(self, dict_content: bytes, /, *, is_raw: bool = False) -> None: ... + def __len__(self, /) -> int: ... + @property + def as_digested_dict(self) -> tuple[Self, int]: ... + @property + def as_prefix(self) -> tuple[Self, int]: ... + @property + def as_undigested_dict(self) -> tuple[Self, int]: ... + @property + def dict_content(self) -> bytes: ... + @property + def dict_id(self) -> int: ... + +class ZstdError(Exception): ... + +def finalize_dict( + custom_dict_bytes: bytes, samples_bytes: bytes, samples_sizes: tuple[int, ...], dict_size: int, compression_level: int, / +) -> bytes: ... +def get_frame_info(frame_buffer: ReadableBuffer) -> tuple[int, int]: ... +def get_frame_size(frame_buffer: ReadableBuffer) -> int: ... +def get_param_bounds(parameter: int, is_compress: bool) -> tuple[int, int]: ... +def set_parameter_types(c_parameter_type: type[CompressionParameter], d_parameter_type: type[DecompressionParameter]) -> None: ... +def train_dict(samples_bytes: bytes, samples_sizes: tuple[int, ...], dict_size: int, /) -> bytes: ... + +zstd_version: Final[str] +zstd_version_number: Final[int] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/aifc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/aifc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/aifc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/aifc.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/annotationlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/annotationlib.pyi new file mode 100644 index 0000000000000..7590c632d7856 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/annotationlib.pyi @@ -0,0 +1,132 @@ +import sys +from typing import Literal + +if sys.version_info >= (3, 14): + import enum + import types + from _typeshed import AnnotateFunc, AnnotationForm, EvaluateFunc, SupportsItems + from collections.abc import Mapping + from typing import Any, ParamSpec, TypeVar, TypeVarTuple, final, overload + from warnings import deprecated + + __all__ = [ + "Format", + "ForwardRef", + "call_annotate_function", + "call_evaluate_function", + "get_annotate_from_class_namespace", + "get_annotations", + "annotations_to_string", + "type_repr", + ] + + class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + + @final + class ForwardRef: + __forward_is_argument__: bool + __forward_is_class__: bool + __forward_module__: str | None + def __init__( + self, arg: str, *, module: str | None = None, owner: object = None, is_argument: bool = True, is_class: bool = False + ) -> None: ... + @overload + def evaluate( + self, + *, + globals: dict[str, Any] | None = None, + locals: Mapping[str, Any] | None = None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] | None = None, + owner: object = None, + format: Literal[Format.STRING], + ) -> str: ... + @overload + def evaluate( + self, + *, + globals: dict[str, Any] | None = None, + locals: Mapping[str, Any] | None = None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] | None = None, + owner: object = None, + format: Literal[Format.FORWARDREF], + ) -> AnnotationForm | ForwardRef: ... + @overload + def evaluate( + self, + *, + globals: dict[str, Any] | None = None, + locals: Mapping[str, Any] | None = None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] | None = None, + owner: object = None, + format: Format = Format.VALUE, # noqa: Y011 + ) -> AnnotationForm: ... + @deprecated("Use ForwardRef.evaluate() or typing.evaluate_forward_ref() instead.") + def _evaluate( + self, + globalns: dict[str, Any] | None, + localns: Mapping[str, Any] | None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = ..., + *, + recursive_guard: frozenset[str], + ) -> AnnotationForm: ... + @property + def __forward_arg__(self) -> str: ... + @property + def __forward_code__(self) -> types.CodeType: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def __or__(self, other: Any) -> types.UnionType: ... + def __ror__(self, other: Any) -> types.UnionType: ... + + @overload + def call_evaluate_function(evaluate: EvaluateFunc, format: Literal[Format.STRING], *, owner: object = None) -> str: ... + @overload + def call_evaluate_function( + evaluate: EvaluateFunc, format: Literal[Format.FORWARDREF], *, owner: object = None + ) -> AnnotationForm | ForwardRef: ... + @overload + def call_evaluate_function(evaluate: EvaluateFunc, format: Format, *, owner: object = None) -> AnnotationForm: ... + @overload + def call_annotate_function( + annotate: AnnotateFunc, format: Literal[Format.STRING], *, owner: object = None + ) -> dict[str, str]: ... + @overload + def call_annotate_function( + annotate: AnnotateFunc, format: Literal[Format.FORWARDREF], *, owner: object = None + ) -> dict[str, AnnotationForm | ForwardRef]: ... + @overload + def call_annotate_function(annotate: AnnotateFunc, format: Format, *, owner: object = None) -> dict[str, AnnotationForm]: ... + def get_annotate_from_class_namespace(obj: Mapping[str, object]) -> AnnotateFunc | None: ... + @overload + def get_annotations( + obj: Any, # any object with __annotations__ or __annotate__ + *, + globals: dict[str, object] | None = None, + locals: Mapping[str, object] | None = None, + eval_str: bool = False, + format: Literal[Format.STRING], + ) -> dict[str, str]: ... + @overload + def get_annotations( + obj: Any, + *, + globals: dict[str, object] | None = None, + locals: Mapping[str, object] | None = None, + eval_str: bool = False, + format: Literal[Format.FORWARDREF], + ) -> dict[str, AnnotationForm | ForwardRef]: ... + @overload + def get_annotations( + obj: Any, + *, + globals: dict[str, object] | None = None, + locals: Mapping[str, object] | None = None, + eval_str: bool = False, + format: Format = Format.VALUE, # noqa: Y011 + ) -> dict[str, AnnotationForm]: ... + def type_repr(value: object) -> str: ... + def annotations_to_string(annotations: SupportsItems[str, object]) -> dict[str, str]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/antigravity.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/antigravity.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/antigravity.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/antigravity.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/argparse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi similarity index 87% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/argparse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi index 32beaff14696b..312093c0aa556 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/argparse.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi @@ -123,6 +123,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): fromfile_prefix_chars: str | None add_help: bool allow_abbrev: bool + exit_on_error: bool + + if sys.version_info >= (3, 14): + suggest_on_error: bool + color: bool # undocumented _positionals: _ArgumentGroup @@ -130,22 +135,44 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): _subparsers: _ArgumentGroup | None # Note: the constructor arguments are also used in _SubParsersAction.add_parser. - def __init__( - self, - prog: str | None = None, - usage: str | None = None, - description: str | None = None, - epilog: str | None = None, - parents: Sequence[ArgumentParser] = [], - formatter_class: _FormatterClass = ..., - prefix_chars: str = "-", - fromfile_prefix_chars: str | None = None, - argument_default: Any = None, - conflict_handler: str = "error", - add_help: bool = True, - allow_abbrev: bool = True, - exit_on_error: bool = True, - ) -> None: ... + if sys.version_info >= (3, 14): + def __init__( + self, + prog: str | None = None, + usage: str | None = None, + description: str | None = None, + epilog: str | None = None, + parents: Sequence[ArgumentParser] = [], + formatter_class: _FormatterClass = ..., + prefix_chars: str = "-", + fromfile_prefix_chars: str | None = None, + argument_default: Any = None, + conflict_handler: str = "error", + add_help: bool = True, + allow_abbrev: bool = True, + exit_on_error: bool = True, + *, + suggest_on_error: bool = False, + color: bool = False, + ) -> None: ... + else: + def __init__( + self, + prog: str | None = None, + usage: str | None = None, + description: str | None = None, + epilog: str | None = None, + parents: Sequence[ArgumentParser] = [], + formatter_class: _FormatterClass = ..., + prefix_chars: str = "-", + fromfile_prefix_chars: str | None = None, + argument_default: Any = None, + conflict_handler: str = "error", + add_help: bool = True, + allow_abbrev: bool = True, + exit_on_error: bool = True, + ) -> None: ... + @overload def parse_args(self, args: Sequence[str] | None = None, namespace: None = None) -> Namespace: ... @overload @@ -252,7 +279,15 @@ class HelpFormatter: def __init__(self, formatter: HelpFormatter, parent: Self | None, heading: str | None = None) -> None: ... def format_help(self) -> str: ... - def __init__(self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None) -> None: ... + if sys.version_info >= (3, 14): + def __init__( + self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None, color: bool = False + ) -> None: ... + else: + def __init__( + self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int | None = None + ) -> None: ... + def _indent(self) -> None: ... def _dedent(self) -> None: ... def _add_item(self, func: Callable[..., str], args: Iterable[Any]) -> None: ... @@ -431,14 +466,30 @@ class Namespace(_AttributeHolder): def __eq__(self, other: object) -> bool: ... __hash__: ClassVar[None] # type: ignore[assignment] -class FileType: - # undocumented - _mode: str - _bufsize: int - _encoding: str | None - _errors: str | None - def __init__(self, mode: str = "r", bufsize: int = -1, encoding: str | None = None, errors: str | None = None) -> None: ... - def __call__(self, string: str) -> IO[Any]: ... +if sys.version_info >= (3, 14): + @deprecated("Deprecated in Python 3.14; Simply open files after parsing arguments") + class FileType: + # undocumented + _mode: str + _bufsize: int + _encoding: str | None + _errors: str | None + def __init__( + self, mode: str = "r", bufsize: int = -1, encoding: str | None = None, errors: str | None = None + ) -> None: ... + def __call__(self, string: str) -> IO[Any]: ... + +else: + class FileType: + # undocumented + _mode: str + _bufsize: int + _encoding: str | None + _errors: str | None + def __init__( + self, mode: str = "r", bufsize: int = -1, encoding: str | None = None, errors: str | None = None + ) -> None: ... + def __call__(self, string: str) -> IO[Any]: ... # undocumented class _ArgumentGroup(_ActionsContainer): @@ -668,7 +719,33 @@ class _SubParsersAction(Action, Generic[_ArgumentParserT]): # Note: `add_parser` accepts all kwargs of `ArgumentParser.__init__`. It also # accepts its own `help` and `aliases` kwargs. - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + def add_parser( + self, + name: str, + *, + deprecated: bool = False, + help: str | None = ..., + aliases: Sequence[str] = ..., + # Kwargs from ArgumentParser constructor + prog: str | None = ..., + usage: str | None = ..., + description: str | None = ..., + epilog: str | None = ..., + parents: Sequence[_ArgumentParserT] = ..., + formatter_class: _FormatterClass = ..., + prefix_chars: str = ..., + fromfile_prefix_chars: str | None = ..., + argument_default: Any = ..., + conflict_handler: str = ..., + add_help: bool = ..., + allow_abbrev: bool = ..., + exit_on_error: bool = ..., + suggest_on_error: bool = False, + color: bool = False, + **kwargs: Any, # Accepting any additional kwargs for custom parser classes + ) -> _ArgumentParserT: ... + elif sys.version_info >= (3, 13): def add_parser( self, name: str, diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/array.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/array.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/array.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/array.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ast.pyi similarity index 88% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ast.pyi index 90c6d2ff0e68d..1e31fadcee66b 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ast.pyi @@ -1,3 +1,5 @@ +import ast +import builtins import os import sys import typing_extensions @@ -7,19 +9,13 @@ from _ast import ( PyCF_TYPE_COMMENTS as PyCF_TYPE_COMMENTS, ) from _typeshed import ReadableBuffer, Unused -from collections.abc import Iterable, Iterator +from collections.abc import Iterable, Iterator, Sequence from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar as _TypeVar, overload from typing_extensions import Self, Unpack, deprecated if sys.version_info >= (3, 13): from _ast import PyCF_OPTIMIZED_AST as PyCF_OPTIMIZED_AST -# Alias used for fields that must always be valid identifiers -# A string `x` counts as a valid identifier if both the following are True -# (1) `x.isidentifier()` evaluates to `True` -# (2) `keyword.iskeyword(x)` evaluates to `False` -_Identifier: typing_extensions.TypeAlias = str - # Used for node end positions in constructor keyword arguments _EndPositionT = typing_extensions.TypeVar("_EndPositionT", int, int | None, default=int | None) @@ -111,7 +107,7 @@ class FunctionDef(stmt): __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment", "type_params") elif sys.version_info >= (3, 10): __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment") - name: _Identifier + name: str args: arguments body: list[stmt] decorator_list: list[expr] @@ -122,7 +118,7 @@ class FunctionDef(stmt): if sys.version_info >= (3, 13): def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt] = ..., decorator_list: list[expr] = ..., @@ -135,7 +131,7 @@ class FunctionDef(stmt): @overload def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt], decorator_list: list[expr], @@ -147,7 +143,7 @@ class FunctionDef(stmt): @overload def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt], decorator_list: list[expr], @@ -160,7 +156,7 @@ class FunctionDef(stmt): else: def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt], decorator_list: list[expr], @@ -173,13 +169,14 @@ class FunctionDef(stmt): def __replace__( self, *, - name: _Identifier = ..., + name: str = ..., args: arguments = ..., body: list[stmt] = ..., decorator_list: list[expr] = ..., returns: expr | None = ..., type_comment: str | None = ..., type_params: list[type_param] = ..., + **kwargs: Unpack[_Attributes], ) -> Self: ... class AsyncFunctionDef(stmt): @@ -187,7 +184,7 @@ class AsyncFunctionDef(stmt): __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment", "type_params") elif sys.version_info >= (3, 10): __match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment") - name: _Identifier + name: str args: arguments body: list[stmt] decorator_list: list[expr] @@ -198,7 +195,7 @@ class AsyncFunctionDef(stmt): if sys.version_info >= (3, 13): def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt] = ..., decorator_list: list[expr] = ..., @@ -211,7 +208,7 @@ class AsyncFunctionDef(stmt): @overload def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt], decorator_list: list[expr], @@ -223,7 +220,7 @@ class AsyncFunctionDef(stmt): @overload def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt], decorator_list: list[expr], @@ -236,7 +233,7 @@ class AsyncFunctionDef(stmt): else: def __init__( self, - name: _Identifier, + name: str, args: arguments, body: list[stmt], decorator_list: list[expr], @@ -249,13 +246,14 @@ class AsyncFunctionDef(stmt): def __replace__( self, *, - name: _Identifier = ..., + name: str = ..., args: arguments = ..., - body: list[stmt], - decorator_list: list[expr], - returns: expr | None, - type_comment: str | None, - type_params: list[type_param], + body: list[stmt] = ..., + decorator_list: list[expr] = ..., + returns: expr | None = ..., + type_comment: str | None = ..., + type_params: list[type_param] = ..., + **kwargs: Unpack[_Attributes], ) -> Self: ... class ClassDef(stmt): @@ -263,7 +261,7 @@ class ClassDef(stmt): __match_args__ = ("name", "bases", "keywords", "body", "decorator_list", "type_params") elif sys.version_info >= (3, 10): __match_args__ = ("name", "bases", "keywords", "body", "decorator_list") - name: _Identifier + name: str bases: list[expr] keywords: list[keyword] body: list[stmt] @@ -273,7 +271,7 @@ class ClassDef(stmt): if sys.version_info >= (3, 13): def __init__( self, - name: _Identifier, + name: str, bases: list[expr] = ..., keywords: list[keyword] = ..., body: list[stmt] = ..., @@ -284,7 +282,7 @@ class ClassDef(stmt): elif sys.version_info >= (3, 12): def __init__( self, - name: _Identifier, + name: str, bases: list[expr], keywords: list[keyword], body: list[stmt], @@ -295,7 +293,7 @@ class ClassDef(stmt): else: def __init__( self, - name: _Identifier, + name: str, bases: list[expr], keywords: list[keyword], body: list[stmt], @@ -307,12 +305,12 @@ class ClassDef(stmt): def __replace__( self, *, - name: _Identifier, - bases: list[expr], - keywords: list[keyword], - body: list[stmt], - decorator_list: list[expr], - type_params: list[type_param], + name: str = ..., + bases: list[expr] = ..., + keywords: list[keyword] = ..., + body: list[stmt] = ..., + decorator_list: list[expr] = ..., + type_params: list[type_param] = ..., **kwargs: Unpack[_Attributes], ) -> Self: ... @@ -383,7 +381,7 @@ if sys.version_info >= (3, 12): ) -> None: ... if sys.version_info >= (3, 14): - def __replace__( + def __replace__( # type: ignore[override] self, *, name: Name = ..., @@ -546,7 +544,9 @@ class While(stmt): def __init__(self, test: expr, body: list[stmt], orelse: list[stmt], **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, test: expr, body: list[stmt], orelse: list[stmt], **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__( + self, *, test: expr = ..., body: list[stmt] = ..., orelse: list[stmt] = ..., **kwargs: Unpack[_Attributes] + ) -> Self: ... class If(stmt): if sys.version_info >= (3, 10): @@ -624,21 +624,6 @@ class AsyncWith(stmt): **kwargs: Unpack[_Attributes], ) -> Self: ... -if sys.version_info >= (3, 10): - class Match(stmt): - __match_args__ = ("subject", "cases") - subject: expr - cases: list[match_case] - if sys.version_info >= (3, 13): - def __init__(self, subject: expr, cases: list[match_case] = ..., **kwargs: Unpack[_Attributes]) -> None: ... - else: - def __init__(self, subject: expr, cases: list[match_case], **kwargs: Unpack[_Attributes]) -> None: ... - - if sys.version_info >= (3, 14): - def __replace__( - self, *, subject: expr = ..., cases: list[match_case] = ..., **kwargs: Unpack[_Attributes] - ) -> Self: ... - class Raise(stmt): if sys.version_info >= (3, 10): __match_args__ = ("exc", "cause") @@ -731,7 +716,7 @@ class Assert(stmt): def __init__(self, test: expr, msg: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, test: expr, msg: expr | None, **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, test: expr = ..., msg: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Import(stmt): if sys.version_info >= (3, 10): @@ -774,26 +759,26 @@ class ImportFrom(stmt): class Global(stmt): if sys.version_info >= (3, 10): __match_args__ = ("names",) - names: list[_Identifier] + names: list[str] if sys.version_info >= (3, 13): - def __init__(self, names: list[_Identifier] = ..., **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> None: ... else: - def __init__(self, names: list[_Identifier], **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, names: list[str], **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, names: list[_Identifier], **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Nonlocal(stmt): if sys.version_info >= (3, 10): __match_args__ = ("names",) - names: list[_Identifier] + names: list[str] if sys.version_info >= (3, 13): - def __init__(self, names: list[_Identifier] = ..., **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> None: ... else: - def __init__(self, names: list[_Identifier], **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, names: list[str], **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, names: list[_Identifier] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, names: list[str] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Expr(stmt): if sys.version_info >= (3, 10): @@ -1065,45 +1050,84 @@ class JoinedStr(expr): if sys.version_info >= (3, 14): def __replace__(self, *, values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... +if sys.version_info >= (3, 14): + class TemplateStr(expr): + __match_args__ = ("values",) + values: list[expr] + def __init__(self, values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> None: ... + def __replace__(self, *, values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + + class Interpolation(expr): + __match_args__ = ("value", "str", "conversion", "format_spec") + value: expr + str: builtins.str + conversion: int + format_spec: expr | None = None + def __init__( + self, + value: expr = ..., + str: builtins.str = ..., + conversion: int = ..., + format_spec: expr | None = ..., + **kwargs: Unpack[_Attributes], + ) -> None: ... + def __replace__( + self, + *, + value: expr = ..., + str: builtins.str = ..., + conversion: int = ..., + format_spec: expr | None = ..., + **kwargs: Unpack[_Attributes], + ) -> Self: ... + +if sys.version_info >= (3, 10): + from types import EllipsisType + + _ConstantValue: typing_extensions.TypeAlias = str | bytes | bool | int | float | complex | None | EllipsisType +else: + # Rely on builtins.ellipsis + _ConstantValue: typing_extensions.TypeAlias = str | bytes | bool | int | float | complex | None | ellipsis # noqa: F821 + class Constant(expr): if sys.version_info >= (3, 10): __match_args__ = ("value", "kind") - value: Any # None, str, bytes, bool, int, float, complex, Ellipsis + value: _ConstantValue kind: str | None if sys.version_info < (3, 14): # Aliases for value, for backwards compatibility - s: Any - n: int | float | complex + s: _ConstantValue + n: _ConstantValue - def __init__(self, value: Any, kind: str | None = None, **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, value: _ConstantValue, kind: str | None = None, **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, value: Any = ..., kind: str | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, value: _ConstantValue = ..., kind: str | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class Attribute(expr): if sys.version_info >= (3, 10): __match_args__ = ("value", "attr", "ctx") value: expr - attr: _Identifier + attr: str ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` - def __init__(self, value: expr, attr: _Identifier, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, value: expr, attr: str, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, *, value: expr = ..., attr: _Identifier = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes] + self, *, value: expr = ..., attr: str = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... class Subscript(expr): if sys.version_info >= (3, 10): __match_args__ = ("value", "slice", "ctx") value: expr - slice: _Slice + slice: expr ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` - def __init__(self, value: expr, slice: _Slice, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, value: expr, slice: expr, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, *, value: expr = ..., slice: _Slice = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes] + self, *, value: expr = ..., slice: expr = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... class Starred(expr): @@ -1119,12 +1143,12 @@ class Starred(expr): class Name(expr): if sys.version_info >= (3, 10): __match_args__ = ("id", "ctx") - id: _Identifier + id: str ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__` - def __init__(self, id: _Identifier, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, id: str, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, id: _Identifier = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, id: str = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class List(expr): if sys.version_info >= (3, 10): @@ -1156,36 +1180,28 @@ class Tuple(expr): @deprecated("Deprecated since Python 3.9.") class slice(AST): ... -_Slice: typing_extensions.TypeAlias = expr -_SliceAttributes: typing_extensions.TypeAlias = _Attributes - -class Slice(_Slice): +class Slice(expr): if sys.version_info >= (3, 10): __match_args__ = ("lower", "upper", "step") lower: expr | None upper: expr | None step: expr | None def __init__( - self, lower: expr | None = None, upper: expr | None = None, step: expr | None = None, **kwargs: Unpack[_SliceAttributes] + self, lower: expr | None = None, upper: expr | None = None, step: expr | None = None, **kwargs: Unpack[_Attributes] ) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, - *, - lower: expr | None = ..., - upper: expr | None = ..., - step: expr | None = ..., - **kwargs: Unpack[_SliceAttributes], + self, *, lower: expr | None = ..., upper: expr | None = ..., step: expr | None = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... @deprecated("Deprecated since Python 3.9. Use ast.Tuple instead.") class ExtSlice(slice): - def __new__(cls, dims: Iterable[slice] = (), **kwargs: Unpack[_SliceAttributes]) -> Tuple: ... # type: ignore[misc] + def __new__(cls, dims: Iterable[slice] = (), **kwargs: Unpack[_Attributes]) -> Tuple: ... # type: ignore[misc] @deprecated("Deprecated since Python 3.9. Use the index value directly instead.") class Index(slice): - def __new__(cls, value: expr, **kwargs: Unpack[_SliceAttributes]) -> expr: ... # type: ignore[misc] + def __new__(cls, value: expr, **kwargs: Unpack[_Attributes]) -> expr: ... # type: ignore[misc] class expr_context(AST): ... @@ -1272,30 +1288,23 @@ class ExceptHandler(excepthandler): if sys.version_info >= (3, 10): __match_args__ = ("type", "name", "body") type: expr | None - name: _Identifier | None + name: str | None body: list[stmt] if sys.version_info >= (3, 13): def __init__( - self, type: expr | None = None, name: _Identifier | None = None, body: list[stmt] = ..., **kwargs: Unpack[_Attributes] + self, type: expr | None = None, name: str | None = None, body: list[stmt] = ..., **kwargs: Unpack[_Attributes] ) -> None: ... else: @overload - def __init__( - self, type: expr | None, name: _Identifier | None, body: list[stmt], **kwargs: Unpack[_Attributes] - ) -> None: ... + def __init__(self, type: expr | None, name: str | None, body: list[stmt], **kwargs: Unpack[_Attributes]) -> None: ... @overload def __init__( - self, type: expr | None = None, name: _Identifier | None = None, *, body: list[stmt], **kwargs: Unpack[_Attributes] + self, type: expr | None = None, name: str | None = None, *, body: list[stmt], **kwargs: Unpack[_Attributes] ) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, - *, - type: expr | None = ..., - name: _Identifier | None = ..., - body: list[stmt] = ..., - **kwargs: Unpack[_Attributes], + self, *, type: expr | None = ..., name: str | None = ..., body: list[stmt] = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... class arguments(AST): @@ -1376,21 +1385,16 @@ class arg(AST): end_col_offset: int | None if sys.version_info >= (3, 10): __match_args__ = ("arg", "annotation", "type_comment") - arg: _Identifier + arg: str annotation: expr | None type_comment: str | None def __init__( - self, arg: _Identifier, annotation: expr | None = None, type_comment: str | None = None, **kwargs: Unpack[_Attributes] + self, arg: str, annotation: expr | None = None, type_comment: str | None = None, **kwargs: Unpack[_Attributes] ) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, - *, - arg: _Identifier = ..., - annotation: expr | None = ..., - type_comment: str | None = ..., - **kwargs: Unpack[_Attributes], + self, *, arg: str = ..., annotation: expr | None = ..., type_comment: str | None = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... class keyword(AST): @@ -1400,29 +1404,33 @@ class keyword(AST): end_col_offset: int | None if sys.version_info >= (3, 10): __match_args__ = ("arg", "value") - arg: _Identifier | None + arg: str | None value: expr @overload - def __init__(self, arg: _Identifier | None, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, arg: str | None, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... @overload - def __init__(self, arg: _Identifier | None = None, *, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... + def __init__(self, arg: str | None = None, *, value: expr, **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, arg: _Identifier | None = ..., value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, arg: str | None = ..., value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class alias(AST): - lineno: int - col_offset: int - end_lineno: int | None - end_col_offset: int | None + name: str + asname: str | None + if sys.version_info >= (3, 10): + lineno: int + col_offset: int + end_lineno: int | None + end_col_offset: int | None if sys.version_info >= (3, 10): __match_args__ = ("name", "asname") - name: str - asname: _Identifier | None - def __init__(self, name: str, asname: _Identifier | None = None, **kwargs: Unpack[_Attributes]) -> None: ... + if sys.version_info >= (3, 10): + def __init__(self, name: str, asname: str | None = None, **kwargs: Unpack[_Attributes]) -> None: ... + else: + def __init__(self, name: str, asname: str | None = None) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, name: str = ..., asname: _Identifier | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... + def __replace__(self, *, name: str = ..., asname: str | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ... class withitem(AST): if sys.version_info >= (3, 10): @@ -1435,37 +1443,48 @@ class withitem(AST): def __replace__(self, *, context_expr: expr = ..., optional_vars: expr | None = ...) -> Self: ... if sys.version_info >= (3, 10): + class pattern(AST): + lineno: int + col_offset: int + end_lineno: int + end_col_offset: int + def __init__(self, **kwargs: Unpack[_Attributes[int]]) -> None: ... + + if sys.version_info >= (3, 14): + def __replace__( + self, *, lineno: int = ..., col_offset: int = ..., end_lineno: int = ..., end_col_offset: int = ... + ) -> Self: ... + class match_case(AST): __match_args__ = ("pattern", "guard", "body") - pattern: _Pattern + pattern: ast.pattern guard: expr | None body: list[stmt] if sys.version_info >= (3, 13): - def __init__(self, pattern: _Pattern, guard: expr | None = None, body: list[stmt] = ...) -> None: ... - else: + def __init__(self, pattern: ast.pattern, guard: expr | None = None, body: list[stmt] = ...) -> None: ... + elif sys.version_info >= (3, 10): @overload - def __init__(self, pattern: _Pattern, guard: expr | None, body: list[stmt]) -> None: ... + def __init__(self, pattern: ast.pattern, guard: expr | None, body: list[stmt]) -> None: ... @overload - def __init__(self, pattern: _Pattern, guard: expr | None = None, *, body: list[stmt]) -> None: ... + def __init__(self, pattern: ast.pattern, guard: expr | None = None, *, body: list[stmt]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, pattern: _Pattern = ..., guard: expr | None = ..., body: list[stmt] = ...) -> Self: ... + def __replace__(self, *, pattern: ast.pattern = ..., guard: expr | None = ..., body: list[stmt] = ...) -> Self: ... - class pattern(AST): - lineno: int - col_offset: int - end_lineno: int - end_col_offset: int - def __init__(self, **kwargs: Unpack[_Attributes[int]]) -> None: ... + class Match(stmt): + __match_args__ = ("subject", "cases") + subject: expr + cases: list[match_case] + if sys.version_info >= (3, 13): + def __init__(self, subject: expr, cases: list[match_case] = ..., **kwargs: Unpack[_Attributes]) -> None: ... + else: + def __init__(self, subject: expr, cases: list[match_case], **kwargs: Unpack[_Attributes]) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, *, lineno: int = ..., col_offset: int = ..., end_lineno: int = ..., end_col_offset: int = ... + self, *, subject: expr = ..., cases: list[match_case] = ..., **kwargs: Unpack[_Attributes] ) -> Self: ... - # Without the alias, Pyright complains variables named pattern are recursively defined - _Pattern: typing_extensions.TypeAlias = pattern - class MatchValue(pattern): __match_args__ = ("value",) value: expr @@ -1497,22 +1516,18 @@ if sys.version_info >= (3, 10): __match_args__ = ("keys", "patterns", "rest") keys: list[expr] patterns: list[pattern] - rest: _Identifier | None + rest: str | None if sys.version_info >= (3, 13): def __init__( self, keys: list[expr] = ..., patterns: list[pattern] = ..., - rest: _Identifier | None = None, + rest: str | None = None, **kwargs: Unpack[_Attributes[int]], ) -> None: ... else: def __init__( - self, - keys: list[expr], - patterns: list[pattern], - rest: _Identifier | None = None, - **kwargs: Unpack[_Attributes[int]], + self, keys: list[expr], patterns: list[pattern], rest: str | None = None, **kwargs: Unpack[_Attributes[int]] ) -> None: ... if sys.version_info >= (3, 14): @@ -1521,7 +1536,7 @@ if sys.version_info >= (3, 10): *, keys: list[expr] = ..., patterns: list[pattern] = ..., - rest: _Identifier | None = ..., + rest: str | None = ..., **kwargs: Unpack[_Attributes[int]], ) -> Self: ... @@ -1529,14 +1544,14 @@ if sys.version_info >= (3, 10): __match_args__ = ("cls", "patterns", "kwd_attrs", "kwd_patterns") cls: expr patterns: list[pattern] - kwd_attrs: list[_Identifier] + kwd_attrs: list[str] kwd_patterns: list[pattern] if sys.version_info >= (3, 13): def __init__( self, cls: expr, patterns: list[pattern] = ..., - kwd_attrs: list[_Identifier] = ..., + kwd_attrs: list[str] = ..., kwd_patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]], ) -> None: ... @@ -1545,7 +1560,7 @@ if sys.version_info >= (3, 10): self, cls: expr, patterns: list[pattern], - kwd_attrs: list[_Identifier], + kwd_attrs: list[str], kwd_patterns: list[pattern], **kwargs: Unpack[_Attributes[int]], ) -> None: ... @@ -1556,30 +1571,30 @@ if sys.version_info >= (3, 10): *, cls: expr = ..., patterns: list[pattern] = ..., - kwd_attrs: list[_Identifier] = ..., + kwd_attrs: list[str] = ..., kwd_patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]], ) -> Self: ... class MatchStar(pattern): __match_args__ = ("name",) - name: _Identifier | None - def __init__(self, name: _Identifier | None, **kwargs: Unpack[_Attributes[int]]) -> None: ... + name: str | None + def __init__(self, name: str | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... if sys.version_info >= (3, 14): - def __replace__(self, *, name: _Identifier | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... + def __replace__(self, *, name: str | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ... class MatchAs(pattern): __match_args__ = ("pattern", "name") - pattern: _Pattern | None - name: _Identifier | None + pattern: ast.pattern | None + name: str | None def __init__( - self, pattern: _Pattern | None = None, name: _Identifier | None = None, **kwargs: Unpack[_Attributes[int]] + self, pattern: ast.pattern | None = None, name: str | None = None, **kwargs: Unpack[_Attributes[int]] ) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, *, pattern: _Pattern | None = ..., name: _Identifier | None = ..., **kwargs: Unpack[_Attributes[int]] + self, *, pattern: ast.pattern | None = ..., name: str | None = ..., **kwargs: Unpack[_Attributes[int]] ) -> Self: ... class MatchOr(pattern): @@ -1621,25 +1636,21 @@ if sys.version_info >= (3, 12): __match_args__ = ("name", "bound", "default_value") else: __match_args__ = ("name", "bound") - name: _Identifier + name: str bound: expr | None if sys.version_info >= (3, 13): default_value: expr | None def __init__( - self, - name: _Identifier, - bound: expr | None = None, - default_value: expr | None = None, - **kwargs: Unpack[_Attributes[int]], + self, name: str, bound: expr | None = None, default_value: expr | None = None, **kwargs: Unpack[_Attributes[int]] ) -> None: ... else: - def __init__(self, name: _Identifier, bound: expr | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... + def __init__(self, name: str, bound: expr | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... if sys.version_info >= (3, 14): def __replace__( self, *, - name: _Identifier = ..., + name: str = ..., bound: expr | None = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]], @@ -1650,18 +1661,16 @@ if sys.version_info >= (3, 12): __match_args__ = ("name", "default_value") else: __match_args__ = ("name",) - name: _Identifier + name: str if sys.version_info >= (3, 13): default_value: expr | None - def __init__( - self, name: _Identifier, default_value: expr | None = None, **kwargs: Unpack[_Attributes[int]] - ) -> None: ... + def __init__(self, name: str, default_value: expr | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... else: - def __init__(self, name: _Identifier, **kwargs: Unpack[_Attributes[int]]) -> None: ... + def __init__(self, name: str, **kwargs: Unpack[_Attributes[int]]) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, *, name: _Identifier = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]] + self, *, name: str = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]] ) -> Self: ... class TypeVarTuple(type_param): @@ -1669,18 +1678,16 @@ if sys.version_info >= (3, 12): __match_args__ = ("name", "default_value") else: __match_args__ = ("name",) - name: _Identifier + name: str if sys.version_info >= (3, 13): default_value: expr | None - def __init__( - self, name: _Identifier, default_value: expr | None = None, **kwargs: Unpack[_Attributes[int]] - ) -> None: ... + def __init__(self, name: str, default_value: expr | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ... else: - def __init__(self, name: _Identifier, **kwargs: Unpack[_Attributes[int]]) -> None: ... + def __init__(self, name: str, **kwargs: Unpack[_Attributes[int]]) -> None: ... if sys.version_info >= (3, 14): def __replace__( - self, *, name: _Identifier = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]] + self, *, name: str = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]] ) -> Self: ... class _ABC(type): @@ -1790,7 +1797,7 @@ if sys.version_info >= (3, 13): type_comments: bool = False, feature_version: None | int | tuple[int, int] = None, optimize: Literal[-1, 0, 1, 2] = -1, - ) -> AST: ... + ) -> mod: ... else: @overload @@ -1861,7 +1868,7 @@ else: *, type_comments: bool = False, feature_version: None | int | tuple[int, int] = None, - ) -> AST: ... + ) -> mod: ... def literal_eval(node_or_string: str | AST) -> Any: ... @@ -1893,8 +1900,12 @@ if sys.version_info >= (3, 14): def compare(left: AST, right: AST, /, *, compare_attributes: bool = False) -> bool: ... class NodeVisitor: + # All visit methods below can be overwritten by subclasses and return an + # arbitrary value, which is passed to the caller. def visit(self, node: AST) -> Any: ... def generic_visit(self, node: AST) -> Any: ... + # The following visit methods are not defined on NodeVisitor, but can + # be implemented by subclasses and are called during a visit if defined. def visit_Module(self, node: Module) -> Any: ... def visit_Interactive(self, node: Interactive) -> Any: ... def visit_Expression(self, node: Expression) -> Any: ... @@ -2038,4 +2049,9 @@ class NodeTransformer(NodeVisitor): # is also allowed in some cases -- this needs to be mapped. def unparse(ast_obj: AST) -> str: ... -def main() -> None: ... + +if sys.version_info >= (3, 14): + def main(args: Sequence[str] | None = None) -> None: ... + +else: + def main() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asynchat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asynchat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asynchat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asynchat.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi new file mode 100644 index 0000000000000..58739816a67eb --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi @@ -0,0 +1,1012 @@ +# This condition is so big, it's clearer to keep to platform condition in two blocks +# Can't NOQA on a specific line: https://github.com/plinss/flake8-noqa/issues/22 +import sys +from collections.abc import Awaitable, Coroutine, Generator +from typing import Any, TypeVar +from typing_extensions import TypeAlias + +# As at runtime, this depends on all submodules defining __all__ accurately. +from .base_events import * +from .coroutines import * +from .events import * +from .exceptions import * +from .futures import * +from .locks import * +from .protocols import * +from .queues import * +from .runners import * +from .streams import * +from .subprocess import * +from .tasks import * +from .threads import * +from .transports import * + +if sys.version_info >= (3, 14): + from .graph import * + +if sys.version_info >= (3, 11): + from .taskgroups import * + from .timeouts import * + +if sys.platform == "win32": + from .windows_events import * +else: + from .unix_events import * + +if sys.platform == "win32": + if sys.version_info >= (3, 14): + + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "_AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "_get_event_loop_policy", # from events + "get_event_loop_policy", # from events + "_set_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "future_discard_from_awaited_by", # from futures + "future_add_to_awaited_by", # from futures + "capture_call_graph", # from graph + "format_call_graph", # from graph + "print_call_graph", # from graph + "FrameCallGraphEntry", # from graph + "FutureCallGraph", # from graph + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "QueueShutDown", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "create_eager_task_factory", # from tasks + "eager_task_factory", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "TaskGroup", # from taskgroups + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from windows_events + "ProactorEventLoop", # from windows_events + "IocpProactor", # from windows_events + "_DefaultEventLoopPolicy", # from windows_events + "_WindowsSelectorEventLoopPolicy", # from windows_events + "_WindowsProactorEventLoopPolicy", # from windows_events + "EventLoop", # from windows_events + ) + elif sys.version_info >= (3, 13): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "QueueShutDown", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "create_eager_task_factory", # from tasks + "eager_task_factory", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "TaskGroup", # from taskgroups + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from windows_events + "ProactorEventLoop", # from windows_events + "IocpProactor", # from windows_events + "DefaultEventLoopPolicy", # from windows_events + "WindowsSelectorEventLoopPolicy", # from windows_events + "WindowsProactorEventLoopPolicy", # from windows_events + "EventLoop", # from windows_events + ) + elif sys.version_info >= (3, 12): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "create_eager_task_factory", # from tasks + "eager_task_factory", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "TaskGroup", # from taskgroups + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from windows_events + "ProactorEventLoop", # from windows_events + "IocpProactor", # from windows_events + "DefaultEventLoopPolicy", # from windows_events + "WindowsSelectorEventLoopPolicy", # from windows_events + "WindowsProactorEventLoopPolicy", # from windows_events + ) + elif sys.version_info >= (3, 11): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from windows_events + "ProactorEventLoop", # from windows_events + "IocpProactor", # from windows_events + "DefaultEventLoopPolicy", # from windows_events + "WindowsSelectorEventLoopPolicy", # from windows_events + "WindowsProactorEventLoopPolicy", # from windows_events + ) + else: + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "coroutine", # from coroutines + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "to_thread", # from threads + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from windows_events + "ProactorEventLoop", # from windows_events + "IocpProactor", # from windows_events + "DefaultEventLoopPolicy", # from windows_events + "WindowsSelectorEventLoopPolicy", # from windows_events + "WindowsProactorEventLoopPolicy", # from windows_events + ) +else: + if sys.version_info >= (3, 14): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "_AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "_get_event_loop_policy", # from events + "get_event_loop_policy", # from events + "_set_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "future_discard_from_awaited_by", # from futures + "future_add_to_awaited_by", # from futures + "capture_call_graph", # from graph + "format_call_graph", # from graph + "print_call_graph", # from graph + "FrameCallGraphEntry", # from graph + "FutureCallGraph", # from graph + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "QueueShutDown", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "open_unix_connection", # from streams + "start_unix_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "create_eager_task_factory", # from tasks + "eager_task_factory", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "TaskGroup", # from taskgroups + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from unix_events + "_DefaultEventLoopPolicy", # from unix_events + "EventLoop", # from unix_events + ) + elif sys.version_info >= (3, 13): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "QueueShutDown", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "open_unix_connection", # from streams + "start_unix_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "create_eager_task_factory", # from tasks + "eager_task_factory", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "TaskGroup", # from taskgroups + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from unix_events + "AbstractChildWatcher", # from unix_events + "SafeChildWatcher", # from unix_events + "FastChildWatcher", # from unix_events + "PidfdChildWatcher", # from unix_events + "MultiLoopChildWatcher", # from unix_events + "ThreadedChildWatcher", # from unix_events + "DefaultEventLoopPolicy", # from unix_events + "EventLoop", # from unix_events + ) + elif sys.version_info >= (3, 12): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "open_unix_connection", # from streams + "start_unix_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "create_eager_task_factory", # from tasks + "eager_task_factory", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "TaskGroup", # from taskgroups + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from unix_events + "AbstractChildWatcher", # from unix_events + "SafeChildWatcher", # from unix_events + "FastChildWatcher", # from unix_events + "PidfdChildWatcher", # from unix_events + "MultiLoopChildWatcher", # from unix_events + "ThreadedChildWatcher", # from unix_events + "DefaultEventLoopPolicy", # from unix_events + ) + elif sys.version_info >= (3, 11): + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "BrokenBarrierError", # from exceptions + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "Barrier", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "Runner", # from runners + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "open_unix_connection", # from streams + "start_unix_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "to_thread", # from threads + "Timeout", # from timeouts + "timeout", # from timeouts + "timeout_at", # from timeouts + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from unix_events + "AbstractChildWatcher", # from unix_events + "SafeChildWatcher", # from unix_events + "FastChildWatcher", # from unix_events + "PidfdChildWatcher", # from unix_events + "MultiLoopChildWatcher", # from unix_events + "ThreadedChildWatcher", # from unix_events + "DefaultEventLoopPolicy", # from unix_events + ) + else: + __all__ = ( + "BaseEventLoop", # from base_events + "Server", # from base_events + "coroutine", # from coroutines + "iscoroutinefunction", # from coroutines + "iscoroutine", # from coroutines + "AbstractEventLoopPolicy", # from events + "AbstractEventLoop", # from events + "AbstractServer", # from events + "Handle", # from events + "TimerHandle", # from events + "get_event_loop_policy", # from events + "set_event_loop_policy", # from events + "get_event_loop", # from events + "set_event_loop", # from events + "new_event_loop", # from events + "get_child_watcher", # from events + "set_child_watcher", # from events + "_set_running_loop", # from events + "get_running_loop", # from events + "_get_running_loop", # from events + "CancelledError", # from exceptions + "InvalidStateError", # from exceptions + "TimeoutError", # from exceptions + "IncompleteReadError", # from exceptions + "LimitOverrunError", # from exceptions + "SendfileNotAvailableError", # from exceptions + "Future", # from futures + "wrap_future", # from futures + "isfuture", # from futures + "Lock", # from locks + "Event", # from locks + "Condition", # from locks + "Semaphore", # from locks + "BoundedSemaphore", # from locks + "BaseProtocol", # from protocols + "Protocol", # from protocols + "DatagramProtocol", # from protocols + "SubprocessProtocol", # from protocols + "BufferedProtocol", # from protocols + "run", # from runners + "Queue", # from queues + "PriorityQueue", # from queues + "LifoQueue", # from queues + "QueueFull", # from queues + "QueueEmpty", # from queues + "StreamReader", # from streams + "StreamWriter", # from streams + "StreamReaderProtocol", # from streams + "open_connection", # from streams + "start_server", # from streams + "open_unix_connection", # from streams + "start_unix_server", # from streams + "create_subprocess_exec", # from subprocess + "create_subprocess_shell", # from subprocess + "Task", # from tasks + "create_task", # from tasks + "FIRST_COMPLETED", # from tasks + "FIRST_EXCEPTION", # from tasks + "ALL_COMPLETED", # from tasks + "wait", # from tasks + "wait_for", # from tasks + "as_completed", # from tasks + "sleep", # from tasks + "gather", # from tasks + "shield", # from tasks + "ensure_future", # from tasks + "run_coroutine_threadsafe", # from tasks + "current_task", # from tasks + "all_tasks", # from tasks + "_register_task", # from tasks + "_unregister_task", # from tasks + "_enter_task", # from tasks + "_leave_task", # from tasks + "to_thread", # from threads + "BaseTransport", # from transports + "ReadTransport", # from transports + "WriteTransport", # from transports + "Transport", # from transports + "DatagramTransport", # from transports + "SubprocessTransport", # from transports + "SelectorEventLoop", # from unix_events + "AbstractChildWatcher", # from unix_events + "SafeChildWatcher", # from unix_events + "FastChildWatcher", # from unix_events + "PidfdChildWatcher", # from unix_events + "MultiLoopChildWatcher", # from unix_events + "ThreadedChildWatcher", # from unix_events + "DefaultEventLoopPolicy", # from unix_events + ) + +_T_co = TypeVar("_T_co", covariant=True) + +# Aliases imported by multiple submodules in typeshed +if sys.version_info >= (3, 12): + _AwaitableLike: TypeAlias = Awaitable[_T_co] # noqa: Y047 + _CoroutineLike: TypeAlias = Coroutine[Any, Any, _T_co] # noqa: Y047 +else: + _AwaitableLike: TypeAlias = Generator[Any, None, _T_co] | Awaitable[_T_co] + _CoroutineLike: TypeAlias = Generator[Any, None, _T_co] | Coroutine[Any, Any, _T_co] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi index afe912d01fe1f..688ef3ed08794 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/events.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi @@ -21,17 +21,21 @@ from .futures import Future from .protocols import BaseProtocol from .tasks import Task from .transports import BaseTransport, DatagramTransport, ReadTransport, SubprocessTransport, Transport, WriteTransport -from .unix_events import AbstractChildWatcher + +if sys.version_info < (3, 14): + from .unix_events import AbstractChildWatcher # Keep asyncio.__all__ updated with any changes to __all__ here if sys.version_info >= (3, 14): __all__ = ( - "AbstractEventLoopPolicy", + "_AbstractEventLoopPolicy", "AbstractEventLoop", "AbstractServer", "Handle", "TimerHandle", + "_get_event_loop_policy", "get_event_loop_policy", + "_set_event_loop_policy", "set_event_loop_policy", "get_event_loop", "set_event_loop", @@ -598,7 +602,7 @@ class AbstractEventLoop: @abstractmethod async def shutdown_default_executor(self) -> None: ... -class AbstractEventLoopPolicy: +class _AbstractEventLoopPolicy: @abstractmethod def get_event_loop(self) -> AbstractEventLoop: ... @abstractmethod @@ -620,13 +624,33 @@ class AbstractEventLoopPolicy: @abstractmethod def set_child_watcher(self, watcher: AbstractChildWatcher) -> None: ... -class BaseDefaultEventLoopPolicy(AbstractEventLoopPolicy, metaclass=ABCMeta): - def get_event_loop(self) -> AbstractEventLoop: ... - def set_event_loop(self, loop: AbstractEventLoop | None) -> None: ... - def new_event_loop(self) -> AbstractEventLoop: ... +if sys.version_info < (3, 14): + AbstractEventLoopPolicy = _AbstractEventLoopPolicy + +if sys.version_info >= (3, 14): + class _BaseDefaultEventLoopPolicy(_AbstractEventLoopPolicy, metaclass=ABCMeta): + def get_event_loop(self) -> AbstractEventLoop: ... + def set_event_loop(self, loop: AbstractEventLoop | None) -> None: ... + def new_event_loop(self) -> AbstractEventLoop: ... + +else: + class BaseDefaultEventLoopPolicy(_AbstractEventLoopPolicy, metaclass=ABCMeta): + def get_event_loop(self) -> AbstractEventLoop: ... + def set_event_loop(self, loop: AbstractEventLoop | None) -> None: ... + def new_event_loop(self) -> AbstractEventLoop: ... + +if sys.version_info >= (3, 14): + def _get_event_loop_policy() -> _AbstractEventLoopPolicy: ... + def _set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... + @deprecated("Deprecated as of Python 3.14; will be removed in Python 3.16") + def get_event_loop_policy() -> _AbstractEventLoopPolicy: ... + @deprecated("Deprecated as of Python 3.14; will be removed in Python 3.16") + def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... + +else: + def get_event_loop_policy() -> _AbstractEventLoopPolicy: ... + def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: ... -def get_event_loop_policy() -> AbstractEventLoopPolicy: ... -def set_event_loop_policy(policy: AbstractEventLoopPolicy | None) -> None: ... def set_event_loop(loop: AbstractEventLoop | None) -> None: ... def new_event_loop() -> AbstractEventLoop: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi new file mode 100644 index 0000000000000..644d2d0e94cab --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi @@ -0,0 +1,23 @@ +import sys +from _asyncio import Future as Future +from concurrent.futures._base import Future as _ConcurrentFuture +from typing import Any, TypeVar +from typing_extensions import TypeIs + +from .events import AbstractEventLoop + +# Keep asyncio.__all__ updated with any changes to __all__ here +if sys.version_info >= (3, 14): + from _asyncio import future_add_to_awaited_by, future_discard_from_awaited_by + + __all__ = ("Future", "wrap_future", "isfuture", "future_discard_from_awaited_by", "future_add_to_awaited_by") +else: + __all__ = ("Future", "wrap_future", "isfuture") + +_T = TypeVar("_T") + +# asyncio defines 'isfuture()' in base_futures.py and re-imports it in futures.py +# but it leads to circular import error in pytype tool. +# That's why the import order is reversed. +def isfuture(obj: object) -> TypeIs[Future[Any]]: ... +def wrap_future(future: _ConcurrentFuture[_T] | Future[_T], *, loop: AbstractEventLoop | None = None) -> Future[_T]: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/graph.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/graph.pyi new file mode 100644 index 0000000000000..cb2cf01749955 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/graph.pyi @@ -0,0 +1,26 @@ +from _typeshed import SupportsWrite +from asyncio import Future +from dataclasses import dataclass +from types import FrameType +from typing import Any, overload + +__all__ = ("capture_call_graph", "format_call_graph", "print_call_graph", "FrameCallGraphEntry", "FutureCallGraph") + +@dataclass(frozen=True) +class FrameCallGraphEntry: + frame: FrameType + +@dataclass(frozen=True) +class FutureCallGraph: + future: Future[Any] + call_stack: tuple[FrameCallGraphEntry, ...] + awaited_by: tuple[FutureCallGraph, ...] + +@overload +def capture_call_graph(future: None = None, /, *, depth: int = 1, limit: int | None = None) -> FutureCallGraph | None: ... +@overload +def capture_call_graph(future: Future[Any], /, *, depth: int = 1, limit: int | None = None) -> FutureCallGraph | None: ... +def format_call_graph(future: Future[Any] | None = None, /, *, depth: int = 1, limit: int | None = None) -> str: ... +def print_call_graph( + future: Future[Any] | None = None, /, *, file: SupportsWrite[str] | None = None, depth: int = 1, limit: int | None = None +) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/log.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/log.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/log.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/log.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi index e42151213e69c..a088e95af653d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi @@ -423,6 +423,25 @@ if sys.version_info >= (3, 12): else: def current_task(loop: AbstractEventLoop | None = None) -> Task[Any] | None: ... +if sys.version_info >= (3, 14): + def eager_task_factory( + loop: AbstractEventLoop | None, + coro: _TaskCompatibleCoro[_T_co], + *, + name: str | None = None, + context: Context | None = None, + eager_start: bool = True, + ) -> Task[_T_co]: ... + +elif sys.version_info >= (3, 12): + def eager_task_factory( + loop: AbstractEventLoop | None, + coro: _TaskCompatibleCoro[_T_co], + *, + name: str | None = None, + context: Context | None = None, + ) -> Task[_T_co]: ... + if sys.version_info >= (3, 12): _TaskT_co = TypeVar("_TaskT_co", bound=Task[Any], covariant=True) @@ -451,10 +470,3 @@ if sys.version_info >= (3, 12): def create_eager_task_factory( custom_task_constructor: _CustomTaskConstructor[_TaskT_co], ) -> _EagerTaskFactoryType[_TaskT_co]: ... - def eager_task_factory( - loop: AbstractEventLoop | None, - coro: _TaskCompatibleCoro[_T_co], - *, - name: str | None = None, - context: Context | None = None, - ) -> Task[_T_co]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tools.pyi new file mode 100644 index 0000000000000..65c7f27e0b85e --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tools.pyi @@ -0,0 +1,41 @@ +from collections.abc import Iterable +from enum import Enum +from typing import NamedTuple, SupportsIndex, type_check_only + +@type_check_only +class _AwaitedInfo(NamedTuple): # AwaitedInfo_Type from _remote_debugging + thread_id: int + awaited_by: list[_TaskInfo] + +@type_check_only +class _TaskInfo(NamedTuple): # TaskInfo_Type from _remote_debugging + task_id: int + task_name: str + coroutine_stack: list[_CoroInfo] + awaited_by: list[_CoroInfo] + +@type_check_only +class _CoroInfo(NamedTuple): # CoroInfo_Type from _remote_debugging + call_stack: list[_FrameInfo] + task_name: int | str + +@type_check_only +class _FrameInfo(NamedTuple): # FrameInfo_Type from _remote_debugging + filename: str + lineno: int + funcname: str + +class NodeType(Enum): + COROUTINE = 1 + TASK = 2 + +class CycleFoundException(Exception): + cycles: list[list[int]] + id2name: dict[int, str] + def __init__(self, cycles: list[list[int]], id2name: dict[int, str]) -> None: ... + +def get_all_awaited_by(pid: SupportsIndex) -> list[_AwaitedInfo]: ... +def build_async_tree(result: Iterable[_AwaitedInfo], task_emoji: str = "(T)", cor_emoji: str = "") -> list[list[str]]: ... +def build_task_table(result: Iterable[_AwaitedInfo]) -> list[list[int | str]]: ... +def display_awaited_by_tasks_table(pid: SupportsIndex) -> None: ... +def display_awaited_by_tasks_tree(pid: SupportsIndex) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi similarity index 89% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi index 79f99fbe37f02..49f200dcdcae7 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi @@ -7,8 +7,8 @@ from socket import socket from typing import Literal from typing_extensions import Self, TypeVarTuple, Unpack, deprecated +from . import events from .base_events import Server, _ProtocolFactory, _SSLContext -from .events import AbstractEventLoop, BaseDefaultEventLoopPolicy from .selector_events import BaseSelectorEventLoop _Ts = TypeVarTuple("_Ts") @@ -16,7 +16,7 @@ _Ts = TypeVarTuple("_Ts") # Keep asyncio.__all__ updated with any changes to __all__ here if sys.platform != "win32": if sys.version_info >= (3, 14): - __all__ = ("SelectorEventLoop", "DefaultEventLoopPolicy", "EventLoop") + __all__ = ("SelectorEventLoop", "_DefaultEventLoopPolicy", "EventLoop") elif sys.version_info >= (3, 13): # Adds EventLoop __all__ = ( @@ -57,7 +57,7 @@ if sys.version_info < (3, 14): @abstractmethod def remove_child_handler(self, pid: int) -> bool: ... @abstractmethod - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... @abstractmethod def close(self) -> None: ... @abstractmethod @@ -78,7 +78,7 @@ if sys.version_info < (3, 14): @abstractmethod def remove_child_handler(self, pid: int) -> bool: ... @abstractmethod - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... @abstractmethod def close(self) -> None: ... @abstractmethod @@ -98,7 +98,7 @@ if sys.platform != "win32": class BaseChildWatcher(AbstractChildWatcher, metaclass=ABCMeta): def close(self) -> None: ... def is_active(self) -> bool: ... - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... @deprecated("Deprecated as of Python 3.12; will be removed in Python 3.14") class SafeChildWatcher(BaseChildWatcher): @@ -128,7 +128,7 @@ if sys.platform != "win32": class BaseChildWatcher(AbstractChildWatcher, metaclass=ABCMeta): def close(self) -> None: ... def is_active(self) -> bool: ... - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... class SafeChildWatcher(BaseChildWatcher): def __enter__(self) -> Self: ... @@ -166,8 +166,10 @@ if sys.platform != "win32": cleanup_socket: bool = True, ) -> Server: ... - class _UnixDefaultEventLoopPolicy(BaseDefaultEventLoopPolicy): - if sys.version_info < (3, 14): + if sys.version_info >= (3, 14): + class _UnixDefaultEventLoopPolicy(events._BaseDefaultEventLoopPolicy): ... + else: + class _UnixDefaultEventLoopPolicy(events.BaseDefaultEventLoopPolicy): if sys.version_info >= (3, 12): @deprecated("Deprecated as of Python 3.12; will be removed in Python 3.14") def get_child_watcher(self) -> AbstractChildWatcher: ... @@ -179,7 +181,10 @@ if sys.platform != "win32": SelectorEventLoop = _UnixSelectorEventLoop - DefaultEventLoopPolicy = _UnixDefaultEventLoopPolicy + if sys.version_info >= (3, 14): + _DefaultEventLoopPolicy = _UnixDefaultEventLoopPolicy + else: + DefaultEventLoopPolicy = _UnixDefaultEventLoopPolicy if sys.version_info >= (3, 13): EventLoop = SelectorEventLoop @@ -198,7 +203,7 @@ if sys.platform != "win32": self, pid: int, callback: Callable[[int, int, Unpack[_Ts]], object], *args: Unpack[_Ts] ) -> None: ... def remove_child_handler(self, pid: int) -> bool: ... - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... else: class MultiLoopChildWatcher(AbstractChildWatcher): @@ -212,7 +217,7 @@ if sys.platform != "win32": self, pid: int, callback: Callable[[int, int, Unpack[_Ts]], object], *args: Unpack[_Ts] ) -> None: ... def remove_child_handler(self, pid: int) -> bool: ... - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... if sys.version_info < (3, 14): class ThreadedChildWatcher(AbstractChildWatcher): @@ -227,7 +232,7 @@ if sys.platform != "win32": self, pid: int, callback: Callable[[int, int, Unpack[_Ts]], object], *args: Unpack[_Ts] ) -> None: ... def remove_child_handler(self, pid: int) -> bool: ... - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... class PidfdChildWatcher(AbstractChildWatcher): def __enter__(self) -> Self: ... @@ -236,7 +241,7 @@ if sys.platform != "win32": ) -> None: ... def is_active(self) -> bool: ... def close(self) -> None: ... - def attach_loop(self, loop: AbstractEventLoop | None) -> None: ... + def attach_loop(self, loop: events.AbstractEventLoop | None) -> None: ... def add_child_handler( self, pid: int, callback: Callable[[int, int, Unpack[_Ts]], object], *args: Unpack[_Ts] ) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi similarity index 76% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi index 2ffc2eccb228a..b454aca1f2628 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi @@ -8,7 +8,17 @@ from . import events, futures, proactor_events, selector_events, streams, window # Keep asyncio.__all__ updated with any changes to __all__ here if sys.platform == "win32": - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + __all__ = ( + "SelectorEventLoop", + "ProactorEventLoop", + "IocpProactor", + "_DefaultEventLoopPolicy", + "_WindowsSelectorEventLoopPolicy", + "_WindowsProactorEventLoopPolicy", + "EventLoop", + ) + elif sys.version_info >= (3, 13): # 3.13 added `EventLoop`. __all__ = ( "SelectorEventLoop", @@ -85,17 +95,27 @@ if sys.platform == "win32": SelectorEventLoop = _WindowsSelectorEventLoop - class WindowsSelectorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): - _loop_factory: ClassVar[type[SelectorEventLoop]] - if sys.version_info < (3, 14): + if sys.version_info >= (3, 14): + class _WindowsSelectorEventLoopPolicy(events._BaseDefaultEventLoopPolicy): + _loop_factory: ClassVar[type[SelectorEventLoop]] + + class _WindowsProactorEventLoopPolicy(events._BaseDefaultEventLoopPolicy): + _loop_factory: ClassVar[type[ProactorEventLoop]] + + else: + class WindowsSelectorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): + _loop_factory: ClassVar[type[SelectorEventLoop]] def get_child_watcher(self) -> NoReturn: ... def set_child_watcher(self, watcher: Any) -> NoReturn: ... - class WindowsProactorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): - _loop_factory: ClassVar[type[ProactorEventLoop]] - def get_child_watcher(self) -> NoReturn: ... - def set_child_watcher(self, watcher: Any) -> NoReturn: ... + class WindowsProactorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): + _loop_factory: ClassVar[type[ProactorEventLoop]] + def get_child_watcher(self) -> NoReturn: ... + def set_child_watcher(self, watcher: Any) -> NoReturn: ... - DefaultEventLoopPolicy = WindowsSelectorEventLoopPolicy + if sys.version_info >= (3, 14): + _DefaultEventLoopPolicy = _WindowsProactorEventLoopPolicy + else: + DefaultEventLoopPolicy = WindowsSelectorEventLoopPolicy if sys.version_info >= (3, 13): EventLoop = ProactorEventLoop diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncore.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncore.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncore.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncore.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/atexit.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/atexit.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/atexit.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/atexit.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/audioop.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/audioop.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/audioop.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/audioop.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/base64.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/base64.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/base64.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/base64.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/bdb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/bdb.pyi similarity index 88% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/bdb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/bdb.pyi index 2004874a52b26..b73f894093ce5 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/bdb.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/bdb.pyi @@ -3,13 +3,14 @@ from _typeshed import ExcInfo, TraceFunction, Unused from collections.abc import Callable, Iterable, Iterator, Mapping from contextlib import contextmanager from types import CodeType, FrameType, TracebackType -from typing import IO, Any, Final, SupportsInt, TypeVar -from typing_extensions import ParamSpec +from typing import IO, Any, Final, Literal, SupportsInt, TypeVar +from typing_extensions import ParamSpec, TypeAlias __all__ = ["BdbQuit", "Bdb", "Breakpoint"] _T = TypeVar("_T") _P = ParamSpec("_P") +_Backend: TypeAlias = Literal["settrace", "monitoring"] # A union of code-object flags at runtime. # The exact values of code-object flags are implementation details, @@ -28,7 +29,12 @@ class Bdb: stopframe: FrameType | None returnframe: FrameType | None stoplineno: int - def __init__(self, skip: Iterable[str] | None = None) -> None: ... + if sys.version_info >= (3, 14): + backend: _Backend + def __init__(self, skip: Iterable[str] | None = None, backend: _Backend = "settrace") -> None: ... + else: + def __init__(self, skip: Iterable[str] | None = None) -> None: ... + def canonic(self, filename: str) -> str: ... def reset(self) -> None: ... if sys.version_info >= (3, 12): @@ -85,6 +91,11 @@ class Bdb: def runeval(self, expr: str, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None) -> None: ... def runctx(self, cmd: str | CodeType, globals: dict[str, Any] | None, locals: Mapping[str, Any] | None) -> None: ... def runcall(self, func: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> _T | None: ... + if sys.version_info >= (3, 14): + def start_trace(self) -> None: ... + def stop_trace(self) -> None: ... + def disable_current_event(self) -> None: ... + def restart_events(self) -> None: ... class Breakpoint: next: int diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/binascii.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/binascii.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/binascii.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/binascii.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/binhex.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/binhex.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/binhex.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/binhex.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/bisect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/bisect.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/bisect.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/bisect.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi similarity index 92% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi index b75250aad3de1..9633b410a61cc 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi @@ -5,7 +5,7 @@ import sys import types from _collections_abc import dict_items, dict_keys, dict_values from _typeshed import ( - AnyStr_co, + AnnotationForm, ConvertibleToFloat, ConvertibleToInt, FileDescriptorOrPath, @@ -32,6 +32,7 @@ from _typeshed import ( ) from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper +from os import PathLike from types import CellType, CodeType, GenericAlias, TracebackType # mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} @@ -72,6 +73,9 @@ from typing_extensions import ( # noqa: Y023 deprecated, ) +if sys.version_info >= (3, 14): + from _typeshed import AnnotateFunc + _T = TypeVar("_T") _I = TypeVar("_I", default=int) _T_co = TypeVar("_T_co", covariant=True) @@ -150,6 +154,9 @@ class staticmethod(Generic[_P, _R_co]): @property def __wrapped__(self) -> Callable[_P, _R_co]: ... def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... + if sys.version_info >= (3, 14): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + __annotate__: AnnotateFunc | None class classmethod(Generic[_T, _P, _R_co]): @property @@ -166,6 +173,9 @@ class classmethod(Generic[_T, _P, _R_co]): __qualname__: str @property def __wrapped__(self) -> Callable[Concatenate[type[_T], _P], _R_co]: ... + if sys.version_info >= (3, 14): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + __annotate__: AnnotateFunc | None class type: # object.__base__ is None. Otherwise, it would be a type. @@ -215,6 +225,9 @@ class type: def __ror__(self, value: Any, /) -> types.UnionType: ... if sys.version_info >= (3, 12): __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] + __annotations__: dict[str, AnnotationForm] + if sys.version_info >= (3, 14): + __annotate__: AnnotateFunc | None class super: @overload @@ -318,7 +331,11 @@ class int: def __trunc__(self) -> int: ... def __ceil__(self) -> int: ... def __floor__(self) -> int: ... - def __round__(self, ndigits: SupportsIndex = ..., /) -> int: ... + if sys.version_info >= (3, 14): + def __round__(self, ndigits: SupportsIndex | None = None, /) -> int: ... + else: + def __round__(self, ndigits: SupportsIndex = ..., /) -> int: ... + def __getnewargs__(self) -> tuple[int]: ... def __eq__(self, value: object, /) -> bool: ... def __ne__(self, value: object, /) -> bool: ... @@ -393,6 +410,9 @@ class float: def __abs__(self) -> float: ... def __hash__(self) -> int: ... def __bool__(self) -> bool: ... + if sys.version_info >= (3, 14): + @classmethod + def from_number(cls, number: float | SupportsIndex | SupportsFloat, /) -> Self: ... class complex: # Python doesn't currently accept SupportsComplex for the second argument @@ -428,6 +448,9 @@ class complex: def __bool__(self) -> bool: ... if sys.version_info >= (3, 11): def __complex__(self) -> complex: ... + if sys.version_info >= (3, 14): + @classmethod + def from_number(cls, number: complex | SupportsComplex | SupportsFloat | SupportsIndex, /) -> Self: ... class _FormatMapMapping(Protocol): def __getitem__(self, key: str, /) -> Any: ... @@ -684,7 +707,7 @@ class bytes(Sequence[int]): def strip(self, bytes: ReadableBuffer | None = None, /) -> bytes: ... def swapcase(self) -> bytes: ... def title(self) -> bytes: ... - def translate(self, table: ReadableBuffer | None, /, delete: bytes = b"") -> bytes: ... + def translate(self, table: ReadableBuffer | None, /, delete: ReadableBuffer = b"") -> bytes: ... def upper(self) -> bytes: ... def zfill(self, width: SupportsIndex, /) -> bytes: ... @classmethod @@ -825,6 +848,8 @@ class bytearray(MutableSequence[int]): def __alloc__(self) -> int: ... def __buffer__(self, flags: int, /) -> memoryview: ... def __release_buffer__(self, buffer: memoryview, /) -> None: ... + if sys.version_info >= (3, 14): + def resize(self, size: int, /) -> None: ... _IntegerFormats: TypeAlias = Literal[ "b", "B", "@b", "@B", "h", "H", "@h", "@H", "i", "I", "@i", "@I", "l", "L", "@l", "@L", "q", "Q", "@q", "@Q", "P", "@P" @@ -902,6 +927,8 @@ class memoryview(Sequence[_I]): # See https://github.com/python/cpython/issues/125420 index: ClassVar[None] # type: ignore[assignment] count: ClassVar[None] # type: ignore[assignment] + if sys.version_info >= (3, 14): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @final class bool(int): @@ -933,7 +960,7 @@ class bool(int): @overload def __rxor__(self, value: int, /) -> int: ... def __getnewargs__(self) -> tuple[int]: ... - @deprecated("Will throw an error in Python 3.14. Use `not` for logical negation of bools instead.") + @deprecated("Will throw an error in Python 3.16. Use `not` for logical negation of bools instead.") def __invert__(self) -> int: ... @final @@ -1004,6 +1031,7 @@ class tuple(Sequence[_T_co]): # Doesn't exist at runtime, but deleting this breaks mypy and pyright. See: # https://github.com/python/typeshed/issues/7580 # https://github.com/python/mypy/issues/8240 +# Obsolete, use types.FunctionType instead. @final @type_check_only class function: @@ -1017,8 +1045,10 @@ class function: def __globals__(self) -> dict[str, Any]: ... __name__: str __qualname__: str - __annotations__: dict[str, Any] - __kwdefaults__: dict[str, Any] + __annotations__: dict[str, AnnotationForm] + if sys.version_info >= (3, 14): + __annotate__: AnnotateFunc | None + __kwdefaults__: dict[str, Any] | None if sys.version_info >= (3, 10): @property def __builtins__(self) -> dict[str, Any]: ... @@ -1026,6 +1056,26 @@ class function: __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] __module__: str + if sys.version_info >= (3, 13): + def __new__( + cls, + code: CodeType, + globals: dict[str, Any], + name: str | None = None, + argdefs: tuple[object, ...] | None = None, + closure: tuple[CellType, ...] | None = None, + kwdefaults: dict[str, object] | None = None, + ) -> Self: ... + else: + def __new__( + cls, + code: CodeType, + globals: dict[str, Any], + name: str | None = None, + argdefs: tuple[object, ...] | None = None, + closure: tuple[CellType, ...] | None = None, + ) -> Self: ... + # mypy uses `builtins.function.__get__` to represent methods, properties, and getset_descriptors so we type the return as Any. def __get__(self, instance: object, owner: type | None = None, /) -> Any: ... @@ -1303,11 +1353,6 @@ def breakpoint(*args: Any, **kws: Any) -> None: ... def callable(obj: object, /) -> TypeIs[Callable[..., object]]: ... def chr(i: int | SupportsIndex, /) -> str: ... -# We define this here instead of using os.PathLike to avoid import cycle issues. -# See https://github.com/python/typeshed/pull/991#issuecomment-288160993 -class _PathLike(Protocol[AnyStr_co]): - def __fspath__(self) -> AnyStr_co: ... - if sys.version_info >= (3, 10): def aiter(async_iterable: SupportsAiter[_SupportsAnextT_co], /) -> _SupportsAnextT_co: ... @@ -1328,7 +1373,7 @@ if sys.version_info >= (3, 10): @overload def compile( source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | ReadableBuffer | _PathLike[Any], + filename: str | ReadableBuffer | PathLike[Any], mode: str, flags: Literal[0], dont_inherit: bool = False, @@ -1339,7 +1384,7 @@ def compile( @overload def compile( source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | ReadableBuffer | _PathLike[Any], + filename: str | ReadableBuffer | PathLike[Any], mode: str, *, dont_inherit: bool = False, @@ -1349,7 +1394,7 @@ def compile( @overload def compile( source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | ReadableBuffer | _PathLike[Any], + filename: str | ReadableBuffer | PathLike[Any], mode: str, flags: Literal[1024], dont_inherit: bool = False, @@ -1360,7 +1405,7 @@ def compile( @overload def compile( source: str | ReadableBuffer | _ast.Module | _ast.Expression | _ast.Interactive, - filename: str | ReadableBuffer | _PathLike[Any], + filename: str | ReadableBuffer | PathLike[Any], mode: str, flags: int, dont_inherit: bool = False, @@ -1494,48 +1539,108 @@ license: _sitebuiltins._Printer def locals() -> dict[str, Any]: ... class map(Generic[_S]): - @overload - def __new__(cls, func: Callable[[_T1], _S], iterable: Iterable[_T1], /) -> Self: ... - @overload - def __new__(cls, func: Callable[[_T1, _T2], _S], iterable: Iterable[_T1], iter2: Iterable[_T2], /) -> Self: ... - @overload - def __new__( - cls, func: Callable[[_T1, _T2, _T3], _S], iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], / - ) -> Self: ... - @overload - def __new__( - cls, - func: Callable[[_T1, _T2, _T3, _T4], _S], - iterable: Iterable[_T1], - iter2: Iterable[_T2], - iter3: Iterable[_T3], - iter4: Iterable[_T4], - /, - ) -> Self: ... - @overload - def __new__( - cls, - func: Callable[[_T1, _T2, _T3, _T4, _T5], _S], - iterable: Iterable[_T1], - iter2: Iterable[_T2], - iter3: Iterable[_T3], - iter4: Iterable[_T4], - iter5: Iterable[_T5], - /, - ) -> Self: ... - @overload - def __new__( - cls, - func: Callable[..., _S], - iterable: Iterable[Any], - iter2: Iterable[Any], - iter3: Iterable[Any], - iter4: Iterable[Any], - iter5: Iterable[Any], - iter6: Iterable[Any], - /, - *iterables: Iterable[Any], - ) -> Self: ... + # 3.14 adds `strict` argument. + if sys.version_info >= (3, 14): + @overload + def __new__(cls, func: Callable[[_T1], _S], iterable: Iterable[_T1], /, *, strict: bool = False) -> Self: ... + @overload + def __new__( + cls, func: Callable[[_T1, _T2], _S], iterable: Iterable[_T1], iter2: Iterable[_T2], /, *, strict: bool = False + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[[_T1, _T2, _T3], _S], + iterable: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + /, + *, + strict: bool = False, + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[[_T1, _T2, _T3, _T4], _S], + iterable: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + /, + *, + strict: bool = False, + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[[_T1, _T2, _T3, _T4, _T5], _S], + iterable: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + /, + *, + strict: bool = False, + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[..., _S], + iterable: Iterable[Any], + iter2: Iterable[Any], + iter3: Iterable[Any], + iter4: Iterable[Any], + iter5: Iterable[Any], + iter6: Iterable[Any], + /, + *iterables: Iterable[Any], + strict: bool = False, + ) -> Self: ... + else: + @overload + def __new__(cls, func: Callable[[_T1], _S], iterable: Iterable[_T1], /) -> Self: ... + @overload + def __new__(cls, func: Callable[[_T1, _T2], _S], iterable: Iterable[_T1], iter2: Iterable[_T2], /) -> Self: ... + @overload + def __new__( + cls, func: Callable[[_T1, _T2, _T3], _S], iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], / + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[[_T1, _T2, _T3, _T4], _S], + iterable: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + /, + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[[_T1, _T2, _T3, _T4, _T5], _S], + iterable: Iterable[_T1], + iter2: Iterable[_T2], + iter3: Iterable[_T3], + iter4: Iterable[_T4], + iter5: Iterable[_T5], + /, + ) -> Self: ... + @overload + def __new__( + cls, + func: Callable[..., _S], + iterable: Iterable[Any], + iter2: Iterable[Any], + iter3: Iterable[Any], + iter4: Iterable[Any], + iter5: Iterable[Any], + iter6: Iterable[Any], + /, + *iterables: Iterable[Any], + ) -> Self: ... + def __iter__(self) -> Self: ... def __next__(self) -> _S: ... @@ -2090,27 +2195,27 @@ if sys.version_info >= (3, 11): def exceptions(self) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: ... @overload def subgroup( - self, condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / + self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / ) -> ExceptionGroup[_ExceptionT] | None: ... @overload def subgroup( - self, condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...], / + self, matcher_value: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...], / ) -> BaseExceptionGroup[_BaseExceptionT] | None: ... @overload def subgroup( - self, condition: Callable[[_BaseExceptionT_co | Self], bool], / + self, matcher_value: Callable[[_BaseExceptionT_co | Self], bool], / ) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ... @overload def split( - self, condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / + self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / ) -> tuple[ExceptionGroup[_ExceptionT] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ... @overload def split( - self, condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...], / + self, matcher_value: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...], / ) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ... @overload def split( - self, condition: Callable[[_BaseExceptionT_co | Self], bool], / + self, matcher_value: Callable[[_BaseExceptionT_co | Self], bool], / ) -> tuple[BaseExceptionGroup[_BaseExceptionT_co] | None, BaseExceptionGroup[_BaseExceptionT_co] | None]: ... # In reality it is `NonEmptySequence`: @overload @@ -2127,17 +2232,19 @@ if sys.version_info >= (3, 11): # We accept a narrower type, but that's OK. @overload # type: ignore[override] def subgroup( - self, condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / + self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / ) -> ExceptionGroup[_ExceptionT] | None: ... @overload - def subgroup(self, condition: Callable[[_ExceptionT_co | Self], bool], /) -> ExceptionGroup[_ExceptionT_co] | None: ... + def subgroup( + self, matcher_value: Callable[[_ExceptionT_co | Self], bool], / + ) -> ExceptionGroup[_ExceptionT_co] | None: ... @overload # type: ignore[override] def split( - self, condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / + self, matcher_value: type[_ExceptionT] | tuple[type[_ExceptionT], ...], / ) -> tuple[ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None]: ... @overload def split( - self, condition: Callable[[_ExceptionT_co | Self], bool], / + self, matcher_value: Callable[[_ExceptionT_co | Self], bool], / ) -> tuple[ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None]: ... if sys.version_info >= (3, 13): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/bz2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/bz2.pyi similarity index 89% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/bz2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/bz2.pyi index 3b21fbcf71176..dce6187a2da10 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/bz2.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/bz2.pyi @@ -1,17 +1,22 @@ -import _compression +import sys from _bz2 import BZ2Compressor as BZ2Compressor, BZ2Decompressor as BZ2Decompressor -from _compression import BaseStream from _typeshed import ReadableBuffer, StrOrBytesPath, WriteableBuffer from collections.abc import Iterable -from typing import IO, Literal, Protocol, SupportsIndex, TextIO, overload +from io import TextIOWrapper +from typing import IO, Literal, Protocol, SupportsIndex, overload from typing_extensions import Self, TypeAlias +if sys.version_info >= (3, 14): + from compression._common._streams import BaseStream, _Reader +else: + from _compression import BaseStream, _Reader + __all__ = ["BZ2File", "BZ2Compressor", "BZ2Decompressor", "open", "compress", "decompress"] # The following attributes and methods are optional: # def fileno(self) -> int: ... # def close(self) -> object: ... -class _ReadableFileobj(_compression._Reader, Protocol): ... +class _ReadableFileobj(_Reader, Protocol): ... class _WritableFileobj(Protocol): def write(self, b: bytes, /) -> object: ... @@ -44,7 +49,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> TextIO: ... +) -> TextIOWrapper: ... @overload def open( filename: _WritableFileobj, @@ -62,7 +67,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> TextIO: ... +) -> TextIOWrapper: ... @overload def open( filename: StrOrBytesPath, @@ -80,7 +85,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> TextIO: ... +) -> TextIOWrapper: ... @overload def open( filename: StrOrBytesPath | _ReadableFileobj | _WritableFileobj, @@ -89,7 +94,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> BZ2File | TextIO: ... +) -> BZ2File | TextIOWrapper: ... class BZ2File(BaseStream, IO[bytes]): def __enter__(self) -> Self: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cProfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cProfile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cProfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cProfile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/calendar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/calendar.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/calendar.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/calendar.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cgi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cgi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cgi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cgi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cgitb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cgitb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cgitb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cgitb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/chunk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/chunk.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/chunk.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/chunk.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cmath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cmath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cmd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cmd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cmd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cmd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/code.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/code.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/code.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/code.pyi index 16721927c2366..0b13c8a5016d4 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/code.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/code.pyi @@ -1,5 +1,5 @@ import sys -from codeop import CommandCompiler +from codeop import CommandCompiler, compile_command as compile_command from collections.abc import Callable from types import CodeType from typing import Any @@ -52,5 +52,3 @@ else: local: dict[str, Any] | None = None, exitmsg: str | None = None, ) -> None: ... - -def compile_command(source: str, filename: str = "", symbol: str = "single") -> CodeType | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/codecs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/codecs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/codeop.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/codeop.pyi new file mode 100644 index 0000000000000..8e311343eb89d --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/codeop.pyi @@ -0,0 +1,21 @@ +import sys +from types import CodeType + +__all__ = ["compile_command", "Compile", "CommandCompiler"] + +if sys.version_info >= (3, 14): + def compile_command(source: str, filename: str = "", symbol: str = "single", flags: int = 0) -> CodeType | None: ... + +else: + def compile_command(source: str, filename: str = "", symbol: str = "single") -> CodeType | None: ... + +class Compile: + flags: int + if sys.version_info >= (3, 13): + def __call__(self, source: str, filename: str, symbol: str, flags: int = 0) -> CodeType: ... + else: + def __call__(self, source: str, filename: str, symbol: str) -> CodeType: ... + +class CommandCompiler: + compiler: Compile + def __call__(self, source: str, filename: str = "", symbol: str = "single") -> CodeType | None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi new file mode 100644 index 0000000000000..bc33d91caa1d0 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi @@ -0,0 +1,499 @@ +import sys +from _collections_abc import dict_items, dict_keys, dict_values +from _typeshed import SupportsItems, SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT +from types import GenericAlias +from typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload +from typing_extensions import Self + +if sys.version_info >= (3, 10): + from collections.abc import ( + Callable, + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + MutableMapping, + MutableSequence, + Sequence, + ValuesView, + ) +else: + from _collections_abc import * + +__all__ = ["ChainMap", "Counter", "OrderedDict", "UserDict", "UserList", "UserString", "defaultdict", "deque", "namedtuple"] + +_S = TypeVar("_S") +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") +_KT_co = TypeVar("_KT_co", covariant=True) +_VT_co = TypeVar("_VT_co", covariant=True) + +# namedtuple is special-cased in the type checker; the initializer is ignored. +def namedtuple( + typename: str, + field_names: str | Iterable[str], + *, + rename: bool = False, + module: str | None = None, + defaults: Iterable[Any] | None = None, +) -> type[tuple[Any, ...]]: ... + +class UserDict(MutableMapping[_KT, _VT]): + data: dict[_KT, _VT] + # __init__ should be kept roughly in line with `dict.__init__`, which has the same semantics + @overload + def __init__(self, dict: None = None, /) -> None: ... + @overload + def __init__( + self: UserDict[str, _VT], dict: None = None, /, **kwargs: _VT # pyright: ignore[reportInvalidTypeVarUse] #11780 + ) -> None: ... + @overload + def __init__(self, dict: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... + @overload + def __init__( + self: UserDict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + dict: SupportsKeysAndGetItem[str, _VT], + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ... + @overload + def __init__( + self: UserDict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + iterable: Iterable[tuple[str, _VT]], + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self: UserDict[str, str], iterable: Iterable[list[str]], /) -> None: ... + @overload + def __init__(self: UserDict[bytes, bytes], iterable: Iterable[list[bytes]], /) -> None: ... + def __len__(self) -> int: ... + def __getitem__(self, key: _KT) -> _VT: ... + def __setitem__(self, key: _KT, item: _VT) -> None: ... + def __delitem__(self, key: _KT) -> None: ... + def __iter__(self) -> Iterator[_KT]: ... + def __contains__(self, key: object) -> bool: ... + def copy(self) -> Self: ... + def __copy__(self) -> Self: ... + + # `UserDict.fromkeys` has the same semantics as `dict.fromkeys`, so should be kept in line with `dict.fromkeys`. + # TODO: Much like `dict.fromkeys`, the true signature of `UserDict.fromkeys` is inexpressible in the current type system. + # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> UserDict[_T, Any | None]: ... + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: _S) -> UserDict[_T, _S]: ... + @overload + def __or__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... + @overload + def __or__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, other: UserDict[_KT, _VT] | dict[_KT, _VT]) -> Self: ... + @overload + def __ror__(self, other: UserDict[_T1, _T2] | dict[_T1, _T2]) -> UserDict[_KT | _T1, _VT | _T2]: ... + # UserDict.__ior__ should be kept roughly in line with MutableMapping.update() + @overload # type: ignore[misc] + def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... + @overload + def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... + if sys.version_info >= (3, 12): + @overload + def get(self, key: _KT, default: None = None) -> _VT | None: ... + @overload + def get(self, key: _KT, default: _VT) -> _VT: ... + @overload + def get(self, key: _KT, default: _T) -> _VT | _T: ... + +class UserList(MutableSequence[_T]): + data: list[_T] + @overload + def __init__(self, initlist: None = None) -> None: ... + @overload + def __init__(self, initlist: Iterable[_T]) -> None: ... + __hash__: ClassVar[None] # type: ignore[assignment] + def __lt__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __le__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __gt__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __ge__(self, other: list[_T] | UserList[_T]) -> bool: ... + def __eq__(self, other: object) -> bool: ... + def __contains__(self, item: object) -> bool: ... + def __len__(self) -> int: ... + @overload + def __getitem__(self, i: SupportsIndex) -> _T: ... + @overload + def __getitem__(self, i: slice) -> Self: ... + @overload + def __setitem__(self, i: SupportsIndex, item: _T) -> None: ... + @overload + def __setitem__(self, i: slice, item: Iterable[_T]) -> None: ... + def __delitem__(self, i: SupportsIndex | slice) -> None: ... + def __add__(self, other: Iterable[_T]) -> Self: ... + def __radd__(self, other: Iterable[_T]) -> Self: ... + def __iadd__(self, other: Iterable[_T]) -> Self: ... + def __mul__(self, n: int) -> Self: ... + def __rmul__(self, n: int) -> Self: ... + def __imul__(self, n: int) -> Self: ... + def append(self, item: _T) -> None: ... + def insert(self, i: int, item: _T) -> None: ... + def pop(self, i: int = -1) -> _T: ... + def remove(self, item: _T) -> None: ... + def copy(self) -> Self: ... + def __copy__(self) -> Self: ... + def count(self, item: _T) -> int: ... + # The runtime signature is "item, *args", and the arguments are then passed + # to `list.index`. In order to give more precise types, we pretend that the + # `item` argument is positional-only. + def index(self, item: _T, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ... + # All arguments are passed to `list.sort` at runtime, so the signature should be kept in line with `list.sort`. + @overload + def sort(self: UserList[SupportsRichComparisonT], *, key: None = None, reverse: bool = False) -> None: ... + @overload + def sort(self, *, key: Callable[[_T], SupportsRichComparison], reverse: bool = False) -> None: ... + def extend(self, other: Iterable[_T]) -> None: ... + +class UserString(Sequence[UserString]): + data: str + def __init__(self, seq: object) -> None: ... + def __int__(self) -> int: ... + def __float__(self) -> float: ... + def __complex__(self) -> complex: ... + def __getnewargs__(self) -> tuple[str]: ... + def __lt__(self, string: str | UserString) -> bool: ... + def __le__(self, string: str | UserString) -> bool: ... + def __gt__(self, string: str | UserString) -> bool: ... + def __ge__(self, string: str | UserString) -> bool: ... + def __eq__(self, string: object) -> bool: ... + def __hash__(self) -> int: ... + def __contains__(self, char: object) -> bool: ... + def __len__(self) -> int: ... + def __getitem__(self, index: SupportsIndex | slice) -> Self: ... + def __iter__(self) -> Iterator[Self]: ... + def __reversed__(self) -> Iterator[Self]: ... + def __add__(self, other: object) -> Self: ... + def __radd__(self, other: object) -> Self: ... + def __mul__(self, n: int) -> Self: ... + def __rmul__(self, n: int) -> Self: ... + def __mod__(self, args: Any) -> Self: ... + def __rmod__(self, template: object) -> Self: ... + def capitalize(self) -> Self: ... + def casefold(self) -> Self: ... + def center(self, width: int, *args: Any) -> Self: ... + def count(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def encode(self: UserString, encoding: str | None = "utf-8", errors: str | None = "strict") -> bytes: ... + def endswith(self, suffix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ... + def expandtabs(self, tabsize: int = 8) -> Self: ... + def find(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def format(self, *args: Any, **kwds: Any) -> str: ... + def format_map(self, mapping: Mapping[str, Any]) -> str: ... + def index(self, sub: str, start: int = 0, end: int = sys.maxsize) -> int: ... + def isalpha(self) -> bool: ... + def isalnum(self) -> bool: ... + def isdecimal(self) -> bool: ... + def isdigit(self) -> bool: ... + def isidentifier(self) -> bool: ... + def islower(self) -> bool: ... + def isnumeric(self) -> bool: ... + def isprintable(self) -> bool: ... + def isspace(self) -> bool: ... + def istitle(self) -> bool: ... + def isupper(self) -> bool: ... + def isascii(self) -> bool: ... + def join(self, seq: Iterable[str]) -> str: ... + def ljust(self, width: int, *args: Any) -> Self: ... + def lower(self) -> Self: ... + def lstrip(self, chars: str | None = None) -> Self: ... + maketrans = str.maketrans + def partition(self, sep: str) -> tuple[str, str, str]: ... + def removeprefix(self, prefix: str | UserString, /) -> Self: ... + def removesuffix(self, suffix: str | UserString, /) -> Self: ... + def replace(self, old: str | UserString, new: str | UserString, maxsplit: int = -1) -> Self: ... + def rfind(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def rindex(self, sub: str | UserString, start: int = 0, end: int = sys.maxsize) -> int: ... + def rjust(self, width: int, *args: Any) -> Self: ... + def rpartition(self, sep: str) -> tuple[str, str, str]: ... + def rstrip(self, chars: str | None = None) -> Self: ... + def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ... + def rsplit(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ... + def splitlines(self, keepends: bool = False) -> list[str]: ... + def startswith(self, prefix: str | tuple[str, ...], start: int | None = 0, end: int | None = sys.maxsize) -> bool: ... + def strip(self, chars: str | None = None) -> Self: ... + def swapcase(self) -> Self: ... + def title(self) -> Self: ... + def translate(self, *args: Any) -> Self: ... + def upper(self) -> Self: ... + def zfill(self, width: int) -> Self: ... + +class deque(MutableSequence[_T]): + @property + def maxlen(self) -> int | None: ... + @overload + def __init__(self, *, maxlen: int | None = None) -> None: ... + @overload + def __init__(self, iterable: Iterable[_T], maxlen: int | None = None) -> None: ... + def append(self, x: _T, /) -> None: ... + def appendleft(self, x: _T, /) -> None: ... + def copy(self) -> Self: ... + def count(self, x: _T, /) -> int: ... + def extend(self, iterable: Iterable[_T], /) -> None: ... + def extendleft(self, iterable: Iterable[_T], /) -> None: ... + def insert(self, i: int, x: _T, /) -> None: ... + def index(self, x: _T, start: int = 0, stop: int = ..., /) -> int: ... + def pop(self) -> _T: ... # type: ignore[override] + def popleft(self) -> _T: ... + def remove(self, value: _T, /) -> None: ... + def rotate(self, n: int = 1, /) -> None: ... + def __copy__(self) -> Self: ... + def __len__(self) -> int: ... + __hash__: ClassVar[None] # type: ignore[assignment] + # These methods of deque don't take slices, unlike MutableSequence, hence the type: ignores + def __getitem__(self, key: SupportsIndex, /) -> _T: ... # type: ignore[override] + def __setitem__(self, key: SupportsIndex, value: _T, /) -> None: ... # type: ignore[override] + def __delitem__(self, key: SupportsIndex, /) -> None: ... # type: ignore[override] + def __contains__(self, key: object, /) -> bool: ... + def __reduce__(self) -> tuple[type[Self], tuple[()], None, Iterator[_T]]: ... + def __iadd__(self, value: Iterable[_T], /) -> Self: ... + def __add__(self, value: Self, /) -> Self: ... + def __mul__(self, value: int, /) -> Self: ... + def __imul__(self, value: int, /) -> Self: ... + def __lt__(self, value: deque[_T], /) -> bool: ... + def __le__(self, value: deque[_T], /) -> bool: ... + def __gt__(self, value: deque[_T], /) -> bool: ... + def __ge__(self, value: deque[_T], /) -> bool: ... + def __eq__(self, value: object, /) -> bool: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +class Counter(dict[_T, int], Generic[_T]): + @overload + def __init__(self, iterable: None = None, /) -> None: ... + @overload + def __init__(self: Counter[str], iterable: None = None, /, **kwargs: int) -> None: ... + @overload + def __init__(self, mapping: SupportsKeysAndGetItem[_T, int], /) -> None: ... + @overload + def __init__(self, iterable: Iterable[_T], /) -> None: ... + def copy(self) -> Self: ... + def elements(self) -> Iterator[_T]: ... + def most_common(self, n: int | None = None) -> list[tuple[_T, int]]: ... + @classmethod + def fromkeys(cls, iterable: Any, v: int | None = None) -> NoReturn: ... # type: ignore[override] + @overload + def subtract(self, iterable: None = None, /) -> None: ... + @overload + def subtract(self, mapping: Mapping[_T, int], /) -> None: ... + @overload + def subtract(self, iterable: Iterable[_T], /) -> None: ... + # Unlike dict.update(), use Mapping instead of SupportsKeysAndGetItem for the first overload + # (source code does an `isinstance(other, Mapping)` check) + # + # The second overload is also deliberately different to dict.update() + # (if it were `Iterable[_T] | Iterable[tuple[_T, int]]`, + # the tuples would be added as keys, breaking type safety) + @overload # type: ignore[override] + def update(self, m: Mapping[_T, int], /, **kwargs: int) -> None: ... + @overload + def update(self, iterable: Iterable[_T], /, **kwargs: int) -> None: ... + @overload + def update(self, iterable: None = None, /, **kwargs: int) -> None: ... + def __missing__(self, key: _T) -> int: ... + def __delitem__(self, elem: object) -> None: ... + if sys.version_info >= (3, 10): + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + + def __add__(self, other: Counter[_S]) -> Counter[_T | _S]: ... + def __sub__(self, other: Counter[_T]) -> Counter[_T]: ... + def __and__(self, other: Counter[_T]) -> Counter[_T]: ... + def __or__(self, other: Counter[_S]) -> Counter[_T | _S]: ... # type: ignore[override] + def __pos__(self) -> Counter[_T]: ... + def __neg__(self) -> Counter[_T]: ... + # several type: ignores because __iadd__ is supposedly incompatible with __add__, etc. + def __iadd__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[misc] + def __isub__(self, other: SupportsItems[_T, int]) -> Self: ... + def __iand__(self, other: SupportsItems[_T, int]) -> Self: ... + def __ior__(self, other: SupportsItems[_T, int]) -> Self: ... # type: ignore[override,misc] + if sys.version_info >= (3, 10): + def total(self) -> int: ... + def __le__(self, other: Counter[Any]) -> bool: ... + def __lt__(self, other: Counter[Any]) -> bool: ... + def __ge__(self, other: Counter[Any]) -> bool: ... + def __gt__(self, other: Counter[Any]) -> bool: ... + +# The pure-Python implementations of the "views" classes +# These are exposed at runtime in `collections/__init__.py` +class _OrderedDictKeysView(KeysView[_KT_co]): + def __reversed__(self) -> Iterator[_KT_co]: ... + +class _OrderedDictItemsView(ItemsView[_KT_co, _VT_co]): + def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... + +class _OrderedDictValuesView(ValuesView[_VT_co]): + def __reversed__(self) -> Iterator[_VT_co]: ... + +# The C implementations of the "views" classes +# (At runtime, these are called `odict_keys`, `odict_items` and `odict_values`, +# but they are not exposed anywhere) +# pyright doesn't have a specific error code for subclassing error! +@final +class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + def __reversed__(self) -> Iterator[_KT_co]: ... + +@final +class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... + +@final +class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + def __reversed__(self) -> Iterator[_VT_co]: ... + +class OrderedDict(dict[_KT, _VT]): + def popitem(self, last: bool = True) -> tuple[_KT, _VT]: ... + def move_to_end(self, key: _KT, last: bool = True) -> None: ... + def copy(self) -> Self: ... + def __reversed__(self) -> Iterator[_KT]: ... + def keys(self) -> _odict_keys[_KT, _VT]: ... + def items(self) -> _odict_items[_KT, _VT]: ... + def values(self) -> _odict_values[_KT, _VT]: ... + # The signature of OrderedDict.fromkeys should be kept in line with `dict.fromkeys`, modulo positional-only differences. + # Like dict.fromkeys, its true signature is not expressible in the current type system. + # See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963. + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: None = None) -> OrderedDict[_T, Any | None]: ... + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: _S) -> OrderedDict[_T, _S]: ... + # Keep OrderedDict.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. + @overload + def setdefault(self: OrderedDict[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... + @overload + def setdefault(self, key: _KT, default: _VT) -> _VT: ... + # Same as dict.pop, but accepts keyword arguments + @overload + def pop(self, key: _KT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _VT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _T) -> _VT | _T: ... + def __eq__(self, value: object, /) -> bool: ... + @overload + def __or__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> OrderedDict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] + +class defaultdict(dict[_KT, _VT]): + default_factory: Callable[[], _VT] | None + @overload + def __init__(self) -> None: ... + @overload + def __init__(self: defaultdict[str, _VT], **kwargs: _VT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + @overload + def __init__(self, default_factory: Callable[[], _VT] | None, /) -> None: ... + @overload + def __init__( + self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + default_factory: Callable[[], _VT] | None, + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self, default_factory: Callable[[], _VT] | None, map: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... + @overload + def __init__( + self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + default_factory: Callable[[], _VT] | None, + map: SupportsKeysAndGetItem[str, _VT], + /, + **kwargs: _VT, + ) -> None: ... + @overload + def __init__(self, default_factory: Callable[[], _VT] | None, iterable: Iterable[tuple[_KT, _VT]], /) -> None: ... + @overload + def __init__( + self: defaultdict[str, _VT], # pyright: ignore[reportInvalidTypeVarUse] #11780 + default_factory: Callable[[], _VT] | None, + iterable: Iterable[tuple[str, _VT]], + /, + **kwargs: _VT, + ) -> None: ... + def __missing__(self, key: _KT, /) -> _VT: ... + def __copy__(self) -> Self: ... + def copy(self) -> Self: ... + @overload + def __or__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> Self: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> defaultdict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc] + +class ChainMap(MutableMapping[_KT, _VT]): + maps: list[MutableMapping[_KT, _VT]] + def __init__(self, *maps: MutableMapping[_KT, _VT]) -> None: ... + def new_child(self, m: MutableMapping[_KT, _VT] | None = None) -> Self: ... + @property + def parents(self) -> Self: ... + def __setitem__(self, key: _KT, value: _VT) -> None: ... + def __delitem__(self, key: _KT) -> None: ... + def __getitem__(self, key: _KT) -> _VT: ... + def __iter__(self) -> Iterator[_KT]: ... + def __len__(self) -> int: ... + def __contains__(self, key: object) -> bool: ... + @overload + def get(self, key: _KT, default: None = None) -> _VT | None: ... + @overload + def get(self, key: _KT, default: _VT) -> _VT: ... + @overload + def get(self, key: _KT, default: _T) -> _VT | _T: ... + def __missing__(self, key: _KT) -> _VT: ... # undocumented + def __bool__(self) -> bool: ... + # Keep ChainMap.setdefault in line with MutableMapping.setdefault, modulo positional-only differences. + @overload + def setdefault(self: ChainMap[_KT, _T | None], key: _KT, default: None = None) -> _T | None: ... + @overload + def setdefault(self, key: _KT, default: _VT) -> _VT: ... + @overload + def pop(self, key: _KT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _VT) -> _VT: ... + @overload + def pop(self, key: _KT, default: _T) -> _VT | _T: ... + def copy(self) -> Self: ... + __copy__ = copy + # All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime, + # so the signature should be kept in line with `dict.fromkeys`. + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T]) -> ChainMap[_T, Any | None]: ... + @classmethod + @overload + # Special-case None: the user probably wants to add non-None values later. + def fromkeys(cls, iterable: Iterable[_T], value: None, /) -> ChainMap[_T, Any | None]: ... + @classmethod + @overload + def fromkeys(cls, iterable: Iterable[_T], value: _S, /) -> ChainMap[_T, _S]: ... + @overload + def __or__(self, other: Mapping[_KT, _VT]) -> Self: ... + @overload + def __or__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, other: Mapping[_KT, _VT]) -> Self: ... + @overload + def __ror__(self, other: Mapping[_T1, _T2]) -> ChainMap[_KT | _T1, _VT | _T2]: ... + # ChainMap.__ior__ should be kept roughly in line with MutableMapping.update() + @overload # type: ignore[misc] + def __ior__(self, other: SupportsKeysAndGetItem[_KT, _VT]) -> Self: ... + @overload + def __ior__(self, other: Iterable[tuple[_KT, _VT]]) -> Self: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/collections/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/collections/abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/colorsys.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/colorsys.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/colorsys.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/colorsys.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/compileall.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compileall.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/compileall.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/compileall.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/compression/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/_common/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/compression/_common/__init__.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/_common/_streams.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/_common/_streams.pyi new file mode 100644 index 0000000000000..b8463973ec671 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/_common/_streams.pyi @@ -0,0 +1,26 @@ +from _typeshed import Incomplete, WriteableBuffer +from collections.abc import Callable +from io import DEFAULT_BUFFER_SIZE, BufferedIOBase, RawIOBase +from typing import Any, Protocol, type_check_only + +BUFFER_SIZE = DEFAULT_BUFFER_SIZE + +@type_check_only +class _Reader(Protocol): + def read(self, n: int, /) -> bytes: ... + def seekable(self) -> bool: ... + def seek(self, n: int, /) -> Any: ... + +class BaseStream(BufferedIOBase): ... + +class DecompressReader(RawIOBase): + def __init__( + self, + fp: _Reader, + decomp_factory: Callable[..., Incomplete], # Consider backporting changes to _compression + trailing_error: type[Exception] | tuple[type[Exception], ...] = (), + **decomp_args: Any, # These are passed to decomp_factory. + ) -> None: ... + def readinto(self, b: WriteableBuffer) -> int: ... + def read(self, size: int = -1) -> bytes: ... + def seek(self, offset: int, whence: int = 0) -> int: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/bz2/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/bz2/__init__.pyi new file mode 100644 index 0000000000000..9ddc39f27c286 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/bz2/__init__.pyi @@ -0,0 +1 @@ +from bz2 import * diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/gzip/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/gzip/__init__.pyi new file mode 100644 index 0000000000000..9422a735c590e --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/gzip/__init__.pyi @@ -0,0 +1 @@ +from gzip import * diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/lzma/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/lzma/__init__.pyi new file mode 100644 index 0000000000000..936c3813db4f1 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/lzma/__init__.pyi @@ -0,0 +1 @@ +from lzma import * diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/zlib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/zlib/__init__.pyi new file mode 100644 index 0000000000000..78d176c03ee83 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/zlib/__init__.pyi @@ -0,0 +1 @@ +from zlib import * diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/zstd/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/zstd/__init__.pyi new file mode 100644 index 0000000000000..24a9633c488e6 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/zstd/__init__.pyi @@ -0,0 +1,87 @@ +import enum +from _typeshed import ReadableBuffer +from collections.abc import Iterable, Mapping +from compression.zstd._zstdfile import ZstdFile, open +from typing import Final, final + +import _zstd +from _zstd import ZstdCompressor, ZstdDecompressor, ZstdDict, ZstdError, get_frame_size, zstd_version + +__all__ = ( + # compression.zstd + "COMPRESSION_LEVEL_DEFAULT", + "compress", + "CompressionParameter", + "decompress", + "DecompressionParameter", + "finalize_dict", + "get_frame_info", + "Strategy", + "train_dict", + # compression.zstd._zstdfile + "open", + "ZstdFile", + # _zstd + "get_frame_size", + "zstd_version", + "zstd_version_info", + "ZstdCompressor", + "ZstdDecompressor", + "ZstdDict", + "ZstdError", +) + +zstd_version_info: Final[tuple[int, int, int]] +COMPRESSION_LEVEL_DEFAULT: Final = _zstd.ZSTD_CLEVEL_DEFAULT + +class FrameInfo: + decompressed_size: int + dictionary_id: int + def __init__(self, decompressed_size: int, dictionary_id: int) -> None: ... + +def get_frame_info(frame_buffer: ReadableBuffer) -> FrameInfo: ... +def train_dict(samples: Iterable[ReadableBuffer], dict_size: int) -> ZstdDict: ... +def finalize_dict(zstd_dict: ZstdDict, /, samples: Iterable[ReadableBuffer], dict_size: int, level: int) -> ZstdDict: ... +def compress( + data: ReadableBuffer, level: int | None = None, options: Mapping[int, int] | None = None, zstd_dict: ZstdDict | None = None +) -> bytes: ... +def decompress(data: ReadableBuffer, zstd_dict: ZstdDict | None = None, options: Mapping[int, int] | None = None) -> bytes: ... +@final +class CompressionParameter(enum.IntEnum): + compression_level = _zstd.ZSTD_c_compressionLevel + window_log = _zstd.ZSTD_c_windowLog + hash_log = _zstd.ZSTD_c_hashLog + chain_log = _zstd.ZSTD_c_chainLog + search_log = _zstd.ZSTD_c_searchLog + min_match = _zstd.ZSTD_c_minMatch + target_length = _zstd.ZSTD_c_targetLength + strategy = _zstd.ZSTD_c_strategy + enable_long_distance_matching = _zstd.ZSTD_c_enableLongDistanceMatching + ldm_hash_log = _zstd.ZSTD_c_ldmHashLog + ldm_min_match = _zstd.ZSTD_c_ldmMinMatch + ldm_bucket_size_log = _zstd.ZSTD_c_ldmBucketSizeLog + ldm_hash_rate_log = _zstd.ZSTD_c_ldmHashRateLog + content_size_flag = _zstd.ZSTD_c_contentSizeFlag + checksum_flag = _zstd.ZSTD_c_checksumFlag + dict_id_flag = _zstd.ZSTD_c_dictIDFlag + nb_workers = _zstd.ZSTD_c_nbWorkers + job_size = _zstd.ZSTD_c_jobSize + overlap_log = _zstd.ZSTD_c_overlapLog + def bounds(self) -> tuple[int, int]: ... + +@final +class DecompressionParameter(enum.IntEnum): + window_log_max = _zstd.ZSTD_d_windowLogMax + def bounds(self) -> tuple[int, int]: ... + +@final +class Strategy(enum.IntEnum): + fast = _zstd.ZSTD_fast + dfast = _zstd.ZSTD_dfast + greedy = _zstd.ZSTD_greedy + lazy = _zstd.ZSTD_lazy + lazy2 = _zstd.ZSTD_lazy2 + btlazy2 = _zstd.ZSTD_btlazy2 + btopt = _zstd.ZSTD_btopt + btultra = _zstd.ZSTD_btultra + btultra2 = _zstd.ZSTD_btultra2 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/compression/zstd/_zstdfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compression/zstd/_zstdfile.pyi new file mode 100644 index 0000000000000..045b2d35acfe0 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/compression/zstd/_zstdfile.pyi @@ -0,0 +1,117 @@ +from _typeshed import ReadableBuffer, StrOrBytesPath, SupportsWrite, WriteableBuffer +from collections.abc import Mapping +from compression._common import _streams +from compression.zstd import ZstdDict +from io import TextIOWrapper, _WrappedBuffer +from typing import Literal, overload, type_check_only +from typing_extensions import TypeAlias + +from _zstd import ZstdCompressor, _ZstdCompressorFlushBlock, _ZstdCompressorFlushFrame + +__all__ = ("ZstdFile", "open") + +_ReadBinaryMode: TypeAlias = Literal["r", "rb"] +_WriteBinaryMode: TypeAlias = Literal["w", "wb", "x", "xb", "a", "ab"] +_ReadTextMode: TypeAlias = Literal["rt"] +_WriteTextMode: TypeAlias = Literal["wt", "xt", "at"] + +@type_check_only +class _FileBinaryRead(_streams._Reader): + def close(self) -> None: ... + +@type_check_only +class _FileBinaryWrite(SupportsWrite[bytes]): + def close(self) -> None: ... + +class ZstdFile(_streams.BaseStream): + FLUSH_BLOCK = ZstdCompressor.FLUSH_BLOCK + FLUSH_FRAME = ZstdCompressor.FLUSH_FRAME + + @overload + def __init__( + self, + file: StrOrBytesPath | _FileBinaryRead, + /, + mode: _ReadBinaryMode = "r", + *, + level: None = None, + options: Mapping[int, int] | None = None, + zstd_dict: ZstdDict | None = None, + ) -> None: ... + @overload + def __init__( + self, + file: StrOrBytesPath | _FileBinaryWrite, + /, + mode: _WriteBinaryMode, + *, + level: int | None = None, + options: Mapping[int, int] | None = None, + zstd_dict: ZstdDict | None = None, + ) -> None: ... + def write(self, data: ReadableBuffer, /) -> int: ... + def flush(self, mode: _ZstdCompressorFlushBlock | _ZstdCompressorFlushFrame = 1) -> bytes: ... # type: ignore[override] + def read(self, size: int | None = -1) -> bytes: ... + def read1(self, size: int | None = -1) -> bytes: ... + def readinto(self, b: WriteableBuffer) -> int: ... + def readinto1(self, b: WriteableBuffer) -> int: ... + def readline(self, size: int | None = -1) -> bytes: ... + def seek(self, offset: int, whence: int = 0) -> int: ... + def peek(self, size: int = -1) -> bytes: ... + @property + def name(self) -> str | bytes: ... + @property + def mode(self) -> Literal["rb", "wb"]: ... + +@overload +def open( + file: StrOrBytesPath | _FileBinaryRead, + /, + mode: _ReadBinaryMode = "rb", + *, + level: None = None, + options: Mapping[int, int] | None = None, + zstd_dict: ZstdDict | None = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, +) -> ZstdFile: ... +@overload +def open( + file: StrOrBytesPath | _FileBinaryWrite, + /, + mode: _WriteBinaryMode, + *, + level: int | None = None, + options: Mapping[int, int] | None = None, + zstd_dict: ZstdDict | None = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, +) -> ZstdFile: ... +@overload +def open( + file: StrOrBytesPath | _WrappedBuffer, + /, + mode: _ReadTextMode, + *, + level: None = None, + options: Mapping[int, int] | None = None, + zstd_dict: ZstdDict | None = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, +) -> TextIOWrapper: ... +@overload +def open( + file: StrOrBytesPath | _WrappedBuffer, + /, + mode: _WriteTextMode, + *, + level: int | None = None, + options: Mapping[int, int] | None = None, + zstd_dict: ZstdDict | None = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, +) -> TextIOWrapper: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi new file mode 100644 index 0000000000000..dd1f6da80c4d6 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi @@ -0,0 +1,71 @@ +import sys + +from ._base import ( + ALL_COMPLETED as ALL_COMPLETED, + FIRST_COMPLETED as FIRST_COMPLETED, + FIRST_EXCEPTION as FIRST_EXCEPTION, + BrokenExecutor as BrokenExecutor, + CancelledError as CancelledError, + Executor as Executor, + Future as Future, + InvalidStateError as InvalidStateError, + TimeoutError as TimeoutError, + as_completed as as_completed, + wait as wait, +) +from .process import ProcessPoolExecutor as ProcessPoolExecutor +from .thread import ThreadPoolExecutor as ThreadPoolExecutor + +if sys.version_info >= (3, 14): + from .interpreter import InterpreterPoolExecutor as InterpreterPoolExecutor + + __all__ = ( + "FIRST_COMPLETED", + "FIRST_EXCEPTION", + "ALL_COMPLETED", + "CancelledError", + "TimeoutError", + "InvalidStateError", + "BrokenExecutor", + "Future", + "Executor", + "wait", + "as_completed", + "ProcessPoolExecutor", + "ThreadPoolExecutor", + "InterpreterPoolExecutor", + ) + +elif sys.version_info >= (3, 13): + __all__ = ( + "FIRST_COMPLETED", + "FIRST_EXCEPTION", + "ALL_COMPLETED", + "CancelledError", + "TimeoutError", + "InvalidStateError", + "BrokenExecutor", + "Future", + "Executor", + "wait", + "as_completed", + "ProcessPoolExecutor", + "ThreadPoolExecutor", + ) +else: + __all__ = ( + "FIRST_COMPLETED", + "FIRST_EXCEPTION", + "ALL_COMPLETED", + "CancelledError", + "TimeoutError", + "BrokenExecutor", + "Future", + "Executor", + "wait", + "as_completed", + "ProcessPoolExecutor", + "ThreadPoolExecutor", + ) + +def __dir__() -> tuple[str, ...]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi similarity index 89% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi index 7294b69567d60..fbf07a3fc78f9 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi @@ -54,9 +54,20 @@ class Future(Generic[_T]): class Executor: def submit(self, fn: Callable[_P, _T], /, *args: _P.args, **kwargs: _P.kwargs) -> Future[_T]: ... - def map( - self, fn: Callable[..., _T], *iterables: Iterable[Any], timeout: float | None = None, chunksize: int = 1 - ) -> Iterator[_T]: ... + if sys.version_info >= (3, 14): + def map( + self, + fn: Callable[..., _T], + *iterables: Iterable[Any], + timeout: float | None = None, + chunksize: int = 1, + buffersize: int | None = None, + ) -> Iterator[_T]: ... + else: + def map( + self, fn: Callable[..., _T], *iterables: Iterable[Any], timeout: float | None = None, chunksize: int = 1 + ) -> Iterator[_T]: ... + def shutdown(self, wait: bool = True, *, cancel_futures: bool = False) -> None: ... def __enter__(self) -> Self: ... def __exit__( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/interpreter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/interpreter.pyi new file mode 100644 index 0000000000000..9c1078983d8cf --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/interpreter.pyi @@ -0,0 +1,100 @@ +import sys +from collections.abc import Callable, Mapping +from concurrent.futures import ThreadPoolExecutor +from typing import Literal, Protocol, overload, type_check_only +from typing_extensions import ParamSpec, Self, TypeAlias, TypeVar, TypeVarTuple, Unpack + +_Task: TypeAlias = tuple[bytes, Literal["function", "script"]] + +@type_check_only +class _TaskFunc(Protocol): + @overload + def __call__(self, fn: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs) -> tuple[bytes, Literal["function"]]: ... + @overload + def __call__(self, fn: str) -> tuple[bytes, Literal["script"]]: ... + +_Ts = TypeVarTuple("_Ts") +_P = ParamSpec("_P") +_R = TypeVar("_R") + +# A `type.simplenamespace` with `__name__` attribute. +@type_check_only +class _HasName(Protocol): + __name__: str + +# `_interpreters.exec` technically gives us a simple namespace. +@type_check_only +class _ExcInfo(Protocol): + formatted: str + msg: str + type: _HasName + +if sys.version_info >= (3, 14): + from concurrent.futures.thread import BrokenThreadPool, WorkerContext as ThreadWorkerContext + + from _interpreters import InterpreterError + + class ExecutionFailed(InterpreterError): + def __init__(self, excinfo: _ExcInfo) -> None: ... # type: ignore[override] + + class WorkerContext(ThreadWorkerContext): + # Parent class doesn't have `shared` argument, + @overload # type: ignore[override] + @classmethod + def prepare( + cls, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]], shared: Mapping[str, object] + ) -> tuple[Callable[[], Self], _TaskFunc]: ... + @overload # type: ignore[override] + @classmethod + def prepare( + cls, initializer: Callable[[], object], initargs: tuple[()], shared: Mapping[str, object] + ) -> tuple[Callable[[], Self], _TaskFunc]: ... + def __init__( + self, initdata: tuple[bytes, Literal["function", "script"]], shared: Mapping[str, object] | None = None + ) -> None: ... # type: ignore[override] + def __del__(self) -> None: ... + def run(self, task: _Task) -> None: ... # type: ignore[override] + + class BrokenInterpreterPool(BrokenThreadPool): ... + + class InterpreterPoolExecutor(ThreadPoolExecutor): + BROKEN: type[BrokenInterpreterPool] + + @overload # type: ignore[override] + @classmethod + def prepare_context( + cls, initializer: Callable[[], object], initargs: tuple[()], shared: Mapping[str, object] + ) -> tuple[Callable[[], WorkerContext], _TaskFunc]: ... + @overload # type: ignore[override] + @classmethod + def prepare_context( + cls, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]], shared: Mapping[str, object] + ) -> tuple[Callable[[], WorkerContext], _TaskFunc]: ... + @overload + def __init__( + self, + max_workers: int | None = None, + thread_name_prefix: str = "", + initializer: Callable[[], object] | None = None, + initargs: tuple[()] = (), + shared: Mapping[str, object] | None = None, + ) -> None: ... + @overload + def __init__( + self, + max_workers: int | None = None, + thread_name_prefix: str = "", + *, + initializer: Callable[[Unpack[_Ts]], object], + initargs: tuple[Unpack[_Ts]], + shared: Mapping[str, object] | None = None, + ) -> None: ... + @overload + def __init__( + self, + max_workers: int | None, + thread_name_prefix: str, + initializer: Callable[[Unpack[_Ts]], object], + initargs: tuple[Unpack[_Ts]], + shared: Mapping[str, object] | None = None, + ) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi index 9c904f793fa9f..607990100369e 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi @@ -236,3 +236,7 @@ class ProcessPoolExecutor(Executor): def _start_executor_manager_thread(self) -> None: ... def _adjust_process_count(self) -> None: ... + + if sys.version_info >= (3, 14): + def kill_workers(self) -> None: ... + def terminate_workers(self) -> None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi new file mode 100644 index 0000000000000..50a6a9c6f43ea --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi @@ -0,0 +1,140 @@ +import queue +import sys +from collections.abc import Callable, Iterable, Mapping, Set as AbstractSet +from threading import Lock, Semaphore, Thread +from types import GenericAlias +from typing import Any, Generic, Protocol, TypeVar, overload, type_check_only +from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack +from weakref import ref + +from ._base import BrokenExecutor, Executor, Future + +_Ts = TypeVarTuple("_Ts") + +_threads_queues: Mapping[Any, Any] +_shutdown: bool +_global_shutdown_lock: Lock + +def _python_exit() -> None: ... + +_S = TypeVar("_S") + +_Task: TypeAlias = tuple[Callable[..., Any], tuple[Any, ...], dict[str, Any]] + +_C = TypeVar("_C", bound=Callable[..., object]) +_KT = TypeVar("_KT", bound=str) +_VT = TypeVar("_VT") + +@type_check_only +class _ResolveTaskFunc(Protocol): + def __call__( + self, func: _C, args: tuple[Unpack[_Ts]], kwargs: dict[_KT, _VT] + ) -> tuple[_C, tuple[Unpack[_Ts]], dict[_KT, _VT]]: ... + +if sys.version_info >= (3, 14): + class WorkerContext: + @overload + @classmethod + def prepare( + cls, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]] + ) -> tuple[Callable[[], Self], _ResolveTaskFunc]: ... + @overload + @classmethod + def prepare( + cls, initializer: Callable[[], object], initargs: tuple[()] + ) -> tuple[Callable[[], Self], _ResolveTaskFunc]: ... + @overload + def __init__(self, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]]) -> None: ... + @overload + def __init__(self, initializer: Callable[[], object], initargs: tuple[()]) -> None: ... + def initialize(self) -> None: ... + def finalize(self) -> None: ... + def run(self, task: _Task) -> None: ... + +if sys.version_info >= (3, 14): + class _WorkItem(Generic[_S]): + future: Future[Any] + task: _Task + def __init__(self, future: Future[Any], task: _Task) -> None: ... + def run(self, ctx: WorkerContext) -> None: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + + def _worker(executor_reference: ref[Any], ctx: WorkerContext, work_queue: queue.SimpleQueue[Any]) -> None: ... + +else: + class _WorkItem(Generic[_S]): + future: Future[_S] + fn: Callable[..., _S] + args: Iterable[Any] + kwargs: Mapping[str, Any] + def __init__(self, future: Future[_S], fn: Callable[..., _S], args: Iterable[Any], kwargs: Mapping[str, Any]) -> None: ... + def run(self) -> None: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + + def _worker( + executor_reference: ref[Any], + work_queue: queue.SimpleQueue[Any], + initializer: Callable[[Unpack[_Ts]], object], + initargs: tuple[Unpack[_Ts]], + ) -> None: ... + +class BrokenThreadPool(BrokenExecutor): ... + +class ThreadPoolExecutor(Executor): + if sys.version_info >= (3, 14): + BROKEN: type[BrokenThreadPool] + + _max_workers: int + _idle_semaphore: Semaphore + _threads: AbstractSet[Thread] + _broken: bool + _shutdown: bool + _shutdown_lock: Lock + _thread_name_prefix: str | None + if sys.version_info >= (3, 14): + _create_worker_context: Callable[[], WorkerContext] + _resolve_work_item_task: _ResolveTaskFunc + else: + _initializer: Callable[..., None] | None + _initargs: tuple[Any, ...] + _work_queue: queue.SimpleQueue[_WorkItem[Any]] + + if sys.version_info >= (3, 14): + @overload + @classmethod + def prepare_context( + cls, initializer: Callable[[], object], initargs: tuple[()] + ) -> tuple[Callable[[], WorkerContext], _ResolveTaskFunc]: ... + @overload + @classmethod + def prepare_context( + cls, initializer: Callable[[Unpack[_Ts]], object], initargs: tuple[Unpack[_Ts]] + ) -> tuple[Callable[[], WorkerContext], _ResolveTaskFunc]: ... + + @overload + def __init__( + self, + max_workers: int | None = None, + thread_name_prefix: str = "", + initializer: Callable[[], object] | None = None, + initargs: tuple[()] = (), + ) -> None: ... + @overload + def __init__( + self, + max_workers: int | None = None, + thread_name_prefix: str = "", + *, + initializer: Callable[[Unpack[_Ts]], object], + initargs: tuple[Unpack[_Ts]], + ) -> None: ... + @overload + def __init__( + self, + max_workers: int | None, + thread_name_prefix: str, + initializer: Callable[[Unpack[_Ts]], object], + initargs: tuple[Unpack[_Ts]], + ) -> None: ... + def _adjust_thread_count(self) -> None: ... + def _initializer_failed(self) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/configparser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/configparser.pyi similarity index 94% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/configparser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/configparser.pyi index 8996c85d9a53f..15c564c025897 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/configparser.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/configparser.pyi @@ -5,7 +5,33 @@ from re import Pattern from typing import Any, ClassVar, Final, Literal, TypeVar, overload from typing_extensions import TypeAlias -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 14): + __all__ = ( + "NoSectionError", + "DuplicateOptionError", + "DuplicateSectionError", + "NoOptionError", + "InterpolationError", + "InterpolationDepthError", + "InterpolationMissingOptionError", + "InterpolationSyntaxError", + "ParsingError", + "MissingSectionHeaderError", + "MultilineContinuationError", + "UnnamedSectionDisabledError", + "InvalidWriteError", + "ConfigParser", + "RawConfigParser", + "Interpolation", + "BasicInterpolation", + "ExtendedInterpolation", + "SectionProxy", + "ConverterMapping", + "DEFAULTSECT", + "MAX_INTERPOLATION_DEPTH", + "UNNAMED_SECTION", + ) +elif sys.version_info >= (3, 13): __all__ = ( "NoSectionError", "DuplicateOptionError", @@ -429,3 +455,10 @@ if sys.version_info >= (3, 13): lineno: int line: str def __init__(self, filename: str, lineno: int, line: str) -> None: ... + +if sys.version_info >= (3, 14): + class UnnamedSectionDisabledError(Error): + msg: Final = "Support for UNNAMED_SECTION is disabled." + def __init__(self) -> None: ... + + class InvalidWriteError(Error): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi index 70d0dbdcb2f10..4663b448c79c8 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi @@ -179,7 +179,7 @@ class AsyncExitStack(_BaseExitStack[_ExitT_co], metaclass=abc.ABCMeta): async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / - ) -> bool: ... + ) -> _ExitT_co: ... if sys.version_info >= (3, 10): class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextvars.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/contextvars.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/contextvars.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/contextvars.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/copy.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/copy.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/copy.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/copy.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/copyreg.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/copyreg.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/copyreg.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/copyreg.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/crypt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/crypt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/crypt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/crypt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/csv.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/csv.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/csv.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/csv.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi new file mode 100644 index 0000000000000..0b14bd856784c --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi @@ -0,0 +1,325 @@ +import sys +from _ctypes import ( + RTLD_GLOBAL as RTLD_GLOBAL, + RTLD_LOCAL as RTLD_LOCAL, + Array as Array, + CFuncPtr as _CFuncPtr, + Structure as Structure, + Union as Union, + _CanCastTo as _CanCastTo, + _CArgObject as _CArgObject, + _CData as _CData, + _CDataType as _CDataType, + _CField as _CField, + _Pointer as _Pointer, + _PointerLike as _PointerLike, + _SimpleCData as _SimpleCData, + addressof as addressof, + alignment as alignment, + byref as byref, + get_errno as get_errno, + resize as resize, + set_errno as set_errno, + sizeof as sizeof, +) +from _typeshed import StrPath +from ctypes._endian import BigEndianStructure as BigEndianStructure, LittleEndianStructure as LittleEndianStructure +from types import GenericAlias +from typing import Any, ClassVar, Generic, Literal, TypeVar, overload, type_check_only +from typing_extensions import Self, TypeAlias, deprecated + +if sys.platform == "win32": + from _ctypes import FormatError as FormatError, get_last_error as get_last_error, set_last_error as set_last_error + + if sys.version_info >= (3, 14): + from _ctypes import COMError as COMError + +if sys.version_info >= (3, 11): + from ctypes._endian import BigEndianUnion as BigEndianUnion, LittleEndianUnion as LittleEndianUnion + +_CT = TypeVar("_CT", bound=_CData) +_T = TypeVar("_T", default=Any) +_DLLT = TypeVar("_DLLT", bound=CDLL) + +if sys.version_info >= (3, 14): + @overload + @deprecated("ctypes.POINTER with string") + def POINTER(cls: str) -> type[Any]: ... + @overload + def POINTER(cls: None) -> type[c_void_p]: ... + @overload + def POINTER(cls: type[_CT]) -> type[_Pointer[_CT]]: ... + def pointer(obj: _CT) -> _Pointer[_CT]: ... + +else: + from _ctypes import POINTER as POINTER, pointer as pointer + +DEFAULT_MODE: int + +class ArgumentError(Exception): ... + +# defined within CDLL.__init__ +# Runtime name is ctypes.CDLL.__init__.._FuncPtr +@type_check_only +class _CDLLFuncPointer(_CFuncPtr): + _flags_: ClassVar[int] + _restype_: ClassVar[type[_CDataType]] + +# Not a real class; _CDLLFuncPointer with a __name__ set on it. +@type_check_only +class _NamedFuncPointer(_CDLLFuncPointer): + __name__: str + +if sys.version_info >= (3, 12): + _NameTypes: TypeAlias = StrPath | None +else: + _NameTypes: TypeAlias = str | None + +class CDLL: + _func_flags_: ClassVar[int] + _func_restype_: ClassVar[type[_CDataType]] + _name: str + _handle: int + _FuncPtr: type[_CDLLFuncPointer] + def __init__( + self, + name: _NameTypes, + mode: int = ..., + handle: int | None = None, + use_errno: bool = False, + use_last_error: bool = False, + winmode: int | None = None, + ) -> None: ... + def __getattr__(self, name: str) -> _NamedFuncPointer: ... + def __getitem__(self, name_or_ordinal: str) -> _NamedFuncPointer: ... + +if sys.platform == "win32": + class OleDLL(CDLL): ... + class WinDLL(CDLL): ... + +class PyDLL(CDLL): ... + +class LibraryLoader(Generic[_DLLT]): + def __init__(self, dlltype: type[_DLLT]) -> None: ... + def __getattr__(self, name: str) -> _DLLT: ... + def __getitem__(self, name: str) -> _DLLT: ... + def LoadLibrary(self, name: str) -> _DLLT: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +cdll: LibraryLoader[CDLL] +if sys.platform == "win32": + windll: LibraryLoader[WinDLL] + oledll: LibraryLoader[OleDLL] +pydll: LibraryLoader[PyDLL] +pythonapi: PyDLL + +# Class definition within CFUNCTYPE / WINFUNCTYPE / PYFUNCTYPE +# Names at runtime are +# ctypes.CFUNCTYPE..CFunctionType +# ctypes.WINFUNCTYPE..WinFunctionType +# ctypes.PYFUNCTYPE..CFunctionType +@type_check_only +class _CFunctionType(_CFuncPtr): + _argtypes_: ClassVar[list[type[_CData | _CDataType]]] + _restype_: ClassVar[type[_CData | _CDataType] | None] + _flags_: ClassVar[int] + +# Alias for either function pointer type +_FuncPointer: TypeAlias = _CDLLFuncPointer | _CFunctionType # noqa: Y047 # not used here + +def CFUNCTYPE( + restype: type[_CData | _CDataType] | None, + *argtypes: type[_CData | _CDataType], + use_errno: bool = False, + use_last_error: bool = False, +) -> type[_CFunctionType]: ... + +if sys.platform == "win32": + def WINFUNCTYPE( + restype: type[_CData | _CDataType] | None, + *argtypes: type[_CData | _CDataType], + use_errno: bool = False, + use_last_error: bool = False, + ) -> type[_CFunctionType]: ... + +def PYFUNCTYPE(restype: type[_CData | _CDataType] | None, *argtypes: type[_CData | _CDataType]) -> type[_CFunctionType]: ... + +# Any type that can be implicitly converted to c_void_p when passed as a C function argument. +# (bytes is not included here, see below.) +_CVoidPLike: TypeAlias = _PointerLike | Array[Any] | _CArgObject | int +# Same as above, but including types known to be read-only (i. e. bytes). +# This distinction is not strictly necessary (ctypes doesn't differentiate between const +# and non-const pointers), but it catches errors like memmove(b'foo', buf, 4) +# when memmove(buf, b'foo', 4) was intended. +_CVoidConstPLike: TypeAlias = _CVoidPLike | bytes + +_CastT = TypeVar("_CastT", bound=_CanCastTo) + +def cast(obj: _CData | _CDataType | _CArgObject | int, typ: type[_CastT]) -> _CastT: ... +def create_string_buffer(init: int | bytes, size: int | None = None) -> Array[c_char]: ... + +c_buffer = create_string_buffer + +def create_unicode_buffer(init: int | str, size: int | None = None) -> Array[c_wchar]: ... +@deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.15") +def SetPointerType(pointer: type[_Pointer[Any]], cls: Any) -> None: ... +def ARRAY(typ: _CT, len: int) -> Array[_CT]: ... # Soft Deprecated, no plans to remove + +if sys.platform == "win32": + def DllCanUnloadNow() -> int: ... + def DllGetClassObject(rclsid: Any, riid: Any, ppv: Any) -> int: ... # TODO: not documented + + # Actually just an instance of _NamedFuncPointer (aka _CDLLFuncPointer), + # but we want to set a more specific __call__ + @type_check_only + class _GetLastErrorFunctionType(_NamedFuncPointer): + def __call__(self) -> int: ... + + GetLastError: _GetLastErrorFunctionType + +# Actually just an instance of _CFunctionType, but we want to set a more +# specific __call__. +@type_check_only +class _MemmoveFunctionType(_CFunctionType): + def __call__(self, dst: _CVoidPLike, src: _CVoidConstPLike, count: int) -> int: ... + +memmove: _MemmoveFunctionType + +# Actually just an instance of _CFunctionType, but we want to set a more +# specific __call__. +@type_check_only +class _MemsetFunctionType(_CFunctionType): + def __call__(self, dst: _CVoidPLike, c: int, count: int) -> int: ... + +memset: _MemsetFunctionType + +def string_at(ptr: _CVoidConstPLike, size: int = -1) -> bytes: ... + +if sys.platform == "win32": + def WinError(code: int | None = None, descr: str | None = None) -> OSError: ... + +def wstring_at(ptr: _CVoidConstPLike, size: int = -1) -> str: ... + +if sys.version_info >= (3, 14): + def memoryview_at(ptr: _CVoidConstPLike, size: int, readonly: bool = False) -> memoryview: ... + +class py_object(_CanCastTo, _SimpleCData[_T]): + _type_: ClassVar[Literal["O"]] + if sys.version_info >= (3, 14): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +class c_bool(_SimpleCData[bool]): + _type_: ClassVar[Literal["?"]] + def __init__(self, value: bool = ...) -> None: ... + +class c_byte(_SimpleCData[int]): + _type_: ClassVar[Literal["b"]] + +class c_ubyte(_SimpleCData[int]): + _type_: ClassVar[Literal["B"]] + +class c_short(_SimpleCData[int]): + _type_: ClassVar[Literal["h"]] + +class c_ushort(_SimpleCData[int]): + _type_: ClassVar[Literal["H"]] + +class c_long(_SimpleCData[int]): + _type_: ClassVar[Literal["l"]] + +class c_ulong(_SimpleCData[int]): + _type_: ClassVar[Literal["L"]] + +class c_int(_SimpleCData[int]): # can be an alias for c_long + _type_: ClassVar[Literal["i", "l"]] + +class c_uint(_SimpleCData[int]): # can be an alias for c_ulong + _type_: ClassVar[Literal["I", "L"]] + +class c_longlong(_SimpleCData[int]): # can be an alias for c_long + _type_: ClassVar[Literal["q", "l"]] + +class c_ulonglong(_SimpleCData[int]): # can be an alias for c_ulong + _type_: ClassVar[Literal["Q", "L"]] + +c_int8 = c_byte +c_uint8 = c_ubyte + +class c_int16(_SimpleCData[int]): # can be an alias for c_short or c_int + _type_: ClassVar[Literal["h", "i"]] + +class c_uint16(_SimpleCData[int]): # can be an alias for c_ushort or c_uint + _type_: ClassVar[Literal["H", "I"]] + +class c_int32(_SimpleCData[int]): # can be an alias for c_int or c_long + _type_: ClassVar[Literal["i", "l"]] + +class c_uint32(_SimpleCData[int]): # can be an alias for c_uint or c_ulong + _type_: ClassVar[Literal["I", "L"]] + +class c_int64(_SimpleCData[int]): # can be an alias for c_long or c_longlong + _type_: ClassVar[Literal["l", "q"]] + +class c_uint64(_SimpleCData[int]): # can be an alias for c_ulong or c_ulonglong + _type_: ClassVar[Literal["L", "Q"]] + +class c_ssize_t(_SimpleCData[int]): # alias for c_int, c_long, or c_longlong + _type_: ClassVar[Literal["i", "l", "q"]] + +class c_size_t(_SimpleCData[int]): # alias for c_uint, c_ulong, or c_ulonglong + _type_: ClassVar[Literal["I", "L", "Q"]] + +class c_float(_SimpleCData[float]): + _type_: ClassVar[Literal["f"]] + +class c_double(_SimpleCData[float]): + _type_: ClassVar[Literal["d"]] + +class c_longdouble(_SimpleCData[float]): # can be an alias for c_double + _type_: ClassVar[Literal["d", "g"]] + +if sys.version_info >= (3, 14) and sys.platform != "win32": + class c_double_complex(_SimpleCData[complex]): + _type_: ClassVar[Literal["D"]] + + class c_float_complex(_SimpleCData[complex]): + _type_: ClassVar[Literal["F"]] + + class c_longdouble_complex(_SimpleCData[complex]): + _type_: ClassVar[Literal["G"]] + +class c_char(_SimpleCData[bytes]): + _type_: ClassVar[Literal["c"]] + def __init__(self, value: int | bytes | bytearray = ...) -> None: ... + +class c_char_p(_PointerLike, _SimpleCData[bytes | None]): + _type_: ClassVar[Literal["z"]] + def __init__(self, value: int | bytes | None = ...) -> None: ... + @classmethod + def from_param(cls, value: Any, /) -> Self | _CArgObject: ... + +class c_void_p(_PointerLike, _SimpleCData[int | None]): + _type_: ClassVar[Literal["P"]] + @classmethod + def from_param(cls, value: Any, /) -> Self | _CArgObject: ... + +c_voidp = c_void_p # backwards compatibility (to a bug) + +class c_wchar(_SimpleCData[str]): + _type_: ClassVar[Literal["u"]] + +class c_wchar_p(_PointerLike, _SimpleCData[str | None]): + _type_: ClassVar[Literal["Z"]] + def __init__(self, value: int | str | None = ...) -> None: ... + @classmethod + def from_param(cls, value: Any, /) -> Self | _CArgObject: ... + +if sys.platform == "win32": + class HRESULT(_SimpleCData[int]): # TODO: undocumented + _type_: ClassVar[Literal["l"]] + +if sys.version_info >= (3, 12): + # At runtime, this is an alias for either c_int32 or c_int64, + # which are themselves an alias for one of c_int, c_long, or c_longlong + # This covers all our bases. + c_time_t: type[c_int32 | c_int64 | c_int | c_long | c_longlong] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/util.pyi new file mode 100644 index 0000000000000..4f18c1d8db345 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/util.pyi @@ -0,0 +1,11 @@ +import sys + +def find_library(name: str) -> str | None: ... + +if sys.platform == "win32": + def find_msvcrt() -> str | None: ... + +if sys.version_info >= (3, 14): + def dllist() -> list[str]: ... + +def test() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi index 63f117787aa0b..e9ed0df24dd13 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi @@ -83,6 +83,15 @@ HACCEL = HANDLE HBITMAP = HANDLE HBRUSH = HANDLE HCOLORSPACE = HANDLE +if sys.version_info >= (3, 14): + HCONV = HANDLE + HCONVLIST = HANDLE + HCURSOR = HANDLE + HDDEDATA = HANDLE + HDROP = HANDLE + HFILE = INT + HRESULT = LONG + HSZ = HANDLE HDC = HANDLE HDESK = HANDLE HDWP = HANDLE diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.pyi new file mode 100644 index 0000000000000..5c157fd7c2f61 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.pyi @@ -0,0 +1,40 @@ +import sys +from _curses import * +from _curses import window as window +from _typeshed import structseq +from collections.abc import Callable +from typing import Final, TypeVar, final, type_check_only +from typing_extensions import Concatenate, ParamSpec + +# NOTE: The _curses module is ordinarily only available on Unix, but the +# windows-curses package makes it available on Windows as well with the same +# contents. + +_T = TypeVar("_T") +_P = ParamSpec("_P") + +# available after calling `curses.initscr()` +LINES: int +COLS: int + +# available after calling `curses.start_color()` +COLORS: int +COLOR_PAIRS: int + +def wrapper(func: Callable[Concatenate[window, _P], _T], /, *arg: _P.args, **kwds: _P.kwargs) -> _T: ... + +# At runtime this class is unexposed and calls itself curses.ncurses_version. +# That name would conflict with the actual curses.ncurses_version, which is +# an instance of this class. +@final +@type_check_only +class _ncurses_version(structseq[int], tuple[int, int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("major", "minor", "patch") + + @property + def major(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def patch(self) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/ascii.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/ascii.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/ascii.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/ascii.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/has_key.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/has_key.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/has_key.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/has_key.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/panel.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/panel.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/panel.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/panel.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/textpad.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/textpad.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/textpad.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/textpad.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi new file mode 100644 index 0000000000000..c76b0b0e61e27 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi @@ -0,0 +1,457 @@ +import enum +import sys +import types +from _typeshed import DataclassInstance +from builtins import type as Type # alias to avoid name clashes with fields named "type" +from collections.abc import Callable, Iterable, Mapping +from types import GenericAlias +from typing import Any, Generic, Literal, Protocol, TypeVar, overload, type_check_only +from typing_extensions import Never, TypeIs + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +__all__ = [ + "dataclass", + "field", + "Field", + "FrozenInstanceError", + "InitVar", + "MISSING", + "fields", + "asdict", + "astuple", + "make_dataclass", + "replace", + "is_dataclass", +] + +if sys.version_info >= (3, 10): + __all__ += ["KW_ONLY"] + +_DataclassT = TypeVar("_DataclassT", bound=DataclassInstance) + +@type_check_only +class _DataclassFactory(Protocol): + def __call__( + self, + cls: type[_T], + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + weakref_slot: bool = False, + ) -> type[_T]: ... + +# define _MISSING_TYPE as an enum within the type stubs, +# even though that is not really its type at runtime +# this allows us to use Literal[_MISSING_TYPE.MISSING] +# for background, see: +# https://github.com/python/typeshed/pull/5900#issuecomment-895513797 +class _MISSING_TYPE(enum.Enum): + MISSING = enum.auto() + +MISSING = _MISSING_TYPE.MISSING + +if sys.version_info >= (3, 10): + class KW_ONLY: ... + +@overload +def asdict(obj: DataclassInstance) -> dict[str, Any]: ... +@overload +def asdict(obj: DataclassInstance, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T: ... +@overload +def astuple(obj: DataclassInstance) -> tuple[Any, ...]: ... +@overload +def astuple(obj: DataclassInstance, *, tuple_factory: Callable[[list[Any]], _T]) -> _T: ... + +if sys.version_info >= (3, 11): + @overload + def dataclass( + cls: type[_T], + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + weakref_slot: bool = False, + ) -> type[_T]: ... + @overload + def dataclass( + cls: None = None, + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + weakref_slot: bool = False, + ) -> Callable[[type[_T]], type[_T]]: ... + +elif sys.version_info >= (3, 10): + @overload + def dataclass( + cls: type[_T], + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + ) -> type[_T]: ... + @overload + def dataclass( + cls: None = None, + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + ) -> Callable[[type[_T]], type[_T]]: ... + +else: + @overload + def dataclass( + cls: type[_T], + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + ) -> type[_T]: ... + @overload + def dataclass( + cls: None = None, + /, + *, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + ) -> Callable[[type[_T]], type[_T]]: ... + +# See https://github.com/python/mypy/issues/10750 +class _DefaultFactory(Protocol[_T_co]): + def __call__(self) -> _T_co: ... + +class Field(Generic[_T]): + name: str + type: Type[_T] | str | Any + default: _T | Literal[_MISSING_TYPE.MISSING] + default_factory: _DefaultFactory[_T] | Literal[_MISSING_TYPE.MISSING] + repr: bool + hash: bool | None + init: bool + compare: bool + metadata: types.MappingProxyType[Any, Any] + + if sys.version_info >= (3, 14): + doc: str | None + + if sys.version_info >= (3, 10): + kw_only: bool | Literal[_MISSING_TYPE.MISSING] + + if sys.version_info >= (3, 14): + def __init__( + self, + default: _T, + default_factory: Callable[[], _T], + init: bool, + repr: bool, + hash: bool | None, + compare: bool, + metadata: Mapping[Any, Any], + kw_only: bool, + doc: str | None, + ) -> None: ... + elif sys.version_info >= (3, 10): + def __init__( + self, + default: _T, + default_factory: Callable[[], _T], + init: bool, + repr: bool, + hash: bool | None, + compare: bool, + metadata: Mapping[Any, Any], + kw_only: bool, + ) -> None: ... + else: + def __init__( + self, + default: _T, + default_factory: Callable[[], _T], + init: bool, + repr: bool, + hash: bool | None, + compare: bool, + metadata: Mapping[Any, Any], + ) -> None: ... + + def __set_name__(self, owner: Type[Any], name: str) -> None: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +# NOTE: Actual return type is 'Field[_T]', but we want to help type checkers +# to understand the magic that happens at runtime. +if sys.version_info >= (3, 14): + @overload # `default` and `default_factory` are optional and mutually exclusive. + def field( + *, + default: _T, + default_factory: Literal[_MISSING_TYPE.MISSING] = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., + doc: str | None = None, + ) -> _T: ... + @overload + def field( + *, + default: Literal[_MISSING_TYPE.MISSING] = ..., + default_factory: Callable[[], _T], + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., + doc: str | None = None, + ) -> _T: ... + @overload + def field( + *, + default: Literal[_MISSING_TYPE.MISSING] = ..., + default_factory: Literal[_MISSING_TYPE.MISSING] = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., + doc: str | None = None, + ) -> Any: ... + +elif sys.version_info >= (3, 10): + @overload # `default` and `default_factory` are optional and mutually exclusive. + def field( + *, + default: _T, + default_factory: Literal[_MISSING_TYPE.MISSING] = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., + ) -> _T: ... + @overload + def field( + *, + default: Literal[_MISSING_TYPE.MISSING] = ..., + default_factory: Callable[[], _T], + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., + ) -> _T: ... + @overload + def field( + *, + default: Literal[_MISSING_TYPE.MISSING] = ..., + default_factory: Literal[_MISSING_TYPE.MISSING] = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + kw_only: bool | Literal[_MISSING_TYPE.MISSING] = ..., + ) -> Any: ... + +else: + @overload # `default` and `default_factory` are optional and mutually exclusive. + def field( + *, + default: _T, + default_factory: Literal[_MISSING_TYPE.MISSING] = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + ) -> _T: ... + @overload + def field( + *, + default: Literal[_MISSING_TYPE.MISSING] = ..., + default_factory: Callable[[], _T], + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + ) -> _T: ... + @overload + def field( + *, + default: Literal[_MISSING_TYPE.MISSING] = ..., + default_factory: Literal[_MISSING_TYPE.MISSING] = ..., + init: bool = True, + repr: bool = True, + hash: bool | None = None, + compare: bool = True, + metadata: Mapping[Any, Any] | None = None, + ) -> Any: ... + +def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tuple[Field[Any], ...]: ... + +# HACK: `obj: Never` typing matches if object argument is using `Any` type. +@overload +def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] +@overload +def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ... +@overload +def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... + +class FrozenInstanceError(AttributeError): ... + +class InitVar(Generic[_T]): + type: Type[_T] + def __init__(self, type: Type[_T]) -> None: ... + @overload + def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm] + @overload + def __class_getitem__(cls, type: Any) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm] + +if sys.version_info >= (3, 14): + def make_dataclass( + cls_name: str, + fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], + *, + bases: tuple[type, ...] = (), + namespace: dict[str, Any] | None = None, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + weakref_slot: bool = False, + module: str | None = None, + decorator: _DataclassFactory = ..., + ) -> type: ... + +elif sys.version_info >= (3, 12): + def make_dataclass( + cls_name: str, + fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], + *, + bases: tuple[type, ...] = (), + namespace: dict[str, Any] | None = None, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + weakref_slot: bool = False, + module: str | None = None, + ) -> type: ... + +elif sys.version_info >= (3, 11): + def make_dataclass( + cls_name: str, + fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], + *, + bases: tuple[type, ...] = (), + namespace: dict[str, Any] | None = None, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + weakref_slot: bool = False, + ) -> type: ... + +elif sys.version_info >= (3, 10): + def make_dataclass( + cls_name: str, + fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], + *, + bases: tuple[type, ...] = (), + namespace: dict[str, Any] | None = None, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + match_args: bool = True, + kw_only: bool = False, + slots: bool = False, + ) -> type: ... + +else: + def make_dataclass( + cls_name: str, + fields: Iterable[str | tuple[str, Any] | tuple[str, Any, Any]], + *, + bases: tuple[type, ...] = (), + namespace: dict[str, Any] | None = None, + init: bool = True, + repr: bool = True, + eq: bool = True, + order: bool = False, + unsafe_hash: bool = False, + frozen: bool = False, + ) -> type: ... + +def replace(obj: _DataclassT, /, **changes: Any) -> _DataclassT: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/datetime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/datetime.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi index 72fb5fceb1fb2..37d6a06dfff95 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/datetime.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi @@ -73,6 +73,11 @@ class date: @property def day(self) -> int: ... def ctime(self) -> str: ... + + if sys.version_info >= (3, 14): + @classmethod + def strptime(cls, date_string: str, format: str, /) -> Self: ... + # On <3.12, the name of the parameter in the pure-Python implementation # didn't match the name in the C implementation, # meaning it is only *safe* to pass it as a keyword argument on 3.12+ @@ -142,6 +147,11 @@ class time: def isoformat(self, timespec: str = ...) -> str: ... @classmethod def fromisoformat(cls, time_string: str, /) -> Self: ... + + if sys.version_info >= (3, 14): + @classmethod + def strptime(cls, date_string: str, format: str, /) -> Self: ... + # On <3.12, the name of the parameter in the pure-Python implementation # didn't match the name in the C implementation, # meaning it is only *safe* to pass it as a keyword argument on 3.12+ diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/decimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/decimal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi index 4ded21e0b017e..b85c000800920 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/decimal.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi @@ -1,4 +1,5 @@ import numbers +import sys from _decimal import ( HAVE_CONTEXTVAR as HAVE_CONTEXTVAR, HAVE_THREADS as HAVE_THREADS, @@ -28,6 +29,9 @@ from types import TracebackType from typing import Any, ClassVar, Literal, NamedTuple, final, overload, type_check_only from typing_extensions import Self, TypeAlias +if sys.version_info >= (3, 14): + from _decimal import IEEE_CONTEXT_MAX_BITS as IEEE_CONTEXT_MAX_BITS, IEEEContext as IEEEContext + _Decimal: TypeAlias = Decimal | int _DecimalNew: TypeAlias = Decimal | float | str | tuple[int, Sequence[int], int] _ComparableNum: TypeAlias = Decimal | float | numbers.Rational @@ -66,6 +70,10 @@ class FloatOperation(DecimalException, TypeError): ... class Decimal: def __new__(cls, value: _DecimalNew = "0", context: Context | None = None) -> Self: ... + if sys.version_info >= (3, 14): + @classmethod + def from_number(cls, number: Decimal | float, /) -> Self: ... + @classmethod def from_float(cls, f: float, /) -> Self: ... def __bool__(self) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/difflib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/difflib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/difflib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/difflib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dis.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dis.pyi similarity index 77% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dis.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dis.pyi index cb69eac89c920..86b6d01e3120d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/dis.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dis.pyi @@ -106,11 +106,40 @@ class Instruction(_Instruction): def jump_target(self) -> int: ... @property def is_jump_target(self) -> bool: ... + if sys.version_info >= (3, 14): + @staticmethod + def make( + opname: str, + arg: int | None, + argval: Any, + argrepr: str, + offset: int, + start_offset: int, + starts_line: bool, + line_number: int | None, + label: int | None = None, + positions: Positions | None = None, + cache_info: list[tuple[str, int, Any]] | None = None, + ) -> Instruction: ... class Bytecode: codeobj: types.CodeType first_line: int - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + show_positions: bool + # 3.14 added `show_positions` + def __init__( + self, + x: _HaveCodeType | str, + *, + first_line: int | None = None, + current_offset: int | None = None, + show_caches: bool = False, + adaptive: bool = False, + show_offsets: bool = False, + show_positions: bool = False, + ) -> None: ... + elif sys.version_info >= (3, 13): show_offsets: bool # 3.13 added `show_offsets` def __init__( @@ -156,7 +185,39 @@ def findlinestarts(code: _HaveCodeType) -> Iterator[tuple[int, int]]: ... def pretty_flags(flags: int) -> str: ... def code_info(x: _HaveCodeType | str) -> str: ... -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 14): + # 3.14 added `show_positions` + def dis( + x: _HaveCodeType | str | bytes | bytearray | None = None, + *, + file: IO[str] | None = None, + depth: int | None = None, + show_caches: bool = False, + adaptive: bool = False, + show_offsets: bool = False, + show_positions: bool = False, + ) -> None: ... + def disassemble( + co: _HaveCodeType, + lasti: int = -1, + *, + file: IO[str] | None = None, + show_caches: bool = False, + adaptive: bool = False, + show_offsets: bool = False, + show_positions: bool = False, + ) -> None: ... + def distb( + tb: types.TracebackType | None = None, + *, + file: IO[str] | None = None, + show_caches: bool = False, + adaptive: bool = False, + show_offsets: bool = False, + show_positions: bool = False, + ) -> None: ... + +elif sys.version_info >= (3, 13): # 3.13 added `show_offsets` def dis( x: _HaveCodeType | str | bytes | bytearray | None = None, @@ -184,10 +245,6 @@ if sys.version_info >= (3, 13): adaptive: bool = False, show_offsets: bool = False, ) -> None: ... - # 3.13 made `show_cache` `None` by default - def get_instructions( - x: _HaveCodeType, *, first_line: int | None = None, show_caches: bool | None = None, adaptive: bool = False - ) -> Iterator[Instruction]: ... elif sys.version_info >= (3, 11): # 3.11 added `show_caches` and `adaptive` @@ -205,9 +262,6 @@ elif sys.version_info >= (3, 11): def distb( tb: types.TracebackType | None = None, *, file: IO[str] | None = None, show_caches: bool = False, adaptive: bool = False ) -> None: ... - def get_instructions( - x: _HaveCodeType, *, first_line: int | None = None, show_caches: bool = False, adaptive: bool = False - ) -> Iterator[Instruction]: ... else: def dis( @@ -215,6 +269,19 @@ else: ) -> None: ... def disassemble(co: _HaveCodeType, lasti: int = -1, *, file: IO[str] | None = None) -> None: ... def distb(tb: types.TracebackType | None = None, *, file: IO[str] | None = None) -> None: ... + +if sys.version_info >= (3, 13): + # 3.13 made `show_cache` `None` by default + def get_instructions( + x: _HaveCodeType, *, first_line: int | None = None, show_caches: bool | None = None, adaptive: bool = False + ) -> Iterator[Instruction]: ... + +elif sys.version_info >= (3, 11): + def get_instructions( + x: _HaveCodeType, *, first_line: int | None = None, show_caches: bool = False, adaptive: bool = False + ) -> Iterator[Instruction]: ... + +else: def get_instructions(x: _HaveCodeType, *, first_line: int | None = None) -> Iterator[Instruction]: ... def show_code(co: _HaveCodeType, *, file: IO[str] | None = None) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi index a4e77ddf13882..7f97bc3a2c9e0 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi @@ -1,4 +1,4 @@ -from _typeshed import BytesPath, Incomplete, StrOrBytesPath, StrPath, Unused +from _typeshed import BytesPath, StrOrBytesPath, StrPath, Unused from abc import abstractmethod from collections.abc import Callable, Iterable from distutils.command.bdist import bdist @@ -226,4 +226,4 @@ class Command: level: Unused = 1, ) -> None: ... def ensure_finalized(self) -> None: ... - def dump_options(self, header: Incomplete | None = None, indent: str = "") -> None: ... + def dump_options(self, header=None, indent: str = "") -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi similarity index 95% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi index 562ff3a5271f8..381e8e466bf16 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi @@ -1,4 +1,4 @@ -from _typeshed import Incomplete, StrOrBytesPath +from _typeshed import StrOrBytesPath from collections.abc import Sequence from re import Pattern from typing import ClassVar, Final, Literal @@ -81,4 +81,4 @@ class config(Command): self, header: str, include_dirs: Sequence[str] | None = None, library_dirs: Sequence[str] | None = None, lang: str = "c" ) -> bool: ... -def dump_file(filename: StrOrBytesPath, head: Incomplete | None = None) -> None: ... +def dump_file(filename: StrOrBytesPath, head=None) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi similarity index 86% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi index cf98e178a9ba1..c3bd62aaa7aa0 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi @@ -1,4 +1,3 @@ -from _typeshed import Incomplete from collections.abc import Callable from typing import Any, ClassVar @@ -18,4 +17,4 @@ class register(PyPIRCCommand): def verify_metadata(self) -> None: ... def send_metadata(self) -> None: ... def build_post_data(self, action): ... - def post_to_server(self, data, auth: Incomplete | None = None): ... + def post_to_server(self, data, auth=None): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/config.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/config.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/core.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/core.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/debug.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/debug.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/debug.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/debug.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dist.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/dist.pyi index 09f2b456d2635..412b94131b54e 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dist.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dist.pyi @@ -112,9 +112,7 @@ class Distribution: command_obj: Incomplete have_run: Incomplete want_user_cfg: bool - def dump_option_dicts( - self, header: Incomplete | None = None, commands: Incomplete | None = None, indent: str = "" - ) -> None: ... + def dump_option_dicts(self, header=None, commands=None, indent: str = "") -> None: ... def find_config_files(self): ... commands: Incomplete def parse_command_line(self): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/errors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/errors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/errors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/extension.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/extension.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/extension.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/extension.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/log.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/log.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/log.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/log.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/version.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/version.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/version.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/version.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/doctest.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/doctest.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/doctest.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/doctest.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/__init__.pyi new file mode 100644 index 0000000000000..53f8c350b01e3 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/__init__.pyi @@ -0,0 +1,60 @@ +from collections.abc import Callable +from email._policybase import _MessageT +from email.message import Message +from email.policy import Policy +from typing import IO, overload +from typing_extensions import TypeAlias + +# At runtime, listing submodules in __all__ without them being imported is +# valid, and causes them to be included in a star import. See #6523 + +__all__ = [ # noqa: F822 # Undefined names in __all__ + "base64mime", # pyright: ignore[reportUnsupportedDunderAll] + "charset", # pyright: ignore[reportUnsupportedDunderAll] + "encoders", # pyright: ignore[reportUnsupportedDunderAll] + "errors", # pyright: ignore[reportUnsupportedDunderAll] + "feedparser", # pyright: ignore[reportUnsupportedDunderAll] + "generator", # pyright: ignore[reportUnsupportedDunderAll] + "header", # pyright: ignore[reportUnsupportedDunderAll] + "iterators", # pyright: ignore[reportUnsupportedDunderAll] + "message", # pyright: ignore[reportUnsupportedDunderAll] + "message_from_file", + "message_from_binary_file", + "message_from_string", + "message_from_bytes", + "mime", # pyright: ignore[reportUnsupportedDunderAll] + "parser", # pyright: ignore[reportUnsupportedDunderAll] + "quoprimime", # pyright: ignore[reportUnsupportedDunderAll] + "utils", # pyright: ignore[reportUnsupportedDunderAll] +] + +# Definitions imported by multiple submodules in typeshed +_ParamType: TypeAlias = str | tuple[str | None, str | None, str] # noqa: Y047 +_ParamsType: TypeAlias = str | None | tuple[str, str | None, str] # noqa: Y047 + +@overload +def message_from_string(s: str) -> Message: ... +@overload +def message_from_string(s: str, _class: Callable[[], _MessageT]) -> _MessageT: ... +@overload +def message_from_string(s: str, _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... +@overload +def message_from_bytes(s: bytes | bytearray) -> Message: ... +@overload +def message_from_bytes(s: bytes | bytearray, _class: Callable[[], _MessageT]) -> _MessageT: ... +@overload +def message_from_bytes( + s: bytes | bytearray, _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT] +) -> _MessageT: ... +@overload +def message_from_file(fp: IO[str]) -> Message: ... +@overload +def message_from_file(fp: IO[str], _class: Callable[[], _MessageT]) -> _MessageT: ... +@overload +def message_from_file(fp: IO[str], _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... +@overload +def message_from_binary_file(fp: IO[bytes]) -> Message: ... +@overload +def message_from_binary_file(fp: IO[bytes], _class: Callable[[], _MessageT]) -> _MessageT: ... +@overload +def message_from_binary_file(fp: IO[bytes], _class: Callable[[], _MessageT] = ..., *, policy: Policy[_MessageT]) -> _MessageT: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi index f4e9ca68d6a99..95ada186c4ec8 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi @@ -1,4 +1,3 @@ -import sys from collections.abc import Iterable, Iterator from email.errors import HeaderParseError, MessageDefect from email.policy import Policy @@ -17,15 +16,13 @@ TOKEN_ENDS: Final[set[str]] ASPECIALS: Final[set[str]] ATTRIBUTE_ENDS: Final[set[str]] EXTENDED_ATTRIBUTE_ENDS: Final[set[str]] -# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 NLSET: Final[set[str]] -# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 SPECIALSNL: Final[set[str]] -if sys.version_info >= (3, 10): - # Added in Python 3.10.17, 3.11.12, 3.12.9, 3.13.2 (may still be backported to 3.9) - def make_quoted_pairs(value: Any) -> str: ... - +# Added in Python 3.9.23, 3.10.17, 3.11.12, 3.12.9, 3.13.2 +def make_quoted_pairs(value: Any) -> str: ... def quote_string(value: Any) -> str: ... rfc2047_matcher: Pattern[str] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/_policybase.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/_policybase.pyi new file mode 100644 index 0000000000000..0fb890d424b10 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/_policybase.pyi @@ -0,0 +1,80 @@ +from abc import ABCMeta, abstractmethod +from email.errors import MessageDefect +from email.header import Header +from email.message import Message +from typing import Any, Generic, Protocol, TypeVar, type_check_only +from typing_extensions import Self + +__all__ = ["Policy", "Compat32", "compat32"] + +_MessageT = TypeVar("_MessageT", bound=Message[Any, Any], default=Message[str, str]) +_MessageT_co = TypeVar("_MessageT_co", covariant=True, bound=Message[Any, Any], default=Message[str, str]) + +@type_check_only +class _MessageFactory(Protocol[_MessageT]): + def __call__(self, policy: Policy[_MessageT]) -> _MessageT: ... + +# Policy below is the only known direct subclass of _PolicyBase. We therefore +# assume that the __init__ arguments and attributes of _PolicyBase are +# the same as those of Policy. +class _PolicyBase(Generic[_MessageT_co]): + max_line_length: int | None + linesep: str + cte_type: str + raise_on_defect: bool + mangle_from_: bool + message_factory: _MessageFactory[_MessageT_co] | None + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 + verify_generated_headers: bool + + def __init__( + self, + *, + max_line_length: int | None = 78, + linesep: str = "\n", + cte_type: str = "8bit", + raise_on_defect: bool = False, + mangle_from_: bool = ..., # default depends on sub-class + message_factory: _MessageFactory[_MessageT_co] | None = None, + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 + verify_generated_headers: bool = True, + ) -> None: ... + def clone( + self, + *, + max_line_length: int | None = ..., + linesep: str = ..., + cte_type: str = ..., + raise_on_defect: bool = ..., + mangle_from_: bool = ..., + message_factory: _MessageFactory[_MessageT_co] | None = ..., + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 + verify_generated_headers: bool = ..., + ) -> Self: ... + def __add__(self, other: Policy) -> Self: ... + +class Policy(_PolicyBase[_MessageT_co], metaclass=ABCMeta): + # Every Message object has a `defects` attribute, so the following + # methods will work for any Message object. + def handle_defect(self, obj: Message[Any, Any], defect: MessageDefect) -> None: ... + def register_defect(self, obj: Message[Any, Any], defect: MessageDefect) -> None: ... + def header_max_count(self, name: str) -> int | None: ... + @abstractmethod + def header_source_parse(self, sourcelines: list[str]) -> tuple[str, str]: ... + @abstractmethod + def header_store_parse(self, name: str, value: str) -> tuple[str, str]: ... + @abstractmethod + def header_fetch_parse(self, name: str, value: str) -> str: ... + @abstractmethod + def fold(self, name: str, value: str) -> str: ... + @abstractmethod + def fold_binary(self, name: str, value: str) -> bytes: ... + +class Compat32(Policy[_MessageT_co]): + def header_source_parse(self, sourcelines: list[str]) -> tuple[str, str]: ... + def header_store_parse(self, name: str, value: str) -> tuple[str, str]: ... + def header_fetch_parse(self, name: str, value: str) -> str | Header: ... # type: ignore[override] + def fold(self, name: str, value: str) -> str: ... + def fold_binary(self, name: str, value: str) -> bytes: ... + +compat32: Compat32[Message[str, str]] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/base64mime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/base64mime.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/base64mime.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/base64mime.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/charset.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/charset.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/charset.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/charset.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/encoders.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/encoders.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/encoders.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/encoders.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/errors.pyi new file mode 100644 index 0000000000000..b501a58665560 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/errors.pyi @@ -0,0 +1,42 @@ +import sys + +class MessageError(Exception): ... +class MessageParseError(MessageError): ... +class HeaderParseError(MessageParseError): ... +class BoundaryError(MessageParseError): ... +class MultipartConversionError(MessageError, TypeError): ... +class CharsetError(MessageError): ... + +# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 +class HeaderWriteError(MessageError): ... + +class MessageDefect(ValueError): + def __init__(self, line: str | None = None) -> None: ... + +class NoBoundaryInMultipartDefect(MessageDefect): ... +class StartBoundaryNotFoundDefect(MessageDefect): ... +class FirstHeaderLineIsContinuationDefect(MessageDefect): ... +class MisplacedEnvelopeHeaderDefect(MessageDefect): ... +class MultipartInvariantViolationDefect(MessageDefect): ... +class InvalidMultipartContentTransferEncodingDefect(MessageDefect): ... +class UndecodableBytesDefect(MessageDefect): ... +class InvalidBase64PaddingDefect(MessageDefect): ... +class InvalidBase64CharactersDefect(MessageDefect): ... +class InvalidBase64LengthDefect(MessageDefect): ... +class CloseBoundaryNotFoundDefect(MessageDefect): ... +class MissingHeaderBodySeparatorDefect(MessageDefect): ... + +MalformedHeaderDefect = MissingHeaderBodySeparatorDefect + +class HeaderDefect(MessageDefect): ... +class InvalidHeaderDefect(HeaderDefect): ... +class HeaderMissingRequiredValue(HeaderDefect): ... + +class NonPrintableDefect(HeaderDefect): + def __init__(self, non_printables: str | None) -> None: ... + +class ObsoleteHeaderDefect(HeaderDefect): ... +class NonASCIILocalPartDefect(HeaderDefect): ... + +if sys.version_info >= (3, 10): + class InvalidDateDefect(HeaderDefect): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/feedparser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/feedparser.pyi similarity index 88% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/feedparser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/feedparser.pyi index 8c268ca1ae18c..d9279e9cd996d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/feedparser.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/feedparser.pyi @@ -1,12 +1,11 @@ from collections.abc import Callable +from email._policybase import _MessageT from email.message import Message from email.policy import Policy -from typing import Generic, TypeVar, overload +from typing import Generic, overload __all__ = ["FeedParser", "BytesFeedParser"] -_MessageT = TypeVar("_MessageT", bound=Message, default=Message) - class FeedParser(Generic[_MessageT]): @overload def __init__(self: FeedParser[Message], _factory: None = None, *, policy: Policy[Message] = ...) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/generator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/generator.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/generator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/generator.pyi index dfa0604a20a98..d30e686299fab 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/generator.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/generator.pyi @@ -7,7 +7,7 @@ from typing_extensions import Self __all__ = ["Generator", "DecodedGenerator", "BytesGenerator"] # By default, generators do not have a message policy. -_MessageT = TypeVar("_MessageT", bound=Message, default=Any) +_MessageT = TypeVar("_MessageT", bound=Message[Any, Any], default=Any) class Generator(Generic[_MessageT]): maxheaderlen: int | None diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/header.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/header.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/header.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/header.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/iterators.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/iterators.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/iterators.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/iterators.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/message.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/message.pyi new file mode 100644 index 0000000000000..e4d14992168a1 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/message.pyi @@ -0,0 +1,172 @@ +from _typeshed import MaybeNone +from collections.abc import Generator, Iterator, Sequence +from email import _ParamsType, _ParamType +from email.charset import Charset +from email.contentmanager import ContentManager +from email.errors import MessageDefect +from email.policy import Policy +from typing import Any, Generic, Literal, Protocol, TypeVar, overload +from typing_extensions import Self, TypeAlias + +__all__ = ["Message", "EmailMessage"] + +_T = TypeVar("_T") +# Type returned by Policy.header_fetch_parse, often str or Header. +_HeaderT_co = TypeVar("_HeaderT_co", covariant=True, default=str) +_HeaderParamT_contra = TypeVar("_HeaderParamT_contra", contravariant=True, default=str) +# Represents headers constructed by HeaderRegistry. Those are sub-classes +# of BaseHeader and another header type. +_HeaderRegistryT_co = TypeVar("_HeaderRegistryT_co", covariant=True, default=Any) +_HeaderRegistryParamT_contra = TypeVar("_HeaderRegistryParamT_contra", contravariant=True, default=Any) + +_PayloadType: TypeAlias = Message | str +_EncodedPayloadType: TypeAlias = Message | bytes +_MultipartPayloadType: TypeAlias = list[_PayloadType] +_CharsetType: TypeAlias = Charset | str | None + +class _SupportsEncodeToPayload(Protocol): + def encode(self, encoding: str, /) -> _PayloadType | _MultipartPayloadType | _SupportsDecodeToPayload: ... + +class _SupportsDecodeToPayload(Protocol): + def decode(self, encoding: str, errors: str, /) -> _PayloadType | _MultipartPayloadType: ... + +class Message(Generic[_HeaderT_co, _HeaderParamT_contra]): + # The policy attributes and arguments in this class and its subclasses + # would ideally use Policy[Self], but this is not possible. + policy: Policy[Any] # undocumented + preamble: str | None + epilogue: str | None + defects: list[MessageDefect] + def __init__(self, policy: Policy[Any] = ...) -> None: ... + def is_multipart(self) -> bool: ... + def set_unixfrom(self, unixfrom: str) -> None: ... + def get_unixfrom(self) -> str | None: ... + def attach(self, payload: _PayloadType) -> None: ... + # `i: int` without a multipart payload results in an error + # `| MaybeNone` acts like `| Any`: can be None for cleared or unset payload, but annoying to check + @overload # multipart + def get_payload(self, i: int, decode: Literal[True]) -> None: ... + @overload # multipart + def get_payload(self, i: int, decode: Literal[False] = False) -> _PayloadType | MaybeNone: ... + @overload # either + def get_payload(self, i: None = None, decode: Literal[False] = False) -> _PayloadType | _MultipartPayloadType | MaybeNone: ... + @overload # not multipart + def get_payload(self, i: None = None, *, decode: Literal[True]) -> _EncodedPayloadType | MaybeNone: ... + @overload # not multipart, IDEM but w/o kwarg + def get_payload(self, i: None, decode: Literal[True]) -> _EncodedPayloadType | MaybeNone: ... + # If `charset=None` and payload supports both `encode` AND `decode`, + # then an invalid payload could be passed, but this is unlikely + # Not[_SupportsEncodeToPayload] + @overload + def set_payload( + self, payload: _SupportsDecodeToPayload | _PayloadType | _MultipartPayloadType, charset: None = None + ) -> None: ... + @overload + def set_payload( + self, + payload: _SupportsEncodeToPayload | _SupportsDecodeToPayload | _PayloadType | _MultipartPayloadType, + charset: Charset | str, + ) -> None: ... + def set_charset(self, charset: _CharsetType) -> None: ... + def get_charset(self) -> _CharsetType: ... + def __len__(self) -> int: ... + def __contains__(self, name: str) -> bool: ... + def __iter__(self) -> Iterator[str]: ... + # Same as `get` with `failobj=None`, but with the expectation that it won't return None in most scenarios + # This is important for protocols using __getitem__, like SupportsKeysAndGetItem + # Morally, the return type should be `AnyOf[_HeaderType, None]`, + # so using "the Any trick" instead. + def __getitem__(self, name: str) -> _HeaderT_co | MaybeNone: ... + def __setitem__(self, name: str, val: _HeaderParamT_contra) -> None: ... + def __delitem__(self, name: str) -> None: ... + def keys(self) -> list[str]: ... + def values(self) -> list[_HeaderT_co]: ... + def items(self) -> list[tuple[str, _HeaderT_co]]: ... + @overload + def get(self, name: str, failobj: None = None) -> _HeaderT_co | None: ... + @overload + def get(self, name: str, failobj: _T) -> _HeaderT_co | _T: ... + @overload + def get_all(self, name: str, failobj: None = None) -> list[_HeaderT_co] | None: ... + @overload + def get_all(self, name: str, failobj: _T) -> list[_HeaderT_co] | _T: ... + def add_header(self, _name: str, _value: str, **_params: _ParamsType) -> None: ... + def replace_header(self, _name: str, _value: _HeaderParamT_contra) -> None: ... + def get_content_type(self) -> str: ... + def get_content_maintype(self) -> str: ... + def get_content_subtype(self) -> str: ... + def get_default_type(self) -> str: ... + def set_default_type(self, ctype: str) -> None: ... + @overload + def get_params( + self, failobj: None = None, header: str = "content-type", unquote: bool = True + ) -> list[tuple[str, str]] | None: ... + @overload + def get_params(self, failobj: _T, header: str = "content-type", unquote: bool = True) -> list[tuple[str, str]] | _T: ... + @overload + def get_param( + self, param: str, failobj: None = None, header: str = "content-type", unquote: bool = True + ) -> _ParamType | None: ... + @overload + def get_param(self, param: str, failobj: _T, header: str = "content-type", unquote: bool = True) -> _ParamType | _T: ... + def del_param(self, param: str, header: str = "content-type", requote: bool = True) -> None: ... + def set_type(self, type: str, header: str = "Content-Type", requote: bool = True) -> None: ... + @overload + def get_filename(self, failobj: None = None) -> str | None: ... + @overload + def get_filename(self, failobj: _T) -> str | _T: ... + @overload + def get_boundary(self, failobj: None = None) -> str | None: ... + @overload + def get_boundary(self, failobj: _T) -> str | _T: ... + def set_boundary(self, boundary: str) -> None: ... + @overload + def get_content_charset(self) -> str | None: ... + @overload + def get_content_charset(self, failobj: _T) -> str | _T: ... + @overload + def get_charsets(self, failobj: None = None) -> list[str | None]: ... + @overload + def get_charsets(self, failobj: _T) -> list[str | _T]: ... + def walk(self) -> Generator[Self, None, None]: ... + def get_content_disposition(self) -> str | None: ... + def as_string(self, unixfrom: bool = False, maxheaderlen: int = 0, policy: Policy[Any] | None = None) -> str: ... + def as_bytes(self, unixfrom: bool = False, policy: Policy[Any] | None = None) -> bytes: ... + def __bytes__(self) -> bytes: ... + def set_param( + self, + param: str, + value: str, + header: str = "Content-Type", + requote: bool = True, + charset: str | None = None, + language: str = "", + replace: bool = False, + ) -> None: ... + # The following two methods are undocumented, but a source code comment states that they are public API + def set_raw(self, name: str, value: _HeaderParamT_contra) -> None: ... + def raw_items(self) -> Iterator[tuple[str, _HeaderT_co]]: ... + +class MIMEPart(Message[_HeaderRegistryT_co, _HeaderRegistryParamT_contra]): + def __init__(self, policy: Policy[Any] | None = None) -> None: ... + def get_body(self, preferencelist: Sequence[str] = ("related", "html", "plain")) -> MIMEPart[_HeaderRegistryT_co] | None: ... + 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. + def iter_attachments(self) -> Iterator[Self]: ... + def iter_parts(self) -> Iterator[MIMEPart[_HeaderRegistryT_co]]: ... + def get_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> Any: ... + def set_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> None: ... + def make_related(self, boundary: str | None = None) -> None: ... + def make_alternative(self, boundary: str | None = None) -> None: ... + def make_mixed(self, boundary: str | None = None) -> None: ... + def add_related(self, *args: Any, content_manager: ContentManager | None = ..., **kw: Any) -> None: ... + def add_alternative(self, *args: Any, content_manager: ContentManager | None = ..., **kw: Any) -> None: ... + def add_attachment(self, *args: Any, content_manager: ContentManager | None = ..., **kw: Any) -> None: ... + def clear(self) -> None: ... + def clear_content(self) -> None: ... + def as_string(self, unixfrom: bool = False, maxheaderlen: int | None = None, policy: Policy[Any] | None = None) -> str: ... + def is_attachment(self) -> bool: ... + +class EmailMessage(MIMEPart): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/application.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/application.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/application.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/application.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/base.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/base.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/base.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/base.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/image.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/image.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/image.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/image.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/message.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/message.pyi new file mode 100644 index 0000000000000..a1e370e2eab51 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/message.pyi @@ -0,0 +1,8 @@ +from email._policybase import _MessageT +from email.mime.nonmultipart import MIMENonMultipart +from email.policy import Policy + +__all__ = ["MIMEMessage"] + +class MIMEMessage(MIMENonMultipart): + def __init__(self, _msg: _MessageT, _subtype: str = "rfc822", *, policy: Policy[_MessageT] | None = None) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi similarity index 85% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi index 1c229f7436a8a..fb9599edbcb8f 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi @@ -1,7 +1,8 @@ from collections.abc import Sequence from email import _ParamsType +from email._policybase import _MessageT from email.mime.base import MIMEBase -from email.policy import Policy, _MessageT +from email.policy import Policy __all__ = ["MIMEMultipart"] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/text.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/text.pyi similarity index 87% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/text.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/text.pyi index 74d5ef4c5caea..edfa67a092427 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/text.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/text.pyi @@ -1,5 +1,5 @@ +from email._policybase import Policy from email.mime.nonmultipart import MIMENonMultipart -from email.policy import Policy __all__ = ["MIMEText"] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.pyi new file mode 100644 index 0000000000000..a4924a6cbd88f --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.pyi @@ -0,0 +1,39 @@ +from _typeshed import SupportsRead +from collections.abc import Callable +from email._policybase import _MessageT +from email.feedparser import BytesFeedParser as BytesFeedParser, FeedParser as FeedParser +from email.message import Message +from email.policy import Policy +from io import _WrappedBuffer +from typing import Generic, overload + +__all__ = ["Parser", "HeaderParser", "BytesParser", "BytesHeaderParser", "FeedParser", "BytesFeedParser"] + +class Parser(Generic[_MessageT]): + @overload + def __init__(self: Parser[Message[str, str]], _class: None = None) -> None: ... + @overload + def __init__(self, _class: None = None, *, policy: Policy[_MessageT]) -> None: ... + @overload + def __init__(self, _class: Callable[[], _MessageT] | None, *, policy: Policy[_MessageT] = ...) -> None: ... + def parse(self, fp: SupportsRead[str], headersonly: bool = False) -> _MessageT: ... + def parsestr(self, text: str, headersonly: bool = False) -> _MessageT: ... + +class HeaderParser(Parser[_MessageT]): + def parse(self, fp: SupportsRead[str], headersonly: bool = True) -> _MessageT: ... + def parsestr(self, text: str, headersonly: bool = True) -> _MessageT: ... + +class BytesParser(Generic[_MessageT]): + parser: Parser[_MessageT] + @overload + def __init__(self: BytesParser[Message[str, str]], _class: None = None) -> None: ... + @overload + def __init__(self, _class: None = None, *, policy: Policy[_MessageT]) -> None: ... + @overload + def __init__(self, _class: Callable[[], _MessageT], *, policy: Policy[_MessageT] = ...) -> None: ... + def parse(self, fp: _WrappedBuffer, headersonly: bool = False) -> _MessageT: ... + def parsebytes(self, text: bytes | bytearray, headersonly: bool = False) -> _MessageT: ... + +class BytesHeaderParser(BytesParser[_MessageT]): + def parse(self, fp: _WrappedBuffer, headersonly: bool = True) -> _MessageT: ... + def parsebytes(self, text: bytes | bytearray, headersonly: bool = True) -> _MessageT: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/policy.pyi similarity index 86% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/policy.pyi index 5b145bcf23180..35c999919eede 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/policy.pyi @@ -1,14 +1,12 @@ from collections.abc import Callable -from email._policybase import Compat32 as Compat32, Policy as Policy, _MessageFactory, compat32 as compat32 +from email._policybase import Compat32 as Compat32, Policy as Policy, _MessageFactory, _MessageT, compat32 as compat32 from email.contentmanager import ContentManager -from email.message import EmailMessage, Message -from typing import Any, TypeVar, overload +from email.message import EmailMessage +from typing import Any, overload from typing_extensions import Self __all__ = ["Compat32", "compat32", "Policy", "EmailPolicy", "default", "strict", "SMTP", "HTTP"] -_MessageT = TypeVar("_MessageT", bound=Message, default=Message) - class EmailPolicy(Policy[_MessageT]): utf8: bool refold_source: str @@ -24,7 +22,7 @@ class EmailPolicy(Policy[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: None = None, - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., utf8: bool = ..., refold_source: str = ..., @@ -41,7 +39,7 @@ class EmailPolicy(Policy[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: _MessageFactory[_MessageT] | None = ..., - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., utf8: bool = ..., refold_source: str = ..., @@ -62,7 +60,7 @@ class EmailPolicy(Policy[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: _MessageFactory[_MessageT] | None = ..., - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., utf8: bool = ..., refold_source: str = ..., diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/utils.pyi similarity index 94% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/utils.pyi index dc3eecb5ef7fb..efc32a7abce29 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/email/utils.pyi @@ -30,11 +30,11 @@ _PDTZ: TypeAlias = tuple[int, int, int, int, int, int, int, int, int, int | None def quote(str: str) -> str: ... def unquote(str: str) -> str: ... -# `strict` parameter added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# `strict` parameter added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 def parseaddr(addr: str | list[str], *, strict: bool = True) -> tuple[str, str]: ... def formataddr(pair: tuple[str | None, str], charset: str | Charset = "utf-8") -> str: ... -# `strict` parameter added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# `strict` parameter added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 def getaddresses(fieldvalues: Iterable[str], *, strict: bool = True) -> list[tuple[str, str]]: ... @overload def parsedate(data: None) -> None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi new file mode 100644 index 0000000000000..12ec6792d49b5 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi @@ -0,0 +1,9 @@ +from codecs import CodecInfo + +class CodecRegistryError(LookupError, SystemError): ... + +def normalize_encoding(encoding: str | bytes) -> str: ... +def search_function(encoding: str) -> CodecInfo | None: ... + +# Needed for submodules +def __getattr__(name: str): ... # incomplete module diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hz.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/hz.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hz.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/hz.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/idna.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/idna.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/idna.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/idna.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/johab.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/johab.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/johab.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/johab.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/oem.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/oem.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/oem.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/oem.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi index 8c88b26a3a2fa..327b135459a00 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi @@ -53,6 +53,7 @@ _EnumerationT = TypeVar("_EnumerationT", bound=type[Enum]) # >>> Enum('Foo', names={'RED': 1, 'YELLOW': 2}) # _EnumNames: TypeAlias = str | Iterable[str] | Iterable[Iterable[str | Any]] | Mapping[str, Any] +_Signature: TypeAlias = Any # TODO: Unable to import Signature from inspect module if sys.version_info >= (3, 11): class nonmember(Generic[_EnumMemberT]): @@ -166,6 +167,9 @@ class EnumMeta(type): if sys.version_info >= (3, 12): @overload def __call__(cls: type[_EnumMemberT], value: Any, *values: Any) -> _EnumMemberT: ... + if sys.version_info >= (3, 14): + @property + def __signature__(cls) -> _Signature: ... _member_names_: list[str] # undocumented _member_map_: dict[str, Enum] # undocumented @@ -212,7 +216,7 @@ class Enum(metaclass=EnumMeta): if sys.version_info >= (3, 11): def __copy__(self) -> Self: ... def __deepcopy__(self, memo: Any) -> Self: ... - if sys.version_info >= (3, 12): + if sys.version_info >= (3, 12) and sys.version_info < (3, 14): @classmethod def __signature__(cls) -> str: ... @@ -299,6 +303,7 @@ if sys.version_info >= (3, 11): def __or__(self, other: int) -> Self: ... def __and__(self, other: int) -> Self: ... def __xor__(self, other: int) -> Self: ... + def __invert__(self) -> Self: ... __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ @@ -309,6 +314,7 @@ else: def __or__(self, other: int) -> Self: ... def __and__(self, other: int) -> Self: ... def __xor__(self, other: int) -> Self: ... + def __invert__(self) -> Self: ... __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/errno.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/errno.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/errno.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/errno.pyi index 84d2b44a6a61b..3ba8b66d28650 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/errno.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/errno.pyi @@ -170,6 +170,9 @@ if sys.platform != "win32" and sys.platform != "darwin": ENOMEDIUM: int ERFKILL: int + if sys.version_info >= (3, 14): + EHWPOISON: int + if sys.platform == "win32": # All of these are undocumented WSABASEERR: int diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/faulthandler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/faulthandler.pyi similarity index 86% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/faulthandler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/faulthandler.pyi index 320a8b6fad150..8f93222c9936e 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/faulthandler.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/faulthandler.pyi @@ -4,6 +4,10 @@ from _typeshed import FileDescriptorLike def cancel_dump_traceback_later() -> None: ... def disable() -> None: ... def dump_traceback(file: FileDescriptorLike = ..., all_threads: bool = ...) -> None: ... + +if sys.version_info >= (3, 14): + def dump_c_stack(file: FileDescriptorLike = ...) -> None: ... + def dump_traceback_later(timeout: float, repeat: bool = ..., file: FileDescriptorLike = ..., exit: bool = ...) -> None: ... def enable(file: FileDescriptorLike = ..., all_threads: bool = ...) -> None: ... def is_enabled() -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fcntl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fcntl.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fcntl.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fcntl.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/filecmp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/filecmp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/filecmp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/filecmp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fileinput.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fileinput.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fileinput.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fileinput.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/fnmatch.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fnmatch.pyi new file mode 100644 index 0000000000000..345c4576497de --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/fnmatch.pyi @@ -0,0 +1,15 @@ +import sys +from collections.abc import Iterable +from typing import AnyStr + +__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] +if sys.version_info >= (3, 14): + __all__ += ["filterfalse"] + +def fnmatch(name: AnyStr, pat: AnyStr) -> bool: ... +def fnmatchcase(name: AnyStr, pat: AnyStr) -> bool: ... +def filter(names: Iterable[AnyStr], pat: AnyStr) -> list[AnyStr]: ... +def translate(pat: str) -> str: ... + +if sys.version_info >= (3, 14): + def filterfalse(names: Iterable[AnyStr], pat: AnyStr) -> list[AnyStr]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/formatter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/formatter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/formatter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/formatter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fractions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fractions.pyi similarity index 81% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fractions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fractions.pyi index 4d5c2160e60a1..16259fcfadc7c 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/fractions.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/fractions.pyi @@ -107,16 +107,31 @@ class Fraction(Rational): def __rdivmod__(a, b: int | Fraction) -> tuple[int, Fraction]: ... @overload def __rdivmod__(a, b: float) -> tuple[float, Fraction]: ... - @overload - def __pow__(a, b: int) -> Fraction: ... - @overload - def __pow__(a, b: float | Fraction) -> float: ... - @overload - def __pow__(a, b: complex) -> complex: ... - @overload - def __rpow__(b, a: float | Fraction) -> float: ... - @overload - def __rpow__(b, a: complex) -> complex: ... + if sys.version_info >= (3, 14): + @overload + def __pow__(a, b: int, modulo: None = None) -> Fraction: ... + @overload + def __pow__(a, b: float | Fraction, modulo: None = None) -> float: ... + @overload + def __pow__(a, b: complex, modulo: None = None) -> complex: ... + else: + @overload + def __pow__(a, b: int) -> Fraction: ... + @overload + def __pow__(a, b: float | Fraction) -> float: ... + @overload + def __pow__(a, b: complex) -> complex: ... + if sys.version_info >= (3, 14): + @overload + def __rpow__(b, a: float | Fraction, modulo: None = None) -> float: ... + @overload + def __rpow__(b, a: complex, modulo: None = None) -> complex: ... + else: + @overload + def __rpow__(b, a: float | Fraction) -> float: ... + @overload + def __rpow__(b, a: complex) -> complex: ... + def __pos__(a) -> Fraction: ... def __neg__(a) -> Fraction: ... def __abs__(a) -> Fraction: ... @@ -145,3 +160,6 @@ class Fraction(Rational): @property def imag(self) -> Literal[0]: ... def conjugate(self) -> Fraction: ... + if sys.version_info >= (3, 14): + @classmethod + def from_number(cls, number: float | Rational | _ConvertibleToIntegerRatio) -> Self: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ftplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ftplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ftplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ftplib.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/functools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/functools.pyi new file mode 100644 index 0000000000000..e31399fb87054 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/functools.pyi @@ -0,0 +1,247 @@ +import sys +import types +from _typeshed import SupportsAllComparisons, SupportsItems +from collections.abc import Callable, Hashable, Iterable, Sized +from types import GenericAlias +from typing import Any, Final, Generic, Literal, NamedTuple, TypedDict, TypeVar, final, overload +from typing_extensions import ParamSpec, Self, TypeAlias + +__all__ = [ + "update_wrapper", + "wraps", + "WRAPPER_ASSIGNMENTS", + "WRAPPER_UPDATES", + "total_ordering", + "cmp_to_key", + "lru_cache", + "reduce", + "partial", + "partialmethod", + "singledispatch", + "cached_property", + "singledispatchmethod", + "cache", +] + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_S = TypeVar("_S") +_PWrapped = ParamSpec("_PWrapped") +_RWrapped = TypeVar("_RWrapped") +_PWrapper = ParamSpec("_PWrapper") +_RWrapper = TypeVar("_RWrapper") + +if sys.version_info >= (3, 14): + @overload + def reduce(function: Callable[[_T, _S], _T], iterable: Iterable[_S], /, initial: _T) -> _T: ... + +else: + @overload + def reduce(function: Callable[[_T, _S], _T], iterable: Iterable[_S], initial: _T, /) -> _T: ... + +@overload +def reduce(function: Callable[[_T, _T], _T], iterable: Iterable[_T], /) -> _T: ... + +class _CacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + +class _CacheParameters(TypedDict): + maxsize: int + typed: bool + +@final +class _lru_cache_wrapper(Generic[_T]): + __wrapped__: Callable[..., _T] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _T: ... + def cache_info(self) -> _CacheInfo: ... + def cache_clear(self) -> None: ... + def cache_parameters(self) -> _CacheParameters: ... + def __copy__(self) -> _lru_cache_wrapper[_T]: ... + def __deepcopy__(self, memo: Any, /) -> _lru_cache_wrapper[_T]: ... + +@overload +def lru_cache(maxsize: int | None = 128, typed: bool = False) -> Callable[[Callable[..., _T]], _lru_cache_wrapper[_T]]: ... +@overload +def lru_cache(maxsize: Callable[..., _T], typed: bool = False) -> _lru_cache_wrapper[_T]: ... + +if sys.version_info >= (3, 14): + WRAPPER_ASSIGNMENTS: Final[ + tuple[ + Literal["__module__"], + Literal["__name__"], + Literal["__qualname__"], + Literal["__doc__"], + Literal["__annotate__"], + Literal["__type_params__"], + ] + ] +elif sys.version_info >= (3, 12): + WRAPPER_ASSIGNMENTS: Final[ + tuple[ + Literal["__module__"], + Literal["__name__"], + Literal["__qualname__"], + Literal["__doc__"], + Literal["__annotations__"], + Literal["__type_params__"], + ] + ] +else: + WRAPPER_ASSIGNMENTS: Final[ + tuple[Literal["__module__"], Literal["__name__"], Literal["__qualname__"], Literal["__doc__"], Literal["__annotations__"]] + ] + +WRAPPER_UPDATES: tuple[Literal["__dict__"]] + +class _Wrapped(Generic[_PWrapped, _RWrapped, _PWrapper, _RWrapper]): + __wrapped__: Callable[_PWrapped, _RWrapped] + def __call__(self, *args: _PWrapper.args, **kwargs: _PWrapper.kwargs) -> _RWrapper: ... + # as with ``Callable``, we'll assume that these attributes exist + __name__: str + __qualname__: str + +class _Wrapper(Generic[_PWrapped, _RWrapped]): + def __call__(self, f: Callable[_PWrapper, _RWrapper]) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + +if sys.version_info >= (3, 14): + def update_wrapper( + wrapper: Callable[_PWrapper, _RWrapper], + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotate__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + def wraps( + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotate__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapper[_PWrapped, _RWrapped]: ... + +elif sys.version_info >= (3, 12): + def update_wrapper( + wrapper: Callable[_PWrapper, _RWrapper], + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + def wraps( + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__", "__type_params__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapper[_PWrapped, _RWrapped]: ... + +else: + def update_wrapper( + wrapper: Callable[_PWrapper, _RWrapper], + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]: ... + def wraps( + wrapped: Callable[_PWrapped, _RWrapped], + assigned: Iterable[str] = ("__module__", "__name__", "__qualname__", "__doc__", "__annotations__"), + updated: Iterable[str] = ("__dict__",), + ) -> _Wrapper[_PWrapped, _RWrapped]: ... + +def total_ordering(cls: type[_T]) -> type[_T]: ... +def cmp_to_key(mycmp: Callable[[_T, _T], int]) -> Callable[[_T], SupportsAllComparisons]: ... + +class partial(Generic[_T]): + @property + def func(self) -> Callable[..., _T]: ... + @property + def args(self) -> tuple[Any, ...]: ... + @property + def keywords(self) -> dict[str, Any]: ... + def __new__(cls, func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> Self: ... + def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +# With protocols, this could change into a generic protocol that defines __get__ and returns _T +_Descriptor: TypeAlias = Any + +class partialmethod(Generic[_T]): + func: Callable[..., _T] | _Descriptor + args: tuple[Any, ...] + keywords: dict[str, Any] + @overload + def __init__(self, func: Callable[..., _T], /, *args: Any, **keywords: Any) -> None: ... + @overload + def __init__(self, func: _Descriptor, /, *args: Any, **keywords: Any) -> None: ... + def __get__(self, obj: Any, cls: type[Any] | None = None) -> Callable[..., _T]: ... + @property + def __isabstractmethod__(self) -> bool: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +if sys.version_info >= (3, 11): + _RegType: TypeAlias = type[Any] | types.UnionType +else: + _RegType: TypeAlias = type[Any] + +class _SingleDispatchCallable(Generic[_T]): + registry: types.MappingProxyType[Any, Callable[..., _T]] + def dispatch(self, cls: Any) -> Callable[..., _T]: ... + # @fun.register(complex) + # def _(arg, verbose=False): ... + @overload + def register(self, cls: _RegType, func: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... + # @fun.register + # def _(arg: int, verbose=False): + @overload + def register(self, cls: Callable[..., _T], func: None = None) -> Callable[..., _T]: ... + # fun.register(int, lambda x: x) + @overload + def register(self, cls: _RegType, func: Callable[..., _T]) -> Callable[..., _T]: ... + def _clear_cache(self) -> None: ... + def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ... + +def singledispatch(func: Callable[..., _T]) -> _SingleDispatchCallable[_T]: ... + +class singledispatchmethod(Generic[_T]): + dispatcher: _SingleDispatchCallable[_T] + func: Callable[..., _T] + def __init__(self, func: Callable[..., _T]) -> None: ... + @property + def __isabstractmethod__(self) -> bool: ... + @overload + def register(self, cls: _RegType, method: None = None) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... + @overload + def register(self, cls: Callable[..., _T], method: None = None) -> Callable[..., _T]: ... + @overload + def register(self, cls: _RegType, method: Callable[..., _T]) -> Callable[..., _T]: ... + def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]: ... + +class cached_property(Generic[_T_co]): + func: Callable[[Any], _T_co] + attrname: str | None + def __init__(self, func: Callable[[Any], _T_co]) -> None: ... + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T_co: ... + def __set_name__(self, owner: type[Any], name: str) -> None: ... + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T_co) -> None: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +def cache(user_function: Callable[..., _T], /) -> _lru_cache_wrapper[_T]: ... +def _make_key( + args: tuple[Hashable, ...], + kwds: SupportsItems[Any, Any], + typed: bool, + kwd_mark: tuple[object, ...] = ..., + fasttypes: set[type] = ..., + tuple: type = ..., + type: Any = ..., + len: Callable[[Sized], int] = ..., +) -> Hashable: ... + +if sys.version_info >= (3, 14): + @final + class _PlaceholderType: ... + + Placeholder: Final[_PlaceholderType] + + __all__ += ["Placeholder"] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/gc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/gc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/gc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/gc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/genericpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.pyi similarity index 90% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/genericpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/genericpath.pyi index 9d87c48fd5200..3caed77a661ac 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/genericpath.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.pyi @@ -2,7 +2,7 @@ import os import sys from _typeshed import BytesPath, FileDescriptorOrPath, StrOrBytesPath, StrPath, SupportsRichComparisonT from collections.abc import Sequence -from typing import Literal, overload +from typing import Literal, NewType, overload from typing_extensions import LiteralString __all__ = [ @@ -17,6 +17,7 @@ __all__ = [ "samefile", "sameopenfile", "samestat", + "ALLOW_MISSING", ] if sys.version_info >= (3, 12): __all__ += ["islink"] @@ -57,3 +58,7 @@ if sys.version_info >= (3, 13): def isjunction(path: StrOrBytesPath) -> bool: ... def isdevdrive(path: StrOrBytesPath) -> bool: ... def lexists(path: StrOrBytesPath) -> bool: ... + +# Added in Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 +_AllowMissingType = NewType("_AllowMissingType", object) +ALLOW_MISSING: _AllowMissingType diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/getopt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/getopt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/getopt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/getopt.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/getpass.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/getpass.pyi new file mode 100644 index 0000000000000..bb3013dfbf393 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/getpass.pyi @@ -0,0 +1,14 @@ +import sys +from typing import TextIO + +__all__ = ["getpass", "getuser", "GetPassWarning"] + +if sys.version_info >= (3, 14): + def getpass(prompt: str = "Password: ", stream: TextIO | None = None, *, echo_char: str | None = None) -> str: ... + +else: + def getpass(prompt: str = "Password: ", stream: TextIO | None = None) -> str: ... + +def getuser() -> str: ... + +class GetPassWarning(UserWarning): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/gettext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/gettext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/gettext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/gettext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/glob.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/glob.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/graphlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/graphlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/graphlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/graphlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/grp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/grp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/grp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/grp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/gzip.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/gzip.pyi similarity index 87% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/gzip.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/gzip.pyi index b7fb40fbd82ee..34ae92b4d8ed6 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/gzip.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/gzip.pyi @@ -1,11 +1,15 @@ -import _compression import sys import zlib -from _typeshed import ReadableBuffer, SizedBuffer, StrOrBytesPath +from _typeshed import ReadableBuffer, SizedBuffer, StrOrBytesPath, WriteableBuffer from io import FileIO, TextIOWrapper from typing import Final, Literal, Protocol, overload from typing_extensions import TypeAlias +if sys.version_info >= (3, 14): + from compression._common._streams import BaseStream, DecompressReader +else: + from _compression import BaseStream, DecompressReader + __all__ = ["BadGzipFile", "GzipFile", "open", "compress", "decompress"] _ReadBinaryMode: TypeAlias = Literal["r", "rb"] @@ -84,7 +88,7 @@ class _PaddedFile: class BadGzipFile(OSError): ... -class GzipFile(_compression.BaseStream): +class GzipFile(BaseStream): myfileobj: FileIO | None mode: object name: str @@ -153,8 +157,17 @@ class GzipFile(_compression.BaseStream): def seek(self, offset: int, whence: int = 0) -> int: ... def readline(self, size: int | None = -1) -> bytes: ... -class _GzipReader(_compression.DecompressReader): + if sys.version_info >= (3, 14): + def readinto(self, b: WriteableBuffer) -> int: ... + def readinto1(self, b: WriteableBuffer) -> int: ... + +class _GzipReader(DecompressReader): def __init__(self, fp: _ReadableFileobj) -> None: ... -def compress(data: SizedBuffer, compresslevel: int = 9, *, mtime: float | None = None) -> bytes: ... +if sys.version_info >= (3, 14): + def compress(data: SizedBuffer, compresslevel: int = 9, *, mtime: float = 0) -> bytes: ... + +else: + def compress(data: SizedBuffer, compresslevel: int = 9, *, mtime: float | None = None) -> bytes: ... + def decompress(data: ReadableBuffer) -> bytes: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/hashlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/hashlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/hashlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/hashlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/heapq.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/heapq.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/heapq.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/heapq.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/hmac.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/hmac.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/hmac.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/hmac.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/html/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/html/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/html/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/html/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/html/entities.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/html/entities.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/html/entities.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/html/entities.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/html/parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/html/parser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/html/parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/html/parser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/client.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/client.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/client.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/client.pyi index 9e0f61598cb81..5c35dff28d43a 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/client.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/client.pyi @@ -5,6 +5,7 @@ import sys import types from _typeshed import MaybeNone, ReadableBuffer, SupportsRead, SupportsReadline, WriteableBuffer from collections.abc import Callable, Iterable, Iterator, Mapping +from email._policybase import _MessageT from socket import socket from typing import BinaryIO, Literal, TypeVar, overload from typing_extensions import Self, TypeAlias @@ -33,7 +34,6 @@ __all__ = [ _DataType: TypeAlias = SupportsRead[bytes] | Iterable[ReadableBuffer] | ReadableBuffer _T = TypeVar("_T") -_MessageT = TypeVar("_MessageT", bound=email.message.Message) _HeaderValue: TypeAlias = ReadableBuffer | str | int HTTP_PORT: int diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookies.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookies.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi new file mode 100644 index 0000000000000..429bb65bb0efc --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi @@ -0,0 +1,130 @@ +import _socket +import email.message +import io +import socketserver +import sys +from _ssl import _PasswordType +from _typeshed import ReadableBuffer, StrOrBytesPath, StrPath, SupportsRead, SupportsWrite +from collections.abc import Callable, Iterable, Mapping, Sequence +from ssl import Purpose, SSLContext +from typing import Any, AnyStr, BinaryIO, ClassVar, Protocol, type_check_only +from typing_extensions import Self, deprecated + +if sys.version_info >= (3, 14): + __all__ = [ + "HTTPServer", + "ThreadingHTTPServer", + "HTTPSServer", + "ThreadingHTTPSServer", + "BaseHTTPRequestHandler", + "SimpleHTTPRequestHandler", + "CGIHTTPRequestHandler", + ] +else: + __all__ = ["HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler"] + +class HTTPServer(socketserver.TCPServer): + server_name: str + server_port: int + +class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): ... + +if sys.version_info >= (3, 14): + @type_check_only + class _SSLModule(Protocol): + @staticmethod + def create_default_context( + purpose: Purpose = ..., + *, + cafile: StrOrBytesPath | None = None, + capath: StrOrBytesPath | None = None, + cadata: str | ReadableBuffer | None = None, + ) -> SSLContext: ... + + class HTTPSServer(HTTPServer): + ssl: _SSLModule + certfile: StrOrBytesPath + keyfile: StrOrBytesPath | None + password: _PasswordType | None + alpn_protocols: Iterable[str] + def __init__( + self, + server_address: socketserver._AfInetAddress, + RequestHandlerClass: Callable[[Any, _socket._RetAddress, Self], socketserver.BaseRequestHandler], + bind_and_activate: bool = True, + *, + certfile: StrOrBytesPath, + keyfile: StrOrBytesPath | None = None, + password: _PasswordType | None = None, + alpn_protocols: Iterable[str] | None = None, + ) -> None: ... + def server_activate(self) -> None: ... + + class ThreadingHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer): ... + +class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): + client_address: tuple[str, int] + close_connection: bool + requestline: str + command: str + path: str + request_version: str + headers: email.message.Message + server_version: str + sys_version: str + error_message_format: str + error_content_type: str + protocol_version: str + MessageClass: type + responses: Mapping[int, tuple[str, str]] + default_request_version: str # undocumented + weekdayname: ClassVar[Sequence[str]] # undocumented + monthname: ClassVar[Sequence[str | None]] # undocumented + def handle_one_request(self) -> None: ... + def handle_expect_100(self) -> bool: ... + def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None: ... + def send_response(self, code: int, message: str | None = None) -> None: ... + def send_header(self, keyword: str, value: str) -> None: ... + def send_response_only(self, code: int, message: str | None = None) -> None: ... + def end_headers(self) -> None: ... + def flush_headers(self) -> None: ... + def log_request(self, code: int | str = "-", size: int | str = "-") -> None: ... + def log_error(self, format: str, *args: Any) -> None: ... + def log_message(self, format: str, *args: Any) -> None: ... + def version_string(self) -> str: ... + def date_time_string(self, timestamp: float | None = None) -> str: ... + def log_date_time_string(self) -> str: ... + def address_string(self) -> str: ... + def parse_request(self) -> bool: ... # undocumented + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + extensions_map: dict[str, str] + if sys.version_info >= (3, 12): + index_pages: ClassVar[tuple[str, ...]] + directory: str + def __init__( + self, + request: socketserver._RequestType, + client_address: _socket._RetAddress, + server: socketserver.BaseServer, + *, + directory: StrPath | None = None, + ) -> None: ... + def do_GET(self) -> None: ... + def do_HEAD(self) -> None: ... + def send_head(self) -> io.BytesIO | BinaryIO | None: ... # undocumented + def list_directory(self, path: StrPath) -> io.BytesIO | None: ... # undocumented + def translate_path(self, path: str) -> str: ... # undocumented + def copyfile(self, source: SupportsRead[AnyStr], outputfile: SupportsWrite[AnyStr]) -> None: ... # undocumented + def guess_type(self, path: StrPath) -> str: ... # undocumented + +def executable(path: StrPath) -> bool: ... # undocumented +@deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.15") +class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): + cgi_directories: list[str] + have_fork: bool # undocumented + def do_POST(self) -> None: ... + def is_cgi(self) -> bool: ... # undocumented + def is_executable(self, path: StrPath) -> bool: ... # undocumented + def is_python(self, path: StrPath) -> bool: ... # undocumented + def run_cgi(self) -> None: ... # undocumented diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/imaplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi similarity index 82% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/imaplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi index ccee92bd5e884..536985a592b7f 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/imaplib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi @@ -1,16 +1,16 @@ import subprocess import sys import time -from _typeshed import ReadableBuffer, SizedBuffer +from _typeshed import ReadableBuffer, SizedBuffer, Unused from builtins import list as _list # conflicts with a method named "list" -from collections.abc import Callable +from collections.abc import Callable, Generator from datetime import datetime from re import Pattern from socket import socket as _socket from ssl import SSLContext, SSLSocket from types import TracebackType from typing import IO, Any, Literal, SupportsAbs, SupportsInt -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, deprecated __all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple", "Int2AP", "ParseFlags", "Time2Internaldate", "IMAP4_SSL"] @@ -42,11 +42,17 @@ class IMAP4: PROTOCOL_VERSION: str def __init__(self, host: str = "", port: int = 143, timeout: float | None = None) -> None: ... def open(self, host: str = "", port: int = 143, timeout: float | None = None) -> None: ... + if sys.version_info >= (3, 14): + @property + @deprecated("IMAP4.file is unsupported, can cause errors, and may be removed.") + def file(self) -> IO[str] | IO[bytes]: ... + else: + file: IO[str] | IO[bytes] + def __getattr__(self, attr: str) -> Any: ... host: str port: int sock: _socket - file: IO[str] | IO[bytes] def read(self, size: int) -> bytes: ... def readline(self) -> bytes: ... def send(self, data: ReadableBuffer) -> None: ... @@ -72,6 +78,9 @@ class IMAP4: def getannotation(self, mailbox: str, entry: str, attribute: str) -> _CommandResults: ... def getquota(self, root: str) -> _CommandResults: ... def getquotaroot(self, mailbox: str) -> _CommandResults: ... + if sys.version_info >= (3, 14): + def idle(self, duration: float | None = None) -> Idler: ... + def list(self, directory: str = '""', pattern: str = "*") -> tuple[str, _AnyResponseData]: ... def login(self, user: str, password: str) -> tuple[Literal["OK"], _list[bytes]]: ... def login_cram_md5(self, user: str, password: str) -> _CommandResults: ... @@ -100,6 +109,15 @@ class IMAP4: def xatom(self, name: str, *args: str) -> _CommandResults: ... def print_log(self) -> None: ... +if sys.version_info >= (3, 14): + class Idler: + def __init__(self, imap: IMAP4, duration: float | None = None) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type: object, exc_val: Unused, exc_tb: Unused) -> Literal[False]: ... + def __iter__(self) -> Self: ... + def __next__(self) -> tuple[str, float | None]: ... + def burst(self, interval: float = 0.1) -> Generator[tuple[str, float | None]]: ... + class IMAP4_SSL(IMAP4): if sys.version_info < (3, 12): keyfile: str @@ -119,14 +137,25 @@ class IMAP4_SSL(IMAP4): timeout: float | None = None, ) -> None: ... sslobj: SSLSocket - file: IO[Any] + if sys.version_info >= (3, 14): + @property + @deprecated("IMAP4_SSL.file is unsupported, can cause errors, and may be removed.") + def file(self) -> IO[Any]: ... + else: + file: IO[Any] + def open(self, host: str = "", port: int | None = 993, timeout: float | None = None) -> None: ... def ssl(self) -> SSLSocket: ... class IMAP4_stream(IMAP4): command: str def __init__(self, command: str) -> None: ... - file: IO[Any] + if sys.version_info >= (3, 14): + @property + @deprecated("IMAP4_stream.file is unsupported, can cause errors, and may be removed.") + def file(self) -> IO[Any]: ... + else: + file: IO[Any] process: subprocess.Popen[bytes] writefile: IO[Any] readfile: IO[Any] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/imghdr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imghdr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/imghdr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/imghdr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/imp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/imp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/imp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.pyi new file mode 100644 index 0000000000000..cf0fd0807b7b8 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.pyi @@ -0,0 +1,183 @@ +import _ast +import sys +import types +from _typeshed import ReadableBuffer, StrPath +from abc import ABCMeta, abstractmethod +from collections.abc import Iterator, Mapping, Sequence +from importlib import _bootstrap_external +from importlib.machinery import ModuleSpec +from io import BufferedReader +from typing import IO, Any, Literal, Protocol, overload, runtime_checkable +from typing_extensions import deprecated + +if sys.version_info >= (3, 11): + __all__ = [ + "Loader", + "MetaPathFinder", + "PathEntryFinder", + "ResourceLoader", + "InspectLoader", + "ExecutionLoader", + "FileLoader", + "SourceLoader", + ] + + if sys.version_info < (3, 12): + __all__ += ["Finder", "ResourceReader", "Traversable", "TraversableResources"] + +if sys.version_info >= (3, 10): + from importlib._abc import Loader as Loader +else: + class Loader(metaclass=ABCMeta): + def load_module(self, fullname: str) -> types.ModuleType: ... + def module_repr(self, module: types.ModuleType) -> str: ... + def create_module(self, spec: ModuleSpec) -> types.ModuleType | None: ... + # Not defined on the actual class for backwards-compatibility reasons, + # but expected in new code. + def exec_module(self, module: types.ModuleType) -> None: ... + +if sys.version_info < (3, 12): + class Finder(metaclass=ABCMeta): ... + +@deprecated("Deprecated as of Python 3.7: Use importlib.resources.abc.TraversableResources instead.") +class ResourceLoader(Loader): + @abstractmethod + def get_data(self, path: str) -> bytes: ... + +class InspectLoader(Loader): + def is_package(self, fullname: str) -> bool: ... + def get_code(self, fullname: str) -> types.CodeType | None: ... + @abstractmethod + def get_source(self, fullname: str) -> str | None: ... + def exec_module(self, module: types.ModuleType) -> None: ... + @staticmethod + def source_to_code( + data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive, path: ReadableBuffer | StrPath = "" + ) -> types.CodeType: ... + +class ExecutionLoader(InspectLoader): + @abstractmethod + def get_filename(self, fullname: str) -> str: ... + +class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLoader, metaclass=ABCMeta): # type: ignore[misc] # incompatible definitions of source_to_code in the base classes + @deprecated("Deprecated as of Python 3.3: Use importlib.resources.abc.SourceLoader.path_stats instead.") + def path_mtime(self, path: str) -> float: ... + def set_data(self, path: str, data: bytes) -> None: ... + def get_source(self, fullname: str) -> str | None: ... + def path_stats(self, path: str) -> Mapping[str, Any]: ... + +# The base classes differ starting in 3.10: +if sys.version_info >= (3, 10): + # Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol + class MetaPathFinder(metaclass=ABCMeta): + if sys.version_info < (3, 12): + def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ... + + def invalidate_caches(self) -> None: ... + # Not defined on the actual class, but expected to exist. + def find_spec( + self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / + ) -> ModuleSpec | None: ... + + class PathEntryFinder(metaclass=ABCMeta): + if sys.version_info < (3, 12): + def find_module(self, fullname: str) -> Loader | None: ... + def find_loader(self, fullname: str) -> tuple[Loader | None, Sequence[str]]: ... + + def invalidate_caches(self) -> None: ... + # Not defined on the actual class, but expected to exist. + def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... + +else: + # Please keep in sync with _typeshed.importlib.MetaPathFinderProtocol + class MetaPathFinder(Finder): + def find_module(self, fullname: str, path: Sequence[str] | None) -> Loader | None: ... + def invalidate_caches(self) -> None: ... + # Not defined on the actual class, but expected to exist. + def find_spec( + self, fullname: str, path: Sequence[str] | None, target: types.ModuleType | None = ..., / + ) -> ModuleSpec | None: ... + + class PathEntryFinder(Finder): + def find_module(self, fullname: str) -> Loader | None: ... + def find_loader(self, fullname: str) -> tuple[Loader | None, Sequence[str]]: ... + def invalidate_caches(self) -> None: ... + # Not defined on the actual class, but expected to exist. + def find_spec(self, fullname: str, target: types.ModuleType | None = ...) -> ModuleSpec | None: ... + +class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader, metaclass=ABCMeta): + name: str + path: str + def __init__(self, fullname: str, path: str) -> None: ... + def get_data(self, path: str) -> bytes: ... + def get_filename(self, name: str | None = None) -> str: ... + def load_module(self, name: str | None = None) -> types.ModuleType: ... + +if sys.version_info < (3, 11): + class ResourceReader(metaclass=ABCMeta): + @abstractmethod + def open_resource(self, resource: str) -> IO[bytes]: ... + @abstractmethod + def resource_path(self, resource: str) -> str: ... + if sys.version_info >= (3, 10): + @abstractmethod + def is_resource(self, path: str) -> bool: ... + else: + @abstractmethod + def is_resource(self, name: str) -> bool: ... + + @abstractmethod + def contents(self) -> Iterator[str]: ... + + @runtime_checkable + class Traversable(Protocol): + @abstractmethod + def is_dir(self) -> bool: ... + @abstractmethod + def is_file(self) -> bool: ... + @abstractmethod + def iterdir(self) -> Iterator[Traversable]: ... + if sys.version_info >= (3, 11): + @abstractmethod + def joinpath(self, *descendants: str) -> Traversable: ... + else: + @abstractmethod + def joinpath(self, child: str, /) -> Traversable: ... + + # The documentation and runtime protocol allows *args, **kwargs arguments, + # but this would mean that all implementers would have to support them, + # which is not the case. + @overload + @abstractmethod + def open(self, mode: Literal["r"] = "r", *, encoding: str | None = None, errors: str | None = None) -> IO[str]: ... + @overload + @abstractmethod + def open(self, mode: Literal["rb"]) -> IO[bytes]: ... + @property + @abstractmethod + def name(self) -> str: ... + if sys.version_info >= (3, 10): + def __truediv__(self, child: str, /) -> Traversable: ... + else: + @abstractmethod + def __truediv__(self, child: str, /) -> Traversable: ... + + @abstractmethod + def read_bytes(self) -> bytes: ... + @abstractmethod + def read_text(self, encoding: str | None = None) -> str: ... + + class TraversableResources(ResourceReader): + @abstractmethod + def files(self) -> Traversable: ... + def open_resource(self, resource: str) -> BufferedReader: ... + def resource_path(self, resource: Any) -> str: ... + def is_resource(self, path: str) -> bool: ... + def contents(self) -> Iterator[str]: ... + +elif sys.version_info < (3, 14): + from importlib.resources.abc import ( + ResourceReader as ResourceReader, + Traversable as Traversable, + TraversableResources as TraversableResources, + ) diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi new file mode 100644 index 0000000000000..767046b70a3d1 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi @@ -0,0 +1,43 @@ +import sys +from importlib._bootstrap import BuiltinImporter as BuiltinImporter, FrozenImporter as FrozenImporter, ModuleSpec as ModuleSpec +from importlib._bootstrap_external import ( + BYTECODE_SUFFIXES as BYTECODE_SUFFIXES, + DEBUG_BYTECODE_SUFFIXES as DEBUG_BYTECODE_SUFFIXES, + EXTENSION_SUFFIXES as EXTENSION_SUFFIXES, + OPTIMIZED_BYTECODE_SUFFIXES as OPTIMIZED_BYTECODE_SUFFIXES, + SOURCE_SUFFIXES as SOURCE_SUFFIXES, + ExtensionFileLoader as ExtensionFileLoader, + FileFinder as FileFinder, + PathFinder as PathFinder, + SourceFileLoader as SourceFileLoader, + SourcelessFileLoader as SourcelessFileLoader, + WindowsRegistryFinder as WindowsRegistryFinder, +) + +if sys.version_info >= (3, 11): + from importlib._bootstrap_external import NamespaceLoader as NamespaceLoader +if sys.version_info >= (3, 14): + from importlib._bootstrap_external import AppleFrameworkLoader as AppleFrameworkLoader + +def all_suffixes() -> list[str]: ... + +if sys.version_info >= (3, 14): + __all__ = [ + "AppleFrameworkLoader", + "BYTECODE_SUFFIXES", + "BuiltinImporter", + "DEBUG_BYTECODE_SUFFIXES", + "EXTENSION_SUFFIXES", + "ExtensionFileLoader", + "FileFinder", + "FrozenImporter", + "ModuleSpec", + "NamespaceLoader", + "OPTIMIZED_BYTECODE_SUFFIXES", + "PathFinder", + "SOURCE_SUFFIXES", + "SourceFileLoader", + "SourcelessFileLoader", + "WindowsRegistryFinder", + "all_suffixes", + ] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi new file mode 100644 index 0000000000000..789878382ceb8 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi @@ -0,0 +1,292 @@ +import abc +import pathlib +import sys +import types +from _collections_abc import dict_keys, dict_values +from _typeshed import StrPath +from collections.abc import Iterable, Iterator, Mapping +from email.message import Message +from importlib.abc import MetaPathFinder +from os import PathLike +from pathlib import Path +from re import Pattern +from typing import Any, ClassVar, Generic, NamedTuple, TypeVar, overload +from typing_extensions import Self, TypeAlias + +_T = TypeVar("_T") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") + +__all__ = [ + "Distribution", + "DistributionFinder", + "PackageNotFoundError", + "distribution", + "distributions", + "entry_points", + "files", + "metadata", + "requires", + "version", +] + +if sys.version_info >= (3, 10): + __all__ += ["PackageMetadata", "packages_distributions"] + +if sys.version_info >= (3, 10): + from importlib.metadata._meta import PackageMetadata as PackageMetadata, SimplePath + def packages_distributions() -> Mapping[str, list[str]]: ... + + _SimplePath: TypeAlias = SimplePath + +else: + _SimplePath: TypeAlias = Path + +class PackageNotFoundError(ModuleNotFoundError): + @property + def name(self) -> str: ... # type: ignore[override] + +if sys.version_info >= (3, 13): + _EntryPointBase = object +elif sys.version_info >= (3, 11): + class DeprecatedTuple: + def __getitem__(self, item: int) -> str: ... + + _EntryPointBase = DeprecatedTuple +else: + class _EntryPointBase(NamedTuple): + name: str + value: str + group: str + +class EntryPoint(_EntryPointBase): + pattern: ClassVar[Pattern[str]] + if sys.version_info >= (3, 11): + name: str + value: str + group: str + + def __init__(self, name: str, value: str, group: str) -> None: ... + + def load(self) -> Any: ... # Callable[[], Any] or an importable module + @property + def extras(self) -> list[str]: ... + @property + def module(self) -> str: ... + @property + def attr(self) -> str: ... + if sys.version_info >= (3, 10): + dist: ClassVar[Distribution | None] + def matches( + self, + *, + name: str = ..., + value: str = ..., + group: str = ..., + module: str = ..., + attr: str = ..., + extras: list[str] = ..., + ) -> bool: ... # undocumented + + def __hash__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + if sys.version_info >= (3, 11): + def __lt__(self, other: object) -> bool: ... + if sys.version_info < (3, 12): + def __iter__(self) -> Iterator[Any]: ... # result of iter((str, Self)), really + +if sys.version_info >= (3, 12): + class EntryPoints(tuple[EntryPoint, ...]): + def __getitem__(self, name: str) -> EntryPoint: ... # type: ignore[override] + def select( + self, + *, + name: str = ..., + value: str = ..., + group: str = ..., + module: str = ..., + attr: str = ..., + extras: list[str] = ..., + ) -> EntryPoints: ... + @property + def names(self) -> set[str]: ... + @property + def groups(self) -> set[str]: ... + +elif sys.version_info >= (3, 10): + class DeprecatedList(list[_T]): ... + + class EntryPoints(DeprecatedList[EntryPoint]): # use as list is deprecated since 3.10 + # int argument is deprecated since 3.10 + def __getitem__(self, name: int | str) -> EntryPoint: ... # type: ignore[override] + def select( + self, + *, + name: str = ..., + value: str = ..., + group: str = ..., + module: str = ..., + attr: str = ..., + extras: list[str] = ..., + ) -> EntryPoints: ... + @property + def names(self) -> set[str]: ... + @property + def groups(self) -> set[str]: ... + +if sys.version_info >= (3, 10) and sys.version_info < (3, 12): + class Deprecated(Generic[_KT, _VT]): + def __getitem__(self, name: _KT) -> _VT: ... + @overload + def get(self, name: _KT, default: None = None) -> _VT | None: ... + @overload + def get(self, name: _KT, default: _VT) -> _VT: ... + @overload + def get(self, name: _KT, default: _T) -> _VT | _T: ... + def __iter__(self) -> Iterator[_KT]: ... + def __contains__(self, *args: object) -> bool: ... + def keys(self) -> dict_keys[_KT, _VT]: ... + def values(self) -> dict_values[_KT, _VT]: ... + + class SelectableGroups(Deprecated[str, EntryPoints], dict[str, EntryPoints]): # use as dict is deprecated since 3.10 + @classmethod + def load(cls, eps: Iterable[EntryPoint]) -> Self: ... + @property + def groups(self) -> set[str]: ... + @property + def names(self) -> set[str]: ... + @overload + def select(self) -> Self: ... + @overload + def select( + self, + *, + name: str = ..., + value: str = ..., + group: str = ..., + module: str = ..., + attr: str = ..., + extras: list[str] = ..., + ) -> EntryPoints: ... + +class PackagePath(pathlib.PurePosixPath): + def read_text(self, encoding: str = "utf-8") -> str: ... + def read_binary(self) -> bytes: ... + def locate(self) -> PathLike[str]: ... + # The following attributes are not defined on PackagePath, but are dynamically added by Distribution.files: + hash: FileHash | None + size: int | None + dist: Distribution + +class FileHash: + mode: str + value: str + def __init__(self, spec: str) -> None: ... + +if sys.version_info >= (3, 12): + class DeprecatedNonAbstract: ... + _distribution_parent = DeprecatedNonAbstract +else: + _distribution_parent = object + +class Distribution(_distribution_parent): + @abc.abstractmethod + def read_text(self, filename: str) -> str | None: ... + @abc.abstractmethod + def locate_file(self, path: StrPath) -> _SimplePath: ... + @classmethod + def from_name(cls, name: str) -> Distribution: ... + @overload + @classmethod + def discover(cls, *, context: DistributionFinder.Context) -> Iterable[Distribution]: ... + @overload + @classmethod + def discover( + cls, *, context: None = None, name: str | None = ..., path: list[str] = ..., **kwargs: Any + ) -> Iterable[Distribution]: ... + @staticmethod + def at(path: StrPath) -> PathDistribution: ... + + if sys.version_info >= (3, 10): + @property + def metadata(self) -> PackageMetadata: ... + @property + def entry_points(self) -> EntryPoints: ... + else: + @property + def metadata(self) -> Message: ... + @property + def entry_points(self) -> list[EntryPoint]: ... + + @property + def version(self) -> str: ... + @property + def files(self) -> list[PackagePath] | None: ... + @property + def requires(self) -> list[str] | None: ... + if sys.version_info >= (3, 10): + @property + def name(self) -> str: ... + if sys.version_info >= (3, 13): + @property + def origin(self) -> types.SimpleNamespace: ... + +class DistributionFinder(MetaPathFinder): + class Context: + name: str | None + def __init__(self, *, name: str | None = ..., path: list[str] = ..., **kwargs: Any) -> None: ... + @property + def path(self) -> list[str]: ... + + @abc.abstractmethod + def find_distributions(self, context: DistributionFinder.Context = ...) -> Iterable[Distribution]: ... + +class MetadataPathFinder(DistributionFinder): + @classmethod + def find_distributions(cls, context: DistributionFinder.Context = ...) -> Iterable[PathDistribution]: ... + if sys.version_info >= (3, 11): + @classmethod + def invalidate_caches(cls) -> None: ... + elif sys.version_info >= (3, 10): + # Yes, this is an instance method that has a parameter named "cls" + def invalidate_caches(cls) -> None: ... + +class PathDistribution(Distribution): + _path: _SimplePath + def __init__(self, path: _SimplePath) -> None: ... + def read_text(self, filename: StrPath) -> str | None: ... + def locate_file(self, path: StrPath) -> _SimplePath: ... + +def distribution(distribution_name: str) -> Distribution: ... +@overload +def distributions(*, context: DistributionFinder.Context) -> Iterable[Distribution]: ... +@overload +def distributions( + *, context: None = None, name: str | None = ..., path: list[str] = ..., **kwargs: Any +) -> Iterable[Distribution]: ... + +if sys.version_info >= (3, 10): + def metadata(distribution_name: str) -> PackageMetadata: ... + +else: + def metadata(distribution_name: str) -> Message: ... + +if sys.version_info >= (3, 12): + def entry_points( + *, name: str = ..., value: str = ..., group: str = ..., module: str = ..., attr: str = ..., extras: list[str] = ... + ) -> EntryPoints: ... + +elif sys.version_info >= (3, 10): + @overload + def entry_points() -> SelectableGroups: ... + @overload + def entry_points( + *, name: str = ..., value: str = ..., group: str = ..., module: str = ..., attr: str = ..., extras: list[str] = ... + ) -> EntryPoints: ... + +else: + def entry_points() -> dict[str, list[EntryPoint]]: ... + +def version(distribution_name: str) -> str: ... +def files(distribution_name: str) -> list[PackagePath] | None: ... +def requires(distribution_name: str) -> list[str] | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/readers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/readers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi new file mode 100644 index 0000000000000..e672a619bd17a --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi @@ -0,0 +1,82 @@ +import os +import sys +from collections.abc import Iterator +from contextlib import AbstractContextManager +from pathlib import Path +from types import ModuleType +from typing import Any, BinaryIO, Literal, TextIO +from typing_extensions import TypeAlias + +if sys.version_info >= (3, 11): + from importlib.resources.abc import Traversable +else: + from importlib.abc import Traversable + +if sys.version_info >= (3, 11): + from importlib.resources._common import Package as Package +else: + Package: TypeAlias = str | ModuleType + +__all__ = [ + "Package", + "as_file", + "contents", + "files", + "is_resource", + "open_binary", + "open_text", + "path", + "read_binary", + "read_text", +] + +if sys.version_info >= (3, 10): + __all__ += ["ResourceReader"] + +if sys.version_info < (3, 13): + __all__ += ["Resource"] + +if sys.version_info < (3, 11): + Resource: TypeAlias = str | os.PathLike[Any] +elif sys.version_info < (3, 13): + Resource: TypeAlias = str + +if sys.version_info >= (3, 12): + from importlib.resources._common import Anchor as Anchor + + __all__ += ["Anchor"] + +if sys.version_info >= (3, 13): + from importlib.resources._functional import ( + contents as contents, + is_resource as is_resource, + open_binary as open_binary, + open_text as open_text, + path as path, + read_binary as read_binary, + read_text as read_text, + ) + +else: + def open_binary(package: Package, resource: Resource) -> BinaryIO: ... + def open_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> TextIO: ... + def read_binary(package: Package, resource: Resource) -> bytes: ... + def read_text(package: Package, resource: Resource, encoding: str = "utf-8", errors: str = "strict") -> str: ... + def path(package: Package, resource: Resource) -> AbstractContextManager[Path, Literal[False]]: ... + def is_resource(package: Package, name: str) -> bool: ... + def contents(package: Package) -> Iterator[str]: ... + +if sys.version_info >= (3, 11): + from importlib.resources._common import as_file as as_file +else: + def as_file(path: Traversable) -> AbstractContextManager[Path, Literal[False]]: ... + +if sys.version_info >= (3, 11): + from importlib.resources._common import files as files +else: + def files(package: Package) -> Traversable: ... + +if sys.version_info >= (3, 11): + from importlib.resources.abc import ResourceReader as ResourceReader +elif sys.version_info >= (3, 10): + from importlib.abc import ResourceReader as ResourceReader diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi similarity index 95% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi index d6a9436544dce..3dd961bb657b1 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi @@ -5,7 +5,7 @@ if sys.version_info >= (3, 11): import types from collections.abc import Callable from contextlib import AbstractContextManager - from importlib.abc import ResourceReader, Traversable + from importlib.resources.abc import ResourceReader, Traversable from pathlib import Path from typing import Literal, overload from typing_extensions import TypeAlias, deprecated diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi new file mode 100644 index 0000000000000..fe0fe64dba0df --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi @@ -0,0 +1,69 @@ +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import Iterator +from io import BufferedReader +from typing import IO, Any, Literal, Protocol, overload, runtime_checkable + +if sys.version_info >= (3, 11): + class ResourceReader(metaclass=ABCMeta): + @abstractmethod + def open_resource(self, resource: str) -> IO[bytes]: ... + @abstractmethod + def resource_path(self, resource: str) -> str: ... + if sys.version_info >= (3, 10): + @abstractmethod + def is_resource(self, path: str) -> bool: ... + else: + @abstractmethod + def is_resource(self, name: str) -> bool: ... + + @abstractmethod + def contents(self) -> Iterator[str]: ... + + @runtime_checkable + class Traversable(Protocol): + @abstractmethod + def is_dir(self) -> bool: ... + @abstractmethod + def is_file(self) -> bool: ... + @abstractmethod + def iterdir(self) -> Iterator[Traversable]: ... + if sys.version_info >= (3, 11): + @abstractmethod + def joinpath(self, *descendants: str) -> Traversable: ... + else: + @abstractmethod + def joinpath(self, child: str, /) -> Traversable: ... + + # The documentation and runtime protocol allows *args, **kwargs arguments, + # but this would mean that all implementers would have to support them, + # which is not the case. + @overload + @abstractmethod + def open(self, mode: Literal["r"] = "r", *, encoding: str | None = None, errors: str | None = None) -> IO[str]: ... + @overload + @abstractmethod + def open(self, mode: Literal["rb"]) -> IO[bytes]: ... + @property + @abstractmethod + def name(self) -> str: ... + if sys.version_info >= (3, 10): + def __truediv__(self, child: str, /) -> Traversable: ... + else: + @abstractmethod + def __truediv__(self, child: str, /) -> Traversable: ... + + @abstractmethod + def read_bytes(self) -> bytes: ... + @abstractmethod + def read_text(self, encoding: str | None = None) -> str: ... + + class TraversableResources(ResourceReader): + @abstractmethod + def files(self) -> Traversable: ... + def open_resource(self, resource: str) -> BufferedReader: ... + def resource_path(self, resource: Any) -> str: ... + def is_resource(self, path: str) -> bool: ... + def contents(self) -> Iterator[str]: ... + + __all__ = ["ResourceReader", "Traversable", "TraversableResources"] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/simple.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/simple.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.pyi new file mode 100644 index 0000000000000..370a08623842e --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.pyi @@ -0,0 +1,49 @@ +import importlib.machinery +import sys +import types +from _typeshed import ReadableBuffer +from collections.abc import Callable +from importlib._bootstrap import module_from_spec as module_from_spec, spec_from_loader as spec_from_loader +from importlib._bootstrap_external import ( + MAGIC_NUMBER as MAGIC_NUMBER, + cache_from_source as cache_from_source, + decode_source as decode_source, + source_from_cache as source_from_cache, + spec_from_file_location as spec_from_file_location, +) +from importlib.abc import Loader +from typing_extensions import ParamSpec + +_P = ParamSpec("_P") + +if sys.version_info < (3, 12): + def module_for_loader(fxn: Callable[_P, types.ModuleType]) -> Callable[_P, types.ModuleType]: ... + def set_loader(fxn: Callable[_P, types.ModuleType]) -> Callable[_P, types.ModuleType]: ... + def set_package(fxn: Callable[_P, types.ModuleType]) -> Callable[_P, types.ModuleType]: ... + +def resolve_name(name: str, package: str | None) -> str: ... +def find_spec(name: str, package: str | None = None) -> importlib.machinery.ModuleSpec | None: ... + +class LazyLoader(Loader): + def __init__(self, loader: Loader) -> None: ... + @classmethod + def factory(cls, loader: Loader) -> Callable[..., LazyLoader]: ... + def exec_module(self, module: types.ModuleType) -> None: ... + +def source_hash(source_bytes: ReadableBuffer) -> bytes: ... + +if sys.version_info >= (3, 14): + __all__ = [ + "LazyLoader", + "Loader", + "MAGIC_NUMBER", + "cache_from_source", + "decode_source", + "find_spec", + "module_from_spec", + "resolve_name", + "source_from_cache", + "source_hash", + "spec_from_file_location", + "spec_from_loader", + ] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/inspect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi similarity index 91% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/inspect.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi index c525418c104b4..e19c2a634aa09 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/inspect.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi @@ -2,7 +2,7 @@ import dis import enum import sys import types -from _typeshed import StrPath +from _typeshed import AnnotationForm, StrPath from collections import OrderedDict from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine, Generator, Mapping, Sequence, Set as AbstractSet from types import ( @@ -28,6 +28,9 @@ from types import ( from typing import Any, ClassVar, Final, Literal, NamedTuple, Protocol, TypeVar, overload from typing_extensions import ParamSpec, Self, TypeAlias, TypeGuard, TypeIs +if sys.version_info >= (3, 14): + from annotationlib import Format + if sys.version_info >= (3, 11): __all__ = [ "ArgInfo", @@ -139,6 +142,8 @@ if sys.version_info >= (3, 11): "getasyncgenstate", "BufferFlags", ] + if sys.version_info >= (3, 14): + __all__ += ["CO_HAS_DOCSTRING", "CO_METHOD", "ispackage"] _P = ParamSpec("_P") _T = TypeVar("_T") @@ -172,6 +177,9 @@ CO_COROUTINE: Final = 128 CO_ITERABLE_COROUTINE: Final = 256 CO_ASYNC_GENERATOR: Final = 512 TPFLAGS_IS_ABSTRACT: Final = 1048576 +if sys.version_info >= (3, 14): + CO_HAS_DOCSTRING: Final = 67108864 + CO_METHOD: Final = 134217728 modulesbyfile: dict[str, Any] @@ -199,6 +207,11 @@ def getmodulename(path: StrPath) -> str | None: ... def ismodule(object: object) -> TypeIs[ModuleType]: ... def isclass(object: object) -> TypeIs[type[Any]]: ... def ismethod(object: object) -> TypeIs[MethodType]: ... + +if sys.version_info >= (3, 14): + # Not TypeIs because it does not return True for all modules + def ispackage(object: object) -> TypeGuard[ModuleType]: ... + def isfunction(object: object) -> TypeIs[FunctionType]: ... if sys.version_info >= (3, 12): @@ -294,7 +307,18 @@ _IntrospectableCallable: TypeAlias = Callable[..., Any] # # Introspecting callables with the Signature object # -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 14): + def signature( + obj: _IntrospectableCallable, + *, + follow_wrapped: bool = True, + globals: Mapping[str, Any] | None = None, + locals: Mapping[str, Any] | None = None, + eval_str: bool = False, + annotation_format: Format = Format.VALUE, # noqa: Y011 + ) -> Signature: ... + +elif sys.version_info >= (3, 10): def signature( obj: _IntrospectableCallable, *, @@ -323,7 +347,19 @@ class Signature: def bind_partial(self, *args: Any, **kwargs: Any) -> BoundArguments: ... def replace(self, *, parameters: Sequence[Parameter] | type[_void] | None = ..., return_annotation: Any = ...) -> Self: ... __replace__ = replace - if sys.version_info >= (3, 10): + if sys.version_info >= (3, 14): + @classmethod + def from_callable( + cls, + obj: _IntrospectableCallable, + *, + follow_wrapped: bool = True, + globals: Mapping[str, Any] | None = None, + locals: Mapping[str, Any] | None = None, + eval_str: bool = False, + annotation_format: Format = Format.VALUE, # noqa: Y011 + ) -> Self: ... + elif sys.version_info >= (3, 10): @classmethod def from_callable( cls, @@ -337,20 +373,24 @@ class Signature: else: @classmethod def from_callable(cls, obj: _IntrospectableCallable, *, follow_wrapped: bool = True) -> Self: ... - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + def format(self, *, max_width: int | None = None, quote_annotation_strings: bool = True) -> str: ... + elif sys.version_info >= (3, 13): def format(self, *, max_width: int | None = None) -> str: ... def __eq__(self, other: object) -> bool: ... def __hash__(self) -> int: ... -if sys.version_info >= (3, 10): +if sys.version_info >= (3, 14): + from annotationlib import get_annotations as get_annotations +elif sys.version_info >= (3, 10): def get_annotations( obj: Callable[..., object] | type[object] | ModuleType, # any callable, class, or module *, globals: Mapping[str, Any] | None = None, # value types depend on the key locals: Mapping[str, Any] | None = None, # value types depend on the key eval_str: bool = False, - ) -> dict[str, Any]: ... # values are type expressions + ) -> dict[str, AnnotationForm]: ... # values are type expressions # The name is the same as the enum's name in CPython class _ParameterKind(enum.IntEnum): @@ -461,7 +501,13 @@ class ArgInfo(NamedTuple): locals: dict[str, Any] def getargvalues(frame: FrameType) -> ArgInfo: ... -def formatannotation(annotation: object, base_module: str | None = None) -> str: ... + +if sys.version_info >= (3, 14): + def formatannotation(annotation: object, base_module: str | None = None, *, quote_annotation_strings: bool = True) -> str: ... + +else: + def formatannotation(annotation: object, base_module: str | None = None) -> str: ... + def formatannotationrelativeto(object: object) -> Callable[[object], str]: ... if sys.version_info < (3, 11): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/io.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/io.pyi similarity index 77% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/io.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/io.pyi index 5c26cb245a2f1..1313df183d36d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/io.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/io.pyi @@ -20,7 +20,7 @@ from _io import ( open as open, open_code as open_code, ) -from typing import Final +from typing import Final, Protocol, TypeVar __all__ = [ "BlockingIOError", @@ -44,11 +44,17 @@ __all__ = [ "SEEK_END", ] +if sys.version_info >= (3, 14): + __all__ += ["Reader", "Writer"] + if sys.version_info >= (3, 11): from _io import text_encoding as text_encoding __all__ += ["DEFAULT_BUFFER_SIZE", "IncrementalNewlineDecoder", "text_encoding"] +_T_co = TypeVar("_T_co", covariant=True) +_T_contra = TypeVar("_T_contra", contravariant=True) + SEEK_SET: Final = 0 SEEK_CUR: Final = 1 SEEK_END: Final = 2 @@ -58,3 +64,10 @@ class IOBase(_IOBase, metaclass=abc.ABCMeta): ... class RawIOBase(_RawIOBase, IOBase): ... class BufferedIOBase(_BufferedIOBase, IOBase): ... class TextIOBase(_TextIOBase, IOBase): ... + +if sys.version_info >= (3, 14): + class Reader(Protocol[_T_co]): + def read(self, size: int = ..., /) -> _T_co: ... + + class Writer(Protocol[_T_contra]): + def write(self, data: _T_contra, /) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ipaddress.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ipaddress.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ipaddress.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ipaddress.pyi index 6883895fd2193..9df6bab7c167b 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ipaddress.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ipaddress.pyi @@ -28,8 +28,9 @@ class _IPAddressBase: def exploded(self) -> str: ... @property def reverse_pointer(self) -> str: ... - @property - def version(self) -> int: ... + if sys.version_info < (3, 14): + @property + def version(self) -> int: ... class _BaseAddress(_IPAddressBase): def __add__(self, other: int) -> Self: ... @@ -104,10 +105,14 @@ class _BaseNetwork(_IPAddressBase, Generic[_A]): def hostmask(self) -> _A: ... class _BaseV4: - @property - def version(self) -> Literal[4]: ... - @property - def max_prefixlen(self) -> Literal[32]: ... + if sys.version_info >= (3, 14): + version: Final = 4 + max_prefixlen: Final = 32 + else: + @property + def version(self) -> Literal[4]: ... + @property + def max_prefixlen(self) -> Literal[32]: ... class IPv4Address(_BaseV4, _BaseAddress): def __init__(self, address: object) -> None: ... @@ -151,10 +156,14 @@ class IPv4Interface(IPv4Address): def with_prefixlen(self) -> str: ... class _BaseV6: - @property - def version(self) -> Literal[6]: ... - @property - def max_prefixlen(self) -> Literal[128]: ... + if sys.version_info >= (3, 14): + version: Final = 6 + max_prefixlen: Final = 128 + else: + @property + def version(self) -> Literal[6]: ... + @property + def max_prefixlen(self) -> Literal[128]: ... class IPv6Address(_BaseV6, _BaseAddress): def __init__(self, address: object) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/itertools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/itertools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/itertools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/itertools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/decoder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/decoder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/decoder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/decoder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/encoder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/encoder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/encoder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/encoder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/scanner.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/scanner.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/tool.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/tool.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/tool.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/tool.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/keyword.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/keyword.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/keyword.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/keyword.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/linecache.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/linecache.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/linecache.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/linecache.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/locale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/locale.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi new file mode 100644 index 0000000000000..24529bd48d6a7 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi @@ -0,0 +1,660 @@ +import sys +import threading +from _typeshed import StrPath, SupportsWrite +from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence +from io import TextIOWrapper +from re import Pattern +from string import Template +from time import struct_time +from types import FrameType, GenericAlias, TracebackType +from typing import Any, ClassVar, Final, Generic, Literal, Protocol, TextIO, TypeVar, overload +from typing_extensions import Self, TypeAlias, deprecated + +__all__ = [ + "BASIC_FORMAT", + "BufferingFormatter", + "CRITICAL", + "DEBUG", + "ERROR", + "FATAL", + "FileHandler", + "Filter", + "Formatter", + "Handler", + "INFO", + "LogRecord", + "Logger", + "LoggerAdapter", + "NOTSET", + "NullHandler", + "StreamHandler", + "WARN", + "WARNING", + "addLevelName", + "basicConfig", + "captureWarnings", + "critical", + "debug", + "disable", + "error", + "exception", + "fatal", + "getLevelName", + "getLogger", + "getLoggerClass", + "info", + "log", + "makeLogRecord", + "setLoggerClass", + "shutdown", + "warning", + "getLogRecordFactory", + "setLogRecordFactory", + "lastResort", + "raiseExceptions", + "warn", +] + +if sys.version_info >= (3, 11): + __all__ += ["getLevelNamesMapping"] +if sys.version_info >= (3, 12): + __all__ += ["getHandlerByName", "getHandlerNames"] + +_SysExcInfoType: TypeAlias = tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] +_ExcInfoType: TypeAlias = None | bool | _SysExcInfoType | BaseException +_ArgsType: TypeAlias = tuple[object, ...] | Mapping[str, object] +_Level: TypeAlias = int | str +_FormatStyle: TypeAlias = Literal["%", "{", "$"] + +if sys.version_info >= (3, 12): + class _SupportsFilter(Protocol): + def filter(self, record: LogRecord, /) -> bool | LogRecord: ... + + _FilterType: TypeAlias = Filter | Callable[[LogRecord], bool | LogRecord] | _SupportsFilter +else: + class _SupportsFilter(Protocol): + def filter(self, record: LogRecord, /) -> bool: ... + + _FilterType: TypeAlias = Filter | Callable[[LogRecord], bool] | _SupportsFilter + +raiseExceptions: bool +logThreads: bool +logMultiprocessing: bool +logProcesses: bool +_srcfile: str | None + +def currentframe() -> FrameType: ... + +_levelToName: dict[int, str] +_nameToLevel: dict[str, int] + +class Filterer: + filters: list[_FilterType] + def addFilter(self, filter: _FilterType) -> None: ... + def removeFilter(self, filter: _FilterType) -> None: ... + if sys.version_info >= (3, 12): + def filter(self, record: LogRecord) -> bool | LogRecord: ... + else: + def filter(self, record: LogRecord) -> bool: ... + +class Manager: # undocumented + root: RootLogger + disable: int + emittedNoHandlerWarning: bool + loggerDict: dict[str, Logger | PlaceHolder] + loggerClass: type[Logger] | None + logRecordFactory: Callable[..., LogRecord] | None + def __init__(self, rootnode: RootLogger) -> None: ... + def getLogger(self, name: str) -> Logger: ... + def setLoggerClass(self, klass: type[Logger]) -> None: ... + def setLogRecordFactory(self, factory: Callable[..., LogRecord]) -> None: ... + +class Logger(Filterer): + name: str # undocumented + level: int # undocumented + parent: Logger | None # undocumented + propagate: bool + handlers: list[Handler] # undocumented + disabled: bool # undocumented + root: ClassVar[RootLogger] # undocumented + manager: Manager # undocumented + def __init__(self, name: str, level: _Level = 0) -> None: ... + def setLevel(self, level: _Level) -> None: ... + def isEnabledFor(self, level: int) -> bool: ... + def getEffectiveLevel(self) -> int: ... + def getChild(self, suffix: str) -> Self: ... # see python/typing#980 + if sys.version_info >= (3, 12): + def getChildren(self) -> set[Logger]: ... + + def debug( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def info( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def warning( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + @deprecated("Deprecated; use warning() instead.") + def warn( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def error( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def exception( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = True, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def critical( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def log( + self, + level: int, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + ) -> None: ... + def _log( + self, + level: int, + msg: object, + args: _ArgsType, + exc_info: _ExcInfoType | None = None, + extra: Mapping[str, object] | None = None, + stack_info: bool = False, + stacklevel: int = 1, + ) -> None: ... # undocumented + fatal = critical + def addHandler(self, hdlr: Handler) -> None: ... + def removeHandler(self, hdlr: Handler) -> None: ... + def findCaller(self, stack_info: bool = False, stacklevel: int = 1) -> tuple[str, int, str, str | None]: ... + def handle(self, record: LogRecord) -> None: ... + def makeRecord( + self, + name: str, + level: int, + fn: str, + lno: int, + msg: object, + args: _ArgsType, + exc_info: _SysExcInfoType | None, + func: str | None = None, + extra: Mapping[str, object] | None = None, + sinfo: str | None = None, + ) -> LogRecord: ... + def hasHandlers(self) -> bool: ... + def callHandlers(self, record: LogRecord) -> None: ... # undocumented + +CRITICAL: Final = 50 +FATAL: Final = CRITICAL +ERROR: Final = 40 +WARNING: Final = 30 +WARN: Final = WARNING +INFO: Final = 20 +DEBUG: Final = 10 +NOTSET: Final = 0 + +class Handler(Filterer): + level: int # undocumented + formatter: Formatter | None # undocumented + lock: threading.Lock | None # undocumented + name: str | None # undocumented + def __init__(self, level: _Level = 0) -> None: ... + def get_name(self) -> str: ... # undocumented + def set_name(self, name: str) -> None: ... # undocumented + def createLock(self) -> None: ... + def acquire(self) -> None: ... + def release(self) -> None: ... + def setLevel(self, level: _Level) -> None: ... + def setFormatter(self, fmt: Formatter | None) -> None: ... + def flush(self) -> None: ... + def close(self) -> None: ... + def handle(self, record: LogRecord) -> bool: ... + def handleError(self, record: LogRecord) -> None: ... + def format(self, record: LogRecord) -> str: ... + def emit(self, record: LogRecord) -> None: ... + +if sys.version_info >= (3, 12): + def getHandlerByName(name: str) -> Handler | None: ... + def getHandlerNames() -> frozenset[str]: ... + +class Formatter: + converter: Callable[[float | None], struct_time] + _fmt: str | None # undocumented + datefmt: str | None # undocumented + _style: PercentStyle # undocumented + default_time_format: str + default_msec_format: str | None + + if sys.version_info >= (3, 10): + def __init__( + self, + fmt: str | None = None, + datefmt: str | None = None, + style: _FormatStyle = "%", + validate: bool = True, + *, + defaults: Mapping[str, Any] | None = None, + ) -> None: ... + else: + def __init__( + self, fmt: str | None = None, datefmt: str | None = None, style: _FormatStyle = "%", validate: bool = True + ) -> None: ... + + def format(self, record: LogRecord) -> str: ... + def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str: ... + def formatException(self, ei: _SysExcInfoType) -> str: ... + def formatMessage(self, record: LogRecord) -> str: ... # undocumented + def formatStack(self, stack_info: str) -> str: ... + def usesTime(self) -> bool: ... # undocumented + +class BufferingFormatter: + linefmt: Formatter + def __init__(self, linefmt: Formatter | None = None) -> None: ... + def formatHeader(self, records: Sequence[LogRecord]) -> str: ... + def formatFooter(self, records: Sequence[LogRecord]) -> str: ... + def format(self, records: Sequence[LogRecord]) -> str: ... + +class Filter: + name: str # undocumented + nlen: int # undocumented + def __init__(self, name: str = "") -> None: ... + if sys.version_info >= (3, 12): + def filter(self, record: LogRecord) -> bool | LogRecord: ... + else: + def filter(self, record: LogRecord) -> bool: ... + +class LogRecord: + # args can be set to None by logging.handlers.QueueHandler + # (see https://bugs.python.org/issue44473) + args: _ArgsType | None + asctime: str + created: float + exc_info: _SysExcInfoType | None + exc_text: str | None + filename: str + funcName: str + levelname: str + levelno: int + lineno: int + module: str + msecs: float + # Only created when logging.Formatter.format is called. See #6132. + message: str + msg: str | Any # The runtime accepts any object, but will be a str in 99% of cases + name: str + pathname: str + process: int | None + processName: str | None + relativeCreated: float + stack_info: str | None + thread: int | None + threadName: str | None + if sys.version_info >= (3, 12): + taskName: str | None + + def __init__( + self, + name: str, + level: int, + pathname: str, + lineno: int, + msg: object, + args: _ArgsType | None, + exc_info: _SysExcInfoType | None, + func: str | None = None, + sinfo: str | None = None, + ) -> None: ... + def getMessage(self) -> str: ... + # Allows setting contextual information on LogRecord objects as per the docs, see #7833 + def __setattr__(self, name: str, value: Any, /) -> None: ... + +_L = TypeVar("_L", bound=Logger | LoggerAdapter[Any]) + +class LoggerAdapter(Generic[_L]): + logger: _L + manager: Manager # undocumented + + if sys.version_info >= (3, 13): + def __init__(self, logger: _L, extra: Mapping[str, object] | None = None, merge_extra: bool = False) -> None: ... + elif sys.version_info >= (3, 10): + def __init__(self, logger: _L, extra: Mapping[str, object] | None = None) -> None: ... + else: + def __init__(self, logger: _L, extra: Mapping[str, object]) -> None: ... + + if sys.version_info >= (3, 10): + extra: Mapping[str, object] | None + else: + extra: Mapping[str, object] + + if sys.version_info >= (3, 13): + merge_extra: bool + + def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> tuple[Any, MutableMapping[str, Any]]: ... + def debug( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def info( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def warning( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + @deprecated("Deprecated; use warning() instead.") + def warn( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def error( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def exception( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = True, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def critical( + self, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def log( + self, + level: int, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, + **kwargs: object, + ) -> None: ... + def isEnabledFor(self, level: int) -> bool: ... + def getEffectiveLevel(self) -> int: ... + def setLevel(self, level: _Level) -> None: ... + def hasHandlers(self) -> bool: ... + if sys.version_info >= (3, 11): + def _log( + self, + level: int, + msg: object, + args: _ArgsType, + *, + exc_info: _ExcInfoType | None = None, + extra: Mapping[str, object] | None = None, + stack_info: bool = False, + ) -> None: ... # undocumented + else: + def _log( + self, + level: int, + msg: object, + args: _ArgsType, + exc_info: _ExcInfoType | None = None, + extra: Mapping[str, object] | None = None, + stack_info: bool = False, + ) -> None: ... # undocumented + + @property + def name(self) -> str: ... # undocumented + if sys.version_info >= (3, 11): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +def getLogger(name: str | None = None) -> Logger: ... +def getLoggerClass() -> type[Logger]: ... +def getLogRecordFactory() -> Callable[..., LogRecord]: ... +def debug( + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +def info( + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +def warning( + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +@deprecated("Deprecated; use warning() instead.") +def warn( + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +def error( + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +def critical( + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +def exception( + msg: object, + *args: object, + exc_info: _ExcInfoType = True, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... +def log( + level: int, + msg: object, + *args: object, + exc_info: _ExcInfoType = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Mapping[str, object] | None = None, +) -> None: ... + +fatal = critical + +def disable(level: int = 50) -> None: ... +def addLevelName(level: int, levelName: str) -> None: ... +@overload +def getLevelName(level: int) -> str: ... +@overload +@deprecated("The str -> int case is considered a mistake.") +def getLevelName(level: str) -> Any: ... + +if sys.version_info >= (3, 11): + def getLevelNamesMapping() -> dict[str, int]: ... + +def makeLogRecord(dict: Mapping[str, object]) -> LogRecord: ... +def basicConfig( + *, + filename: StrPath | None = ..., + filemode: str = ..., + format: str = ..., + datefmt: str | None = ..., + style: _FormatStyle = ..., + level: _Level | None = ..., + stream: SupportsWrite[str] | None = ..., + handlers: Iterable[Handler] | None = ..., + force: bool | None = ..., + encoding: str | None = ..., + errors: str | None = ..., +) -> None: ... +def shutdown(handlerList: Sequence[Any] = ...) -> None: ... # handlerList is undocumented +def setLoggerClass(klass: type[Logger]) -> None: ... +def captureWarnings(capture: bool) -> None: ... +def setLogRecordFactory(factory: Callable[..., LogRecord]) -> None: ... + +lastResort: Handler | None + +_StreamT = TypeVar("_StreamT", bound=SupportsWrite[str]) + +class StreamHandler(Handler, Generic[_StreamT]): + stream: _StreamT # undocumented + terminator: str + @overload + def __init__(self: StreamHandler[TextIO], stream: None = None) -> None: ... + @overload + def __init__(self: StreamHandler[_StreamT], stream: _StreamT) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + def setStream(self, stream: _StreamT) -> _StreamT | None: ... + if sys.version_info >= (3, 11): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +class FileHandler(StreamHandler[TextIOWrapper]): + baseFilename: str # undocumented + mode: str # undocumented + encoding: str | None # undocumented + delay: bool # undocumented + errors: str | None # undocumented + def __init__( + self, filename: StrPath, mode: str = "a", encoding: str | None = None, delay: bool = False, errors: str | None = None + ) -> None: ... + def _open(self) -> TextIOWrapper: ... # undocumented + +class NullHandler(Handler): ... + +class PlaceHolder: # undocumented + loggerMap: dict[Logger, None] + def __init__(self, alogger: Logger) -> None: ... + def append(self, alogger: Logger) -> None: ... + +# Below aren't in module docs but still visible + +class RootLogger(Logger): + def __init__(self, level: int) -> None: ... + +root: RootLogger + +class PercentStyle: # undocumented + default_format: str + asctime_format: str + asctime_search: str + validation_pattern: Pattern[str] + _fmt: str + if sys.version_info >= (3, 10): + def __init__(self, fmt: str, *, defaults: Mapping[str, Any] | None = None) -> None: ... + else: + def __init__(self, fmt: str) -> None: ... + + def usesTime(self) -> bool: ... + def validate(self) -> None: ... + def format(self, record: Any) -> str: ... + +class StrFormatStyle(PercentStyle): # undocumented + fmt_spec: Pattern[str] + field_spec: Pattern[str] + +class StringTemplateStyle(PercentStyle): # undocumented + _tpl: Template + +_STYLES: Final[dict[str, tuple[PercentStyle, str]]] + +BASIC_FORMAT: Final[str] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/config.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/config.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/logging/config.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/logging/config.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/handlers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/handlers.pyi similarity index 91% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/logging/handlers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/logging/handlers.pyi index 2c7ec05afe9a3..9636b81dc4f3c 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/handlers.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/logging/handlers.pyi @@ -8,7 +8,9 @@ from logging import FileHandler, Handler, LogRecord from re import Pattern from socket import SocketKind, socket from threading import Thread +from types import TracebackType from typing import Any, ClassVar, Final, Protocol, TypeVar +from typing_extensions import Self _T = TypeVar("_T") @@ -142,9 +144,19 @@ class SysLogHandler(Handler): priority_names: ClassVar[dict[str, int]] # undocumented facility_names: ClassVar[dict[str, int]] # undocumented priority_map: ClassVar[dict[str, str]] # undocumented - def __init__( - self, address: tuple[str, int] | str = ("localhost", 514), facility: str | int = 1, socktype: SocketKind | None = None - ) -> None: ... + if sys.version_info >= (3, 14): + timeout: float | None + def __init__( + self, + address: tuple[str, int] | str = ("localhost", 514), + facility: str | int = 1, + socktype: SocketKind | None = None, + timeout: float | None = None, + ) -> None: ... + else: + def __init__( + self, address: tuple[str, int] | str = ("localhost", 514), facility: str | int = 1, socktype: SocketKind | None = None + ) -> None: ... if sys.version_info >= (3, 11): def createSocket(self) -> None: ... @@ -237,3 +249,9 @@ class QueueListener: def stop(self) -> None: ... def enqueue_sentinel(self) -> None: ... def handle(self, record: LogRecord) -> None: ... + + if sys.version_info >= (3, 14): + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None + ) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lzma.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lzma.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lzma.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lzma.pyi index 2f0279f5986bd..b7ef607b75cbf 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/lzma.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lzma.pyi @@ -1,4 +1,4 @@ -from _compression import BaseStream +import sys from _lzma import ( CHECK_CRC32 as CHECK_CRC32, CHECK_CRC64 as CHECK_CRC64, @@ -35,9 +35,15 @@ from _lzma import ( is_check_supported as is_check_supported, ) from _typeshed import ReadableBuffer, StrOrBytesPath -from typing import IO, Literal, TextIO, overload +from io import TextIOWrapper +from typing import IO, Literal, overload from typing_extensions import Self, TypeAlias +if sys.version_info >= (3, 14): + from compression._common._streams import BaseStream +else: + from _compression import BaseStream + __all__ = [ "CHECK_NONE", "CHECK_CRC32", @@ -139,7 +145,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> TextIO: ... +) -> TextIOWrapper: ... @overload def open( filename: StrOrBytesPath, @@ -152,7 +158,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> TextIO: ... +) -> TextIOWrapper: ... @overload def open( filename: _PathOrFile, @@ -165,7 +171,7 @@ def open( encoding: str | None = None, errors: str | None = None, newline: str | None = None, -) -> LZMAFile | TextIO: ... +) -> LZMAFile | TextIOWrapper: ... def compress( data: ReadableBuffer, format: int = 1, check: int = -1, preset: int | None = None, filters: _FilterChain | None = None ) -> bytes: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mailbox.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mailbox.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mailbox.pyi index dc2fbd593d674..ff605c0661fb1 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/mailbox.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.pyi @@ -4,6 +4,7 @@ import sys from _typeshed import StrPath, SupportsNoArgReadline, SupportsRead from abc import ABCMeta, abstractmethod from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence +from email._policybase import _MessageT from types import GenericAlias, TracebackType from typing import IO, Any, AnyStr, Generic, Literal, Protocol, TypeVar, overload from typing_extensions import Self, TypeAlias @@ -29,7 +30,6 @@ __all__ = [ ] _T = TypeVar("_T") -_MessageT = TypeVar("_MessageT", bound=Message) class _SupportsReadAndReadline(SupportsRead[bytes], SupportsNoArgReadline[bytes], Protocol): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mailcap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mailcap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mailcap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mailcap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/marshal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/marshal.pyi similarity index 78% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/marshal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/marshal.pyi index 6ab202637ddaa..46c421e4ce307 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/marshal.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/marshal.pyi @@ -2,10 +2,10 @@ import builtins import sys import types from _typeshed import ReadableBuffer, SupportsRead, SupportsWrite -from typing import Any +from typing import Any, Final from typing_extensions import TypeAlias -version: int +version: Final[int] _Marshallable: TypeAlias = ( # handled in w_object() in marshal.c @@ -28,14 +28,22 @@ _Marshallable: TypeAlias = ( | ReadableBuffer ) -if sys.version_info >= (3, 13): +if sys.version_info >= (3, 14): + def dump(value: _Marshallable, file: SupportsWrite[bytes], version: int = 5, /, *, allow_code: bool = True) -> None: ... + def dumps(value: _Marshallable, version: int = 5, /, *, allow_code: bool = True) -> bytes: ... + +elif sys.version_info >= (3, 13): def dump(value: _Marshallable, file: SupportsWrite[bytes], version: int = 4, /, *, allow_code: bool = True) -> None: ... - def load(file: SupportsRead[bytes], /, *, allow_code: bool = True) -> Any: ... def dumps(value: _Marshallable, version: int = 4, /, *, allow_code: bool = True) -> bytes: ... - def loads(bytes: ReadableBuffer, /, *, allow_code: bool = True) -> Any: ... else: def dump(value: _Marshallable, file: SupportsWrite[bytes], version: int = 4, /) -> None: ... - def load(file: SupportsRead[bytes], /) -> Any: ... def dumps(value: _Marshallable, version: int = 4, /) -> bytes: ... + +if sys.version_info >= (3, 13): + def load(file: SupportsRead[bytes], /, *, allow_code: bool = True) -> Any: ... + def loads(bytes: ReadableBuffer, /, *, allow_code: bool = True) -> Any: ... + +else: + def load(file: SupportsRead[bytes], /) -> Any: ... def loads(bytes: ReadableBuffer, /) -> Any: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/math.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/math.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/math.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/math.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mimetypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mimetypes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mimetypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mimetypes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mmap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mmap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/modulefinder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/modulefinder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/modulefinder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/modulefinder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/schema.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/schema.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/schema.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/schema.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/text.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/text.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/text.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/text.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msvcrt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msvcrt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msvcrt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msvcrt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi new file mode 100644 index 0000000000000..c4af295d23161 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi @@ -0,0 +1,45 @@ +import sys +from _typeshed import FileDescriptorLike, Unused +from collections.abc import Sequence +from struct import Struct +from typing import Any, Final + +__all__ = ["ensure_running", "get_inherited_fds", "connect_to_new_process", "set_forkserver_preload"] + +MAXFDS_TO_SEND: Final = 256 +SIGNED_STRUCT: Final[Struct] + +class ForkServer: + def set_forkserver_preload(self, modules_names: list[str]) -> None: ... + def get_inherited_fds(self) -> list[int] | None: ... + def connect_to_new_process(self, fds: Sequence[int]) -> tuple[int, int]: ... + def ensure_running(self) -> None: ... + +if sys.version_info >= (3, 14): + def main( + listener_fd: int | None, + alive_r: FileDescriptorLike, + preload: Sequence[str], + main_path: str | None = None, + sys_path: list[str] | None = None, + *, + authkey_r: int | None = None, + ) -> None: ... + +else: + def main( + listener_fd: int | None, + alive_r: FileDescriptorLike, + preload: Sequence[str], + main_path: str | None = None, + sys_path: Unused = None, + ) -> None: ... + +def read_signed(fd: int) -> Any: ... +def write_signed(fd: int, n: int) -> None: ... + +_forkserver: ForkServer +ensure_running = _forkserver.ensure_running +get_inherited_fds = _forkserver.get_inherited_fds +connect_to_new_process = _forkserver.connect_to_new_process +set_forkserver_preload = _forkserver.set_forkserver_preload diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi similarity index 80% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi index 50e4f1c1fe662..b0ccac41b9253 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi @@ -2,7 +2,17 @@ import queue import sys import threading from _typeshed import SupportsKeysAndGetItem, SupportsRichComparison, SupportsRichComparisonT -from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence +from collections.abc import ( + Callable, + Iterable, + Iterator, + Mapping, + MutableMapping, + MutableSequence, + MutableSet, + Sequence, + Set as AbstractSet, +) from types import GenericAlias, TracebackType from typing import Any, AnyStr, ClassVar, Generic, SupportsIndex, TypeVar, overload from typing_extensions import Self, TypeAlias @@ -18,6 +28,7 @@ __all__ = ["BaseManager", "SyncManager", "BaseProxy", "Token", "SharedMemoryMana _T = TypeVar("_T") _KT = TypeVar("_KT") _VT = TypeVar("_VT") +_S = TypeVar("_S") class Namespace: def __init__(self, **kwds: Any) -> None: ... @@ -111,6 +122,51 @@ else: def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override] def values(self) -> list[_VT]: ... # type: ignore[override] +if sys.version_info >= (3, 14): + class _BaseSetProxy(BaseProxy, MutableSet[_T]): + __builtins__: ClassVar[dict[str, Any]] + # Copied from builtins.set + def add(self, element: _T, /) -> None: ... + def copy(self) -> set[_T]: ... + def clear(self) -> None: ... + def difference(self, *s: Iterable[Any]) -> set[_T]: ... + def difference_update(self, *s: Iterable[Any]) -> None: ... + def discard(self, element: _T, /) -> None: ... + def intersection(self, *s: Iterable[Any]) -> set[_T]: ... + def intersection_update(self, *s: Iterable[Any]) -> None: ... + def isdisjoint(self, s: Iterable[Any], /) -> bool: ... + def issubset(self, s: Iterable[Any], /) -> bool: ... + def issuperset(self, s: Iterable[Any], /) -> bool: ... + def pop(self) -> _T: ... + def remove(self, element: _T, /) -> None: ... + def symmetric_difference(self, s: Iterable[_T], /) -> set[_T]: ... + def symmetric_difference_update(self, s: Iterable[_T], /) -> None: ... + def union(self, *s: Iterable[_S]) -> set[_T | _S]: ... + def update(self, *s: Iterable[_T]) -> None: ... + def __len__(self) -> int: ... + def __contains__(self, o: object, /) -> bool: ... + def __iter__(self) -> Iterator[_T]: ... + def __and__(self, value: AbstractSet[object], /) -> set[_T]: ... + def __iand__(self, value: AbstractSet[object], /) -> Self: ... + def __or__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... + def __ior__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] + def __sub__(self, value: AbstractSet[_T | None], /) -> set[_T]: ... + def __isub__(self, value: AbstractSet[object], /) -> Self: ... + def __xor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... + def __ixor__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] + def __le__(self, value: AbstractSet[object], /) -> bool: ... + def __lt__(self, value: AbstractSet[object], /) -> bool: ... + def __ge__(self, value: AbstractSet[object], /) -> bool: ... + def __gt__(self, value: AbstractSet[object], /) -> bool: ... + def __eq__(self, value: object, /) -> bool: ... + def __rand__(self, value: AbstractSet[object], /) -> set[_T]: ... + def __ror__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] + def __rsub__(self, value: AbstractSet[_T], /) -> set[_T]: ... + def __rxor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + + class SetProxy(_BaseSetProxy[_T]): ... + class BaseListProxy(BaseProxy, MutableSequence[_T]): __builtins__: ClassVar[dict[str, Any]] def __len__(self) -> int: ... @@ -273,6 +329,11 @@ class SyncManager(BaseManager): def list(self, sequence: Sequence[_T], /) -> ListProxy[_T]: ... @overload def list(self) -> ListProxy[Any]: ... + if sys.version_info >= (3, 14): + @overload + def set(self, iterable: Iterable[_T], /) -> SetProxy[_T]: ... + @overload + def set(self) -> SetProxy[Any]: ... class RemoteError(Exception): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi similarity index 89% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi index 4fcbfd99a8d0d..5e53b055cc797 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi @@ -18,6 +18,9 @@ if sys.platform != "win32": def duplicate_for_child(self, fd: int) -> int: ... def poll(self, flag: int = 1) -> int | None: ... def wait(self, timeout: float | None = None) -> int | None: ... + if sys.version_info >= (3, 14): + def interrupt(self) -> None: ... + def terminate(self) -> None: ... def kill(self) -> None: ... def close(self) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi index 942e92ce530ec..490ae195c20e2 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi @@ -43,7 +43,8 @@ if sys.platform == "win32": def detach(self) -> int: ... else: - ACKNOWLEDGE: Final[bool] + if sys.version_info < (3, 14): + ACKNOWLEDGE: Final[bool] def recvfds(sock: socket, size: int) -> list[int]: ... def send_handle(conn: HasFileno, handle: int, destination_pid: Unused) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi new file mode 100644 index 0000000000000..ecb4a7ddec7d2 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi @@ -0,0 +1,108 @@ +import sys +import threading +from _typeshed import ConvertibleToInt, Incomplete, Unused +from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence +from logging import Logger, _Level as _LoggingLevel +from typing import Any, Final, Generic, TypeVar, overload + +__all__ = [ + "sub_debug", + "debug", + "info", + "sub_warning", + "get_logger", + "log_to_stderr", + "get_temp_dir", + "register_after_fork", + "is_exiting", + "Finalize", + "ForkAwareThreadLock", + "ForkAwareLocal", + "close_all_fds_except", + "SUBDEBUG", + "SUBWARNING", +] + +if sys.version_info >= (3, 14): + __all__ += ["warn"] + +_T = TypeVar("_T") +_R_co = TypeVar("_R_co", default=Any, covariant=True) + +NOTSET: Final = 0 +SUBDEBUG: Final = 5 +DEBUG: Final = 10 +INFO: Final = 20 +SUBWARNING: Final = 25 +if sys.version_info >= (3, 14): + WARNING: Final = 30 + +LOGGER_NAME: Final[str] +DEFAULT_LOGGING_FORMAT: Final[str] + +def sub_debug(msg: object, *args: object) -> None: ... +def debug(msg: object, *args: object) -> None: ... +def info(msg: object, *args: object) -> None: ... + +if sys.version_info >= (3, 14): + def warn(msg: object, *args: object) -> None: ... + +def sub_warning(msg: object, *args: object) -> None: ... +def get_logger() -> Logger: ... +def log_to_stderr(level: _LoggingLevel | None = None) -> Logger: ... +def is_abstract_socket_namespace(address: str | bytes | None) -> bool: ... + +abstract_sockets_supported: bool + +def get_temp_dir() -> str: ... +def register_after_fork(obj: _T, func: Callable[[_T], object]) -> None: ... + +class Finalize(Generic[_R_co]): + # "args" and "kwargs" are passed as arguments to "callback". + @overload + def __init__( + self, + obj: None, + callback: Callable[..., _R_co], + *, + args: Sequence[Any] = (), + kwargs: Mapping[str, Any] | None = None, + exitpriority: int, + ) -> None: ... + @overload + def __init__( + self, obj: None, callback: Callable[..., _R_co], args: Sequence[Any], kwargs: Mapping[str, Any] | None, exitpriority: int + ) -> None: ... + @overload + def __init__( + self, + obj: Any, + callback: Callable[..., _R_co], + args: Sequence[Any] = (), + kwargs: Mapping[str, Any] | None = None, + exitpriority: int | None = None, + ) -> None: ... + def __call__( + self, + wr: Unused = None, + _finalizer_registry: MutableMapping[Incomplete, Incomplete] = {}, + sub_debug: Callable[..., object] = ..., + getpid: Callable[[], int] = ..., + ) -> _R_co: ... + def cancel(self) -> None: ... + def still_active(self) -> bool: ... + +def is_exiting() -> bool: ... + +class ForkAwareThreadLock: + acquire: Callable[[bool, float], bool] + release: Callable[[], None] + def __enter__(self) -> bool: ... + def __exit__(self, *args: Unused) -> None: ... + +class ForkAwareLocal(threading.local): ... + +MAXFD: Final[int] + +def close_all_fds_except(fds: Iterable[int]) -> None: ... +def spawnv_passfds(path: bytes, args: Sequence[ConvertibleToInt], passfds: Sequence[int]) -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/netrc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/netrc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/netrc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/netrc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nis.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nis.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nis.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nis.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nntplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nntplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nntplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nntplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ntpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.pyi similarity index 87% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ntpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ntpath.pyi index ebe305ef708c2..074df075b9727 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ntpath.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.pyi @@ -1,6 +1,8 @@ import sys from _typeshed import BytesPath, StrOrBytesPath, StrPath from genericpath import ( + ALLOW_MISSING as ALLOW_MISSING, + _AllowMissingType, commonprefix as commonprefix, exists as exists, getatime as getatime, @@ -89,6 +91,7 @@ __all__ = [ "sameopenfile", "samestat", "commonpath", + "ALLOW_MISSING", ] if sys.version_info >= (3, 12): __all__ += ["isjunction", "splitroot"] @@ -108,16 +111,10 @@ def join(path: StrPath, /, *paths: StrPath) -> str: ... def join(path: BytesPath, /, *paths: BytesPath) -> bytes: ... if sys.platform == "win32": - if sys.version_info >= (3, 10): - @overload - def realpath(path: PathLike[AnyStr], *, strict: bool = False) -> AnyStr: ... - @overload - def realpath(path: AnyStr, *, strict: bool = False) -> AnyStr: ... - else: - @overload - def realpath(path: PathLike[AnyStr]) -> AnyStr: ... - @overload - def realpath(path: AnyStr) -> AnyStr: ... + @overload + def realpath(path: PathLike[AnyStr], *, strict: bool | _AllowMissingType = False) -> AnyStr: ... + @overload + def realpath(path: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... else: realpath = abspath diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi new file mode 100644 index 0000000000000..c38a359469d2d --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi @@ -0,0 +1,12 @@ +import sys +from typing_extensions import deprecated + +if sys.version_info >= (3, 14): + @deprecated("nturl2path module was deprecated since Python 3.14") + def url2pathname(url: str) -> str: ... + @deprecated("nturl2path module was deprecated since Python 3.14") + def pathname2url(p: str) -> str: ... + +else: + def url2pathname(url: str) -> str: ... + def pathname2url(p: str) -> str: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/numbers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/numbers.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/numbers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/numbers.pyi index f2bca4e58bc58..02d469ce0ee54 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/numbers.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/numbers.pyi @@ -7,7 +7,6 @@ # (since type checkers don't see `complex` as a subtype of `numbers.Complex`, # nor `float` as a subtype of `numbers.Real`, etc.) -from _typeshed import Incomplete from abc import ABCMeta, abstractmethod from typing import ClassVar, Literal, Protocol, overload @@ -166,7 +165,7 @@ class Integral(Rational, _IntegralLike): def __int__(self) -> int: ... def __index__(self) -> int: ... @abstractmethod - def __pow__(self, exponent, modulus: Incomplete | None = None) -> _IntegralLike: ... + def __pow__(self, exponent, modulus=None) -> _IntegralLike: ... @abstractmethod def __lshift__(self, other) -> _IntegralLike: ... @abstractmethod diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/opcode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/opcode.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/operator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/operator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/operator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/operator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/optparse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/optparse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi new file mode 100644 index 0000000000000..dd4479f9030a2 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi @@ -0,0 +1,1664 @@ +import sys +from _typeshed import ( + AnyStr_co, + BytesPath, + FileDescriptor, + FileDescriptorLike, + FileDescriptorOrPath, + GenericPath, + OpenBinaryMode, + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, + OpenTextMode, + ReadableBuffer, + StrOrBytesPath, + StrPath, + SupportsLenAndGetItem, + Unused, + WriteableBuffer, + structseq, +) +from abc import ABC, abstractmethod +from builtins import OSError +from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping, Sequence +from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper +from subprocess import Popen +from types import GenericAlias, TracebackType +from typing import ( + IO, + Any, + AnyStr, + BinaryIO, + Final, + Generic, + Literal, + NoReturn, + Protocol, + TypeVar, + final, + overload, + runtime_checkable, +) +from typing_extensions import Self, TypeAlias, Unpack, deprecated + +from . import path as _path + +__all__ = [ + "F_OK", + "O_APPEND", + "O_CREAT", + "O_EXCL", + "O_RDONLY", + "O_RDWR", + "O_TRUNC", + "O_WRONLY", + "P_NOWAIT", + "P_NOWAITO", + "P_WAIT", + "R_OK", + "SEEK_CUR", + "SEEK_END", + "SEEK_SET", + "TMP_MAX", + "W_OK", + "X_OK", + "DirEntry", + "_exit", + "abort", + "access", + "altsep", + "chdir", + "chmod", + "close", + "closerange", + "cpu_count", + "curdir", + "defpath", + "device_encoding", + "devnull", + "dup", + "dup2", + "environ", + "error", + "execl", + "execle", + "execlp", + "execlpe", + "execv", + "execve", + "execvp", + "execvpe", + "extsep", + "fdopen", + "fsdecode", + "fsencode", + "fspath", + "fstat", + "fsync", + "ftruncate", + "get_exec_path", + "get_inheritable", + "get_terminal_size", + "getcwd", + "getcwdb", + "getenv", + "getlogin", + "getpid", + "getppid", + "isatty", + "kill", + "linesep", + "link", + "listdir", + "lseek", + "lstat", + "makedirs", + "mkdir", + "name", + "open", + "pardir", + "path", + "pathsep", + "pipe", + "popen", + "putenv", + "read", + "readlink", + "remove", + "removedirs", + "rename", + "renames", + "replace", + "rmdir", + "scandir", + "sep", + "set_inheritable", + "spawnl", + "spawnle", + "spawnv", + "spawnve", + "stat", + "stat_result", + "statvfs_result", + "strerror", + "supports_bytes_environ", + "symlink", + "system", + "terminal_size", + "times", + "times_result", + "truncate", + "umask", + "uname_result", + "unlink", + "unsetenv", + "urandom", + "utime", + "waitpid", + "waitstatus_to_exitcode", + "walk", + "write", +] +if sys.version_info >= (3, 14): + __all__ += ["readinto"] +if sys.platform == "darwin" and sys.version_info >= (3, 12): + __all__ += ["PRIO_DARWIN_BG", "PRIO_DARWIN_NONUI", "PRIO_DARWIN_PROCESS", "PRIO_DARWIN_THREAD"] +if sys.platform == "darwin" and sys.version_info >= (3, 10): + __all__ += ["O_EVTONLY", "O_NOFOLLOW_ANY", "O_SYMLINK"] +if sys.platform == "linux": + __all__ += [ + "GRND_NONBLOCK", + "GRND_RANDOM", + "MFD_ALLOW_SEALING", + "MFD_CLOEXEC", + "MFD_HUGETLB", + "MFD_HUGE_16GB", + "MFD_HUGE_16MB", + "MFD_HUGE_1GB", + "MFD_HUGE_1MB", + "MFD_HUGE_256MB", + "MFD_HUGE_2GB", + "MFD_HUGE_2MB", + "MFD_HUGE_32MB", + "MFD_HUGE_512KB", + "MFD_HUGE_512MB", + "MFD_HUGE_64KB", + "MFD_HUGE_8MB", + "MFD_HUGE_MASK", + "MFD_HUGE_SHIFT", + "O_DIRECT", + "O_LARGEFILE", + "O_NOATIME", + "O_PATH", + "O_RSYNC", + "O_TMPFILE", + "P_PIDFD", + "RTLD_DEEPBIND", + "SCHED_BATCH", + "SCHED_IDLE", + "SCHED_RESET_ON_FORK", + "XATTR_CREATE", + "XATTR_REPLACE", + "XATTR_SIZE_MAX", + "copy_file_range", + "getrandom", + "getxattr", + "listxattr", + "memfd_create", + "pidfd_open", + "removexattr", + "setxattr", + ] +if sys.platform == "linux" and sys.version_info >= (3, 14): + __all__ += ["SCHED_DEADLINE", "SCHED_NORMAL"] +if sys.platform == "linux" and sys.version_info >= (3, 13): + __all__ += [ + "POSIX_SPAWN_CLOSEFROM", + "TFD_CLOEXEC", + "TFD_NONBLOCK", + "TFD_TIMER_ABSTIME", + "TFD_TIMER_CANCEL_ON_SET", + "timerfd_create", + "timerfd_gettime", + "timerfd_gettime_ns", + "timerfd_settime", + "timerfd_settime_ns", + ] +if sys.platform == "linux" and sys.version_info >= (3, 12): + __all__ += [ + "CLONE_FILES", + "CLONE_FS", + "CLONE_NEWCGROUP", + "CLONE_NEWIPC", + "CLONE_NEWNET", + "CLONE_NEWNS", + "CLONE_NEWPID", + "CLONE_NEWTIME", + "CLONE_NEWUSER", + "CLONE_NEWUTS", + "CLONE_SIGHAND", + "CLONE_SYSVSEM", + "CLONE_THREAD", + "CLONE_VM", + "setns", + "unshare", + "PIDFD_NONBLOCK", + ] +if sys.platform == "linux" and sys.version_info >= (3, 10): + __all__ += [ + "EFD_CLOEXEC", + "EFD_NONBLOCK", + "EFD_SEMAPHORE", + "RWF_APPEND", + "SPLICE_F_MORE", + "SPLICE_F_MOVE", + "SPLICE_F_NONBLOCK", + "eventfd", + "eventfd_read", + "eventfd_write", + "splice", + ] +if sys.platform == "win32": + __all__ += [ + "O_BINARY", + "O_NOINHERIT", + "O_RANDOM", + "O_SEQUENTIAL", + "O_SHORT_LIVED", + "O_TEMPORARY", + "O_TEXT", + "P_DETACH", + "P_OVERLAY", + "get_handle_inheritable", + "set_handle_inheritable", + "startfile", + ] +if sys.platform == "win32" and sys.version_info >= (3, 12): + __all__ += ["listdrives", "listmounts", "listvolumes"] +if sys.platform != "win32": + __all__ += [ + "CLD_CONTINUED", + "CLD_DUMPED", + "CLD_EXITED", + "CLD_KILLED", + "CLD_STOPPED", + "CLD_TRAPPED", + "EX_CANTCREAT", + "EX_CONFIG", + "EX_DATAERR", + "EX_IOERR", + "EX_NOHOST", + "EX_NOINPUT", + "EX_NOPERM", + "EX_NOUSER", + "EX_OSERR", + "EX_OSFILE", + "EX_PROTOCOL", + "EX_SOFTWARE", + "EX_TEMPFAIL", + "EX_UNAVAILABLE", + "EX_USAGE", + "F_LOCK", + "F_TEST", + "F_TLOCK", + "F_ULOCK", + "NGROUPS_MAX", + "O_ACCMODE", + "O_ASYNC", + "O_CLOEXEC", + "O_DIRECTORY", + "O_DSYNC", + "O_NDELAY", + "O_NOCTTY", + "O_NOFOLLOW", + "O_NONBLOCK", + "O_SYNC", + "POSIX_SPAWN_CLOSE", + "POSIX_SPAWN_DUP2", + "POSIX_SPAWN_OPEN", + "PRIO_PGRP", + "PRIO_PROCESS", + "PRIO_USER", + "P_ALL", + "P_PGID", + "P_PID", + "RTLD_GLOBAL", + "RTLD_LAZY", + "RTLD_LOCAL", + "RTLD_NODELETE", + "RTLD_NOLOAD", + "RTLD_NOW", + "SCHED_FIFO", + "SCHED_OTHER", + "SCHED_RR", + "SEEK_DATA", + "SEEK_HOLE", + "ST_NOSUID", + "ST_RDONLY", + "WCONTINUED", + "WCOREDUMP", + "WEXITED", + "WEXITSTATUS", + "WIFCONTINUED", + "WIFEXITED", + "WIFSIGNALED", + "WIFSTOPPED", + "WNOHANG", + "WNOWAIT", + "WSTOPPED", + "WSTOPSIG", + "WTERMSIG", + "WUNTRACED", + "chown", + "chroot", + "confstr", + "confstr_names", + "ctermid", + "environb", + "fchdir", + "fchown", + "fork", + "forkpty", + "fpathconf", + "fstatvfs", + "fwalk", + "getegid", + "getenvb", + "geteuid", + "getgid", + "getgrouplist", + "getgroups", + "getloadavg", + "getpgid", + "getpgrp", + "getpriority", + "getsid", + "getuid", + "initgroups", + "killpg", + "lchown", + "lockf", + "major", + "makedev", + "minor", + "mkfifo", + "mknod", + "nice", + "openpty", + "pathconf", + "pathconf_names", + "posix_spawn", + "posix_spawnp", + "pread", + "preadv", + "pwrite", + "pwritev", + "readv", + "register_at_fork", + "sched_get_priority_max", + "sched_get_priority_min", + "sched_yield", + "sendfile", + "setegid", + "seteuid", + "setgid", + "setgroups", + "setpgid", + "setpgrp", + "setpriority", + "setregid", + "setreuid", + "setsid", + "setuid", + "spawnlp", + "spawnlpe", + "spawnvp", + "spawnvpe", + "statvfs", + "sync", + "sysconf", + "sysconf_names", + "tcgetpgrp", + "tcsetpgrp", + "ttyname", + "uname", + "wait", + "wait3", + "wait4", + "writev", + ] +if sys.platform != "win32" and sys.version_info >= (3, 13): + __all__ += ["grantpt", "posix_openpt", "ptsname", "unlockpt"] +if sys.platform != "win32" and sys.version_info >= (3, 11): + __all__ += ["login_tty"] +if sys.platform != "win32" and sys.version_info >= (3, 10): + __all__ += ["O_FSYNC"] +if sys.platform != "darwin" and sys.platform != "win32": + __all__ += [ + "POSIX_FADV_DONTNEED", + "POSIX_FADV_NOREUSE", + "POSIX_FADV_NORMAL", + "POSIX_FADV_RANDOM", + "POSIX_FADV_SEQUENTIAL", + "POSIX_FADV_WILLNEED", + "RWF_DSYNC", + "RWF_HIPRI", + "RWF_NOWAIT", + "RWF_SYNC", + "ST_APPEND", + "ST_MANDLOCK", + "ST_NOATIME", + "ST_NODEV", + "ST_NODIRATIME", + "ST_NOEXEC", + "ST_RELATIME", + "ST_SYNCHRONOUS", + "ST_WRITE", + "fdatasync", + "getresgid", + "getresuid", + "pipe2", + "posix_fadvise", + "posix_fallocate", + "sched_getaffinity", + "sched_getparam", + "sched_getscheduler", + "sched_param", + "sched_rr_get_interval", + "sched_setaffinity", + "sched_setparam", + "sched_setscheduler", + "setresgid", + "setresuid", + ] +if sys.platform != "linux" and sys.platform != "win32": + __all__ += ["O_EXLOCK", "O_SHLOCK", "chflags", "lchflags"] +if sys.platform != "linux" and sys.platform != "win32" and sys.version_info >= (3, 13): + __all__ += ["O_EXEC", "O_SEARCH"] +if sys.platform != "darwin" or sys.version_info >= (3, 13): + if sys.platform != "win32": + __all__ += ["waitid", "waitid_result"] +if sys.platform != "win32" or sys.version_info >= (3, 13): + __all__ += ["fchmod"] + if sys.platform != "linux": + __all__ += ["lchmod"] +if sys.platform != "win32" or sys.version_info >= (3, 12): + __all__ += ["get_blocking", "set_blocking"] +if sys.platform != "win32" or sys.version_info >= (3, 11): + __all__ += ["EX_OK"] + +# This unnecessary alias is to work around various errors +path = _path + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") + +# ----- os variables ----- + +error = OSError + +supports_bytes_environ: bool + +supports_dir_fd: set[Callable[..., Any]] +supports_fd: set[Callable[..., Any]] +supports_effective_ids: set[Callable[..., Any]] +supports_follow_symlinks: set[Callable[..., Any]] + +if sys.platform != "win32": + # Unix only + PRIO_PROCESS: int + PRIO_PGRP: int + PRIO_USER: int + + F_LOCK: int + F_TLOCK: int + F_ULOCK: int + F_TEST: int + + if sys.platform != "darwin": + POSIX_FADV_NORMAL: int + POSIX_FADV_SEQUENTIAL: int + POSIX_FADV_RANDOM: int + POSIX_FADV_NOREUSE: int + POSIX_FADV_WILLNEED: int + POSIX_FADV_DONTNEED: int + + if sys.platform != "linux" and sys.platform != "darwin": + # In the os-module docs, these are marked as being available + # on "Unix, not Emscripten, not WASI." + # However, in the source code, a comment indicates they're "FreeBSD constants". + # sys.platform could have one of many values on a FreeBSD Python build, + # so the sys-module docs recommend doing `if sys.platform.startswith('freebsd')` + # to detect FreeBSD builds. Unfortunately that would be too dynamic + # for type checkers, however. + SF_NODISKIO: int + SF_MNOWAIT: int + SF_SYNC: int + + if sys.version_info >= (3, 11): + SF_NOCACHE: int + + if sys.platform == "linux": + XATTR_SIZE_MAX: int + XATTR_CREATE: int + XATTR_REPLACE: int + + P_PID: int + P_PGID: int + P_ALL: int + + if sys.platform == "linux": + P_PIDFD: int + + WEXITED: int + WSTOPPED: int + WNOWAIT: int + + CLD_EXITED: int + CLD_DUMPED: int + CLD_TRAPPED: int + CLD_CONTINUED: int + CLD_KILLED: int + CLD_STOPPED: int + + SCHED_OTHER: int + SCHED_FIFO: int + SCHED_RR: int + if sys.platform != "darwin" and sys.platform != "linux": + SCHED_SPORADIC: int + +if sys.platform == "linux": + SCHED_BATCH: int + SCHED_IDLE: int + SCHED_RESET_ON_FORK: int + +if sys.version_info >= (3, 14) and sys.platform == "linux": + SCHED_DEADLINE: int + SCHED_NORMAL: int + +if sys.platform != "win32": + RTLD_LAZY: int + RTLD_NOW: int + RTLD_GLOBAL: int + RTLD_LOCAL: int + RTLD_NODELETE: int + RTLD_NOLOAD: int + +if sys.platform == "linux": + RTLD_DEEPBIND: int + GRND_NONBLOCK: int + GRND_RANDOM: int + +if sys.platform == "darwin" and sys.version_info >= (3, 12): + PRIO_DARWIN_BG: int + PRIO_DARWIN_NONUI: int + PRIO_DARWIN_PROCESS: int + PRIO_DARWIN_THREAD: int + +SEEK_SET: int +SEEK_CUR: int +SEEK_END: int +if sys.platform != "win32": + SEEK_DATA: int + SEEK_HOLE: int + +O_RDONLY: int +O_WRONLY: int +O_RDWR: int +O_APPEND: int +O_CREAT: int +O_EXCL: int +O_TRUNC: int +if sys.platform == "win32": + O_BINARY: int + O_NOINHERIT: int + O_SHORT_LIVED: int + O_TEMPORARY: int + O_RANDOM: int + O_SEQUENTIAL: int + O_TEXT: int + +if sys.platform != "win32": + O_DSYNC: int + O_SYNC: int + O_NDELAY: int + O_NONBLOCK: int + O_NOCTTY: int + O_CLOEXEC: int + O_ASYNC: int # Gnu extension if in C library + O_DIRECTORY: int # Gnu extension if in C library + O_NOFOLLOW: int # Gnu extension if in C library + O_ACCMODE: int # TODO: when does this exist? + +if sys.platform == "linux": + O_RSYNC: int + O_DIRECT: int # Gnu extension if in C library + O_NOATIME: int # Gnu extension if in C library + O_PATH: int # Gnu extension if in C library + O_TMPFILE: int # Gnu extension if in C library + O_LARGEFILE: int # Gnu extension if in C library + +if sys.platform != "linux" and sys.platform != "win32": + O_SHLOCK: int + O_EXLOCK: int + +if sys.platform == "darwin" and sys.version_info >= (3, 10): + O_EVTONLY: int + O_NOFOLLOW_ANY: int + O_SYMLINK: int + +if sys.platform != "win32" and sys.version_info >= (3, 10): + O_FSYNC: int + +if sys.platform != "linux" and sys.platform != "win32" and sys.version_info >= (3, 13): + O_EXEC: int + O_SEARCH: int + +if sys.platform != "win32" and sys.platform != "darwin": + # posix, but apparently missing on macos + ST_APPEND: int + ST_MANDLOCK: int + ST_NOATIME: int + ST_NODEV: int + ST_NODIRATIME: int + ST_NOEXEC: int + ST_RELATIME: int + ST_SYNCHRONOUS: int + ST_WRITE: int + +if sys.platform != "win32": + NGROUPS_MAX: int + ST_NOSUID: int + ST_RDONLY: int + +curdir: str +pardir: str +sep: str +if sys.platform == "win32": + altsep: str +else: + altsep: str | None +extsep: str +pathsep: str +defpath: str +linesep: Literal["\n", "\r\n"] +devnull: str +name: str + +F_OK: int +R_OK: int +W_OK: int +X_OK: int + +_EnvironCodeFunc: TypeAlias = Callable[[AnyStr], AnyStr] + +class _Environ(MutableMapping[AnyStr, AnyStr], Generic[AnyStr]): + encodekey: _EnvironCodeFunc[AnyStr] + decodekey: _EnvironCodeFunc[AnyStr] + encodevalue: _EnvironCodeFunc[AnyStr] + decodevalue: _EnvironCodeFunc[AnyStr] + def __init__( + self, + data: MutableMapping[AnyStr, AnyStr], + encodekey: _EnvironCodeFunc[AnyStr], + decodekey: _EnvironCodeFunc[AnyStr], + encodevalue: _EnvironCodeFunc[AnyStr], + decodevalue: _EnvironCodeFunc[AnyStr], + ) -> None: ... + def setdefault(self, key: AnyStr, value: AnyStr) -> AnyStr: ... + def copy(self) -> dict[AnyStr, AnyStr]: ... + def __delitem__(self, key: AnyStr) -> None: ... + def __getitem__(self, key: AnyStr) -> AnyStr: ... + def __setitem__(self, key: AnyStr, value: AnyStr) -> None: ... + def __iter__(self) -> Iterator[AnyStr]: ... + def __len__(self) -> int: ... + def __or__(self, other: Mapping[_T1, _T2]) -> dict[AnyStr | _T1, AnyStr | _T2]: ... + def __ror__(self, other: Mapping[_T1, _T2]) -> dict[AnyStr | _T1, AnyStr | _T2]: ... + # We use @overload instead of a Union for reasons similar to those given for + # overloading MutableMapping.update in stdlib/typing.pyi + # The type: ignore is needed due to incompatible __or__/__ior__ signatures + @overload # type: ignore[misc] + def __ior__(self, other: Mapping[AnyStr, AnyStr]) -> Self: ... + @overload + def __ior__(self, other: Iterable[tuple[AnyStr, AnyStr]]) -> Self: ... + +environ: _Environ[str] +if sys.platform != "win32": + environb: _Environ[bytes] + +if sys.version_info >= (3, 11) or sys.platform != "win32": + EX_OK: int + +if sys.platform != "win32": + confstr_names: dict[str, int] + pathconf_names: dict[str, int] + sysconf_names: dict[str, int] + + EX_USAGE: int + EX_DATAERR: int + EX_NOINPUT: int + EX_NOUSER: int + EX_NOHOST: int + EX_UNAVAILABLE: int + EX_SOFTWARE: int + EX_OSERR: int + EX_OSFILE: int + EX_CANTCREAT: int + EX_IOERR: int + EX_TEMPFAIL: int + EX_PROTOCOL: int + EX_NOPERM: int + EX_CONFIG: int + +# Exists on some Unix platforms, e.g. Solaris. +if sys.platform != "win32" and sys.platform != "darwin" and sys.platform != "linux": + EX_NOTFOUND: int + +P_NOWAIT: int +P_NOWAITO: int +P_WAIT: int +if sys.platform == "win32": + P_DETACH: int + P_OVERLAY: int + +# wait()/waitpid() options +if sys.platform != "win32": + WNOHANG: int # Unix only + WCONTINUED: int # some Unix systems + WUNTRACED: int # Unix only + +TMP_MAX: int # Undocumented, but used by tempfile + +# ----- os classes (structures) ----- +@final +class stat_result(structseq[float], tuple[int, int, int, int, int, int, int, float, float, float]): + # The constructor of this class takes an iterable of variable length (though it must be at least 10). + # + # However, this class behaves like a tuple of 10 elements, + # no matter how long the iterable supplied to the constructor is. + # https://github.com/python/typeshed/pull/6560#discussion_r767162532 + # + # The 10 elements always present are st_mode, st_ino, st_dev, st_nlink, + # st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime. + # + # More items may be added at the end by some implementations. + if sys.version_info >= (3, 10): + __match_args__: Final = ("st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size") + + @property + def st_mode(self) -> int: ... # protection bits, + @property + def st_ino(self) -> int: ... # inode number, + @property + def st_dev(self) -> int: ... # device, + @property + def st_nlink(self) -> int: ... # number of hard links, + @property + def st_uid(self) -> int: ... # user id of owner, + @property + def st_gid(self) -> int: ... # group id of owner, + @property + def st_size(self) -> int: ... # size of file, in bytes, + @property + def st_atime(self) -> float: ... # time of most recent access, + @property + def st_mtime(self) -> float: ... # time of most recent content modification, + # platform dependent (time of most recent metadata change on Unix, or the time of creation on Windows) + if sys.version_info >= (3, 12) and sys.platform == "win32": + @property + @deprecated( + """\ +Use st_birthtime instead to retrieve the file creation time. \ +In the future, this property will contain the last metadata change time.""" + ) + def st_ctime(self) -> float: ... + else: + @property + def st_ctime(self) -> float: ... + + @property + def st_atime_ns(self) -> int: ... # time of most recent access, in nanoseconds + @property + def st_mtime_ns(self) -> int: ... # time of most recent content modification in nanoseconds + # platform dependent (time of most recent metadata change on Unix, or the time of creation on Windows) in nanoseconds + @property + def st_ctime_ns(self) -> int: ... + if sys.platform == "win32": + @property + def st_file_attributes(self) -> int: ... + @property + def st_reparse_tag(self) -> int: ... + if sys.version_info >= (3, 12): + @property + def st_birthtime(self) -> float: ... # time of file creation in seconds + @property + def st_birthtime_ns(self) -> int: ... # time of file creation in nanoseconds + else: + @property + def st_blocks(self) -> int: ... # number of blocks allocated for file + @property + def st_blksize(self) -> int: ... # filesystem blocksize + @property + def st_rdev(self) -> int: ... # type of device if an inode device + if sys.platform != "linux": + # These properties are available on MacOS, but not Ubuntu. + # On other Unix systems (such as FreeBSD), the following attributes may be + # available (but may be only filled out if root tries to use them): + @property + def st_gen(self) -> int: ... # file generation number + @property + def st_birthtime(self) -> float: ... # time of file creation in seconds + if sys.platform == "darwin": + @property + def st_flags(self) -> int: ... # user defined flags for file + # Attributes documented as sometimes appearing, but deliberately omitted from the stub: `st_creator`, `st_rsize`, `st_type`. + # See https://github.com/python/typeshed/pull/6560#issuecomment-991253327 + +# mypy and pyright object to this being both ABC and Protocol. +# At runtime it inherits from ABC and is not a Protocol, but it will be +# on the allowlist for use as a Protocol starting in 3.14. +@runtime_checkable +class PathLike(ABC, Protocol[AnyStr_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + @abstractmethod + def __fspath__(self) -> AnyStr_co: ... + +@overload +def listdir(path: StrPath | None = None) -> list[str]: ... +@overload +def listdir(path: BytesPath) -> list[bytes]: ... +@overload +def listdir(path: int) -> list[str]: ... +@final +class DirEntry(Generic[AnyStr]): + # This is what the scandir iterator yields + # The constructor is hidden + + @property + def name(self) -> AnyStr: ... + @property + def path(self) -> AnyStr: ... + def inode(self) -> int: ... + def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... + def is_file(self, *, follow_symlinks: bool = True) -> bool: ... + def is_symlink(self) -> bool: ... + def stat(self, *, follow_symlinks: bool = True) -> stat_result: ... + def __fspath__(self) -> AnyStr: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + if sys.version_info >= (3, 12): + def is_junction(self) -> bool: ... + +@final +class statvfs_result(structseq[int], tuple[int, int, int, int, int, int, int, int, int, int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ( + "f_bsize", + "f_frsize", + "f_blocks", + "f_bfree", + "f_bavail", + "f_files", + "f_ffree", + "f_favail", + "f_flag", + "f_namemax", + ) + + @property + def f_bsize(self) -> int: ... + @property + def f_frsize(self) -> int: ... + @property + def f_blocks(self) -> int: ... + @property + def f_bfree(self) -> int: ... + @property + def f_bavail(self) -> int: ... + @property + def f_files(self) -> int: ... + @property + def f_ffree(self) -> int: ... + @property + def f_favail(self) -> int: ... + @property + def f_flag(self) -> int: ... + @property + def f_namemax(self) -> int: ... + @property + def f_fsid(self) -> int: ... + +# ----- os function stubs ----- +def fsencode(filename: StrOrBytesPath) -> bytes: ... +def fsdecode(filename: StrOrBytesPath) -> str: ... +@overload +def fspath(path: str) -> str: ... +@overload +def fspath(path: bytes) -> bytes: ... +@overload +def fspath(path: PathLike[AnyStr]) -> AnyStr: ... +def get_exec_path(env: Mapping[str, str] | None = None) -> list[str]: ... +def getlogin() -> str: ... +def getpid() -> int: ... +def getppid() -> int: ... +def strerror(code: int, /) -> str: ... +def umask(mask: int, /) -> int: ... +@final +class uname_result(structseq[str], tuple[str, str, str, str, str]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("sysname", "nodename", "release", "version", "machine") + + @property + def sysname(self) -> str: ... + @property + def nodename(self) -> str: ... + @property + def release(self) -> str: ... + @property + def version(self) -> str: ... + @property + def machine(self) -> str: ... + +if sys.platform != "win32": + def ctermid() -> str: ... + def getegid() -> int: ... + def geteuid() -> int: ... + def getgid() -> int: ... + def getgrouplist(user: str, group: int, /) -> list[int]: ... + def getgroups() -> list[int]: ... # Unix only, behaves differently on Mac + def initgroups(username: str, gid: int, /) -> None: ... + def getpgid(pid: int) -> int: ... + def getpgrp() -> int: ... + def getpriority(which: int, who: int) -> int: ... + def setpriority(which: int, who: int, priority: int) -> None: ... + if sys.platform != "darwin": + def getresuid() -> tuple[int, int, int]: ... + def getresgid() -> tuple[int, int, int]: ... + + def getuid() -> int: ... + def setegid(egid: int, /) -> None: ... + def seteuid(euid: int, /) -> None: ... + def setgid(gid: int, /) -> None: ... + def setgroups(groups: Sequence[int], /) -> None: ... + def setpgrp() -> None: ... + def setpgid(pid: int, pgrp: int, /) -> None: ... + def setregid(rgid: int, egid: int, /) -> None: ... + if sys.platform != "darwin": + def setresgid(rgid: int, egid: int, sgid: int, /) -> None: ... + def setresuid(ruid: int, euid: int, suid: int, /) -> None: ... + + def setreuid(ruid: int, euid: int, /) -> None: ... + def getsid(pid: int, /) -> int: ... + def setsid() -> None: ... + def setuid(uid: int, /) -> None: ... + def uname() -> uname_result: ... + +@overload +def getenv(key: str) -> str | None: ... +@overload +def getenv(key: str, default: _T) -> str | _T: ... + +if sys.platform != "win32": + @overload + def getenvb(key: bytes) -> bytes | None: ... + @overload + def getenvb(key: bytes, default: _T) -> bytes | _T: ... + def putenv(name: StrOrBytesPath, value: StrOrBytesPath, /) -> None: ... + def unsetenv(name: StrOrBytesPath, /) -> None: ... + +else: + def putenv(name: str, value: str, /) -> None: ... + def unsetenv(name: str, /) -> None: ... + +_Opener: TypeAlias = Callable[[str, int], int] + +@overload +def fdopen( + fd: int, + mode: OpenTextMode = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: _Opener | None = ..., +) -> TextIOWrapper: ... +@overload +def fdopen( + fd: int, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = ..., + opener: _Opener | None = ..., +) -> FileIO: ... +@overload +def fdopen( + fd: int, + mode: OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = ..., + opener: _Opener | None = ..., +) -> BufferedRandom: ... +@overload +def fdopen( + fd: int, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = ..., + opener: _Opener | None = ..., +) -> BufferedWriter: ... +@overload +def fdopen( + fd: int, + mode: OpenBinaryModeReading, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = ..., + opener: _Opener | None = ..., +) -> BufferedReader: ... +@overload +def fdopen( + fd: int, + mode: OpenBinaryMode, + buffering: int = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + closefd: bool = ..., + opener: _Opener | None = ..., +) -> BinaryIO: ... +@overload +def fdopen( + fd: int, + mode: str, + buffering: int = -1, + encoding: str | None = None, + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: _Opener | None = ..., +) -> IO[Any]: ... +def close(fd: int) -> None: ... +def closerange(fd_low: int, fd_high: int, /) -> None: ... +def device_encoding(fd: int) -> str | None: ... +def dup(fd: int, /) -> int: ... +def dup2(fd: int, fd2: int, inheritable: bool = True) -> int: ... +def fstat(fd: int) -> stat_result: ... +def ftruncate(fd: int, length: int, /) -> None: ... +def fsync(fd: FileDescriptorLike) -> None: ... +def isatty(fd: int, /) -> bool: ... + +if sys.platform != "win32" and sys.version_info >= (3, 11): + def login_tty(fd: int, /) -> None: ... + +if sys.version_info >= (3, 11): + def lseek(fd: int, position: int, whence: int, /) -> int: ... + +else: + def lseek(fd: int, position: int, how: int, /) -> int: ... + +def open(path: StrOrBytesPath, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: ... +def pipe() -> tuple[int, int]: ... +def read(fd: int, length: int, /) -> bytes: ... + +if sys.version_info >= (3, 12) or sys.platform != "win32": + def get_blocking(fd: int, /) -> bool: ... + def set_blocking(fd: int, blocking: bool, /) -> None: ... + +if sys.platform != "win32": + def fchown(fd: int, uid: int, gid: int) -> None: ... + def fpathconf(fd: int, name: str | int, /) -> int: ... + def fstatvfs(fd: int, /) -> statvfs_result: ... + def lockf(fd: int, command: int, length: int, /) -> None: ... + def openpty() -> tuple[int, int]: ... # some flavors of Unix + if sys.platform != "darwin": + def fdatasync(fd: FileDescriptorLike) -> None: ... + def pipe2(flags: int, /) -> tuple[int, int]: ... # some flavors of Unix + def posix_fallocate(fd: int, offset: int, length: int, /) -> None: ... + def posix_fadvise(fd: int, offset: int, length: int, advice: int, /) -> None: ... + + def pread(fd: int, length: int, offset: int, /) -> bytes: ... + def pwrite(fd: int, buffer: ReadableBuffer, offset: int, /) -> int: ... + # In CI, stubtest sometimes reports that these are available on MacOS, sometimes not + def preadv(fd: int, buffers: SupportsLenAndGetItem[WriteableBuffer], offset: int, flags: int = 0, /) -> int: ... + def pwritev(fd: int, buffers: SupportsLenAndGetItem[ReadableBuffer], offset: int, flags: int = 0, /) -> int: ... + if sys.platform != "darwin": + if sys.version_info >= (3, 10): + RWF_APPEND: int # docs say available on 3.7+, stubtest says otherwise + RWF_DSYNC: int + RWF_SYNC: int + RWF_HIPRI: int + RWF_NOWAIT: int + + if sys.platform == "linux": + def sendfile(out_fd: FileDescriptor, in_fd: FileDescriptor, offset: int | None, count: int) -> int: ... + else: + def sendfile( + out_fd: FileDescriptor, + in_fd: FileDescriptor, + offset: int, + count: int, + headers: Sequence[ReadableBuffer] = ..., + trailers: Sequence[ReadableBuffer] = ..., + flags: int = 0, + ) -> int: ... # FreeBSD and Mac OS X only + + def readv(fd: int, buffers: SupportsLenAndGetItem[WriteableBuffer], /) -> int: ... + def writev(fd: int, buffers: SupportsLenAndGetItem[ReadableBuffer], /) -> int: ... + +if sys.version_info >= (3, 14): + def readinto(fd: int, buffer: ReadableBuffer, /) -> int: ... + +@final +class terminal_size(structseq[int], tuple[int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("columns", "lines") + + @property + def columns(self) -> int: ... + @property + def lines(self) -> int: ... + +def get_terminal_size(fd: int = ..., /) -> terminal_size: ... +def get_inheritable(fd: int, /) -> bool: ... +def set_inheritable(fd: int, inheritable: bool, /) -> None: ... + +if sys.platform == "win32": + def get_handle_inheritable(handle: int, /) -> bool: ... + def set_handle_inheritable(handle: int, inheritable: bool, /) -> None: ... + +if sys.platform != "win32": + # Unix only + def tcgetpgrp(fd: int, /) -> int: ... + def tcsetpgrp(fd: int, pgid: int, /) -> None: ... + def ttyname(fd: int, /) -> str: ... + +def write(fd: int, data: ReadableBuffer, /) -> int: ... +def access( + path: FileDescriptorOrPath, mode: int, *, dir_fd: int | None = None, effective_ids: bool = False, follow_symlinks: bool = True +) -> bool: ... +def chdir(path: FileDescriptorOrPath) -> None: ... + +if sys.platform != "win32": + def fchdir(fd: FileDescriptorLike) -> None: ... + +def getcwd() -> str: ... +def getcwdb() -> bytes: ... +def chmod(path: FileDescriptorOrPath, mode: int, *, dir_fd: int | None = None, follow_symlinks: bool = ...) -> None: ... + +if sys.platform != "win32" and sys.platform != "linux": + def chflags(path: StrOrBytesPath, flags: int, follow_symlinks: bool = True) -> None: ... # some flavors of Unix + def lchflags(path: StrOrBytesPath, flags: int) -> None: ... + +if sys.platform != "win32": + def chroot(path: StrOrBytesPath) -> None: ... + def chown( + path: FileDescriptorOrPath, uid: int, gid: int, *, dir_fd: int | None = None, follow_symlinks: bool = True + ) -> None: ... + def lchown(path: StrOrBytesPath, uid: int, gid: int) -> None: ... + +def link( + src: StrOrBytesPath, + dst: StrOrBytesPath, + *, + src_dir_fd: int | None = None, + dst_dir_fd: int | None = None, + follow_symlinks: bool = True, +) -> None: ... +def lstat(path: StrOrBytesPath, *, dir_fd: int | None = None) -> stat_result: ... +def mkdir(path: StrOrBytesPath, mode: int = 0o777, *, dir_fd: int | None = None) -> None: ... + +if sys.platform != "win32": + def mkfifo(path: StrOrBytesPath, mode: int = 0o666, *, dir_fd: int | None = None) -> None: ... # Unix only + +def makedirs(name: StrOrBytesPath, mode: int = 0o777, exist_ok: bool = False) -> None: ... + +if sys.platform != "win32": + def mknod(path: StrOrBytesPath, mode: int = 0o600, device: int = 0, *, dir_fd: int | None = None) -> None: ... + def major(device: int, /) -> int: ... + def minor(device: int, /) -> int: ... + def makedev(major: int, minor: int, /) -> int: ... + def pathconf(path: FileDescriptorOrPath, name: str | int) -> int: ... # Unix only + +def readlink(path: GenericPath[AnyStr], *, dir_fd: int | None = None) -> AnyStr: ... +def remove(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... +def removedirs(name: StrOrBytesPath) -> None: ... +def rename(src: StrOrBytesPath, dst: StrOrBytesPath, *, src_dir_fd: int | None = None, dst_dir_fd: int | None = None) -> None: ... +def renames(old: StrOrBytesPath, new: StrOrBytesPath) -> None: ... +def replace( + src: StrOrBytesPath, dst: StrOrBytesPath, *, src_dir_fd: int | None = None, dst_dir_fd: int | None = None +) -> None: ... +def rmdir(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... +@final +class _ScandirIterator(Generic[AnyStr]): + def __del__(self) -> None: ... + def __iter__(self) -> Self: ... + def __next__(self) -> DirEntry[AnyStr]: ... + def __enter__(self) -> Self: ... + def __exit__(self, *args: Unused) -> None: ... + def close(self) -> None: ... + +@overload +def scandir(path: None = None) -> _ScandirIterator[str]: ... +@overload +def scandir(path: int) -> _ScandirIterator[str]: ... +@overload +def scandir(path: GenericPath[AnyStr]) -> _ScandirIterator[AnyStr]: ... +def stat(path: FileDescriptorOrPath, *, dir_fd: int | None = None, follow_symlinks: bool = True) -> stat_result: ... + +if sys.platform != "win32": + def statvfs(path: FileDescriptorOrPath) -> statvfs_result: ... # Unix only + +def symlink( + src: StrOrBytesPath, dst: StrOrBytesPath, target_is_directory: bool = False, *, dir_fd: int | None = None +) -> None: ... + +if sys.platform != "win32": + def sync() -> None: ... # Unix only + +def truncate(path: FileDescriptorOrPath, length: int) -> None: ... # Unix only up to version 3.4 +def unlink(path: StrOrBytesPath, *, dir_fd: int | None = None) -> None: ... +def utime( + path: FileDescriptorOrPath, + times: tuple[int, int] | tuple[float, float] | None = None, + *, + ns: tuple[int, int] = ..., + dir_fd: int | None = None, + follow_symlinks: bool = True, +) -> None: ... + +_OnError: TypeAlias = Callable[[OSError], object] + +def walk( + top: GenericPath[AnyStr], topdown: bool = True, onerror: _OnError | None = None, followlinks: bool = False +) -> Iterator[tuple[AnyStr, list[AnyStr], list[AnyStr]]]: ... + +if sys.platform != "win32": + @overload + def fwalk( + top: StrPath = ".", + topdown: bool = True, + onerror: _OnError | None = None, + *, + follow_symlinks: bool = False, + dir_fd: int | None = None, + ) -> Iterator[tuple[str, list[str], list[str], int]]: ... + @overload + def fwalk( + top: BytesPath, + topdown: bool = True, + onerror: _OnError | None = None, + *, + follow_symlinks: bool = False, + dir_fd: int | None = None, + ) -> Iterator[tuple[bytes, list[bytes], list[bytes], int]]: ... + if sys.platform == "linux": + def getxattr(path: FileDescriptorOrPath, attribute: StrOrBytesPath, *, follow_symlinks: bool = True) -> bytes: ... + def listxattr(path: FileDescriptorOrPath | None = None, *, follow_symlinks: bool = True) -> list[str]: ... + def removexattr(path: FileDescriptorOrPath, attribute: StrOrBytesPath, *, follow_symlinks: bool = True) -> None: ... + def setxattr( + path: FileDescriptorOrPath, + attribute: StrOrBytesPath, + value: ReadableBuffer, + flags: int = 0, + *, + follow_symlinks: bool = True, + ) -> None: ... + +def abort() -> NoReturn: ... + +# These are defined as execl(file, *args) but the first *arg is mandatory. +def execl(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> NoReturn: ... +def execlp(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> NoReturn: ... + +# These are: execle(file, *args, env) but env is pulled from the last element of the args. +def execle( + file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]] +) -> NoReturn: ... +def execlpe( + file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]] +) -> NoReturn: ... + +# The docs say `args: tuple or list of strings` +# The implementation enforces tuple or list so we can't use Sequence. +# Not separating out PathLike[str] and PathLike[bytes] here because it doesn't make much difference +# in practice, and doing so would explode the number of combinations in this already long union. +# All these combinations are necessary due to list being invariant. +_ExecVArgs: TypeAlias = ( + tuple[StrOrBytesPath, ...] + | list[bytes] + | list[str] + | list[PathLike[Any]] + | list[bytes | str] + | list[bytes | PathLike[Any]] + | list[str | PathLike[Any]] + | list[bytes | str | PathLike[Any]] +) +# Depending on the OS, the keys and values are passed either to +# PyUnicode_FSDecoder (which accepts str | ReadableBuffer) or to +# PyUnicode_FSConverter (which accepts StrOrBytesPath). For simplicity, +# we limit to str | bytes. +_ExecEnv: TypeAlias = Mapping[bytes, bytes | str] | Mapping[str, bytes | str] + +def execv(path: StrOrBytesPath, argv: _ExecVArgs, /) -> NoReturn: ... +def execve(path: FileDescriptorOrPath, argv: _ExecVArgs, env: _ExecEnv) -> NoReturn: ... +def execvp(file: StrOrBytesPath, args: _ExecVArgs) -> NoReturn: ... +def execvpe(file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> NoReturn: ... +def _exit(status: int) -> NoReturn: ... +def kill(pid: int, signal: int, /) -> None: ... + +if sys.platform != "win32": + # Unix only + def fork() -> int: ... + def forkpty() -> tuple[int, int]: ... # some flavors of Unix + def killpg(pgid: int, signal: int, /) -> None: ... + def nice(increment: int, /) -> int: ... + if sys.platform != "darwin" and sys.platform != "linux": + def plock(op: int, /) -> None: ... + +class _wrap_close: + def __init__(self, stream: TextIOWrapper, proc: Popen[str]) -> None: ... + def close(self) -> int | None: ... + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None + ) -> None: ... + def __iter__(self) -> Iterator[str]: ... + # Methods below here don't exist directly on the _wrap_close object, but + # are copied from the wrapped TextIOWrapper object via __getattr__. + # The full set of TextIOWrapper methods are technically available this way, + # but undocumented. Only a subset are currently included here. + def read(self, size: int | None = -1, /) -> str: ... + def readable(self) -> bool: ... + def readline(self, size: int = -1, /) -> str: ... + def readlines(self, hint: int = -1, /) -> list[str]: ... + def writable(self) -> bool: ... + def write(self, s: str, /) -> int: ... + def writelines(self, lines: Iterable[str], /) -> None: ... + +def popen(cmd: str, mode: str = "r", buffering: int = -1) -> _wrap_close: ... +def spawnl(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... +def spawnle(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise sig + +if sys.platform != "win32": + def spawnv(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... + def spawnve(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... + +else: + def spawnv(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, /) -> int: ... + def spawnve(mode: int, path: StrOrBytesPath, argv: _ExecVArgs, env: _ExecEnv, /) -> int: ... + +def system(command: StrOrBytesPath) -> int: ... +@final +class times_result(structseq[float], tuple[float, float, float, float, float]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("user", "system", "children_user", "children_system", "elapsed") + + @property + def user(self) -> float: ... + @property + def system(self) -> float: ... + @property + def children_user(self) -> float: ... + @property + def children_system(self) -> float: ... + @property + def elapsed(self) -> float: ... + +def times() -> times_result: ... +def waitpid(pid: int, options: int, /) -> tuple[int, int]: ... + +if sys.platform == "win32": + if sys.version_info >= (3, 10): + def startfile( + filepath: StrOrBytesPath, + operation: str = ..., + arguments: str = "", + cwd: StrOrBytesPath | None = None, + show_cmd: int = 1, + ) -> None: ... + else: + def startfile(filepath: StrOrBytesPath, operation: str = ...) -> None: ... + +else: + def spawnlp(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: StrOrBytesPath) -> int: ... + def spawnlpe(mode: int, file: StrOrBytesPath, arg0: StrOrBytesPath, *args: Any) -> int: ... # Imprecise signature + def spawnvp(mode: int, file: StrOrBytesPath, args: _ExecVArgs) -> int: ... + def spawnvpe(mode: int, file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> int: ... + def wait() -> tuple[int, int]: ... # Unix only + # Added to MacOS in 3.13 + if sys.platform != "darwin" or sys.version_info >= (3, 13): + @final + class waitid_result(structseq[int], tuple[int, int, int, int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("si_pid", "si_uid", "si_signo", "si_status", "si_code") + + @property + def si_pid(self) -> int: ... + @property + def si_uid(self) -> int: ... + @property + def si_signo(self) -> int: ... + @property + def si_status(self) -> int: ... + @property + def si_code(self) -> int: ... + + def waitid(idtype: int, ident: int, options: int, /) -> waitid_result | None: ... + + from resource import struct_rusage + + def wait3(options: int) -> tuple[int, int, struct_rusage]: ... + def wait4(pid: int, options: int) -> tuple[int, int, struct_rusage]: ... + def WCOREDUMP(status: int, /) -> bool: ... + def WIFCONTINUED(status: int) -> bool: ... + def WIFSTOPPED(status: int) -> bool: ... + def WIFSIGNALED(status: int) -> bool: ... + def WIFEXITED(status: int) -> bool: ... + def WEXITSTATUS(status: int) -> int: ... + def WSTOPSIG(status: int) -> int: ... + def WTERMSIG(status: int) -> int: ... + def posix_spawn( + path: StrOrBytesPath, + argv: _ExecVArgs, + env: _ExecEnv, + /, + *, + file_actions: Sequence[tuple[Any, ...]] | None = ..., + setpgroup: int | None = ..., + resetids: bool = ..., + setsid: bool = ..., + setsigmask: Iterable[int] = ..., + setsigdef: Iterable[int] = ..., + scheduler: tuple[Any, sched_param] | None = ..., + ) -> int: ... + def posix_spawnp( + path: StrOrBytesPath, + argv: _ExecVArgs, + env: _ExecEnv, + /, + *, + file_actions: Sequence[tuple[Any, ...]] | None = ..., + setpgroup: int | None = ..., + resetids: bool = ..., + setsid: bool = ..., + setsigmask: Iterable[int] = ..., + setsigdef: Iterable[int] = ..., + scheduler: tuple[Any, sched_param] | None = ..., + ) -> int: ... + POSIX_SPAWN_OPEN: int + POSIX_SPAWN_CLOSE: int + POSIX_SPAWN_DUP2: int + +if sys.platform != "win32": + @final + class sched_param(structseq[int], tuple[int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("sched_priority",) + + def __new__(cls, sched_priority: int) -> Self: ... + @property + def sched_priority(self) -> int: ... + + def sched_get_priority_min(policy: int) -> int: ... # some flavors of Unix + def sched_get_priority_max(policy: int) -> int: ... # some flavors of Unix + def sched_yield() -> None: ... # some flavors of Unix + if sys.platform != "darwin": + def sched_setscheduler(pid: int, policy: int, param: sched_param, /) -> None: ... # some flavors of Unix + def sched_getscheduler(pid: int, /) -> int: ... # some flavors of Unix + def sched_rr_get_interval(pid: int, /) -> float: ... # some flavors of Unix + def sched_setparam(pid: int, param: sched_param, /) -> None: ... # some flavors of Unix + def sched_getparam(pid: int, /) -> sched_param: ... # some flavors of Unix + def sched_setaffinity(pid: int, mask: Iterable[int], /) -> None: ... # some flavors of Unix + def sched_getaffinity(pid: int, /) -> set[int]: ... # some flavors of Unix + +def cpu_count() -> int | None: ... + +if sys.version_info >= (3, 13): + # Documented to return `int | None`, but falls back to `len(sched_getaffinity(0))` when + # available. See https://github.com/python/cpython/blob/417c130/Lib/os.py#L1175-L1186. + if sys.platform != "win32" and sys.platform != "darwin": + def process_cpu_count() -> int: ... + else: + def process_cpu_count() -> int | None: ... + +if sys.platform != "win32": + # Unix only + def confstr(name: str | int, /) -> str | None: ... + def getloadavg() -> tuple[float, float, float]: ... + def sysconf(name: str | int, /) -> int: ... + +if sys.platform == "linux": + def getrandom(size: int, flags: int = 0) -> bytes: ... + +def urandom(size: int, /) -> bytes: ... + +if sys.platform != "win32": + def register_at_fork( + *, + before: Callable[..., Any] | None = ..., + after_in_parent: Callable[..., Any] | None = ..., + after_in_child: Callable[..., Any] | None = ..., + ) -> None: ... + +if sys.platform == "win32": + class _AddedDllDirectory: + path: str | None + def __init__(self, path: str | None, cookie: _T, remove_dll_directory: Callable[[_T], object]) -> None: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, *args: Unused) -> None: ... + + def add_dll_directory(path: str) -> _AddedDllDirectory: ... + +if sys.platform == "linux": + MFD_CLOEXEC: int + MFD_ALLOW_SEALING: int + MFD_HUGETLB: int + MFD_HUGE_SHIFT: int + MFD_HUGE_MASK: int + MFD_HUGE_64KB: int + MFD_HUGE_512KB: int + MFD_HUGE_1MB: int + MFD_HUGE_2MB: int + MFD_HUGE_8MB: int + MFD_HUGE_16MB: int + MFD_HUGE_32MB: int + MFD_HUGE_256MB: int + MFD_HUGE_512MB: int + MFD_HUGE_1GB: int + MFD_HUGE_2GB: int + MFD_HUGE_16GB: int + def memfd_create(name: str, flags: int = ...) -> int: ... + def copy_file_range(src: int, dst: int, count: int, offset_src: int | None = ..., offset_dst: int | None = ...) -> int: ... + +def waitstatus_to_exitcode(status: int) -> int: ... + +if sys.platform == "linux": + def pidfd_open(pid: int, flags: int = ...) -> int: ... + +if sys.version_info >= (3, 12) and sys.platform == "linux": + PIDFD_NONBLOCK: Final = 2048 + +if sys.version_info >= (3, 12) and sys.platform == "win32": + def listdrives() -> list[str]: ... + def listmounts(volume: str) -> list[str]: ... + def listvolumes() -> list[str]: ... + +if sys.version_info >= (3, 10) and sys.platform == "linux": + EFD_CLOEXEC: int + EFD_NONBLOCK: int + EFD_SEMAPHORE: int + SPLICE_F_MORE: int + SPLICE_F_MOVE: int + SPLICE_F_NONBLOCK: int + def eventfd(initval: int, flags: int = 524288) -> FileDescriptor: ... + def eventfd_read(fd: FileDescriptor) -> int: ... + def eventfd_write(fd: FileDescriptor, value: int) -> None: ... + def splice( + src: FileDescriptor, + dst: FileDescriptor, + count: int, + offset_src: int | None = ..., + offset_dst: int | None = ..., + flags: int = 0, + ) -> int: ... + +if sys.version_info >= (3, 12) and sys.platform == "linux": + CLONE_FILES: int + CLONE_FS: int + CLONE_NEWCGROUP: int # Linux 4.6+ + CLONE_NEWIPC: int # Linux 2.6.19+ + CLONE_NEWNET: int # Linux 2.6.24+ + CLONE_NEWNS: int + CLONE_NEWPID: int # Linux 3.8+ + CLONE_NEWTIME: int # Linux 5.6+ + CLONE_NEWUSER: int # Linux 3.8+ + CLONE_NEWUTS: int # Linux 2.6.19+ + CLONE_SIGHAND: int + CLONE_SYSVSEM: int # Linux 2.6.26+ + CLONE_THREAD: int + CLONE_VM: int + def unshare(flags: int) -> None: ... + def setns(fd: FileDescriptorLike, nstype: int = 0) -> None: ... + +if sys.version_info >= (3, 13) and sys.platform != "win32": + def posix_openpt(oflag: int, /) -> int: ... + def grantpt(fd: FileDescriptorLike, /) -> None: ... + def unlockpt(fd: FileDescriptorLike, /) -> None: ... + def ptsname(fd: FileDescriptorLike, /) -> str: ... + +if sys.version_info >= (3, 13) and sys.platform == "linux": + TFD_TIMER_ABSTIME: Final = 1 + TFD_TIMER_CANCEL_ON_SET: Final = 2 + TFD_NONBLOCK: Final[int] + TFD_CLOEXEC: Final[int] + POSIX_SPAWN_CLOSEFROM: Final[int] + + def timerfd_create(clockid: int, /, *, flags: int = 0) -> int: ... + def timerfd_settime( + fd: FileDescriptor, /, *, flags: int = 0, initial: float = 0.0, interval: float = 0.0 + ) -> tuple[float, float]: ... + def timerfd_settime_ns(fd: FileDescriptor, /, *, flags: int = 0, initial: int = 0, interval: int = 0) -> tuple[int, int]: ... + def timerfd_gettime(fd: FileDescriptor, /) -> tuple[float, float]: ... + def timerfd_gettime_ns(fd: FileDescriptor, /) -> tuple[int, int]: ... + +if sys.version_info >= (3, 13) or sys.platform != "win32": + # Added to Windows in 3.13. + def fchmod(fd: int, mode: int) -> None: ... + +if sys.platform != "linux": + if sys.version_info >= (3, 13) or sys.platform != "win32": + # Added to Windows in 3.13. + def lchmod(path: StrOrBytesPath, mode: int) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/os/path.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/os/path.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/os/path.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/os/path.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/parser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/parser.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi new file mode 100644 index 0000000000000..b84fc69313a15 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi @@ -0,0 +1,307 @@ +import sys +import types +from _typeshed import ( + OpenBinaryMode, + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, + OpenTextMode, + ReadableBuffer, + StrOrBytesPath, + StrPath, + Unused, +) +from collections.abc import Callable, Generator, Iterator, Sequence +from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper +from os import PathLike, stat_result +from types import GenericAlias, TracebackType +from typing import IO, Any, BinaryIO, ClassVar, Literal, TypeVar, overload +from typing_extensions import Never, Self, deprecated + +_PathT = TypeVar("_PathT", bound=PurePath) + +__all__ = ["PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath"] + +if sys.version_info >= (3, 14): + from pathlib.types import PathInfo + +if sys.version_info >= (3, 13): + __all__ += ["UnsupportedOperation"] + +class PurePath(PathLike[str]): + if sys.version_info >= (3, 13): + parser: ClassVar[types.ModuleType] + def full_match(self, pattern: StrPath, *, case_sensitive: bool | None = None) -> bool: ... + + @property + def parts(self) -> tuple[str, ...]: ... + @property + def drive(self) -> str: ... + @property + def root(self) -> str: ... + @property + def anchor(self) -> str: ... + @property + def name(self) -> str: ... + @property + def suffix(self) -> str: ... + @property + def suffixes(self) -> list[str]: ... + @property + def stem(self) -> str: ... + if sys.version_info >= (3, 12): + def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... + def __init__(self, *args: StrPath) -> None: ... # pyright: ignore[reportInconsistentConstructor] + else: + def __new__(cls, *args: StrPath) -> Self: ... + + def __hash__(self) -> int: ... + def __fspath__(self) -> str: ... + def __lt__(self, other: PurePath) -> bool: ... + def __le__(self, other: PurePath) -> bool: ... + def __gt__(self, other: PurePath) -> bool: ... + def __ge__(self, other: PurePath) -> bool: ... + def __truediv__(self, key: StrPath) -> Self: ... + def __rtruediv__(self, key: StrPath) -> Self: ... + def __bytes__(self) -> bytes: ... + def as_posix(self) -> str: ... + def as_uri(self) -> str: ... + def is_absolute(self) -> bool: ... + def is_reserved(self) -> bool: ... + if sys.version_info >= (3, 14): + def is_relative_to(self, other: StrPath) -> bool: ... + elif sys.version_info >= (3, 12): + def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool: ... + else: + def is_relative_to(self, *other: StrPath) -> bool: ... + + if sys.version_info >= (3, 12): + def match(self, path_pattern: str, *, case_sensitive: bool | None = None) -> bool: ... + else: + def match(self, path_pattern: str) -> bool: ... + + if sys.version_info >= (3, 14): + def relative_to(self, other: StrPath, *, walk_up: bool = False) -> Self: ... + elif sys.version_info >= (3, 12): + def relative_to(self, other: StrPath, /, *_deprecated: StrPath, walk_up: bool = False) -> Self: ... + else: + def relative_to(self, *other: StrPath) -> Self: ... + + def with_name(self, name: str) -> Self: ... + def with_stem(self, stem: str) -> Self: ... + def with_suffix(self, suffix: str) -> Self: ... + def joinpath(self, *other: StrPath) -> Self: ... + @property + def parents(self) -> Sequence[Self]: ... + @property + def parent(self) -> Self: ... + if sys.version_info < (3, 11): + def __class_getitem__(cls, type: Any) -> GenericAlias: ... + + if sys.version_info >= (3, 12): + def with_segments(self, *args: StrPath) -> Self: ... + +class PurePosixPath(PurePath): ... +class PureWindowsPath(PurePath): ... + +class Path(PurePath): + if sys.version_info >= (3, 12): + def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... # pyright: ignore[reportInconsistentConstructor] + else: + def __new__(cls, *args: StrPath, **kwargs: Unused) -> Self: ... + + @classmethod + def cwd(cls) -> Self: ... + if sys.version_info >= (3, 10): + def stat(self, *, follow_symlinks: bool = True) -> stat_result: ... + def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: ... + else: + def stat(self) -> stat_result: ... + def chmod(self, mode: int) -> None: ... + + if sys.version_info >= (3, 13): + @classmethod + def from_uri(cls, uri: str) -> Self: ... + def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... + def is_file(self, *, follow_symlinks: bool = True) -> bool: ... + def read_text(self, encoding: str | None = None, errors: str | None = None, newline: str | None = None) -> str: ... + else: + def __enter__(self) -> Self: ... + def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ... + def is_dir(self) -> bool: ... + def is_file(self) -> bool: ... + def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: ... + + if sys.version_info >= (3, 13): + def glob(self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False) -> Iterator[Self]: ... + def rglob( + self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False + ) -> Iterator[Self]: ... + elif sys.version_info >= (3, 12): + def glob(self, pattern: str, *, case_sensitive: bool | None = None) -> Generator[Self, None, None]: ... + def rglob(self, pattern: str, *, case_sensitive: bool | None = None) -> Generator[Self, None, None]: ... + else: + def glob(self, pattern: str) -> Generator[Self, None, None]: ... + def rglob(self, pattern: str) -> Generator[Self, None, None]: ... + + if sys.version_info >= (3, 12): + def exists(self, *, follow_symlinks: bool = True) -> bool: ... + else: + def exists(self) -> bool: ... + + def is_symlink(self) -> bool: ... + def is_socket(self) -> bool: ... + def is_fifo(self) -> bool: ... + def is_block_device(self) -> bool: ... + def is_char_device(self) -> bool: ... + if sys.version_info >= (3, 12): + def is_junction(self) -> bool: ... + + def iterdir(self) -> Generator[Self, None, None]: ... + def lchmod(self, mode: int) -> None: ... + def lstat(self) -> stat_result: ... + def mkdir(self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False) -> None: ... + + if sys.version_info >= (3, 14): + + @property + def info(self) -> PathInfo: ... + @overload + def move_into(self, target_dir: _PathT) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def move_into(self, target_dir: StrPath) -> Self: ... # type: ignore[overload-overlap] + @overload + def move(self, target: _PathT) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def move(self, target: StrPath) -> Self: ... # type: ignore[overload-overlap] + @overload + def copy_into(self, target_dir: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def copy_into(self, target_dir: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> Self: ... # type: ignore[overload-overlap] + @overload + def copy(self, target: _PathT, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> _PathT: ... # type: ignore[overload-overlap] + @overload + def copy(self, target: StrPath, *, follow_symlinks: bool = True, preserve_metadata: bool = False) -> Self: ... # type: ignore[overload-overlap] + + # Adapted from builtins.open + # Text mode: always returns a TextIOWrapper + # The Traversable .open in stdlib/importlib/abc.pyi should be kept in sync with this. + @overload + def open( + self, + mode: OpenTextMode = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> TextIOWrapper: ... + # Unbuffered binary mode: returns a FileIO + @overload + def open( + self, mode: OpenBinaryMode, buffering: Literal[0], encoding: None = None, errors: None = None, newline: None = None + ) -> FileIO: ... + # Buffering is on: return BufferedRandom, BufferedReader, or BufferedWriter + @overload + def open( + self, + mode: OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + ) -> BufferedRandom: ... + @overload + def open( + self, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + ) -> BufferedWriter: ... + @overload + def open( + self, + mode: OpenBinaryModeReading, + buffering: Literal[-1, 1] = -1, + encoding: None = None, + errors: None = None, + newline: None = None, + ) -> BufferedReader: ... + # Buffering cannot be determined: fall back to BinaryIO + @overload + def open( + self, mode: OpenBinaryMode, buffering: int = -1, encoding: None = None, errors: None = None, newline: None = None + ) -> BinaryIO: ... + # Fallback if mode is not specified + @overload + def open( + self, mode: str, buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None + ) -> IO[Any]: ... + + # These methods do "exist" on Windows on <3.13, but they always raise NotImplementedError. + if sys.platform == "win32": + if sys.version_info < (3, 13): + def owner(self: Never) -> str: ... # type: ignore[misc] + def group(self: Never) -> str: ... # type: ignore[misc] + else: + if sys.version_info >= (3, 13): + def owner(self, *, follow_symlinks: bool = True) -> str: ... + def group(self, *, follow_symlinks: bool = True) -> str: ... + else: + def owner(self) -> str: ... + def group(self) -> str: ... + + # This method does "exist" on Windows on <3.12, but always raises NotImplementedError + # On py312+, it works properly on Windows, as with all other platforms + if sys.platform == "win32" and sys.version_info < (3, 12): + def is_mount(self: Never) -> bool: ... # type: ignore[misc] + else: + def is_mount(self) -> bool: ... + + def readlink(self) -> Self: ... + + if sys.version_info >= (3, 10): + def rename(self, target: StrPath) -> Self: ... + def replace(self, target: StrPath) -> Self: ... + else: + def rename(self, target: str | PurePath) -> Self: ... + def replace(self, target: str | PurePath) -> Self: ... + + def resolve(self, strict: bool = False) -> Self: ... + def rmdir(self) -> None: ... + def symlink_to(self, target: StrOrBytesPath, target_is_directory: bool = False) -> None: ... + if sys.version_info >= (3, 10): + def hardlink_to(self, target: StrOrBytesPath) -> None: ... + + def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: ... + def unlink(self, missing_ok: bool = False) -> None: ... + @classmethod + def home(cls) -> Self: ... + def absolute(self) -> Self: ... + def expanduser(self) -> Self: ... + def read_bytes(self) -> bytes: ... + def samefile(self, other_path: StrPath) -> bool: ... + def write_bytes(self, data: ReadableBuffer) -> int: ... + if sys.version_info >= (3, 10): + def write_text( + self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None + ) -> int: ... + else: + def write_text(self, data: str, encoding: str | None = None, errors: str | None = None) -> int: ... + if sys.version_info < (3, 12): + if sys.version_info >= (3, 10): + @deprecated("Deprecated as of Python 3.10 and removed in Python 3.12. Use hardlink_to() instead.") + def link_to(self, target: StrOrBytesPath) -> None: ... + else: + def link_to(self, target: StrOrBytesPath) -> None: ... + if sys.version_info >= (3, 12): + def walk( + self, top_down: bool = ..., on_error: Callable[[OSError], object] | None = ..., follow_symlinks: bool = ... + ) -> Iterator[tuple[Self, list[str], list[str]]]: ... + +class PosixPath(Path, PurePosixPath): ... +class WindowsPath(Path, PureWindowsPath): ... + +if sys.version_info >= (3, 13): + class UnsupportedOperation(NotImplementedError): ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/types.pyi new file mode 100644 index 0000000000000..9f9a650846deb --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/types.pyi @@ -0,0 +1,8 @@ +from typing import Protocol, runtime_checkable + +@runtime_checkable +class PathInfo(Protocol): + def exists(self, *, follow_symlinks: bool = True) -> bool: ... + def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... + def is_file(self, *, follow_symlinks: bool = True) -> bool: ... + def is_symlink(self) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pdb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pdb.pyi similarity index 76% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pdb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pdb.pyi index 61e8b7176e849..ad69fcab16de0 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/pdb.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pdb.pyi @@ -1,17 +1,21 @@ import signal import sys -from bdb import Bdb +from bdb import Bdb, _Backend from cmd import Cmd from collections.abc import Callable, Iterable, Mapping, Sequence from inspect import _SourceObjectType +from linecache import _ModuleGlobals from types import CodeType, FrameType, TracebackType -from typing import IO, Any, ClassVar, Final, TypeVar -from typing_extensions import ParamSpec, Self +from typing import IO, Any, ClassVar, Final, Literal, TypeVar +from typing_extensions import ParamSpec, Self, TypeAlias __all__ = ["run", "pm", "Pdb", "runeval", "runctx", "runcall", "set_trace", "post_mortem", "help"] +if sys.version_info >= (3, 14): + __all__ += ["set_default_backend", "get_default_backend"] _T = TypeVar("_T") _P = ParamSpec("_P") +_Mode: TypeAlias = Literal["inline", "cli"] line_prefix: str # undocumented @@ -21,7 +25,16 @@ def run(statement: str, globals: dict[str, Any] | None = None, locals: Mapping[s def runeval(expression: str, globals: dict[str, Any] | None = None, locals: Mapping[str, Any] | None = None) -> Any: ... def runctx(statement: str, globals: dict[str, Any], locals: Mapping[str, Any]) -> None: ... def runcall(func: Callable[_P, _T], *args: _P.args, **kwds: _P.kwargs) -> _T | None: ... -def set_trace(*, header: str | None = None) -> None: ... + +if sys.version_info >= (3, 14): + def set_default_backend(backend: _Backend) -> None: ... + def get_default_backend() -> _Backend: ... + def set_trace(*, header: str | None = None, commands: Iterable[str] | None = None) -> None: ... + async def set_trace_async(*, header: str | None = None, commands: Iterable[str] | None = None) -> None: ... + +else: + def set_trace(*, header: str | None = None) -> None: ... + def post_mortem(t: TracebackType | None = None) -> None: ... def pm() -> None: ... @@ -47,15 +60,35 @@ class Pdb(Bdb, Cmd): curindex: int curframe: FrameType | None curframe_locals: Mapping[str, Any] - def __init__( - self, - completekey: str = "tab", - stdin: IO[str] | None = None, - stdout: IO[str] | None = None, - skip: Iterable[str] | None = None, - nosigint: bool = False, - readrc: bool = True, - ) -> None: ... + if sys.version_info >= (3, 14): + mode: _Mode | None + colorize: bool + def __init__( + self, + completekey: str = "tab", + stdin: IO[str] | None = None, + stdout: IO[str] | None = None, + skip: Iterable[str] | None = None, + nosigint: bool = False, + readrc: bool = True, + mode: _Mode | None = None, + backend: _Backend | None = None, + colorize: bool = False, + ) -> None: ... + else: + def __init__( + self, + completekey: str = "tab", + stdin: IO[str] | None = None, + stdout: IO[str] | None = None, + skip: Iterable[str] | None = None, + nosigint: bool = False, + readrc: bool = True, + ) -> None: ... + if sys.version_info >= (3, 14): + def set_trace(self, frame: FrameType | None = None, *, commands: Iterable[str] | None = None) -> None: ... + async def set_trace_async(self, frame: FrameType | None = None, *, commands: Iterable[str] | None = None) -> None: ... + def forget(self) -> None: ... def setup(self, f: FrameType | None, tb: TracebackType | None) -> None: ... if sys.version_info < (3, 11): @@ -75,14 +108,25 @@ class Pdb(Bdb, Cmd): def handle_command_def(self, line: str) -> bool: ... def defaultFile(self) -> str: ... def lineinfo(self, identifier: str) -> tuple[None, None, None] | tuple[str, str, int]: ... - def checkline(self, filename: str, lineno: int) -> int: ... + if sys.version_info >= (3, 14): + def checkline(self, filename: str, lineno: int, module_globals: _ModuleGlobals | None = None) -> int: ... + else: + def checkline(self, filename: str, lineno: int) -> int: ... + def _getval(self, arg: str) -> object: ... - def print_stack_trace(self) -> None: ... + if sys.version_info >= (3, 14): + def print_stack_trace(self, count: int | None = None) -> None: ... + else: + def print_stack_trace(self) -> None: ... + def print_stack_entry(self, frame_lineno: tuple[FrameType, int], prompt_prefix: str = "\n-> ") -> None: ... def lookupmodule(self, filename: str) -> str | None: ... if sys.version_info < (3, 11): def _runscript(self, filename: str) -> None: ... + if sys.version_info >= (3, 14): + def complete_multiline_names(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: ... + if sys.version_info >= (3, 13): def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pickle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pickletools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pickletools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pickletools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pickletools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pipes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pipes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pipes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pipes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pkgutil.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi similarity index 79% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pkgutil.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi index d60e9bad53ae0..e764d08e79f80 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/pkgutil.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi @@ -8,8 +8,6 @@ from typing_extensions import deprecated __all__ = [ "get_importer", "iter_importers", - "get_loader", - "find_loader", "walk_packages", "iter_modules", "get_data", @@ -17,6 +15,8 @@ __all__ = [ "extend_path", "ModuleInfo", ] +if sys.version_info < (3, 14): + __all__ += ["get_loader", "find_loader"] if sys.version_info < (3, 12): __all__ += ["ImpImporter", "ImpLoader"] @@ -36,11 +36,13 @@ if sys.version_info < (3, 12): class ImpLoader: def __init__(self, fullname: str, file: IO[str], filename: StrOrBytesPath, etc: tuple[str, str, int]) -> None: ... -@deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.") -def find_loader(fullname: str) -> LoaderProtocol | None: ... +if sys.version_info < (3, 14): + @deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.") + def find_loader(fullname: str) -> LoaderProtocol | None: ... + @deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.") + def get_loader(module_or_name: str) -> LoaderProtocol | None: ... + def get_importer(path_item: StrOrBytesPath) -> PathEntryFinderProtocol | None: ... -@deprecated("Use importlib.util.find_spec() instead. Will be removed in Python 3.14.") -def get_loader(module_or_name: str) -> LoaderProtocol | None: ... def iter_importers(fullname: str = "") -> Iterator[MetaPathFinderProtocol | PathEntryFinderProtocol]: ... def iter_modules(path: Iterable[StrOrBytesPath] | None = None, prefix: str = "") -> Iterator[ModuleInfo]: ... def read_code(stream: SupportsRead[bytes]) -> Any: ... # undocumented diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/platform.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/platform.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi index 19fac26134eb6..fbc73c6c91775 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/platform.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi @@ -82,3 +82,6 @@ if sys.version_info >= (3, 13): is_emulator: bool = False, ) -> AndroidVer: ... def ios_ver(system: str = "", release: str = "", model: str = "", is_simulator: bool = False) -> IOSVersionInfo: ... + +if sys.version_info >= (3, 14): + def invalidate_caches() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/plistlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/plistlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/plistlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/plistlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/poplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/poplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/posix.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/posix.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/posix.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/posix.pyi index 88f4135af2a79..6d0d76ab82176 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/posix.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/posix.pyi @@ -250,6 +250,12 @@ if sys.platform != "win32": timerfd_settime_ns as timerfd_settime_ns, ) + if sys.version_info >= (3, 14): + from os import readinto as readinto + + if sys.version_info >= (3, 14) and sys.platform == "linux": + from os import SCHED_DEADLINE as SCHED_DEADLINE, SCHED_NORMAL as SCHED_NORMAL + if sys.platform != "linux": from os import O_EXLOCK as O_EXLOCK, O_SHLOCK as O_SHLOCK, chflags as chflags, lchflags as lchflags, lchmod as lchmod diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/posixpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.pyi similarity index 92% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/posixpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/posixpath.pyi index 3313667f1781b..84e1b1e028bde 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/posixpath.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.pyi @@ -2,6 +2,8 @@ import sys from _typeshed import AnyOrLiteralStr, BytesPath, FileDescriptorOrPath, StrOrBytesPath, StrPath from collections.abc import Iterable from genericpath import ( + ALLOW_MISSING as ALLOW_MISSING, + _AllowMissingType, commonprefix as commonprefix, exists as exists, getatime as getatime, @@ -61,6 +63,7 @@ __all__ = [ "relpath", "commonpath", ] +__all__ += ["ALLOW_MISSING"] if sys.version_info >= (3, 12): __all__ += ["isjunction", "splitroot"] if sys.version_info >= (3, 13): @@ -122,19 +125,10 @@ def join(a: LiteralString, /, *paths: LiteralString) -> LiteralString: ... def join(a: StrPath, /, *paths: StrPath) -> str: ... @overload def join(a: BytesPath, /, *paths: BytesPath) -> bytes: ... - -if sys.version_info >= (3, 10): - @overload - def realpath(filename: PathLike[AnyStr], *, strict: bool = False) -> AnyStr: ... - @overload - def realpath(filename: AnyStr, *, strict: bool = False) -> AnyStr: ... - -else: - @overload - def realpath(filename: PathLike[AnyStr]) -> AnyStr: ... - @overload - def realpath(filename: AnyStr) -> AnyStr: ... - +@overload +def realpath(filename: PathLike[AnyStr], *, strict: bool | _AllowMissingType = False) -> AnyStr: ... +@overload +def realpath(filename: AnyStr, *, strict: bool | _AllowMissingType = False) -> AnyStr: ... @overload def relpath(path: LiteralString, start: LiteralString | None = None) -> LiteralString: ... @overload diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pprint.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pprint.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pprint.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pprint.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/profile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/profile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/profile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/profile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pstats.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pstats.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pstats.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pstats.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pty.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pty.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pwd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pwd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pwd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pwd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/py_compile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/py_compile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/py_compile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/py_compile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyclbr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyclbr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyclbr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyclbr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi index 144f782acad57..f14b9d1bb6998 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi @@ -6,7 +6,7 @@ from collections.abc import Callable, Container, Mapping, MutableMapping from reprlib import Repr from types import MethodType, ModuleType, TracebackType from typing import IO, Any, AnyStr, Final, NoReturn, Protocol, TypeVar -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, deprecated __all__ = ["help"] @@ -31,7 +31,14 @@ def stripid(text: str) -> str: ... def allmethods(cl: type) -> MutableMapping[str, MethodType]: ... def visiblename(name: str, all: Container[str] | None = None, obj: object = None) -> bool: ... def classify_class_attrs(object: object) -> list[tuple[str, str, type, str]]: ... -def ispackage(path: str) -> bool: ... + +if sys.version_info >= (3, 13): + @deprecated("Deprecated in Python 3.13.") + def ispackage(path: str) -> bool: ... + +else: + def ispackage(path: str) -> bool: ... + def source_synopsis(file: IO[AnyStr]) -> AnyStr | None: ... def synopsis(filename: str, cache: MutableMapping[str, tuple[int, str]] = {}) -> str | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi new file mode 100644 index 0000000000000..493ae03456044 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi @@ -0,0 +1,53 @@ +import sys +from typing import Final +from typing_extensions import LiteralString + +codes: dict[str, int] +messages: dict[int, str] + +XML_ERROR_ABORTED: Final[LiteralString] +XML_ERROR_ASYNC_ENTITY: Final[LiteralString] +XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF: Final[LiteralString] +XML_ERROR_BAD_CHAR_REF: Final[LiteralString] +XML_ERROR_BINARY_ENTITY_REF: Final[LiteralString] +XML_ERROR_CANT_CHANGE_FEATURE_ONCE_PARSING: Final[LiteralString] +XML_ERROR_DUPLICATE_ATTRIBUTE: Final[LiteralString] +XML_ERROR_ENTITY_DECLARED_IN_PE: Final[LiteralString] +XML_ERROR_EXTERNAL_ENTITY_HANDLING: Final[LiteralString] +XML_ERROR_FEATURE_REQUIRES_XML_DTD: Final[LiteralString] +XML_ERROR_FINISHED: Final[LiteralString] +XML_ERROR_INCOMPLETE_PE: Final[LiteralString] +XML_ERROR_INCORRECT_ENCODING: Final[LiteralString] +XML_ERROR_INVALID_TOKEN: Final[LiteralString] +XML_ERROR_JUNK_AFTER_DOC_ELEMENT: Final[LiteralString] +XML_ERROR_MISPLACED_XML_PI: Final[LiteralString] +XML_ERROR_NOT_STANDALONE: Final[LiteralString] +XML_ERROR_NOT_SUSPENDED: Final[LiteralString] +XML_ERROR_NO_ELEMENTS: Final[LiteralString] +XML_ERROR_NO_MEMORY: Final[LiteralString] +XML_ERROR_PARAM_ENTITY_REF: Final[LiteralString] +XML_ERROR_PARTIAL_CHAR: Final[LiteralString] +XML_ERROR_PUBLICID: Final[LiteralString] +XML_ERROR_RECURSIVE_ENTITY_REF: Final[LiteralString] +XML_ERROR_SUSPENDED: Final[LiteralString] +XML_ERROR_SUSPEND_PE: Final[LiteralString] +XML_ERROR_SYNTAX: Final[LiteralString] +XML_ERROR_TAG_MISMATCH: Final[LiteralString] +XML_ERROR_TEXT_DECL: Final[LiteralString] +XML_ERROR_UNBOUND_PREFIX: Final[LiteralString] +XML_ERROR_UNCLOSED_CDATA_SECTION: Final[LiteralString] +XML_ERROR_UNCLOSED_TOKEN: Final[LiteralString] +XML_ERROR_UNDECLARING_PREFIX: Final[LiteralString] +XML_ERROR_UNDEFINED_ENTITY: Final[LiteralString] +XML_ERROR_UNEXPECTED_STATE: Final[LiteralString] +XML_ERROR_UNKNOWN_ENCODING: Final[LiteralString] +XML_ERROR_XML_DECL: Final[LiteralString] +if sys.version_info >= (3, 11): + XML_ERROR_RESERVED_PREFIX_XML: Final[LiteralString] + XML_ERROR_RESERVED_PREFIX_XMLNS: Final[LiteralString] + XML_ERROR_RESERVED_NAMESPACE_URI: Final[LiteralString] + XML_ERROR_INVALID_ARGUMENT: Final[LiteralString] + XML_ERROR_NO_BUFFER: Final[LiteralString] + XML_ERROR_AMPLIFICATION_LIMIT_BREACH: Final[LiteralString] +if sys.version_info >= (3, 14): + XML_ERROR_NOT_STARTED: Final[LiteralString] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/queue.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/queue.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/queue.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/queue.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/quopri.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/quopri.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/quopri.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/quopri.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/random.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/random.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/random.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/random.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/re.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/re.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/re.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/re.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/readline.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/readline.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/readline.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/readline.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/reprlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/reprlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/reprlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/reprlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/resource.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/resource.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/resource.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/resource.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/rlcompleter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/rlcompleter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/rlcompleter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/rlcompleter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/runpy.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/runpy.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/runpy.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/runpy.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sched.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sched.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sched.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sched.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/secrets.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/secrets.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/secrets.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/secrets.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/select.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/select.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/select.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/select.pyi index 42941b9e41fab..0235473902733 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/select.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/select.pyi @@ -148,6 +148,8 @@ if sys.platform == "linux": EPOLLWRBAND: int EPOLLWRNORM: int EPOLL_CLOEXEC: int + if sys.version_info >= (3, 14): + EPOLLWAKEUP: int if sys.platform != "linux" and sys.platform != "darwin" and sys.platform != "win32": # Solaris only diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/selectors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/selectors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/selectors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/selectors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/shelve.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shelve.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/shelve.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/shelve.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/shlex.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shlex.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/shlex.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/shlex.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/shutil.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/shutil.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi index ea2c29d4625f0..c66d8fa128bec 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/shutil.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi @@ -18,7 +18,6 @@ __all__ = [ "rmtree", "Error", "SpecialFileError", - "ExecError", "make_archive", "get_archive_formats", "register_archive_format", @@ -34,6 +33,8 @@ __all__ = [ "SameFileError", "disk_usage", ] +if sys.version_info < (3, 14): + __all__ += ["ExecError"] _StrOrBytesPathT = TypeVar("_StrOrBytesPathT", bound=StrOrBytesPath) _StrPathT = TypeVar("_StrPathT", bound=StrPath) @@ -42,7 +43,13 @@ _BytesPathT = TypeVar("_BytesPathT", bound=BytesPath) class Error(OSError): ... class SameFileError(Error): ... class SpecialFileError(OSError): ... -class ExecError(OSError): ... + +if sys.version_info >= (3, 14): + ExecError = RuntimeError # Deprecated in Python 3.14; removal scheduled for Python 3.16 + +else: + class ExecError(OSError): ... + class ReadError(OSError): ... class RegistryError(Exception): ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/signal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/signal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/signal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/signal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/site.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/site.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/site.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/site.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/smtpd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/smtpd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/smtpd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/smtpd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/smtplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/smtplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/smtplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/smtplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sndhdr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sndhdr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sndhdr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sndhdr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/socket.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/socket.pyi similarity index 96% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/socket.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/socket.pyi index ff89dcc72209f..b4fa4381a72ca 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/socket.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/socket.pyi @@ -773,6 +773,10 @@ if sys.platform == "linux": if sys.version_info < (3, 11): from _socket import CAN_RAW_ERR_FILTER as CAN_RAW_ERR_FILTER + __all__ += ["CAN_RAW_ERR_FILTER"] + if sys.version_info >= (3, 13): + from _socket import CAN_RAW_ERR_FILTER as CAN_RAW_ERR_FILTER + __all__ += ["CAN_RAW_ERR_FILTER"] if sys.platform == "linux": @@ -1023,6 +1027,39 @@ if sys.platform != "linux": __all__ += ["IPPROTO_GGP", "IPPROTO_IPV4", "IPPROTO_MAX", "IPPROTO_ND", "IP_RECVDSTADDR", "SO_USELOOPBACK"] +if sys.version_info >= (3, 14): + from _socket import IP_RECVTTL as IP_RECVTTL + + __all__ += ["IP_RECVTTL"] + + if sys.platform == "win32" or sys.platform == "linux": + from _socket import IP_RECVERR as IP_RECVERR, IPV6_RECVERR as IPV6_RECVERR, SO_ORIGINAL_DST as SO_ORIGINAL_DST + + __all__ += ["IP_RECVERR", "IPV6_RECVERR", "SO_ORIGINAL_DST"] + + if sys.platform == "win32": + from _socket import ( + SO_BTH_ENCRYPT as SO_BTH_ENCRYPT, + SO_BTH_MTU as SO_BTH_MTU, + SO_BTH_MTU_MAX as SO_BTH_MTU_MAX, + SO_BTH_MTU_MIN as SO_BTH_MTU_MIN, + SOL_RFCOMM as SOL_RFCOMM, + TCP_QUICKACK as TCP_QUICKACK, + ) + + __all__ += ["SOL_RFCOMM", "SO_BTH_ENCRYPT", "SO_BTH_MTU", "SO_BTH_MTU_MAX", "SO_BTH_MTU_MIN", "TCP_QUICKACK"] + + if sys.platform == "linux": + from _socket import ( + CAN_RAW_ERR_FILTER as CAN_RAW_ERR_FILTER, + IP_FREEBIND as IP_FREEBIND, + IP_RECVORIGDSTADDR as IP_RECVORIGDSTADDR, + SO_ORIGINAL_DST as SO_ORIGINAL_DST, + VMADDR_CID_LOCAL as VMADDR_CID_LOCAL, + ) + + __all__ += ["CAN_RAW_ERR_FILTER", "IP_FREEBIND", "IP_RECVORIGDSTADDR", "VMADDR_CID_LOCAL"] + # Re-exported from errno EBADF: int EAGAIN: int diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/socketserver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/socketserver.pyi similarity index 96% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/socketserver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/socketserver.pyi index 061932f0fac7e..f321d14a792b2 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/socketserver.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/socketserver.pyi @@ -35,6 +35,7 @@ if sys.platform != "win32": _RequestType: TypeAlias = _socket | tuple[bytes, _socket] _AfUnixAddress: TypeAlias = str | ReadableBuffer # address acceptable for an AF_UNIX socket _AfInetAddress: TypeAlias = tuple[str | bytes | bytearray, int] # address acceptable for an AF_INET socket +_AfInet6Address: TypeAlias = tuple[str | bytes | bytearray, int, int, int] # address acceptable for an AF_INET6 socket # This can possibly be generic at some point: class BaseServer: @@ -71,10 +72,10 @@ class TCPServer(BaseServer): socket_type: int if sys.version_info >= (3, 11): allow_reuse_port: bool - server_address: _AfInetAddress + server_address: _AfInetAddress | _AfInet6Address def __init__( self, - server_address: _AfInetAddress, + server_address: _AfInetAddress | _AfInet6Address, RequestHandlerClass: Callable[[Any, _RetAddress, Self], BaseRequestHandler], bind_and_activate: bool = True, ) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/spwd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/spwd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/spwd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/spwd.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi new file mode 100644 index 0000000000000..5d3c2330be5e8 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi @@ -0,0 +1,469 @@ +import sys +from _typeshed import MaybeNone, ReadableBuffer, StrOrBytesPath, SupportsLenAndGetItem, Unused +from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence +from sqlite3.dbapi2 import ( + PARSE_COLNAMES as PARSE_COLNAMES, + PARSE_DECLTYPES as PARSE_DECLTYPES, + SQLITE_ALTER_TABLE as SQLITE_ALTER_TABLE, + SQLITE_ANALYZE as SQLITE_ANALYZE, + SQLITE_ATTACH as SQLITE_ATTACH, + SQLITE_CREATE_INDEX as SQLITE_CREATE_INDEX, + SQLITE_CREATE_TABLE as SQLITE_CREATE_TABLE, + SQLITE_CREATE_TEMP_INDEX as SQLITE_CREATE_TEMP_INDEX, + SQLITE_CREATE_TEMP_TABLE as SQLITE_CREATE_TEMP_TABLE, + SQLITE_CREATE_TEMP_TRIGGER as SQLITE_CREATE_TEMP_TRIGGER, + SQLITE_CREATE_TEMP_VIEW as SQLITE_CREATE_TEMP_VIEW, + SQLITE_CREATE_TRIGGER as SQLITE_CREATE_TRIGGER, + SQLITE_CREATE_VIEW as SQLITE_CREATE_VIEW, + SQLITE_CREATE_VTABLE as SQLITE_CREATE_VTABLE, + SQLITE_DELETE as SQLITE_DELETE, + SQLITE_DENY as SQLITE_DENY, + SQLITE_DETACH as SQLITE_DETACH, + SQLITE_DONE as SQLITE_DONE, + SQLITE_DROP_INDEX as SQLITE_DROP_INDEX, + SQLITE_DROP_TABLE as SQLITE_DROP_TABLE, + SQLITE_DROP_TEMP_INDEX as SQLITE_DROP_TEMP_INDEX, + SQLITE_DROP_TEMP_TABLE as SQLITE_DROP_TEMP_TABLE, + SQLITE_DROP_TEMP_TRIGGER as SQLITE_DROP_TEMP_TRIGGER, + SQLITE_DROP_TEMP_VIEW as SQLITE_DROP_TEMP_VIEW, + SQLITE_DROP_TRIGGER as SQLITE_DROP_TRIGGER, + SQLITE_DROP_VIEW as SQLITE_DROP_VIEW, + SQLITE_DROP_VTABLE as SQLITE_DROP_VTABLE, + SQLITE_FUNCTION as SQLITE_FUNCTION, + SQLITE_IGNORE as SQLITE_IGNORE, + SQLITE_INSERT as SQLITE_INSERT, + SQLITE_OK as SQLITE_OK, + SQLITE_PRAGMA as SQLITE_PRAGMA, + SQLITE_READ as SQLITE_READ, + SQLITE_RECURSIVE as SQLITE_RECURSIVE, + SQLITE_REINDEX as SQLITE_REINDEX, + SQLITE_SAVEPOINT as SQLITE_SAVEPOINT, + SQLITE_SELECT as SQLITE_SELECT, + SQLITE_TRANSACTION as SQLITE_TRANSACTION, + SQLITE_UPDATE as SQLITE_UPDATE, + Binary as Binary, + Date as Date, + DateFromTicks as DateFromTicks, + Time as Time, + TimeFromTicks as TimeFromTicks, + TimestampFromTicks as TimestampFromTicks, + adapt as adapt, + adapters as adapters, + apilevel as apilevel, + complete_statement as complete_statement, + connect as connect, + converters as converters, + enable_callback_tracebacks as enable_callback_tracebacks, + paramstyle as paramstyle, + register_adapter as register_adapter, + register_converter as register_converter, + sqlite_version as sqlite_version, + sqlite_version_info as sqlite_version_info, + threadsafety as threadsafety, +) +from types import TracebackType +from typing import Any, Literal, Protocol, SupportsIndex, TypeVar, final, overload, type_check_only +from typing_extensions import Self, TypeAlias + +if sys.version_info < (3, 14): + from sqlite3.dbapi2 import version_info as version_info + +if sys.version_info >= (3, 12): + from sqlite3.dbapi2 import ( + LEGACY_TRANSACTION_CONTROL as LEGACY_TRANSACTION_CONTROL, + SQLITE_DBCONFIG_DEFENSIVE as SQLITE_DBCONFIG_DEFENSIVE, + SQLITE_DBCONFIG_DQS_DDL as SQLITE_DBCONFIG_DQS_DDL, + SQLITE_DBCONFIG_DQS_DML as SQLITE_DBCONFIG_DQS_DML, + SQLITE_DBCONFIG_ENABLE_FKEY as SQLITE_DBCONFIG_ENABLE_FKEY, + SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER as SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, + SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION as SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + SQLITE_DBCONFIG_ENABLE_QPSG as SQLITE_DBCONFIG_ENABLE_QPSG, + SQLITE_DBCONFIG_ENABLE_TRIGGER as SQLITE_DBCONFIG_ENABLE_TRIGGER, + SQLITE_DBCONFIG_ENABLE_VIEW as SQLITE_DBCONFIG_ENABLE_VIEW, + SQLITE_DBCONFIG_LEGACY_ALTER_TABLE as SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, + SQLITE_DBCONFIG_LEGACY_FILE_FORMAT as SQLITE_DBCONFIG_LEGACY_FILE_FORMAT, + SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE as SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, + SQLITE_DBCONFIG_RESET_DATABASE as SQLITE_DBCONFIG_RESET_DATABASE, + SQLITE_DBCONFIG_TRIGGER_EQP as SQLITE_DBCONFIG_TRIGGER_EQP, + SQLITE_DBCONFIG_TRUSTED_SCHEMA as SQLITE_DBCONFIG_TRUSTED_SCHEMA, + SQLITE_DBCONFIG_WRITABLE_SCHEMA as SQLITE_DBCONFIG_WRITABLE_SCHEMA, + ) + +if sys.version_info >= (3, 11): + from sqlite3.dbapi2 import ( + SQLITE_ABORT as SQLITE_ABORT, + SQLITE_ABORT_ROLLBACK as SQLITE_ABORT_ROLLBACK, + SQLITE_AUTH as SQLITE_AUTH, + SQLITE_AUTH_USER as SQLITE_AUTH_USER, + SQLITE_BUSY as SQLITE_BUSY, + SQLITE_BUSY_RECOVERY as SQLITE_BUSY_RECOVERY, + SQLITE_BUSY_SNAPSHOT as SQLITE_BUSY_SNAPSHOT, + SQLITE_BUSY_TIMEOUT as SQLITE_BUSY_TIMEOUT, + SQLITE_CANTOPEN as SQLITE_CANTOPEN, + SQLITE_CANTOPEN_CONVPATH as SQLITE_CANTOPEN_CONVPATH, + SQLITE_CANTOPEN_DIRTYWAL as SQLITE_CANTOPEN_DIRTYWAL, + SQLITE_CANTOPEN_FULLPATH as SQLITE_CANTOPEN_FULLPATH, + SQLITE_CANTOPEN_ISDIR as SQLITE_CANTOPEN_ISDIR, + SQLITE_CANTOPEN_NOTEMPDIR as SQLITE_CANTOPEN_NOTEMPDIR, + SQLITE_CANTOPEN_SYMLINK as SQLITE_CANTOPEN_SYMLINK, + SQLITE_CONSTRAINT as SQLITE_CONSTRAINT, + SQLITE_CONSTRAINT_CHECK as SQLITE_CONSTRAINT_CHECK, + SQLITE_CONSTRAINT_COMMITHOOK as SQLITE_CONSTRAINT_COMMITHOOK, + SQLITE_CONSTRAINT_FOREIGNKEY as SQLITE_CONSTRAINT_FOREIGNKEY, + SQLITE_CONSTRAINT_FUNCTION as SQLITE_CONSTRAINT_FUNCTION, + SQLITE_CONSTRAINT_NOTNULL as SQLITE_CONSTRAINT_NOTNULL, + SQLITE_CONSTRAINT_PINNED as SQLITE_CONSTRAINT_PINNED, + SQLITE_CONSTRAINT_PRIMARYKEY as SQLITE_CONSTRAINT_PRIMARYKEY, + SQLITE_CONSTRAINT_ROWID as SQLITE_CONSTRAINT_ROWID, + SQLITE_CONSTRAINT_TRIGGER as SQLITE_CONSTRAINT_TRIGGER, + SQLITE_CONSTRAINT_UNIQUE as SQLITE_CONSTRAINT_UNIQUE, + SQLITE_CONSTRAINT_VTAB as SQLITE_CONSTRAINT_VTAB, + SQLITE_CORRUPT as SQLITE_CORRUPT, + SQLITE_CORRUPT_INDEX as SQLITE_CORRUPT_INDEX, + SQLITE_CORRUPT_SEQUENCE as SQLITE_CORRUPT_SEQUENCE, + SQLITE_CORRUPT_VTAB as SQLITE_CORRUPT_VTAB, + SQLITE_EMPTY as SQLITE_EMPTY, + SQLITE_ERROR as SQLITE_ERROR, + SQLITE_ERROR_MISSING_COLLSEQ as SQLITE_ERROR_MISSING_COLLSEQ, + SQLITE_ERROR_RETRY as SQLITE_ERROR_RETRY, + SQLITE_ERROR_SNAPSHOT as SQLITE_ERROR_SNAPSHOT, + SQLITE_FORMAT as SQLITE_FORMAT, + SQLITE_FULL as SQLITE_FULL, + SQLITE_INTERNAL as SQLITE_INTERNAL, + SQLITE_INTERRUPT as SQLITE_INTERRUPT, + SQLITE_IOERR as SQLITE_IOERR, + SQLITE_IOERR_ACCESS as SQLITE_IOERR_ACCESS, + SQLITE_IOERR_AUTH as SQLITE_IOERR_AUTH, + SQLITE_IOERR_BEGIN_ATOMIC as SQLITE_IOERR_BEGIN_ATOMIC, + SQLITE_IOERR_BLOCKED as SQLITE_IOERR_BLOCKED, + SQLITE_IOERR_CHECKRESERVEDLOCK as SQLITE_IOERR_CHECKRESERVEDLOCK, + SQLITE_IOERR_CLOSE as SQLITE_IOERR_CLOSE, + SQLITE_IOERR_COMMIT_ATOMIC as SQLITE_IOERR_COMMIT_ATOMIC, + SQLITE_IOERR_CONVPATH as SQLITE_IOERR_CONVPATH, + SQLITE_IOERR_CORRUPTFS as SQLITE_IOERR_CORRUPTFS, + SQLITE_IOERR_DATA as SQLITE_IOERR_DATA, + SQLITE_IOERR_DELETE as SQLITE_IOERR_DELETE, + SQLITE_IOERR_DELETE_NOENT as SQLITE_IOERR_DELETE_NOENT, + SQLITE_IOERR_DIR_CLOSE as SQLITE_IOERR_DIR_CLOSE, + SQLITE_IOERR_DIR_FSYNC as SQLITE_IOERR_DIR_FSYNC, + SQLITE_IOERR_FSTAT as SQLITE_IOERR_FSTAT, + SQLITE_IOERR_FSYNC as SQLITE_IOERR_FSYNC, + SQLITE_IOERR_GETTEMPPATH as SQLITE_IOERR_GETTEMPPATH, + SQLITE_IOERR_LOCK as SQLITE_IOERR_LOCK, + SQLITE_IOERR_MMAP as SQLITE_IOERR_MMAP, + SQLITE_IOERR_NOMEM as SQLITE_IOERR_NOMEM, + SQLITE_IOERR_RDLOCK as SQLITE_IOERR_RDLOCK, + SQLITE_IOERR_READ as SQLITE_IOERR_READ, + SQLITE_IOERR_ROLLBACK_ATOMIC as SQLITE_IOERR_ROLLBACK_ATOMIC, + SQLITE_IOERR_SEEK as SQLITE_IOERR_SEEK, + SQLITE_IOERR_SHMLOCK as SQLITE_IOERR_SHMLOCK, + SQLITE_IOERR_SHMMAP as SQLITE_IOERR_SHMMAP, + SQLITE_IOERR_SHMOPEN as SQLITE_IOERR_SHMOPEN, + SQLITE_IOERR_SHMSIZE as SQLITE_IOERR_SHMSIZE, + SQLITE_IOERR_SHORT_READ as SQLITE_IOERR_SHORT_READ, + SQLITE_IOERR_TRUNCATE as SQLITE_IOERR_TRUNCATE, + SQLITE_IOERR_UNLOCK as SQLITE_IOERR_UNLOCK, + SQLITE_IOERR_VNODE as SQLITE_IOERR_VNODE, + SQLITE_IOERR_WRITE as SQLITE_IOERR_WRITE, + SQLITE_LIMIT_ATTACHED as SQLITE_LIMIT_ATTACHED, + SQLITE_LIMIT_COLUMN as SQLITE_LIMIT_COLUMN, + SQLITE_LIMIT_COMPOUND_SELECT as SQLITE_LIMIT_COMPOUND_SELECT, + SQLITE_LIMIT_EXPR_DEPTH as SQLITE_LIMIT_EXPR_DEPTH, + SQLITE_LIMIT_FUNCTION_ARG as SQLITE_LIMIT_FUNCTION_ARG, + SQLITE_LIMIT_LENGTH as SQLITE_LIMIT_LENGTH, + SQLITE_LIMIT_LIKE_PATTERN_LENGTH as SQLITE_LIMIT_LIKE_PATTERN_LENGTH, + SQLITE_LIMIT_SQL_LENGTH as SQLITE_LIMIT_SQL_LENGTH, + SQLITE_LIMIT_TRIGGER_DEPTH as SQLITE_LIMIT_TRIGGER_DEPTH, + SQLITE_LIMIT_VARIABLE_NUMBER as SQLITE_LIMIT_VARIABLE_NUMBER, + SQLITE_LIMIT_VDBE_OP as SQLITE_LIMIT_VDBE_OP, + SQLITE_LIMIT_WORKER_THREADS as SQLITE_LIMIT_WORKER_THREADS, + SQLITE_LOCKED as SQLITE_LOCKED, + SQLITE_LOCKED_SHAREDCACHE as SQLITE_LOCKED_SHAREDCACHE, + SQLITE_LOCKED_VTAB as SQLITE_LOCKED_VTAB, + SQLITE_MISMATCH as SQLITE_MISMATCH, + SQLITE_MISUSE as SQLITE_MISUSE, + SQLITE_NOLFS as SQLITE_NOLFS, + SQLITE_NOMEM as SQLITE_NOMEM, + SQLITE_NOTADB as SQLITE_NOTADB, + SQLITE_NOTFOUND as SQLITE_NOTFOUND, + SQLITE_NOTICE as SQLITE_NOTICE, + SQLITE_NOTICE_RECOVER_ROLLBACK as SQLITE_NOTICE_RECOVER_ROLLBACK, + SQLITE_NOTICE_RECOVER_WAL as SQLITE_NOTICE_RECOVER_WAL, + SQLITE_OK_LOAD_PERMANENTLY as SQLITE_OK_LOAD_PERMANENTLY, + SQLITE_OK_SYMLINK as SQLITE_OK_SYMLINK, + SQLITE_PERM as SQLITE_PERM, + SQLITE_PROTOCOL as SQLITE_PROTOCOL, + SQLITE_RANGE as SQLITE_RANGE, + SQLITE_READONLY as SQLITE_READONLY, + SQLITE_READONLY_CANTINIT as SQLITE_READONLY_CANTINIT, + SQLITE_READONLY_CANTLOCK as SQLITE_READONLY_CANTLOCK, + SQLITE_READONLY_DBMOVED as SQLITE_READONLY_DBMOVED, + SQLITE_READONLY_DIRECTORY as SQLITE_READONLY_DIRECTORY, + SQLITE_READONLY_RECOVERY as SQLITE_READONLY_RECOVERY, + SQLITE_READONLY_ROLLBACK as SQLITE_READONLY_ROLLBACK, + SQLITE_ROW as SQLITE_ROW, + SQLITE_SCHEMA as SQLITE_SCHEMA, + SQLITE_TOOBIG as SQLITE_TOOBIG, + SQLITE_WARNING as SQLITE_WARNING, + SQLITE_WARNING_AUTOINDEX as SQLITE_WARNING_AUTOINDEX, + ) + +if sys.version_info < (3, 12): + from sqlite3.dbapi2 import enable_shared_cache as enable_shared_cache, version as version + +if sys.version_info < (3, 10): + from sqlite3.dbapi2 import OptimizedUnicode as OptimizedUnicode + +_CursorT = TypeVar("_CursorT", bound=Cursor) +_SqliteData: TypeAlias = str | ReadableBuffer | int | float | None +# Data that is passed through adapters can be of any type accepted by an adapter. +_AdaptedInputData: TypeAlias = _SqliteData | Any +# The Mapping must really be a dict, but making it invariant is too annoying. +_Parameters: TypeAlias = SupportsLenAndGetItem[_AdaptedInputData] | Mapping[str, _AdaptedInputData] + +class _AnyParamWindowAggregateClass(Protocol): + def step(self, *args: Any) -> object: ... + def inverse(self, *args: Any) -> object: ... + def value(self) -> _SqliteData: ... + def finalize(self) -> _SqliteData: ... + +class _WindowAggregateClass(Protocol): + step: Callable[..., object] + inverse: Callable[..., object] + def value(self) -> _SqliteData: ... + def finalize(self) -> _SqliteData: ... + +class _AggregateProtocol(Protocol): + def step(self, value: int, /) -> object: ... + def finalize(self) -> int: ... + +class _SingleParamWindowAggregateClass(Protocol): + def step(self, param: Any, /) -> object: ... + def inverse(self, param: Any, /) -> object: ... + def value(self) -> _SqliteData: ... + def finalize(self) -> _SqliteData: ... + +# These classes are implemented in the C module _sqlite3. At runtime, they're imported +# from there into sqlite3.dbapi2 and from that module to here. However, they +# consider themselves to live in the sqlite3.* namespace, so we'll define them here. + +class Error(Exception): + if sys.version_info >= (3, 11): + sqlite_errorcode: int + sqlite_errorname: str + +class DatabaseError(Error): ... +class DataError(DatabaseError): ... +class IntegrityError(DatabaseError): ... +class InterfaceError(Error): ... +class InternalError(DatabaseError): ... +class NotSupportedError(DatabaseError): ... +class OperationalError(DatabaseError): ... +class ProgrammingError(DatabaseError): ... +class Warning(Exception): ... + +class Connection: + @property + def DataError(self) -> type[DataError]: ... + @property + def DatabaseError(self) -> type[DatabaseError]: ... + @property + def Error(self) -> type[Error]: ... + @property + def IntegrityError(self) -> type[IntegrityError]: ... + @property + def InterfaceError(self) -> type[InterfaceError]: ... + @property + def InternalError(self) -> type[InternalError]: ... + @property + def NotSupportedError(self) -> type[NotSupportedError]: ... + @property + def OperationalError(self) -> type[OperationalError]: ... + @property + def ProgrammingError(self) -> type[ProgrammingError]: ... + @property + def Warning(self) -> type[Warning]: ... + @property + def in_transaction(self) -> bool: ... + isolation_level: str | None # one of '', 'DEFERRED', 'IMMEDIATE' or 'EXCLUSIVE' + @property + def total_changes(self) -> int: ... + if sys.version_info >= (3, 12): + @property + def autocommit(self) -> int: ... + @autocommit.setter + def autocommit(self, val: int) -> None: ... + row_factory: Any + text_factory: Any + if sys.version_info >= (3, 12): + def __init__( + self, + database: StrOrBytesPath, + timeout: float = ..., + detect_types: int = ..., + isolation_level: str | None = ..., + check_same_thread: bool = ..., + factory: type[Connection] | None = ..., + cached_statements: int = ..., + uri: bool = ..., + autocommit: bool = ..., + ) -> None: ... + else: + def __init__( + self, + database: StrOrBytesPath, + timeout: float = ..., + detect_types: int = ..., + isolation_level: str | None = ..., + check_same_thread: bool = ..., + factory: type[Connection] | None = ..., + cached_statements: int = ..., + uri: bool = ..., + ) -> None: ... + + def close(self) -> None: ... + if sys.version_info >= (3, 11): + def blobopen(self, table: str, column: str, row: int, /, *, readonly: bool = False, name: str = "main") -> Blob: ... + + def commit(self) -> None: ... + def create_aggregate(self, name: str, n_arg: int, aggregate_class: Callable[[], _AggregateProtocol]) -> None: ... + if sys.version_info >= (3, 11): + # num_params determines how many params will be passed to the aggregate class. We provide an overload + # for the case where num_params = 1, which is expected to be the common case. + @overload + def create_window_function( + self, name: str, num_params: Literal[1], aggregate_class: Callable[[], _SingleParamWindowAggregateClass] | None, / + ) -> None: ... + # And for num_params = -1, which means the aggregate must accept any number of parameters. + @overload + def create_window_function( + self, name: str, num_params: Literal[-1], aggregate_class: Callable[[], _AnyParamWindowAggregateClass] | None, / + ) -> None: ... + @overload + def create_window_function( + self, name: str, num_params: int, aggregate_class: Callable[[], _WindowAggregateClass] | None, / + ) -> None: ... + + def create_collation(self, name: str, callback: Callable[[str, str], int | SupportsIndex] | None, /) -> None: ... + def create_function( + self, name: str, narg: int, func: Callable[..., _SqliteData] | None, *, deterministic: bool = False + ) -> None: ... + @overload + def cursor(self, factory: None = None) -> Cursor: ... + @overload + def cursor(self, factory: Callable[[Connection], _CursorT]) -> _CursorT: ... + def execute(self, sql: str, parameters: _Parameters = ..., /) -> Cursor: ... + def executemany(self, sql: str, parameters: Iterable[_Parameters], /) -> Cursor: ... + def executescript(self, sql_script: str, /) -> Cursor: ... + def interrupt(self) -> None: ... + if sys.version_info >= (3, 13): + def iterdump(self, *, filter: str | None = None) -> Generator[str, None, None]: ... + else: + def iterdump(self) -> Generator[str, None, None]: ... + + def rollback(self) -> None: ... + def set_authorizer( + self, authorizer_callback: Callable[[int, str | None, str | None, str | None, str | None], int] | None + ) -> None: ... + def set_progress_handler(self, progress_handler: Callable[[], int | None] | None, n: int) -> None: ... + def set_trace_callback(self, trace_callback: Callable[[str], object] | None) -> None: ... + # enable_load_extension and load_extension is not available on python distributions compiled + # without sqlite3 loadable extension support. see footnotes https://docs.python.org/3/library/sqlite3.html#f1 + def enable_load_extension(self, enable: bool, /) -> None: ... + if sys.version_info >= (3, 12): + def load_extension(self, name: str, /, *, entrypoint: str | None = None) -> None: ... + else: + def load_extension(self, name: str, /) -> None: ... + + def backup( + self, + target: Connection, + *, + pages: int = -1, + progress: Callable[[int, int, int], object] | None = None, + name: str = "main", + sleep: float = 0.25, + ) -> None: ... + if sys.version_info >= (3, 11): + def setlimit(self, category: int, limit: int, /) -> int: ... + def getlimit(self, category: int, /) -> int: ... + def serialize(self, *, name: str = "main") -> bytes: ... + def deserialize(self, data: ReadableBuffer, /, *, name: str = "main") -> None: ... + if sys.version_info >= (3, 12): + def getconfig(self, op: int, /) -> bool: ... + def setconfig(self, op: int, enable: bool = True, /) -> bool: ... + + def __call__(self, sql: str, /) -> _Statement: ... + def __enter__(self) -> Self: ... + def __exit__( + self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None, / + ) -> Literal[False]: ... + +class Cursor: + arraysize: int + @property + def connection(self) -> Connection: ... + # May be None, but using `| MaybeNone` (`| Any`) instead to avoid slightly annoying false positives. + @property + def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | MaybeNone: ... + @property + def lastrowid(self) -> int | None: ... + row_factory: Callable[[Cursor, Row], object] | None + @property + def rowcount(self) -> int: ... + def __init__(self, cursor: Connection, /) -> None: ... + def close(self) -> None: ... + def execute(self, sql: str, parameters: _Parameters = (), /) -> Self: ... + def executemany(self, sql: str, seq_of_parameters: Iterable[_Parameters], /) -> Self: ... + def executescript(self, sql_script: str, /) -> Cursor: ... + def fetchall(self) -> list[Any]: ... + def fetchmany(self, size: int | None = 1) -> list[Any]: ... + # Returns either a row (as created by the row_factory) or None, but + # putting None in the return annotation causes annoying false positives. + def fetchone(self) -> Any: ... + def setinputsizes(self, sizes: Unused, /) -> None: ... # does nothing + def setoutputsize(self, size: Unused, column: Unused = None, /) -> None: ... # does nothing + def __iter__(self) -> Self: ... + def __next__(self) -> Any: ... + +@final +class PrepareProtocol: + def __init__(self, *args: object, **kwargs: object) -> None: ... + +class Row(Sequence[Any]): + def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> Self: ... + def keys(self) -> list[str]: ... + @overload + def __getitem__(self, key: int | str, /) -> Any: ... + @overload + def __getitem__(self, key: slice, /) -> tuple[Any, ...]: ... + def __hash__(self) -> int: ... + def __iter__(self) -> Iterator[Any]: ... + def __len__(self) -> int: ... + # These return NotImplemented for anything that is not a Row. + def __eq__(self, value: object, /) -> bool: ... + def __ge__(self, value: object, /) -> bool: ... + def __gt__(self, value: object, /) -> bool: ... + def __le__(self, value: object, /) -> bool: ... + def __lt__(self, value: object, /) -> bool: ... + def __ne__(self, value: object, /) -> bool: ... + +# This class is not exposed. It calls itself sqlite3.Statement. +@final +@type_check_only +class _Statement: ... + +if sys.version_info >= (3, 11): + @final + class Blob: + def close(self) -> None: ... + def read(self, length: int = -1, /) -> bytes: ... + def write(self, data: ReadableBuffer, /) -> None: ... + def tell(self) -> int: ... + # whence must be one of os.SEEK_SET, os.SEEK_CUR, os.SEEK_END + def seek(self, offset: int, origin: int = 0, /) -> None: ... + def __len__(self) -> int: ... + def __enter__(self) -> Self: ... + def __exit__(self, type: object, val: object, tb: object, /) -> Literal[False]: ... + def __getitem__(self, key: SupportsIndex | slice, /) -> int: ... + def __setitem__(self, key: SupportsIndex | slice, value: int, /) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_compile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_compile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sre_compile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sre_compile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_constants.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sre_constants.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.pyi index c41a52b26d5ab..a3921aa0fc3b8 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_constants.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.pyi @@ -23,6 +23,8 @@ AT_LOCALE: dict[_NamedIntConstant, _NamedIntConstant] AT_UNICODE: dict[_NamedIntConstant, _NamedIntConstant] CH_LOCALE: dict[_NamedIntConstant, _NamedIntConstant] CH_UNICODE: dict[_NamedIntConstant, _NamedIntConstant] +if sys.version_info >= (3, 14): + CH_NEGATE: dict[_NamedIntConstant, _NamedIntConstant] # flags if sys.version_info < (3, 13): SRE_FLAG_TEMPLATE: Final = 1 diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_parse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_parse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sre_parse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sre_parse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ssl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ssl.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/stat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/stat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/stat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/stat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/statistics.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/statistics.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/statistics.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/statistics.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.pyi new file mode 100644 index 0000000000000..29fe27f39b809 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/string/__init__.pyi @@ -0,0 +1,79 @@ +import sys +from _typeshed import StrOrLiteralStr +from collections.abc import Iterable, Mapping, Sequence +from re import Pattern, RegexFlag +from typing import Any, ClassVar, overload +from typing_extensions import LiteralString + +__all__ = [ + "ascii_letters", + "ascii_lowercase", + "ascii_uppercase", + "capwords", + "digits", + "hexdigits", + "octdigits", + "printable", + "punctuation", + "whitespace", + "Formatter", + "Template", +] + +ascii_letters: LiteralString +ascii_lowercase: LiteralString +ascii_uppercase: LiteralString +digits: LiteralString +hexdigits: LiteralString +octdigits: LiteralString +punctuation: LiteralString +printable: LiteralString +whitespace: LiteralString + +def capwords(s: StrOrLiteralStr, sep: StrOrLiteralStr | None = None) -> StrOrLiteralStr: ... + +class Template: + template: str + delimiter: ClassVar[str] + idpattern: ClassVar[str] + braceidpattern: ClassVar[str | None] + if sys.version_info >= (3, 14): + flags: ClassVar[RegexFlag | None] + else: + flags: ClassVar[RegexFlag] + pattern: ClassVar[Pattern[str]] + def __init__(self, template: str) -> None: ... + def substitute(self, mapping: Mapping[str, object] = {}, /, **kwds: object) -> str: ... + def safe_substitute(self, mapping: Mapping[str, object] = {}, /, **kwds: object) -> str: ... + if sys.version_info >= (3, 11): + def get_identifiers(self) -> list[str]: ... + def is_valid(self) -> bool: ... + +class Formatter: + @overload + def format(self, format_string: LiteralString, /, *args: LiteralString, **kwargs: LiteralString) -> LiteralString: ... + @overload + def format(self, format_string: str, /, *args: Any, **kwargs: Any) -> str: ... + @overload + def vformat( + self, format_string: LiteralString, args: Sequence[LiteralString], kwargs: Mapping[LiteralString, LiteralString] + ) -> LiteralString: ... + @overload + def vformat(self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> str: ... + def _vformat( # undocumented + self, + format_string: str, + args: Sequence[Any], + kwargs: Mapping[str, Any], + used_args: set[int | str], + recursion_depth: int, + auto_arg_index: int = 0, + ) -> tuple[str, int]: ... + def parse( + self, format_string: StrOrLiteralStr + ) -> Iterable[tuple[StrOrLiteralStr, StrOrLiteralStr | None, StrOrLiteralStr | None, StrOrLiteralStr | None]]: ... + def get_field(self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> Any: ... + def get_value(self, key: int | str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> Any: ... + def check_unused_args(self, used_args: set[int | str], args: Sequence[Any], kwargs: Mapping[str, Any]) -> None: ... + def format_field(self, value: Any, format_spec: str) -> Any: ... + def convert_field(self, value: Any, conversion: str | None) -> Any: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/string/templatelib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/string/templatelib.pyi new file mode 100644 index 0000000000000..324447f5f34ce --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/string/templatelib.pyi @@ -0,0 +1,31 @@ +from collections.abc import Iterator +from types import GenericAlias +from typing import Any, Literal, final + +__all__ = ["Interpolation", "Template"] + +@final +class Template: # TODO: consider making `Template` generic on `TypeVarTuple` + strings: tuple[str, ...] + interpolations: tuple[Interpolation, ...] + + def __new__(cls, *args: str | Interpolation) -> Template: ... + def __iter__(self) -> Iterator[str | Interpolation]: ... + def __add__(self, other: Template | str) -> Template: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + @property + def values(self) -> tuple[Any, ...]: ... # Tuple of interpolation values, which can have any type + +@final +class Interpolation: + value: Any # TODO: consider making `Interpolation` generic in runtime + expression: str + conversion: Literal["a", "r", "s"] | None + format_spec: str + + __match_args__ = ("value", "expression", "conversion", "format_spec") + + def __new__( + cls, value: Any, expression: str, conversion: Literal["a", "r", "s"] | None = None, format_spec: str = "" + ) -> Interpolation: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/stringprep.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/stringprep.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/stringprep.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/stringprep.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/struct.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/struct.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/struct.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/struct.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/subprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/subprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/subprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/subprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sunau.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sunau.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/symbol.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/symbol.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/symbol.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/symbol.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/symtable.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/symtable.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi new file mode 100644 index 0000000000000..0ca30396a8785 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi @@ -0,0 +1,487 @@ +import sys +from _typeshed import MaybeNone, OptExcInfo, ProfileFunction, StrOrBytesPath, TraceFunction, structseq +from _typeshed.importlib import MetaPathFinderProtocol, PathEntryFinderProtocol +from builtins import object as _object +from collections.abc import AsyncGenerator, Callable, Sequence +from io import TextIOWrapper +from types import FrameType, ModuleType, TracebackType +from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeVar, final, type_check_only +from typing_extensions import LiteralString, TypeAlias, deprecated + +_T = TypeVar("_T") + +# see https://github.com/python/typeshed/issues/8513#issue-1333671093 for the rationale behind this alias +_ExitCode: TypeAlias = str | int | None + +# ----- sys variables ----- +if sys.platform != "win32": + abiflags: str +argv: list[str] +base_exec_prefix: str +base_prefix: str +byteorder: Literal["little", "big"] +builtin_module_names: Sequence[str] # actually a tuple of strings +copyright: str +if sys.platform == "win32": + dllhandle: int +dont_write_bytecode: bool +displayhook: Callable[[object], Any] +excepthook: Callable[[type[BaseException], BaseException, TracebackType | None], Any] +exec_prefix: str +executable: str +float_repr_style: Literal["short", "legacy"] +hexversion: int +last_type: type[BaseException] | None +last_value: BaseException | None +last_traceback: TracebackType | None +if sys.version_info >= (3, 12): + last_exc: BaseException # or undefined. +maxsize: int +maxunicode: int +meta_path: list[MetaPathFinderProtocol] +modules: dict[str, ModuleType] +if sys.version_info >= (3, 10): + orig_argv: list[str] +path: list[str] +path_hooks: list[Callable[[str], PathEntryFinderProtocol]] +path_importer_cache: dict[str, PathEntryFinderProtocol | None] +platform: LiteralString +platlibdir: str +prefix: str +pycache_prefix: str | None +ps1: object +ps2: object + +# TextIO is used instead of more specific types for the standard streams, +# since they are often monkeypatched at runtime. At startup, the objects +# are initialized to instances of TextIOWrapper, but can also be None under +# some circumstances. +# +# To use methods from TextIOWrapper, use an isinstance check to ensure that +# the streams have not been overridden: +# +# if isinstance(sys.stdout, io.TextIOWrapper): +# sys.stdout.reconfigure(...) +stdin: TextIO | MaybeNone +stdout: TextIO | MaybeNone +stderr: TextIO | MaybeNone + +if sys.version_info >= (3, 10): + stdlib_module_names: frozenset[str] + +__stdin__: Final[TextIOWrapper | None] # Contains the original value of stdin +__stdout__: Final[TextIOWrapper | None] # Contains the original value of stdout +__stderr__: Final[TextIOWrapper | None] # Contains the original value of stderr +tracebacklimit: int | None +version: str +api_version: int +warnoptions: Any +# Each entry is a tuple of the form (action, message, category, module, +# lineno) +if sys.platform == "win32": + winver: str +_xoptions: dict[Any, Any] + +# Type alias used as a mixin for structseq classes that cannot be instantiated at runtime +# This can't be represented in the type system, so we just use `structseq[Any]` +_UninstantiableStructseq: TypeAlias = structseq[Any] + +flags: _flags + +# This class is not exposed at runtime. It calls itself sys.flags. +# As a tuple, it can have a length between 15 and 18. We don't model +# the exact length here because that varies by patch version due to +# the backported security fix int_max_str_digits. The exact length shouldn't +# be relied upon. See #13031 +# This can be re-visited when typeshed drops support for 3.10, +# at which point all supported versions will include int_max_str_digits +# in all patch versions. +# 3.9 is 15 or 16-tuple +# 3.10 is 16 or 17-tuple +# 3.11+ is an 18-tuple. +@final +@type_check_only +class _flags(_UninstantiableStructseq, tuple[int, ...]): + # `safe_path` was added in py311 + if sys.version_info >= (3, 11): + __match_args__: Final = ( + "debug", + "inspect", + "interactive", + "optimize", + "dont_write_bytecode", + "no_user_site", + "no_site", + "ignore_environment", + "verbose", + "bytes_warning", + "quiet", + "hash_randomization", + "isolated", + "dev_mode", + "utf8_mode", + "warn_default_encoding", + "safe_path", + "int_max_str_digits", + ) + elif sys.version_info >= (3, 10): + __match_args__: Final = ( + "debug", + "inspect", + "interactive", + "optimize", + "dont_write_bytecode", + "no_user_site", + "no_site", + "ignore_environment", + "verbose", + "bytes_warning", + "quiet", + "hash_randomization", + "isolated", + "dev_mode", + "utf8_mode", + "warn_default_encoding", + "int_max_str_digits", + ) + + @property + def debug(self) -> int: ... + @property + def inspect(self) -> int: ... + @property + def interactive(self) -> int: ... + @property + def optimize(self) -> int: ... + @property + def dont_write_bytecode(self) -> int: ... + @property + def no_user_site(self) -> int: ... + @property + def no_site(self) -> int: ... + @property + def ignore_environment(self) -> int: ... + @property + def verbose(self) -> int: ... + @property + def bytes_warning(self) -> int: ... + @property + def quiet(self) -> int: ... + @property + def hash_randomization(self) -> int: ... + @property + def isolated(self) -> int: ... + @property + def dev_mode(self) -> bool: ... + @property + def utf8_mode(self) -> int: ... + if sys.version_info >= (3, 10): + @property + def warn_default_encoding(self) -> int: ... + if sys.version_info >= (3, 11): + @property + def safe_path(self) -> bool: ... + # Whether or not this exists on lower versions of Python + # may depend on which patch release you're using + # (it was backported to all Python versions on 3.8+ as a security fix) + # Added in: 3.9.14, 3.10.7 + # and present in all versions of 3.11 and later. + @property + def int_max_str_digits(self) -> int: ... + +float_info: _float_info + +# This class is not exposed at runtime. It calls itself sys.float_info. +@final +@type_check_only +class _float_info(structseq[float], tuple[float, int, int, float, int, int, int, int, float, int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ( + "max", + "max_exp", + "max_10_exp", + "min", + "min_exp", + "min_10_exp", + "dig", + "mant_dig", + "epsilon", + "radix", + "rounds", + ) + + @property + def max(self) -> float: ... # DBL_MAX + @property + def max_exp(self) -> int: ... # DBL_MAX_EXP + @property + def max_10_exp(self) -> int: ... # DBL_MAX_10_EXP + @property + def min(self) -> float: ... # DBL_MIN + @property + def min_exp(self) -> int: ... # DBL_MIN_EXP + @property + def min_10_exp(self) -> int: ... # DBL_MIN_10_EXP + @property + def dig(self) -> int: ... # DBL_DIG + @property + def mant_dig(self) -> int: ... # DBL_MANT_DIG + @property + def epsilon(self) -> float: ... # DBL_EPSILON + @property + def radix(self) -> int: ... # FLT_RADIX + @property + def rounds(self) -> int: ... # FLT_ROUNDS + +hash_info: _hash_info + +# This class is not exposed at runtime. It calls itself sys.hash_info. +@final +@type_check_only +class _hash_info(structseq[Any | int], tuple[int, int, int, int, int, str, int, int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("width", "modulus", "inf", "nan", "imag", "algorithm", "hash_bits", "seed_bits", "cutoff") + + @property + def width(self) -> int: ... + @property + def modulus(self) -> int: ... + @property + def inf(self) -> int: ... + @property + def nan(self) -> int: ... + @property + def imag(self) -> int: ... + @property + def algorithm(self) -> str: ... + @property + def hash_bits(self) -> int: ... + @property + def seed_bits(self) -> int: ... + @property + def cutoff(self) -> int: ... # undocumented + +implementation: _implementation + +# This class isn't really a thing. At runtime, implementation is an instance +# of types.SimpleNamespace. This allows for better typing. +@type_check_only +class _implementation: + name: str + version: _version_info + hexversion: int + cache_tag: str + # Define __getattr__, as the documentation states: + # > sys.implementation may contain additional attributes specific to the Python implementation. + # > These non-standard attributes must start with an underscore, and are not described here. + def __getattr__(self, name: str) -> Any: ... + +int_info: _int_info + +# This class is not exposed at runtime. It calls itself sys.int_info. +@final +@type_check_only +class _int_info(structseq[int], tuple[int, int, int, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("bits_per_digit", "sizeof_digit", "default_max_str_digits", "str_digits_check_threshold") + + @property + def bits_per_digit(self) -> int: ... + @property + def sizeof_digit(self) -> int: ... + @property + def default_max_str_digits(self) -> int: ... + @property + def str_digits_check_threshold(self) -> int: ... + +_ThreadInfoName: TypeAlias = Literal["nt", "pthread", "pthread-stubs", "solaris"] +_ThreadInfoLock: TypeAlias = Literal["semaphore", "mutex+cond"] | None + +# This class is not exposed at runtime. It calls itself sys.thread_info. +@final +@type_check_only +class _thread_info(_UninstantiableStructseq, tuple[_ThreadInfoName, _ThreadInfoLock, str | None]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("name", "lock", "version") + + @property + def name(self) -> _ThreadInfoName: ... + @property + def lock(self) -> _ThreadInfoLock: ... + @property + def version(self) -> str | None: ... + +thread_info: _thread_info +_ReleaseLevel: TypeAlias = Literal["alpha", "beta", "candidate", "final"] + +# This class is not exposed at runtime. It calls itself sys.version_info. +@final +@type_check_only +class _version_info(_UninstantiableStructseq, tuple[int, int, int, _ReleaseLevel, int]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("major", "minor", "micro", "releaselevel", "serial") + + @property + def major(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> _ReleaseLevel: ... + @property + def serial(self) -> int: ... + +version_info: _version_info + +def call_tracing(func: Callable[..., _T], args: Any, /) -> _T: ... + +if sys.version_info >= (3, 13): + @deprecated("Deprecated in Python 3.13; use _clear_internal_caches() instead.") + def _clear_type_cache() -> None: ... + +else: + def _clear_type_cache() -> None: ... + +def _current_frames() -> dict[int, FrameType]: ... +def _getframe(depth: int = 0, /) -> FrameType: ... + +if sys.version_info >= (3, 12): + def _getframemodulename(depth: int = 0) -> str | None: ... + +def _debugmallocstats() -> None: ... +def __displayhook__(object: object, /) -> None: ... +def __excepthook__(exctype: type[BaseException], value: BaseException, traceback: TracebackType | None, /) -> None: ... +def exc_info() -> OptExcInfo: ... + +if sys.version_info >= (3, 11): + def exception() -> BaseException | None: ... + +def exit(status: _ExitCode = None, /) -> NoReturn: ... +def getallocatedblocks() -> int: ... +def getdefaultencoding() -> str: ... + +if sys.platform != "win32": + def getdlopenflags() -> int: ... + +def getfilesystemencoding() -> str: ... +def getfilesystemencodeerrors() -> str: ... +def getrefcount(object: Any, /) -> int: ... +def getrecursionlimit() -> int: ... +def getsizeof(obj: object, default: int = ...) -> int: ... +def getswitchinterval() -> float: ... +def getprofile() -> ProfileFunction | None: ... +def setprofile(function: ProfileFunction | None, /) -> None: ... +def gettrace() -> TraceFunction | None: ... +def settrace(function: TraceFunction | None, /) -> None: ... + +if sys.platform == "win32": + # A tuple of length 5, even though it has more than 5 attributes. + @final + class _WinVersion(_UninstantiableStructseq, tuple[int, int, int, int, str]): + @property + def major(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def build(self) -> int: ... + @property + def platform(self) -> int: ... + @property + def service_pack(self) -> str: ... + @property + def service_pack_minor(self) -> int: ... + @property + def service_pack_major(self) -> int: ... + @property + def suite_mask(self) -> int: ... + @property + def product_type(self) -> int: ... + @property + def platform_version(self) -> tuple[int, int, int]: ... + + def getwindowsversion() -> _WinVersion: ... + +def intern(string: str, /) -> str: ... + +if sys.version_info >= (3, 13): + def _is_gil_enabled() -> bool: ... + def _clear_internal_caches() -> None: ... + def _is_interned(string: str, /) -> bool: ... + +def is_finalizing() -> bool: ... +def breakpointhook(*args: Any, **kwargs: Any) -> Any: ... + +__breakpointhook__ = breakpointhook # Contains the original value of breakpointhook + +if sys.platform != "win32": + def setdlopenflags(flags: int, /) -> None: ... + +def setrecursionlimit(limit: int, /) -> None: ... +def setswitchinterval(interval: float, /) -> None: ... +def gettotalrefcount() -> int: ... # Debug builds only + +# Doesn't exist at runtime, but exported in the stubs so pytest etc. can annotate their code more easily. +@type_check_only +class UnraisableHookArgs(Protocol): + exc_type: type[BaseException] + exc_value: BaseException | None + exc_traceback: TracebackType | None + err_msg: str | None + object: _object + +unraisablehook: Callable[[UnraisableHookArgs], Any] + +def __unraisablehook__(unraisable: UnraisableHookArgs, /) -> Any: ... +def addaudithook(hook: Callable[[str, tuple[Any, ...]], Any]) -> None: ... +def audit(event: str, /, *args: Any) -> None: ... + +_AsyncgenHook: TypeAlias = Callable[[AsyncGenerator[Any, Any]], None] | None + +# This class is not exposed at runtime. It calls itself builtins.asyncgen_hooks. +@final +@type_check_only +class _asyncgen_hooks(structseq[_AsyncgenHook], tuple[_AsyncgenHook, _AsyncgenHook]): + if sys.version_info >= (3, 10): + __match_args__: Final = ("firstiter", "finalizer") + + @property + def firstiter(self) -> _AsyncgenHook: ... + @property + def finalizer(self) -> _AsyncgenHook: ... + +def get_asyncgen_hooks() -> _asyncgen_hooks: ... +def set_asyncgen_hooks(firstiter: _AsyncgenHook = ..., finalizer: _AsyncgenHook = ...) -> None: ... + +if sys.platform == "win32": + def _enablelegacywindowsfsencoding() -> None: ... + +def get_coroutine_origin_tracking_depth() -> int: ... +def set_coroutine_origin_tracking_depth(depth: int) -> None: ... + +# The following two functions were added in 3.11.0, 3.10.7, and 3.9.14, +# as part of the response to CVE-2020-10735 +def set_int_max_str_digits(maxdigits: int) -> None: ... +def get_int_max_str_digits() -> int: ... + +if sys.version_info >= (3, 12): + if sys.version_info >= (3, 13): + def getunicodeinternedsize(*, _only_immortal: bool = False) -> int: ... + else: + def getunicodeinternedsize() -> int: ... + + def deactivate_stack_trampoline() -> None: ... + def is_stack_trampoline_active() -> bool: ... + # It always exists, but raises on non-linux platforms: + if sys.platform == "linux": + def activate_stack_trampoline(backend: str, /) -> None: ... + else: + def activate_stack_trampoline(backend: str, /) -> NoReturn: ... + + from . import _monitoring + + monitoring = _monitoring + +if sys.version_info >= (3, 14): + def is_remote_debug_enabled() -> bool: ... + def remote_exec(pid: int, script: StrOrBytesPath) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sysconfig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sysconfig.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/syslog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/syslog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/syslog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/syslog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tabnanny.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tabnanny.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tabnanny.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tabnanny.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi similarity index 95% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi index cd31b101c886b..a18ef0b823f9b 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi @@ -7,7 +7,7 @@ from collections.abc import Callable, Iterable, Iterator, Mapping from gzip import _ReadableFileobj as _GzipReadableFileobj, _WritableFileobj as _GzipWritableFileobj from types import TracebackType from typing import IO, ClassVar, Literal, Protocol, overload -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, deprecated __all__ = [ "TarFile", @@ -38,6 +38,8 @@ if sys.version_info >= (3, 12): "AbsolutePathError", "LinkOutsideDestinationError", ] +if sys.version_info >= (3, 13): + __all__ += ["LinkFallbackError"] _FilterFunction: TypeAlias = Callable[[TarInfo, str], TarInfo | None] _TarfileFilter: TypeAlias = Literal["fully_trusted", "tar", "data"] | _FilterFunction @@ -550,7 +552,14 @@ class TarFile: filter: _TarfileFilter | None = ..., ) -> None: ... def _extract_member( - self, tarinfo: TarInfo, targetpath: str, set_attrs: bool = True, numeric_owner: bool = False + self, + tarinfo: TarInfo, + targetpath: str, + set_attrs: bool = True, + numeric_owner: bool = False, + *, + filter_function: _FilterFunction | None = None, + extraction_root: str | None = None, ) -> None: ... # undocumented def extractfile(self, member: str | TarInfo) -> IO[bytes] | None: ... def makedir(self, tarinfo: TarInfo, targetpath: StrOrBytesPath) -> None: ... # undocumented @@ -559,6 +568,9 @@ class TarFile: def makefifo(self, tarinfo: TarInfo, targetpath: StrOrBytesPath) -> None: ... # undocumented def makedev(self, tarinfo: TarInfo, targetpath: StrOrBytesPath) -> None: ... # undocumented def makelink(self, tarinfo: TarInfo, targetpath: StrOrBytesPath) -> None: ... # undocumented + def makelink_with_filter( + self, tarinfo: TarInfo, targetpath: StrOrBytesPath, filter_function: _FilterFunction, extraction_root: str + ) -> None: ... # undocumented def chown(self, tarinfo: TarInfo, targetpath: StrOrBytesPath, numeric_owner: bool) -> None: ... # undocumented def chmod(self, tarinfo: TarInfo, targetpath: StrOrBytesPath) -> None: ... # undocumented def utime(self, tarinfo: TarInfo, targetpath: StrOrBytesPath) -> None: ... # undocumented @@ -607,6 +619,9 @@ class AbsoluteLinkError(FilterError): class LinkOutsideDestinationError(FilterError): def __init__(self, tarinfo: TarInfo, path: str) -> None: ... +class LinkFallbackError(FilterError): + def __init__(self, tarinfo: TarInfo, path: str) -> None: ... + def fully_trusted_filter(member: TarInfo, dest_path: str) -> TarInfo: ... def tar_filter(member: TarInfo, dest_path: str) -> TarInfo: ... def data_filter(member: TarInfo, dest_path: str) -> TarInfo: ... @@ -622,7 +637,6 @@ class TarInfo: offset: int offset_data: int sparse: bytes | None - tarfile: TarFile | None mode: int type: bytes linkname: str @@ -632,6 +646,16 @@ class TarInfo: gname: str pax_headers: Mapping[str, str] def __init__(self, name: str = "") -> None: ... + if sys.version_info >= (3, 13): + @property + @deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.16") + def tarfile(self) -> TarFile | None: ... + @tarfile.setter + @deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.16") + def tarfile(self, tarfile: TarFile | None) -> None: ... + else: + tarfile: TarFile | None + @classmethod def frombuf(cls, buf: bytes | bytearray, encoding: str, errors: str) -> Self: ... @classmethod diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/telnetlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/telnetlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/telnetlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/telnetlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tempfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tempfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tempfile.pyi index c4861f7c6f39f..ea6e057e410d4 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tempfile.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.pyi @@ -384,7 +384,7 @@ class SpooledTemporaryFile(IO[AnyStr], _SpooledTemporaryFileBase): def write(self: SpooledTemporaryFile[bytes], s: ReadableBuffer) -> int: ... @overload def write(self, s: AnyStr) -> int: ... - @overload + @overload # type: ignore[override] def writelines(self: SpooledTemporaryFile[str], iterable: Iterable[str]) -> None: ... @overload def writelines(self: SpooledTemporaryFile[bytes], iterable: Iterable[ReadableBuffer]) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/termios.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/termios.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/termios.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/termios.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/textwrap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/textwrap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/textwrap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/textwrap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/this.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/this.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/this.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/this.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/threading.pyi similarity index 77% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/threading.pyi index e3965fab0e803..d31351754d056 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/threading.pyi @@ -3,8 +3,10 @@ import sys from _thread import _excepthook, _ExceptHookArgs, get_native_id as get_native_id from _typeshed import ProfileFunction, TraceFunction from collections.abc import Callable, Iterable, Mapping +from contextvars import ContextVar from types import TracebackType from typing import Any, TypeVar, final +from typing_extensions import deprecated _T = TypeVar("_T") @@ -44,9 +46,11 @@ if sys.version_info >= (3, 12): _profile_hook: ProfileFunction | None def active_count() -> int: ... -def activeCount() -> int: ... # deprecated alias for active_count() +@deprecated("Use active_count() instead") +def activeCount() -> int: ... def current_thread() -> Thread: ... -def currentThread() -> Thread: ... # deprecated alias for current_thread() +@deprecated("Use current_thread() instead") +def currentThread() -> Thread: ... def get_ident() -> int: ... def enumerate() -> list[Thread]: ... def main_thread() -> Thread: ... @@ -73,27 +77,44 @@ class Thread: @property def ident(self) -> int | None: ... daemon: bool - def __init__( - self, - group: None = None, - target: Callable[..., object] | None = None, - name: str | None = None, - args: Iterable[Any] = (), - kwargs: Mapping[str, Any] | None = None, - *, - daemon: bool | None = None, - ) -> None: ... + if sys.version_info >= (3, 14): + def __init__( + self, + group: None = None, + target: Callable[..., object] | None = None, + name: str | None = None, + args: Iterable[Any] = (), + kwargs: Mapping[str, Any] | None = None, + *, + daemon: bool | None = None, + context: ContextVar[Any] | None = None, + ) -> None: ... + else: + def __init__( + self, + group: None = None, + target: Callable[..., object] | None = None, + name: str | None = None, + args: Iterable[Any] = (), + kwargs: Mapping[str, Any] | None = None, + *, + daemon: bool | None = None, + ) -> None: ... + def start(self) -> None: ... def run(self) -> None: ... def join(self, timeout: float | None = None) -> None: ... @property def native_id(self) -> int | None: ... # only available on some platforms def is_alive(self) -> bool: ... - # the following methods are all deprecated - def getName(self) -> str: ... - def setName(self, name: str) -> None: ... + @deprecated("Get the daemon attribute instead") def isDaemon(self) -> bool: ... + @deprecated("Set the daemon attribute instead") def setDaemon(self, daemonic: bool) -> None: ... + @deprecated("Use the name attribute instead") + def getName(self) -> str: ... + @deprecated("Use the name attribute instead") + def setName(self, name: str) -> None: ... class _DummyThread(Thread): def __init__(self) -> None: ... @@ -110,6 +131,9 @@ class _RLock: __enter__ = acquire def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ... + if sys.version_info >= (3, 14): + def locked(self) -> bool: ... + RLock = _thread.RLock # Actually a function at runtime. class Condition: @@ -124,7 +148,8 @@ class Condition: def wait_for(self, predicate: Callable[[], _T], timeout: float | None = None) -> _T: ... def notify(self, n: int = 1) -> None: ... def notify_all(self) -> None: ... - def notifyAll(self) -> None: ... # deprecated alias for notify_all() + @deprecated("Use notify_all() instead") + def notifyAll(self) -> None: ... class Semaphore: _value: int @@ -138,7 +163,8 @@ class BoundedSemaphore(Semaphore): ... class Event: def is_set(self) -> bool: ... - def isSet(self) -> bool: ... # deprecated alias for is_set() + @deprecated("Use is_set() instead") + def isSet(self) -> bool: ... def set(self) -> None: ... def clear(self) -> None: ... def wait(self, timeout: float | None = None) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/time.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/time.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/time.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/time.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/timeit.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/timeit.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/timeit.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/timeit.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi new file mode 100644 index 0000000000000..db0e34d737a62 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi @@ -0,0 +1,4083 @@ +import _tkinter +import sys +from _typeshed import Incomplete, MaybeNone, StrOrBytesPath +from collections.abc import Callable, Iterable, Mapping, Sequence +from tkinter.constants import * +from tkinter.font import _FontDescription +from types import GenericAlias, TracebackType +from typing import Any, ClassVar, Generic, Literal, NamedTuple, Protocol, TypedDict, TypeVar, overload, type_check_only +from typing_extensions import TypeAlias, TypeVarTuple, Unpack, deprecated + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from enum import Enum + +__all__ = [ + "TclError", + "NO", + "FALSE", + "OFF", + "YES", + "TRUE", + "ON", + "N", + "S", + "W", + "E", + "NW", + "SW", + "NE", + "SE", + "NS", + "EW", + "NSEW", + "CENTER", + "NONE", + "X", + "Y", + "BOTH", + "LEFT", + "TOP", + "RIGHT", + "BOTTOM", + "RAISED", + "SUNKEN", + "FLAT", + "RIDGE", + "GROOVE", + "SOLID", + "HORIZONTAL", + "VERTICAL", + "NUMERIC", + "CHAR", + "WORD", + "BASELINE", + "INSIDE", + "OUTSIDE", + "SEL", + "SEL_FIRST", + "SEL_LAST", + "END", + "INSERT", + "CURRENT", + "ANCHOR", + "ALL", + "NORMAL", + "DISABLED", + "ACTIVE", + "HIDDEN", + "CASCADE", + "CHECKBUTTON", + "COMMAND", + "RADIOBUTTON", + "SEPARATOR", + "SINGLE", + "BROWSE", + "MULTIPLE", + "EXTENDED", + "DOTBOX", + "UNDERLINE", + "PIESLICE", + "CHORD", + "ARC", + "FIRST", + "LAST", + "BUTT", + "PROJECTING", + "ROUND", + "BEVEL", + "MITER", + "MOVETO", + "SCROLL", + "UNITS", + "PAGES", + "TkVersion", + "TclVersion", + "READABLE", + "WRITABLE", + "EXCEPTION", + "EventType", + "Event", + "NoDefaultRoot", + "Variable", + "StringVar", + "IntVar", + "DoubleVar", + "BooleanVar", + "mainloop", + "getint", + "getdouble", + "getboolean", + "Misc", + "CallWrapper", + "XView", + "YView", + "Wm", + "Tk", + "Tcl", + "Pack", + "Place", + "Grid", + "BaseWidget", + "Widget", + "Toplevel", + "Button", + "Canvas", + "Checkbutton", + "Entry", + "Frame", + "Label", + "Listbox", + "Menu", + "Menubutton", + "Message", + "Radiobutton", + "Scale", + "Scrollbar", + "Text", + "OptionMenu", + "Image", + "PhotoImage", + "BitmapImage", + "image_names", + "image_types", + "Spinbox", + "LabelFrame", + "PanedWindow", +] + +# Using anything from tkinter.font in this file means that 'import tkinter' +# seems to also load tkinter.font. That's not how it actually works, but +# unfortunately not much can be done about it. https://github.com/python/typeshed/pull/4346 + +TclError = _tkinter.TclError +wantobjects: int +TkVersion: float +TclVersion: float +READABLE = _tkinter.READABLE +WRITABLE = _tkinter.WRITABLE +EXCEPTION = _tkinter.EXCEPTION + +# Quick guide for figuring out which widget class to choose: +# - Misc: any widget (don't use BaseWidget because Tk doesn't inherit from BaseWidget) +# - Widget: anything that is meant to be put into another widget with e.g. pack or grid +# +# Don't trust tkinter's docstrings, because they have been created by copy/pasting from +# Tk's manual pages more than 10 years ago. Use the latest manual pages instead: +# +# $ sudo apt install tk-doc tcl-doc +# $ man 3tk label # tkinter.Label +# $ man 3tk ttk_label # tkinter.ttk.Label +# $ man 3tcl after # tkinter.Misc.after +# +# You can also read the manual pages online: https://www.tcl.tk/doc/ + +# Some widgets have an option named -compound that accepts different values +# than the _Compound defined here. Many other options have similar things. +_Anchor: TypeAlias = Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] # manual page: Tk_GetAnchor +_ButtonCommand: TypeAlias = str | Callable[[], Any] # accepts string of tcl code, return value is returned from Button.invoke() +_Compound: TypeAlias = Literal["top", "left", "center", "right", "bottom", "none"] # -compound in manual page named 'options' +# manual page: Tk_GetCursor +_Cursor: TypeAlias = str | tuple[str] | tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] +# example when it's sequence: entry['invalidcommand'] = [entry.register(print), '%P'] +_EntryValidateCommand: TypeAlias = str | list[str] | tuple[str, ...] | Callable[[], bool] +_ImageSpec: TypeAlias = _Image | str # str can be from e.g. tkinter.image_names() +_Relief: TypeAlias = Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] # manual page: Tk_GetRelief +_ScreenUnits: TypeAlias = str | float # Often the right type instead of int. Manual page: Tk_GetPixels +# -xscrollcommand and -yscrollcommand in 'options' manual page +_XYScrollCommand: TypeAlias = str | Callable[[float, float], object] +_TakeFocusValue: TypeAlias = bool | Literal[0, 1, ""] | Callable[[str], bool | None] # -takefocus in manual page named 'options' + +if sys.version_info >= (3, 11): + @type_check_only + class _VersionInfoTypeBase(NamedTuple): + major: int + minor: int + micro: int + releaselevel: str + serial: int + + class _VersionInfoType(_VersionInfoTypeBase): ... + +if sys.version_info >= (3, 11): + class EventType(StrEnum): + Activate = "36" + ButtonPress = "4" + Button = ButtonPress + ButtonRelease = "5" + Circulate = "26" + CirculateRequest = "27" + ClientMessage = "33" + Colormap = "32" + Configure = "22" + ConfigureRequest = "23" + Create = "16" + Deactivate = "37" + Destroy = "17" + Enter = "7" + Expose = "12" + FocusIn = "9" + FocusOut = "10" + GraphicsExpose = "13" + Gravity = "24" + KeyPress = "2" + Key = "2" + KeyRelease = "3" + Keymap = "11" + Leave = "8" + Map = "19" + MapRequest = "20" + Mapping = "34" + Motion = "6" + MouseWheel = "38" + NoExpose = "14" + Property = "28" + Reparent = "21" + ResizeRequest = "25" + Selection = "31" + SelectionClear = "29" + SelectionRequest = "30" + Unmap = "18" + VirtualEvent = "35" + Visibility = "15" + +else: + class EventType(str, Enum): + Activate = "36" + ButtonPress = "4" + Button = ButtonPress + ButtonRelease = "5" + Circulate = "26" + CirculateRequest = "27" + ClientMessage = "33" + Colormap = "32" + Configure = "22" + ConfigureRequest = "23" + Create = "16" + Deactivate = "37" + Destroy = "17" + Enter = "7" + Expose = "12" + FocusIn = "9" + FocusOut = "10" + GraphicsExpose = "13" + Gravity = "24" + KeyPress = "2" + Key = KeyPress + KeyRelease = "3" + Keymap = "11" + Leave = "8" + Map = "19" + MapRequest = "20" + Mapping = "34" + Motion = "6" + MouseWheel = "38" + NoExpose = "14" + Property = "28" + Reparent = "21" + ResizeRequest = "25" + Selection = "31" + SelectionClear = "29" + SelectionRequest = "30" + Unmap = "18" + VirtualEvent = "35" + Visibility = "15" + +_W = TypeVar("_W", bound=Misc) +# Events considered covariant because you should never assign to event.widget. +_W_co = TypeVar("_W_co", covariant=True, bound=Misc, default=Misc) + +class Event(Generic[_W_co]): + serial: int + num: int + focus: bool + height: int + width: int + keycode: int + state: int | str + time: int + x: int + y: int + x_root: int + y_root: int + char: str + send_event: bool + keysym: str + keysym_num: int + type: EventType + widget: _W_co + delta: int + if sys.version_info >= (3, 14): + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +def NoDefaultRoot() -> None: ... + +class Variable: + def __init__(self, master: Misc | None = None, value=None, name: str | None = None) -> None: ... + def set(self, value) -> None: ... + initialize = set + def get(self): ... + def trace_add(self, mode: Literal["array", "read", "write", "unset"], callback: Callable[[str, str, str], object]) -> str: ... + def trace_remove(self, mode: Literal["array", "read", "write", "unset"], cbname: str) -> None: ... + def trace_info(self) -> list[tuple[tuple[Literal["array", "read", "write", "unset"], ...], str]]: ... + @deprecated("use trace_add() instead of trace()") + def trace(self, mode, callback): ... + @deprecated("use trace_add() instead of trace_variable()") + def trace_variable(self, mode, callback): ... + @deprecated("use trace_remove() instead of trace_vdelete()") + def trace_vdelete(self, mode, cbname) -> None: ... + @deprecated("use trace_info() instead of trace_vinfo()") + def trace_vinfo(self): ... + def __eq__(self, other: object) -> bool: ... + def __del__(self) -> None: ... + __hash__: ClassVar[None] # type: ignore[assignment] + +class StringVar(Variable): + def __init__(self, master: Misc | None = None, value: str | None = None, name: str | None = None) -> None: ... + def set(self, value: str) -> None: ... + initialize = set + def get(self) -> str: ... + +class IntVar(Variable): + def __init__(self, master: Misc | None = None, value: int | None = None, name: str | None = None) -> None: ... + def set(self, value: int) -> None: ... + initialize = set + def get(self) -> int: ... + +class DoubleVar(Variable): + def __init__(self, master: Misc | None = None, value: float | None = None, name: str | None = None) -> None: ... + def set(self, value: float) -> None: ... + initialize = set + def get(self) -> float: ... + +class BooleanVar(Variable): + def __init__(self, master: Misc | None = None, value: bool | None = None, name: str | None = None) -> None: ... + def set(self, value: bool) -> None: ... + initialize = set + def get(self) -> bool: ... + +def mainloop(n: int = 0) -> None: ... + +getint: Incomplete +getdouble: Incomplete + +def getboolean(s): ... + +_Ts = TypeVarTuple("_Ts") + +class _GridIndexInfo(TypedDict, total=False): + minsize: _ScreenUnits + pad: _ScreenUnits + uniform: str | None + weight: int + +class _BusyInfo(TypedDict): + cursor: _Cursor + +class Misc: + master: Misc | None + tk: _tkinter.TkappType + children: dict[str, Widget] + def destroy(self) -> None: ... + def deletecommand(self, name: str) -> None: ... + def tk_strictMotif(self, boolean=None): ... + def tk_bisque(self) -> None: ... + def tk_setPalette(self, *args, **kw) -> None: ... + def wait_variable(self, name: str | Variable = "PY_VAR") -> None: ... + waitvar = wait_variable + def wait_window(self, window: Misc | None = None) -> None: ... + def wait_visibility(self, window: Misc | None = None) -> None: ... + def setvar(self, name: str = "PY_VAR", value: str = "1") -> None: ... + def getvar(self, name: str = "PY_VAR"): ... + def getint(self, s): ... + def getdouble(self, s): ... + def getboolean(self, s): ... + def focus_set(self) -> None: ... + focus = focus_set + def focus_force(self) -> None: ... + def focus_get(self) -> Misc | None: ... + def focus_displayof(self) -> Misc | None: ... + def focus_lastfor(self) -> Misc | None: ... + def tk_focusFollowsMouse(self) -> None: ... + def tk_focusNext(self) -> Misc | None: ... + def tk_focusPrev(self) -> Misc | None: ... + # .after() can be called without the "func" argument, but it is basically never what you want. + # It behaves like time.sleep() and freezes the GUI app. + def after(self, ms: int | Literal["idle"], func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... + # after_idle is essentially partialmethod(after, "idle") + def after_idle(self, func: Callable[[Unpack[_Ts]], object], *args: Unpack[_Ts]) -> str: ... + def after_cancel(self, id: str) -> None: ... + if sys.version_info >= (3, 13): + def after_info(self, id: str | None = None) -> tuple[str, ...]: ... + + def bell(self, displayof: Literal[0] | Misc | None = 0) -> None: ... + if sys.version_info >= (3, 13): + # Supports options from `_BusyInfo`` + def tk_busy_cget(self, option: Literal["cursor"]) -> _Cursor: ... + busy_cget = tk_busy_cget + def tk_busy_configure(self, cnf: Any = None, **kw: Any) -> Any: ... + tk_busy_config = tk_busy_configure + busy_configure = tk_busy_configure + busy_config = tk_busy_configure + def tk_busy_current(self, pattern: str | None = None) -> list[Misc]: ... + busy_current = tk_busy_current + def tk_busy_forget(self) -> None: ... + busy_forget = tk_busy_forget + def tk_busy_hold(self, **kw: Unpack[_BusyInfo]) -> None: ... + tk_busy = tk_busy_hold + busy_hold = tk_busy_hold + busy = tk_busy_hold + def tk_busy_status(self) -> bool: ... + busy_status = tk_busy_status + + def clipboard_get(self, *, displayof: Misc = ..., type: str = ...) -> str: ... + def clipboard_clear(self, *, displayof: Misc = ...) -> None: ... + def clipboard_append(self, string: str, *, displayof: Misc = ..., format: str = ..., type: str = ...) -> None: ... + def grab_current(self): ... + def grab_release(self) -> None: ... + def grab_set(self) -> None: ... + def grab_set_global(self) -> None: ... + def grab_status(self) -> Literal["local", "global"] | None: ... + def option_add( + self, pattern, value, priority: int | Literal["widgetDefault", "startupFile", "userDefault", "interactive"] | None = None + ) -> None: ... + def option_clear(self) -> None: ... + def option_get(self, name, className): ... + def option_readfile(self, fileName, priority=None) -> None: ... + def selection_clear(self, **kw) -> None: ... + def selection_get(self, **kw): ... + def selection_handle(self, command, **kw) -> None: ... + def selection_own(self, **kw) -> None: ... + def selection_own_get(self, **kw): ... + def send(self, interp, cmd, *args): ... + def lower(self, belowThis=None) -> None: ... + def tkraise(self, aboveThis=None) -> None: ... + lift = tkraise + if sys.version_info >= (3, 11): + def info_patchlevel(self) -> _VersionInfoType: ... + + def winfo_atom(self, name: str, displayof: Literal[0] | Misc | None = 0) -> int: ... + def winfo_atomname(self, id: int, displayof: Literal[0] | Misc | None = 0) -> str: ... + def winfo_cells(self) -> int: ... + def winfo_children(self) -> list[Widget]: ... # Widget because it can't be Toplevel or Tk + def winfo_class(self) -> str: ... + def winfo_colormapfull(self) -> bool: ... + def winfo_containing(self, rootX: int, rootY: int, displayof: Literal[0] | Misc | None = 0) -> Misc | None: ... + def winfo_depth(self) -> int: ... + def winfo_exists(self) -> bool: ... + def winfo_fpixels(self, number: _ScreenUnits) -> float: ... + def winfo_geometry(self) -> str: ... + def winfo_height(self) -> int: ... + def winfo_id(self) -> int: ... + def winfo_interps(self, displayof: Literal[0] | Misc | None = 0) -> tuple[str, ...]: ... + def winfo_ismapped(self) -> bool: ... + def winfo_manager(self) -> str: ... + def winfo_name(self) -> str: ... + def winfo_parent(self) -> str: ... # return value needs nametowidget() + def winfo_pathname(self, id: int, displayof: Literal[0] | Misc | None = 0): ... + def winfo_pixels(self, number: _ScreenUnits) -> int: ... + def winfo_pointerx(self) -> int: ... + def winfo_pointerxy(self) -> tuple[int, int]: ... + def winfo_pointery(self) -> int: ... + def winfo_reqheight(self) -> int: ... + def winfo_reqwidth(self) -> int: ... + def winfo_rgb(self, color: str) -> tuple[int, int, int]: ... + def winfo_rootx(self) -> int: ... + def winfo_rooty(self) -> int: ... + def winfo_screen(self) -> str: ... + def winfo_screencells(self) -> int: ... + def winfo_screendepth(self) -> int: ... + def winfo_screenheight(self) -> int: ... + def winfo_screenmmheight(self) -> int: ... + def winfo_screenmmwidth(self) -> int: ... + def winfo_screenvisual(self) -> str: ... + def winfo_screenwidth(self) -> int: ... + def winfo_server(self) -> str: ... + def winfo_toplevel(self) -> Tk | Toplevel: ... + def winfo_viewable(self) -> bool: ... + def winfo_visual(self) -> str: ... + def winfo_visualid(self) -> str: ... + def winfo_visualsavailable(self, includeids: bool = False) -> list[tuple[str, int]]: ... + def winfo_vrootheight(self) -> int: ... + def winfo_vrootwidth(self) -> int: ... + def winfo_vrootx(self) -> int: ... + def winfo_vrooty(self) -> int: ... + def winfo_width(self) -> int: ... + def winfo_x(self) -> int: ... + def winfo_y(self) -> int: ... + def update(self) -> None: ... + def update_idletasks(self) -> None: ... + @overload + def bindtags(self, tagList: None = None) -> tuple[str, ...]: ... + @overload + def bindtags(self, tagList: list[str] | tuple[str, ...]) -> None: ... + # bind with isinstance(func, str) doesn't return anything, but all other + # binds do. The default value of func is not str. + @overload + def bind( + self, + sequence: str | None = None, + func: Callable[[Event[Misc]], object] | None = None, + add: Literal["", "+"] | bool | None = None, + ) -> str: ... + @overload + def bind(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + @overload + def bind(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + # There's no way to know what type of widget bind_all and bind_class + # callbacks will get, so those are Misc. + @overload + def bind_all( + self, + sequence: str | None = None, + func: Callable[[Event[Misc]], object] | None = None, + add: Literal["", "+"] | bool | None = None, + ) -> str: ... + @overload + def bind_all(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + @overload + def bind_all(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + @overload + def bind_class( + self, + className: str, + sequence: str | None = None, + func: Callable[[Event[Misc]], object] | None = None, + add: Literal["", "+"] | bool | None = None, + ) -> str: ... + @overload + def bind_class(self, className: str, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + @overload + def bind_class(self, className: str, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + def unbind(self, sequence: str, funcid: str | None = None) -> None: ... + def unbind_all(self, sequence: str) -> None: ... + def unbind_class(self, className: str, sequence: str) -> None: ... + def mainloop(self, n: int = 0) -> None: ... + def quit(self) -> None: ... + @property + def _windowingsystem(self) -> Literal["win32", "aqua", "x11"]: ... + def nametowidget(self, name: str | Misc | _tkinter.Tcl_Obj) -> Any: ... + def register( + self, func: Callable[..., object], subst: Callable[..., Sequence[Any]] | None = None, needcleanup: int = 1 + ) -> str: ... + def keys(self) -> list[str]: ... + @overload + def pack_propagate(self, flag: bool) -> bool | None: ... + @overload + def pack_propagate(self) -> None: ... + propagate = pack_propagate + def grid_anchor(self, anchor: _Anchor | None = None) -> None: ... + anchor = grid_anchor + @overload + def grid_bbox( + self, column: None = None, row: None = None, col2: None = None, row2: None = None + ) -> tuple[int, int, int, int] | None: ... + @overload + def grid_bbox(self, column: int, row: int, col2: None = None, row2: None = None) -> tuple[int, int, int, int] | None: ... + @overload + def grid_bbox(self, column: int, row: int, col2: int, row2: int) -> tuple[int, int, int, int] | None: ... + bbox = grid_bbox + def grid_columnconfigure( + self, + index: int | str | list[int] | tuple[int, ...], + cnf: _GridIndexInfo = {}, + *, + minsize: _ScreenUnits = ..., + pad: _ScreenUnits = ..., + uniform: str = ..., + weight: int = ..., + ) -> _GridIndexInfo | MaybeNone: ... # can be None but annoying to check + def grid_rowconfigure( + self, + index: int | str | list[int] | tuple[int, ...], + cnf: _GridIndexInfo = {}, + *, + minsize: _ScreenUnits = ..., + pad: _ScreenUnits = ..., + uniform: str = ..., + weight: int = ..., + ) -> _GridIndexInfo | MaybeNone: ... # can be None but annoying to check + columnconfigure = grid_columnconfigure + rowconfigure = grid_rowconfigure + def grid_location(self, x: _ScreenUnits, y: _ScreenUnits) -> tuple[int, int]: ... + @overload + def grid_propagate(self, flag: bool) -> None: ... + @overload + def grid_propagate(self) -> bool: ... + def grid_size(self) -> tuple[int, int]: ... + size = grid_size + # Widget because Toplevel or Tk is never a slave + def pack_slaves(self) -> list[Widget]: ... + def grid_slaves(self, row: int | None = None, column: int | None = None) -> list[Widget]: ... + def place_slaves(self) -> list[Widget]: ... + slaves = pack_slaves + def event_add(self, virtual: str, *sequences: str) -> None: ... + def event_delete(self, virtual: str, *sequences: str) -> None: ... + def event_generate( + self, + sequence: str, + *, + above: Misc | int = ..., + borderwidth: _ScreenUnits = ..., + button: int = ..., + count: int = ..., + data: Any = ..., # anything with usable str() value + delta: int = ..., + detail: str = ..., + focus: bool = ..., + height: _ScreenUnits = ..., + keycode: int = ..., + keysym: str = ..., + mode: str = ..., + override: bool = ..., + place: Literal["PlaceOnTop", "PlaceOnBottom"] = ..., + root: Misc | int = ..., + rootx: _ScreenUnits = ..., + rooty: _ScreenUnits = ..., + sendevent: bool = ..., + serial: int = ..., + state: int | str = ..., + subwindow: Misc | int = ..., + time: int = ..., + warp: bool = ..., + width: _ScreenUnits = ..., + when: Literal["now", "tail", "head", "mark"] = ..., + x: _ScreenUnits = ..., + y: _ScreenUnits = ..., + ) -> None: ... + def event_info(self, virtual: str | None = None) -> tuple[str, ...]: ... + def image_names(self) -> tuple[str, ...]: ... + def image_types(self) -> tuple[str, ...]: ... + # See #4363 and #4891 + def __setitem__(self, key: str, value: Any) -> None: ... + def __getitem__(self, key: str) -> Any: ... + def cget(self, key: str) -> Any: ... + def configure(self, cnf: Any = None) -> Any: ... + # TODO: config is an alias of configure, but adding that here creates + # conflict with the type of config in the subclasses. See #13149 + +class CallWrapper: + func: Incomplete + subst: Incomplete + widget: Incomplete + def __init__(self, func, subst, widget) -> None: ... + def __call__(self, *args): ... + +class XView: + @overload + def xview(self) -> tuple[float, float]: ... + @overload + def xview(self, *args): ... + def xview_moveto(self, fraction: float) -> None: ... + @overload + def xview_scroll(self, number: int, what: Literal["units", "pages"]) -> None: ... + @overload + def xview_scroll(self, number: _ScreenUnits, what: Literal["pixels"]) -> None: ... + +class YView: + @overload + def yview(self) -> tuple[float, float]: ... + @overload + def yview(self, *args): ... + def yview_moveto(self, fraction: float) -> None: ... + @overload + def yview_scroll(self, number: int, what: Literal["units", "pages"]) -> None: ... + @overload + def yview_scroll(self, number: _ScreenUnits, what: Literal["pixels"]) -> None: ... + +if sys.platform == "darwin": + @type_check_only + class _WmAttributes(TypedDict): + alpha: float + fullscreen: bool + modified: bool + notify: bool + titlepath: str + topmost: bool + transparent: bool + type: str # Present, but not actually used on darwin + +elif sys.platform == "win32": + @type_check_only + class _WmAttributes(TypedDict): + alpha: float + transparentcolor: str + disabled: bool + fullscreen: bool + toolwindow: bool + topmost: bool + +else: + # X11 + @type_check_only + class _WmAttributes(TypedDict): + alpha: float + topmost: bool + zoomed: bool + fullscreen: bool + type: str + +class Wm: + @overload + def wm_aspect(self, minNumer: int, minDenom: int, maxNumer: int, maxDenom: int) -> None: ... + @overload + def wm_aspect( + self, minNumer: None = None, minDenom: None = None, maxNumer: None = None, maxDenom: None = None + ) -> tuple[int, int, int, int] | None: ... + aspect = wm_aspect + if sys.version_info >= (3, 13): + @overload + def wm_attributes(self, *, return_python_dict: Literal[False] = False) -> tuple[Any, ...]: ... + @overload + def wm_attributes(self, *, return_python_dict: Literal[True]) -> _WmAttributes: ... + + else: + @overload + def wm_attributes(self) -> tuple[Any, ...]: ... + + @overload + def wm_attributes(self, option: Literal["-alpha"], /) -> float: ... + @overload + def wm_attributes(self, option: Literal["-fullscreen"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["-topmost"], /) -> bool: ... + if sys.platform == "darwin": + @overload + def wm_attributes(self, option: Literal["-modified"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["-notify"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["-titlepath"], /) -> str: ... + @overload + def wm_attributes(self, option: Literal["-transparent"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["-type"], /) -> str: ... + elif sys.platform == "win32": + @overload + def wm_attributes(self, option: Literal["-transparentcolor"], /) -> str: ... + @overload + def wm_attributes(self, option: Literal["-disabled"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["-toolwindow"], /) -> bool: ... + else: + # X11 + @overload + def wm_attributes(self, option: Literal["-zoomed"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["-type"], /) -> str: ... + if sys.version_info >= (3, 13): + @overload + def wm_attributes(self, option: Literal["alpha"], /) -> float: ... + @overload + def wm_attributes(self, option: Literal["fullscreen"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["topmost"], /) -> bool: ... + if sys.platform == "darwin": + @overload + def wm_attributes(self, option: Literal["modified"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["notify"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["titlepath"], /) -> str: ... + @overload + def wm_attributes(self, option: Literal["transparent"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["type"], /) -> str: ... + elif sys.platform == "win32": + @overload + def wm_attributes(self, option: Literal["transparentcolor"], /) -> str: ... + @overload + def wm_attributes(self, option: Literal["disabled"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["toolwindow"], /) -> bool: ... + else: + # X11 + @overload + def wm_attributes(self, option: Literal["zoomed"], /) -> bool: ... + @overload + def wm_attributes(self, option: Literal["type"], /) -> str: ... + + @overload + def wm_attributes(self, option: str, /): ... + @overload + def wm_attributes(self, option: Literal["-alpha"], value: float, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-fullscreen"], value: bool, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-topmost"], value: bool, /) -> Literal[""]: ... + if sys.platform == "darwin": + @overload + def wm_attributes(self, option: Literal["-modified"], value: bool, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-notify"], value: bool, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-titlepath"], value: str, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-transparent"], value: bool, /) -> Literal[""]: ... + elif sys.platform == "win32": + @overload + def wm_attributes(self, option: Literal["-transparentcolor"], value: str, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-disabled"], value: bool, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-toolwindow"], value: bool, /) -> Literal[""]: ... + else: + # X11 + @overload + def wm_attributes(self, option: Literal["-zoomed"], value: bool, /) -> Literal[""]: ... + @overload + def wm_attributes(self, option: Literal["-type"], value: str, /) -> Literal[""]: ... + + @overload + def wm_attributes(self, option: str, value, /, *__other_option_value_pairs: Any) -> Literal[""]: ... + if sys.version_info >= (3, 13): + if sys.platform == "darwin": + @overload + def wm_attributes( + self, + *, + alpha: float = ..., + fullscreen: bool = ..., + modified: bool = ..., + notify: bool = ..., + titlepath: str = ..., + topmost: bool = ..., + transparent: bool = ..., + ) -> None: ... + elif sys.platform == "win32": + @overload + def wm_attributes( + self, + *, + alpha: float = ..., + transparentcolor: str = ..., + disabled: bool = ..., + fullscreen: bool = ..., + toolwindow: bool = ..., + topmost: bool = ..., + ) -> None: ... + else: + # X11 + @overload + def wm_attributes( + self, *, alpha: float = ..., topmost: bool = ..., zoomed: bool = ..., fullscreen: bool = ..., type: str = ... + ) -> None: ... + + attributes = wm_attributes + def wm_client(self, name: str | None = None) -> str: ... + client = wm_client + @overload + def wm_colormapwindows(self) -> list[Misc]: ... + @overload + def wm_colormapwindows(self, wlist: list[Misc] | tuple[Misc, ...], /) -> None: ... + @overload + def wm_colormapwindows(self, first_wlist_item: Misc, /, *other_wlist_items: Misc) -> None: ... + colormapwindows = wm_colormapwindows + def wm_command(self, value: str | None = None) -> str: ... + command = wm_command + # Some of these always return empty string, but return type is set to None to prevent accidentally using it + def wm_deiconify(self) -> None: ... + deiconify = wm_deiconify + def wm_focusmodel(self, model: Literal["active", "passive"] | None = None) -> Literal["active", "passive", ""]: ... + focusmodel = wm_focusmodel + def wm_forget(self, window: Wm) -> None: ... + forget = wm_forget + def wm_frame(self) -> str: ... + frame = wm_frame + @overload + def wm_geometry(self, newGeometry: None = None) -> str: ... + @overload + def wm_geometry(self, newGeometry: str) -> None: ... + geometry = wm_geometry + def wm_grid(self, baseWidth=None, baseHeight=None, widthInc=None, heightInc=None): ... + grid = wm_grid + def wm_group(self, pathName=None): ... + group = wm_group + def wm_iconbitmap(self, bitmap=None, default=None): ... + iconbitmap = wm_iconbitmap + def wm_iconify(self) -> None: ... + iconify = wm_iconify + def wm_iconmask(self, bitmap=None): ... + iconmask = wm_iconmask + def wm_iconname(self, newName=None) -> str: ... + iconname = wm_iconname + def wm_iconphoto(self, default: bool, image1: _PhotoImageLike | str, /, *args: _PhotoImageLike | str) -> None: ... + iconphoto = wm_iconphoto + def wm_iconposition(self, x: int | None = None, y: int | None = None) -> tuple[int, int] | None: ... + iconposition = wm_iconposition + def wm_iconwindow(self, pathName=None): ... + iconwindow = wm_iconwindow + def wm_manage(self, widget) -> None: ... + manage = wm_manage + @overload + def wm_maxsize(self, width: None = None, height: None = None) -> tuple[int, int]: ... + @overload + def wm_maxsize(self, width: int, height: int) -> None: ... + maxsize = wm_maxsize + @overload + def wm_minsize(self, width: None = None, height: None = None) -> tuple[int, int]: ... + @overload + def wm_minsize(self, width: int, height: int) -> None: ... + minsize = wm_minsize + @overload + def wm_overrideredirect(self, boolean: None = None) -> bool | None: ... # returns True or None + @overload + def wm_overrideredirect(self, boolean: bool) -> None: ... + overrideredirect = wm_overrideredirect + def wm_positionfrom(self, who: Literal["program", "user"] | None = None) -> Literal["", "program", "user"]: ... + positionfrom = wm_positionfrom + @overload + def wm_protocol(self, name: str, func: Callable[[], object] | str) -> None: ... + @overload + def wm_protocol(self, name: str, func: None = None) -> str: ... + @overload + def wm_protocol(self, name: None = None, func: None = None) -> tuple[str, ...]: ... + protocol = wm_protocol + @overload + def wm_resizable(self, width: None = None, height: None = None) -> tuple[bool, bool]: ... + @overload + def wm_resizable(self, width: bool, height: bool) -> None: ... + resizable = wm_resizable + def wm_sizefrom(self, who: Literal["program", "user"] | None = None) -> Literal["", "program", "user"]: ... + sizefrom = wm_sizefrom + @overload + def wm_state(self, newstate: None = None) -> str: ... + @overload + def wm_state(self, newstate: str) -> None: ... + state = wm_state + @overload + def wm_title(self, string: None = None) -> str: ... + @overload + def wm_title(self, string: str) -> None: ... + title = wm_title + @overload + def wm_transient(self, master: None = None) -> _tkinter.Tcl_Obj: ... + @overload + def wm_transient(self, master: Wm | _tkinter.Tcl_Obj) -> None: ... + transient = wm_transient + def wm_withdraw(self) -> None: ... + withdraw = wm_withdraw + +class Tk(Misc, Wm): + master: None + def __init__( + # Make sure to keep in sync with other functions that use the same + # args. + # use `git grep screenName` to find them + self, + screenName: str | None = None, + baseName: str | None = None, + className: str = "Tk", + useTk: bool = True, + sync: bool = False, + use: str | None = None, + ) -> None: ... + # Keep this in sync with ttktheme.ThemedTk. See issue #13858 + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + menu: Menu = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + takefocus: _TakeFocusValue = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def destroy(self) -> None: ... + def readprofile(self, baseName: str, className: str) -> None: ... + report_callback_exception: Callable[[type[BaseException], BaseException, TracebackType | None], object] + # Tk has __getattr__ so that tk_instance.foo falls back to tk_instance.tk.foo + # Please keep in sync with _tkinter.TkappType. + # Some methods are intentionally missing because they are inherited from Misc instead. + def adderrorinfo(self, msg, /): ... + def call(self, command: Any, /, *args: Any) -> Any: ... + def createcommand(self, name, func, /): ... + if sys.platform != "win32": + def createfilehandler(self, file, mask, func, /): ... + def deletefilehandler(self, file, /): ... + + def createtimerhandler(self, milliseconds, func, /): ... + def dooneevent(self, flags: int = ..., /): ... + def eval(self, script: str, /) -> str: ... + def evalfile(self, fileName, /): ... + def exprboolean(self, s, /): ... + def exprdouble(self, s, /): ... + def exprlong(self, s, /): ... + def exprstring(self, s, /): ... + def globalgetvar(self, *args, **kwargs): ... + def globalsetvar(self, *args, **kwargs): ... + def globalunsetvar(self, *args, **kwargs): ... + def interpaddr(self) -> int: ... + def loadtk(self) -> None: ... + def record(self, script, /): ... + if sys.version_info < (3, 11): + def split(self, arg, /): ... + + def splitlist(self, arg, /): ... + def unsetvar(self, *args, **kwargs): ... + def wantobjects(self, *args, **kwargs): ... + def willdispatch(self): ... + +def Tcl(screenName: str | None = None, baseName: str | None = None, className: str = "Tk", useTk: bool = False) -> Tk: ... + +_InMiscTotal = TypedDict("_InMiscTotal", {"in": Misc}) +_InMiscNonTotal = TypedDict("_InMiscNonTotal", {"in": Misc}, total=False) + +class _PackInfo(_InMiscTotal): + # 'before' and 'after' never appear in _PackInfo + anchor: _Anchor + expand: bool + fill: Literal["none", "x", "y", "both"] + side: Literal["left", "right", "top", "bottom"] + # Paddings come out as int or tuple of int, even though any _ScreenUnits + # can be specified in pack(). + ipadx: int + ipady: int + padx: int | tuple[int, int] + pady: int | tuple[int, int] + +class Pack: + # _PackInfo is not the valid type for cnf because pad stuff accepts any + # _ScreenUnits instead of int only. I didn't bother to create another + # TypedDict for cnf because it appears to be a legacy thing that was + # replaced by **kwargs. + def pack_configure( + self, + cnf: Mapping[str, Any] | None = {}, + *, + after: Misc = ..., + anchor: _Anchor = ..., + before: Misc = ..., + expand: bool | Literal[0, 1] = 0, + fill: Literal["none", "x", "y", "both"] = ..., + side: Literal["left", "right", "top", "bottom"] = ..., + ipadx: _ScreenUnits = ..., + ipady: _ScreenUnits = ..., + padx: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., + pady: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., + in_: Misc = ..., + **kw: Any, # allow keyword argument named 'in', see #4836 + ) -> None: ... + def pack_forget(self) -> None: ... + def pack_info(self) -> _PackInfo: ... # errors if widget hasn't been packed + pack = pack_configure + forget = pack_forget + propagate = Misc.pack_propagate + +class _PlaceInfo(_InMiscNonTotal): # empty dict if widget hasn't been placed + anchor: _Anchor + bordermode: Literal["inside", "outside", "ignore"] + width: str # can be int()ed (even after e.g. widget.place(height='2.3c') or similar) + height: str # can be int()ed + x: str # can be int()ed + y: str # can be int()ed + relheight: str # can be float()ed if not empty string + relwidth: str # can be float()ed if not empty string + relx: str # can be float()ed if not empty string + rely: str # can be float()ed if not empty string + +class Place: + def place_configure( + self, + cnf: Mapping[str, Any] | None = {}, + *, + anchor: _Anchor = ..., + bordermode: Literal["inside", "outside", "ignore"] = ..., + width: _ScreenUnits = ..., + height: _ScreenUnits = ..., + x: _ScreenUnits = ..., + y: _ScreenUnits = ..., + # str allowed for compatibility with place_info() + relheight: str | float = ..., + relwidth: str | float = ..., + relx: str | float = ..., + rely: str | float = ..., + in_: Misc = ..., + **kw: Any, # allow keyword argument named 'in', see #4836 + ) -> None: ... + def place_forget(self) -> None: ... + def place_info(self) -> _PlaceInfo: ... + place = place_configure + info = place_info + +class _GridInfo(_InMiscNonTotal): # empty dict if widget hasn't been gridded + column: int + columnspan: int + row: int + rowspan: int + ipadx: int + ipady: int + padx: int | tuple[int, int] + pady: int | tuple[int, int] + sticky: str # consists of letters 'n', 's', 'w', 'e', no repeats, may be empty + +class Grid: + def grid_configure( + self, + cnf: Mapping[str, Any] | None = {}, + *, + column: int = ..., + columnspan: int = ..., + row: int = ..., + rowspan: int = ..., + ipadx: _ScreenUnits = ..., + ipady: _ScreenUnits = ..., + padx: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., + pady: _ScreenUnits | tuple[_ScreenUnits, _ScreenUnits] = ..., + sticky: str = ..., # consists of letters 'n', 's', 'w', 'e', may contain repeats, may be empty + in_: Misc = ..., + **kw: Any, # allow keyword argument named 'in', see #4836 + ) -> None: ... + def grid_forget(self) -> None: ... + def grid_remove(self) -> None: ... + def grid_info(self) -> _GridInfo: ... + grid = grid_configure + location = Misc.grid_location + size = Misc.grid_size + +class BaseWidget(Misc): + master: Misc + widgetName: Incomplete + def __init__(self, master, widgetName, cnf={}, kw={}, extra=()) -> None: ... + def destroy(self) -> None: ... + +# This class represents any widget except Toplevel or Tk. +class Widget(BaseWidget, Pack, Place, Grid): + # Allow bind callbacks to take e.g. Event[Label] instead of Event[Misc]. + # Tk and Toplevel get notified for their child widgets' events, but other + # widgets don't. + @overload + def bind( + self: _W, + sequence: str | None = None, + func: Callable[[Event[_W]], object] | None = None, + add: Literal["", "+"] | bool | None = None, + ) -> str: ... + @overload + def bind(self, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + @overload + def bind(self, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + +class Toplevel(BaseWidget, Wm): + # Toplevel and Tk have the same options because they correspond to the same + # Tcl/Tk toplevel widget. For some reason, config and configure must be + # copy/pasted here instead of aliasing as 'config = Tk.config'. + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + background: str = ..., + bd: _ScreenUnits = 0, + bg: str = ..., + border: _ScreenUnits = 0, + borderwidth: _ScreenUnits = 0, + class_: str = "Toplevel", + colormap: Literal["new", ""] | Misc = "", + container: bool = False, + cursor: _Cursor = "", + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + menu: Menu = ..., + name: str = ..., + padx: _ScreenUnits = 0, + pady: _ScreenUnits = 0, + relief: _Relief = "flat", + screen: str = "", # can't be changed after creating widget + takefocus: _TakeFocusValue = 0, + use: int = ..., + visual: str | tuple[str, int] = "", + width: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + menu: Menu = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + takefocus: _TakeFocusValue = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + +class Button(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = "center", + background: str = ..., + bd: _ScreenUnits = ..., # same as borderwidth + bg: str = ..., # same as background + bitmap: str = "", + border: _ScreenUnits = ..., # same as borderwidth + borderwidth: _ScreenUnits = ..., + command: _ButtonCommand = "", + compound: _Compound = "none", + cursor: _Cursor = "", + default: Literal["normal", "active", "disabled"] = "disabled", + disabledforeground: str = ..., + fg: str = ..., # same as foreground + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + # width and height must be int for buttons containing just text, but + # ints are also valid _ScreenUnits + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 1, + image: _ImageSpec = "", + justify: Literal["left", "center", "right"] = "center", + name: str = ..., + overrelief: _Relief | Literal[""] = "", + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + repeatdelay: int = ..., + repeatinterval: int = ..., + state: Literal["normal", "active", "disabled"] = "normal", + takefocus: _TakeFocusValue = "", + text: float | str = "", + # We allow the textvariable to be any Variable, not necessarily + # StringVar. This is useful for e.g. a button that displays the value + # of an IntVar. + textvariable: Variable = ..., + underline: int = -1, + width: _ScreenUnits = 0, + wraplength: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: _ButtonCommand = ..., + compound: _Compound = ..., + cursor: _Cursor = ..., + default: Literal["normal", "active", "disabled"] = ..., + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + image: _ImageSpec = ..., + justify: Literal["left", "center", "right"] = ..., + overrelief: _Relief | Literal[""] = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + repeatdelay: int = ..., + repeatinterval: int = ..., + state: Literal["normal", "active", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + textvariable: Variable = ..., + underline: int = ..., + width: _ScreenUnits = ..., + wraplength: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def flash(self) -> None: ... + def invoke(self) -> Any: ... + +class Canvas(Widget, XView, YView): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + background: str = ..., + bd: _ScreenUnits = 0, + bg: str = ..., + border: _ScreenUnits = 0, + borderwidth: _ScreenUnits = 0, + closeenough: float = 1.0, + confine: bool = True, + cursor: _Cursor = "", + # canvas manual page has a section named COORDINATES, and the first + # part of it describes _ScreenUnits. + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = 0, + insertofftime: int = 300, + insertontime: int = 600, + insertwidth: _ScreenUnits = 2, + name: str = ..., + offset=..., # undocumented + relief: _Relief = "flat", + # Setting scrollregion to None doesn't reset it back to empty, + # but setting it to () does. + scrollregion: tuple[_ScreenUnits, _ScreenUnits, _ScreenUnits, _ScreenUnits] | tuple[()] = (), + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = 1, + selectforeground: str = ..., + # man page says that state can be 'hidden', but it can't + state: Literal["normal", "disabled"] = "normal", + takefocus: _TakeFocusValue = "", + width: _ScreenUnits = ..., + xscrollcommand: _XYScrollCommand = "", + xscrollincrement: _ScreenUnits = 0, + yscrollcommand: _XYScrollCommand = "", + yscrollincrement: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + closeenough: float = ..., + confine: bool = ..., + cursor: _Cursor = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = ..., + insertofftime: int = ..., + insertontime: int = ..., + insertwidth: _ScreenUnits = ..., + offset=..., # undocumented + relief: _Relief = ..., + scrollregion: tuple[_ScreenUnits, _ScreenUnits, _ScreenUnits, _ScreenUnits] | tuple[()] = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + state: Literal["normal", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + width: _ScreenUnits = ..., + xscrollcommand: _XYScrollCommand = ..., + xscrollincrement: _ScreenUnits = ..., + yscrollcommand: _XYScrollCommand = ..., + yscrollincrement: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def addtag(self, *args): ... # internal method + def addtag_above(self, newtag: str, tagOrId: str | int) -> None: ... + def addtag_all(self, newtag: str) -> None: ... + def addtag_below(self, newtag: str, tagOrId: str | int) -> None: ... + def addtag_closest( + self, newtag: str, x: _ScreenUnits, y: _ScreenUnits, halo: _ScreenUnits | None = None, start: str | int | None = None + ) -> None: ... + def addtag_enclosed(self, newtag: str, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: _ScreenUnits) -> None: ... + def addtag_overlapping(self, newtag: str, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: _ScreenUnits) -> None: ... + def addtag_withtag(self, newtag: str, tagOrId: str | int) -> None: ... + def find(self, *args): ... # internal method + def find_above(self, tagOrId: str | int) -> tuple[int, ...]: ... + def find_all(self) -> tuple[int, ...]: ... + def find_below(self, tagOrId: str | int) -> tuple[int, ...]: ... + def find_closest( + self, x: _ScreenUnits, y: _ScreenUnits, halo: _ScreenUnits | None = None, start: str | int | None = None + ) -> tuple[int, ...]: ... + def find_enclosed(self, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: _ScreenUnits) -> tuple[int, ...]: ... + def find_overlapping(self, x1: _ScreenUnits, y1: _ScreenUnits, x2: _ScreenUnits, y2: float) -> tuple[int, ...]: ... + def find_withtag(self, tagOrId: str | int) -> tuple[int, ...]: ... + # Incompatible with Misc.bbox(), tkinter violates LSP + def bbox(self, *args: str | int) -> tuple[int, int, int, int]: ... # type: ignore[override] + @overload + def tag_bind( + self, + tagOrId: str | int, + sequence: str | None = None, + func: Callable[[Event[Canvas]], object] | None = None, + add: Literal["", "+"] | bool | None = None, + ) -> str: ... + @overload + def tag_bind( + self, tagOrId: str | int, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None + ) -> None: ... + @overload + def tag_bind(self, tagOrId: str | int, *, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + def tag_unbind(self, tagOrId: str | int, sequence: str, funcid: str | None = None) -> None: ... + def canvasx(self, screenx, gridspacing=None): ... + def canvasy(self, screeny, gridspacing=None): ... + @overload + def coords(self, tagOrId: str | int, /) -> list[float]: ... + @overload + def coords(self, tagOrId: str | int, args: list[int] | list[float] | tuple[float, ...], /) -> None: ... + @overload + def coords(self, tagOrId: str | int, x1: float, y1: float, /, *args: float) -> None: ... + # create_foo() methods accept coords as a list or tuple, or as separate arguments. + # Lists and tuples can be flat as in [1, 2, 3, 4], or nested as in [(1, 2), (3, 4)]. + # Keyword arguments should be the same in all overloads of each method. + def create_arc(self, *args, **kw) -> int: ... + def create_bitmap(self, *args, **kw) -> int: ... + def create_image(self, *args, **kw) -> int: ... + @overload + def create_line( + self, + x0: float, + y0: float, + x1: float, + y1: float, + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + arrow: Literal["first", "last", "both"] = ..., + arrowshape: tuple[float, float, float] = ..., + capstyle: Literal["round", "projecting", "butt"] = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + joinstyle: Literal["round", "bevel", "miter"] = ..., + offset: _ScreenUnits = ..., + smooth: bool = ..., + splinesteps: float = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_line( + self, + xy_pair_0: tuple[float, float], + xy_pair_1: tuple[float, float], + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + arrow: Literal["first", "last", "both"] = ..., + arrowshape: tuple[float, float, float] = ..., + capstyle: Literal["round", "projecting", "butt"] = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + joinstyle: Literal["round", "bevel", "miter"] = ..., + offset: _ScreenUnits = ..., + smooth: bool = ..., + splinesteps: float = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_line( + self, + coords: ( + tuple[float, float, float, float] + | tuple[tuple[float, float], tuple[float, float]] + | list[int] + | list[float] + | list[tuple[int, int]] + | list[tuple[float, float]] + ), + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + arrow: Literal["first", "last", "both"] = ..., + arrowshape: tuple[float, float, float] = ..., + capstyle: Literal["round", "projecting", "butt"] = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + joinstyle: Literal["round", "bevel", "miter"] = ..., + offset: _ScreenUnits = ..., + smooth: bool = ..., + splinesteps: float = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_oval( + self, + x0: float, + y0: float, + x1: float, + y1: float, + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_oval( + self, + xy_pair_0: tuple[float, float], + xy_pair_1: tuple[float, float], + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_oval( + self, + coords: ( + tuple[float, float, float, float] + | tuple[tuple[float, float], tuple[float, float]] + | list[int] + | list[float] + | list[tuple[int, int]] + | list[tuple[float, float]] + ), + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_polygon( + self, + x0: float, + y0: float, + x1: float, + y1: float, + /, + *xy_pairs: float, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + joinstyle: Literal["round", "bevel", "miter"] = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + smooth: bool = ..., + splinesteps: float = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_polygon( + self, + xy_pair_0: tuple[float, float], + xy_pair_1: tuple[float, float], + /, + *xy_pairs: tuple[float, float], + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + joinstyle: Literal["round", "bevel", "miter"] = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + smooth: bool = ..., + splinesteps: float = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_polygon( + self, + coords: ( + tuple[float, ...] + | tuple[tuple[float, float], ...] + | list[int] + | list[float] + | list[tuple[int, int]] + | list[tuple[float, float]] + ), + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + joinstyle: Literal["round", "bevel", "miter"] = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + smooth: bool = ..., + splinesteps: float = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_rectangle( + self, + x0: float, + y0: float, + x1: float, + y1: float, + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_rectangle( + self, + xy_pair_0: tuple[float, float], + xy_pair_1: tuple[float, float], + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_rectangle( + self, + coords: ( + tuple[float, float, float, float] + | tuple[tuple[float, float], tuple[float, float]] + | list[int] + | list[float] + | list[tuple[int, int]] + | list[tuple[float, float]] + ), + /, + *, + activedash: str | int | list[int] | tuple[int, ...] = ..., + activefill: str = ..., + activeoutline: str = ..., + activeoutlinestipple: str = ..., + activestipple: str = ..., + activewidth: _ScreenUnits = ..., + dash: str | int | list[int] | tuple[int, ...] = ..., + dashoffset: _ScreenUnits = ..., + disableddash: str | int | list[int] | tuple[int, ...] = ..., + disabledfill: str = ..., + disabledoutline: str = ..., + disabledoutlinestipple: str = ..., + disabledstipple: str = ..., + disabledwidth: _ScreenUnits = ..., + fill: str = ..., + offset: _ScreenUnits = ..., + outline: str = ..., + outlineoffset: _ScreenUnits = ..., + outlinestipple: str = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_text( + self, + x: float, + y: float, + /, + *, + activefill: str = ..., + activestipple: str = ..., + anchor: _Anchor = ..., + angle: float | str = ..., + disabledfill: str = ..., + disabledstipple: str = ..., + fill: str = ..., + font: _FontDescription = ..., + justify: Literal["left", "center", "right"] = ..., + offset: _ScreenUnits = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + text: float | str = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_text( + self, + coords: tuple[float, float] | list[int] | list[float], + /, + *, + activefill: str = ..., + activestipple: str = ..., + anchor: _Anchor = ..., + angle: float | str = ..., + disabledfill: str = ..., + disabledstipple: str = ..., + fill: str = ..., + font: _FontDescription = ..., + justify: Literal["left", "center", "right"] = ..., + offset: _ScreenUnits = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + stipple: str = ..., + tags: str | list[str] | tuple[str, ...] = ..., + text: float | str = ..., + width: _ScreenUnits = ..., + ) -> int: ... + @overload + def create_window( + self, + x: float, + y: float, + /, + *, + anchor: _Anchor = ..., + height: _ScreenUnits = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + window: Widget = ..., + ) -> int: ... + @overload + def create_window( + self, + coords: tuple[float, float] | list[int] | list[float], + /, + *, + anchor: _Anchor = ..., + height: _ScreenUnits = ..., + state: Literal["normal", "hidden", "disabled"] = ..., + tags: str | list[str] | tuple[str, ...] = ..., + width: _ScreenUnits = ..., + window: Widget = ..., + ) -> int: ... + def dchars(self, *args) -> None: ... + def delete(self, *tagsOrCanvasIds: str | int) -> None: ... + @overload + def dtag(self, tag: str, tag_to_delete: str | None = ..., /) -> None: ... + @overload + def dtag(self, id: int, tag_to_delete: str, /) -> None: ... + def focus(self, *args): ... + def gettags(self, tagOrId: str | int, /) -> tuple[str, ...]: ... + def icursor(self, *args) -> None: ... + def index(self, *args): ... + def insert(self, *args) -> None: ... + def itemcget(self, tagOrId, option): ... + # itemconfigure kwargs depend on item type, which is not known when type checking + def itemconfigure( + self, tagOrId: str | int, cnf: dict[str, Any] | None = None, **kw: Any + ) -> dict[str, tuple[str, str, str, str, str]] | None: ... + itemconfig = itemconfigure + def move(self, *args) -> None: ... + def moveto(self, tagOrId: str | int, x: Literal[""] | float = "", y: Literal[""] | float = "") -> None: ... + def postscript(self, cnf={}, **kw): ... + # tkinter does: + # lower = tag_lower + # lift = tkraise = tag_raise + # + # But mypy doesn't like aliasing here (maybe because Misc defines the same names) + def tag_lower(self, first: str | int, second: str | int | None = ..., /) -> None: ... + def lower(self, first: str | int, second: str | int | None = ..., /) -> None: ... # type: ignore[override] + def tag_raise(self, first: str | int, second: str | int | None = ..., /) -> None: ... + def tkraise(self, first: str | int, second: str | int | None = ..., /) -> None: ... # type: ignore[override] + def lift(self, first: str | int, second: str | int | None = ..., /) -> None: ... # type: ignore[override] + def scale( + self, tagOrId: str | int, xOrigin: _ScreenUnits, yOrigin: _ScreenUnits, xScale: float, yScale: float, / + ) -> None: ... + def scan_mark(self, x, y) -> None: ... + def scan_dragto(self, x, y, gain: int = 10) -> None: ... + def select_adjust(self, tagOrId, index) -> None: ... + def select_clear(self) -> None: ... + def select_from(self, tagOrId, index) -> None: ... + def select_item(self): ... + def select_to(self, tagOrId, index) -> None: ... + def type(self, tagOrId: str | int) -> int | None: ... + +class Checkbutton(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = "center", + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = "", + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: _ButtonCommand = "", + compound: _Compound = "none", + cursor: _Cursor = "", + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 1, + image: _ImageSpec = "", + indicatoron: bool = True, + justify: Literal["left", "center", "right"] = "center", + name: str = ..., + offrelief: _Relief = ..., + # The checkbutton puts a value to its variable when it's checked or + # unchecked. We don't restrict the type of that value here, so + # Any-typing is fine. + # + # I think Checkbutton shouldn't be generic, because then specifying + # "any checkbutton regardless of what variable it uses" would be + # difficult, and we might run into issues just like how list[float] + # and list[int] are incompatible. Also, we would need a way to + # specify "Checkbutton not associated with any variable", which is + # done by setting variable to empty string (the default). + offvalue: Any = 0, + onvalue: Any = 1, + overrelief: _Relief | Literal[""] = "", + padx: _ScreenUnits = 1, + pady: _ScreenUnits = 1, + relief: _Relief = "flat", + selectcolor: str = ..., + selectimage: _ImageSpec = "", + state: Literal["normal", "active", "disabled"] = "normal", + takefocus: _TakeFocusValue = "", + text: float | str = "", + textvariable: Variable = ..., + tristateimage: _ImageSpec = "", + tristatevalue: Any = "", + underline: int = -1, + variable: Variable | Literal[""] = ..., + width: _ScreenUnits = 0, + wraplength: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: _ButtonCommand = ..., + compound: _Compound = ..., + cursor: _Cursor = ..., + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + justify: Literal["left", "center", "right"] = ..., + offrelief: _Relief = ..., + offvalue: Any = ..., + onvalue: Any = ..., + overrelief: _Relief | Literal[""] = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + selectcolor: str = ..., + selectimage: _ImageSpec = ..., + state: Literal["normal", "active", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + textvariable: Variable = ..., + tristateimage: _ImageSpec = ..., + tristatevalue: Any = ..., + underline: int = ..., + variable: Variable | Literal[""] = ..., + width: _ScreenUnits = ..., + wraplength: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def deselect(self) -> None: ... + def flash(self) -> None: ... + def invoke(self) -> Any: ... + def select(self) -> None: ... + def toggle(self) -> None: ... + +class Entry(Widget, XView): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = "xterm", + disabledbackground: str = ..., + disabledforeground: str = ..., + exportselection: bool = True, + fg: str = ..., + font: _FontDescription = "TkTextFont", + foreground: str = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = 0, + insertofftime: int = 300, + insertontime: int = 600, + insertwidth: _ScreenUnits = ..., + invalidcommand: _EntryValidateCommand = "", + invcmd: _EntryValidateCommand = "", # same as invalidcommand + justify: Literal["left", "center", "right"] = "left", + name: str = ..., + readonlybackground: str = ..., + relief: _Relief = "sunken", + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + show: str = "", + state: Literal["normal", "disabled", "readonly"] = "normal", + takefocus: _TakeFocusValue = "", + textvariable: Variable = ..., + validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = "none", + validatecommand: _EntryValidateCommand = "", + vcmd: _EntryValidateCommand = "", # same as validatecommand + width: int = 20, + xscrollcommand: _XYScrollCommand = "", + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + disabledbackground: str = ..., + disabledforeground: str = ..., + exportselection: bool = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = ..., + insertofftime: int = ..., + insertontime: int = ..., + insertwidth: _ScreenUnits = ..., + invalidcommand: _EntryValidateCommand = ..., + invcmd: _EntryValidateCommand = ..., + justify: Literal["left", "center", "right"] = ..., + readonlybackground: str = ..., + relief: _Relief = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + show: str = ..., + state: Literal["normal", "disabled", "readonly"] = ..., + takefocus: _TakeFocusValue = ..., + textvariable: Variable = ..., + validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = ..., + validatecommand: _EntryValidateCommand = ..., + vcmd: _EntryValidateCommand = ..., + width: int = ..., + xscrollcommand: _XYScrollCommand = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def delete(self, first: str | int, last: str | int | None = None) -> None: ... + def get(self) -> str: ... + def icursor(self, index: str | int) -> None: ... + def index(self, index: str | int) -> int: ... + def insert(self, index: str | int, string: str) -> None: ... + def scan_mark(self, x) -> None: ... + def scan_dragto(self, x) -> None: ... + def selection_adjust(self, index: str | int) -> None: ... + def selection_clear(self) -> None: ... # type: ignore[override] + def selection_from(self, index: str | int) -> None: ... + def selection_present(self) -> bool: ... + def selection_range(self, start: str | int, end: str | int) -> None: ... + def selection_to(self, index: str | int) -> None: ... + select_adjust = selection_adjust + select_clear = selection_clear + select_from = selection_from + select_present = selection_present + select_range = selection_range + select_to = selection_to + +class Frame(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + background: str = ..., + bd: _ScreenUnits = 0, + bg: str = ..., + border: _ScreenUnits = 0, + borderwidth: _ScreenUnits = 0, + class_: str = "Frame", # can't be changed with configure() + colormap: Literal["new", ""] | Misc = "", # can't be changed with configure() + container: bool = False, # can't be changed with configure() + cursor: _Cursor = "", + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + name: str = ..., + padx: _ScreenUnits = 0, + pady: _ScreenUnits = 0, + relief: _Relief = "flat", + takefocus: _TakeFocusValue = 0, + visual: str | tuple[str, int] = "", # can't be changed with configure() + width: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + takefocus: _TakeFocusValue = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + +class Label(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = "center", + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = "", + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + compound: _Compound = "none", + cursor: _Cursor = "", + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + image: _ImageSpec = "", + justify: Literal["left", "center", "right"] = "center", + name: str = ..., + padx: _ScreenUnits = 1, + pady: _ScreenUnits = 1, + relief: _Relief = "flat", + state: Literal["normal", "active", "disabled"] = "normal", + takefocus: _TakeFocusValue = 0, + text: float | str = "", + textvariable: Variable = ..., + underline: int = -1, + width: _ScreenUnits = 0, + wraplength: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + compound: _Compound = ..., + cursor: _Cursor = ..., + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + image: _ImageSpec = ..., + justify: Literal["left", "center", "right"] = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + state: Literal["normal", "active", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + textvariable: Variable = ..., + underline: int = ..., + width: _ScreenUnits = ..., + wraplength: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + +class Listbox(Widget, XView, YView): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activestyle: Literal["dotbox", "none", "underline"] = ..., + background: str = ..., + bd: _ScreenUnits = 1, + bg: str = ..., + border: _ScreenUnits = 1, + borderwidth: _ScreenUnits = 1, + cursor: _Cursor = "", + disabledforeground: str = ..., + exportselection: bool | Literal[0, 1] = 1, + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: int = 10, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + justify: Literal["left", "center", "right"] = "left", + # There's no tkinter.ListVar, but seems like bare tkinter.Variable + # actually works for this: + # + # >>> import tkinter + # >>> lb = tkinter.Listbox() + # >>> var = lb['listvariable'] = tkinter.Variable() + # >>> var.set(['foo', 'bar', 'baz']) + # >>> lb.get(0, 'end') + # ('foo', 'bar', 'baz') + listvariable: Variable = ..., + name: str = ..., + relief: _Relief = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = 0, + selectforeground: str = ..., + # from listbox man page: "The value of the [selectmode] option may be + # arbitrary, but the default bindings expect it to be either single, + # browse, multiple, or extended" + # + # I have never seen anyone setting this to something else than what + # "the default bindings expect", but let's support it anyway. + selectmode: str | Literal["single", "browse", "multiple", "extended"] = "browse", # noqa: Y051 + setgrid: bool = False, + state: Literal["normal", "disabled"] = "normal", + takefocus: _TakeFocusValue = "", + width: int = 20, + xscrollcommand: _XYScrollCommand = "", + yscrollcommand: _XYScrollCommand = "", + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activestyle: Literal["dotbox", "none", "underline"] = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + disabledforeground: str = ..., + exportselection: bool = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: int = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + justify: Literal["left", "center", "right"] = ..., + listvariable: Variable = ..., + relief: _Relief = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + selectmode: str | Literal["single", "browse", "multiple", "extended"] = ..., # noqa: Y051 + setgrid: bool = ..., + state: Literal["normal", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + width: int = ..., + xscrollcommand: _XYScrollCommand = ..., + yscrollcommand: _XYScrollCommand = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def activate(self, index: str | int) -> None: ... + def bbox(self, index: str | int) -> tuple[int, int, int, int] | None: ... # type: ignore[override] + def curselection(self): ... + def delete(self, first: str | int, last: str | int | None = None) -> None: ... + def get(self, first: str | int, last: str | int | None = None): ... + def index(self, index: str | int) -> int: ... + def insert(self, index: str | int, *elements: str | float) -> None: ... + def nearest(self, y): ... + def scan_mark(self, x, y) -> None: ... + def scan_dragto(self, x, y) -> None: ... + def see(self, index: str | int) -> None: ... + def selection_anchor(self, index: str | int) -> None: ... + select_anchor = selection_anchor + def selection_clear(self, first: str | int, last: str | int | None = None) -> None: ... # type: ignore[override] + select_clear = selection_clear + def selection_includes(self, index: str | int): ... + select_includes = selection_includes + def selection_set(self, first: str | int, last: str | int | None = None) -> None: ... + select_set = selection_set + def size(self) -> int: ... # type: ignore[override] + def itemcget(self, index: str | int, option): ... + def itemconfigure(self, index: str | int, cnf=None, **kw): ... + itemconfig = itemconfigure + +class Menu(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activeborderwidth: _ScreenUnits = ..., + activeforeground: str = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = "arrow", + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + name: str = ..., + postcommand: Callable[[], object] | str = "", + relief: _Relief = ..., + selectcolor: str = ..., + takefocus: _TakeFocusValue = 0, + tearoff: bool | Literal[0, 1] = 1, + # I guess tearoffcommand arguments are supposed to be widget objects, + # but they are widget name strings. Use nametowidget() to handle the + # arguments of tearoffcommand. + tearoffcommand: Callable[[str, str], object] | str = "", + title: str = "", + type: Literal["menubar", "tearoff", "normal"] = "normal", + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activeborderwidth: _ScreenUnits = ..., + activeforeground: str = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + postcommand: Callable[[], object] | str = ..., + relief: _Relief = ..., + selectcolor: str = ..., + takefocus: _TakeFocusValue = ..., + tearoff: bool = ..., + tearoffcommand: Callable[[str, str], object] | str = ..., + title: str = ..., + type: Literal["menubar", "tearoff", "normal"] = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def tk_popup(self, x: int, y: int, entry: str | int = "") -> None: ... + def activate(self, index: str | int) -> None: ... + def add(self, itemType, cnf={}, **kw): ... # docstring says "Internal function." + def insert(self, index, itemType, cnf={}, **kw): ... # docstring says "Internal function." + def add_cascade( + self, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + label: str = ..., + menu: Menu = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + ) -> None: ... + def add_checkbutton( + self, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + label: str = ..., + offvalue: Any = ..., + onvalue: Any = ..., + selectcolor: str = ..., + selectimage: _ImageSpec = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + variable: Variable = ..., + ) -> None: ... + def add_command( + self, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + label: str = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + ) -> None: ... + def add_radiobutton( + self, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + label: str = ..., + selectcolor: str = ..., + selectimage: _ImageSpec = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + value: Any = ..., + variable: Variable = ..., + ) -> None: ... + def add_separator(self, cnf: dict[str, Any] | None = {}, *, background: str = ...) -> None: ... + def insert_cascade( + self, + index: str | int, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + label: str = ..., + menu: Menu = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + ) -> None: ... + def insert_checkbutton( + self, + index: str | int, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + label: str = ..., + offvalue: Any = ..., + onvalue: Any = ..., + selectcolor: str = ..., + selectimage: _ImageSpec = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + variable: Variable = ..., + ) -> None: ... + def insert_command( + self, + index: str | int, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + label: str = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + ) -> None: ... + def insert_radiobutton( + self, + index: str | int, + cnf: dict[str, Any] | None = {}, + *, + accelerator: str = ..., + activebackground: str = ..., + activeforeground: str = ..., + background: str = ..., + bitmap: str = ..., + columnbreak: int = ..., + command: Callable[[], object] | str = ..., + compound: _Compound = ..., + font: _FontDescription = ..., + foreground: str = ..., + hidemargin: bool = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + label: str = ..., + selectcolor: str = ..., + selectimage: _ImageSpec = ..., + state: Literal["normal", "active", "disabled"] = ..., + underline: int = ..., + value: Any = ..., + variable: Variable = ..., + ) -> None: ... + def insert_separator(self, index: str | int, cnf: dict[str, Any] | None = {}, *, background: str = ...) -> None: ... + def delete(self, index1: str | int, index2: str | int | None = None) -> None: ... + def entrycget(self, index: str | int, option: str) -> Any: ... + def entryconfigure( + self, index: str | int, cnf: dict[str, Any] | None = None, **kw: Any + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + entryconfig = entryconfigure + def index(self, index: str | int) -> int | None: ... + def invoke(self, index: str | int) -> Any: ... + def post(self, x: int, y: int) -> None: ... + def type(self, index: str | int) -> Literal["cascade", "checkbutton", "command", "radiobutton", "separator"]: ... + def unpost(self) -> None: ... + def xposition(self, index: str | int) -> int: ... + def yposition(self, index: str | int) -> int: ... + +class Menubutton(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = "", + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + compound: _Compound = "none", + cursor: _Cursor = "", + direction: Literal["above", "below", "left", "right", "flush"] = "below", + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + image: _ImageSpec = "", + indicatoron: bool = ..., + justify: Literal["left", "center", "right"] = ..., + menu: Menu = ..., + name: str = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = "flat", + state: Literal["normal", "active", "disabled"] = "normal", + takefocus: _TakeFocusValue = 0, + text: float | str = "", + textvariable: Variable = ..., + underline: int = -1, + width: _ScreenUnits = 0, + wraplength: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + compound: _Compound = ..., + cursor: _Cursor = ..., + direction: Literal["above", "below", "left", "right", "flush"] = ..., + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + justify: Literal["left", "center", "right"] = ..., + menu: Menu = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + state: Literal["normal", "active", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + textvariable: Variable = ..., + underline: int = ..., + width: _ScreenUnits = ..., + wraplength: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + +class Message(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + anchor: _Anchor = "center", + aspect: int = 150, + background: str = ..., + bd: _ScreenUnits = 1, + bg: str = ..., + border: _ScreenUnits = 1, + borderwidth: _ScreenUnits = 1, + cursor: _Cursor = "", + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + justify: Literal["left", "center", "right"] = "left", + name: str = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = "flat", + takefocus: _TakeFocusValue = 0, + text: float | str = "", + textvariable: Variable = ..., + # there's width but no height + width: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + anchor: _Anchor = ..., + aspect: int = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + justify: Literal["left", "center", "right"] = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + textvariable: Variable = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + +class Radiobutton(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = "center", + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = "", + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: _ButtonCommand = "", + compound: _Compound = "none", + cursor: _Cursor = "", + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 1, + image: _ImageSpec = "", + indicatoron: bool = True, + justify: Literal["left", "center", "right"] = "center", + name: str = ..., + offrelief: _Relief = ..., + overrelief: _Relief | Literal[""] = "", + padx: _ScreenUnits = 1, + pady: _ScreenUnits = 1, + relief: _Relief = "flat", + selectcolor: str = ..., + selectimage: _ImageSpec = "", + state: Literal["normal", "active", "disabled"] = "normal", + takefocus: _TakeFocusValue = "", + text: float | str = "", + textvariable: Variable = ..., + tristateimage: _ImageSpec = "", + tristatevalue: Any = "", + underline: int = -1, + value: Any = "", + variable: Variable | Literal[""] = ..., + width: _ScreenUnits = 0, + wraplength: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activeforeground: str = ..., + anchor: _Anchor = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bitmap: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: _ButtonCommand = ..., + compound: _Compound = ..., + cursor: _Cursor = ..., + disabledforeground: str = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + image: _ImageSpec = ..., + indicatoron: bool = ..., + justify: Literal["left", "center", "right"] = ..., + offrelief: _Relief = ..., + overrelief: _Relief | Literal[""] = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + selectcolor: str = ..., + selectimage: _ImageSpec = ..., + state: Literal["normal", "active", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + textvariable: Variable = ..., + tristateimage: _ImageSpec = ..., + tristatevalue: Any = ..., + underline: int = ..., + value: Any = ..., + variable: Variable | Literal[""] = ..., + width: _ScreenUnits = ..., + wraplength: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def deselect(self) -> None: ... + def flash(self) -> None: ... + def invoke(self) -> Any: ... + def select(self) -> None: ... + +class Scale(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + background: str = ..., + bd: _ScreenUnits = 1, + bg: str = ..., + bigincrement: float = 0.0, + border: _ScreenUnits = 1, + borderwidth: _ScreenUnits = 1, + # don't know why the callback gets string instead of float + command: str | Callable[[str], object] = "", + cursor: _Cursor = "", + digits: int = 0, + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + from_: float = 0.0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + label: str = "", + length: _ScreenUnits = 100, + name: str = ..., + orient: Literal["horizontal", "vertical"] = "vertical", + relief: _Relief = "flat", + repeatdelay: int = 300, + repeatinterval: int = 100, + resolution: float = 1.0, + showvalue: bool = True, + sliderlength: _ScreenUnits = 30, + sliderrelief: _Relief = "raised", + state: Literal["normal", "active", "disabled"] = "normal", + takefocus: _TakeFocusValue = "", + tickinterval: float = 0.0, + to: float = 100.0, + troughcolor: str = ..., + variable: IntVar | DoubleVar = ..., + width: _ScreenUnits = 15, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + bigincrement: float = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: str | Callable[[str], object] = ..., + cursor: _Cursor = ..., + digits: int = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + from_: float = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + label: str = ..., + length: _ScreenUnits = ..., + orient: Literal["horizontal", "vertical"] = ..., + relief: _Relief = ..., + repeatdelay: int = ..., + repeatinterval: int = ..., + resolution: float = ..., + showvalue: bool = ..., + sliderlength: _ScreenUnits = ..., + sliderrelief: _Relief = ..., + state: Literal["normal", "active", "disabled"] = ..., + takefocus: _TakeFocusValue = ..., + tickinterval: float = ..., + to: float = ..., + troughcolor: str = ..., + variable: IntVar | DoubleVar = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def get(self) -> float: ... + def set(self, value) -> None: ... + def coords(self, value: float | None = None) -> tuple[int, int]: ... + def identify(self, x, y) -> Literal["", "slider", "trough1", "trough2"]: ... + +class Scrollbar(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + activerelief: _Relief = "raised", + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + # There are many ways how the command may get called. Search for + # 'SCROLLING COMMANDS' in scrollbar man page. There doesn't seem to + # be any way to specify an overloaded callback function, so we say + # that it can take any args while it can't in reality. + command: Callable[..., tuple[float, float] | None] | str = "", + cursor: _Cursor = "", + elementborderwidth: _ScreenUnits = -1, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + jump: bool = False, + name: str = ..., + orient: Literal["horizontal", "vertical"] = "vertical", + relief: _Relief = ..., + repeatdelay: int = 300, + repeatinterval: int = 100, + takefocus: _TakeFocusValue = "", + troughcolor: str = ..., + width: _ScreenUnits = ..., + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + activerelief: _Relief = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + command: Callable[..., tuple[float, float] | None] | str = ..., + cursor: _Cursor = ..., + elementborderwidth: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + jump: bool = ..., + orient: Literal["horizontal", "vertical"] = ..., + relief: _Relief = ..., + repeatdelay: int = ..., + repeatinterval: int = ..., + takefocus: _TakeFocusValue = ..., + troughcolor: str = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def activate(self, index=None): ... + def delta(self, deltax: int, deltay: int) -> float: ... + def fraction(self, x: int, y: int) -> float: ... + def identify(self, x: int, y: int) -> Literal["arrow1", "arrow2", "slider", "trough1", "trough2", ""]: ... + def get(self) -> tuple[float, float, float, float] | tuple[float, float]: ... + def set(self, first: float | str, last: float | str) -> None: ... + +_TextIndex: TypeAlias = _tkinter.Tcl_Obj | str | float | Misc +_WhatToCount: TypeAlias = Literal[ + "chars", "displaychars", "displayindices", "displaylines", "indices", "lines", "xpixels", "ypixels" +] + +class Text(Widget, XView, YView): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + autoseparators: bool = True, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + blockcursor: bool = False, + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = "xterm", + endline: int | Literal[""] = "", + exportselection: bool = True, + fg: str = ..., + font: _FontDescription = "TkFixedFont", + foreground: str = ..., + # width is always int, but height is allowed to be ScreenUnits. + # This doesn't make any sense to me, and this isn't documented. + # The docs seem to say that both should be integers. + height: _ScreenUnits = 24, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + inactiveselectbackground: str = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = 0, + insertofftime: int = 300, + insertontime: int = 600, + insertunfocussed: Literal["none", "hollow", "solid"] = "none", + insertwidth: _ScreenUnits = ..., + maxundo: int = 0, + name: str = ..., + padx: _ScreenUnits = 1, + pady: _ScreenUnits = 1, + relief: _Relief = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + setgrid: bool = False, + spacing1: _ScreenUnits = 0, + spacing2: _ScreenUnits = 0, + spacing3: _ScreenUnits = 0, + startline: int | Literal[""] = "", + state: Literal["normal", "disabled"] = "normal", + # Literal inside Tuple doesn't actually work + tabs: _ScreenUnits | str | tuple[_ScreenUnits | str, ...] = "", + tabstyle: Literal["tabular", "wordprocessor"] = "tabular", + takefocus: _TakeFocusValue = "", + undo: bool = False, + width: int = 80, + wrap: Literal["none", "char", "word"] = "char", + xscrollcommand: _XYScrollCommand = "", + yscrollcommand: _XYScrollCommand = "", + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + autoseparators: bool = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + blockcursor: bool = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + endline: int | Literal[""] = ..., + exportselection: bool = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + inactiveselectbackground: str = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = ..., + insertofftime: int = ..., + insertontime: int = ..., + insertunfocussed: Literal["none", "hollow", "solid"] = ..., + insertwidth: _ScreenUnits = ..., + maxundo: int = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + setgrid: bool = ..., + spacing1: _ScreenUnits = ..., + spacing2: _ScreenUnits = ..., + spacing3: _ScreenUnits = ..., + startline: int | Literal[""] = ..., + state: Literal["normal", "disabled"] = ..., + tabs: _ScreenUnits | str | tuple[_ScreenUnits | str, ...] = ..., + tabstyle: Literal["tabular", "wordprocessor"] = ..., + takefocus: _TakeFocusValue = ..., + undo: bool = ..., + width: int = ..., + wrap: Literal["none", "char", "word"] = ..., + xscrollcommand: _XYScrollCommand = ..., + yscrollcommand: _XYScrollCommand = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def bbox(self, index: _TextIndex) -> tuple[int, int, int, int] | None: ... # type: ignore[override] + def compare(self, index1: _TextIndex, op: Literal["<", "<=", "==", ">=", ">", "!="], index2: _TextIndex) -> bool: ... + if sys.version_info >= (3, 13): + @overload + def count(self, index1: _TextIndex, index2: _TextIndex, *, return_ints: Literal[True]) -> int: ... + @overload + def count( + self, index1: _TextIndex, index2: _TextIndex, arg: _WhatToCount | Literal["update"], /, *, return_ints: Literal[True] + ) -> int: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: Literal["update"], + arg2: _WhatToCount, + /, + *, + return_ints: Literal[True], + ) -> int: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: _WhatToCount, + arg2: Literal["update"], + /, + *, + return_ints: Literal[True], + ) -> int: ... + @overload + def count( + self, index1: _TextIndex, index2: _TextIndex, arg1: _WhatToCount, arg2: _WhatToCount, /, *, return_ints: Literal[True] + ) -> tuple[int, int]: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: _WhatToCount | Literal["update"], + arg2: _WhatToCount | Literal["update"], + arg3: _WhatToCount | Literal["update"], + /, + *args: _WhatToCount | Literal["update"], + return_ints: Literal[True], + ) -> tuple[int, ...]: ... + @overload + def count(self, index1: _TextIndex, index2: _TextIndex, *, return_ints: Literal[False] = False) -> tuple[int] | None: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg: _WhatToCount | Literal["update"], + /, + *, + return_ints: Literal[False] = False, + ) -> tuple[int] | None: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: Literal["update"], + arg2: _WhatToCount, + /, + *, + return_ints: Literal[False] = False, + ) -> int | None: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: _WhatToCount, + arg2: Literal["update"], + /, + *, + return_ints: Literal[False] = False, + ) -> int | None: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: _WhatToCount, + arg2: _WhatToCount, + /, + *, + return_ints: Literal[False] = False, + ) -> tuple[int, int]: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: _WhatToCount | Literal["update"], + arg2: _WhatToCount | Literal["update"], + arg3: _WhatToCount | Literal["update"], + /, + *args: _WhatToCount | Literal["update"], + return_ints: Literal[False] = False, + ) -> tuple[int, ...]: ... + else: + @overload + def count(self, index1: _TextIndex, index2: _TextIndex) -> tuple[int] | None: ... + @overload + def count( + self, index1: _TextIndex, index2: _TextIndex, arg: _WhatToCount | Literal["update"], / + ) -> tuple[int] | None: ... + @overload + def count(self, index1: _TextIndex, index2: _TextIndex, arg1: Literal["update"], arg2: _WhatToCount, /) -> int | None: ... + @overload + def count(self, index1: _TextIndex, index2: _TextIndex, arg1: _WhatToCount, arg2: Literal["update"], /) -> int | None: ... + @overload + def count(self, index1: _TextIndex, index2: _TextIndex, arg1: _WhatToCount, arg2: _WhatToCount, /) -> tuple[int, int]: ... + @overload + def count( + self, + index1: _TextIndex, + index2: _TextIndex, + arg1: _WhatToCount | Literal["update"], + arg2: _WhatToCount | Literal["update"], + arg3: _WhatToCount | Literal["update"], + /, + *args: _WhatToCount | Literal["update"], + ) -> tuple[int, ...]: ... + + @overload + def debug(self, boolean: None = None) -> bool: ... + @overload + def debug(self, boolean: bool) -> None: ... + def delete(self, index1: _TextIndex, index2: _TextIndex | None = None) -> None: ... + def dlineinfo(self, index: _TextIndex) -> tuple[int, int, int, int, int] | None: ... + @overload + def dump( + self, + index1: _TextIndex, + index2: _TextIndex | None = None, + command: None = None, + *, + all: bool = ..., + image: bool = ..., + mark: bool = ..., + tag: bool = ..., + text: bool = ..., + window: bool = ..., + ) -> list[tuple[str, str, str]]: ... + @overload + def dump( + self, + index1: _TextIndex, + index2: _TextIndex | None, + command: Callable[[str, str, str], object] | str, + *, + all: bool = ..., + image: bool = ..., + mark: bool = ..., + tag: bool = ..., + text: bool = ..., + window: bool = ..., + ) -> None: ... + @overload + def dump( + self, + index1: _TextIndex, + index2: _TextIndex | None = None, + *, + command: Callable[[str, str, str], object] | str, + all: bool = ..., + image: bool = ..., + mark: bool = ..., + tag: bool = ..., + text: bool = ..., + window: bool = ..., + ) -> None: ... + def edit(self, *args): ... # docstring says "Internal method" + @overload + def edit_modified(self, arg: None = None) -> bool: ... # actually returns Literal[0, 1] + @overload + def edit_modified(self, arg: bool) -> None: ... # actually returns empty string + def edit_redo(self) -> None: ... # actually returns empty string + def edit_reset(self) -> None: ... # actually returns empty string + def edit_separator(self) -> None: ... # actually returns empty string + def edit_undo(self) -> None: ... # actually returns empty string + def get(self, index1: _TextIndex, index2: _TextIndex | None = None) -> str: ... + @overload + def image_cget(self, index: _TextIndex, option: Literal["image", "name"]) -> str: ... + @overload + def image_cget(self, index: _TextIndex, option: Literal["padx", "pady"]) -> int: ... + @overload + def image_cget(self, index: _TextIndex, option: Literal["align"]) -> Literal["baseline", "bottom", "center", "top"]: ... + @overload + def image_cget(self, index: _TextIndex, option: str) -> Any: ... + @overload + def image_configure(self, index: _TextIndex, cnf: str) -> tuple[str, str, str, str, str | int]: ... + @overload + def image_configure( + self, + index: _TextIndex, + cnf: dict[str, Any] | None = {}, + *, + align: Literal["baseline", "bottom", "center", "top"] = ..., + image: _ImageSpec = ..., + name: str = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, str, str | int]] | None: ... + def image_create( + self, + index: _TextIndex, + cnf: dict[str, Any] | None = {}, + *, + align: Literal["baseline", "bottom", "center", "top"] = ..., + image: _ImageSpec = ..., + name: str = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + ) -> str: ... + def image_names(self) -> tuple[str, ...]: ... + def index(self, index: _TextIndex) -> str: ... + def insert(self, index: _TextIndex, chars: str, *args: str | list[str] | tuple[str, ...]) -> None: ... + @overload + def mark_gravity(self, markName: str, direction: None = None) -> Literal["left", "right"]: ... + @overload + def mark_gravity(self, markName: str, direction: Literal["left", "right"]) -> None: ... # actually returns empty string + def mark_names(self) -> tuple[str, ...]: ... + def mark_set(self, markName: str, index: _TextIndex) -> None: ... + def mark_unset(self, *markNames: str) -> None: ... + def mark_next(self, index: _TextIndex) -> str | None: ... + def mark_previous(self, index: _TextIndex) -> str | None: ... + # **kw of peer_create is same as the kwargs of Text.__init__ + def peer_create(self, newPathName: str | Text, cnf: dict[str, Any] = {}, **kw) -> None: ... + def peer_names(self) -> tuple[_tkinter.Tcl_Obj, ...]: ... + def replace(self, index1: _TextIndex, index2: _TextIndex, chars: str, *args: str | list[str] | tuple[str, ...]) -> None: ... + def scan_mark(self, x: int, y: int) -> None: ... + def scan_dragto(self, x: int, y: int) -> None: ... + def search( + self, + pattern: str, + index: _TextIndex, + stopindex: _TextIndex | None = None, + forwards: bool | None = None, + backwards: bool | None = None, + exact: bool | None = None, + regexp: bool | None = None, + nocase: bool | None = None, + count: Variable | None = None, + elide: bool | None = None, + ) -> str: ... # returns empty string for not found + def see(self, index: _TextIndex) -> None: ... + def tag_add(self, tagName: str, index1: _TextIndex, *args: _TextIndex) -> None: ... + # tag_bind stuff is very similar to Canvas + @overload + def tag_bind( + self, + tagName: str, + sequence: str | None, + func: Callable[[Event[Text]], object] | None, + add: Literal["", "+"] | bool | None = None, + ) -> str: ... + @overload + def tag_bind(self, tagName: str, sequence: str | None, func: str, add: Literal["", "+"] | bool | None = None) -> None: ... + def tag_unbind(self, tagName: str, sequence: str, funcid: str | None = None) -> None: ... + # allowing any string for cget instead of just Literals because there's no other way to look up tag options + def tag_cget(self, tagName: str, option: str): ... + @overload + def tag_configure( + self, + tagName: str, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bgstipple: str = ..., + borderwidth: _ScreenUnits = ..., + border: _ScreenUnits = ..., # alias for borderwidth + elide: bool = ..., + fgstipple: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + justify: Literal["left", "right", "center"] = ..., + lmargin1: _ScreenUnits = ..., + lmargin2: _ScreenUnits = ..., + lmargincolor: str = ..., + offset: _ScreenUnits = ..., + overstrike: bool = ..., + overstrikefg: str = ..., + relief: _Relief = ..., + rmargin: _ScreenUnits = ..., + rmargincolor: str = ..., + selectbackground: str = ..., + selectforeground: str = ..., + spacing1: _ScreenUnits = ..., + spacing2: _ScreenUnits = ..., + spacing3: _ScreenUnits = ..., + tabs: Any = ..., # the exact type is kind of complicated, see manual page + tabstyle: Literal["tabular", "wordprocessor"] = ..., + underline: bool = ..., + underlinefg: str = ..., + wrap: Literal["none", "char", "word"] = ..., # be careful with "none" vs None + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def tag_configure(self, tagName: str, cnf: str) -> tuple[str, str, str, Any, Any]: ... + tag_config = tag_configure + def tag_delete(self, first_tag_name: str, /, *tagNames: str) -> None: ... # error if no tag names given + def tag_lower(self, tagName: str, belowThis: str | None = None) -> None: ... + def tag_names(self, index: _TextIndex | None = None) -> tuple[str, ...]: ... + def tag_nextrange( + self, tagName: str, index1: _TextIndex, index2: _TextIndex | None = None + ) -> tuple[str, str] | tuple[()]: ... + def tag_prevrange( + self, tagName: str, index1: _TextIndex, index2: _TextIndex | None = None + ) -> tuple[str, str] | tuple[()]: ... + def tag_raise(self, tagName: str, aboveThis: str | None = None) -> None: ... + def tag_ranges(self, tagName: str) -> tuple[_tkinter.Tcl_Obj, ...]: ... + # tag_remove and tag_delete are different + def tag_remove(self, tagName: str, index1: _TextIndex, index2: _TextIndex | None = None) -> None: ... + @overload + def window_cget(self, index: _TextIndex, option: Literal["padx", "pady"]) -> int: ... + @overload + def window_cget(self, index: _TextIndex, option: Literal["stretch"]) -> bool: ... # actually returns Literal[0, 1] + @overload + def window_cget(self, index: _TextIndex, option: Literal["align"]) -> Literal["baseline", "bottom", "center", "top"]: ... + @overload # window is set to a widget, but read as the string name. + def window_cget(self, index: _TextIndex, option: Literal["create", "window"]) -> str: ... + @overload + def window_cget(self, index: _TextIndex, option: str) -> Any: ... + @overload + def window_configure(self, index: _TextIndex, cnf: str) -> tuple[str, str, str, str, str | int]: ... + @overload + def window_configure( + self, + index: _TextIndex, + cnf: dict[str, Any] | None = None, + *, + align: Literal["baseline", "bottom", "center", "top"] = ..., + create: str = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + stretch: bool | Literal[0, 1] = ..., + window: Misc | str = ..., + ) -> dict[str, tuple[str, str, str, str, str | int]] | None: ... + window_config = window_configure + def window_create( + self, + index: _TextIndex, + cnf: dict[str, Any] | None = {}, + *, + align: Literal["baseline", "bottom", "center", "top"] = ..., + create: str = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + stretch: bool | Literal[0, 1] = ..., + window: Misc | str = ..., + ) -> None: ... + def window_names(self) -> tuple[str, ...]: ... + def yview_pickplace(self, *what): ... # deprecated + +class _setit: + def __init__(self, var, value, callback=None) -> None: ... + def __call__(self, *args) -> None: ... + +# manual page: tk_optionMenu +class OptionMenu(Menubutton): + widgetName: Incomplete + menuname: Incomplete + def __init__( + # differs from other widgets + self, + master: Misc | None, + variable: StringVar, + value: str, + *values: str, + # kwarg only from now on + command: Callable[[StringVar], object] | None = ..., + ) -> None: ... + # configure, config, cget are inherited from Menubutton + # destroy and __getitem__ are overridden, signature does not change + +# This matches tkinter's image classes (PhotoImage and BitmapImage) +# and PIL's tkinter-compatible class (PIL.ImageTk.PhotoImage), +# but not a plain PIL image that isn't tkinter compatible. +# The reason is that PIL has width and height attributes, not methods. +@type_check_only +class _Image(Protocol): + def width(self) -> int: ... + def height(self) -> int: ... + +@type_check_only +class _BitmapImageLike(_Image): ... + +@type_check_only +class _PhotoImageLike(_Image): ... + +class Image(_Image): + name: Incomplete + tk: _tkinter.TkappType + def __init__(self, imgtype, name=None, cnf={}, master: Misc | _tkinter.TkappType | None = None, **kw) -> None: ... + def __del__(self) -> None: ... + def __setitem__(self, key, value) -> None: ... + def __getitem__(self, key): ... + configure: Incomplete + config: Incomplete + def type(self): ... + +class PhotoImage(Image, _PhotoImageLike): + # This should be kept in sync with PIL.ImageTK.PhotoImage.__init__() + def __init__( + self, + name: str | None = None, + cnf: dict[str, Any] = {}, + master: Misc | _tkinter.TkappType | None = None, + *, + data: str | bytes = ..., # not same as data argument of put() + format: str = ..., + file: StrOrBytesPath = ..., + gamma: float = ..., + height: int = ..., + palette: int | str = ..., + width: int = ..., + ) -> None: ... + def configure( + self, + *, + data: str | bytes = ..., + format: str = ..., + file: StrOrBytesPath = ..., + gamma: float = ..., + height: int = ..., + palette: int | str = ..., + width: int = ..., + ) -> None: ... + config = configure + def blank(self) -> None: ... + def cget(self, option: str) -> str: ... + def __getitem__(self, key: str) -> str: ... # always string: image['height'] can be '0' + if sys.version_info >= (3, 13): + def copy( + self, + *, + from_coords: Iterable[int] | None = None, + zoom: int | tuple[int, int] | list[int] | None = None, + subsample: int | tuple[int, int] | list[int] | None = None, + ) -> PhotoImage: ... + def subsample(self, x: int, y: Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... + def zoom(self, x: int, y: Literal[""] = "", *, from_coords: Iterable[int] | None = None) -> PhotoImage: ... + def copy_replace( + self, + sourceImage: PhotoImage | str, + *, + from_coords: Iterable[int] | None = None, + to: Iterable[int] | None = None, + shrink: bool = False, + zoom: int | tuple[int, int] | list[int] | None = None, + subsample: int | tuple[int, int] | list[int] | None = None, + # `None` defaults to overlay. + compositingrule: Literal["overlay", "set"] | None = None, + ) -> None: ... + else: + def copy(self) -> PhotoImage: ... + def zoom(self, x: int, y: int | Literal[""] = "") -> PhotoImage: ... + def subsample(self, x: int, y: int | Literal[""] = "") -> PhotoImage: ... + + def get(self, x: int, y: int) -> tuple[int, int, int]: ... + def put( + self, + data: ( + str + | bytes + | list[str] + | list[list[str]] + | list[tuple[str, ...]] + | tuple[str, ...] + | tuple[list[str], ...] + | tuple[tuple[str, ...], ...] + ), + to: tuple[int, int] | tuple[int, int, int, int] | None = None, + ) -> None: ... + if sys.version_info >= (3, 13): + def read( + self, + filename: StrOrBytesPath, + format: str | None = None, + *, + from_coords: Iterable[int] | None = None, + to: Iterable[int] | None = None, + shrink: bool = False, + ) -> None: ... + def write( + self, + filename: StrOrBytesPath, + format: str | None = None, + from_coords: Iterable[int] | None = None, + *, + background: str | None = None, + grayscale: bool = False, + ) -> None: ... + @overload + def data( + self, format: str, *, from_coords: Iterable[int] | None = None, background: str | None = None, grayscale: bool = False + ) -> bytes: ... + @overload + def data( + self, + format: None = None, + *, + from_coords: Iterable[int] | None = None, + background: str | None = None, + grayscale: bool = False, + ) -> tuple[str, ...]: ... + + else: + def write( + self, filename: StrOrBytesPath, format: str | None = None, from_coords: tuple[int, int] | None = None + ) -> None: ... + + def transparency_get(self, x: int, y: int) -> bool: ... + def transparency_set(self, x: int, y: int, boolean: bool) -> None: ... + +class BitmapImage(Image, _BitmapImageLike): + # This should be kept in sync with PIL.ImageTK.BitmapImage.__init__() + def __init__( + self, + name=None, + cnf: dict[str, Any] = {}, + master: Misc | _tkinter.TkappType | None = None, + *, + background: str = ..., + data: str | bytes = ..., + file: StrOrBytesPath = ..., + foreground: str = ..., + maskdata: str = ..., + maskfile: StrOrBytesPath = ..., + ) -> None: ... + +def image_names() -> tuple[str, ...]: ... +def image_types() -> tuple[str, ...]: ... + +class Spinbox(Widget, XView): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + activebackground: str = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + buttonbackground: str = ..., + buttoncursor: _Cursor = "", + buttondownrelief: _Relief = ..., + buttonuprelief: _Relief = ..., + # percent substitutions don't seem to be supported, it's similar to Entry's validation stuff + command: Callable[[], object] | str | list[str] | tuple[str, ...] = "", + cursor: _Cursor = "xterm", + disabledbackground: str = ..., + disabledforeground: str = ..., + exportselection: bool = True, + fg: str = ..., + font: _FontDescription = "TkTextFont", + foreground: str = ..., + format: str = "", + from_: float = 0.0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + increment: float = 1.0, + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = 0, + insertofftime: int = 300, + insertontime: int = 600, + insertwidth: _ScreenUnits = ..., + invalidcommand: _EntryValidateCommand = "", + invcmd: _EntryValidateCommand = "", + justify: Literal["left", "center", "right"] = "left", + name: str = ..., + readonlybackground: str = ..., + relief: _Relief = "sunken", + repeatdelay: int = 400, + repeatinterval: int = 100, + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + state: Literal["normal", "disabled", "readonly"] = "normal", + takefocus: _TakeFocusValue = "", + textvariable: Variable = ..., + to: float = 0.0, + validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = "none", + validatecommand: _EntryValidateCommand = "", + vcmd: _EntryValidateCommand = "", + values: list[str] | tuple[str, ...] = ..., + width: int = 20, + wrap: bool = False, + xscrollcommand: _XYScrollCommand = "", + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + activebackground: str = ..., + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + buttonbackground: str = ..., + buttoncursor: _Cursor = ..., + buttondownrelief: _Relief = ..., + buttonuprelief: _Relief = ..., + command: Callable[[], object] | str | list[str] | tuple[str, ...] = ..., + cursor: _Cursor = ..., + disabledbackground: str = ..., + disabledforeground: str = ..., + exportselection: bool = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + format: str = ..., + from_: float = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + increment: float = ..., + insertbackground: str = ..., + insertborderwidth: _ScreenUnits = ..., + insertofftime: int = ..., + insertontime: int = ..., + insertwidth: _ScreenUnits = ..., + invalidcommand: _EntryValidateCommand = ..., + invcmd: _EntryValidateCommand = ..., + justify: Literal["left", "center", "right"] = ..., + readonlybackground: str = ..., + relief: _Relief = ..., + repeatdelay: int = ..., + repeatinterval: int = ..., + selectbackground: str = ..., + selectborderwidth: _ScreenUnits = ..., + selectforeground: str = ..., + state: Literal["normal", "disabled", "readonly"] = ..., + takefocus: _TakeFocusValue = ..., + textvariable: Variable = ..., + to: float = ..., + validate: Literal["none", "focus", "focusin", "focusout", "key", "all"] = ..., + validatecommand: _EntryValidateCommand = ..., + vcmd: _EntryValidateCommand = ..., + values: list[str] | tuple[str, ...] = ..., + width: int = ..., + wrap: bool = ..., + xscrollcommand: _XYScrollCommand = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def bbox(self, index) -> tuple[int, int, int, int] | None: ... # type: ignore[override] + def delete(self, first, last=None) -> Literal[""]: ... + def get(self) -> str: ... + def icursor(self, index): ... + def identify(self, x: int, y: int) -> Literal["", "buttondown", "buttonup", "entry"]: ... + def index(self, index: str | int) -> int: ... + def insert(self, index: str | int, s: str) -> Literal[""]: ... + # spinbox.invoke("asdf") gives error mentioning .invoke("none"), but it's not documented + def invoke(self, element: Literal["none", "buttonup", "buttondown"]) -> Literal[""]: ... + def scan(self, *args): ... + def scan_mark(self, x): ... + def scan_dragto(self, x): ... + def selection(self, *args) -> tuple[int, ...]: ... + def selection_adjust(self, index): ... + def selection_clear(self): ... # type: ignore[override] + def selection_element(self, element=None): ... + def selection_from(self, index: int) -> None: ... + def selection_present(self) -> None: ... + def selection_range(self, start: int, end: int) -> None: ... + def selection_to(self, index: int) -> None: ... + +class LabelFrame(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + background: str = ..., + bd: _ScreenUnits = 2, + bg: str = ..., + border: _ScreenUnits = 2, + borderwidth: _ScreenUnits = 2, + class_: str = "Labelframe", # can't be changed with configure() + colormap: Literal["new", ""] | Misc = "", # can't be changed with configure() + container: bool = False, # undocumented, can't be changed with configure() + cursor: _Cursor = "", + fg: str = ..., + font: _FontDescription = "TkDefaultFont", + foreground: str = ..., + height: _ScreenUnits = 0, + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = 0, + # 'ne' and 'en' are valid labelanchors, but only 'ne' is a valid _Anchor. + labelanchor: Literal["nw", "n", "ne", "en", "e", "es", "se", "s", "sw", "ws", "w", "wn"] = "nw", + labelwidget: Misc = ..., + name: str = ..., + padx: _ScreenUnits = 0, + pady: _ScreenUnits = 0, + relief: _Relief = "groove", + takefocus: _TakeFocusValue = 0, + text: float | str = "", + visual: str | tuple[str, int] = "", # can't be changed with configure() + width: _ScreenUnits = 0, + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + fg: str = ..., + font: _FontDescription = ..., + foreground: str = ..., + height: _ScreenUnits = ..., + highlightbackground: str = ..., + highlightcolor: str = ..., + highlightthickness: _ScreenUnits = ..., + labelanchor: Literal["nw", "n", "ne", "en", "e", "es", "se", "s", "sw", "ws", "w", "wn"] = ..., + labelwidget: Misc = ..., + padx: _ScreenUnits = ..., + pady: _ScreenUnits = ..., + relief: _Relief = ..., + takefocus: _TakeFocusValue = ..., + text: float | str = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + +class PanedWindow(Widget): + def __init__( + self, + master: Misc | None = None, + cnf: dict[str, Any] | None = {}, + *, + background: str = ..., + bd: _ScreenUnits = 1, + bg: str = ..., + border: _ScreenUnits = 1, + borderwidth: _ScreenUnits = 1, + cursor: _Cursor = "", + handlepad: _ScreenUnits = 8, + handlesize: _ScreenUnits = 8, + height: _ScreenUnits = "", + name: str = ..., + opaqueresize: bool = True, + orient: Literal["horizontal", "vertical"] = "horizontal", + proxybackground: str = "", + proxyborderwidth: _ScreenUnits = 2, + proxyrelief: _Relief = "flat", + relief: _Relief = "flat", + sashcursor: _Cursor = "", + sashpad: _ScreenUnits = 0, + sashrelief: _Relief = "flat", + sashwidth: _ScreenUnits = 3, + showhandle: bool = False, + width: _ScreenUnits = "", + ) -> None: ... + @overload + def configure( + self, + cnf: dict[str, Any] | None = None, + *, + background: str = ..., + bd: _ScreenUnits = ..., + bg: str = ..., + border: _ScreenUnits = ..., + borderwidth: _ScreenUnits = ..., + cursor: _Cursor = ..., + handlepad: _ScreenUnits = ..., + handlesize: _ScreenUnits = ..., + height: _ScreenUnits = ..., + opaqueresize: bool = ..., + orient: Literal["horizontal", "vertical"] = ..., + proxybackground: str = ..., + proxyborderwidth: _ScreenUnits = ..., + proxyrelief: _Relief = ..., + relief: _Relief = ..., + sashcursor: _Cursor = ..., + sashpad: _ScreenUnits = ..., + sashrelief: _Relief = ..., + sashwidth: _ScreenUnits = ..., + showhandle: bool = ..., + width: _ScreenUnits = ..., + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: ... + @overload + def configure(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... + config = configure + def add(self, child: Widget, **kw) -> None: ... + def remove(self, child) -> None: ... + forget: Incomplete + def identify(self, x: int, y: int): ... + def proxy(self, *args): ... + def proxy_coord(self): ... + def proxy_forget(self): ... + def proxy_place(self, x, y): ... + def sash(self, *args): ... + def sash_coord(self, index): ... + def sash_mark(self, index): ... + def sash_place(self, index, x, y): ... + def panecget(self, child, option): ... + def paneconfigure(self, tagOrId, cnf=None, **kw): ... + paneconfig: Incomplete + def panes(self): ... + +def _test() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi new file mode 100644 index 0000000000000..6dba6bd609284 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi @@ -0,0 +1,14 @@ +from collections.abc import Mapping +from tkinter import Misc +from typing import Any, ClassVar + +__all__ = ["Dialog"] + +class Dialog: + command: ClassVar[str | None] + master: Misc | None + # Types of options are very dynamic. They depend on the command and are + # sometimes changed to a different type. + options: Mapping[str, Any] + def __init__(self, master: Misc | None = None, **options: Any) -> None: ... + def show(self, **options: Any) -> Any: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi new file mode 100644 index 0000000000000..971b64f091253 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi @@ -0,0 +1,13 @@ +from collections.abc import Mapping +from tkinter import Widget +from typing import Any, Final + +__all__ = ["Dialog"] + +DIALOG_ICON: Final = "questhead" + +class Dialog(Widget): + widgetName: str + num: int + def __init__(self, master=None, cnf: Mapping[str, Any] = {}, **kw) -> None: ... + def destroy(self) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi similarity index 76% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi index cafcf61e8635d..b6ef8f45d0350 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi @@ -1,6 +1,6 @@ -from _typeshed import Incomplete, StrOrBytesPath -from collections.abc import Iterable -from tkinter import Button, Entry, Frame, Listbox, Misc, Scrollbar, StringVar, Toplevel, commondialog +from _typeshed import Incomplete, StrOrBytesPath, StrPath +from collections.abc import Hashable, Iterable +from tkinter import Button, Entry, Event, Frame, Listbox, Misc, Scrollbar, StringVar, Toplevel, commondialog from typing import IO, ClassVar, Literal __all__ = [ @@ -19,12 +19,12 @@ __all__ = [ "askdirectory", ] -dialogstates: dict[Incomplete, tuple[Incomplete, Incomplete]] +dialogstates: dict[Hashable, tuple[str, str]] class FileDialog: title: str - master: Incomplete - directory: Incomplete | None + master: Misc + directory: str | None top: Toplevel botframe: Frame selection: Entry @@ -38,23 +38,23 @@ class FileDialog: filter_button: Button cancel_button: Button def __init__( - self, master, title: Incomplete | None = None + self, master: Misc, title: str | None = None ) -> None: ... # title is usually a str or None, but e.g. int doesn't raise en exception either - how: Incomplete | None - def go(self, dir_or_file=".", pattern: str = "*", default: str = "", key: Incomplete | None = None): ... - def quit(self, how: Incomplete | None = None) -> None: ... - def dirs_double_event(self, event) -> None: ... - def dirs_select_event(self, event) -> None: ... - def files_double_event(self, event) -> None: ... - def files_select_event(self, event) -> None: ... - def ok_event(self, event) -> None: ... + how: str | None + def go(self, dir_or_file: StrPath = ".", pattern: StrPath = "*", default: StrPath = "", key: Hashable | None = None): ... + def quit(self, how: str | None = None) -> None: ... + def dirs_double_event(self, event: Event) -> None: ... + def dirs_select_event(self, event: Event) -> None: ... + def files_double_event(self, event: Event) -> None: ... + def files_select_event(self, event: Event) -> None: ... + def ok_event(self, event: Event) -> None: ... def ok_command(self) -> None: ... - def filter_command(self, event: Incomplete | None = None) -> None: ... - def get_filter(self): ... - def get_selection(self): ... - def cancel_command(self, event: Incomplete | None = None) -> None: ... - def set_filter(self, dir, pat) -> None: ... - def set_selection(self, file) -> None: ... + def filter_command(self, event: Event | None = None) -> None: ... + def get_filter(self) -> tuple[str, str]: ... + def get_selection(self) -> str: ... + def cancel_command(self, event: Event | None = None) -> None: ... + def set_filter(self, dir: StrPath, pat: StrPath) -> None: ... + def set_selection(self, file: StrPath) -> None: ... class LoadFileDialog(FileDialog): title: str diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/font.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/font.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi new file mode 100644 index 0000000000000..8e5a88f92ea15 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi @@ -0,0 +1,98 @@ +from tkinter import Misc +from tkinter.commondialog import Dialog +from typing import ClassVar, Final, Literal + +__all__ = ["showinfo", "showwarning", "showerror", "askquestion", "askokcancel", "askyesno", "askyesnocancel", "askretrycancel"] + +ERROR: Final = "error" +INFO: Final = "info" +QUESTION: Final = "question" +WARNING: Final = "warning" +ABORTRETRYIGNORE: Final = "abortretryignore" +OK: Final = "ok" +OKCANCEL: Final = "okcancel" +RETRYCANCEL: Final = "retrycancel" +YESNO: Final = "yesno" +YESNOCANCEL: Final = "yesnocancel" +ABORT: Final = "abort" +RETRY: Final = "retry" +IGNORE: Final = "ignore" +CANCEL: Final = "cancel" +YES: Final = "yes" +NO: Final = "no" + +class Message(Dialog): + command: ClassVar[str] + +def showinfo( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["ok"] = ..., + parent: Misc = ..., +) -> str: ... +def showwarning( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["ok"] = ..., + parent: Misc = ..., +) -> str: ... +def showerror( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["ok"] = ..., + parent: Misc = ..., +) -> str: ... +def askquestion( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["yes", "no"] = ..., + parent: Misc = ..., +) -> str: ... +def askokcancel( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["ok", "cancel"] = ..., + parent: Misc = ..., +) -> bool: ... +def askyesno( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["yes", "no"] = ..., + parent: Misc = ..., +) -> bool: ... +def askyesnocancel( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["cancel", "yes", "no"] = ..., + parent: Misc = ..., +) -> bool | None: ... +def askretrycancel( + title: str | None = None, + message: str | None = None, + *, + detail: str = ..., + icon: Literal["error", "info", "question", "warning"] = ..., + default: Literal["retry", "cancel"] = ..., + parent: Misc = ..., +) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi similarity index 97% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi index 5328e461ebdc2..50b9cd8f9bcde 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi @@ -35,7 +35,7 @@ __all__ = [ ] def tclobjs_to_py(adict: dict[Any, Any]) -> dict[Any, Any]: ... -def setup_master(master: Incomplete | None = None): ... +def setup_master(master=None): ... _Padding: TypeAlias = ( tkinter._ScreenUnits @@ -52,14 +52,14 @@ class Style: master: Incomplete tk: _tkinter.TkappType def __init__(self, master: tkinter.Misc | None = None) -> None: ... - def configure(self, style, query_opt: Incomplete | None = None, **kw): ... - def map(self, style, query_opt: Incomplete | None = None, **kw): ... - def lookup(self, style, option, state: Incomplete | None = None, default: Incomplete | None = None): ... - def layout(self, style, layoutspec: Incomplete | None = None): ... + def configure(self, style, query_opt=None, **kw): ... + def map(self, style, query_opt=None, **kw): ... + def lookup(self, style, option, state=None, default=None): ... + def layout(self, style, layoutspec=None): ... def element_create(self, elementname, etype, *args, **kw) -> None: ... def element_names(self): ... def element_options(self, elementname): ... - def theme_create(self, themename, parent: Incomplete | None = None, settings: Incomplete | None = None) -> None: ... + def theme_create(self, themename, parent=None, settings=None) -> None: ... def theme_settings(self, themename, settings) -> None: ... def theme_names(self) -> tuple[str, ...]: ... @overload @@ -68,10 +68,10 @@ class Style: def theme_use(self, themename: None = None) -> str: ... class Widget(tkinter.Widget): - def __init__(self, master: tkinter.Misc | None, widgetname, kw: Incomplete | None = None) -> None: ... + def __init__(self, master: tkinter.Misc | None, widgetname, kw=None) -> None: ... def identify(self, x: int, y: int) -> str: ... - def instate(self, statespec, callback: Incomplete | None = None, *args, **kw): ... - def state(self, statespec: Incomplete | None = None): ... + def instate(self, statespec, callback=None, *args, **kw): ... + def state(self, statespec=None): ... class Button(Widget): def __init__( @@ -562,13 +562,13 @@ class Notebook(Widget): compound: tkinter._Compound = ..., underline: int = ..., ) -> None: ... - def forget(self, tab_id) -> None: ... + def forget(self, tab_id) -> None: ... # type: ignore[override] def hide(self, tab_id) -> None: ... def identify(self, x: int, y: int) -> str: ... def index(self, tab_id): ... def insert(self, pos, child, **kw) -> None: ... - def select(self, tab_id: Incomplete | None = None): ... - def tab(self, tab_id, option: Incomplete | None = None, **kw): ... + def select(self, tab_id=None): ... + def tab(self, tab_id, option=None, **kw): ... def tabs(self): ... def enable_traversal(self) -> None: ... @@ -617,8 +617,8 @@ class Panedwindow(Widget, tkinter.PanedWindow): def config(self, cnf: str) -> tuple[str, str, str, Any, Any]: ... forget: Incomplete def insert(self, pos, child, **kw) -> None: ... - def pane(self, pane, option: Incomplete | None = None, **kw): ... - def sashpos(self, index, newpos: Incomplete | None = None): ... + def pane(self, pane, option=None, **kw): ... + def sashpos(self, index, newpos=None): ... PanedWindow = Panedwindow diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/token.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/token.pyi new file mode 100644 index 0000000000000..fd1b10da1d12e --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/token.pyi @@ -0,0 +1,169 @@ +import sys +from typing import Final + +__all__ = [ + "AMPER", + "AMPEREQUAL", + "AT", + "ATEQUAL", + "CIRCUMFLEX", + "CIRCUMFLEXEQUAL", + "COLON", + "COLONEQUAL", + "COMMA", + "DEDENT", + "DOT", + "DOUBLESLASH", + "DOUBLESLASHEQUAL", + "DOUBLESTAR", + "DOUBLESTAREQUAL", + "ELLIPSIS", + "ENDMARKER", + "EQEQUAL", + "EQUAL", + "ERRORTOKEN", + "GREATER", + "GREATEREQUAL", + "INDENT", + "ISEOF", + "ISNONTERMINAL", + "ISTERMINAL", + "LBRACE", + "LEFTSHIFT", + "LEFTSHIFTEQUAL", + "LESS", + "LESSEQUAL", + "LPAR", + "LSQB", + "MINEQUAL", + "MINUS", + "NAME", + "NEWLINE", + "NOTEQUAL", + "NT_OFFSET", + "NUMBER", + "N_TOKENS", + "OP", + "PERCENT", + "PERCENTEQUAL", + "PLUS", + "PLUSEQUAL", + "RARROW", + "RBRACE", + "RIGHTSHIFT", + "RIGHTSHIFTEQUAL", + "RPAR", + "RSQB", + "SEMI", + "SLASH", + "SLASHEQUAL", + "STAR", + "STAREQUAL", + "STRING", + "TILDE", + "TYPE_COMMENT", + "TYPE_IGNORE", + "VBAR", + "VBAREQUAL", + "tok_name", + "ENCODING", + "NL", + "COMMENT", +] +if sys.version_info < (3, 13): + __all__ += ["ASYNC", "AWAIT"] + +if sys.version_info >= (3, 10): + __all__ += ["SOFT_KEYWORD"] + +if sys.version_info >= (3, 12): + __all__ += ["EXCLAMATION", "FSTRING_END", "FSTRING_MIDDLE", "FSTRING_START", "EXACT_TOKEN_TYPES"] + +if sys.version_info >= (3, 14): + __all__ += ["TSTRING_START", "TSTRING_MIDDLE", "TSTRING_END"] + +ENDMARKER: Final[int] +NAME: Final[int] +NUMBER: Final[int] +STRING: Final[int] +NEWLINE: Final[int] +INDENT: Final[int] +DEDENT: Final[int] +LPAR: Final[int] +RPAR: Final[int] +LSQB: Final[int] +RSQB: Final[int] +COLON: Final[int] +COMMA: Final[int] +SEMI: Final[int] +PLUS: Final[int] +MINUS: Final[int] +STAR: Final[int] +SLASH: Final[int] +VBAR: Final[int] +AMPER: Final[int] +LESS: Final[int] +GREATER: Final[int] +EQUAL: Final[int] +DOT: Final[int] +PERCENT: Final[int] +LBRACE: Final[int] +RBRACE: Final[int] +EQEQUAL: Final[int] +NOTEQUAL: Final[int] +LESSEQUAL: Final[int] +GREATEREQUAL: Final[int] +TILDE: Final[int] +CIRCUMFLEX: Final[int] +LEFTSHIFT: Final[int] +RIGHTSHIFT: Final[int] +DOUBLESTAR: Final[int] +PLUSEQUAL: Final[int] +MINEQUAL: Final[int] +STAREQUAL: Final[int] +SLASHEQUAL: Final[int] +PERCENTEQUAL: Final[int] +AMPEREQUAL: Final[int] +VBAREQUAL: Final[int] +CIRCUMFLEXEQUAL: Final[int] +LEFTSHIFTEQUAL: Final[int] +RIGHTSHIFTEQUAL: Final[int] +DOUBLESTAREQUAL: Final[int] +DOUBLESLASH: Final[int] +DOUBLESLASHEQUAL: Final[int] +AT: Final[int] +RARROW: Final[int] +ELLIPSIS: Final[int] +ATEQUAL: Final[int] +if sys.version_info < (3, 13): + AWAIT: Final[int] + ASYNC: Final[int] +OP: Final[int] +ERRORTOKEN: Final[int] +N_TOKENS: Final[int] +NT_OFFSET: Final[int] +tok_name: Final[dict[int, str]] +COMMENT: Final[int] +NL: Final[int] +ENCODING: Final[int] +TYPE_COMMENT: Final[int] +TYPE_IGNORE: Final[int] +COLONEQUAL: Final[int] +EXACT_TOKEN_TYPES: Final[dict[str, int]] +if sys.version_info >= (3, 10): + SOFT_KEYWORD: Final[int] + +if sys.version_info >= (3, 12): + EXCLAMATION: Final[int] + FSTRING_END: Final[int] + FSTRING_MIDDLE: Final[int] + FSTRING_START: Final[int] + +if sys.version_info >= (3, 14): + TSTRING_START: Final[int] + TSTRING_MIDDLE: Final[int] + TSTRING_END: Final[int] + +def ISTERMINAL(x: int) -> bool: ... +def ISNONTERMINAL(x: int) -> bool: ... +def ISEOF(x: int) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tokenize.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.pyi similarity index 92% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tokenize.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tokenize.pyi index 86e87704eb02d..1a3a80937f22e 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tokenize.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.pyi @@ -3,10 +3,15 @@ from _typeshed import FileDescriptorOrPath from collections.abc import Callable, Generator, Iterable, Sequence from re import Pattern from token import * -from token import EXACT_TOKEN_TYPES as EXACT_TOKEN_TYPES from typing import Any, NamedTuple, TextIO, type_check_only from typing_extensions import TypeAlias +if sys.version_info < (3, 12): + # Avoid double assignment to Final name by imports, which pyright objects to. + # EXACT_TOKEN_TYPES is already defined by 'from token import *' above + # in Python 3.12+. + from token import EXACT_TOKEN_TYPES as EXACT_TOKEN_TYPES + __all__ = [ "AMPER", "AMPEREQUAL", @@ -93,6 +98,9 @@ if sys.version_info >= (3, 12): if sys.version_info >= (3, 13): __all__ += ["TokenError", "open"] +if sys.version_info >= (3, 14): + __all__ += ["TSTRING_START", "TSTRING_MIDDLE", "TSTRING_END"] + cookie_re: Pattern[str] blank_re: Pattern[bytes] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tomllib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tomllib.pyi new file mode 100644 index 0000000000000..c160ffc38bfdd --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tomllib.pyi @@ -0,0 +1,26 @@ +import sys +from _typeshed import SupportsRead +from collections.abc import Callable +from typing import Any, overload +from typing_extensions import deprecated + +__all__ = ("loads", "load", "TOMLDecodeError") + +if sys.version_info >= (3, 14): + class TOMLDecodeError(ValueError): + msg: str + doc: str + pos: int + lineno: int + colno: int + @overload + def __init__(self, msg: str, doc: str, pos: int) -> None: ... + @overload + @deprecated("Deprecated in Python 3.14; Please set 'msg', 'doc' and 'pos' arguments only.") + def __init__(self, msg: str | type = ..., doc: str | type = ..., pos: int | type = ..., *args: Any) -> None: ... + +else: + class TOMLDecodeError(ValueError): ... + +def load(fp: SupportsRead[bytes], /, *, parse_float: Callable[[str], Any] = ...) -> dict[str, Any]: ... +def loads(s: str, /, *, parse_float: Callable[[str], Any] = ...) -> dict[str, Any]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/trace.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/trace.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/trace.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/trace.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/traceback.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/traceback.pyi similarity index 99% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/traceback.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/traceback.pyi index 4f132d51c617f..4553dbd08384d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/traceback.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/traceback.pyi @@ -27,6 +27,9 @@ __all__ = [ "walk_tb", ] +if sys.version_info >= (3, 14): + __all__ += ["print_list"] + _FrameSummaryTuple: TypeAlias = tuple[str, int, str, str | None] def print_tb(tb: TracebackType | None, limit: int | None = None, file: SupportsWrite[str] | None = None) -> None: ... @@ -81,8 +84,6 @@ def print_stack(f: FrameType | None = None, limit: int | None = None, file: Supp def extract_tb(tb: TracebackType | None, limit: int | None = None) -> StackSummary: ... def extract_stack(f: FrameType | None = None, limit: int | None = None) -> StackSummary: ... def format_list(extracted_list: Iterable[FrameSummary | _FrameSummaryTuple]) -> list[str]: ... - -# undocumented def print_list(extracted_list: Iterable[FrameSummary | _FrameSummaryTuple], file: SupportsWrite[str] | None = None) -> None: ... if sys.version_info >= (3, 13): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tracemalloc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tracemalloc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tracemalloc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tracemalloc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tty.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tty.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tty.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tty.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/turtle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi similarity index 95% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/turtle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi index a2ab728de943f..9c62c64e718aa 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/turtle.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi @@ -1,5 +1,7 @@ import sys -from collections.abc import Callable, Sequence +from _typeshed import StrPath +from collections.abc import Callable, Generator, Sequence +from contextlib import contextmanager from tkinter import Canvas, Frame, Misc, PhotoImage, Scrollbar from typing import Any, ClassVar, Literal, TypedDict, overload from typing_extensions import Self, TypeAlias @@ -128,6 +130,9 @@ __all__ = [ "Terminator", ] +if sys.version_info >= (3, 14): + __all__ += ["fill", "no_animation", "poly", "save"] + if sys.version_info >= (3, 12): __all__ += ["teleport"] @@ -231,6 +236,10 @@ class TurtleScreen(TurtleScreenBase): def delay(self, delay: None = None) -> int: ... @overload def delay(self, delay: int) -> None: ... + if sys.version_info >= (3, 14): + @contextmanager + def no_animation(self) -> Generator[None]: ... + def update(self) -> None: ... def window_width(self) -> int: ... def window_height(self) -> int: ... @@ -249,6 +258,8 @@ class TurtleScreen(TurtleScreenBase): # Looks like if self.cv is not a ScrolledCanvas, this could return a tuple as well @overload def screensize(self, canvwidth: int, canvheight: int, bg: _Color | None = None) -> None: ... + if sys.version_info >= (3, 14): + def save(self, filename: StrPath, *, overwrite: bool = False) -> None: ... onscreenclick = onclick resetscreen = reset clearscreen = clear @@ -428,12 +439,20 @@ class RawTurtle(TPen, TNavigator): # type: ignore[misc] # Conflicting methods def clearstamp(self, stampid: int | tuple[int, ...]) -> None: ... def clearstamps(self, n: int | None = None) -> None: ... def filling(self) -> bool: ... + if sys.version_info >= (3, 14): + @contextmanager + def fill(self) -> Generator[None]: ... + def begin_fill(self) -> None: ... def end_fill(self) -> None: ... def dot(self, size: int | None = None, *color: _Color) -> None: ... def write( self, arg: object, move: bool = False, align: str = "left", font: tuple[str, int, str] = ("Arial", 8, "normal") ) -> None: ... + if sys.version_info >= (3, 14): + @contextmanager + def poly(self) -> Generator[None]: ... + def begin_poly(self) -> None: ... def end_poly(self) -> None: ... def get_poly(self) -> _PolygonCoords | None: ... @@ -516,6 +535,11 @@ def tracer(n: int, delay: int | None = None) -> None: ... def delay(delay: None = None) -> int: ... @overload def delay(delay: int) -> None: ... + +if sys.version_info >= (3, 14): + @contextmanager + def no_animation() -> Generator[None]: ... + def update() -> None: ... def window_width() -> int: ... def window_height() -> int: ... @@ -534,6 +558,9 @@ def screensize(canvwidth: None = None, canvheight: None = None, bg: None = None) @overload def screensize(canvwidth: int, canvheight: int, bg: _Color | None = None) -> None: ... +if sys.version_info >= (3, 14): + def save(filename: StrPath, *, overwrite: bool = False) -> None: ... + onscreenclick = onclick resetscreen = reset clearscreen = clear @@ -705,10 +732,20 @@ def stamp() -> Any: ... def clearstamp(stampid: int | tuple[int, ...]) -> None: ... def clearstamps(n: int | None = None) -> None: ... def filling() -> bool: ... + +if sys.version_info >= (3, 14): + @contextmanager + def fill() -> Generator[None]: ... + def begin_fill() -> None: ... def end_fill() -> None: ... def dot(size: int | None = None, *color: _Color) -> None: ... def write(arg: object, move: bool = False, align: str = "left", font: tuple[str, int, str] = ("Arial", 8, "normal")) -> None: ... + +if sys.version_info >= (3, 14): + @contextmanager + def poly() -> Generator[None]: ... + def begin_poly() -> None: ... def end_poly() -> None: ... def get_poly() -> _PolygonCoords | None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi new file mode 100644 index 0000000000000..44bd3eeb3f533 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi @@ -0,0 +1,713 @@ +import sys +from _typeshed import AnnotationForm, MaybeNone, SupportsKeysAndGetItem +from _typeshed.importlib import LoaderProtocol +from collections.abc import ( + AsyncGenerator, + Awaitable, + Callable, + Coroutine, + Generator, + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + MutableSequence, + ValuesView, +) +from importlib.machinery import ModuleSpec +from typing import Any, ClassVar, Literal, TypeVar, final, overload +from typing_extensions import ParamSpec, Self, TypeAliasType, TypeVarTuple, deprecated + +if sys.version_info >= (3, 14): + from _typeshed import AnnotateFunc + +__all__ = [ + "FunctionType", + "LambdaType", + "CodeType", + "MappingProxyType", + "SimpleNamespace", + "GeneratorType", + "CoroutineType", + "AsyncGeneratorType", + "MethodType", + "BuiltinFunctionType", + "ModuleType", + "TracebackType", + "FrameType", + "GetSetDescriptorType", + "MemberDescriptorType", + "new_class", + "prepare_class", + "DynamicClassAttribute", + "coroutine", + "BuiltinMethodType", + "ClassMethodDescriptorType", + "MethodDescriptorType", + "MethodWrapperType", + "WrapperDescriptorType", + "resolve_bases", + "CellType", + "GenericAlias", +] + +if sys.version_info >= (3, 10): + __all__ += ["EllipsisType", "NoneType", "NotImplementedType", "UnionType"] + +if sys.version_info >= (3, 12): + __all__ += ["get_original_bases"] + +if sys.version_info >= (3, 13): + __all__ += ["CapsuleType"] + +# Note, all classes "defined" here require special handling. + +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_KT = TypeVar("_KT") +_VT_co = TypeVar("_VT_co", covariant=True) + +# Make sure this class definition stays roughly in line with `builtins.function` +@final +class FunctionType: + @property + def __closure__(self) -> tuple[CellType, ...] | None: ... + __code__: CodeType + __defaults__: tuple[Any, ...] | None + __dict__: dict[str, Any] + @property + def __globals__(self) -> dict[str, Any]: ... + __name__: str + __qualname__: str + __annotations__: dict[str, AnnotationForm] + if sys.version_info >= (3, 14): + __annotate__: AnnotateFunc | None + __kwdefaults__: dict[str, Any] | None + if sys.version_info >= (3, 10): + @property + def __builtins__(self) -> dict[str, Any]: ... + if sys.version_info >= (3, 12): + __type_params__: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] + + __module__: str + if sys.version_info >= (3, 13): + def __new__( + cls, + code: CodeType, + globals: dict[str, Any], + name: str | None = None, + argdefs: tuple[object, ...] | None = None, + closure: tuple[CellType, ...] | None = None, + kwdefaults: dict[str, object] | None = None, + ) -> Self: ... + else: + def __new__( + cls, + code: CodeType, + globals: dict[str, Any], + name: str | None = None, + argdefs: tuple[object, ...] | None = None, + closure: tuple[CellType, ...] | None = None, + ) -> Self: ... + + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + @overload + def __get__(self, instance: None, owner: type, /) -> FunctionType: ... + @overload + def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... + +LambdaType = FunctionType + +@final +class CodeType: + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + @property + def co_argcount(self) -> int: ... + @property + def co_posonlyargcount(self) -> int: ... + @property + def co_kwonlyargcount(self) -> int: ... + @property + def co_nlocals(self) -> int: ... + @property + def co_stacksize(self) -> int: ... + @property + def co_flags(self) -> int: ... + @property + def co_code(self) -> bytes: ... + @property + def co_consts(self) -> tuple[Any, ...]: ... + @property + def co_names(self) -> tuple[str, ...]: ... + @property + def co_varnames(self) -> tuple[str, ...]: ... + @property + def co_filename(self) -> str: ... + @property + def co_name(self) -> str: ... + @property + def co_firstlineno(self) -> int: ... + if sys.version_info >= (3, 10): + @property + @deprecated("Will be removed in Python 3.15. Use the co_lines() method instead.") + def co_lnotab(self) -> bytes: ... + else: + @property + def co_lnotab(self) -> bytes: ... + + @property + def co_freevars(self) -> tuple[str, ...]: ... + @property + def co_cellvars(self) -> tuple[str, ...]: ... + if sys.version_info >= (3, 10): + @property + def co_linetable(self) -> bytes: ... + def co_lines(self) -> Iterator[tuple[int, int, int | None]]: ... + if sys.version_info >= (3, 11): + @property + def co_exceptiontable(self) -> bytes: ... + @property + def co_qualname(self) -> str: ... + def co_positions(self) -> Iterable[tuple[int | None, int | None, int | None, int | None]]: ... + if sys.version_info >= (3, 14): + def co_branches(self) -> Iterator[tuple[int, int, int]]: ... + + if sys.version_info >= (3, 11): + def __new__( + cls, + argcount: int, + posonlyargcount: int, + kwonlyargcount: int, + nlocals: int, + stacksize: int, + flags: int, + codestring: bytes, + constants: tuple[object, ...], + names: tuple[str, ...], + varnames: tuple[str, ...], + filename: str, + name: str, + qualname: str, + firstlineno: int, + linetable: bytes, + exceptiontable: bytes, + freevars: tuple[str, ...] = ..., + cellvars: tuple[str, ...] = ..., + /, + ) -> Self: ... + elif sys.version_info >= (3, 10): + def __new__( + cls, + argcount: int, + posonlyargcount: int, + kwonlyargcount: int, + nlocals: int, + stacksize: int, + flags: int, + codestring: bytes, + constants: tuple[object, ...], + names: tuple[str, ...], + varnames: tuple[str, ...], + filename: str, + name: str, + firstlineno: int, + linetable: bytes, + freevars: tuple[str, ...] = ..., + cellvars: tuple[str, ...] = ..., + /, + ) -> Self: ... + else: + def __new__( + cls, + argcount: int, + posonlyargcount: int, + kwonlyargcount: int, + nlocals: int, + stacksize: int, + flags: int, + codestring: bytes, + constants: tuple[object, ...], + names: tuple[str, ...], + varnames: tuple[str, ...], + filename: str, + name: str, + firstlineno: int, + lnotab: bytes, + freevars: tuple[str, ...] = ..., + cellvars: tuple[str, ...] = ..., + /, + ) -> Self: ... + if sys.version_info >= (3, 11): + def replace( + self, + *, + co_argcount: int = -1, + co_posonlyargcount: int = -1, + co_kwonlyargcount: int = -1, + co_nlocals: int = -1, + co_stacksize: int = -1, + co_flags: int = -1, + co_firstlineno: int = -1, + co_code: bytes = ..., + co_consts: tuple[object, ...] = ..., + co_names: tuple[str, ...] = ..., + co_varnames: tuple[str, ...] = ..., + co_freevars: tuple[str, ...] = ..., + co_cellvars: tuple[str, ...] = ..., + co_filename: str = ..., + co_name: str = ..., + co_qualname: str = ..., + co_linetable: bytes = ..., + co_exceptiontable: bytes = ..., + ) -> Self: ... + elif sys.version_info >= (3, 10): + def replace( + self, + *, + co_argcount: int = -1, + co_posonlyargcount: int = -1, + co_kwonlyargcount: int = -1, + co_nlocals: int = -1, + co_stacksize: int = -1, + co_flags: int = -1, + co_firstlineno: int = -1, + co_code: bytes = ..., + co_consts: tuple[object, ...] = ..., + co_names: tuple[str, ...] = ..., + co_varnames: tuple[str, ...] = ..., + co_freevars: tuple[str, ...] = ..., + co_cellvars: tuple[str, ...] = ..., + co_filename: str = ..., + co_name: str = ..., + co_linetable: bytes = ..., + ) -> Self: ... + else: + def replace( + self, + *, + co_argcount: int = -1, + co_posonlyargcount: int = -1, + co_kwonlyargcount: int = -1, + co_nlocals: int = -1, + co_stacksize: int = -1, + co_flags: int = -1, + co_firstlineno: int = -1, + co_code: bytes = ..., + co_consts: tuple[object, ...] = ..., + co_names: tuple[str, ...] = ..., + co_varnames: tuple[str, ...] = ..., + co_freevars: tuple[str, ...] = ..., + co_cellvars: tuple[str, ...] = ..., + co_filename: str = ..., + co_name: str = ..., + co_lnotab: bytes = ..., + ) -> Self: ... + + if sys.version_info >= (3, 13): + __replace__ = replace + +@final +class MappingProxyType(Mapping[_KT, _VT_co]): + __hash__: ClassVar[None] # type: ignore[assignment] + def __new__(cls, mapping: SupportsKeysAndGetItem[_KT, _VT_co]) -> Self: ... + def __getitem__(self, key: _KT, /) -> _VT_co: ... + def __iter__(self) -> Iterator[_KT]: ... + def __len__(self) -> int: ... + def __eq__(self, value: object, /) -> bool: ... + def copy(self) -> dict[_KT, _VT_co]: ... + def keys(self) -> KeysView[_KT]: ... + def values(self) -> ValuesView[_VT_co]: ... + def items(self) -> ItemsView[_KT, _VT_co]: ... + @overload + def get(self, key: _KT, /) -> _VT_co | None: ... + @overload + def get(self, key: _KT, default: _VT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter + @overload + def get(self, key: _KT, default: _T2, /) -> _VT_co | _T2: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + def __reversed__(self) -> Iterator[_KT]: ... + def __or__(self, value: Mapping[_T1, _T2], /) -> dict[_KT | _T1, _VT_co | _T2]: ... + def __ror__(self, value: Mapping[_T1, _T2], /) -> dict[_KT | _T1, _VT_co | _T2]: ... + +class SimpleNamespace: + __hash__: ClassVar[None] # type: ignore[assignment] + if sys.version_info >= (3, 13): + def __init__(self, mapping_or_iterable: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), /, **kwargs: Any) -> None: ... + else: + def __init__(self, **kwargs: Any) -> None: ... + + def __eq__(self, value: object, /) -> bool: ... + def __getattribute__(self, name: str, /) -> Any: ... + def __setattr__(self, name: str, value: Any, /) -> None: ... + def __delattr__(self, name: str, /) -> None: ... + if sys.version_info >= (3, 13): + def __replace__(self, **kwargs: Any) -> Self: ... + +class ModuleType: + __name__: str + __file__: str | None + @property + def __dict__(self) -> dict[str, Any]: ... # type: ignore[override] + __loader__: LoaderProtocol | None + __package__: str | None + __path__: MutableSequence[str] + __spec__: ModuleSpec | None + # N.B. Although this is the same type as `builtins.object.__doc__`, + # it is deliberately redeclared here. Most symbols declared in the namespace + # of `types.ModuleType` are available as "implicit globals" within a module's + # namespace, but this is not true for symbols declared in the namespace of `builtins.object`. + # Redeclaring `__doc__` here helps some type checkers understand that `__doc__` is available + # as an implicit global in all modules, similar to `__name__`, `__file__`, `__spec__`, etc. + __doc__: str | None + __annotations__: dict[str, AnnotationForm] + if sys.version_info >= (3, 14): + __annotate__: AnnotateFunc | None + + def __init__(self, name: str, doc: str | None = ...) -> None: ... + # __getattr__ doesn't exist at runtime, + # but having it here in typeshed makes dynamic imports + # using `builtins.__import__` or `importlib.import_module` less painful + def __getattr__(self, name: str) -> Any: ... + +@final +class CellType: + def __new__(cls, contents: object = ..., /) -> Self: ... + __hash__: ClassVar[None] # type: ignore[assignment] + cell_contents: Any + +_YieldT_co = TypeVar("_YieldT_co", covariant=True) +_SendT_contra = TypeVar("_SendT_contra", contravariant=True) +_ReturnT_co = TypeVar("_ReturnT_co", covariant=True) + +@final +class GeneratorType(Generator[_YieldT_co, _SendT_contra, _ReturnT_co]): + @property + def gi_code(self) -> CodeType: ... + @property + def gi_frame(self) -> FrameType: ... + @property + def gi_running(self) -> bool: ... + @property + def gi_yieldfrom(self) -> Iterator[_YieldT_co] | None: ... + if sys.version_info >= (3, 11): + @property + def gi_suspended(self) -> bool: ... + __name__: str + __qualname__: str + def __iter__(self) -> Self: ... + def __next__(self) -> _YieldT_co: ... + def send(self, arg: _SendT_contra, /) -> _YieldT_co: ... + @overload + def throw( + self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / + ) -> _YieldT_co: ... + @overload + def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... + if sys.version_info >= (3, 13): + def __class_getitem__(cls, item: Any, /) -> Any: ... + +@final +class AsyncGeneratorType(AsyncGenerator[_YieldT_co, _SendT_contra]): + @property + def ag_await(self) -> Awaitable[Any] | None: ... + @property + def ag_code(self) -> CodeType: ... + @property + def ag_frame(self) -> FrameType: ... + @property + def ag_running(self) -> bool: ... + __name__: str + __qualname__: str + if sys.version_info >= (3, 12): + @property + def ag_suspended(self) -> bool: ... + + def __aiter__(self) -> Self: ... + def __anext__(self) -> Coroutine[Any, Any, _YieldT_co]: ... + def asend(self, val: _SendT_contra, /) -> Coroutine[Any, Any, _YieldT_co]: ... + @overload + async def athrow( + self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / + ) -> _YieldT_co: ... + @overload + async def athrow(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... + def aclose(self) -> Coroutine[Any, Any, None]: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + +@final +class CoroutineType(Coroutine[_YieldT_co, _SendT_contra, _ReturnT_co]): + __name__: str + __qualname__: str + @property + def cr_await(self) -> Any | None: ... + @property + def cr_code(self) -> CodeType: ... + @property + def cr_frame(self) -> FrameType: ... + @property + def cr_running(self) -> bool: ... + @property + def cr_origin(self) -> tuple[tuple[str, int, str], ...] | None: ... + if sys.version_info >= (3, 11): + @property + def cr_suspended(self) -> bool: ... + + def close(self) -> None: ... + def __await__(self) -> Generator[Any, None, _ReturnT_co]: ... + def send(self, arg: _SendT_contra, /) -> _YieldT_co: ... + @overload + def throw( + self, typ: type[BaseException], val: BaseException | object = ..., tb: TracebackType | None = ..., / + ) -> _YieldT_co: ... + @overload + def throw(self, typ: BaseException, val: None = None, tb: TracebackType | None = ..., /) -> _YieldT_co: ... + if sys.version_info >= (3, 13): + def __class_getitem__(cls, item: Any, /) -> Any: ... + +@final +class MethodType: + @property + def __closure__(self) -> tuple[CellType, ...] | None: ... # inherited from the added function + @property + def __code__(self) -> CodeType: ... # inherited from the added function + @property + def __defaults__(self) -> tuple[Any, ...] | None: ... # inherited from the added function + @property + def __func__(self) -> Callable[..., Any]: ... + @property + def __self__(self) -> object: ... + @property + def __name__(self) -> str: ... # inherited from the added function + @property + def __qualname__(self) -> str: ... # inherited from the added function + def __new__(cls, func: Callable[..., Any], instance: object, /) -> Self: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + + if sys.version_info >= (3, 13): + def __get__(self, instance: object, owner: type | None = None, /) -> Self: ... + + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + +@final +class BuiltinFunctionType: + @property + def __self__(self) -> object | ModuleType: ... + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + +BuiltinMethodType = BuiltinFunctionType + +@final +class WrapperDescriptorType: + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + @property + def __objclass__(self) -> type: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... + +@final +class MethodWrapperType: + @property + def __self__(self) -> object: ... + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + @property + def __objclass__(self) -> type: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + def __eq__(self, value: object, /) -> bool: ... + def __ne__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + +@final +class MethodDescriptorType: + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + @property + def __objclass__(self) -> type: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... + +@final +class ClassMethodDescriptorType: + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + @property + def __objclass__(self) -> type: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... + +@final +class TracebackType: + def __new__(cls, tb_next: TracebackType | None, tb_frame: FrameType, tb_lasti: int, tb_lineno: int) -> Self: ... + tb_next: TracebackType | None + # the rest are read-only + @property + def tb_frame(self) -> FrameType: ... + @property + def tb_lasti(self) -> int: ... + @property + def tb_lineno(self) -> int: ... + +@final +class FrameType: + @property + def f_back(self) -> FrameType | None: ... + @property + def f_builtins(self) -> dict[str, Any]: ... + @property + def f_code(self) -> CodeType: ... + @property + def f_globals(self) -> dict[str, Any]: ... + @property + def f_lasti(self) -> int: ... + # see discussion in #6769: f_lineno *can* sometimes be None, + # but you should probably file a bug report with CPython if you encounter it being None in the wild. + # An `int | None` annotation here causes too many false-positive errors, so applying `int | Any`. + @property + def f_lineno(self) -> int | MaybeNone: ... + @property + def f_locals(self) -> dict[str, Any]: ... + f_trace: Callable[[FrameType, str, Any], Any] | None + f_trace_lines: bool + f_trace_opcodes: bool + def clear(self) -> None: ... + if sys.version_info >= (3, 14): + @property + def f_generator(self) -> GeneratorType[Any, Any, Any] | CoroutineType[Any, Any, Any] | None: ... + +@final +class GetSetDescriptorType: + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + @property + def __objclass__(self) -> type: ... + def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... + def __set__(self, instance: Any, value: Any, /) -> None: ... + def __delete__(self, instance: Any, /) -> None: ... + +@final +class MemberDescriptorType: + @property + def __name__(self) -> str: ... + @property + def __qualname__(self) -> str: ... + @property + def __objclass__(self) -> type: ... + def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ... + def __set__(self, instance: Any, value: Any, /) -> None: ... + def __delete__(self, instance: Any, /) -> None: ... + +def new_class( + name: str, + bases: Iterable[object] = (), + kwds: dict[str, Any] | None = None, + exec_body: Callable[[dict[str, Any]], object] | None = None, +) -> type: ... +def resolve_bases(bases: Iterable[object]) -> tuple[Any, ...]: ... +def prepare_class( + name: str, bases: tuple[type, ...] = (), kwds: dict[str, Any] | None = None +) -> tuple[type, dict[str, Any], dict[str, Any]]: ... + +if sys.version_info >= (3, 12): + def get_original_bases(cls: type, /) -> tuple[Any, ...]: ... + +# Does not actually inherit from property, but saying it does makes sure that +# pyright handles this class correctly. +class DynamicClassAttribute(property): + fget: Callable[[Any], Any] | None + fset: Callable[[Any, Any], object] | None # type: ignore[assignment] + fdel: Callable[[Any], object] | None # type: ignore[assignment] + overwrite_doc: bool + __isabstractmethod__: bool + def __init__( + self, + fget: Callable[[Any], Any] | None = None, + fset: Callable[[Any, Any], object] | None = None, + fdel: Callable[[Any], object] | None = None, + doc: str | None = None, + ) -> None: ... + def __get__(self, instance: Any, ownerclass: type | None = None) -> Any: ... + def __set__(self, instance: Any, value: Any) -> None: ... + def __delete__(self, instance: Any) -> None: ... + def getter(self, fget: Callable[[Any], Any]) -> DynamicClassAttribute: ... + def setter(self, fset: Callable[[Any, Any], object]) -> DynamicClassAttribute: ... + def deleter(self, fdel: Callable[[Any], object]) -> DynamicClassAttribute: ... + +_Fn = TypeVar("_Fn", bound=Callable[..., object]) +_R = TypeVar("_R") +_P = ParamSpec("_P") + +# it's not really an Awaitable, but can be used in an await expression. Real type: Generator & Awaitable +@overload +def coroutine(func: Callable[_P, Generator[Any, Any, _R]]) -> Callable[_P, Awaitable[_R]]: ... +@overload +def coroutine(func: _Fn) -> _Fn: ... + +class GenericAlias: + @property + def __origin__(self) -> type | TypeAliasType: ... + @property + def __args__(self) -> tuple[Any, ...]: ... + @property + def __parameters__(self) -> tuple[Any, ...]: ... + def __new__(cls, origin: type, args: Any, /) -> Self: ... + def __getitem__(self, typeargs: Any, /) -> GenericAlias: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + def __mro_entries__(self, bases: Iterable[object], /) -> tuple[type, ...]: ... + if sys.version_info >= (3, 11): + @property + def __unpacked__(self) -> bool: ... + @property + def __typing_unpacked_tuple_args__(self) -> tuple[Any, ...] | None: ... + if sys.version_info >= (3, 10): + def __or__(self, value: Any, /) -> UnionType: ... + def __ror__(self, value: Any, /) -> UnionType: ... + + # GenericAlias delegates attr access to `__origin__` + def __getattr__(self, name: str) -> Any: ... + +if sys.version_info >= (3, 10): + @final + class NoneType: + def __bool__(self) -> Literal[False]: ... + + @final + class EllipsisType: ... + + from builtins import _NotImplementedType + + NotImplementedType = _NotImplementedType + @final + class UnionType: + @property + def __args__(self) -> tuple[Any, ...]: ... + @property + def __parameters__(self) -> tuple[Any, ...]: ... + def __or__(self, value: Any, /) -> UnionType: ... + def __ror__(self, value: Any, /) -> UnionType: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + +if sys.version_info >= (3, 13): + @final + class CapsuleType: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi similarity index 80% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi index df753cfd9bcaa..d296c8d921498 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi @@ -1,5 +1,4 @@ # Since this module defines "overload" it is not recognized by Ruff as typing.overload -# ruff: noqa: F811 # TODO: The collections import is required, otherwise mypy crashes. # https://github.com/python/mypy/issues/16744 import collections # noqa: F401 # pyright: ignore[reportUnusedImport] @@ -23,6 +22,11 @@ from types import ( ) from typing_extensions import Never as _Never, ParamSpec as _ParamSpec, deprecated +if sys.version_info >= (3, 14): + from _typeshed import EvaluateFunc + + from annotationlib import Format + if sys.version_info >= (3, 10): from types import UnionType @@ -37,7 +41,6 @@ __all__ = [ "AsyncIterator", "Awaitable", "BinaryIO", - "ByteString", "Callable", "ChainMap", "ClassVar", @@ -106,6 +109,12 @@ __all__ = [ "runtime_checkable", ] +if sys.version_info < (3, 14): + __all__ += ["ByteString"] + +if sys.version_info >= (3, 14): + __all__ += ["evaluate_forward_ref"] + if sys.version_info >= (3, 10): __all__ += ["Concatenate", "ParamSpec", "ParamSpecArgs", "ParamSpecKwargs", "TypeAlias", "TypeGuard", "is_typeddict"] @@ -132,6 +141,10 @@ if sys.version_info >= (3, 12): if sys.version_info >= (3, 13): __all__ += ["get_protocol_members", "is_protocol", "NoDefault", "TypeIs", "ReadOnly"] +# We can't use this name here because it leads to issues with mypy, likely +# due to an import cycle. Below instead we use Any with a comment. +# from _typeshed import AnnotationForm + class Any: ... class _Final: ... @@ -141,9 +154,9 @@ class TypeVar: @property def __name__(self) -> str: ... @property - def __bound__(self) -> Any | None: ... + def __bound__(self) -> Any | None: ... # AnnotationForm @property - def __constraints__(self) -> tuple[Any, ...]: ... + def __constraints__(self) -> tuple[Any, ...]: ... # AnnotationForm @property def __covariant__(self) -> bool: ... @property @@ -153,46 +166,64 @@ class TypeVar: def __infer_variance__(self) -> bool: ... if sys.version_info >= (3, 13): @property - def __default__(self) -> Any: ... + def __default__(self) -> Any: ... # AnnotationForm if sys.version_info >= (3, 13): def __new__( cls, name: str, - *constraints: Any, - bound: Any | None = None, + *constraints: Any, # AnnotationForm + bound: Any | None = None, # AnnotationForm contravariant: bool = False, covariant: bool = False, infer_variance: bool = False, - default: Any = ..., + default: Any = ..., # AnnotationForm ) -> Self: ... elif sys.version_info >= (3, 12): def __new__( cls, name: str, - *constraints: Any, - bound: Any | None = None, + *constraints: Any, # AnnotationForm + bound: Any | None = None, # AnnotationForm covariant: bool = False, contravariant: bool = False, infer_variance: bool = False, ) -> Self: ... elif sys.version_info >= (3, 11): def __new__( - cls, name: str, *constraints: Any, bound: Any | None = None, covariant: bool = False, contravariant: bool = False + cls, + name: str, + *constraints: Any, # AnnotationForm + bound: Any | None = None, # AnnotationForm + covariant: bool = False, + contravariant: bool = False, ) -> Self: ... else: def __init__( - self, name: str, *constraints: Any, bound: Any | None = None, covariant: bool = False, contravariant: bool = False + self, + name: str, + *constraints: Any, # AnnotationForm + bound: Any | None = None, # AnnotationForm + covariant: bool = False, + contravariant: bool = False, ) -> None: ... if sys.version_info >= (3, 10): - def __or__(self, right: Any) -> _SpecialForm: ... - def __ror__(self, left: Any) -> _SpecialForm: ... + def __or__(self, right: Any) -> _SpecialForm: ... # AnnotationForm + def __ror__(self, left: Any) -> _SpecialForm: ... # AnnotationForm if sys.version_info >= (3, 11): def __typing_subst__(self, arg: Any) -> Any: ... if sys.version_info >= (3, 13): def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... def has_default(self) -> bool: ... + if sys.version_info >= (3, 14): + @property + def evaluate_bound(self) -> EvaluateFunc | None: ... + @property + def evaluate_constraints(self) -> EvaluateFunc | None: ... + @property + def evaluate_default(self) -> EvaluateFunc | None: ... # Used for an undocumented mypy feature. Does not exist at runtime. +# Obsolete, use _typeshed._type_checker_internals.promote instead. _promote = object() # N.B. Keep this definition in sync with typing_extensions._SpecialForm @@ -232,10 +263,10 @@ if sys.version_info >= (3, 11): def __name__(self) -> str: ... if sys.version_info >= (3, 13): @property - def __default__(self) -> Any: ... + def __default__(self) -> Any: ... # AnnotationForm def has_default(self) -> bool: ... if sys.version_info >= (3, 13): - def __new__(cls, name: str, *, default: Any = ...) -> Self: ... + def __new__(cls, name: str, *, default: Any = ...) -> Self: ... # AnnotationForm elif sys.version_info >= (3, 12): def __new__(cls, name: str) -> Self: ... else: @@ -244,6 +275,9 @@ if sys.version_info >= (3, 11): def __iter__(self) -> Any: ... def __typing_subst__(self, arg: Never) -> Never: ... def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... + if sys.version_info >= (3, 14): + @property + def evaluate_default(self) -> EvaluateFunc | None: ... if sys.version_info >= (3, 10): @final @@ -275,7 +309,7 @@ if sys.version_info >= (3, 10): @property def __name__(self) -> str: ... @property - def __bound__(self) -> Any | None: ... + def __bound__(self) -> Any | None: ... # AnnotationForm @property def __covariant__(self) -> bool: ... @property @@ -285,35 +319,45 @@ if sys.version_info >= (3, 10): def __infer_variance__(self) -> bool: ... if sys.version_info >= (3, 13): @property - def __default__(self) -> Any: ... + def __default__(self) -> Any: ... # AnnotationForm if sys.version_info >= (3, 13): def __new__( cls, name: str, *, - bound: Any | None = None, + bound: Any | None = None, # AnnotationForm contravariant: bool = False, covariant: bool = False, infer_variance: bool = False, - default: Any = ..., + default: Any = ..., # AnnotationForm ) -> Self: ... elif sys.version_info >= (3, 12): def __new__( cls, name: str, *, - bound: Any | None = None, + bound: Any | None = None, # AnnotationForm contravariant: bool = False, covariant: bool = False, infer_variance: bool = False, ) -> Self: ... elif sys.version_info >= (3, 11): def __new__( - cls, name: str, *, bound: Any | None = None, contravariant: bool = False, covariant: bool = False + cls, + name: str, + *, + bound: Any | None = None, # AnnotationForm + contravariant: bool = False, + covariant: bool = False, ) -> Self: ... else: def __init__( - self, name: str, *, bound: Any | None = None, contravariant: bool = False, covariant: bool = False + self, + name: str, + *, + bound: Any | None = None, # AnnotationForm + contravariant: bool = False, + covariant: bool = False, ) -> None: ... @property @@ -328,13 +372,16 @@ if sys.version_info >= (3, 10): def __ror__(self, left: Any) -> _SpecialForm: ... if sys.version_info >= (3, 13): def has_default(self) -> bool: ... + if sys.version_info >= (3, 14): + @property + def evaluate_default(self) -> EvaluateFunc | None: ... Concatenate: _SpecialForm TypeAlias: _SpecialForm TypeGuard: _SpecialForm class NewType: - def __init__(self, name: str, tp: Any) -> None: ... + def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm if sys.version_info >= (3, 11): @staticmethod def __call__(x: _T, /) -> _T: ... @@ -531,6 +578,7 @@ class Coroutine(Awaitable[_ReturnT_nd_co], Generic[_YieldT_co, _SendT_nd_contra, # NOTE: This type does not exist in typing.py or PEP 484 but mypy needs it to exist. # The parameters correspond to Generator, but the 4th is the original type. +# Obsolete, use _typeshed._type_checker_internals.AwaitableGenerator instead. @type_check_only class AwaitableGenerator( Awaitable[_ReturnT_nd_co], @@ -697,7 +745,9 @@ class Mapping(Collection[_KT], Generic[_KT, _VT_co]): @overload def get(self, key: _KT, /) -> _VT_co | None: ... @overload - def get(self, key: _KT, /, default: _VT_co | _T) -> _VT_co | _T: ... + def get(self, key: _KT, /, default: _VT_co) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # Covariant type as parameter + @overload + def get(self, key: _KT, /, default: _T) -> _VT_co | _T: ... def items(self) -> ItemsView[_KT, _VT_co]: ... def keys(self) -> KeysView[_KT]: ... def values(self) -> ValuesView[_VT_co]: ... @@ -748,11 +798,15 @@ class MutableMapping(Mapping[_KT, _VT]): # -- weakref.WeakValueDictionary.__ior__ # -- weakref.WeakKeyDictionary.__ior__ @overload - def update(self, m: SupportsKeysAndGetItem[_KT, _VT], /, **kwargs: _VT) -> None: ... + def update(self, m: SupportsKeysAndGetItem[_KT, _VT], /) -> None: ... @overload - def update(self, m: Iterable[tuple[_KT, _VT]], /, **kwargs: _VT) -> None: ... + def update(self: Mapping[str, _VT], m: SupportsKeysAndGetItem[str, _VT], /, **kwargs: _VT) -> None: ... @overload - def update(self, **kwargs: _VT) -> None: ... + def update(self, m: Iterable[tuple[_KT, _VT]], /) -> None: ... + @overload + def update(self: Mapping[str, _VT], m: Iterable[tuple[str, _VT]], /, **kwargs: _VT) -> None: ... + @overload + def update(self: Mapping[str, _VT], **kwargs: _VT) -> None: ... Text = str @@ -858,13 +912,25 @@ _get_type_hints_obj_allowed_types: typing_extensions.TypeAlias = ( # noqa: Y042 | MethodDescriptorType ) -def get_type_hints( - obj: _get_type_hints_obj_allowed_types, - globalns: dict[str, Any] | None = None, - localns: Mapping[str, Any] | None = None, - include_extras: bool = False, -) -> dict[str, Any]: ... -def get_args(tp: Any) -> tuple[Any, ...]: ... +if sys.version_info >= (3, 14): + def get_type_hints( + obj: _get_type_hints_obj_allowed_types, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, + *, + format: Format | None = None, + ) -> dict[str, Any]: ... # AnnotationForm + +else: + def get_type_hints( + obj: _get_type_hints_obj_allowed_types, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, + ) -> dict[str, Any]: ... # AnnotationForm + +def get_args(tp: Any) -> tuple[Any, ...]: ... # AnnotationForm if sys.version_info >= (3, 10): @overload @@ -875,7 +941,7 @@ if sys.version_info >= (3, 10): @overload def get_origin(tp: GenericAlias) -> type: ... @overload -def get_origin(tp: Any) -> Any | None: ... +def get_origin(tp: Any) -> Any | None: ... # AnnotationForm @overload def cast(typ: type[_T], val: Any) -> _T: ... @overload @@ -886,7 +952,7 @@ def cast(typ: object, val: Any) -> Any: ... if sys.version_info >= (3, 11): def reveal_type(obj: _T, /) -> _T: ... def assert_never(arg: Never, /) -> Never: ... - def assert_type(val: _T, typ: Any, /) -> _T: ... + def assert_type(val: _T, typ: Any, /) -> _T: ... # AnnotationForm def clear_overloads() -> None: ... def get_overloads(func: Callable[..., object]) -> Sequence[Callable[..., object]]: ... def dataclass_transform( @@ -901,6 +967,7 @@ if sys.version_info >= (3, 11): # Type constructors +# Obsolete, will be changed to a function. Use _typeshed._type_checker_internals.NamedTupleFallback instead. class NamedTuple(tuple[Any, ...]): _field_defaults: ClassVar[dict[str, Any]] _fields: ClassVar[tuple[str, ...]] @@ -925,6 +992,7 @@ class NamedTuple(tuple[Any, ...]): # Internal mypy fallback type for all typed dicts (does not exist at runtime) # N.B. Keep this mostly in sync with typing_extensions._TypedDict/mypy_extensions._TypedDict +# Obsolete, use _typeshed._type_checker_internals.TypedDictFallback instead. @type_check_only class _TypedDict(Mapping[str, object], metaclass=ABCMeta): __total__: ClassVar[bool] @@ -960,56 +1028,70 @@ class _TypedDict(Mapping[str, object], metaclass=ABCMeta): # supposedly incompatible definitions of __or__ and __ior__ def __ior__(self, value: typing_extensions.Self, /) -> typing_extensions.Self: ... # type: ignore[misc] -@final -class ForwardRef(_Final): - __forward_arg__: str - __forward_code__: CodeType - __forward_evaluated__: bool - __forward_value__: Any | None - __forward_is_argument__: bool - __forward_is_class__: bool - __forward_module__: Any | None +if sys.version_info >= (3, 14): + from annotationlib import ForwardRef as ForwardRef - def __init__(self, arg: str, is_argument: bool = True, module: Any | None = None, *, is_class: bool = False) -> None: ... + def evaluate_forward_ref( + forward_ref: ForwardRef, + *, + owner: object = None, + globals: dict[str, Any] | None = None, + locals: Mapping[str, Any] | None = None, + type_params: tuple[TypeVar, ParamSpec, TypeVarTuple] | None = None, + format: Format | None = None, + ) -> Any: ... # AnnotationForm - if sys.version_info >= (3, 13): - @overload - @deprecated( - "Failing to pass a value to the 'type_params' parameter of ForwardRef._evaluate() is deprecated, " - "as it leads to incorrect behaviour when evaluating a stringified annotation " - "that references a PEP 695 type parameter. It will be disallowed in Python 3.15." - ) - def _evaluate( - self, globalns: dict[str, Any] | None, localns: Mapping[str, Any] | None, *, recursive_guard: frozenset[str] - ) -> Any | None: ... - @overload - def _evaluate( - self, - globalns: dict[str, Any] | None, - localns: Mapping[str, Any] | None, - type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...], - *, - recursive_guard: frozenset[str], - ) -> Any | None: ... - elif sys.version_info >= (3, 12): - def _evaluate( - self, - globalns: dict[str, Any] | None, - localns: Mapping[str, Any] | None, - type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] | None = None, - *, - recursive_guard: frozenset[str], - ) -> Any | None: ... - else: - def _evaluate( - self, globalns: dict[str, Any] | None, localns: Mapping[str, Any] | None, recursive_guard: frozenset[str] - ) -> Any | None: ... +else: + @final + class ForwardRef(_Final): + __forward_arg__: str + __forward_code__: CodeType + __forward_evaluated__: bool + __forward_value__: Any | None # AnnotationForm + __forward_is_argument__: bool + __forward_is_class__: bool + __forward_module__: Any | None - def __eq__(self, other: object) -> bool: ... - def __hash__(self) -> int: ... - if sys.version_info >= (3, 11): - def __or__(self, other: Any) -> _SpecialForm: ... - def __ror__(self, other: Any) -> _SpecialForm: ... + def __init__(self, arg: str, is_argument: bool = True, module: Any | None = None, *, is_class: bool = False) -> None: ... + + if sys.version_info >= (3, 13): + @overload + @deprecated( + "Failing to pass a value to the 'type_params' parameter of ForwardRef._evaluate() is deprecated, " + "as it leads to incorrect behaviour when evaluating a stringified annotation " + "that references a PEP 695 type parameter. It will be disallowed in Python 3.15." + ) + def _evaluate( + self, globalns: dict[str, Any] | None, localns: Mapping[str, Any] | None, *, recursive_guard: frozenset[str] + ) -> Any | None: ... # AnnotationForm + @overload + def _evaluate( + self, + globalns: dict[str, Any] | None, + localns: Mapping[str, Any] | None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...], + *, + recursive_guard: frozenset[str], + ) -> Any | None: ... # AnnotationForm + elif sys.version_info >= (3, 12): + def _evaluate( + self, + globalns: dict[str, Any] | None, + localns: Mapping[str, Any] | None, + type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] | None = None, + *, + recursive_guard: frozenset[str], + ) -> Any | None: ... # AnnotationForm + else: + def _evaluate( + self, globalns: dict[str, Any] | None, localns: Mapping[str, Any] | None, recursive_guard: frozenset[str] + ) -> Any | None: ... # AnnotationForm + + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + if sys.version_info >= (3, 11): + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... if sys.version_info >= (3, 10): def is_typeddict(tp: object) -> bool: ... @@ -1022,19 +1104,22 @@ if sys.version_info >= (3, 12): class TypeAliasType: def __new__(cls, name: str, value: Any, *, type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = ()) -> Self: ... @property - def __value__(self) -> Any: ... + def __value__(self) -> Any: ... # AnnotationForm @property def __type_params__(self) -> tuple[TypeVar | ParamSpec | TypeVarTuple, ...]: ... @property - def __parameters__(self) -> tuple[Any, ...]: ... + def __parameters__(self) -> tuple[Any, ...]: ... # AnnotationForm @property def __name__(self) -> str: ... # It's writable on types, but not on instances of TypeAliasType. @property def __module__(self) -> str | None: ... # type: ignore[override] - def __getitem__(self, parameters: Any) -> GenericAlias: ... + def __getitem__(self, parameters: Any) -> GenericAlias: ... # AnnotationForm def __or__(self, right: Any) -> _SpecialForm: ... def __ror__(self, left: Any) -> _SpecialForm: ... + if sys.version_info >= (3, 14): + @property + def evaluate_value(self) -> EvaluateFunc: ... if sys.version_info >= (3, 13): def is_protocol(tp: type, /) -> bool: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi new file mode 100644 index 0000000000000..3f7c257120814 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi @@ -0,0 +1,702 @@ +import abc +import enum +import sys +from _collections_abc import dict_items, dict_keys, dict_values +from _typeshed import AnnotationForm, IdentityFunction, Incomplete, Unused +from collections.abc import ( + AsyncGenerator as AsyncGenerator, + AsyncIterable as AsyncIterable, + AsyncIterator as AsyncIterator, + Awaitable as Awaitable, + Collection as Collection, + Container as Container, + Coroutine as Coroutine, + Generator as Generator, + Hashable as Hashable, + ItemsView as ItemsView, + Iterable as Iterable, + Iterator as Iterator, + KeysView as KeysView, + Mapping as Mapping, + MappingView as MappingView, + MutableMapping as MutableMapping, + MutableSequence as MutableSequence, + MutableSet as MutableSet, + Reversible as Reversible, + Sequence as Sequence, + Sized as Sized, + ValuesView as ValuesView, +) +from contextlib import AbstractAsyncContextManager as AsyncContextManager, AbstractContextManager as ContextManager +from re import Match as Match, Pattern as Pattern +from types import GenericAlias, ModuleType +from typing import ( # noqa: Y022,Y037,Y038,Y039,UP035 + IO as IO, + TYPE_CHECKING as TYPE_CHECKING, + AbstractSet as AbstractSet, + Any as Any, + AnyStr as AnyStr, + BinaryIO as BinaryIO, + Callable as Callable, + ChainMap as ChainMap, + ClassVar as ClassVar, + Counter as Counter, + DefaultDict as DefaultDict, + Deque as Deque, + Dict as Dict, + ForwardRef as ForwardRef, + FrozenSet as FrozenSet, + Generic as Generic, + List as List, + NoReturn as NoReturn, + Optional as Optional, + Set as Set, + Text as Text, + TextIO as TextIO, + Tuple as Tuple, + Type as Type, + TypedDict as TypedDict, + TypeVar as _TypeVar, + Union as Union, + _Alias, + cast as cast, + no_type_check as no_type_check, + no_type_check_decorator as no_type_check_decorator, + overload as overload, + type_check_only, +) + +if sys.version_info >= (3, 10): + from types import UnionType + +# Please keep order the same as at runtime. +__all__ = [ + # Super-special typing primitives. + "Any", + "ClassVar", + "Concatenate", + "Final", + "LiteralString", + "ParamSpec", + "ParamSpecArgs", + "ParamSpecKwargs", + "Self", + "Type", + "TypeVar", + "TypeVarTuple", + "Unpack", + # ABCs (from collections.abc). + "Awaitable", + "AsyncIterator", + "AsyncIterable", + "Coroutine", + "AsyncGenerator", + "AsyncContextManager", + "Buffer", + "ChainMap", + # Concrete collection types. + "ContextManager", + "Counter", + "Deque", + "DefaultDict", + "NamedTuple", + "OrderedDict", + "TypedDict", + # Structural checks, a.k.a. protocols. + "SupportsAbs", + "SupportsBytes", + "SupportsComplex", + "SupportsFloat", + "SupportsIndex", + "SupportsInt", + "SupportsRound", + "Reader", + "Writer", + # One-off things. + "Annotated", + "assert_never", + "assert_type", + "clear_overloads", + "dataclass_transform", + "deprecated", + "Doc", + "evaluate_forward_ref", + "get_overloads", + "final", + "Format", + "get_annotations", + "get_args", + "get_origin", + "get_original_bases", + "get_protocol_members", + "get_type_hints", + "IntVar", + "is_protocol", + "is_typeddict", + "Literal", + "NewType", + "overload", + "override", + "Protocol", + "Sentinel", + "reveal_type", + "runtime", + "runtime_checkable", + "Text", + "TypeAlias", + "TypeAliasType", + "TypeForm", + "TypeGuard", + "TypeIs", + "TYPE_CHECKING", + "Never", + "NoReturn", + "ReadOnly", + "Required", + "NotRequired", + "NoDefault", + "NoExtraItems", + # Pure aliases, have always been in typing + "AbstractSet", + "AnyStr", + "BinaryIO", + "Callable", + "Collection", + "Container", + "Dict", + "ForwardRef", + "FrozenSet", + "Generator", + "Generic", + "Hashable", + "IO", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "List", + "Mapping", + "MappingView", + "Match", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Optional", + "Pattern", + "Reversible", + "Sequence", + "Set", + "Sized", + "TextIO", + "Tuple", + "Union", + "ValuesView", + "cast", + "no_type_check", + "no_type_check_decorator", + # Added dynamically + "CapsuleType", +] + +_T = _TypeVar("_T") +_F = _TypeVar("_F", bound=Callable[..., Any]) +_TC = _TypeVar("_TC", bound=type[object]) +_T_co = _TypeVar("_T_co", covariant=True) # Any type covariant containers. +_T_contra = _TypeVar("_T_contra", contravariant=True) + +class _Final: ... # This should be imported from typing but that breaks pytype + +# unfortunately we have to duplicate this class definition from typing.pyi or we break pytype +class _SpecialForm(_Final): + def __getitem__(self, parameters: Any) -> object: ... + if sys.version_info >= (3, 10): + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... + +# Do not import (and re-export) Protocol or runtime_checkable from +# typing module because type checkers need to be able to distinguish +# typing.Protocol and typing_extensions.Protocol so they can properly +# warn users about potential runtime exceptions when using typing.Protocol +# on older versions of Python. +Protocol: _SpecialForm + +def runtime_checkable(cls: _TC) -> _TC: ... + +# This alias for above is kept here for backwards compatibility. +runtime = runtime_checkable +Final: _SpecialForm + +def final(f: _F) -> _F: ... + +Literal: _SpecialForm + +def IntVar(name: str) -> Any: ... # returns a new TypeVar + +# Internal mypy fallback type for all typed dicts (does not exist at runtime) +# N.B. Keep this mostly in sync with typing._TypedDict/mypy_extensions._TypedDict +@type_check_only +class _TypedDict(Mapping[str, object], metaclass=abc.ABCMeta): + __required_keys__: ClassVar[frozenset[str]] + __optional_keys__: ClassVar[frozenset[str]] + __total__: ClassVar[bool] + __orig_bases__: ClassVar[tuple[Any, ...]] + # PEP 705 + __readonly_keys__: ClassVar[frozenset[str]] + __mutable_keys__: ClassVar[frozenset[str]] + # PEP 728 + __closed__: ClassVar[bool] + __extra_items__: ClassVar[AnnotationForm] + def copy(self) -> Self: ... + # Using Never so that only calls using mypy plugin hook that specialize the signature + # can go through. + def setdefault(self, k: Never, default: object) -> object: ... + # Mypy plugin hook for 'pop' expects that 'default' has a type variable type. + def pop(self, k: Never, default: _T = ...) -> object: ... # pyright: ignore[reportInvalidTypeVarUse] + def update(self, m: Self, /) -> None: ... + def items(self) -> dict_items[str, object]: ... + def keys(self) -> dict_keys[str, object]: ... + def values(self) -> dict_values[str, object]: ... + def __delitem__(self, k: Never) -> None: ... + @overload + def __or__(self, value: Self, /) -> Self: ... + @overload + def __or__(self, value: dict[str, Any], /) -> dict[str, object]: ... + @overload + def __ror__(self, value: Self, /) -> Self: ... + @overload + def __ror__(self, value: dict[str, Any], /) -> dict[str, object]: ... + # supposedly incompatible definitions of `__ior__` and `__or__`: + # Since this module defines "Self" it is not recognized by Ruff as typing_extensions.Self + def __ior__(self, value: Self, /) -> Self: ... # type: ignore[misc] + +OrderedDict = _Alias() + +if sys.version_info >= (3, 13): + from typing import get_type_hints as get_type_hints +else: + def get_type_hints( + obj: Any, globalns: dict[str, Any] | None = None, localns: Mapping[str, Any] | None = None, include_extras: bool = False + ) -> dict[str, AnnotationForm]: ... + +def get_args(tp: AnnotationForm) -> tuple[AnnotationForm, ...]: ... + +if sys.version_info >= (3, 10): + @overload + def get_origin(tp: UnionType) -> type[UnionType]: ... + +@overload +def get_origin(tp: GenericAlias) -> type: ... +@overload +def get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ... +@overload +def get_origin(tp: AnnotationForm) -> AnnotationForm | None: ... + +Annotated: _SpecialForm +_AnnotatedAlias: Any # undocumented + +# New and changed things in 3.10 +if sys.version_info >= (3, 10): + from typing import ( + Concatenate as Concatenate, + ParamSpecArgs as ParamSpecArgs, + ParamSpecKwargs as ParamSpecKwargs, + TypeAlias as TypeAlias, + TypeGuard as TypeGuard, + is_typeddict as is_typeddict, + ) +else: + @final + class ParamSpecArgs: + @property + def __origin__(self) -> ParamSpec: ... + def __init__(self, origin: ParamSpec) -> None: ... + + @final + class ParamSpecKwargs: + @property + def __origin__(self) -> ParamSpec: ... + def __init__(self, origin: ParamSpec) -> None: ... + + Concatenate: _SpecialForm + TypeAlias: _SpecialForm + TypeGuard: _SpecialForm + def is_typeddict(tp: object) -> bool: ... + +# New and changed things in 3.11 +if sys.version_info >= (3, 11): + from typing import ( + LiteralString as LiteralString, + NamedTuple as NamedTuple, + Never as Never, + NewType as NewType, + NotRequired as NotRequired, + Required as Required, + Self as Self, + Unpack as Unpack, + assert_never as assert_never, + assert_type as assert_type, + clear_overloads as clear_overloads, + dataclass_transform as dataclass_transform, + get_overloads as get_overloads, + reveal_type as reveal_type, + ) +else: + Self: _SpecialForm + Never: _SpecialForm + def reveal_type(obj: _T, /) -> _T: ... + def assert_never(arg: Never, /) -> Never: ... + def assert_type(val: _T, typ: AnnotationForm, /) -> _T: ... + def clear_overloads() -> None: ... + def get_overloads(func: Callable[..., object]) -> Sequence[Callable[..., object]]: ... + + Required: _SpecialForm + NotRequired: _SpecialForm + LiteralString: _SpecialForm + Unpack: _SpecialForm + + def dataclass_transform( + *, + eq_default: bool = True, + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, + field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), + **kwargs: object, + ) -> IdentityFunction: ... + + class NamedTuple(tuple[Any, ...]): + _field_defaults: ClassVar[dict[str, Any]] + _fields: ClassVar[tuple[str, ...]] + __orig_bases__: ClassVar[tuple[Any, ...]] + @overload + def __init__(self, typename: str, fields: Iterable[tuple[str, Any]] = ...) -> None: ... + @overload + def __init__(self, typename: str, fields: None = None, **kwargs: Any) -> None: ... + @classmethod + def _make(cls, iterable: Iterable[Any]) -> Self: ... + def _asdict(self) -> dict[str, Any]: ... + def _replace(self, **kwargs: Any) -> Self: ... + + class NewType: + def __init__(self, name: str, tp: AnnotationForm) -> None: ... + def __call__(self, obj: _T, /) -> _T: ... + __supertype__: type | NewType + if sys.version_info >= (3, 10): + def __or__(self, other: Any) -> _SpecialForm: ... + def __ror__(self, other: Any) -> _SpecialForm: ... + +if sys.version_info >= (3, 12): + from collections.abc import Buffer as Buffer + from types import get_original_bases as get_original_bases + from typing import ( + SupportsAbs as SupportsAbs, + SupportsBytes as SupportsBytes, + SupportsComplex as SupportsComplex, + SupportsFloat as SupportsFloat, + SupportsIndex as SupportsIndex, + SupportsInt as SupportsInt, + SupportsRound as SupportsRound, + override as override, + ) +else: + def override(arg: _F, /) -> _F: ... + def get_original_bases(cls: type, /) -> tuple[Any, ...]: ... + + # mypy and pyright object to this being both ABC and Protocol. + # At runtime it inherits from ABC and is not a Protocol, but it is on the + # allowlist for use as a Protocol. + @runtime_checkable + class Buffer(Protocol, abc.ABC): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + # Not actually a Protocol at runtime; see + # https://github.com/python/typeshed/issues/10224 for why we're defining it this way + def __buffer__(self, flags: int, /) -> memoryview: ... + + @runtime_checkable + class SupportsInt(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def __int__(self) -> int: ... + + @runtime_checkable + class SupportsFloat(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def __float__(self) -> float: ... + + @runtime_checkable + class SupportsComplex(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def __complex__(self) -> complex: ... + + @runtime_checkable + class SupportsBytes(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def __bytes__(self) -> bytes: ... + + @runtime_checkable + class SupportsIndex(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def __index__(self) -> int: ... + + @runtime_checkable + class SupportsAbs(Protocol[_T_co]): + @abc.abstractmethod + def __abs__(self) -> _T_co: ... + + @runtime_checkable + class SupportsRound(Protocol[_T_co]): + @overload + @abc.abstractmethod + def __round__(self) -> int: ... + @overload + @abc.abstractmethod + def __round__(self, ndigits: int, /) -> _T_co: ... + +if sys.version_info >= (3, 14): + from io import Reader as Reader, Writer as Writer +else: + @runtime_checkable + class Reader(Protocol[_T_co]): + @abc.abstractmethod + def read(self, size: int = ..., /) -> _T_co: ... + + @runtime_checkable + class Writer(Protocol[_T_contra]): + @abc.abstractmethod + def write(self, data: _T_contra, /) -> int: ... + +if sys.version_info >= (3, 13): + from types import CapsuleType as CapsuleType + from typing import ( + NoDefault as NoDefault, + ParamSpec as ParamSpec, + ReadOnly as ReadOnly, + TypeIs as TypeIs, + TypeVar as TypeVar, + TypeVarTuple as TypeVarTuple, + get_protocol_members as get_protocol_members, + is_protocol as is_protocol, + ) + from warnings import deprecated as deprecated +else: + def is_protocol(tp: type, /) -> bool: ... + def get_protocol_members(tp: type, /) -> frozenset[str]: ... + @final + class _NoDefaultType: ... + + NoDefault: _NoDefaultType + @final + class CapsuleType: ... + + class deprecated: + message: LiteralString + category: type[Warning] | None + stacklevel: int + def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ... + def __call__(self, arg: _T, /) -> _T: ... + + @final + class TypeVar: + @property + def __name__(self) -> str: ... + @property + def __bound__(self) -> AnnotationForm | None: ... + @property + def __constraints__(self) -> tuple[AnnotationForm, ...]: ... + @property + def __covariant__(self) -> bool: ... + @property + def __contravariant__(self) -> bool: ... + @property + def __infer_variance__(self) -> bool: ... + @property + def __default__(self) -> AnnotationForm: ... + def __init__( + self, + name: str, + *constraints: AnnotationForm, + bound: AnnotationForm | None = None, + covariant: bool = False, + contravariant: bool = False, + default: AnnotationForm = ..., + infer_variance: bool = False, + ) -> None: ... + def has_default(self) -> bool: ... + def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... + if sys.version_info >= (3, 10): + def __or__(self, right: Any) -> _SpecialForm: ... + def __ror__(self, left: Any) -> _SpecialForm: ... + if sys.version_info >= (3, 11): + def __typing_subst__(self, arg: Any) -> Any: ... + + @final + class ParamSpec: + @property + def __name__(self) -> str: ... + @property + def __bound__(self) -> AnnotationForm | None: ... + @property + def __covariant__(self) -> bool: ... + @property + def __contravariant__(self) -> bool: ... + @property + def __infer_variance__(self) -> bool: ... + @property + def __default__(self) -> AnnotationForm: ... + def __init__( + self, + name: str, + *, + bound: None | AnnotationForm | str = None, + contravariant: bool = False, + covariant: bool = False, + default: AnnotationForm = ..., + ) -> None: ... + @property + def args(self) -> ParamSpecArgs: ... + @property + def kwargs(self) -> ParamSpecKwargs: ... + def has_default(self) -> bool: ... + def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... + if sys.version_info >= (3, 10): + def __or__(self, right: Any) -> _SpecialForm: ... + def __ror__(self, left: Any) -> _SpecialForm: ... + + @final + class TypeVarTuple: + @property + def __name__(self) -> str: ... + @property + def __default__(self) -> AnnotationForm: ... + def __init__(self, name: str, *, default: AnnotationForm = ...) -> None: ... + def __iter__(self) -> Any: ... # Unpack[Self] + def has_default(self) -> bool: ... + def __typing_prepare_subst__(self, alias: Any, args: Any) -> tuple[Any, ...]: ... + + ReadOnly: _SpecialForm + TypeIs: _SpecialForm + +# TypeAliasType was added in Python 3.12, but had significant changes in 3.14. +if sys.version_info >= (3, 14): + from typing import TypeAliasType as TypeAliasType +else: + @final + class TypeAliasType: + def __init__( + self, name: str, value: AnnotationForm, *, type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = () + ) -> None: ... + @property + def __value__(self) -> AnnotationForm: ... + @property + def __type_params__(self) -> tuple[TypeVar | ParamSpec | TypeVarTuple, ...]: ... + @property + # `__parameters__` can include special forms if a `TypeVarTuple` was + # passed as a `type_params` element to the constructor method. + def __parameters__(self) -> tuple[TypeVar | ParamSpec | AnnotationForm, ...]: ... + @property + def __name__(self) -> str: ... + # It's writable on types, but not on instances of TypeAliasType. + @property + def __module__(self) -> str | None: ... # type: ignore[override] + # Returns typing._GenericAlias, which isn't stubbed. + def __getitem__(self, parameters: Incomplete | tuple[Incomplete, ...]) -> AnnotationForm: ... + def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> NoReturn: ... + if sys.version_info >= (3, 10): + def __or__(self, right: Any) -> _SpecialForm: ... + def __ror__(self, left: Any) -> _SpecialForm: ... + +# PEP 727 +class Doc: + documentation: str + def __init__(self, documentation: str, /) -> None: ... + def __hash__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +# PEP 728 +class _NoExtraItemsType: ... + +NoExtraItems: _NoExtraItemsType + +# PEP 747 +TypeForm: _SpecialForm + +# PEP 649/749 +if sys.version_info >= (3, 14): + from typing import evaluate_forward_ref as evaluate_forward_ref + + from annotationlib import Format as Format, get_annotations as get_annotations +else: + class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + + @overload + def get_annotations( + obj: Any, # any object with __annotations__ or __annotate__ + *, + globals: Mapping[str, Any] | None = None, # value types depend on the key + locals: Mapping[str, Any] | None = None, # value types depend on the key + eval_str: bool = False, + format: Literal[Format.STRING], + ) -> dict[str, str]: ... + @overload + def get_annotations( + obj: Any, # any object with __annotations__ or __annotate__ + *, + globals: Mapping[str, Any] | None = None, # value types depend on the key + locals: Mapping[str, Any] | None = None, # value types depend on the key + eval_str: bool = False, + format: Literal[Format.FORWARDREF], + ) -> dict[str, AnnotationForm | ForwardRef]: ... + @overload + def get_annotations( + obj: Any, # any object with __annotations__ or __annotate__ + *, + globals: Mapping[str, Any] | None = None, # value types depend on the key + locals: Mapping[str, Any] | None = None, # value types depend on the key + eval_str: bool = False, + format: Format = Format.VALUE, # noqa: Y011 + ) -> dict[str, AnnotationForm]: ... + @overload + def evaluate_forward_ref( + forward_ref: ForwardRef, + *, + owner: Callable[..., object] | type[object] | ModuleType | None = None, # any callable, class, or module + globals: Mapping[str, Any] | None = None, # value types depend on the key + locals: Mapping[str, Any] | None = None, # value types depend on the key + type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None, + format: Literal[Format.STRING], + _recursive_guard: Container[str] = ..., + ) -> str: ... + @overload + def evaluate_forward_ref( + forward_ref: ForwardRef, + *, + owner: Callable[..., object] | type[object] | ModuleType | None = None, # any callable, class, or module + globals: Mapping[str, Any] | None = None, # value types depend on the key + locals: Mapping[str, Any] | None = None, # value types depend on the key + type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None, + format: Literal[Format.FORWARDREF], + _recursive_guard: Container[str] = ..., + ) -> AnnotationForm | ForwardRef: ... + @overload + def evaluate_forward_ref( + forward_ref: ForwardRef, + *, + owner: Callable[..., object] | type[object] | ModuleType | None = None, # any callable, class, or module + globals: Mapping[str, Any] | None = None, # value types depend on the key + locals: Mapping[str, Any] | None = None, # value types depend on the key + type_params: Iterable[TypeVar | ParamSpec | TypeVarTuple] | None = None, + format: Format | None = None, + _recursive_guard: Container[str] = ..., + ) -> AnnotationForm: ... + +# PEP 661 +class Sentinel: + def __init__(self, name: str, repr: str | None = None) -> None: ... + if sys.version_info >= (3, 14): + def __or__(self, other: Any) -> UnionType: ... # other can be any type form legal for unions + def __ror__(self, other: Any) -> UnionType: ... # other can be any type form legal for unions + elif sys.version_info >= (3, 10): + def __or__(self, other: Any) -> _SpecialForm: ... # other can be any type form legal for unions + def __ror__(self, other: Any) -> _SpecialForm: ... # other can be any type form legal for unions diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unicodedata.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unicodedata.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unicodedata.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unicodedata.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/_log.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/_log.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/_log.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/_log.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/case.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi similarity index 93% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/case.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi index 7d1a382a54a43..89bcabf104c25 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/case.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi @@ -18,6 +18,7 @@ _T = TypeVar("_T") _S = TypeVar("_S", bound=SupportsSub[Any, Any]) _E = TypeVar("_E", bound=BaseException) _FT = TypeVar("_FT", bound=Callable[..., Any]) +_SB = TypeVar("_SB", str, bytes, bytearray) _P = ParamSpec("_P") DIFF_OMITTED: Final[str] @@ -289,6 +290,16 @@ class TestCase: # Runtime has *args, **kwargs, but will error if any are supplied def __init_subclass__(cls, *args: Never, **kwargs: Never) -> None: ... + if sys.version_info >= (3, 14): + def assertIsSubclass(self, cls: type, superclass: type | tuple[type, ...], msg: Any = None) -> None: ... + def assertNotIsSubclass(self, cls: type, superclass: type | tuple[type, ...], msg: Any = None) -> None: ... + def assertHasAttr(self, obj: object, name: str, msg: Any = None) -> None: ... + def assertNotHasAttr(self, obj: object, name: str, msg: Any = None) -> None: ... + def assertStartsWith(self, s: _SB, prefix: _SB | tuple[_SB, ...], msg: Any = None) -> None: ... + def assertNotStartsWith(self, s: _SB, prefix: _SB | tuple[_SB, ...], msg: Any = None) -> None: ... + def assertEndsWith(self, s: _SB, suffix: _SB | tuple[_SB, ...], msg: Any = None) -> None: ... + def assertNotEndsWith(self, s: _SB, suffix: _SB | tuple[_SB, ...], msg: Any = None) -> None: ... + class FunctionTestCase(TestCase): def __init__( self, diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/loader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/loader.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/main.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/main.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi index d2664465097f3..9e353900f2d7f 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi @@ -1,4 +1,5 @@ import sys +from _typeshed import MaybeNone from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping, Sequence from contextlib import _GeneratorContextManager from types import TracebackType @@ -69,16 +70,13 @@ _CallValue: TypeAlias = str | tuple[Any, ...] | Mapping[str, Any] | _ArgsKwargs class _Call(tuple[Any, ...]): def __new__( - cls, value: _CallValue = (), name: str | None = "", parent: Any | None = None, two: bool = False, from_kall: bool = True + cls, value: _CallValue = (), name: str | None = "", parent: _Call | None = None, two: bool = False, from_kall: bool = True ) -> Self: ... - name: Any - parent: Any - from_kall: Any def __init__( self, value: _CallValue = (), name: str | None = None, - parent: Any | None = None, + parent: _Call | None = None, two: bool = False, from_kall: bool = True, ) -> None: ... @@ -162,7 +160,7 @@ class NonCallableMock(Base, Any): side_effect: Any called: bool call_count: int - call_args: Any + call_args: _Call | MaybeNone call_args_list: _CallList mock_calls: _CallList def _format_mock_call_signature(self, args: Any, kwargs: Any) -> str: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/result.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/result.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/result.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/result.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/runner.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/runner.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/runner.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/runner.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/signals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/signals.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/signals.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/signals.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/suite.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/suite.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/suite.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/suite.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/error.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/error.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/error.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/error.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/parse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/parse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/parse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/parse.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi new file mode 100644 index 0000000000000..d8fc5e0d8f48d --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi @@ -0,0 +1,416 @@ +import ssl +import sys +from _typeshed import ReadableBuffer, StrOrBytesPath, SupportsRead +from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence +from email.message import Message +from http.client import HTTPConnection, HTTPMessage, HTTPResponse +from http.cookiejar import CookieJar +from re import Pattern +from typing import IO, Any, ClassVar, NoReturn, Protocol, TypeVar, overload +from typing_extensions import TypeAlias, deprecated +from urllib.error import HTTPError as HTTPError +from urllib.response import addclosehook, addinfourl + +__all__ = [ + "Request", + "OpenerDirector", + "BaseHandler", + "HTTPDefaultErrorHandler", + "HTTPRedirectHandler", + "HTTPCookieProcessor", + "ProxyHandler", + "HTTPPasswordMgr", + "HTTPPasswordMgrWithDefaultRealm", + "HTTPPasswordMgrWithPriorAuth", + "AbstractBasicAuthHandler", + "HTTPBasicAuthHandler", + "ProxyBasicAuthHandler", + "AbstractDigestAuthHandler", + "HTTPDigestAuthHandler", + "ProxyDigestAuthHandler", + "HTTPHandler", + "FileHandler", + "FTPHandler", + "CacheFTPHandler", + "DataHandler", + "UnknownHandler", + "HTTPErrorProcessor", + "urlopen", + "install_opener", + "build_opener", + "pathname2url", + "url2pathname", + "getproxies", + "urlretrieve", + "urlcleanup", + "HTTPSHandler", +] +if sys.version_info < (3, 14): + __all__ += ["URLopener", "FancyURLopener"] + +_T = TypeVar("_T") +_UrlopenRet: TypeAlias = Any +_DataType: TypeAlias = ReadableBuffer | SupportsRead[bytes] | Iterable[bytes] | None + +if sys.version_info >= (3, 13): + def urlopen( + url: str | Request, data: _DataType | None = None, timeout: float | None = ..., *, context: ssl.SSLContext | None = None + ) -> _UrlopenRet: ... + +else: + def urlopen( + url: str | Request, + data: _DataType | None = None, + timeout: float | None = ..., + *, + cafile: str | None = None, + capath: str | None = None, + cadefault: bool = False, + context: ssl.SSLContext | None = None, + ) -> _UrlopenRet: ... + +def install_opener(opener: OpenerDirector) -> None: ... +def build_opener(*handlers: BaseHandler | Callable[[], BaseHandler]) -> OpenerDirector: ... + +if sys.version_info >= (3, 14): + def url2pathname(url: str, *, require_scheme: bool = False, resolve_host: bool = False) -> str: ... + def pathname2url(pathname: str, *, add_scheme: bool = False) -> str: ... + +else: + if sys.platform == "win32": + from nturl2path import pathname2url as pathname2url, url2pathname as url2pathname + else: + def url2pathname(pathname: str) -> str: ... + def pathname2url(pathname: str) -> str: ... + +def getproxies() -> dict[str, str]: ... +def getproxies_environment() -> dict[str, str]: ... +def parse_http_list(s: str) -> list[str]: ... +def parse_keqv_list(l: list[str]) -> dict[str, str]: ... + +if sys.platform == "win32" or sys.platform == "darwin": + def proxy_bypass(host: str) -> Any: ... # undocumented + +else: + def proxy_bypass(host: str, proxies: Mapping[str, str] | None = None) -> Any: ... # undocumented + +class Request: + @property + def full_url(self) -> str: ... + @full_url.setter + def full_url(self, value: str) -> None: ... + @full_url.deleter + def full_url(self) -> None: ... + type: str + host: str + origin_req_host: str + selector: str + data: _DataType + headers: MutableMapping[str, str] + unredirected_hdrs: dict[str, str] + unverifiable: bool + method: str | None + timeout: float | None # Undocumented, only set after __init__() by OpenerDirector.open() + def __init__( + self, + url: str, + data: _DataType = None, + headers: MutableMapping[str, str] = {}, + origin_req_host: str | None = None, + unverifiable: bool = False, + method: str | None = None, + ) -> None: ... + def get_method(self) -> str: ... + def add_header(self, key: str, val: str) -> None: ... + def add_unredirected_header(self, key: str, val: str) -> None: ... + def has_header(self, header_name: str) -> bool: ... + def remove_header(self, header_name: str) -> None: ... + def get_full_url(self) -> str: ... + def set_proxy(self, host: str, type: str) -> None: ... + @overload + def get_header(self, header_name: str) -> str | None: ... + @overload + def get_header(self, header_name: str, default: _T) -> str | _T: ... + def header_items(self) -> list[tuple[str, str]]: ... + def has_proxy(self) -> bool: ... + +class OpenerDirector: + addheaders: list[tuple[str, str]] + def add_handler(self, handler: BaseHandler) -> None: ... + def open(self, fullurl: str | Request, data: _DataType = None, timeout: float | None = ...) -> _UrlopenRet: ... + def error(self, proto: str, *args: Any) -> _UrlopenRet: ... + def close(self) -> None: ... + +class BaseHandler: + handler_order: ClassVar[int] + parent: OpenerDirector + def add_parent(self, parent: OpenerDirector) -> None: ... + def close(self) -> None: ... + def __lt__(self, other: object) -> bool: ... + +class HTTPDefaultErrorHandler(BaseHandler): + def http_error_default( + self, req: Request, fp: IO[bytes], code: int, msg: str, hdrs: HTTPMessage + ) -> HTTPError: ... # undocumented + +class HTTPRedirectHandler(BaseHandler): + max_redirections: ClassVar[int] # undocumented + max_repeats: ClassVar[int] # undocumented + inf_msg: ClassVar[str] # undocumented + def redirect_request( + self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage, newurl: str + ) -> Request | None: ... + def http_error_301(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + def http_error_302(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + def http_error_303(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + def http_error_307(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + if sys.version_info >= (3, 11): + def http_error_308( + self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage + ) -> _UrlopenRet | None: ... + +class HTTPCookieProcessor(BaseHandler): + cookiejar: CookieJar + def __init__(self, cookiejar: CookieJar | None = None) -> None: ... + def http_request(self, request: Request) -> Request: ... # undocumented + def http_response(self, request: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented + def https_request(self, request: Request) -> Request: ... # undocumented + def https_response(self, request: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented + +class ProxyHandler(BaseHandler): + def __init__(self, proxies: dict[str, str] | None = None) -> None: ... + def proxy_open(self, req: Request, proxy: str, type: str) -> _UrlopenRet | None: ... # undocumented + # TODO: add a method for every (common) proxy protocol + +class HTTPPasswordMgr: + def add_password(self, realm: str, uri: str | Sequence[str], user: str, passwd: str) -> None: ... + def find_user_password(self, realm: str, authuri: str) -> tuple[str | None, str | None]: ... + def is_suburi(self, base: str, test: str) -> bool: ... # undocumented + def reduce_uri(self, uri: str, default_port: bool = True) -> tuple[str, str]: ... # undocumented + +class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr): + def add_password(self, realm: str | None, uri: str | Sequence[str], user: str, passwd: str) -> None: ... + def find_user_password(self, realm: str | None, authuri: str) -> tuple[str | None, str | None]: ... + +class HTTPPasswordMgrWithPriorAuth(HTTPPasswordMgrWithDefaultRealm): + def add_password( + self, realm: str | None, uri: str | Sequence[str], user: str, passwd: str, is_authenticated: bool = False + ) -> None: ... + def update_authenticated(self, uri: str | Sequence[str], is_authenticated: bool = False) -> None: ... + def is_authenticated(self, authuri: str) -> bool | None: ... + +class AbstractBasicAuthHandler: + rx: ClassVar[Pattern[str]] # undocumented + passwd: HTTPPasswordMgr + add_password: Callable[[str, str | Sequence[str], str, str], None] + def __init__(self, password_mgr: HTTPPasswordMgr | None = None) -> None: ... + def http_error_auth_reqed(self, authreq: str, host: str, req: Request, headers: HTTPMessage) -> None: ... + def http_request(self, req: Request) -> Request: ... # undocumented + def http_response(self, req: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented + def https_request(self, req: Request) -> Request: ... # undocumented + def https_response(self, req: Request, response: HTTPResponse) -> HTTPResponse: ... # undocumented + def retry_http_basic_auth(self, host: str, req: Request, realm: str) -> _UrlopenRet | None: ... # undocumented + +class HTTPBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): + auth_header: ClassVar[str] # undocumented + def http_error_401(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + +class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): + auth_header: ClassVar[str] + def http_error_407(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + +class AbstractDigestAuthHandler: + def __init__(self, passwd: HTTPPasswordMgr | None = None) -> None: ... + def reset_retry_count(self) -> None: ... + def http_error_auth_reqed(self, auth_header: str, host: str, req: Request, headers: HTTPMessage) -> None: ... + def retry_http_digest_auth(self, req: Request, auth: str) -> _UrlopenRet | None: ... + def get_cnonce(self, nonce: str) -> str: ... + def get_authorization(self, req: Request, chal: Mapping[str, str]) -> str | None: ... + def get_algorithm_impls(self, algorithm: str) -> tuple[Callable[[str], str], Callable[[str, str], str]]: ... + def get_entity_digest(self, data: ReadableBuffer | None, chal: Mapping[str, str]) -> str | None: ... + +class HTTPDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): + auth_header: ClassVar[str] # undocumented + def http_error_401(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + +class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): + auth_header: ClassVar[str] # undocumented + def http_error_407(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> _UrlopenRet | None: ... + +class _HTTPConnectionProtocol(Protocol): + def __call__( + self, + host: str, + /, + *, + port: int | None = ..., + timeout: float = ..., + source_address: tuple[str, int] | None = ..., + blocksize: int = ..., + ) -> HTTPConnection: ... + +class AbstractHTTPHandler(BaseHandler): # undocumented + if sys.version_info >= (3, 12): + def __init__(self, debuglevel: int | None = None) -> None: ... + else: + def __init__(self, debuglevel: int = 0) -> None: ... + + def set_http_debuglevel(self, level: int) -> None: ... + def do_request_(self, request: Request) -> Request: ... + def do_open(self, http_class: _HTTPConnectionProtocol, req: Request, **http_conn_args: Any) -> HTTPResponse: ... + +class HTTPHandler(AbstractHTTPHandler): + def http_open(self, req: Request) -> HTTPResponse: ... + def http_request(self, request: Request) -> Request: ... # undocumented + +class HTTPSHandler(AbstractHTTPHandler): + if sys.version_info >= (3, 12): + def __init__( + self, debuglevel: int | None = None, context: ssl.SSLContext | None = None, check_hostname: bool | None = None + ) -> None: ... + else: + def __init__( + self, debuglevel: int = 0, context: ssl.SSLContext | None = None, check_hostname: bool | None = None + ) -> None: ... + + def https_open(self, req: Request) -> HTTPResponse: ... + def https_request(self, request: Request) -> Request: ... # undocumented + +class FileHandler(BaseHandler): + names: ClassVar[tuple[str, ...] | None] # undocumented + def file_open(self, req: Request) -> addinfourl: ... + def get_names(self) -> tuple[str, ...]: ... # undocumented + def open_local_file(self, req: Request) -> addinfourl: ... # undocumented + +class DataHandler(BaseHandler): + def data_open(self, req: Request) -> addinfourl: ... + +class ftpwrapper: # undocumented + def __init__( + self, user: str, passwd: str, host: str, port: int, dirs: str, timeout: float | None = None, persistent: bool = True + ) -> None: ... + def close(self) -> None: ... + def endtransfer(self) -> None: ... + def file_close(self) -> None: ... + def init(self) -> None: ... + def real_close(self) -> None: ... + def retrfile(self, file: str, type: str) -> tuple[addclosehook, int | None]: ... + +class FTPHandler(BaseHandler): + def ftp_open(self, req: Request) -> addinfourl: ... + def connect_ftp( + self, user: str, passwd: str, host: str, port: int, dirs: str, timeout: float + ) -> ftpwrapper: ... # undocumented + +class CacheFTPHandler(FTPHandler): + def setTimeout(self, t: float) -> None: ... + def setMaxConns(self, m: int) -> None: ... + def check_cache(self) -> None: ... # undocumented + def clear_cache(self) -> None: ... # undocumented + +class UnknownHandler(BaseHandler): + def unknown_open(self, req: Request) -> NoReturn: ... + +class HTTPErrorProcessor(BaseHandler): + def http_response(self, request: Request, response: HTTPResponse) -> _UrlopenRet: ... + def https_response(self, request: Request, response: HTTPResponse) -> _UrlopenRet: ... + +def urlretrieve( + url: str, + filename: StrOrBytesPath | None = None, + reporthook: Callable[[int, int, int], object] | None = None, + data: _DataType = None, +) -> tuple[str, HTTPMessage]: ... +def urlcleanup() -> None: ... + +if sys.version_info < (3, 14): + @deprecated("Deprecated since Python 3.3; Removed in 3.14; Use newer urlopen functions and methods.") + class URLopener: + version: ClassVar[str] + def __init__(self, proxies: dict[str, str] | None = None, **x509: str) -> None: ... + def open(self, fullurl: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... + def open_unknown(self, fullurl: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... + def retrieve( + self, + url: str, + filename: str | None = None, + reporthook: Callable[[int, int, int], object] | None = None, + data: ReadableBuffer | None = None, + ) -> tuple[str, Message | None]: ... + def addheader(self, *args: tuple[str, str]) -> None: ... # undocumented + def cleanup(self) -> None: ... # undocumented + def close(self) -> None: ... # undocumented + def http_error( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: bytes | None = None + ) -> _UrlopenRet: ... # undocumented + def http_error_default( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage + ) -> _UrlopenRet: ... # undocumented + def open_data(self, url: str, data: ReadableBuffer | None = None) -> addinfourl: ... # undocumented + def open_file(self, url: str) -> addinfourl: ... # undocumented + def open_ftp(self, url: str) -> addinfourl: ... # undocumented + def open_http(self, url: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... # undocumented + def open_https(self, url: str, data: ReadableBuffer | None = None) -> _UrlopenRet: ... # undocumented + def open_local_file(self, url: str) -> addinfourl: ... # undocumented + def open_unknown_proxy(self, proxy: str, fullurl: str, data: ReadableBuffer | None = None) -> None: ... # undocumented + def __del__(self) -> None: ... + + @deprecated("Deprecated since Python 3.3; Removed in 3.14; Use newer urlopen functions and methods.") + class FancyURLopener(URLopener): + def prompt_user_passwd(self, host: str, realm: str) -> tuple[str, str]: ... + def get_user_passwd(self, host: str, realm: str, clear_cache: int = 0) -> tuple[str, str]: ... # undocumented + def http_error_301( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None + ) -> _UrlopenRet | addinfourl | None: ... # undocumented + def http_error_302( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None + ) -> _UrlopenRet | addinfourl | None: ... # undocumented + def http_error_303( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None + ) -> _UrlopenRet | addinfourl | None: ... # undocumented + def http_error_307( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None + ) -> _UrlopenRet | addinfourl | None: ... # undocumented + if sys.version_info >= (3, 11): + def http_error_308( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None = None + ) -> _UrlopenRet | addinfourl | None: ... # undocumented + + def http_error_401( + self, + url: str, + fp: IO[bytes], + errcode: int, + errmsg: str, + headers: HTTPMessage, + data: ReadableBuffer | None = None, + retry: bool = False, + ) -> _UrlopenRet | None: ... # undocumented + def http_error_407( + self, + url: str, + fp: IO[bytes], + errcode: int, + errmsg: str, + headers: HTTPMessage, + data: ReadableBuffer | None = None, + retry: bool = False, + ) -> _UrlopenRet | None: ... # undocumented + def http_error_default( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage + ) -> addinfourl: ... # undocumented + def redirect_internal( + self, url: str, fp: IO[bytes], errcode: int, errmsg: str, headers: HTTPMessage, data: ReadableBuffer | None + ) -> _UrlopenRet | None: ... # undocumented + def retry_http_basic_auth( + self, url: str, realm: str, data: ReadableBuffer | None = None + ) -> _UrlopenRet | None: ... # undocumented + def retry_https_basic_auth( + self, url: str, realm: str, data: ReadableBuffer | None = None + ) -> _UrlopenRet | None: ... # undocumented + def retry_proxy_http_basic_auth( + self, url: str, realm: str, data: ReadableBuffer | None = None + ) -> _UrlopenRet | None: ... # undocumented + def retry_proxy_https_basic_auth( + self, url: str, realm: str, data: ReadableBuffer | None = None + ) -> _UrlopenRet | None: ... # undocumented diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/response.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/response.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/uu.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/uu.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/uu.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/uu.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/uuid.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi similarity index 78% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/uuid.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi index 3202ae212cae6..99ac6eb223ef3 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/uuid.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi @@ -1,7 +1,8 @@ import builtins import sys from enum import Enum -from typing_extensions import TypeAlias +from typing import Final +from typing_extensions import LiteralString, TypeAlias _FieldsType: TypeAlias = tuple[int, int, int, int, int, int] @@ -67,6 +68,11 @@ class UUID: def getnode() -> int: ... def uuid1(node: int | None = None, clock_seq: int | None = None) -> UUID: ... +if sys.version_info >= (3, 14): + def uuid6(node: int | None = None, clock_seq: int | None = None) -> UUID: ... + def uuid7() -> UUID: ... + def uuid8(a: int | None = None, b: int | None = None, c: int | None = None) -> UUID: ... + if sys.version_info >= (3, 12): def uuid3(namespace: UUID, name: str | bytes) -> UUID: ... @@ -81,14 +87,18 @@ if sys.version_info >= (3, 12): else: def uuid5(namespace: UUID, name: str) -> UUID: ... -NAMESPACE_DNS: UUID -NAMESPACE_URL: UUID -NAMESPACE_OID: UUID -NAMESPACE_X500: UUID -RESERVED_NCS: str -RFC_4122: str -RESERVED_MICROSOFT: str -RESERVED_FUTURE: str +if sys.version_info >= (3, 14): + NIL: Final[UUID] + MAX: Final[UUID] + +NAMESPACE_DNS: Final[UUID] +NAMESPACE_URL: Final[UUID] +NAMESPACE_OID: Final[UUID] +NAMESPACE_X500: Final[UUID] +RESERVED_NCS: Final[LiteralString] +RFC_4122: Final[LiteralString] +RESERVED_MICROSOFT: Final[LiteralString] +RESERVED_FUTURE: Final[LiteralString] if sys.version_info >= (3, 12): def main() -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/venv/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/venv/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/venv/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/venv/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/warnings.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/warnings.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/warnings.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/warnings.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wave.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wave.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/weakref.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/weakref.pyi similarity index 98% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/weakref.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/weakref.pyi index 593eb4615c8f4..334fab7e7468c 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/weakref.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/weakref.pyi @@ -99,6 +99,8 @@ class WeakValueDictionary(MutableMapping[_KT, _VT]): @overload def get(self, key: _KT, default: None = None) -> _VT | None: ... @overload + def get(self, key: _KT, default: _VT) -> _VT: ... + @overload def get(self, key: _KT, default: _T) -> _VT | _T: ... # These are incompatible with Mapping def keys(self) -> Iterator[_KT]: ... # type: ignore[override] @@ -149,6 +151,8 @@ class WeakKeyDictionary(MutableMapping[_KT, _VT]): @overload def get(self, key: _KT, default: None = None) -> _VT | None: ... @overload + def get(self, key: _KT, default: _VT) -> _VT: ... + @overload def get(self, key: _KT, default: _T) -> _VT | _T: ... # These are incompatible with Mapping def keys(self) -> Iterator[_KT]: ... # type: ignore[override] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/webbrowser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/webbrowser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/winreg.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/winreg.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/winreg.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/winreg.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/winsound.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/winsound.pyi similarity index 75% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/winsound.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/winsound.pyi index a20e81f94f98f..39dfa7b8b9c42 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/winsound.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/winsound.pyi @@ -13,12 +13,22 @@ if sys.platform == "win32": SND_NODEFAULT: Final = 2 SND_NOSTOP: Final = 16 SND_NOWAIT: Final = 8192 + if sys.version_info >= (3, 14): + SND_SENTRY: Final = 524288 + SND_SYNC: Final = 0 + SND_SYSTEM: Final = 2097152 MB_ICONASTERISK: Final = 64 MB_ICONEXCLAMATION: Final = 48 MB_ICONHAND: Final = 16 MB_ICONQUESTION: Final = 32 MB_OK: Final = 0 + if sys.version_info >= (3, 14): + MB_ICONERROR: Final = 16 + MB_ICONINFORMATION: Final = 64 + MB_ICONSTOP: Final = 16 + MB_ICONWARNING: Final = 48 + def Beep(frequency: int, duration: int) -> None: ... # Can actually accept anything ORed with 4, and if not it's definitely str, but that's inexpressible @overload diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xdrlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xdrlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xdrlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xdrlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi diff --git a/crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi similarity index 100% rename from crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi new file mode 100644 index 0000000000000..ebe92d28c74d9 --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi @@ -0,0 +1,42 @@ +import sys +from _typeshed import ReadableBuffer, StrPath, SupportsRead, _T_co +from collections.abc import Iterable +from typing import Protocol +from typing_extensions import TypeAlias +from xml.sax._exceptions import ( + SAXException as SAXException, + SAXNotRecognizedException as SAXNotRecognizedException, + SAXNotSupportedException as SAXNotSupportedException, + SAXParseException as SAXParseException, + SAXReaderNotAvailable as SAXReaderNotAvailable, +) +from xml.sax.handler import ContentHandler as ContentHandler, ErrorHandler as ErrorHandler +from xml.sax.xmlreader import InputSource as InputSource, XMLReader + +class _SupportsReadClose(SupportsRead[_T_co], Protocol[_T_co]): + def close(self) -> None: ... + +_Source: TypeAlias = StrPath | _SupportsReadClose[bytes] | _SupportsReadClose[str] + +default_parser_list: list[str] + +def make_parser(parser_list: Iterable[str] = ()) -> XMLReader: ... +def parse(source: _Source, handler: ContentHandler, errorHandler: ErrorHandler = ...) -> None: ... +def parseString(string: ReadableBuffer | str, handler: ContentHandler, errorHandler: ErrorHandler | None = ...) -> None: ... +def _create_parser(parser_name: str) -> XMLReader: ... + +if sys.version_info >= (3, 14): + __all__ = [ + "ContentHandler", + "ErrorHandler", + "InputSource", + "SAXException", + "SAXNotRecognizedException", + "SAXNotSupportedException", + "SAXParseException", + "SAXReaderNotAvailable", + "default_parser_list", + "make_parser", + "parse", + "parseString", + ] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi diff --git a/crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/__init__.py b/crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi similarity index 100% rename from crates/ruff_python_resolver/resources/test/airflow/airflow/jobs/__init__.py rename to crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xxlimited.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xxlimited.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xxlimited.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xxlimited.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipapp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipapp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipapp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipapp.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi new file mode 100644 index 0000000000000..27c1ef0246c7a --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi @@ -0,0 +1,387 @@ +import io +import sys +from _typeshed import SizedBuffer, StrOrBytesPath, StrPath +from collections.abc import Callable, Iterable, Iterator +from io import TextIOWrapper +from os import PathLike +from types import TracebackType +from typing import IO, Final, Literal, Protocol, overload +from typing_extensions import Self, TypeAlias + +__all__ = [ + "BadZipFile", + "BadZipfile", + "Path", + "error", + "ZIP_STORED", + "ZIP_DEFLATED", + "ZIP_BZIP2", + "ZIP_LZMA", + "is_zipfile", + "ZipInfo", + "ZipFile", + "PyZipFile", + "LargeZipFile", +] + +if sys.version_info >= (3, 14): + __all__ += ["ZIP_ZSTANDARD"] + +# TODO: use TypeAlias for these two when mypy bugs are fixed +# https://github.com/python/mypy/issues/16581 +_DateTuple = tuple[int, int, int, int, int, int] # noqa: Y026 +_ZipFileMode = Literal["r", "w", "x", "a"] # noqa: Y026 + +_ReadWriteMode: TypeAlias = Literal["r", "w"] + +class BadZipFile(Exception): ... + +BadZipfile = BadZipFile +error = BadZipfile + +class LargeZipFile(Exception): ... + +class _ZipStream(Protocol): + def read(self, n: int, /) -> bytes: ... + # The following methods are optional: + # def seekable(self) -> bool: ... + # def tell(self) -> int: ... + # def seek(self, n: int, /) -> object: ... + +# Stream shape as required by _EndRecData() and _EndRecData64(). +class _SupportsReadSeekTell(Protocol): + def read(self, n: int = ..., /) -> bytes: ... + def seek(self, cookie: int, whence: int, /) -> object: ... + def tell(self) -> int: ... + +class _ClosableZipStream(_ZipStream, Protocol): + def close(self) -> object: ... + +class ZipExtFile(io.BufferedIOBase): + MAX_N: int + MIN_READ_SIZE: int + MAX_SEEK_READ: int + newlines: list[bytes] | None + mode: _ReadWriteMode + name: str + @overload + def __init__( + self, fileobj: _ClosableZipStream, mode: _ReadWriteMode, zipinfo: ZipInfo, pwd: bytes | None, close_fileobj: Literal[True] + ) -> None: ... + @overload + def __init__( + self, + fileobj: _ClosableZipStream, + mode: _ReadWriteMode, + zipinfo: ZipInfo, + pwd: bytes | None = None, + *, + close_fileobj: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + fileobj: _ZipStream, + mode: _ReadWriteMode, + zipinfo: ZipInfo, + pwd: bytes | None = None, + close_fileobj: Literal[False] = False, + ) -> None: ... + def read(self, n: int | None = -1) -> bytes: ... + def readline(self, limit: int = -1) -> bytes: ... # type: ignore[override] + def peek(self, n: int = 1) -> bytes: ... + def read1(self, n: int | None) -> bytes: ... # type: ignore[override] + def seek(self, offset: int, whence: int = 0) -> int: ... + +class _Writer(Protocol): + def write(self, s: str, /) -> object: ... + +class _ZipReadable(Protocol): + def seek(self, offset: int, whence: int = 0, /) -> int: ... + def read(self, n: int = -1, /) -> bytes: ... + +class _ZipTellable(Protocol): + def tell(self) -> int: ... + +class _ZipReadableTellable(_ZipReadable, _ZipTellable, Protocol): ... + +class _ZipWritable(Protocol): + def flush(self) -> None: ... + def close(self) -> None: ... + def write(self, b: bytes, /) -> int: ... + +class ZipFile: + filename: str | None + debug: int + comment: bytes + filelist: list[ZipInfo] + fp: IO[bytes] | None + NameToInfo: dict[str, ZipInfo] + start_dir: int # undocumented + compression: int # undocumented + compresslevel: int | None # undocumented + mode: _ZipFileMode # undocumented + pwd: bytes | None # undocumented + # metadata_encoding is new in 3.11 + if sys.version_info >= (3, 11): + @overload + def __init__( + self, + file: StrPath | IO[bytes], + mode: _ZipFileMode = "r", + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + metadata_encoding: str | None = None, + ) -> None: ... + # metadata_encoding is only allowed for read mode + @overload + def __init__( + self, + file: StrPath | _ZipReadable, + mode: Literal["r"] = "r", + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + metadata_encoding: str | None = None, + ) -> None: ... + @overload + def __init__( + self, + file: StrPath | _ZipWritable, + mode: Literal["w", "x"] = ..., + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + metadata_encoding: None = None, + ) -> None: ... + @overload + def __init__( + self, + file: StrPath | _ZipReadableTellable, + mode: Literal["a"] = ..., + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + metadata_encoding: None = None, + ) -> None: ... + else: + @overload + def __init__( + self, + file: StrPath | IO[bytes], + mode: _ZipFileMode = "r", + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + ) -> None: ... + @overload + def __init__( + self, + file: StrPath | _ZipReadable, + mode: Literal["r"] = "r", + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + ) -> None: ... + @overload + def __init__( + self, + file: StrPath | _ZipWritable, + mode: Literal["w", "x"] = ..., + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + ) -> None: ... + @overload + def __init__( + self, + file: StrPath | _ZipReadableTellable, + mode: Literal["a"] = ..., + compression: int = 0, + allowZip64: bool = True, + compresslevel: int | None = None, + *, + strict_timestamps: bool = True, + ) -> None: ... + + def __enter__(self) -> Self: ... + def __exit__( + self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> None: ... + def close(self) -> None: ... + def getinfo(self, name: str) -> ZipInfo: ... + def infolist(self) -> list[ZipInfo]: ... + def namelist(self) -> list[str]: ... + def open( + self, name: str | ZipInfo, mode: _ReadWriteMode = "r", pwd: bytes | None = None, *, force_zip64: bool = False + ) -> IO[bytes]: ... + def extract(self, member: str | ZipInfo, path: StrPath | None = None, pwd: bytes | None = None) -> str: ... + def extractall( + self, path: StrPath | None = None, members: Iterable[str | ZipInfo] | None = None, pwd: bytes | None = None + ) -> None: ... + def printdir(self, file: _Writer | None = None) -> None: ... + def setpassword(self, pwd: bytes) -> None: ... + def read(self, name: str | ZipInfo, pwd: bytes | None = None) -> bytes: ... + def testzip(self) -> str | None: ... + def write( + self, + filename: StrPath, + arcname: StrPath | None = None, + compress_type: int | None = None, + compresslevel: int | None = None, + ) -> None: ... + def writestr( + self, + zinfo_or_arcname: str | ZipInfo, + data: SizedBuffer | str, + compress_type: int | None = None, + compresslevel: int | None = None, + ) -> None: ... + if sys.version_info >= (3, 11): + def mkdir(self, zinfo_or_directory_name: str | ZipInfo, mode: int = 0o777) -> None: ... + if sys.version_info >= (3, 14): + @property + def data_offset(self) -> int | None: ... + + def __del__(self) -> None: ... + +class PyZipFile(ZipFile): + def __init__( + self, file: str | IO[bytes], mode: _ZipFileMode = "r", compression: int = 0, allowZip64: bool = True, optimize: int = -1 + ) -> None: ... + def writepy(self, pathname: str, basename: str = "", filterfunc: Callable[[str], bool] | None = None) -> None: ... + +class ZipInfo: + filename: str + date_time: _DateTuple + compress_type: int + comment: bytes + extra: bytes + create_system: int + create_version: int + extract_version: int + reserved: int + flag_bits: int + volume: int + internal_attr: int + external_attr: int + header_offset: int + CRC: int + compress_size: int + file_size: int + orig_filename: str # undocumented + if sys.version_info >= (3, 13): + compress_level: int | None + + def __init__(self, filename: str = "NoName", date_time: _DateTuple = (1980, 1, 1, 0, 0, 0)) -> None: ... + @classmethod + def from_file(cls, filename: StrPath, arcname: StrPath | None = None, *, strict_timestamps: bool = True) -> Self: ... + def is_dir(self) -> bool: ... + def FileHeader(self, zip64: bool | None = None) -> bytes: ... + +if sys.version_info >= (3, 12): + from zipfile._path import CompleteDirs as CompleteDirs, Path as Path + +else: + class CompleteDirs(ZipFile): + def resolve_dir(self, name: str) -> str: ... + @overload + @classmethod + def make(cls, source: ZipFile) -> CompleteDirs: ... + @overload + @classmethod + def make(cls, source: StrPath | IO[bytes]) -> Self: ... + + class Path: + root: CompleteDirs + at: str + def __init__(self, root: ZipFile | StrPath | IO[bytes], at: str = "") -> None: ... + @property + def name(self) -> str: ... + @property + def parent(self) -> PathLike[str]: ... # undocumented + if sys.version_info >= (3, 10): + @property + def filename(self) -> PathLike[str]: ... # undocumented + if sys.version_info >= (3, 11): + @property + def suffix(self) -> str: ... + @property + def suffixes(self) -> list[str]: ... + @property + def stem(self) -> str: ... + + @overload + def open( + self, + mode: Literal["r", "w"] = "r", + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + line_buffering: bool = ..., + write_through: bool = ..., + *, + pwd: bytes | None = None, + ) -> TextIOWrapper: ... + @overload + def open(self, mode: Literal["rb", "wb"], *, pwd: bytes | None = None) -> IO[bytes]: ... + + if sys.version_info >= (3, 10): + def iterdir(self) -> Iterator[Self]: ... + else: + def iterdir(self) -> Iterator[Path]: ... + + def is_dir(self) -> bool: ... + def is_file(self) -> bool: ... + def exists(self) -> bool: ... + def read_text( + self, + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + line_buffering: bool = ..., + write_through: bool = ..., + ) -> str: ... + def read_bytes(self) -> bytes: ... + if sys.version_info >= (3, 10): + def joinpath(self, *other: StrPath) -> Path: ... + else: + def joinpath(self, add: StrPath) -> Path: ... # undocumented + + def __truediv__(self, add: StrPath) -> Path: ... + +def is_zipfile(filename: StrOrBytesPath | _SupportsReadSeekTell) -> bool: ... + +ZIP64_LIMIT: Final[int] +ZIP_FILECOUNT_LIMIT: Final[int] +ZIP_MAX_COMMENT: Final[int] + +ZIP_STORED: Final = 0 +ZIP_DEFLATED: Final = 8 +ZIP_BZIP2: Final = 12 +ZIP_LZMA: Final = 14 +if sys.version_info >= (3, 14): + ZIP_ZSTANDARD: Final = 93 + +DEFAULT_VERSION: Final[int] +ZIP64_VERSION: Final[int] +BZIP2_VERSION: Final[int] +LZMA_VERSION: Final[int] +if sys.version_info >= (3, 14): + ZSTANDARD_VERSION: Final[int] +MAX_EXTRACT_VERSION: Final[int] diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipimport.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.pyi similarity index 76% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipimport.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipimport.pyi index 3e94c681b7a2b..4aab318e7c71d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipimport.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.pyi @@ -1,10 +1,14 @@ import sys from _typeshed import StrOrBytesPath -from importlib.abc import ResourceReader from importlib.machinery import ModuleSpec from types import CodeType, ModuleType from typing_extensions import deprecated +if sys.version_info >= (3, 10): + from importlib.readers import ZipReader +else: + from importlib.abc import ResourceReader + if sys.version_info >= (3, 10): from _frozen_importlib_external import _LoaderBasics else: @@ -29,7 +33,13 @@ class zipimporter(_LoaderBasics): def get_code(self, fullname: str) -> CodeType: ... def get_data(self, pathname: str) -> bytes: ... def get_filename(self, fullname: str) -> str: ... - def get_resource_reader(self, fullname: str) -> ResourceReader | None: ... # undocumented + if sys.version_info >= (3, 14): + def get_resource_reader(self, fullname: str) -> ZipReader: ... # undocumented + elif sys.version_info >= (3, 10): + def get_resource_reader(self, fullname: str) -> ZipReader | None: ... # undocumented + else: + def get_resource_reader(self, fullname: str) -> ResourceReader | None: ... # undocumented + def get_source(self, fullname: str) -> str | None: ... def is_package(self, fullname: str) -> bool: ... @deprecated("Deprecated since 3.10; use exec_module() instead") diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi new file mode 100644 index 0000000000000..e9f54fbf2a26c --- /dev/null +++ b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi @@ -0,0 +1,34 @@ +import sys +from collections.abc import Iterable +from datetime import datetime, timedelta, tzinfo +from typing_extensions import Self +from zoneinfo._common import ZoneInfoNotFoundError as ZoneInfoNotFoundError, _IOBytes +from zoneinfo._tzpath import ( + TZPATH as TZPATH, + InvalidTZPathWarning as InvalidTZPathWarning, + available_timezones as available_timezones, + reset_tzpath as reset_tzpath, +) + +__all__ = ["ZoneInfo", "reset_tzpath", "available_timezones", "TZPATH", "ZoneInfoNotFoundError", "InvalidTZPathWarning"] + +class ZoneInfo(tzinfo): + @property + def key(self) -> str: ... + def __new__(cls, key: str) -> Self: ... + @classmethod + def no_cache(cls, key: str) -> Self: ... + if sys.version_info >= (3, 12): + @classmethod + def from_file(cls, file_obj: _IOBytes, /, key: str | None = None) -> Self: ... + else: + @classmethod + def from_file(cls, fobj: _IOBytes, /, key: str | None = None) -> Self: ... + + @classmethod + def clear_cache(cls, *, only_keys: Iterable[str] | None = None) -> None: ... + def tzname(self, dt: datetime | None, /) -> str | None: ... + def utcoffset(self, dt: datetime | None, /) -> timedelta | None: ... + def dst(self, dt: datetime | None, /) -> timedelta | None: ... + +def __dir__() -> list[str]: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi diff --git a/crates/ty_wasm/Cargo.toml b/crates/ty_wasm/Cargo.toml new file mode 100644 index 0000000000000..42dfb6611801b --- /dev/null +++ b/crates/ty_wasm/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "ty_wasm" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +description = "WebAssembly bindings for ty" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +ty_ide = { workspace = true } +ty_project = { workspace = true, default-features = false, features = [ + "deflate", + "format" +] } +ty_python_semantic = { workspace = true } + +ruff_db = { workspace = true, default-features = false, features = [] } +ruff_notebook = { workspace = true } +ruff_python_formatter = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } + +console_error_panic_hook = { workspace = true, optional = true } +console_log = { workspace = true } +js-sys = { workspace = true } +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 } +# Not a direct dependency but required to compile for Wasm. +uuid = { workspace = true, features = ["js"] } + +wasm-bindgen = { workspace = true } + +[dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ty_wasm/build.rs b/crates/ty_wasm/build.rs new file mode 100644 index 0000000000000..5355a7c669d38 --- /dev/null +++ b/crates/ty_wasm/build.rs @@ -0,0 +1,90 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; + +fn main() { + // The workspace root directory is not available without walking up the tree + // https://github.com/rust-lang/cargo/issues/3946 + let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join(".."); + + commit_info(&workspace_root); +} + +/// Retrieve commit information from the Git repository. +fn commit_info(workspace_root: &Path) { + // If not in a git repository, do not attempt to retrieve commit information + let git_dir = workspace_root.join(".git"); + if !git_dir.exists() { + return; + } + + if let Some(git_head_path) = git_head(&git_dir) { + println!("cargo:rerun-if-changed={}", git_head_path.display()); + + let git_head_contents = fs::read_to_string(git_head_path); + if let Ok(git_head_contents) = git_head_contents { + // The contents are either a commit or a reference in the following formats + // - "" when the head is detached + // - "ref " when working on a branch + // If a commit, checking if the HEAD file has changed is sufficient + // If a ref, we need to add the head file for that ref to rebuild on commit + let mut git_ref_parts = git_head_contents.split_whitespace(); + git_ref_parts.next(); + if let Some(git_ref) = git_ref_parts.next() { + let git_ref_path = git_dir.join(git_ref); + println!("cargo:rerun-if-changed={}", git_ref_path.display()); + } + } + } + + let output = match Command::new("git") + .arg("log") + .arg("-1") + .arg("--date=short") + .arg("--abbrev=9") + .arg("--format=%H %h %cd %(describe:tags)") + .current_dir(workspace_root) + .output() + { + Ok(output) if output.status.success() => output, + _ => return, + }; + + let stdout = String::from_utf8(output.stdout).unwrap(); + + let mut parts = stdout.split_whitespace(); + let mut next = || parts.next().unwrap(); + let _commit_hash = next(); + println!("cargo::rustc-env=TY_WASM_COMMIT_SHORT_HASH={}", next()); +} + +fn git_head(git_dir: &Path) -> Option { + // The typical case is a standard git repository. + let git_head_path = git_dir.join("HEAD"); + if git_head_path.exists() { + return Some(git_head_path); + } + if !git_dir.is_file() { + return None; + } + // If `.git/HEAD` doesn't exist and `.git` is actually a file, + // then let's try to attempt to read it as a worktree. If it's + // a worktree, then its contents will look like this, e.g.: + // + // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 + // + // And the HEAD file we want to watch will be at: + // + // /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD + let contents = fs::read_to_string(git_dir).ok()?; + let (label, worktree_path) = contents.split_once(':')?; + if label != "gitdir" { + return None; + } + let worktree_path = worktree_path.trim(); + Some(PathBuf::from(worktree_path)) +} diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs new file mode 100644 index 0000000000000..4533175828b24 --- /dev/null +++ b/crates/ty_wasm/src/lib.rs @@ -0,0 +1,990 @@ +use std::any::Any; + +use js_sys::{Error, JsString}; +use ruff_db::Db as _; +use ruff_db::diagnostic::{self, DisplayDiagnosticConfig}; +use ruff_db::files::{File, FileRange, system_path_to_file}; +use ruff_db::source::{line_index, source_text}; +use ruff_db::system::walk_directory::WalkDirectoryBuilder; +use ruff_db::system::{ + CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System, + SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, +}; +use ruff_notebook::Notebook; +use ruff_python_formatter::formatted_file; +use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; +use ruff_text_size::{Ranged, TextSize}; +use ty_ide::signature_help; +use ty_ide::{MarkupKind, goto_type_definition, hover, inlay_hints}; +use ty_project::ProjectMetadata; +use ty_project::metadata::options::Options; +use ty_project::metadata::value::ValueSource; +use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; +use ty_project::{Db, ProjectDatabase}; +use ty_python_semantic::Program; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn version() -> String { + option_env!("TY_WASM_COMMIT_SHORT_HASH") + .or_else(|| option_env!("CARGO_PKG_VERSION")) + .unwrap_or("unknown") + .to_string() +} + +#[wasm_bindgen(start)] +pub fn run() { + use log::Level; + + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + + console_log::init_with_level(Level::Debug).expect("Initializing logger went wrong."); +} + +#[wasm_bindgen] +pub struct Workspace { + db: ProjectDatabase, + position_encoding: PositionEncoding, + system: WasmSystem, +} + +#[wasm_bindgen] +impl Workspace { + #[wasm_bindgen(constructor)] + pub fn new( + root: &str, + position_encoding: PositionEncoding, + options: JsValue, + ) -> Result { + 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 project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None) + .map_err(into_error)?; + + let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?; + + Ok(Self { + db, + position_encoding, + system, + }) + } + + #[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)?; + + 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, self.db.vendored()) + .map_err(into_error)?; + Program::get(&self.db).update_from_settings(&mut self.db, program_settings); + + 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 { + let path = SystemPath::absolute(path, self.db.project().root(&self.db)); + + self.system + .fs + .write_file_all(&path, contents) + .map_err(into_error)?; + + self.db.apply_changes( + vec![ChangeEvent::Created { + path: path.clone(), + kind: CreatedKind::File, + }], + None, + ); + + let file = system_path_to_file(&self.db, &path).expect("File to exist"); + + self.db.project().open_file(&mut self.db, file); + + Ok(FileHandle { path, file }) + } + + #[wasm_bindgen(js_name = "updateFile")] + pub fn update_file(&mut self, file_id: &FileHandle, contents: &str) -> Result<(), Error> { + if !self.system.fs.exists(&file_id.path) { + return Err(Error::new("File does not exist")); + } + + self.system + .fs + .write_file(&file_id.path, contents) + .map_err(into_error)?; + + self.db.apply_changes( + vec![ + ChangeEvent::Changed { + path: file_id.path.to_path_buf(), + kind: ChangedKind::FileContent, + }, + ChangeEvent::Changed { + path: file_id.path.to_path_buf(), + kind: ChangedKind::FileMetadata, + }, + ], + None, + ); + + Ok(()) + } + + #[wasm_bindgen(js_name = "closeFile")] + #[allow( + clippy::needless_pass_by_value, + reason = "It's intentional that the file handle is consumed because it is no longer valid after closing" + )] + pub fn close_file(&mut self, file_id: FileHandle) -> Result<(), Error> { + let file = file_id.file; + + self.db.project().close_file(&mut self.db, file); + self.system + .fs + .remove_file(&file_id.path) + .map_err(into_error)?; + + self.db.apply_changes( + vec![ChangeEvent::Deleted { + path: file_id.path.to_path_buf(), + kind: DeletedKind::File, + }], + None, + ); + + Ok(()) + } + + /// Checks a single file. + #[wasm_bindgen(js_name = "checkFile")] + pub fn check_file(&self, file_id: &FileHandle) -> Result, Error> { + let result = self.db.check_file(file_id.file); + + Ok(result.into_iter().map(Diagnostic::wrap).collect()) + } + + /// Checks all open files + pub fn check(&self) -> Result, Error> { + let result = self.db.check(); + + Ok(result.into_iter().map(Diagnostic::wrap).collect()) + } + + /// Returns the parsed AST for `path` + pub fn parsed(&self, file_id: &FileHandle) -> Result { + let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file).load(&self.db); + + Ok(format!("{:#?}", parsed.syntax())) + } + + pub fn format(&self, file_id: &FileHandle) -> Result, Error> { + formatted_file(&self.db, file_id.file).map_err(into_error) + } + + /// Returns the token stream for `path` serialized as a string. + pub fn tokens(&self, file_id: &FileHandle) -> Result { + let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file).load(&self.db); + + Ok(format!("{:#?}", parsed.tokens())) + } + + #[wasm_bindgen(js_name = "sourceText")] + pub fn source_text(&self, file_id: &FileHandle) -> Result { + let source_text = ruff_db::source::source_text(&self.db, file_id.file); + + Ok(source_text.to_string()) + } + + #[wasm_bindgen(js_name = "gotoTypeDefinition")] + pub fn goto_type_definition( + &self, + file_id: &FileHandle, + position: Position, + ) -> Result, Error> { + let source = source_text(&self.db, file_id.file); + let index = line_index(&self.db, file_id.file); + + let offset = position.to_text_size(&source, &index, self.position_encoding)?; + + let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else { + return Ok(Vec::new()); + }; + + let source_range = Range::from_text_range( + targets.file_range().range(), + &index, + &source, + self.position_encoding, + ); + + let links: Vec<_> = targets + .into_iter() + .map(|target| LocationLink { + path: target.file().path(&self.db).to_string(), + full_range: Range::from_file_range( + &self.db, + FileRange::new(target.file(), target.full_range()), + self.position_encoding, + ), + selection_range: Some(Range::from_file_range( + &self.db, + FileRange::new(target.file(), target.focus_range()), + self.position_encoding, + )), + origin_selection_range: Some(source_range), + }) + .collect(); + + Ok(links) + } + + #[wasm_bindgen] + pub fn hover(&self, file_id: &FileHandle, position: Position) -> Result, Error> { + let source = source_text(&self.db, file_id.file); + let index = line_index(&self.db, file_id.file); + + let offset = position.to_text_size(&source, &index, self.position_encoding)?; + + let Some(range_info) = hover(&self.db, file_id.file, offset) else { + return Ok(None); + }; + + let source_range = Range::from_text_range( + range_info.file_range().range(), + &index, + &source, + self.position_encoding, + ); + + Ok(Some(Hover { + markdown: range_info + .display(&self.db, MarkupKind::Markdown) + .to_string(), + range: source_range, + })) + } + + #[wasm_bindgen] + pub fn completions( + &self, + file_id: &FileHandle, + position: Position, + ) -> Result, Error> { + let source = source_text(&self.db, file_id.file); + let index = line_index(&self.db, file_id.file); + + let offset = position.to_text_size(&source, &index, self.position_encoding)?; + + let completions = ty_ide::completion(&self.db, file_id.file, offset); + + Ok(completions + .into_iter() + .map(|completion| Completion { + kind: completion.kind(&self.db).map(CompletionKind::from), + name: completion.name.into(), + }) + .collect()) + } + + #[wasm_bindgen(js_name = "inlayHints")] + pub fn inlay_hints(&self, file_id: &FileHandle, range: Range) -> Result, Error> { + let index = line_index(&self.db, file_id.file); + let source = source_text(&self.db, file_id.file); + + let result = inlay_hints( + &self.db, + file_id.file, + range.to_text_range(&index, &source, self.position_encoding)?, + ); + + Ok(result + .into_iter() + .map(|hint| InlayHint { + markdown: hint.display(&self.db).to_string(), + position: Position::from_text_size( + hint.position, + &index, + &source, + self.position_encoding, + ), + }) + .collect()) + } + + #[wasm_bindgen(js_name = "semanticTokens")] + pub fn semantic_tokens(&self, file_id: &FileHandle) -> Result, Error> { + let index = line_index(&self.db, file_id.file); + let source = source_text(&self.db, file_id.file); + + let semantic_token = ty_ide::semantic_tokens(&self.db, file_id.file, None); + + let result = semantic_token + .iter() + .map(|token| SemanticToken { + kind: token.token_type.into(), + modifiers: token.modifiers.bits(), + range: Range::from_text_range(token.range, &index, &source, self.position_encoding), + }) + .collect::>(); + + Ok(result) + } + + #[wasm_bindgen(js_name = "semanticTokensInRange")] + pub fn semantic_tokens_in_range( + &self, + file_id: &FileHandle, + range: Range, + ) -> Result, Error> { + let index = line_index(&self.db, file_id.file); + let source = source_text(&self.db, file_id.file); + + let semantic_token = ty_ide::semantic_tokens( + &self.db, + file_id.file, + Some(range.to_text_range(&index, &source, self.position_encoding)?), + ); + + let result = semantic_token + .iter() + .map(|token| SemanticToken { + kind: token.token_type.into(), + modifiers: token.modifiers.bits(), + range: Range::from_text_range(token.range, &index, &source, self.position_encoding), + }) + .collect::>(); + + Ok(result) + } + + #[wasm_bindgen(js_name = "signatureHelp")] + pub fn signature_help( + &self, + file_id: &FileHandle, + position: Position, + ) -> Result, Error> { + let source = source_text(&self.db, file_id.file); + let index = line_index(&self.db, file_id.file); + + let offset = position.to_text_size(&source, &index, self.position_encoding)?; + + let Some(signature_help_info) = signature_help(&self.db, file_id.file, offset) else { + return Ok(None); + }; + + let signatures = signature_help_info + .signatures + .into_iter() + .map(|sig| { + let parameters = sig + .parameters + .into_iter() + .map(|param| ParameterInformation { + label: param.label, + documentation: param.documentation, + }) + .collect(); + + SignatureInformation { + label: sig.label, + documentation: sig.documentation, + parameters, + active_parameter: sig.active_parameter.and_then(|p| u32::try_from(p).ok()), + } + }) + .collect(); + + Ok(Some(SignatureHelp { + signatures, + active_signature: signature_help_info + .active_signature + .and_then(|s| u32::try_from(s).ok()), + })) + } +} + +pub(crate) fn into_error(err: E) -> Error { + Error::new(&err.to_string()) +} + +#[derive(Debug, Eq, PartialEq)] +#[wasm_bindgen(inspectable)] +pub struct FileHandle { + path: SystemPathBuf, + file: File, +} + +#[wasm_bindgen] +impl FileHandle { + #[wasm_bindgen(js_name = toString)] + pub fn js_to_string(&self) -> String { + format!("file(id: {:?}, path: {})", self.file, self.path) + } + + pub fn path(&self) -> String { + self.path.to_string() + } +} + +#[wasm_bindgen] +pub struct Diagnostic { + #[wasm_bindgen(readonly)] + inner: diagnostic::Diagnostic, +} + +#[wasm_bindgen] +impl Diagnostic { + fn wrap(diagnostic: diagnostic::Diagnostic) -> Self { + Self { inner: diagnostic } + } + + #[wasm_bindgen] + pub fn message(&self) -> JsString { + JsString::from(self.inner.concise_message().to_string()) + } + + #[wasm_bindgen] + pub fn id(&self) -> JsString { + JsString::from(self.inner.id().to_string()) + } + + #[wasm_bindgen] + pub fn severity(&self) -> Severity { + Severity::from(self.inner.severity()) + } + + #[wasm_bindgen(js_name = "textRange")] + pub fn text_range(&self) -> Option { + self.inner + .primary_span() + .and_then(|span| Some(TextRange::from(span.range()?))) + } + + #[wasm_bindgen(js_name = "toRange")] + pub fn to_range(&self, workspace: &Workspace) -> Option { + self.inner.primary_span().and_then(|span| { + Some(Range::from_file_range( + &workspace.db, + FileRange::new(span.expect_ty_file(), span.range()?), + workspace.position_encoding, + )) + }) + } + + #[wasm_bindgen] + pub fn display(&self, workspace: &Workspace) -> JsString { + let config = DisplayDiagnosticConfig::default().color(false); + self.inner + .display(&workspace.db, &config) + .to_string() + .into() + } +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Range { + pub start: Position, + pub end: Position, +} + +#[wasm_bindgen] +impl Range { + #[wasm_bindgen(constructor)] + pub fn new(start: Position, end: Position) -> Self { + Self { start, end } + } +} + +impl Range { + fn from_file_range( + db: &dyn Db, + file_range: FileRange, + position_encoding: PositionEncoding, + ) -> Self { + let index = line_index(db, file_range.file()); + let source = source_text(db, file_range.file()); + + Self::from_text_range(file_range.range(), &index, &source, position_encoding) + } + + fn from_text_range( + text_range: ruff_text_size::TextRange, + line_index: &LineIndex, + source: &str, + position_encoding: PositionEncoding, + ) -> Self { + Self { + start: Position::from_text_size( + text_range.start(), + line_index, + source, + position_encoding, + ), + end: Position::from_text_size(text_range.end(), line_index, source, position_encoding), + } + } + + fn to_text_range( + self, + line_index: &LineIndex, + source: &str, + position_encoding: PositionEncoding, + ) -> Result { + let start = self + .start + .to_text_size(source, line_index, position_encoding)?; + let end = self + .end + .to_text_size(source, line_index, position_encoding)?; + + Ok(ruff_text_size::TextRange::new(start, end)) + } +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Position { + /// One indexed line number + pub line: usize, + + /// One indexed column number (the nth character on the line) + pub column: usize, +} + +#[wasm_bindgen] +impl Position { + #[wasm_bindgen(constructor)] + pub fn new(line: usize, column: usize) -> Self { + Self { line, column } + } +} + +impl Position { + fn to_text_size( + self, + text: &str, + index: &LineIndex, + position_encoding: PositionEncoding, + ) -> Result { + let text_size = index.offset( + SourceLocation { + line: OneIndexed::new(self.line).ok_or_else(|| { + Error::new( + "Invalid value `0` for `position.line`. The line index is 1-indexed.", + ) + })?, + character_offset: OneIndexed::new(self.column).ok_or_else(|| { + Error::new( + "Invalid value `0` for `position.column`. The column index is 1-indexed.", + ) + })?, + }, + text, + position_encoding.into(), + ); + + Ok(text_size) + } + + fn from_text_size( + offset: TextSize, + line_index: &LineIndex, + source: &str, + position_encoding: PositionEncoding, + ) -> Self { + let location = line_index.source_location(offset, source, position_encoding.into()); + Self { + line: location.line.get(), + column: location.character_offset.get(), + } + } +} + +#[wasm_bindgen] +#[derive(Copy, Clone, Hash, PartialEq, Eq)] +pub enum Severity { + Info, + Warning, + Error, + Fatal, +} + +impl From for Severity { + fn from(value: diagnostic::Severity) -> Self { + match value { + diagnostic::Severity::Info => Self::Info, + diagnostic::Severity::Warning => Self::Warning, + diagnostic::Severity::Error => Self::Error, + diagnostic::Severity::Fatal => Self::Fatal, + } + } +} + +#[wasm_bindgen] +pub struct TextRange { + pub start: u32, + pub end: u32, +} + +impl From for TextRange { + fn from(value: ruff_text_size::TextRange) -> Self { + Self { + start: value.start().into(), + end: value.end().into(), + } + } +} + +#[derive(Default, Copy, Clone)] +#[wasm_bindgen] +pub enum PositionEncoding { + #[default] + Utf8, + Utf16, + Utf32, +} + +impl From for ruff_source_file::PositionEncoding { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::Utf8 => Self::Utf8, + PositionEncoding::Utf16 => Self::Utf16, + PositionEncoding::Utf32 => Self::Utf32, + } + } +} + +#[wasm_bindgen] +pub struct LocationLink { + /// The target file path + #[wasm_bindgen(getter_with_clone)] + pub path: String, + + /// The full range of the target + pub full_range: Range, + /// The target's range that should be selected/highlighted + pub selection_range: Option, + /// The range of the origin. + pub origin_selection_range: Option, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Hover { + #[wasm_bindgen(getter_with_clone)] + pub markdown: String, + + pub range: Range, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Completion { + #[wasm_bindgen(getter_with_clone)] + pub name: String, + pub kind: Option, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompletionKind { + Text, + Method, + Function, + Constructor, + Field, + Variable, + Class, + Interface, + Module, + Property, + Unit, + Value, + Enum, + Keyword, + Snippet, + Color, + File, + Reference, + Folder, + EnumMember, + Constant, + Struct, + Event, + Operator, + TypeParameter, +} + +impl From for CompletionKind { + fn from(value: ty_python_semantic::CompletionKind) -> Self { + match value { + ty_python_semantic::CompletionKind::Text => Self::Text, + ty_python_semantic::CompletionKind::Method => Self::Method, + ty_python_semantic::CompletionKind::Function => Self::Function, + ty_python_semantic::CompletionKind::Constructor => Self::Constructor, + ty_python_semantic::CompletionKind::Field => Self::Field, + ty_python_semantic::CompletionKind::Variable => Self::Variable, + ty_python_semantic::CompletionKind::Class => Self::Class, + ty_python_semantic::CompletionKind::Interface => Self::Interface, + ty_python_semantic::CompletionKind::Module => Self::Module, + ty_python_semantic::CompletionKind::Property => Self::Property, + ty_python_semantic::CompletionKind::Unit => Self::Unit, + ty_python_semantic::CompletionKind::Value => Self::Value, + ty_python_semantic::CompletionKind::Enum => Self::Enum, + ty_python_semantic::CompletionKind::Keyword => Self::Keyword, + ty_python_semantic::CompletionKind::Snippet => Self::Snippet, + ty_python_semantic::CompletionKind::Color => Self::Color, + ty_python_semantic::CompletionKind::File => Self::File, + ty_python_semantic::CompletionKind::Reference => Self::Reference, + ty_python_semantic::CompletionKind::Folder => Self::Folder, + ty_python_semantic::CompletionKind::EnumMember => Self::EnumMember, + ty_python_semantic::CompletionKind::Constant => Self::Constant, + ty_python_semantic::CompletionKind::Struct => Self::Struct, + ty_python_semantic::CompletionKind::Event => Self::Event, + ty_python_semantic::CompletionKind::Operator => Self::Operator, + ty_python_semantic::CompletionKind::TypeParameter => Self::TypeParameter, + } + } +} + +#[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InlayHint { + #[wasm_bindgen(getter_with_clone)] + pub markdown: String, + + pub position: Position, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticToken { + pub kind: SemanticTokenKind, + pub modifiers: u32, + pub range: Range, +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct SignatureHelp { + #[wasm_bindgen(getter_with_clone)] + pub signatures: Vec, + pub active_signature: Option, +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct SignatureInformation { + #[wasm_bindgen(getter_with_clone)] + pub label: String, + #[wasm_bindgen(getter_with_clone)] + pub documentation: Option, + #[wasm_bindgen(getter_with_clone)] + pub parameters: Vec, + pub active_parameter: Option, +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct ParameterInformation { + #[wasm_bindgen(getter_with_clone)] + pub label: String, + #[wasm_bindgen(getter_with_clone)] + pub documentation: Option, +} + +#[wasm_bindgen] +impl SemanticToken { + pub fn kinds() -> Vec { + ty_ide::SemanticTokenType::all() + .iter() + .map(|ty| ty.as_lsp_concept().to_string()) + .collect() + } + + pub fn modifiers() -> Vec { + ty_ide::SemanticTokenModifier::all_names() + .iter() + .map(|name| (*name).to_string()) + .collect() + } +} + +#[wasm_bindgen] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u32)] +pub enum SemanticTokenKind { + Namespace, + Class, + Parameter, + SelfParameter, + ClsParameter, + Variable, + Property, + Function, + Method, + Keyword, + String, + Number, + Decorator, + BuiltinConstant, + TypeParameter, +} + +impl From for SemanticTokenKind { + fn from(value: ty_ide::SemanticTokenType) -> Self { + match value { + ty_ide::SemanticTokenType::Namespace => Self::Namespace, + ty_ide::SemanticTokenType::Class => Self::Class, + ty_ide::SemanticTokenType::Parameter => Self::Parameter, + ty_ide::SemanticTokenType::SelfParameter => Self::SelfParameter, + ty_ide::SemanticTokenType::ClsParameter => Self::ClsParameter, + ty_ide::SemanticTokenType::Variable => Self::Variable, + ty_ide::SemanticTokenType::Property => Self::Property, + ty_ide::SemanticTokenType::Function => Self::Function, + ty_ide::SemanticTokenType::Method => Self::Method, + ty_ide::SemanticTokenType::Keyword => Self::Keyword, + ty_ide::SemanticTokenType::String => Self::String, + ty_ide::SemanticTokenType::Number => Self::Number, + ty_ide::SemanticTokenType::Decorator => Self::Decorator, + ty_ide::SemanticTokenType::BuiltinConstant => Self::BuiltinConstant, + ty_ide::SemanticTokenType::TypeParameter => Self::TypeParameter, + } + } +} + +#[derive(Debug, Clone)] +struct WasmSystem { + fs: MemoryFileSystem, +} + +impl WasmSystem { + fn new(root: &SystemPath) -> Self { + Self { + fs: MemoryFileSystem::with_current_directory(root), + } + } +} + +impl System for WasmSystem { + fn path_metadata(&self, path: &SystemPath) -> ruff_db::system::Result { + self.fs.metadata(path) + } + + fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result { + self.fs.canonicalize(path) + } + + fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result { + self.fs.read_to_string(path) + } + + fn read_to_notebook( + &self, + path: &SystemPath, + ) -> Result { + let content = self.read_to_string(path)?; + Notebook::from_source_code(&content) + } + + fn read_virtual_path_to_string( + &self, + _path: &SystemVirtualPath, + ) -> ruff_db::system::Result { + Err(not_found()) + } + + fn read_virtual_path_to_notebook( + &self, + _path: &SystemVirtualPath, + ) -> Result { + Err(ruff_notebook::NotebookError::Io(not_found())) + } + + fn path_exists_case_sensitive(&self, path: &SystemPath, _prefix: &SystemPath) -> bool { + self.path_exists(path) + } + + fn case_sensitivity(&self) -> CaseSensitivity { + CaseSensitivity::CaseSensitive + } + + fn current_directory(&self) -> &SystemPath { + self.fs.current_directory() + } + + fn user_config_directory(&self) -> Option { + None + } + + fn cache_dir(&self) -> Option { + None + } + + fn read_directory<'a>( + &'a self, + path: &SystemPath, + ) -> ruff_db::system::Result< + Box> + 'a>, + > { + Ok(Box::new(self.fs.read_directory(path)?)) + } + + fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder { + self.fs.walk_directory(path) + } + + fn glob( + &self, + pattern: &str, + ) -> Result> + '_>, PatternError> { + Ok(Box::new(self.fs.glob(pattern)?)) + } + + fn as_writable(&self) -> Option<&dyn WritableSystem> { + None + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +fn not_found() -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory") +} diff --git a/crates/ty_wasm/tests/api.rs b/crates/ty_wasm/tests/api.rs new file mode 100644 index 0000000000000..f98381f43b8fc --- /dev/null +++ b/crates/ty_wasm/tests/api.rs @@ -0,0 +1,34 @@ +#![cfg(target_arch = "wasm32")] + +use ty_wasm::{Position, PositionEncoding, Workspace}; +use wasm_bindgen_test::wasm_bindgen_test; + +#[wasm_bindgen_test] +fn check() { + let mut workspace = Workspace::new( + "/", + PositionEncoding::Utf32, + js_sys::JSON::parse("{}").unwrap(), + ) + .expect("Workspace to be created"); + + workspace + .open_file("test.py", "import random22\n") + .expect("File to be opened"); + + let result = workspace.check().expect("Check to succeed"); + + assert_eq!(result.len(), 1); + + let diagnostic = &result[0]; + + assert_eq!(diagnostic.id(), "unresolved-import"); + assert_eq!( + diagnostic.to_range(&workspace).unwrap().start, + Position { line: 1, column: 8 } + ); + assert_eq!( + diagnostic.message(), + "Cannot resolve imported module `random22`" + ); +} diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000000000..15782a99a6bf8 --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,75 @@ +[workspace] +members = ["cargo:."] +packages = ["ruff"] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.28.5-prerelease.1" +# Whether to consider the binaries in a package for distribution (defaults true) +dist = false +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell"] +# The archive format to use for windows builds (defaults .zip) +windows-archive = ".zip" +# The archive format to use for non-windows builds (defaults .tar.xz) +unix-archive = ".tar.gz" +# Target platforms to build apps for (Rust target-triple syntax) +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-pc-windows-msvc", + "arm-unknown-linux-musleabihf", + "armv7-unknown-linux-gnueabihf", + "armv7-unknown-linux-musleabihf", + "x86_64-apple-darwin", + "powerpc64-unknown-linux-gnu", + "powerpc64le-unknown-linux-gnu", + "s390x-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", + "i686-unknown-linux-gnu", + "i686-unknown-linux-musl", + "i686-pc-windows-msvc" +] +# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) +auto-includes = false +# Whether dist should create a Github Release or use an existing draft +create-release = true +# Which actions to run on pull requests +pr-run-mode = "plan" +# Whether CI should trigger releases with dispatches instead of tag pushes +dispatch-releases = true +# Which phase dist should use to create the GitHub release +github-release = "announce" +# Whether CI should include auto-generated code to build local artifacts +build-local-artifacts = false +# Local artifacts jobs to run in CI +local-artifacts-jobs = ["./build-binaries", "./build-docker"] +# Publish jobs to run in CI +publish-jobs = ["./publish-pypi", "./publish-wasm"] +# Post-announce jobs to run in CI +post-announce-jobs = [ + "./notify-dependents", + "./publish-docs", + "./publish-playground" +] +# Custom permissions for GitHub Jobs +github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } } +# Whether to install an updater program +install-updater = false +# Path that installers should place binaries in +install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] + +[dist.github-custom-runners] +global = "depot-ubuntu-latest-4" + +[dist.github-action-commits] +"actions/checkout" = "09d2acae674a48949e3602304ab46fd20ae0c42f" # v4 +"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 +"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 +"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 diff --git a/docs/configuration.md b/docs/configuration.md index 953a4baf59a8c..533c04c71dcd8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -51,7 +51,7 @@ If left unspecified, Ruff's default configuration is equivalent to: target-version = "py39" [tool.ruff.lint] - # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. + # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. select = ["E4", "E7", "E9", "F"] @@ -133,7 +133,7 @@ If left unspecified, Ruff's default configuration is equivalent to: target-version = "py39" [lint] - # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. + # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. select = ["E4", "E7", "E9", "F"] @@ -608,7 +608,7 @@ Options: RUFF_OUTPUT_FILE=] --target-version The minimum Python version that should be supported [possible values: - py37, py38, py39, py310, py311, py312, py313] + py37, py38, py39, py310, py311, py312, py313, py314] --preview Enable preview mode; checks will include unstable rules and fixes. Use `--no-preview` to disable @@ -723,7 +723,7 @@ Options: notebooks, use `--extension ipy:ipynb` --target-version The minimum Python version that should be supported [possible values: - py37, py38, py39, py310, py311, py312, py313] + py37, py38, py39, py310, py311, py312, py313, py314] --preview Enable preview mode; enables unstable formatting. Use `--no-preview` to disable diff --git a/docs/editors/settings.md b/docs/editors/settings.md index 1eb36af3a6d28..d15d145ef0323 100644 --- a/docs/editors/settings.md +++ b/docs/editors/settings.md @@ -66,7 +66,9 @@ _Using configuration file path:_ ```lua require('lspconfig').ruff.setup { init_options = { - configuration = "~/path/to/ruff.toml" + settings = { + configuration = "~/path/to/ruff.toml" + } } } ``` @@ -117,20 +119,22 @@ _Using inline configuration:_ ```lua require('lspconfig').ruff.setup { init_options = { - configuration = { - lint = { - unfixable = {"F401"}, - ["extend-select"] = {"TID251"}, - ["flake8-tidy-imports"] = { - ["banned-api"] = { - ["typing.TypedDict"] = { - msg = "Use `typing_extensions.TypedDict` instead" + settings = { + configuration = { + lint = { + unfixable = {"F401"}, + ["extend-select"] = {"TID251"}, + ["flake8-tidy-imports"] = { + ["banned-api"] = { + ["typing.TypedDict"] = { + msg = "Use `typing_extensions.TypedDict` instead" + } } } + }, + format = { + ["quote-style"] = "single" } - }, - format = { - ["quote-style"] = "single" } } } diff --git a/docs/editors/setup.md b/docs/editors/setup.md index 0603a6b029b5d..f50f3abbd395d 100644 --- a/docs/editors/setup.md +++ b/docs/editors/setup.md @@ -36,15 +36,31 @@ Ruff Language Server in Neovim. To set it up, install [configuration](https://github.com/neovim/nvim-lspconfig#configuration) documentation, and add the following to your `init.lua`: -```lua -require('lspconfig').ruff.setup({ - init_options = { - settings = { - -- Ruff language server settings go here - } - } -}) -``` +=== "Neovim 0.10 (with [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig))" + + ```lua + require('lspconfig').ruff.setup({ + init_options = { + settings = { + -- Ruff language server settings go here + } + } + }) + ``` + +=== "Neovim 0.11+ (with [`vim.lsp.config`](https://neovim.io/doc/user/lsp.html#vim.lsp.config()))" + + ```lua + vim.lsp.config('ruff', { + init_options = { + settings = { + -- Ruff language server settings go here + } + } + }) + + vim.lsp.enable('ruff') + ``` !!! note @@ -115,6 +131,63 @@ To view the trace logs between Neovim and Ruff, set the log level for Neovim's L vim.lsp.set_log_level('debug') ``` +
+With the conform.nvim plugin for Neovim. + +```lua +require("conform").setup({ + formatters_by_ft = { + python = { + -- To fix auto-fixable lint errors. + "ruff_fix", + -- To run the Ruff formatter. + "ruff_format", + -- To organize the imports. + "ruff_organize_imports", + }, + }, +}) +``` + +
+ +
+With the nvim-lint plugin for Neovim. + +```lua +require("lint").linters_by_ft = { + python = { "ruff" }, +} +``` + +
+ +
+With the ALE plugin for Neovim or Vim. + +Neovim (using Lua): + +```lua +-- Linters +vim.g.ale_linters = { python = { "ruff" } } +-- Fixers +vim.g.ale_fixers = { python = { "ruff", "ruff_format" } } +``` + +Vim (using Vimscript): + +```vim +" Linters +let g:ale_linters = { "python": ["ruff"] } +" Fixers +let g:ale_fixers = { "python": ["ruff", "ruff_format"] } +``` + +For the fixers, ruff will run ruff check --fix (to fix all auto-fixable +problems) whereas ruff_format will run ruff format. + +
+ ## Vim The [`vim-lsp`](https://github.com/prabirshrestha/vim-lsp) plugin can be used to configure the Ruff Language Server in Vim. @@ -153,24 +226,8 @@ endfunction Ruff is also available as part of the [coc-pyright](https://github.com/fannheyward/coc-pyright) extension for [coc.nvim](https://github.com/neoclide/coc.nvim). -
-With the ALE plugin for Vim or Neovim. - -```vim -" Linters -let g:ale_linters = { "python": ["ruff"] } -" Fixers -let g:ale_fixers = { "python": ["ruff", "ruff_format"] } -``` - -For the fixers, `ruff` will run `ruff check --fix` (to fix all auto-fixable problems) whereas -`ruff_format` will run `ruff format`. - -
-
Ruff can also be integrated via efm language server in just a few lines. -
Following is an example config for efm to use Ruff for linting and formatting Python files: @@ -187,38 +244,6 @@ tools:
-
-With the conform.nvim plugin for Neovim. -
- -```lua -require("conform").setup({ - formatters_by_ft = { - python = { - -- To fix auto-fixable lint errors. - "ruff_fix", - -- To run the Ruff formatter. - "ruff_format", - -- To organize the imports. - "ruff_organize_imports", - }, - }, -}) -``` - -
- -
-With the nvim-lint plugin for Neovim. - -```lua -require("lint").linters_by_ft = { - python = { "ruff" }, -} -``` - -
- ## Helix Open the [language configuration file](https://docs.helix-editor.com/languages.html#languagestoml-files) for diff --git a/docs/faq.md b/docs/faq.md index d2dde465305d7..6ed942e1f3b84 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -309,7 +309,31 @@ my_project When Ruff sees an import like `import foo`, it will then iterate over the `src` directories, looking for a corresponding Python module (in reality, a directory named `foo` or a file named -`foo.py`). +`foo.py`). For module paths with multiple components like `import foo.bar`, +the default behavior is to search only for a directory named `foo` or a file +named `foo.py`. However, if `preview` is enabled, Ruff will require that the full relative path `foo/bar` exists as a directory, or that `foo/bar.py` or `foo/bar.pyi` exist as files. Finally, imports of the form `from foo import bar`, Ruff will only use `foo` when determining whether a module is first-party or third-party. + +If there is a directory +whose name matches a third-party package, but does not contain Python code, +it could happen that the above algorithm incorrectly infers an import to be first-party. +To prevent this, you can modify the [`known-third-party`](settings.md#lint_isort_known-third-party) setting. For example, if you import +the package `wandb` but also have a subdirectory of your `src` with +the same name, you can add the following: + +=== "pyproject.toml" + + ```toml + [tool.ruff.lint.isort] + known-third-party = ["wandb"] + ``` + +=== "ruff.toml" + + ```toml + [lint.isort] + known-third-party = ["wandb"] + ``` + If the `src` field is omitted, Ruff will default to using the "project root", along with a `"src"` subdirectory, as the first-party sources, to support both flat and nested project layouts. diff --git a/docs/integrations.md b/docs/integrations.md index fe5cbf528690c..9639ee0ddc801 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.11.6-alpine + name: ghcr.io/astral-sh/ruff:0.12.3-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.12.3 hooks: # Run the linter. - id: ruff @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.12.3 hooks: # Run the linter. - id: ruff @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.12.3 hooks: # Run the linter. - id: ruff diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt index 3f423e6ee858d..0edb8fe579bcc 100644 --- a/docs/requirements-insiders.txt +++ b/docs/requirements-insiders.txt @@ -1,7 +1,8 @@ PyYAML==6.0.2 -ruff==0.9.10 +ruff==0.12.2 mkdocs==1.6.1 mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff mkdocs-redirects==1.2.2 mdformat==0.7.22 -mdformat-mkdocs==4.1.2 +mdformat-mkdocs==4.3.0 +mkdocs-github-admonitions-plugin @ git+https://github.com/PGijsbers/admonitions.git#7343d2f4a92e4d1491094530ef3d0d02d93afbb7 diff --git a/docs/requirements.txt b/docs/requirements.txt index fadd51dfd7c5a..25f8bec0b7d48 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,8 @@ PyYAML==6.0.2 -ruff==0.9.10 +ruff==0.12.2 mkdocs==1.6.1 mkdocs-material==9.5.38 mkdocs-redirects==1.2.2 mdformat==0.7.22 -mdformat-mkdocs==4.1.2 +mdformat-mkdocs==4.3.0 +mkdocs-github-admonitions-plugin @ git+https://github.com/PGijsbers/admonitions.git#7343d2f4a92e4d1491094530ef3d0d02d93afbb7 diff --git a/docs/tutorial.md b/docs/tutorial.md index 531d3c132a884..e350a7f0d9443 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.12.3 hooks: # Run the linter. - id: ruff diff --git a/fuzz/.gitignore b/fuzz/.gitignore index 0aae9a3e34315..41cc193fba36a 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,3 +1,3 @@ artifacts/ -corpus/ruff_fix_validity +corpus/ Cargo.lock diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2027caaefb6b3..e3d0cb63f811c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -17,8 +17,6 @@ libfuzzer = ["libfuzzer-sys/link_libfuzzer"] cargo-fuzz = true [dependencies] -red_knot_python_semantic = { path = "../crates/red_knot_python_semantic" } -red_knot_vendored = { path = "../crates/red_knot_vendored" } ruff_db = { path = "../crates/ruff_db" } ruff_linter = { path = "../crates/ruff_linter" } ruff_python_ast = { path = "../crates/ruff_python_ast" } @@ -28,8 +26,11 @@ ruff_source_file = { path = "../crates/ruff_source_file" } ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } +ty_python_semantic = { path = "../crates/ty_python_semantic" } +ty_vendored = { path = "../crates/ty_vendored" } + libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" } +salsa = { git = "https://github.com/salsa-rs/salsa", rev = "fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" } similar = { version = "2.5.0" } tracing = { version = "0.1.40" } @@ -38,8 +39,8 @@ tracing = { version = "0.1.40" } members = ["."] [[bin]] -name = "red_knot_check_invalid_syntax" -path = "fuzz_targets/red_knot_check_invalid_syntax.rs" +name = "ty_check_invalid_syntax" +path = "fuzz_targets/ty_check_invalid_syntax.rs" [[bin]] name = "ruff_parse_simple" diff --git a/fuzz/README.md b/fuzz/README.md index 5265149b8d8bf..8d249780d0a51 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -12,8 +12,11 @@ To use the fuzzers provided in this directory, start by invoking: This will install [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz) and optionally download a [dataset](https://zenodo.org/record/3628784) which improves the efficacy of the testing. -**This step is necessary for initialising the corpus directory, as all fuzzers share a common -corpus.** + +> [!NOTE] +> +> This step is necessary for initialising the corpus directory, as all fuzzers share a common corpus. + The dataset may take several hours to download and clean, so if you're just looking to try out the fuzzers, skip the dataset download, though be warned that some features simply cannot be tested without it (very unlikely for the fuzzer to generate valid python code from "thin air"). @@ -24,13 +27,23 @@ Once you have initialised the fuzzers, you can then execute any fuzzer with: cargo fuzz run -s none name_of_fuzzer -- -timeout=1 ``` -**Users using Apple M1 devices must use a nightly compiler and omit the `-s none` portion of this -command, as this architecture does not support fuzzing without a sanitizer.** +> [!NOTE] +> +> Users using Apple M1 devices must use a nightly compiler and omit the `-s none` portion of this +> command, as this architecture does not support fuzzing without a sanitizer. +> +> ```shell +> cargo +nightly fuzz run name_of_fuzzer -- -timeout=1 +> ``` + You can view the names of the available fuzzers with `cargo fuzz list`. For specific details about how each fuzzer works, please read this document in its entirety. -**IMPORTANT: You should run `./reinit-fuzzer.sh` after adding more file-based testcases.** This will -allow the testing of new features that you've added unit tests for. +> [!NOTE] +> +> Re-run `./init-fuzzer.sh` (say no to the dataset download) after adding more file-based test cases +> to the repository. This will make sure that the corpus is up to date with any new Python code +> added to the repository. ### Debugging a crash @@ -74,9 +87,9 @@ Each fuzzer harness in [`fuzz_targets`](fuzz_targets) targets a different aspect them in different ways. While there is implementation-specific documentation in the source code itself, each harness is briefly described below. -### `red_knot_check_invalid_syntax` +### `ty_check_invalid_syntax` -This fuzz harness checks that the type checker (Red Knot) does not panic when checking a source +This fuzz harness checks that the type checker (ty) does not panic when checking a source file with invalid syntax. This rejects any corpus entries that is already valid Python code. Currently, this is limited to syntax errors that's produced by Ruff's Python parser which means that it does not cover all possible syntax errors (). diff --git a/fuzz/corpus/red_knot_check_invalid_syntax b/fuzz/corpus/red_knot_check_invalid_syntax deleted file mode 120000 index 38dc5bc1ea310..0000000000000 --- a/fuzz/corpus/red_knot_check_invalid_syntax +++ /dev/null @@ -1 +0,0 @@ -ruff_fix_validity \ No newline at end of file diff --git a/fuzz/corpus/ruff_formatter_idempotency b/fuzz/corpus/ruff_formatter_idempotency deleted file mode 120000 index 38dc5bc1ea310..0000000000000 --- a/fuzz/corpus/ruff_formatter_idempotency +++ /dev/null @@ -1 +0,0 @@ -ruff_fix_validity \ No newline at end of file diff --git a/fuzz/corpus/ruff_formatter_validity b/fuzz/corpus/ruff_formatter_validity deleted file mode 120000 index 38dc5bc1ea310..0000000000000 --- a/fuzz/corpus/ruff_formatter_validity +++ /dev/null @@ -1 +0,0 @@ -ruff_fix_validity \ No newline at end of file diff --git a/fuzz/corpus/ruff_new_parser_equiv b/fuzz/corpus/ruff_new_parser_equiv deleted file mode 120000 index 38dc5bc1ea310..0000000000000 --- a/fuzz/corpus/ruff_new_parser_equiv +++ /dev/null @@ -1 +0,0 @@ -ruff_fix_validity \ No newline at end of file diff --git a/fuzz/corpus/ruff_parse_idempotency b/fuzz/corpus/ruff_parse_idempotency deleted file mode 120000 index 61e7ad4b4cd6f..0000000000000 --- a/fuzz/corpus/ruff_parse_idempotency +++ /dev/null @@ -1 +0,0 @@ -ruff_parse_simple \ No newline at end of file diff --git a/fuzz/corpus/ruff_parse_simple b/fuzz/corpus/ruff_parse_simple deleted file mode 120000 index 018c02efec25c..0000000000000 --- a/fuzz/corpus/ruff_parse_simple +++ /dev/null @@ -1 +0,0 @@ -ruff_fix_validity/ \ No newline at end of file diff --git a/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs b/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs deleted file mode 100644 index 1da7da48dd3c0..0000000000000 --- a/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Fuzzer harness that runs the type checker to catch for panics for source code containing -//! syntax errors. - -#![no_main] - -use std::sync::{Arc, Mutex, OnceLock}; - -use libfuzzer_sys::{fuzz_target, Corpus}; - -use red_knot_python_semantic::lint::LintRegistry; -use red_knot_python_semantic::types::check_types; -use red_knot_python_semantic::{ - default_lint_registry, lint::RuleSelection, Db as SemanticDb, Program, ProgramSettings, - PythonPlatform, SearchPathSettings, -}; -use ruff_db::files::{system_path_to_file, File, Files}; -use ruff_db::system::{ - DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem, -}; -use ruff_db::vendored::VendoredFileSystem; -use ruff_db::{Db as SourceDb, Upcast}; -use ruff_python_ast::PythonVersion; -use ruff_python_parser::{parse_unchecked, Mode, ParseOptions}; - -/// Database that can be used for testing. -/// -/// Uses an in memory filesystem and it stubs out the vendored files by default. -#[salsa::db] -#[derive(Clone)] -struct TestDb { - storage: salsa::Storage, - files: Files, - system: TestSystem, - vendored: VendoredFileSystem, - events: Arc>>, - rule_selection: Arc, -} - -impl TestDb { - fn new() -> Self { - Self { - storage: salsa::Storage::default(), - system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), - events: Arc::default(), - files: Files::default(), - rule_selection: RuleSelection::from_registry(default_lint_registry()).into(), - } - } -} - -#[salsa::db] -impl SourceDb for TestDb { - fn vendored(&self) -> &VendoredFileSystem { - &self.vendored - } - - fn system(&self) -> &dyn System { - &self.system - } - - fn files(&self) -> &Files { - &self.files - } -} - -impl DbWithTestSystem for TestDb { - fn test_system(&self) -> &TestSystem { - &self.system - } - - fn test_system_mut(&mut self) -> &mut TestSystem { - &mut self.system - } -} - -impl Upcast for TestDb { - fn upcast(&self) -> &(dyn SourceDb + 'static) { - self - } - fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) { - self - } -} - -#[salsa::db] -impl SemanticDb for TestDb { - fn is_file_open(&self, file: File) -> bool { - !file.path(self).is_vendored_path() - } - - fn rule_selection(&self) -> Arc { - self.rule_selection.clone() - } - - fn lint_registry(&self) -> &LintRegistry { - default_lint_registry() - } -} - -#[salsa::db] -impl salsa::Database for TestDb { - fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) { - let event = event(); - tracing::trace!("event: {:?}", event); - let mut events = self.events.lock().unwrap(); - events.push(event); - } -} - -fn setup_db() -> TestDb { - let db = TestDb::new(); - - let src_root = SystemPathBuf::from("/src"); - db.memory_file_system() - .create_directory_all(&src_root) - .unwrap(); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersion::default(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings::new(vec![src_root]), - }, - ) - .expect("Valid search path settings"); - - db -} - -static TEST_DB: OnceLock> = OnceLock::new(); - -fn do_fuzz(case: &[u8]) -> Corpus { - let Ok(code) = std::str::from_utf8(case) else { - return Corpus::Reject; - }; - - let parsed = parse_unchecked(code, ParseOptions::from(Mode::Module)); - if parsed.has_valid_syntax() { - return Corpus::Reject; - } - - let mut db = TEST_DB - .get_or_init(|| Mutex::new(setup_db())) - .lock() - .unwrap(); - - for path in &["/src/a.py", "/src/a.pyi"] { - db.write_file(path, code).unwrap(); - let file = system_path_to_file(&*db, path).unwrap(); - check_types(&*db, file); - db.memory_file_system().remove_file(path).unwrap(); - file.sync(&mut *db); - } - - Corpus::Keep -} - -fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) }); diff --git a/fuzz/fuzz_targets/ruff_formatter_validity.rs b/fuzz/fuzz_targets/ruff_formatter_validity.rs index cd7b39e78ec91..b9bdca7c25c34 100644 --- a/fuzz/fuzz_targets/ruff_formatter_validity.rs +++ b/fuzz/fuzz_targets/ruff_formatter_validity.rs @@ -43,7 +43,7 @@ fn do_fuzz(case: &[u8]) -> Corpus { let mut warnings = HashMap::new(); - for msg in &linter_result.messages { + for msg in &linter_result.diagnostics { let count: &mut usize = warnings.entry(msg.name()).or_default(); *count += 1; } @@ -67,7 +67,7 @@ fn do_fuzz(case: &[u8]) -> Corpus { "formatter introduced a parse error" ); - for msg in &linter_result.messages { + for msg in &linter_result.diagnostics { if let Some(count) = warnings.get_mut(msg.name()) { if let Some(decremented) = count.checked_sub(1) { *count = decremented; diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs new file mode 100644 index 0000000000000..4dd62cd0c53bd --- /dev/null +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -0,0 +1,151 @@ +//! Fuzzer harness that runs the type checker to catch for panics for source code containing +//! syntax errors. + +#![no_main] + +use std::sync::{Arc, Mutex, OnceLock}; + +use libfuzzer_sys::{Corpus, fuzz_target}; + +use ruff_db::Db as SourceDb; +use ruff_db::files::{File, Files, system_path_to_file}; +use ruff_db::system::{ + DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem, +}; +use ruff_db::vendored::VendoredFileSystem; +use ruff_python_ast::PythonVersion; +use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; +use ty_python_semantic::lint::LintRegistry; +use ty_python_semantic::types::check_types; +use ty_python_semantic::{ + Db as SemanticDb, Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, + SearchPathSettings, default_lint_registry, lint::RuleSelection, +}; + +/// Database that can be used for testing. +/// +/// Uses an in memory filesystem and it stubs out the vendored files by default. +#[salsa::db] +#[derive(Clone)] +struct TestDb { + storage: salsa::Storage, + files: Files, + system: TestSystem, + vendored: VendoredFileSystem, + rule_selection: Arc, +} + +impl TestDb { + fn new() -> Self { + Self { + storage: salsa::Storage::new(Some(Box::new({ + move |event| { + tracing::trace!("event: {:?}", event); + } + }))), + system: TestSystem::default(), + vendored: ty_vendored::file_system().clone(), + files: Files::default(), + rule_selection: RuleSelection::from_registry(default_lint_registry()).into(), + } + } +} + +#[salsa::db] +impl SourceDb for TestDb { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system + } + + fn files(&self) -> &Files { + &self.files + } + + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } +} + +impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } +} + +#[salsa::db] +impl SemanticDb for TestDb { + fn is_file_open(&self, file: File) -> bool { + !file.path(self).is_vendored_path() + } + + fn rule_selection(&self, _file: File) -> &RuleSelection { + &self.rule_selection + } + + fn lint_registry(&self) -> &LintRegistry { + default_lint_registry() + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +fn setup_db() -> TestDb { + let db = TestDb::new(); + + let src_root = SystemPathBuf::from("/src"); + db.memory_file_system() + .create_directory_all(&src_root) + .unwrap(); + + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored()) + .expect("Valid search path settings"), + }, + ); + + db +} + +static TEST_DB: OnceLock> = OnceLock::new(); + +fn do_fuzz(case: &[u8]) -> Corpus { + let Ok(code) = std::str::from_utf8(case) else { + return Corpus::Reject; + }; + + let parsed = parse_unchecked(code, ParseOptions::from(Mode::Module)); + if parsed.has_valid_syntax() { + return Corpus::Reject; + } + + let mut db = TEST_DB + .get_or_init(|| Mutex::new(setup_db())) + .lock() + .unwrap(); + + for path in &["/src/a.py", "/src/a.pyi"] { + db.write_file(path, code).unwrap(); + let file = system_path_to_file(&*db, path).unwrap(); + check_types(&*db, file); + db.memory_file_system().remove_file(path).unwrap(); + file.sync(&mut *db); + } + + Corpus::Keep +} + +fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) }); diff --git a/fuzz/init-fuzzer.sh b/fuzz/init-fuzzer.sh index 22f81f0378358..2419ae24662f9 100755 --- a/fuzz/init-fuzzer.sh +++ b/fuzz/init-fuzzer.sh @@ -6,24 +6,33 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "$SCRIPT_DIR" if ! cargo fuzz --help >&/dev/null; then + echo "Installing cargo-fuzz..." cargo install --git https://github.com/rust-fuzz/cargo-fuzz.git fi -if [ ! -d corpus/ruff_fix_validity ]; then - mkdir -p corpus/ruff_fix_validity +if [ ! -d corpus/common ]; then + mkdir -p corpus/common + + echo "Creating symlinks for fuzz targets to the common corpus directory..." + for target in fuzz_targets/*; do + corpus_dir="$(basename "$target" .rs)" + ln -vs "./common" "corpus/$corpus_dir" + done ( - cd corpus/ruff_fix_validity + cd corpus/common read -p "Would you like to build a corpus from a python source code dataset? (this will take a long time!) [Y/n] " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Downloading the Python source code dataset..." curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz fi # Build a smaller corpus in addition to the (optional) larger corpus + echo "Building a smaller corpus dataset..." curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.13.0.tar.gz' | tar xz - cp -r "../../../crates/red_knot_project/resources/test/corpus" "red_knot_project" + cp -r "../../../crates/ty_project/resources/test/corpus" "ty_project" cp -r "../../../crates/ruff_linter/resources/test/fixtures" "ruff_linter" cp -r "../../../crates/ruff_python_formatter/resources/test/fixtures" "ruff_python_formatter" cp -r "../../../crates/ruff_python_parser/resources" "ruff_python_parser" @@ -32,11 +41,12 @@ if [ ! -d corpus/ruff_fix_validity ]; then find . -type f -not -name "*.py" -delete ) + echo "Minifying the corpus dataset..." if [[ "$OSTYPE" == "darwin"* ]]; then - cargo +nightly fuzz cmin ruff_fix_validity -- -timeout=5 + cargo +nightly fuzz cmin ruff_fix_validity corpus/common -- -timeout=5 else - cargo fuzz cmin -s none ruff_fix_validity -- -timeout=5 + cargo fuzz cmin -s none ruff_fix_validity corpus/common -- -timeout=5 fi fi -echo "Done! You are ready to fuzz." +echo "Done! You are ready to fuzz" diff --git a/fuzz/reinit-fuzzer.sh b/fuzz/reinit-fuzzer.sh deleted file mode 100755 index f1e2a698fb192..0000000000000 --- a/fuzz/reinit-fuzzer.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# https://stackoverflow.com/a/246128/3549270 -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -cd "$SCRIPT_DIR" - -cd corpus/ruff_fix_validity -curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.12.0b2.tar.gz' | tar xz -cp -r "../../../crates/ruff_linter/resources/test" . -cd - -cargo fuzz cmin -s none ruff_fix_validity -- -timeout=5 - -echo "Done! You are ready to fuzz." diff --git a/knot.schema.json b/knot.schema.json deleted file mode 100644 index 5911ab01d3108..0000000000000 --- a/knot.schema.json +++ /dev/null @@ -1,826 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Options", - "description": "The options for the project.", - "type": "object", - "properties": { - "environment": { - "description": "Configures the type checking environment.", - "anyOf": [ - { - "$ref": "#/definitions/EnvironmentOptions" - }, - { - "type": "null" - } - ] - }, - "rules": { - "description": "Configures the enabled lints and their severity.", - "anyOf": [ - { - "$ref": "#/definitions/Rules" - }, - { - "type": "null" - } - ] - }, - "src": { - "anyOf": [ - { - "$ref": "#/definitions/SrcOptions" - }, - { - "type": "null" - } - ] - }, - "terminal": { - "anyOf": [ - { - "$ref": "#/definitions/TerminalOptions" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "DiagnosticFormat": { - "description": "The diagnostic output format.", - "oneOf": [ - { - "description": "The default full mode will print \"pretty\" diagnostics.\n\nThat is, color will be used when printing to a `tty`. Moreover, diagnostic messages may include additional context and annotations on the input to help understand the message.", - "type": "string", - "enum": [ - "full" - ] - }, - { - "description": "Print diagnostics in a concise mode.\n\nThis will guarantee that each diagnostic is printed on a single line. Only the most important or primary aspects of the diagnostic are included. Contextual information is dropped.\n\nThis may use color when printing to a `tty`.", - "type": "string", - "enum": [ - "concise" - ] - } - ] - }, - "EnvironmentOptions": { - "type": "object", - "properties": { - "extra-paths": { - "description": "List of user-provided paths that should take first priority in the module resolution. Examples in other type checkers are mypy's MYPYPATH environment variable, or pyright's stubPath configuration setting.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "python": { - "description": "Path to the Python installation from which Red Knot resolves type information and third-party dependencies.\n\nRed Knot will search in the path's `site-packages` directories for type information and third-party imports.\n\nThis option is commonly used to specify the path to a virtual environment.", - "type": [ - "string", - "null" - ] - }, - "python-platform": { - "description": "Specifies the target platform that will be used to analyze the source code. If specified, Red Knot will tailor its use of type stub files, which conditionalize type definitions based on the platform.\n\nIf no platform is specified, knot will use the current platform: - `win32` for Windows - `darwin` for macOS - `android` for Android - `ios` for iOS - `linux` for everything else", - "anyOf": [ - { - "$ref": "#/definitions/PythonPlatform" - }, - { - "type": "null" - } - ] - }, - "python-version": { - "description": "Specifies the version of Python that will be used to analyze the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. \"3.0\" or \"3.6\"). If a version is provided, knot will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.", - "anyOf": [ - { - "$ref": "#/definitions/PythonVersion" - }, - { - "type": "null" - } - ] - }, - "typeshed": { - "description": "Optional path to a \"typeshed\" directory on disk for us to use for standard-library types. If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, bundled as a zip file in the binary", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "Level": { - "oneOf": [ - { - "title": "Ignore", - "description": "The lint is disabled and should not run.", - "type": "string", - "enum": [ - "ignore" - ] - }, - { - "title": "Warn", - "description": "The lint is enabled and diagnostic should have a warning severity.", - "type": "string", - "enum": [ - "warn" - ] - }, - { - "title": "Error", - "description": "The lint is enabled and diagnostics have an error severity.", - "type": "string", - "enum": [ - "error" - ] - } - ] - }, - "PythonPlatform": { - "anyOf": [ - { - "type": "string" - }, - { - "description": "Do not make any assumptions about the target platform.", - "const": "all" - }, - { - "description": "Darwin", - "const": "darwin" - }, - { - "description": "Linux", - "const": "linux" - }, - { - "description": "Windows", - "const": "win32" - } - ] - }, - "PythonVersion": { - "anyOf": [ - { - "type": "string", - "pattern": "^\\d+\\.\\d+$" - }, - { - "description": "Python 3.7", - "const": "3.7" - }, - { - "description": "Python 3.8", - "const": "3.8" - }, - { - "description": "Python 3.9", - "const": "3.9" - }, - { - "description": "Python 3.10", - "const": "3.10" - }, - { - "description": "Python 3.11", - "const": "3.11" - }, - { - "description": "Python 3.12", - "const": "3.12" - }, - { - "description": "Python 3.13", - "const": "3.13" - } - ] - }, - "Rules": { - "type": "object", - "properties": { - "byte-string-type-annotation": { - "title": "detects byte strings in type annotation positions", - "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "call-non-callable": { - "title": "detects calls to non-callable objects", - "description": "## What it does\nChecks for calls to non-callable objects.\n\n## Why is this bad?\nCalling a non-callable object will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4() # TypeError: 'int' object is not callable\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "call-possibly-unbound-method": { - "title": "detects calls to possibly unbound methods", - "description": "## What it does\nChecks for calls to possibly unbound methods.\n\nTODO #14889", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "conflicting-argument-forms": { - "title": "detects when an argument is used as both a value and a type form in a call", - "description": "## What it does\nChecks whether an argument is used as both a value and a type form in a call", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "conflicting-declarations": { - "title": "detects conflicting declarations", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "conflicting-metaclass": { - "title": "detects conflicting metaclasses", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "cyclic-class-definition": { - "title": "detects cyclic class definitions", - "description": "## What it does\nChecks for class definitions with a cyclic inheritance chain.\n\n## Why is it bad?\nTODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "division-by-zero": { - "title": "detects division by zero", - "description": "## What it does\nIt detects division by zero.\n\n## Why is this bad?\nDividing by zero raises a `ZeroDivisionError` at runtime.\n\n## Examples\n```python\n5 / 0\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "duplicate-base": { - "title": "detects class definitions with duplicate bases", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "escape-character-in-forward-annotation": { - "title": "detects forward type annotations with escape characters", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "fstring-type-annotation": { - "title": "detects F-strings in type annotation positions", - "description": "## What it does\nChecks for f-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use f-string notation.\n\n## Examples\n```python\ndef test(): -> f\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "implicit-concatenated-string-type-annotation": { - "title": "detects implicit concatenated strings in type annotations", - "description": "## What it does\nChecks for implicit concatenated strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings.\n\n## Examples\n```python\ndef test(): -> \"Literal[\" \"5\" \"]\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"Literal[5]\":\n ...\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "incompatible-slots": { - "title": "detects class definitions whose MRO has conflicting `__slots__`", - "description": "## What it does\nChecks for classes whose bases define incompatible `__slots__`.\n\n## Why is this bad?\nInheriting from bases with incompatible `__slots__`s\nwill lead to a `TypeError` at runtime.\n\nClasses with no or empty `__slots__` are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\nMultiple inheritance from more than one different class\ndefining non-empty `__slots__` is not allowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\n## Known problems\nDynamic (not tuple or string literal) `__slots__` are not checked.\nAdditionally, classes inheriting from built-in classes with implicit layouts\nlike `str` or `int` are also not checked.\n\n```pycon\n>>> hasattr(int, \"__slots__\")\nFalse\n>>> hasattr(str, \"__slots__\")\nFalse\n>>> class A(int, str): ...\nTraceback (most recent call last):\n File \"\", line 1, in \n class A(int, str): ...\nTypeError: multiple bases have instance lay-out conflict\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "inconsistent-mro": { - "title": "detects class definitions with an inconsistent MRO", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "index-out-of-bounds": { - "title": "detects index out of bounds errors", - "description": "## What it does\nTODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-argument-type": { - "title": "detects call arguments whose type is not assignable to the corresponding typed parameter", - "description": "## What it does\nDetects call arguments whose type is not assignable to the corresponding typed parameter.\n\n## Why is this bad?\nPassing an argument of a type the function (or callable object) does not accept violates\nthe expectations of the function author and may cause unexpected runtime errors within the\nbody of the function.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc(\"foo\") # error: [invalid-argument-type]\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-assignment": { - "title": "detects invalid assignments", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-attribute-access": { - "title": "Invalid attribute access", - "description": "## What it does\nMakes sure that instance attribute accesses are valid.\n\n## Examples\n```python\nclass C:\n var: ClassVar[int] = 1\n\nC.var = 3 # okay\nC().var = 3 # error: Cannot assign to class variable\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-base": { - "title": "detects class definitions with an invalid base", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-context-manager": { - "title": "detects expressions used in with statements that don't implement the context manager protocol", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-declaration": { - "title": "detects invalid declarations", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-exception-caught": { - "title": "detects exception handlers that catch classes that do not inherit from `BaseException`", - "description": "## What it does\nChecks for exception handlers that catch non-exception classes.\n\n## Why is this bad?\nCatching classes that do not inherit from `BaseException` will raise a TypeError at runtime.\n\n## Example\n```python\ntry:\n 1 / 0\nexcept 1:\n ...\n```\n\nUse instead:\n```python\ntry:\n 1 / 0\nexcept ZeroDivisionError:\n ...\n```\n\n## References\n- [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)\n\n## Ruff rule\n This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-ignore-comment": { - "title": "detects ignore comments that use invalid syntax", - "description": "## What it does\nChecks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-metaclass": { - "title": "detects invalid `metaclass=` arguments", - "description": "## What it does\nChecks for arguments to `metaclass=` that are invalid.\n\n## Why is this bad?\nPython allows arbitrary expressions to be used as the argument to `metaclass=`.\nThese expressions, however, need to be callable and accept the same arguments\nas `type.__new__`.\n\n## Example\n\n```python\ndef f(): ...\n\n# TypeError: f() takes 0 positional arguments but 3 were given\nclass B(metaclass=f): ...\n```\n\n## References\n- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-parameter-default": { - "title": "detects default values that can't be assigned to the parameter's annotated type", - "description": "## What it does\nChecks for default values that can't be assigned to the parameter's annotated type.\n\n## Why is this bad?\nTODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-raise": { - "title": "detects `raise` statements that raise invalid exceptions or use invalid causes", - "description": "Checks for `raise` statements that raise non-exceptions or use invalid\ncauses for their raised exceptions.\n\n## Why is this bad?\nOnly subclasses or instances of `BaseException` can be raised.\nFor an exception's cause, the same rules apply, except that `None` is also\npermitted. Violating these rules results in a `TypeError` at runtime.\n\n## Examples\n```python\ndef f():\n try:\n something()\n except NameError:\n raise \"oops!\" from f\n\ndef g():\n raise NotImplemented from 42\n```\n\nUse instead:\n```python\ndef f():\n try:\n something()\n except NameError as e:\n raise RuntimeError(\"oops!\") from e\n\ndef g():\n raise NotImplementedError from None\n```\n\n## References\n- [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-return-type": { - "title": "detects returned values that can't be assigned to the function's annotated return type", - "description": "## What it does\nDetects returned values that can't be assigned to the function's annotated return type.\n\n## Why is this bad?\nReturning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function.\n\n## Examples\n```python\ndef func() -> int:\n return \"a\" # error: [invalid-return-type]\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-super-argument": { - "title": "detects invalid arguments for `super()`", - "description": "## What it does\nDetects `super()` calls where:\n- the first argument is not a valid class literal, or\n- the second argument is not an instance or subclass of the first argument.\n\n## Why is this bad?\n`super(type, obj)` expects:\n- the first argument to be a class,\n- and the second argument to satisfy one of the following:\n - `isinstance(obj, type)` is `True`\n - `issubclass(obj, type)` is `True`\n\nViolating this relationship will raise a `TypeError` at runtime.\n\n## Examples\n```python\nclass A:\n ...\nclass B(A):\n ...\n\nsuper(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)`\n\nsuper(A(), B()) # error: `A()` is not a class\n\nsuper(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)`\nsuper(B, A) # error: `A` does not satisfy `issubclass(A, B)`\n```\n\n## References\n- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-syntax-in-forward-annotation": { - "title": "detects invalid syntax in forward annotations", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-type-checking-constant": { - "title": "detects invalid TYPE_CHECKING constant assignments", - "description": "## What it does\nChecks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an\nannotation not assignable from `bool`.\n\n## Why is this bad?\nThe name `TYPE_CHECKING` is reserved for a flag that can be used to provide conditional\ncode seen only by the type checker, and not at runtime. Normally this flag is imported from\n`typing` or `typing_extensions`, but it can also be defined locally. If defined locally, it\nmust be assigned the value `False` at runtime; the type checker will consider its value to\nbe `True`. If annotated, it must be annotated as a type that can accept `bool` values.", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-type-form": { - "title": "detects invalid type forms", - "description": "## What it does\nChecks for invalid type expressions.\n\n## Why is this bad?\nTODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "invalid-type-variable-constraints": { - "title": "detects invalid type variable constraints", - "description": "TODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "missing-argument": { - "title": "detects missing required arguments in a call", - "description": "## What it does\nChecks for missing required arguments in a call.\n\n## Why is this bad?\nFailing to provide a required argument will raise a `TypeError` at runtime.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc() # TypeError: func() missing 1 required positional argument: 'x'\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "no-matching-overload": { - "title": "detects calls that do not match any overload", - "description": "## What it does\nChecks for calls to an overloaded function that do not match any of the overloads.\n\n## Why is this bad?\nFailing to provide the correct arguments to one of the overloads will raise a `TypeError`\nat runtime.\n\n## Examples\n```python\n@overload\ndef func(x: int): ...\n@overload\ndef func(x: bool): ...\nfunc(\"string\") # error: [no-matching-overload]\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "non-subscriptable": { - "title": "detects subscripting objects that do not support subscripting", - "description": "## What it does\nChecks for subscripting objects that do not support subscripting.\n\n## Why is this bad?\nSubscripting an object that does not support it will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4[1] # TypeError: 'int' object is not subscriptable\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "not-iterable": { - "title": "detects iteration over an object that is not iterable", - "description": "## What it does\nChecks for objects that are not iterable but are used in a context that requires them to be.\n\n## Why is this bad?\nIterating over an object that is not iterable will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\nfor i in 34: # TypeError: 'int' object is not iterable\n pass\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "parameter-already-assigned": { - "title": "detects multiple arguments for the same parameter", - "description": "## What it does\nChecks for calls which provide more than one argument for a single parameter.\n\n## Why is this bad?\nProviding multiple values for a single parameter will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\ndef f(x: int) -> int: ...\n\nf(1, x=2) # Error raised here\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "possibly-unbound-attribute": { - "title": "detects references to possibly unbound attributes", - "description": "## What it does\nChecks for possibly unbound attributes.\n\nTODO #14889", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "possibly-unbound-import": { - "title": "detects possibly unbound imports", - "description": "TODO #14889", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "possibly-unresolved-reference": { - "title": "detects references to possibly undefined names", - "description": "## What it does\nChecks for references to names that are possibly not defined.\n\n## Why is this bad?\nUsing an undefined variable will raise a `NameError` at runtime.\n\n## Example\n\n```python\nfor i in range(0):\n x = i\n\nprint(x) # NameError: name 'x' is not defined\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "raw-string-type-annotation": { - "title": "detects raw strings in type annotation positions", - "description": "## What it does\nChecks for raw-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use raw-string notation.\n\n## Examples\n```python\ndef test(): -> r\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "redundant-cast": { - "title": "detects redundant `cast` calls", - "description": "## What it does\nDetects redundant `cast` calls where the value already has the target type.\n\n## Why is this bad?\nThese casts have no effect and can be removed.\n\n## Example\n```python\ndef f() -> int:\n return 10\n\ncast(int, f()) # Redundant\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "static-assert-error": { - "title": "Failed static assertion", - "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Examples\n```python\nfrom knot_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "subclass-of-final-class": { - "title": "detects subclasses of final classes", - "description": "## What it does\nChecks for classes that subclass final classes.\n\n## Why is this bad?\nDecorating a class with `@final` declares to the type checker that it should not be subclassed.\n\n## Example\n\n```python\nfrom typing import final\n\n@final\nclass A: ...\nclass B(A): ... # Error raised here\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "too-many-positional-arguments": { - "title": "detects calls passing too many positional arguments", - "description": "## What it does\nChecks for calls that pass more positional arguments than the callable can accept.\n\n## Why is this bad?\nPassing too many positional arguments will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(): ...\n\nf(\"foo\") # Error raised here\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "type-assertion-failure": { - "title": "detects failed type assertions", - "description": "## What it does\nChecks for `assert_type()` and `assert_never()` calls where the actual type\nis not the same as the asserted type.\n\n## Why is this bad?\n`assert_type()` allows confirming the inferred type of a certain value.\n\n## Example\n\n```python\ndef _(x: int):\n assert_type(x, int) # fine\n assert_type(x, str) # error: Actual type does not match asserted type\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unavailable-implicit-super-arguments": { - "title": "detects invalid `super()` calls where implicit arguments are unavailable.", - "description": "## What it does\nDetects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable.\n\n## Why is this bad?\nWhen `super()` is used without arguments, Python tries to find two things:\nthe nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls).\nIf either of these is missing, the call will fail at runtime with a `RuntimeError`.\n\n## Examples\n```python\nsuper() # error: no enclosing class or function found\n\ndef func():\n super() # error: no enclosing class or first argument exists\n\nclass A:\n f = super() # error: no enclosing function to provide the first argument\n\n def method(self):\n def nested():\n super() # error: first argument does not exist in this nested function\n\n lambda: super() # error: first argument does not exist in this lambda\n\n (super() for _ in range(10)) # error: argument is not available in generator expression\n\n super() # okay! both enclosing class and first argument are available\n```\n\n## References\n- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "undefined-reveal": { - "title": "detects usages of `reveal_type` without importing it", - "description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\nTODO #14889", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unknown-argument": { - "title": "detects unknown keyword arguments in calls", - "description": "## What it does\nChecks for keyword arguments in calls that don't match any parameter of the callable.\n\n## Why is this bad?\nProviding an unknown keyword argument will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(x: int) -> int: ...\n\nf(x=1, y=2) # Error raised here\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unknown-rule": { - "title": "detects `knot: ignore` comments that reference unknown rules", - "description": "## What it does\nChecks for `knot: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `knot: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # knot: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # knot: ignore[division-by-zero]\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unresolved-attribute": { - "title": "detects references to unresolved attributes", - "description": "## What it does\nChecks for unresolved attributes.\n\nTODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unresolved-import": { - "title": "detects unresolved imports", - "description": "## What it does\nChecks for import statements for which the module cannot be resolved.\n\n## Why is this bad?\nImporting a module that cannot be resolved will raise an `ImportError` at runtime.", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unresolved-reference": { - "title": "detects references to names that are not defined", - "description": "## What it does\nChecks for references to names that are not defined.\n\n## Why is this bad?\nUsing an undefined variable will raise a `NameError` at runtime.\n\n## Example\n\n```python\nprint(x) # NameError: name 'x' is not defined\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unsupported-bool-conversion": { - "title": "detects boolean conversion where the object incorrectly implements `__bool__`", - "description": "## What it does\nChecks for bool conversions where the object doesn't correctly implement `__bool__`.\n\n## Why is this bad?\nIf an exception is raised when you attempt to evaluate the truthiness of an object,\nusing the object in a boolean context will fail at runtime.\n\n## Examples\n\n```python\nclass NotBoolable:\n __bool__ = None\n\nb1 = NotBoolable()\nb2 = NotBoolable()\n\nif b1: # exception raised here\n pass\n\nb1 and b2 # exception raised here\nnot b1 # exception raised here\nb1 < b2 < b1 # exception raised here\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unsupported-operator": { - "title": "detects binary, unary, or comparison expressions where the operands don't support the operator", - "description": "## What it does\nChecks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.\n\nTODO #14889", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "unused-ignore-comment": { - "title": "detects unused `type: ignore` comments", - "description": "## What it does\nChecks for `type: ignore` or `knot: ignore` directives that are no longer applicable.\n\n## Why is this bad?\nA `type: ignore` directive that no longer matches any diagnostic violations is likely\nincluded by mistake, and should be removed to avoid confusion.\n\n## Examples\n```py\na = 20 / 2 # knot: ignore[division-by-zero]\n```\n\nUse instead:\n\n```py\na = 20 / 2\n```", - "default": "warn", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - }, - "zero-stepsize-in-slice": { - "title": "detects a slice step size of zero", - "description": "## What it does\nChecks for step size 0 in slices.\n\n## Why is this bad?\nA slice with a step size of zero will raise a `ValueError` at runtime.\n\n## Examples\n```python\nl = list(range(10))\nl[1:10:0] # ValueError: slice step cannot be zero\n```", - "default": "error", - "oneOf": [ - { - "$ref": "#/definitions/Level" - } - ] - } - }, - "additionalProperties": { - "$ref": "#/definitions/Level" - } - }, - "SrcOptions": { - "type": "object", - "properties": { - "root": { - "description": "The root of the project, used for finding first-party modules.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "TerminalOptions": { - "type": "object", - "properties": { - "error-on-warning": { - "description": "Use exit code 1 if there are any warning-level diagnostics.\n\nDefaults to `false`.", - "type": [ - "boolean", - "null" - ] - }, - "output-format": { - "description": "The format to use for printing diagnostic messages.\n\nDefaults to `full`.", - "anyOf": [ - { - "$ref": "#/definitions/DiagnosticFormat" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - } -} \ No newline at end of file diff --git a/mkdocs.template.yml b/mkdocs.template.yml index 8c42b369c5433..41f9bbddcc3c8 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -62,6 +62,7 @@ markdown_extensions: alternate_style: true plugins: - search + - gh-admonitions extra_css: - stylesheets/extra.css extra_javascript: diff --git a/playground/.prettierignore b/playground/.prettierignore index ac9e43b7a3b32..8c7763fecfb74 100644 --- a/playground/.prettierignore +++ b/playground/.prettierignore @@ -1,5 +1,5 @@ **.md ruff/dist ruff/ruff_wasm -knot/dist -knot/red_knot_wasm \ No newline at end of file +ty/dist +ty/ty_wasm \ No newline at end of file diff --git a/playground/README.md b/playground/README.md index efe28b75577bf..16c4269a9b62a 100644 --- a/playground/README.md +++ b/playground/README.md @@ -4,9 +4,9 @@ In-browser playground for Ruff. Available [https://play.ruff.rs/](https://play.r ## Getting started -Install the NPM dependencies with `npm install`, and run, and run the development server with -`npm start --workspace ruff-playground` or `npm start --workspace knot-playground`. -You may need to restart the server after making changes to Ruff or Red Knot to re-build the WASM +Install the NPM dependencies with `npm install`, and run the development server with +`npm start --workspace ruff-playground` or `npm start --workspace ty-playground`. +You may need to restart the server after making changes to Ruff or ty to re-build the WASM module. To run the datastore, which is based @@ -37,4 +37,4 @@ additional inspiration from the [Biome Playground](https://biomejs.dev/playgroun ### Stack overflows If you see stack overflows in the playground, build the WASM module in release mode: -`npm run --workspace knot-playground build:wasm`. +`npm run --workspace ty-playground build:wasm`. diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index 5d4a5076ca18c..1ee09270fb767 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20230801.0", - "miniflare": "^3.20230801.1", + "miniflare": "^4.0.0", "typescript": "^5.1.6", "wrangler": "^4.1.0" } @@ -33,14 +33,14 @@ } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", - "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.3.tgz", + "integrity": "sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { - "unenv": "2.0.0-rc.14", - "workerd": "^1.20250124.0" + "unenv": "2.0.0-rc.17", + "workerd": "^1.20250508.0" }, "peerDependenciesMeta": { "workerd": { @@ -49,9 +49,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250214.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250214.0.tgz", - "integrity": "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q==", + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250617.0.tgz", + "integrity": "sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==", "cpu": [ "x64" ], @@ -66,9 +66,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250214.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250214.0.tgz", - "integrity": "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw==", + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250617.0.tgz", + "integrity": "sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==", "cpu": [ "arm64" ], @@ -83,9 +83,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250214.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250214.0.tgz", - "integrity": "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ==", + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250617.0.tgz", + "integrity": "sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==", "cpu": [ "x64" ], @@ -100,9 +100,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250214.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250214.0.tgz", - "integrity": "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA==", + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250617.0.tgz", + "integrity": "sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==", "cpu": [ "arm64" ], @@ -117,9 +117,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250214.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250214.0.tgz", - "integrity": "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ==", + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250617.0.tgz", + "integrity": "sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==", "cpu": [ "x64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20250317.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250317.0.tgz", - "integrity": "sha512-ud1x5D1ksDIdx35jsx6wG9G8a/SLeg7kfGv/c732umLYn7I+DZ7TdKTvM1LaWTLzp+yYGyXZOrInJywfUU8bVw==", + "version": "4.20250705.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250705.0.tgz", + "integrity": "sha512-+eKIUEW0loB+CCgkfpa6UCTacnn3YOCllLpLbKfwR0qdoIfzmeOpadqY/W6ZSi/9v0tpQqTPB80tkhu7a7Cyug==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -164,9 +164,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], @@ -181,9 +181,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], @@ -215,9 +215,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], @@ -232,9 +232,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], @@ -249,9 +249,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], @@ -266,9 +266,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], @@ -283,9 +283,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], @@ -300,9 +300,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], @@ -317,9 +317,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], @@ -334,9 +334,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], @@ -351,9 +351,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], @@ -368,9 +368,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], @@ -385,9 +385,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], @@ -402,9 +402,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], @@ -419,9 +419,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], @@ -436,9 +436,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], @@ -453,9 +453,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ "arm64" ], @@ -470,9 +470,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], @@ -487,9 +487,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "cpu": [ "arm64" ], @@ -504,9 +504,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], @@ -521,9 +521,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], @@ -538,9 +538,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], @@ -555,9 +555,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], @@ -572,9 +572,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], @@ -1106,7 +1106,6 @@ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -1121,7 +1120,6 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1134,8 +1132,7 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", @@ -1143,17 +1140,17 @@ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1190,15 +1187,14 @@ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1209,31 +1205,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/execa": { @@ -1331,8 +1327,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/is-stream": { "version": "3.0.0", @@ -1388,9 +1383,9 @@ } }, "node_modules/miniflare": { - "version": "3.20250214.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250214.1.tgz", - "integrity": "sha512-NE66QV+2n9ZndaP5jgPlcVref3Arvizb+l2QqhgeXtKM5Orhi8UU2mijoiN3mHEUexKaBES2S1VubT4LDPqkxQ==", + "version": "4.20250617.5", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250617.5.tgz", + "integrity": "sha512-Qqn30jR6dCjXaKVizT6vH4KOb+GyLccoxLNOJEfu63yBPn8eoXa7PrdiSGTmjs2RY8/tr7eTO8Wu/Yr14k0xVA==", "dev": true, "license": "MIT", "dependencies": { @@ -1399,18 +1394,19 @@ "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^5.28.5", - "workerd": "1.20250214.0", + "workerd": "1.20250617.0", "ws": "8.18.0", - "youch": "3.2.3", + "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.0.0" } }, "node_modules/mustache": { @@ -1542,7 +1538,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", @@ -1606,7 +1601,6 @@ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } @@ -1660,9 +1654,9 @@ "optional": true }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1674,9 +1668,9 @@ } }, "node_modules/ufo": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", - "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "dev": true, "license": "MIT" }, @@ -1694,23 +1688,23 @@ } }, "node_modules/unenv": { - "version": "2.0.0-rc.14", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", - "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "version": "2.0.0-rc.17", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.17.tgz", + "integrity": "sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==", "dev": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", - "exsolve": "^1.0.1", - "ohash": "^2.0.10", + "exsolve": "^1.0.4", + "ohash": "^2.0.11", "pathe": "^2.0.3", - "ufo": "^1.5.4" + "ufo": "^1.6.1" } }, "node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -1746,9 +1740,9 @@ } }, "node_modules/workerd": { - "version": "1.20250214.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250214.0.tgz", - "integrity": "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g==", + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250617.0.tgz", + "integrity": "sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1759,28 +1753,28 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250214.0", - "@cloudflare/workerd-darwin-arm64": "1.20250214.0", - "@cloudflare/workerd-linux-64": "1.20250214.0", - "@cloudflare/workerd-linux-arm64": "1.20250214.0", - "@cloudflare/workerd-windows-64": "1.20250214.0" + "@cloudflare/workerd-darwin-64": "1.20250617.0", + "@cloudflare/workerd-darwin-arm64": "1.20250617.0", + "@cloudflare/workerd-linux-64": "1.20250617.0", + "@cloudflare/workerd-linux-arm64": "1.20250617.0", + "@cloudflare/workerd-windows-64": "1.20250617.0" } }, "node_modules/wrangler": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.1.0.tgz", - "integrity": "sha512-HcQZ2YappySGipEDEdbjMq01g3v+mv+xZYZSzwPTmRsoTfnbL5yteObshcK1JX9jdx7Qw23Ywd/4BPa1JyKIUQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.23.0.tgz", + "integrity": "sha512-JSeDt3IwA4TEmg/V3tRblImPjdxynBt9PUVO/acQJ83XGlMMSwswDKL1FuwvbFzgX6+JXc3GMHeu7r8AQIxw9w==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.0.2", + "@cloudflare/unenv-preset": "2.3.3", "blake3-wasm": "2.1.5", - "esbuild": "0.24.2", - "miniflare": "4.20250317.0", + "esbuild": "0.25.4", + "miniflare": "4.20250617.5", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.14", - "workerd": "1.20250317.0" + "unenv": "2.0.0-rc.17", + "workerd": "1.20250617.0" }, "bin": { "wrangler": "bin/wrangler.js", @@ -1790,11 +1784,10 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "fsevents": "~2.3.2", - "sharp": "^0.33.5" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250317.0" + "@cloudflare/workers-types": "^4.20250617.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -1802,138 +1795,6 @@ } } }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250317.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250317.0.tgz", - "integrity": "sha512-ZnpF+MP/azHJ7sUOW9Ut/5pqeijsEOSmRUpONDXImv/DiHgtCd2BA/He11srp8nG2XeWav3jk+Ob84NKrrXXHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250317.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250317.0.tgz", - "integrity": "sha512-ypn2/SIK7LAouYx5oB0NNhzb3h+ZdXtDh94VCcsNV81xAVdDXKp6xvTnqY8CWjGfuKWJocbRZVZvU+Lquhuujg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250317.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250317.0.tgz", - "integrity": "sha512-KfAHN9VHF2NxGjDjj7udLAatZ72GIg4xmN9r2AZ6N1/hsGDlbn+NbVkSJtWjpXBcCoWYxQqtAdpHyO4eb7nIvQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250317.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250317.0.tgz", - "integrity": "sha512-o7a3poQ4vzw553xGudUWm8yGsfdRWSGxqDEdYyuzT5k3z4qjsYMGsZgW9Yw8x3f1SSpPgYpdLlc8IKg9n7eukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250317.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250317.0.tgz", - "integrity": "sha512-tfDSioKY5OKP0nZ7Mkc6bLcwY2fIrROwoq2WjekQ62x91KRbKCJwjkOSvyFJYbshDATK90GutYoblqV80e34jw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/miniflare": { - "version": "4.20250317.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250317.0.tgz", - "integrity": "sha512-fCyFTa3G41Vyo24QUZD5xgdm+6RMKT6VC3vk9Usmr+Pwf/15HcH1AVLPVgzmJaJosWVb8r4S0HQ9a/+bmmZx0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "8.14.0", - "acorn-walk": "8.3.2", - "exit-hook": "2.2.1", - "glob-to-regexp": "0.4.1", - "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250317.0", - "ws": "8.18.0", - "youch": "3.2.3", - "zod": "3.22.3" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/wrangler/node_modules/workerd": { - "version": "1.20250317.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250317.0.tgz", - "integrity": "sha512-m+aqA4RS/jsIaml0KuTi96UBlkx1vC0mcLClGKPFNPiMStK75hVQxUhupXEMI4knHtb/vgNQyPFMKAJtxW5c6w==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250317.0", - "@cloudflare/workerd-darwin-arm64": "1.20250317.0", - "@cloudflare/workerd-linux-64": "1.20250317.0", - "@cloudflare/workerd-linux-arm64": "1.20250317.0", - "@cloudflare/workerd-windows-64": "1.20250317.0" - } - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -1957,12 +1818,13 @@ } }, "node_modules/youch": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.2.3.tgz", - "integrity": "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", "dev": true, + "license": "MIT", "dependencies": { - "cookie": "^0.5.0", + "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } diff --git a/playground/api/package.json b/playground/api/package.json index 87d9ab8709743..79e9d41c7dcdd 100644 --- a/playground/api/package.json +++ b/playground/api/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "devDependencies": { "@cloudflare/workers-types": "^4.20230801.0", - "miniflare": "^3.20230801.1", + "miniflare": "^4.0.0", "typescript": "^5.1.6", "wrangler": "^4.1.0" }, diff --git a/playground/api/src/index.ts b/playground/api/src/index.ts index d75c8135f2739..9ac1ff5a97d6d 100644 --- a/playground/api/src/index.ts +++ b/playground/api/src/index.ts @@ -31,7 +31,11 @@ const PRODUCTION_HEADERS = { "Access-Control-Allow-Origin": "https://play.ruff.rs", }; -const ALLOWED_DOMAINS = ["https://playknot.ruff.rs", "https://types.ruff.rs"]; +const ALLOWED_DOMAINS = new Set([ + "https://playknot.ruff.rs", + "https://types.ruff.rs", + "https://play.ty.dev", +]); export default { async fetch( @@ -45,7 +49,7 @@ export default { if (!DEV) { const origin = request.headers.get("origin"); - if (origin && ALLOWED_DOMAINS.includes(origin)) { + if (origin && ALLOWED_DOMAINS.has(origin)) { headers["Access-Control-Allow-Origin"] = origin; } } diff --git a/playground/knot/index.html b/playground/knot/index.html deleted file mode 100644 index 368ba81eb4935..0000000000000 --- a/playground/knot/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - Playground | Red Knot - - - - - - - - - - - - - -
- - - diff --git a/playground/knot/package.json b/playground/knot/package.json deleted file mode 100644 index 1e0001187d378..0000000000000 --- a/playground/knot/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "knot-playground", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "prebuild": "npm run build:wasm", - "build": "vite build", - "build:wasm": "wasm-pack build ../../crates/red_knot_wasm --target web --out-dir ../../playground/knot/red_knot_wasm", - "dev:wasm": "wasm-pack build ../../crates/red_knot_wasm --dev --target web --out-dir ../../playground/knot/red_knot_wasm", - "predev:build": "npm run dev:wasm", - "dev:build": "vite build", - "prestart": "npm run dev:wasm", - "start": "vite", - "preview": "vite preview" - }, - "dependencies": { - "@monaco-editor/react": "^4.7.0", - "classnames": "^2.5.1", - "lz-string": "^1.5.0", - "monaco-editor": "^0.52.2", - "pyodide": "^0.27.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.7", - "red_knot_wasm": "file:red_knot_wasm", - "shared": "0.0.0", - "smol-toml": "^1.3.1" - }, - "overrides": { - "@monaco-editor/react": { - "react": "$react", - "react-dom": "$react-dom" - } - }, - "devDependencies": { - "vite-plugin-static-copy": "^2.3.0" - } -} diff --git a/playground/knot/public/favicon-16x16.png b/playground/knot/public/favicon-16x16.png deleted file mode 100644 index 00eee0db92344..0000000000000 Binary files a/playground/knot/public/favicon-16x16.png and /dev/null differ diff --git a/playground/knot/public/favicon-32x32.png b/playground/knot/public/favicon-32x32.png deleted file mode 100644 index bc5e2fd1bc201..0000000000000 Binary files a/playground/knot/public/favicon-32x32.png and /dev/null differ diff --git a/playground/knot/public/favicon.ico b/playground/knot/public/favicon.ico deleted file mode 100644 index 8103f77074683..0000000000000 Binary files a/playground/knot/public/favicon.ico and /dev/null differ diff --git a/playground/knot/src/Editor/Editor.tsx b/playground/knot/src/Editor/Editor.tsx deleted file mode 100644 index 73d8f7527b211..0000000000000 --- a/playground/knot/src/Editor/Editor.tsx +++ /dev/null @@ -1,467 +0,0 @@ -/** - * Editor for the Python source code. - */ - -import Moncao, { Monaco, OnMount } from "@monaco-editor/react"; -import { - CancellationToken, - editor, - IDisposable, - IPosition, - IRange, - languages, - MarkerSeverity, - Position, - Range, - Uri, -} from "monaco-editor"; -import { useCallback, useEffect, useRef } from "react"; -import { Theme } from "shared"; -import { - Range as KnotRange, - Severity, - type Workspace, - Position as KnotPosition, -} from "red_knot_wasm"; - -import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; -import { FileId, ReadonlyFiles } from "../Playground"; -import { isPythonFile } from "./Files"; -import { Diagnostic } from "./Diagnostics"; - -type Props = { - visible: boolean; - fileName: string; - selected: FileId; - files: ReadonlyFiles; - diagnostics: Diagnostic[]; - theme: Theme; - workspace: Workspace; - onChange(content: string): void; - onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void; - onOpenFile(file: FileId): void; -}; - -export default function Editor({ - visible, - fileName, - selected, - files, - theme, - diagnostics, - workspace, - onChange, - onMount, - onOpenFile, -}: Props) { - const serverRef = useRef(null); - - if (serverRef.current != null) { - serverRef.current.update({ - files, - workspace, - onOpenFile, - }); - } - - // Update the diagnostics in the editor. - useEffect(() => { - const server = serverRef.current; - - if (server == null) { - return; - } - - server.updateDiagnostics(diagnostics); - }, [diagnostics]); - - const handleChange = useCallback( - (value: string | undefined) => { - onChange(value ?? ""); - }, - [onChange], - ); - - useEffect(() => { - return () => { - const server = serverRef.current; - - if (server != null) { - server.dispose(); - } - }; - }, []); - - const handleMount: OnMount = useCallback( - (editor, instance) => { - serverRef.current?.dispose(); - - const server = new PlaygroundServer(instance, { - workspace, - files, - onOpenFile, - }); - - server.updateDiagnostics(diagnostics); - serverRef.current = server; - - onMount(editor, instance); - }, - - [files, onOpenFile, workspace, onMount, diagnostics], - ); - - return ( - - ); -} - -interface PlaygroundServerProps { - workspace: Workspace; - files: ReadonlyFiles; - onOpenFile: (file: FileId) => void; -} - -class PlaygroundServer - implements - languages.TypeDefinitionProvider, - editor.ICodeEditorOpener, - languages.HoverProvider, - languages.InlayHintsProvider, - languages.DocumentFormattingEditProvider -{ - private typeDefinitionProviderDisposable: IDisposable; - private editorOpenerDisposable: IDisposable; - private hoverDisposable: IDisposable; - private inlayHintsDisposable: IDisposable; - private formatDisposable: IDisposable; - - constructor( - private monaco: Monaco, - private props: PlaygroundServerProps, - ) { - this.typeDefinitionProviderDisposable = - monaco.languages.registerTypeDefinitionProvider("python", this); - this.hoverDisposable = monaco.languages.registerHoverProvider( - "python", - this, - ); - this.inlayHintsDisposable = monaco.languages.registerInlayHintsProvider( - "python", - this, - ); - this.editorOpenerDisposable = monaco.editor.registerEditorOpener(this); - this.formatDisposable = - monaco.languages.registerDocumentFormattingEditProvider("python", this); - } - - provideInlayHints( - _model: editor.ITextModel, - range: Range, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _token: CancellationToken, - ): languages.ProviderResult { - const workspace = this.props.workspace; - const selectedFile = this.props.files.selected; - - if (selectedFile == null) { - return; - } - - const selectedHandle = this.props.files.handles[selectedFile]; - - if (selectedHandle == null) { - return; - } - - const inlayHints = workspace.inlayHints( - selectedHandle, - iRangeToKnotRange(range), - ); - - if (inlayHints.length === 0) { - return undefined; - } - - return { - dispose: () => {}, - hints: inlayHints.map((hint) => ({ - label: hint.markdown, - position: { - lineNumber: hint.position.line, - column: hint.position.column, - }, - })), - }; - } - - resolveInlayHint( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _hint: languages.InlayHint, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _token: CancellationToken, - ): languages.ProviderResult { - return undefined; - } - - update(props: PlaygroundServerProps) { - this.props = props; - } - - updateDiagnostics(diagnostics: Array) { - if (this.props.files.selected == null) { - return; - } - - const handle = this.props.files.handles[this.props.files.selected]; - - if (handle == null) { - return; - } - - const editor = this.monaco.editor; - const model = editor.getModel(Uri.parse(handle.path())); - - if (model == null) { - return; - } - - editor.setModelMarkers( - model, - "owner", - diagnostics.map((diagnostic) => { - const mapSeverity = (severity: Severity) => { - switch (severity) { - case Severity.Info: - return MarkerSeverity.Info; - case Severity.Warning: - return MarkerSeverity.Warning; - case Severity.Error: - return MarkerSeverity.Error; - case Severity.Fatal: - return MarkerSeverity.Error; - } - }; - - const range = diagnostic.range; - - return { - code: diagnostic.id, - startLineNumber: range?.start?.line ?? 0, - startColumn: range?.start?.column ?? 0, - endLineNumber: range?.end?.line ?? 0, - endColumn: range?.end?.column ?? 0, - message: diagnostic.message, - severity: mapSeverity(diagnostic.severity), - tags: [], - }; - }), - ); - } - - provideHover( - model: editor.ITextModel, - position: Position, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _token: CancellationToken, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - context?: languages.HoverContext | undefined, - ): languages.ProviderResult { - const workspace = this.props.workspace; - - const selectedFile = this.props.files.selected; - if (selectedFile == null) { - return; - } - - const selectedHandle = this.props.files.handles[selectedFile]; - - if (selectedHandle == null) { - return; - } - - const hover = workspace.hover( - selectedHandle, - new KnotPosition(position.lineNumber, position.column), - ); - - if (hover == null) { - return; - } - - return { - range: knotRangeToIRange(hover.range), - contents: [{ value: hover.markdown, isTrusted: true }], - }; - } - - provideTypeDefinition( - model: editor.ITextModel, - position: Position, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _: CancellationToken, - ): languages.ProviderResult { - const workspace = this.props.workspace; - - const selectedFile = this.props.files.selected; - if (selectedFile == null) { - return; - } - - const selectedHandle = this.props.files.handles[selectedFile]; - - if (selectedHandle == null) { - return; - } - - const links = workspace.gotoTypeDefinition( - selectedHandle, - new KnotPosition(position.lineNumber, position.column), - ); - - return ( - links - .map((link) => { - const targetSelection = - link.selection_range == null - ? undefined - : knotRangeToIRange(link.selection_range); - - const originSelection = - link.origin_selection_range == null - ? undefined - : knotRangeToIRange(link.origin_selection_range); - - return { - uri: Uri.parse(link.path), - range: knotRangeToIRange(link.full_range), - targetSelectionRange: targetSelection, - originSelectionRange: originSelection, - } as languages.LocationLink; - }) - // Filter out vendored files because they aren't open in the editor. - .filter((link) => link.uri.scheme !== "vendored") - ); - } - - openCodeEditor( - source: editor.ICodeEditor, - resource: Uri, - selectionOrPosition?: IRange | IPosition, - ): boolean { - const files = this.props.files; - - const fileId = files.index.find((file) => { - return Uri.file(file.name).toString() === resource.toString(); - })?.id; - - if (fileId == null) { - return false; - } - - const handle = files.handles[fileId]; - - let model = this.monaco.editor.getModel(resource); - if (model == null) { - const language = - handle != null && isPythonFile(handle) ? "python" : undefined; - model = this.monaco.editor.createModel( - files.contents[fileId], - language, - resource, - ); - } - - // it's a bit hacky to create the model manually - // but only using `onOpenFile` isn't enough - // because the model doesn't get updated until the next render. - if (files.selected !== fileId) { - source.setModel(model); - - this.props.onOpenFile(fileId); - } - - if (selectionOrPosition != null) { - if (Position.isIPosition(selectionOrPosition)) { - source.setPosition(selectionOrPosition); - source.revealPosition(selectionOrPosition); - } else { - source.setSelection(selectionOrPosition); - source.revealPosition({ - lineNumber: selectionOrPosition.startLineNumber, - column: selectionOrPosition.startColumn, - }); - } - } - - return true; - } - - provideDocumentFormattingEdits( - model: editor.ITextModel, - ): languages.ProviderResult { - if (this.props.files.selected == null) { - return null; - } - - const fileHandle = this.props.files.handles[this.props.files.selected]; - - if (fileHandle == null) { - return null; - } - - const formatted = this.props.workspace.format(fileHandle); - if (formatted != null) { - return [ - { - range: model.getFullModelRange(), - text: formatted, - }, - ]; - } - - return null; - } - - dispose() { - this.hoverDisposable.dispose(); - this.editorOpenerDisposable.dispose(); - this.typeDefinitionProviderDisposable.dispose(); - this.inlayHintsDisposable.dispose(); - this.formatDisposable.dispose(); - } -} - -function knotRangeToIRange(range: KnotRange): IRange { - return { - startLineNumber: range.start.line, - startColumn: range.start.column, - endLineNumber: range.end.line, - endColumn: range.end.column, - }; -} - -function iRangeToKnotRange(range: IRange): KnotRange { - return new KnotRange( - new KnotPosition(range.startLineNumber, range.startColumn), - new KnotPosition(range.endLineNumber, range.endColumn), - ); -} diff --git a/playground/package-lock.json b/playground/package-lock.json index c3a4847fff532..e729fbbc1f71f 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -8,7 +8,7 @@ "name": "playground", "version": "0.0.0", "workspaces": [ - "knot", + "ty", "ruff", "shared" ], @@ -26,34 +26,24 @@ "tailwindcss": "^4.0.14", "typescript": "^5.8.2", "typescript-eslint": "^8.26.1", - "vite": "^6.2.2", + "vite": "^7.0.0", "wasm-pack": "^0.13.1" } }, - "knot": { - "name": "knot-playground", - "version": "0.0.0", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@monaco-editor/react": "^4.7.0", - "classnames": "^2.5.1", - "lz-string": "^1.5.0", - "monaco-editor": "^0.52.2", - "pyodide": "^0.27.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.7", - "red_knot_wasm": "file:red_knot_wasm", - "shared": "0.0.0", - "smol-toml": "^1.3.1" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, - "devDependencies": { - "vite-plugin-static-copy": "^2.3.0" + "engines": { + "node": ">=6.0.0" } }, - "knot/red_knot_wasm": { - "version": "0.0.0", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -480,9 +470,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -522,9 +512,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -537,9 +527,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", - "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -547,9 +537,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -560,9 +550,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -584,13 +574,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", - "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -604,13 +597,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -683,6 +676,82 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@monaco-editor/loader": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", @@ -744,10 +813,17 @@ "node": ">= 8" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", + "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz", - "integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", + "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", "cpu": [ "arm" ], @@ -759,9 +835,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.36.0.tgz", - "integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", + "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", "cpu": [ "arm64" ], @@ -773,9 +849,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.36.0.tgz", - "integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", + "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", "cpu": [ "arm64" ], @@ -787,9 +863,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.36.0.tgz", - "integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", + "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", "cpu": [ "x64" ], @@ -801,9 +877,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.36.0.tgz", - "integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", + "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", "cpu": [ "arm64" ], @@ -815,9 +891,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.36.0.tgz", - "integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", + "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", "cpu": [ "x64" ], @@ -829,9 +905,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.36.0.tgz", - "integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", + "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", "cpu": [ "arm" ], @@ -843,9 +919,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.36.0.tgz", - "integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", + "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", "cpu": [ "arm" ], @@ -857,9 +933,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.36.0.tgz", - "integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", + "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", "cpu": [ "arm64" ], @@ -871,9 +947,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.36.0.tgz", - "integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", + "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", "cpu": [ "arm64" ], @@ -885,9 +961,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.36.0.tgz", - "integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", + "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", "cpu": [ "loong64" ], @@ -899,9 +975,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.36.0.tgz", - "integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", + "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", "cpu": [ "ppc64" ], @@ -913,9 +989,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.36.0.tgz", - "integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", + "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", + "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", "cpu": [ "riscv64" ], @@ -927,9 +1017,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.36.0.tgz", - "integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", + "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", "cpu": [ "s390x" ], @@ -941,9 +1031,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.36.0.tgz", - "integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", + "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", "cpu": [ "x64" ], @@ -955,9 +1045,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.36.0.tgz", - "integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", + "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", "cpu": [ "x64" ], @@ -969,9 +1059,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.36.0.tgz", - "integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", + "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", "cpu": [ "arm64" ], @@ -983,9 +1073,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.36.0.tgz", - "integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", + "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", "cpu": [ "ia32" ], @@ -997,9 +1087,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.36.0.tgz", - "integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", + "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", "cpu": [ "x64" ], @@ -1018,15 +1108,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.11.tgz", - "integrity": "sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.9.tgz", + "integrity": "sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.19" + "@swc/types": "^0.1.23" }, "engines": { "node": ">=10" @@ -1036,19 +1126,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.11", - "@swc/core-darwin-x64": "1.11.11", - "@swc/core-linux-arm-gnueabihf": "1.11.11", - "@swc/core-linux-arm64-gnu": "1.11.11", - "@swc/core-linux-arm64-musl": "1.11.11", - "@swc/core-linux-x64-gnu": "1.11.11", - "@swc/core-linux-x64-musl": "1.11.11", - "@swc/core-win32-arm64-msvc": "1.11.11", - "@swc/core-win32-ia32-msvc": "1.11.11", - "@swc/core-win32-x64-msvc": "1.11.11" + "@swc/core-darwin-arm64": "1.12.9", + "@swc/core-darwin-x64": "1.12.9", + "@swc/core-linux-arm-gnueabihf": "1.12.9", + "@swc/core-linux-arm64-gnu": "1.12.9", + "@swc/core-linux-arm64-musl": "1.12.9", + "@swc/core-linux-x64-gnu": "1.12.9", + "@swc/core-linux-x64-musl": "1.12.9", + "@swc/core-win32-arm64-msvc": "1.12.9", + "@swc/core-win32-ia32-msvc": "1.12.9", + "@swc/core-win32-x64-msvc": "1.12.9" }, "peerDependencies": { - "@swc/helpers": "*" + "@swc/helpers": ">=0.5.17" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1057,9 +1147,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.11.tgz", - "integrity": "sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz", + "integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==", "cpu": [ "arm64" ], @@ -1074,9 +1164,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.11.tgz", - "integrity": "sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.9.tgz", + "integrity": "sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==", "cpu": [ "x64" ], @@ -1091,9 +1181,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.11.tgz", - "integrity": "sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.9.tgz", + "integrity": "sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==", "cpu": [ "arm" ], @@ -1108,9 +1198,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.11.tgz", - "integrity": "sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz", + "integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==", "cpu": [ "arm64" ], @@ -1125,9 +1215,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.11.tgz", - "integrity": "sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.9.tgz", + "integrity": "sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==", "cpu": [ "arm64" ], @@ -1142,9 +1232,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.11.tgz", - "integrity": "sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz", + "integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==", "cpu": [ "x64" ], @@ -1159,9 +1249,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.11.tgz", - "integrity": "sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz", + "integrity": "sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==", "cpu": [ "x64" ], @@ -1176,9 +1266,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.11.tgz", - "integrity": "sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.9.tgz", + "integrity": "sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==", "cpu": [ "arm64" ], @@ -1193,9 +1283,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.11.tgz", - "integrity": "sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.9.tgz", + "integrity": "sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==", "cpu": [ "ia32" ], @@ -1210,9 +1300,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.11.tgz", - "integrity": "sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==", + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.9.tgz", + "integrity": "sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==", "cpu": [ "x64" ], @@ -1234,9 +1324,9 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", - "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1244,44 +1334,54 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz", - "integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", "dev": true, "license": "MIT", "dependencies": { + "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "tailwindcss": "4.0.14" + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz", - "integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.0.14", - "@tailwindcss/oxide-darwin-arm64": "4.0.14", - "@tailwindcss/oxide-darwin-x64": "4.0.14", - "@tailwindcss/oxide-freebsd-x64": "4.0.14", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", - "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", - "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", - "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", - "@tailwindcss/oxide-linux-x64-musl": "4.0.14", - "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", - "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz", - "integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", "cpu": [ "arm64" ], @@ -1296,9 +1396,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz", - "integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", "cpu": [ "arm64" ], @@ -1313,9 +1413,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz", - "integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", "cpu": [ "x64" ], @@ -1330,9 +1430,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz", - "integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", "cpu": [ "x64" ], @@ -1347,9 +1447,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz", - "integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", "cpu": [ "arm" ], @@ -1364,9 +1464,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz", - "integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", "cpu": [ "arm64" ], @@ -1381,9 +1481,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz", - "integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", "cpu": [ "arm64" ], @@ -1398,9 +1498,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz", - "integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", "cpu": [ "x64" ], @@ -1415,9 +1515,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz", - "integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", "cpu": [ "x64" ], @@ -1431,10 +1531,100 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz", - "integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", "cpu": [ "arm64" ], @@ -1449,9 +1639,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz", - "integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", "cpu": [ "x64" ], @@ -1465,26 +1655,102 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@tailwindcss/vite": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.14.tgz", - "integrity": "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.0.14", - "@tailwindcss/oxide": "4.0.14", - "lightningcss": "1.29.2", - "tailwindcss": "4.0.14" + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" }, "peerDependencies": { - "vite": "^5.2.0 || ^6" + "vite": "^5.2.0 || ^6 || ^7" } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -1503,9 +1769,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.0.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.11.tgz", - "integrity": "sha512-vrdxRZfo9ALXth6yPfV16PYTLZwsUWhVjjC+DkfE5t1suNSbBrWC9YqSuuxJZ8Ps6z1o2ycRpIqzZJIgklq4Tw==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, "license": "MIT", "dependencies": { @@ -1513,9 +1779,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1523,21 +1789,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", - "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", + "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/type-utils": "8.26.1", - "@typescript-eslint/utils": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/type-utils": "8.35.1", + "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1547,22 +1813,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.35.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", - "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", + "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/typescript-estree": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4" }, "engines": { @@ -1577,15 +1853,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", + "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.1", + "@typescript-eslint/types": "^8.35.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", - "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", + "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1" + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1595,17 +1893,34 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", + "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", - "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", + "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.1", - "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/utils": "8.35.1", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1620,9 +1935,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", - "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", + "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", "dev": true, "license": "MIT", "engines": { @@ -1634,20 +1949,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", - "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", + "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/project-service": "8.35.1", + "@typescript-eslint/tsconfig-utils": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1661,9 +1978,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1687,9 +2004,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -1700,16 +2017,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", - "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", + "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/typescript-estree": "8.26.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1724,14 +2041,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", - "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", + "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.35.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1742,22 +2059,23 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", - "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", + "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==", "dev": true, "license": "MIT", "dependencies": { - "@swc/core": "^1.10.15" + "@rolldown/pluginutils": "1.0.0-beta.11", + "@swc/core": "^1.11.31" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6" + "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1849,18 +2167,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2380,9 +2700,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2432,9 +2752,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -2442,18 +2762,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -2465,21 +2785,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -2488,7 +2811,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -2660,20 +2983,20 @@ } }, "node_modules/eslint": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", - "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.1.0", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.22.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.30.1", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2684,9 +3007,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2743,9 +3066,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -2771,30 +3094,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -2815,9 +3138,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -2831,7 +3154,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -2879,9 +3202,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2896,9 +3219,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2909,15 +3232,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3771,6 +4094,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4078,10 +4414,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/knot-playground": { - "resolved": "knot", - "link": true - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4097,9 +4429,9 @@ } }, "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -4113,22 +4445,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "cpu": [ "arm64" ], @@ -4147,9 +4479,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "cpu": [ "x64" ], @@ -4168,9 +4500,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "cpu": [ "x64" ], @@ -4189,9 +4521,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "cpu": [ "arm" ], @@ -4210,9 +4542,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "cpu": [ "arm64" ], @@ -4231,9 +4563,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "cpu": [ "arm64" ], @@ -4252,9 +4584,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "cpu": [ "x64" ], @@ -4273,9 +4605,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "cpu": [ "x64" ], @@ -4294,9 +4626,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "cpu": [ "arm64" ], @@ -4315,9 +4647,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "cpu": [ "x64" ], @@ -4380,6 +4712,16 @@ "lz-string": "bin/bin.js" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4501,9 +4843,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", - "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -4831,9 +5173,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -4851,7 +5193,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4870,9 +5212,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -4908,10 +5250,10 @@ } }, "node_modules/pyodide": { - "version": "0.27.5", - "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.5.tgz", - "integrity": "sha512-nXErpLzEdtQolt+sNQ/5mKuN9XTUwhxR2MRhRhZ6oDRGpYLXrOp5+kkTPGEwK+wn1ZA8+poNmoxKTj2sq/p9og==", - "license": "Apache-2.0", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.28.0.tgz", + "integrity": "sha512-QML/Gh8eu50q5zZKLNpW6rgS0XUdK+94OSL54AUSKV8eJAxgwZrMebqj+CyM0EbF3EUX8JFJU3ryaxBViHammQ==", + "license": "MPL-2.0", "dependencies": { "ws": "^8.5.0" }, @@ -4941,24 +5283,24 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", "dependencies": { - "scheduler": "^0.25.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.0.0" + "react": "^19.1.0" } }, "node_modules/react-is": { @@ -4969,9 +5311,9 @@ "license": "MIT" }, "node_modules/react-resizable-panels": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", - "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.3.tgz", + "integrity": "sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==", "license": "MIT", "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", @@ -4991,10 +5333,6 @@ "node": ">=8.10.0" } }, - "node_modules/red_knot_wasm": { - "resolved": "knot/red_knot_wasm", - "link": true - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5099,13 +5437,13 @@ } }, "node_modules/rollup": { - "version": "4.36.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz", - "integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", + "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5115,25 +5453,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.36.0", - "@rollup/rollup-android-arm64": "4.36.0", - "@rollup/rollup-darwin-arm64": "4.36.0", - "@rollup/rollup-darwin-x64": "4.36.0", - "@rollup/rollup-freebsd-arm64": "4.36.0", - "@rollup/rollup-freebsd-x64": "4.36.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.36.0", - "@rollup/rollup-linux-arm-musleabihf": "4.36.0", - "@rollup/rollup-linux-arm64-gnu": "4.36.0", - "@rollup/rollup-linux-arm64-musl": "4.36.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.36.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.36.0", - "@rollup/rollup-linux-riscv64-gnu": "4.36.0", - "@rollup/rollup-linux-s390x-gnu": "4.36.0", - "@rollup/rollup-linux-x64-gnu": "4.36.0", - "@rollup/rollup-linux-x64-musl": "4.36.0", - "@rollup/rollup-win32-arm64-msvc": "4.36.0", - "@rollup/rollup-win32-ia32-msvc": "4.36.0", - "@rollup/rollup-win32-x64-msvc": "4.36.0", + "@rollup/rollup-android-arm-eabi": "4.44.2", + "@rollup/rollup-android-arm64": "4.44.2", + "@rollup/rollup-darwin-arm64": "4.44.2", + "@rollup/rollup-darwin-x64": "4.44.2", + "@rollup/rollup-freebsd-arm64": "4.44.2", + "@rollup/rollup-freebsd-x64": "4.44.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", + "@rollup/rollup-linux-arm-musleabihf": "4.44.2", + "@rollup/rollup-linux-arm64-gnu": "4.44.2", + "@rollup/rollup-linux-arm64-musl": "4.44.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-musl": "4.44.2", + "@rollup/rollup-linux-s390x-gnu": "4.44.2", + "@rollup/rollup-linux-x64-gnu": "4.44.2", + "@rollup/rollup-linux-x64-musl": "4.44.2", + "@rollup/rollup-win32-arm64-msvc": "4.44.2", + "@rollup/rollup-win32-ia32-msvc": "4.44.2", + "@rollup/rollup-win32-x64-msvc": "4.44.2", "fsevents": "~2.3.2" } }, @@ -5225,9 +5564,9 @@ } }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, "node_modules/semver": { @@ -5393,9 +5732,9 @@ } }, "node_modules/smol-toml": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz", - "integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.1.tgz", + "integrity": "sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -5420,6 +5759,20 @@ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -5568,9 +5921,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz", - "integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", "dev": true, "license": "MIT" }, @@ -5602,6 +5955,51 @@ "node": ">=10" } }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5616,9 +6014,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -5641,6 +6039,14 @@ "strip-bom": "^3.0.0" } }, + "node_modules/ty_wasm": { + "resolved": "ty/ty_wasm", + "link": true + }, + "node_modules/ty-playground": { + "resolved": "ty", + "link": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5733,9 +6139,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5747,15 +6153,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", - "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz", + "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.26.1", - "@typescript-eslint/parser": "8.26.1", - "@typescript-eslint/utils": "8.26.1" + "@typescript-eslint/eslint-plugin": "8.35.1", + "@typescript-eslint/parser": "8.35.1", + "@typescript-eslint/utils": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5809,21 +6215,24 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.2.tgz", + "integrity": "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "postcss": "^8.5.3", - "rollup": "^4.30.1" + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -5832,14 +6241,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -5881,23 +6290,51 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz", - "integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.0.tgz", + "integrity": "sha512-ONFBaYoN1qIiCxMCfeHI96lqLza7ujx/QClIXp4kEULUbyH2qLgYoaL8JHhk3FWjSB4TpzoaN3iMCyCFldyXzw==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.3", - "fast-glob": "^3.2.11", - "fs-extra": "^11.1.0", + "fs-extra": "^11.3.0", "p-map": "^7.0.3", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.14" }, "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/wasm-pack": { @@ -6037,9 +6474,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6087,14 +6524,14 @@ "monaco-editor": "^0.52.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.0.0", + "react-resizable-panels": "^3.0.0", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", "smol-toml": "^1.3.0" } }, "ruff/ruff_wasm": { - "version": "0.11.3", + "version": "0.11.10", "license": "MIT" }, "shared": { @@ -6103,8 +6540,32 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "react": "^19.0.0", - "react-resizable-panels": "^2.1.7" + "react-resizable-panels": "^3.0.0" } + }, + "ty": { + "name": "ty-playground", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "classnames": "^2.5.1", + "lz-string": "^1.5.0", + "monaco-editor": "^0.52.2", + "pyodide": "^0.28.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-resizable-panels": "^3.0.0", + "shared": "0.0.0", + "smol-toml": "^1.3.1", + "ty_wasm": "file:ty_wasm" + }, + "devDependencies": { + "vite-plugin-static-copy": "^3.1.0" + } + }, + "ty/ty_wasm": { + "version": "0.0.0", + "license": "MIT" } } } diff --git a/playground/package.json b/playground/package.json index 82a74c906cbcd..771b356ae01d9 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,15 +5,15 @@ "type": "module", "scripts": { "check": "npm run dev:wasm && npm run lint && npm run tsc", - "dev:wasm": "npm run dev:wasm --workspace knot-playground && npm run dev:wasm --workspace ruff-playground", - "dev:build": "npm run dev:build --workspace knot-playground && npm run dev:build --workspace ruff-playground", + "dev:wasm": "npm run dev:wasm --workspace ty-playground && npm run dev:wasm --workspace ruff-playground", + "dev:build": "npm run dev:build --workspace ty-playground && npm run dev:build --workspace ruff-playground", "fmt": "prettier --cache -w .", "fmt:check": "prettier --cache --check .", - "lint": "eslint --cache --ext .ts,.tsx ruff/src knot/src", + "lint": "eslint --cache --ext .ts,.tsx ruff/src ty/src", "tsc": "tsc" }, "workspaces": [ - "knot", + "ty", "ruff", "shared" ], @@ -34,7 +34,7 @@ "tailwindcss": "^4.0.14", "typescript": "^5.8.2", "typescript-eslint": "^8.26.1", - "vite": "^6.2.2", + "vite": "^7.0.0", "wasm-pack": "^0.13.1" } } diff --git a/playground/ruff/package.json b/playground/ruff/package.json index 9e99e572d6805..b14927998d447 100644 --- a/playground/ruff/package.json +++ b/playground/ruff/package.json @@ -21,7 +21,7 @@ "monaco-editor": "^0.52.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.0.0", + "react-resizable-panels": "^3.0.0", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", "smol-toml": "^1.3.0" diff --git a/playground/ruff/src/Editor/Chrome.tsx b/playground/ruff/src/Editor/Chrome.tsx index db70b064712d9..3fc9ddca2a997 100644 --- a/playground/ruff/src/Editor/Chrome.tsx +++ b/playground/ruff/src/Editor/Chrome.tsx @@ -87,8 +87,8 @@ export default function Chrome() {
= new Map(); + return (
    - {diagnostics.map((diagnostic, index) => { + {diagnostics.map((diagnostic) => { + const row = diagnostic.start_location.row; + const column = diagnostic.start_location.column; + const mostlyUniqueId = `${row}:${column}-${diagnostic.code}`; + + const disambiguator = uniqueIds.get(mostlyUniqueId) ?? 0; + uniqueIds.set(mostlyUniqueId, disambiguator + 1); + return ( -
  • +
  • diff --git a/playground/shared/package.json b/playground/shared/package.json index b527ec6aace93..50b86da136b6a 100644 --- a/playground/shared/package.json +++ b/playground/shared/package.json @@ -7,7 +7,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "react": "^19.0.0", - "react-resizable-panels": "^2.1.7" + "react-resizable-panels": "^3.0.0" }, "exports": "./src/index.ts" } diff --git a/playground/shared/src/Header.tsx b/playground/shared/src/Header.tsx index 3dfad3785270e..7881fb53447e3 100644 --- a/playground/shared/src/Header.tsx +++ b/playground/shared/src/Header.tsx @@ -9,7 +9,7 @@ import AstralButton from "./AstralButton"; export default function Header({ edit, theme, - logo, + tool, version, onChangeTheme, onReset, @@ -17,7 +17,7 @@ export default function Header({ }: { edit: number | null; theme: Theme; - logo: "ruff" | "astral"; + tool: "ruff" | "ty"; version: string | null; onChangeTheme: (theme: Theme) => void; onReset?(): void; @@ -37,16 +37,16 @@ export default function Header({ " >
    - +
    {version ? (
    - v{version} + {version}
    ) : null} - +
    @@ -77,30 +77,26 @@ function Divider() { ); } -function Logo({ - name, - className, -}: { - name: "ruff" | "astral"; - className: string; -}) { +function Logo({ name, className }: { name: "ruff" | "ty"; className: string }) { switch (name) { case "ruff": return ( - - - + + + + + ); - case "astral": + case "ty": return ( + + + + + + + + + + + + Playground | ty + + + + + + + + + + + + + +
    + + + diff --git a/playground/ty/package.json b/playground/ty/package.json new file mode 100644 index 0000000000000..4c4c5233e7679 --- /dev/null +++ b/playground/ty/package.json @@ -0,0 +1,39 @@ +{ + "name": "ty-playground", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "prebuild": "npm run build:wasm", + "build": "vite build", + "build:wasm": "wasm-pack build ../../crates/ty_wasm --target web --out-dir ../../playground/ty/ty_wasm", + "dev:wasm": "wasm-pack build ../../crates/ty_wasm --dev --target web --out-dir ../../playground/ty/ty_wasm", + "predev:build": "npm run dev:wasm", + "dev:build": "vite build", + "prestart": "npm run dev:wasm", + "start": "vite", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "classnames": "^2.5.1", + "lz-string": "^1.5.0", + "monaco-editor": "^0.52.2", + "pyodide": "^0.28.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-resizable-panels": "^3.0.0", + "shared": "0.0.0", + "smol-toml": "^1.3.1", + "ty_wasm": "file:ty_wasm" + }, + "overrides": { + "@monaco-editor/react": { + "react": "$react", + "react-dom": "$react-dom" + } + }, + "devDependencies": { + "vite-plugin-static-copy": "^3.0.0" + } +} diff --git a/playground/knot/public/Astral.png b/playground/ty/public/Astral.png similarity index 100% rename from playground/knot/public/Astral.png rename to playground/ty/public/Astral.png diff --git a/playground/knot/public/apple-touch-icon.png b/playground/ty/public/apple-touch-icon.png similarity index 100% rename from playground/knot/public/apple-touch-icon.png rename to playground/ty/public/apple-touch-icon.png diff --git a/playground/ty/public/favicon-16x16.png b/playground/ty/public/favicon-16x16.png new file mode 100644 index 0000000000000..10c29f0955899 Binary files /dev/null and b/playground/ty/public/favicon-16x16.png differ diff --git a/playground/ty/public/favicon-32x32.png b/playground/ty/public/favicon-32x32.png new file mode 100644 index 0000000000000..4c2ff24155a86 Binary files /dev/null and b/playground/ty/public/favicon-32x32.png differ diff --git a/playground/ty/public/favicon.ico b/playground/ty/public/favicon.ico new file mode 100644 index 0000000000000..1f3780390840a Binary files /dev/null and b/playground/ty/public/favicon.ico differ diff --git a/playground/knot/src/Editor/Chrome.tsx b/playground/ty/src/Editor/Chrome.tsx similarity index 99% rename from playground/knot/src/Editor/Chrome.tsx rename to playground/ty/src/Editor/Chrome.tsx index 4e055015d6d91..334f47a282610 100644 --- a/playground/knot/src/Editor/Chrome.tsx +++ b/playground/ty/src/Editor/Chrome.tsx @@ -13,7 +13,7 @@ import { Theme, VerticalResizeHandle, } from "shared"; -import type { Workspace } from "red_knot_wasm"; +import type { Workspace } from "ty_wasm"; import { Panel, PanelGroup } from "react-resizable-panels"; import { Files, isPythonFile } from "./Files"; import SecondarySideBar from "./SecondarySideBar"; diff --git a/playground/knot/src/Editor/Diagnostics.tsx b/playground/ty/src/Editor/Diagnostics.tsx similarity index 97% rename from playground/knot/src/Editor/Diagnostics.tsx rename to playground/ty/src/Editor/Diagnostics.tsx index a8616c9bb00ef..c1a395d2a12ff 100644 --- a/playground/knot/src/Editor/Diagnostics.tsx +++ b/playground/ty/src/Editor/Diagnostics.tsx @@ -1,4 +1,4 @@ -import type { Severity, Range, TextRange } from "red_knot_wasm"; +import type { Severity, Range, TextRange } from "ty_wasm"; import classNames from "classnames"; import { Theme } from "shared"; import { useMemo } from "react"; diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx new file mode 100644 index 0000000000000..0fe8d5149a91f --- /dev/null +++ b/playground/ty/src/Editor/Editor.tsx @@ -0,0 +1,739 @@ +/** + * Editor for the Python source code. + */ + +import Moncao, { Monaco, OnMount } from "@monaco-editor/react"; +import { + CancellationToken, + editor, + IDisposable, + IPosition, + IRange, + languages, + MarkerSeverity, + Position, + Range, + Uri, +} from "monaco-editor"; +import { useCallback, useEffect, useRef } from "react"; +import { Theme } from "shared"; +import { + Position as TyPosition, + Range as TyRange, + SemanticToken, + Severity, + type Workspace, + CompletionKind, +} from "ty_wasm"; +import { FileId, ReadonlyFiles } from "../Playground"; +import { isPythonFile } from "./Files"; +import { Diagnostic } from "./Diagnostics"; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; +import CompletionItemKind = languages.CompletionItemKind; + +type Props = { + visible: boolean; + fileName: string; + selected: FileId; + files: ReadonlyFiles; + diagnostics: Diagnostic[]; + theme: Theme; + workspace: Workspace; + onChange(content: string): void; + onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void; + onOpenFile(file: FileId): void; +}; + +export default function Editor({ + visible, + fileName, + selected, + files, + theme, + diagnostics, + workspace, + onChange, + onMount, + onOpenFile, +}: Props) { + const serverRef = useRef(null); + + if (serverRef.current != null) { + serverRef.current.update({ + files, + workspace, + onOpenFile, + }); + } + + // Update the diagnostics in the editor. + useEffect(() => { + const server = serverRef.current; + + if (server == null) { + return; + } + + server.updateDiagnostics(diagnostics); + }, [diagnostics]); + + const handleChange = useCallback( + (value: string | undefined) => { + onChange(value ?? ""); + }, + [onChange], + ); + + useEffect(() => { + return () => { + const server = serverRef.current; + + if (server != null) { + server.dispose(); + } + }; + }, []); + + const handleMount: OnMount = useCallback( + (editor, instance) => { + serverRef.current?.dispose(); + + const server = new PlaygroundServer(instance, { + workspace, + files, + onOpenFile, + }); + + server.updateDiagnostics(diagnostics); + serverRef.current = server; + + onMount(editor, instance); + }, + + [files, onOpenFile, workspace, onMount, diagnostics], + ); + + return ( + + ); +} + +interface PlaygroundServerProps { + workspace: Workspace; + files: ReadonlyFiles; + onOpenFile: (file: FileId) => void; +} + +class PlaygroundServer + implements + languages.TypeDefinitionProvider, + editor.ICodeEditorOpener, + languages.HoverProvider, + languages.InlayHintsProvider, + languages.DocumentFormattingEditProvider, + languages.CompletionItemProvider, + languages.DocumentSemanticTokensProvider, + languages.DocumentRangeSemanticTokensProvider, + languages.SignatureHelpProvider +{ + private typeDefinitionProviderDisposable: IDisposable; + private editorOpenerDisposable: IDisposable; + private hoverDisposable: IDisposable; + private inlayHintsDisposable: IDisposable; + private formatDisposable: IDisposable; + private completionDisposable: IDisposable; + private semanticTokensDisposable: IDisposable; + private rangeSemanticTokensDisposable: IDisposable; + private signatureHelpDisposable: IDisposable; + + constructor( + private monaco: Monaco, + private props: PlaygroundServerProps, + ) { + this.typeDefinitionProviderDisposable = + monaco.languages.registerTypeDefinitionProvider("python", this); + this.hoverDisposable = monaco.languages.registerHoverProvider( + "python", + this, + ); + this.inlayHintsDisposable = monaco.languages.registerInlayHintsProvider( + "python", + this, + ); + this.completionDisposable = monaco.languages.registerCompletionItemProvider( + "python", + this, + ); + this.semanticTokensDisposable = + monaco.languages.registerDocumentSemanticTokensProvider("python", this); + this.rangeSemanticTokensDisposable = + monaco.languages.registerDocumentRangeSemanticTokensProvider( + "python", + this, + ); + this.editorOpenerDisposable = monaco.editor.registerEditorOpener(this); + this.formatDisposable = + monaco.languages.registerDocumentFormattingEditProvider("python", this); + this.signatureHelpDisposable = + monaco.languages.registerSignatureHelpProvider("python", this); + } + + triggerCharacters: string[] = ["."]; + signatureHelpTriggerCharacters: string[] = ["(", ","]; + signatureHelpRetriggerCharacters: string[] = [")"]; + + getLegend(): languages.SemanticTokensLegend { + return { + tokenTypes: SemanticToken.kinds(), + tokenModifiers: SemanticToken.modifiers(), + }; + } + + provideDocumentSemanticTokens( + model: editor.ITextModel, + ): languages.SemanticTokens | null { + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return null; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return null; + } + + const tokens = this.props.workspace.semanticTokens(selectedHandle); + return generateMonacoTokens(tokens, model); + } + + releaseDocumentSemanticTokens() {} + + provideDocumentRangeSemanticTokens( + model: editor.ITextModel, + range: Range, + ): languages.SemanticTokens | null { + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return null; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return null; + } + + const tyRange = monacoRangeToTyRange(range); + + const tokens = this.props.workspace.semanticTokensInRange( + selectedHandle, + tyRange, + ); + + return generateMonacoTokens(tokens, model); + } + + provideCompletionItems( + model: editor.ITextModel, + position: Position, + ): languages.ProviderResult { + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const completions = this.props.workspace.completions( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + // If completions is 100, this gives us "99" which has a length of two + const digitsLength = String(completions.length - 1).length; + + return { + suggestions: completions.map((completion, i) => ({ + label: completion.name, + sortText: String(i).padStart(digitsLength, "0"), + kind: + completion.kind == null + ? CompletionItemKind.Variable + : mapCompletionKind(completion.kind), + insertText: completion.name, + // TODO(micha): It's unclear why this field is required for monaco but not VS Code. + // and omitting it works just fine? The LSP doesn't expose this information right now + // which is why we go with undefined for now. + range: undefined as any, + })), + }; + } + + resolveCompletionItem: undefined; + + provideSignatureHelp( + model: editor.ITextModel, + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: CancellationToken, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _context: languages.SignatureHelpContext, + ): languages.ProviderResult { + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const signatureHelp = this.props.workspace.signatureHelp( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + if (signatureHelp == null) { + return undefined; + } + + return { + dispose() {}, + value: { + signatures: signatureHelp.signatures.map((sig) => ({ + label: sig.label, + documentation: sig.documentation + ? { value: sig.documentation } + : undefined, + parameters: sig.parameters.map((param) => ({ + label: param.label, + documentation: param.documentation + ? { value: param.documentation } + : undefined, + })), + activeParameter: sig.active_parameter, + })), + activeSignature: signatureHelp.active_signature ?? 0, + activeParameter: + signatureHelp.active_signature != null + ? (signatureHelp.signatures[signatureHelp.active_signature] + ?.active_parameter ?? 0) + : 0, + }, + }; + } + + provideInlayHints( + _model: editor.ITextModel, + range: Range, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: CancellationToken, + ): languages.ProviderResult { + const workspace = this.props.workspace; + const selectedFile = this.props.files.selected; + + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const inlayHints = workspace.inlayHints( + selectedHandle, + monacoRangeToTyRange(range), + ); + + if (inlayHints.length === 0) { + return undefined; + } + + return { + dispose: () => {}, + hints: inlayHints.map((hint) => ({ + label: hint.markdown, + position: { + lineNumber: hint.position.line, + column: hint.position.column, + }, + })), + }; + } + + resolveInlayHint( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _hint: languages.InlayHint, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: CancellationToken, + ): languages.ProviderResult { + return undefined; + } + + update(props: PlaygroundServerProps) { + this.props = props; + } + + updateDiagnostics(diagnostics: Array) { + if (this.props.files.selected == null) { + return; + } + + const handle = this.props.files.handles[this.props.files.selected]; + + if (handle == null) { + return; + } + + const editor = this.monaco.editor; + const model = editor.getModel(Uri.parse(handle.path())); + + if (model == null) { + return; + } + + editor.setModelMarkers( + model, + "owner", + diagnostics.map((diagnostic) => { + const mapSeverity = (severity: Severity) => { + switch (severity) { + case Severity.Info: + return MarkerSeverity.Info; + case Severity.Warning: + return MarkerSeverity.Warning; + case Severity.Error: + return MarkerSeverity.Error; + case Severity.Fatal: + return MarkerSeverity.Error; + } + }; + + const range = diagnostic.range; + + return { + code: diagnostic.id, + startLineNumber: range?.start?.line ?? 0, + startColumn: range?.start?.column ?? 0, + endLineNumber: range?.end?.line ?? 0, + endColumn: range?.end?.column ?? 0, + message: diagnostic.message, + severity: mapSeverity(diagnostic.severity), + tags: [], + }; + }), + ); + } + + provideHover( + model: editor.ITextModel, + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _token: CancellationToken, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context?: languages.HoverContext | undefined, + ): languages.ProviderResult { + const workspace = this.props.workspace; + + const selectedFile = this.props.files.selected; + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const hover = workspace.hover( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + if (hover == null) { + return; + } + + return { + range: tyRangeToMonacoRange(hover.range), + contents: [{ value: hover.markdown, isTrusted: true }], + }; + } + + provideTypeDefinition( + model: editor.ITextModel, + position: Position, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _: CancellationToken, + ): languages.ProviderResult { + const workspace = this.props.workspace; + + const selectedFile = this.props.files.selected; + if (selectedFile == null) { + return; + } + + const selectedHandle = this.props.files.handles[selectedFile]; + + if (selectedHandle == null) { + return; + } + + const links = workspace.gotoTypeDefinition( + selectedHandle, + new TyPosition(position.lineNumber, position.column), + ); + + return ( + links + .map((link) => { + const targetSelection = + link.selection_range == null + ? undefined + : tyRangeToMonacoRange(link.selection_range); + + const originSelection = + link.origin_selection_range == null + ? undefined + : tyRangeToMonacoRange(link.origin_selection_range); + + return { + uri: Uri.parse(link.path), + range: tyRangeToMonacoRange(link.full_range), + targetSelectionRange: targetSelection, + originSelectionRange: originSelection, + } as languages.LocationLink; + }) + // Filter out vendored files because they aren't open in the editor. + .filter((link) => link.uri.scheme !== "vendored") + ); + } + + openCodeEditor( + source: editor.ICodeEditor, + resource: Uri, + selectionOrPosition?: IRange | IPosition, + ): boolean { + const files = this.props.files; + + const fileId = files.index.find((file) => { + return Uri.file(file.name).toString() === resource.toString(); + })?.id; + + if (fileId == null) { + return false; + } + + const handle = files.handles[fileId]; + + let model = this.monaco.editor.getModel(resource); + if (model == null) { + const language = + handle != null && isPythonFile(handle) ? "python" : undefined; + model = this.monaco.editor.createModel( + files.contents[fileId], + language, + resource, + ); + } + + // it's a bit hacky to create the model manually + // but only using `onOpenFile` isn't enough + // because the model doesn't get updated until the next render. + if (files.selected !== fileId) { + source.setModel(model); + + this.props.onOpenFile(fileId); + } + + if (selectionOrPosition != null) { + if (Position.isIPosition(selectionOrPosition)) { + source.setPosition(selectionOrPosition); + source.revealPosition(selectionOrPosition); + } else { + source.setSelection(selectionOrPosition); + source.revealPosition({ + lineNumber: selectionOrPosition.startLineNumber, + column: selectionOrPosition.startColumn, + }); + } + } + + return true; + } + + provideDocumentFormattingEdits( + model: editor.ITextModel, + ): languages.ProviderResult { + if (this.props.files.selected == null) { + return null; + } + + const fileHandle = this.props.files.handles[this.props.files.selected]; + + if (fileHandle == null) { + return null; + } + + const formatted = this.props.workspace.format(fileHandle); + if (formatted != null) { + return [ + { + range: model.getFullModelRange(), + text: formatted, + }, + ]; + } + + return null; + } + + dispose() { + this.hoverDisposable.dispose(); + this.editorOpenerDisposable.dispose(); + this.typeDefinitionProviderDisposable.dispose(); + this.inlayHintsDisposable.dispose(); + this.formatDisposable.dispose(); + this.rangeSemanticTokensDisposable.dispose(); + this.semanticTokensDisposable.dispose(); + this.completionDisposable.dispose(); + this.signatureHelpDisposable.dispose(); + } +} + +function tyRangeToMonacoRange(range: TyRange): IRange { + return { + startLineNumber: range.start.line, + startColumn: range.start.column, + endLineNumber: range.end.line, + endColumn: range.end.column, + }; +} + +function monacoRangeToTyRange(range: IRange): TyRange { + return new TyRange( + new TyPosition(range.startLineNumber, range.startColumn), + new TyPosition(range.endLineNumber, range.endColumn), + ); +} + +function generateMonacoTokens( + semantic: SemanticToken[], + model: editor.ITextModel, +): languages.SemanticTokens { + const result = []; + + let prevLine = 0; + let prevChar = 0; + + for (const token of semantic) { + // Convert from 1-based to 0-based indexing for Monaco + const line = token.range.start.line - 1; + const char = token.range.start.column - 1; + + const length = model.getValueLengthInRange( + tyRangeToMonacoRange(token.range), + ); + + result.push( + line - prevLine, + prevLine === line ? char - prevChar : char, + length, + token.kind, + token.modifiers, + ); + + prevLine = line; + prevChar = char; + } + + return { data: Uint32Array.from(result) }; +} + +function mapCompletionKind(kind: CompletionKind): CompletionItemKind { + switch (kind) { + case CompletionKind.Text: + return CompletionItemKind.Text; + case CompletionKind.Method: + return CompletionItemKind.Method; + case CompletionKind.Function: + return CompletionItemKind.Function; + case CompletionKind.Constructor: + return CompletionItemKind.Constructor; + case CompletionKind.Field: + return CompletionItemKind.Field; + case CompletionKind.Variable: + return CompletionItemKind.Variable; + case CompletionKind.Class: + return CompletionItemKind.Class; + case CompletionKind.Interface: + return CompletionItemKind.Interface; + case CompletionKind.Module: + return CompletionItemKind.Module; + case CompletionKind.Property: + return CompletionItemKind.Property; + case CompletionKind.Unit: + return CompletionItemKind.Unit; + case CompletionKind.Value: + return CompletionItemKind.Value; + case CompletionKind.Enum: + return CompletionItemKind.Enum; + case CompletionKind.Keyword: + return CompletionItemKind.Keyword; + case CompletionKind.Snippet: + return CompletionItemKind.Snippet; + case CompletionKind.Color: + return CompletionItemKind.Color; + case CompletionKind.File: + return CompletionItemKind.File; + case CompletionKind.Reference: + return CompletionItemKind.Reference; + case CompletionKind.Folder: + return CompletionItemKind.Folder; + case CompletionKind.EnumMember: + return CompletionItemKind.EnumMember; + case CompletionKind.Constant: + return CompletionItemKind.Constant; + case CompletionKind.Struct: + return CompletionItemKind.Struct; + case CompletionKind.Event: + return CompletionItemKind.Event; + case CompletionKind.Operator: + return CompletionItemKind.Operator; + case CompletionKind.TypeParameter: + return CompletionItemKind.TypeParameter; + } +} diff --git a/playground/knot/src/Editor/Files.tsx b/playground/ty/src/Editor/Files.tsx similarity index 99% rename from playground/knot/src/Editor/Files.tsx rename to playground/ty/src/Editor/Files.tsx index 215baa2476446..d345754901331 100644 --- a/playground/knot/src/Editor/Files.tsx +++ b/playground/ty/src/Editor/Files.tsx @@ -2,7 +2,7 @@ import { Icons, Theme } from "shared"; import classNames from "classnames"; import { useState } from "react"; import { FileId } from "../Playground"; -import { type FileHandle } from "red_knot_wasm"; +import { type FileHandle } from "ty_wasm"; export interface Props { // The file names diff --git a/playground/knot/src/Editor/SecondaryPanel.tsx b/playground/ty/src/Editor/SecondaryPanel.tsx similarity index 100% rename from playground/knot/src/Editor/SecondaryPanel.tsx rename to playground/ty/src/Editor/SecondaryPanel.tsx diff --git a/playground/knot/src/Editor/SecondarySideBar.tsx b/playground/ty/src/Editor/SecondarySideBar.tsx similarity index 100% rename from playground/knot/src/Editor/SecondarySideBar.tsx rename to playground/ty/src/Editor/SecondarySideBar.tsx diff --git a/playground/knot/src/Editor/api.ts b/playground/ty/src/Editor/api.ts similarity index 100% rename from playground/knot/src/Editor/api.ts rename to playground/ty/src/Editor/api.ts diff --git a/playground/knot/src/Editor/persist.ts b/playground/ty/src/Editor/persist.ts similarity index 100% rename from playground/knot/src/Editor/persist.ts rename to playground/ty/src/Editor/persist.ts diff --git a/playground/knot/src/Playground.tsx b/playground/ty/src/Playground.tsx similarity index 91% rename from playground/knot/src/Playground.tsx rename to playground/ty/src/Playground.tsx index c3440a55e6604..9c87ff6c3c5b2 100644 --- a/playground/knot/src/Playground.tsx +++ b/playground/ty/src/Playground.tsx @@ -10,17 +10,17 @@ import { useState, } from "react"; import { ErrorMessage, Header, setupMonaco, useTheme } from "shared"; -import { FileHandle, Workspace } from "red_knot_wasm"; +import { FileHandle, PositionEncoding, Workspace } from "ty_wasm"; import { persist, persistLocal, restore } from "./Editor/persist"; import { loader } from "@monaco-editor/react"; -import knotSchema from "../../../knot.schema.json"; +import tySchema from "../../../ty.schema.json"; import Chrome, { formatError } from "./Editor/Chrome"; -export const SETTINGS_FILE_NAME = "knot.json"; +export const SETTINGS_FILE_NAME = "ty.json"; export default function Playground() { const [theme, setTheme] = useTheme(); - const [version, setVersion] = useState("0.0.0"); + const [version, setVersion] = useState(null); const [error, setError] = useState(null); const workspacePromiseRef = useRef | null>(null); const [workspace, setWorkspace] = useState(null); @@ -30,7 +30,7 @@ export default function Playground() { workspacePromiseRef.current = workspacePromise = startPlayground().then( (fetched) => { setVersion(fetched.version); - const workspace = new Workspace("/", {}); + const workspace = new Workspace("/", PositionEncoding.Utf16, {}); restoreWorkspace(workspace, fetched.workspace, dispatchFiles, setError); setWorkspace(workspace); return workspace; @@ -156,7 +156,7 @@ export default function Playground() {
    ; @@ -451,14 +451,15 @@ export interface InitializedPlayground { // Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state. async function startPlayground(): Promise { - const red_knot = await import("../red_knot_wasm"); - await red_knot.default(); + const ty = await import("../ty_wasm"); + await ty.default(); + const version = ty.version(); const monaco = await loader.init(); setupMonaco(monaco, { - uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/knot.schema.json", - fileMatch: ["knot.json"], - schema: knotSchema, + uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ty.schema.json", + fileMatch: ["ty.json"], + schema: tySchema, }); const restored = await restore(); @@ -466,7 +467,7 @@ async function startPlayground(): Promise { const workspace = restored ?? DEFAULT_WORKSPACE; return { - version: "0.0.0", + version, workspace, }; } @@ -483,7 +484,7 @@ function updateOptions( workspace?.updateOptions(settings); setError(null); } catch (error) { - setError(`Failed to update 'knot.json' options: ${formatError(error)}`); + setError(`Failed to update 'ty.json' options: ${formatError(error)}`); } } @@ -520,8 +521,17 @@ function restoreWorkspace( ) { let hasSettings = false; - for (const [name, content] of Object.entries(state.files)) { + // eslint-disable-next-line prefer-const + for (let [name, content] of Object.entries(state.files)) { let handle = null; + + if ( + name === "knot.json" && + !Object.keys(state.files).includes(SETTINGS_FILE_NAME) + ) { + name = SETTINGS_FILE_NAME; + } + if (name === SETTINGS_FILE_NAME) { updateOptions(workspace, content, setError); hasSettings = true; @@ -536,8 +546,11 @@ function restoreWorkspace( updateOptions(workspace, null, setError); } + const selected = + state.current === "knot.json" ? SETTINGS_FILE_NAME : state.current; + dispatchFiles({ type: "selectFileByName", - name: state.current, + name: selected, }); } diff --git a/playground/knot/src/index.css b/playground/ty/src/index.css similarity index 100% rename from playground/knot/src/index.css rename to playground/ty/src/index.css diff --git a/playground/knot/src/main.tsx b/playground/ty/src/main.tsx similarity index 100% rename from playground/knot/src/main.tsx rename to playground/ty/src/main.tsx diff --git a/playground/knot/src/third-party.d.ts b/playground/ty/src/third-party.d.ts similarity index 100% rename from playground/knot/src/third-party.d.ts rename to playground/ty/src/third-party.d.ts diff --git a/playground/knot/src/vite-env.d.ts b/playground/ty/src/vite-env.d.ts similarity index 100% rename from playground/knot/src/vite-env.d.ts rename to playground/ty/src/vite-env.d.ts diff --git a/playground/knot/vite.config.ts b/playground/ty/vite.config.ts similarity index 100% rename from playground/knot/vite.config.ts rename to playground/ty/vite.config.ts diff --git a/pyproject.toml b/pyproject.toml index 5b8626e935f3b..99d855fd00638 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.11.6" +version = "0.12.3" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" @@ -53,14 +53,11 @@ exclude = [ "crates/ruff_linter/resources/test/fixtures/**/*", "crates/ruff_linter/src/rules/*/snapshots/**/*" ] -include = [ - "rust-toolchain.toml" -] [tool.ruff] target-version = "py38" extend-exclude = [ - "crates/red_knot_vendored/vendor/", + "crates/ty_vendored/vendor/", "crates/ruff/resources/", "crates/ruff_linter/resources/", "crates/ruff_python_formatter/resources/", @@ -96,10 +93,14 @@ ignore = [ [tool.ruff.lint.isort] required-imports = ["from __future__ import annotations"] +[tool.ruff.per-file-target-version] +"crates/ty_python_semantic/mdtest.py" = "py310" +"crates/ty_vendored/ty_extensions/ty_extensions.pyi" = "py312" + [tool.black] force-exclude = ''' /( - | crates/red_knot_vendored/vendor + | crates/ty_vendored/vendor | crates/ruff_linter/resources | crates/ruff_python_formatter/resources | crates/ruff_python_parser/resources @@ -110,7 +111,7 @@ force-exclude = ''' major_labels = [] # Ruff never uses the major version number minor_labels = ["breaking"] # Bump the minor version on breaking changes -changelog_ignore_labels = ["internal", "ci", "red-knot", "testing"] +changelog_ignore_labels = ["internal", "ci", "testing", "ty"] changelog_sections.breaking = "Breaking changes" changelog_sections.preview = "Preview features" diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py index 1028ca2d90622..7735bef15cda5 100644 --- a/python/py-fuzzer/fuzz.py +++ b/python/py-fuzzer/fuzz.py @@ -12,7 +12,7 @@ `uvx --from ./python/py-fuzzer fuzz --bin ruff 0-2 78 93` - Run the fuzzer concurrently using seeds in range 0-10 inclusive, but only reporting bugs that are new on your branch: - `uvx --from ./python/py-fuzzer fuzz --bin ruff 0-10 --new-bugs-only` + `uvx --from ./python/py-fuzzer fuzz --bin ruff 0-10 --only-new-bugs` - Run the fuzzer concurrently on 10,000 different Python source-code files, using a random selection of seeds, and only print a summary at the end (the `shuf` command is Unix-specific): @@ -32,6 +32,7 @@ import enum import subprocess import tempfile +from collections.abc import Callable from dataclasses import KW_ONLY, dataclass from functools import partial from pathlib import Path @@ -47,17 +48,15 @@ ExitCode = NewType("ExitCode", int) -def redknot_contains_bug(code: str, *, red_knot_executable: Path) -> bool: +def ty_contains_bug(code: str, *, ty_executable: Path) -> bool: """Return `True` if the code triggers a panic in type-checking code.""" with tempfile.TemporaryDirectory() as tempdir: - Path(tempdir, "pyproject.toml").write_text('[project]\n\tname = "fuzz-input"') - Path(tempdir, "input.py").write_text(code) + input_file = Path(tempdir, "input.py") + input_file.write_text(code) completed_process = subprocess.run( - [red_knot_executable, "check", "--project", tempdir], - capture_output=True, - text=True, + [ty_executable, "check", input_file], capture_output=True, text=True ) - return completed_process.returncode != 0 and completed_process.returncode != 1 + return completed_process.returncode not in {0, 1, 2} def ruff_contains_bug(code: str, *, ruff_executable: Path) -> bool: @@ -86,8 +85,8 @@ def contains_bug(code: str, *, executable: Executable, executable_path: Path) -> match executable: case Executable.RUFF: return ruff_contains_bug(code, ruff_executable=executable_path) - case Executable.RED_KNOT: - return redknot_contains_bug(code, red_knot_executable=executable_path) + case Executable.TY: + return ty_contains_bug(code, ty_executable=executable_path) case _ as unreachable: assert_never(unreachable) @@ -121,6 +120,8 @@ class FuzzResult: maybe_bug: MinimizedSourceCode | None # The executable we're testing executable: Executable + _: KW_ONLY + only_new_bugs: bool def print_description(self, index: int, num_seeds: int) -> None: """Describe the results of fuzzing the parser with this seed.""" @@ -132,12 +133,14 @@ def print_description(self, index: int, num_seeds: int) -> None: ) print(f"{msg:<60} {progress:>15}", flush=True) + new = "new " if self.only_new_bugs else "" + if self.maybe_bug: match self.executable: case Executable.RUFF: - panic_message = "The following code triggers a parser bug:" - case Executable.RED_KNOT: - panic_message = "The following code triggers a red-knot panic:" + panic_message = f"The following code triggers a {new}parser bug:" + case Executable.TY: + panic_message = f"The following code triggers a {new}ty panic:" case _ as unreachable: assert_never(unreachable) @@ -149,48 +152,67 @@ def print_description(self, index: int, num_seeds: int) -> None: def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult: """Return a `FuzzResult` instance describing the fuzzing result from this seed.""" + # TODO(carljm) remove once we debug the slowness of these seeds + skip_check = seed in {120, 160, 335} + code = generate_random_code(seed) - has_bug = ( - contains_new_bug( + bug_found = False + minimizer_callback: Callable[[str], bool] | None = None + + if args.baseline_executable_path is None: + only_new_bugs = False + if not skip_check and contains_bug( + code, executable=args.executable, executable_path=args.test_executable_path + ): + bug_found = True + minimizer_callback = partial( + contains_bug, + executable=args.executable, + executable_path=args.test_executable_path, + ) + else: + only_new_bugs = True + if not skip_check and contains_new_bug( code, executable=args.executable, test_executable_path=args.test_executable_path, baseline_executable_path=args.baseline_executable_path, - ) - if args.baseline_executable_path is not None - else contains_bug( - code, executable=args.executable, executable_path=args.test_executable_path - ) - ) - if has_bug: - callback = partial( - contains_bug, - executable=args.executable, - executable_path=args.test_executable_path, - ) + ): + bug_found = True + minimizer_callback = partial( + contains_new_bug, + executable=args.executable, + test_executable_path=args.test_executable_path, + baseline_executable_path=args.baseline_executable_path, + ) + + if not bug_found: + return FuzzResult(seed, None, args.executable, only_new_bugs=only_new_bugs) + + assert minimizer_callback is not None + + try: + maybe_bug = MinimizedSourceCode(minimize_repro(code, minimizer_callback)) + except CouldNotMinimize as e: + # This is to double-check that there isn't a bug in + # `pysource-minimize`/`pysource-codegen`. + # `pysource-minimize` *should* never produce code that's invalid syntax. try: - maybe_bug = MinimizedSourceCode(minimize_repro(code, callback)) - except CouldNotMinimize as e: - # This is to double-check that there isn't a bug in - # `pysource-minimize`/`pysource-codegen`. - # `pysource-minimize` *should* never produce code that's invalid syntax. - try: - ast.parse(code) - except SyntaxError: - raise e from None - else: - maybe_bug = MinimizedSourceCode(code) + ast.parse(code) + except SyntaxError: + raise e from None + else: + maybe_bug = MinimizedSourceCode(code) - else: - maybe_bug = None - return FuzzResult(seed, maybe_bug, args.executable) + return FuzzResult(seed, maybe_bug, args.executable, only_new_bugs=only_new_bugs) def run_fuzzer_concurrently(args: ResolvedCliArgs) -> list[FuzzResult]: num_seeds = len(args.seeds) print( f"Concurrently running the fuzzer on " - f"{num_seeds} randomly generated source-code files..." + f"{num_seeds} randomly generated source-code " + f"file{'s' if num_seeds != 1 else ''}..." ) bugs: list[FuzzResult] = [] with concurrent.futures.ProcessPoolExecutor() as executor: @@ -218,7 +240,8 @@ def run_fuzzer_sequentially(args: ResolvedCliArgs) -> list[FuzzResult]: num_seeds = len(args.seeds) print( f"Sequentially running the fuzzer on " - f"{num_seeds} randomly generated source-code files..." + f"{num_seeds} randomly generated source-code " + f"file{'s' if num_seeds != 1 else ''}..." ) bugs: list[FuzzResult] = [] for i, seed in enumerate(args.seeds, start=1): @@ -245,6 +268,10 @@ def run_fuzzer(args: ResolvedCliArgs) -> ExitCode: return ExitCode(0) +def absolute_path(p: str) -> Path: + return Path(p).absolute() + + def parse_seed_argument(arg: str) -> int | range: """Helper for argument parsing""" if "-" in arg: @@ -270,7 +297,7 @@ def parse_seed_argument(arg: str) -> int | range: class Executable(enum.StrEnum): RUFF = "ruff" - RED_KNOT = "red_knot" + TY = "ty" @dataclass(slots=True) @@ -314,7 +341,7 @@ def parse_args() -> ResolvedCliArgs: "Executable to test. " "Defaults to a fresh build of the currently checked-out branch." ), - type=Path, + type=absolute_path, ) parser.add_argument( "--baseline-executable", @@ -323,7 +350,7 @@ def parse_args() -> ResolvedCliArgs: "Defaults to whatever version is installed " "in the Python environment." ), - type=Path, + type=absolute_path, ) parser.add_argument( "--bin", diff --git a/python/py-fuzzer/pyproject.toml b/python/py-fuzzer/pyproject.toml index fe6a35a6f89df..d6e380395ac6d 100644 --- a/python/py-fuzzer/pyproject.toml +++ b/python/py-fuzzer/pyproject.toml @@ -5,10 +5,10 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "pysource-codegen>=0.6.0", - "pysource-minimize>=0.7.0", - "rich-argparse>=1.6.0", - "ruff>=0.8.0", - "termcolor>=2.5.0", + "pysource-minimize>=0.8.0", + "rich-argparse>=1.7.0", + "ruff>=0.11.9", + "termcolor>=3.1.0", ] [project.scripts] diff --git a/python/py-fuzzer/uv.lock b/python/py-fuzzer/uv.lock index ce3e7d6ef1809..1be4ceed235b2 100644 --- a/python/py-fuzzer/uv.lock +++ b/python/py-fuzzer/uv.lock @@ -1,40 +1,7 @@ version = 1 +revision = 1 requires-python = ">=3.12" -[[package]] -name = "astunparse" -version = "1.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, - { name = "wheel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, -] - -[[package]] -name = "click" -version = "8.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - [[package]] name = "markdown-it-py" version = "3.0.0" @@ -109,10 +76,10 @@ dev = [ [package.metadata] requires-dist = [ { name = "pysource-codegen", specifier = ">=0.6.0" }, - { name = "pysource-minimize", specifier = ">=0.7.0" }, - { name = "rich-argparse", specifier = ">=1.6.0" }, - { name = "ruff", specifier = ">=0.8.0" }, - { name = "termcolor", specifier = ">=2.5.0" }, + { name = "pysource-minimize", specifier = ">=0.8.0" }, + { name = "rich-argparse", specifier = ">=1.7.0" }, + { name = "ruff", specifier = ">=0.11.9" }, + { name = "termcolor", specifier = ">=3.1.0" }, ] [package.metadata.requires-dev] @@ -144,16 +111,11 @@ wheels = [ [[package]] name = "pysource-minimize" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astunparse" }, - { name = "click" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/74/a095ade9a0d8b33b3eec3d84510dc2ff4327cd9fcd8340e107aaad8721fd/pysource_minimize-0.7.0.tar.gz", hash = "sha256:5fd477cba4d6251912d4e6cd21a19e6de03a72d1da1ec3bb40aa82ce468ddbe1", size = 792926 } +sdist = { url = "https://files.pythonhosted.org/packages/90/d3/6786a52121987875b2e9d273399504e2bdb868e7b80b603ecb29c900846f/pysource_minimize-0.8.0.tar.gz", hash = "sha256:e9a88c68717891dc7dc74beab769ef4c2e397599e1620b2046b89783fb500652", size = 715267 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/0f/68b7bef766029025412b919bb25694779666dfc09fe6d8604f0034778b3d/pysource_minimize-0.7.0-py3-none-any.whl", hash = "sha256:3d1f3e53ee51a697a1ed9416e243aae5d7bf5e8d21af534108901016bf50d535", size = 13525 }, + { url = "https://files.pythonhosted.org/packages/4a/7d/4e9ed2a376bb7372d74fdec557f35f70c2bf5373f2c67e05535555d0a6d4/pysource_minimize-0.8.0-py3-none-any.whl", hash = "sha256:edee433c24a2e8f81701aa7e01ba4c1e63f481f683dd3a561610762bc03ed6c3", size = 14635 }, ] [[package]] @@ -171,57 +133,48 @@ wheels = [ [[package]] name = "rich-argparse" -version = "1.6.0" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/ee/c410251ff6123d4417f2fe8e72c8628f187682b70ce34134a2a3e307a2d5/rich_argparse-1.6.0.tar.gz", hash = "sha256:092083c30da186f25bcdff8b1d47fdfb571288510fb051e0488a72cc3128de13", size = 17499 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b9/ff53663ee7fa6a4195fa96d91da499f2e00ca067541e016d345cce1c9ad2/rich_argparse-1.7.0.tar.gz", hash = "sha256:f31d809c465ee43f367d599ccaf88b73bc2c4d75d74ed43f2d538838c53544ba", size = 38009 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/45/54b95bb72bb17c27a7252bee5034955020b5869a33918b660ffc29cbf608/rich_argparse-1.6.0-py3-none-any.whl", hash = "sha256:fbe70a1d821b3f2fa8958cddf0cae131870a6e9faa04ab52b409cb1eda809bd7", size = 20072 }, + { url = "https://files.pythonhosted.org/packages/bb/9c/dc7cbeb99a7b7422392ed7f327efdbb958bc0faf424aef5f130309320bda/rich_argparse-1.7.0-py3-none-any.whl", hash = "sha256:b8ec8943588e9731967f4f97b735b03dc127c416f480a083060433a97baf2fd3", size = 25339 }, ] [[package]] name = "ruff" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, - { url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 }, - { url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 }, - { url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 }, - { url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 }, - { url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 }, - { url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 }, - { url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 }, - { url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 }, - { url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 }, - { url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 }, - { url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 }, - { url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 }, - { url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 }, - { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, - { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, - { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, -] - -[[package]] -name = "six" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +version = "0.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453 }, + { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566 }, + { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020 }, + { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935 }, + { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971 }, + { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631 }, + { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236 }, + { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436 }, + { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759 }, + { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985 }, + { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775 }, + { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307 }, + { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026 }, + { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627 }, + { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340 }, + { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080 }, ] [[package]] name = "termcolor" -version = "2.5.0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684 }, ] [[package]] @@ -232,12 +185,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec3 wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] - -[[package]] -name = "wheel" -version = "0.45.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, -] diff --git a/python/ruff-ecosystem/ruff_ecosystem/defaults.py b/python/ruff-ecosystem/ruff_ecosystem/defaults.py index 1fd66f4afd3f7..3c07c571ee0d3 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/defaults.py +++ b/python/ruff-ecosystem/ruff_ecosystem/defaults.py @@ -4,6 +4,7 @@ from ruff_ecosystem.projects import ( CheckOptions, + FormatOptions, Project, Repository, ) @@ -134,7 +135,12 @@ Project(repo=Repository(owner="wntrblm", name="nox", ref="main")), Project(repo=Repository(owner="pytest-dev", name="pytest", ref="main")), Project(repo=Repository(owner="encode", name="httpx", ref="master")), - Project(repo=Repository(owner="mesonbuild", name="meson-python", ref="main")), + Project( + repo=Repository(owner="mesonbuild", name="meson-python", ref="main"), + format_options=FormatOptions( + exclude="tests/packages/symlinks/baz.py,tests/packages/symlinks/qux.py" + ), + ), Project(repo=Repository(owner="pdm-project", name="pdm", ref="main")), Project(repo=Repository(owner="astropy", name="astropy", ref="main")), ] diff --git a/python/ruff/__main__.py b/python/ruff/__main__.py index 21fa9a20fa8e7..8569844c4ed34 100644 --- a/python/ruff/__main__.py +++ b/python/ruff/__main__.py @@ -78,7 +78,7 @@ def get_last_three_path_parts(path: str) -> list[str]: if __name__ == "__main__": - ruff = os.fsdecode(find_ruff_bin()) + ruff = find_ruff_bin() if sys.platform == "win32": import subprocess diff --git a/ruff.schema.json b/ruff.schema.json index cbe8fa22cb282..0489f90b8ae5e 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -873,14 +873,14 @@ "description": "Construct a map from module to its dependencies (i.e., the modules that it imports).", "type": "string", "enum": [ - "Dependencies" + "dependencies" ] }, { "description": "Construct a map from module to its dependents (i.e., the modules that import it).", "type": "string", "enum": [ - "Dependents" + "dependents" ] } ] @@ -1438,7 +1438,7 @@ } }, "banned-module-level-imports": { - "description": "List of specific modules that may not be imported at module level, and should instead be imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` block, or some other nested context).", + "description": "List of specific modules that may not be imported at module level, and should instead be imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` block, or some other nested context). This also affects the rule `import-outside-top-level` if `banned-module-level-imports` is enabled.", "type": [ "array", "null" @@ -1588,7 +1588,7 @@ ] }, "skip-magic-trailing-comma": { - "description": "Ruff uses existing trailing commas as an indication that short lines should be left separate. If this option is set to `true`, the magic trailing comma is ignored.\n\nFor example, Ruff leaves the arguments separate even though collapsing the arguments to a single line doesn't exceed the line length if `skip-magic-trailing-comma = false`:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test( a, b, ): pass ```\n\nSetting `skip-magic-trailing-comma = true` changes the formatting to:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test(a, b): pass ```", + "description": "Ruff uses existing trailing commas as an indication that short lines should be left separate. If this option is set to `true`, the magic trailing comma is ignored.\n\nFor example, Ruff leaves the arguments separate even though collapsing the arguments to a single line doesn't exceed the line length if `skip-magic-trailing-comma = false`:\n\n```python # The arguments remain on separate lines because of the trailing comma after `b` def test( a, b, ): pass ```\n\nSetting `skip-magic-trailing-comma = true` changes the formatting to:\n\n```python # The arguments are collapsed to a single line because the trailing comma is ignored def test(a, b): pass ```", "type": [ "boolean", "null" @@ -2459,6 +2459,13 @@ "type": "string" } }, + "typing-extensions": { + "description": "Whether to allow imports from the third-party `typing_extensions` module for Python versions before a symbol was added to the first-party `typing` module.\n\nMany rules try to import symbols from the `typing` module but fall back to `typing_extensions` for earlier versions of Python. This option can be used to disable this fallback behavior in cases where `typing_extensions` is not installed.", + "type": [ + "boolean", + "null" + ] + }, "typing-modules": { "description": "A list of modules whose exports should be treated equivalently to members of the `typing` module.\n\nThis is useful for ensuring proper type annotation inference for projects that re-export `typing` and `typing_extensions` members from a compatibility module. If omitted, any members imported from modules apart from `typing` and `typing_extensions` will be treated as ordinary Python objects.", "type": [ @@ -2841,7 +2848,8 @@ "py310", "py311", "py312", - "py313" + "py313", + "py314" ] }, "Quote": { @@ -3580,6 +3588,7 @@ "PLC020", "PLC0205", "PLC0206", + "PLC0207", "PLC0208", "PLC04", "PLC041", @@ -3881,6 +3890,7 @@ "PTH208", "PTH21", "PTH210", + "PTH211", "PYI", "PYI0", "PYI00", @@ -4027,6 +4037,11 @@ "RUF057", "RUF058", "RUF059", + "RUF06", + "RUF060", + "RUF061", + "RUF063", + "RUF064", "RUF1", "RUF10", "RUF100", @@ -4076,7 +4091,6 @@ "S318", "S319", "S32", - "S320", "S321", "S323", "S324", @@ -4289,6 +4303,8 @@ "UP046", "UP047", "UP049", + "UP05", + "UP050", "W", "W1", "W19", diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e7f22fb8ba993..c95c90571ff47 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.86" +channel = "1.88" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000000000..f3e454b618cda --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2024" +style_edition = "2024" diff --git a/scripts/add_rule.py b/scripts/add_rule.py index f285878670124..400c5ac6ec056 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -92,9 +92,9 @@ def main(*, name: str, prefix: str, code: str, linter: str) -> None: with (rules_dir / f"{rule_name_snake}.rs").open("w") as fp: fp.write( f"""\ -use ruff_diagnostics::Violation; -use ruff_macros::{{derive_message_formats, ViolationMetadata}}; +use ruff_macros::{{ViolationMetadata, derive_message_formats}}; +use crate::Violation; use crate::checkers::ast::Checker; /// ## What it does diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 1670ea1e767f2..12d65710c76c3 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.11.6" +version = "0.12.3" description = "" authors = ["Charles Marsh "] diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index dc45579ca02f1..f4ac1b5314f52 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -57,6 +57,7 @@ "incorrect-blank-line-after-class", "incorrect-blank-line-before-class", "indentation-with-invalid-multiple", + "indentation-with-invalid-multiple-comment", "line-too-long", "missing-trailing-comma", "missing-whitespace", @@ -111,7 +112,6 @@ # For some docs, Ruff is unable to parse the example code. KNOWN_PARSE_ERRORS = [ "blank-line-with-whitespace", - "indentation-with-invalid-multiple-comment", "indented-form-feed", "missing-newline-at-end-of-file", "mixed-spaces-and-tabs", diff --git a/scripts/generate_builtin_modules.py b/scripts/generate_builtin_modules.py index b1a193773153a..8f1171498fcce 100644 --- a/scripts/generate_builtin_modules.py +++ b/scripts/generate_builtin_modules.py @@ -70,7 +70,7 @@ def generate_module( /// modules. /// /// [builtin module]: https://docs.python.org/3/library/sys.html#sys.builtin_module_names - #[allow(clippy::unnested_or_patterns)] + #[expect(clippy::unnested_or_patterns)] pub fn is_builtin_module(minor_version: u8, module: &str) -> bool { matches!((minor_version, module), """, diff --git a/scripts/generate_known_standard_library.py b/scripts/generate_known_standard_library.py index 22117b60d4f99..84355af82a4f7 100644 --- a/scripts/generate_known_standard_library.py +++ b/scripts/generate_known_standard_library.py @@ -13,6 +13,7 @@ (3, 11), (3, 12), (3, 13), + (3, 14), ] with PATH.open("w") as f: diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index 54e224cd66df6..d78307b28e42c 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -54,7 +54,6 @@ class Section(NamedTuple): Section("Contributing", "contributing.md", generated=True), ] - LINK_REWRITES: dict[str, str] = { "https://docs.astral.sh/ruff/": "index.md", "https://docs.astral.sh/ruff/configuration/": "configuration.md", @@ -62,8 +61,8 @@ class Section(NamedTuple): "configuration.md#config-file-discovery" ), "https://docs.astral.sh/ruff/contributing/": "contributing.md", + "https://docs.astral.sh/ruff/editors": "editors/index.md", "https://docs.astral.sh/ruff/editors/setup": "editors/setup.md", - "https://docs.astral.sh/ruff/integrations/": "integrations.md", "https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8": ( "faq.md#how-does-ruffs-linter-compare-to-flake8" ), diff --git a/scripts/knot_benchmark/README.md b/scripts/knot_benchmark/README.md deleted file mode 100644 index 622d6da747873..0000000000000 --- a/scripts/knot_benchmark/README.md +++ /dev/null @@ -1,21 +0,0 @@ -## Getting started - -1. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/) - -- Unix: `curl -LsSf https://astral.sh/uv/install.sh | sh` -- Windows: `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` - -1. Build red_knot: `cargo build --bin red_knot --release` -1. `cd` into the benchmark directory: `cd scripts/knot_benchmark` -1. Run benchmarks: `uv run benchmark` - -## Known limitations - -Red Knot only implements a tiny fraction of Mypy's and Pyright's functionality, -so the benchmarks aren't in any way a fair comparison today. However, -they'll become more meaningful as we build out more type checking features in Red Knot. - -### Windows support - -The script should work on Windows, but we haven't tested it yet. -We do make use of `shlex` which has known limitations when using non-POSIX shells. diff --git a/scripts/mypy_primer.sh b/scripts/mypy_primer.sh new file mode 100755 index 0000000000000..e1014497a1bef --- /dev/null +++ b/scripts/mypy_primer.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -eu + +echo "Enabling mypy primer specific configuration overloads (see .github/mypy-primer-ty.toml)" +mkdir -p ~/.config/ty +cp .github/mypy-primer-ty.toml ~/.config/ty/ty.toml + +PRIMER_SELECTOR="$(paste -s -d'|' $PRIMER_SELECTOR)" + +echo "new commit" +git rev-list --format=%s --max-count=1 "$GITHUB_SHA" + +MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")" +git checkout -b base_commit "$MERGE_BASE" +echo "base commit" +git rev-list --format=%s --max-count=1 base_commit + +cd .. + +echo "Project selector: $PRIMER_SELECTOR" +# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs +uvx \ + --from="git+https://github.com/hauntsaninja/mypy_primer@59509d48de6da6aaa4e3a2f5e338769bc471f2d7" \ + mypy_primer \ + --repo ruff \ + --type-checker ty \ + --old base_commit \ + --new "$GITHUB_SHA" \ + --project-selector "/($PRIMER_SELECTOR)\$" \ + --output concise \ + --debug > $DIFF_FILE || [ $? -eq 1 ] + +# Output diff with ANSI color codes +cat $DIFF_FILE + +# Remove ANSI color codes before uploading +sed -ie 's/\x1b\[[0-9;]*m//g' $DIFF_FILE diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 475e14805e4a2..8b964fb7db441 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -2,7 +2,7 @@ name = "scripts" version = "0.0.1" dependencies = ["stdlibs"] -requires-python = ">=3.11" +requires-python = ">=3.12" [tool.black] line-length = 88 diff --git a/scripts/ty_benchmark/README.md b/scripts/ty_benchmark/README.md new file mode 100644 index 0000000000000..e55150d4f1a14 --- /dev/null +++ b/scripts/ty_benchmark/README.md @@ -0,0 +1,21 @@ +## Getting started + +1. [Install `uv`](https://docs.astral.sh/uv/getting-started/installation/) + +- Unix: `curl -LsSf https://astral.sh/uv/install.sh | sh` +- Windows: `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` + +1. Build ty: `cargo build --bin ty --release` +1. `cd` into the benchmark directory: `cd scripts/ty_benchmark` +1. Run benchmarks: `uv run benchmark` + +## Known limitations + +ty only implements a tiny fraction of Mypy's and Pyright's functionality, +so the benchmarks aren't in any way a fair comparison today. However, +they'll become more meaningful as we build out more type checking features in ty. + +### Windows support + +The script should work on Windows, but we haven't tested it yet. +We do make use of `shlex` which has known limitations when using non-POSIX shells. diff --git a/scripts/knot_benchmark/pyproject.toml b/scripts/ty_benchmark/pyproject.toml similarity index 79% rename from scripts/knot_benchmark/pyproject.toml rename to scripts/ty_benchmark/pyproject.toml index e1c5191232cf7..99040466fb1a9 100644 --- a/scripts/knot_benchmark/pyproject.toml +++ b/scripts/ty_benchmark/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "knot_benchmark" +name = "ty_benchmark" version = "0.0.1" -description = "Package for running end-to-end Red Knot benchmarks" +description = "Package for running end-to-end ty benchmarks" requires-python = ">=3.12" dependencies = ["mypy", "pyright"] diff --git a/scripts/knot_benchmark/src/benchmark/__init__.py b/scripts/ty_benchmark/src/benchmark/__init__.py similarity index 100% rename from scripts/knot_benchmark/src/benchmark/__init__.py rename to scripts/ty_benchmark/src/benchmark/__init__.py diff --git a/scripts/knot_benchmark/src/benchmark/cases.py b/scripts/ty_benchmark/src/benchmark/cases.py similarity index 94% rename from scripts/knot_benchmark/src/benchmark/cases.py rename to scripts/ty_benchmark/src/benchmark/cases.py index c95350dba59a6..9dd0012792b80 100644 --- a/scripts/knot_benchmark/src/benchmark/cases.py +++ b/scripts/ty_benchmark/src/benchmark/cases.py @@ -53,18 +53,18 @@ def warm_command(self, project: Project, venv: Venv) -> Command | None: return None -class Knot(Tool): +class Ty(Tool): path: Path name: str def __init__(self, *, path: Path | None = None): - self.name = str(path) or "knot" - self.path = path or ( - (Path(__file__) / "../../../../../target/release/red_knot").resolve() - ) + self.name = str(path) or "ty" + self.path = ( + path or (Path(__file__) / "../../../../../target/release/ty") + ).resolve() assert self.path.is_file(), ( - f"Red Knot not found at '{self.path}'. Run `cargo build --release --bin red_knot`." + f"ty not found at '{self.path}'. Run `cargo build --release --bin ty`." ) def cold_command(self, project: Project, venv: Venv) -> Command: @@ -73,7 +73,7 @@ def cold_command(self, project: Project, venv: Venv) -> Command: command.extend(["--python", str(venv.path)]) return Command( - name="knot", + name=self.name, command=command, ) diff --git a/scripts/knot_benchmark/src/benchmark/projects.py b/scripts/ty_benchmark/src/benchmark/projects.py similarity index 100% rename from scripts/knot_benchmark/src/benchmark/projects.py rename to scripts/ty_benchmark/src/benchmark/projects.py diff --git a/scripts/knot_benchmark/src/benchmark/run.py b/scripts/ty_benchmark/src/benchmark/run.py similarity index 88% rename from scripts/knot_benchmark/src/benchmark/run.py rename to scripts/ty_benchmark/src/benchmark/run.py index eab1449651733..2e8d0cdcfdbe7 100644 --- a/scripts/knot_benchmark/src/benchmark/run.py +++ b/scripts/ty_benchmark/src/benchmark/run.py @@ -8,7 +8,7 @@ from pathlib import Path from benchmark import Hyperfine -from benchmark.cases import Benchmark, Knot, Mypy, Pyright, Tool, Venv +from benchmark.cases import Benchmark, Mypy, Pyright, Tool, Ty, Venv from benchmark.projects import ALL as all_projects from benchmark.projects import DEFAULT as default_projects @@ -19,7 +19,7 @@ def main() -> None: """Run the benchmark.""" parser = argparse.ArgumentParser( - description="Benchmark knot against other packaging tools." + description="Benchmark ty against other packaging tools." ) parser.add_argument( "--verbose", "-v", action="store_true", help="Print verbose output." @@ -63,14 +63,14 @@ def main() -> None: action="store_true", ) parser.add_argument( - "--knot", - help="Whether to benchmark knot (assumes a red_knot binary exists at `./target/release/red_knot`).", + "--ty", + help="Whether to benchmark ty (assumes a ty binary exists at `./target/release/ty`).", action="store_true", ) parser.add_argument( - "--knot-path", + "--ty-path", type=Path, - help="Path(s) to the red_knot binary to benchmark.", + help="Path(s) to the ty binary to benchmark.", action="append", ) @@ -90,17 +90,17 @@ def main() -> None: if args.pyright: suites.append(Pyright()) - if args.knot: - suites.append(Knot()) + if args.ty: + suites.append(Ty()) - for path in args.knot_path or []: - suites.append(Knot(path=path)) + for path in args.ty_path or []: + suites.append(Ty(path=path)) if args.mypy: suites.append(Mypy()) # If no tools were specified, default to benchmarking all tools. - suites = suites or [Knot(), Pyright(), Mypy()] + suites = suites or [Ty(), Pyright(), Mypy()] # Determine the benchmarks to run, based on user input. benchmarks = ( diff --git a/scripts/knot_benchmark/uv.lock b/scripts/ty_benchmark/uv.lock similarity index 76% rename from scripts/knot_benchmark/uv.lock rename to scripts/ty_benchmark/uv.lock index 0fcb11197a1e9..1091d51dd904e 100644 --- a/scripts/knot_benchmark/uv.lock +++ b/scripts/ty_benchmark/uv.lock @@ -1,21 +1,7 @@ version = 1 +revision = 2 requires-python = ">=3.12" -[[package]] -name = "knot-benchmark" -version = "0.0.1" -source = { editable = "." } -dependencies = [ - { name = "mypy" }, - { name = "pyright" }, -] - -[package.metadata] -requires-dist = [ - { name = "mypy" }, - { name = "pyright" }, -] - [[package]] name = "mypy" version = "1.11.1" @@ -24,32 +10,32 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369, upload_time = "2024-07-30T22:38:50.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789 }, - { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696 }, - { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772 }, - { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190 }, - { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138 }, - { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257 }, + { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789, upload_time = "2024-07-30T22:37:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696, upload_time = "2024-07-30T22:38:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772, upload_time = "2024-07-30T22:37:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190, upload_time = "2024-07-30T22:37:31.244Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138, upload_time = "2024-07-30T22:37:19.849Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257, upload_time = "2024-07-30T22:37:40.567Z" }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload_time = "2023-02-04T12:11:27.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload_time = "2023-02-04T12:11:25.002Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload_time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload_time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -59,16 +45,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/f0/25b0db363d6888164adb7c828b877bbf2c30936955fb9513922ae03e70e4/pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf", size = 17484 } +sdist = { url = "https://files.pythonhosted.org/packages/49/f0/25b0db363d6888164adb7c828b877bbf2c30936955fb9513922ae03e70e4/pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf", size = 17484, upload_time = "2024-08-21T02:25:15.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/c9/89c40c4de44fe9463e77dddd0c4e2d2dd7a93e8ddc6858dfe7d5f75d263d/pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162", size = 18223 }, + { url = "https://files.pythonhosted.org/packages/34/c9/89c40c4de44fe9463e77dddd0c4e2d2dd7a93e8ddc6858dfe7d5f75d263d/pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162", size = 18223, upload_time = "2024-08-21T02:25:14.585Z" }, +] + +[[package]] +name = "ty-benchmark" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "mypy" }, + { name = "pyright" }, +] + +[package.metadata] +requires-dist = [ + { name = "mypy" }, + { name = "pyright" }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload_time = "2024-06-07T18:52:15.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload_time = "2024-06-07T18:52:13.582Z" }, ] diff --git a/scripts/update_schemastore.py b/scripts/update_schemastore.py index 841f97dabdf6c..5ec1ff9d3b1a8 100644 --- a/scripts/update_schemastore.py +++ b/scripts/update_schemastore.py @@ -1,8 +1,12 @@ """Update ruff.json in schemastore. -This script will clone astral-sh/schemastore, update the schema and push the changes +This script will clone `astral-sh/schemastore`, update the schema and push the changes to a new branch tagged with the ruff git hash. You should see a URL to create the PR to schemastore in the CLI. + +Usage: + + uv run --only-dev scripts/update_schemastore.py """ from __future__ import annotations @@ -14,11 +18,17 @@ from tempfile import TemporaryDirectory from typing import NamedTuple, assert_never -ruff_repo = "https://github.com/astral-sh/ruff" -root = Path( - check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip(), -) -ruff_json = Path("schemas/json/ruff.json") +# The remote URL for the `ruff` repository. +RUFF_REPO = "https://github.com/astral-sh/ruff" + +# The path to the root of the `ruff` repository. +RUFF_ROOT = Path(__file__).parent.parent + +# The path to the JSON schema in the `ruff` repository. +RUFF_SCHEMA = RUFF_ROOT / "ruff.schema.json" + +# The path to the JSON schema in the `schemastore` repository. +RUFF_JSON = Path("schemas/json/ruff.json") class SchemastoreRepos(NamedTuple): @@ -49,7 +59,7 @@ def schemastore_repos(self) -> SchemastoreRepos: def update_schemastore( schemastore_path: Path, schemastore_repos: SchemastoreRepos ) -> None: - if not schemastore_path.is_dir(): + if not (schemastore_path / ".git").is_dir(): check_call( ["git", "clone", schemastore_repos.fork, schemastore_path, "--depth=1"] ) @@ -78,13 +88,13 @@ def update_schemastore( ) # Run npm install - src = schemastore_path.joinpath("src") + src = schemastore_path / "src" check_call(["npm", "install"], cwd=schemastore_path) # Update the schema and format appropriately - schema = json.loads(root.joinpath("ruff.schema.json").read_text()) + schema = json.loads(RUFF_SCHEMA.read_text()) schema["$id"] = "https://json.schemastore.org/ruff.json" - src.joinpath(ruff_json).write_text( + (src / RUFF_JSON).write_text( json.dumps(dict(schema.items()), indent=2, ensure_ascii=False), ) check_call( @@ -93,7 +103,7 @@ def update_schemastore( "--plugin", "prettier-plugin-sort-json", "--write", - ruff_json, + RUFF_JSON, ], cwd=src, ) @@ -102,7 +112,7 @@ def update_schemastore( # https://stackoverflow.com/a/9393642/3549270 if check_output(["git", "status", "-s"], cwd=schemastore_path).strip(): # Schema has changed, commit and push - commit_url = f"{ruff_repo}/commit/{current_sha}" + commit_url = f"{RUFF_REPO}/commit/{current_sha}" commit_body = ( f"This updates ruff's JSON schema to [{current_sha}]({commit_url})" ) @@ -146,14 +156,12 @@ def determine_git_protocol(argv: list[str] | None = None) -> GitProtocol: def main() -> None: schemastore_repos = determine_git_protocol().schemastore_repos() - schemastore_existing = root.joinpath("schemastore") + schemastore_existing = RUFF_ROOT / "schemastore" if schemastore_existing.is_dir(): update_schemastore(schemastore_existing, schemastore_repos) else: - with TemporaryDirectory() as temp_dir: - update_schemastore( - Path(temp_dir).joinpath("schemastore"), schemastore_repos - ) + with TemporaryDirectory(prefix="ruff-schemastore-") as temp_dir: + update_schemastore(Path(temp_dir), schemastore_repos) if __name__ == "__main__": diff --git a/ty.schema.json b/ty.schema.json new file mode 100644 index 0000000000000..7579b18235c3b --- /dev/null +++ b/ty.schema.json @@ -0,0 +1,1005 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Options", + "type": "object", + "properties": { + "environment": { + "description": "Configures the type checking environment.", + "anyOf": [ + { + "$ref": "#/definitions/EnvironmentOptions" + }, + { + "type": "null" + } + ] + }, + "overrides": { + "description": "Override configurations for specific file patterns.\n\nEach override specifies include/exclude patterns and rule configurations that apply to matching files. Multiple overrides can match the same file, with later overrides taking precedence.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/OverrideOptions" + } + }, + "rules": { + "description": "Configures the enabled rules and their severity.\n\nSee [the rules documentation](https://ty.dev/rules) for a list of all available rules.\n\nValid severities are:\n\n* `ignore`: Disable the rule. * `warn`: Enable the rule and create a warning diagnostic. * `error`: Enable the rule and create an error diagnostic. ty will exit with a non-zero code if any error diagnostics are emitted.", + "anyOf": [ + { + "$ref": "#/definitions/Rules" + }, + { + "type": "null" + } + ] + }, + "src": { + "anyOf": [ + { + "$ref": "#/definitions/SrcOptions" + }, + { + "type": "null" + } + ] + }, + "terminal": { + "anyOf": [ + { + "$ref": "#/definitions/TerminalOptions" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "DiagnosticFormat": { + "description": "The diagnostic output format.", + "oneOf": [ + { + "description": "The default full mode will print \"pretty\" diagnostics.\n\nThat is, color will be used when printing to a `tty`. Moreover, diagnostic messages may include additional context and annotations on the input to help understand the message.", + "type": "string", + "enum": [ + "full" + ] + }, + { + "description": "Print diagnostics in a concise mode.\n\nThis will guarantee that each diagnostic is printed on a single line. Only the most important or primary aspects of the diagnostic are included. Contextual information is dropped.\n\nThis may use color when printing to a `tty`.", + "type": "string", + "enum": [ + "concise" + ] + } + ] + }, + "EnvironmentOptions": { + "type": "object", + "properties": { + "extra-paths": { + "description": "List of user-provided paths that should take first priority in the module resolution. Examples in other type checkers are mypy's `MYPYPATH` environment variable, or pyright's `stubPath` configuration setting.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "python": { + "description": "Path to the Python installation from which ty resolves type information and third-party dependencies.\n\nty will search in the path's `site-packages` directories for type information and third-party imports.\n\nThis option is commonly used to specify the path to a virtual environment.", + "type": [ + "string", + "null" + ] + }, + "python-platform": { + "description": "Specifies the target platform that will be used to analyze the source code. If specified, ty will understand conditions based on comparisons with `sys.platform`, such as are commonly found in typeshed to reflect the differing contents of the standard library across platforms. If `all` is specified, ty will assume that the source code can run on any platform.\n\nIf no platform is specified, ty will use the current platform: - `win32` for Windows - `darwin` for macOS - `android` for Android - `ios` for iOS - `linux` for everything else", + "anyOf": [ + { + "$ref": "#/definitions/PythonPlatform" + }, + { + "type": "null" + } + ] + }, + "python-version": { + "description": "Specifies the version of Python that will be used to analyze the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. `\"3.0\"` or `\"3.6\"`). If a version is provided, ty will generate errors if the source code makes use of language features that are not supported in that version.\n\nIf a version is not specified, ty will try the following techniques in order of preference to determine a value: 1. Check for the `project.requires-python` setting in a `pyproject.toml` file and use the minimum version from the specified range 2. Check for an activated or configured Python environment and attempt to infer the Python version of that environment 3. Fall back to the default value (see below)\n\nFor some language features, ty can also understand conditionals based on comparisons with `sys.version_info`. These are commonly found in typeshed, for example, to reflect the differing contents of the standard library across Python versions.", + "anyOf": [ + { + "$ref": "#/definitions/PythonVersion" + }, + { + "type": "null" + } + ] + }, + "root": { + "description": "The root paths of the project, used for finding first-party modules.\n\nAccepts a list of directory paths searched in priority order (first has highest priority).\n\nIf left unspecified, ty will try to detect common project layouts and initialize `root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `.//` directory exists, include `.` and `./` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), it will also be included in the first party search path.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "typeshed": { + "description": "Optional path to a \"typeshed\" directory on disk for us to use for standard-library types. If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, bundled as a zip file in the binary", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "Level": { + "oneOf": [ + { + "title": "Ignore", + "description": "The lint is disabled and should not run.", + "type": "string", + "enum": [ + "ignore" + ] + }, + { + "title": "Warn", + "description": "The lint is enabled and diagnostic should have a warning severity.", + "type": "string", + "enum": [ + "warn" + ] + }, + { + "title": "Error", + "description": "The lint is enabled and diagnostics have an error severity.", + "type": "string", + "enum": [ + "error" + ] + } + ] + }, + "OverrideOptions": { + "type": "object", + "properties": { + "exclude": { + "description": "A list of file and directory patterns to exclude from this override.\n\nPatterns follow a syntax similar to `.gitignore`. Exclude patterns take precedence over include patterns within the same override.\n\nIf not specified, defaults to `[]` (excludes no files).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "include": { + "description": "A list of file and directory patterns to include for this override.\n\nThe `include` option follows a similar syntax to `.gitignore` but reversed: Including a file or directory will make it so that it (and its contents) are affected by this override.\n\nIf not specified, defaults to `[\"**\"]` (matches all files).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "rules": { + "description": "Rule overrides for files matching the include/exclude patterns.\n\nThese rules will be merged with the global rules, with override rules taking precedence for matching files. You can set rules to different severity levels or disable them entirely.", + "anyOf": [ + { + "$ref": "#/definitions/Rules" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "PythonPlatform": { + "description": "The target platform to assume when resolving types.\n", + "anyOf": [ + { + "type": "string" + }, + { + "description": "Do not make any assumptions about the target platform.", + "const": "all" + }, + { + "description": "Darwin", + "const": "darwin" + }, + { + "description": "Linux", + "const": "linux" + }, + { + "description": "Windows", + "const": "win32" + } + ] + }, + "PythonVersion": { + "anyOf": [ + { + "type": "string", + "pattern": "^\\d+\\.\\d+$" + }, + { + "description": "Python 3.7", + "const": "3.7" + }, + { + "description": "Python 3.8", + "const": "3.8" + }, + { + "description": "Python 3.9", + "const": "3.9" + }, + { + "description": "Python 3.10", + "const": "3.10" + }, + { + "description": "Python 3.11", + "const": "3.11" + }, + { + "description": "Python 3.12", + "const": "3.12" + }, + { + "description": "Python 3.13", + "const": "3.13" + }, + { + "description": "Python 3.14", + "const": "3.14" + } + ] + }, + "Rules": { + "type": "object", + "properties": { + "byte-string-type-annotation": { + "title": "detects byte strings in type annotation positions", + "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "call-non-callable": { + "title": "detects calls to non-callable objects", + "description": "## What it does\nChecks for calls to non-callable objects.\n\n## Why is this bad?\nCalling a non-callable object will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4() # TypeError: 'int' object is not callable\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "conflicting-argument-forms": { + "title": "detects when an argument is used as both a value and a type form in a call", + "description": "## What it does\nChecks whether an argument is used as both a value and a type form in a call.\n\n## Why is this bad?\nSuch calls have confusing semantics and often indicate a logic error.\n\n## Examples\n```python\nfrom typing import reveal_type\nfrom ty_extensions import is_singleton\n\nif flag:\n f = repr # Expects a value\nelse:\n f = is_singleton # Expects a type form\n\nf(int) # error\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "conflicting-declarations": { + "title": "detects conflicting declarations", + "description": "## What it does\nChecks whether a variable has been declared as two conflicting types.\n\n## Why is this bad\nA variable with two conflicting declarations likely indicates a mistake.\nMoreover, it could lead to incorrect or ill-defined type inference for\nother code that relies on these variables.\n\n## Examples\n```python\nif b:\n a: int\nelse:\n a: str\n\na = 1\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "conflicting-metaclass": { + "title": "detects conflicting metaclasses", + "description": "## What it does\nChecks for class definitions where the metaclass of the class\nbeing created would not be a subclass of the metaclasses of\nall the class's bases.\n\n## Why is it bad?\nSuch a class definition raises a `TypeError` at runtime.\n\n## Examples\n```python\nclass M1(type): ...\nclass M2(type): ...\nclass A(metaclass=M1): ...\nclass B(metaclass=M2): ...\n\n# TypeError: metaclass conflict\nclass C(A, B): ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "cyclic-class-definition": { + "title": "detects cyclic class definitions", + "description": "## What it does\nChecks for class definitions in stub files that inherit\n(directly or indirectly) from themselves.\n\n## Why is it bad?\nAlthough forward references are natively supported in stub files,\ninheritance cycles are still disallowed, as it is impossible to\nresolve a consistent [method resolution order] for a class that\ninherits from itself.\n\n## Examples\n```python\n# foo.pyi\nclass A(B): ...\nclass B(A): ...\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "division-by-zero": { + "title": "detects division by zero", + "description": "## What it does\nIt detects division by zero.\n\n## Why is this bad?\nDividing by zero raises a `ZeroDivisionError` at runtime.\n\n## Examples\n```python\n5 / 0\n```", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "duplicate-base": { + "title": "detects class definitions with duplicate bases", + "description": "## What it does\nChecks for class definitions with duplicate bases.\n\n## Why is this bad?\nClass definitions with duplicate bases raise `TypeError` at runtime.\n\n## Examples\n```python\nclass A: ...\n\n# TypeError: duplicate base class\nclass B(A, A): ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "duplicate-kw-only": { + "title": "detects dataclass definitions with more than one usage of `KW_ONLY`", + "description": "## What it does\nChecks for dataclass definitions with more than one field\nannotated with `KW_ONLY`.\n\n## Why is this bad?\n`dataclasses.KW_ONLY` is a special marker used to\nemulate the `*` syntax in normal signatures.\nIt can only be used once per dataclass.\n\nAttempting to annotate two different fields with\nit will lead to a runtime error.\n\n## Examples\n```python\nfrom dataclasses import dataclass, KW_ONLY\n\n@dataclass\nclass A: # Crash at runtime\n b: int\n _1: KW_ONLY\n c: str\n _2: KW_ONLY\n d: bytes\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "escape-character-in-forward-annotation": { + "title": "detects forward type annotations with escape characters", + "description": "TODO #14889", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "fstring-type-annotation": { + "title": "detects F-strings in type annotation positions", + "description": "## What it does\nChecks for f-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use f-string notation.\n\n## Examples\n```python\ndef test(): -> f\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "implicit-concatenated-string-type-annotation": { + "title": "detects implicit concatenated strings in type annotations", + "description": "## What it does\nChecks for implicit concatenated strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use implicit concatenated strings.\n\n## Examples\n```python\ndef test(): -> \"Literal[\" \"5\" \"]\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"Literal[5]\":\n ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "inconsistent-mro": { + "title": "detects class definitions with an inconsistent MRO", + "description": "## What it does\nChecks for classes with an inconsistent [method resolution order] (MRO).\n\n## Why is this bad?\nClasses with an inconsistent MRO will raise a `TypeError` at runtime.\n\n## Examples\n```python\nclass A: ...\nclass B(A): ...\n\n# TypeError: Cannot create a consistent method resolution order\nclass C(A, B): ...\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "index-out-of-bounds": { + "title": "detects index out of bounds errors", + "description": "## What it does\nChecks for attempts to use an out of bounds index to get an item from\na container.\n\n## Why is this bad?\nUsing an out of bounds index will raise an `IndexError` at runtime.\n\n## Examples\n```python\nt = (0, 1, 2)\nt[3] # IndexError: tuple index out of range\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "instance-layout-conflict": { + "title": "detects class definitions that raise `TypeError` due to instance layout conflict", + "description": "## What it does\nChecks for classes definitions which will fail at runtime due to\n\"instance memory layout conflicts\".\n\nThis error is usually caused by attempting to combine multiple classes\nthat define non-empty `__slots__` in a class's [Method Resolution Order]\n(MRO), or by attempting to combine multiple builtin classes in a class's\nMRO.\n\n## Why is this bad?\nInheriting from bases with conflicting instance memory layouts\nwill lead to a `TypeError` at runtime.\n\nAn instance memory layout conflict occurs when CPython cannot determine\nthe memory layout instances of a class should have, because the instance\nmemory layout of one of its bases conflicts with the instance memory layout\nof one or more of its other bases.\n\nFor example, if a Python class defines non-empty `__slots__`, this will\nimpact the memory layout of instances of that class. Multiple inheritance\nfrom more than one different class defining non-empty `__slots__` is not\nallowed:\n\n```python\nclass A:\n __slots__ = (\"a\", \"b\")\n\nclass B:\n __slots__ = (\"a\", \"b\") # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\nAn instance layout conflict can also be caused by attempting to use\nmultiple inheritance with two builtin classes, due to the way that these\nclasses are implemented in a CPython C extension:\n\n```python\nclass A(int, float): ... # TypeError: multiple bases have instance lay-out conflict\n```\n\nNote that pure-Python classes with no `__slots__`, or pure-Python classes\nwith empty `__slots__`, are always compatible:\n\n```python\nclass A: ...\nclass B:\n __slots__ = ()\nclass C:\n __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\n## Known problems\nClasses that have \"dynamic\" definitions of `__slots__` (definitions do not consist\nof string literals, or tuples of string literals) are not currently considered solid\nbases by ty.\n\nAdditionally, this check is not exhaustive: many C extensions (including several in\nthe standard library) define classes that use extended memory layouts and thus cannot\ncoexist in a single MRO. Since it is currently not possible to represent this fact in\nstub files, having a full knowledge of these classes is also impossible. When it comes\nto classes that do not define `__slots__` at the Python level, therefore, ty, currently\nonly hard-codes a number of cases where it knows that a class will produce instances with\nan atypical memory layout.\n\n## Further reading\n- [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)\n- [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)\n\n[Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-argument-type": { + "title": "detects call arguments whose type is not assignable to the corresponding typed parameter", + "description": "## What it does\nDetects call arguments whose type is not assignable to the corresponding typed parameter.\n\n## Why is this bad?\nPassing an argument of a type the function (or callable object) does not accept violates\nthe expectations of the function author and may cause unexpected runtime errors within the\nbody of the function.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc(\"foo\") # error: [invalid-argument-type]\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-assignment": { + "title": "detects invalid assignments", + "description": "## What it does\nChecks for assignments where the type of the value\nis not [assignable to] the type of the assignee.\n\n## Why is this bad?\nSuch assignments break the rules of the type system and\nweaken a type checker's ability to accurately reason about your code.\n\n## Examples\n```python\na: int = ''\n```\n\n[assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-attribute-access": { + "title": "Invalid attribute access", + "description": "## What it does\nChecks for assignments to class variables from instances\nand assignments to instance variables from its class.\n\n## Why is this bad?\nIncorrect assignments break the rules of the type system and\nweaken a type checker's ability to accurately reason about your code.\n\n## Examples\n```python\nclass C:\n class_var: ClassVar[int] = 1\n instance_var: int\n\nC.class_var = 3 # okay\nC().class_var = 3 # error: Cannot assign to class variable\n\nC().instance_var = 3 # okay\nC.instance_var = 3 # error: Cannot assign to instance variable\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-base": { + "title": "detects class bases that will cause the class definition to raise an exception at runtime", + "description": "## What it does\nChecks for class definitions that have bases which are not instances of `type`.\n\n## Why is this bad?\nClass definitions with bases like this will lead to `TypeError` being raised at runtime.\n\n## Examples\n```python\nclass A(42): ... # error: [invalid-base]\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-context-manager": { + "title": "detects expressions used in with statements that don't implement the context manager protocol", + "description": "## What it does\nChecks for expressions used in `with` statements\nthat do not implement the context manager protocol.\n\n## Why is this bad?\nSuch a statement will raise `TypeError` at runtime.\n\n## Examples\n```python\n# TypeError: 'int' object does not support the context manager protocol\nwith 1:\n print(2)\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-declaration": { + "title": "detects invalid declarations", + "description": "## What it does\nChecks for declarations where the inferred type of an existing symbol\nis not [assignable to] its post-hoc declared type.\n\n## Why is this bad?\nSuch declarations break the rules of the type system and\nweaken a type checker's ability to accurately reason about your code.\n\n## Examples\n```python\na = 1\na: str\n```\n\n[assignable to]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-exception-caught": { + "title": "detects exception handlers that catch classes that do not inherit from `BaseException`", + "description": "## What it does\nChecks for exception handlers that catch non-exception classes.\n\n## Why is this bad?\nCatching classes that do not inherit from `BaseException` will raise a TypeError at runtime.\n\n## Example\n```python\ntry:\n 1 / 0\nexcept 1:\n ...\n```\n\nUse instead:\n```python\ntry:\n 1 / 0\nexcept ZeroDivisionError:\n ...\n```\n\n## References\n- [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)\n\n## Ruff rule\n This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-generic-class": { + "title": "detects invalid generic classes", + "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-ignore-comment": { + "title": "detects ignore comments that use invalid syntax", + "description": "## What it does\nChecks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-legacy-type-variable": { + "title": "detects invalid legacy type variables", + "description": "## What it does\nChecks for the creation of invalid legacy `TypeVar`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a legacy `TypeVar`.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar(\"T\") # okay\nQ = TypeVar(\"S\") # error: TypeVar name must match the variable it's assigned to\nT = TypeVar(\"T\") # error: TypeVars should not be redefined\n\n# error: TypeVar must be immediately assigned to a variable\ndef f(t: TypeVar(\"U\")): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-metaclass": { + "title": "detects invalid `metaclass=` arguments", + "description": "## What it does\nChecks for arguments to `metaclass=` that are invalid.\n\n## Why is this bad?\nPython allows arbitrary expressions to be used as the argument to `metaclass=`.\nThese expressions, however, need to be callable and accept the same arguments\nas `type.__new__`.\n\n## Example\n\n```python\ndef f(): ...\n\n# TypeError: f() takes 0 positional arguments but 3 were given\nclass B(metaclass=f): ...\n```\n\n## References\n- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-overload": { + "title": "detects invalid `@overload` usages", + "description": "## What it does\nChecks for various invalid `@overload` usages.\n\n## Why is this bad?\nThe `@overload` decorator is used to define functions and methods that accepts different\ncombinations of arguments and return different types based on the arguments passed. This is\nmainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type\nchecker may not be able to provide correct type information.\n\n## Example\n\nDefining only one overload:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo(x: int) -> int: ...\ndef foo(x: int | None) -> int | None:\n return x\n```\n\nOr, not providing an implementation for the overloaded definition:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo() -> None: ...\n@overload\ndef foo(x: int) -> int: ...\n```\n\n## References\n- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-parameter-default": { + "title": "detects default values that can't be assigned to the parameter's annotated type", + "description": "## What it does\nChecks for default values that can't be\nassigned to the parameter's annotated type.\n\n## Why is this bad?\nThis breaks the rules of the type system and\nweakens a type checker's ability to accurately reason about your code.\n\n## Examples\n```python\ndef f(a: int = ''): ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-protocol": { + "title": "detects invalid protocol class definitions", + "description": "## What it does\nChecks for invalidly defined protocol classes.\n\n## Why is this bad?\nAn invalidly defined protocol class may lead to the type checker inferring\nunexpected things. It may also lead to `TypeError`s at runtime.\n\n## Examples\nA `Protocol` class cannot inherit from a non-`Protocol` class;\nthis raises a `TypeError` at runtime:\n\n```pycon\n>>> from typing import Protocol\n>>> class Foo(int, Protocol): ...\n...\nTraceback (most recent call last):\n File \"\", line 1, in \n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got \n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-raise": { + "title": "detects `raise` statements that raise invalid exceptions or use invalid causes", + "description": "Checks for `raise` statements that raise non-exceptions or use invalid\ncauses for their raised exceptions.\n\n## Why is this bad?\nOnly subclasses or instances of `BaseException` can be raised.\nFor an exception's cause, the same rules apply, except that `None` is also\npermitted. Violating these rules results in a `TypeError` at runtime.\n\n## Examples\n```python\ndef f():\n try:\n something()\n except NameError:\n raise \"oops!\" from f\n\ndef g():\n raise NotImplemented from 42\n```\n\nUse instead:\n```python\ndef f():\n try:\n something()\n except NameError as e:\n raise RuntimeError(\"oops!\") from e\n\ndef g():\n raise NotImplementedError from None\n```\n\n## References\n- [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-return-type": { + "title": "detects returned values that can't be assigned to the function's annotated return type", + "description": "## What it does\nDetects returned values that can't be assigned to the function's annotated return type.\n\n## Why is this bad?\nReturning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function.\n\n## Examples\n```python\ndef func() -> int:\n return \"a\" # error: [invalid-return-type]\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-super-argument": { + "title": "detects invalid arguments for `super()`", + "description": "## What it does\nDetects `super()` calls where:\n- the first argument is not a valid class literal, or\n- the second argument is not an instance or subclass of the first argument.\n\n## Why is this bad?\n`super(type, obj)` expects:\n- the first argument to be a class,\n- and the second argument to satisfy one of the following:\n - `isinstance(obj, type)` is `True`\n - `issubclass(obj, type)` is `True`\n\nViolating this relationship will raise a `TypeError` at runtime.\n\n## Examples\n```python\nclass A:\n ...\nclass B(A):\n ...\n\nsuper(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)`\n\nsuper(A(), B()) # error: `A()` is not a class\n\nsuper(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)`\nsuper(B, A) # error: `A` does not satisfy `issubclass(A, B)`\n```\n\n## References\n- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-syntax-in-forward-annotation": { + "title": "detects invalid syntax in forward annotations", + "description": "TODO #14889", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-alias-type": { + "title": "detects invalid TypeAliasType definitions", + "description": "## What it does\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n```python\nfrom typing import TypeAliasType\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\nNewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-checking-constant": { + "title": "detects invalid `TYPE_CHECKING` constant assignments", + "description": "## What it does\nChecks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an\nannotation not assignable from `bool`.\n\n## Why is this bad?\nThe name `TYPE_CHECKING` is reserved for a flag that can be used to provide conditional\ncode seen only by the type checker, and not at runtime. Normally this flag is imported from\n`typing` or `typing_extensions`, but it can also be defined locally. If defined locally, it\nmust be assigned the value `False` at runtime; the type checker will consider its value to\nbe `True`. If annotated, it must be annotated as a type that can accept `bool` values.\n\n## Examples\n```python\nTYPE_CHECKING: str\nTYPE_CHECKING = ''\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-form": { + "title": "detects invalid type forms", + "description": "## What it does\nChecks for expressions that are used as [type expressions]\nbut cannot validly be interpreted as such.\n\n## Why is this bad?\nSuch expressions cannot be understood by ty.\nIn some cases, they might raise errors at runtime.\n\n## Examples\n```python\nfrom typing import Annotated\n\na: type[1] # `1` is not a type\nb: Annotated[int] # `Annotated` expects at least two arguments\n```\n[type expressions]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-guard-call": { + "title": "detects type guard function calls that has no narrowing effect", + "description": "## What it does\nChecks for type guard function calls without a valid target.\n\n## Why is this bad?\nThe first non-keyword non-variadic argument to a type guard function\nis its target and must map to a symbol.\n\nStarred (`is_str(*a)`), literal (`is_str(42)`) and other non-symbol-like\nexpressions are invalid as narrowing targets.\n\n## Examples\n```python\nfrom typing import TypeIs\n\ndef f(v: object) -> TypeIs[int]: ...\n\nf() # Error\nf(*a) # Error\nf(10) # Error\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-guard-definition": { + "title": "detects malformed type guard functions", + "description": "## What it does\nChecks for type guard functions without\na first non-self-like non-keyword-only non-variadic parameter.\n\n## Why is this bad?\nType narrowing functions must accept at least one positional argument\n(non-static methods must accept another in addition to `self`/`cls`).\n\nExtra parameters/arguments are allowed but do not affect narrowing.\n\n## Examples\n```python\nfrom typing import TypeIs\n\ndef f() -> TypeIs[int]: ... # Error, no parameter\ndef f(*, v: object) -> TypeIs[int]: ... # Error, no positional arguments allowed\ndef f(*args: object) -> TypeIs[int]: ... # Error, expect variadic arguments\nclass C:\n def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "invalid-type-variable-constraints": { + "title": "detects invalid type variable constraints", + "description": "## What it does\nChecks for constrained [type variables] with only one constraint.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "missing-argument": { + "title": "detects missing required arguments in a call", + "description": "## What it does\nChecks for missing required arguments in a call.\n\n## Why is this bad?\nFailing to provide a required argument will raise a `TypeError` at runtime.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc() # TypeError: func() missing 1 required positional argument: 'x'\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "no-matching-overload": { + "title": "detects calls that do not match any overload", + "description": "## What it does\nChecks for calls to an overloaded function that do not match any of the overloads.\n\n## Why is this bad?\nFailing to provide the correct arguments to one of the overloads will raise a `TypeError`\nat runtime.\n\n## Examples\n```python\n@overload\ndef func(x: int): ...\n@overload\ndef func(x: bool): ...\nfunc(\"string\") # error: [no-matching-overload]\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "non-subscriptable": { + "title": "detects subscripting objects that do not support subscripting", + "description": "## What it does\nChecks for subscripting objects that do not support subscripting.\n\n## Why is this bad?\nSubscripting an object that does not support it will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4[1] # TypeError: 'int' object is not subscriptable\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "not-iterable": { + "title": "detects iteration over an object that is not iterable", + "description": "## What it does\nChecks for objects that are not iterable but are used in a context that requires them to be.\n\n## Why is this bad?\nIterating over an object that is not iterable will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\nfor i in 34: # TypeError: 'int' object is not iterable\n pass\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "parameter-already-assigned": { + "title": "detects multiple arguments for the same parameter", + "description": "## What it does\nChecks for calls which provide more than one argument for a single parameter.\n\n## Why is this bad?\nProviding multiple values for a single parameter will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\ndef f(x: int) -> int: ...\n\nf(1, x=2) # Error raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "possibly-unbound-attribute": { + "title": "detects references to possibly unbound attributes", + "description": "## What it does\nChecks for possibly unbound attributes.\n\n## Why is this bad?\nAttempting to access an unbound attribute will raise an `AttributeError` at runtime.\n\n## Examples\n```python\nclass A:\n if b:\n c = 0\n\nA.c # AttributeError: type object 'A' has no attribute 'c'\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "possibly-unbound-implicit-call": { + "title": "detects implicit calls to possibly unbound methods", + "description": "## What it does\nChecks for implicit calls to possibly unbound methods.\n\n## Why is this bad?\nExpressions such as `x[y]` and `x * y` call methods\nunder the hood (`__getitem__` and `__mul__` respectively).\nCalling an unbound method will raise an `AttributeError` at runtime.\n\n## Examples\n```python\nimport datetime\n\nclass A:\n if datetime.date.today().weekday() != 6:\n def __getitem__(self, v): ...\n\nA()[0] # TypeError: 'A' object is not subscriptable\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "possibly-unbound-import": { + "title": "detects possibly unbound imports", + "description": "## What it does\nChecks for imports of symbols that may be unbound.\n\n## Why is this bad?\nImporting an unbound module or name will raise a `ModuleNotFoundError`\nor `ImportError` at runtime.\n\n## Examples\n```python\n# module.py\nimport datetime\n\nif datetime.date.today().weekday() != 6:\n a = 1\n\n# main.py\nfrom module import a # ImportError: cannot import name 'a' from 'module'\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "possibly-unresolved-reference": { + "title": "detects references to possibly undefined names", + "description": "## What it does\nChecks for references to names that are possibly not defined.\n\n## Why is this bad?\nUsing an undefined variable will raise a `NameError` at runtime.\n\n## Example\n\n```python\nfor i in range(0):\n x = i\n\nprint(x) # NameError: name 'x' is not defined\n```", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "raw-string-type-annotation": { + "title": "detects raw strings in type annotation positions", + "description": "## What it does\nChecks for raw-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use raw-string notation.\n\n## Examples\n```python\ndef test(): -> r\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "redundant-cast": { + "title": "detects redundant `cast` calls", + "description": "## What it does\nDetects redundant `cast` calls where the value already has the target type.\n\n## Why is this bad?\nThese casts have no effect and can be removed.\n\n## Example\n```python\ndef f() -> int:\n return 10\n\ncast(int, f()) # Redundant\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "static-assert-error": { + "title": "Failed static assertion", + "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Why is this bad?\nA `static_assert` call represents an explicit request from the user\nfor the type checker to emit an error if the argument cannot be verified\nto evaluate to `True` in a boolean context.\n\n## Examples\n```python\nfrom ty_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "subclass-of-final-class": { + "title": "detects subclasses of final classes", + "description": "## What it does\nChecks for classes that subclass final classes.\n\n## Why is this bad?\nDecorating a class with `@final` declares to the type checker that it should not be subclassed.\n\n## Example\n\n```python\nfrom typing import final\n\n@final\nclass A: ...\nclass B(A): ... # Error raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "too-many-positional-arguments": { + "title": "detects calls passing too many positional arguments", + "description": "## What it does\nChecks for calls that pass more positional arguments than the callable can accept.\n\n## Why is this bad?\nPassing too many positional arguments will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(): ...\n\nf(\"foo\") # Error raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "type-assertion-failure": { + "title": "detects failed type assertions", + "description": "## What it does\nChecks for `assert_type()` and `assert_never()` calls where the actual type\nis not the same as the asserted type.\n\n## Why is this bad?\n`assert_type()` allows confirming the inferred type of a certain value.\n\n## Example\n\n```python\ndef _(x: int):\n assert_type(x, int) # fine\n assert_type(x, str) # error: Actual type does not match asserted type\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unavailable-implicit-super-arguments": { + "title": "detects invalid `super()` calls where implicit arguments are unavailable.", + "description": "## What it does\nDetects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable.\n\n## Why is this bad?\nWhen `super()` is used without arguments, Python tries to find two things:\nthe nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls).\nIf either of these is missing, the call will fail at runtime with a `RuntimeError`.\n\n## Examples\n```python\nsuper() # error: no enclosing class or function found\n\ndef func():\n super() # error: no enclosing class or first argument exists\n\nclass A:\n f = super() # error: no enclosing function to provide the first argument\n\n def method(self):\n def nested():\n super() # error: first argument does not exist in this nested function\n\n lambda: super() # error: first argument does not exist in this lambda\n\n (super() for _ in range(10)) # error: argument is not available in generator expression\n\n super() # okay! both enclosing class and first argument are available\n```\n\n## References\n- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "undefined-reveal": { + "title": "detects usages of `reveal_type` without importing it", + "description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\n```python\nreveal_type(1) # NameError: name 'reveal_type' is not defined\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unknown-argument": { + "title": "detects unknown keyword arguments in calls", + "description": "## What it does\nChecks for keyword arguments in calls that don't match any parameter of the callable.\n\n## Why is this bad?\nProviding an unknown keyword argument will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(x: int) -> int: ...\n\nf(x=1, y=2) # Error raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unknown-rule": { + "title": "detects `ty: ignore` comments that reference unknown rules", + "description": "## What it does\nChecks for `ty: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `ty: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # ty: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # ty: ignore[division-by-zero]\n```", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unresolved-attribute": { + "title": "detects references to unresolved attributes", + "description": "## What it does\nChecks for unresolved attributes.\n\n## Why is this bad?\nAccessing an unbound attribute will raise an `AttributeError` at runtime.\nAn unresolved attribute is not guaranteed to exist from the type alone,\nso this could also indicate that the object is not of the type that the user expects.\n\n## Examples\n```python\nclass A: ...\n\nA().foo # AttributeError: 'A' object has no attribute 'foo'\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unresolved-import": { + "title": "detects unresolved imports", + "description": "## What it does\nChecks for import statements for which the module cannot be resolved.\n\n## Why is this bad?\nImporting a module that cannot be resolved will raise a `ModuleNotFoundError`\nat runtime.\n\n## Examples\n```python\nimport foo # ModuleNotFoundError: No module named 'foo'\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unresolved-reference": { + "title": "detects references to names that are not defined", + "description": "## What it does\nChecks for references to names that are not defined.\n\n## Why is this bad?\nUsing an undefined variable will raise a `NameError` at runtime.\n\n## Example\n\n```python\nprint(x) # NameError: name 'x' is not defined\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unsupported-base": { + "title": "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", + "description": "## What it does\nChecks for class definitions that have bases which are unsupported by ty.\n\n## Why is this bad?\nIf a class has a base that is an instance of a complex type such as a union type,\nty will not be able to resolve the [method resolution order] (MRO) for the class.\nThis will lead to an inferior understanding of your codebase and unpredictable\ntype-checking behavior.\n\n## Examples\n```python\nimport datetime\n\nclass A: ...\nclass B: ...\n\nif datetime.date.today().weekday() != 6:\n C = A\nelse:\n C = B\n\nclass D(C): ... # error: [unsupported-base]\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", + "default": "warn", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unsupported-bool-conversion": { + "title": "detects boolean conversion where the object incorrectly implements `__bool__`", + "description": "## What it does\nChecks for bool conversions where the object doesn't correctly implement `__bool__`.\n\n## Why is this bad?\nIf an exception is raised when you attempt to evaluate the truthiness of an object,\nusing the object in a boolean context will fail at runtime.\n\n## Examples\n\n```python\nclass NotBoolable:\n __bool__ = None\n\nb1 = NotBoolable()\nb2 = NotBoolable()\n\nif b1: # exception raised here\n pass\n\nb1 and b2 # exception raised here\nnot b1 # exception raised here\nb1 < b2 < b1 # exception raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unsupported-operator": { + "title": "detects binary, unary, or comparison expressions where the operands don't support the operator", + "description": "## What it does\nChecks for binary expressions, comparisons, and unary expressions where\nthe operands don't support the operator.\n\n## Why is this bad?\nAttempting to use an unsupported operator will raise a `TypeError` at\nruntime.\n\n## Examples\n```python\nclass A: ...\n\nA() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unused-ignore-comment": { + "title": "detects unused `type: ignore` comments", + "description": "## What it does\nChecks for `type: ignore` or `ty: ignore` directives that are no longer applicable.\n\n## Why is this bad?\nA `type: ignore` directive that no longer matches any diagnostic violations is likely\nincluded by mistake, and should be removed to avoid confusion.\n\n## Examples\n```py\na = 20 / 2 # ty: ignore[division-by-zero]\n```\n\nUse instead:\n\n```py\na = 20 / 2\n```", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "zero-stepsize-in-slice": { + "title": "detects a slice step size of zero", + "description": "## What it does\nChecks for step size 0 in slices.\n\n## Why is this bad?\nA slice with a step size of zero will raise a `ValueError` at runtime.\n\n## Examples\n```python\nl = list(range(10))\nl[1:10:0] # ValueError: slice step cannot be zero\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + } + }, + "additionalProperties": { + "$ref": "#/definitions/Level" + } + }, + "SrcOptions": { + "type": "object", + "properties": { + "exclude": { + "description": "A list of file and directory patterns to exclude from type checking.\n\nPatterns follow a syntax similar to `.gitignore`:\n\n- `./src/` matches only a directory - `./src` matches both files and directories - `src` matches files or directories named `src` - `*` matches any (possibly empty) sequence of characters (except `/`). - `**` matches zero or more path components. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. A sequence of more than two consecutive `*` characters is also invalid. - `?` matches any single character except `/` - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid. - `!pattern` negates a pattern (undoes the exclusion of files that would otherwise be excluded)\n\nAll paths are anchored relative to the project root (`src` only matches `/src` and not `/test/src`). To exclude any directory or file named `src`, use `**/src` instead.\n\nBy default, ty excludes commonly ignored directories:\n\n- `**/.bzr/` - `**/.direnv/` - `**/.eggs/` - `**/.git/` - `**/.git-rewrite/` - `**/.hg/` - `**/.mypy_cache/` - `**/.nox/` - `**/.pants.d/` - `**/.pytype/` - `**/.ruff_cache/` - `**/.svn/` - `**/.tox/` - `**/.venv/` - `**/__pypackages__/` - `**/_build/` - `**/buck-out/` - `**/dist/` - `**/node_modules/` - `**/venv/`\n\nYou can override any default exclude by using a negated pattern. For example, to re-include `dist` use `exclude = [\"!dist\"]`", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "include": { + "description": "A list of files and directories to check. The `include` option follows a similar syntax to `.gitignore` but reversed: Including a file or directory will make it so that it (and its contents) are type checked.\n\n- `./src/` matches only a directory - `./src` matches both files and directories - `src` matches a file or directory named `src` - `*` matches any (possibly empty) sequence of characters (except `/`). - `**` matches zero or more path components. This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error. A sequence of more than two consecutive `*` characters is also invalid. - `?` matches any single character except `/` - `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode, so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.\n\nAll paths are anchored relative to the project root (`src` only matches `/src` and not `/test/src`).\n\n`exclude` takes precedence over `include`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "respect-ignore-files": { + "description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.", + "type": [ + "boolean", + "null" + ] + }, + "root": { + "description": "The root of the project, used for finding first-party modules.\n\nIf left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `.//` directory exists, include `.` and `./` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), it will also be included in the first party search path.", + "deprecated": true, + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "TerminalOptions": { + "type": "object", + "properties": { + "error-on-warning": { + "description": "Use exit code 1 if there are any warning-level diagnostics.\n\nDefaults to `false`.", + "type": [ + "boolean", + "null" + ] + }, + "output-format": { + "description": "The format to use for printing diagnostic messages.\n\nDefaults to `full`.", + "anyOf": [ + { + "$ref": "#/definitions/DiagnosticFormat" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file